import { SecurityError } from "./errors.ts"; const DANGEROUS_KEYS = new Set(["__proto__", "prototype", "constructor"]); export interface SafeObjectOptions { maxDepth?: number; maxKeys?: number; allowInstances?: boolean; } export function isDangerousObjectKey(key: string): boolean { return DANGEROUS_KEYS.has(key); } export function assertSafeObject(value: unknown, options: SafeObjectOptions = {}): void { const maxDepth = options.maxDepth ?? 32; const maxKeys = options.maxKeys ?? 10_000; const seen = new WeakSet(); let keyCount = 0; const visit = (current: unknown, depth: number): void => { if (current === null || typeof current !== "object") return; if (depth > maxDepth) { throw new SecurityError("WRN-SEC-OBJECT-DEPTH", `Object depth exceeds ${maxDepth}.`, 413); } if (seen.has(current)) { throw new SecurityError("WRN-SEC-OBJECT-CYCLE", "Cyclic objects are not accepted."); } seen.add(current); const proto = Object.getPrototypeOf(current); if ( !options.allowInstances && !Array.isArray(current) && proto !== Object.prototype && proto !== null && !(current instanceof Date) && !(current instanceof URL) ) { throw new SecurityError( "WRN-SEC-OBJECT-PROTOTYPE", "Only plain objects, arrays, dates, and URLs are accepted.", ); } for (const key of Object.keys(current)) { keyCount++; if (keyCount > maxKeys) { throw new SecurityError( "WRN-SEC-OBJECT-KEYS", `Object contains more than ${maxKeys} keys.`, 413, ); } if (isDangerousObjectKey(key)) { throw new SecurityError( "WRN-SEC-PROTOTYPE-POLLUTION", `Dangerous object key '${key}' is not allowed.`, ); } visit((current as Record)[key], depth + 1); } seen.delete(current); }; visit(value, 0); } export function safeMerge>( target: T, ...sources: Array | undefined | null> ): T { for (const source of sources) { if (!source) continue; assertSafeObject(source); for (const [key, value] of Object.entries(source)) { if (isDangerousObjectKey(key)) continue; const existing = target[key]; if ( value && existing && typeof value === "object" && typeof existing === "object" && !Array.isArray(value) && !Array.isArray(existing) ) { safeMerge(existing as Record, value as Record); } else { target[key as keyof T] = value as T[keyof T]; } } } return target; }