@soulcraft/sdk 2.0.1 → 2.1.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.
Files changed (92) hide show
  1. package/dist/client/index.d.ts +5 -38
  2. package/dist/client/index.d.ts.map +1 -1
  3. package/dist/client/index.js +5 -47
  4. package/dist/client/index.js.map +1 -1
  5. package/dist/client/namespace-proxy.d.ts +3 -4
  6. package/dist/client/namespace-proxy.d.ts.map +1 -1
  7. package/dist/client/namespace-proxy.js +3 -4
  8. package/dist/client/namespace-proxy.js.map +1 -1
  9. package/dist/index.d.ts +6 -6
  10. package/dist/index.d.ts.map +1 -1
  11. package/dist/index.js +4 -4
  12. package/dist/index.js.map +1 -1
  13. package/dist/modules/hall/admin.d.ts +39 -0
  14. package/dist/modules/hall/admin.d.ts.map +1 -0
  15. package/dist/modules/hall/admin.js +115 -0
  16. package/dist/modules/hall/admin.js.map +1 -0
  17. package/dist/modules/hall/browser.d.ts +83 -27
  18. package/dist/modules/hall/browser.d.ts.map +1 -1
  19. package/dist/modules/hall/browser.js +238 -49
  20. package/dist/modules/hall/browser.js.map +1 -1
  21. package/dist/modules/hall/media.d.ts +164 -0
  22. package/dist/modules/hall/media.d.ts.map +1 -0
  23. package/dist/modules/hall/media.js +182 -0
  24. package/dist/modules/hall/media.js.map +1 -0
  25. package/dist/modules/hall/server.d.ts +119 -6
  26. package/dist/modules/hall/server.d.ts.map +1 -1
  27. package/dist/modules/hall/server.js +299 -9
  28. package/dist/modules/hall/server.js.map +1 -1
  29. package/dist/modules/hall/types.d.ts +705 -25
  30. package/dist/modules/hall/types.d.ts.map +1 -1
  31. package/dist/modules/hall/types.js +12 -7
  32. package/dist/modules/hall/types.js.map +1 -1
  33. package/dist/server/hall-handlers.d.ts +60 -14
  34. package/dist/server/hall-handlers.d.ts.map +1 -1
  35. package/dist/server/hall-handlers.js +61 -12
  36. package/dist/server/hall-handlers.js.map +1 -1
  37. package/dist/server/hono-router.d.ts +2 -9
  38. package/dist/server/hono-router.d.ts.map +1 -1
  39. package/dist/server/hono-router.js +2 -46
  40. package/dist/server/hono-router.js.map +1 -1
  41. package/dist/server/index.d.ts +4 -19
  42. package/dist/server/index.d.ts.map +1 -1
  43. package/dist/server/index.js +10 -29
  44. package/dist/server/index.js.map +1 -1
  45. package/dist/types.d.ts +2 -41
  46. package/dist/types.d.ts.map +1 -1
  47. package/docs/ADR-005-hall-integration.md +449 -0
  48. package/package.json +1 -1
  49. package/dist/client/create-client-sdk.d.ts +0 -113
  50. package/dist/client/create-client-sdk.d.ts.map +0 -1
  51. package/dist/client/create-client-sdk.js +0 -169
  52. package/dist/client/create-client-sdk.js.map +0 -1
  53. package/dist/modules/app-context/index.d.ts +0 -214
  54. package/dist/modules/app-context/index.d.ts.map +0 -1
  55. package/dist/modules/app-context/index.js +0 -569
  56. package/dist/modules/app-context/index.js.map +0 -1
  57. package/dist/modules/billing/firestore-provider.d.ts +0 -60
  58. package/dist/modules/billing/firestore-provider.d.ts.map +0 -1
  59. package/dist/modules/billing/firestore-provider.js +0 -315
  60. package/dist/modules/billing/firestore-provider.js.map +0 -1
  61. package/dist/modules/brainy/proxy.d.ts +0 -48
  62. package/dist/modules/brainy/proxy.d.ts.map +0 -1
  63. package/dist/modules/brainy/proxy.js +0 -95
  64. package/dist/modules/brainy/proxy.js.map +0 -1
  65. package/dist/server/create-sdk.d.ts +0 -74
  66. package/dist/server/create-sdk.d.ts.map +0 -1
  67. package/dist/server/create-sdk.js +0 -104
  68. package/dist/server/create-sdk.js.map +0 -1
  69. package/dist/server/from-license.d.ts +0 -252
  70. package/dist/server/from-license.d.ts.map +0 -1
  71. package/dist/server/from-license.js +0 -349
  72. package/dist/server/from-license.js.map +0 -1
  73. package/dist/server/handlers.d.ts +0 -312
  74. package/dist/server/handlers.d.ts.map +0 -1
  75. package/dist/server/handlers.js +0 -376
  76. package/dist/server/handlers.js.map +0 -1
  77. package/dist/server/postmessage-handler.d.ts +0 -152
  78. package/dist/server/postmessage-handler.d.ts.map +0 -1
  79. package/dist/server/postmessage-handler.js +0 -138
  80. package/dist/server/postmessage-handler.js.map +0 -1
  81. package/dist/transports/http.d.ts +0 -86
  82. package/dist/transports/http.d.ts.map +0 -1
  83. package/dist/transports/http.js +0 -137
  84. package/dist/transports/http.js.map +0 -1
  85. package/dist/transports/postmessage.d.ts +0 -159
  86. package/dist/transports/postmessage.d.ts.map +0 -1
  87. package/dist/transports/postmessage.js +0 -207
  88. package/dist/transports/postmessage.js.map +0 -1
  89. package/dist/transports/workshop.d.ts +0 -173
  90. package/dist/transports/workshop.d.ts.map +0 -1
  91. package/dist/transports/workshop.js +0 -307
  92. package/dist/transports/workshop.js.map +0 -1
@@ -2,20 +2,25 @@
2
2
  * @module modules/hall/types
3
3
  * @description Types for the sdk.hall namespace — the Soulcraft real-time communication layer.
4
4
  *
5
- * Hall is a standalone Rust server at `hall.soulcraft.com`. It handles WebRTC SFU
6
- * (video/audio/data channels), in-process Whisper ASR transcription, BERT concept
7
- * matching, RFC 6464 speaker detection, and per-track MKV recording.
5
+ * Hall is a standalone Rust server at `hall.soulcraft.com`. It provides:
6
+ * - **WebRTC SFU** — video/audio/data channels with RFC 6464 speaker detection
7
+ * - **Whisper ASR** in-process transcription with BERT concept matching
8
+ * - **Pub/Sub** — product-scoped topic routing with presence tracking and replay buffers
9
+ * - **Media Pipeline** — upload, ffmpeg transcoding, thumbnails, async notifications
10
+ * - **Broadcast** — three-tier auto-scaling: participant (SFU) → WHEP → LL-HLS
11
+ * - **Recording** — per-track MKV + optional composite MP4 with webhook notifications
8
12
  *
9
13
  * The `sdk.hall` namespace has two faces depending on context:
10
14
  *
11
15
  * **Server mode** (`@soulcraft/sdk/server`) — the product backend (Academy, Workshop, Venue)
12
- * connects to Hall with a shared secret, creates rooms, and mints short-lived session tokens.
13
- * Use `createHallModule(options)` from `@soulcraft/sdk/server`.
16
+ * connects to Hall with a shared secret, creates rooms, manages pub/sub topics, uploads
17
+ * media, and mints session tokens. Use `createHallModule(options)` from `@soulcraft/sdk/server`.
14
18
  *
15
19
  * **Client mode** (`@soulcraft/sdk/client`) — a browser kit app joins a room using a session
16
- * token issued by the product backend. Use `HallRoomClient` from `@soulcraft/sdk/client`.
20
+ * token, or connects to pub/sub using a pubsub token. Use `joinHallRoom()` or
21
+ * `joinHallPubsub()` from `@soulcraft/sdk/client`.
17
22
  *
18
- * Products never pass their shared secret to the browser. The token is the only browser credential.
23
+ * Products never pass their shared secret to the browser. Tokens are the only browser credentials.
19
24
  */
20
25
  /**
21
26
  * Options for the server-side Hall connection (product backend → Hall server).
@@ -54,7 +59,28 @@ export interface ConceptInput {
54
59
  /** Kit-defined subtype, if any. */
55
60
  subtype?: string;
56
61
  }
57
- /** Options for creating a room. All fields are optional with safe defaults. */
62
+ /**
63
+ * Options for creating a room. All fields are optional with safe defaults.
64
+ *
65
+ * @example Standard room
66
+ * ```typescript
67
+ * await hall.createRoom('cohort-123', {
68
+ * enableTranscription: true,
69
+ * concepts: await loadConcepts(cohortId),
70
+ * })
71
+ * ```
72
+ *
73
+ * @example Broadcast room (webinar/lecture)
74
+ * ```typescript
75
+ * await hall.createRoom('lecture-456', {
76
+ * maxParticipants: 5,
77
+ * allowBroadcast: true,
78
+ * enableRecording: true,
79
+ * recordingComposite: true,
80
+ * recordingWebhookUrl: 'https://academy.soulcraft.com/api/recordings/complete',
81
+ * })
82
+ * ```
83
+ */
58
84
  export interface RoomOptions {
59
85
  /** Maximum peers before the SFU stops accepting new connections (default: 30). */
60
86
  maxPeers?: number;
@@ -72,6 +98,49 @@ export interface RoomOptions {
72
98
  * Pass an empty array to disable concept matching while keeping transcription.
73
99
  */
74
100
  concepts?: ConceptInput[];
101
+ /**
102
+ * Maximum bidirectional participants in broadcast rooms (default: 30).
103
+ * Overflow joiners are assigned as viewers (WHEP or LL-HLS).
104
+ * Only meaningful when `allowBroadcast` is true.
105
+ */
106
+ maxParticipants?: number;
107
+ /**
108
+ * Enable auto-scaling broadcast. When true, overflow joiners become viewers
109
+ * using WHEP (sub-second latency) or LL-HLS (2–3s, unlimited scale).
110
+ * Default: false.
111
+ */
112
+ allowBroadcast?: boolean;
113
+ /**
114
+ * Produce a composite MP4 recording (mixed audio + active speaker video)
115
+ * in addition to individual per-track MKV files.
116
+ * Default: false.
117
+ */
118
+ recordingComposite?: boolean;
119
+ /**
120
+ * Webhook URL for recording completion notification.
121
+ * Hall POSTs `{ sessionId, manifest }` JSON when recording finishes.
122
+ * Retried with exponential backoff (3 attempts).
123
+ */
124
+ recordingWebhookUrl?: string;
125
+ /**
126
+ * Minimum peer count before selective transcription kicks in.
127
+ * When the room has at least this many peers, only the active speaker's audio
128
+ * is sent to Whisper (saves CPU). Set to 0 or omit to transcribe all peers.
129
+ * Default: 0 (disabled).
130
+ */
131
+ selectiveTranscriptionThreshold?: number;
132
+ /**
133
+ * Webhook URL for room event delivery via HTTP POST.
134
+ * Hall POSTs JSON events with `X-Hall-Signature: hmac-sha256(body, product_secret)`
135
+ * header for verification. Retried with exponential backoff (3 attempts).
136
+ */
137
+ webhookUrl?: string;
138
+ /**
139
+ * Filter which events are delivered to `webhookUrl`.
140
+ * e.g. `["peerJoined", "peerLeft", "transcript", "roomClosed"]`.
141
+ * If omitted, all room events are delivered.
142
+ */
143
+ webhookEvents?: string[];
75
144
  }
76
145
  /**
77
146
  * Manifest emitted when recording stops.
@@ -89,6 +158,16 @@ export interface RecordingManifest {
89
158
  audioTracks: string[];
90
159
  /** Absolute paths to per-participant video track MKV files. */
91
160
  videoTracks: string[];
161
+ /**
162
+ * Path to the composite MP4 (mixed audio + active speaker video).
163
+ * Only present when `RoomOptions.recordingComposite` was true.
164
+ */
165
+ compositePath?: string;
166
+ /**
167
+ * Cloud storage URLs (if cloud upload was configured in `hall.toml`).
168
+ * Populated after upload completes (GCS, S3, etc.).
169
+ */
170
+ cloudUrls?: string[];
92
171
  }
93
172
  /** A Whisper ASR transcript segment from a peer in the room. */
94
173
  export interface TranscriptEvent {
@@ -146,6 +225,272 @@ export interface PeerLeftEvent {
146
225
  roomId: string;
147
226
  peerId: string;
148
227
  }
228
+ /** Confirmation that a session token was revoked. */
229
+ export interface TokenRevokedEvent {
230
+ /** Hash of the revoked token (for log correlation, not the token itself). */
231
+ tokenHash: string;
232
+ }
233
+ /** Result of creating a WHIP ingest session. */
234
+ export interface WhipSession {
235
+ /** Resource ID for trickle ICE and teardown (URL path suffix). */
236
+ resourceId: string;
237
+ /** SDP answer to pass back to the WHIP publisher (OBS, FFmpeg, etc.). */
238
+ answerSdp: string;
239
+ /** Room ID this session publishes into. */
240
+ roomId: string;
241
+ /** Peer ID assigned to this publisher in the room. */
242
+ peerId: string;
243
+ }
244
+ /** Options for creating a Hall admin client. */
245
+ export interface HallAdminOptions {
246
+ /** Base HTTP URL of the Hall server (e.g. `"https://hall.soulcraft.com"`). */
247
+ hallUrl: string;
248
+ /** Admin secret from `[admin].secret` in `hall.toml`. */
249
+ secret: string;
250
+ }
251
+ /** A room entry from the admin room list. */
252
+ export interface AdminRoom {
253
+ roomId: string;
254
+ sessionId: string;
255
+ }
256
+ /** A connected product from the admin products list. */
257
+ export interface AdminProduct {
258
+ name: string;
259
+ connections: number;
260
+ }
261
+ /** An audit log entry from the admin audit endpoint. */
262
+ export interface AuditEntry {
263
+ timestampMs: number;
264
+ eventType: string;
265
+ [key: string]: unknown;
266
+ }
267
+ /**
268
+ * REST client for the Hall `/admin` API.
269
+ *
270
+ * Separate from `HallModule` — uses HTTP with `Authorization: Bearer {admin_secret}`
271
+ * rather than the product WebSocket connection. Create via `createHallAdmin()`.
272
+ *
273
+ * @example
274
+ * ```typescript
275
+ * import { createHallAdmin } from '@soulcraft/sdk/server'
276
+ *
277
+ * const admin = createHallAdmin({
278
+ * hallUrl: 'https://hall.soulcraft.com',
279
+ * secret: process.env.HALL_ADMIN_SECRET!,
280
+ * })
281
+ * const { rooms } = await admin.listRooms()
282
+ * await admin.kickPeer('room-1', 'disruptive-user')
283
+ * ```
284
+ */
285
+ export interface HallAdminClient {
286
+ /** List all active rooms with session IDs. */
287
+ listRooms(): Promise<{
288
+ rooms: AdminRoom[];
289
+ count: number;
290
+ }>;
291
+ /** Get detail for a single room. */
292
+ getRoom(roomId: string): Promise<AdminRoom>;
293
+ /** Force-close a room and disconnect all peers. */
294
+ closeRoom(roomId: string): Promise<void>;
295
+ /** Kick a peer from a room. */
296
+ kickPeer(roomId: string, peerId: string): Promise<void>;
297
+ /** Revoke a session token. */
298
+ revokeToken(token: string): Promise<void>;
299
+ /** List connected products with connection counts. */
300
+ listProducts(): Promise<{
301
+ products: AdminProduct[];
302
+ }>;
303
+ /** List pub/sub topics with subscriber counts. */
304
+ listTopics(): Promise<{
305
+ topicCount: number;
306
+ totalSubscribers: number;
307
+ totalMessages: number;
308
+ }>;
309
+ /** Get current config (secrets redacted). */
310
+ getConfig(): Promise<Record<string, unknown>>;
311
+ /** Trigger a config reload (equivalent to SIGHUP). */
312
+ reloadConfig(): Promise<{
313
+ status: string;
314
+ }>;
315
+ /** Get recent audit log entries. */
316
+ getAuditLog(limit?: number): Promise<{
317
+ entries: AuditEntry[];
318
+ count: number;
319
+ }>;
320
+ }
321
+ /** A single replayed message from a topic's replay buffer. */
322
+ export interface ReplayEntry {
323
+ /** Sender identifier (product name or peer ID). */
324
+ senderId: string;
325
+ /** The message payload. */
326
+ payload: unknown;
327
+ /** Unix timestamp in milliseconds when the message was originally sent. */
328
+ timestampMs: number;
329
+ }
330
+ /** Confirmation of a pub/sub topic subscription. */
331
+ export interface TopicSubscribedEvent {
332
+ /** Topic that was subscribed to. */
333
+ topic: string;
334
+ /** Current number of subscribers on this topic. */
335
+ subscriberCount: number;
336
+ /** Replay buffer entries (most recent messages, if any). */
337
+ replay: ReplayEntry[];
338
+ }
339
+ /** Confirmation of a pub/sub topic unsubscription. */
340
+ export interface TopicUnsubscribedEvent {
341
+ /** Topic that was unsubscribed from. */
342
+ topic: string;
343
+ }
344
+ /** A message received on a subscribed pub/sub topic. */
345
+ export interface TopicMessageEvent {
346
+ /** Topic the message was published to. */
347
+ topic: string;
348
+ /** Sender identifier (product name or peer ID). */
349
+ senderId: string;
350
+ /** The message payload. */
351
+ payload: unknown;
352
+ /** Unix timestamp in milliseconds. */
353
+ timestampMs: number;
354
+ }
355
+ /** A subscriber joined or left a pub/sub topic. */
356
+ export interface PresenceUpdateEvent {
357
+ /** Topic the presence event occurred on. */
358
+ topic: string;
359
+ /** Peer who joined or left. */
360
+ peerId: string;
361
+ /** Whether the peer joined or left. */
362
+ action: 'joined' | 'left';
363
+ /** Presence metadata (only present on join). */
364
+ metadata?: Record<string, unknown>;
365
+ /** Current subscriber count after this event. */
366
+ subscriberCount: number;
367
+ }
368
+ /** A single subscriber in a presence snapshot. */
369
+ export interface PresenceEntry {
370
+ /** Subscriber identifier. */
371
+ peerId: string;
372
+ /** Optional presence metadata set at subscribe time. */
373
+ metadata?: Record<string, unknown>;
374
+ }
375
+ /** Full snapshot of all current subscribers on a topic. */
376
+ export interface PresenceSnapshotEvent {
377
+ /** Topic queried. */
378
+ topic: string;
379
+ /** All current subscribers with their metadata. */
380
+ subscribers: PresenceEntry[];
381
+ }
382
+ /** Metadata for a processed media file. */
383
+ export interface MediaInfo {
384
+ /** Unique media identifier. */
385
+ mediaId: string;
386
+ /** Product that uploaded this media. */
387
+ productName: string;
388
+ /** Original filename from the upload. */
389
+ originalFilename: string;
390
+ /** MIME type of the original file. */
391
+ mimeType: string;
392
+ /** File size in bytes. */
393
+ size: number;
394
+ /** Duration in seconds (audio/video only). */
395
+ duration?: number;
396
+ /** Width × height in pixels (image/video only). */
397
+ dimensions?: [number, number];
398
+ /** Whether the file was transcoded from its original format. */
399
+ transcoded: boolean;
400
+ /** Processing status. */
401
+ status: 'processing' | 'ready' | 'error';
402
+ /** ISO 8601 timestamp when the media was uploaded. */
403
+ createdAt: string;
404
+ }
405
+ /** Notification that a media file has been processed and is ready. */
406
+ export interface MediaReadyEvent {
407
+ /** Unique media identifier. */
408
+ mediaId: string;
409
+ /** MIME type of the original file. */
410
+ mimeType: string;
411
+ /** File size in bytes. */
412
+ size: number;
413
+ /** Duration in seconds (audio/video only). */
414
+ duration?: number;
415
+ /** Width × height in pixels (image/video only). */
416
+ dimensions?: [number, number];
417
+ }
418
+ /** Notification that media processing failed. */
419
+ export interface MediaErrorEvent {
420
+ /** Media identifier that failed. */
421
+ mediaId: string;
422
+ /** Human-readable error description. */
423
+ error: string;
424
+ }
425
+ /**
426
+ * Supported transcode target formats for media uploads.
427
+ * Sent as the `transcode` field in the upload form.
428
+ */
429
+ export type TranscodeTarget = 'audio/mp3' | 'video/mp4' | 'image/webp';
430
+ /**
431
+ * Role assigned to a peer in a broadcast room.
432
+ * - `participant` — full bidirectional WebRTC (can publish and receive media)
433
+ * - `viewer` — receive-only (WHEP or LL-HLS, cannot publish)
434
+ */
435
+ export type HallPeerRole = 'participant' | 'viewer';
436
+ /** A viewer was promoted to full participant. */
437
+ export interface PeerPromotedEvent {
438
+ roomId: string;
439
+ peerId: string;
440
+ }
441
+ /** A participant was demoted to viewer. */
442
+ export interface PeerDemotedEvent {
443
+ roomId: string;
444
+ peerId: string;
445
+ }
446
+ /** Periodic viewer count update for broadcast rooms (every ~5s). */
447
+ export interface ViewerCountEvent {
448
+ roomId: string;
449
+ /** Number of full bidirectional participants. */
450
+ participants: number;
451
+ /** Number of WHEP (sub-second latency) viewers. */
452
+ whepViewers: number;
453
+ /** Number of LL-HLS viewers. */
454
+ hlsViewers: number;
455
+ }
456
+ /**
457
+ * Screen share simulcast mode.
458
+ * - `static` — prioritize resolution (presentations, documents)
459
+ * - `motion` — prioritize framerate (demos, video playback)
460
+ */
461
+ export type ScreenShareMode = 'static' | 'motion';
462
+ /** Periodic thumbnail of an active screen share. */
463
+ export interface ScreenShareThumbnailEvent {
464
+ roomId: string;
465
+ peerId: string;
466
+ /** URL to the latest thumbnail image. */
467
+ thumbnailUrl: string;
468
+ }
469
+ /** A single chat message in room history. */
470
+ export interface ChatMessage {
471
+ /** Peer who sent the message. */
472
+ peerId: string;
473
+ /** Message text. */
474
+ text: string;
475
+ /** Unix timestamp in milliseconds. */
476
+ timestampMs: number;
477
+ }
478
+ /** Response to a `getChatHistory` request. */
479
+ export interface ChatHistoryEvent {
480
+ roomId: string;
481
+ /** Chat messages in chronological order. */
482
+ messages: ChatMessage[];
483
+ }
484
+ /**
485
+ * Sent to a browser client when their role changes (promoted or demoted).
486
+ * The SDK handles transport upgrade/downgrade transparently.
487
+ */
488
+ export interface RoleChangedEvent {
489
+ /** New role: `"participant"` or `"viewer"`. */
490
+ role: HallPeerRole;
491
+ /** Whether the peer can now publish media tracks. */
492
+ canPublish: boolean;
493
+ }
149
494
  /**
150
495
  * Messages sent from the product backend to the Hall server over the product WebSocket.
151
496
  * Encoded as msgpack with `t` (type tag) and `d` (data) fields — matching Rust serde
@@ -179,6 +524,7 @@ export type HallClientMessage = {
179
524
  roomId: string;
180
525
  peerId: string;
181
526
  ttlSecs?: number | undefined;
527
+ role?: HallPeerRole | undefined;
182
528
  };
183
529
  } | {
184
530
  t: 'startRecording';
@@ -197,6 +543,65 @@ export type HallClientMessage = {
197
543
  channel: string;
198
544
  payload: Uint8Array;
199
545
  };
546
+ } | {
547
+ t: 'subscribeTopic';
548
+ d: {
549
+ topic: string;
550
+ metadata?: Record<string, unknown> | undefined;
551
+ };
552
+ } | {
553
+ t: 'unsubscribeTopic';
554
+ d: {
555
+ topic: string;
556
+ };
557
+ } | {
558
+ t: 'broadcastTopic';
559
+ d: {
560
+ topic: string;
561
+ payload: unknown;
562
+ };
563
+ } | {
564
+ t: 'getPresence';
565
+ d: {
566
+ topic: string;
567
+ };
568
+ } | {
569
+ t: 'createPubsubToken';
570
+ d: {
571
+ peerId: string;
572
+ allowedTopics?: string[] | undefined;
573
+ ttlSecs?: number | undefined;
574
+ };
575
+ } | {
576
+ t: 'promotePeer';
577
+ d: {
578
+ roomId: string;
579
+ peerId: string;
580
+ };
581
+ } | {
582
+ t: 'demotePeer';
583
+ d: {
584
+ roomId: string;
585
+ peerId: string;
586
+ };
587
+ } | {
588
+ t: 'setScreenShareMode';
589
+ d: {
590
+ roomId: string;
591
+ peerId: string;
592
+ mode: ScreenShareMode;
593
+ };
594
+ } | {
595
+ t: 'getChatHistory';
596
+ d: {
597
+ roomId: string;
598
+ lastN?: number | undefined;
599
+ };
600
+ } | {
601
+ t: 'revokeToken';
602
+ d: {
603
+ token: string;
604
+ };
200
605
  };
201
606
  /**
202
607
  * Messages sent from the Hall server to the product backend.
@@ -276,6 +681,51 @@ export type HallServerMessage = {
276
681
  } | {
277
682
  t: 'peerLeft';
278
683
  d: PeerLeftEvent;
684
+ } | {
685
+ t: 'topicSubscribed';
686
+ d: TopicSubscribedEvent;
687
+ } | {
688
+ t: 'topicUnsubscribed';
689
+ d: TopicUnsubscribedEvent;
690
+ } | {
691
+ t: 'topicMessage';
692
+ d: TopicMessageEvent;
693
+ } | {
694
+ t: 'presenceUpdate';
695
+ d: PresenceUpdateEvent;
696
+ } | {
697
+ t: 'presenceSnapshot';
698
+ d: PresenceSnapshotEvent;
699
+ } | {
700
+ t: 'pubsubToken';
701
+ d: {
702
+ token: string;
703
+ expiresAt: number;
704
+ };
705
+ } | {
706
+ t: 'mediaReady';
707
+ d: MediaReadyEvent;
708
+ } | {
709
+ t: 'mediaError';
710
+ d: MediaErrorEvent;
711
+ } | {
712
+ t: 'peerPromoted';
713
+ d: PeerPromotedEvent;
714
+ } | {
715
+ t: 'peerDemoted';
716
+ d: PeerDemotedEvent;
717
+ } | {
718
+ t: 'viewerCount';
719
+ d: ViewerCountEvent;
720
+ } | {
721
+ t: 'screenShareThumbnail';
722
+ d: ScreenShareThumbnailEvent;
723
+ } | {
724
+ t: 'chatHistory';
725
+ d: ChatHistoryEvent;
726
+ } | {
727
+ t: 'tokenRevoked';
728
+ d: TokenRevokedEvent;
279
729
  };
280
730
  /** Event map for server-side {@link HallRoom} listeners. */
281
731
  export interface HallRoomEvents {
@@ -286,6 +736,13 @@ export interface HallRoomEvents {
286
736
  peerJoined: PeerJoinedEvent;
287
737
  peerLeft: PeerLeftEvent;
288
738
  recordingManifest: RecordingManifest;
739
+ peerPromoted: PeerPromotedEvent;
740
+ peerDemoted: PeerDemotedEvent;
741
+ viewerCount: ViewerCountEvent;
742
+ screenShareThumbnail: ScreenShareThumbnailEvent;
743
+ chatHistory: ChatHistoryEvent;
744
+ mediaReady: MediaReadyEvent;
745
+ mediaError: MediaErrorEvent;
289
746
  closed: {
290
747
  roomId: string;
291
748
  };
@@ -293,9 +750,9 @@ export interface HallRoomEvents {
293
750
  /**
294
751
  * The `sdk.hall` namespace on `SoulcraftSDK` in **server mode**.
295
752
  *
296
- * Products call these methods in request handlers to create rooms and issue session
297
- * tokens that browser clients use to join. Rooms are managed by the Hall server;
298
- * the product only holds a control-plane connection over WebSocket.
753
+ * Products call these methods in request handlers to create rooms, manage pub/sub
754
+ * topics, upload media, and issue session tokens. Rooms and topics are managed by
755
+ * the Hall server; the product only holds a control-plane connection over WebSocket.
299
756
  *
300
757
  * Obtain via `createHallModule(options)` from `@soulcraft/sdk/server`.
301
758
  *
@@ -330,11 +787,27 @@ export interface HallModule {
330
787
  * @throws {Error} If auth fails or the connection cannot be established.
331
788
  */
332
789
  connect(): Promise<void>;
790
+ /**
791
+ * Registers a listener for unexpected disconnects (not triggered by `close()`).
792
+ *
793
+ * @param listener - Called with a human-readable disconnect reason.
794
+ */
795
+ onDisconnect(listener: (reason: string) => void): void;
796
+ /**
797
+ * Registers a listener for successful reconnections.
798
+ * On reconnect, rooms from before the disconnect are gone — re-create them.
799
+ */
800
+ onReconnect(listener: () => void): void;
801
+ /**
802
+ * Gracefully closes the Hall connection and releases resources.
803
+ * Called automatically by `sdk.shutdown()` in server mode.
804
+ */
805
+ close(): Promise<void>;
333
806
  /**
334
807
  * Creates a new room. Errors if a room with the same ID already exists.
335
808
  *
336
809
  * @param roomId - Unique room identifier (e.g. Brainy session entity ID).
337
- * @param options - Room options: peer limit, transcription, recording, concept list.
810
+ * @param options - Room options: peer limit, transcription, recording, broadcast, concept list.
338
811
  * @returns The HallRoom handle — register event listeners on it immediately.
339
812
  */
340
813
  createRoom(roomId: string, options?: RoomOptions): Promise<HallRoom>;
@@ -357,15 +830,16 @@ export interface HallModule {
357
830
  */
358
831
  getRoom(roomId: string): HallRoom | undefined;
359
832
  /**
360
- * Mints a short-lived session token for a browser client.
833
+ * Mints a short-lived session token for a browser client to join a room.
361
834
  * Pass `{ token, hallUrl }` to the browser — never the product secret.
362
835
  *
363
836
  * @param roomId - The room the browser will join.
364
837
  * @param peerId - The peer's unique identifier (e.g. authenticated user ID).
365
838
  * @param ttlSecs - Token lifetime in seconds (default: 300).
839
+ * @param role - Optional role hint: `"participant"`, `"viewer"`, or undefined (auto).
366
840
  * @returns `{ token, expiresAt }`.
367
841
  */
368
- createSessionToken(roomId: string, peerId: string, ttlSecs?: number): Promise<{
842
+ createSessionToken(roomId: string, peerId: string, ttlSecs?: number, role?: HallPeerRole): Promise<{
369
843
  token: string;
370
844
  expiresAt: number;
371
845
  }>;
@@ -384,21 +858,128 @@ export interface HallModule {
384
858
  */
385
859
  stopRecording(roomId: string): Promise<void>;
386
860
  /**
387
- * Registers a listener for unexpected disconnects (not triggered by `close()`).
861
+ * Subscribe to a pub/sub topic. Topics are product-scoped no cross-product visibility.
862
+ * The returned event fires on the connection-level `onTopicSubscribed` listener.
388
863
  *
389
- * @param listener - Called with a human-readable disconnect reason.
864
+ * @param topic - Topic name to subscribe to.
865
+ * @param metadata - Optional presence metadata visible to other subscribers.
390
866
  */
391
- onDisconnect(listener: (reason: string) => void): void;
867
+ subscribeTopic(topic: string, metadata?: Record<string, unknown>): void;
392
868
  /**
393
- * Registers a listener for successful reconnections.
394
- * On reconnect, rooms from before the disconnect are gone — re-create them.
869
+ * Unsubscribe from a pub/sub topic.
870
+ *
871
+ * @param topic - Topic name to unsubscribe from.
395
872
  */
396
- onReconnect(listener: () => void): void;
873
+ unsubscribeTopic(topic: string): void;
397
874
  /**
398
- * Gracefully closes the Hall connection and releases resources.
399
- * Called automatically by `sdk.shutdown()` in server mode.
875
+ * Broadcast a message to all subscribers of a pub/sub topic.
876
+ *
877
+ * @param topic - Topic to broadcast to.
878
+ * @param payload - Arbitrary JSON payload delivered to all subscribers.
400
879
  */
401
- close(): Promise<void>;
880
+ broadcastTopic(topic: string, payload: unknown): void;
881
+ /**
882
+ * Request a snapshot of all current subscribers on a topic.
883
+ * The result fires on the connection-level `onPresenceSnapshot` listener.
884
+ *
885
+ * @param topic - Topic to query presence for.
886
+ */
887
+ getPresence(topic: string): void;
888
+ /**
889
+ * Register a listener for pub/sub events on the product connection.
890
+ *
891
+ * @param event - The pub/sub event name.
892
+ * @param listener - Callback invoked with the typed event payload.
893
+ */
894
+ onPubsub<K extends keyof HallPubsubEvents>(event: K, listener: (data: HallPubsubEvents[K]) => void): void;
895
+ /**
896
+ * Issue a browser pub/sub token. The browser uses this to connect to
897
+ * `wss://hall.soulcraft.com/ws/pubsub/{token}` for client-side pub/sub.
898
+ *
899
+ * @param peerId - Peer ID the token is issued for.
900
+ * @param allowedTopics - Optional topic whitelist. If omitted, grants access to all topics.
901
+ * @param ttlSecs - Token lifetime in seconds (default: 300).
902
+ * @returns `{ token, expiresAt }`.
903
+ */
904
+ createPubsubToken(peerId: string, allowedTopics?: string[], ttlSecs?: number): Promise<{
905
+ token: string;
906
+ expiresAt: number;
907
+ }>;
908
+ /**
909
+ * Promote a viewer to a full bidirectional participant in a broadcast room.
910
+ *
911
+ * @param roomId - Room containing the peer.
912
+ * @param peerId - Peer ID to promote.
913
+ */
914
+ promotePeer(roomId: string, peerId: string): void;
915
+ /**
916
+ * Demote a participant to a receive-only viewer in a broadcast room.
917
+ *
918
+ * @param roomId - Room containing the peer.
919
+ * @param peerId - Peer ID to demote.
920
+ */
921
+ demotePeer(roomId: string, peerId: string): void;
922
+ /**
923
+ * Set the screen share simulcast strategy for a peer.
924
+ *
925
+ * @param roomId - Room containing the screensharing peer.
926
+ * @param peerId - Peer sharing their screen.
927
+ * @param mode - `"static"` (prioritize resolution) or `"motion"` (prioritize framerate).
928
+ */
929
+ setScreenShareMode(roomId: string, peerId: string, mode: ScreenShareMode): void;
930
+ /**
931
+ * Request historical chat messages for a room.
932
+ * Chat is also bridged to pub/sub topic `room:{roomId}:chat` with replay buffer.
933
+ *
934
+ * @param roomId - Room to get chat history for.
935
+ * @param lastN - Maximum number of recent messages to return (default: all stored).
936
+ */
937
+ getChatHistory(roomId: string, lastN?: number): void;
938
+ /**
939
+ * Revoke a session token. The token becomes invalid immediately — any browser
940
+ * client attempting to connect with it will be rejected.
941
+ *
942
+ * @param token - The session token string to revoke.
943
+ */
944
+ revokeToken(token: string): void;
945
+ /**
946
+ * Register a listener for token revocation confirmations.
947
+ *
948
+ * @param listener - Called with the token hash (not the token itself) on confirmation.
949
+ */
950
+ onTokenRevoked(listener: (data: TokenRevokedEvent) => void): void;
951
+ /**
952
+ * Create a WHIP ingest session for an external publisher (OBS, FFmpeg, GStreamer).
953
+ * The publisher's media is injected into the room as a named peer.
954
+ *
955
+ * @param roomId - Room to publish into (must already exist).
956
+ * @param sdpOffer - Raw SDP offer from the publisher.
957
+ * @returns WHIP session with `resourceId` and `answerSdp`.
958
+ */
959
+ createWhipSession(roomId: string, sdpOffer: string): Promise<WhipSession>;
960
+ /**
961
+ * Send a trickle ICE candidate to an active WHIP session.
962
+ *
963
+ * @param roomId - Room the WHIP session belongs to.
964
+ * @param resourceId - Resource ID from `createWhipSession`.
965
+ * @param candidate - ICE candidate in `application/trickle-ice-sdpfrag` format.
966
+ */
967
+ addWhipIceCandidate(roomId: string, resourceId: string, candidate: string): Promise<void>;
968
+ /**
969
+ * Tear down a WHIP session and remove the publisher peer from the room.
970
+ *
971
+ * @param roomId - Room the WHIP session belongs to.
972
+ * @param resourceId - Resource ID from `createWhipSession`.
973
+ */
974
+ closeWhipSession(roomId: string, resourceId: string): Promise<void>;
975
+ }
976
+ /** Event map for pub/sub events on the server-side Hall connection. */
977
+ export interface HallPubsubEvents {
978
+ topicSubscribed: TopicSubscribedEvent;
979
+ topicUnsubscribed: TopicUnsubscribedEvent;
980
+ topicMessage: TopicMessageEvent;
981
+ presenceUpdate: PresenceUpdateEvent;
982
+ presenceSnapshot: PresenceSnapshotEvent;
402
983
  }
403
984
  /**
404
985
  * A handle for a live Hall room, returned by `sdk.hall.createRoom()`.
@@ -408,10 +989,14 @@ export interface HallModule {
408
989
  *
409
990
  * @example
410
991
  * ```typescript
411
- * const room = await hall.createRoom('cohort-123', { enableTranscription: true })
992
+ * const room = await hall.createRoom('cohort-123', {
993
+ * enableTranscription: true,
994
+ * allowBroadcast: true,
995
+ * })
412
996
  * room.on('transcript', (t) => console.log(`${t.peerId}: ${t.text}`))
413
997
  * room.on('conceptMention', (c) => brain.relate({ from: c.peerId, to: c.nodeId }))
414
998
  * room.on('peerJoined', (p) => updatePresence(p.peerId, 'joined'))
999
+ * room.on('viewerCount', (v) => updateDashboard(v.participants, v.whepViewers + v.hlsViewers))
415
1000
  * ```
416
1001
  */
417
1002
  export interface HallRoom {
@@ -462,6 +1047,8 @@ export interface HallRoomHandleEvents {
462
1047
  peerId: string;
463
1048
  track: MediaStreamTrack;
464
1049
  };
1050
+ /** Role changed (promoted or demoted in a broadcast room). */
1051
+ roleChanged: RoleChangedEvent;
465
1052
  /** The Hall server closed the room. */
466
1053
  closed: {
467
1054
  roomId: string;
@@ -478,6 +1065,9 @@ export interface HallRoomHandleEvents {
478
1065
  * (transcript, conceptMention, relationProposed) alongside media track events
479
1066
  * so kit apps have a single event surface for the full session experience.
480
1067
  *
1068
+ * In broadcast rooms, the `roleChanged` event fires when the peer is promoted
1069
+ * or demoted. The SDK handles transport upgrade/downgrade transparently.
1070
+ *
481
1071
  * @example
482
1072
  * ```typescript
483
1073
  * const room = await joinHallRoom({ token, hallUrl: 'wss://hall.soulcraft.com' })
@@ -489,7 +1079,7 @@ export interface HallRoomHandleEvents {
489
1079
  * room.on('transcript', ({ peerId, text, isFinal }) => {
490
1080
  * if (isFinal) appendTranscript(peerId, text)
491
1081
  * })
492
- * room.on('conceptMention', ({ nodeId, confidence }) => pulseGraphNode(nodeId, confidence))
1082
+ * room.on('roleChanged', ({ role, canPublish }) => updateUI(role, canPublish))
493
1083
  * ```
494
1084
  */
495
1085
  export interface HallRoomHandle {
@@ -497,6 +1087,10 @@ export interface HallRoomHandle {
497
1087
  readonly roomId: string;
498
1088
  /** The peer ID for this connection (decoded from the session token). */
499
1089
  readonly peerId: string;
1090
+ /** The assigned role in the room. Updated when `roleChanged` fires. */
1091
+ readonly role: HallPeerRole;
1092
+ /** Whether this peer can publish media tracks. Updated when `roleChanged` fires. */
1093
+ readonly canPublish: boolean;
500
1094
  /**
501
1095
  * Register a listener for a room event.
502
1096
  *
@@ -524,4 +1118,90 @@ export interface HallRoomHandle {
524
1118
  */
525
1119
  close(): void;
526
1120
  }
1121
+ /** Events emitted by a `HallPubsubHandle` in the browser. */
1122
+ export interface HallPubsubHandleEvents {
1123
+ /** Topic subscription confirmed. */
1124
+ topicSubscribed: TopicSubscribedEvent;
1125
+ /** Topic unsubscription confirmed. */
1126
+ topicUnsubscribed: TopicUnsubscribedEvent;
1127
+ /** Message received on a subscribed topic. */
1128
+ topicMessage: TopicMessageEvent;
1129
+ /** A subscriber joined or left a topic. */
1130
+ presenceUpdate: PresenceUpdateEvent;
1131
+ /** Full presence snapshot for a topic. */
1132
+ presenceSnapshot: PresenceSnapshotEvent;
1133
+ /** A connection or protocol error occurred. */
1134
+ error: {
1135
+ code: string;
1136
+ message: string;
1137
+ };
1138
+ /** The pub/sub connection was closed. */
1139
+ closed: void;
1140
+ }
1141
+ /**
1142
+ * An active pub/sub connection to Hall, returned by `joinHallPubsub()`.
1143
+ *
1144
+ * Wraps a WebSocket connection to `wss://hall.soulcraft.com/ws/pubsub/{token}`.
1145
+ * Uses msgpack wire format, same as the product WebSocket.
1146
+ *
1147
+ * @example
1148
+ * ```typescript
1149
+ * import { joinHallPubsub } from '@soulcraft/sdk/client'
1150
+ *
1151
+ * const pubsub = await joinHallPubsub({ token, hallUrl: 'wss://hall.soulcraft.com' })
1152
+ * pubsub.subscribe('room:abc:chat', { username: 'Alice' })
1153
+ * pubsub.on('topicMessage', ({ topic, senderId, payload }) => showMessage(senderId, payload))
1154
+ * pubsub.on('presenceUpdate', ({ peerId, action }) => updatePresence(peerId, action))
1155
+ *
1156
+ * pubsub.broadcast('room:abc:chat', { text: 'Hello!' })
1157
+ * // On leave:
1158
+ * pubsub.close()
1159
+ * ```
1160
+ */
1161
+ export interface HallPubsubHandle {
1162
+ /** The peer ID for this connection (decoded from the pubsub token). */
1163
+ readonly peerId: string;
1164
+ /**
1165
+ * Subscribe to a pub/sub topic.
1166
+ *
1167
+ * @param topic - Topic name to subscribe to.
1168
+ * @param metadata - Optional presence metadata visible to other subscribers.
1169
+ */
1170
+ subscribe(topic: string, metadata?: Record<string, unknown>): void;
1171
+ /**
1172
+ * Unsubscribe from a pub/sub topic.
1173
+ *
1174
+ * @param topic - Topic name to unsubscribe from.
1175
+ */
1176
+ unsubscribe(topic: string): void;
1177
+ /**
1178
+ * Broadcast a message to all subscribers of a topic.
1179
+ *
1180
+ * @param topic - Topic to broadcast to.
1181
+ * @param payload - Arbitrary JSON payload delivered to all subscribers.
1182
+ */
1183
+ broadcast(topic: string, payload: unknown): void;
1184
+ /**
1185
+ * Request a presence snapshot for a topic.
1186
+ *
1187
+ * @param topic - Topic to query presence for.
1188
+ */
1189
+ getPresence(topic: string): void;
1190
+ /**
1191
+ * Register a listener for a pub/sub event.
1192
+ *
1193
+ * @param event - The event name.
1194
+ * @param listener - Callback invoked with the typed event payload.
1195
+ */
1196
+ on<K extends keyof HallPubsubHandleEvents>(event: K, listener: (data: HallPubsubHandleEvents[K]) => void): this;
1197
+ /**
1198
+ * Remove a previously registered listener.
1199
+ *
1200
+ * @param event - The event name.
1201
+ * @param listener - The exact function reference passed to `on`.
1202
+ */
1203
+ off<K extends keyof HallPubsubHandleEvents>(event: K, listener: (data: HallPubsubHandleEvents[K]) => void): this;
1204
+ /** Disconnect the pub/sub WebSocket. */
1205
+ close(): void;
1206
+ }
527
1207
  //# sourceMappingURL=types.d.ts.map