import { createDemoSessionAdapter, createExternalAuthSessionAdapter, createUnavailableSessionAdapter, type ExternalSessionOwner, } from "../adapters/auth/external-session-adapter.ts"; import { createDiagnosticsAdapter } from "../adapters/diagnostics/bounded-diagnostics.ts"; import { createHttpClient } from "../adapters/http/client.ts"; import { createContractHttpExecutor, type HttpExecutionObservation, } from "../adapters/http/http-execution-v3.ts"; import { attemptBucket, durationBucket, statusGroup, type DiagnosticRecordInput, } from "../contracts/diagnostics.ts"; import type { TelemetryEventName } from "../contracts/telemetry.ts"; import { createBrowserCrossContextInvalidationFromHost } from "../adapters/cross-context-invalidation/index.ts"; import { createTanStackCacheCoordinator, } from "../adapters/query-cache/tanstack-cache-coordinator.ts"; import { createQueryClient } from "../adapters/query-cache/tanstack-query-cache.ts"; import { createServerStateScopeRuntime } from "../adapters/query-cache/server-state-scope-runtime.ts"; import { createConditionalValidatorStore } from "../adapters/query-cache/conditional-validator-store.ts"; 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 { 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"; import { createRestProviderProfile, INSTALLED_REST_AUTH_PROFILES, } from "../contracts/rest-profiles.ts"; import type { ClockPort } from "../application/ports/clock-port.ts"; import type { MutationIntent } from "../contracts/mutation-intent.ts"; import { createInstalledFeatureInputs } from "../features/installed-feature-adapters.ts"; import { INVALIDATION_REGISTRY, INVALIDATION_TOPIC_VERSIONS, } from "../features/installed-feature-contracts.ts"; import { INSTALLED_RUNTIME_CAPABILITIES } from "../features/installed-runtime-capabilities.ts"; import { COMPILED_PRODUCT_FEATURE_IDS, INSTALLED_PRODUCT_FEATURE_IDS, } from "../features/installed-product-manifest.ts"; import { activeProductFeatureIds, resolveProductFeatures, } from "../contracts/product-features.ts"; import { describeRuntimeCapabilities } from "../contracts/runtime-capabilities.ts"; import { fetchReleaseManifest, type ReleaseManifest, } from "./load-release-manifest.ts"; import type { RuntimeConfigLoadResult } from "./load-runtime-config.ts"; import { createServerStateGenerationStore } from "./server-state-generation-store.ts"; import { COMPOSED_CONTRACT_CONTRIBUTIONS } from "../features/installed-contract-contributions.ts"; import { indexInvalidationRegistry, indexInvalidationTopicVersions, } from "../contracts/query-invalidation.ts"; type HttpClientDependencies = Parameters[0]; export type RuntimeHttpContract = Pick< HttpClientDependencies, "getOperation" | "validatePayload" | "validateRequest" | "mapPayload" >; type RuntimeHttpContext = Readonly<{ runtime: RuntimeConfigLoadResult; authSession: AuthSessionPort; fetcher?: typeof fetch; clock?: ClockPort; scheduler?: HttpClientDependencies["scheduler"]; diagnostics?: HttpClientDependencies["diagnostics"]; telemetry?: HttpClientDependencies["telemetry"]; }>; export type RuntimeAdaptersContext = Readonly<{ runtime: RuntimeConfigLoadResult; release: Readonly; host?: Record; fetcher?: typeof fetch; }>; /** * §5.2. The composition root is where a manifest becomes release info. A V2 * manifest states contract identity as a verified contract set and a V1 * manifest as the legacy scalar; the application layer sees one shape and never * branches on the schema version to find the identity. */ function toReleaseInfo(manifest: Readonly): ReleaseInfo { const { contractSet, legacyApiContractVersion, ...rest } = structuredClone(manifest); return Object.freeze({ ...rest, ...(legacyApiContractVersion === undefined ? {} : { apiContractVersion: legacyApiContractVersion }), ...(contractSet === null ? {} : { contractSetDigest: contractSet.setDigest }), }); } const EXTERNAL_OWNER_METHODS = Object.freeze([ "readState", "subscribe", "beginSignIn", "signOut", "attachCredential", "recoverSession", "notifyUnauthenticated", ] as const); function isRecord(value: unknown): value is Record { return Boolean(value) && typeof value === "object"; } function externalOwnerFrom( host: Record, ): ExternalSessionOwner | null { const candidate = host.__CA_FRONTEND_AUTH_OWNER__; if (!isRecord(candidate)) return null; return EXTERNAL_OWNER_METHODS.every( (name) => typeof candidate[name] === "function", ) ? (candidate as ExternalSessionOwner) : null; } function hostValue( host: Record, property: string, ): unknown { try { return Reflect.get(host, property); } catch { return undefined; } } function storageOrUndefined(value: unknown): Storage | undefined { if (!value || typeof value !== "object") return undefined; const candidate = value as Record; try { return ["getItem", "setItem", "removeItem"].every( (method) => typeof Reflect.get(candidate, method) === "function", ) ? (value as Storage) : undefined; } catch { return undefined; } } /** * Runtime-aware transport factory. Feature gateway composition calls this * factory when a registered API capability is installed. */ export function createRuntimeHttpClient( context: RuntimeHttpContext, contract: RuntimeHttpContract = {}, ) { return createHttpClient({ baseUrl: context.runtime.config.API_BASE_URL, timeoutMs: context.runtime.config.REQUEST_TIMEOUT_MS, maxRetryAttempts: context.runtime.config.MAX_RETRY_ATTEMPTS, authSession: context.authSession, providerProfile: createRestProviderProfile( "PRIMARY_API", context.runtime.config.API_BASE_URL, ["omit"], ), fetcher: context.fetcher, clock: context.clock, scheduler: context.scheduler, diagnostics: context.diagnostics, telemetry: context.telemetry, ...contract, }); } /** * VD-07. Exactly one diagnostic per logical V3 execution and exactly one * `api.request.failed` telemetry event per terminal non-abort failure. * * The projection is closed: only registered context keys and bucketed values * reach the sinks, and neither sink can change the HTTP outcome, because the * caller invokes this inside the executor's isolated observation boundary. */ export function createHttpObservationProjector( sinks: Readonly<{ diagnostics: Readonly<{ record(input: DiagnosticRecordInput): void }>; telemetry: Readonly<{ emit( eventName: TelemetryEventName, attributes: Record, ): void; }>; }>, ): (observation: HttpExecutionObservation) => void { return (observation) => { const safeAttributes = { route_id: observation.routeId, operation_id: observation.operationId, error_kind: observation.errorKind, http_status_group: statusGroup(observation.status), attempt_count_bucket: attemptBucket(observation.attemptCount), duration_bucket: durationBucket(observation.durationMs), }; try { sinks.diagnostics.record({ level: observation.outcome === "SUCCESS" ? "info" : "warn", eventId: "http.request.completed", context: { ...safeAttributes, operation: observation.diagnosticsOperation, outcome: observation.outcome, }, }); } catch { // Diagnostics cannot change a contract execution outcome. } if (!isTerminalNonAbortFailure(observation)) return; try { sinks.telemetry.emit("api.request.failed", { ...safeAttributes }); } catch { // Telemetry cannot change a contract execution outcome. } }; } /** * LIVE-05. Cancellation and scope fencing are caller- or generation-owned * decisions, not API failures: they produce a diagnostic once and never * `api.request.failed`. * * A `DEADLINE` owner is the opposite case. Nobody asked for it — the API did * not answer inside the contract's own budget — so excluding it would hide * exactly the outage this event exists to report. */ const CALLER_OWNED_CANCELLATION: ReadonlySet = new Set([ "CALLER", "ROUTE_TRANSITION", "SCOPE_FENCE", "APPLICATION_SHUTDOWN", ]); function isTerminalNonAbortFailure( observation: HttpExecutionObservation, ): boolean { if (observation.outcome === "SUCCESS") return false; if (observation.outcome === "CANCELLED") return false; if ( observation.cancellationOwner !== undefined && CALLER_OWNED_CANCELLATION.has(observation.cancellationOwner) ) { return false; } return !( observation.outcome === "CONTRACT_VIOLATION" && observation.errorKind === "SCOPE_FENCED" ); } export async function createRuntimeAdapters( context: RuntimeAdaptersContext, ) { const host = context.host ?? (globalThis as unknown as Record); const config = context.runtime.config; const mutationIntentFactory = createBrowserMutationIntentFactory(); const externalOwner = externalOwnerFrom(host); const authSession = config.AUTH_MODE === "demo" ? createDemoSessionAdapter() : externalOwner ? createExternalAuthSessionAdapter(externalOwner) : createUnavailableSessionAdapter(); const diagnostics = createDiagnosticsAdapter(); const telemetry = createTelemetryAdapter({ enabled: config.TELEMETRY_ENABLED, endpoint: config.TELEMETRY_ENDPOINT, fetcher: context.fetcher, onDrop(event) { const attributes = event.attributes && typeof event.attributes === "object" ? event.attributes : {}; diagnostics.record({ level: "warn", eventId: "telemetry.delivery.dropped", context: attributes, }); }, }); const invalidationIndex = indexInvalidationRegistry(INVALIDATION_REGISTRY); const invalidationTopicVersions = indexInvalidationTopicVersions( INVALIDATION_REGISTRY, INVALIDATION_TOPIC_VERSIONS, ); const conditionalValidators = createConditionalValidatorStore(); const serverStateGeneration = createServerStateGenerationStore(() => { const queryClient = createQueryClient({ diagnostics }); const crossContextInvalidation = createBrowserCrossContextInvalidationFromHost({ ...(context.host === undefined ? {} : { host: context.host }), cacheEpoch: `release.${context.release.releaseId}`, topics: [...invalidationTopicVersions].map(([topic, topicVersion]) => Object.freeze({ topic, topicVersion, }), ), observe(observation) { if ( observation.outcome !== "FAILED" && observation.outcome !== "DEGRADED" ) { return; } diagnostics.record({ level: "warn", eventId: "cache.operation.failed", context: { operation: observation.operation, outcome: observation.outcome, reason: observation.reason, }, }); }, }); const queryInvalidation = createTanStackCacheCoordinator({ queryClient, invalidationIndex, topicVersions: invalidationTopicVersions, crossContext: crossContextInvalidation, diagnostics, }); return Object.freeze({ queryClient, queryInvalidation, crossContextStatus: () => crossContextInvalidation?.getStatus() ?? "DEGRADED_LOCAL_ONLY", }); }); const serverStateScope = createServerStateScopeRuntime({ session: authSession, queryInvalidation: { resetLocal: () => serverStateGeneration.resetCurrent(), }, participants: [ { order: 4, label: "conditional-validators", close: () => conditionalValidators.clear(), }, ], activateNextGeneration: () => serverStateGeneration.activateNext(), }); const storage = createBrowserStorageAdapter({ localStorage: storageOrUndefined(hostValue(host, "localStorage")), sessionStorage: storageOrUndefined( hostValue(host, "sessionStorage"), ), diagnostics, }); const releaseInfo = Object.freeze({ async getCurrent() { return toReleaseInfo(context.release); }, async refresh() { return toReleaseInfo( await fetchReleaseManifest(config.RELEASE_MANIFEST_URL, { fetcher: context.fetcher, buildId: context.release.buildId, releaseId: context.release.releaseId, }), ); }, }); const runtimeCapabilities = Object.freeze({ getSnapshot() { return describeRuntimeCapabilities( INSTALLED_RUNTIME_CAPABILITIES, config.CAPABILITY_OVERRIDES, ); }, }); /** * §3.5. The two halves of the feature answer meet here and nowhere else: the * manifest says what the build compiled in, the runtime document says what is * switched off. Neither can add to the other. */ const productFeatureStatuses = resolveProductFeatures( COMPILED_PRODUCT_FEATURE_IDS, INSTALLED_PRODUCT_FEATURE_IDS, config.FEATURE_OVERRIDES, ); const activeFeatureIds = new Set( activeProductFeatureIds(productFeatureStatuses), ); const productFeatures = Object.freeze({ getSnapshot() { return productFeatureStatuses; }, isActive(featureId: string) { return activeFeatureIds.has(featureId); }, }); const navigation = Object.freeze({ reload() { const location = host.location; if (!isRecord(location) || typeof location.reload !== "function") { throw new Error("Browser reload is unavailable"); } 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 the CSRF header reaches the 17 JSON operations that * require it — every operation except `getStudioSession` itself) 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. * * Fix round 1 (C1). The original comment here claimed "`getStudioSession` * is a SAFE operation and needs no CSRF, so there is no true cycle" — that * was wrong. `retrySemantics: "SAFE"` and the auth profile's * `requiredCredentialHeaders` are orthogonal; every operation stamped with * `TECH_LOG_STUDIO_SESSION` (originally including `getStudioSession` * itself) required the CSRF header, so fetching the token required already * having it — an unconditional, deterministic cycle, reproduced against the * real composition root as unbounded recursion * (`RangeError: Maximum call stack size exceeded`), not a first-request * race. The real fix is `getStudioSession` now using the credential-free * `TECH_LOG_STUDIO_BOOTSTRAP` profile (`rest-profiles.ts`, * `tech-log-studio-contract-contribution.ts`), so its own * `attachCredentials` call never reaches this provider. `let` plus a * forward reference inside this closure still defers the read of * `contractOperations` to call time instead of declaration time — that part * of the original design was fine — but the binding below is a throwing * stub rather than a bare `let x!: T` definite-assignment assertion: `!` * silently accepts `undefined` forever if a future refactor inserts an * `await` between this declaration and the real assignment, where a stub * fails loudly instead. */ let contractOperations: TechLogStudioOperationExecutor = Object.freeze({ async execute() { throw new Error( "contractOperations used before assignment in createRuntimeAdapters — " + "construction order regressed.", ); }, }); 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, // §6.1. `REQUEST_TIMEOUT_MS` was declared, validated and then dropped on the // floor here: every V3 operation ran on its contract's own 10s deadline and // the deployment dial did nothing. It is a ceiling, so it can tighten an // operation but never loosen one. requestDeadlineCeilingMs: config.REQUEST_TIMEOUT_MS, fetcher: context.fetcher, // §7.7. The installed registry owns Fetch credentials and the exact // credential-header sets; this collaborator only supplies proof headers. authProfiles: INSTALLED_REST_AUTH_PROFILES, async attachCredentials(operation, authContext) { if (serverStateScope.getPhase() !== "READY") { return Object.freeze({ kind: "SCOPE_FENCED" as const }); } // Fix round 1 (C1, M4). Extracted to `studio-session-credentials.ts` so // this exact code — not a re-implementation of it — is what // `tests/features/tech-log/studio-csrf-composition.test.ts` exercises // against the real executor and provider. const studioOutcome = await attachStudioSessionCredentials( operation.authProfileId, authContext, techLogCsrf, ); if (studioOutcome) return studioOutcome; const state = authSession.getState(); if (state === "integration-failed") { return Object.freeze({ kind: "UNAVAILABLE" as const }); } if (state !== "authenticated") { return Object.freeze({ kind: "UNAUTHENTICATED" as const }); } try { const patch = await authSession.credentialPatch( { origin: new URL(config.API_BASE_URL).origin, method: operation.method, operationId: operation.operationId, }, authContext, ); if (serverStateScope.getPhase() !== "READY") { return Object.freeze({ kind: "SCOPE_FENCED" as const }); } return Object.freeze({ kind: "READY" as const, headers: patch.headers, }); } catch { return Object.freeze({ kind: "UNAVAILABLE" as const }); } }, observe: createHttpObservationProjector({ diagnostics, telemetry }), }); contractOperations = Object.freeze({ async execute( operationId: string, input: unknown, executionContext: Readonly<{ routeId: string; signal?: AbortSignal; intent?: MutationIntent; }>, ) { const operation = COMPOSED_CONTRACT_CONTRIBUTIONS.httpByOperationId.get(operationId); if (!operation) { return Object.freeze({ kind: "CONTRACT_VIOLATION" as const, effect: "NOT_STARTED" as const, violation: Object.freeze({ kind: "FINAL_REQUEST_INVARIANT_FAILED" as const, operation: "REQUEST" as const, }), }); } const outcome = await contractHttp.execute(operation, input, { routeId: executionContext.routeId, scope: serverStateScope.getSnapshot(), ...(executionContext.signal === undefined ? {} : { signal: executionContext.signal }), ...(executionContext.intent === undefined ? {} : { intent: executionContext.intent }), }); if (outcome.kind === "UNAUTHENTICATED") { authSession.onUnauthenticated(); } // 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`). // The auth profile scopes it to Studio operations: this executor serves // every installed feature, so without it an unrelated 403 discarded the // TechLog token. invalidateTechLogCsrfOnOutcome( outcome.kind, operation.frontend.authProfileId, techLogCsrf, ); return outcome; }, }); 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({ outputPorts: Object.freeze({ session: authSession, preferences: storage, diagnostics, telemetry, releaseInfo, runtimeCapabilities, productFeatures, navigation, }), infrastructure: Object.freeze({ get queryClient() { return serverStateGeneration.getSnapshot().queryClient; }, get queryInvalidation() { return serverStateGeneration.getSnapshot().queryInvalidation; }, serverStateGeneration, serverStateScope, mutationIntentFactory, conditionalValidators, crossContextInvalidationStatus: () => serverStateGeneration.getSnapshot().crossContextStatus(), dispose() { // N-04. Telemetry is torn down first: it must stop scheduling and // delivering before the diagnostics and state dependencies it observes // are destroyed. telemetry.dispose(); conditionalValidators.clear(); serverStateScope.dispose(); serverStateGeneration.dispose(); }, }), featureInputs, }); }