/** * Descriptor-based exact decoding for values that cross a trust boundary. * * Several adapters independently wrote "check the shape, then read it again to * copy it". That order is the bug: between the check and the copy an accessor * or a Proxy can answer differently, so the value that was validated and the * value that was installed are two different things. Every helper here reads a * property exactly once, through its own data descriptor, and hands back an * owned plain object. Validation then runs on the snapshot, never on the source. * * The helpers are total: a hostile `getPrototypeOf`, `ownKeys` or * `getOwnPropertyDescriptor` trap yields `null`, never a thrown exception, so a * caller can keep its own typed failure vocabulary. */ const DEFAULT_PROTOTYPES: readonly (object | null)[] = Object.freeze([ Object.prototype, null, ]); export type ExactObjectPolicy = Readonly<{ /** Every own key the value may carry. Anything else rejects the snapshot. */ allowed: readonly string[]; /** Keys that must be present as own data properties. */ required?: readonly string[]; /** * Prototypes the value may have. Defaults to a plain object or a null * prototype, which is what a decoded wire payload or a literal produces. */ prototypes?: readonly (object | null)[]; }>; /** * Reads `source[key]` exactly once through its own data descriptor. An accessor, * an inherited property or a missing key all answer `undefined`, and a trap that * throws answers `undefined` rather than escaping. */ export function ownDataValue(source: unknown, key: string): unknown { if (source === null || typeof source !== "object") return undefined; try { const descriptor = Object.getOwnPropertyDescriptor(source, key); if (!descriptor || !("value" in descriptor)) return undefined; return descriptor.value; } catch { return undefined; } } /** True when `key` is present as an own data property. */ export function hasOwnDataKey(source: unknown, key: string): boolean { if (source === null || typeof source !== "object") return false; try { const descriptor = Object.getOwnPropertyDescriptor(source, key); return Boolean(descriptor) && "value" in (descriptor as PropertyDescriptor); } catch { return false; } } /** * Copies `source` into a frozen plain object, reading every property exactly * once. Returns `null` when the value is not an object, carries a symbol or an * unexpected own key, exposes an accessor, has an unapproved prototype, misses a * required key, or makes any reflection operation throw. */ export function snapshotExactObject( source: unknown, policy: ExactObjectPolicy, ): Readonly> | null { if (source === null || typeof source !== "object") return null; try { const prototypes = policy.prototypes ?? DEFAULT_PROTOTYPES; if (!prototypes.includes(Reflect.getPrototypeOf(source))) return null; if (Object.getOwnPropertySymbols(source).length > 0) return null; const allowed = new Set(policy.allowed); const names = Object.getOwnPropertyNames(source); const snapshot: Record = {}; for (const name of names) { if (!allowed.has(name)) return null; const descriptor = Object.getOwnPropertyDescriptor(source, name); // A non-enumerable own property is as much a smuggled field as an // inherited one, and an accessor is a second read waiting to happen. if ( !descriptor || !("value" in descriptor) || descriptor.enumerable !== true ) { return null; } Object.defineProperty(snapshot, name, { value: descriptor.value, enumerable: true, writable: false, configurable: false, }); } for (const name of policy.required ?? []) { if (!Object.hasOwn(snapshot, name)) return null; } return Object.freeze(snapshot); } catch { return null; } } /** * Copies an open-keyed record — a header bag, a query map — into a frozen owned * object, reading every property exactly once. The key set is not constrained * here; admission against an allow-list stays with the policy that owns it, so * the more specific rejection can still be reported. Returns `null` for a * non-object, a symbol key, an accessor, a non-enumerable own key, an * unapproved prototype, more than `maximumKeys` entries, or a throwing trap. */ export function snapshotOwnDataRecord( source: unknown, maximumKeys = 64, ): Readonly> | null { if (source === null || typeof source !== "object") return null; try { if (!DEFAULT_PROTOTYPES.includes(Reflect.getPrototypeOf(source))) { return null; } if (Object.getOwnPropertySymbols(source).length > 0) return null; const names = Object.getOwnPropertyNames(source); if (names.length > maximumKeys) return null; const snapshot: Record = {}; for (const name of names) { const descriptor = Object.getOwnPropertyDescriptor(source, name); if ( !descriptor || !("value" in descriptor) || descriptor.enumerable !== true ) { return null; } Object.defineProperty(snapshot, name, { value: descriptor.value, enumerable: true, writable: false, configurable: false, }); } return Object.freeze(snapshot); } catch { return null; } } /** * Copies a genuine array into a frozen owned array, reading each element exactly * once. Returns `null` for a non-array, a hostile length or a trap that throws. */ export function snapshotExactArray( source: unknown, maximumLength = 4_096, ): readonly unknown[] | null { try { if (!Array.isArray(source)) return null; const length = source.length; if (!Number.isSafeInteger(length) || length < 0 || length > maximumLength) { return null; } const items: unknown[] = []; for (let index = 0; index < length; index += 1) { const descriptor = Object.getOwnPropertyDescriptor(source, String(index)); if (!descriptor || !("value" in descriptor)) return null; items.push(descriptor.value); } return Object.freeze(items); } catch { return null; } }