ping-openmls-sdk 0.1.2 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.d.cts CHANGED
@@ -1,18 +1,30 @@
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';
1
+ import { M as MessageEnvelope, S as Storage, T as Transport, C as ConversationId, K as KeyPackageEntry, a as ConversationMeta, L as LinkingTicket, U as UserId, b as DeviceId, I as IncomingMessage, c as CatchupAppEvent, d as CatchupSnapshotView } from './types-DYtsj5dy.cjs';
2
+ export { B as Bytes, e as CatchupSnapshotEntry, f as DeviceInfo, D as DiscoveredDevice, H as Hlc, g as MessageKind } from './types-DYtsj5dy.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";
6
+ /**
7
+ * Ping wire contract version. Bumped on incompatible profile changes.
8
+ *
9
+ * Currently `2` — CR-6 changes the `content_hash` semantics for `Application` envelopes
10
+ * (now hashed over plaintext, not the MLS ciphertext). Receivers run a 90-day dual-accept
11
+ * window per Q-ARCH-02; see [[validate]] for the per-version routing.
12
+ */
13
+ declare const PING_WIRE_CONTRACT_VERSION = 2;
14
+ /** Lowest envelope version accepted by [[validate]] during the dual-accept window. */
15
+ declare const WIRE_VERSION_MIN_ACCEPTED = 1;
16
+ /** Canonical CBOR content type. Production transports SHOULD use this. */
17
+ declare const CONTENT_TYPE_CBOR = "application/vnd.ping.wire.v2+cbor";
10
18
  /** JSON projection — development and tooling only. Production MUST NOT use this. */
11
- declare const CONTENT_TYPE_JSON = "application/vnd.ping.wire.v1+json";
19
+ declare const CONTENT_TYPE_JSON = "application/vnd.ping.wire.v2+json";
20
+ /** Legacy v1 CBOR type, accepted during the CR-6 dual-accept window. */
21
+ declare const CONTENT_TYPE_CBOR_LEGACY_V1 = "application/vnd.ping.wire.v1+cbor";
22
+ /** Legacy v1 JSON type, accepted during the CR-6 dual-accept window. */
23
+ declare const CONTENT_TYPE_JSON_LEGACY_V1 = "application/vnd.ping.wire.v1+json";
12
24
  /** Header used over transports that don't surface a content type (e.g. WebSocket). */
13
25
  declare const PING_WIRE_CONTRACT_HEADER = "X-Ping-Wire-Contract";
14
26
  /** Header value: `ping/<VERSION>`. */
15
- declare const PING_WIRE_CONTRACT_HEADER_VALUE = "ping/1";
27
+ declare const PING_WIRE_CONTRACT_HEADER_VALUE = "ping/2";
16
28
  type ValidationErrorKind = "UnsupportedVersion" | "BadConversationIdLen" | "BadSenderDeviceLen" | "ContentHashMismatch";
17
29
  declare class ValidationError extends Error {
18
30
  readonly kind: ValidationErrorKind;
@@ -24,12 +36,25 @@ declare class ValidationError extends Error {
24
36
  * Receivers claiming contract conformance MUST call this before applying any envelope.
25
37
  * External consumers using the raw envelope under their own profile are not bound by
26
38
  * these rules.
39
+ *
40
+ * CR-6 dual-accept window: `v ∈ [WIRE_VERSION_MIN_ACCEPTED, PING_WIRE_CONTRACT_VERSION]`
41
+ * passes the version gate. For v=2 `Application` envelopes the content_hash is over
42
+ * plaintext, so this function skips that check — callers MUST run
43
+ * [[verifyApplicationContentHash]] after MLS decrypt to close the loop.
27
44
  */
28
45
  declare function validate(env: MessageEnvelope): Promise<void>;
46
+ /**
47
+ * Verify a v=2 `Application` envelope's content_hash against the decrypted plaintext.
48
+ *
49
+ * Caller MUST run this after MLS decrypt for application envelopes whose `v >= 2`. For
50
+ * v=1 envelopes or handshake kinds this is a no-op (those are covered by [[validate]]).
51
+ */
52
+ declare function verifyApplicationContentHash(env: MessageEnvelope, plaintext: Uint8Array): Promise<void>;
29
53
  /**
30
54
  * True if the supplied content type negotiates the ping-wire-contract.
31
55
  *
32
- * Tolerates whitespace and parameters (e.g. `; charset=utf-8`).
56
+ * Accepts the v2 canonical types AND the v1 legacy types during the CR-6 dual-accept
57
+ * window. Tolerates whitespace and parameters (e.g. `; charset=utf-8`).
33
58
  */
34
59
  declare function negotiates(contentType: string | null | undefined): boolean;
35
60
 
@@ -44,7 +69,9 @@ interface ClientConfig {
44
69
  interface Conversation {
45
70
  readonly id: ConversationId;
46
71
  send(plaintext: Uint8Array): Promise<MessageEnvelope>;
47
- addMembers(keyPackages: Uint8Array[]): Promise<void>;
72
+ /** [CR-2] Add members by (device_id, KeyPackage) pair. Hosts typically pull these
73
+ * straight from `transport.discoverDevices(userId)`. */
74
+ addMembers(entries: KeyPackageEntry[]): Promise<void>;
48
75
  removeMembers(leafIndexes: number[]): Promise<void>;
49
76
  meta(): ConversationMeta;
50
77
  }
@@ -63,6 +90,30 @@ declare class MessagingClient {
63
90
  private constructor();
64
91
  /** Generate a fresh identity. The returned bytes are a secret — store them encrypted. */
65
92
  static generateIdentity(): Promise<Uint8Array>;
93
+ /**
94
+ * HPKE-seal a `LinkingTicket` for delivery to a new device ([CR-3]).
95
+ *
96
+ * Returns CBOR-encoded bytes safe to relay through an untrusted inbox. The new device's
97
+ * X25519 public key (`newDevicePub`) is exchanged out-of-band — typically as part of a
98
+ * QR-encoded handshake on the linking screen. Pure function; runs in an ephemeral
99
+ * worker so no live `MessagingClient` is required on the sender side.
100
+ *
101
+ * Suite: `DHKEM(X25519, HKDF-SHA256), HKDF-SHA256, AES-128-GCM` (matches MLS).
102
+ */
103
+ static sealLinkingTicket(ticket: LinkingTicket, newDevicePub: Uint8Array): Promise<Uint8Array>;
104
+ /**
105
+ * HPKE-open a sealed `LinkingTicket` on the new device ([CR-3]).
106
+ *
107
+ * The receiving device hasn't booted a `MessagingClient` yet — it only has its X25519
108
+ * ephemeral keypair. This static method runs the open in an ephemeral worker, returning
109
+ * the cleartext ticket which the host then feeds to `init()` + `consumeLinkingTicket()`.
110
+ *
111
+ * On failure the error message is deliberately generic (no discriminator between "wrong
112
+ * key" vs "tampered ciphertext") so a probing attacker can't tell the new device's key
113
+ * was the issue.
114
+ */
115
+ static openLinkingTicket(sealed: Uint8Array, newDevicePriv: Uint8Array): Promise<LinkingTicket>;
116
+ private static ephemeralWorkerCall;
66
117
  static init(cfg: ClientConfig): Promise<MessagingClient>;
67
118
  /** Internal: route an inbound envelope to the right handler. */
68
119
  private dispatchIncoming;
@@ -83,8 +134,81 @@ declare class MessagingClient {
83
134
  listConversations(): Promise<ConversationMeta[]>;
84
135
  syncConversations(): Promise<IncomingMessage[]>;
85
136
  processEnvelope(envelope: MessageEnvelope): Promise<IncomingMessage | null>;
86
- buildLinkingTicket(newDeviceId: DeviceId, newDeviceKp: Uint8Array): Promise<LinkingTicket>;
137
+ /**
138
+ * Build a `LinkingTicket` for a new device.
139
+ *
140
+ * [CR-13] `lastAppEvents` is host-supplied per-conversation "what you missed" data
141
+ * (decrypted AppEvent bytes the new device will render before sync catches up).
142
+ * Pass `[]` to suppress catchup data — the new device will see an empty conversation
143
+ * list until normal sync runs.
144
+ *
145
+ * Caller is responsible for HPKE-sealing the returned ticket via
146
+ * `MessagingClient.sealLinkingTicket` before transmitting (CR-3).
147
+ */
148
+ buildLinkingTicket(newDeviceId: DeviceId, newDeviceKp: Uint8Array, lastAppEvents?: CatchupAppEvent[]): Promise<LinkingTicket>;
149
+ /**
150
+ * Decode a `LinkingTicket.catchup_snapshot` blob ([CR-13]).
151
+ *
152
+ * Pure WASM — runs in an ephemeral worker so the new device can call this before
153
+ * `MessagingClient.init()` completes (just like `openLinkingTicket`). Returns the
154
+ * structured view: per-conversation metas + last-known AppEvent bytes.
155
+ *
156
+ * Throws on malformed or oversized inputs. Empty bytes return `null` — that's the
157
+ * "no catchup data" case (sender passed `[]` to buildLinkingTicket).
158
+ */
159
+ static decodeCatchupSnapshot(snapshotBytes: Uint8Array): Promise<CatchupSnapshotView | null>;
87
160
  consumeLinkingTicket(ticket: LinkingTicket): Promise<void>;
161
+ /**
162
+ * Revoke a device ([CR-2]).
163
+ *
164
+ * Removes `deviceId`'s leaf from every conversation where this client locally
165
+ * tracked the device→leaf mapping (i.e. conversations where this client itself
166
+ * admitted the device via `addMembers`, or where this client is the device being
167
+ * revoked). The SDK has already broadcast each Commit envelope via
168
+ * `transport.send`; the returned array is for any additional host-side handling
169
+ * (audit logs, UI). An empty array is a valid outcome — the device wasn't locally
170
+ * known anywhere.
171
+ *
172
+ * **Scope note.** Conversations where a peer admitted the device on this client's
173
+ * behalf are silently skipped — the leaf index isn't locally known. Host can fall
174
+ * back to `transport.discoverDevices` + a manual `remove_members(leafIndex)` for
175
+ * those, or wait for the affected user to revoke from a device that did the admit.
176
+ * See `docs/architecture/multi-device.md §Device removal`.
177
+ */
178
+ revokeDevice(deviceId: DeviceId): Promise<MessageEnvelope[]>;
179
+ /**
180
+ * Export a portable MLS state snapshot for one conversation ([CR-7]).
181
+ *
182
+ * Returned bytes can be embedded in a recovery blob or shipped through a
183
+ * linking ticket so another device of the same user identity can re-attach to
184
+ * the group via [[importStateSnapshot]]. The bytes contain past epoch secrets;
185
+ * never log, never persist unencrypted, wipe (`.fill(0)`) after use.
186
+ */
187
+ exportConversationStateSnapshot(conv: ConversationId): Promise<Uint8Array>;
188
+ /**
189
+ * Import a `GroupStateSnapshot` from another device of the same user identity
190
+ * ([CR-7]).
191
+ *
192
+ * On success, the conversation appears in [[listConversations]] and decryption
193
+ * of subsequent peer-originated traffic works against the imported state. For
194
+ * full multi-device participation, the recovered device SHOULD then issue an
195
+ * MLS Update Proposal to rotate onto a fresh leaf (post-v1 follow-up — see
196
+ * `docs/design/CR4_CR7_PERSISTENCE.md` open question #3).
197
+ */
198
+ importStateSnapshot(snapshotBytes: Uint8Array): Promise<ConversationId>;
199
+ /**
200
+ * Export a derived secret from a conversation's MLS exporter ([CR-8]).
201
+ *
202
+ * Used to seed the ephemeral channel (`ping/ephemeral`), call-media keys
203
+ * (`ping/calls/media/<call_id>`), and call-ephemeral framer keys
204
+ * (`ping/calls/ephemeral/<call_id>`). The returned bytes are a secret — never log,
205
+ * persist only encrypted, and call `bytes.fill(0)` after use to wipe the typed array.
206
+ *
207
+ * Same `(conversationId, current_epoch, label, context, length)` produces the same
208
+ * bytes across every binding (Rust core, native UniFFI, WASM/JS). Cross-platform
209
+ * byte-equality is enforced by `protocol/conformance/export_secret/`.
210
+ */
211
+ exportConversationSecret(conv: ConversationId, label: string, context: Uint8Array, length: number): Promise<Uint8Array>;
88
212
  /** Subscribe to decrypted application messages. */
89
213
  onMessage(handler: (m: IncomingMessage) => void): () => void;
90
214
  /** Fired after every state-changing event for a conversation (Commit, member changes). */
@@ -100,4 +224,4 @@ declare class MessagingClient {
100
224
  private handleWorker;
101
225
  }
102
226
 
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 };
227
+ export { CONTENT_TYPE_CBOR, CONTENT_TYPE_CBOR_LEGACY_V1, CONTENT_TYPE_JSON, CONTENT_TYPE_JSON_LEGACY_V1, CatchupAppEvent, CatchupSnapshotView, type ClientConfig, type Conversation, ConversationId, ConversationMeta, DeviceId, IncomingMessage, KeyPackageEntry, LinkingTicket, MessageEnvelope, MessagingClient, PING_WIRE_CONTRACT_HEADER, PING_WIRE_CONTRACT_HEADER_VALUE, PING_WIRE_CONTRACT_VERSION, Storage, Transport, UserId, ValidationError, WIRE_VERSION_MIN_ACCEPTED, negotiates, validate, verifyApplicationContentHash };
package/dist/index.d.ts CHANGED
@@ -1,18 +1,30 @@
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';
1
+ import { M as MessageEnvelope, S as Storage, T as Transport, C as ConversationId, K as KeyPackageEntry, a as ConversationMeta, L as LinkingTicket, U as UserId, b as DeviceId, I as IncomingMessage, c as CatchupAppEvent, d as CatchupSnapshotView } from './types-DYtsj5dy.js';
2
+ export { B as Bytes, e as CatchupSnapshotEntry, f as DeviceInfo, D as DiscoveredDevice, H as Hlc, g as MessageKind } from './types-DYtsj5dy.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";
6
+ /**
7
+ * Ping wire contract version. Bumped on incompatible profile changes.
8
+ *
9
+ * Currently `2` — CR-6 changes the `content_hash` semantics for `Application` envelopes
10
+ * (now hashed over plaintext, not the MLS ciphertext). Receivers run a 90-day dual-accept
11
+ * window per Q-ARCH-02; see [[validate]] for the per-version routing.
12
+ */
13
+ declare const PING_WIRE_CONTRACT_VERSION = 2;
14
+ /** Lowest envelope version accepted by [[validate]] during the dual-accept window. */
15
+ declare const WIRE_VERSION_MIN_ACCEPTED = 1;
16
+ /** Canonical CBOR content type. Production transports SHOULD use this. */
17
+ declare const CONTENT_TYPE_CBOR = "application/vnd.ping.wire.v2+cbor";
10
18
  /** JSON projection — development and tooling only. Production MUST NOT use this. */
11
- declare const CONTENT_TYPE_JSON = "application/vnd.ping.wire.v1+json";
19
+ declare const CONTENT_TYPE_JSON = "application/vnd.ping.wire.v2+json";
20
+ /** Legacy v1 CBOR type, accepted during the CR-6 dual-accept window. */
21
+ declare const CONTENT_TYPE_CBOR_LEGACY_V1 = "application/vnd.ping.wire.v1+cbor";
22
+ /** Legacy v1 JSON type, accepted during the CR-6 dual-accept window. */
23
+ declare const CONTENT_TYPE_JSON_LEGACY_V1 = "application/vnd.ping.wire.v1+json";
12
24
  /** Header used over transports that don't surface a content type (e.g. WebSocket). */
13
25
  declare const PING_WIRE_CONTRACT_HEADER = "X-Ping-Wire-Contract";
14
26
  /** Header value: `ping/<VERSION>`. */
15
- declare const PING_WIRE_CONTRACT_HEADER_VALUE = "ping/1";
27
+ declare const PING_WIRE_CONTRACT_HEADER_VALUE = "ping/2";
16
28
  type ValidationErrorKind = "UnsupportedVersion" | "BadConversationIdLen" | "BadSenderDeviceLen" | "ContentHashMismatch";
17
29
  declare class ValidationError extends Error {
18
30
  readonly kind: ValidationErrorKind;
@@ -24,12 +36,25 @@ declare class ValidationError extends Error {
24
36
  * Receivers claiming contract conformance MUST call this before applying any envelope.
25
37
  * External consumers using the raw envelope under their own profile are not bound by
26
38
  * these rules.
39
+ *
40
+ * CR-6 dual-accept window: `v ∈ [WIRE_VERSION_MIN_ACCEPTED, PING_WIRE_CONTRACT_VERSION]`
41
+ * passes the version gate. For v=2 `Application` envelopes the content_hash is over
42
+ * plaintext, so this function skips that check — callers MUST run
43
+ * [[verifyApplicationContentHash]] after MLS decrypt to close the loop.
27
44
  */
28
45
  declare function validate(env: MessageEnvelope): Promise<void>;
46
+ /**
47
+ * Verify a v=2 `Application` envelope's content_hash against the decrypted plaintext.
48
+ *
49
+ * Caller MUST run this after MLS decrypt for application envelopes whose `v >= 2`. For
50
+ * v=1 envelopes or handshake kinds this is a no-op (those are covered by [[validate]]).
51
+ */
52
+ declare function verifyApplicationContentHash(env: MessageEnvelope, plaintext: Uint8Array): Promise<void>;
29
53
  /**
30
54
  * True if the supplied content type negotiates the ping-wire-contract.
31
55
  *
32
- * Tolerates whitespace and parameters (e.g. `; charset=utf-8`).
56
+ * Accepts the v2 canonical types AND the v1 legacy types during the CR-6 dual-accept
57
+ * window. Tolerates whitespace and parameters (e.g. `; charset=utf-8`).
33
58
  */
34
59
  declare function negotiates(contentType: string | null | undefined): boolean;
35
60
 
@@ -44,7 +69,9 @@ interface ClientConfig {
44
69
  interface Conversation {
45
70
  readonly id: ConversationId;
46
71
  send(plaintext: Uint8Array): Promise<MessageEnvelope>;
47
- addMembers(keyPackages: Uint8Array[]): Promise<void>;
72
+ /** [CR-2] Add members by (device_id, KeyPackage) pair. Hosts typically pull these
73
+ * straight from `transport.discoverDevices(userId)`. */
74
+ addMembers(entries: KeyPackageEntry[]): Promise<void>;
48
75
  removeMembers(leafIndexes: number[]): Promise<void>;
49
76
  meta(): ConversationMeta;
50
77
  }
@@ -63,6 +90,30 @@ declare class MessagingClient {
63
90
  private constructor();
64
91
  /** Generate a fresh identity. The returned bytes are a secret — store them encrypted. */
65
92
  static generateIdentity(): Promise<Uint8Array>;
93
+ /**
94
+ * HPKE-seal a `LinkingTicket` for delivery to a new device ([CR-3]).
95
+ *
96
+ * Returns CBOR-encoded bytes safe to relay through an untrusted inbox. The new device's
97
+ * X25519 public key (`newDevicePub`) is exchanged out-of-band — typically as part of a
98
+ * QR-encoded handshake on the linking screen. Pure function; runs in an ephemeral
99
+ * worker so no live `MessagingClient` is required on the sender side.
100
+ *
101
+ * Suite: `DHKEM(X25519, HKDF-SHA256), HKDF-SHA256, AES-128-GCM` (matches MLS).
102
+ */
103
+ static sealLinkingTicket(ticket: LinkingTicket, newDevicePub: Uint8Array): Promise<Uint8Array>;
104
+ /**
105
+ * HPKE-open a sealed `LinkingTicket` on the new device ([CR-3]).
106
+ *
107
+ * The receiving device hasn't booted a `MessagingClient` yet — it only has its X25519
108
+ * ephemeral keypair. This static method runs the open in an ephemeral worker, returning
109
+ * the cleartext ticket which the host then feeds to `init()` + `consumeLinkingTicket()`.
110
+ *
111
+ * On failure the error message is deliberately generic (no discriminator between "wrong
112
+ * key" vs "tampered ciphertext") so a probing attacker can't tell the new device's key
113
+ * was the issue.
114
+ */
115
+ static openLinkingTicket(sealed: Uint8Array, newDevicePriv: Uint8Array): Promise<LinkingTicket>;
116
+ private static ephemeralWorkerCall;
66
117
  static init(cfg: ClientConfig): Promise<MessagingClient>;
67
118
  /** Internal: route an inbound envelope to the right handler. */
68
119
  private dispatchIncoming;
@@ -83,8 +134,81 @@ declare class MessagingClient {
83
134
  listConversations(): Promise<ConversationMeta[]>;
84
135
  syncConversations(): Promise<IncomingMessage[]>;
85
136
  processEnvelope(envelope: MessageEnvelope): Promise<IncomingMessage | null>;
86
- buildLinkingTicket(newDeviceId: DeviceId, newDeviceKp: Uint8Array): Promise<LinkingTicket>;
137
+ /**
138
+ * Build a `LinkingTicket` for a new device.
139
+ *
140
+ * [CR-13] `lastAppEvents` is host-supplied per-conversation "what you missed" data
141
+ * (decrypted AppEvent bytes the new device will render before sync catches up).
142
+ * Pass `[]` to suppress catchup data — the new device will see an empty conversation
143
+ * list until normal sync runs.
144
+ *
145
+ * Caller is responsible for HPKE-sealing the returned ticket via
146
+ * `MessagingClient.sealLinkingTicket` before transmitting (CR-3).
147
+ */
148
+ buildLinkingTicket(newDeviceId: DeviceId, newDeviceKp: Uint8Array, lastAppEvents?: CatchupAppEvent[]): Promise<LinkingTicket>;
149
+ /**
150
+ * Decode a `LinkingTicket.catchup_snapshot` blob ([CR-13]).
151
+ *
152
+ * Pure WASM — runs in an ephemeral worker so the new device can call this before
153
+ * `MessagingClient.init()` completes (just like `openLinkingTicket`). Returns the
154
+ * structured view: per-conversation metas + last-known AppEvent bytes.
155
+ *
156
+ * Throws on malformed or oversized inputs. Empty bytes return `null` — that's the
157
+ * "no catchup data" case (sender passed `[]` to buildLinkingTicket).
158
+ */
159
+ static decodeCatchupSnapshot(snapshotBytes: Uint8Array): Promise<CatchupSnapshotView | null>;
87
160
  consumeLinkingTicket(ticket: LinkingTicket): Promise<void>;
161
+ /**
162
+ * Revoke a device ([CR-2]).
163
+ *
164
+ * Removes `deviceId`'s leaf from every conversation where this client locally
165
+ * tracked the device→leaf mapping (i.e. conversations where this client itself
166
+ * admitted the device via `addMembers`, or where this client is the device being
167
+ * revoked). The SDK has already broadcast each Commit envelope via
168
+ * `transport.send`; the returned array is for any additional host-side handling
169
+ * (audit logs, UI). An empty array is a valid outcome — the device wasn't locally
170
+ * known anywhere.
171
+ *
172
+ * **Scope note.** Conversations where a peer admitted the device on this client's
173
+ * behalf are silently skipped — the leaf index isn't locally known. Host can fall
174
+ * back to `transport.discoverDevices` + a manual `remove_members(leafIndex)` for
175
+ * those, or wait for the affected user to revoke from a device that did the admit.
176
+ * See `docs/architecture/multi-device.md §Device removal`.
177
+ */
178
+ revokeDevice(deviceId: DeviceId): Promise<MessageEnvelope[]>;
179
+ /**
180
+ * Export a portable MLS state snapshot for one conversation ([CR-7]).
181
+ *
182
+ * Returned bytes can be embedded in a recovery blob or shipped through a
183
+ * linking ticket so another device of the same user identity can re-attach to
184
+ * the group via [[importStateSnapshot]]. The bytes contain past epoch secrets;
185
+ * never log, never persist unencrypted, wipe (`.fill(0)`) after use.
186
+ */
187
+ exportConversationStateSnapshot(conv: ConversationId): Promise<Uint8Array>;
188
+ /**
189
+ * Import a `GroupStateSnapshot` from another device of the same user identity
190
+ * ([CR-7]).
191
+ *
192
+ * On success, the conversation appears in [[listConversations]] and decryption
193
+ * of subsequent peer-originated traffic works against the imported state. For
194
+ * full multi-device participation, the recovered device SHOULD then issue an
195
+ * MLS Update Proposal to rotate onto a fresh leaf (post-v1 follow-up — see
196
+ * `docs/design/CR4_CR7_PERSISTENCE.md` open question #3).
197
+ */
198
+ importStateSnapshot(snapshotBytes: Uint8Array): Promise<ConversationId>;
199
+ /**
200
+ * Export a derived secret from a conversation's MLS exporter ([CR-8]).
201
+ *
202
+ * Used to seed the ephemeral channel (`ping/ephemeral`), call-media keys
203
+ * (`ping/calls/media/<call_id>`), and call-ephemeral framer keys
204
+ * (`ping/calls/ephemeral/<call_id>`). The returned bytes are a secret — never log,
205
+ * persist only encrypted, and call `bytes.fill(0)` after use to wipe the typed array.
206
+ *
207
+ * Same `(conversationId, current_epoch, label, context, length)` produces the same
208
+ * bytes across every binding (Rust core, native UniFFI, WASM/JS). Cross-platform
209
+ * byte-equality is enforced by `protocol/conformance/export_secret/`.
210
+ */
211
+ exportConversationSecret(conv: ConversationId, label: string, context: Uint8Array, length: number): Promise<Uint8Array>;
88
212
  /** Subscribe to decrypted application messages. */
89
213
  onMessage(handler: (m: IncomingMessage) => void): () => void;
90
214
  /** Fired after every state-changing event for a conversation (Commit, member changes). */
@@ -100,4 +224,4 @@ declare class MessagingClient {
100
224
  private handleWorker;
101
225
  }
102
226
 
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 };
227
+ export { CONTENT_TYPE_CBOR, CONTENT_TYPE_CBOR_LEGACY_V1, CONTENT_TYPE_JSON, CONTENT_TYPE_JSON_LEGACY_V1, CatchupAppEvent, CatchupSnapshotView, type ClientConfig, type Conversation, ConversationId, ConversationMeta, DeviceId, IncomingMessage, KeyPackageEntry, LinkingTicket, MessageEnvelope, MessagingClient, PING_WIRE_CONTRACT_HEADER, PING_WIRE_CONTRACT_HEADER_VALUE, PING_WIRE_CONTRACT_VERSION, Storage, Transport, UserId, ValidationError, WIRE_VERSION_MIN_ACCEPTED, negotiates, validate, verifyApplicationContentHash };