ping-openmls-sdk 0.1.1 → 0.2.0

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.js CHANGED
@@ -68,9 +68,12 @@ var IndexedDbStorage = class {
68
68
  };
69
69
 
70
70
  // src/wire-contract.ts
71
- var PING_WIRE_CONTRACT_VERSION = 1;
72
- var CONTENT_TYPE_CBOR = "application/vnd.ping.wire.v1+cbor";
73
- var CONTENT_TYPE_JSON = "application/vnd.ping.wire.v1+json";
71
+ var PING_WIRE_CONTRACT_VERSION = 2;
72
+ var WIRE_VERSION_MIN_ACCEPTED = 1;
73
+ var CONTENT_TYPE_CBOR = "application/vnd.ping.wire.v2+cbor";
74
+ var CONTENT_TYPE_JSON = "application/vnd.ping.wire.v2+json";
75
+ var CONTENT_TYPE_CBOR_LEGACY_V1 = "application/vnd.ping.wire.v1+cbor";
76
+ var CONTENT_TYPE_JSON_LEGACY_V1 = "application/vnd.ping.wire.v1+json";
74
77
  var PING_WIRE_CONTRACT_HEADER = "X-Ping-Wire-Contract";
75
78
  var PING_WIRE_CONTRACT_HEADER_VALUE = `ping/${PING_WIRE_CONTRACT_VERSION}`;
76
79
  var ValidationError = class extends Error {
@@ -99,10 +102,10 @@ function bytesEqual(a, b) {
99
102
  return true;
100
103
  }
101
104
  async function validate(env) {
102
- if (env.v !== PING_WIRE_CONTRACT_VERSION) {
105
+ if (env.v < WIRE_VERSION_MIN_ACCEPTED || env.v > PING_WIRE_CONTRACT_VERSION) {
103
106
  throw new ValidationError(
104
107
  "UnsupportedVersion",
105
- `unsupported wire-contract version: envelope says v=${env.v}, expected v=${PING_WIRE_CONTRACT_VERSION}`
108
+ `unsupported wire-contract version: envelope says v=${env.v}, expected v\u2208[${WIRE_VERSION_MIN_ACCEPTED}, ${PING_WIRE_CONTRACT_VERSION}]`
106
109
  );
107
110
  }
108
111
  if (env.conversation_id.length !== 16) {
@@ -117,6 +120,9 @@ async function validate(env) {
117
120
  `sender_device must be 32 bytes, got ${env.sender_device.length}`
118
121
  );
119
122
  }
123
+ if (env.v >= 2 && env.kind === "Application") {
124
+ return;
125
+ }
120
126
  const buf = new Uint8Array(1 + env.payload.length);
121
127
  buf[0] = KIND_DISCRIMINANT[env.kind];
122
128
  buf.set(env.payload, 1);
@@ -128,10 +134,20 @@ async function validate(env) {
128
134
  );
129
135
  }
130
136
  }
137
+ async function verifyApplicationContentHash(env, plaintext) {
138
+ if (env.v < 2 || env.kind !== "Application") return;
139
+ const computed = await sha256(plaintext);
140
+ if (!bytesEqual(computed, env.content_hash)) {
141
+ throw new ValidationError(
142
+ "ContentHashMismatch",
143
+ "v=2 application content_hash mismatch against decrypted plaintext"
144
+ );
145
+ }
146
+ }
131
147
  function negotiates(contentType) {
132
148
  if (!contentType) return false;
133
149
  const primary = contentType.split(";")[0]?.trim().toLowerCase() ?? "";
134
- return primary === CONTENT_TYPE_CBOR || primary === CONTENT_TYPE_JSON;
150
+ return primary === CONTENT_TYPE_CBOR || primary === CONTENT_TYPE_JSON || primary === CONTENT_TYPE_CBOR_LEGACY_V1 || primary === CONTENT_TYPE_JSON_LEGACY_V1;
135
151
  }
136
152
 
137
153
  // src/transport/websocket.ts
@@ -244,17 +260,67 @@ var MessagingClient = class _MessagingClient {
244
260
  }
245
261
  /** Generate a fresh identity. The returned bytes are a secret — store them encrypted. */
246
262
  static async generateIdentity() {
263
+ return _MessagingClient.ephemeralWorkerCall({
264
+ kind: "generateIdentity",
265
+ id: 1
266
+ });
267
+ }
268
+ /**
269
+ * HPKE-seal a `LinkingTicket` for delivery to a new device ([CR-3]).
270
+ *
271
+ * Returns CBOR-encoded bytes safe to relay through an untrusted inbox. The new device's
272
+ * X25519 public key (`newDevicePub`) is exchanged out-of-band — typically as part of a
273
+ * QR-encoded handshake on the linking screen. Pure function; runs in an ephemeral
274
+ * worker so no live `MessagingClient` is required on the sender side.
275
+ *
276
+ * Suite: `DHKEM(X25519, HKDF-SHA256), HKDF-SHA256, AES-128-GCM` (matches MLS).
277
+ */
278
+ static async sealLinkingTicket(ticket, newDevicePub) {
279
+ if (newDevicePub.length !== 32) {
280
+ throw new Error("sealLinkingTicket: newDevicePub must be 32 bytes");
281
+ }
282
+ return _MessagingClient.ephemeralWorkerCall({
283
+ kind: "sealLinkingTicket",
284
+ id: 1,
285
+ ticket,
286
+ newDevicePub
287
+ });
288
+ }
289
+ /**
290
+ * HPKE-open a sealed `LinkingTicket` on the new device ([CR-3]).
291
+ *
292
+ * The receiving device hasn't booted a `MessagingClient` yet — it only has its X25519
293
+ * ephemeral keypair. This static method runs the open in an ephemeral worker, returning
294
+ * the cleartext ticket which the host then feeds to `init()` + `consumeLinkingTicket()`.
295
+ *
296
+ * On failure the error message is deliberately generic (no discriminator between "wrong
297
+ * key" vs "tampered ciphertext") so a probing attacker can't tell the new device's key
298
+ * was the issue.
299
+ */
300
+ static async openLinkingTicket(sealed, newDevicePriv) {
301
+ if (newDevicePriv.length !== 32) {
302
+ throw new Error("openLinkingTicket: newDevicePriv must be 32 bytes");
303
+ }
304
+ return _MessagingClient.ephemeralWorkerCall({
305
+ kind: "openLinkingTicket",
306
+ id: 1,
307
+ sealed,
308
+ newDevicePriv
309
+ });
310
+ }
311
+ // Spawn-call-terminate helper for stateless WASM functions exposed at module scope (no
312
+ // PingClient required). Each call gets a fresh worker; the cost is acceptable because
313
+ // these are one-shot, off-the-hot-path operations (identity gen, linking ticket seal/open).
314
+ static ephemeralWorkerCall(req) {
247
315
  const w = new Worker(new URL("./worker.js", import.meta.url), { type: "module" });
248
- const id = 1;
249
316
  return new Promise((resolve, reject) => {
250
317
  w.onmessage = (ev) => {
251
318
  const msg = ev.data;
252
- if (msg.kind === "result" && msg.id === id) {
319
+ if (msg.kind === "result" && msg.id === req.id) {
253
320
  w.terminate();
254
321
  msg.ok ? resolve(msg.value) : reject(new Error(msg.error));
255
322
  }
256
323
  };
257
- const req = { kind: "generateIdentity", id };
258
324
  w.postMessage(req);
259
325
  });
260
326
  }
@@ -354,15 +420,45 @@ var MessagingClient = class _MessagingClient {
354
420
  nowMs: this.now()
355
421
  });
356
422
  }
357
- async buildLinkingTicket(newDeviceId, newDeviceKp) {
423
+ /**
424
+ * Build a `LinkingTicket` for a new device.
425
+ *
426
+ * [CR-13] `lastAppEvents` is host-supplied per-conversation "what you missed" data
427
+ * (decrypted AppEvent bytes the new device will render before sync catches up).
428
+ * Pass `[]` to suppress catchup data — the new device will see an empty conversation
429
+ * list until normal sync runs.
430
+ *
431
+ * Caller is responsible for HPKE-sealing the returned ticket via
432
+ * `MessagingClient.sealLinkingTicket` before transmitting (CR-3).
433
+ */
434
+ async buildLinkingTicket(newDeviceId, newDeviceKp, lastAppEvents = []) {
358
435
  return await this.call({
359
436
  kind: "buildLinkingTicket",
360
437
  id: this.nextId++,
361
438
  newDeviceId,
362
439
  newDeviceKp,
440
+ lastAppEvents,
363
441
  nowMs: this.now()
364
442
  });
365
443
  }
444
+ /**
445
+ * Decode a `LinkingTicket.catchup_snapshot` blob ([CR-13]).
446
+ *
447
+ * Pure WASM — runs in an ephemeral worker so the new device can call this before
448
+ * `MessagingClient.init()` completes (just like `openLinkingTicket`). Returns the
449
+ * structured view: per-conversation metas + last-known AppEvent bytes.
450
+ *
451
+ * Throws on malformed or oversized inputs. Empty bytes return `null` — that's the
452
+ * "no catchup data" case (sender passed `[]` to buildLinkingTicket).
453
+ */
454
+ static async decodeCatchupSnapshot(snapshotBytes) {
455
+ if (snapshotBytes.length === 0) return null;
456
+ return _MessagingClient.ephemeralWorkerCall({
457
+ kind: "decodeCatchupSnapshot",
458
+ id: 1,
459
+ snapshotBytes
460
+ });
461
+ }
366
462
  async consumeLinkingTicket(ticket) {
367
463
  await this.call({
368
464
  kind: "consumeLinkingTicket",
@@ -371,6 +467,90 @@ var MessagingClient = class _MessagingClient {
371
467
  nowMs: this.now()
372
468
  });
373
469
  }
470
+ /**
471
+ * Revoke a device ([CR-2]).
472
+ *
473
+ * Removes `deviceId`'s leaf from every conversation where this client locally
474
+ * tracked the device→leaf mapping (i.e. conversations where this client itself
475
+ * admitted the device via `addMembers`, or where this client is the device being
476
+ * revoked). The SDK has already broadcast each Commit envelope via
477
+ * `transport.send`; the returned array is for any additional host-side handling
478
+ * (audit logs, UI). An empty array is a valid outcome — the device wasn't locally
479
+ * known anywhere.
480
+ *
481
+ * **Scope note.** Conversations where a peer admitted the device on this client's
482
+ * behalf are silently skipped — the leaf index isn't locally known. Host can fall
483
+ * back to `transport.discoverDevices` + a manual `remove_members(leafIndex)` for
484
+ * those, or wait for the affected user to revoke from a device that did the admit.
485
+ * See `docs/architecture/multi-device.md §Device removal`.
486
+ */
487
+ async revokeDevice(deviceId) {
488
+ return await this.call({
489
+ kind: "revokeDevice",
490
+ id: this.nextId++,
491
+ deviceId,
492
+ nowMs: this.now()
493
+ });
494
+ }
495
+ /**
496
+ * Export a portable MLS state snapshot for one conversation ([CR-7]).
497
+ *
498
+ * Returned bytes can be embedded in a recovery blob or shipped through a
499
+ * linking ticket so another device of the same user identity can re-attach to
500
+ * the group via [[importStateSnapshot]]. The bytes contain past epoch secrets;
501
+ * never log, never persist unencrypted, wipe (`.fill(0)`) after use.
502
+ */
503
+ async exportConversationStateSnapshot(conv) {
504
+ return await this.call({
505
+ kind: "exportConversationStateSnapshot",
506
+ id: this.nextId++,
507
+ conv,
508
+ nowMs: this.now()
509
+ });
510
+ }
511
+ /**
512
+ * Import a `GroupStateSnapshot` from another device of the same user identity
513
+ * ([CR-7]).
514
+ *
515
+ * On success, the conversation appears in [[listConversations]] and decryption
516
+ * of subsequent peer-originated traffic works against the imported state. For
517
+ * full multi-device participation, the recovered device SHOULD then issue an
518
+ * MLS Update Proposal to rotate onto a fresh leaf (post-v1 follow-up — see
519
+ * `docs/design/CR4_CR7_PERSISTENCE.md` open question #3).
520
+ */
521
+ async importStateSnapshot(snapshotBytes) {
522
+ return await this.call({
523
+ kind: "importStateSnapshot",
524
+ id: this.nextId++,
525
+ snapshotBytes,
526
+ nowMs: this.now()
527
+ });
528
+ }
529
+ /**
530
+ * Export a derived secret from a conversation's MLS exporter ([CR-8]).
531
+ *
532
+ * Used to seed the ephemeral channel (`ping/ephemeral`), call-media keys
533
+ * (`ping/calls/media/<call_id>`), and call-ephemeral framer keys
534
+ * (`ping/calls/ephemeral/<call_id>`). The returned bytes are a secret — never log,
535
+ * persist only encrypted, and call `bytes.fill(0)` after use to wipe the typed array.
536
+ *
537
+ * Same `(conversationId, current_epoch, label, context, length)` produces the same
538
+ * bytes across every binding (Rust core, native UniFFI, WASM/JS). Cross-platform
539
+ * byte-equality is enforced by `protocol/conformance/export_secret/`.
540
+ */
541
+ async exportConversationSecret(conv, label, context, length) {
542
+ if (length <= 0 || length > 1024) {
543
+ throw new Error("exportConversationSecret: length must be 1..=1024");
544
+ }
545
+ return await this.call({
546
+ kind: "exportConversationSecret",
547
+ id: this.nextId++,
548
+ conv,
549
+ label,
550
+ context,
551
+ length
552
+ });
553
+ }
374
554
  /** Subscribe to decrypted application messages. */
375
555
  onMessage(handler) {
376
556
  this.messageHandlers.add(handler);
@@ -404,12 +584,12 @@ var MessagingClient = class _MessagingClient {
404
584
  nowMs: self.now()
405
585
  });
406
586
  },
407
- async addMembers(kps) {
587
+ async addMembers(entries) {
408
588
  await self.call({
409
589
  kind: "addMembers",
410
590
  id: self.nextId++,
411
591
  conv: id,
412
- kps,
592
+ entries,
413
593
  nowMs: self.now()
414
594
  });
415
595
  },
@@ -495,15 +675,19 @@ function sameBytesU8(a, b) {
495
675
  }
496
676
  export {
497
677
  CONTENT_TYPE_CBOR,
678
+ CONTENT_TYPE_CBOR_LEGACY_V1,
498
679
  CONTENT_TYPE_JSON,
680
+ CONTENT_TYPE_JSON_LEGACY_V1,
499
681
  IndexedDbStorage,
500
682
  MessagingClient,
501
683
  PING_WIRE_CONTRACT_HEADER,
502
684
  PING_WIRE_CONTRACT_HEADER_VALUE,
503
685
  PING_WIRE_CONTRACT_VERSION,
504
686
  ValidationError,
687
+ WIRE_VERSION_MIN_ACCEPTED,
505
688
  WebSocketTransport,
506
689
  negotiates,
507
- validate
690
+ validate,
691
+ verifyApplicationContentHash
508
692
  };
509
693
  //# sourceMappingURL=index.js.map
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/** Ping wire contract version. Bumped on incompatible profile changes. */\nexport const PING_WIRE_CONTRACT_VERSION = 1;\n\n/** Canonical CBOR content type. Production transports MUST use this. */\nexport const CONTENT_TYPE_CBOR = \"application/vnd.ping.wire.v1+cbor\";\n\n/** JSON projection — development and tooling only. Production MUST NOT use this. */\nexport const CONTENT_TYPE_JSON = \"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 */\nexport async function validate(env: MessageEnvelope): Promise<void> {\n if (env.v !== PING_WIRE_CONTRACT_VERSION) {\n throw new ValidationError(\n \"UnsupportedVersion\",\n `unsupported wire-contract version: envelope says v=${env.v}, expected v=${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 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 * True if the supplied content type negotiates the ping-wire-contract.\n *\n * 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 primary === CONTENT_TYPE_CBOR || primary === CONTENT_TYPE_JSON;\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 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\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 CONTENT_TYPE_CBOR,\n CONTENT_TYPE_JSON,\n ValidationError,\n validate,\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\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"],"mappings":";AAIA,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;;;ACjEO,IAAM,6BAA6B;AAGnC,IAAM,oBAAoB;AAG1B,IAAM,oBAAoB;AAG1B,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;AASA,eAAsB,SAAS,KAAqC;AAClE,MAAI,IAAI,MAAM,4BAA4B;AACxC,UAAM,IAAI;AAAA,MACR;AAAA,MACA,sDAAsD,IAAI,CAAC,gBAAgB,0BAA0B;AAAA,IACvG;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;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;AAOO,SAAS,WAAW,aAAiD;AAC1E,MAAI,CAAC,YAAa,QAAO;AACzB,QAAM,UAAU,YAAY,MAAM,GAAG,EAAE,CAAC,GAAG,KAAK,EAAE,YAAY,KAAK;AACnE,SAAO,YAAY,qBAAqB,YAAY;AACtD;;;ACxFO,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;;;ACrEO,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;AAEnD,UAAM,IAAI,IAAI,OAAO,IAAI,IAAI,eAAe,YAAY,GAAG,GAAG,EAAE,MAAM,SAAS,CAAC;AAChF,UAAM,KAAK;AACX,WAAO,IAAI,QAAoB,CAAC,SAAS,WAAW;AAClD,QAAE,YAAY,CAAC,OAAO;AACpB,cAAM,MAAM,GAAG;AACf,YAAI,IAAI,SAAS,YAAY,IAAI,OAAO,IAAI;AAC1C,YAAE,UAAU;AACZ,cAAI,KAAK,QAAQ,IAAI,KAAmB,IAAI,OAAO,IAAI,MAAM,IAAI,KAAK,CAAC;AAAA,QACzE;AAAA,MACF;AACA,YAAM,MAAe,EAAE,MAAM,oBAAoB,GAAG;AACpD,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,IACpB,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,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,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,EAEA,MAAM,mBAAmB,aAAuB,aAAiD;AAC/F,WAAO,MAAM,KAAK,KAAK;AAAA,MACrB,MAAM;AAAA,MAAsB,IAAI,KAAK;AAAA,MACrC;AAAA,MAAa;AAAA,MAAa,OAAO,KAAK,IAAI;AAAA,IAC5C,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,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,KAAK;AACpB,cAAM,KAAK,KAAK;AAAA,UACd,MAAM;AAAA,UAAc,IAAI,KAAK;AAAA,UAAU,MAAM;AAAA,UAAI;AAAA,UAAK,OAAO,KAAK,IAAI;AAAA,QACxE,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/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 CatchupAppEvent, CatchupSnapshotView,\n ConversationId, ConversationMeta, DeviceId, IncomingMessage,\n KeyPackageEntry, 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\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\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 // 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 });\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 /**\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 /**\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,4 +1,4 @@
1
- import { S as Storage } from '../types-jd8CLdi_.cjs';
1
+ import { S as Storage } from '../types-DYtsj5dy.cjs';
2
2
 
3
3
  declare class IndexedDbStorage implements Storage {
4
4
  private dbPromise;
@@ -1,4 +1,4 @@
1
- import { S as Storage } from '../types-jd8CLdi_.js';
1
+ import { S as Storage } from '../types-DYtsj5dy.js';
2
2
 
3
3
  declare class IndexedDbStorage implements Storage {
4
4
  private dbPromise;
@@ -25,8 +25,8 @@ __export(websocket_exports, {
25
25
  module.exports = __toCommonJS(websocket_exports);
26
26
 
27
27
  // src/wire-contract.ts
28
- var PING_WIRE_CONTRACT_VERSION = 1;
29
- var CONTENT_TYPE_JSON = "application/vnd.ping.wire.v1+json";
28
+ var PING_WIRE_CONTRACT_VERSION = 2;
29
+ var CONTENT_TYPE_JSON = "application/vnd.ping.wire.v2+json";
30
30
  var PING_WIRE_CONTRACT_HEADER = "X-Ping-Wire-Contract";
31
31
  var PING_WIRE_CONTRACT_HEADER_VALUE = `ping/${PING_WIRE_CONTRACT_VERSION}`;
32
32
 
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/transport/websocket.ts","../../src/wire-contract.ts"],"sourcesContent":["// 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","// 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/** Ping wire contract version. Bumped on incompatible profile changes. */\nexport const PING_WIRE_CONTRACT_VERSION = 1;\n\n/** Canonical CBOR content type. Production transports MUST use this. */\nexport const CONTENT_TYPE_CBOR = \"application/vnd.ping.wire.v1+cbor\";\n\n/** JSON projection — development and tooling only. Production MUST NOT use this. */\nexport const CONTENT_TYPE_JSON = \"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 */\nexport async function validate(env: MessageEnvelope): Promise<void> {\n if (env.v !== PING_WIRE_CONTRACT_VERSION) {\n throw new ValidationError(\n \"UnsupportedVersion\",\n `unsupported wire-contract version: envelope says v=${env.v}, expected v=${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 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 * True if the supplied content type negotiates the ping-wire-contract.\n *\n * 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 primary === CONTENT_TYPE_CBOR || primary === CONTENT_TYPE_JSON;\n}\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../../src/transport/websocket.ts","../../src/wire-contract.ts"],"sourcesContent":["// 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","// 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"],"mappings":"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","names":[]}
@@ -1,4 +1,4 @@
1
- import { T as Transport, M as MessageEnvelope, C as ConversationId, U as UserId, D as DiscoveredDevice } from '../types-jd8CLdi_.cjs';
1
+ import { T as Transport, M as MessageEnvelope, C as ConversationId, U as UserId, D as DiscoveredDevice } from '../types-DYtsj5dy.cjs';
2
2
 
3
3
  interface WsTransportConfig {
4
4
  baseUrl: string;
@@ -1,4 +1,4 @@
1
- import { T as Transport, M as MessageEnvelope, C as ConversationId, U as UserId, D as DiscoveredDevice } from '../types-jd8CLdi_.js';
1
+ import { T as Transport, M as MessageEnvelope, C as ConversationId, U as UserId, D as DiscoveredDevice } from '../types-DYtsj5dy.js';
2
2
 
3
3
  interface WsTransportConfig {
4
4
  baseUrl: string;
@@ -1,6 +1,6 @@
1
1
  // src/wire-contract.ts
2
- var PING_WIRE_CONTRACT_VERSION = 1;
3
- var CONTENT_TYPE_JSON = "application/vnd.ping.wire.v1+json";
2
+ var PING_WIRE_CONTRACT_VERSION = 2;
3
+ var CONTENT_TYPE_JSON = "application/vnd.ping.wire.v2+json";
4
4
  var PING_WIRE_CONTRACT_HEADER = "X-Ping-Wire-Contract";
5
5
  var PING_WIRE_CONTRACT_HEADER_VALUE = `ping/${PING_WIRE_CONTRACT_VERSION}`;
6
6