@rapidmx/meet-plugin 0.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 (108) hide show
  1. package/LICENSE +382 -0
  2. package/README.md +38 -0
  3. package/apps/meet/[token].tsx +144 -0
  4. package/apps/meet/_CallControls.tsx +69 -0
  5. package/apps/meet/_CallView.tsx +338 -0
  6. package/apps/meet/_MeetChrome.tsx +30 -0
  7. package/apps/meet/_MeetLobby.tsx +223 -0
  8. package/apps/meet/_ParticipantTile.tsx +66 -0
  9. package/apps/meet/_layout.tsx +34 -0
  10. package/apps/meet/_meetApi.ts +66 -0
  11. package/apps/meet/index.tsx +22 -0
  12. package/apps/settings-video-conferencing/_PersonalRoomCard.tsx +140 -0
  13. package/apps/settings-video-conferencing/_layout.tsx +47 -0
  14. package/apps/settings-video-conferencing/index.tsx +192 -0
  15. package/apps/shared/media/activeSpeaker.ts +45 -0
  16. package/apps/shared/media/deviceMedia.ts +141 -0
  17. package/apps/shared/media/levelMeter.ts +91 -0
  18. package/apps/shared/push/GuestSignalingClient.ts +338 -0
  19. package/apps/shared/webrtc/MeshConnectionManager.ts +350 -0
  20. package/apps/shared/webrtc/realPeerConnection.ts +37 -0
  21. package/apps/shared/webrtc/types.ts +90 -0
  22. package/dist/apps/meet/[token].d.ts +33 -0
  23. package/dist/apps/meet/[token].js +101 -0
  24. package/dist/apps/meet/_CallControls.d.ts +18 -0
  25. package/dist/apps/meet/_CallControls.js +6 -0
  26. package/dist/apps/meet/_CallView.d.ts +41 -0
  27. package/dist/apps/meet/_CallView.js +225 -0
  28. package/dist/apps/meet/_MeetChrome.d.ts +13 -0
  29. package/dist/apps/meet/_MeetChrome.js +10 -0
  30. package/dist/apps/meet/_MeetLobby.d.ts +29 -0
  31. package/dist/apps/meet/_MeetLobby.js +120 -0
  32. package/dist/apps/meet/_ParticipantTile.d.ts +18 -0
  33. package/dist/apps/meet/_ParticipantTile.js +25 -0
  34. package/dist/apps/meet/_layout.d.ts +9 -0
  35. package/dist/apps/meet/_layout.js +8 -0
  36. package/dist/apps/meet/_meetApi.d.ts +47 -0
  37. package/dist/apps/meet/_meetApi.js +23 -0
  38. package/dist/apps/meet/index.d.ts +5 -0
  39. package/dist/apps/meet/index.js +8 -0
  40. package/dist/apps/settings-video-conferencing/_PersonalRoomCard.d.ts +24 -0
  41. package/dist/apps/settings-video-conferencing/_PersonalRoomCard.js +77 -0
  42. package/dist/apps/settings-video-conferencing/_layout.d.ts +16 -0
  43. package/dist/apps/settings-video-conferencing/_layout.js +8 -0
  44. package/dist/apps/settings-video-conferencing/index.d.ts +16 -0
  45. package/dist/apps/settings-video-conferencing/index.js +92 -0
  46. package/dist/apps/shared/media/activeSpeaker.d.ts +20 -0
  47. package/dist/apps/shared/media/activeSpeaker.js +39 -0
  48. package/dist/apps/shared/media/deviceMedia.d.ts +65 -0
  49. package/dist/apps/shared/media/deviceMedia.js +110 -0
  50. package/dist/apps/shared/media/levelMeter.d.ts +40 -0
  51. package/dist/apps/shared/media/levelMeter.js +61 -0
  52. package/dist/apps/shared/push/GuestSignalingClient.d.ts +89 -0
  53. package/dist/apps/shared/push/GuestSignalingClient.js +291 -0
  54. package/dist/apps/shared/webrtc/MeshConnectionManager.d.ts +97 -0
  55. package/dist/apps/shared/webrtc/MeshConnectionManager.js +262 -0
  56. package/dist/apps/shared/webrtc/realPeerConnection.d.ts +7 -0
  57. package/dist/apps/shared/webrtc/realPeerConnection.js +26 -0
  58. package/dist/apps/shared/webrtc/types.d.ts +94 -0
  59. package/dist/apps/shared/webrtc/types.js +9 -0
  60. package/dist/lib/index.js +25 -0
  61. package/dist/lib/index.js.map +1 -0
  62. package/dist/lib/models/mongo/VideoMeetingInviteeMongo.js +87 -0
  63. package/dist/lib/models/mongo/VideoMeetingInviteeMongo.js.map +1 -0
  64. package/dist/lib/models/mongo/VideoMeetingMongo.js +126 -0
  65. package/dist/lib/models/mongo/VideoMeetingMongo.js.map +1 -0
  66. package/dist/lib/models/sql/VideoMeetingInviteeSQL.js +87 -0
  67. package/dist/lib/models/sql/VideoMeetingInviteeSQL.js.map +1 -0
  68. package/dist/lib/models/sql/VideoMeetingSQL.js +127 -0
  69. package/dist/lib/models/sql/VideoMeetingSQL.js.map +1 -0
  70. package/dist/lib/models/types.js +19 -0
  71. package/dist/lib/models/types.js.map +1 -0
  72. package/dist/lib/mongo.js +13 -0
  73. package/dist/lib/mongo.js.map +1 -0
  74. package/dist/lib/routes/BaseVideoMeetingRoute.js +743 -0
  75. package/dist/lib/routes/BaseVideoMeetingRoute.js.map +1 -0
  76. package/dist/lib/routes/mongo/VideoMeetingRouteMongo.js +31 -0
  77. package/dist/lib/routes/mongo/VideoMeetingRouteMongo.js.map +1 -0
  78. package/dist/lib/routes/sql/VideoMeetingRouteSQL.js +31 -0
  79. package/dist/lib/routes/sql/VideoMeetingRouteSQL.js.map +1 -0
  80. package/dist/lib/sql.js +13 -0
  81. package/dist/lib/sql.js.map +1 -0
  82. package/dist/lib/util/BookingIntegrationUtils.js +115 -0
  83. package/dist/lib/util/BookingIntegrationUtils.js.map +1 -0
  84. package/dist/lib/util/IceServerUtils.js +68 -0
  85. package/dist/lib/util/IceServerUtils.js.map +1 -0
  86. package/dist/lib/util/PublicUrlUtils.js +37 -0
  87. package/dist/lib/util/PublicUrlUtils.js.map +1 -0
  88. package/dist/lib/util/RouteAccessUtils.js +32 -0
  89. package/dist/lib/util/RouteAccessUtils.js.map +1 -0
  90. package/dist/lib/util/TokenUtils.js +33 -0
  91. package/dist/lib/util/TokenUtils.js.map +1 -0
  92. package/dist/types/index.d.ts +20 -0
  93. package/dist/types/models/mongo/VideoMeetingInviteeMongo.d.ts +16 -0
  94. package/dist/types/models/mongo/VideoMeetingMongo.d.ts +23 -0
  95. package/dist/types/models/sql/VideoMeetingInviteeSQL.d.ts +16 -0
  96. package/dist/types/models/sql/VideoMeetingSQL.d.ts +23 -0
  97. package/dist/types/models/types.d.ts +131 -0
  98. package/dist/types/mongo.d.ts +8 -0
  99. package/dist/types/routes/BaseVideoMeetingRoute.d.ts +368 -0
  100. package/dist/types/routes/mongo/VideoMeetingRouteMongo.d.ts +10 -0
  101. package/dist/types/routes/sql/VideoMeetingRouteSQL.d.ts +10 -0
  102. package/dist/types/sql.d.ts +8 -0
  103. package/dist/types/util/BookingIntegrationUtils.d.ts +51 -0
  104. package/dist/types/util/IceServerUtils.d.ts +57 -0
  105. package/dist/types/util/PublicUrlUtils.d.ts +11 -0
  106. package/dist/types/util/RouteAccessUtils.d.ts +8 -0
  107. package/dist/types/util/TokenUtils.d.ts +23 -0
  108. package/package.json +181 -0
@@ -0,0 +1,131 @@
1
+ import type { BaseEntity } from "@rapidrest/service-core";
2
+ /** Whether a `VideoMeeting` is joinable only by its explicitly invited `VideoMeetingInvitee`s (each with their
3
+ * own unguessable `joinToken`), or by anyone holding the meeting's single `publicSlug` link. */
4
+ export declare enum VideoMeetingVisibility {
5
+ PRIVATE = "private",
6
+ PUBLIC = "public"
7
+ }
8
+ /** The lifecycle of a `VideoMeeting`. Deliberately minimal for Phase 1 - `ACTIVE` and `ENDED` are not yet set by
9
+ * any code in this package (no signaling event currently transitions a meeting automatically); they exist so the
10
+ * shape doesn't need to change once a later phase starts using them. `CANCELLED` is the one transition
11
+ * `BaseVideoMeetingRoute.update()` actually performs today. */
12
+ export declare enum VideoMeetingStatus {
13
+ SCHEDULED = "scheduled",
14
+ ACTIVE = "active",
15
+ ENDED = "ended",
16
+ CANCELLED = "cancelled"
17
+ }
18
+ /**
19
+ * A WebRTC video meeting owned by a `Mailbox` - joinable directly via this plugin's own API (for now; a later
20
+ * phase wires an "Add video conferencing" hook into calendar event compose, see `calendarEventUid` below) or by
21
+ * anyone holding a valid link (a private invitee's `joinToken`, or a public meeting's `publicSlug`).
22
+ *
23
+ * Anonymous access to this entity is NOT granted through the `AccessControlList`'s class-level default (see
24
+ * `@Protect`'s deny-all `records` on the concrete `VideoMeetingMongo`/`VideoMeetingSQL`) - `BaseVideoMeetingRoute`
25
+ * does its own authorization: the owner's routes check `ACLUtils.hasPermission()` against the meeting's
26
+ * `mailboxUid` (with the caller's trusted roles stripped first - see `util/RouteAccessUtils.ts`), and the
27
+ * anonymous `join()` route resolves a caller by `joinToken`/`publicSlug` directly, the same posture
28
+ * `booking-plugin`'s `BaseBookingRoute` documents for `Booking`/`BookingType`.
29
+ *
30
+ * Unlike `Booking`, this entity DOES get its own per-record `AccessControlList` (`@Protect`'s `recordACL: true`) -
31
+ * its own `uid` doubles as its `/push` signaling channel (see `BaseVideoMeetingRoute`'s class doc comment), and a
32
+ * `/push` channel is authorized purely by whether a per-channel `AccessControlList` document grants the caller an
33
+ * action on it (`ACLUtils.hasPermission(user, channelUid, action)`); there would be no such document to check
34
+ * against at all without `recordACL: true`. Creating a meeting's own ACL with `parentUid` set to its `mailboxUid`
35
+ * (see `BaseVideoMeetingRoute.create()`) lets a mailbox owner/delegate reach it exactly as they reach any other
36
+ * mailbox-scoped record, through the normal ACL parent-chain inheritance - no separate mechanism is needed for
37
+ * the owner's own push access, only for a guest's (see `join()`'s doc comment).
38
+ *
39
+ * @author Jean-Philippe Steinmetz
40
+ */
41
+ export interface VideoMeeting extends BaseEntity {
42
+ /** The unique identifier of the `Mailbox` that owns this meeting. Managing it (create/list/delete/cancel) is
43
+ * permission-checked against this mailbox's `AccessControlList`, the same as every other mailbox-scoped child
44
+ * entity - see the architecture note on `Message.mailboxUid` in `@rapidmx/restapi`'s `models/types.ts`. */
45
+ mailboxUid: string;
46
+ /** The unique identifier of the `CalendarEvent` this meeting was minted for, if any. Optional in Phase 1: a
47
+ * meeting can exist with no calendar event at all (created directly through this plugin's own API, e.g. for
48
+ * testing); a later phase wires an "Add video conferencing" control into calendar event compose that sets
49
+ * this when it mints a meeting for a real invite. */
50
+ calendarEventUid?: string;
51
+ title: string;
52
+ visibility: VideoMeetingVisibility;
53
+ /**
54
+ * The public join link's unique identifier, set only when `visibility` is `PUBLIC`. Random rather than
55
+ * name-derived (unlike `booking-plugin`'s human-chosen `BookingType.slug`) - see `util/TokenUtils.ts`'s
56
+ * `mintPublicSlug()` doc comment for the exact shape/entropy and why.
57
+ *
58
+ * The concrete `VideoMeetingMongo`/`VideoMeetingSQL` classes index this unique **globally**, matching exactly
59
+ * what `BaseVideoMeetingRoute.join()`'s public URL actually looks up (`/join/:token`, not
60
+ * `/join/:mailboxUid/:token` - the lookup carries no mailbox segment at all). An initial design considered
61
+ * scoping the database constraint to the owning mailbox instead (mirroring `BookingType.slug`'s own scope,
62
+ * for consistency with `booking-plugin`'s precedent - see this package's `.claude/NOTES.md`'s Phase 1 entry),
63
+ * but a per-mailbox *compound* sparse index doesn't actually work: it still indexes a document carrying at
64
+ * least one of its keys, and every row has `mailboxUid`, so two *private* meetings in one mailbox (both
65
+ * missing `publicSlug`) would collide on `(mailboxUid, null)` and the second could never be created - a real
66
+ * bug, not just a weaker-than-intended constraint, found and fixed before release. A single-field sparse
67
+ * index skips a document missing the field entirely, which both fixes that bug and happens to match the
68
+ * lookup's real (global) scope exactly - see `organizerSlug` below, which hit the identical pitfall.
69
+ */
70
+ publicSlug?: string;
71
+ /**
72
+ * The organizer's own join link identifier, minted only for a `PRIVATE` meeting, at creation. It exists solely
73
+ * so the meeting's organizer has *some* token `BaseVideoMeetingRoute.requireMeetingByToken()` can resolve to
74
+ * their own meeting: a private meeting's only other resolvable credentials are its invitees' `joinToken`s, and
75
+ * the calendar integration that mints a meeting per event deliberately builds `invitees` from the event's
76
+ * attendees *excluding* the organizer (the organizer manages the meeting through ownership, not as a guest), so
77
+ * without this the organizer of their own private meeting would have no link that resolves at all.
78
+ *
79
+ * **Unlike `publicSlug`, holding this value is not by itself a credential.** Resolving a token through this
80
+ * field never grants anonymous or guest access: `BaseVideoMeetingRoute.join()` additionally requires a real,
81
+ * already-authenticated (non-guest) caller who holds `READ` on the meeting's own `mailboxUid`, and answers the
82
+ * same bare `404` as an entirely unknown token for anyone else - see that method's doc comment. The value is
83
+ * unguessable all the same (the same `mintPublicSlug()` shape/entropy as `publicSlug`, stored in its own
84
+ * column), but that unguessability is defense in depth here, not the authorization itself.
85
+ *
86
+ * A meeting's `visibility` never changes after creation, and neither does this field. It is never set for a
87
+ * `PUBLIC` meeting. It is, however, genuinely optional even for a private one: a private meeting minted through
88
+ * `createSingleInviteeVideoMeeting()` (`util/BookingIntegrationUtils.ts` - `booking-plugin`'s in-process
89
+ * integration, whose meetings are managed from the booking flow and have no organizer to hand a link to) has
90
+ * none, so code must check for its presence rather than infer it from `visibility` alone.
91
+ *
92
+ * The concrete `VideoMeetingMongo`/`VideoMeetingSQL` classes index this unique globally, single-field sparse -
93
+ * the same shape `publicSlug`'s own index was corrected to (see above); this field never had the per-mailbox
94
+ * version to begin with, having been added after that pitfall was already found.
95
+ */
96
+ organizerSlug?: string;
97
+ status: VideoMeetingStatus;
98
+ /** Informational only in Phase 1 - not used for any availability or conflict checking (a video meeting has no
99
+ * concept of "busy" the way a `Booking` does). */
100
+ startTime?: Date;
101
+ /** Informational only in Phase 1 - see `startTime`. */
102
+ endTime?: Date;
103
+ }
104
+ /**
105
+ * One invited participant of a `PRIVATE` `VideoMeeting`, minted server-side by `BaseVideoMeetingRoute.create()` -
106
+ * one row per `{ email, displayName? }` the owner supplied at creation time. `joinToken` is this invitee's only
107
+ * credential (mirroring `Booking.manageToken` exactly - see `util/TokenUtils.ts`'s `mintJoinToken()`): possession
108
+ * of the link is what lets an anonymous invitee reach `BaseVideoMeetingRoute.join()`, never an
109
+ * `AccessControlList` grant of its own (this entity's class ACL is deny-all, like `Booking`'s - see
110
+ * `@Protect` on the concrete `VideoMeetingInviteeMongo`/`VideoMeetingInviteeSQL`).
111
+ *
112
+ * @author Jean-Philippe Steinmetz
113
+ */
114
+ export interface VideoMeetingInvitee extends BaseEntity {
115
+ /** The unique identifier of the `VideoMeeting` this invitee belongs to. */
116
+ meetingUid: string;
117
+ /**
118
+ * The unique identifier of the host `Mailbox`, denormalized from the meeting - mirroring `Booking.mailboxUid`'s
119
+ * own doc comment exactly: this lets `ErasureExecutionJob` find and purge every invitee of an erased mailbox's
120
+ * meetings by `mailboxUid` alone (the `@MailboxScopedData()` contract), without a join back through
121
+ * `VideoMeeting`, and lets a host's invitees be listed without one either.
122
+ */
123
+ mailboxUid: string;
124
+ /** The invitee's email address, normalized to lowercase. */
125
+ email: string;
126
+ displayName?: string;
127
+ /** The unguessable token embedded in this invitee's join link, minted server-side (32 random bytes) and
128
+ * immutable thereafter. Like `Booking.manageToken`, this has no expiry and no GC job of its own - see
129
+ * `BaseVideoMeetingRoute`'s class doc comment for the same documented tradeoff. */
130
+ joinToken: string;
131
+ }
@@ -0,0 +1,8 @@
1
+ /**
2
+ * This plugin's `./mongo` entry point: exactly the classes a server host loads for a Mongo deployment - the video
3
+ * meeting models and the route mounted at `/api/mail/video-meetings`. Anything else exported here would be
4
+ * registered by the host too, so the abstract route and utilities stay in the package root.
5
+ */
6
+ export { VideoMeetingMongo } from "./models/mongo/VideoMeetingMongo.js";
7
+ export { VideoMeetingInviteeMongo } from "./models/mongo/VideoMeetingInviteeMongo.js";
8
+ export { VideoMeetingRouteMongo } from "./routes/mongo/VideoMeetingRouteMongo.js";
@@ -0,0 +1,368 @@
1
+ import { type JWTUser } from "@rapidrest/core";
2
+ import { HttpRequest } from "@rapidrest/service-core";
3
+ import { Mailbox } from "@rapidmx/restapi";
4
+ import { IceServerConfig } from "../util/IceServerUtils.js";
5
+ import { VideoMeeting, VideoMeetingInvitee, VideoMeetingStatus, VideoMeetingVisibility } from "../models/types.js";
6
+ /** How long a guest JWT minted by `join()` remains valid for. Four hours comfortably covers a realistic call's
7
+ * length (including some run-over) without a guest's session dying mid-call; there is no refresh mechanism in
8
+ * Phase 1, so a guest who is still on the call past this needs to re-open the join link. */
9
+ export declare const GUEST_JWT_TTL_SECONDS: number;
10
+ /** The fixed prefix every guest uid `mintGuestToken()` mints starts with - also how `join()` tells a genuine,
11
+ * already-authenticated RapidMX identity apart from a returning guest presenting a JWT from an earlier `join()`
12
+ * call (see `join()`'s doc comment): a guest uid is never a real mailbox-owning identity, so this prefix is a safe,
13
+ * cheap discriminator with no separate "is this a guest" flag needed anywhere. */
14
+ export declare const GUEST_UID_PREFIX = "guest:";
15
+ /** The request body accepted by `create()`. */
16
+ export interface CreateVideoMeetingBody {
17
+ mailboxUid?: string;
18
+ title?: string;
19
+ visibility?: string;
20
+ /** The `CalendarEvent` this meeting is being minted for, if any - see `VideoMeeting.calendarEventUid`. */
21
+ calendarEventUid?: string;
22
+ startTime?: string;
23
+ endTime?: string;
24
+ /** Required (and non-empty), only when `visibility` is `"private"`. */
25
+ invitees?: {
26
+ email?: string;
27
+ displayName?: string;
28
+ }[];
29
+ }
30
+ /** One invitee of a newly created private meeting, as returned by `create()` - everything a caller needs to build
31
+ * that invitee's own share of the calendar invite (Phase 3's job). */
32
+ export interface VideoMeetingInviteeJoinInfo {
33
+ uid: string;
34
+ email: string;
35
+ displayName?: string;
36
+ /** `undefined` when `mail:videoconf:public_url` isn't configured - see `BaseVideoMeetingRoute`'s class doc
37
+ * comment. */
38
+ joinUrl?: string;
39
+ }
40
+ /** `create()`'s response: the persisted meeting, plus whatever link(s) a caller needs to invite people to it. */
41
+ export interface VideoMeetingCreateResult<T extends VideoMeeting = VideoMeeting> {
42
+ meeting: T;
43
+ /** Present (possibly empty only if `invitees` validation somehow let that through - it never does, see
44
+ * `validateCreateBody()`) exactly when `meeting.visibility` is `"private"`. */
45
+ invitees?: VideoMeetingInviteeJoinInfo[];
46
+ /** Present exactly when `meeting.visibility` is `"public"`; `undefined` within that case only when
47
+ * `mail:videoconf:public_url` isn't configured. */
48
+ publicJoinUrl?: string;
49
+ /** The organizer's own join link, present exactly when `meeting.organizerSlug` is set - i.e. for every private
50
+ * meeting this route creates, *alongside* `invitees` rather than instead of it (`undefined` within that case
51
+ * only when `mail:videoconf:public_url` isn't configured). The organizer of a private meeting is deliberately
52
+ * never one of its own `invitees`, so this is the only link that resolves to the meeting for them - see
53
+ * `VideoMeeting.organizerSlug` and `join()`'s doc comment for why holding it is not by itself a credential. */
54
+ organizerJoinUrl?: string;
55
+ }
56
+ /** Which field of which row `requireMeetingByToken()` matched the caller's token against - `join()` authorizes each
57
+ * differently, so the resolution is returned explicitly rather than re-derived by comparing strings afterwards. */
58
+ export type VideoMeetingTokenResolution = "invitee" | "publicSlug" | "organizerSlug";
59
+ /** The public projection of a `VideoMeeting`, as returned to an anonymous joiner - deliberately narrow: a guest
60
+ * never sees `mailboxUid`, `calendarEventUid` or any other internal identifier. */
61
+ export interface PublicVideoMeeting {
62
+ uid: string;
63
+ title: string;
64
+ visibility: VideoMeetingVisibility;
65
+ status: VideoMeetingStatus;
66
+ /** The host mailbox's `displayName`, when it has one. */
67
+ hostDisplayName?: string;
68
+ }
69
+ /**
70
+ * `join()`'s response.
71
+ *
72
+ * **`authenticated`/`selfUid`/`token` - reading this shape correctly.** A caller who already presented a valid
73
+ * session for a real, non-guest RapidMX identity (see `join()`'s doc comment for exactly how that's told apart
74
+ * from a returning guest) gets `authenticated: true` and no `token`/`expiresAt` at all - there is no guest JWT to
75
+ * hand over, because the caller's own already-existing session cookie/header already authenticates `/push` for
76
+ * them, with zero new client-side auth handling. The frontend must not write any cookie in that case (doing so
77
+ * would be pointless at best, and at worst overwrite the caller's real cookie with... the value it already has).
78
+ * The common anonymous case (`authenticated: false`) is unchanged from Phase 1: `token`/`expiresAt` are present
79
+ * and the frontend applies `token` as the `jwt` cookie before connecting - see
80
+ * `apps/shared/push/GuestSignalingClient.ts`.
81
+ */
82
+ export interface VideoMeetingJoinResult {
83
+ meeting: PublicVideoMeeting;
84
+ iceServers: IceServerConfig[];
85
+ /** `true` when the caller already presented a valid session for a real RapidMX identity when calling `join()`;
86
+ * `false` for the common anonymous case, where a fresh guest identity was just minted instead. */
87
+ authenticated: boolean;
88
+ /** The identity now holding `READ`/`CREATE` on the meeting's own push channel, and the uid the frontend's mesh
89
+ * connection manager (`apps/shared/webrtc/MeshConnectionManager.ts`) must identify itself as: the caller's own
90
+ * real uid when `authenticated` is `true`, otherwise the freshly minted synthetic `guest:<random>` uid `token`
91
+ * authenticates as. Always present - this replaces Phase 1's guest-only `guestUid` field now that the same
92
+ * grant-and-identify step also runs for a real, already-authenticated caller. */
93
+ selfUid: string;
94
+ /** A short-lived guest JWT, immediately usable against `/push` to subscribe to and publish on `meeting.uid` -
95
+ * and nothing else. Present only when `authenticated` is `false`; omitted entirely for an already-authenticated
96
+ * real caller, who has no guest token minted for them at all (see `join()`). See this class's doc comment. */
97
+ token?: string;
98
+ /** ISO 8601 instant `token` expires at. Present only when `authenticated` is `false`, exactly when `token`
99
+ * itself is. */
100
+ expiresAt?: string;
101
+ }
102
+ /**
103
+ * The owner's management of their own `VideoMeeting`s (JWT-authenticated, mailbox-ACL-checked exactly like any
104
+ * other mailbox-scoped entity - see `booking-plugin`'s `BaseBookingTypeRoute` for the same "must own or hold a
105
+ * grant on the mailbox" pattern this class hand-implements) and the anonymous `join()` endpoint a private
106
+ * invitee's `joinToken` or a public meeting's `publicSlug` resolves through.
107
+ *
108
+ * Like `BaseBookingRoute`, this is a standalone class, NOT a `CRUDRoute`/`BaseScopedChildRoute` subclass: `create()`'s
109
+ * response is not itself a bare `VideoMeeting` (it also carries invitee join links / the public join link, which the
110
+ * generic CRUD return type can't express), and `join()` is anonymous and token-resolved, the same shape of
111
+ * requirement that keeps `BaseBookingRoute` standalone. Every repo call passes `ignoreACL: true` because this class
112
+ * performs its own authorization: the owner-side methods check `ACLUtils.hasPermission()` against the meeting's
113
+ * `mailboxUid` with the caller's trusted roles stripped first (`util/RouteAccessUtils.ts` - see that module's doc
114
+ * comment for why: without it, `ACLUtils.hasPermission()`'s own "trusted users always have permission" rule would
115
+ * let an administrator read or manage a mailbox they hold no grant on, which is never true for mail here), and
116
+ * `join()` resolves an anonymous caller purely by the token/slug's own database match. Like `Booking`/`BookingType`,
117
+ * the concrete `VideoMeetingMongo`/`VideoMeetingSQL`/`VideoMeetingInviteeMongo`/`VideoMeetingInviteeSQL` classes carry
118
+ * an ordinary deny-all class ACL; `"anonymous"` is never granted an action anywhere.
119
+ *
120
+ * Like `BaseBookingRoute`/`BaseMailIngestRoute`/`BasePushRoute`, this class carries no `@Route`/`@ApiRoute` of its
121
+ * own - the consuming Mongo/SQL concrete subclass applies one (`mail/video-meetings`).
122
+ *
123
+ * ## Path shape
124
+ *
125
+ * `join()` lives at the literal `/join/:token` (not `/:token` at the root), the same reasoning `BaseBookingRoute`'s
126
+ * doc comment gives for its own `/manage/<token>` vs. `/:slug/slots` split: a bare `/:token` at the root would
127
+ * collide with (and make router registration order decide between) this class's own `/:id` owner routes.
128
+ *
129
+ * ## Why `VideoMeeting` gets its own per-record `AccessControlList`
130
+ *
131
+ * Unlike `Booking`, `VideoMeeting.uid` doubles as a `/push` signaling channel (see `.claude/NOTES.md`'s "Signaling"
132
+ * design note): SDP offers/answers and ICE candidates are ordinary `NotificationUtils.sendMessage()` payloads
133
+ * published to it, exactly as `MailPushRoute`'s doc comment describes for a `Mailbox`/`Folder` channel. A `/push`
134
+ * channel is authorized purely by `ACLUtils.hasPermission(user, channelUid, action)` against a per-record
135
+ * `AccessControlList` document keyed by that exact uid - there being no such document at all (the case for
136
+ * `Booking`, whose `@Protect` sets `recordACL: false`) makes `hasPermission()` always answer `false`. So
137
+ * `VideoMeetingMongo`/`VideoMeetingSQL` set `recordACL: true`, and `create()` below claims that per-record ACL
138
+ * with `parentUid` set to the meeting's own `mailboxUid` (mirroring `BaseFolderRoute`'s identical `acl: { uid,
139
+ * parentUid: mailboxUid, records: [] }` pattern) - which is also what lets a mailbox owner/delegate reach the
140
+ * meeting (and its channel) through the ordinary ACL parent-chain, with no extra mechanism needed on top of what
141
+ * `RepoUtils.create()`'s automatic per-record ACL claim already does (it grants the creator full rights on the
142
+ * fresh ACL - see `RepoUtils.claimRecordACL()`).
143
+ *
144
+ * ## The channel-ACL-grant mechanism
145
+ *
146
+ * An anonymous guest holds no `AccessControlList` grant of their own and has no `JWTUser` to authenticate `/push`
147
+ * with in the first place - `join()` therefore does two things together: it mints a short-lived, scope-limited
148
+ * guest JWT (`mintGuestToken()`: a synthetic `guest:<random>` uid, no roles, `GUEST_JWT_TTL_SECONDS` expiry, real and
149
+ * verifiable since it's signed with the same `auth` config every other token is), and it adds an explicit
150
+ * `ACLRecord` for that exact uid onto the meeting's own `AccessControlList` (`ensureChannelGrant()`), granting
151
+ * `READ` (so `BasePushRoute`'s SUBSCRIBE succeeds) and `CREATE` (so a publish does - see `MailPushRoute`'s doc
152
+ * comment: "publishing to a channel needs CREATE on it as an ordinary user would"). The grant is scoped to exactly
153
+ * that one meeting's uid, nothing else - the same "possession of a link is the credential" pattern
154
+ * `BaseBookingRoute.requireBookingByToken()`/`resolveEffectiveUser()`'s `share:<token>` identity already establish
155
+ * elsewhere in this codebase, extended one step further because signaling needs a channel *subscription*, not just
156
+ * a stateless REST call.
157
+ *
158
+ * **Known limitation, documented rather than silently assumed away**: because each `join()` call for an anonymous
159
+ * caller mints a *fresh* random guest uid (so simultaneous participants of one shared link are distinguishable from
160
+ * each other in the signaling channel), each such join adds one more `ACLRecord` to the meeting's own ACL document,
161
+ * and nothing in Phase 1 ever removes one. A meeting joined many times over its lifetime accumulates unused records;
162
+ * there is no GC job, matching this codebase's own precedent for `Booking.manageToken` (documented as never
163
+ * expiring, no GC job either). Since the guest JWTs themselves expire, an accumulated record is inert (unusable)
164
+ * well before it becomes a real concern - a cleanup pass is a reasonable thing for a later phase to add, not a
165
+ * Phase 1 requirement. An already-authenticated real caller (see below) is granted their own stable uid instead, so
166
+ * repeated joins by the same real identity never add more than the one record `ensureChannelGrant()`'s own
167
+ * idempotency check already collapses them to.
168
+ *
169
+ * ## Real, already-authenticated callers (the browser-session-collision fix)
170
+ *
171
+ * `join()` also accepts an optional `@AuthUser`: whatever `req.user` the framework's own `JWTStrategy` already
172
+ * populated from the caller's existing `Authorization` header or `jwt` cookie, exactly like every other
173
+ * authenticated-optional endpoint in this codebase's family (e.g. `BaseScopedChildRoute.resolveEffectiveUser()`'s
174
+ * "prefer the real authenticated user, fall back to the anonymous token" precedent). This matters because a
175
+ * logged-in RapidMX user's browser already carries a real, `HttpOnly` `jwt` session cookie for this origin - a
176
+ * cookie `apps/shared/push/GuestSignalingClient.ts` cannot overwrite with a guest token even if `join()` minted one
177
+ * (browsers refuse to let a script override an `HttpOnly` cookie of the same name), so a guest-only `join()` would
178
+ * leave that browser's `/push` WebSocket authenticating as the real session while the meeting's ACL only names a
179
+ * synthetic guest uid - the subscribe is simply, safely refused, and the real user could never actually join.
180
+ *
181
+ * The fix: when `user` is present and is a *real* identity - not a guest uid from a previous `join()` call, told
182
+ * apart by the `GUEST_UID_PREFIX` a guest uid always starts with and a real, mailbox-owning identity never can -
183
+ * `join()` grants `user.uid` itself (not a synthetic one) `READ`/`CREATE` on the meeting's channel via
184
+ * `ensureChannelGrant()`, mints no guest JWT at all, and returns `authenticated: true` with `selfUid: user.uid`. The
185
+ * caller's own already-existing session cookie now already authenticates `/push` for them with zero new
186
+ * client-side auth handling - the frontend must not write a cookie of its own in this case (see
187
+ * `VideoMeetingJoinResult`'s doc comment). When `user` is absent (the common, true-anonymous case - no existing
188
+ * session at all), behavior is exactly Phase 1's: a fresh guest identity is minted and granted instead.
189
+ *
190
+ * ## The organizer's own slug (`VideoMeeting.organizerSlug`)
191
+ *
192
+ * The fix above lets a real, already-authenticated caller join *if they already hold something that resolves to the
193
+ * meeting*. The organizer of their own private meeting does not: the calendar integration that mints a meeting per
194
+ * event builds `invitees` from the event's attendees **excluding the organizer** (who manages the meeting through
195
+ * ownership, not as a guest), and `publicSlug` is minted only for a public meeting - so the one person who owns the
196
+ * meeting had no token `requireMeetingByToken()` could resolve for them at all. `persistMeeting()` therefore also
197
+ * mints an `organizerSlug` for every private meeting, `create()` returns it as `organizerJoinUrl` alongside the
198
+ * per-invitee links, and `findById()` returns the same link for a meeting loaded later.
199
+ *
200
+ * **This does not widen the `"private"` invariant by one caller.** The other two resolutions are credentials in
201
+ * themselves - possession of an invitee `joinToken` or a `publicSlug` is exactly what authorizes the join, by
202
+ * design. An `organizerSlug` is not: `requireMeetingByToken()` reports *which* field resolved the match, and for
203
+ * `"organizerSlug"` `join()` requires, before computing or returning anything about the meeting, both that the
204
+ * caller is a real already-authenticated identity (the same non-guest `GUEST_UID_PREFIX` check as above, so a
205
+ * returning guest presenting a prior `join()`'s own guest JWT never qualifies) and that this identity holds `READ`
206
+ * on the meeting's own `mailboxUid` - the very same `ACLUtils.hasPermission()` call, trusted roles stripped, that
207
+ * `requireMailboxAccess()` makes for every owner-side route, so a trusted administrator with no explicit grant is
208
+ * refused here exactly as they are there. Every caller who fails either condition - a true anonymous stranger, a
209
+ * returning guest, or a real but unrelated logged-in user - gets the identical bare `404` an entirely unknown token
210
+ * gets, never a `403`: this class never leaks whether a token almost-matched something, and an
211
+ * organizer-slug-shaped probe must be indistinguishable from a slug naming nothing at all. A caller who satisfies
212
+ * both proceeds through exactly the authenticated branch described above, with no new response field.
213
+ *
214
+ * ## Recovering a join link on a later read (`find()`/`findById()`, Phase 4)
215
+ *
216
+ * `create()`'s response computes `organizerJoinUrl`/`publicJoinUrl` inline, once, from the slug it just minted -
217
+ * fine for the moment of creation, but Phase 4's settings page (`apps/settings-video-conferencing`) needs a
218
+ * meeting's own persistent link on every later page load too, not only the one response `create()` ever sent (a
219
+ * user's "personal room", by this codebase's Phase 4 convention, is simply their oldest non-cancelled `PUBLIC`
220
+ * meeting - see `.claude/NOTES.md`'s Phase 4 entry for why no new field/route was needed to name it as such). Since
221
+ * a plain `RepoUtils.find()`/`findOne()` returns only the persisted slug columns, `find()` and `findById()` both
222
+ * now run every loaded meeting through `withJoinUrls()` - the exact same computation `create()` already did,
223
+ * applied uniformly on read instead of only once on write.
224
+ *
225
+ * ## Other known limitations
226
+ *
227
+ * **`VideoMeetingInvitee.joinToken` never expires** and has no GC job - identical tradeoff to `Booking.manageToken`.
228
+ *
229
+ * **`VideoMeeting.publicSlug` is only uniqueness-checked within its own mailbox** by the database, while `join()`'s
230
+ * lookup is global (the public join URL carries no mailbox segment) - see the `VideoMeeting.publicSlug` doc comment
231
+ * for the full reasoning; a cross-mailbox collision is not actually prevented, only made astronomically unlikely by
232
+ * the slug's own entropy. `VideoMeeting.organizerSlug` has the same shape and entropy but is indexed unique
233
+ * *globally*, which is exactly the scope its own lookup uses - see its doc comment for why a per-mailbox compound
234
+ * index cannot work for a field only half the rows carry. Less rides on it either way: resolving through it grants
235
+ * nothing by itself, so an unlucky collision there would cost a caller a `404`, never access.
236
+ *
237
+ * @author Jean-Philippe Steinmetz
238
+ */
239
+ export declare abstract class BaseVideoMeetingRoute<VM extends VideoMeeting, VMI extends VideoMeetingInvitee, M extends Mailbox> {
240
+ protected abstract meetingClass: any;
241
+ protected abstract inviteeClass: any;
242
+ protected abstract mailboxClass: any;
243
+ private _objectFactory?;
244
+ private meetingRepo?;
245
+ private inviteeRepo?;
246
+ private mailboxRepo?;
247
+ private aclUtils?;
248
+ private trustedRoles;
249
+ /** The `auth` config every other JWT in this deployment is signed/verified with - used by `mintGuestToken()` to
250
+ * mint a guest's own, short-lived token with the exact same signature the framework's `JWTStrategy` verifies. */
251
+ private authConfig;
252
+ /** The externally reachable base URL of the public join pages, used to build the invite links `create()`
253
+ * returns. Same single-value-config pattern as `mail:booking:public_url`/`mail:autodiscover:public_url`; when
254
+ * unset (or unsafe - see `buildBaseUrl()`) a join link is simply omitted rather than returning a broken one. */
255
+ private publicUrl;
256
+ private turnUrl;
257
+ private turnUsername;
258
+ private turnCredential;
259
+ private turnSharedSecret;
260
+ private logger;
261
+ /**
262
+ * Exposes the `@Model(...)`-supplied entity class as an instance property so `@Transactional()` on
263
+ * `persistMeeting()` can resolve which datasource to open a transaction against - identical to
264
+ * `BaseBookingRoute`'s own `modelClass` getter, for the same reason (this class deliberately doesn't extend
265
+ * `ModelRoute`, which defines the same getter for its own subclasses).
266
+ */
267
+ get modelClass(): any;
268
+ private init;
269
+ /** Rejects a `403` unless `user` (with its trusted roles stripped - see `util/RouteAccessUtils.ts`) holds
270
+ * `action` on `mailboxUid`, by ownership or an explicit ACL grant. */
271
+ private requireMailboxAccess;
272
+ /** Loads the meeting `id` names, `404` if it doesn't exist, then enforces `requireMailboxAccess()` against its
273
+ * `mailboxUid`. Used by every owner-side method that acts on a single existing meeting. */
274
+ private requireOwnedMeeting;
275
+ /** Pure shape/format validation of `create()`'s body - independent of any permission check, matching
276
+ * `BaseBookingRoute.validateBook()`'s split between format checks (here) and business-rule checks (in the
277
+ * handler itself, which needs a DB round trip this does not). */
278
+ private validateCreateBody;
279
+ /** Parses a caller-supplied ISO timestamp, rejecting anything unparseable with a `400` - matches
280
+ * `BaseBookingRoute.requireDate()`. `startTime`/`endTime` are optional, so this is only called when present. */
281
+ private requireOptionalDate;
282
+ /**
283
+ * Writes the `VideoMeeting`/`VideoMeetingInvitee` rows for a new meeting. `@Transactional()` (resolving its
284
+ * datasource from the `@Model(...)` on the concrete subclass, via the `modelClass` getter above) makes every
285
+ * write atomic, matching `BaseBookingRoute.persistBooking()`'s identical reasoning.
286
+ */
287
+ protected persistMeeting(mailboxUid: string, body: CreateVideoMeetingBody, visibility: VideoMeetingVisibility, startTime: Date | undefined, endTime: Date | undefined, user: JWTUser | undefined): Promise<{
288
+ meeting: VM;
289
+ invitees: VMI[];
290
+ }>;
291
+ /** The join URL for a token/slug, or `undefined` when no public URL is configured - see this class's doc
292
+ * comment on `mail:videoconf:public_url`. */
293
+ private joinUrl;
294
+ create(rawBody: CreateVideoMeetingBody | undefined, req: HttpRequest, user?: JWTUser): Promise<VideoMeetingCreateResult<VM>>;
295
+ /**
296
+ * Adds `organizerJoinUrl`/`publicJoinUrl` (see `joinUrl()`) to a persisted meeting for a caller re-reading it
297
+ * later. `create()`'s own response computes the same links inline from values it just minted, but `find()`/
298
+ * `findById()` load the plain persisted entity, which carries only the slugs themselves - so a caller who
299
+ * didn't keep `create()`'s one-time response (e.g. this plugin's own settings page, reloaded after the meeting
300
+ * that IS a user's "personal room" - see `.claude/NOTES.md`'s Phase 4 entry - was created in an earlier visit)
301
+ * would otherwise have no way to recover a public meeting's shareable link, or a private meeting's organizer
302
+ * link, at all. Spreads rather than mutates: the loaded instance is the repo's own entity, and both fields are
303
+ * response-only.
304
+ */
305
+ private withJoinUrls;
306
+ find(mailboxUid: string | undefined, limit: string | undefined, page: string | undefined, user?: JWTUser): Promise<(VM & {
307
+ organizerJoinUrl?: string;
308
+ publicJoinUrl?: string;
309
+ })[]>;
310
+ findById(id: string, user?: JWTUser): Promise<VM & {
311
+ organizerJoinUrl?: string;
312
+ publicJoinUrl?: string;
313
+ }>;
314
+ update(id: string, body: {
315
+ title?: string;
316
+ status?: string;
317
+ } | undefined, user?: JWTUser): Promise<VM>;
318
+ delete(id: string, user?: JWTUser): Promise<void>;
319
+ /**
320
+ * Resolves `token` to the meeting it names, and to *how* it named it: an invitee's `joinToken` (43 base64url
321
+ * characters - `JOIN_TOKEN_PATTERN`), or one of the two 11-character slugs (`PUBLIC_SLUG_PATTERN`) - a public
322
+ * meeting's `publicSlug` or a private meeting's `organizerSlug`. The token and slug lengths never overlap (see
323
+ * `util/TokenUtils.ts`), so an invitee token is never tried as a slug or vice versa - unlike trying both in
324
+ * sequence, this can't accidentally treat one as the other just because the lookup that should have matched
325
+ * happened to miss. A stale/unknown/malformed/wrongly-shaped token, a cancelled meeting, or a meeting whose
326
+ * visibility no longer matches how the token was resolved (defense in depth - a meeting's `visibility` cannot
327
+ * actually change after creation) all answer identically: a plain `404`, matching
328
+ * `BaseBookingRoute.requireBookingByToken()`'s exact posture of never leaking whether a token almost-matched
329
+ * something.
330
+ *
331
+ * The two slug columns are disjoint by construction (`persistMeeting()` mints `publicSlug` only for a public
332
+ * meeting and `organizerSlug` only for a private one), so a slug-shaped token is looked up against `publicSlug`
333
+ * first - exactly Phase 1's lookup, with exactly Phase 1's outcome whenever it matches a row at all - and only
334
+ * a token that matches no `publicSlug` row is then looked up against `organizerSlug`.
335
+ *
336
+ * The resolution is returned alongside the meeting because `join()` authorizes the three cases differently: an
337
+ * invitee token and a `publicSlug` are each a self-contained credential ("possession of the link"), while an
338
+ * `organizerSlug` is not - see `join()`'s doc comment and `VideoMeeting.organizerSlug`.
339
+ */
340
+ private requireMeetingByToken;
341
+ /**
342
+ * Adds an `ACLRecord` granting `uid` `READ`/`CREATE` on `meetingUid`'s own `AccessControlList`, unless one
343
+ * already exists (idempotent - a retried/duplicate call for the same uid, guest or real, is a no-op). Retries a
344
+ * handful of times on an optimistic-lock conflict (`saveACL()`'s version check): a public meeting can be joined
345
+ * by several callers at once, each racing to add their own record to the very same ACL document, and a lost
346
+ * race must be retried against the freshly re-read version rather than surfaced to the caller as an error.
347
+ * Named generically (not `ensureGuestChannelGrant()`, its Phase 1 name) since `join()` now calls this for a
348
+ * real, already-authenticated caller's own uid too - see this class's doc comment on "Real, already-
349
+ * authenticated callers".
350
+ */
351
+ private ensureChannelGrant;
352
+ /**
353
+ * Mints a short-lived, scope-limited guest identity - see this class's doc comment on the channel-ACL-grant
354
+ * mechanism. Only called for the true-anonymous case (`join()`'s `user` is absent, or presents a prior guest
355
+ * uid rather than a real one). The deployment's real `auth` config normally carries its own `options.expiresIn`
356
+ * (every other token's session length), which `jsonwebtoken` refuses to combine with an explicit `exp` claim in
357
+ * the payload ("Bad 'options.expiresIn' option the payload already has an 'exp' property") - so this signs with
358
+ * a shallow copy of `authConfig` that omits `options.expiresIn`, letting the payload's own `exp` (this guest
359
+ * token's own, shorter `GUEST_JWT_TTL_SECONDS` lifetime) govern instead. Everything else about `authConfig` -
360
+ * the secret, the algorithm, `audience`/`issuer` - is unchanged, so this guest token verifies through the exact
361
+ * same `JWTStrategy` every other token does.
362
+ */
363
+ private mintGuestToken;
364
+ /** The host mailbox's `displayName`, or `undefined` when the mailbox has none (or no longer exists - an
365
+ * orphaned meeting should still be joinable, just without a host name to show). */
366
+ private hostDisplayName;
367
+ join(token: string, user?: JWTUser): Promise<VideoMeetingJoinResult>;
368
+ }
@@ -0,0 +1,10 @@
1
+ import { MailboxMongo } from "@rapidmx/restapi/mongo";
2
+ import { VideoMeetingMongo } from "../../models/mongo/VideoMeetingMongo.js";
3
+ import { VideoMeetingInviteeMongo } from "../../models/mongo/VideoMeetingInviteeMongo.js";
4
+ import { BaseVideoMeetingRoute } from "../BaseVideoMeetingRoute.js";
5
+ /** The owner's video meeting management endpoints and the anonymous join endpoint (`/api/mail/video-meetings`). */
6
+ export declare class VideoMeetingRouteMongo extends BaseVideoMeetingRoute<VideoMeetingMongo, VideoMeetingInviteeMongo, MailboxMongo> {
7
+ protected meetingClass: any;
8
+ protected inviteeClass: any;
9
+ protected mailboxClass: any;
10
+ }
@@ -0,0 +1,10 @@
1
+ import { MailboxSQL } from "@rapidmx/restapi/sql";
2
+ import { VideoMeetingSQL } from "../../models/sql/VideoMeetingSQL.js";
3
+ import { VideoMeetingInviteeSQL } from "../../models/sql/VideoMeetingInviteeSQL.js";
4
+ import { BaseVideoMeetingRoute } from "../BaseVideoMeetingRoute.js";
5
+ /** The owner's video meeting management endpoints and the anonymous join endpoint (`/api/mail/video-meetings`). */
6
+ export declare class VideoMeetingRouteSQL extends BaseVideoMeetingRoute<VideoMeetingSQL, VideoMeetingInviteeSQL, MailboxSQL> {
7
+ protected meetingClass: any;
8
+ protected inviteeClass: any;
9
+ protected mailboxClass: any;
10
+ }
@@ -0,0 +1,8 @@
1
+ /**
2
+ * This plugin's `./sql` entry point: exactly the classes a server host loads for a SQL deployment - the video
3
+ * meeting models and the route mounted at `/api/mail/video-meetings`. Anything else exported here would be
4
+ * registered by the host too, so the abstract route and utilities stay in the package root.
5
+ */
6
+ export { VideoMeetingSQL } from "./models/sql/VideoMeetingSQL.js";
7
+ export { VideoMeetingInviteeSQL } from "./models/sql/VideoMeetingInviteeSQL.js";
8
+ export { VideoMeetingRouteSQL } from "./routes/sql/VideoMeetingRouteSQL.js";
@@ -0,0 +1,51 @@
1
+ import { ObjectFactory } from "@rapidrest/service-core";
2
+ import { VideoMeeting, VideoMeetingInvitee } from "../models/types.js";
3
+ /** The sole invitee `createSingleInviteeVideoMeeting()` mints a new private meeting for. */
4
+ export interface SingleMeetingInvitee {
5
+ email: string;
6
+ displayName?: string;
7
+ }
8
+ /**
9
+ * **The integration surface other plugins call.** This is the one thing `@rapidmx/videoconf-plugin` exposes for
10
+ * another, independently-installed plugin to use in-process: mints a private `VideoMeeting` for exactly one
11
+ * invitee and returns just that invitee's join URL. `booking-plugin` is today's only caller - a video-location
12
+ * booking with no host-preset URL mints one of these automatically (the booker as the sole invitee, matching this
13
+ * plugin's own "one link per invitee" private-meeting design) - see that package's `BaseBookingRoute`/`.claude/
14
+ * NOTES.md` for the calling side, and this plugin's own `.claude/NOTES.md` for the design this integration was
15
+ * built against.
16
+ *
17
+ * This is deliberately the ONLY integration point this package exposes: everything else about `VideoMeeting`/
18
+ * `VideoMeetingInvitee` (routes, signaling, the join flow, cancellation, ...) is reached only through this
19
+ * package's own HTTP API. A caller never gets a `VideoMeeting`/`VideoMeetingInvitee` instance back, only the one
20
+ * value it actually needs, so this package stays free to change anything else about their shape without breaking
21
+ * a caller compiled against an older version.
22
+ *
23
+ * Mirrors `BaseVideoMeetingRoute.create()`'s own private-meeting-plus-single-invitee path exactly, including its
24
+ * `acl: { uid: instance.uid, parentUid: mailboxUid, records: [] }` claim (see that class's doc comment on why
25
+ * `VideoMeeting` needs its own per-record `AccessControlList`) - but skips everything about validating an HTTP
26
+ * request body or checking a caller's own permission on the mailbox: this is an in-process call from code that has
27
+ * already established both by its own means (e.g. `booking-plugin`'s already-authorized, already-validated
28
+ * booking flow).
29
+ *
30
+ * A caller-supplied `title`/`invitee.displayName` longer than this package's own route-level limits is silently
31
+ * truncated rather than rejected - there is no request here to answer with a `400`, and a slightly-shortened title
32
+ * is a far better outcome than throwing back into whatever the caller's own flow was doing. (`booking-plugin`'s own
33
+ * call site additionally wraps this whole function in a `try`/`catch` so even a genuine failure here - a database
34
+ * error, a stale/misconfigured deployment - never blocks the caller's own operation; see its doc comment.)
35
+ *
36
+ * @param objectFactory The caller's own `ObjectFactory` - used exactly as `BaseVideoMeetingRoute` uses its own, to
37
+ * build the repos this needs and to resolve `mail:videoconf:public_url`.
38
+ * @param meetingClass The concrete `VideoMeeting` model class for the caller's own backend (`VideoMeetingMongo`/
39
+ * `VideoMeetingSQL`, from this package's `./mongo`/`./sql` entry points) - this package has no notion of "Mongo" vs.
40
+ * "SQL" itself (see `BaseVideoMeetingRoute`'s identical `meetingClass` constructor parameter), so the caller
41
+ * supplies it, the same way `booking-plugin`'s own `BookingRouteMongo`/`BookingRouteSQL` supply their own concrete
42
+ * classes to `BaseBookingRoute`.
43
+ * @param inviteeClass The concrete `VideoMeetingInvitee` model class for the same backend.
44
+ * @param mailboxUid The mailbox the meeting is minted under - the caller's own already-authorized mailbox.
45
+ * @param title A short, human-readable meeting title (e.g. the caller's own meeting/event name).
46
+ * @param invitee The sole invitee of the new private meeting.
47
+ * @returns The invitee's join URL, or `undefined` when `mail:videoconf:public_url` isn't configured on this
48
+ * deployment - mirroring `VideoMeetingInviteeJoinInfo.joinUrl`'s identical semantics exactly. Never throws for
49
+ * that reason alone; a genuine failure (a bad `objectFactory`, a database error, ...) still propagates.
50
+ */
51
+ export declare function createSingleInviteeVideoMeeting<VM extends VideoMeeting, VMI extends VideoMeetingInvitee>(objectFactory: ObjectFactory, meetingClass: new (...args: any[]) => VM, inviteeClass: new (...args: any[]) => VMI, mailboxUid: string, title: string, invitee: SingleMeetingInvitee): Promise<string | undefined>;