chore: initialize from frontend template 4dc033c

This commit is contained in:
DongHyeonka
2026-08-13 18:23:26 +09:00
commit 40107eec84
897 changed files with 234824 additions and 0 deletions
+208
View File
@@ -0,0 +1,208 @@
export type ApiOperation = Readonly<{
method: string;
path: string;
operationId: string;
auth: "none" | "external-session";
timeoutMs: number | null;
idempotency: "safe" | "keyed" | "none";
retry: "runtime" | "never";
requestSource: "search" | "body" | "none";
requestSchema: string;
responseSchema: string;
owner: string;
contractVersion?: 2;
protocol?: "REST";
semantics?: "QUERY" | "COMMAND";
replayPolicy?: "SAFE" | "IDEMPOTENT" | "KEYED_COMMAND" | "NON_REPLAYABLE";
idempotencyKeyPolicy?: "NONE" | "REQUIRED";
mapperId?: string;
successStatuses?: readonly number[];
responseMediaTypes?: readonly string[];
maxResponseBytes?: number;
providerId?: string;
authProfileId?: string;
csrfProfileId?: string;
pathSchema?: string;
pathParameterNames?: readonly string[];
maxEncodedSearchBytes?: number;
}>;
export type RestOperationV2 = ApiOperation &
Readonly<{
contractVersion: 2;
protocol: "REST";
semantics: "QUERY" | "COMMAND";
replayPolicy:
| "SAFE"
| "IDEMPOTENT"
| "KEYED_COMMAND"
| "NON_REPLAYABLE";
idempotencyKeyPolicy: "NONE" | "REQUIRED";
mapperId: string;
successStatuses: readonly number[];
responseMediaTypes: readonly string[];
maxResponseBytes: number;
providerId: string;
authProfileId: string;
csrfProfileId: string;
pathSchema: string;
pathParameterNames: readonly string[];
maxEncodedSearchBytes: number;
}>;
const MAX_RESPONSE_BYTES = 8 * 1024 * 1024;
const OPERATION_ID = /^[A-Z][A-Z0-9_]{2,79}$/;
const MEDIA_TYPE = /^[a-z0-9!#$&^_.+-]+\/[a-z0-9!#$&^_.+-]+$/;
export function defineRestOperation(
operation: RestOperationV2,
): RestOperationV2 {
validateRestOperation(operation);
return Object.freeze({
...operation,
successStatuses: Object.freeze([...operation.successStatuses]),
responseMediaTypes: Object.freeze([...operation.responseMediaTypes]),
pathParameterNames: Object.freeze([...operation.pathParameterNames]),
});
}
export function composeApiOperations(
contributions: readonly Readonly<Record<string, ApiOperation>>[],
): Readonly<Record<string, ApiOperation>> {
const result: Record<string, ApiOperation> = Object.create(null);
for (const contribution of contributions) {
for (const [registryId, operation] of Object.entries(contribution)) {
if (registryId !== operation.operationId) {
throw new TypeError("API operation registry key does not match operationId.");
}
if (Object.hasOwn(result, registryId)) {
throw new TypeError(`Duplicate API operation: ${registryId}`);
}
if (operation.contractVersion === 2) {
validateRestOperation(operation as RestOperationV2);
}
result[registryId] = operation;
}
}
return Object.freeze(result);
}
export function validateApiRuntimeBindings(
operations: Readonly<Record<string, ApiOperation>>,
schemaMetadata: Readonly<Record<string, Readonly<{ schemaId: string }>>>,
schemaCodecs: Readonly<Record<string, Readonly<{ schemaId: string }>>>,
mappers: Readonly<
Record<
string,
Readonly<{ mapperId: string; inputSchemaId: string; maxOutputItems: number }>
>
>,
): true {
for (const operation of Object.values(operations)) {
if (operation.contractVersion !== 2) continue;
for (const schemaId of [
operation.pathSchema,
operation.requestSchema,
operation.responseSchema,
]) {
if (
!schemaId ||
schemaMetadata[schemaId]?.schemaId !== schemaId ||
schemaCodecs[schemaId]?.schemaId !== schemaId
) {
throw new TypeError(
`Unresolved API schema binding: ${operation.operationId}`,
);
}
}
const mapper = mappers[operation.mapperId ?? ""];
if (
!mapper ||
mapper.mapperId !== operation.mapperId ||
mapper.inputSchemaId !== operation.responseSchema ||
mapper.maxOutputItems < 1
) {
throw new TypeError(
`Unresolved API mapper binding: ${operation.operationId}`,
);
}
}
return true;
}
function validateRestOperation(operation: RestOperationV2): void {
if (
operation.protocol !== "REST" ||
!OPERATION_ID.test(operation.operationId) ||
!operation.owner ||
!operation.mapperId ||
!operation.providerId ||
!operation.authProfileId ||
!operation.csrfProfileId ||
!operation.pathSchema ||
!operation.path.startsWith("/") ||
operation.path.startsWith("//") ||
operation.path.includes("?") ||
operation.path.includes("#") ||
!Number.isSafeInteger(operation.maxResponseBytes) ||
operation.maxResponseBytes < 1 ||
operation.maxResponseBytes > MAX_RESPONSE_BYTES ||
!Number.isSafeInteger(operation.maxEncodedSearchBytes) ||
operation.maxEncodedSearchBytes < 0 ||
operation.maxEncodedSearchBytes > 32_768 ||
operation.successStatuses.length === 0 ||
operation.successStatuses.some(
(status) => !Number.isInteger(status) || status < 200 || status > 299,
) ||
new Set(operation.successStatuses).size !== operation.successStatuses.length ||
operation.responseMediaTypes.length === 0 ||
operation.responseMediaTypes.some(
(value) => !MEDIA_TYPE.test(value) || value !== value.toLowerCase(),
)
) {
throw new TypeError(`Invalid REST operation contract: ${operation.operationId}`);
}
const placeholders = [
...operation.path.matchAll(
/:([A-Za-z][A-Za-z0-9_]*)|\{([A-Za-z][A-Za-z0-9_]*)\}/g,
),
]
.map((match) => match[1] ?? match[2] ?? "")
.sort();
const codecKeys = [...operation.pathParameterNames].sort();
if (
new Set(codecKeys).size !== codecKeys.length ||
placeholders.length !== codecKeys.length ||
placeholders.some((name, index) => name !== codecKeys[index])
) {
throw new TypeError(
`REST path codec does not match its template: ${operation.operationId}`,
);
}
const isQuery = operation.semantics === "QUERY";
if (
(isQuery && !["GET", "HEAD"].includes(operation.method)) ||
(isQuery && !["SAFE", "IDEMPOTENT"].includes(operation.replayPolicy)) ||
(operation.replayPolicy === "KEYED_COMMAND" &&
(operation.idempotencyKeyPolicy !== "REQUIRED" ||
operation.idempotency !== "keyed")) ||
(operation.replayPolicy === "NON_REPLAYABLE" &&
(operation.retry !== "never" || operation.idempotency !== "none"))
) {
throw new TypeError(`Incoherent REST replay contract: ${operation.operationId}`);
}
}
export const API_OPERATIONS: Readonly<Record<string, ApiOperation>> =
Object.freeze({});
export function getApiOperation(
operationId: string,
operations: Readonly<Record<string, ApiOperation>> = API_OPERATIONS,
): ApiOperation {
const selected = operations[operationId];
if (!selected) {
throw new Error(`Unregistered API operation: ${operationId}`);
}
return selected;
}
+69
View File
@@ -0,0 +1,69 @@
export type MappingFailureCode =
| "MAPPING_INVARIANT_REJECTED"
| "UNSUPPORTED_WIRE_VALUE"
| "OUTPUT_LIMIT_EXCEEDED";
export type MappingResult<Value> =
| Readonly<{ ok: true; value: Value }>
| Readonly<{ ok: false; code: MappingFailureCode }>;
export type BoundaryMapper<Input, Output> = Readonly<{
mapperId: string;
mapperVersion: number;
inputSchemaId: string;
outputContractId: string;
owner: string;
maxOutputItems: number;
map(input: Input): MappingResult<Output>;
}>;
export type InstalledBoundaryMapper = BoundaryMapper<unknown, unknown>;
export function composeBoundaryMapperRegistry(
contributions: readonly Readonly<Record<string, InstalledBoundaryMapper>>[],
): Readonly<Record<string, InstalledBoundaryMapper>> {
const result: Record<string, InstalledBoundaryMapper> = Object.create(null);
for (const contribution of contributions) {
for (const [registryId, mapper] of Object.entries(contribution)) {
if (
registryId !== mapper.mapperId ||
!mapper.owner ||
!mapper.inputSchemaId ||
!mapper.outputContractId ||
!Number.isSafeInteger(mapper.mapperVersion) ||
mapper.mapperVersion < 1 ||
!Number.isSafeInteger(mapper.maxOutputItems) ||
mapper.maxOutputItems < 1 ||
Object.hasOwn(result, registryId)
) {
throw new TypeError(
`Invalid or duplicate boundary mapper: ${registryId}`,
);
}
result[registryId] = mapper;
}
}
return Object.freeze(result);
}
export function mapWithBoundaryRegistry(
mapperId: string,
input: unknown,
registry: Readonly<Record<string, InstalledBoundaryMapper>>,
): MappingResult<unknown> {
const mapper = registry[mapperId];
if (!mapper) return mappingFailure("MAPPING_INVARIANT_REJECTED");
try {
return mapper.map(input);
} catch {
return mappingFailure("MAPPING_INVARIANT_REJECTED");
}
}
export function mappingSuccess<Value>(value: Value): MappingResult<Value> {
return Object.freeze({ ok: true, value });
}
export function mappingFailure(code: MappingFailureCode): MappingResult<never> {
return Object.freeze({ ok: false, code });
}
+772
View File
@@ -0,0 +1,772 @@
import type { InstalledBoundaryMapper } from "./boundary-mapper.ts";
import type { RuntimeSchemaCodec } from "./schema-registry.ts";
export const BROWSER_RPC_CONTRACT_VERSION = 3 as const;
export const BROWSER_RPC_HARD_LIMITS = Object.freeze({
maxRequestMessageBytes: 8 * 1024 * 1024,
maxResponseMessageBytes: 8 * 1024 * 1024,
maxTotalResponseBytes: 64 * 1024 * 1024,
maxBufferedBytes: 16 * 1024 * 1024,
maxResponseMessages: 10_000,
maxDeadlineMs: 30 * 60_000,
maxAttempts: 4,
maxBackoffMs: 30_000,
maxRetryAfterMs: 60_000,
maxProfiles: 128,
maxOperations: 512,
});
export type BrowserRpcProtocol = "CONNECT_HTTP" | "GRPC_WEB";
export type BrowserRpcRuntimeKind =
| "CONNECT_WEB_FETCH"
| "OFFICIAL_GRPC_WEB_XHR"
| "CUSTOM_FETCH_FRAMED";
export type BrowserRpcClientApiKind =
| "PROMISE_UNARY"
| "ASYNC_ITERABLE"
| "CALLBACK_STREAM";
export type BrowserRpcKind = "UNARY" | "SERVER_STREAM";
export type BrowserRpcMessageEncoding = "PROTO" | "JSON";
export type BrowserRpcFraming =
| "CONNECT_BARE"
| "CONNECT_ENVELOPE"
| "GRPC_WEB_BINARY_ENVELOPE"
| "GRPC_WEB_BASE64_TEXT";
export type BrowserRpcRequestMethod = "POST" | "GET";
export type BrowserRpcSemantics = "QUERY" | "COMMAND" | "SERVER_STREAM";
export type BrowserRpcReplayPolicy =
| "SAFE"
| "IDEMPOTENT"
| "KEYED_COMMAND"
| "NON_REPLAYABLE";
export type BrowserRpcIdempotencyLevel =
| "NONE"
| "IDEMPOTENT"
| "NO_SIDE_EFFECTS";
export type BrowserRpcDataClassification =
| "PUBLIC"
| "INTERNAL"
| "CONFIDENTIAL";
export type BrowserRpcRetryOwner =
| "FRONTEND_ADAPTER"
| "EDGE_PROXY"
| "NONE";
export type BrowserRpcRawByteCeilingOwner =
| "EDGE_PROXY"
| "BOUNDED_TRANSPORT"
| "EDGE_AND_TRANSPORT";
export type BrowserRpcDeadlineDialect =
| "CONNECT_TIMEOUT_MS"
| "GRPC_TIMEOUT"
| "OFFICIAL_DEADLINE_METADATA";
export type BrowserRpcCancelDialect =
| "ABORT_SIGNAL"
| "CLIENT_READABLE_STREAM_CANCEL";
export type BrowserRpcTransportFailureCode =
| "NETWORK_UNREACHABLE"
| "CANCELED"
| "DEADLINE_EXCEEDED"
| "UNAUTHENTICATED"
| "PERMISSION_DENIED"
| "NOT_FOUND"
| "ALREADY_EXISTS"
| "ABORTED"
| "FAILED_PRECONDITION"
| "INVALID_ARGUMENT"
| "RESOURCE_EXHAUSTED"
| "UNAVAILABLE"
| "UNIMPLEMENTED"
| "INTERNAL"
| "DATA_LOSS"
| "PROTOCOL_MISMATCH"
| "MESSAGE_LIMIT";
export type BrowserRpcOperationV3 = Readonly<{
contractVersion: typeof BROWSER_RPC_CONTRACT_VERSION;
operationId: string;
owner: string;
protocol: BrowserRpcProtocol;
semantics: BrowserRpcSemantics;
replayPolicy: BrowserRpcReplayPolicy;
idempotencyKeyPolicy: "NONE" | "REQUIRED";
idempotencyLevel: BrowserRpcIdempotencyLevel;
dataClassification: BrowserRpcDataClassification;
runtimeProfileId: string;
providerId: string;
fullyQualifiedService: string;
method: string;
rpcKind: BrowserRpcKind;
requestMessageId: string;
responseMessageId: string;
descriptorArtifactId: string;
descriptorDigest: string;
requestSchemaId: string;
responseSchemaId: string;
requestEncoderId: string;
mapperId: string;
authProfileId: string;
csrfProfileId: string;
errorProfileId: string;
deadlineProfileId: string;
retryProfileId: string;
serverStateProfileId: string | null;
maxRequestMessageBytes: number;
maxResponseMessageBytes: number;
maxResponseMessages: number;
maxTotalResponseBytes: number;
maxBufferedBytes: number;
idleDeadlineMs: number | null;
totalDeadlineMs: number;
}>;
export type BrowserRpcProviderProfile = Readonly<{
runtimeProfileId: string;
providerId: string;
fixedBaseUrl: string;
runtimeId: string;
runtimeVersion: string;
runtimeDigest: string;
protocol: BrowserRpcProtocol;
runtimeKind: BrowserRpcRuntimeKind;
clientApiKind: BrowserRpcClientApiKind;
rpcKind: BrowserRpcKind;
messageEncoding: BrowserRpcMessageEncoding;
framing: BrowserRpcFraming;
requestMethod: BrowserRpcRequestMethod;
descriptorArtifactId: string;
descriptorDigest: string;
allowedProcedures: readonly string[];
authProfileId: string;
csrfProfileId: string;
corsProfileId: string;
errorProfileId: string;
deadlineProfileId: string;
retryProfileId: string;
retryOwner: BrowserRpcRetryOwner;
maxAttempts: number;
backoffMs: readonly number[];
retryableFailures: readonly BrowserRpcTransportFailureCode[];
maxRetryAfterMs: number;
deadlineDialect: BrowserRpcDeadlineDialect;
cancelDialect: BrowserRpcCancelDialect;
rawByteCeilingOwner: BrowserRpcRawByteCeilingOwner;
streamMessageCompression: "IDENTITY_ONLY";
}>;
export type BrowserRpcRequestEncoder = Readonly<{
encoderId: string;
operationId: string;
encode(value: unknown):
| Readonly<{ ok: true; value: unknown; encodedBytes: number }>
| Readonly<{ ok: false; code: string }>;
}>;
export type BrowserRpcRuntimeBindingIdentity = Readonly<{
runtimeProfileId: string;
providerId: string;
protocol: BrowserRpcProtocol;
rpcKind: BrowserRpcKind;
}>;
export type BrowserRpcContractBindings = Readonly<{
operations: Readonly<Record<string, BrowserRpcOperationV3>>;
profiles: Readonly<Record<string, BrowserRpcProviderProfile>>;
schemaCodecs: Readonly<Record<string, RuntimeSchemaCodec>>;
mappers: Readonly<Record<string, InstalledBoundaryMapper>>;
requestEncoders: Readonly<Record<string, BrowserRpcRequestEncoder>>;
runtimeBindings?: Readonly<Record<string, BrowserRpcRuntimeBindingIdentity>>;
}>;
const REGISTRY_ID = /^[A-Z][A-Z0-9_]{2,79}$/;
const ARTIFACT_ID = /^[A-Za-z][A-Za-z0-9_.:-]{2,159}$/;
const OWNER = /^[a-z][a-z0-9-]{2,159}$/;
const SERVICE =
/^(?:[a-z][a-z0-9_]*\.)+[A-Z][A-Za-z0-9_]{1,79}$/;
const METHOD = /^[A-Z][A-Za-z0-9_]{1,79}$/;
const VERSION = /^[0-9A-Za-z][0-9A-Za-z.+_-]{0,79}$/;
const SHA256 = /^[a-f0-9]{64}$/;
const RETRYABLE_FAILURES = new Set<BrowserRpcTransportFailureCode>([
"NETWORK_UNREACHABLE",
"RESOURCE_EXHAUSTED",
"UNAVAILABLE",
]);
const PROTOCOLS = new Set<string>(["CONNECT_HTTP", "GRPC_WEB"]);
const RUNTIME_KINDS = new Set<string>([
"CONNECT_WEB_FETCH",
"OFFICIAL_GRPC_WEB_XHR",
"CUSTOM_FETCH_FRAMED",
]);
const CLIENT_API_KINDS = new Set<string>([
"PROMISE_UNARY",
"ASYNC_ITERABLE",
"CALLBACK_STREAM",
]);
const RPC_KINDS = new Set<string>(["UNARY", "SERVER_STREAM"]);
const MESSAGE_ENCODINGS = new Set<string>(["PROTO", "JSON"]);
const FRAMINGS = new Set<string>([
"CONNECT_BARE",
"CONNECT_ENVELOPE",
"GRPC_WEB_BINARY_ENVELOPE",
"GRPC_WEB_BASE64_TEXT",
]);
const REQUEST_METHODS = new Set<string>(["POST", "GET"]);
const SEMANTICS = new Set<string>([
"QUERY",
"COMMAND",
"SERVER_STREAM",
]);
const REPLAY_POLICIES = new Set<string>([
"SAFE",
"IDEMPOTENT",
"KEYED_COMMAND",
"NON_REPLAYABLE",
]);
const IDEMPOTENCY_KEY_POLICIES = new Set<string>(["NONE", "REQUIRED"]);
const IDEMPOTENCY_LEVELS = new Set<string>([
"NONE",
"IDEMPOTENT",
"NO_SIDE_EFFECTS",
]);
const DATA_CLASSIFICATIONS = new Set<string>([
"PUBLIC",
"INTERNAL",
"CONFIDENTIAL",
]);
const RETRY_OWNERS = new Set<string>([
"FRONTEND_ADAPTER",
"EDGE_PROXY",
"NONE",
]);
const DEADLINE_DIALECTS = new Set<string>([
"CONNECT_TIMEOUT_MS",
"GRPC_TIMEOUT",
"OFFICIAL_DEADLINE_METADATA",
]);
const CANCEL_DIALECTS = new Set<string>([
"ABORT_SIGNAL",
"CLIENT_READABLE_STREAM_CANCEL",
]);
const RAW_BYTE_CEILING_OWNERS = new Set<string>([
"EDGE_PROXY",
"BOUNDED_TRANSPORT",
"EDGE_AND_TRANSPORT",
]);
export function defineBrowserRpcOperation(
operation: BrowserRpcOperationV3,
): BrowserRpcOperationV3 {
validateOperation(operation);
return Object.freeze({ ...operation });
}
export function defineBrowserRpcProviderProfile(
profile: BrowserRpcProviderProfile,
): BrowserRpcProviderProfile {
validateProviderProfile(profile);
return Object.freeze({
...profile,
allowedProcedures: Object.freeze([...profile.allowedProcedures]),
backoffMs: Object.freeze([...profile.backoffMs]),
retryableFailures: Object.freeze([...profile.retryableFailures]),
});
}
export function defineBrowserRpcRequestEncoder(
encoder: BrowserRpcRequestEncoder,
): BrowserRpcRequestEncoder {
if (
!ARTIFACT_ID.test(encoder.encoderId) ||
!REGISTRY_ID.test(encoder.operationId) ||
typeof encoder.encode !== "function"
) {
throw new TypeError("Browser RPC request encoder is invalid.");
}
return Object.freeze({ ...encoder });
}
export function composeBrowserRpcOperationRegistry(
contributions: readonly Readonly<
Record<string, BrowserRpcOperationV3>
>[],
): Readonly<Record<string, BrowserRpcOperationV3>> {
return composeRegistry(
contributions,
(operation) => operation.operationId,
defineBrowserRpcOperation,
"operation",
BROWSER_RPC_HARD_LIMITS.maxOperations,
);
}
export function composeBrowserRpcProviderProfileRegistry(
contributions: readonly Readonly<
Record<string, BrowserRpcProviderProfile>
>[],
): Readonly<Record<string, BrowserRpcProviderProfile>> {
return composeRegistry(
contributions,
(profile) => profile.runtimeProfileId,
defineBrowserRpcProviderProfile,
"provider profile",
BROWSER_RPC_HARD_LIMITS.maxProfiles,
);
}
export function composeBrowserRpcRequestEncoderRegistry(
contributions: readonly Readonly<
Record<string, BrowserRpcRequestEncoder>
>[],
): Readonly<Record<string, BrowserRpcRequestEncoder>> {
return composeRegistry(
contributions,
(encoder) => encoder.encoderId,
defineBrowserRpcRequestEncoder,
"request encoder",
BROWSER_RPC_HARD_LIMITS.maxOperations,
);
}
export function validateBrowserRpcContractBindings(
bindings: BrowserRpcContractBindings,
): true {
for (const [profileId, profile] of Object.entries(bindings.profiles)) {
if (profileId !== profile.runtimeProfileId) {
throw new TypeError(
`Browser RPC provider profile registry is invalid: ${profileId}`,
);
}
validateProviderProfile(profile);
}
for (const [schemaId, schema] of Object.entries(bindings.schemaCodecs)) {
if (schemaId !== schema.schemaId || typeof schema.parse !== "function") {
throw new TypeError(
`Browser RPC schema registry is invalid: ${schemaId}`,
);
}
}
for (const [mapperId, mapper] of Object.entries(bindings.mappers)) {
if (
mapperId !== mapper.mapperId ||
typeof mapper.map !== "function" ||
!Number.isSafeInteger(mapper.mapperVersion) ||
mapper.mapperVersion < 1
) {
throw new TypeError(
`Browser RPC mapper registry is invalid: ${mapperId}`,
);
}
}
for (const [encoderId, encoder] of Object.entries(
bindings.requestEncoders,
)) {
if (encoderId !== encoder.encoderId) {
throw new TypeError(
`Browser RPC request encoder registry is invalid: ${encoderId}`,
);
}
defineBrowserRpcRequestEncoder(encoder);
}
for (const [profileId, runtime] of Object.entries(
bindings.runtimeBindings ?? {},
)) {
if (
profileId !== runtime.runtimeProfileId ||
!REGISTRY_ID.test(runtime.runtimeProfileId) ||
!REGISTRY_ID.test(runtime.providerId) ||
!PROTOCOLS.has(runtime.protocol) ||
!RPC_KINDS.has(runtime.rpcKind)
) {
throw new TypeError(
`Browser RPC runtime registry is invalid: ${profileId}`,
);
}
}
for (const [operationId, operation] of Object.entries(
bindings.operations,
)) {
if (operationId !== operation.operationId) {
throw new TypeError(
`Browser RPC operation registry is invalid: ${operationId}`,
);
}
validateOperation(operation);
const profile = bindings.profiles[operation.runtimeProfileId];
const requestSchema = bindings.schemaCodecs[operation.requestSchemaId];
const responseSchema = bindings.schemaCodecs[operation.responseSchemaId];
const mapper = bindings.mappers[operation.mapperId];
const encoder = bindings.requestEncoders[operation.requestEncoderId];
const runtime = bindings.runtimeBindings?.[operation.runtimeProfileId];
const procedure = `${operation.fullyQualifiedService}/${operation.method}`;
if (
!profile ||
profile.providerId !== operation.providerId ||
profile.protocol !== operation.protocol ||
profile.rpcKind !== operation.rpcKind ||
profile.descriptorArtifactId !== operation.descriptorArtifactId ||
profile.descriptorDigest !== operation.descriptorDigest ||
profile.authProfileId !== operation.authProfileId ||
profile.csrfProfileId !== operation.csrfProfileId ||
profile.errorProfileId !== operation.errorProfileId ||
profile.deadlineProfileId !== operation.deadlineProfileId ||
profile.retryProfileId !== operation.retryProfileId ||
!profile.allowedProcedures.includes(procedure)
) {
throw new TypeError(
`Browser RPC provider binding is invalid: ${operation.operationId}`,
);
}
if (
requestSchema?.schemaId !== operation.requestSchemaId ||
responseSchema?.schemaId !== operation.responseSchemaId ||
mapper?.mapperId !== operation.mapperId ||
mapper.inputSchemaId !== operation.responseSchemaId ||
mapper.maxOutputItems < 1 ||
encoder?.encoderId !== operation.requestEncoderId ||
encoder.operationId !== operation.operationId
) {
throw new TypeError(
`Browser RPC schema/mapper binding is invalid: ${operation.operationId}`,
);
}
if (
runtime &&
(runtime.runtimeProfileId !== operation.runtimeProfileId ||
runtime.providerId !== operation.providerId ||
runtime.protocol !== operation.protocol ||
runtime.rpcKind !== operation.rpcKind)
) {
throw new TypeError(
`Browser RPC runtime binding is invalid: ${operation.operationId}`,
);
}
if (
profile.requestMethod === "GET" &&
(operation.protocol !== "CONNECT_HTTP" ||
operation.rpcKind !== "UNARY" ||
operation.semantics !== "QUERY" ||
operation.replayPolicy !== "SAFE" ||
operation.idempotencyLevel !== "NO_SIDE_EFFECTS" ||
operation.dataClassification !== "PUBLIC" ||
operation.authProfileId !== "ANONYMOUS" ||
operation.csrfProfileId !== "NONE")
) {
throw new TypeError(
`Browser RPC GET binding is invalid: ${operation.operationId}`,
);
}
if (
profile.retryOwner === "FRONTEND_ADAPTER" &&
!isFrontendReplayAllowed(operation)
) {
throw new TypeError(
`Browser RPC retry binding is invalid: ${operation.operationId}`,
);
}
}
return true;
}
function validateOperation(operation: BrowserRpcOperationV3): void {
if (
operation.contractVersion !== BROWSER_RPC_CONTRACT_VERSION ||
!REGISTRY_ID.test(operation.operationId) ||
!OWNER.test(operation.owner) ||
!PROTOCOLS.has(operation.protocol) ||
!SEMANTICS.has(operation.semantics) ||
!REPLAY_POLICIES.has(operation.replayPolicy) ||
!IDEMPOTENCY_KEY_POLICIES.has(operation.idempotencyKeyPolicy) ||
!IDEMPOTENCY_LEVELS.has(operation.idempotencyLevel) ||
!DATA_CLASSIFICATIONS.has(operation.dataClassification) ||
!REGISTRY_ID.test(operation.runtimeProfileId) ||
!REGISTRY_ID.test(operation.providerId) ||
!SERVICE.test(operation.fullyQualifiedService) ||
!METHOD.test(operation.method) ||
!RPC_KINDS.has(operation.rpcKind) ||
!ARTIFACT_ID.test(operation.requestMessageId) ||
!ARTIFACT_ID.test(operation.responseMessageId) ||
!ARTIFACT_ID.test(operation.descriptorArtifactId) ||
!SHA256.test(operation.descriptorDigest) ||
!ARTIFACT_ID.test(operation.requestSchemaId) ||
!ARTIFACT_ID.test(operation.responseSchemaId) ||
!ARTIFACT_ID.test(operation.requestEncoderId) ||
!ARTIFACT_ID.test(operation.mapperId) ||
!REGISTRY_ID.test(operation.authProfileId) ||
!REGISTRY_ID.test(operation.csrfProfileId) ||
!REGISTRY_ID.test(operation.errorProfileId) ||
!REGISTRY_ID.test(operation.deadlineProfileId) ||
!REGISTRY_ID.test(operation.retryProfileId) ||
(operation.serverStateProfileId !== null &&
!ARTIFACT_ID.test(operation.serverStateProfileId)) ||
!positiveIntegerWithin(
operation.maxRequestMessageBytes,
BROWSER_RPC_HARD_LIMITS.maxRequestMessageBytes,
) ||
!positiveIntegerWithin(
operation.maxResponseMessageBytes,
BROWSER_RPC_HARD_LIMITS.maxResponseMessageBytes,
) ||
!positiveIntegerWithin(
operation.maxResponseMessages,
BROWSER_RPC_HARD_LIMITS.maxResponseMessages,
) ||
!positiveIntegerWithin(
operation.maxTotalResponseBytes,
BROWSER_RPC_HARD_LIMITS.maxTotalResponseBytes,
) ||
!positiveIntegerWithin(
operation.maxBufferedBytes,
BROWSER_RPC_HARD_LIMITS.maxBufferedBytes,
) ||
!positiveIntegerWithin(
operation.totalDeadlineMs,
BROWSER_RPC_HARD_LIMITS.maxDeadlineMs,
) ||
operation.maxTotalResponseBytes < operation.maxResponseMessageBytes ||
operation.maxBufferedBytes < operation.maxResponseMessageBytes
) {
throw new TypeError(
`Invalid Browser RPC operation: ${operation.operationId}`,
);
}
const unary = operation.rpcKind === "UNARY";
if (
(unary &&
(operation.semantics === "SERVER_STREAM" ||
operation.maxResponseMessages !== 1 ||
operation.idleDeadlineMs !== null)) ||
(!unary &&
(operation.semantics !== "SERVER_STREAM" ||
!positiveIntegerWithin(
operation.idleDeadlineMs,
operation.totalDeadlineMs,
))) ||
(operation.semantics === "QUERY" &&
!["SAFE", "IDEMPOTENT"].includes(operation.replayPolicy)) ||
(operation.semantics === "COMMAND" &&
["SAFE", "IDEMPOTENT"].includes(operation.replayPolicy)) ||
(operation.replayPolicy === "KEYED_COMMAND" &&
operation.idempotencyKeyPolicy !== "REQUIRED") ||
(operation.replayPolicy !== "KEYED_COMMAND" &&
operation.idempotencyKeyPolicy !== "NONE") ||
(operation.idempotencyLevel === "NO_SIDE_EFFECTS" &&
operation.semantics !== "QUERY")
) {
throw new TypeError(
`Incoherent Browser RPC operation: ${operation.operationId}`,
);
}
}
function validateProviderProfile(profile: BrowserRpcProviderProfile): void {
let endpoint: URL;
try {
endpoint = new URL(profile.fixedBaseUrl);
} catch {
throw new TypeError("Browser RPC provider profile is invalid.");
}
const localHttp =
endpoint.protocol === "http:" &&
["localhost", "127.0.0.1", "[::1]"].includes(endpoint.hostname);
if (
!REGISTRY_ID.test(profile.runtimeProfileId) ||
!REGISTRY_ID.test(profile.providerId) ||
!ARTIFACT_ID.test(profile.runtimeId) ||
!VERSION.test(profile.runtimeVersion) ||
!SHA256.test(profile.runtimeDigest) ||
!PROTOCOLS.has(profile.protocol) ||
!RUNTIME_KINDS.has(profile.runtimeKind) ||
!CLIENT_API_KINDS.has(profile.clientApiKind) ||
!RPC_KINDS.has(profile.rpcKind) ||
!MESSAGE_ENCODINGS.has(profile.messageEncoding) ||
!FRAMINGS.has(profile.framing) ||
!REQUEST_METHODS.has(profile.requestMethod) ||
!ARTIFACT_ID.test(profile.descriptorArtifactId) ||
!SHA256.test(profile.descriptorDigest) ||
!REGISTRY_ID.test(profile.authProfileId) ||
!REGISTRY_ID.test(profile.csrfProfileId) ||
!REGISTRY_ID.test(profile.corsProfileId) ||
!REGISTRY_ID.test(profile.errorProfileId) ||
!REGISTRY_ID.test(profile.deadlineProfileId) ||
!REGISTRY_ID.test(profile.retryProfileId) ||
!RETRY_OWNERS.has(profile.retryOwner) ||
!DEADLINE_DIALECTS.has(profile.deadlineDialect) ||
!CANCEL_DIALECTS.has(profile.cancelDialect) ||
!RAW_BYTE_CEILING_OWNERS.has(profile.rawByteCeilingOwner) ||
profile.streamMessageCompression !== "IDENTITY_ONLY" ||
(endpoint.protocol !== "https:" && !localHttp) ||
endpoint.username ||
endpoint.password ||
endpoint.search ||
endpoint.hash ||
profile.allowedProcedures.length === 0 ||
profile.allowedProcedures.length > BROWSER_RPC_HARD_LIMITS.maxOperations ||
new Set(profile.allowedProcedures).size !==
profile.allowedProcedures.length ||
profile.allowedProcedures.some((procedure) => {
const separator = procedure.lastIndexOf("/");
return (
separator < 1 ||
!SERVICE.test(procedure.slice(0, separator)) ||
!METHOD.test(procedure.slice(separator + 1))
);
}) ||
!positiveIntegerWithin(
profile.maxAttempts,
BROWSER_RPC_HARD_LIMITS.maxAttempts,
) ||
!nonNegativeIntegerWithin(
profile.maxRetryAfterMs,
BROWSER_RPC_HARD_LIMITS.maxRetryAfterMs,
) ||
new Set(profile.retryableFailures).size !==
profile.retryableFailures.length ||
profile.retryableFailures.some(
(failure) => !RETRYABLE_FAILURES.has(failure),
) ||
profile.backoffMs.some(
(delay) =>
!nonNegativeIntegerWithin(
delay,
BROWSER_RPC_HARD_LIMITS.maxBackoffMs,
),
) ||
(profile.retryOwner === "FRONTEND_ADAPTER"
? profile.maxAttempts < 2 ||
profile.backoffMs.length !== profile.maxAttempts - 1 ||
profile.retryableFailures.length === 0
: profile.maxAttempts !== 1 ||
profile.backoffMs.length !== 0 ||
profile.retryableFailures.length !== 0) ||
(profile.rpcKind === "SERVER_STREAM" &&
profile.retryOwner !== "NONE") ||
!runtimeTupleIsValid(profile)
) {
throw new TypeError("Browser RPC provider profile is invalid.");
}
}
function runtimeTupleIsValid(profile: BrowserRpcProviderProfile): boolean {
if (profile.protocol === "CONNECT_HTTP") {
if (
profile.runtimeKind !== "CONNECT_WEB_FETCH" ||
profile.deadlineDialect !== "CONNECT_TIMEOUT_MS" ||
profile.cancelDialect !== "ABORT_SIGNAL"
) {
return false;
}
if (profile.rpcKind === "UNARY") {
return (
profile.clientApiKind === "PROMISE_UNARY" &&
profile.framing === "CONNECT_BARE"
);
}
return (
profile.requestMethod === "POST" &&
profile.clientApiKind === "ASYNC_ITERABLE" &&
profile.framing === "CONNECT_ENVELOPE"
);
}
if (profile.requestMethod !== "POST") return false;
if (profile.runtimeKind === "OFFICIAL_GRPC_WEB_XHR") {
if (
profile.messageEncoding !== "PROTO" ||
profile.deadlineDialect !== "OFFICIAL_DEADLINE_METADATA"
) {
return false;
}
if (profile.rpcKind === "UNARY") {
return (
["PROMISE_UNARY", "CALLBACK_STREAM"].includes(
profile.clientApiKind,
) &&
["GRPC_WEB_BINARY_ENVELOPE", "GRPC_WEB_BASE64_TEXT"].includes(
profile.framing,
) &&
(profile.clientApiKind === "CALLBACK_STREAM"
? profile.cancelDialect === "CLIENT_READABLE_STREAM_CANCEL"
: profile.cancelDialect === "ABORT_SIGNAL")
);
}
return (
profile.clientApiKind === "CALLBACK_STREAM" &&
profile.framing === "GRPC_WEB_BASE64_TEXT" &&
profile.cancelDialect === "CLIENT_READABLE_STREAM_CANCEL"
);
}
if (profile.runtimeKind === "CONNECT_WEB_FETCH") {
return (
profile.deadlineDialect === "GRPC_TIMEOUT" &&
profile.cancelDialect === "ABORT_SIGNAL" &&
profile.framing === "GRPC_WEB_BINARY_ENVELOPE" &&
(profile.rpcKind === "UNARY"
? profile.clientApiKind === "PROMISE_UNARY"
: profile.clientApiKind === "ASYNC_ITERABLE")
);
}
return (
profile.runtimeKind === "CUSTOM_FETCH_FRAMED" &&
profile.deadlineDialect === "GRPC_TIMEOUT" &&
profile.cancelDialect === "ABORT_SIGNAL" &&
(profile.rpcKind === "UNARY"
? profile.clientApiKind === "PROMISE_UNARY"
: profile.clientApiKind === "ASYNC_ITERABLE")
);
}
function isFrontendReplayAllowed(
operation: BrowserRpcOperationV3,
): boolean {
return (
["SAFE", "IDEMPOTENT"].includes(operation.replayPolicy) ||
(operation.replayPolicy === "KEYED_COMMAND" &&
operation.idempotencyKeyPolicy === "REQUIRED")
);
}
function composeRegistry<Value>(
contributions: readonly Readonly<Record<string, Value>>[],
identity: (value: Value) => string,
define: (value: Value) => Value,
label: string,
maximumRows: number,
): Readonly<Record<string, Value>> {
const result: Record<string, Value> = Object.create(null);
let rows = 0;
for (const contribution of contributions) {
for (const [registryId, value] of Object.entries(contribution)) {
rows += 1;
if (
rows > maximumRows ||
registryId !== identity(value) ||
Object.hasOwn(result, registryId)
) {
throw new TypeError(
`Invalid or duplicate Browser RPC ${label}: ${registryId}`,
);
}
result[registryId] = define(value);
}
}
return Object.freeze(result);
}
function positiveIntegerWithin(
value: number | null,
maximum: number,
): value is number {
return Number.isSafeInteger(value) && value !== null && value > 0 && value <= maximum;
}
function nonNegativeIntegerWithin(
value: number,
maximum: number,
): boolean {
return Number.isSafeInteger(value) && value >= 0 && value <= maximum;
}
+241
View File
@@ -0,0 +1,241 @@
/**
* Cross-context cache invalidation is a best-effort hint protocol. The wire
* event intentionally carries neither cached data nor a concrete query key.
* Receivers resolve the allowlisted topic through their local policy.
*/
export const CACHE_INVALIDATION_PROTOCOL_VERSION = 1 as const;
export const CACHE_INVALIDATION_WIRE_LIMITS = Object.freeze({
maxWireBytes: 2_048,
maxOpaqueIdentifierLength: 128,
maxTopicLength: 64,
maxEventTtlMs: 5 * 60 * 1_000,
maxFutureClockSkewMs: 30_000,
});
export type CacheInvalidationTopicDefinition = Readonly<{
topic: string;
topicVersion: number;
}>;
export type CacheInvalidationWireEvent = Readonly<{
protocolVersion: typeof CACHE_INVALIDATION_PROTOCOL_VERSION;
eventId: string;
sourceId: string;
sourceEpoch: string;
sequence: number;
cacheEpoch: string;
topic: string;
topicVersion: number;
emittedAt: number;
expiresAt: number;
}>;
export type CacheInvalidationParseFailureReason =
| "CACHE_EPOCH_MISMATCH"
| "EXPIRED"
| "INVALID_ENVELOPE"
| "MALFORMED_JSON"
| "OVERSIZED"
| "PROTOCOL_MISMATCH"
| "TOPIC_REJECTED";
export type CacheInvalidationParseResult =
| Readonly<{ ok: true; value: CacheInvalidationWireEvent }>
| Readonly<{
ok: false;
reason: CacheInvalidationParseFailureReason;
}>;
export type CacheInvalidationParsePolicy = Readonly<{
cacheEpoch: string;
topicVersions: Readonly<Record<string, number>>;
nowEpochMilliseconds: number;
}>;
const WIRE_KEYS = Object.freeze([
"cacheEpoch",
"emittedAt",
"eventId",
"expiresAt",
"protocolVersion",
"sequence",
"sourceEpoch",
"sourceId",
"topic",
"topicVersion",
] as const);
const OPAQUE_IDENTIFIER = /^[A-Za-z0-9][A-Za-z0-9._:-]*$/u;
const TOPIC = /^[a-z][a-z0-9.-]*$/u;
export function isCacheInvalidationOpaqueIdentifier(
value: unknown,
): value is string {
return (
typeof value === "string" &&
value.length >= 1 &&
value.length <=
CACHE_INVALIDATION_WIRE_LIMITS.maxOpaqueIdentifierLength &&
OPAQUE_IDENTIFIER.test(value)
);
}
export function isCacheInvalidationTopic(
value: unknown,
): value is string {
return (
typeof value === "string" &&
value.length >= 1 &&
value.length <= CACHE_INVALIDATION_WIRE_LIMITS.maxTopicLength &&
TOPIC.test(value)
);
}
export function cacheInvalidationWireByteLength(value: string): number {
return new TextEncoder().encode(value).byteLength;
}
export function decodeCacheInvalidationWireEvent(
raw: string,
policy: CacheInvalidationParsePolicy,
): CacheInvalidationParseResult {
if (
typeof raw !== "string" ||
raw.length > CACHE_INVALIDATION_WIRE_LIMITS.maxWireBytes ||
cacheInvalidationWireByteLength(raw) >
CACHE_INVALIDATION_WIRE_LIMITS.maxWireBytes
) {
return failure("OVERSIZED");
}
let parsed: unknown;
try {
parsed = JSON.parse(raw);
} catch {
return failure("MALFORMED_JSON");
}
return parseCacheInvalidationWireEvent(parsed, policy);
}
export function parseCacheInvalidationWireEvent(
input: unknown,
policy: CacheInvalidationParsePolicy,
): CacheInvalidationParseResult {
try {
return parseCacheInvalidationWireEventUnsafe(input, policy);
} catch {
return failure("INVALID_ENVELOPE");
}
}
function parseCacheInvalidationWireEventUnsafe(
input: unknown,
policy: CacheInvalidationParsePolicy,
): CacheInvalidationParseResult {
if (!isExactWireRecord(input)) {
return failure("INVALID_ENVELOPE");
}
let serialized: string;
try {
serialized = JSON.stringify(input);
} catch {
return failure("INVALID_ENVELOPE");
}
if (
cacheInvalidationWireByteLength(serialized) >
CACHE_INVALIDATION_WIRE_LIMITS.maxWireBytes
) {
return failure("OVERSIZED");
}
if (input.protocolVersion !== CACHE_INVALIDATION_PROTOCOL_VERSION) {
return failure("PROTOCOL_MISMATCH");
}
if (
!isCacheInvalidationOpaqueIdentifier(input.eventId) ||
!isCacheInvalidationOpaqueIdentifier(input.sourceId) ||
!isCacheInvalidationOpaqueIdentifier(input.sourceEpoch) ||
!isCacheInvalidationOpaqueIdentifier(input.cacheEpoch) ||
!isCacheInvalidationTopic(input.topic) ||
!isPositiveSafeInteger(input.sequence) ||
!isPositiveSafeInteger(input.topicVersion) ||
!isEpochMilliseconds(input.emittedAt) ||
!isEpochMilliseconds(input.expiresAt) ||
input.expiresAt <= input.emittedAt ||
input.expiresAt - input.emittedAt >
CACHE_INVALIDATION_WIRE_LIMITS.maxEventTtlMs ||
!isEpochMilliseconds(policy.nowEpochMilliseconds)
) {
return failure("INVALID_ENVELOPE");
}
if (input.cacheEpoch !== policy.cacheEpoch) {
return failure("CACHE_EPOCH_MISMATCH");
}
if (
!Object.hasOwn(policy.topicVersions, input.topic) ||
policy.topicVersions[input.topic] !== input.topicVersion
) {
return failure("TOPIC_REJECTED");
}
if (
input.expiresAt <= policy.nowEpochMilliseconds ||
input.emittedAt >
policy.nowEpochMilliseconds +
CACHE_INVALIDATION_WIRE_LIMITS.maxFutureClockSkewMs
) {
return failure("EXPIRED");
}
return {
ok: true,
value: Object.freeze({
protocolVersion: CACHE_INVALIDATION_PROTOCOL_VERSION,
eventId: input.eventId,
sourceId: input.sourceId,
sourceEpoch: input.sourceEpoch,
sequence: input.sequence,
cacheEpoch: input.cacheEpoch,
topic: input.topic,
topicVersion: input.topicVersion,
emittedAt: input.emittedAt,
expiresAt: input.expiresAt,
}),
};
}
function isExactWireRecord(
value: unknown,
): value is Readonly<Record<(typeof WIRE_KEYS)[number], unknown>> {
if (!value || typeof value !== "object" || Array.isArray(value)) {
return false;
}
const keys = Object.keys(value).sort();
return (
keys.length === WIRE_KEYS.length &&
keys.every((key, index) => key === WIRE_KEYS[index])
);
}
function isPositiveSafeInteger(value: unknown): value is number {
return (
typeof value === "number" &&
Number.isSafeInteger(value) &&
value >= 1
);
}
function isEpochMilliseconds(value: unknown): value is number {
return (
typeof value === "number" &&
Number.isSafeInteger(value) &&
value >= 0
);
}
function failure(
reason: CacheInvalidationParseFailureReason,
): Extract<CacheInvalidationParseResult, { ok: false }> {
return Object.freeze({ ok: false, reason });
}
+111
View File
@@ -0,0 +1,111 @@
/**
* §5.3. The single canonical byte producer for a contract set.
*
* Node build scripts and the browser runtime share this function. Only the hash
* adapter differs (Node `crypto` vs Web Crypto), so a digest can never diverge
* because of JSON property order or a locale-sensitive sort.
*/
export type ContractSetPackage = Readonly<{
packageId: string;
version: string;
digest: `sha256:${string}`;
runtimeProtocolVersion: 1;
sourceRevision: string;
}>;
export const CONTRACT_SET_ALGORITHM = "CA_CONTRACT_SET_V1" as const;
const HEADER = "CA_FRONTEND_CONTRACT_SET_V1\u0000";
const encoder = new TextEncoder();
function compareUtf8(left: string, right: string): number {
const a = encoder.encode(left);
const b = encoder.encode(right);
const shared = Math.min(a.length, b.length);
for (let index = 0; index < shared; index += 1) {
const difference = (a[index] as number) - (b[index] as number);
if (difference !== 0) return difference;
}
return a.length - b.length;
}
export function canonicalizeContractSet(
packages: readonly ContractSetPackage[],
): Uint8Array {
const seen = new Set<string>();
for (const entry of packages) {
if (seen.has(entry.packageId)) {
throw new TypeError("Duplicate contract set package identity.");
}
seen.add(entry.packageId);
}
const sorted = [...packages].sort((left, right) =>
compareUtf8(left.packageId, right.packageId),
);
const chunks: Uint8Array[] = [encoder.encode(HEADER)];
for (const entry of sorted) {
appendString(chunks, entry.packageId);
appendString(chunks, entry.version);
appendString(chunks, entry.digest);
chunks.push(u32be(entry.runtimeProtocolVersion));
appendString(chunks, entry.sourceRevision);
}
const total = chunks.reduce((sum, chunk) => sum + chunk.length, 0);
const output = new Uint8Array(total);
let offset = 0;
for (const chunk of chunks) {
output.set(chunk, offset);
offset += chunk.length;
}
return output;
}
function appendString(chunks: Uint8Array[], value: string): void {
const bytes = encoder.encode(value);
chunks.push(u32be(bytes.length));
chunks.push(bytes);
}
function u32be(value: number): Uint8Array {
if (!Number.isInteger(value) || value < 0 || value > 0xffff_ffff) {
throw new TypeError("Contract set length prefix is out of range.");
}
const bytes = new Uint8Array(4);
new DataView(bytes.buffer).setUint32(0, value, false);
return bytes;
}
export function toLowerHex(digest: ArrayBuffer | Uint8Array): string {
const bytes =
digest instanceof Uint8Array ? digest : new Uint8Array(digest);
let output = "";
for (const byte of bytes) output += byte.toString(16).padStart(2, "0");
return output;
}
/**
* Browser-side digest. Node callers pass their own `crypto.createHash` adapter
* through {@link computeContractSetDigestWith}.
*/
export async function computeContractSetDigest(
packages: readonly ContractSetPackage[],
): Promise<`sha256:${string}`> {
const bytes = canonicalizeContractSet(packages);
const buffer = await crypto.subtle.digest(
"SHA-256",
bytes.slice().buffer as ArrayBuffer,
);
return `sha256:${toLowerHex(buffer)}`;
}
export function computeContractSetDigestWith(
packages: readonly ContractSetPackage[],
sha256: (bytes: Uint8Array) => Uint8Array,
): `sha256:${string}` {
return `sha256:${toLowerHex(sha256(canonicalizeContractSet(packages)))}`;
}
+129
View File
@@ -0,0 +1,129 @@
import { z } from "zod";
import {
CONTRACT_SET_ALGORITHM,
computeContractSetDigest,
type ContractSetPackage,
} from "./contract-set-canonical.ts";
/**
* §5. Contract set and release coherence.
*
* The frontend verifies that the packages compiled into this build match the
* packages the release manifest declares. It never negotiates ranges, resolves
* `latest`, or infers a provider runtime version.
*/
export type ContractSetFailureCode =
| "CONTRACT_SET_SCHEMA_INVALID"
| "CONTRACT_SET_ENTRY_INVALID"
| "CONTRACT_SET_DUPLICATE_PACKAGE"
| "CONTRACT_SET_DIGEST_INVALID"
| "CONTRACT_SET_DIGEST_MISMATCH"
| "CONTRACT_SET_PACKAGE_MISSING"
| "CONTRACT_SET_PACKAGE_UNEXPECTED"
| "CONTRACT_SET_VERSION_MISMATCH"
| "CONTRACT_RUNTIME_PROTOCOL_UNSUPPORTED";
const digestSchema = z.string().regex(/^sha256:[0-9a-f]{64}$/);
export const contractSetPackageSchema = z
.object({
packageId: z
.string()
.regex(/^@[a-z0-9][a-z0-9._-]{0,62}\/[a-z0-9][a-z0-9._-]{0,62}$/),
version: z
.string()
.regex(
/^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-[0-9A-Za-z.-]+)?(?:\+[0-9A-Za-z.-]+)?$/,
),
digest: digestSchema,
runtimeProtocolVersion: z.literal(1),
sourceRevision: z.string().regex(/^[0-9a-f]{7,64}$/),
})
.strict();
export const contractSetSchema = z
.object({
setAlgorithm: z.literal(CONTRACT_SET_ALGORITHM),
setDigest: digestSchema,
packages: z.array(contractSetPackageSchema).max(256),
})
.strict();
export type ContractSet = z.output<typeof contractSetSchema>;
export type ContractSetVerification =
| Readonly<{ ok: true }>
| Readonly<{ ok: false; code: ContractSetFailureCode }>;
/**
* §5.5. Every comparison below must hold. There is no precedence rule between
* the embedded expectation and the manifest: a disagreement fails the boot.
*/
export async function verifyContractSet(
input: Readonly<{
expected: readonly ContractSetPackage[];
manifest: ContractSet;
expectedSetDigest?: `sha256:${string}`;
}>,
): Promise<ContractSetVerification> {
const manifestIds = new Set<string>();
for (const entry of input.manifest.packages) {
if (manifestIds.has(entry.packageId)) {
return failure("CONTRACT_SET_DUPLICATE_PACKAGE");
}
manifestIds.add(entry.packageId);
if (entry.runtimeProtocolVersion !== 1) {
return failure("CONTRACT_RUNTIME_PROTOCOL_UNSUPPORTED");
}
}
const expectedById = new Map(
input.expected.map((entry) => [entry.packageId, entry] as const),
);
for (const entry of input.manifest.packages) {
if (!expectedById.has(entry.packageId)) {
return failure("CONTRACT_SET_PACKAGE_UNEXPECTED");
}
}
for (const entry of input.expected) {
const found = input.manifest.packages.find(
(candidate) => candidate.packageId === entry.packageId,
);
if (!found) return failure("CONTRACT_SET_PACKAGE_MISSING");
if (
found.version !== entry.version ||
found.sourceRevision !== entry.sourceRevision
) {
return failure("CONTRACT_SET_VERSION_MISMATCH");
}
if (found.digest !== entry.digest) {
return failure("CONTRACT_SET_DIGEST_MISMATCH");
}
}
let recomputed: `sha256:${string}`;
try {
recomputed = await computeContractSetDigest(
input.manifest.packages as readonly ContractSetPackage[],
);
} catch {
return failure("CONTRACT_SET_DIGEST_INVALID");
}
if (recomputed !== input.manifest.setDigest) {
return failure("CONTRACT_SET_DIGEST_MISMATCH");
}
const expectedDigest =
input.expectedSetDigest ?? (await computeContractSetDigest(input.expected));
if (expectedDigest !== input.manifest.setDigest) {
return failure("CONTRACT_SET_DIGEST_MISMATCH");
}
return Object.freeze({ ok: true as const });
}
function failure(code: ContractSetFailureCode): ContractSetVerification {
return Object.freeze({ ok: false as const, code });
}
+23
View File
@@ -0,0 +1,23 @@
import type { Result } from "../application/result.ts";
export type CursorPage<Value> = Readonly<{
items: readonly Value[];
nextCursor: string | null;
hasMore: boolean;
snapshotToken: string | null;
}>;
export type CursorPaginationProfile = Readonly<{
profileId: string;
maxPages: number;
maxTotalItems: number;
maxEstimatedBytes: number;
maxCursorBytes: number;
allowSparsePage: boolean;
}>;
export type CursorPaginationRuntime<Value> = Readonly<{
loadAll(context: Readonly<{ signal?: AbortSignal }>): Promise<
Result<readonly Value[]>
>;
}>;
+8
View File
@@ -0,0 +1,8 @@
export type QueueSizeBucket = "0" | "1-10" | "11-50" | "51+";
export function queueSizeBucket(size: number): QueueSizeBucket {
if (size <= 0) return "0";
if (size <= 10) return "1-10";
if (size <= 50) return "11-50";
return "51+";
}
+177
View File
@@ -0,0 +1,177 @@
export const DIAGNOSTIC_LEVELS = Object.freeze([
"debug",
"info",
"warn",
"error",
] as const);
export { queueSizeBucket } from "./diagnostic-buckets.ts";
export type DiagnosticLevel = (typeof DIAGNOSTIC_LEVELS)[number];
export const DIAGNOSTIC_EVENT_REGISTRY = Object.freeze({
"app.boot.failed": Object.freeze({ level: "error" }),
"http.request.completed": Object.freeze({ level: "info" }),
"cache.operation.failed": Object.freeze({ level: "warn" }),
"storage.operation.failed": Object.freeze({ level: "warn" }),
"route.changed": Object.freeze({ level: "info" }),
"ui.render.failed": Object.freeze({ level: "error" }),
"release.mismatch.detected": Object.freeze({ level: "warn" }),
"telemetry.delivery.dropped": Object.freeze({ level: "warn" }),
});
export type DiagnosticEventId = keyof typeof DIAGNOSTIC_EVENT_REGISTRY;
export const DIAGNOSTIC_CONTEXT_ALLOWLIST = Object.freeze([
"app_version",
"build_id",
"release_id",
"active_release_id",
"config_schema_version",
"api_contract_version",
"route_id",
"operation_id",
"correlation_id",
"error_kind",
"outcome",
"http_status_group",
"attempt_count_bucket",
"duration_bucket",
"component_boundary",
"mismatch_kind",
"operation",
"reason",
"queue_size_bucket",
] as const);
export type DiagnosticContextKey =
(typeof DIAGNOSTIC_CONTEXT_ALLOWLIST)[number];
export type DiagnosticContext = Readonly<
Partial<Record<DiagnosticContextKey, string | number | boolean>>
>;
export type DiagnosticRecordInput = Readonly<{
level: DiagnosticLevel;
eventId: DiagnosticEventId;
context?: Readonly<Record<string, unknown>>;
}>;
export type DiagnosticRecord = Readonly<{
level: DiagnosticLevel;
eventId: DiagnosticEventId;
timestamp: string;
context: DiagnosticContext;
}>;
const SAFE_VALUE = /^[A-Za-z0-9][A-Za-z0-9._:@/-]{0,63}$/;
function projectDiagnosticRecordUnsafe(
input: DiagnosticRecordInput,
now: () => number = Date.now,
):
| Readonly<{ success: true; record: DiagnosticRecord }>
| Readonly<{ success: false; reason: string }> {
if (!Object.hasOwn(DIAGNOSTIC_EVENT_REGISTRY, input.eventId)) {
return { success: false, reason: "unregistered-event" };
}
if (!DIAGNOSTIC_LEVELS.includes(input.level)) {
return { success: false, reason: "invalid-level" };
}
const contextEntries = Object.entries(input.context ?? {});
if (contextEntries.length > DIAGNOSTIC_CONTEXT_ALLOWLIST.length) {
return { success: false, reason: "invalid-context" };
}
const projected: Partial<
Record<DiagnosticContextKey, string | number | boolean>
> = {};
for (const [key, value] of contextEntries) {
if (
!DIAGNOSTIC_CONTEXT_ALLOWLIST.includes(key as DiagnosticContextKey)
) {
return { success: false, reason: "unknown-context" };
}
if (typeof value === "string") {
if (!SAFE_VALUE.test(value)) {
return { success: false, reason: "invalid-context" };
}
projected[key as DiagnosticContextKey] = value;
} else if (typeof value === "number" && Number.isFinite(value)) {
projected[key as DiagnosticContextKey] = value;
} else if (typeof value === "boolean") {
projected[key as DiagnosticContextKey] = value;
} else {
return { success: false, reason: "invalid-context" };
}
}
let timestamp: string;
try {
timestamp = new Date(now()).toISOString();
} catch {
timestamp = new Date(0).toISOString();
}
return {
success: true,
record: Object.freeze({
level: input.level,
eventId: input.eventId,
timestamp,
context: Object.freeze(projected),
}),
};
}
export function projectDiagnosticRecord(
input: DiagnosticRecordInput,
now: () => number = Date.now,
): ReturnType<typeof projectDiagnosticRecordUnsafe> {
try {
return projectDiagnosticRecordUnsafe(input, now);
} catch {
return { success: false, reason: "serialization-failure" };
}
}
export function safeErrorKind(error: unknown): string {
try {
if (error && typeof error === "object") {
const record = error as Readonly<Record<string, unknown>>;
if (
typeof record.kind === "string" &&
/^[A-Z][A-Z0-9_]{0,63}$/.test(record.kind)
) {
return record.kind;
}
if (
typeof record.name === "string" &&
/^[A-Za-z][A-Za-z0-9]{0,63}$/.test(record.name)
) {
const normalized = record.name
.replace(/([a-z0-9])([A-Z])/g, "$1_$2")
.toUpperCase();
return /^[A-Z][A-Z0-9_]{0,63}$/.test(normalized)
? normalized
: "UNKNOWN_FAILURE";
}
}
} catch {
return "UNKNOWN_FAILURE";
}
return "UNKNOWN_FAILURE";
}
export function statusGroup(status: number | undefined): string {
return typeof status === "number" && Number.isFinite(status)
? `${Math.max(0, Math.min(9, Math.floor(status / 100)))}xx`
: "none";
}
export function attemptBucket(attemptCount: number): string {
if (attemptCount <= 1) return "1";
if (attemptCount === 2) return "2";
if (attemptCount <= 4) return "3-4";
return "5+";
}
export function durationBucket(durationMs: number): string {
if (!Number.isFinite(durationMs) || durationMs < 0) return "unknown";
if (durationMs < 100) return "lt100ms";
if (durationMs < 500) return "100-499ms";
if (durationMs < 2_000) return "500-1999ms";
return "gte2000ms";
}
+109
View File
@@ -0,0 +1,109 @@
/**
* §6.3. Case-insensitive key fragments that can never appear in a client
* configuration document.
*/
const FORBIDDEN_CONFIG_NAME_FRAGMENTS = Object.freeze([
"PASSWORD",
"SECRET",
"TOKEN",
"PRIVATE_KEY",
"CLIENT_SECRET",
"ACCESS_KEY",
"REFRESH_TOKEN",
"COOKIE",
"AUTHORIZATION",
]);
/**
* Exact top-level keys whose fragment match is a semantic enum name, not a
* credential. The allowlist is exact-key only; it is never applied to arbitrary
* nested keys.
*/
const SEMANTIC_KEY_ALLOWLIST = Object.freeze(
new Set(["AUTH_MODE", "TELEMETRY_ENABLED"]),
);
export type EnvironmentPhase = "build" | "runtime";
export type EnvironmentDefinition = Readonly<{
phase: EnvironmentPhase;
classification: string;
required: boolean;
defaultValue: unknown;
}>;
export const ENV_REGISTRY = Object.freeze({
VITE_BUILD_ID: build("public-metadata", true, null),
VITE_COMMIT_SHA: build("public-metadata", false, "local"),
VITE_ROUTER_BASE_PATH: build("compile-time", true, "/"),
VITE_RUNTIME_CONFIG_URL: build("compile-time", true, "/config.json"),
APP_ENV: runtime("public", true, null),
API_BASE_URL: runtime("public-sensitive", true, null),
REQUEST_TIMEOUT_MS: runtime("public", false, 10_000),
MAX_RETRY_ATTEMPTS: runtime("public", false, 2),
TELEMETRY_ENABLED: runtime("public", true, false),
TELEMETRY_ENDPOINT: runtime("public-sensitive", false, null),
AUTH_MODE: runtime("public", true, "external"),
CONFIG_SCHEMA_VERSION: runtime("public", true, null),
RELEASE_MANIFEST_URL: runtime("public", true, "/release-manifest.json"),
// §3.5: overrides may only disable an installed capability, never enable one.
CAPABILITY_OVERRIDES: runtime("public", false, null),
});
function build(
classification: string,
required: boolean,
defaultValue: unknown,
): EnvironmentDefinition {
return Object.freeze({ phase: "build", classification, required, defaultValue });
}
function runtime(
classification: string,
required: boolean,
defaultValue: unknown,
): EnvironmentDefinition {
return Object.freeze({ phase: "runtime", classification, required, defaultValue });
}
export function assertSafeConfigNames(
config: Readonly<Record<string, unknown>>,
depth = 0,
): void {
if (depth > 4) {
throw new Error("Client configuration nesting exceeds its bound");
}
for (const [name, value] of Object.entries(config)) {
const allowlisted = depth === 0 && SEMANTIC_KEY_ALLOWLIST.has(name);
if (!allowlisted && isForbiddenConfigName(name)) {
throw new Error(`Forbidden client configuration key: ${name}`);
}
if (value && typeof value === "object" && !Array.isArray(value)) {
assertSafeConfigNames(value as Record<string, unknown>, depth + 1);
}
}
}
function isForbiddenConfigName(name: string): boolean {
const upper = name.toUpperCase();
return FORBIDDEN_CONFIG_NAME_FRAGMENTS.some((fragment) =>
upper.includes(fragment),
);
}
export type BuildEnvironment = Readonly<{
VITE_BUILD_ID?: string;
VITE_COMMIT_SHA?: string;
VITE_ROUTER_BASE_PATH?: string;
VITE_RUNTIME_CONFIG_URL?: string;
}>;
export function getBuildConfig(
environment: BuildEnvironment = import.meta.env as BuildEnvironment,
) {
const buildId = environment.VITE_BUILD_ID || "local-build";
const commitSha = environment.VITE_COMMIT_SHA || "local";
const routerBasePath = environment.VITE_ROUTER_BASE_PATH || "/";
const runtimeConfigUrl = environment.VITE_RUNTIME_CONFIG_URL || "/config.json";
return Object.freeze({ buildId, commitSha, routerBasePath, runtimeConfigUrl });
}
+424
View File
@@ -0,0 +1,424 @@
const DROP_SENSITIVE = Object.freeze([
"cause",
"body",
"headers",
"authorization",
"url",
"query",
"stack",
"storageValue",
] as const);
export type ErrorAction =
| "retry"
| "reauth"
| "navigate"
| "reload-once"
| "contact-support"
| "none";
export type ErrorDefinition<Kind extends string> = Readonly<{
kind: Kind;
defaultRetryable: boolean;
severity: string;
userMessageKey: string;
action: ErrorAction;
telemetryEvent: string;
redaction: readonly string[];
}>;
const row = <Kind extends string>(
kind: Kind,
defaultRetryable: boolean,
severity: string,
action: ErrorAction,
telemetryEvent = "api.request.failed",
): ErrorDefinition<Kind> =>
Object.freeze({
kind,
defaultRetryable,
severity,
userMessageKey: `error.${kind.toLowerCase()}`,
action,
telemetryEvent,
redaction: DROP_SENSITIVE,
});
export const ERROR_REGISTRY = Object.freeze({
NETWORK_UNREACHABLE: row("NETWORK_UNREACHABLE", true, "warning", "retry"),
REQUEST_TIMEOUT: row("REQUEST_TIMEOUT", true, "warning", "retry"),
REQUEST_ABORTED: row("REQUEST_ABORTED", false, "info", "none"),
CONTENT_TYPE_MISMATCH: row(
"CONTENT_TYPE_MISMATCH",
false,
"error",
"contact-support",
),
MALFORMED_JSON: row("MALFORMED_JSON", false, "error", "contact-support"),
RESPONSE_BODY_LIMIT: row(
"RESPONSE_BODY_LIMIT",
false,
"error",
"contact-support",
),
ENVELOPE_MISMATCH: row("ENVELOPE_MISMATCH", false, "error", "contact-support"),
SCHEMA_MISMATCH: row("SCHEMA_MISMATCH", false, "error", "contact-support"),
MAPPING_CONTRACT_VIOLATION: row(
"MAPPING_CONTRACT_VIOLATION",
false,
"error",
"contact-support",
),
RESULT_LIMIT_EXCEEDED: row(
"RESULT_LIMIT_EXCEEDED",
false,
"error",
"contact-support",
),
SCOPE_GENERATION_CHANGED: row(
"SCOPE_GENERATION_CHANGED",
false,
"info",
"none",
),
IDENTITY_INTERN_LIMIT_EXCEEDED: row(
"IDENTITY_INTERN_LIMIT_EXCEEDED",
false,
"warning",
"retry",
),
DUPLICATE_IN_FLIGHT: row(
"DUPLICATE_IN_FLIGHT",
false,
"info",
"none",
),
PAGINATION_CONTRACT_VIOLATION: row(
"PAGINATION_CONTRACT_VIOLATION",
false,
"error",
"contact-support",
),
AUTH_REQUIRED: row("AUTH_REQUIRED", false, "info", "reauth"),
AUTH_INTEGRATION_FAILURE: row(
"AUTH_INTEGRATION_FAILURE",
false,
"error",
"contact-support",
),
FORBIDDEN: row("FORBIDDEN", false, "warning", "navigate"),
NOT_FOUND: row("NOT_FOUND", false, "info", "navigate"),
CONFLICT: row("CONFLICT", false, "warning", "retry"),
VALIDATION_REJECTED: row("VALIDATION_REJECTED", false, "info", "none"),
UNKNOWN_CLIENT_FAILURE: row(
"UNKNOWN_CLIENT_FAILURE",
false,
"warning",
"contact-support",
),
RATE_LIMITED: row("RATE_LIMITED", true, "warning", "retry"),
SERVER_FAILURE: row("SERVER_FAILURE", true, "error", "retry"),
CHUNK_LOAD_FAILURE: row(
"CHUNK_LOAD_FAILURE",
false,
"error",
"reload-once",
"release.mismatch.detected",
),
BOOT_CONFIG_FAILURE: row(
"BOOT_CONFIG_FAILURE",
false,
"error",
"contact-support",
"app.boot.failed",
),
RELEASE_MANIFEST_FAILURE: row(
"RELEASE_MANIFEST_FAILURE",
false,
"error",
"contact-support",
"app.boot.failed",
),
DEPLOY_MISMATCH: row(
"DEPLOY_MISMATCH",
false,
"error",
"reload-once",
"release.mismatch.detected",
),
BUILD_MISMATCH: row(
"BUILD_MISMATCH",
false,
"error",
"reload-once",
"release.mismatch.detected",
),
CONFIG_MISMATCH: row(
"CONFIG_MISMATCH",
false,
"error",
"contact-support",
"app.boot.failed",
),
API_CONTRACT_MISMATCH: row(
"API_CONTRACT_MISMATCH",
false,
"error",
"contact-support",
"app.boot.failed",
),
RELEASE_MISMATCH: row(
"RELEASE_MISMATCH",
false,
"error",
"reload-once",
"release.mismatch.detected",
),
ASSET_MISMATCH: row(
"ASSET_MISMATCH",
false,
"error",
"reload-once",
"release.mismatch.detected",
),
STORAGE_UNAVAILABLE: row(
"STORAGE_UNAVAILABLE",
false,
"warning",
"none",
"storage.operation.failed",
),
STORAGE_QUOTA_EXCEEDED: row(
"STORAGE_QUOTA_EXCEEDED",
false,
"warning",
"none",
"storage.operation.failed",
),
RENDER_FAILURE: row(
"RENDER_FAILURE",
false,
"error",
"reload-once",
"ui.render.failed",
),
TELEMETRY_FAILURE: row(
"TELEMETRY_FAILURE",
false,
"info",
"none",
"telemetry.delivery.dropped",
),
QUERY_CACHE_FAILURE: row(
"QUERY_CACHE_FAILURE",
false,
"error",
"retry",
"query.cache.failed",
),
UNKNOWN_FAILURE: row("UNKNOWN_FAILURE", false, "error", "contact-support"),
});
/**
* Every failure crossing an application input boundary must use one of the
* registry-owned kinds. Adapters may accept untrusted backend codes, but must
* map those codes to this closed vocabulary before returning.
*/
export type FailureKind = keyof typeof ERROR_REGISTRY;
export type ValidationIssue = Readonly<{ path: string; code: string }>;
export type FailureEffectCertainty =
| "NOT_APPLICABLE"
| "NOT_STARTED"
| "NOT_APPLIED"
| "APPLIED_CONFIRMED"
| "MAYBE_APPLIED";
export type AppFailure = Readonly<{
kind: FailureKind;
code: string;
httpStatus?: number;
retryable: boolean;
operationId: string;
attemptCount: number;
requestId?: string;
traceId?: string;
retryAfterMs?: number;
effect?: FailureEffectCertainty;
validationIssues?: readonly ValidationIssue[];
userMessageKey: string;
action: ErrorAction;
causeClass?: string;
}>;
/**
* Backward-compatible transport-facing name. New application and presentation
* code should prefer AppFailure.
*
*/
export type ApiFailure = AppFailure;
export type FailureDetails = Readonly<{
code?: string;
httpStatus?: number;
requestId?: string;
traceId?: string;
retryAfterMs?: number;
effect?: FailureEffectCertainty;
validationIssues?: readonly ValidationIssue[];
causeClass?: string;
}>;
export function createFailure(
kind: FailureKind,
operationId: string,
attempt: number,
details: FailureDetails = {},
): AppFailure {
const definition: ErrorDefinition<FailureKind> = ERROR_REGISTRY[kind];
return Object.freeze({
kind: definition.kind,
code: typeof details.code === "string" ? details.code : definition.kind,
retryable:
details.effect === "MAYBE_APPLIED" ||
details.effect === "APPLIED_CONFIRMED"
? false
: definition.defaultRetryable,
operationId,
attemptCount: Math.max(1, attempt + 1),
...(Number.isInteger(details.httpStatus)
? { httpStatus: details.httpStatus }
: {}),
...(typeof details.requestId === "string" ? { requestId: details.requestId } : {}),
...(typeof details.traceId === "string" ? { traceId: details.traceId } : {}),
...(typeof details.retryAfterMs === "number"
? { retryAfterMs: details.retryAfterMs }
: {}),
...(details.effect === undefined ? {} : { effect: details.effect }),
...(Array.isArray(details.validationIssues)
? {
validationIssues: Object.freeze(
details.validationIssues
.filter(
(issue) =>
issue &&
typeof issue === "object" &&
typeof issue.path === "string" &&
typeof issue.code === "string",
)
.slice(0, 50)
.map((issue) =>
Object.freeze({ path: issue.path, code: issue.code }),
),
),
}
: {}),
...(typeof details.causeClass === "string"
? { causeClass: details.causeClass }
: {}),
userMessageKey: definition.userMessageKey,
action:
details.effect === "MAYBE_APPLIED"
? "contact-support"
: details.effect === "APPLIED_CONFIRMED"
? "none"
: definition.action,
});
}
/**
* Adds controller-owned mutation effect knowledge without weakening the
* fail-safe handling required for an unknown server-side outcome.
*/
export function withFailureEffect(
failure: AppFailure,
effect: Exclude<FailureEffectCertainty, "NOT_APPLICABLE">,
): AppFailure {
return Object.freeze({
...failure,
effect,
...(effect === "MAYBE_APPLIED"
? { retryable: false as const, action: "contact-support" as const }
: effect === "APPLIED_CONFIRMED"
? { retryable: false as const, action: "none" as const }
: {}),
});
}
/**
* Projects an untrusted 422 details payload into the only validation metadata
* allowed to cross the HTTP boundary. Backend copy and additional values are
* deliberately discarded.
*
*/
export function safeValidationIssues(
value: unknown,
): readonly ValidationIssue[] {
if (!value || typeof value !== "object") return Object.freeze([]);
const candidate = value as Readonly<{
issues?: unknown;
fieldErrors?: unknown;
}>;
const issues = Array.isArray(candidate.issues)
? candidate.issues
: Array.isArray(candidate.fieldErrors)
? candidate.fieldErrors
: [];
return Object.freeze(
issues
.filter(
(issue): issue is ValidationIssue =>
issue &&
typeof issue === "object" &&
"path" in issue &&
"code" in issue &&
typeof issue.path === "string" &&
typeof issue.code === "string" &&
issue.path.length <= 120 &&
issue.code.length <= 80,
)
.slice(0, 50)
.map((issue) =>
Object.freeze({
path: issue.path,
code: issue.code,
}),
),
);
}
export function kindForStatus(status: number): FailureKind {
if (status === 401) return "AUTH_REQUIRED";
if (status === 403) return "FORBIDDEN";
if (status === 404) return "NOT_FOUND";
if (status === 409) return "CONFLICT";
if (status === 422) return "VALIDATION_REJECTED";
if (status === 429) return "RATE_LIMITED";
if (status >= 500) return "SERVER_FAILURE";
if (status >= 400) return "UNKNOWN_CLIENT_FAILURE";
return "ENVELOPE_MISMATCH";
}
/**
* Total catch-all that intentionally discards the thrown value.
*
*/
export function normalizeUnknownFailure(
value: unknown,
context: Readonly<{ operationId?: string; attempt?: number }> = {},
): AppFailure {
const causeClass =
value instanceof Error
? value.name
: value === null
? "null"
: typeof value;
return createFailure(
"UNKNOWN_FAILURE",
context.operationId ?? "UNKNOWN_OPERATION",
context.attempt ?? 0,
{ code: "UNKNOWN_FAILURE", causeClass },
);
}
+582
View File
@@ -0,0 +1,582 @@
/**
* External contract package consumer boundary (§4).
*
* This repository does not own OpenAPI/AsyncAPI source, operation semantics,
* Problem Details meaning or event payload schemas. It owns only the normalized
* descriptor interface, the runtime validator protocol, and the bounds it
* applies before a contribution may be composed.
*/
/** §7.3 hard ceilings. A contribution may lower these, never raise them. */
export const HTTP_EXECUTION_CEILINGS = Object.freeze({
defaultRequestBytes: 262_144,
hardRequestBytes: 1_048_576,
defaultResponseBytes: 1_048_576,
hardResponseBytes: 8_388_608,
problemResponseBytes: 65_536,
pathTemplateBytes: 512,
encodedQueryBytes: 8_192,
examinedHeaderValueBytes: 8_192,
defaultTotalDeadlineMs: 10_000,
hardTotalDeadlineMs: 60_000,
hardRetryCount: 2,
finalUrlBytes: 16_384,
});
export type RuntimeValidationIssue = Readonly<{
path: readonly (string | number)[];
code: string;
}>;
export type RuntimeValidationResult<T> =
| Readonly<{ success: true; data: T }>
| Readonly<{ success: false; issues: readonly RuntimeValidationIssue[] }>;
export interface RuntimeValidator<T> {
readonly schemaId: string;
safeParse(value: unknown): RuntimeValidationResult<T>;
}
/**
* A validator invocation never escapes as a native throw. `THROWN` is the
* `CONTRACT_RUNTIME_FAILURE` signal; `false` success is the ordinary
* `CONTRACT_VALUE_INVALID` signal.
*/
export type ValidatorInvocation<T> =
| Readonly<{ outcome: "VALID"; value: T }>
| Readonly<{ outcome: "INVALID"; issues: readonly RuntimeValidationIssue[] }>
| Readonly<{ outcome: "THROWN" }>;
export function invokeValidator<T>(
validator: RuntimeValidator<T>,
value: unknown,
): ValidatorInvocation<T> {
let result: RuntimeValidationResult<T>;
try {
result = validator.safeParse(value);
} catch {
return Object.freeze({ outcome: "THROWN" as const });
}
if (!result || typeof result !== "object" || !("success" in result)) {
return Object.freeze({ outcome: "THROWN" as const });
}
if (result.success) {
return Object.freeze({ outcome: "VALID" as const, value: result.data });
}
return Object.freeze({
outcome: "INVALID" as const,
issues: Object.freeze([...(result.issues ?? [])]),
});
}
export type MappingResult<T> =
| Readonly<{ ok: true; value: T }>
| Readonly<{
ok: false;
error: Readonly<{ kind: "MAPPING_CONTRACT_VIOLATION"; code: string }>;
}>;
export function mappingViolation(code: string): MappingResult<never> {
return Object.freeze({
ok: false as const,
error: Object.freeze({
kind: "MAPPING_CONTRACT_VIOLATION" as const,
code,
}),
});
}
export type CommandRecoveryDescriptor = Readonly<{
mode: "IDEMPOTENCY_REPLAY" | "INSPECT_OPERATION";
operationIdentityField: string;
inspectOperationId?: string;
}>;
export type CommandEffectClassification =
| "NOT_APPLIED"
| "APPLIED_CONFIRMED"
| "MAYBE_APPLIED";
export interface CommandEffectDescriptor<Problem> {
readonly successEffect: "APPLIED_CONFIRMED";
classifyProblem(
input: Readonly<{ status: number; problem: Problem }>,
): CommandEffectClassification;
}
export type HttpMethod =
| "GET"
| "HEAD"
| "POST"
| "PUT"
| "PATCH"
| "DELETE";
export type RetrySemantics = "SAFE" | "IDEMPOTENT" | "KEYED" | "NEVER";
export interface HttpExecutionPolicy {
readonly policyId: string;
readonly requestByteLimit: number;
readonly responseByteLimit: number;
readonly totalDeadlineMs: number;
readonly retryBudget: 0 | 1 | 2;
readonly authProfileId: string;
readonly diagnosticsOperation: string;
}
export interface InstalledHttpContract<Input, WireOutput, Problem> {
readonly contract: Readonly<{
operationId: string;
method: HttpMethod;
pathTemplate: string;
inputValidator: RuntimeValidator<Input>;
outputValidator: RuntimeValidator<WireOutput>;
problemValidator: RuntimeValidator<Problem>;
acceptedStatuses: readonly number[];
emptyBodyStatuses: readonly number[];
retrySemantics: RetrySemantics;
requestBody: "NONE" | "JSON";
responseBody: "REQUIRED_JSON" | "OPTIONAL_JSON" | "NONE";
commandRecovery: CommandRecoveryDescriptor | null;
commandEffect: CommandEffectDescriptor<Problem> | null;
/**
* Descriptor-owned projection from canonical application input to the wire
* request. The frontend never re-derives method, path or query semantics.
*/
projectRequest(input: Input): HttpRequestProjection;
}>;
readonly frontend: HttpExecutionPolicy;
}
export type HttpRequestProjection = Readonly<{
/** Ordered path placeholder values keyed by descriptor placeholder name. */
pathValues: Readonly<Record<string, string>>;
/** Descriptor-generated query entry order; array encoding is descriptor-owned. */
queryEntries: readonly (readonly [string, string])[];
/** Canonical JSON body value, or `null` when `requestBody` is `NONE`. */
body: unknown;
}>;
export interface InstalledEventContract<Envelope, Payload> {
readonly eventType: string;
readonly envelopeValidator: RuntimeValidator<Envelope>;
readonly payloadValidator: RuntimeValidator<Payload>;
}
export interface InstalledContractPackageIdentity {
readonly packageId: string;
readonly version: string;
readonly digest: `sha256:${string}`;
readonly runtimeProtocolVersion: 1;
readonly sourceRevision: string;
}
export type ContractContributionSource =
| Readonly<{
kind: "EXTERNAL_PACKAGE";
package: InstalledContractPackageIdentity;
}>
| Readonly<{
kind: "TEMPLATE_FIXTURE";
fixtureId: "REFERENCE_FEATURE_V1";
revision: 1;
}>;
export interface InstalledContractContribution {
readonly contributionId: string;
readonly featureId: string;
readonly source: ContractContributionSource;
readonly http: readonly InstalledHttpContract<unknown, unknown, unknown>[];
readonly events: readonly InstalledEventContract<unknown, unknown>[];
}
export type ContractCompositionFailureCode =
| "CONTRACT_CONTRIBUTION_INVALID"
| "CONTRACT_RUNTIME_PROTOCOL_UNSUPPORTED";
export class ContractContributionError extends Error {
readonly code: ContractCompositionFailureCode;
readonly reason: string;
constructor(reason: string, code: ContractCompositionFailureCode = "CONTRACT_CONTRIBUTION_INVALID") {
super("Installed contract contribution is not composable");
this.name = "ContractContributionError";
this.code = code;
this.reason = reason;
}
}
// §4.9 exact validation vocabulary.
const FEATURE_ID = /^[a-z][a-z0-9-]{0,63}$/;
const CONTRIBUTION_ID = /^[a-z][a-z0-9._-]{0,127}$/;
const PACKAGE_ID =
/^@[a-z0-9][a-z0-9._-]{0,62}\/[a-z0-9][a-z0-9._-]{0,62}$/;
const SEM_VER =
/^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-((?:0|[1-9]\d*|\d*[A-Za-z-][0-9A-Za-z-]*)(?:\.(?:0|[1-9]\d*|\d*[A-Za-z-][0-9A-Za-z-]*))*))?(?:\+([0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*))?$/;
const DIGEST = /^sha256:[0-9a-f]{64}$/;
const SOURCE_REVISION = /^[0-9a-f]{7,64}$/;
const OPERATION_ID = /^[A-Za-z][A-Za-z0-9_.-]{0,127}$/;
/** Whitespace and C0/C1 control characters are rejected in an event type. */
function hasControlOrSpace(value: string): boolean {
for (const character of value) {
const code = character.codePointAt(0) ?? 0;
if (code <= 0x20 || (code >= 0x7f && code <= 0x9f)) return true;
}
return false;
}
const encoder = new TextEncoder();
function utf8Bytes(value: string): number {
return encoder.encode(value).byteLength;
}
function fail(reason: string): never {
throw new ContractContributionError(reason);
}
function assertPackageIdentity(
identity: InstalledContractPackageIdentity,
featureId: string,
): void {
if (!identity || typeof identity !== "object") {
fail(`${featureId}: package identity`);
}
if (identity.runtimeProtocolVersion !== 1) {
throw new ContractContributionError(
`${featureId}: runtime protocol version must be exactly 1`,
"CONTRACT_RUNTIME_PROTOCOL_UNSUPPORTED",
);
}
if (!PACKAGE_ID.test(identity.packageId)) fail(`${featureId}: packageId`);
if (
typeof identity.version !== "string" ||
identity.version !== identity.version.trim() ||
identity.version.startsWith("v") ||
!SEM_VER.test(identity.version)
) {
fail(`${featureId}: package version must be exact SemVer`);
}
if (!DIGEST.test(identity.digest)) fail(`${featureId}: package digest`);
if (!SOURCE_REVISION.test(identity.sourceRevision)) {
fail(`${featureId}: package sourceRevision`);
}
}
function assertValidator(
validator: RuntimeValidator<unknown>,
label: string,
): void {
if (
!validator ||
typeof validator.safeParse !== "function" ||
typeof validator.schemaId !== "string" ||
validator.schemaId.length === 0
) {
fail(`${label}: runtime validator with non-empty schemaId is required`);
}
}
function assertStatuses(
statuses: readonly number[],
label: string,
): void {
if (!Array.isArray(statuses)) fail(`${label}: status array required`);
if (statuses.length < 1 || statuses.length > 32) {
fail(`${label}: 1..32 statuses required`);
}
for (let index = 0; index < statuses.length; index += 1) {
const status = statuses[index] as number;
if (!Number.isInteger(status) || status < 100 || status > 599) {
fail(`${label}: status out of range`);
}
const previous = statuses[index - 1];
if (index > 0 && previous !== undefined && status <= previous) {
fail(`${label}: statuses must be sorted and unique`);
}
}
}
function assertExecutionPolicy(
policy: HttpExecutionPolicy,
label: string,
): void {
const ceilings = HTTP_EXECUTION_CEILINGS;
if (
!policy ||
typeof policy.policyId !== "string" ||
policy.policyId.length === 0 ||
typeof policy.authProfileId !== "string" ||
policy.authProfileId.length === 0 ||
typeof policy.diagnosticsOperation !== "string" ||
policy.diagnosticsOperation.length === 0
) {
fail(`${label}: frontend execution policy identity`);
}
if (
!Number.isSafeInteger(policy.requestByteLimit) ||
policy.requestByteLimit < 0 ||
policy.requestByteLimit > ceilings.hardRequestBytes
) {
fail(`${label}: requestByteLimit exceeds the hard ceiling`);
}
if (
!Number.isSafeInteger(policy.responseByteLimit) ||
policy.responseByteLimit < 1 ||
policy.responseByteLimit > ceilings.hardResponseBytes
) {
fail(`${label}: responseByteLimit exceeds the hard ceiling`);
}
if (
!Number.isSafeInteger(policy.totalDeadlineMs) ||
policy.totalDeadlineMs < 1 ||
policy.totalDeadlineMs > ceilings.hardTotalDeadlineMs
) {
fail(`${label}: totalDeadlineMs exceeds the hard ceiling`);
}
if (
policy.retryBudget !== 0 &&
policy.retryBudget !== 1 &&
policy.retryBudget !== 2
) {
fail(`${label}: retryBudget must be 0, 1 or 2`);
}
}
function assertHttpContract(
installed: InstalledHttpContract<unknown, unknown, unknown>,
featureId: string,
): void {
const contract = installed?.contract;
if (!contract || typeof contract !== "object") {
fail(`${featureId}: http contract descriptor missing`);
}
const label = `${featureId}/${String(contract.operationId)}`;
if (
typeof contract.operationId !== "string" ||
!OPERATION_ID.test(contract.operationId)
) {
fail(`${label}: operationId`);
}
if (
contract.method !== "GET" &&
contract.method !== "HEAD" &&
contract.method !== "POST" &&
contract.method !== "PUT" &&
contract.method !== "PATCH" &&
contract.method !== "DELETE"
) {
fail(`${label}: method`);
}
if (
typeof contract.pathTemplate !== "string" ||
!contract.pathTemplate.startsWith("/") ||
utf8Bytes(contract.pathTemplate) > HTTP_EXECUTION_CEILINGS.pathTemplateBytes ||
contract.pathTemplate.includes("?") ||
contract.pathTemplate.includes("#")
) {
fail(`${label}: pathTemplate`);
}
assertValidator(contract.inputValidator, `${label}.input`);
assertValidator(contract.outputValidator, `${label}.output`);
assertValidator(contract.problemValidator, `${label}.problem`);
if (typeof contract.projectRequest !== "function") {
fail(`${label}: descriptor request projection is required`);
}
if (
contract.retrySemantics !== "SAFE" &&
contract.retrySemantics !== "IDEMPOTENT" &&
contract.retrySemantics !== "KEYED" &&
contract.retrySemantics !== "NEVER"
) {
fail(`${label}: retrySemantics`);
}
if (contract.requestBody !== "NONE" && contract.requestBody !== "JSON") {
fail(`${label}: requestBody`);
}
if (
contract.responseBody !== "REQUIRED_JSON" &&
contract.responseBody !== "OPTIONAL_JSON" &&
contract.responseBody !== "NONE"
) {
fail(`${label}: responseBody`);
}
assertStatuses(contract.acceptedStatuses, `${label}.acceptedStatuses`);
if (contract.emptyBodyStatuses.length > 0) {
assertStatuses(contract.emptyBodyStatuses, `${label}.emptyBodyStatuses`);
const accepted = new Set(contract.acceptedStatuses);
for (const status of contract.emptyBodyStatuses) {
if (!accepted.has(status)) {
fail(`${label}: empty-body status must be an accepted status`);
}
}
}
const isRead = contract.method === "GET" || contract.method === "HEAD";
if (isRead) {
if (contract.commandEffect !== null || contract.commandRecovery !== null) {
fail(`${label}: read operations carry no command descriptors`);
}
if (contract.requestBody !== "NONE") {
fail(`${label}: read operations carry no request body`);
}
} else {
if (!contract.commandEffect) {
fail(`${label}: command operations require a command effect descriptor`);
}
if (contract.commandEffect.successEffect !== "APPLIED_CONFIRMED") {
fail(`${label}: command success effect`);
}
if (typeof contract.commandEffect.classifyProblem !== "function") {
fail(`${label}: command effect classifier is required`);
}
if (contract.retrySemantics === "KEYED" && !contract.commandRecovery) {
fail(`${label}: KEYED commands require a recovery descriptor`);
}
}
if (contract.commandRecovery) {
const recovery = contract.commandRecovery;
if (
(recovery.mode !== "IDEMPOTENCY_REPLAY" &&
recovery.mode !== "INSPECT_OPERATION") ||
!recovery.operationIdentityField ||
(recovery.mode === "INSPECT_OPERATION" && !recovery.inspectOperationId)
) {
fail(`${label}: command recovery descriptor`);
}
}
assertExecutionPolicy(installed.frontend, label);
if (
installed.frontend.retryBudget > 0 &&
contract.retrySemantics === "NEVER"
) {
fail(`${label}: retry budget contradicts NEVER retry semantics`);
}
}
function assertEventContract(
event: InstalledEventContract<unknown, unknown>,
featureId: string,
): void {
const label = `${featureId}/${String(event?.eventType)}`;
if (
typeof event?.eventType !== "string" ||
event.eventType.length === 0 ||
utf8Bytes(event.eventType) > 160 ||
hasControlOrSpace(event.eventType)
) {
fail(`${label}: eventType`);
}
assertValidator(event.envelopeValidator, `${label}.envelope`);
assertValidator(event.payloadValidator, `${label}.payload`);
}
export type ComposedContractContributions = Readonly<{
contributions: readonly InstalledContractContribution[];
httpByOperationId: ReadonlyMap<
string,
InstalledHttpContract<unknown, unknown, unknown>
>;
eventByType: ReadonlyMap<string, InstalledEventContract<unknown, unknown>>;
externalPackages: readonly InstalledContractPackageIdentity[];
}>;
/**
* §4.8–§4.9. The only place installed contributions become a runtime registry.
* Every bound is checked before composition; a violation stops the boot rather
* than degrading into an assumed meaning.
*/
export function composeContractContributions(
contributions: readonly InstalledContractContribution[],
): ComposedContractContributions {
if (!Array.isArray(contributions)) fail("contributions: array required");
const httpByOperationId = new Map<
string,
InstalledHttpContract<unknown, unknown, unknown>
>();
const eventByType = new Map<
string,
InstalledEventContract<unknown, unknown>
>();
const packagesById = new Map<string, InstalledContractPackageIdentity>();
const contributionIds = new Set<string>();
for (const contribution of contributions) {
if (!contribution || typeof contribution !== "object") {
fail("contribution: object required");
}
const contributionId = contribution.contributionId;
if (
typeof contributionId !== "string" ||
!CONTRIBUTION_ID.test(contributionId)
) {
fail(`contributionId: ${String(contributionId)}`);
}
if (contributionIds.has(contributionId)) {
fail(`duplicate contributionId: ${contributionId}`);
}
contributionIds.add(contributionId);
const featureId = contribution?.featureId;
if (typeof featureId !== "string" || !FEATURE_ID.test(featureId)) {
fail(`featureId: ${String(featureId)}`);
}
const source = contribution.source;
if (!source || typeof source !== "object" || !("kind" in source)) {
fail(`${featureId}: source`);
}
if (!Array.isArray(contribution.http) || !Array.isArray(contribution.events)) {
fail(`${featureId}: contribution arrays`);
}
if (source.kind === "EXTERNAL_PACKAGE") {
assertPackageIdentity(source.package, featureId);
const existing = packagesById.get(source.package.packageId);
if (
existing &&
(existing.version !== source.package.version ||
existing.digest !== source.package.digest ||
existing.sourceRevision !== source.package.sourceRevision)
) {
fail(
`${featureId}: package ${source.package.packageId} has conflicting identities`,
);
}
packagesById.set(source.package.packageId, source.package);
} else if (source.kind === "TEMPLATE_FIXTURE") {
if (source.fixtureId !== "REFERENCE_FEATURE_V1" || source.revision !== 1) {
fail(`${featureId}: template fixture identity`);
}
if (contribution.events.length !== 0) {
fail(`${featureId}: template fixture must not contribute events`);
}
} else {
fail(`${featureId}: unknown contribution source kind`);
}
for (const installed of contribution.http) {
assertHttpContract(installed, featureId);
const operationId = installed.contract.operationId;
const previous = httpByOperationId.get(operationId);
if (previous) fail(`duplicate operation: ${operationId}`);
httpByOperationId.set(operationId, installed);
}
for (const event of contribution.events) {
assertEventContract(event, featureId);
if (eventByType.has(event.eventType)) {
fail(`duplicate event type: ${event.eventType}`);
}
eventByType.set(event.eventType, event);
}
}
const externalPackages = [...packagesById.values()].map((identity) =>
Object.freeze({ ...identity }),
);
return Object.freeze({
contributions: Object.freeze([...contributions]),
httpByOperationId,
eventByType,
externalPackages: Object.freeze(externalPackages),
});
}
+59
View File
@@ -0,0 +1,59 @@
export const MUTATION_INTENT_BOUNDS = Object.freeze({
intentIdMaxBytes: 256,
operationIdMaxBytes: 256,
canonicalInputIdentityMaxBytes: 16_384,
idempotencyKeyMaxBytes: 256,
} as const);
export type MutationIntent = Readonly<{
intentId: string;
operationId: string;
canonicalInputIdentity: string;
idempotencyKey?: string;
createdAtMonotonicMs: number;
}>;
const UTF8 = new TextEncoder();
function validBoundedString(value: unknown, maxBytes: number): value is string {
return (
typeof value === "string" &&
value.trim().length > 0 &&
UTF8.encode(value).byteLength <= maxBytes
);
}
export function defineMutationIntent(intent: MutationIntent): MutationIntent {
if (
!validBoundedString(
intent.intentId,
MUTATION_INTENT_BOUNDS.intentIdMaxBytes,
) ||
!validBoundedString(
intent.operationId,
MUTATION_INTENT_BOUNDS.operationIdMaxBytes,
) ||
!validBoundedString(
intent.canonicalInputIdentity,
MUTATION_INTENT_BOUNDS.canonicalInputIdentityMaxBytes,
) ||
(intent.idempotencyKey !== undefined &&
!validBoundedString(
intent.idempotencyKey,
MUTATION_INTENT_BOUNDS.idempotencyKeyMaxBytes,
)) ||
!Number.isFinite(intent.createdAtMonotonicMs) ||
intent.createdAtMonotonicMs < 0
) {
throw new TypeError("Mutation intent is invalid.");
}
return Object.freeze({
intentId: intent.intentId,
operationId: intent.operationId,
canonicalInputIdentity: intent.canonicalInputIdentity,
...(intent.idempotencyKey === undefined
? {}
: { idempotencyKey: intent.idempotencyKey }),
createdAtMonotonicMs: intent.createdAtMonotonicMs,
});
}
+154
View File
@@ -0,0 +1,154 @@
/**
* §19. Offline Command and Background Sync contract.
*
* The product capability is `NOT_SELECTED` (§19.1). An operation only becomes
* queueable when the external package contribution provides `KEYED` retry
* semantics plus a non-null recovery descriptor (§19.3); the frontend never
* defines server idempotency or an inspect protocol of its own.
*/
export const OFFLINE_COMMAND_BOUNDS = Object.freeze({
operations: 64,
defaultRequestBytes: 262_144,
hardRequestBytes: 1_048_576,
records: 1_000,
datasetBytes: 50 * 1024 * 1024,
senderLeaseMs: 30_000,
leaseRenewMs: 10_000,
batchCount: 10,
batchWindowMs: 30_000,
parallelSend: 1,
retryBaseMs: 1_000,
retryMaxMs: 300_000,
attemptCap: 10,
ordinaryRetentionMs: 7 * 24 * 60 * 60 * 1_000,
conflictRetentionMs: 30 * 24 * 60 * 60 * 1_000,
ackedSummaryRetentionMs: 24 * 60 * 60 * 1_000,
syncReregisterMinimumMs: 60_000,
});
/** §19.18. Background Sync is a wake-up hint only; it never sends a command. */
export const OFFLINE_SYNC_TAG = "ca-outbox-v1" as const;
export interface InstalledOfflineOperation {
readonly operationId: string;
readonly contractPackageId: string;
readonly maximumRequestBytes: number;
readonly retentionClass: "STANDARD_7D";
}
export interface InstalledOfflineCommandContribution {
readonly datasetId: "OFFLINE_COMMANDS_V1";
readonly operations: readonly InstalledOfflineOperation[];
}
export type OfflineCommandState =
| "PENDING"
| "LEASED"
| "FOREGROUND_REQUIRED"
| "SENDING"
| "RETRY_WAIT"
| "ACKED"
| "CONFLICT"
| "EFFECT_UNKNOWN"
| "EXPIRED";
export interface OfflineCommandRecordV1 {
readonly recordVersion: 1;
readonly commandId: string;
readonly operationId: string;
readonly contractPackageId: string;
readonly contractPackageVersion: string;
readonly contractPackageDigest: `sha256:${string}`;
readonly scopePartition: string;
readonly requestDigest: `sha256:${string}`;
readonly requestPayload: Uint8Array;
readonly idempotencyKey: string;
readonly state: OfflineCommandState;
readonly attempt: number;
readonly createdAt: string;
readonly updatedAt: string;
readonly nextAttemptAt?: string;
readonly leaseOwner?: string;
readonly leaseExpiresAt?: string;
readonly terminalCode?: string;
}
/**
* §19.10. Anything not listed is corruption. In particular an expired
* `SENDING` record is never reset to `PENDING`: it becomes `EFFECT_UNKNOWN`.
*/
const ALLOWED_TRANSITIONS = Object.freeze({
PENDING: Object.freeze(["LEASED", "FOREGROUND_REQUIRED", "EXPIRED"]),
LEASED: Object.freeze(["SENDING", "PENDING"]),
SENDING: Object.freeze([
"ACKED",
"RETRY_WAIT",
"CONFLICT",
"EFFECT_UNKNOWN",
]),
RETRY_WAIT: Object.freeze(["LEASED", "FOREGROUND_REQUIRED", "EXPIRED"]),
FOREGROUND_REQUIRED: Object.freeze(["LEASED", "EXPIRED", "PENDING"]),
ACKED: Object.freeze([]),
CONFLICT: Object.freeze([]),
EFFECT_UNKNOWN: Object.freeze(["ACKED", "PENDING"]),
EXPIRED: Object.freeze([]),
} satisfies Readonly<Record<OfflineCommandState, readonly OfflineCommandState[]>>);
export function isAllowedOfflineTransition(
from: OfflineCommandState,
to: OfflineCommandState,
): boolean {
const allowed: readonly OfflineCommandState[] = ALLOWED_TRANSITIONS[from];
return allowed.includes(to);
}
/** §19.21. Nothing sensitive reaches the UI: no payload, key, digest or partition. */
export interface OfflineCommandSummary {
readonly commandId: string;
readonly operationLabelKey: string;
readonly state: OfflineCommandState;
readonly createdAt: string;
readonly nextAction:
| "WAIT"
| "OPEN_APP"
| "CHECK_STATUS"
| "RESOLVE_CONFLICT"
| "CONTACT_SUPPORT"
| "DISMISS";
}
export function validateOfflineCommandContribution(
contribution: InstalledOfflineCommandContribution,
): InstalledOfflineCommandContribution {
if (contribution.datasetId !== "OFFLINE_COMMANDS_V1") {
throw new TypeError("Offline command dataset identity is invalid.");
}
if (
contribution.operations.length === 0 ||
contribution.operations.length > OFFLINE_COMMAND_BOUNDS.operations
) {
throw new TypeError("Offline command operation count is out of range.");
}
const seen = new Set<string>();
for (const operation of contribution.operations) {
if (!operation.operationId || seen.has(operation.operationId)) {
throw new TypeError("Duplicate offline command operation.");
}
seen.add(operation.operationId);
if (
!Number.isSafeInteger(operation.maximumRequestBytes) ||
operation.maximumRequestBytes < 1 ||
operation.maximumRequestBytes > OFFLINE_COMMAND_BOUNDS.hardRequestBytes ||
operation.retentionClass !== "STANDARD_7D"
) {
throw new TypeError(
`Offline command operation bounds invalid: ${operation.operationId}`,
);
}
}
return Object.freeze({
datasetId: contribution.datasetId,
operations: Object.freeze([...contribution.operations]),
});
}
+258
View File
@@ -0,0 +1,258 @@
import { isCacheInvalidationTopic } from "./cache-invalidation.ts";
import {
defineQueryNamespaceIdentity,
queryNamespaceIdentityKey,
type QueryNamespaceIdentity,
} from "./query-keys.ts";
declare const queryInvalidationTopicBrand: unique symbol;
/**
* Opaque registry-issued invalidation identity. The brand prevents feature
* code from accidentally passing a concrete query-key string to the mutation
* bridge.
*/
export type QueryInvalidationTopic = string &
Readonly<{ [queryInvalidationTopicBrand]: true }>;
export function defineQueryInvalidationTopic(
value: string,
): QueryInvalidationTopic {
if (!isCacheInvalidationTopic(value)) {
throw new TypeError("Query invalidation topic is invalid.");
}
return value as QueryInvalidationTopic;
}
/**
* §12.2. Many-to-many topic/namespace registry.
*
* Topics stay opaque on the wire; the registry is what turns one received topic
* into the local namespaces that must revalidate. Bounds are checked at startup
* so a fan-out explosion cannot be introduced at runtime.
*/
export const INVALIDATION_REGISTRY_BOUNDS = Object.freeze({
maxTopics: 256,
maxNamespaces: 256,
maxEdges: 1_024,
maxTopicFanOut: 64,
maxNamespaceFanIn: 64,
maxIdBytes: 80,
});
export type InvalidationRegistryEdge = Readonly<{
topicId: string;
namespace: QueryNamespaceIdentity;
}>;
export interface InvalidationRegistry {
readonly topics: readonly string[];
readonly namespaces: readonly QueryNamespaceIdentity[];
readonly edges: readonly InvalidationRegistryEdge[];
}
export type InvalidationRegistryIndex = Readonly<{
namespacesForTopic: ReadonlyMap<
string,
readonly QueryNamespaceIdentity[]
>;
topicsForNamespace: ReadonlyMap<string, readonly string[]>;
}>;
export type InvalidationTopicVersionDefinition = Readonly<{
topicId: string;
topicVersion: number;
}>;
function hasControlCharacter(value: string): boolean {
for (const character of value) {
const codePoint = character.codePointAt(0) ?? 0;
if (codePoint <= 0x1f || codePoint === 0x7f) return true;
}
return false;
}
/**
* Rejects duplicates, orphan topics and orphan namespaces at startup. An edge
* that points at an unregistered endpoint is a composition defect, not a
* runtime condition to be tolerated.
*/
export function indexInvalidationRegistry(
registry: InvalidationRegistry,
): InvalidationRegistryIndex {
const bounds = INVALIDATION_REGISTRY_BOUNDS;
const encoder = new TextEncoder();
const assertId = (value: string, label: string) => {
if (
typeof value !== "string" ||
value.length === 0 ||
hasControlCharacter(value) ||
encoder.encode(value).byteLength > bounds.maxIdBytes
) {
throw new TypeError(`Invalidation registry ${label} is invalid.`);
}
};
if (
registry.topics.length > bounds.maxTopics ||
registry.namespaces.length > bounds.maxNamespaces ||
registry.edges.length > bounds.maxEdges
) {
throw new TypeError("Invalidation registry exceeds its bounds.");
}
const topics = new Set<string>();
for (const topic of registry.topics) {
assertId(topic, "topic");
if (topics.has(topic)) {
throw new TypeError(`Duplicate invalidation topic: ${topic}`);
}
topics.add(topic);
}
const namespaces = new Map<string, QueryNamespaceIdentity>();
for (const namespace of registry.namespaces) {
let namespaceKey: string;
let namespaceSnapshot: QueryNamespaceIdentity;
try {
namespaceSnapshot = defineQueryNamespaceIdentity(
namespace.namespaceId,
namespace.namespaceVersion,
);
namespaceKey = queryNamespaceIdentityKey(namespaceSnapshot);
} catch (error) {
throw new TypeError("Invalidation registry namespace is invalid.", {
cause: error,
});
}
if (namespaces.has(namespaceKey)) {
throw new TypeError(`Duplicate invalidation namespace: ${namespaceKey}`);
}
namespaces.set(namespaceKey, namespaceSnapshot);
}
const namespacesForTopic = new Map<string, QueryNamespaceIdentity[]>();
const topicsForNamespace = new Map<string, string[]>();
const seenEdges = new Map<string, Set<string>>();
for (const edge of registry.edges) {
let namespaceKey: string;
try {
namespaceKey = queryNamespaceIdentityKey(edge.namespace);
} catch (error) {
throw new TypeError("Invalidation registry namespace is invalid.", {
cause: error,
});
}
const registeredNamespace = namespaces.get(namespaceKey);
if (!topics.has(edge.topicId) || !registeredNamespace) {
throw new TypeError("Invalidation edge references an unknown endpoint.");
}
const seenNamespaces = seenEdges.get(edge.topicId) ?? new Set<string>();
if (seenNamespaces.has(namespaceKey)) {
throw new TypeError("Duplicate invalidation edge.");
}
seenNamespaces.add(namespaceKey);
seenEdges.set(edge.topicId, seenNamespaces);
const fanOut = namespacesForTopic.get(edge.topicId) ?? [];
fanOut.push(registeredNamespace);
if (fanOut.length > bounds.maxTopicFanOut) {
throw new TypeError(`Invalidation topic fan-out exceeded: ${edge.topicId}`);
}
namespacesForTopic.set(edge.topicId, fanOut);
const fanIn = topicsForNamespace.get(namespaceKey) ?? [];
fanIn.push(edge.topicId);
if (fanIn.length > bounds.maxNamespaceFanIn) {
throw new TypeError(
`Invalidation namespace fan-in exceeded: ${namespaceKey}`,
);
}
topicsForNamespace.set(namespaceKey, fanIn);
}
for (const topic of topics) {
if (!namespacesForTopic.has(topic)) {
throw new TypeError(`Orphan invalidation topic: ${topic}`);
}
}
for (const namespaceKey of namespaces.keys()) {
if (!topicsForNamespace.has(namespaceKey)) {
throw new TypeError(`Orphan invalidation namespace: ${namespaceKey}`);
}
}
return Object.freeze({
namespacesForTopic: new Map(
[...namespacesForTopic].map(([key, value]) => [
key,
Object.freeze([...value]) as readonly QueryNamespaceIdentity[],
]),
),
topicsForNamespace: new Map(
[...topicsForNamespace].map(([key, value]) => [
key,
Object.freeze([...value]) as readonly string[],
]),
),
});
}
/** Projects the graph's wire-only topic versions and rejects composition drift. */
export function indexInvalidationTopicVersions(
registry: Pick<InvalidationRegistry, "topics">,
definitions: readonly InvalidationTopicVersionDefinition[],
): ReadonlyMap<string, number> {
if (
registry.topics.length > INVALIDATION_REGISTRY_BOUNDS.maxTopics ||
definitions.length > INVALIDATION_REGISTRY_BOUNDS.maxTopics
) {
throw new TypeError("Invalidation topic version registry exceeds its bounds.");
}
const registeredTopics = new Set(registry.topics);
if (
registeredTopics.size !== registry.topics.length ||
definitions.length !== registeredTopics.size
) {
throw new TypeError("Invalidation topic version registry is inconsistent.");
}
const versions = new Map<string, number>();
for (const definition of definitions) {
if (
!isCacheInvalidationTopic(definition.topicId) ||
!registeredTopics.has(definition.topicId) ||
!Number.isSafeInteger(definition.topicVersion) ||
definition.topicVersion < 1 ||
versions.has(definition.topicId)
) {
throw new TypeError("Invalidation topic version registry is invalid.");
}
versions.set(definition.topicId, definition.topicVersion);
}
return versions;
}
export type QueryMutationLease = Readonly<{
/**
* Releases one local mutation fence. Remote hints coalesced while the fence
* was held are applied once after the final lease for each topic is released.
*/
release(): Promise<void>;
}>;
/**
* Presentation-side facade for server-state invalidation.
*
* The caller knows only registry-issued topics. Query keys, BroadcastChannel
* envelopes and browser transports stay inside the query infrastructure.
*/
export interface QueryInvalidationCoordinator {
invalidate(topics: readonly QueryInvalidationTopic[]): Promise<void>;
beginMutation(topics: readonly QueryInvalidationTopic[]): QueryMutationLease;
/**
* Local verified lifecycle only. A remote invalidation hint is never allowed
* to clear the complete cache.
*/
resetLocal(): Promise<void>;
dispose(): void;
}
+315
View File
@@ -0,0 +1,315 @@
export const QUERY_KEY_SCHEMA_VERSION = 2 as const;
export const QUERY_NAMESPACE_ID_MAX_BYTES = 80 as const;
export type QueryNamespaceIdentity = Readonly<{
namespaceId: string;
namespaceVersion: number;
}>;
function hasControlCharacter(value: string): boolean {
for (const character of value) {
const codePoint = character.codePointAt(0) ?? 0;
if (
codePoint <= 0x1f ||
(codePoint >= 0x7f && codePoint <= 0x9f)
) {
return true;
}
}
return false;
}
function assertQueryNamespaceIdentity(
namespace: QueryNamespaceIdentity,
): void {
if (
!namespace ||
typeof namespace !== "object" ||
typeof namespace.namespaceId !== "string" ||
namespace.namespaceId.length === 0 ||
hasControlCharacter(namespace.namespaceId) ||
new TextEncoder().encode(namespace.namespaceId).byteLength >
QUERY_NAMESPACE_ID_MAX_BYTES ||
!Number.isSafeInteger(namespace.namespaceVersion) ||
namespace.namespaceVersion < 1
) {
throw new TypeError("Query namespace identity is invalid.");
}
}
export function defineQueryNamespaceIdentity(
namespaceId: string,
namespaceVersion: number,
): QueryNamespaceIdentity {
const namespace = { namespaceId, namespaceVersion };
assertQueryNamespaceIdentity(namespace);
return Object.freeze(namespace);
}
export function queryNamespaceIdentityKey(
namespace: QueryNamespaceIdentity,
): string {
assertQueryNamespaceIdentity(namespace);
return JSON.stringify([namespace.namespaceId, namespace.namespaceVersion]);
}
export function createQueryInvalidationPrefix(
namespace: QueryNamespaceIdentity,
) {
assertQueryNamespaceIdentity(namespace);
return Object.freeze([
"query",
QUERY_KEY_SCHEMA_VERSION,
namespace.namespaceId,
namespace.namespaceVersion,
] as const);
}
export function createBoundQueryKey(
namespace: QueryNamespaceIdentity,
scopeFingerprint: string,
definitionVersion: number,
identityToken: string,
) {
if (!Number.isSafeInteger(definitionVersion) || definitionVersion < 1) {
throw new TypeError("Query definition version is invalid.");
}
return Object.freeze([
...createQueryInvalidationPrefix(namespace),
scopeFingerprint,
definitionVersion,
identityToken,
] as const);
}
export type CanonicalValue =
| null
| boolean
| number
| string
| readonly CanonicalValue[]
| Readonly<{ [key: string]: CanonicalValue }>;
const DEFAULT_LIMITS = Object.freeze({
maxDepth: 12,
maxNodes: 512,
maxStringBytes: 2_048,
maxEncodedBytes: 16_384,
});
export function canonicalize(value: unknown): CanonicalValue {
const seen = new WeakSet<object>();
let nodes = 0;
const encoder = new TextEncoder();
function visit(candidate: unknown, depth: number): CanonicalValue {
nodes += 1;
if (nodes > DEFAULT_LIMITS.maxNodes || depth > DEFAULT_LIMITS.maxDepth) {
throw new TypeError("Query identity exceeds its structural budget.");
}
if (
candidate === null ||
typeof candidate === "boolean" ||
(typeof candidate === "number" &&
Number.isFinite(candidate) &&
!Object.is(candidate, -0))
) {
return candidate;
}
if (typeof candidate === "string") {
if (encoder.encode(candidate).byteLength > DEFAULT_LIMITS.maxStringBytes) {
throw new TypeError("Query identity string exceeds its byte budget.");
}
return candidate;
}
if (!candidate || typeof candidate !== "object") {
throw new TypeError("Query identity contains a non-canonical value.");
}
if (seen.has(candidate)) {
throw new TypeError("Query identity contains a cycle or shared reference.");
}
seen.add(candidate);
if (Array.isArray(candidate)) {
for (let index = 0; index < candidate.length; index += 1) {
if (!Object.hasOwn(candidate, index)) {
throw new TypeError("Query identity contains a sparse array.");
}
}
return Object.freeze(candidate.map((item) => visit(item, depth + 1)));
}
const prototype = Object.getPrototypeOf(candidate);
if (prototype !== Object.prototype && prototype !== null) {
throw new TypeError("Query identity requires plain objects.");
}
const descriptors = Object.getOwnPropertyDescriptors(candidate);
const output: Record<string, CanonicalValue> = Object.create(null);
for (const key of Object.keys(descriptors).sort()) {
if (key === "__proto__" || key === "prototype" || key === "constructor") {
throw new TypeError("Query identity contains a forbidden key.");
}
const descriptor = descriptors[key];
if (!descriptor || !("value" in descriptor)) {
throw new TypeError("Query identity contains an accessor.");
}
output[key] = visit(descriptor.value, depth + 1);
}
return Object.freeze(output);
}
const result = visit(value, 0);
if (encoder.encode(JSON.stringify(result)).byteLength > DEFAULT_LIMITS.maxEncodedBytes) {
throw new TypeError("Query identity exceeds its encoded byte budget.");
}
return result;
}
export type RuntimeIdentityBinding = Readonly<{
token: string;
acquire(): void;
release(): void;
}>;
export type RuntimeIdentityRegistry = Readonly<{
intern(value: unknown): RuntimeIdentityBinding;
close(): void;
inspect(): Readonly<{
entries: number;
canonicalBytes: number;
activeLeases: number;
closed: boolean;
}>;
}>;
type IdentityRow = {
canonical: string;
canonicalBytes: number;
token: string;
refCount: number;
touched: number;
};
export function createRuntimeIdentityRegistry(
options: Readonly<{
maxEntries?: number;
maxCanonicalBytes?: number;
tokenFactory?: () => string;
}> = {},
): RuntimeIdentityRegistry {
const maxEntries = options.maxEntries ?? 4_096;
const maxCanonicalBytes = options.maxCanonicalBytes ?? 4 * 1024 * 1024;
const tokenFactory =
options.tokenFactory ??
(() => {
if (
typeof crypto === "undefined" ||
typeof crypto.randomUUID !== "function"
) {
throw new TypeError("Secure runtime identity generation is unavailable.");
}
return crypto.randomUUID();
});
const byCanonical = new Map<string, IdentityRow>();
const byToken = new Map<string, IdentityRow>();
let totalCanonicalBytes = 0;
let sequence = 0;
let closed = false;
function evictAvailable(requiredBytes: number): void {
const candidates = [...byCanonical.values()]
.filter((row) => row.refCount === 0)
.sort((left, right) => left.touched - right.touched);
for (const row of candidates) {
if (
byCanonical.size < maxEntries &&
totalCanonicalBytes + requiredBytes <= maxCanonicalBytes
) {
return;
}
byCanonical.delete(row.canonical);
byToken.delete(row.token);
totalCanonicalBytes -= row.canonicalBytes;
}
}
return Object.freeze({
intern(value): RuntimeIdentityBinding {
if (closed) throw new TypeError("Runtime identity registry is closed.");
const canonical = JSON.stringify(canonicalize(value));
const canonicalBytes = new TextEncoder().encode(canonical).byteLength;
let row = byCanonical.get(canonical);
if (!row) {
evictAvailable(canonicalBytes);
if (
byCanonical.size >= maxEntries ||
totalCanonicalBytes + canonicalBytes > maxCanonicalBytes
) {
throw new TypeError("Runtime identity capacity exceeded.");
}
let token = "";
for (let attempt = 0; attempt < 8; attempt += 1) {
const candidate = tokenFactory();
if (
/^[A-Za-z0-9._:-]{16,128}$/.test(candidate) &&
!byToken.has(candidate)
) {
token = candidate;
break;
}
}
if (!token) {
throw new TypeError("Runtime identity token collision.");
}
row = {
canonical,
canonicalBytes,
token,
refCount: 0,
touched: sequence++,
};
byCanonical.set(canonical, row);
byToken.set(token, row);
totalCanonicalBytes += canonicalBytes;
}
row.touched = sequence++;
let leaseCount = 0;
return Object.freeze({
token: row.token,
acquire() {
if (closed) return;
leaseCount += 1;
row.refCount += 1;
row.touched = sequence++;
},
release() {
if (leaseCount === 0) return;
leaseCount -= 1;
row.refCount = Math.max(0, row.refCount - 1);
row.touched = sequence++;
},
});
},
close() {
closed = true;
byCanonical.clear();
byToken.clear();
totalCanonicalBytes = 0;
},
inspect() {
return Object.freeze({
entries: byCanonical.size,
canonicalBytes: totalCanonicalBytes,
activeLeases: [...byCanonical.values()].reduce(
(total, row) => total + row.refCount,
0,
),
closed,
});
},
});
}
const defaultIdentityRegistry = createRuntimeIdentityRegistry();
export function runtimeIdentityToken(value: unknown): string {
return defaultIdentityRegistry.intern(value).token;
}
+185
View File
@@ -0,0 +1,185 @@
import {
REALTIME_EVENT_PROTOCOL,
type EventTypeId,
type StreamRegistrationId,
} from "./realtime-streams.ts";
export const REALTIME_EVENT_FIELD_LIMITS = Object.freeze({
maxOpaqueIdentifierLength: 128,
maxScopeBindingLength: 256,
maxResumeCursorLength: 1_024,
maxSequenceDigits: 20,
maxTimestampFractionDigits: 9,
});
export const REALTIME_MAX_SEQUENCE = "18446744073709551615";
export type RealtimeEventBase<Payload> = Readonly<{
protocol: typeof REALTIME_EVENT_PROTOCOL;
streamId: StreamRegistrationId;
streamEpoch: string;
eventType: EventTypeId;
eventId: string;
sequence: string;
occurredAt: string;
scopeBinding: string;
payload: Payload;
}>;
export type RealtimeEventEnvelope<Payload = unknown> = Readonly<
RealtimeEventBase<Payload> &
(
| Readonly<{
recoveryMode: "CURSOR";
resumeCursor: string;
}>
| Readonly<{
recoveryMode: "SNAPSHOT_ONLY" | "SESSION_REBUILD";
resumeCursor: null;
}>
)
>;
export type SnapshotCheckpoint = Readonly<
{
streamEpoch: string;
lastAppliedSequence: string;
snapshotRevision: string;
} & (
| Readonly<{
recoveryMode: "CURSOR";
resumeCursor: string;
}>
| Readonly<{
recoveryMode: "SNAPSHOT_ONLY";
resumeCursor: null;
}>
)
>;
export type RealtimeResumeState = Readonly<
{
streamEpoch: string;
lastAppliedSequence: string;
} & (
| Readonly<{
recoveryMode: "CURSOR";
resumeCursor: string;
}>
| Readonly<{
recoveryMode: "SNAPSHOT_ONLY" | "SESSION_REBUILD";
resumeCursor: null;
}>
)
>;
const OPAQUE_IDENTIFIER = /^[A-Za-z0-9][A-Za-z0-9._:~+/=-]*$/u;
const HEADER_SAFE_CURSOR = /^[\x21-\x7e]+$/u;
const CANONICAL_SEQUENCE = /^(?:0|[1-9][0-9]{0,19})$/u;
const RFC_3339 =
/^([0-9]{4})-(0[1-9]|1[0-2])-([0-2][0-9]|3[01])T([01][0-9]|2[0-3]):([0-5][0-9]):([0-5][0-9])(?:\.([0-9]{1,9}))?(Z|([+-])([01][0-9]|2[0-3]):([0-5][0-9]))$/u;
export function isRealtimeOpaqueIdentifier(
value: unknown,
): value is string {
return (
typeof value === "string" &&
value.length >= 1 &&
value.length <=
REALTIME_EVENT_FIELD_LIMITS.maxOpaqueIdentifierLength &&
OPAQUE_IDENTIFIER.test(value)
);
}
export function isRealtimeScopeBinding(
value: unknown,
): value is string {
return (
typeof value === "string" &&
value.length >= 1 &&
value.length <= REALTIME_EVENT_FIELD_LIMITS.maxScopeBindingLength &&
OPAQUE_IDENTIFIER.test(value)
);
}
export function isRealtimeResumeCursor(
value: unknown,
): value is string {
return (
typeof value === "string" &&
value.length >= 1 &&
value.length <=
REALTIME_EVENT_FIELD_LIMITS.maxResumeCursorLength &&
HEADER_SAFE_CURSOR.test(value) &&
!value.includes("\0") &&
!value.includes("\r") &&
!value.includes("\n")
);
}
export function isCanonicalRealtimeSequence(
value: unknown,
): value is string {
if (
typeof value !== "string" ||
value.length > REALTIME_EVENT_FIELD_LIMITS.maxSequenceDigits ||
!CANONICAL_SEQUENCE.test(value)
) {
return false;
}
try {
return BigInt(value) <= BigInt(REALTIME_MAX_SEQUENCE);
} catch {
return false;
}
}
export function compareRealtimeSequences(
left: string,
right: string,
): -1 | 0 | 1 {
if (
!isCanonicalRealtimeSequence(left) ||
!isCanonicalRealtimeSequence(right)
) {
throw new TypeError("Realtime sequence is invalid.");
}
const leftValue = BigInt(left);
const rightValue = BigInt(right);
return leftValue < rightValue ? -1 : leftValue > rightValue ? 1 : 0;
}
export function nextRealtimeSequence(value: string): string | null {
if (!isCanonicalRealtimeSequence(value)) {
throw new TypeError("Realtime sequence is invalid.");
}
const next = BigInt(value) + 1n;
return next > BigInt(REALTIME_MAX_SEQUENCE) ? null : next.toString(10);
}
/**
* A bounded RFC 3339 profile: uppercase T/Z, a required offset, real calendar
* dates, seconds 00-59 and at most nanosecond fractional precision.
*/
export function isStrictRealtimeTimestamp(
value: unknown,
): value is string {
if (typeof value !== "string") return false;
const match = RFC_3339.exec(value);
if (!match) return false;
const year = Number(match[1]);
const month = Number(match[2]);
const day = Number(match[3]);
if (year < 1 || day > daysInMonth(year, month)) return false;
return Number.isFinite(Date.parse(value));
}
function daysInMonth(year: number, month: number): number {
if (month === 2) {
return year % 4 === 0 && (year % 100 !== 0 || year % 400 === 0)
? 29
: 28;
}
return [4, 6, 9, 11].includes(month) ? 30 : 31;
}
+548
View File
@@ -0,0 +1,548 @@
import type { ApiOperation } from "./api-operations.ts";
import type { InstalledBoundaryMapper } from "./boundary-mapper.ts";
import type { RuntimeSchemaCodec } from "./schema-registry.ts";
declare const streamRegistrationIdBrand: unique symbol;
declare const eventTypeIdBrand: unique symbol;
declare const realtimeEndpointIdBrand: unique symbol;
declare const externalEventEffectProfileIdBrand: unique symbol;
declare const killSwitchIdBrand: unique symbol;
export type StreamRegistrationId = string &
Readonly<{ [streamRegistrationIdBrand]: true }>;
export type EventTypeId = string &
Readonly<{ [eventTypeIdBrand]: true }>;
export type RealtimeEndpointId = string &
Readonly<{ [realtimeEndpointIdBrand]: true }>;
export type ExternalEventEffectProfileId = string &
Readonly<{ [externalEventEffectProfileIdBrand]: true }>;
export type KillSwitchId = string &
Readonly<{ [killSwitchIdBrand]: true }>;
export const REALTIME_EVENT_PROTOCOL = "REALTIME_EVENT_V1" as const;
export const REALTIME_HARD_LIMITS = Object.freeze({
maxEventBytes: 64 * 1024,
maxPayloadDepth: 16,
maxPayloadNodes: 4_096,
maxQueueEvents: 256,
maxQueueBytes: 4 * 1024 * 1024,
maxDedupeEntries: 2_048,
maxDedupeBytes: 4 * 1024 * 1024,
dedupeTtlMs: 10 * 60 * 1_000,
});
export type RealtimeLimits = Readonly<{
maxEventBytes: number;
maxPayloadDepth: number;
maxPayloadNodes: number;
maxQueueEvents: number;
maxQueueBytes: number;
maxDedupeEntries: number;
/**
* Additional implementation memory ceiling for semantic conflict
* fingerprints. Reaching it has the same recovery meaning as exhausting the
* count/time dedupe window.
*/
maxDedupeBytes: number;
dedupeTtlMs: number;
}>;
export type RealtimeRecoveryProfile =
| Readonly<{
mode: "CURSOR";
snapshotOperationId: string;
checkpointCodecId: string;
barrier: "REPLAY";
}>
| Readonly<{
mode: "SNAPSHOT_ONLY";
snapshotOperationId: string;
checkpointCodecId: string;
barrier: "CONNECT_BUFFER" | "SERVER_HOLD" | "NONE";
}>
| Readonly<{
mode: "SESSION_REBUILD";
rebuildInputId: string;
}>;
export type RealtimeStreamRegistration = Readonly<{
id: StreamRegistrationId;
protocol: typeof REALTIME_EVENT_PROTOCOL;
owner: string;
scope: "ORIGIN_SHARED" | "ACCOUNT_BOUND" | "SESSION_BOUND";
primaryTransport: "SSE" | "WEBSOCKET" | "NONE";
endpointId: RealtimeEndpointId;
eventTypeIds: readonly EventTypeId[];
delivery: "INVALIDATION_HINT" | "AUTHORITATIVE_DELTA" | "EPHEMERAL";
recovery: RealtimeRecoveryProfile;
fallback: "BOUNDED_POLLING" | "EXPLICITLY_STALE";
hiddenPolicy: "CLOSE" | "BOUNDED_GRACE";
limits: RealtimeLimits;
killSwitchId: KillSwitchId;
}>;
export type RealtimeEventTypeRegistration = Readonly<{
id: EventTypeId;
owner: string;
payloadSchemaId: string;
mapperId: string;
effectProfileId: ExternalEventEffectProfileId;
stateBearing: boolean;
}>;
export type RealtimePolicyRegistryBindings = Readonly<{
schemaCodecs: Readonly<Record<string, RuntimeSchemaCodec>>;
mappers: Readonly<Record<string, InstalledBoundaryMapper>>;
apiOperations: Readonly<Record<string, ApiOperation>>;
endpointIds: readonly RealtimeEndpointId[];
effectProfileIds: readonly ExternalEventEffectProfileId[];
killSwitchIds: readonly KillSwitchId[];
rebuildInputIds?: readonly string[];
}>;
export type RealtimePolicyRegistry = Readonly<{
findStream(id: string): RealtimeStreamRegistration | undefined;
findEventType(id: string): RealtimeEventTypeRegistration | undefined;
findStreamEventType(
streamId: string,
eventTypeId: string,
): RealtimeEventTypeRegistration | undefined;
listStreams(): readonly RealtimeStreamRegistration[];
listEventTypes(): readonly RealtimeEventTypeRegistration[];
}>;
export type RealtimePolicyRegistryInput = Readonly<{
streams: readonly RealtimeStreamRegistration[];
eventTypes: readonly RealtimeEventTypeRegistration[];
bindings: RealtimePolicyRegistryBindings;
}>;
const REGISTRY_ID = /^[A-Z][A-Z0-9_]{2,79}$/u;
const OWNED_ID = /^[A-Za-z][A-Za-z0-9._:-]{0,127}$/u;
const OWNER = /^[a-z0-9][a-z0-9._:-]{0,127}$/u;
const STREAM_KEYS = Object.freeze([
"delivery",
"endpointId",
"eventTypeIds",
"fallback",
"hiddenPolicy",
"id",
"killSwitchId",
"limits",
"owner",
"primaryTransport",
"protocol",
"recovery",
"scope",
] as const);
const EVENT_TYPE_KEYS = Object.freeze([
"effectProfileId",
"id",
"mapperId",
"owner",
"payloadSchemaId",
"stateBearing",
] as const);
const LIMIT_KEYS = Object.freeze([
"dedupeTtlMs",
"maxDedupeBytes",
"maxDedupeEntries",
"maxEventBytes",
"maxPayloadDepth",
"maxPayloadNodes",
"maxQueueBytes",
"maxQueueEvents",
] as const);
export function defineStreamRegistrationId(
value: string,
): StreamRegistrationId {
return defineRegistryId(value, "stream") as StreamRegistrationId;
}
export function defineEventTypeId(value: string): EventTypeId {
return defineRegistryId(value, "event type") as EventTypeId;
}
export function defineRealtimeEndpointId(
value: string,
): RealtimeEndpointId {
return defineRegistryId(value, "endpoint") as RealtimeEndpointId;
}
export function defineExternalEventEffectProfileId(
value: string,
): ExternalEventEffectProfileId {
return defineRegistryId(
value,
"effect profile",
) as ExternalEventEffectProfileId;
}
export function defineRealtimeKillSwitchId(value: string): KillSwitchId {
return defineRegistryId(value, "kill switch") as KillSwitchId;
}
/**
* Builds a composition-time registry and retains no caller-owned registration
* object or array.
*/
export function createRealtimePolicyRegistry(
input: RealtimePolicyRegistryInput,
): RealtimePolicyRegistry {
if (
!input ||
typeof input !== "object" ||
!Array.isArray(input.streams) ||
input.streams.length < 1 ||
input.streams.length > 128 ||
!Array.isArray(input.eventTypes) ||
input.eventTypes.length < 1 ||
input.eventTypes.length > 512
) {
throw new TypeError("Realtime policy registry is invalid.");
}
const endpointIds = identifierSet(
input.bindings.endpointIds,
"endpoint",
);
const effectProfileIds = identifierSet(
input.bindings.effectProfileIds,
"effect profile",
);
const killSwitchIds = identifierSet(
input.bindings.killSwitchIds,
"kill switch",
);
const rebuildInputIds = ownedIdentifierSet(
input.bindings.rebuildInputIds ?? [],
"rebuild input",
);
const eventTypes = new Map<EventTypeId, RealtimeEventTypeRegistration>();
for (const candidate of input.eventTypes) {
const registration = snapshotEventType(
candidate,
input.bindings,
effectProfileIds,
);
if (eventTypes.has(registration.id)) {
throw new TypeError("Realtime event type is duplicated.");
}
eventTypes.set(registration.id, registration);
}
const streams = new Map<StreamRegistrationId, RealtimeStreamRegistration>();
const referencedEventTypes = new Set<EventTypeId>();
for (const candidate of input.streams) {
const registration = snapshotStream(
candidate,
input.bindings,
eventTypes,
endpointIds,
killSwitchIds,
rebuildInputIds,
);
if (streams.has(registration.id)) {
throw new TypeError("Realtime stream is duplicated.");
}
streams.set(registration.id, registration);
for (const eventTypeId of registration.eventTypeIds) {
referencedEventTypes.add(eventTypeId);
}
}
if (
[...eventTypes.keys()].some(
(eventTypeId) => !referencedEventTypes.has(eventTypeId),
)
) {
throw new TypeError("Realtime event type is not owned by a stream.");
}
const streamList = Object.freeze([...streams.values()]);
const eventTypeList = Object.freeze([...eventTypes.values()]);
return Object.freeze({
findStream(id: string) {
return streams.get(id as StreamRegistrationId);
},
findEventType(id: string) {
return eventTypes.get(id as EventTypeId);
},
findStreamEventType(streamId: string, eventTypeId: string) {
const stream = streams.get(streamId as StreamRegistrationId);
if (!stream || !stream.eventTypeIds.includes(eventTypeId as EventTypeId)) {
return undefined;
}
return eventTypes.get(eventTypeId as EventTypeId);
},
listStreams: () => streamList,
listEventTypes: () => eventTypeList,
});
}
function snapshotEventType(
input: RealtimeEventTypeRegistration,
bindings: RealtimePolicyRegistryBindings,
effectProfileIds: ReadonlySet<string>,
): RealtimeEventTypeRegistration {
if (
!hasExactKeys(input, EVENT_TYPE_KEYS) ||
!REGISTRY_ID.test(input.id) ||
!OWNER.test(input.owner) ||
!OWNED_ID.test(input.payloadSchemaId) ||
!OWNED_ID.test(input.mapperId) ||
!REGISTRY_ID.test(input.effectProfileId) ||
typeof input.stateBearing !== "boolean" ||
bindings.schemaCodecs[input.payloadSchemaId]?.schemaId !==
input.payloadSchemaId ||
bindings.mappers[input.mapperId]?.mapperId !== input.mapperId ||
bindings.mappers[input.mapperId]?.inputSchemaId !==
input.payloadSchemaId ||
!effectProfileIds.has(input.effectProfileId)
) {
throw new TypeError("Realtime event type registration is invalid.");
}
return Object.freeze({ ...input });
}
function snapshotStream(
input: RealtimeStreamRegistration,
bindings: RealtimePolicyRegistryBindings,
eventTypes: ReadonlyMap<EventTypeId, RealtimeEventTypeRegistration>,
endpointIds: ReadonlySet<string>,
killSwitchIds: ReadonlySet<string>,
rebuildInputIds: ReadonlySet<string>,
): RealtimeStreamRegistration {
if (
!hasExactKeys(input, STREAM_KEYS) ||
!REGISTRY_ID.test(input.id) ||
input.protocol !== REALTIME_EVENT_PROTOCOL ||
!OWNER.test(input.owner) ||
!["ORIGIN_SHARED", "ACCOUNT_BOUND", "SESSION_BOUND"].includes(
input.scope,
) ||
!["SSE", "WEBSOCKET", "NONE"].includes(input.primaryTransport) ||
!REGISTRY_ID.test(input.endpointId) ||
!endpointIds.has(input.endpointId) ||
!Array.isArray(input.eventTypeIds) ||
input.eventTypeIds.length < 1 ||
input.eventTypeIds.length > 128 ||
new Set(input.eventTypeIds).size !== input.eventTypeIds.length ||
input.eventTypeIds.some(
(eventTypeId) =>
!REGISTRY_ID.test(eventTypeId) || !eventTypes.has(eventTypeId),
) ||
!["INVALIDATION_HINT", "AUTHORITATIVE_DELTA", "EPHEMERAL"].includes(
input.delivery,
) ||
!["BOUNDED_POLLING", "EXPLICITLY_STALE"].includes(input.fallback) ||
!["CLOSE", "BOUNDED_GRACE"].includes(input.hiddenPolicy) ||
!REGISTRY_ID.test(input.killSwitchId) ||
!killSwitchIds.has(input.killSwitchId)
) {
throw new TypeError("Realtime stream registration is invalid.");
}
const limits = snapshotLimits(input.limits);
const recovery = snapshotRecovery(
input.recovery,
bindings,
rebuildInputIds,
);
const selectedEventTypes = input.eventTypeIds.map((eventTypeId) => {
const selected = eventTypes.get(eventTypeId);
if (!selected) {
throw new TypeError("Realtime stream event type is unresolved.");
}
return selected;
});
const hasStateBearingEvent = selectedEventTypes.some(
(eventType) => eventType.stateBearing,
);
const hasNonStateBearingEvent = selectedEventTypes.some(
(eventType) => !eventType.stateBearing,
);
if (
(hasStateBearingEvent && recovery.mode === "SESSION_REBUILD") ||
(hasStateBearingEvent &&
recovery.mode === "SNAPSHOT_ONLY" &&
recovery.barrier === "NONE") ||
(input.delivery === "EPHEMERAL" && hasStateBearingEvent) ||
(input.delivery === "AUTHORITATIVE_DELTA" &&
hasNonStateBearingEvent) ||
(recovery.mode === "SESSION_REBUILD" &&
input.delivery !== "EPHEMERAL") ||
(input.delivery === "EPHEMERAL" &&
input.fallback === "BOUNDED_POLLING")
) {
throw new TypeError("Realtime stream recovery contract is contradictory.");
}
return Object.freeze({
...input,
eventTypeIds: Object.freeze([...input.eventTypeIds]),
recovery,
limits,
});
}
function snapshotRecovery(
input: RealtimeRecoveryProfile,
bindings: RealtimePolicyRegistryBindings,
rebuildInputIds: ReadonlySet<string>,
): RealtimeRecoveryProfile {
if (!input || typeof input !== "object") {
throw new TypeError("Realtime recovery profile is invalid.");
}
if (input.mode === "CURSOR") {
if (
!hasExactKeys(input, [
"barrier",
"checkpointCodecId",
"mode",
"snapshotOperationId",
]) ||
input.barrier !== "REPLAY" ||
!validSnapshotBindings(input, bindings)
) {
throw new TypeError("Realtime cursor recovery profile is invalid.");
}
return Object.freeze({ ...input });
}
if (input.mode === "SNAPSHOT_ONLY") {
if (
!hasExactKeys(input, [
"barrier",
"checkpointCodecId",
"mode",
"snapshotOperationId",
]) ||
!["CONNECT_BUFFER", "SERVER_HOLD", "NONE"].includes(input.barrier) ||
!validSnapshotBindings(input, bindings)
) {
throw new TypeError("Realtime snapshot recovery profile is invalid.");
}
return Object.freeze({ ...input });
}
if (
input.mode !== "SESSION_REBUILD" ||
!hasExactKeys(input, ["mode", "rebuildInputId"]) ||
!OWNED_ID.test(input.rebuildInputId) ||
!rebuildInputIds.has(input.rebuildInputId)
) {
throw new TypeError("Realtime session rebuild profile is invalid.");
}
return Object.freeze({ ...input });
}
function validSnapshotBindings(
input: Readonly<{
snapshotOperationId: string;
checkpointCodecId: string;
}>,
bindings: RealtimePolicyRegistryBindings,
): boolean {
const operation = bindings.apiOperations[input.snapshotOperationId];
return (
OWNED_ID.test(input.snapshotOperationId) &&
OWNED_ID.test(input.checkpointCodecId) &&
bindings.schemaCodecs[input.checkpointCodecId]?.schemaId ===
input.checkpointCodecId &&
operation?.contractVersion === 2 &&
operation.protocol === "REST" &&
operation.semantics === "QUERY" &&
(operation.replayPolicy === "SAFE" ||
operation.replayPolicy === "IDEMPOTENT")
);
}
function snapshotLimits(input: RealtimeLimits): RealtimeLimits {
if (!hasExactKeys(input, LIMIT_KEYS)) {
throw new TypeError("Realtime limits are invalid.");
}
for (const key of LIMIT_KEYS) {
const value = input[key];
if (
!Number.isSafeInteger(value) ||
value < 1 ||
value > REALTIME_HARD_LIMITS[key]
) {
throw new TypeError("Realtime limits exceed implementation ceilings.");
}
}
if (
input.maxQueueBytes < input.maxEventBytes ||
input.maxDedupeBytes < input.maxEventBytes
) {
throw new TypeError("Realtime memory limits cannot hold one event.");
}
return Object.freeze({ ...input });
}
function identifierSet(
values: readonly string[],
label: string,
): ReadonlySet<string> {
if (!Array.isArray(values)) {
throw new TypeError(`Realtime ${label} bindings are invalid.`);
}
const result = new Set<string>();
for (const value of values) {
if (!REGISTRY_ID.test(value) || result.has(value)) {
throw new TypeError(`Realtime ${label} bindings are invalid.`);
}
result.add(value);
}
return result;
}
function ownedIdentifierSet(
values: readonly string[],
label: string,
): ReadonlySet<string> {
if (!Array.isArray(values)) {
throw new TypeError(`Realtime ${label} bindings are invalid.`);
}
const result = new Set<string>();
for (const value of values) {
if (!OWNED_ID.test(value) || result.has(value)) {
throw new TypeError(`Realtime ${label} bindings are invalid.`);
}
result.add(value);
}
return result;
}
function defineRegistryId(value: string, label: string): string {
if (!REGISTRY_ID.test(value)) {
throw new TypeError(`Realtime ${label} ID is invalid.`);
}
return value;
}
function hasExactKeys(
value: unknown,
expected: readonly string[],
): value is Readonly<Record<string, unknown>> {
if (
!value ||
typeof value !== "object" ||
Array.isArray(value) ||
(Object.getPrototypeOf(value) !== Object.prototype &&
Object.getPrototypeOf(value) !== null)
) {
return false;
}
const keys = Object.keys(value).sort();
const selected = [...expected].sort();
return (
keys.length === selected.length &&
keys.every((key, index) => key === selected[index])
);
}
+268
View File
@@ -0,0 +1,268 @@
import { z } from "zod";
import { contractSetSchema } from "./contract-set.ts";
const versionSchema = z.string().regex(/^\d+(?:\.\d+){0,2}$/);
function assertEndpointUrl(
value: string,
local: boolean,
options: Readonly<{ trailingSlashPath?: boolean }> = {},
): void {
const parsed = new URL(value);
if (
(parsed.protocol !== "http:" && parsed.protocol !== "https:") ||
parsed.username ||
parsed.password ||
parsed.hash ||
parsed.search ||
(!local && parsed.protocol !== "https:")
) {
throw new TypeError("invalid");
}
if (options.trailingSlashPath && !parsed.pathname.endsWith("/")) {
throw new TypeError("invalid");
}
}
export function isValidReleaseManifestUrl(value: string): boolean {
if (!value.startsWith("/") || value.startsWith("//")) return false;
if (new TextEncoder().encode(value).byteLength > 256) return false;
if (value.includes("?") || value.includes("#") || value.includes("\\")) {
return false;
}
if (/%2f|%5c/i.test(value)) return false;
return !value
.split("/")
.some((segment) => segment === "." || segment === "..");
}
export const capabilityOverrideArtifactSchema = z
.object({
REALTIME: z.enum(["DEFAULT", "DISABLED"]).default("DEFAULT"),
WEB_WORKER: z.enum(["DEFAULT", "DISABLED"]).default("DEFAULT"),
SERVICE_WORKER: z.enum(["DEFAULT", "DISABLED"]).default("DEFAULT"),
OFFLINE_COMMANDS: z.enum(["DEFAULT", "DISABLED"]).default("DEFAULT"),
})
.strict()
.default({
REALTIME: "DEFAULT",
WEB_WORKER: "DEFAULT",
SERVICE_WORKER: "DEFAULT",
OFFLINE_COMMANDS: "DEFAULT",
});
type RuntimeConfigArtifactDraft = Readonly<{
APP_ENV: "local" | "development" | "staging" | "production";
API_BASE_URL: string;
TELEMETRY_ENABLED: boolean;
TELEMETRY_ENDPOINT?: string;
AUTH_MODE: "external" | "demo";
RELEASE_MANIFEST_URL: string;
}>;
function runtimeConfigArtifactInvariants(
config: RuntimeConfigArtifactDraft,
context: z.RefinementCtx,
): void {
const local = config.APP_ENV === "local" || config.APP_ENV === "development";
if (config.TELEMETRY_ENABLED && !config.TELEMETRY_ENDPOINT) {
context.addIssue({
code: "custom",
path: ["TELEMETRY_ENDPOINT"],
message: "required when telemetry is enabled",
});
}
if (!local && config.AUTH_MODE === "demo") {
context.addIssue({
code: "custom",
path: ["AUTH_MODE"],
message: "demo authentication is limited to local environments",
});
}
try {
assertEndpointUrl(config.API_BASE_URL, local, { trailingSlashPath: true });
} catch {
context.addIssue({
code: "custom",
path: ["API_BASE_URL"],
message:
"absolute credential-free URL ending in / is required; HTTPS outside local",
});
}
if (config.TELEMETRY_ENDPOINT) {
try {
assertEndpointUrl(config.TELEMETRY_ENDPOINT, local);
} catch {
context.addIssue({
code: "custom",
path: ["TELEMETRY_ENDPOINT"],
message:
"absolute credential-free URL is required; HTTPS outside local",
});
}
}
if (!isValidReleaseManifestUrl(config.RELEASE_MANIFEST_URL)) {
context.addIssue({
code: "custom",
path: ["RELEASE_MANIFEST_URL"],
message: "same-origin absolute path without query, hash or traversal",
});
}
}
const runtimeConfigArtifactFields = {
APP_ENV: z.enum(["local", "development", "staging", "production"]),
API_BASE_URL: z.url(),
REQUEST_TIMEOUT_MS: z.int().min(100).max(60_000).default(10_000),
MAX_RETRY_ATTEMPTS: z.int().min(0).max(2).default(2),
TELEMETRY_ENABLED: z.boolean(),
TELEMETRY_ENDPOINT: z.url().optional(),
AUTH_MODE: z.enum(["external", "demo"]),
RELEASE_MANIFEST_URL: z.string().min(1).default("/release-manifest.json"),
RELEASE_ID: z.string().min(1).optional(),
BUILD_ID: z.string().min(1).optional(),
} as const;
export const runtimeConfigV1ArtifactSchema = z
.object({
...runtimeConfigArtifactFields,
CONFIG_SCHEMA_VERSION: z.literal("1"),
API_CONTRACT_VERSION: versionSchema,
})
.strict()
.superRefine(runtimeConfigArtifactInvariants);
export const runtimeConfigV2ArtifactSchema = z
.object({
...runtimeConfigArtifactFields,
CONFIG_SCHEMA_VERSION: z.literal("2.0"),
CAPABILITY_OVERRIDES: capabilityOverrideArtifactSchema,
})
.strict()
.superRefine(runtimeConfigArtifactInvariants);
export const runtimeConfigArtifactSchema = z.discriminatedUnion(
"CONFIG_SCHEMA_VERSION",
[
runtimeConfigV1ArtifactSchema,
runtimeConfigV2ArtifactSchema,
],
);
const releaseManifestArtifactFields = {
appVersion: z.string().min(1),
buildId: z.string().min(1),
commitSha: z.string().min(1),
assetManifestHash: z.string().min(1),
releaseId: z.string().min(1),
builtAt: z.string().min(1),
routeChunks: z.record(z.string().min(1), z.string().min(1)),
} as const;
export const releaseManifestV1ArtifactSchema = z
.object({
schemaVersion: z.literal(1),
...releaseManifestArtifactFields,
configSchemaVersion: versionSchema,
apiContractVersion: versionSchema,
})
.strict();
export const releaseManifestV2ArtifactSchema = z
.object({
schemaVersion: z.literal(2),
...releaseManifestArtifactFields,
configSchemaVersion: z.literal("2.0"),
contractSet: contractSetSchema,
})
.strict();
export const releaseManifestArtifactSchema = z.discriminatedUnion(
"schemaVersion",
[releaseManifestV1ArtifactSchema, releaseManifestV2ArtifactSchema],
);
export const buildManifestArtifactSchema = z
.object({
schemaVersion: z.literal(1),
buildId: z.string().min(1),
commitSha: z.string().min(1),
releaseId: z.string().min(1),
moduleInventoryHash: z.string().min(1),
generatedAt: z.string().min(1),
buildContext: z
.object({
nodeVersion: z.string().min(1),
packageManagerVersion: z.string().min(1),
runnerImage: z.string().min(1),
sourceDateEpoch: z.string().min(1).nullable(),
})
.strict(),
outputs: z
.object({
directory: z.string().min(1),
viteManifest: z.string().min(1),
moduleInventory: z.string().min(1),
routeChunks: z.record(z.string().min(1), z.string().min(1)),
runtimeConfigSchema: z.string().min(1),
})
.strict(),
})
.strict();
export type RuntimeConfigV1Artifact = z.output<
typeof runtimeConfigV1ArtifactSchema
>;
export type RuntimeConfigV2Artifact = z.output<
typeof runtimeConfigV2ArtifactSchema
>;
export type CapabilityOverrideArtifact = z.output<
typeof capabilityOverrideArtifactSchema
>;
export type RuntimeConfigArtifact = z.output<typeof runtimeConfigArtifactSchema>;
export type ReleaseManifestV1Artifact = z.output<
typeof releaseManifestV1ArtifactSchema
>;
export type ReleaseManifestV2Artifact = z.output<
typeof releaseManifestV2ArtifactSchema
>;
export type ReleaseArtifact = z.output<typeof releaseManifestArtifactSchema>;
export type BuildManifestArtifact = z.output<typeof buildManifestArtifactSchema>;
export function parseReleaseArtifact(value: unknown): ReleaseArtifact {
return releaseManifestArtifactSchema.parse(value);
}
export function parseRuntimeConfigArtifact(value: unknown): RuntimeConfigArtifact {
return runtimeConfigArtifactSchema.parse(value);
}
export function parseBuildManifestArtifact(value: unknown): BuildManifestArtifact {
return buildManifestArtifactSchema.parse(value);
}
export function projectReleaseTokens(release: ReleaseArtifact) {
const common = {
schemaVersion: release.schemaVersion,
appVersion: release.appVersion,
buildId: release.buildId,
commitSha: release.commitSha,
configSchemaVersion: release.configSchemaVersion,
assetManifestHash: release.assetManifestHash,
releaseId: release.releaseId,
builtAt: release.builtAt,
} as const;
return release.schemaVersion === 1
? Object.freeze({
...common,
schemaVersion: 1 as const,
apiContractVersion: release.apiContractVersion,
})
: Object.freeze({
...common,
schemaVersion: 2 as const,
contractSetDigest: release.contractSet.setDigest,
});
}
+66
View File
@@ -0,0 +1,66 @@
import { verifyCompatibilityTuple } from "../application/policies/compatibility.ts";
export const RELEASE_TOKEN_REGISTRY = Object.freeze({
appVersion: token("appVersion", "manifest", "human release label"),
buildId: token("buildId", "CI build", "asset and HTML coherence"),
commitSha: token("commitSha", "VCS", "source traceability"),
configSchemaVersion: token(
"configSchemaVersion",
"runtime config schema",
"boot compatibility",
),
apiContractVersion: token(
"apiContractVersion",
"frontend/backend agreement",
"legacy V1 scalar; superseded by contractSetDigest",
),
contractSetDigest: token(
"contractSetDigest",
"compiled external contract package set",
"release coherence for multi-package contracts",
),
assetManifestHash: token(
"assetManifestHash",
"build output",
"chunk integrity and mismatch detection",
),
releaseId: token("releaseId", "deploy system", "rollback target"),
builtAt: token("builtAt", "CI", "diagnostics only; never cache identity"),
});
function token(name: string, source: string, compatibilityRole: string) {
return Object.freeze({ token: name, source, compatibilityRole });
}
/**
* Compare a release manifest and runtime configuration structurally. Version
* fields are delegated to the numeric compatibility policy, never compared
* lexically.
*
*/
export function compareReleaseToRuntime(
release: Readonly<{
buildId: string;
configSchemaVersion: string;
apiContractVersion: string;
assetManifestHash: string;
releaseId: string;
}>,
runtimeConfig: Readonly<{
BUILD_ID: string;
CONFIG_SCHEMA_VERSION: string;
API_CONTRACT_VERSION: string;
RELEASE_ID: string;
}>,
) {
return verifyCompatibilityTuple({
frontend: release,
runtime: {
buildId: runtimeConfig.BUILD_ID,
configSchemaVersion: runtimeConfig.CONFIG_SCHEMA_VERSION,
apiContractVersion: runtimeConfig.API_CONTRACT_VERSION,
assetManifestHash: release.assetManifestHash,
releaseId: runtimeConfig.RELEASE_ID,
},
});
}
+151
View File
@@ -0,0 +1,151 @@
export type FetchCredentialsMode = "omit" | "same-origin" | "include";
export type RestProviderProfile = Readonly<{
providerId: string;
baseUrl: string;
allowedCredentialsModes: readonly FetchCredentialsMode[];
redirect: "error";
referrerPolicy: "no-referrer";
}>;
export type RestAuthProfile = Readonly<{
authProfileId: string;
transport: "ANONYMOUS" | "BEARER_HEADER" | "SAME_ORIGIN_COOKIE";
credentials: FetchCredentialsMode;
allowedCredentialHeaders: readonly ("authorization" | "x-csrf-token")[];
}>;
export type RestCsrfProfile = Readonly<{
csrfProfileId: string;
mode: "NONE" | "HEADER";
headerName: "x-csrf-token" | null;
}>;
export const REST_AUTH_PROFILES = Object.freeze({
REFERENCE_EXTERNAL_BEARER: Object.freeze({
authProfileId: "REFERENCE_EXTERNAL_BEARER",
transport: "BEARER_HEADER",
credentials: "omit",
allowedCredentialHeaders: Object.freeze(["authorization"] as const),
}),
ANONYMOUS: Object.freeze({
authProfileId: "ANONYMOUS",
transport: "ANONYMOUS",
credentials: "omit",
allowedCredentialHeaders: Object.freeze([]),
}),
} satisfies Readonly<Record<string, RestAuthProfile>>);
export const REST_CSRF_PROFILES = Object.freeze({
NO_CSRF_BEARER: Object.freeze({
csrfProfileId: "NO_CSRF_BEARER",
mode: "NONE",
headerName: null,
}),
} satisfies Readonly<Record<string, RestCsrfProfile>>);
export function createRestProviderProfile(
providerId: string,
baseUrl: string,
allowedCredentialsModes: readonly FetchCredentialsMode[] = ["omit"],
): RestProviderProfile {
const parsed = new URL(baseUrl);
const localHttp =
parsed.protocol === "http:" &&
["localhost", "127.0.0.1", "[::1]"].includes(parsed.hostname);
if (
!providerId ||
(parsed.protocol !== "https:" && !localHttp) ||
parsed.username ||
parsed.password ||
parsed.search ||
parsed.hash ||
allowedCredentialsModes.length === 0 ||
new Set(allowedCredentialsModes).size !== allowedCredentialsModes.length
) {
throw new TypeError("Invalid REST provider profile.");
}
return Object.freeze({
providerId,
baseUrl: parsed.href,
allowedCredentialsModes: Object.freeze([...allowedCredentialsModes]),
redirect: "error",
referrerPolicy: "no-referrer",
});
}
export function resolveRestSecurityProfiles(
operation: Readonly<{
method: string;
auth: "none" | "external-session";
authProfileId?: string;
csrfProfileId?: string;
}>,
provider: RestProviderProfile,
authProfiles: Readonly<Record<string, RestAuthProfile>> = REST_AUTH_PROFILES,
csrfProfiles: Readonly<Record<string, RestCsrfProfile>> = REST_CSRF_PROFILES,
): Readonly<{ auth: RestAuthProfile; csrf: RestCsrfProfile }> {
const auth = authProfiles[operation.authProfileId ?? ""];
const csrf = csrfProfiles[operation.csrfProfileId ?? ""];
const unsafe = !["GET", "HEAD", "OPTIONS"].includes(operation.method);
if (
!auth ||
!csrf ||
!provider.allowedCredentialsModes.includes(auth.credentials) ||
(operation.auth === "none" && auth.transport !== "ANONYMOUS") ||
(operation.auth === "external-session" &&
auth.transport === "ANONYMOUS") ||
(auth.transport === "BEARER_HEADER" && csrf.mode !== "NONE") ||
(unsafe &&
auth.transport === "SAME_ORIGIN_COOKIE" &&
csrf.mode !== "HEADER")
) {
throw new TypeError("REST security profiles are incoherent.");
}
return Object.freeze({ auth, csrf });
}
export function validateRestProfileBindings(
operations: Readonly<
Record<
string,
Readonly<{
contractVersion?: number;
operationId: string;
method: string;
auth: "none" | "external-session";
providerId?: string;
authProfileId?: string;
csrfProfileId?: string;
}>
>
>,
providerCredentialModes: Readonly<
Record<string, readonly FetchCredentialsMode[]>
>,
authProfiles: Readonly<Record<string, RestAuthProfile>> = REST_AUTH_PROFILES,
csrfProfiles: Readonly<Record<string, RestCsrfProfile>> = REST_CSRF_PROFILES,
): true {
for (const operation of Object.values(operations)) {
if (operation.contractVersion !== 2) continue;
const allowed = providerCredentialModes[operation.providerId ?? ""];
if (!allowed) {
throw new TypeError(
`Unregistered REST provider binding: ${operation.operationId}`,
);
}
resolveRestSecurityProfiles(
operation,
Object.freeze({
providerId: operation.providerId ?? "",
baseUrl: "https://contract.invalid/",
allowedCredentialsModes: allowed,
redirect: "error",
referrerPolicy: "no-referrer",
}),
authProfiles,
csrfProfiles,
);
}
return true;
}
+50
View File
@@ -0,0 +1,50 @@
export type RouteCodecId = "none" | "NotFoundSplat";
export type RouteRuntimeDefinition = Readonly<{
routeId: string;
moduleId: string;
paramsCodec: RouteCodecId;
searchCodec: RouteCodecId;
}>;
const runtime = <Definition extends RouteRuntimeDefinition>(
value: Definition,
): Readonly<Definition> => Object.freeze(value);
export const PLATFORM_ROUTE_RUNTIME_CONTRACT = Object.freeze({
APP_HOME: runtime({
routeId: "APP_HOME",
moduleId: "home-page",
paramsCodec: "none",
searchCodec: "none",
}),
EXAMPLES_PLATFORM: runtime({
routeId: "EXAMPLES_PLATFORM",
moduleId: "platform-overview-page",
paramsCodec: "none",
searchCodec: "none",
}),
EXAMPLES_UI: runtime({
routeId: "EXAMPLES_UI",
moduleId: "ui-gallery-page",
paramsCodec: "none",
searchCodec: "none",
}),
EXAMPLES_STATES: runtime({
routeId: "EXAMPLES_STATES",
moduleId: "state-gallery-page",
paramsCodec: "none",
searchCodec: "none",
}),
EXAMPLES_AUTH: runtime({
routeId: "EXAMPLES_AUTH",
moduleId: "auth-example-page",
paramsCodec: "none",
searchCodec: "none",
}),
NOT_FOUND: runtime({
routeId: "NOT_FOUND",
moduleId: "not-found-page",
paramsCodec: "NotFoundSplat",
searchCodec: "none",
}),
});
+98
View File
@@ -0,0 +1,98 @@
export type RouteDefinition = Readonly<{
routeId: string;
path: string;
paramsSchema: string | null;
searchSchema: string | null;
access: "public" | "session-required";
loadingSurface: string;
errorSurface: string;
chunkId: string;
title: string;
navigationLabel: string | null;
navigationOrder: number | null;
}>;
const route = <Definition extends RouteDefinition>(
definition: Definition,
): Readonly<Definition> => Object.freeze(definition);
export const PLATFORM_ROUTE_REGISTRY = Object.freeze({
APP_HOME: route({
routeId: "APP_HOME",
path: "/",
paramsSchema: null,
searchSchema: null,
access: "public",
loadingSurface: "app-shell",
errorSurface: "route-boundary",
chunkId: "route-home",
title: "시작",
navigationLabel: "시작",
navigationOrder: 10,
}),
EXAMPLES_PLATFORM: route({
routeId: "EXAMPLES_PLATFORM",
path: "/examples/platform",
paramsSchema: null,
searchSchema: null,
access: "public",
loadingSurface: "example-page",
errorSurface: "route-boundary",
chunkId: "route-examples-platform",
title: "플랫폼 구성",
navigationLabel: "플랫폼 구성",
navigationOrder: 15,
}),
EXAMPLES_UI: route({
routeId: "EXAMPLES_UI",
path: "/examples/ui",
paramsSchema: null,
searchSchema: null,
access: "public",
loadingSurface: "example-page",
errorSurface: "route-boundary",
chunkId: "route-examples-ui",
title: "UI 구성요소",
navigationLabel: "UI 구성요소",
navigationOrder: 20,
}),
EXAMPLES_STATES: route({
routeId: "EXAMPLES_STATES",
path: "/examples/states",
paramsSchema: null,
searchSchema: null,
access: "public",
loadingSurface: "example-page",
errorSurface: "route-boundary",
chunkId: "route-examples-states",
title: "화면 상태",
navigationLabel: "화면 상태",
navigationOrder: 30,
}),
EXAMPLES_AUTH: route({
routeId: "EXAMPLES_AUTH",
path: "/examples/auth",
paramsSchema: null,
searchSchema: null,
access: "public",
loadingSurface: "example-page",
errorSurface: "route-boundary",
chunkId: "route-examples-auth",
title: "인증 연동",
navigationLabel: "인증 연동",
navigationOrder: 40,
}),
NOT_FOUND: route({
routeId: "NOT_FOUND",
path: "*",
paramsSchema: "NotFoundSplat",
searchSchema: null,
access: "public",
loadingSurface: "none",
errorSurface: "not-found",
chunkId: "route-not-found",
title: "페이지를 찾을 수 없음",
navigationLabel: null,
navigationOrder: null,
}),
});
+266
View File
@@ -0,0 +1,266 @@
import type { InstalledOfflineCommandContribution } from "./offline-command.ts";
import type { InstalledServiceWorkerSelection } from "./service-worker.ts";
import type { InstalledWebWorkerContribution } from "./web-worker.ts";
/**
* §3.4–§3.6. Optional runtime capability selection and hosting.
*
* Source contribution decides what is installed. Runtime Config may only carry
* `DEFAULT | DISABLED`, so a configuration document can never switch on a
* capability whose source is absent.
*/
export type RuntimeCapabilityOverride = "DEFAULT" | "DISABLED";
export type RuntimeStopReason =
| "APPLICATION_SHUTDOWN"
| "SCOPE_FENCED"
| "FEATURE_DISABLED"
| "HIDDEN_POLICY"
| "INCIDENT_CONTAINMENT";
export interface RuntimeLifecycle {
start(): void | Promise<void>;
stop(reason: RuntimeStopReason): void | Promise<void>;
dispose(): void | Promise<void>;
}
/** §20.2. `FAILED -> STARTING` is never automatic. */
export type RuntimeLifecycleState =
| "NEW"
| "STARTING"
| "RUNNING"
| "STOPPING"
| "STOPPED"
| "FAILED"
| "DISPOSING"
| "DISPOSED";
/** §22.12. Health is per capability; there is no global `healthy` boolean. */
export type RuntimeHealth =
| "AVAILABLE"
| "DEGRADED"
| "UNAVAILABLE"
| "INCOMPATIBLE"
| "DISABLED";
// §13.2. Realtime product contribution.
export type RealtimeEffectKind =
| "INVALIDATE_TOPICS"
| "APPLY_AUTHORITATIVE_DELTA"
| "EPHEMERAL_NOTIFICATION";
export interface InstalledRealtimeEventEffect {
readonly eventType: string;
readonly mapperId: string;
readonly effect: RealtimeEffectKind;
readonly invalidationTopics: readonly string[];
}
export type InstalledRealtimeTransport =
| Readonly<{ kind: "SSE"; endpointId: string }>
| Readonly<{ kind: "WEBSOCKET"; endpointId: string }>
| Readonly<{ kind: "POLLING"; operationId: string; intervalMs: number }>;
export interface InstalledRealtimeContribution {
readonly contributionId: string;
readonly featureId: string;
readonly contractSourcePackageId: string;
readonly streamId: string;
readonly recoveryMode: "CURSOR" | "SNAPSHOT_ONLY" | "SESSION_REBUILD";
readonly eventEffects: readonly InstalledRealtimeEventEffect[];
readonly transport: InstalledRealtimeTransport;
}
export const REALTIME_CONTRIBUTION_BOUNDS = Object.freeze({
contributions: 64,
streams: 128,
eventTypes: 512,
effectsPerEvent: 8,
invalidationTopicsPerEvent: 32,
minimumPollingIntervalMs: 5_000,
maximumPollingIntervalMs: 300_000,
});
export interface InstalledRuntimeCapabilities {
readonly realtime: readonly InstalledRealtimeContribution[];
readonly webWorkers: readonly InstalledWebWorkerContribution[];
readonly serviceWorker: InstalledServiceWorkerSelection | null;
readonly offlineCommands: InstalledOfflineCommandContribution | null;
}
export type CapabilityOverrideMap = Readonly<{
REALTIME: RuntimeCapabilityOverride;
WEB_WORKER: RuntimeCapabilityOverride;
SERVICE_WORKER: RuntimeCapabilityOverride;
OFFLINE_COMMANDS: RuntimeCapabilityOverride;
}>;
/**
* The effective selection after applying runtime overrides. `serviceWorkerMode`
* keeps the §3.6 persistent-registration exception explicit: a statically
* `ACTIVE` worker that runtime config disables still performs exactly one
* owned-registration lookup and at most one unregister, and deletes no cache.
*/
export type ResolvedRuntimeCapabilities = Readonly<{
realtime: readonly InstalledRealtimeContribution[];
webWorkers: readonly InstalledWebWorkerContribution[];
serviceWorker: InstalledServiceWorkerSelection | null;
serviceWorkerDisabledCleanup: boolean;
offlineCommands: InstalledOfflineCommandContribution | null;
}>;
export function resolveRuntimeCapabilities(
installed: InstalledRuntimeCapabilities,
overrides: CapabilityOverrideMap,
): ResolvedRuntimeCapabilities {
const realtimeDisabled = overrides.REALTIME === "DISABLED";
const workersDisabled = overrides.WEB_WORKER === "DISABLED";
const serviceWorkerDisabled = overrides.SERVICE_WORKER === "DISABLED";
const offlineDisabled = overrides.OFFLINE_COMMANDS === "DISABLED";
return Object.freeze({
realtime: realtimeDisabled ? Object.freeze([]) : installed.realtime,
webWorkers: workersDisabled ? Object.freeze([]) : installed.webWorkers,
serviceWorker: serviceWorkerDisabled ? null : installed.serviceWorker,
serviceWorkerDisabledCleanup:
serviceWorkerDisabled && installed.serviceWorker?.mode === "ACTIVE",
offlineCommands: offlineDisabled ? null : installed.offlineCommands,
});
}
export function validateRealtimeContributions(
contributions: readonly InstalledRealtimeContribution[],
): readonly InstalledRealtimeContribution[] {
const bounds = REALTIME_CONTRIBUTION_BOUNDS;
if (contributions.length > bounds.contributions) {
throw new TypeError("Realtime contributions exceed their bound.");
}
const contributionIds = new Set<string>();
const streamIds = new Set<string>();
let eventTypeCount = 0;
for (const contribution of contributions) {
if (
!contribution.contributionId ||
contributionIds.has(contribution.contributionId)
) {
throw new TypeError("Duplicate realtime contribution identity.");
}
contributionIds.add(contribution.contributionId);
streamIds.add(contribution.streamId);
if (streamIds.size > bounds.streams) {
throw new TypeError("Realtime streams exceed their bound.");
}
const transport = contribution.transport;
if (transport.kind === "POLLING") {
if (
!Number.isSafeInteger(transport.intervalMs) ||
transport.intervalMs < bounds.minimumPollingIntervalMs ||
transport.intervalMs > bounds.maximumPollingIntervalMs
) {
throw new TypeError(
`Realtime polling interval is out of range: ${contribution.contributionId}`,
);
}
}
const seenEvents = new Map<string, number>();
for (const effect of contribution.eventEffects) {
eventTypeCount += 1;
if (eventTypeCount > bounds.eventTypes) {
throw new TypeError("Realtime event types exceed their bound.");
}
const count = (seenEvents.get(effect.eventType) ?? 0) + 1;
if (count > bounds.effectsPerEvent) {
throw new TypeError(
`Realtime effects per event exceeded: ${effect.eventType}`,
);
}
seenEvents.set(effect.eventType, count);
if (
effect.invalidationTopics.length > bounds.invalidationTopicsPerEvent
) {
throw new TypeError(
`Realtime invalidation topics exceeded: ${effect.eventType}`,
);
}
if (
effect.effect === "INVALIDATE_TOPICS" &&
effect.invalidationTopics.length === 0
) {
throw new TypeError(
`INVALIDATE_TOPICS effect declares no topic: ${effect.eventType}`,
);
}
}
}
return Object.freeze([...contributions]);
}
export type RuntimeCapabilityId =
| "REALTIME"
| "WEB_WORKER"
| "SERVICE_WORKER"
| "OFFLINE_COMMANDS";
/**
* §3.5. A bounded, serialisable view of one capability. `selected` is the
* static SSOT count and `active` is what survived the runtime override, so the
* difference between the two is exactly the operator's effect. An override can
* only subtract, which is why a never-selected capability stays at zero.
*/
export type RuntimeCapabilityStatus = Readonly<{
capabilityId: RuntimeCapabilityId;
selected: number;
active: number;
override: RuntimeCapabilityOverride;
}>;
export type RuntimeCapabilitySnapshot = readonly RuntimeCapabilityStatus[];
const CAPABILITY_ORDER = Object.freeze([
"REALTIME",
"WEB_WORKER",
"SERVICE_WORKER",
"OFFLINE_COMMANDS",
] as const);
export function describeRuntimeCapabilities(
installed: InstalledRuntimeCapabilities,
overrides: CapabilityOverrideMap,
): RuntimeCapabilitySnapshot {
const resolved = resolveRuntimeCapabilities(installed, overrides);
const counts: Readonly<
Record<RuntimeCapabilityId, Readonly<{ selected: number; active: number }>>
> = Object.freeze({
REALTIME: Object.freeze({
selected: installed.realtime.length,
active: resolved.realtime.length,
}),
WEB_WORKER: Object.freeze({
selected: installed.webWorkers.length,
active: resolved.webWorkers.length,
}),
SERVICE_WORKER: Object.freeze({
selected: installed.serviceWorker === null ? 0 : 1,
active: resolved.serviceWorker === null ? 0 : 1,
}),
OFFLINE_COMMANDS: Object.freeze({
selected: installed.offlineCommands === null ? 0 : 1,
active: resolved.offlineCommands === null ? 0 : 1,
}),
});
return Object.freeze(
CAPABILITY_ORDER.map((capabilityId) =>
Object.freeze({
capabilityId,
selected: counts[capabilityId].selected,
active: counts[capabilityId].active,
override: overrides[capabilityId],
}),
),
);
}
+111
View File
@@ -0,0 +1,111 @@
export type SchemaDefinition = Readonly<{
schemaId: string;
boundary:
| "route-params"
| "route-search"
| "route-search-api-request"
| "api-request"
| "api-response";
owner: string;
runtime: "zod";
schemaVersion?: number;
direction?: "REQUEST" | "RESPONSE";
unknownFieldPolicy?: "REJECT_UNKNOWN" | "STRIP_UNKNOWN";
}>;
export type RuntimeSchemaResult =
| Readonly<{ success: true; data: unknown }>
| Readonly<{
success: false;
issues: readonly Readonly<{ path: string; code: string }>[];
}>;
export type RuntimeSchemaCodec = Readonly<{
schemaId: string;
parse(value: unknown): RuntimeSchemaResult;
}>;
export function composeRuntimeSchemaCodecs(
contributions: readonly Readonly<Record<string, RuntimeSchemaCodec>>[],
): Readonly<Record<string, RuntimeSchemaCodec>> {
const result: Record<string, RuntimeSchemaCodec> = Object.create(null);
for (const contribution of contributions) {
for (const [registryId, codec] of Object.entries(contribution)) {
if (
registryId !== codec.schemaId ||
Object.hasOwn(result, registryId)
) {
throw new TypeError(
`Invalid or duplicate runtime schema codec: ${registryId}`,
);
}
result[registryId] = codec;
}
}
return Object.freeze(result);
}
export function validateWithRuntimeSchemaRegistry(
schemaId: string,
value: unknown,
registry: Readonly<Record<string, RuntimeSchemaCodec>>,
): RuntimeSchemaResult {
const codec = registry[schemaId];
if (!codec) {
return Object.freeze({
success: false,
issues: Object.freeze([
Object.freeze({ path: "", code: "SCHEMA_NOT_REGISTERED" }),
]),
});
}
try {
return codec.parse(value);
} catch {
return Object.freeze({
success: false,
issues: Object.freeze([
Object.freeze({ path: "", code: "SCHEMA_EXECUTION_FAILED" }),
]),
});
}
}
export function composeSchemaRegistry(
contributions: readonly Readonly<Record<string, SchemaDefinition>>[],
): Readonly<Record<string, SchemaDefinition>> {
const result: Record<string, SchemaDefinition> = Object.create(null);
for (const contribution of contributions) {
for (const [registryId, definition] of Object.entries(contribution)) {
if (
registryId !== definition.schemaId ||
!definition.owner ||
(definition.schemaVersion !== undefined &&
(!Number.isSafeInteger(definition.schemaVersion) ||
definition.schemaVersion < 1)) ||
Object.hasOwn(result, registryId)
) {
throw new TypeError(`Invalid or duplicate schema definition: ${registryId}`);
}
result[registryId] = definition;
}
}
return Object.freeze(result);
}
export const PLATFORM_SCHEMA_REGISTRY: Readonly<
Record<string, SchemaDefinition>
> = Object.freeze({
none: Object.freeze({
schemaId: "none",
boundary: "route-params",
owner: "feature-frontend-routing-release-recovery-runtime",
runtime: "zod",
}),
NotFoundSplat: Object.freeze({
schemaId: "NotFoundSplat",
boundary: "route-params",
owner: "feature-frontend-routing-release-recovery-runtime",
runtime: "zod",
}),
});
+36
View File
@@ -0,0 +1,36 @@
import type { RuntimeIdentityRegistry } from "./query-keys.ts";
export type CacheScopeSnapshot = Readonly<{
generation: number;
fingerprint: string;
identities: RuntimeIdentityRegistry;
/** Aborted synchronously when this generation is fenced or disposed. */
signal: AbortSignal;
isCurrent(): boolean;
}>;
/**
* §10.5. Client scope authority lifecycle.
*
* `FENCED` is published synchronously so no subscriber can render a value that
* belonged to the previous identity. `READY` arrives only after the exact reset
* sequence in §10.6 has completed.
*/
export type ClientScopeLifecycleEvent =
| Readonly<{ kind: "FENCED"; previousGeneration: number }>
| Readonly<{ kind: "READY"; snapshot: CacheScopeSnapshot }>
| Readonly<{ kind: "FAILED"; generation: number }>
| Readonly<{ kind: "DISPOSED" }>;
/** UI reads `FENCED` as the `scope-transition` state, never as stale data. */
export type ClientScopePhase = "READY" | "FENCED" | "FAILED" | "DISPOSED";
export type ServerStateScopeRuntime = Readonly<{
getSnapshot(): CacheScopeSnapshot;
getPhase(): ClientScopePhase;
subscribe(listener: () => void): () => void;
subscribeLifecycle(
listener: (event: ClientScopeLifecycleEvent) => void,
): () => void;
dispose(): void;
}>;
+271
View File
@@ -0,0 +1,271 @@
import type { Result } from "../application/result.ts";
import type { QueryInvalidationTopic } from "./query-invalidation.ts";
import {
createBoundQueryKey,
defineQueryNamespaceIdentity,
type RuntimeIdentityBinding,
} from "./query-keys.ts";
import type { CacheScopeSnapshot } from "./server-state-scope.ts";
import type { MutationIntent } from "./mutation-intent.ts";
/**
* §10.2. The four fixed profiles. A feature selects one by ID; it never
* declares its own numbers. If none of the four can express a requirement, the
* design document and this registry are amended together.
*/
export type ServerStateProfileId =
| "DETAIL_STANDARD"
| "LIST_STANDARD"
| "LOOKUP_STABLE"
| "VOLATILE_STATUS";
export type ServerStateProfile = Readonly<{
profileId: string;
staleTimeMs: number;
gcTimeMs: number;
refetchOnMount: boolean | "always";
refetchOnFocus: boolean;
refetchOnReconnect: boolean;
retryOwner: "TRANSPORT" | "QUERY" | "NONE";
maxResultItems: number;
maxEstimatedResultBytes: number;
}>;
export const SERVER_STATE_PROFILES: Readonly<
Record<ServerStateProfileId, ServerStateProfile>
> = Object.freeze({
DETAIL_STANDARD: Object.freeze({
profileId: "DETAIL_STANDARD",
staleTimeMs: 30_000,
gcTimeMs: 300_000,
refetchOnMount: true,
refetchOnFocus: true,
refetchOnReconnect: true,
retryOwner: "TRANSPORT",
maxResultItems: 1,
maxEstimatedResultBytes: 262_144,
}),
LIST_STANDARD: Object.freeze({
profileId: "LIST_STANDARD",
staleTimeMs: 15_000,
gcTimeMs: 300_000,
refetchOnMount: true,
refetchOnFocus: true,
refetchOnReconnect: true,
retryOwner: "TRANSPORT",
maxResultItems: 200,
maxEstimatedResultBytes: 1_048_576,
}),
LOOKUP_STABLE: Object.freeze({
profileId: "LOOKUP_STABLE",
staleTimeMs: 300_000,
gcTimeMs: 1_800_000,
refetchOnMount: false,
refetchOnFocus: false,
refetchOnReconnect: true,
retryOwner: "TRANSPORT",
maxResultItems: 500,
maxEstimatedResultBytes: 2_097_152,
}),
VOLATILE_STATUS: Object.freeze({
profileId: "VOLATILE_STATUS",
staleTimeMs: 0,
gcTimeMs: 60_000,
refetchOnMount: "always",
refetchOnFocus: true,
refetchOnReconnect: true,
retryOwner: "TRANSPORT",
maxResultItems: 1,
maxEstimatedResultBytes: 65_536,
}),
});
export function getServerStateProfile(
profileId: ServerStateProfileId,
): ServerStateProfile {
const profile = SERVER_STATE_PROFILES[profileId];
if (!profile) {
throw new TypeError(`Unregistered server-state profile: ${profileId}`);
}
return profile;
}
/** §10.3. Feature-owned, mandatory result measurement. */
export type QueryResultMeasure = Readonly<{
itemCount: number;
estimatedBytes: number;
}>;
export type ResultAdmission =
| Readonly<{ ok: true; measure: QueryResultMeasure }>
| Readonly<{
ok: false;
code: "RESULT_MEASUREMENT_FAILED" | "RESULT_BUDGET_EXCEEDED";
}>;
/**
* §10.4. There is no generic fallback: `JSON.stringify` sizing, wire DTO
* re-serialization and recursive walkers are all prohibited, so a definition
* without a working `measureResult` fails closed instead of guessing.
*/
export function admitQueryResult<Value>(
measureResult: (value: Value) => QueryResultMeasure,
value: Value,
profile: ServerStateProfile,
): ResultAdmission {
let measure: QueryResultMeasure;
try {
measure = measureResult(value);
} catch {
return Object.freeze({
ok: false as const,
code: "RESULT_MEASUREMENT_FAILED" as const,
});
}
if (
!measure ||
!Number.isSafeInteger(measure.itemCount) ||
measure.itemCount < 0 ||
!Number.isSafeInteger(measure.estimatedBytes) ||
measure.estimatedBytes < 0
) {
return Object.freeze({
ok: false as const,
code: "RESULT_MEASUREMENT_FAILED" as const,
});
}
if (
measure.itemCount > profile.maxResultItems ||
measure.estimatedBytes > profile.maxEstimatedResultBytes
) {
return Object.freeze({
ok: false as const,
code: "RESULT_BUDGET_EXCEEDED" as const,
});
}
return Object.freeze({ ok: true as const, measure: Object.freeze(measure) });
}
export type BoundQuery<Value> = Readonly<{
definitionId: string;
queryKey: readonly unknown[];
profile: ServerStateProfile;
identity: RuntimeIdentityBinding;
scope: CacheScopeSnapshot;
measureResult(value: Value): QueryResultMeasure;
execute(context: Readonly<{ signal: AbortSignal }>): Promise<Result<Value>>;
}>;
export type QueryDefinition<Input, Value> = Readonly<{
definitionId: string;
definitionVersion: number;
owner: string;
namespace: string;
namespaceVersion: number;
operationId: string;
profileId: ServerStateProfileId;
measureResult(value: Value): QueryResultMeasure;
execute(
input: Input,
context: Readonly<{ signal: AbortSignal }>,
): Promise<Result<Value>>;
}>;
export function bindQuery<Input, Value>(
definition: QueryDefinition<Input, Value>,
input: Input,
scope: CacheScopeSnapshot,
): BoundQuery<Value> {
if (typeof definition.measureResult !== "function") {
throw new TypeError(
`Query definition requires measureResult: ${definition.definitionId}`,
);
}
const namespace = defineQueryNamespaceIdentity(
definition.namespace,
definition.namespaceVersion,
);
const identity = scope.identities.intern(input);
return Object.freeze({
definitionId: definition.definitionId,
// §10.7. Opaque runtime identity only. No raw account/resource ID, URL,
// filter object, document or cursor ever enters a query key.
queryKey: createBoundQueryKey(
namespace,
scope.fingerprint,
definition.definitionVersion,
identity.token,
),
profile: getServerStateProfile(definition.profileId),
identity,
scope,
measureResult: (value: Value) => definition.measureResult(value),
execute: (context) => definition.execute(input, context),
});
}
/**
* §11.2. `REJECT_WHILE_ACTIVE` is the command default. `JOIN_IDENTICAL` is
* only valid when scope, definition, canonical input and user intent all match;
* `ALLOW_PARALLEL` is an explicit per-feature decision.
*/
export type MutationDuplicatePolicy =
| "JOIN_IDENTICAL"
| "REJECT_WHILE_ACTIVE"
| "ALLOW_PARALLEL";
/** §11.5. Ordered optimistic layer bounds. Overflow means pessimistic execution. */
export const OPTIMISTIC_LAYER_BOUNDS = Object.freeze({
maxLayersPerQueryKey: 8,
maxSingleLayerBytes: 262_144,
maxTotalLayerBytesPerQueryKey: 2_097_152,
maxLayerAgeGraceMs: 60_000,
});
/** §11.3. Duplicate coordinator bounds. The map is never used as a result cache. */
export const MUTATION_COORDINATOR_BOUNDS = Object.freeze({
activeDefinitionsPerRuntime: 256,
activeIntentsTotal: 1_024,
canonicalIdentityBytes: 16_384,
waitersPerJoinedIntent: 32,
settledRetentionMs: 0,
});
export type BoundMutation<Input, Value> = Readonly<{
definitionId: string;
definitionVersion: number;
operationId: string;
requiresIdempotencyKey: boolean;
owner: string;
duplicatePolicy: MutationDuplicatePolicy;
scope: CacheScopeSnapshot;
execute(
input: Input,
context: Readonly<{ signal: AbortSignal; intent: MutationIntent }>,
): Promise<Result<Value>>;
invalidate: readonly QueryInvalidationTopic[];
optimistic?: Readonly<{
queryKey: readonly unknown[];
update(previous: unknown, input: Input): unknown;
}>;
}>;
export function defineServerStateProfile(
profile: ServerStateProfile,
): ServerStateProfile {
if (
!profile.profileId ||
!Number.isSafeInteger(profile.staleTimeMs) ||
profile.staleTimeMs < 0 ||
!Number.isSafeInteger(profile.gcTimeMs) ||
profile.gcTimeMs < 1 ||
profile.retryOwner === "QUERY" ||
!Number.isSafeInteger(profile.maxResultItems) ||
profile.maxResultItems < 1 ||
!Number.isSafeInteger(profile.maxEstimatedResultBytes) ||
profile.maxEstimatedResultBytes < 1
) {
throw new TypeError("Invalid server-state profile.");
}
return Object.freeze({ ...profile });
}
+152
View File
@@ -0,0 +1,152 @@
/**
* §17–§18. Service Worker contract.
*
* One physical registration per scope covers PWA lifecycle, verified static
* asset fetch, Web Push and the optional sync wake-up. A separate registration
* for any of those is prohibited.
*/
export const SERVICE_WORKER_PROTOCOL_VERSION = 1 as const;
export const SERVICE_WORKER_CACHE_SCHEMA_VERSION = 1 as const;
export const SERVICE_WORKER_SCRIPT_PATH = "service-worker.js" as const;
export const SERVICE_WORKER_BOUNDS = Object.freeze({
assets: 256,
singleAssetBytes: 2 * 1024 * 1024,
assetSetBytes: 5 * 1024 * 1024,
fetchConcurrency: 4,
installDeadlineMs: 60_000,
clientDrainMs: 30_000,
updateCheckIntervalMs: 6 * 60 * 60 * 1_000,
retainedPreviousCaches: 1,
reloadGuardBytes: 512,
reloadGuardTtlMs: 10 * 60 * 1_000,
});
export type ServiceWorkerHandlerId =
| "WEB_PUSH"
| "PWA_STATIC_ASSETS"
| "OFFLINE_SYNC_WAKEUP";
/**
* §17.3. Removal is staged. `ACTIVE` never transitions directly to `null`:
* a registration that already exists in a browser must first be unregistered,
* then have its owned resources purged, before the source may disappear.
*/
export type InstalledServiceWorkerSelection =
| Readonly<{
mode: "ACTIVE";
scriptPath: typeof SERVICE_WORKER_SCRIPT_PATH;
handlers: readonly ServiceWorkerHandlerId[];
}>
| Readonly<{
mode: "REMOVE_REGISTRATION";
scriptPath: typeof SERVICE_WORKER_SCRIPT_PATH;
}>
| Readonly<{
mode: "PURGE_OWNED_RESOURCES";
scriptPath: typeof SERVICE_WORKER_SCRIPT_PATH;
}>;
/** §17.7. Page and worker read the same compile-time identity tuple. */
export type ServiceWorkerProtocolIdentity = Readonly<{
serviceWorkerProtocolVersion: typeof SERVICE_WORKER_PROTOCOL_VERSION;
cacheSchemaVersion: typeof SERVICE_WORKER_CACHE_SCHEMA_VERSION;
buildId: string;
releaseId: string;
contractSetDigest: string;
staticAssetSetDigest: string;
}>;
export type ServiceWorkerMessageKind =
| "PAGE_HELLO"
| "WORKER_HELLO_ACK"
| "UPDATE_READY"
| "ACTIVATE_REQUEST"
| "ACTIVATE_ACCEPTED"
| "ACTIVATE_REJECTED"
| "CLIENT_DRAIN_REQUEST"
| "CLIENT_DRAINED"
| "ACTIVATED_RELOAD_REQUIRED"
| "CACHE_RESET_REQUEST"
| "CACHE_RESET_RESULT"
| "SYNC_WAKE_OBSERVED";
export type ServiceWorkerMessage = Readonly<{
protocolVersion: typeof SERVICE_WORKER_PROTOCOL_VERSION;
kind: ServiceWorkerMessageKind;
messageId: string;
sourceBuildId: string;
targetBuildId?: string;
nonce?: string;
/** Present only on a successful CACHE_RESET_RESULT. */
cachesDeleted?: number;
}>;
/** §18.3. Compile-time asset manifest; the worker never fetches one. */
export interface StaticAssetManifestV1 {
readonly schemaVersion: 1;
readonly buildId: string;
readonly releaseId: string;
readonly setDigest: `sha256:${string}`;
readonly assets: readonly Readonly<{
url: string;
sha256: `sha256:${string}`;
bytes: number;
contentType: string;
}>[];
}
/** §18.2. `ca-static-v1-<first 16 lower-hex of staticAssetSetDigest>`. */
export const STATIC_CACHE_PREFIX = "ca-static-v1-" as const;
export function staticCacheName(setDigest: string): string {
const hex = setDigest.replace(/^sha256:/, "").slice(0, 16);
if (!/^[0-9a-f]{16}$/.test(hex)) {
throw new TypeError("Static asset set digest is invalid.");
}
return `${STATIC_CACHE_PREFIX}${hex}`;
}
export function isOwnedStaticCacheName(name: string): boolean {
return (
name.startsWith(STATIC_CACHE_PREFIX) &&
/^[0-9a-f]{16}$/.test(name.slice(STATIC_CACHE_PREFIX.length))
);
}
export type ServiceWorkerStartOutcome =
| Readonly<{ kind: "ACTIVE"; buildId: string }>
| Readonly<{ kind: "RELOAD_TO_ENABLE" }>
| Readonly<{ kind: "UPDATE_WAITING" }>
| Readonly<{ kind: "DISABLED" }>
| Readonly<{ kind: "INCOMPATIBLE" }>
| Readonly<{ kind: "FAILED"; code: string }>;
export type ServiceWorkerActivationOutcome =
| Readonly<{ kind: "ACTIVATED_RELOAD_REQUIRED" }>
| Readonly<{ kind: "BLOCKED_DIRTY_CLIENT" }>
| Readonly<{ kind: "CLIENT_DRAIN_TIMEOUT" }>
| Readonly<{ kind: "NO_WAITING_WORKER" }>
| Readonly<{ kind: "PROTOCOL_MISMATCH" }>
| Readonly<{ kind: "FAILED"; code: string }>;
export type ServiceWorkerResetOutcome =
| Readonly<{ kind: "RESET"; cachesDeleted: number }>
| Readonly<{ kind: "NOT_CONTROLLED" }>
| Readonly<{ kind: "PROTOCOL_MISMATCH" }>
| Readonly<{ kind: "FAILED"; code: string }>;
export type ServiceWorkerRemovalOutcome =
| Readonly<{ kind: "ABSENT" }>
| Readonly<{ kind: "UNREGISTERED" }>
| Readonly<{ kind: "PURGED"; cachesDeleted: number; metadataDeleted: number }>
| Readonly<{ kind: "OWNERSHIP_MISMATCH" }>
| Readonly<{ kind: "FAILED"; operation: "LOOKUP" | "UNREGISTER" | "PURGE" }>;
export interface ServiceWorkerRuntimeHost {
start(): Promise<ServiceWorkerStartOutcome>;
requestActivation(): Promise<ServiceWorkerActivationOutcome>;
resetOwnedCaches(): Promise<ServiceWorkerResetOutcome>;
stop(): Promise<void>;
}
+157
View File
@@ -0,0 +1,157 @@
const APP_NAMESPACE = "ca-frontend";
export type StorageBackend =
| "memory"
| "sessionStorage"
| "localStorage"
| "disabled"
| "forbidden";
export type StorageValueCodec =
| "color-scheme-v1"
| "opaque-string-v1"
| "none";
export type StorageKeyInput = Readonly<{
logicalName: string;
scope: string;
name: string;
backend: StorageBackend;
classification:
| "public-preference"
| "opaque-cache"
| "sensitive-forbidden";
schemaVersion: number;
valueCodec: StorageValueCodec;
ttl: number | "session" | null;
migration: "discard";
quotaFallback: "memory" | "no-persist" | "feature-disable";
}>;
export type StorageDefinition = Readonly<
StorageKeyInput & { physicalKey: string }
>;
export const STORAGE_REGISTRY = Object.freeze({
COLOR_SCHEME: defineStorageKey({
logicalName: "COLOR_SCHEME",
scope: "preference",
name: "color-scheme",
backend: "localStorage",
classification: "public-preference",
schemaVersion: 1,
valueCodec: "color-scheme-v1",
ttl: null,
migration: "discard",
quotaFallback: "memory",
}),
CHUNK_RELOAD_GUARD: defineStorageKey({
logicalName: "CHUNK_RELOAD_GUARD",
scope: "release",
name: "chunk-reload-guard",
backend: "sessionStorage",
classification: "opaque-cache",
schemaVersion: 1,
valueCodec: "opaque-string-v1",
ttl: "session",
migration: "discard",
quotaFallback: "no-persist",
}),
QUERY_PERSISTENCE: defineStorageKey({
logicalName: "QUERY_PERSISTENCE",
scope: "cache",
name: "query-persistence",
backend: "disabled",
classification: "sensitive-forbidden",
schemaVersion: 1,
valueCodec: "none",
ttl: null,
migration: "discard",
quotaFallback: "feature-disable",
}),
AUTH_TOKEN: defineStorageKey({
logicalName: "AUTH_TOKEN",
scope: "auth",
name: "auth-token",
backend: "forbidden",
classification: "sensitive-forbidden",
schemaVersion: 1,
valueCodec: "none",
ttl: null,
migration: "discard",
quotaFallback: "feature-disable",
}),
});
export function defineStorageKey<Definition extends StorageKeyInput>(
definition: Definition,
): Readonly<Definition & { physicalKey: string }> {
if (
!["color-scheme-v1", "opaque-string-v1", "none"].includes(
definition.valueCodec,
)
) {
throw new Error("Unknown client storage value codec");
}
if (definition.migration !== "discard") {
throw new Error("Unsupported client storage migration policy");
}
if (definition.classification === "sensitive-forbidden") {
if (!["disabled", "forbidden"].includes(definition.backend)) {
throw new Error("Sensitive client storage registration is forbidden");
}
if (definition.valueCodec !== "none") {
throw new Error("Sensitive client storage codec is forbidden");
}
} else if (definition.valueCodec === "none") {
throw new Error("Persisted storage keys require a value codec");
}
if (!Number.isInteger(definition.schemaVersion) || definition.schemaVersion < 1) {
throw new Error("Storage schemaVersion must be a positive integer");
}
return Object.freeze({
...definition,
physicalKey: buildPhysicalKey(
definition.scope,
definition.schemaVersion,
definition.name,
),
});
}
export function buildPhysicalKey(
scope: string,
schemaVersion: number,
name: string,
): string {
return `${APP_NAMESPACE}:${scope}:v${schemaVersion}:${name}`;
}
export function getStorageDefinition(logicalName: string): StorageDefinition {
const registry: Readonly<Record<string, StorageDefinition>> = STORAGE_REGISTRY;
const definition = registry[logicalName];
if (!definition) throw new Error(`Unregistered storage key: ${logicalName}`);
if (definition.classification === "sensitive-forbidden") {
throw new Error(`Forbidden storage key: ${logicalName}`);
}
return definition;
}
export function isStorageValueAllowed(
definition: StorageDefinition,
value: unknown,
): boolean {
switch (definition.valueCodec) {
case "color-scheme-v1":
return value === "light" || value === "dark" || value === "system";
case "opaque-string-v1":
return (
typeof value === "string" &&
value.length >= 1 &&
value.length <= 2_048
);
default:
return false;
}
}
+237
View File
@@ -0,0 +1,237 @@
export const TELEMETRY_ATTRIBUTE_ALLOWLIST = Object.freeze([
"app_version",
"build_id",
"release_id",
"config_schema_version",
"api_contract_version",
"route_id",
"operation_id",
"error_kind",
"http_status_group",
"attempt_count_bucket",
"duration_bucket",
"component_boundary",
"active_release_id",
"mismatch_kind",
"reason",
"queue_size_bucket",
] as const);
export const TELEMETRY_FORBIDDEN_ATTRIBUTES = Object.freeze([
"access_token",
"refresh_token",
"authorization_header",
"cookie",
"email",
"user_name",
"raw_user_id",
"raw_url",
"query_string",
"request_body",
"response_body",
"storage_value",
"stack_in_user_message",
] as const);
type TelemetryDefinitionFor<Name extends string> = Readonly<{
eventName: Name;
trigger: string;
requiredAttributes: readonly string[];
optionalAttributes: readonly string[];
forbiddenAttributes: readonly string[];
sampling: string;
delivery: "best-effort";
}>;
const event = <Name extends string>(
eventName: Name,
trigger: string,
requiredAttributes: readonly string[],
optionalAttributes: readonly string[] = [],
sampling = "all",
): TelemetryDefinitionFor<Name> =>
Object.freeze({
eventName,
trigger,
requiredAttributes: Object.freeze(requiredAttributes),
optionalAttributes: Object.freeze(optionalAttributes),
forbiddenAttributes: TELEMETRY_FORBIDDEN_ATTRIBUTES,
sampling,
delivery: "best-effort",
});
export const TELEMETRY_REGISTRY = Object.freeze({
"app.boot.failed": event("app.boot.failed", "boot validation failure", [
"error_kind",
"build_id",
"config_schema_version",
]),
"api.request.failed": event("api.request.failed", "terminal API failure", [
"error_kind",
"http_status_group",
"attempt_count_bucket",
"route_id",
], ["operation_id", "duration_bucket"]),
"ui.render.failed": event("ui.render.failed", "React boundary catch", [
"route_id",
"build_id",
"component_boundary",
]),
"release.mismatch.detected": event(
"release.mismatch.detected",
"release tuple mismatch",
["build_id", "active_release_id", "mismatch_kind"],
),
"telemetry.delivery.dropped": event(
"telemetry.delivery.dropped",
"queue or sink failure",
["reason", "queue_size_bucket"],
[],
"internal-counter",
),
});
export type TelemetryEventName = keyof typeof TELEMETRY_REGISTRY;
export type TelemetryDefinition = TelemetryDefinitionFor<TelemetryEventName>;
export type TelemetryEvent = Readonly<{
eventName: TelemetryEventName;
timestamp: string;
attributes: Readonly<Record<string, unknown>>;
}>;
export type TelemetryProjectionResult =
| Readonly<{ success: true; event: TelemetryEvent }>
| Readonly<{ success: false; reason: string }>;
const SAFE_IDENTIFIER = /^[A-Za-z0-9][A-Za-z0-9._:@/-]{0,63}$/;
const ATTRIBUTE_VALUE_POLICIES: Readonly<
Record<string, (value: string) => boolean>
> = Object.freeze({
route_id: (value) => /^[A-Z][A-Z0-9_]{0,63}$/.test(value),
operation_id: (value) => /^[A-Z][A-Z0-9_]{0,63}$/.test(value),
error_kind: (value) => /^[A-Z][A-Z0-9_]{0,63}$/.test(value),
http_status_group: (value) => /^(?:[1-5]xx|none)$/.test(value),
attempt_count_bucket: (value) => /^(?:1|2|3|3-4|5\+)$/.test(value),
duration_bucket: (value) =>
/^(?:lt100ms|100-499ms|500-1999ms|gte2000ms|unknown)$/.test(
value,
),
component_boundary: (value) =>
/^(?:route|feature|boot)$/.test(value),
mismatch_kind: (value) => /^[A-Z][A-Z0-9_]{0,63}$/.test(value),
reason: (value) =>
/^(?:queue-full|sink-failure|invalid-event|invalid-context|serialization-failure)$/.test(
value,
),
queue_size_bucket: (value) =>
/^(?:0|1-10|11-50|51\+)$/.test(value),
});
function validAttributeValue(key: string, value: unknown): value is string {
if (typeof value !== "string") return false;
const policy = ATTRIBUTE_VALUE_POLICIES[key];
return policy ? policy(value) : SAFE_IDENTIFIER.test(value);
}
function isTelemetryEventName(value: string): value is TelemetryEventName {
return Object.hasOwn(TELEMETRY_REGISTRY, value);
}
function includesAttribute(list: readonly string[], key: string): boolean {
return list.includes(key);
}
function projectTelemetryEventUnsafe(
eventName: string,
attributes: Readonly<Record<string, unknown>>,
now: () => number = Date.now,
): TelemetryProjectionResult {
if (!isTelemetryEventName(eventName)) {
return {
success: false,
reason: "unregistered-event",
};
}
const definition = TELEMETRY_REGISTRY[eventName];
const attributeKeys = Object.keys(attributes);
if (
attributeKeys.length >
TELEMETRY_ATTRIBUTE_ALLOWLIST.length +
TELEMETRY_FORBIDDEN_ATTRIBUTES.length
) {
return {
success: false,
reason: "invalid-attribute-value",
};
}
const unknown = attributeKeys.filter(
(key) =>
!includesAttribute(TELEMETRY_ATTRIBUTE_ALLOWLIST, key) &&
!includesAttribute(TELEMETRY_FORBIDDEN_ATTRIBUTES, key),
);
if (unknown.length > 0) {
return {
success: false,
reason: "unknown-attributes",
};
}
const projected = Object.fromEntries(
Object.entries(attributes).filter(
([key, value]) =>
includesAttribute(TELEMETRY_ATTRIBUTE_ALLOWLIST, key) &&
!includesAttribute(TELEMETRY_FORBIDDEN_ATTRIBUTES, key) &&
validAttributeValue(key, value),
),
);
const invalid = Object.entries(attributes).filter(
([key, value]) =>
includesAttribute(TELEMETRY_ATTRIBUTE_ALLOWLIST, key) &&
!validAttributeValue(key, value),
);
if (invalid.length > 0) {
return {
success: false,
reason: "invalid-attribute-value",
};
}
const missing = definition.requiredAttributes.filter(
(key) => projected[key] === undefined,
);
if (missing.length > 0) {
return {
success: false,
reason: "missing-required-attributes",
};
}
let timestamp: string;
try {
timestamp = new Date(now()).toISOString();
} catch {
timestamp = new Date(0).toISOString();
}
return {
success: true,
event: Object.freeze({
eventName,
timestamp,
attributes: Object.freeze(projected),
}),
};
}
export function projectTelemetryEvent(
eventName: string,
attributes: Readonly<Record<string, unknown>>,
now: () => number = Date.now,
): TelemetryProjectionResult {
try {
return projectTelemetryEventUnsafe(eventName, attributes, now);
} catch {
return {
success: false,
reason: "serialization-failure",
};
}
}
+200
View File
@@ -0,0 +1,200 @@
export const WEB_PUSH_LIMITS = Object.freeze({
decodedHintBytes: 3 * 1024,
hintFutureSkewMs: 5 * 60 * 1_000,
hintMaxLifetimeMs: 24 * 60 * 60 * 1_000,
handlerDeadlineMs: 10_000,
fenceOperationDeadlineMs: 2_000,
nativeOperationDeadlineMs: 30_000,
backendOperationDeadlineMs: 15_000,
notificationCleanupCount: 64,
notificationCleanupDeadlineMs: 2_000,
clientHandoffCount: 32,
} as const);
export const WEB_PUSH_PROTOCOLS = Object.freeze({
control: "PUSH_CONTROL_V1",
hint: "WEB_PUSH_HINT_V1",
click: "NOTIFICATION_CLICK_DATA_V1",
registration: "WEB_PUSH_REGISTRATION_V1",
reconciliation: "WEB_PUSH_RECONCILIATION_V1",
revoke: "WEB_PUSH_REVOKE_V1",
clickHandoff: "WEB_PUSH_CLICK_HANDOFF_V1",
reconcileRequired: "WEB_PUSH_RECONCILE_REQUIRED_V1",
} as const);
/**
* Product IDs stay opaque to the mechanism. The selected composition must
* provide a closed registry that resolves these syntactically validated IDs.
*/
export type NotificationTypeId = string;
export type NotificationRouteIntentId = string;
export type PushAuthoritySnapshot = Readonly<{
fenceGeneration: string;
sessionBindingEpoch: string;
releaseEpoch: string;
}>;
export type PushControlAssociationV1 =
| Readonly<{ state: "UNASSOCIATED" }>
| Readonly<{
state: "ACTIVE" | "REVOKED";
associationEpoch: string;
}>;
/**
* One origin-scoped durable authority record shared by the window and worker.
* It intentionally contains no account identifier or native subscription
* material. `updatedAt` is diagnostic metadata, never an ordering authority.
*/
export type PushControlV1 = Readonly<{
protocol: typeof WEB_PUSH_PROTOCOLS.control;
fenceGeneration: string;
sessionBindingEpoch: string;
releaseEpoch: string;
updatedAt: string;
association: PushControlAssociationV1;
}>;
export type WebPushHintV1 = Readonly<{
protocol: typeof WEB_PUSH_PROTOCOLS.hint;
notificationType: NotificationTypeId;
notificationId: string;
associationEpoch: string;
releaseEpoch: string;
issuedAt: string;
expiresAt: string;
routeIntent: NotificationRouteIntentId;
}>;
export type NotificationClickDataV1 = Readonly<{
protocol: typeof WEB_PUSH_PROTOCOLS.click;
notificationId: string;
routeIntent: NotificationRouteIntentId;
associationEpoch: string;
releaseEpoch: string;
expiresAt: string;
}>;
export type WebPushReadinessState =
| "PUSH_READY"
| "PUSH_PERMISSION_REQUIRED"
| "PUSH_DENIED"
| "PUSH_UNSUPPORTED"
| "PUSH_UNAVAILABLE";
export type WebPushUnavailableReason =
| "ABORTED"
| "BACKEND_ASSOCIATION_MISSING"
| "BACKEND_REVOKE_AMBIGUOUS"
| "BUSY"
| "CLOSED"
| "LOCAL_FENCE_UNSAFE"
| "NATIVE_UNSUBSCRIBE_AMBIGUOUS"
| "NATIVE_SUBSCRIPTION_MISSING"
| "PERMISSION_DISMISSED"
| "REGISTRATION_NOT_ACTIVE"
| "REVOKED"
| "SESSION_AUTHORITY_CHANGED"
| "SUBSCRIPTION_KEY_MISMATCH"
| "WORKER_UNAVAILABLE";
export type WebPushReadiness = Readonly<{
state: WebPushReadinessState;
reason?: WebPushUnavailableReason;
}>;
export type WebPushOperation =
| "CONTROL_OPEN"
| "CONTROL_READ"
| "CONTROL_PREPARE"
| "CONTROL_ACTIVATE"
| "CONTROL_REVOKE"
| "CONTROL_PURGE"
| "PERMISSION_REQUEST"
| "SUBSCRIPTION_INSPECT"
| "SUBSCRIPTION_CREATE"
| "SUBSCRIPTION_RECONCILE"
| "SUBSCRIPTION_REVOKE"
| "PUSH_DECODE"
| "PUSH_HANDLE"
| "NOTIFICATION_SHOW"
| "NOTIFICATION_CLICK"
| "NOTIFICATION_CLEANUP";
export type WebPushFailureCode =
| "ABORTED"
| "ASSOCIATION_MISMATCH"
| "BLOCKED"
| "CONTRACT_REJECTED"
| "CONTROL_CORRUPT"
| "DEADLINE_EXCEEDED"
| "DECLARATIVE_PUSH_FORBIDDEN"
| "EXPIRED"
| "INVALID_INPUT"
| "LIMIT_EXCEEDED"
| "NATIVE_FAILURE"
| "PERMISSION_DENIED"
| "PROVIDER_UNAVAILABLE"
| "RELEASE_MISMATCH"
| "STALE_AUTHORITY"
| "STALE_REVISION"
| "TOMBSTONE_CONFLICT"
| "UNSUPPORTED";
export type WebPushFailure = Readonly<{
code: WebPushFailureCode;
operation: WebPushOperation;
retryable: boolean;
}>;
export type WebPushResult<Value> =
| Readonly<{ ok: true; value: Value }>
| Readonly<{ ok: false; error: WebPushFailure }>;
export type WebPushObservationEvent =
| "web_push_permission_finished"
| "web_push_registration_finished"
| "web_push_subscription_rotated"
| "web_push_hint_processed"
| "web_push_notification_finished"
| "web_push_click_dispatched"
| "web_push_association_revoked";
export type WebPushObservation = Readonly<{
event: WebPushObservationEvent;
outcome: "SUCCEEDED" | "FAILED" | "DEGRADED";
reason?: WebPushFailureCode | WebPushUnavailableReason;
}>;
export interface WebPushObserver {
record(observation: WebPushObservation): void;
}
export function webPushSuccess<Value>(
value: Value,
): WebPushResult<Value> {
return Object.freeze({ ok: true, value });
}
export function webPushFailure(
code: WebPushFailureCode,
operation: WebPushOperation,
retryable = false,
): WebPushResult<never> {
return Object.freeze({
ok: false,
error: Object.freeze({ code, operation, retryable }),
});
}
export function samePushAuthority(
left: PushAuthoritySnapshot,
right: PushAuthoritySnapshot,
): boolean {
return (
left.fenceGeneration === right.fenceGeneration &&
left.sessionBindingEpoch === right.sessionBindingEpoch &&
left.releaseEpoch === right.releaseEpoch
);
}
+139
View File
@@ -0,0 +1,139 @@
/**
* §16. Generic CPU Web Worker contract.
*
* The capability is `NOT_SELECTED` by default (§16.2). These types exist so a
* selection can be expressed and validated, but no production worker entry is
* created until a measured, CPU-bound task with an owner is contributed. This
* runtime is separate from the OPFS dedicated worker and the Service Worker.
*/
export const WEB_WORKER_PROTOCOL = "CA_WEB_WORKER_V1" as const;
export const WEB_WORKER_BOUNDS = Object.freeze({
taskGroups: 16,
tasksPerGroup: 32,
activeGroups: 4,
queuedPerGroup: 32,
queuedBytesPerGroup: 16 * 1024 * 1024,
defaultInputBytes: 1024 * 1024,
hardInputBytes: 8 * 1024 * 1024,
defaultOutputBytes: 1024 * 1024,
hardOutputBytes: 8 * 1024 * 1024,
defaultDeadlineMs: 5_000,
hardDeadlineMs: 30_000,
cancelGraceMs: 250,
idleTerminateMs: 60_000,
restartsPerWindow: 3,
restartWindowMs: 300_000,
transferables: 16,
mainThreadChunkBudgetMs: 8,
mainThreadFallbackInputBytes: 1024 * 1024,
});
export interface InstalledWorkerTask {
readonly taskId: string;
readonly taskVersion: 1;
readonly maximumInputBytes: number;
readonly maximumOutputBytes: number;
readonly deadlineMs: number;
}
export interface InstalledWebWorkerContribution {
readonly taskGroupId: string;
readonly tasks: readonly InstalledWorkerTask[];
readonly fallback: "MAIN_THREAD_CHUNKED" | "UNSUPPORTED";
}
export type WorkerRequestMessage = Readonly<{
protocol: typeof WEB_WORKER_PROTOCOL;
kind: "EXECUTE";
taskId: string;
taskVersion: 1;
requestId: string;
workerGeneration: number;
deadlineEpochMs: number;
payload: unknown;
}>;
export type WorkerCancelMessage = Readonly<{
protocol: typeof WEB_WORKER_PROTOCOL;
kind: "CANCEL";
requestId: string;
workerGeneration: number;
}>;
export type WorkerFailureCode =
| "TASK_UNKNOWN"
| "VERSION_UNSUPPORTED"
| "INPUT_INVALID"
| "INPUT_TOO_LARGE"
| "OUTPUT_INVALID"
| "OUTPUT_TOO_LARGE"
| "QUEUE_FULL"
| "DEADLINE_EXCEEDED"
| "CANCELLED"
| "CRASHED"
| "TRANSFER_FAILED"
| "STALE_RESULT"
| "RUNTIME_PROTOCOL_FAILURE";
export type WorkerResponseMessage =
| Readonly<{
protocol: typeof WEB_WORKER_PROTOCOL;
kind: "SUCCESS";
requestId: string;
workerGeneration: number;
payload: unknown;
}>
| Readonly<{
protocol: typeof WEB_WORKER_PROTOCOL;
kind: "FAILURE";
requestId: string;
workerGeneration: number;
code: WorkerFailureCode;
}>;
export function validateWebWorkerContributions(
contributions: readonly InstalledWebWorkerContribution[],
): readonly InstalledWebWorkerContribution[] {
const bounds = WEB_WORKER_BOUNDS;
if (contributions.length > bounds.taskGroups) {
throw new TypeError("Web Worker task groups exceed their bound.");
}
const groupIds = new Set<string>();
const taskIds = new Set<string>();
for (const contribution of contributions) {
if (!contribution.taskGroupId || groupIds.has(contribution.taskGroupId)) {
throw new TypeError("Web Worker task group identity is invalid.");
}
groupIds.add(contribution.taskGroupId);
if (
contribution.tasks.length === 0 ||
contribution.tasks.length > bounds.tasksPerGroup
) {
throw new TypeError("Web Worker task count is out of range.");
}
for (const task of contribution.tasks) {
const qualified = `${contribution.taskGroupId}/${task.taskId}`;
if (!task.taskId || taskIds.has(qualified)) {
throw new TypeError("Duplicate Web Worker task.");
}
taskIds.add(qualified);
if (
task.taskVersion !== 1 ||
!Number.isSafeInteger(task.maximumInputBytes) ||
task.maximumInputBytes < 1 ||
task.maximumInputBytes > bounds.hardInputBytes ||
!Number.isSafeInteger(task.maximumOutputBytes) ||
task.maximumOutputBytes < 1 ||
task.maximumOutputBytes > bounds.hardOutputBytes ||
!Number.isSafeInteger(task.deadlineMs) ||
task.deadlineMs < 1 ||
task.deadlineMs > bounds.hardDeadlineMs
) {
throw new TypeError(`Web Worker task bounds invalid: ${qualified}`);
}
}
}
return Object.freeze([...contributions]);
}