/** * LIVE-02 / LIVE-03. A composed registry is authority, not data. * * `Object.freeze(new Map(...))` only freezes the wrapper object: `set`, * `delete` and `clear` still reach the backing store, so anything holding the * exported singleton can empty a validated registry after composition and * silently change what every later request resolves. The fix is structural — * the store stays private in a closure and only read operations are exported. * * The facade is deliberately *not* a `Map` instance, so borrowing a mutator * (`Map.prototype.clear.call(facade)`) fails on the missing internal slot * rather than succeeding. */ export type ReadOnlyRegistry = Readonly<{ get(key: Key): Value | undefined; has(key: Key): boolean; keys(): IterableIterator; values(): IterableIterator; entries(): IterableIterator; forEach(visit: (value: Value, key: Key) => void): void; readonly size: number; [Symbol.iterator](): IterableIterator; }>; export function createReadOnlyRegistry( entries: Iterable, ): ReadOnlyRegistry { const store = new Map(entries as Iterable<[Key, Value]>); const facade = { get: (key: Key) => store.get(key), has: (key: Key) => store.has(key), keys: () => store.keys(), values: () => store.values(), entries: () => store.entries(), forEach: (visit: (value: Value, key: Key) => void) => { for (const [key, value] of store) visit(value, key); }, get size() { return store.size; }, [Symbol.iterator]: () => store.entries(), }; return Object.freeze(facade) as ReadOnlyRegistry; } /** * Rejects anything that is not an exact own-data record over `allowedKeys`. * * A validated row must survive the validation: an accessor re-runs on every * later read, an inherited field can be replaced through the prototype, and a * symbol-keyed field escapes a name-based sweep entirely. Only own data * descriptors are copied, and the result is frozen. */ export function exactOwnDataSnapshot( source: unknown, allowedKeys: readonly (keyof Shape & string)[], requiredKeys: readonly (keyof Shape & string)[], onViolation: (detail: string) => never, ): Readonly { if (source === null || typeof source !== "object") { onViolation("object required"); } if (Object.getOwnPropertySymbols(source).length > 0) { onViolation("symbol-keyed field"); } const allowed = new Set(allowedKeys); const snapshot: Record = {}; for (const key of Object.getOwnPropertyNames(source)) { if (!allowed.has(key)) onViolation(`unexpected field ${key}`); const descriptor = Object.getOwnPropertyDescriptor(source, key); if (!descriptor || !("value" in descriptor)) { onViolation(`accessor field ${key}`); } snapshot[key] = descriptor.value; } for (const key of requiredKeys) { if (!Object.hasOwn(snapshot, key)) onViolation(`missing field ${key}`); } return Object.freeze(snapshot) as Readonly; }