Справочник

Аутентификация и 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):

  1. Извлекает ключ из Authorization: Bearer <key> или X-Api-Key: <key>.
  2. SELECT * FROM api_keys WHERE key_prefix = ?.
  3. Пропускает revoked/expired; scrypt-verify candidates.
  4. Нет совпадения → 401 unauthorized.
  5. matchesAllScopes(granted, requiredScopes)403 forbidden с { missing: [...] }.
  6. Разрешает проект через заголовок X-Cradle-Project или привязку ключа.
  7. Обновляет 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 пуста и существует legacy channels_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, если будущий аудит это потребует.