Authentification et scopes
Clés API scopées, hachage scrypt, binding projet et le flux de bootstrap qui garde Cradle sécurisé par défaut.
Cradle utilise des clés API scopées au lieu d'un seul secret partagé. Chaque requête HTTP résout un AuthContext via une vérification scrypt et un contrôle de scope par route.
Stockage des clés
CREATE TABLE api_keys (
id TEXT PRIMARY KEY,
name TEXT NOT NULL,
key_hash TEXT NOT NULL UNIQUE,
key_prefix TEXT NOT NULL,
project_id TEXT REFERENCES projects(id) ON DELETE CASCADE,
scopes TEXT NOT NULL DEFAULT '[]',
created_at INTEGER NOT NULL,
last_used_at INTEGER,
expires_at INTEGER,
revoked_at INTEGER
);
CREATE INDEX api_keys_key_prefix_idx ON api_keys (key_prefix);api_key_usage journalise chaque requête (endpoint, statut, IP, timestamp) avec un job de rétention de 30 jours.
Format de clé et hachage
- Plaintext :
ck_live_<64 hex>(~256 bits d'entropie). - Préfixe : les 12 premiers caractères, indexé pour la recherche.
- Hash :
scrypt$<salt-hex>$<derived-hex>en utilisantcrypto.scryptde Node.
Le plaintext est affiché une seule fois à la création et jamais persisté à nouveau. La recherche est préfixe → lignes candidates → scrypt-verify jusqu'à trouver une correspondance.
Grammaire des scopes
Les scopes suivent <espace-de-noms>:<action> :
messages:read messages:write
agents:read agents:write agents:execute
kb:read kb:write
models:read models:write
tickets:read tickets:approve
admin:* — accès completLe matching supporte les wildcards d'espace de noms :
matchesScope(['agents:*'], 'agents:write')→ true.matchesScope(['admin:*'], <anything>)→ true.matchesAllScopes(granted, required)vérifie chaque scope requis.
Flux du middleware
requireAuth(req, reply, requiredScopes) :
- Extrait la clé depuis
Authorization: Bearer <key>ouX-Api-Key: <key>. SELECT * FROM api_keys WHERE key_prefix = ?.- Ignore les clés révoquées ou expirées ; scrypt-verify les candidates.
- Aucune correspondance → 401 unauthorized.
matchesAllScopes(granted, requiredScopes)→ 403 forbidden avec{ missing: [...] }.- Résout le projet via l'en-tête
X-Cradle-Projectou le binding de la clé. - Met à jour
last_used_at, enregistre l'usage, renvoieAuthContext.
Exemple de déclaration d'endpoint :
server.get('/api/v1/admin/agents', async (req, reply) => {
const auth = await requireAuth(req, reply, ['agents:read'])
if (!auth) return
return getDb().select().from(agents).where(eq(agents.projectId, auth.projectId)).all()
})Changement de projet
Les clés admin (admin:*) peuvent utiliser X-Cradle-Project: <slug> pour agir sur un autre projet par requête. Les clés scopées ne peuvent utiliser que le projet auquel elles sont liées ; tout autre renvoie 403 project-mismatch. Les slugs inconnus renvoient 404 project-not-found ; les projets archivés renvoient 410 project-archived.
Bootstrap
À chaque démarrage, ensureBootstrapAdminKey() s'exécute :
- Si
api_keysest vide et qu'une clé legacychannels_config.api.config.apiKeyexiste, elle est hachée et insérée comme cléadmin:*. - Sinon, une nouvelle clé
ck_live_…est générée, écrite dans la config du canal, et insérée. Le plaintext est logué une fois pour que l'opérateur puisse le copier.
Cela rend les installations headless immédiatement utilisables sans création manuelle de clé.
Pourquoi scrypt et pas argon2
Les clés API sont des chaînes aléatoires à haute entropie, pas des mots de passe utilisateur. Les KDF lents défendent principalement les brute-force à faible entropie, et scrypt de la stdlib Node n'a pas de dépendance native supplémentaire. Le remplacer par argon2 est un changement d'un seul fichier dans src/core/auth/keys.ts si un audit futur l'exige.