@droponair/sdk-js 0.26.0 → 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,28 @@
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
+
3
26
  ## 0.26.0
4
27
 
5
28
  ### Added
package/README.md CHANGED
@@ -359,6 +359,17 @@ client.onMessage(async (msg) => {
359
359
  | `sendCleartextGroupMessage(groupId, plaintext)` | `Promise<{ messageId }>` | Send a plaintext (non-encrypted) group message; server fans out to every other member. |
360
360
  | `onGroupMessage(callback)` | `() => void` | Listen for group messages |
361
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
+
362
373
  ### 1-to-1 Calls
363
374
 
364
375
  | Method | Returns | Description |
@@ -374,6 +374,14 @@ export declare class MessagingClient implements DropOnAirClient {
374
374
  */
375
375
  private fetchMyOtherDeviceKeys;
376
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;
377
385
  private fetchAndProcessOfflineMessages;
378
386
  private getValidDropOnAirJwt;
379
387
  private extractSubject;
@@ -1779,6 +1779,9 @@ class MessagingClient {
1779
1779
  if (this.dropOnAirJwt) {
1780
1780
  this.scheduleProactiveTokenRefresh(this.dropOnAirJwt);
1781
1781
  }
1782
+ this.fetchAndProcessOfflineGroupMessages().catch((err) => {
1783
+ this.logError('group_offline_fetch_failed', { error: String(err) });
1784
+ });
1782
1785
  this.fetchAndProcessOfflineMessages().catch((err) => {
1783
1786
  this.logError('offline_fetch_failed', { error: String(err?.message ?? err) });
1784
1787
  this.emitEvent({ type: 'ERROR', reason: 'OFFLINE_FETCH_FAILED' });
@@ -1887,7 +1890,14 @@ class MessagingClient {
1887
1890
  if (frame.kind === 'groupAck') {
1888
1891
  this.emitEvent({
1889
1892
  type: frame.data.type,
1890
- 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
+ }),
1891
1901
  });
1892
1902
  return;
1893
1903
  }
@@ -2233,6 +2243,58 @@ class MessagingClient {
2233
2243
  }
2234
2244
  this.log('key_publish_ok', { url, status: response.status, deviceId: myDeviceId });
2235
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
+ }
2236
2298
  async fetchAndProcessOfflineMessages() {
2237
2299
  const jwt = await this.getValidDropOnAirJwt(false);
2238
2300
  let page = 0;
@@ -133,6 +133,12 @@ export interface WireGroupAck {
133
133
  messageId: string;
134
134
  groupId: string;
135
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;
136
142
  }
137
143
  export interface WireGroupCallFrame {
138
144
  type: string;
@@ -193,7 +193,8 @@ const GroupMessageNotificationType = new protobuf.Type('GroupMessageNotification
193
193
  const GroupAckType = new protobuf.Type('GroupAck')
194
194
  .add(new protobuf.Field('messageId', 1, 'string'))
195
195
  .add(new protobuf.Field('groupId', 2, 'string'))
196
- .add(new protobuf.Field('type', 3, 'string'));
196
+ .add(new protobuf.Field('type', 3, 'string'))
197
+ .add(new protobuf.Field('memberUserId', 4, 'string'));
197
198
  const GroupCallFrameType = new protobuf.Type('GroupCallFrame')
198
199
  .add(new protobuf.Field('type', 1, 'string'))
199
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.26.0";
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.26.0';
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.26.0",
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",