@ai-matrx/meet 0.7.2 → 0.7.5

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.cjs CHANGED
@@ -34,6 +34,7 @@ __export(src_exports, {
34
34
  CALL_INVITE_VERSION: () => CALL_INVITE_VERSION,
35
35
  CONSENT_NOTICE_ATTRIBUTE: () => CONSENT_NOTICE_ATTRIBUTE,
36
36
  DATA_TOPICS: () => DATA_TOPICS,
37
+ DEFAULT_EFFECTS: () => DEFAULT_EFFECTS,
37
38
  DEFAULT_PAGE_SIZE: () => DEFAULT_PAGE_SIZE,
38
39
  END_MEETING_NOT_SERVED: () => END_MEETING_NOT_SERVED,
39
40
  FILE_SESSION_PATH: () => FILE_SESSION_PATH,
@@ -51,6 +52,7 @@ __export(src_exports, {
51
52
  MEET_EVENTS: () => MEET_EVENTS,
52
53
  MEET_ROUTES: () => MEET_ROUTES,
53
54
  MEET_SCHEMA: () => MEET_SCHEMA,
55
+ MODERATION_ACTIONS: () => MODERATION_ACTIONS,
54
56
  MeetError: () => MeetError,
55
57
  NOTE_TAKER_IDENTITY_PREFIX: () => NOTE_TAKER_IDENTITY_PREFIX,
56
58
  NOTE_TAKER_METADATA_KEY: () => NOTE_TAKER_METADATA_KEY,
@@ -69,6 +71,7 @@ __export(src_exports, {
69
71
  TRANSCRIPTION_ATTRIBUTES: () => TRANSCRIPTION_ATTRIBUTES,
70
72
  TRANSCRIPTION_TOPIC: () => TRANSCRIPTION_TOPIC,
71
73
  absenceSentence: () => absenceSentence,
74
+ announceMeeting: () => announceMeeting,
72
75
  applyRoomEvent: () => applyRoomEvent,
73
76
  asCallId: () => asCallId,
74
77
  asMeetingId: () => asMeetingId,
@@ -83,11 +86,13 @@ __export(src_exports, {
83
86
  canModerateParticipant: () => canModerateParticipant,
84
87
  canTransition: () => canTransition,
85
88
  captionSegment: () => captionSegment,
89
+ changesPatch: () => changesPatch,
86
90
  computeStage: () => computeStage,
87
91
  countLabel: () => countLabel,
88
92
  createCallCenter: () => createCallCenter,
89
93
  createCallInviteHandler: () => createCallInviteHandler,
90
94
  createDeviceManager: () => createDeviceManager,
95
+ createInRoomClient: () => createInRoomClient,
91
96
  createLiveKitDriver: () => createLiveKitDriver,
92
97
  createMeetAi: () => createMeetAi,
93
98
  createMeetApi: () => createMeetApi,
@@ -100,6 +105,7 @@ __export(src_exports, {
100
105
  createRoomEngine: () => createRoomEngine,
101
106
  createTokenClient: () => createTokenClient,
102
107
  createWebDeviceStorage: () => createWebDeviceStorage,
108
+ createWebEffectsStorage: () => createWebEffectsStorage,
103
109
  createWebRoomTokenStorage: () => createWebRoomTokenStorage,
104
110
  displayNameFor: () => displayNameFor,
105
111
  fileDownloadPath: () => fileDownloadPath,
@@ -127,6 +133,7 @@ __export(src_exports, {
127
133
  mapQuality: () => mapQuality,
128
134
  meetingInviteAction: () => meetingInviteAction,
129
135
  meetingLink: () => meetingLink,
136
+ meetingPath: () => meetingPath,
130
137
  meetingSlug: () => meetingSlug,
131
138
  meetingTopic: () => meetingTopic,
132
139
  meetingWhen: () => meetingWhen,
@@ -143,9 +150,15 @@ __export(src_exports, {
143
150
  parseRoomToken: () => parseRoomToken,
144
151
  participantSummary: () => participantSummary,
145
152
  playableRecordingFileId: () => playableRecordingFileId,
153
+ projectInvitee: () => projectInvitee,
146
154
  projectNote: () => projectNote,
155
+ projectOccurrence: () => projectOccurrence,
156
+ projectOverride: () => projectOverride,
147
157
  projectParticipant: () => projectParticipant,
158
+ projectRsvpInvitation: () => projectRsvpInvitation,
148
159
  projectSegment: () => projectSegment,
160
+ projectUpcoming: () => projectUpcoming,
161
+ pushMeetingToGoogleCalendar: () => pushMeetingToGoogleCalendar,
149
162
  readServerRefusal: () => readServerRefusal,
150
163
  readableTranscript: () => readableTranscript,
151
164
  recordingLabel: () => recordingLabel,
@@ -153,6 +166,7 @@ __export(src_exports, {
153
166
  roleOf: () => roleOf,
154
167
  roomTopic: () => roomTopic,
155
168
  setGuestRecordAccess: () => setGuestRecordAccess,
169
+ settingsPatch: () => settingsPatch,
156
170
  snapshotParticipant: () => snapshotParticipant,
157
171
  unavailableFrom: () => unavailableFrom,
158
172
  userTopic: () => userTopic,
@@ -187,7 +201,7 @@ var MeetError = class extends Error {
187
201
  };
188
202
  var FORBIDDEN_CODES = /* @__PURE__ */ new Set(["42501", "PGRST301", "PGRST302"]);
189
203
  var NOT_FOUND_CODES = /* @__PURE__ */ new Set(["PGRST116", "PGRST205"]);
190
- var CONFLICT_CODES = /* @__PURE__ */ new Set(["23505"]);
204
+ var CONFLICT_CODES = /* @__PURE__ */ new Set(["23505", "40001", "55000"]);
191
205
  var SESSION_MARKERS = [
192
206
  "auth session missing",
193
207
  "jwt expired",
@@ -345,8 +359,28 @@ var MEET_ROUTES = {
345
359
  */
346
360
  record: "/api/v1/meet/record",
347
361
  /** A host chooses what guests may read of ONE meeting (MRF-1). */
348
- recordAccess: "/api/v1/meet/record/access"
362
+ recordAccess: "/api/v1/meet/record/access",
363
+ /**
364
+ * Meet wave 1 (0.8): bring every invitee up to date after a write — sends the
365
+ * invitation / update / cancellation emails with their `.ics` and re-queues
366
+ * reminders. Idempotent. `{meeting_id}` is substituted by `meetingPath`.
367
+ */
368
+ meetingAnnounce: "/api/v1/meet/meetings/{meeting_id}/announce",
369
+ /** Put a one-off meeting on the caller's own Google Calendar (gated by the server). */
370
+ meetingGoogleCalendar: "/api/v1/meet/meetings/{meeting_id}/google-calendar",
371
+ /**
372
+ * Meet wave 2: EVERY in-meeting host power (mute, stop video, mute all, ask to
373
+ * unmute, spotlight, lower hands, co-host, hand over, rename, remove, report).
374
+ * Decided by the server from the meeting itself; a guest's pass rides
375
+ * `x-meet-room-token`.
376
+ */
377
+ moderate: "/api/v1/meet/moderate",
378
+ /** Meet wave 2: the SAVED chat — `POST` sends to everyone, `GET ?meeting_id=` reads history. */
379
+ chat: "/api/v1/meet/chat"
349
380
  };
381
+ function meetingPath(template, meetingId) {
382
+ return template.replace("{meeting_id}", encodeURIComponent(meetingId));
383
+ }
350
384
  var ROOM_TOKEN_HEADER = "x-meet-room-token";
351
385
  var REFRESH_RATIO = 0.8;
352
386
  function readString(source, key) {
@@ -476,6 +510,19 @@ function createTokenClient(options) {
476
510
  "The link is wrong or the meeting was deleted. Show the 'meeting not found' screen; a durable link that 404s is never a retry."
477
511
  );
478
512
  }
513
+ if (response.status === 410) {
514
+ let reason = "This meeting was cancelled.";
515
+ try {
516
+ const body = await response.json();
517
+ if (typeof body?.detail?.message === "string") reason = body.detail.message;
518
+ } catch {
519
+ }
520
+ throw new MeetError(
521
+ "cancelled",
522
+ reason,
523
+ "Nobody can join a cancelled meeting. Show the cancelled screen with this sentence; ask the host to schedule a new one."
524
+ );
525
+ }
479
526
  if (response.status === 423) {
480
527
  throw new MeetError(
481
528
  "room-locked",
@@ -677,12 +724,12 @@ function createMeetAi(options) {
677
724
  async function dispatch(route, body, operation) {
678
725
  const response = await send({ path: route, body, operation, requireAuth: true });
679
726
  if (!response.ok) {
680
- const refusal = await refusalFor(response, operation);
681
- options.store.patch({ noteTakerRefusal: refusal });
727
+ const refusal2 = await refusalFor(response, operation);
728
+ options.store.patch({ noteTakerRefusal: refusal2 });
682
729
  throw new MeetError(
683
730
  response.status === 403 ? "forbidden" : "transport",
684
- refusal.reason,
685
- refusal.remedy
731
+ refusal2.reason,
732
+ refusal2.remedy
686
733
  );
687
734
  }
688
735
  options.store.patch({ noteTakerRefusal: null });
@@ -720,11 +767,11 @@ function createMeetAi(options) {
720
767
  ...signal !== void 0 ? { signal } : {}
721
768
  });
722
769
  if (!response.ok) {
723
- const refusal = await refusalFor(response, "meet.ai.ask");
770
+ const refusal2 = await refusalFor(response, "meet.ai.ask");
724
771
  throw new MeetError(
725
772
  response.status === 403 || response.status === 401 ? "forbidden" : response.status === 404 ? "not-found" : "transport",
726
- refusal.reason,
727
- refusal.remedy
773
+ refusal2.reason,
774
+ refusal2.remedy
728
775
  );
729
776
  }
730
777
  const body = response.body;
@@ -833,14 +880,14 @@ function noteTakerStatus(args) {
833
880
  const row = typeof raw === "object" && raw !== null && !Array.isArray(raw) ? raw : {};
834
881
  const recorded = readState(row["state"]);
835
882
  const detail = typeof row["reason"] === "string" && row["reason"].length > 0 ? row["reason"] : null;
836
- const refusal = args.refusal ?? null;
883
+ const refusal2 = args.refusal ?? null;
837
884
  if (recorded === null) {
838
- return { state: "absent", detail, present, refusal };
885
+ return { state: "absent", detail, present, refusal: refusal2 };
839
886
  }
840
887
  if (recorded === "transcribing" && !present) {
841
- return { state: "handoff_pending", detail, present, refusal };
888
+ return { state: "handoff_pending", detail, present, refusal: refusal2 };
842
889
  }
843
- return { state: recorded, detail, present, refusal };
890
+ return { state: recorded, detail, present, refusal: refusal2 };
844
891
  }
845
892
  function noteTakerLabel(status) {
846
893
  switch (status.state) {
@@ -1253,7 +1300,11 @@ var TABLES = {
1253
1300
  invites: "meet_call_invites",
1254
1301
  recordings: "meet_recordings",
1255
1302
  transcripts: "meet_transcript_segments",
1256
- notes: "meet_notes"
1303
+ notes: "meet_notes",
1304
+ /** Wave 1 (0.8): who a meeting was sent to, and what they answered. */
1305
+ invitees: "meet_invitees",
1306
+ /** Wave 1 (0.8): one occurrence of a series cancelled or moved. */
1307
+ occurrenceOverrides: "meet_occurrence_overrides"
1257
1308
  };
1258
1309
  var RPCS = {
1259
1310
  /** Atomic: create the invite row and stamp its expiry in one statement. */
@@ -1269,10 +1320,41 @@ var RPCS = {
1269
1320
  admit: "meet_admit_participant",
1270
1321
  setLock: "meet_set_lock",
1271
1322
  endMeeting: "meet_end_meeting",
1272
- recordConsent: "meet_record_consent"
1323
+ recordConsent: "meet_record_consent",
1324
+ /*
1325
+ * ── MEET WAVE 1 — management (0.8). Every write is a SECURITY DEFINER door
1326
+ * authorized by the meeting's host or a real iam grant (editor to change,
1327
+ * admin = co-host to archive or name a co-host) — never an organization
1328
+ * match. After any of these writes, call `announceMeeting` so the server
1329
+ * sends invitations / updates / cancellations and re-queues reminders.
1330
+ */
1331
+ /** Schedule with every setting at create; unset settings come from the knobs. */
1332
+ scheduleMeeting: "meet_schedule_meeting",
1333
+ /** Edit / reschedule — a JSON patch with a version compare-and-swap. */
1334
+ updateMeeting: "meet_update_meeting",
1335
+ /** Cancel is a state (`cancelled_at`); the link keeps resolving. */
1336
+ cancelMeeting: "meet_cancel_meeting",
1337
+ /** Archive (soft delete) — the invitations and exceptions cascade. */
1338
+ archiveMeeting: "meet_archive_meeting",
1339
+ restoreMeeting: "meet_restore_meeting",
1340
+ /** Cancel, move or restore ONE occurrence of a series by its original start. */
1341
+ setOccurrence: "meet_set_occurrence",
1342
+ addInvitees: "meet_add_invitees",
1343
+ setInviteeRole: "meet_set_invitee_role",
1344
+ removeInvitee: "meet_remove_invitee",
1345
+ /** A signed-in invitee answers for themselves. */
1346
+ respond: "meet_respond",
1347
+ /** The emailed RSVP link (no account): read and answer. */
1348
+ invitationByToken: "meet_invitation_by_token",
1349
+ respondByToken: "meet_respond_by_token",
1350
+ /** One meeting's occurrences between two instants (INVOKER — the reader's RLS). */
1351
+ meetingOccurrences: "meet_meeting_occurrences",
1352
+ /** Every occurrence the reader can see between two instants — the Upcoming list. */
1353
+ occurrencesBetween: "meet_occurrences_between"
1273
1354
  };
1274
1355
  var VOID_RPCS = [RPCS.admit, RPCS.recordConsent];
1275
- var MEETING_COLUMNS = "id,organization_id,room_name,slug,title,kind,host_user_id,scheduled_for,scheduled_duration_minutes,started_at,ended_at,locked,lobby_enabled,recording_policy,ai_enabled,metadata";
1356
+ var MEETING_COLUMNS = "id,organization_id,room_name,slug,title,kind,host_user_id,scheduled_for,scheduled_duration_minutes,started_at,ended_at,locked,lobby_enabled,recording_policy,ai_enabled,metadata,time_zone,agenda,recurrence_rule,join_before_host,cancelled_at,cancellation_reason,deleted_at,calendar_sequence,version";
1357
+ var INVITEE_COLUMNS = "id,meeting_id,invitee_user_id,email,display_name,role,rsvp_state,responded_at,rsvp_note,last_sent_sequence,last_sent_at";
1276
1358
  var RECORDING_COLUMNS = "id,meeting_id,state,started_at,ended_at,started_by,file_id,failure_reason";
1277
1359
  var SEGMENT_COLUMNS = "id,meeting_id,identity,speaker,text,started_at,is_final,metadata";
1278
1360
  var NOTE_COLUMNS = "id,meeting_id,text,kind,assignee_user_id,created_at,metadata";
@@ -1370,6 +1452,138 @@ function projectNote(row) {
1370
1452
  source: "record"
1371
1453
  };
1372
1454
  }
1455
+ var RSVP_STATES = ["needs_action", "accepted", "declined", "tentative"];
1456
+ var OCCURRENCE_STATES = ["scheduled", "moved", "cancelled"];
1457
+ function rsvpState(value) {
1458
+ return RSVP_STATES.includes(value) ? value : "needs_action";
1459
+ }
1460
+ function num(row, key) {
1461
+ const value = row[key];
1462
+ return typeof value === "number" ? value : null;
1463
+ }
1464
+ function when(row, key) {
1465
+ const value = row[key];
1466
+ if (value instanceof Date) return value.toISOString();
1467
+ return text(row, key);
1468
+ }
1469
+ function projectInvitee(row) {
1470
+ const operation = "meet.projectInvitee";
1471
+ const userId = text(row, "invitee_user_id");
1472
+ return {
1473
+ id: requireText(row, "id", operation),
1474
+ meetingId: asMeetingId(requireText(row, "meeting_id", operation)),
1475
+ userId: userId === null ? null : asUserId(userId),
1476
+ email: text(row, "email"),
1477
+ displayName: text(row, "display_name"),
1478
+ role: text(row, "role") === "cohost" ? "cohost" : "invitee",
1479
+ rsvpState: rsvpState(text(row, "rsvp_state")),
1480
+ respondedAt: when(row, "responded_at"),
1481
+ rsvpNote: text(row, "rsvp_note"),
1482
+ lastSentSequence: num(row, "last_sent_sequence"),
1483
+ lastSentAt: when(row, "last_sent_at")
1484
+ };
1485
+ }
1486
+ function projectOccurrence(row) {
1487
+ const operation = "meet.projectOccurrence";
1488
+ const state = text(row, "state");
1489
+ return {
1490
+ meetingId: asMeetingId(requireText(row, "meeting_id", operation)),
1491
+ originalStart: when(row, "original_start") ?? requireText(row, "original_start", operation),
1492
+ occurrenceStart: when(row, "occurrence_start") ?? requireText(row, "occurrence_start", operation),
1493
+ durationMinutes: num(row, "duration_minutes"),
1494
+ // An unknown state is shown as scheduled, never dropped: the row is real.
1495
+ state: OCCURRENCE_STATES.includes(state) ? state : "scheduled",
1496
+ sequence: num(row, "sequence") ?? 0
1497
+ };
1498
+ }
1499
+ function projectUpcoming(row) {
1500
+ const operation = "meet.projectUpcoming";
1501
+ const kind = text(row, "kind");
1502
+ const myRole = text(row, "my_role");
1503
+ const myRsvp = text(row, "my_rsvp");
1504
+ return {
1505
+ ...projectOccurrence(row),
1506
+ organizationId: asOrganizationId(requireText(row, "organization_id", operation)),
1507
+ title: text(row, "title") ?? "Meeting",
1508
+ slug: requireText(row, "slug", operation),
1509
+ kind: kind === "scheduled" || kind === "recurring" ? kind : "instant",
1510
+ hostUserId: asUserId(requireText(row, "host_user_id", operation)),
1511
+ timeZone: text(row, "time_zone") ?? "UTC",
1512
+ meetingCancelled: bool(row, "meeting_cancelled", false),
1513
+ myRsvp: myRsvp === null ? null : rsvpState(myRsvp),
1514
+ myRole: myRole === "host" || myRole === "cohost" || myRole === "invitee" ? myRole : null
1515
+ };
1516
+ }
1517
+ function projectOverride(row) {
1518
+ const operation = "meet.projectOverride";
1519
+ return {
1520
+ id: requireText(row, "id", operation),
1521
+ meetingId: asMeetingId(requireText(row, "meeting_id", operation)),
1522
+ originalStart: when(row, "original_start") ?? requireText(row, "original_start", operation),
1523
+ state: text(row, "state") === "moved" ? "moved" : "cancelled",
1524
+ newStart: when(row, "new_start"),
1525
+ newDurationMinutes: num(row, "new_duration_minutes"),
1526
+ reason: text(row, "reason"),
1527
+ calendarSequence: num(row, "calendar_sequence") ?? 0,
1528
+ deletedAt: when(row, "deleted_at")
1529
+ };
1530
+ }
1531
+ function projectRsvpInvitation(body) {
1532
+ const operation = "meet.projectRsvpInvitation";
1533
+ if (typeof body !== "object" || body === null) {
1534
+ throw invalidResponse(operation, "the RSVP door returned no object");
1535
+ }
1536
+ const row = body;
1537
+ if (row["ok"] !== true) {
1538
+ return {
1539
+ ok: false,
1540
+ message: text(row, "message") ?? "This invitation link is no longer valid \u2014 ask the host to send it again."
1541
+ };
1542
+ }
1543
+ const meeting = row["meeting"] ?? {};
1544
+ const invitee = row["invitee"] ?? {};
1545
+ return {
1546
+ ok: true,
1547
+ meeting: {
1548
+ title: text(meeting, "title") ?? "Meeting",
1549
+ slug: requireText(meeting, "slug", operation),
1550
+ scheduledFor: when(meeting, "scheduled_for"),
1551
+ durationMinutes: num(meeting, "duration_minutes"),
1552
+ timeZone: text(meeting, "time_zone") ?? "UTC",
1553
+ agenda: text(meeting, "agenda"),
1554
+ recurrenceRule: text(meeting, "recurrence_rule"),
1555
+ cancelledAt: when(meeting, "cancelled_at"),
1556
+ cancellationReason: text(meeting, "cancellation_reason"),
1557
+ endedAt: when(meeting, "ended_at")
1558
+ },
1559
+ invitee: {
1560
+ displayName: text(invitee, "display_name"),
1561
+ email: text(invitee, "email"),
1562
+ rsvpState: rsvpState(text(invitee, "rsvp_state")),
1563
+ respondedAt: when(invitee, "responded_at")
1564
+ }
1565
+ };
1566
+ }
1567
+ function settingsPatch(settings) {
1568
+ const patch = {};
1569
+ if (settings.lobbyEnabled !== void 0) patch["lobby_enabled"] = settings.lobbyEnabled;
1570
+ if (settings.recordingPolicy !== void 0) patch["recording_policy"] = settings.recordingPolicy;
1571
+ if (settings.aiEnabled !== void 0) patch["ai_enabled"] = settings.aiEnabled;
1572
+ if (settings.joinBeforeHost !== void 0) patch["join_before_host"] = settings.joinBeforeHost;
1573
+ return patch;
1574
+ }
1575
+ function changesPatch(changes) {
1576
+ const patch = settingsPatch(changes);
1577
+ if (changes.title !== void 0) patch["title"] = changes.title;
1578
+ if (changes.agenda !== void 0) patch["agenda"] = changes.agenda;
1579
+ if (changes.timeZone !== void 0) patch["time_zone"] = changes.timeZone;
1580
+ if (changes.scheduledFor !== void 0) patch["scheduled_for"] = changes.scheduledFor;
1581
+ if (changes.scheduledDurationMinutes !== void 0) {
1582
+ patch["scheduled_duration_minutes"] = changes.scheduledDurationMinutes;
1583
+ }
1584
+ if (changes.recurrenceRule !== void 0) patch["recurrence_rule"] = changes.recurrenceRule;
1585
+ return patch;
1586
+ }
1373
1587
  function createMeetRepository(options) {
1374
1588
  const db = options.client.schema(MEET_SCHEMA);
1375
1589
  async function guardVoid(operation, run) {
@@ -1439,7 +1653,16 @@ function createMeetRepository(options) {
1439
1653
  lobbyEnabled: bool(row, "lobby_enabled", true),
1440
1654
  recordingPolicy: policy === "disabled" || policy === "always-on" ? policy : "host-controlled",
1441
1655
  aiEnabled: bool(row, "ai_enabled", true),
1442
- metadata: typeof metadata === "object" && metadata !== null && !Array.isArray(metadata) ? metadata : {}
1656
+ metadata: typeof metadata === "object" && metadata !== null && !Array.isArray(metadata) ? metadata : {},
1657
+ timeZone: text(row, "time_zone") ?? "UTC",
1658
+ agenda: text(row, "agenda"),
1659
+ recurrenceRule: text(row, "recurrence_rule"),
1660
+ joinBeforeHost: bool(row, "join_before_host", true),
1661
+ cancelledAt: text(row, "cancelled_at"),
1662
+ cancellationReason: text(row, "cancellation_reason"),
1663
+ deletedAt: text(row, "deleted_at"),
1664
+ calendarSequence: typeof row["calendar_sequence"] === "number" ? row["calendar_sequence"] : 0,
1665
+ version: typeof row["version"] === "number" ? row["version"] : 1
1443
1666
  };
1444
1667
  }
1445
1668
  function projectRecording(row) {
@@ -1526,6 +1749,170 @@ function createMeetRepository(options) {
1526
1749
  projectNote,
1527
1750
  projectRecording,
1528
1751
  projectAttendee,
1752
+ projectInvitee,
1753
+ async scheduleMeeting(input) {
1754
+ requireOrg(input.organizationId, "meet.scheduleMeeting");
1755
+ const row = await guard(
1756
+ "meet.scheduleMeeting",
1757
+ () => db.rpc(RPCS.scheduleMeeting, {
1758
+ p_organization_id: input.organizationId,
1759
+ p_host_user_id: input.hostUserId,
1760
+ p_title: input.title,
1761
+ p_scheduled_for: input.scheduledFor,
1762
+ p_time_zone: input.timeZone,
1763
+ p_duration_minutes: input.durationMinutes ?? null,
1764
+ p_agenda: input.agenda ?? null,
1765
+ p_recurrence_rule: input.recurrenceRule ?? null,
1766
+ p_settings: settingsPatch(input.settings ?? {})
1767
+ })
1768
+ );
1769
+ return projectMeeting(row);
1770
+ },
1771
+ async updateMeeting(meetingId, changes, options2 = {}) {
1772
+ const row = await guard(
1773
+ "meet.updateMeeting",
1774
+ () => db.rpc(RPCS.updateMeeting, {
1775
+ p_meeting_id: meetingId,
1776
+ p_changes: changesPatch(changes),
1777
+ p_expected_version: options2.expectedVersion ?? null,
1778
+ p_by_user_id: options2.byUserId ?? null
1779
+ })
1780
+ );
1781
+ return projectMeeting(row);
1782
+ },
1783
+ async cancelMeeting(meetingId, reason, byUserId = null) {
1784
+ const row = await guard(
1785
+ "meet.cancelMeeting",
1786
+ () => db.rpc(RPCS.cancelMeeting, {
1787
+ p_meeting_id: meetingId,
1788
+ p_reason: reason,
1789
+ p_by_user_id: byUserId
1790
+ })
1791
+ );
1792
+ return projectMeeting(row);
1793
+ },
1794
+ async archiveMeeting(meetingId, byUserId = null) {
1795
+ const row = await guard(
1796
+ "meet.archiveMeeting",
1797
+ () => db.rpc(RPCS.archiveMeeting, { p_meeting_id: meetingId, p_by_user_id: byUserId })
1798
+ );
1799
+ return projectMeeting(row);
1800
+ },
1801
+ async restoreMeeting(meetingId, byUserId = null) {
1802
+ const row = await guard(
1803
+ "meet.restoreMeeting",
1804
+ () => db.rpc(RPCS.restoreMeeting, { p_meeting_id: meetingId, p_by_user_id: byUserId })
1805
+ );
1806
+ return projectMeeting(row);
1807
+ },
1808
+ async setOccurrence(args) {
1809
+ const row = await guard(
1810
+ "meet.setOccurrence",
1811
+ () => db.rpc(RPCS.setOccurrence, {
1812
+ p_meeting_id: args.meetingId,
1813
+ p_original_start: args.originalStart,
1814
+ p_action: args.action,
1815
+ p_new_start: args.newStart ?? null,
1816
+ p_new_duration_minutes: args.newDurationMinutes ?? null,
1817
+ p_reason: args.reason ?? null,
1818
+ p_by_user_id: args.byUserId ?? null
1819
+ })
1820
+ );
1821
+ return projectOverride(row);
1822
+ },
1823
+ async meetingOccurrences(meetingId, { from, to, limit = 200 }) {
1824
+ const rows = await guard(
1825
+ "meet.meetingOccurrences",
1826
+ () => db.rpc(RPCS.meetingOccurrences, {
1827
+ p_meeting_id: meetingId,
1828
+ p_from: from,
1829
+ p_to: to,
1830
+ p_limit: limit
1831
+ })
1832
+ );
1833
+ return rows.map(projectOccurrence);
1834
+ },
1835
+ async occurrencesBetween({ from, to, organizationId = null, limit = 500 }) {
1836
+ const rows = await guard(
1837
+ "meet.occurrencesBetween",
1838
+ () => db.rpc(RPCS.occurrencesBetween, {
1839
+ p_from: from,
1840
+ p_to: to,
1841
+ p_organization_id: organizationId,
1842
+ p_limit: limit
1843
+ })
1844
+ );
1845
+ return rows.map(projectUpcoming);
1846
+ },
1847
+ async invitees(meetingId) {
1848
+ const response = await db.from(TABLES.invitees).select(INVITEE_COLUMNS).eq("meeting_id", meetingId).is("deleted_at", null).order("created_at", { ascending: true }).order("id", { ascending: true });
1849
+ if (response.error !== null) throw normalizeMeetError(response.error, "meet.invitees");
1850
+ return (response.data ?? []).map(projectInvitee);
1851
+ },
1852
+ async addInvitees(meetingId, people, byUserId = null) {
1853
+ const rows = await guard(
1854
+ "meet.addInvitees",
1855
+ () => db.rpc(RPCS.addInvitees, {
1856
+ p_meeting_id: meetingId,
1857
+ p_invitees: people.map((person) => ({
1858
+ ...person.userId !== void 0 ? { user_id: person.userId } : {},
1859
+ ...person.email !== void 0 ? { email: person.email } : {},
1860
+ ...person.displayName !== void 0 ? { display_name: person.displayName } : {},
1861
+ ...person.role !== void 0 ? { role: person.role } : {}
1862
+ })),
1863
+ p_by_user_id: byUserId
1864
+ })
1865
+ );
1866
+ return rows.map(projectInvitee);
1867
+ },
1868
+ async setInviteeRole(inviteeId, role, byUserId = null) {
1869
+ const row = await guard(
1870
+ "meet.setInviteeRole",
1871
+ () => db.rpc(RPCS.setInviteeRole, {
1872
+ p_invitee_id: inviteeId,
1873
+ p_role: role,
1874
+ p_by_user_id: byUserId
1875
+ })
1876
+ );
1877
+ return projectInvitee(row);
1878
+ },
1879
+ async removeInvitee(inviteeId, byUserId = null) {
1880
+ const row = await guard(
1881
+ "meet.removeInvitee",
1882
+ () => db.rpc(RPCS.removeInvitee, { p_invitee_id: inviteeId, p_by_user_id: byUserId })
1883
+ );
1884
+ return projectInvitee(row);
1885
+ },
1886
+ async respond(meetingId, answer, note = null) {
1887
+ const row = await guard(
1888
+ "meet.respond",
1889
+ () => db.rpc(RPCS.respond, {
1890
+ p_meeting_id: meetingId,
1891
+ p_response: answer,
1892
+ p_note: note,
1893
+ p_by_user_id: null
1894
+ })
1895
+ );
1896
+ return projectInvitee(row);
1897
+ },
1898
+ async invitationByToken(secret) {
1899
+ const body = await guard(
1900
+ "meet.invitationByToken",
1901
+ () => db.rpc(RPCS.invitationByToken, { p_secret: secret })
1902
+ );
1903
+ return projectRsvpInvitation(body);
1904
+ },
1905
+ async respondByToken(secret, answer, note = null) {
1906
+ const body = await guard(
1907
+ "meet.respondByToken",
1908
+ () => db.rpc(RPCS.respondByToken, {
1909
+ p_secret: secret,
1910
+ p_response: answer,
1911
+ p_note: note
1912
+ })
1913
+ );
1914
+ return projectRsvpInvitation(body);
1915
+ },
1529
1916
  async attendees(meetingId) {
1530
1917
  const response = await db.from(TABLES.participants).select(ATTENDEE_COLUMNS).eq("meeting_id", meetingId).order("joined_at", { ascending: true }).order("id", { ascending: true });
1531
1918
  if (response.error !== null) throw normalizeMeetError(response.error, "meet.attendees");
@@ -1707,7 +2094,19 @@ var ROOM_EVENTS = {
1707
2094
  * production 2026-09-09 the guest lane showed no indicator at all while the
1708
2095
  * host recorded (MRI-C8).
1709
2096
  */
1710
- recording: "matrx.meet.recording"
2097
+ recording: "matrx.meet.recording",
2098
+ /**
2099
+ * A chat message to everyone was SAVED (Meet wave 2). Payload
2100
+ * `{ message: { id, meeting_id, sender_identity, sender_name, body, created_at } }`.
2101
+ */
2102
+ chat: "matrx.meet.chat",
2103
+ /**
2104
+ * A host or co-host acted (Meet wave 2). Payload `{ meeting_id, kind, … }`:
2105
+ * `muted {source, by}` · `ask-unmute {by}` · `removed {by}` — addressed to the
2106
+ * person concerned; `lower-hands {identities: [...] | "*"}` · `role {identity,
2107
+ * role}` — to everyone.
2108
+ */
2109
+ moderation: "matrx.meet.moderation"
1711
2110
  };
1712
2111
  function isRoomEventTopic(topic) {
1713
2112
  return topic.startsWith(ROOM_EVENT_PREFIX);
@@ -1743,6 +2142,10 @@ function applyRoomEvent(args) {
1743
2142
  return applyEnded(store, payload);
1744
2143
  case ROOM_EVENTS.recording:
1745
2144
  return applyRecording(store, payload);
2145
+ case ROOM_EVENTS.chat:
2146
+ return applyChat(store, payload);
2147
+ case ROOM_EVENTS.moderation:
2148
+ return applyModeration(store, payload, now);
1746
2149
  default:
1747
2150
  return ignored(`${topic} is not known to this version of @ai-matrx/meet`);
1748
2151
  }
@@ -1834,6 +2237,101 @@ function applyEnded(store, payload) {
1834
2237
  store.patch({ meeting: { ...meeting, endedAt } });
1835
2238
  return HANDLED;
1836
2239
  }
2240
+ function chatFromRow(row) {
2241
+ const id = text2(row["id"]);
2242
+ const identity = text2(row["sender_identity"]);
2243
+ const body = text2(row["body"]);
2244
+ if (id === null || identity === null || body === null) return null;
2245
+ return {
2246
+ id,
2247
+ identity: asParticipantIdentity(identity),
2248
+ displayName: text2(row["sender_name"]) ?? "Participant",
2249
+ text: body,
2250
+ at: text2(row["created_at"]) ?? (/* @__PURE__ */ new Date()).toISOString(),
2251
+ // `false` only from a server whose chat table has not landed: the room heard
2252
+ // it, nobody will see it later — and the line says so.
2253
+ saved: row["saved"] !== false
2254
+ };
2255
+ }
2256
+ function applyChat(store, payload) {
2257
+ const row = object(payload["message"]);
2258
+ if (row === null) return ignored("a chat event with no `message` row");
2259
+ const meeting = store.snapshot().meeting;
2260
+ if (meeting !== null && text2(row["meeting_id"]) !== null && row["meeting_id"] !== meeting.id) {
2261
+ return ignored("a chat message for a different meeting");
2262
+ }
2263
+ const message = chatFromRow(row);
2264
+ if (message === null) return ignored("a chat row with no id, sender or body");
2265
+ store.addChat(message);
2266
+ return HANDLED;
2267
+ }
2268
+ function moderationText(kind, payload) {
2269
+ const by = text2(payload["by"]) ?? "The host";
2270
+ switch (kind) {
2271
+ case "muted": {
2272
+ const source = text2(payload["source"]) ?? "microphone";
2273
+ return source === "microphone" ? `${by} muted you.` : source === "camera" ? `${by} turned off your camera.` : `${by} stopped your screen share.`;
2274
+ }
2275
+ case "ask-unmute":
2276
+ return `${by} is asking you to unmute.`;
2277
+ case "removed":
2278
+ return `${by} removed you from this meeting.`;
2279
+ default:
2280
+ return null;
2281
+ }
2282
+ }
2283
+ function applyModeration(store, payload, now) {
2284
+ const snapshot = store.snapshot();
2285
+ const meetingId = text2(payload["meeting_id"]);
2286
+ if (snapshot.meeting !== null && meetingId !== null && meetingId !== snapshot.meeting.id) {
2287
+ return ignored("a moderation event for a different meeting");
2288
+ }
2289
+ const kind = text2(payload["kind"]);
2290
+ switch (kind) {
2291
+ case "lower-hands": {
2292
+ const raw = payload["identities"];
2293
+ const everyone = raw === "*";
2294
+ const named = new Set(
2295
+ Array.isArray(raw) ? raw.filter((value) => typeof value === "string") : []
2296
+ );
2297
+ for (const participant of snapshot.participants) {
2298
+ if (participant.handRaised && (everyone || named.has(participant.identity))) {
2299
+ store.setHand(participant.identity, false);
2300
+ }
2301
+ }
2302
+ return HANDLED;
2303
+ }
2304
+ case "role": {
2305
+ const identity = text2(payload["identity"]);
2306
+ const role = text2(payload["role"]);
2307
+ if (identity === null || role === null || !ROLES.includes(role)) {
2308
+ return ignored("a role event with no identity or an unknown role");
2309
+ }
2310
+ if (identity === snapshot.localIdentity) {
2311
+ store.patch({
2312
+ grants: { ...snapshot.grants, role },
2313
+ moderationNotice: {
2314
+ kind: "role",
2315
+ text: role === "host" ? "You are now the host." : role === "cohost" ? "You are now a co-host." : "You are no longer a co-host.",
2316
+ at: now()
2317
+ }
2318
+ });
2319
+ }
2320
+ return HANDLED;
2321
+ }
2322
+ case "muted":
2323
+ case "ask-unmute":
2324
+ case "removed": {
2325
+ const sentence = moderationText(kind, payload);
2326
+ if (sentence === null) return ignored("a moderation event with no sentence");
2327
+ store.patch({ moderationNotice: { kind, text: sentence, at: now() } });
2328
+ return HANDLED;
2329
+ }
2330
+ default:
2331
+ return ignored(`a moderation kind this version does not know: ${String(kind)}`);
2332
+ }
2333
+ }
2334
+ var ROLES = ["host", "cohost", "participant", "guest"];
1837
2335
 
1838
2336
  // src/core/devices.ts
1839
2337
  var KINDS = ["audioinput", "videoinput", "audiooutput"];
@@ -2035,8 +2533,8 @@ function callStateLabel(state, outgoing) {
2035
2533
  return "Call";
2036
2534
  }
2037
2535
  }
2038
- function countLabel(count, singular, plural = `${singular}s`) {
2039
- return `${count} ${count === 1 ? singular : plural}`;
2536
+ function countLabel(count2, singular, plural = `${singular}s`) {
2537
+ return `${count2} ${count2 === 1 ? singular : plural}`;
2040
2538
  }
2041
2539
 
2042
2540
  // src/core/invitations.ts
@@ -2166,7 +2664,8 @@ function computeStage(args) {
2166
2664
  const ordered = orderParticipants(args.participants, args.activeSpeakers);
2167
2665
  const sharer = ordered.find((participant) => participant.screenSharing) ?? null;
2168
2666
  const pinnedParticipant = args.pinned === null ? null : ordered.find((participant) => participant.identity === args.pinned) ?? null;
2169
- const feature = sharer !== null ? sharer : args.layout === "grid" ? null : pinnedParticipant ?? ordered[0] ?? null;
2667
+ const spotlit = args.spotlight == null ? null : ordered.find((participant) => participant.identity === args.spotlight) ?? null;
2668
+ const feature = sharer !== null ? sharer : pinnedParticipant !== null && args.layout !== "grid" ? pinnedParticipant : spotlit !== null ? spotlit : args.layout === "grid" ? null : ordered[0] ?? null;
2170
2669
  const rest = feature === null ? ordered : ordered.filter((participant) => participant.identity !== feature.identity);
2171
2670
  const pageCount = Math.max(1, Math.ceil(rest.length / pageSize));
2172
2671
  const page = Math.min(Math.max(0, args.page ?? 0), pageCount - 1);
@@ -2180,11 +2679,11 @@ function computeStage(args) {
2180
2679
  presenting: sharer !== null
2181
2680
  };
2182
2681
  }
2183
- function gridColumns(count) {
2184
- if (count <= 1) return 1;
2185
- if (count <= 4) return 2;
2186
- if (count <= 9) return 3;
2187
- if (count <= 16) return 4;
2682
+ function gridColumns(count2) {
2683
+ if (count2 <= 1) return 1;
2684
+ if (count2 <= 4) return 2;
2685
+ if (count2 <= 9) return 3;
2686
+ if (count2 <= 16) return 4;
2188
2687
  return 5;
2189
2688
  }
2190
2689
 
@@ -2192,8 +2691,7 @@ function gridColumns(count) {
2192
2691
  var DATA_TOPICS = {
2193
2692
  hand: "mx.hand",
2194
2693
  reaction: "mx.reaction",
2195
- chat: "mx.chat",
2196
- hostAction: "mx.host"
2694
+ chat: "mx.chat"
2197
2695
  };
2198
2696
  var TRANSCRIPTION_TOPIC = "lk.transcription";
2199
2697
  var TRANSCRIPTION_ATTRIBUTES = {
@@ -2254,11 +2752,48 @@ function snapshotParticipant(participant, isLocal) {
2254
2752
  screenSharing: participant.isScreenShareEnabled
2255
2753
  };
2256
2754
  }
2755
+ var DEFAULT_AUDIO_PROCESSING = {
2756
+ noiseSuppression: true,
2757
+ echoCancellation: true,
2758
+ autoGainControl: true
2759
+ };
2760
+ function videoEffectsSupported() {
2761
+ const scope = globalThis;
2762
+ if (typeof scope.OffscreenCanvas === "undefined" || scope.document === void 0) return false;
2763
+ try {
2764
+ return scope.document.createElement("canvas").getContext("webgl2") !== null;
2765
+ } catch {
2766
+ return false;
2767
+ }
2768
+ }
2769
+ async function applyVideoEffect(target, effect) {
2770
+ const { Track } = await import("livekit-client");
2771
+ const track = target.localParticipant.getTrackPublication(Track.Source.Camera)?.track;
2772
+ if (track === void 0) return;
2773
+ try {
2774
+ if (effect.kind === "none") {
2775
+ await track.stopProcessor();
2776
+ return;
2777
+ }
2778
+ const processors = await import("@livekit/track-processors");
2779
+ const processor = effect.kind === "blur" ? processors.BackgroundBlur(effect.radius ?? 12) : processors.VirtualBackground(effect.imageUrl);
2780
+ await track.setProcessor(processor);
2781
+ } catch (error) {
2782
+ throw new MeetError(
2783
+ "media-unsupported",
2784
+ "The background effect could not start on this camera",
2785
+ "Background effects need a browser with WebGL2 (current Chrome, Edge, Firefox or Safari). Your video continues without the effect.",
2786
+ error
2787
+ );
2788
+ }
2789
+ }
2257
2790
  var encoder = new TextEncoder();
2258
2791
  var decoder = new TextDecoder();
2259
2792
  function createLiveKitDriver(handlers, options = {}) {
2260
2793
  let room = null;
2261
2794
  let projectRoster = null;
2795
+ let audioCapture = DEFAULT_AUDIO_PROCESSING;
2796
+ let videoEffect = { kind: "none" };
2262
2797
  async function load() {
2263
2798
  if (room !== null) return room;
2264
2799
  let module2;
@@ -2287,7 +2822,9 @@ function createLiveKitDriver(handlers, options = {}) {
2287
2822
  handlers.participants([local, ...remotes]);
2288
2823
  };
2289
2824
  projectRoster = roster;
2290
- created.on(RoomEvent.ParticipantConnected, roster).on(RoomEvent.ParticipantDisconnected, roster).on(RoomEvent.TrackSubscribed, roster).on(RoomEvent.TrackUnsubscribed, roster).on(RoomEvent.TrackMuted, roster).on(RoomEvent.TrackUnmuted, roster).on(RoomEvent.LocalTrackPublished, roster).on(RoomEvent.LocalTrackUnpublished, roster).on(RoomEvent.ActiveSpeakersChanged, (speakers) => {
2825
+ created.on(RoomEvent.ParticipantConnected, roster).on(RoomEvent.ParticipantDisconnected, roster).on(RoomEvent.TrackSubscribed, roster).on(RoomEvent.TrackUnsubscribed, roster).on(RoomEvent.TrackMuted, roster).on(RoomEvent.TrackUnmuted, roster).on(RoomEvent.LocalTrackPublished, roster).on(RoomEvent.LocalTrackUnpublished, roster).on(RoomEvent.ParticipantNameChanged, roster).on(RoomEvent.ParticipantAttributesChanged, roster).on(RoomEvent.ParticipantMetadataChanged, roster).on(RoomEvent.RoomMetadataChanged, (metadata) => {
2826
+ handlers.roomMetadata?.(metadata ?? "");
2827
+ }).on(RoomEvent.ActiveSpeakersChanged, (speakers) => {
2291
2828
  handlers.activeSpeakers(speakers.map((speaker) => speaker.identity));
2292
2829
  }).on(RoomEvent.ConnectionQualityChanged, (quality, participant) => {
2293
2830
  handlers.quality(participant.identity, mapQuality(quality));
@@ -2364,6 +2901,7 @@ function createLiveKitDriver(handlers, options = {}) {
2364
2901
  throw normalizeDeviceError(error, "meet.connect");
2365
2902
  }
2366
2903
  projectRoster?.();
2904
+ handlers.roomMetadata?.(target.metadata ?? "");
2367
2905
  },
2368
2906
  async disconnect() {
2369
2907
  if (room === null) return;
@@ -2378,14 +2916,19 @@ function createLiveKitDriver(handlers, options = {}) {
2378
2916
  },
2379
2917
  async setMicrophoneEnabled(enabled) {
2380
2918
  try {
2381
- await requireRoom("setMicrophoneEnabled").localParticipant.setMicrophoneEnabled(enabled);
2919
+ await requireRoom("setMicrophoneEnabled").localParticipant.setMicrophoneEnabled(
2920
+ enabled,
2921
+ audioCapture
2922
+ );
2382
2923
  } catch (error) {
2383
2924
  throw normalizeDeviceError(error, "meet.setMicrophoneEnabled");
2384
2925
  }
2385
2926
  },
2386
2927
  async setCameraEnabled(enabled) {
2387
2928
  try {
2388
- await requireRoom("setCameraEnabled").localParticipant.setCameraEnabled(enabled);
2929
+ const target = requireRoom("setCameraEnabled");
2930
+ await target.localParticipant.setCameraEnabled(enabled);
2931
+ if (enabled && videoEffect.kind !== "none") await applyVideoEffect(target, videoEffect);
2389
2932
  } catch (error) {
2390
2933
  throw normalizeDeviceError(error, "meet.setCameraEnabled");
2391
2934
  }
@@ -2397,14 +2940,40 @@ function createLiveKitDriver(handlers, options = {}) {
2397
2940
  throw normalizeDeviceError(error, "meet.setScreenShareEnabled");
2398
2941
  }
2399
2942
  },
2400
- async publishData(topic, payload) {
2943
+ async publishData(topic, payload, to) {
2401
2944
  await requireRoom("publishData").localParticipant.publishData(
2402
2945
  // `reliable` is not optional for a control plane: a dropped
2403
2946
  // "hand lowered" leaves a hand up on every other screen forever.
2404
2947
  encoder.encode(payload),
2405
- { reliable: true, topic }
2948
+ to === void 0 || to.length === 0 ? { reliable: true, topic } : { reliable: true, topic, destinationIdentities: [...to] }
2406
2949
  );
2407
2950
  },
2951
+ async setAudioProcessing(processing) {
2952
+ audioCapture = { ...audioCapture, ...processing };
2953
+ const target = room;
2954
+ if (target === null) return;
2955
+ const { Track } = await import("livekit-client");
2956
+ const publication = target.localParticipant.getTrackPublication(Track.Source.Microphone);
2957
+ const track = publication?.track;
2958
+ if (track === void 0 || publication?.isMuted === true) return;
2959
+ try {
2960
+ await track.restartTrack({
2961
+ ...audioCapture,
2962
+ ...target.getActiveDevice("audioinput") !== void 0 ? { deviceId: target.getActiveDevice("audioinput") } : {}
2963
+ });
2964
+ } catch (error) {
2965
+ throw normalizeDeviceError(error, "meet.setAudioProcessing");
2966
+ }
2967
+ },
2968
+ async setVideoEffect(effect) {
2969
+ videoEffect = effect;
2970
+ const target = room;
2971
+ if (target === null) return;
2972
+ await applyVideoEffect(target, effect);
2973
+ },
2974
+ supportsVideoEffects() {
2975
+ return videoEffectsSupported();
2976
+ },
2408
2977
  attach(identity, kind, element) {
2409
2978
  const target = requireRoom("attach");
2410
2979
  const source = sources?.[kind];
@@ -2627,6 +3196,11 @@ function listenOnlyNotice(media) {
2627
3196
  }
2628
3197
 
2629
3198
  // src/core/store.ts
3199
+ var DEFAULT_EFFECTS = {
3200
+ noiseSuppression: true,
3201
+ background: "none",
3202
+ backgroundImage: null
3203
+ };
2630
3204
  var NO_GRANTS = {
2631
3205
  role: "participant",
2632
3206
  canRecord: false,
@@ -2649,7 +3223,12 @@ var EMPTY = {
2649
3223
  recordingSource: "local",
2650
3224
  recordingId: null,
2651
3225
  chat: [],
3226
+ chatLoaded: false,
2652
3227
  reactions: [],
3228
+ spotlight: null,
3229
+ moderationNotice: null,
3230
+ actionNotice: null,
3231
+ effects: DEFAULT_EFFECTS,
2653
3232
  transcript: [],
2654
3233
  notes: [],
2655
3234
  names: {},
@@ -2688,6 +3267,7 @@ function createMeetStore(initial = {}) {
2688
3267
  setParticipants(participants) {
2689
3268
  const names = learnNames(state.names, participants);
2690
3269
  const hands = new Map(state.participants.map((p) => [p.identity, p.handRaised]));
3270
+ const handTimes = new Map(state.participants.map((p) => [p.identity, p.handRaisedAt ?? null]));
2691
3271
  const quality = new Map(state.participants.map((p) => [p.identity, p.quality]));
2692
3272
  commit({
2693
3273
  ...state,
@@ -2701,6 +3281,7 @@ function createMeetStore(initial = {}) {
2701
3281
  // name book. `roster-names.test.ts` fails if a token ever gets past it.
2702
3282
  displayName: displayNameFor(names, participant.identity, participant.displayName),
2703
3283
  handRaised: hands.get(participant.identity) ?? participant.handRaised,
3284
+ handRaisedAt: handTimes.get(participant.identity) ?? participant.handRaisedAt ?? null,
2704
3285
  quality: participant.quality === "unknown" ? quality.get(participant.identity) ?? "unknown" : participant.quality
2705
3286
  }))
2706
3287
  });
@@ -2723,11 +3304,17 @@ function createMeetStore(initial = {}) {
2723
3304
  )
2724
3305
  });
2725
3306
  },
2726
- setHand(identity, raised) {
3307
+ setHand(identity, raised, at) {
2727
3308
  commit({
2728
3309
  ...state,
2729
3310
  participants: state.participants.map(
2730
- (participant) => participant.identity === identity ? { ...participant, handRaised: raised } : participant
3311
+ (participant) => participant.identity === identity ? {
3312
+ ...participant,
3313
+ handRaised: raised,
3314
+ // A hand that is ALREADY up keeps its place in the queue when
3315
+ // it is re-announced (to a late joiner): the first time wins.
3316
+ handRaisedAt: raised ? participant.handRaised && participant.handRaisedAt != null ? participant.handRaisedAt : at ?? Date.now() : null
3317
+ } : participant
2731
3318
  )
2732
3319
  });
2733
3320
  },
@@ -2737,6 +3324,31 @@ function createMeetStore(initial = {}) {
2737
3324
  const names = learnName(state.names, message.identity, message.displayName);
2738
3325
  commit({ ...state, chat: chat.slice(-CHAT_LIMIT), names });
2739
3326
  },
3327
+ loadChat(messages) {
3328
+ const known = new Set(state.chat.map((message) => message.id));
3329
+ const merged = [...messages.filter((message) => !known.has(message.id)), ...state.chat].sort(
3330
+ (a, b) => a.at < b.at ? -1 : a.at > b.at ? 1 : 0
3331
+ );
3332
+ const names = learnNames(
3333
+ state.names,
3334
+ messages.map((message) => ({ identity: message.identity, displayName: message.displayName }))
3335
+ );
3336
+ commit({ ...state, chat: merged.slice(-CHAT_LIMIT), chatLoaded: true, names });
3337
+ },
3338
+ settleChat(localId, next) {
3339
+ const index = state.chat.findIndex((message) => message.id === localId);
3340
+ if (index === -1) return;
3341
+ const current = state.chat[index];
3342
+ let replacement;
3343
+ if ("failed" in next && typeof next.failed === "string" && !("id" in next)) {
3344
+ replacement = { ...current, failed: next.failed };
3345
+ } else {
3346
+ replacement = next;
3347
+ }
3348
+ const duplicate = replacement.id !== localId && state.chat.some((message) => message.id === replacement.id);
3349
+ const chat = duplicate ? state.chat.filter((message) => message.id !== localId) : state.chat.map((message, position) => position === index ? replacement : message);
3350
+ commit({ ...state, chat });
3351
+ },
2740
3352
  addReaction(reaction) {
2741
3353
  commit({ ...state, reactions: [...state.reactions, reaction] });
2742
3354
  },
@@ -2827,7 +3439,7 @@ function createMeetStore(initial = {}) {
2827
3439
  commit({ ...state, media: { ...state.media, [kind]: next } });
2828
3440
  },
2829
3441
  reset() {
2830
- commit({ ...EMPTY });
3442
+ commit({ ...EMPTY, effects: state.effects });
2831
3443
  }
2832
3444
  };
2833
3445
  }
@@ -3157,11 +3769,11 @@ function createMeetingSession(options) {
3157
3769
  } catch {
3158
3770
  body = null;
3159
3771
  }
3160
- const refusal = readServerRefusal(body);
3772
+ const refusal2 = readServerRefusal(body);
3161
3773
  throw new MeetError(
3162
3774
  response.status === 403 ? "forbidden" : "transport",
3163
- refusal?.reason ?? `meet.endForEveryone: the server answered ${response.status}`,
3164
- refusal?.remedy !== void 0 && refusal.remedy.length > 0 ? refusal.remedy : "The meeting is still running. Only a host may end it, and the server re-checks that \u2014 being a host on this screen is not the decision."
3775
+ refusal2?.reason ?? `meet.endForEveryone: the server answered ${response.status}`,
3776
+ refusal2?.remedy !== void 0 && refusal2.remedy.length > 0 ? refusal2.remedy : "The meeting is still running. Only a host may end it, and the server re-checks that \u2014 being a host on this screen is not the decision."
3165
3777
  );
3166
3778
  }
3167
3779
  let payload = null;
@@ -3289,6 +3901,57 @@ function fileDownloadPath(fileId) {
3289
3901
  return `/files/${encodeURIComponent(fileId)}/download?inline=1`;
3290
3902
  }
3291
3903
 
3904
+ // src/core/management.ts
3905
+ async function refusal(response, operation) {
3906
+ let body = null;
3907
+ try {
3908
+ body = await response.json();
3909
+ } catch {
3910
+ body = null;
3911
+ }
3912
+ const said = readServerRefusal(body);
3913
+ return new MeetError(
3914
+ response.status === 403 || response.status === 401 ? "forbidden" : response.status === 404 ? "not-found" : response.status === 409 ? "conflict" : "transport",
3915
+ `${operation}: ${said?.reason ?? `the server answered ${response.status}`}`,
3916
+ said?.remedy || "The meeting itself is saved \u2014 only the telling failed. Try again; the call is idempotent."
3917
+ );
3918
+ }
3919
+ function count(body, key) {
3920
+ const value = body[key];
3921
+ return typeof value === "number" ? value : 0;
3922
+ }
3923
+ async function announceMeeting(api, meetingId) {
3924
+ const operation = "meet.announceMeeting";
3925
+ const response = await api.request({
3926
+ path: meetingPath(MEET_ROUTES.meetingAnnounce, meetingId),
3927
+ method: "POST",
3928
+ requireAuth: true,
3929
+ operation
3930
+ });
3931
+ if (!response.ok) throw await refusal(response, operation);
3932
+ const body = await response.json();
3933
+ return {
3934
+ invitations: count(body, "invitations"),
3935
+ updates: count(body, "updates"),
3936
+ cancellations: count(body, "cancellations"),
3937
+ alreadyCurrent: count(body, "already_current"),
3938
+ remindersQueued: count(body, "reminders_queued"),
3939
+ remindersCancelled: count(body, "reminders_cancelled")
3940
+ };
3941
+ }
3942
+ async function pushMeetingToGoogleCalendar(api, meetingId, target) {
3943
+ const operation = "meet.pushMeetingToGoogleCalendar";
3944
+ const response = await api.request({
3945
+ path: meetingPath(MEET_ROUTES.meetingGoogleCalendar, meetingId),
3946
+ method: "POST",
3947
+ requireAuth: true,
3948
+ operation,
3949
+ body: { connection_id: target.connectionId, calendar_id: target.calendarId ?? "primary" }
3950
+ });
3951
+ if (!response.ok) throw await refusal(response, operation);
3952
+ return await response.json();
3953
+ }
3954
+
3292
3955
  // src/core/room.ts
3293
3956
  var PARTICIPANT_ROLE_ATTRIBUTE = "matrx.participant.role";
3294
3957
  var PARTICIPANT_CAN_RECORD_ATTRIBUTE = "matrx.participant.can_record";
@@ -3408,7 +4071,37 @@ function createRoomEngine(options) {
3408
4071
  const handlers = {
3409
4072
  participants(list) {
3410
4073
  const joinedAt = new Date(now()).toISOString();
4074
+ const before = new Set(store.snapshot().participants.map((p) => p.identity));
3411
4075
  store.setParticipants(list.map((entry) => projectParticipant(entry, joinedAt)));
4076
+ const snapshot = store.snapshot();
4077
+ const newcomer = snapshot.participants.some(
4078
+ (participant) => !participant.isLocal && !before.has(participant.identity)
4079
+ );
4080
+ const me = snapshot.participants.find((p) => p.identity === snapshot.localIdentity);
4081
+ if (newcomer && before.size > 0 && me?.handRaised === true) {
4082
+ void publish(DATA_TOPICS.hand, { raised: true, raisedAt: me.handRaisedAt ?? now() }).catch(
4083
+ () => void 0
4084
+ );
4085
+ }
4086
+ },
4087
+ roomMetadata(metadata) {
4088
+ let spotlight = null;
4089
+ if (metadata.length > 0) {
4090
+ try {
4091
+ const parsed = JSON.parse(metadata);
4092
+ if (typeof parsed === "object" && parsed !== null) {
4093
+ const value = parsed.spotlight;
4094
+ if (typeof value === "string" && value.length > 0) spotlight = asParticipantIdentity(value);
4095
+ }
4096
+ } catch {
4097
+ report(
4098
+ "warn",
4099
+ "meet: the room's metadata is not JSON, so no spotlight was read from it",
4100
+ "Only aidream writes room metadata; check services/meet/moderation.py."
4101
+ );
4102
+ }
4103
+ }
4104
+ if (store.snapshot().spotlight !== spotlight) store.patch({ spotlight });
3412
4105
  },
3413
4106
  activeSpeakers(identities) {
3414
4107
  store.setSpeaking(identities.map(asParticipantIdentity));
@@ -3512,7 +4205,11 @@ function createRoomEngine(options) {
3512
4205
  const sender = asParticipantIdentity(from ?? "");
3513
4206
  switch (topic) {
3514
4207
  case DATA_TOPICS.hand:
3515
- store.setHand(sender, envelope.raised === true);
4208
+ store.setHand(
4209
+ sender,
4210
+ envelope.raised === true,
4211
+ typeof envelope.raisedAt === "number" ? envelope.raisedAt : now()
4212
+ );
3516
4213
  return;
3517
4214
  case DATA_TOPICS.reaction:
3518
4215
  if (typeof envelope.emoji === "string") {
@@ -3526,12 +4223,14 @@ function createRoomEngine(options) {
3526
4223
  return;
3527
4224
  case DATA_TOPICS.chat:
3528
4225
  if (typeof envelope.text === "string") {
4226
+ const local = store.snapshot().localIdentity;
3529
4227
  store.addChat({
3530
4228
  id: envelope.id ?? `${sender}-${now()}`,
3531
4229
  identity: sender,
3532
4230
  displayName: envelope.name ?? sender,
3533
4231
  text: envelope.text,
3534
- at: envelope.at ?? new Date(now()).toISOString()
4232
+ at: envelope.at ?? new Date(now()).toISOString(),
4233
+ ...typeof envelope.to === "string" && local !== null ? { to: local } : {}
3535
4234
  });
3536
4235
  }
3537
4236
  return;
@@ -3577,6 +4276,24 @@ function createRoomEngine(options) {
3577
4276
  store.patch({ phase: "connected", failure: null });
3578
4277
  store.setLocalTrack("microphone", IDLE_TRACK);
3579
4278
  store.setLocalTrack("camera", IDLE_TRACK);
4279
+ const effects = options.effectsStorage?.load() ?? store.snapshot().effects;
4280
+ store.patch({ effects });
4281
+ if (!effects.noiseSuppression || effects.background !== "none") {
4282
+ try {
4283
+ await applyEffects(created, effects);
4284
+ } catch {
4285
+ }
4286
+ }
4287
+ for (const kind of ["audioinput", "videoinput", "audiooutput"]) {
4288
+ const chosen = options.resolveDevice?.(kind) ?? null;
4289
+ if (chosen === null || chosen === "default") continue;
4290
+ try {
4291
+ await created.switchDevice(kind, chosen);
4292
+ } catch (error) {
4293
+ const normalized = normalizeDeviceError(error, `meet.switchDevice(${kind})`);
4294
+ report("warn", normalized.message, normalized.remedy);
4295
+ }
4296
+ }
3580
4297
  if (request.mode === "video") {
3581
4298
  await enableTrack(created, "camera", true);
3582
4299
  }
@@ -3599,9 +4316,30 @@ function createRoomEngine(options) {
3599
4316
  return state;
3600
4317
  }
3601
4318
  }
3602
- async function publish(topic, body) {
4319
+ async function publish(topic, body, to) {
3603
4320
  if (driver === null) return;
3604
- await driver.publishData(topic, JSON.stringify(body));
4321
+ await driver.publishData(topic, JSON.stringify(body), to);
4322
+ }
4323
+ async function applyEffects(target, effects) {
4324
+ try {
4325
+ await target.setAudioProcessing?.({
4326
+ noiseSuppression: effects.noiseSuppression,
4327
+ echoCancellation: true,
4328
+ autoGainControl: true
4329
+ });
4330
+ } catch (error) {
4331
+ report("warn", `meet: noise suppression could not be applied: ${String(error)}`, "The microphone keeps working without it.");
4332
+ }
4333
+ if (target.setVideoEffect === void 0) return;
4334
+ const effect = effects.background === "blur" ? { kind: "blur" } : effects.background === "image" && effects.backgroundImage !== null ? { kind: "image", imageUrl: effects.backgroundImage } : { kind: "none" };
4335
+ if (effect.kind !== "none" && target.supportsVideoEffects?.() === false) return;
4336
+ try {
4337
+ await target.setVideoEffect(effect);
4338
+ } catch (error) {
4339
+ const normalized = normalizeMeetError(error, "meet.setVideoEffect");
4340
+ report("warn", normalized.message, normalized.remedy);
4341
+ throw normalized;
4342
+ }
3605
4343
  }
3606
4344
  return {
3607
4345
  join(request) {
@@ -3657,7 +4395,7 @@ function createRoomEngine(options) {
3657
4395
  async raiseHand(raised) {
3658
4396
  const local = store.snapshot().localIdentity;
3659
4397
  if (local !== null) store.setHand(local, raised);
3660
- await publish(DATA_TOPICS.hand, { raised });
4398
+ await publish(DATA_TOPICS.hand, { raised, raisedAt: now() });
3661
4399
  },
3662
4400
  async react(emoji) {
3663
4401
  const local = store.snapshot().localIdentity;
@@ -3667,32 +4405,41 @@ function createRoomEngine(options) {
3667
4405
  }
3668
4406
  await publish(DATA_TOPICS.reaction, { id, emoji });
3669
4407
  },
3670
- async sendChat(text3) {
4408
+ async sendPrivateChat(to, text3) {
3671
4409
  const trimmed = text3.trim();
3672
4410
  if (trimmed.length === 0) return;
3673
4411
  const local = store.snapshot().localIdentity;
3674
4412
  const id = `${local ?? "local"}-${now()}`;
3675
4413
  const at = new Date(now()).toISOString();
3676
4414
  if (local !== null) {
3677
- store.addChat({
3678
- id,
3679
- identity: local,
3680
- displayName: identity.displayName,
3681
- text: trimmed,
3682
- at
3683
- });
4415
+ store.addChat({ id, identity: local, displayName: identity.displayName, text: trimmed, at, to });
3684
4416
  }
3685
- await publish(DATA_TOPICS.chat, { id, text: trimmed, at, name: identity.displayName });
4417
+ await publish(
4418
+ DATA_TOPICS.chat,
4419
+ { id, text: trimmed, at, name: identity.displayName, to },
4420
+ [to]
4421
+ );
3686
4422
  },
3687
- async publishHostAction(action) {
3688
- if (!isHostRole(effectiveRole(store.snapshot()))) {
3689
- throw new MeetError(
3690
- "forbidden",
3691
- `meet.hostAction(${action.kind}): this participant is not a host`,
3692
- "Host powers come from the server-minted token metadata. Hide the control instead of rendering a dead button (no dead ends)."
3693
- );
4423
+ async setEffects(next) {
4424
+ const previous = store.snapshot().effects;
4425
+ const merged = { ...previous, ...next };
4426
+ store.patch({ effects: merged });
4427
+ options.effectsStorage?.save(merged);
4428
+ if (driver === null) return;
4429
+ try {
4430
+ await applyEffects(driver, merged);
4431
+ } catch {
4432
+ const reverted = { ...merged, background: previous.background, backgroundImage: previous.backgroundImage };
4433
+ store.patch({ effects: reverted });
4434
+ options.effectsStorage?.save(reverted);
3694
4435
  }
3695
- await publish(DATA_TOPICS.hostAction, { ...action });
4436
+ },
4437
+ supportsVideoEffects() {
4438
+ return driver?.supportsVideoEffects?.() ?? false;
4439
+ },
4440
+ async switchDevice(kind, deviceId) {
4441
+ if (driver === null) return;
4442
+ await driver.switchDevice(kind, deviceId);
3696
4443
  },
3697
4444
  pin(who) {
3698
4445
  store.patch({ pinned: who, layout: who === null ? "grid" : "speaker" });
@@ -3723,6 +4470,192 @@ function hostUserIds(participants) {
3723
4470
  return participants.filter((participant) => participant.role === "host" && participant.userId !== null).map((participant) => participant.userId);
3724
4471
  }
3725
4472
 
4473
+ // src/core/effects.ts
4474
+ function coerce(value) {
4475
+ if (typeof value !== "object" || value === null) return DEFAULT_EFFECTS;
4476
+ const row = value;
4477
+ const background = row["background"] === "blur" || row["background"] === "image" ? row["background"] : "none";
4478
+ const backgroundImage = typeof row["backgroundImage"] === "string" ? row["backgroundImage"] : null;
4479
+ return {
4480
+ noiseSuppression: row["noiseSuppression"] !== false,
4481
+ background: background === "image" && backgroundImage === null ? "none" : background,
4482
+ backgroundImage
4483
+ };
4484
+ }
4485
+ function createWebEffectsStorage(key = "mx.meet.effects") {
4486
+ let memory = DEFAULT_EFFECTS;
4487
+ const storage = globalThis.localStorage;
4488
+ return {
4489
+ load() {
4490
+ if (storage === void 0) return memory;
4491
+ try {
4492
+ const raw = storage.getItem(key);
4493
+ return raw === null ? memory : coerce(JSON.parse(raw));
4494
+ } catch {
4495
+ return memory;
4496
+ }
4497
+ },
4498
+ save(effects) {
4499
+ memory = effects;
4500
+ try {
4501
+ storage?.setItem(key, JSON.stringify(effects));
4502
+ } catch {
4503
+ }
4504
+ }
4505
+ };
4506
+ }
4507
+
4508
+ // src/core/in-room.ts
4509
+ var MODERATION_ACTIONS = [
4510
+ "mute",
4511
+ "stop_video",
4512
+ "stop_screen",
4513
+ "mute_all",
4514
+ "ask_unmute",
4515
+ "ask_all_unmute",
4516
+ "spotlight",
4517
+ "lower_hand",
4518
+ "lower_all_hands",
4519
+ "make_cohost",
4520
+ "revoke_cohost",
4521
+ "transfer_host",
4522
+ "rename",
4523
+ "remove",
4524
+ "report"
4525
+ ];
4526
+ function createInRoomClient(options) {
4527
+ const { api, store } = options;
4528
+ const now = options.now ?? (() => Date.now());
4529
+ function meetingId(operation) {
4530
+ const meeting = store.snapshot().meeting;
4531
+ if (meeting === null) {
4532
+ throw new MeetError(
4533
+ "not-found",
4534
+ `${operation}: this room has no meeting record`,
4535
+ "Moderation and saved chat belong to a meeting. An ad-hoc call has neither."
4536
+ );
4537
+ }
4538
+ return meeting.id;
4539
+ }
4540
+ function headers() {
4541
+ const token = options.resolveRoomToken();
4542
+ return token === null ? void 0 : { [ROOM_TOKEN_HEADER]: token };
4543
+ }
4544
+ async function refusal2(response, operation, remedy) {
4545
+ let body = null;
4546
+ try {
4547
+ body = await response.json();
4548
+ } catch {
4549
+ body = null;
4550
+ }
4551
+ const said = readServerRefusal(body);
4552
+ return new MeetError(
4553
+ response.status === 403 || response.status === 401 ? "forbidden" : response.status === 404 ? "not-found" : response.status === 409 ? "conflict" : "transport",
4554
+ said?.reason ?? `${operation}: the server answered ${response.status}`,
4555
+ said?.remedy !== void 0 && said.remedy.length > 0 ? said.remedy : remedy
4556
+ );
4557
+ }
4558
+ return {
4559
+ async moderate(request) {
4560
+ const operation = `meet.moderate(${request.action})`;
4561
+ const hdrs = headers();
4562
+ const response = await api.request({
4563
+ path: MEET_ROUTES.moderate,
4564
+ method: "POST",
4565
+ operation,
4566
+ body: {
4567
+ meeting_id: meetingId(operation),
4568
+ action: request.action,
4569
+ ...request.identity != null ? { identity: request.identity } : {},
4570
+ ...request.name !== void 0 ? { name: request.name } : {},
4571
+ ...request.reason !== void 0 ? { reason: request.reason } : {}
4572
+ },
4573
+ ...hdrs !== void 0 ? { headers: hdrs } : {}
4574
+ });
4575
+ if (!response.ok) {
4576
+ throw await refusal2(
4577
+ response,
4578
+ operation,
4579
+ "Nothing was changed. The server decides host powers from the meeting itself."
4580
+ );
4581
+ }
4582
+ const payload = await response.json();
4583
+ return {
4584
+ action: request.action,
4585
+ affected: Array.isArray(payload["affected"]) ? payload["affected"].filter((value) => typeof value === "string") : [],
4586
+ message: typeof payload["message"] === "string" ? payload["message"] : "Done."
4587
+ };
4588
+ },
4589
+ async sendChat(text3) {
4590
+ const body = text3.trim();
4591
+ if (body.length === 0) return;
4592
+ const operation = "meet.sendChat";
4593
+ const id = meetingId(operation);
4594
+ const snapshot = store.snapshot();
4595
+ const me = snapshot.participants.find((p) => p.identity === snapshot.localIdentity);
4596
+ const localId = `pending-${now()}`;
4597
+ const pending = {
4598
+ id: localId,
4599
+ identity: snapshot.localIdentity ?? "local",
4600
+ displayName: me?.displayName ?? "You",
4601
+ text: body,
4602
+ at: new Date(now()).toISOString(),
4603
+ saved: false
4604
+ };
4605
+ store.addChat(pending);
4606
+ const hdrs = headers();
4607
+ let response;
4608
+ try {
4609
+ response = await api.request({
4610
+ path: MEET_ROUTES.chat,
4611
+ method: "POST",
4612
+ operation,
4613
+ body: { meeting_id: id, body },
4614
+ ...hdrs !== void 0 ? { headers: hdrs } : {}
4615
+ });
4616
+ } catch (error) {
4617
+ store.settleChat(localId, { failed: "Not sent \u2014 the server could not be reached." });
4618
+ throw error;
4619
+ }
4620
+ if (!response.ok) {
4621
+ const error = await refusal2(response, operation, "Your message was not sent.");
4622
+ store.settleChat(localId, { failed: error.message });
4623
+ throw error;
4624
+ }
4625
+ const saved = chatFromRow(await response.json());
4626
+ if (saved === null) {
4627
+ store.settleChat(localId, { failed: "The server's answer could not be read." });
4628
+ return;
4629
+ }
4630
+ store.settleChat(localId, saved);
4631
+ },
4632
+ async loadChat() {
4633
+ const operation = "meet.loadChat";
4634
+ const id = meetingId(operation);
4635
+ const hdrs = headers();
4636
+ const response = await api.request({
4637
+ path: `${MEET_ROUTES.chat}?meeting_id=${encodeURIComponent(id)}`,
4638
+ method: "GET",
4639
+ operation,
4640
+ ...hdrs !== void 0 ? { headers: hdrs } : {}
4641
+ });
4642
+ if (!response.ok) {
4643
+ throw await refusal2(
4644
+ response,
4645
+ operation,
4646
+ "Messages sent before you joined could not be read. New messages still arrive."
4647
+ );
4648
+ }
4649
+ const payload = await response.json();
4650
+ const rows = Array.isArray(payload.messages) ? payload.messages : [];
4651
+ const messages = rows.map(
4652
+ (row) => typeof row === "object" && row !== null ? chatFromRow(row) : null
4653
+ ).filter((message) => message !== null);
4654
+ store.loadChat(messages);
4655
+ }
4656
+ };
4657
+ }
4658
+
3726
4659
  // src/core/record.ts
3727
4660
  var import_format = require("@ai-matrx/kit/format");
3728
4661
  function activeWrapUpSummary(notes) {
@@ -3863,7 +4796,7 @@ function meetingWhen(meeting, locale) {
3863
4796
  if (start === null) return "Time not recorded";
3864
4797
  const startedAt = new Date(start);
3865
4798
  if (Number.isNaN(startedAt.getTime())) return "Time not recorded";
3866
- const when = startedAt.toLocaleString(locale, {
4799
+ const when2 = startedAt.toLocaleString(locale, {
3867
4800
  weekday: "long",
3868
4801
  year: "numeric",
3869
4802
  month: "long",
@@ -3871,11 +4804,11 @@ function meetingWhen(meeting, locale) {
3871
4804
  hour: "numeric",
3872
4805
  minute: "2-digit"
3873
4806
  });
3874
- if (meeting.endedAt === null || meeting.startedAt === null) return when;
4807
+ if (meeting.endedAt === null || meeting.startedAt === null) return when2;
3875
4808
  const endedAt = new Date(meeting.endedAt);
3876
- if (Number.isNaN(endedAt.getTime())) return when;
4809
+ if (Number.isNaN(endedAt.getTime())) return when2;
3877
4810
  const elapsedMs = endedAt.getTime() - startedAt.getTime();
3878
- return `${when} \xB7 ${(0, import_format.formatDurationMs)(elapsedMs, { style: "long" })}`;
4811
+ return `${when2} \xB7 ${(0, import_format.formatDurationMs)(elapsedMs, { style: "long" })}`;
3879
4812
  }
3880
4813
  var GUEST_RECORD_ACCESS_LEVELS = [
3881
4814
  "none",
@@ -3951,8 +4884,8 @@ async function loadGuestMeetingRecord(args) {
3951
4884
  );
3952
4885
  }
3953
4886
  if (!response.ok) {
3954
- const refusal = await readRefusal(response);
3955
- return refusedBundle(meeting, refusal.message, refusal.remedy);
4887
+ const refusal2 = await readRefusal(response);
4888
+ return refusedBundle(meeting, refusal2.message, refusal2.remedy);
3956
4889
  }
3957
4890
  const body = await response.json();
3958
4891
  const payload = typeof body["data"] === "object" && body["data"] !== null ? body["data"] : body;
@@ -4043,8 +4976,8 @@ async function setGuestRecordAccess(args) {
4043
4976
  }
4044
4977
  });
4045
4978
  if (!response.ok) {
4046
- const refusal = await readRefusal(response);
4047
- throw new MeetError("forbidden", refusal.message, refusal.remedy);
4979
+ const refusal2 = await readRefusal(response);
4980
+ throw new MeetError("forbidden", refusal2.message, refusal2.remedy);
4048
4981
  }
4049
4982
  const body = await response.json();
4050
4983
  const payload = typeof body["data"] === "object" && body["data"] !== null ? body["data"] : body;