2635 lines
81 KiB
TypeScript
2635 lines
81 KiB
TypeScript
// 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 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<StorageValue>;
|
|
setItem(key: string, value: string): void | Promise<void>;
|
|
removeItem(key: string): void | Promise<void>;
|
|
}
|
|
|
|
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<Uint8Array>;
|
|
decrypt(key: Uint8Array, input: Uint8Array): Promise<Uint8Array>;
|
|
encryptText(key: Uint8Array, plaintext: string): Promise<string>;
|
|
decryptText(key: Uint8Array, ciphertext: string): Promise<string>;
|
|
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<string, unknown>,
|
|
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 {
|
|
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<RawBindings.InitInput>
|
|
| {
|
|
module_or_path: RawBindings.InitInput | Promise<RawBindings.InitInput>;
|
|
};
|
|
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;
|
|
}
|
|
|
|
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<MTPResolvedSignerKeys>;
|
|
|
|
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<T = MTPDataValue> {
|
|
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<boolean>;
|
|
}
|
|
|
|
/**
|
|
* 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<void>;
|
|
close(): Promise<void>;
|
|
abort(): void;
|
|
readonly pipeId: number;
|
|
}
|
|
|
|
export interface MTPPipeReader {
|
|
read(): Promise<Uint8Array | null>;
|
|
readonly pipeId: number;
|
|
readonly description: string;
|
|
}
|
|
|
|
export interface MTPPipeRequest {
|
|
pipeId: number;
|
|
description: string;
|
|
}
|
|
|
|
export interface MTPOutgoingPipeHandle {
|
|
readonly pipeId: number;
|
|
readonly description: string;
|
|
wait(): Promise<MTPPipeWriter | null>;
|
|
}
|
|
|
|
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<StorageValue> {
|
|
return storage ? await storage.getItem(key) : null;
|
|
}
|
|
|
|
async function storageSet(
|
|
storage: MTPCredentialStorage | undefined,
|
|
key: string,
|
|
value: string,
|
|
): Promise<void> {
|
|
if (storage) {
|
|
await storage.setItem(key, value);
|
|
}
|
|
}
|
|
|
|
async function storageRemove(
|
|
storage: MTPCredentialStorage | undefined,
|
|
key: string,
|
|
): Promise<void> {
|
|
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");
|
|
}
|
|
}
|
|
|
|
export class MTPClient {
|
|
static readonly crypto = crypto;
|
|
static readonly codec = codec;
|
|
|
|
#credentials: InternalCredentials | null;
|
|
#options: NormalizedMTPClientOptions;
|
|
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.#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(options: MTPClientOptions): Promise<MTPClient> {
|
|
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<Awaited<ReturnType<typeof initWasmOnce>>> {
|
|
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<void> {
|
|
if (this.#credentials?.clientId != null && this.#options.hostPublicKey) {
|
|
await this.auth();
|
|
return;
|
|
}
|
|
|
|
await this.connectUnauthenticated();
|
|
}
|
|
|
|
async connectUnauthenticated(): Promise<void> {
|
|
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<bigint> {
|
|
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<bigint> {
|
|
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<void> {
|
|
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,
|
|
};
|
|
}
|
|
|
|
#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<string, unknown>,
|
|
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<void>;
|
|
async send(
|
|
type: MTPCommunicationType,
|
|
data: Record<string, unknown>,
|
|
options?: MTPSendOptions,
|
|
): Promise<void>;
|
|
async send(
|
|
typeOrFrame: Uint8Array | MTPCommunicationType,
|
|
data?: Record<string, unknown>,
|
|
options?: MTPSendOptions,
|
|
): Promise<void> {
|
|
const message = this.#buildFrame(typeOrFrame, data, 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<void> {
|
|
await this.send(this.forwardRelayFrame(frame, nextHopId));
|
|
}
|
|
|
|
async request(
|
|
message: Uint8Array,
|
|
data?: never,
|
|
options?: MTPRequestOptions,
|
|
): Promise<ParsedFrame>;
|
|
async request(
|
|
type: MTPCommunicationType,
|
|
data: Record<string, unknown>,
|
|
options?: MTPRequestOptions,
|
|
): Promise<ParsedFrame>;
|
|
async request(
|
|
typeOrFrame: Uint8Array | MTPCommunicationType,
|
|
data?: Record<string, unknown>,
|
|
options: MTPRequestOptions = {},
|
|
): Promise<ParsedFrame> {
|
|
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 frame = this.#buildFrame(typeOrFrame, data, 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.
|
|
return await this.raw.client.request(
|
|
frame,
|
|
options.responseType ?? null,
|
|
timeoutMs,
|
|
);
|
|
}
|
|
|
|
subscribe(
|
|
type: MTPCommunicationType,
|
|
handler: (message: ParsedFrame) => void,
|
|
): 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");
|
|
}
|
|
const id = this.raw.client.subscribe(type, handler);
|
|
return () => 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<Uint8Array[] | null> {
|
|
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<MTPVerifiedRelayMetadata> {
|
|
// 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<MTPVerifiedRelayContent> {
|
|
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<T = MTPDataValue>(
|
|
frame: MTPProtectedFrameInput,
|
|
options: MTPOpenProtectedOptions,
|
|
): Promise<MTPVerifiedProtectedMessage<T>> {
|
|
const opened = await this.#openProtectedFrame(frame, options);
|
|
return opened.message as MTPVerifiedProtectedMessage<T>;
|
|
}
|
|
|
|
subscribeProtected<T = MTPDataValue>(
|
|
type: MTPCommunicationType,
|
|
handler: (
|
|
message: MTPVerifiedProtectedMessage<T>,
|
|
frame: ParsedFrame,
|
|
) => void | Promise<void>,
|
|
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<T>,
|
|
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<void> {
|
|
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<void> {
|
|
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<void>,
|
|
options?: MTPEncryptedSubscriptionOptions,
|
|
): Unsubscribe;
|
|
subscribeSealedRelay(
|
|
handler: (
|
|
content: MTPVerifiedRelayContent,
|
|
frame: ParsedFrame,
|
|
) => void | Promise<void>,
|
|
options?: MTPEncryptedSubscriptionOptions,
|
|
): Unsubscribe;
|
|
subscribeSealedRelay(
|
|
typeOrHandler:
|
|
| MTPCommunicationType
|
|
| ((
|
|
content: MTPVerifiedRelayContent,
|
|
frame: ParsedFrame,
|
|
) => void | Promise<void>),
|
|
maybeHandlerOrOptions?:
|
|
| ((
|
|
content: MTPVerifiedRelayContent,
|
|
frame: ParsedFrame,
|
|
) => void | Promise<void>)
|
|
| 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<void>,
|
|
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<void>,
|
|
options: MTPOpenRelayMetadataOptions = {},
|
|
): Unsubscribe {
|
|
return this.subscribeRelayMetadata(handler, options);
|
|
}
|
|
|
|
async setEncryptedSecret(record: MTPEncryptedSecretRecord): Promise<void> {
|
|
await this.encryptedSecretProvider.set(record);
|
|
}
|
|
|
|
async getEncryptedSecret(id: string): Promise<MTPEncryptedSecretRecord | null> {
|
|
return this.encryptedSecretProvider.get(id);
|
|
}
|
|
|
|
async deleteEncryptedSecret(id: string): Promise<void> {
|
|
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<MTPOutgoingPipeHandle> {
|
|
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<MTPPipeWriter | null> {
|
|
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<MTPEncryptedPipeWriter | null> {
|
|
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<MTPPipeReader> {
|
|
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<MTPEncryptedPipeReader> {
|
|
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<void> {
|
|
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";
|