Sous l’étiquette
Un fichier dans votre bucket, une réponse de votre client.
colis est volontairement petit. Cette page en fait le tour : ce qu’est un colis, ce que note l’accusé de réception, comment voyagent les gros fichiers, et le protocole que partagent la page, la CLI et les automatisations.
Vous. Votre bucket. Votre client. Rien d’autre.
● une livraison entière · 14 s · en boucle
- 01 · envoi
Vous envoyez
Le fichier arrive dans votre bucket en un seul objet, sous un code tout neuf de huit caractères, avec son expiration inscrite sur l’objet et un accusé de réception à côté.
- 02 · le code
Le code change de mains
Un lien dans un e-mail, huit caractères dictés au téléphone, un QR code sur un écran. Aucun compte, d’un côté comme de l’autre.
- 03 · validation
Votre client répond
Il tape le code, fautes réparées, prévisualise le fichier et le valide. L’accusé le note, et le webhook part.
01L’accusé de réception
Envoyé, ouvert, validé ou à corriger.
Chaque colis a son accusé de réception, rangé à côté de lui dans votre bucket : quatre états, chacun avec son heure, et une réponse qui ne change plus.
- envoyé
Écrit à la création du colis, avec l’empreinte de votre jeton d’expéditeur. Ce jeton permet de distinguer vos propres visites de celles de votre client.
- ouvert
La première fois que quelqu’un d’autre que vous ouvre la page de retrait, télécharge le fichier ou répond. La première fois seulement.
- validé
Le client a cliqué sur valider. Définitif : une seconde réponse est refusée.
- à corriger
Le client demande des corrections et dit lesquelles, en 1 000 caractères au plus. Définitif aussi : la version suivante est un nouveau colis.
$ colis statut K7QP2M4X
à corriger
envoyé 2026-09-22 10:00
ouvert 2026-09-22 11:30
à corriger 2026-09-22 12:45
comment Le logo en SVG, et le fond plus clair.
$ colis statut K7QP2M4X --json # the whole receipt, for a scriptL’accusé survit au fichier : il répond encore après qu’un téléchargement unique a supprimé le code, aussi longtemps que la plus longue durée de vie du déploiement. Votre page d’envoi le suit toute seule, colis statut l’affiche, et chaque changement est un webhook.
02Un colis
Un objet, un code, une durée de vie.
Le fichier est stocké tel quel, avec son nom, son type et son expiration dans les métadonnées de l’objet. Tout ce que la page sait d’autre tient dans de petits enregistrements à côté, sous `meta/`, hors de portée des routes de transfert.
drop/K7QP2M4X # the file
Content-Type: application/pdf
Content-Disposition: attachment; filename="maquette-v2.pdf"
x-amz-meta-colis-kind: file
x-amz-meta-colis-expires-at: 2026-09-23T10:00:00.000Z
Body: the bytes, untouched
drop/meta/K7QP2M4X # note, device, one-time flag, password hash
drop/meta/K7QP2M4X.delivery # sent, and the sender's token hash
drop/meta/K7QP2M4X.opened # the first time the client opened it
drop/meta/K7QP2M4X.verdict # approved, or changes and the commentUn colis expiré n’est jamais servi, même si l’objet est encore dans le bucket. Un code expiré répond le même NOT_FOUND qu’un code qui n’a jamais existé.
Supprimer l’objet, c’est le travail de votre bucket, via une règle de cycle de vie sur le préfixe. colis verifier contrôle qu’elle existe.
Le mot de passe d’un colis est haché avec scrypt, avec son propre sel. Seule l’empreinte est stockée : personne ne peut le relire ni le réinitialiser.
03Gros fichiers
Du navigateur à votre bucket, sans détour.
Au-delà de `DROP_MAX_SIZE_MB`, rien ne passe par la fonction. Le serveur signe des URL et assemble ; le navigateur envoie les morceaux lui-même.
POST /api/transfers/uploads # a fresh code, a multipart upload
→ { code, uploadId, partSize, partCount } # parts of 8 MiB
POST /api/transfers/uploads/:code/parts # presigned PUT URLs, 50 at most
PUT <the bucket> # the browser, 4 parts at a time
POST /api/transfers/uploads/:code/complete # assembled, checked, then 201
DELETE /api/transfers/uploads/:code # abandonedLes morceaux font 8 Mio, davantage quand un fichier en demanderait plus de 10 000. Le navigateur en envoie quatre à la fois et retente trois fois un morceau qui échoue.
Il se souvient aussi de l’envoi. Redéposez le même fichier après une coupure ou un onglet fermé : il demande au bucket quels morceaux sont déjà là et envoie le reste.
Le bucket a besoin d’une règle CORS qui autorise PUT depuis votre page et expose ETag. Sans elle, les petits fichiers fonctionnent toujours, et un gros échoue avec un message qui dit pourquoi.
04Le code
Quarante bits qui survivent à un appel.
Le code, c’est ce que manipule votre client. Il s’affiche sur votre écran et il le tape sur le sien : tout en découle.
Base32 de Crockford
Ni I, ni L, ni O, ni U. Les trois premiers se confondent à la lecture ; retirer le quatrième évite qu’un code aléatoire forme un mot malheureux.
Réparé au retour
Séparateurs retirés, casse unifiée, O lu comme zéro. La réparation se fait dans le navigateur, avant toute requête, et ce que le client a tapé reste tel quel.
Réservé par écriture conditionnelle
Le serveur choisit le code et écrit avec ifAbsent : une collision échoue franchement et réessaie avec un nouveau code au lieu d’écraser le colis de quelqu’un d’autre.
05Le protocole
Quatre routes, un format d’erreur. Écrits noir sur blanc.
Un navigateur ne peut pas détenir vos identifiants S3 : un serveur se place entre les deux. La page de livraison est ce serveur, et sa forme est un protocole, pas un détail d’implémentation.
POST / # create a transfer, get the code back
GET /:code # metadata
GET /:code/raw # the bytes, or a 302 to a presigned URL
DELETE /:code # burn it
# what the delivery page adds
GET /:code/status # sent, opened, approved or changes
POST /:code/opened # the client opened it
POST /:code/verdict # { "decision": "approved" }, or "changes" and a comment
# every error, same shape
{ "error": { "code": "NOT_FOUND", "message": "Unknown or expired code." } }import { createTransferClient } from '@colis/protocol'
const transfers = createTransferClient({ baseUrl: '/api/transfers' })
const { code } = await transfers.createFile({ body: file, filename: file.name })
const meta = await transfers.read(typed) // null when unknown or expired
const bytes = await transfers.readBytes(code) // the file back
await transfers.remove(code)La CLI pointée vers --remote parle les mêmes quatre routes : c’est pourquoi colis envoyer fonctionne avec votre page de livraison et un jeton, sans clés S3.
La page de livraison ajoute trois routes à elle pour l’accusé de réception. Un client qui n’en connaît aucune fonctionne quand même : ce qu’il crée est un colis comme les autres, avec les réglages par défaut du déploiement.
Le protocole de transfert, route par route06Les paquets
Une seule contrainte décide du découpage.
Un navigateur ne doit jamais se retrouver avec un client de stockage dans ses dépendances. Le paquet protocole est ce que les deux moitiés partagent.
@mdemb/colis
core, protocolenvoyer, recevoir, statut, annuler, verifier, init et config. Une seule implémentation, le client du protocole, branché sur fetch ou directement sur le handler.
@colis/core
aws-sdk, protocolFichiers, enregistrements, écritures conditionnelles et handler de transfert. Le seul paquet qui détient des identifiants.
@colis/protocol
nanoidLe format d’échange, un client fondé sur fetch, les codes, et la signature des webhooks : signWebhook et verifyWebhook.
@colis/react
protocol, react (peer)Envoyer, recevoir, et un champ de code. Aucun chemin de votre bundle ne mène au SDK AWS.
n8n-nodes-colis
protocol, n8n-workflow (peer)Un déclencheur qui vérifie la signature, et un nœud d’action qui envoie, lit et supprime des colis.
- Runtime serveur
- Node 20 ou plus, ce qu’exige le SDK AWS v3
- Page de livraison
- Next.js, déployée sur Vercel depuis le monorepo
- Paquets navigateur
- fetch, et rien d’autre
- Sur npm
- Pas encore : construits depuis le dépôt
Vous envoyez. Ils valident. Vous le savez.
Votre stockage, une page de livraison à votre nom, un webhook à chaque étape. Rien d’hébergé chez un tiers.