@droponair/sdk-js 0.24.3 → 0.25.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +66 -14
- package/README.md +52 -7
- package/dist/attachment/attachment-client.d.ts +1 -1
- package/dist/attachment/attachment-client.js +1 -1
- package/dist/core/messaging-client.d.ts +10 -3
- package/dist/core/messaging-client.js +19 -10
- package/dist/core/types.d.ts +85 -4
- package/dist/crypto/crypto-service.d.ts +10 -4
- package/dist/crypto/crypto-service.js +41 -44
- package/dist/crypto/nacl-identity-provider.d.ts +25 -0
- package/dist/crypto/nacl-identity-provider.js +60 -0
- package/dist/crypto/webcrypto-identity-provider.d.ts +116 -0
- package/dist/crypto/webcrypto-identity-provider.js +265 -0
- package/dist/index.d.ts +29 -0
- package/dist/index.js +49 -5
- package/dist/transport/auto-select.d.ts +1 -1
- package/dist/transport/auto-select.js +1 -1
- package/dist/transport/protobuf-codec.d.ts +1 -1
- package/dist/transport/protobuf-codec.js +1 -1
- package/dist/transport/sse-transport.d.ts +1 -1
- package/dist/transport/sse-transport.js +1 -1
- package/dist/transport/webtransport-transport.d.ts +3 -3
- package/dist/transport/webtransport-transport.js +3 -3
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +27 -8
package/CHANGELOG.md
CHANGED
|
@@ -1,12 +1,65 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.25.1
|
|
4
|
+
|
|
5
|
+
### Changed
|
|
6
|
+
|
|
7
|
+
- Documentation only, no behaviour or API change. Comments and notes that shipped in the
|
|
8
|
+
package described the platform's own internals rather than the integration surface: some
|
|
9
|
+
named components a consumer never talks to, and some carried planning tags that mean nothing
|
|
10
|
+
outside our own backlog. They are rewritten in terms of what the code does and what the
|
|
11
|
+
platform offers.
|
|
12
|
+
- Where availability used to be described as a specific plan flag, it now points at the pricing
|
|
13
|
+
page and the app's subscription, which are the two places that stay correct as plans change.
|
|
14
|
+
- `PROTOCOL_VERSION` references are untouched. That is the contract a consumer integrates
|
|
15
|
+
against and it remains the way to tell what a relay supports.
|
|
16
|
+
- Em dashes are gone from the shipped text, per the house style. Each was judged in place
|
|
17
|
+
rather than swapped mechanically, so clauses read as sentences rather than comma splices.
|
|
18
|
+
|
|
19
|
+
## 0.25.0
|
|
20
|
+
|
|
21
|
+
### Added
|
|
22
|
+
|
|
23
|
+
- **`keyCustody` on the client**, reporting how the identity private key is held:
|
|
24
|
+
`'non-extractable'` when the key exists only inside the platform's crypto agent and cannot
|
|
25
|
+
be read by script, `'software'` when it is bytes the process can read. Every release before
|
|
26
|
+
this one behaved as `'software'`, and still does unless the new option below is used. Read it
|
|
27
|
+
rather than assuming: a consumer that believes it has the stronger custody and does not is
|
|
28
|
+
worse off than one that knows.
|
|
29
|
+
- **`createSecureIdentity()`** and **`InitializeOptions.identity`**, an opt-in identity whose
|
|
30
|
+
private key script cannot read. The key is generated non-extractable and used through the
|
|
31
|
+
agent for the key agreement, so an attacker who runs script on the origin can ask it to
|
|
32
|
+
derive while the page is open, and cannot steal the key for later or elsewhere.
|
|
33
|
+
- **`WebCryptoIdentityProvider`**, **`IndexedDbIdentityRecordStore`**,
|
|
34
|
+
**`MemoryIdentityRecordStore`** and the **`IdentityRecordStore`** interface are exported, so
|
|
35
|
+
custody can be implemented against something else entirely.
|
|
36
|
+
|
|
37
|
+
### Notes
|
|
38
|
+
|
|
39
|
+
- **Opt-in, and unchanged by default.** Passing no `identity` keeps exactly the previous
|
|
40
|
+
behaviour. The stronger path is offered, not imposed.
|
|
41
|
+
- **`createSecureIdentity()` returns `null` where the platform cannot deliver it**, rather than
|
|
42
|
+
throwing or quietly substituting something weaker. Two conditions are checked: whether the
|
|
43
|
+
primitive exists, and whether a key survives being stored and read back.
|
|
44
|
+
- **Availability differs by engine.** The protected path requires an X25519 key that persists.
|
|
45
|
+
Chromium provides that. WebKit generates and derives correctly and then loses the key when it
|
|
46
|
+
is stored, reporting the write as successful, so `createSecureIdentity()` returns `null`
|
|
47
|
+
there and the ordinary path is used. This is checked at runtime rather than assumed from a
|
|
48
|
+
user agent.
|
|
49
|
+
- **Existing identities can be carried over** with `migrateFromKeyStorage()`, which imports the
|
|
50
|
+
stored key, verifies it, and only then removes the readable copy. The public key is unchanged,
|
|
51
|
+
so peers still address the same identity and existing conversations keep decrypting. Nothing
|
|
52
|
+
is re-keyed and no history is lost.
|
|
53
|
+
- The wire format is untouched. Both paths perform the same X25519 agreement and derive the
|
|
54
|
+
same message key, which is covered by tests in both directions.
|
|
55
|
+
|
|
3
56
|
## 0.24.3
|
|
4
57
|
|
|
5
58
|
- Fix: broadcasts (and events/acks) misrouted. Inbound Ack/GroupAck discriminators now require a real SCREAMING_SNAKE ack token, so a BroadcastNotification (publisherId in the type slot) is no longer decoded as a GroupAck before reaching the broadcast branch. Completes the frame-classifier fix.
|
|
6
59
|
|
|
7
60
|
## 0.24.2
|
|
8
61
|
|
|
9
|
-
- Fix: E2EE group message decryption failed because the decrypt AAD bound `recipientId` to the group id instead of the receiving user's own id (which is what the sender encrypts against). Now matches the sender and the other SDKs. Requires
|
|
62
|
+
- Fix: E2EE group message decryption failed because the decrypt AAD bound `recipientId` to the group id instead of the receiving user's own id (which is what the sender encrypts against). Now matches the sender and the other SDKs. Requires a relay that preserves the sender timestamp on group notifications.
|
|
10
63
|
|
|
11
64
|
## 0.24.1
|
|
12
65
|
|
|
@@ -184,7 +237,7 @@ This project follows [Semantic Versioning](https://semver.org/).
|
|
|
184
237
|
|
|
185
238
|
### Changed
|
|
186
239
|
|
|
187
|
-
- **`revokeAttachment` accepts an `AttachmentRef`.** Calling `revokeAttachment(ref)` now also revokes the linked preview thumbnail (`ref.thumbnailAttachmentId`) in the same call. `revokeAttachment(attachmentId)` with a plain id string is unchanged and revokes only that one attachment. Additive
|
|
240
|
+
- **`revokeAttachment` accepts an `AttachmentRef`.** Calling `revokeAttachment(ref)` now also revokes the linked preview thumbnail (`ref.thumbnailAttachmentId`) in the same call. `revokeAttachment(attachmentId)` with a plain id string is unchanged and revokes only that one attachment. Additive, so no breaking change.
|
|
188
241
|
|
|
189
242
|
---
|
|
190
243
|
|
|
@@ -193,13 +246,13 @@ This project follows [Semantic Versioning](https://semver.org/).
|
|
|
193
246
|
### Added
|
|
194
247
|
|
|
195
248
|
- **Group read receipts.** When a group has `readReceiptsVisibleToGroup` enabled, a member's `markRead(messageId, groupId)` is fanned to every other member, not just the reader's own devices. The inbound `ReadReceiptEvent` now carries `fromUserId` so you know which member read the message. New `client.updateGroup(groupId, { name?, readReceiptsVisibleToGroup? })` controls the per-group flag (it is a group-level choice; off by default). Broadcasts stay own-device only by design.
|
|
196
|
-
- **Attachment revoke.** New `client.revokeAttachment(attachmentId)
|
|
197
|
-
- **Attachment preview thumbnails.** `prepareAttachmentAndUpload` accepts an optional `thumbnail` (and `thumbnailMimeType`). Entirely optional
|
|
249
|
+
- **Attachment revoke.** New `client.revokeAttachment(attachmentId)`. The platform stops issuing download URLs for it and recipients with a live connection get an `ATTACHMENT_REVOKED` event via `onEvent` (metadata = attachmentId). Bytes already downloaded cannot be recalled.
|
|
250
|
+
- **Attachment preview thumbnails.** `prepareAttachmentAndUpload` accepts an optional `thumbnail` (and `thumbnailMimeType`). Entirely optional: if provided, the SDK uploads it as a separate attachment encrypted the same way and links it via `AttachmentRef.thumbnailAttachmentId`; download it like any attachment. If omitted, there is simply no thumbnail.
|
|
198
251
|
|
|
199
252
|
### Notes
|
|
200
253
|
|
|
201
254
|
- Download authorization for GROUP attachments is now checked against *current* group membership: a user removed from a group can no longer download its attachments even if they were a recipient at upload time.
|
|
202
|
-
- No PROTOCOL_VERSION change
|
|
255
|
+
- No PROTOCOL_VERSION change: additive proto fields (`SyncFrame.fromUserId`, `AttachmentRef.thumbnailAttachmentId`) and an additive event type.
|
|
203
256
|
|
|
204
257
|
---
|
|
205
258
|
|
|
@@ -207,14 +260,14 @@ This project follows [Semantic Versioning](https://semver.org/).
|
|
|
207
260
|
|
|
208
261
|
### Added
|
|
209
262
|
|
|
210
|
-
- **Notification clear sync.** `client.clearNotification(conversationId)` tells the user's other devices a conversation's notifications were dismissed; `client.onNotificationCleared(cb)` listens for it. Always available (no opt-out)
|
|
263
|
+
- **Notification clear sync.** `client.clearNotification(conversationId)` tells the user's other devices a conversation's notifications were dismissed; `client.onNotificationCleared(cb)` listens for it. Always available (no opt-out), since it is plain own-device hygiene.
|
|
211
264
|
- **Draft sync.** `client.syncDraft(conversationId, draftText)` pushes a conversation draft to the user's other devices; `client.onDraftSync(cb)` listens for it. **Opt-in:** the app owner enables it in the dashboard, and the draft text crosses the relay in cleartext (fanned out, never stored). When disabled the server drops the frame.
|
|
212
265
|
- New exported types: `NotificationClearEvent` / `NotificationClearCallback`, `DraftSyncEvent` / `DraftSyncCallback`.
|
|
213
266
|
|
|
214
267
|
### Notes
|
|
215
268
|
|
|
216
269
|
- Both ride the existing `SyncFrame` wire type (own-device fan-out only, never delivered to a different user). No PROTOCOL_VERSION change.
|
|
217
|
-
- Your app decides when to call `clearNotification()` / `syncDraft()
|
|
270
|
+
- Your app decides when to call `clearNotification()` / `syncDraft()`. The platform never infers dismissal or tracks drafts.
|
|
218
271
|
|
|
219
272
|
---
|
|
220
273
|
|
|
@@ -222,13 +275,13 @@ This project follows [Semantic Versioning](https://semver.org/).
|
|
|
222
275
|
|
|
223
276
|
### Added
|
|
224
277
|
|
|
225
|
-
- **Cross-device read receipts.** New `client.markRead(messageId, conversationId?)` reports a message as read; the receipt is relayed to the user's *other* devices (never to the message's sender). New `client.onReadReceipt(callback)` listens for receipts reported by the user's other devices
|
|
278
|
+
- **Cross-device read receipts.** New `client.markRead(messageId, conversationId?)` reports a message as read; the receipt is relayed to the user's *other* devices (never to the message's sender). New `client.onReadReceipt(callback)` listens for receipts reported by the user's other devices. Use it to clear unread state your app maintains.
|
|
226
279
|
- New exported `ReadReceiptEvent` type and `ReadReceiptCallback`.
|
|
227
280
|
- **PROTOCOL_VERSION bumped to 6.** Additive only: new `SyncFrame` wire type. Existing 0.10.x clients keep working against the new server.
|
|
228
281
|
|
|
229
282
|
### Notes
|
|
230
283
|
|
|
231
|
-
- The platform is deliberately unopinionated: **your app decides when `markRead()` is called
|
|
284
|
+
- The platform is deliberately unopinionated: **your app decides when `markRead()` is called**. The server never infers read state, and unread counts stay client-side. The app owner can disable read receipts entirely from the dashboard, in which case the server silently drops the frame.
|
|
232
285
|
- This release is own-device sync only: the *sender* of a message is not told when a recipient read it. Sender-side read notifications are a separate future capability.
|
|
233
286
|
|
|
234
287
|
---
|
|
@@ -237,7 +290,7 @@ This project follows [Semantic Versioning](https://semver.org/).
|
|
|
237
290
|
|
|
238
291
|
### Added
|
|
239
292
|
|
|
240
|
-
- **Device trust.** New `client.listMyDevices()` returns the current user's registered devices; `client.revokeMyDevice(deviceId)` revokes one of them. A revoked device's live session is closed immediately and all future connections from it are denied (revoke is permanent
|
|
293
|
+
- **Device trust.** New `client.listMyDevices()` returns the current user's registered devices; `client.revokeMyDevice(deviceId)` revokes one of them. A revoked device's live session is closed immediately and all future connections from it are denied (revoke is permanent, and re-registering yields a fresh deviceId).
|
|
241
294
|
- **`DEVICE_REVOKED` event.** When this device is revoked (from another of the user's devices, or by the app owner), the SDK stops its reconnect loop, closes the socket, and surfaces a `DEVICE_REVOKED` event through `onEvent` so the app can clear local key storage and show a re-register prompt.
|
|
242
295
|
- New exported `DeviceInfo` type.
|
|
243
296
|
|
|
@@ -289,7 +342,7 @@ This project follows [Semantic Versioning](https://semver.org/).
|
|
|
289
342
|
### Notes
|
|
290
343
|
|
|
291
344
|
- Wire protocol unchanged (PROTOCOL_VERSION stays 4). The proto schema was already correct; only the JS hand-rolled codec was out of sync.
|
|
292
|
-
- `protoFieldNumbers` for the group types are now documented
|
|
345
|
+
- `protoFieldNumbers` for the group types are now documented alongside the shared protocol test vectors, for cross-platform reference.
|
|
293
346
|
|
|
294
347
|
---
|
|
295
348
|
|
|
@@ -358,9 +411,8 @@ This project follows [Semantic Versioning](https://semver.org/).
|
|
|
358
411
|
`getDisplayMedia()` (web) / `MediaProjection` (Android) / `ReplayKit` (iOS)
|
|
359
412
|
in the app, add the track to the existing peer connection, then call
|
|
360
413
|
`startScreenShare(...)` to notify the peer.
|
|
361
|
-
-
|
|
362
|
-
|
|
363
|
-
side, on the customer's backend).
|
|
414
|
+
- Availability depends on the app's plan; see the pricing page or the app's
|
|
415
|
+
subscription in the dashboard.
|
|
364
416
|
|
|
365
417
|
---
|
|
366
418
|
|
package/README.md
CHANGED
|
@@ -107,9 +107,54 @@ const client = await initialize(options);
|
|
|
107
107
|
| `syncDraft(conversationId, draftText)` | `void` | Push a conversation draft to the user's other devices (opt-in, cleartext). |
|
|
108
108
|
| `onDraftSync(callback)` | `() => void` | Listen for draft syncs from the user's other devices. |
|
|
109
109
|
|
|
110
|
+
### Key custody
|
|
111
|
+
|
|
112
|
+
Every client reports how it is holding the identity private key:
|
|
113
|
+
|
|
114
|
+
```ts
|
|
115
|
+
const client = await initialize({ /* ... */ });
|
|
116
|
+
console.log(client.keyCustody); // 'software' | 'non-extractable'
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
`'software'` means the key is bytes the process can read. That is what every release before
|
|
120
|
+
0.25.0 did, and what this one still does unless you opt in below.
|
|
121
|
+
|
|
122
|
+
`'non-extractable'` means the key exists only inside the browser's crypto agent: your code can
|
|
123
|
+
ask it to derive a shared secret, and cannot read the key itself. Script that runs on your
|
|
124
|
+
origin can therefore use the identity while the page is open, and cannot copy it and decrypt
|
|
125
|
+
elsewhere or later.
|
|
126
|
+
|
|
127
|
+
Opt in by supplying an identity:
|
|
128
|
+
|
|
129
|
+
```ts
|
|
130
|
+
import { initialize, createSecureIdentity } from '@droponair/sdk-js';
|
|
131
|
+
|
|
132
|
+
const identity = await createSecureIdentity();
|
|
133
|
+
if (identity) {
|
|
134
|
+
// Optional, once: carry an existing key over so the identity and its history survive.
|
|
135
|
+
await identity.migrateFromKeyStorage(myStorage);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
const client = await initialize({
|
|
139
|
+
appId, publicApiKey, getUserJwt,
|
|
140
|
+
identity: identity ?? undefined, // null means this browser cannot provide it
|
|
141
|
+
});
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
`createSecureIdentity()` returns `null` where the platform cannot deliver the guarantee, rather
|
|
145
|
+
than throwing or quietly giving you something weaker. It checks two things: whether the
|
|
146
|
+
primitive exists, and whether a key survives being stored and read back. That second check
|
|
147
|
+
matters, because one major engine generates and derives with the key correctly and then loses
|
|
148
|
+
it on write while reporting success. Read `client.keyCustody` afterwards to see what you
|
|
149
|
+
actually got.
|
|
150
|
+
|
|
151
|
+
Migration is yours to trigger, not automatic, because it removes the readable copy of the key.
|
|
152
|
+
It imports the existing key, verifies it, and only then deletes the original. The public key is
|
|
153
|
+
unchanged, so peers still address the same identity and existing conversations keep decrypting.
|
|
154
|
+
|
|
110
155
|
### Cross-device read receipts
|
|
111
156
|
|
|
112
|
-
Available since SDK `0.11.0`. **Your app decides when a message is read
|
|
157
|
+
Available since SDK `0.11.0`. **Your app decides when a message is read**, and the platform never infers it. Call `markRead()` at that moment; the receipt syncs to the user's *other* devices so they can clear their unread UI. It is not sent to the message's sender. The app owner can switch read receipts off entirely from the dashboard.
|
|
113
158
|
|
|
114
159
|
```typescript
|
|
115
160
|
// When your UI decides the message has been read
|
|
@@ -117,11 +162,11 @@ client.markRead(messageId, peerUserId);
|
|
|
117
162
|
|
|
118
163
|
// On the user's other devices
|
|
119
164
|
client.onReadReceipt(e => {
|
|
120
|
-
// e.messageId was read elsewhere
|
|
165
|
+
// e.messageId was read elsewhere, so clear your unread state for it
|
|
121
166
|
});
|
|
122
167
|
```
|
|
123
168
|
|
|
124
|
-
**Group read receipts.** Since SDK `0.13.0`, a group can opt into sharing read receipts with every member. Call `updateGroup(groupId, { readReceiptsVisibleToGroup: true })` (owner/admin); after that, a member's `markRead()` on a group message is broadcast to every other member, and `onReadReceipt` fires with `e.fromUserId` set to the member who read it. It is **opt-in per group
|
|
169
|
+
**Group read receipts.** Since SDK `0.13.0`, a group can opt into sharing read receipts with every member. Call `updateGroup(groupId, { readReceiptsVisibleToGroup: true })` (owner/admin); after that, a member's `markRead()` on a group message is broadcast to every other member, and `onReadReceipt` fires with `e.fromUserId` set to the member who read it. It is **opt-in per group**. The platform never enables it for you, and it has no effect unless read receipts are also enabled for your app.
|
|
125
170
|
|
|
126
171
|
```typescript
|
|
127
172
|
await client.updateGroup(groupId, { readReceiptsVisibleToGroup: true });
|
|
@@ -133,7 +178,7 @@ client.onReadReceipt(e => {
|
|
|
133
178
|
|
|
134
179
|
### Notification clear & draft sync
|
|
135
180
|
|
|
136
|
-
Available since SDK `0.12.0`, both own-device only. `clearNotification()` tells your user's other devices a conversation's notifications were dismissed
|
|
181
|
+
Available since SDK `0.12.0`, both own-device only. `clearNotification()` tells your user's other devices a conversation's notifications were dismissed, and is always available. `syncDraft()` pushes a draft so the user can keep typing on another device, and is **opt-in** (the app owner enables it in the dashboard) and the draft text crosses the relay in cleartext.
|
|
137
182
|
|
|
138
183
|
```typescript
|
|
139
184
|
client.clearNotification(conversationId);
|
|
@@ -145,7 +190,7 @@ client.onDraftSync(e => { /* pre-fill composer with e.draftText */ });
|
|
|
145
190
|
|
|
146
191
|
### Device trust
|
|
147
192
|
|
|
148
|
-
Available since SDK `0.10.0`. Any device that completes a connection is implicitly trusted. `listMyDevices()` powers a "Your devices" screen; `revokeMyDevice()` cuts a device off immediately. When the *current* device is revoked, the SDK stops reconnecting and emits a `DEVICE_REVOKED` event via `onEvent
|
|
193
|
+
Available since SDK `0.10.0`. Any device that completes a connection is implicitly trusted. `listMyDevices()` powers a "Your devices" screen; `revokeMyDevice()` cuts a device off immediately. When the *current* device is revoked, the SDK stops reconnecting and emits a `DEVICE_REVOKED` event via `onEvent`. Listen for it to clear local key storage and prompt re-registration.
|
|
149
194
|
|
|
150
195
|
```typescript
|
|
151
196
|
const devices = await client.listMyDevices();
|
|
@@ -434,7 +479,7 @@ await wt.connect();
|
|
|
434
479
|
await wt.sendEnvelope(envelopeBytes);
|
|
435
480
|
```
|
|
436
481
|
|
|
437
|
-
Uses the browser-native `WebTransport` API; opens a single bidirectional stream and rides raw bytes both ways. Browser support: Chromium 97+ (stable), Firefox 125+ (stable), Safari Technology Preview only, Node 22+ behind experimental flags. The
|
|
482
|
+
Uses the browser-native `WebTransport` API; opens a single bidirectional stream and rides raw bytes both ways. Browser support: Chromium 97+ (stable), Firefox 125+ (stable), Safari Technology Preview only, Node 22+ behind experimental flags. The endpoint advertises `transports: ["webtransport"]` only where the lane is available; check `GET /api/info.transports` and `features.transport_webtransport` to detect it. Bring your own protobuf decoder. WebSocket remains the default; this primitive is purely opt-in.
|
|
438
483
|
|
|
439
484
|
### Transport auto-select
|
|
440
485
|
|
|
@@ -455,7 +500,7 @@ Foundation for an `init({ transport: 'auto' })` shortcut once full `MessagingCli
|
|
|
455
500
|
|
|
456
501
|
### Call Recording
|
|
457
502
|
|
|
458
|
-
Available since SDK `0.16.0`. The SDK **signals** recording state on a group or room call; your app does the actual media capture (`MediaRecorder`) and uploads the file to your own storage
|
|
503
|
+
Available since SDK `0.16.0`. The SDK **signals** recording state on a group or room call; your app does the actual media capture (`MediaRecorder`) and uploads the file to your own storage. The platform never holds the media. The recording signal is broadcast to every participant (including anyone who joins later); that transparency is enforced server-side.
|
|
459
504
|
|
|
460
505
|
| Method | Description |
|
|
461
506
|
|--------|-------------|
|
|
@@ -44,7 +44,7 @@ export declare class AttachmentClient {
|
|
|
44
44
|
*/
|
|
45
45
|
prepareAttachmentAndUpload(input: Uint8Array, opts: PrepareAttachmentOptions): Promise<AttachmentRef>;
|
|
46
46
|
/**
|
|
47
|
-
* Revoke an attachment you sent
|
|
47
|
+
* Revoke an attachment you sent. After revoke the platform
|
|
48
48
|
* refuses to issue further download URLs and recipients with a live
|
|
49
49
|
* connection get an ATTACHMENT_REVOKED event. Bytes a recipient already
|
|
50
50
|
* downloaded cannot be recalled.
|
|
@@ -147,7 +147,7 @@ class AttachmentClient {
|
|
|
147
147
|
};
|
|
148
148
|
}
|
|
149
149
|
/**
|
|
150
|
-
* Revoke an attachment you sent
|
|
150
|
+
* Revoke an attachment you sent. After revoke the platform
|
|
151
151
|
* refuses to issue further download URLs and recipients with a live
|
|
152
152
|
* connection get an ATTACHMENT_REVOKED event. Bytes a recipient already
|
|
153
153
|
* downloaded cannot be recalled.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { CryptoService } from '../crypto/crypto-service';
|
|
2
2
|
import { SessionManager } from './session-manager';
|
|
3
|
-
import { BroadcastCallback, CallEventCallback, DropOnAirClient, EventCallback, CreateRoomOptions, GroupCallEventCallback, GroupInfo, GroupMessageCallback, Room, UpdateRoomOptions, SfuToken, SfuRecording, InitializeOptions, MessageCallback, MessageDeleteCallback, MessageEditCallback, TurnCredentials, DeviceInfo, ReadReceiptCallback, NotificationClearCallback, DraftSyncCallback } from './types';
|
|
3
|
+
import { BroadcastCallback, CallEventCallback, DropOnAirClient, EventCallback, CreateRoomOptions, GroupCallEventCallback, GroupInfo, GroupMessageCallback, Room, UpdateRoomOptions, SfuToken, SfuRecording, InitializeOptions, MessageCallback, MessageDeleteCallback, MessageEditCallback, TurnCredentials, DeviceInfo, ReadReceiptCallback, NotificationClearCallback, DraftSyncCallback, KeyCustody } from './types';
|
|
4
4
|
import { AttachmentRef, CreateUploadSessionOptions, DownloadedAttachment, PrepareAttachmentOptions, UploadSession } from '../attachment/attachment-types';
|
|
5
5
|
export declare class MessagingClient implements DropOnAirClient {
|
|
6
6
|
private readonly options;
|
|
@@ -39,7 +39,7 @@ export declare class MessagingClient implements DropOnAirClient {
|
|
|
39
39
|
/** Pending startGroupCall resolver. */
|
|
40
40
|
private pendingGroupInviteResolve;
|
|
41
41
|
private pendingGroupInviteReject;
|
|
42
|
-
/** Pending joinRoom resolver
|
|
42
|
+
/** Pending joinRoom resolver, keyed by roomId. */
|
|
43
43
|
private readonly pendingRoomJoins;
|
|
44
44
|
private reconnectDelayMs;
|
|
45
45
|
/** Schedule a single reconnect, replacing any pending one (never stack timers). */
|
|
@@ -100,6 +100,13 @@ export declare class MessagingClient implements DropOnAirClient {
|
|
|
100
100
|
finalizeAttachment(attachmentId: string, sha256: string): Promise<void>;
|
|
101
101
|
downloadAttachment(ref: AttachmentRef): Promise<DownloadedAttachment>;
|
|
102
102
|
revokeAttachment(attachment: string | AttachmentRef): Promise<void>;
|
|
103
|
+
/**
|
|
104
|
+
* How the identity private key is held. See {@link KeyCustody}.
|
|
105
|
+
*
|
|
106
|
+
* Read from the crypto service rather than remembered at construction, so it
|
|
107
|
+
* reflects what is actually in use rather than what was requested.
|
|
108
|
+
*/
|
|
109
|
+
get keyCustody(): KeyCustody;
|
|
103
110
|
connect(): Promise<void>;
|
|
104
111
|
disconnect(): void;
|
|
105
112
|
sendMessage(toUserId: string, plaintextMessage: string, options?: {
|
|
@@ -231,7 +238,7 @@ export declare class MessagingClient implements DropOnAirClient {
|
|
|
231
238
|
getGroup(groupId: string): Promise<GroupInfo>;
|
|
232
239
|
/**
|
|
233
240
|
* Update a group. Any omitted field is left unchanged. `readReceiptsVisibleToGroup`
|
|
234
|
-
*
|
|
241
|
+
* controls whether members' group read receipts are visible to
|
|
235
242
|
* the whole group or stay own-device only.
|
|
236
243
|
*/
|
|
237
244
|
updateGroup(groupId: string, update: {
|
|
@@ -246,7 +246,7 @@ class MessagingClient {
|
|
|
246
246
|
/** Pending startGroupCall resolver. */
|
|
247
247
|
this.pendingGroupInviteResolve = null;
|
|
248
248
|
this.pendingGroupInviteReject = null;
|
|
249
|
-
/** Pending joinRoom resolver
|
|
249
|
+
/** Pending joinRoom resolver, keyed by roomId. */
|
|
250
250
|
this.pendingRoomJoins = new Map();
|
|
251
251
|
this.messageListeners = new Set();
|
|
252
252
|
this.eventListeners = new Set();
|
|
@@ -313,6 +313,15 @@ class MessagingClient {
|
|
|
313
313
|
await this.attachmentClient.revokeAttachment(attachment.thumbnailAttachmentId);
|
|
314
314
|
}
|
|
315
315
|
}
|
|
316
|
+
/**
|
|
317
|
+
* How the identity private key is held. See {@link KeyCustody}.
|
|
318
|
+
*
|
|
319
|
+
* Read from the crypto service rather than remembered at construction, so it
|
|
320
|
+
* reflects what is actually in use rather than what was requested.
|
|
321
|
+
*/
|
|
322
|
+
get keyCustody() {
|
|
323
|
+
return this.cryptoService.keyCustody;
|
|
324
|
+
}
|
|
316
325
|
async connect() {
|
|
317
326
|
this.log('connect_start');
|
|
318
327
|
this.shouldReconnect = true;
|
|
@@ -649,7 +658,7 @@ class MessagingClient {
|
|
|
649
658
|
this.transport.send(this.codec.encodePushRegistrationFrame(frame));
|
|
650
659
|
}
|
|
651
660
|
// ---------------------------------------------------------------------------
|
|
652
|
-
// Device trust (PROTOCOL_VERSION 5+
|
|
661
|
+
// Device trust (PROTOCOL_VERSION 5+)
|
|
653
662
|
// ---------------------------------------------------------------------------
|
|
654
663
|
/**
|
|
655
664
|
* List the current user's registered devices. Each device is implicitly
|
|
@@ -702,7 +711,7 @@ class MessagingClient {
|
|
|
702
711
|
}
|
|
703
712
|
}
|
|
704
713
|
// ---------------------------------------------------------------------------
|
|
705
|
-
// Cross-device read receipts (PROTOCOL_VERSION 6+
|
|
714
|
+
// Cross-device read receipts (PROTOCOL_VERSION 6+)
|
|
706
715
|
// ---------------------------------------------------------------------------
|
|
707
716
|
/**
|
|
708
717
|
* Mark a message as read. Call this when YOUR app decides a message has
|
|
@@ -941,7 +950,7 @@ class MessagingClient {
|
|
|
941
950
|
}
|
|
942
951
|
/**
|
|
943
952
|
* Update a group. Any omitted field is left unchanged. `readReceiptsVisibleToGroup`
|
|
944
|
-
*
|
|
953
|
+
* controls whether members' group read receipts are visible to
|
|
945
954
|
* the whole group or stay own-device only.
|
|
946
955
|
*/
|
|
947
956
|
async updateGroup(groupId, update) {
|
|
@@ -986,7 +995,7 @@ class MessagingClient {
|
|
|
986
995
|
throw new Error(`deleteGroup failed (HTTP ${res.status})`);
|
|
987
996
|
}
|
|
988
997
|
// ---------------------------------------------------------------------------
|
|
989
|
-
// Rooms (
|
|
998
|
+
// Rooms (scheduled & persistent meeting rooms)
|
|
990
999
|
// ---------------------------------------------------------------------------
|
|
991
1000
|
async createRoom(options) {
|
|
992
1001
|
const jwt = await this.getValidDropOnAirJwt(false);
|
|
@@ -1316,7 +1325,7 @@ class MessagingClient {
|
|
|
1316
1325
|
return () => this.groupCallListeners.delete(callback);
|
|
1317
1326
|
}
|
|
1318
1327
|
// Group call moderation + waiting room. The server validates host/co-host
|
|
1319
|
-
// authority and
|
|
1328
|
+
// authority and whether moderation is available on the app's plan; on denial it
|
|
1320
1329
|
// emits a LIMIT_REACHED event with reason="MODERATION_NOT_ENABLED".
|
|
1321
1330
|
transferHost(callId, groupId, newHostUserId) {
|
|
1322
1331
|
this.sendGroupCallFrame({
|
|
@@ -1390,7 +1399,7 @@ class MessagingClient {
|
|
|
1390
1399
|
payload: JSON.stringify({ userId }),
|
|
1391
1400
|
});
|
|
1392
1401
|
}
|
|
1393
|
-
// Live stage controls
|
|
1402
|
+
// Live stage controls. These act on a room call running in
|
|
1394
1403
|
// stage mode; pass the callId from joinRoom(). raiseHand / lowerHand and
|
|
1395
1404
|
// submitStageQuestion are open to any participant; promote / demote are
|
|
1396
1405
|
// host / co-host authority. Roles surface via GROUP_CALL_ROLE_CHANGED.
|
|
@@ -1419,7 +1428,7 @@ class MessagingClient {
|
|
|
1419
1428
|
submitStageQuestion(callId, text) {
|
|
1420
1429
|
this.sendGroupCallFrame({ type: 'GROUP_CALL_STAGE_QUESTION', callId, groupId: '', payload: text });
|
|
1421
1430
|
}
|
|
1422
|
-
// Call recording
|
|
1431
|
+
// Call recording. These SIGNAL recording state; the actual
|
|
1423
1432
|
// media capture + upload to your storage is your app's job (same split as
|
|
1424
1433
|
// screen sharing). The platform broadcasts the signal to every participant.
|
|
1425
1434
|
startRecording(callId) {
|
|
@@ -1580,7 +1589,7 @@ class MessagingClient {
|
|
|
1580
1589
|
plaintext = await this.cryptoService.decrypt(myPayload.encryptedPayload, sharedKey, {
|
|
1581
1590
|
messageId: notif.messageId,
|
|
1582
1591
|
senderId: notif.fromUserId,
|
|
1583
|
-
// AAD recipientId is the receiving user's own id
|
|
1592
|
+
// AAD recipientId is the receiving user's own id, and this MUST match the
|
|
1584
1593
|
// sender's encrypt AAD, which binds recipientId to the target member's
|
|
1585
1594
|
// userId (memberId). Using notif.groupId here (the previous value) never
|
|
1586
1595
|
// matched, so E2EE group messages failed GCM auth on decrypt. android /
|
|
@@ -1625,7 +1634,7 @@ class MessagingClient {
|
|
|
1625
1634
|
this.pendingGroupInviteReject = null;
|
|
1626
1635
|
reject(new Error(`GROUP_CALL_ALREADY_ACTIVE:${wire.payload ?? ''}`));
|
|
1627
1636
|
}
|
|
1628
|
-
// Resolve / reject a pending joinRoom()
|
|
1637
|
+
// Resolve / reject a pending joinRoom(). Room-call frames
|
|
1629
1638
|
// carry roomId; the join outcome is one of these four types.
|
|
1630
1639
|
if (wire.roomId) {
|
|
1631
1640
|
const pending = this.pendingRoomJoins.get(wire.roomId);
|
package/dist/core/types.d.ts
CHANGED
|
@@ -117,7 +117,7 @@ export interface GroupInfo {
|
|
|
117
117
|
members: GroupMemberInfo[];
|
|
118
118
|
createdAt: number;
|
|
119
119
|
/**
|
|
120
|
-
*
|
|
120
|
+
* When true, a member's group read receipt is fanned to all other
|
|
121
121
|
* members; when false it stays own-device only. Controlled via updateGroup().
|
|
122
122
|
*/
|
|
123
123
|
readReceiptsVisibleToGroup?: boolean;
|
|
@@ -140,7 +140,7 @@ export interface GroupCallEvent {
|
|
|
140
140
|
type: GroupCallEventType | string;
|
|
141
141
|
callId: string;
|
|
142
142
|
groupId: string;
|
|
143
|
-
/** Set instead of groupId when the event belongs to a room call
|
|
143
|
+
/** Set instead of groupId when the event belongs to a room call. */
|
|
144
144
|
roomId?: string;
|
|
145
145
|
targetUserId?: string;
|
|
146
146
|
payload?: string;
|
|
@@ -160,7 +160,7 @@ export interface RoomPolicy {
|
|
|
160
160
|
/** When true, the room flips to CLOSED once the last participant leaves the call. */
|
|
161
161
|
autoCloseWhenEmpty?: boolean;
|
|
162
162
|
/**
|
|
163
|
-
* When true, the room's live call runs in stage mode
|
|
163
|
+
* When true, the room's live call runs in stage mode: hosts
|
|
164
164
|
* join as speakers, everyone else as receive-only audience who can raise a
|
|
165
165
|
* hand to be promoted. Mesh-scale - suited to panels and small stages.
|
|
166
166
|
*/
|
|
@@ -191,7 +191,7 @@ export interface SfuToken {
|
|
|
191
191
|
expiresAt: number;
|
|
192
192
|
}
|
|
193
193
|
/**
|
|
194
|
-
* A server-side recording of an SFU-mode room
|
|
194
|
+
* A server-side recording of an SFU-mode room. The platform
|
|
195
195
|
* never holds the recorded bytes - LiveKit Egress uploads the finalized file
|
|
196
196
|
* directly to the destination configured in the panel under `destinationId`.
|
|
197
197
|
*/
|
|
@@ -251,6 +251,57 @@ export interface KeyStorageAdapter {
|
|
|
251
251
|
set(key: string, value: string): Promise<void>;
|
|
252
252
|
remove(key: string): Promise<void>;
|
|
253
253
|
}
|
|
254
|
+
/**
|
|
255
|
+
* How the identity private key is held.
|
|
256
|
+
*
|
|
257
|
+
* - `non-extractable`: the key lives in the agent as a CryptoKey that script
|
|
258
|
+
* cannot export. An attacker running script on the origin can still use it
|
|
259
|
+
* while the page is open, which is unavoidable, but cannot copy it out and use
|
|
260
|
+
* it later or elsewhere.
|
|
261
|
+
* - `software`: the key exists as bytes reachable from script, so anything that
|
|
262
|
+
* can run script on the origin can read it once and decrypt that user's
|
|
263
|
+
* conversations from anywhere, indefinitely.
|
|
264
|
+
*
|
|
265
|
+
* Exposed so an application can tell its users the truth rather than assume.
|
|
266
|
+
*/
|
|
267
|
+
export type KeyCustody = 'non-extractable' | 'software';
|
|
268
|
+
/**
|
|
269
|
+
* Owns the identity keypair and the key agreement that uses it.
|
|
270
|
+
*
|
|
271
|
+
* This exists because a storage interface alone cannot make key material
|
|
272
|
+
* XSS-resistant. The strongest browsers offer is a non-extractable CryptoKey,
|
|
273
|
+
* and by definition its bytes cannot be handed back to a caller, so any design
|
|
274
|
+
* shaped as "give me the private key and I will do the maths" forces the key to
|
|
275
|
+
* be extractable. The agreement has to happen behind the same boundary that
|
|
276
|
+
* holds the key, which is what this interface is.
|
|
277
|
+
*
|
|
278
|
+
* Implement it to hold keys somewhere the SDK cannot reach, for example an OS
|
|
279
|
+
* keychain or a hardware token. The SDK ships two implementations and picks the
|
|
280
|
+
* conservative one unless asked otherwise; it never silently upgrades you.
|
|
281
|
+
*/
|
|
282
|
+
export interface SecureIdentityProvider {
|
|
283
|
+
/** Base64 X25519 public key for this device, created on first use. */
|
|
284
|
+
getPublicKey(): Promise<string>;
|
|
285
|
+
/**
|
|
286
|
+
* Perform X25519 with the peer's public key, then HKDF-SHA256 to an AES-GCM
|
|
287
|
+
* key, and return it.
|
|
288
|
+
*
|
|
289
|
+
* Returns the derived key rather than the shared secret so the secret need
|
|
290
|
+
* never exist outside the implementation. The returned key must be
|
|
291
|
+
* non-extractable: it is used for encrypt and decrypt and nothing else.
|
|
292
|
+
*
|
|
293
|
+
* `salt` and `info` are supplied by the SDK and are part of the wire format.
|
|
294
|
+
* An implementation that changes them will produce keys that no other
|
|
295
|
+
* participant can reproduce.
|
|
296
|
+
*/
|
|
297
|
+
deriveMessageKey(params: {
|
|
298
|
+
peerPublicKey: string;
|
|
299
|
+
salt: Uint8Array;
|
|
300
|
+
info: Uint8Array;
|
|
301
|
+
}): Promise<CryptoKey>;
|
|
302
|
+
/** Whether the private key can be read by script. See {@link KeyCustody}. */
|
|
303
|
+
readonly keyCustody: KeyCustody;
|
|
304
|
+
}
|
|
254
305
|
export interface InitializeOptions {
|
|
255
306
|
appId: string;
|
|
256
307
|
publicApiKey: string;
|
|
@@ -262,6 +313,22 @@ export interface InitializeOptions {
|
|
|
262
313
|
keyDirectoryEndpoint?: string;
|
|
263
314
|
fetchFn?: typeof fetch;
|
|
264
315
|
storage?: KeyStorageAdapter;
|
|
316
|
+
/**
|
|
317
|
+
* Owns the identity keypair instead of {@link storage}, when the consumer wants
|
|
318
|
+
* a private key that script cannot read.
|
|
319
|
+
*
|
|
320
|
+
* Optional and off by default, deliberately. Passing nothing keeps exactly the
|
|
321
|
+
* behaviour every existing release has: the identity is a raw key held through
|
|
322
|
+
* {@link storage}. Switching that silently would change key custody under
|
|
323
|
+
* applications that never asked for it, and on engines where the stronger path
|
|
324
|
+
* cannot persist it would do so without delivering anything.
|
|
325
|
+
*
|
|
326
|
+
* Build one with `createSecureIdentity()`, which returns `null` where the
|
|
327
|
+
* platform cannot support it so the choice is explicit rather than accidental.
|
|
328
|
+
* Read {@link DropOnAirClient.keyCustody} afterwards to see what was actually
|
|
329
|
+
* obtained.
|
|
330
|
+
*/
|
|
331
|
+
identity?: SecureIdentityProvider;
|
|
265
332
|
/** When true, verbose diagnostic logs are emitted to console for all SDK operations. */
|
|
266
333
|
debug?: boolean;
|
|
267
334
|
/**
|
|
@@ -299,6 +366,20 @@ export interface DeviceInfo {
|
|
|
299
366
|
revokedBy?: 'END_USER' | 'APP_OWNER' | 'SYSTEM';
|
|
300
367
|
}
|
|
301
368
|
export interface DropOnAirClient {
|
|
369
|
+
/**
|
|
370
|
+
* How this client is holding the identity private key.
|
|
371
|
+
*
|
|
372
|
+
* `'non-extractable'` means the key exists only inside the platform's crypto
|
|
373
|
+
* agent: script may ask it to derive, and cannot read it, so storage taken from
|
|
374
|
+
* the device yields nothing usable. `'software'` means the key is bytes the
|
|
375
|
+
* process can read, which is what every release before this one did and what
|
|
376
|
+
* this one still does unless {@link InitializeOptions.identity} is supplied.
|
|
377
|
+
*
|
|
378
|
+
* Exposed because a consumer that believes it has the stronger custody and does
|
|
379
|
+
* not is worse off than one that knows. Log it, show it, or gate a feature on
|
|
380
|
+
* it, but do not assume it.
|
|
381
|
+
*/
|
|
382
|
+
readonly keyCustody: KeyCustody;
|
|
302
383
|
connect(): Promise<void>;
|
|
303
384
|
disconnect(): void;
|
|
304
385
|
/**
|
|
@@ -1,9 +1,16 @@
|
|
|
1
1
|
import { SessionManager } from '../core/session-manager';
|
|
2
|
-
import { KeyStorageAdapter } from '../core/types';
|
|
2
|
+
import { KeyCustody, KeyStorageAdapter, SecureIdentityProvider } from '../core/types';
|
|
3
3
|
export declare class CryptoService {
|
|
4
|
-
private readonly storage;
|
|
5
4
|
private readonly sessionManager;
|
|
6
|
-
|
|
5
|
+
private readonly identity;
|
|
6
|
+
/**
|
|
7
|
+
* Accepts either an identity provider or, for compatibility, the storage
|
|
8
|
+
* adapter it used to take. Passing storage keeps the previous behaviour
|
|
9
|
+
* exactly, by wrapping it in the tweetnacl provider.
|
|
10
|
+
*/
|
|
11
|
+
constructor(storageOrIdentity: KeyStorageAdapter | SecureIdentityProvider, sessionManager: SessionManager);
|
|
12
|
+
/** How the identity private key is held. See {@link KeyCustody}. */
|
|
13
|
+
get keyCustody(): KeyCustody;
|
|
7
14
|
generateIdentity(): Promise<{
|
|
8
15
|
publicKey: string;
|
|
9
16
|
}>;
|
|
@@ -23,6 +30,5 @@ export declare class CryptoService {
|
|
|
23
30
|
recipientId: string;
|
|
24
31
|
timestamp: number;
|
|
25
32
|
}): Promise<string>;
|
|
26
|
-
private deriveAesKey;
|
|
27
33
|
private buildAad;
|
|
28
34
|
}
|