Three trust boundaries checked a caller's object and then read it again to use it. Between those two reads an accessor or a Proxy can answer differently, so the value that passed validation and the value that was installed were not the same value. A credential owner's answer was read field by field outside the auth boundary: a throwing `kind` getter escaped into the transport catch and an auth outage reached operators as `NETWORK_FAILURE`. Contract composition validated a contribution and then copied it, so a policy that answered 10,000 to the ceiling check and 999,999 to the copy installed the second value. The cursor runtime validated its profile once and re-read it on every page, so raising `maxPages` after construction widened a cap that had already been checked. `src/contracts/exact-snapshot.ts` is the one descriptor-based decoder they now share: every property is read exactly once, an accessor, a symbol, an inherited or non-enumerable field and a throwing trap all resolve to a typed failure, and validation runs on the owned copy. Separately, the `responseBody: NONE` probe awaited a bare `read()`. The deadline produced a bounded public result while the raw reader kept its lease, so the body stayed locked and the outer compensator could not cancel it. The probe now takes the operation lifetime and owns the cancel and the lock release itself. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
338 lines
11 KiB
TypeScript
338 lines
11 KiB
TypeScript
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();
|
|
}
|
|
});
|
|
|
|
/**
|
|
* NS-02. Composition validated the caller's object and then read it again to
|
|
* copy it. Between those two reads a stateful answer could swap a deadline or
|
|
* a retry budget, so the value that passed the ceiling check and the value the
|
|
* executor ran with were two different things.
|
|
*/
|
|
describe("validate the snapshot, never the source", () => {
|
|
/** Answers a safe value to a plain read and a hostile one to a copy. */
|
|
const statefulFrontend = () =>
|
|
new Proxy(
|
|
{ ...TEST_CONTRACT_CONTRIBUTION.http[0]!.frontend },
|
|
{
|
|
get(target, key, receiver) {
|
|
if (key === "totalDeadlineMs") return 10_000;
|
|
return Reflect.get(target, key, receiver);
|
|
},
|
|
getOwnPropertyDescriptor(target, key) {
|
|
if (key === "totalDeadlineMs") {
|
|
return {
|
|
configurable: true,
|
|
enumerable: true,
|
|
value: 999_999,
|
|
};
|
|
}
|
|
return Reflect.getOwnPropertyDescriptor(target, key);
|
|
},
|
|
},
|
|
);
|
|
|
|
const contributionWith = (row: unknown) => [
|
|
{ ...TEST_CONTRACT_CONTRIBUTION, http: [row] },
|
|
];
|
|
|
|
it("validates the deadline it will install, not the one it was shown", () => {
|
|
// The copied value exceeds the hard ceiling, so composition must stop
|
|
// rather than install a deadline no check ever saw.
|
|
expect(() =>
|
|
composeContractContributions(
|
|
contributionWith({
|
|
...TEST_CONTRACT_CONTRIBUTION.http[0]!,
|
|
frontend: statefulFrontend(),
|
|
}) as never,
|
|
),
|
|
).toThrow();
|
|
});
|
|
|
|
it("keys the registry by the operation id it copied, not the one it was shown", () => {
|
|
const base = { ...TEST_CONTRACT_CONTRIBUTION.http[0]!.contract };
|
|
const contract = new Proxy(base, {
|
|
get(target, key, receiver) {
|
|
if (key === "operationId") return base.operationId;
|
|
return Reflect.get(target, key, receiver);
|
|
},
|
|
getOwnPropertyDescriptor(target, key) {
|
|
if (key === "operationId") {
|
|
return {
|
|
configurable: true,
|
|
enumerable: true,
|
|
value: "SwappedOperation",
|
|
};
|
|
}
|
|
return Reflect.getOwnPropertyDescriptor(target, key);
|
|
},
|
|
});
|
|
const composed = composeContractContributions(
|
|
contributionWith({
|
|
...TEST_CONTRACT_CONTRIBUTION.http[0]!,
|
|
contract,
|
|
}) as never,
|
|
);
|
|
expect([...composed.httpByOperationId.keys()]).toEqual([
|
|
"SwappedOperation",
|
|
]);
|
|
expect(
|
|
composed.httpByOperationId.get("SwappedOperation")!.contract
|
|
.operationId,
|
|
).toBe("SwappedOperation");
|
|
});
|
|
|
|
const hostileRows = [
|
|
{
|
|
label: "an installed row with an extra own field",
|
|
row: () => ({
|
|
...TEST_CONTRACT_CONTRIBUTION.http[0]!,
|
|
injected: true,
|
|
}),
|
|
},
|
|
{
|
|
label: "an installed row with a symbol field",
|
|
row: () => ({
|
|
...TEST_CONTRACT_CONTRIBUTION.http[0]!,
|
|
[Symbol.for("injected")]: true,
|
|
}),
|
|
},
|
|
{
|
|
label: "an installed row with a custom prototype",
|
|
row: () =>
|
|
Object.assign(Object.create({ injected: true }), {
|
|
...TEST_CONTRACT_CONTRIBUTION.http[0]!,
|
|
}),
|
|
},
|
|
{
|
|
label: "an installed row hiding a non-enumerable own field",
|
|
row: () =>
|
|
Object.defineProperty(
|
|
{ ...TEST_CONTRACT_CONTRIBUTION.http[0]! },
|
|
"injected",
|
|
{ enumerable: false, value: true },
|
|
),
|
|
},
|
|
{
|
|
label: "an installed row behind a throwing ownKeys trap",
|
|
row: () =>
|
|
new Proxy(
|
|
{ ...TEST_CONTRACT_CONTRIBUTION.http[0]! },
|
|
{
|
|
ownKeys() {
|
|
throw new TypeError("hostile ownKeys trap");
|
|
},
|
|
},
|
|
),
|
|
},
|
|
];
|
|
|
|
for (const { label, row } of hostileRows) {
|
|
it(`refuses to compose ${label}`, () => {
|
|
expect(() =>
|
|
composeContractContributions(contributionWith(row()) as never),
|
|
).toThrow();
|
|
});
|
|
}
|
|
|
|
it("refuses a contribution whose own shape is not exact", () => {
|
|
for (const contribution of [
|
|
{ ...TEST_CONTRACT_CONTRIBUTION, injected: true },
|
|
Object.assign(Object.create({ injected: true }), {
|
|
...TEST_CONTRACT_CONTRIBUTION,
|
|
}),
|
|
{
|
|
...TEST_CONTRACT_CONTRIBUTION,
|
|
[Symbol.for("injected")]: true,
|
|
},
|
|
]) {
|
|
expect(() =>
|
|
composeContractContributions([contribution] as never),
|
|
).toThrow();
|
|
}
|
|
});
|
|
|
|
it("refuses an event contract whose own shape is not exact", () => {
|
|
const events = TEST_CONTRACT_CONTRIBUTION.events;
|
|
if (events.length === 0) return;
|
|
for (const event of [
|
|
{ ...events[0]!, injected: true },
|
|
Object.assign(Object.create({ injected: true }), { ...events[0]! }),
|
|
]) {
|
|
expect(() =>
|
|
composeContractContributions([
|
|
{ ...TEST_CONTRACT_CONTRIBUTION, events: [event] },
|
|
] as never),
|
|
).toThrow();
|
|
}
|
|
});
|
|
});
|
|
});
|