@droponair/sdk-js 0.11.0 → 0.13.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 +30 -0
- package/README.md +20 -0
- package/dist/attachment/attachment-client.d.ts +7 -0
- package/dist/attachment/attachment-client.js +31 -0
- package/dist/attachment/attachment-types.d.ts +15 -0
- package/dist/core/messaging-client.d.ts +32 -1
- package/dist/core/messaging-client.js +95 -0
- package/dist/core/types.d.ts +59 -0
- package/dist/index.d.ts +1 -1
- package/dist/transport/protobuf-codec.d.ts +3 -0
- package/dist/transport/protobuf-codec.js +5 -2
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -6,6 +6,36 @@ This project follows [Semantic Versioning](https://semver.org/).
|
|
|
6
6
|
|
|
7
7
|
---
|
|
8
8
|
|
|
9
|
+
## [0.13.0], 2026-05-21
|
|
10
|
+
|
|
11
|
+
### Added
|
|
12
|
+
|
|
13
|
+
- **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.
|
|
14
|
+
- **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.
|
|
15
|
+
- **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.
|
|
16
|
+
|
|
17
|
+
### Notes
|
|
18
|
+
|
|
19
|
+
- 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.
|
|
20
|
+
- No PROTOCOL_VERSION change — additive proto fields (`SyncFrame.fromUserId`, `AttachmentRef.thumbnailAttachmentId`) and an additive event type.
|
|
21
|
+
|
|
22
|
+
---
|
|
23
|
+
|
|
24
|
+
## [0.12.0], 2026-05-20
|
|
25
|
+
|
|
26
|
+
### Added
|
|
27
|
+
|
|
28
|
+
- **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) — it is plain own-device hygiene.
|
|
29
|
+
- **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.
|
|
30
|
+
- New exported types: `NotificationClearEvent` / `NotificationClearCallback`, `DraftSyncEvent` / `DraftSyncCallback`.
|
|
31
|
+
|
|
32
|
+
### Notes
|
|
33
|
+
|
|
34
|
+
- Both ride the existing `SyncFrame` wire type (own-device fan-out only, never delivered to a different user). No PROTOCOL_VERSION change.
|
|
35
|
+
- Your app decides when to call `clearNotification()` / `syncDraft()` — the platform never infers dismissal or tracks drafts.
|
|
36
|
+
|
|
37
|
+
---
|
|
38
|
+
|
|
9
39
|
## [0.11.0], 2026-05-20
|
|
10
40
|
|
|
11
41
|
### Added
|
package/README.md
CHANGED
|
@@ -102,6 +102,10 @@ const client = await initialize(options);
|
|
|
102
102
|
| `revokeMyDevice(deviceId)` | `Promise<DeviceInfo>` | Revoke one of the current user's devices. Permanent. |
|
|
103
103
|
| `markRead(messageId, conversationId?)` | `void` | Mark a message as read; relays a receipt to the user's other devices. |
|
|
104
104
|
| `onReadReceipt(callback)` | `() => void` | Listen for read receipts from the user's other devices. |
|
|
105
|
+
| `clearNotification(conversationId)` | `void` | Tell the user's other devices a conversation's notifications were dismissed. |
|
|
106
|
+
| `onNotificationCleared(callback)` | `() => void` | Listen for notification-clear syncs from the user's other devices. |
|
|
107
|
+
| `syncDraft(conversationId, draftText)` | `void` | Push a conversation draft to the user's other devices (opt-in, cleartext). |
|
|
108
|
+
| `onDraftSync(callback)` | `() => void` | Listen for draft syncs from the user's other devices. |
|
|
105
109
|
|
|
106
110
|
### Cross-device read receipts
|
|
107
111
|
|
|
@@ -117,6 +121,18 @@ client.onReadReceipt(e => {
|
|
|
117
121
|
});
|
|
118
122
|
```
|
|
119
123
|
|
|
124
|
+
### Notification clear & draft sync
|
|
125
|
+
|
|
126
|
+
Available since SDK `0.12.0`, both own-device only. `clearNotification()` tells your user's other devices a conversation's notifications were dismissed — always available. `syncDraft()` pushes a draft so the user can keep typing on another device — **opt-in** (the app owner enables it in the dashboard) and the draft text crosses the relay in cleartext.
|
|
127
|
+
|
|
128
|
+
```typescript
|
|
129
|
+
client.clearNotification(conversationId);
|
|
130
|
+
client.onNotificationCleared(e => { /* clear badge for e.conversationId */ });
|
|
131
|
+
|
|
132
|
+
client.syncDraft(conversationId, composerText);
|
|
133
|
+
client.onDraftSync(e => { /* pre-fill composer with e.draftText */ });
|
|
134
|
+
```
|
|
135
|
+
|
|
120
136
|
### Device trust
|
|
121
137
|
|
|
122
138
|
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.
|
|
@@ -215,8 +231,12 @@ client.onMessage(async (msg) => {
|
|
|
215
231
|
| `createUploadSession(options)` | `Promise<UploadSession>` | Low-level: presigned PUT URL only |
|
|
216
232
|
| `finalizeAttachment(attachmentId, sha256)` | `Promise<void>` | Low-level: commit integrity hash |
|
|
217
233
|
| `downloadAttachment(ref)` | `Promise<DownloadedAttachment>` | Get presigned URL + download + decrypt |
|
|
234
|
+
| `revokeAttachment(attachmentId)` | `Promise<void>` | Revoke an attachment you sent; recipients get `ATTACHMENT_REVOKED` |
|
|
218
235
|
| `sendMessage(toUserId, text, { attachments })` | `Promise<{ messageId }>` | Send with attachments |
|
|
219
236
|
|
|
237
|
+
- **Preview thumbnails (optional).** Pass `thumbnail` (bytes) in `prepareAttachmentAndUpload` options and the SDK uploads it as a separate encrypted attachment, linked via `AttachmentRef.thumbnailAttachmentId`. Purely the developer's choice; omit it and there is no thumbnail. Download the thumbnail like any attachment.
|
|
238
|
+
- **Revoke.** `revokeAttachment()` stops the platform issuing new download URLs and notifies recipients. Bytes a recipient already downloaded cannot be recalled. For GROUP attachments, download is authorized against *current* group membership, so a removed member loses access.
|
|
239
|
+
|
|
220
240
|
- E2EE: a random AES-256-GCM file key encrypts the bytes; the file key is wrapped per recipient device using X25519 + HKDF (same model as message payloads). Server never sees the unwrapped file key.
|
|
221
241
|
- Availability and per-file / per-month limits depend on your plan and are enforced server-side before the presigned URL is issued. See the [pricing page](https://www.droponair.com/pricing) and your dashboard Subscription page for what's enabled on your app.
|
|
222
242
|
- Upload URL TTL = 15 min. Download URL TTL = 5 min. Download authorization checks that the requester is the original sender or in the captured recipient list.
|
|
@@ -43,6 +43,13 @@ export declare class AttachmentClient {
|
|
|
43
43
|
* finalizes, and returns a fully-formed AttachmentRef for sendMessage.
|
|
44
44
|
*/
|
|
45
45
|
prepareAttachmentAndUpload(input: Uint8Array, opts: PrepareAttachmentOptions): Promise<AttachmentRef>;
|
|
46
|
+
/**
|
|
47
|
+
* Revoke an attachment you sent (Phase 2f). After revoke the platform
|
|
48
|
+
* refuses to issue further download URLs and recipients with a live
|
|
49
|
+
* connection get an ATTACHMENT_REVOKED event. Bytes a recipient already
|
|
50
|
+
* downloaded cannot be recalled.
|
|
51
|
+
*/
|
|
52
|
+
revokeAttachment(attachmentId: string): Promise<void>;
|
|
46
53
|
getDownloadUrl(attachmentId: string): Promise<{
|
|
47
54
|
url: string;
|
|
48
55
|
method: string;
|
|
@@ -78,6 +78,20 @@ class AttachmentClient {
|
|
|
78
78
|
if (!input || input.length === 0) {
|
|
79
79
|
throw new Error('attachment bytes are empty');
|
|
80
80
|
}
|
|
81
|
+
// Optional preview thumbnail: the developer's choice. If supplied, upload
|
|
82
|
+
// it as an independent attachment (same conversation + recipients +
|
|
83
|
+
// encryption) and link it from the main ref. The recursive call passes
|
|
84
|
+
// thumbnail: undefined so it terminates.
|
|
85
|
+
let thumbnailAttachmentId;
|
|
86
|
+
if (opts.thumbnail && opts.thumbnail.length > 0) {
|
|
87
|
+
const thumbRef = await this.prepareAttachmentAndUpload(opts.thumbnail, {
|
|
88
|
+
...opts,
|
|
89
|
+
thumbnail: undefined,
|
|
90
|
+
mimeType: opts.thumbnailMimeType ?? 'image/jpeg',
|
|
91
|
+
onUploadProgress: undefined,
|
|
92
|
+
});
|
|
93
|
+
thumbnailAttachmentId = thumbRef.attachmentId;
|
|
94
|
+
}
|
|
81
95
|
const encryptionType = opts.encryptionType ?? 'E2EE';
|
|
82
96
|
const conversationType = opts.groupId ? 'GROUP' : 'ONE_TO_ONE';
|
|
83
97
|
if (conversationType === 'ONE_TO_ONE' && !opts.toUserId) {
|
|
@@ -129,8 +143,23 @@ class AttachmentClient {
|
|
|
129
143
|
sha256,
|
|
130
144
|
encryptionType,
|
|
131
145
|
wrappedKeys,
|
|
146
|
+
thumbnailAttachmentId,
|
|
132
147
|
};
|
|
133
148
|
}
|
|
149
|
+
/**
|
|
150
|
+
* Revoke an attachment you sent (Phase 2f). After revoke the platform
|
|
151
|
+
* refuses to issue further download URLs and recipients with a live
|
|
152
|
+
* connection get an ATTACHMENT_REVOKED event. Bytes a recipient already
|
|
153
|
+
* downloaded cannot be recalled.
|
|
154
|
+
*/
|
|
155
|
+
async revokeAttachment(attachmentId) {
|
|
156
|
+
const jwt = await this.deps.getValidDropOnAirJwt();
|
|
157
|
+
const resp = await this.deps.fetchFn(`${this.deps.httpUrl}/v1/attachments/${encodeURIComponent(attachmentId)}/revoke`, { method: 'POST', headers: { Authorization: `Bearer ${jwt}` } });
|
|
158
|
+
if (!resp.ok) {
|
|
159
|
+
const text = await resp.text().catch(() => '');
|
|
160
|
+
throw new Error(`Failed to revoke attachment (HTTP ${resp.status}): ${text}`);
|
|
161
|
+
}
|
|
162
|
+
}
|
|
134
163
|
async getDownloadUrl(attachmentId) {
|
|
135
164
|
const jwt = await this.deps.getValidDropOnAirJwt();
|
|
136
165
|
const resp = await this.deps.fetchFn(`${this.deps.httpUrl}/v1/attachments/${encodeURIComponent(attachmentId)}/download-url`, {
|
|
@@ -204,6 +233,7 @@ class AttachmentClient {
|
|
|
204
233
|
senderPublicKey: wk.senderPublicKey,
|
|
205
234
|
nonce: wk.nonce,
|
|
206
235
|
})),
|
|
236
|
+
thumbnailAttachmentId: ref.thumbnailAttachmentId ?? '',
|
|
207
237
|
};
|
|
208
238
|
}
|
|
209
239
|
/** Convert a wire AttachmentRef (from a received message) into the public type. */
|
|
@@ -214,6 +244,7 @@ class AttachmentClient {
|
|
|
214
244
|
mimeType: wire.mimeType,
|
|
215
245
|
sizeBytes: Number(wire.sizeBytes),
|
|
216
246
|
sha256: wire.sha256,
|
|
247
|
+
thumbnailAttachmentId: wire.thumbnailAttachmentId ? wire.thumbnailAttachmentId : undefined,
|
|
217
248
|
encryptionType: wire.encryptionType === 1 ? 'CLEARTEXT' : 'E2EE',
|
|
218
249
|
wrappedKeys: (wire.wrappedKeys ?? []).map((wk) => ({
|
|
219
250
|
deviceId: wk.deviceId,
|
|
@@ -27,6 +27,12 @@ export interface AttachmentRef {
|
|
|
27
27
|
encryptionType: AttachmentEncryptionType;
|
|
28
28
|
/** Empty for CLEARTEXT. One entry per recipient device for E2EE. */
|
|
29
29
|
wrappedKeys: DeviceWrappedKey[];
|
|
30
|
+
/**
|
|
31
|
+
* Optional. The attachmentId of a separate attachment holding a preview
|
|
32
|
+
* thumbnail. Present only when the sender chose to attach one. Download it
|
|
33
|
+
* like any attachment via {@link DropOnAirClient.downloadAttachment}.
|
|
34
|
+
*/
|
|
35
|
+
thumbnailAttachmentId?: string;
|
|
30
36
|
}
|
|
31
37
|
/** Optional metadata for {@link DropOnAirClient.createUploadSession}. */
|
|
32
38
|
export interface CreateUploadSessionOptions {
|
|
@@ -66,6 +72,15 @@ export interface PrepareAttachmentOptions {
|
|
|
66
72
|
mimeType?: string;
|
|
67
73
|
/** Progress callback called with bytes uploaded so far. */
|
|
68
74
|
onUploadProgress?: (bytesUploaded: number, totalBytes: number) => void;
|
|
75
|
+
/**
|
|
76
|
+
* Optional preview thumbnail bytes. Entirely the developer's choice - if
|
|
77
|
+
* provided, the SDK uploads it as a separate attachment (encrypted with the
|
|
78
|
+
* same scheme as the main file) and links it via thumbnailAttachmentId. If
|
|
79
|
+
* omitted, the AttachmentRef simply has no thumbnail.
|
|
80
|
+
*/
|
|
81
|
+
thumbnail?: Uint8Array;
|
|
82
|
+
/** Mime type of the thumbnail bytes (defaults to 'image/jpeg'). */
|
|
83
|
+
thumbnailMimeType?: string;
|
|
69
84
|
}
|
|
70
85
|
/** Bytes payload returned by downloadAttachment. */
|
|
71
86
|
export interface DownloadedAttachment {
|
|
@@ -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, GroupCallEventCallback, GroupInfo, GroupMessageCallback, InitializeOptions, MessageCallback, MessageDeleteCallback, MessageEditCallback, TurnCredentials, DeviceInfo, ReadReceiptCallback } from './types';
|
|
3
|
+
import { BroadcastCallback, CallEventCallback, DropOnAirClient, EventCallback, GroupCallEventCallback, GroupInfo, GroupMessageCallback, InitializeOptions, MessageCallback, MessageDeleteCallback, MessageEditCallback, TurnCredentials, DeviceInfo, ReadReceiptCallback, NotificationClearCallback, DraftSyncCallback } 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;
|
|
@@ -72,6 +72,8 @@ export declare class MessagingClient implements DropOnAirClient {
|
|
|
72
72
|
private readonly broadcastListeners;
|
|
73
73
|
private readonly messageEditListeners;
|
|
74
74
|
private readonly readReceiptListeners;
|
|
75
|
+
private readonly notificationClearListeners;
|
|
76
|
+
private readonly draftSyncListeners;
|
|
75
77
|
private readonly messageDeleteListeners;
|
|
76
78
|
/** ------------------------------------------------------------------
|
|
77
79
|
* Lightweight structured logger. Only active when options.debug === true.
|
|
@@ -91,6 +93,7 @@ export declare class MessagingClient implements DropOnAirClient {
|
|
|
91
93
|
createUploadSession(options: CreateUploadSessionOptions): Promise<UploadSession>;
|
|
92
94
|
finalizeAttachment(attachmentId: string, sha256: string): Promise<void>;
|
|
93
95
|
downloadAttachment(ref: AttachmentRef): Promise<DownloadedAttachment>;
|
|
96
|
+
revokeAttachment(attachmentId: string): Promise<void>;
|
|
94
97
|
connect(): Promise<void>;
|
|
95
98
|
disconnect(): void;
|
|
96
99
|
sendMessage(toUserId: string, plaintextMessage: string, options?: {
|
|
@@ -178,6 +181,25 @@ export declare class MessagingClient implements DropOnAirClient {
|
|
|
178
181
|
* other devices so they can bucket the receipt
|
|
179
182
|
*/
|
|
180
183
|
markRead(messageId: string, conversationId?: string): void;
|
|
184
|
+
/**
|
|
185
|
+
* Tell this user's other devices that the notification(s) for a
|
|
186
|
+
* conversation have been dismissed - call this when YOUR app dismisses a
|
|
187
|
+
* notification or the user opens the conversation. The other devices clear
|
|
188
|
+
* the matching badge. The relay never decides what "dismissed" means.
|
|
189
|
+
*/
|
|
190
|
+
clearNotification(conversationId: string): void;
|
|
191
|
+
/**
|
|
192
|
+
* Push the current draft text for a conversation to this user's other
|
|
193
|
+
* devices so the user can continue typing on another device. Draft sync is
|
|
194
|
+
* opt-in: the app owner must enable it in the dashboard, and the draft text
|
|
195
|
+
* crosses the relay in cleartext (it is fanned out, never stored). If the
|
|
196
|
+
* feature is disabled the server silently drops the frame.
|
|
197
|
+
*/
|
|
198
|
+
syncDraft(conversationId: string, draftText: string): void;
|
|
199
|
+
/** Register a listener for notification-clear syncs from the user's other devices. */
|
|
200
|
+
onNotificationCleared(callback: NotificationClearCallback): () => void;
|
|
201
|
+
/** Register a listener for draft syncs from the user's other devices. */
|
|
202
|
+
onDraftSync(callback: DraftSyncCallback): () => void;
|
|
181
203
|
private handleIncomingSync;
|
|
182
204
|
sendCleartextMessage(toUserId: string, plaintext: string): Promise<{
|
|
183
205
|
messageId: string;
|
|
@@ -192,6 +214,15 @@ export declare class MessagingClient implements DropOnAirClient {
|
|
|
192
214
|
createGroup(name: string, memberUserIds?: string[]): Promise<GroupInfo>;
|
|
193
215
|
listGroups(): Promise<GroupInfo[]>;
|
|
194
216
|
getGroup(groupId: string): Promise<GroupInfo>;
|
|
217
|
+
/**
|
|
218
|
+
* Update a group. Any omitted field is left unchanged. `readReceiptsVisibleToGroup`
|
|
219
|
+
* (Phase 2e) controls whether members' group read receipts are visible to
|
|
220
|
+
* the whole group or stay own-device only.
|
|
221
|
+
*/
|
|
222
|
+
updateGroup(groupId: string, update: {
|
|
223
|
+
name?: string;
|
|
224
|
+
readReceiptsVisibleToGroup?: boolean;
|
|
225
|
+
}): Promise<GroupInfo>;
|
|
195
226
|
addGroupMembers(groupId: string, userIds: string[]): Promise<GroupInfo>;
|
|
196
227
|
removeGroupMember(groupId: string, userId: string): Promise<GroupInfo>;
|
|
197
228
|
deleteGroup(groupId: string): Promise<void>;
|
|
@@ -221,6 +221,8 @@ class MessagingClient {
|
|
|
221
221
|
this.broadcastListeners = new Set();
|
|
222
222
|
this.messageEditListeners = new Set();
|
|
223
223
|
this.readReceiptListeners = new Set();
|
|
224
|
+
this.notificationClearListeners = new Set();
|
|
225
|
+
this.draftSyncListeners = new Set();
|
|
224
226
|
this.messageDeleteListeners = new Set();
|
|
225
227
|
this.wsUrl = options.messagingWsUrl ?? 'wss://sdk.droponair.com/ws';
|
|
226
228
|
this.httpUrl = options.messagingHttpUrl ?? 'https://sdk.droponair.com';
|
|
@@ -268,6 +270,9 @@ class MessagingClient {
|
|
|
268
270
|
async downloadAttachment(ref) {
|
|
269
271
|
return this.attachmentClient.downloadAttachment(ref);
|
|
270
272
|
}
|
|
273
|
+
async revokeAttachment(attachmentId) {
|
|
274
|
+
return this.attachmentClient.revokeAttachment(attachmentId);
|
|
275
|
+
}
|
|
271
276
|
async connect() {
|
|
272
277
|
this.log('connect_start');
|
|
273
278
|
this.shouldReconnect = true;
|
|
@@ -667,12 +672,61 @@ class MessagingClient {
|
|
|
667
672
|
};
|
|
668
673
|
this.ws.send(this.codec.encodeSyncFrame(frame));
|
|
669
674
|
}
|
|
675
|
+
/**
|
|
676
|
+
* Tell this user's other devices that the notification(s) for a
|
|
677
|
+
* conversation have been dismissed - call this when YOUR app dismisses a
|
|
678
|
+
* notification or the user opens the conversation. The other devices clear
|
|
679
|
+
* the matching badge. The relay never decides what "dismissed" means.
|
|
680
|
+
*/
|
|
681
|
+
clearNotification(conversationId) {
|
|
682
|
+
if (!this.ws || this.ws.readyState !== WebSocket.OPEN) {
|
|
683
|
+
throw new Error('DropOnAir websocket is not connected');
|
|
684
|
+
}
|
|
685
|
+
const frame = {
|
|
686
|
+
type: 'SYNC_CLEAR_NOTIFICATION',
|
|
687
|
+
messageId: '',
|
|
688
|
+
conversationId,
|
|
689
|
+
timestamp: Date.now(),
|
|
690
|
+
};
|
|
691
|
+
this.ws.send(this.codec.encodeSyncFrame(frame));
|
|
692
|
+
}
|
|
693
|
+
/**
|
|
694
|
+
* Push the current draft text for a conversation to this user's other
|
|
695
|
+
* devices so the user can continue typing on another device. Draft sync is
|
|
696
|
+
* opt-in: the app owner must enable it in the dashboard, and the draft text
|
|
697
|
+
* crosses the relay in cleartext (it is fanned out, never stored). If the
|
|
698
|
+
* feature is disabled the server silently drops the frame.
|
|
699
|
+
*/
|
|
700
|
+
syncDraft(conversationId, draftText) {
|
|
701
|
+
if (!this.ws || this.ws.readyState !== WebSocket.OPEN) {
|
|
702
|
+
throw new Error('DropOnAir websocket is not connected');
|
|
703
|
+
}
|
|
704
|
+
const frame = {
|
|
705
|
+
type: 'SYNC_DRAFT',
|
|
706
|
+
messageId: '',
|
|
707
|
+
conversationId,
|
|
708
|
+
timestamp: Date.now(),
|
|
709
|
+
payload: draftText,
|
|
710
|
+
};
|
|
711
|
+
this.ws.send(this.codec.encodeSyncFrame(frame));
|
|
712
|
+
}
|
|
713
|
+
/** Register a listener for notification-clear syncs from the user's other devices. */
|
|
714
|
+
onNotificationCleared(callback) {
|
|
715
|
+
this.notificationClearListeners.add(callback);
|
|
716
|
+
return () => this.notificationClearListeners.delete(callback);
|
|
717
|
+
}
|
|
718
|
+
/** Register a listener for draft syncs from the user's other devices. */
|
|
719
|
+
onDraftSync(callback) {
|
|
720
|
+
this.draftSyncListeners.add(callback);
|
|
721
|
+
return () => this.draftSyncListeners.delete(callback);
|
|
722
|
+
}
|
|
670
723
|
handleIncomingSync(frame) {
|
|
671
724
|
if (frame.type === 'SYNC_READ_RECEIPT') {
|
|
672
725
|
const event = {
|
|
673
726
|
messageId: frame.messageId,
|
|
674
727
|
conversationId: frame.conversationId || undefined,
|
|
675
728
|
timestamp: frame.timestamp,
|
|
729
|
+
fromUserId: frame.fromUserId || undefined,
|
|
676
730
|
};
|
|
677
731
|
for (const listener of this.readReceiptListeners) {
|
|
678
732
|
try {
|
|
@@ -681,6 +735,31 @@ class MessagingClient {
|
|
|
681
735
|
catch { /* listener errors must not break the socket */ }
|
|
682
736
|
}
|
|
683
737
|
}
|
|
738
|
+
else if (frame.type === 'SYNC_CLEAR_NOTIFICATION') {
|
|
739
|
+
const event = {
|
|
740
|
+
conversationId: frame.conversationId || '',
|
|
741
|
+
timestamp: frame.timestamp,
|
|
742
|
+
};
|
|
743
|
+
for (const listener of this.notificationClearListeners) {
|
|
744
|
+
try {
|
|
745
|
+
listener(event);
|
|
746
|
+
}
|
|
747
|
+
catch { /* listener errors must not break the socket */ }
|
|
748
|
+
}
|
|
749
|
+
}
|
|
750
|
+
else if (frame.type === 'SYNC_DRAFT') {
|
|
751
|
+
const event = {
|
|
752
|
+
conversationId: frame.conversationId || '',
|
|
753
|
+
draftText: frame.payload || '',
|
|
754
|
+
timestamp: frame.timestamp,
|
|
755
|
+
};
|
|
756
|
+
for (const listener of this.draftSyncListeners) {
|
|
757
|
+
try {
|
|
758
|
+
listener(event);
|
|
759
|
+
}
|
|
760
|
+
catch { /* listener errors must not break the socket */ }
|
|
761
|
+
}
|
|
762
|
+
}
|
|
684
763
|
}
|
|
685
764
|
// ---------------------------------------------------------------------------
|
|
686
765
|
// Cleartext messaging (no E2EE key exchange required)
|
|
@@ -803,6 +882,22 @@ class MessagingClient {
|
|
|
803
882
|
throw new Error(`getGroup failed (HTTP ${res.status})`);
|
|
804
883
|
return res.json();
|
|
805
884
|
}
|
|
885
|
+
/**
|
|
886
|
+
* Update a group. Any omitted field is left unchanged. `readReceiptsVisibleToGroup`
|
|
887
|
+
* (Phase 2e) controls whether members' group read receipts are visible to
|
|
888
|
+
* the whole group or stay own-device only.
|
|
889
|
+
*/
|
|
890
|
+
async updateGroup(groupId, update) {
|
|
891
|
+
const jwt = await this.getValidDropOnAirJwt(false);
|
|
892
|
+
const res = await this.fetchFn(`${this.httpUrl}/api/groups/${encodeURIComponent(groupId)}`, {
|
|
893
|
+
method: 'PUT',
|
|
894
|
+
headers: { Authorization: `Bearer ${jwt}`, 'Content-Type': 'application/json' },
|
|
895
|
+
body: JSON.stringify(update),
|
|
896
|
+
});
|
|
897
|
+
if (!res.ok)
|
|
898
|
+
throw new Error(`updateGroup failed (HTTP ${res.status})`);
|
|
899
|
+
return res.json();
|
|
900
|
+
}
|
|
806
901
|
async addGroupMembers(groupId, userIds) {
|
|
807
902
|
const jwt = await this.getValidDropOnAirJwt(false);
|
|
808
903
|
const res = await this.fetchFn(`${this.httpUrl}/api/groups/${encodeURIComponent(groupId)}/members`, {
|
package/dist/core/types.d.ts
CHANGED
|
@@ -58,8 +58,34 @@ export interface ReadReceiptEvent {
|
|
|
58
58
|
/** Optional peer userId / group id, echoed from markRead() for bucketing. */
|
|
59
59
|
conversationId?: string;
|
|
60
60
|
timestamp: number;
|
|
61
|
+
/**
|
|
62
|
+
* The user who read the message. Set when a group read receipt is fanned
|
|
63
|
+
* to other members (so you know WHICH member read it). Undefined / your own
|
|
64
|
+
* userId for plain own-device sync.
|
|
65
|
+
*/
|
|
66
|
+
fromUserId?: string;
|
|
61
67
|
}
|
|
62
68
|
export type ReadReceiptCallback = (event: ReadReceiptEvent) => void;
|
|
69
|
+
/**
|
|
70
|
+
* Delivered to the user's OTHER devices when one device calls
|
|
71
|
+
* clearNotification(). Use it to clear the conversation's notification badge.
|
|
72
|
+
*/
|
|
73
|
+
export interface NotificationClearEvent {
|
|
74
|
+
conversationId: string;
|
|
75
|
+
timestamp: number;
|
|
76
|
+
}
|
|
77
|
+
export type NotificationClearCallback = (event: NotificationClearEvent) => void;
|
|
78
|
+
/**
|
|
79
|
+
* Delivered to the user's OTHER devices when one device calls syncDraft().
|
|
80
|
+
* Use it to pre-fill the message composer for that conversation. Draft sync
|
|
81
|
+
* is opt-in and the text crosses the relay in cleartext.
|
|
82
|
+
*/
|
|
83
|
+
export interface DraftSyncEvent {
|
|
84
|
+
conversationId: string;
|
|
85
|
+
draftText: string;
|
|
86
|
+
timestamp: number;
|
|
87
|
+
}
|
|
88
|
+
export type DraftSyncCallback = (event: DraftSyncEvent) => void;
|
|
63
89
|
export interface BroadcastMessage {
|
|
64
90
|
broadcastId: string;
|
|
65
91
|
channelId: string;
|
|
@@ -90,6 +116,11 @@ export interface GroupInfo {
|
|
|
90
116
|
createdBy: string;
|
|
91
117
|
members: GroupMemberInfo[];
|
|
92
118
|
createdAt: number;
|
|
119
|
+
/**
|
|
120
|
+
* Phase 2e. When true, a member's group read receipt is fanned to all other
|
|
121
|
+
* members; when false it stays own-device only. Controlled via updateGroup().
|
|
122
|
+
*/
|
|
123
|
+
readReceiptsVisibleToGroup?: boolean;
|
|
93
124
|
}
|
|
94
125
|
export interface GroupMemberInfo {
|
|
95
126
|
userId: string;
|
|
@@ -203,6 +234,20 @@ export interface DropOnAirClient {
|
|
|
203
234
|
markRead(messageId: string, conversationId?: string): void;
|
|
204
235
|
/** Register a listener for read receipts reported by this user's other devices. */
|
|
205
236
|
onReadReceipt(callback: ReadReceiptCallback): () => void;
|
|
237
|
+
/**
|
|
238
|
+
* Tell the user's other devices a conversation's notifications were
|
|
239
|
+
* dismissed. The app decides what "dismissed" means.
|
|
240
|
+
*/
|
|
241
|
+
clearNotification(conversationId: string): void;
|
|
242
|
+
/** Listen for notification-clear syncs from the user's other devices. */
|
|
243
|
+
onNotificationCleared(callback: NotificationClearCallback): () => void;
|
|
244
|
+
/**
|
|
245
|
+
* Push a conversation draft to the user's other devices. Opt-in; the draft
|
|
246
|
+
* text crosses the relay in cleartext and is never stored.
|
|
247
|
+
*/
|
|
248
|
+
syncDraft(conversationId: string, draftText: string): void;
|
|
249
|
+
/** Listen for draft syncs from the user's other devices. */
|
|
250
|
+
onDraftSync(callback: DraftSyncCallback): () => void;
|
|
206
251
|
/**
|
|
207
252
|
* Register this device's push notification token. The platform delivers a
|
|
208
253
|
* push via APNs / FCM / Web Push when a sender attaches a pushPayload to
|
|
@@ -240,6 +285,12 @@ export interface DropOnAirClient {
|
|
|
240
285
|
finalizeAttachment(attachmentId: string, sha256: string): Promise<void>;
|
|
241
286
|
/** Download (and for E2EE decrypt) an attachment referenced inside a received message. */
|
|
242
287
|
downloadAttachment(ref: import('../attachment/attachment-types').AttachmentRef): Promise<import('../attachment/attachment-types').DownloadedAttachment>;
|
|
288
|
+
/**
|
|
289
|
+
* Revoke an attachment you sent (Phase 2f). The platform stops issuing
|
|
290
|
+
* download URLs for it and notifies recipients. Bytes already downloaded
|
|
291
|
+
* cannot be recalled.
|
|
292
|
+
*/
|
|
293
|
+
revokeAttachment(attachmentId: string): Promise<void>;
|
|
243
294
|
/** Send a cleartext message to a user (no encryption). */
|
|
244
295
|
sendCleartextMessage(toUserId: string, plaintext: string): Promise<{
|
|
245
296
|
messageId: string;
|
|
@@ -290,6 +341,14 @@ export interface DropOnAirClient {
|
|
|
290
341
|
listGroups(): Promise<GroupInfo[]>;
|
|
291
342
|
/** Get details of a specific group. */
|
|
292
343
|
getGroup(groupId: string): Promise<GroupInfo>;
|
|
344
|
+
/**
|
|
345
|
+
* Update a group (requires OWNER/ADMIN role). Omitted fields are unchanged.
|
|
346
|
+
* `readReceiptsVisibleToGroup` toggles group-wide read-receipt visibility.
|
|
347
|
+
*/
|
|
348
|
+
updateGroup(groupId: string, update: {
|
|
349
|
+
name?: string;
|
|
350
|
+
readReceiptsVisibleToGroup?: boolean;
|
|
351
|
+
}): Promise<GroupInfo>;
|
|
293
352
|
/** Add members to a group (requires OWNER/ADMIN role). */
|
|
294
353
|
addGroupMembers(groupId: string, userIds: string[]): Promise<GroupInfo>;
|
|
295
354
|
/** Remove a member from a group (OWNER/ADMIN can remove others; anyone can leave). */
|
package/dist/index.d.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { InitializeOptions, DropOnAirClient } from './core/types';
|
|
2
2
|
export { SDK_VERSION, PROTOCOL_VERSION, PAYLOAD_FORMAT_VERSION } from './version';
|
|
3
3
|
export declare function initialize(options: InitializeOptions): Promise<DropOnAirClient>;
|
|
4
|
-
export type { InitializeOptions, DropOnAirClient, DropOnAirEvent, MessageCallback, EventCallback, DecryptedMessage, KeyStorageAdapter, CallEvent, CallEventType, CallEventCallback, TurnCredentials, BroadcastMessage, BroadcastCallback, GroupInfo, GroupMemberInfo, DecryptedGroupMessage, GroupMessageCallback, GroupCallEvent, GroupCallEventType, GroupCallEventCallback, MessageEditEvent, MessageEditCallback, MessageDeleteEvent, MessageDeleteCallback, DeviceInfo, ReadReceiptEvent, ReadReceiptCallback, } from './core/types';
|
|
4
|
+
export type { InitializeOptions, DropOnAirClient, DropOnAirEvent, MessageCallback, EventCallback, DecryptedMessage, KeyStorageAdapter, CallEvent, CallEventType, CallEventCallback, TurnCredentials, BroadcastMessage, BroadcastCallback, GroupInfo, GroupMemberInfo, DecryptedGroupMessage, GroupMessageCallback, GroupCallEvent, GroupCallEventType, GroupCallEventCallback, MessageEditEvent, MessageEditCallback, MessageDeleteEvent, MessageDeleteCallback, DeviceInfo, ReadReceiptEvent, ReadReceiptCallback, NotificationClearEvent, NotificationClearCallback, DraftSyncEvent, DraftSyncCallback, } from './core/types';
|
|
5
5
|
export type { AttachmentEncryptionType, AttachmentConversationType, AttachmentRef, DeviceWrappedKey, CreateUploadSessionOptions, UploadSession, PrepareAttachmentOptions, DownloadedAttachment, } from './attachment/attachment-types';
|
|
6
6
|
declare const _default: {
|
|
7
7
|
initialize: typeof initialize;
|
|
@@ -20,6 +20,7 @@ export interface WireAttachmentRef {
|
|
|
20
20
|
sha256: string;
|
|
21
21
|
encryptionType: number;
|
|
22
22
|
wrappedKeys?: WireDeviceWrappedKey[];
|
|
23
|
+
thumbnailAttachmentId?: string;
|
|
23
24
|
}
|
|
24
25
|
export interface WireEnvelope {
|
|
25
26
|
messageId: string;
|
|
@@ -157,6 +158,8 @@ export interface WireSyncFrame {
|
|
|
157
158
|
messageId: string;
|
|
158
159
|
conversationId?: string;
|
|
159
160
|
timestamp: number;
|
|
161
|
+
payload?: string;
|
|
162
|
+
fromUserId?: string;
|
|
160
163
|
}
|
|
161
164
|
/** Tombstone frame, scope FOR_EVERYONE or FOR_ME. */
|
|
162
165
|
export interface WireMessageDeleteFrame {
|
|
@@ -59,7 +59,8 @@ const AttachmentRefType = new protobuf.Type('AttachmentRef')
|
|
|
59
59
|
.add(new protobuf.Field('sizeBytes', 4, 'int64'))
|
|
60
60
|
.add(new protobuf.Field('sha256', 5, 'string'))
|
|
61
61
|
.add(new protobuf.Field('encryptionType', 6, 'EncryptionType'))
|
|
62
|
-
.add(new protobuf.Field('wrappedKeys', 7, 'DeviceWrappedKey', 'repeated'))
|
|
62
|
+
.add(new protobuf.Field('wrappedKeys', 7, 'DeviceWrappedKey', 'repeated'))
|
|
63
|
+
.add(new protobuf.Field('thumbnailAttachmentId', 8, 'string'));
|
|
63
64
|
const EnvelopeType = new protobuf.Type('Envelope')
|
|
64
65
|
.add(EncryptionTypeEnum)
|
|
65
66
|
.add(DeviceEncryptedPayloadType) // nested type must be added first
|
|
@@ -224,7 +225,9 @@ const SyncFrameType = new protobuf.Type('SyncFrame')
|
|
|
224
225
|
.add(new protobuf.Field('type', 1, 'string'))
|
|
225
226
|
.add(new protobuf.Field('messageId', 2, 'string'))
|
|
226
227
|
.add(new protobuf.Field('conversationId', 3, 'string'))
|
|
227
|
-
.add(new protobuf.Field('timestamp', 4, 'int64'))
|
|
228
|
+
.add(new protobuf.Field('timestamp', 4, 'int64'))
|
|
229
|
+
.add(new protobuf.Field('payload', 5, 'string'))
|
|
230
|
+
.add(new protobuf.Field('fromUserId', 6, 'string'));
|
|
228
231
|
class ProtobufCodec {
|
|
229
232
|
encodeEnvelope(value) {
|
|
230
233
|
return EnvelopeType.encode(value).finish();
|
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.13.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.13.0';
|
|
14
14
|
/**
|
|
15
15
|
* Binary encrypted-payload format version.
|
|
16
16
|
* Included as the first byte of every encrypted payload so receivers can
|