@droponair/sdk-js 0.25.1 → 0.27.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 CHANGED
@@ -1,5 +1,57 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.27.0
4
+
5
+ ### Added
6
+
7
+ - **Group messages sent while you were offline are now delivered.** On connect the SDK
8
+ drains anything it missed and hands each message to `onGroupMessage` exactly as if it
9
+ had arrived live, so a message sent to a closed or disconnected app is no longer lost.
10
+ Nothing to call: it happens on every connect, beside the one-to-one drain.
11
+ - **Delivery receipts on group messages name the member they are about.** The group
12
+ acknowledgement now carries `memberUserId`. A group message has many recipients, so
13
+ delivery is a set rather than a flag, and the platform hands you the set rather than
14
+ collapsing it: show a single tick once everyone has it, or a read-by list, whichever
15
+ your app wants. `memberUserId` is empty on `SERVER_RECEIVED`, which is about the relay
16
+ rather than any member.
17
+
18
+ ### Notes
19
+
20
+ - Group messages were always stored for absent members but nothing ever returned them,
21
+ so they were silently lost and `DELIVERED` could never arrive. Both are fixed.
22
+ - Additive and backward compatible. An older SDK simply never asks for the missed
23
+ messages and behaves exactly as it does today; a relay that predates this answers
24
+ 404 and the SDK treats that as nothing to catch up on.
25
+
26
+ ## 0.26.0
27
+
28
+ ### Added
29
+
30
+ - **Push payloads on outgoing messages.** `sendMessage` and `sendGroupMessage` accept
31
+ `options.push`, and `defaultPushPayload` on the client config attaches one to every
32
+ message that does not carry its own. Setting the default once is the recommended
33
+ route: passing a payload per send means forgetting it once stops push with no error
34
+ to notice.
35
+ - **`PushDelivery` chooses how a push behaves** on the recipient's device, and the
36
+ platform translates it per transport rather than deciding for you:
37
+ - `alert` shows title and body as supplied. Suits apps sending cleartext.
38
+ - `mutable` shows them as a placeholder and lets your app replace the text before it
39
+ appears, which is how an end-to-end encrypted app puts real content on a lock
40
+ screen without the relay seeing it. On iOS this is also the reliable way to be
41
+ woken, because the system throttles silent pushes.
42
+ - `silent` wakes the app and shows nothing.
43
+ - `voip` is the call invite path, superseding the `voip` boolean, which is still honoured.
44
+
45
+ ### Notes
46
+
47
+ - Push previously could not be triggered at all: no SDK exposed a way to attach a
48
+ payload, and the server only pushes when the sender supplies one.
49
+ - Never put message content in `title` or `body`. Every field is readable by the relay,
50
+ which is why it can act on them, and putting plaintext there leaks it through the one
51
+ path the platform promises never sees it.
52
+ - Additive and backward compatible. Messages sent without a payload behave exactly as
53
+ before, and an absent or unrecognised delivery mode is treated as `alert`.
54
+
3
55
  ## 0.25.1
4
56
 
5
57
  ### Changed
@@ -483,7 +535,7 @@ This project follows [Semantic Versioning](https://semver.org/).
483
535
  ### Added
484
536
  - **SDK versioning infrastructure:** `SDK_VERSION`, `PROTOCOL_VERSION`, and `PAYLOAD_FORMAT_VERSION` exported from `src/version.ts`
485
537
  - **Version telemetry:** `X-SDK-Version` header sent on token exchange; `sdkVersion` and `protocolVersion` query params on WebSocket handshake
486
- - **Server info endpoint:** SDK-BE now exposes `GET /api/info` (no auth) returning `protocolVersion`, `minSdkVersion`, and supported `features`
538
+ - **Server info endpoint:** the platform now exposes `GET /api/info` (no auth) returning `protocolVersion`, `minSdkVersion`, and supported `features`
487
539
  - **Multi-device E2EE:** Per-device encrypted payloads (`devicePayloads[]` in Envelope proto), sender encrypts once per recipient device + self-sync to own other devices
488
540
  - **Self-sync:** Sent messages are encrypted for the sender's other devices so all devices see the conversation in real time
489
541
  - **Legacy fallback:** When recipient has no device keys, SDK falls back to single `encryptedPayload` path for backward compat with older clients
package/README.md CHANGED
@@ -224,6 +224,45 @@ await client.registerPushToken({ platform: 'WEB_PUSH', token: JSON.stringify(sub
224
224
  await client.unregisterPushToken({ platform: 'APNS' });
225
225
  ```
226
226
 
227
+ #### Attaching a payload
228
+
229
+ A push is only sent when the sender attaches a payload, so set a default once and
230
+ every message carries it. Passing one on each send also works, but forgetting once
231
+ stops push with no error to notice.
232
+
233
+ ```typescript
234
+ const client = await initialize({
235
+ appId, publicApiKey, getUserJwt,
236
+ defaultPushPayload: { title: 'New message', delivery: 'mutable' },
237
+ });
238
+
239
+ // Override for a single message
240
+ await client.sendMessage(userId, text, {
241
+ push: { title: 'New photo', delivery: 'mutable', threadId: conversationId },
242
+ });
243
+ ```
244
+
245
+ #### Choosing how the push behaves
246
+
247
+ `delivery` says what the recipient's device should do. The platform translates it
248
+ per transport, so you pick what suits your app rather than adopting somebody else's
249
+ model.
250
+
251
+ | `delivery` | What happens |
252
+ | --- | --- |
253
+ | `'alert'` | Title and body are shown exactly as supplied. Suits apps sending cleartext. |
254
+ | `'mutable'` | Shown as a placeholder, then your app replaces the text before it appears. This is how an end-to-end encrypted app shows real content without the relay seeing it. On iOS it needs a notification service extension; on Android it arrives as a data message. |
255
+ | `'silent'` | The app is woken and nothing is shown. **iOS throttles these and does not guarantee delivery**, so use `'mutable'` when being woken matters. |
256
+ | `'voip'` | Call invite: PushKit on iOS, high priority data on Android. Requires a registered VoIP token. |
257
+
258
+ Omit `delivery` and it defaults to `'alert'`.
259
+
260
+ **Never put message content in `title` or `body`.** Every field in the payload is
261
+ readable by the relay, which is exactly why it can act on them, so putting plaintext
262
+ there leaks it through the one path that otherwise never sees it. Send a placeholder
263
+ and use `'mutable'` to replace it on the device.
264
+
265
+
227
266
  E2EE invariant: the push body is sender-supplied cleartext metadata only (e.g. "1 new message from Alice"), never the encrypted message contents. The recipient SDK decrypts the real message after the push wakes the device and the WebSocket reconnects.
228
267
 
229
268
  ### Message Edit & Delete
@@ -320,6 +359,17 @@ client.onMessage(async (msg) => {
320
359
  | `sendCleartextGroupMessage(groupId, plaintext)` | `Promise<{ messageId }>` | Send a plaintext (non-encrypted) group message; server fans out to every other member. |
321
360
  | `onGroupMessage(callback)` | `() => void` | Listen for group messages |
322
361
 
362
+ **Group delivery is per member.** A group message has many recipients, so the platform
363
+ reports delivery as a set rather than a single flag and leaves the presentation to you.
364
+ `DELIVERED` and `SEEN` arrive as events whose `metadata` carries `messageId`, `groupId`
365
+ and `memberUserId`, the member the acknowledgement is about. Show one tick once the set
366
+ covers the group, or a read-by list, whichever suits your app. `memberUserId` is absent
367
+ on `SERVER_RECEIVED`, which is about the relay rather than any member.
368
+
369
+ **Group messages missed while offline arrive on connect.** Anything sent while the app
370
+ was closed or disconnected is drained automatically when it reconnects and delivered
371
+ through `onGroupMessage` just like a live message. There is nothing to call.
372
+
323
373
  ### 1-to-1 Calls
324
374
 
325
375
  | Method | Returns | Description |
@@ -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, KeyCustody } 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, 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;
@@ -11,6 +11,7 @@ export declare class MessagingClient implements DropOnAirClient {
11
11
  private readonly httpUrl;
12
12
  private readonly tokenExchangeEndpoint;
13
13
  private readonly keyDirectoryEndpoint;
14
+ private readonly defaultPushPayload?;
14
15
  private readonly fetchFn;
15
16
  private readonly codec;
16
17
  private attachmentClient;
@@ -29,6 +30,12 @@ export declare class MessagingClient implements DropOnAirClient {
29
30
  private visibilityChangeHandler;
30
31
  private readonly autoAckIncomingMessages;
31
32
  private static readonly DEVICE_KEYS_CACHE_TTL_MS;
33
+ /**
34
+ * Resolves the payload for a send: what the caller passed, otherwise the
35
+ * client default, otherwise none at all. Returns undefined when there is
36
+ * nothing to send, so no push is requested rather than an empty one.
37
+ */
38
+ private resolvePushPayload;
32
39
  private readonly deviceKeysCache;
33
40
  private readonly callListeners;
34
41
  /** Pending startCall resolver, only one outgoing call can be in-flight at a time. */
@@ -112,6 +119,7 @@ export declare class MessagingClient implements DropOnAirClient {
112
119
  sendMessage(toUserId: string, plaintextMessage: string, options?: {
113
120
  attachments?: AttachmentRef[];
114
121
  clientMessageId?: string;
122
+ push?: PushPayload;
115
123
  }): Promise<{
116
124
  messageId: string;
117
125
  }>;
@@ -282,6 +290,7 @@ export declare class MessagingClient implements DropOnAirClient {
282
290
  */
283
291
  sendGroupMessage(groupId: string, plaintext: string, memberUserIds: string[], options?: {
284
292
  attachments?: AttachmentRef[];
293
+ push?: PushPayload;
285
294
  }): Promise<{
286
295
  messageId: string;
287
296
  }>;
@@ -365,6 +374,14 @@ export declare class MessagingClient implements DropOnAirClient {
365
374
  */
366
375
  private fetchMyOtherDeviceKeys;
367
376
  private ensureIdentityPublished;
377
+ /**
378
+ * Group messages that arrived while this device had no connection.
379
+ *
380
+ * Without this a group message is delivered only to sessions that happen to be
381
+ * live at that instant and is unreachable afterwards, so anyone closed loses it.
382
+ * Fetching is also what tells the sender the message reached this member.
383
+ */
384
+ private fetchAndProcessOfflineGroupMessages;
368
385
  private fetchAndProcessOfflineMessages;
369
386
  private getValidDropOnAirJwt;
370
387
  private extractSubject;
@@ -11,6 +11,30 @@ const version_1 = require("../version");
11
11
  const attachment_client_1 = require("../attachment/attachment-client");
12
12
  const STORAGE_DEVICE_ID = 'droponair.device.id.v1';
13
13
  class MessagingClient {
14
+ /**
15
+ * Resolves the payload for a send: what the caller passed, otherwise the
16
+ * client default, otherwise none at all. Returns undefined when there is
17
+ * nothing to send, so no push is requested rather than an empty one.
18
+ */
19
+ resolvePushPayload(override) {
20
+ const payload = override ?? this.defaultPushPayload;
21
+ if (!payload)
22
+ return undefined;
23
+ const deliveryCodes = { alert: 0, mutable: 1, silent: 2, voip: 3 };
24
+ const delivery = deliveryCodes[payload.delivery ?? 'alert'] ?? 0;
25
+ return {
26
+ title: payload.title ?? '',
27
+ body: payload.body ?? '',
28
+ badge: payload.badge,
29
+ sound: payload.sound,
30
+ category: payload.category,
31
+ threadId: payload.threadId,
32
+ customJson: payload.customJson,
33
+ delivery,
34
+ // The server still reads the older boolean, so keep it consistent.
35
+ voip: delivery === 3 ? true : undefined,
36
+ };
37
+ }
14
38
  reconnectDelayMs() {
15
39
  // Exponential backoff with jitter: ~2s, 4s, 8s, 16s, 32s, 60s (capped), +/-20%.
16
40
  // Jitter avoids many clients reconnecting in lockstep after a server blip.
@@ -260,6 +284,7 @@ class MessagingClient {
260
284
  this.httpUrl = options.messagingHttpUrl ?? 'https://sdk.droponair.com';
261
285
  this.tokenExchangeEndpoint = options.tokenExchangeEndpoint ?? '/api/messaging/token-exchange';
262
286
  this.keyDirectoryEndpoint = options.keyDirectoryEndpoint ?? '/api/messaging/keys';
287
+ this.defaultPushPayload = options.defaultPushPayload;
263
288
  const providedFetch = options.fetchFn;
264
289
  const globalFetch = typeof globalThis !== 'undefined' ? globalThis.fetch : undefined;
265
290
  const resolvedFetch = providedFetch ?? globalFetch;
@@ -419,6 +444,7 @@ class MessagingClient {
419
444
  if (options?.attachments && options.attachments.length > 0) {
420
445
  envelope.attachments = options.attachments.map(a => this.attachmentClient.toWire(a));
421
446
  }
447
+ envelope.pushPayload = this.resolvePushPayload(options?.push);
422
448
  this.sendFrame(this.codec.encodeEnvelope(envelope));
423
449
  }
424
450
  else {
@@ -442,6 +468,7 @@ class MessagingClient {
442
468
  if (options?.attachments && options.attachments.length > 0) {
443
469
  envelope.attachments = options.attachments.map(a => this.attachmentClient.toWire(a));
444
470
  }
471
+ envelope.pushPayload = this.resolvePushPayload(options?.push);
445
472
  this.sendFrame(this.codec.encodeEnvelope(envelope));
446
473
  }
447
474
  return { messageId };
@@ -1223,6 +1250,7 @@ class MessagingClient {
1223
1250
  encryptionType: 0, // E2EE
1224
1251
  memberPayloads,
1225
1252
  };
1253
+ frame.pushPayload = this.resolvePushPayload(options?.push);
1226
1254
  if (options?.attachments && options.attachments.length > 0) {
1227
1255
  frame.attachments = options.attachments.map(a => this.attachmentClient.toWire(a));
1228
1256
  }
@@ -1751,6 +1779,9 @@ class MessagingClient {
1751
1779
  if (this.dropOnAirJwt) {
1752
1780
  this.scheduleProactiveTokenRefresh(this.dropOnAirJwt);
1753
1781
  }
1782
+ this.fetchAndProcessOfflineGroupMessages().catch((err) => {
1783
+ this.logError('group_offline_fetch_failed', { error: String(err) });
1784
+ });
1754
1785
  this.fetchAndProcessOfflineMessages().catch((err) => {
1755
1786
  this.logError('offline_fetch_failed', { error: String(err?.message ?? err) });
1756
1787
  this.emitEvent({ type: 'ERROR', reason: 'OFFLINE_FETCH_FAILED' });
@@ -1859,7 +1890,14 @@ class MessagingClient {
1859
1890
  if (frame.kind === 'groupAck') {
1860
1891
  this.emitEvent({
1861
1892
  type: frame.data.type,
1862
- metadata: JSON.stringify({ messageId: frame.data.messageId, groupId: frame.data.groupId }),
1893
+ // memberUserId says which member this concerns. A group message has
1894
+ // many recipients, so an acknowledgement without it cannot be turned
1895
+ // into anything more useful than a single tick.
1896
+ metadata: JSON.stringify({
1897
+ messageId: frame.data.messageId,
1898
+ groupId: frame.data.groupId,
1899
+ ...(frame.data.memberUserId ? { memberUserId: frame.data.memberUserId } : {}),
1900
+ }),
1863
1901
  });
1864
1902
  return;
1865
1903
  }
@@ -2205,6 +2243,58 @@ class MessagingClient {
2205
2243
  }
2206
2244
  this.log('key_publish_ok', { url, status: response.status, deviceId: myDeviceId });
2207
2245
  }
2246
+ /**
2247
+ * Group messages that arrived while this device had no connection.
2248
+ *
2249
+ * Without this a group message is delivered only to sessions that happen to be
2250
+ * live at that instant and is unreachable afterwards, so anyone closed loses it.
2251
+ * Fetching is also what tells the sender the message reached this member.
2252
+ */
2253
+ async fetchAndProcessOfflineGroupMessages() {
2254
+ const jwt = await this.getValidDropOnAirJwt(false);
2255
+ let page = 0;
2256
+ let totalPages = 1;
2257
+ let totalProcessed = 0;
2258
+ this.log('group_offline_fetch_started', { httpUrl: this.httpUrl, pageSize: 100 });
2259
+ while (page < totalPages) {
2260
+ const response = await this.fetchFn(`${this.httpUrl}/v1/groups/messages/offline?page=${page}&size=100`, {
2261
+ method: 'GET',
2262
+ headers: { Authorization: `Bearer ${jwt}`, 'Content-Type': 'application/json' },
2263
+ });
2264
+ if (!response.ok) {
2265
+ // A relay that predates this endpoint answers 404. That is not an error
2266
+ // worth surfacing: the SDK simply has nothing to catch up on there.
2267
+ if (response.status !== 404) {
2268
+ this.logError('group_offline_fetch_page_failed', { page, status: response.status });
2269
+ }
2270
+ return;
2271
+ }
2272
+ const body = await response.json();
2273
+ totalPages = body.totalPages ?? 0;
2274
+ for (const offline of body.messages) {
2275
+ const notification = {
2276
+ messageId: offline.messageId,
2277
+ groupId: offline.groupId,
2278
+ fromUserId: offline.fromUserId,
2279
+ timestamp: new Date(offline.createdAt).getTime(),
2280
+ encryptionType: offline.encryptionType === 'CLEARTEXT' ? 1 : 0,
2281
+ plaintextPayload: offline.plaintextPayload,
2282
+ senderDeviceId: offline.senderDeviceId,
2283
+ devicePayloads: (offline.devicePayloads ?? []).map(dp => ({
2284
+ deviceId: dp.deviceId,
2285
+ encryptedPayload: (0, bytes_1.fromBase64)(dp.encryptedPayloadBase64),
2286
+ senderPublicKey: (0, bytes_1.fromBase64)(dp.senderPublicKeyBase64),
2287
+ })),
2288
+ };
2289
+ // The same path a live message takes, so decryption, de-duplication and
2290
+ // the callback behave identically.
2291
+ await this.handleIncomingGroupMessage(notification);
2292
+ totalProcessed += 1;
2293
+ }
2294
+ page += 1;
2295
+ }
2296
+ this.log('group_offline_fetch_done', { totalProcessed });
2297
+ }
2208
2298
  async fetchAndProcessOfflineMessages() {
2209
2299
  const jwt = await this.getValidDropOnAirJwt(false);
2210
2300
  let page = 0;
@@ -1,3 +1,46 @@
1
+ /**
2
+ * How a push should behave on the recipient's device.
3
+ *
4
+ * The platform translates this per transport; it does not decide for you. Pick
5
+ * the one that matches your app, not the one that matches ours.
6
+ */
7
+ export type PushDelivery =
8
+ /** Show title and body exactly as supplied. Suits apps sending cleartext. */
9
+ 'alert'
10
+ /**
11
+ * Show them as a placeholder, then replace the text on the device before it
12
+ * appears. This is how an end-to-end encrypted app shows real content on a
13
+ * lock screen without the relay ever seeing it. On iOS it is also the reliable
14
+ * way to be woken, because silent pushes are throttled by the system.
15
+ */
16
+ | 'mutable'
17
+ /** Wake the app, show nothing. Delivery is not guaranteed on iOS. */
18
+ | 'silent'
19
+ /** Call invite. PushKit on iOS, high priority data on Android. */
20
+ | 'voip';
21
+ /**
22
+ * Cleartext metadata that wakes a recipient's device when they have no live
23
+ * connection.
24
+ *
25
+ * Every field here is readable by the relay, which is precisely why it can act on
26
+ * them. Never put message content in `title` or `body`: it would leak through the
27
+ * one path the platform promises never sees it. A placeholder such as
28
+ * "New message" is the point, and `mutable` lets the device replace it.
29
+ */
30
+ export interface PushPayload {
31
+ title?: string;
32
+ body?: string;
33
+ badge?: number;
34
+ sound?: string;
35
+ /** iOS UNNotificationCategory, Android channel id. */
36
+ category?: string;
37
+ /** Groups related notifications together. */
38
+ threadId?: string;
39
+ /** App-defined JSON passed through to the provider payload untouched. */
40
+ customJson?: string;
41
+ /** Defaults to 'alert'. */
42
+ delivery?: PushDelivery;
43
+ }
1
44
  export type DropOnAirEventType = 'SERVER_RECEIVED' | 'DELIVERED' | 'PROCESSED' | 'LIMIT_REACHED' | 'IMPERSONATION_DETECTED' | 'ERROR' | 'CONNECTED' | 'DISCONNECTED' | 'RECONNECTING';
2
45
  export interface DropOnAirEvent {
3
46
  type: DropOnAirEventType | string;
@@ -311,6 +354,14 @@ export interface InitializeOptions {
311
354
  messagingHttpUrl?: string;
312
355
  tokenExchangeEndpoint?: string;
313
356
  keyDirectoryEndpoint?: string;
357
+ /**
358
+ * Attached to every message that does not carry its own.
359
+ *
360
+ * Set this once and push works; the alternative is remembering to pass a
361
+ * payload on every send, where forgetting once means push silently stops with
362
+ * no error to notice. Omit it entirely and no push is ever requested.
363
+ */
364
+ defaultPushPayload?: PushPayload;
314
365
  fetchFn?: typeof fetch;
315
366
  storage?: KeyStorageAdapter;
316
367
  /**
@@ -395,6 +446,8 @@ export interface DropOnAirClient {
395
446
  sendMessage(toUserId: string, plaintextMessage: string, options?: {
396
447
  attachments?: import('../attachment/attachment-types').AttachmentRef[];
397
448
  clientMessageId?: string;
449
+ /** Overrides `defaultPushPayload` for this message. */
450
+ push?: PushPayload;
398
451
  }): Promise<{
399
452
  messageId: string;
400
453
  }>;
@@ -565,6 +618,8 @@ export interface DropOnAirClient {
565
618
  */
566
619
  sendGroupMessage(groupId: string, plaintext: string, memberUserIds: string[], options?: {
567
620
  attachments?: import('../attachment/attachment-types').AttachmentRef[];
621
+ /** Overrides `defaultPushPayload` for this message. */
622
+ push?: PushPayload;
568
623
  }): Promise<{
569
624
  messageId: string;
570
625
  }>;
@@ -22,6 +22,26 @@ export interface WireAttachmentRef {
22
22
  wrappedKeys?: WireDeviceWrappedKey[];
23
23
  thumbnailAttachmentId?: string;
24
24
  }
25
+ /**
26
+ * Cleartext metadata that wakes a recipient's device. Never message content: the
27
+ * relay can read every field here, which is the whole reason it can act on them.
28
+ *
29
+ * `delivery` says how the device should treat the push. The platform translates
30
+ * it per transport rather than deciding for the app.
31
+ */
32
+ export interface WirePushPayload {
33
+ title?: string;
34
+ body?: string;
35
+ badge?: number;
36
+ sound?: string;
37
+ category?: string;
38
+ threadId?: string;
39
+ customJson?: string;
40
+ /** Superseded by `delivery: 3`; still honoured by the server. */
41
+ voip?: boolean;
42
+ /** 0 ALERT, 1 MUTABLE, 2 SILENT, 3 VOIP. */
43
+ delivery?: number;
44
+ }
25
45
  export interface WireEnvelope {
26
46
  messageId: string;
27
47
  appId: string;
@@ -40,6 +60,8 @@ export interface WireEnvelope {
40
60
  plaintextPayload?: string;
41
61
  /** Zero or more attachment pointers (PROTOCOL_VERSION 4+). */
42
62
  attachments?: WireAttachmentRef[];
63
+ /** Wakes the recipient when they have no live session (PROTOCOL_VERSION 5+). */
64
+ pushPayload?: WirePushPayload;
43
65
  }
44
66
  export interface WireAck {
45
67
  messageId: string;
@@ -93,6 +115,8 @@ export interface WireGroupEnvelope {
93
115
  plaintextPayload?: string;
94
116
  memberPayloads: WireGroupMemberPayload[];
95
117
  attachments?: WireAttachmentRef[];
118
+ /** Wakes offline members (PROTOCOL_VERSION 5+). */
119
+ pushPayload?: WirePushPayload;
96
120
  }
97
121
  export interface WireGroupMessageNotification {
98
122
  messageId: string;
@@ -109,6 +133,12 @@ export interface WireGroupAck {
109
133
  messageId: string;
110
134
  groupId: string;
111
135
  type: string;
136
+ /**
137
+ * Which member the acknowledgement is about. A group message has many
138
+ * recipients, so the useful fact is which of them has it. Empty on
139
+ * SERVER_RECEIVED, which concerns the relay rather than any member.
140
+ */
141
+ memberUserId?: string;
112
142
  }
113
143
  export interface WireGroupCallFrame {
114
144
  type: string;
@@ -70,10 +70,29 @@ function buildAttachmentRefType() {
70
70
  .add(new protobuf.Field('wrappedKeys', 7, 'DeviceWrappedKey', 'repeated'))
71
71
  .add(new protobuf.Field('thumbnailAttachmentId', 8, 'string'));
72
72
  }
73
+ // Same rule as AttachmentRef above: Envelope and GroupEnvelope each nest
74
+ // PushPayload, so each needs its own instance. Sharing one would re-parent it
75
+ // onto whichever added it last and orphan it from the other, which is how the
76
+ // AttachmentRef outage happened. The Delivery enum nests inside it and is
77
+ // rebuilt per call for the same reason.
78
+ function buildPushPayloadType() {
79
+ return new protobuf.Type('PushPayload')
80
+ .add(new protobuf.Enum('Delivery', { ALERT: 0, MUTABLE: 1, SILENT: 2, VOIP: 3 }))
81
+ .add(new protobuf.Field('title', 1, 'string'))
82
+ .add(new protobuf.Field('body', 2, 'string'))
83
+ .add(new protobuf.Field('badge', 3, 'int32'))
84
+ .add(new protobuf.Field('sound', 4, 'string'))
85
+ .add(new protobuf.Field('category', 5, 'string'))
86
+ .add(new protobuf.Field('threadId', 6, 'string'))
87
+ .add(new protobuf.Field('customJson', 7, 'string'))
88
+ .add(new protobuf.Field('voip', 8, 'bool'))
89
+ .add(new protobuf.Field('delivery', 9, 'Delivery'));
90
+ }
73
91
  const EnvelopeType = new protobuf.Type('Envelope')
74
92
  .add(EncryptionTypeEnum)
75
93
  .add(DeviceEncryptedPayloadType) // nested type must be added first
76
94
  .add(buildAttachmentRefType())
95
+ .add(buildPushPayloadType())
77
96
  .add(new protobuf.Field('messageId', 1, 'string'))
78
97
  .add(new protobuf.Field('appId', 2, 'string'))
79
98
  .add(new protobuf.Field('fromUserId', 3, 'string'))
@@ -85,7 +104,8 @@ const EnvelopeType = new protobuf.Type('Envelope')
85
104
  .add(new protobuf.Field('senderDeviceId', 9, 'string'))
86
105
  .add(new protobuf.Field('encryptionType', 10, 'EncryptionType'))
87
106
  .add(new protobuf.Field('plaintextPayload', 11, 'string'))
88
- .add(new protobuf.Field('attachments', 12, 'AttachmentRef', 'repeated'));
107
+ .add(new protobuf.Field('attachments', 12, 'AttachmentRef', 'repeated'))
108
+ .add(new protobuf.Field('pushPayload', 13, 'PushPayload'));
89
109
  const AckType = new protobuf.Type('Ack')
90
110
  .add(new protobuf.Field('messageId', 1, 'string'))
91
111
  .add(new protobuf.Field('type', 2, 'string'));
@@ -138,6 +158,7 @@ const GroupEnvelopeType = new protobuf.Type('GroupEnvelope')
138
158
  .add(GroupEnvelopeEncryptionTypeEnum)
139
159
  .add(GroupMemberPayloadType)
140
160
  .add(buildAttachmentRefType())
161
+ .add(buildPushPayloadType())
141
162
  .add(new protobuf.Field('messageId', 1, 'string'))
142
163
  .add(new protobuf.Field('appId', 2, 'string'))
143
164
  .add(new protobuf.Field('groupId', 3, 'string'))
@@ -149,7 +170,8 @@ const GroupEnvelopeType = new protobuf.Type('GroupEnvelope')
149
170
  .add(new protobuf.Field('plaintextPayload', 9, 'string'))
150
171
  .add(new protobuf.Field('memberPayloads', 10, 'GroupMemberPayload', 'repeated'))
151
172
  // field 11 reserved upstream
152
- .add(new protobuf.Field('attachments', 12, 'AttachmentRef', 'repeated'));
173
+ .add(new protobuf.Field('attachments', 12, 'AttachmentRef', 'repeated'))
174
+ .add(new protobuf.Field('pushPayload', 13, 'PushPayload'));
153
175
  const GroupNotifEncryptionTypeEnum = new protobuf.Enum('EncryptionType', { E2EE: 0, CLEARTEXT: 1 });
154
176
  const GroupNotifDeviceType = new protobuf.Type('DeviceEncryptedPayload')
155
177
  .add(new protobuf.Field('deviceId', 1, 'string'))
@@ -171,7 +193,8 @@ const GroupMessageNotificationType = new protobuf.Type('GroupMessageNotification
171
193
  const GroupAckType = new protobuf.Type('GroupAck')
172
194
  .add(new protobuf.Field('messageId', 1, 'string'))
173
195
  .add(new protobuf.Field('groupId', 2, 'string'))
174
- .add(new protobuf.Field('type', 3, 'string'));
196
+ .add(new protobuf.Field('type', 3, 'string'))
197
+ .add(new protobuf.Field('memberUserId', 4, 'string'));
175
198
  const GroupCallFrameType = new protobuf.Type('GroupCallFrame')
176
199
  .add(new protobuf.Field('type', 1, 'string'))
177
200
  .add(new protobuf.Field('callId', 2, 'string'))
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.25.1";
10
+ export declare const SDK_VERSION = "0.27.0";
11
11
  /**
12
12
  * Binary encrypted-payload format version.
13
13
  * Included as the first byte of every encrypted payload so receivers can
@@ -19,4 +19,4 @@ export declare const PAYLOAD_FORMAT_VERSION = 1;
19
19
  * Increment when adding required proto fields or changing frame semantics.
20
20
  * The server advertises its supported range via GET /api/info.
21
21
  */
22
- export declare const PROTOCOL_VERSION = 6;
22
+ export declare const PROTOCOL_VERSION = 7;
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.25.1';
13
+ exports.SDK_VERSION = '0.27.0';
14
14
  /**
15
15
  * Binary encrypted-payload format version.
16
16
  * Included as the first byte of every encrypted payload so receivers can
@@ -22,4 +22,4 @@ exports.PAYLOAD_FORMAT_VERSION = 1;
22
22
  * Increment when adding required proto fields or changing frame semantics.
23
23
  * The server advertises its supported range via GET /api/info.
24
24
  */
25
- exports.PROTOCOL_VERSION = 6;
25
+ exports.PROTOCOL_VERSION = 7;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@droponair/sdk-js",
3
- "version": "0.25.1",
3
+ "version": "0.27.0",
4
4
  "description": "End-to-end encrypted messaging, voice and video calling SDK. The relay never sees your keys or message content.",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",