feat: 기능 추가 과정중
This commit is contained in:
@@ -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;
|
||||
}
|
||||
Reference in New Issue
Block a user