Аутентификация и scopes
Scoped API keys, scrypt-хеширование, привязка к проекту и bootstrap-flow, которые делают Cradle безопасным по умолчанию.
Cradle использует scoped API keys вместо единого общего секрета. Каждый HTTP-запрос
разрешает AuthContext через scrypt-проверку и per-route проверку scope.
Хранение ключей
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 логирует каждый запрос (endpoint, статус, IP, timestamp) с задачей
retention 30 дней.
Формат ключа и хеширование
- Plaintext:
ck_live_<64 hex>(~256 бит энтропии). - Prefix: первые 12 символов, индексируется для поиска.
- Hash:
scrypt$<salt-hex>$<derived-hex>черезcrypto.scryptиз Node.
Plaintext показывается один раз при создании и больше никогда не хранится. Поиск идёт prefix → candidate rows → scrypt-verify до первого совпадения.
Грамматика scope
Scopes имеют вид <namespace>:<action>:
messages:read messages:write
agents:read agents:write agents:execute
kb:read kb:write
models:read models:write
tickets:read tickets:approve
admin:* — полный доступMatching поддерживает wildcard по namespace:
matchesScope(['agents:*'], 'agents:write')→ true.matchesScope(['admin:*'], <anything>)→ true.matchesAllScopes(granted, required)проверяет каждый требуемый scope.
Поток middleware
requireAuth(req, reply, requiredScopes):
- Извлекает ключ из
Authorization: Bearer <key>илиX-Api-Key: <key>. SELECT * FROM api_keys WHERE key_prefix = ?.- Пропускает revoked/expired; scrypt-verify candidates.
- Нет совпадения → 401 unauthorized.
matchesAllScopes(granted, requiredScopes)→ 403 forbidden с{ missing: [...] }.- Разрешает проект через заголовок
X-Cradle-Projectили привязку ключа. - Обновляет
last_used_at, записывает usage, возвращаетAuthContext.
Пример объявления 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()
})Переключение проекта
Admin-ключи (admin:*) могут использовать X-Cradle-Project: <slug> для работы
с другим проектом на каждый запрос. Scoped-ключи могут использовать только свой
проект; любой другой вернёт 403 project-mismatch. Неизвестный slug → 404
project-not-found; архивированный проект → 410 project-archived.
Bootstrap
При каждом старте запускается ensureBootstrapAdminKey():
- Если
api_keysпуста и существует legacychannels_config.api.config.apiKey, он хешируется и вставляется какadmin:*ключ. - Иначе генерируется свежий
ck_live_…, записывается обратно в channel config и вставляется. Plaintext логируется один раз, чтобы оператор мог скопировать.
Так headless-установки становятся сразу работоспособными без ручного создания ключа.
Почему scrypt, а не argon2
API keys — это высокоэнтропийные случайные строки, не пользовательские пароли.
Медленные KDF в первую очередь защищают от low-entropy brute-force, а scrypt из
Node stdlib не требует лишних native-зависимостей. Заменить на argon2 — замена
одного файла в src/core/auth/keys.ts, если будущий аудит это потребует.