fix: close the live V3 authority findings from the adapter re-review

LIVE-01. A credential collaborator that returns UNAVAILABLE, throws, rejects
or answers off-contract is an outage of the auth integration, not evidence
about the user's session. Each of those now closes as AUTH_INTEGRATION_FAILURE
with zero fetches, so the composition root's logout path stays reserved for a
genuinely absent session. The synchronous and asynchronous failure sites share
one classifier.

LIVE-02 / LIVE-03. Object.freeze(new Map(...)) freezes the wrapper, not the
backing store, so an exported registry could still be cleared or replaced after
composition. Both the installed REST auth profile registry and the composed
HTTP/event lookups are now read facades over private stores, and every composed
row is an exact own-data snapshot that rejects accessors, inherited and
symbol-keyed fields.

LIVE-04. The total deadline now bounds the physical waits rather than being
checked between them: dispatch and response admission race the attempt signal,
the bounded reader takes that signal, and an abandoned operation is still
observed once so a late native rejection cannot surface unhandled. A body that
completes after the deadline or the caller owns the execution is no longer
admitted; a stale generation keeps its more specific SCOPE_FENCED verdict.

LIVE-05. DEADLINE is no longer treated as a caller-owned cancellation, so a
timeout reaches api.request.failed exactly once while caller, route, scope and
shutdown aborts stay excluded.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
DongHyeonka
2026-08-14 13:43:59 +09:00
co-authored by Claude Opus 5
parent 3b481eb4cf
commit f4bfdf0365
9 changed files with 2172 additions and 28 deletions
File diff suppressed because it is too large Load Diff
+34 -1
View File
@@ -28,9 +28,36 @@ export function declaredContentLength(response: Response): number | null {
return Number.isFinite(value) && value >= 0 ? value : null;
}
/**
* LIVE-04. A `read()` that never settles is a physical wait, so the reader
* accepts the operation's lifetime signal. A cooperative stream stops here; a
* non-cooperative one is abandoned with its reader cancelled, and the caller's
* own race against the same signal still bounds the public result.
*/
const READ_ABANDONED = Symbol("bounded-read-abandoned");
async function readOrAbandon(
reader: ReadableStreamDefaultReader<Uint8Array>,
signal: AbortSignal | undefined,
): Promise<ReadableStreamReadResult<Uint8Array> | typeof READ_ABANDONED> {
if (!signal) return reader.read();
if (signal.aborted) return READ_ABANDONED;
let onAbort: (() => void) | undefined;
const abandoned = new Promise<typeof READ_ABANDONED>((resolve) => {
onAbort = () => resolve(READ_ABANDONED);
signal.addEventListener("abort", onAbort, { once: true });
});
try {
return await Promise.race([reader.read(), abandoned]);
} finally {
if (onAbort) signal.removeEventListener("abort", onAbort);
}
}
export async function readBoundedBytes(
response: Response,
maximumBytes: number,
signal?: AbortSignal,
): Promise<BoundedBytesOutcome> {
const declared = declaredContentLength(response);
if (declared !== null && declared > maximumBytes) {
@@ -46,7 +73,13 @@ export async function readBoundedBytes(
let total = 0;
try {
for (;;) {
const next = await reader.read();
const next = await readOrAbandon(reader, signal);
if (next === READ_ABANDONED) {
// Never awaited: cancelling a stream whose source ignores its signal
// can itself hang, and the caller already owns the terminal result.
void reader.cancel().catch(() => {});
return failure("RESPONSE_STREAM_FAILURE");
}
if (next.done) break;
if (!next.value) continue;
total += next.value.byteLength;
+205 -16
View File
@@ -145,6 +145,15 @@ export type HttpExecutionOutcome<Value, Problem> =
*/
export type AuthIntegrationFailureReason =
| "UNKNOWN_AUTH_PROFILE"
/**
* LIVE-01. The collaborator answered that the auth system itself cannot serve
* this request. That is an outage of the integration, not a statement about
* the user's session, so it must never reach the composition root's logout
* path.
*/
| "CREDENTIAL_OWNER_UNAVAILABLE"
/** LIVE-01. The collaborator threw, rejected, or answered off-contract. */
| "CREDENTIAL_OWNER_FAILED"
| CredentialAdmissionFailure;
/**
@@ -567,6 +576,10 @@ export function createContractHttpExecutor(
// §7.7 / §8.4. Credentials are resolved before send. A response 401 is
// terminal; there is no hidden refresh-and-replay.
//
// LIVE-01. A synchronous throw and an asynchronous rejection are the same
// event seen from two call sites, so one classifier owns both. Neither is
// evidence about the user's session.
let patchOperation: Promise<CredentialPatchOutcome>;
try {
patchOperation = Promise.resolve(
@@ -583,12 +596,23 @@ export function createContractHttpExecutor(
),
);
} catch {
return finish(unauthenticated("NOT_STARTED", isCommand), "NOT_STARTED");
return finish(
authIntegrationFailure("CREDENTIAL_OWNER_FAILED", isCommand),
"AUTH_INTEGRATION_FAILURE",
);
}
let patchResult: CredentialPatchOutcome | typeof ABORTED;
try {
patchResult = await awaitWithAbort(
patchOperation,
lifetimeController.signal,
);
} catch {
return finish(
authIntegrationFailure("CREDENTIAL_OWNER_FAILED", isCommand),
"AUTH_INTEGRATION_FAILURE",
);
}
const patchResult = await awaitWithAbort(
patchOperation,
lifetimeController.signal,
);
if (patchResult === ABORTED) {
if (terminalCancellation === "SCOPE_FENCE") {
return finish(
@@ -604,13 +628,31 @@ export function createContractHttpExecutor(
);
}
const patch = patchResult;
if (patch.kind === "SCOPE_FENCED") {
if (patch?.kind === "SCOPE_FENCED") {
return finish(scopeFenced(preDispatchEffect(isCommand)), "NOT_STARTED");
}
if (patch.kind !== "READY") {
if (patch?.kind === "UNAUTHENTICATED") {
// A missing credential never downgrades into an anonymous request.
return finish(unauthenticated("NOT_STARTED", isCommand), "NOT_STARTED");
}
if (patch?.kind === "UNAVAILABLE") {
return finish(
authIntegrationFailure("CREDENTIAL_OWNER_UNAVAILABLE", isCommand),
"AUTH_INTEGRATION_FAILURE",
);
}
if (
patch?.kind !== "READY" ||
patch.headers === null ||
typeof patch.headers !== "object"
) {
// An off-contract answer is a collaborator breach, never a session
// verdict the caller may act on.
return finish(
authIntegrationFailure("CREDENTIAL_OWNER_FAILED", isCommand),
"AUTH_INTEGRATION_FAILURE",
);
}
// The idempotency key is contract-owned, so a credential owner supplying it
// stays the more specific request-contract violation.
@@ -733,6 +775,11 @@ export function createContractHttpExecutor(
let response: Response;
attemptState = "READY_TO_SEND";
// LIVE-04. The dispatch wait is raced against the attempt signal, which
// already carries the caller, the scope fence and the total deadline. A
// `fetch` that ignores its own `signal` therefore still cannot outlive
// the operation, and a response that lands late is drained, not admitted.
let dispatch: BoundedRace<Response>;
try {
attempts += 1;
const pending = fetcher(projected.request.url, init);
@@ -740,9 +787,18 @@ export function createContractHttpExecutor(
// D-01. Dispatch is the point of no return for the logical execution.
// No later retry may claim the command never started.
observeCertainty(certaintyForAbandonedAttempt("DISPATCHED", isCommand));
response = await pending;
attemptState = "RESPONSE_HEADERS";
dispatch = await raceTerminal(
pending,
controller.signal,
cancelResponseBody,
);
} catch {
dispatch = REJECTED_RACE;
}
if (dispatch.kind === "VALUE") {
response = dispatch.value;
attemptState = "RESPONSE_HEADERS";
} else {
clearTimeout(deadlineTimer);
lifetimeController.signal.removeEventListener("abort", forwardLifetime);
const owner = terminalCancellation;
@@ -792,14 +848,58 @@ export function createContractHttpExecutor(
}
try {
const outcome = await admitResponse(
operation,
response,
context,
attemptState,
readResponseBytes,
// LIVE-04. Response admission reads a body, so it is a physical wait
// too. It is bounded by the same signal, the reader is handed that
// signal so a cooperative stream stops early, and an admission that
// completes after the terminal owner fired is discarded.
const admission = await raceTerminal(
admitResponse(
operation,
response,
context,
attemptState,
readResponseBytes,
controller.signal,
),
controller.signal,
() => cancelResponseBody(response),
);
// LIVE-04. Once response headers are in hand the request demonstrably
// reached the server, so a terminal owner that lands during admission
// keeps the dispatched classification: a stale generation stays the
// `SCOPE_FENCED` contract violation it has always been, and only the
// deadline and the caller reclassify the outcome.
const abandonAdmission = ():
| HttpExecutionOutcome<WireOutput, Problem>
| null => {
switch (terminalCancellation) {
case "DEADLINE":
return finish(abandonedTransportFailure("TIMEOUT"), "TIMEOUT");
case "CALLER":
return finish(cancelled(abandonedCertainty()), "CANCELLED");
case "SCOPE_FENCE":
return finish(
scopeFenced(abandonedCertainty()),
"SCOPE_FENCED",
);
default:
return null;
}
};
if (admission.kind !== "VALUE") {
cancelResponseBody(response);
return (
abandonAdmission() ??
finish(
abandonedTransportFailure("NETWORK_FAILURE"),
"NETWORK_FAILURE",
)
);
}
const outcome = admission.value;
attemptState = "SETTLED";
const abandoned = abandonAdmission();
if (abandoned) return abandoned;
if (
outcome.retryHint &&
retryIndex < retryCeiling &&
@@ -867,6 +967,7 @@ async function admitResponse<Input, WireOutput, Problem>(
context: HttpExecutionContext,
attemptState: PhysicalAttemptState,
readResponseBytes: typeof readBoundedBytes,
signal: AbortSignal,
): Promise<AdmissionOutcome<WireOutput, Problem>> {
const contract = operation.contract;
const policy = operation.frontend;
@@ -929,6 +1030,7 @@ async function admitResponse<Input, WireOutput, Problem>(
metadata,
attemptState,
readResponseBytes,
signal,
);
}
@@ -972,7 +1074,11 @@ async function admitResponse<Input, WireOutput, Problem>(
const emptyAllowed = contract.emptyBodyStatuses.includes(status);
const mediaOk = isJsonMediaType(response.headers.get("content-type"));
const bytes = await readResponseBytes(response, policy.responseByteLimit);
const bytes = await readResponseBytes(
response,
policy.responseByteLimit,
signal,
);
if (!bytes.ok) {
return settled(
bytes.code === "RESPONSE_TOO_LARGE"
@@ -1083,6 +1189,87 @@ async function admitResponse<Input, WireOutput, Problem>(
);
}
/**
* LIVE-04. The outcome of a physical wait that the operation's terminal signal
* bounds.
*
* `REJECTED` is kept distinct from `TERMINAL` on purpose: a collaborator's own
* rejection is evidence about the request, and forging it into a cancellation
* state would erase the reason the attempt actually failed.
*/
type BoundedRace<Value> =
| Readonly<{ kind: "VALUE"; value: Value }>
| Readonly<{ kind: "REJECTED" }>
| Readonly<{ kind: "TERMINAL" }>;
const TERMINAL_RACE: BoundedRace<never> = Object.freeze({
kind: "TERMINAL" as const,
});
const REJECTED_RACE: BoundedRace<never> = Object.freeze({
kind: "REJECTED" as const,
});
/**
* LIVE-04. Races a physical operation against the terminal signal so a
* non-cooperative `fetch` or reader cannot hold the port result open past the
* total deadline.
*
* Two properties matter beyond the race itself. A value that arrives while the
* terminal owner has already fired is *late*, so it is compensated rather than
* admitted. And the abandoned operation is still observed exactly once, so a
* late native rejection never surfaces as an unhandled rejection.
*/
async function raceTerminal<Value>(
operation: Promise<Value>,
signal: AbortSignal,
compensate: (value: Value) => void,
): Promise<BoundedRace<Value>> {
let landed: BoundedRace<Value> | null = null;
const settled: Promise<BoundedRace<Value>> = operation.then(
(value) => (landed = Object.freeze({ kind: "VALUE" as const, value })),
() => (landed = REJECTED_RACE),
);
const observeLate = () => {
void settled.then((outcome) => {
if (outcome.kind !== "VALUE") return;
try {
compensate(outcome.value);
} catch {
// Compensation is outside the execution authority.
}
});
};
let onAbort: (() => void) | undefined;
const terminal = new Promise<BoundedRace<Value>>((resolve) => {
if (signal.aborted) {
resolve(TERMINAL_RACE);
return;
}
onAbort = () => resolve(TERMINAL_RACE);
signal.addEventListener("abort", onAbort, { once: true });
});
try {
const winner = await Promise.race([settled, terminal]);
if (winner !== TERMINAL_RACE) return winner;
// The terminal owner reached the await first. Drain the microtask queue
// once so an operation that had *already* settled can still hand over its
// value: a microtask turn cannot be extended by a collaborator that has
// not settled, so a non-cooperative operation is still abandoned here.
for (let turn = 0; turn < 4 && landed === null; turn += 1) {
await Promise.resolve();
}
if (landed !== null) return landed;
observeLate();
return TERMINAL_RACE;
} finally {
if (onAbort) signal.removeEventListener("abort", onAbort);
}
}
function cancelResponseBody(response: Response): void {
void response.body?.cancel().catch(() => {});
}
const ABORTED = Symbol("http-operation-aborted");
async function awaitWithAbort<Value>(
@@ -1109,6 +1296,7 @@ async function admitProblem<Input, WireOutput, Problem>(
metadata: SafeResponseMetadata,
attemptState: PhysicalAttemptState,
readResponseBytes: typeof readBoundedBytes,
signal: AbortSignal,
): Promise<AdmissionOutcome<WireOutput, Problem>> {
const contract = operation.contract;
const isCommand = contract.commandEffect !== null;
@@ -1117,6 +1305,7 @@ async function admitProblem<Input, WireOutput, Problem>(
const bytes = await readResponseBytes(
response,
HTTP_EXECUTION_CEILINGS.problemResponseBytes,
signal,
);
if (!bytes.ok || isEffectivelyEmpty(bytes.bytes)) {
// An unclassifiable failure stays uncertain for a command.
+20 -3
View File
@@ -226,15 +226,32 @@ export function createHttpObservationProjector(
}
/**
* Cancellation and scope fencing are caller- or generation-owned decisions, not
* API failures. They produce a diagnostic once and never `api.request.failed`.
* 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) return false;
if (
observation.cancellationOwner !== undefined &&
CALLER_OWNED_CANCELLATION.has(observation.cancellationOwner)
) {
return false;
}
return !(
observation.outcome === "CONTRACT_VIOLATION" &&
observation.errorKind === "SCOPE_FENCED"
+110 -6
View File
@@ -8,6 +8,11 @@
*/
import { INSTALLED_REST_AUTH_PROFILES } from "./rest-profiles.ts";
import {
createReadOnlyRegistry,
exactOwnDataSnapshot,
type ReadOnlyRegistry,
} from "./read-only-registry.ts";
/** §7.3 hard ceilings. A contribution may lower these, never raise them. */
export const HTTP_EXECUTION_CEILINGS = Object.freeze({
@@ -480,14 +485,107 @@ function assertEventContract(
export type ComposedContractContributions = Readonly<{
contributions: readonly InstalledContractContribution[];
httpByOperationId: ReadonlyMap<
/**
* LIVE-03. Read facades over private stores. The executor resolves an
* operation on every request, so an exported `Map` would let any holder of
* the composed singleton delete or replace a validated row after boot.
*/
httpByOperationId: ReadOnlyRegistry<
string,
InstalledHttpContract<unknown, unknown, unknown>
>;
eventByType: ReadonlyMap<string, InstalledEventContract<unknown, unknown>>;
eventByType: ReadOnlyRegistry<
string,
InstalledEventContract<unknown, unknown>
>;
externalPackages: readonly InstalledContractPackageIdentity[];
}>;
const EXECUTION_POLICY_KEYS = [
"policyId",
"requestByteLimit",
"responseByteLimit",
"totalDeadlineMs",
"retryBudget",
"authProfileId",
"diagnosticsOperation",
] as const;
const HTTP_CONTRACT_KEYS = [
"operationId",
"method",
"pathTemplate",
"inputValidator",
"outputValidator",
"problemValidator",
"acceptedStatuses",
"emptyBodyStatuses",
"retrySemantics",
"requestBody",
"responseBody",
"commandRecovery",
"commandEffect",
"projectRequest",
] as const;
const COMMAND_RECOVERY_KEYS = [
"mode",
"operationIdentityField",
"inspectOperationId",
] as const;
/**
* LIVE-03. Composition is the last point at which a contribution row is
* trusted, so the registry keeps an exact own-data copy rather than the
* caller's object. A later mutation of the source — including one that swaps a
* deadline or a credential policy — cannot reach what the executor reads.
*
* Validators and the descriptor-owned `projectRequest` stay by reference: they
* are behaviour the contribution owns, not data this repository re-derives.
*/
function snapshotHttpContract(
installed: InstalledHttpContract<unknown, unknown, unknown>,
label: string,
): InstalledHttpContract<unknown, unknown, unknown> {
const reject = (detail: string): never => fail(`${label}: ${detail}`);
const frontend = exactOwnDataSnapshot<HttpExecutionPolicy>(
installed.frontend,
EXECUTION_POLICY_KEYS,
EXECUTION_POLICY_KEYS,
reject,
);
const source = exactOwnDataSnapshot<
InstalledHttpContract<unknown, unknown, unknown>["contract"]
>(installed.contract, HTTP_CONTRACT_KEYS, HTTP_CONTRACT_KEYS, reject);
const contract = Object.freeze({
...source,
acceptedStatuses: Object.freeze([...source.acceptedStatuses]),
emptyBodyStatuses: Object.freeze([...source.emptyBodyStatuses]),
commandRecovery:
source.commandRecovery === null
? null
: exactOwnDataSnapshot<CommandRecoveryDescriptor>(
source.commandRecovery,
COMMAND_RECOVERY_KEYS,
["mode", "operationIdentityField"],
reject,
),
});
return Object.freeze({ contract, frontend });
}
function snapshotEventContract(
event: InstalledEventContract<unknown, unknown>,
label: string,
): InstalledEventContract<unknown, unknown> {
return exactOwnDataSnapshot<InstalledEventContract<unknown, unknown>>(
event,
["eventType", "envelopeValidator", "payloadValidator"],
["eventType", "envelopeValidator", "payloadValidator"],
(detail) => fail(`${label}: ${detail}`),
);
}
/**
* §4.8–§4.9. The only place installed contributions become a runtime registry.
* Every bound is checked before composition; a violation stops the boot rather
@@ -564,7 +662,10 @@ export function composeContractContributions(
const operationId = installed.contract.operationId;
const previous = httpByOperationId.get(operationId);
if (previous) fail(`duplicate operation: ${operationId}`);
httpByOperationId.set(operationId, installed);
httpByOperationId.set(
operationId,
snapshotHttpContract(installed, `${featureId}/${operationId}`),
);
}
for (const event of contribution.events) {
@@ -572,7 +673,10 @@ export function composeContractContributions(
if (eventByType.has(event.eventType)) {
fail(`duplicate event type: ${event.eventType}`);
}
eventByType.set(event.eventType, event);
eventByType.set(
event.eventType,
snapshotEventContract(event, `${featureId}/${event.eventType}`),
);
}
}
@@ -582,8 +686,8 @@ export function composeContractContributions(
return Object.freeze({
contributions: Object.freeze([...contributions]),
httpByOperationId,
eventByType,
httpByOperationId: createReadOnlyRegistry(httpByOperationId),
eventByType: createReadOnlyRegistry(eventByType),
externalPackages: Object.freeze(externalPackages),
});
}
+80
View File
@@ -0,0 +1,80 @@
/**
* LIVE-02 / LIVE-03. A composed registry is authority, not data.
*
* `Object.freeze(new Map(...))` only freezes the wrapper object: `set`,
* `delete` and `clear` still reach the backing store, so anything holding the
* exported singleton can empty a validated registry after composition and
* silently change what every later request resolves. The fix is structural —
* the store stays private in a closure and only read operations are exported.
*
* The facade is deliberately *not* a `Map` instance, so borrowing a mutator
* (`Map.prototype.clear.call(facade)`) fails on the missing internal slot
* rather than succeeding.
*/
export type ReadOnlyRegistry<Key, Value> = Readonly<{
get(key: Key): Value | undefined;
has(key: Key): boolean;
keys(): IterableIterator<Key>;
values(): IterableIterator<Value>;
entries(): IterableIterator<readonly [Key, Value]>;
forEach(visit: (value: Value, key: Key) => void): void;
readonly size: number;
[Symbol.iterator](): IterableIterator<readonly [Key, Value]>;
}>;
export function createReadOnlyRegistry<Key, Value>(
entries: Iterable<readonly [Key, Value]>,
): ReadOnlyRegistry<Key, Value> {
const store = new Map<Key, Value>(entries as Iterable<[Key, Value]>);
const facade = {
get: (key: Key) => store.get(key),
has: (key: Key) => store.has(key),
keys: () => store.keys(),
values: () => store.values(),
entries: () => store.entries(),
forEach: (visit: (value: Value, key: Key) => void) => {
for (const [key, value] of store) visit(value, key);
},
get size() {
return store.size;
},
[Symbol.iterator]: () => store.entries(),
};
return Object.freeze(facade) as ReadOnlyRegistry<Key, Value>;
}
/**
* Rejects anything that is not an exact own-data record over `allowedKeys`.
*
* A validated row must survive the validation: an accessor re-runs on every
* later read, an inherited field can be replaced through the prototype, and a
* symbol-keyed field escapes a name-based sweep entirely. Only own data
* descriptors are copied, and the result is frozen.
*/
export function exactOwnDataSnapshot<Shape extends object>(
source: unknown,
allowedKeys: readonly (keyof Shape & string)[],
requiredKeys: readonly (keyof Shape & string)[],
onViolation: (detail: string) => never,
): Readonly<Shape> {
if (source === null || typeof source !== "object") {
onViolation("object required");
}
if (Object.getOwnPropertySymbols(source).length > 0) {
onViolation("symbol-keyed field");
}
const allowed = new Set<string>(allowedKeys);
const snapshot: Record<string, unknown> = {};
for (const key of Object.getOwnPropertyNames(source)) {
if (!allowed.has(key)) onViolation(`unexpected field ${key}`);
const descriptor = Object.getOwnPropertyDescriptor(source, key);
if (!descriptor || !("value" in descriptor)) {
onViolation(`accessor field ${key}`);
}
snapshot[key] = descriptor.value;
}
for (const key of requiredKeys) {
if (!Object.hasOwn(snapshot, key)) onViolation(`missing field ${key}`);
}
return Object.freeze(snapshot) as Readonly<Shape>;
}
+15 -2
View File
@@ -1,3 +1,8 @@
import {
createReadOnlyRegistry,
type ReadOnlyRegistry,
} from "./read-only-registry.ts";
export type FetchCredentialsMode = "omit" | "same-origin" | "include";
export type RestProviderProfile = Readonly<{
@@ -34,7 +39,15 @@ export type RestAuthProfile = Readonly<{
requiredCredentialHeaders: readonly CredentialHeaderName[];
}>;
export type InstalledRestAuthProfiles = ReadonlyMap<string, RestAuthProfile>;
/**
* LIVE-02. A read facade over a private store, never a `Map`. The executor
* resolves a profile on every request, so a post-installation `clear()` would
* otherwise turn every authenticated call into `UNKNOWN_AUTH_PROFILE`.
*/
export type InstalledRestAuthProfiles = ReadOnlyRegistry<
string,
RestAuthProfile
>;
export type RestCsrfProfile = Readonly<{
csrfProfileId: string;
@@ -166,7 +179,7 @@ export function installRestAuthProfileRegistry(
if (installed.size === 0) {
throw new TypeError("REST auth profile registry cannot be empty.");
}
return Object.freeze(new Map(installed)) as InstalledRestAuthProfiles;
return createReadOnlyRegistry(installed);
}
/** The single installed registry every composition root shares. */
@@ -0,0 +1,412 @@
import { describe, expect, it, vi } from "vitest";
import {
createContractHttpExecutor,
type HttpExecutionObservation,
} from "../../src/adapters/http/http-execution-v3.ts";
import { createHttpObservationProjector } from "../../src/bootstrap/runtime-adapters.ts";
import { installRestAuthProfileRegistry } from "../../src/contracts/rest-profiles.ts";
import { TEST_LIST_HTTP_CONTRACT } from "../helpers/external-contract-fixture.ts";
const ROUTE_ID = "TEST_ROUTE";
const TEST_PROFILES = installRestAuthProfileRegistry({
TEST_AUTH: {
authProfileId: "TEST_AUTH",
transport: "BEARER_HEADER",
credentials: "omit",
allowedCredentialHeaders: ["authorization"],
requiredCredentialHeaders: ["authorization"],
},
});
function scopeSnapshot(signal: AbortSignal = new AbortController().signal) {
return Object.freeze({
generation: 1,
fingerprint: "scope-1",
identities: Object.freeze({}) as never,
signal,
isCurrent: () => true,
});
}
function bearerOperation(deadlineMs = 10_000) {
return {
...TEST_LIST_HTTP_CONTRACT,
frontend: {
...TEST_LIST_HTTP_CONTRACT.frontend,
authProfileId: "TEST_AUTH",
totalDeadlineMs: deadlineMs,
},
};
}
function jsonResponse(body: unknown): Response {
return new Response(JSON.stringify(body), {
status: 200,
headers: { "content-type": "application/json" },
});
}
/**
* LIVE-01. A credential collaborator that is broken, unavailable or throwing is
* an integration failure of the auth system. Reporting it as `UNAUTHENTICATED`
* makes the composition root run its logout path, so an auth outage would sign
* every user out.
*/
describe("LIVE-01 credential integration failures are not user session failures", () => {
const brokenOwners = [
{
label: "returns UNAVAILABLE",
attach: () => Object.freeze({ kind: "UNAVAILABLE" as const }),
},
{
label: "throws synchronously",
attach: () => {
throw new Error("credential owner exploded");
},
},
{
label: "rejects asynchronously",
attach: () => Promise.reject(new Error("credential owner exploded")),
},
{
label: "returns a malformed outcome",
attach: () => ({ kind: "TOTALLY_UNKNOWN" }) as never,
},
];
for (const owner of brokenOwners) {
it(`closes as AUTH_INTEGRATION_FAILURE when the owner ${owner.label}`, async () => {
const fetcher = vi.fn(async () => jsonResponse([]));
const executor = createContractHttpExecutor({
baseUrl: "https://api.example/",
maxRetryAttempts: 0,
authProfiles: TEST_PROFILES,
attachCredentials: owner.attach,
fetcher: fetcher as unknown as typeof fetch,
});
const outcome = await executor.execute(
bearerOperation(),
{ limit: 1 },
{ routeId: ROUTE_ID, scope: scopeSnapshot() },
);
expect(outcome.kind).toBe("AUTH_INTEGRATION_FAILURE");
expect(outcome.effect).toBe("NOT_APPLICABLE");
expect(fetcher).toHaveBeenCalledTimes(0);
});
}
it("still reports a real absent session as UNAUTHENTICATED", async () => {
const fetcher = vi.fn(async () => jsonResponse([]));
const executor = createContractHttpExecutor({
baseUrl: "https://api.example/",
maxRetryAttempts: 0,
authProfiles: TEST_PROFILES,
attachCredentials: () =>
Object.freeze({ kind: "UNAUTHENTICATED" as const }),
fetcher: fetcher as unknown as typeof fetch,
});
const outcome = await executor.execute(
bearerOperation(),
{ limit: 1 },
{ routeId: ROUTE_ID, scope: scopeSnapshot() },
);
expect(outcome.kind).toBe("UNAUTHENTICATED");
expect(fetcher).toHaveBeenCalledTimes(0);
});
});
/**
* LIVE-04. The total deadline must bound the physical wait, not merely be
* checked between awaits. A non-cooperative `fetch` or body reader that ignores
* the abort signal cannot hold the port result open, and a value that arrives
* after the deadline already owns the execution must not be admitted.
*/
describe("LIVE-04 the total deadline owns every physical wait", () => {
it("does not wait for a non-cooperative fetch past the deadline", async () => {
const executor = createContractHttpExecutor({
baseUrl: "https://api.example/",
maxRetryAttempts: 0,
authProfiles: TEST_PROFILES,
attachCredentials: () =>
Object.freeze({
kind: "READY" as const,
headers: { authorization: "Bearer t" },
}),
fetcher: (() => new Promise<Response>(() => {})) as unknown as typeof fetch,
});
const outcome = await executor.execute(
bearerOperation(5),
{ limit: 1 },
{ routeId: ROUTE_ID, scope: scopeSnapshot() },
);
expect(outcome.kind).toBe("TRANSPORT_FAILURE");
expect(
outcome.kind === "TRANSPORT_FAILURE" ? outcome.failure.kind : null,
).toBe("TIMEOUT");
});
it("does not wait for a non-cooperative body reader past the deadline", async () => {
const neverEndingBody = new ReadableStream<Uint8Array>({
pull() {
return new Promise<void>(() => {});
},
});
const executor = createContractHttpExecutor({
baseUrl: "https://api.example/",
maxRetryAttempts: 0,
authProfiles: TEST_PROFILES,
attachCredentials: () =>
Object.freeze({
kind: "READY" as const,
headers: { authorization: "Bearer t" },
}),
fetcher: (async () =>
new Response(neverEndingBody, {
status: 200,
headers: { "content-type": "application/json" },
})) as unknown as typeof fetch,
});
const outcome = await executor.execute(
bearerOperation(20),
{ limit: 1 },
{ routeId: ROUTE_ID, scope: scopeSnapshot() },
);
expect(outcome.kind).toBe("TRANSPORT_FAILURE");
expect(
outcome.kind === "TRANSPORT_FAILURE" ? outcome.failure.kind : null,
).toBe("TIMEOUT");
});
it("does not admit a body that completes after the deadline owns the execution", async () => {
let releaseBody: (() => void) | undefined;
const lateBody = new ReadableStream<Uint8Array>({
pull(controller) {
return new Promise<void>((resolve) => {
releaseBody = () => {
// The executor is expected to have cancelled this reader already;
// enqueueing into the closed controller then throws, which is the
// late producer this scenario is about.
try {
controller.enqueue(new TextEncoder().encode("[]"));
controller.close();
} catch {
// The stream was already cancelled by the deadline owner.
}
resolve();
};
});
},
});
const executor = createContractHttpExecutor({
baseUrl: "https://api.example/",
maxRetryAttempts: 0,
authProfiles: TEST_PROFILES,
attachCredentials: () =>
Object.freeze({
kind: "READY" as const,
headers: { authorization: "Bearer t" },
}),
fetcher: (async () =>
new Response(lateBody, {
status: 200,
headers: { "content-type": "application/json" },
})) as unknown as typeof fetch,
});
const pending = executor.execute(
bearerOperation(10),
{ limit: 1 },
{ routeId: ROUTE_ID, scope: scopeSnapshot() },
);
setTimeout(() => releaseBody?.(), 40);
const outcome = await pending;
expect(outcome.kind).toBe("TRANSPORT_FAILURE");
expect(
outcome.kind === "TRANSPORT_FAILURE" ? outcome.failure.kind : null,
).toBe("TIMEOUT");
});
it("preserves the caller and the scope as distinct cancellation owners", async () => {
const observations: HttpExecutionObservation[] = [];
const makeExecutor = () =>
createContractHttpExecutor({
baseUrl: "https://api.example/",
maxRetryAttempts: 0,
authProfiles: TEST_PROFILES,
attachCredentials: () =>
Object.freeze({
kind: "READY" as const,
headers: { authorization: "Bearer t" },
}),
fetcher: (() =>
new Promise<Response>(() => {})) as unknown as typeof fetch,
observe: (observation) => observations.push(observation),
});
const callerController = new AbortController();
const callerPending = makeExecutor().execute(
bearerOperation(10_000),
{ limit: 1 },
{
routeId: ROUTE_ID,
scope: scopeSnapshot(),
signal: callerController.signal,
},
);
callerController.abort();
expect((await callerPending).kind).toBe("CANCELLED");
expect(observations.at(-1)?.cancellationOwner).toBe("CALLER");
const scopeController = new AbortController();
const scopePending = makeExecutor().execute(
bearerOperation(10_000),
{ limit: 1 },
{ routeId: ROUTE_ID, scope: scopeSnapshot(scopeController.signal) },
);
scopeController.abort();
expect((await scopePending).kind).toBe("TRANSPORT_FAILURE");
expect(observations.at(-1)?.cancellationOwner).toBe("SCOPE_FENCE");
});
it("observes a late native rejection without an unhandled rejection", async () => {
const rejections: unknown[] = [];
const onUnhandled = (event: PromiseRejectionEvent) => {
rejections.push(event.reason);
event.preventDefault();
};
globalThis.addEventListener?.(
"unhandledrejection",
onUnhandled as EventListener,
);
try {
const executor = createContractHttpExecutor({
baseUrl: "https://api.example/",
maxRetryAttempts: 0,
authProfiles: TEST_PROFILES,
attachCredentials: () =>
Object.freeze({
kind: "READY" as const,
headers: { authorization: "Bearer t" },
}),
fetcher: (() =>
new Promise<Response>((_resolve, reject) => {
setTimeout(() => reject(new Error("late native failure")), 30);
})) as unknown as typeof fetch,
});
const outcome = await executor.execute(
bearerOperation(5),
{ limit: 1 },
{ routeId: ROUTE_ID, scope: scopeSnapshot() },
);
expect(outcome.kind).toBe("TRANSPORT_FAILURE");
await new Promise((resolve) => setTimeout(resolve, 60));
expect(rejections).toEqual([]);
} finally {
globalThis.removeEventListener?.(
"unhandledrejection",
onUnhandled as EventListener,
);
}
});
});
/**
* LIVE-05. A deadline TIMEOUT is an operational failure of the API call, not a
* caller decision. Excluding it from `api.request.failed` hides exactly the
* class of outage the telemetry exists to surface.
*/
describe("LIVE-05 deadline timeouts reach failure telemetry", () => {
function projectorHarness() {
const emitted: string[] = [];
const recorded: string[] = [];
const project = createHttpObservationProjector({
diagnostics: {
record: (input) => recorded.push(input.eventId),
},
telemetry: {
emit: (eventName) => emitted.push(eventName),
},
});
return { emitted, recorded, project };
}
const base = Object.freeze({
routeId: ROUTE_ID,
operationId: "TEST_LIST_ENTITIES",
diagnosticsOperation: "test.read",
errorKind: "TIMEOUT",
attemptCount: 1,
durationMs: 10,
effect: "NOT_APPLICABLE" as const,
terminalReason: "TIMEOUT",
});
it("emits exactly one api.request.failed for a deadline timeout", () => {
const harness = projectorHarness();
harness.project(
Object.freeze({
...base,
outcome: "TRANSPORT_FAILURE" as const,
cancellationOwner: "DEADLINE" as const,
}),
);
expect(harness.emitted).toEqual(["api.request.failed"]);
expect(harness.recorded).toEqual(["http.request.completed"]);
});
it("emits nothing for caller, route and shutdown cancellation", () => {
for (const owner of [
"CALLER",
"ROUTE_TRANSITION",
"SCOPE_FENCE",
"APPLICATION_SHUTDOWN",
] as const) {
const harness = projectorHarness();
harness.project(
Object.freeze({
...base,
outcome: "CANCELLED" as const,
errorKind: "REQUEST_ABORTED",
cancellationOwner: owner,
}),
);
expect(harness.emitted).toEqual([]);
}
});
it("still emits for an ordinary network and auth integration failure", () => {
const network = projectorHarness();
network.project(
Object.freeze({
...base,
outcome: "TRANSPORT_FAILURE" as const,
errorKind: "NETWORK_FAILURE",
terminalReason: "NETWORK_FAILURE",
}),
);
expect(network.emitted).toEqual(["api.request.failed"]);
const auth = projectorHarness();
auth.project(
Object.freeze({
...base,
outcome: "AUTH_INTEGRATION_FAILURE" as const,
errorKind: "CREDENTIAL_OWNER_FAILED",
terminalReason: "AUTH_INTEGRATION_FAILURE",
}),
);
expect(auth.emitted).toEqual(["api.request.failed"]);
});
});
@@ -0,0 +1,172 @@
import { describe, expect, it } from "vitest";
import {
composeContractContributions,
type ComposedContractContributions,
} from "../../src/contracts/external-contract-runtime.ts";
import {
installRestAuthProfileRegistry,
INSTALLED_REST_AUTH_PROFILES,
REST_AUTH_PROFILES,
} from "../../src/contracts/rest-profiles.ts";
import { TEST_CONTRACT_CONTRIBUTION } from "../helpers/external-contract-fixture.ts";
/**
* LIVE-02 / LIVE-03. `Object.freeze(new Map(...))` freezes the wrapper object,
* not the backing store: `set`, `delete` and `clear` keep working. Every
* registry the executor consults after composition must therefore be a read
* facade over a private store, and the rows it hands back must be exact
* own-data snapshots that a later mutation of the source cannot reach.
*/
const MUTATORS = ["set", "delete", "clear"] as const;
function borrowedMapMutation(
facade: unknown,
mutator: (typeof MUTATORS)[number],
): "THREW" | "MUTATED" {
try {
switch (mutator) {
case "set":
Map.prototype.set.call(facade as never, "INJECTED", {} as never);
break;
case "delete":
Map.prototype.delete.call(facade as never, "ANONYMOUS");
break;
case "clear":
Map.prototype.clear.call(facade as never);
break;
}
return "MUTATED";
} catch {
return "THREW";
}
}
describe("LIVE-02 installed REST auth profile registry", () => {
it("exposes no mutation API", () => {
const registry = INSTALLED_REST_AUTH_PROFILES as unknown as Record<
string,
unknown
>;
for (const mutator of MUTATORS) {
expect(registry[mutator]).toBeUndefined();
}
});
it("survives a cast mutation and a borrowed Map.prototype mutator", () => {
const registry = installRestAuthProfileRegistry(REST_AUTH_PROFILES);
const before = registry.size;
const identity = registry.get("ANONYMOUS");
expect(identity).toBeDefined();
for (const mutator of MUTATORS) {
expect(borrowedMapMutation(registry, mutator)).toBe("THREW");
}
expect(registry.size).toBe(before);
expect(registry.get("ANONYMOUS")).toBe(identity);
expect(registry.get("REFERENCE_EXTERNAL_BEARER")?.credentials).toBe("omit");
});
it("does not observe a post-installation mutation of the source record", () => {
const source: Record<string, (typeof REST_AUTH_PROFILES)["ANONYMOUS"]> = {
ANONYMOUS: {
authProfileId: "ANONYMOUS",
transport: "ANONYMOUS",
credentials: "omit",
allowedCredentialHeaders: [],
requiredCredentialHeaders: [],
},
};
const registry = installRestAuthProfileRegistry(source);
delete source.ANONYMOUS;
expect(registry.get("ANONYMOUS")?.transport).toBe("ANONYMOUS");
});
it("keeps read APIs the executor depends on", () => {
const registry = INSTALLED_REST_AUTH_PROFILES;
expect(registry.has("ANONYMOUS")).toBe(true);
expect(registry.has("NO_SUCH_PROFILE")).toBe(false);
expect([...registry.keys()].sort()).toEqual([
"ANONYMOUS",
"REFERENCE_EXTERNAL_BEARER",
]);
expect([...registry.entries()].length).toBe(registry.size);
expect([...registry.values()].length).toBe(registry.size);
});
});
describe("LIVE-03 composed contract registry", () => {
function compose(): ComposedContractContributions {
return composeContractContributions([TEST_CONTRACT_CONTRIBUTION] as never);
}
it("exposes no mutation API on either lookup", () => {
const composed = compose();
for (const facade of [composed.httpByOperationId, composed.eventByType]) {
const record = facade as unknown as Record<string, unknown>;
for (const mutator of MUTATORS) {
expect(record[mutator]).toBeUndefined();
}
for (const mutator of MUTATORS) {
expect(borrowedMapMutation(facade, mutator)).toBe("THREW");
}
}
expect(composed.httpByOperationId.size).toBeGreaterThan(0);
});
it("snapshots the frontend policy so a later source mutation cannot reach it", () => {
const mutablePolicy = {
...TEST_CONTRACT_CONTRIBUTION.http[0]!.frontend,
};
const contribution = {
...TEST_CONTRACT_CONTRIBUTION,
http: [
{
...TEST_CONTRACT_CONTRIBUTION.http[0]!,
frontend: mutablePolicy,
},
],
};
const composed = composeContractContributions([contribution] as never);
const operationId = [...composed.httpByOperationId.keys()][0]!;
const installedBefore =
composed.httpByOperationId.get(operationId)!.frontend.totalDeadlineMs;
mutablePolicy.totalDeadlineMs = 999_999;
expect(
composed.httpByOperationId.get(operationId)!.frontend.totalDeadlineMs,
).toBe(installedBefore);
expect(installedBefore).not.toBe(999_999);
});
it("rejects an accessor or inherited policy field", () => {
const inherited = Object.create({ diagnosticsOperation: "INHERITED" }) as
Record<string, unknown>;
for (const [key, value] of Object.entries(
TEST_CONTRACT_CONTRIBUTION.http[0]!.frontend,
)) {
if (key === "diagnosticsOperation") continue;
inherited[key] = value;
}
const accessor = { ...TEST_CONTRACT_CONTRIBUTION.http[0]!.frontend };
Object.defineProperty(accessor, "totalDeadlineMs", {
configurable: true,
enumerable: true,
get: () => 10_000,
});
for (const frontend of [inherited, accessor]) {
expect(() =>
composeContractContributions([
{
...TEST_CONTRACT_CONTRIBUTION,
http: [{ ...TEST_CONTRACT_CONTRIBUTION.http[0]!, frontend }],
},
] as never),
).toThrow();
}
});
});