feat: add the TechLog asset multipart upload transport

Wires the whole Asset capability into the running application: the
multipart upload transport (the contract runtime can only express JSON
bodies), a single composition-root-owned CSRF provider shared between
the platform's credential collaborator (18 JSON operations) and the
upload transport (1 multipart operation), and Studio/StudioShell
exposure of the Asset gateway alongside the existing document gateway.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
DongHyeonka
2026-08-18 02:39:51 +09:00
co-authored by Claude Opus 5
parent d9c2d8bc5e
commit c9c832c365
15 changed files with 473 additions and 3 deletions
+67 -1
View File
@@ -27,6 +27,8 @@ import { createConditionalValidatorStore } from "../adapters/query-cache/conditi
import { createBrowserStorageAdapter } from "../adapters/storage/browser-storage-adapter.ts";
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 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";
import {
@@ -420,6 +422,43 @@ export async function createRuntimeAdapters(
location.reload();
},
});
/**
* §7.7 / Task 7. There is exactly one CSRF provider per Studio session, and
* it is owned by the composition root — not by the TechLog feature input —
* because two collaborators share it: `attachCredentials` below (the only
* path by which `x-csrf-token` reaches the 18 JSON operations) and the
* multipart upload transport, which bypasses the platform executor
* entirely and must set the header itself. If each built its own provider,
* one Studio session would hold two different tokens.
*
* `execute` calls `getStudioSession` through `contractOperations`, which is
* declared further below — a real ordering hazard, since `attachCredentials`
* (needed to build `contractHttp`, needed to build `contractOperations`)
* needs this provider first. `getStudioSession` is a SAFE operation and
* needs no CSRF itself, so there is no true cycle: the callback below only
* *runs* once the whole runtime is composed and a Studio request is made,
* by which point `contractOperations` is assigned. `let` plus a forward
* reference inside this closure defers the read to call time instead of
* declaration time.
*/
let contractOperations!: TechLogStudioOperationExecutor;
const techLogCsrf = createCsrfTokenProvider({
async execute(options) {
const outcome = await contractOperations.execute(
"getStudioSession",
{},
{
routeId: "TECH_LOG_STUDIO",
...(options?.signal ? { signal: options.signal } : {}),
},
);
if (outcome.kind !== "SUCCESS") {
throw new Error("studio session is unavailable");
}
const value = outcome.value as { csrfToken: string; csrfHeaderName: string };
return { csrfToken: value.csrfToken, csrfHeaderName: value.csrfHeaderName };
},
});
const contractHttp = createContractHttpExecutor({
baseUrl: config.API_BASE_URL,
maxRetryAttempts: config.MAX_RETRY_ATTEMPTS,
@@ -436,6 +475,25 @@ export async function createRuntimeAdapters(
if (serverStateScope.getPhase() !== "READY") {
return Object.freeze({ kind: "SCOPE_FENCED" as const });
}
if (operation.authProfileId === "TECH_LOG_STUDIO_SESSION") {
// Studio authenticates with a session cookie and carries only the
// CSRF token as a proof header. Read operations use this profile too
// — the server does not require the header for them — so a failure
// to fetch the token fails only this one request (`UNAVAILABLE`) and
// is not promoted to a session-level `UNAUTHENTICATED`, which would
// trigger a global re-authentication flow the session itself did not
// warrant.
try {
return Object.freeze({
kind: "READY" as const,
headers: Object.freeze({
"x-csrf-token": await techLogCsrf.token({ signal: authContext.signal }),
}),
});
} catch {
return Object.freeze({ kind: "UNAVAILABLE" as const });
}
}
const state = authSession.getState();
if (state === "integration-failed") {
return Object.freeze({ kind: "UNAVAILABLE" as const });
@@ -465,7 +523,7 @@ export async function createRuntimeAdapters(
},
observe: createHttpObservationProjector({ diagnostics, telemetry }),
});
const contractOperations = Object.freeze({
contractOperations = Object.freeze({
async execute(
operationId: string,
input: unknown,
@@ -499,6 +557,11 @@ 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();
}
return outcome;
},
@@ -506,6 +569,9 @@ export async function createRuntimeAdapters(
const featureInputs = createInstalledFeatureInputs({
contractOperations,
studioSource: config.TECH_LOG_STUDIO_SOURCE,
apiBaseUrl: config.API_BASE_URL,
requestTimeoutMs: config.REQUEST_TIMEOUT_MS,
csrf: techLogCsrf,
});
return Object.freeze({