ADRADR-0007 · La famille de cles sk_ est retiree, pas reparee

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 :

SurfaceCe qu'elle faisait reellement
POST/GET/DELETE /api/keys, /api/keys/[keyId], /api/keys/validatefrapper et lister des cles qui s'evaporaient
POST /api/channels/apiun canal de chat REST facture, joignable par personne
POST /api/mcp/usageun 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-switcherun 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

OptionPourquoi non
Persister le Map dans une table ApiKeyCa 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 RedisMeme 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 casseUne 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 PRLe 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:claims le verifie.
  • reserveCreditsForChannel perd un appelant sur cinq. Les quatre portes restantes sont enumerees dans src/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.apiKey reste en base, vide, jusqu'a une migration operateur. Les trois champs apiKey des 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 : withApiAuth etait le seul, a l'etape 4 de sa chaine (new FeatureGate(subscription) puis canAccess). Le module et ses 12 tests restent, sans consommateur — suivi dans backlog/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 dans src/services/billing/entitlement.ts et src/types/billing-plans.ts, et src/test/billing-gating-single-source.test.ts empeche la recidive.
  • withApiAuth disparait 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 :

GardeCe qu'elle refuse
src/test/credential-minting.test.tsqu'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.tsqu'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.tsqu'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.