ping-openmls-sdk 0.1.2 → 0.5.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.
@@ -1,6 +1,6 @@
1
1
  // src/wire-contract.ts
2
- var PING_WIRE_CONTRACT_VERSION = 1;
3
- var CONTENT_TYPE_JSON = "application/vnd.ping.wire.v1+json";
2
+ var PING_WIRE_CONTRACT_VERSION = 2;
3
+ var CONTENT_TYPE_JSON = "application/vnd.ping.wire.v2+json";
4
4
  var PING_WIRE_CONTRACT_HEADER = "X-Ping-Wire-Contract";
5
5
  var PING_WIRE_CONTRACT_HEADER_VALUE = `ping/${PING_WIRE_CONTRACT_VERSION}`;
6
6
 
@@ -1 +1 @@
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":[]}
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/**\n * Ping wire contract version. Bumped on incompatible profile changes.\n *\n * Currently `2` — CR-6 changes the `content_hash` semantics for `Application` envelopes\n * (now hashed over plaintext, not the MLS ciphertext). Receivers run a 90-day dual-accept\n * window per Q-ARCH-02; see [[validate]] for the per-version routing.\n */\nexport const PING_WIRE_CONTRACT_VERSION = 2;\n\n/** Lowest envelope version accepted by [[validate]] during the dual-accept window. */\nexport const WIRE_VERSION_MIN_ACCEPTED = 1;\n\n/** Canonical CBOR content type. Production transports SHOULD use this. */\nexport const CONTENT_TYPE_CBOR = \"application/vnd.ping.wire.v2+cbor\";\n\n/** JSON projection — development and tooling only. Production MUST NOT use this. */\nexport const CONTENT_TYPE_JSON = \"application/vnd.ping.wire.v2+json\";\n\n/** Legacy v1 CBOR type, accepted during the CR-6 dual-accept window. */\nexport const CONTENT_TYPE_CBOR_LEGACY_V1 = \"application/vnd.ping.wire.v1+cbor\";\n\n/** Legacy v1 JSON type, accepted during the CR-6 dual-accept window. */\nexport const CONTENT_TYPE_JSON_LEGACY_V1 = \"application/vnd.ping.wire.v1+json\";\n\n/** Header used over transports that don't surface a content type (e.g. WebSocket). */\nexport const PING_WIRE_CONTRACT_HEADER = \"X-Ping-Wire-Contract\";\n\n/** Header value: `ping/<VERSION>`. */\nexport const PING_WIRE_CONTRACT_HEADER_VALUE = `ping/${PING_WIRE_CONTRACT_VERSION}`;\n\nexport type ValidationErrorKind =\n | \"UnsupportedVersion\"\n | \"BadConversationIdLen\"\n | \"BadSenderDeviceLen\"\n | \"ContentHashMismatch\";\n\nexport class ValidationError extends Error {\n constructor(public readonly kind: ValidationErrorKind, message: string) {\n super(message);\n this.name = \"PingWireContractValidationError\";\n }\n}\n\nconst KIND_DISCRIMINANT: Record<MessageEnvelope[\"kind\"], number> = {\n Application: 1,\n Commit: 2,\n Welcome: 3,\n Proposal: 4,\n KeyPackage: 5,\n};\n\nasync function sha256(bytes: Uint8Array): Promise<Uint8Array> {\n // `crypto.subtle` is available in browsers, Node 20+, and Workers. `bytes.buffer`\n // is typed as `ArrayBufferLike` which TS won't accept as `BufferSource` (the union\n // includes `SharedArrayBuffer`); pass an explicit ArrayBuffer slice instead.\n const ab = bytes.buffer.slice(bytes.byteOffset, bytes.byteOffset + bytes.byteLength) as ArrayBuffer;\n const hash = await crypto.subtle.digest(\"SHA-256\", ab);\n return new Uint8Array(hash);\n}\n\nfunction bytesEqual(a: Uint8Array, b: Uint8Array): boolean {\n if (a.length !== b.length) return false;\n for (let i = 0; i < a.length; i++) if (a[i] !== b[i]) return false;\n return true;\n}\n\n/**\n * Validate an envelope against the ping-wire-contract.\n *\n * Receivers claiming contract conformance MUST call this before applying any envelope.\n * External consumers using the raw envelope under their own profile are not bound by\n * these rules.\n *\n * CR-6 dual-accept window: `v ∈ [WIRE_VERSION_MIN_ACCEPTED, PING_WIRE_CONTRACT_VERSION]`\n * passes the version gate. For v=2 `Application` envelopes the content_hash is over\n * plaintext, so this function skips that check — callers MUST run\n * [[verifyApplicationContentHash]] after MLS decrypt to close the loop.\n */\nexport async function validate(env: MessageEnvelope): Promise<void> {\n if (env.v < WIRE_VERSION_MIN_ACCEPTED || env.v > PING_WIRE_CONTRACT_VERSION) {\n throw new ValidationError(\n \"UnsupportedVersion\",\n `unsupported wire-contract version: envelope says v=${env.v}, expected v∈[${WIRE_VERSION_MIN_ACCEPTED}, ${PING_WIRE_CONTRACT_VERSION}]`,\n );\n }\n if (env.conversation_id.length !== 16) {\n throw new ValidationError(\n \"BadConversationIdLen\",\n `conversation_id must be 16 bytes (ULID), got ${env.conversation_id.length}`,\n );\n }\n if (env.sender_device.length !== 32) {\n throw new ValidationError(\n \"BadSenderDeviceLen\",\n `sender_device must be 32 bytes, got ${env.sender_device.length}`,\n );\n }\n // CR-6 routing: skip the hash check for v=2 application envelopes.\n if (env.v >= 2 && env.kind === \"Application\") {\n return;\n }\n const buf = new Uint8Array(1 + env.payload.length);\n buf[0] = KIND_DISCRIMINANT[env.kind];\n buf.set(env.payload, 1);\n const computed = await sha256(buf);\n if (!bytesEqual(computed, env.content_hash)) {\n throw new ValidationError(\n \"ContentHashMismatch\",\n \"content_hash mismatch: envelope is forged, corrupted, or built by a non-conformant sender\",\n );\n }\n}\n\n/**\n * Verify a v=2 `Application` envelope's content_hash against the decrypted plaintext.\n *\n * Caller MUST run this after MLS decrypt for application envelopes whose `v >= 2`. For\n * v=1 envelopes or handshake kinds this is a no-op (those are covered by [[validate]]).\n */\nexport async function verifyApplicationContentHash(\n env: MessageEnvelope,\n plaintext: Uint8Array,\n): Promise<void> {\n if (env.v < 2 || env.kind !== \"Application\") return;\n const computed = await sha256(plaintext);\n if (!bytesEqual(computed, env.content_hash)) {\n throw new ValidationError(\n \"ContentHashMismatch\",\n \"v=2 application content_hash mismatch against decrypted plaintext\",\n );\n }\n}\n\n/**\n * True if the supplied content type negotiates the ping-wire-contract.\n *\n * Accepts the v2 canonical types AND the v1 legacy types during the CR-6 dual-accept\n * window. Tolerates whitespace and parameters (e.g. `; charset=utf-8`).\n */\nexport function negotiates(contentType: string | null | undefined): boolean {\n if (!contentType) return false;\n const primary = contentType.split(\";\")[0]?.trim().toLowerCase() ?? \"\";\n return (\n primary === CONTENT_TYPE_CBOR\n || primary === CONTENT_TYPE_JSON\n || primary === CONTENT_TYPE_CBOR_LEGACY_V1\n || primary === CONTENT_TYPE_JSON_LEGACY_V1\n );\n}\n","// Reference WebSocket transport. Treats the server as an opaque relay that:\n// - accepts a CBOR-encoded MessageEnvelope on `send`\n// - returns events newer than a cursor on `fetchSince`\n// - streams new events on the WS itself for `subscribe`\n// - exposes a directory endpoint for `discoverDevices`\n//\n// This is a sample. Real deployments will tune framing / auth.\n\nimport type { ConversationId, DiscoveredDevice, MessageEnvelope, Transport, UserId } from \"../types\";\nimport {\n CONTENT_TYPE_JSON,\n PING_WIRE_CONTRACT_HEADER,\n PING_WIRE_CONTRACT_HEADER_VALUE,\n} from \"../wire-contract\";\n\nexport interface WsTransportConfig {\n baseUrl: string; // ws[s]://...\n authHeader?: string; // optional bearer token\n fetchImpl?: typeof fetch; // override for SSR / tests\n}\n\nexport class WebSocketTransport implements Transport {\n private ws: WebSocket | null = null;\n private liveHandlers = new Set<(env: MessageEnvelope) => void>();\n private fetchImpl: typeof fetch;\n\n constructor(private cfg: WsTransportConfig) {\n this.fetchImpl = cfg.fetchImpl ?? globalThis.fetch.bind(globalThis);\n this.openSocket();\n }\n\n private openSocket() {\n if (typeof WebSocket === \"undefined\") return;\n const url = `${this.cfg.baseUrl.replace(/^http/, \"ws\")}/stream`;\n this.ws = new WebSocket(url);\n this.ws.onmessage = (ev) => {\n // Server sends one JSON envelope per text frame.\n const text = typeof ev.data === \"string\" ? ev.data : new TextDecoder().decode(ev.data as ArrayBuffer);\n try {\n const env = JSON.parse(text, reviver) as MessageEnvelope;\n for (const h of this.liveHandlers) h(env);\n } catch (e) {\n if (typeof console !== \"undefined\") console.debug(\"[ping-sdk] WS frame parse error:\", e);\n }\n };\n this.ws.onclose = () => setTimeout(() => this.openSocket(), 1000);\n }\n\n async send(envelope: MessageEnvelope): Promise<void> {\n // We're emitting the JSON projection of the ping-wire-contract. Production transports\n // would use CONTENT_TYPE_CBOR with a real CBOR codec; for the demo path the JSON shape\n // is the documented development projection.\n const res = await this.fetchImpl(`${this.cfg.baseUrl}/send`, {\n method: \"POST\",\n headers: {\n \"content-type\": CONTENT_TYPE_JSON,\n [PING_WIRE_CONTRACT_HEADER]: PING_WIRE_CONTRACT_HEADER_VALUE,\n ...(this.cfg.authHeader ? { authorization: this.cfg.authHeader } : {}),\n },\n body: JSON.stringify(envelope, replacer),\n });\n if (res.status === 409) throw new Error(\"EpochOccupied\");\n if (!res.ok) throw new Error(`transport send failed: ${res.status}`);\n }\n\n async fetchSince(conversationId: ConversationId, cursor: Uint8Array, limit: number): Promise<MessageEnvelope[]> {\n const url = new URL(`${this.cfg.baseUrl}/sync`);\n url.searchParams.set(\"conv\", toHex(conversationId));\n url.searchParams.set(\"cursor\", btoaUrl(cursor));\n url.searchParams.set(\"limit\", String(limit));\n const res = await this.fetchImpl(url.toString(), {\n headers: { ...(this.cfg.authHeader ? { authorization: this.cfg.authHeader } : {}) },\n });\n if (!res.ok) throw new Error(`transport fetch failed: ${res.status}`);\n const text = await res.text();\n return JSON.parse(text, reviver) as MessageEnvelope[];\n }\n\n subscribe(onEvent: (env: MessageEnvelope) => void): { unsubscribe(): void } {\n this.liveHandlers.add(onEvent);\n return { unsubscribe: () => this.liveHandlers.delete(onEvent) };\n }\n\n async discoverDevices(userId: UserId): Promise<DiscoveredDevice[]> {\n const res = await this.fetchImpl(`${this.cfg.baseUrl}/devices/${toHex(userId)}`, {\n headers: { ...(this.cfg.authHeader ? { authorization: this.cfg.authHeader } : {}) },\n });\n if (!res.ok) throw new Error(`device discovery failed: ${res.status}`);\n const text = await res.text();\n return JSON.parse(text, reviver) as DiscoveredDevice[];\n }\n}\n\n// ---- Wire codec ----\n//\n// Demo wire format: JSON with byte fields wrapped as `{ _bytes: number[] }`. The relay's\n// `envFromJson`/`envToJson` use the same shape. The spec calls for CBOR; v0.2 swaps in a\n// real CBOR codec, but the JSON form is much friendlier to debug.\n//\n// (decodeEnvelope helpers used to live here; collapsed into inline JSON.parse calls above.)\n\nfunction replacer(_k: string, v: unknown): unknown {\n if (v instanceof Uint8Array) return { _bytes: Array.from(v) };\n return v;\n}\nfunction reviver(_k: string, v: unknown): unknown {\n if (v && typeof v === \"object\" && Array.isArray((v as { _bytes?: number[] })._bytes)) {\n return new Uint8Array((v as { _bytes: number[] })._bytes);\n }\n return v;\n}\n\nfunction toHex(b: Uint8Array): string {\n let s = \"\"; for (const v of b) s += v.toString(16).padStart(2, \"0\"); return s;\n}\nfunction btoaUrl(b: Uint8Array): string {\n let s = \"\"; for (const v of b) s += String.fromCharCode(v);\n return btoa(s).replace(/\\+/g, \"-\").replace(/\\//g, \"_\").replace(/=+$/, \"\");\n}\n"],"mappings":"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","names":[]}
@@ -55,6 +55,33 @@ interface DiscoveredDevice {
55
55
  device_id: DeviceId;
56
56
  key_package: Bytes;
57
57
  }
58
+ /** [CR-2] Pair supplied to `addMembers`. Maps 1:1 to `DiscoveredDevice` so hosts can
59
+ * rename a directory result and pass it through unchanged. */
60
+ interface KeyPackageEntry {
61
+ deviceId: DeviceId;
62
+ keyPackage: Bytes;
63
+ }
64
+ /** [CR-13] One host-supplied (conversation, last-AppEvent) pair for
65
+ * `buildLinkingTicket`. The new device renders these as "what you missed". */
66
+ interface CatchupAppEvent {
67
+ conversationId: ConversationId;
68
+ appEventBytes: Bytes;
69
+ }
70
+ /** [CR-13] Decoded `LinkingTicket.catchup_snapshot`. The `group_state_bytes` field is
71
+ * reserved for CR-7 — empty until that ships. */
72
+ interface CatchupSnapshotEntry {
73
+ conversation_id: ConversationId;
74
+ meta: ConversationMeta;
75
+ group_state_bytes: Bytes;
76
+ }
77
+ interface CatchupSnapshotView {
78
+ v: number;
79
+ conversation_metas: CatchupSnapshotEntry[];
80
+ last_app_events_per_conv: {
81
+ conversation_id: ConversationId;
82
+ app_event_bytes: Bytes;
83
+ }[];
84
+ }
58
85
  interface Storage {
59
86
  get(namespace: string, key: string): Promise<Uint8Array | null>;
60
87
  put(namespace: string, key: string, value: Uint8Array): Promise<void>;
@@ -71,4 +98,4 @@ interface Transport {
71
98
  discoverDevices(userId: UserId): Promise<DiscoveredDevice[]>;
72
99
  }
73
100
 
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 };
101
+ export type { Bytes as B, ConversationId as C, DiscoveredDevice as D, Hlc as H, IncomingMessage as I, KeyPackageEntry as K, LinkingTicket as L, MessageEnvelope as M, Storage as S, Transport as T, UserId as U, ConversationMeta as a, DeviceId as b, CatchupAppEvent as c, CatchupSnapshotView as d, CatchupSnapshotEntry as e, DeviceInfo as f, MessageKind as g };
@@ -55,6 +55,33 @@ interface DiscoveredDevice {
55
55
  device_id: DeviceId;
56
56
  key_package: Bytes;
57
57
  }
58
+ /** [CR-2] Pair supplied to `addMembers`. Maps 1:1 to `DiscoveredDevice` so hosts can
59
+ * rename a directory result and pass it through unchanged. */
60
+ interface KeyPackageEntry {
61
+ deviceId: DeviceId;
62
+ keyPackage: Bytes;
63
+ }
64
+ /** [CR-13] One host-supplied (conversation, last-AppEvent) pair for
65
+ * `buildLinkingTicket`. The new device renders these as "what you missed". */
66
+ interface CatchupAppEvent {
67
+ conversationId: ConversationId;
68
+ appEventBytes: Bytes;
69
+ }
70
+ /** [CR-13] Decoded `LinkingTicket.catchup_snapshot`. The `group_state_bytes` field is
71
+ * reserved for CR-7 — empty until that ships. */
72
+ interface CatchupSnapshotEntry {
73
+ conversation_id: ConversationId;
74
+ meta: ConversationMeta;
75
+ group_state_bytes: Bytes;
76
+ }
77
+ interface CatchupSnapshotView {
78
+ v: number;
79
+ conversation_metas: CatchupSnapshotEntry[];
80
+ last_app_events_per_conv: {
81
+ conversation_id: ConversationId;
82
+ app_event_bytes: Bytes;
83
+ }[];
84
+ }
58
85
  interface Storage {
59
86
  get(namespace: string, key: string): Promise<Uint8Array | null>;
60
87
  put(namespace: string, key: string, value: Uint8Array): Promise<void>;
@@ -71,4 +98,4 @@ interface Transport {
71
98
  discoverDevices(userId: UserId): Promise<DiscoveredDevice[]>;
72
99
  }
73
100
 
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 };
101
+ export type { Bytes as B, ConversationId as C, DiscoveredDevice as D, Hlc as H, IncomingMessage as I, KeyPackageEntry as K, LinkingTicket as L, MessageEnvelope as M, Storage as S, Transport as T, UserId as U, ConversationMeta as a, DeviceId as b, CatchupAppEvent as c, CatchupSnapshotView as d, CatchupSnapshotEntry as e, DeviceInfo as f, MessageKind as g };
package/dist/worker.cjs CHANGED
@@ -93,7 +93,12 @@ async function dispatch(msg) {
93
93
  msg.deviceLabel,
94
94
  proxyStorage,
95
95
  proxyTransport,
96
- msg.nowMs
96
+ msg.nowMs,
97
+ // Optional Ed25519 device signing secret — see protocol.ts
98
+ // and ClientConfig.deviceSigningSecretKey. Forwarding `null`
99
+ // explicitly when absent so the wasm-bindgen `Option<Vec<u8>>`
100
+ // shape is matched correctly.
101
+ msg.deviceSigningSecretKey ?? null
97
102
  );
98
103
  client.onMessage((m) => {
99
104
  self.postMessage({ kind: "event.message", msg: m });
@@ -125,9 +130,11 @@ async function dispatch(msg) {
125
130
  case "send":
126
131
  return await ensureClient().send(msg.conv, msg.plaintext, msg.nowMs);
127
132
  case "addMembers":
128
- return await ensureClient().addMembers(msg.conv, msg.kps, msg.nowMs);
133
+ return await ensureClient().addMembers(msg.conv, msg.entries, msg.nowMs);
129
134
  case "removeMembers":
130
135
  return await ensureClient().removeMembers(msg.conv, new Uint32Array(msg.leaves), msg.nowMs);
136
+ case "revokeDevice":
137
+ return await ensureClient().revokeDevice(msg.deviceId, msg.nowMs);
131
138
  case "processEnvelope": {
132
139
  const result = await ensureClient().processEnvelope(msg.envelope, msg.nowMs);
133
140
  self.postMessage({
@@ -140,10 +147,32 @@ async function dispatch(msg) {
140
147
  case "syncConversations":
141
148
  return await ensureClient().syncConversations(msg.nowMs);
142
149
  case "buildLinkingTicket":
143
- return await ensureClient().buildLinkingTicket(msg.newDeviceId, msg.newDeviceKp, msg.nowMs);
150
+ return await ensureClient().buildLinkingTicket(
151
+ msg.newDeviceId,
152
+ msg.newDeviceKp,
153
+ msg.lastAppEvents,
154
+ msg.nowMs
155
+ );
144
156
  case "consumeLinkingTicket":
145
157
  await ensureClient().consumeLinkingTicket(msg.ticket, msg.nowMs);
146
158
  return null;
159
+ case "decodeCatchupSnapshot":
160
+ return (0, import_ping_wasm.decodeCatchupSnapshot)(msg.snapshotBytes);
161
+ case "exportConversationSecret":
162
+ return ensureClient().exportConversationSecret(
163
+ msg.conv,
164
+ msg.label,
165
+ msg.context,
166
+ msg.length
167
+ );
168
+ case "sealLinkingTicket":
169
+ return (0, import_ping_wasm.sealLinkingTicket)(msg.ticket, msg.newDevicePub);
170
+ case "openLinkingTicket":
171
+ return (0, import_ping_wasm.openLinkingTicket)(msg.sealed, msg.newDevicePriv);
172
+ case "exportConversationStateSnapshot":
173
+ return ensureClient().exportConversationStateSnapshot(msg.conv, msg.nowMs);
174
+ case "importStateSnapshot":
175
+ return await ensureClient().importStateSnapshot(msg.snapshotBytes, msg.nowMs);
147
176
  default:
148
177
  throw new Error(`unknown request kind: ${msg.kind}`);
149
178
  }
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/worker.ts"],"sourcesContent":["// Web Worker entry. Owns the WASM `PingClient` instance. Receives Requests from the main thread\n// and posts Responses back. Storage / Transport calls back to the main thread are tracked by id.\n//\n// The WASM module is loaded lazily on the first `init` so that ephemeral identity-only spawns\n// (used by `MessagingClient.generateIdentity()`) don't pay the cost.\n\n/// <reference lib=\"webworker\" />\n\nimport init, { PingClient } from \"../wasm/ping_wasm\";\nimport type { Request, Response } from \"./protocol\";\nimport type { MessageEnvelope } from \"./types\";\n\ndeclare const self: DedicatedWorkerGlobalScope;\n\nlet client: PingClient | null = null;\nlet wasmReady: Promise<void> | null = null;\n\nconst pendingStorage = new Map<number, (r: { ok: boolean; value: unknown }) => void>();\nconst pendingTransport = new Map<number, (r: { ok: boolean; value: unknown }) => void>();\nlet nextStorageId = 1;\nlet nextTransportId = 1;\n\nconst proxyStorage = {\n get: (namespace: string, key: string) => callMain<Uint8Array | null>(\"storage\", \"get\", [namespace, key]),\n put: (namespace: string, key: string, value: Uint8Array) =>\n callMain<void>(\"storage\", \"put\", [namespace, key, value]),\n del: (namespace: string, key: string) => callMain<void>(\"storage\", \"del\", [namespace, key]),\n listKeys: (namespace: string, prefix: string) =>\n callMain<string[]>(\"storage\", \"listKeys\", [namespace, prefix]),\n};\nconst proxyTransport = {\n send: (envelope: MessageEnvelope) => callMain<void>(\"transport\", \"send\", [envelope]),\n fetchSince: (conv: Uint8Array, cursor: Uint8Array, limit: number) =>\n callMain<MessageEnvelope[]>(\"transport\", \"fetchSince\", [conv, cursor, limit]),\n discoverDevices: (userId: Uint8Array) => callMain<unknown[]>(\"transport\", \"discoverDevices\", [userId]),\n};\n\nfunction callMain<T>(kind: \"storage\" | \"transport\", method: string, args: unknown[]): Promise<T> {\n return new Promise((resolve, reject) => {\n const id = kind === \"storage\" ? nextStorageId++ : nextTransportId++;\n const map = kind === \"storage\" ? pendingStorage : pendingTransport;\n map.set(id, ({ ok, value }) => ok ? resolve(value as T) : reject(new Error(String(value))));\n self.postMessage({ kind: `${kind}.call`, id, method, args } as Response);\n });\n}\n\nasync function ensureWasm() {\n if (wasmReady) return wasmReady;\n // The bundled wasm path is rewritten by the build script to be relative to `dist/`.\n wasmReady = init().then(() => undefined);\n return wasmReady;\n}\n\nself.onmessage = async (ev: MessageEvent<Request>) => {\n const msg = ev.data;\n\n // Storage / transport responses from the main thread.\n if (msg.kind === \"storage.reply\") {\n const slot = pendingStorage.get(msg.id);\n if (slot) {\n pendingStorage.delete(msg.id);\n msg.ok ? slot({ ok: true, value: msg.value })\n : slot({ ok: false, value: msg.error });\n }\n return;\n }\n if (msg.kind === \"transport.reply\") {\n const slot = pendingTransport.get(msg.id);\n if (slot) {\n pendingTransport.delete(msg.id);\n msg.ok ? slot({ ok: true, value: msg.value })\n : slot({ ok: false, value: msg.error });\n }\n return;\n }\n\n try {\n const value = await dispatch(msg);\n (self.postMessage as (m: Response) => void)({ kind: \"result\", id: msg.id, ok: true, value });\n } catch (e) {\n (self.postMessage as (m: Response) => void)({\n kind: \"result\", id: msg.id, ok: false, error: e instanceof Error ? e.message : String(e),\n });\n }\n};\n\nasync function dispatch(msg: Request): Promise<unknown> {\n await ensureWasm();\n switch (msg.kind) {\n case \"init\": {\n client = await PingClient.init(\n msg.identityExport, msg.deviceLabel,\n proxyStorage as unknown as never,\n proxyTransport as unknown as never,\n msg.nowMs,\n );\n // Wire decrypted application-message callback back to the main thread.\n client.onMessage((m: unknown) => {\n (self.postMessage as (m: Response) => void)({ kind: \"event.message\", msg: m as never });\n });\n return null;\n }\n case \"generateIdentity\":\n return PingClient.generateIdentity();\n case \"userId\": return ensureClient().userId();\n case \"deviceId\": return ensureClient().deviceId();\n case \"freshKeyPackage\": return ensureClient().freshKeyPackage();\n case \"createConversation\":\n return await ensureClient().createConversation(msg.name ?? undefined, msg.nowMs);\n case \"joinConversation\": {\n const id = await ensureClient().joinConversation(msg.welcome, msg.nowMs) as Uint8Array;\n // Notify the main thread that we just entered a new conversation. We forward the\n // Welcome's `sender_device` so the host can label \"joined chat with <inviter>\".\n (self.postMessage as (m: Response) => void)({\n kind: \"event.conversation\",\n id,\n epoch: 0,\n sender: msg.welcome.sender_device,\n });\n return id;\n }\n case \"listConversations\":\n return ensureClient().listConversations();\n case \"send\":\n return await ensureClient().send(msg.conv, msg.plaintext, msg.nowMs);\n case \"addMembers\":\n return await ensureClient().addMembers(msg.conv, msg.kps, msg.nowMs);\n case \"removeMembers\":\n // wasm-bindgen exposes Vec<u32> as Uint32Array; convert from the protocol's number[].\n return await ensureClient().removeMembers(msg.conv, new Uint32Array(msg.leaves), msg.nowMs);\n case \"processEnvelope\": {\n const result = await ensureClient().processEnvelope(msg.envelope, msg.nowMs);\n (self.postMessage as (m: Response) => void)({\n kind: \"event.conversation\",\n id: msg.envelope.conversation_id,\n epoch: msg.envelope.epoch,\n });\n return result;\n }\n case \"syncConversations\":\n return await ensureClient().syncConversations(msg.nowMs);\n case \"buildLinkingTicket\":\n return await ensureClient().buildLinkingTicket(msg.newDeviceId, msg.newDeviceKp, msg.nowMs);\n case \"consumeLinkingTicket\":\n await ensureClient().consumeLinkingTicket(msg.ticket, msg.nowMs);\n return null;\n default:\n throw new Error(`unknown request kind: ${(msg as { kind: string }).kind}`);\n }\n}\n\nfunction ensureClient(): PingClient {\n if (!client) throw new Error(\"client not initialised\");\n return client;\n}\n"],"mappings":"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","names":["init"]}
1
+ {"version":3,"sources":["../src/worker.ts"],"sourcesContent":["// Web Worker entry. Owns the WASM `PingClient` instance. Receives Requests from the main thread\n// and posts Responses back. Storage / Transport calls back to the main thread are tracked by id.\n//\n// The WASM module is loaded lazily on the first `init` so that ephemeral identity-only spawns\n// (used by `MessagingClient.generateIdentity()`) don't pay the cost.\n\n/// <reference lib=\"webworker\" />\n\nimport init, {\n PingClient,\n sealLinkingTicket as wasmSealLinkingTicket,\n openLinkingTicket as wasmOpenLinkingTicket,\n decodeCatchupSnapshot as wasmDecodeCatchupSnapshot,\n} from \"../wasm/ping_wasm\";\nimport type { Request, Response } from \"./protocol\";\nimport type { MessageEnvelope } from \"./types\";\n\ndeclare const self: DedicatedWorkerGlobalScope;\n\nlet client: PingClient | null = null;\nlet wasmReady: Promise<void> | null = null;\n\nconst pendingStorage = new Map<number, (r: { ok: boolean; value: unknown }) => void>();\nconst pendingTransport = new Map<number, (r: { ok: boolean; value: unknown }) => void>();\nlet nextStorageId = 1;\nlet nextTransportId = 1;\n\nconst proxyStorage = {\n get: (namespace: string, key: string) => callMain<Uint8Array | null>(\"storage\", \"get\", [namespace, key]),\n put: (namespace: string, key: string, value: Uint8Array) =>\n callMain<void>(\"storage\", \"put\", [namespace, key, value]),\n del: (namespace: string, key: string) => callMain<void>(\"storage\", \"del\", [namespace, key]),\n listKeys: (namespace: string, prefix: string) =>\n callMain<string[]>(\"storage\", \"listKeys\", [namespace, prefix]),\n};\nconst proxyTransport = {\n send: (envelope: MessageEnvelope) => callMain<void>(\"transport\", \"send\", [envelope]),\n fetchSince: (conv: Uint8Array, cursor: Uint8Array, limit: number) =>\n callMain<MessageEnvelope[]>(\"transport\", \"fetchSince\", [conv, cursor, limit]),\n discoverDevices: (userId: Uint8Array) => callMain<unknown[]>(\"transport\", \"discoverDevices\", [userId]),\n};\n\nfunction callMain<T>(kind: \"storage\" | \"transport\", method: string, args: unknown[]): Promise<T> {\n return new Promise((resolve, reject) => {\n const id = kind === \"storage\" ? nextStorageId++ : nextTransportId++;\n const map = kind === \"storage\" ? pendingStorage : pendingTransport;\n map.set(id, ({ ok, value }) => ok ? resolve(value as T) : reject(new Error(String(value))));\n self.postMessage({ kind: `${kind}.call`, id, method, args } as Response);\n });\n}\n\nasync function ensureWasm() {\n if (wasmReady) return wasmReady;\n // The bundled wasm path is rewritten by the build script to be relative to `dist/`.\n wasmReady = init().then(() => undefined);\n return wasmReady;\n}\n\nself.onmessage = async (ev: MessageEvent<Request>) => {\n const msg = ev.data;\n\n // Storage / transport responses from the main thread.\n if (msg.kind === \"storage.reply\") {\n const slot = pendingStorage.get(msg.id);\n if (slot) {\n pendingStorage.delete(msg.id);\n msg.ok ? slot({ ok: true, value: msg.value })\n : slot({ ok: false, value: msg.error });\n }\n return;\n }\n if (msg.kind === \"transport.reply\") {\n const slot = pendingTransport.get(msg.id);\n if (slot) {\n pendingTransport.delete(msg.id);\n msg.ok ? slot({ ok: true, value: msg.value })\n : slot({ ok: false, value: msg.error });\n }\n return;\n }\n\n try {\n const value = await dispatch(msg);\n (self.postMessage as (m: Response) => void)({ kind: \"result\", id: msg.id, ok: true, value });\n } catch (e) {\n (self.postMessage as (m: Response) => void)({\n kind: \"result\", id: msg.id, ok: false, error: e instanceof Error ? e.message : String(e),\n });\n }\n};\n\nasync function dispatch(msg: Request): Promise<unknown> {\n await ensureWasm();\n switch (msg.kind) {\n case \"init\": {\n client = await PingClient.init(\n msg.identityExport, msg.deviceLabel,\n proxyStorage as unknown as never,\n proxyTransport as unknown as never,\n msg.nowMs,\n // Optional Ed25519 device signing secret — see protocol.ts\n // and ClientConfig.deviceSigningSecretKey. Forwarding `null`\n // explicitly when absent so the wasm-bindgen `Option<Vec<u8>>`\n // shape is matched correctly.\n msg.deviceSigningSecretKey ?? null,\n );\n // Wire decrypted application-message callback back to the main thread.\n client.onMessage((m: unknown) => {\n (self.postMessage as (m: Response) => void)({ kind: \"event.message\", msg: m as never });\n });\n return null;\n }\n case \"generateIdentity\":\n return PingClient.generateIdentity();\n case \"userId\": return ensureClient().userId();\n case \"deviceId\": return ensureClient().deviceId();\n case \"freshKeyPackage\": return ensureClient().freshKeyPackage();\n case \"createConversation\":\n return await ensureClient().createConversation(msg.name ?? undefined, msg.nowMs);\n case \"joinConversation\": {\n const id = await ensureClient().joinConversation(msg.welcome, msg.nowMs) as Uint8Array;\n // Notify the main thread that we just entered a new conversation. We forward the\n // Welcome's `sender_device` so the host can label \"joined chat with <inviter>\".\n (self.postMessage as (m: Response) => void)({\n kind: \"event.conversation\",\n id,\n epoch: 0,\n sender: msg.welcome.sender_device,\n });\n return id;\n }\n case \"listConversations\":\n return ensureClient().listConversations();\n case \"send\":\n return await ensureClient().send(msg.conv, msg.plaintext, msg.nowMs);\n case \"addMembers\":\n // wasm-bindgen takes the entries as a plain array of {deviceId, keyPackage};\n // serde-wasm-bindgen on the Rust side deserialises into Vec<JsAddMemberEntry>.\n return await ensureClient().addMembers(msg.conv, msg.entries, msg.nowMs);\n case \"removeMembers\":\n // wasm-bindgen exposes Vec<u32> as Uint32Array; convert from the protocol's number[].\n return await ensureClient().removeMembers(msg.conv, new Uint32Array(msg.leaves), msg.nowMs);\n case \"revokeDevice\":\n // [CR-2] returns the array of Commit envelopes the SDK has already broadcast.\n return await ensureClient().revokeDevice(msg.deviceId, msg.nowMs);\n case \"processEnvelope\": {\n const result = await ensureClient().processEnvelope(msg.envelope, msg.nowMs);\n (self.postMessage as (m: Response) => void)({\n kind: \"event.conversation\",\n id: msg.envelope.conversation_id,\n epoch: msg.envelope.epoch,\n });\n return result;\n }\n case \"syncConversations\":\n return await ensureClient().syncConversations(msg.nowMs);\n case \"buildLinkingTicket\":\n return await ensureClient().buildLinkingTicket(\n msg.newDeviceId, msg.newDeviceKp, msg.lastAppEvents, msg.nowMs,\n );\n case \"consumeLinkingTicket\":\n await ensureClient().consumeLinkingTicket(msg.ticket, msg.nowMs);\n return null;\n case \"decodeCatchupSnapshot\":\n // Pure WASM — no client state required. The new device may call this in an\n // ephemeral worker before init() completes.\n return wasmDecodeCatchupSnapshot(msg.snapshotBytes);\n case \"exportConversationSecret\":\n // wasm-bindgen returns a Uint8Array sized to `length`. The bytes are a secret —\n // the main thread is responsible for clearing the typed array when done.\n return ensureClient().exportConversationSecret(\n msg.conv, msg.label, msg.context, msg.length,\n );\n case \"sealLinkingTicket\":\n // Pure WASM call — no client state required. Lets a sender seal a ticket from an\n // ephemeral worker without standing up the full PingClient.\n return wasmSealLinkingTicket(msg.ticket, msg.newDevicePub);\n case \"openLinkingTicket\":\n // Same — runs without an initialised client, which is the receiving device's\n // pre-bootstrap state (it has the X25519 ephemeral key but no identity yet).\n return wasmOpenLinkingTicket(msg.sealed, msg.newDevicePriv);\n case \"exportConversationStateSnapshot\":\n // [CR-7] state snapshot bytes are a secret (past epoch keys); the main thread\n // is responsible for clearing the typed array when done.\n return ensureClient().exportConversationStateSnapshot(msg.conv, msg.nowMs);\n case \"importStateSnapshot\":\n // [CR-7] returns the imported conversation id as a Uint8Array.\n return await ensureClient().importStateSnapshot(msg.snapshotBytes, msg.nowMs);\n default:\n throw new Error(`unknown request kind: ${(msg as { kind: string }).kind}`);\n }\n}\n\nfunction ensureClient(): PingClient {\n if (!client) throw new Error(\"client not initialised\");\n return client;\n}\n"],"mappings":"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","names":["init","wasmDecodeCatchupSnapshot","wasmSealLinkingTicket","wasmOpenLinkingTicket"]}
package/dist/worker.js CHANGED
@@ -1,5 +1,10 @@
1
1
  // src/worker.ts
2
- import init, { PingClient } from "../wasm/ping_wasm";
2
+ import init, {
3
+ PingClient,
4
+ sealLinkingTicket as wasmSealLinkingTicket,
5
+ openLinkingTicket as wasmOpenLinkingTicket,
6
+ decodeCatchupSnapshot as wasmDecodeCatchupSnapshot
7
+ } from "../wasm/ping_wasm";
3
8
  var client = null;
4
9
  var wasmReady = null;
5
10
  var pendingStorage = /* @__PURE__ */ new Map();
@@ -69,7 +74,12 @@ async function dispatch(msg) {
69
74
  msg.deviceLabel,
70
75
  proxyStorage,
71
76
  proxyTransport,
72
- msg.nowMs
77
+ msg.nowMs,
78
+ // Optional Ed25519 device signing secret — see protocol.ts
79
+ // and ClientConfig.deviceSigningSecretKey. Forwarding `null`
80
+ // explicitly when absent so the wasm-bindgen `Option<Vec<u8>>`
81
+ // shape is matched correctly.
82
+ msg.deviceSigningSecretKey ?? null
73
83
  );
74
84
  client.onMessage((m) => {
75
85
  self.postMessage({ kind: "event.message", msg: m });
@@ -101,9 +111,11 @@ async function dispatch(msg) {
101
111
  case "send":
102
112
  return await ensureClient().send(msg.conv, msg.plaintext, msg.nowMs);
103
113
  case "addMembers":
104
- return await ensureClient().addMembers(msg.conv, msg.kps, msg.nowMs);
114
+ return await ensureClient().addMembers(msg.conv, msg.entries, msg.nowMs);
105
115
  case "removeMembers":
106
116
  return await ensureClient().removeMembers(msg.conv, new Uint32Array(msg.leaves), msg.nowMs);
117
+ case "revokeDevice":
118
+ return await ensureClient().revokeDevice(msg.deviceId, msg.nowMs);
107
119
  case "processEnvelope": {
108
120
  const result = await ensureClient().processEnvelope(msg.envelope, msg.nowMs);
109
121
  self.postMessage({
@@ -116,10 +128,32 @@ async function dispatch(msg) {
116
128
  case "syncConversations":
117
129
  return await ensureClient().syncConversations(msg.nowMs);
118
130
  case "buildLinkingTicket":
119
- return await ensureClient().buildLinkingTicket(msg.newDeviceId, msg.newDeviceKp, msg.nowMs);
131
+ return await ensureClient().buildLinkingTicket(
132
+ msg.newDeviceId,
133
+ msg.newDeviceKp,
134
+ msg.lastAppEvents,
135
+ msg.nowMs
136
+ );
120
137
  case "consumeLinkingTicket":
121
138
  await ensureClient().consumeLinkingTicket(msg.ticket, msg.nowMs);
122
139
  return null;
140
+ case "decodeCatchupSnapshot":
141
+ return wasmDecodeCatchupSnapshot(msg.snapshotBytes);
142
+ case "exportConversationSecret":
143
+ return ensureClient().exportConversationSecret(
144
+ msg.conv,
145
+ msg.label,
146
+ msg.context,
147
+ msg.length
148
+ );
149
+ case "sealLinkingTicket":
150
+ return wasmSealLinkingTicket(msg.ticket, msg.newDevicePub);
151
+ case "openLinkingTicket":
152
+ return wasmOpenLinkingTicket(msg.sealed, msg.newDevicePriv);
153
+ case "exportConversationStateSnapshot":
154
+ return ensureClient().exportConversationStateSnapshot(msg.conv, msg.nowMs);
155
+ case "importStateSnapshot":
156
+ return await ensureClient().importStateSnapshot(msg.snapshotBytes, msg.nowMs);
123
157
  default:
124
158
  throw new Error(`unknown request kind: ${msg.kind}`);
125
159
  }
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/worker.ts"],"sourcesContent":["// Web Worker entry. Owns the WASM `PingClient` instance. Receives Requests from the main thread\n// and posts Responses back. Storage / Transport calls back to the main thread are tracked by id.\n//\n// The WASM module is loaded lazily on the first `init` so that ephemeral identity-only spawns\n// (used by `MessagingClient.generateIdentity()`) don't pay the cost.\n\n/// <reference lib=\"webworker\" />\n\nimport init, { PingClient } from \"../wasm/ping_wasm\";\nimport type { Request, Response } from \"./protocol\";\nimport type { MessageEnvelope } from \"./types\";\n\ndeclare const self: DedicatedWorkerGlobalScope;\n\nlet client: PingClient | null = null;\nlet wasmReady: Promise<void> | null = null;\n\nconst pendingStorage = new Map<number, (r: { ok: boolean; value: unknown }) => void>();\nconst pendingTransport = new Map<number, (r: { ok: boolean; value: unknown }) => void>();\nlet nextStorageId = 1;\nlet nextTransportId = 1;\n\nconst proxyStorage = {\n get: (namespace: string, key: string) => callMain<Uint8Array | null>(\"storage\", \"get\", [namespace, key]),\n put: (namespace: string, key: string, value: Uint8Array) =>\n callMain<void>(\"storage\", \"put\", [namespace, key, value]),\n del: (namespace: string, key: string) => callMain<void>(\"storage\", \"del\", [namespace, key]),\n listKeys: (namespace: string, prefix: string) =>\n callMain<string[]>(\"storage\", \"listKeys\", [namespace, prefix]),\n};\nconst proxyTransport = {\n send: (envelope: MessageEnvelope) => callMain<void>(\"transport\", \"send\", [envelope]),\n fetchSince: (conv: Uint8Array, cursor: Uint8Array, limit: number) =>\n callMain<MessageEnvelope[]>(\"transport\", \"fetchSince\", [conv, cursor, limit]),\n discoverDevices: (userId: Uint8Array) => callMain<unknown[]>(\"transport\", \"discoverDevices\", [userId]),\n};\n\nfunction callMain<T>(kind: \"storage\" | \"transport\", method: string, args: unknown[]): Promise<T> {\n return new Promise((resolve, reject) => {\n const id = kind === \"storage\" ? nextStorageId++ : nextTransportId++;\n const map = kind === \"storage\" ? pendingStorage : pendingTransport;\n map.set(id, ({ ok, value }) => ok ? resolve(value as T) : reject(new Error(String(value))));\n self.postMessage({ kind: `${kind}.call`, id, method, args } as Response);\n });\n}\n\nasync function ensureWasm() {\n if (wasmReady) return wasmReady;\n // The bundled wasm path is rewritten by the build script to be relative to `dist/`.\n wasmReady = init().then(() => undefined);\n return wasmReady;\n}\n\nself.onmessage = async (ev: MessageEvent<Request>) => {\n const msg = ev.data;\n\n // Storage / transport responses from the main thread.\n if (msg.kind === \"storage.reply\") {\n const slot = pendingStorage.get(msg.id);\n if (slot) {\n pendingStorage.delete(msg.id);\n msg.ok ? slot({ ok: true, value: msg.value })\n : slot({ ok: false, value: msg.error });\n }\n return;\n }\n if (msg.kind === \"transport.reply\") {\n const slot = pendingTransport.get(msg.id);\n if (slot) {\n pendingTransport.delete(msg.id);\n msg.ok ? slot({ ok: true, value: msg.value })\n : slot({ ok: false, value: msg.error });\n }\n return;\n }\n\n try {\n const value = await dispatch(msg);\n (self.postMessage as (m: Response) => void)({ kind: \"result\", id: msg.id, ok: true, value });\n } catch (e) {\n (self.postMessage as (m: Response) => void)({\n kind: \"result\", id: msg.id, ok: false, error: e instanceof Error ? e.message : String(e),\n });\n }\n};\n\nasync function dispatch(msg: Request): Promise<unknown> {\n await ensureWasm();\n switch (msg.kind) {\n case \"init\": {\n client = await PingClient.init(\n msg.identityExport, msg.deviceLabel,\n proxyStorage as unknown as never,\n proxyTransport as unknown as never,\n msg.nowMs,\n );\n // Wire decrypted application-message callback back to the main thread.\n client.onMessage((m: unknown) => {\n (self.postMessage as (m: Response) => void)({ kind: \"event.message\", msg: m as never });\n });\n return null;\n }\n case \"generateIdentity\":\n return PingClient.generateIdentity();\n case \"userId\": return ensureClient().userId();\n case \"deviceId\": return ensureClient().deviceId();\n case \"freshKeyPackage\": return ensureClient().freshKeyPackage();\n case \"createConversation\":\n return await ensureClient().createConversation(msg.name ?? undefined, msg.nowMs);\n case \"joinConversation\": {\n const id = await ensureClient().joinConversation(msg.welcome, msg.nowMs) as Uint8Array;\n // Notify the main thread that we just entered a new conversation. We forward the\n // Welcome's `sender_device` so the host can label \"joined chat with <inviter>\".\n (self.postMessage as (m: Response) => void)({\n kind: \"event.conversation\",\n id,\n epoch: 0,\n sender: msg.welcome.sender_device,\n });\n return id;\n }\n case \"listConversations\":\n return ensureClient().listConversations();\n case \"send\":\n return await ensureClient().send(msg.conv, msg.plaintext, msg.nowMs);\n case \"addMembers\":\n return await ensureClient().addMembers(msg.conv, msg.kps, msg.nowMs);\n case \"removeMembers\":\n // wasm-bindgen exposes Vec<u32> as Uint32Array; convert from the protocol's number[].\n return await ensureClient().removeMembers(msg.conv, new Uint32Array(msg.leaves), msg.nowMs);\n case \"processEnvelope\": {\n const result = await ensureClient().processEnvelope(msg.envelope, msg.nowMs);\n (self.postMessage as (m: Response) => void)({\n kind: \"event.conversation\",\n id: msg.envelope.conversation_id,\n epoch: msg.envelope.epoch,\n });\n return result;\n }\n case \"syncConversations\":\n return await ensureClient().syncConversations(msg.nowMs);\n case \"buildLinkingTicket\":\n return await ensureClient().buildLinkingTicket(msg.newDeviceId, msg.newDeviceKp, msg.nowMs);\n case \"consumeLinkingTicket\":\n await ensureClient().consumeLinkingTicket(msg.ticket, msg.nowMs);\n return null;\n default:\n throw new Error(`unknown request kind: ${(msg as { kind: string }).kind}`);\n }\n}\n\nfunction ensureClient(): PingClient {\n if (!client) throw new Error(\"client not initialised\");\n return client;\n}\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../src/worker.ts"],"sourcesContent":["// Web Worker entry. Owns the WASM `PingClient` instance. Receives Requests from the main thread\n// and posts Responses back. Storage / Transport calls back to the main thread are tracked by id.\n//\n// The WASM module is loaded lazily on the first `init` so that ephemeral identity-only spawns\n// (used by `MessagingClient.generateIdentity()`) don't pay the cost.\n\n/// <reference lib=\"webworker\" />\n\nimport init, {\n PingClient,\n sealLinkingTicket as wasmSealLinkingTicket,\n openLinkingTicket as wasmOpenLinkingTicket,\n decodeCatchupSnapshot as wasmDecodeCatchupSnapshot,\n} from \"../wasm/ping_wasm\";\nimport type { Request, Response } from \"./protocol\";\nimport type { MessageEnvelope } from \"./types\";\n\ndeclare const self: DedicatedWorkerGlobalScope;\n\nlet client: PingClient | null = null;\nlet wasmReady: Promise<void> | null = null;\n\nconst pendingStorage = new Map<number, (r: { ok: boolean; value: unknown }) => void>();\nconst pendingTransport = new Map<number, (r: { ok: boolean; value: unknown }) => void>();\nlet nextStorageId = 1;\nlet nextTransportId = 1;\n\nconst proxyStorage = {\n get: (namespace: string, key: string) => callMain<Uint8Array | null>(\"storage\", \"get\", [namespace, key]),\n put: (namespace: string, key: string, value: Uint8Array) =>\n callMain<void>(\"storage\", \"put\", [namespace, key, value]),\n del: (namespace: string, key: string) => callMain<void>(\"storage\", \"del\", [namespace, key]),\n listKeys: (namespace: string, prefix: string) =>\n callMain<string[]>(\"storage\", \"listKeys\", [namespace, prefix]),\n};\nconst proxyTransport = {\n send: (envelope: MessageEnvelope) => callMain<void>(\"transport\", \"send\", [envelope]),\n fetchSince: (conv: Uint8Array, cursor: Uint8Array, limit: number) =>\n callMain<MessageEnvelope[]>(\"transport\", \"fetchSince\", [conv, cursor, limit]),\n discoverDevices: (userId: Uint8Array) => callMain<unknown[]>(\"transport\", \"discoverDevices\", [userId]),\n};\n\nfunction callMain<T>(kind: \"storage\" | \"transport\", method: string, args: unknown[]): Promise<T> {\n return new Promise((resolve, reject) => {\n const id = kind === \"storage\" ? nextStorageId++ : nextTransportId++;\n const map = kind === \"storage\" ? pendingStorage : pendingTransport;\n map.set(id, ({ ok, value }) => ok ? resolve(value as T) : reject(new Error(String(value))));\n self.postMessage({ kind: `${kind}.call`, id, method, args } as Response);\n });\n}\n\nasync function ensureWasm() {\n if (wasmReady) return wasmReady;\n // The bundled wasm path is rewritten by the build script to be relative to `dist/`.\n wasmReady = init().then(() => undefined);\n return wasmReady;\n}\n\nself.onmessage = async (ev: MessageEvent<Request>) => {\n const msg = ev.data;\n\n // Storage / transport responses from the main thread.\n if (msg.kind === \"storage.reply\") {\n const slot = pendingStorage.get(msg.id);\n if (slot) {\n pendingStorage.delete(msg.id);\n msg.ok ? slot({ ok: true, value: msg.value })\n : slot({ ok: false, value: msg.error });\n }\n return;\n }\n if (msg.kind === \"transport.reply\") {\n const slot = pendingTransport.get(msg.id);\n if (slot) {\n pendingTransport.delete(msg.id);\n msg.ok ? slot({ ok: true, value: msg.value })\n : slot({ ok: false, value: msg.error });\n }\n return;\n }\n\n try {\n const value = await dispatch(msg);\n (self.postMessage as (m: Response) => void)({ kind: \"result\", id: msg.id, ok: true, value });\n } catch (e) {\n (self.postMessage as (m: Response) => void)({\n kind: \"result\", id: msg.id, ok: false, error: e instanceof Error ? e.message : String(e),\n });\n }\n};\n\nasync function dispatch(msg: Request): Promise<unknown> {\n await ensureWasm();\n switch (msg.kind) {\n case \"init\": {\n client = await PingClient.init(\n msg.identityExport, msg.deviceLabel,\n proxyStorage as unknown as never,\n proxyTransport as unknown as never,\n msg.nowMs,\n // Optional Ed25519 device signing secret — see protocol.ts\n // and ClientConfig.deviceSigningSecretKey. Forwarding `null`\n // explicitly when absent so the wasm-bindgen `Option<Vec<u8>>`\n // shape is matched correctly.\n msg.deviceSigningSecretKey ?? null,\n );\n // Wire decrypted application-message callback back to the main thread.\n client.onMessage((m: unknown) => {\n (self.postMessage as (m: Response) => void)({ kind: \"event.message\", msg: m as never });\n });\n return null;\n }\n case \"generateIdentity\":\n return PingClient.generateIdentity();\n case \"userId\": return ensureClient().userId();\n case \"deviceId\": return ensureClient().deviceId();\n case \"freshKeyPackage\": return ensureClient().freshKeyPackage();\n case \"createConversation\":\n return await ensureClient().createConversation(msg.name ?? undefined, msg.nowMs);\n case \"joinConversation\": {\n const id = await ensureClient().joinConversation(msg.welcome, msg.nowMs) as Uint8Array;\n // Notify the main thread that we just entered a new conversation. We forward the\n // Welcome's `sender_device` so the host can label \"joined chat with <inviter>\".\n (self.postMessage as (m: Response) => void)({\n kind: \"event.conversation\",\n id,\n epoch: 0,\n sender: msg.welcome.sender_device,\n });\n return id;\n }\n case \"listConversations\":\n return ensureClient().listConversations();\n case \"send\":\n return await ensureClient().send(msg.conv, msg.plaintext, msg.nowMs);\n case \"addMembers\":\n // wasm-bindgen takes the entries as a plain array of {deviceId, keyPackage};\n // serde-wasm-bindgen on the Rust side deserialises into Vec<JsAddMemberEntry>.\n return await ensureClient().addMembers(msg.conv, msg.entries, msg.nowMs);\n case \"removeMembers\":\n // wasm-bindgen exposes Vec<u32> as Uint32Array; convert from the protocol's number[].\n return await ensureClient().removeMembers(msg.conv, new Uint32Array(msg.leaves), msg.nowMs);\n case \"revokeDevice\":\n // [CR-2] returns the array of Commit envelopes the SDK has already broadcast.\n return await ensureClient().revokeDevice(msg.deviceId, msg.nowMs);\n case \"processEnvelope\": {\n const result = await ensureClient().processEnvelope(msg.envelope, msg.nowMs);\n (self.postMessage as (m: Response) => void)({\n kind: \"event.conversation\",\n id: msg.envelope.conversation_id,\n epoch: msg.envelope.epoch,\n });\n return result;\n }\n case \"syncConversations\":\n return await ensureClient().syncConversations(msg.nowMs);\n case \"buildLinkingTicket\":\n return await ensureClient().buildLinkingTicket(\n msg.newDeviceId, msg.newDeviceKp, msg.lastAppEvents, msg.nowMs,\n );\n case \"consumeLinkingTicket\":\n await ensureClient().consumeLinkingTicket(msg.ticket, msg.nowMs);\n return null;\n case \"decodeCatchupSnapshot\":\n // Pure WASM — no client state required. The new device may call this in an\n // ephemeral worker before init() completes.\n return wasmDecodeCatchupSnapshot(msg.snapshotBytes);\n case \"exportConversationSecret\":\n // wasm-bindgen returns a Uint8Array sized to `length`. The bytes are a secret —\n // the main thread is responsible for clearing the typed array when done.\n return ensureClient().exportConversationSecret(\n msg.conv, msg.label, msg.context, msg.length,\n );\n case \"sealLinkingTicket\":\n // Pure WASM call — no client state required. Lets a sender seal a ticket from an\n // ephemeral worker without standing up the full PingClient.\n return wasmSealLinkingTicket(msg.ticket, msg.newDevicePub);\n case \"openLinkingTicket\":\n // Same — runs without an initialised client, which is the receiving device's\n // pre-bootstrap state (it has the X25519 ephemeral key but no identity yet).\n return wasmOpenLinkingTicket(msg.sealed, msg.newDevicePriv);\n case \"exportConversationStateSnapshot\":\n // [CR-7] state snapshot bytes are a secret (past epoch keys); the main thread\n // is responsible for clearing the typed array when done.\n return ensureClient().exportConversationStateSnapshot(msg.conv, msg.nowMs);\n case \"importStateSnapshot\":\n // [CR-7] returns the imported conversation id as a Uint8Array.\n return await ensureClient().importStateSnapshot(msg.snapshotBytes, msg.nowMs);\n default:\n throw new Error(`unknown request kind: ${(msg as { kind: string }).kind}`);\n }\n}\n\nfunction ensureClient(): PingClient {\n if (!client) throw new Error(\"client not initialised\");\n return client;\n}\n"],"mappings":"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","names":[]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ping-openmls-sdk",
3
- "version": "0.1.2",
3
+ "version": "0.5.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",
package/wasm/package.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "name": "ping-openmls-sdk-wasm",
3
3
  "type": "module",
4
4
  "description": "wasm-bindgen surface for the Ping OpenMLS SDK — drives the ping-openmls-sdk npm package",
5
- "version": "0.1.4",
5
+ "version": "0.4.0",
6
6
  "license": "Apache-2.0",
7
7
  "files": [
8
8
  "ping_wasm_bg.wasm",
@@ -5,25 +5,72 @@ export class PingClient {
5
5
  private constructor();
6
6
  free(): void;
7
7
  [Symbol.dispose](): void;
8
- addMembers(conv_id: Uint8Array, key_packages: Uint8Array[], now_ms: number): Promise<void>;
9
- buildLinkingTicket(new_device_id: Uint8Array, new_device_kp: Uint8Array, now_ms: number): Promise<any>;
8
+ /**
9
+ * Add members ([CR-2]). `entries` is an array of `{ deviceId: Uint8Array,
10
+ * keyPackage: Uint8Array }`. Bundling the device_id with the KeyPackage lets us
11
+ * persist a per-conversation device→leaf map for [`revokeDevice`].
12
+ */
13
+ addMembers(conv_id: Uint8Array, entries: any, now_ms: number): Promise<void>;
14
+ /**
15
+ * Build a `LinkingTicket` ([CR-13] populates `catchup_snapshot` from `last_app_events`).
16
+ *
17
+ * `last_app_events` is an array of `{ conversationId: Uint8Array, appEventBytes:
18
+ * Uint8Array }`. Pass `[]` to suppress catchup data.
19
+ */
20
+ buildLinkingTicket(new_device_id: Uint8Array, new_device_kp: Uint8Array, last_app_events: any, now_ms: number): Promise<any>;
10
21
  consumeLinkingTicket(ticket: any, now_ms: number): Promise<void>;
11
22
  createConversation(name: string | null | undefined, now_ms: number): Promise<Uint8Array>;
12
23
  deviceId(): Uint8Array;
24
+ /**
25
+ * Export a derived secret from a conversation's MLS exporter ([CR-8]).
26
+ *
27
+ * The core wraps the bytes in `Zeroizing<Vec<u8>>`; once they cross the WASM/JS
28
+ * boundary they live in a regular JS `Uint8Array` and JS GC controls the lifetime.
29
+ * Callers in JS MUST treat the buffer as a secret — never log, persist only
30
+ * encrypted, and clear the typed array when done (`u8.fill(0)`).
31
+ */
32
+ exportConversationSecret(conv_id: Uint8Array, label: string, context: Uint8Array, length: number): Uint8Array;
33
+ /**
34
+ * [CR-7] Export the MLS state snapshot for one conversation.
35
+ */
36
+ exportConversationStateSnapshot(conv_id: Uint8Array, now_ms: number): Uint8Array;
13
37
  freshKeyPackage(): Uint8Array;
14
38
  /**
15
39
  * Generate a fresh identity. Returns the export bytes — store them under the user's account.
16
40
  */
17
41
  static generateIdentity(): Uint8Array;
42
+ /**
43
+ * [CR-7] Import an MLS state snapshot produced by another device of the same
44
+ * user identity. Returns the conversation id the snapshot describes.
45
+ */
46
+ importStateSnapshot(snapshot_bytes: Uint8Array, now_ms: number): Promise<Uint8Array>;
18
47
  /**
19
48
  * Initialise the client. Pass an exported identity (use `generateIdentity()` for a fresh one).
49
+ *
50
+ * `device_signing_secret_key`: optional 32-byte Ed25519 secret
51
+ * key the SDK should adopt as its device signing key on FIRST
52
+ * init. When supplied, `device_id` becomes
53
+ * `SHA-256(public_key_of(secret))` — deterministic from the
54
+ * caller's input, so the host can align the SDK's `device_id`
55
+ * (which the SDK stamps into every envelope's `sender_device`)
56
+ * with whatever device_id the auth layer registered in the JWT.
57
+ * Ignored on re-init when a `LocalDevice` is already persisted
58
+ * in `storage`. Pass `null`/`undefined` to keep the legacy
59
+ * random-key behaviour.
20
60
  */
21
- static init(identity_export: Uint8Array, device_label: string, storage: JsStorage, transport: JsTransport, now_ms: number): Promise<PingClient>;
61
+ static init(identity_export: Uint8Array, device_label: string, storage: JsStorage, transport: JsTransport, now_ms: number, device_signing_secret_key?: Uint8Array | null): Promise<PingClient>;
22
62
  joinConversation(welcome: any, now_ms: number): Promise<Uint8Array>;
23
63
  listConversations(): any;
24
64
  onMessage(cb: Function): void;
25
65
  processEnvelope(env: any, now_ms: number): Promise<any>;
26
66
  removeMembers(conv_id: Uint8Array, leaves: Uint32Array, now_ms: number): Promise<void>;
67
+ /**
68
+ * Revoke a device ([CR-2]). Returns an array of Commit envelopes — one per
69
+ * conversation the device was a locally-known leaf in. Each envelope has already
70
+ * been handed to the transport; the host's hand-back is for any additional
71
+ * receipt-of-revocation handling.
72
+ */
73
+ revokeDevice(device_id: Uint8Array, now_ms: number): Promise<any>;
27
74
  send(conv_id: Uint8Array, plaintext: Uint8Array, now_ms: number): Promise<any>;
28
75
  syncConversations(now_ms: number): Promise<any>;
29
76
  userId(): Uint8Array;
@@ -119,27 +166,59 @@ export function __wbgtest_module_signature(): bigint | undefined;
119
166
 
120
167
  export function _start(): void;
121
168
 
169
+ /**
170
+ * HPKE-open a sealed `LinkingTicket` ([CR-3]). Pure function — usable without an
171
+ * initialized `PingClient`, which is exactly the situation the receiving device is in
172
+ * (pre-bootstrap; no identity yet).
173
+ *
174
+ * `new_device_priv` must be 32 bytes (X25519 private key). Errors are returned with a
175
+ * generic message — the new device shouldn't get to discriminate "wrong key" from
176
+ * "tampered ciphertext".
177
+ * [CR-13] Decode a `LinkingTicket.catchup_snapshot` blob. Pure function; no client
178
+ * required. The new device calls this after `consumeLinkingTicket` to populate its
179
+ * initial UI.
180
+ */
181
+ export function decodeCatchupSnapshot(snapshot_bytes: Uint8Array): any;
182
+
183
+ export function openLinkingTicket(sealed: Uint8Array, new_device_priv: Uint8Array): any;
184
+
185
+ /**
186
+ * HPKE-seal a `LinkingTicket` ([CR-3]). Pure function — does not need an initialized
187
+ * `PingClient`, so it's exported at module scope and the JS facade can call it from an
188
+ * ephemeral worker (no full client init needed on the sender side).
189
+ *
190
+ * `new_device_pub` must be 32 bytes. Returns CBOR-encoded sealed bytes.
191
+ */
192
+ export function sealLinkingTicket(ticket: any, new_device_pub: Uint8Array): Uint8Array;
193
+
122
194
  export type InitInput = RequestInfo | URL | Response | BufferSource | WebAssembly.Module;
123
195
 
124
196
  export interface InitOutput {
125
197
  readonly memory: WebAssembly.Memory;
126
198
  readonly __wbg_pingclient_free: (a: number, b: number) => void;
127
- readonly pingclient_addMembers: (a: number, b: number, c: number, d: number, e: number, f: number) => number;
128
- readonly pingclient_buildLinkingTicket: (a: number, b: number, c: number, d: number, e: number, f: number) => number;
199
+ readonly decodeCatchupSnapshot: (a: number, b: number, c: number) => void;
200
+ readonly openLinkingTicket: (a: number, b: number, c: number, d: number, e: number) => void;
201
+ readonly pingclient_addMembers: (a: number, b: number, c: number, d: number, e: number) => number;
202
+ readonly pingclient_buildLinkingTicket: (a: number, b: number, c: number, d: number, e: number, f: number, g: number) => number;
129
203
  readonly pingclient_consumeLinkingTicket: (a: number, b: number, c: number) => number;
130
204
  readonly pingclient_createConversation: (a: number, b: number, c: number, d: number) => number;
131
205
  readonly pingclient_deviceId: (a: number, b: number) => void;
206
+ readonly pingclient_exportConversationSecret: (a: number, b: number, c: number, d: number, e: number, f: number, g: number, h: number, i: number) => void;
207
+ readonly pingclient_exportConversationStateSnapshot: (a: number, b: number, c: number, d: number, e: number) => void;
132
208
  readonly pingclient_freshKeyPackage: (a: number, b: number) => void;
133
209
  readonly pingclient_generateIdentity: (a: number) => void;
134
- readonly pingclient_init: (a: number, b: number, c: number, d: number, e: number, f: number, g: number) => number;
210
+ readonly pingclient_importStateSnapshot: (a: number, b: number, c: number, d: number) => number;
211
+ readonly pingclient_init: (a: number, b: number, c: number, d: number, e: number, f: number, g: number, h: number, i: number) => number;
135
212
  readonly pingclient_joinConversation: (a: number, b: number, c: number) => number;
136
213
  readonly pingclient_listConversations: (a: number, b: number) => void;
137
214
  readonly pingclient_onMessage: (a: number, b: number) => void;
138
215
  readonly pingclient_processEnvelope: (a: number, b: number, c: number) => number;
139
216
  readonly pingclient_removeMembers: (a: number, b: number, c: number, d: number, e: number, f: number) => number;
217
+ readonly pingclient_revokeDevice: (a: number, b: number, c: number, d: number) => number;
140
218
  readonly pingclient_send: (a: number, b: number, c: number, d: number, e: number, f: number) => number;
141
219
  readonly pingclient_syncConversations: (a: number, b: number) => number;
142
220
  readonly pingclient_userId: (a: number, b: number) => void;
221
+ readonly sealLinkingTicket: (a: number, b: number, c: number, d: number) => void;
143
222
  readonly _start: () => void;
144
223
  readonly __wbg_wasmbindgentestcontext_free: (a: number, b: number) => void;
145
224
  readonly __wbgbench_dump: (a: number) => void;
@@ -156,9 +235,9 @@ export interface InitOutput {
156
235
  readonly wasmbindgentestcontext_include_ignored: (a: number, b: number) => void;
157
236
  readonly wasmbindgentestcontext_new: (a: number) => number;
158
237
  readonly wasmbindgentestcontext_run: (a: number, b: number, c: number) => number;
159
- readonly __wasm_bindgen_func_elem_2213: (a: number, b: number, c: number, d: number, e: number) => void;
160
- readonly __wasm_bindgen_func_elem_2203: (a: number, b: number, c: number, d: number) => void;
161
- readonly __wasm_bindgen_func_elem_2212: (a: number, b: number, c: number, d: number) => void;
238
+ readonly __wasm_bindgen_func_elem_2611: (a: number, b: number, c: number, d: number, e: number) => void;
239
+ readonly __wasm_bindgen_func_elem_2601: (a: number, b: number, c: number, d: number) => void;
240
+ readonly __wasm_bindgen_func_elem_2610: (a: number, b: number, c: number, d: number) => void;
162
241
  readonly __wbindgen_export: (a: number, b: number) => number;
163
242
  readonly __wbindgen_export2: (a: number, b: number, c: number, d: number) => number;
164
243
  readonly __wbindgen_export3: (a: number) => void;