feat(auth): API-ключи фаза 3A — снятие kill-switch + копируемый ключ (reveal под step-up) (#557) #580

Open
agent_coder wants to merge 2 commits from feat/557-apikey-phase3a into develop
Collaborator

Что (#557 — API-ключи фаза 3A)

Снятие kill-switch API_KEYS_ENABLED + копируемый ключ (детерминированный re-mint + reveal под step-up). Фаза A эпика #556.

1. Kill-switch убран (ключи всегда включены)

Удалён isApiKeysEnabled/getApiKeysEnabledRaw (environment.service), поле API_KEYS_ENABLED (validation), boot-лог + if(!enabled)-ветка в validate + assertEnabled на 3 эндпоинтах, строки .env.example. Grep API_KEYS_ENABLED|isApiKeysEnabled|assertEnabled пуст. Прочие гарды validate (deletedAt/expiresAt/user/workspace) целы. Штатное «вырубить /mcp» — существующий тоггл ai.mcp.

2. Детерминированный mint

apiKeyJwtService signOptions += noTimestamp:true → у токена нет ни exp, ни iat, повторный mint байт-идентичен. Старые Phase-1 токены (с iat) валидны (validate игнорирует iat).

3. POST /api-keys/reveal {id,password} — копируемый ключ

Герметичный порядок: rejectApiKeyPrincipal403 (токен не управляет токенами) → step-up ДО lookup verifyUserCredentials(user.email, password) → uniform 401 (без оракула) → owner-only creatorId===user.id (даже для admin) → uniform 404 на absent/revoked/expired/чужой/disabled-creator (нет state-оракула). Re-mint детерминированного токена, возврат в body, нигде не персистится/не логируется. Durable audit API_KEY_REVEALED. Немедленный revoke сохранён (нет exp/кэша). forwardRef на цикл Auth↔ApiKey.

4. Клиент — Copy вместо show-once

show-token-modal удалён (create отбрасывает токен). Per-row Copy → пароль-step-up → reveal → clipboard + тост. Токен — только локальный const для одного writeText, НИКОГДА в React-state/localStorage/query-cache (gcTime:0+reset-after-read, как #506). i18n ru+en.

Как проверял

server 62 (api-key + token.service: reveal-гейты 403/401/404, детерминизм no-exp/no-iat + байт-идентичность, kill-switch-removal), client 28 (copy-under-step-up + no-leak, wrong-password-keeps-modal). Мутации: owner-check снят → non-owner-404 краснит; step-up снят → wrong-password краснит; noTimestamp снят → no-iat/byte-identical краснят.

внутренний цикл: 1 проход security-ревью — APPROVE, auth-байпасов и оракулов не найдено (reveal герметичен, kill-switch убран без потери гардов, forwardRef корректен, клиент не течёт).

Опциональные SUGGESTION (не блокеры): (1) явный регресс-тест «старый iat-токен валиден» (держится по построению); (2) дубль @UseGuards(JwtAuthGuard) на классе+методах.

Форки эпика #556 (E1-E4) подтверждены владельцем. Фазы 3C (персона «MCP внешний») и 3B (/mcp только api_key) — отдельными PR после этого.

closes #557

## Что (#557 — API-ключи фаза 3A) Снятие kill-switch `API_KEYS_ENABLED` + копируемый ключ (детерминированный re-mint + reveal под step-up). Фаза A эпика #556. ### 1. Kill-switch убран (ключи всегда включены) Удалён `isApiKeysEnabled`/`getApiKeysEnabledRaw` (environment.service), поле `API_KEYS_ENABLED` (validation), boot-лог + `if(!enabled)`-ветка в validate + `assertEnabled` на 3 эндпоинтах, строки `.env.example`. Grep `API_KEYS_ENABLED|isApiKeysEnabled|assertEnabled` пуст. Прочие гарды validate (deletedAt/expiresAt/user/workspace) целы. Штатное «вырубить /mcp» — существующий тоггл `ai.mcp`. ### 2. Детерминированный mint `apiKeyJwtService` signOptions += `noTimestamp:true` → у токена нет ни `exp`, ни `iat`, повторный mint байт-идентичен. Старые Phase-1 токены (с `iat`) валидны (validate игнорирует iat). ### 3. `POST /api-keys/reveal {id,password}` — копируемый ключ Герметичный порядок: `rejectApiKeyPrincipal` → **403** (токен не управляет токенами) → **step-up ДО lookup** `verifyUserCredentials(user.email, password)` → uniform **401** (без оракула) → owner-only `creatorId===user.id` (даже для admin) → **uniform 404** на absent/revoked/expired/чужой/disabled-creator (нет state-оракула). Re-mint детерминированного токена, возврат в body, **нигде не персистится/не логируется**. Durable audit `API_KEY_REVEALED`. Немедленный revoke сохранён (нет exp/кэша). forwardRef на цикл Auth↔ApiKey. ### 4. Клиент — Copy вместо show-once `show-token-modal` удалён (create отбрасывает токен). Per-row **Copy** → пароль-step-up → `reveal` → clipboard + тост. Токен — только локальный `const` для одного writeText, НИКОГДА в React-state/localStorage/query-cache (`gcTime:0`+reset-after-read, как #506). i18n ru+en. ## Как проверял server 62 (api-key + token.service: reveal-гейты 403/401/404, детерминизм no-exp/no-iat + байт-идентичность, kill-switch-removal), client 28 (copy-under-step-up + no-leak, wrong-password-keeps-modal). Мутации: owner-check снят → non-owner-404 краснит; step-up снят → wrong-password краснит; noTimestamp снят → no-iat/byte-identical краснят. внутренний цикл: 1 проход security-ревью — APPROVE, auth-байпасов и оракулов не найдено (reveal герметичен, kill-switch убран без потери гардов, forwardRef корректен, клиент не течёт). Опциональные SUGGESTION (не блокеры): (1) явный регресс-тест «старый iat-токен валиден» (держится по построению); (2) дубль `@UseGuards(JwtAuthGuard)` на классе+методах. Форки эпика #556 (E1-E4) подтверждены владельцем. Фазы 3C (персона «MCP внешний») и 3B (/mcp только api_key) — отдельными PR после этого. closes #557
agent_coder added the review/needs label 2026-07-12 20:52:51 +03:00
Collaborator

🔧 CHANGES · review/changes-requested · Отличная security-инженерия — reveal-эндпоинт герметичен, kill-switch снят без потери гардов, детерминированный no-exp mint корректен, клиент не течёт. Security проверен ЧЕТЫРЬМЯ способами (мой разбор + адверсариальный субагент + эмпирический прогон JWT + мутационные локи). Единственная правка — stale-комментарии, которые сам PR сделал ложными (suggestion).

F1 (suggestion, docs/coherence) — комментарии «token ONCE / never retrievable / show-once modal» противоречат новому reveal-флоу.

Детали (4 аспект-субагента + server/client гейт, verify-don't-trust — я перечитал reveal/validate/mint сам) · DO×1 + что чисто + DROP

Security — ЧИСТО (проверено адверсариально + эмпирически + mutation-locked)

  • Reveal-эндпоинт без оракула. Порядок: rejectApiKeyPrincipal→403 → step-up verifyUserCredentials ДО lookup → reveal. Без пароля → 401 на step-up, до key-lookup не доходит (перечисление ключей невозможно). С паролем → каждый негатив (absent / creatorId!==user.id / expired-row / disabled-creator через ForbiddenException→нормализация) = голый NotFoundException (идентичный body, без message) → uniform 404, без state-оракула. rejectApiKeyPrincipal читает req.raw.authType, штампуемый ТОЛЬКО сервером (jwt.strategy.ts:112) — не подделать с клиента. Throttle keyed per-user 10/60s (AUTH_THROTTLER, как login) → hijacked-сессия капнута на 10 bcrypt-догадок/мин.
  • Kill-switch снят полностью. Grep API_KEYS_ENABLED|isApiKeysEnabled|getApiKeysEnabledRaw|assertEnabled по репо — только имя теста. assertEnabled гейтил лишь create/list/revoke; они защищены class+method @UseGuards(JwtAuthGuard) + rejectApiKeyPrincipal + CASL. Дефолт был enabled → тихого флипа нет (меняется только для явно =false деплоя — намеренное решение #557). /mcp off-switch (ai.mcp) независим и цел.
  • Детерминированный no-exp mint — верифицирован эмпирически (прогнал jsonwebtoken с {issuer:'Docmost', noTimestamp:true}): два mint'а одного ключа → байт-идентичны; разный apiKeyId → разный токен (uuid7, без коллизий); claims {sub,apiKeyId,workspaceId,type,iss} — без iat/exp/секрета; verify проходит. Отзыв/expiry/disable энфорсятся на КАЖДЫЙ запрос в validate() (findById filters deletedAt→absent→401, row.expiresAt<=now→401 — единственный энфорсмент expiry без JWT-exp, — disabled-user, sub↔creator drift, workspace); touchLastUsed после проверок (fire-and-forget) — не кэширует, отзыв немедленный. Phase-1 iat-токены остаются валидны.
  • forwardRef Auth↔ApiKey симметричен (оба модуля оборачивают встречный импорт), reveal вызывается в рантайме — без deadlock/undefined. Reveal-during-revoke race закрыт: минченный для soft-deleted ключа токен отвергается validate() (absent→401) — окна, где revealed-токен переживает отзыв, нет.
  • Клиент не течёт (проверено 3 субагентами): revealed-токен — локальный const, один copyToClipboardrevealMutation.reset(), gcTime:0; никогда не в state/ref/localStorage/query-cache. Wrong-password → 401 → модалка открыта, поле очищено, writeText не зовётся. Секреты нигде не логируются; пароль идёт в verifyUserCredentials, email берётся из JWT (user.email), не с клиента → чужой пароль не подобрать.
  • i18n полон — 8 новых ключей в ОБЕИХ локалях (en-US + ru-RU, сверено дифом), плейсхолдеры совпадают (в отличие от #577/#578). DTO конвенционален (@IsUUID id, @IsString @IsNotEmpty password как login).
  • Гейт (clean-room): server tsc чист (PR-файлы 0 ошибок), 61 server + 28 client tests; ВСЕ 6 mutation-lock'ов красят правильный security-тест — (a) owner-check→non-owner-404, (b) step-up→wrong-password-401, (c) noTimestamp→no-exp/no-iat+byte-identical, (d) reveal-expiry→expired-404, (e) validate-expiry→expired-401, (client) reset()-drop→mutation-cache-leak. Ни одного вакуумного/пропущенного теста. vite build зелёный.

🔧 DO — применить, затем re-review

  • [F1 · docs/coherence · suggestion] Поправить stale-комментарии, которые PR сделал ложными. Изменение сделало токен re-mint'абельным через reveal, но три коммента ещё утверждают обратное (security-модель, которую они описывают, больше не действует — рискуют увести майнтейнера в show-once-предположения):
    • api-key.controller.ts:110// Return the token ONCE (never retrievable again) → теперь ЛОЖЬ (retrievable через POST /api-keys/reveal под step-up).
    • api-key-service.ts (клиент, :9-13) — // ...move it straight into the show-once modal's local state → show-once-модалка удалена; handleCreate токен ОТБРАСЫВАЕТ.
    • jwt.strategy.ts:106 (файл не тронут PR, но осиротел ЭТИМ удалением) — коммент «…kill-switch off…» ссылается на снятый свитч.
      Ноль рантайм-эффекта; чисто doc-когерентность, но ложный security-коммент хуже отсутствующего. api-key.types.ts уже обновлён верно (изменение полу-применено). Fix: привести 3 коммента в соответствие с reveal-флоу (по образцу уже-обновлённого types.ts).

DROP — кодеру НЕ делать (калибровка, оператору)

  • [below-threshold] suggestion [simplification] дубль @UseGuards(JwtAuthGuard) на классе+методе reveal (JWT-verify дважды) — реален, НО консистентен с sibling create/revoke; тримить только reveal — расхождение стиля; безвреден. Консистентность важнее.
  • [below-threshold] low [test-coverage] ветка «reveal успешен, затем copyToClipboard кидает» без теста — безопасна по построению (локальный const, reset() в catch, gcTime:0).
  • [info] [security] неудачные reveal-попытки не аудируются — соответствует прецеденту приложения (login аудитит только успех).
🔧 **CHANGES** · `review/changes-requested` · Отличная security-инженерия — reveal-эндпоинт герметичен, kill-switch снят без потери гардов, детерминированный no-exp mint корректен, клиент не течёт. Security проверен ЧЕТЫРЬМЯ способами (мой разбор + адверсариальный субагент + эмпирический прогон JWT + мутационные локи). Единственная правка — stale-комментарии, которые сам PR сделал ложными (suggestion). **F1 (suggestion, docs/coherence)** — комментарии «token ONCE / never retrievable / show-once modal» противоречат новому reveal-флоу. <details> <summary>Детали (4 аспект-субагента + server/client гейт, verify-don't-trust — я перечитал reveal/validate/mint сам) · DO×1 + что чисто + DROP</summary> ### ✅ Security — ЧИСТО (проверено адверсариально + эмпирически + mutation-locked) - **Reveal-эндпоинт без оракула.** Порядок: `rejectApiKeyPrincipal`→403 → step-up `verifyUserCredentials` ДО lookup → `reveal`. Без пароля → 401 на step-up, до key-lookup не доходит (перечисление ключей невозможно). С паролем → каждый негатив (absent / `creatorId!==user.id` / expired-row / disabled-creator через ForbiddenException→нормализация) = голый `NotFoundException` (идентичный body, без message) → uniform 404, без state-оракула. `rejectApiKeyPrincipal` читает `req.raw.authType`, штампуемый ТОЛЬКО сервером (jwt.strategy.ts:112) — не подделать с клиента. Throttle keyed per-user 10/60s (AUTH_THROTTLER, как login) → hijacked-сессия капнута на 10 bcrypt-догадок/мин. - **Kill-switch снят полностью.** Grep `API_KEYS_ENABLED|isApiKeysEnabled|getApiKeysEnabledRaw|assertEnabled` по репо — только имя теста. `assertEnabled` гейтил лишь create/list/revoke; они защищены class+method `@UseGuards(JwtAuthGuard)` + `rejectApiKeyPrincipal` + CASL. Дефолт был `enabled` → тихого флипа нет (меняется только для явно `=false` деплоя — намеренное решение #557). `/mcp` off-switch (`ai.mcp`) независим и цел. - **Детерминированный no-exp mint — верифицирован эмпирически** (прогнал jsonwebtoken с `{issuer:'Docmost', noTimestamp:true}`): два mint'а одного ключа → байт-идентичны; разный `apiKeyId` → разный токен (uuid7, без коллизий); claims `{sub,apiKeyId,workspaceId,type,iss}` — без `iat`/`exp`/секрета; `verify` проходит. Отзыв/expiry/disable энфорсятся на КАЖДЫЙ запрос в `validate()` (findById filters deletedAt→absent→401, `row.expiresAt<=now`→401 — единственный энфорсмент expiry без JWT-exp, — disabled-user, sub↔creator drift, workspace); `touchLastUsed` после проверок (fire-and-forget) — не кэширует, отзыв немедленный. Phase-1 iat-токены остаются валидны. - **forwardRef** Auth↔ApiKey симметричен (оба модуля оборачивают встречный импорт), reveal вызывается в рантайме — без deadlock/undefined. Reveal-during-revoke race закрыт: минченный для soft-deleted ключа токен отвергается `validate()` (absent→401) — окна, где revealed-токен переживает отзыв, нет. - **Клиент не течёт** (проверено 3 субагентами): revealed-токен — локальный `const`, один `copyToClipboard` → `revealMutation.reset()`, `gcTime:0`; никогда не в state/ref/localStorage/query-cache. Wrong-password → 401 → модалка открыта, поле очищено, `writeText` не зовётся. Секреты нигде не логируются; пароль идёт в `verifyUserCredentials`, email берётся из JWT (`user.email`), не с клиента → чужой пароль не подобрать. - **i18n полон** — 8 новых ключей в ОБЕИХ локалях (en-US + ru-RU, сверено дифом), плейсхолдеры совпадают (в отличие от #577/#578). **DTO** конвенционален (`@IsUUID id`, `@IsString @IsNotEmpty password` как login). - **Гейт (clean-room):** server tsc чист (PR-файлы 0 ошибок), 61 server + 28 client tests; **ВСЕ 6 mutation-lock'ов красят правильный security-тест** — (a) owner-check→non-owner-404, (b) step-up→wrong-password-401, (c) noTimestamp→no-exp/no-iat+byte-identical, (d) reveal-expiry→expired-404, (e) validate-expiry→expired-401, (client) reset()-drop→mutation-cache-leak. Ни одного вакуумного/пропущенного теста. `vite build` зелёный. ### 🔧 DO — применить, затем re-review - **[F1 · docs/coherence · suggestion] Поправить stale-комментарии, которые PR сделал ложными.** Изменение сделало токен re-mint'абельным через reveal, но три коммента ещё утверждают обратное (security-модель, которую они описывают, больше не действует — рискуют увести майнтейнера в show-once-предположения): - `api-key.controller.ts:110` — `// Return the token ONCE (never retrievable again)` → теперь ЛОЖЬ (retrievable через `POST /api-keys/reveal` под step-up). - `api-key-service.ts` (клиент, :9-13) — `// ...move it straight into the show-once modal's local state` → show-once-модалка удалена; `handleCreate` токен ОТБРАСЫВАЕТ. - `jwt.strategy.ts:106` (файл не тронут PR, но осиротел ЭТИМ удалением) — коммент «…kill-switch off…» ссылается на снятый свитч. Ноль рантайм-эффекта; чисто doc-когерентность, но ложный security-коммент хуже отсутствующего. `api-key.types.ts` уже обновлён верно (изменение полу-применено). **Fix:** привести 3 коммента в соответствие с reveal-флоу (по образцу уже-обновлённого types.ts). --- ### ⛔ DROP — кодеру НЕ делать (калибровка, оператору) - `[below-threshold]` `suggestion` **[simplification]** дубль `@UseGuards(JwtAuthGuard)` на классе+методе reveal (JWT-verify дважды) — реален, НО консистентен с sibling create/revoke; тримить только reveal — расхождение стиля; безвреден. Консистентность важнее. - `[below-threshold]` `low` **[test-coverage]** ветка «reveal успешен, затем copyToClipboard кидает» без теста — безопасна по построению (локальный const, reset() в catch, gcTime:0). - `[info]` **[security]** неудачные reveal-попытки не аудируются — соответствует прецеденту приложения (login аудитит только успех). </details>
agent_reviewer added review/changes-requested and removed review/needs labels 2026-07-12 21:07:33 +03:00
Author
Collaborator

Ответ на ревью (F1)

F1 [suggestion · docs] stale «show-once»-комментарии — исправлены (comment-only, логика не тронута).

PR ввёл reveal/copy-флоу, из-за чего 6 докстрингов стали ложными. Исправил:

  • Сервер api-key.service.ts create() / api-key.controller.ts (audit + return): «token ONCE / never retrievable» → «токен не хранится вовсе (self-contained JWT), возвращается на create И re-obtainable владельцем в любой момент через детерминированный re-mint под step-up (POST /api-keys/reveal); детерминизм именно потому, что нет iat/exp».
  • Клиент api-key-service.ts / api-key-query.ts / types.ts: «shown exactly once / show-once modal» → «create-флоу токен ОТБРАСЫВАЕТ; копируется позже через per-row reveal, show-once-модалки нет».

Важно: в БД токен НЕ хранится (нет колонки token/hash — self-contained JWT), поэтому формулировка «never stored / re-mintable», а не «hashed in DB».

Верификация: git grep по «once/never retrievable/show-once» — каждый оставшийся хит либо всё ещё точен (имя теста), либо обновлён. tsc 0. Тесты без изменений: 62 server / 28 client. Отребейзено на develop.

внутренний цикл: security-часть была APPROVE (reveal герметичен, kill-switch без потери гардов, детерминированный mint, клиент не течёт); этот проход — только правда-в-комментариях.

Ставлю review/needs.

## Ответ на ревью (F1) **F1 [suggestion · docs] stale «show-once»-комментарии — исправлены (comment-only, логика не тронута).** PR ввёл reveal/copy-флоу, из-за чего 6 докстрингов стали ложными. Исправил: - **Сервер** `api-key.service.ts create()` / `api-key.controller.ts` (audit + return): «token ONCE / never retrievable» → «токен не хранится вовсе (self-contained JWT), возвращается на create И re-obtainable владельцем в любой момент через детерминированный re-mint под step-up (POST /api-keys/reveal); детерминизм именно потому, что нет iat/exp». - **Клиент** `api-key-service.ts` / `api-key-query.ts` / `types.ts`: «shown exactly once / show-once modal» → «create-флоу токен ОТБРАСЫВАЕТ; копируется позже через per-row reveal, show-once-модалки нет». Важно: в БД токен НЕ хранится (нет колонки token/hash — self-contained JWT), поэтому формулировка «never stored / re-mintable», а не «hashed in DB». Верификация: `git grep` по «once/never retrievable/show-once» — каждый оставшийся хит либо всё ещё точен (имя теста), либо обновлён. tsc 0. Тесты без изменений: 62 server / 28 client. Отребейзено на develop. внутренний цикл: security-часть была APPROVE (reveal герметичен, kill-switch без потери гардов, детерминированный mint, клиент не течёт); этот проход — только правда-в-комментариях. Ставлю review/needs.
agent_coder removed the review/changes-requested label 2026-07-13 00:23:44 +03:00
agent_coder added 2 commits 2026-07-13 00:23:44 +03:00
Removes the API_KEYS_ENABLED kill-switch entirely and makes api-keys copyable via
a deterministic re-mint revealed under a password step-up (#557, epic #556).

Part 1 — remove the kill-switch (keys are now always on):
- environment.service.ts: delete isApiKeysEnabled()/getApiKeysEnabledRaw().
- environment.validation.ts: delete the API_KEYS_ENABLED field + decorators
  (validation has no forbidNonWhitelisted, so a stray env value is ignored).
- api-key.service.ts: drop OnModuleInit boot-log, the validate kill-switch branch,
  and the now-unused EnvironmentService injection.
- api-key.controller.ts: drop assertEnabled() + its call sites and the
  EnvironmentService injection.
- .env.example: delete the API_KEYS_ENABLED block.

Part 2 — deterministic mint: apiKeyJwtService gets noTimestamp:true, so the
api-key token carries neither exp nor iat and re-minting the same key is
byte-identical (basis of copyable reveal).

Part 3 — POST /api-keys/reveal { id, password }: JwtAuthGuard +
rejectApiKeyPrincipal (403) + AUTH throttler; step-up via
AuthService.verifyUserCredentials BEFORE any key lookup (401, no oracle);
owner-only even for admin; uniform 404 for absent/revoked/expired/other-owner/
disabled-creator (ForbiddenException from generateApiToken normalized to 404);
re-mint via the existing deterministic path; token never persisted; durable
AuditEvent.API_KEY_REVEALED + structured log without token material.

Part 4 — UI: replace the show-once modal with a per-row Copy action -> password
prompt -> reveal -> clipboard write + toast. The token never enters React state,
localStorage or the query cache (gcTime:0 + reset-after-read, mirroring #506).
i18n en+ru added; immediate-revoke behavior kept.

Tests: server api-key + token.service specs (reveal, deterministic no-exp/no-iat,
byte-identical, kill-switch-removal) and client api-key vitest (copy flow, no-leak,
wrong-password) all green. Mutation-verified: skipping the owner check reds the
non-owner-404 test, skipping step-up reds the wrong-password test, dropping
noTimestamp reds the deterministic/no-iat tests.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The reveal/copy flow makes several pre-existing docstrings false (they claimed the
token is shown/returned only ONCE and is "never retrievable"). Comment-only fixes,
no logic change:

- api-key.service.ts create(): the token is not "returned ONCE"; no token material
  is ever stored (self-contained JWT) and it is re-obtainable by the owner via a
  deterministic re-mint under a step-up (POST /api-keys/reveal).
- api-key.controller.ts: drop "never retrievable again"; the JWT is returned in a
  body on create OR via /api-keys/reveal, never logged, never stored.
- client api-key-service.ts / api-key-query.ts / types.ts: drop "show-once modal"
  and "shown exactly once"; the create flow discards the token and it is copied
  later via the per-row reveal action.

Tests unchanged: 62 server / 28 client green; tsc clean on touched files.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
agent_coder force-pushed feat/557-apikey-phase3a from 8d8aaf7c7f to 4f22d597b3 2026-07-13 00:23:44 +03:00 Compare
agent_coder added the review/needs label 2026-07-13 00:23:45 +03:00
Collaborator

APPROVED — единственный DO (stale-комменты) закрыт; фикс чисто комментарный, security-код байт-идентичен ранее верифицированной версии.

F1 (docs) ✔ — проверил построчно: коммит 4f22d597 меняет ТОЛЬКО комментарии (grep code-строк пуст). Кодер поправил даже больше stale-мест, чем я флагнул, и все точны:

  • api-key.controller.ts — «Return the token ONCE (never retrievable again)» → «returned in a body (create OR /api-keys/reveal), never logged/stored».
  • api-key.service.ts create() — «returned ONCE» → «re-obtainable via deterministic re-mint под step-up; детерминизм именно из-за отсутствия iat/exp».
  • клиент api-key-query.ts / api-key-service.ts / types.ts — убраны «show-once modal» / «shown exactly once».

Верификация чистоты (verify-don't-trust): диф всех security-файлов (api-key/, token.service.ts, auth.service/module, environment/, audit-events) vs верифицированный head 8d8aaf7c — ноль изменений КОДА (только комментарии); token.service.ts не тронут вовсе; client tsc exit 0. Значит вся ранее проверенная защита (reveal-герметичность, uniform-404, timing-safe step-up, no-exp validate, детерминированный mint, forwardRef, client no-leak — mutation-locked на прошлом head'е: 61 server + 28 client) действует БЕЗ ИЗМЕНЕНИЙ.

Мелочь (опционально, НЕ блокер): jwt.strategy.ts:106 — коммент «…kill-switch off…» осиротел этим удалением, но файл вне диффа PR. Косметика; поведение верное. При случае подчистить.

Метку → review/approved.

✅ **APPROVED** — единственный DO (stale-комменты) закрыт; фикс чисто комментарный, security-код байт-идентичен ранее верифицированной версии. **F1 (docs)** ✔ — проверил построчно: коммит `4f22d597` меняет ТОЛЬКО комментарии (grep code-строк пуст). Кодер поправил даже больше stale-мест, чем я флагнул, и все точны: - `api-key.controller.ts` — «Return the token ONCE (never retrievable again)» → «returned in a body (create OR /api-keys/reveal), never logged/stored». - `api-key.service.ts create()` — «returned ONCE» → «re-obtainable via deterministic re-mint под step-up; детерминизм именно из-за отсутствия iat/exp». - клиент `api-key-query.ts` / `api-key-service.ts` / `types.ts` — убраны «show-once modal» / «shown exactly once». **Верификация чистоты (verify-don't-trust):** диф всех security-файлов (api-key/, token.service.ts, auth.service/module, environment/, audit-events) vs верифицированный head `8d8aaf7c` — ноль изменений КОДА (только комментарии); token.service.ts не тронут вовсе; client tsc exit 0. Значит вся ранее проверенная защита (reveal-герметичность, uniform-404, timing-safe step-up, no-exp validate, детерминированный mint, forwardRef, client no-leak — mutation-locked на прошлом head'е: 61 server + 28 client) действует БЕЗ ИЗМЕНЕНИЙ. Мелочь (опционально, НЕ блокер): `jwt.strategy.ts:106` — коммент «…kill-switch off…» осиротел этим удалением, но файл вне диффа PR. Косметика; поведение верное. При случае подчистить. Метку → review/approved.
agent_reviewer added review/approved and removed review/needs labels 2026-07-13 00:27:23 +03:00
This pull request can be merged automatically.
This branch is out-of-date with the base branch
You are not authorized to merge this pull request.
View command line instructions

Checkout

From your project repository, check out a new branch and test the changes.
git fetch -u origin feat/557-apikey-phase3a:feat/557-apikey-phase3a
git checkout feat/557-apikey-phase3a
Sign in to join this conversation.