@droponair/sdk-js 0.6.0 → 0.7.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,6 +6,42 @@ This project follows [Semantic Versioning](https://semver.org/).
6
6
 
7
7
  ---
8
8
 
9
+ ## [0.7.0], 2026-05-19
10
+
11
+ ### Added
12
+ - **Conference moderation and waiting room.** Host designated to the call
13
+ initiator and transferable mid-call; host can appoint co-hosts. Host and
14
+ co-host can mute and remove participants and admit / reject users from a
15
+ waiting room. If the host leaves without transferring, the server
16
+ auto-promotes the longest-joined remaining participant.
17
+ - New `client` methods:
18
+ - `transferHost(callId, groupId, newHostUserId)`
19
+ - `appointCoHost(callId, groupId, userId)` / `revokeCoHost(callId, groupId, userId)`
20
+ - `muteParticipant(callId, groupId, targetUserId)` (signaling hint; the
21
+ target SDK applies the mute locally)
22
+ - `removeParticipant(callId, groupId, targetUserId)` (server-enforced; the
23
+ target is dropped from the call session)
24
+ - `requestWaitingRoomEntry(callId, groupId)` (would-be joiner side)
25
+ - `admitFromWaitingRoom(callId, groupId, userId)` / `rejectFromWaitingRoom(callId, groupId, userId)`
26
+ - New `GroupCallEventType` values: `'GROUP_CALL_HOST_TRANSFER'`,
27
+ `'GROUP_CALL_COHOST_APPOINT'`, `'GROUP_CALL_COHOST_REVOKE'`,
28
+ `'GROUP_CALL_ROLE_CHANGED'`, `'GROUP_CALL_MUTE_PARTICIPANT'`,
29
+ `'GROUP_CALL_REMOVE_PARTICIPANT'`, `'GROUP_CALL_PARTICIPANT_REMOVED'`,
30
+ `'GROUP_CALL_WAITING_ROOM_REQUEST'`, `'GROUP_CALL_WAITING_ROOM_JOINED'`,
31
+ `'GROUP_CALL_WAITING_ROOM_ADMIT'`, `'GROUP_CALL_WAITING_ROOM_ADMITTED'`,
32
+ `'GROUP_CALL_WAITING_ROOM_REJECT'`, `'GROUP_CALL_WAITING_ROOM_REJECTED'`.
33
+ - `GET /api/info` now advertises `"moderation"` in the `features` array.
34
+
35
+ ### Notes
36
+ - Wire-level additive: legacy 0.6.x clients ignore the new signal type
37
+ strings (string discriminators on the existing `GroupCallFrame.type`).
38
+ No proto schema change, no `PROTOCOL_VERSION` bump.
39
+ - Availability depends on your plan. See the
40
+ [pricing page](https://www.droponair.com/pricing) and your dashboard
41
+ Subscription page for what's enabled on your app.
42
+
43
+ ---
44
+
9
45
  ## [0.6.0], 2026-05-18
10
46
 
11
47
  ### Added
package/README.md CHANGED
@@ -160,7 +160,7 @@ client.onMessage(async (msg) => {
160
160
  | `sendMessage(toUserId, text, { attachments })` | `Promise<{ messageId }>` | Send with attachments |
161
161
 
162
162
  - E2EE: a random AES-256-GCM file key encrypts the bytes; the file key is wrapped per recipient device using X25519 + HKDF (same model as message payloads). Server never sees the unwrapped file key.
163
- - Plan limits: `maxAttachmentSizeMb` per file + `maxAttachmentsPerMonth`. Enforced server-side before the presigned URL is issued.
163
+ - Availability and per-file / per-month limits depend on your plan and are enforced server-side before the presigned URL is issued. See the [pricing page](https://www.droponair.com/pricing) and your dashboard Subscription page for what's enabled on your app.
164
164
  - Upload URL TTL = 15 min. Download URL TTL = 5 min. Download authorization checks that the requester is the original sender or in the captured recipient list.
165
165
 
166
166
  ### Broadcast Channels
@@ -252,7 +252,46 @@ client.onGroupCallEvent((evt) => {
252
252
 
253
253
  - The server enforces **one concurrent sharer per group call**. If another participant already holds the slot when you call `startGroupScreenShare`, the SDK delivers a `GROUP_CALL_SCREEN_SHARE_STOPPED` event with the current holder's userId in `payload`, so you can roll back the optimistic UI.
254
254
  - When a sharer leaves the call, the server broadcasts a STOPPED event to remaining participants before they see `PARTICIPANT_LEFT`.
255
- - Plan gate: `screen_sharing` is on `PRO`/`GROWTH`/`PAYG`/`ENTERPRISE`, off on `FREE`. Read `subscription.featuresEnabled.screen_sharing` on your backend to decide whether to expose the share button.
255
+ - Availability depends on your plan. See the [pricing page](https://www.droponair.com/pricing) and your dashboard Subscription page for what's enabled on your app.
256
+
257
+ ### Conference moderation and waiting room
258
+
259
+ Available since SDK `0.7.0`. The call initiator is the default host; host can transfer the role mid-call or appoint co-hosts. Host and co-host can mute, remove, and gate joiners through a waiting room. If the host leaves without transferring, the server auto-promotes the longest-joined remaining participant and broadcasts `GROUP_CALL_ROLE_CHANGED`.
260
+
261
+ ```typescript
262
+ // Host actions
263
+ client.transferHost(callId, groupId, 'alice');
264
+ client.appointCoHost(callId, groupId, 'bob');
265
+ client.muteParticipant(callId, groupId, 'eve'); // signaling hint
266
+ client.removeParticipant(callId, groupId, 'mallory'); // server-enforced drop
267
+
268
+ // Waiting room
269
+ client.requestWaitingRoomEntry(callId, groupId); // would-be joiner side
270
+ client.admitFromWaitingRoom(callId, groupId, 'newbie'); // host / co-host
271
+ client.rejectFromWaitingRoom(callId, groupId, 'troll');
272
+
273
+ client.onGroupCallEvent((evt) => {
274
+ if (evt.type === 'GROUP_CALL_ROLE_CHANGED') updateHostUi(evt.payload); // {"userId":"...","role":"HOST|CO_HOST|PARTICIPANT"}
275
+ if (evt.type === 'GROUP_CALL_WAITING_ROOM_JOINED') showWaitingApprovalPrompt(evt.payload);
276
+ if (evt.type === 'GROUP_CALL_WAITING_ROOM_ADMITTED') joinTheCall(evt.callId, evt.groupId);
277
+ if (evt.type === 'GROUP_CALL_MUTE_PARTICIPANT') muteMyMicLocally();
278
+ if (evt.type === 'GROUP_CALL_PARTICIPANT_REMOVED') exitCallUi();
279
+ });
280
+ ```
281
+
282
+ | Method | Returns | Description |
283
+ |--------|---------|-------------|
284
+ | `transferHost(callId, groupId, newHostUserId)` | `void` | Host only |
285
+ | `appointCoHost(callId, groupId, userId)` | `void` | Host only |
286
+ | `revokeCoHost(callId, groupId, userId)` | `void` | Host only |
287
+ | `muteParticipant(callId, groupId, targetUserId)` | `void` | Host / co-host. Signaling hint - the target SDK mutes the local mic. |
288
+ | `removeParticipant(callId, groupId, targetUserId)` | `void` | Host / co-host. Server drops the target from the call session. |
289
+ | `requestWaitingRoomEntry(callId, groupId)` | `void` | Would-be joiner |
290
+ | `admitFromWaitingRoom(callId, groupId, userId)` | `void` | Host / co-host |
291
+ | `rejectFromWaitingRoom(callId, groupId, userId)` | `void` | Host / co-host |
292
+
293
+ - Mute is intentionally a signaling hint: WebRTC media tracks can't be muted from outside the producing client. Remove is server-enforced - the target SDK receives `GROUP_CALL_PARTICIPANT_REMOVED` and the others receive `PARTICIPANT_LEFT`.
294
+ - Availability depends on your plan. See the [pricing page](https://www.droponair.com/pricing) and your dashboard Subscription page for what's enabled on your app.
256
295
 
257
296
  ## Security
258
297
 
@@ -21,8 +21,8 @@ interface AttachmentClientDeps {
21
21
  logError: (stage: string, data?: Record<string, unknown>) => void;
22
22
  }
23
23
  /**
24
- * SDK-side coordinator for phase1/attachments. Handles:
25
- * - createUploadSession: reserves an attachmentId + presigned PUT URL via sdk-be.
24
+ * SDK-side coordinator for attachments. Handles:
25
+ * - createUploadSession: reserves an attachmentId + presigned PUT URL.
26
26
  * - uploadBytes: PUTs bytes directly to the customer's bucket.
27
27
  * - finalize: confirms upload + commits sha256.
28
28
  * - prepareAttachmentAndUpload: convenience wrapper that does upload session,
@@ -20,8 +20,8 @@ async function sha256Hex(bytes) {
20
20
  return hex.join('');
21
21
  }
22
22
  /**
23
- * SDK-side coordinator for phase1/attachments. Handles:
24
- * - createUploadSession: reserves an attachmentId + presigned PUT URL via sdk-be.
23
+ * SDK-side coordinator for attachments. Handles:
24
+ * - createUploadSession: reserves an attachmentId + presigned PUT URL.
25
25
  * - uploadBytes: PUTs bytes directly to the customer's bucket.
26
26
  * - finalize: confirms upload + commits sha256.
27
27
  * - prepareAttachmentAndUpload: convenience wrapper that does upload session,
@@ -4,7 +4,7 @@
4
4
  //
5
5
  // DropOnAir never holds file bytes. The SDK uploads/downloads directly
6
6
  // against the customer's storage bucket via short-lived presigned URLs
7
- // minted by sdk-be. For E2EE messages a per-attachment AES-256-GCM file
7
+ // minted by the DropOnAir platform. For E2EE messages a per-attachment AES-256-GCM file
8
8
  // key is wrapped per recipient device using the same X25519 + HKDF path
9
9
  // used for message payloads.
10
10
  // ---------------------------------------------------------------------------
@@ -139,6 +139,14 @@ export declare class MessagingClient implements DropOnAirClient {
139
139
  startGroupScreenShare(callId: string, groupId: string, payload?: string): void;
140
140
  stopGroupScreenShare(callId: string, groupId: string, payload?: string): void;
141
141
  onGroupCallEvent(callback: GroupCallEventCallback): () => void;
142
+ transferHost(callId: string, groupId: string, newHostUserId: string): void;
143
+ appointCoHost(callId: string, groupId: string, userId: string): void;
144
+ revokeCoHost(callId: string, groupId: string, userId: string): void;
145
+ muteParticipant(callId: string, groupId: string, targetUserId: string): void;
146
+ removeParticipant(callId: string, groupId: string, targetUserId: string): void;
147
+ requestWaitingRoomEntry(callId: string, groupId: string): void;
148
+ admitFromWaitingRoom(callId: string, groupId: string, userId: string): void;
149
+ rejectFromWaitingRoom(callId: string, groupId: string, userId: string): void;
142
150
  private sendGroupCallFrame;
143
151
  /**
144
152
  * Initiate an outgoing call.
@@ -254,7 +254,7 @@ class MessagingClient {
254
254
  logError: (stage, data) => this.logError(stage, data),
255
255
  });
256
256
  }
257
- // Attachment API (phase1/attachments, PROTOCOL_VERSION 4+)
257
+ // Attachment API (PROTOCOL_VERSION 4+)
258
258
  async prepareAttachmentAndUpload(bytes, options) {
259
259
  return this.attachmentClient.prepareAttachmentAndUpload(bytes, options);
260
260
  }
@@ -762,6 +762,81 @@ class MessagingClient {
762
762
  this.groupCallListeners.add(callback);
763
763
  return () => this.groupCallListeners.delete(callback);
764
764
  }
765
+ // Group call moderation + waiting room. The server validates host/co-host
766
+ // authority and the `featuresEnabled.moderation` plan flag; on denial it
767
+ // emits a LIMIT_REACHED event with reason="MODERATION_NOT_ENABLED".
768
+ transferHost(callId, groupId, newHostUserId) {
769
+ this.sendGroupCallFrame({
770
+ type: 'GROUP_CALL_HOST_TRANSFER',
771
+ callId,
772
+ groupId,
773
+ targetUserId: '',
774
+ payload: JSON.stringify({ userId: newHostUserId }),
775
+ });
776
+ }
777
+ appointCoHost(callId, groupId, userId) {
778
+ this.sendGroupCallFrame({
779
+ type: 'GROUP_CALL_COHOST_APPOINT',
780
+ callId,
781
+ groupId,
782
+ targetUserId: '',
783
+ payload: JSON.stringify({ userId }),
784
+ });
785
+ }
786
+ revokeCoHost(callId, groupId, userId) {
787
+ this.sendGroupCallFrame({
788
+ type: 'GROUP_CALL_COHOST_REVOKE',
789
+ callId,
790
+ groupId,
791
+ targetUserId: '',
792
+ payload: JSON.stringify({ userId }),
793
+ });
794
+ }
795
+ muteParticipant(callId, groupId, targetUserId) {
796
+ this.sendGroupCallFrame({
797
+ type: 'GROUP_CALL_MUTE_PARTICIPANT',
798
+ callId,
799
+ groupId,
800
+ targetUserId: '',
801
+ payload: JSON.stringify({ targetUserId }),
802
+ });
803
+ }
804
+ removeParticipant(callId, groupId, targetUserId) {
805
+ this.sendGroupCallFrame({
806
+ type: 'GROUP_CALL_REMOVE_PARTICIPANT',
807
+ callId,
808
+ groupId,
809
+ targetUserId: '',
810
+ payload: JSON.stringify({ targetUserId }),
811
+ });
812
+ }
813
+ requestWaitingRoomEntry(callId, groupId) {
814
+ this.sendGroupCallFrame({
815
+ type: 'GROUP_CALL_WAITING_ROOM_REQUEST',
816
+ callId,
817
+ groupId,
818
+ targetUserId: '',
819
+ payload: '',
820
+ });
821
+ }
822
+ admitFromWaitingRoom(callId, groupId, userId) {
823
+ this.sendGroupCallFrame({
824
+ type: 'GROUP_CALL_WAITING_ROOM_ADMIT',
825
+ callId,
826
+ groupId,
827
+ targetUserId: '',
828
+ payload: JSON.stringify({ userId }),
829
+ });
830
+ }
831
+ rejectFromWaitingRoom(callId, groupId, userId) {
832
+ this.sendGroupCallFrame({
833
+ type: 'GROUP_CALL_WAITING_ROOM_REJECT',
834
+ callId,
835
+ groupId,
836
+ targetUserId: '',
837
+ payload: JSON.stringify({ userId }),
838
+ });
839
+ }
765
840
  sendGroupCallFrame(frame) {
766
841
  if (!this.ws || this.ws.readyState !== WebSocket.OPEN) {
767
842
  throw new Error('DropOnAir websocket is not connected');
@@ -11,7 +11,7 @@ export interface DecryptedMessage {
11
11
  timestamp: number;
12
12
  plaintext: string;
13
13
  /**
14
- * Attachment pointers (phase1/attachments, PROTOCOL_VERSION 4+).
14
+ * Attachment pointers (PROTOCOL_VERSION 4+).
15
15
  * Empty/undefined for messages with no attachments. To fetch and decrypt the
16
16
  * bytes, call {@code client.downloadAttachment(ref)}.
17
17
  */
@@ -92,7 +92,7 @@ export interface DecryptedGroupMessage {
92
92
  plaintext: string;
93
93
  }
94
94
  export type GroupMessageCallback = (message: DecryptedGroupMessage) => void;
95
- export type GroupCallEventType = 'GROUP_CALL_INVITE' | 'GROUP_CALL_RINGING' | 'GROUP_CALL_JOIN' | 'GROUP_CALL_LEAVE' | 'GROUP_CALL_END' | 'GROUP_CALL_ENDED' | 'GROUP_CALL_PARTICIPANT_JOINED' | 'GROUP_CALL_PARTICIPANT_LEFT' | 'GROUP_CALL_ALREADY_ACTIVE' | 'GROUP_CALL_SDP_OFFER' | 'GROUP_CALL_SDP_ANSWER' | 'GROUP_CALL_ICE_CANDIDATE' | 'GROUP_CALL_SCREEN_SHARE_STARTED' | 'GROUP_CALL_SCREEN_SHARE_STOPPED';
95
+ export type GroupCallEventType = 'GROUP_CALL_INVITE' | 'GROUP_CALL_RINGING' | 'GROUP_CALL_JOIN' | 'GROUP_CALL_LEAVE' | 'GROUP_CALL_END' | 'GROUP_CALL_ENDED' | 'GROUP_CALL_PARTICIPANT_JOINED' | 'GROUP_CALL_PARTICIPANT_LEFT' | 'GROUP_CALL_PARTICIPANT_REMOVED' | 'GROUP_CALL_ALREADY_ACTIVE' | 'GROUP_CALL_SDP_OFFER' | 'GROUP_CALL_SDP_ANSWER' | 'GROUP_CALL_ICE_CANDIDATE' | 'GROUP_CALL_SCREEN_SHARE_STARTED' | 'GROUP_CALL_SCREEN_SHARE_STOPPED' | 'GROUP_CALL_HOST_TRANSFER' | 'GROUP_CALL_COHOST_APPOINT' | 'GROUP_CALL_COHOST_REVOKE' | 'GROUP_CALL_ROLE_CHANGED' | 'GROUP_CALL_MUTE_PARTICIPANT' | 'GROUP_CALL_REMOVE_PARTICIPANT' | 'GROUP_CALL_WAITING_ROOM_REQUEST' | 'GROUP_CALL_WAITING_ROOM_JOINED' | 'GROUP_CALL_WAITING_ROOM_ADMIT' | 'GROUP_CALL_WAITING_ROOM_ADMITTED' | 'GROUP_CALL_WAITING_ROOM_REJECT' | 'GROUP_CALL_WAITING_ROOM_REJECTED';
96
96
  export interface GroupCallEvent {
97
97
  type: GroupCallEventType | string;
98
98
  callId: string;
@@ -214,7 +214,7 @@ export interface DropOnAirClient {
214
214
  sendCallSignal(type: 'CALL_SDP_OFFER' | 'CALL_SDP_ANSWER' | 'CALL_ICE_CANDIDATE', callId: string, payload: string): void;
215
215
  /**
216
216
  * Signal that this user started sharing their screen in a 1:1 call
217
- * (phase1/screen-sharing, PROTOCOL_VERSION 4+). The actual screen MediaStreamTrack
217
+ * (PROTOCOL_VERSION 4+). The actual screen MediaStreamTrack
218
218
  * is added to the existing peer connection by the application; the SDK only
219
219
  * broadcasts the start/stop notification so the remote SDK can update its UI.
220
220
  * Optional `payload` (JSON string) can carry track-id or app-specific metadata.
@@ -266,4 +266,20 @@ export interface DropOnAirClient {
266
266
  stopGroupScreenShare(callId: string, groupId: string, payload?: string): void;
267
267
  /** Register a listener for group call events. Returns an unsubscribe function. */
268
268
  onGroupCallEvent(callback: GroupCallEventCallback): () => void;
269
+ /** Transfer host to another participant (current host only). */
270
+ transferHost(callId: string, groupId: string, newHostUserId: string): void;
271
+ /** Appoint a participant as co-host (host only). */
272
+ appointCoHost(callId: string, groupId: string, userId: string): void;
273
+ /** Revoke a participant's co-host status (host only). */
274
+ revokeCoHost(callId: string, groupId: string, userId: string): void;
275
+ /** Mute a participant's mic (host / co-host). Signaling hint; the target SDK applies it locally. */
276
+ muteParticipant(callId: string, groupId: string, targetUserId: string): void;
277
+ /** Remove a participant from the call (host / co-host). Server-enforced. */
278
+ removeParticipant(callId: string, groupId: string, targetUserId: string): void;
279
+ /** Request entry into a call's waiting room. Sent by would-be joiners; host receives WAITING_ROOM_JOINED. */
280
+ requestWaitingRoomEntry(callId: string, groupId: string): void;
281
+ /** Admit a user from the waiting room (host / co-host). */
282
+ admitFromWaitingRoom(callId: string, groupId: string, userId: string): void;
283
+ /** Reject a user waiting to join (host / co-host). */
284
+ rejectFromWaitingRoom(callId: string, groupId: string, userId: string): void;
269
285
  }
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.6.0";
10
+ export declare const SDK_VERSION = "0.7.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.6.0';
13
+ exports.SDK_VERSION = '0.7.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.6.0",
3
+ "version": "0.7.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",