fix: break the TechLog CSRF bootstrap cycle and close the review's fix-round-1 items

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>
This commit is contained in:
DongHyeonka
2026-08-18 03:07:02 +09:00
co-authored by Claude Opus 5
parent c9c832c365
commit 2cab4974b7
16 changed files with 793 additions and 55 deletions
@@ -9,6 +9,10 @@ import type {
import { TECH_LOG_FEATURE_ID } from "../application/tech-log-feature-input.ts";
import canonicalSource from "./studio/canonical-source.json" with { type: "json" };
import { STUDIO_ERROR_CODES } from "../adapters/http/studio-error-mapping.ts";
import {
TECH_LOG_STUDIO_BOOTSTRAP_AUTH_PROFILE_ID,
TECH_LOG_STUDIO_SESSION_AUTH_PROFILE_ID,
} from "../adapters/http/studio-session-credentials.ts";
function zodValidator<T>(schemaId: string, schema: z.ZodType<T>): RuntimeValidator<T> {
return Object.freeze({
@@ -75,6 +79,11 @@ function safeOperation(
pathTemplate: string,
responseByteLimit: number,
project: (input: never) => Readonly<{ pathValues: PathValues; queryEntries: QueryEntries }>,
// Task 7 fix round 1 (C1). `getStudioSession` issues the CSRF token, so it
// is the one operation that must NOT require one — it is the only caller
// of `TECH_LOG_STUDIO_BOOTSTRAP`. Every other safe/keyed operation keeps
// the default `TECH_LOG_STUDIO_SESSION`.
authProfileId: string = TECH_LOG_STUDIO_SESSION_AUTH_PROFILE_ID,
): InstalledHttpContract<unknown, unknown, unknown> {
return Object.freeze({
contract: Object.freeze({
@@ -102,7 +111,7 @@ function safeOperation(
responseByteLimit,
totalDeadlineMs: 10_000,
retryBudget: 2 as const,
authProfileId: "TECH_LOG_STUDIO_SESSION",
authProfileId,
diagnosticsOperation: `techLog.studio.${operationId}`,
}),
}) as InstalledHttpContract<unknown, unknown, unknown>;
@@ -151,7 +160,7 @@ function keyedOperation(
totalDeadlineMs: 10_000,
// §8.3. 발신된 KEYED 명령은 자동 재시도하지 않는다.
retryBudget: 0 as const,
authProfileId: "TECH_LOG_STUDIO_SESSION",
authProfileId: TECH_LOG_STUDIO_SESSION_AUTH_PROFILE_ID,
diagnosticsOperation: `techLog.studio.${operationId}`,
}),
}) as InstalledHttpContract<unknown, unknown, unknown>;
@@ -178,6 +187,7 @@ const GET_STUDIO_SESSION = safeOperation(
"/api/v1/studio/session",
8_192,
() => Object.freeze({ pathValues: NO_PATH, queryEntries: NO_QUERY }),
TECH_LOG_STUDIO_BOOTSTRAP_AUTH_PROFILE_ID,
);
const GET_STUDIO_DASHBOARD = safeOperation(