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
@@ -1,3 +1,7 @@
|
||||
import {
|
||||
createReadOnlyRegistry,
|
||||
type ReadOnlyRegistry,
|
||||
} from "./read-only-registry.ts";
|
||||
import type { InstalledBoundaryMapper } from "./boundary-mapper.ts";
|
||||
import type { RuntimeSchemaCodec } from "./schema-registry.ts";
|
||||
|
||||
@@ -327,6 +331,209 @@ export function composeBrowserRpcRequestEncoderRegistry(
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* RPC-RR-03. Read facades, never `Map`s. `Object.freeze(new Map(...))` leaves
|
||||
* `set`, `delete` and `clear` working, so an installed registry could still be
|
||||
* emptied or re-pointed after the snapshot was validated.
|
||||
*/
|
||||
export type InstalledBrowserRpcContractBindings = Readonly<{
|
||||
operations: ReadOnlyRegistry<string, BrowserRpcOperationV3>;
|
||||
profiles: ReadOnlyRegistry<string, BrowserRpcProviderProfile>;
|
||||
schemaCodecs: ReadOnlyRegistry<string, RuntimeSchemaCodec>;
|
||||
mappers: ReadOnlyRegistry<string, InstalledBoundaryMapper>;
|
||||
requestEncoders: ReadOnlyRegistry<string, BrowserRpcRequestEncoder>;
|
||||
runtimeBindings: ReadOnlyRegistry<string, BrowserRpcRuntimeBindingIdentity>;
|
||||
}>;
|
||||
|
||||
/**
|
||||
* R-04. Parse → validate → install.
|
||||
*
|
||||
* `Readonly` is a TypeScript annotation, not a runtime guarantee, and a source
|
||||
* registry can be mutated after validation so replay policy, deadlines, byte
|
||||
* ceilings or transport selection differ from what was checked. Every row is
|
||||
* therefore copied once into a frozen null-prototype snapshot built from exact
|
||||
* own data properties. A getter, an extra or symbol key, a malformed descriptor
|
||||
* or a revoked proxy is a composition-time `TypeError`, and the runtime reads
|
||||
* only the snapshot afterwards.
|
||||
*/
|
||||
function installRegistrySnapshot<Value extends object>(
|
||||
source: Readonly<Record<string, Value>>,
|
||||
label: string,
|
||||
allowedKeys: readonly string[],
|
||||
): ReadOnlyRegistry<string, Value> {
|
||||
let ownKeys: string[];
|
||||
let symbols: readonly symbol[];
|
||||
let prototype: object | null;
|
||||
try {
|
||||
// RPC-03. Own *names*, not just enumerable keys: a non-enumerable own entry
|
||||
// is as much a smuggled row as an inherited one, and `Object.keys` never
|
||||
// saw either.
|
||||
ownKeys = Object.getOwnPropertyNames(source);
|
||||
symbols = Object.getOwnPropertySymbols(source);
|
||||
prototype = Reflect.getPrototypeOf(source);
|
||||
} catch {
|
||||
throw new TypeError(`Browser RPC ${label} registry is unreadable.`);
|
||||
}
|
||||
if (symbols.length > 0) {
|
||||
throw new TypeError(`Browser RPC ${label} registry has symbol keys.`);
|
||||
}
|
||||
if (prototype !== Object.prototype && prototype !== null) {
|
||||
throw new TypeError(`Browser RPC ${label} registry has a custom prototype.`);
|
||||
}
|
||||
const installed = new Map<string, Value>();
|
||||
for (const key of ownKeys) {
|
||||
const descriptor = Object.getOwnPropertyDescriptor(source, key);
|
||||
if (!descriptor || !("value" in descriptor)) {
|
||||
throw new TypeError(
|
||||
`Browser RPC ${label} registry entry is not a data property: ${key}`,
|
||||
);
|
||||
}
|
||||
installed.set(
|
||||
key,
|
||||
installRowSnapshot(descriptor.value as Value, `${label}.${key}`, allowedKeys),
|
||||
);
|
||||
}
|
||||
return createReadOnlyRegistry(installed);
|
||||
}
|
||||
|
||||
function installRowSnapshot<Value extends object>(
|
||||
row: Value,
|
||||
label: string,
|
||||
allowedKeys: readonly string[],
|
||||
): Value {
|
||||
if (!row || typeof row !== "object") {
|
||||
throw new TypeError(`Browser RPC ${label} row is not an object.`);
|
||||
}
|
||||
let ownKeys: string[];
|
||||
let symbols: readonly symbol[];
|
||||
let prototype: object | null;
|
||||
try {
|
||||
ownKeys = Object.getOwnPropertyNames(row);
|
||||
symbols = Object.getOwnPropertySymbols(row);
|
||||
prototype = Reflect.getPrototypeOf(row);
|
||||
} catch {
|
||||
throw new TypeError(`Browser RPC ${label} row is unreadable.`);
|
||||
}
|
||||
if (symbols.length > 0) {
|
||||
throw new TypeError(`Browser RPC ${label} row has symbol keys.`);
|
||||
}
|
||||
// RPC-03. A custom prototype carries fields the name sweep never sees and
|
||||
// stays live after installation, so the installed row would not be the row
|
||||
// that was checked.
|
||||
if (prototype !== Object.prototype && prototype !== null) {
|
||||
throw new TypeError(`Browser RPC ${label} row has a custom prototype.`);
|
||||
}
|
||||
const snapshot = Object.create(null) as Record<string, unknown>;
|
||||
for (const key of ownKeys) {
|
||||
if (!allowedKeys.includes(key)) {
|
||||
throw new TypeError(
|
||||
`Browser RPC ${label} row has an unexpected key: ${key}`,
|
||||
);
|
||||
}
|
||||
const descriptor = Object.getOwnPropertyDescriptor(row, key);
|
||||
// Reading an accessor would invoke a getter; refuse without calling it.
|
||||
if (!descriptor || !("value" in descriptor)) {
|
||||
throw new TypeError(
|
||||
`Browser RPC ${label} row key is not a data property: ${key}`,
|
||||
);
|
||||
}
|
||||
const value = descriptor.value as unknown;
|
||||
snapshot[key] = Array.isArray(value)
|
||||
? Object.freeze([...value])
|
||||
: value;
|
||||
}
|
||||
return Object.freeze(snapshot) as Value;
|
||||
}
|
||||
|
||||
const OPERATION_KEYS = Object.freeze([
|
||||
"contractVersion", "operationId", "owner", "protocol", "semantics",
|
||||
"replayPolicy", "idempotencyKeyPolicy", "idempotencyLevel",
|
||||
"dataClassification", "runtimeProfileId", "providerId",
|
||||
"fullyQualifiedService", "method", "rpcKind", "requestMessageId",
|
||||
"responseMessageId", "descriptorArtifactId", "descriptorDigest",
|
||||
"requestSchemaId", "responseSchemaId", "requestEncoderId", "mapperId",
|
||||
"authProfileId", "csrfProfileId", "errorProfileId", "deadlineProfileId",
|
||||
"retryProfileId", "serverStateProfileId", "maxRequestMessageBytes",
|
||||
"maxResponseMessageBytes", "maxResponseMessages", "maxTotalResponseBytes",
|
||||
"maxBufferedBytes", "idleDeadlineMs", "totalDeadlineMs",
|
||||
] as const);
|
||||
const PROFILE_KEYS = Object.freeze([
|
||||
"runtimeProfileId", "providerId", "fixedBaseUrl", "runtimeId",
|
||||
"runtimeVersion", "runtimeDigest", "protocol", "runtimeKind",
|
||||
"clientApiKind", "rpcKind", "messageEncoding", "framing", "requestMethod",
|
||||
"descriptorArtifactId", "descriptorDigest", "allowedProcedures",
|
||||
"authProfileId", "csrfProfileId", "corsProfileId", "errorProfileId",
|
||||
"deadlineProfileId", "retryProfileId", "retryOwner", "maxAttempts",
|
||||
"backoffMs", "retryableFailures", "maxRetryAfterMs", "deadlineDialect",
|
||||
"cancelDialect", "rawByteCeilingOwner", "streamMessageCompression",
|
||||
] as const);
|
||||
const SCHEMA_KEYS = Object.freeze(["schemaId", "parse"] as const);
|
||||
const MAPPER_KEYS = Object.freeze([
|
||||
"mapperId", "mapperVersion", "inputSchemaId", "outputContractId", "owner",
|
||||
"maxOutputItems", "map",
|
||||
] as const);
|
||||
const ENCODER_KEYS = Object.freeze([
|
||||
"encoderId", "operationId", "encode",
|
||||
] as const);
|
||||
const RUNTIME_BINDING_KEYS = Object.freeze([
|
||||
"runtimeProfileId", "providerId", "protocol", "rpcKind",
|
||||
] as const);
|
||||
|
||||
export function installBrowserRpcContractBindings(
|
||||
bindings: BrowserRpcContractBindings,
|
||||
): InstalledBrowserRpcContractBindings {
|
||||
// Parse first. Snapshotting from own data descriptors rejects accessors
|
||||
// without ever invoking them, so a hostile getter cannot observe validation
|
||||
// or return a different value to it than to the runtime.
|
||||
const operations = installRegistrySnapshot(
|
||||
bindings.operations,
|
||||
"operation",
|
||||
OPERATION_KEYS,
|
||||
);
|
||||
const profiles = installRegistrySnapshot(
|
||||
bindings.profiles,
|
||||
"profile",
|
||||
PROFILE_KEYS,
|
||||
);
|
||||
const schemaCodecs = installRegistrySnapshot(
|
||||
bindings.schemaCodecs,
|
||||
"schema",
|
||||
SCHEMA_KEYS,
|
||||
);
|
||||
const mappers = installRegistrySnapshot(
|
||||
bindings.mappers,
|
||||
"mapper",
|
||||
MAPPER_KEYS,
|
||||
);
|
||||
const requestEncoders = installRegistrySnapshot(
|
||||
bindings.requestEncoders,
|
||||
"encoder",
|
||||
ENCODER_KEYS,
|
||||
);
|
||||
const runtimeBindings = installRegistrySnapshot(
|
||||
bindings.runtimeBindings ?? {},
|
||||
"runtime",
|
||||
RUNTIME_BINDING_KEYS,
|
||||
);
|
||||
// Then validate the snapshot, so what was checked is exactly what installs.
|
||||
validateBrowserRpcContractBindings({
|
||||
operations: Object.fromEntries(operations),
|
||||
profiles: Object.fromEntries(profiles),
|
||||
schemaCodecs: Object.fromEntries(schemaCodecs),
|
||||
mappers: Object.fromEntries(mappers),
|
||||
requestEncoders: Object.fromEntries(requestEncoders),
|
||||
runtimeBindings: Object.fromEntries(runtimeBindings),
|
||||
});
|
||||
return Object.freeze({
|
||||
operations,
|
||||
profiles,
|
||||
schemaCodecs,
|
||||
mappers,
|
||||
requestEncoders,
|
||||
runtimeBindings,
|
||||
});
|
||||
}
|
||||
|
||||
export function validateBrowserRpcContractBindings(
|
||||
bindings: BrowserRpcContractBindings,
|
||||
): true {
|
||||
|
||||
Reference in New Issue
Block a user