ping-openmls-sdk 0.0.1

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/README.md ADDED
@@ -0,0 +1,63 @@
1
+ # ping-openmls-sdk
2
+
3
+ OpenMLS-based secure messaging for browsers, React Native, RN-Web, and Electron/Tauri.
4
+ Crypto runs in a Web Worker so the UI is never blocked.
5
+
6
+ ## Install
7
+
8
+ ```bash
9
+ npm install ping-openmls-sdk
10
+ ```
11
+
12
+ ## Usage
13
+
14
+ ```typescript
15
+ import { MessagingClient } from "ping-openmls-sdk";
16
+ import { IndexedDbStorage } from "ping-openmls-sdk/storage/indexeddb";
17
+ import { WebSocketTransport } from "ping-openmls-sdk/transport/websocket";
18
+
19
+ // 1. Identity — generate once per user, then persist (encrypted) by your auth flow.
20
+ const identityExport = await MessagingClient.generateIdentity();
21
+
22
+ // 2. Plug in storage + transport.
23
+ const storage = new IndexedDbStorage();
24
+ const transport = new WebSocketTransport({ baseUrl: "https://relay.example.com" });
25
+
26
+ // 3. Init.
27
+ const client = await MessagingClient.init({
28
+ identityExport, deviceLabel: "Web", storage, transport,
29
+ });
30
+
31
+ // 4. Use.
32
+ const convo = await client.createConversation({ name: "Team" });
33
+ await convo.addMembers([bobKeyPackage]);
34
+ await convo.send(new TextEncoder().encode("hello"));
35
+
36
+ client.onMessage((msg) => {
37
+ console.log(new TextDecoder().decode(msg.plaintext));
38
+ });
39
+
40
+ // 5. Catch up after reconnect.
41
+ await client.syncConversations();
42
+ ```
43
+
44
+ ## Architecture
45
+
46
+ The SDK is a thin TypeScript shell over a Rust core compiled to WebAssembly. The Worker owns
47
+ the WASM instance; the main thread does not import WASM at all. See
48
+ [`docs/ARCHITECTURE.md`](../../docs/ARCHITECTURE.md) for details.
49
+
50
+ ## Bundle size
51
+
52
+ Aim: ≤ 350 KiB gzipped for the WASM blob, ≤ 8 KiB gzipped for the TS surface. We achieve this
53
+ by:
54
+
55
+ - Building the WASM with `--release`, `wasm-opt -O3`, and stripping symbols.
56
+ - Lazy-initializing WASM only on first `init()` (so `MessagingClient.generateIdentity()` doesn't
57
+ pay the cost on cold pages).
58
+ - No CBOR or crypto libraries on the main thread.
59
+
60
+ ## React Native
61
+
62
+ On RN, native binaries are preferred — install `@ping-openmls-sdk/react-native-macos` (autolinked). On RN-Web,
63
+ this package's WASM build is used as-is.
package/dist/index.cjs ADDED
@@ -0,0 +1,463 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+ var __copyProps = (to, from, except, desc) => {
11
+ if (from && typeof from === "object" || typeof from === "function") {
12
+ for (let key of __getOwnPropNames(from))
13
+ if (!__hasOwnProp.call(to, key) && key !== except)
14
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
+ }
16
+ return to;
17
+ };
18
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
+
20
+ // src/index.ts
21
+ var src_exports = {};
22
+ __export(src_exports, {
23
+ IndexedDbStorage: () => IndexedDbStorage,
24
+ MessagingClient: () => MessagingClient,
25
+ WebSocketTransport: () => WebSocketTransport
26
+ });
27
+ module.exports = __toCommonJS(src_exports);
28
+
29
+ // src/storage/indexeddb.ts
30
+ var DB_NAME = "ping-sdk";
31
+ var DB_VERSION = 1;
32
+ var STORE = "kv";
33
+ function open() {
34
+ return new Promise((resolve, reject) => {
35
+ const req = indexedDB.open(DB_NAME, DB_VERSION);
36
+ req.onupgradeneeded = () => {
37
+ const db = req.result;
38
+ if (!db.objectStoreNames.contains(STORE)) {
39
+ const os = db.createObjectStore(STORE, { keyPath: ["ns", "key"] });
40
+ os.createIndex("ns", "ns", { unique: false });
41
+ }
42
+ };
43
+ req.onsuccess = () => resolve(req.result);
44
+ req.onerror = () => reject(req.error);
45
+ });
46
+ }
47
+ var IndexedDbStorage = class {
48
+ dbPromise;
49
+ constructor() {
50
+ this.dbPromise = open();
51
+ }
52
+ async get(namespace, key) {
53
+ const db = await this.dbPromise;
54
+ return new Promise((resolve, reject) => {
55
+ const tx = db.transaction(STORE, "readonly");
56
+ const r = tx.objectStore(STORE).get([namespace, key]);
57
+ r.onsuccess = () => resolve(r.result?.value ?? null);
58
+ r.onerror = () => reject(r.error);
59
+ });
60
+ }
61
+ async put(namespace, key, value) {
62
+ const db = await this.dbPromise;
63
+ return new Promise((resolve, reject) => {
64
+ const tx = db.transaction(STORE, "readwrite");
65
+ tx.objectStore(STORE).put({ ns: namespace, key, value });
66
+ tx.oncomplete = () => resolve();
67
+ tx.onerror = () => reject(tx.error);
68
+ });
69
+ }
70
+ async del(namespace, key) {
71
+ const db = await this.dbPromise;
72
+ return new Promise((resolve, reject) => {
73
+ const tx = db.transaction(STORE, "readwrite");
74
+ tx.objectStore(STORE).delete([namespace, key]);
75
+ tx.oncomplete = () => resolve();
76
+ tx.onerror = () => reject(tx.error);
77
+ });
78
+ }
79
+ async listKeys(namespace, prefix) {
80
+ const db = await this.dbPromise;
81
+ return new Promise((resolve, reject) => {
82
+ const tx = db.transaction(STORE, "readonly");
83
+ const idx = tx.objectStore(STORE).index("ns");
84
+ const req = idx.openCursor(IDBKeyRange.only(namespace));
85
+ const out = [];
86
+ req.onsuccess = () => {
87
+ const cur = req.result;
88
+ if (!cur) return resolve(out);
89
+ const row = cur.value;
90
+ if (!prefix || row.key.startsWith(prefix)) out.push(row.key);
91
+ cur.continue();
92
+ };
93
+ req.onerror = () => reject(req.error);
94
+ });
95
+ }
96
+ };
97
+
98
+ // src/transport/websocket.ts
99
+ var WebSocketTransport = class {
100
+ constructor(cfg) {
101
+ this.cfg = cfg;
102
+ this.fetchImpl = cfg.fetchImpl ?? globalThis.fetch.bind(globalThis);
103
+ this.openSocket();
104
+ }
105
+ cfg;
106
+ ws = null;
107
+ liveHandlers = /* @__PURE__ */ new Set();
108
+ fetchImpl;
109
+ openSocket() {
110
+ if (typeof WebSocket === "undefined") return;
111
+ const url = `${this.cfg.baseUrl.replace(/^http/, "ws")}/stream`;
112
+ this.ws = new WebSocket(url);
113
+ this.ws.onmessage = (ev) => {
114
+ const text = typeof ev.data === "string" ? ev.data : new TextDecoder().decode(ev.data);
115
+ try {
116
+ const env = JSON.parse(text, reviver);
117
+ for (const h of this.liveHandlers) h(env);
118
+ } catch (e) {
119
+ if (typeof console !== "undefined") console.debug("[ping-sdk] WS frame parse error:", e);
120
+ }
121
+ };
122
+ this.ws.onclose = () => setTimeout(() => this.openSocket(), 1e3);
123
+ }
124
+ async send(envelope) {
125
+ const res = await this.fetchImpl(`${this.cfg.baseUrl}/send`, {
126
+ method: "POST",
127
+ headers: {
128
+ "content-type": "application/json",
129
+ ...this.cfg.authHeader ? { authorization: this.cfg.authHeader } : {}
130
+ },
131
+ body: JSON.stringify(envelope, replacer)
132
+ });
133
+ if (res.status === 409) throw new Error("EpochOccupied");
134
+ if (!res.ok) throw new Error(`transport send failed: ${res.status}`);
135
+ }
136
+ async fetchSince(conversationId, cursor, limit) {
137
+ const url = new URL(`${this.cfg.baseUrl}/sync`);
138
+ url.searchParams.set("conv", toHex(conversationId));
139
+ url.searchParams.set("cursor", btoaUrl(cursor));
140
+ url.searchParams.set("limit", String(limit));
141
+ const res = await this.fetchImpl(url.toString(), {
142
+ headers: { ...this.cfg.authHeader ? { authorization: this.cfg.authHeader } : {} }
143
+ });
144
+ if (!res.ok) throw new Error(`transport fetch failed: ${res.status}`);
145
+ const text = await res.text();
146
+ return JSON.parse(text, reviver);
147
+ }
148
+ subscribe(onEvent) {
149
+ this.liveHandlers.add(onEvent);
150
+ return { unsubscribe: () => this.liveHandlers.delete(onEvent) };
151
+ }
152
+ async discoverDevices(userId) {
153
+ const res = await this.fetchImpl(`${this.cfg.baseUrl}/devices/${toHex(userId)}`, {
154
+ headers: { ...this.cfg.authHeader ? { authorization: this.cfg.authHeader } : {} }
155
+ });
156
+ if (!res.ok) throw new Error(`device discovery failed: ${res.status}`);
157
+ const text = await res.text();
158
+ return JSON.parse(text, reviver);
159
+ }
160
+ };
161
+ function replacer(_k, v) {
162
+ if (v instanceof Uint8Array) return { _bytes: Array.from(v) };
163
+ return v;
164
+ }
165
+ function reviver(_k, v) {
166
+ if (v && typeof v === "object" && Array.isArray(v._bytes)) {
167
+ return new Uint8Array(v._bytes);
168
+ }
169
+ return v;
170
+ }
171
+ function toHex(b) {
172
+ let s = "";
173
+ for (const v of b) s += v.toString(16).padStart(2, "0");
174
+ return s;
175
+ }
176
+ function btoaUrl(b) {
177
+ let s = "";
178
+ for (const v of b) s += String.fromCharCode(v);
179
+ return btoa(s).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
180
+ }
181
+
182
+ // src/index.ts
183
+ var import_meta = {};
184
+ var MessagingClient = class _MessagingClient {
185
+ worker;
186
+ nextId = 1;
187
+ pending = /* @__PURE__ */ new Map();
188
+ storage;
189
+ transport;
190
+ now;
191
+ messageHandlers = /* @__PURE__ */ new Set();
192
+ conversationHandlers = /* @__PURE__ */ new Set();
193
+ subscription = null;
194
+ metaCache = /* @__PURE__ */ new Map();
195
+ // Cached at init() so dispatchIncoming can skip envelopes we sent ourselves — the relay
196
+ // broadcasts to all WS subscribers including the sender, and re-applying our own already-
197
+ // merged commits would error with "epoch differs".
198
+ ownDeviceId = null;
199
+ constructor(cfg, worker) {
200
+ this.worker = worker;
201
+ this.storage = cfg.storage;
202
+ this.transport = cfg.transport;
203
+ this.now = cfg.now ?? (() => Date.now());
204
+ this.worker.onmessage = (ev) => this.handleWorker(ev.data);
205
+ }
206
+ /** Generate a fresh identity. The returned bytes are a secret — store them encrypted. */
207
+ static async generateIdentity() {
208
+ const w = new Worker(new URL("./worker.js", import_meta.url), { type: "module" });
209
+ const id = 1;
210
+ return new Promise((resolve, reject) => {
211
+ w.onmessage = (ev) => {
212
+ const msg = ev.data;
213
+ if (msg.kind === "result" && msg.id === id) {
214
+ w.terminate();
215
+ msg.ok ? resolve(msg.value) : reject(new Error(msg.error));
216
+ }
217
+ };
218
+ const req = { kind: "generateIdentity", id };
219
+ w.postMessage(req);
220
+ });
221
+ }
222
+ static async init(cfg) {
223
+ const worker = new Worker(new URL("./worker.js", import_meta.url), { type: "module" });
224
+ const client = new _MessagingClient(cfg, worker);
225
+ await client.call({
226
+ kind: "init",
227
+ id: client.nextId++,
228
+ identityExport: cfg.identityExport,
229
+ deviceLabel: cfg.deviceLabel,
230
+ nowMs: client.now()
231
+ });
232
+ client.ownDeviceId = await client.deviceId();
233
+ client.subscription = cfg.transport.subscribe((envelope) => {
234
+ void client.dispatchIncoming(envelope);
235
+ });
236
+ return client;
237
+ }
238
+ /** Internal: route an inbound envelope to the right handler. */
239
+ async dispatchIncoming(envelope) {
240
+ if (this.ownDeviceId && sameBytesU8(envelope.sender_device, this.ownDeviceId)) {
241
+ return;
242
+ }
243
+ if (envelope.kind === "Welcome") {
244
+ try {
245
+ const known = await this.listConversations();
246
+ const already = known.some((m) => sameBytesU8(m.id, envelope.conversation_id));
247
+ if (already) return;
248
+ await this.joinConversation(envelope);
249
+ } catch (e) {
250
+ if (typeof console !== "undefined") console.debug("[ping-sdk] welcome ignored:", e);
251
+ }
252
+ return;
253
+ }
254
+ try {
255
+ await this.processEnvelope(envelope);
256
+ } catch (e) {
257
+ if (typeof console !== "undefined") console.debug("[ping-sdk] envelope drop:", e);
258
+ }
259
+ }
260
+ // ------------------------ Public API ------------------------
261
+ async userId() {
262
+ return await this.call({ kind: "userId", id: this.nextId++ });
263
+ }
264
+ async deviceId() {
265
+ return await this.call({ kind: "deviceId", id: this.nextId++ });
266
+ }
267
+ async freshKeyPackage() {
268
+ return await this.call({ kind: "freshKeyPackage", id: this.nextId++ });
269
+ }
270
+ async createConversation(opts = {}) {
271
+ const idBytes = await this.call({
272
+ kind: "createConversation",
273
+ id: this.nextId++,
274
+ name: opts.name ?? null,
275
+ nowMs: this.now()
276
+ });
277
+ return this.conversationProxy(idBytes);
278
+ }
279
+ async joinConversation(welcome) {
280
+ const idBytes = await this.call({
281
+ kind: "joinConversation",
282
+ id: this.nextId++,
283
+ welcome,
284
+ nowMs: this.now()
285
+ });
286
+ return this.conversationProxy(idBytes);
287
+ }
288
+ /**
289
+ * Get a `Conversation` proxy for an already-known conversation. Use this when you have a
290
+ * conversation id (e.g., from `listConversations()`) and want to send/addMembers without
291
+ * re-creating it. Throws if the id isn't known to the worker — call `listConversations()`
292
+ * first to verify membership.
293
+ */
294
+ getConversation(id) {
295
+ return this.conversationProxy(id);
296
+ }
297
+ async listConversations() {
298
+ const metas = await this.call({ kind: "listConversations", id: this.nextId++ });
299
+ this.metaCache.clear();
300
+ for (const m of metas) this.metaCache.set(hex(m.id), m);
301
+ return metas;
302
+ }
303
+ async syncConversations() {
304
+ return await this.call({
305
+ kind: "syncConversations",
306
+ id: this.nextId++,
307
+ nowMs: this.now()
308
+ });
309
+ }
310
+ async processEnvelope(envelope) {
311
+ return await this.call({
312
+ kind: "processEnvelope",
313
+ id: this.nextId++,
314
+ envelope,
315
+ nowMs: this.now()
316
+ });
317
+ }
318
+ async buildLinkingTicket(newDeviceId, newDeviceKp) {
319
+ return await this.call({
320
+ kind: "buildLinkingTicket",
321
+ id: this.nextId++,
322
+ newDeviceId,
323
+ newDeviceKp,
324
+ nowMs: this.now()
325
+ });
326
+ }
327
+ async consumeLinkingTicket(ticket) {
328
+ await this.call({
329
+ kind: "consumeLinkingTicket",
330
+ id: this.nextId++,
331
+ ticket,
332
+ nowMs: this.now()
333
+ });
334
+ }
335
+ /** Subscribe to decrypted application messages. */
336
+ onMessage(handler) {
337
+ this.messageHandlers.add(handler);
338
+ return () => this.messageHandlers.delete(handler);
339
+ }
340
+ /** Fired after every state-changing event for a conversation (Commit, member changes). */
341
+ /**
342
+ * Fires after every conversation state change — joining via Welcome, processing a Commit,
343
+ * etc. `sender` carries the device id of whoever triggered the event when known (e.g. the
344
+ * inviter's device on auto-join), so the host can label peers in its UI.
345
+ */
346
+ onConversationUpdated(handler) {
347
+ this.conversationHandlers.add(handler);
348
+ return () => this.conversationHandlers.delete(handler);
349
+ }
350
+ async close() {
351
+ this.subscription?.unsubscribe();
352
+ this.worker.terminate();
353
+ }
354
+ // ------------------------ Internals ------------------------
355
+ conversationProxy(id) {
356
+ const self = this;
357
+ return {
358
+ id,
359
+ async send(plaintext) {
360
+ return await self.call({
361
+ kind: "send",
362
+ id: self.nextId++,
363
+ conv: id,
364
+ plaintext,
365
+ nowMs: self.now()
366
+ });
367
+ },
368
+ async addMembers(kps) {
369
+ await self.call({
370
+ kind: "addMembers",
371
+ id: self.nextId++,
372
+ conv: id,
373
+ kps,
374
+ nowMs: self.now()
375
+ });
376
+ },
377
+ async removeMembers(leaves) {
378
+ await self.call({
379
+ kind: "removeMembers",
380
+ id: self.nextId++,
381
+ conv: id,
382
+ leaves,
383
+ nowMs: self.now()
384
+ });
385
+ },
386
+ meta() {
387
+ const cached = self.metaCache.get(hex(id));
388
+ if (!cached) throw new Error("conversation meta not loaded; call listConversations() first");
389
+ return cached;
390
+ }
391
+ };
392
+ }
393
+ call(req) {
394
+ return new Promise((resolve, reject) => {
395
+ this.pending.set(req.id, { resolve, reject });
396
+ this.worker.postMessage(req);
397
+ });
398
+ }
399
+ async handleWorker(msg) {
400
+ switch (msg.kind) {
401
+ case "result": {
402
+ const slot = this.pending.get(msg.id);
403
+ if (!slot) return;
404
+ this.pending.delete(msg.id);
405
+ msg.ok ? slot.resolve(msg.value) : slot.reject(new Error(msg.error));
406
+ return;
407
+ }
408
+ case "event.message": {
409
+ for (const h of this.messageHandlers) h(msg.msg);
410
+ return;
411
+ }
412
+ case "event.conversation": {
413
+ for (const h of this.conversationHandlers) h(msg.id, msg.epoch, msg.sender);
414
+ return;
415
+ }
416
+ case "storage.call": {
417
+ try {
418
+ const v = await this.storage[msg.method]?.(...msg.args);
419
+ this.worker.postMessage({ kind: "storage.reply", id: msg.id, ok: true, value: v });
420
+ } catch (e) {
421
+ this.worker.postMessage({
422
+ kind: "storage.reply",
423
+ id: msg.id,
424
+ ok: false,
425
+ error: String(e)
426
+ });
427
+ }
428
+ return;
429
+ }
430
+ case "transport.call": {
431
+ try {
432
+ const v = await this.transport[msg.method]?.(...msg.args);
433
+ this.worker.postMessage({ kind: "transport.reply", id: msg.id, ok: true, value: v });
434
+ } catch (e) {
435
+ this.worker.postMessage({
436
+ kind: "transport.reply",
437
+ id: msg.id,
438
+ ok: false,
439
+ error: String(e)
440
+ });
441
+ }
442
+ return;
443
+ }
444
+ }
445
+ }
446
+ };
447
+ function hex(b) {
448
+ let s = "";
449
+ for (const v of b) s += v.toString(16).padStart(2, "0");
450
+ return s;
451
+ }
452
+ function sameBytesU8(a, b) {
453
+ if (a.length !== b.length) return false;
454
+ for (let i = 0; i < a.length; i++) if (a[i] !== b[i]) return false;
455
+ return true;
456
+ }
457
+ // Annotate the CommonJS export names for ESM import in node:
458
+ 0 && (module.exports = {
459
+ IndexedDbStorage,
460
+ MessagingClient,
461
+ WebSocketTransport
462
+ });
463
+ //# sourceMappingURL=index.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/index.ts","../src/storage/indexeddb.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 ConversationId, ConversationMeta, DeviceId, IncomingMessage,\n LinkingTicket, MessageEnvelope, 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\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\nexport interface Conversation {\n readonly id: ConversationId;\n send(plaintext: Uint8Array): Promise<MessageEnvelope>;\n addMembers(keyPackages: Uint8Array[]): 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 // Spawn an ephemeral worker to call `generateIdentity` so we don't pull WASM into the main thread.\n const w = new Worker(new URL(\"./worker.js\", import.meta.url), { type: \"module\" });\n const id = 1;\n return new Promise<Uint8Array>((resolve, reject) => {\n w.onmessage = (ev) => {\n const msg = ev.data as Response;\n if (msg.kind === \"result\" && msg.id === id) {\n w.terminate();\n msg.ok ? resolve(msg.value as Uint8Array) : reject(new Error(msg.error));\n }\n };\n const req: Request = { kind: \"generateIdentity\", id };\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 });\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 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 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 async buildLinkingTicket(newDeviceId: DeviceId, newDeviceKp: Uint8Array): Promise<LinkingTicket> {\n return await this.call({\n kind: \"buildLinkingTicket\", id: this.nextId++,\n newDeviceId, newDeviceKp, nowMs: this.now(),\n }) as LinkingTicket;\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 /** 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(kps) {\n await self.call({\n kind: \"addMembers\", id: self.nextId++, conv: id, kps, 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","// 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\";\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 const res = await this.fetchImpl(`${this.cfg.baseUrl}/send`, {\n method: \"POST\",\n headers: {\n \"content-type\": \"application/json\",\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":[]}
@@ -0,0 +1,73 @@
1
+ import { S as Storage, T as Transport, C as ConversationId, M as MessageEnvelope, a as ConversationMeta, U as UserId, D as DeviceId, I as IncomingMessage, L as LinkingTicket } from './types-COHY5NU8.cjs';
2
+ export { B as Bytes, b as DeviceInfo, c as DiscoveredDevice, H as Hlc, d as MessageKind } from './types-COHY5NU8.cjs';
3
+ export { IndexedDbStorage } from './storage/indexeddb.cjs';
4
+ export { WebSocketTransport } from './transport/websocket.cjs';
5
+
6
+ interface ClientConfig {
7
+ identityExport: Uint8Array;
8
+ deviceLabel: string;
9
+ storage: Storage;
10
+ transport: Transport;
11
+ /** Override the wall clock (test/sim only). */
12
+ now?: () => number;
13
+ }
14
+ interface Conversation {
15
+ readonly id: ConversationId;
16
+ send(plaintext: Uint8Array): Promise<MessageEnvelope>;
17
+ addMembers(keyPackages: Uint8Array[]): Promise<void>;
18
+ removeMembers(leafIndexes: number[]): Promise<void>;
19
+ meta(): ConversationMeta;
20
+ }
21
+ declare class MessagingClient {
22
+ private worker;
23
+ private nextId;
24
+ private pending;
25
+ private storage;
26
+ private transport;
27
+ private now;
28
+ private messageHandlers;
29
+ private conversationHandlers;
30
+ private subscription;
31
+ private metaCache;
32
+ private ownDeviceId;
33
+ private constructor();
34
+ /** Generate a fresh identity. The returned bytes are a secret — store them encrypted. */
35
+ static generateIdentity(): Promise<Uint8Array>;
36
+ static init(cfg: ClientConfig): Promise<MessagingClient>;
37
+ /** Internal: route an inbound envelope to the right handler. */
38
+ private dispatchIncoming;
39
+ userId(): Promise<UserId>;
40
+ deviceId(): Promise<DeviceId>;
41
+ freshKeyPackage(): Promise<Uint8Array>;
42
+ createConversation(opts?: {
43
+ name?: string;
44
+ }): Promise<Conversation>;
45
+ joinConversation(welcome: MessageEnvelope): Promise<Conversation>;
46
+ /**
47
+ * Get a `Conversation` proxy for an already-known conversation. Use this when you have a
48
+ * conversation id (e.g., from `listConversations()`) and want to send/addMembers without
49
+ * re-creating it. Throws if the id isn't known to the worker — call `listConversations()`
50
+ * first to verify membership.
51
+ */
52
+ getConversation(id: ConversationId): Conversation;
53
+ listConversations(): Promise<ConversationMeta[]>;
54
+ syncConversations(): Promise<IncomingMessage[]>;
55
+ processEnvelope(envelope: MessageEnvelope): Promise<IncomingMessage | null>;
56
+ buildLinkingTicket(newDeviceId: DeviceId, newDeviceKp: Uint8Array): Promise<LinkingTicket>;
57
+ consumeLinkingTicket(ticket: LinkingTicket): Promise<void>;
58
+ /** Subscribe to decrypted application messages. */
59
+ onMessage(handler: (m: IncomingMessage) => void): () => void;
60
+ /** Fired after every state-changing event for a conversation (Commit, member changes). */
61
+ /**
62
+ * Fires after every conversation state change — joining via Welcome, processing a Commit,
63
+ * etc. `sender` carries the device id of whoever triggered the event when known (e.g. the
64
+ * inviter's device on auto-join), so the host can label peers in its UI.
65
+ */
66
+ onConversationUpdated(handler: (id: ConversationId, epoch: number, sender?: DeviceId) => void): () => void;
67
+ close(): Promise<void>;
68
+ private conversationProxy;
69
+ private call;
70
+ private handleWorker;
71
+ }
72
+
73
+ export { type ClientConfig, type Conversation, ConversationId, ConversationMeta, DeviceId, IncomingMessage, LinkingTicket, MessageEnvelope, MessagingClient, Storage, Transport, UserId };