@droponair/sdk-js 0.22.0 → 0.23.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
@@ -6,12 +6,28 @@ This project follows [Semantic Versioning](https://semver.org/).
6
6
 
7
7
  ---
8
8
 
9
+ ## [0.23.0], 2026-06-01
10
+
11
+ ### Added
12
+
13
+ - **Idempotent retries via `sendMessage(..., { clientMessageId })`.** Pass a stable id to reuse across retries of the same logical message. The relay now dedups by `(appId, fromUserId, messageId)`, so re-sending after a lost ack or a reconnect delivers the message at most once instead of creating duplicates. Omit it for new messages and a fresh id is generated (unchanged default behavior). Requires relay support shipped alongside this release; wire format and `PROTOCOL_VERSION` are unchanged (the `clientMessageId` field already existed).
14
+
15
+ ---
16
+
17
+ ## [0.22.1], 2026-05-31
18
+
19
+ ### Fixed
20
+
21
+ - **Message send/receive broken when attachments support is compiled in.** The protobuf reflection tree shared a single `AttachmentRef` type instance across `Envelope`, `GroupEnvelope`, and `GroupMessageNotification`. Because a protobufjs reflection object can have only one parent, adding it to a later message re-parented it and removed it from `Envelope`, so encoding any 1:1 message threw `no such Type or Enum 'AttachmentRef' in Type Envelope` before a frame was ever produced. Each message now builds its own `AttachmentRef` (and nested `DeviceWrappedKey` / `EncryptionType`) instance. Wire format is unchanged, this is purely a client-side construction fix. Also resolves the same latent failure on `GroupMessageNotification` decode.
22
+
23
+ ---
24
+
9
25
  ## [0.22.0], 2026-05-25
10
26
 
11
27
  ### Added
12
28
 
13
29
  - **MessagingClient transport selection.** `initialize({ transport: 'ws' | 'sse' | 'wt' | 'auto' })` lets your app choose the signaling lane at init time. Default stays `'ws'`, so existing integrations are byte-identical. `'auto'` queries `GET /api/info.transports` and picks the best available lane from what the runtime supports. WebSocket close-code semantics (1008 JWT-expiry surfacing, 4001 JWT-refresh-on-event) are preserved on the WebSocket path; SSE/WT synthesize close events.
14
- - **MQTT (IoT) broadcast lane (4.5b).** Broadcasts now mirror to MQTT topic `droponair/v1/broadcast/{appId}/{channelId}` on `mqtts://mqtt.droponair.com:8883`. For IoT subscribers that consume your broadcasts without our full SDK. Subscribe-only, per-app credentials, TLS only. Visible via `GET /api/info` (`transports` includes `"mqtt"`, `features` includes `"transport_mqtt"`). No SDK API change, the lane is consumed directly by MQTT clients.
30
+ - **MQTT (IoT) broadcast lane.** Broadcasts now mirror to MQTT topic `droponair/v1/broadcast/{appId}/{channelId}` on `mqtts://mqtt.droponair.com:8883`. For IoT subscribers that consume your broadcasts without our full SDK. Subscribe-only, per-app credentials, TLS only. Visible via `GET /api/info` (`transports` includes `"mqtt"`, `features` includes `"transport_mqtt"`). No SDK API change, the lane is consumed directly by MQTT clients.
15
31
 
16
32
  ### Changed
17
33
 
@@ -23,11 +39,11 @@ This project follows [Semantic Versioning](https://semver.org/).
23
39
 
24
40
  ### Added
25
41
 
26
- - **Transport auto-select helper (Phase 4.6).** New `selectTransport({ httpUrl })` reads `/api/info.transports`, intersects with the runtime's `WebTransport`/`WebSocket`/`EventSource` availability, and returns the best lane (preference: WebTransport -> WebSocket -> SSE; overridable via `preference`). Caller then constructs the corresponding primitive. Pluggable `fetchFn` for Node + tests.
42
+ - **Transport auto-select helper.** New `selectTransport({ httpUrl })` reads `/api/info.transports`, intersects with the runtime's `WebTransport`/`WebSocket`/`EventSource` availability, and returns the best lane (preference: WebTransport -> WebSocket -> SSE; overridable via `preference`). Caller then constructs the corresponding primitive. Pluggable `fetchFn` for Node + tests.
27
43
 
28
44
  ### Notes
29
45
 
30
- - Foundation for an `init({ transport: 'auto' })` shortcut once full `MessagingClient` integration lands (4.2.1b). Today it's a small standalone helper so existing customers can adopt the pattern without waiting.
46
+ - Foundation for an `init({ transport: 'auto' })` shortcut once full `MessagingClient` integration lands in a follow-up release. Today it's a small standalone helper so existing customers can adopt the pattern without waiting.
31
47
 
32
48
  ---
33
49
 
@@ -35,12 +51,12 @@ This project follows [Semantic Versioning](https://semver.org/).
35
51
 
36
52
  ### Added
37
53
 
38
- - **WebTransport (HTTP/3) transport primitive (Phase 4.3).** New `WebTransportTransport` class uses the browser-native `WebTransport` API to ride the platform's `/v1/transport/wt` HTTP/3 endpoint, terminated by the `droponair-webtransport` sidecar. Same shape as `SseTransport`: `onFrame`, `sendEnvelope`, `connect`, `close`. Caller brings own protobuf decoder. Browser-only for v1 (Chromium 97+, Firefox 125+, Safari TP-only); Node 22+ has experimental support behind flags.
54
+ - **WebTransport (HTTP/3) transport primitive.** New `WebTransportTransport` class uses the browser-native `WebTransport` API to ride the platform's `/v1/transport/wt` HTTP/3 endpoint,. Same shape as `SseTransport`: `onFrame`, `sendEnvelope`, `connect`, `close`. Caller brings own protobuf decoder. Browser-only for v1 (Chromium 97+, Firefox 125+, Safari TP-only); Node 22+ has experimental support behind flags.
39
55
 
40
56
  ### Notes
41
57
 
42
58
  - WebTransport gives multi-stream perf wins over WebSocket on flaky cellular and similar networks. WebSocket on `/ws` remains the default; this primitive is opt-in for environments that have native WebTransport.
43
- - The droponair.com endpoint only advertises `transports: ["webtransport"]` once the `droponair-webtransport` sidecar is up and `app.transport.webtransport-enabled` is flipped on sdk-be. Check `GET /api/info.transports` for the live list.
59
+ - The droponair.com endpoint only advertises `transports: ["webtransport"]` once the HTTP/3 lane is enabled and `app.transport.webtransport-enabled` is flipped on the platform. Check `GET /api/info.transports` for the live list.
44
60
 
45
61
  ---
46
62
 
@@ -48,11 +64,11 @@ This project follows [Semantic Versioning](https://semver.org/).
48
64
 
49
65
  ### Added
50
66
 
51
- - **SSE transport primitive (Phase 4.2.1).** New `SseTransport` class exposes the platform's HTTP-fallback lane (`/v1/transport/stream` + `/v1/transport/send`) as a small standalone client, for environments where WebSocket upgrades are blocked by corporate firewalls but HTTPS GET/POST work. EventSource-based receive (auto-reconnects), `fetch` POST for send. Bring your own protobuf decoder (use the codec already exported from the SDK).
67
+ - **SSE transport primitive.** New `SseTransport` class exposes the platform's HTTP-fallback lane (`/v1/transport/stream` + `/v1/transport/send`) as a small standalone client, for environments where WebSocket upgrades are blocked by corporate firewalls but HTTPS GET/POST work. EventSource-based receive (auto-reconnects), `fetch` POST for send. Bring your own protobuf decoder (use the codec already exported from the SDK).
52
68
 
53
69
  ### Notes
54
70
 
55
- - v1 is a primitive; full `init({ transport: 'sse' })` integration into `MessagingClient` lands in 4.2.1b. Customers who need the SSE lane today can use `SseTransport` directly alongside the existing WS client.
71
+ - v1 is a primitive; full `init({ transport: 'sse' })` integration into `MessagingClient` lands in a follow-up release. Customers who need the SSE lane today can use `SseTransport` directly alongside the existing WS client.
56
72
  - WebSocket on `/ws` remains the default for all SDK calls; this addition is purely opt-in. Older code paths and existing apps are unaffected.
57
73
 
58
74
  ---
@@ -61,7 +77,7 @@ This project follows [Semantic Versioning](https://semver.org/).
61
77
 
62
78
  ### Added
63
79
 
64
- - **SFU recording (Phase 3.7.5).** Server-side recording for SFU-mode rooms with `mediaEncryption: 'SFU'`. The platform's media server records the composite of every track in the room and uploads the finalized file directly to the customer's storage bucket - the platform never holds the recorded bytes. Storage destinations (S3 / GCS / Azure) are registered once in the panel; the SDK references each by an opaque `destinationId`.
80
+ - **SFU recording.** Server-side recording for SFU-mode rooms with `mediaEncryption: 'SFU'`. The platform's media server records the composite of every track in the room and uploads the finalized file directly to the customer's storage bucket - the platform never holds the recorded bytes. Storage destinations (S3 / GCS / Azure) are registered once in the panel; the SDK references each by an opaque `destinationId`.
65
81
  - Three new client methods:
66
82
  - `startSfuRecording(roomId, destinationId)` -> `SfuRecording`
67
83
  - `stopSfuRecording(roomId, recordingId)` -> `SfuRecording`
@@ -326,7 +342,6 @@ This project follows [Semantic Versioning](https://semver.org/).
326
342
  - Wire-level additive: legacy 0.4.x clients ignore the new `Envelope.attachments` field (proto3 forwards-compat). Existing apps continue to work unchanged.
327
343
  - v1 storage adapters supported by the server: S3-compatible (S3, R2, MinIO, B2, Wasabi), Google Cloud Storage, Azure Blob.
328
344
  - Plan limits: `maxAttachmentSizeMb` per file + `maxAttachmentsPerMonth`. FREE = attachments disabled. PRO = 25 MB / 1000 per month. GROWTH = 100 MB / 10000. PAYG = 250 MB / metered. ENTERPRISE = custom.
329
- - Requires sdk-be `0.5.0` server.
330
345
 
331
346
  ---
332
347
 
@@ -342,7 +357,6 @@ This project follows [Semantic Versioning](https://semver.org/).
342
357
  ### Notes
343
358
  - Each edit and each `FOR_EVERYONE` delete counts as **one** MESSAGE usage record (subject to plan quotas + rate limits).
344
359
  - Wire-level additive: legacy 0.3.x clients ignore unknown frame types (proto3 forwards-compat), so existing TwinFlame and CrowdSyncer apps continue to work unchanged.
345
- - Requires sdk-be `0.4.0` server.
346
360
 
347
361
  ---
348
362
 
package/README.md CHANGED
@@ -183,7 +183,7 @@ E2EE invariant: the push body is sender-supplied cleartext metadata only (e.g. "
183
183
 
184
184
  ### Message Edit & Delete
185
185
 
186
- Available since SDK `0.4.0` and requires sdk-be `0.4.0`.
186
+ Available since SDK `0.4.0`.
187
187
 
188
188
  ```typescript
189
189
  // Edit an encrypted message
@@ -213,7 +213,7 @@ client.onMessageDelete((del) => {
213
213
 
214
214
  ### Attachments (Customer-Managed Storage)
215
215
 
216
- Available since SDK `0.5.0` and requires sdk-be `0.5.0`. Configure your bucket once in the DropOnAir panel (S3-compatible, GCS, or Azure Blob). DropOnAir never holds file bytes - the SDK uploads/downloads directly against your bucket via short-lived presigned URLs.
216
+ Available since SDK `0.5.0`. Configure your bucket once in the DropOnAir panel (S3-compatible, GCS, or Azure Blob). DropOnAir never holds file bytes - the SDK uploads/downloads directly against your bucket via short-lived presigned URLs.
217
217
 
218
218
  ```typescript
219
219
  // Convenience: encrypt + upload + finalize in one call, then send with the message.
@@ -101,6 +101,7 @@ export declare class MessagingClient implements DropOnAirClient {
101
101
  disconnect(): void;
102
102
  sendMessage(toUserId: string, plaintextMessage: string, options?: {
103
103
  attachments?: AttachmentRef[];
104
+ clientMessageId?: string;
104
105
  }): Promise<{
105
106
  messageId: string;
106
107
  }>;
@@ -337,7 +337,10 @@ class MessagingClient {
337
337
  throw new Error('JWT_EXPIRED: Secure messaging session is reconnecting; please retry in a moment');
338
338
  }
339
339
  const myDeviceId = this.deviceId ?? await this.getOrCreateDeviceId();
340
- const messageId = crypto.randomUUID();
340
+ // Reuse a caller-supplied id for idempotent retries: the relay dedups by
341
+ // (appId, fromUserId, messageId), so re-sending with the same id is a no-op
342
+ // delivery instead of a duplicate. Defaults to a fresh id for new messages.
343
+ const messageId = options?.clientMessageId ?? crypto.randomUUID();
341
344
  const timestamp = Date.now();
342
345
  const myIdentity = await this.cryptoService.getOrCreateIdentity();
343
346
  const myPublicKeyBytes = (0, bytes_1.fromBase64)(myIdentity.publicKey);
@@ -304,9 +304,16 @@ export interface DropOnAirClient {
304
304
  /**
305
305
  * Send a 1:1 E2EE message. Optionally attach one or more attachments
306
306
  * prepared via {@link prepareAttachmentAndUpload} or {@link createUploadSession}.
307
+ *
308
+ * Pass `options.clientMessageId` to reuse a stable id across retries: the relay
309
+ * dedups by `(appId, fromUserId, messageId)`, so re-sending a message with the
310
+ * same id delivers it at most once instead of creating a duplicate. Omit it for
311
+ * new messages and a fresh id is generated. The chosen id is returned as
312
+ * `messageId`.
307
313
  */
308
314
  sendMessage(toUserId: string, plaintextMessage: string, options?: {
309
315
  attachments?: import('../attachment/attachment-types').AttachmentRef[];
316
+ clientMessageId?: string;
310
317
  }): Promise<{
311
318
  messageId: string;
312
319
  }>;
@@ -42,29 +42,38 @@ const DeviceEncryptedPayloadType = new protobuf.Type('DeviceEncryptedPayload')
42
42
  .add(new protobuf.Field('deviceId', 1, 'string'))
43
43
  .add(new protobuf.Field('encryptedPayload', 2, 'bytes'))
44
44
  .add(new protobuf.Field('senderPublicKey', 3, 'bytes'));
45
- // Per-device wrapped file key for E2EE attachments (PROTOCOL_VERSION 4+)
46
- const DeviceWrappedKeyType = new protobuf.Type('DeviceWrappedKey')
47
- .add(new protobuf.Field('deviceId', 1, 'string'))
48
- .add(new protobuf.Field('wrappedKey', 2, 'bytes'))
49
- .add(new protobuf.Field('senderPublicKey', 3, 'bytes'))
50
- .add(new protobuf.Field('nonce', 4, 'bytes'));
51
- // Reference to an out-of-band attachment in customer-managed storage.
52
- const AttachmentRefEncTypeEnum = new protobuf.Enum('EncryptionType', { E2EE: 0, CLEARTEXT: 1 });
53
- const AttachmentRefType = new protobuf.Type('AttachmentRef')
54
- .add(AttachmentRefEncTypeEnum)
55
- .add(DeviceWrappedKeyType)
56
- .add(new protobuf.Field('attachmentId', 1, 'string'))
57
- .add(new protobuf.Field('storageHint', 2, 'string'))
58
- .add(new protobuf.Field('mimeType', 3, 'string'))
59
- .add(new protobuf.Field('sizeBytes', 4, 'int64'))
60
- .add(new protobuf.Field('sha256', 5, 'string'))
61
- .add(new protobuf.Field('encryptionType', 6, 'EncryptionType'))
62
- .add(new protobuf.Field('wrappedKeys', 7, 'DeviceWrappedKey', 'repeated'))
63
- .add(new protobuf.Field('thumbnailAttachmentId', 8, 'string'));
45
+ // Reference to an out-of-band attachment in customer-managed storage, plus its
46
+ // nested per-device wrapped file key (PROTOCOL_VERSION 4+).
47
+ //
48
+ // A protobufjs reflection object can have only ONE parent: calling `.add(obj)`
49
+ // on a second message re-parents `obj` and silently removes it from the first.
50
+ // Envelope, GroupEnvelope and GroupMessageNotification each nest AttachmentRef,
51
+ // so each needs its OWN instance, built fresh here. Sharing a single instance
52
+ // across them re-parented it onto the last adder and orphaned it from Envelope,
53
+ // breaking every 1:1 send with "no such Type or Enum 'AttachmentRef' in Type
54
+ // Envelope". The EncryptionType enum and DeviceWrappedKey type are nested inside
55
+ // each AttachmentRef and so are rebuilt per call for the same reason.
56
+ function buildAttachmentRefType() {
57
+ return new protobuf.Type('AttachmentRef')
58
+ .add(new protobuf.Enum('EncryptionType', { E2EE: 0, CLEARTEXT: 1 }))
59
+ .add(new protobuf.Type('DeviceWrappedKey')
60
+ .add(new protobuf.Field('deviceId', 1, 'string'))
61
+ .add(new protobuf.Field('wrappedKey', 2, 'bytes'))
62
+ .add(new protobuf.Field('senderPublicKey', 3, 'bytes'))
63
+ .add(new protobuf.Field('nonce', 4, 'bytes')))
64
+ .add(new protobuf.Field('attachmentId', 1, 'string'))
65
+ .add(new protobuf.Field('storageHint', 2, 'string'))
66
+ .add(new protobuf.Field('mimeType', 3, 'string'))
67
+ .add(new protobuf.Field('sizeBytes', 4, 'int64'))
68
+ .add(new protobuf.Field('sha256', 5, 'string'))
69
+ .add(new protobuf.Field('encryptionType', 6, 'EncryptionType'))
70
+ .add(new protobuf.Field('wrappedKeys', 7, 'DeviceWrappedKey', 'repeated'))
71
+ .add(new protobuf.Field('thumbnailAttachmentId', 8, 'string'));
72
+ }
64
73
  const EnvelopeType = new protobuf.Type('Envelope')
65
74
  .add(EncryptionTypeEnum)
66
75
  .add(DeviceEncryptedPayloadType) // nested type must be added first
67
- .add(AttachmentRefType)
76
+ .add(buildAttachmentRefType())
68
77
  .add(new protobuf.Field('messageId', 1, 'string'))
69
78
  .add(new protobuf.Field('appId', 2, 'string'))
70
79
  .add(new protobuf.Field('fromUserId', 3, 'string'))
@@ -128,7 +137,7 @@ const GroupEnvelopeEncryptionTypeEnum = new protobuf.Enum('EncryptionType', { E2
128
137
  const GroupEnvelopeType = new protobuf.Type('GroupEnvelope')
129
138
  .add(GroupEnvelopeEncryptionTypeEnum)
130
139
  .add(GroupMemberPayloadType)
131
- .add(AttachmentRefType)
140
+ .add(buildAttachmentRefType())
132
141
  .add(new protobuf.Field('messageId', 1, 'string'))
133
142
  .add(new protobuf.Field('appId', 2, 'string'))
134
143
  .add(new protobuf.Field('groupId', 3, 'string'))
@@ -149,6 +158,7 @@ const GroupNotifDeviceType = new protobuf.Type('DeviceEncryptedPayload')
149
158
  const GroupMessageNotificationType = new protobuf.Type('GroupMessageNotification')
150
159
  .add(GroupNotifEncryptionTypeEnum)
151
160
  .add(GroupNotifDeviceType)
161
+ .add(buildAttachmentRefType())
152
162
  .add(new protobuf.Field('messageId', 1, 'string'))
153
163
  .add(new protobuf.Field('groupId', 2, 'string'))
154
164
  .add(new protobuf.Field('fromUserId', 3, '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.22.0";
10
+ export declare const SDK_VERSION = "0.23.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.22.0';
13
+ exports.SDK_VERSION = '0.23.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.22.0",
3
+ "version": "0.23.0",
4
4
  "description": "DropOnAir SDK for end-to-end encrypted messaging",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",