# La bibliothèque

> Le paquet Node de colis, la pièce sous la page de livraison : une API fichiers sur votre bucket, les routes de transfert en un seul fichier de route, de petits enregistrements avec expiration, des écritures conditionnelles et des codes d’erreur stables.

Canonical: https://colis-site.vercel.app/library · Markdown: https://colis-site.vercel.app/library.md · English: https://colis-site.vercel.app/en/library

Le paquet sur lequel repose la page de livraison : une API fichiers sur votre bucket, les routes de transfert en un seul fichier, et de petits enregistrements à côté de chaque fichier. Le seul paquet qui détient des identifiants, donc le seul qui tourne sur votre serveur.

Pas encore sur npm : les paquets se construisent depuis [le dépôt](https://github.com/mamadouwhile/colis) en attendant leur publication.

```ts
import { createBucket, createTransferHandler } from '@colis/core'

const store = createBucket({ bucket: 'livraisons' })

// The transfer routes on your own domain, in one route file.
export const { GET, POST, DELETE } = createTransferHandler({
  bucket: store,
  expiresIn: 7 * 24 * 3600,
  authorize: (request) => request.headers.get('authorization') === `Bearer ${process.env.TOKEN}`,
})
```

## Cinq verbes sur votre bucket.
Chaînes, buffers, Blobs et flux sont acceptés. Les clés font l’aller-retour : ce que renvoie upload() se redonne tel quel à get(), getUrl() et delete().

```ts
await store.upload(file)     // → { key, path, url?, size?, etag?, contentType }
await store.put(id, file)    // one file per identifier: create or replace
await store.get(id)          // → the file back, or null
await store.getUrl(id)       // → public or presigned URL
await store.delete(id)       // → void

await store.put(code, file, { ifAbsent: true }) // claim a code, never overwrite one
store.client                 // the plain S3Client, for anything else
```

`getUrl()` renvoie une URL présignée par défaut, ou une URL non signée quand une `publicUrl` est configurée, avec une option `download` qui fixe le nom sous lequel le navigateur enregistre.

`ifAbsent` n’écrit que si rien n’existe encore sous la clé : c’est ainsi qu’un nouveau code est réservé sans piétiner un code en service. Un flux demande une `contentLength`, et `maxSize` refuse un corps trop gros avant que quoi que ce soit ne parte sur le réseau.

Tout ce que le paquet n’enveloppe pas est à une commande près via `store.client`, le `S3Client` brut.

## Les routes de transfert, en un fichier de route.
createTransferHandler() sert le protocole à quatre routes : créer, lire, télécharger, supprimer. Une Request en entrée, une Response en sortie : route Next, route Hono, Bun.serve ou worker, sans adaptateur.

**Next.js App Router**
```ts
// app/api/transfers/[[...route]]/route.ts
import { createBucket, createTransferHandler } from '@colis/core'

export const { GET, POST, DELETE } = createTransferHandler({
  bucket: createBucket({ bucket: 'livraisons' }),
  expiresIn: 7 * 24 * 3600,
  raw: 'redirect', // downloads 302 to a presigned URL
  authorize: (request) => request.headers.get('authorization') === `Bearer ${process.env.TOKEN}`,
})
```
**Hono**
```ts
import { Hono } from 'hono'
import { createBucket, createTransferHandler } from '@colis/core'

const transfers = createTransferHandler({ bucket: createBucket(), basePath: '/api/transfers' })

const app = new Hono()
app.all('/api/transfers', (c) => transfers(c.req.raw))
app.all('/api/transfers/*', (c) => transfers(c.req.raw))
```
**Bun.serve**
```ts
import { createBucket, createTransferHandler } from '@colis/core'

const transfers = createTransferHandler({ bucket: createBucket(), basePath: '/api/transfers' })

Bun.serve({
  fetch(request) {
    if (new URL(request.url).pathname.startsWith('/api/transfers')) return transfers(request)
    return new Response('Not found', { status: 404 })
  },
})
```

Toutes les routes sont publiques tant que vous ne passez pas `authorize`. Renvoyez `false` pour un simple 401, ou votre propre `Response`. Avec `raw: 'redirect'`, un téléchargement répond 302 vers une URL présignée, et les octets ne transitent pas deux fois par votre serveur. La page de livraison place devant ce handler son mot de passe, son accusé de réception et ses webhooks.

## Un petit JSON à côté du fichier, avec une expiration.
putSnapshot() range une valeur dans une enveloppe auto-descriptive, compressée, avec le nom de votre application, une version de schéma et une expiration. La page de livraison tient ainsi ses accusés de réception.

```ts
// How the delivery page keeps a client's answer beside the transfer.
await store.putSnapshot(`meta/${code}.verdict`, { decision: 'approved', at }, {
  app: 'drop',
  expiresIn: 7 * 24 * 3600,
  ifAbsent: true, // one answer, never overwritten
})

const verdict = await store.getSnapshot(`meta/${code}.verdict`)
verdict?.data // → the record, or null when unknown or expired
```

- **null une fois expiré** — Un enregistrement expiré n’est jamais renvoyé, même si l’objet est encore dans le bucket.
- **La première écriture gagne** — Avec `ifAbsent`, une seconde écriture sous la même clé échoue en `PRECONDITION_FAILED` au lieu de remplacer la première : c’est ce qui rend la réponse d’un client définitive.

## Tout lève une ColisError avec un code stable.
Les échecs détectables localement, un code invalide, un corps trop gros, des données non sérialisables, sont levés avant que quoi que ce soit ne parte sur le réseau.

```ts
import { isColisError } from '@colis/core'

try {
  await store.upload(body, { filename })
} catch (error) {
  if (isColisError(error) && error.code === 'FILE_TOO_LARGE') {
    return Response.json({ error: 'Too large to send in one piece' }, { status: 413 })
  }
  throw error
}
```

- `INVALID_SYNC_CODE`: Vide, ou des caractères hors de l’alphabet
- `FILE_TOO_LARGE`: Corps au-delà du maxSize configuré
- `PRECONDITION_FAILED`: Une écriture ifMatch ou ifAbsent a perdu la course
- `SNAPSHOT_TOO_NEW`: Version de schéma au-delà du maxVersion donné
- `INVALID_KEY / INVALID_BODY`: Une clé ou un type de corps que le bucket n’accepte pas
- `UPLOAD_FAILED / GET_FAILED / …`: S3 a refusé la requête ; l’erreur d’origine est dans cause

## Chaque option, et la variable d’environnement derrière.
createBucket() sans argument fonctionne dès que COLIS_BUCKET et les variables AWS habituelles sont définies. Avec un endpoint, la région passe à auto et l’adressage par chemin s’active, ce qu’attendent R2, MinIO et Scaleway.

```ts
createBucket({
  bucket: 'livraisons',         // or COLIS_BUCKET / S3_BUCKET
  region: 'eu-west-3',          // or COLIS_REGION / AWS_REGION
  credentials: { … },           // omit for the AWS provider chain
  endpoint: 'https://…',        // R2, MinIO, Scaleway, Wasabi — or COLIS_ENDPOINT
  prefix: 'drop',               // internal namespace
  maxSize: 4 * 1024 * 1024,     // reject before any network call
  syncCode: { length: 8 },      // the shape of store.codes
})
```

Via votre serveur, un envoi est limité par la taille de requête de votre runtime : 4,5 Mo sur les fonctions Vercel. Réglez `maxSize` juste en dessous, et un envoi trop gros coûte une comparaison au lieu d’une requête tronquée ; la page de livraison envoie tout ce qui dépasse directement au bucket.

`createBucket()` ne coûte rien : le client sous-jacent se construit à la première requête, l’appeler au niveau du module ne pose aucun problème.

