// Private SDK client implementation. The public facade remains in index.ts. import { ConnectionConfig, ConnectionState, WasmClient, WasmPipeHandle, keyring_generate, } from "mtp/raw"; import * as bindings from "mtp/raw"; import { unixTimeMillis, utf8Encode } from "./utils.js"; import type * as RawBindings from "../raw/index"; import type { MTPCommunicationType } from "../type-map/index"; import { MTPProtocol } from "./schema.js"; import type { MTPMessageType, MTPFrame, MTPNoSchemas, MTPRequestData, MTPResponseFrame, MTPSchemaRegistry, } from "./schema.js"; import type { MTPSessionStorage, MTPSessionState } from "./session"; import { MTPSessionManager } from "./session.js"; import { assertApplicationCommunicationType, assertKnownCommunicationType, base64ToBytes, bytesFrom, bytesToBase64, cloneParsedFrame, cloneParsedValue, codec, crypto, decode, decodeDataValueWithLimits, decodeWithLimits, encode, encodeMTPDataValue, errorMessage, format, inputU64, isBytes, keyringToKeys, normalizeBytes, parseProtectedFrame, protectedFrameBytes, publicKeyBundleToKeys, secretKeyFromString, legacySecretKeyFromStringV1, deriveKeyFromPassphraseSync, validateMTPDataValue, } from "./codec.js"; import type { MTPEncryptedSecretRecord, MTPEncryptedSecretProvider, } from "./encrypted-secret"; import { InMemoryEncryptedSecretProvider } from "./encrypted-secret.js"; import { InMemorySessionStorage } from "./session.js"; import { publicCredentials, zeroCredentials, } from "./credentials.js"; import type { InternalCredentials } from "./credentials.js"; import { withTimeout } from "./timeout.js"; import { initWasmOnce } from "./wasm-init.js"; import { acceptMTPPipeSession, acceptMTPPipeSessionAuto, acceptMTPForwardSecurePipeSession, initiateMTPForwardSecurePipeSession, initiateMTPPipeSession, MTPEncryptedPipeReader, MTPEncryptedPipeWriter, validateApplicationProtectionPurpose, } from "./encrypted-pipe.js"; import { InMemoryReplayGuard, MTPReplayError, effectiveProtectionSignatureSuite, normalizeRecipientBundles, protectedOpeningError, protectionSignatureSuiteValue, resolveDecryptionIdentity, resolveProtectionIdentity, } from "./protection.js"; import type { ResolvedDecryptionIdentity, SignerResolutionOptions, } from "./protection.js"; import { MTPVerifiedRelayMetadata, RELAY_METADATA_TOKEN, relayMetadataState, relayOpeningError, registerRelayMetadata, } from "./relay.js"; import type { MTPRelayMetadataState } from "./relay.js"; import { DEFAULT_SIGNATURE_VERIFICATION_POLICY, MTPSignatureVerificationError, resolveSignatureVerificationPolicy, signatureVerificationPolicyValue, signerKeysUnavailable, } from "./signature-policy.js"; import type { MTPSignatureVerificationPolicy } from "./signature-policy.js"; export type { MTPSignatureVerificationErrorCode, MTPSignatureVerificationPolicy, } from "./signature-policy.js"; export { DEFAULT_SIGNATURE_VERIFICATION_POLICY, MTPSignatureVerificationError, resolveSignatureVerificationPolicy, } from "./signature-policy.js"; export type StorageValue = string | null; export interface MTPCredentialStorage { getItem(key: string): StorageValue | Promise; setItem(key: string, value: string): void | Promise; removeItem(key: string): void | Promise; } export type MTPStorage = MTPCredentialStorage; export type MTPLogEvent = | { hint: "info" | "warning"; type: string; data: unknown; direction?: "send" | "recv"; } | { hint: "error"; type: string | "error"; error: string; data?: unknown; direction?: "send" | "recv"; }; export type ParsedFrame = RawBindings.ParsedFrame; export type Ed25519GenerateResult = ReturnType< typeof bindings.ed25519_generate >; export type WasmEncapsulated = RawBindings.WasmEncapsulated; export interface MTPCrypto { generateKeyring(): Uint8Array; generateEd25519(): Ed25519GenerateResult; keyringFromEd25519(secretKey: Uint8Array, publicKey: Uint8Array): Uint8Array; verifyEd25519( publicKey: Uint8Array, message: Uint8Array, signature: Uint8Array, ): void; deriveEncryptionKey( ikm: Uint8Array, salt: Uint8Array, context: Uint8Array, ): Uint8Array; hkdfExpand( ikm: Uint8Array, salt: Uint8Array, info: Uint8Array, len: number, ): Uint8Array; sha256(data: Uint8Array): Uint8Array; sha256Double(data: Uint8Array): Uint8Array; keyringToKeys(keyring: MTPKeyMaterialInput): MTPKeyringKeys; publicKeyBundleToKeys( publicKeyBundle: MTPKeyMaterialInput, ): MTPPublicKeyBundleKeys; encrypt(key: Uint8Array, input: Uint8Array): Promise; decrypt(key: Uint8Array, input: Uint8Array): Promise; encryptText(key: Uint8Array, plaintext: string): Promise; decryptText(key: Uint8Array, ciphertext: string): Promise; encapsulate(otherPublicKey: Uint8Array): WasmEncapsulated; decapsulate(ownPrivateKey: Uint8Array, ciphertext: Uint8Array): Uint8Array; } export type MTPRawBindings = typeof bindings; export interface MTPRaw { /** * Underlying generated WASM client instance. * * Prefer the `MTPClient` methods for application code. Calling the raw client * bypasses SDK-level validation, credential persistence, logging, timeout * handling, frame parsing helpers, and ping lifecycle management. Use this * escape hatch only when integrating a feature that the SDK wrapper does not * expose yet. */ client: RawBindings.WasmClient; /** * Generated WASM binding module exported by `mtp/raw`. * * These bindings mirror the lower-level WASM API and can change shape as the * generated interface evolves. Prefer the SDK wrapper where possible so your * code keeps the safer, typed MTPClient flow instead of depending directly on * transport internals. */ bindings: MTPRawBindings; } export type MTPBytesInput = Uint8Array | number[]; /** A textual key/blob input whose wire encoding is selected explicitly. */ export interface MTPEncodedBytesInput { value: string; encoding: "hex" | "base64"; } export type MTPKeyMaterialInput = MTPBytesInput | MTPEncodedBytesInput; export interface MTPCodecOptions { id?: number; sender?: bigint | number; receiver?: bigint | number; } export interface MTPCodec { encode( type: MTPCommunicationType, data: Record, options?: MTPCodecOptions, ): Uint8Array; decode(frame: MTPBytesInput): ParsedFrame; format(frame: MTPBytesInput): string; } export interface MTPCredentials { clientId: bigint | string | number | null; keyring: MTPBytesInput; hostPublicKey?: MTPKeyMaterialInput; } export interface MTPClientCredentials { clientId: bigint | null; keyring: Uint8Array; hostPublicKey?: Uint8Array; } export interface MTPKeyringKeys { kemPublicKey: Uint8Array; kemSecretKey: Uint8Array; sigPqPublicKey: Uint8Array; sigPqSecretKey: Uint8Array; sigClPublicKey: Uint8Array; sigClSecretKey: Uint8Array; } export interface MTPPublicKeyBundleKeys { kemPublicKey: Uint8Array; sigPqPublicKey: Uint8Array; sigClPublicKey: Uint8Array; } export interface MTPClientOptions< Registry extends MTPSchemaRegistry = MTPNoSchemas, > { url: string; descriptor?: string; hostPublicKey?: MTPKeyMaterialInput; credentials?: MTPCredentials | string | null; credentialsStorageKey?: string; storage?: MTPCredentialStorage; serverCertificateHashes?: string[]; maxMessageSize?: number; authTimeoutMs?: number; /** Require the hybrid PQ authentication proof when the host supports it. */ requirePq?: boolean; requestTimeoutMs?: number; pings?: boolean | { intervalMs?: number }; wasm?: | RawBindings.InitInput | Promise | { module_or_path: RawBindings.InitInput | Promise; }; logger?: (event: MTPLogEvent) => void; sessionStorage?: MTPSessionStorage; /** * Independent caller-managed encrypted-secret storage. Session state is not * routed through this provider automatically. */ encryptedSecretProvider?: MTPEncryptedSecretProvider; /** Default receiver policy for protected signatures. */ defaultSignatureVerificationPolicy?: MTPSignatureVerificationPolicy; /** Coherent security defaults for protected messages, encrypted pipes, and authentication. */ securityProfile?: MTPSecurityProfile; /** One receive resource policy shared by frame and protected-value opening. */ receiveLimits?: MTPReceiveLimits; /** Application request and response schemas, keyed by communication type. */ schemas?: Registry; /** Reject `request()` when the correlated response is an `Error*` frame. */ throwProtocolErrors?: boolean; /** Receives subscription validation failures. Request failures reject normally. */ onValidationError?: (error: import("./schema.js").MTPValidationError) => void; } export interface MTPSecurityProfile { /** Signature suite used by protected-message and relay senders. */ protectedSignatureSuite?: MTPProtectionSignatureSuite; /** Receiver policy for protected messages and relay metadata/content. */ protectedSignaturePolicy?: MTPSignatureVerificationPolicy; /** Signature suite used by encrypted-pipe senders. */ encryptedPipeSignatureSuite?: MTPProtectionSignatureSuite; /** Receiver policy used by encrypted-pipe acceptors. */ encryptedPipeSignaturePolicy?: MTPSignatureVerificationPolicy; /** Authentication PQ requirement when the host supports the hybrid proof. */ requirePq?: boolean; } export interface MTPEncodeLimits { maxDepth?: number; maxValues?: number; maxOutputSize?: number; } /** Resource limits forwarded to the bounded native/WASM receive decoder. */ export interface MTPReceiveLimits { maxDepth?: number; maxValues?: number; maxBlobSize?: number; maxRecipients?: number; maxAllocatedBytes?: number; /** Maximum reconstructed signed-value encoding size. */ maxOutputSize?: number; maxMessageIdBytes?: number; maxMetadataEncodedBytes?: number; maxSignerKeyHistory?: number; maxDecryptionKeyHistory?: number; } export type Unsubscribe = () => void; export interface MTPFrameIdOptions { id?: number; } export interface MTPAddressedFrameOptions extends MTPFrameIdOptions { sender?: bigint | number; receiver?: bigint | number; } export interface MTPSendOptions extends MTPAddressedFrameOptions {} export interface MTPProtectionIdentity { signerId: bigint | number | string; keyring: MTPKeyMaterialInput; } /** * Key material used to open protected MTP values. * * This identity is independent from transport authentication. Its optional * ID is used only for structural destination checks when a receive operation * supports one. */ export interface MTPDecryptionIdentity { id?: bigint | number | string; keyring: MTPKeyMaterialInput; /** * Previously used recipient keyrings, ordered newest to oldest. The * current keyring is always attempted first. */ keyringHistory?: MTPKeyMaterialInput[]; } /** Decoded value returned by the MTP DataValue codec. */ export type MTPDataValue = RawBindings.ParsedDataValue; /** JavaScript values accepted by the MTP DataValue encoder. */ export type MTPDataValueInput = | null | boolean | number | bigint | string | Uint8Array | MTPDataValueInput[] | { [key: string]: MTPDataValueInput }; /** Public-key material trusted for one protected signer identity. */ export type MTPResolvedSignerKeys = MTPKeyMaterialInput[]; /** * Resolve trusted public-key bundles for a claimed, unverified signer ID. * The ID is used only as a trusted-key lookup key and becomes authenticated * after the native protected codec verifies the signature. */ export type MTPSignerKeyResolver = ( signerId: bigint, ) => MTPResolvedSignerKeys | Promise; export interface MTPRelayPlan { nextHopId: bigint | number | string; finalRecipientId: bigint | number | string; metadataRecipients: MTPKeyMaterialInput[]; contentRecipients: MTPKeyMaterialInput[]; metadata?: MTPDataValueInput; } export interface MTPSendProtectedOptions extends MTPFrameIdOptions { receiverId: bigint | number | string; identity?: MTPProtectionIdentity; recipients: MTPKeyMaterialInput[]; signaturePurpose: number; encryptionPurpose: number; signatureSuite?: MTPProtectionSignatureSuite; exposeSender?: boolean; /** Semantic protected-field limits applied before the WASM boundary. */ limits?: MTPReceiveLimits; } export interface MTPSendSealedRelayOptions extends MTPRelayPlan { identity?: MTPProtectionIdentity; signatureSuite?: MTPProtectionSignatureSuite; /** Semantic protected/relay limits applied before the WASM boundary. */ limits?: MTPReceiveLimits; } export interface MTPRelayVerificationOptions { /** Key material used for protected opening, independent from transport auth. */ recipient?: MTPDecryptionIdentity; /** Require the protected signer to be this MTP identity. */ expectedSignerId?: bigint | number | string; /** Resolve trusted keys for a claimed, unverified signer lookup ID. */ resolveSignerPublicKeys?: MTPSignerKeyResolver; /** Receiver policy applied to relay metadata and content signatures. */ signaturePolicy?: MTPSignatureVerificationPolicy; /** Override the client's receive resource policy for this operation. */ limits?: MTPReceiveLimits; } export interface MTPOpenRelayMetadataOptions extends MTPRelayVerificationOptions { /** Override the default process-local guard with an application-owned guard. */ replayGuard?: MTPReplayGuard; } export interface MTPOpenRelayContentOptions extends MTPRelayVerificationOptions { /** Validate the authenticated final recipient when supplied. */ expectedFinalRecipientId?: bigint | number | string; } export interface MTPOpenProtectedOptions { recipient?: MTPDecryptionIdentity; expectedSignerId?: bigint | number | string; expectedReceiverId?: bigint | number | string; resolveSignerPublicKeys: MTPSignerKeyResolver; signaturePolicy?: MTPSignatureVerificationPolicy; signaturePurpose: number; encryptionPurpose: number; /** Override the default process-local replay guard for durable storage. */ replayGuard?: MTPReplayGuard; /** Override the client's receive resource policy for this operation. */ limits?: MTPReceiveLimits; } export interface MTPVerifiedProtectedMessage { type: string; /** Authenticated direct-message envelope schema version. */ protectedVersion: number; signerId: bigint; /** Authenticated destination from the protected envelope. */ finalRecipientId: bigint; /** Authenticated application message identifier. */ messageId: string; /** Authenticated Unix epoch timestamp in milliseconds. */ createdAt: bigint; receiver?: bigint; outerSender?: bigint; data: T; } export type MTPProtectedFrameInput = ParsedFrame | MTPBytesInput; /** Options shared by the sealed-relay subscription APIs. */ export interface MTPEncryptedSubscriptionOptions extends MTPOpenRelayContentOptions { /** Consume authenticated relay IDs while dispatching the subscription. */ replayGuard?: MTPReplayGuard; } export type MTPProtectionSignatureSuite = "ed25519" | "dual"; export interface MTPReplayGuard { /** * Return true and atomically record the pair when it has not been seen. * `createdAt` is authenticated metadata for retention/observability; the * replay identity is only `(signerId, messageId)`. */ accept( signerId: bigint, messageId: string, createdAt: bigint, ): boolean | Promise; } /** * Bounded process-local duplicate-suppression guard used by high-level direct * protected and relay receives when the caller does not provide durable * storage. Once the fixed cache is full, the oldest entry is evicted and may * be accepted again; use a durable `MTPReplayGuard` for security-sensitive * replay protection that must survive cache eviction, reloads, or multiple * receiver processes. */ export interface MTPVerifiedRelayContent { type: string; data: MTPDataValue; signerId: bigint; finalRecipientId: bigint; messageId: string; /** Unix epoch milliseconds from the authenticated relay metadata. */ createdAt: bigint; metadata?: MTPDataValue; } export interface MTPRequestOptions extends MTPSendOptions { responseType?: MTPCommunicationType; timeoutMs?: number; } export interface MTPPipeWriter { write(data: Uint8Array): Promise; close(): Promise; abort(): void; readonly pipeId: number; } export interface MTPPipeReader { read(): Promise; readonly pipeId: number; readonly description: string; } export interface MTPPipeRequest { pipeId: number; description: string; } export interface MTPOutgoingPipeHandle { readonly pipeId: number; readonly description: string; wait(): Promise; } export interface MTPCreateEncryptedPipeOptions { recipientId: bigint | number | string; recipientPublicKey?: MTPKeyMaterialInput; recipientPublicKeys?: MTPKeyMaterialInput[]; description?: string; purpose?: number; direction?: number; signatureSuite?: MTPProtectionSignatureSuite; } export interface MTPAcceptEncryptedPipeOptions { senderId: bigint | number | string; senderPublicKey?: MTPKeyMaterialInput; senderPublicKeys?: MTPKeyMaterialInput[]; purpose?: number; direction?: number; signaturePolicy?: MTPSignatureVerificationPolicy; } type NormalizedMTPClientOptions = Omit< MTPClientOptions, "hostPublicKey" | "receiveLimits" > & { hostPublicKey?: Uint8Array; receiveLimits?: MTPReceiveLimits; receiveLimitsExplicit: boolean; securityProfile: ResolvedSecurityProfile; }; interface ResolvedSecurityProfile { protectedSignatureSuite: MTPProtectionSignatureSuite; protectedSignaturePolicy: MTPSignatureVerificationPolicy; encryptedPipeSignatureSuite: MTPProtectionSignatureSuite; encryptedPipeSignaturePolicy: MTPSignatureVerificationPolicy; requirePq: boolean; } const DEFAULT_CREDENTIALS_KEY = "mtp:credentials"; const DEFAULT_MAX_MESSAGE_SIZE = 16 * 1024 * 1024; const DEFAULT_MAX_DEPTH = 64; const DEFAULT_MAX_VALUES = 65_536; const DEFAULT_MAX_RECIPIENTS = 64; /** Must match codec::DEFAULT_TRANSPORT_ALLOCATION_FACTOR. */ export const DEFAULT_TRANSPORT_ALLOCATION_FACTOR = 4; function createMessageId(): string { const bytes = new Uint8Array(16); globalThis.crypto.getRandomValues(bytes); return Array.from(bytes, (byte) => byte.toString(16).padStart(2, "0")).join( "", ); } function emit( logger: MTPClientOptions["logger"] | undefined, event: MTPLogEvent, ): void { if (typeof logger === "function") { logger(event); } } function isErrorType(type: string): boolean { return ( type === "Error" || type.startsWith("Error") || [ "BadRequest", "Unauthorized", "Forbidden", "NotFound", "TooManyRequests", "InternalServerError", "BadGateway", "ServiceUnavailable", "GatewayTimeout", ].includes(type) ); } async function storageGet( storage: MTPCredentialStorage | undefined, key: string, ): Promise { return storage ? await storage.getItem(key) : null; } async function storageSet( storage: MTPCredentialStorage | undefined, key: string, value: string, ): Promise { if (storage) { await storage.setItem(key, value); } } async function storageRemove( storage: MTPCredentialStorage | undefined, key: string, ): Promise { if (storage) { await storage.removeItem(key); } } function normalizeCredentials( value: MTPCredentials | string | null, ): MTPCredentials | null { if (!value) { return null; } if (typeof value === "string") { return JSON.parse(value); } return value; } function toBigInt( value: bigint | string | number | null | undefined, ): bigint | null { if (value == null || value === "") { return null; } return inputU64(value, "clientId"); } function generateKeyringBytes() { const checkedGenerator = ( bindings as typeof bindings & { keyring_generate_checked?: () => Uint8Array; } ).keyring_generate_checked; return checkedGenerator?.() ?? keyring_generate(); } function serializeCredentials(credentials) { return JSON.stringify({ clientId: credentials.clientId?.toString() ?? null, keyring: Array.from(credentials.keyring ?? []), hostPublicKey: credentials.hostPublicKey ? Array.from(credentials.hostPublicKey) : undefined, }); } const RECEIVE_LIMIT_KEYS = [ "maxDepth", "maxValues", "maxBlobSize", "maxRecipients", "maxAllocatedBytes", "maxOutputSize", "maxMessageIdBytes", "maxMetadataEncodedBytes", "maxSignerKeyHistory", "maxDecryptionKeyHistory", ] as const; function normalizeReceiveLimits( value: MTPReceiveLimits | undefined, name: string, ): MTPReceiveLimits | undefined { if (value == null) return undefined; if (typeof value !== "object" || Array.isArray(value)) { throw new TypeError(`${name} must be an object`); } const normalized: MTPReceiveLimits = {}; for (const key of RECEIVE_LIMIT_KEYS) { const limit = value[key]; if (limit == null) continue; if (!Number.isSafeInteger(limit) || limit < 0) { throw new TypeError(`${name}.${key} must be a non-negative safe integer`); } normalized[key] = limit; } return normalized; } function effectiveReceiveLimits( configured: MTPReceiveLimits | undefined, maxMessageSize: number, ): MTPReceiveLimits { const transportBlob = Math.min( Math.max(0, maxMessageSize - 4), 0xffff_ffff, ); const transportAllocated = maxMessageSize > Number.MAX_SAFE_INTEGER / DEFAULT_TRANSPORT_ALLOCATION_FACTOR ? Number.MAX_SAFE_INTEGER : maxMessageSize * DEFAULT_TRANSPORT_ALLOCATION_FACTOR; const intersect = (left: number | undefined, right: number): number => Math.min(left ?? right, right); return { ...configured, maxDepth: intersect(configured?.maxDepth, DEFAULT_MAX_DEPTH), maxValues: intersect(configured?.maxValues, DEFAULT_MAX_VALUES), maxBlobSize: intersect(configured?.maxBlobSize, transportBlob), maxRecipients: intersect(configured?.maxRecipients, DEFAULT_MAX_RECIPIENTS), maxAllocatedBytes: intersect( configured?.maxAllocatedBytes, transportAllocated, ), maxOutputSize: intersect(configured?.maxOutputSize, maxMessageSize), }; } function resolveSecurityProfile(options: MTPClientOptions): ResolvedSecurityProfile { const profile = options.securityProfile ?? {}; const suite = (value: unknown, name: string): MTPProtectionSignatureSuite => { if (value == null) return "ed25519"; if (value !== "ed25519" && value !== "dual") { throw new TypeError(`${name} must be 'ed25519' or 'dual'`); } return value; }; const policy = ( value: MTPSignatureVerificationPolicy | undefined, name: string, ): MTPSignatureVerificationPolicy => { try { return resolveSignatureVerificationPolicy(value, undefined); } catch (error) { throw new TypeError(`${name} is invalid`, { cause: error }); } }; if (profile.requirePq != null && typeof profile.requirePq !== "boolean") { throw new TypeError("securityProfile.requirePq must be a boolean"); } return { protectedSignatureSuite: suite( profile.protectedSignatureSuite, "securityProfile.protectedSignatureSuite", ), protectedSignaturePolicy: policy( profile.protectedSignaturePolicy, "securityProfile.protectedSignaturePolicy", ), encryptedPipeSignatureSuite: suite( profile.encryptedPipeSignatureSuite, "securityProfile.encryptedPipeSignatureSuite", ), encryptedPipeSignaturePolicy: policy( profile.encryptedPipeSignaturePolicy, "securityProfile.encryptedPipeSignaturePolicy", ), requirePq: profile.requirePq ?? true, }; } type BoundedBindings = typeof bindings & { protected_claimed_signer_id_with_limits?: ( frame: Uint8Array, keyrings: Uint8Array[], encryptionPurpose: number, limits: MTPReceiveLimits, ) => bigint; open_protected_with_keyrings_with_limits_without_replay?: ( frame: Uint8Array, keyrings: Uint8Array[], expectedSignerId: bigint, signerPublicKeys: Uint8Array[], expectedReceiverId: bigint | null, signaturePurpose: number, encryptionPurpose: number, signatureSuite: number, limits: MTPReceiveLimits, ) => RawBindings.WasmVerifiedProtectedMessage; relay_metadata_claimed_signer_id_with_limits?: ( frame: Uint8Array, keyrings: Uint8Array[], limits: MTPReceiveLimits, ) => bigint; open_relay_metadata_with_keyrings_with_limits_without_replay?: ( frame: Uint8Array, keyrings: Uint8Array[], expectedSignerId: bigint, signerPublicKeys: Uint8Array[], signatureSuite: number, limits: MTPReceiveLimits, ) => RawBindings.WasmVerifiedRelayMetadata; open_relay_content_with_keyrings_with_limits_without_replay?: ( metadata: RawBindings.WasmVerifiedRelayMetadata, keyrings: Uint8Array[], signerPublicKeys: Uint8Array[], expectedFinalRecipientId: bigint | null, signatureSuite: number, limits: MTPReceiveLimits, ) => RawBindings.WasmVerifiedRelayContent; }; function boundedBindings(): BoundedBindings { return bindings as unknown as BoundedBindings; } function deserializeCredentials(credentials) { const normalized = normalizeCredentials(credentials); if (!normalized) { return null; } const keyring = normalized.keyring; if (!isBytes(keyring)) { throw new TypeError("credentials.keyring must be a Uint8Array or number[]"); } return { clientId: toBigInt(normalized.clientId), keyring: bytesFrom(keyring, "credentials.keyring").slice(), hostPublicKey: normalized.hostPublicKey == null ? undefined : normalizeBytes(normalized.hostPublicKey, "credentials.hostPublicKey").slice(), }; } function validateOptions(options) { if (!options || typeof options !== "object") { throw new TypeError("MTPClient.create requires an options object"); } if (typeof options.url !== "string" || !options.url.trim()) { throw new TypeError("MTPClient.create requires a non-empty url"); } if (options.descriptor != null && typeof options.descriptor !== "string") { throw new TypeError("descriptor must be a string"); } resolveSignatureVerificationPolicy( undefined, options.defaultSignatureVerificationPolicy, ); if ( options.securityProfile != null && (typeof options.securityProfile !== "object" || Array.isArray(options.securityProfile)) ) { throw new TypeError("securityProfile must be an object"); } resolveSecurityProfile(options); normalizeReceiveLimits(options.receiveLimits, "receiveLimits"); if (options.storage) { for (const method of ["getItem", "setItem", "removeItem"]) { if (typeof options.storage[method] !== "function") { throw new TypeError(`storage.${method} must be a function`); } } } if ( options.maxMessageSize != null && (!Number.isSafeInteger(options.maxMessageSize) || options.maxMessageSize <= 0) ) { throw new TypeError("maxMessageSize must be a positive safe integer"); } if ( options.authTimeoutMs != null && (!Number.isSafeInteger(options.authTimeoutMs) || options.authTimeoutMs <= 0) ) { throw new TypeError("authTimeoutMs must be a positive safe integer"); } if (options.requirePq != null && typeof options.requirePq !== "boolean") { throw new TypeError("requirePq must be a boolean"); } if ( options.requestTimeoutMs != null && (!Number.isSafeInteger(options.requestTimeoutMs) || options.requestTimeoutMs <= 0) ) { throw new TypeError("requestTimeoutMs must be a positive safe integer"); } if (options.schemas != null) { if (typeof options.schemas !== "object" || Array.isArray(options.schemas)) { throw new TypeError("schemas must be an object"); } for (const [type, pair] of Object.entries(options.schemas)) { if ( !pair || typeof pair !== "object" || typeof (pair as { request?: { parseAsync?: unknown } }).request ?.parseAsync !== "function" || typeof (pair as { response?: { parseAsync?: unknown } }).response ?.parseAsync !== "function" ) { throw new TypeError( `schemas.${type} must contain request and response schemas with parseAsync()`, ); } } } } export class MTPClient { static readonly crypto = crypto; static readonly codec = codec; #credentials: InternalCredentials | null; #options: NormalizedMTPClientOptions; readonly #protocol: MTPProtocol | undefined; readonly #protectedReplayGuard = new InMemoryReplayGuard(); readonly #relayReplayGuard = new InMemoryReplayGuard(); readonly raw: MTPRaw; readonly crypto = MTPClient.crypto; readonly codec = MTPClient.codec; readonly sessionManager: MTPSessionManager; /** Independent encrypted-secret storage selected by the caller. */ readonly encryptedSecretProvider: MTPEncryptedSecretProvider; private constructor( options: NormalizedMTPClientOptions, client: RawBindings.WasmClient, ) { this.#options = options; this.#protocol = options.schemas ? new MTPProtocol({ schemas: options.schemas, throwProtocolErrors: options.throwProtocolErrors, onValidationError: options.onValidationError, }) : undefined; this.#credentials = deserializeCredentials(options.credentials); this.raw = { client, bindings }; this.encryptedSecretProvider = options.encryptedSecretProvider ?? new InMemoryEncryptedSecretProvider(); this.sessionManager = new MTPSessionManager( options.sessionStorage ?? new InMemorySessionStorage(), ); } static async create< const Registry extends MTPSchemaRegistry = MTPNoSchemas, >( options: MTPClientOptions, ): Promise> { validateOptions(options); await MTPClient.init(options.wasm); const normalizedOptions = { ...options, hostPublicKey: options.hostPublicKey == null ? undefined : normalizeBytes(options.hostPublicKey, "hostPublicKey").slice(), receiveLimits: effectiveReceiveLimits( normalizeReceiveLimits(options.receiveLimits, "receiveLimits"), options.maxMessageSize ?? DEFAULT_MAX_MESSAGE_SIZE, ), // Always retain and apply the effective transport policy. Even when the // caller did not provide overrides, protected/relay opening must not // fall back to a larger codec default after frame admission. receiveLimitsExplicit: true, securityProfile: resolveSecurityProfile(options), }; let sdk: MTPClient | undefined; const client = new WasmClient( (state) => emit(normalizedOptions.logger, { hint: "info", type: "state", data: ConnectionState[state] ?? state, }), (frame) => { if (sdk) { sdk.#handleFrame(frame); } }, (error) => emit(normalizedOptions.logger, { hint: "error", type: "Error", error: String(error), }), ); if (normalizedOptions.receiveLimits) { const rawClient = client as unknown as { set_receive_limits?: (limits: MTPReceiveLimits) => void; setReceiveLimits?: (limits: MTPReceiveLimits) => void; }; const setReceiveLimits = rawClient.set_receive_limits ?? rawClient.setReceiveLimits; if (!setReceiveLimits) { throw new Error( "effective receive limits require a rebuilt bounded WASM package", ); } setReceiveLimits.call(rawClient, normalizedOptions.receiveLimits); } sdk = new MTPClient(normalizedOptions, client); await sdk.#loadStoredCredentials(); if (!sdk.#credentials) { sdk.#credentials = { clientId: null, keyring: generateKeyringBytes(), hostPublicKey: normalizedOptions.hostPublicKey, }; } else if ( !sdk.#credentials.hostPublicKey && normalizedOptions.hostPublicKey ) { sdk.#credentials = { ...sdk.#credentials, hostPublicKey: normalizedOptions.hostPublicKey, }; } else if ( !normalizedOptions.hostPublicKey && sdk.#credentials.hostPublicKey ) { sdk.#options = { ...sdk.#options, hostPublicKey: sdk.#credentials.hostPublicKey, }; } return sdk; } static isSupported(): boolean { return WasmClient.is_supported(); } static async init( wasm?: MTPClientOptions["wasm"], ): Promise>> { return await initWasmOnce(wasm); } get credentials(): MTPClientCredentials | null { return publicCredentials(this.#credentials); } get defaultSignatureVerificationPolicy(): MTPSignatureVerificationPolicy { return resolveSignatureVerificationPolicy( undefined, this.#options.defaultSignatureVerificationPolicy ?? this.#options.securityProfile.protectedSignaturePolicy, ); } get state(): RawBindings.ConnectionState { return this.raw.client.state; } get pingMs(): number | null { return this.raw.client.ping_ms ?? null; } async #loadStoredCredentials() { if (this.#credentials || !this.#options.storage) { return; } const stored = await storageGet( this.#options.storage, this.#options.credentialsStorageKey ?? DEFAULT_CREDENTIALS_KEY, ); this.#credentials = deserializeCredentials(stored); } #connectionConfig() { const config = new ConnectionConfig(this.#options.url); if (this.#options.serverCertificateHashes) { config.server_certificate_hashes = this.#options.serverCertificateHashes; } if (this.#options.maxMessageSize != null) { config.max_message_size = this.#options.maxMessageSize; } config.require_pq = this.#options.requirePq ?? this.#options.securityProfile.requirePq; if (this.#options.descriptor != null) { config.description = this.#options.descriptor; } return config; } async connect(): Promise { if (this.#credentials?.clientId != null && this.#options.hostPublicKey) { await this.auth(); return; } await this.connectUnauthenticated(); } async connectUnauthenticated(): Promise { const config = this.#connectionConfig(); try { // The deprecated raw method clones its borrowed config synchronously and // remains safe when the SDK timeout wins the race. Keep using it here so // applications that instrument the historical raw API continue to work. await withTimeout( this.raw.client.connect(config), this.#options.authTimeoutMs, "connection timed out", () => this.raw.client.disconnect(), ); const clientId = ( this.raw.client as RawBindings.WasmClient & { readonly client_id: bigint } ).client_id; this.#startPings(clientId); } finally { config.free(); } } async auth(): Promise { if (!this.#options.hostPublicKey) { throw new Error("MTPClient.auth requires hostPublicKey"); } return this.#credentials?.clientId == null ? await this.register() : await this.#connectAuthenticated(); } async #connectAuthenticated() { if (!this.#options.hostPublicKey) { throw new Error( "MTPClient.connect requires hostPublicKey for authenticated connections", ); } if ( !this.#credentials?.keyring?.length || this.#credentials.clientId == null ) { throw new Error( "MTPClient.connect requires credentials with clientId and keyring", ); } const config = this.#connectionConfig(); try { // Keep the deprecated raw spelling as the compatibility path. The WASM // wrapper takes owned copies before entering its asynchronous handshake. const clientId = await withTimeout( this.raw.client.auth_connect( config, this.#options.hostPublicKey, this.#credentials.keyring, this.#credentials.clientId, ), this.#options.authTimeoutMs, "authentication timed out", () => this.raw.client.disconnect(), ); this.#credentials = { ...this.#credentials, clientId }; await this.#persistCredentials(); this.#startPings(clientId); return clientId; } finally { config.free(); } } async register(): Promise { if (!this.#options.hostPublicKey) { throw new Error("MTPClient.register requires hostPublicKey"); } if (!this.#credentials?.keyring?.length) { this.#credentials = { clientId: null, keyring: generateKeyringBytes(), hostPublicKey: this.#options.hostPublicKey, }; } const config = this.#connectionConfig(); try { const clientId = await withTimeout( this.raw.client.auth_register( config, this.#options.hostPublicKey, this.#credentials.keyring, ), this.#options.authTimeoutMs, "authentication timed out", () => this.raw.client.disconnect(), ); this.#credentials = { ...this.#credentials, clientId }; await this.#persistCredentials(); this.#startPings(clientId); return clientId; } finally { config.free(); } } async #persistCredentials() { await storageSet( this.#options.storage, this.#options.credentialsStorageKey ?? DEFAULT_CREDENTIALS_KEY, serializeCredentials(this.#credentials), ); } async clearCredentials(): Promise { zeroCredentials(this.#credentials); this.#credentials = null; await storageRemove( this.#options.storage, this.#options.credentialsStorageKey ?? DEFAULT_CREDENTIALS_KEY, ); } #startPings(clientId) { const pings = this.#options.pings; if (!pings) { this.raw.client.stop_protocol_pings(); return; } const intervalMs = typeof pings === "object" ? (pings.intervalMs ?? 30_000) : 30_000; this.raw.client.start_protocol_pings(intervalMs, clientId); } #encodeLimits(): MTPEncodeLimits { return { maxDepth: DEFAULT_MAX_DEPTH, maxValues: DEFAULT_MAX_VALUES, maxOutputSize: this.#options.maxMessageSize ?? DEFAULT_MAX_MESSAGE_SIZE, }; } async #parseRequestData( type: MTPCommunicationType, data: unknown, ): Promise> { if (!this.#protocol || !this.#protocol.schemas[type]) { return (data ?? {}) as Record; } const parsed = await this.#protocol.parseRequest( type as MTPMessageType, data as never, ); return (parsed ?? {}) as Record; } async #parseResponseData( requestedType: MTPCommunicationType, frame: ParsedFrame, phase: "response" | "subscription" = "response", ): Promise> { if (!this.#protocol || !this.#protocol.schemas[requestedType]) { return frame; } return await this.#protocol.parseResponse( requestedType as MTPMessageType, frame, phase, ); } #buildFrame(typeOrFrame, data, options) { if (typeOrFrame instanceof Uint8Array) { if ( typeOrFrame.length > (this.#options.maxMessageSize ?? DEFAULT_MAX_MESSAGE_SIZE) ) { throw new RangeError("MTP frame exceeds maxMessageSize"); } return typeOrFrame; } if (typeof typeOrFrame !== "string" || !typeOrFrame) { throw new TypeError( "message type must be a non-empty string or Uint8Array frame", ); } if (data == null || typeof data !== "object" || Array.isArray(data)) { throw new TypeError("message data must be an object"); } const limits = this.#encodeLimits(); validateMTPDataValue(data as MTPDataValueInput, limits); const bounded = ( this.raw.bindings as typeof bindings & { build_frame_with_limits?: ( type: string, data: Record, options: MTPCodecOptions, limits: MTPEncodeLimits, ) => Uint8Array; } ).build_frame_with_limits; if (!bounded) { throw new Error("bounded WASM frame encoding is unavailable; rebuild mtp-wasm"); } const frame = bounded(typeOrFrame, data, options ?? {}, limits); if (frame.length > (limits.maxOutputSize ?? DEFAULT_MAX_MESSAGE_SIZE)) { throw new RangeError("MTP frame exceeds maxMessageSize"); } return frame; } async send(message: Uint8Array): Promise; async send>( type: Type, data?: MTPRequestData, options?: MTPSendOptions, ): Promise; async send( type: MTPCommunicationType, data: Record, options?: MTPSendOptions, ): Promise; async send( typeOrFrame: Uint8Array | MTPCommunicationType, data?: unknown, options?: MTPSendOptions, ): Promise { const parsedData = typeof typeOrFrame === "string" ? await this.#parseRequestData(typeOrFrame, data) : data; const message = this.#buildFrame(typeOrFrame, parsedData, options); try { const frame = this.raw.bindings.parse_frame(message); emit( this.#options.logger, isErrorType(frame.type) ? { hint: "error", type: frame.type, error: errorMessage(frame), data: frame.data, direction: "send", } : { hint: "info", type: frame.type, data: frame.data, direction: "send", }, ); } catch (error) { emit(this.#options.logger, { hint: "error", type: "Error", error: String(error), direction: "send", }); } await this.raw.client.send(message); } /** Re-route an opaque sealed relay payload without opening it. */ forwardRelayFrame( frame: Uint8Array | ParsedFrame, nextHopId: bigint | number | string, ): Uint8Array { const raw = frame instanceof Uint8Array ? frame : frame.raw; const nextHop = inputU64(nextHopId, "nextHopId"); return bindings.forward_encrypted_relay_frame(raw, nextHop); } /** Forward an opaque sealed relay frame through this client connection. */ async forwardRelay( frame: Uint8Array | ParsedFrame, nextHopId: bigint | number | string, ): Promise { await this.send(this.forwardRelayFrame(frame, nextHopId)); } async request( message: Uint8Array, data?: never, options?: MTPRequestOptions, ): Promise; async request>( type: Type, data?: MTPRequestData, options?: MTPRequestOptions, ): Promise>; async request( type: MTPCommunicationType, data: Record, options?: MTPRequestOptions, ): Promise; async request( typeOrFrame: Uint8Array | MTPCommunicationType, data?: unknown, options: MTPRequestOptions = {}, ): Promise> { const timeoutMs = options.timeoutMs ?? this.#options.requestTimeoutMs ?? 30_000; if (!Number.isSafeInteger(timeoutMs) || timeoutMs <= 0) { throw new TypeError("request timeoutMs must be a positive safe integer"); } const parsedData = typeof typeOrFrame === "string" ? await this.#parseRequestData(typeOrFrame, data) : data; const frame = this.#buildFrame(typeOrFrame, parsedData, options); try { const parsed = this.raw.bindings.parse_frame(frame); emit( this.#options.logger, isErrorType(parsed.type) ? { hint: "error", type: parsed.type, error: errorMessage(parsed), data: parsed.data, direction: "send", } : { hint: "info", type: parsed.type, data: parsed.data, direction: "send", }, ); } catch (error) { emit(this.#options.logger, { hint: "error", type: "Error", error: String(error), direction: "send", }); } // The WASM client owns request expiry and its late-response tombstones. // Keeping a second Promise timer here can reject the SDK call while the // protocol request is still allowed to complete successfully. const response = await this.raw.client.request( frame, options.responseType ?? null, timeoutMs, ); return typeof typeOrFrame === "string" ? await this.#parseResponseData(typeOrFrame, response) : response; } subscribe>( type: Type, handler: ( message: MTPResponseFrame, ) => void | Promise, ): Unsubscribe; subscribe( type: MTPCommunicationType, handler: (message: MTPFrame) => void | Promise, ): Unsubscribe { if (typeof type !== "string" || !type) { throw new TypeError("subscription type must be a non-empty string"); } if (typeof handler !== "function") { throw new TypeError("subscription handler must be a function"); } let active = true; const id = this.raw.client.subscribe(type, (message) => { if (!this.#protocol || !this.#protocol.schemas[type]) { void handler(message); return; } void this.#parseResponseData(type, message, "subscription").then( (parsed) => { if (active) void handler(parsed); }, (error) => { this.#protocol?.reportValidationError(error); }, ); }); return () => { active = false; this.raw.client.unsubscribe(id); }; } #handleFrame(frame) { if (isErrorType(frame.type)) { emit(this.#options.logger, { hint: "error", type: frame.type, error: errorMessage(frame), data: frame.data, direction: "recv", }); } else { emit(this.#options.logger, { hint: "info", type: frame.type, data: frame.data, direction: "recv", }); } } #getKemPublicKey(): Uint8Array { if (!this.#credentials?.keyring?.length) { throw new Error("No keyring available"); } const keys = keyringToKeys(this.#credentials.keyring); return keys.kemPublicKey; } #getKemSecretKey(): Uint8Array { if (!this.#credentials?.keyring?.length) { throw new Error("No keyring available"); } const keys = keyringToKeys(this.#credentials.keyring); return keys.kemSecretKey; } #getPublicKeyBundleBytes(): Uint8Array { if (!this.#credentials?.keyring?.length) { throw new Error("No keyring available"); } const keyring = bindings.WasmKeyring.from_bytes( this.#credentials.keyring, ); try { const bundle = keyring.public_key_bundle(); try { const fallible = ( bundle as typeof bundle & { try_to_bytes?: () => Uint8Array } ).try_to_bytes; if (!fallible) { throw new Error( "fallible public-key serialization is unavailable; rebuild mtp-wasm", ); } return fallible.call(bundle); } finally { bundle.free(); } } finally { keyring.free(); } } #resolveDecryptionIdentity( explicit?: MTPDecryptionIdentity, ): ResolvedDecryptionIdentity { return resolveDecryptionIdentity(explicit, this.#credentials); } #assertExpectedSignerId( signerId: bigint, expectedSignerId: bigint | number | string | undefined, ): void { if ( expectedSignerId != null && inputU64(expectedSignerId, "expectedSignerId") !== signerId ) { throw new Error("protected signer ID mismatch"); } } async #resolveSignerPublicKeys( signerId: bigint, options: SignerResolutionOptions, ): Promise { if (options.resolveSignerPublicKeys) { let resolved: MTPResolvedSignerKeys; try { resolved = await options.resolveSignerPublicKeys(signerId); } catch { throw signerKeysUnavailable(signerId); } if (!Array.isArray(resolved) || resolved.length === 0) { throw signerKeysUnavailable(signerId); } return resolved.map((value, index) => { try { const bytes = normalizeBytes(value, `senderPublicKeys[${index}]`); publicKeyBundleToKeys(bytes); return bytes; } catch { throw signerKeysUnavailable(signerId); } }); } if (this.#credentials?.clientId === signerId) { try { const bytes = this.#getPublicKeyBundleBytes(); publicKeyBundleToKeys(bytes); return [bytes]; } catch { throw signerKeysUnavailable(signerId); } } return null; } /** * Decrypt and verify only relay metadata. The content remains opaque so a * metadata-only relay participant can index or forward it without possessing * a content-recipient key. */ async openRelayMetadata( frame: ParsedFrame, options: MTPOpenRelayMetadataOptions = {}, ): Promise { // The resolver is asynchronous. Keep an immutable byte snapshot so the // signer claim and the later native verification refer to one frame. const signaturePolicy = resolveSignatureVerificationPolicy( options.signaturePolicy, this.#options.defaultSignatureVerificationPolicy ?? this.#options.securityProfile.protectedSignaturePolicy, ); const requestedReceiveLimits = normalizeReceiveLimits( options.limits, "limits", ); const receiveLimits = requestedReceiveLimits ? effectiveReceiveLimits( requestedReceiveLimits, this.#options.maxMessageSize ?? DEFAULT_MAX_MESSAGE_SIZE, ) : this.#options.receiveLimits; const receiveLimitsExplicit = options.limits != null || this.#options.receiveLimitsExplicit; const frameBytes = frame.raw.slice(); const frameSnapshot = receiveLimitsExplicit && receiveLimits ? decodeWithLimits(frameBytes, receiveLimits) : bindings.parse_frame(frameBytes); const recipient = this.#resolveDecryptionIdentity(options.recipient); let signerId: bigint; try { const bounded = boundedBindings(); if (bounded.relay_metadata_claimed_signer_id_with_limits) { signerId = BigInt( bounded.relay_metadata_claimed_signer_id_with_limits( frameBytes, recipient.keyrings, receiveLimits ?? {}, ), ); } else if (receiveLimitsExplicit) { throw new Error( "configured receive limits require a rebuilt bounded WASM package", ); } else { signerId = BigInt( bindings.relay_metadata_claimed_signer_id( frameBytes, recipient.keyrings, ), ); } } catch (error) { throw relayOpeningError(error); } this.#assertExpectedSignerId(signerId, options.expectedSignerId); const signerBundles = await this.#resolveSignerPublicKeys( signerId, options, ); if (!signerBundles) { throw signerKeysUnavailable(signerId); } let native: RawBindings.WasmVerifiedRelayMetadata | undefined; let ownershipTransferred = false; try { const bounded = boundedBindings(); if (!bounded.open_relay_metadata_with_keyrings_with_limits_without_replay) { throw new Error( "configured receive limits require a rebuilt bounded WASM package", ); } native = bounded.open_relay_metadata_with_keyrings_with_limits_without_replay( frameBytes, recipient.keyrings, signerId, signerBundles, signatureVerificationPolicyValue(signaturePolicy), receiveLimits ?? {}, ); } catch (error) { throw relayOpeningError(error, signerId); } try { if (!native) throw new Error("relay metadata opening returned no handle"); const metadataBytes = native.metadata(); const hasApplicationMetadata = metadataBytes != null; const applicationMetadata = hasApplicationMetadata ? (cloneParsedValue( receiveLimits ? decodeDataValueWithLimits(metadataBytes, receiveLimits) : bindings.parse_data_value(metadataBytes), ) as MTPDataValue) : undefined; const nativeSignerId = BigInt(native.signer_id()); const messageId = native.message_id(); const createdAt = BigInt(native.created_at()); const replayGuard = options.replayGuard ?? this.#relayReplayGuard; const accepted = await replayGuard.accept( nativeSignerId, messageId, createdAt, ); if (!accepted) throw new MTPReplayError(nativeSignerId, messageId); const matchedSignerKeyIndex = Number(native.matched_signer_key_index()); if ( !Number.isSafeInteger(matchedSignerKeyIndex) || matchedSignerKeyIndex < 0 || matchedSignerKeyIndex >= signerBundles.length ) { throw new Error("relay verification returned an invalid key index"); } const verified = new MTPVerifiedRelayMetadata(RELAY_METADATA_TOKEN, { frame: frameSnapshot, native, relayVersion: Number(native.relay_version()), signerId: nativeSignerId, finalRecipientId: BigInt(native.final_recipient_id()), messageId, createdAt, hasMetadata: hasApplicationMetadata, metadata: applicationMetadata, encryptedContent: native.encrypted_content().slice(), signerPublicKeys: signerBundles.map((bundle) => bundle.slice()), matchedSignerKeyIndex, signaturePolicy, receiveLimits: receiveLimits ? { ...receiveLimits } : undefined, receiveLimitsExplicit, disposed: false, finalizerToken: {}, }); // Register only after the JS wrapper owns the native handle. The // finally block below handles every failure before this transfer. const state = relayMetadataState.get(verified); if (!state) throw new Error("relay metadata state was not initialized"); registerRelayMetadata(verified, native, state.finalizerToken); ownershipTransferred = true; return verified; } finally { if (!ownershipTransferred) { try { native?.free(); } catch { // Preserve the original opening or conversion failure. } } } } /** Open and verify content after relay metadata has been authenticated. */ async openRelayContent( metadata: MTPVerifiedRelayMetadata, options: MTPOpenRelayContentOptions = {}, ): Promise { const recipient = this.#resolveDecryptionIdentity(options.recipient); const state = relayMetadataState.get(metadata); if (!state) { throw new Error( "relay metadata was not produced by authenticated opening", ); } if (state.disposed) { throw new Error("relay metadata has been disposed"); } const expectedFinalRecipientId = options.expectedFinalRecipientId == null ? recipient.id : inputU64( options.expectedFinalRecipientId, "expectedFinalRecipientId", ); const requestedReceiveLimits = normalizeReceiveLimits( options.limits, "limits", ); const receiveLimits = requestedReceiveLimits ? effectiveReceiveLimits( requestedReceiveLimits, this.#options.maxMessageSize ?? DEFAULT_MAX_MESSAGE_SIZE, ) : state.receiveLimits; const receiveLimitsExplicit = options.limits != null || state.receiveLimitsExplicit; // Content belongs to the authenticated metadata operation. Inherit its // policy when no content override is supplied so a client default cannot // split the two relay verification layers. const signaturePolicy = resolveSignatureVerificationPolicy( options.signaturePolicy, state.signaturePolicy, ); if (signaturePolicy !== state.signaturePolicy) { throw new MTPSignatureVerificationError( "policy-rejected", state.signerId, ); } let signerBundles: Uint8Array[] = state.signerPublicKeys.map((bundle) => bundle.slice(), ); this.#assertExpectedSignerId(state.signerId, options.expectedSignerId); if (options.resolveSignerPublicKeys != null) { const resolved = await this.#resolveSignerPublicKeys( state.signerId, options, ); if (state.disposed) { throw new Error("relay metadata has been disposed"); } if (!resolved) { throw signerKeysUnavailable(state.signerId); } signerBundles = resolved; } // A caller may dispose the capability while asynchronous signer-key // resolution is in flight. Never hand a freed native handle back to // wasm-bindgen after that await, even if the resolver returned no keys. if (state.disposed) { throw new Error("relay metadata has been disposed"); } let nativeContent: RawBindings.WasmVerifiedRelayContent; try { const bounded = boundedBindings(); const nativeExpectedFinalRecipientId = expectedFinalRecipientId == null ? null : BigInt(expectedFinalRecipientId); if (!bounded.open_relay_content_with_keyrings_with_limits_without_replay) { throw new Error( "configured receive limits require a rebuilt bounded WASM package", ); } nativeContent = bounded.open_relay_content_with_keyrings_with_limits_without_replay( state.native, recipient.keyrings, signerBundles, nativeExpectedFinalRecipientId, signatureVerificationPolicyValue(signaturePolicy), receiveLimits ?? {}, ); } catch (error) { throw relayOpeningError(error, state.signerId); } try { return this.#formatRelayContent(nativeContent, state); } finally { nativeContent.free(); } } #formatRelayContent( nativeContent: RawBindings.WasmVerifiedRelayContent, state: MTPRelayMetadataState, ): MTPVerifiedRelayContent { const contentBytes = nativeContent.content(); const data = state.receiveLimitsExplicit && state.receiveLimits ? decodeDataValueWithLimits(contentBytes, state.receiveLimits) : (bindings.parse_data_value(contentBytes) as MTPDataValue); return { type: nativeContent.message_type(), data: cloneParsedValue(data) as MTPDataValue, signerId: BigInt(nativeContent.signer_id()), finalRecipientId: BigInt(nativeContent.final_recipient_id()), messageId: state.messageId, createdAt: state.createdAt, metadata: state.hasMetadata ? (cloneParsedValue(state.metadata) as MTPDataValue) : undefined, }; } async #openProtectedFrame( frameInput: MTPProtectedFrameInput, options: MTPOpenProtectedOptions, ): Promise<{ frame: ParsedFrame; message: MTPVerifiedProtectedMessage; }> { if (!options || typeof options !== "object") { throw new TypeError("openProtected requires an options object"); } if (typeof options.resolveSignerPublicKeys !== "function") { throw new TypeError("openProtected requires resolveSignerPublicKeys"); } // Detach parsed-object inputs before awaiting signer-key resolution. This // keeps the authenticated payload and routing fields bound to one // snapshot even when a caller reuses or mutates its frame object. const requestedReceiveLimits = normalizeReceiveLimits( options.limits, "limits", ); const receiveLimits = requestedReceiveLimits ? effectiveReceiveLimits( requestedReceiveLimits, this.#options.maxMessageSize ?? DEFAULT_MAX_MESSAGE_SIZE, ) : this.#options.receiveLimits; const receiveLimitsExplicit = options.limits != null || this.#options.receiveLimitsExplicit; const frame = cloneParsedFrame( parseProtectedFrame( frameInput, receiveLimitsExplicit ? receiveLimits : undefined, ), ); assertKnownCommunicationType(frame); const frameBytes = protectedFrameBytes(frame, receiveLimits); validateApplicationProtectionPurpose(options.signaturePurpose); validateApplicationProtectionPurpose(options.encryptionPurpose); const recipient = this.#resolveDecryptionIdentity(options.recipient); const signaturePolicy = resolveSignatureVerificationPolicy( options.signaturePolicy, this.#options.defaultSignatureVerificationPolicy ?? this.#options.securityProfile.protectedSignaturePolicy, ); const expectedReceiverId = options.expectedReceiverId == null ? recipient.id : inputU64(options.expectedReceiverId, "expectedReceiverId"); let signerId: bigint; try { const bounded = boundedBindings(); if (bounded.protected_claimed_signer_id_with_limits) { signerId = BigInt( bounded.protected_claimed_signer_id_with_limits( frameBytes, recipient.keyrings, options.encryptionPurpose, receiveLimits ?? {}, ), ); } else if (receiveLimitsExplicit) { throw new Error( "configured receive limits require a rebuilt bounded WASM package", ); } else { signerId = BigInt( bindings.protected_claimed_signer_id( frameBytes, recipient.keyrings, options.encryptionPurpose, ), ); } } catch (error) { throw protectedOpeningError(error); } this.#assertExpectedSignerId(signerId, options.expectedSignerId); const signerBundles = await this.#resolveSignerPublicKeys(signerId, options); if (!signerBundles) { throw signerKeysUnavailable(signerId); } let native: RawBindings.WasmVerifiedProtectedMessage; try { const bounded = boundedBindings(); const nativeExpectedReceiverId = expectedReceiverId == null ? null : expectedReceiverId; if (!bounded.open_protected_with_keyrings_with_limits_without_replay) { throw new Error( "configured receive limits require a rebuilt bounded WASM package", ); } native = bounded.open_protected_with_keyrings_with_limits_without_replay( frameBytes, recipient.keyrings, signerId, signerBundles, nativeExpectedReceiverId, options.signaturePurpose, options.encryptionPurpose, signatureVerificationPolicyValue(signaturePolicy), receiveLimits ?? {}, ); } catch (error) { throw protectedOpeningError(error, signerId); } const replayGuard = options.replayGuard ?? this.#protectedReplayGuard; const nativeSignerId = BigInt(native.signer_id()); const messageId = native.message_id(); const createdAt = BigInt(native.created_at()); try { const accepted = await replayGuard.accept( nativeSignerId, messageId, createdAt, ); if (!accepted) throw new MTPReplayError(nativeSignerId, messageId); const contentBytes = native.content(); const data = receiveLimitsExplicit && receiveLimits ? decodeDataValueWithLimits(contentBytes, receiveLimits) : (bindings.parse_data_value(contentBytes) as MTPDataValue); const receiver = frame.receiver; const outerSender = frame.sender; return { frame, message: { type: native.message_type(), protectedVersion: Number(native.protected_version()), signerId: nativeSignerId, finalRecipientId: BigInt(native.final_recipient_id()), messageId, createdAt, ...(receiver == null ? {} : { receiver }), ...(outerSender == null ? {} : { outerSender }), data: cloneParsedValue(data) as MTPDataValue, }, }; } finally { native.free(); } } async openProtected( frame: MTPProtectedFrameInput, options: MTPOpenProtectedOptions, ): Promise> { const opened = await this.#openProtectedFrame(frame, options); return opened.message as MTPVerifiedProtectedMessage; } subscribeProtected( type: MTPCommunicationType, handler: ( message: MTPVerifiedProtectedMessage, frame: ParsedFrame, ) => void | Promise, options: MTPOpenProtectedOptions, ): Unsubscribe { if (typeof type !== "string" || !type) { throw new TypeError( "protected subscription type must be a non-empty string", ); } const applicationType = assertApplicationCommunicationType(type); if (typeof handler !== "function") { throw new TypeError("protected handler must be a function"); } const replayGuard = options.replayGuard ?? new InMemoryReplayGuard(); const subscriptionOptions = { ...options, replayGuard }; const sub = this.raw.client.subscribe( applicationType, async (frame: ParsedFrame) => { try { const opened = await this.#openProtectedFrame( frame, subscriptionOptions, ); if (opened.message.type !== applicationType) return; await handler( opened.message as MTPVerifiedProtectedMessage, opened.frame, ); } catch (error) { emit(this.#options.logger, { hint: "error", type: "E2EE", error: String(error), direction: "recv", }); } }, ); return () => this.raw.client.unsubscribe(sub); } async sendProtected( type: MTPCommunicationType, data: MTPDataValueInput, options: MTPSendProtectedOptions, ): Promise { const messageType = assertApplicationCommunicationType(type); const identity = resolveProtectionIdentity( options.identity, this.#credentials, ); const receiverId = inputU64(options.receiverId, "receiverId"); const recipients = normalizeRecipientBundles(options.recipients, "recipients"); const messageId = createMessageId(); const createdAt = unixTimeMillis(); validateApplicationProtectionPurpose(options.signaturePurpose); validateApplicationProtectionPurpose(options.encryptionPurpose); const signatureSuite = effectiveProtectionSignatureSuite( identity.keyring, options.signatureSuite ?? this.#options.securityProfile.protectedSignatureSuite, ); const encodeLimits = this.#encodeLimits(); const protectedLimits = normalizeReceiveLimits(options.limits, "options.limits"); const messageIdBytes = utf8Encode(messageId).length; if ( protectedLimits?.maxMessageIdBytes != null && messageIdBytes > protectedLimits.maxMessageIdBytes ) { throw new RangeError("protected message ID exceeds maxMessageIdBytes"); } const encodedContent = encodeMTPDataValue(data, encodeLimits); const builderLimits = { ...encodeLimits, ...protectedLimits }; let frame: Uint8Array; try { const bounded = ( bindings as typeof bindings & { build_protected_frame_with_keyring_with_limits?: ( messageType: string, encodedContent: Uint8Array, signerId: bigint, finalRecipientId: bigint, messageId: string, createdAt: bigint, signaturePurpose: number, encryptionPurpose: number, keyring: Uint8Array, signatureSuite: number, frameId: number | null, exposeSender: boolean, recipients: Uint8Array[], limits: MTPReceiveLimits & MTPEncodeLimits, ) => Uint8Array; } ).build_protected_frame_with_keyring_with_limits; if (!bounded) { throw new Error( "bounded WASM protected-message encoding is unavailable; rebuild mtp-wasm", ); } frame = bounded( messageType, encodedContent, identity.signerId, receiverId, messageId, createdAt, options.signaturePurpose, options.encryptionPurpose, identity.keyring, protectionSignatureSuiteValue(signatureSuite), options.id ?? null, options.exposeSender ?? false, recipients, builderLimits, ); } catch (error) { throw protectedOpeningError(error, identity.signerId); } await this.send(frame); } async sendSealedRelay( type: MTPCommunicationType, data: MTPDataValueInput, options: MTPSendSealedRelayOptions, ): Promise { const messageType = assertApplicationCommunicationType(type); const identity = resolveProtectionIdentity( options.identity, this.#credentials, ); const finalRecipientId = inputU64( options.finalRecipientId, "finalRecipientId", ); const nextHopId = inputU64(options.nextHopId, "nextHopId"); const metadataRecipients = normalizeRecipientBundles( options.metadataRecipients, "metadataRecipients", ); const contentRecipients = normalizeRecipientBundles( options.contentRecipients, "contentRecipients", ); const signatureSuite = effectiveProtectionSignatureSuite( identity.keyring, options.signatureSuite ?? this.#options.securityProfile.protectedSignatureSuite, ); const encodeLimits = this.#encodeLimits(); const protectedLimits = normalizeReceiveLimits(options.limits, "options.limits"); validateMTPDataValue(data, encodeLimits); const encodedMetadata = options.metadata === undefined ? undefined : encodeMTPDataValue(options.metadata, encodeLimits); if ( encodedMetadata && protectedLimits?.maxMetadataEncodedBytes != null && encodedMetadata.length > protectedLimits.maxMetadataEncodedBytes ) { throw new RangeError("relay metadata exceeds maxMetadataEncodedBytes"); } const builderLimits = { ...encodeLimits, ...protectedLimits }; const messageId = createMessageId(); const createdAt = unixTimeMillis(); const bounded = ( bindings as typeof bindings & { build_encrypted_relay_frame_with_keyring_with_limits?: ( messageType: string, data: MTPDataValueInput, signerId: bigint, finalRecipientId: bigint, nextHopId: bigint, messageId: string, createdAt: bigint, encodedMetadata: Uint8Array | undefined, keyring: Uint8Array, signatureSuite: number, metadataRecipients: Uint8Array[], contentRecipients: Uint8Array[], limits: MTPReceiveLimits & MTPEncodeLimits, ) => Uint8Array; } ).build_encrypted_relay_frame_with_keyring_with_limits; if (!bounded) { throw new Error( "bounded WASM relay encoding is unavailable; rebuild mtp-wasm", ); } const frame = bounded( messageType, data, identity.signerId, finalRecipientId, nextHopId, messageId, createdAt, encodedMetadata, identity.keyring, protectionSignatureSuiteValue(signatureSuite), metadataRecipients, contentRecipients, builderLimits, ); await this.send(frame); } subscribeSealedRelay( type: MTPCommunicationType, handler: ( content: MTPVerifiedRelayContent, frame: ParsedFrame, ) => void | Promise, options?: MTPEncryptedSubscriptionOptions, ): Unsubscribe; subscribeSealedRelay( handler: ( content: MTPVerifiedRelayContent, frame: ParsedFrame, ) => void | Promise, options?: MTPEncryptedSubscriptionOptions, ): Unsubscribe; subscribeSealedRelay( typeOrHandler: | MTPCommunicationType | (( content: MTPVerifiedRelayContent, frame: ParsedFrame, ) => void | Promise), maybeHandlerOrOptions?: | (( content: MTPVerifiedRelayContent, frame: ParsedFrame, ) => void | Promise) | MTPEncryptedSubscriptionOptions, maybeOptions?: MTPEncryptedSubscriptionOptions, ): Unsubscribe { const expectedInnerType = typeof typeOrHandler === "function" ? null : typeOrHandler; if (expectedInnerType != null) { assertApplicationCommunicationType(expectedInnerType); } const handler = typeof typeOrHandler === "function" ? typeOrHandler : typeof maybeHandlerOrOptions === "function" ? maybeHandlerOrOptions : null; if (!handler) { throw new TypeError("sealed relay handler must be a function"); } const options: MTPEncryptedSubscriptionOptions = typeof typeOrHandler === "function" ? typeof maybeHandlerOrOptions === "function" || maybeHandlerOrOptions == null ? {} : maybeHandlerOrOptions : (maybeOptions ?? {}); const replayGuard = options.replayGuard ?? new InMemoryReplayGuard(); const subscriptionOptions = { ...options, replayGuard }; const sub = this.raw.client.subscribe( "Relay", async (frame: ParsedFrame) => { let metadata: MTPVerifiedRelayMetadata | undefined; try { metadata = await this.openRelayMetadata(frame, subscriptionOptions); const opened = await this.openRelayContent( metadata, subscriptionOptions, ); if ( !opened || (expectedInnerType && opened.type !== expectedInnerType) ) { return; } const parsedFrame: ParsedFrame = { ...frame, type: opened.type, data: opened.data as ParsedFrame["data"], }; await handler(opened, parsedFrame); } catch (e) { emit(this.#options.logger, { hint: "error", type: "E2EE", error: String(e), direction: "recv", }); } finally { metadata?.dispose(); } }, ); return () => this.raw.client.unsubscribe(sub); } /** * Subscribe to verified relay metadata without attempting content * decryption. The metadata capability is callback-scoped: it is disposed * after the handler resolves, so do not retain it for later content opening. * Call `openRelayMetadata()` directly when a longer-lived capability is * required and dispose it when finished. */ subscribeRelayMetadata( handler: ( metadata: MTPVerifiedRelayMetadata, frame: ParsedFrame, ) => void | Promise, options: MTPOpenRelayMetadataOptions = {}, ): Unsubscribe { if (typeof handler !== "function") { throw new TypeError("relay metadata handler must be a function"); } const replayGuard = options.replayGuard ?? new InMemoryReplayGuard(); const subscriptionOptions = { ...options, replayGuard }; const sub = this.raw.client.subscribe( "Relay", async (frame: ParsedFrame) => { let metadata: MTPVerifiedRelayMetadata | undefined; try { metadata = await this.openRelayMetadata(frame, subscriptionOptions); await handler(metadata, frame); } catch (e) { emit(this.#options.logger, { hint: "error", type: "E2EE", error: String(e), direction: "recv", }); } finally { metadata?.dispose(); } }, ); return () => this.raw.client.unsubscribe(sub); } /** Explicit alias for callers that want to emphasize encrypted metadata. */ subscribeEncryptedMetadata( handler: ( metadata: MTPVerifiedRelayMetadata, frame: ParsedFrame, ) => void | Promise, options: MTPOpenRelayMetadataOptions = {}, ): Unsubscribe { return this.subscribeRelayMetadata(handler, options); } async setEncryptedSecret(record: MTPEncryptedSecretRecord): Promise { await this.encryptedSecretProvider.set(record); } async getEncryptedSecret(id: string): Promise { return this.encryptedSecretProvider.get(id); } async deleteEncryptedSecret(id: string): Promise { await this.encryptedSecretProvider.delete(id); } setOnPipeRequest(handler: ((request: MTPPipeRequest) => void) | null): void { if (handler == null) { this.raw.client.set_on_pipe_request(null); return; } this.raw.client.set_on_pipe_request( (event: { pipeId: number; description: string }) => { emit(this.#options.logger, { hint: "info", type: "PipeRequest", data: event, direction: "recv", }); handler({ pipeId: event.pipeId, description: event.description }); }, ); } async createPipe(description: string): Promise { if (typeof description !== "string") { throw new TypeError("description must be a string"); } const handle: WasmPipeHandle = await this.raw.client.create_pipe(description); const sdk = this; return { pipeId: handle.pipeId, description: handle.description, async wait(): Promise { const result = await handle.wait(); if (result == null) { return null; } emit(sdk.#options.logger, { hint: "info", type: "PipeCreated", data: { pipeId: result.pipeId }, direction: "send", }); return result as unknown as MTPPipeWriter; }, }; } /** * Create and negotiate an encrypted pipe with the actual MTP pipe ID and * local identity bound automatically. A recipient array creates a group * bootstrap; membership changes should create a fresh session with the new * array. */ async createEncryptedPipe( options: MTPCreateEncryptedPipeOptions, ): Promise { const credentials = this.#credentials; if (!credentials || credentials.clientId == null) { throw new Error("Client not registered"); } const recipientId = inputU64(options.recipientId, "recipientId"); if ( options.recipientPublicKey != null && options.recipientPublicKeys != null ) { throw new Error( "provide recipientPublicKey or recipientPublicKeys, not both", ); } const rawRecipients = options.recipientPublicKeys ?? (options.recipientPublicKey != null ? [options.recipientPublicKey] : []); if (rawRecipients.length === 0) { throw new Error("at least one recipient public key is required"); } const recipients = rawRecipients.map((value, index) => normalizeBytes(value, `recipientPublicKeys[${index}]`), ); recipients.forEach((value) => publicKeyBundleToKeys(value)); const purpose = options.purpose ?? 0x40; const direction = options.direction ?? 0; validateApplicationProtectionPurpose(purpose); if (!Number.isInteger(direction) || direction < 0 || direction > 0xff) { throw new RangeError("direction must be a u8"); } const handle = await this.createPipe( options.description ?? "encrypted pipe", ); const writer = await handle.wait(); if (!writer) return null; if (writer.pipeId !== handle.pipeId) { throw new Error("created pipe ID does not match the accepted writer"); } const sessionId = new Uint8Array(32); globalThis.crypto.getRandomValues(sessionId); return initiateMTPPipeSession( writer, { sessionId, pipeId: writer.pipeId, senderId: credentials.clientId, recipientId, purpose, direction, }, credentials.keyring, recipients, options.signatureSuite ?? this.#options.securityProfile.encryptedPipeSignatureSuite, ); } async acceptPipe(pipeId: number): Promise { if (typeof pipeId !== "number" || !Number.isFinite(pipeId)) { throw new TypeError("pipeId must be a finite number"); } const reader = await this.raw.client.accept_pipe(pipeId); emit(this.#options.logger, { hint: "info", type: "PipeAccepted", data: { pipeId: reader.pipeId, description: reader.description }, direction: "send", }); return reader as unknown as MTPPipeReader; } /** Accept a pipe and learn its authenticated session ID from the offer. */ async acceptEncryptedPipe( request: MTPPipeRequest, options: MTPAcceptEncryptedPipeOptions, ): Promise { const credentials = this.#credentials; if (!credentials || credentials.clientId == null) { throw new Error("Client not registered"); } if (!Number.isSafeInteger(request.pipeId) || request.pipeId <= 0) { throw new TypeError("request.pipeId must be a non-zero safe integer"); } const senderId = inputU64(options.senderId, "senderId"); const purpose = options.purpose ?? 0x40; const direction = options.direction ?? 0; validateApplicationProtectionPurpose(purpose); if (!Number.isInteger(direction) || direction < 0 || direction > 0xff) { throw new RangeError("direction must be a u8"); } if ( options.senderPublicKeys != null && options.senderPublicKeys.length === 0 ) { throw new Error("senderPublicKeys must contain at least one key"); } const singleSenderPublicKey = options.senderPublicKey; if (options.senderPublicKeys == null && singleSenderPublicKey == null) { throw new Error("senderPublicKey or senderPublicKeys is required"); } const senderPublicKey = options.senderPublicKeys != null ? options.senderPublicKeys.map((value, index) => normalizeBytes(value, `senderPublicKeys[${index}]`), ) : normalizeBytes(singleSenderPublicKey!, "senderPublicKey"); const reader = await this.acceptPipe(request.pipeId); if (reader.pipeId !== request.pipeId) { throw new Error("accepted pipe ID does not match the requested pipe"); } const signaturePolicy = resolveSignatureVerificationPolicy( options.signaturePolicy, this.#options.defaultSignatureVerificationPolicy ?? this.#options.securityProfile.encryptedPipeSignaturePolicy, ); return acceptMTPPipeSessionAuto( reader, { pipeId: reader.pipeId, senderId, recipientId: credentials.clientId, purpose, direction, }, credentials.keyring, senderPublicKey, signaturePolicy, ); } async denyPipe(pipeId: number): Promise { if (typeof pipeId !== "number" || !Number.isFinite(pipeId)) { throw new TypeError("pipeId must be a finite number"); } await this.raw.client.deny_pipe(pipeId); emit(this.#options.logger, { hint: "info", type: "PipeDenied", data: { pipeId }, direction: "send", }); } disconnect(): void { this.raw.client.stop_protocol_pings(); this.raw.client.disconnect(); } } export { ConnectionState, bindings as raw }; export { crypto, codec, encode, decode, decodeDataValueWithLimits, decodeWithLimits, format, bytesToBase64, base64ToBytes, bytesFromEncodedString, strictHexDecode, strictBase64Decode, secretKeyFromBytes, secretKeyFromHex, secretKeyFromBase64, secretKeyFromString, legacySecretKeyFromStringV1, deriveKeyFromPassphrase, deriveKeyFromPassphraseSync, keyringToKeys, publicKeyBundleToKeys, } from "./codec.js"; export { InMemoryReplayGuard, MTPReplayError, MTPMissingProtectedVersionError, MTPResourceLimitError, MTPUnsupportedProtectedVersionError, } from "./protection.js"; export { MTPMissingRelayVersionError, MTPUnsupportedRelayVersionError, MTPVerifiedRelayMetadata, } from "./relay.js"; // E2EE exports export type { MTPSessionState, MTPSessionStorage, MTPSessionTranscriptContext, SkippedMessageKey, } from "./session"; export { MTPSessionManager, InMemorySessionStorage, derivePeerSessionId, deriveSessionKeys, buildSessionTranscript, } from "./session.js"; export { MTPRatchet } from "./ratchet.js"; export type { RatchetStep } from "./ratchet.js"; export { MTPEncryptedPipeError, MTPEncryptedPipeReader, MTPEncryptedPipeWriter, MTPPipeProtectionContext, MAX_ENCRYPTED_PIPE_RECORD, pipeSessionSignaturePurpose, pipeSessionEncryptionPurpose, validateApplicationProtectionPurpose, MAX_PIPE_SESSION_OFFER, initiateMTPPipeSession, acceptMTPPipeSession, acceptMTPPipeSessionAuto, initiateMTPForwardSecurePipeSession, acceptMTPForwardSecurePipeSession, } from "./encrypted-pipe.js"; export type { MTPReadablePipe, MTPWritablePipe, MTPPipeSessionParameters, MTPPipeSessionExpectation, MTPDuplexPipe, MTPEncryptedPipeReaderSource, MTPEncryptedPipeWriterSource, } from "./encrypted-pipe.js"; export { serializeEncryptedMessage, parseEncryptedMessage, encryptPayload, decryptPayload, MTP_E2EE_VERSION, FLAG_INIT, MAX_RATCHET_SKIP, } from "./encrypted-message.js"; export type { EncryptedMessageHeader, SerializedEncryptedMessage, } from "./encrypted-message"; export type { MTPEncryptedSecretRecord, MTPEncryptedSecretProvider, } from "./encrypted-secret"; export { InMemoryEncryptedSecretProvider } from "./encrypted-secret.js";