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
+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.