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>
This commit is contained in:
co-authored by
Claude Opus 5
parent
002ba3624e
commit
4bff9ca151
@@ -0,0 +1,174 @@
|
||||
/**
|
||||
* 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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user