@rapidmx/meet-plugin 0.15.0 → 0.16.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 (74) hide show
  1. package/README.md +35 -55
  2. package/apps/meet/[token].tsx +341 -341
  3. package/apps/meet/_CallControls.tsx +508 -483
  4. package/apps/meet/_CallView.tsx +1060 -924
  5. package/apps/meet/_ChatPanel.tsx +194 -0
  6. package/apps/meet/_ColorWheel.tsx +136 -0
  7. package/apps/meet/_MeetLobby.tsx +224 -224
  8. package/apps/meet/_ParticipantTile.tsx +243 -242
  9. package/apps/meet/_Whiteboard.tsx +577 -0
  10. package/apps/meet/_icons.tsx +135 -90
  11. package/apps/meet/_meetApi.ts +213 -212
  12. package/apps/shared/collab/CollabSession.ts +675 -0
  13. package/apps/shared/collab/WhiteboardModel.ts +216 -0
  14. package/apps/shared/collab/chatMarkdown.tsx +375 -0
  15. package/apps/shared/collab/color.ts +67 -0
  16. package/apps/shared/collab/protocol.ts +321 -0
  17. package/apps/shared/collab/whiteboardRender.ts +311 -0
  18. package/apps/shared/media/filters/VideoFilterProcessor.ts +408 -408
  19. package/apps/shared/media/filters/mlModels.ts +174 -174
  20. package/apps/shared/relay/RelayClient.ts +2 -1
  21. package/apps/shared/relay/RelayTransport.ts +2 -1
  22. package/apps/shared/relay/relayEnv.ts +1 -1
  23. package/apps/shared/signaling/SignalingClient.ts +335 -0
  24. package/apps/shared/webrtc/MeshConnectionManager.ts +971 -971
  25. package/apps/shared/webrtc/types.ts +335 -316
  26. package/dist/apps/meet/[token].js +1 -1
  27. package/dist/apps/meet/_CallControls.d.ts +12 -4
  28. package/dist/apps/meet/_CallControls.js +5 -5
  29. package/dist/apps/meet/_CallView.d.ts +19 -4
  30. package/dist/apps/meet/_CallView.js +100 -11
  31. package/dist/apps/meet/_ChatPanel.d.ts +24 -0
  32. package/dist/apps/meet/_ChatPanel.js +116 -0
  33. package/dist/apps/meet/_ColorWheel.d.ts +14 -0
  34. package/dist/apps/meet/_ColorWheel.js +82 -0
  35. package/dist/apps/meet/_ParticipantTile.d.ts +2 -1
  36. package/dist/apps/meet/_ParticipantTile.js +1 -1
  37. package/dist/apps/meet/_Whiteboard.d.ts +64 -0
  38. package/dist/apps/meet/_Whiteboard.js +312 -0
  39. package/dist/apps/meet/_icons.d.ts +12 -0
  40. package/dist/apps/meet/_icons.js +12 -0
  41. package/dist/apps/meet/_meetApi.d.ts +8 -7
  42. package/dist/apps/meet/_meetApi.js +2 -2
  43. package/dist/apps/shared/collab/CollabSession.d.ts +208 -0
  44. package/dist/apps/shared/collab/CollabSession.js +559 -0
  45. package/dist/apps/shared/collab/WhiteboardModel.d.ts +54 -0
  46. package/dist/apps/shared/collab/WhiteboardModel.js +197 -0
  47. package/dist/apps/shared/collab/chatMarkdown.d.ts +65 -0
  48. package/dist/apps/shared/collab/chatMarkdown.js +311 -0
  49. package/dist/apps/shared/collab/color.d.ts +18 -0
  50. package/dist/apps/shared/collab/color.js +59 -0
  51. package/dist/apps/shared/collab/protocol.d.ts +157 -0
  52. package/dist/apps/shared/collab/protocol.js +222 -0
  53. package/dist/apps/shared/collab/whiteboardRender.d.ts +31 -0
  54. package/dist/apps/shared/collab/whiteboardRender.js +268 -0
  55. package/dist/apps/shared/relay/RelayClient.js +2 -1
  56. package/dist/apps/shared/relay/RelayTransport.d.ts +2 -1
  57. package/dist/apps/shared/relay/RelayTransport.js +2 -1
  58. package/dist/apps/shared/relay/relayEnv.d.ts +1 -1
  59. package/dist/apps/shared/signaling/SignalingClient.d.ts +95 -0
  60. package/dist/apps/shared/signaling/SignalingClient.js +278 -0
  61. package/dist/apps/shared/webrtc/MeshConnectionManager.d.ts +7 -7
  62. package/dist/apps/shared/webrtc/MeshConnectionManager.js +8 -8
  63. package/dist/apps/shared/webrtc/types.d.ts +38 -24
  64. package/dist/apps/shared/webrtc/types.js +3 -4
  65. package/dist/lib/routes/BaseVideoMeetingRoute.js +171 -69
  66. package/dist/lib/routes/BaseVideoMeetingRoute.js.map +1 -1
  67. package/dist/lib/util/SignalingHub.js +339 -0
  68. package/dist/lib/util/SignalingHub.js.map +1 -0
  69. package/dist/types/routes/BaseVideoMeetingRoute.d.ts +68 -46
  70. package/dist/types/util/SignalingHub.d.ts +112 -0
  71. package/package.json +208 -208
  72. package/apps/shared/push/GuestSignalingClient.ts +0 -359
  73. package/dist/apps/shared/push/GuestSignalingClient.d.ts +0 -98
  74. package/dist/apps/shared/push/GuestSignalingClient.js +0 -312
@@ -1,212 +1,213 @@
1
- ///////////////////////////////////////////////////////////////////////////////
2
- // Copyright (C) 2026 Jean-Philippe Steinmetz. All rights reserved.
3
- // SPDX-License-Identifier: MPL-2.0
4
- ///////////////////////////////////////////////////////////////////////////////
5
- /**
6
- * Typed client for this plugin's one public, unauthenticated endpoint - `GET /api/mail/video-meetings/join/:token`
7
- * (`BaseVideoMeetingRoute.join()`) - mirroring `booking-plugin`'s own `apps/shared/bookingApi.ts` convention of
8
- * redefining the response shape locally rather than importing it from `../../src/...`: `apps/` compiles under its
9
- * own `tsconfig.apps.json` (bundler resolution, DOM-facing), independent of the backend's `tsconfig.json` (NodeNext),
10
- * and every other plugin app in this codebase keeps that boundary the same way.
11
- */
12
- import { apiFetch } from "@rapidmx/web-client/lib/util/api.js";
13
-
14
- export type VideoMeetingVisibility = "private" | "public";
15
- export type VideoMeetingStatus = "scheduled" | "active" | "ended" | "cancelled";
16
-
17
- /** Mirrors `BaseVideoMeetingRoute.ts`'s `IceServerConfig` - already the exact shape `RTCPeerConnection`'s
18
- * `iceServers` constructor option expects, passed straight through with no reshaping. */
19
- export interface IceServerConfig {
20
- urls: string | string[];
21
- username?: string;
22
- credential?: string;
23
- }
24
-
25
- /** Mirrors `BaseVideoMeetingRoute.ts`'s `PublicVideoMeeting`. */
26
- export interface PublicVideoMeeting {
27
- uid: string;
28
- title: string;
29
- visibility: VideoMeetingVisibility;
30
- status: VideoMeetingStatus;
31
- hostDisplayName?: string;
32
- /** The account uid that owns the meeting - the sole source of a "host" identity on the client. Absent when the
33
- * server can't resolve one (an orphaned mailbox, or a server that predates this field), in which case no host
34
- * controls are shown to anyone. */
35
- hostUid?: string;
36
- /** When `true`, a joining participant's microphone starts muted - see `useLocalMedia`'s `forceMuteOnJoin`
37
- * option, which this is threaded into. Absent reads as `false`. */
38
- forceMuteOnJoin?: boolean;
39
- /** Whether this meeting currently requires a password - never the password or its hash, just whether one is
40
- * set. Absent reads as `false`. */
41
- hasPassword?: boolean;
42
- /** Whether this meeting currently requires the host to admit each participant before they join - see
43
- * `requestAdmission()`. Absent reads as `false`. */
44
- waitingRoomEnabled?: boolean;
45
- }
46
-
47
- /**
48
- * Mirrors `BaseVideoMeetingRoute.ts`'s `VideoMeetingJoinResult`. `authenticated: true` means the visiting browser
49
- * already held a valid session for a real RapidMX identity when it called `join()` - `token`/`expiresAt` are then
50
- * omitted entirely (there is no guest token to hand over), and the caller (`[token].tsx`) must not write any
51
- * cookie of its own before connecting to `/push`: the browser's own already-existing `jwt` session cookie already
52
- * authenticates it there, with zero new client-side auth handling. `authenticated: false` (the common anonymous
53
- * case) is unchanged from Phase 1: `token`/`expiresAt` are present, and `GuestSignalingClient` applies `token` as a
54
- * cookie before connecting.
55
- */
56
- export interface VideoMeetingJoinResult {
57
- meeting: PublicVideoMeeting;
58
- iceServers: IceServerConfig[];
59
- authenticated: boolean;
60
- /** The identity to identify as on the signaling channel/mesh - the caller's own real uid when `authenticated`
61
- * is `true`, otherwise a freshly minted synthetic `guest:<random>` uid. Always present. */
62
- selfUid: string;
63
- /** A short-lived guest JWT, usable against `/push`. Present only when `authenticated` is `false`. */
64
- token?: string;
65
- /** ISO 8601 instant `token` expires at. Present only when `authenticated` is `false`. */
66
- expiresAt?: string;
67
- /** Whether the server offers the WebSocket media relay - the last-resort path for a participant neither a direct
68
- * connection nor the TURN server can reach. Absent from a server that predates it, which reads as off. */
69
- relayEnabled?: boolean;
70
- /** The base URL the video filters' machine-learning runtime and models are hosted under, when the administrator
71
- * hosts them (`mail:videoconf:effects:assets_url`). Absent means the public CDN defaults. */
72
- effectsAssetsUrl?: string;
73
- }
74
-
75
- /** Mirrors `BaseVideoMeetingRoute.ts`'s `VideoMeetingPasswordRequiredResult` - returned by `joinMeeting()` in place
76
- * of a `VideoMeetingJoinResult` when the meeting requires a password that hasn't been submitted yet. Carries only
77
- * enough to show a password prompt (the meeting's title/host); nothing here grants anything. Narrow on
78
- * `"requiresPassword" in result && result.requiresPassword`. */
79
- export interface VideoMeetingPasswordRequiredResult {
80
- meeting: PublicVideoMeeting;
81
- requiresPassword: true;
82
- }
83
-
84
- /** Mirrors `BaseVideoMeetingRoute.ts`'s `VideoMeetingAdmissionRequiredResult` - returned by `requestAdmission()`
85
- * (and re-returned by `pollAdmission()`) while a `waitingRoomEnabled` meeting's admission request is still
86
- * pending. Unlike `VideoMeetingPasswordRequiredResult`, this carries `selfUid` (and, for a guest, `token`) - a
87
- * caller needs a stable, authenticated identity to poll with. `joinMeeting()` itself also answers this shape, with
88
- * none of those fields, when a waiting room applies and no request has been filed yet. Narrow on
89
- * `"requiresAdmission" in result`. */
90
- export interface VideoMeetingAdmissionRequiredResult {
91
- meeting: PublicVideoMeeting;
92
- requiresAdmission: true;
93
- authenticated?: boolean;
94
- selfUid?: string;
95
- token?: string;
96
- expiresAt?: string;
97
- }
98
-
99
- /** `joinMeeting()`'s actual return type - see `VideoMeetingPasswordRequiredResult`'s and
100
- * `VideoMeetingAdmissionRequiredResult`'s own doc comments. */
101
- export type VideoMeetingJoinResponse = VideoMeetingJoinResult | VideoMeetingPasswordRequiredResult | VideoMeetingAdmissionRequiredResult;
102
-
103
- /** Resolves a join token (an invitee's own link) or a public meeting's slug to its meeting info, ICE servers, and
104
- * either a short-lived guest signaling token (the common anonymous case) or confirmation that the caller's own
105
- * existing session already grants them signaling access (`authenticated: true` - see
106
- * `VideoMeetingJoinResult`'s doc comment) - see `BaseVideoMeetingRoute.join()`'s own doc comment. Rejects with
107
- * `ApiRequestError` (status `404`) for a stale/unknown/malformed token, cancelled meeting, or the wrong
108
- * private/public token shape for what it's being used against - the caller (`[token].tsx`) shows a single,
109
- * friendly "this meeting link isn't valid" state for all of those rather than trying to distinguish them, matching
110
- * `BaseVideoMeetingRoute.requireMeetingByToken()`'s own explicit "never leak which almost-matched" posture. */
111
- export function joinMeeting(token: string): Promise<VideoMeetingJoinResponse> {
112
- return apiFetch(`/mail/video-meetings/join/${encodeURIComponent(token)}`);
113
- }
114
-
115
- /** Submits a password for a meeting `joinMeeting()` reported `requiresPassword` for. On success, returns an
116
- * ordinary granted `VideoMeetingJoinResult` - the same shape and same meaning as `joinMeeting()`'s own non-password
117
- * case. Rejects with `ApiRequestError` (status `403`) for a wrong or missing password. */
118
- export function verifyMeetingPassword(token: string, password: string): Promise<VideoMeetingJoinResult> {
119
- return apiFetch(`/mail/video-meetings/join/${encodeURIComponent(token)}/verify`, {
120
- method: "POST",
121
- body: JSON.stringify({ password }),
122
- });
123
- }
124
-
125
- /** Sets, replaces, or removes (`password: null`) the meeting's join password - host only, enforced the same way
126
- * as every other owner-side video-meeting update. Hashed server-side; the plaintext is never stored or returned. */
127
- export function setMeetingPassword(meetingUid: string, password: string | null): Promise<void> {
128
- return apiFetch(`/mail/video-meetings/${encodeURIComponent(meetingUid)}`, {
129
- method: "PUT",
130
- body: JSON.stringify({ password }),
131
- });
132
- }
133
-
134
- /** Files an admission request for a meeting `joinMeeting()` reported `requiresAdmission` for - `password` only
135
- * when `meeting.hasPassword` is also set (both are checked together on this one call, never in two steps). On
136
- * success returns `VideoMeetingAdmissionRequiredResult` with a `selfUid` (and, for a guest, `token`) to poll with
137
- * via `pollAdmission()` - nothing is granted yet. Rejects with `ApiRequestError` (status `403`) for a wrong
138
- * password, or `400` if `meeting.hasPassword` and no password was given. */
139
- export function requestAdmission(token: string, body: { name: string; password?: string }): Promise<VideoMeetingAdmissionRequiredResult> {
140
- return apiFetch(`/mail/video-meetings/join/${encodeURIComponent(token)}/verify`, {
141
- method: "POST",
142
- body: JSON.stringify(body),
143
- });
144
- }
145
-
146
- /** Polls whether the host has responded to a pending admission request - authenticated as whichever identity
147
- * `requestAdmission()` (or a previous poll) returned: `guestToken`, when given, is sent as an `Authorization`
148
- * header (a guest has no session cookie of their own); omitted for an already-authenticated real caller, whose
149
- * existing session cookie already identifies them. Resolves to a granted `VideoMeetingJoinResult` once admitted,
150
- * or re-resolves to `VideoMeetingAdmissionRequiredResult` while still pending. Rejects with `ApiRequestError`
151
- * (status `403`) once denied, or `404` if this identity never requested admission (or already polled past a final
152
- * answer). */
153
- export function pollAdmission(token: string, guestToken?: string): Promise<VideoMeetingJoinResult | VideoMeetingAdmissionRequiredResult> {
154
- return apiFetch(`/mail/video-meetings/join/${encodeURIComponent(token)}/status`, {
155
- headers: guestToken ? { Authorization: `jwt ${guestToken}` } : undefined,
156
- });
157
- }
158
-
159
- /** One pending admission request, as `listWaitingParticipants()` returns it. */
160
- export interface WaitingParticipant {
161
- uid: string;
162
- name: string;
163
- requestedAt: string;
164
- }
165
-
166
- /** Lists a meeting's pending admission requests, oldest first - host only. */
167
- export function listWaitingParticipants(meetingUid: string): Promise<WaitingParticipant[]> {
168
- return apiFetch(`/mail/video-meetings/${encodeURIComponent(meetingUid)}/waiting`);
169
- }
170
-
171
- /** Admits a pending admission request - host only. The requester's own next `pollAdmission()` call completes
172
- * their join; this alone does not notify them of anything. A no-op if `uid` has no pending request. */
173
- export function admitParticipant(meetingUid: string, uid: string): Promise<void> {
174
- return apiFetch(`/mail/video-meetings/${encodeURIComponent(meetingUid)}/admit/${encodeURIComponent(uid)}`, { method: "POST" });
175
- }
176
-
177
- /** Denies a pending admission request - host only. The requester's own next `pollAdmission()` call then rejects
178
- * with a 403. A no-op if `uid` has no pending request. */
179
- export function denyParticipant(meetingUid: string, uid: string): Promise<void> {
180
- return apiFetch(`/mail/video-meetings/${encodeURIComponent(meetingUid)}/deny/${encodeURIComponent(uid)}`, { method: "POST" });
181
- }
182
-
183
- /** Sets whether the meeting requires the host to admit each participant (`PublicVideoMeeting.waitingRoomEnabled`) -
184
- * host only, enforced the same way as every other owner-side video-meeting update. Takes effect for anyone who
185
- * joins (or reloads the join link and joins again) after this call resolves. */
186
- export function setWaitingRoomEnabled(meetingUid: string, waitingRoomEnabled: boolean): Promise<void> {
187
- return apiFetch(`/mail/video-meetings/${encodeURIComponent(meetingUid)}`, {
188
- method: "PUT",
189
- body: JSON.stringify({ waitingRoomEnabled }),
190
- });
191
- }
192
-
193
- /** Removes a participant from the meeting - host only, enforced server-side with the caller's own authenticated
194
- * session (the same mailbox-ACL check every other owner-side video-meeting route uses), not by anything the
195
- * client claims. Revokes `uid`'s grant on the meeting's own push channel, so they can no longer signal or relay
196
- * media even if their client ignores the cooperative "kicked" signal a host's UI sends alongside this
197
- * (`MeshConnectionManager.sendKick()`). `uid` is the participant's real account uid (`MeshParticipant.uid` minus
198
- * its tab-scoped suffix - see `_CallView.tsx`'s `accountUidOf()`), not their tab-scoped peer id. */
199
- export function kickParticipant(meetingUid: string, uid: string): Promise<void> {
200
- return apiFetch(`/mail/video-meetings/${encodeURIComponent(meetingUid)}/kick/${encodeURIComponent(uid)}`, { method: "POST" });
201
- }
202
-
203
- /** Sets whether a newly joining participant starts muted (`PublicVideoMeeting.forceMuteOnJoin`) - host only,
204
- * enforced the same way as every other owner-side video-meeting update. Takes effect for anyone who joins (or
205
- * reloads the join link and joins again) after this call resolves; it does not retroactively mute anyone already
206
- * in the call - the host's own "Mute" button (see `kickParticipant()`) is the tool for that. */
207
- export function setForceMuteOnJoin(meetingUid: string, forceMuteOnJoin: boolean): Promise<void> {
208
- return apiFetch(`/mail/video-meetings/${encodeURIComponent(meetingUid)}`, {
209
- method: "PUT",
210
- body: JSON.stringify({ forceMuteOnJoin }),
211
- });
212
- }
1
+ ///////////////////////////////////////////////////////////////////////////////
2
+ // Copyright (C) 2026 Jean-Philippe Steinmetz. All rights reserved.
3
+ // SPDX-License-Identifier: MPL-2.0
4
+ ///////////////////////////////////////////////////////////////////////////////
5
+ /**
6
+ * Typed client for this plugin's one public, unauthenticated endpoint - `GET /api/mail/video-meetings/join/:token`
7
+ * (`BaseVideoMeetingRoute.join()`) - mirroring `booking-plugin`'s own `apps/shared/bookingApi.ts` convention of
8
+ * redefining the response shape locally rather than importing it from `../../src/...`: `apps/` compiles under its
9
+ * own `tsconfig.apps.json` (bundler resolution, DOM-facing), independent of the backend's `tsconfig.json` (NodeNext),
10
+ * and every other plugin app in this codebase keeps that boundary the same way.
11
+ */
12
+ import { apiFetch } from "@rapidmx/web-client/lib/util/api.js";
13
+
14
+ export type VideoMeetingVisibility = "private" | "public";
15
+ export type VideoMeetingStatus = "scheduled" | "active" | "ended" | "cancelled";
16
+
17
+ /** Mirrors `BaseVideoMeetingRoute.ts`'s `IceServerConfig` - already the exact shape `RTCPeerConnection`'s
18
+ * `iceServers` constructor option expects, passed straight through with no reshaping. */
19
+ export interface IceServerConfig {
20
+ urls: string | string[];
21
+ username?: string;
22
+ credential?: string;
23
+ }
24
+
25
+ /** Mirrors `BaseVideoMeetingRoute.ts`'s `PublicVideoMeeting`. */
26
+ export interface PublicVideoMeeting {
27
+ uid: string;
28
+ title: string;
29
+ visibility: VideoMeetingVisibility;
30
+ status: VideoMeetingStatus;
31
+ hostDisplayName?: string;
32
+ /** The account uid that owns the meeting - the sole source of a "host" identity on the client. Absent when the
33
+ * server can't resolve one (an orphaned mailbox, or a server that predates this field), in which case no host
34
+ * controls are shown to anyone. */
35
+ hostUid?: string;
36
+ /** When `true`, a joining participant's microphone starts muted - see `useLocalMedia`'s `forceMuteOnJoin`
37
+ * option, which this is threaded into. Absent reads as `false`. */
38
+ forceMuteOnJoin?: boolean;
39
+ /** Whether this meeting currently requires a password - never the password or its hash, just whether one is
40
+ * set. Absent reads as `false`. */
41
+ hasPassword?: boolean;
42
+ /** Whether this meeting currently requires the host to admit each participant before they join - see
43
+ * `requestAdmission()`. Absent reads as `false`. */
44
+ waitingRoomEnabled?: boolean;
45
+ }
46
+
47
+ /**
48
+ * Mirrors `BaseVideoMeetingRoute.ts`'s `VideoMeetingJoinResult`. `authenticated: true` means the visiting browser
49
+ * already held a valid session for a real RapidMX identity when it called `join()` - `token`/`expiresAt` are then
50
+ * omitted entirely (there is no guest token to hand over), and the caller (`[token].tsx`) must not write any
51
+ * cookie of its own before opening the signaling socket: the browser's own already-existing `jwt` session cookie
52
+ * already authenticates it there, with zero new client-side auth handling. `authenticated: false` (the common
53
+ * anonymous case) is unchanged from Phase 1: `token`/`expiresAt` are present, and `SignalingClient` applies `token` as
54
+ * a cookie before connecting.
55
+ */
56
+ export interface VideoMeetingJoinResult {
57
+ meeting: PublicVideoMeeting;
58
+ iceServers: IceServerConfig[];
59
+ authenticated: boolean;
60
+ /** The identity to identify as on the signaling channel/mesh - the caller's own real uid when `authenticated`
61
+ * is `true`, otherwise a freshly minted synthetic `guest:<random>` uid. Always present. */
62
+ selfUid: string;
63
+ /** A short-lived guest JWT, usable against the meeting's signaling and relay sockets. Present only when
64
+ * `authenticated` is `false`. */
65
+ token?: string;
66
+ /** ISO 8601 instant `token` expires at. Present only when `authenticated` is `false`. */
67
+ expiresAt?: string;
68
+ /** Whether the server offers the WebSocket media relay - the last-resort path for a participant neither a direct
69
+ * connection nor the TURN server can reach. Absent from a server that predates it, which reads as off. */
70
+ relayEnabled?: boolean;
71
+ /** The base URL the video filters' machine-learning runtime and models are hosted under, when the administrator
72
+ * hosts them (`mail:videoconf:effects:assets_url`). Absent means the public CDN defaults. */
73
+ effectsAssetsUrl?: string;
74
+ }
75
+
76
+ /** Mirrors `BaseVideoMeetingRoute.ts`'s `VideoMeetingPasswordRequiredResult` - returned by `joinMeeting()` in place
77
+ * of a `VideoMeetingJoinResult` when the meeting requires a password that hasn't been submitted yet. Carries only
78
+ * enough to show a password prompt (the meeting's title/host); nothing here grants anything. Narrow on
79
+ * `"requiresPassword" in result && result.requiresPassword`. */
80
+ export interface VideoMeetingPasswordRequiredResult {
81
+ meeting: PublicVideoMeeting;
82
+ requiresPassword: true;
83
+ }
84
+
85
+ /** Mirrors `BaseVideoMeetingRoute.ts`'s `VideoMeetingAdmissionRequiredResult` - returned by `requestAdmission()`
86
+ * (and re-returned by `pollAdmission()`) while a `waitingRoomEnabled` meeting's admission request is still
87
+ * pending. Unlike `VideoMeetingPasswordRequiredResult`, this carries `selfUid` (and, for a guest, `token`) - a
88
+ * caller needs a stable, authenticated identity to poll with. `joinMeeting()` itself also answers this shape, with
89
+ * none of those fields, when a waiting room applies and no request has been filed yet. Narrow on
90
+ * `"requiresAdmission" in result`. */
91
+ export interface VideoMeetingAdmissionRequiredResult {
92
+ meeting: PublicVideoMeeting;
93
+ requiresAdmission: true;
94
+ authenticated?: boolean;
95
+ selfUid?: string;
96
+ token?: string;
97
+ expiresAt?: string;
98
+ }
99
+
100
+ /** `joinMeeting()`'s actual return type - see `VideoMeetingPasswordRequiredResult`'s and
101
+ * `VideoMeetingAdmissionRequiredResult`'s own doc comments. */
102
+ export type VideoMeetingJoinResponse = VideoMeetingJoinResult | VideoMeetingPasswordRequiredResult | VideoMeetingAdmissionRequiredResult;
103
+
104
+ /** Resolves a join token (an invitee's own link) or a public meeting's slug to its meeting info, ICE servers, and
105
+ * either a short-lived guest signaling token (the common anonymous case) or confirmation that the caller's own
106
+ * existing session already grants them signaling access (`authenticated: true` - see
107
+ * `VideoMeetingJoinResult`'s doc comment) - see `BaseVideoMeetingRoute.join()`'s own doc comment. Rejects with
108
+ * `ApiRequestError` (status `404`) for a stale/unknown/malformed token, cancelled meeting, or the wrong
109
+ * private/public token shape for what it's being used against - the caller (`[token].tsx`) shows a single,
110
+ * friendly "this meeting link isn't valid" state for all of those rather than trying to distinguish them, matching
111
+ * `BaseVideoMeetingRoute.requireMeetingByToken()`'s own explicit "never leak which almost-matched" posture. */
112
+ export function joinMeeting(token: string): Promise<VideoMeetingJoinResponse> {
113
+ return apiFetch(`/mail/video-meetings/join/${encodeURIComponent(token)}`);
114
+ }
115
+
116
+ /** Submits a password for a meeting `joinMeeting()` reported `requiresPassword` for. On success, returns an
117
+ * ordinary granted `VideoMeetingJoinResult` - the same shape and same meaning as `joinMeeting()`'s own non-password
118
+ * case. Rejects with `ApiRequestError` (status `403`) for a wrong or missing password. */
119
+ export function verifyMeetingPassword(token: string, password: string): Promise<VideoMeetingJoinResult> {
120
+ return apiFetch(`/mail/video-meetings/join/${encodeURIComponent(token)}/verify`, {
121
+ method: "POST",
122
+ body: JSON.stringify({ password }),
123
+ });
124
+ }
125
+
126
+ /** Sets, replaces, or removes (`password: null`) the meeting's join password - host only, enforced the same way
127
+ * as every other owner-side video-meeting update. Hashed server-side; the plaintext is never stored or returned. */
128
+ export function setMeetingPassword(meetingUid: string, password: string | null): Promise<void> {
129
+ return apiFetch(`/mail/video-meetings/${encodeURIComponent(meetingUid)}`, {
130
+ method: "PUT",
131
+ body: JSON.stringify({ password }),
132
+ });
133
+ }
134
+
135
+ /** Files an admission request for a meeting `joinMeeting()` reported `requiresAdmission` for - `password` only
136
+ * when `meeting.hasPassword` is also set (both are checked together on this one call, never in two steps). On
137
+ * success returns `VideoMeetingAdmissionRequiredResult` with a `selfUid` (and, for a guest, `token`) to poll with
138
+ * via `pollAdmission()` - nothing is granted yet. Rejects with `ApiRequestError` (status `403`) for a wrong
139
+ * password, or `400` if `meeting.hasPassword` and no password was given. */
140
+ export function requestAdmission(token: string, body: { name: string; password?: string }): Promise<VideoMeetingAdmissionRequiredResult> {
141
+ return apiFetch(`/mail/video-meetings/join/${encodeURIComponent(token)}/verify`, {
142
+ method: "POST",
143
+ body: JSON.stringify(body),
144
+ });
145
+ }
146
+
147
+ /** Polls whether the host has responded to a pending admission request - authenticated as whichever identity
148
+ * `requestAdmission()` (or a previous poll) returned: `guestToken`, when given, is sent as an `Authorization`
149
+ * header (a guest has no session cookie of their own); omitted for an already-authenticated real caller, whose
150
+ * existing session cookie already identifies them. Resolves to a granted `VideoMeetingJoinResult` once admitted,
151
+ * or re-resolves to `VideoMeetingAdmissionRequiredResult` while still pending. Rejects with `ApiRequestError`
152
+ * (status `403`) once denied, or `404` if this identity never requested admission (or already polled past a final
153
+ * answer). */
154
+ export function pollAdmission(token: string, guestToken?: string): Promise<VideoMeetingJoinResult | VideoMeetingAdmissionRequiredResult> {
155
+ return apiFetch(`/mail/video-meetings/join/${encodeURIComponent(token)}/status`, {
156
+ headers: guestToken ? { Authorization: `jwt ${guestToken}` } : undefined,
157
+ });
158
+ }
159
+
160
+ /** One pending admission request, as `listWaitingParticipants()` returns it. */
161
+ export interface WaitingParticipant {
162
+ uid: string;
163
+ name: string;
164
+ requestedAt: string;
165
+ }
166
+
167
+ /** Lists a meeting's pending admission requests, oldest first - host only. */
168
+ export function listWaitingParticipants(meetingUid: string): Promise<WaitingParticipant[]> {
169
+ return apiFetch(`/mail/video-meetings/${encodeURIComponent(meetingUid)}/waiting`);
170
+ }
171
+
172
+ /** Admits a pending admission request - host only. The requester's own next `pollAdmission()` call completes
173
+ * their join; this alone does not notify them of anything. A no-op if `uid` has no pending request. */
174
+ export function admitParticipant(meetingUid: string, uid: string): Promise<void> {
175
+ return apiFetch(`/mail/video-meetings/${encodeURIComponent(meetingUid)}/admit/${encodeURIComponent(uid)}`, { method: "POST" });
176
+ }
177
+
178
+ /** Denies a pending admission request - host only. The requester's own next `pollAdmission()` call then rejects
179
+ * with a 403. A no-op if `uid` has no pending request. */
180
+ export function denyParticipant(meetingUid: string, uid: string): Promise<void> {
181
+ return apiFetch(`/mail/video-meetings/${encodeURIComponent(meetingUid)}/deny/${encodeURIComponent(uid)}`, { method: "POST" });
182
+ }
183
+
184
+ /** Sets whether the meeting requires the host to admit each participant (`PublicVideoMeeting.waitingRoomEnabled`) -
185
+ * host only, enforced the same way as every other owner-side video-meeting update. Takes effect for anyone who
186
+ * joins (or reloads the join link and joins again) after this call resolves. */
187
+ export function setWaitingRoomEnabled(meetingUid: string, waitingRoomEnabled: boolean): Promise<void> {
188
+ return apiFetch(`/mail/video-meetings/${encodeURIComponent(meetingUid)}`, {
189
+ method: "PUT",
190
+ body: JSON.stringify({ waitingRoomEnabled }),
191
+ });
192
+ }
193
+
194
+ /** Removes a participant from the meeting - host only, enforced server-side with the caller's own authenticated
195
+ * session (the same mailbox-ACL check every other owner-side video-meeting route uses), not by anything the
196
+ * client claims. Revokes `uid`'s grant on the meeting and closes every signaling socket they hold in it, so they can
197
+ * no longer signal or relay media even if their client ignores the cooperative "kicked" signal a host's UI sends alongside this
198
+ * (`MeshConnectionManager.sendKick()`). `uid` is the participant's real account uid (`MeshParticipant.uid` minus
199
+ * its tab-scoped suffix - see `_CallView.tsx`'s `accountUidOf()`), not their tab-scoped peer id. */
200
+ export function kickParticipant(meetingUid: string, uid: string): Promise<void> {
201
+ return apiFetch(`/mail/video-meetings/${encodeURIComponent(meetingUid)}/kick/${encodeURIComponent(uid)}`, { method: "POST" });
202
+ }
203
+
204
+ /** Sets whether a newly joining participant starts muted (`PublicVideoMeeting.forceMuteOnJoin`) - host only,
205
+ * enforced the same way as every other owner-side video-meeting update. Takes effect for anyone who joins (or
206
+ * reloads the join link and joins again) after this call resolves; it does not retroactively mute anyone already
207
+ * in the call - the host's own "Mute" button (see `kickParticipant()`) is the tool for that. */
208
+ export function setForceMuteOnJoin(meetingUid: string, forceMuteOnJoin: boolean): Promise<void> {
209
+ return apiFetch(`/mail/video-meetings/${encodeURIComponent(meetingUid)}`, {
210
+ method: "PUT",
211
+ body: JSON.stringify({ forceMuteOnJoin }),
212
+ });
213
+ }