ping-openmls-sdk 0.0.1 → 0.1.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.cjs CHANGED
@@ -20,9 +20,17 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
20
20
  // src/index.ts
21
21
  var src_exports = {};
22
22
  __export(src_exports, {
23
+ CONTENT_TYPE_CBOR: () => CONTENT_TYPE_CBOR,
24
+ CONTENT_TYPE_JSON: () => CONTENT_TYPE_JSON,
23
25
  IndexedDbStorage: () => IndexedDbStorage,
24
26
  MessagingClient: () => MessagingClient,
25
- WebSocketTransport: () => WebSocketTransport
27
+ PING_WIRE_CONTRACT_HEADER: () => PING_WIRE_CONTRACT_HEADER,
28
+ PING_WIRE_CONTRACT_HEADER_VALUE: () => PING_WIRE_CONTRACT_HEADER_VALUE,
29
+ PING_WIRE_CONTRACT_VERSION: () => PING_WIRE_CONTRACT_VERSION,
30
+ ValidationError: () => ValidationError,
31
+ WebSocketTransport: () => WebSocketTransport,
32
+ negotiates: () => negotiates,
33
+ validate: () => validate
26
34
  });
27
35
  module.exports = __toCommonJS(src_exports);
28
36
 
@@ -95,6 +103,73 @@ var IndexedDbStorage = class {
95
103
  }
96
104
  };
97
105
 
106
+ // src/wire-contract.ts
107
+ var PING_WIRE_CONTRACT_VERSION = 1;
108
+ var CONTENT_TYPE_CBOR = "application/vnd.ping.wire.v1+cbor";
109
+ var CONTENT_TYPE_JSON = "application/vnd.ping.wire.v1+json";
110
+ var PING_WIRE_CONTRACT_HEADER = "X-Ping-Wire-Contract";
111
+ var PING_WIRE_CONTRACT_HEADER_VALUE = `ping/${PING_WIRE_CONTRACT_VERSION}`;
112
+ var ValidationError = class extends Error {
113
+ constructor(kind, message) {
114
+ super(message);
115
+ this.kind = kind;
116
+ this.name = "PingWireContractValidationError";
117
+ }
118
+ kind;
119
+ };
120
+ var KIND_DISCRIMINANT = {
121
+ Application: 1,
122
+ Commit: 2,
123
+ Welcome: 3,
124
+ Proposal: 4,
125
+ KeyPackage: 5
126
+ };
127
+ async function sha256(bytes) {
128
+ const ab = bytes.buffer.slice(bytes.byteOffset, bytes.byteOffset + bytes.byteLength);
129
+ const hash = await crypto.subtle.digest("SHA-256", ab);
130
+ return new Uint8Array(hash);
131
+ }
132
+ function bytesEqual(a, b) {
133
+ if (a.length !== b.length) return false;
134
+ for (let i = 0; i < a.length; i++) if (a[i] !== b[i]) return false;
135
+ return true;
136
+ }
137
+ async function validate(env) {
138
+ if (env.v !== PING_WIRE_CONTRACT_VERSION) {
139
+ throw new ValidationError(
140
+ "UnsupportedVersion",
141
+ `unsupported wire-contract version: envelope says v=${env.v}, expected v=${PING_WIRE_CONTRACT_VERSION}`
142
+ );
143
+ }
144
+ if (env.conversation_id.length !== 16) {
145
+ throw new ValidationError(
146
+ "BadConversationIdLen",
147
+ `conversation_id must be 16 bytes (ULID), got ${env.conversation_id.length}`
148
+ );
149
+ }
150
+ if (env.sender_device.length !== 32) {
151
+ throw new ValidationError(
152
+ "BadSenderDeviceLen",
153
+ `sender_device must be 32 bytes, got ${env.sender_device.length}`
154
+ );
155
+ }
156
+ const buf = new Uint8Array(1 + env.payload.length);
157
+ buf[0] = KIND_DISCRIMINANT[env.kind];
158
+ buf.set(env.payload, 1);
159
+ const computed = await sha256(buf);
160
+ if (!bytesEqual(computed, env.content_hash)) {
161
+ throw new ValidationError(
162
+ "ContentHashMismatch",
163
+ "content_hash mismatch: envelope is forged, corrupted, or built by a non-conformant sender"
164
+ );
165
+ }
166
+ }
167
+ function negotiates(contentType) {
168
+ if (!contentType) return false;
169
+ const primary = contentType.split(";")[0]?.trim().toLowerCase() ?? "";
170
+ return primary === CONTENT_TYPE_CBOR || primary === CONTENT_TYPE_JSON;
171
+ }
172
+
98
173
  // src/transport/websocket.ts
99
174
  var WebSocketTransport = class {
100
175
  constructor(cfg) {
@@ -125,7 +200,8 @@ var WebSocketTransport = class {
125
200
  const res = await this.fetchImpl(`${this.cfg.baseUrl}/send`, {
126
201
  method: "POST",
127
202
  headers: {
128
- "content-type": "application/json",
203
+ "content-type": CONTENT_TYPE_JSON,
204
+ [PING_WIRE_CONTRACT_HEADER]: PING_WIRE_CONTRACT_HEADER_VALUE,
129
205
  ...this.cfg.authHeader ? { authorization: this.cfg.authHeader } : {}
130
206
  },
131
207
  body: JSON.stringify(envelope, replacer)
@@ -456,8 +532,16 @@ function sameBytesU8(a, b) {
456
532
  }
457
533
  // Annotate the CommonJS export names for ESM import in node:
458
534
  0 && (module.exports = {
535
+ CONTENT_TYPE_CBOR,
536
+ CONTENT_TYPE_JSON,
459
537
  IndexedDbStorage,
460
538
  MessagingClient,
461
- WebSocketTransport
539
+ PING_WIRE_CONTRACT_HEADER,
540
+ PING_WIRE_CONTRACT_HEADER_VALUE,
541
+ PING_WIRE_CONTRACT_VERSION,
542
+ ValidationError,
543
+ WebSocketTransport,
544
+ negotiates,
545
+ validate
462
546
  });
463
547
  //# sourceMappingURL=index.cjs.map
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/index.ts","../src/storage/indexeddb.ts","../src/transport/websocket.ts"],"sourcesContent":["// Public entry point. Spawns the dedicated Worker that hosts the WASM instance and exposes a\n// Promise-based, identical-to-native API. Crypto runs off the main thread.\n\nimport type {\n ConversationId, ConversationMeta, DeviceId, IncomingMessage,\n LinkingTicket, MessageEnvelope, Storage, Transport, UserId,\n} from \"./types\";\nimport type { Request, Response } from \"./protocol\";\n\nexport type * from \"./types\";\n\n// Re-export the default Storage and Transport implementations from the main entry so consumers\n// whose bundlers don't honor package.json#exports (Metro with strict resolution, some legacy\n// Webpack configs) can still get them with a plain `import { ... } from \"ping-openmls-sdk\"`. Power\n// users who care about tree-shaking can keep using the subpath imports.\nexport { IndexedDbStorage } from \"./storage/indexeddb\";\nexport { WebSocketTransport } from \"./transport/websocket\";\n\nexport interface ClientConfig {\n identityExport: Uint8Array;\n deviceLabel: string;\n storage: Storage;\n transport: Transport;\n /** Override the wall clock (test/sim only). */\n now?: () => number;\n}\n\nexport interface Conversation {\n readonly id: ConversationId;\n send(plaintext: Uint8Array): Promise<MessageEnvelope>;\n addMembers(keyPackages: Uint8Array[]): Promise<void>;\n removeMembers(leafIndexes: number[]): Promise<void>;\n meta(): ConversationMeta;\n}\n\nexport class MessagingClient {\n private worker: Worker;\n private nextId = 1;\n private pending = new Map<number, { resolve: (v: unknown) => void; reject: (e: Error) => void }>();\n private storage: Storage;\n private transport: Transport;\n private now: () => number;\n private messageHandlers = new Set<(m: IncomingMessage) => void>();\n private conversationHandlers = new Set<(id: ConversationId, epoch: number, sender?: DeviceId) => void>();\n private subscription: { unsubscribe(): void } | null = null;\n private metaCache = new Map<string, ConversationMeta>();\n // Cached at init() so dispatchIncoming can skip envelopes we sent ourselves — the relay\n // broadcasts to all WS subscribers including the sender, and re-applying our own already-\n // merged commits would error with \"epoch differs\".\n private ownDeviceId: Uint8Array | null = null;\n\n private constructor(cfg: ClientConfig, worker: Worker) {\n this.worker = worker;\n this.storage = cfg.storage;\n this.transport = cfg.transport;\n this.now = cfg.now ?? (() => Date.now());\n this.worker.onmessage = (ev) => this.handleWorker(ev.data as Response);\n }\n\n /** Generate a fresh identity. The returned bytes are a secret — store them encrypted. */\n static async generateIdentity(): Promise<Uint8Array> {\n // Spawn an ephemeral worker to call `generateIdentity` so we don't pull WASM into the main thread.\n const w = new Worker(new URL(\"./worker.js\", import.meta.url), { type: \"module\" });\n const id = 1;\n return new Promise<Uint8Array>((resolve, reject) => {\n w.onmessage = (ev) => {\n const msg = ev.data as Response;\n if (msg.kind === \"result\" && msg.id === id) {\n w.terminate();\n msg.ok ? resolve(msg.value as Uint8Array) : reject(new Error(msg.error));\n }\n };\n const req: Request = { kind: \"generateIdentity\", id };\n w.postMessage(req);\n });\n }\n\n static async init(cfg: ClientConfig): Promise<MessagingClient> {\n const worker = new Worker(new URL(\"./worker.js\", import.meta.url), { type: \"module\" });\n const client = new MessagingClient(cfg, worker);\n\n await client.call({\n kind: \"init\",\n id: client.nextId++,\n identityExport: cfg.identityExport,\n deviceLabel: cfg.deviceLabel,\n nowMs: client.now(),\n });\n\n // Cache the device id once so we can drop envelopes we sent ourselves on the WS echo.\n client.ownDeviceId = await client.deviceId();\n\n // Start the live subscription. We dispatch each envelope to the right handler:\n // - Welcome envelopes for unknown conversations → joinConversation\n // - Welcomes for already-joined conversations → drop (server may broadcast duplicates)\n // - Welcomes addressed to other devices → drop (we can't decrypt)\n // - Everything else → processEnvelope\n client.subscription = cfg.transport.subscribe((envelope) => {\n void client.dispatchIncoming(envelope);\n });\n return client;\n }\n\n /** Internal: route an inbound envelope to the right handler. */\n private async dispatchIncoming(envelope: MessageEnvelope): Promise<void> {\n // The relay broadcasts every envelope to every WS subscriber, including the sender. For\n // our own sends, the local MLS state has already been advanced in-process — re-applying\n // would either fail with \"epoch differs\" (commits) or be a no-op dedupe (application\n // messages). Skip them here.\n if (this.ownDeviceId && sameBytesU8(envelope.sender_device, this.ownDeviceId)) {\n return;\n }\n if (envelope.kind === \"Welcome\") {\n try {\n const known = await this.listConversations();\n const already = known.some((m) => sameBytesU8(m.id, envelope.conversation_id));\n if (already) return;\n await this.joinConversation(envelope);\n } catch (e) {\n // Welcomes are broadcast to every connected client; only one device's KeyPackage can\n // decrypt each one, so most fail. Log at debug, drop silently.\n if (typeof console !== \"undefined\") console.debug(\"[ping-sdk] welcome ignored:\", e);\n }\n return;\n }\n try {\n await this.processEnvelope(envelope);\n } catch (e) {\n if (typeof console !== \"undefined\") console.debug(\"[ping-sdk] envelope drop:\", e);\n }\n }\n\n // ------------------------ Public API ------------------------\n\n async userId(): Promise<UserId> {\n return await this.call({ kind: \"userId\", id: this.nextId++ }) as UserId;\n }\n\n async deviceId(): Promise<DeviceId> {\n return await this.call({ kind: \"deviceId\", id: this.nextId++ }) as DeviceId;\n }\n\n async freshKeyPackage(): Promise<Uint8Array> {\n return await this.call({ kind: \"freshKeyPackage\", id: this.nextId++ }) as Uint8Array;\n }\n\n async createConversation(opts: { name?: string } = {}): Promise<Conversation> {\n const idBytes = await this.call({\n kind: \"createConversation\", id: this.nextId++, name: opts.name ?? null, nowMs: this.now(),\n }) as ConversationId;\n return this.conversationProxy(idBytes);\n }\n\n async joinConversation(welcome: MessageEnvelope): Promise<Conversation> {\n const idBytes = await this.call({\n kind: \"joinConversation\", id: this.nextId++, welcome, nowMs: this.now(),\n }) as ConversationId;\n return this.conversationProxy(idBytes);\n }\n\n /**\n * Get a `Conversation` proxy for an already-known conversation. Use this when you have a\n * conversation id (e.g., from `listConversations()`) and want to send/addMembers without\n * re-creating it. Throws if the id isn't known to the worker — call `listConversations()`\n * first to verify membership.\n */\n getConversation(id: ConversationId): Conversation {\n return this.conversationProxy(id);\n }\n\n async listConversations(): Promise<ConversationMeta[]> {\n const metas = await this.call({ kind: \"listConversations\", id: this.nextId++ }) as ConversationMeta[];\n this.metaCache.clear();\n for (const m of metas) this.metaCache.set(hex(m.id), m);\n return metas;\n }\n\n async syncConversations(): Promise<IncomingMessage[]> {\n return await this.call({\n kind: \"syncConversations\", id: this.nextId++, nowMs: this.now(),\n }) as IncomingMessage[];\n }\n\n async processEnvelope(envelope: MessageEnvelope): Promise<IncomingMessage | null> {\n return await this.call({\n kind: \"processEnvelope\", id: this.nextId++, envelope, nowMs: this.now(),\n }) as IncomingMessage | null;\n }\n\n async buildLinkingTicket(newDeviceId: DeviceId, newDeviceKp: Uint8Array): Promise<LinkingTicket> {\n return await this.call({\n kind: \"buildLinkingTicket\", id: this.nextId++,\n newDeviceId, newDeviceKp, nowMs: this.now(),\n }) as LinkingTicket;\n }\n\n async consumeLinkingTicket(ticket: LinkingTicket): Promise<void> {\n await this.call({\n kind: \"consumeLinkingTicket\", id: this.nextId++, ticket, nowMs: this.now(),\n });\n }\n\n /** Subscribe to decrypted application messages. */\n onMessage(handler: (m: IncomingMessage) => void): () => void {\n this.messageHandlers.add(handler);\n return () => this.messageHandlers.delete(handler);\n }\n\n /** Fired after every state-changing event for a conversation (Commit, member changes). */\n /**\n * Fires after every conversation state change — joining via Welcome, processing a Commit,\n * etc. `sender` carries the device id of whoever triggered the event when known (e.g. the\n * inviter's device on auto-join), so the host can label peers in its UI.\n */\n onConversationUpdated(handler: (id: ConversationId, epoch: number, sender?: DeviceId) => void): () => void {\n this.conversationHandlers.add(handler);\n return () => this.conversationHandlers.delete(handler);\n }\n\n async close(): Promise<void> {\n this.subscription?.unsubscribe();\n this.worker.terminate();\n }\n\n // ------------------------ Internals ------------------------\n\n private conversationProxy(id: ConversationId): Conversation {\n const self = this;\n return {\n id,\n async send(plaintext) {\n return await self.call({\n kind: \"send\", id: self.nextId++, conv: id, plaintext, nowMs: self.now(),\n }) as MessageEnvelope;\n },\n async addMembers(kps) {\n await self.call({\n kind: \"addMembers\", id: self.nextId++, conv: id, kps, nowMs: self.now(),\n });\n },\n async removeMembers(leaves) {\n await self.call({\n kind: \"removeMembers\", id: self.nextId++, conv: id, leaves, nowMs: self.now(),\n });\n },\n meta() {\n const cached = self.metaCache.get(hex(id));\n if (!cached) throw new Error(\"conversation meta not loaded; call listConversations() first\");\n return cached;\n },\n };\n }\n\n private call(req: Request): Promise<unknown> {\n return new Promise((resolve, reject) => {\n this.pending.set((req as { id: number }).id, { resolve, reject });\n this.worker.postMessage(req);\n });\n }\n\n private async handleWorker(msg: Response) {\n switch (msg.kind) {\n case \"result\": {\n const slot = this.pending.get(msg.id);\n if (!slot) return;\n this.pending.delete(msg.id);\n msg.ok ? slot.resolve(msg.value) : slot.reject(new Error(msg.error));\n return;\n }\n case \"event.message\": {\n for (const h of this.messageHandlers) h(msg.msg);\n return;\n }\n case \"event.conversation\": {\n for (const h of this.conversationHandlers) h(msg.id, msg.epoch, msg.sender);\n return;\n }\n case \"storage.call\": {\n try {\n const v = await (this.storage as unknown as Record<string, (...a: unknown[]) => unknown>)\n [msg.method]?.(...msg.args);\n this.worker.postMessage({ kind: \"storage.reply\", id: msg.id, ok: true, value: v } as Request);\n } catch (e) {\n this.worker.postMessage({\n kind: \"storage.reply\", id: msg.id, ok: false, error: String(e),\n } as Request);\n }\n return;\n }\n case \"transport.call\": {\n try {\n const v = await (this.transport as unknown as Record<string, (...a: unknown[]) => unknown>)\n [msg.method]?.(...msg.args);\n this.worker.postMessage({ kind: \"transport.reply\", id: msg.id, ok: true, value: v } as Request);\n } catch (e) {\n this.worker.postMessage({\n kind: \"transport.reply\", id: msg.id, ok: false, error: String(e),\n } as Request);\n }\n return;\n }\n }\n }\n}\n\nfunction hex(b: Uint8Array): string {\n let s = \"\";\n for (const v of b) s += v.toString(16).padStart(2, \"0\");\n return s;\n}\n\nfunction sameBytesU8(a: Uint8Array, b: Uint8Array): boolean {\n if (a.length !== b.length) return false;\n for (let i = 0; i < a.length; i++) if (a[i] !== b[i]) return false;\n return true;\n}\n","// Default IndexedDB-backed Storage implementation. Hosts can supply their own.\n\nimport type { Storage } from \"../types\";\n\nconst DB_NAME = \"ping-sdk\";\nconst DB_VERSION = 1;\nconst STORE = \"kv\";\n\ninterface Row { ns: string; key: string; value: Uint8Array }\n\nfunction open(): Promise<IDBDatabase> {\n return new Promise((resolve, reject) => {\n const req = indexedDB.open(DB_NAME, DB_VERSION);\n req.onupgradeneeded = () => {\n const db = req.result;\n if (!db.objectStoreNames.contains(STORE)) {\n const os = db.createObjectStore(STORE, { keyPath: [\"ns\", \"key\"] });\n os.createIndex(\"ns\", \"ns\", { unique: false });\n }\n };\n req.onsuccess = () => resolve(req.result);\n req.onerror = () => reject(req.error);\n });\n}\n\nexport class IndexedDbStorage implements Storage {\n private dbPromise: Promise<IDBDatabase>;\n\n constructor() { this.dbPromise = open(); }\n\n async get(namespace: string, key: string): Promise<Uint8Array | null> {\n const db = await this.dbPromise;\n return new Promise((resolve, reject) => {\n const tx = db.transaction(STORE, \"readonly\");\n const r = tx.objectStore(STORE).get([namespace, key]);\n r.onsuccess = () => resolve(((r.result as Row | undefined)?.value) ?? null);\n r.onerror = () => reject(r.error);\n });\n }\n\n async put(namespace: string, key: string, value: Uint8Array): Promise<void> {\n const db = await this.dbPromise;\n return new Promise((resolve, reject) => {\n const tx = db.transaction(STORE, \"readwrite\");\n tx.objectStore(STORE).put({ ns: namespace, key, value } as Row);\n tx.oncomplete = () => resolve();\n tx.onerror = () => reject(tx.error);\n });\n }\n\n async del(namespace: string, key: string): Promise<void> {\n const db = await this.dbPromise;\n return new Promise((resolve, reject) => {\n const tx = db.transaction(STORE, \"readwrite\");\n tx.objectStore(STORE).delete([namespace, key]);\n tx.oncomplete = () => resolve();\n tx.onerror = () => reject(tx.error);\n });\n }\n\n async listKeys(namespace: string, prefix: string): Promise<string[]> {\n const db = await this.dbPromise;\n return new Promise((resolve, reject) => {\n const tx = db.transaction(STORE, \"readonly\");\n const idx = tx.objectStore(STORE).index(\"ns\");\n const req = idx.openCursor(IDBKeyRange.only(namespace));\n const out: string[] = [];\n req.onsuccess = () => {\n const cur = req.result;\n if (!cur) return resolve(out);\n const row = cur.value as Row;\n if (!prefix || row.key.startsWith(prefix)) out.push(row.key);\n cur.continue();\n };\n req.onerror = () => reject(req.error);\n });\n }\n}\n","// Reference WebSocket transport. Treats the server as an opaque relay that:\n// - accepts a CBOR-encoded MessageEnvelope on `send`\n// - returns events newer than a cursor on `fetchSince`\n// - streams new events on the WS itself for `subscribe`\n// - exposes a directory endpoint for `discoverDevices`\n//\n// This is a sample. Real deployments will tune framing / auth.\n\nimport type { ConversationId, DiscoveredDevice, MessageEnvelope, Transport, UserId } from \"../types\";\n\nexport interface WsTransportConfig {\n baseUrl: string; // ws[s]://...\n authHeader?: string; // optional bearer token\n fetchImpl?: typeof fetch; // override for SSR / tests\n}\n\nexport class WebSocketTransport implements Transport {\n private ws: WebSocket | null = null;\n private liveHandlers = new Set<(env: MessageEnvelope) => void>();\n private fetchImpl: typeof fetch;\n\n constructor(private cfg: WsTransportConfig) {\n this.fetchImpl = cfg.fetchImpl ?? globalThis.fetch.bind(globalThis);\n this.openSocket();\n }\n\n private openSocket() {\n if (typeof WebSocket === \"undefined\") return;\n const url = `${this.cfg.baseUrl.replace(/^http/, \"ws\")}/stream`;\n this.ws = new WebSocket(url);\n this.ws.onmessage = (ev) => {\n // Server sends one JSON envelope per text frame.\n const text = typeof ev.data === \"string\" ? ev.data : new TextDecoder().decode(ev.data as ArrayBuffer);\n try {\n const env = JSON.parse(text, reviver) as MessageEnvelope;\n for (const h of this.liveHandlers) h(env);\n } catch (e) {\n if (typeof console !== \"undefined\") console.debug(\"[ping-sdk] WS frame parse error:\", e);\n }\n };\n this.ws.onclose = () => setTimeout(() => this.openSocket(), 1000);\n }\n\n async send(envelope: MessageEnvelope): Promise<void> {\n const res = await this.fetchImpl(`${this.cfg.baseUrl}/send`, {\n method: \"POST\",\n headers: {\n \"content-type\": \"application/json\",\n ...(this.cfg.authHeader ? { authorization: this.cfg.authHeader } : {}),\n },\n body: JSON.stringify(envelope, replacer),\n });\n if (res.status === 409) throw new Error(\"EpochOccupied\");\n if (!res.ok) throw new Error(`transport send failed: ${res.status}`);\n }\n\n async fetchSince(conversationId: ConversationId, cursor: Uint8Array, limit: number): Promise<MessageEnvelope[]> {\n const url = new URL(`${this.cfg.baseUrl}/sync`);\n url.searchParams.set(\"conv\", toHex(conversationId));\n url.searchParams.set(\"cursor\", btoaUrl(cursor));\n url.searchParams.set(\"limit\", String(limit));\n const res = await this.fetchImpl(url.toString(), {\n headers: { ...(this.cfg.authHeader ? { authorization: this.cfg.authHeader } : {}) },\n });\n if (!res.ok) throw new Error(`transport fetch failed: ${res.status}`);\n const text = await res.text();\n return JSON.parse(text, reviver) as MessageEnvelope[];\n }\n\n subscribe(onEvent: (env: MessageEnvelope) => void): { unsubscribe(): void } {\n this.liveHandlers.add(onEvent);\n return { unsubscribe: () => this.liveHandlers.delete(onEvent) };\n }\n\n async discoverDevices(userId: UserId): Promise<DiscoveredDevice[]> {\n const res = await this.fetchImpl(`${this.cfg.baseUrl}/devices/${toHex(userId)}`, {\n headers: { ...(this.cfg.authHeader ? { authorization: this.cfg.authHeader } : {}) },\n });\n if (!res.ok) throw new Error(`device discovery failed: ${res.status}`);\n const text = await res.text();\n return JSON.parse(text, reviver) as DiscoveredDevice[];\n }\n}\n\n// ---- Wire codec ----\n//\n// Demo wire format: JSON with byte fields wrapped as `{ _bytes: number[] }`. The relay's\n// `envFromJson`/`envToJson` use the same shape. The spec calls for CBOR; v0.2 swaps in a\n// real CBOR codec, but the JSON form is much friendlier to debug.\n//\n// (decodeEnvelope helpers used to live here; collapsed into inline JSON.parse calls above.)\n\nfunction replacer(_k: string, v: unknown): unknown {\n if (v instanceof Uint8Array) return { _bytes: Array.from(v) };\n return v;\n}\nfunction reviver(_k: string, v: unknown): unknown {\n if (v && typeof v === \"object\" && Array.isArray((v as { _bytes?: number[] })._bytes)) {\n return new Uint8Array((v as { _bytes: number[] })._bytes);\n }\n return v;\n}\n\nfunction toHex(b: Uint8Array): string {\n let s = \"\"; for (const v of b) s += v.toString(16).padStart(2, \"0\"); return s;\n}\nfunction btoaUrl(b: Uint8Array): string {\n let s = \"\"; for (const v of b) s += String.fromCharCode(v);\n return btoa(s).replace(/\\+/g, \"-\").replace(/\\//g, \"_\").replace(/=+$/, \"\");\n}\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../src/index.ts","../src/storage/indexeddb.ts","../src/wire-contract.ts","../src/transport/websocket.ts"],"sourcesContent":["// Public entry point. Spawns the dedicated Worker that hosts the WASM instance and exposes a\n// Promise-based, identical-to-native API. Crypto runs off the main thread.\n\nimport type {\n 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","// 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"],"mappings":"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","names":[]}
package/dist/index.d.cts CHANGED
@@ -1,8 +1,38 @@
1
- import { S as Storage, T as Transport, C as ConversationId, M as MessageEnvelope, a as ConversationMeta, U as UserId, D as DeviceId, I as IncomingMessage, L as LinkingTicket } from './types-COHY5NU8.cjs';
2
- export { B as Bytes, b as DeviceInfo, c as DiscoveredDevice, H as Hlc, d as MessageKind } from './types-COHY5NU8.cjs';
1
+ import { M as MessageEnvelope, S as Storage, T as Transport, C as ConversationId, a as ConversationMeta, U as UserId, b as DeviceId, I as IncomingMessage, L as LinkingTicket } from './types-jd8CLdi_.cjs';
2
+ export { B as Bytes, c as DeviceInfo, D as DiscoveredDevice, H as Hlc, d as MessageKind } from './types-jd8CLdi_.cjs';
3
3
  export { IndexedDbStorage } from './storage/indexeddb.cjs';
4
4
  export { WebSocketTransport } from './transport/websocket.cjs';
5
5
 
6
+ /** Ping wire contract version. Bumped on incompatible profile changes. */
7
+ declare const PING_WIRE_CONTRACT_VERSION = 1;
8
+ /** Canonical CBOR content type. Production transports MUST use this. */
9
+ declare const CONTENT_TYPE_CBOR = "application/vnd.ping.wire.v1+cbor";
10
+ /** JSON projection — development and tooling only. Production MUST NOT use this. */
11
+ declare const CONTENT_TYPE_JSON = "application/vnd.ping.wire.v1+json";
12
+ /** Header used over transports that don't surface a content type (e.g. WebSocket). */
13
+ declare const PING_WIRE_CONTRACT_HEADER = "X-Ping-Wire-Contract";
14
+ /** Header value: `ping/<VERSION>`. */
15
+ declare const PING_WIRE_CONTRACT_HEADER_VALUE = "ping/1";
16
+ type ValidationErrorKind = "UnsupportedVersion" | "BadConversationIdLen" | "BadSenderDeviceLen" | "ContentHashMismatch";
17
+ declare class ValidationError extends Error {
18
+ readonly kind: ValidationErrorKind;
19
+ constructor(kind: ValidationErrorKind, message: string);
20
+ }
21
+ /**
22
+ * Validate an envelope against the ping-wire-contract.
23
+ *
24
+ * Receivers claiming contract conformance MUST call this before applying any envelope.
25
+ * External consumers using the raw envelope under their own profile are not bound by
26
+ * these rules.
27
+ */
28
+ declare function validate(env: MessageEnvelope): Promise<void>;
29
+ /**
30
+ * True if the supplied content type negotiates the ping-wire-contract.
31
+ *
32
+ * Tolerates whitespace and parameters (e.g. `; charset=utf-8`).
33
+ */
34
+ declare function negotiates(contentType: string | null | undefined): boolean;
35
+
6
36
  interface ClientConfig {
7
37
  identityExport: Uint8Array;
8
38
  deviceLabel: string;
@@ -70,4 +100,4 @@ declare class MessagingClient {
70
100
  private handleWorker;
71
101
  }
72
102
 
73
- export { type ClientConfig, type Conversation, ConversationId, ConversationMeta, DeviceId, IncomingMessage, LinkingTicket, MessageEnvelope, MessagingClient, Storage, Transport, UserId };
103
+ export { CONTENT_TYPE_CBOR, CONTENT_TYPE_JSON, type ClientConfig, type Conversation, ConversationId, ConversationMeta, DeviceId, IncomingMessage, LinkingTicket, MessageEnvelope, MessagingClient, PING_WIRE_CONTRACT_HEADER, PING_WIRE_CONTRACT_HEADER_VALUE, PING_WIRE_CONTRACT_VERSION, Storage, Transport, UserId, ValidationError, negotiates, validate };
package/dist/index.d.ts CHANGED
@@ -1,8 +1,38 @@
1
- import { S as Storage, T as Transport, C as ConversationId, M as MessageEnvelope, a as ConversationMeta, U as UserId, D as DeviceId, I as IncomingMessage, L as LinkingTicket } from './types-COHY5NU8.js';
2
- export { B as Bytes, b as DeviceInfo, c as DiscoveredDevice, H as Hlc, d as MessageKind } from './types-COHY5NU8.js';
1
+ import { M as MessageEnvelope, S as Storage, T as Transport, C as ConversationId, a as ConversationMeta, U as UserId, b as DeviceId, I as IncomingMessage, L as LinkingTicket } from './types-jd8CLdi_.js';
2
+ export { B as Bytes, c as DeviceInfo, D as DiscoveredDevice, H as Hlc, d as MessageKind } from './types-jd8CLdi_.js';
3
3
  export { IndexedDbStorage } from './storage/indexeddb.js';
4
4
  export { WebSocketTransport } from './transport/websocket.js';
5
5
 
6
+ /** Ping wire contract version. Bumped on incompatible profile changes. */
7
+ declare const PING_WIRE_CONTRACT_VERSION = 1;
8
+ /** Canonical CBOR content type. Production transports MUST use this. */
9
+ declare const CONTENT_TYPE_CBOR = "application/vnd.ping.wire.v1+cbor";
10
+ /** JSON projection — development and tooling only. Production MUST NOT use this. */
11
+ declare const CONTENT_TYPE_JSON = "application/vnd.ping.wire.v1+json";
12
+ /** Header used over transports that don't surface a content type (e.g. WebSocket). */
13
+ declare const PING_WIRE_CONTRACT_HEADER = "X-Ping-Wire-Contract";
14
+ /** Header value: `ping/<VERSION>`. */
15
+ declare const PING_WIRE_CONTRACT_HEADER_VALUE = "ping/1";
16
+ type ValidationErrorKind = "UnsupportedVersion" | "BadConversationIdLen" | "BadSenderDeviceLen" | "ContentHashMismatch";
17
+ declare class ValidationError extends Error {
18
+ readonly kind: ValidationErrorKind;
19
+ constructor(kind: ValidationErrorKind, message: string);
20
+ }
21
+ /**
22
+ * Validate an envelope against the ping-wire-contract.
23
+ *
24
+ * Receivers claiming contract conformance MUST call this before applying any envelope.
25
+ * External consumers using the raw envelope under their own profile are not bound by
26
+ * these rules.
27
+ */
28
+ declare function validate(env: MessageEnvelope): Promise<void>;
29
+ /**
30
+ * True if the supplied content type negotiates the ping-wire-contract.
31
+ *
32
+ * Tolerates whitespace and parameters (e.g. `; charset=utf-8`).
33
+ */
34
+ declare function negotiates(contentType: string | null | undefined): boolean;
35
+
6
36
  interface ClientConfig {
7
37
  identityExport: Uint8Array;
8
38
  deviceLabel: string;
@@ -70,4 +100,4 @@ declare class MessagingClient {
70
100
  private handleWorker;
71
101
  }
72
102
 
73
- export { type ClientConfig, type Conversation, ConversationId, ConversationMeta, DeviceId, IncomingMessage, LinkingTicket, MessageEnvelope, MessagingClient, Storage, Transport, UserId };
103
+ export { CONTENT_TYPE_CBOR, CONTENT_TYPE_JSON, type ClientConfig, type Conversation, ConversationId, ConversationMeta, DeviceId, IncomingMessage, LinkingTicket, MessageEnvelope, MessagingClient, PING_WIRE_CONTRACT_HEADER, PING_WIRE_CONTRACT_HEADER_VALUE, PING_WIRE_CONTRACT_VERSION, Storage, Transport, UserId, ValidationError, negotiates, validate };
package/dist/index.js CHANGED
@@ -67,6 +67,73 @@ var IndexedDbStorage = class {
67
67
  }
68
68
  };
69
69
 
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";
74
+ var PING_WIRE_CONTRACT_HEADER = "X-Ping-Wire-Contract";
75
+ var PING_WIRE_CONTRACT_HEADER_VALUE = `ping/${PING_WIRE_CONTRACT_VERSION}`;
76
+ var ValidationError = class extends Error {
77
+ constructor(kind, message) {
78
+ super(message);
79
+ this.kind = kind;
80
+ this.name = "PingWireContractValidationError";
81
+ }
82
+ kind;
83
+ };
84
+ var KIND_DISCRIMINANT = {
85
+ Application: 1,
86
+ Commit: 2,
87
+ Welcome: 3,
88
+ Proposal: 4,
89
+ KeyPackage: 5
90
+ };
91
+ async function sha256(bytes) {
92
+ const ab = bytes.buffer.slice(bytes.byteOffset, bytes.byteOffset + bytes.byteLength);
93
+ const hash = await crypto.subtle.digest("SHA-256", ab);
94
+ return new Uint8Array(hash);
95
+ }
96
+ function bytesEqual(a, b) {
97
+ if (a.length !== b.length) return false;
98
+ for (let i = 0; i < a.length; i++) if (a[i] !== b[i]) return false;
99
+ return true;
100
+ }
101
+ async function validate(env) {
102
+ if (env.v !== PING_WIRE_CONTRACT_VERSION) {
103
+ throw new ValidationError(
104
+ "UnsupportedVersion",
105
+ `unsupported wire-contract version: envelope says v=${env.v}, expected v=${PING_WIRE_CONTRACT_VERSION}`
106
+ );
107
+ }
108
+ if (env.conversation_id.length !== 16) {
109
+ throw new ValidationError(
110
+ "BadConversationIdLen",
111
+ `conversation_id must be 16 bytes (ULID), got ${env.conversation_id.length}`
112
+ );
113
+ }
114
+ if (env.sender_device.length !== 32) {
115
+ throw new ValidationError(
116
+ "BadSenderDeviceLen",
117
+ `sender_device must be 32 bytes, got ${env.sender_device.length}`
118
+ );
119
+ }
120
+ const buf = new Uint8Array(1 + env.payload.length);
121
+ buf[0] = KIND_DISCRIMINANT[env.kind];
122
+ buf.set(env.payload, 1);
123
+ const computed = await sha256(buf);
124
+ if (!bytesEqual(computed, env.content_hash)) {
125
+ throw new ValidationError(
126
+ "ContentHashMismatch",
127
+ "content_hash mismatch: envelope is forged, corrupted, or built by a non-conformant sender"
128
+ );
129
+ }
130
+ }
131
+ function negotiates(contentType) {
132
+ if (!contentType) return false;
133
+ const primary = contentType.split(";")[0]?.trim().toLowerCase() ?? "";
134
+ return primary === CONTENT_TYPE_CBOR || primary === CONTENT_TYPE_JSON;
135
+ }
136
+
70
137
  // src/transport/websocket.ts
71
138
  var WebSocketTransport = class {
72
139
  constructor(cfg) {
@@ -97,7 +164,8 @@ var WebSocketTransport = class {
97
164
  const res = await this.fetchImpl(`${this.cfg.baseUrl}/send`, {
98
165
  method: "POST",
99
166
  headers: {
100
- "content-type": "application/json",
167
+ "content-type": CONTENT_TYPE_JSON,
168
+ [PING_WIRE_CONTRACT_HEADER]: PING_WIRE_CONTRACT_HEADER_VALUE,
101
169
  ...this.cfg.authHeader ? { authorization: this.cfg.authHeader } : {}
102
170
  },
103
171
  body: JSON.stringify(envelope, replacer)
@@ -426,8 +494,16 @@ function sameBytesU8(a, b) {
426
494
  return true;
427
495
  }
428
496
  export {
497
+ CONTENT_TYPE_CBOR,
498
+ CONTENT_TYPE_JSON,
429
499
  IndexedDbStorage,
430
500
  MessagingClient,
431
- WebSocketTransport
501
+ PING_WIRE_CONTRACT_HEADER,
502
+ PING_WIRE_CONTRACT_HEADER_VALUE,
503
+ PING_WIRE_CONTRACT_VERSION,
504
+ ValidationError,
505
+ WebSocketTransport,
506
+ negotiates,
507
+ validate
432
508
  };
433
509
  //# sourceMappingURL=index.js.map
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/storage/indexeddb.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","// Reference WebSocket transport. Treats the server as an opaque relay that:\n// - accepts a CBOR-encoded MessageEnvelope on `send`\n// - returns events newer than a cursor on `fetchSince`\n// - streams new events on the WS itself for `subscribe`\n// - exposes a directory endpoint for `discoverDevices`\n//\n// This is a sample. Real deployments will tune framing / auth.\n\nimport type { ConversationId, DiscoveredDevice, MessageEnvelope, Transport, UserId } from \"../types\";\n\nexport interface WsTransportConfig {\n baseUrl: string; // ws[s]://...\n authHeader?: string; // optional bearer token\n fetchImpl?: typeof fetch; // override for SSR / tests\n}\n\nexport class WebSocketTransport implements Transport {\n private ws: WebSocket | null = null;\n private liveHandlers = new Set<(env: MessageEnvelope) => void>();\n private fetchImpl: typeof fetch;\n\n constructor(private cfg: WsTransportConfig) {\n this.fetchImpl = cfg.fetchImpl ?? globalThis.fetch.bind(globalThis);\n this.openSocket();\n }\n\n private openSocket() {\n if (typeof WebSocket === \"undefined\") return;\n const url = `${this.cfg.baseUrl.replace(/^http/, \"ws\")}/stream`;\n this.ws = new WebSocket(url);\n this.ws.onmessage = (ev) => {\n // Server sends one JSON envelope per text frame.\n const text = typeof ev.data === \"string\" ? ev.data : new TextDecoder().decode(ev.data as ArrayBuffer);\n try {\n const env = JSON.parse(text, reviver) as MessageEnvelope;\n for (const h of this.liveHandlers) h(env);\n } catch (e) {\n if (typeof console !== \"undefined\") console.debug(\"[ping-sdk] WS frame parse error:\", e);\n }\n };\n this.ws.onclose = () => setTimeout(() => this.openSocket(), 1000);\n }\n\n async send(envelope: MessageEnvelope): Promise<void> {\n const res = await this.fetchImpl(`${this.cfg.baseUrl}/send`, {\n method: \"POST\",\n headers: {\n \"content-type\": \"application/json\",\n ...(this.cfg.authHeader ? { authorization: this.cfg.authHeader } : {}),\n },\n body: JSON.stringify(envelope, replacer),\n });\n if (res.status === 409) throw new Error(\"EpochOccupied\");\n if (!res.ok) throw new Error(`transport send failed: ${res.status}`);\n }\n\n async fetchSince(conversationId: ConversationId, cursor: Uint8Array, limit: number): Promise<MessageEnvelope[]> {\n const url = new URL(`${this.cfg.baseUrl}/sync`);\n url.searchParams.set(\"conv\", toHex(conversationId));\n url.searchParams.set(\"cursor\", btoaUrl(cursor));\n url.searchParams.set(\"limit\", String(limit));\n const res = await this.fetchImpl(url.toString(), {\n headers: { ...(this.cfg.authHeader ? { authorization: this.cfg.authHeader } : {}) },\n });\n if (!res.ok) throw new Error(`transport fetch failed: ${res.status}`);\n const text = await res.text();\n return JSON.parse(text, reviver) as MessageEnvelope[];\n }\n\n subscribe(onEvent: (env: MessageEnvelope) => void): { unsubscribe(): void } {\n this.liveHandlers.add(onEvent);\n return { unsubscribe: () => this.liveHandlers.delete(onEvent) };\n }\n\n async discoverDevices(userId: UserId): Promise<DiscoveredDevice[]> {\n const res = await this.fetchImpl(`${this.cfg.baseUrl}/devices/${toHex(userId)}`, {\n headers: { ...(this.cfg.authHeader ? { authorization: this.cfg.authHeader } : {}) },\n });\n if (!res.ok) throw new Error(`device discovery failed: ${res.status}`);\n const text = await res.text();\n return JSON.parse(text, reviver) as DiscoveredDevice[];\n }\n}\n\n// ---- Wire codec ----\n//\n// Demo wire format: JSON with byte fields wrapped as `{ _bytes: number[] }`. The relay's\n// `envFromJson`/`envToJson` use the same shape. The spec calls for CBOR; v0.2 swaps in a\n// real CBOR codec, but the JSON form is much friendlier to debug.\n//\n// (decodeEnvelope helpers used to live here; collapsed into inline JSON.parse calls above.)\n\nfunction replacer(_k: string, v: unknown): unknown {\n if (v instanceof Uint8Array) return { _bytes: Array.from(v) };\n return v;\n}\nfunction reviver(_k: string, v: unknown): unknown {\n if (v && typeof v === \"object\" && Array.isArray((v as { _bytes?: number[] })._bytes)) {\n return new Uint8Array((v as { _bytes: number[] })._bytes);\n }\n return v;\n}\n\nfunction toHex(b: Uint8Array): string {\n let s = \"\"; for (const v of b) s += v.toString(16).padStart(2, \"0\"); return s;\n}\nfunction btoaUrl(b: Uint8Array): string {\n let s = \"\"; for (const v of b) s += String.fromCharCode(v);\n return btoa(s).replace(/\\+/g, \"-\").replace(/\\//g, \"_\").replace(/=+$/, \"\");\n}\n","// Public entry point. Spawns the dedicated Worker that hosts the WASM instance and exposes a\n// Promise-based, identical-to-native API. Crypto runs off the main thread.\n\nimport type {\n ConversationId, ConversationMeta, DeviceId, IncomingMessage,\n LinkingTicket, MessageEnvelope, Storage, Transport, UserId,\n} from \"./types\";\nimport type { Request, Response } from \"./protocol\";\n\nexport type * from \"./types\";\n\n// Re-export the default Storage and Transport implementations from the main entry so consumers\n// whose bundlers don't honor package.json#exports (Metro with strict resolution, some legacy\n// Webpack configs) can still get them with a plain `import { ... } from \"ping-openmls-sdk\"`. Power\n// users who care about tree-shaking can keep using the subpath imports.\nexport { IndexedDbStorage } from \"./storage/indexeddb\";\nexport { WebSocketTransport } from \"./transport/websocket\";\n\nexport interface ClientConfig {\n identityExport: Uint8Array;\n deviceLabel: string;\n storage: Storage;\n transport: Transport;\n /** Override the wall clock (test/sim only). */\n now?: () => number;\n}\n\nexport interface Conversation {\n readonly id: ConversationId;\n send(plaintext: Uint8Array): Promise<MessageEnvelope>;\n addMembers(keyPackages: Uint8Array[]): Promise<void>;\n removeMembers(leafIndexes: number[]): Promise<void>;\n meta(): ConversationMeta;\n}\n\nexport class MessagingClient {\n private worker: Worker;\n private nextId = 1;\n private pending = new Map<number, { resolve: (v: unknown) => void; reject: (e: Error) => void }>();\n private storage: Storage;\n private transport: Transport;\n private now: () => number;\n private messageHandlers = new Set<(m: IncomingMessage) => void>();\n private conversationHandlers = new Set<(id: ConversationId, epoch: number, sender?: DeviceId) => void>();\n private subscription: { unsubscribe(): void } | null = null;\n private metaCache = new Map<string, ConversationMeta>();\n // Cached at init() so dispatchIncoming can skip envelopes we sent ourselves — the relay\n // broadcasts to all WS subscribers including the sender, and re-applying our own already-\n // merged commits would error with \"epoch differs\".\n private ownDeviceId: Uint8Array | null = null;\n\n private constructor(cfg: ClientConfig, worker: Worker) {\n this.worker = worker;\n this.storage = cfg.storage;\n this.transport = cfg.transport;\n this.now = cfg.now ?? (() => Date.now());\n this.worker.onmessage = (ev) => this.handleWorker(ev.data as Response);\n }\n\n /** Generate a fresh identity. The returned bytes are a secret — store them encrypted. */\n static async generateIdentity(): Promise<Uint8Array> {\n // Spawn an ephemeral worker to call `generateIdentity` so we don't pull WASM into the main thread.\n const w = new Worker(new URL(\"./worker.js\", import.meta.url), { type: \"module\" });\n const id = 1;\n return new Promise<Uint8Array>((resolve, reject) => {\n w.onmessage = (ev) => {\n const msg = ev.data as Response;\n if (msg.kind === \"result\" && msg.id === id) {\n w.terminate();\n msg.ok ? resolve(msg.value as Uint8Array) : reject(new Error(msg.error));\n }\n };\n const req: Request = { kind: \"generateIdentity\", id };\n w.postMessage(req);\n });\n }\n\n static async init(cfg: ClientConfig): Promise<MessagingClient> {\n const worker = new Worker(new URL(\"./worker.js\", import.meta.url), { type: \"module\" });\n const client = new MessagingClient(cfg, worker);\n\n await client.call({\n kind: \"init\",\n id: client.nextId++,\n identityExport: cfg.identityExport,\n deviceLabel: cfg.deviceLabel,\n nowMs: client.now(),\n });\n\n // Cache the device id once so we can drop envelopes we sent ourselves on the WS echo.\n client.ownDeviceId = await client.deviceId();\n\n // Start the live subscription. We dispatch each envelope to the right handler:\n // - Welcome envelopes for unknown conversations → joinConversation\n // - Welcomes for already-joined conversations → drop (server may broadcast duplicates)\n // - Welcomes addressed to other devices → drop (we can't decrypt)\n // - Everything else → processEnvelope\n client.subscription = cfg.transport.subscribe((envelope) => {\n void client.dispatchIncoming(envelope);\n });\n return client;\n }\n\n /** Internal: route an inbound envelope to the right handler. */\n private async dispatchIncoming(envelope: MessageEnvelope): Promise<void> {\n // The relay broadcasts every envelope to every WS subscriber, including the sender. For\n // our own sends, the local MLS state has already been advanced in-process — re-applying\n // would either fail with \"epoch differs\" (commits) or be a no-op dedupe (application\n // messages). Skip them here.\n if (this.ownDeviceId && sameBytesU8(envelope.sender_device, this.ownDeviceId)) {\n return;\n }\n if (envelope.kind === \"Welcome\") {\n try {\n const known = await this.listConversations();\n const already = known.some((m) => sameBytesU8(m.id, envelope.conversation_id));\n if (already) return;\n await this.joinConversation(envelope);\n } catch (e) {\n // Welcomes are broadcast to every connected client; only one device's KeyPackage can\n // decrypt each one, so most fail. Log at debug, drop silently.\n if (typeof console !== \"undefined\") console.debug(\"[ping-sdk] welcome ignored:\", e);\n }\n return;\n }\n try {\n await this.processEnvelope(envelope);\n } catch (e) {\n if (typeof console !== \"undefined\") console.debug(\"[ping-sdk] envelope drop:\", e);\n }\n }\n\n // ------------------------ Public API ------------------------\n\n async userId(): Promise<UserId> {\n return await this.call({ kind: \"userId\", id: this.nextId++ }) as UserId;\n }\n\n async deviceId(): Promise<DeviceId> {\n return await this.call({ kind: \"deviceId\", id: this.nextId++ }) as DeviceId;\n }\n\n async freshKeyPackage(): Promise<Uint8Array> {\n return await this.call({ kind: \"freshKeyPackage\", id: this.nextId++ }) as Uint8Array;\n }\n\n async createConversation(opts: { name?: string } = {}): Promise<Conversation> {\n const idBytes = await this.call({\n kind: \"createConversation\", id: this.nextId++, name: opts.name ?? null, nowMs: this.now(),\n }) as ConversationId;\n return this.conversationProxy(idBytes);\n }\n\n async joinConversation(welcome: MessageEnvelope): Promise<Conversation> {\n const idBytes = await this.call({\n kind: \"joinConversation\", id: this.nextId++, welcome, nowMs: this.now(),\n }) as ConversationId;\n return this.conversationProxy(idBytes);\n }\n\n /**\n * Get a `Conversation` proxy for an already-known conversation. Use this when you have a\n * conversation id (e.g., from `listConversations()`) and want to send/addMembers without\n * re-creating it. Throws if the id isn't known to the worker — call `listConversations()`\n * first to verify membership.\n */\n getConversation(id: ConversationId): Conversation {\n return this.conversationProxy(id);\n }\n\n async listConversations(): Promise<ConversationMeta[]> {\n const metas = await this.call({ kind: \"listConversations\", id: this.nextId++ }) as ConversationMeta[];\n this.metaCache.clear();\n for (const m of metas) this.metaCache.set(hex(m.id), m);\n return metas;\n }\n\n async syncConversations(): Promise<IncomingMessage[]> {\n return await this.call({\n kind: \"syncConversations\", id: this.nextId++, nowMs: this.now(),\n }) as IncomingMessage[];\n }\n\n async processEnvelope(envelope: MessageEnvelope): Promise<IncomingMessage | null> {\n return await this.call({\n kind: \"processEnvelope\", id: this.nextId++, envelope, nowMs: this.now(),\n }) as IncomingMessage | null;\n }\n\n async buildLinkingTicket(newDeviceId: DeviceId, newDeviceKp: Uint8Array): Promise<LinkingTicket> {\n return await this.call({\n kind: \"buildLinkingTicket\", id: this.nextId++,\n newDeviceId, newDeviceKp, nowMs: this.now(),\n }) as LinkingTicket;\n }\n\n async consumeLinkingTicket(ticket: LinkingTicket): Promise<void> {\n await this.call({\n kind: \"consumeLinkingTicket\", id: this.nextId++, ticket, nowMs: this.now(),\n });\n }\n\n /** Subscribe to decrypted application messages. */\n onMessage(handler: (m: IncomingMessage) => void): () => void {\n this.messageHandlers.add(handler);\n return () => this.messageHandlers.delete(handler);\n }\n\n /** Fired after every state-changing event for a conversation (Commit, member changes). */\n /**\n * Fires after every conversation state change — joining via Welcome, processing a Commit,\n * etc. `sender` carries the device id of whoever triggered the event when known (e.g. the\n * inviter's device on auto-join), so the host can label peers in its UI.\n */\n onConversationUpdated(handler: (id: ConversationId, epoch: number, sender?: DeviceId) => void): () => void {\n this.conversationHandlers.add(handler);\n return () => this.conversationHandlers.delete(handler);\n }\n\n async close(): Promise<void> {\n this.subscription?.unsubscribe();\n this.worker.terminate();\n }\n\n // ------------------------ Internals ------------------------\n\n private conversationProxy(id: ConversationId): Conversation {\n const self = this;\n return {\n id,\n async send(plaintext) {\n return await self.call({\n kind: \"send\", id: self.nextId++, conv: id, plaintext, nowMs: self.now(),\n }) as MessageEnvelope;\n },\n async addMembers(kps) {\n await self.call({\n kind: \"addMembers\", id: self.nextId++, conv: id, kps, nowMs: self.now(),\n });\n },\n async removeMembers(leaves) {\n await self.call({\n kind: \"removeMembers\", id: self.nextId++, conv: id, leaves, nowMs: self.now(),\n });\n },\n meta() {\n const cached = self.metaCache.get(hex(id));\n if (!cached) throw new Error(\"conversation meta not loaded; call listConversations() first\");\n return cached;\n },\n };\n }\n\n private call(req: Request): Promise<unknown> {\n return new Promise((resolve, reject) => {\n this.pending.set((req as { id: number }).id, { resolve, reject });\n this.worker.postMessage(req);\n });\n }\n\n private async handleWorker(msg: Response) {\n switch (msg.kind) {\n case \"result\": {\n const slot = this.pending.get(msg.id);\n if (!slot) return;\n this.pending.delete(msg.id);\n msg.ok ? slot.resolve(msg.value) : slot.reject(new Error(msg.error));\n return;\n }\n case \"event.message\": {\n for (const h of this.messageHandlers) h(msg.msg);\n return;\n }\n case \"event.conversation\": {\n for (const h of this.conversationHandlers) h(msg.id, msg.epoch, msg.sender);\n return;\n }\n case \"storage.call\": {\n try {\n const v = await (this.storage as unknown as Record<string, (...a: unknown[]) => unknown>)\n [msg.method]?.(...msg.args);\n this.worker.postMessage({ kind: \"storage.reply\", id: msg.id, ok: true, value: v } as Request);\n } catch (e) {\n this.worker.postMessage({\n kind: \"storage.reply\", id: msg.id, ok: false, error: String(e),\n } as Request);\n }\n return;\n }\n case \"transport.call\": {\n try {\n const v = await (this.transport as unknown as Record<string, (...a: unknown[]) => unknown>)\n [msg.method]?.(...msg.args);\n this.worker.postMessage({ kind: \"transport.reply\", id: msg.id, ok: true, value: v } as Request);\n } catch (e) {\n this.worker.postMessage({\n kind: \"transport.reply\", id: msg.id, ok: false, error: String(e),\n } as Request);\n }\n return;\n }\n }\n }\n}\n\nfunction hex(b: Uint8Array): string {\n let s = \"\";\n for (const v of b) s += v.toString(16).padStart(2, \"0\");\n return s;\n}\n\nfunction sameBytesU8(a: Uint8Array, b: Uint8Array): boolean {\n if (a.length !== b.length) return false;\n for (let i = 0; i < a.length; i++) if (a[i] !== b[i]) return false;\n return true;\n}\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../src/storage/indexeddb.ts","../src/wire-contract.ts","../src/transport/websocket.ts","../src/index.ts"],"sourcesContent":["// Default IndexedDB-backed Storage implementation. Hosts can supply their own.\n\nimport type { Storage } from \"../types\";\n\nconst DB_NAME = \"ping-sdk\";\nconst DB_VERSION = 1;\nconst STORE = \"kv\";\n\ninterface Row { ns: string; key: string; value: Uint8Array }\n\nfunction open(): Promise<IDBDatabase> {\n return new Promise((resolve, reject) => {\n const req = indexedDB.open(DB_NAME, DB_VERSION);\n req.onupgradeneeded = () => {\n const db = req.result;\n if (!db.objectStoreNames.contains(STORE)) {\n const os = db.createObjectStore(STORE, { keyPath: [\"ns\", \"key\"] });\n os.createIndex(\"ns\", \"ns\", { unique: false });\n }\n };\n req.onsuccess = () => resolve(req.result);\n req.onerror = () => reject(req.error);\n });\n}\n\nexport class IndexedDbStorage implements Storage {\n private dbPromise: Promise<IDBDatabase>;\n\n constructor() { this.dbPromise = open(); }\n\n async get(namespace: string, key: string): Promise<Uint8Array | null> {\n const db = await this.dbPromise;\n return new Promise((resolve, reject) => {\n const tx = db.transaction(STORE, \"readonly\");\n const r = tx.objectStore(STORE).get([namespace, key]);\n r.onsuccess = () => resolve(((r.result as Row | undefined)?.value) ?? null);\n r.onerror = () => reject(r.error);\n });\n }\n\n async put(namespace: string, key: string, value: Uint8Array): Promise<void> {\n const db = await this.dbPromise;\n return new Promise((resolve, reject) => {\n const tx = db.transaction(STORE, \"readwrite\");\n tx.objectStore(STORE).put({ ns: namespace, key, value } as Row);\n tx.oncomplete = () => resolve();\n tx.onerror = () => reject(tx.error);\n });\n }\n\n async del(namespace: string, key: string): Promise<void> {\n const db = await this.dbPromise;\n return new Promise((resolve, reject) => {\n const tx = db.transaction(STORE, \"readwrite\");\n tx.objectStore(STORE).delete([namespace, key]);\n tx.oncomplete = () => resolve();\n tx.onerror = () => reject(tx.error);\n });\n }\n\n async listKeys(namespace: string, prefix: string): Promise<string[]> {\n const db = await this.dbPromise;\n return new Promise((resolve, reject) => {\n const tx = db.transaction(STORE, \"readonly\");\n const idx = tx.objectStore(STORE).index(\"ns\");\n const req = idx.openCursor(IDBKeyRange.only(namespace));\n const out: string[] = [];\n req.onsuccess = () => {\n const cur = req.result;\n if (!cur) return resolve(out);\n const row = cur.value as Row;\n if (!prefix || row.key.startsWith(prefix)) out.push(row.key);\n cur.continue();\n };\n req.onerror = () => reject(req.error);\n });\n }\n}\n","// Ping wire contract — TypeScript counterpart to the `ping-wire-contract` Rust crate.\n//\n// MUST stay in lockstep with `core/ping-wire-contract/src/lib.rs`. The full normative spec\n// lives in `docs/PING_WIRE_CONTRACT.md`.\n//\n// What this module is for: code paths that cross the network boundary (transports, relays,\n// service workers) that need to advertise or validate ping-wire-contract conformance from\n// JavaScript without round-tripping through WASM.\n\nimport type { MessageEnvelope } from \"./types\";\n\n/** 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":"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","names":[]}
@@ -1,4 +1,4 @@
1
- import { S as Storage } from '../types-COHY5NU8.cjs';
1
+ import { S as Storage } from '../types-jd8CLdi_.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-COHY5NU8.js';
1
+ import { S as Storage } from '../types-jd8CLdi_.js';
2
2
 
3
3
  declare class IndexedDbStorage implements Storage {
4
4
  private dbPromise;
@@ -23,6 +23,14 @@ __export(websocket_exports, {
23
23
  WebSocketTransport: () => WebSocketTransport
24
24
  });
25
25
  module.exports = __toCommonJS(websocket_exports);
26
+
27
+ // src/wire-contract.ts
28
+ var PING_WIRE_CONTRACT_VERSION = 1;
29
+ var CONTENT_TYPE_JSON = "application/vnd.ping.wire.v1+json";
30
+ var PING_WIRE_CONTRACT_HEADER = "X-Ping-Wire-Contract";
31
+ var PING_WIRE_CONTRACT_HEADER_VALUE = `ping/${PING_WIRE_CONTRACT_VERSION}`;
32
+
33
+ // src/transport/websocket.ts
26
34
  var WebSocketTransport = class {
27
35
  constructor(cfg) {
28
36
  this.cfg = cfg;
@@ -52,7 +60,8 @@ var WebSocketTransport = class {
52
60
  const res = await this.fetchImpl(`${this.cfg.baseUrl}/send`, {
53
61
  method: "POST",
54
62
  headers: {
55
- "content-type": "application/json",
63
+ "content-type": CONTENT_TYPE_JSON,
64
+ [PING_WIRE_CONTRACT_HEADER]: PING_WIRE_CONTRACT_HEADER_VALUE,
56
65
  ...this.cfg.authHeader ? { authorization: this.cfg.authHeader } : {}
57
66
  },
58
67
  body: JSON.stringify(envelope, replacer)
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/transport/websocket.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\";\n\nexport interface WsTransportConfig {\n baseUrl: string; // ws[s]://...\n authHeader?: string; // optional bearer token\n fetchImpl?: typeof fetch; // override for SSR / tests\n}\n\nexport class WebSocketTransport implements Transport {\n private ws: WebSocket | null = null;\n private liveHandlers = new Set<(env: MessageEnvelope) => void>();\n private fetchImpl: typeof fetch;\n\n constructor(private cfg: WsTransportConfig) {\n this.fetchImpl = cfg.fetchImpl ?? globalThis.fetch.bind(globalThis);\n this.openSocket();\n }\n\n private openSocket() {\n if (typeof WebSocket === \"undefined\") return;\n const url = `${this.cfg.baseUrl.replace(/^http/, \"ws\")}/stream`;\n this.ws = new WebSocket(url);\n this.ws.onmessage = (ev) => {\n // Server sends one JSON envelope per text frame.\n const text = typeof ev.data === \"string\" ? ev.data : new TextDecoder().decode(ev.data as ArrayBuffer);\n try {\n const env = JSON.parse(text, reviver) as MessageEnvelope;\n for (const h of this.liveHandlers) h(env);\n } catch (e) {\n if (typeof console !== \"undefined\") console.debug(\"[ping-sdk] WS frame parse error:\", e);\n }\n };\n this.ws.onclose = () => setTimeout(() => this.openSocket(), 1000);\n }\n\n async send(envelope: MessageEnvelope): Promise<void> {\n const res = await this.fetchImpl(`${this.cfg.baseUrl}/send`, {\n method: \"POST\",\n headers: {\n \"content-type\": \"application/json\",\n ...(this.cfg.authHeader ? { authorization: this.cfg.authHeader } : {}),\n },\n body: JSON.stringify(envelope, replacer),\n });\n if (res.status === 409) throw new Error(\"EpochOccupied\");\n if (!res.ok) throw new Error(`transport send failed: ${res.status}`);\n }\n\n async fetchSince(conversationId: ConversationId, cursor: Uint8Array, limit: number): Promise<MessageEnvelope[]> {\n const url = new URL(`${this.cfg.baseUrl}/sync`);\n url.searchParams.set(\"conv\", toHex(conversationId));\n url.searchParams.set(\"cursor\", btoaUrl(cursor));\n url.searchParams.set(\"limit\", String(limit));\n const res = await this.fetchImpl(url.toString(), {\n headers: { ...(this.cfg.authHeader ? { authorization: this.cfg.authHeader } : {}) },\n });\n if (!res.ok) throw new Error(`transport fetch failed: ${res.status}`);\n const text = await res.text();\n return JSON.parse(text, reviver) as MessageEnvelope[];\n }\n\n subscribe(onEvent: (env: MessageEnvelope) => void): { unsubscribe(): void } {\n this.liveHandlers.add(onEvent);\n return { unsubscribe: () => this.liveHandlers.delete(onEvent) };\n }\n\n async discoverDevices(userId: UserId): Promise<DiscoveredDevice[]> {\n const res = await this.fetchImpl(`${this.cfg.baseUrl}/devices/${toHex(userId)}`, {\n headers: { ...(this.cfg.authHeader ? { authorization: this.cfg.authHeader } : {}) },\n });\n if (!res.ok) throw new Error(`device discovery failed: ${res.status}`);\n const text = await res.text();\n return JSON.parse(text, reviver) as DiscoveredDevice[];\n }\n}\n\n// ---- Wire codec ----\n//\n// Demo wire format: JSON with byte fields wrapped as `{ _bytes: number[] }`. The relay's\n// `envFromJson`/`envToJson` use the same shape. The spec calls for CBOR; v0.2 swaps in a\n// real CBOR codec, but the JSON form is much friendlier to debug.\n//\n// (decodeEnvelope helpers used to live here; collapsed into inline JSON.parse calls above.)\n\nfunction replacer(_k: string, v: unknown): unknown {\n if (v instanceof Uint8Array) return { _bytes: Array.from(v) };\n return v;\n}\nfunction reviver(_k: string, v: unknown): unknown {\n if (v && typeof v === \"object\" && Array.isArray((v as { _bytes?: number[] })._bytes)) {\n return new Uint8Array((v as { _bytes: number[] })._bytes);\n }\n return v;\n}\n\nfunction toHex(b: Uint8Array): string {\n let s = \"\"; for (const v of b) s += v.toString(16).padStart(2, \"0\"); return s;\n}\nfunction btoaUrl(b: Uint8Array): string {\n let s = \"\"; for (const v of b) s += String.fromCharCode(v);\n return btoa(s).replace(/\\+/g, \"-\").replace(/\\//g, \"_\").replace(/=+$/, \"\");\n}\n"],"mappings":"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","names":[]}
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,4 +1,4 @@
1
- import { T as Transport, M as MessageEnvelope, C as ConversationId, U as UserId, c as DiscoveredDevice } from '../types-COHY5NU8.cjs';
1
+ import { T as Transport, M as MessageEnvelope, C as ConversationId, U as UserId, D as DiscoveredDevice } from '../types-jd8CLdi_.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, c as DiscoveredDevice } from '../types-COHY5NU8.js';
1
+ import { T as Transport, M as MessageEnvelope, C as ConversationId, U as UserId, D as DiscoveredDevice } from '../types-jd8CLdi_.js';
2
2
 
3
3
  interface WsTransportConfig {
4
4
  baseUrl: string;
@@ -1,3 +1,9 @@
1
+ // src/wire-contract.ts
2
+ var PING_WIRE_CONTRACT_VERSION = 1;
3
+ var CONTENT_TYPE_JSON = "application/vnd.ping.wire.v1+json";
4
+ var PING_WIRE_CONTRACT_HEADER = "X-Ping-Wire-Contract";
5
+ var PING_WIRE_CONTRACT_HEADER_VALUE = `ping/${PING_WIRE_CONTRACT_VERSION}`;
6
+
1
7
  // src/transport/websocket.ts
2
8
  var WebSocketTransport = class {
3
9
  constructor(cfg) {
@@ -28,7 +34,8 @@ var WebSocketTransport = class {
28
34
  const res = await this.fetchImpl(`${this.cfg.baseUrl}/send`, {
29
35
  method: "POST",
30
36
  headers: {
31
- "content-type": "application/json",
37
+ "content-type": CONTENT_TYPE_JSON,
38
+ [PING_WIRE_CONTRACT_HEADER]: PING_WIRE_CONTRACT_HEADER_VALUE,
32
39
  ...this.cfg.authHeader ? { authorization: this.cfg.authHeader } : {}
33
40
  },
34
41
  body: JSON.stringify(envelope, replacer)
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/transport/websocket.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\";\n\nexport interface WsTransportConfig {\n baseUrl: string; // ws[s]://...\n authHeader?: string; // optional bearer token\n fetchImpl?: typeof fetch; // override for SSR / tests\n}\n\nexport class WebSocketTransport implements Transport {\n private ws: WebSocket | null = null;\n private liveHandlers = new Set<(env: MessageEnvelope) => void>();\n private fetchImpl: typeof fetch;\n\n constructor(private cfg: WsTransportConfig) {\n this.fetchImpl = cfg.fetchImpl ?? globalThis.fetch.bind(globalThis);\n this.openSocket();\n }\n\n private openSocket() {\n if (typeof WebSocket === \"undefined\") return;\n const url = `${this.cfg.baseUrl.replace(/^http/, \"ws\")}/stream`;\n this.ws = new WebSocket(url);\n this.ws.onmessage = (ev) => {\n // Server sends one JSON envelope per text frame.\n const text = typeof ev.data === \"string\" ? ev.data : new TextDecoder().decode(ev.data as ArrayBuffer);\n try {\n const env = JSON.parse(text, reviver) as MessageEnvelope;\n for (const h of this.liveHandlers) h(env);\n } catch (e) {\n if (typeof console !== \"undefined\") console.debug(\"[ping-sdk] WS frame parse error:\", e);\n }\n };\n this.ws.onclose = () => setTimeout(() => this.openSocket(), 1000);\n }\n\n async send(envelope: MessageEnvelope): Promise<void> {\n const res = await this.fetchImpl(`${this.cfg.baseUrl}/send`, {\n method: \"POST\",\n headers: {\n \"content-type\": \"application/json\",\n ...(this.cfg.authHeader ? { authorization: this.cfg.authHeader } : {}),\n },\n body: JSON.stringify(envelope, replacer),\n });\n if (res.status === 409) throw new Error(\"EpochOccupied\");\n if (!res.ok) throw new Error(`transport send failed: ${res.status}`);\n }\n\n async fetchSince(conversationId: ConversationId, cursor: Uint8Array, limit: number): Promise<MessageEnvelope[]> {\n const url = new URL(`${this.cfg.baseUrl}/sync`);\n url.searchParams.set(\"conv\", toHex(conversationId));\n url.searchParams.set(\"cursor\", btoaUrl(cursor));\n url.searchParams.set(\"limit\", String(limit));\n const res = await this.fetchImpl(url.toString(), {\n headers: { ...(this.cfg.authHeader ? { authorization: this.cfg.authHeader } : {}) },\n });\n if (!res.ok) throw new Error(`transport fetch failed: ${res.status}`);\n const text = await res.text();\n return JSON.parse(text, reviver) as MessageEnvelope[];\n }\n\n subscribe(onEvent: (env: MessageEnvelope) => void): { unsubscribe(): void } {\n this.liveHandlers.add(onEvent);\n return { unsubscribe: () => this.liveHandlers.delete(onEvent) };\n }\n\n async discoverDevices(userId: UserId): Promise<DiscoveredDevice[]> {\n const res = await this.fetchImpl(`${this.cfg.baseUrl}/devices/${toHex(userId)}`, {\n headers: { ...(this.cfg.authHeader ? { authorization: this.cfg.authHeader } : {}) },\n });\n if (!res.ok) throw new Error(`device discovery failed: ${res.status}`);\n const text = await res.text();\n return JSON.parse(text, reviver) as DiscoveredDevice[];\n }\n}\n\n// ---- Wire codec ----\n//\n// Demo wire format: JSON with byte fields wrapped as `{ _bytes: number[] }`. The relay's\n// `envFromJson`/`envToJson` use the same shape. The spec calls for CBOR; v0.2 swaps in a\n// real CBOR codec, but the JSON form is much friendlier to debug.\n//\n// (decodeEnvelope helpers used to live here; collapsed into inline JSON.parse calls above.)\n\nfunction replacer(_k: string, v: unknown): unknown {\n if (v instanceof Uint8Array) return { _bytes: Array.from(v) };\n return v;\n}\nfunction reviver(_k: string, v: unknown): unknown {\n if (v && typeof v === \"object\" && Array.isArray((v as { _bytes?: number[] })._bytes)) {\n return new Uint8Array((v as { _bytes: number[] })._bytes);\n }\n return v;\n}\n\nfunction toHex(b: Uint8Array): string {\n let s = \"\"; for (const v of b) s += v.toString(16).padStart(2, \"0\"); return s;\n}\nfunction btoaUrl(b: Uint8Array): string {\n let s = \"\"; for (const v of b) s += String.fromCharCode(v);\n return btoa(s).replace(/\\+/g, \"-\").replace(/\\//g, \"_\").replace(/=+$/, \"\");\n}\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../../src/wire-contract.ts","../../src/transport/websocket.ts"],"sourcesContent":["// 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"],"mappings":"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","names":[]}
@@ -71,4 +71,4 @@ interface Transport {
71
71
  discoverDevices(userId: UserId): Promise<DiscoveredDevice[]>;
72
72
  }
73
73
 
74
- export type { Bytes as B, ConversationId as C, DeviceId as D, Hlc as H, IncomingMessage as I, LinkingTicket as L, MessageEnvelope as M, Storage as S, Transport as T, UserId as U, ConversationMeta as a, DeviceInfo as b, DiscoveredDevice as c, MessageKind as d };
74
+ export type { Bytes as B, ConversationId as C, DiscoveredDevice as D, Hlc as H, IncomingMessage as I, LinkingTicket as L, MessageEnvelope as M, Storage as S, Transport as T, UserId as U, ConversationMeta as a, DeviceId as b, DeviceInfo as c, MessageKind as d };
@@ -71,4 +71,4 @@ interface Transport {
71
71
  discoverDevices(userId: UserId): Promise<DiscoveredDevice[]>;
72
72
  }
73
73
 
74
- export type { Bytes as B, ConversationId as C, DeviceId as D, Hlc as H, IncomingMessage as I, LinkingTicket as L, MessageEnvelope as M, Storage as S, Transport as T, UserId as U, ConversationMeta as a, DeviceInfo as b, DiscoveredDevice as c, MessageKind as d };
74
+ export type { Bytes as B, ConversationId as C, DiscoveredDevice as D, Hlc as H, IncomingMessage as I, LinkingTicket as L, MessageEnvelope as M, Storage as S, Transport as T, UserId as U, ConversationMeta as a, DeviceId as b, DeviceInfo as c, MessageKind as d };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ping-openmls-sdk",
3
- "version": "0.0.1",
3
+ "version": "0.1.0",
4
4
  "description": "OpenMLS-based secure messaging SDK — runs in browsers, React Native, RN-Web, and Electron/Tauri.",
5
5
  "license": "Apache-2.0",
6
6
  "type": "module",