fix(housekeeping): close schema reflection escapes
This commit is contained in:
1 parent
7895188b56
commit
01dceac073
3 files changed
+539
-37
No files matched your search
@@ -25,6 +25,52 @@ function command(
|
||||
};
|
||||
}
|
||||
|
||||
type DescriptorReader = (
|
||||
target: object,
|
||||
property: PropertyKey,
|
||||
) => PropertyDescriptor | undefined;
|
||||
|
||||
const descriptorReaders = [
|
||||
[
|
||||
"Object.getOwnPropertyDescriptor",
|
||||
"object-descriptor",
|
||||
Object.getOwnPropertyDescriptor,
|
||||
],
|
||||
[
|
||||
"Object.getOwnPropertyDescriptors",
|
||||
"object-descriptors",
|
||||
(target: object, property: PropertyKey) =>
|
||||
Reflect.get(Object.getOwnPropertyDescriptors(target), property) as
|
||||
| PropertyDescriptor
|
||||
| undefined,
|
||||
],
|
||||
[
|
||||
"Reflect.getOwnPropertyDescriptor",
|
||||
"reflect-descriptor",
|
||||
Reflect.getOwnPropertyDescriptor,
|
||||
],
|
||||
] as const satisfies readonly (readonly [string, string, DescriptorReader])[];
|
||||
|
||||
function readDescriptorValue(
|
||||
target: object,
|
||||
property: PropertyKey,
|
||||
readDescriptor: DescriptorReader,
|
||||
): unknown {
|
||||
const descriptor = readDescriptor(target, property);
|
||||
if (!descriptor) throw new Error(`missing descriptor: ${String(property)}`);
|
||||
if ("value" in descriptor) return descriptor.value;
|
||||
if (descriptor.get) return Reflect.apply(descriptor.get, target, []);
|
||||
return undefined;
|
||||
}
|
||||
|
||||
function attemptMutation(mutate: () => void): void {
|
||||
try {
|
||||
mutate();
|
||||
} catch {
|
||||
// Readonly public views may reject the mutation directly.
|
||||
}
|
||||
}
|
||||
|
||||
describe("housekeeping command registry", () => {
|
||||
it("returns the validated snapshot registered under its global ID", () => {
|
||||
const registered = command("people.registry.lookup");
|
||||
@@ -296,6 +342,170 @@ describe("housekeeping command registry", () => {
|
||||
});
|
||||
expect(String(thrown)).not.toContain("caller lazy secret");
|
||||
});
|
||||
it.each(descriptorReaders)(
|
||||
"keeps private validation unchanged after nested mutation through %s",
|
||||
(_api, suffix, readDescriptor) => {
|
||||
const input = z.object({ value: z.string().min(3) });
|
||||
const id = `people.registry.${suffix}`;
|
||||
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 definition = readDescriptorValue(
|
||||
registered.input,
|
||||
"def",
|
||||
readDescriptor,
|
||||
) as object;
|
||||
const shape = readDescriptorValue(
|
||||
definition,
|
||||
"shape",
|
||||
readDescriptor,
|
||||
) as { value: z.ZodType };
|
||||
const child = shape.value;
|
||||
const childDefinition = readDescriptorValue(
|
||||
child,
|
||||
"def",
|
||||
readDescriptor,
|
||||
) as object;
|
||||
const checks = readDescriptorValue(
|
||||
childDefinition,
|
||||
"checks",
|
||||
readDescriptor,
|
||||
) as readonly object[];
|
||||
const internal = readDescriptorValue(
|
||||
checks[0] as object,
|
||||
"_zod",
|
||||
readDescriptor,
|
||||
) as object;
|
||||
const checkDefinition = readDescriptorValue(
|
||||
internal,
|
||||
"def",
|
||||
readDescriptor,
|
||||
) as { minimum: number };
|
||||
|
||||
attemptMutation(() => {
|
||||
shape.value = z.number();
|
||||
});
|
||||
attemptMutation(() => {
|
||||
checkDefinition.minimum = 0;
|
||||
});
|
||||
|
||||
expect(registered.input.safeParse({ value: "ab" }).success).toBe(false);
|
||||
expect(registered.input.safeParse({ value: "abcd" }).success).toBe(true);
|
||||
expect(registered.input.safeParse({ value: 7 }).success).toBe(false);
|
||||
},
|
||||
);
|
||||
|
||||
it("keeps own-key and symbol iteration detached from private checks", () => {
|
||||
const input = z.object({ value: z.string().min(3) });
|
||||
const id = "people.registry.symbol-iteration";
|
||||
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 publicInput = registered.input as z.ZodObject<{
|
||||
value: z.ZodString;
|
||||
}>;
|
||||
const shape = publicInput.shape;
|
||||
expect(Reflect.ownKeys(publicInput.def)).toContain("shape");
|
||||
expect(Object.keys(shape)).toEqual(["value"]);
|
||||
const child = Object.entries(shape)[0]?.[1];
|
||||
if (!child) throw new Error("public child schema missing");
|
||||
const checks = child.def.checks ?? [];
|
||||
const spreadChecks = [...checks];
|
||||
const iterator = checks[Symbol.iterator]();
|
||||
const iteratedCheck = iterator.next().value;
|
||||
if (!iteratedCheck) throw new Error("public check missing");
|
||||
expect(spreadChecks[0]).toBe(iteratedCheck);
|
||||
|
||||
attemptMutation(() => {
|
||||
(
|
||||
iteratedCheck as unknown as {
|
||||
_zod: { def: { minimum: number } };
|
||||
}
|
||||
)._zod.def.minimum = 0;
|
||||
});
|
||||
|
||||
expect(registered.input.safeParse({ value: "ab" }).success).toBe(false);
|
||||
expect(registered.input.safeParse({ value: "abcd" }).success).toBe(true);
|
||||
});
|
||||
|
||||
it("keeps prototype methods operational without passing the private schema to callbacks", async () => {
|
||||
const input = z.object({ value: z.string().min(3) });
|
||||
const id = "people.registry.prototype-methods";
|
||||
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 publicInput = registered.input as z.ZodObject<{
|
||||
value: z.ZodString;
|
||||
}>;
|
||||
|
||||
const publicPrototype = Object.getPrototypeOf(publicInput);
|
||||
expect(publicPrototype).toBe(Reflect.getPrototypeOf(publicInput));
|
||||
const prototypeMutation = Symbol("prototype-mutation");
|
||||
Object.defineProperty(publicPrototype, prototypeMutation, {
|
||||
configurable: true,
|
||||
value(this: z.ZodObject<{ value: z.ZodString }>): unknown {
|
||||
attemptMutation(() => {
|
||||
this.def.shape.value = z.number() as never;
|
||||
});
|
||||
return this.def;
|
||||
},
|
||||
});
|
||||
try {
|
||||
(
|
||||
publicInput as typeof publicInput & {
|
||||
[prototypeMutation](): unknown;
|
||||
}
|
||||
)[prototypeMutation]();
|
||||
} finally {
|
||||
Reflect.deleteProperty(publicPrototype, prototypeMutation);
|
||||
}
|
||||
expect(publicInput.safeParse({ value: "abcd" }).success).toBe(true);
|
||||
expect(publicInput.safeParse({ value: 7 }).success).toBe(false);
|
||||
|
||||
let appliedSchema: z.ZodType | undefined;
|
||||
const applied = publicInput.apply((schema) => {
|
||||
appliedSchema = schema;
|
||||
return schema;
|
||||
});
|
||||
expect(appliedSchema).toBe(publicInput);
|
||||
expect(applied).toBe(publicInput);
|
||||
|
||||
let externallyRegistered: z.ZodType | undefined;
|
||||
const externalRegistry = {
|
||||
add(schema: z.ZodType): void {
|
||||
externallyRegistered = schema;
|
||||
},
|
||||
};
|
||||
expect(
|
||||
publicInput.register(externalRegistry as never, undefined as never),
|
||||
).toBe(publicInput);
|
||||
expect(externallyRegistered).toBe(publicInput);
|
||||
|
||||
const partial = publicInput.partial();
|
||||
const picked = publicInput.pick({ value: true });
|
||||
expect(partial.safeParse({}).success).toBe(true);
|
||||
expect(picked.safeParse({ value: "abcd" }).success).toBe(true);
|
||||
expect((await publicInput.safeParseAsync({ value: "ab" })).success).toBe(
|
||||
false,
|
||||
);
|
||||
expect((await publicInput.safeParseAsync({ value: "abcd" })).success).toBe(
|
||||
true,
|
||||
);
|
||||
});
|
||||
it("seals the global registry after deterministic bootstrap", () => {
|
||||
sealHousekeepingCommandRegistry();
|
||||
|
||||
|
||||
Reference in new issue
Block a user