fix(housekeeping): isolate executable command schemas
This commit is contained in:
1 parent
00618fb2a3
commit
7895188b56
5 files changed
+431
-67
No files matched your search
@@ -1,10 +1,58 @@
|
|||||||
import { describe, expect, it } from "vitest";
|
import { describe, expect, it } from "vitest";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import { anyCapability, ok } from "../contracts";
|
import { anyCapability, ok } from "../contracts";
|
||||||
import { housekeepingCommandRegistryReady } from "./bootstrap";
|
import {
|
||||||
import { registerHousekeepingCommand } from "./registry";
|
defineHousekeepingCommands,
|
||||||
|
housekeepingCommandRegistryReady,
|
||||||
|
registerHousekeepingCommands,
|
||||||
|
} from "./bootstrap";
|
||||||
|
import {
|
||||||
|
type HousekeepingCommand,
|
||||||
|
registerHousekeepingCommand,
|
||||||
|
} from "./registry";
|
||||||
|
|
||||||
describe("housekeeping command bootstrap", () => {
|
describe("housekeeping command bootstrap", () => {
|
||||||
|
it("preserves heterogeneous concrete input and output types", () => {
|
||||||
|
const stringCommand: HousekeepingCommand<
|
||||||
|
{ value: string },
|
||||||
|
{ length: number }
|
||||||
|
> = {
|
||||||
|
id: "system.bootstrap.string-shape",
|
||||||
|
owner: "system",
|
||||||
|
risk: "safe",
|
||||||
|
capability: anyCapability("admin.settings.view"),
|
||||||
|
input: z.object({ value: z.string() }),
|
||||||
|
requiresReason: false,
|
||||||
|
rateLimit: { attempts: 1, windowMs: 1_000 },
|
||||||
|
execute: async (context, input) =>
|
||||||
|
ok({ length: input.value.length }, context.correlationId),
|
||||||
|
};
|
||||||
|
const numberCommand: HousekeepingCommand<
|
||||||
|
{ count: number },
|
||||||
|
{ doubled: number }
|
||||||
|
> = {
|
||||||
|
id: "system.bootstrap.number-shape",
|
||||||
|
owner: "system",
|
||||||
|
risk: "safe",
|
||||||
|
capability: anyCapability("admin.settings.view"),
|
||||||
|
input: z.object({ count: z.number().int() }),
|
||||||
|
requiresReason: false,
|
||||||
|
rateLimit: { attempts: 2, windowMs: 2_000 },
|
||||||
|
execute: async (context, input) =>
|
||||||
|
ok({ doubled: input.count * 2 }, context.correlationId),
|
||||||
|
};
|
||||||
|
|
||||||
|
const commands = defineHousekeepingCommands(stringCommand, numberCommand);
|
||||||
|
const stringInput: z.input<(typeof commands)[0]["input"]> = {
|
||||||
|
value: "typed",
|
||||||
|
};
|
||||||
|
const numberInput: z.input<(typeof commands)[1]["input"]> = { count: 4 };
|
||||||
|
expect(commands[0].input.parse(stringInput)).toEqual({ value: "typed" });
|
||||||
|
expect(commands[1].input.parse(numberInput)).toEqual({ count: 4 });
|
||||||
|
expect(() => registerHousekeepingCommands(commands)).toThrow(
|
||||||
|
"command registry is sealed",
|
||||||
|
);
|
||||||
|
});
|
||||||
it("registers the complete current list and seals during module initialization", () => {
|
it("registers the complete current list and seals during module initialization", () => {
|
||||||
expect(housekeepingCommandRegistryReady).toBe(true);
|
expect(housekeepingCommandRegistryReady).toBe(true);
|
||||||
expect(() =>
|
expect(() =>
|
||||||
|
|||||||
@@ -6,12 +6,37 @@ import {
|
|||||||
sealHousekeepingCommandRegistry,
|
sealHousekeepingCommandRegistry,
|
||||||
} from "./registry";
|
} from "./registry";
|
||||||
|
|
||||||
const currentHousekeepingCommands =
|
type PreserveHousekeepingCommand<Definition> =
|
||||||
[] as const satisfies readonly HousekeepingCommand<unknown, unknown>[];
|
Definition extends HousekeepingCommand<infer _Input, infer _Output>
|
||||||
|
? Definition
|
||||||
|
: never;
|
||||||
|
|
||||||
for (const command of currentHousekeepingCommands) {
|
type HousekeepingCommandTuple<Commands extends readonly unknown[]> =
|
||||||
registerHousekeepingCommand(command);
|
Commands & {
|
||||||
|
readonly [Index in keyof Commands]: PreserveHousekeepingCommand<
|
||||||
|
Commands[Index]
|
||||||
|
>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export function defineHousekeepingCommands<
|
||||||
|
const Commands extends readonly unknown[],
|
||||||
|
>(...commands: HousekeepingCommandTuple<Commands>): Commands {
|
||||||
|
return commands;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function registerHousekeepingCommands<
|
||||||
|
const Commands extends readonly unknown[],
|
||||||
|
>(commands: HousekeepingCommandTuple<Commands>): void {
|
||||||
|
for (const command of commands) {
|
||||||
|
registerHousekeepingCommand(
|
||||||
|
command as unknown as HousekeepingCommand<unknown, unknown>,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const currentHousekeepingCommands = defineHousekeepingCommands();
|
||||||
|
|
||||||
|
registerHousekeepingCommands(currentHousekeepingCommands);
|
||||||
sealHousekeepingCommandRegistry();
|
sealHousekeepingCommandRegistry();
|
||||||
|
|
||||||
export const housekeepingCommandRegistryReady = true;
|
export const housekeepingCommandRegistryReady = true;
|
||||||
@@ -385,7 +385,7 @@ describe("dispatchHousekeepingCommand", () => {
|
|||||||
]);
|
]);
|
||||||
expect(audit.entries[1]?.correlationId).toBe(result.correlationId);
|
expect(audit.entries[1]?.correlationId).toBe(result.correlationId);
|
||||||
});
|
});
|
||||||
it("runs sensitive validation and rate preflight before intent and execution", async () => {
|
it("runs sensitive capability and rate preflight before intent and execution", async () => {
|
||||||
const events: string[] = [];
|
const events: string[] = [];
|
||||||
const audit = auditRecorder(events);
|
const audit = auditRecorder(events);
|
||||||
const tracedContext = {
|
const tracedContext = {
|
||||||
@@ -399,9 +399,7 @@ describe("dispatchHousekeepingCommand", () => {
|
|||||||
baseCommand("system.dispatch.preflight-order", {
|
baseCommand("system.dispatch.preflight-order", {
|
||||||
risk: "sensitive",
|
risk: "sensitive",
|
||||||
requiresReason: true,
|
requiresReason: true,
|
||||||
input: z.object({ enabled: z.boolean() }).superRefine(() => {
|
input: z.object({ enabled: z.boolean() }),
|
||||||
events.push("input");
|
|
||||||
}),
|
|
||||||
execute: async (context) => {
|
execute: async (context) => {
|
||||||
events.push("execute");
|
events.push("execute");
|
||||||
return ok(null, context.correlationId);
|
return ok(null, context.correlationId);
|
||||||
@@ -427,7 +425,6 @@ describe("dispatchHousekeepingCommand", () => {
|
|||||||
|
|
||||||
expect(events).toEqual([
|
expect(events).toEqual([
|
||||||
"capability",
|
"capability",
|
||||||
"input",
|
|
||||||
"rate",
|
"rate",
|
||||||
"audit:intent",
|
"audit:intent",
|
||||||
"execute",
|
"execute",
|
||||||
@@ -566,15 +563,19 @@ describe("dispatchHousekeepingCommand", () => {
|
|||||||
const audit = auditRecorder();
|
const audit = auditRecorder();
|
||||||
register(
|
register(
|
||||||
baseCommand("system.dispatch.schema-exception", {
|
baseCommand("system.dispatch.schema-exception", {
|
||||||
input: z.preprocess(() => {
|
input: z.object({ value: z.string() }),
|
||||||
throw new Error("schema secret exposed");
|
|
||||||
}, z.object({})),
|
|
||||||
execute: async (context) => ok(null, context.correlationId),
|
execute: async (context) => ok(null, context.correlationId),
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
const throwingInput = Object.defineProperty({}, "value", {
|
||||||
|
enumerable: true,
|
||||||
|
get: () => {
|
||||||
|
throw new Error("schema secret exposed");
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
const result = await dispatchHousekeepingCommand(
|
const result = await dispatchHousekeepingCommand(
|
||||||
{ commandId: "system.dispatch.schema-exception", input: {} },
|
{ commandId: "system.dispatch.schema-exception", input: throwingInput },
|
||||||
dependencies({ audit: audit.writer }),
|
dependencies({ audit: audit.writer }),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import {
|
|||||||
type HousekeepingCommand,
|
type HousekeepingCommand,
|
||||||
registerHousekeepingCommand,
|
registerHousekeepingCommand,
|
||||||
sealHousekeepingCommandRegistry,
|
sealHousekeepingCommandRegistry,
|
||||||
|
UnsupportedHousekeepingCommandSchemaError,
|
||||||
} from "./registry";
|
} from "./registry";
|
||||||
|
|
||||||
function command(
|
function command(
|
||||||
@@ -154,6 +155,147 @@ describe("housekeeping command registry", () => {
|
|||||||
registered.input.safeParse({ value: 4, guard: "abcd" }).success,
|
registered.input.safeParse({ value: 4, guard: "abcd" }).success,
|
||||||
).toBe(false);
|
).toBe(false);
|
||||||
});
|
});
|
||||||
|
it("snapshots caller-owned lazy targets across supported container schemas", () => {
|
||||||
|
let lazyChild: z.ZodType = z.string().min(2);
|
||||||
|
let defaultValue = "registered-default";
|
||||||
|
const input = z.object({
|
||||||
|
choice: z.union([z.lazy(() => lazyChild), z.number().int()]),
|
||||||
|
optional: z.lazy(() => lazyChild).optional(),
|
||||||
|
defaulted: z.string().default(() => defaultValue),
|
||||||
|
list: z.array(z.lazy(() => lazyChild)),
|
||||||
|
lookup: z.record(
|
||||||
|
z.string(),
|
||||||
|
z.lazy(() => lazyChild),
|
||||||
|
),
|
||||||
|
});
|
||||||
|
registerHousekeepingCommand({
|
||||||
|
...command("people.registry.lazy-containers"),
|
||||||
|
input,
|
||||||
|
execute: async (context) => ok({ id: 1 }, context.correlationId),
|
||||||
|
} as HousekeepingCommand<z.output<typeof input>, { id: number }>);
|
||||||
|
const registered = getHousekeepingCommand(
|
||||||
|
"people.registry.lazy-containers",
|
||||||
|
);
|
||||||
|
if (!registered) throw new Error("registered command missing");
|
||||||
|
|
||||||
|
lazyChild = z.boolean();
|
||||||
|
defaultValue = "caller-mutated-default";
|
||||||
|
|
||||||
|
const parsed = registered.input.safeParse({
|
||||||
|
choice: "ok",
|
||||||
|
list: ["one"],
|
||||||
|
lookup: { key: "two" },
|
||||||
|
});
|
||||||
|
expect(parsed).toMatchObject({
|
||||||
|
success: true,
|
||||||
|
data: { defaulted: "registered-default" },
|
||||||
|
});
|
||||||
|
expect(
|
||||||
|
registered.input.safeParse({
|
||||||
|
choice: true,
|
||||||
|
optional: true,
|
||||||
|
list: [true],
|
||||||
|
lookup: { key: true },
|
||||||
|
}).success,
|
||||||
|
).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("snapshots recursive lazy back-edges with cycle safety", () => {
|
||||||
|
type TreeNode = { name: string; children: TreeNode[] };
|
||||||
|
let nameSchema: z.ZodType = z.string().min(2);
|
||||||
|
let recursiveSchema: z.ZodType<TreeNode>;
|
||||||
|
recursiveSchema = z.object({
|
||||||
|
name: z.lazy(() => nameSchema),
|
||||||
|
children: z.array(z.lazy(() => recursiveSchema)),
|
||||||
|
}) as z.ZodType<TreeNode>;
|
||||||
|
registerHousekeepingCommand({
|
||||||
|
...command("people.registry.recursive-lazy"),
|
||||||
|
input: recursiveSchema,
|
||||||
|
execute: async (context) => ok({ id: 1 }, context.correlationId),
|
||||||
|
} as HousekeepingCommand<TreeNode, { id: number }>);
|
||||||
|
const registered = getHousekeepingCommand("people.registry.recursive-lazy");
|
||||||
|
if (!registered) throw new Error("registered command missing");
|
||||||
|
|
||||||
|
nameSchema = z.number();
|
||||||
|
recursiveSchema = z.object({
|
||||||
|
name: z.number(),
|
||||||
|
children: z.array(z.unknown()),
|
||||||
|
}) as unknown as z.ZodType<TreeNode>;
|
||||||
|
|
||||||
|
expect(
|
||||||
|
registered.input.safeParse({
|
||||||
|
name: "root",
|
||||||
|
children: [{ name: "leaf", children: [] }],
|
||||||
|
}).success,
|
||||||
|
).toBe(true);
|
||||||
|
expect(
|
||||||
|
registered.input.safeParse({
|
||||||
|
name: "root",
|
||||||
|
children: [{ name: 7, children: [] }],
|
||||||
|
}).success,
|
||||||
|
).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it.each([
|
||||||
|
["refine", "custom", z.string().refine((value) => value === "allowed")],
|
||||||
|
["super-refine", "custom", z.string().superRefine(() => undefined)],
|
||||||
|
[
|
||||||
|
"preprocess",
|
||||||
|
"transform",
|
||||||
|
z.preprocess((value) => String(value), z.string()),
|
||||||
|
],
|
||||||
|
["transform", "transform", z.string().transform((value) => value.length)],
|
||||||
|
["async-refine", "custom", z.string().refine(async () => true)],
|
||||||
|
] as const)(
|
||||||
|
"rejects unsupported executable validation schema %s",
|
||||||
|
(suffix, schemaKind, input) => {
|
||||||
|
const id = `people.registry.unsupported-${suffix}`;
|
||||||
|
let thrown: unknown;
|
||||||
|
try {
|
||||||
|
registerHousekeepingCommand({
|
||||||
|
...command(id),
|
||||||
|
input,
|
||||||
|
execute: async (context) => ok({ id: 1 }, context.correlationId),
|
||||||
|
} as HousekeepingCommand<unknown, { id: number }>);
|
||||||
|
} catch (error) {
|
||||||
|
thrown = error;
|
||||||
|
}
|
||||||
|
|
||||||
|
expect(thrown).toBeInstanceOf(UnsupportedHousekeepingCommandSchemaError);
|
||||||
|
expect(thrown).toMatchObject({
|
||||||
|
name: "UnsupportedHousekeepingCommandSchemaError",
|
||||||
|
code: "UNSUPPORTED_COMMAND_INPUT_SCHEMA",
|
||||||
|
commandId: id,
|
||||||
|
schemaKind,
|
||||||
|
});
|
||||||
|
expect(getHousekeepingCommand(id)).toBeUndefined();
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
it("rejects a lazy edge that cannot be resolved at registration", () => {
|
||||||
|
const id = "people.registry.unresolvable-lazy";
|
||||||
|
let thrown: unknown;
|
||||||
|
try {
|
||||||
|
registerHousekeepingCommand({
|
||||||
|
...command(id),
|
||||||
|
input: z.lazy(() => {
|
||||||
|
throw new Error("caller lazy secret");
|
||||||
|
}),
|
||||||
|
execute: async (context) => ok({ id: 1 }, context.correlationId),
|
||||||
|
} as HousekeepingCommand<unknown, { id: number }>);
|
||||||
|
} catch (error) {
|
||||||
|
thrown = error;
|
||||||
|
}
|
||||||
|
|
||||||
|
expect(thrown).toBeInstanceOf(UnsupportedHousekeepingCommandSchemaError);
|
||||||
|
expect(thrown).toMatchObject({
|
||||||
|
name: "UnsupportedHousekeepingCommandSchemaError",
|
||||||
|
code: "UNSUPPORTED_COMMAND_INPUT_SCHEMA",
|
||||||
|
commandId: id,
|
||||||
|
schemaKind: "lazy",
|
||||||
|
});
|
||||||
|
expect(String(thrown)).not.toContain("caller lazy secret");
|
||||||
|
});
|
||||||
it("seals the global registry after deterministic bootstrap", () => {
|
it("seals the global registry after deterministic bootstrap", () => {
|
||||||
sealHousekeepingCommandRegistry();
|
sealHousekeepingCommandRegistry();
|
||||||
|
|
||||||
|
|||||||
@@ -21,6 +21,11 @@ export interface HousekeepingCommand<I, O> {
|
|||||||
readonly owner: HousekeepingDomainId;
|
readonly owner: HousekeepingDomainId;
|
||||||
readonly risk: "safe" | "sensitive";
|
readonly risk: "safe" | "sensitive";
|
||||||
readonly capability: CapabilityRequirement;
|
readonly capability: CapabilityRequirement;
|
||||||
|
/**
|
||||||
|
* Registration snapshots structural/built-in Zod graphs and resolvable lazy
|
||||||
|
* edges. Stateful custom refinements, transforms, preprocessors, and other
|
||||||
|
* executable schema callbacks are rejected.
|
||||||
|
*/
|
||||||
readonly input: z.ZodType<I>;
|
readonly input: z.ZodType<I>;
|
||||||
readonly requiresReason: boolean;
|
readonly requiresReason: boolean;
|
||||||
readonly rateLimit: Readonly<{ attempts: number; windowMs: number }>;
|
readonly rateLimit: Readonly<{ attempts: number; windowMs: number }>;
|
||||||
@@ -30,6 +35,24 @@ export interface HousekeepingCommand<I, O> {
|
|||||||
) => Promise<HousekeepingResult<O>>;
|
) => Promise<HousekeepingResult<O>>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type UnsupportedHousekeepingCommandSchemaKind =
|
||||||
|
| "custom"
|
||||||
|
| "transform"
|
||||||
|
| "lazy"
|
||||||
|
| "executable";
|
||||||
|
|
||||||
|
export class UnsupportedHousekeepingCommandSchemaError extends Error {
|
||||||
|
readonly name = "UnsupportedHousekeepingCommandSchemaError";
|
||||||
|
readonly code = "UNSUPPORTED_COMMAND_INPUT_SCHEMA";
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
readonly commandId: string,
|
||||||
|
readonly schemaKind: UnsupportedHousekeepingCommandSchemaKind,
|
||||||
|
) {
|
||||||
|
super(`unsupported command input schema: ${commandId} (${schemaKind})`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
type RegisteredHousekeepingCommand = HousekeepingCommand<unknown, unknown>;
|
type RegisteredHousekeepingCommand = HousekeepingCommand<unknown, unknown>;
|
||||||
|
|
||||||
type ZodInternalNode = {
|
type ZodInternalNode = {
|
||||||
@@ -63,7 +86,7 @@ export function registerHousekeepingCommand<I, O>(
|
|||||||
owner: command.owner,
|
owner: command.owner,
|
||||||
risk: command.risk,
|
risk: command.risk,
|
||||||
capability,
|
capability,
|
||||||
input: isolateValidationGraph(command.input),
|
input: isolateValidationGraph(command.input, command.id),
|
||||||
requiresReason: command.requiresReason,
|
requiresReason: command.requiresReason,
|
||||||
rateLimit: Object.freeze({ ...command.rateLimit }),
|
rateLimit: Object.freeze({ ...command.rateLimit }),
|
||||||
execute: command.execute,
|
execute: command.execute,
|
||||||
@@ -143,74 +166,183 @@ function validateCapability(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function isolateValidationGraph<T extends z.ZodType>(schema: T): T {
|
function isolateValidationGraph<T extends z.ZodType>(
|
||||||
const seen = new WeakMap<object, unknown>();
|
schema: T,
|
||||||
|
commandId: string,
|
||||||
|
): T {
|
||||||
|
type SchemaCell = {
|
||||||
|
current?: z.ZodType;
|
||||||
|
reference?: z.ZodType;
|
||||||
|
};
|
||||||
|
|
||||||
|
const graphValues = new WeakMap<object, unknown>();
|
||||||
|
const schemaCells = new WeakMap<z.ZodType, SchemaCell>();
|
||||||
|
|
||||||
|
function unsupported(
|
||||||
|
schemaKind: UnsupportedHousekeepingCommandSchemaKind,
|
||||||
|
): never {
|
||||||
|
throw new UnsupportedHousekeepingCommandSchemaError(commandId, schemaKind);
|
||||||
|
}
|
||||||
|
|
||||||
|
function cloneSchema<S extends z.ZodType>(value: S): S {
|
||||||
|
const existing = schemaCells.get(value);
|
||||||
|
if (existing?.current) return existing.current as S;
|
||||||
|
if (existing) {
|
||||||
|
existing.reference ??= z.lazy(() => {
|
||||||
|
if (!existing.current) {
|
||||||
|
throw new Error("housekeeping schema registration incomplete");
|
||||||
|
}
|
||||||
|
return existing.current;
|
||||||
|
});
|
||||||
|
return existing.reference as S;
|
||||||
|
}
|
||||||
|
|
||||||
|
const definition = value.def as { type?: unknown; getter?: unknown };
|
||||||
|
if (definition.type === "custom") unsupported("custom");
|
||||||
|
if (definition.type === "transform") unsupported("transform");
|
||||||
|
|
||||||
|
const cell: SchemaCell = {};
|
||||||
|
schemaCells.set(value, cell);
|
||||||
|
if (definition.type === "lazy") {
|
||||||
|
let ownedTarget: z.ZodType | undefined;
|
||||||
|
const ownedLazy = z.lazy(() => {
|
||||||
|
if (!ownedTarget) {
|
||||||
|
throw new Error("housekeeping lazy schema target unavailable");
|
||||||
|
}
|
||||||
|
return ownedTarget;
|
||||||
|
});
|
||||||
|
cell.current = ownedLazy;
|
||||||
|
if (typeof definition.getter !== "function") unsupported("lazy");
|
||||||
|
|
||||||
|
let callerTarget: unknown;
|
||||||
|
try {
|
||||||
|
callerTarget = definition.getter();
|
||||||
|
} catch {
|
||||||
|
unsupported("lazy");
|
||||||
|
}
|
||||||
|
if (!(callerTarget instanceof z.ZodType)) unsupported("lazy");
|
||||||
|
ownedTarget = cloneSchema(callerTarget);
|
||||||
|
return ownedLazy as unknown as S;
|
||||||
|
}
|
||||||
|
|
||||||
|
const clonedDefinition = cloneGraph(value.def);
|
||||||
|
const cloned = value.clone(clonedDefinition as typeof value.def);
|
||||||
|
cell.current = cloned;
|
||||||
|
return cloned;
|
||||||
|
}
|
||||||
|
|
||||||
|
function cloneBuiltInCheck(value: ZodInternalNode): unknown {
|
||||||
|
const definition = value._zod.def;
|
||||||
|
if (typeof definition !== "object" || definition === null) {
|
||||||
|
unsupported("executable");
|
||||||
|
}
|
||||||
|
const checkDefinition = definition as {
|
||||||
|
check?: unknown;
|
||||||
|
type?: unknown;
|
||||||
|
};
|
||||||
|
if (
|
||||||
|
checkDefinition.check === "custom" ||
|
||||||
|
checkDefinition.type === "custom"
|
||||||
|
) {
|
||||||
|
unsupported("custom");
|
||||||
|
}
|
||||||
|
|
||||||
|
const clonedDefinition = cloneRecord(definition, true);
|
||||||
|
const cloned = new value._zod.constr(clonedDefinition as never) as {
|
||||||
|
_zod?: { check?: unknown };
|
||||||
|
};
|
||||||
|
if (
|
||||||
|
typeof (value._zod as { check?: unknown }).check === "function" &&
|
||||||
|
typeof cloned._zod?.check !== "function"
|
||||||
|
) {
|
||||||
|
unsupported("executable");
|
||||||
|
}
|
||||||
|
return cloned;
|
||||||
|
}
|
||||||
|
|
||||||
|
function cloneRecord(value: object, omitBuiltInWhen = false): object {
|
||||||
|
const cached = graphValues.get(value);
|
||||||
|
if (cached !== undefined) return cached as object;
|
||||||
|
|
||||||
|
const prototype = Object.getPrototypeOf(value);
|
||||||
|
const cloned = Object.create(prototype) as Record<PropertyKey, unknown>;
|
||||||
|
graphValues.set(value, cloned);
|
||||||
|
for (const key of Reflect.ownKeys(value)) {
|
||||||
|
const descriptor = Object.getOwnPropertyDescriptor(value, key);
|
||||||
|
if (!descriptor) continue;
|
||||||
|
if (
|
||||||
|
omitBuiltInWhen &&
|
||||||
|
key === "when" &&
|
||||||
|
"value" in descriptor &&
|
||||||
|
typeof descriptor.value === "function"
|
||||||
|
) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
let resolved: unknown;
|
||||||
|
if ("value" in descriptor) {
|
||||||
|
resolved = descriptor.value;
|
||||||
|
} else if (descriptor.get) {
|
||||||
|
try {
|
||||||
|
resolved = Reflect.get(value, key);
|
||||||
|
} catch {
|
||||||
|
unsupported("executable");
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
unsupported("executable");
|
||||||
|
}
|
||||||
|
Object.defineProperty(cloned, key, {
|
||||||
|
configurable: descriptor.configurable,
|
||||||
|
enumerable: descriptor.enumerable,
|
||||||
|
value: cloneGraph(resolved),
|
||||||
|
writable: "writable" in descriptor ? descriptor.writable : false,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return cloned;
|
||||||
|
}
|
||||||
|
|
||||||
function cloneGraph(value: unknown): unknown {
|
function cloneGraph(value: unknown): unknown {
|
||||||
|
if (typeof value === "function") unsupported("executable");
|
||||||
if (typeof value !== "object" || value === null) return value;
|
if (typeof value !== "object" || value === null) return value;
|
||||||
const cached = seen.get(value);
|
if (value instanceof z.ZodType) return cloneSchema(value);
|
||||||
if (cached !== undefined) return cached;
|
|
||||||
|
|
||||||
if (value instanceof z.ZodType) {
|
const cached = graphValues.get(value);
|
||||||
const clonedDefinition = cloneGraph(value.def);
|
if (cached !== undefined) return cached;
|
||||||
const cloned = value.clone(clonedDefinition as typeof value.def);
|
if (isZodInternalNode(value)) return cloneBuiltInCheck(value);
|
||||||
seen.set(value, cloned);
|
|
||||||
return cloned;
|
|
||||||
}
|
|
||||||
if (isZodInternalNode(value)) {
|
|
||||||
const clonedDefinition = cloneGraph(value._zod.def);
|
|
||||||
const cloned = new value._zod.constr(clonedDefinition as never) as {
|
|
||||||
_zod: { check?: unknown };
|
|
||||||
};
|
|
||||||
const runtimeCheck = (value._zod as { check?: unknown }).check;
|
|
||||||
if (runtimeCheck !== undefined && cloned._zod.check === undefined) {
|
|
||||||
cloned._zod.check = runtimeCheck;
|
|
||||||
}
|
|
||||||
seen.set(value, cloned);
|
|
||||||
return cloned;
|
|
||||||
}
|
|
||||||
if (Array.isArray(value)) {
|
if (Array.isArray(value)) {
|
||||||
const cloned: unknown[] = [];
|
const cloned: unknown[] = [];
|
||||||
seen.set(value, cloned);
|
graphValues.set(value, cloned);
|
||||||
for (const item of value) cloned.push(cloneGraph(item));
|
for (const item of value) cloned.push(cloneGraph(item));
|
||||||
return cloned;
|
return cloned;
|
||||||
}
|
}
|
||||||
if (value instanceof RegExp) {
|
if (value instanceof RegExp) {
|
||||||
const cloned = new RegExp(value.source, value.flags);
|
const cloned = new RegExp(value.source, value.flags);
|
||||||
seen.set(value, cloned);
|
graphValues.set(value, cloned);
|
||||||
return cloned;
|
return cloned;
|
||||||
}
|
}
|
||||||
if (value instanceof Date) {
|
if (value instanceof Date) {
|
||||||
const cloned = new Date(value.getTime());
|
const cloned = new Date(value.getTime());
|
||||||
seen.set(value, cloned);
|
graphValues.set(value, cloned);
|
||||||
return cloned;
|
return cloned;
|
||||||
}
|
}
|
||||||
|
if (value instanceof Map) {
|
||||||
const cloned = Object.create(Object.getPrototypeOf(value)) as Record<
|
const cloned = new Map<unknown, unknown>();
|
||||||
PropertyKey,
|
graphValues.set(value, cloned);
|
||||||
unknown
|
for (const [key, item] of value) {
|
||||||
>;
|
cloned.set(cloneGraph(key), cloneGraph(item));
|
||||||
seen.set(value, cloned);
|
}
|
||||||
for (const key of Reflect.ownKeys(value)) {
|
return cloned;
|
||||||
const descriptor = Object.getOwnPropertyDescriptor(value, key);
|
|
||||||
if (!descriptor) continue;
|
|
||||||
const clonedDescriptor =
|
|
||||||
"value" in descriptor
|
|
||||||
? { ...descriptor, value: cloneGraph(descriptor.value) }
|
|
||||||
: descriptor.get
|
|
||||||
? {
|
|
||||||
configurable: descriptor.configurable,
|
|
||||||
enumerable: descriptor.enumerable,
|
|
||||||
writable: false,
|
|
||||||
value: cloneGraph(Reflect.get(value, key)),
|
|
||||||
}
|
|
||||||
: descriptor;
|
|
||||||
Object.defineProperty(cloned, key, clonedDescriptor);
|
|
||||||
}
|
}
|
||||||
return cloned;
|
if (value instanceof Set) {
|
||||||
|
const cloned = new Set<unknown>();
|
||||||
|
graphValues.set(value, cloned);
|
||||||
|
for (const item of value) cloned.add(cloneGraph(item));
|
||||||
|
return cloned;
|
||||||
|
}
|
||||||
|
return cloneRecord(value);
|
||||||
}
|
}
|
||||||
|
|
||||||
return createReadonlyValidationFacade(cloneGraph(schema) as T);
|
return createReadonlyValidationFacade(cloneSchema(schema));
|
||||||
}
|
}
|
||||||
|
|
||||||
function isZodInternalNode(value: object): value is ZodInternalNode {
|
function isZodInternalNode(value: object): value is ZodInternalNode {
|
||||||
@@ -227,15 +359,31 @@ function createReadonlyValidationFacade<T extends z.ZodType>(schema: T): T {
|
|||||||
const views = new WeakMap<object, unknown>();
|
const views = new WeakMap<object, unknown>();
|
||||||
|
|
||||||
function readonlyView(value: unknown): unknown {
|
function readonlyView(value: unknown): unknown {
|
||||||
if (typeof value !== "object" || value === null) return value;
|
if (
|
||||||
|
(typeof value !== "object" && typeof value !== "function") ||
|
||||||
|
value === null
|
||||||
|
) {
|
||||||
|
return value;
|
||||||
|
}
|
||||||
const cached = views.get(value);
|
const cached = views.get(value);
|
||||||
if (cached !== undefined) return cached;
|
if (cached !== undefined) return cached;
|
||||||
if (value instanceof z.ZodType) return schemaFacade(value);
|
if (value instanceof z.ZodType) return schemaFacade(value);
|
||||||
|
if (typeof value === "function") {
|
||||||
|
const wrapped = (...args: unknown[]) =>
|
||||||
|
readonlyView(Reflect.apply(value, undefined, args));
|
||||||
|
views.set(value, wrapped);
|
||||||
|
return Object.freeze(wrapped);
|
||||||
|
}
|
||||||
|
|
||||||
const view = new Proxy(value, {
|
const view = new Proxy(value, {
|
||||||
defineProperty: () => false,
|
defineProperty: () => false,
|
||||||
deleteProperty: () => false,
|
deleteProperty: () => false,
|
||||||
get: (target, property) => readonlyView(Reflect.get(target, property)),
|
get: (target, property, receiver) => {
|
||||||
|
const raw = Reflect.get(target, property, receiver);
|
||||||
|
if (typeof raw !== "function") return readonlyView(raw);
|
||||||
|
return (...args: unknown[]) =>
|
||||||
|
readonlyView(Reflect.apply(raw, receiver, args));
|
||||||
|
},
|
||||||
set: () => false,
|
set: () => false,
|
||||||
setPrototypeOf: () => false,
|
setPrototypeOf: () => false,
|
||||||
});
|
});
|
||||||
@@ -259,7 +407,7 @@ function createReadonlyValidationFacade<T extends z.ZodType>(schema: T): T {
|
|||||||
? (...args: unknown[]) => {
|
? (...args: unknown[]) => {
|
||||||
const result = Reflect.apply(raw, target, args);
|
const result = Reflect.apply(raw, target, args);
|
||||||
return result instanceof z.ZodType
|
return result instanceof z.ZodType
|
||||||
? isolateValidationGraph(result)
|
? isolateValidationGraph(result, "derived-schema")
|
||||||
: result;
|
: result;
|
||||||
}
|
}
|
||||||
: readonlyView(raw);
|
: readonlyView(raw);
|
||||||
|
|||||||
Reference in new issue
Block a user