ADR-0007 — La famille de cles sk_ est retiree, pas reparee
lib/security/api-keys.ts exposait un ApiKeyManager qui frappait trois formats de credential — sk_user_, sk_org_, sk_proj_ — et les gardait dans un Map au niveau du module. C'est tout le stockage qu'il y avait : aucune…
Statut
Accepté · 2026-09-05
Piliers : security-identity, ai-platform, integrations, platform-ops, billing
Contexte
lib/security/api-keys.ts exposait un ApiKeyManager qui frappait trois
formats de credential — sk_user_, sk_org_, sk_proj_ — et les gardait
dans un Map au niveau du module. C'est tout le stockage qu'il y avait :
aucune table, aucune colonne, aucun hash en base.
Sur Vercel, un Map de module vit dans une instance de fonction. Il ne
survit pas a un cold start et n'est pas partage entre instances. La
consequence n'est pas « les cles expirent trop vite » : c'est que la cle
rendue a l'utilisateur par POST /api/keys n'etait deja plus valide au
moment ou il la collait, sauf a retomber par hasard sur l'instance chaude
qui venait de la frapper. Le systeme n'a donc jamais fonctionne, pas une
seule fois, depuis le jour ou il a ete ecrit.
Ce qui en dependait :
| Surface | Ce qu'elle faisait reellement |
|---|---|
POST/GET/DELETE /api/keys, /api/keys/[keyId], /api/keys/validate | frapper et lister des cles qui s'evaporaient |
POST /api/channels/api | un canal de chat REST facture, joignable par personne |
POST /api/mcp/usage | un rapport d'usage derriere withApiAuth |
PATCH/DELETE /api/registry/skills/[slug] | 401 pour tout le monde, y compris l'auteur |
lib/security/auth-middleware.ts (withApiAuth) | l'unique consommateur du Map |
Store.apiKey, Organization.apiKey, User.apiKey (types), le champ « API key » de l'org-switcher | un secret affichable qu'aucune ligne de code n'a jamais ecrit |
Trois credentials fonctionnels existent a cote, chacun avec son
resolveur et son hash en base : bst_ (lib/security/api-token.ts,
ApiToken.tokenHash), bei_ (lib/security/intelligence-api-key.ts,
IntelligenceApiKey.keyHash) et le bearer MCP par store
(app/api/mcp/[storeId]/auth). Aucun ne passait par le Map.
Décision
On supprime la famille sk_ et tout ce qui la sert, plutot que de lui
donner une table. Les credentials programmables supportes sont bst_,
bei_ et le bearer MCP par store.
La seule surface qui gagne du comportement est
PATCH/DELETE /api/registry/skills/[slug], qui passe a
withSessionAuth : le controle d'auteur qu'elle portait n'avait jamais
pu s'executer.
Alternatives écartées
| Option | Pourquoi non |
|---|---|
Persister le Map dans une table ApiKey | Ca revient a construire un quatrieme systeme de credentials la ou trois existent deja et fonctionnent. bst_ couvre exactement le besoin (jeton porteur, scope par org, hash en base, revocable) ; le seul ecart etait le prefixe. |
Persister le Map dans Redis | Meme objection, plus une dependance de disponibilite sur le chemin d'authentification : Upstash indisponible = plus personne ne s'authentifie. La base est deja sur ce chemin. |
Garder /api/channels/api en le rebranchant sur bst_ | Un canal de chat REST est un produit, pas un refactor. Il facture des credits, choisit un modele, gere le streaming SSE : le rebrancher aurait signifie mettre en production, sans specification ni test d'integration, un chemin de facturation que personne n'a jamais appele. Ce que couterait une vraie surface REST est ecrit dans backlog/security-identity/0353. |
| Ne rien supprimer et documenter que c'est casse | Une route qui repond 401 a tout le monde depuis toujours n'est pas de la documentation, c'est du bruit dans la surface d'attaque et dans le compte de handlers. |
Deposer la colonne Store.apiKey dans la meme PR | Le schema guard est additif par construction : il ne supprime rien. Un DROP COLUMN est une action operateur deliberee, pas un effet de bord de deploiement. Suivi dans backlog/data-platform/0354. |
Conséquences
Ce que ca coute :
- Aucune API REST tierce ne peut piloter le chat aujourd'hui. C'etait deja le cas — la difference est que ca ne se pretend plus.
- Le compte de handlers d'API passe de 371 a 366, et
docs:claimsle verifie. reserveCreditsForChannelperd un appelant sur cinq. Les quatre portes restantes sont enumerees danssrc/test/ai-use-every-door.test.ts, et cette liste vient de gagner un contre-controle derive : reintroduire un canal facture sans l'y inscrire passe au rouge.- La colonne
Store.apiKeyreste en base, vide, jusqu'a une migration operateur. Les trois champsapiKeydes types clients (Store,Organization,User) et le champ « API key » de l'org-switcher, eux, sont supprimes : ils affichaient un secret que rien n'ecrivait. FeatureGate(src/modules/billing/feature-gating.ts) perd son dernier appelant de production :withApiAuthetait le seul, a l'etape 4 de sa chaine (new FeatureGate(subscription)puiscanAccess). Le module et ses 12 tests restent, sans consommateur — suivi dansbacklog/billing/0355. Suite (2026-09-06) : ce fichier n'existe plus, il a ete supprime. Chacun de ses exports a ete verifie symbole par symbole — zero appelant de production pour les huit — donc la decision de cet ADR est allee jusqu'au bout au lieu de laisser un vocabulaire de gating que personne n'applique. La verite du gating par plan vit desormais danssrc/services/billing/entitlement.tsetsrc/types/billing-plans.ts, etsrc/test/billing-gating-single-source.test.tsempeche la recidive.withApiAuthdisparait de trois gardes derivees (api-authorization-coverage,tenant-query-param-coverage,ai-use-every-door). Reduire une liste d'autorisateurs est l'edit qu'un relecteur doit regarder en premier : c'est pourquoi la troisieme a gagne un contre-controle derive dans la meme PR.
Le signal qui indiquerait qu'il faut revisiter : une demande client reelle
d'API REST de chat. La reponse sera alors backlog/security-identity/0353
et le prefixe bst_, jamais un nouveau sk_.
Comment c'est appliqué
Trois gardes derivees, pas une convention :
| Garde | Ce qu'elle refuse |
|---|---|
src/test/credential-minting.test.ts | qu'un litteral sk_user_ / sk_org_ / sk_proj_ reapparaisse dans du code, et qu'un second frappeur de jetons existe a cote de lib/security/api-token.ts |
src/test/credential-storage.test.ts | qu'un resolveur de credential de lib/security/ detienne un Map au niveau du module au lieu de lire la base |
src/test/ai-use-every-door.test.ts | qu'un fichier reserve des credits sans etre enregistre comme porte soumise a ai.use — donc qu'un canal facture soit reintroduit sans garde |
Les deux premieres sont derivees du contenu de src/lib/security/, pas
d'une liste tenue a la main : elles suivent un renommage.