C1 (Critical): getStudioSession was stamped with the same TECH_LOG_STUDIO_SESSION auth profile as every other Studio operation, and that profile requires the CSRF header it is getStudioSession's own job to issue -- an unconditional cycle that recursed without bound in HTTP mode. Fixed with a credential-free TECH_LOG_STUDIO_BOOTSTRAP auth profile for getStudioSession alone, a synchronous re-entrancy guard in createCsrfTokenProvider as defense in depth, and a throwing stub in place of the prior `let x!: T` assertion. Added a composition-level regression test that wires the real executor, CSRF provider, and credential-attach function together and proves getStudioSession dispatches exactly once while its token reaches both a JSON operation and the multipart upload. Also: invalidate the cached CSRF token on a 401/403 from the upload path (I2), a throwing useStudioAssetGateway() accessor so Task 11 cannot silently compile a null-gateway UI (I3), and the M1-M5 minors from the review (guard a malformed success body, cover the untested error fallbacks, align aborted uploads with the JSON path's non-retryable CANCELLED mapping, derive the credential header name from one source instead of two, and correct the adapter review doc's operation count). Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
89 lines
3.2 KiB
TypeScript
89 lines
3.2 KiB
TypeScript
import assert from "node:assert/strict";
|
|
import { test } from "vitest";
|
|
|
|
import {
|
|
createCsrfTokenProvider,
|
|
type CsrfTokenProvider,
|
|
} from "../../../src/features/tech-log/adapters/http/studio-session-csrf.ts";
|
|
|
|
/**
|
|
* Task 7 fix round 1 (C1, item 2). `createCsrfTokenProvider`'s `resolve()`
|
|
* previously used `inFlight ??= deps.execute(options).then(...)`, which
|
|
* evaluates `deps.execute(options)` — and therefore any re-entrant call back
|
|
* into this same provider — before the `??=` assignment to `inFlight`
|
|
* completes. If the operation an `execute` implementation calls happens to
|
|
* require this same provider's token, `resolve()` re-enters itself while
|
|
* `inFlight` is still `null`, recursing without bound
|
|
* (`RangeError: Maximum call stack size exceeded`) instead of deduplicating.
|
|
* The real fix for the TechLog Studio case is giving `getStudioSession` a
|
|
* credential-free auth profile (`studio-csrf-composition.test.ts` proves
|
|
* that end to end); this file pins the provider's own defense-in-depth
|
|
* guard in isolation, so any future `execute` implementation with the same
|
|
* mistake fails loudly and immediately instead of overflowing the stack.
|
|
*/
|
|
|
|
test("shares one in-flight request across concurrent callers", async () => {
|
|
let executions = 0;
|
|
let resolveExecute: ((snapshot: { csrfToken: string; csrfHeaderName: string }) => void) | undefined;
|
|
const provider = createCsrfTokenProvider({
|
|
execute() {
|
|
executions += 1;
|
|
return new Promise((resolve) => {
|
|
resolveExecute = resolve;
|
|
});
|
|
},
|
|
});
|
|
|
|
const first = provider.token();
|
|
const second = provider.token();
|
|
resolveExecute?.({ csrfToken: "csrf-1", csrfHeaderName: "X-CSRF-TOKEN" });
|
|
|
|
assert.equal(await first, "csrf-1");
|
|
assert.equal(await second, "csrf-1");
|
|
assert.equal(executions, 1);
|
|
});
|
|
|
|
test("caches the token after the first successful resolution", async () => {
|
|
let executions = 0;
|
|
const provider = createCsrfTokenProvider({
|
|
async execute() {
|
|
executions += 1;
|
|
return { csrfToken: `csrf-${executions}`, csrfHeaderName: "X-CSRF-TOKEN" };
|
|
},
|
|
});
|
|
|
|
assert.equal(await provider.token(), "csrf-1");
|
|
assert.equal(await provider.token(), "csrf-1");
|
|
assert.equal(executions, 1);
|
|
});
|
|
|
|
test("re-fetches after invalidate()", async () => {
|
|
let executions = 0;
|
|
const provider = createCsrfTokenProvider({
|
|
async execute() {
|
|
executions += 1;
|
|
return { csrfToken: `csrf-${executions}`, csrfHeaderName: "X-CSRF-TOKEN" };
|
|
},
|
|
});
|
|
|
|
assert.equal(await provider.token(), "csrf-1");
|
|
provider.invalidate();
|
|
assert.equal(await provider.token(), "csrf-2");
|
|
assert.equal(executions, 2);
|
|
});
|
|
|
|
test("fails loudly instead of recursing when execute re-enters the provider before its first request settles", async () => {
|
|
let provider!: CsrfTokenProvider;
|
|
provider = createCsrfTokenProvider({
|
|
async execute() {
|
|
// Reproduces the shape of the C1 cycle directly: the operation that
|
|
// issues the token itself asks this same provider for the token,
|
|
// synchronously re-entering `resolve()` before `inFlight` is assigned.
|
|
await provider.token();
|
|
return { csrfToken: "unreachable", csrfHeaderName: "X-CSRF-TOKEN" };
|
|
},
|
|
});
|
|
|
|
await assert.rejects(provider.token(), /re-entered/);
|
|
});
|