import { createReadOnlyRegistry, type ReadOnlyRegistry, } from "./read-only-registry.ts"; 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>; profiles: Readonly>; schemaCodecs: Readonly>; mappers: Readonly>; requestEncoders: Readonly>; runtimeBindings?: Readonly>; }>; 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([ "NETWORK_UNREACHABLE", "RESOURCE_EXHAUSTED", "UNAVAILABLE", ]); const PROTOCOLS = new Set(["CONNECT_HTTP", "GRPC_WEB"]); const RUNTIME_KINDS = new Set([ "CONNECT_WEB_FETCH", "OFFICIAL_GRPC_WEB_XHR", "CUSTOM_FETCH_FRAMED", ]); const CLIENT_API_KINDS = new Set([ "PROMISE_UNARY", "ASYNC_ITERABLE", "CALLBACK_STREAM", ]); const RPC_KINDS = new Set(["UNARY", "SERVER_STREAM"]); const MESSAGE_ENCODINGS = new Set(["PROTO", "JSON"]); const FRAMINGS = new Set([ "CONNECT_BARE", "CONNECT_ENVELOPE", "GRPC_WEB_BINARY_ENVELOPE", "GRPC_WEB_BASE64_TEXT", ]); const REQUEST_METHODS = new Set(["POST", "GET"]); const SEMANTICS = new Set([ "QUERY", "COMMAND", "SERVER_STREAM", ]); const REPLAY_POLICIES = new Set([ "SAFE", "IDEMPOTENT", "KEYED_COMMAND", "NON_REPLAYABLE", ]); const IDEMPOTENCY_KEY_POLICIES = new Set(["NONE", "REQUIRED"]); const IDEMPOTENCY_LEVELS = new Set([ "NONE", "IDEMPOTENT", "NO_SIDE_EFFECTS", ]); const DATA_CLASSIFICATIONS = new Set([ "PUBLIC", "INTERNAL", "CONFIDENTIAL", ]); const RETRY_OWNERS = new Set([ "FRONTEND_ADAPTER", "EDGE_PROXY", "NONE", ]); const DEADLINE_DIALECTS = new Set([ "CONNECT_TIMEOUT_MS", "GRPC_TIMEOUT", "OFFICIAL_DEADLINE_METADATA", ]); const CANCEL_DIALECTS = new Set([ "ABORT_SIGNAL", "CLIENT_READABLE_STREAM_CANCEL", ]); const RAW_BYTE_CEILING_OWNERS = new Set([ "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 >[], ): Readonly> { return composeRegistry( contributions, (operation) => operation.operationId, defineBrowserRpcOperation, "operation", BROWSER_RPC_HARD_LIMITS.maxOperations, ); } export function composeBrowserRpcProviderProfileRegistry( contributions: readonly Readonly< Record >[], ): Readonly> { return composeRegistry( contributions, (profile) => profile.runtimeProfileId, defineBrowserRpcProviderProfile, "provider profile", BROWSER_RPC_HARD_LIMITS.maxProfiles, ); } export function composeBrowserRpcRequestEncoderRegistry( contributions: readonly Readonly< Record >[], ): Readonly> { return composeRegistry( contributions, (encoder) => encoder.encoderId, defineBrowserRpcRequestEncoder, "request encoder", BROWSER_RPC_HARD_LIMITS.maxOperations, ); } /** * RPC-RR-03. Read facades, never `Map`s. `Object.freeze(new Map(...))` leaves * `set`, `delete` and `clear` working, so an installed registry could still be * emptied or re-pointed after the snapshot was validated. */ export type InstalledBrowserRpcContractBindings = Readonly<{ operations: ReadOnlyRegistry; profiles: ReadOnlyRegistry; schemaCodecs: ReadOnlyRegistry; mappers: ReadOnlyRegistry; requestEncoders: ReadOnlyRegistry; runtimeBindings: ReadOnlyRegistry; }>; /** * R-04. Parse → validate → install. * * `Readonly` is a TypeScript annotation, not a runtime guarantee, and a source * registry can be mutated after validation so replay policy, deadlines, byte * ceilings or transport selection differ from what was checked. Every row is * therefore copied once into a frozen null-prototype snapshot built from exact * own data properties. A getter, an extra or symbol key, a malformed descriptor * or a revoked proxy is a composition-time `TypeError`, and the runtime reads * only the snapshot afterwards. */ function installRegistrySnapshot( source: Readonly>, label: string, allowedKeys: readonly string[], ): ReadOnlyRegistry { let ownKeys: string[]; let symbols: readonly symbol[]; let prototype: object | null; try { // RPC-03. Own *names*, not just enumerable keys: a non-enumerable own entry // is as much a smuggled row as an inherited one, and `Object.keys` never // saw either. ownKeys = Object.getOwnPropertyNames(source); symbols = Object.getOwnPropertySymbols(source); prototype = Reflect.getPrototypeOf(source); } catch { throw new TypeError(`Browser RPC ${label} registry is unreadable.`); } if (symbols.length > 0) { throw new TypeError(`Browser RPC ${label} registry has symbol keys.`); } if (prototype !== Object.prototype && prototype !== null) { throw new TypeError(`Browser RPC ${label} registry has a custom prototype.`); } const installed = new Map(); for (const key of ownKeys) { const descriptor = Object.getOwnPropertyDescriptor(source, key); if (!descriptor || !("value" in descriptor)) { throw new TypeError( `Browser RPC ${label} registry entry is not a data property: ${key}`, ); } installed.set( key, installRowSnapshot(descriptor.value as Value, `${label}.${key}`, allowedKeys), ); } return createReadOnlyRegistry(installed); } function installRowSnapshot( row: Value, label: string, allowedKeys: readonly string[], ): Value { if (!row || typeof row !== "object") { throw new TypeError(`Browser RPC ${label} row is not an object.`); } let ownKeys: string[]; let symbols: readonly symbol[]; let prototype: object | null; try { ownKeys = Object.getOwnPropertyNames(row); symbols = Object.getOwnPropertySymbols(row); prototype = Reflect.getPrototypeOf(row); } catch { throw new TypeError(`Browser RPC ${label} row is unreadable.`); } if (symbols.length > 0) { throw new TypeError(`Browser RPC ${label} row has symbol keys.`); } // RPC-03. A custom prototype carries fields the name sweep never sees and // stays live after installation, so the installed row would not be the row // that was checked. if (prototype !== Object.prototype && prototype !== null) { throw new TypeError(`Browser RPC ${label} row has a custom prototype.`); } const snapshot = Object.create(null) as Record; for (const key of ownKeys) { if (!allowedKeys.includes(key)) { throw new TypeError( `Browser RPC ${label} row has an unexpected key: ${key}`, ); } const descriptor = Object.getOwnPropertyDescriptor(row, key); // Reading an accessor would invoke a getter; refuse without calling it. if (!descriptor || !("value" in descriptor)) { throw new TypeError( `Browser RPC ${label} row key is not a data property: ${key}`, ); } const value = descriptor.value as unknown; snapshot[key] = Array.isArray(value) ? Object.freeze([...value]) : value; } return Object.freeze(snapshot) as Value; } const OPERATION_KEYS = Object.freeze([ "contractVersion", "operationId", "owner", "protocol", "semantics", "replayPolicy", "idempotencyKeyPolicy", "idempotencyLevel", "dataClassification", "runtimeProfileId", "providerId", "fullyQualifiedService", "method", "rpcKind", "requestMessageId", "responseMessageId", "descriptorArtifactId", "descriptorDigest", "requestSchemaId", "responseSchemaId", "requestEncoderId", "mapperId", "authProfileId", "csrfProfileId", "errorProfileId", "deadlineProfileId", "retryProfileId", "serverStateProfileId", "maxRequestMessageBytes", "maxResponseMessageBytes", "maxResponseMessages", "maxTotalResponseBytes", "maxBufferedBytes", "idleDeadlineMs", "totalDeadlineMs", ] as const); const PROFILE_KEYS = Object.freeze([ "runtimeProfileId", "providerId", "fixedBaseUrl", "runtimeId", "runtimeVersion", "runtimeDigest", "protocol", "runtimeKind", "clientApiKind", "rpcKind", "messageEncoding", "framing", "requestMethod", "descriptorArtifactId", "descriptorDigest", "allowedProcedures", "authProfileId", "csrfProfileId", "corsProfileId", "errorProfileId", "deadlineProfileId", "retryProfileId", "retryOwner", "maxAttempts", "backoffMs", "retryableFailures", "maxRetryAfterMs", "deadlineDialect", "cancelDialect", "rawByteCeilingOwner", "streamMessageCompression", ] as const); const SCHEMA_KEYS = Object.freeze(["schemaId", "parse"] as const); const MAPPER_KEYS = Object.freeze([ "mapperId", "mapperVersion", "inputSchemaId", "outputContractId", "owner", "maxOutputItems", "map", ] as const); const ENCODER_KEYS = Object.freeze([ "encoderId", "operationId", "encode", ] as const); const RUNTIME_BINDING_KEYS = Object.freeze([ "runtimeProfileId", "providerId", "protocol", "rpcKind", ] as const); export function installBrowserRpcContractBindings( bindings: BrowserRpcContractBindings, ): InstalledBrowserRpcContractBindings { // Parse first. Snapshotting from own data descriptors rejects accessors // without ever invoking them, so a hostile getter cannot observe validation // or return a different value to it than to the runtime. const operations = installRegistrySnapshot( bindings.operations, "operation", OPERATION_KEYS, ); const profiles = installRegistrySnapshot( bindings.profiles, "profile", PROFILE_KEYS, ); const schemaCodecs = installRegistrySnapshot( bindings.schemaCodecs, "schema", SCHEMA_KEYS, ); const mappers = installRegistrySnapshot( bindings.mappers, "mapper", MAPPER_KEYS, ); const requestEncoders = installRegistrySnapshot( bindings.requestEncoders, "encoder", ENCODER_KEYS, ); const runtimeBindings = installRegistrySnapshot( bindings.runtimeBindings ?? {}, "runtime", RUNTIME_BINDING_KEYS, ); // Then validate the snapshot, so what was checked is exactly what installs. validateBrowserRpcContractBindings({ operations: Object.fromEntries(operations), profiles: Object.fromEntries(profiles), schemaCodecs: Object.fromEntries(schemaCodecs), mappers: Object.fromEntries(mappers), requestEncoders: Object.fromEntries(requestEncoders), runtimeBindings: Object.fromEntries(runtimeBindings), }); return Object.freeze({ operations, profiles, schemaCodecs, mappers, requestEncoders, runtimeBindings, }); } export function validateBrowserRpcContractBindings( bindings: BrowserRpcContractBindings, ): true { 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( contributions: readonly Readonly>[], identity: (value: Value) => string, define: (value: Value) => Value, label: string, maximumRows: number, ): Readonly> { const result: Record = 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; }