Médias privés
Un fichier privé n'a pas d'URL publique. Comment fonctionne la visibilité, comment un jeton d'asset autorise la signature, et comment votre backend remet au lecteur une URL qui expire en cinq minutes.
Un fichier privé vit dans un autre bucket - un bucket lié à aucun nom d'hôte public. Aucune URL n'en sert les octets au monde. La seule façon de le lire est une URL signée à durée brève, que votre backend demande à cmssy de forger, pour un lecteur dont votre backend a décidé qu'il y a droit.
Ce partage est tout le principe. cmssy décide si l'appelant peut signer ; vous décidez qui est le lecteur. cmssy n'apprend jamais qui sont vos utilisateurs finaux, et n'en a pas besoin.
Rendre un fichier privé
Envoyez-le en privé, ou basculez un fichier existant depuis la médiathèque. La bascule copie l'objet dans l'autre bucket, vérifie que la copie est arrivée entière, met à jour la bibliothèque, et seulement ensuite retire l'ancienne copie - dans cet ordre, pour qu'un échec quelque part laisse le fichier lisible plutôt qu'échoué.
Si seule la dernière étape échoue, vous obtenez :
The file is now private, but the old public copy could not be removed. Run this again to clear it.Relancer l'opération est sans danger, et c'est exactement ce qui nettoie le reliquat.
Deux choses à savoir avant de basculer un fichier déjà posé sur une page :
- L'URL publique cesse de fonctionner. Tout ce qui la détient - une page en cache, un signet, un autre site qui la pointe - casse immédiatement.
- La livraison se met à renvoyer
url: null. Un bloc qui rend son image sans condition rendra une image cassée.
Ce que renvoie la livraison
{
"id": "6a6495e44d1ee7dedcae1f52",
"url": null,
"visibility": "private",
"alt": "Couverture du rapport trimestriel",
"width": 1600,
"height": 900
}Tout sauf les octets passe encore : l'identifiant, les dimensions, le texte alternatif. C'est ce qui rend la dégradation élégante possible - vous savez qu'une image existe, vous connaissez sa forme, et vous savez que vous n'y avez pas droit ici.
const src = mediaUrl(content.src);
if (!src) return null;Une transformation sur une référence privée ne fait rien, puisqu'il n'y a pas d'URL à transformer.
La génération de pages par IA n'offre jamais non plus un fichier privé au modèle. Son adresse stockée pointe vers un bucket sans hôte public : la proposer reviendrait seulement à faire écrire au modèle une URL morte dans la page.
Jetons d'asset
La signature est autorisée par un jeton d'asset : une habilitation qui appartient à l'espace de travail et non à une personne, et qui fait exactement une chose - forger des URL signées pour les fichiers privés de cet espace.
Ce n'est délibérément pas un jeton d'API. Un jeton d'API authentifie un utilisateur, et tout ce que cet utilisateur peut faire en découle ; en confier un au serveur d'un consommateur accorderait l'ensemble pour permettre une seule opération.
Créez-en un dans Paramètres → Headless, ou via GraphQL :
mutation {
assetToken {
create(name: "storefront", expiresAt: "2027-01-01T00:00:00Z") {
id
prefix
token
}
}
}Le token est montré une fois et jamais plus - cmssy n'en garde qu'un hash. Ce que vous voyez ensuite est le prefix : csa_ suivi de sept caractères, assez pour distinguer deux jetons au moment de choisir lequel révoquer. expiresAt est facultatif et mérite d'être rempli.
assetToken { list } les renvoie avec lastUsedAt : un jeton inutilisé depuis des mois se repère d'un coup d'œil. assetToken { delete(id: "...") } en révoque un immédiatement. La création comme la révocation sont écrites dans le journal d'audit, au nom de la personne qui les a faites.
Côté serveur uniquement. Dans un bundle navigateur, ce jeton accorde la signature de tous les fichiers privés de l'espace de travail. Sa place est une variable d'environnement serveur - jamais NEXT_PUBLIC_, jamais un composant client, jamais une constante inlinée au build.
Forger une URL signée
POST https://api.cmssy.io/media/{assetId}/sign
Authorization: Bearer csa_...{
"url": "https://...",
"expiresAt": "2026-08-09T18:35:00.000Z"
}L'URL vaut cinq minutes. Traitez-la par requête : forgez-la quand un lecteur demande la page, remettez-la, laissez-la expirer.
Un asset public répond au même appel par son URL CDN et expiresAt: null : un consommateur n'a donc jamais à se ramifier sur la visibilité avant de demander.
Les échecs méritent une lecture précise :
401- pas de jeton, ou jeton invalide ou expiré.404- aucun asset de ce genre dans l'espace de travail de ce jeton. Un fichier appartenant à quelqu'un d'autre répond 404 plutôt que 403, et c'est voulu : un 403 confirmerait que l'identifiant existe, ce qui ferait de l'endpoint un oracle de consultation pour les autres locataires.429- 120 signatures par minute et par jeton, 300 par minute et par adresse appelante. La limite par adresse est débitée avant même d'examiner le jeton, car le valider coûte une comparaison bcrypt - et ce n'est pas quelque chose qu'un inconnu devrait pouvoir dépenser à votre place.
Chaque signature est comptée sur l'espace de travail. Aucun plan ne la plafonne aujourd'hui ; elle est mesurée pour être visible.
Le câblage
Mettez le jeton dans votre backend et une route devant. C'est dans la route que vit l'habilitation - un niveau d'adhésion, un achat, un abonnement - exactement comme pour tout ce que vous protégez déjà :
// app/api/asset/[id]/route.ts
export async function GET(request: Request, { params }) {
const session = await auth();
if (!session?.user) return new Response("Unauthorized", { status: 401 });
const { id } = await params;
const signed = await fetch(`${process.env.CMSSY_API_HOST}/media/${id}/sign`, {
method: "POST",
headers: { authorization: `Bearer ${process.env.CMSSY_ASSET_TOKEN}` },
});
if (!signed.ok) return new Response("Not found", { status: 404 });
const { url } = await signed.json();
return Response.redirect(url, 307);
}Redirigez plutôt que de renvoyer l'URL. Un lien signé dans votre HTML y reste pendant les cinq minutes où il est valide - dans un cache CDN, dans une source de page partagée, dans le code source affiché par quelqu'un.
Attention à l'hôte : CMSSY_API_URL pointe vers l'endpoint GraphQL, alors que la signature se trouve un niveau au-dessus. Gardez l'hôte de l'API dans sa propre variable plutôt que de charcuter l'autre chaîne.
Étapes suivantes
- Médias - la bibliothèque, le champ média, les transformations et la suppression.
- Schéma de bloc & types de champs -
fields.mediaparmi les autres.