ping-openmls-sdk 0.7.4 → 0.7.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs CHANGED
@@ -791,6 +791,15 @@ var MessagingClient = class _MessagingClient {
791
791
  nowMs: self.now()
792
792
  });
793
793
  },
794
+ async setConversationName(name) {
795
+ await self.call({
796
+ kind: "setConversationName",
797
+ id: self.nextId++,
798
+ conv: id,
799
+ name,
800
+ nowMs: self.now()
801
+ });
802
+ },
794
803
  meta() {
795
804
  const cached = self.metaCache.get(hex(id));
796
805
  if (!cached) throw new Error("conversation meta not loaded; call listConversations() first");
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/index.ts","../src/storage/indexeddb.ts","../src/wire-contract.ts","../src/transport/websocket.ts"],"sourcesContent":["// Public entry point. Spawns the dedicated Worker that hosts the WASM instance and exposes a\n// Promise-based, identical-to-native API. Crypto runs off the main thread.\n\nimport type {\n AdmitChatEntry, AdmitChatOutcome,\n CatchupAppEvent, CatchupSnapshotView,\n ConversationId, ConversationMeta, DeviceId, IncomingMessage,\n HistoryBundle, MemberInfo,\n KeyPackageEntry, LinkingTicket, MessageEnvelope, RecoveryBackup, Storage, Transport, UserId,\n} from \"./types\";\nimport type { Request, Response } from \"./protocol\";\n\nexport type * from \"./types\";\n\n// Re-export the default Storage and Transport implementations from the main entry so consumers\n// whose bundlers don't honor package.json#exports (Metro with strict resolution, some legacy\n// Webpack configs) can still get them with a plain `import { ... } from \"ping-openmls-sdk\"`. Power\n// users who care about tree-shaking can keep using the subpath imports.\nexport { IndexedDbStorage } from \"./storage/indexeddb\";\nexport { WebSocketTransport } from \"./transport/websocket\";\n\n// Ping wire contract — version, content-type constants, and runtime validators. Internal\n// products MUST claim conformance; external consumers MAY adopt or run their own profile.\n// See docs/PING_WIRE_CONTRACT.md.\nexport {\n PING_WIRE_CONTRACT_VERSION,\n PING_WIRE_CONTRACT_HEADER,\n PING_WIRE_CONTRACT_HEADER_VALUE,\n WIRE_VERSION_MIN_ACCEPTED,\n CONTENT_TYPE_CBOR,\n CONTENT_TYPE_JSON,\n CONTENT_TYPE_CBOR_LEGACY_V1,\n CONTENT_TYPE_JSON_LEGACY_V1,\n ValidationError,\n validate,\n verifyApplicationContentHash,\n negotiates,\n} from \"./wire-contract\";\n\nexport interface ClientConfig {\n identityExport: Uint8Array;\n deviceLabel: string;\n storage: Storage;\n transport: Transport;\n /** Override the wall clock (test/sim only). */\n now?: () => number;\n /**\n * Optional 32-byte Ed25519 secret key. When supplied AND no\n * `LocalDevice` is yet persisted in `storage`, the SDK adopts this\n * as its device signing key — so `deviceId()` returns\n * `SHA-256(public_key_of(secret))`, deterministic from what the\n * caller passed.\n *\n * Use case: align the SDK's `device_id` (which the SDK stamps into\n * every envelope's `sender_device` field) with an externally-\n * computed device id — typically `SHA-256(device_signing_pubkey)`\n * in the host's auth layer, where the JWT carries that same value\n * as its `device_id` claim. Without this alignment, a server that\n * validates `envelope.sender_device == jwt.device_id` rejects every\n * upload with `sender_device_mismatch`.\n *\n * Ignored on subsequent inits (when `storage` already holds a\n * persisted `LocalDevice`) — the on-disk identity is authoritative\n * for stability across restarts.\n */\n deviceSigningSecretKey?: Uint8Array;\n}\n\nexport interface Conversation {\n readonly id: ConversationId;\n send(plaintext: Uint8Array): Promise<MessageEnvelope>;\n /** [CR-2] Add members by (device_id, KeyPackage) pair. Hosts typically pull these\n * straight from `transport.discoverDevices(userId)`. */\n addMembers(entries: KeyPackageEntry[]): Promise<void>;\n removeMembers(leafIndexes: number[]): Promise<void>;\n meta(): ConversationMeta;\n}\n\nexport class MessagingClient {\n private worker: Worker;\n private nextId = 1;\n private pending = new Map<number, { resolve: (v: unknown) => void; reject: (e: Error) => void }>();\n private storage: Storage;\n private transport: Transport;\n private now: () => number;\n private messageHandlers = new Set<(m: IncomingMessage) => void>();\n private conversationHandlers = new Set<(id: ConversationId, epoch: number, sender?: DeviceId) => void>();\n private subscription: { unsubscribe(): void } | null = null;\n private metaCache = new Map<string, ConversationMeta>();\n // Cached at init() so dispatchIncoming can skip envelopes we sent ourselves — the relay\n // broadcasts to all WS subscribers including the sender, and re-applying our own already-\n // merged commits would error with \"epoch differs\".\n private ownDeviceId: Uint8Array | null = null;\n\n private constructor(cfg: ClientConfig, worker: Worker) {\n this.worker = worker;\n this.storage = cfg.storage;\n this.transport = cfg.transport;\n this.now = cfg.now ?? (() => Date.now());\n this.worker.onmessage = (ev) => this.handleWorker(ev.data as Response);\n }\n\n /** Generate a fresh identity. The returned bytes are a secret — store them encrypted. */\n static async generateIdentity(): Promise<Uint8Array> {\n return MessagingClient.ephemeralWorkerCall<Uint8Array>({\n kind: \"generateIdentity\", id: 1,\n });\n }\n\n /**\n * HPKE-seal a `LinkingTicket` for delivery to a new device ([CR-3]).\n *\n * Returns CBOR-encoded bytes safe to relay through an untrusted inbox. The new device's\n * X25519 public key (`newDevicePub`) is exchanged out-of-band — typically as part of a\n * QR-encoded handshake on the linking screen. Pure function; runs in an ephemeral\n * worker so no live `MessagingClient` is required on the sender side.\n *\n * Suite: `DHKEM(X25519, HKDF-SHA256), HKDF-SHA256, AES-128-GCM` (matches MLS).\n */\n static async sealLinkingTicket(\n ticket: LinkingTicket,\n newDevicePub: Uint8Array,\n ): Promise<Uint8Array> {\n if (newDevicePub.length !== 32) {\n throw new Error(\"sealLinkingTicket: newDevicePub must be 32 bytes\");\n }\n return MessagingClient.ephemeralWorkerCall<Uint8Array>({\n kind: \"sealLinkingTicket\", id: 1, ticket, newDevicePub,\n });\n }\n\n /**\n * HPKE-open a sealed `LinkingTicket` on the new device ([CR-3]).\n *\n * The receiving device hasn't booted a `MessagingClient` yet — it only has its X25519\n * ephemeral keypair. This static method runs the open in an ephemeral worker, returning\n * the cleartext ticket which the host then feeds to `init()` + `consumeLinkingTicket()`.\n *\n * On failure the error message is deliberately generic (no discriminator between \"wrong\n * key\" vs \"tampered ciphertext\") so a probing attacker can't tell the new device's key\n * was the issue.\n */\n static async openLinkingTicket(\n sealed: Uint8Array,\n newDevicePriv: Uint8Array,\n ): Promise<LinkingTicket> {\n if (newDevicePriv.length !== 32) {\n throw new Error(\"openLinkingTicket: newDevicePriv must be 32 bytes\");\n }\n return MessagingClient.ephemeralWorkerCall<LinkingTicket>({\n kind: \"openLinkingTicket\", id: 1, sealed, newDevicePriv,\n });\n }\n\n /**\n * Generic HPKE-Base seal of arbitrary bytes to `recipientPub` (32-byte X25519)\n * under a caller-supplied `info`. One shared HPKE implementation so app layers\n * (e.g. the device-linking auth envelope) stop hand-rolling per-platform HPKE\n * that drifts and breaks cross-platform. Runs in an ephemeral worker.\n */\n static async hpkeSeal(\n plaintext: Uint8Array,\n recipientPub: Uint8Array,\n info: Uint8Array,\n ): Promise<Uint8Array> {\n if (recipientPub.length !== 32) {\n throw new Error(\"hpkeSeal: recipientPub must be 32 bytes\");\n }\n return MessagingClient.ephemeralWorkerCall<Uint8Array>({\n kind: \"hpkeSeal\", id: 1, plaintext, recipientPub, info,\n });\n }\n\n /** Generic HPKE-Base open — inverse of {@link hpkeSeal}. `info` must match the sender. */\n static async hpkeOpen(\n sealed: Uint8Array,\n recipientPriv: Uint8Array,\n info: Uint8Array,\n ): Promise<Uint8Array> {\n if (recipientPriv.length !== 32) {\n throw new Error(\"hpkeOpen: recipientPriv must be 32 bytes\");\n }\n return MessagingClient.ephemeralWorkerCall<Uint8Array>({\n kind: \"hpkeOpen\", id: 1, sealed, recipientPriv, info,\n });\n }\n\n // Spawn-call-terminate helper for stateless WASM functions exposed at module scope (no\n // PingClient required). Each call gets a fresh worker; the cost is acceptable because\n // these are one-shot, off-the-hot-path operations (identity gen, linking ticket seal/open).\n private static ephemeralWorkerCall<T>(req: Request): Promise<T> {\n const w = new Worker(new URL(\"./worker.js\", import.meta.url), { type: \"module\" });\n return new Promise<T>((resolve, reject) => {\n w.onmessage = (ev) => {\n const msg = ev.data as Response;\n if (msg.kind === \"result\" && msg.id === (req as { id: number }).id) {\n w.terminate();\n msg.ok ? resolve(msg.value as T) : reject(new Error(msg.error));\n }\n };\n w.postMessage(req);\n });\n }\n\n static async init(cfg: ClientConfig): Promise<MessagingClient> {\n const worker = new Worker(new URL(\"./worker.js\", import.meta.url), { type: \"module\" });\n const client = new MessagingClient(cfg, worker);\n\n await client.call({\n kind: \"init\",\n id: client.nextId++,\n identityExport: cfg.identityExport,\n deviceLabel: cfg.deviceLabel,\n nowMs: client.now(),\n ...(cfg.deviceSigningSecretKey\n ? { deviceSigningSecretKey: cfg.deviceSigningSecretKey }\n : {}),\n });\n\n // Cache the device id once so we can drop envelopes we sent ourselves on the WS echo.\n client.ownDeviceId = await client.deviceId();\n\n // Start the live subscription. We dispatch each envelope to the right handler:\n // - Welcome envelopes for unknown conversations → joinConversation\n // - Welcomes for already-joined conversations → drop (server may broadcast duplicates)\n // - Welcomes addressed to other devices → drop (we can't decrypt)\n // - Everything else → processEnvelope\n client.subscription = cfg.transport.subscribe((envelope) => {\n void client.dispatchIncoming(envelope);\n });\n return client;\n }\n\n /** Internal: route an inbound envelope to the right handler. */\n private async dispatchIncoming(envelope: MessageEnvelope): Promise<void> {\n // The relay broadcasts every envelope to every WS subscriber, including the sender. For\n // our own sends, the local MLS state has already been advanced in-process — re-applying\n // would either fail with \"epoch differs\" (commits) or be a no-op dedupe (application\n // messages). Skip them here.\n if (this.ownDeviceId && sameBytesU8(envelope.sender_device, this.ownDeviceId)) {\n return;\n }\n if (envelope.kind === \"Welcome\") {\n try {\n const known = await this.listConversations();\n const already = known.some((m) => sameBytesU8(m.id, envelope.conversation_id));\n if (already) return;\n await this.joinConversation(envelope);\n } catch (e) {\n // Welcomes are broadcast to every connected client; only one device's KeyPackage can\n // decrypt each one, so most fail. Log at debug, drop silently.\n if (typeof console !== \"undefined\") console.debug(\"[ping-sdk] welcome ignored:\", e);\n }\n return;\n }\n try {\n await this.processEnvelope(envelope);\n } catch (e) {\n if (typeof console !== \"undefined\") console.debug(\"[ping-sdk] envelope drop:\", e);\n }\n }\n\n // ------------------------ Public API ------------------------\n\n async userId(): Promise<UserId> {\n return await this.call({ kind: \"userId\", id: this.nextId++ }) as UserId;\n }\n\n /**\n * Export the account identity seed (CBOR-wrapped Ed25519, **SECRET**).\n * Transfer to a newly-linked device over the sealed linking channel so all of\n * an account's devices share one `userId` — the basis for cross-device\n * self-attribution (`IncomingMessage.sender_user_id === userId()`). Never log\n * or persist in cleartext.\n */\n async exportIdentity(): Promise<Uint8Array> {\n return await this.call({ kind: \"exportIdentity\", id: this.nextId++ }) as Uint8Array;\n }\n\n async deviceId(): Promise<DeviceId> {\n return await this.call({ kind: \"deviceId\", id: this.nextId++ }) as DeviceId;\n }\n\n async freshKeyPackage(): Promise<Uint8Array> {\n return await this.call({ kind: \"freshKeyPackage\", id: this.nextId++ }) as Uint8Array;\n }\n\n async createConversation(opts: { name?: string } = {}): Promise<Conversation> {\n const idBytes = await this.call({\n kind: \"createConversation\", id: this.nextId++, name: opts.name ?? null, nowMs: this.now(),\n }) as ConversationId;\n return this.conversationProxy(idBytes);\n }\n\n async joinConversation(welcome: MessageEnvelope): Promise<Conversation> {\n const idBytes = await this.call({\n kind: \"joinConversation\", id: this.nextId++, welcome, nowMs: this.now(),\n }) as ConversationId;\n return this.conversationProxy(idBytes);\n }\n\n /**\n * Get a `Conversation` proxy for an already-known conversation. Use this when you have a\n * conversation id (e.g., from `listConversations()`) and want to send/addMembers without\n * re-creating it. Throws if the id isn't known to the worker — call `listConversations()`\n * first to verify membership.\n */\n getConversation(id: ConversationId): Conversation {\n return this.conversationProxy(id);\n }\n\n async listConversations(): Promise<ConversationMeta[]> {\n const metas = await this.call({ kind: \"listConversations\", id: this.nextId++ }) as ConversationMeta[];\n this.metaCache.clear();\n for (const m of metas) this.metaCache.set(hex(m.id), m);\n return metas;\n }\n\n /**\n * Member roster for a conversation, recovered locally from the MLS\n * group's leaf credentials (no network, no out-of-band message). Returns\n * one entry per device leaf — a multi-device user appears multiple times,\n * so dedup by `user_id` for a per-user roster. Empty for an unknown id.\n */\n async members(conv: ConversationId): Promise<MemberInfo[]> {\n return await this.call({ kind: \"members\", id: this.nextId++, conv }) as MemberInfo[];\n }\n\n async syncConversations(): Promise<IncomingMessage[]> {\n return await this.call({\n kind: \"syncConversations\", id: this.nextId++, nowMs: this.now(),\n }) as IncomingMessage[];\n }\n\n async processEnvelope(envelope: MessageEnvelope): Promise<IncomingMessage | null> {\n return await this.call({\n kind: \"processEnvelope\", id: this.nextId++, envelope, nowMs: this.now(),\n }) as IncomingMessage | null;\n }\n\n /**\n * Build a `LinkingTicket` for a new device.\n *\n * [CR-13] `lastAppEvents` is host-supplied per-conversation \"what you missed\" data\n * (decrypted AppEvent bytes the new device will render before sync catches up).\n * Pass `[]` to suppress catchup data — the new device will see an empty conversation\n * list until normal sync runs.\n *\n * Caller is responsible for HPKE-sealing the returned ticket via\n * `MessagingClient.sealLinkingTicket` before transmitting (CR-3).\n */\n async buildLinkingTicket(\n newDeviceId: DeviceId,\n newDeviceKp: Uint8Array,\n lastAppEvents: CatchupAppEvent[] = [],\n ): Promise<LinkingTicket> {\n return await this.call({\n kind: \"buildLinkingTicket\", id: this.nextId++,\n newDeviceId, newDeviceKp, lastAppEvents, nowMs: this.now(),\n }) as LinkingTicket;\n }\n\n /**\n * Decode a `LinkingTicket.catchup_snapshot` blob ([CR-13]).\n *\n * Pure WASM — runs in an ephemeral worker so the new device can call this before\n * `MessagingClient.init()` completes (just like `openLinkingTicket`). Returns the\n * structured view: per-conversation metas + last-known AppEvent bytes.\n *\n * Throws on malformed or oversized inputs. Empty bytes return `null` — that's the\n * \"no catchup data\" case (sender passed `[]` to buildLinkingTicket).\n */\n static async decodeCatchupSnapshot(\n snapshotBytes: Uint8Array,\n ): Promise<CatchupSnapshotView | null> {\n if (snapshotBytes.length === 0) return null;\n return MessagingClient.ephemeralWorkerCall<CatchupSnapshotView>({\n kind: \"decodeCatchupSnapshot\", id: 1, snapshotBytes,\n });\n }\n\n async consumeLinkingTicket(ticket: LinkingTicket): Promise<void> {\n await this.call({\n kind: \"consumeLinkingTicket\", id: this.nextId++, ticket, nowMs: this.now(),\n });\n }\n\n // ------------------------ Recovery (account backup) ------------------------\n // All static + stateless: they run on an ephemeral worker, so the recovery screen can\n // call them before any client is initialised. Same Argon2id/AES-256-GCM IdentityBackup\n // wire format as the iOS/Android FFI bindings — backups are portable across platforms.\n\n /** Generate a fresh 12-word BIP39 mnemonic, returned as its canonical phrase string. */\n static async generateMnemonicPhrase(): Promise<string> {\n return MessagingClient.ephemeralWorkerCall<string>({\n kind: \"generateMnemonicPhrase\", id: 1,\n });\n }\n\n /** Validate + canonicalise a user-entered phrase (BIP39 wordlist + checksum). Rejects\n * malformed input. Use on the recovery screen before calling `decryptBackup`. */\n static async normalizeMnemonicPhrase(phrase: string): Promise<string> {\n return MessagingClient.ephemeralWorkerCall<string>({\n kind: \"normalizeMnemonicPhrase\", id: 1, phrase,\n });\n }\n\n /** Encrypt an IdentityBackup for `POST /v1/recovery/blobs`. `deviceGroupSnapshot` may be\n * empty (Conservative path) or the DeviceGroup's `exportConversationStateSnapshot`. */\n static async encryptBackup(args: {\n mnemonicPhrase: string;\n accountId: Uint8Array;\n identityExport: Uint8Array;\n deviceGroupSnapshot: Uint8Array;\n /** External conversations to embed so recovery restores them offline. Pass\n * `[]` (the default) for identity + DeviceGroup only. Each `groupStateBytes`\n * is `Conversation.exportConversationStateSnapshot` output. Forward-secrecy\n * trade-off — see the SDK recovery docs. */\n conversationSnapshots?: { conversationId: Uint8Array; groupStateBytes: Uint8Array }[];\n createdAtMs: number;\n }): Promise<Uint8Array> {\n return MessagingClient.ephemeralWorkerCall<Uint8Array>({\n kind: \"encryptBackup\", id: 1,\n mnemonicPhrase: args.mnemonicPhrase,\n accountId: args.accountId,\n identityExport: args.identityExport,\n deviceGroupSnapshot: args.deviceGroupSnapshot,\n conversationSnapshots: args.conversationSnapshots ?? [],\n createdAtMs: args.createdAtMs,\n });\n }\n\n /** Decrypt a recovery blob from `GET /v1/recovery/blobs`. Throws on wrong mnemonic or a\n * tampered blob. The returned `identity_export` is the secret seed for `init`. */\n static async decryptBackup(\n blob: Uint8Array,\n mnemonicPhrase: string,\n ): Promise<RecoveryBackup> {\n return MessagingClient.ephemeralWorkerCall<RecoveryBackup>({\n kind: \"decryptBackup\", id: 1, blob, mnemonicPhrase,\n });\n }\n\n // ------------------------ Peer-assisted history transfer ------------------------\n // Authenticated (sign-then-encrypt) device-to-device history used by linking and\n // post-recovery peer re-add. Confidential to the recipient's ephemeral X25519 key AND\n // signed by the account identity key, so the receiver proves the sender. Plaintext-only\n // share — preserves forward secrecy (no MLS keys leave the device).\n\n /** Seal+sign a `HistoryBundle` for a recipient device. `recipientPub` (X25519, 32B) is the\n * recipient's ephemeral pubkey from the linking QR; `senderIdentitySecret` (Ed25519 seed,\n * 32B) is this account's identity key. Returns the blob to POST to /v1/history-transfers. */\n static async sealHistoryBundle(\n bundle: HistoryBundle,\n recipientPub: Uint8Array,\n senderIdentitySecret: Uint8Array,\n ): Promise<Uint8Array> {\n return MessagingClient.ephemeralWorkerCall<Uint8Array>({\n kind: \"sealHistoryBundle\", id: 1, bundle, recipientPub, senderIdentitySecret,\n });\n }\n\n /** Open+verify a sealed history bundle. `recipientPriv` (X25519, 32B) is this device's\n * ephemeral private key; `senderIdentityPub` (Ed25519, 32B) is the account identity pubkey\n * from the linking handshake. Throws if the signature doesn't verify. */\n static async openHistoryBundle(\n sealed: Uint8Array,\n recipientPriv: Uint8Array,\n senderIdentityPub: Uint8Array,\n ): Promise<HistoryBundle> {\n return MessagingClient.ephemeralWorkerCall<HistoryBundle>({\n kind: \"openHistoryBundle\", id: 1, sealed, recipientPriv, senderIdentityPub,\n });\n }\n\n /**\n * Admit a freshly-linked device to every chat in `entries` — one MLS\n * Commit + Welcome per chat, with the Welcome's `?recipient=` priming\n * handled inside the SDK. This is the post-link reconciliation step\n * that follows `consumeLinkingTicket`: the new device has the\n * DeviceGroup (from the ticket) and metadata for external chats (from\n * the catchup snapshot), but its leaf is not yet in any external\n * MLS group. The host on the EXISTING device calls this once the\n * new device shows up in `/v1/auth/sessions` and has uploaded its\n * KeyPackage batch, passing one freshly-claimed KP per chat.\n *\n * Per-chat failures (stale KP, transport error, unknown chat) are\n * captured in the returned outcomes rather than aborting — one bad\n * chat shouldn't lose the user access to all the others.\n */\n async admitDeviceToChats(\n deviceId: DeviceId,\n entries: AdmitChatEntry[],\n ): Promise<AdmitChatOutcome[]> {\n return await this.call({\n kind: \"admitDeviceToChats\", id: this.nextId++,\n deviceId, entries, nowMs: this.now(),\n }) as AdmitChatOutcome[];\n }\n\n /**\n * Revoke a device ([CR-2]).\n *\n * Removes `deviceId`'s leaf from every conversation where this client locally\n * tracked the device→leaf mapping (i.e. conversations where this client itself\n * admitted the device via `addMembers`, or where this client is the device being\n * revoked). The SDK has already broadcast each Commit envelope via\n * `transport.send`; the returned array is for any additional host-side handling\n * (audit logs, UI). An empty array is a valid outcome — the device wasn't locally\n * known anywhere.\n *\n * **Scope note.** Conversations where a peer admitted the device on this client's\n * behalf are silently skipped — the leaf index isn't locally known. Host can fall\n * back to `transport.discoverDevices` + a manual `remove_members(leafIndex)` for\n * those, or wait for the affected user to revoke from a device that did the admit.\n * See `docs/architecture/multi-device.md §Device removal`.\n */\n async revokeDevice(deviceId: DeviceId): Promise<MessageEnvelope[]> {\n return await this.call({\n kind: \"revokeDevice\", id: this.nextId++, deviceId, nowMs: this.now(),\n }) as MessageEnvelope[];\n }\n\n /**\n * Export a portable MLS state snapshot for one conversation ([CR-7]).\n *\n * Returned bytes can be embedded in a recovery blob or shipped through a\n * linking ticket so another device of the same user identity can re-attach to\n * the group via [[importStateSnapshot]]. The bytes contain past epoch secrets;\n * never log, never persist unencrypted, wipe (`.fill(0)`) after use.\n */\n async exportConversationStateSnapshot(conv: ConversationId): Promise<Uint8Array> {\n return await this.call({\n kind: \"exportConversationStateSnapshot\", id: this.nextId++,\n conv, nowMs: this.now(),\n }) as Uint8Array;\n }\n\n /**\n * Import a `GroupStateSnapshot` from another device of the same user identity\n * ([CR-7]).\n *\n * On success, the conversation appears in [[listConversations]] and decryption\n * of subsequent peer-originated traffic works against the imported state. For\n * full multi-device participation, the recovered device SHOULD then issue an\n * MLS Update Proposal to rotate onto a fresh leaf (post-v1 follow-up — see\n * `docs/design/CR4_CR7_PERSISTENCE.md` open question #3).\n */\n async importStateSnapshot(snapshotBytes: Uint8Array): Promise<ConversationId> {\n return await this.call({\n kind: \"importStateSnapshot\", id: this.nextId++,\n snapshotBytes, nowMs: this.now(),\n }) as ConversationId;\n }\n\n /**\n * Export a derived secret from a conversation's MLS exporter ([CR-8]).\n *\n * Used to seed the ephemeral channel (`ping/ephemeral`), call-media keys\n * (`ping/calls/media/<call_id>`), and call-ephemeral framer keys\n * (`ping/calls/ephemeral/<call_id>`). The returned bytes are a secret — never log,\n * persist only encrypted, and call `bytes.fill(0)` after use to wipe the typed array.\n *\n * Same `(conversationId, current_epoch, label, context, length)` produces the same\n * bytes across every binding (Rust core, native UniFFI, WASM/JS). Cross-platform\n * byte-equality is enforced by `protocol/conformance/export_secret/`.\n */\n async exportConversationSecret(\n conv: ConversationId,\n label: string,\n context: Uint8Array,\n length: number,\n ): Promise<Uint8Array> {\n if (length <= 0 || length > 1024) {\n throw new Error(\"exportConversationSecret: length must be 1..=1024\");\n }\n return await this.call({\n kind: \"exportConversationSecret\", id: this.nextId++,\n conv, label, context, length,\n }) as Uint8Array;\n }\n\n /** Subscribe to decrypted application messages. */\n onMessage(handler: (m: IncomingMessage) => void): () => void {\n this.messageHandlers.add(handler);\n return () => this.messageHandlers.delete(handler);\n }\n\n /** Fired after every state-changing event for a conversation (Commit, member changes). */\n /**\n * Fires after every conversation state change — joining via Welcome, processing a Commit,\n * etc. `sender` carries the device id of whoever triggered the event when known (e.g. the\n * inviter's device on auto-join), so the host can label peers in its UI.\n */\n onConversationUpdated(handler: (id: ConversationId, epoch: number, sender?: DeviceId) => void): () => void {\n this.conversationHandlers.add(handler);\n return () => this.conversationHandlers.delete(handler);\n }\n\n async close(): Promise<void> {\n this.subscription?.unsubscribe();\n this.worker.terminate();\n }\n\n // ------------------------ Internals ------------------------\n\n private conversationProxy(id: ConversationId): Conversation {\n const self = this;\n return {\n id,\n async send(plaintext) {\n return await self.call({\n kind: \"send\", id: self.nextId++, conv: id, plaintext, nowMs: self.now(),\n }) as MessageEnvelope;\n },\n async addMembers(entries) {\n await self.call({\n kind: \"addMembers\", id: self.nextId++, conv: id, entries, nowMs: self.now(),\n });\n },\n async removeMembers(leaves) {\n await self.call({\n kind: \"removeMembers\", id: self.nextId++, conv: id, leaves, nowMs: self.now(),\n });\n },\n meta() {\n const cached = self.metaCache.get(hex(id));\n if (!cached) throw new Error(\"conversation meta not loaded; call listConversations() first\");\n return cached;\n },\n };\n }\n\n private call(req: Request): Promise<unknown> {\n return new Promise((resolve, reject) => {\n this.pending.set((req as { id: number }).id, { resolve, reject });\n this.worker.postMessage(req);\n });\n }\n\n private async handleWorker(msg: Response) {\n switch (msg.kind) {\n case \"result\": {\n const slot = this.pending.get(msg.id);\n if (!slot) return;\n this.pending.delete(msg.id);\n msg.ok ? slot.resolve(msg.value) : slot.reject(new Error(msg.error));\n return;\n }\n case \"event.message\": {\n for (const h of this.messageHandlers) h(msg.msg);\n return;\n }\n case \"event.conversation\": {\n for (const h of this.conversationHandlers) h(msg.id, msg.epoch, msg.sender);\n return;\n }\n case \"storage.call\": {\n try {\n const v = await (this.storage as unknown as Record<string, (...a: unknown[]) => unknown>)\n [msg.method]?.(...msg.args);\n this.worker.postMessage({ kind: \"storage.reply\", id: msg.id, ok: true, value: v } as Request);\n } catch (e) {\n this.worker.postMessage({\n kind: \"storage.reply\", id: msg.id, ok: false, error: String(e),\n } as Request);\n }\n return;\n }\n case \"transport.call\": {\n try {\n const v = await (this.transport as unknown as Record<string, (...a: unknown[]) => unknown>)\n [msg.method]?.(...msg.args);\n this.worker.postMessage({ kind: \"transport.reply\", id: msg.id, ok: true, value: v } as Request);\n } catch (e) {\n this.worker.postMessage({\n kind: \"transport.reply\", id: msg.id, ok: false, error: String(e),\n } as Request);\n }\n return;\n }\n }\n }\n}\n\nfunction hex(b: Uint8Array): string {\n let s = \"\";\n for (const v of b) s += v.toString(16).padStart(2, \"0\");\n return s;\n}\n\nfunction sameBytesU8(a: Uint8Array, b: Uint8Array): boolean {\n if (a.length !== b.length) return false;\n for (let i = 0; i < a.length; i++) if (a[i] !== b[i]) return false;\n return true;\n}\n","// Default IndexedDB-backed Storage implementation. Hosts can supply their own.\n\nimport type { Storage } from \"../types\";\n\nconst DB_NAME = \"ping-sdk\";\nconst DB_VERSION = 1;\nconst STORE = \"kv\";\n\ninterface Row { ns: string; key: string; value: Uint8Array }\n\nfunction open(): Promise<IDBDatabase> {\n return new Promise((resolve, reject) => {\n const req = indexedDB.open(DB_NAME, DB_VERSION);\n req.onupgradeneeded = () => {\n const db = req.result;\n if (!db.objectStoreNames.contains(STORE)) {\n const os = db.createObjectStore(STORE, { keyPath: [\"ns\", \"key\"] });\n os.createIndex(\"ns\", \"ns\", { unique: false });\n }\n };\n req.onsuccess = () => resolve(req.result);\n req.onerror = () => reject(req.error);\n });\n}\n\nexport class IndexedDbStorage implements Storage {\n private dbPromise: Promise<IDBDatabase>;\n\n constructor() { this.dbPromise = open(); }\n\n async get(namespace: string, key: string): Promise<Uint8Array | null> {\n const db = await this.dbPromise;\n return new Promise((resolve, reject) => {\n const tx = db.transaction(STORE, \"readonly\");\n const r = tx.objectStore(STORE).get([namespace, key]);\n r.onsuccess = () => resolve(((r.result as Row | undefined)?.value) ?? null);\n r.onerror = () => reject(r.error);\n });\n }\n\n async put(namespace: string, key: string, value: Uint8Array): Promise<void> {\n const db = await this.dbPromise;\n return new Promise((resolve, reject) => {\n const tx = db.transaction(STORE, \"readwrite\");\n tx.objectStore(STORE).put({ ns: namespace, key, value } as Row);\n tx.oncomplete = () => resolve();\n tx.onerror = () => reject(tx.error);\n });\n }\n\n async del(namespace: string, key: string): Promise<void> {\n const db = await this.dbPromise;\n return new Promise((resolve, reject) => {\n const tx = db.transaction(STORE, \"readwrite\");\n tx.objectStore(STORE).delete([namespace, key]);\n tx.oncomplete = () => resolve();\n tx.onerror = () => reject(tx.error);\n });\n }\n\n async listKeys(namespace: string, prefix: string): Promise<string[]> {\n const db = await this.dbPromise;\n return new Promise((resolve, reject) => {\n const tx = db.transaction(STORE, \"readonly\");\n const idx = tx.objectStore(STORE).index(\"ns\");\n const req = idx.openCursor(IDBKeyRange.only(namespace));\n const out: string[] = [];\n req.onsuccess = () => {\n const cur = req.result;\n if (!cur) return resolve(out);\n const row = cur.value as Row;\n if (!prefix || row.key.startsWith(prefix)) out.push(row.key);\n cur.continue();\n };\n req.onerror = () => reject(req.error);\n });\n }\n}\n","// Ping wire contract — TypeScript counterpart to the `ping-wire-contract` Rust crate.\n//\n// MUST stay in lockstep with `core/ping-wire-contract/src/lib.rs`. The full normative spec\n// lives in `docs/PING_WIRE_CONTRACT.md`.\n//\n// What this module is for: code paths that cross the network boundary (transports, relays,\n// service workers) that need to advertise or validate ping-wire-contract conformance from\n// JavaScript without round-tripping through WASM.\n\nimport type { MessageEnvelope } from \"./types\";\n\n/**\n * Ping wire contract version. Bumped on incompatible profile changes.\n *\n * Currently `2` — CR-6 changes the `content_hash` semantics for `Application` envelopes\n * (now hashed over plaintext, not the MLS ciphertext). Receivers run a 90-day dual-accept\n * window per Q-ARCH-02; see [[validate]] for the per-version routing.\n */\nexport const PING_WIRE_CONTRACT_VERSION = 2;\n\n/** Lowest envelope version accepted by [[validate]] during the dual-accept window. */\nexport const WIRE_VERSION_MIN_ACCEPTED = 1;\n\n/** Canonical CBOR content type. Production transports SHOULD use this. */\nexport const CONTENT_TYPE_CBOR = \"application/vnd.ping.wire.v2+cbor\";\n\n/** JSON projection — development and tooling only. Production MUST NOT use this. */\nexport const CONTENT_TYPE_JSON = \"application/vnd.ping.wire.v2+json\";\n\n/** Legacy v1 CBOR type, accepted during the CR-6 dual-accept window. */\nexport const CONTENT_TYPE_CBOR_LEGACY_V1 = \"application/vnd.ping.wire.v1+cbor\";\n\n/** Legacy v1 JSON type, accepted during the CR-6 dual-accept window. */\nexport const CONTENT_TYPE_JSON_LEGACY_V1 = \"application/vnd.ping.wire.v1+json\";\n\n/** Header used over transports that don't surface a content type (e.g. WebSocket). */\nexport const PING_WIRE_CONTRACT_HEADER = \"X-Ping-Wire-Contract\";\n\n/** Header value: `ping/<VERSION>`. */\nexport const PING_WIRE_CONTRACT_HEADER_VALUE = `ping/${PING_WIRE_CONTRACT_VERSION}`;\n\nexport type ValidationErrorKind =\n | \"UnsupportedVersion\"\n | \"BadConversationIdLen\"\n | \"BadSenderDeviceLen\"\n | \"ContentHashMismatch\";\n\nexport class ValidationError extends Error {\n constructor(public readonly kind: ValidationErrorKind, message: string) {\n super(message);\n this.name = \"PingWireContractValidationError\";\n }\n}\n\nconst KIND_DISCRIMINANT: Record<MessageEnvelope[\"kind\"], number> = {\n Application: 1,\n Commit: 2,\n Welcome: 3,\n Proposal: 4,\n KeyPackage: 5,\n};\n\nasync function sha256(bytes: Uint8Array): Promise<Uint8Array> {\n // `crypto.subtle` is available in browsers, Node 20+, and Workers. `bytes.buffer`\n // is typed as `ArrayBufferLike` which TS won't accept as `BufferSource` (the union\n // includes `SharedArrayBuffer`); pass an explicit ArrayBuffer slice instead.\n const ab = bytes.buffer.slice(bytes.byteOffset, bytes.byteOffset + bytes.byteLength) as ArrayBuffer;\n const hash = await crypto.subtle.digest(\"SHA-256\", ab);\n return new Uint8Array(hash);\n}\n\nfunction bytesEqual(a: Uint8Array, b: Uint8Array): boolean {\n if (a.length !== b.length) return false;\n for (let i = 0; i < a.length; i++) if (a[i] !== b[i]) return false;\n return true;\n}\n\n/**\n * Validate an envelope against the ping-wire-contract.\n *\n * Receivers claiming contract conformance MUST call this before applying any envelope.\n * External consumers using the raw envelope under their own profile are not bound by\n * these rules.\n *\n * CR-6 dual-accept window: `v ∈ [WIRE_VERSION_MIN_ACCEPTED, PING_WIRE_CONTRACT_VERSION]`\n * passes the version gate. For v=2 `Application` envelopes the content_hash is over\n * plaintext, so this function skips that check — callers MUST run\n * [[verifyApplicationContentHash]] after MLS decrypt to close the loop.\n */\nexport async function validate(env: MessageEnvelope): Promise<void> {\n if (env.v < WIRE_VERSION_MIN_ACCEPTED || env.v > PING_WIRE_CONTRACT_VERSION) {\n throw new ValidationError(\n \"UnsupportedVersion\",\n `unsupported wire-contract version: envelope says v=${env.v}, expected v∈[${WIRE_VERSION_MIN_ACCEPTED}, ${PING_WIRE_CONTRACT_VERSION}]`,\n );\n }\n if (env.conversation_id.length !== 16) {\n throw new ValidationError(\n \"BadConversationIdLen\",\n `conversation_id must be 16 bytes (ULID), got ${env.conversation_id.length}`,\n );\n }\n if (env.sender_device.length !== 32) {\n throw new ValidationError(\n \"BadSenderDeviceLen\",\n `sender_device must be 32 bytes, got ${env.sender_device.length}`,\n );\n }\n // CR-6 routing: skip the hash check for v=2 application envelopes.\n if (env.v >= 2 && env.kind === \"Application\") {\n return;\n }\n const buf = new Uint8Array(1 + env.payload.length);\n buf[0] = KIND_DISCRIMINANT[env.kind];\n buf.set(env.payload, 1);\n const computed = await sha256(buf);\n if (!bytesEqual(computed, env.content_hash)) {\n throw new ValidationError(\n \"ContentHashMismatch\",\n \"content_hash mismatch: envelope is forged, corrupted, or built by a non-conformant sender\",\n );\n }\n}\n\n/**\n * Verify a v=2 `Application` envelope's content_hash against the decrypted plaintext.\n *\n * Caller MUST run this after MLS decrypt for application envelopes whose `v >= 2`. For\n * v=1 envelopes or handshake kinds this is a no-op (those are covered by [[validate]]).\n */\nexport async function verifyApplicationContentHash(\n env: MessageEnvelope,\n plaintext: Uint8Array,\n): Promise<void> {\n if (env.v < 2 || env.kind !== \"Application\") return;\n const computed = await sha256(plaintext);\n if (!bytesEqual(computed, env.content_hash)) {\n throw new ValidationError(\n \"ContentHashMismatch\",\n \"v=2 application content_hash mismatch against decrypted plaintext\",\n );\n }\n}\n\n/**\n * True if the supplied content type negotiates the ping-wire-contract.\n *\n * Accepts the v2 canonical types AND the v1 legacy types during the CR-6 dual-accept\n * window. Tolerates whitespace and parameters (e.g. `; charset=utf-8`).\n */\nexport function negotiates(contentType: string | null | undefined): boolean {\n if (!contentType) return false;\n const primary = contentType.split(\";\")[0]?.trim().toLowerCase() ?? \"\";\n return (\n primary === CONTENT_TYPE_CBOR\n || primary === CONTENT_TYPE_JSON\n || primary === CONTENT_TYPE_CBOR_LEGACY_V1\n || primary === CONTENT_TYPE_JSON_LEGACY_V1\n );\n}\n","// Reference WebSocket transport. Treats the server as an opaque relay that:\n// - accepts a CBOR-encoded MessageEnvelope on `send`\n// - returns events newer than a cursor on `fetchSince`\n// - streams new events on the WS itself for `subscribe`\n// - exposes a directory endpoint for `discoverDevices`\n//\n// This is a sample. Real deployments will tune framing / auth.\n\nimport type { ConversationId, DiscoveredDevice, MessageEnvelope, Transport, UserId } from \"../types\";\nimport {\n CONTENT_TYPE_JSON,\n PING_WIRE_CONTRACT_HEADER,\n PING_WIRE_CONTRACT_HEADER_VALUE,\n} from \"../wire-contract\";\n\nexport interface WsTransportConfig {\n baseUrl: string; // ws[s]://...\n authHeader?: string; // optional bearer token\n fetchImpl?: typeof fetch; // override for SSR / tests\n}\n\nexport class WebSocketTransport implements Transport {\n private ws: WebSocket | null = null;\n private liveHandlers = new Set<(env: MessageEnvelope) => void>();\n private fetchImpl: typeof fetch;\n\n constructor(private cfg: WsTransportConfig) {\n this.fetchImpl = cfg.fetchImpl ?? globalThis.fetch.bind(globalThis);\n this.openSocket();\n }\n\n private openSocket() {\n if (typeof WebSocket === \"undefined\") return;\n const url = `${this.cfg.baseUrl.replace(/^http/, \"ws\")}/stream`;\n this.ws = new WebSocket(url);\n this.ws.onmessage = (ev) => {\n // Server sends one JSON envelope per text frame.\n const text = typeof ev.data === \"string\" ? ev.data : new TextDecoder().decode(ev.data as ArrayBuffer);\n try {\n const env = JSON.parse(text, reviver) as MessageEnvelope;\n for (const h of this.liveHandlers) h(env);\n } catch (e) {\n if (typeof console !== \"undefined\") console.debug(\"[ping-sdk] WS frame parse error:\", e);\n }\n };\n this.ws.onclose = () => setTimeout(() => this.openSocket(), 1000);\n }\n\n async send(envelope: MessageEnvelope): Promise<void> {\n // We're emitting the JSON projection of the ping-wire-contract. Production transports\n // would use CONTENT_TYPE_CBOR with a real CBOR codec; for the demo path the JSON shape\n // is the documented development projection.\n const res = await this.fetchImpl(`${this.cfg.baseUrl}/send`, {\n method: \"POST\",\n headers: {\n \"content-type\": CONTENT_TYPE_JSON,\n [PING_WIRE_CONTRACT_HEADER]: PING_WIRE_CONTRACT_HEADER_VALUE,\n ...(this.cfg.authHeader ? { authorization: this.cfg.authHeader } : {}),\n },\n body: JSON.stringify(envelope, replacer),\n });\n if (res.status === 409) throw new Error(\"EpochOccupied\");\n if (!res.ok) throw new Error(`transport send failed: ${res.status}`);\n }\n\n async fetchSince(conversationId: ConversationId, cursor: Uint8Array, limit: number): Promise<MessageEnvelope[]> {\n const url = new URL(`${this.cfg.baseUrl}/sync`);\n url.searchParams.set(\"conv\", toHex(conversationId));\n url.searchParams.set(\"cursor\", btoaUrl(cursor));\n url.searchParams.set(\"limit\", String(limit));\n const res = await this.fetchImpl(url.toString(), {\n headers: { ...(this.cfg.authHeader ? { authorization: this.cfg.authHeader } : {}) },\n });\n if (!res.ok) throw new Error(`transport fetch failed: ${res.status}`);\n const text = await res.text();\n return JSON.parse(text, reviver) as MessageEnvelope[];\n }\n\n subscribe(onEvent: (env: MessageEnvelope) => void): { unsubscribe(): void } {\n this.liveHandlers.add(onEvent);\n return { unsubscribe: () => this.liveHandlers.delete(onEvent) };\n }\n\n async discoverDevices(userId: UserId): Promise<DiscoveredDevice[]> {\n const res = await this.fetchImpl(`${this.cfg.baseUrl}/devices/${toHex(userId)}`, {\n headers: { ...(this.cfg.authHeader ? { authorization: this.cfg.authHeader } : {}) },\n });\n if (!res.ok) throw new Error(`device discovery failed: ${res.status}`);\n const text = await res.text();\n return JSON.parse(text, reviver) as DiscoveredDevice[];\n }\n}\n\n// ---- Wire codec ----\n//\n// Demo wire format: JSON with byte fields wrapped as `{ _bytes: number[] }`. The relay's\n// `envFromJson`/`envToJson` use the same shape. The spec calls for CBOR; v0.2 swaps in a\n// real CBOR codec, but the JSON form is much friendlier to debug.\n//\n// (decodeEnvelope helpers used to live here; collapsed into inline JSON.parse calls above.)\n\nfunction replacer(_k: string, v: unknown): unknown {\n if (v instanceof Uint8Array) return { _bytes: Array.from(v) };\n return v;\n}\nfunction reviver(_k: string, v: unknown): unknown {\n if (v && typeof v === \"object\" && Array.isArray((v as { _bytes?: number[] })._bytes)) {\n return new Uint8Array((v as { _bytes: number[] })._bytes);\n }\n return v;\n}\n\nfunction toHex(b: Uint8Array): string {\n let s = \"\"; for (const v of b) s += v.toString(16).padStart(2, \"0\"); return s;\n}\nfunction btoaUrl(b: Uint8Array): string {\n let s = \"\"; for (const v of b) s += String.fromCharCode(v);\n return btoa(s).replace(/\\+/g, \"-\").replace(/\\//g, \"_\").replace(/=+$/, \"\");\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;;ACIA,IAAM,UAAU;AAChB,IAAM,aAAa;AACnB,IAAM,QAAQ;AAId,SAAS,OAA6B;AACpC,SAAO,IAAI,QAAQ,CAAC,SAAS,WAAW;AACtC,UAAM,MAAM,UAAU,KAAK,SAAS,UAAU;AAC9C,QAAI,kBAAkB,MAAM;AAC1B,YAAM,KAAK,IAAI;AACf,UAAI,CAAC,GAAG,iBAAiB,SAAS,KAAK,GAAG;AACxC,cAAM,KAAK,GAAG,kBAAkB,OAAO,EAAE,SAAS,CAAC,MAAM,KAAK,EAAE,CAAC;AACjE,WAAG,YAAY,MAAM,MAAM,EAAE,QAAQ,MAAM,CAAC;AAAA,MAC9C;AAAA,IACF;AACA,QAAI,YAAY,MAAM,QAAQ,IAAI,MAAM;AACxC,QAAI,UAAU,MAAM,OAAO,IAAI,KAAK;AAAA,EACtC,CAAC;AACH;AAEO,IAAM,mBAAN,MAA0C;AAAA,EACvC;AAAA,EAER,cAAc;AAAE,SAAK,YAAY,KAAK;AAAA,EAAG;AAAA,EAEzC,MAAM,IAAI,WAAmB,KAAyC;AACpE,UAAM,KAAK,MAAM,KAAK;AACtB,WAAO,IAAI,QAAQ,CAAC,SAAS,WAAW;AACtC,YAAM,KAAK,GAAG,YAAY,OAAO,UAAU;AAC3C,YAAM,IAAI,GAAG,YAAY,KAAK,EAAE,IAAI,CAAC,WAAW,GAAG,CAAC;AACpD,QAAE,YAAY,MAAM,QAAU,EAAE,QAA4B,SAAU,IAAI;AAC1E,QAAE,UAAU,MAAM,OAAO,EAAE,KAAK;AAAA,IAClC,CAAC;AAAA,EACH;AAAA,EAEA,MAAM,IAAI,WAAmB,KAAa,OAAkC;AAC1E,UAAM,KAAK,MAAM,KAAK;AACtB,WAAO,IAAI,QAAQ,CAAC,SAAS,WAAW;AACtC,YAAM,KAAK,GAAG,YAAY,OAAO,WAAW;AAC5C,SAAG,YAAY,KAAK,EAAE,IAAI,EAAE,IAAI,WAAW,KAAK,MAAM,CAAQ;AAC9D,SAAG,aAAa,MAAM,QAAQ;AAC9B,SAAG,UAAU,MAAM,OAAO,GAAG,KAAK;AAAA,IACpC,CAAC;AAAA,EACH;AAAA,EAEA,MAAM,IAAI,WAAmB,KAA4B;AACvD,UAAM,KAAK,MAAM,KAAK;AACtB,WAAO,IAAI,QAAQ,CAAC,SAAS,WAAW;AACtC,YAAM,KAAK,GAAG,YAAY,OAAO,WAAW;AAC5C,SAAG,YAAY,KAAK,EAAE,OAAO,CAAC,WAAW,GAAG,CAAC;AAC7C,SAAG,aAAa,MAAM,QAAQ;AAC9B,SAAG,UAAU,MAAM,OAAO,GAAG,KAAK;AAAA,IACpC,CAAC;AAAA,EACH;AAAA,EAEA,MAAM,SAAS,WAAmB,QAAmC;AACnE,UAAM,KAAK,MAAM,KAAK;AACtB,WAAO,IAAI,QAAQ,CAAC,SAAS,WAAW;AACtC,YAAM,KAAK,GAAG,YAAY,OAAO,UAAU;AAC3C,YAAM,MAAM,GAAG,YAAY,KAAK,EAAE,MAAM,IAAI;AAC5C,YAAM,MAAM,IAAI,WAAW,YAAY,KAAK,SAAS,CAAC;AACtD,YAAM,MAAgB,CAAC;AACvB,UAAI,YAAY,MAAM;AACpB,cAAM,MAAM,IAAI;AAChB,YAAI,CAAC,IAAK,QAAO,QAAQ,GAAG;AAC5B,cAAM,MAAM,IAAI;AAChB,YAAI,CAAC,UAAU,IAAI,IAAI,WAAW,MAAM,EAAG,KAAI,KAAK,IAAI,GAAG;AAC3D,YAAI,SAAS;AAAA,MACf;AACA,UAAI,UAAU,MAAM,OAAO,IAAI,KAAK;AAAA,IACtC,CAAC;AAAA,EACH;AACF;;;AC3DO,IAAM,6BAA6B;AAGnC,IAAM,4BAA4B;AAGlC,IAAM,oBAAoB;AAG1B,IAAM,oBAAoB;AAG1B,IAAM,8BAA8B;AAGpC,IAAM,8BAA8B;AAGpC,IAAM,4BAA4B;AAGlC,IAAM,kCAAkC,QAAQ,0BAA0B;AAQ1E,IAAM,kBAAN,cAA8B,MAAM;AAAA,EACzC,YAA4B,MAA2B,SAAiB;AACtE,UAAM,OAAO;AADa;AAE1B,SAAK,OAAO;AAAA,EACd;AAAA,EAH4B;AAI9B;AAEA,IAAM,oBAA6D;AAAA,EACjE,aAAa;AAAA,EACb,QAAQ;AAAA,EACR,SAAS;AAAA,EACT,UAAU;AAAA,EACV,YAAY;AACd;AAEA,eAAe,OAAO,OAAwC;AAI5D,QAAM,KAAK,MAAM,OAAO,MAAM,MAAM,YAAY,MAAM,aAAa,MAAM,UAAU;AACnF,QAAM,OAAO,MAAM,OAAO,OAAO,OAAO,WAAW,EAAE;AACrD,SAAO,IAAI,WAAW,IAAI;AAC5B;AAEA,SAAS,WAAW,GAAe,GAAwB;AACzD,MAAI,EAAE,WAAW,EAAE,OAAQ,QAAO;AAClC,WAAS,IAAI,GAAG,IAAI,EAAE,QAAQ,IAAK,KAAI,EAAE,CAAC,MAAM,EAAE,CAAC,EAAG,QAAO;AAC7D,SAAO;AACT;AAcA,eAAsB,SAAS,KAAqC;AAClE,MAAI,IAAI,IAAI,6BAA6B,IAAI,IAAI,4BAA4B;AAC3E,UAAM,IAAI;AAAA,MACR;AAAA,MACA,sDAAsD,IAAI,CAAC,sBAAiB,yBAAyB,KAAK,0BAA0B;AAAA,IACtI;AAAA,EACF;AACA,MAAI,IAAI,gBAAgB,WAAW,IAAI;AACrC,UAAM,IAAI;AAAA,MACR;AAAA,MACA,gDAAgD,IAAI,gBAAgB,MAAM;AAAA,IAC5E;AAAA,EACF;AACA,MAAI,IAAI,cAAc,WAAW,IAAI;AACnC,UAAM,IAAI;AAAA,MACR;AAAA,MACA,uCAAuC,IAAI,cAAc,MAAM;AAAA,IACjE;AAAA,EACF;AAEA,MAAI,IAAI,KAAK,KAAK,IAAI,SAAS,eAAe;AAC5C;AAAA,EACF;AACA,QAAM,MAAM,IAAI,WAAW,IAAI,IAAI,QAAQ,MAAM;AACjD,MAAI,CAAC,IAAI,kBAAkB,IAAI,IAAI;AACnC,MAAI,IAAI,IAAI,SAAS,CAAC;AACtB,QAAM,WAAW,MAAM,OAAO,GAAG;AACjC,MAAI,CAAC,WAAW,UAAU,IAAI,YAAY,GAAG;AAC3C,UAAM,IAAI;AAAA,MACR;AAAA,MACA;AAAA,IACF;AAAA,EACF;AACF;AAQA,eAAsB,6BACpB,KACA,WACe;AACf,MAAI,IAAI,IAAI,KAAK,IAAI,SAAS,cAAe;AAC7C,QAAM,WAAW,MAAM,OAAO,SAAS;AACvC,MAAI,CAAC,WAAW,UAAU,IAAI,YAAY,GAAG;AAC3C,UAAM,IAAI;AAAA,MACR;AAAA,MACA;AAAA,IACF;AAAA,EACF;AACF;AAQO,SAAS,WAAW,aAAiD;AAC1E,MAAI,CAAC,YAAa,QAAO;AACzB,QAAM,UAAU,YAAY,MAAM,GAAG,EAAE,CAAC,GAAG,KAAK,EAAE,YAAY,KAAK;AACnE,SACE,YAAY,qBACT,YAAY,qBACZ,YAAY,+BACZ,YAAY;AAEnB;;;AC1IO,IAAM,qBAAN,MAA8C;AAAA,EAKnD,YAAoB,KAAwB;AAAxB;AAClB,SAAK,YAAY,IAAI,aAAa,WAAW,MAAM,KAAK,UAAU;AAClE,SAAK,WAAW;AAAA,EAClB;AAAA,EAHoB;AAAA,EAJZ,KAAuB;AAAA,EACvB,eAAe,oBAAI,IAAoC;AAAA,EACvD;AAAA,EAOA,aAAa;AACnB,QAAI,OAAO,cAAc,YAAa;AACtC,UAAM,MAAM,GAAG,KAAK,IAAI,QAAQ,QAAQ,SAAS,IAAI,CAAC;AACtD,SAAK,KAAK,IAAI,UAAU,GAAG;AAC3B,SAAK,GAAG,YAAY,CAAC,OAAO;AAE1B,YAAM,OAAO,OAAO,GAAG,SAAS,WAAW,GAAG,OAAO,IAAI,YAAY,EAAE,OAAO,GAAG,IAAmB;AACpG,UAAI;AACF,cAAM,MAAM,KAAK,MAAM,MAAM,OAAO;AACpC,mBAAW,KAAK,KAAK,aAAc,GAAE,GAAG;AAAA,MAC1C,SAAS,GAAG;AACV,YAAI,OAAO,YAAY,YAAa,SAAQ,MAAM,oCAAoC,CAAC;AAAA,MACzF;AAAA,IACF;AACA,SAAK,GAAG,UAAU,MAAM,WAAW,MAAM,KAAK,WAAW,GAAG,GAAI;AAAA,EAClE;AAAA,EAEA,MAAM,KAAK,UAA0C;AAInD,UAAM,MAAM,MAAM,KAAK,UAAU,GAAG,KAAK,IAAI,OAAO,SAAS;AAAA,MAC3D,QAAQ;AAAA,MACR,SAAS;AAAA,QACP,gBAAgB;AAAA,QAChB,CAAC,yBAAyB,GAAG;AAAA,QAC7B,GAAI,KAAK,IAAI,aAAa,EAAE,eAAe,KAAK,IAAI,WAAW,IAAI,CAAC;AAAA,MACtE;AAAA,MACA,MAAM,KAAK,UAAU,UAAU,QAAQ;AAAA,IACzC,CAAC;AACD,QAAI,IAAI,WAAW,IAAK,OAAM,IAAI,MAAM,eAAe;AACvD,QAAI,CAAC,IAAI,GAAI,OAAM,IAAI,MAAM,0BAA0B,IAAI,MAAM,EAAE;AAAA,EACrE;AAAA,EAEA,MAAM,WAAW,gBAAgC,QAAoB,OAA2C;AAC9G,UAAM,MAAM,IAAI,IAAI,GAAG,KAAK,IAAI,OAAO,OAAO;AAC9C,QAAI,aAAa,IAAI,QAAQ,MAAM,cAAc,CAAC;AAClD,QAAI,aAAa,IAAI,UAAU,QAAQ,MAAM,CAAC;AAC9C,QAAI,aAAa,IAAI,SAAS,OAAO,KAAK,CAAC;AAC3C,UAAM,MAAM,MAAM,KAAK,UAAU,IAAI,SAAS,GAAG;AAAA,MAC/C,SAAS,EAAE,GAAI,KAAK,IAAI,aAAa,EAAE,eAAe,KAAK,IAAI,WAAW,IAAI,CAAC,EAAG;AAAA,IACpF,CAAC;AACD,QAAI,CAAC,IAAI,GAAI,OAAM,IAAI,MAAM,2BAA2B,IAAI,MAAM,EAAE;AACpE,UAAM,OAAO,MAAM,IAAI,KAAK;AAC5B,WAAO,KAAK,MAAM,MAAM,OAAO;AAAA,EACjC;AAAA,EAEA,UAAU,SAAkE;AAC1E,SAAK,aAAa,IAAI,OAAO;AAC7B,WAAO,EAAE,aAAa,MAAM,KAAK,aAAa,OAAO,OAAO,EAAE;AAAA,EAChE;AAAA,EAEA,MAAM,gBAAgB,QAA6C;AACjE,UAAM,MAAM,MAAM,KAAK,UAAU,GAAG,KAAK,IAAI,OAAO,YAAY,MAAM,MAAM,CAAC,IAAI;AAAA,MAC/E,SAAS,EAAE,GAAI,KAAK,IAAI,aAAa,EAAE,eAAe,KAAK,IAAI,WAAW,IAAI,CAAC,EAAG;AAAA,IACpF,CAAC;AACD,QAAI,CAAC,IAAI,GAAI,OAAM,IAAI,MAAM,4BAA4B,IAAI,MAAM,EAAE;AACrE,UAAM,OAAO,MAAM,IAAI,KAAK;AAC5B,WAAO,KAAK,MAAM,MAAM,OAAO;AAAA,EACjC;AACF;AAUA,SAAS,SAAS,IAAY,GAAqB;AACjD,MAAI,aAAa,WAAY,QAAO,EAAE,QAAQ,MAAM,KAAK,CAAC,EAAE;AAC5D,SAAO;AACT;AACA,SAAS,QAAQ,IAAY,GAAqB;AAChD,MAAI,KAAK,OAAO,MAAM,YAAY,MAAM,QAAS,EAA4B,MAAM,GAAG;AACpF,WAAO,IAAI,WAAY,EAA2B,MAAM;AAAA,EAC1D;AACA,SAAO;AACT;AAEA,SAAS,MAAM,GAAuB;AACpC,MAAI,IAAI;AAAI,aAAW,KAAK,EAAG,MAAK,EAAE,SAAS,EAAE,EAAE,SAAS,GAAG,GAAG;AAAG,SAAO;AAC9E;AACA,SAAS,QAAQ,GAAuB;AACtC,MAAI,IAAI;AAAI,aAAW,KAAK,EAAG,MAAK,OAAO,aAAa,CAAC;AACzD,SAAO,KAAK,CAAC,EAAE,QAAQ,OAAO,GAAG,EAAE,QAAQ,OAAO,GAAG,EAAE,QAAQ,OAAO,EAAE;AAC1E;;;AHtHA;AA8EO,IAAM,kBAAN,MAAM,iBAAgB;AAAA,EACnB;AAAA,EACA,SAAS;AAAA,EACT,UAAU,oBAAI,IAA2E;AAAA,EACzF;AAAA,EACA;AAAA,EACA;AAAA,EACA,kBAAkB,oBAAI,IAAkC;AAAA,EACxD,uBAAuB,oBAAI,IAAoE;AAAA,EAC/F,eAA+C;AAAA,EAC/C,YAAY,oBAAI,IAA8B;AAAA;AAAA;AAAA;AAAA,EAI9C,cAAiC;AAAA,EAEjC,YAAY,KAAmB,QAAgB;AACrD,SAAK,SAAS;AACd,SAAK,UAAU,IAAI;AACnB,SAAK,YAAY,IAAI;AACrB,SAAK,MAAM,IAAI,QAAQ,MAAM,KAAK,IAAI;AACtC,SAAK,OAAO,YAAY,CAAC,OAAO,KAAK,aAAa,GAAG,IAAgB;AAAA,EACvE;AAAA;AAAA,EAGA,aAAa,mBAAwC;AACnD,WAAO,iBAAgB,oBAAgC;AAAA,MACrD,MAAM;AAAA,MAAoB,IAAI;AAAA,IAChC,CAAC;AAAA,EACH;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAYA,aAAa,kBACX,QACA,cACqB;AACrB,QAAI,aAAa,WAAW,IAAI;AAC9B,YAAM,IAAI,MAAM,kDAAkD;AAAA,IACpE;AACA,WAAO,iBAAgB,oBAAgC;AAAA,MACrD,MAAM;AAAA,MAAqB,IAAI;AAAA,MAAG;AAAA,MAAQ;AAAA,IAC5C,CAAC;AAAA,EACH;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAaA,aAAa,kBACX,QACA,eACwB;AACxB,QAAI,cAAc,WAAW,IAAI;AAC/B,YAAM,IAAI,MAAM,mDAAmD;AAAA,IACrE;AACA,WAAO,iBAAgB,oBAAmC;AAAA,MACxD,MAAM;AAAA,MAAqB,IAAI;AAAA,MAAG;AAAA,MAAQ;AAAA,IAC5C,CAAC;AAAA,EACH;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAQA,aAAa,SACX,WACA,cACA,MACqB;AACrB,QAAI,aAAa,WAAW,IAAI;AAC9B,YAAM,IAAI,MAAM,yCAAyC;AAAA,IAC3D;AACA,WAAO,iBAAgB,oBAAgC;AAAA,MACrD,MAAM;AAAA,MAAY,IAAI;AAAA,MAAG;AAAA,MAAW;AAAA,MAAc;AAAA,IACpD,CAAC;AAAA,EACH;AAAA;AAAA,EAGA,aAAa,SACX,QACA,eACA,MACqB;AACrB,QAAI,cAAc,WAAW,IAAI;AAC/B,YAAM,IAAI,MAAM,0CAA0C;AAAA,IAC5D;AACA,WAAO,iBAAgB,oBAAgC;AAAA,MACrD,MAAM;AAAA,MAAY,IAAI;AAAA,MAAG;AAAA,MAAQ;AAAA,MAAe;AAAA,IAClD,CAAC;AAAA,EACH;AAAA;AAAA;AAAA;AAAA,EAKA,OAAe,oBAAuB,KAA0B;AAC9D,UAAM,IAAI,IAAI,OAAO,IAAI,IAAI,eAAe,YAAY,GAAG,GAAG,EAAE,MAAM,SAAS,CAAC;AAChF,WAAO,IAAI,QAAW,CAAC,SAAS,WAAW;AACzC,QAAE,YAAY,CAAC,OAAO;AACpB,cAAM,MAAM,GAAG;AACf,YAAI,IAAI,SAAS,YAAY,IAAI,OAAQ,IAAuB,IAAI;AAClE,YAAE,UAAU;AACZ,cAAI,KAAK,QAAQ,IAAI,KAAU,IAAI,OAAO,IAAI,MAAM,IAAI,KAAK,CAAC;AAAA,QAChE;AAAA,MACF;AACA,QAAE,YAAY,GAAG;AAAA,IACnB,CAAC;AAAA,EACH;AAAA,EAEA,aAAa,KAAK,KAA6C;AAC7D,UAAM,SAAS,IAAI,OAAO,IAAI,IAAI,eAAe,YAAY,GAAG,GAAG,EAAE,MAAM,SAAS,CAAC;AACrF,UAAM,SAAS,IAAI,iBAAgB,KAAK,MAAM;AAE9C,UAAM,OAAO,KAAK;AAAA,MAChB,MAAM;AAAA,MACN,IAAI,OAAO;AAAA,MACX,gBAAgB,IAAI;AAAA,MACpB,aAAa,IAAI;AAAA,MACjB,OAAO,OAAO,IAAI;AAAA,MAClB,GAAI,IAAI,yBACJ,EAAE,wBAAwB,IAAI,uBAAuB,IACrD,CAAC;AAAA,IACP,CAAC;AAGD,WAAO,cAAc,MAAM,OAAO,SAAS;AAO3C,WAAO,eAAe,IAAI,UAAU,UAAU,CAAC,aAAa;AAC1D,WAAK,OAAO,iBAAiB,QAAQ;AAAA,IACvC,CAAC;AACD,WAAO;AAAA,EACT;AAAA;AAAA,EAGA,MAAc,iBAAiB,UAA0C;AAKvE,QAAI,KAAK,eAAe,YAAY,SAAS,eAAe,KAAK,WAAW,GAAG;AAC7E;AAAA,IACF;AACA,QAAI,SAAS,SAAS,WAAW;AAC/B,UAAI;AACF,cAAM,QAAQ,MAAM,KAAK,kBAAkB;AAC3C,cAAM,UAAU,MAAM,KAAK,CAAC,MAAM,YAAY,EAAE,IAAI,SAAS,eAAe,CAAC;AAC7E,YAAI,QAAS;AACb,cAAM,KAAK,iBAAiB,QAAQ;AAAA,MACtC,SAAS,GAAG;AAGV,YAAI,OAAO,YAAY,YAAa,SAAQ,MAAM,+BAA+B,CAAC;AAAA,MACpF;AACA;AAAA,IACF;AACA,QAAI;AACF,YAAM,KAAK,gBAAgB,QAAQ;AAAA,IACrC,SAAS,GAAG;AACV,UAAI,OAAO,YAAY,YAAa,SAAQ,MAAM,6BAA6B,CAAC;AAAA,IAClF;AAAA,EACF;AAAA;AAAA,EAIA,MAAM,SAA0B;AAC9B,WAAO,MAAM,KAAK,KAAK,EAAE,MAAM,UAAU,IAAI,KAAK,SAAS,CAAC;AAAA,EAC9D;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EASA,MAAM,iBAAsC;AAC1C,WAAO,MAAM,KAAK,KAAK,EAAE,MAAM,kBAAkB,IAAI,KAAK,SAAS,CAAC;AAAA,EACtE;AAAA,EAEA,MAAM,WAA8B;AAClC,WAAO,MAAM,KAAK,KAAK,EAAE,MAAM,YAAY,IAAI,KAAK,SAAS,CAAC;AAAA,EAChE;AAAA,EAEA,MAAM,kBAAuC;AAC3C,WAAO,MAAM,KAAK,KAAK,EAAE,MAAM,mBAAmB,IAAI,KAAK,SAAS,CAAC;AAAA,EACvE;AAAA,EAEA,MAAM,mBAAmB,OAA0B,CAAC,GAA0B;AAC5E,UAAM,UAAU,MAAM,KAAK,KAAK;AAAA,MAC9B,MAAM;AAAA,MAAsB,IAAI,KAAK;AAAA,MAAU,MAAM,KAAK,QAAQ;AAAA,MAAM,OAAO,KAAK,IAAI;AAAA,IAC1F,CAAC;AACD,WAAO,KAAK,kBAAkB,OAAO;AAAA,EACvC;AAAA,EAEA,MAAM,iBAAiB,SAAiD;AACtE,UAAM,UAAU,MAAM,KAAK,KAAK;AAAA,MAC9B,MAAM;AAAA,MAAoB,IAAI,KAAK;AAAA,MAAU;AAAA,MAAS,OAAO,KAAK,IAAI;AAAA,IACxE,CAAC;AACD,WAAO,KAAK,kBAAkB,OAAO;AAAA,EACvC;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAQA,gBAAgB,IAAkC;AAChD,WAAO,KAAK,kBAAkB,EAAE;AAAA,EAClC;AAAA,EAEA,MAAM,oBAAiD;AACrD,UAAM,QAAQ,MAAM,KAAK,KAAK,EAAE,MAAM,qBAAqB,IAAI,KAAK,SAAS,CAAC;AAC9E,SAAK,UAAU,MAAM;AACrB,eAAW,KAAK,MAAO,MAAK,UAAU,IAAI,IAAI,EAAE,EAAE,GAAG,CAAC;AACtD,WAAO;AAAA,EACT;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAQA,MAAM,QAAQ,MAA6C;AACzD,WAAO,MAAM,KAAK,KAAK,EAAE,MAAM,WAAW,IAAI,KAAK,UAAU,KAAK,CAAC;AAAA,EACrE;AAAA,EAEA,MAAM,oBAAgD;AACpD,WAAO,MAAM,KAAK,KAAK;AAAA,MACrB,MAAM;AAAA,MAAqB,IAAI,KAAK;AAAA,MAAU,OAAO,KAAK,IAAI;AAAA,IAChE,CAAC;AAAA,EACH;AAAA,EAEA,MAAM,gBAAgB,UAA4D;AAChF,WAAO,MAAM,KAAK,KAAK;AAAA,MACrB,MAAM;AAAA,MAAmB,IAAI,KAAK;AAAA,MAAU;AAAA,MAAU,OAAO,KAAK,IAAI;AAAA,IACxE,CAAC;AAAA,EACH;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAaA,MAAM,mBACJ,aACA,aACA,gBAAmC,CAAC,GACZ;AACxB,WAAO,MAAM,KAAK,KAAK;AAAA,MACrB,MAAM;AAAA,MAAsB,IAAI,KAAK;AAAA,MACrC;AAAA,MAAa;AAAA,MAAa;AAAA,MAAe,OAAO,KAAK,IAAI;AAAA,IAC3D,CAAC;AAAA,EACH;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAYA,aAAa,sBACX,eACqC;AACrC,QAAI,cAAc,WAAW,EAAG,QAAO;AACvC,WAAO,iBAAgB,oBAAyC;AAAA,MAC9D,MAAM;AAAA,MAAyB,IAAI;AAAA,MAAG;AAAA,IACxC,CAAC;AAAA,EACH;AAAA,EAEA,MAAM,qBAAqB,QAAsC;AAC/D,UAAM,KAAK,KAAK;AAAA,MACd,MAAM;AAAA,MAAwB,IAAI,KAAK;AAAA,MAAU;AAAA,MAAQ,OAAO,KAAK,IAAI;AAAA,IAC3E,CAAC;AAAA,EACH;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAQA,aAAa,yBAA0C;AACrD,WAAO,iBAAgB,oBAA4B;AAAA,MACjD,MAAM;AAAA,MAA0B,IAAI;AAAA,IACtC,CAAC;AAAA,EACH;AAAA;AAAA;AAAA,EAIA,aAAa,wBAAwB,QAAiC;AACpE,WAAO,iBAAgB,oBAA4B;AAAA,MACjD,MAAM;AAAA,MAA2B,IAAI;AAAA,MAAG;AAAA,IAC1C,CAAC;AAAA,EACH;AAAA;AAAA;AAAA,EAIA,aAAa,cAAc,MAWH;AACtB,WAAO,iBAAgB,oBAAgC;AAAA,MACrD,MAAM;AAAA,MAAiB,IAAI;AAAA,MAC3B,gBAAgB,KAAK;AAAA,MACrB,WAAW,KAAK;AAAA,MAChB,gBAAgB,KAAK;AAAA,MACrB,qBAAqB,KAAK;AAAA,MAC1B,uBAAuB,KAAK,yBAAyB,CAAC;AAAA,MACtD,aAAa,KAAK;AAAA,IACpB,CAAC;AAAA,EACH;AAAA;AAAA;AAAA,EAIA,aAAa,cACX,MACA,gBACyB;AACzB,WAAO,iBAAgB,oBAAoC;AAAA,MACzD,MAAM;AAAA,MAAiB,IAAI;AAAA,MAAG;AAAA,MAAM;AAAA,IACtC,CAAC;AAAA,EACH;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAWA,aAAa,kBACX,QACA,cACA,sBACqB;AACrB,WAAO,iBAAgB,oBAAgC;AAAA,MACrD,MAAM;AAAA,MAAqB,IAAI;AAAA,MAAG;AAAA,MAAQ;AAAA,MAAc;AAAA,IAC1D,CAAC;AAAA,EACH;AAAA;AAAA;AAAA;AAAA,EAKA,aAAa,kBACX,QACA,eACA,mBACwB;AACxB,WAAO,iBAAgB,oBAAmC;AAAA,MACxD,MAAM;AAAA,MAAqB,IAAI;AAAA,MAAG;AAAA,MAAQ;AAAA,MAAe;AAAA,IAC3D,CAAC;AAAA,EACH;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAiBA,MAAM,mBACJ,UACA,SAC6B;AAC7B,WAAO,MAAM,KAAK,KAAK;AAAA,MACrB,MAAM;AAAA,MAAsB,IAAI,KAAK;AAAA,MACrC;AAAA,MAAU;AAAA,MAAS,OAAO,KAAK,IAAI;AAAA,IACrC,CAAC;AAAA,EACH;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAmBA,MAAM,aAAa,UAAgD;AACjE,WAAO,MAAM,KAAK,KAAK;AAAA,MACrB,MAAM;AAAA,MAAgB,IAAI,KAAK;AAAA,MAAU;AAAA,MAAU,OAAO,KAAK,IAAI;AAAA,IACrE,CAAC;AAAA,EACH;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAUA,MAAM,gCAAgC,MAA2C;AAC/E,WAAO,MAAM,KAAK,KAAK;AAAA,MACrB,MAAM;AAAA,MAAmC,IAAI,KAAK;AAAA,MAClD;AAAA,MAAM,OAAO,KAAK,IAAI;AAAA,IACxB,CAAC;AAAA,EACH;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAYA,MAAM,oBAAoB,eAAoD;AAC5E,WAAO,MAAM,KAAK,KAAK;AAAA,MACrB,MAAM;AAAA,MAAuB,IAAI,KAAK;AAAA,MACtC;AAAA,MAAe,OAAO,KAAK,IAAI;AAAA,IACjC,CAAC;AAAA,EACH;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAcA,MAAM,yBACJ,MACA,OACA,SACA,QACqB;AACrB,QAAI,UAAU,KAAK,SAAS,MAAM;AAChC,YAAM,IAAI,MAAM,mDAAmD;AAAA,IACrE;AACA,WAAO,MAAM,KAAK,KAAK;AAAA,MACrB,MAAM;AAAA,MAA4B,IAAI,KAAK;AAAA,MAC3C;AAAA,MAAM;AAAA,MAAO;AAAA,MAAS;AAAA,IACxB,CAAC;AAAA,EACH;AAAA;AAAA,EAGA,UAAU,SAAmD;AAC3D,SAAK,gBAAgB,IAAI,OAAO;AAChC,WAAO,MAAM,KAAK,gBAAgB,OAAO,OAAO;AAAA,EAClD;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAQA,sBAAsB,SAAqF;AACzG,SAAK,qBAAqB,IAAI,OAAO;AACrC,WAAO,MAAM,KAAK,qBAAqB,OAAO,OAAO;AAAA,EACvD;AAAA,EAEA,MAAM,QAAuB;AAC3B,SAAK,cAAc,YAAY;AAC/B,SAAK,OAAO,UAAU;AAAA,EACxB;AAAA;AAAA,EAIQ,kBAAkB,IAAkC;AAC1D,UAAM,OAAO;AACb,WAAO;AAAA,MACL;AAAA,MACA,MAAM,KAAK,WAAW;AACpB,eAAO,MAAM,KAAK,KAAK;AAAA,UACrB,MAAM;AAAA,UAAQ,IAAI,KAAK;AAAA,UAAU,MAAM;AAAA,UAAI;AAAA,UAAW,OAAO,KAAK,IAAI;AAAA,QACxE,CAAC;AAAA,MACH;AAAA,MACA,MAAM,WAAW,SAAS;AACxB,cAAM,KAAK,KAAK;AAAA,UACd,MAAM;AAAA,UAAc,IAAI,KAAK;AAAA,UAAU,MAAM;AAAA,UAAI;AAAA,UAAS,OAAO,KAAK,IAAI;AAAA,QAC5E,CAAC;AAAA,MACH;AAAA,MACA,MAAM,cAAc,QAAQ;AAC1B,cAAM,KAAK,KAAK;AAAA,UACd,MAAM;AAAA,UAAiB,IAAI,KAAK;AAAA,UAAU,MAAM;AAAA,UAAI;AAAA,UAAQ,OAAO,KAAK,IAAI;AAAA,QAC9E,CAAC;AAAA,MACH;AAAA,MACA,OAAO;AACL,cAAM,SAAS,KAAK,UAAU,IAAI,IAAI,EAAE,CAAC;AACzC,YAAI,CAAC,OAAQ,OAAM,IAAI,MAAM,8DAA8D;AAC3F,eAAO;AAAA,MACT;AAAA,IACF;AAAA,EACF;AAAA,EAEQ,KAAK,KAAgC;AAC3C,WAAO,IAAI,QAAQ,CAAC,SAAS,WAAW;AACtC,WAAK,QAAQ,IAAK,IAAuB,IAAI,EAAE,SAAS,OAAO,CAAC;AAChE,WAAK,OAAO,YAAY,GAAG;AAAA,IAC7B,CAAC;AAAA,EACH;AAAA,EAEA,MAAc,aAAa,KAAe;AACxC,YAAQ,IAAI,MAAM;AAAA,MAChB,KAAK,UAAU;AACb,cAAM,OAAO,KAAK,QAAQ,IAAI,IAAI,EAAE;AACpC,YAAI,CAAC,KAAM;AACX,aAAK,QAAQ,OAAO,IAAI,EAAE;AAC1B,YAAI,KAAK,KAAK,QAAQ,IAAI,KAAK,IAAI,KAAK,OAAO,IAAI,MAAM,IAAI,KAAK,CAAC;AACnE;AAAA,MACF;AAAA,MACA,KAAK,iBAAiB;AACpB,mBAAW,KAAK,KAAK,gBAAiB,GAAE,IAAI,GAAG;AAC/C;AAAA,MACF;AAAA,MACA,KAAK,sBAAsB;AACzB,mBAAW,KAAK,KAAK,qBAAsB,GAAE,IAAI,IAAI,IAAI,OAAO,IAAI,MAAM;AAC1E;AAAA,MACF;AAAA,MACA,KAAK,gBAAgB;AACnB,YAAI;AACF,gBAAM,IAAI,MAAO,KAAK,QACnB,IAAI,MAAM,IAAI,GAAG,IAAI,IAAI;AAC5B,eAAK,OAAO,YAAY,EAAE,MAAM,iBAAiB,IAAI,IAAI,IAAI,IAAI,MAAM,OAAO,EAAE,CAAY;AAAA,QAC9F,SAAS,GAAG;AACV,eAAK,OAAO,YAAY;AAAA,YACtB,MAAM;AAAA,YAAiB,IAAI,IAAI;AAAA,YAAI,IAAI;AAAA,YAAO,OAAO,OAAO,CAAC;AAAA,UAC/D,CAAY;AAAA,QACd;AACA;AAAA,MACF;AAAA,MACA,KAAK,kBAAkB;AACrB,YAAI;AACF,gBAAM,IAAI,MAAO,KAAK,UACnB,IAAI,MAAM,IAAI,GAAG,IAAI,IAAI;AAC5B,eAAK,OAAO,YAAY,EAAE,MAAM,mBAAmB,IAAI,IAAI,IAAI,IAAI,MAAM,OAAO,EAAE,CAAY;AAAA,QAChG,SAAS,GAAG;AACV,eAAK,OAAO,YAAY;AAAA,YACtB,MAAM;AAAA,YAAmB,IAAI,IAAI;AAAA,YAAI,IAAI;AAAA,YAAO,OAAO,OAAO,CAAC;AAAA,UACjE,CAAY;AAAA,QACd;AACA;AAAA,MACF;AAAA,IACF;AAAA,EACF;AACF;AAEA,SAAS,IAAI,GAAuB;AAClC,MAAI,IAAI;AACR,aAAW,KAAK,EAAG,MAAK,EAAE,SAAS,EAAE,EAAE,SAAS,GAAG,GAAG;AACtD,SAAO;AACT;AAEA,SAAS,YAAY,GAAe,GAAwB;AAC1D,MAAI,EAAE,WAAW,EAAE,OAAQ,QAAO;AAClC,WAAS,IAAI,GAAG,IAAI,EAAE,QAAQ,IAAK,KAAI,EAAE,CAAC,MAAM,EAAE,CAAC,EAAG,QAAO;AAC7D,SAAO;AACT;","names":[]}
1
+ {"version":3,"sources":["../src/index.ts","../src/storage/indexeddb.ts","../src/wire-contract.ts","../src/transport/websocket.ts"],"sourcesContent":["// Public entry point. Spawns the dedicated Worker that hosts the WASM instance and exposes a\n// Promise-based, identical-to-native API. Crypto runs off the main thread.\n\nimport type {\n AdmitChatEntry, AdmitChatOutcome,\n CatchupAppEvent, CatchupSnapshotView,\n ConversationId, ConversationMeta, DeviceId, IncomingMessage,\n HistoryBundle, MemberInfo,\n KeyPackageEntry, LinkingTicket, MessageEnvelope, RecoveryBackup, Storage, Transport, UserId,\n} from \"./types\";\nimport type { Request, Response } from \"./protocol\";\n\nexport type * from \"./types\";\n\n// Re-export the default Storage and Transport implementations from the main entry so consumers\n// whose bundlers don't honor package.json#exports (Metro with strict resolution, some legacy\n// Webpack configs) can still get them with a plain `import { ... } from \"ping-openmls-sdk\"`. Power\n// users who care about tree-shaking can keep using the subpath imports.\nexport { IndexedDbStorage } from \"./storage/indexeddb\";\nexport { WebSocketTransport } from \"./transport/websocket\";\n\n// Ping wire contract — version, content-type constants, and runtime validators. Internal\n// products MUST claim conformance; external consumers MAY adopt or run their own profile.\n// See docs/PING_WIRE_CONTRACT.md.\nexport {\n PING_WIRE_CONTRACT_VERSION,\n PING_WIRE_CONTRACT_HEADER,\n PING_WIRE_CONTRACT_HEADER_VALUE,\n WIRE_VERSION_MIN_ACCEPTED,\n CONTENT_TYPE_CBOR,\n CONTENT_TYPE_JSON,\n CONTENT_TYPE_CBOR_LEGACY_V1,\n CONTENT_TYPE_JSON_LEGACY_V1,\n ValidationError,\n validate,\n verifyApplicationContentHash,\n negotiates,\n} from \"./wire-contract\";\n\nexport interface ClientConfig {\n identityExport: Uint8Array;\n deviceLabel: string;\n storage: Storage;\n transport: Transport;\n /** Override the wall clock (test/sim only). */\n now?: () => number;\n /**\n * Optional 32-byte Ed25519 secret key. When supplied AND no\n * `LocalDevice` is yet persisted in `storage`, the SDK adopts this\n * as its device signing key — so `deviceId()` returns\n * `SHA-256(public_key_of(secret))`, deterministic from what the\n * caller passed.\n *\n * Use case: align the SDK's `device_id` (which the SDK stamps into\n * every envelope's `sender_device` field) with an externally-\n * computed device id — typically `SHA-256(device_signing_pubkey)`\n * in the host's auth layer, where the JWT carries that same value\n * as its `device_id` claim. Without this alignment, a server that\n * validates `envelope.sender_device == jwt.device_id` rejects every\n * upload with `sender_device_mismatch`.\n *\n * Ignored on subsequent inits (when `storage` already holds a\n * persisted `LocalDevice`) — the on-disk identity is authoritative\n * for stability across restarts.\n */\n deviceSigningSecretKey?: Uint8Array;\n}\n\nexport interface Conversation {\n readonly id: ConversationId;\n send(plaintext: Uint8Array): Promise<MessageEnvelope>;\n /** [CR-2] Add members by (device_id, KeyPackage) pair. Hosts typically pull these\n * straight from `transport.discoverDevices(userId)`. */\n addMembers(entries: KeyPackageEntry[]): Promise<void>;\n removeMembers(leafIndexes: number[]): Promise<void>;\n /** Change the conversation `name` (carried in the GroupContext) via an MLS\n * GroupContextExtensions commit — the rename / embedded avatar-media-id change\n * propagates to every member and future joiner through group STATE, not a side\n * broadcast. Pass `null` to clear. All members must have re-linked since the\n * group-name capability shipped, else the SDK throws. */\n setConversationName(name: string | null): Promise<void>;\n meta(): ConversationMeta;\n}\n\nexport class MessagingClient {\n private worker: Worker;\n private nextId = 1;\n private pending = new Map<number, { resolve: (v: unknown) => void; reject: (e: Error) => void }>();\n private storage: Storage;\n private transport: Transport;\n private now: () => number;\n private messageHandlers = new Set<(m: IncomingMessage) => void>();\n private conversationHandlers = new Set<(id: ConversationId, epoch: number, sender?: DeviceId) => void>();\n private subscription: { unsubscribe(): void } | null = null;\n private metaCache = new Map<string, ConversationMeta>();\n // Cached at init() so dispatchIncoming can skip envelopes we sent ourselves — the relay\n // broadcasts to all WS subscribers including the sender, and re-applying our own already-\n // merged commits would error with \"epoch differs\".\n private ownDeviceId: Uint8Array | null = null;\n\n private constructor(cfg: ClientConfig, worker: Worker) {\n this.worker = worker;\n this.storage = cfg.storage;\n this.transport = cfg.transport;\n this.now = cfg.now ?? (() => Date.now());\n this.worker.onmessage = (ev) => this.handleWorker(ev.data as Response);\n }\n\n /** Generate a fresh identity. The returned bytes are a secret — store them encrypted. */\n static async generateIdentity(): Promise<Uint8Array> {\n return MessagingClient.ephemeralWorkerCall<Uint8Array>({\n kind: \"generateIdentity\", id: 1,\n });\n }\n\n /**\n * HPKE-seal a `LinkingTicket` for delivery to a new device ([CR-3]).\n *\n * Returns CBOR-encoded bytes safe to relay through an untrusted inbox. The new device's\n * X25519 public key (`newDevicePub`) is exchanged out-of-band — typically as part of a\n * QR-encoded handshake on the linking screen. Pure function; runs in an ephemeral\n * worker so no live `MessagingClient` is required on the sender side.\n *\n * Suite: `DHKEM(X25519, HKDF-SHA256), HKDF-SHA256, AES-128-GCM` (matches MLS).\n */\n static async sealLinkingTicket(\n ticket: LinkingTicket,\n newDevicePub: Uint8Array,\n ): Promise<Uint8Array> {\n if (newDevicePub.length !== 32) {\n throw new Error(\"sealLinkingTicket: newDevicePub must be 32 bytes\");\n }\n return MessagingClient.ephemeralWorkerCall<Uint8Array>({\n kind: \"sealLinkingTicket\", id: 1, ticket, newDevicePub,\n });\n }\n\n /**\n * HPKE-open a sealed `LinkingTicket` on the new device ([CR-3]).\n *\n * The receiving device hasn't booted a `MessagingClient` yet — it only has its X25519\n * ephemeral keypair. This static method runs the open in an ephemeral worker, returning\n * the cleartext ticket which the host then feeds to `init()` + `consumeLinkingTicket()`.\n *\n * On failure the error message is deliberately generic (no discriminator between \"wrong\n * key\" vs \"tampered ciphertext\") so a probing attacker can't tell the new device's key\n * was the issue.\n */\n static async openLinkingTicket(\n sealed: Uint8Array,\n newDevicePriv: Uint8Array,\n ): Promise<LinkingTicket> {\n if (newDevicePriv.length !== 32) {\n throw new Error(\"openLinkingTicket: newDevicePriv must be 32 bytes\");\n }\n return MessagingClient.ephemeralWorkerCall<LinkingTicket>({\n kind: \"openLinkingTicket\", id: 1, sealed, newDevicePriv,\n });\n }\n\n /**\n * Generic HPKE-Base seal of arbitrary bytes to `recipientPub` (32-byte X25519)\n * under a caller-supplied `info`. One shared HPKE implementation so app layers\n * (e.g. the device-linking auth envelope) stop hand-rolling per-platform HPKE\n * that drifts and breaks cross-platform. Runs in an ephemeral worker.\n */\n static async hpkeSeal(\n plaintext: Uint8Array,\n recipientPub: Uint8Array,\n info: Uint8Array,\n ): Promise<Uint8Array> {\n if (recipientPub.length !== 32) {\n throw new Error(\"hpkeSeal: recipientPub must be 32 bytes\");\n }\n return MessagingClient.ephemeralWorkerCall<Uint8Array>({\n kind: \"hpkeSeal\", id: 1, plaintext, recipientPub, info,\n });\n }\n\n /** Generic HPKE-Base open — inverse of {@link hpkeSeal}. `info` must match the sender. */\n static async hpkeOpen(\n sealed: Uint8Array,\n recipientPriv: Uint8Array,\n info: Uint8Array,\n ): Promise<Uint8Array> {\n if (recipientPriv.length !== 32) {\n throw new Error(\"hpkeOpen: recipientPriv must be 32 bytes\");\n }\n return MessagingClient.ephemeralWorkerCall<Uint8Array>({\n kind: \"hpkeOpen\", id: 1, sealed, recipientPriv, info,\n });\n }\n\n // Spawn-call-terminate helper for stateless WASM functions exposed at module scope (no\n // PingClient required). Each call gets a fresh worker; the cost is acceptable because\n // these are one-shot, off-the-hot-path operations (identity gen, linking ticket seal/open).\n private static ephemeralWorkerCall<T>(req: Request): Promise<T> {\n const w = new Worker(new URL(\"./worker.js\", import.meta.url), { type: \"module\" });\n return new Promise<T>((resolve, reject) => {\n w.onmessage = (ev) => {\n const msg = ev.data as Response;\n if (msg.kind === \"result\" && msg.id === (req as { id: number }).id) {\n w.terminate();\n msg.ok ? resolve(msg.value as T) : reject(new Error(msg.error));\n }\n };\n w.postMessage(req);\n });\n }\n\n static async init(cfg: ClientConfig): Promise<MessagingClient> {\n const worker = new Worker(new URL(\"./worker.js\", import.meta.url), { type: \"module\" });\n const client = new MessagingClient(cfg, worker);\n\n await client.call({\n kind: \"init\",\n id: client.nextId++,\n identityExport: cfg.identityExport,\n deviceLabel: cfg.deviceLabel,\n nowMs: client.now(),\n ...(cfg.deviceSigningSecretKey\n ? { deviceSigningSecretKey: cfg.deviceSigningSecretKey }\n : {}),\n });\n\n // Cache the device id once so we can drop envelopes we sent ourselves on the WS echo.\n client.ownDeviceId = await client.deviceId();\n\n // Start the live subscription. We dispatch each envelope to the right handler:\n // - Welcome envelopes for unknown conversations → joinConversation\n // - Welcomes for already-joined conversations → drop (server may broadcast duplicates)\n // - Welcomes addressed to other devices → drop (we can't decrypt)\n // - Everything else → processEnvelope\n client.subscription = cfg.transport.subscribe((envelope) => {\n void client.dispatchIncoming(envelope);\n });\n return client;\n }\n\n /** Internal: route an inbound envelope to the right handler. */\n private async dispatchIncoming(envelope: MessageEnvelope): Promise<void> {\n // The relay broadcasts every envelope to every WS subscriber, including the sender. For\n // our own sends, the local MLS state has already been advanced in-process — re-applying\n // would either fail with \"epoch differs\" (commits) or be a no-op dedupe (application\n // messages). Skip them here.\n if (this.ownDeviceId && sameBytesU8(envelope.sender_device, this.ownDeviceId)) {\n return;\n }\n if (envelope.kind === \"Welcome\") {\n try {\n const known = await this.listConversations();\n const already = known.some((m) => sameBytesU8(m.id, envelope.conversation_id));\n if (already) return;\n await this.joinConversation(envelope);\n } catch (e) {\n // Welcomes are broadcast to every connected client; only one device's KeyPackage can\n // decrypt each one, so most fail. Log at debug, drop silently.\n if (typeof console !== \"undefined\") console.debug(\"[ping-sdk] welcome ignored:\", e);\n }\n return;\n }\n try {\n await this.processEnvelope(envelope);\n } catch (e) {\n if (typeof console !== \"undefined\") console.debug(\"[ping-sdk] envelope drop:\", e);\n }\n }\n\n // ------------------------ Public API ------------------------\n\n async userId(): Promise<UserId> {\n return await this.call({ kind: \"userId\", id: this.nextId++ }) as UserId;\n }\n\n /**\n * Export the account identity seed (CBOR-wrapped Ed25519, **SECRET**).\n * Transfer to a newly-linked device over the sealed linking channel so all of\n * an account's devices share one `userId` — the basis for cross-device\n * self-attribution (`IncomingMessage.sender_user_id === userId()`). Never log\n * or persist in cleartext.\n */\n async exportIdentity(): Promise<Uint8Array> {\n return await this.call({ kind: \"exportIdentity\", id: this.nextId++ }) as Uint8Array;\n }\n\n async deviceId(): Promise<DeviceId> {\n return await this.call({ kind: \"deviceId\", id: this.nextId++ }) as DeviceId;\n }\n\n async freshKeyPackage(): Promise<Uint8Array> {\n return await this.call({ kind: \"freshKeyPackage\", id: this.nextId++ }) as Uint8Array;\n }\n\n async createConversation(opts: { name?: string } = {}): Promise<Conversation> {\n const idBytes = await this.call({\n kind: \"createConversation\", id: this.nextId++, name: opts.name ?? null, nowMs: this.now(),\n }) as ConversationId;\n return this.conversationProxy(idBytes);\n }\n\n async joinConversation(welcome: MessageEnvelope): Promise<Conversation> {\n const idBytes = await this.call({\n kind: \"joinConversation\", id: this.nextId++, welcome, nowMs: this.now(),\n }) as ConversationId;\n return this.conversationProxy(idBytes);\n }\n\n /**\n * Get a `Conversation` proxy for an already-known conversation. Use this when you have a\n * conversation id (e.g., from `listConversations()`) and want to send/addMembers without\n * re-creating it. Throws if the id isn't known to the worker — call `listConversations()`\n * first to verify membership.\n */\n getConversation(id: ConversationId): Conversation {\n return this.conversationProxy(id);\n }\n\n async listConversations(): Promise<ConversationMeta[]> {\n const metas = await this.call({ kind: \"listConversations\", id: this.nextId++ }) as ConversationMeta[];\n this.metaCache.clear();\n for (const m of metas) this.metaCache.set(hex(m.id), m);\n return metas;\n }\n\n /**\n * Member roster for a conversation, recovered locally from the MLS\n * group's leaf credentials (no network, no out-of-band message). Returns\n * one entry per device leaf — a multi-device user appears multiple times,\n * so dedup by `user_id` for a per-user roster. Empty for an unknown id.\n */\n async members(conv: ConversationId): Promise<MemberInfo[]> {\n return await this.call({ kind: \"members\", id: this.nextId++, conv }) as MemberInfo[];\n }\n\n async syncConversations(): Promise<IncomingMessage[]> {\n return await this.call({\n kind: \"syncConversations\", id: this.nextId++, nowMs: this.now(),\n }) as IncomingMessage[];\n }\n\n async processEnvelope(envelope: MessageEnvelope): Promise<IncomingMessage | null> {\n return await this.call({\n kind: \"processEnvelope\", id: this.nextId++, envelope, nowMs: this.now(),\n }) as IncomingMessage | null;\n }\n\n /**\n * Build a `LinkingTicket` for a new device.\n *\n * [CR-13] `lastAppEvents` is host-supplied per-conversation \"what you missed\" data\n * (decrypted AppEvent bytes the new device will render before sync catches up).\n * Pass `[]` to suppress catchup data — the new device will see an empty conversation\n * list until normal sync runs.\n *\n * Caller is responsible for HPKE-sealing the returned ticket via\n * `MessagingClient.sealLinkingTicket` before transmitting (CR-3).\n */\n async buildLinkingTicket(\n newDeviceId: DeviceId,\n newDeviceKp: Uint8Array,\n lastAppEvents: CatchupAppEvent[] = [],\n ): Promise<LinkingTicket> {\n return await this.call({\n kind: \"buildLinkingTicket\", id: this.nextId++,\n newDeviceId, newDeviceKp, lastAppEvents, nowMs: this.now(),\n }) as LinkingTicket;\n }\n\n /**\n * Decode a `LinkingTicket.catchup_snapshot` blob ([CR-13]).\n *\n * Pure WASM — runs in an ephemeral worker so the new device can call this before\n * `MessagingClient.init()` completes (just like `openLinkingTicket`). Returns the\n * structured view: per-conversation metas + last-known AppEvent bytes.\n *\n * Throws on malformed or oversized inputs. Empty bytes return `null` — that's the\n * \"no catchup data\" case (sender passed `[]` to buildLinkingTicket).\n */\n static async decodeCatchupSnapshot(\n snapshotBytes: Uint8Array,\n ): Promise<CatchupSnapshotView | null> {\n if (snapshotBytes.length === 0) return null;\n return MessagingClient.ephemeralWorkerCall<CatchupSnapshotView>({\n kind: \"decodeCatchupSnapshot\", id: 1, snapshotBytes,\n });\n }\n\n async consumeLinkingTicket(ticket: LinkingTicket): Promise<void> {\n await this.call({\n kind: \"consumeLinkingTicket\", id: this.nextId++, ticket, nowMs: this.now(),\n });\n }\n\n // ------------------------ Recovery (account backup) ------------------------\n // All static + stateless: they run on an ephemeral worker, so the recovery screen can\n // call them before any client is initialised. Same Argon2id/AES-256-GCM IdentityBackup\n // wire format as the iOS/Android FFI bindings — backups are portable across platforms.\n\n /** Generate a fresh 12-word BIP39 mnemonic, returned as its canonical phrase string. */\n static async generateMnemonicPhrase(): Promise<string> {\n return MessagingClient.ephemeralWorkerCall<string>({\n kind: \"generateMnemonicPhrase\", id: 1,\n });\n }\n\n /** Validate + canonicalise a user-entered phrase (BIP39 wordlist + checksum). Rejects\n * malformed input. Use on the recovery screen before calling `decryptBackup`. */\n static async normalizeMnemonicPhrase(phrase: string): Promise<string> {\n return MessagingClient.ephemeralWorkerCall<string>({\n kind: \"normalizeMnemonicPhrase\", id: 1, phrase,\n });\n }\n\n /** Encrypt an IdentityBackup for `POST /v1/recovery/blobs`. `deviceGroupSnapshot` may be\n * empty (Conservative path) or the DeviceGroup's `exportConversationStateSnapshot`. */\n static async encryptBackup(args: {\n mnemonicPhrase: string;\n accountId: Uint8Array;\n identityExport: Uint8Array;\n deviceGroupSnapshot: Uint8Array;\n /** External conversations to embed so recovery restores them offline. Pass\n * `[]` (the default) for identity + DeviceGroup only. Each `groupStateBytes`\n * is `Conversation.exportConversationStateSnapshot` output. Forward-secrecy\n * trade-off — see the SDK recovery docs. */\n conversationSnapshots?: { conversationId: Uint8Array; groupStateBytes: Uint8Array }[];\n createdAtMs: number;\n }): Promise<Uint8Array> {\n return MessagingClient.ephemeralWorkerCall<Uint8Array>({\n kind: \"encryptBackup\", id: 1,\n mnemonicPhrase: args.mnemonicPhrase,\n accountId: args.accountId,\n identityExport: args.identityExport,\n deviceGroupSnapshot: args.deviceGroupSnapshot,\n conversationSnapshots: args.conversationSnapshots ?? [],\n createdAtMs: args.createdAtMs,\n });\n }\n\n /** Decrypt a recovery blob from `GET /v1/recovery/blobs`. Throws on wrong mnemonic or a\n * tampered blob. The returned `identity_export` is the secret seed for `init`. */\n static async decryptBackup(\n blob: Uint8Array,\n mnemonicPhrase: string,\n ): Promise<RecoveryBackup> {\n return MessagingClient.ephemeralWorkerCall<RecoveryBackup>({\n kind: \"decryptBackup\", id: 1, blob, mnemonicPhrase,\n });\n }\n\n // ------------------------ Peer-assisted history transfer ------------------------\n // Authenticated (sign-then-encrypt) device-to-device history used by linking and\n // post-recovery peer re-add. Confidential to the recipient's ephemeral X25519 key AND\n // signed by the account identity key, so the receiver proves the sender. Plaintext-only\n // share — preserves forward secrecy (no MLS keys leave the device).\n\n /** Seal+sign a `HistoryBundle` for a recipient device. `recipientPub` (X25519, 32B) is the\n * recipient's ephemeral pubkey from the linking QR; `senderIdentitySecret` (Ed25519 seed,\n * 32B) is this account's identity key. Returns the blob to POST to /v1/history-transfers. */\n static async sealHistoryBundle(\n bundle: HistoryBundle,\n recipientPub: Uint8Array,\n senderIdentitySecret: Uint8Array,\n ): Promise<Uint8Array> {\n return MessagingClient.ephemeralWorkerCall<Uint8Array>({\n kind: \"sealHistoryBundle\", id: 1, bundle, recipientPub, senderIdentitySecret,\n });\n }\n\n /** Open+verify a sealed history bundle. `recipientPriv` (X25519, 32B) is this device's\n * ephemeral private key; `senderIdentityPub` (Ed25519, 32B) is the account identity pubkey\n * from the linking handshake. Throws if the signature doesn't verify. */\n static async openHistoryBundle(\n sealed: Uint8Array,\n recipientPriv: Uint8Array,\n senderIdentityPub: Uint8Array,\n ): Promise<HistoryBundle> {\n return MessagingClient.ephemeralWorkerCall<HistoryBundle>({\n kind: \"openHistoryBundle\", id: 1, sealed, recipientPriv, senderIdentityPub,\n });\n }\n\n /**\n * Admit a freshly-linked device to every chat in `entries` — one MLS\n * Commit + Welcome per chat, with the Welcome's `?recipient=` priming\n * handled inside the SDK. This is the post-link reconciliation step\n * that follows `consumeLinkingTicket`: the new device has the\n * DeviceGroup (from the ticket) and metadata for external chats (from\n * the catchup snapshot), but its leaf is not yet in any external\n * MLS group. The host on the EXISTING device calls this once the\n * new device shows up in `/v1/auth/sessions` and has uploaded its\n * KeyPackage batch, passing one freshly-claimed KP per chat.\n *\n * Per-chat failures (stale KP, transport error, unknown chat) are\n * captured in the returned outcomes rather than aborting — one bad\n * chat shouldn't lose the user access to all the others.\n */\n async admitDeviceToChats(\n deviceId: DeviceId,\n entries: AdmitChatEntry[],\n ): Promise<AdmitChatOutcome[]> {\n return await this.call({\n kind: \"admitDeviceToChats\", id: this.nextId++,\n deviceId, entries, nowMs: this.now(),\n }) as AdmitChatOutcome[];\n }\n\n /**\n * Revoke a device ([CR-2]).\n *\n * Removes `deviceId`'s leaf from every conversation where this client locally\n * tracked the device→leaf mapping (i.e. conversations where this client itself\n * admitted the device via `addMembers`, or where this client is the device being\n * revoked). The SDK has already broadcast each Commit envelope via\n * `transport.send`; the returned array is for any additional host-side handling\n * (audit logs, UI). An empty array is a valid outcome — the device wasn't locally\n * known anywhere.\n *\n * **Scope note.** Conversations where a peer admitted the device on this client's\n * behalf are silently skipped — the leaf index isn't locally known. Host can fall\n * back to `transport.discoverDevices` + a manual `remove_members(leafIndex)` for\n * those, or wait for the affected user to revoke from a device that did the admit.\n * See `docs/architecture/multi-device.md §Device removal`.\n */\n async revokeDevice(deviceId: DeviceId): Promise<MessageEnvelope[]> {\n return await this.call({\n kind: \"revokeDevice\", id: this.nextId++, deviceId, nowMs: this.now(),\n }) as MessageEnvelope[];\n }\n\n /**\n * Export a portable MLS state snapshot for one conversation ([CR-7]).\n *\n * Returned bytes can be embedded in a recovery blob or shipped through a\n * linking ticket so another device of the same user identity can re-attach to\n * the group via [[importStateSnapshot]]. The bytes contain past epoch secrets;\n * never log, never persist unencrypted, wipe (`.fill(0)`) after use.\n */\n async exportConversationStateSnapshot(conv: ConversationId): Promise<Uint8Array> {\n return await this.call({\n kind: \"exportConversationStateSnapshot\", id: this.nextId++,\n conv, nowMs: this.now(),\n }) as Uint8Array;\n }\n\n /**\n * Import a `GroupStateSnapshot` from another device of the same user identity\n * ([CR-7]).\n *\n * On success, the conversation appears in [[listConversations]] and decryption\n * of subsequent peer-originated traffic works against the imported state. For\n * full multi-device participation, the recovered device SHOULD then issue an\n * MLS Update Proposal to rotate onto a fresh leaf (post-v1 follow-up — see\n * `docs/design/CR4_CR7_PERSISTENCE.md` open question #3).\n */\n async importStateSnapshot(snapshotBytes: Uint8Array): Promise<ConversationId> {\n return await this.call({\n kind: \"importStateSnapshot\", id: this.nextId++,\n snapshotBytes, nowMs: this.now(),\n }) as ConversationId;\n }\n\n /**\n * Export a derived secret from a conversation's MLS exporter ([CR-8]).\n *\n * Used to seed the ephemeral channel (`ping/ephemeral`), call-media keys\n * (`ping/calls/media/<call_id>`), and call-ephemeral framer keys\n * (`ping/calls/ephemeral/<call_id>`). The returned bytes are a secret — never log,\n * persist only encrypted, and call `bytes.fill(0)` after use to wipe the typed array.\n *\n * Same `(conversationId, current_epoch, label, context, length)` produces the same\n * bytes across every binding (Rust core, native UniFFI, WASM/JS). Cross-platform\n * byte-equality is enforced by `protocol/conformance/export_secret/`.\n */\n async exportConversationSecret(\n conv: ConversationId,\n label: string,\n context: Uint8Array,\n length: number,\n ): Promise<Uint8Array> {\n if (length <= 0 || length > 1024) {\n throw new Error(\"exportConversationSecret: length must be 1..=1024\");\n }\n return await this.call({\n kind: \"exportConversationSecret\", id: this.nextId++,\n conv, label, context, length,\n }) as Uint8Array;\n }\n\n /** Subscribe to decrypted application messages. */\n onMessage(handler: (m: IncomingMessage) => void): () => void {\n this.messageHandlers.add(handler);\n return () => this.messageHandlers.delete(handler);\n }\n\n /** Fired after every state-changing event for a conversation (Commit, member changes). */\n /**\n * Fires after every conversation state change — joining via Welcome, processing a Commit,\n * etc. `sender` carries the device id of whoever triggered the event when known (e.g. the\n * inviter's device on auto-join), so the host can label peers in its UI.\n */\n onConversationUpdated(handler: (id: ConversationId, epoch: number, sender?: DeviceId) => void): () => void {\n this.conversationHandlers.add(handler);\n return () => this.conversationHandlers.delete(handler);\n }\n\n async close(): Promise<void> {\n this.subscription?.unsubscribe();\n this.worker.terminate();\n }\n\n // ------------------------ Internals ------------------------\n\n private conversationProxy(id: ConversationId): Conversation {\n const self = this;\n return {\n id,\n async send(plaintext) {\n return await self.call({\n kind: \"send\", id: self.nextId++, conv: id, plaintext, nowMs: self.now(),\n }) as MessageEnvelope;\n },\n async addMembers(entries) {\n await self.call({\n kind: \"addMembers\", id: self.nextId++, conv: id, entries, nowMs: self.now(),\n });\n },\n async removeMembers(leaves) {\n await self.call({\n kind: \"removeMembers\", id: self.nextId++, conv: id, leaves, nowMs: self.now(),\n });\n },\n async setConversationName(name) {\n await self.call({\n kind: \"setConversationName\", id: self.nextId++, conv: id, name, nowMs: self.now(),\n });\n },\n meta() {\n const cached = self.metaCache.get(hex(id));\n if (!cached) throw new Error(\"conversation meta not loaded; call listConversations() first\");\n return cached;\n },\n };\n }\n\n private call(req: Request): Promise<unknown> {\n return new Promise((resolve, reject) => {\n this.pending.set((req as { id: number }).id, { resolve, reject });\n this.worker.postMessage(req);\n });\n }\n\n private async handleWorker(msg: Response) {\n switch (msg.kind) {\n case \"result\": {\n const slot = this.pending.get(msg.id);\n if (!slot) return;\n this.pending.delete(msg.id);\n msg.ok ? slot.resolve(msg.value) : slot.reject(new Error(msg.error));\n return;\n }\n case \"event.message\": {\n for (const h of this.messageHandlers) h(msg.msg);\n return;\n }\n case \"event.conversation\": {\n for (const h of this.conversationHandlers) h(msg.id, msg.epoch, msg.sender);\n return;\n }\n case \"storage.call\": {\n try {\n const v = await (this.storage as unknown as Record<string, (...a: unknown[]) => unknown>)\n [msg.method]?.(...msg.args);\n this.worker.postMessage({ kind: \"storage.reply\", id: msg.id, ok: true, value: v } as Request);\n } catch (e) {\n this.worker.postMessage({\n kind: \"storage.reply\", id: msg.id, ok: false, error: String(e),\n } as Request);\n }\n return;\n }\n case \"transport.call\": {\n try {\n const v = await (this.transport as unknown as Record<string, (...a: unknown[]) => unknown>)\n [msg.method]?.(...msg.args);\n this.worker.postMessage({ kind: \"transport.reply\", id: msg.id, ok: true, value: v } as Request);\n } catch (e) {\n this.worker.postMessage({\n kind: \"transport.reply\", id: msg.id, ok: false, error: String(e),\n } as Request);\n }\n return;\n }\n }\n }\n}\n\nfunction hex(b: Uint8Array): string {\n let s = \"\";\n for (const v of b) s += v.toString(16).padStart(2, \"0\");\n return s;\n}\n\nfunction sameBytesU8(a: Uint8Array, b: Uint8Array): boolean {\n if (a.length !== b.length) return false;\n for (let i = 0; i < a.length; i++) if (a[i] !== b[i]) return false;\n return true;\n}\n","// Default IndexedDB-backed Storage implementation. Hosts can supply their own.\n\nimport type { Storage } from \"../types\";\n\nconst DB_NAME = \"ping-sdk\";\nconst DB_VERSION = 1;\nconst STORE = \"kv\";\n\ninterface Row { ns: string; key: string; value: Uint8Array }\n\nfunction open(): Promise<IDBDatabase> {\n return new Promise((resolve, reject) => {\n const req = indexedDB.open(DB_NAME, DB_VERSION);\n req.onupgradeneeded = () => {\n const db = req.result;\n if (!db.objectStoreNames.contains(STORE)) {\n const os = db.createObjectStore(STORE, { keyPath: [\"ns\", \"key\"] });\n os.createIndex(\"ns\", \"ns\", { unique: false });\n }\n };\n req.onsuccess = () => resolve(req.result);\n req.onerror = () => reject(req.error);\n });\n}\n\nexport class IndexedDbStorage implements Storage {\n private dbPromise: Promise<IDBDatabase>;\n\n constructor() { this.dbPromise = open(); }\n\n async get(namespace: string, key: string): Promise<Uint8Array | null> {\n const db = await this.dbPromise;\n return new Promise((resolve, reject) => {\n const tx = db.transaction(STORE, \"readonly\");\n const r = tx.objectStore(STORE).get([namespace, key]);\n r.onsuccess = () => resolve(((r.result as Row | undefined)?.value) ?? null);\n r.onerror = () => reject(r.error);\n });\n }\n\n async put(namespace: string, key: string, value: Uint8Array): Promise<void> {\n const db = await this.dbPromise;\n return new Promise((resolve, reject) => {\n const tx = db.transaction(STORE, \"readwrite\");\n tx.objectStore(STORE).put({ ns: namespace, key, value } as Row);\n tx.oncomplete = () => resolve();\n tx.onerror = () => reject(tx.error);\n });\n }\n\n async del(namespace: string, key: string): Promise<void> {\n const db = await this.dbPromise;\n return new Promise((resolve, reject) => {\n const tx = db.transaction(STORE, \"readwrite\");\n tx.objectStore(STORE).delete([namespace, key]);\n tx.oncomplete = () => resolve();\n tx.onerror = () => reject(tx.error);\n });\n }\n\n async listKeys(namespace: string, prefix: string): Promise<string[]> {\n const db = await this.dbPromise;\n return new Promise((resolve, reject) => {\n const tx = db.transaction(STORE, \"readonly\");\n const idx = tx.objectStore(STORE).index(\"ns\");\n const req = idx.openCursor(IDBKeyRange.only(namespace));\n const out: string[] = [];\n req.onsuccess = () => {\n const cur = req.result;\n if (!cur) return resolve(out);\n const row = cur.value as Row;\n if (!prefix || row.key.startsWith(prefix)) out.push(row.key);\n cur.continue();\n };\n req.onerror = () => reject(req.error);\n });\n }\n}\n","// Ping wire contract — TypeScript counterpart to the `ping-wire-contract` Rust crate.\n//\n// MUST stay in lockstep with `core/ping-wire-contract/src/lib.rs`. The full normative spec\n// lives in `docs/PING_WIRE_CONTRACT.md`.\n//\n// What this module is for: code paths that cross the network boundary (transports, relays,\n// service workers) that need to advertise or validate ping-wire-contract conformance from\n// JavaScript without round-tripping through WASM.\n\nimport type { MessageEnvelope } from \"./types\";\n\n/**\n * Ping wire contract version. Bumped on incompatible profile changes.\n *\n * Currently `2` — CR-6 changes the `content_hash` semantics for `Application` envelopes\n * (now hashed over plaintext, not the MLS ciphertext). Receivers run a 90-day dual-accept\n * window per Q-ARCH-02; see [[validate]] for the per-version routing.\n */\nexport const PING_WIRE_CONTRACT_VERSION = 2;\n\n/** Lowest envelope version accepted by [[validate]] during the dual-accept window. */\nexport const WIRE_VERSION_MIN_ACCEPTED = 1;\n\n/** Canonical CBOR content type. Production transports SHOULD use this. */\nexport const CONTENT_TYPE_CBOR = \"application/vnd.ping.wire.v2+cbor\";\n\n/** JSON projection — development and tooling only. Production MUST NOT use this. */\nexport const CONTENT_TYPE_JSON = \"application/vnd.ping.wire.v2+json\";\n\n/** Legacy v1 CBOR type, accepted during the CR-6 dual-accept window. */\nexport const CONTENT_TYPE_CBOR_LEGACY_V1 = \"application/vnd.ping.wire.v1+cbor\";\n\n/** Legacy v1 JSON type, accepted during the CR-6 dual-accept window. */\nexport const CONTENT_TYPE_JSON_LEGACY_V1 = \"application/vnd.ping.wire.v1+json\";\n\n/** Header used over transports that don't surface a content type (e.g. WebSocket). */\nexport const PING_WIRE_CONTRACT_HEADER = \"X-Ping-Wire-Contract\";\n\n/** Header value: `ping/<VERSION>`. */\nexport const PING_WIRE_CONTRACT_HEADER_VALUE = `ping/${PING_WIRE_CONTRACT_VERSION}`;\n\nexport type ValidationErrorKind =\n | \"UnsupportedVersion\"\n | \"BadConversationIdLen\"\n | \"BadSenderDeviceLen\"\n | \"ContentHashMismatch\";\n\nexport class ValidationError extends Error {\n constructor(public readonly kind: ValidationErrorKind, message: string) {\n super(message);\n this.name = \"PingWireContractValidationError\";\n }\n}\n\nconst KIND_DISCRIMINANT: Record<MessageEnvelope[\"kind\"], number> = {\n Application: 1,\n Commit: 2,\n Welcome: 3,\n Proposal: 4,\n KeyPackage: 5,\n};\n\nasync function sha256(bytes: Uint8Array): Promise<Uint8Array> {\n // `crypto.subtle` is available in browsers, Node 20+, and Workers. `bytes.buffer`\n // is typed as `ArrayBufferLike` which TS won't accept as `BufferSource` (the union\n // includes `SharedArrayBuffer`); pass an explicit ArrayBuffer slice instead.\n const ab = bytes.buffer.slice(bytes.byteOffset, bytes.byteOffset + bytes.byteLength) as ArrayBuffer;\n const hash = await crypto.subtle.digest(\"SHA-256\", ab);\n return new Uint8Array(hash);\n}\n\nfunction bytesEqual(a: Uint8Array, b: Uint8Array): boolean {\n if (a.length !== b.length) return false;\n for (let i = 0; i < a.length; i++) if (a[i] !== b[i]) return false;\n return true;\n}\n\n/**\n * Validate an envelope against the ping-wire-contract.\n *\n * Receivers claiming contract conformance MUST call this before applying any envelope.\n * External consumers using the raw envelope under their own profile are not bound by\n * these rules.\n *\n * CR-6 dual-accept window: `v ∈ [WIRE_VERSION_MIN_ACCEPTED, PING_WIRE_CONTRACT_VERSION]`\n * passes the version gate. For v=2 `Application` envelopes the content_hash is over\n * plaintext, so this function skips that check — callers MUST run\n * [[verifyApplicationContentHash]] after MLS decrypt to close the loop.\n */\nexport async function validate(env: MessageEnvelope): Promise<void> {\n if (env.v < WIRE_VERSION_MIN_ACCEPTED || env.v > PING_WIRE_CONTRACT_VERSION) {\n throw new ValidationError(\n \"UnsupportedVersion\",\n `unsupported wire-contract version: envelope says v=${env.v}, expected v∈[${WIRE_VERSION_MIN_ACCEPTED}, ${PING_WIRE_CONTRACT_VERSION}]`,\n );\n }\n if (env.conversation_id.length !== 16) {\n throw new ValidationError(\n \"BadConversationIdLen\",\n `conversation_id must be 16 bytes (ULID), got ${env.conversation_id.length}`,\n );\n }\n if (env.sender_device.length !== 32) {\n throw new ValidationError(\n \"BadSenderDeviceLen\",\n `sender_device must be 32 bytes, got ${env.sender_device.length}`,\n );\n }\n // CR-6 routing: skip the hash check for v=2 application envelopes.\n if (env.v >= 2 && env.kind === \"Application\") {\n return;\n }\n const buf = new Uint8Array(1 + env.payload.length);\n buf[0] = KIND_DISCRIMINANT[env.kind];\n buf.set(env.payload, 1);\n const computed = await sha256(buf);\n if (!bytesEqual(computed, env.content_hash)) {\n throw new ValidationError(\n \"ContentHashMismatch\",\n \"content_hash mismatch: envelope is forged, corrupted, or built by a non-conformant sender\",\n );\n }\n}\n\n/**\n * Verify a v=2 `Application` envelope's content_hash against the decrypted plaintext.\n *\n * Caller MUST run this after MLS decrypt for application envelopes whose `v >= 2`. For\n * v=1 envelopes or handshake kinds this is a no-op (those are covered by [[validate]]).\n */\nexport async function verifyApplicationContentHash(\n env: MessageEnvelope,\n plaintext: Uint8Array,\n): Promise<void> {\n if (env.v < 2 || env.kind !== \"Application\") return;\n const computed = await sha256(plaintext);\n if (!bytesEqual(computed, env.content_hash)) {\n throw new ValidationError(\n \"ContentHashMismatch\",\n \"v=2 application content_hash mismatch against decrypted plaintext\",\n );\n }\n}\n\n/**\n * True if the supplied content type negotiates the ping-wire-contract.\n *\n * Accepts the v2 canonical types AND the v1 legacy types during the CR-6 dual-accept\n * window. Tolerates whitespace and parameters (e.g. `; charset=utf-8`).\n */\nexport function negotiates(contentType: string | null | undefined): boolean {\n if (!contentType) return false;\n const primary = contentType.split(\";\")[0]?.trim().toLowerCase() ?? \"\";\n return (\n primary === CONTENT_TYPE_CBOR\n || primary === CONTENT_TYPE_JSON\n || primary === CONTENT_TYPE_CBOR_LEGACY_V1\n || primary === CONTENT_TYPE_JSON_LEGACY_V1\n );\n}\n","// Reference WebSocket transport. Treats the server as an opaque relay that:\n// - accepts a CBOR-encoded MessageEnvelope on `send`\n// - returns events newer than a cursor on `fetchSince`\n// - streams new events on the WS itself for `subscribe`\n// - exposes a directory endpoint for `discoverDevices`\n//\n// This is a sample. Real deployments will tune framing / auth.\n\nimport type { ConversationId, DiscoveredDevice, MessageEnvelope, Transport, UserId } from \"../types\";\nimport {\n CONTENT_TYPE_JSON,\n PING_WIRE_CONTRACT_HEADER,\n PING_WIRE_CONTRACT_HEADER_VALUE,\n} from \"../wire-contract\";\n\nexport interface WsTransportConfig {\n baseUrl: string; // ws[s]://...\n authHeader?: string; // optional bearer token\n fetchImpl?: typeof fetch; // override for SSR / tests\n}\n\nexport class WebSocketTransport implements Transport {\n private ws: WebSocket | null = null;\n private liveHandlers = new Set<(env: MessageEnvelope) => void>();\n private fetchImpl: typeof fetch;\n\n constructor(private cfg: WsTransportConfig) {\n this.fetchImpl = cfg.fetchImpl ?? globalThis.fetch.bind(globalThis);\n this.openSocket();\n }\n\n private openSocket() {\n if (typeof WebSocket === \"undefined\") return;\n const url = `${this.cfg.baseUrl.replace(/^http/, \"ws\")}/stream`;\n this.ws = new WebSocket(url);\n this.ws.onmessage = (ev) => {\n // Server sends one JSON envelope per text frame.\n const text = typeof ev.data === \"string\" ? ev.data : new TextDecoder().decode(ev.data as ArrayBuffer);\n try {\n const env = JSON.parse(text, reviver) as MessageEnvelope;\n for (const h of this.liveHandlers) h(env);\n } catch (e) {\n if (typeof console !== \"undefined\") console.debug(\"[ping-sdk] WS frame parse error:\", e);\n }\n };\n this.ws.onclose = () => setTimeout(() => this.openSocket(), 1000);\n }\n\n async send(envelope: MessageEnvelope): Promise<void> {\n // We're emitting the JSON projection of the ping-wire-contract. Production transports\n // would use CONTENT_TYPE_CBOR with a real CBOR codec; for the demo path the JSON shape\n // is the documented development projection.\n const res = await this.fetchImpl(`${this.cfg.baseUrl}/send`, {\n method: \"POST\",\n headers: {\n \"content-type\": CONTENT_TYPE_JSON,\n [PING_WIRE_CONTRACT_HEADER]: PING_WIRE_CONTRACT_HEADER_VALUE,\n ...(this.cfg.authHeader ? { authorization: this.cfg.authHeader } : {}),\n },\n body: JSON.stringify(envelope, replacer),\n });\n if (res.status === 409) throw new Error(\"EpochOccupied\");\n if (!res.ok) throw new Error(`transport send failed: ${res.status}`);\n }\n\n async fetchSince(conversationId: ConversationId, cursor: Uint8Array, limit: number): Promise<MessageEnvelope[]> {\n const url = new URL(`${this.cfg.baseUrl}/sync`);\n url.searchParams.set(\"conv\", toHex(conversationId));\n url.searchParams.set(\"cursor\", btoaUrl(cursor));\n url.searchParams.set(\"limit\", String(limit));\n const res = await this.fetchImpl(url.toString(), {\n headers: { ...(this.cfg.authHeader ? { authorization: this.cfg.authHeader } : {}) },\n });\n if (!res.ok) throw new Error(`transport fetch failed: ${res.status}`);\n const text = await res.text();\n return JSON.parse(text, reviver) as MessageEnvelope[];\n }\n\n subscribe(onEvent: (env: MessageEnvelope) => void): { unsubscribe(): void } {\n this.liveHandlers.add(onEvent);\n return { unsubscribe: () => this.liveHandlers.delete(onEvent) };\n }\n\n async discoverDevices(userId: UserId): Promise<DiscoveredDevice[]> {\n const res = await this.fetchImpl(`${this.cfg.baseUrl}/devices/${toHex(userId)}`, {\n headers: { ...(this.cfg.authHeader ? { authorization: this.cfg.authHeader } : {}) },\n });\n if (!res.ok) throw new Error(`device discovery failed: ${res.status}`);\n const text = await res.text();\n return JSON.parse(text, reviver) as DiscoveredDevice[];\n }\n}\n\n// ---- Wire codec ----\n//\n// Demo wire format: JSON with byte fields wrapped as `{ _bytes: number[] }`. The relay's\n// `envFromJson`/`envToJson` use the same shape. The spec calls for CBOR; v0.2 swaps in a\n// real CBOR codec, but the JSON form is much friendlier to debug.\n//\n// (decodeEnvelope helpers used to live here; collapsed into inline JSON.parse calls above.)\n\nfunction replacer(_k: string, v: unknown): unknown {\n if (v instanceof Uint8Array) return { _bytes: Array.from(v) };\n return v;\n}\nfunction reviver(_k: string, v: unknown): unknown {\n if (v && typeof v === \"object\" && Array.isArray((v as { _bytes?: number[] })._bytes)) {\n return new Uint8Array((v as { _bytes: number[] })._bytes);\n }\n return v;\n}\n\nfunction toHex(b: Uint8Array): string {\n let s = \"\"; for (const v of b) s += v.toString(16).padStart(2, \"0\"); return s;\n}\nfunction btoaUrl(b: Uint8Array): string {\n let s = \"\"; for (const v of b) s += String.fromCharCode(v);\n return btoa(s).replace(/\\+/g, \"-\").replace(/\\//g, \"_\").replace(/=+$/, \"\");\n}\n"],"mappings":"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","names":[]}
package/dist/index.d.cts CHANGED
@@ -93,6 +93,12 @@ interface Conversation {
93
93
  * straight from `transport.discoverDevices(userId)`. */
94
94
  addMembers(entries: KeyPackageEntry[]): Promise<void>;
95
95
  removeMembers(leafIndexes: number[]): Promise<void>;
96
+ /** Change the conversation `name` (carried in the GroupContext) via an MLS
97
+ * GroupContextExtensions commit — the rename / embedded avatar-media-id change
98
+ * propagates to every member and future joiner through group STATE, not a side
99
+ * broadcast. Pass `null` to clear. All members must have re-linked since the
100
+ * group-name capability shipped, else the SDK throws. */
101
+ setConversationName(name: string | null): Promise<void>;
96
102
  meta(): ConversationMeta;
97
103
  }
98
104
  declare class MessagingClient {
package/dist/index.d.ts CHANGED
@@ -93,6 +93,12 @@ interface Conversation {
93
93
  * straight from `transport.discoverDevices(userId)`. */
94
94
  addMembers(entries: KeyPackageEntry[]): Promise<void>;
95
95
  removeMembers(leafIndexes: number[]): Promise<void>;
96
+ /** Change the conversation `name` (carried in the GroupContext) via an MLS
97
+ * GroupContextExtensions commit — the rename / embedded avatar-media-id change
98
+ * propagates to every member and future joiner through group STATE, not a side
99
+ * broadcast. Pass `null` to clear. All members must have re-linked since the
100
+ * group-name capability shipped, else the SDK throws. */
101
+ setConversationName(name: string | null): Promise<void>;
96
102
  meta(): ConversationMeta;
97
103
  }
98
104
  declare class MessagingClient {
package/dist/index.js CHANGED
@@ -750,6 +750,15 @@ var MessagingClient = class _MessagingClient {
750
750
  nowMs: self.now()
751
751
  });
752
752
  },
753
+ async setConversationName(name) {
754
+ await self.call({
755
+ kind: "setConversationName",
756
+ id: self.nextId++,
757
+ conv: id,
758
+ name,
759
+ nowMs: self.now()
760
+ });
761
+ },
753
762
  meta() {
754
763
  const cached = self.metaCache.get(hex(id));
755
764
  if (!cached) throw new Error("conversation meta not loaded; call listConversations() first");
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/storage/indexeddb.ts","../src/wire-contract.ts","../src/transport/websocket.ts","../src/index.ts"],"sourcesContent":["// Default IndexedDB-backed Storage implementation. Hosts can supply their own.\n\nimport type { Storage } from \"../types\";\n\nconst DB_NAME = \"ping-sdk\";\nconst DB_VERSION = 1;\nconst STORE = \"kv\";\n\ninterface Row { ns: string; key: string; value: Uint8Array }\n\nfunction open(): Promise<IDBDatabase> {\n return new Promise((resolve, reject) => {\n const req = indexedDB.open(DB_NAME, DB_VERSION);\n req.onupgradeneeded = () => {\n const db = req.result;\n if (!db.objectStoreNames.contains(STORE)) {\n const os = db.createObjectStore(STORE, { keyPath: [\"ns\", \"key\"] });\n os.createIndex(\"ns\", \"ns\", { unique: false });\n }\n };\n req.onsuccess = () => resolve(req.result);\n req.onerror = () => reject(req.error);\n });\n}\n\nexport class IndexedDbStorage implements Storage {\n private dbPromise: Promise<IDBDatabase>;\n\n constructor() { this.dbPromise = open(); }\n\n async get(namespace: string, key: string): Promise<Uint8Array | null> {\n const db = await this.dbPromise;\n return new Promise((resolve, reject) => {\n const tx = db.transaction(STORE, \"readonly\");\n const r = tx.objectStore(STORE).get([namespace, key]);\n r.onsuccess = () => resolve(((r.result as Row | undefined)?.value) ?? null);\n r.onerror = () => reject(r.error);\n });\n }\n\n async put(namespace: string, key: string, value: Uint8Array): Promise<void> {\n const db = await this.dbPromise;\n return new Promise((resolve, reject) => {\n const tx = db.transaction(STORE, \"readwrite\");\n tx.objectStore(STORE).put({ ns: namespace, key, value } as Row);\n tx.oncomplete = () => resolve();\n tx.onerror = () => reject(tx.error);\n });\n }\n\n async del(namespace: string, key: string): Promise<void> {\n const db = await this.dbPromise;\n return new Promise((resolve, reject) => {\n const tx = db.transaction(STORE, \"readwrite\");\n tx.objectStore(STORE).delete([namespace, key]);\n tx.oncomplete = () => resolve();\n tx.onerror = () => reject(tx.error);\n });\n }\n\n async listKeys(namespace: string, prefix: string): Promise<string[]> {\n const db = await this.dbPromise;\n return new Promise((resolve, reject) => {\n const tx = db.transaction(STORE, \"readonly\");\n const idx = tx.objectStore(STORE).index(\"ns\");\n const req = idx.openCursor(IDBKeyRange.only(namespace));\n const out: string[] = [];\n req.onsuccess = () => {\n const cur = req.result;\n if (!cur) return resolve(out);\n const row = cur.value as Row;\n if (!prefix || row.key.startsWith(prefix)) out.push(row.key);\n cur.continue();\n };\n req.onerror = () => reject(req.error);\n });\n }\n}\n","// Ping wire contract — TypeScript counterpart to the `ping-wire-contract` Rust crate.\n//\n// MUST stay in lockstep with `core/ping-wire-contract/src/lib.rs`. The full normative spec\n// lives in `docs/PING_WIRE_CONTRACT.md`.\n//\n// What this module is for: code paths that cross the network boundary (transports, relays,\n// service workers) that need to advertise or validate ping-wire-contract conformance from\n// JavaScript without round-tripping through WASM.\n\nimport type { MessageEnvelope } from \"./types\";\n\n/**\n * Ping wire contract version. Bumped on incompatible profile changes.\n *\n * Currently `2` — CR-6 changes the `content_hash` semantics for `Application` envelopes\n * (now hashed over plaintext, not the MLS ciphertext). Receivers run a 90-day dual-accept\n * window per Q-ARCH-02; see [[validate]] for the per-version routing.\n */\nexport const PING_WIRE_CONTRACT_VERSION = 2;\n\n/** Lowest envelope version accepted by [[validate]] during the dual-accept window. */\nexport const WIRE_VERSION_MIN_ACCEPTED = 1;\n\n/** Canonical CBOR content type. Production transports SHOULD use this. */\nexport const CONTENT_TYPE_CBOR = \"application/vnd.ping.wire.v2+cbor\";\n\n/** JSON projection — development and tooling only. Production MUST NOT use this. */\nexport const CONTENT_TYPE_JSON = \"application/vnd.ping.wire.v2+json\";\n\n/** Legacy v1 CBOR type, accepted during the CR-6 dual-accept window. */\nexport const CONTENT_TYPE_CBOR_LEGACY_V1 = \"application/vnd.ping.wire.v1+cbor\";\n\n/** Legacy v1 JSON type, accepted during the CR-6 dual-accept window. */\nexport const CONTENT_TYPE_JSON_LEGACY_V1 = \"application/vnd.ping.wire.v1+json\";\n\n/** Header used over transports that don't surface a content type (e.g. WebSocket). */\nexport const PING_WIRE_CONTRACT_HEADER = \"X-Ping-Wire-Contract\";\n\n/** Header value: `ping/<VERSION>`. */\nexport const PING_WIRE_CONTRACT_HEADER_VALUE = `ping/${PING_WIRE_CONTRACT_VERSION}`;\n\nexport type ValidationErrorKind =\n | \"UnsupportedVersion\"\n | \"BadConversationIdLen\"\n | \"BadSenderDeviceLen\"\n | \"ContentHashMismatch\";\n\nexport class ValidationError extends Error {\n constructor(public readonly kind: ValidationErrorKind, message: string) {\n super(message);\n this.name = \"PingWireContractValidationError\";\n }\n}\n\nconst KIND_DISCRIMINANT: Record<MessageEnvelope[\"kind\"], number> = {\n Application: 1,\n Commit: 2,\n Welcome: 3,\n Proposal: 4,\n KeyPackage: 5,\n};\n\nasync function sha256(bytes: Uint8Array): Promise<Uint8Array> {\n // `crypto.subtle` is available in browsers, Node 20+, and Workers. `bytes.buffer`\n // is typed as `ArrayBufferLike` which TS won't accept as `BufferSource` (the union\n // includes `SharedArrayBuffer`); pass an explicit ArrayBuffer slice instead.\n const ab = bytes.buffer.slice(bytes.byteOffset, bytes.byteOffset + bytes.byteLength) as ArrayBuffer;\n const hash = await crypto.subtle.digest(\"SHA-256\", ab);\n return new Uint8Array(hash);\n}\n\nfunction bytesEqual(a: Uint8Array, b: Uint8Array): boolean {\n if (a.length !== b.length) return false;\n for (let i = 0; i < a.length; i++) if (a[i] !== b[i]) return false;\n return true;\n}\n\n/**\n * Validate an envelope against the ping-wire-contract.\n *\n * Receivers claiming contract conformance MUST call this before applying any envelope.\n * External consumers using the raw envelope under their own profile are not bound by\n * these rules.\n *\n * CR-6 dual-accept window: `v ∈ [WIRE_VERSION_MIN_ACCEPTED, PING_WIRE_CONTRACT_VERSION]`\n * passes the version gate. For v=2 `Application` envelopes the content_hash is over\n * plaintext, so this function skips that check — callers MUST run\n * [[verifyApplicationContentHash]] after MLS decrypt to close the loop.\n */\nexport async function validate(env: MessageEnvelope): Promise<void> {\n if (env.v < WIRE_VERSION_MIN_ACCEPTED || env.v > PING_WIRE_CONTRACT_VERSION) {\n throw new ValidationError(\n \"UnsupportedVersion\",\n `unsupported wire-contract version: envelope says v=${env.v}, expected v∈[${WIRE_VERSION_MIN_ACCEPTED}, ${PING_WIRE_CONTRACT_VERSION}]`,\n );\n }\n if (env.conversation_id.length !== 16) {\n throw new ValidationError(\n \"BadConversationIdLen\",\n `conversation_id must be 16 bytes (ULID), got ${env.conversation_id.length}`,\n );\n }\n if (env.sender_device.length !== 32) {\n throw new ValidationError(\n \"BadSenderDeviceLen\",\n `sender_device must be 32 bytes, got ${env.sender_device.length}`,\n );\n }\n // CR-6 routing: skip the hash check for v=2 application envelopes.\n if (env.v >= 2 && env.kind === \"Application\") {\n return;\n }\n const buf = new Uint8Array(1 + env.payload.length);\n buf[0] = KIND_DISCRIMINANT[env.kind];\n buf.set(env.payload, 1);\n const computed = await sha256(buf);\n if (!bytesEqual(computed, env.content_hash)) {\n throw new ValidationError(\n \"ContentHashMismatch\",\n \"content_hash mismatch: envelope is forged, corrupted, or built by a non-conformant sender\",\n );\n }\n}\n\n/**\n * Verify a v=2 `Application` envelope's content_hash against the decrypted plaintext.\n *\n * Caller MUST run this after MLS decrypt for application envelopes whose `v >= 2`. For\n * v=1 envelopes or handshake kinds this is a no-op (those are covered by [[validate]]).\n */\nexport async function verifyApplicationContentHash(\n env: MessageEnvelope,\n plaintext: Uint8Array,\n): Promise<void> {\n if (env.v < 2 || env.kind !== \"Application\") return;\n const computed = await sha256(plaintext);\n if (!bytesEqual(computed, env.content_hash)) {\n throw new ValidationError(\n \"ContentHashMismatch\",\n \"v=2 application content_hash mismatch against decrypted plaintext\",\n );\n }\n}\n\n/**\n * True if the supplied content type negotiates the ping-wire-contract.\n *\n * Accepts the v2 canonical types AND the v1 legacy types during the CR-6 dual-accept\n * window. Tolerates whitespace and parameters (e.g. `; charset=utf-8`).\n */\nexport function negotiates(contentType: string | null | undefined): boolean {\n if (!contentType) return false;\n const primary = contentType.split(\";\")[0]?.trim().toLowerCase() ?? \"\";\n return (\n primary === CONTENT_TYPE_CBOR\n || primary === CONTENT_TYPE_JSON\n || primary === CONTENT_TYPE_CBOR_LEGACY_V1\n || primary === CONTENT_TYPE_JSON_LEGACY_V1\n );\n}\n","// Reference WebSocket transport. Treats the server as an opaque relay that:\n// - accepts a CBOR-encoded MessageEnvelope on `send`\n// - returns events newer than a cursor on `fetchSince`\n// - streams new events on the WS itself for `subscribe`\n// - exposes a directory endpoint for `discoverDevices`\n//\n// This is a sample. Real deployments will tune framing / auth.\n\nimport type { ConversationId, DiscoveredDevice, MessageEnvelope, Transport, UserId } from \"../types\";\nimport {\n CONTENT_TYPE_JSON,\n PING_WIRE_CONTRACT_HEADER,\n PING_WIRE_CONTRACT_HEADER_VALUE,\n} from \"../wire-contract\";\n\nexport interface WsTransportConfig {\n baseUrl: string; // ws[s]://...\n authHeader?: string; // optional bearer token\n fetchImpl?: typeof fetch; // override for SSR / tests\n}\n\nexport class WebSocketTransport implements Transport {\n private ws: WebSocket | null = null;\n private liveHandlers = new Set<(env: MessageEnvelope) => void>();\n private fetchImpl: typeof fetch;\n\n constructor(private cfg: WsTransportConfig) {\n this.fetchImpl = cfg.fetchImpl ?? globalThis.fetch.bind(globalThis);\n this.openSocket();\n }\n\n private openSocket() {\n if (typeof WebSocket === \"undefined\") return;\n const url = `${this.cfg.baseUrl.replace(/^http/, \"ws\")}/stream`;\n this.ws = new WebSocket(url);\n this.ws.onmessage = (ev) => {\n // Server sends one JSON envelope per text frame.\n const text = typeof ev.data === \"string\" ? ev.data : new TextDecoder().decode(ev.data as ArrayBuffer);\n try {\n const env = JSON.parse(text, reviver) as MessageEnvelope;\n for (const h of this.liveHandlers) h(env);\n } catch (e) {\n if (typeof console !== \"undefined\") console.debug(\"[ping-sdk] WS frame parse error:\", e);\n }\n };\n this.ws.onclose = () => setTimeout(() => this.openSocket(), 1000);\n }\n\n async send(envelope: MessageEnvelope): Promise<void> {\n // We're emitting the JSON projection of the ping-wire-contract. Production transports\n // would use CONTENT_TYPE_CBOR with a real CBOR codec; for the demo path the JSON shape\n // is the documented development projection.\n const res = await this.fetchImpl(`${this.cfg.baseUrl}/send`, {\n method: \"POST\",\n headers: {\n \"content-type\": CONTENT_TYPE_JSON,\n [PING_WIRE_CONTRACT_HEADER]: PING_WIRE_CONTRACT_HEADER_VALUE,\n ...(this.cfg.authHeader ? { authorization: this.cfg.authHeader } : {}),\n },\n body: JSON.stringify(envelope, replacer),\n });\n if (res.status === 409) throw new Error(\"EpochOccupied\");\n if (!res.ok) throw new Error(`transport send failed: ${res.status}`);\n }\n\n async fetchSince(conversationId: ConversationId, cursor: Uint8Array, limit: number): Promise<MessageEnvelope[]> {\n const url = new URL(`${this.cfg.baseUrl}/sync`);\n url.searchParams.set(\"conv\", toHex(conversationId));\n url.searchParams.set(\"cursor\", btoaUrl(cursor));\n url.searchParams.set(\"limit\", String(limit));\n const res = await this.fetchImpl(url.toString(), {\n headers: { ...(this.cfg.authHeader ? { authorization: this.cfg.authHeader } : {}) },\n });\n if (!res.ok) throw new Error(`transport fetch failed: ${res.status}`);\n const text = await res.text();\n return JSON.parse(text, reviver) as MessageEnvelope[];\n }\n\n subscribe(onEvent: (env: MessageEnvelope) => void): { unsubscribe(): void } {\n this.liveHandlers.add(onEvent);\n return { unsubscribe: () => this.liveHandlers.delete(onEvent) };\n }\n\n async discoverDevices(userId: UserId): Promise<DiscoveredDevice[]> {\n const res = await this.fetchImpl(`${this.cfg.baseUrl}/devices/${toHex(userId)}`, {\n headers: { ...(this.cfg.authHeader ? { authorization: this.cfg.authHeader } : {}) },\n });\n if (!res.ok) throw new Error(`device discovery failed: ${res.status}`);\n const text = await res.text();\n return JSON.parse(text, reviver) as DiscoveredDevice[];\n }\n}\n\n// ---- Wire codec ----\n//\n// Demo wire format: JSON with byte fields wrapped as `{ _bytes: number[] }`. The relay's\n// `envFromJson`/`envToJson` use the same shape. The spec calls for CBOR; v0.2 swaps in a\n// real CBOR codec, but the JSON form is much friendlier to debug.\n//\n// (decodeEnvelope helpers used to live here; collapsed into inline JSON.parse calls above.)\n\nfunction replacer(_k: string, v: unknown): unknown {\n if (v instanceof Uint8Array) return { _bytes: Array.from(v) };\n return v;\n}\nfunction reviver(_k: string, v: unknown): unknown {\n if (v && typeof v === \"object\" && Array.isArray((v as { _bytes?: number[] })._bytes)) {\n return new Uint8Array((v as { _bytes: number[] })._bytes);\n }\n return v;\n}\n\nfunction toHex(b: Uint8Array): string {\n let s = \"\"; for (const v of b) s += v.toString(16).padStart(2, \"0\"); return s;\n}\nfunction btoaUrl(b: Uint8Array): string {\n let s = \"\"; for (const v of b) s += String.fromCharCode(v);\n return btoa(s).replace(/\\+/g, \"-\").replace(/\\//g, \"_\").replace(/=+$/, \"\");\n}\n","// Public entry point. Spawns the dedicated Worker that hosts the WASM instance and exposes a\n// Promise-based, identical-to-native API. Crypto runs off the main thread.\n\nimport type {\n AdmitChatEntry, AdmitChatOutcome,\n CatchupAppEvent, CatchupSnapshotView,\n ConversationId, ConversationMeta, DeviceId, IncomingMessage,\n HistoryBundle, MemberInfo,\n KeyPackageEntry, LinkingTicket, MessageEnvelope, RecoveryBackup, Storage, Transport, UserId,\n} from \"./types\";\nimport type { Request, Response } from \"./protocol\";\n\nexport type * from \"./types\";\n\n// Re-export the default Storage and Transport implementations from the main entry so consumers\n// whose bundlers don't honor package.json#exports (Metro with strict resolution, some legacy\n// Webpack configs) can still get them with a plain `import { ... } from \"ping-openmls-sdk\"`. Power\n// users who care about tree-shaking can keep using the subpath imports.\nexport { IndexedDbStorage } from \"./storage/indexeddb\";\nexport { WebSocketTransport } from \"./transport/websocket\";\n\n// Ping wire contract — version, content-type constants, and runtime validators. Internal\n// products MUST claim conformance; external consumers MAY adopt or run their own profile.\n// See docs/PING_WIRE_CONTRACT.md.\nexport {\n PING_WIRE_CONTRACT_VERSION,\n PING_WIRE_CONTRACT_HEADER,\n PING_WIRE_CONTRACT_HEADER_VALUE,\n WIRE_VERSION_MIN_ACCEPTED,\n CONTENT_TYPE_CBOR,\n CONTENT_TYPE_JSON,\n CONTENT_TYPE_CBOR_LEGACY_V1,\n CONTENT_TYPE_JSON_LEGACY_V1,\n ValidationError,\n validate,\n verifyApplicationContentHash,\n negotiates,\n} from \"./wire-contract\";\n\nexport interface ClientConfig {\n identityExport: Uint8Array;\n deviceLabel: string;\n storage: Storage;\n transport: Transport;\n /** Override the wall clock (test/sim only). */\n now?: () => number;\n /**\n * Optional 32-byte Ed25519 secret key. When supplied AND no\n * `LocalDevice` is yet persisted in `storage`, the SDK adopts this\n * as its device signing key — so `deviceId()` returns\n * `SHA-256(public_key_of(secret))`, deterministic from what the\n * caller passed.\n *\n * Use case: align the SDK's `device_id` (which the SDK stamps into\n * every envelope's `sender_device` field) with an externally-\n * computed device id — typically `SHA-256(device_signing_pubkey)`\n * in the host's auth layer, where the JWT carries that same value\n * as its `device_id` claim. Without this alignment, a server that\n * validates `envelope.sender_device == jwt.device_id` rejects every\n * upload with `sender_device_mismatch`.\n *\n * Ignored on subsequent inits (when `storage` already holds a\n * persisted `LocalDevice`) — the on-disk identity is authoritative\n * for stability across restarts.\n */\n deviceSigningSecretKey?: Uint8Array;\n}\n\nexport interface Conversation {\n readonly id: ConversationId;\n send(plaintext: Uint8Array): Promise<MessageEnvelope>;\n /** [CR-2] Add members by (device_id, KeyPackage) pair. Hosts typically pull these\n * straight from `transport.discoverDevices(userId)`. */\n addMembers(entries: KeyPackageEntry[]): Promise<void>;\n removeMembers(leafIndexes: number[]): Promise<void>;\n meta(): ConversationMeta;\n}\n\nexport class MessagingClient {\n private worker: Worker;\n private nextId = 1;\n private pending = new Map<number, { resolve: (v: unknown) => void; reject: (e: Error) => void }>();\n private storage: Storage;\n private transport: Transport;\n private now: () => number;\n private messageHandlers = new Set<(m: IncomingMessage) => void>();\n private conversationHandlers = new Set<(id: ConversationId, epoch: number, sender?: DeviceId) => void>();\n private subscription: { unsubscribe(): void } | null = null;\n private metaCache = new Map<string, ConversationMeta>();\n // Cached at init() so dispatchIncoming can skip envelopes we sent ourselves — the relay\n // broadcasts to all WS subscribers including the sender, and re-applying our own already-\n // merged commits would error with \"epoch differs\".\n private ownDeviceId: Uint8Array | null = null;\n\n private constructor(cfg: ClientConfig, worker: Worker) {\n this.worker = worker;\n this.storage = cfg.storage;\n this.transport = cfg.transport;\n this.now = cfg.now ?? (() => Date.now());\n this.worker.onmessage = (ev) => this.handleWorker(ev.data as Response);\n }\n\n /** Generate a fresh identity. The returned bytes are a secret — store them encrypted. */\n static async generateIdentity(): Promise<Uint8Array> {\n return MessagingClient.ephemeralWorkerCall<Uint8Array>({\n kind: \"generateIdentity\", id: 1,\n });\n }\n\n /**\n * HPKE-seal a `LinkingTicket` for delivery to a new device ([CR-3]).\n *\n * Returns CBOR-encoded bytes safe to relay through an untrusted inbox. The new device's\n * X25519 public key (`newDevicePub`) is exchanged out-of-band — typically as part of a\n * QR-encoded handshake on the linking screen. Pure function; runs in an ephemeral\n * worker so no live `MessagingClient` is required on the sender side.\n *\n * Suite: `DHKEM(X25519, HKDF-SHA256), HKDF-SHA256, AES-128-GCM` (matches MLS).\n */\n static async sealLinkingTicket(\n ticket: LinkingTicket,\n newDevicePub: Uint8Array,\n ): Promise<Uint8Array> {\n if (newDevicePub.length !== 32) {\n throw new Error(\"sealLinkingTicket: newDevicePub must be 32 bytes\");\n }\n return MessagingClient.ephemeralWorkerCall<Uint8Array>({\n kind: \"sealLinkingTicket\", id: 1, ticket, newDevicePub,\n });\n }\n\n /**\n * HPKE-open a sealed `LinkingTicket` on the new device ([CR-3]).\n *\n * The receiving device hasn't booted a `MessagingClient` yet — it only has its X25519\n * ephemeral keypair. This static method runs the open in an ephemeral worker, returning\n * the cleartext ticket which the host then feeds to `init()` + `consumeLinkingTicket()`.\n *\n * On failure the error message is deliberately generic (no discriminator between \"wrong\n * key\" vs \"tampered ciphertext\") so a probing attacker can't tell the new device's key\n * was the issue.\n */\n static async openLinkingTicket(\n sealed: Uint8Array,\n newDevicePriv: Uint8Array,\n ): Promise<LinkingTicket> {\n if (newDevicePriv.length !== 32) {\n throw new Error(\"openLinkingTicket: newDevicePriv must be 32 bytes\");\n }\n return MessagingClient.ephemeralWorkerCall<LinkingTicket>({\n kind: \"openLinkingTicket\", id: 1, sealed, newDevicePriv,\n });\n }\n\n /**\n * Generic HPKE-Base seal of arbitrary bytes to `recipientPub` (32-byte X25519)\n * under a caller-supplied `info`. One shared HPKE implementation so app layers\n * (e.g. the device-linking auth envelope) stop hand-rolling per-platform HPKE\n * that drifts and breaks cross-platform. Runs in an ephemeral worker.\n */\n static async hpkeSeal(\n plaintext: Uint8Array,\n recipientPub: Uint8Array,\n info: Uint8Array,\n ): Promise<Uint8Array> {\n if (recipientPub.length !== 32) {\n throw new Error(\"hpkeSeal: recipientPub must be 32 bytes\");\n }\n return MessagingClient.ephemeralWorkerCall<Uint8Array>({\n kind: \"hpkeSeal\", id: 1, plaintext, recipientPub, info,\n });\n }\n\n /** Generic HPKE-Base open — inverse of {@link hpkeSeal}. `info` must match the sender. */\n static async hpkeOpen(\n sealed: Uint8Array,\n recipientPriv: Uint8Array,\n info: Uint8Array,\n ): Promise<Uint8Array> {\n if (recipientPriv.length !== 32) {\n throw new Error(\"hpkeOpen: recipientPriv must be 32 bytes\");\n }\n return MessagingClient.ephemeralWorkerCall<Uint8Array>({\n kind: \"hpkeOpen\", id: 1, sealed, recipientPriv, info,\n });\n }\n\n // Spawn-call-terminate helper for stateless WASM functions exposed at module scope (no\n // PingClient required). Each call gets a fresh worker; the cost is acceptable because\n // these are one-shot, off-the-hot-path operations (identity gen, linking ticket seal/open).\n private static ephemeralWorkerCall<T>(req: Request): Promise<T> {\n const w = new Worker(new URL(\"./worker.js\", import.meta.url), { type: \"module\" });\n return new Promise<T>((resolve, reject) => {\n w.onmessage = (ev) => {\n const msg = ev.data as Response;\n if (msg.kind === \"result\" && msg.id === (req as { id: number }).id) {\n w.terminate();\n msg.ok ? resolve(msg.value as T) : reject(new Error(msg.error));\n }\n };\n w.postMessage(req);\n });\n }\n\n static async init(cfg: ClientConfig): Promise<MessagingClient> {\n const worker = new Worker(new URL(\"./worker.js\", import.meta.url), { type: \"module\" });\n const client = new MessagingClient(cfg, worker);\n\n await client.call({\n kind: \"init\",\n id: client.nextId++,\n identityExport: cfg.identityExport,\n deviceLabel: cfg.deviceLabel,\n nowMs: client.now(),\n ...(cfg.deviceSigningSecretKey\n ? { deviceSigningSecretKey: cfg.deviceSigningSecretKey }\n : {}),\n });\n\n // Cache the device id once so we can drop envelopes we sent ourselves on the WS echo.\n client.ownDeviceId = await client.deviceId();\n\n // Start the live subscription. We dispatch each envelope to the right handler:\n // - Welcome envelopes for unknown conversations → joinConversation\n // - Welcomes for already-joined conversations → drop (server may broadcast duplicates)\n // - Welcomes addressed to other devices → drop (we can't decrypt)\n // - Everything else → processEnvelope\n client.subscription = cfg.transport.subscribe((envelope) => {\n void client.dispatchIncoming(envelope);\n });\n return client;\n }\n\n /** Internal: route an inbound envelope to the right handler. */\n private async dispatchIncoming(envelope: MessageEnvelope): Promise<void> {\n // The relay broadcasts every envelope to every WS subscriber, including the sender. For\n // our own sends, the local MLS state has already been advanced in-process — re-applying\n // would either fail with \"epoch differs\" (commits) or be a no-op dedupe (application\n // messages). Skip them here.\n if (this.ownDeviceId && sameBytesU8(envelope.sender_device, this.ownDeviceId)) {\n return;\n }\n if (envelope.kind === \"Welcome\") {\n try {\n const known = await this.listConversations();\n const already = known.some((m) => sameBytesU8(m.id, envelope.conversation_id));\n if (already) return;\n await this.joinConversation(envelope);\n } catch (e) {\n // Welcomes are broadcast to every connected client; only one device's KeyPackage can\n // decrypt each one, so most fail. Log at debug, drop silently.\n if (typeof console !== \"undefined\") console.debug(\"[ping-sdk] welcome ignored:\", e);\n }\n return;\n }\n try {\n await this.processEnvelope(envelope);\n } catch (e) {\n if (typeof console !== \"undefined\") console.debug(\"[ping-sdk] envelope drop:\", e);\n }\n }\n\n // ------------------------ Public API ------------------------\n\n async userId(): Promise<UserId> {\n return await this.call({ kind: \"userId\", id: this.nextId++ }) as UserId;\n }\n\n /**\n * Export the account identity seed (CBOR-wrapped Ed25519, **SECRET**).\n * Transfer to a newly-linked device over the sealed linking channel so all of\n * an account's devices share one `userId` — the basis for cross-device\n * self-attribution (`IncomingMessage.sender_user_id === userId()`). Never log\n * or persist in cleartext.\n */\n async exportIdentity(): Promise<Uint8Array> {\n return await this.call({ kind: \"exportIdentity\", id: this.nextId++ }) as Uint8Array;\n }\n\n async deviceId(): Promise<DeviceId> {\n return await this.call({ kind: \"deviceId\", id: this.nextId++ }) as DeviceId;\n }\n\n async freshKeyPackage(): Promise<Uint8Array> {\n return await this.call({ kind: \"freshKeyPackage\", id: this.nextId++ }) as Uint8Array;\n }\n\n async createConversation(opts: { name?: string } = {}): Promise<Conversation> {\n const idBytes = await this.call({\n kind: \"createConversation\", id: this.nextId++, name: opts.name ?? null, nowMs: this.now(),\n }) as ConversationId;\n return this.conversationProxy(idBytes);\n }\n\n async joinConversation(welcome: MessageEnvelope): Promise<Conversation> {\n const idBytes = await this.call({\n kind: \"joinConversation\", id: this.nextId++, welcome, nowMs: this.now(),\n }) as ConversationId;\n return this.conversationProxy(idBytes);\n }\n\n /**\n * Get a `Conversation` proxy for an already-known conversation. Use this when you have a\n * conversation id (e.g., from `listConversations()`) and want to send/addMembers without\n * re-creating it. Throws if the id isn't known to the worker — call `listConversations()`\n * first to verify membership.\n */\n getConversation(id: ConversationId): Conversation {\n return this.conversationProxy(id);\n }\n\n async listConversations(): Promise<ConversationMeta[]> {\n const metas = await this.call({ kind: \"listConversations\", id: this.nextId++ }) as ConversationMeta[];\n this.metaCache.clear();\n for (const m of metas) this.metaCache.set(hex(m.id), m);\n return metas;\n }\n\n /**\n * Member roster for a conversation, recovered locally from the MLS\n * group's leaf credentials (no network, no out-of-band message). Returns\n * one entry per device leaf — a multi-device user appears multiple times,\n * so dedup by `user_id` for a per-user roster. Empty for an unknown id.\n */\n async members(conv: ConversationId): Promise<MemberInfo[]> {\n return await this.call({ kind: \"members\", id: this.nextId++, conv }) as MemberInfo[];\n }\n\n async syncConversations(): Promise<IncomingMessage[]> {\n return await this.call({\n kind: \"syncConversations\", id: this.nextId++, nowMs: this.now(),\n }) as IncomingMessage[];\n }\n\n async processEnvelope(envelope: MessageEnvelope): Promise<IncomingMessage | null> {\n return await this.call({\n kind: \"processEnvelope\", id: this.nextId++, envelope, nowMs: this.now(),\n }) as IncomingMessage | null;\n }\n\n /**\n * Build a `LinkingTicket` for a new device.\n *\n * [CR-13] `lastAppEvents` is host-supplied per-conversation \"what you missed\" data\n * (decrypted AppEvent bytes the new device will render before sync catches up).\n * Pass `[]` to suppress catchup data — the new device will see an empty conversation\n * list until normal sync runs.\n *\n * Caller is responsible for HPKE-sealing the returned ticket via\n * `MessagingClient.sealLinkingTicket` before transmitting (CR-3).\n */\n async buildLinkingTicket(\n newDeviceId: DeviceId,\n newDeviceKp: Uint8Array,\n lastAppEvents: CatchupAppEvent[] = [],\n ): Promise<LinkingTicket> {\n return await this.call({\n kind: \"buildLinkingTicket\", id: this.nextId++,\n newDeviceId, newDeviceKp, lastAppEvents, nowMs: this.now(),\n }) as LinkingTicket;\n }\n\n /**\n * Decode a `LinkingTicket.catchup_snapshot` blob ([CR-13]).\n *\n * Pure WASM — runs in an ephemeral worker so the new device can call this before\n * `MessagingClient.init()` completes (just like `openLinkingTicket`). Returns the\n * structured view: per-conversation metas + last-known AppEvent bytes.\n *\n * Throws on malformed or oversized inputs. Empty bytes return `null` — that's the\n * \"no catchup data\" case (sender passed `[]` to buildLinkingTicket).\n */\n static async decodeCatchupSnapshot(\n snapshotBytes: Uint8Array,\n ): Promise<CatchupSnapshotView | null> {\n if (snapshotBytes.length === 0) return null;\n return MessagingClient.ephemeralWorkerCall<CatchupSnapshotView>({\n kind: \"decodeCatchupSnapshot\", id: 1, snapshotBytes,\n });\n }\n\n async consumeLinkingTicket(ticket: LinkingTicket): Promise<void> {\n await this.call({\n kind: \"consumeLinkingTicket\", id: this.nextId++, ticket, nowMs: this.now(),\n });\n }\n\n // ------------------------ Recovery (account backup) ------------------------\n // All static + stateless: they run on an ephemeral worker, so the recovery screen can\n // call them before any client is initialised. Same Argon2id/AES-256-GCM IdentityBackup\n // wire format as the iOS/Android FFI bindings — backups are portable across platforms.\n\n /** Generate a fresh 12-word BIP39 mnemonic, returned as its canonical phrase string. */\n static async generateMnemonicPhrase(): Promise<string> {\n return MessagingClient.ephemeralWorkerCall<string>({\n kind: \"generateMnemonicPhrase\", id: 1,\n });\n }\n\n /** Validate + canonicalise a user-entered phrase (BIP39 wordlist + checksum). Rejects\n * malformed input. Use on the recovery screen before calling `decryptBackup`. */\n static async normalizeMnemonicPhrase(phrase: string): Promise<string> {\n return MessagingClient.ephemeralWorkerCall<string>({\n kind: \"normalizeMnemonicPhrase\", id: 1, phrase,\n });\n }\n\n /** Encrypt an IdentityBackup for `POST /v1/recovery/blobs`. `deviceGroupSnapshot` may be\n * empty (Conservative path) or the DeviceGroup's `exportConversationStateSnapshot`. */\n static async encryptBackup(args: {\n mnemonicPhrase: string;\n accountId: Uint8Array;\n identityExport: Uint8Array;\n deviceGroupSnapshot: Uint8Array;\n /** External conversations to embed so recovery restores them offline. Pass\n * `[]` (the default) for identity + DeviceGroup only. Each `groupStateBytes`\n * is `Conversation.exportConversationStateSnapshot` output. Forward-secrecy\n * trade-off — see the SDK recovery docs. */\n conversationSnapshots?: { conversationId: Uint8Array; groupStateBytes: Uint8Array }[];\n createdAtMs: number;\n }): Promise<Uint8Array> {\n return MessagingClient.ephemeralWorkerCall<Uint8Array>({\n kind: \"encryptBackup\", id: 1,\n mnemonicPhrase: args.mnemonicPhrase,\n accountId: args.accountId,\n identityExport: args.identityExport,\n deviceGroupSnapshot: args.deviceGroupSnapshot,\n conversationSnapshots: args.conversationSnapshots ?? [],\n createdAtMs: args.createdAtMs,\n });\n }\n\n /** Decrypt a recovery blob from `GET /v1/recovery/blobs`. Throws on wrong mnemonic or a\n * tampered blob. The returned `identity_export` is the secret seed for `init`. */\n static async decryptBackup(\n blob: Uint8Array,\n mnemonicPhrase: string,\n ): Promise<RecoveryBackup> {\n return MessagingClient.ephemeralWorkerCall<RecoveryBackup>({\n kind: \"decryptBackup\", id: 1, blob, mnemonicPhrase,\n });\n }\n\n // ------------------------ Peer-assisted history transfer ------------------------\n // Authenticated (sign-then-encrypt) device-to-device history used by linking and\n // post-recovery peer re-add. Confidential to the recipient's ephemeral X25519 key AND\n // signed by the account identity key, so the receiver proves the sender. Plaintext-only\n // share — preserves forward secrecy (no MLS keys leave the device).\n\n /** Seal+sign a `HistoryBundle` for a recipient device. `recipientPub` (X25519, 32B) is the\n * recipient's ephemeral pubkey from the linking QR; `senderIdentitySecret` (Ed25519 seed,\n * 32B) is this account's identity key. Returns the blob to POST to /v1/history-transfers. */\n static async sealHistoryBundle(\n bundle: HistoryBundle,\n recipientPub: Uint8Array,\n senderIdentitySecret: Uint8Array,\n ): Promise<Uint8Array> {\n return MessagingClient.ephemeralWorkerCall<Uint8Array>({\n kind: \"sealHistoryBundle\", id: 1, bundle, recipientPub, senderIdentitySecret,\n });\n }\n\n /** Open+verify a sealed history bundle. `recipientPriv` (X25519, 32B) is this device's\n * ephemeral private key; `senderIdentityPub` (Ed25519, 32B) is the account identity pubkey\n * from the linking handshake. Throws if the signature doesn't verify. */\n static async openHistoryBundle(\n sealed: Uint8Array,\n recipientPriv: Uint8Array,\n senderIdentityPub: Uint8Array,\n ): Promise<HistoryBundle> {\n return MessagingClient.ephemeralWorkerCall<HistoryBundle>({\n kind: \"openHistoryBundle\", id: 1, sealed, recipientPriv, senderIdentityPub,\n });\n }\n\n /**\n * Admit a freshly-linked device to every chat in `entries` — one MLS\n * Commit + Welcome per chat, with the Welcome's `?recipient=` priming\n * handled inside the SDK. This is the post-link reconciliation step\n * that follows `consumeLinkingTicket`: the new device has the\n * DeviceGroup (from the ticket) and metadata for external chats (from\n * the catchup snapshot), but its leaf is not yet in any external\n * MLS group. The host on the EXISTING device calls this once the\n * new device shows up in `/v1/auth/sessions` and has uploaded its\n * KeyPackage batch, passing one freshly-claimed KP per chat.\n *\n * Per-chat failures (stale KP, transport error, unknown chat) are\n * captured in the returned outcomes rather than aborting — one bad\n * chat shouldn't lose the user access to all the others.\n */\n async admitDeviceToChats(\n deviceId: DeviceId,\n entries: AdmitChatEntry[],\n ): Promise<AdmitChatOutcome[]> {\n return await this.call({\n kind: \"admitDeviceToChats\", id: this.nextId++,\n deviceId, entries, nowMs: this.now(),\n }) as AdmitChatOutcome[];\n }\n\n /**\n * Revoke a device ([CR-2]).\n *\n * Removes `deviceId`'s leaf from every conversation where this client locally\n * tracked the device→leaf mapping (i.e. conversations where this client itself\n * admitted the device via `addMembers`, or where this client is the device being\n * revoked). The SDK has already broadcast each Commit envelope via\n * `transport.send`; the returned array is for any additional host-side handling\n * (audit logs, UI). An empty array is a valid outcome — the device wasn't locally\n * known anywhere.\n *\n * **Scope note.** Conversations where a peer admitted the device on this client's\n * behalf are silently skipped — the leaf index isn't locally known. Host can fall\n * back to `transport.discoverDevices` + a manual `remove_members(leafIndex)` for\n * those, or wait for the affected user to revoke from a device that did the admit.\n * See `docs/architecture/multi-device.md §Device removal`.\n */\n async revokeDevice(deviceId: DeviceId): Promise<MessageEnvelope[]> {\n return await this.call({\n kind: \"revokeDevice\", id: this.nextId++, deviceId, nowMs: this.now(),\n }) as MessageEnvelope[];\n }\n\n /**\n * Export a portable MLS state snapshot for one conversation ([CR-7]).\n *\n * Returned bytes can be embedded in a recovery blob or shipped through a\n * linking ticket so another device of the same user identity can re-attach to\n * the group via [[importStateSnapshot]]. The bytes contain past epoch secrets;\n * never log, never persist unencrypted, wipe (`.fill(0)`) after use.\n */\n async exportConversationStateSnapshot(conv: ConversationId): Promise<Uint8Array> {\n return await this.call({\n kind: \"exportConversationStateSnapshot\", id: this.nextId++,\n conv, nowMs: this.now(),\n }) as Uint8Array;\n }\n\n /**\n * Import a `GroupStateSnapshot` from another device of the same user identity\n * ([CR-7]).\n *\n * On success, the conversation appears in [[listConversations]] and decryption\n * of subsequent peer-originated traffic works against the imported state. For\n * full multi-device participation, the recovered device SHOULD then issue an\n * MLS Update Proposal to rotate onto a fresh leaf (post-v1 follow-up — see\n * `docs/design/CR4_CR7_PERSISTENCE.md` open question #3).\n */\n async importStateSnapshot(snapshotBytes: Uint8Array): Promise<ConversationId> {\n return await this.call({\n kind: \"importStateSnapshot\", id: this.nextId++,\n snapshotBytes, nowMs: this.now(),\n }) as ConversationId;\n }\n\n /**\n * Export a derived secret from a conversation's MLS exporter ([CR-8]).\n *\n * Used to seed the ephemeral channel (`ping/ephemeral`), call-media keys\n * (`ping/calls/media/<call_id>`), and call-ephemeral framer keys\n * (`ping/calls/ephemeral/<call_id>`). The returned bytes are a secret — never log,\n * persist only encrypted, and call `bytes.fill(0)` after use to wipe the typed array.\n *\n * Same `(conversationId, current_epoch, label, context, length)` produces the same\n * bytes across every binding (Rust core, native UniFFI, WASM/JS). Cross-platform\n * byte-equality is enforced by `protocol/conformance/export_secret/`.\n */\n async exportConversationSecret(\n conv: ConversationId,\n label: string,\n context: Uint8Array,\n length: number,\n ): Promise<Uint8Array> {\n if (length <= 0 || length > 1024) {\n throw new Error(\"exportConversationSecret: length must be 1..=1024\");\n }\n return await this.call({\n kind: \"exportConversationSecret\", id: this.nextId++,\n conv, label, context, length,\n }) as Uint8Array;\n }\n\n /** Subscribe to decrypted application messages. */\n onMessage(handler: (m: IncomingMessage) => void): () => void {\n this.messageHandlers.add(handler);\n return () => this.messageHandlers.delete(handler);\n }\n\n /** Fired after every state-changing event for a conversation (Commit, member changes). */\n /**\n * Fires after every conversation state change — joining via Welcome, processing a Commit,\n * etc. `sender` carries the device id of whoever triggered the event when known (e.g. the\n * inviter's device on auto-join), so the host can label peers in its UI.\n */\n onConversationUpdated(handler: (id: ConversationId, epoch: number, sender?: DeviceId) => void): () => void {\n this.conversationHandlers.add(handler);\n return () => this.conversationHandlers.delete(handler);\n }\n\n async close(): Promise<void> {\n this.subscription?.unsubscribe();\n this.worker.terminate();\n }\n\n // ------------------------ Internals ------------------------\n\n private conversationProxy(id: ConversationId): Conversation {\n const self = this;\n return {\n id,\n async send(plaintext) {\n return await self.call({\n kind: \"send\", id: self.nextId++, conv: id, plaintext, nowMs: self.now(),\n }) as MessageEnvelope;\n },\n async addMembers(entries) {\n await self.call({\n kind: \"addMembers\", id: self.nextId++, conv: id, entries, nowMs: self.now(),\n });\n },\n async removeMembers(leaves) {\n await self.call({\n kind: \"removeMembers\", id: self.nextId++, conv: id, leaves, nowMs: self.now(),\n });\n },\n meta() {\n const cached = self.metaCache.get(hex(id));\n if (!cached) throw new Error(\"conversation meta not loaded; call listConversations() first\");\n return cached;\n },\n };\n }\n\n private call(req: Request): Promise<unknown> {\n return new Promise((resolve, reject) => {\n this.pending.set((req as { id: number }).id, { resolve, reject });\n this.worker.postMessage(req);\n });\n }\n\n private async handleWorker(msg: Response) {\n switch (msg.kind) {\n case \"result\": {\n const slot = this.pending.get(msg.id);\n if (!slot) return;\n this.pending.delete(msg.id);\n msg.ok ? slot.resolve(msg.value) : slot.reject(new Error(msg.error));\n return;\n }\n case \"event.message\": {\n for (const h of this.messageHandlers) h(msg.msg);\n return;\n }\n case \"event.conversation\": {\n for (const h of this.conversationHandlers) h(msg.id, msg.epoch, msg.sender);\n return;\n }\n case \"storage.call\": {\n try {\n const v = await (this.storage as unknown as Record<string, (...a: unknown[]) => unknown>)\n [msg.method]?.(...msg.args);\n this.worker.postMessage({ kind: \"storage.reply\", id: msg.id, ok: true, value: v } as Request);\n } catch (e) {\n this.worker.postMessage({\n kind: \"storage.reply\", id: msg.id, ok: false, error: String(e),\n } as Request);\n }\n return;\n }\n case \"transport.call\": {\n try {\n const v = await (this.transport as unknown as Record<string, (...a: unknown[]) => unknown>)\n [msg.method]?.(...msg.args);\n this.worker.postMessage({ kind: \"transport.reply\", id: msg.id, ok: true, value: v } as Request);\n } catch (e) {\n this.worker.postMessage({\n kind: \"transport.reply\", id: msg.id, ok: false, error: String(e),\n } as Request);\n }\n return;\n }\n }\n }\n}\n\nfunction hex(b: Uint8Array): string {\n let s = \"\";\n for (const v of b) s += v.toString(16).padStart(2, \"0\");\n return s;\n}\n\nfunction sameBytesU8(a: Uint8Array, b: Uint8Array): boolean {\n if (a.length !== b.length) return false;\n for (let i = 0; i < a.length; i++) if (a[i] !== b[i]) return false;\n return true;\n}\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../src/storage/indexeddb.ts","../src/wire-contract.ts","../src/transport/websocket.ts","../src/index.ts"],"sourcesContent":["// Default IndexedDB-backed Storage implementation. Hosts can supply their own.\n\nimport type { Storage } from \"../types\";\n\nconst DB_NAME = \"ping-sdk\";\nconst DB_VERSION = 1;\nconst STORE = \"kv\";\n\ninterface Row { ns: string; key: string; value: Uint8Array }\n\nfunction open(): Promise<IDBDatabase> {\n return new Promise((resolve, reject) => {\n const req = indexedDB.open(DB_NAME, DB_VERSION);\n req.onupgradeneeded = () => {\n const db = req.result;\n if (!db.objectStoreNames.contains(STORE)) {\n const os = db.createObjectStore(STORE, { keyPath: [\"ns\", \"key\"] });\n os.createIndex(\"ns\", \"ns\", { unique: false });\n }\n };\n req.onsuccess = () => resolve(req.result);\n req.onerror = () => reject(req.error);\n });\n}\n\nexport class IndexedDbStorage implements Storage {\n private dbPromise: Promise<IDBDatabase>;\n\n constructor() { this.dbPromise = open(); }\n\n async get(namespace: string, key: string): Promise<Uint8Array | null> {\n const db = await this.dbPromise;\n return new Promise((resolve, reject) => {\n const tx = db.transaction(STORE, \"readonly\");\n const r = tx.objectStore(STORE).get([namespace, key]);\n r.onsuccess = () => resolve(((r.result as Row | undefined)?.value) ?? null);\n r.onerror = () => reject(r.error);\n });\n }\n\n async put(namespace: string, key: string, value: Uint8Array): Promise<void> {\n const db = await this.dbPromise;\n return new Promise((resolve, reject) => {\n const tx = db.transaction(STORE, \"readwrite\");\n tx.objectStore(STORE).put({ ns: namespace, key, value } as Row);\n tx.oncomplete = () => resolve();\n tx.onerror = () => reject(tx.error);\n });\n }\n\n async del(namespace: string, key: string): Promise<void> {\n const db = await this.dbPromise;\n return new Promise((resolve, reject) => {\n const tx = db.transaction(STORE, \"readwrite\");\n tx.objectStore(STORE).delete([namespace, key]);\n tx.oncomplete = () => resolve();\n tx.onerror = () => reject(tx.error);\n });\n }\n\n async listKeys(namespace: string, prefix: string): Promise<string[]> {\n const db = await this.dbPromise;\n return new Promise((resolve, reject) => {\n const tx = db.transaction(STORE, \"readonly\");\n const idx = tx.objectStore(STORE).index(\"ns\");\n const req = idx.openCursor(IDBKeyRange.only(namespace));\n const out: string[] = [];\n req.onsuccess = () => {\n const cur = req.result;\n if (!cur) return resolve(out);\n const row = cur.value as Row;\n if (!prefix || row.key.startsWith(prefix)) out.push(row.key);\n cur.continue();\n };\n req.onerror = () => reject(req.error);\n });\n }\n}\n","// Ping wire contract — TypeScript counterpart to the `ping-wire-contract` Rust crate.\n//\n// MUST stay in lockstep with `core/ping-wire-contract/src/lib.rs`. The full normative spec\n// lives in `docs/PING_WIRE_CONTRACT.md`.\n//\n// What this module is for: code paths that cross the network boundary (transports, relays,\n// service workers) that need to advertise or validate ping-wire-contract conformance from\n// JavaScript without round-tripping through WASM.\n\nimport type { MessageEnvelope } from \"./types\";\n\n/**\n * Ping wire contract version. Bumped on incompatible profile changes.\n *\n * Currently `2` — CR-6 changes the `content_hash` semantics for `Application` envelopes\n * (now hashed over plaintext, not the MLS ciphertext). Receivers run a 90-day dual-accept\n * window per Q-ARCH-02; see [[validate]] for the per-version routing.\n */\nexport const PING_WIRE_CONTRACT_VERSION = 2;\n\n/** Lowest envelope version accepted by [[validate]] during the dual-accept window. */\nexport const WIRE_VERSION_MIN_ACCEPTED = 1;\n\n/** Canonical CBOR content type. Production transports SHOULD use this. */\nexport const CONTENT_TYPE_CBOR = \"application/vnd.ping.wire.v2+cbor\";\n\n/** JSON projection — development and tooling only. Production MUST NOT use this. */\nexport const CONTENT_TYPE_JSON = \"application/vnd.ping.wire.v2+json\";\n\n/** Legacy v1 CBOR type, accepted during the CR-6 dual-accept window. */\nexport const CONTENT_TYPE_CBOR_LEGACY_V1 = \"application/vnd.ping.wire.v1+cbor\";\n\n/** Legacy v1 JSON type, accepted during the CR-6 dual-accept window. */\nexport const CONTENT_TYPE_JSON_LEGACY_V1 = \"application/vnd.ping.wire.v1+json\";\n\n/** Header used over transports that don't surface a content type (e.g. WebSocket). */\nexport const PING_WIRE_CONTRACT_HEADER = \"X-Ping-Wire-Contract\";\n\n/** Header value: `ping/<VERSION>`. */\nexport const PING_WIRE_CONTRACT_HEADER_VALUE = `ping/${PING_WIRE_CONTRACT_VERSION}`;\n\nexport type ValidationErrorKind =\n | \"UnsupportedVersion\"\n | \"BadConversationIdLen\"\n | \"BadSenderDeviceLen\"\n | \"ContentHashMismatch\";\n\nexport class ValidationError extends Error {\n constructor(public readonly kind: ValidationErrorKind, message: string) {\n super(message);\n this.name = \"PingWireContractValidationError\";\n }\n}\n\nconst KIND_DISCRIMINANT: Record<MessageEnvelope[\"kind\"], number> = {\n Application: 1,\n Commit: 2,\n Welcome: 3,\n Proposal: 4,\n KeyPackage: 5,\n};\n\nasync function sha256(bytes: Uint8Array): Promise<Uint8Array> {\n // `crypto.subtle` is available in browsers, Node 20+, and Workers. `bytes.buffer`\n // is typed as `ArrayBufferLike` which TS won't accept as `BufferSource` (the union\n // includes `SharedArrayBuffer`); pass an explicit ArrayBuffer slice instead.\n const ab = bytes.buffer.slice(bytes.byteOffset, bytes.byteOffset + bytes.byteLength) as ArrayBuffer;\n const hash = await crypto.subtle.digest(\"SHA-256\", ab);\n return new Uint8Array(hash);\n}\n\nfunction bytesEqual(a: Uint8Array, b: Uint8Array): boolean {\n if (a.length !== b.length) return false;\n for (let i = 0; i < a.length; i++) if (a[i] !== b[i]) return false;\n return true;\n}\n\n/**\n * Validate an envelope against the ping-wire-contract.\n *\n * Receivers claiming contract conformance MUST call this before applying any envelope.\n * External consumers using the raw envelope under their own profile are not bound by\n * these rules.\n *\n * CR-6 dual-accept window: `v ∈ [WIRE_VERSION_MIN_ACCEPTED, PING_WIRE_CONTRACT_VERSION]`\n * passes the version gate. For v=2 `Application` envelopes the content_hash is over\n * plaintext, so this function skips that check — callers MUST run\n * [[verifyApplicationContentHash]] after MLS decrypt to close the loop.\n */\nexport async function validate(env: MessageEnvelope): Promise<void> {\n if (env.v < WIRE_VERSION_MIN_ACCEPTED || env.v > PING_WIRE_CONTRACT_VERSION) {\n throw new ValidationError(\n \"UnsupportedVersion\",\n `unsupported wire-contract version: envelope says v=${env.v}, expected v∈[${WIRE_VERSION_MIN_ACCEPTED}, ${PING_WIRE_CONTRACT_VERSION}]`,\n );\n }\n if (env.conversation_id.length !== 16) {\n throw new ValidationError(\n \"BadConversationIdLen\",\n `conversation_id must be 16 bytes (ULID), got ${env.conversation_id.length}`,\n );\n }\n if (env.sender_device.length !== 32) {\n throw new ValidationError(\n \"BadSenderDeviceLen\",\n `sender_device must be 32 bytes, got ${env.sender_device.length}`,\n );\n }\n // CR-6 routing: skip the hash check for v=2 application envelopes.\n if (env.v >= 2 && env.kind === \"Application\") {\n return;\n }\n const buf = new Uint8Array(1 + env.payload.length);\n buf[0] = KIND_DISCRIMINANT[env.kind];\n buf.set(env.payload, 1);\n const computed = await sha256(buf);\n if (!bytesEqual(computed, env.content_hash)) {\n throw new ValidationError(\n \"ContentHashMismatch\",\n \"content_hash mismatch: envelope is forged, corrupted, or built by a non-conformant sender\",\n );\n }\n}\n\n/**\n * Verify a v=2 `Application` envelope's content_hash against the decrypted plaintext.\n *\n * Caller MUST run this after MLS decrypt for application envelopes whose `v >= 2`. For\n * v=1 envelopes or handshake kinds this is a no-op (those are covered by [[validate]]).\n */\nexport async function verifyApplicationContentHash(\n env: MessageEnvelope,\n plaintext: Uint8Array,\n): Promise<void> {\n if (env.v < 2 || env.kind !== \"Application\") return;\n const computed = await sha256(plaintext);\n if (!bytesEqual(computed, env.content_hash)) {\n throw new ValidationError(\n \"ContentHashMismatch\",\n \"v=2 application content_hash mismatch against decrypted plaintext\",\n );\n }\n}\n\n/**\n * True if the supplied content type negotiates the ping-wire-contract.\n *\n * Accepts the v2 canonical types AND the v1 legacy types during the CR-6 dual-accept\n * window. Tolerates whitespace and parameters (e.g. `; charset=utf-8`).\n */\nexport function negotiates(contentType: string | null | undefined): boolean {\n if (!contentType) return false;\n const primary = contentType.split(\";\")[0]?.trim().toLowerCase() ?? \"\";\n return (\n primary === CONTENT_TYPE_CBOR\n || primary === CONTENT_TYPE_JSON\n || primary === CONTENT_TYPE_CBOR_LEGACY_V1\n || primary === CONTENT_TYPE_JSON_LEGACY_V1\n );\n}\n","// Reference WebSocket transport. Treats the server as an opaque relay that:\n// - accepts a CBOR-encoded MessageEnvelope on `send`\n// - returns events newer than a cursor on `fetchSince`\n// - streams new events on the WS itself for `subscribe`\n// - exposes a directory endpoint for `discoverDevices`\n//\n// This is a sample. Real deployments will tune framing / auth.\n\nimport type { ConversationId, DiscoveredDevice, MessageEnvelope, Transport, UserId } from \"../types\";\nimport {\n CONTENT_TYPE_JSON,\n PING_WIRE_CONTRACT_HEADER,\n PING_WIRE_CONTRACT_HEADER_VALUE,\n} from \"../wire-contract\";\n\nexport interface WsTransportConfig {\n baseUrl: string; // ws[s]://...\n authHeader?: string; // optional bearer token\n fetchImpl?: typeof fetch; // override for SSR / tests\n}\n\nexport class WebSocketTransport implements Transport {\n private ws: WebSocket | null = null;\n private liveHandlers = new Set<(env: MessageEnvelope) => void>();\n private fetchImpl: typeof fetch;\n\n constructor(private cfg: WsTransportConfig) {\n this.fetchImpl = cfg.fetchImpl ?? globalThis.fetch.bind(globalThis);\n this.openSocket();\n }\n\n private openSocket() {\n if (typeof WebSocket === \"undefined\") return;\n const url = `${this.cfg.baseUrl.replace(/^http/, \"ws\")}/stream`;\n this.ws = new WebSocket(url);\n this.ws.onmessage = (ev) => {\n // Server sends one JSON envelope per text frame.\n const text = typeof ev.data === \"string\" ? ev.data : new TextDecoder().decode(ev.data as ArrayBuffer);\n try {\n const env = JSON.parse(text, reviver) as MessageEnvelope;\n for (const h of this.liveHandlers) h(env);\n } catch (e) {\n if (typeof console !== \"undefined\") console.debug(\"[ping-sdk] WS frame parse error:\", e);\n }\n };\n this.ws.onclose = () => setTimeout(() => this.openSocket(), 1000);\n }\n\n async send(envelope: MessageEnvelope): Promise<void> {\n // We're emitting the JSON projection of the ping-wire-contract. Production transports\n // would use CONTENT_TYPE_CBOR with a real CBOR codec; for the demo path the JSON shape\n // is the documented development projection.\n const res = await this.fetchImpl(`${this.cfg.baseUrl}/send`, {\n method: \"POST\",\n headers: {\n \"content-type\": CONTENT_TYPE_JSON,\n [PING_WIRE_CONTRACT_HEADER]: PING_WIRE_CONTRACT_HEADER_VALUE,\n ...(this.cfg.authHeader ? { authorization: this.cfg.authHeader } : {}),\n },\n body: JSON.stringify(envelope, replacer),\n });\n if (res.status === 409) throw new Error(\"EpochOccupied\");\n if (!res.ok) throw new Error(`transport send failed: ${res.status}`);\n }\n\n async fetchSince(conversationId: ConversationId, cursor: Uint8Array, limit: number): Promise<MessageEnvelope[]> {\n const url = new URL(`${this.cfg.baseUrl}/sync`);\n url.searchParams.set(\"conv\", toHex(conversationId));\n url.searchParams.set(\"cursor\", btoaUrl(cursor));\n url.searchParams.set(\"limit\", String(limit));\n const res = await this.fetchImpl(url.toString(), {\n headers: { ...(this.cfg.authHeader ? { authorization: this.cfg.authHeader } : {}) },\n });\n if (!res.ok) throw new Error(`transport fetch failed: ${res.status}`);\n const text = await res.text();\n return JSON.parse(text, reviver) as MessageEnvelope[];\n }\n\n subscribe(onEvent: (env: MessageEnvelope) => void): { unsubscribe(): void } {\n this.liveHandlers.add(onEvent);\n return { unsubscribe: () => this.liveHandlers.delete(onEvent) };\n }\n\n async discoverDevices(userId: UserId): Promise<DiscoveredDevice[]> {\n const res = await this.fetchImpl(`${this.cfg.baseUrl}/devices/${toHex(userId)}`, {\n headers: { ...(this.cfg.authHeader ? { authorization: this.cfg.authHeader } : {}) },\n });\n if (!res.ok) throw new Error(`device discovery failed: ${res.status}`);\n const text = await res.text();\n return JSON.parse(text, reviver) as DiscoveredDevice[];\n }\n}\n\n// ---- Wire codec ----\n//\n// Demo wire format: JSON with byte fields wrapped as `{ _bytes: number[] }`. The relay's\n// `envFromJson`/`envToJson` use the same shape. The spec calls for CBOR; v0.2 swaps in a\n// real CBOR codec, but the JSON form is much friendlier to debug.\n//\n// (decodeEnvelope helpers used to live here; collapsed into inline JSON.parse calls above.)\n\nfunction replacer(_k: string, v: unknown): unknown {\n if (v instanceof Uint8Array) return { _bytes: Array.from(v) };\n return v;\n}\nfunction reviver(_k: string, v: unknown): unknown {\n if (v && typeof v === \"object\" && Array.isArray((v as { _bytes?: number[] })._bytes)) {\n return new Uint8Array((v as { _bytes: number[] })._bytes);\n }\n return v;\n}\n\nfunction toHex(b: Uint8Array): string {\n let s = \"\"; for (const v of b) s += v.toString(16).padStart(2, \"0\"); return s;\n}\nfunction btoaUrl(b: Uint8Array): string {\n let s = \"\"; for (const v of b) s += String.fromCharCode(v);\n return btoa(s).replace(/\\+/g, \"-\").replace(/\\//g, \"_\").replace(/=+$/, \"\");\n}\n","// Public entry point. Spawns the dedicated Worker that hosts the WASM instance and exposes a\n// Promise-based, identical-to-native API. Crypto runs off the main thread.\n\nimport type {\n AdmitChatEntry, AdmitChatOutcome,\n CatchupAppEvent, CatchupSnapshotView,\n ConversationId, ConversationMeta, DeviceId, IncomingMessage,\n HistoryBundle, MemberInfo,\n KeyPackageEntry, LinkingTicket, MessageEnvelope, RecoveryBackup, Storage, Transport, UserId,\n} from \"./types\";\nimport type { Request, Response } from \"./protocol\";\n\nexport type * from \"./types\";\n\n// Re-export the default Storage and Transport implementations from the main entry so consumers\n// whose bundlers don't honor package.json#exports (Metro with strict resolution, some legacy\n// Webpack configs) can still get them with a plain `import { ... } from \"ping-openmls-sdk\"`. Power\n// users who care about tree-shaking can keep using the subpath imports.\nexport { IndexedDbStorage } from \"./storage/indexeddb\";\nexport { WebSocketTransport } from \"./transport/websocket\";\n\n// Ping wire contract — version, content-type constants, and runtime validators. Internal\n// products MUST claim conformance; external consumers MAY adopt or run their own profile.\n// See docs/PING_WIRE_CONTRACT.md.\nexport {\n PING_WIRE_CONTRACT_VERSION,\n PING_WIRE_CONTRACT_HEADER,\n PING_WIRE_CONTRACT_HEADER_VALUE,\n WIRE_VERSION_MIN_ACCEPTED,\n CONTENT_TYPE_CBOR,\n CONTENT_TYPE_JSON,\n CONTENT_TYPE_CBOR_LEGACY_V1,\n CONTENT_TYPE_JSON_LEGACY_V1,\n ValidationError,\n validate,\n verifyApplicationContentHash,\n negotiates,\n} from \"./wire-contract\";\n\nexport interface ClientConfig {\n identityExport: Uint8Array;\n deviceLabel: string;\n storage: Storage;\n transport: Transport;\n /** Override the wall clock (test/sim only). */\n now?: () => number;\n /**\n * Optional 32-byte Ed25519 secret key. When supplied AND no\n * `LocalDevice` is yet persisted in `storage`, the SDK adopts this\n * as its device signing key — so `deviceId()` returns\n * `SHA-256(public_key_of(secret))`, deterministic from what the\n * caller passed.\n *\n * Use case: align the SDK's `device_id` (which the SDK stamps into\n * every envelope's `sender_device` field) with an externally-\n * computed device id — typically `SHA-256(device_signing_pubkey)`\n * in the host's auth layer, where the JWT carries that same value\n * as its `device_id` claim. Without this alignment, a server that\n * validates `envelope.sender_device == jwt.device_id` rejects every\n * upload with `sender_device_mismatch`.\n *\n * Ignored on subsequent inits (when `storage` already holds a\n * persisted `LocalDevice`) — the on-disk identity is authoritative\n * for stability across restarts.\n */\n deviceSigningSecretKey?: Uint8Array;\n}\n\nexport interface Conversation {\n readonly id: ConversationId;\n send(plaintext: Uint8Array): Promise<MessageEnvelope>;\n /** [CR-2] Add members by (device_id, KeyPackage) pair. Hosts typically pull these\n * straight from `transport.discoverDevices(userId)`. */\n addMembers(entries: KeyPackageEntry[]): Promise<void>;\n removeMembers(leafIndexes: number[]): Promise<void>;\n /** Change the conversation `name` (carried in the GroupContext) via an MLS\n * GroupContextExtensions commit — the rename / embedded avatar-media-id change\n * propagates to every member and future joiner through group STATE, not a side\n * broadcast. Pass `null` to clear. All members must have re-linked since the\n * group-name capability shipped, else the SDK throws. */\n setConversationName(name: string | null): Promise<void>;\n meta(): ConversationMeta;\n}\n\nexport class MessagingClient {\n private worker: Worker;\n private nextId = 1;\n private pending = new Map<number, { resolve: (v: unknown) => void; reject: (e: Error) => void }>();\n private storage: Storage;\n private transport: Transport;\n private now: () => number;\n private messageHandlers = new Set<(m: IncomingMessage) => void>();\n private conversationHandlers = new Set<(id: ConversationId, epoch: number, sender?: DeviceId) => void>();\n private subscription: { unsubscribe(): void } | null = null;\n private metaCache = new Map<string, ConversationMeta>();\n // Cached at init() so dispatchIncoming can skip envelopes we sent ourselves — the relay\n // broadcasts to all WS subscribers including the sender, and re-applying our own already-\n // merged commits would error with \"epoch differs\".\n private ownDeviceId: Uint8Array | null = null;\n\n private constructor(cfg: ClientConfig, worker: Worker) {\n this.worker = worker;\n this.storage = cfg.storage;\n this.transport = cfg.transport;\n this.now = cfg.now ?? (() => Date.now());\n this.worker.onmessage = (ev) => this.handleWorker(ev.data as Response);\n }\n\n /** Generate a fresh identity. The returned bytes are a secret — store them encrypted. */\n static async generateIdentity(): Promise<Uint8Array> {\n return MessagingClient.ephemeralWorkerCall<Uint8Array>({\n kind: \"generateIdentity\", id: 1,\n });\n }\n\n /**\n * HPKE-seal a `LinkingTicket` for delivery to a new device ([CR-3]).\n *\n * Returns CBOR-encoded bytes safe to relay through an untrusted inbox. The new device's\n * X25519 public key (`newDevicePub`) is exchanged out-of-band — typically as part of a\n * QR-encoded handshake on the linking screen. Pure function; runs in an ephemeral\n * worker so no live `MessagingClient` is required on the sender side.\n *\n * Suite: `DHKEM(X25519, HKDF-SHA256), HKDF-SHA256, AES-128-GCM` (matches MLS).\n */\n static async sealLinkingTicket(\n ticket: LinkingTicket,\n newDevicePub: Uint8Array,\n ): Promise<Uint8Array> {\n if (newDevicePub.length !== 32) {\n throw new Error(\"sealLinkingTicket: newDevicePub must be 32 bytes\");\n }\n return MessagingClient.ephemeralWorkerCall<Uint8Array>({\n kind: \"sealLinkingTicket\", id: 1, ticket, newDevicePub,\n });\n }\n\n /**\n * HPKE-open a sealed `LinkingTicket` on the new device ([CR-3]).\n *\n * The receiving device hasn't booted a `MessagingClient` yet — it only has its X25519\n * ephemeral keypair. This static method runs the open in an ephemeral worker, returning\n * the cleartext ticket which the host then feeds to `init()` + `consumeLinkingTicket()`.\n *\n * On failure the error message is deliberately generic (no discriminator between \"wrong\n * key\" vs \"tampered ciphertext\") so a probing attacker can't tell the new device's key\n * was the issue.\n */\n static async openLinkingTicket(\n sealed: Uint8Array,\n newDevicePriv: Uint8Array,\n ): Promise<LinkingTicket> {\n if (newDevicePriv.length !== 32) {\n throw new Error(\"openLinkingTicket: newDevicePriv must be 32 bytes\");\n }\n return MessagingClient.ephemeralWorkerCall<LinkingTicket>({\n kind: \"openLinkingTicket\", id: 1, sealed, newDevicePriv,\n });\n }\n\n /**\n * Generic HPKE-Base seal of arbitrary bytes to `recipientPub` (32-byte X25519)\n * under a caller-supplied `info`. One shared HPKE implementation so app layers\n * (e.g. the device-linking auth envelope) stop hand-rolling per-platform HPKE\n * that drifts and breaks cross-platform. Runs in an ephemeral worker.\n */\n static async hpkeSeal(\n plaintext: Uint8Array,\n recipientPub: Uint8Array,\n info: Uint8Array,\n ): Promise<Uint8Array> {\n if (recipientPub.length !== 32) {\n throw new Error(\"hpkeSeal: recipientPub must be 32 bytes\");\n }\n return MessagingClient.ephemeralWorkerCall<Uint8Array>({\n kind: \"hpkeSeal\", id: 1, plaintext, recipientPub, info,\n });\n }\n\n /** Generic HPKE-Base open — inverse of {@link hpkeSeal}. `info` must match the sender. */\n static async hpkeOpen(\n sealed: Uint8Array,\n recipientPriv: Uint8Array,\n info: Uint8Array,\n ): Promise<Uint8Array> {\n if (recipientPriv.length !== 32) {\n throw new Error(\"hpkeOpen: recipientPriv must be 32 bytes\");\n }\n return MessagingClient.ephemeralWorkerCall<Uint8Array>({\n kind: \"hpkeOpen\", id: 1, sealed, recipientPriv, info,\n });\n }\n\n // Spawn-call-terminate helper for stateless WASM functions exposed at module scope (no\n // PingClient required). Each call gets a fresh worker; the cost is acceptable because\n // these are one-shot, off-the-hot-path operations (identity gen, linking ticket seal/open).\n private static ephemeralWorkerCall<T>(req: Request): Promise<T> {\n const w = new Worker(new URL(\"./worker.js\", import.meta.url), { type: \"module\" });\n return new Promise<T>((resolve, reject) => {\n w.onmessage = (ev) => {\n const msg = ev.data as Response;\n if (msg.kind === \"result\" && msg.id === (req as { id: number }).id) {\n w.terminate();\n msg.ok ? resolve(msg.value as T) : reject(new Error(msg.error));\n }\n };\n w.postMessage(req);\n });\n }\n\n static async init(cfg: ClientConfig): Promise<MessagingClient> {\n const worker = new Worker(new URL(\"./worker.js\", import.meta.url), { type: \"module\" });\n const client = new MessagingClient(cfg, worker);\n\n await client.call({\n kind: \"init\",\n id: client.nextId++,\n identityExport: cfg.identityExport,\n deviceLabel: cfg.deviceLabel,\n nowMs: client.now(),\n ...(cfg.deviceSigningSecretKey\n ? { deviceSigningSecretKey: cfg.deviceSigningSecretKey }\n : {}),\n });\n\n // Cache the device id once so we can drop envelopes we sent ourselves on the WS echo.\n client.ownDeviceId = await client.deviceId();\n\n // Start the live subscription. We dispatch each envelope to the right handler:\n // - Welcome envelopes for unknown conversations → joinConversation\n // - Welcomes for already-joined conversations → drop (server may broadcast duplicates)\n // - Welcomes addressed to other devices → drop (we can't decrypt)\n // - Everything else → processEnvelope\n client.subscription = cfg.transport.subscribe((envelope) => {\n void client.dispatchIncoming(envelope);\n });\n return client;\n }\n\n /** Internal: route an inbound envelope to the right handler. */\n private async dispatchIncoming(envelope: MessageEnvelope): Promise<void> {\n // The relay broadcasts every envelope to every WS subscriber, including the sender. For\n // our own sends, the local MLS state has already been advanced in-process — re-applying\n // would either fail with \"epoch differs\" (commits) or be a no-op dedupe (application\n // messages). Skip them here.\n if (this.ownDeviceId && sameBytesU8(envelope.sender_device, this.ownDeviceId)) {\n return;\n }\n if (envelope.kind === \"Welcome\") {\n try {\n const known = await this.listConversations();\n const already = known.some((m) => sameBytesU8(m.id, envelope.conversation_id));\n if (already) return;\n await this.joinConversation(envelope);\n } catch (e) {\n // Welcomes are broadcast to every connected client; only one device's KeyPackage can\n // decrypt each one, so most fail. Log at debug, drop silently.\n if (typeof console !== \"undefined\") console.debug(\"[ping-sdk] welcome ignored:\", e);\n }\n return;\n }\n try {\n await this.processEnvelope(envelope);\n } catch (e) {\n if (typeof console !== \"undefined\") console.debug(\"[ping-sdk] envelope drop:\", e);\n }\n }\n\n // ------------------------ Public API ------------------------\n\n async userId(): Promise<UserId> {\n return await this.call({ kind: \"userId\", id: this.nextId++ }) as UserId;\n }\n\n /**\n * Export the account identity seed (CBOR-wrapped Ed25519, **SECRET**).\n * Transfer to a newly-linked device over the sealed linking channel so all of\n * an account's devices share one `userId` — the basis for cross-device\n * self-attribution (`IncomingMessage.sender_user_id === userId()`). Never log\n * or persist in cleartext.\n */\n async exportIdentity(): Promise<Uint8Array> {\n return await this.call({ kind: \"exportIdentity\", id: this.nextId++ }) as Uint8Array;\n }\n\n async deviceId(): Promise<DeviceId> {\n return await this.call({ kind: \"deviceId\", id: this.nextId++ }) as DeviceId;\n }\n\n async freshKeyPackage(): Promise<Uint8Array> {\n return await this.call({ kind: \"freshKeyPackage\", id: this.nextId++ }) as Uint8Array;\n }\n\n async createConversation(opts: { name?: string } = {}): Promise<Conversation> {\n const idBytes = await this.call({\n kind: \"createConversation\", id: this.nextId++, name: opts.name ?? null, nowMs: this.now(),\n }) as ConversationId;\n return this.conversationProxy(idBytes);\n }\n\n async joinConversation(welcome: MessageEnvelope): Promise<Conversation> {\n const idBytes = await this.call({\n kind: \"joinConversation\", id: this.nextId++, welcome, nowMs: this.now(),\n }) as ConversationId;\n return this.conversationProxy(idBytes);\n }\n\n /**\n * Get a `Conversation` proxy for an already-known conversation. Use this when you have a\n * conversation id (e.g., from `listConversations()`) and want to send/addMembers without\n * re-creating it. Throws if the id isn't known to the worker — call `listConversations()`\n * first to verify membership.\n */\n getConversation(id: ConversationId): Conversation {\n return this.conversationProxy(id);\n }\n\n async listConversations(): Promise<ConversationMeta[]> {\n const metas = await this.call({ kind: \"listConversations\", id: this.nextId++ }) as ConversationMeta[];\n this.metaCache.clear();\n for (const m of metas) this.metaCache.set(hex(m.id), m);\n return metas;\n }\n\n /**\n * Member roster for a conversation, recovered locally from the MLS\n * group's leaf credentials (no network, no out-of-band message). Returns\n * one entry per device leaf — a multi-device user appears multiple times,\n * so dedup by `user_id` for a per-user roster. Empty for an unknown id.\n */\n async members(conv: ConversationId): Promise<MemberInfo[]> {\n return await this.call({ kind: \"members\", id: this.nextId++, conv }) as MemberInfo[];\n }\n\n async syncConversations(): Promise<IncomingMessage[]> {\n return await this.call({\n kind: \"syncConversations\", id: this.nextId++, nowMs: this.now(),\n }) as IncomingMessage[];\n }\n\n async processEnvelope(envelope: MessageEnvelope): Promise<IncomingMessage | null> {\n return await this.call({\n kind: \"processEnvelope\", id: this.nextId++, envelope, nowMs: this.now(),\n }) as IncomingMessage | null;\n }\n\n /**\n * Build a `LinkingTicket` for a new device.\n *\n * [CR-13] `lastAppEvents` is host-supplied per-conversation \"what you missed\" data\n * (decrypted AppEvent bytes the new device will render before sync catches up).\n * Pass `[]` to suppress catchup data — the new device will see an empty conversation\n * list until normal sync runs.\n *\n * Caller is responsible for HPKE-sealing the returned ticket via\n * `MessagingClient.sealLinkingTicket` before transmitting (CR-3).\n */\n async buildLinkingTicket(\n newDeviceId: DeviceId,\n newDeviceKp: Uint8Array,\n lastAppEvents: CatchupAppEvent[] = [],\n ): Promise<LinkingTicket> {\n return await this.call({\n kind: \"buildLinkingTicket\", id: this.nextId++,\n newDeviceId, newDeviceKp, lastAppEvents, nowMs: this.now(),\n }) as LinkingTicket;\n }\n\n /**\n * Decode a `LinkingTicket.catchup_snapshot` blob ([CR-13]).\n *\n * Pure WASM — runs in an ephemeral worker so the new device can call this before\n * `MessagingClient.init()` completes (just like `openLinkingTicket`). Returns the\n * structured view: per-conversation metas + last-known AppEvent bytes.\n *\n * Throws on malformed or oversized inputs. Empty bytes return `null` — that's the\n * \"no catchup data\" case (sender passed `[]` to buildLinkingTicket).\n */\n static async decodeCatchupSnapshot(\n snapshotBytes: Uint8Array,\n ): Promise<CatchupSnapshotView | null> {\n if (snapshotBytes.length === 0) return null;\n return MessagingClient.ephemeralWorkerCall<CatchupSnapshotView>({\n kind: \"decodeCatchupSnapshot\", id: 1, snapshotBytes,\n });\n }\n\n async consumeLinkingTicket(ticket: LinkingTicket): Promise<void> {\n await this.call({\n kind: \"consumeLinkingTicket\", id: this.nextId++, ticket, nowMs: this.now(),\n });\n }\n\n // ------------------------ Recovery (account backup) ------------------------\n // All static + stateless: they run on an ephemeral worker, so the recovery screen can\n // call them before any client is initialised. Same Argon2id/AES-256-GCM IdentityBackup\n // wire format as the iOS/Android FFI bindings — backups are portable across platforms.\n\n /** Generate a fresh 12-word BIP39 mnemonic, returned as its canonical phrase string. */\n static async generateMnemonicPhrase(): Promise<string> {\n return MessagingClient.ephemeralWorkerCall<string>({\n kind: \"generateMnemonicPhrase\", id: 1,\n });\n }\n\n /** Validate + canonicalise a user-entered phrase (BIP39 wordlist + checksum). Rejects\n * malformed input. Use on the recovery screen before calling `decryptBackup`. */\n static async normalizeMnemonicPhrase(phrase: string): Promise<string> {\n return MessagingClient.ephemeralWorkerCall<string>({\n kind: \"normalizeMnemonicPhrase\", id: 1, phrase,\n });\n }\n\n /** Encrypt an IdentityBackup for `POST /v1/recovery/blobs`. `deviceGroupSnapshot` may be\n * empty (Conservative path) or the DeviceGroup's `exportConversationStateSnapshot`. */\n static async encryptBackup(args: {\n mnemonicPhrase: string;\n accountId: Uint8Array;\n identityExport: Uint8Array;\n deviceGroupSnapshot: Uint8Array;\n /** External conversations to embed so recovery restores them offline. Pass\n * `[]` (the default) for identity + DeviceGroup only. Each `groupStateBytes`\n * is `Conversation.exportConversationStateSnapshot` output. Forward-secrecy\n * trade-off — see the SDK recovery docs. */\n conversationSnapshots?: { conversationId: Uint8Array; groupStateBytes: Uint8Array }[];\n createdAtMs: number;\n }): Promise<Uint8Array> {\n return MessagingClient.ephemeralWorkerCall<Uint8Array>({\n kind: \"encryptBackup\", id: 1,\n mnemonicPhrase: args.mnemonicPhrase,\n accountId: args.accountId,\n identityExport: args.identityExport,\n deviceGroupSnapshot: args.deviceGroupSnapshot,\n conversationSnapshots: args.conversationSnapshots ?? [],\n createdAtMs: args.createdAtMs,\n });\n }\n\n /** Decrypt a recovery blob from `GET /v1/recovery/blobs`. Throws on wrong mnemonic or a\n * tampered blob. The returned `identity_export` is the secret seed for `init`. */\n static async decryptBackup(\n blob: Uint8Array,\n mnemonicPhrase: string,\n ): Promise<RecoveryBackup> {\n return MessagingClient.ephemeralWorkerCall<RecoveryBackup>({\n kind: \"decryptBackup\", id: 1, blob, mnemonicPhrase,\n });\n }\n\n // ------------------------ Peer-assisted history transfer ------------------------\n // Authenticated (sign-then-encrypt) device-to-device history used by linking and\n // post-recovery peer re-add. Confidential to the recipient's ephemeral X25519 key AND\n // signed by the account identity key, so the receiver proves the sender. Plaintext-only\n // share — preserves forward secrecy (no MLS keys leave the device).\n\n /** Seal+sign a `HistoryBundle` for a recipient device. `recipientPub` (X25519, 32B) is the\n * recipient's ephemeral pubkey from the linking QR; `senderIdentitySecret` (Ed25519 seed,\n * 32B) is this account's identity key. Returns the blob to POST to /v1/history-transfers. */\n static async sealHistoryBundle(\n bundle: HistoryBundle,\n recipientPub: Uint8Array,\n senderIdentitySecret: Uint8Array,\n ): Promise<Uint8Array> {\n return MessagingClient.ephemeralWorkerCall<Uint8Array>({\n kind: \"sealHistoryBundle\", id: 1, bundle, recipientPub, senderIdentitySecret,\n });\n }\n\n /** Open+verify a sealed history bundle. `recipientPriv` (X25519, 32B) is this device's\n * ephemeral private key; `senderIdentityPub` (Ed25519, 32B) is the account identity pubkey\n * from the linking handshake. Throws if the signature doesn't verify. */\n static async openHistoryBundle(\n sealed: Uint8Array,\n recipientPriv: Uint8Array,\n senderIdentityPub: Uint8Array,\n ): Promise<HistoryBundle> {\n return MessagingClient.ephemeralWorkerCall<HistoryBundle>({\n kind: \"openHistoryBundle\", id: 1, sealed, recipientPriv, senderIdentityPub,\n });\n }\n\n /**\n * Admit a freshly-linked device to every chat in `entries` — one MLS\n * Commit + Welcome per chat, with the Welcome's `?recipient=` priming\n * handled inside the SDK. This is the post-link reconciliation step\n * that follows `consumeLinkingTicket`: the new device has the\n * DeviceGroup (from the ticket) and metadata for external chats (from\n * the catchup snapshot), but its leaf is not yet in any external\n * MLS group. The host on the EXISTING device calls this once the\n * new device shows up in `/v1/auth/sessions` and has uploaded its\n * KeyPackage batch, passing one freshly-claimed KP per chat.\n *\n * Per-chat failures (stale KP, transport error, unknown chat) are\n * captured in the returned outcomes rather than aborting — one bad\n * chat shouldn't lose the user access to all the others.\n */\n async admitDeviceToChats(\n deviceId: DeviceId,\n entries: AdmitChatEntry[],\n ): Promise<AdmitChatOutcome[]> {\n return await this.call({\n kind: \"admitDeviceToChats\", id: this.nextId++,\n deviceId, entries, nowMs: this.now(),\n }) as AdmitChatOutcome[];\n }\n\n /**\n * Revoke a device ([CR-2]).\n *\n * Removes `deviceId`'s leaf from every conversation where this client locally\n * tracked the device→leaf mapping (i.e. conversations where this client itself\n * admitted the device via `addMembers`, or where this client is the device being\n * revoked). The SDK has already broadcast each Commit envelope via\n * `transport.send`; the returned array is for any additional host-side handling\n * (audit logs, UI). An empty array is a valid outcome — the device wasn't locally\n * known anywhere.\n *\n * **Scope note.** Conversations where a peer admitted the device on this client's\n * behalf are silently skipped — the leaf index isn't locally known. Host can fall\n * back to `transport.discoverDevices` + a manual `remove_members(leafIndex)` for\n * those, or wait for the affected user to revoke from a device that did the admit.\n * See `docs/architecture/multi-device.md §Device removal`.\n */\n async revokeDevice(deviceId: DeviceId): Promise<MessageEnvelope[]> {\n return await this.call({\n kind: \"revokeDevice\", id: this.nextId++, deviceId, nowMs: this.now(),\n }) as MessageEnvelope[];\n }\n\n /**\n * Export a portable MLS state snapshot for one conversation ([CR-7]).\n *\n * Returned bytes can be embedded in a recovery blob or shipped through a\n * linking ticket so another device of the same user identity can re-attach to\n * the group via [[importStateSnapshot]]. The bytes contain past epoch secrets;\n * never log, never persist unencrypted, wipe (`.fill(0)`) after use.\n */\n async exportConversationStateSnapshot(conv: ConversationId): Promise<Uint8Array> {\n return await this.call({\n kind: \"exportConversationStateSnapshot\", id: this.nextId++,\n conv, nowMs: this.now(),\n }) as Uint8Array;\n }\n\n /**\n * Import a `GroupStateSnapshot` from another device of the same user identity\n * ([CR-7]).\n *\n * On success, the conversation appears in [[listConversations]] and decryption\n * of subsequent peer-originated traffic works against the imported state. For\n * full multi-device participation, the recovered device SHOULD then issue an\n * MLS Update Proposal to rotate onto a fresh leaf (post-v1 follow-up — see\n * `docs/design/CR4_CR7_PERSISTENCE.md` open question #3).\n */\n async importStateSnapshot(snapshotBytes: Uint8Array): Promise<ConversationId> {\n return await this.call({\n kind: \"importStateSnapshot\", id: this.nextId++,\n snapshotBytes, nowMs: this.now(),\n }) as ConversationId;\n }\n\n /**\n * Export a derived secret from a conversation's MLS exporter ([CR-8]).\n *\n * Used to seed the ephemeral channel (`ping/ephemeral`), call-media keys\n * (`ping/calls/media/<call_id>`), and call-ephemeral framer keys\n * (`ping/calls/ephemeral/<call_id>`). The returned bytes are a secret — never log,\n * persist only encrypted, and call `bytes.fill(0)` after use to wipe the typed array.\n *\n * Same `(conversationId, current_epoch, label, context, length)` produces the same\n * bytes across every binding (Rust core, native UniFFI, WASM/JS). Cross-platform\n * byte-equality is enforced by `protocol/conformance/export_secret/`.\n */\n async exportConversationSecret(\n conv: ConversationId,\n label: string,\n context: Uint8Array,\n length: number,\n ): Promise<Uint8Array> {\n if (length <= 0 || length > 1024) {\n throw new Error(\"exportConversationSecret: length must be 1..=1024\");\n }\n return await this.call({\n kind: \"exportConversationSecret\", id: this.nextId++,\n conv, label, context, length,\n }) as Uint8Array;\n }\n\n /** Subscribe to decrypted application messages. */\n onMessage(handler: (m: IncomingMessage) => void): () => void {\n this.messageHandlers.add(handler);\n return () => this.messageHandlers.delete(handler);\n }\n\n /** Fired after every state-changing event for a conversation (Commit, member changes). */\n /**\n * Fires after every conversation state change — joining via Welcome, processing a Commit,\n * etc. `sender` carries the device id of whoever triggered the event when known (e.g. the\n * inviter's device on auto-join), so the host can label peers in its UI.\n */\n onConversationUpdated(handler: (id: ConversationId, epoch: number, sender?: DeviceId) => void): () => void {\n this.conversationHandlers.add(handler);\n return () => this.conversationHandlers.delete(handler);\n }\n\n async close(): Promise<void> {\n this.subscription?.unsubscribe();\n this.worker.terminate();\n }\n\n // ------------------------ Internals ------------------------\n\n private conversationProxy(id: ConversationId): Conversation {\n const self = this;\n return {\n id,\n async send(plaintext) {\n return await self.call({\n kind: \"send\", id: self.nextId++, conv: id, plaintext, nowMs: self.now(),\n }) as MessageEnvelope;\n },\n async addMembers(entries) {\n await self.call({\n kind: \"addMembers\", id: self.nextId++, conv: id, entries, nowMs: self.now(),\n });\n },\n async removeMembers(leaves) {\n await self.call({\n kind: \"removeMembers\", id: self.nextId++, conv: id, leaves, nowMs: self.now(),\n });\n },\n async setConversationName(name) {\n await self.call({\n kind: \"setConversationName\", id: self.nextId++, conv: id, name, nowMs: self.now(),\n });\n },\n meta() {\n const cached = self.metaCache.get(hex(id));\n if (!cached) throw new Error(\"conversation meta not loaded; call listConversations() first\");\n return cached;\n },\n };\n }\n\n private call(req: Request): Promise<unknown> {\n return new Promise((resolve, reject) => {\n this.pending.set((req as { id: number }).id, { resolve, reject });\n this.worker.postMessage(req);\n });\n }\n\n private async handleWorker(msg: Response) {\n switch (msg.kind) {\n case \"result\": {\n const slot = this.pending.get(msg.id);\n if (!slot) return;\n this.pending.delete(msg.id);\n msg.ok ? slot.resolve(msg.value) : slot.reject(new Error(msg.error));\n return;\n }\n case \"event.message\": {\n for (const h of this.messageHandlers) h(msg.msg);\n return;\n }\n case \"event.conversation\": {\n for (const h of this.conversationHandlers) h(msg.id, msg.epoch, msg.sender);\n return;\n }\n case \"storage.call\": {\n try {\n const v = await (this.storage as unknown as Record<string, (...a: unknown[]) => unknown>)\n [msg.method]?.(...msg.args);\n this.worker.postMessage({ kind: \"storage.reply\", id: msg.id, ok: true, value: v } as Request);\n } catch (e) {\n this.worker.postMessage({\n kind: \"storage.reply\", id: msg.id, ok: false, error: String(e),\n } as Request);\n }\n return;\n }\n case \"transport.call\": {\n try {\n const v = await (this.transport as unknown as Record<string, (...a: unknown[]) => unknown>)\n [msg.method]?.(...msg.args);\n this.worker.postMessage({ kind: \"transport.reply\", id: msg.id, ok: true, value: v } as Request);\n } catch (e) {\n this.worker.postMessage({\n kind: \"transport.reply\", id: msg.id, ok: false, error: String(e),\n } as Request);\n }\n return;\n }\n }\n }\n}\n\nfunction hex(b: Uint8Array): string {\n let s = \"\";\n for (const v of b) s += v.toString(16).padStart(2, \"0\");\n return s;\n}\n\nfunction sameBytesU8(a: Uint8Array, b: Uint8Array): boolean {\n if (a.length !== b.length) return false;\n for (let i = 0; i < a.length; i++) if (a[i] !== b[i]) return false;\n return true;\n}\n"],"mappings":"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","names":[]}
package/dist/worker.cjs CHANGED
@@ -144,6 +144,8 @@ async function dispatch(msg) {
144
144
  return await ensureClient().send(msg.conv, msg.plaintext, msg.nowMs);
145
145
  case "addMembers":
146
146
  return await ensureClient().addMembers(msg.conv, msg.entries, msg.nowMs);
147
+ case "setConversationName":
148
+ return await ensureClient().setConversationName(msg.conv, msg.name ?? void 0, msg.nowMs);
147
149
  case "admitDeviceToChats":
148
150
  return await ensureClient().admitDeviceToChats(msg.deviceId, msg.entries, msg.nowMs);
149
151
  case "removeMembers":
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/worker.ts"],"sourcesContent":["// Web Worker entry. Owns the WASM `PingClient` instance. Receives Requests from the main thread\n// and posts Responses back. Storage / Transport calls back to the main thread are tracked by id.\n//\n// The WASM module is loaded lazily on the first `init` so that ephemeral identity-only spawns\n// (used by `MessagingClient.generateIdentity()`) don't pay the cost.\n\n/// <reference lib=\"webworker\" />\n\nimport init, {\n PingClient,\n sealLinkingTicket as wasmSealLinkingTicket,\n openLinkingTicket as wasmOpenLinkingTicket,\n hpkeSeal as wasmHpkeSeal,\n hpkeOpen as wasmHpkeOpen,\n decodeCatchupSnapshot as wasmDecodeCatchupSnapshot,\n generateMnemonicPhrase as wasmGenerateMnemonicPhrase,\n normalizeMnemonicPhrase as wasmNormalizeMnemonicPhrase,\n encryptBackup as wasmEncryptBackup,\n decryptBackup as wasmDecryptBackup,\n sealHistoryBundle as wasmSealHistoryBundle,\n openHistoryBundle as wasmOpenHistoryBundle,\n} from \"../wasm/ping_wasm\";\nimport type { Request, Response } from \"./protocol\";\nimport type { MessageEnvelope } from \"./types\";\n\ndeclare const self: DedicatedWorkerGlobalScope;\n\nlet client: PingClient | null = null;\nlet wasmReady: Promise<void> | null = null;\n\nconst pendingStorage = new Map<number, (r: { ok: boolean; value: unknown }) => void>();\nconst pendingTransport = new Map<number, (r: { ok: boolean; value: unknown }) => void>();\nlet nextStorageId = 1;\nlet nextTransportId = 1;\n\nconst proxyStorage = {\n get: (namespace: string, key: string) => callMain<Uint8Array | null>(\"storage\", \"get\", [namespace, key]),\n put: (namespace: string, key: string, value: Uint8Array) =>\n callMain<void>(\"storage\", \"put\", [namespace, key, value]),\n del: (namespace: string, key: string) => callMain<void>(\"storage\", \"del\", [namespace, key]),\n listKeys: (namespace: string, prefix: string) =>\n callMain<string[]>(\"storage\", \"listKeys\", [namespace, prefix]),\n};\nconst proxyTransport = {\n send: (envelope: MessageEnvelope) => callMain<void>(\"transport\", \"send\", [envelope]),\n fetchSince: (conv: Uint8Array, cursor: Uint8Array, limit: number) =>\n callMain<MessageEnvelope[]>(\"transport\", \"fetchSince\", [conv, cursor, limit]),\n discoverDevices: (userId: Uint8Array) => callMain<unknown[]>(\"transport\", \"discoverDevices\", [userId]),\n // Optional on the host (see `Transport.setNextWelcomeRecipients` in types.ts).\n // The main-thread dispatch (index.ts `handleWorker → case \"transport.call\"`)\n // resolves `undefined` to a `transport.reply { ok: true, value: undefined }`\n // when the host hasn't implemented the method, which we treat as a no-op.\n setNextWelcomeRecipients: (conv: Uint8Array, recipients: Uint8Array[]) =>\n callMain<void>(\"transport\", \"setNextWelcomeRecipients\", [conv, recipients]),\n};\n\nfunction callMain<T>(kind: \"storage\" | \"transport\", method: string, args: unknown[]): Promise<T> {\n return new Promise((resolve, reject) => {\n const id = kind === \"storage\" ? nextStorageId++ : nextTransportId++;\n const map = kind === \"storage\" ? pendingStorage : pendingTransport;\n map.set(id, ({ ok, value }) => ok ? resolve(value as T) : reject(new Error(String(value))));\n self.postMessage({ kind: `${kind}.call`, id, method, args } as Response);\n });\n}\n\nasync function ensureWasm() {\n if (wasmReady) return wasmReady;\n // The bundled wasm path is rewritten by the build script to be relative to `dist/`.\n wasmReady = init().then(() => undefined);\n return wasmReady;\n}\n\n// The WASM `PingClient` is SINGLE-THREADED (this one worker). Its async\n// methods hold internal locks across `.await` (e.g. a `conversations`\n// RwLock guard kept while `snapshot_to_storage` awaits IndexedDB). If we\n// let `onmessage` dispatch a SECOND top-level request while the first is\n// suspended at an await, the two WASM calls interleave on the single\n// thread, the second re-acquires a held lock, and `parking_lot`'s wasm\n// stub `panic!`s with \"Parking not supported on this platform\" — which\n// poisons the module (`RuntimeError: unreachable`) and breaks every\n// subsequent call. We therefore SERIALIZE top-level request dispatch:\n// each request runs to completion before the next begins.\n//\n// CRUCIAL: `storage.reply` / `transport.reply` are NOT serialized — an\n// in-flight request awaits those replies (via `callMain`), so queuing\n// them behind the request would deadlock. They resolve pending callbacks\n// immediately, exactly as before.\nlet dispatchChain: Promise<void> = Promise.resolve();\n\nasync function runRequest(msg: Request): Promise<void> {\n try {\n const value = await dispatch(msg);\n (self.postMessage as (m: Response) => void)({ kind: \"result\", id: msg.id, ok: true, value });\n } catch (e) {\n (self.postMessage as (m: Response) => void)({\n kind: \"result\", id: msg.id, ok: false, error: e instanceof Error ? e.message : String(e),\n });\n }\n}\n\nself.onmessage = (ev: MessageEvent<Request>) => {\n const msg = ev.data;\n\n // Storage / transport responses from the main thread — handle\n // immediately (they unblock the in-flight request). Never queued.\n if (msg.kind === \"storage.reply\") {\n const slot = pendingStorage.get(msg.id);\n if (slot) {\n pendingStorage.delete(msg.id);\n msg.ok ? slot({ ok: true, value: msg.value })\n : slot({ ok: false, value: msg.error });\n }\n return;\n }\n if (msg.kind === \"transport.reply\") {\n const slot = pendingTransport.get(msg.id);\n if (slot) {\n pendingTransport.delete(msg.id);\n msg.ok ? slot({ ok: true, value: msg.value })\n : slot({ ok: false, value: msg.error });\n }\n return;\n }\n\n // Top-level client request — chain it after the previous one so only a\n // single WASM call is ever in flight. `runRequest` never rejects (it\n // posts the error as a result), so the chain can't break.\n dispatchChain = dispatchChain.then(() => runRequest(msg));\n};\n\nasync function dispatch(msg: Request): Promise<unknown> {\n await ensureWasm();\n switch (msg.kind) {\n case \"init\": {\n client = await PingClient.init(\n msg.identityExport, msg.deviceLabel,\n proxyStorage as unknown as never,\n proxyTransport as unknown as never,\n msg.nowMs,\n // Optional Ed25519 device signing secret — see protocol.ts\n // and ClientConfig.deviceSigningSecretKey. Forwarding `null`\n // explicitly when absent so the wasm-bindgen `Option<Vec<u8>>`\n // shape is matched correctly.\n msg.deviceSigningSecretKey ?? null,\n );\n // Wire decrypted application-message callback back to the main thread.\n client.onMessage((m: unknown) => {\n (self.postMessage as (m: Response) => void)({ kind: \"event.message\", msg: m as never });\n });\n return null;\n }\n case \"generateIdentity\":\n return PingClient.generateIdentity();\n case \"userId\": return ensureClient().userId();\n case \"exportIdentity\": return ensureClient().exportIdentity();\n case \"deviceId\": return ensureClient().deviceId();\n case \"freshKeyPackage\": return ensureClient().freshKeyPackage();\n case \"createConversation\":\n return await ensureClient().createConversation(msg.name ?? undefined, msg.nowMs);\n case \"joinConversation\": {\n const id = await ensureClient().joinConversation(msg.welcome, msg.nowMs) as Uint8Array;\n // Notify the main thread that we just entered a new conversation. We forward the\n // Welcome's `sender_device` so the host can label \"joined chat with <inviter>\".\n (self.postMessage as (m: Response) => void)({\n kind: \"event.conversation\",\n id,\n epoch: 0,\n sender: msg.welcome.sender_device,\n });\n return id;\n }\n case \"listConversations\":\n return ensureClient().listConversations();\n case \"members\":\n return ensureClient().members(msg.conv);\n case \"send\":\n return await ensureClient().send(msg.conv, msg.plaintext, msg.nowMs);\n case \"addMembers\":\n // wasm-bindgen takes the entries as a plain array of {deviceId, keyPackage};\n // serde-wasm-bindgen on the Rust side deserialises into Vec<JsAddMemberEntry>.\n return await ensureClient().addMembers(msg.conv, msg.entries, msg.nowMs);\n case \"admitDeviceToChats\":\n // Per-chat add for a newly-linked device. Returns Vec<JsAdmitChatOutcome>\n // — the host inspects per-entry status to decide whether to retry.\n return await ensureClient().admitDeviceToChats(msg.deviceId, msg.entries, msg.nowMs);\n case \"removeMembers\":\n // wasm-bindgen exposes Vec<u32> as Uint32Array; convert from the protocol's number[].\n return await ensureClient().removeMembers(msg.conv, new Uint32Array(msg.leaves), msg.nowMs);\n case \"revokeDevice\":\n // [CR-2] returns the array of Commit envelopes the SDK has already broadcast.\n return await ensureClient().revokeDevice(msg.deviceId, msg.nowMs);\n case \"processEnvelope\": {\n const result = await ensureClient().processEnvelope(msg.envelope, msg.nowMs);\n (self.postMessage as (m: Response) => void)({\n kind: \"event.conversation\",\n id: msg.envelope.conversation_id,\n epoch: msg.envelope.epoch,\n });\n return result;\n }\n case \"syncConversations\":\n return await ensureClient().syncConversations(msg.nowMs);\n case \"buildLinkingTicket\":\n return await ensureClient().buildLinkingTicket(\n msg.newDeviceId, msg.newDeviceKp, msg.lastAppEvents, msg.nowMs,\n );\n case \"consumeLinkingTicket\":\n await ensureClient().consumeLinkingTicket(msg.ticket, msg.nowMs);\n return null;\n case \"decodeCatchupSnapshot\":\n // Pure WASM — no client state required. The new device may call this in an\n // ephemeral worker before init() completes.\n return wasmDecodeCatchupSnapshot(msg.snapshotBytes);\n case \"exportConversationSecret\":\n // wasm-bindgen returns a Uint8Array sized to `length`. The bytes are a secret —\n // the main thread is responsible for clearing the typed array when done.\n return ensureClient().exportConversationSecret(\n msg.conv, msg.label, msg.context, msg.length,\n );\n case \"sealLinkingTicket\":\n // Pure WASM call — no client state required. Lets a sender seal a ticket from an\n // ephemeral worker without standing up the full PingClient.\n return wasmSealLinkingTicket(msg.ticket, msg.newDevicePub);\n case \"openLinkingTicket\":\n // Same — runs without an initialised client, which is the receiving device's\n // pre-bootstrap state (it has the X25519 ephemeral key but no identity yet).\n return wasmOpenLinkingTicket(msg.sealed, msg.newDevicePriv);\n case \"hpkeSeal\":\n // Generic HPKE seal for app-layer envelopes (e.g. linking auth envelope).\n return wasmHpkeSeal(msg.plaintext, msg.recipientPub, msg.info);\n case \"hpkeOpen\":\n return wasmHpkeOpen(msg.sealed, msg.recipientPriv, msg.info);\n case \"exportConversationStateSnapshot\":\n // [CR-7] state snapshot bytes are a secret (past epoch keys); the main thread\n // is responsible for clearing the typed array when done.\n return ensureClient().exportConversationStateSnapshot(msg.conv, msg.nowMs);\n case \"importStateSnapshot\":\n // [CR-7] returns the imported conversation id as a Uint8Array.\n return await ensureClient().importStateSnapshot(msg.snapshotBytes, msg.nowMs);\n // Recovery — all pure WASM; no client state required.\n case \"generateMnemonicPhrase\":\n return wasmGenerateMnemonicPhrase();\n case \"normalizeMnemonicPhrase\":\n return wasmNormalizeMnemonicPhrase(msg.phrase);\n case \"encryptBackup\":\n return wasmEncryptBackup(\n msg.mnemonicPhrase, msg.accountId, msg.identityExport,\n msg.deviceGroupSnapshot,\n // Map idiomatic camelCase → the snake_case keys serde expects on the\n // WASM boundary (matches the decrypt view's account_id / group_state_bytes).\n msg.conversationSnapshots.map((c) => ({\n conversation_id: c.conversationId,\n group_state_bytes: c.groupStateBytes,\n })),\n msg.createdAtMs,\n );\n case \"decryptBackup\":\n return wasmDecryptBackup(msg.blob, msg.mnemonicPhrase);\n case \"sealHistoryBundle\":\n return wasmSealHistoryBundle(msg.bundle, msg.recipientPub, msg.senderIdentitySecret);\n case \"openHistoryBundle\":\n return wasmOpenHistoryBundle(msg.sealed, msg.recipientPriv, msg.senderIdentityPub);\n default:\n throw new Error(`unknown request kind: ${(msg as { kind: string }).kind}`);\n }\n}\n\nfunction ensureClient(): PingClient {\n if (!client) throw new Error(\"client not initialised\");\n return client;\n}\n"],"mappings":"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","names":["init","wasmDecodeCatchupSnapshot","wasmSealLinkingTicket","wasmOpenLinkingTicket","wasmHpkeSeal","wasmHpkeOpen","wasmGenerateMnemonicPhrase","wasmNormalizeMnemonicPhrase","wasmEncryptBackup","wasmDecryptBackup","wasmSealHistoryBundle","wasmOpenHistoryBundle"]}
1
+ {"version":3,"sources":["../src/worker.ts"],"sourcesContent":["// Web Worker entry. Owns the WASM `PingClient` instance. Receives Requests from the main thread\n// and posts Responses back. Storage / Transport calls back to the main thread are tracked by id.\n//\n// The WASM module is loaded lazily on the first `init` so that ephemeral identity-only spawns\n// (used by `MessagingClient.generateIdentity()`) don't pay the cost.\n\n/// <reference lib=\"webworker\" />\n\nimport init, {\n PingClient,\n sealLinkingTicket as wasmSealLinkingTicket,\n openLinkingTicket as wasmOpenLinkingTicket,\n hpkeSeal as wasmHpkeSeal,\n hpkeOpen as wasmHpkeOpen,\n decodeCatchupSnapshot as wasmDecodeCatchupSnapshot,\n generateMnemonicPhrase as wasmGenerateMnemonicPhrase,\n normalizeMnemonicPhrase as wasmNormalizeMnemonicPhrase,\n encryptBackup as wasmEncryptBackup,\n decryptBackup as wasmDecryptBackup,\n sealHistoryBundle as wasmSealHistoryBundle,\n openHistoryBundle as wasmOpenHistoryBundle,\n} from \"../wasm/ping_wasm\";\nimport type { Request, Response } from \"./protocol\";\nimport type { MessageEnvelope } from \"./types\";\n\ndeclare const self: DedicatedWorkerGlobalScope;\n\nlet client: PingClient | null = null;\nlet wasmReady: Promise<void> | null = null;\n\nconst pendingStorage = new Map<number, (r: { ok: boolean; value: unknown }) => void>();\nconst pendingTransport = new Map<number, (r: { ok: boolean; value: unknown }) => void>();\nlet nextStorageId = 1;\nlet nextTransportId = 1;\n\nconst proxyStorage = {\n get: (namespace: string, key: string) => callMain<Uint8Array | null>(\"storage\", \"get\", [namespace, key]),\n put: (namespace: string, key: string, value: Uint8Array) =>\n callMain<void>(\"storage\", \"put\", [namespace, key, value]),\n del: (namespace: string, key: string) => callMain<void>(\"storage\", \"del\", [namespace, key]),\n listKeys: (namespace: string, prefix: string) =>\n callMain<string[]>(\"storage\", \"listKeys\", [namespace, prefix]),\n};\nconst proxyTransport = {\n send: (envelope: MessageEnvelope) => callMain<void>(\"transport\", \"send\", [envelope]),\n fetchSince: (conv: Uint8Array, cursor: Uint8Array, limit: number) =>\n callMain<MessageEnvelope[]>(\"transport\", \"fetchSince\", [conv, cursor, limit]),\n discoverDevices: (userId: Uint8Array) => callMain<unknown[]>(\"transport\", \"discoverDevices\", [userId]),\n // Optional on the host (see `Transport.setNextWelcomeRecipients` in types.ts).\n // The main-thread dispatch (index.ts `handleWorker → case \"transport.call\"`)\n // resolves `undefined` to a `transport.reply { ok: true, value: undefined }`\n // when the host hasn't implemented the method, which we treat as a no-op.\n setNextWelcomeRecipients: (conv: Uint8Array, recipients: Uint8Array[]) =>\n callMain<void>(\"transport\", \"setNextWelcomeRecipients\", [conv, recipients]),\n};\n\nfunction callMain<T>(kind: \"storage\" | \"transport\", method: string, args: unknown[]): Promise<T> {\n return new Promise((resolve, reject) => {\n const id = kind === \"storage\" ? nextStorageId++ : nextTransportId++;\n const map = kind === \"storage\" ? pendingStorage : pendingTransport;\n map.set(id, ({ ok, value }) => ok ? resolve(value as T) : reject(new Error(String(value))));\n self.postMessage({ kind: `${kind}.call`, id, method, args } as Response);\n });\n}\n\nasync function ensureWasm() {\n if (wasmReady) return wasmReady;\n // The bundled wasm path is rewritten by the build script to be relative to `dist/`.\n wasmReady = init().then(() => undefined);\n return wasmReady;\n}\n\n// The WASM `PingClient` is SINGLE-THREADED (this one worker). Its async\n// methods hold internal locks across `.await` (e.g. a `conversations`\n// RwLock guard kept while `snapshot_to_storage` awaits IndexedDB). If we\n// let `onmessage` dispatch a SECOND top-level request while the first is\n// suspended at an await, the two WASM calls interleave on the single\n// thread, the second re-acquires a held lock, and `parking_lot`'s wasm\n// stub `panic!`s with \"Parking not supported on this platform\" — which\n// poisons the module (`RuntimeError: unreachable`) and breaks every\n// subsequent call. We therefore SERIALIZE top-level request dispatch:\n// each request runs to completion before the next begins.\n//\n// CRUCIAL: `storage.reply` / `transport.reply` are NOT serialized — an\n// in-flight request awaits those replies (via `callMain`), so queuing\n// them behind the request would deadlock. They resolve pending callbacks\n// immediately, exactly as before.\nlet dispatchChain: Promise<void> = Promise.resolve();\n\nasync function runRequest(msg: Request): Promise<void> {\n try {\n const value = await dispatch(msg);\n (self.postMessage as (m: Response) => void)({ kind: \"result\", id: msg.id, ok: true, value });\n } catch (e) {\n (self.postMessage as (m: Response) => void)({\n kind: \"result\", id: msg.id, ok: false, error: e instanceof Error ? e.message : String(e),\n });\n }\n}\n\nself.onmessage = (ev: MessageEvent<Request>) => {\n const msg = ev.data;\n\n // Storage / transport responses from the main thread — handle\n // immediately (they unblock the in-flight request). Never queued.\n if (msg.kind === \"storage.reply\") {\n const slot = pendingStorage.get(msg.id);\n if (slot) {\n pendingStorage.delete(msg.id);\n msg.ok ? slot({ ok: true, value: msg.value })\n : slot({ ok: false, value: msg.error });\n }\n return;\n }\n if (msg.kind === \"transport.reply\") {\n const slot = pendingTransport.get(msg.id);\n if (slot) {\n pendingTransport.delete(msg.id);\n msg.ok ? slot({ ok: true, value: msg.value })\n : slot({ ok: false, value: msg.error });\n }\n return;\n }\n\n // Top-level client request — chain it after the previous one so only a\n // single WASM call is ever in flight. `runRequest` never rejects (it\n // posts the error as a result), so the chain can't break.\n dispatchChain = dispatchChain.then(() => runRequest(msg));\n};\n\nasync function dispatch(msg: Request): Promise<unknown> {\n await ensureWasm();\n switch (msg.kind) {\n case \"init\": {\n client = await PingClient.init(\n msg.identityExport, msg.deviceLabel,\n proxyStorage as unknown as never,\n proxyTransport as unknown as never,\n msg.nowMs,\n // Optional Ed25519 device signing secret — see protocol.ts\n // and ClientConfig.deviceSigningSecretKey. Forwarding `null`\n // explicitly when absent so the wasm-bindgen `Option<Vec<u8>>`\n // shape is matched correctly.\n msg.deviceSigningSecretKey ?? null,\n );\n // Wire decrypted application-message callback back to the main thread.\n client.onMessage((m: unknown) => {\n (self.postMessage as (m: Response) => void)({ kind: \"event.message\", msg: m as never });\n });\n return null;\n }\n case \"generateIdentity\":\n return PingClient.generateIdentity();\n case \"userId\": return ensureClient().userId();\n case \"exportIdentity\": return ensureClient().exportIdentity();\n case \"deviceId\": return ensureClient().deviceId();\n case \"freshKeyPackage\": return ensureClient().freshKeyPackage();\n case \"createConversation\":\n return await ensureClient().createConversation(msg.name ?? undefined, msg.nowMs);\n case \"joinConversation\": {\n const id = await ensureClient().joinConversation(msg.welcome, msg.nowMs) as Uint8Array;\n // Notify the main thread that we just entered a new conversation. We forward the\n // Welcome's `sender_device` so the host can label \"joined chat with <inviter>\".\n (self.postMessage as (m: Response) => void)({\n kind: \"event.conversation\",\n id,\n epoch: 0,\n sender: msg.welcome.sender_device,\n });\n return id;\n }\n case \"listConversations\":\n return ensureClient().listConversations();\n case \"members\":\n return ensureClient().members(msg.conv);\n case \"send\":\n return await ensureClient().send(msg.conv, msg.plaintext, msg.nowMs);\n case \"addMembers\":\n // wasm-bindgen takes the entries as a plain array of {deviceId, keyPackage};\n // serde-wasm-bindgen on the Rust side deserialises into Vec<JsAddMemberEntry>.\n return await ensureClient().addMembers(msg.conv, msg.entries, msg.nowMs);\n case \"setConversationName\":\n // GroupContextExtensions commit carrying the new conversation name — the\n // rename / avatar-id change propagates to every member via group state.\n return await ensureClient().setConversationName(msg.conv, msg.name ?? undefined, msg.nowMs);\n case \"admitDeviceToChats\":\n // Per-chat add for a newly-linked device. Returns Vec<JsAdmitChatOutcome>\n // — the host inspects per-entry status to decide whether to retry.\n return await ensureClient().admitDeviceToChats(msg.deviceId, msg.entries, msg.nowMs);\n case \"removeMembers\":\n // wasm-bindgen exposes Vec<u32> as Uint32Array; convert from the protocol's number[].\n return await ensureClient().removeMembers(msg.conv, new Uint32Array(msg.leaves), msg.nowMs);\n case \"revokeDevice\":\n // [CR-2] returns the array of Commit envelopes the SDK has already broadcast.\n return await ensureClient().revokeDevice(msg.deviceId, msg.nowMs);\n case \"processEnvelope\": {\n const result = await ensureClient().processEnvelope(msg.envelope, msg.nowMs);\n (self.postMessage as (m: Response) => void)({\n kind: \"event.conversation\",\n id: msg.envelope.conversation_id,\n epoch: msg.envelope.epoch,\n });\n return result;\n }\n case \"syncConversations\":\n return await ensureClient().syncConversations(msg.nowMs);\n case \"buildLinkingTicket\":\n return await ensureClient().buildLinkingTicket(\n msg.newDeviceId, msg.newDeviceKp, msg.lastAppEvents, msg.nowMs,\n );\n case \"consumeLinkingTicket\":\n await ensureClient().consumeLinkingTicket(msg.ticket, msg.nowMs);\n return null;\n case \"decodeCatchupSnapshot\":\n // Pure WASM — no client state required. The new device may call this in an\n // ephemeral worker before init() completes.\n return wasmDecodeCatchupSnapshot(msg.snapshotBytes);\n case \"exportConversationSecret\":\n // wasm-bindgen returns a Uint8Array sized to `length`. The bytes are a secret —\n // the main thread is responsible for clearing the typed array when done.\n return ensureClient().exportConversationSecret(\n msg.conv, msg.label, msg.context, msg.length,\n );\n case \"sealLinkingTicket\":\n // Pure WASM call — no client state required. Lets a sender seal a ticket from an\n // ephemeral worker without standing up the full PingClient.\n return wasmSealLinkingTicket(msg.ticket, msg.newDevicePub);\n case \"openLinkingTicket\":\n // Same — runs without an initialised client, which is the receiving device's\n // pre-bootstrap state (it has the X25519 ephemeral key but no identity yet).\n return wasmOpenLinkingTicket(msg.sealed, msg.newDevicePriv);\n case \"hpkeSeal\":\n // Generic HPKE seal for app-layer envelopes (e.g. linking auth envelope).\n return wasmHpkeSeal(msg.plaintext, msg.recipientPub, msg.info);\n case \"hpkeOpen\":\n return wasmHpkeOpen(msg.sealed, msg.recipientPriv, msg.info);\n case \"exportConversationStateSnapshot\":\n // [CR-7] state snapshot bytes are a secret (past epoch keys); the main thread\n // is responsible for clearing the typed array when done.\n return ensureClient().exportConversationStateSnapshot(msg.conv, msg.nowMs);\n case \"importStateSnapshot\":\n // [CR-7] returns the imported conversation id as a Uint8Array.\n return await ensureClient().importStateSnapshot(msg.snapshotBytes, msg.nowMs);\n // Recovery — all pure WASM; no client state required.\n case \"generateMnemonicPhrase\":\n return wasmGenerateMnemonicPhrase();\n case \"normalizeMnemonicPhrase\":\n return wasmNormalizeMnemonicPhrase(msg.phrase);\n case \"encryptBackup\":\n return wasmEncryptBackup(\n msg.mnemonicPhrase, msg.accountId, msg.identityExport,\n msg.deviceGroupSnapshot,\n // Map idiomatic camelCase → the snake_case keys serde expects on the\n // WASM boundary (matches the decrypt view's account_id / group_state_bytes).\n msg.conversationSnapshots.map((c) => ({\n conversation_id: c.conversationId,\n group_state_bytes: c.groupStateBytes,\n })),\n msg.createdAtMs,\n );\n case \"decryptBackup\":\n return wasmDecryptBackup(msg.blob, msg.mnemonicPhrase);\n case \"sealHistoryBundle\":\n return wasmSealHistoryBundle(msg.bundle, msg.recipientPub, msg.senderIdentitySecret);\n case \"openHistoryBundle\":\n return wasmOpenHistoryBundle(msg.sealed, msg.recipientPriv, msg.senderIdentityPub);\n default:\n throw new Error(`unknown request kind: ${(msg as { kind: string }).kind}`);\n }\n}\n\nfunction ensureClient(): PingClient {\n if (!client) throw new Error(\"client not initialised\");\n return client;\n}\n"],"mappings":"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","names":["init","wasmDecodeCatchupSnapshot","wasmSealLinkingTicket","wasmOpenLinkingTicket","wasmHpkeSeal","wasmHpkeOpen","wasmGenerateMnemonicPhrase","wasmNormalizeMnemonicPhrase","wasmEncryptBackup","wasmDecryptBackup","wasmSealHistoryBundle","wasmOpenHistoryBundle"]}
package/dist/worker.js CHANGED
@@ -133,6 +133,8 @@ async function dispatch(msg) {
133
133
  return await ensureClient().send(msg.conv, msg.plaintext, msg.nowMs);
134
134
  case "addMembers":
135
135
  return await ensureClient().addMembers(msg.conv, msg.entries, msg.nowMs);
136
+ case "setConversationName":
137
+ return await ensureClient().setConversationName(msg.conv, msg.name ?? void 0, msg.nowMs);
136
138
  case "admitDeviceToChats":
137
139
  return await ensureClient().admitDeviceToChats(msg.deviceId, msg.entries, msg.nowMs);
138
140
  case "removeMembers":
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/worker.ts"],"sourcesContent":["// Web Worker entry. Owns the WASM `PingClient` instance. Receives Requests from the main thread\n// and posts Responses back. Storage / Transport calls back to the main thread are tracked by id.\n//\n// The WASM module is loaded lazily on the first `init` so that ephemeral identity-only spawns\n// (used by `MessagingClient.generateIdentity()`) don't pay the cost.\n\n/// <reference lib=\"webworker\" />\n\nimport init, {\n PingClient,\n sealLinkingTicket as wasmSealLinkingTicket,\n openLinkingTicket as wasmOpenLinkingTicket,\n hpkeSeal as wasmHpkeSeal,\n hpkeOpen as wasmHpkeOpen,\n decodeCatchupSnapshot as wasmDecodeCatchupSnapshot,\n generateMnemonicPhrase as wasmGenerateMnemonicPhrase,\n normalizeMnemonicPhrase as wasmNormalizeMnemonicPhrase,\n encryptBackup as wasmEncryptBackup,\n decryptBackup as wasmDecryptBackup,\n sealHistoryBundle as wasmSealHistoryBundle,\n openHistoryBundle as wasmOpenHistoryBundle,\n} from \"../wasm/ping_wasm\";\nimport type { Request, Response } from \"./protocol\";\nimport type { MessageEnvelope } from \"./types\";\n\ndeclare const self: DedicatedWorkerGlobalScope;\n\nlet client: PingClient | null = null;\nlet wasmReady: Promise<void> | null = null;\n\nconst pendingStorage = new Map<number, (r: { ok: boolean; value: unknown }) => void>();\nconst pendingTransport = new Map<number, (r: { ok: boolean; value: unknown }) => void>();\nlet nextStorageId = 1;\nlet nextTransportId = 1;\n\nconst proxyStorage = {\n get: (namespace: string, key: string) => callMain<Uint8Array | null>(\"storage\", \"get\", [namespace, key]),\n put: (namespace: string, key: string, value: Uint8Array) =>\n callMain<void>(\"storage\", \"put\", [namespace, key, value]),\n del: (namespace: string, key: string) => callMain<void>(\"storage\", \"del\", [namespace, key]),\n listKeys: (namespace: string, prefix: string) =>\n callMain<string[]>(\"storage\", \"listKeys\", [namespace, prefix]),\n};\nconst proxyTransport = {\n send: (envelope: MessageEnvelope) => callMain<void>(\"transport\", \"send\", [envelope]),\n fetchSince: (conv: Uint8Array, cursor: Uint8Array, limit: number) =>\n callMain<MessageEnvelope[]>(\"transport\", \"fetchSince\", [conv, cursor, limit]),\n discoverDevices: (userId: Uint8Array) => callMain<unknown[]>(\"transport\", \"discoverDevices\", [userId]),\n // Optional on the host (see `Transport.setNextWelcomeRecipients` in types.ts).\n // The main-thread dispatch (index.ts `handleWorker → case \"transport.call\"`)\n // resolves `undefined` to a `transport.reply { ok: true, value: undefined }`\n // when the host hasn't implemented the method, which we treat as a no-op.\n setNextWelcomeRecipients: (conv: Uint8Array, recipients: Uint8Array[]) =>\n callMain<void>(\"transport\", \"setNextWelcomeRecipients\", [conv, recipients]),\n};\n\nfunction callMain<T>(kind: \"storage\" | \"transport\", method: string, args: unknown[]): Promise<T> {\n return new Promise((resolve, reject) => {\n const id = kind === \"storage\" ? nextStorageId++ : nextTransportId++;\n const map = kind === \"storage\" ? pendingStorage : pendingTransport;\n map.set(id, ({ ok, value }) => ok ? resolve(value as T) : reject(new Error(String(value))));\n self.postMessage({ kind: `${kind}.call`, id, method, args } as Response);\n });\n}\n\nasync function ensureWasm() {\n if (wasmReady) return wasmReady;\n // The bundled wasm path is rewritten by the build script to be relative to `dist/`.\n wasmReady = init().then(() => undefined);\n return wasmReady;\n}\n\n// The WASM `PingClient` is SINGLE-THREADED (this one worker). Its async\n// methods hold internal locks across `.await` (e.g. a `conversations`\n// RwLock guard kept while `snapshot_to_storage` awaits IndexedDB). If we\n// let `onmessage` dispatch a SECOND top-level request while the first is\n// suspended at an await, the two WASM calls interleave on the single\n// thread, the second re-acquires a held lock, and `parking_lot`'s wasm\n// stub `panic!`s with \"Parking not supported on this platform\" — which\n// poisons the module (`RuntimeError: unreachable`) and breaks every\n// subsequent call. We therefore SERIALIZE top-level request dispatch:\n// each request runs to completion before the next begins.\n//\n// CRUCIAL: `storage.reply` / `transport.reply` are NOT serialized — an\n// in-flight request awaits those replies (via `callMain`), so queuing\n// them behind the request would deadlock. They resolve pending callbacks\n// immediately, exactly as before.\nlet dispatchChain: Promise<void> = Promise.resolve();\n\nasync function runRequest(msg: Request): Promise<void> {\n try {\n const value = await dispatch(msg);\n (self.postMessage as (m: Response) => void)({ kind: \"result\", id: msg.id, ok: true, value });\n } catch (e) {\n (self.postMessage as (m: Response) => void)({\n kind: \"result\", id: msg.id, ok: false, error: e instanceof Error ? e.message : String(e),\n });\n }\n}\n\nself.onmessage = (ev: MessageEvent<Request>) => {\n const msg = ev.data;\n\n // Storage / transport responses from the main thread — handle\n // immediately (they unblock the in-flight request). Never queued.\n if (msg.kind === \"storage.reply\") {\n const slot = pendingStorage.get(msg.id);\n if (slot) {\n pendingStorage.delete(msg.id);\n msg.ok ? slot({ ok: true, value: msg.value })\n : slot({ ok: false, value: msg.error });\n }\n return;\n }\n if (msg.kind === \"transport.reply\") {\n const slot = pendingTransport.get(msg.id);\n if (slot) {\n pendingTransport.delete(msg.id);\n msg.ok ? slot({ ok: true, value: msg.value })\n : slot({ ok: false, value: msg.error });\n }\n return;\n }\n\n // Top-level client request — chain it after the previous one so only a\n // single WASM call is ever in flight. `runRequest` never rejects (it\n // posts the error as a result), so the chain can't break.\n dispatchChain = dispatchChain.then(() => runRequest(msg));\n};\n\nasync function dispatch(msg: Request): Promise<unknown> {\n await ensureWasm();\n switch (msg.kind) {\n case \"init\": {\n client = await PingClient.init(\n msg.identityExport, msg.deviceLabel,\n proxyStorage as unknown as never,\n proxyTransport as unknown as never,\n msg.nowMs,\n // Optional Ed25519 device signing secret — see protocol.ts\n // and ClientConfig.deviceSigningSecretKey. Forwarding `null`\n // explicitly when absent so the wasm-bindgen `Option<Vec<u8>>`\n // shape is matched correctly.\n msg.deviceSigningSecretKey ?? null,\n );\n // Wire decrypted application-message callback back to the main thread.\n client.onMessage((m: unknown) => {\n (self.postMessage as (m: Response) => void)({ kind: \"event.message\", msg: m as never });\n });\n return null;\n }\n case \"generateIdentity\":\n return PingClient.generateIdentity();\n case \"userId\": return ensureClient().userId();\n case \"exportIdentity\": return ensureClient().exportIdentity();\n case \"deviceId\": return ensureClient().deviceId();\n case \"freshKeyPackage\": return ensureClient().freshKeyPackage();\n case \"createConversation\":\n return await ensureClient().createConversation(msg.name ?? undefined, msg.nowMs);\n case \"joinConversation\": {\n const id = await ensureClient().joinConversation(msg.welcome, msg.nowMs) as Uint8Array;\n // Notify the main thread that we just entered a new conversation. We forward the\n // Welcome's `sender_device` so the host can label \"joined chat with <inviter>\".\n (self.postMessage as (m: Response) => void)({\n kind: \"event.conversation\",\n id,\n epoch: 0,\n sender: msg.welcome.sender_device,\n });\n return id;\n }\n case \"listConversations\":\n return ensureClient().listConversations();\n case \"members\":\n return ensureClient().members(msg.conv);\n case \"send\":\n return await ensureClient().send(msg.conv, msg.plaintext, msg.nowMs);\n case \"addMembers\":\n // wasm-bindgen takes the entries as a plain array of {deviceId, keyPackage};\n // serde-wasm-bindgen on the Rust side deserialises into Vec<JsAddMemberEntry>.\n return await ensureClient().addMembers(msg.conv, msg.entries, msg.nowMs);\n case \"admitDeviceToChats\":\n // Per-chat add for a newly-linked device. Returns Vec<JsAdmitChatOutcome>\n // — the host inspects per-entry status to decide whether to retry.\n return await ensureClient().admitDeviceToChats(msg.deviceId, msg.entries, msg.nowMs);\n case \"removeMembers\":\n // wasm-bindgen exposes Vec<u32> as Uint32Array; convert from the protocol's number[].\n return await ensureClient().removeMembers(msg.conv, new Uint32Array(msg.leaves), msg.nowMs);\n case \"revokeDevice\":\n // [CR-2] returns the array of Commit envelopes the SDK has already broadcast.\n return await ensureClient().revokeDevice(msg.deviceId, msg.nowMs);\n case \"processEnvelope\": {\n const result = await ensureClient().processEnvelope(msg.envelope, msg.nowMs);\n (self.postMessage as (m: Response) => void)({\n kind: \"event.conversation\",\n id: msg.envelope.conversation_id,\n epoch: msg.envelope.epoch,\n });\n return result;\n }\n case \"syncConversations\":\n return await ensureClient().syncConversations(msg.nowMs);\n case \"buildLinkingTicket\":\n return await ensureClient().buildLinkingTicket(\n msg.newDeviceId, msg.newDeviceKp, msg.lastAppEvents, msg.nowMs,\n );\n case \"consumeLinkingTicket\":\n await ensureClient().consumeLinkingTicket(msg.ticket, msg.nowMs);\n return null;\n case \"decodeCatchupSnapshot\":\n // Pure WASM — no client state required. The new device may call this in an\n // ephemeral worker before init() completes.\n return wasmDecodeCatchupSnapshot(msg.snapshotBytes);\n case \"exportConversationSecret\":\n // wasm-bindgen returns a Uint8Array sized to `length`. The bytes are a secret —\n // the main thread is responsible for clearing the typed array when done.\n return ensureClient().exportConversationSecret(\n msg.conv, msg.label, msg.context, msg.length,\n );\n case \"sealLinkingTicket\":\n // Pure WASM call — no client state required. Lets a sender seal a ticket from an\n // ephemeral worker without standing up the full PingClient.\n return wasmSealLinkingTicket(msg.ticket, msg.newDevicePub);\n case \"openLinkingTicket\":\n // Same — runs without an initialised client, which is the receiving device's\n // pre-bootstrap state (it has the X25519 ephemeral key but no identity yet).\n return wasmOpenLinkingTicket(msg.sealed, msg.newDevicePriv);\n case \"hpkeSeal\":\n // Generic HPKE seal for app-layer envelopes (e.g. linking auth envelope).\n return wasmHpkeSeal(msg.plaintext, msg.recipientPub, msg.info);\n case \"hpkeOpen\":\n return wasmHpkeOpen(msg.sealed, msg.recipientPriv, msg.info);\n case \"exportConversationStateSnapshot\":\n // [CR-7] state snapshot bytes are a secret (past epoch keys); the main thread\n // is responsible for clearing the typed array when done.\n return ensureClient().exportConversationStateSnapshot(msg.conv, msg.nowMs);\n case \"importStateSnapshot\":\n // [CR-7] returns the imported conversation id as a Uint8Array.\n return await ensureClient().importStateSnapshot(msg.snapshotBytes, msg.nowMs);\n // Recovery — all pure WASM; no client state required.\n case \"generateMnemonicPhrase\":\n return wasmGenerateMnemonicPhrase();\n case \"normalizeMnemonicPhrase\":\n return wasmNormalizeMnemonicPhrase(msg.phrase);\n case \"encryptBackup\":\n return wasmEncryptBackup(\n msg.mnemonicPhrase, msg.accountId, msg.identityExport,\n msg.deviceGroupSnapshot,\n // Map idiomatic camelCase → the snake_case keys serde expects on the\n // WASM boundary (matches the decrypt view's account_id / group_state_bytes).\n msg.conversationSnapshots.map((c) => ({\n conversation_id: c.conversationId,\n group_state_bytes: c.groupStateBytes,\n })),\n msg.createdAtMs,\n );\n case \"decryptBackup\":\n return wasmDecryptBackup(msg.blob, msg.mnemonicPhrase);\n case \"sealHistoryBundle\":\n return wasmSealHistoryBundle(msg.bundle, msg.recipientPub, msg.senderIdentitySecret);\n case \"openHistoryBundle\":\n return wasmOpenHistoryBundle(msg.sealed, msg.recipientPriv, msg.senderIdentityPub);\n default:\n throw new Error(`unknown request kind: ${(msg as { kind: string }).kind}`);\n }\n}\n\nfunction ensureClient(): PingClient {\n if (!client) throw new Error(\"client not initialised\");\n return client;\n}\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../src/worker.ts"],"sourcesContent":["// Web Worker entry. Owns the WASM `PingClient` instance. Receives Requests from the main thread\n// and posts Responses back. Storage / Transport calls back to the main thread are tracked by id.\n//\n// The WASM module is loaded lazily on the first `init` so that ephemeral identity-only spawns\n// (used by `MessagingClient.generateIdentity()`) don't pay the cost.\n\n/// <reference lib=\"webworker\" />\n\nimport init, {\n PingClient,\n sealLinkingTicket as wasmSealLinkingTicket,\n openLinkingTicket as wasmOpenLinkingTicket,\n hpkeSeal as wasmHpkeSeal,\n hpkeOpen as wasmHpkeOpen,\n decodeCatchupSnapshot as wasmDecodeCatchupSnapshot,\n generateMnemonicPhrase as wasmGenerateMnemonicPhrase,\n normalizeMnemonicPhrase as wasmNormalizeMnemonicPhrase,\n encryptBackup as wasmEncryptBackup,\n decryptBackup as wasmDecryptBackup,\n sealHistoryBundle as wasmSealHistoryBundle,\n openHistoryBundle as wasmOpenHistoryBundle,\n} from \"../wasm/ping_wasm\";\nimport type { Request, Response } from \"./protocol\";\nimport type { MessageEnvelope } from \"./types\";\n\ndeclare const self: DedicatedWorkerGlobalScope;\n\nlet client: PingClient | null = null;\nlet wasmReady: Promise<void> | null = null;\n\nconst pendingStorage = new Map<number, (r: { ok: boolean; value: unknown }) => void>();\nconst pendingTransport = new Map<number, (r: { ok: boolean; value: unknown }) => void>();\nlet nextStorageId = 1;\nlet nextTransportId = 1;\n\nconst proxyStorage = {\n get: (namespace: string, key: string) => callMain<Uint8Array | null>(\"storage\", \"get\", [namespace, key]),\n put: (namespace: string, key: string, value: Uint8Array) =>\n callMain<void>(\"storage\", \"put\", [namespace, key, value]),\n del: (namespace: string, key: string) => callMain<void>(\"storage\", \"del\", [namespace, key]),\n listKeys: (namespace: string, prefix: string) =>\n callMain<string[]>(\"storage\", \"listKeys\", [namespace, prefix]),\n};\nconst proxyTransport = {\n send: (envelope: MessageEnvelope) => callMain<void>(\"transport\", \"send\", [envelope]),\n fetchSince: (conv: Uint8Array, cursor: Uint8Array, limit: number) =>\n callMain<MessageEnvelope[]>(\"transport\", \"fetchSince\", [conv, cursor, limit]),\n discoverDevices: (userId: Uint8Array) => callMain<unknown[]>(\"transport\", \"discoverDevices\", [userId]),\n // Optional on the host (see `Transport.setNextWelcomeRecipients` in types.ts).\n // The main-thread dispatch (index.ts `handleWorker → case \"transport.call\"`)\n // resolves `undefined` to a `transport.reply { ok: true, value: undefined }`\n // when the host hasn't implemented the method, which we treat as a no-op.\n setNextWelcomeRecipients: (conv: Uint8Array, recipients: Uint8Array[]) =>\n callMain<void>(\"transport\", \"setNextWelcomeRecipients\", [conv, recipients]),\n};\n\nfunction callMain<T>(kind: \"storage\" | \"transport\", method: string, args: unknown[]): Promise<T> {\n return new Promise((resolve, reject) => {\n const id = kind === \"storage\" ? nextStorageId++ : nextTransportId++;\n const map = kind === \"storage\" ? pendingStorage : pendingTransport;\n map.set(id, ({ ok, value }) => ok ? resolve(value as T) : reject(new Error(String(value))));\n self.postMessage({ kind: `${kind}.call`, id, method, args } as Response);\n });\n}\n\nasync function ensureWasm() {\n if (wasmReady) return wasmReady;\n // The bundled wasm path is rewritten by the build script to be relative to `dist/`.\n wasmReady = init().then(() => undefined);\n return wasmReady;\n}\n\n// The WASM `PingClient` is SINGLE-THREADED (this one worker). Its async\n// methods hold internal locks across `.await` (e.g. a `conversations`\n// RwLock guard kept while `snapshot_to_storage` awaits IndexedDB). If we\n// let `onmessage` dispatch a SECOND top-level request while the first is\n// suspended at an await, the two WASM calls interleave on the single\n// thread, the second re-acquires a held lock, and `parking_lot`'s wasm\n// stub `panic!`s with \"Parking not supported on this platform\" — which\n// poisons the module (`RuntimeError: unreachable`) and breaks every\n// subsequent call. We therefore SERIALIZE top-level request dispatch:\n// each request runs to completion before the next begins.\n//\n// CRUCIAL: `storage.reply` / `transport.reply` are NOT serialized — an\n// in-flight request awaits those replies (via `callMain`), so queuing\n// them behind the request would deadlock. They resolve pending callbacks\n// immediately, exactly as before.\nlet dispatchChain: Promise<void> = Promise.resolve();\n\nasync function runRequest(msg: Request): Promise<void> {\n try {\n const value = await dispatch(msg);\n (self.postMessage as (m: Response) => void)({ kind: \"result\", id: msg.id, ok: true, value });\n } catch (e) {\n (self.postMessage as (m: Response) => void)({\n kind: \"result\", id: msg.id, ok: false, error: e instanceof Error ? e.message : String(e),\n });\n }\n}\n\nself.onmessage = (ev: MessageEvent<Request>) => {\n const msg = ev.data;\n\n // Storage / transport responses from the main thread — handle\n // immediately (they unblock the in-flight request). Never queued.\n if (msg.kind === \"storage.reply\") {\n const slot = pendingStorage.get(msg.id);\n if (slot) {\n pendingStorage.delete(msg.id);\n msg.ok ? slot({ ok: true, value: msg.value })\n : slot({ ok: false, value: msg.error });\n }\n return;\n }\n if (msg.kind === \"transport.reply\") {\n const slot = pendingTransport.get(msg.id);\n if (slot) {\n pendingTransport.delete(msg.id);\n msg.ok ? slot({ ok: true, value: msg.value })\n : slot({ ok: false, value: msg.error });\n }\n return;\n }\n\n // Top-level client request — chain it after the previous one so only a\n // single WASM call is ever in flight. `runRequest` never rejects (it\n // posts the error as a result), so the chain can't break.\n dispatchChain = dispatchChain.then(() => runRequest(msg));\n};\n\nasync function dispatch(msg: Request): Promise<unknown> {\n await ensureWasm();\n switch (msg.kind) {\n case \"init\": {\n client = await PingClient.init(\n msg.identityExport, msg.deviceLabel,\n proxyStorage as unknown as never,\n proxyTransport as unknown as never,\n msg.nowMs,\n // Optional Ed25519 device signing secret — see protocol.ts\n // and ClientConfig.deviceSigningSecretKey. Forwarding `null`\n // explicitly when absent so the wasm-bindgen `Option<Vec<u8>>`\n // shape is matched correctly.\n msg.deviceSigningSecretKey ?? null,\n );\n // Wire decrypted application-message callback back to the main thread.\n client.onMessage((m: unknown) => {\n (self.postMessage as (m: Response) => void)({ kind: \"event.message\", msg: m as never });\n });\n return null;\n }\n case \"generateIdentity\":\n return PingClient.generateIdentity();\n case \"userId\": return ensureClient().userId();\n case \"exportIdentity\": return ensureClient().exportIdentity();\n case \"deviceId\": return ensureClient().deviceId();\n case \"freshKeyPackage\": return ensureClient().freshKeyPackage();\n case \"createConversation\":\n return await ensureClient().createConversation(msg.name ?? undefined, msg.nowMs);\n case \"joinConversation\": {\n const id = await ensureClient().joinConversation(msg.welcome, msg.nowMs) as Uint8Array;\n // Notify the main thread that we just entered a new conversation. We forward the\n // Welcome's `sender_device` so the host can label \"joined chat with <inviter>\".\n (self.postMessage as (m: Response) => void)({\n kind: \"event.conversation\",\n id,\n epoch: 0,\n sender: msg.welcome.sender_device,\n });\n return id;\n }\n case \"listConversations\":\n return ensureClient().listConversations();\n case \"members\":\n return ensureClient().members(msg.conv);\n case \"send\":\n return await ensureClient().send(msg.conv, msg.plaintext, msg.nowMs);\n case \"addMembers\":\n // wasm-bindgen takes the entries as a plain array of {deviceId, keyPackage};\n // serde-wasm-bindgen on the Rust side deserialises into Vec<JsAddMemberEntry>.\n return await ensureClient().addMembers(msg.conv, msg.entries, msg.nowMs);\n case \"setConversationName\":\n // GroupContextExtensions commit carrying the new conversation name — the\n // rename / avatar-id change propagates to every member via group state.\n return await ensureClient().setConversationName(msg.conv, msg.name ?? undefined, msg.nowMs);\n case \"admitDeviceToChats\":\n // Per-chat add for a newly-linked device. Returns Vec<JsAdmitChatOutcome>\n // — the host inspects per-entry status to decide whether to retry.\n return await ensureClient().admitDeviceToChats(msg.deviceId, msg.entries, msg.nowMs);\n case \"removeMembers\":\n // wasm-bindgen exposes Vec<u32> as Uint32Array; convert from the protocol's number[].\n return await ensureClient().removeMembers(msg.conv, new Uint32Array(msg.leaves), msg.nowMs);\n case \"revokeDevice\":\n // [CR-2] returns the array of Commit envelopes the SDK has already broadcast.\n return await ensureClient().revokeDevice(msg.deviceId, msg.nowMs);\n case \"processEnvelope\": {\n const result = await ensureClient().processEnvelope(msg.envelope, msg.nowMs);\n (self.postMessage as (m: Response) => void)({\n kind: \"event.conversation\",\n id: msg.envelope.conversation_id,\n epoch: msg.envelope.epoch,\n });\n return result;\n }\n case \"syncConversations\":\n return await ensureClient().syncConversations(msg.nowMs);\n case \"buildLinkingTicket\":\n return await ensureClient().buildLinkingTicket(\n msg.newDeviceId, msg.newDeviceKp, msg.lastAppEvents, msg.nowMs,\n );\n case \"consumeLinkingTicket\":\n await ensureClient().consumeLinkingTicket(msg.ticket, msg.nowMs);\n return null;\n case \"decodeCatchupSnapshot\":\n // Pure WASM — no client state required. The new device may call this in an\n // ephemeral worker before init() completes.\n return wasmDecodeCatchupSnapshot(msg.snapshotBytes);\n case \"exportConversationSecret\":\n // wasm-bindgen returns a Uint8Array sized to `length`. The bytes are a secret —\n // the main thread is responsible for clearing the typed array when done.\n return ensureClient().exportConversationSecret(\n msg.conv, msg.label, msg.context, msg.length,\n );\n case \"sealLinkingTicket\":\n // Pure WASM call — no client state required. Lets a sender seal a ticket from an\n // ephemeral worker without standing up the full PingClient.\n return wasmSealLinkingTicket(msg.ticket, msg.newDevicePub);\n case \"openLinkingTicket\":\n // Same — runs without an initialised client, which is the receiving device's\n // pre-bootstrap state (it has the X25519 ephemeral key but no identity yet).\n return wasmOpenLinkingTicket(msg.sealed, msg.newDevicePriv);\n case \"hpkeSeal\":\n // Generic HPKE seal for app-layer envelopes (e.g. linking auth envelope).\n return wasmHpkeSeal(msg.plaintext, msg.recipientPub, msg.info);\n case \"hpkeOpen\":\n return wasmHpkeOpen(msg.sealed, msg.recipientPriv, msg.info);\n case \"exportConversationStateSnapshot\":\n // [CR-7] state snapshot bytes are a secret (past epoch keys); the main thread\n // is responsible for clearing the typed array when done.\n return ensureClient().exportConversationStateSnapshot(msg.conv, msg.nowMs);\n case \"importStateSnapshot\":\n // [CR-7] returns the imported conversation id as a Uint8Array.\n return await ensureClient().importStateSnapshot(msg.snapshotBytes, msg.nowMs);\n // Recovery — all pure WASM; no client state required.\n case \"generateMnemonicPhrase\":\n return wasmGenerateMnemonicPhrase();\n case \"normalizeMnemonicPhrase\":\n return wasmNormalizeMnemonicPhrase(msg.phrase);\n case \"encryptBackup\":\n return wasmEncryptBackup(\n msg.mnemonicPhrase, msg.accountId, msg.identityExport,\n msg.deviceGroupSnapshot,\n // Map idiomatic camelCase → the snake_case keys serde expects on the\n // WASM boundary (matches the decrypt view's account_id / group_state_bytes).\n msg.conversationSnapshots.map((c) => ({\n conversation_id: c.conversationId,\n group_state_bytes: c.groupStateBytes,\n })),\n msg.createdAtMs,\n );\n case \"decryptBackup\":\n return wasmDecryptBackup(msg.blob, msg.mnemonicPhrase);\n case \"sealHistoryBundle\":\n return wasmSealHistoryBundle(msg.bundle, msg.recipientPub, msg.senderIdentitySecret);\n case \"openHistoryBundle\":\n return wasmOpenHistoryBundle(msg.sealed, msg.recipientPriv, msg.senderIdentityPub);\n default:\n throw new Error(`unknown request kind: ${(msg as { kind: string }).kind}`);\n }\n}\n\nfunction ensureClient(): PingClient {\n if (!client) throw new Error(\"client not initialised\");\n return client;\n}\n"],"mappings":"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","names":[]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ping-openmls-sdk",
3
- "version": "0.7.4",
3
+ "version": "0.7.6",
4
4
  "description": "OpenMLS-based secure messaging SDK — runs in browsers, React Native, RN-Web, and Electron/Tauri.",
5
5
  "license": "Apache-2.0",
6
6
  "type": "module",
package/wasm/package.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "name": "ping-openmls-sdk-wasm",
3
3
  "type": "module",
4
4
  "description": "wasm-bindgen surface for the Ping OpenMLS SDK — drives the ping-openmls-sdk npm package",
5
- "version": "0.6.5",
5
+ "version": "0.6.8",
6
6
  "license": "Apache-2.0",
7
7
  "files": [
8
8
  "ping_wasm_bg.wasm",
@@ -100,6 +100,15 @@ export class PingClient {
100
100
  */
101
101
  revokeDevice(device_id: Uint8Array, now_ms: number): Promise<any>;
102
102
  send(conv_id: Uint8Array, plaintext: Uint8Array, now_ms: number): Promise<any>;
103
+ /**
104
+ * Change a conversation's `name` (carried in the GroupContext) via an MLS
105
+ * GroupContextExtensions Commit, broadcasting it to every member through
106
+ * group STATE. Hosts use this to make a rename or an embedded
107
+ * avatar-media-id change bulletproof (the `name` carries the
108
+ * `ping:meta:v1:` blob). No Welcome. `name` of `null`/`undefined` clears it.
109
+ * All members must have re-linked since the group-name capability shipped.
110
+ */
111
+ setConversationName(conv_id: Uint8Array, name: string | null | undefined, now_ms: number): Promise<void>;
103
112
  syncConversations(now_ms: number): Promise<any>;
104
113
  userId(): Uint8Array;
105
114
  }
@@ -305,6 +314,7 @@ export interface InitOutput {
305
314
  readonly pingclient_removeMembers: (a: number, b: number, c: number, d: number, e: number, f: number) => number;
306
315
  readonly pingclient_revokeDevice: (a: number, b: number, c: number, d: number) => number;
307
316
  readonly pingclient_send: (a: number, b: number, c: number, d: number, e: number, f: number) => number;
317
+ readonly pingclient_setConversationName: (a: number, b: number, c: number, d: number, e: number, f: number) => number;
308
318
  readonly pingclient_syncConversations: (a: number, b: number) => number;
309
319
  readonly pingclient_userId: (a: number, b: number) => void;
310
320
  readonly sealHistoryBundle: (a: number, b: number, c: number, d: number, e: number, f: number) => void;
@@ -325,9 +335,9 @@ export interface InitOutput {
325
335
  readonly wasmbindgentestcontext_include_ignored: (a: number, b: number) => void;
326
336
  readonly wasmbindgentestcontext_new: (a: number) => number;
327
337
  readonly wasmbindgentestcontext_run: (a: number, b: number, c: number) => number;
328
- readonly __wasm_bindgen_func_elem_2878: (a: number, b: number, c: number, d: number, e: number) => void;
329
- readonly __wasm_bindgen_func_elem_2864: (a: number, b: number, c: number, d: number) => void;
330
- readonly __wasm_bindgen_func_elem_2888: (a: number, b: number, c: number, d: number) => void;
338
+ readonly __wasm_bindgen_func_elem_2901: (a: number, b: number, c: number, d: number, e: number) => void;
339
+ readonly __wasm_bindgen_func_elem_2887: (a: number, b: number, c: number, d: number) => void;
340
+ readonly __wasm_bindgen_func_elem_2911: (a: number, b: number, c: number, d: number) => void;
331
341
  readonly __wbindgen_export: (a: number, b: number) => number;
332
342
  readonly __wbindgen_export2: (a: number, b: number, c: number, d: number) => number;
333
343
  readonly __wbindgen_export3: (a: number) => void;
package/wasm/ping_wasm.js CHANGED
@@ -365,6 +365,26 @@ export class PingClient {
365
365
  const ret = wasm.pingclient_send(this.__wbg_ptr, ptr0, len0, ptr1, len1, now_ms);
366
366
  return takeObject(ret);
367
367
  }
368
+ /**
369
+ * Change a conversation's `name` (carried in the GroupContext) via an MLS
370
+ * GroupContextExtensions Commit, broadcasting it to every member through
371
+ * group STATE. Hosts use this to make a rename or an embedded
372
+ * avatar-media-id change bulletproof (the `name` carries the
373
+ * `ping:meta:v1:` blob). No Welcome. `name` of `null`/`undefined` clears it.
374
+ * All members must have re-linked since the group-name capability shipped.
375
+ * @param {Uint8Array} conv_id
376
+ * @param {string | null | undefined} name
377
+ * @param {number} now_ms
378
+ * @returns {Promise<void>}
379
+ */
380
+ setConversationName(conv_id, name, now_ms) {
381
+ const ptr0 = passArray8ToWasm0(conv_id, wasm.__wbindgen_export);
382
+ const len0 = WASM_VECTOR_LEN;
383
+ var ptr1 = isLikeNone(name) ? 0 : passStringToWasm0(name, wasm.__wbindgen_export, wasm.__wbindgen_export2);
384
+ var len1 = WASM_VECTOR_LEN;
385
+ const ret = wasm.pingclient_setConversationName(this.__wbg_ptr, ptr0, len0, ptr1, len1, now_ms);
386
+ return takeObject(ret);
387
+ }
368
388
  /**
369
389
  * @param {number} now_ms
370
390
  * @returns {Promise<any>}
@@ -1079,11 +1099,11 @@ function __wbg_get_imports() {
1079
1099
  const ret = getObject(arg0).crypto;
1080
1100
  return addHeapObject(ret);
1081
1101
  },
1082
- __wbg_del_fb742226355f9100: function(arg0, arg1, arg2, arg3, arg4) {
1102
+ __wbg_del_13a0f2ceb092e774: function(arg0, arg1, arg2, arg3, arg4) {
1083
1103
  const ret = getObject(arg0).del(getStringFromWasm0(arg1, arg2), getStringFromWasm0(arg3, arg4));
1084
1104
  return addHeapObject(ret);
1085
1105
  },
1086
- __wbg_discoverDevices_6db91bc14572310c: function(arg0, arg1) {
1106
+ __wbg_discoverDevices_818dad92d58ab3bc: function(arg0, arg1) {
1087
1107
  const ret = getObject(arg0).discoverDevices(takeObject(arg1));
1088
1108
  return addHeapObject(ret);
1089
1109
  },
@@ -1109,7 +1129,7 @@ function __wbg_get_imports() {
1109
1129
  wasm.__wbindgen_export4(deferred0_0, deferred0_1, 1);
1110
1130
  }
1111
1131
  },
1112
- __wbg_fetchSince_6a39eedf22532dcd: function(arg0, arg1, arg2, arg3) {
1132
+ __wbg_fetchSince_bcaeaffd51dc8590: function(arg0, arg1, arg2, arg3) {
1113
1133
  const ret = getObject(arg0).fetchSince(takeObject(arg1), takeObject(arg2), arg3 >>> 0);
1114
1134
  return addHeapObject(ret);
1115
1135
  },
@@ -1120,7 +1140,7 @@ function __wbg_get_imports() {
1120
1140
  const a = state0.a;
1121
1141
  state0.a = 0;
1122
1142
  try {
1123
- return __wasm_bindgen_func_elem_2878(a, state0.b, arg0, arg1, arg2);
1143
+ return __wasm_bindgen_func_elem_2901(a, state0.b, arg0, arg1, arg2);
1124
1144
  } finally {
1125
1145
  state0.a = a;
1126
1146
  }
@@ -1144,12 +1164,12 @@ function __wbg_get_imports() {
1144
1164
  __wbg_getRandomValues_c44a50d8cfdaebeb: function() { return handleError(function (arg0, arg1) {
1145
1165
  getObject(arg0).getRandomValues(getObject(arg1));
1146
1166
  }, arguments); },
1147
- __wbg_get_652f640b3b0b6e3e: function(arg0, arg1) {
1148
- const ret = getObject(arg0)[arg1 >>> 0];
1167
+ __wbg_get_55a2a9b891c0ea19: function(arg0, arg1, arg2, arg3, arg4) {
1168
+ const ret = getObject(arg0).get(getStringFromWasm0(arg1, arg2), getStringFromWasm0(arg3, arg4));
1149
1169
  return addHeapObject(ret);
1150
1170
  },
1151
- __wbg_get_7bec9f999084c299: function(arg0, arg1, arg2, arg3, arg4) {
1152
- const ret = getObject(arg0).get(getStringFromWasm0(arg1, arg2), getStringFromWasm0(arg3, arg4));
1171
+ __wbg_get_652f640b3b0b6e3e: function(arg0, arg1) {
1172
+ const ret = getObject(arg0)[arg1 >>> 0];
1153
1173
  return addHeapObject(ret);
1154
1174
  },
1155
1175
  __wbg_get_9cfea9b7bbf12a15: function() { return handleError(function (arg0, arg1) {
@@ -1204,7 +1224,7 @@ function __wbg_get_imports() {
1204
1224
  const ret = getObject(arg0).length;
1205
1225
  return ret;
1206
1226
  },
1207
- __wbg_listKeys_f36aa1afd2513d32: function(arg0, arg1, arg2, arg3, arg4) {
1227
+ __wbg_listKeys_b905d90e3311e88c: function(arg0, arg1, arg2, arg3, arg4) {
1208
1228
  const ret = getObject(arg0).listKeys(getStringFromWasm0(arg1, arg2), getStringFromWasm0(arg3, arg4));
1209
1229
  return addHeapObject(ret);
1210
1230
  },
@@ -1261,7 +1281,7 @@ function __wbg_get_imports() {
1261
1281
  const a = state0.a;
1262
1282
  state0.a = 0;
1263
1283
  try {
1264
- return __wasm_bindgen_func_elem_2888(a, state0.b, arg0, arg1);
1284
+ return __wasm_bindgen_func_elem_2911(a, state0.b, arg0, arg1);
1265
1285
  } finally {
1266
1286
  state0.a = a;
1267
1287
  }
@@ -1311,7 +1331,7 @@ function __wbg_get_imports() {
1311
1331
  __wbg_prototypesetcall_fd4050e806e1d519: function(arg0, arg1, arg2) {
1312
1332
  Uint8Array.prototype.set.call(getArrayU8FromWasm0(arg0, arg1), getObject(arg2));
1313
1333
  },
1314
- __wbg_put_289abc8b71e26347: function(arg0, arg1, arg2, arg3, arg4, arg5) {
1334
+ __wbg_put_9ede268afa93a0e7: function(arg0, arg1, arg2, arg3, arg4, arg5) {
1315
1335
  const ret = getObject(arg0).put(getStringFromWasm0(arg1, arg2), getStringFromWasm0(arg3, arg4), takeObject(arg5));
1316
1336
  return addHeapObject(ret);
1317
1337
  },
@@ -1337,11 +1357,11 @@ function __wbg_get_imports() {
1337
1357
  const ret = getObject(arg0).self;
1338
1358
  return isLikeNone(ret) ? 0 : addHeapObject(ret);
1339
1359
  },
1340
- __wbg_send_7aee7e3e869b2498: function(arg0, arg1) {
1360
+ __wbg_send_5279803fc0a94e10: function(arg0, arg1) {
1341
1361
  const ret = getObject(arg0).send(takeObject(arg1));
1342
1362
  return addHeapObject(ret);
1343
1363
  },
1344
- __wbg_setNextWelcomeRecipients_d196713f242b4749: function(arg0, arg1, arg2) {
1364
+ __wbg_setNextWelcomeRecipients_7cd9cda6ec266446: function(arg0, arg1, arg2) {
1345
1365
  const ret = getObject(arg0).setNextWelcomeRecipients(takeObject(arg1), takeObject(arg2));
1346
1366
  return addHeapObject(ret);
1347
1367
  },
@@ -1438,8 +1458,8 @@ function __wbg_get_imports() {
1438
1458
  return addHeapObject(ret);
1439
1459
  },
1440
1460
  __wbindgen_cast_0000000000000001: function(arg0, arg1) {
1441
- // Cast intrinsic for `Closure(Closure { owned: true, function: Function { arguments: [Externref], shim_idx: 781, ret: Result(Unit), inner_ret: Some(Result(Unit)) }, mutable: true }) -> Externref`.
1442
- const ret = makeMutClosure(arg0, arg1, __wasm_bindgen_func_elem_2864);
1461
+ // Cast intrinsic for `Closure(Closure { owned: true, function: Function { arguments: [Externref], shim_idx: 788, ret: Result(Unit), inner_ret: Some(Result(Unit)) }, mutable: true }) -> Externref`.
1462
+ const ret = makeMutClosure(arg0, arg1, __wasm_bindgen_func_elem_2887);
1443
1463
  return addHeapObject(ret);
1444
1464
  },
1445
1465
  __wbindgen_cast_0000000000000002: function(arg0) {
@@ -1483,10 +1503,10 @@ function __wbg_get_imports() {
1483
1503
  };
1484
1504
  }
1485
1505
 
1486
- function __wasm_bindgen_func_elem_2864(arg0, arg1, arg2) {
1506
+ function __wasm_bindgen_func_elem_2887(arg0, arg1, arg2) {
1487
1507
  try {
1488
1508
  const retptr = wasm.__wbindgen_add_to_stack_pointer(-16);
1489
- wasm.__wasm_bindgen_func_elem_2864(retptr, arg0, arg1, addHeapObject(arg2));
1509
+ wasm.__wasm_bindgen_func_elem_2887(retptr, arg0, arg1, addHeapObject(arg2));
1490
1510
  var r0 = getDataViewMemory0().getInt32(retptr + 4 * 0, true);
1491
1511
  var r1 = getDataViewMemory0().getInt32(retptr + 4 * 1, true);
1492
1512
  if (r1) {
@@ -1497,12 +1517,12 @@ function __wasm_bindgen_func_elem_2864(arg0, arg1, arg2) {
1497
1517
  }
1498
1518
  }
1499
1519
 
1500
- function __wasm_bindgen_func_elem_2888(arg0, arg1, arg2, arg3) {
1501
- wasm.__wasm_bindgen_func_elem_2888(arg0, arg1, addHeapObject(arg2), addHeapObject(arg3));
1520
+ function __wasm_bindgen_func_elem_2911(arg0, arg1, arg2, arg3) {
1521
+ wasm.__wasm_bindgen_func_elem_2911(arg0, arg1, addHeapObject(arg2), addHeapObject(arg3));
1502
1522
  }
1503
1523
 
1504
- function __wasm_bindgen_func_elem_2878(arg0, arg1, arg2, arg3, arg4) {
1505
- wasm.__wasm_bindgen_func_elem_2878(arg0, arg1, addHeapObject(arg2), arg3, addHeapObject(arg4));
1524
+ function __wasm_bindgen_func_elem_2901(arg0, arg1, arg2, arg3, arg4) {
1525
+ wasm.__wasm_bindgen_func_elem_2901(arg0, arg1, addHeapObject(arg2), arg3, addHeapObject(arg4));
1506
1526
  }
1507
1527
 
1508
1528
  const WasmBindgenTestContextFinalization = (typeof FinalizationRegistry === 'undefined')
Binary file
@@ -32,6 +32,7 @@ export const pingclient_processEnvelope: (a: number, b: number, c: number) => nu
32
32
  export const pingclient_removeMembers: (a: number, b: number, c: number, d: number, e: number, f: number) => number;
33
33
  export const pingclient_revokeDevice: (a: number, b: number, c: number, d: number) => number;
34
34
  export const pingclient_send: (a: number, b: number, c: number, d: number, e: number, f: number) => number;
35
+ export const pingclient_setConversationName: (a: number, b: number, c: number, d: number, e: number, f: number) => number;
35
36
  export const pingclient_syncConversations: (a: number, b: number) => number;
36
37
  export const pingclient_userId: (a: number, b: number) => void;
37
38
  export const sealHistoryBundle: (a: number, b: number, c: number, d: number, e: number, f: number) => void;
@@ -52,9 +53,9 @@ export const wasmbindgentestcontext_filtered_count: (a: number, b: number) => vo
52
53
  export const wasmbindgentestcontext_include_ignored: (a: number, b: number) => void;
53
54
  export const wasmbindgentestcontext_new: (a: number) => number;
54
55
  export const wasmbindgentestcontext_run: (a: number, b: number, c: number) => number;
55
- export const __wasm_bindgen_func_elem_2878: (a: number, b: number, c: number, d: number, e: number) => void;
56
- export const __wasm_bindgen_func_elem_2864: (a: number, b: number, c: number, d: number) => void;
57
- export const __wasm_bindgen_func_elem_2888: (a: number, b: number, c: number, d: number) => void;
56
+ export const __wasm_bindgen_func_elem_2901: (a: number, b: number, c: number, d: number, e: number) => void;
57
+ export const __wasm_bindgen_func_elem_2887: (a: number, b: number, c: number, d: number) => void;
58
+ export const __wasm_bindgen_func_elem_2911: (a: number, b: number, c: number, d: number) => void;
58
59
  export const __wbindgen_export: (a: number, b: number) => number;
59
60
  export const __wbindgen_export2: (a: number, b: number, c: number, d: number) => number;
60
61
  export const __wbindgen_export3: (a: number) => void;