# Protocole

> Les quatre routes HTTP entre un serveur colis et ses clients, le format d’erreur, les codes, et comment monter les routes vous-même.

Canonical: https://colis-docs.vercel.app/docs/protocole · Markdown: https://colis-docs.vercel.app/docs/protocole.md

Un navigateur ne peut pas détenir vos clés S3 : il y a toujours un serveur entre le client et le
bucket. La forme de ce serveur, c’est ce protocole : quatre routes, un format d’erreur. Il est écrit
noir sur blanc pour qu’un client fonctionne contre n’importe quel serveur qui y répond, et c’est
pourquoi `colis envoyer --remote` fonctionne contre votre page de livraison.

## Les routes

Toutes sont relatives à leur point de montage, `/api/transfers` par défaut.

### `POST /`

Crée un colis et renvoie son code. Avec un `Content-Type` autre que JSON, le corps est le fichier ;
son nom voyage encodé en pourcentage dans `X-Colis-Filename`. Avec `Content-Type: application/json`,
le corps `{ data, version?, device? }` est un enregistrement JSON, rendu tel quel à la lecture.

```json title="201"
{ "code": "K7QP2M4X", "kind": "file", "createdAt": "…", "expiresAt": "…", "size": 1843200 }
```

Le serveur choisit le code, jamais le client : il l’écrit avec une écriture conditionnelle, et en
tire un nouveau en cas de collision plutôt que d’écraser un colis existant.

### `GET /:code`

Tout ce que l’on sait d’un colis : `code`, `kind`, `createdAt`, `expiresAt`, et pour un fichier
`filename`, `contentType` et `size`. Le contenu d’un fichier ne vient pas ici : il est sur `/raw`.

### `GET /:code/raw`

Les octets, avec le `Content-Type` stocké et un `Content-Disposition` qui porte le nom du fichier.
Avec `raw: 'redirect'`, le serveur répond `302` vers une URL présignée, pour que les octets ne
transitent pas par lui.

### `DELETE /:code`

Détruit le colis et répond `204`. Détruire un colis déjà disparu n’est pas une erreur.

La page de livraison ajoute ses propres routes à côté de celles-ci : l’accusé de réception
(`/:code/status`, `/:code/opened`, `/:code/verdict`, voir [Suivi et validation](https://colis-docs.vercel.app/docs/suivi-et-validation#les-routes))
et les [gros fichiers](https://colis-docs.vercel.app/docs/gros-fichiers) (`/uploads/…`).

## Les erreurs

Chaque réponse hors 2xx porte le même corps :

```json
{ "error": { "code": "NOT_FOUND", "message": "Unknown or expired code." } }
```

| `code`              | HTTP  | Ce qui s’est passé                                         |
| ------------------- | ----- | ---------------------------------------------------------- |
| `INVALID_REQUEST`   | `400` | Corps mal formé, ou méthode que la route ne sert pas       |
| `INVALID_SYNC_CODE` | `400` | Ce qui a été tapé ne peut pas être un code de cet alphabet |
| `UNAUTHORIZED`      | `401` | `authorize` a refusé                                       |
| `NOT_FOUND`         | `404` | Aucun colis sous ce code, ou il a expiré                   |
| `CODE_TAKEN`        | `409` | Aucun code libre trouvé ; hautement improbable à 40 bits   |
| `SNAPSHOT_TOO_NEW`  | `409` | Enregistrement écrit par un schéma plus récent             |
| `TOO_LARGE`         | `413` | Au-delà de `maxSize`                                       |
| `INTERNAL`          | `500` | Tout le reste                                              |

`NOT_FOUND` couvre exprès l’expiration comme l’absence : les distinguer permettrait de sonder quels
codes ont servi. La table est exportée sous le nom `TRANSFER_ERROR_STATUS`.

## Les codes

Un code est huit caractères en base32 de Crockford : `0123456789ABCDEFGHJKMNPQRSTVWXYZ`, soit
40 bits. Pas de `I`, `L`, `O` ni `U`, pour qu’un code survive au papier, au clavier d’un téléphone et
à un appel.

Chaque route normalise le code avant de chercher : séparateurs retirés, casse unifiée, et `O`, `I`,
`L` lus comme `0`, `1`, `1`. `k7qp-2m4x`, `K7QP 2M4X` et `K7QP2M4X` désignent le même colis. Ce qui a
été tapé reste tel quel côté client ; seule la recherche est corrigée.

Un code vaut accès : qui le détient peut prendre le fichier tant qu’il vit. Pour un fichier sensible,
donnez au colis une courte durée de vie, un mot de passe, ou la destruction au premier
téléchargement. La forme se règle avec `syncCode`, voir [Configuration](https://colis-docs.vercel.app/docs/configuration#la-forme-des-codes).

## Le client

```ts
import { createTransferClient } from '@colis/protocol'

const transfers = createTransferClient({
  baseUrl: 'https://votre-livraison.vercel.app/api/transfers',
  headers: { authorization: `Bearer ${process.env.DROP_PASSWORD}` },
})

const { code } = await transfers.createFile({ body: bytes, filename: 'maquette-v2.pdf' })
const colis = await transfers.read('k7qp-2m4x') // null si inconnu ou expiré
const contenu = await transfers.readBytes(code)
await transfers.remove(code)
```

`@colis/protocol` n’utilise que `fetch`. Son seul arbre de dépendances est nanoid, et aucun chemin ne
mène au SDK AWS : il s’embarque dans un navigateur, un worker ou une fonction sans client de
stockage. Les clients lèvent `TransferError`, qui porte le `code` ci-dessus :

```ts
import { isTransferError } from '@colis/protocol'

try {
  await transfers.createFile({ body, filename })
} catch (error) {
  if (isTransferError(error) && error.code === 'TOO_LARGE') {
    // proposer l’envoi depuis la page de livraison
  }
}
```

## Monter les routes

`createTransferHandler()` de `@colis/core` prend une `Request` et renvoie une `Response` : une route
Next.js, Hono, `Bun.serve`, Deno ou un worker, sans adaptateur. Dans une app Next.js, un fichier
suffit :

```ts title="app/api/transfers/[[...route]]/route.ts"
import { createBucket, createTransferHandler } from '@colis/core'

export const { GET, POST, DELETE } = createTransferHandler({
  bucket: createBucket({ bucket: process.env.COLIS_BUCKET!, prefix: 'drop', maxSize: 4 * 1024 * 1024 }),
  expiresIn: 24 * 3600,
  authorize: (request) =>
    request.method !== 'POST' || request.headers.get('authorization') === `Bearer ${process.env.DROP_PASSWORD}`,
})
```

Le segment attrape-tout compte : le gestionnaire sert `/`, `/:code` et `/:code/raw`.

| Option       | Rôle                                                                                                                                    |
| ------------ | --------------------------------------------------------------------------------------------------------------------------------------- |
| `bucket`     | Un `Bucket`. Donnez-lui un `prefix` propre aux colis.                                                                                   |
| `basePath`   | Le point de montage, `/api/transfers` par défaut. À changer si vous montez ailleurs.                                                    |
| `expiresIn`  | Durée de vie en secondes, une heure par défaut ; `null` pour ne jamais expirer.                                                         |
| `authorize`  | Appelé avant tout. `false` pour un `401` simple, ou une `Response` pour répondre vous-même. Sans lui, toutes les routes sont publiques. |
| `raw`        | `'stream'` fait passer les octets par le serveur ; `'redirect'` répond `302` vers une URL présignée.                                    |
| `app`        | Une étiquette enregistrée sur chaque enregistrement JSON.                                                                               |
| `maxVersion` | Le schéma le plus récent que ce déploiement comprend, pour les enregistrements JSON.                                                    |

C’est exactement ce que fait la page de livraison, avec devant ces routes ce qui lui est propre :
les options `x-drop-*`, le mot de passe d’un colis, l’usage unique, l’accusé et les webhooks. Si vous
n’avez besoin que du transfert, ce fichier suffit ; pour livrer à un client, déployez plutôt la
[page de livraison](https://colis-docs.vercel.app/docs/page-de-livraison).

**Depuis une autre origine**, la requête préalable CORS est à votre charge : le gestionnaire ne parle
que le protocole. Autorisez `content-type`, `authorization` et `x-colis-filename`, sans quoi les
envois de fichiers échouent.

## L’expiration est vérifiée à la lecture

Un colis passé son `expiresAt` n’est jamais remis, mais l’objet reste dans le bucket : le supprimer
est le travail d’une règle de cycle de vie, que colis ne crée pas pour vous. `colis verifier` vérifie
qu’elle existe.
