From 8ae837766200d33f889d1d11a5486f51dacd2ad1 Mon Sep 17 00:00:00 2001 From: Alois Date: Thu, 2 Jul 2026 23:38:38 +0200 Subject: [PATCH 1/3] (feat): add auth() function (fix): wasm error --- README.md | 4 +--- example/web-client/src/main.ts | 5 +---- src/sdk/index.ts | 15 +++++++++++++-- wasm/src/client.rs | 16 ++++++++-------- wasm/src/transport.rs | 7 +++++-- 5 files changed, 28 insertions(+), 19 deletions(-) diff --git a/README.md b/README.md index a5df4d2..265f7b3 100644 --- a/README.md +++ b/README.md @@ -62,9 +62,7 @@ const client = await MTPClient.create({ client.subscribe("SomeType", (message) => console.log(message)); -const clientId = client.credentials?.clientId == null - ? await client.register() - : (await client.connect(), client.credentials.clientId); +const clientId = await client.auth(); await client.send("SomeType", { value: "hello" }); console.log("Connected MTP client", clientId, client.state); diff --git a/example/web-client/src/main.ts b/example/web-client/src/main.ts index 07f24bd..2e8da5c 100644 --- a/example/web-client/src/main.ts +++ b/example/web-client/src/main.ts @@ -199,10 +199,7 @@ async function connect() { log(`Subscribed Pong: ${formatParsedFrame(frame)}`, "received"); }); - const existingClientId = client.credentials?.clientId; - const activeClientId = existingClientId == null - ? await client.register() - : (await client.connect(), BigInt(existingClientId)); + const activeClientId = await client.auth(); clientId = activeClientId; loadKeys(); log(`Connected as client ${activeClientId}`); diff --git a/src/sdk/index.ts b/src/sdk/index.ts index 7006849..3e3d077 100644 --- a/src/sdk/index.ts +++ b/src/sdk/index.ts @@ -128,6 +128,7 @@ type NormalizedMTPClientOptions = Omit & { }; const DEFAULT_CREDENTIALS_KEY = "mtp:credentials"; +let wasmInitPromise: Promise>> | undefined; function emit(logger, event) { if (typeof logger === "function") { @@ -394,7 +395,8 @@ export class MTPClient { } static async init(wasm?: MTPClientOptions["wasm"]): Promise>> { - return await initWasm(wasm); + wasmInitPromise ??= initWasm(wasm); + return await wasmInitPromise; } get credentials(): MTPClientCredentials | null { @@ -430,7 +432,7 @@ export class MTPClient { async connect(): Promise { if (this.#credentials?.clientId != null && this.#options.hostPublicKey) { - await this.#connectAuthenticated(); + await this.auth(); return; } @@ -447,6 +449,15 @@ export class MTPClient { } } + 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"); diff --git a/wasm/src/client.rs b/wasm/src/client.rs index 7aef180..16c33eb 100644 --- a/wasm/src/client.rs +++ b/wasm/src/client.rs @@ -705,19 +705,19 @@ impl WasmClient { let loop_pending_requests = pending_requests.clone(); let ping_timer = self.ping_timer.clone(); wasm_bindgen_futures::spawn_local(async move { - let route_frame = Closure::wrap(Box::new(move |frame: JsValue| { - route_incoming_frame(&frame, &on_msg, &subscriptions, &loop_pending_requests); - }) as Box); loop_transport .receive_loop( - route_frame - .as_ref() - .unchecked_ref::() - .clone(), + move |frame: JsValue| { + route_incoming_frame( + &frame, + &on_msg, + &subscriptions, + &loop_pending_requests, + ); + }, on_err.clone(), ) .await; - drop(route_frame); state.set(ConnectionState::Disconnected); stop_ping_timer(&ping_timer); reject_pending_requests(&pending_requests, "disconnected"); diff --git a/wasm/src/transport.rs b/wasm/src/transport.rs index df59c30..1bdf39c 100644 --- a/wasm/src/transport.rs +++ b/wasm/src/transport.rs @@ -347,12 +347,15 @@ impl WasmTransport { /// Background loop: deliver every incoming frame to `on_message` until the /// connection closes. Shares reader state with `read_one_frame`, so frames /// buffered during the handshake are not lost. - pub async fn receive_loop(&self, on_message: js_sys::Function, on_error: js_sys::Function) { + pub async fn receive_loop(&self, mut on_message: F, on_error: js_sys::Function) + where + F: FnMut(JsValue), + { loop { match self.next_frame().await { Ok(FrameOutcome::Frame(frame)) => match parse_frame_value(&frame) { Ok(parsed) => { - let _ = on_message.call1(&JsValue::NULL, &parsed); + on_message(parsed); } Err(e) => { let message = e.as_string().unwrap_or_else(|| format!("{:?}", e)); From c3fbeb2e78df9b9bd4ccebbc2ef91e9cfbaa0dab Mon Sep 17 00:00:00 2001 From: Alois Date: Thu, 2 Jul 2026 23:58:40 +0200 Subject: [PATCH 2/3] (feat): add descriptor to wasm & ts-sdk --- src/sdk/index.ts | 7 +++++++ wasm/types/mtp_wasm.d.ts | 1 + 2 files changed, 8 insertions(+) diff --git a/src/sdk/index.ts b/src/sdk/index.ts index 3e3d077..9038e06 100644 --- a/src/sdk/index.ts +++ b/src/sdk/index.ts @@ -93,6 +93,7 @@ export interface MTPClientCredentials { export interface MTPClientOptions { url: string; + descriptor?: string; hostPublicKey?: MTPBytesInput | string; credentials?: MTPCredentials | string | null; credentialsStorageKey?: string; @@ -296,6 +297,9 @@ function validateOptions(options) { 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"); + } if (options.storage) { for (const method of ["getItem", "setItem", "removeItem"]) { if (typeof options.storage[method] !== "function") { @@ -427,6 +431,9 @@ export class MTPClient { if (this.#options.maxMessageSize != null) { config.max_message_size = this.#options.maxMessageSize; } + if (this.#options.descriptor != null) { + config.description = this.#options.descriptor; + } return config; } diff --git a/wasm/types/mtp_wasm.d.ts b/wasm/types/mtp_wasm.d.ts index 4ecec6b..b7ac775 100644 --- a/wasm/types/mtp_wasm.d.ts +++ b/wasm/types/mtp_wasm.d.ts @@ -48,6 +48,7 @@ export class ConnectionConfig implements DisposableWasmObject { free(): void; [Symbol.dispose](): void; client_id: bigint; + description: string | undefined; max_message_size: number; server_certificate_hashes: string[]; readonly url: string; From 8dc8b54b26544554f98d7af32200d6bf9a01d300 Mon Sep 17 00:00:00 2001 From: Alois Date: Fri, 3 Jul 2026 14:45:55 +0200 Subject: [PATCH 3/3] (feat): add codec helpers to ts-sdk --- README.md | 9 +++++++++ docs/WASM-CLIENT.md | 10 ++++++++++ src/sdk/index.ts | 32 ++++++++++++++++++++++++++++++++ 3 files changed, 51 insertions(+) diff --git a/README.md b/README.md index 265f7b3..048b882 100644 --- a/README.md +++ b/README.md @@ -72,6 +72,15 @@ console.log("Connected MTP client", clientId, client.state); Use `MTPClient.crypto` for SDK-level crypto helpers such as `generateKeyring()`, `generateEd25519()`, `keyringFromEd25519()`, `verifyEd25519()`, `sha256()`, `sha256Double()`, `hkdfExpand()`, and `deriveEncryptionKey()`. +Use `codec` to encode and decode MTP frames from the main SDK export: + +```typescript +import { codec } from "mtp"; + +const frame = codec.encode("SomeType", { value: "hello" }); +const parsed = codec.decode(frame); +``` + ## Getting Started Add the `mtp` crate with your desired features: diff --git a/docs/WASM-CLIENT.md b/docs/WASM-CLIENT.md index b70ab3a..3cd4699 100644 --- a/docs/WASM-CLIENT.md +++ b/docs/WASM-CLIENT.md @@ -258,6 +258,16 @@ Raw message helpers that remain available include: - `format_frame(frame)` - `parse_auth_response(frame)` +The SDK export also exposes the same frame codec through `codec`: + +```typescript +import { codec } from "mtp"; + +const frame = codec.encode("SomeType", { value: "hello" }); +const parsed = codec.decode(frame); +const display = codec.format(frame); +``` + Raw crypto and key helpers include: - `ed25519_generate()` diff --git a/src/sdk/index.ts b/src/sdk/index.ts index 9038e06..eabcc8a 100644 --- a/src/sdk/index.ts +++ b/src/sdk/index.ts @@ -75,6 +75,36 @@ export interface MTPRaw { export type MTPBytesInput = Uint8Array | number[]; +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 function encode(type: MTPCommunicationType, data: Record, options?: MTPCodecOptions): Uint8Array { + return bindings.build_frame(type, data, options ?? {}); +} + +export function decode(frame: MTPBytesInput): ParsedFrame { + return bindings.parse_frame(bytesFrom(frame, "frame")); +} + +export function format(frame: MTPBytesInput): string { + return bindings.format_frame(bytesFrom(frame, "frame")); +} + +export const codec: MTPCodec = { + encode, + decode, + format, +}; + export interface MTPCredentials { clientId: bigint | string | number | null; keyring: MTPBytesInput; @@ -335,12 +365,14 @@ async function withTimeout(promise, timeoutMs, message) { export class MTPClient { static readonly crypto = crypto; + static readonly codec = codec; #credentials: InternalCredentials | null; #options: NormalizedMTPClientOptions; readonly raw: MTPRaw; readonly crypto = MTPClient.crypto; + readonly codec = MTPClient.codec; private constructor(options: NormalizedMTPClientOptions, client: RawBindings.WasmClient) { this.#options = options;