@ai-matrx/meet 0.7.84 → 0.7.85

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.d.ts CHANGED
@@ -1,3780 +1,5 @@
1
- import { RealtimeManager } from '@ai-matrx/realtime';
2
- import { MessagingSupabaseClient, ActionHandler } from '@ai-matrx/messaging';
3
- import { RoomOptions, ConnectionQuality as ConnectionQuality$1, Participant } from 'livekit-client';
4
-
5
- /**
6
- * THE MEET DOMAIN MODEL.
7
- *
8
- * D3: an audio call, a video call, and a meeting are ONE thing — a room. An
9
- * audio call is a room with video off; a meeting is a room plus scheduling,
10
- * lobby and host metadata. That ruling is expressed here as ONE `MeetRoom`
11
- * identity carried by both `/calls` and `/meetings`, not as three parallel
12
- * hierarchies that drift.
13
- *
14
- * Identity-bearing values are BRANDED (C22). A `MeetingId` and a `RoomName` are
15
- * both strings and are NOT interchangeable — passing the meeting's row id where
16
- * LiveKit wants the room name produces a room nobody else joins, which looks
17
- * exactly like "the other person's camera is broken".
18
- */
19
- declare const brand: unique symbol;
20
- type Brand<T, B extends string> = T & {
21
- readonly [brand]: B;
22
- };
23
- type MeetingId = Brand<string, "MeetingId">;
24
- type CallId = Brand<string, "CallId">;
25
- /** The LiveKit room name. THE MEDIA ROOM KEY — durable for a meeting link. */
26
- type RoomName = Brand<string, "RoomName">;
27
- type UserId = Brand<string, "UserId">;
28
- type OrganizationId = Brand<string, "OrganizationId">;
29
- /** LiveKit participant identity. `user:<uuid>` or `guest:<opaque>`. */
30
- type ParticipantIdentity = Brand<string, "ParticipantIdentity">;
31
- declare const asMeetingId: (value: string) => MeetingId;
32
- declare const asCallId: (value: string) => CallId;
33
- declare const asRoomName: (value: string) => RoomName;
34
- declare const asUserId: (value: string) => UserId;
35
- declare const asOrganizationId: (value: string) => OrganizationId;
36
- declare const asParticipantIdentity: (value: string) => ParticipantIdentity;
37
- type JsonValue = string | number | boolean | null | JsonValue[] | {
38
- [key: string]: JsonValue;
39
- };
40
- type JsonObject = {
41
- [key: string]: JsonValue;
42
- };
43
- /** Audio-only or with video. A call escalates; it never becomes a new call. */
44
- type CallMode = "audio" | "video";
45
- /**
46
- * WHO IS IN THE ROOM, as far as authorization is concerned.
47
- *
48
- * A guest has no `userId` — by construction, not by convention. Everything that
49
- * requires authentication (host powers, recording, starting a call) takes a
50
- * `userId` and therefore cannot compile against a guest identity (D6).
51
- */
52
- interface MeetIdentity {
53
- readonly userId: UserId | null;
54
- readonly organizationId: OrganizationId;
55
- readonly displayName: string;
56
- readonly avatarUrl: string | null;
57
- /** Stable per-device value so a reconnect keeps ONE LiveKit identity. */
58
- readonly deviceId: string;
59
- }
60
- declare function isGuest(identity: MeetIdentity): boolean;
61
- /**
62
- * WHO SOMEBODY IS IN A ROOM — the SERVER's word, never this client's (MRI-A11).
63
- *
64
- * `host` is `meet_meetings.host_user_id` or a real `iam` grant on the meeting;
65
- * `participant` is an authenticated member of the meeting's organization;
66
- * `guest` is a link-follower with no session. Organization membership is
67
- * TENANCY and never permission, which is why "member of the org" lands on
68
- * `participant` and not on `host`.
69
- *
70
- * `cohost` has no server-side minter yet and is kept because the rank ladder
71
- * below is the thing that stops a co-host from removing the host; the moment a
72
- * co-host grant exists it slots in without a second vocabulary.
73
- */
74
- type ParticipantRole = "host" | "cohost" | "participant" | "guest";
75
- /** Rank, so "can this actor do that to that participant" is one comparison. */
76
- declare const ROLE_RANK: Readonly<Record<ParticipantRole, number>>;
77
- /**
78
- * 🚨 THE ONE PREDICATE FOR "DOES THIS PERSON HOLD HOST POWERS".
79
- *
80
- * Every host control in this package answers this question and no other. It is
81
- * a pure function of a SERVER-minted role, so there is no path by which a
82
- * client's own state can grant it anything.
83
- */
84
- declare function isHostRole(role: ParticipantRole): boolean;
85
- /**
86
- * THE LOBBY LADDER (D6). A guest link-follower starts `knocking`; the host
87
- * admits or denies. An authenticated member of the meeting's org is `admitted`
88
- * on arrival unless the meeting is locked.
89
- */
90
- type AdmissionState = "knocking" | "admitted" | "denied" | "removed";
91
- interface LobbyEntry {
92
- readonly identity: ParticipantIdentity;
93
- readonly displayName: string;
94
- readonly avatarUrl: string | null;
95
- readonly userId: UserId | null;
96
- readonly state: AdmissionState;
97
- readonly knockedAt: string;
98
- readonly decidedAt: string | null;
99
- readonly decidedBy: UserId | null;
100
- }
101
- type ConnectionQuality = "excellent" | "good" | "poor" | "lost" | "unknown";
102
- /** One person in the room, as the UI needs them. Media state is a FACT here. */
103
- interface MeetParticipant {
104
- readonly identity: ParticipantIdentity;
105
- readonly userId: UserId | null;
106
- readonly displayName: string;
107
- readonly avatarUrl: string | null;
108
- readonly role: ParticipantRole;
109
- /**
110
- * Whether the SERVER granted this participant the power to start a recording
111
- * (MRI-A11). It is not `role === "host"`: a meeting whose recording policy is
112
- * `always-on` is the server's to start and a host may not touch it, and a
113
- * `disabled` policy grants it to nobody. The client renders this; it never
114
- * recomputes the policy.
115
- */
116
- readonly canRecord: boolean;
117
- readonly isLocal: boolean;
118
- readonly isAgent: boolean;
119
- readonly micEnabled: boolean;
120
- readonly cameraEnabled: boolean;
121
- readonly screenSharing: boolean;
122
- readonly speaking: boolean;
123
- readonly handRaised: boolean;
124
- /**
125
- * When the hand went up (epoch ms), or null. The HAND QUEUE is ordered by it —
126
- * first raised, first called on (Meet wave 2). Optional so a roster built by a
127
- * host before 0.8 still type-checks.
128
- */
129
- readonly handRaisedAt?: number | null | undefined;
130
- readonly quality: ConnectionQuality;
131
- readonly joinedAt: string;
132
- /**
133
- * LiveKit participant attributes, verbatim. The note-taker announces itself
134
- * here (`matrx.agent.consent_notice`), which is how a consent notice can be
135
- * rendered for a participant whose token metadata this client cannot read.
136
- */
137
- readonly attributes: Readonly<Record<string, string>>;
138
- }
139
- type RoomPhase = "idle" | "requesting-token" | "lobby" | "connecting" | "connected" | "reconnecting" | "left" | "failed";
140
- type MeetingKind = "instant" | "scheduled" | "recurring";
141
- interface MeetingRecord {
142
- readonly id: MeetingId;
143
- readonly organizationId: OrganizationId;
144
- /** Durable: the same room name before, during, and after (R3). */
145
- readonly roomName: RoomName;
146
- /** The public slug in a meeting link. Durable for recurring meetings. */
147
- readonly slug: string;
148
- readonly title: string;
149
- readonly kind: MeetingKind;
150
- readonly hostUserId: UserId;
151
- readonly scheduledFor: string | null;
152
- readonly scheduledDurationMinutes: number | null;
153
- readonly startedAt: string | null;
154
- readonly endedAt: string | null;
155
- readonly locked: boolean;
156
- readonly lobbyEnabled: boolean;
157
- readonly recordingPolicy: RecordingPolicy;
158
- readonly aiEnabled: boolean;
159
- readonly metadata: JsonObject;
160
- /** IANA zone the meeting's wall-clock time is authored in (`America/Los_Angeles`). */
161
- readonly timeZone?: string;
162
- /** Description / agenda text. */
163
- readonly agenda?: string | null;
164
- /** RFC 5545 RRULE subset — one row and one durable link per series. */
165
- readonly recurrenceRule?: string | null;
166
- /** Signed-in members and invitees may enter before a host arrives. */
167
- readonly joinBeforeHost?: boolean;
168
- /** Cancel is a STATE: the link still resolves and says so; nobody can join. */
169
- readonly cancelledAt?: string | null;
170
- readonly cancellationReason?: string | null;
171
- /** Archived (soft deleted). Restorable by the host or a co-host. */
172
- readonly deletedAt?: string | null;
173
- /** iCalendar SEQUENCE — bumped by the database on every reschedule and on cancel. */
174
- readonly calendarSequence?: number;
175
- /** Row version, for the compare-and-swap in `updateMeeting`. */
176
- readonly version?: number;
177
- }
178
- /** An invitee's answer. `needs_action` = not answered yet (iCalendar NEEDS-ACTION). */
179
- type RsvpState = "needs_action" | "accepted" | "declined" | "tentative";
180
- /** The answers a person can give. */
181
- type RsvpAnswer = Exclude<RsvpState, "needs_action">;
182
- /** A co-host holds host moderation through a real `iam` admin grant on the meeting. */
183
- type InviteeRole = "invitee" | "cohost";
184
- /** One person a meeting was sent to (`communication.meet_invitees`). */
185
- interface MeetingInvitee {
186
- readonly id: string;
187
- readonly meetingId: MeetingId;
188
- /** Set when the person has an account — they skip the waiting room and answer in-app. */
189
- readonly userId: UserId | null;
190
- readonly email: string | null;
191
- readonly displayName: string | null;
192
- readonly role: InviteeRole;
193
- readonly rsvpState: RsvpState;
194
- readonly respondedAt: string | null;
195
- readonly rsvpNote: string | null;
196
- /** The calendar_sequence of the last invitation they were sent; null = never told. */
197
- readonly lastSentSequence: number | null;
198
- readonly lastSentAt: string | null;
199
- }
200
- /** Who to invite: an account (`userId`) or an address (`email`), or both. */
201
- interface InviteeInput {
202
- readonly userId?: UserId | undefined;
203
- readonly email?: string | undefined;
204
- readonly displayName?: string | undefined;
205
- /** Omit to keep an existing invitee's role; `cohost` needs the host or a co-host. */
206
- readonly role?: InviteeRole | undefined;
207
- }
208
- /** Where one occurrence of a meeting stands. */
209
- type OccurrenceState = "scheduled" | "moved" | "cancelled";
210
- /** One occurrence — for a one-off meeting, the meeting itself. */
211
- interface MeetingOccurrence {
212
- readonly meetingId: MeetingId;
213
- /** The start the rule generates (iCalendar RECURRENCE-ID). The key for exceptions. */
214
- readonly originalStart: string;
215
- /** When it actually happens (differs from `originalStart` only when `moved`). */
216
- readonly occurrenceStart: string;
217
- readonly durationMinutes: number | null;
218
- readonly state: OccurrenceState;
219
- readonly sequence: number;
220
- }
221
- /** One row of the Upcoming list — an occurrence plus what the list shows about its meeting. */
222
- interface UpcomingOccurrence extends MeetingOccurrence {
223
- readonly organizationId: OrganizationId;
224
- readonly title: string;
225
- readonly slug: string;
226
- readonly kind: MeetingKind;
227
- readonly hostUserId: UserId;
228
- readonly timeZone: string;
229
- readonly meetingCancelled: boolean;
230
- /** The signed-in reader's own answer, when they are an invitee. */
231
- readonly myRsvp: RsvpState | null;
232
- /** `host`, `cohost` or `invitee` — null when the reader simply can see it (org member). */
233
- readonly myRole: "host" | InviteeRole | null;
234
- }
235
- /** An exception row (`communication.meet_occurrence_overrides`). */
236
- interface OccurrenceOverride {
237
- readonly id: string;
238
- readonly meetingId: MeetingId;
239
- readonly originalStart: string;
240
- readonly state: "cancelled" | "moved";
241
- readonly newStart: string | null;
242
- readonly newDurationMinutes: number | null;
243
- readonly reason: string | null;
244
- readonly calendarSequence: number;
245
- /** Set when the exception was withdrawn (`restore`) and the occurrence is back on schedule. */
246
- readonly deletedAt: string | null;
247
- }
248
- /** Settings a meeting carries; unset ones come from the person's / organization's knobs. */
249
- interface MeetingSettings {
250
- readonly lobbyEnabled?: boolean | undefined;
251
- readonly recordingPolicy?: RecordingPolicy | undefined;
252
- readonly aiEnabled?: boolean | undefined;
253
- readonly joinBeforeHost?: boolean | undefined;
254
- }
255
- /** A partial edit. `null` clears a clearable field (agenda, recurrence rule, time). */
256
- interface MeetingChanges extends MeetingSettings {
257
- readonly title?: string | undefined;
258
- readonly agenda?: string | null | undefined;
259
- readonly timeZone?: string | undefined;
260
- readonly scheduledFor?: string | null | undefined;
261
- readonly scheduledDurationMinutes?: number | null | undefined;
262
- readonly recurrenceRule?: string | null | undefined;
263
- /**
264
- * When the SERIES moves (start, rule or zone), carry its per-date changes
265
- * (cancelled or moved occurrences) to the new schedule instead of archiving
266
- * them — Google Calendar's and Outlook's "keep changes to individual events".
267
- * Absent or false archives them (restorable). Ask the person; never decide.
268
- */
269
- readonly keepExceptions?: boolean | undefined;
270
- }
271
- /** What the emailed RSVP link shows and answers (no account needed). */
272
- type RsvpInvitation = {
273
- readonly ok: true;
274
- readonly meeting: {
275
- readonly title: string;
276
- readonly slug: string;
277
- readonly scheduledFor: string | null;
278
- readonly durationMinutes: number | null;
279
- readonly timeZone: string;
280
- readonly agenda: string | null;
281
- readonly recurrenceRule: string | null;
282
- readonly cancelledAt: string | null;
283
- readonly cancellationReason: string | null;
284
- readonly endedAt: string | null;
285
- };
286
- readonly invitee: {
287
- readonly displayName: string | null;
288
- readonly email: string | null;
289
- readonly rsvpState: RsvpState;
290
- readonly respondedAt: string | null;
291
- };
292
- } | {
293
- readonly ok: false;
294
- readonly message: string;
295
- };
296
- /** Org-configurable, never hardcoded taste (Law 6 / "opinions become knobs"). */
297
- type RecordingPolicy = "disabled" | "host-controlled" | "always-on";
298
- type RecordingState = "idle" | "starting" | "recording" | "stopping" | "processing" | "available"
299
- /** Bytes were purged under retention; notes and transcript remain durable. */
300
- | "expired" | "failed";
301
- /**
302
- * WHICH transport last said what the recording is doing (MRI-A16).
303
- *
304
- * `local` — this browser's own optimistic `starting`/`stopping`, which is all
305
- * the indicator ever had through 0.4.7, and the reason it never went solid.
306
- * `room` — a `matrx.meet.recording` packet, the ONLY feed a guest has.
307
- * `record` — the durable `meet_recordings` row. It outranks both (D16).
308
- */
309
- type RecordingSource = "local" | "room" | "record";
310
- /** The server's durable retention receipt; never inferred from a browser clock. */
311
- interface RecordingRetention {
312
- readonly operationId: string;
313
- readonly policyId: string;
314
- readonly policyUpdatedAt: string;
315
- readonly phase: "warning" | "expired";
316
- readonly dueAt: string;
317
- readonly expiredAt: string | null;
318
- }
319
- interface RecordingRecord {
320
- readonly id: string;
321
- readonly meetingId: MeetingId;
322
- readonly state: RecordingState;
323
- readonly startedAt: string | null;
324
- readonly endedAt: string | null;
325
- readonly startedBy: UserId | null;
326
- /** The PLATFORM file id. Never a provider URL (R3). */
327
- readonly fileId: string | null;
328
- readonly failureReason: string | null;
329
- /** Present when central retention warned or purged this recording. */
330
- readonly retention: RecordingRetention | null;
331
- }
332
- /**
333
- * WHERE A TRANSCRIPT LINE CAME FROM — and therefore how much it is worth.
334
- *
335
- * D16: `lk.transcription` is published by a room participant and its
336
- * `sender_identity` is client-supplied and unvalidated by LiveKit, so a caption
337
- * is a HINT — good enough to read live, never good enough to reason from. A
338
- * `record` line is a durable `meet_transcript_segments` row written server-side
339
- * by the note-taker: that is the transcript. A record always beats a caption;
340
- * a caption never overwrites a record.
341
- */
342
- type TranscriptSource = "caption" | "record";
343
- interface TranscriptSegment {
344
- readonly id: string;
345
- readonly meetingId: MeetingId;
346
- readonly identity: ParticipantIdentity;
347
- readonly speaker: string;
348
- readonly text: string;
349
- readonly startedAt: string;
350
- /** Interim text is replaced in place by its final; it is never appended. */
351
- readonly final: boolean;
352
- readonly source: TranscriptSource;
353
- /**
354
- * The `lk.segment_id` this line was captioned under, when known. A durable
355
- * row carries it too (the worker writes it into the row's metadata), which is
356
- * what lets a record REPLACE the caption it supersedes rather than doubling it.
357
- */
358
- readonly captionId: string | null;
359
- }
360
- /**
361
- * SOMEBODY WHO WAS IN THE MEETING — the durable `meet_participants` row, which
362
- * outlives the LiveKit room (MRI-D2). `MeetParticipant` above is a live tile;
363
- * THIS is the attendance record a person reads a week later.
364
- */
365
- interface MeetingAttendee {
366
- readonly id: string;
367
- readonly meetingId: MeetingId;
368
- readonly identity: ParticipantIdentity;
369
- readonly displayName: string;
370
- readonly avatarUrl: string | null;
371
- readonly userId: UserId | null;
372
- readonly role: ParticipantRole;
373
- readonly admissionState: AdmissionState;
374
- readonly isAgent: boolean;
375
- /** FIRST arrival in the room — a rejoin never moves it. */
376
- readonly joinedAt: string | null;
377
- /** Null while in the room, else the last time they left. */
378
- readonly leftAt: string | null;
379
- /**
380
- * Every stay in the room, oldest first (`metadata.sessions`, written by the
381
- * server's one attendance rule). Empty for a row recorded before stays were
382
- * kept; the report then falls back to `joinedAt`–`leftAt`.
383
- */
384
- readonly sessions?: readonly AttendanceStay[] | undefined;
385
- }
386
- /** One stay in the room: arrived, and left (null = still there or never recorded). */
387
- interface AttendanceStay {
388
- readonly joinedAt: string;
389
- readonly leftAt: string | null;
390
- }
391
- type MeetingNoteKind = "note" | "decision" | "action-item";
392
- /**
393
- * ONE ROW OF WHAT THE MEETING DECIDED. `meet_notes.kind` is a live CHECK
394
- * constraint of exactly three values (D5) — there is no `summary` kind and no
395
- * `transcript` kind — so the wrap-up summary is a `note` carrying
396
- * `metadata.role = "summary"`, and a live-window note carries `metadata.window`.
397
- * Both are projected here so the UI never re-reads raw metadata.
398
- */
399
- interface MeetingNote {
400
- readonly id: string;
401
- readonly meetingId: MeetingId;
402
- readonly text: string;
403
- readonly createdAt: string;
404
- readonly kind: MeetingNoteKind;
405
- /** Set ONLY when the assignee resolved to a real user (D18). */
406
- readonly assigneeUserId: UserId | null;
407
- /** Who the item is for, as a person reads it. A guest keeps their name here. */
408
- readonly assigneeDisplayName: string | null;
409
- /** `"summary"` for the wrap-up; `null` for everything else. */
410
- readonly role: "summary" | null;
411
- /** The live-notes window index this came out of, when it came from one. */
412
- readonly window: number | null;
413
- /**
414
- * 🚨 THE WRAP-UP IS THE AUTHORITATIVE POST-MEETING SET (D20).
415
- *
416
- * A live-notes window and the wrap-up both write decisions and action items,
417
- * and on production 2026-09-09 meeting `9cb-39f8-782` carried the SAME
418
- * decision twice — once from the note-taker at 04:43:57 and once from the
419
- * wrap-up at 04:47:15 — and the record view listed both. A reader cannot tell
420
- * a repeated decision from two decisions.
421
- *
422
- * So when the wrap-up lands, the server MARKS the window-written decisions
423
- * and action items it supersedes (`metadata.superseded_by` = the wrap-up's
424
- * run id) and never deletes them: the row is still the timeline of what was
425
- * known at 04:43. This field carries that marker, and the record view and the
426
- * assistant panel render only the rows where it is `null`.
427
- */
428
- readonly supersededBy: string | null;
429
- /**
430
- * WHICH TRANSPORT DELIVERED THIS COPY (MRI-A12). The same durable row reaches
431
- * a signed-in member twice — once as a `meet_notes` row over Postgres Changes
432
- * (`"record"`) and once as a `matrx.meet.note` packet on the media plane
433
- * (`"room"`), which is the ONLY copy a session-less guest can receive, because
434
- * `anon` holds no grant on the table (MRI-A10).
435
- *
436
- * They carry the same id and the same words, so the store renders one note.
437
- * Where they could ever disagree, `"record"` wins and `"room"` never overwrites
438
- * it — the database is the record and the packet is the courtesy copy. That is
439
- * the same precedence captions and segments already follow (D16).
440
- */
441
- readonly source: "record" | "room";
442
- }
443
- /**
444
- * THE WRAP-UP, ANNOUNCED (MRI-A12). The summary, decisions and action items
445
- * arrive as ordinary notes; this says the pass that wrote them FINISHED, which
446
- * is the difference between "no summary yet" and "no summary is coming".
447
- *
448
- * 🚨 It reaches a screen only while the room still exists. `POST /v1/meet/end`
449
- * deletes the LiveKit room and the wrap-up runs afterwards, so on the ordinary
450
- * ending path nobody is in the room to be told — the post-meeting record view is
451
- * where the summary is read. A surface must therefore treat `null` as "not
452
- * announced", never as "not written".
453
- */
454
- interface WrapUpAnnouncement {
455
- readonly meetingId: MeetingId;
456
- /** The `meet_notes` ids the pass wrote, in the order it wrote them. */
457
- readonly noteIds: readonly string[];
458
- /** How many durable transcript segments it summarized. */
459
- readonly segmentCount: number;
460
- readonly at: string;
461
- }
462
- /**
463
- * THE NOTE-TAKER'S STATE, as the meeting row records it
464
- * (`meet_meetings.metadata.note_taker.state`, MRI-A2). Five values and no
465
- * spinner: a surface renders the state it is in, or the server's refusal.
466
- *
467
- * - `absent` — nobody summoned it. The control offers to.
468
- * - `joining` — dispatched; it has not entered the room yet.
469
- * - `transcribing` — the server recorded active transcription and the exact
470
- * deterministic actor is present in the room.
471
- * - `handoff_pending` — a row that said transcribing no longer has that exact
472
- * actor in the room. The next state and any missed audio are not inferred.
473
- * - `ended` — the recorded note-taker stopped. The cause is not inferred.
474
- */
475
- type NoteTakerState = "absent" | "joining" | "transcribing" | "handoff_pending" | "ended";
476
- /** The lifecycle of a 1:1 or ad-hoc group call (R2). */
477
- type CallState = "idle" | "dialing" | "ringing" | "connecting" | "active" | "declined" | "missed" | "busy" | "cancelled" | "ended" | "failed";
478
- interface CallInvite {
479
- readonly id: CallId;
480
- readonly organizationId: OrganizationId;
481
- readonly roomName: RoomName;
482
- readonly mode: CallMode;
483
- readonly callerUserId: UserId;
484
- readonly callerName: string;
485
- readonly callerAvatarUrl: string | null;
486
- readonly calleeUserIds: readonly UserId[];
487
- /** The durable conversation the invite was delivered into, if any. */
488
- readonly conversationId: string | null;
489
- readonly createdAt: string;
490
- readonly expiresAt: string;
491
- readonly state: CallState;
492
- readonly declineMessage: string | null;
493
- }
494
- /** A person the UI can call. Deliberately minimal: hosts already have this. */
495
- interface CallablePerson {
496
- readonly userId: UserId;
497
- readonly displayName: string;
498
- readonly avatarUrl: string | null;
499
- }
500
- type StageLayout = "grid" | "speaker" | "sidebar";
501
-
502
- /**
503
- * THE ONE WAY THIS PACKAGE TALKS TO AIDREAM (MRI-A16).
504
- *
505
- * 🚨 **Every request to the aidream server carries `x-organization-id`, and the
506
- * only way to guarantee "every" is for there to be ONE place that builds the
507
- * request.** aidream's org-admission middleware refuses an authenticated
508
- * request that does not name the organization it is acting in — 400
509
- * `organization_required`, before the router is ever reached.
510
- *
511
- * 0.1.3 learned that lesson and put the header on the three meet callers by
512
- * hand. That fixed the instances and left the CLASS wide open, and on
513
- * 2026-09-09 the class collected its next victim: `<RecordingPlayer>` fetched
514
- * `GET /files/{id}/download?inline=1` through a bare `fetch` with only a bearer
515
- * token, so a perfectly landed 1.38 MB recording could not be played by the
516
- * host who made it (MRI-C8; the identical URL returned `200 video/mp4` the
517
- * moment the header was added by hand). A fourth caller was written months
518
- * after the lesson and never heard it, because a lesson written into three call
519
- * sites is not a lesson, it is three copies of a workaround.
520
- *
521
- * So: this module owns the base URL, the organization header and the bearer
522
- * token, and `api.test.ts` holds a guard that FAILS THE BUILD if any other file
523
- * in the package calls `fetch(` against a base URL or points an element `src=`
524
- * at one. A new caller cannot be written without the header any more.
525
- *
526
- * 🚨 **HOW A `<video>` GETS ITS BYTES, given that signed URLs are eradicated.**
527
- * A browser media element cannot attach a header, so a bare
528
- * `src="https://server…/files/{id}/download"` carries no session and no
529
- * organization and is refused — which is precisely what MRI-C8 saw. The
530
- * platform's answer is NOT a signed URL (`aidream/api/routers/files` mints
531
- * none, by law) but the FILE-SESSION COOKIE: `POST /files/session` mints an
532
- * HttpOnly `SameSite=None` cookie that the auth middleware accepts on exactly
533
- * the GET byte-serving routes (`COOKIE_AUTH_PATH_RE` in
534
- * `matrx_connect/middleware/session_cookie.py`), and a cookie rides a media
535
- * request that a header cannot. Every private `<img>`/`<video>` in the platform
536
- * already renders this way.
537
- *
538
- * So `fileMedia` mints the cookie and hands back a STREAMING url — which is
539
- * what lets a person scrub a two-hour recording instead of waiting for a
540
- * gigabyte to arrive. When the mint is refused (no session, an older server,
541
- * a browser blocking third-party cookies) it falls back to reading the bytes
542
- * with the organization header into a `blob:` url — and it SAYS it did, with
543
- * the reason, because a silent fallback that downloads a gigabyte is exactly
544
- * the kind of automatic intervention that must announce itself.
545
- */
546
-
547
- /**
548
- * How the host tells us who is asking. Returns null before sign-in resolves,
549
- * and for a guest, who has no session at all and is still a first-class joiner.
550
- *
551
- * It lives HERE rather than in `tokens.ts` (where it was born) because this is
552
- * now the one module that turns a session into a request.
553
- */
554
- type AccessTokenSource = () => Promise<string | null>;
555
- /** The header aidream's org-admission middleware demands. One definition. */
556
- declare const ORGANIZATION_HEADER = "x-organization-id";
557
- interface MeetApiOptions {
558
- /** aidream's origin. Never a LiveKit or provider URL. */
559
- baseUrl: string;
560
- /** Named on EVERY request; the server refuses one that omits it. */
561
- organizationId: OrganizationId;
562
- accessToken: AccessTokenSource;
563
- fetchImpl?: typeof fetch | undefined;
564
- }
565
- interface MeetApiRequest {
566
- /** An absolute path on the aidream origin, e.g. `/api/v1/meet/token`. */
567
- path: string;
568
- method?: "GET" | "POST" | undefined;
569
- /** JSON body. Serialized here so no caller can forget the content type. */
570
- body?: Record<string, unknown> | undefined;
571
- /**
572
- * `true` — refuse before the wire when there is no session, with the given
573
- * operation named. `false` — send whatever session exists, or none (a guest
574
- * minting a room token is a real, authorized case).
575
- */
576
- requireAuth?: boolean | undefined;
577
- /** Names the caller in every error this raises. */
578
- operation: string;
579
- /** Extra headers. May not override the organization header. */
580
- headers?: Record<string, string> | undefined;
581
- /**
582
- * `"include"` for the ONE request that mints the file-session cookie — a
583
- * `Set-Cookie` on a cross-origin response is dropped without it.
584
- */
585
- credentials?: RequestCredentials | undefined;
586
- /** Cancels the request. The Q&A stream is abortable by the person asking. */
587
- signal?: AbortSignal | undefined;
588
- }
589
- interface MeetApi {
590
- readonly baseUrl: string;
591
- readonly organizationId: OrganizationId;
592
- /** The URL a request to `path` would go to. For diagnostics, never for fetching. */
593
- url(path: string): string;
594
- /** One request, with the organization header and the session already on it. */
595
- request(input: MeetApiRequest): Promise<Response>;
596
- /**
597
- * Read an authenticated file into a `blob:` URL an element can load. The
598
- * caller MUST `URL.revokeObjectURL` it — a leaked one pins the whole file in
599
- * memory for the life of the tab.
600
- */
601
- fetchObjectUrl(input: MeetApiRequest): Promise<string>;
602
- /**
603
- * A url a `<video>`/`<img>` can load for a platform file. Streams when the
604
- * file-session cookie can be minted; otherwise reads the bytes with the
605
- * organization header and says why it had to.
606
- */
607
- fileMedia(fileId: string, operation: string): Promise<MeetFileMedia>;
608
- /** Re-mint the file-session cookie. Returns whether it now exists. */
609
- refreshFileSession(): Promise<boolean>;
610
- }
611
- /** Where a media element's bytes are coming from, and whether that is plan A. */
612
- interface MeetFileMedia {
613
- readonly url: string;
614
- /** `stream` seeks and starts instantly; `bytes` is a fully downloaded blob. */
615
- readonly kind: "stream" | "bytes";
616
- /**
617
- * Set only for `bytes`, and never null-and-hoping: the sentence a surface
618
- * shows to explain why it had to download the whole file first.
619
- */
620
- readonly fallbackReason: string | null;
621
- }
622
- /** `POST /files/session` — mints the `mx_files_session` cookie (files law). */
623
- declare const FILE_SESSION_PATH = "/files/session";
624
- declare function createMeetApi(options: MeetApiOptions): MeetApi;
625
- /** `GET /files/{id}/download?inline=1` — the platform files route (R3). */
626
- declare function fileDownloadPath(fileId: string): string;
627
-
628
- /**
629
- * ONE error classification for the whole package (C22).
630
- *
631
- * The banned alternative is a host catching a LiveKit or PostgREST error and
632
- * deciding for itself whether the camera is busy, the token expired, or the
633
- * meeting is locked — which is how the same five-branch `if` gets copy-pasted
634
- * into every consumer and drifts. Everything this package throws is a
635
- * `MeetError` with a stable `code` and a `remedy` SENTENCE. Nothing fails
636
- * silently, and nothing fails without saying what to do about it.
637
- *
638
- * The device codes exist because "it doesn't work" in a video product is almost
639
- * always one of four specific, differently-remediable things: permission
640
- * denied, no such device, device already in use by another app, or insecure
641
- * context (http, where `getUserMedia` does not exist at all). A single
642
- * "media-error" would make the UI say the wrong sentence three times out of
643
- * four.
644
- */
645
- type MeetErrorCode = "session-unavailable" | "forbidden" | "not-found" | "conflict" | "token" | "transport" | "invalid-response" | "misconfigured" | "permission-denied" | "device-not-found" | "device-in-use" | "insecure-context" | "media-unsupported" | "room-locked" | "lobby-denied" | "capacity"
646
- /** Meet wave 1: the meeting was cancelled — its link resolves, its room never opens. */
647
- | "cancelled" | "unknown";
648
- declare class MeetError extends Error {
649
- readonly code: MeetErrorCode;
650
- /** What to DO. Rendered to the user for the device codes. */
651
- readonly remedy: string;
652
- readonly cause?: unknown;
653
- constructor(code: MeetErrorCode, message: string, remedy: string, cause?: unknown);
654
- /** True when retrying after the host re-establishes a session is the fix. */
655
- get isRetryable(): boolean;
656
- /** True when the user, not the code, must act — the UI shows `remedy`. */
657
- get isUserActionable(): boolean;
658
- }
659
- /**
660
- * THE ONE PLACE a raw database/transport error becomes a typed one. Called at
661
- * every repository boundary; nothing else in the package inspects a raw error,
662
- * and no consumer should ever have to.
663
- */
664
- declare function normalizeMeetError(error: unknown, operation: string): MeetError;
665
- /**
666
- * THE DEVICE ERROR TRANSLATOR — the second-most-copied block in every video app.
667
- *
668
- * `getUserMedia` rejects with DOMException names, and every one of them means
669
- * something a user could fix if we told them. This is the whole reason the
670
- * device layer lives in the package (C22 test 3: less-than-perfect unless done
671
- * perfectly).
672
- */
673
- declare function normalizeDeviceError(error: unknown, operation: string): MeetError;
674
- /** A response whose SHAPE is wrong — the ingress boundary, not the wire. */
675
- declare function invalidResponse(operation: string, detail: string): MeetError;
676
-
677
- /**
678
- * WHO SAID THAT — one answer, every surface, both lanes (MRI-A16).
679
- *
680
- * 🚨 **A RAW LIVEKIT IDENTITY IS NEVER A PERSON'S NAME.** On production
681
- * 2026-09-09 the host's caption rail read *"Ada Lovelace: I planted a marigold
682
- * in the courtyard"* and the guest's — the same meeting, the same second —
683
- * read *"guest:c8-spk-1"*, because the host resolved the name from the durable
684
- * rows it can read and the guest had no fallback at all and printed the token
685
- * (MRI-C8). Two surfaces disagreeing about who is speaking is worse than either
686
- * one being ugly: a person reading the transcript cannot tell whether those are
687
- * one speaker or two.
688
- *
689
- * The cure is that nothing resolves a name for itself. Every source that
690
- * happens to KNOW a name teaches this book — the LiveKit roster, a durable
691
- * `meet_transcript_segments` row, a durable `meet_participants` row, a chat
692
- * message, our own identity — and every surface asks it. Three rules:
693
- *
694
- * 1. **A name is learned, never unlearned.** Someone who leaves the room keeps
695
- * the name they had; a caption that lands after them still says who spoke.
696
- * This is the "last known name" rule, and it is why the book is a store
697
- * field rather than a derivation over the current roster.
698
- * 2. **An identity is not a name.** `learn` refuses a "name" that is the
699
- * identity itself — which is exactly what the LiveKit roster hands back for
700
- * a participant whose token carried no name, and exactly how the token got
701
- * onto the screen in the first place.
702
- * 3. **When nobody knows, SAY nobody knows.** `displayNameFor` answers with an
703
- * honest placeholder — "Unnamed guest" / "Unnamed participant" — never a
704
- * token, and never an invented name. The placeholder is deliberately not
705
- * unique, so surfaces that must keep two unnamed people apart group by
706
- * IDENTITY rather than by the rendered name.
707
- */
708
-
709
- /** Identity → the best display name anything has ever known for it. */
710
- type NameBook = Readonly<Record<string, string>>;
711
- /** A guest's identity is minted `guest:<something>` by the token route. */
712
- declare function isGuestIdentity(identity: ParticipantIdentity | string): boolean;
713
- /**
714
- * Would this string be a real name for this identity? A name equal to the
715
- * identity is the roster's way of saying it has none.
716
- */
717
- declare function isRealName(identity: ParticipantIdentity | string, name: string | null | undefined): boolean;
718
- /**
719
- * Teach the book one identity's name. Returns the SAME book when nothing was
720
- * learned, so a store commit can be skipped rather than waking every subscriber
721
- * on every caption.
722
- */
723
- declare function learnName(book: NameBook, identity: ParticipantIdentity | string, name: string | null | undefined): NameBook;
724
- /** Teach the book several at once — a roster replace, or a durable read. */
725
- declare function learnNames(book: NameBook, entries: readonly {
726
- identity: ParticipantIdentity | string;
727
- displayName?: string | null | undefined;
728
- }[]): NameBook;
729
- /**
730
- * The name a surface renders for `identity`. Never a raw token, never invented.
731
- *
732
- * `preferred` is what the arriving thing itself claimed (a caption's roster
733
- * lookup, a durable row's `speaker` column); it wins when it is a real name,
734
- * because it is the freshest, and teaches the book on its way past.
735
- */
736
- declare function displayNameFor(book: NameBook, identity: ParticipantIdentity | string, preferred?: string | null | undefined): string;
737
-
738
- /**
739
- * LISTEN-ONLY ATTENDANCE — being IN the room is never contingent on a device.
740
- *
741
- * 0.3.1 and every version before it enabled the microphone as the last step of
742
- * `connect()`, inside the same `try` as the join. So a denied, absent, or busy
743
- * microphone did not produce a muted participant — it threw, the join was
744
- * failed with a device remedy, and the user was dropped back to `<PreJoin>`
745
- * having never entered the meeting. The whole in-room surface (tiles, captions,
746
- * live notes, Q&A, the note-taker consent notice) was unreachable to anyone
747
- * without a working mic, which is ordinary: a locked-down laptop, a conference
748
- * room dial-in, a phone on a call, a browser profile that denies capture by
749
- * policy, or a person who simply wants to listen. Every competitor allows it.
750
- *
751
- * THE CLASS, and how it is closed here:
752
- *
753
- * 1. **Connecting and publishing are two different operations.** The engine
754
- * reaches `connected` the moment the transport is up. Media is attempted
755
- * AFTER that, per track, and a rejection never unwinds the join.
756
- * 2. **A track that could not be acquired is STATE, not an exception.** The
757
- * reason and its remedy — the browser's own, translated by
758
- * `normalizeDeviceError` — ride in the snapshot so the UI says WHY the mic
759
- * is off instead of showing a mute button that silently does nothing.
760
- * 3. **Enabling later runs the SAME code path.** `setMicrophoneEnabled(true)`
761
- * from the control bar records exactly what the join attempt records, so a
762
- * person who plugs in a headset mid-meeting is one click from speaking and
763
- * a person whose second attempt also fails gets the same honest sentence.
764
- * A separate "retry" path is how the two drift.
765
- * 4. **The notice is one sentence, defined once.** Both the package's own room
766
- * chrome and any host that renders its own read `listenOnlyNotice()`, so
767
- * there is no second wording to keep in sync.
768
- */
769
-
770
- /** Why a local track is not publishing, in the browser's terms plus a remedy. */
771
- interface MediaUnavailable {
772
- readonly code: string;
773
- readonly message: string;
774
- readonly remedy: string;
775
- }
776
- interface LocalTrackState {
777
- /** True only when the track is actually published. Never optimistic. */
778
- readonly enabled: boolean;
779
- /**
780
- * Set when acquisition was ATTEMPTED and refused. `null` covers both "on"
781
- * and "deliberately off" — a user who unchecked the mic in pre-join is not
782
- * looking at an error, they are looking at a choice they made.
783
- */
784
- readonly unavailable: MediaUnavailable | null;
785
- }
786
- interface LocalMediaState {
787
- readonly microphone: LocalTrackState;
788
- readonly camera: LocalTrackState;
789
- }
790
- declare const IDLE_TRACK: LocalTrackState;
791
- declare const IDLE_MEDIA: LocalMediaState;
792
- declare function unavailableFrom(error: MeetError): MediaUnavailable;
793
- /**
794
- * THE SENTENCE. One definition, so the room chrome, a host's own chrome and the
795
- * tests cannot say three different things about the same fact.
796
- */
797
- declare const LISTEN_ONLY_NOTICE = "You're attending without a microphone \u2014 others can't hear you";
798
- /**
799
- * True when this participant is in the room with NO USABLE microphone — the
800
- * device was refused or is missing. A person who simply muted themselves has a
801
- * working microphone and is not "attending without" one, so a mute never
802
- * counts: `unavailable` is the only signal.
803
- */
804
- declare function isListenOnly(media: LocalMediaState): boolean;
805
- /**
806
- * The notice to render while in the room, or `null` when the mic is live.
807
- * Returns the plain sentence; the reason and remedy, when there is one, are
808
- * rendered beside it from `media.microphone.unavailable`.
809
- */
810
- declare function listenOnlyNotice(media: LocalMediaState): string | null;
811
-
812
- /**
813
- * THE NOTE-TAKER, AS A SURFACE MUST RENDER IT.
814
- *
815
- * 🚨 **THE PACKAGE NEVER GUESSES WHAT THE AGENT IS DOING.** Two facts decide
816
- * everything here and both come from outside this client: the meeting row's
817
- * `metadata.note_taker` (written by the server with a CAS, MRI-A2/D9) and
818
- * whether the agent participant is actually in the LiveKit room. A spinner —
819
- * "starting…" held open by a client-side timer — is the exact lie this file
820
- * exists to prevent: it survives a worker that never came, a dispatch nobody
821
- * claimed, and a job that died on a release.
822
- *
823
- * 🚨 **A REFUSAL IS RENDERED VERBATIM.** `/agent/start` answers a 503 whose body
824
- * carries `reason` and `remedy` written by the readiness gate (D7) — "no worker
825
- * is registered under matrx-note-taker", "every worker is at capacity",
826
- * "the fleet is draining". Those sentences are the truth about the system and
827
- * they reach the screen unedited. This package adds no sentence of its own on
828
- * top of one the server already wrote.
829
- *
830
- * The agent's identity is DETERMINISTIC — `agent:note_taker:<meeting_id>` (D9) —
831
- * which is what makes "is it in the room" answerable without a round trip, and
832
- * what makes a second note-taker impossible to mistake for the first.
833
- */
834
-
835
- /** The one spelling of the agent's participant identity (D9). */
836
- declare const NOTE_TAKER_IDENTITY_PREFIX = "agent:note_taker:";
837
- declare function noteTakerIdentity(meetingId: string): string;
838
- /**
839
- * The participant attribute the note-taker sets on itself so EVERY client —
840
- * including one that cannot read the token metadata — knows a notice is owed
841
- * (D10).
842
- */
843
- declare const CONSENT_NOTICE_ATTRIBUTE = "matrx.agent.consent_notice";
844
- /** Where the state lives on the meeting row. One spelling, one file. */
845
- declare const NOTE_TAKER_METADATA_KEY = "note_taker";
846
- /** The server's refusal, as it wrote it. Never paraphrased. */
847
- interface NoteTakerRefusal {
848
- readonly reason: string;
849
- readonly remedy: string;
850
- readonly status: number;
851
- }
852
- interface NoteTakerStatus {
853
- readonly state: NoteTakerState;
854
- /** The server's own words for why it is in this state, when it gave any. */
855
- readonly detail: string | null;
856
- /** True when the agent participant is in the room RIGHT NOW. */
857
- readonly present: boolean;
858
- /** The last dispatch refusal, rendered verbatim. */
859
- readonly refusal: NoteTakerRefusal | null;
860
- }
861
- /** Is this participant the note-taker (or any agent that owes a notice, D10)? */
862
- declare function isNoteTaker(participant: MeetParticipant): boolean;
863
- /** The agent participant in the room, if any. */
864
- declare function noteTakerParticipant(participants: readonly MeetParticipant[]): MeetParticipant | null;
865
- /**
866
- * THE STATE, from the two facts that decide it.
867
- *
868
- * The durable row is the authority for work. Exact room presence corroborates
869
- * only a durable `transcribing` state; an arbitrary agent, a consent marker,
870
- * or a stale note-taker participant never proves speech capture.
871
- */
872
- declare function noteTakerStatus(args: {
873
- meeting: MeetingRecord | null;
874
- participants: readonly MeetParticipant[];
875
- refusal?: NoteTakerRefusal | null;
876
- }): NoteTakerStatus;
877
- /**
878
- * THE SENTENCE FOR EACH STATE. One place, so the room, the panel and any future
879
- * surface say the same true thing — and so a state can never render as a bare
880
- * enum name.
881
- */
882
- declare function noteTakerLabel(status: NoteTakerStatus): string;
883
-
884
- /**
885
- * MEETING ROOM ITEMS — polls, Q&A, the shared whiteboard and breakout rooms (Meet wave 5).
886
- *
887
- * The server decides every one of them (`aidream/services/meet/collab.py`,
888
- * `breakouts.py`): who may create, launch, vote, ask, answer, draw, erase, open rooms or
889
- * move people — and what each viewer SEES (a draft poll is the host's alone; counts
890
- * reach everyone only once shared; anonymous votes and questions carry no identity;
891
- * a dismissed question stays with the hosts). This file only holds what the server
892
- * said and folds the room's `matrx.meet.collab` packets into it.
893
- *
894
- * Two reads, one truth: `GET /v1/meet/collab` once per join (a late joiner sees every
895
- * poll, question, stroke and the open breakout plan), then packets. Every item is keyed
896
- * by its row id, so the same item arriving twice is one item.
897
- */
898
-
899
- interface MeetPollOption {
900
- readonly id: string;
901
- readonly text: string;
902
- /** Present when this viewer may see counts (a host, or results shared). */
903
- readonly votes?: number | undefined;
904
- /** Who picked it — hosts only, never for an anonymous poll. */
905
- readonly voters?: readonly string[] | undefined;
906
- }
907
- type MeetPollState = "draft" | "live" | "closed";
908
- interface MeetPoll {
909
- readonly id: string;
910
- readonly question: string;
911
- readonly options: readonly MeetPollOption[];
912
- readonly multiple: boolean;
913
- readonly anonymous: boolean;
914
- readonly state: MeetPollState;
915
- readonly resultsShared: boolean;
916
- readonly voterCount: number | null;
917
- /** This viewer's own answer — kept locally, since a room packet cannot know it. */
918
- readonly myVote: readonly string[];
919
- readonly createdByName: string;
920
- readonly createdAt: string | null;
921
- }
922
- type MeetQuestionState = "open" | "answered" | "dismissed";
923
- interface MeetQuestion {
924
- readonly id: string;
925
- readonly text: string;
926
- readonly askerName: string;
927
- readonly anonymous: boolean;
928
- readonly state: MeetQuestionState;
929
- readonly answer: string | null;
930
- readonly answeredByName: string | null;
931
- readonly upvotes: number;
932
- readonly upvotedByMe: boolean;
933
- readonly mine: boolean;
934
- readonly createdAt: string | null;
935
- }
936
- interface MeetStroke {
937
- readonly id: string;
938
- /** Points in the board's own 0..1 space. */
939
- readonly points: readonly (readonly [number, number])[];
940
- readonly color: string;
941
- readonly width: number;
942
- readonly tool: "pen" | "highlighter";
943
- readonly by: string;
944
- readonly byName: string;
945
- /** True while this browser's stroke has not come back from the server. */
946
- readonly pending?: boolean | undefined;
947
- }
948
- interface MeetBreakoutRoom {
949
- readonly id: string;
950
- readonly name: string;
951
- readonly identities: readonly string[];
952
- }
953
- interface MeetBreakoutBroadcast {
954
- readonly text: string;
955
- readonly by: string;
956
- readonly at: string;
957
- }
958
- interface MeetBreakoutPlan {
959
- readonly id: string;
960
- readonly state: "open" | "closed";
961
- readonly rooms: readonly MeetBreakoutRoom[];
962
- readonly allowChoose: boolean;
963
- readonly minutes: number;
964
- readonly openedAt: string | null;
965
- readonly endsAt: string | null;
966
- readonly closedAt: string | null;
967
- readonly broadcasts: readonly MeetBreakoutBroadcast[];
968
- }
969
- /** The org's knobs for this meeting — which controls EXIST (absent, never dead). */
970
- interface MeetCollabSettings {
971
- readonly pollsEnabled: boolean;
972
- readonly pollMaxOptions: number;
973
- readonly qaEnabled: boolean;
974
- readonly qaAnonymousAllowed: boolean;
975
- readonly whiteboardEnabled: boolean;
976
- readonly breakoutsEnabled: boolean;
977
- readonly breakoutMaxRooms: number;
978
- readonly breakoutDefaultMinutes: number;
979
- readonly captionTranslationEnabled: boolean;
980
- /** Empty = the viewer's own language. */
981
- readonly captionTranslationLanguage: string;
982
- }
983
- type CollabPanel = "polls" | "qa" | "whiteboard" | "breakouts";
984
- interface CollabSnapshot {
985
- readonly loaded: boolean;
986
- /** Why the state could not be read, in words — or null. */
987
- readonly error: string | null;
988
- readonly settings: MeetCollabSettings | null;
989
- readonly polls: readonly MeetPoll[];
990
- readonly questions: readonly MeetQuestion[];
991
- readonly strokes: readonly MeetStroke[];
992
- readonly breakout: MeetBreakoutPlan | null;
993
- /** The newest message a host sent to every breakout room, for the banner. */
994
- readonly broadcast: MeetBreakoutBroadcast | null;
995
- /** A host moved THIS person (or everyone came back): where to go. `undefined` = nothing asked. */
996
- readonly moveRequest: {
997
- readonly roomId: string | null;
998
- readonly at: number;
999
- } | null;
1000
- /** Something asked a panel to open (a poll launched, a question arrived for a host). */
1001
- readonly panelRequest: {
1002
- readonly panel: CollabPanel;
1003
- readonly at: number;
1004
- } | null;
1005
- /** Caption translations for THIS viewer: line id → text, for `captionLanguage`. */
1006
- readonly captionLanguage: string | null;
1007
- readonly captionTranslations: Readonly<Record<string, string>>;
1008
- readonly captionTranslationError: string | null;
1009
- }
1010
- declare const EMPTY_COLLAB: CollabSnapshot;
1011
- type Row$1 = Record<string, unknown>;
1012
- /** The server's `GET /collab` body → a loaded snapshot (keeps this viewer's caption state). */
1013
- declare function collabFromState(body: Row$1, previous: CollabSnapshot): CollabSnapshot;
1014
- /**
1015
- * Fold one server item into the snapshot. `fromRoom` = it came on the room broadcast,
1016
- * which cannot know THIS viewer's vote, upvote or authorship — those are kept from what
1017
- * the viewer's own reads and answers already said.
1018
- */
1019
- declare function withItem(collab: CollabSnapshot, row: Row$1, fromRoom?: boolean): CollabSnapshot;
1020
- interface CollabPacketContext {
1021
- readonly localIdentity: ParticipantIdentity | null;
1022
- /** Hosts and co-hosts receive a second, fuller packet; they ignore the room copy. */
1023
- readonly isHost: boolean;
1024
- readonly now: number;
1025
- }
1026
- /**
1027
- * One `matrx.meet.collab` packet → the next snapshot, or `null` when it must be dropped
1028
- * (with the reason in `dropped`). Pure; the room-event dispatcher patches the store.
1029
- */
1030
- declare function applyCollabPacket(collab: CollabSnapshot, payload: Row$1, context: CollabPacketContext): {
1031
- next: CollabSnapshot | null;
1032
- dropped?: string;
1033
- };
1034
- /** The breakout room a person is assigned to, or null. */
1035
- declare function assignedRoom(plan: MeetBreakoutPlan | null, identity: string | null): MeetBreakoutRoom | null;
1036
- /**
1037
- * Split people into `rooms` rooms, round-robin in the order given — Zoom's "assign
1038
- * automatically". Hosts are left out (they visit); pass them in `exclude`.
1039
- */
1040
- declare function autoAssign(identities: readonly string[], rooms: number, exclude?: readonly string[]): string[][];
1041
- /** Milliseconds until the plan's timer ends, or null when it has none. */
1042
- declare function breakoutRemainingMs(plan: MeetBreakoutPlan | null, now: number): number | null;
1043
- /** The language translated captions start in: the knob, else the viewer's own. */
1044
- declare function defaultCaptionLanguage(settings: MeetCollabSettings | null, viewerLanguage: string | null | undefined): string;
1045
- /** Languages offered in the captions menu (tags the translator understands). */
1046
- declare const CAPTION_LANGUAGES: readonly {
1047
- readonly tag: string;
1048
- readonly name: string;
1049
- }[];
1050
- interface MeetingActivities {
1051
- /** Polls (with results), questions, whiteboard strokes — or null when refused. */
1052
- readonly collab: CollabSnapshot | null;
1053
- readonly refusal: {
1054
- readonly message: string;
1055
- readonly remedy: string;
1056
- } | null;
1057
- }
1058
- /**
1059
- * THE MEETING'S POLLS, Q&A AND WHITEBOARD, AFTER IT (Meet wave 5) — for the record page.
1060
- * The same `GET /v1/meet/collab` the room reads on join; a refusal is returned as data,
1061
- * so the record still renders everything else.
1062
- */
1063
- declare function loadMeetingActivities(api: MeetApi, meetingId: string, roomToken?: string | null): Promise<MeetingActivities>;
1064
-
1065
- /**
1066
- * THE ROOM SNAPSHOT — one immutable object the UI renders and nothing else.
1067
- *
1068
- * Deliberately NOT Redux, NOT a context full of `useState`, and NOT a bag of
1069
- * refs: one store with `getSnapshot`/`subscribe` is exactly what
1070
- * `useSyncExternalStore` wants, which is what makes tearing impossible when a
1071
- * roster event and a speaker event land in the same tick.
1072
- *
1073
- * The rules encoded here:
1074
- *
1075
- * - **The roster is replaced wholesale, never patched.** The driver hands us
1076
- * the full truth on every media event (see `room-driver.ts`); merging deltas
1077
- * from six event sources is how a tile for someone who left stays on screen.
1078
- * - **Ephemera EXPIRE.** Reactions and speaking flags are time-boxed in the
1079
- * snapshot, so a dropped "stop" event cannot leave a permanent 🎉 or a
1080
- * permanently-speaking ring. (Nothing here relies on receiving an end event.)
1081
- * - **An interim transcript line is REPLACED in place by its final.** Appending
1082
- * both is how a transcript ends up saying everything twice.
1083
- */
1084
-
1085
- interface RoomChatMessage {
1086
- readonly id: string;
1087
- readonly identity: ParticipantIdentity;
1088
- readonly displayName: string;
1089
- readonly text: string;
1090
- readonly at: string;
1091
- /**
1092
- * Set for a PRIVATE message: the one identity it was addressed to (from the
1093
- * sender's side) or this participant (from the receiver's). Private messages
1094
- * travel the data plane only and are never saved (Meet wave 2).
1095
- */
1096
- readonly to?: ParticipantIdentity | null | undefined;
1097
- /** True once the server has SAVED it — a message everyone sees, late joiners too. */
1098
- readonly saved?: boolean | undefined;
1099
- /** A send the server refused, with its sentence. The message stays visible, marked. */
1100
- readonly failed?: string | undefined;
1101
- }
1102
- /**
1103
- * A HOST ACTED ON *ME* (Meet wave 2) — the server's own `matrx.meet.moderation`
1104
- * packet, addressed to this participant. Rendered once, dismissible, in words.
1105
- */
1106
- interface ModerationNotice {
1107
- readonly kind: "muted" | "ask-unmute" | "removed" | "role";
1108
- readonly text: string;
1109
- readonly at: number;
1110
- }
1111
- /** What this participant chose for their own media. Remembered per browser. */
1112
- interface MediaEffects {
1113
- readonly noiseSuppression: boolean;
1114
- readonly background: "none" | "blur" | "image";
1115
- readonly backgroundImage: string | null;
1116
- }
1117
- declare const DEFAULT_EFFECTS: MediaEffects;
1118
- interface RoomReaction {
1119
- readonly id: string;
1120
- readonly identity: ParticipantIdentity;
1121
- readonly emoji: string;
1122
- readonly at: number;
1123
- }
1124
- /**
1125
- * 🚨 WHAT THE SERVER GRANTED THIS JOINER, straight off the token response
1126
- * (MRI-A11). It is placed in the snapshot the instant the token lands — before
1127
- * the LiveKit roster exists at all — so a host's controls are correct on the
1128
- * first frame of the room rather than one roster event later. The roster entry
1129
- * for the local participant carries the SAME decision as a LiveKit attribute;
1130
- * the two agree because one server statement produced both, and the UI reads
1131
- * whichever has arrived.
1132
- */
1133
- interface MeetGrants {
1134
- readonly role: ParticipantRole;
1135
- readonly canRecord: boolean;
1136
- readonly canPublish: boolean;
1137
- readonly canSubscribe: boolean;
1138
- }
1139
- /** No token yet grants nothing. Never "assume host until told otherwise". */
1140
- declare const NO_GRANTS: MeetGrants;
1141
- interface MeetSnapshot {
1142
- readonly phase: RoomPhase;
1143
- readonly meeting: MeetingRecord | null;
1144
- readonly localIdentity: ParticipantIdentity | null;
1145
- /** The server's word on what THIS joiner may do. See `MeetGrants`. */
1146
- readonly grants: MeetGrants;
1147
- readonly participants: readonly MeetParticipant[];
1148
- readonly activeSpeakers: readonly ParticipantIdentity[];
1149
- readonly pinned: ParticipantIdentity | null;
1150
- readonly layout: StageLayout;
1151
- readonly lobby: readonly LobbyEntry[];
1152
- readonly locked: boolean;
1153
- /**
1154
- * 🚨 THE DURABLE `meet_recordings.state`, not a client's hope. Through 0.4.7
1155
- * this only ever held what THIS browser optimistically patched — `starting`
1156
- * on the click, `stopping` on the stop — so the indicator read "Starting
1157
- * recording…" for the whole meeting while a perfectly healthy egress ran, and
1158
- * "Stopping recording…" forever after (MRI-C8). The server's transitions
1159
- * arrive on two transports now: the `meet_recordings` row for a member, and
1160
- * a `matrx.meet.recording` room event for a guest, who has no database.
1161
- */
1162
- readonly recording: RecordingState;
1163
- readonly recordingStartedBy: UserId | null;
1164
- /**
1165
- * WHICH transport last spoke, so the two cannot undo each other (D16). The
1166
- * database wins: `"record"` is never overwritten by `"room"` or `"local"`.
1167
- */
1168
- readonly recordingSource: RecordingSource;
1169
- /**
1170
- * WHICH recording this state is about. It is what tells a LATE packet for a
1171
- * finished recording apart from the FIRST packet of the next one — without
1172
- * it, "the database wins" either blocks a second recording forever or lets a
1173
- * stale packet drag a finished one back to `recording`.
1174
- */
1175
- readonly recordingId: string | null;
1176
- readonly chat: readonly RoomChatMessage[];
1177
- /** Whether the SAVED chat has been read for this meeting (late joiners see history). */
1178
- readonly chatLoaded: boolean;
1179
- /** Why the saved chat could not be read on join, in words — or null. */
1180
- readonly chatHistoryError: string | null;
1181
- readonly reactions: readonly RoomReaction[];
1182
- /**
1183
- * The participant a HOST spotlighted for everyone, from the room's metadata
1184
- * (server-set, delivered on join). A local pin outranks it on this screen only.
1185
- */
1186
- readonly spotlight: ParticipantIdentity | null;
1187
- /** The last thing a host did to THIS participant, in words. Dismissible. */
1188
- readonly moderationNotice: ModerationNotice | null;
1189
- /**
1190
- * The server's answer to the last host action THIS participant took ("Muted 3
1191
- * people", or the refusal). One place, so whichever surface acted — a tile's
1192
- * menu, the People panel, More options — the sentence survives the menu closing.
1193
- */
1194
- readonly actionNotice: {
1195
- readonly text: string;
1196
- readonly failed: boolean;
1197
- readonly at: number;
1198
- } | null;
1199
- /**
1200
- * "Message privately" asked for from ANY per-person menu — a tile's, the
1201
- * People panel's — and answered by the one `ControlBar` that owns the chat
1202
- * panel (0.7.8). Through 0.7.7 only the People panel could open a private
1203
- * chat, so the Board's tile menu (and the Room's) had no such item while the
1204
- * People panel did: two menus for one person that disagreed.
1205
- */
1206
- readonly privateChatRequest: {
1207
- readonly to: ParticipantIdentity;
1208
- readonly at: number;
1209
- } | null;
1210
- readonly effects: MediaEffects;
1211
- /**
1212
- * Captions AND durable rows, reconciled into one list (D16). A record
1213
- * replaces the caption it supersedes; a caption never overwrites a record.
1214
- */
1215
- readonly transcript: readonly TranscriptSegment[];
1216
- readonly notes: readonly MeetingNote[];
1217
- /**
1218
- * 🚨 IDENTITY → THE BEST NAME ANYTHING HAS EVER KNOWN (MRI-A16). Learned from
1219
- * every source that has one — the roster, a durable segment, a durable
1220
- * attendance row, a chat message — and never unlearned, so a caption that
1221
- * lands after somebody leaves still says who spoke. It exists because the
1222
- * host and the guest rendered DIFFERENT names for the same speaker in the
1223
- * same second, one of them a raw LiveKit token (MRI-C8). See `core/names.ts`.
1224
- */
1225
- readonly names: NameBook;
1226
- /**
1227
- * The wrap-up pass ANNOUNCED itself (MRI-A12). `null` means it has not been
1228
- * announced in this room — which is not the same as "no summary exists", and
1229
- * no surface may render it as such.
1230
- */
1231
- readonly wrapUp: WrapUpAnnouncement | null;
1232
- /**
1233
- * The server's last refusal to dispatch the note-taker, in ITS words (D7).
1234
- * Rendered verbatim; never replaced by a spinner or a house sentence.
1235
- */
1236
- readonly noteTakerRefusal: NoteTakerRefusal | null;
1237
- /**
1238
- * What the LOCAL participant is actually publishing, and why not when it is
1239
- * not. Attendance never depends on it (see `attendance.ts`): a refused
1240
- * microphone is a state with a remedy, not a failed join.
1241
- */
1242
- readonly media: LocalMediaState;
1243
- /** The last failure, with its remedy. Never null-and-hoping. */
1244
- readonly failure: {
1245
- readonly message: string;
1246
- readonly remedy: string;
1247
- } | null;
1248
- /**
1249
- * Meet wave 5: polls, Q&A, the shared whiteboard and the breakout plan — what the
1250
- * server said, per viewer (`core/collab.ts`). Read once per join, then packets.
1251
- */
1252
- readonly collab: CollabSnapshot;
1253
- /** The breakout room THIS participant is in, or null for the main room. */
1254
- readonly breakoutRoomId: string | null;
1255
- }
1256
- /** Reactions live this long. A dropped end event cannot outlive it. */
1257
- declare const REACTION_TTL_MS = 5000;
1258
- interface MeetStore {
1259
- snapshot(): MeetSnapshot;
1260
- subscribe(listener: () => void): () => void;
1261
- patch(next: Partial<MeetSnapshot>): void;
1262
- setParticipants(participants: readonly MeetParticipant[]): void;
1263
- setSpeaking(identities: readonly ParticipantIdentity[]): void;
1264
- setQuality(identity: ParticipantIdentity, quality: ConnectionQuality): void;
1265
- /**
1266
- * Raise or lower a hand. `at` is WHEN it went up, which is the hand queue's
1267
- * order — first raised, first called on, the way Meet orders it.
1268
- */
1269
- setHand(identity: ParticipantIdentity, raised: boolean, at?: number): void;
1270
- addChat(message: RoomChatMessage): void;
1271
- /** The saved chat, read once per join. Merged with anything that arrived live. */
1272
- loadChat(messages: readonly RoomChatMessage[]): void;
1273
- /** Replace a pending message (by id) with what the server saved, or mark it failed. */
1274
- settleChat(localId: string, next: RoomChatMessage | {
1275
- failed: string;
1276
- }): void;
1277
- addReaction(reaction: RoomReaction): void;
1278
- /** Drops reactions past their TTL. Called by the UI's animation frame. */
1279
- expireReactions(now: number): void;
1280
- /**
1281
- * The recording reached a state, and WHO says so. `"local"` is this browser's
1282
- * optimistic `starting`/`stopping`; `"room"` is the server's packet on the
1283
- * media plane; `"record"` is the durable row. Precedence is D16's: the
1284
- * database's word stands until the database changes it.
1285
- */
1286
- setRecording(state: RecordingState, by: UserId | null, source: RecordingSource, recordingId?: string | null): void;
1287
- upsertLobby(entry: LobbyEntry): void;
1288
- setAdmission(identity: ParticipantIdentity, state: AdmissionState, by: UserId | null): void;
1289
- /**
1290
- * Teach the name book. Every source that knows a name calls this, and only
1291
- * a REAL name is learned — an identity handed back as its own name is not one.
1292
- */
1293
- learn(entries: readonly {
1294
- identity: ParticipantIdentity;
1295
- displayName?: string | null;
1296
- }[]): void;
1297
- /** A live caption from the media plane. A HINT (D16). */
1298
- ingestCaption(segment: TranscriptSegment): void;
1299
- /** A durable `meet_transcript_segments` row. THE TRANSCRIPT (D16). */
1300
- ingestRecord(segment: TranscriptSegment): void;
1301
- /** Durable notes are re-written in place — a re-run rewrites the same ids. */
1302
- upsertNote(note: MeetingNote): void;
1303
- /** The wrap-up pass finished. Announced on the media plane only (MRI-A12). */
1304
- announceWrapUp(announcement: WrapUpAnnouncement): void;
1305
- /** Record what a local track ACTUALLY did — published, or refused and why. */
1306
- setLocalTrack(kind: "microphone" | "camera", state: LocalTrackState): void;
1307
- reset(): void;
1308
- }
1309
- declare function createMeetStore(initial?: Partial<MeetSnapshot>): MeetStore;
1310
-
1311
- /**
1312
- * AI-NATIVE, ON BY DEFAULT — and SERVER-OWNED as of 0.3.0 (R4, D2, D16).
1313
- *
1314
- * 🚨 **THE PACKAGE NO LONGER KNOWS WHO ANSWERS.** Through 0.2.1 a host injected
1315
- * `MeetAgents` — four database row ids — and this module ran the agent itself,
1316
- * shipping the transcript up as `variables`. Both halves of that are gone:
1317
- *
1318
- * 1. **No agent id crosses this boundary, and none is injected.** Reasoning runs
1319
- * behind Mandates whose Holder is resolved from the database at run time
1320
- * (`meet.live_notes`, `meet.live_intelligence`, `meet.wrap_up`), so an
1321
- * organization rebinds the job in the Mandate admin with no release of this
1322
- * package and no change in the host. A host that had to supply an id was a
1323
- * host that could supply the WRONG id, and a capability that silently
1324
- * disappeared when nobody had filled a config line.
1325
- * 2. **No transcript crosses this boundary either.** `POST /v1/meet/intelligence/ask`
1326
- * takes `{meeting_id, question}` and NOTHING else; the answer is built
1327
- * server-side from durable rows. Captions are hints and this client holds
1328
- * only hints (D16) — sending them would be sending the model our guesses.
1329
- *
1330
- * What is left here is small and honest: ask a question and render the answer
1331
- * as it streams, summon or dismiss the note-taker, end the meeting for
1332
- * everyone, and report the server's refusal in the server's own words.
1333
- *
1334
- * 🚨 **THE USER-INPUT LAW still holds** and is now the server's to keep:
1335
- * `question` is the one genuine human utterance in this module, and it is the
1336
- * only free text this package sends anywhere near a model.
1337
- */
1338
-
1339
- interface MeetAiOptions {
1340
- /** The ONE org-aware aidream client (MRI-A16). Owns base url and headers. */
1341
- api: MeetApi;
1342
- store: MeetStore;
1343
- /**
1344
- * Resolves the current meeting's room pass for a guest Q&A request. The
1345
- * provider owns this because it alone has both the active room and its
1346
- * persisted guest pass. `null` leaves the header absent for signed-in record
1347
- * Q&A, where the session remains the credential.
1348
- */
1349
- resolveRoomToken?: (() => string | null) | undefined;
1350
- }
1351
- /** What the answer was built from. The server measures it; we never guess it. */
1352
- interface MeetAnswerCoverage {
1353
- readonly segmentsUsed: number;
1354
- readonly segmentsTotal: number;
1355
- readonly truncated: boolean;
1356
- readonly speakers: readonly string[];
1357
- }
1358
- interface MeetAiAnswer {
1359
- readonly text: string;
1360
- readonly coverage: MeetAnswerCoverage | null;
1361
- }
1362
- interface AskArgs {
1363
- readonly meetingId: MeetingId;
1364
- readonly question: string;
1365
- readonly signal?: AbortSignal | undefined;
1366
- /** Called with the answer so far, every time it grows. Rendering is live. */
1367
- readonly onText?: ((text: string) => void) | undefined;
1368
- /** The server's phase, verbatim, so a slow answer is never a bare spinner. */
1369
- readonly onPhase?: ((phase: string) => void) | undefined;
1370
- }
1371
- interface NoteTakerDispatch {
1372
- readonly dispatched: boolean;
1373
- /** The server's sentence. Never empty on a refusal. */
1374
- readonly detail: string;
1375
- }
1376
- interface MeetAi {
1377
- /** In-meeting Q&A. The ONE human utterance this package sends (D16). */
1378
- ask(args: AskArgs): Promise<MeetAiAnswer>;
1379
- /** Ask aidream to dispatch the note-taker INTO the room. Host-only. */
1380
- startNoteTaker(args: {
1381
- meetingId: MeetingId;
1382
- roomName: RoomName;
1383
- }): Promise<NoteTakerDispatch>;
1384
- /** Dismiss it. Nothing automatic brings a dismissed note-taker back (D9). */
1385
- stopNoteTaker(args: {
1386
- meetingId: MeetingId;
1387
- }): Promise<NoteTakerDispatch>;
1388
- }
1389
- /** The event vocabulary `/intelligence/ask` emits, pinned by the server's FEATURE.md. */
1390
- declare const MEET_ANSWER_EVENTS: {
1391
- readonly chunk: "chunk";
1392
- readonly info: "info";
1393
- readonly phase: "phase";
1394
- readonly error: "error";
1395
- readonly end: "end";
1396
- };
1397
- /** The `info` event carrying what the answer was built from. */
1398
- declare const MEET_ANSWER_CONTEXT_EVENT = "meet_intelligence_context";
1399
-
1400
- interface ServerRefusal {
1401
- readonly reason: string;
1402
- readonly remedy: string;
1403
- }
1404
- /**
1405
- * THE SERVER'S REFUSAL, READ RATHER THAN RE-WRITTEN.
1406
- *
1407
- * aidream answers a refused dispatch with a body carrying its own `reason` and
1408
- * `remedy` — "no worker is registered under matrx-note-taker", and what to do
1409
- * about it. FastAPI nests a raised `HTTPException` under `detail`, and a
1410
- * validation failure produces a list. All three shapes are read here so the
1411
- * screen shows the system's own words instead of "something went wrong".
1412
- */
1413
- declare function readServerRefusal(body: unknown): ServerRefusal | null;
1414
- declare function createMeetAi(options: MeetAiOptions): MeetAi;
1415
-
1416
- /**
1417
- * CAPTIONS ARE HINTS; SEGMENTS ARE TRUTH (D16).
1418
- *
1419
- * Two independent streams describe the same speech and they arrive out of order
1420
- * with different identities:
1421
- *
1422
- * 1. **Captions** ride the LiveKit `lk.transcription` text stream, keyed by
1423
- * `lk.segment_id`, interim-then-final, milliseconds behind the voice. They
1424
- * exist so a person can READ the room live. Their `sender_identity` is
1425
- * supplied by the publishing client and LiveKit does not validate it, so
1426
- * nothing downstream — notes, Q&A, the wrap-up — may ever read them.
1427
- * 2. **Records** are `communication.meet_transcript_segments` rows, written
1428
- * server-side by the note-taker with a deterministic id (D6) and delivered
1429
- * here by Postgres Changes. They are the meeting's transcript.
1430
- *
1431
- * The reconciliation below is what keeps the screen from saying everything
1432
- * twice. A record REPLACES the caption it supersedes — matched by the caption
1433
- * id the worker carries into the row, and failing that by speaker + text — and
1434
- * a caption never overwrites a record, however late it lands.
1435
- *
1436
- * Everything here is a pure function over a list so the rules are provable
1437
- * without a store, a socket, or a browser.
1438
- */
1439
-
1440
- /** Comparable text: case, punctuation and spacing are not differences. */
1441
- declare function normalizeSpoken(text: string): string;
1442
- /**
1443
- * A caption arrived. Interim replaces interim in place; a final never loses to a
1444
- * late interim; a record is never touched.
1445
- */
1446
- declare function mergeCaption(list: readonly TranscriptSegment[], caption: TranscriptSegment): readonly TranscriptSegment[];
1447
- /**
1448
- * A durable row arrived. It replaces the caption it supersedes — in place, so
1449
- * the line does not jump down the panel — or lands as a new line if no caption
1450
- * ever showed it (a surface that joined late, or captions that never came).
1451
- */
1452
- declare function mergeRecord(list: readonly TranscriptSegment[], record: TranscriptSegment): readonly TranscriptSegment[];
1453
- /** The transcript a person reads: chronological, and never an empty line. */
1454
- declare function readableTranscript(list: readonly TranscriptSegment[]): readonly TranscriptSegment[];
1455
- /** Build a caption line from the media plane. */
1456
- declare function captionSegment(args: {
1457
- captionId: string;
1458
- meetingId: TranscriptSegment["meetingId"];
1459
- identity: ParticipantIdentity;
1460
- speaker: string;
1461
- text: string;
1462
- final: boolean;
1463
- startedAt: string;
1464
- }): TranscriptSegment;
1465
-
1466
- /**
1467
- * THE DATA CONTRACT — the ONE place a meet table or RPC name exists.
1468
- *
1469
- * 🚨 **STATE OF THE SCHEMA (read this before believing a green test).** The
1470
- * `communication.meet_*` tables below are the canonical schema DESIGNED by this
1471
- * package per R8; they are **not deployed to Matrx Main yet**. They were placed
1472
- * by inspecting the live database on 2026-08-31: `communication` is the schema
1473
- * that already holds `dm_*` (messaging) and `sms_*` (telephony), so meet takes
1474
- * the `meet_*` prefix in the same schema rather than inventing a new one. Until
1475
- * the migration lands, every read here fails LOUDLY with `not-found` and a
1476
- * remedy naming this file — never silently, and never with a fabricated row.
1477
- * The migration and the aidream server half are one deliverable; see FEATURE.md
1478
- * § Deliberately NOT in this package.
1479
- *
1480
- * Four rules hold this file together — the same four that hold messaging's:
1481
- *
1482
- * 1. **Org is explicit on every write (R5).** Not defaulted by a trigger we
1483
- * hope fires — passed, and refused in-package when absent.
1484
- * 2. **Authorization is the database's job.** There is no permission branch in
1485
- * this file. An RPC that says no is a `forbidden` MeetError, never a
1486
- * client-side re-decision. Guest capability is scoped by the room TOKEN, not
1487
- * by a row this client could write.
1488
- * 3. **Writes go through SECURITY DEFINER RPCs; reads are RLS-backed table
1489
- * reads** (C17's hybrid ruling). Every write here is an RPC, which is also
1490
- * what keeps admission and settle-a-call ATOMIC — two hosts clicking "admit"
1491
- * and a callee accepting on two devices are the ordinary case, not the edge.
1492
- * 4. **Pagination is keyset, terminated by a unique column.** `(created_at,
1493
- * id)` — never a timestamp alone, which duplicates or skips rows whenever
1494
- * two share a millisecond.
1495
- *
1496
- * The Supabase client is accepted STRUCTURALLY, through the shape
1497
- * `@ai-matrx/messaging` already owns. One definition of "a Supabase client" for
1498
- * the whole communications family; this package adds no second copy.
1499
- *
1500
- * 🚨 AND THE PUBLIC BOUNDARY IS THE SHALLOW ONE (0.2.1). `RepositoryOptions`
1501
- * and `<MeetProvider client>` take `MessagingSupabaseClient` — method names and
1502
- * arity, results as `unknown` — never the rich `SupabaseLike`. Checking a
1503
- * host's real `SupabaseClient<Database>` (built from generated types) against
1504
- * the rich shape makes tsc chase PostgREST's schema machinery until it bails
1505
- * with `TS2589: Type instantiation is excessively deep and possibly infinite`,
1506
- * so the only way to pass the client a host ALREADY HAS was a cast — which is
1507
- * what the first real consumer had to write (register MRI-D1). Messaging 0.9.0
1508
- * learned this and wrote it down; this package did not apply the lesson until
1509
- * now. The rich shape stays the INTERNAL contract, and we narrow to it ONCE,
1510
- * here, in-package.
1511
- */
1512
-
1513
- /** The schema. Meet is never in `public`. */
1514
- declare const MEET_SCHEMA = "communication";
1515
- declare const TABLES: {
1516
- readonly meetings: "meet_meetings";
1517
- readonly participants: "meet_participants";
1518
- readonly invites: "meet_call_invites";
1519
- readonly recordings: "meet_recordings";
1520
- readonly transcripts: "meet_transcript_segments";
1521
- readonly notes: "meet_notes";
1522
- /** Wave 1 (0.8): who a meeting was sent to, and what they answered. */
1523
- readonly invitees: "meet_invitees";
1524
- /** Wave 1 (0.8): one occurrence of a series cancelled or moved. */
1525
- readonly occurrenceOverrides: "meet_occurrence_overrides";
1526
- };
1527
- declare const RPCS: {
1528
- /** Atomic: create the invite row and stamp its expiry in one statement. */
1529
- readonly createCallInvite: "meet_create_call_invite";
1530
- /** Atomic settle. Refuses a transition out of a terminal state. */
1531
- readonly settleCallInvite: "meet_settle_call_invite";
1532
- /** Every invite still ringing this user — the reconnect backfill door. */
1533
- readonly pendingCallInvites: "meet_pending_call_invites";
1534
- /** Instant meeting, or the durable one behind a recurring link. */
1535
- readonly getOrCreateMeeting: "meet_get_or_create_meeting";
1536
- /** Resolve a durable public link, before/during/after its scheduled time. */
1537
- readonly meetingBySlug: "meet_meeting_by_slug";
1538
- readonly admit: "meet_admit_participant";
1539
- readonly setLock: "meet_set_lock";
1540
- readonly endMeeting: "meet_end_meeting";
1541
- readonly recordConsent: "meet_record_consent";
1542
- /** Schedule with every setting at create; unset settings come from the knobs. */
1543
- readonly scheduleMeeting: "meet_schedule_meeting";
1544
- /** Edit / reschedule — a JSON patch with a version compare-and-swap. */
1545
- readonly updateMeeting: "meet_update_meeting";
1546
- /** Cancel is a state (`cancelled_at`); the link keeps resolving. */
1547
- readonly cancelMeeting: "meet_cancel_meeting";
1548
- /** Archive (soft delete) — the invitations and exceptions cascade. */
1549
- readonly archiveMeeting: "meet_archive_meeting";
1550
- readonly restoreMeeting: "meet_restore_meeting";
1551
- /** Cancel, move or restore ONE occurrence of a series by its original start. */
1552
- readonly setOccurrence: "meet_set_occurrence";
1553
- readonly addInvitees: "meet_add_invitees";
1554
- readonly setInviteeRole: "meet_set_invitee_role";
1555
- readonly removeInvitee: "meet_remove_invitee";
1556
- /** A signed-in invitee answers for themselves. */
1557
- readonly respond: "meet_respond";
1558
- /** The emailed RSVP link (no account): read and answer. */
1559
- readonly invitationByToken: "meet_invitation_by_token";
1560
- readonly respondByToken: "meet_respond_by_token";
1561
- /** One meeting's occurrences between two instants (INVOKER — the reader's RLS). */
1562
- readonly meetingOccurrences: "meet_meeting_occurrences";
1563
- /** Every occurrence the reader can see between two instants — the Upcoming list. */
1564
- readonly occurrencesBetween: "meet_occurrences_between";
1565
- /** The recordings library. */
1566
- readonly myRecordings: "meet_my_recordings";
1567
- /** Titles, agendas, current notes and final transcript lines; snippets mark hits with ⟦ ⟧. */
1568
- readonly search: "meet_search";
1569
- };
1570
- type Row = Record<string, unknown>;
1571
- interface CreateCallInviteInput {
1572
- readonly roomName: RoomName;
1573
- readonly mode: CallMode;
1574
- readonly callerUserId: UserId;
1575
- readonly callerName: string;
1576
- readonly callerAvatarUrl: string | null;
1577
- readonly calleeUserIds: readonly UserId[];
1578
- readonly organizationId: OrganizationId;
1579
- readonly conversationId: string | null;
1580
- readonly expiresAt: string;
1581
- }
1582
- interface CreateMeetingInput {
1583
- readonly organizationId: OrganizationId;
1584
- readonly hostUserId: UserId;
1585
- readonly title: string;
1586
- readonly kind: MeetingKind;
1587
- readonly scheduledFor?: string | null;
1588
- readonly scheduledDurationMinutes?: number | null;
1589
- readonly lobbyEnabled?: boolean;
1590
- readonly recordingPolicy?: RecordingPolicy;
1591
- readonly aiEnabled?: boolean;
1592
- /** Reuse a durable slug (a recurring meeting keeps ONE link — R3). */
1593
- readonly slug?: string | undefined;
1594
- }
1595
- interface MeetRepository {
1596
- readonly schema: string;
1597
- readonly tables: typeof TABLES;
1598
- projectInvite(row: Row): CallInvite;
1599
- projectMeeting(row: Row): MeetingRecord;
1600
- createCallInvite(input: CreateCallInviteInput): Promise<CallInvite>;
1601
- settleCallInvite(id: CallId, state: CallState, declineMessage: string | null): Promise<CallInvite>;
1602
- pendingInvites(userId: UserId): Promise<readonly CallInvite[]>;
1603
- getOrCreateMeeting(input: CreateMeetingInput): Promise<MeetingRecord>;
1604
- meetingBySlug(slug: string): Promise<MeetingRecord>;
1605
- meeting(id: MeetingId): Promise<MeetingRecord>;
1606
- /**
1607
- * Keyset paged, `(scheduled_for, id)`. Every meeting the person can access, in
1608
- * ALL their organizations — `organizationId` is an OPTIONAL page filter
1609
- * (absent/null = all organizations), never the active organization
1610
- * (active-org-is-never-a-list-filter.md). Access is decided by RLS.
1611
- */
1612
- listMeetings(args: {
1613
- organizationId?: OrganizationId | null;
1614
- limit?: number;
1615
- before?: {
1616
- scheduledFor: string;
1617
- id: MeetingId;
1618
- } | null;
1619
- }): Promise<{
1620
- readonly items: readonly MeetingRecord[];
1621
- readonly hasMore: boolean;
1622
- }>;
1623
- admit(args: {
1624
- meetingId: MeetingId;
1625
- identity: string;
1626
- admitted: boolean;
1627
- byUserId: UserId;
1628
- }): Promise<void>;
1629
- setLock(meetingId: MeetingId, locked: boolean, byUserId: UserId): Promise<MeetingRecord>;
1630
- endMeeting(meetingId: MeetingId, byUserId: UserId): Promise<MeetingRecord>;
1631
- projectSegment(row: Row): TranscriptSegment;
1632
- projectNote(row: Row): MeetingNote;
1633
- /** One `meet_recordings` row, however it arrived (row or room packet). */
1634
- projectRecording(row: Row): RecordingRecord;
1635
- /** One durable `meet_participants` row — attendance, and the lobby queue. */
1636
- projectAttendee(row: Row): MeetingAttendee;
1637
- projectAttendee(row: Row): MeetingAttendee;
1638
- /**
1639
- * WHO ATTENDED, oldest arrival first. Everyone the meeting ever admitted —
1640
- * including the note-taker, which is a labelled participant and never a hidden
1641
- * listener (R4) — so the record can say who was in the room AND that it was
1642
- * transcribed.
1643
- */
1644
- attendees(meetingId: MeetingId): Promise<readonly MeetingAttendee[]>;
1645
- /**
1646
- * THE DURABLE TRANSCRIPT (D16). Final rows only — the table holds nothing
1647
- * else — oldest first, capped so a four-hour meeting cannot hand a surface
1648
- * fifty thousand rows in one read.
1649
- */
1650
- transcript(meetingId: MeetingId, limit?: number): Promise<readonly TranscriptSegment[]>;
1651
- /** Live notes, decisions, action items and the wrap-up summary, oldest first. */
1652
- notes(meetingId: MeetingId): Promise<readonly MeetingNote[]>;
1653
- /** R5: consent is RECORDED, not assumed. One row per participant per recording. */
1654
- recordConsent(args: {
1655
- meetingId: MeetingId;
1656
- identity: string;
1657
- acknowledgedAt: string;
1658
- }): Promise<void>;
1659
- recordings(meetingId: MeetingId): Promise<readonly RecordingRecord[]>;
1660
- /** Meet wave 3: every recording of the reader's own meetings, newest first. */
1661
- myRecordings(args?: {
1662
- includeArchived?: boolean;
1663
- limit?: number;
1664
- }): Promise<readonly LibraryRecording[]>;
1665
- /** Meet wave 3: search the reader's own meetings. */
1666
- searchMeetings(query: string, limit?: number): Promise<readonly MeetingSearchHit[]>;
1667
- /** Meet wave 3: name a recording (`null` = back to the meeting's title and date). */
1668
- renameRecording(recordingId: string, title: string | null): Promise<void>;
1669
- /** Meet wave 3: archive (soft delete) or restore a recording. Never a purge. */
1670
- setRecordingArchived(recordingId: string, archived: boolean): Promise<void>;
1671
- scheduleMeeting(input: ScheduleMeetingInput): Promise<MeetingRecord>;
1672
- /** `expectedVersion` from the row you edited; a stale one is a `conflict`. */
1673
- updateMeeting(meetingId: MeetingId, changes: MeetingChanges, options?: {
1674
- expectedVersion?: number | null;
1675
- byUserId?: UserId | null;
1676
- }): Promise<MeetingRecord>;
1677
- cancelMeeting(meetingId: MeetingId, reason: string | null, byUserId?: UserId | null): Promise<MeetingRecord>;
1678
- archiveMeeting(meetingId: MeetingId, byUserId?: UserId | null): Promise<MeetingRecord>;
1679
- restoreMeeting(meetingId: MeetingId, byUserId?: UserId | null): Promise<MeetingRecord>;
1680
- setOccurrence(args: {
1681
- meetingId: MeetingId;
1682
- originalStart: string;
1683
- action: "cancel" | "move" | "restore";
1684
- newStart?: string | null;
1685
- newDurationMinutes?: number | null;
1686
- reason?: string | null;
1687
- byUserId?: UserId | null;
1688
- }): Promise<OccurrenceOverride>;
1689
- meetingOccurrences(meetingId: MeetingId, range: {
1690
- from: string;
1691
- to: string;
1692
- limit?: number;
1693
- }): Promise<readonly MeetingOccurrence[]>;
1694
- /** The Upcoming list: every occurrence the reader may see. `organizationId` FILTERS; RLS decides. */
1695
- occurrencesBetween(range: {
1696
- from: string;
1697
- to: string;
1698
- organizationId?: OrganizationId | null;
1699
- limit?: number;
1700
- }): Promise<readonly UpcomingOccurrence[]>;
1701
- invitees(meetingId: MeetingId): Promise<readonly MeetingInvitee[]>;
1702
- addInvitees(meetingId: MeetingId, people: readonly InviteeInput[], byUserId?: UserId | null): Promise<readonly MeetingInvitee[]>;
1703
- setInviteeRole(inviteeId: string, role: InviteeRole, byUserId?: UserId | null): Promise<MeetingInvitee>;
1704
- removeInvitee(inviteeId: string, byUserId?: UserId | null): Promise<MeetingInvitee>;
1705
- respond(meetingId: MeetingId, answer: RsvpAnswer, note?: string | null): Promise<MeetingInvitee>;
1706
- invitationByToken(secret: string): Promise<RsvpInvitation>;
1707
- respondByToken(secret: string, answer: RsvpAnswer, note?: string | null): Promise<RsvpInvitation>;
1708
- projectInvitee(row: Row): MeetingInvitee;
1709
- }
1710
- interface ScheduleMeetingInput {
1711
- readonly organizationId: OrganizationId;
1712
- readonly hostUserId: UserId;
1713
- readonly title: string;
1714
- readonly scheduledFor: string | null;
1715
- /** IANA zone, e.g. `America/Los_Angeles`. The wall-clock time a series keeps across DST. */
1716
- readonly timeZone: string;
1717
- /** Omit to use the person's `meet.default_duration_minutes`. */
1718
- readonly durationMinutes?: number | null | undefined;
1719
- readonly agenda?: string | null | undefined;
1720
- /** RRULE subset: FREQ=DAILY|WEEKLY|MONTHLY, INTERVAL, BYDAY, BYMONTHDAY, COUNT, UNTIL. */
1721
- readonly recurrenceRule?: string | null | undefined;
1722
- readonly settings?: MeetingSettings | undefined;
1723
- }
1724
- interface RepositoryOptions {
1725
- /**
1726
- * Any Supabase client. Deliberately the SHALLOW public shape — pass the
1727
- * `SupabaseClient<Database>` you already have, with no cast. See the header.
1728
- */
1729
- client: MessagingSupabaseClient;
1730
- /** Resolve a session; used to retry ONCE across the sign-in gap. */
1731
- session?: (() => Promise<unknown>) | undefined;
1732
- }
1733
- /**
1734
- * 🚨 THE PROJECTIONS ARE MODULE-LEVEL AND EXPORTED, DELIBERATELY (MRI-A12).
1735
- *
1736
- * A durable Meet row reaches a client on one of two transports: as a
1737
- * `meet_notes` / `meet_transcript_segments` row over Postgres Changes, which
1738
- * needs a Supabase session and an org grant, or as a `matrx.meet.*` packet on
1739
- * the LiveKit data plane, which needs only the room token — and is therefore the
1740
- * ONLY one a session-less guest can receive (MRI-A10 settled that `anon` never
1741
- * gets a grant on an org-scoped table).
1742
- *
1743
- * Both carry the same row. If each transport projected it with its own function,
1744
- * a guest and a member would render the same meeting differently the first time
1745
- * the two drifted. So there is one function per row shape, it takes no client,
1746
- * and `core/room-events.ts` calls exactly these.
1747
- */
1748
- declare function projectSegment(row: Row): TranscriptSegment;
1749
- declare function projectNote(row: Row): MeetingNote;
1750
- declare function projectInvitee(row: Row): MeetingInvitee;
1751
- declare function projectOccurrence(row: Row): MeetingOccurrence;
1752
- declare function projectUpcoming(row: Row): UpcomingOccurrence;
1753
- declare function projectOverride(row: Row): OccurrenceOverride;
1754
- /** The jsonb the RSVP doors return, typed. A body that is neither shape is refused loudly. */
1755
- declare function projectRsvpInvitation(body: unknown): RsvpInvitation;
1756
- /** Settings → the door's snake_case object, carrying ONLY what was set. */
1757
- declare function settingsPatch(settings: MeetingSettings): Record<string, unknown>;
1758
- /**
1759
- * A partial edit → the `meet_update_meeting` patch. `undefined` = leave alone,
1760
- * `null` = clear — the difference a JSON patch exists to carry.
1761
- */
1762
- declare function changesPatch(changes: MeetingChanges): Record<string, unknown>;
1763
- declare function createMeetRepository(options: RepositoryOptions): MeetRepository;
1764
- /** One row of the recordings library (`meet_my_recordings`). */
1765
- interface LibraryRecording {
1766
- readonly id: string;
1767
- readonly meetingId: MeetingId;
1768
- readonly meetingTitle: string;
1769
- readonly meetingSlug: string;
1770
- /** The reader's strongest role on the meeting. */
1771
- readonly role: "host" | "cohost" | "invitee" | "attendee";
1772
- readonly state: RecordingState;
1773
- /** A name a host gave it; null = show the meeting title. */
1774
- readonly title: string | null;
1775
- readonly fileId: string | null;
1776
- readonly startedAt: string | null;
1777
- readonly endedAt: string | null;
1778
- readonly durationMs: number | null;
1779
- readonly sizeBytes: number | null;
1780
- readonly failureReason: string | null;
1781
- readonly archivedAt: string | null;
1782
- readonly organizationId: OrganizationId;
1783
- }
1784
- declare function projectLibraryRecording(row: Row): LibraryRecording;
1785
- /** Where a search hit was found. */
1786
- type MeetingSearchSource = "title" | "summary" | "decision" | "action_item" | "note" | "transcript";
1787
- /** One hit from `meet_search`. `snippet` marks matched words with ⟦ ⟧. */
1788
- interface MeetingSearchHit {
1789
- readonly meetingId: MeetingId;
1790
- readonly meetingTitle: string;
1791
- readonly meetingSlug: string;
1792
- readonly meetingAt: string | null;
1793
- readonly source: MeetingSearchSource;
1794
- /** The note or transcript segment id (the meeting id for a title hit). */
1795
- readonly refId: string;
1796
- readonly speaker: string | null;
1797
- readonly snippet: string;
1798
- /** When it was said / written. */
1799
- readonly at: string | null;
1800
- readonly rank: number;
1801
- }
1802
- declare function projectSearchHit(row: Row): MeetingSearchHit;
1803
- /** A snippet split into plain and matched runs, for highlighting without HTML. */
1804
- declare function snippetRuns(snippet: string): readonly {
1805
- readonly text: string;
1806
- readonly hit: boolean;
1807
- }[];
1808
-
1809
- /**
1810
- * THE CALL CENTER — ringing, answering, and the six ways a call ends badly.
1811
- *
1812
- * A call reaches you on whatever surface you are on (R2), which is why this is
1813
- * mounted by the provider and not by a call page. It is also why delivery is
1814
- * DUAL by design, not by belt-and-braces nervousness:
1815
- *
1816
- * - **The durable leg** is the invite ROW plus its Postgres Changes event. It
1817
- * survives a reload, a late-joining tab, and a dropped socket, and it is what
1818
- * makes a missed call a fact rather than a lost packet.
1819
- * - **The instant leg** is a broadcast on the callee's user channel, which
1820
- * arrives without a database round trip.
1821
- *
1822
- * Both are deduplicated on the invite id, so a call rings ONCE however it
1823
- * arrives. Neither is a fallback for the other: removing the durable leg loses
1824
- * calls, and removing the instant leg makes ringing feel slow.
1825
- *
1826
- * 🚨 THE FOUR RULES THAT MAKE RINGING SURVIVABLE
1827
- *
1828
- * 1. **A ring always ends.** Every outgoing call arms a timeout that writes
1829
- * `missed`. A ring with no terminal state is a phone that never stops.
1830
- * 2. **Answering on one device stops every other.** The `settled` signal on
1831
- * your OWN user channel silences the other tabs. Without it, answering on a
1832
- * laptop leaves a phone ringing in the room.
1833
- * 3. **Busy is answered, not ignored.** Being in an active call auto-declines
1834
- * with `busy`, so the caller learns something instead of ringing out.
1835
- * 4. **The state machine is a MACHINE.** Transitions are validated: a
1836
- * `declined` call cannot become `active` because a late accept arrived.
1837
- */
1838
-
1839
- declare function canTransition(from: CallState, to: CallState): boolean;
1840
- interface CallCenterOptions {
1841
- manager: RealtimeManager;
1842
- repository: MeetRepository;
1843
- store: MeetStore;
1844
- identity: MeetIdentity;
1845
- /** Ring for this long before the call is `missed`. Org knob, not a constant. */
1846
- ringTimeoutMs?: number | undefined;
1847
- onIncoming?: ((invite: CallInvite) => void) | undefined;
1848
- onSettled?: ((invite: CallInvite) => void) | undefined;
1849
- onDiagnostic?: ((event: {
1850
- level: "info" | "warn" | "error";
1851
- message: string;
1852
- remedy?: string;
1853
- }) => void) | undefined;
1854
- timers?: {
1855
- setTimeout: (run: () => void, ms: number) => unknown;
1856
- clearTimeout: (handle: unknown) => void;
1857
- now: () => number;
1858
- };
1859
- }
1860
- interface CallCenter {
1861
- /** Subscribe to this user's calling channel. Call once, from the provider. */
1862
- start(): Promise<void>;
1863
- /** Ring someone. Returns the invite the UI shows as "calling…". */
1864
- call(args: {
1865
- people: readonly UserId[];
1866
- mode: CallMode;
1867
- conversationId?: string | undefined;
1868
- }): Promise<CallInvite>;
1869
- accept(id: CallId, mode: CallMode): Promise<CallInvite>;
1870
- decline(id: CallId, message?: string): Promise<CallInvite>;
1871
- cancel(id: CallId): Promise<CallInvite>;
1872
- /** Every invite currently ringing this device. */
1873
- incoming(): readonly CallInvite[];
1874
- outgoing(): CallInvite | null;
1875
- /** Feed a ring that arrived through another path (a messaging action chip). */
1876
- ingest(invite: CallInvite): void;
1877
- dispose(): void;
1878
- }
1879
- declare function createCallCenter(options: CallCenterOptions): CallCenter;
1880
-
1881
- /**
1882
- * THE MEET CHANNEL NAMESPACES.
1883
- *
1884
- * Zero hand-rolled `.channel(` in this package (the realtime doctrine). Every
1885
- * topic is declared through `@ai-matrx/realtime`'s registry, which refuses a
1886
- * colliding re-declaration.
1887
- *
1888
- * THREE channels, each because it has a different AUDIENCE:
1889
- *
1890
- * - `meet-user` (per user): where a call REACHES you — the ring, the cancel,
1891
- * the "answered on another device". You are subscribed to this everywhere the
1892
- * provider is mounted, whether or not you are in a meeting.
1893
- * - `meet-room` (per room): the pre-media room — who is knocking, who was
1894
- * admitted, whether recording started. It carries the moments that happen
1895
- * BEFORE and AFTER media, which LiveKit's own data plane cannot: a guest in
1896
- * the lobby is not in the LiveKit room yet, by construction (D6).
1897
- * - `meet-meeting` (per meeting): the durable record's feed — transcript
1898
- * segments, notes, the recording row going `available` — so a surface that is
1899
- * NOT in the call (a meeting page, an admin console) stays live.
1900
- *
1901
- * 🚨 The in-MEDIA control plane (hand raise, reactions, in-room chat) rides
1902
- * LiveKit data messages, NOT these channels — see `DATA_TOPICS`. Splitting them
1903
- * this way is deliberate: anything that must reach a guest with no Supabase
1904
- * session travels on the media plane, where their room token is the authority.
1905
- */
1906
-
1907
- declare function userTopic(userId: UserId): string;
1908
- declare function roomTopic(roomName: RoomName): string;
1909
- declare function meetingTopic(meetingId: MeetingId): string;
1910
- /** Broadcast event names. One place, so a sender and a receiver cannot drift. */
1911
- declare const MEET_EVENTS: {
1912
- /** A call is ringing you. Carries the whole invite; no follow-up read. */
1913
- readonly ring: "mx.call.ring";
1914
- /** The caller hung up before you answered. */
1915
- readonly cancel: "mx.call.cancel";
1916
- /** You answered (or declined) on another device. Stop ringing here. */
1917
- readonly settled: "mx.call.settled";
1918
- /** Someone is in the lobby. Hosts render the knock. */
1919
- readonly knock: "mx.lobby.knock";
1920
- /** A host decided. The knocker learns it here — they have no DB read. */
1921
- readonly admission: "mx.lobby.admission";
1922
- /** The room was locked or unlocked. */
1923
- readonly lock: "mx.room.lock";
1924
- /** Recording started or stopped. EVERY participant renders the indicator. */
1925
- readonly recording: "mx.room.recording";
1926
- };
1927
- /**
1928
- * 🚨 `mx.meeting.transcript` and `mx.meeting.note` WERE declared here and
1929
- * emitted and subscribed by nothing — a broadcast fallback that existed only as
1930
- * a name (MRI-A10 found it; feedback `b25db4d4`). They are gone. The thing they
1931
- * described is real now and lives somewhere better: the platform publishes the
1932
- * durable rows into the LIVEKIT ROOM under `matrx.meet.*` (`core/room-events.ts`,
1933
- * MRI-A12), where a session-less guest's room token is the authority. Sending
1934
- * them over a Supabase broadcast channel would have required the guest to hold a
1935
- * Supabase connection, which is the exact thing they do not have.
1936
- */
1937
- type MeetEvent = (typeof MEET_EVENTS)[keyof typeof MEET_EVENTS];
1938
-
1939
- /**
1940
- * THE PLATFORM'S OWN VOICE IN THE ROOM — and the ONLY live feed a guest has.
1941
- *
1942
- * 🚨 **Why this file exists.** Everything durable a meeting produces — a live
1943
- * note, a decision, an action item, the note-taker's state, the ending — lands
1944
- * in `communication.meet_*`, and a signed-in member learns about it over
1945
- * Postgres Changes. A GUEST cannot: `anon` holds no SELECT grant on
1946
- * `meet_notes`, `meet_transcript_segments` or `meet_participants`, and MRI-A10
1947
- * settled that widening it is forbidden, because those are org-scoped tables and
1948
- * a meeting link is not org membership. A guest's authority is the ROOM TOKEN.
1949
- * So the platform publishes the same rows into the room, where the token is the
1950
- * authorization, and this file turns those packets into exactly the same store
1951
- * calls the database path makes.
1952
- *
1953
- * Three rules hold it together:
1954
- *
1955
- * 1. **Same row, same projection, same reducer.** A `matrx.meet.note` packet
1956
- * carries a `meet_notes` row and is projected by `projectNote` — the very
1957
- * function the Postgres Changes handler uses — then handed to
1958
- * `store.upsertNote`. A guest and a member therefore converge on one code
1959
- * path rather than on two implementations that agree until they don't.
1960
- * 2. **Keyed by the row id, so both copies are one thing.** A member receives
1961
- * every note TWICE. They render once because the id is the row's id on both
1962
- * transports, and where the two could disagree the database wins
1963
- * (`store.upsertNote`'s precedence rule, D16).
1964
- * 3. **`matrx.meet.*` is the server's namespace and nothing else's.** LiveKit
1965
- * does not validate a data packet's sender, so any participant could publish
1966
- * any bytes under any topic. What makes these trustworthy is that publishing
1967
- * them requires a `roomAdmin` grant, which only the server's own control
1968
- * token carries — a participant token cannot claim it. The in-room control
1969
- * plane participants DO publish (hand, reaction, chat) lives under `mx.*`
1970
- * and is handled separately, precisely so the two can never be confused.
1971
- */
1972
-
1973
- /**
1974
- * The server's namespace. Pinned against `aidream/services/meet/livekit.py`'s
1975
- * `ROOM_EVENT_PREFIX` — a rename on either side must break a named test here.
1976
- */
1977
- declare const ROOM_EVENT_PREFIX = "matrx.meet.";
1978
- declare const ROOM_EVENTS: {
1979
- /** One durable `meet_notes` row was written. Payload `{ note: <row> }`. */
1980
- readonly note: "matrx.meet.note";
1981
- /** `meet_meetings.metadata.note_taker` changed. Payload `{ meeting_id, note_taker }`. */
1982
- readonly noteTaker: "matrx.meet.note_taker";
1983
- /** A wrap-up pass finished. Payload `{ meeting_id, note_ids, segment_count }`. */
1984
- readonly wrapUp: "matrx.meet.wrap_up";
1985
- /** The meeting ended for everyone. Payload `{ meeting_id, ended_at, trigger }`. */
1986
- readonly ended: "matrx.meet.ended";
1987
- /**
1988
- * A `meet_recordings` row changed state. Payload
1989
- * `{ meeting_id, recording: { id, state, started_by } }`.
1990
- *
1991
- * 🚨 THIS IS THE ONLY WAY A GUEST LEARNS A RECORDING IS RUNNING. PreJoin
1992
- * promises every joiner an indicator, and `anon` holds no SELECT grant on
1993
- * `meet_recordings` (MRI-A10 settled that widening it is forbidden), so
1994
- * without this packet the promise was a sentence with nothing behind it: on
1995
- * production 2026-09-09 the guest lane showed no indicator at all while the
1996
- * host recorded (MRI-C8).
1997
- */
1998
- readonly recording: "matrx.meet.recording";
1999
- /**
2000
- * A chat message to everyone was SAVED (Meet wave 2). Payload
2001
- * `{ message: { id, meeting_id, sender_identity, sender_name, body, created_at } }`.
2002
- */
2003
- readonly chat: "matrx.meet.chat";
2004
- /**
2005
- * A host or co-host acted (Meet wave 2). Payload `{ meeting_id, kind, … }`:
2006
- * `muted {source, by}` · `ask-unmute {by}` · `removed {by}` — addressed to the
2007
- * person concerned; `lower-hands {identities: [...] | "*"}` · `role {identity,
2008
- * role}` — to everyone.
2009
- */
2010
- readonly moderation: "matrx.meet.moderation";
2011
- /**
2012
- * A meeting ROOM ITEM changed (Meet wave 5): a poll, a Q&A question, a whiteboard
2013
- * stroke or the breakout plan. Payload `{ meeting_id, item }` · `{ removed: [ids] }` ·
2014
- * `{ hidden: [ids] }` · `{ cleared: "whiteboard" }` · `{ broadcast }` · `{ item, move }`.
2015
- * A host's fuller copy of a poll or question carries `audience: "hosts"`.
2016
- */
2017
- readonly collab: "matrx.meet.collab";
2018
- };
2019
- type RoomEventTopic = (typeof ROOM_EVENTS)[keyof typeof ROOM_EVENTS];
2020
- declare function isRoomEventTopic(topic: string): boolean;
2021
- /**
2022
- * What happened to one packet. Returned rather than logged from inside, so the
2023
- * room engine can report a drop with its own diagnostic channel and a test can
2024
- * assert on it — a packet that is silently ignored is indistinguishable from
2025
- * one that never arrived, which is the failure this whole item is about.
2026
- */
2027
- interface RoomEventOutcome {
2028
- readonly handled: boolean;
2029
- /** Present when `handled` is false. Says what was wrong, in a sentence. */
2030
- readonly ignored?: string;
2031
- }
2032
- interface ApplyRoomEventArgs {
2033
- readonly store: MeetStore;
2034
- readonly topic: string;
2035
- /** The packet's already-parsed JSON body. */
2036
- readonly payload: unknown;
2037
- readonly now?: (() => number) | undefined;
2038
- }
2039
- /**
2040
- * Apply one `matrx.meet.*` packet to the store. Never throws.
2041
- *
2042
- * A malformed packet is DROPPED with a reason, never partially applied: half a
2043
- * note on a guest's screen is worse than no note, because the guest cannot tell
2044
- * which half they are missing.
2045
- */
2046
- declare function applyRoomEvent(args: ApplyRoomEventArgs): RoomEventOutcome;
2047
-
2048
- /**
2049
- * DEVICE MANAGEMENT — the part users blame the product for.
2050
- *
2051
- * Every rule here exists because it is a support ticket somewhere:
2052
- *
2053
- * - **Labels are empty until permission is granted.** `enumerateDevices()`
2054
- * happily returns three cameras with blank labels before the prompt. A picker
2055
- * rendering "Camera 1 / Camera 2" is the classic symptom; we report
2056
- * `labelsHidden` so the UI can say *why* instead.
2057
- * - **A remembered device can vanish.** Unplug a headset between meetings and a
2058
- * stored `deviceId` produces `OverconstrainedError`. The manager falls back to
2059
- * the system default AND SAYS SO — a silent fallback is how someone joins on
2060
- * the laptop mic while believing they are on their headset.
2061
- * - **Selections persist per KIND, per device.** Injected storage (C10:
2062
- * persistence-injected — this package's core purpose is not storage), with a
2063
- * memory fallback that announces itself rather than pretending to be durable.
2064
- * - **Enumeration is repeated on `devicechange`.** A picker that lists what was
2065
- * plugged in at mount is wrong within seconds of a headset arriving.
2066
- */
2067
- type DeviceKind = "audioinput" | "videoinput" | "audiooutput";
2068
- interface MeetDevice {
2069
- readonly deviceId: string;
2070
- readonly kind: DeviceKind;
2071
- readonly label: string;
2072
- }
2073
- interface DeviceSnapshot {
2074
- readonly devices: Readonly<Record<DeviceKind, readonly MeetDevice[]>>;
2075
- readonly selected: Readonly<Record<DeviceKind, string | null>>;
2076
- /** True when the browser is hiding labels because permission was not granted. */
2077
- readonly labelsHidden: boolean;
2078
- readonly permission: "unknown" | "granted" | "denied";
2079
- /** Set when a remembered device was gone and we fell back. Shown, not swallowed. */
2080
- readonly fallbackNotice: string | null;
2081
- }
2082
- interface DeviceStorage {
2083
- read(): Record<string, string>;
2084
- write(value: Record<string, string>): void;
2085
- readonly durable: boolean;
2086
- }
2087
- interface MediaDevicesLike {
2088
- enumerateDevices(): Promise<readonly {
2089
- deviceId: string;
2090
- kind: string;
2091
- label: string;
2092
- }[]>;
2093
- getUserMedia(constraints: {
2094
- audio?: unknown;
2095
- video?: unknown;
2096
- }): Promise<{
2097
- getTracks(): readonly {
2098
- stop(): void;
2099
- }[];
2100
- }>;
2101
- addEventListener?: ((type: "devicechange", handler: () => void) => void) | undefined;
2102
- removeEventListener?: ((type: "devicechange", handler: () => void) => void) | undefined;
2103
- }
2104
- interface DeviceManagerOptions {
2105
- media: MediaDevicesLike;
2106
- storage?: DeviceStorage | undefined;
2107
- onChange?: ((snapshot: DeviceSnapshot) => void) | undefined;
2108
- }
2109
- interface DeviceManager {
2110
- /**
2111
- * 🚨 THE SAME REFERENCE until something actually changes. This is a
2112
- * `useSyncExternalStore` `getSnapshot` source (see `useDeviceSnapshot`), and
2113
- * React compares snapshots by IDENTITY: a fresh object literal per call makes
2114
- * every render report a change, which React reports as *"The result of
2115
- * getSnapshot should be cached"* and then kills the tree with *"Maximum
2116
- * update depth exceeded"*. `createMeetStore()` has always held this contract;
2117
- * 0.2.0 shipped without it here and every room crashed at `<PreJoin>`.
2118
- */
2119
- snapshot(): DeviceSnapshot;
2120
- /** Enumerate without prompting. Labels may be hidden; that is reported. */
2121
- refresh(): Promise<DeviceSnapshot>;
2122
- /** Prompt, then enumerate with real labels. The pre-join screen calls this. */
2123
- requestPermission(kinds: {
2124
- audio: boolean;
2125
- video: boolean;
2126
- }): Promise<DeviceSnapshot>;
2127
- select(kind: DeviceKind, deviceId: string): DeviceSnapshot;
2128
- /** The id to actually use — the remembered one, or the default when it is gone. */
2129
- resolve(kind: DeviceKind): string | null;
2130
- subscribe(listener: (snapshot: DeviceSnapshot) => void): () => void;
2131
- dispose(): void;
2132
- }
2133
- /** localStorage when it exists; memory otherwise — and it says which. */
2134
- declare function createWebDeviceStorage(key?: string): DeviceStorage;
2135
- declare function createMemoryDeviceStorage(): DeviceStorage;
2136
- declare function createDeviceManager(options: DeviceManagerOptions): DeviceManager;
2137
-
2138
- /**
2139
- * PRESENTATION HELPERS specific to a CALL. Pure, tested, and shared by every
2140
- * surface — so a roster line reads the same in the meeting, in the history
2141
- * list, and in the notification.
2142
- *
2143
- * The GENERIC formatters this file used to define — `formatDuration`,
2144
- * `getInitials`, `avatarPaletteIndex` — moved to `@ai-matrx/kit/format` on
2145
- * 2026-09-07 (duplication census H1). They were byte-for-byte duplicated in
2146
- * `@ai-matrx/messaging`, and because two packages claimed them, ~65 host
2147
- * twins across six repos had no correct owner to point at: no surface should
2148
- * take a dependency on the video-calling package to print a duration. Nothing
2149
- * is re-exported from here — import `@ai-matrx/kit/format` directly.
2150
- */
2151
-
2152
- /** "Ana, Ben and 3 others" — the roster line, never a raw array join. */
2153
- declare function participantSummary(participants: readonly MeetParticipant[]): string;
2154
- /** What a settled call says in history. One sentence per terminal state. */
2155
- declare function callStateLabel(state: CallState, outgoing: boolean): string;
2156
- /** "2 waiting" for the lobby badge — plural handled once, not per surface. */
2157
- declare function countLabel(count: number, singular: string, plural?: string): string;
2158
-
2159
- /**
2160
- * THE MESSAGING SEAM — a call invitation is an ACTIONABLE MESSAGE (D8).
2161
- *
2162
- * `@ai-matrx/messaging` deliberately knows nothing about meetings; its action
2163
- * registry exists so packages like this one can add a kind without messaging
2164
- * learning about them. This file is that registration, and it is the whole
2165
- * coupling between the two packages.
2166
- *
2167
- * Why an invitation is a message and not only a ring: a ring is EPHEMERAL. If
2168
- * you were away from your desk, a broadcast that expired is gone, and "someone
2169
- * tried to reach me" is exactly the information you most needed. The message is
2170
- * the durable half — it is still in the thread tomorrow, with the outcome
2171
- * stamped on it.
2172
- *
2173
- * 🚨 **THE PAYLOAD IS NOT AUTHORIZATION.** The chip's payload says what was
2174
- * ASKED, never what is ALLOWED. The handler re-resolves the invite through the
2175
- * repository (an auth-checked RPC) before it joins anything. Trusting the
2176
- * payload would turn any message anyone can send into a room grant.
2177
- *
2178
- * Idempotence is the registry's, not ours: two tabs and a double tap settle
2179
- * ONCE and share one receipt.
2180
- */
2181
-
2182
- declare const CALL_INVITE_KIND = "meet.call-invite";
2183
- declare const CALL_INVITE_VERSION = 1;
2184
- interface CallInvitePayload {
2185
- readonly invite_id: string;
2186
- readonly room_name: string;
2187
- readonly mode: CallMode;
2188
- readonly caller_name: string;
2189
- readonly expires_at: string;
2190
- }
2191
- /** The `action_data` a caller attaches to the invitation message. */
2192
- declare function callInviteAction(invite: CallInvite): {
2193
- readonly kind: string;
2194
- readonly version: number;
2195
- readonly payload: CallInvitePayload;
2196
- };
2197
- /** The message body. Readable on a surface that cannot render the chips. */
2198
- declare function callInviteText(invite: CallInvite): string;
2199
- interface CallInviteHandlerOptions {
2200
- calls: CallCenter;
2201
- repository: MeetRepository;
2202
- /** Called after a successful accept, with the room to enter. */
2203
- onJoin: (invite: CallInvite) => void | Promise<void>;
2204
- now?: (() => number) | undefined;
2205
- }
2206
- declare function createCallInviteHandler(options: CallInviteHandlerOptions): ActionHandler<CallInvitePayload>;
2207
- /**
2208
- * A meeting invitation — the scheduled twin. Deliberately NOT the same kind: a
2209
- * scheduled meeting's chip must never say "Join call" three days early, and a
2210
- * receiver that only understands calls must render NOTHING for it rather than a
2211
- * button it cannot honor.
2212
- */
2213
- declare const MEETING_INVITE_KIND = "meet.meeting-invite";
2214
- declare const MEETING_INVITE_VERSION = 1;
2215
- interface MeetingInvitePayload {
2216
- readonly meeting_id: string;
2217
- readonly slug: string;
2218
- readonly title: string;
2219
- readonly scheduled_for: string | null;
2220
- readonly link: string;
2221
- }
2222
- declare function meetingInviteAction(payload: MeetingInvitePayload): {
2223
- readonly kind: string;
2224
- readonly version: number;
2225
- readonly payload: MeetingInvitePayload;
2226
- };
2227
- interface MeetingInviteHandlerOptions {
2228
- onOpen: (payload: MeetingInvitePayload) => void | Promise<void>;
2229
- now?: (() => number) | undefined;
2230
- }
2231
- declare function createMeetingInviteHandler(options: MeetingInviteHandlerOptions): ActionHandler<MeetingInvitePayload>;
2232
-
2233
- /**
2234
- * THE STAGE — who is big, who is small, and who is not rendered at all.
2235
- *
2236
- * This is pure arithmetic on purpose: layout is the part of a meeting UI that
2237
- * everyone re-derives inside a component and gets subtly wrong, and it is
2238
- * trivially testable once it stops living in JSX.
2239
- *
2240
- * R5 SCALE HONESTY lives here. A 200-person meeting does not render 200 video
2241
- * tiles — every browser dies, and LiveKit's adaptive stream cannot save a
2242
- * layout that asked for 200 subscriptions. The grid PAGES, and the ordering is
2243
- * deliberate rather than "whatever the roster map iterated":
2244
- *
2245
- * 1. a screen share (or an explicit pin) owns the stage;
2246
- * 2. then the active speakers, most recent first;
2247
- * 3. then anyone with a hand raised — they are asking to be seen;
2248
- * 4. then cameras on before cameras off (an avatar tile is cheap);
2249
- * 5. then stable by join time, so tiles do not shuffle every time someone
2250
- * breathes. Shuffling tiles is the single most disorienting thing a
2251
- * meeting UI can do.
2252
- */
2253
-
2254
- interface StageArgs {
2255
- readonly participants: readonly MeetParticipant[];
2256
- readonly layout: StageLayout;
2257
- readonly pinned: ParticipantIdentity | null;
2258
- readonly activeSpeakers: readonly ParticipantIdentity[];
2259
- /**
2260
- * The participant a HOST spotlighted for everyone (Meet wave 2). A local pin
2261
- * outranks it on this screen; a screen share outranks both.
2262
- */
2263
- readonly spotlight?: ParticipantIdentity | null | undefined;
2264
- /** How many tiles this viewport can honestly render. Default 12. */
2265
- readonly pageSize?: number;
2266
- readonly page?: number;
2267
- }
2268
- interface Stage {
2269
- /** The large tile, when the layout has one. */
2270
- readonly feature: MeetParticipant | null;
2271
- /** What actually renders as a tile, already paged. */
2272
- readonly tiles: readonly MeetParticipant[];
2273
- /** Everyone not on this page. Rendered as "+N others", never dropped silently. */
2274
- readonly overflow: number;
2275
- readonly page: number;
2276
- readonly pageCount: number;
2277
- /** True when a screen share, not a person, is the reason for the big tile. */
2278
- readonly presenting: boolean;
2279
- }
2280
- declare const DEFAULT_PAGE_SIZE = 12;
2281
- declare function orderParticipants(participants: readonly MeetParticipant[], activeSpeakers: readonly ParticipantIdentity[]): readonly MeetParticipant[];
2282
- declare function computeStage(args: StageArgs): Stage;
2283
- /** Column count for a grid of N tiles — the shape that keeps tiles squarest. */
2284
- declare function gridColumns(count: number): number;
2285
-
2286
- /**
2287
- * THE MEDIA SEAM — the ONE interface the room engine talks to.
2288
- *
2289
- * D1/D2: LiveKit Cloud is the media infrastructure, hardcoded. This interface
2290
- * is NOT a provider abstraction and is not here so a second SFU can be swapped
2291
- * in — it exists for two reasons that are both about correctness:
2292
- *
2293
- * 1. **The engine must be provable without a browser.** Everything expensive in
2294
- * a calling product (ring timeouts, reconnection, admission, escalation,
2295
- * speaker tracking) is logic, and logic proven only through a real SFU is
2296
- * logic that is never proven. `testing/fake-driver.ts` drives every one of
2297
- * those paths in a node test.
2298
- * 2. **The SDK stays out of the initial chunk.** `livekit-client` is reached
2299
- * only through a dynamic `import()` inside the default driver, so importing
2300
- * this package during SSR — or on a page with a call BUTTON but no call —
2301
- * costs nothing and touches no browser API.
2302
- *
2303
- * The default implementation ships (`createLiveKitDriver`) and is what
2304
- * `<MeetProvider>` uses when a host injects nothing (THE ALL-INCLUSIVE LAW).
2305
- */
2306
-
2307
- /** A participant as the media layer sees them, before roles or profiles. */
2308
- interface DriverParticipant {
2309
- readonly identity: string;
2310
- readonly name: string;
2311
- /** Opaque JSON string the token minted for this participant. */
2312
- readonly metadata: string | null;
2313
- /**
2314
- * The participant's LiveKit attributes. Unlike `metadata` these are set by
2315
- * the participant itself at run time, which is how the note-taker announces
2316
- * that a consent notice is owed (D10).
2317
- */
2318
- readonly attributes: Readonly<Record<string, string>>;
2319
- readonly isLocal: boolean;
2320
- readonly micEnabled: boolean;
2321
- readonly cameraEnabled: boolean;
2322
- readonly screenSharing: boolean;
2323
- }
2324
- type DriverConnectionState = "connecting" | "connected" | "reconnecting" | "disconnected";
2325
- type DriverTrackKind = "camera" | "microphone" | "screen";
2326
- /**
2327
- * ONE LIVE CAPTION off the `lk.transcription` text stream.
2328
- *
2329
- * `speakerIdentity` is `lk.transcribed_participant_identity` — WHO IS BEING
2330
- * TRANSCRIBED, not who published the stream (the note-taker publishes every
2331
- * one of them). The SFU-reported publisher identity below is independently
2332
- * checked against the meeting's note-taker; captions remain hints and durable
2333
- * segments remain the record (D16).
2334
- */
2335
- interface DriverTranscription {
2336
- /** `lk.segment_id`. Interim and final share it; that is what makes replacement possible. */
2337
- readonly segmentId: string;
2338
- readonly speakerIdentity: string;
2339
- /**
2340
- * The LiveKit participant that opened this text stream. Unlike
2341
- * `speakerIdentity`, this comes from the SFU and is the authority for who
2342
- * published a caption.
2343
- */
2344
- readonly publisherIdentity: string;
2345
- readonly text: string;
2346
- /** `lk.transcription_final`. */
2347
- readonly final: boolean;
2348
- /** `lk.transcribed_track_id`, when the sender supplied it. */
2349
- readonly trackId: string | null;
2350
- readonly at: string;
2351
- }
2352
- interface RoomDriverHandlers {
2353
- /** The FULL roster, every time. A delta protocol here drifts; a snapshot cannot. */
2354
- participants(participants: readonly DriverParticipant[]): void;
2355
- activeSpeakers(identities: readonly string[]): void;
2356
- quality(identity: string, quality: ConnectionQuality): void;
2357
- connectionState(state: DriverConnectionState): void;
2358
- /** In-room data messages: hand raise, reactions, in-room chat, AI events. */
2359
- data(topic: string, payload: string, fromIdentity: string | null): void;
2360
- /** A live caption. Interim and final both arrive here (D16). */
2361
- transcription(event: DriverTranscription): void;
2362
- /** The room ended for this client. `reason` is shown to the user verbatim. */
2363
- disconnected(reason: string | null): void;
2364
- /**
2365
- * The ROOM's metadata — set only by the server (`UpdateRoomMetadata`), and
2366
- * delivered to every joiner ON JOIN, which is why the spotlight rides it: a
2367
- * data packet reaches only the people already in the room. Called once after
2368
- * connect and on every change. Optional so an older host driver still works.
2369
- */
2370
- roomMetadata?(metadata: string): void;
2371
- }
2372
- /** A camera background effect. `image` replaces the background with `imageUrl`. */
2373
- type VideoEffect = {
2374
- readonly kind: "none";
2375
- } | {
2376
- readonly kind: "blur";
2377
- readonly radius?: number;
2378
- } | {
2379
- readonly kind: "image";
2380
- readonly imageUrl: string;
2381
- };
2382
- /** Microphone processing the browser applies before anything is sent. */
2383
- interface AudioProcessing {
2384
- readonly noiseSuppression: boolean;
2385
- readonly echoCancellation: boolean;
2386
- readonly autoGainControl: boolean;
2387
- }
2388
- interface MeetRoomDriver {
2389
- connect(serverUrl: string, token: string): Promise<void>;
2390
- disconnect(): Promise<void>;
2391
- setMicrophoneEnabled(enabled: boolean): Promise<void>;
2392
- setCameraEnabled(enabled: boolean): Promise<void>;
2393
- setScreenShareEnabled(enabled: boolean): Promise<void>;
2394
- /**
2395
- * Reliable, topic-addressed data. The in-room control plane. `to` addresses
2396
- * named identities only (a private chat message); absent = everyone.
2397
- */
2398
- publishData(topic: string, payload: string, to?: readonly string[]): Promise<void>;
2399
- /**
2400
- * Apply microphone processing to the live track and to every future one.
2401
- * Optional: a driver without it makes the control ABSENT, never dead.
2402
- */
2403
- setAudioProcessing?(processing: AudioProcessing): Promise<void>;
2404
- /**
2405
- * Apply a background effect to the live camera and to every future one.
2406
- * Rejects with a sentence when this browser cannot run it.
2407
- */
2408
- setVideoEffect?(effect: VideoEffect): Promise<void>;
2409
- /** Whether this browser can run background effects at all. */
2410
- supportsVideoEffects?(): boolean;
2411
- /** Whether this browser can share a screen at all (iOS cannot). Absent = assume yes. */
2412
- supportsScreenShare?(): boolean;
2413
- /**
2414
- * Attach a participant's track to a media element. Returns the detach
2415
- * function — returning it (rather than exposing `detach`) is what makes a
2416
- * leaked attachment impossible from the React side: the effect's cleanup IS
2417
- * the return value.
2418
- */
2419
- attach(identity: string, kind: DriverTrackKind, element: HTMLMediaElement): () => void;
2420
- /** The device the driver actually selected, so the UI never lies about it. */
2421
- activeDevices(): Readonly<Record<"audioinput" | "videoinput" | "audiooutput", string | null>>;
2422
- switchDevice(kind: "audioinput" | "videoinput" | "audiooutput", deviceId: string): Promise<void>;
2423
- }
2424
- type RoomDriverFactory = (handlers: RoomDriverHandlers) => MeetRoomDriver | Promise<MeetRoomDriver>;
2425
- /** Topics for the in-room control plane. One place, so senders cannot drift. */
2426
- declare const DATA_TOPICS: {
2427
- readonly hand: "mx.hand";
2428
- readonly reaction: "mx.reaction";
2429
- readonly chat: "mx.chat";
2430
- };
2431
- /**
2432
- * 🚨 `mx.ai.note` and `mx.ai.transcript` WERE declared here and emitted by
2433
- * nothing. They are gone (MRI-A12): the platform's own events are published by
2434
- * the SERVER under `matrx.meet.*` — see `core/room-events.ts` — because a packet
2435
- * a participant could publish is a packet a guest could forge, and these carry
2436
- * durable rows. A participant topic for the same thing would be a second,
2437
- * unauthenticated way to say it.
2438
- */
2439
- type DataTopic = (typeof DATA_TOPICS)[keyof typeof DATA_TOPICS];
2440
- /**
2441
- * THE LIVEKIT TRANSCRIPTION CONTRACT — not ours, so it is written down once
2442
- * rather than spelled inline. The topic and these four attribute names are the
2443
- * SDK's own (`lk.*`); the worker publishes to exactly them (MRI-A2) and a
2444
- * rename on either side must break here, loudly, in one place.
2445
- */
2446
- declare const TRANSCRIPTION_TOPIC = "lk.transcription";
2447
- declare const TRANSCRIPTION_ATTRIBUTES: {
2448
- readonly segmentId: "lk.segment_id";
2449
- readonly trackId: "lk.transcribed_track_id";
2450
- readonly final: "lk.transcription_final";
2451
- /**
2452
- * WHO IS BEING TRANSCRIBED. The LiveKit JS client reports the PUBLISHER of a
2453
- * text stream, and the publisher is always the note-taker — so a caption
2454
- * carries the speaker in this attribute and falls back to the publisher only
2455
- * when it is absent (which attributes the line to the agent, visibly, rather
2456
- * than to the wrong person, silently).
2457
- */
2458
- readonly speakerIdentity: "lk.transcribed_participant_identity";
2459
- };
2460
-
2461
- /**
2462
- * THE ONE PLACE `livekit-client` IS TOUCHED.
2463
- *
2464
- * Everything above this file talks to `MeetRoomDriver`. That containment is
2465
- * what lets the engine's whole behavior — ring timeouts, admission, escalation,
2466
- * reconnection — be proven in a node test, and it is why an SDK upgrade is a
2467
- * single-file review rather than a package-wide audit.
2468
- *
2469
- * 🚨 **THE IMPORT IS DYNAMIC, DELIBERATELY.** `import type` is erased, and the
2470
- * runtime `await import("livekit-client")` happens inside `connect()`. So:
2471
- * importing `@ai-matrx/meet` during SSR, or on a page that renders a call
2472
- * BUTTON but never a call, pulls in none of the media SDK and touches no
2473
- * browser API (independence contract §4). A static import here would put ~400KB
2474
- * of WebRTC into every consumer's first load and break RSC rendering.
2475
- *
2476
- * The mapping helpers below are exported for their own tests: an SDK enum
2477
- * renamed upstream must fail a named test, not silently become "unknown" on
2478
- * every tile.
2479
- */
2480
-
2481
- /**
2482
- * LiveKit's quality enum → ours. Mapped explicitly rather than cast: the SDK
2483
- * has five states and our UI has five sentences, and an unmapped one must
2484
- * degrade to `unknown` (an honest "we don't know") rather than to `excellent`
2485
- * (a lie that hides a failing call).
2486
- */
2487
- declare function mapQuality(quality: ConnectionQuality$1 | string): ConnectionQuality;
2488
- declare function mapConnectionState(state: string): DriverConnectionState;
2489
- /**
2490
- * A LiveKit participant → our snapshot.
2491
- *
2492
- * 🚨 `name` IS REPORTED EXACTLY AS LIVEKIT GAVE IT, empty string and all
2493
- * (MRI-A18). Through 0.4.12 it fell back to the identity here, which meant a
2494
- * driver — the thing furthest from any screen — invented a "name" that
2495
- * `core/names.ts` then had to recognize and refuse. A driver reports; it does
2496
- * not name. The one fallback lives in `projectParticipant`, and it is the name
2497
- * book's honest placeholder rather than a token.
2498
- */
2499
- declare function snapshotParticipant(participant: Pick<Participant, "identity" | "name" | "metadata" | "isMicrophoneEnabled" | "isCameraEnabled" | "isScreenShareEnabled"> & {
2500
- attributes?: Readonly<Record<string, string>> | undefined;
2501
- }, isLocal: boolean): DriverParticipant;
2502
- interface LiveKitDriverOptions {
2503
- /** Adaptive stream + dynacast default ON — R5 scale honesty, not a knob. */
2504
- roomOptions?: RoomOptions | undefined;
2505
- }
2506
- declare function createLiveKitDriver(handlers: RoomDriverHandlers, options?: LiveKitDriverOptions): MeetRoomDriver;
2507
-
2508
- /**
2509
- * MEETINGS — the lobby, the host powers, and the durable link.
2510
- *
2511
- * A meeting is a room plus scheduling, lobby and host metadata (D3). Everything
2512
- * here is about the moments OUTSIDE the media session, which is exactly why it
2513
- * cannot ride LiveKit's data plane: a guest waiting in the lobby is not in the
2514
- * LiveKit room yet, by construction. The lobby therefore lives on the meet-room
2515
- * realtime channel.
2516
- *
2517
- * 🚨 THE THREE THINGS THAT MAKE GUEST JOIN SAFE (D6)
2518
- *
2519
- * 1. **The link is an identity, not a session.** A durable slug resolves before,
2520
- * during, and after the scheduled time, and a recurring meeting keeps ONE
2521
- * link. Nothing here mints a per-occurrence URL.
2522
- * 2. **A guest's capability is the TOKEN, and nothing else.** They are scoped to
2523
- * exactly one room. There is no guest row this client writes that could grant
2524
- * anything, and no client-side "is this person allowed" branch.
2525
- * 3. **Admission is a SERVER decision, recorded.** `admit` is an RPC; the
2526
- * broadcast that tells the knocker is a NOTIFICATION of that decision, never
2527
- * the decision itself. Two hosts clicking admit is the ordinary case, and the
2528
- * RPC is what makes it idempotent.
2529
- *
2530
- * Host powers are checked against the role the SERVER minted into the token
2531
- * (see `room.ts`), and the UI hides what an actor cannot do rather than
2532
- * rendering a dead button (no dead ends).
2533
- */
2534
-
2535
- interface MeetingSessionOptions {
2536
- manager: RealtimeManager;
2537
- repository: MeetRepository;
2538
- store: MeetStore;
2539
- identity: MeetIdentity;
2540
- /**
2541
- * The ONE org-aware aidream client (MRI-A16) — `endForEveryone` is a SERVER
2542
- * decision (MRI-A6), and every request it makes carries the organization.
2543
- */
2544
- api: MeetApi;
2545
- onDiagnostic?: ((event: {
2546
- level: "info" | "warn" | "error";
2547
- message: string;
2548
- remedy?: string;
2549
- }) => void) | undefined;
2550
- /**
2551
- * 🚨 THE HOST ADMITTED *US*. Called exactly once per admission verdict that
2552
- * names this participant, and only for `admitted`.
2553
- *
2554
- * Admission is not a UI state change: a lobby token is minted with
2555
- * `canPublish: false` and `canSubscribe: false`, so the waiting room can only
2556
- * become a room by minting a NEW token. Before this existed the waiting room
2557
- * said "you will join automatically once you are admitted" and then waited
2558
- * forever — a sentence on screen that was not true. The provider wires this
2559
- * to `engine.rejoin()`.
2560
- */
2561
- onAdmitted?: (() => void) | undefined;
2562
- /**
2563
- * 🚨 THE HOST DENIED *US*. Called once per denial verdict that names this
2564
- * knocker. Without it the waiting room kept promising "you will join
2565
- * automatically" to a person the host had already turned away (production
2566
- * 2026-09-27). The provider wires it to `engine.rejoin()`: the server then
2567
- * refuses the mint with its own sentence ("The host did not admit you…"),
2568
- * so the words on screen are the server's decision, never the client's guess.
2569
- */
2570
- onDenied?: (() => void) | undefined;
2571
- }
2572
- interface MeetingSession {
2573
- /** Subscribe to the room's out-of-band plane. Hosts see knocks; guests see verdicts. */
2574
- watch(roomName: RoomName): void;
2575
- /**
2576
- * Subscribe to the MEETING RECORD's durable feed — transcript segments, notes,
2577
- * and the meeting row itself (which is where the note-taker's state lives).
2578
- * Reconnecting IS re-reading: the backfill door re-reads all three, because
2579
- * realtime has no replay and a socket blip would otherwise leave a transcript
2580
- * permanently missing the minute nobody noticed.
2581
- */
2582
- watchMeeting(meetingId: MeetingId): void;
2583
- /** A guest (or a late arrival at a locked room) announces themselves. */
2584
- knock(roomName: RoomName): void;
2585
- admit(entry: LobbyEntry, admitted: boolean): Promise<void>;
2586
- admitAll(): Promise<void>;
2587
- setLock(locked: boolean): Promise<MeetingRecord>;
2588
- /** End it for everyone, through the server (MRI-A6). */
2589
- endForEveryone(): Promise<EndMeetingResult>;
2590
- dispose(): void;
2591
- }
2592
- /** Can `actor` moderate `target`? A host outranks a co-host outranks everyone. */
2593
- declare function canModerateParticipant(actor: MeetParticipant | undefined, target: MeetParticipant | undefined): boolean;
2594
- declare function roleOf(participants: readonly MeetParticipant[], identity: ParticipantIdentity | null): ParticipantRole;
2595
- /**
2596
- * What ending a meeting produced. `served: false` is the honest answer from a
2597
- * server that has not landed `POST /v1/meet/end` yet (MRI-A6) — the control is
2598
- * real, the refusal is a sentence, and nothing pretends the meeting ended.
2599
- */
2600
- interface EndMeetingResult {
2601
- readonly endedAt: string | null;
2602
- readonly served: boolean;
2603
- }
2604
- /** The sentence a server without MRI-A6 earns. Pinned so every surface says it. */
2605
- declare const END_MEETING_NOT_SERVED = "Ending a meeting is not available on this server yet";
2606
- declare function createMeetingSession(options: MeetingSessionOptions): MeetingSession;
2607
- declare function meetingSlug(random?: () => number): string;
2608
- /** The link a host copies. ONE shape, so every surface produces the same URL. */
2609
- declare function meetingLink(baseUrl: string, slug: string): string;
2610
-
2611
- /**
2612
- * RECORDING — LiveKit Egress, host-controlled, consent-enforced (R3/R5).
2613
- *
2614
- * 🚨 **CONSENT IS NOT OPTIONAL AND NOT A PROP.** There is no
2615
- * `showRecordingIndicator={false}`. Recording a person without telling them is
2616
- * illegal in a two-party-consent jurisdiction and is indefensible everywhere
2617
- * else, so the indicator and the join-time notice are properties of the
2618
- * package, not decisions a consumer gets to make. The only knob is the org's
2619
- * `recordingPolicy` — disabled / host-controlled / always-on — which decides
2620
- * whether recording may happen at all, never whether people are told.
2621
- *
2622
- * 🚨 **THE RECORDING BELONGS TO THE PLATFORM, NOT TO LIVEKIT.** The server
2623
- * lands the artifact in the platform files system and returns a FILE ID. A
2624
- * provider URL would be an orphan the day the room is garbage collected — the
2625
- * reason `RecordingRecord.fileId` is not `url`.
2626
- *
2627
- * The client half is small on purpose: start/stop are aidream calls (the API
2628
- * secret never comes near a browser), and the truth about a recording arrives
2629
- * from the server's webhook through the room channel. This module never
2630
- * *guesses* that a recording started.
2631
- */
2632
-
2633
- interface RecordingControllerOptions {
2634
- /** The ONE org-aware aidream client (MRI-A16). Owns base url and headers. */
2635
- api: MeetApi;
2636
- store: MeetStore;
2637
- /** Announce the new state to everyone in the room (the indicator is for all). */
2638
- announce?: ((state: RecordingState, by: UserId | null) => void) | undefined;
2639
- /** Persist that this participant saw the notice. Consent is RECORDED (R5). */
2640
- acknowledgeConsent?: ((meetingId: MeetingId) => Promise<void>) | undefined;
2641
- }
2642
- interface RecordingController {
2643
- /** True when the org policy and the actor's role allow starting a recording. */
2644
- canStart(meeting: MeetingRecord | null, isHost: boolean): boolean;
2645
- start(args: {
2646
- meetingId: MeetingId;
2647
- roomName: RoomName;
2648
- by: UserId;
2649
- }): Promise<void>;
2650
- stop(args: {
2651
- meetingId: MeetingId;
2652
- by: UserId;
2653
- }): Promise<void>;
2654
- /** The join-time notice text. One sentence, one place, every surface. */
2655
- consentNotice(meeting: MeetingRecord | null): string | null;
2656
- acknowledge(meetingId: MeetingId): Promise<void>;
2657
- }
2658
- declare function createRecordingController(options: RecordingControllerOptions): RecordingController;
2659
- /** The indicator's words. One definition, so no surface invents a softer one. */
2660
- declare function recordingLabel(state: RecordingState): string | null;
2661
-
2662
- /**
2663
- * THE TOKEN FLOW — the ONLY authorization path into a room (R1).
2664
- *
2665
- * 🚨 **THE CLIENT NEVER HOLDS A LIVEKIT SECRET.** Not in an env var, not in a
2666
- * build-time constant, not "just for local dev". A LiveKit API secret in a
2667
- * browser bundle mints a token for ANY room in the project, including one the
2668
- * holder was never invited to — it is a full compromise of every meeting in the
2669
- * organization. This package therefore has no code path that could accept one:
2670
- * it asks aidream, and aidream mints.
2671
- *
2672
- * Four hard parts live here so no host reimplements them (C22):
2673
- *
2674
- * 1. **Single flight.** A React strict-mode double effect, a re-render, and a
2675
- * reconnect all ask at once; they share ONE request. Two tokens for one
2676
- * identity is not harmless — LiveKit disconnects the older session, which
2677
- * looks exactly like "my call randomly drops when I open the tab".
2678
- * 2. **Expiry is honored BEFORE it bites.** A token is refreshed at 80% of its
2679
- * life, not when a reconnect fails at 100%. A meeting that outlives its
2680
- * token and cannot reconnect is the classic 2-hour-meeting bug.
2681
- * 3. **Guests are first-class, not an exception.** A guest has no session and
2682
- * the request carries a display name instead; the SERVER decides what a
2683
- * guest may do. There is no client-side "is this person allowed" branch.
2684
- * 4. **A missing session is a lifecycle moment.** Between sign-in and token
2685
- * arrival, the fetch retries ONCE after the host's session resolves and then
2686
- * reports `session-unavailable` as a warning — never a red error, never a
2687
- * retry storm.
2688
- */
2689
-
2690
- interface RoomToken {
2691
- readonly token: string;
2692
- readonly serverUrl: string;
2693
- readonly roomName: RoomName;
2694
- readonly identity: ParticipantIdentity;
2695
- readonly displayName: string;
2696
- /** ISO. The package refreshes at 80% of the remaining life. */
2697
- readonly expiresAt: string;
2698
- /** What the SERVER granted. The client renders from this; it never decides. */
2699
- readonly canPublish: boolean;
2700
- readonly canSubscribe: boolean;
2701
- readonly canRecord: boolean;
2702
- readonly isHost: boolean;
2703
- /**
2704
- * 🚨 THE SERVER'S WORD ON WHO THIS JOINER IS (MRI-A11) — `host`,
2705
- * `participant` or `guest`, minted from `host_user_id` or a real iam grant.
2706
- * The same string is minted into the LiveKit token's participant metadata and
2707
- * its `matrx.participant.role` attribute, so the roster and this response
2708
- * cannot disagree. A server that predates MRI-A11 sends no role, and the
2709
- * fallback below reads the older `is_host` boolean rather than pretending.
2710
- */
2711
- readonly role: ParticipantRole;
2712
- /** True when the server put this joiner in the lobby (D6). */
2713
- readonly lobby: boolean;
2714
- /**
2715
- * Meet wave 5: the breakout room this pass is for, or null for the main room. The
2716
- * pass is still keyed by the MEETING's room name — it is this person's pass for the
2717
- * meeting, and the server accepts a breakout pass wherever the meeting's is asked.
2718
- */
2719
- readonly breakoutRoomId?: string | null | undefined;
2720
- }
2721
-
2722
- interface TokenClientOptions {
2723
- /** The ONE org-aware aidream client (MRI-A16). Owns base url and headers. */
2724
- api: MeetApi;
2725
- /** Injected clock, so expiry logic is provable without waiting two hours. */
2726
- now?: (() => number) | undefined;
2727
- /**
2728
- * Where a GUEST's pass survives a page close (MRF-1). Absent means passes
2729
- * live only in memory, which is correct for a member (they have a session to
2730
- * mint a new one from) and is why this is optional rather than assumed.
2731
- */
2732
- storage?: RoomTokenStorage | undefined;
2733
- }
2734
- /**
2735
- * 🚨 **WHY A GUEST'S PASS MUST OUTLIVE THE TAB (MRF-1).**
2736
- *
2737
- * A guest has no session, so their only credential for a meeting is the room
2738
- * token this server minted for them. After the meeting ends, `/token` refuses
2739
- * to mint another one — correctly: the room is gone and nobody may rejoin it.
2740
- * So a guest who closes the tab and comes back to read the summary has, in a
2741
- * purely in-memory client, *no way to prove they were ever there* — and the one
2742
- * honest thing left to show them is a refusal.
2743
- *
2744
- * Persisting the pass is what makes the durable link durable for them. It is
2745
- * per-device by nature (it is stored beside the device id that minted it) and
2746
- * it expires on its own: `meet.room_token_ttl_seconds` is the organization's
2747
- * setting for how long, and an expired pass produces the renew-the-link
2748
- * sentence, never "you were never here".
2749
- */
2750
- interface RoomTokenStorage {
2751
- read(roomName: RoomName): string | null;
2752
- write(roomName: RoomName, serialized: string): void;
2753
- remove(roomName: RoomName): void;
2754
- }
2755
- /** `localStorage` when it exists; a no-op that SAYS so otherwise. */
2756
- declare function createWebRoomTokenStorage(onDiagnostic?: (message: string, remedy: string) => void): RoomTokenStorage;
2757
- interface TokenRequest {
2758
- readonly roomName: RoomName;
2759
- readonly identity: MeetIdentity;
2760
- /** Present for a meeting join; absent for an ad-hoc call room. */
2761
- readonly meetingId?: string | undefined;
2762
- /** The public meeting slug, when joining by link without knowing the id. */
2763
- readonly slug?: string | undefined;
2764
- /** Meet wave 5: a breakout room of this meeting; absent = the main room. */
2765
- readonly breakoutRoomId?: string | null | undefined;
2766
- }
2767
- interface TokenClient {
2768
- /** A currently-valid token, minting or refreshing only when it must. */
2769
- get(request: TokenRequest): Promise<RoomToken>;
2770
- /** Drop the cached token for a room (after a `denied`, or on leave). */
2771
- invalidate(roomName: RoomName): void;
2772
- /** True when the cached token is inside its refresh window. */
2773
- needsRefresh(roomName: RoomName): boolean;
2774
- /**
2775
- * The pass this DEVICE holds for a room, **including an expired one** (MRF-1).
2776
- *
2777
- * 🚨 The expired case is the whole reason this returns the token instead of
2778
- * `null`: "your pass expired, open the link again" and "you were never in
2779
- * this meeting" are different sentences, and only the caller that can see the
2780
- * expiry can say the right one. Never used to join — `get` mints for that.
2781
- */
2782
- remembered(roomName: RoomName): RoomToken | null;
2783
- }
2784
- /** The aidream routes this package calls. ONE place, so a rename is one edit. */
2785
- /**
2786
- * EVERY aidream ROUTE THIS PACKAGE CALLS, in one object.
2787
- *
2788
- * This is the contract in both directions: aidream carries a test
2789
- * (`services/meet/tests/test_meet_service.py`) that reads THIS object out of
2790
- * this source file and fails if the server stops serving one of these paths.
2791
- * The package is the spec.
2792
- *
2793
- * `meetingEnd` is pinned here BEFORE the server serves it (MRI-A6 owns landing
2794
- * it). That is deliberate and it is not a lie on screen: the control that calls
2795
- * it renders, and a 404 from a server that has not landed A6 is reported as the
2796
- * honest sentence "Ending a meeting is not available on this server yet" —
2797
- * never a dead button, never a spinner, and never a silent no-op.
2798
- */
2799
- declare const MEET_ROUTES: {
2800
- readonly token: "/api/v1/meet/token";
2801
- readonly recordingStart: "/api/v1/meet/recording/start";
2802
- readonly recordingStop: "/api/v1/meet/recording/stop";
2803
- readonly noteTakerStart: "/api/v1/meet/agent/start";
2804
- readonly noteTakerStop: "/api/v1/meet/agent/stop";
2805
- /** In-meeting Q&A. Streams NDJSON; body is `{meeting_id, question}` only. */
2806
- readonly intelligenceAsk: "/api/v1/meet/intelligence/ask";
2807
- /** A host ends the meeting for everyone. Served by MRI-A6. */
2808
- readonly meetingEnd: "/api/v1/meet/end";
2809
- /**
2810
- * THE POST-MEETING RECORD FOR A READER WITH NO DATABASE (MRF-1). A `GET`,
2811
- * authorized by the room token this device already holds, carried in the
2812
- * `x-meet-room-token` header — never a query parameter, because a bearer
2813
- * credential in a URL lands in access logs, referrers and history.
2814
- */
2815
- readonly record: "/api/v1/meet/record";
2816
- /** A host chooses what guests may read of ONE meeting (MRF-1). */
2817
- readonly recordAccess: "/api/v1/meet/record/access";
2818
- /**
2819
- * KEEP YOUR NOTES (2026-10-01): a guest who just created an account presents
2820
- * their kept pass in `x-meet-room-token` with their new session, and the
2821
- * meeting they attended becomes theirs to read. Idempotent; never a grant.
2822
- */
2823
- readonly claim: "/api/v1/meet/claim";
2824
- /**
2825
- * Meet wave 1 (0.8): bring every invitee up to date after a write — sends the
2826
- * invitation / update / cancellation emails with their `.ics` and re-queues
2827
- * reminders. Idempotent. `{meeting_id}` is substituted by `meetingPath`.
2828
- */
2829
- readonly meetingAnnounce: "/api/v1/meet/meetings/{meeting_id}/announce";
2830
- /** Put a one-off meeting on the caller's own Google Calendar (gated by the server). */
2831
- readonly meetingGoogleCalendar: "/api/v1/meet/meetings/{meeting_id}/google-calendar";
2832
- /**
2833
- * Meet wave 3: the recap email after the meeting. `GET` = the draft (subject,
2834
- * body from the wrap-up, who it can go to); `POST` = send what the host
2835
- * reviewed (`dry_run` renders and queues nothing). Never sent automatically.
2836
- */
2837
- readonly meetingFollowUp: "/api/v1/meet/meetings/{meeting_id}/follow-up";
2838
- /**
2839
- * Meet wave 2: EVERY in-meeting host power (mute, stop video, mute all, ask to
2840
- * unmute, spotlight, lower hands, co-host, hand over, rename, remove, report).
2841
- * Decided by the server from the meeting itself; a guest's pass rides
2842
- * `x-meet-room-token`.
2843
- */
2844
- readonly moderate: "/api/v1/meet/moderate";
2845
- /** Meet wave 2: the SAVED chat — `POST` sends to everyone, `GET ?meeting_id=` reads history. */
2846
- readonly chat: "/api/v1/meet/chat";
2847
- /**
2848
- * Meet wave 5: polls, Q&A, the shared whiteboard and breakout rooms. `POST` acts
2849
- * (`{meeting_id, action, …}`), `GET ?meeting_id=` reads what this viewer may see.
2850
- */
2851
- readonly collab: "/api/v1/meet/collab";
2852
- /** Meet wave 5: translate finished caption lines into this viewer's language. */
2853
- readonly captionsTranslate: "/api/v1/meet/captions/translate";
2854
- };
2855
- /** A `MEET_ROUTES` template with its `{meeting_id}` filled in, URL-encoded. */
2856
- declare function meetingPath(template: string, meetingId: string): string;
2857
- /** The header the guest's pass rides on. Pinned on both sides. */
2858
- declare const ROOM_TOKEN_HEADER = "x-meet-room-token";
2859
- declare function parseRoomToken(body: unknown, roomName: RoomName): RoomToken;
2860
- declare function createTokenClient(options: TokenClientOptions): TokenClient;
2861
-
2862
- /**
2863
- * THE MEETING RECORD — what a meeting IS once the room is gone (MRI-D2).
2864
- *
2865
- * 🚨 THE DEFECT THIS EXISTS TO KILL. Through 2026-09-08 a meeting produced a
2866
- * wrap-up summary, decisions, owner-assigned action items, five windows of live
2867
- * notes, thirty-two transcript segments and a recording — and then had NO
2868
- * SURFACE AT ALL. `/meet/<slug>` fell back to `<PreJoin>` on an ended meeting,
2869
- * so the durable link that R3 promises would outlive the meeting resolved to a
2870
- * screen offering to join a room that no longer exists. Everything the campaign
2871
- * built after the room closed was reachable only from SQL.
2872
- *
2873
- * Three rules hold this file together:
2874
- *
2875
- * 1. **Every read is INDEPENDENT and every refusal is NAMED.** The transcript,
2876
- * the notes, the attendance list and the recording live in four RLS-guarded
2877
- * tables, and a guest holding the link can read NONE of them (only
2878
- * `meet_meeting_by_slug` is granted to `anon`). One `Promise.all` would turn
2879
- * a permission boundary into a blank page. Known guest restrictions never issue private reads; each authenticated read that is refused
2880
- * becomes a `MeetingRecordRestriction` with a sentence — the reader is told
2881
- * what exists and why they cannot see it, which is the difference between a
2882
- * record they can ask for and a product that looks broken.
2883
- * 2. **A partial record is still a record.** A failed transcript read never
2884
- * hides the summary somebody CAN read.
2885
- * 3. **Nothing here computes meaning.** Grouping, ordering and formatting only.
2886
- * The summary, decisions and action items are durable rows written by the
2887
- * wrap-up mandate (D5/D18); this module never re-derives them from the
2888
- * transcript, because two sources for one fact is how they start to disagree.
2889
- */
2890
-
2891
- /** One thing the reader could not see, and the honest reason. Never a blank. */
2892
- interface MeetingRecordRestriction {
2893
- /** Which part of the record: `transcript`, `notes`, `attendance`, `recording`. */
2894
- readonly part: "transcript" | "notes" | "attendance" | "recording";
2895
- readonly message: string;
2896
- readonly remedy: string;
2897
- }
2898
- /** One visible access state, made from restrictions that share one remedy. */
2899
- interface MeetingRecordRestrictionNotice {
2900
- readonly parts: readonly MeetingRecordRestriction["part"][];
2901
- readonly message: string;
2902
- readonly remedy: string;
2903
- }
2904
- /** A run of consecutive lines by one speaker — how a person reads a transcript. */
2905
- interface TranscriptBlock {
2906
- readonly id: string;
2907
- /** Who spoke. Blocks break on THIS, never on the rendered name (MRI-A16). */
2908
- readonly identity: ParticipantIdentity;
2909
- readonly speaker: string;
2910
- /** ISO of the first line in the block. */
2911
- readonly startedAt: string;
2912
- readonly lines: readonly TranscriptSegment[];
2913
- }
2914
- interface MeetingRecordBundle {
2915
- readonly meeting: MeetingRecord;
2916
- readonly attendees: readonly MeetingAttendee[];
2917
- /** The wrap-up summary: `kind='note'` with `metadata.role='summary'` (D5). */
2918
- readonly summary: MeetingNote | null;
2919
- /** D20: the AUTHORITATIVE set — window rows the wrap-up superseded are out. */
2920
- readonly decisions: readonly MeetingNote[];
2921
- readonly actionItems: readonly MeetingNote[];
2922
- /** The live-notes windows, in window order (D3). */
2923
- readonly liveNotes: readonly MeetingNote[];
2924
- readonly transcript: readonly TranscriptSegment[];
2925
- /** The most recent recording attempt, whatever state it reached. */
2926
- readonly recording: RecordingRecord | null;
2927
- /** What this reader could NOT see. Rendered; never swallowed. */
2928
- readonly restrictions: readonly MeetingRecordRestriction[];
2929
- /**
2930
- * Identity → name, learned from everything this read COULD see: the durable
2931
- * attendance rows and the transcript's own `speaker` column. It is what stops
2932
- * a raw LiveKit token reaching the screen when one of those reads was refused
2933
- * (MRI-A16).
2934
- */
2935
- readonly names: NameBook;
2936
- }
2937
- /**
2938
- * The current wrap-up summary. Repairs preserve the prior durable row for
2939
- * history, then mark it `superseded_by`; every current-facing projection must
2940
- * therefore make the same distinction as decisions and action items.
2941
- */
2942
- declare function activeWrapUpSummary(notes: readonly MeetingNote[]): MeetingNote | null;
2943
- /**
2944
- * The four durable reads behind a record, as the database returned them, plus
2945
- * what the reader could not see. `assembleMeetingRecord` turns them into the
2946
- * bundle; a cache that keeps them can apply a realtime row to one part and
2947
- * assemble again, without reading the other three (0.7.79).
2948
- */
2949
- interface MeetingRecordParts {
2950
- readonly attendees: readonly MeetingAttendee[];
2951
- /** Every note row (all kinds, superseded included), oldest first. */
2952
- readonly notes: readonly MeetingNote[];
2953
- readonly transcript: readonly TranscriptSegment[];
2954
- /** Newest first; archived recordings are not here. */
2955
- readonly recordings: readonly RecordingRecord[];
2956
- readonly restrictions: readonly MeetingRecordRestriction[];
2957
- }
2958
- /**
2959
- * Load the record parts the reader may request; state all restrictions.
2960
- *
2961
- * `meeting` is passed in when the caller already resolved it (the slug route
2962
- * does, through the `anon`-granted RPC) — re-reading it through the table would
2963
- * fail for exactly the reader who most needs the page to work.
2964
- */
2965
- declare function loadMeetingRecord(repository: MeetRepository, meetingId: MeetingId, meeting: MeetingRecord, reader?: Pick<MeetIdentity, "userId">): Promise<MeetingRecordBundle>;
2966
- /** The four reads, each with its own verdict (see `loadMeetingRecord`). */
2967
- declare function loadMeetingRecordParts(repository: MeetRepository, meetingId: MeetingId, reader?: Pick<MeetIdentity, "userId">): Promise<MeetingRecordParts>;
2968
- /** The bundle a reader sees, from the parts. Grouping and ordering only (rule 3). */
2969
- declare function assembleMeetingRecord(meeting: MeetingRecord, parts: MeetingRecordParts): MeetingRecordBundle;
2970
- /**
2971
- * 🚨 **A READ THAT WAS REFUSED IS NOT AN EMPTY RESULT (MRI-A16).**
2972
- *
2973
- * On production 2026-09-09 a guest's record view said *"No wrap-up was written
2974
- * for this meeting"* directly underneath a panel that had just told them, in
2975
- * the same screen, that the notes are not readable from their account — and
2976
- * *"Nothing was recorded as a decision"* beside a decision that exists
2977
- * (MRI-C8). The page asserted an absence it had no way to know, which is worse
2978
- * than saying nothing: a person who reads "no wrap-up was written" stops
2979
- * looking for one.
2980
- *
2981
- * So no surface writes its own empty sentence any more. It asks here, and gets
2982
- * either its absence sentence — earned, because the read SUCCEEDED and returned
2983
- * nothing — or the refusal, said plainly.
2984
- */
2985
- declare const REFUSED_ABSENCE = "This record is not shared with guests.";
2986
- /** Was this part of the record refused to this reader? */
2987
- declare function wasRefused(bundle: MeetingRecordBundle, part: MeetingRecordRestriction["part"]): boolean;
2988
- /**
2989
- * The sentence to show where a part of the record has nothing in it. NEVER an
2990
- * absence claim over a read that was refused.
2991
- */
2992
- declare function absenceSentence(bundle: MeetingRecordBundle, part: MeetingRecordRestriction["part"], absence: string): string;
2993
- /**
2994
- * 🚨 **"NO WRAP-UP WAS WRITTEN" IS A CLAIM, AND FOR ~33 SECONDS IT WAS FALSE.**
2995
- *
2996
- * On production 2026-09-09 the HOST's record view said *"No wrap-up was written
2997
- * for this meeting"* between 04:46:42 — the flip from the room to the record —
2998
- * and 04:47:15, when the wrap-up landed. The read was permitted and genuinely
2999
- * empty, so this is not the refusal class above; it is the OTHER way a screen
3000
- * lies, by reading "not here yet" as "never happened". A person who reads *"no
3001
- * wrap-up was written"* closes the tab.
3002
- *
3003
- * The meeting row already knows the difference, and it is not a guess: the
3004
- * wrap-up is a durable job, `enqueue_wrap_up` stamps
3005
- * `metadata.note_taker.intelligence.wrap_up` when it queues it, and the graph
3006
- * action stamps `state: "failed"` when the pass refuses TERMINALLY. So:
3007
- *
3008
- * - a summary exists → `written`;
3009
- * - the notes read was refused → `refused` (never an absence claim);
3010
- * - the job is marked failed → `failed`, in the server's own words;
3011
- * - the job is queued or running → `in-progress`, with a refresh affordance;
3012
- * - nothing ever queued one → `absent`, which is the only EARNED absence.
3013
- *
3014
- * There is no timeout that flips `in-progress` into `absent`. A wrap-up that is
3015
- * genuinely stuck says it is still being written — which is TRUE — rather than
3016
- * inventing a verdict this browser has no way to reach.
3017
- */
3018
- type WrapUpStatus = {
3019
- readonly state: "written";
3020
- } | {
3021
- readonly state: "in-progress";
3022
- readonly message: string;
3023
- readonly canRefresh: true;
3024
- } | {
3025
- readonly state: "failed";
3026
- readonly message: string;
3027
- } | {
3028
- readonly state: "refused";
3029
- readonly message: string;
3030
- } | {
3031
- readonly state: "absent";
3032
- readonly message: string;
3033
- };
3034
- /** Where the wrap-up actually is. Never "absent" over a job that is running. */
3035
- declare function wrapUpStatus(bundle: MeetingRecordBundle): WrapUpStatus;
3036
- /**
3037
- * GROUP THE TRANSCRIPT THE WAY A PERSON READS IT: consecutive lines by one
3038
- * speaker become one block with one timestamp. Reading thirty-two rows each
3039
- * re-stamped with a name and a clock is how a transcript becomes unreadable.
3040
- */
3041
- declare function groupTranscript(segments: readonly TranscriptSegment[], names?: NameBook): readonly TranscriptBlock[];
3042
- /**
3043
- * WHAT THE RECORDING IS DOING, in a sentence — including the two states people
3044
- * hit most and understand least. `processing` is not "broken" and `failed` is
3045
- * never silence: both say what happened and what to do.
3046
- */
3047
- declare function recordingNotice(recording: RecordingRecord | null): string | null;
3048
- /** Only an `available` recording has a file to play. Nothing else pretends to. */
3049
- declare function playableRecordingFileId(recording: RecordingRecord | null): string | null;
3050
- /**
3051
- * A MEETING IS OVER WHEN THE SERVER SAYS SO. `ended_at` is stamped by
3052
- * `POST /v1/meet/end` or by the auto-end evaluation (MRI-A6) — never by a
3053
- * client noticing the room is empty.
3054
- */
3055
- declare function hasEnded(meeting: MeetingRecord | null): boolean;
3056
- /**
3057
- * "Thursday, 8 September 2026, 4:21 PM · 19 minutes". One shape everywhere.
3058
- *
3059
- * THE SPAN IS `formatDurationMs`'s (2026-09-12). This body used to divide by
3060
- * `60_000` and pluralise "minute" itself — a duration formatter living in a
3061
- * PACKAGE, found the day `aidream/apps/shared` became a scan root. `@ai-matrx/
3062
- * meet` already depends on `@ai-matrx/kit`, and `style: "long"` is precisely
3063
- * this voice ("19 minutes", "1 minute"). Three things improve by adopting it:
3064
- * a 40-second meeting read "0 minutes" and now reads "40 seconds" (the
3065
- * confident zero the package refuses), a 19-minute-35-second meeting read
3066
- * "20 minutes" and now reads "19 minutes" (prose asserts at-least, so it
3067
- * floors), and a 90-minute meeting reads "1 hour" instead of "90 minutes".
3068
- */
3069
- declare function meetingWhen(meeting: MeetingRecord, locale?: string): string;
3070
- /** How much of a finished meeting a GUEST may read. Pinned with the server. */
3071
- type GuestRecordAccess = "none" | "summary" | "full";
3072
- /** Widest last. The ORDER is the comparison, exactly as it is on the server. */
3073
- declare const GUEST_RECORD_ACCESS_LEVELS: readonly GuestRecordAccess[];
3074
- /**
3075
- * What a HOST may choose for one meeting. `default` is not a fourth level — it
3076
- * is the absence of a per-meeting choice, and it is the only way to un-choose.
3077
- */
3078
- type GuestRecordAccessSetting = "default" | GuestRecordAccess;
3079
- declare const GUEST_RECORD_ACCESS_CHOICES: readonly GuestRecordAccessSetting[];
3080
- /** `meet_meetings.metadata.guest_record_access`. No DDL; a setting on a row. */
3081
- declare const GUEST_RECORD_ACCESS_METADATA_KEY = "guest_record_access";
3082
- /** What a host chose for THIS meeting, read from the row they already have. */
3083
- declare function guestRecordAccessSetting(meeting: MeetingRecord | null): GuestRecordAccessSetting;
3084
- /** The sentence on the host's control. Plain English, never a token. */
3085
- declare function guestRecordAccessLabel(setting: GuestRecordAccessSetting): string;
3086
- /** What each choice actually means for the people who were in the room. */
3087
- declare function guestRecordAccessExplanation(setting: GuestRecordAccessSetting): string;
3088
- /** What came back from the server, so a surface never has to infer a level. */
3089
- interface GuestRecordAccessResolution {
3090
- readonly level: GuestRecordAccess;
3091
- /** `meeting` when a host chose for this one; `organization` otherwise. */
3092
- readonly source: "meeting" | "organization";
3093
- }
3094
- /**
3095
- * Repeated policy refusals are one access state, not four alarming cards.
3096
- *
3097
- * The individual rows remain durable data for absence handling in their own
3098
- * sections. This projection groups only rows with the exact same remedy, so a
3099
- * distinct transport or integrity failure remains visible on its own.
3100
- */
3101
- declare function summarizeMeetingRecordRestrictions(restrictions: readonly MeetingRecordRestriction[]): readonly MeetingRecordRestrictionNotice[];
3102
- /**
3103
- * Read the record the server shares with THIS guest.
3104
- *
3105
- * `roomToken` is the pass this device kept from the meeting
3106
- * (`TokenClient.remembered`). It is presented in a header, never appended to
3107
- * the URL. A guest with no pass, an expired pass, or a level of `none` gets the
3108
- * server's refusal — as restrictions on a real bundle, so the screen says what
3109
- * it cannot show instead of collapsing into an error page.
3110
- */
3111
- declare function loadGuestMeetingRecord(args: {
3112
- api: MeetApi;
3113
- repository: MeetRepository;
3114
- meetingId: MeetingId;
3115
- meeting: MeetingRecord;
3116
- roomToken: RoomToken | null;
3117
- /**
3118
- * A SIGNED-IN reader the database refused (from another organization, or a
3119
- * guest who later created an account). Their session stands in for the
3120
- * pass: the server reads their `user:<id>` admission or the attendance they
3121
- * claimed, and shares what guests may read (2026-10-01).
3122
- */
3123
- session?: boolean;
3124
- }): Promise<MeetingRecordBundle>;
3125
- /**
3126
- * A HOST chooses what guests may read of THIS meeting.
3127
- *
3128
- * Host power is the SERVER's decision and it re-checks it; this function exists
3129
- * so the control has one call to make, and it returns what the choice RESOLVES
3130
- * to so the screen never has to guess what "my organization's setting" means
3131
- * today.
3132
- */
3133
- declare function setGuestRecordAccess(args: {
3134
- api: MeetApi;
3135
- meetingId: MeetingId;
3136
- organizationId: OrganizationId;
3137
- access: GuestRecordAccessSetting;
3138
- }): Promise<{
3139
- readonly setting: GuestRecordAccessSetting;
3140
- readonly effective: GuestRecordAccessResolution;
3141
- }>;
3142
- /** What a guest is told when the level shares nothing. Rendered, never hidden. */
3143
- declare const GUEST_RECORD_NOT_SHARED: Readonly<Record<"recording" | "transcript" | "notes" | "attendance", string>>;
3144
-
3145
- /**
3146
- * AFTER THE MEETING (Meet wave 3) — the pure pieces every client needs once the
3147
- * room is gone: the attendance report, the transcript and notes as documents,
3148
- * a moment in the transcript as a moment in the recording, search within one
3149
- * transcript, and the two server calls behind the recap email and the saved
3150
- * chat.
3151
- *
3152
- * Nothing here computes MEANING (same rule as `record.ts`): the summary,
3153
- * decisions and action items are the wrap-up's durable rows; this file only
3154
- * arranges, measures and formats them. Every function is framework-free so a
3155
- * worker, a test and a browser produce byte-identical exports.
3156
- */
3157
-
3158
- /**
3159
- * Who someone was, in the words an attendance report uses. `signed-in guest` is
3160
- * an account from outside the meeting's organization that followed the link;
3161
- * `guest` has no account at all.
3162
- */
3163
- type AttendanceKind = "host" | "cohost" | "member" | "signed-in guest" | "guest" | "assistant";
3164
- interface AttendanceRow {
3165
- readonly id: string;
3166
- readonly identity: string;
3167
- readonly name: string;
3168
- readonly userId: string | null;
3169
- readonly kind: AttendanceKind;
3170
- /** First time they were in the room; null when they knocked and never came in. */
3171
- readonly joinedAt: string | null;
3172
- /** When they left; null while still in the room or when the leave was never recorded. */
3173
- readonly leftAt: string | null;
3174
- /**
3175
- * Time in the room. A missing `leftAt` is measured to the meeting's end —
3176
- * the room closed around them — and `null` only when neither is known.
3177
- */
3178
- readonly durationMs: number | null;
3179
- /** True when `durationMs` was measured to the meeting's end, not a recorded leave. */
3180
- readonly durationToEnd: boolean;
3181
- readonly admission: MeetingAttendee["admissionState"];
3182
- /** How many times this person came in (a rejoin or a second device). */
3183
- readonly joins: number;
3184
- }
3185
- /**
3186
- * The attendance report of ONE meeting, host first then by arrival. People who
3187
- * knocked and were never let in are kept (with no join time) — "who asked to
3188
- * come in" is part of the record — but a denied knock has no duration.
3189
- */
3190
- /**
3191
- * WHO a participant row is, as a PERSON (verifier, 2026-09-27: an export listed
3192
- * the same attendee twice under "attended"). A signed-in person is their
3193
- * account — every device and every rejoin is one person. A guest without an
3194
- * account gets a fresh `guest:<random>` identity on every join, so the name
3195
- * they typed is the only thread between their joins (Zoom's attendee report
3196
- * merges link guests the same way); two different accountless guests who
3197
- * typed the identical name read as one, which is the lesser error.
3198
- */
3199
- declare function attendeeKey(person: MeetingAttendee, names?: NameBook): string;
3200
- /**
3201
- * One row per PERSON, in first-seen order: the row that actually joined (the
3202
- * earliest) represents them. For a list of names or a head-count; the
3203
- * attendance REPORT merges times as well (`attendanceReport`).
3204
- */
3205
- declare function uniqueAttendees(attendees: readonly MeetingAttendee[], names?: NameBook): readonly MeetingAttendee[];
3206
- declare function attendanceReport(meeting: MeetingRecord, attendees: readonly MeetingAttendee[], names?: NameBook): readonly AttendanceRow[];
3207
- /** Headline numbers for the top of a report. The assistant is never a person. */
3208
- declare function attendanceTotals(rows: readonly AttendanceRow[]): {
3209
- readonly people: number;
3210
- readonly guests: number;
3211
- readonly neverAdmitted: number;
3212
- readonly averageMs: number | null;
3213
- };
3214
- declare const ATTENDANCE_CSV_HEADER: readonly ["Meeting", "Meeting started", "Name", "Attended as", "Joined", "Left", "Minutes in the meeting", "Admission"];
3215
- /**
3216
- * CSV for a spreadsheet: one header row, one row per person, ISO timestamps (a
3217
- * spreadsheet parses them; a formatted local clock loses the zone). Several
3218
- * meetings — a series — concatenate with one header.
3219
- */
3220
- declare function attendanceCsv(reports: readonly {
3221
- meeting: MeetingRecord;
3222
- rows: readonly AttendanceRow[];
3223
- }[]): string;
3224
- /**
3225
- * Where an instant of the meeting falls in the RECORDING, in milliseconds from
3226
- * its start — or `null` when the recording does not cover it (a recording is
3227
- * often a few minutes of a longer meeting). Never a clamp: seeking a line that
3228
- * was said before recording began to 0:00 would play the wrong words.
3229
- */
3230
- declare function recordingOffsetMs(at: string, recording: RecordingRecord | null): number | null;
3231
- /** `m:ss` or `h:mm:ss` — a position in a recording or a meeting. */
3232
- declare function clockPosition(offsetMs: number): string;
3233
- interface TranscriptMatch {
3234
- readonly segmentId: string;
3235
- /** [start, end) character ranges inside the segment's text, in order. */
3236
- readonly ranges: readonly (readonly [number, number])[];
3237
- }
3238
- /**
3239
- * Case-insensitive search of the transcript text AND the speaker's
3240
- * name. Returns matches in transcript order. An empty query matches nothing.
3241
- */
3242
- declare function searchTranscript(segments: readonly TranscriptSegment[], query: string, names?: NameBook): readonly TranscriptMatch[];
3243
- type TranscriptFormat = "txt" | "md" | "vtt";
3244
- interface ExportOptions {
3245
- readonly names?: NameBook;
3246
- /** Timestamps as a clock of day (default) or offsets from `zero`. */
3247
- readonly locale?: string;
3248
- readonly timeZone?: string;
3249
- }
3250
- /**
3251
- * The transcript as a document. `txt` and `md` group consecutive lines by one
3252
- * speaker with the clock of day; `vtt` is WebVTT cued from the recording's
3253
- * start when a recording exists (so it plays as captions over it), otherwise
3254
- * from the meeting's start — and a cue ends where the next line begins.
3255
- */
3256
- declare function transcriptDocument(bundle: Pick<MeetingRecordBundle, "meeting" | "transcript" | "recording" | "names">, format: TranscriptFormat, options?: ExportOptions): string;
3257
- /**
3258
- * The meeting's notes as Markdown: when, who attended, the summary, decisions
3259
- * and action items (with owners), and the link back. What a person pastes into
3260
- * a doc or an email; also the source for PDF / Word export.
3261
- */
3262
- declare function notesDocument(bundle: Pick<MeetingRecordBundle, "meeting" | "summary" | "decisions" | "actionItems" | "attendees" | "names">, options?: {
3263
- readonly link?: string | null;
3264
- readonly when?: string | null;
3265
- }): string;
3266
- /** How long the meeting ran, in words ("42 minutes"), or null when unknown. */
3267
- declare function meetingLength(meeting: MeetingRecord): string | null;
3268
- interface MeetingChatHistory {
3269
- readonly messages: readonly RoomChatMessage[];
3270
- /** The server's refusal, in its words, when the history could not be read. */
3271
- readonly refusal: {
3272
- readonly message: string;
3273
- readonly remedy: string;
3274
- } | null;
3275
- }
3276
- /**
3277
- * The saved chat of a finished meeting (`GET /v1/meet/chat`). Private messages
3278
- * are never saved, by design, so they are never here. A refusal is returned as
3279
- * data — the record still renders everything else.
3280
- */
3281
- declare function loadMeetingChat(api: MeetApi, meetingId: MeetingId, roomToken?: string | null): Promise<MeetingChatHistory>;
3282
- /** One person the recap can go to, as the server resolved them. */
3283
- interface FollowUpRecipient {
3284
- /** Stable key for the choice: `user:<id>` or `email:<address>`. */
3285
- readonly key: string;
3286
- readonly name: string;
3287
- /** The address, when this reader may see it (the host sees their guests'). */
3288
- readonly email: string | null;
3289
- readonly userId: string | null;
3290
- /** `invitee`, `attendee` or both — where the server found them. */
3291
- readonly sources: readonly ("invitee" | "attendee")[];
3292
- /** False when the server has no way to reach them (no account, no address). */
3293
- readonly reachable: boolean;
3294
- }
3295
- /** The draft the host reviews. The text is the wrap-up's own, arranged. */
3296
- interface FollowUpDraft {
3297
- readonly subject: string;
3298
- readonly body: string;
3299
- readonly recipients: readonly FollowUpRecipient[];
3300
- /** False while the wrap-up has not been written — the draft says so. */
3301
- readonly wrapUpWritten: boolean;
3302
- /** When a recap for this meeting was last sent, and to how many. */
3303
- readonly lastSent: {
3304
- readonly at: string;
3305
- readonly count: number;
3306
- } | null;
3307
- }
3308
- interface FollowUpSendResult {
3309
- readonly dryRun: boolean;
3310
- /** Recipients the send queued (or, on a dry run, would queue). */
3311
- readonly queued: readonly string[];
3312
- /** Recipient keys skipped, each with the reason. */
3313
- readonly skipped: readonly {
3314
- readonly key: string;
3315
- readonly reason: string;
3316
- }[];
3317
- }
3318
- /** The recap draft for a finished meeting. The host, a co-host or an editor. */
3319
- declare function loadFollowUpDraft(api: MeetApi, meetingId: MeetingId): Promise<FollowUpDraft>;
3320
- /**
3321
- * Send the recap the host reviewed. `dryRun` resolves every recipient and
3322
- * renders every email but queues nothing — how a test proves the path without
3323
- * mailing a real inbox. Never called automatically.
3324
- */
3325
- declare function sendFollowUp(api: MeetApi, meetingId: MeetingId, input: {
3326
- readonly subject: string;
3327
- readonly body: string;
3328
- readonly recipientKeys: readonly string[];
3329
- readonly dryRun?: boolean;
3330
- }): Promise<FollowUpSendResult>;
3331
-
3332
- /**
3333
- * MEET WAVE 1 — the two server calls a management screen makes (0.8).
3334
- *
3335
- * Every WRITE (schedule, edit, reschedule, cancel, archive, occurrence change,
3336
- * invite, answer) goes straight to the database through `MeetRepository` — the
3337
- * database decides who may. What a browser cannot do is send an email with a
3338
- * calendar object in it, so after any of those writes a screen calls
3339
- * `announceMeeting`: the server tells each invitee exactly what they have not
3340
- * been told (invitation, update or cancellation, each with its `.ics`), and
3341
- * re-queues reminders. It is idempotent — call it after every save.
3342
- */
3343
-
3344
- interface AnnounceResult {
3345
- readonly invitations: number;
3346
- readonly updates: number;
3347
- readonly cancellations: number;
3348
- readonly alreadyCurrent: number;
3349
- readonly remindersQueued: number;
3350
- readonly remindersCancelled: number;
3351
- }
3352
- /** Bring every invitee's inbox and calendar up to date. The host, a co-host or an editor. */
3353
- declare function announceMeeting(api: MeetApi, meetingId: MeetingId): Promise<AnnounceResult>;
3354
- /**
3355
- * Put a one-off meeting on the caller's own connected Google Calendar. GATED by
3356
- * the server to Google's review accounts while the Calendar scope is in review —
3357
- * a refusal is a `forbidden` MeetError carrying the server's sentence. Everyone
3358
- * already gets the meeting into any calendar through the invitation's `.ics`.
3359
- */
3360
- declare function pushMeetingToGoogleCalendar(api: MeetApi, meetingId: MeetingId, target: {
3361
- connectionId: string;
3362
- calendarId?: string;
3363
- }): Promise<Record<string, unknown>>;
3364
-
3365
- /**
3366
- * THE ROOM ENGINE — the object a host mounts once and never reasons about.
3367
- *
3368
- * It owns every sequence a calling product gets wrong: ask for a token, connect,
3369
- * project the roster, carry the in-room control plane, keep the phase honest
3370
- * through a reconnect, and tear everything down exactly once. A consumer's job
3371
- * is `engine.join(...)` and rendering a snapshot.
3372
- *
3373
- * THREE rules are structural here, not documented-and-hoped:
3374
- *
3375
- * 1. **The phase never lies.** `connecting` becomes `connected` only when the
3376
- * driver says so, and any failure becomes `failed` WITH the remedy attached
3377
- * to the snapshot. There is no path that leaves a spinner running forever —
3378
- * the single most common complaint about every video product.
3379
- * 2. **Role comes from the TOKEN, never from the client.** A participant's role
3380
- * is read out of the server-minted metadata. A client that could decide it
3381
- * was host could mute anyone; here the claim is unforgeable because the
3382
- * server signed it.
3383
- * 3. **Teardown is idempotent and total.** Leaving twice, or unmounting during
3384
- * a connect, disconnects once and leaves no listener behind. The half-torn
3385
- * room is what produces "my camera light stays on after the call".
3386
- */
3387
-
3388
- interface JoinRequest {
3389
- readonly roomName: RoomName;
3390
- readonly meetingId?: string | undefined;
3391
- readonly slug?: string | undefined;
3392
- /** `audio` joins with the camera off. Same room, same code path (D3). */
3393
- readonly mode: CallMode;
3394
- /**
3395
- * Whether to ATTEMPT the microphone on join. Defaults to true. `false` is a
3396
- * deliberate listen-only join — the pre-join screen's "Join with microphone"
3397
- * toggle. Either way the join itself never depends on the outcome; a refused
3398
- * device lands in `snapshot.media.microphone.unavailable`, not in a throw.
3399
- */
3400
- readonly microphone?: boolean | undefined;
3401
- /** Meet wave 5: a breakout room of this meeting; absent/null = the main room. */
3402
- readonly breakoutRoomId?: string | null | undefined;
3403
- }
3404
- interface RoomEngineOptions {
3405
- store: MeetStore;
3406
- tokens: TokenClient;
3407
- identity: MeetIdentity;
3408
- driverFactory: RoomDriverFactory;
3409
- /** Canonical public metadata read; room packets alone never authorize teardown. */
3410
- resolveMeeting?: ((slug: string) => Promise<MeetingRecord>) | undefined;
3411
- onDiagnostic?: ((event: {
3412
- level: "info" | "warn" | "error";
3413
- message: string;
3414
- remedy?: string;
3415
- }) => void) | undefined;
3416
- /** Fired when the local participant leaves for any reason, including kicked. */
3417
- onLeft?: ((reason: string | null) => void) | undefined;
3418
- now?: (() => number) | undefined;
3419
- /** Remembered noise suppression / background choice (Meet wave 2). */
3420
- effectsStorage?: EffectsStorage | undefined;
3421
- /**
3422
- * The device the person PICKED for each kind (the pre-join pickers, the
3423
- * settings panel). Applied before any track is created — through 0.7.x the
3424
- * pickers stored a choice nothing ever read, so every join used the browser
3425
- * default whatever the screen said.
3426
- */
3427
- resolveDevice?: ((kind: "audioinput" | "videoinput" | "audiooutput") => string | null) | undefined;
3428
- }
3429
- interface RoomEngine {
3430
- join(request: JoinRequest): Promise<void>;
3431
- /**
3432
- * 🚨 THE HOST SAID YES — COME IN.
3433
- *
3434
- * A lobby token is minted with `canPublish: false` and `canSubscribe: false`
3435
- * (that is what makes the lobby real rather than a screen over a live
3436
- * microphone), so admission is not a UI state change: the cached token has to
3437
- * be thrown away and a NEW one minted with the grants the server now gives.
3438
- * Without this the waiting room's own sentence — "you will join automatically
3439
- * once you are admitted" — is a lie, and the guest waits forever.
3440
- */
3441
- rejoin(): Promise<void>;
3442
- /**
3443
- * Meet wave 5: go to a breakout room (`null` = back to the main room). The server
3444
- * mints the pass (`/token` with `breakout_room_id`) and refuses a room the plan does
3445
- * not allow; the move is a reconnect that never reads as leaving the meeting.
3446
- */
3447
- moveToBreakout(roomId: string | null): Promise<void>;
3448
- leave(): Promise<void>;
3449
- setMicrophoneEnabled(enabled: boolean): Promise<void>;
3450
- setCameraEnabled(enabled: boolean): Promise<void>;
3451
- setScreenShareEnabled(enabled: boolean): Promise<void>;
3452
- raiseHand(raised: boolean): Promise<void>;
3453
- react(emoji: string): Promise<void>;
3454
- /**
3455
- * A PRIVATE message to one person (Meet wave 2). Data plane only, addressed
3456
- * to that identity, NEVER saved — see `core/chat.ts` for the saved,
3457
- * everyone-chat, which goes through the server.
3458
- */
3459
- sendPrivateChat(to: ParticipantIdentity, text: string): Promise<void>;
3460
- /**
3461
- * Change this participant's own media processing — noise suppression and the
3462
- * camera background — on the live tracks, and remember it in this browser.
3463
- * A refused effect is a diagnostic and a revert, never a lost camera.
3464
- */
3465
- setEffects(next: Partial<MediaEffects>): Promise<void>;
3466
- /** Whether this browser can run a background effect at all (absent, never dead). */
3467
- supportsVideoEffects(): boolean;
3468
- /** Whether this browser can share a screen at all — the control is ABSENT where it cannot. */
3469
- supportsScreenShare(): boolean;
3470
- /** Choose the speaker (audio output) for this meeting. */
3471
- switchDevice(kind: "audioinput" | "videoinput" | "audiooutput", deviceId: string): Promise<void>;
3472
- pin(identity: ParticipantIdentity | null): void;
3473
- setLayout(layout: StageLayout): void;
3474
- attach(identity: ParticipantIdentity, kind: "camera" | "microphone" | "screen", element: HTMLMediaElement): () => void;
3475
- token(): RoomToken | null;
3476
- dispose(): Promise<void>;
3477
- }
3478
- /** Where this browser keeps a participant's media choices. */
3479
- interface EffectsStorage {
3480
- load(): MediaEffects;
3481
- save(effects: MediaEffects): void;
3482
- }
3483
- /**
3484
- * 🚨 THE SERVER'S WORD, ON TWO CARRIERS (MRI-A11, feedback `7418ef78`).
3485
- *
3486
- * aidream mints the role into the token's participant METADATA (a JSON blob)
3487
- * AND as a LiveKit participant ATTRIBUTE. Both come out of one decision in
3488
- * `services/meet/service.py`; neither can be written by a client. The attribute
3489
- * is preferred because LiveKit delivers the attribute map on every roster
3490
- * update, while a roster event can carry an empty metadata string — which is
3491
- * precisely how, through 2026-09-08, EVERY participant projected as
3492
- * "participant" and no host ever saw a host control in a browser.
3493
- *
3494
- * These strings are pinned on the server side too: `test_meet_service.py` reads
3495
- * them out of THIS file and fails if the two sides drift.
3496
- */
3497
- declare const PARTICIPANT_ROLE_ATTRIBUTE = "matrx.participant.role";
3498
- declare const PARTICIPANT_CAN_RECORD_ATTRIBUTE = "matrx.participant.can_record";
3499
- /**
3500
- * Metadata is SERVER-MINTED and therefore trusted; it is still parsed
3501
- * defensively, because a malformed blob must degrade a tile to "participant"
3502
- * rather than throw inside a roster event and freeze the whole grid.
3503
- */
3504
- declare function projectParticipant(driver: DriverParticipant, joinedAt: string): MeetParticipant;
3505
- declare function createRoomEngine(options: RoomEngineOptions): RoomEngine;
3506
- /** Exported for the host-power check in UI code — one definition, one rank. */
3507
- declare function canModerate(participant: MeetParticipant | undefined): boolean;
3508
- declare function hostUserIds(participants: readonly MeetParticipant[]): readonly UserId[];
3509
-
3510
- /**
3511
- * A PARTICIPANT'S OWN MEDIA CHOICES, REMEMBERED (Meet wave 2).
3512
- *
3513
- * Noise suppression and the camera background are personal and per device —
3514
- * Zoom and Meet both remember them — so they live in this browser, never on a
3515
- * server. Blocked storage means the defaults (suppression on, no background),
3516
- * never a failed join.
3517
- */
3518
-
3519
- declare function createWebEffectsStorage(key?: string): EffectsStorage;
3520
-
3521
- /**
3522
- * THE IN-ROOM SERVER CALLS (Meet wave 2) — host moderation and the SAVED chat.
3523
- *
3524
- * 🚨 **Why these are server calls and not data packets.** Through 0.7.x a host's
3525
- * "Mute" and "Remove" were published on the room's data plane (`mx.host`) and
3526
- * nothing anywhere listened: the buttons did nothing. A client cannot be the
3527
- * authority on host power, and a guest cannot write the database. So both go to
3528
- * aidream (`POST /v1/meet/moderate`, `POST|GET /v1/meet/chat`), which decides the
3529
- * actor's role from the meeting itself, performs the act through LiveKit's server
3530
- * API, writes what is durable, and tells the room on its own `matrx.meet.*` topic.
3531
- *
3532
- * A guest proves who they are with the room pass this server signed for them, in
3533
- * `x-meet-room-token` — a header, never a URL.
3534
- */
3535
-
3536
- /** Every host power the server performs. One vocabulary with `services/meet/moderation.py`. */
3537
- type ModerationAction = "mute" | "stop_video" | "stop_screen" | "mute_all" | "ask_unmute" | "ask_all_unmute" | "spotlight" | "lower_hand" | "lower_all_hands" | "make_cohost" | "revoke_cohost" | "transfer_host" | "rename" | "remove" | "report";
3538
- declare const MODERATION_ACTIONS: readonly ModerationAction[];
3539
- interface ModerationRequest {
3540
- readonly action: ModerationAction;
3541
- readonly identity?: ParticipantIdentity | null | undefined;
3542
- readonly name?: string | undefined;
3543
- readonly reason?: string | undefined;
3544
- }
3545
- interface ModerationResult {
3546
- readonly action: ModerationAction;
3547
- readonly affected: readonly string[];
3548
- /** The server's sentence for the actor. */
3549
- readonly message: string;
3550
- }
3551
- interface InRoomClient {
3552
- moderate(request: ModerationRequest): Promise<ModerationResult>;
3553
- /** Send a message to EVERYONE. Saved; shows at once as pending, then as saved. */
3554
- sendChat(text: string): Promise<void>;
3555
- /** Read the saved chat once per join, so a late joiner sees what was said. */
3556
- loadChat(): Promise<void>;
3557
- /**
3558
- * Meet wave 5: read the polls, questions, whiteboard and breakout plan this viewer
3559
- * may see, once per join. A refusal is written to `collab.error`, never thrown away.
3560
- */
3561
- loadCollab(): Promise<void>;
3562
- /**
3563
- * Meet wave 5: one poll / Q&A / whiteboard / breakout action. The server's answer is
3564
- * folded into the store at once (the room packet that follows is the same item).
3565
- * Returns the server's sentence; throws a `MeetError` with the server's words.
3566
- */
3567
- collab(request: CollabRequest): Promise<CollabResult>;
3568
- /**
3569
- * Meet wave 5: translate finished caption lines into `language` for this viewer.
3570
- * Results land in `collab.captionTranslations`; a refusal in `captionTranslationError`.
3571
- */
3572
- translateCaptions(language: string, lines: readonly {
3573
- id: string;
3574
- text: string;
3575
- }[]): Promise<void>;
3576
- }
3577
- /** Every room-item action the server performs. One vocabulary with `services/meet/collab.py`. */
3578
- type CollabAction = "poll_create" | "poll_update" | "poll_launch" | "poll_close" | "poll_share" | "poll_vote" | "poll_delete" | "question_ask" | "question_upvote" | "question_answer" | "question_reopen" | "question_dismiss" | "question_delete" | "stroke_add" | "stroke_erase" | "whiteboard_clear" | "breakout_open" | "breakout_update" | "breakout_broadcast" | "breakout_move" | "breakout_join" | "breakout_close" | "breakout_expire";
3579
- interface CollabRequest {
3580
- readonly action: CollabAction;
3581
- readonly itemId?: string | undefined;
3582
- readonly question?: string | undefined;
3583
- readonly options?: readonly string[] | undefined;
3584
- readonly multiple?: boolean | undefined;
3585
- readonly anonymous?: boolean | undefined;
3586
- readonly optionIds?: readonly string[] | undefined;
3587
- readonly text?: string | undefined;
3588
- readonly stroke?: {
3589
- readonly points: readonly (readonly [number, number])[];
3590
- readonly color: string;
3591
- readonly width: number;
3592
- readonly tool: "pen" | "highlighter";
3593
- } | undefined;
3594
- readonly rooms?: readonly {
3595
- readonly name: string;
3596
- readonly identities: readonly string[];
3597
- }[] | undefined;
3598
- readonly allowChoose?: boolean | undefined;
3599
- readonly minutes?: number | undefined;
3600
- readonly identity?: string | undefined;
3601
- /** `null` = the main room (breakout_join / breakout_move). */
3602
- readonly roomId?: string | null | undefined;
3603
- }
3604
- interface CollabResult {
3605
- readonly action: CollabAction;
3606
- readonly message: string;
3607
- readonly removed: readonly string[];
3608
- /** The id of the item the server returned (a created poll, a new question), or null. */
3609
- readonly itemId: string | null;
3610
- }
3611
- interface InRoomClientOptions {
3612
- api: MeetApi;
3613
- store: MeetStore;
3614
- /** The room pass for THIS meeting, or null (a signed-in member needs none). */
3615
- resolveRoomToken: () => string | null;
3616
- now?: (() => number) | undefined;
3617
- }
3618
- declare function createInRoomClient(options: InRoomClientOptions): InRoomClient;
3619
-
3620
- /**
3621
- * ONE MEETING RECORD PER READER PER TAB — read once, kept current by one
3622
- * ref-counted channel (0.7.79).
3623
- *
3624
- * 🚨 THE DEFECT THIS CLOSES. `useMeetingRecord` held the bundle in its own
3625
- * component state and read all four parts (`meet_participants`, `meet_notes`,
3626
- * `meet_transcript_segments`, `meet_recordings`) in a mount effect. A host that
3627
- * hides a view without unmounting its data — a board tile asleep under React
3628
- * `<Activity>`, which tears every effect down and runs it again on wake — read
3629
- * the whole record again on every wake, on a Remove + Undo, and once more for
3630
- * every second view of the same meeting (the record tab, the meeting's agent
3631
- * surface, five "meeting notes" tiles). The view owned the data.
3632
- *
3633
- * THE SHAPE. One entry per (reader, meeting). The first view reads; every later
3634
- * view — the same view after a wake or a remount included — renders the entry
3635
- * and reads nothing. A read already in flight is joined. A forced read (Retry,
3636
- * a reconnect) supersedes it, and an answer that lands after a newer read was
3637
- * asked is DROPPED (the late-answer guard), so an old answer can never paint
3638
- * over a newer one.
3639
- *
3640
- * LIVE WHILE HELD. A member's entry (a direct read, kept as its four parts) is
3641
- * patched row by row from the database's change feed for the four tables, then
3642
- * assembled again — one row, never four reads. The channel opens on the first
3643
- * holder and closes `MEETING_RECORD_GRACE_MS` after the last one lets go, so a
3644
- * wake or an Undo inside the grace costs nothing. Past the grace nothing
3645
- * watched the meeting, so the entry is marked stale and the next view reads it
3646
- * once. A guest's record arrives through the server as a finished bundle; it
3647
- * has no change feed and is kept as it is.
3648
- *
3649
- * Framework-free on purpose: `react/hooks.ts` is one reader of it.
3650
- */
3651
-
3652
- /** How long a meeting's channel and kept record outlive the last view of it. */
3653
- declare const MEETING_RECORD_GRACE_MS = 60000;
3654
- interface MeetingRecordFailure {
3655
- readonly message: string;
3656
- readonly remedy: string;
3657
- }
3658
- interface MeetingRecordState {
3659
- readonly loading: boolean;
3660
- readonly record: MeetingRecordBundle | null;
3661
- /** A failure that stopped the WHOLE record loading (never kept as an answer). */
3662
- readonly failure: MeetingRecordFailure | null;
3663
- }
3664
- /** What a read produced: the four parts (patchable), or a finished bundle (a guest's). */
3665
- type MeetingRecordAnswer = {
3666
- readonly kind: "parts";
3667
- readonly meeting: MeetingRecord;
3668
- readonly parts: MeetingRecordParts;
3669
- } | {
3670
- readonly kind: "bundle";
3671
- readonly bundle: MeetingRecordBundle;
3672
- };
3673
- /** One changed row, already projected by the repository. `null` + `removedId` = gone. */
3674
- type MeetingRecordChange = {
3675
- readonly part: "transcript";
3676
- readonly row: MeetingRecordParts["transcript"][number] | null;
3677
- readonly removedId?: string | undefined;
3678
- } | {
3679
- readonly part: "notes";
3680
- readonly row: MeetingRecordParts["notes"][number] | null;
3681
- readonly removedId?: string | undefined;
3682
- } | {
3683
- readonly part: "attendance";
3684
- readonly row: MeetingRecordParts["attendees"][number] | null;
3685
- readonly removedId?: string | undefined;
3686
- } | {
3687
- readonly part: "recording";
3688
- readonly row: MeetingRecordParts["recordings"][number] | null;
3689
- readonly removedId?: string | undefined;
3690
- };
3691
- interface MeetingRecordCache {
3692
- /** The kept state for `key` (stable until it changes). */
3693
- state(key: string | null): MeetingRecordState;
3694
- subscribe(listener: () => void): () => void;
3695
- /**
3696
- * Read unless the entry is kept and current (or a read is in flight — joined).
3697
- * `force` reads anyway and supersedes a read in flight. Never rejects: a
3698
- * failure is in the state.
3699
- */
3700
- ensure(key: string, read: () => Promise<MeetingRecordAnswer>, options?: {
3701
- force?: boolean;
3702
- }): Promise<void>;
3703
- /** The meeting row moved (a rename, it ended): assemble again — no read. */
3704
- setMeeting(key: string, meeting: MeetingRecord): void;
3705
- /** Apply one changed row from the change feed — no read. */
3706
- apply(key: string, change: MeetingRecordChange): void;
3707
- /**
3708
- * Hold the live half: `open` on the first holder, its close a grace after the
3709
- * last. `owner` is what the channel runs on (the realtime manager): a new
3710
- * owner means the old one's channels are gone, so it opens again.
3711
- */
3712
- hold(key: string, open: () => () => void, owner?: unknown): () => void;
3713
- /** Tests only. */
3714
- reset(): void;
3715
- }
3716
- declare function createMeetingRecordCache(options?: {
3717
- graceMs?: number;
3718
- }): MeetingRecordCache;
3719
-
3720
- /**
3721
- * THE OBSERVATION CONTRACT, DERIVED FROM TODAY'S STORE (CORE-DESIGN §7 step S0).
3722
- *
3723
- * The real-browser scenario harness reads a meeting through a small set of
3724
- * `data-meet-*` attributes (common-docs `meet/duplicates/HARNESS-CONTRACT.md`,
3725
- * owned by the harness lane). This module turns the store's `MeetSnapshot`
3726
- * into exactly those values — pure, framework-free, rendered ONCE by the
3727
- * package (`useCoreRootProps()` / `<MeetRoot>`), never per skin.
3728
- *
3729
- * S0 rule: TODAY'S VALUES ONLY. Every value here is what the person actually
3730
- * sees on today's screens, including today's defects — a `failed` or `left`
3731
- * room renders the pre-join screen, so its phase is `prejoin`, and the harness
3732
- * records that as the defect it is. A value the store does not know (host
3733
- * presence before joining, autoplay blocking) is OMITTED rather than guessed:
3734
- * an absent attribute reads as "unknown", a guessed one would lie.
3735
- */
3736
-
3737
- /** `data-meet-phase` values this release renders (a subset of the contract's §2). */
3738
- type ObservedPhase = "resolving" | `refused:${string}` | "prejoin" | "joining" | "waiting:knocking" | "in_call" | "disconnected" | "ended";
3739
- type ObservedConnection = "stable" | "reconnecting" | "server_unreachable";
3740
- interface MeetObservation {
3741
- readonly phase: ObservedPhase;
3742
- readonly reason: string | null;
3743
- readonly role: ParticipantRole | "none";
3744
- readonly connection: ObservedConnection;
3745
- /** null = this tab does not know (it is not in the room). */
3746
- readonly hostPresent: boolean | null;
3747
- readonly lobbyCount: number;
3748
- readonly recording: boolean;
3749
- /** null = today's store does not track autoplay blocking. */
3750
- readonly audioBlocked: boolean | null;
3751
- /** null outside `joining` / `in_call` — no device is in play there today. */
3752
- readonly camera: string | null;
3753
- readonly microphone: string | null;
3754
- }
3755
- interface ObserveInput {
3756
- readonly snapshot: MeetSnapshot | null;
3757
- /** The row the consumer resolved, when the store has none yet (the guest lane). */
3758
- readonly meeting?: MeetingRecord | null | undefined;
3759
- }
3760
- /** The store's device state → the contract's `camera` / `microphone` value. */
3761
- declare function observedDevice(track: LocalTrackState): string;
3762
- /**
3763
- * The phase the person is looking at, by the same branches `<MeetingRoom>`
3764
- * renders on: record → pre-join (idle/failed/left) → waiting room (lobby) →
3765
- * the room (everything else).
3766
- */
3767
- declare function observedPhase(input: ObserveInput): ObservedPhase;
3768
- declare function observe(input: ObserveInput): MeetObservation;
3769
- /** A phase the consumer renders itself (link resolution, the guest-name step, a dead link). */
3770
- declare function staticObservation(phase: ObservedPhase, reason?: string | null): MeetObservation;
3771
- /** The root element's attributes. Unknown values are omitted, never guessed. */
3772
- declare function rootAttributes(o: MeetObservation): Record<string, string>;
3773
- /** The contract's per-person connection vocabulary. `unknown` is not a reading. */
3774
- declare function observedQuality(quality: ConnectionQuality): string | null;
3775
- /** `[data-meet-participant]` attributes for one person's tile or roster row. */
3776
- declare function participantAttributes(p: MeetParticipant): Record<string, string>;
3777
- /** The listen-only notice's contract code, from why the microphone is off. */
3778
- declare function microphoneNoticeCode(track: LocalTrackState): "mic-blocked" | "device-lost" | null;
3779
-
3780
- export { ATTENDANCE_CSV_HEADER, type AccessTokenSource, type AdmissionState, type AnnounceResult, type ApplyRoomEventArgs, type AskArgs, type AttendanceKind, type AttendanceRow, type AudioProcessing, CALL_INVITE_KIND, CALL_INVITE_VERSION, CAPTION_LANGUAGES, CONSENT_NOTICE_ATTRIBUTE, type CallCenter, type CallCenterOptions, type CallId, type CallInvite, type CallInviteHandlerOptions, type CallInvitePayload, type CallMode, type CallState, type CallablePerson, type CollabAction, type CollabPanel, type CollabRequest, type CollabResult, type CollabSnapshot, type ConnectionQuality, type CreateCallInviteInput, type CreateMeetingInput, DATA_TOPICS, DEFAULT_EFFECTS, DEFAULT_PAGE_SIZE, type DataTopic, type DeviceKind, type DeviceManager, type DeviceManagerOptions, type DeviceSnapshot, type DeviceStorage, type DriverConnectionState, type DriverParticipant, type DriverTrackKind, type DriverTranscription, EMPTY_COLLAB, END_MEETING_NOT_SERVED, type EffectsStorage, type EndMeetingResult, FILE_SESSION_PATH, type FollowUpDraft, type FollowUpRecipient, type FollowUpSendResult, GUEST_RECORD_ACCESS_CHOICES, GUEST_RECORD_ACCESS_LEVELS, GUEST_RECORD_ACCESS_METADATA_KEY, GUEST_RECORD_NOT_SHARED, type GuestRecordAccess, type GuestRecordAccessResolution, type GuestRecordAccessSetting, IDLE_MEDIA, IDLE_TRACK, type InRoomClient, type InRoomClientOptions, type InviteeInput, type InviteeRole, type JoinRequest, type JsonObject, type JsonValue, LISTEN_ONLY_NOTICE, type LibraryRecording, type LiveKitDriverOptions, type LobbyEntry, type LocalMediaState, type LocalTrackState, MEETING_INVITE_KIND, MEETING_INVITE_VERSION, MEETING_RECORD_GRACE_MS, MEET_ANSWER_CONTEXT_EVENT, MEET_ANSWER_EVENTS, MEET_EVENTS, MEET_ROUTES, MEET_SCHEMA, MODERATION_ACTIONS, type MediaDevicesLike, type MediaEffects, type MediaUnavailable, type MeetAi, type MeetAiAnswer, type MeetAiOptions, type MeetAnswerCoverage, type MeetApi, type MeetApiOptions, type MeetApiRequest, type MeetBreakoutBroadcast, type MeetBreakoutPlan, type MeetBreakoutRoom, type MeetCollabSettings, type MeetDevice, MeetError, type MeetErrorCode, type MeetEvent, type MeetFileMedia, type MeetGrants, type MeetIdentity, type MeetObservation, type MeetParticipant, type MeetPoll, type MeetPollOption, type MeetPollState, type MeetQuestion, type MeetQuestionState, type MeetRepository, type MeetRoomDriver, type MeetSnapshot, type MeetStore, type MeetStroke, type MeetingActivities, type MeetingAttendee, type MeetingChanges, type MeetingChatHistory, type MeetingId, type MeetingInviteHandlerOptions, type MeetingInvitePayload, type MeetingInvitee, type MeetingKind, type MeetingNote, type MeetingNoteKind, type MeetingOccurrence, type MeetingRecord, type MeetingRecordAnswer, type MeetingRecordBundle, type MeetingRecordCache, type MeetingRecordChange, type MeetingRecordFailure, type MeetingRecordParts, type MeetingRecordRestriction, type MeetingRecordRestrictionNotice, type MeetingRecordState, type MeetingSearchHit, type MeetingSearchSource, type MeetingSession, type MeetingSessionOptions, type MeetingSettings, type ModerationAction, type ModerationNotice, type ModerationRequest, type ModerationResult, NOTE_TAKER_IDENTITY_PREFIX, NOTE_TAKER_METADATA_KEY, NO_GRANTS, type NameBook, type NoteTakerDispatch, type NoteTakerRefusal, type NoteTakerState, type NoteTakerStatus, ORGANIZATION_HEADER, type ObserveInput, type ObservedConnection, type ObservedPhase, type OccurrenceOverride, type OccurrenceState, type OrganizationId, PARTICIPANT_CAN_RECORD_ATTRIBUTE, PARTICIPANT_ROLE_ATTRIBUTE, type ParticipantIdentity, type ParticipantRole, REACTION_TTL_MS, REFUSED_ABSENCE, ROLE_RANK, ROOM_EVENTS, ROOM_EVENT_PREFIX, ROOM_TOKEN_HEADER, RPCS, type RecordingController, type RecordingControllerOptions, type RecordingPolicy, type RecordingRecord, type RecordingState, type RepositoryOptions, type RoomChatMessage, type RoomDriverFactory, type RoomDriverHandlers, type RoomEngine, type RoomEngineOptions, type RoomEventOutcome, type RoomEventTopic, type RoomName, type RoomPhase, type RoomReaction, type RoomToken, type RoomTokenStorage, type RsvpAnswer, type RsvpInvitation, type RsvpState, type ScheduleMeetingInput, type Stage, type StageArgs, type StageLayout, TABLES, TRANSCRIPTION_ATTRIBUTES, TRANSCRIPTION_TOPIC, type TokenClient, type TokenClientOptions, type TokenRequest, type TranscriptBlock, type TranscriptFormat, type TranscriptMatch, type TranscriptSegment, type TranscriptSource, type UpcomingOccurrence, type UserId, type VideoEffect, type WrapUpAnnouncement, type WrapUpStatus, absenceSentence, activeWrapUpSummary, announceMeeting, applyCollabPacket, applyRoomEvent, asCallId, asMeetingId, asOrganizationId, asParticipantIdentity, asRoomName, asUserId, assembleMeetingRecord, assignedRoom, attendanceCsv, attendanceReport, attendanceTotals, attendeeKey, autoAssign, breakoutRemainingMs, callInviteAction, callInviteText, callStateLabel, canModerate, canModerateParticipant, canTransition, captionSegment, changesPatch, clockPosition, collabFromState, computeStage, countLabel, createCallCenter, createCallInviteHandler, createDeviceManager, createInRoomClient, createLiveKitDriver, createMeetAi, createMeetApi, createMeetRepository, createMeetStore, createMeetingInviteHandler, createMeetingRecordCache, createMeetingSession, createMemoryDeviceStorage, createRecordingController, createRoomEngine, createTokenClient, createWebDeviceStorage, createWebEffectsStorage, createWebRoomTokenStorage, defaultCaptionLanguage, displayNameFor, fileDownloadPath, gridColumns, groupTranscript, guestRecordAccessExplanation, guestRecordAccessLabel, guestRecordAccessSetting, hasEnded, hostUserIds, invalidResponse, isGuest, isGuestIdentity, isHostRole, isListenOnly, isNoteTaker, isRealName, isRoomEventTopic, learnName, learnNames, listenOnlyNotice, loadFollowUpDraft, loadGuestMeetingRecord, loadMeetingActivities, loadMeetingChat, loadMeetingRecord, loadMeetingRecordParts, mapConnectionState, mapQuality, meetingInviteAction, meetingLength, meetingLink, meetingPath, meetingSlug, meetingTopic, meetingWhen, mergeCaption, mergeRecord, microphoneNoticeCode, normalizeDeviceError, normalizeMeetError, normalizeSpoken, noteTakerIdentity, noteTakerLabel, noteTakerParticipant, noteTakerStatus, notesDocument, observe as observeMeeting, observedDevice, observedPhase, observedQuality, orderParticipants, parseRoomToken, participantAttributes, participantSummary, playableRecordingFileId, projectInvitee, projectLibraryRecording, projectNote, projectOccurrence, projectOverride, projectParticipant, projectRsvpInvitation, projectSearchHit, projectSegment, projectUpcoming, pushMeetingToGoogleCalendar, readServerRefusal, readableTranscript, recordingLabel, recordingNotice, recordingOffsetMs, roleOf, roomTopic, rootAttributes, searchTranscript, sendFollowUp, setGuestRecordAccess, settingsPatch, snapshotParticipant, snippetRuns, staticObservation, summarizeMeetingRecordRestrictions, transcriptDocument, unavailableFrom, uniqueAttendees, userTopic, wasRefused, withItem, wrapUpStatus };
1
+ export { ATTENDANCE_CSV_HEADER, AnnounceResult, ApplyRoomEventArgs, AskArgs, AttendanceKind, AttendanceRow, AudioProcessing, CALL_INVITE_KIND, CALL_INVITE_VERSION, CONSENT_NOTICE_ATTRIBUTE, CallCenter, CallCenterOptions, CallInviteHandlerOptions, CallInvitePayload, CollabAction, CollabRequest, CollabResult, CollabRuntime, CollabStoreLike, CreateCallInviteInput, CreateMeetingInput, DATA_TOPICS, DEFAULT_EFFECTS, DEFAULT_PAGE_SIZE, DataTopic, DeviceKind, DeviceManager, DeviceManagerOptions, DeviceSnapshot, DeviceStorage, DriverConnectionState, DriverParticipant, DriverTrackKind, DriverTranscription, END_MEETING_NOT_SERVED, EffectsStorage, EndMeetingResult, FollowUpDraft, FollowUpRecipient, FollowUpSendResult, GUEST_RECORD_ACCESS_CHOICES, GUEST_RECORD_ACCESS_LEVELS, GUEST_RECORD_ACCESS_METADATA_KEY, GUEST_RECORD_NOT_SHARED, GuestRecordAccess, GuestRecordAccessResolution, GuestRecordAccessSetting, IDLE_MEDIA, IDLE_TRACK, InRoomClient, InRoomClientOptions, JoinRequest, LISTEN_ONLY_NOTICE, LibraryRecording, LiveKitDriverOptions, LocalMediaState, LocalTrackState, MEETING_INVITE_KIND, MEETING_INVITE_VERSION, MEETING_RECORD_GRACE_MS, MEET_ANSWER_CONTEXT_EVENT, MEET_ANSWER_EVENTS, MEET_EVENTS, MEET_REACTIONS, MEET_ROUTES, MEET_SCHEMA, MODERATION_ACTIONS, MediaDevicesLike, MediaEffects, MediaUnavailable, MeetAi, MeetAiAnswer, MeetAiOptions, MeetAnswerCoverage, MeetDevice, MeetError, MeetErrorCode, MeetEvent, MeetGrants, MeetObservation, MeetRepository, MeetRoomDriver, MeetSnapshot, MeetStore, MeetingChatHistory, MeetingInviteHandlerOptions, MeetingInvitePayload, MeetingRecordAnswer, MeetingRecordBundle, MeetingRecordCache, MeetingRecordChange, MeetingRecordFailure, MeetingRecordParts, MeetingRecordRestriction, MeetingRecordRestrictionNotice, MeetingRecordState, MeetingSearchHit, MeetingSearchSource, MeetingSession, MeetingSessionOptions, ModerationAction, ModerationNotice, ModerationRequest, ModerationResult, NOTE_TAKER_IDENTITY_PREFIX, NOTE_TAKER_METADATA_KEY, NO_GRANTS, NameBook, NoteTakerDispatch, NoteTakerRefusal, NoteTakerStatus, ObserveInput, ObservedConnection, ObservedPhase, PARTICIPANT_CAN_RECORD_ATTRIBUTE, PARTICIPANT_ROLE_ATTRIBUTE, REACTION_TTL_MS, REFUSED_ABSENCE, ROOM_EVENTS, ROOM_EVENT_PREFIX, ROOM_TOKEN_HEADER, RPCS, RecordingController, RecordingControllerOptions, RepositoryOptions, RoomChatMessage, RoomDriverFactory, RoomDriverHandlers, RoomEngine, RoomEngineOptions, RoomEventOutcome, RoomEventTopic, RoomReaction, RoomToken, RoomTokenStorage, ScheduleMeetingInput, Stage, StageArgs, TABLES, TRANSCRIPTION_ATTRIBUTES, TRANSCRIPTION_TOPIC, TokenClient, TokenClientOptions, TokenRequest, TranscriptBlock, TranscriptFormat, TranscriptMatch, VideoEffect, WrapUpStatus, absenceSentence, activeWrapUpSummary, activityAttention, announceMeeting, applyRoomEvent, assembleMeetingRecord, attendanceCsv, attendanceReport, attendanceTotals, attendeeKey, availableActivities, callInviteAction, callInviteText, callStateLabel, canModerate, canModerateParticipant, canTransition, captionSegment, changesPatch, clockPosition, computeStage, countLabel, createCallCenter, createCallInviteHandler, createDeviceManager, createInRoomClient, createLiveKitDriver, createMeetAi, createMeetRepository, createMeetStore, createMeetingInviteHandler, createMeetingRecordCache, createMeetingSession, createMemoryDeviceStorage, createRecordingController, createRoomEngine, createTokenClient, createWebDeviceStorage, createWebEffectsStorage, createWebRoomTokenStorage, displayNameFor, gridColumns, groupTranscript, guestRecordAccessExplanation, guestRecordAccessLabel, guestRecordAccessSetting, hasEnded, hostUserIds, invalidResponse, isGuestIdentity, isListenOnly, isNoteTaker, isRealName, isRoomEventTopic, learnName, learnNames, listenOnlyNotice, loadFollowUpDraft, loadGuestMeetingRecord, loadMeetingChat, loadMeetingRecord, loadMeetingRecordParts, mapConnectionState, mapQuality, meetingInviteAction, meetingLength, meetingLink, meetingPath, meetingSlug, meetingTopic, meetingWhen, mergeCaption, mergeRecord, microphoneNoticeCode, moveToRoom, nextTranslationBatch, normalizeDeviceError, normalizeMeetError, normalizeSpoken, noteTakerIdentity, noteTakerLabel, noteTakerParticipant, noteTakerStatus, notesDocument, observeMeeting, observedDevice, observedPhase, observedQuality, orderParticipants, parseRoomToken, participantAttributes, participantSummary, performCollab, playableRecordingFileId, projectInvitee, projectLibraryRecording, projectNote, projectOccurrence, projectOverride, projectParticipant, projectRsvpInvitation, projectSearchHit, projectSegment, projectUpcoming, pushMeetingToGoogleCalendar, readServerRefusal, readableTranscript, recordingLabel, recordingNotice, recordingOffsetMs, roleOf, roomTopic, rootAttributes, searchTranscript, sendFollowUp, setCaptionLanguage, setGuestRecordAccess, settingsPatch, snapshotParticipant, snippetRuns, staticObservation, summarizeMeetingRecordRestrictions, takeMoveRequest, takePanelRequest, transcriptDocument, unavailableFrom, uniqueAttendees, untranslatedLines, userTopic, wasRefused, wrapUpStatus } from './core.js';
2
+ export { A as AccessTokenSource, j as AdmissionState, k as CAPTION_LANGUAGES, l as CallId, d as CallInvite, f as CallMode, m as CallState, n as CallablePerson, C as CollabPanel, i as CollabSnapshot, o as ConnectionQuality, E as EMPTY_COLLAB, F as FILE_SESSION_PATH, I as InviteeInput, p as InviteeRole, J as JsonObject, q as JsonValue, L as LobbyEntry, b as MeetApi, r as MeetApiOptions, s as MeetApiRequest, t as MeetBreakoutBroadcast, u as MeetBreakoutPlan, v as MeetBreakoutRoom, w as MeetCollabSettings, x as MeetFileMedia, c as MeetIdentity, a as MeetParticipant, y as MeetPoll, z as MeetPollOption, B as MeetPollState, D as MeetQuestion, G as MeetQuestionState, H as MeetStroke, K as MeetingActivities, N as MeetingAttendee, Q as MeetingChanges, h as MeetingId, V as MeetingInvitee, X as MeetingKind, g as MeetingNote, Y as MeetingNoteKind, Z as MeetingOccurrence, M as MeetingRecord, _ as MeetingSettings, $ as NoteTakerState, a0 as ORGANIZATION_HEADER, a1 as OccurrenceOverride, a2 as OccurrenceState, O as OrganizationId, P as ParticipantIdentity, e as ParticipantRole, a3 as ROLE_RANK, a4 as RecordingPolicy, a5 as RecordingRecord, a6 as RecordingState, R as RoomName, a7 as RoomPhase, a8 as RsvpAnswer, a9 as RsvpInvitation, aa as RsvpState, S as StageLayout, ab as TranscriptSegment, T as TranscriptSource, ac as UpcomingOccurrence, U as UserId, W as WrapUpAnnouncement, ad as applyCollabPacket, ae as asCallId, af as asMeetingId, ag as asOrganizationId, ah as asParticipantIdentity, ai as asRoomName, aj as asUserId, ak as assignedRoom, al as autoAssign, am as breakoutRemainingMs, an as collabFromState, ao as createMeetApi, ap as defaultCaptionLanguage, aq as fileDownloadPath, ar as isGuest, as as isHostRole, at as loadMeetingActivities, au as withItem } from './collab-Bss-_Jfp.js';
3
+ import '@ai-matrx/realtime';
4
+ import '@ai-matrx/messaging';
5
+ import 'livekit-client';