@droponair/sdk-js 0.25.0 → 0.26.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,50 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.26.0
4
+
5
+ ### Added
6
+
7
+ - **Push payloads on outgoing messages.** `sendMessage` and `sendGroupMessage` accept
8
+ `options.push`, and `defaultPushPayload` on the client config attaches one to every
9
+ message that does not carry its own. Setting the default once is the recommended
10
+ route: passing a payload per send means forgetting it once stops push with no error
11
+ to notice.
12
+ - **`PushDelivery` chooses how a push behaves** on the recipient's device, and the
13
+ platform translates it per transport rather than deciding for you:
14
+ - `alert` shows title and body as supplied. Suits apps sending cleartext.
15
+ - `mutable` shows them as a placeholder and lets your app replace the text before it
16
+ appears, which is how an end-to-end encrypted app puts real content on a lock
17
+ screen without the relay seeing it. On iOS this is also the reliable way to be
18
+ woken, because the system throttles silent pushes.
19
+ - `silent` wakes the app and shows nothing.
20
+ - `voip` is the call invite path, superseding the `voip` boolean, which is still honoured.
21
+
22
+ ### Notes
23
+
24
+ - Push previously could not be triggered at all: no SDK exposed a way to attach a
25
+ payload, and the server only pushes when the sender supplies one.
26
+ - Never put message content in `title` or `body`. Every field is readable by the relay,
27
+ which is why it can act on them, and putting plaintext there leaks it through the one
28
+ path the platform promises never sees it.
29
+ - Additive and backward compatible. Messages sent without a payload behave exactly as
30
+ before, and an absent or unrecognised delivery mode is treated as `alert`.
31
+
32
+ ## 0.25.1
33
+
34
+ ### Changed
35
+
36
+ - Documentation only, no behaviour or API change. Comments and notes that shipped in the
37
+ package described the platform's own internals rather than the integration surface: some
38
+ named components a consumer never talks to, and some carried planning tags that mean nothing
39
+ outside our own backlog. They are rewritten in terms of what the code does and what the
40
+ platform offers.
41
+ - Where availability used to be described as a specific plan flag, it now points at the pricing
42
+ page and the app's subscription, which are the two places that stay correct as plans change.
43
+ - `PROTOCOL_VERSION` references are untouched. That is the contract a consumer integrates
44
+ against and it remains the way to tell what a relay supports.
45
+ - Em dashes are gone from the shipped text, per the house style. Each was judged in place
46
+ rather than swapped mechanically, so clauses read as sentences rather than comma splices.
47
+
3
48
  ## 0.25.0
4
49
 
5
50
  ### Added
@@ -43,7 +88,7 @@
43
88
 
44
89
  ## 0.24.2
45
90
 
46
- - Fix: E2EE group message decryption failed because the decrypt AAD bound `recipientId` to the group id instead of the receiving user's own id (which is what the sender encrypts against). Now matches the sender and the other SDKs. Requires sdk-be that preserves the sender timestamp on group notifications.
91
+ - Fix: E2EE group message decryption failed because the decrypt AAD bound `recipientId` to the group id instead of the receiving user's own id (which is what the sender encrypts against). Now matches the sender and the other SDKs. Requires a relay that preserves the sender timestamp on group notifications.
47
92
 
48
93
  ## 0.24.1
49
94
 
@@ -221,7 +266,7 @@ This project follows [Semantic Versioning](https://semver.org/).
221
266
 
222
267
  ### Changed
223
268
 
224
- - **`revokeAttachment` accepts an `AttachmentRef`.** Calling `revokeAttachment(ref)` now also revokes the linked preview thumbnail (`ref.thumbnailAttachmentId`) in the same call. `revokeAttachment(attachmentId)` with a plain id string is unchanged and revokes only that one attachment. Additive no breaking change.
269
+ - **`revokeAttachment` accepts an `AttachmentRef`.** Calling `revokeAttachment(ref)` now also revokes the linked preview thumbnail (`ref.thumbnailAttachmentId`) in the same call. `revokeAttachment(attachmentId)` with a plain id string is unchanged and revokes only that one attachment. Additive, so no breaking change.
225
270
 
226
271
  ---
227
272
 
@@ -230,13 +275,13 @@ This project follows [Semantic Versioning](https://semver.org/).
230
275
  ### Added
231
276
 
232
277
  - **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.
233
- - **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.
234
- - **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.
278
+ - **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.
279
+ - **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.
235
280
 
236
281
  ### Notes
237
282
 
238
283
  - 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.
239
- - No PROTOCOL_VERSION change additive proto fields (`SyncFrame.fromUserId`, `AttachmentRef.thumbnailAttachmentId`) and an additive event type.
284
+ - No PROTOCOL_VERSION change: additive proto fields (`SyncFrame.fromUserId`, `AttachmentRef.thumbnailAttachmentId`) and an additive event type.
240
285
 
241
286
  ---
242
287
 
@@ -244,14 +289,14 @@ This project follows [Semantic Versioning](https://semver.org/).
244
289
 
245
290
  ### Added
246
291
 
247
- - **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.
292
+ - **Notification clear sync.** `client.clearNotification(conversationId)` tells the user's other devices a conversation's notifications were dismissed; `client.onNotificationCleared(cb)` listens for it. Always available (no opt-out), since it is plain own-device hygiene.
248
293
  - **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.
249
294
  - New exported types: `NotificationClearEvent` / `NotificationClearCallback`, `DraftSyncEvent` / `DraftSyncCallback`.
250
295
 
251
296
  ### Notes
252
297
 
253
298
  - Both ride the existing `SyncFrame` wire type (own-device fan-out only, never delivered to a different user). No PROTOCOL_VERSION change.
254
- - Your app decides when to call `clearNotification()` / `syncDraft()` the platform never infers dismissal or tracks drafts.
299
+ - Your app decides when to call `clearNotification()` / `syncDraft()`. The platform never infers dismissal or tracks drafts.
255
300
 
256
301
  ---
257
302
 
@@ -259,13 +304,13 @@ This project follows [Semantic Versioning](https://semver.org/).
259
304
 
260
305
  ### Added
261
306
 
262
- - **Cross-device read receipts.** New `client.markRead(messageId, conversationId?)` reports a message as read; the receipt is relayed to the user's *other* devices (never to the message's sender). New `client.onReadReceipt(callback)` listens for receipts reported by the user's other devices use it to clear unread state your app maintains.
307
+ - **Cross-device read receipts.** New `client.markRead(messageId, conversationId?)` reports a message as read; the receipt is relayed to the user's *other* devices (never to the message's sender). New `client.onReadReceipt(callback)` listens for receipts reported by the user's other devices. Use it to clear unread state your app maintains.
263
308
  - New exported `ReadReceiptEvent` type and `ReadReceiptCallback`.
264
309
  - **PROTOCOL_VERSION bumped to 6.** Additive only: new `SyncFrame` wire type. Existing 0.10.x clients keep working against the new server.
265
310
 
266
311
  ### Notes
267
312
 
268
- - The platform is deliberately unopinionated: **your app decides when `markRead()` is called** the server never infers read state, and unread counts stay client-side. The app owner can disable read receipts entirely from the dashboard, in which case the server silently drops the frame.
313
+ - The platform is deliberately unopinionated: **your app decides when `markRead()` is called**. The server never infers read state, and unread counts stay client-side. The app owner can disable read receipts entirely from the dashboard, in which case the server silently drops the frame.
269
314
  - This release is own-device sync only: the *sender* of a message is not told when a recipient read it. Sender-side read notifications are a separate future capability.
270
315
 
271
316
  ---
@@ -274,7 +319,7 @@ This project follows [Semantic Versioning](https://semver.org/).
274
319
 
275
320
  ### Added
276
321
 
277
- - **Device trust.** New `client.listMyDevices()` returns the current user's registered devices; `client.revokeMyDevice(deviceId)` revokes one of them. A revoked device's live session is closed immediately and all future connections from it are denied (revoke is permanent re-registering yields a fresh deviceId).
322
+ - **Device trust.** New `client.listMyDevices()` returns the current user's registered devices; `client.revokeMyDevice(deviceId)` revokes one of them. A revoked device's live session is closed immediately and all future connections from it are denied (revoke is permanent, and re-registering yields a fresh deviceId).
278
323
  - **`DEVICE_REVOKED` event.** When this device is revoked (from another of the user's devices, or by the app owner), the SDK stops its reconnect loop, closes the socket, and surfaces a `DEVICE_REVOKED` event through `onEvent` so the app can clear local key storage and show a re-register prompt.
279
324
  - New exported `DeviceInfo` type.
280
325
 
@@ -326,7 +371,7 @@ This project follows [Semantic Versioning](https://semver.org/).
326
371
  ### Notes
327
372
 
328
373
  - Wire protocol unchanged (PROTOCOL_VERSION stays 4). The proto schema was already correct; only the JS hand-rolled codec was out of sync.
329
- - `protoFieldNumbers` for the group types are now documented in `droponair-sdk-shared/test-vectors/protobuf-vectors.json` for cross-platform reference.
374
+ - `protoFieldNumbers` for the group types are now documented alongside the shared protocol test vectors, for cross-platform reference.
330
375
 
331
376
  ---
332
377
 
@@ -395,9 +440,8 @@ This project follows [Semantic Versioning](https://semver.org/).
395
440
  `getDisplayMedia()` (web) / `MediaProjection` (Android) / `ReplayKit` (iOS)
396
441
  in the app, add the track to the existing peer connection, then call
397
442
  `startScreenShare(...)` to notify the peer.
398
- - Plan gate: `screen_sharing` is enabled on PRO/GROWTH/PAYG/ENTERPRISE and
399
- disabled on FREE. Check `subscription.featuresEnabled.screen_sharing` (server
400
- side, on the customer's backend).
443
+ - Availability depends on the app's plan; see the pricing page or the app's
444
+ subscription in the dashboard.
401
445
 
402
446
  ---
403
447
 
@@ -468,7 +512,7 @@ This project follows [Semantic Versioning](https://semver.org/).
468
512
  ### Added
469
513
  - **SDK versioning infrastructure:** `SDK_VERSION`, `PROTOCOL_VERSION`, and `PAYLOAD_FORMAT_VERSION` exported from `src/version.ts`
470
514
  - **Version telemetry:** `X-SDK-Version` header sent on token exchange; `sdkVersion` and `protocolVersion` query params on WebSocket handshake
471
- - **Server info endpoint:** SDK-BE now exposes `GET /api/info` (no auth) returning `protocolVersion`, `minSdkVersion`, and supported `features`
515
+ - **Server info endpoint:** the platform now exposes `GET /api/info` (no auth) returning `protocolVersion`, `minSdkVersion`, and supported `features`
472
516
  - **Multi-device E2EE:** Per-device encrypted payloads (`devicePayloads[]` in Envelope proto), sender encrypts once per recipient device + self-sync to own other devices
473
517
  - **Self-sync:** Sent messages are encrypted for the sender's other devices so all devices see the conversation in real time
474
518
  - **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
@@ -154,7 +154,7 @@ unchanged, so peers still address the same identity and existing conversations k
154
154
 
155
155
  ### Cross-device read receipts
156
156
 
157
- Available since SDK `0.11.0`. **Your app decides when a message is read** the platform never infers it. Call `markRead()` at that moment; the receipt syncs to the user's *other* devices so they can clear their unread UI. It is not sent to the message's sender. The app owner can switch read receipts off entirely from the dashboard.
157
+ Available since SDK `0.11.0`. **Your app decides when a message is read**, and the platform never infers it. Call `markRead()` at that moment; the receipt syncs to the user's *other* devices so they can clear their unread UI. It is not sent to the message's sender. The app owner can switch read receipts off entirely from the dashboard.
158
158
 
159
159
  ```typescript
160
160
  // When your UI decides the message has been read
@@ -162,11 +162,11 @@ client.markRead(messageId, peerUserId);
162
162
 
163
163
  // On the user's other devices
164
164
  client.onReadReceipt(e => {
165
- // e.messageId was read elsewhere clear your unread state for it
165
+ // e.messageId was read elsewhere, so clear your unread state for it
166
166
  });
167
167
  ```
168
168
 
169
- **Group read receipts.** Since SDK `0.13.0`, a group can opt into sharing read receipts with every member. Call `updateGroup(groupId, { readReceiptsVisibleToGroup: true })` (owner/admin); after that, a member's `markRead()` on a group message is broadcast to every other member, and `onReadReceipt` fires with `e.fromUserId` set to the member who read it. It is **opt-in per group** the platform never enables it for you, and it has no effect unless read receipts are also enabled for your app.
169
+ **Group read receipts.** Since SDK `0.13.0`, a group can opt into sharing read receipts with every member. Call `updateGroup(groupId, { readReceiptsVisibleToGroup: true })` (owner/admin); after that, a member's `markRead()` on a group message is broadcast to every other member, and `onReadReceipt` fires with `e.fromUserId` set to the member who read it. It is **opt-in per group**. The platform never enables it for you, and it has no effect unless read receipts are also enabled for your app.
170
170
 
171
171
  ```typescript
172
172
  await client.updateGroup(groupId, { readReceiptsVisibleToGroup: true });
@@ -178,7 +178,7 @@ client.onReadReceipt(e => {
178
178
 
179
179
  ### Notification clear & draft sync
180
180
 
181
- 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.
181
+ Available since SDK `0.12.0`, both own-device only. `clearNotification()` tells your user's other devices a conversation's notifications were dismissed, and is always available. `syncDraft()` pushes a draft so the user can keep typing on another device, and is **opt-in** (the app owner enables it in the dashboard) and the draft text crosses the relay in cleartext.
182
182
 
183
183
  ```typescript
184
184
  client.clearNotification(conversationId);
@@ -190,7 +190,7 @@ client.onDraftSync(e => { /* pre-fill composer with e.draftText */ });
190
190
 
191
191
  ### Device trust
192
192
 
193
- Available since SDK `0.10.0`. Any device that completes a connection is implicitly trusted. `listMyDevices()` powers a "Your devices" screen; `revokeMyDevice()` cuts a device off immediately. When the *current* device is revoked, the SDK stops reconnecting and emits a `DEVICE_REVOKED` event via `onEvent` listen for it to clear local key storage and prompt re-registration.
193
+ Available since SDK `0.10.0`. Any device that completes a connection is implicitly trusted. `listMyDevices()` powers a "Your devices" screen; `revokeMyDevice()` cuts a device off immediately. When the *current* device is revoked, the SDK stops reconnecting and emits a `DEVICE_REVOKED` event via `onEvent`. Listen for it to clear local key storage and prompt re-registration.
194
194
 
195
195
  ```typescript
196
196
  const devices = await client.listMyDevices();
@@ -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
@@ -479,7 +518,7 @@ await wt.connect();
479
518
  await wt.sendEnvelope(envelopeBytes);
480
519
  ```
481
520
 
482
- Uses the browser-native `WebTransport` API; opens a single bidirectional stream and rides raw bytes both ways. Browser support: Chromium 97+ (stable), Firefox 125+ (stable), Safari Technology Preview only, Node 22+ behind experimental flags. The `droponair.com` endpoint advertises `transports: ["webtransport"]` only when the `droponair-webtransport` sidecar is live; check `GET /api/info.transports` and `features.transport_webtransport` to detect availability. Bring your own protobuf decoder. WebSocket remains the default; this primitive is purely opt-in.
521
+ Uses the browser-native `WebTransport` API; opens a single bidirectional stream and rides raw bytes both ways. Browser support: Chromium 97+ (stable), Firefox 125+ (stable), Safari Technology Preview only, Node 22+ behind experimental flags. The endpoint advertises `transports: ["webtransport"]` only where the lane is available; check `GET /api/info.transports` and `features.transport_webtransport` to detect it. Bring your own protobuf decoder. WebSocket remains the default; this primitive is purely opt-in.
483
522
 
484
523
  ### Transport auto-select
485
524
 
@@ -500,7 +539,7 @@ Foundation for an `init({ transport: 'auto' })` shortcut once full `MessagingCli
500
539
 
501
540
  ### Call Recording
502
541
 
503
- Available since SDK `0.16.0`. The SDK **signals** recording state on a group or room call; your app does the actual media capture (`MediaRecorder`) and uploads the file to your own storage the platform never holds the media. The recording signal is broadcast to every participant (including anyone who joins later); that transparency is enforced server-side.
542
+ Available since SDK `0.16.0`. The SDK **signals** recording state on a group or room call; your app does the actual media capture (`MediaRecorder`) and uploads the file to your own storage. The platform never holds the media. The recording signal is broadcast to every participant (including anyone who joins later); that transparency is enforced server-side.
504
543
 
505
544
  | Method | Description |
506
545
  |--------|-------------|
@@ -44,7 +44,7 @@ export declare class AttachmentClient {
44
44
  */
45
45
  prepareAttachmentAndUpload(input: Uint8Array, opts: PrepareAttachmentOptions): Promise<AttachmentRef>;
46
46
  /**
47
- * Revoke an attachment you sent (Phase 2f). After revoke the platform
47
+ * Revoke an attachment you sent. After revoke the platform
48
48
  * refuses to issue further download URLs and recipients with a live
49
49
  * connection get an ATTACHMENT_REVOKED event. Bytes a recipient already
50
50
  * downloaded cannot be recalled.
@@ -147,7 +147,7 @@ class AttachmentClient {
147
147
  };
148
148
  }
149
149
  /**
150
- * Revoke an attachment you sent (Phase 2f). After revoke the platform
150
+ * Revoke an attachment you sent. After revoke the platform
151
151
  * refuses to issue further download URLs and recipients with a live
152
152
  * connection get an ATTACHMENT_REVOKED event. Bytes a recipient already
153
153
  * downloaded cannot be recalled.
@@ -1,6 +1,6 @@
1
1
  import { CryptoService } from '../crypto/crypto-service';
2
2
  import { SessionManager } from './session-manager';
3
- import { BroadcastCallback, CallEventCallback, DropOnAirClient, EventCallback, CreateRoomOptions, GroupCallEventCallback, GroupInfo, GroupMessageCallback, Room, UpdateRoomOptions, SfuToken, SfuRecording, InitializeOptions, MessageCallback, MessageDeleteCallback, MessageEditCallback, TurnCredentials, DeviceInfo, ReadReceiptCallback, NotificationClearCallback, DraftSyncCallback, 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. */
@@ -39,7 +46,7 @@ export declare class MessagingClient implements DropOnAirClient {
39
46
  /** Pending startGroupCall resolver. */
40
47
  private pendingGroupInviteResolve;
41
48
  private pendingGroupInviteReject;
42
- /** Pending joinRoom resolver (Feature 3.1), keyed by roomId. */
49
+ /** Pending joinRoom resolver, keyed by roomId. */
43
50
  private readonly pendingRoomJoins;
44
51
  private reconnectDelayMs;
45
52
  /** Schedule a single reconnect, replacing any pending one (never stack timers). */
@@ -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
  }>;
@@ -238,7 +246,7 @@ export declare class MessagingClient implements DropOnAirClient {
238
246
  getGroup(groupId: string): Promise<GroupInfo>;
239
247
  /**
240
248
  * Update a group. Any omitted field is left unchanged. `readReceiptsVisibleToGroup`
241
- * (Phase 2e) controls whether members' group read receipts are visible to
249
+ * controls whether members' group read receipts are visible to
242
250
  * the whole group or stay own-device only.
243
251
  */
244
252
  updateGroup(groupId: string, update: {
@@ -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
  }>;
@@ -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.
@@ -246,7 +270,7 @@ class MessagingClient {
246
270
  /** Pending startGroupCall resolver. */
247
271
  this.pendingGroupInviteResolve = null;
248
272
  this.pendingGroupInviteReject = null;
249
- /** Pending joinRoom resolver (Feature 3.1), keyed by roomId. */
273
+ /** Pending joinRoom resolver, keyed by roomId. */
250
274
  this.pendingRoomJoins = new Map();
251
275
  this.messageListeners = new Set();
252
276
  this.eventListeners = new Set();
@@ -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 };
@@ -658,7 +685,7 @@ class MessagingClient {
658
685
  this.transport.send(this.codec.encodePushRegistrationFrame(frame));
659
686
  }
660
687
  // ---------------------------------------------------------------------------
661
- // Device trust (PROTOCOL_VERSION 5+ / Phase 2b)
688
+ // Device trust (PROTOCOL_VERSION 5+)
662
689
  // ---------------------------------------------------------------------------
663
690
  /**
664
691
  * List the current user's registered devices. Each device is implicitly
@@ -711,7 +738,7 @@ class MessagingClient {
711
738
  }
712
739
  }
713
740
  // ---------------------------------------------------------------------------
714
- // Cross-device read receipts (PROTOCOL_VERSION 6+ / Phase 2c)
741
+ // Cross-device read receipts (PROTOCOL_VERSION 6+)
715
742
  // ---------------------------------------------------------------------------
716
743
  /**
717
744
  * Mark a message as read. Call this when YOUR app decides a message has
@@ -950,7 +977,7 @@ class MessagingClient {
950
977
  }
951
978
  /**
952
979
  * Update a group. Any omitted field is left unchanged. `readReceiptsVisibleToGroup`
953
- * (Phase 2e) controls whether members' group read receipts are visible to
980
+ * controls whether members' group read receipts are visible to
954
981
  * the whole group or stay own-device only.
955
982
  */
956
983
  async updateGroup(groupId, update) {
@@ -995,7 +1022,7 @@ class MessagingClient {
995
1022
  throw new Error(`deleteGroup failed (HTTP ${res.status})`);
996
1023
  }
997
1024
  // ---------------------------------------------------------------------------
998
- // Rooms (Feature 3.1 - scheduled & persistent meeting rooms)
1025
+ // Rooms (scheduled & persistent meeting rooms)
999
1026
  // ---------------------------------------------------------------------------
1000
1027
  async createRoom(options) {
1001
1028
  const jwt = await this.getValidDropOnAirJwt(false);
@@ -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
  }
@@ -1325,7 +1353,7 @@ class MessagingClient {
1325
1353
  return () => this.groupCallListeners.delete(callback);
1326
1354
  }
1327
1355
  // Group call moderation + waiting room. The server validates host/co-host
1328
- // authority and the `featuresEnabled.moderation` plan flag; on denial it
1356
+ // authority and whether moderation is available on the app's plan; on denial it
1329
1357
  // emits a LIMIT_REACHED event with reason="MODERATION_NOT_ENABLED".
1330
1358
  transferHost(callId, groupId, newHostUserId) {
1331
1359
  this.sendGroupCallFrame({
@@ -1399,7 +1427,7 @@ class MessagingClient {
1399
1427
  payload: JSON.stringify({ userId }),
1400
1428
  });
1401
1429
  }
1402
- // Live stage controls (Feature 3.2). These act on a room call running in
1430
+ // Live stage controls. These act on a room call running in
1403
1431
  // stage mode; pass the callId from joinRoom(). raiseHand / lowerHand and
1404
1432
  // submitStageQuestion are open to any participant; promote / demote are
1405
1433
  // host / co-host authority. Roles surface via GROUP_CALL_ROLE_CHANGED.
@@ -1428,7 +1456,7 @@ class MessagingClient {
1428
1456
  submitStageQuestion(callId, text) {
1429
1457
  this.sendGroupCallFrame({ type: 'GROUP_CALL_STAGE_QUESTION', callId, groupId: '', payload: text });
1430
1458
  }
1431
- // Call recording (Feature 3.5a). These SIGNAL recording state; the actual
1459
+ // Call recording. These SIGNAL recording state; the actual
1432
1460
  // media capture + upload to your storage is your app's job (same split as
1433
1461
  // screen sharing). The platform broadcasts the signal to every participant.
1434
1462
  startRecording(callId) {
@@ -1589,7 +1617,7 @@ class MessagingClient {
1589
1617
  plaintext = await this.cryptoService.decrypt(myPayload.encryptedPayload, sharedKey, {
1590
1618
  messageId: notif.messageId,
1591
1619
  senderId: notif.fromUserId,
1592
- // AAD recipientId is the receiving user's own id this MUST match the
1620
+ // AAD recipientId is the receiving user's own id, and this MUST match the
1593
1621
  // sender's encrypt AAD, which binds recipientId to the target member's
1594
1622
  // userId (memberId). Using notif.groupId here (the previous value) never
1595
1623
  // matched, so E2EE group messages failed GCM auth on decrypt. android /
@@ -1634,7 +1662,7 @@ class MessagingClient {
1634
1662
  this.pendingGroupInviteReject = null;
1635
1663
  reject(new Error(`GROUP_CALL_ALREADY_ACTIVE:${wire.payload ?? ''}`));
1636
1664
  }
1637
- // Resolve / reject a pending joinRoom() (Feature 3.1). Room-call frames
1665
+ // Resolve / reject a pending joinRoom(). Room-call frames
1638
1666
  // carry roomId; the join outcome is one of these four types.
1639
1667
  if (wire.roomId) {
1640
1668
  const pending = this.pendingRoomJoins.get(wire.roomId);
@@ -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;
@@ -117,7 +160,7 @@ export interface GroupInfo {
117
160
  members: GroupMemberInfo[];
118
161
  createdAt: number;
119
162
  /**
120
- * Phase 2e. When true, a member's group read receipt is fanned to all other
163
+ * When true, a member's group read receipt is fanned to all other
121
164
  * members; when false it stays own-device only. Controlled via updateGroup().
122
165
  */
123
166
  readReceiptsVisibleToGroup?: boolean;
@@ -140,7 +183,7 @@ export interface GroupCallEvent {
140
183
  type: GroupCallEventType | string;
141
184
  callId: string;
142
185
  groupId: string;
143
- /** Set instead of groupId when the event belongs to a room call (Feature 3.1). */
186
+ /** Set instead of groupId when the event belongs to a room call. */
144
187
  roomId?: string;
145
188
  targetUserId?: string;
146
189
  payload?: string;
@@ -160,7 +203,7 @@ export interface RoomPolicy {
160
203
  /** When true, the room flips to CLOSED once the last participant leaves the call. */
161
204
  autoCloseWhenEmpty?: boolean;
162
205
  /**
163
- * When true, the room's live call runs in stage mode (Feature 3.2): hosts
206
+ * When true, the room's live call runs in stage mode: hosts
164
207
  * join as speakers, everyone else as receive-only audience who can raise a
165
208
  * hand to be promoted. Mesh-scale - suited to panels and small stages.
166
209
  */
@@ -191,7 +234,7 @@ export interface SfuToken {
191
234
  expiresAt: number;
192
235
  }
193
236
  /**
194
- * A server-side recording of an SFU-mode room (Phase 3.7.5). The platform
237
+ * A server-side recording of an SFU-mode room. The platform
195
238
  * never holds the recorded bytes - LiveKit Egress uploads the finalized file
196
239
  * directly to the destination configured in the panel under `destinationId`.
197
240
  */
@@ -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
  }>;
package/dist/index.js CHANGED
@@ -59,13 +59,13 @@ async function initialize(options) {
59
59
  }
60
60
  return client;
61
61
  }
62
- // Phase 4.2.1: HTTP fallback lane primitive for restrictive networks.
62
+ // HTTP fallback lane primitive for restrictive networks.
63
63
  var sse_transport_1 = require("./transport/sse-transport");
64
64
  Object.defineProperty(exports, "SseTransport", { enumerable: true, get: function () { return sse_transport_1.SseTransport; } });
65
- // Phase 4.3: WebTransport (HTTP/3) lane primitive.
65
+ // WebTransport (HTTP/3) lane primitive.
66
66
  var webtransport_transport_1 = require("./transport/webtransport-transport");
67
67
  Object.defineProperty(exports, "WebTransportTransport", { enumerable: true, get: function () { return webtransport_transport_1.WebTransportTransport; } });
68
- // Phase 4.6: transport auto-select - intersects runtime + platform support.
68
+ // transport auto-select - intersects runtime + platform support.
69
69
  var auto_select_1 = require("./transport/auto-select");
70
70
  Object.defineProperty(exports, "selectTransport", { enumerable: true, get: function () { return auto_select_1.selectTransport; } });
71
71
  /**
@@ -1,5 +1,5 @@
1
1
  /**
2
- * Transport auto-select (Phase 4.6).
2
+ * Transport auto-select.
3
3
  *
4
4
  * Tiny helper that probes which transport lanes the runtime AND the platform
5
5
  * BOTH support, and returns the best one for this client/host pair.
@@ -1,6 +1,6 @@
1
1
  "use strict";
2
2
  /**
3
- * Transport auto-select (Phase 4.6).
3
+ * Transport auto-select.
4
4
  *
5
5
  * Tiny helper that probes which transport lanes the runtime AND the platform
6
6
  * BOTH support, and returns the best one for this client/host pair.
@@ -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;
@@ -116,7 +140,7 @@ export interface WireGroupCallFrame {
116
140
  groupId: string;
117
141
  targetUserId?: string;
118
142
  payload?: string;
119
- /** Set instead of groupId when the multi-party call belongs to a Room (Feature 3.1). */
143
+ /** Set instead of groupId when the multi-party call belongs to a Room. */
120
144
  roomId?: string;
121
145
  }
122
146
  /**
@@ -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'))
@@ -404,7 +426,7 @@ class ProtobufCodec {
404
426
  // Event.type is a SCREAMING_SNAKE_CASE enum string (e.g. LIMIT_REACHED).
405
427
  // A GroupMessageNotification, BroadcastNotification, or Ack decoded here
406
428
  // carries a UUID (messageId / broadcastId) in field 1, which never matches
407
- // this shape so this reliably rejects those overlapping frame types and
429
+ // this shape, so this reliably rejects those overlapping frame types and
408
430
  // lets Event be probed before them.
409
431
  if (!decoded.type || !/^[A-Z][A-Z0-9_]*$/.test(decoded.type)) {
410
432
  return null;
@@ -1,5 +1,5 @@
1
1
  /**
2
- * SSE-lane primitive (Phase 4.2.1 v1).
2
+ * SSE-lane primitive.
3
3
  *
4
4
  * Standalone client for the platform's HTTP fallback transport
5
5
  * (`GET /v1/transport/stream` for receive, `POST /v1/transport/send` for
@@ -1,6 +1,6 @@
1
1
  "use strict";
2
2
  /**
3
- * SSE-lane primitive (Phase 4.2.1 v1).
3
+ * SSE-lane primitive.
4
4
  *
5
5
  * Standalone client for the platform's HTTP fallback transport
6
6
  * (`GET /v1/transport/stream` for receive, `POST /v1/transport/send` for
@@ -1,8 +1,8 @@
1
1
  /**
2
- * WebTransport (HTTP/3) lane primitive (Phase 4.3).
2
+ * WebTransport (HTTP/3) lane primitive.
3
3
  *
4
- * Standalone client for the platform's WebTransport endpoint, terminated by
5
- * the `droponair-webtransport` Go sidecar and bridged into sdk-be `/ws`.
4
+ * Standalone client for the platform's WebTransport endpoint, which carries
5
+ * the same session as the WebSocket lane.
6
6
  * Browser-native `WebTransport` API; no polyfill ships with the SDK.
7
7
  *
8
8
  * Browser support as of 2026-05:
@@ -1,9 +1,9 @@
1
1
  "use strict";
2
2
  /**
3
- * WebTransport (HTTP/3) lane primitive (Phase 4.3).
3
+ * WebTransport (HTTP/3) lane primitive.
4
4
  *
5
- * Standalone client for the platform's WebTransport endpoint, terminated by
6
- * the `droponair-webtransport` Go sidecar and bridged into sdk-be `/ws`.
5
+ * Standalone client for the platform's WebTransport endpoint, which carries
6
+ * the same session as the WebSocket lane.
7
7
  * Browser-native `WebTransport` API; no polyfill ships with the SDK.
8
8
  *
9
9
  * Browser support as of 2026-05:
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.0";
10
+ export declare const SDK_VERSION = "0.26.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.25.0';
13
+ exports.SDK_VERSION = '0.26.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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@droponair/sdk-js",
3
- "version": "0.25.0",
3
+ "version": "0.26.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",