`contractOperations.execute` is the single executor every installed feature dispatches through, and it called `invalidateTechLogCsrfOnOutcome` for every operation. A 403 on an unrelated reference-feature request therefore threw away a perfectly good TechLog CSRF token, forcing an avoidable `getStudioSession` round trip on the next Studio operation -- and, when the session endpoint is itself unhealthy, turning someone else's authorization failure into a Studio outage. `invalidateTechLogCsrfOnOutcome` now takes the operation's auth profile and acts only on the two TechLog Studio profiles. Required, not optional, so the scoping cannot be dropped again by omission, and the predicate lives in the feature file: `bootstrap/runtime-adapters.ts` is template-synced and its change is the one added argument. The composition test now installs both contributions the way `installed-contract-contributions.ts` does, and asserts a reference-feature 403 leaves the cached token alone. Reverting the scope check fails exactly that test. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
644 lines
23 KiB
TypeScript
644 lines
23 KiB
TypeScript
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<typeof createHttpClient>[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<ReleaseManifest>;
|
|
host?: Record<string, unknown>;
|
|
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<ReleaseManifest>): 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<string, unknown> {
|
|
return Boolean(value) && typeof value === "object";
|
|
}
|
|
|
|
function externalOwnerFrom(
|
|
host: Record<string, unknown>,
|
|
): 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<string, unknown>,
|
|
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<string, unknown>;
|
|
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<string, unknown>,
|
|
): 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<string> = 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<string, unknown>);
|
|
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,
|
|
});
|
|
}
|