@droponair/sdk-js 0.13.1 → 0.15.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,33 @@ This project follows [Semantic Versioning](https://semver.org/).
6
6
 
7
7
  ---
8
8
 
9
+ ## [0.15.0], 2026-05-21
10
+
11
+ ### Added
12
+
13
+ - **Live stage roles & audience controls.** A room created with `policy.stageMode` runs its live call as a stage: hosts join as speakers, everyone else joins as receive-only audience. New `raiseHand` / `lowerHand` (any participant), `promoteToSpeaker` / `demoteToAudience` (host / co-host), and `submitStageQuestion` - all act on the callId from `joinRoom`. Role changes surface via `GROUP_CALL_ROLE_CHANGED` events (role `SPEAKER` / `AUDIENCE`); the room-call participant list now carries each participant's role.
14
+ - New `GROUP_CALL_STAGE_*` event types.
15
+
16
+ ### Notes
17
+
18
+ - Stage mode is a roles + signaling layer, the media topology is still the WebRTC mesh, suited to panels and small/medium stages. Large-audience streaming needs an SFU and is out of scope. Pure control-plane signaling; no proto change.
19
+
20
+ ---
21
+
22
+ ## [0.14.0], 2026-05-21
23
+
24
+ ### Added
25
+
26
+ - **Scheduled & persistent rooms.** New `createRoom` / `listRooms` / `getRoom` / `updateRoom` / `deleteRoom` REST methods and `joinRoom(roomId)` / `leaveRoom(callId)`. A room is an addressable container a live multi-party call runs inside - it is not tied to a group's member roster. Per-room `policy` knobs (`waitingRoom`, `requireHost`, `maxParticipants`, `autoCloseWhenEmpty`) and optional `scheduledStartAt` / `scheduledEndAt` metadata let you compose scheduled or persistent meetings; the platform imposes no meeting model.
27
+ - `joinRoom` resolves with a callId that works with the existing group-call signaling methods (`sendGroupCallSignal`, screen-share, moderation, `leaveGroupCall`) - pass an empty string for `groupId`. Room-call events arrive via `onGroupCallEvent` and carry `roomId`.
28
+ - New `Room`, `RoomPolicy`, `CreateRoomOptions`, `UpdateRoomOptions` types.
29
+
30
+ ### Notes
31
+
32
+ - Additive only. The live room call reuses the group-call mesh; on the wire it is the existing `GroupCallFrame` with the new optional `roomId` field. No PROTOCOL_VERSION change.
33
+
34
+ ---
35
+
9
36
  ## [0.13.1], 2026-05-21
10
37
 
11
38
  ### Changed
package/README.md CHANGED
@@ -299,6 +299,69 @@ client.onMessage(async (msg) => {
299
299
  | `sendGroupCallSignal(type, callId, groupId, targetUserId, payload)` | `void` | Send SDP/ICE to a peer |
300
300
  | `onGroupCallEvent(callback)` | `() => void` | Listen for group call events |
301
301
 
302
+ ### Scheduled & Persistent Rooms
303
+
304
+ Available since SDK `0.14.0`. A **room** is an addressable container a live multi-party call runs inside - unlike a group, it is not tied to a member roster. Anyone in your app with the room id can attempt to join; the room's policy is the gate. "Scheduled" vs "persistent" is just how you compose the knobs - the platform imposes no meeting model.
305
+
306
+ | Method | Returns | Description |
307
+ |--------|---------|-------------|
308
+ | `createRoom(options?)` | `Promise<Room>` | Create a room (name, schedule, policy, hosts) |
309
+ | `listRooms()` | `Promise<Room[]>` | Rooms you created or host |
310
+ | `getRoom(roomId)` | `Promise<Room>` | One room, including live-call state |
311
+ | `updateRoom(roomId, update)` | `Promise<Room>` | Update name/schedule/policy/hosts (creator only) |
312
+ | `deleteRoom(roomId)` | `Promise<void>` | Delete a room (creator only) |
313
+ | `joinRoom(roomId)` | `Promise<string>` | Join the room's live call, returns callId |
314
+ | `leaveRoom(callId)` | `Promise<void>` | Leave the room's live call |
315
+
316
+ Room policy (`RoomPolicy`, all optional): `waitingRoom` (non-hosts wait for host admit), `requireHost` (non-hosts cannot open the call), `maxParticipants` (per-room cap), `autoCloseWhenEmpty` (room flips to `CLOSED` when the last participant leaves).
317
+
318
+ ```typescript
319
+ const room = await client.createRoom({
320
+ name: 'Weekly sync',
321
+ scheduledStartAt: Date.now() + 3_600_000, // metadata - your app drives the countdown UX
322
+ policy: { waitingRoom: true, requireHost: true },
323
+ });
324
+
325
+ // Join the live call. The returned callId works with every group-call
326
+ // signaling method - pass '' for groupId.
327
+ const callId = await client.joinRoom(room.roomId);
328
+ client.onGroupCallEvent((e) => {
329
+ // room-call events carry e.roomId; e.type GROUP_CALL_PARTICIPANT_JOINED, etc.
330
+ });
331
+ client.sendGroupCallSignal('GROUP_CALL_SDP_OFFER', callId, '', peerUserId, sdp);
332
+ ```
333
+
334
+ `joinRoom` rejects with `HOST_REQUIRED`, `ROOM_CLOSED`, or `WAITING_ROOM_PENDING` (listen for `GROUP_CALL_WAITING_ROOM_ADMITTED`, then call `joinRoom` again).
335
+
336
+ ### Live Stage
337
+
338
+ Available since SDK `0.15.0`. Create a room with `policy.stageMode` and its live call runs as a stage: hosts join as **speakers**, everyone else joins as receive-only **audience**. Audience can raise a hand; a host promotes them to speaker. The media topology is still the WebRTC mesh, so a stage is suited to panels and small/medium audiences - large-audience streaming needs an SFU and is out of scope.
339
+
340
+ | Method | Description |
341
+ |--------|-------------|
342
+ | `raiseHand(callId)` | Request promotion to speaker (any participant) |
343
+ | `lowerHand(callId)` | Lower your raised hand |
344
+ | `promoteToSpeaker(callId, userId)` | Promote an audience member (host / co-host) |
345
+ | `demoteToAudience(callId, userId)` | Demote a speaker (host / co-host) |
346
+ | `submitStageQuestion(callId, text)` | Submit a text question to the stage's speakers |
347
+
348
+ ```typescript
349
+ const room = await client.createRoom({ name: 'AMA', policy: { stageMode: true, requireHost: true } });
350
+ const callId = await client.joinRoom(room.roomId);
351
+
352
+ // Audience side
353
+ client.raiseHand(callId);
354
+ client.submitStageQuestion(callId, 'How does E2EE key rotation work?');
355
+
356
+ // Host side - role changes arrive as GROUP_CALL_ROLE_CHANGED events
357
+ client.onGroupCallEvent((e) => {
358
+ if (e.type === 'GROUP_CALL_STAGE_HAND_RAISED') client.promoteToSpeaker(callId, e.payload!);
359
+ if (e.type === 'GROUP_CALL_ROLE_CHANGED') updateStageUi(e.payload); // {"userId","role":"SPEAKER"|"AUDIENCE"}
360
+ });
361
+ ```
362
+
363
+ Audience members are receive-only by convention: on `GROUP_CALL_ROLE_CHANGED` to `AUDIENCE`, your app simply does not publish a local media track. The platform signals the role; your app owns the WebRTC tracks.
364
+
302
365
  ### Screen Sharing
303
366
 
304
367
  Available since SDK `0.6.0`. The SDK only signals start/stop - capture (via the browser's `getDisplayMedia()`) and adding the resulting `MediaStreamTrack` to the existing peer connection are the app's responsibility.
@@ -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, GroupCallEventCallback, GroupInfo, GroupMessageCallback, InitializeOptions, MessageCallback, MessageDeleteCallback, MessageEditCallback, TurnCredentials, DeviceInfo, ReadReceiptCallback, NotificationClearCallback, DraftSyncCallback } from './types';
3
+ import { BroadcastCallback, CallEventCallback, DropOnAirClient, EventCallback, CreateRoomOptions, GroupCallEventCallback, GroupInfo, GroupMessageCallback, Room, UpdateRoomOptions, InitializeOptions, MessageCallback, MessageDeleteCallback, MessageEditCallback, TurnCredentials, DeviceInfo, ReadReceiptCallback, NotificationClearCallback, DraftSyncCallback } 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;
@@ -37,6 +37,8 @@ export declare class MessagingClient implements DropOnAirClient {
37
37
  /** Pending startGroupCall resolver. */
38
38
  private pendingGroupInviteResolve;
39
39
  private pendingGroupInviteReject;
40
+ /** Pending joinRoom resolver (Feature 3.1), keyed by roomId. */
41
+ private readonly pendingRoomJoins;
40
42
  private reconnectDelayMs;
41
43
  /**
42
44
  * Register a document visibilitychange listener so that when the iOS app
@@ -226,6 +228,22 @@ export declare class MessagingClient implements DropOnAirClient {
226
228
  addGroupMembers(groupId: string, userIds: string[]): Promise<GroupInfo>;
227
229
  removeGroupMember(groupId: string, userId: string): Promise<GroupInfo>;
228
230
  deleteGroup(groupId: string): Promise<void>;
231
+ createRoom(options?: CreateRoomOptions): Promise<Room>;
232
+ listRooms(): Promise<Room[]>;
233
+ getRoom(roomId: string): Promise<Room>;
234
+ updateRoom(roomId: string, update: UpdateRoomOptions): Promise<Room>;
235
+ deleteRoom(roomId: string): Promise<void>;
236
+ /**
237
+ * Join the live call in a room. Resolves with the callId once joined; that
238
+ * callId works with the existing group-call signaling methods (pass an empty
239
+ * string for groupId). Rejects with an Error whose message is the reason when
240
+ * the room is closed (`ROOM_CLOSED`), a host is required but absent
241
+ * (`HOST_REQUIRED`), or the joiner was placed in the waiting room
242
+ * (`WAITING_ROOM_PENDING` - listen for GROUP_CALL_WAITING_ROOM_ADMITTED, then
243
+ * call joinRoom again).
244
+ */
245
+ joinRoom(roomId: string): Promise<string>;
246
+ leaveRoom(callId: string): Promise<void>;
229
247
  /**
230
248
  * Send an end-to-end encrypted group message. Mirrors the Android and iOS
231
249
  * SDKs: the SDK encrypts the plaintext per recipient device (sender-side
@@ -271,6 +289,11 @@ export declare class MessagingClient implements DropOnAirClient {
271
289
  requestWaitingRoomEntry(callId: string, groupId: string): void;
272
290
  admitFromWaitingRoom(callId: string, groupId: string, userId: string): void;
273
291
  rejectFromWaitingRoom(callId: string, groupId: string, userId: string): void;
292
+ raiseHand(callId: string): void;
293
+ lowerHand(callId: string): void;
294
+ promoteToSpeaker(callId: string, userId: string): void;
295
+ demoteToAudience(callId: string, userId: string): void;
296
+ submitStageQuestion(callId: string, text: string): void;
274
297
  private sendGroupCallFrame;
275
298
  /**
276
299
  * Initiate an outgoing call.
@@ -216,6 +216,8 @@ class MessagingClient {
216
216
  /** Pending startGroupCall resolver. */
217
217
  this.pendingGroupInviteResolve = null;
218
218
  this.pendingGroupInviteReject = null;
219
+ /** Pending joinRoom resolver (Feature 3.1), keyed by roomId. */
220
+ this.pendingRoomJoins = new Map();
219
221
  this.messageListeners = new Set();
220
222
  this.eventListeners = new Set();
221
223
  this.broadcastListeners = new Set();
@@ -937,6 +939,112 @@ class MessagingClient {
937
939
  throw new Error(`deleteGroup failed (HTTP ${res.status})`);
938
940
  }
939
941
  // ---------------------------------------------------------------------------
942
+ // Rooms (Feature 3.1 - scheduled & persistent meeting rooms)
943
+ // ---------------------------------------------------------------------------
944
+ async createRoom(options) {
945
+ const jwt = await this.getValidDropOnAirJwt(false);
946
+ const body = {};
947
+ if (options?.name !== undefined)
948
+ body.name = options.name;
949
+ if (options?.scheduledStartAt !== undefined) {
950
+ body.scheduledStartAt = new Date(options.scheduledStartAt).toISOString();
951
+ }
952
+ if (options?.scheduledEndAt !== undefined) {
953
+ body.scheduledEndAt = new Date(options.scheduledEndAt).toISOString();
954
+ }
955
+ if (options?.policy !== undefined)
956
+ body.policy = options.policy;
957
+ if (options?.hostUserIds !== undefined)
958
+ body.hostUserIds = options.hostUserIds;
959
+ const res = await this.fetchFn(`${this.httpUrl}/api/rooms`, {
960
+ method: 'POST',
961
+ headers: { Authorization: `Bearer ${jwt}`, 'Content-Type': 'application/json' },
962
+ body: JSON.stringify(body),
963
+ });
964
+ if (!res.ok)
965
+ throw new Error(`createRoom failed (HTTP ${res.status})`);
966
+ return res.json();
967
+ }
968
+ async listRooms() {
969
+ const jwt = await this.getValidDropOnAirJwt(false);
970
+ const res = await this.fetchFn(`${this.httpUrl}/api/rooms`, {
971
+ method: 'GET',
972
+ headers: { Authorization: `Bearer ${jwt}` },
973
+ });
974
+ if (!res.ok)
975
+ throw new Error(`listRooms failed (HTTP ${res.status})`);
976
+ return res.json();
977
+ }
978
+ async getRoom(roomId) {
979
+ const jwt = await this.getValidDropOnAirJwt(false);
980
+ const res = await this.fetchFn(`${this.httpUrl}/api/rooms/${encodeURIComponent(roomId)}`, {
981
+ method: 'GET',
982
+ headers: { Authorization: `Bearer ${jwt}` },
983
+ });
984
+ if (!res.ok)
985
+ throw new Error(`getRoom failed (HTTP ${res.status})`);
986
+ return res.json();
987
+ }
988
+ async updateRoom(roomId, update) {
989
+ const jwt = await this.getValidDropOnAirJwt(false);
990
+ const body = {};
991
+ if (update.name !== undefined)
992
+ body.name = update.name;
993
+ if (update.scheduledStartAt !== undefined) {
994
+ body.scheduledStartAt = new Date(update.scheduledStartAt).toISOString();
995
+ }
996
+ if (update.scheduledEndAt !== undefined) {
997
+ body.scheduledEndAt = new Date(update.scheduledEndAt).toISOString();
998
+ }
999
+ if (update.policy !== undefined)
1000
+ body.policy = update.policy;
1001
+ if (update.hostUserIds !== undefined)
1002
+ body.hostUserIds = update.hostUserIds;
1003
+ const res = await this.fetchFn(`${this.httpUrl}/api/rooms/${encodeURIComponent(roomId)}`, {
1004
+ method: 'PATCH',
1005
+ headers: { Authorization: `Bearer ${jwt}`, 'Content-Type': 'application/json' },
1006
+ body: JSON.stringify(body),
1007
+ });
1008
+ if (!res.ok)
1009
+ throw new Error(`updateRoom failed (HTTP ${res.status})`);
1010
+ return res.json();
1011
+ }
1012
+ async deleteRoom(roomId) {
1013
+ const jwt = await this.getValidDropOnAirJwt(false);
1014
+ const res = await this.fetchFn(`${this.httpUrl}/api/rooms/${encodeURIComponent(roomId)}`, {
1015
+ method: 'DELETE',
1016
+ headers: { Authorization: `Bearer ${jwt}` },
1017
+ });
1018
+ if (!res.ok)
1019
+ throw new Error(`deleteRoom failed (HTTP ${res.status})`);
1020
+ }
1021
+ /**
1022
+ * Join the live call in a room. Resolves with the callId once joined; that
1023
+ * callId works with the existing group-call signaling methods (pass an empty
1024
+ * string for groupId). Rejects with an Error whose message is the reason when
1025
+ * the room is closed (`ROOM_CLOSED`), a host is required but absent
1026
+ * (`HOST_REQUIRED`), or the joiner was placed in the waiting room
1027
+ * (`WAITING_ROOM_PENDING` - listen for GROUP_CALL_WAITING_ROOM_ADMITTED, then
1028
+ * call joinRoom again).
1029
+ */
1030
+ joinRoom(roomId) {
1031
+ if (!this.ws || this.ws.readyState !== WebSocket.OPEN) {
1032
+ return Promise.reject(new Error('DropOnAir websocket is not connected'));
1033
+ }
1034
+ return new Promise((resolve, reject) => {
1035
+ this.pendingRoomJoins.set(roomId, { resolve, reject });
1036
+ this.ws.send(this.codec.encodeGroupCallFrame({
1037
+ type: 'GROUP_CALL_JOIN',
1038
+ callId: '',
1039
+ groupId: '',
1040
+ roomId,
1041
+ }));
1042
+ });
1043
+ }
1044
+ async leaveRoom(callId) {
1045
+ this.sendGroupCallFrame({ type: 'GROUP_CALL_LEAVE', callId, groupId: '' });
1046
+ }
1047
+ // ---------------------------------------------------------------------------
940
1048
  // Group messaging
941
1049
  // ---------------------------------------------------------------------------
942
1050
  /**
@@ -1199,6 +1307,35 @@ class MessagingClient {
1199
1307
  payload: JSON.stringify({ userId }),
1200
1308
  });
1201
1309
  }
1310
+ // Live stage controls (Feature 3.2). These act on a room call running in
1311
+ // stage mode; pass the callId from joinRoom(). raiseHand / lowerHand and
1312
+ // submitStageQuestion are open to any participant; promote / demote are
1313
+ // host / co-host authority. Roles surface via GROUP_CALL_ROLE_CHANGED.
1314
+ raiseHand(callId) {
1315
+ this.sendGroupCallFrame({ type: 'GROUP_CALL_STAGE_HAND_RAISED', callId, groupId: '' });
1316
+ }
1317
+ lowerHand(callId) {
1318
+ this.sendGroupCallFrame({ type: 'GROUP_CALL_STAGE_HAND_LOWERED', callId, groupId: '' });
1319
+ }
1320
+ promoteToSpeaker(callId, userId) {
1321
+ this.sendGroupCallFrame({
1322
+ type: 'GROUP_CALL_STAGE_PROMOTE',
1323
+ callId,
1324
+ groupId: '',
1325
+ payload: JSON.stringify({ userId }),
1326
+ });
1327
+ }
1328
+ demoteToAudience(callId, userId) {
1329
+ this.sendGroupCallFrame({
1330
+ type: 'GROUP_CALL_STAGE_DEMOTE',
1331
+ callId,
1332
+ groupId: '',
1333
+ payload: JSON.stringify({ userId }),
1334
+ });
1335
+ }
1336
+ submitStageQuestion(callId, text) {
1337
+ this.sendGroupCallFrame({ type: 'GROUP_CALL_STAGE_QUESTION', callId, groupId: '', payload: text });
1338
+ }
1202
1339
  sendGroupCallFrame(frame) {
1203
1340
  if (!this.ws || this.ws.readyState !== WebSocket.OPEN) {
1204
1341
  throw new Error('DropOnAir websocket is not connected');
@@ -1387,10 +1524,28 @@ class MessagingClient {
1387
1524
  this.pendingGroupInviteReject = null;
1388
1525
  reject(new Error(`GROUP_CALL_ALREADY_ACTIVE:${wire.payload ?? ''}`));
1389
1526
  }
1527
+ // Resolve / reject a pending joinRoom() (Feature 3.1). Room-call frames
1528
+ // carry roomId; the join outcome is one of these four types.
1529
+ if (wire.roomId) {
1530
+ const pending = this.pendingRoomJoins.get(wire.roomId);
1531
+ if (pending) {
1532
+ if (wire.type === 'GROUP_CALL_JOINED') {
1533
+ this.pendingRoomJoins.delete(wire.roomId);
1534
+ pending.resolve(wire.callId);
1535
+ }
1536
+ else if (wire.type === 'GROUP_CALL_HOST_REQUIRED' ||
1537
+ wire.type === 'GROUP_CALL_ROOM_CLOSED' ||
1538
+ wire.type === 'GROUP_CALL_WAITING_ROOM_PENDING') {
1539
+ this.pendingRoomJoins.delete(wire.roomId);
1540
+ pending.reject(new Error(wire.type.replace('GROUP_CALL_', '')));
1541
+ }
1542
+ }
1543
+ }
1390
1544
  const event = {
1391
1545
  type: wire.type,
1392
1546
  callId: wire.callId,
1393
1547
  groupId: wire.groupId,
1548
+ roomId: wire.roomId,
1394
1549
  targetUserId: wire.targetUserId,
1395
1550
  payload: wire.payload,
1396
1551
  };
@@ -135,15 +135,69 @@ export interface DecryptedGroupMessage {
135
135
  plaintext: string;
136
136
  }
137
137
  export type GroupMessageCallback = (message: DecryptedGroupMessage) => void;
138
- 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';
138
+ 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' | 'GROUP_CALL_JOINED' | 'GROUP_CALL_WAITING_ROOM_PENDING' | 'GROUP_CALL_HOST_REQUIRED' | 'GROUP_CALL_ROOM_CLOSED' | 'GROUP_CALL_STAGE_HAND_RAISED' | 'GROUP_CALL_STAGE_HAND_LOWERED' | 'GROUP_CALL_STAGE_PROMOTE' | 'GROUP_CALL_STAGE_DEMOTE' | 'GROUP_CALL_STAGE_QUESTION';
139
139
  export interface GroupCallEvent {
140
140
  type: GroupCallEventType | string;
141
141
  callId: string;
142
142
  groupId: string;
143
+ /** Set instead of groupId when the event belongs to a room call (Feature 3.1). */
144
+ roomId?: string;
143
145
  targetUserId?: string;
144
146
  payload?: string;
145
147
  }
146
148
  export type GroupCallEventCallback = (event: GroupCallEvent) => void;
149
+ /**
150
+ * Per-room policy. Each flag is a capability you compose per room; the platform
151
+ * imposes no meeting model of its own.
152
+ */
153
+ export interface RoomPolicy {
154
+ /** Non-host joiners are held in the waiting room until a host admits them. */
155
+ waitingRoom?: boolean;
156
+ /** Non-host joiners cannot open the call; they wait until a host is present. */
157
+ requireHost?: boolean;
158
+ /** Per-room participant cap. Omit to use the plan default. */
159
+ maxParticipants?: number;
160
+ /** When true, the room flips to CLOSED once the last participant leaves the call. */
161
+ autoCloseWhenEmpty?: boolean;
162
+ /**
163
+ * When true, the room's live call runs in stage mode (Feature 3.2): hosts
164
+ * join as speakers, everyone else as receive-only audience who can raise a
165
+ * hand to be promoted. Mesh-scale - suited to panels and small stages.
166
+ */
167
+ stageMode?: boolean;
168
+ }
169
+ export interface Room {
170
+ roomId: string;
171
+ name: string;
172
+ createdBy: string;
173
+ hostUserIds: string[];
174
+ /** Schedule metadata, Unix epoch millis. The platform does not enforce a join window. */
175
+ scheduledStartAt?: number | null;
176
+ scheduledEndAt?: number | null;
177
+ policy: RoomPolicy;
178
+ status: 'ACTIVE' | 'CLOSED';
179
+ /** True when a live call is currently running in the room. */
180
+ isLive: boolean;
181
+ participantCount: number;
182
+ createdAt: number;
183
+ }
184
+ /** Options for creating a room. All fields optional except none are required. */
185
+ export interface CreateRoomOptions {
186
+ name?: string;
187
+ scheduledStartAt?: number;
188
+ scheduledEndAt?: number;
189
+ policy?: RoomPolicy;
190
+ /** Additional designated hosts. The creator is always a host. */
191
+ hostUserIds?: string[];
192
+ }
193
+ /** Patch for updateRoom. Any omitted field is left unchanged. */
194
+ export interface UpdateRoomOptions {
195
+ name?: string;
196
+ scheduledStartAt?: number;
197
+ scheduledEndAt?: number;
198
+ policy?: RoomPolicy;
199
+ hostUserIds?: string[];
200
+ }
147
201
  export interface KeyStorageAdapter {
148
202
  get(key: string): Promise<string | null>;
149
203
  set(key: string, value: string): Promise<void>;
@@ -418,4 +472,34 @@ export interface DropOnAirClient {
418
472
  admitFromWaitingRoom(callId: string, groupId: string, userId: string): void;
419
473
  /** Reject a user waiting to join (host / co-host). */
420
474
  rejectFromWaitingRoom(callId: string, groupId: string, userId: string): void;
475
+ /** Create a scheduled or persistent room. */
476
+ createRoom(options?: CreateRoomOptions): Promise<Room>;
477
+ /** List rooms you created or are a designated host of. */
478
+ listRooms(): Promise<Room[]>;
479
+ /** Fetch a single room, including its live-call state. */
480
+ getRoom(roomId: string): Promise<Room>;
481
+ /** Update a room's name, schedule, policy, or hosts (creator only). */
482
+ updateRoom(roomId: string, update: UpdateRoomOptions): Promise<Room>;
483
+ /** Delete a room (creator only); any live call is ended. */
484
+ deleteRoom(roomId: string): Promise<void>;
485
+ /**
486
+ * Join the live call in a room. Resolves with the callId once joined; that
487
+ * callId is then used with the group-call signaling methods (sendGroupCallSignal,
488
+ * screen-share, moderation, leaveGroupCall) - pass an empty string for groupId.
489
+ * Rejects when the room is closed, a host is required but absent, or the
490
+ * joiner was placed in the waiting room (error message carries the reason).
491
+ */
492
+ joinRoom(roomId: string): Promise<string>;
493
+ /** Leave the live call in a room. */
494
+ leaveRoom(callId: string): Promise<void>;
495
+ /** Raise your hand to request promotion to speaker (any participant). */
496
+ raiseHand(callId: string): void;
497
+ /** Lower your previously raised hand. */
498
+ lowerHand(callId: string): void;
499
+ /** Promote an audience member to speaker (host / co-host). */
500
+ promoteToSpeaker(callId: string, userId: string): void;
501
+ /** Demote a speaker back to audience (host / co-host). */
502
+ demoteToAudience(callId: string, userId: string): void;
503
+ /** Submit a text question; relayed to the stage's speakers. */
504
+ submitStageQuestion(callId: string, text: string): void;
421
505
  }
package/dist/index.d.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  import { InitializeOptions, DropOnAirClient } from './core/types';
2
2
  export { SDK_VERSION, PROTOCOL_VERSION, PAYLOAD_FORMAT_VERSION } from './version';
3
3
  export declare function initialize(options: InitializeOptions): Promise<DropOnAirClient>;
4
- export type { InitializeOptions, DropOnAirClient, DropOnAirEvent, MessageCallback, EventCallback, DecryptedMessage, KeyStorageAdapter, CallEvent, CallEventType, CallEventCallback, TurnCredentials, BroadcastMessage, BroadcastCallback, GroupInfo, GroupMemberInfo, DecryptedGroupMessage, GroupMessageCallback, GroupCallEvent, GroupCallEventType, GroupCallEventCallback, MessageEditEvent, MessageEditCallback, MessageDeleteEvent, MessageDeleteCallback, DeviceInfo, ReadReceiptEvent, ReadReceiptCallback, NotificationClearEvent, NotificationClearCallback, DraftSyncEvent, DraftSyncCallback, } from './core/types';
4
+ export type { InitializeOptions, DropOnAirClient, DropOnAirEvent, MessageCallback, EventCallback, DecryptedMessage, KeyStorageAdapter, CallEvent, CallEventType, CallEventCallback, TurnCredentials, BroadcastMessage, BroadcastCallback, GroupInfo, GroupMemberInfo, DecryptedGroupMessage, GroupMessageCallback, GroupCallEvent, GroupCallEventType, GroupCallEventCallback, Room, RoomPolicy, CreateRoomOptions, UpdateRoomOptions, MessageEditEvent, MessageEditCallback, MessageDeleteEvent, MessageDeleteCallback, DeviceInfo, ReadReceiptEvent, ReadReceiptCallback, NotificationClearEvent, NotificationClearCallback, DraftSyncEvent, DraftSyncCallback, } from './core/types';
5
5
  export type { AttachmentEncryptionType, AttachmentConversationType, AttachmentRef, DeviceWrappedKey, CreateUploadSessionOptions, UploadSession, PrepareAttachmentOptions, DownloadedAttachment, } from './attachment/attachment-types';
6
6
  declare const _default: {
7
7
  initialize: typeof initialize;
@@ -116,6 +116,8 @@ export interface WireGroupCallFrame {
116
116
  groupId: string;
117
117
  targetUserId?: string;
118
118
  payload?: string;
119
+ /** Set instead of groupId when the multi-party call belongs to a Room (Feature 3.1). */
120
+ roomId?: string;
119
121
  }
120
122
  /**
121
123
  * Edit frame, sender re-encrypts new content for all recipient devices.
@@ -167,7 +167,8 @@ const GroupCallFrameType = new protobuf.Type('GroupCallFrame')
167
167
  .add(new protobuf.Field('callId', 2, 'string'))
168
168
  .add(new protobuf.Field('groupId', 3, 'string'))
169
169
  .add(new protobuf.Field('targetUserId', 4, 'string'))
170
- .add(new protobuf.Field('payload', 5, 'string'));
170
+ .add(new protobuf.Field('payload', 5, 'string'))
171
+ .add(new protobuf.Field('roomId', 7, 'string'));
171
172
  // ---------------------------------------------------------------------------
172
173
  // Message Edit and Delete (PROTOCOL_VERSION 3+)
173
174
  // ---------------------------------------------------------------------------
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.13.1";
10
+ export declare const SDK_VERSION = "0.15.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.13.1';
13
+ exports.SDK_VERSION = '0.15.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.13.1",
3
+ "version": "0.15.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",