feat(api-key): фаза 3A — снять kill-switch API_KEYS_ENABLED + копируемый ключ (reveal под step-up)
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>
This commit is contained in:
@@ -15,6 +15,7 @@ vi.mock("@/lib/api-client", () => ({
|
||||
import {
|
||||
createApiKey,
|
||||
getApiKeys,
|
||||
revealApiKey,
|
||||
} from "@/features/api-key/services/api-key-service";
|
||||
|
||||
beforeEach(() => {
|
||||
@@ -75,4 +76,23 @@ describe("api-key-service response-contract unwrap", () => {
|
||||
expect(result).toHaveLength(2);
|
||||
expect(post).toHaveBeenCalledWith("/api-keys/list");
|
||||
});
|
||||
|
||||
it("revealApiKey unwraps the envelope to the bare token string", async () => {
|
||||
const SECRET = "gm_revealed-token";
|
||||
post.mockResolvedValue({
|
||||
data: { token: SECRET },
|
||||
success: true,
|
||||
status: 200,
|
||||
});
|
||||
|
||||
const result = await revealApiKey({ id: "key-1", password: "pw" });
|
||||
|
||||
// The service returns ONLY the token string (not the { token } wrapper), so
|
||||
// the caller can copy it straight to the clipboard.
|
||||
expect(result).toBe(SECRET);
|
||||
expect(post).toHaveBeenCalledWith("/api-keys/reveal", {
|
||||
id: "key-1",
|
||||
password: "pw",
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user