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.
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.
{ "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)
et les gros fichiers (/uploads/…).
Les erreurs
Chaque réponse hors 2xx porte le même corps :
{ "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.
Le client
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 :
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 :
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.
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.