Référence

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 utilisant crypto.scrypt de 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 complet

Le 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) :

  1. Extrait la clé depuis Authorization: Bearer <key> ou X-Api-Key: <key>.
  2. SELECT * FROM api_keys WHERE key_prefix = ?.
  3. Ignore les clés révoquées ou expirées ; scrypt-verify les candidates.
  4. Aucune correspondance → 401 unauthorized.
  5. matchesAllScopes(granted, requiredScopes)403 forbidden avec { missing: [...] }.
  6. Résout le projet via l'en-tête X-Cradle-Project ou le binding de la clé.
  7. Met à jour last_used_at, enregistre l'usage, renvoie AuthContext.

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_keys est vide et qu'une clé legacy channels_config.api.config.apiKey existe, 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.