fix: invalidate the TechLog CSRF token on 403 and harden the bootstrap profile wiring
Item 1 (real bug): contractOperations.execute only invalidated the cached CSRF token on UNAUTHENTICATED (401). A CSRF-specific rejection normally arrives as FORBIDDEN (403) -- the platform classifies any 403 response as FORBIDDEN unconditionally -- so a token rejected during an ordinary document save left the stale token cached and every subsequent Studio mutation kept failing until reload. Extracted invalidateTechLogCsrfOnOutcome() so production and the composition test call the identical function; it now invalidates on both UNAUTHENTICATED and FORBIDDEN. Item 2: the upload transport's uncontracted-status fallback hardcoded status 503, so an uncontracted 401/403 body never reached the gateway's error.status === 401 || 403 invalidation check. Passes the real response.status through. Item 3: safeOperation()'s auth-profile parameter is now typed as a union of the two valid profile constants instead of a bare string, and assertExactlyOneTechLogStudioBootstrapOperation() fails composition closed if getStudioSession stops being the sole caller of the credential-free bootstrap profile. Item 4: corrected two stale operation counts in the adapter review doc. Both new tests for items 1 and 2 were run and shown failing before their fix, per this task's TDD standard for error-path changes. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
2cab4974b7
commit
35cc5c868a
@@ -28,7 +28,10 @@ import { createBrowserStorageAdapter } from "../adapters/storage/browser-storage
|
||||
import { createBrowserMutationIntentFactory } from "../adapters/platform/browser-mutation-intent-factory.ts";
|
||||
import { createTelemetryAdapter } from "../adapters/telemetry/best-effort-telemetry.ts";
|
||||
import { createCsrfTokenProvider } from "../features/tech-log/adapters/http/studio-session-csrf.ts";
|
||||
import { attachStudioSessionCredentials } from "../features/tech-log/adapters/http/studio-session-credentials.ts";
|
||||
import {
|
||||
attachStudioSessionCredentials,
|
||||
invalidateTechLogCsrfOnOutcome,
|
||||
} from "../features/tech-log/adapters/http/studio-session-credentials.ts";
|
||||
import type { StudioOperationExecutor as TechLogStudioOperationExecutor } from "../features/tech-log/adapters/http/http-studio-gateway.ts";
|
||||
import type { AuthSessionPort } from "../application/ports/auth-session-port.ts";
|
||||
import type { ReleaseInfo } from "../application/ports/release-info-port.ts";
|
||||
@@ -573,12 +576,16 @@ export async function createRuntimeAdapters(
|
||||
});
|
||||
if (outcome.kind === "UNAUTHENTICATED") {
|
||||
authSession.onUnauthenticated();
|
||||
// The Studio session (and the CSRF token it issued) expired. The
|
||||
// cache owner discards it here, not the gateway — the gateway has no
|
||||
// way to know a 401 on one operation invalidates a token shared by
|
||||
// every other in-flight and future Studio request.
|
||||
techLogCsrf.invalidate();
|
||||
}
|
||||
// Fix round 2, item 1. `UNAUTHENTICATED` (401) and `FORBIDDEN` (403 —
|
||||
// the shape a CSRF-specific rejection normally takes) both leave a
|
||||
// stale token cached for every other in-flight and future Studio
|
||||
// request if nothing discards it. The cache owner discards it here,
|
||||
// not the gateway — the gateway has no way to know a rejection on one
|
||||
// operation invalidates a token shared across all of them. Same call
|
||||
// the composition test drives
|
||||
// (`tests/features/tech-log/studio-csrf-composition.test.ts`).
|
||||
invalidateTechLogCsrfOnOutcome(outcome.kind, techLogCsrf);
|
||||
return outcome;
|
||||
},
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user