Références et téléversements
title: Références et téléversements description: Ce qu’un membre téléverse pour générer à partir de lui : références, téléversements sans débit, image à partir de références et CSP.
Audit de la phase 3, G1-03, G1-04, G1-22 ; portage de studio-references.ts
et safe-fetch.ts de la plateforme, corrigé. Une référence, c'est ce qui
fait arriver le vrai produit jusqu'au modèle (« ce que tu génères n'a rien à
voir avec mon vrai produit », le constat du fondateur qui a fait naître le
module sur la plateforme). Elle est résolue pour un scope (un produit du
registre, ou libre) par resolveReferences
(packages/engine/src/generation/references.ts) :
| Référence | Admise quand | Lue par |
|---|---|---|
un asset, par id (referenceAssetIds) | StudioAsset du même scope, dont le fichier est un objet de ce store | chemin, dans le store |
un téléversement, par id (referenceIds) | StudioReference du même scope, de type image | chemin, dans le store |
une URL (referenceUrls) sur le store du studio | hôte exact du store, chemin sous le préfixe du scope | chemin, dans le store (jamais une requête) |
une URL cdn.shopify.com | https, sans identifiants ni port | safeFetch |
| une URL d'une boutique déclarée | hôte exact listé dans references.shopDomains du produit au registre (vide partout aujourd'hui) | safeFetch |
Tout le reste est refusé, avant la ligne de journal et avant toute
réserve : une référence refusée ne débite rien et ne laisse pas de ligne. Le
refus est reference-refused, avec son motif (ReferenceReason : too-many,
duplicate, malformed, not-https, userinfo, port,
host-not-allowed, other-scope, not-found, not-an-image, too-large,
unreadable, redirect-refused) et la référence en cause ; la page lit la
phrase française de fr.generation.referenceReasons. Une seule référence en
défaut refuse toute la demande : « j'ai utilisé deux de tes trois
références » est un résultat que personne ne peut vérifier.
Ce que le portage a corrigé :
- Un seul store. La plateforme admettait tout
*.public.blob.vercel-storage.comet ne regardait que le chemin : l'objet d'un autre store dont le chemin imitait le bon tenant passait. Ici l'hôte doit être celui du store du studio, et l'objet est lu par chemin. - Le type vient des octets (
sniffImageType: PNG, JPEG, WebP, GIF), jamais ducontent-typede la réponse ni du nom de fichier. - Le plafond est compté sur le flux : 8 Mo (
MAX_REFERENCE_BYTES), la lecture s'arrête et annule le reste au premier octet de trop ; uncontent-lengthdéclaré plus grand est refusé avant de lire. - Chaque redirection est revérifiée (
safeFetch, trois sauts au plus) contre la même liste : un saut vers un autre hôte, le store du studio compris, est refusé sans être demandé. https seulement, jamais d'identifiants dans l'URL (https://cdn.shopify.com@ailleurs/…est refusé), jamais de port. - Six au plus (
MAX_REFERENCE_IMAGES, le plafond « haute fidélité » documenté de Gemini 3 Pro Image), une seule pour une image de départ (MAX_START_FRAME_REFERENCES, lot suivant). Lues en parallèle, chacune sous le budgetreferenceFetch(20 s, tous sauts et corps compris).
Image à partir de références. Sur la route « langage » (Nano Banana),
l'instruction et les images partent dans un seul message utilisateur, le
texte d'abord, puis une partie file par image, dans l'ordre demandé
(assets, téléversements, URL), en octets (jamais une URL que la Gateway
irait chercher). Sur la route « image » (GPT Image, lot suivant), elles sont
les images du prompt. La ligne garde references: { assets, uploads, urls }
et params.referenceCount. Le prix d'une image reste son forfait ; le coût que
la Gateway rend, s'il diffère (des jetons d'entrée en plus), le remplace
(section 7.1).
Téléversements, sans débit. Rien n'appelle un fournisseur, rien ne bouge
d'argent, chaque enregistrement est audité reference.uploaded.
| Chemin | Pour | Règles |
|---|---|---|
uploadReference(productId, form) (action serveur) | une image | requireMember puis generate ; 4 Mo au plus (sous les 4,5 Mo d'une requête de fonction Vercel ; serverActions.bodySizeLimit à 4,5 Mo), PNG, JPEG, WebP ou GIF par ses octets ; clé <scope>references/<id>/<nom>.<ext> ; produit prévu refusé |
POST /api/references/upload (@vercel/blob/client, handleUpload) | un rush ou une piste (mp4, mov, webm, mp3, wav, m4a) | la demande de jeton relit le membre (requireMemberApi("generate")), lit le scope dans clientPayload ({ productId }), exige une clé sous <scope>references/<id>/, n'autorise que le type de l'extension, 50 Mo, un suffixe aléatoire, jamais d'écrasement, 15 minutes ; le jeton porte le membre et le scope |
| rappel de fin du store (même route) | l'enregistrement | pas de session : la porte est la signature HMAC-SHA256 du store (x-vercel-signature, sous le jeton lecture-écriture), vérifiée avant tout ; enregistre pour le membre et le scope du jeton |
confirmReferenceUpload({ productId, url }) (action serveur) | un poste sans URL publique, que le rappel n'atteint pas | l'URL ne fait que nommer l'objet : elle doit être du store du studio et sous le dossier references/ du scope ; rien n'est téléchargé |
Le rappel et la confirmation peuvent arriver tous les deux, dans n'importe quel
ordre : l'enregistrement est idempotent sur le chemin. Ce qui est inscrit
est ce que le store dit de l'objet (head : taille, type), jamais ce que le
navigateur a annoncé. La durée d'un rush ou d'une piste (mp4, QuickTime,
m4a) est mesurée dans ses octets stockés (storedMp4DurationMs : une plage
de tête de 256 Kio, puis la fin sur 1 Mio si le moov est écrit en dernier),
par chemin dans le store : une URL fournie par un appelant n'est jamais
téléchargée pour mesurer un clip (G1-22). WebM, MP3 et WAV n'ont pas de durée
lisible ainsi : durationMs reste nul.
La CSP laisse le cockpit montrer et téléverser (lib/security/headers.ts) :
l'hôte exact du store dans img-src et media-src, et le point d'envoi du
SDK (https://vercel.com/api/blob/, préfixe de chemin) dans connect-src.
Sans store configuré, ni l'un ni l'autre. Le proxy laisse passer exactement
^/api/references/upload$ (le rappel du store n'a pas de session) ;
every-door-checks.test.ts tient cette route pour une double porte : la
branche du store s'ouvre sur la signature, celle du cockpit sur le membre, et
rien ne change d'état avant sa porte.