fix(housekeeping): harden command dispatch boundaries
This commit is contained in:
@@ -32,6 +32,13 @@ export interface HousekeepingCommand<I, O> {
|
||||
|
||||
type RegisteredHousekeepingCommand = HousekeepingCommand<unknown, unknown>;
|
||||
|
||||
type ZodInternalNode = {
|
||||
readonly _zod: {
|
||||
readonly constr: new (definition: never) => unknown;
|
||||
readonly def: unknown;
|
||||
};
|
||||
};
|
||||
|
||||
const approvedOwners = new Set<string>(HOUSEKEEPING_DOMAIN_IDS);
|
||||
const knownCapabilitySlugs = new Set<string>(Object.values(PERMS));
|
||||
const commandIdPattern = /^[a-z][a-z0-9-]*(?:\.[a-z0-9][a-z0-9-]*)+$/;
|
||||
@@ -56,7 +63,7 @@ export function registerHousekeepingCommand<I, O>(
|
||||
owner: command.owner,
|
||||
risk: command.risk,
|
||||
capability,
|
||||
input: Object.freeze(command.input.clone()),
|
||||
input: isolateValidationGraph(command.input),
|
||||
requiresReason: command.requiresReason,
|
||||
rateLimit: Object.freeze({ ...command.rateLimit }),
|
||||
execute: command.execute,
|
||||
@@ -74,11 +81,12 @@ export function getHousekeepingCommand(
|
||||
return commands.get(id);
|
||||
}
|
||||
|
||||
export function isHousekeepingCommandId(value: unknown): value is string {
|
||||
return isNormalizedIdentifier(value) && commandIdPattern.test(value);
|
||||
}
|
||||
|
||||
function validateCommand<I, O>(command: HousekeepingCommand<I, O>): void {
|
||||
if (
|
||||
!isNormalizedIdentifier(command.id) ||
|
||||
!commandIdPattern.test(command.id)
|
||||
) {
|
||||
if (!isHousekeepingCommandId(command.id)) {
|
||||
throw new Error(`invalid command id: ${String(command.id)}`);
|
||||
}
|
||||
if (
|
||||
@@ -135,6 +143,138 @@ function validateCapability(
|
||||
}
|
||||
}
|
||||
|
||||
function isolateValidationGraph<T extends z.ZodType>(schema: T): T {
|
||||
const seen = new WeakMap<object, unknown>();
|
||||
|
||||
function cloneGraph(value: unknown): unknown {
|
||||
if (typeof value !== "object" || value === null) return value;
|
||||
const cached = seen.get(value);
|
||||
if (cached !== undefined) return cached;
|
||||
|
||||
if (value instanceof z.ZodType) {
|
||||
const clonedDefinition = cloneGraph(value.def);
|
||||
const cloned = value.clone(clonedDefinition as typeof value.def);
|
||||
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)) {
|
||||
const cloned: unknown[] = [];
|
||||
seen.set(value, cloned);
|
||||
for (const item of value) cloned.push(cloneGraph(item));
|
||||
return cloned;
|
||||
}
|
||||
if (value instanceof RegExp) {
|
||||
const cloned = new RegExp(value.source, value.flags);
|
||||
seen.set(value, cloned);
|
||||
return cloned;
|
||||
}
|
||||
if (value instanceof Date) {
|
||||
const cloned = new Date(value.getTime());
|
||||
seen.set(value, cloned);
|
||||
return cloned;
|
||||
}
|
||||
|
||||
const cloned = Object.create(Object.getPrototypeOf(value)) as Record<
|
||||
PropertyKey,
|
||||
unknown
|
||||
>;
|
||||
seen.set(value, cloned);
|
||||
for (const key of Reflect.ownKeys(value)) {
|
||||
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;
|
||||
}
|
||||
|
||||
return createReadonlyValidationFacade(cloneGraph(schema) as T);
|
||||
}
|
||||
|
||||
function isZodInternalNode(value: object): value is ZodInternalNode {
|
||||
const internal = (value as { _zod?: unknown })._zod;
|
||||
return (
|
||||
typeof internal === "object" &&
|
||||
internal !== null &&
|
||||
typeof (internal as { constr?: unknown }).constr === "function" &&
|
||||
"def" in internal
|
||||
);
|
||||
}
|
||||
|
||||
function createReadonlyValidationFacade<T extends z.ZodType>(schema: T): T {
|
||||
const views = new WeakMap<object, unknown>();
|
||||
|
||||
function readonlyView(value: unknown): unknown {
|
||||
if (typeof value !== "object" || value === null) return value;
|
||||
const cached = views.get(value);
|
||||
if (cached !== undefined) return cached;
|
||||
if (value instanceof z.ZodType) return schemaFacade(value);
|
||||
|
||||
const view = new Proxy(value, {
|
||||
defineProperty: () => false,
|
||||
deleteProperty: () => false,
|
||||
get: (target, property) => readonlyView(Reflect.get(target, property)),
|
||||
set: () => false,
|
||||
setPrototypeOf: () => false,
|
||||
});
|
||||
views.set(value, view);
|
||||
return view;
|
||||
}
|
||||
|
||||
function schemaFacade<S extends z.ZodType>(target: S): S {
|
||||
const cached = views.get(target);
|
||||
if (cached !== undefined) return cached as S;
|
||||
|
||||
const facade = Object.create(Object.getPrototypeOf(target)) as Record<
|
||||
PropertyKey,
|
||||
unknown
|
||||
>;
|
||||
views.set(target, facade);
|
||||
for (const key of Reflect.ownKeys(target)) {
|
||||
const raw = Reflect.get(target, key);
|
||||
const exposed =
|
||||
typeof raw === "function"
|
||||
? (...args: unknown[]) => {
|
||||
const result = Reflect.apply(raw, target, args);
|
||||
return result instanceof z.ZodType
|
||||
? isolateValidationGraph(result)
|
||||
: result;
|
||||
}
|
||||
: readonlyView(raw);
|
||||
Object.defineProperty(facade, key, {
|
||||
configurable: false,
|
||||
enumerable: Object.prototype.propertyIsEnumerable.call(target, key),
|
||||
value: exposed,
|
||||
writable: false,
|
||||
});
|
||||
}
|
||||
return Object.freeze(facade) as S;
|
||||
}
|
||||
|
||||
return schemaFacade(schema);
|
||||
}
|
||||
function isNormalizedIdentifier(value: unknown): value is string {
|
||||
return (
|
||||
typeof value === "string" &&
|
||||
|
||||
Reference in New Issue
Block a user