docs(api-key): correct stale "show-once" comments for the reveal/copy flow (#557)
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>
This commit is contained in:
@@ -31,12 +31,12 @@ export function useApiKeysQuery(): UseQueryResult<IApiKey[], Error> {
|
||||
/**
|
||||
* Create mutation.
|
||||
*
|
||||
* SECURITY: the response contains the token exactly once. This hook deliberately
|
||||
* does NOT stash it anywhere — the caller reads it from `mutateAsync`'s resolved
|
||||
* value, moves it into the show-once modal's local state, then calls
|
||||
* `mutation.reset()` to purge react-query's own copy immediately. `gcTime: 0`
|
||||
* is a second belt so nothing lingers in the mutation cache after the observer
|
||||
* unmounts. The list is invalidated here (the list carries no token).
|
||||
* SECURITY: the response contains the token. This hook deliberately does NOT
|
||||
* stash it anywhere — the caller reads it from `mutateAsync`'s resolved value
|
||||
* and immediately calls `mutation.reset()` to purge react-query's own copy (the
|
||||
* create flow discards the token; it is re-obtainable later via the reveal/copy
|
||||
* action). `gcTime: 0` is a second belt so nothing lingers in the mutation cache
|
||||
* after the observer unmounts. The list is invalidated here (it carries no token).
|
||||
*/
|
||||
export function useCreateApiKeyMutation() {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
@@ -6,11 +6,12 @@ import {
|
||||
IRevealApiKey,
|
||||
} from "@/features/api-key/types/api-key.types";
|
||||
|
||||
// Mint a new key. The response carries the token ONCE — the caller must move it
|
||||
// straight into the show-once modal's local state and never cache it. See
|
||||
// Mint a new key. The response carries the token, but the create flow now
|
||||
// DISCARDS it (the user copies the key later via the per-row reveal action, so
|
||||
// there is no show-once modal) — the caller must never cache or persist it. See
|
||||
// queries/api-key-query.ts (gcTime: 0 + query invalidation) and
|
||||
// components/api-keys-manager.tsx `handleCreate` (createMutation.reset() right
|
||||
// after reading the token) for the reset()-after-read pattern.
|
||||
// after the mint) for the reset()-after-read discipline the reveal path mirrors.
|
||||
export async function createApiKey(
|
||||
data: ICreateApiKey,
|
||||
): Promise<ICreateApiKeyResponse> {
|
||||
|
||||
@@ -31,7 +31,8 @@ export interface ICreateApiKey {
|
||||
}
|
||||
|
||||
// The metadata half of the create response. The token itself is carried
|
||||
// separately (see ICreateApiKeyResponse) and is shown exactly once.
|
||||
// separately (see ICreateApiKeyResponse); it is re-obtainable later by its owner
|
||||
// via a deterministic re-mint under a step-up (POST /api-keys/reveal).
|
||||
export interface ICreatedApiKey {
|
||||
id: string;
|
||||
name: string;
|
||||
|
||||
@@ -98,7 +98,8 @@ export class ApiKeyController {
|
||||
});
|
||||
// Durable audit is via DatabaseAuditService (#496); this structured log is a
|
||||
// second, container-log trail. No token material — the JWT is only ever
|
||||
// returned in the response.
|
||||
// returned in a response body (here on create, or via /api-keys/reveal),
|
||||
// never logged and never stored.
|
||||
this.logger.log(
|
||||
`API key created: id=${key.id} name=${JSON.stringify(
|
||||
key.name,
|
||||
@@ -107,9 +108,9 @@ export class ApiKeyController {
|
||||
} ip=${this.clientIp(req)}`,
|
||||
);
|
||||
|
||||
// Return the token ONCE (never retrievable again) and the computed expiry so
|
||||
// the caller/UI can surface "expires <date>" (the year-default time-bomb
|
||||
// early-warning).
|
||||
// Return the token (also re-obtainable later by the owner via
|
||||
// /api-keys/reveal under a step-up) and the computed expiry so the caller/UI
|
||||
// can surface "expires <date>" (the year-default time-bomb early-warning).
|
||||
return {
|
||||
token,
|
||||
apiKey: {
|
||||
|
||||
@@ -48,8 +48,11 @@ export class ApiKeyService {
|
||||
* written (inert), so a half-created key cannot exist.
|
||||
* 3. insert the row last. A lost response leaves an orphaned row that is
|
||||
* visible in `list` and self-heals (the user revokes it).
|
||||
* The token is returned ONCE and never stored — the JWT is self-contained, so
|
||||
* no token material lives in the table.
|
||||
* No token material is ever stored — the JWT is self-contained (its `api_keys`
|
||||
* row holds only metadata + lifetime, never the token). The token is returned
|
||||
* here on create AND is re-obtainable any time by its owner via a deterministic
|
||||
* re-mint under a password step-up (POST /api-keys/reveal); it is deterministic
|
||||
* precisely because it carries no `iat`/`exp` (see TokenService).
|
||||
*
|
||||
* `expiresAt`: `undefined` -> default 1 year; `null` -> unlimited (explicit);
|
||||
* a Date -> that instant (a past date is rejected at the DTO layer).
|
||||
|
||||
Reference in New Issue
Block a user