fix(housekeeping): harden readonly schema facade
This commit is contained in:
1 parent
139e8cfc3a
commit
2970dff563
3 files changed
+782
-15
No files matched your search
@@ -71,6 +71,45 @@ function attemptMutation(mutate: () => void): void {
|
||||
}
|
||||
}
|
||||
|
||||
function mutationWasRejected(mutate: () => void): boolean {
|
||||
try {
|
||||
mutate();
|
||||
return false;
|
||||
} catch {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
const unsafeSchemaCallbackMessage =
|
||||
"callback-bearing schema arguments are not supported";
|
||||
|
||||
const callbackOptionMethodNames = [
|
||||
"parse",
|
||||
"safeParse",
|
||||
"parseAsync",
|
||||
"safeParseAsync",
|
||||
"spa",
|
||||
"encode",
|
||||
"decode",
|
||||
"encodeAsync",
|
||||
"decodeAsync",
|
||||
"safeEncode",
|
||||
"safeDecode",
|
||||
"safeEncodeAsync",
|
||||
"safeDecodeAsync",
|
||||
] as const;
|
||||
|
||||
function attemptMinimumMutation(instance: unknown): void {
|
||||
if (typeof instance !== "object" || instance === null) return;
|
||||
const internal = Reflect.get(instance, "_zod") as
|
||||
| { def?: { minimum?: number } }
|
||||
| undefined;
|
||||
if (typeof internal?.def?.minimum !== "number") return;
|
||||
attemptMutation(() => {
|
||||
if (internal.def) internal.def.minimum = 0;
|
||||
});
|
||||
}
|
||||
|
||||
describe("housekeeping command registry", () => {
|
||||
it("returns the validated snapshot registered under its global ID", () => {
|
||||
const registered = command("people.registry.lookup");
|
||||
@@ -506,6 +545,337 @@ describe("housekeeping command registry", () => {
|
||||
true,
|
||||
);
|
||||
});
|
||||
it.each(callbackOptionMethodNames)(
|
||||
"rejects callback-bearing %s options before private issue nodes are exposed",
|
||||
async (methodName) => {
|
||||
const input = z.object({ value: z.string().min(3) });
|
||||
const id = `people.registry.callback-${methodName.toLowerCase()}`;
|
||||
registerHousekeepingCommand({
|
||||
...command(id),
|
||||
input,
|
||||
execute: async (context) => ok({ id: 1 }, context.correlationId),
|
||||
} as HousekeepingCommand<z.output<typeof input>, { id: number }>);
|
||||
const registered = getHousekeepingCommand(id);
|
||||
if (!registered) throw new Error("registered command missing");
|
||||
|
||||
let callbackCalls = 0;
|
||||
let capturedInstance: unknown;
|
||||
const errorCallback = (issue: unknown): string => {
|
||||
callbackCalls += 1;
|
||||
capturedInstance = Reflect.get(issue as object, "inst");
|
||||
attemptMinimumMutation(capturedInstance);
|
||||
return "forged validation error";
|
||||
};
|
||||
const method = Reflect.get(registered.input, methodName);
|
||||
if (typeof method !== "function") {
|
||||
throw new Error(`public schema method missing: ${methodName}`);
|
||||
}
|
||||
const attack = Promise.resolve().then(() =>
|
||||
Reflect.apply(method, registered.input, [
|
||||
{ value: "x" },
|
||||
{ error: errorCallback },
|
||||
]),
|
||||
);
|
||||
|
||||
await expect(attack).rejects.toThrow(unsafeSchemaCallbackMessage);
|
||||
expect(callbackCalls).toBe(0);
|
||||
expect(capturedInstance).toBeUndefined();
|
||||
expect(registered.input.safeParse({ value: "x" }).success).toBe(false);
|
||||
expect(registered.input.safeParse({ value: "valid" }).success).toBe(true);
|
||||
},
|
||||
);
|
||||
|
||||
it("rejects JSON-schema overrides before private schema or check nodes are exposed", () => {
|
||||
const input = z.object({ value: z.string().min(3) });
|
||||
const id = "people.registry.callback-json-schema";
|
||||
registerHousekeepingCommand({
|
||||
...command(id),
|
||||
input,
|
||||
execute: async (context) => ok({ id: 1 }, context.correlationId),
|
||||
} as HousekeepingCommand<z.output<typeof input>, { id: number }>);
|
||||
const registered = getHousekeepingCommand(id);
|
||||
if (!registered) throw new Error("registered command missing");
|
||||
|
||||
let callbackCalls = 0;
|
||||
let capturedSchema: unknown;
|
||||
let attackError: unknown;
|
||||
try {
|
||||
registered.input.toJSONSchema({
|
||||
override: ({ zodSchema }) => {
|
||||
callbackCalls += 1;
|
||||
capturedSchema = zodSchema;
|
||||
const checks = (zodSchema._zod.def as { checks?: unknown[] }).checks;
|
||||
if (checks?.[0]) attemptMinimumMutation(checks[0]);
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
attackError = error;
|
||||
}
|
||||
|
||||
expect({
|
||||
attackError:
|
||||
attackError instanceof TypeError ? attackError.message : undefined,
|
||||
callbackCalls,
|
||||
capturedSchema,
|
||||
shortValueAccepted: registered.input.safeParse({ value: "x" }).success,
|
||||
validValueAccepted: registered.input.safeParse({ value: "valid" })
|
||||
.success,
|
||||
}).toEqual({
|
||||
attackError: unsafeSchemaCallbackMessage,
|
||||
callbackCalls: 0,
|
||||
capturedSchema: undefined,
|
||||
shortValueAccepted: false,
|
||||
validValueAccepted: true,
|
||||
});
|
||||
});
|
||||
|
||||
it("rejects an error callback concealed behind an option Proxy", () => {
|
||||
const input = z.object({ value: z.string().min(3) });
|
||||
const id = "people.registry.callback-proxy-options";
|
||||
registerHousekeepingCommand({
|
||||
...command(id),
|
||||
input,
|
||||
execute: async (context) => ok({ id: 1 }, context.correlationId),
|
||||
} as HousekeepingCommand<z.output<typeof input>, { id: number }>);
|
||||
const registered = getHousekeepingCommand(id);
|
||||
if (!registered) throw new Error("registered command missing");
|
||||
|
||||
let callbackCalls = 0;
|
||||
let capturedInstance: unknown;
|
||||
const errorCallback = (issue: unknown): string => {
|
||||
callbackCalls += 1;
|
||||
capturedInstance = Reflect.get(issue as object, "inst");
|
||||
attemptMinimumMutation(capturedInstance);
|
||||
return "forged validation error";
|
||||
};
|
||||
const concealedOptions = new Proxy(
|
||||
{},
|
||||
{
|
||||
get: (_target, property) =>
|
||||
property === "error" ? errorCallback : undefined,
|
||||
getOwnPropertyDescriptor: () => undefined,
|
||||
ownKeys: () => [],
|
||||
},
|
||||
);
|
||||
let attackError: unknown;
|
||||
try {
|
||||
registered.input.safeParse({ value: "x" }, concealedOptions as never);
|
||||
} catch (error) {
|
||||
attackError = error;
|
||||
}
|
||||
|
||||
expect({
|
||||
attackError:
|
||||
attackError instanceof TypeError ? attackError.message : undefined,
|
||||
callbackCalls,
|
||||
capturedInstance,
|
||||
shortValueAccepted: registered.input.safeParse({ value: "x" }).success,
|
||||
validValueAccepted: registered.input.safeParse({ value: "valid" })
|
||||
.success,
|
||||
}).toEqual({
|
||||
attackError: unsafeSchemaCallbackMessage,
|
||||
callbackCalls: 0,
|
||||
capturedInstance: undefined,
|
||||
shortValueAccepted: false,
|
||||
validValueAccepted: true,
|
||||
});
|
||||
});
|
||||
|
||||
it("keeps callback-free sync and async parsing available", async () => {
|
||||
const input = z.object({ value: z.string().min(3) });
|
||||
const id = "people.registry.callback-free-parse";
|
||||
registerHousekeepingCommand({
|
||||
...command(id),
|
||||
input,
|
||||
execute: async (context) => ok({ id: 1 }, context.correlationId),
|
||||
} as HousekeepingCommand<z.output<typeof input>, { id: number }>);
|
||||
const registered = getHousekeepingCommand(id);
|
||||
if (!registered) throw new Error("registered command missing");
|
||||
|
||||
expect(
|
||||
registered.input.parse({ value: "valid" }, { jitless: true }),
|
||||
).toEqual({ value: "valid" });
|
||||
expect(
|
||||
registered.input.safeParse({ value: "valid" }, { reportInput: true }),
|
||||
).toMatchObject({ success: true, data: { value: "valid" } });
|
||||
await expect(
|
||||
registered.input.parseAsync({ value: "valid" }, { jitless: true }),
|
||||
).resolves.toEqual({ value: "valid" });
|
||||
await expect(
|
||||
registered.input.safeParseAsync({ value: "x" }, { reportInput: true }),
|
||||
).resolves.toMatchObject({ success: false });
|
||||
});
|
||||
|
||||
it("exposes detached Map, Set, and Date values as behaviorally readonly views", () => {
|
||||
const sourceMap = new Map<string, { count: number }>([
|
||||
["registered", { count: 1 }],
|
||||
]);
|
||||
const sourceSet = new Set(["registered"]);
|
||||
const sourceDate = new Date("2026-08-26T12:34:56.000Z");
|
||||
const input = z.object({ value: z.string() });
|
||||
Object.assign(input.def, {
|
||||
exposedMap: sourceMap,
|
||||
exposedSet: sourceSet,
|
||||
exposedDate: sourceDate,
|
||||
});
|
||||
const id = "people.registry.readonly-builtins";
|
||||
registerHousekeepingCommand({
|
||||
...command(id),
|
||||
input,
|
||||
execute: async (context) => ok({ id: 1 }, context.correlationId),
|
||||
} as HousekeepingCommand<z.output<typeof input>, { id: number }>);
|
||||
const registered = getHousekeepingCommand(id);
|
||||
if (!registered) throw new Error("registered command missing");
|
||||
const publicDefinition = registered.input
|
||||
.def as typeof registered.input.def & {
|
||||
exposedMap: ReadonlyMap<string, { count: number }>;
|
||||
exposedSet: ReadonlySet<string>;
|
||||
exposedDate: Date;
|
||||
};
|
||||
const publicMap = publicDefinition.exposedMap;
|
||||
const publicSet = publicDefinition.exposedSet;
|
||||
const publicDate = publicDefinition.exposedDate;
|
||||
|
||||
sourceMap.set("caller-late", { count: 2 });
|
||||
sourceSet.add("caller-late");
|
||||
sourceDate.setTime(0);
|
||||
const mapForEachReceivers: unknown[] = [];
|
||||
const mapEntries: Array<[string, number]> = [];
|
||||
publicMap.forEach((value, key, collection) => {
|
||||
mapEntries.push([key, value.count]);
|
||||
mapForEachReceivers.push(collection);
|
||||
});
|
||||
const setForEachReceivers: unknown[] = [];
|
||||
const setEntries: string[] = [];
|
||||
publicSet.forEach((value, duplicate, collection) => {
|
||||
expect(duplicate).toBe(value);
|
||||
setEntries.push(value);
|
||||
setForEachReceivers.push(collection);
|
||||
});
|
||||
const dateIsoBefore = publicDate.toISOString();
|
||||
|
||||
const mapEscape = Symbol("map-escape");
|
||||
const setEscape = Symbol("set-escape");
|
||||
const dateEscape = Symbol("date-escape");
|
||||
Object.defineProperty(Map.prototype, mapEscape, {
|
||||
configurable: true,
|
||||
value(this: Map<unknown, unknown>): Map<unknown, unknown> {
|
||||
return this;
|
||||
},
|
||||
});
|
||||
Object.defineProperty(Set.prototype, setEscape, {
|
||||
configurable: true,
|
||||
value(this: Set<unknown>): Set<unknown> {
|
||||
return this;
|
||||
},
|
||||
});
|
||||
Object.defineProperty(Date.prototype, dateEscape, {
|
||||
configurable: true,
|
||||
value(this: Date): Date {
|
||||
return this;
|
||||
},
|
||||
});
|
||||
let escapedMap: unknown;
|
||||
let escapedSet: unknown;
|
||||
let escapedDate: unknown;
|
||||
try {
|
||||
const mapMethod = Reflect.get(publicMap, mapEscape);
|
||||
const setMethod = Reflect.get(publicSet, setEscape);
|
||||
const dateMethod = Reflect.get(publicDate, dateEscape);
|
||||
if (
|
||||
typeof mapMethod !== "function" ||
|
||||
typeof setMethod !== "function" ||
|
||||
typeof dateMethod !== "function"
|
||||
) {
|
||||
throw new Error("dynamic builtin method missing");
|
||||
}
|
||||
escapedMap = Reflect.apply(mapMethod, publicMap, []);
|
||||
escapedSet = Reflect.apply(setMethod, publicSet, []);
|
||||
escapedDate = Reflect.apply(dateMethod, publicDate, []);
|
||||
} finally {
|
||||
Reflect.deleteProperty(Map.prototype, mapEscape);
|
||||
Reflect.deleteProperty(Set.prototype, setEscape);
|
||||
Reflect.deleteProperty(Date.prototype, dateEscape);
|
||||
}
|
||||
const escapeMutationResults = [
|
||||
mutationWasRejected(() =>
|
||||
(escapedMap as Map<string, { count: number }>).set("escaped-forged", {
|
||||
count: 11,
|
||||
}),
|
||||
),
|
||||
mutationWasRejected(() =>
|
||||
(escapedSet as Set<string>).add("escaped-forged"),
|
||||
),
|
||||
mutationWasRejected(() => (escapedDate as Date).setTime(0)),
|
||||
];
|
||||
|
||||
const mapMutationResults = [
|
||||
mutationWasRejected(() =>
|
||||
(publicMap as Map<string, { count: number }>).set("forged", {
|
||||
count: 9,
|
||||
}),
|
||||
),
|
||||
mutationWasRejected(() =>
|
||||
(publicMap as Map<string, { count: number }>).delete("registered"),
|
||||
),
|
||||
mutationWasRejected(() =>
|
||||
(publicMap as Map<string, { count: number }>).clear(),
|
||||
),
|
||||
mutationWasRejected(() =>
|
||||
Map.prototype.set.call(
|
||||
publicMap as Map<string, { count: number }>,
|
||||
"prototype-forged",
|
||||
{ count: 10 },
|
||||
),
|
||||
),
|
||||
];
|
||||
const setMutationResults = [
|
||||
mutationWasRejected(() => (publicSet as Set<string>).add("forged")),
|
||||
mutationWasRejected(() =>
|
||||
(publicSet as Set<string>).delete("registered"),
|
||||
),
|
||||
mutationWasRejected(() => (publicSet as Set<string>).clear()),
|
||||
mutationWasRejected(() =>
|
||||
Set.prototype.add.call(publicSet as Set<string>, "prototype-forged"),
|
||||
),
|
||||
];
|
||||
const dateMutationResults = Object.getOwnPropertyNames(Date.prototype)
|
||||
.filter((property) => property.startsWith("set"))
|
||||
.map((property) =>
|
||||
mutationWasRejected(() => {
|
||||
const method = Reflect.get(publicDate, property);
|
||||
if (typeof method !== "function") {
|
||||
throw new TypeError(`date mutator unavailable: ${property}`);
|
||||
}
|
||||
Reflect.apply(method, publicDate, [0]);
|
||||
}),
|
||||
);
|
||||
dateMutationResults.push(
|
||||
mutationWasRejected(() => Date.prototype.setTime.call(publicDate, 0)),
|
||||
);
|
||||
let dateIsoAfter: string | undefined;
|
||||
try {
|
||||
dateIsoAfter = publicDate.toISOString();
|
||||
} catch {
|
||||
dateIsoAfter = undefined;
|
||||
}
|
||||
|
||||
expect(dateMutationResults.length).toBeGreaterThan(1);
|
||||
expect(dateMutationResults.every(Boolean)).toBe(true);
|
||||
expect(escapeMutationResults).toEqual([true, true, true]);
|
||||
expect(mapEntries).toEqual([["registered", 1]]);
|
||||
expect(mapForEachReceivers).toHaveLength(1);
|
||||
expect(mapForEachReceivers[0]).toBe(publicMap);
|
||||
expect(mapMutationResults).toEqual([true, true, true, true]);
|
||||
expect([...publicMap.entries()]).toEqual([["registered", { count: 1 }]]);
|
||||
expect(setEntries).toEqual(["registered"]);
|
||||
expect(setForEachReceivers).toHaveLength(1);
|
||||
expect(setForEachReceivers[0]).toBe(publicSet);
|
||||
expect(setMutationResults).toEqual([true, true, true, true]);
|
||||
expect([...publicSet]).toEqual(["registered"]);
|
||||
expect(dateIsoBefore).toBe("2026-08-26T12:34:56.000Z");
|
||||
expect(dateIsoAfter).toBe("2026-08-26T12:34:56.000Z");
|
||||
});
|
||||
it("seals the global registry after deterministic bootstrap", () => {
|
||||
sealHousekeepingCommandRegistry();
|
||||
|
||||
|
||||
Reference in new issue
Block a user