Files
tech-log-frontend/src/contracts/exact-snapshot.ts
T
DongHyeonkaandClaude Opus 5 4bff9ca151 chore: sync the frontend template from 4dc033c to 8157ad4
The product was materialized from the template at `4dc033c` and has stayed
on it through 43 template commits, so it was missing all three rounds of
adapter remediation — including files it never had, such as the shared
`abortable-operation` primitive and the `exact-snapshot` decoder that
later fixes are written against. Taking only the newest round was not
possible for that reason: the delta is coherent only as a whole.

The product had not touched `src/adapters` at all since materialization,
so the 140-file delta applied with a three-way merge and no conflicts.
`package.json` was the single overlap and merged cleanly: the product owns
`name`, the template contributed `check:adapter-inventory`,
`check:remediation-ledger` and the image-resolve-signal type fixture.
All 24 product-owned files — README, index.html, CI workflow, i18n
catalog, home page, generated schemas, evidence scripts, component and
visual snapshots — are byte-identical to `main`.

`template.lock.json` now pins the synced revision and tree.

Verified in this repository, not inherited from the template: six type
projects, lint, nine gates (adapter inventory, remediation ledger,
registries, diagnostics, realtime boundaries, architecture, browser
file/storage boundaries, optional recipes, documentation), the production
build, and 2,054 of 2,073 tests. The 19 failures are all in
`tests/unit/ci-artifact-contract.test.ts` and are the same pre-existing
sandbox RLIMIT, EMFILE, umask and `/tmp` permission behaviour the template
records; four suites that failed once under parallel load pass in
isolation.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-15 12:04:58 +09:00

175 lines
6.1 KiB
TypeScript

/**
* 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<Record<string, unknown>> | 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<string, unknown> = {};
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<Record<string, unknown>> | 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<string, unknown> = {};
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;
}
}