@droponair/sdk-js 0.32.1 → 0.34.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/CHANGELOG.md +28 -0
- package/README.md +20 -0
- package/dist/attachment/attachment-client.js +4 -0
- package/dist/attachment/attachment-types.d.ts +11 -0
- package/dist/core/messaging-client.d.ts +38 -1
- package/dist/core/messaging-client.js +286 -1
- package/dist/core/types.d.ts +57 -0
- package/dist/index.d.ts +1 -1
- package/dist/transport/protobuf-codec.d.ts +6 -0
- package/dist/transport/protobuf-codec.js +13 -2
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,33 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.34.0
|
|
4
|
+
|
|
5
|
+
### Added
|
|
6
|
+
|
|
7
|
+
- **Files in group messages.** `sendGroupMessage(groupId, text, memberUserIds, { attachments })`
|
|
8
|
+
now carries attachment references, and a received `DecryptedGroupMessage` carries
|
|
9
|
+
`attachments`. Each member is handed the pointer and only the file keys wrapped for
|
|
10
|
+
their own devices. A file sent while a member was away arrives with the message when
|
|
11
|
+
they catch up, which previously carried nothing.
|
|
12
|
+
- **A lifetime per file.** `retentionSeconds` on an upload session asks for one file to
|
|
13
|
+
live a set time. Never longer than the app's own retention.
|
|
14
|
+
- **`deleteWhenEveryoneHasIt`.** The platform deletes the stored bytes as soon as every
|
|
15
|
+
recipient has fetched them, and answers a later request with "no longer available"
|
|
16
|
+
rather than a broken link.
|
|
17
|
+
|
|
18
|
+
## 0.33.0
|
|
19
|
+
|
|
20
|
+
### Added
|
|
21
|
+
|
|
22
|
+
- **Edit and delete group messages.** `editGroupMessage(groupId, originalMessageId,
|
|
23
|
+
newText, memberUserIds)`, `editCleartextGroupMessage` and `deleteGroupMessage`
|
|
24
|
+
(scope `FOR_EVERYONE` by default, or `FOR_ME`). An E2EE edit is encrypted for each
|
|
25
|
+
member and device, as a group message is. Only the sender of a message can change it.
|
|
26
|
+
- **`onGroupMessageEdit` and `onGroupMessageDelete`.** Fired live, and again from
|
|
27
|
+
the offline fetch for a member who was away, after that member's messages, so
|
|
28
|
+
applying one twice must be harmless. A member who fetches after a delete for
|
|
29
|
+
everyone is not handed the message.
|
|
30
|
+
|
|
3
31
|
## 0.32.1
|
|
4
32
|
|
|
5
33
|
### Fixed
|
package/README.md
CHANGED
|
@@ -93,6 +93,11 @@ const client = await initialize(options);
|
|
|
93
93
|
| `editCleartextMessage(originalMessageId, toUserId, newText)` | `Promise<{ editId }>` | Edit a previously sent cleartext message |
|
|
94
94
|
| `onMessage(callback)` | `() => void` | Listen for incoming messages. Returns unsubscribe function |
|
|
95
95
|
| `onMessageEdit(callback)` | `() => void` | Listen for inbound edits to 1:1 messages |
|
|
96
|
+
| `editGroupMessage(groupId, originalMessageId, newText, memberUserIds)` | `Promise<{ editId }>` | Edit a message you sent to a group, encrypted per member |
|
|
97
|
+
| `editCleartextGroupMessage(groupId, originalMessageId, newText)` | `Promise<{ editId }>` | Edit a cleartext message you sent to a group |
|
|
98
|
+
| `deleteGroupMessage(groupId, originalMessageId, scope?)` | `Promise<{ deleteId }>` | Delete a message you sent to a group. `scope` defaults to `FOR_EVERYONE` |
|
|
99
|
+
| `onGroupMessageEdit(callback)` | `() => void` | Listen for edits to group messages |
|
|
100
|
+
| `onGroupMessageDelete(callback)` | `() => void` | Listen for deletes of group messages |
|
|
96
101
|
| `onMessageDelete(callback)` | `() => void` | Listen for inbound delete tombstones for 1:1 messages |
|
|
97
102
|
| `onEvent(callback)` | `() => void` | Listen for system events (CONNECTED, DELIVERED, ERROR, etc.) |
|
|
98
103
|
| `ack(messageId)` | `Promise<void>` | Manually acknowledge a message |
|
|
@@ -286,6 +291,19 @@ await client.deleteMessage(originalMessageId, 'recipient-user-id', 'FOR_EVERYONE
|
|
|
286
291
|
// Delete only on the sender's own devices
|
|
287
292
|
await client.deleteMessage(originalMessageId, 'recipient-user-id', 'FOR_ME');
|
|
288
293
|
|
|
294
|
+
// A group message: name the group, and pass the members you sent to
|
|
295
|
+
await client.editGroupMessage(groupId, messageId, 'Updated text', memberUserIds);
|
|
296
|
+
await client.deleteGroupMessage(groupId, messageId); // FOR_EVERYONE by default
|
|
297
|
+
|
|
298
|
+
client.onGroupMessageEdit((edit) => {
|
|
299
|
+
console.log('group edit', edit.groupId, edit.originalMessageId, edit.plaintext);
|
|
300
|
+
});
|
|
301
|
+
client.onGroupMessageDelete((del) => {
|
|
302
|
+
console.log('group delete', del.groupId, del.originalMessageId);
|
|
303
|
+
});
|
|
304
|
+
// Group edits and deletes also arrive after an absence, so applying one twice
|
|
305
|
+
// must be harmless, and a delete can name a message this device never received.
|
|
306
|
+
|
|
289
307
|
client.onMessageEdit((edit) => {
|
|
290
308
|
console.log('edited', edit.originalMessageId, edit.text);
|
|
291
309
|
});
|
|
@@ -328,6 +346,8 @@ client.onMessage(async (msg) => {
|
|
|
328
346
|
| `prepareAttachmentAndUpload(bytes, options)` | `Promise<AttachmentRef>` | Encrypt (E2EE), upload, finalize, return ref |
|
|
329
347
|
| `createUploadSession(options)` | `Promise<UploadSession>` | Low-level: presigned PUT URL only |
|
|
330
348
|
| `finalizeAttachment(attachmentId, sha256)` | `Promise<void>` | Low-level: commit integrity hash |
|
|
349
|
+
| `retentionSeconds` (upload option) | | How long this one file should live. Never granted longer than your own retention. |
|
|
350
|
+
| `deleteWhenEveryoneHasIt` (upload option) | | Delete the stored bytes as soon as every recipient has fetched them. A later request is answered "no longer available". |
|
|
331
351
|
| `downloadAttachment(ref)` | `Promise<DownloadedAttachment>` | Get presigned URL + download + decrypt |
|
|
332
352
|
| `revokeAttachment(attachmentId \| ref)` | `Promise<void>` | Revoke an attachment you sent; recipients get `ATTACHMENT_REVOKED` |
|
|
333
353
|
| `sendMessage(toUserId, text, { attachments })` | `Promise<{ messageId }>` | Send with attachments |
|
|
@@ -46,6 +46,10 @@ class AttachmentClient {
|
|
|
46
46
|
conversationId: opts.conversationId ?? '',
|
|
47
47
|
recipientUserIds: opts.recipientUserIds ?? [],
|
|
48
48
|
};
|
|
49
|
+
if (opts.retentionSeconds !== undefined)
|
|
50
|
+
body.retentionSeconds = opts.retentionSeconds;
|
|
51
|
+
if (opts.deleteWhenEveryoneHasIt)
|
|
52
|
+
body.deleteWhenEveryoneHasIt = true;
|
|
49
53
|
const resp = await this.deps.fetchFn(`${this.deps.httpUrl}/v1/attachments/upload-session`, {
|
|
50
54
|
method: 'POST',
|
|
51
55
|
headers: { Authorization: `Bearer ${jwt}`, 'Content-Type': 'application/json' },
|
|
@@ -44,6 +44,17 @@ export interface CreateUploadSessionOptions {
|
|
|
44
44
|
conversationId?: string;
|
|
45
45
|
/** Recipient user IDs (used by the server for download authorization). */
|
|
46
46
|
recipientUserIds?: string[];
|
|
47
|
+
/**
|
|
48
|
+
* How long this one file should live, in seconds. Optional, and never granted
|
|
49
|
+
* longer than the app's own retention. An app that works out a window per file
|
|
50
|
+
* sets it here instead of living with one number for everything.
|
|
51
|
+
*/
|
|
52
|
+
retentionSeconds?: number;
|
|
53
|
+
/**
|
|
54
|
+
* Delete the stored bytes as soon as every recipient has fetched them. The
|
|
55
|
+
* file then exists only on the devices it was sent to.
|
|
56
|
+
*/
|
|
57
|
+
deleteWhenEveryoneHasIt?: boolean;
|
|
47
58
|
}
|
|
48
59
|
/** Result of reserving an upload session. */
|
|
49
60
|
export interface UploadSession {
|
|
@@ -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, TypingCallback, NotificationClearCallback, DraftSyncCallback, KeyCustody, PushPayload } from './types';
|
|
3
|
+
import { BroadcastCallback, CallEventCallback, DropOnAirClient, EventCallback, CreateRoomOptions, GroupCallEventCallback, GroupInfo, GroupMessageCallback, GroupMessageDeleteCallback, GroupMessageEditCallback, Room, UpdateRoomOptions, SfuToken, SfuRecording, InitializeOptions, MessageCallback, MessageDeleteCallback, MessageEditCallback, TurnCredentials, DeviceInfo, ReadReceiptCallback, TypingCallback, NotificationClearCallback, DraftSyncCallback, KeyCustody, PushPayload } 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;
|
|
@@ -94,6 +94,8 @@ export declare class MessagingClient implements DropOnAirClient {
|
|
|
94
94
|
private readonly notificationClearListeners;
|
|
95
95
|
private readonly draftSyncListeners;
|
|
96
96
|
private readonly messageDeleteListeners;
|
|
97
|
+
private readonly groupMessageEditListeners;
|
|
98
|
+
private readonly groupMessageDeleteListeners;
|
|
97
99
|
/** ------------------------------------------------------------------
|
|
98
100
|
* Lightweight structured logger. Only active when options.debug === true.
|
|
99
101
|
* Fields are safe to log: no plaintext, no private keys, no full JWTs.
|
|
@@ -142,6 +144,16 @@ export declare class MessagingClient implements DropOnAirClient {
|
|
|
142
144
|
onEvent(callback: EventCallback): () => void;
|
|
143
145
|
onMessageEdit(callback: MessageEditCallback): () => void;
|
|
144
146
|
onMessageDelete(callback: MessageDeleteCallback): () => void;
|
|
147
|
+
/**
|
|
148
|
+
* Listen for edits of group messages. An edit arrives live and again from the
|
|
149
|
+
* offline fetch, so applying one twice must be harmless.
|
|
150
|
+
*/
|
|
151
|
+
onGroupMessageEdit(callback: GroupMessageEditCallback): () => void;
|
|
152
|
+
/**
|
|
153
|
+
* Listen for deletes of group messages. A delete arrives live and again from
|
|
154
|
+
* the offline fetch, and can name a message this device never received.
|
|
155
|
+
*/
|
|
156
|
+
onGroupMessageDelete(callback: GroupMessageDeleteCallback): () => void;
|
|
145
157
|
/**
|
|
146
158
|
* Register a listener for read receipts that this user's OTHER devices
|
|
147
159
|
* reported. Fires when another device of the same user calls markRead();
|
|
@@ -162,6 +174,26 @@ export declare class MessagingClient implements DropOnAirClient {
|
|
|
162
174
|
deleteMessage(originalMessageId: string, toUserId: string, scope: 'FOR_EVERYONE' | 'FOR_ME'): Promise<{
|
|
163
175
|
deleteId: string;
|
|
164
176
|
}>;
|
|
177
|
+
/**
|
|
178
|
+
* Edit a message you sent to a group. Encrypted per member and per device with
|
|
179
|
+
* the edit id, exactly as {@link sendGroupMessage} encrypts with the message id,
|
|
180
|
+
* so pass the same members. Only the sender of a message can edit it.
|
|
181
|
+
*/
|
|
182
|
+
editGroupMessage(groupId: string, originalMessageId: string, newPlaintext: string, memberUserIds: string[]): Promise<{
|
|
183
|
+
editId: string;
|
|
184
|
+
}>;
|
|
185
|
+
/** Edit a cleartext message you sent to a group. */
|
|
186
|
+
editCleartextGroupMessage(groupId: string, originalMessageId: string, newPlaintext: string): Promise<{
|
|
187
|
+
editId: string;
|
|
188
|
+
}>;
|
|
189
|
+
/**
|
|
190
|
+
* Delete a message you sent to a group. FOR_EVERYONE tells every member, and a
|
|
191
|
+
* member who fetches later is not handed the message. FOR_ME tells only your
|
|
192
|
+
* other devices. Only the sender of a message can delete it.
|
|
193
|
+
*/
|
|
194
|
+
deleteGroupMessage(groupId: string, originalMessageId: string, scope?: 'FOR_EVERYONE' | 'FOR_ME'): Promise<{
|
|
195
|
+
deleteId: string;
|
|
196
|
+
}>;
|
|
165
197
|
/**
|
|
166
198
|
* Register this device's push notification token with the DropOnAir platform.
|
|
167
199
|
* The token is opaque to the server; the platform fans out push notifications
|
|
@@ -378,6 +410,11 @@ export declare class MessagingClient implements DropOnAirClient {
|
|
|
378
410
|
private emitGroupCallEvent;
|
|
379
411
|
private resolveTransport;
|
|
380
412
|
private connectWebSocket;
|
|
413
|
+
/**
|
|
414
|
+
* One group edit, live or from the offline fetch. Decrypted exactly as a group
|
|
415
|
+
* message is, with the edit id in place of the message id.
|
|
416
|
+
*/
|
|
417
|
+
private handleIncomingGroupEdit;
|
|
381
418
|
private handleIncomingMessageEdit;
|
|
382
419
|
private handleIncomingMessageDelete;
|
|
383
420
|
private handleIncomingEnvelope;
|
|
@@ -292,6 +292,8 @@ class MessagingClient {
|
|
|
292
292
|
this.notificationClearListeners = new Set();
|
|
293
293
|
this.draftSyncListeners = new Set();
|
|
294
294
|
this.messageDeleteListeners = new Set();
|
|
295
|
+
this.groupMessageEditListeners = new Set();
|
|
296
|
+
this.groupMessageDeleteListeners = new Set();
|
|
295
297
|
this.wsUrl = options.messagingWsUrl ?? 'wss://sdk.droponair.com/ws';
|
|
296
298
|
this.httpUrl = options.messagingHttpUrl ?? 'https://sdk.droponair.com';
|
|
297
299
|
this.tokenExchangeEndpoint = options.tokenExchangeEndpoint ?? '/api/messaging/token-exchange';
|
|
@@ -521,6 +523,22 @@ class MessagingClient {
|
|
|
521
523
|
this.messageDeleteListeners.add(callback);
|
|
522
524
|
return () => this.messageDeleteListeners.delete(callback);
|
|
523
525
|
}
|
|
526
|
+
/**
|
|
527
|
+
* Listen for edits of group messages. An edit arrives live and again from the
|
|
528
|
+
* offline fetch, so applying one twice must be harmless.
|
|
529
|
+
*/
|
|
530
|
+
onGroupMessageEdit(callback) {
|
|
531
|
+
this.groupMessageEditListeners.add(callback);
|
|
532
|
+
return () => this.groupMessageEditListeners.delete(callback);
|
|
533
|
+
}
|
|
534
|
+
/**
|
|
535
|
+
* Listen for deletes of group messages. A delete arrives live and again from
|
|
536
|
+
* the offline fetch, and can name a message this device never received.
|
|
537
|
+
*/
|
|
538
|
+
onGroupMessageDelete(callback) {
|
|
539
|
+
this.groupMessageDeleteListeners.add(callback);
|
|
540
|
+
return () => this.groupMessageDeleteListeners.delete(callback);
|
|
541
|
+
}
|
|
524
542
|
/**
|
|
525
543
|
* Register a listener for read receipts that this user's OTHER devices
|
|
526
544
|
* reported. Fires when another device of the same user calls markRead();
|
|
@@ -653,6 +671,139 @@ class MessagingClient {
|
|
|
653
671
|
this.transport.send(this.codec.encodeMessageDeleteFrame(frame));
|
|
654
672
|
return { deleteId };
|
|
655
673
|
}
|
|
674
|
+
/**
|
|
675
|
+
* Edit a message you sent to a group. Encrypted per member and per device with
|
|
676
|
+
* the edit id, exactly as {@link sendGroupMessage} encrypts with the message id,
|
|
677
|
+
* so pass the same members. Only the sender of a message can edit it.
|
|
678
|
+
*/
|
|
679
|
+
async editGroupMessage(groupId, originalMessageId, newPlaintext, memberUserIds) {
|
|
680
|
+
if (!this.transport?.isOpen()) {
|
|
681
|
+
throw new Error('DropOnAir websocket is not connected');
|
|
682
|
+
}
|
|
683
|
+
if (!this.currentUserId) {
|
|
684
|
+
throw new Error('Missing sender identity from DropOnAir JWT subject');
|
|
685
|
+
}
|
|
686
|
+
if (this.rateLimited) {
|
|
687
|
+
throw new Error('Sending is temporarily blocked by LIMIT_REACHED');
|
|
688
|
+
}
|
|
689
|
+
if (this.dropOnAirJwt && (0, bytes_1.isJwtExpired)(this.dropOnAirJwt)) {
|
|
690
|
+
throw new Error('JWT_EXPIRED: Secure messaging session is reconnecting; please retry in a moment');
|
|
691
|
+
}
|
|
692
|
+
if (!memberUserIds || memberUserIds.length === 0) {
|
|
693
|
+
throw new Error('editGroupMessage requires at least one recipient userId in memberUserIds');
|
|
694
|
+
}
|
|
695
|
+
const myDeviceId = this.deviceId ?? await this.getOrCreateDeviceId();
|
|
696
|
+
const editId = crypto.randomUUID();
|
|
697
|
+
const timestamp = Date.now();
|
|
698
|
+
const myIdentity = await this.cryptoService.getOrCreateIdentity();
|
|
699
|
+
const myPublicKeyBytes = (0, bytes_1.fromBase64)(myIdentity.publicKey);
|
|
700
|
+
const memberPayloads = [];
|
|
701
|
+
for (const memberId of memberUserIds) {
|
|
702
|
+
if (memberId === this.currentUserId)
|
|
703
|
+
continue; // skip self, as sendGroupMessage does
|
|
704
|
+
const deviceKeys = await this.fetchDeviceKeys(memberId);
|
|
705
|
+
const devicePayloads = [];
|
|
706
|
+
if (deviceKeys.length > 0) {
|
|
707
|
+
for (const dk of deviceKeys) {
|
|
708
|
+
const sharedKey = await this.cryptoService.deriveSharedSecret(memberId, dk.publicKey, this.currentUserId);
|
|
709
|
+
const encrypted = await this.cryptoService.encrypt(newPlaintext, sharedKey, {
|
|
710
|
+
messageId: editId,
|
|
711
|
+
senderId: this.currentUserId,
|
|
712
|
+
recipientId: memberId,
|
|
713
|
+
timestamp,
|
|
714
|
+
});
|
|
715
|
+
devicePayloads.push({ deviceId: dk.deviceId, encryptedPayload: encrypted, senderPublicKey: myPublicKeyBytes });
|
|
716
|
+
}
|
|
717
|
+
}
|
|
718
|
+
else {
|
|
719
|
+
const sharedKey = await this.getPeerSharedKey(memberId);
|
|
720
|
+
const encrypted = await this.cryptoService.encrypt(newPlaintext, sharedKey, {
|
|
721
|
+
messageId: editId,
|
|
722
|
+
senderId: this.currentUserId,
|
|
723
|
+
recipientId: memberId,
|
|
724
|
+
timestamp,
|
|
725
|
+
});
|
|
726
|
+
devicePayloads.push({ deviceId: 'default', encryptedPayload: encrypted, senderPublicKey: myPublicKeyBytes });
|
|
727
|
+
}
|
|
728
|
+
memberPayloads.push({ userId: memberId, devicePayloads });
|
|
729
|
+
}
|
|
730
|
+
const frame = {
|
|
731
|
+
type: 'MESSAGE_EDIT',
|
|
732
|
+
editId,
|
|
733
|
+
originalMessageId,
|
|
734
|
+
appId: this.options.appId,
|
|
735
|
+
fromUserId: this.currentUserId,
|
|
736
|
+
toUserId: '',
|
|
737
|
+
groupId,
|
|
738
|
+
timestamp,
|
|
739
|
+
encryptionType: 0, // E2EE
|
|
740
|
+
memberPayloads,
|
|
741
|
+
senderDeviceId: myDeviceId,
|
|
742
|
+
clientEditId: editId,
|
|
743
|
+
};
|
|
744
|
+
this.transport.send(this.codec.encodeMessageEditFrame(frame));
|
|
745
|
+
return { editId };
|
|
746
|
+
}
|
|
747
|
+
/** Edit a cleartext message you sent to a group. */
|
|
748
|
+
async editCleartextGroupMessage(groupId, originalMessageId, newPlaintext) {
|
|
749
|
+
if (!this.transport?.isOpen()) {
|
|
750
|
+
throw new Error('DropOnAir websocket is not connected');
|
|
751
|
+
}
|
|
752
|
+
if (!this.currentUserId) {
|
|
753
|
+
throw new Error('Missing sender identity from DropOnAir JWT subject');
|
|
754
|
+
}
|
|
755
|
+
if (this.dropOnAirJwt && (0, bytes_1.isJwtExpired)(this.dropOnAirJwt)) {
|
|
756
|
+
throw new Error('JWT_EXPIRED: Secure messaging session is reconnecting; please retry in a moment');
|
|
757
|
+
}
|
|
758
|
+
const editId = crypto.randomUUID();
|
|
759
|
+
const frame = {
|
|
760
|
+
type: 'MESSAGE_EDIT',
|
|
761
|
+
editId,
|
|
762
|
+
originalMessageId,
|
|
763
|
+
appId: this.options.appId,
|
|
764
|
+
fromUserId: this.currentUserId,
|
|
765
|
+
toUserId: '',
|
|
766
|
+
groupId,
|
|
767
|
+
timestamp: Date.now(),
|
|
768
|
+
encryptionType: 1, // CLEARTEXT
|
|
769
|
+
plaintextPayload: newPlaintext,
|
|
770
|
+
senderDeviceId: this.deviceId ?? await this.getOrCreateDeviceId(),
|
|
771
|
+
clientEditId: editId,
|
|
772
|
+
};
|
|
773
|
+
this.transport.send(this.codec.encodeMessageEditFrame(frame));
|
|
774
|
+
return { editId };
|
|
775
|
+
}
|
|
776
|
+
/**
|
|
777
|
+
* Delete a message you sent to a group. FOR_EVERYONE tells every member, and a
|
|
778
|
+
* member who fetches later is not handed the message. FOR_ME tells only your
|
|
779
|
+
* other devices. Only the sender of a message can delete it.
|
|
780
|
+
*/
|
|
781
|
+
async deleteGroupMessage(groupId, originalMessageId, scope = 'FOR_EVERYONE') {
|
|
782
|
+
if (!this.transport?.isOpen()) {
|
|
783
|
+
throw new Error('DropOnAir websocket is not connected');
|
|
784
|
+
}
|
|
785
|
+
if (!this.currentUserId) {
|
|
786
|
+
throw new Error('Missing sender identity from DropOnAir JWT subject');
|
|
787
|
+
}
|
|
788
|
+
if (this.dropOnAirJwt && (0, bytes_1.isJwtExpired)(this.dropOnAirJwt)) {
|
|
789
|
+
throw new Error('JWT_EXPIRED: Secure messaging session is reconnecting; please retry in a moment');
|
|
790
|
+
}
|
|
791
|
+
const deleteId = crypto.randomUUID();
|
|
792
|
+
const frame = {
|
|
793
|
+
type: 'MESSAGE_DELETE',
|
|
794
|
+
deleteId,
|
|
795
|
+
originalMessageId,
|
|
796
|
+
appId: this.options.appId,
|
|
797
|
+
fromUserId: this.currentUserId,
|
|
798
|
+
toUserId: '',
|
|
799
|
+
groupId,
|
|
800
|
+
timestamp: Date.now(),
|
|
801
|
+
scope,
|
|
802
|
+
clientDeleteId: deleteId,
|
|
803
|
+
};
|
|
804
|
+
this.transport.send(this.codec.encodeMessageDeleteFrame(frame));
|
|
805
|
+
return { deleteId };
|
|
806
|
+
}
|
|
656
807
|
// ---------------------------------------------------------------------------
|
|
657
808
|
// Push notification token registration (PROTOCOL_VERSION 5+)
|
|
658
809
|
// ---------------------------------------------------------------------------
|
|
@@ -1712,6 +1863,7 @@ class MessagingClient {
|
|
|
1712
1863
|
fromUserId: notif.fromUserId,
|
|
1713
1864
|
timestamp: notif.timestamp,
|
|
1714
1865
|
plaintext,
|
|
1866
|
+
attachments: (notif.attachments ?? []).map(a => attachment_client_1.AttachmentClient.fromWire(a)),
|
|
1715
1867
|
};
|
|
1716
1868
|
for (const listener of this.groupMessageListeners) {
|
|
1717
1869
|
listener(msg);
|
|
@@ -2011,7 +2163,67 @@ class MessagingClient {
|
|
|
2011
2163
|
});
|
|
2012
2164
|
});
|
|
2013
2165
|
}
|
|
2166
|
+
/**
|
|
2167
|
+
* One group edit, live or from the offline fetch. Decrypted exactly as a group
|
|
2168
|
+
* message is, with the edit id in place of the message id.
|
|
2169
|
+
*/
|
|
2170
|
+
async handleIncomingGroupEdit(frame) {
|
|
2171
|
+
try {
|
|
2172
|
+
let plaintext;
|
|
2173
|
+
if (frame.encryptionType === 1) {
|
|
2174
|
+
plaintext = frame.plaintextPayload ?? '';
|
|
2175
|
+
}
|
|
2176
|
+
else if (frame.devicePayloads && frame.devicePayloads.length > 0) {
|
|
2177
|
+
const myDeviceId = this.deviceId ?? await this.getOrCreateDeviceId();
|
|
2178
|
+
const myPayload = frame.devicePayloads.find(dp => dp.deviceId === myDeviceId);
|
|
2179
|
+
if (!myPayload) {
|
|
2180
|
+
this.log('incoming_group_edit_no_device_payload', {
|
|
2181
|
+
editId: frame.editId,
|
|
2182
|
+
myDeviceId,
|
|
2183
|
+
availableDeviceIds: frame.devicePayloads.map(dp => dp.deviceId),
|
|
2184
|
+
});
|
|
2185
|
+
return;
|
|
2186
|
+
}
|
|
2187
|
+
const peerUserIdForHkdf = frame.fromUserId === this.currentUserId ? this.currentUserId : frame.fromUserId;
|
|
2188
|
+
const sharedKey = await this.cryptoService.deriveSharedSecret(peerUserIdForHkdf, (0, bytes_1.toBase64)(myPayload.senderPublicKey), this.currentUserId);
|
|
2189
|
+
plaintext = await this.cryptoService.decrypt(myPayload.encryptedPayload, sharedKey, {
|
|
2190
|
+
messageId: frame.editId,
|
|
2191
|
+
senderId: frame.fromUserId,
|
|
2192
|
+
recipientId: this.currentUserId,
|
|
2193
|
+
timestamp: frame.timestamp,
|
|
2194
|
+
});
|
|
2195
|
+
}
|
|
2196
|
+
else {
|
|
2197
|
+
this.log('incoming_group_edit_no_payload', { editId: frame.editId });
|
|
2198
|
+
return;
|
|
2199
|
+
}
|
|
2200
|
+
const event = {
|
|
2201
|
+
editId: frame.editId,
|
|
2202
|
+
originalMessageId: frame.originalMessageId,
|
|
2203
|
+
groupId: frame.groupId,
|
|
2204
|
+
fromUserId: frame.fromUserId,
|
|
2205
|
+
timestamp: frame.timestamp,
|
|
2206
|
+
plaintext,
|
|
2207
|
+
};
|
|
2208
|
+
for (const listener of this.groupMessageEditListeners) {
|
|
2209
|
+
listener(event);
|
|
2210
|
+
}
|
|
2211
|
+
}
|
|
2212
|
+
catch (error) {
|
|
2213
|
+
this.logError('incoming_group_edit_failed', {
|
|
2214
|
+
editId: frame.editId,
|
|
2215
|
+
originalMessageId: frame.originalMessageId,
|
|
2216
|
+
groupId: frame.groupId,
|
|
2217
|
+
error: String(error?.message ?? error),
|
|
2218
|
+
});
|
|
2219
|
+
this.emitEvent({ type: 'ERROR', reason: 'DECRYPT_FAILED', metadata: frame.editId });
|
|
2220
|
+
}
|
|
2221
|
+
}
|
|
2014
2222
|
async handleIncomingMessageEdit(frame) {
|
|
2223
|
+
if (frame.groupId) {
|
|
2224
|
+
await this.handleIncomingGroupEdit(frame);
|
|
2225
|
+
return;
|
|
2226
|
+
}
|
|
2015
2227
|
try {
|
|
2016
2228
|
this.log('incoming_message_edit_received', {
|
|
2017
2229
|
editId: frame.editId,
|
|
@@ -2083,6 +2295,20 @@ class MessagingClient {
|
|
|
2083
2295
|
}
|
|
2084
2296
|
}
|
|
2085
2297
|
handleIncomingMessageDelete(frame) {
|
|
2298
|
+
if (frame.groupId) {
|
|
2299
|
+
const event = {
|
|
2300
|
+
deleteId: frame.deleteId,
|
|
2301
|
+
originalMessageId: frame.originalMessageId,
|
|
2302
|
+
groupId: frame.groupId,
|
|
2303
|
+
fromUserId: frame.fromUserId,
|
|
2304
|
+
timestamp: frame.timestamp,
|
|
2305
|
+
scope: frame.scope || 'FOR_EVERYONE',
|
|
2306
|
+
};
|
|
2307
|
+
for (const listener of this.groupMessageDeleteListeners) {
|
|
2308
|
+
listener(event);
|
|
2309
|
+
}
|
|
2310
|
+
return;
|
|
2311
|
+
}
|
|
2086
2312
|
this.log('incoming_message_delete_received', {
|
|
2087
2313
|
deleteId: frame.deleteId,
|
|
2088
2314
|
originalMessageId: frame.originalMessageId,
|
|
@@ -2382,6 +2608,9 @@ class MessagingClient {
|
|
|
2382
2608
|
let page = 0;
|
|
2383
2609
|
let totalPages = 1;
|
|
2384
2610
|
let totalProcessed = 0;
|
|
2611
|
+
// Edits and deletes come on the first page and are applied after every
|
|
2612
|
+
// message, because a change can be about a message on a later page.
|
|
2613
|
+
let changes = [];
|
|
2385
2614
|
this.log('group_offline_fetch_started', { httpUrl: this.httpUrl, pageSize: 100 });
|
|
2386
2615
|
while (page < totalPages) {
|
|
2387
2616
|
const response = await this.fetchFn(`${this.httpUrl}/v1/groups/messages/offline?page=${page}&size=100`, {
|
|
@@ -2398,6 +2627,9 @@ class MessagingClient {
|
|
|
2398
2627
|
}
|
|
2399
2628
|
const body = await response.json();
|
|
2400
2629
|
totalPages = body.totalPages ?? 0;
|
|
2630
|
+
if (page === 0) {
|
|
2631
|
+
changes = body.changes ?? [];
|
|
2632
|
+
}
|
|
2401
2633
|
for (const offline of body.messages) {
|
|
2402
2634
|
const notification = {
|
|
2403
2635
|
messageId: offline.messageId,
|
|
@@ -2412,6 +2644,24 @@ class MessagingClient {
|
|
|
2412
2644
|
encryptedPayload: (0, bytes_1.fromBase64)(dp.encryptedPayloadBase64),
|
|
2413
2645
|
senderPublicKey: (0, bytes_1.fromBase64)(dp.senderPublicKeyBase64),
|
|
2414
2646
|
})),
|
|
2647
|
+
// Files travel with a late message too. Without this, somebody who was
|
|
2648
|
+
// away came back to a message that mentioned nothing at all: the live
|
|
2649
|
+
// path carried the pointer and the catch-up did not.
|
|
2650
|
+
attachments: (offline.attachments ?? []).map(a => ({
|
|
2651
|
+
attachmentId: a.attachmentId,
|
|
2652
|
+
storageHint: a.storageHint ?? '',
|
|
2653
|
+
mimeType: a.mimeType ?? '',
|
|
2654
|
+
sizeBytes: a.sizeBytes ?? 0,
|
|
2655
|
+
sha256: a.sha256 ?? '',
|
|
2656
|
+
encryptionType: a.encryptionType === 'CLEARTEXT' ? 1 : 0,
|
|
2657
|
+
thumbnailAttachmentId: a.thumbnailAttachmentId ?? '',
|
|
2658
|
+
wrappedKeys: (a.wrappedKeys ?? []).map(k => ({
|
|
2659
|
+
deviceId: k.deviceId,
|
|
2660
|
+
wrappedKey: (0, bytes_1.fromBase64)(k.wrappedKeyBase64 ?? ''),
|
|
2661
|
+
senderPublicKey: (0, bytes_1.fromBase64)(k.senderPublicKeyBase64 ?? ''),
|
|
2662
|
+
nonce: (0, bytes_1.fromBase64)(k.nonceBase64 ?? ''),
|
|
2663
|
+
})),
|
|
2664
|
+
})),
|
|
2415
2665
|
};
|
|
2416
2666
|
// The same path a live message takes, so decryption, de-duplication and
|
|
2417
2667
|
// the callback behave identically.
|
|
@@ -2420,7 +2670,42 @@ class MessagingClient {
|
|
|
2420
2670
|
}
|
|
2421
2671
|
page += 1;
|
|
2422
2672
|
}
|
|
2423
|
-
|
|
2673
|
+
for (const change of changes) {
|
|
2674
|
+
if (change.kind === 'EDIT') {
|
|
2675
|
+
await this.handleIncomingGroupEdit({
|
|
2676
|
+
type: 'MESSAGE_EDIT',
|
|
2677
|
+
editId: change.changeId,
|
|
2678
|
+
originalMessageId: change.originalMessageId,
|
|
2679
|
+
appId: this.options.appId,
|
|
2680
|
+
fromUserId: change.fromUserId,
|
|
2681
|
+
toUserId: '',
|
|
2682
|
+
groupId: change.groupId,
|
|
2683
|
+
timestamp: Number(change.timestamp),
|
|
2684
|
+
encryptionType: change.encryptionType === 'CLEARTEXT' ? 1 : 0,
|
|
2685
|
+
plaintextPayload: change.plaintextPayload ?? undefined,
|
|
2686
|
+
senderDeviceId: change.senderDeviceId ?? undefined,
|
|
2687
|
+
devicePayloads: (change.devicePayloads ?? []).map(dp => ({
|
|
2688
|
+
deviceId: dp.deviceId,
|
|
2689
|
+
encryptedPayload: (0, bytes_1.fromBase64)(dp.encryptedPayloadBase64),
|
|
2690
|
+
senderPublicKey: (0, bytes_1.fromBase64)(dp.senderPublicKeyBase64 ?? ''),
|
|
2691
|
+
})),
|
|
2692
|
+
});
|
|
2693
|
+
}
|
|
2694
|
+
else if (change.kind === 'DELETE') {
|
|
2695
|
+
this.handleIncomingMessageDelete({
|
|
2696
|
+
type: 'MESSAGE_DELETE',
|
|
2697
|
+
deleteId: change.changeId,
|
|
2698
|
+
originalMessageId: change.originalMessageId,
|
|
2699
|
+
appId: this.options.appId,
|
|
2700
|
+
fromUserId: change.fromUserId,
|
|
2701
|
+
toUserId: '',
|
|
2702
|
+
groupId: change.groupId,
|
|
2703
|
+
timestamp: Number(change.timestamp),
|
|
2704
|
+
scope: change.scope || 'FOR_EVERYONE',
|
|
2705
|
+
});
|
|
2706
|
+
}
|
|
2707
|
+
}
|
|
2708
|
+
this.log('group_offline_fetch_done', { totalProcessed, changes: changes.length });
|
|
2424
2709
|
}
|
|
2425
2710
|
async fetchAndProcessOfflineMessages() {
|
|
2426
2711
|
const jwt = await this.getValidDropOnAirJwt(false);
|
package/dist/core/types.d.ts
CHANGED
|
@@ -91,6 +91,34 @@ export interface MessageDeleteEvent {
|
|
|
91
91
|
}
|
|
92
92
|
export type MessageEditCallback = (event: MessageEditEvent) => void;
|
|
93
93
|
export type MessageDeleteCallback = (event: MessageDeleteEvent) => void;
|
|
94
|
+
/**
|
|
95
|
+
* A message in a group, edited by its sender. Delivered live and again from the
|
|
96
|
+
* offline fetch, so applying the same edit twice must be harmless. Two edits of
|
|
97
|
+
* one message arrive oldest first.
|
|
98
|
+
*/
|
|
99
|
+
export interface GroupMessageEditEvent {
|
|
100
|
+
editId: string;
|
|
101
|
+
originalMessageId: string;
|
|
102
|
+
groupId: string;
|
|
103
|
+
fromUserId: string;
|
|
104
|
+
timestamp: number;
|
|
105
|
+
/** New plaintext for both E2EE (after decryption) and CLEARTEXT messages. */
|
|
106
|
+
plaintext: string;
|
|
107
|
+
}
|
|
108
|
+
/**
|
|
109
|
+
* A message in a group, deleted by its sender. Delivered live and again from the
|
|
110
|
+
* offline fetch, and can name a message this device never received.
|
|
111
|
+
*/
|
|
112
|
+
export interface GroupMessageDeleteEvent {
|
|
113
|
+
deleteId: string;
|
|
114
|
+
originalMessageId: string;
|
|
115
|
+
groupId: string;
|
|
116
|
+
fromUserId: string;
|
|
117
|
+
timestamp: number;
|
|
118
|
+
scope: 'FOR_EVERYONE' | 'FOR_ME' | string;
|
|
119
|
+
}
|
|
120
|
+
export type GroupMessageEditCallback = (event: GroupMessageEditEvent) => void;
|
|
121
|
+
export type GroupMessageDeleteCallback = (event: GroupMessageDeleteEvent) => void;
|
|
94
122
|
/**
|
|
95
123
|
* Delivered to the user's OTHER devices when one device calls markRead().
|
|
96
124
|
* Use it to clear whatever unread state your app keeps for that message.
|
|
@@ -194,6 +222,12 @@ export interface DecryptedGroupMessage {
|
|
|
194
222
|
fromUserId: string;
|
|
195
223
|
timestamp: number;
|
|
196
224
|
plaintext: string;
|
|
225
|
+
/**
|
|
226
|
+
* Files this message points at, if any. The bytes are fetched separately with
|
|
227
|
+
* `downloadAttachment`; what arrives here is the pointer and the file key
|
|
228
|
+
* wrapped for this device.
|
|
229
|
+
*/
|
|
230
|
+
attachments?: import('../attachment/attachment-types').AttachmentRef[];
|
|
197
231
|
}
|
|
198
232
|
export type GroupMessageCallback = (message: DecryptedGroupMessage) => void;
|
|
199
233
|
export type GroupCallEventType = 'GROUP_CALL_INVITE' | 'GROUP_CALL_RINGING' | 'GROUP_CALL_JOIN' | 'GROUP_CALL_LEAVE' | 'GROUP_CALL_END' | 'GROUP_CALL_ENDED' | 'GROUP_CALL_PARTICIPANT_JOINED' | 'GROUP_CALL_PARTICIPANT_LEFT' | 'GROUP_CALL_PARTICIPANT_REMOVED' | 'GROUP_CALL_ALREADY_ACTIVE' | 'GROUP_CALL_SDP_OFFER' | 'GROUP_CALL_SDP_ANSWER' | 'GROUP_CALL_ICE_CANDIDATE' | 'GROUP_CALL_SCREEN_SHARE_STARTED' | 'GROUP_CALL_SCREEN_SHARE_STOPPED' | 'GROUP_CALL_HOST_TRANSFER' | 'GROUP_CALL_COHOST_APPOINT' | 'GROUP_CALL_COHOST_REVOKE' | 'GROUP_CALL_ROLE_CHANGED' | 'GROUP_CALL_MUTE_PARTICIPANT' | 'GROUP_CALL_REMOVE_PARTICIPANT' | 'GROUP_CALL_WAITING_ROOM_REQUEST' | 'GROUP_CALL_WAITING_ROOM_JOINED' | 'GROUP_CALL_WAITING_ROOM_ADMIT' | 'GROUP_CALL_WAITING_ROOM_ADMITTED' | 'GROUP_CALL_WAITING_ROOM_REJECT' | 'GROUP_CALL_WAITING_ROOM_REJECTED' | 'GROUP_CALL_JOINED' | 'GROUP_CALL_WAITING_ROOM_PENDING' | 'GROUP_CALL_HOST_REQUIRED' | 'GROUP_CALL_ROOM_CLOSED' | 'GROUP_CALL_STAGE_HAND_RAISED' | 'GROUP_CALL_STAGE_HAND_LOWERED' | 'GROUP_CALL_STAGE_PROMOTE' | 'GROUP_CALL_STAGE_DEMOTE' | 'GROUP_CALL_STAGE_QUESTION' | 'GROUP_CALL_RECORDING_STARTED' | 'GROUP_CALL_RECORDING_STOPPED' | 'GROUP_CALL_RECORDING_AVAILABLE';
|
|
@@ -664,6 +698,29 @@ export interface DropOnAirClient {
|
|
|
664
698
|
}>;
|
|
665
699
|
/** Register a listener for incoming group messages. Returns an unsubscribe function. */
|
|
666
700
|
onGroupMessage(callback: GroupMessageCallback): () => void;
|
|
701
|
+
/**
|
|
702
|
+
* Edit a message you sent to a group, encrypted per member as
|
|
703
|
+
* {@link sendGroupMessage} is. Pass the same members. Only the sender can edit.
|
|
704
|
+
*/
|
|
705
|
+
editGroupMessage(groupId: string, originalMessageId: string, newPlaintext: string, memberUserIds: string[]): Promise<{
|
|
706
|
+
editId: string;
|
|
707
|
+
}>;
|
|
708
|
+
/** Edit a cleartext message you sent to a group. */
|
|
709
|
+
editCleartextGroupMessage(groupId: string, originalMessageId: string, newPlaintext: string): Promise<{
|
|
710
|
+
editId: string;
|
|
711
|
+
}>;
|
|
712
|
+
/**
|
|
713
|
+
* Delete a message you sent to a group. FOR_EVERYONE tells every member and
|
|
714
|
+
* a member who fetches later is not handed the message; FOR_ME tells only
|
|
715
|
+
* your other devices. Only the sender can delete.
|
|
716
|
+
*/
|
|
717
|
+
deleteGroupMessage(groupId: string, originalMessageId: string, scope?: 'FOR_EVERYONE' | 'FOR_ME'): Promise<{
|
|
718
|
+
deleteId: string;
|
|
719
|
+
}>;
|
|
720
|
+
/** Register a listener for edits of group messages. */
|
|
721
|
+
onGroupMessageEdit(callback: GroupMessageEditCallback): () => void;
|
|
722
|
+
/** Register a listener for deletes of group messages. */
|
|
723
|
+
onGroupMessageDelete(callback: GroupMessageDeleteCallback): () => void;
|
|
667
724
|
/** Initiate a group call. Returns the callId. */
|
|
668
725
|
startGroupCall(groupId: string): Promise<string>;
|
|
669
726
|
/** Join an active group call. */
|
package/dist/index.d.ts
CHANGED
|
@@ -22,7 +22,7 @@ export { SDK_VERSION, PROTOCOL_VERSION, PAYLOAD_FORMAT_VERSION } from './version
|
|
|
22
22
|
*/
|
|
23
23
|
export declare function createSecureIdentity(store?: IdentityRecordStore): Promise<WebCryptoIdentityProvider | null>;
|
|
24
24
|
export declare function initialize(options: InitializeOptions): Promise<DropOnAirClient>;
|
|
25
|
-
export type { InitializeOptions, DropOnAirClient, DropOnAirEvent, MessageCallback, EventCallback, DecryptedMessage, KeyStorageAdapter, CallEvent, CallEventType, CallEventCallback, TurnCredentials, BroadcastMessage, BroadcastCallback, GroupInfo, GroupMemberInfo, DecryptedGroupMessage, GroupMessageCallback, GroupCallEvent, GroupCallEventType, GroupCallEventCallback, Room, RoomPolicy, CreateRoomOptions, UpdateRoomOptions, SfuToken, SfuRecording, MessageEditEvent, MessageEditCallback, MessageDeleteEvent, MessageDeleteCallback, DeviceInfo, ReadReceiptEvent, ReadReceiptCallback, TypingEvent, TypingCallback, NotificationClearEvent, NotificationClearCallback, DraftSyncEvent, DraftSyncCallback, } from './core/types';
|
|
25
|
+
export type { InitializeOptions, DropOnAirClient, DropOnAirEvent, MessageCallback, EventCallback, DecryptedMessage, KeyStorageAdapter, CallEvent, CallEventType, CallEventCallback, TurnCredentials, BroadcastMessage, BroadcastCallback, GroupInfo, GroupMemberInfo, DecryptedGroupMessage, GroupMessageCallback, GroupCallEvent, GroupCallEventType, GroupCallEventCallback, Room, RoomPolicy, CreateRoomOptions, UpdateRoomOptions, SfuToken, SfuRecording, MessageEditEvent, MessageEditCallback, MessageDeleteEvent, MessageDeleteCallback, GroupMessageEditEvent, GroupMessageEditCallback, GroupMessageDeleteEvent, GroupMessageDeleteCallback, DeviceInfo, ReadReceiptEvent, ReadReceiptCallback, TypingEvent, TypingCallback, NotificationClearEvent, NotificationClearCallback, DraftSyncEvent, DraftSyncCallback, } from './core/types';
|
|
26
26
|
export type { AttachmentEncryptionType, AttachmentConversationType, AttachmentRef, DeviceWrappedKey, CreateUploadSessionOptions, UploadSession, PrepareAttachmentOptions, DownloadedAttachment, } from './attachment/attachment-types';
|
|
27
27
|
export { SseTransport, type SseTransportOptions, type SseFrameHandler, type SseStateHandler, } from './transport/sse-transport';
|
|
28
28
|
export { WebTransportTransport, type WebTransportTransportOptions, type WTFrameHandler, type WTStateHandler, } from './transport/webtransport-transport';
|
|
@@ -168,6 +168,10 @@ export interface WireMessageEditFrame {
|
|
|
168
168
|
senderDeviceId?: string;
|
|
169
169
|
plaintextPayload?: string;
|
|
170
170
|
clientEditId?: string;
|
|
171
|
+
/** Set when the edited message was sent to a group. */
|
|
172
|
+
groupId?: string;
|
|
173
|
+
/** Group E2EE edits sent to the relay: one payload set per member. */
|
|
174
|
+
memberPayloads?: WireGroupMemberPayload[];
|
|
171
175
|
}
|
|
172
176
|
/**
|
|
173
177
|
* Push token registration / unregister. Client -> server only. The server
|
|
@@ -204,6 +208,8 @@ export interface WireMessageDeleteFrame {
|
|
|
204
208
|
timestamp: number;
|
|
205
209
|
scope: string;
|
|
206
210
|
clientDeleteId?: string;
|
|
211
|
+
/** Set when the deleted message was sent to a group. */
|
|
212
|
+
groupId?: string;
|
|
207
213
|
}
|
|
208
214
|
export type InboundFrame = {
|
|
209
215
|
kind: 'envelope';
|
|
@@ -217,9 +217,15 @@ const MessageEditDeviceType = new protobuf.Type('DeviceEncryptedPayload')
|
|
|
217
217
|
.add(new protobuf.Field('encryptedPayload', 2, 'bytes'))
|
|
218
218
|
.add(new protobuf.Field('senderPublicKey', 3, 'bytes'));
|
|
219
219
|
const MessageEditEncryptionTypeEnum = new protobuf.Enum('EncryptionType', { E2EE: 0, CLEARTEXT: 1 });
|
|
220
|
+
// Its own instance, never the group envelope's. A protobuf.Type belongs to one
|
|
221
|
+
// parent, and sharing one across two broke every message this SDK sent.
|
|
222
|
+
const MessageEditMemberPayloadType = new protobuf.Type('GroupMemberPayload')
|
|
223
|
+
.add(new protobuf.Field('userId', 1, 'string'))
|
|
224
|
+
.add(new protobuf.Field('devicePayloads', 2, 'DeviceEncryptedPayload', 'repeated'));
|
|
220
225
|
const MessageEditFrameType = new protobuf.Type('MessageEditFrame')
|
|
221
226
|
.add(MessageEditEncryptionTypeEnum)
|
|
222
227
|
.add(MessageEditDeviceType)
|
|
228
|
+
.add(MessageEditMemberPayloadType)
|
|
223
229
|
.add(new protobuf.Field('type', 1, 'string'))
|
|
224
230
|
.add(new protobuf.Field('editId', 2, 'string'))
|
|
225
231
|
.add(new protobuf.Field('originalMessageId', 3, 'string'))
|
|
@@ -232,7 +238,11 @@ const MessageEditFrameType = new protobuf.Type('MessageEditFrame')
|
|
|
232
238
|
.add(new protobuf.Field('devicePayloads', 10, 'DeviceEncryptedPayload', 'repeated'))
|
|
233
239
|
.add(new protobuf.Field('senderDeviceId', 11, 'string'))
|
|
234
240
|
.add(new protobuf.Field('plaintextPayload', 12, 'string'))
|
|
235
|
-
.add(new protobuf.Field('clientEditId', 13, 'string'))
|
|
241
|
+
.add(new protobuf.Field('clientEditId', 13, 'string'))
|
|
242
|
+
// Group edits: groupId names the group; an E2EE edit sent to the relay carries
|
|
243
|
+
// one payload set per member, and each member receives only their own.
|
|
244
|
+
.add(new protobuf.Field('groupId', 14, 'string'))
|
|
245
|
+
.add(new protobuf.Field('memberPayloads', 15, 'GroupMemberPayload', 'repeated'));
|
|
236
246
|
const MessageDeleteFrameType = new protobuf.Type('MessageDeleteFrame')
|
|
237
247
|
.add(new protobuf.Field('type', 1, 'string'))
|
|
238
248
|
.add(new protobuf.Field('deleteId', 2, 'string'))
|
|
@@ -242,7 +252,8 @@ const MessageDeleteFrameType = new protobuf.Type('MessageDeleteFrame')
|
|
|
242
252
|
.add(new protobuf.Field('toUserId', 6, 'string'))
|
|
243
253
|
.add(new protobuf.Field('timestamp', 7, 'int64'))
|
|
244
254
|
.add(new protobuf.Field('scope', 8, 'string'))
|
|
245
|
-
.add(new protobuf.Field('clientDeleteId', 9, 'string'))
|
|
255
|
+
.add(new protobuf.Field('clientDeleteId', 9, 'string'))
|
|
256
|
+
.add(new protobuf.Field('groupId', 10, 'string'));
|
|
246
257
|
// Push token registration frame (PROTOCOL_VERSION 5+). Client to server only;
|
|
247
258
|
// the server emits a PUSH_REGISTERED / PUSH_UNREGISTERED Ack back. The token
|
|
248
259
|
// is opaque to the server and stored against (appId, userId, deviceId, platform).
|
package/dist/version.d.ts
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
* MINOR, additive feature (e.g. multi-device payloads, new call event type)
|
|
8
8
|
* PATCH, bug-fix / perf improvement with no wire or API change
|
|
9
9
|
*/
|
|
10
|
-
export declare const SDK_VERSION = "0.
|
|
10
|
+
export declare const SDK_VERSION = "0.34.0";
|
|
11
11
|
/**
|
|
12
12
|
* Binary encrypted-payload format version.
|
|
13
13
|
* Included as the first byte of every encrypted payload so receivers can
|
package/dist/version.js
CHANGED
|
@@ -10,7 +10,7 @@ exports.PROTOCOL_VERSION = exports.PAYLOAD_FORMAT_VERSION = exports.SDK_VERSION
|
|
|
10
10
|
* MINOR, additive feature (e.g. multi-device payloads, new call event type)
|
|
11
11
|
* PATCH, bug-fix / perf improvement with no wire or API change
|
|
12
12
|
*/
|
|
13
|
-
exports.SDK_VERSION = '0.
|
|
13
|
+
exports.SDK_VERSION = '0.34.0';
|
|
14
14
|
/**
|
|
15
15
|
* Binary encrypted-payload format version.
|
|
16
16
|
* Included as the first byte of every encrypted payload so receivers can
|
package/package.json
CHANGED