@ai-matrx/meet 0.9.0 → 0.10.1

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
@@ -80,6 +80,7 @@ __export(src_exports, {
80
80
  activeWrapUpSummary: () => activeWrapUpSummary,
81
81
  activityAttention: () => activityAttention,
82
82
  announceMeeting: () => announceMeeting,
83
+ appPanelOf: () => appPanelOf,
83
84
  applyCollabPacket: () => applyCollabPacket,
84
85
  applyRoomEvent: () => applyRoomEvent,
85
86
  asCallId: () => asCallId,
@@ -131,6 +132,7 @@ __export(src_exports, {
131
132
  defaultCaptionLanguage: () => defaultCaptionLanguage,
132
133
  displayNameFor: () => displayNameFor,
133
134
  fileDownloadPath: () => fileDownloadPath,
135
+ focusFromRow: () => focusFromRow,
134
136
  gridColumns: () => gridColumns,
135
137
  groupTranscript: () => groupTranscript,
136
138
  guestRecordAccessExplanation: () => guestRecordAccessExplanation,
@@ -145,6 +147,7 @@ __export(src_exports, {
145
147
  isLinkFollower: () => isLinkFollower,
146
148
  isListenOnly: () => isListenOnly,
147
149
  isNoteTaker: () => isNoteTaker,
150
+ isObserverRole: () => isObserverRole,
148
151
  isRealName: () => isRealName,
149
152
  isRoomEventTopic: () => isRoomEventTopic,
150
153
  learnName: () => learnName,
@@ -209,6 +212,7 @@ __export(src_exports, {
209
212
  roleOf: () => roleOf,
210
213
  roomTopic: () => roomTopic,
211
214
  rootAttributes: () => rootAttributes,
215
+ scheduleSettingsPatch: () => scheduleSettingsPatch,
212
216
  searchTranscript: () => searchTranscript,
213
217
  sendFollowUp: () => sendFollowUp,
214
218
  setCaptionLanguage: () => setCaptionLanguage,
@@ -534,7 +538,17 @@ var MEET_ROUTES = {
534
538
  */
535
539
  collab: "/api/v1/meet/collab",
536
540
  /** Meet wave 5: translate finished caption lines into this viewer's language. */
537
- captionsTranslate: "/api/v1/meet/captions/translate"
541
+ captionsTranslate: "/api/v1/meet/captions/translate",
542
+ /**
543
+ * S4 — THE STATE READ (CORE-DESIGN §2.6): `GET ?meeting_id=` with `x-meet-pass`. This
544
+ * person's admission (lazy expiry applied), whether anyone who can admit is here, the run's
545
+ * phase. Read every `waiting_poll_seconds` while waiting and on every `mx.state.changed`.
546
+ */
547
+ state: "/api/v1/meet/state",
548
+ /** S4: `withdraw` (stop waiting) or `leave` (close this tab); the pass may ride in the body. */
549
+ leave: "/api/v1/meet/leave",
550
+ /** S4: a pass holder (guests included) saw the recording notice, for this run. */
551
+ consent: "/api/v1/meet/consent"
538
552
  };
539
553
  function meetingPath(template, meetingId) {
540
554
  return template.replace("{meeting_id}", encodeURIComponent(meetingId));
@@ -608,7 +622,10 @@ function parseRoomToken(body, roomName) {
608
622
  meetingPassExpiresAt: readString(payload, "meeting_pass_expires_at"),
609
623
  sessionId: readString(payload, "session_id"),
610
624
  lobby: readBoolean(payload, "lobby", false),
611
- breakoutRoomId: readString(payload, "breakout_room_id") ?? null
625
+ admissionState: readString(payload, "admission_state") ?? void 0,
626
+ pollSeconds: typeof payload["poll_seconds"] === "number" ? payload["poll_seconds"] : void 0,
627
+ breakoutRoomId: readString(payload, "breakout_room_id") ?? null,
628
+ observersVisibleTo: readString(payload, "observers_visible_to") === "hosts" ? "hosts" : "everyone"
612
629
  };
613
630
  }
614
631
  function createTokenClient(options) {
@@ -657,6 +674,7 @@ function createTokenClient(options) {
657
674
  ...input.meetingId !== void 0 ? { meeting_id: input.meetingId } : {},
658
675
  ...input.slug !== void 0 ? { slug: input.slug } : {},
659
676
  ...input.breakoutRoomId != null ? { breakout_room_id: input.breakoutRoomId } : {},
677
+ ...input.observe === true ? { observe: true } : {},
660
678
  organization_id: input.identity.organizationId,
661
679
  display_name: input.identity.displayName,
662
680
  device_id: input.identity.deviceId,
@@ -728,7 +746,8 @@ function createTokenClient(options) {
728
746
  return {
729
747
  get(input) {
730
748
  const entry = cache.get(input.roomName);
731
- const sameRoom = entry !== void 0 && (entry.token.breakoutRoomId ?? null) === (input.breakoutRoomId ?? null);
749
+ const sameRoom = entry !== void 0 && (entry.token.breakoutRoomId ?? null) === (input.breakoutRoomId ?? null) && // An observer's pass is not a participant's, and back (MD-16).
750
+ entry.token.role === "viewer" === (input.observe === true);
732
751
  if (entry !== void 0 && sameRoom) {
733
752
  if (entry.inFlight !== null) return entry.inFlight;
734
753
  if (!expired(entry)) return Promise.resolve(entry.token);
@@ -1220,14 +1239,15 @@ var MEET_EVENTS = {
1220
1239
  cancel: "mx.call.cancel",
1221
1240
  /** You answered (or declined) on another device. Stop ringing here. */
1222
1241
  settled: "mx.call.settled",
1223
- /** Someone is in the lobby. Hosts render the knock. */
1224
- knock: "mx.lobby.knock",
1225
- /** A host decided. The knocker learns it here — they have no DB read. */
1226
- admission: "mx.lobby.admission",
1227
- /** The room was locked or unlocked. */
1228
- lock: "mx.room.lock",
1229
- /** Recording started or stopped. EVERY participant renders the indicator. */
1230
- recording: "mx.room.recording"
1242
+ /**
1243
+ * 🚨 S4 (CORE-DESIGN §2.6): the ONE room-channel event — payload-free "something changed,
1244
+ * read again". Nothing a person must see rides it: a waiting person learns admitted /
1245
+ * denied / expired / ended by READING their row (`GET /v1/meet/state`) on a timer and on
1246
+ * every hint. The four payload broadcasts (`mx.lobby.knock`, `mx.lobby.admission`,
1247
+ * `mx.room.lock`, `mx.room.recording`) are gone with their senders and receivers. The
1248
+ * server publishes the same event on the same topic (aidream `admission.room_topic`).
1249
+ */
1250
+ stateChanged: "mx.state.changed"
1231
1251
  };
1232
1252
 
1233
1253
  // src/core/types.ts
@@ -1253,6 +1273,10 @@ function isLinkFollower(standing) {
1253
1273
  function isHostRole(role) {
1254
1274
  return role === "host" || role === "cohost";
1255
1275
  }
1276
+ function isObserverRole(role) {
1277
+ return role === "viewer";
1278
+ }
1279
+ var WAITING_ADMISSION_STATES = ["waiting_host", "knocking"];
1256
1280
 
1257
1281
  // src/core/calls.ts
1258
1282
  var TRANSITIONS = {
@@ -1501,813 +1525,492 @@ function createCallCenter(options) {
1501
1525
  };
1502
1526
  }
1503
1527
 
1504
- // src/core/collab.ts
1505
- var EMPTY_COLLAB = {
1506
- loaded: false,
1507
- error: null,
1508
- settings: null,
1509
- polls: [],
1510
- questions: [],
1511
- strokes: [],
1512
- breakout: null,
1513
- broadcast: null,
1514
- moveRequest: null,
1515
- panelRequest: null,
1516
- captionLanguage: null,
1517
- captionTranslations: {},
1518
- captionTranslationError: null
1528
+ // src/core/repository.ts
1529
+ var MEET_SCHEMA = "communication";
1530
+ var TABLES = {
1531
+ meetings: "meet_meetings",
1532
+ participants: "meet_participants",
1533
+ invites: "meet_call_invites",
1534
+ recordings: "meet_recordings",
1535
+ transcripts: "meet_transcript_segments",
1536
+ notes: "meet_notes",
1537
+ /** Wave 1 (0.8): who a meeting was sent to, and what they answered. */
1538
+ invitees: "meet_invitees",
1539
+ /** Wave 1 (0.8): one occurrence of a series cancelled or moved. */
1540
+ occurrenceOverrides: "meet_occurrence_overrides"
1519
1541
  };
1520
- function obj(value) {
1521
- return typeof value === "object" && value !== null && !Array.isArray(value) ? value : null;
1522
- }
1523
- function str(value) {
1524
- return typeof value === "string" ? value : null;
1525
- }
1526
- function num(value) {
1527
- return typeof value === "number" && Number.isFinite(value) ? value : null;
1542
+ var RPCS = {
1543
+ /** Atomic: create the invite row and stamp its expiry in one statement. */
1544
+ createCallInvite: "meet_create_call_invite",
1545
+ /** Atomic settle. Refuses a transition out of a terminal state. */
1546
+ settleCallInvite: "meet_settle_call_invite",
1547
+ /** Every invite still ringing this user — the reconnect backfill door. */
1548
+ pendingCallInvites: "meet_pending_call_invites",
1549
+ /** Instant meeting, or the durable one behind a recurring link. */
1550
+ getOrCreateMeeting: "meet_get_or_create_meeting",
1551
+ /** Resolve a durable public link, before/during/after its scheduled time. */
1552
+ meetingBySlug: "meet_meeting_by_slug",
1553
+ /*
1554
+ * ── S4 ADMISSION (CORE-DESIGN §2.4). Each returns the decided row; the caller then
1555
+ * sends the payload-free `mx.state.changed` hint. Authorized by `who_may_admit`.
1556
+ */
1557
+ admit: "meet_admit",
1558
+ deny: "meet_deny",
1559
+ undeny: "meet_undeny",
1560
+ admitAll: "meet_admit_all",
1561
+ setLock: "meet_set_lock",
1562
+ recordConsent: "meet_record_consent",
1563
+ /*
1564
+ * ── MEET WAVE 1 — management (0.8). Every write is a SECURITY DEFINER door
1565
+ * authorized by the meeting's host or a real iam grant (editor to change,
1566
+ * admin = co-host to archive or name a co-host) — never an organization
1567
+ * match. After any of these writes, call `announceMeeting` so the server
1568
+ * sends invitations / updates / cancellations and re-queues reminders.
1569
+ */
1570
+ /** Schedule with every setting at create; unset settings come from the knobs. */
1571
+ scheduleMeeting: "meet_schedule_meeting",
1572
+ /** Edit / reschedule — a JSON patch with a version compare-and-swap. */
1573
+ updateMeeting: "meet_update_meeting",
1574
+ /** Cancel is a state (`cancelled_at`); the link keeps resolving. */
1575
+ cancelMeeting: "meet_cancel_meeting",
1576
+ /** Archive (soft delete) — the invitations and exceptions cascade. */
1577
+ archiveMeeting: "meet_archive_meeting",
1578
+ restoreMeeting: "meet_restore_meeting",
1579
+ /** Cancel, move or restore ONE occurrence of a series by its original start. */
1580
+ setOccurrence: "meet_set_occurrence",
1581
+ addInvitees: "meet_add_invitees",
1582
+ setInviteeRole: "meet_set_invitee_role",
1583
+ removeInvitee: "meet_remove_invitee",
1584
+ /** A signed-in invitee answers for themselves. */
1585
+ respond: "meet_respond",
1586
+ /** The emailed RSVP link (no account): read and answer. */
1587
+ invitationByToken: "meet_invitation_by_token",
1588
+ respondByToken: "meet_respond_by_token",
1589
+ /** One meeting's occurrences between two instants (INVOKER — the reader's RLS). */
1590
+ meetingOccurrences: "meet_meeting_occurrences",
1591
+ /** Every occurrence the reader can see between two instants — the Upcoming list. */
1592
+ occurrencesBetween: "meet_occurrences_between",
1593
+ /*
1594
+ * ── MEET WAVE 3 — after the meeting. Both INVOKER (the reader's RLS) and both
1595
+ * scoped to the reader's OWN meetings (host, invitee, attended) — RLS is the
1596
+ * ceiling, never the view.
1597
+ */
1598
+ /** The recordings library. */
1599
+ myRecordings: "meet_my_recordings",
1600
+ /** Titles, agendas, current notes and final transcript lines; snippets mark hits with ⟦ ⟧. */
1601
+ search: "meet_search"
1602
+ };
1603
+ var VOID_RPCS = [RPCS.recordConsent];
1604
+ 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";
1605
+ 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";
1606
+ var RECORDING_COLUMNS = "id,meeting_id,state,started_at,ended_at,started_by,file_id,failure_reason";
1607
+ var SEGMENT_COLUMNS = "id,meeting_id,identity,speaker,text,started_at,is_final,metadata";
1608
+ var NOTE_COLUMNS = "id,meeting_id,text,kind,assignee_user_id,created_at,metadata";
1609
+ var ATTENDEE_COLUMNS = "id,meeting_id,identity,participant_user_id,display_name,avatar_url,role,admission_state,knocked_at,knock_expires_at,is_agent,joined_at,left_at,sessions:metadata->sessions";
1610
+ function projectStays(value) {
1611
+ if (!Array.isArray(value)) return [];
1612
+ const stays = [];
1613
+ for (const item of value) {
1614
+ if (item === null || typeof item !== "object") continue;
1615
+ const joined = item["joined_at"];
1616
+ const left = item["left_at"];
1617
+ if (typeof joined !== "string" || joined.length === 0) continue;
1618
+ stays.push({ joinedAt: joined, leftAt: typeof left === "string" && left.length > 0 ? left : null });
1619
+ }
1620
+ return stays;
1528
1621
  }
1529
- function strings(value) {
1530
- return Array.isArray(value) ? value.filter((v) => typeof v === "string") : [];
1622
+ function text(row, key) {
1623
+ const value = row[key];
1624
+ return typeof value === "string" && value.length > 0 ? value : null;
1531
1625
  }
1532
- function pollFromRow(row) {
1533
- const id = str(row["id"]);
1534
- if (id === null) return null;
1535
- const options = [];
1536
- for (const raw of Array.isArray(row["options"]) ? row["options"] : []) {
1537
- const option = obj(raw);
1538
- const optionId = option === null ? null : str(option["id"]);
1539
- if (option === null || optionId === null) continue;
1540
- const votes = num(option["votes"]);
1541
- options.push({
1542
- id: optionId,
1543
- text: str(option["text"]) ?? "",
1544
- ...votes !== null ? { votes } : {},
1545
- ...Array.isArray(option["voters"]) ? { voters: strings(option["voters"]) } : {}
1546
- });
1626
+ function requireText(row, key, operation) {
1627
+ const value = text(row, key);
1628
+ if (value === null) {
1629
+ throw invalidResponse(operation, `the row is missing "${key}"`);
1547
1630
  }
1548
- const state = str(row["state"]);
1549
- return {
1550
- id,
1551
- question: str(row["question"]) ?? "",
1552
- options,
1553
- multiple: row["multiple"] === true,
1554
- anonymous: row["anonymous"] === true,
1555
- state: state === "live" || state === "closed" ? state : "draft",
1556
- resultsShared: row["results_shared"] === true,
1557
- voterCount: num(row["voter_count"]),
1558
- myVote: strings(row["my_vote"]),
1559
- createdByName: str(row["created_by_name"]) ?? "",
1560
- createdAt: str(row["created_at"])
1561
- };
1631
+ return value;
1562
1632
  }
1563
- function questionFromRow(row) {
1564
- const id = str(row["id"]);
1565
- if (id === null) return null;
1566
- const state = str(row["state"]);
1567
- return {
1568
- id,
1569
- text: str(row["text"]) ?? "",
1570
- askerName: str(row["asker_name"]) ?? "",
1571
- anonymous: row["anonymous"] === true,
1572
- state: state === "answered" || state === "dismissed" ? state : "open",
1573
- answer: str(row["answer"]),
1574
- answeredByName: str(row["answered_by_name"]),
1575
- upvotes: num(row["upvotes"]) ?? 0,
1576
- upvotedByMe: row["upvoted_by_me"] === true,
1577
- mine: row["mine"] === true,
1578
- createdAt: str(row["created_at"])
1579
- };
1633
+ function bool(row, key, fallback) {
1634
+ const value = row[key];
1635
+ return typeof value === "boolean" ? value : fallback;
1580
1636
  }
1581
- function strokeFromRow(row) {
1582
- const id = str(row["id"]);
1583
- if (id === null || !Array.isArray(row["points"])) return null;
1584
- const points = [];
1585
- for (const raw of row["points"]) {
1586
- if (Array.isArray(raw) && typeof raw[0] === "number" && typeof raw[1] === "number") {
1587
- points.push([raw[0], raw[1]]);
1588
- }
1589
- }
1590
- return {
1591
- id,
1592
- points,
1593
- color: str(row["color"]) ?? "#111827",
1594
- width: num(row["width"]) ?? 3,
1595
- tool: row["tool"] === "highlighter" ? "highlighter" : "pen",
1596
- by: str(row["by"]) ?? "",
1597
- byName: str(row["by_name"]) ?? ""
1598
- };
1637
+ function stringArray(row, key) {
1638
+ const value = row[key];
1639
+ if (!Array.isArray(value)) return [];
1640
+ return value.filter((entry) => typeof entry === "string");
1599
1641
  }
1600
- function broadcastFrom(value) {
1601
- const row = obj(value);
1602
- const text3 = row === null ? null : str(row["text"]);
1603
- if (row === null || text3 === null) return null;
1604
- return { text: text3, by: str(row["by"]) ?? "", at: str(row["at"]) ?? "" };
1642
+ var CALL_STATES = [
1643
+ "idle",
1644
+ "dialing",
1645
+ "ringing",
1646
+ "connecting",
1647
+ "active",
1648
+ "declined",
1649
+ "missed",
1650
+ "busy",
1651
+ "cancelled",
1652
+ "ended",
1653
+ "failed"
1654
+ ];
1655
+ function callState(value) {
1656
+ return CALL_STATES.includes(value) ? value : "failed";
1605
1657
  }
1606
- function planFromRow(row) {
1607
- const id = str(row["id"]);
1608
- if (id === null) return null;
1609
- const rooms = [];
1610
- for (const raw of Array.isArray(row["rooms"]) ? row["rooms"] : []) {
1611
- const room = obj(raw);
1612
- const roomId = room === null ? null : str(room["id"]);
1613
- if (room === null || roomId === null) continue;
1614
- rooms.push({ id: roomId, name: str(room["name"]) ?? roomId, identities: strings(room["identities"]) });
1615
- }
1658
+ function projectSegment(row) {
1659
+ const operation = "meet.projectSegment";
1660
+ const id = requireText(row, "id", operation);
1616
1661
  return {
1617
1662
  id,
1618
- state: row["state"] === "open" ? "open" : "closed",
1619
- rooms,
1620
- allowChoose: row["allow_choose"] === true,
1621
- minutes: num(row["minutes"]) ?? 0,
1622
- openedAt: str(row["opened_at"]),
1623
- endsAt: str(row["ends_at"]),
1624
- closedAt: str(row["closed_at"]),
1625
- broadcasts: (Array.isArray(row["broadcasts"]) ? row["broadcasts"] : []).map(broadcastFrom).filter((b) => b !== null)
1663
+ meetingId: asMeetingId(requireText(row, "meeting_id", operation)),
1664
+ identity: asParticipantIdentity(text(row, "identity") ?? "unknown"),
1665
+ // A nameless line is a line nobody can act on. The identity is a worse
1666
+ // name than a real one and a far better one than an empty string.
1667
+ speaker: text(row, "speaker") ?? text(row, "identity") ?? "Someone",
1668
+ text: text(row, "text") ?? "",
1669
+ startedAt: text(row, "started_at") ?? (/* @__PURE__ */ new Date(0)).toISOString(),
1670
+ // The table holds finals only; a row that somehow says otherwise is still
1671
+ // a durable record and is rendered as one.
1672
+ final: bool(row, "is_final", true),
1673
+ source: "record",
1674
+ // The worker publishes the caption under THIS row's id (`lk.segment_id` =
1675
+ // the durable segment id), which is what lets a record replace the exact
1676
+ // caption it supersedes instead of doubling the line (D16).
1677
+ captionId: id
1626
1678
  };
1627
1679
  }
1628
- function settingsFromRow(row) {
1629
- if (row === null) return null;
1680
+ function projectNote(row) {
1681
+ const operation = "meet.projectNote";
1682
+ const kind = text(row, "kind");
1683
+ const rawMetadata = row["metadata"];
1684
+ const metadata = typeof rawMetadata === "object" && rawMetadata !== null && !Array.isArray(rawMetadata) ? rawMetadata : {};
1685
+ const assignee = metadata["assignee"];
1686
+ const assigneeRow = typeof assignee === "object" && assignee !== null && !Array.isArray(assignee) ? assignee : {};
1687
+ const windowValue = metadata["window"];
1688
+ const windowRow = typeof windowValue === "object" && windowValue !== null && !Array.isArray(windowValue) ? windowValue : {};
1689
+ const assigneeUserId = text(row, "assignee_user_id");
1690
+ const displayName = assigneeRow["display_name"];
1630
1691
  return {
1631
- pollsEnabled: row["polls_enabled"] === true,
1632
- pollMaxOptions: num(row["poll_max_options"]) ?? 10,
1633
- qaEnabled: row["qa_enabled"] === true,
1634
- qaAnonymousAllowed: row["qa_anonymous_allowed"] === true,
1635
- whiteboardEnabled: row["whiteboard_enabled"] === true,
1636
- breakoutsEnabled: row["breakouts_enabled"] === true,
1637
- breakoutMaxRooms: num(row["breakout_max_rooms"]) ?? 20,
1638
- breakoutDefaultMinutes: num(row["breakout_default_minutes"]) ?? 15,
1639
- captionTranslationEnabled: row["caption_translation_enabled"] === true,
1640
- captionTranslationLanguage: str(row["caption_translation_language"]) ?? ""
1692
+ id: requireText(row, "id", operation),
1693
+ meetingId: asMeetingId(requireText(row, "meeting_id", operation)),
1694
+ text: (text(row, "text") ?? "").trim(),
1695
+ createdAt: text(row, "created_at") ?? (/* @__PURE__ */ new Date()).toISOString(),
1696
+ // An unknown kind is a NOTE, never dropped: a row the database accepted
1697
+ // is a row a person wrote down, whatever a future migration calls it.
1698
+ kind: kind === "decision" || kind === "action-item" ? kind : "note",
1699
+ assigneeUserId: assigneeUserId === null ? null : asUserId(assigneeUserId),
1700
+ // D18: a guest never gets an `assignee_user_id` (the FK targets
1701
+ // `auth.users`), so their name lives in the metadata and is read here —
1702
+ // an action item whose owner has no name is an action item nobody owns.
1703
+ assigneeDisplayName: typeof displayName === "string" && displayName.length > 0 ? displayName : null,
1704
+ role: metadata["role"] === "summary" ? "summary" : null,
1705
+ window: typeof windowRow["index"] === "number" ? windowRow["index"] : null,
1706
+ // D20. `superseded_by` is in `NOTE_PUBLIC_METADATA_KEYS` server-side, so a
1707
+ // session-less guest's room-borne copy carries it too — otherwise the guest
1708
+ // would be the one lane still reading a decision twice.
1709
+ supersededBy: typeof metadata["superseded_by"] === "string" && metadata["superseded_by"].length > 0 ? metadata["superseded_by"] : null,
1710
+ // THE DATABASE ROW. `core/room-events.ts` marks its copy of the same row
1711
+ // `"room"`, and the store's precedence rule reads this field (MRI-A12).
1712
+ source: "record"
1641
1713
  };
1642
1714
  }
1643
- function collabFromState(body, previous) {
1644
- const list = (key, parse) => (Array.isArray(body[key]) ? body[key] : []).map((raw) => {
1645
- const row = obj(raw);
1646
- return row === null ? null : parse(row);
1647
- }).filter((item) => item !== null);
1648
- const breakoutRow = obj(body["breakout"]);
1715
+ var RSVP_STATES = ["needs_action", "accepted", "declined", "tentative"];
1716
+ var OCCURRENCE_STATES = ["scheduled", "moved", "cancelled"];
1717
+ function rsvpState(value) {
1718
+ return RSVP_STATES.includes(value) ? value : "needs_action";
1719
+ }
1720
+ function num(row, key) {
1721
+ const value = row[key];
1722
+ return typeof value === "number" ? value : null;
1723
+ }
1724
+ function when(row, key) {
1725
+ const value = row[key];
1726
+ if (value instanceof Date) return value.toISOString();
1727
+ return text(row, key);
1728
+ }
1729
+ function projectInvitee(row) {
1730
+ const operation = "meet.projectInvitee";
1731
+ const userId = text(row, "invitee_user_id");
1649
1732
  return {
1650
- ...previous,
1651
- loaded: true,
1652
- error: null,
1653
- settings: settingsFromRow(obj(body["settings"])),
1654
- polls: list("polls", pollFromRow),
1655
- questions: list("questions", questionFromRow),
1656
- strokes: list("strokes", strokeFromRow),
1657
- breakout: breakoutRow === null ? null : planFromRow(breakoutRow)
1733
+ id: requireText(row, "id", operation),
1734
+ meetingId: asMeetingId(requireText(row, "meeting_id", operation)),
1735
+ userId: userId === null ? null : asUserId(userId),
1736
+ email: text(row, "email"),
1737
+ displayName: text(row, "display_name"),
1738
+ role: text(row, "role") === "cohost" ? "cohost" : "invitee",
1739
+ rsvpState: rsvpState(text(row, "rsvp_state")),
1740
+ respondedAt: when(row, "responded_at"),
1741
+ rsvpNote: text(row, "rsvp_note"),
1742
+ lastSentSequence: num(row, "last_sent_sequence"),
1743
+ lastSentAt: when(row, "last_sent_at")
1658
1744
  };
1659
1745
  }
1660
- function upsert(items, next) {
1661
- const index = items.findIndex((item) => item.id === next.id);
1662
- if (index === -1) return [...items, next];
1663
- const copy = items.slice();
1664
- copy[index] = next;
1665
- return copy;
1746
+ function projectOccurrence(row) {
1747
+ const operation = "meet.projectOccurrence";
1748
+ const state = text(row, "state");
1749
+ return {
1750
+ meetingId: asMeetingId(requireText(row, "meeting_id", operation)),
1751
+ originalStart: when(row, "original_start") ?? requireText(row, "original_start", operation),
1752
+ occurrenceStart: when(row, "occurrence_start") ?? requireText(row, "occurrence_start", operation),
1753
+ durationMinutes: num(row, "duration_minutes"),
1754
+ // An unknown state is shown as scheduled, never dropped: the row is real.
1755
+ state: OCCURRENCE_STATES.includes(state) ? state : "scheduled",
1756
+ sequence: num(row, "sequence") ?? 0
1757
+ };
1666
1758
  }
1667
- function withItem(collab, row, fromRoom = false) {
1668
- const kind = str(row["kind"]);
1669
- if (kind === "poll") {
1670
- const poll = pollFromRow(row);
1671
- if (poll === null) return collab;
1672
- const known = collab.polls.find((p) => p.id === poll.id);
1673
- let merged = fromRoom && known !== void 0 ? { ...poll, myVote: known.myVote } : poll;
1674
- if (fromRoom && known !== void 0 && !poll.options.some((o) => o.votes !== void 0)) {
1675
- const counted = new Map(known.options.map((o) => [o.id, o]));
1676
- merged = {
1677
- ...merged,
1678
- voterCount: known.voterCount,
1679
- options: merged.options.map((o) => {
1680
- const had = counted.get(o.id);
1681
- return had === void 0 ? o : { ...o, votes: had.votes, voters: had.voters };
1682
- })
1683
- };
1684
- }
1685
- return { ...collab, polls: upsert(collab.polls, merged) };
1686
- }
1687
- if (kind === "question") {
1688
- const question = questionFromRow(row);
1689
- if (question === null) return collab;
1690
- const known = collab.questions.find((q) => q.id === question.id);
1691
- const merged = fromRoom && known !== void 0 ? { ...question, mine: known.mine, upvotedByMe: known.upvotedByMe } : question;
1692
- return { ...collab, questions: upsert(collab.questions, merged) };
1693
- }
1694
- if (kind === "stroke") {
1695
- const stroke = strokeFromRow(row);
1696
- if (stroke === null) return collab;
1697
- return { ...collab, strokes: upsert(collab.strokes, stroke) };
1759
+ function projectUpcoming(row) {
1760
+ const operation = "meet.projectUpcoming";
1761
+ const kind = text(row, "kind");
1762
+ const myRole = text(row, "my_role");
1763
+ const myRsvp = text(row, "my_rsvp");
1764
+ return {
1765
+ ...projectOccurrence(row),
1766
+ organizationId: asOrganizationId(requireText(row, "organization_id", operation)),
1767
+ title: text(row, "title") ?? "Meeting",
1768
+ slug: requireText(row, "slug", operation),
1769
+ kind: kind === "scheduled" || kind === "recurring" ? kind : "instant",
1770
+ hostUserId: asUserId(requireText(row, "host_user_id", operation)),
1771
+ timeZone: text(row, "time_zone") ?? "UTC",
1772
+ meetingCancelled: bool(row, "meeting_cancelled", false),
1773
+ myRsvp: myRsvp === null ? null : rsvpState(myRsvp),
1774
+ myRole: myRole === "host" || myRole === "cohost" || myRole === "invitee" ? myRole : null
1775
+ };
1776
+ }
1777
+ function projectOverride(row) {
1778
+ const operation = "meet.projectOverride";
1779
+ return {
1780
+ id: requireText(row, "id", operation),
1781
+ meetingId: asMeetingId(requireText(row, "meeting_id", operation)),
1782
+ originalStart: when(row, "original_start") ?? requireText(row, "original_start", operation),
1783
+ state: text(row, "state") === "moved" ? "moved" : "cancelled",
1784
+ newStart: when(row, "new_start"),
1785
+ newDurationMinutes: num(row, "new_duration_minutes"),
1786
+ reason: text(row, "reason"),
1787
+ calendarSequence: num(row, "calendar_sequence") ?? 0,
1788
+ deletedAt: when(row, "deleted_at")
1789
+ };
1790
+ }
1791
+ function projectRsvpInvitation(body) {
1792
+ const operation = "meet.projectRsvpInvitation";
1793
+ if (typeof body !== "object" || body === null) {
1794
+ throw invalidResponse(operation, "the RSVP door returned no object");
1698
1795
  }
1699
- if (kind === "breakout") {
1700
- const plan = planFromRow(row);
1701
- if (plan === null) return collab;
1702
- return { ...collab, breakout: plan };
1796
+ const row = body;
1797
+ if (row["ok"] !== true) {
1798
+ return {
1799
+ ok: false,
1800
+ message: text(row, "message") ?? "This invitation link is no longer valid \u2014 ask the host to send it again."
1801
+ };
1703
1802
  }
1704
- return collab;
1705
- }
1706
- function without(collab, ids) {
1707
- const gone = new Set(ids);
1803
+ const meeting = row["meeting"] ?? {};
1804
+ const invitee = row["invitee"] ?? {};
1708
1805
  return {
1709
- ...collab,
1710
- polls: collab.polls.filter((p) => !gone.has(p.id)),
1711
- questions: collab.questions.filter((q) => !gone.has(q.id)),
1712
- strokes: collab.strokes.filter((s) => !gone.has(s.id))
1806
+ ok: true,
1807
+ meeting: {
1808
+ title: text(meeting, "title") ?? "Meeting",
1809
+ slug: requireText(meeting, "slug", operation),
1810
+ scheduledFor: when(meeting, "scheduled_for"),
1811
+ durationMinutes: num(meeting, "duration_minutes"),
1812
+ timeZone: text(meeting, "time_zone") ?? "UTC",
1813
+ agenda: text(meeting, "agenda"),
1814
+ recurrenceRule: text(meeting, "recurrence_rule"),
1815
+ cancelledAt: when(meeting, "cancelled_at"),
1816
+ cancellationReason: text(meeting, "cancellation_reason"),
1817
+ endedAt: when(meeting, "ended_at")
1818
+ },
1819
+ invitee: {
1820
+ displayName: text(invitee, "display_name"),
1821
+ email: text(invitee, "email"),
1822
+ rsvpState: rsvpState(text(invitee, "rsvp_state")),
1823
+ respondedAt: when(invitee, "responded_at")
1824
+ }
1713
1825
  };
1714
1826
  }
1715
- function applyCollabPacket(collab, payload, context) {
1716
- const item = obj(payload["item"]);
1717
- if (item !== null) {
1718
- const kind = str(item["kind"]);
1719
- let next = withItem(collab, item, true);
1720
- if (kind === "poll" && str(item["state"]) === "live") {
1721
- const before = collab.polls.find((p) => p.id === str(item["id"]));
1722
- if (before === void 0 || before.state !== "live") {
1723
- next = { ...next, panelRequest: { panel: "polls", at: context.now } };
1827
+ function settingsPatch(settings) {
1828
+ const patch = {};
1829
+ if (settings.lobbyEnabled !== void 0) patch["lobby_enabled"] = settings.lobbyEnabled;
1830
+ if (settings.recordingPolicy !== void 0) patch["recording_policy"] = settings.recordingPolicy;
1831
+ if (settings.aiEnabled !== void 0) patch["ai_enabled"] = settings.aiEnabled;
1832
+ if (settings.joinBeforeHost !== void 0) patch["join_before_host"] = settings.joinBeforeHost;
1833
+ return patch;
1834
+ }
1835
+ function changesPatch(changes) {
1836
+ const patch = settingsPatch(changes);
1837
+ if (changes.title !== void 0) patch["title"] = changes.title;
1838
+ if (changes.agenda !== void 0) patch["agenda"] = changes.agenda;
1839
+ if (changes.timeZone !== void 0) patch["time_zone"] = changes.timeZone;
1840
+ if (changes.scheduledFor !== void 0) patch["scheduled_for"] = changes.scheduledFor;
1841
+ if (changes.scheduledDurationMinutes !== void 0) {
1842
+ patch["scheduled_duration_minutes"] = changes.scheduledDurationMinutes;
1843
+ }
1844
+ if (changes.recurrenceRule !== void 0) patch["recurrence_rule"] = changes.recurrenceRule;
1845
+ if (changes.keepExceptions !== void 0) patch["keep_exceptions"] = changes.keepExceptions;
1846
+ return patch;
1847
+ }
1848
+ function createMeetRepository(options) {
1849
+ const db = options.client.schema(MEET_SCHEMA);
1850
+ async function guardVoid(operation, run) {
1851
+ let response = await run();
1852
+ if (response.error !== null) {
1853
+ const normalized = normalizeMeetError(response.error, operation);
1854
+ if (normalized.code === "session-unavailable" && options.session !== void 0) {
1855
+ await options.session();
1856
+ response = await run();
1724
1857
  }
1858
+ if (response.error !== null) throw normalizeMeetError(response.error, operation);
1725
1859
  }
1726
- if (kind === "breakout") {
1727
- const plan = planFromRow(item);
1728
- if (plan !== null && plan.state === "closed") {
1729
- next = { ...next, moveRequest: { roomId: null, at: context.now } };
1860
+ }
1861
+ async function guard(operation, run) {
1862
+ let response = await run();
1863
+ if (response.error !== null) {
1864
+ const normalized = normalizeMeetError(response.error, operation);
1865
+ if (normalized.code === "session-unavailable" && options.session !== void 0) {
1866
+ await options.session();
1867
+ response = await run();
1730
1868
  }
1869
+ if (response.error !== null) throw normalizeMeetError(response.error, operation);
1731
1870
  }
1732
- const move = obj(payload["move"]);
1733
- if (move !== null && str(move["identity"]) === context.localIdentity) {
1734
- next = { ...next, moveRequest: { roomId: str(move["room_id"]), at: context.now } };
1871
+ if (response.data === null) {
1872
+ throw invalidResponse(operation, "the database returned no row");
1735
1873
  }
1736
- return { next };
1737
- }
1738
- if (Array.isArray(payload["removed"])) {
1739
- return { next: without(collab, strings(payload["removed"])) };
1740
- }
1741
- if (Array.isArray(payload["hidden"])) {
1742
- return context.isHost ? { next: null, dropped: "hosts keep dismissed questions" } : { next: without(collab, strings(payload["hidden"])) };
1743
- }
1744
- if (payload["cleared"] === "whiteboard") {
1745
- return { next: { ...collab, strokes: [] } };
1874
+ return response.data;
1746
1875
  }
1747
- const broadcast = broadcastFrom(payload["broadcast"]);
1748
- if (broadcast !== null) {
1749
- return { next: { ...collab, broadcast } };
1750
- }
1751
- return { next: null, dropped: "a collab packet with nothing this version understands" };
1752
- }
1753
- function assignedRoom(plan, identity) {
1754
- if (plan === null || plan.state !== "open" || identity === null) return null;
1755
- return plan.rooms.find((room) => room.identities.includes(identity)) ?? null;
1756
- }
1757
- function autoAssign(identities, rooms, exclude = []) {
1758
- const count2 = Math.max(1, Math.floor(rooms));
1759
- const out = Array.from({ length: count2 }, () => []);
1760
- const skip = new Set(exclude);
1761
- let index = 0;
1762
- for (const identity of identities) {
1763
- if (skip.has(identity)) continue;
1764
- out[index % count2].push(identity);
1765
- index += 1;
1876
+ function projectInvite(row) {
1877
+ const operation = "meet.projectInvite";
1878
+ const mode = text(row, "mode");
1879
+ return {
1880
+ id: asCallId(requireText(row, "id", operation)),
1881
+ organizationId: asOrganizationId(requireText(row, "organization_id", operation)),
1882
+ roomName: asRoomName(requireText(row, "room_name", operation)),
1883
+ mode: mode === "video" ? "video" : "audio",
1884
+ callerUserId: asUserId(requireText(row, "caller_user_id", operation)),
1885
+ callerName: text(row, "caller_name") ?? "Someone",
1886
+ callerAvatarUrl: text(row, "caller_avatar_url"),
1887
+ calleeUserIds: stringArray(row, "callee_ids").map(asUserId),
1888
+ conversationId: text(row, "conversation_id"),
1889
+ createdAt: requireText(row, "created_at", operation),
1890
+ expiresAt: text(row, "expires_at") ?? requireText(row, "created_at", operation),
1891
+ state: callState(text(row, "state")),
1892
+ declineMessage: text(row, "decline_message")
1893
+ };
1766
1894
  }
1767
- return out;
1768
- }
1769
- function breakoutRemainingMs(plan, now) {
1770
- if (plan === null || plan.state !== "open" || plan.endsAt === null) return null;
1771
- const end = Date.parse(plan.endsAt);
1772
- return Number.isNaN(end) ? null : Math.max(0, end - now);
1773
- }
1774
- function defaultCaptionLanguage(settings, viewerLanguage) {
1775
- const configured = settings?.captionTranslationLanguage.trim() ?? "";
1776
- if (configured.length > 0) return configured;
1777
- const own = (viewerLanguage ?? "").trim();
1778
- return own.length > 0 ? own : "en";
1779
- }
1780
- var CAPTION_LANGUAGES = [
1781
- { tag: "en", name: "English" },
1782
- { tag: "es", name: "Spanish" },
1783
- { tag: "fr", name: "French" },
1784
- { tag: "de", name: "German" },
1785
- { tag: "it", name: "Italian" },
1786
- { tag: "pt", name: "Portuguese" },
1787
- { tag: "nl", name: "Dutch" },
1788
- { tag: "pl", name: "Polish" },
1789
- { tag: "ru", name: "Russian" },
1790
- { tag: "uk", name: "Ukrainian" },
1791
- { tag: "tr", name: "Turkish" },
1792
- { tag: "ar", name: "Arabic" },
1793
- { tag: "he", name: "Hebrew" },
1794
- { tag: "fa", name: "Persian" },
1795
- { tag: "hi", name: "Hindi" },
1796
- { tag: "bn", name: "Bengali" },
1797
- { tag: "zh", name: "Chinese" },
1798
- { tag: "ja", name: "Japanese" },
1799
- { tag: "ko", name: "Korean" },
1800
- { tag: "vi", name: "Vietnamese" },
1801
- { tag: "th", name: "Thai" },
1802
- { tag: "id", name: "Indonesian" },
1803
- { tag: "sv", name: "Swedish" }
1804
- ];
1805
- async function loadMeetingActivities(api, meetingId, meetingPass = null) {
1806
- let response;
1807
- try {
1808
- response = await api.request({
1809
- path: `${MEET_ROUTES.collab}?meeting_id=${encodeURIComponent(meetingId)}`,
1810
- method: "GET",
1811
- operation: "meet.loadMeetingActivities",
1812
- requireAuth: meetingPass === null,
1813
- ...meetingPass !== null ? { headers: { [MEETING_PASS_HEADER]: meetingPass } } : {}
1814
- });
1815
- } catch (error) {
1895
+ function projectMeeting(row) {
1896
+ const operation = "meet.projectMeeting";
1897
+ const kind = text(row, "kind");
1898
+ const policy = text(row, "recording_policy");
1899
+ const duration = row["scheduled_duration_minutes"];
1900
+ const metadata = row["metadata"];
1816
1901
  return {
1817
- collab: null,
1818
- refusal: {
1819
- message: `Polls, questions and the whiteboard could not be read (${error.message}).`,
1820
- remedy: error.remedy ?? "Reload the page."
1821
- }
1902
+ id: asMeetingId(requireText(row, "id", operation)),
1903
+ organizationId: asOrganizationId(requireText(row, "organization_id", operation)),
1904
+ roomName: asRoomName(requireText(row, "room_name", operation)),
1905
+ slug: requireText(row, "slug", operation),
1906
+ title: text(row, "title") ?? "Meeting",
1907
+ kind: kind === "scheduled" || kind === "recurring" ? kind : "instant",
1908
+ hostUserId: asUserId(requireText(row, "host_user_id", operation)),
1909
+ scheduledFor: text(row, "scheduled_for"),
1910
+ scheduledDurationMinutes: typeof duration === "number" ? duration : null,
1911
+ startedAt: text(row, "started_at"),
1912
+ endedAt: text(row, "ended_at"),
1913
+ locked: bool(row, "locked", false),
1914
+ lobbyEnabled: bool(row, "lobby_enabled", true),
1915
+ recordingPolicy: policy === "disabled" || policy === "always-on" ? policy : "host-controlled",
1916
+ aiEnabled: bool(row, "ai_enabled", true),
1917
+ metadata: typeof metadata === "object" && metadata !== null && !Array.isArray(metadata) ? metadata : {},
1918
+ timeZone: text(row, "time_zone") ?? "UTC",
1919
+ agenda: text(row, "agenda"),
1920
+ recurrenceRule: text(row, "recurrence_rule"),
1921
+ joinBeforeHost: bool(row, "join_before_host", true),
1922
+ cancelledAt: text(row, "cancelled_at"),
1923
+ cancellationReason: text(row, "cancellation_reason"),
1924
+ deletedAt: text(row, "deleted_at"),
1925
+ calendarSequence: typeof row["calendar_sequence"] === "number" ? row["calendar_sequence"] : 0,
1926
+ version: typeof row["version"] === "number" ? row["version"] : 1
1822
1927
  };
1823
1928
  }
1824
- if (!response.ok) {
1825
- let body = null;
1826
- try {
1827
- body = await response.json();
1828
- } catch {
1829
- body = null;
1830
- }
1831
- const said = readServerRefusal(body);
1929
+ function projectRecording(row) {
1930
+ const operation = "meet.projectRecording";
1931
+ const state = text(row, "state");
1932
+ const known = [
1933
+ "idle",
1934
+ "starting",
1935
+ "recording",
1936
+ "stopping",
1937
+ "processing",
1938
+ "available",
1939
+ "expired",
1940
+ "failed"
1941
+ ];
1832
1942
  return {
1833
- collab: null,
1834
- refusal: {
1835
- message: said?.reason ?? `Polls and questions could not be read (the server answered ${response.status}).`,
1836
- remedy: said?.remedy || "Reload the page."
1837
- }
1943
+ id: requireText(row, "id", operation),
1944
+ meetingId: asMeetingId(requireText(row, "meeting_id", operation)),
1945
+ state: known.includes(state) ? state : "failed",
1946
+ startedAt: text(row, "started_at"),
1947
+ endedAt: text(row, "ended_at"),
1948
+ startedBy: text(row, "started_by") === null ? null : asUserId(text(row, "started_by")),
1949
+ // A PLATFORM file id, never a provider URL (R3). A recording that only
1950
+ // exists as a LiveKit URL is orphaned the day the room is garbage
1951
+ // collected, which is the whole reason this column is not `url`.
1952
+ fileId: text(row, "file_id"),
1953
+ failureReason: text(row, "failure_reason"),
1954
+ retention: projectRecordingRetention(row["metadata"])
1838
1955
  };
1839
1956
  }
1840
- return { collab: collabFromState(await response.json(), EMPTY_COLLAB), refusal: null };
1841
- }
1842
-
1843
- // src/core/repository.ts
1844
- var MEET_SCHEMA = "communication";
1845
- var TABLES = {
1846
- meetings: "meet_meetings",
1847
- participants: "meet_participants",
1848
- invites: "meet_call_invites",
1849
- recordings: "meet_recordings",
1850
- transcripts: "meet_transcript_segments",
1851
- notes: "meet_notes",
1852
- /** Wave 1 (0.8): who a meeting was sent to, and what they answered. */
1853
- invitees: "meet_invitees",
1854
- /** Wave 1 (0.8): one occurrence of a series cancelled or moved. */
1855
- occurrenceOverrides: "meet_occurrence_overrides"
1856
- };
1857
- var RPCS = {
1858
- /** Atomic: create the invite row and stamp its expiry in one statement. */
1859
- createCallInvite: "meet_create_call_invite",
1860
- /** Atomic settle. Refuses a transition out of a terminal state. */
1861
- settleCallInvite: "meet_settle_call_invite",
1862
- /** Every invite still ringing this user — the reconnect backfill door. */
1863
- pendingCallInvites: "meet_pending_call_invites",
1864
- /** Instant meeting, or the durable one behind a recurring link. */
1865
- getOrCreateMeeting: "meet_get_or_create_meeting",
1866
- /** Resolve a durable public link, before/during/after its scheduled time. */
1867
- meetingBySlug: "meet_meeting_by_slug",
1868
- admit: "meet_admit_participant",
1869
- setLock: "meet_set_lock",
1870
- recordConsent: "meet_record_consent",
1871
- /*
1872
- * ── MEET WAVE 1 — management (0.8). Every write is a SECURITY DEFINER door
1873
- * authorized by the meeting's host or a real iam grant (editor to change,
1874
- * admin = co-host to archive or name a co-host) — never an organization
1875
- * match. After any of these writes, call `announceMeeting` so the server
1876
- * sends invitations / updates / cancellations and re-queues reminders.
1877
- */
1878
- /** Schedule with every setting at create; unset settings come from the knobs. */
1879
- scheduleMeeting: "meet_schedule_meeting",
1880
- /** Edit / reschedule — a JSON patch with a version compare-and-swap. */
1881
- updateMeeting: "meet_update_meeting",
1882
- /** Cancel is a state (`cancelled_at`); the link keeps resolving. */
1883
- cancelMeeting: "meet_cancel_meeting",
1884
- /** Archive (soft delete) — the invitations and exceptions cascade. */
1885
- archiveMeeting: "meet_archive_meeting",
1886
- restoreMeeting: "meet_restore_meeting",
1887
- /** Cancel, move or restore ONE occurrence of a series by its original start. */
1888
- setOccurrence: "meet_set_occurrence",
1889
- addInvitees: "meet_add_invitees",
1890
- setInviteeRole: "meet_set_invitee_role",
1891
- removeInvitee: "meet_remove_invitee",
1892
- /** A signed-in invitee answers for themselves. */
1893
- respond: "meet_respond",
1894
- /** The emailed RSVP link (no account): read and answer. */
1895
- invitationByToken: "meet_invitation_by_token",
1896
- respondByToken: "meet_respond_by_token",
1897
- /** One meeting's occurrences between two instants (INVOKER — the reader's RLS). */
1898
- meetingOccurrences: "meet_meeting_occurrences",
1899
- /** Every occurrence the reader can see between two instants — the Upcoming list. */
1900
- occurrencesBetween: "meet_occurrences_between",
1901
- /*
1902
- * ── MEET WAVE 3 — after the meeting. Both INVOKER (the reader's RLS) and both
1903
- * scoped to the reader's OWN meetings (host, invitee, attended) — RLS is the
1904
- * ceiling, never the view.
1905
- */
1906
- /** The recordings library. */
1907
- myRecordings: "meet_my_recordings",
1908
- /** Titles, agendas, current notes and final transcript lines; snippets mark hits with ⟦ ⟧. */
1909
- search: "meet_search"
1910
- };
1911
- var VOID_RPCS = [RPCS.admit, RPCS.recordConsent];
1912
- 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";
1913
- 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";
1914
- var RECORDING_COLUMNS = "id,meeting_id,state,started_at,ended_at,started_by,file_id,failure_reason";
1915
- var SEGMENT_COLUMNS = "id,meeting_id,identity,speaker,text,started_at,is_final,metadata";
1916
- var NOTE_COLUMNS = "id,meeting_id,text,kind,assignee_user_id,created_at,metadata";
1917
- var ATTENDEE_COLUMNS = "id,meeting_id,identity,participant_user_id,display_name,avatar_url,role,admission_state,is_agent,joined_at,left_at,sessions:metadata->sessions";
1918
- function projectStays(value) {
1919
- if (!Array.isArray(value)) return [];
1920
- const stays = [];
1921
- for (const item of value) {
1922
- if (item === null || typeof item !== "object") continue;
1923
- const joined = item["joined_at"];
1924
- const left = item["left_at"];
1925
- if (typeof joined !== "string" || joined.length === 0) continue;
1926
- stays.push({ joinedAt: joined, leftAt: typeof left === "string" && left.length > 0 ? left : null });
1957
+ function projectRecordingRetention(value) {
1958
+ if (typeof value !== "object" || value === null || Array.isArray(value)) return null;
1959
+ const retention = value["retention"];
1960
+ if (typeof retention !== "object" || retention === null || Array.isArray(retention)) return null;
1961
+ const row = retention;
1962
+ const phase = row["phase"];
1963
+ const operationId = row["operation_id"];
1964
+ const policyId = row["policy_id"];
1965
+ const policyUpdatedAt = row["policy_updated_at"];
1966
+ const dueAt = row["due_at"];
1967
+ const expiredAt = row["expired_at"];
1968
+ if (phase !== "warning" && phase !== "expired" || typeof operationId !== "string" || typeof policyId !== "string" || typeof policyUpdatedAt !== "string" || typeof dueAt !== "string" || expiredAt !== null && typeof expiredAt !== "string") return null;
1969
+ return { operationId, policyId, policyUpdatedAt, phase, dueAt, expiredAt };
1927
1970
  }
1928
- return stays;
1929
- }
1930
- function text(row, key) {
1931
- const value = row[key];
1932
- return typeof value === "string" && value.length > 0 ? value : null;
1933
- }
1934
- function requireText(row, key, operation) {
1935
- const value = text(row, key);
1936
- if (value === null) {
1937
- throw invalidResponse(operation, `the row is missing "${key}"`);
1938
- }
1939
- return value;
1940
- }
1941
- function bool(row, key, fallback) {
1942
- const value = row[key];
1943
- return typeof value === "boolean" ? value : fallback;
1944
- }
1945
- function stringArray(row, key) {
1946
- const value = row[key];
1947
- if (!Array.isArray(value)) return [];
1948
- return value.filter((entry) => typeof entry === "string");
1949
- }
1950
- var CALL_STATES = [
1951
- "idle",
1952
- "dialing",
1953
- "ringing",
1954
- "connecting",
1955
- "active",
1956
- "declined",
1957
- "missed",
1958
- "busy",
1959
- "cancelled",
1960
- "ended",
1961
- "failed"
1962
- ];
1963
- function callState(value) {
1964
- return CALL_STATES.includes(value) ? value : "failed";
1965
- }
1966
- function projectSegment(row) {
1967
- const operation = "meet.projectSegment";
1968
- const id = requireText(row, "id", operation);
1969
- return {
1970
- id,
1971
- meetingId: asMeetingId(requireText(row, "meeting_id", operation)),
1972
- identity: asParticipantIdentity(text(row, "identity") ?? "unknown"),
1973
- // A nameless line is a line nobody can act on. The identity is a worse
1974
- // name than a real one and a far better one than an empty string.
1975
- speaker: text(row, "speaker") ?? text(row, "identity") ?? "Someone",
1976
- text: text(row, "text") ?? "",
1977
- startedAt: text(row, "started_at") ?? (/* @__PURE__ */ new Date(0)).toISOString(),
1978
- // The table holds finals only; a row that somehow says otherwise is still
1979
- // a durable record and is rendered as one.
1980
- final: bool(row, "is_final", true),
1981
- source: "record",
1982
- // The worker publishes the caption under THIS row's id (`lk.segment_id` =
1983
- // the durable segment id), which is what lets a record replace the exact
1984
- // caption it supersedes instead of doubling the line (D16).
1985
- captionId: id
1986
- };
1987
- }
1988
- function projectNote(row) {
1989
- const operation = "meet.projectNote";
1990
- const kind = text(row, "kind");
1991
- const rawMetadata = row["metadata"];
1992
- const metadata = typeof rawMetadata === "object" && rawMetadata !== null && !Array.isArray(rawMetadata) ? rawMetadata : {};
1993
- const assignee = metadata["assignee"];
1994
- const assigneeRow = typeof assignee === "object" && assignee !== null && !Array.isArray(assignee) ? assignee : {};
1995
- const windowValue = metadata["window"];
1996
- const windowRow = typeof windowValue === "object" && windowValue !== null && !Array.isArray(windowValue) ? windowValue : {};
1997
- const assigneeUserId = text(row, "assignee_user_id");
1998
- const displayName = assigneeRow["display_name"];
1999
- return {
2000
- id: requireText(row, "id", operation),
2001
- meetingId: asMeetingId(requireText(row, "meeting_id", operation)),
2002
- text: (text(row, "text") ?? "").trim(),
2003
- createdAt: text(row, "created_at") ?? (/* @__PURE__ */ new Date()).toISOString(),
2004
- // An unknown kind is a NOTE, never dropped: a row the database accepted
2005
- // is a row a person wrote down, whatever a future migration calls it.
2006
- kind: kind === "decision" || kind === "action-item" ? kind : "note",
2007
- assigneeUserId: assigneeUserId === null ? null : asUserId(assigneeUserId),
2008
- // D18: a guest never gets an `assignee_user_id` (the FK targets
2009
- // `auth.users`), so their name lives in the metadata and is read here —
2010
- // an action item whose owner has no name is an action item nobody owns.
2011
- assigneeDisplayName: typeof displayName === "string" && displayName.length > 0 ? displayName : null,
2012
- role: metadata["role"] === "summary" ? "summary" : null,
2013
- window: typeof windowRow["index"] === "number" ? windowRow["index"] : null,
2014
- // D20. `superseded_by` is in `NOTE_PUBLIC_METADATA_KEYS` server-side, so a
2015
- // session-less guest's room-borne copy carries it too — otherwise the guest
2016
- // would be the one lane still reading a decision twice.
2017
- supersededBy: typeof metadata["superseded_by"] === "string" && metadata["superseded_by"].length > 0 ? metadata["superseded_by"] : null,
2018
- // THE DATABASE ROW. `core/room-events.ts` marks its copy of the same row
2019
- // `"room"`, and the store's precedence rule reads this field (MRI-A12).
2020
- source: "record"
2021
- };
2022
- }
2023
- var RSVP_STATES = ["needs_action", "accepted", "declined", "tentative"];
2024
- var OCCURRENCE_STATES = ["scheduled", "moved", "cancelled"];
2025
- function rsvpState(value) {
2026
- return RSVP_STATES.includes(value) ? value : "needs_action";
2027
- }
2028
- function num2(row, key) {
2029
- const value = row[key];
2030
- return typeof value === "number" ? value : null;
2031
- }
2032
- function when(row, key) {
2033
- const value = row[key];
2034
- if (value instanceof Date) return value.toISOString();
2035
- return text(row, key);
2036
- }
2037
- function projectInvitee(row) {
2038
- const operation = "meet.projectInvitee";
2039
- const userId = text(row, "invitee_user_id");
2040
- return {
2041
- id: requireText(row, "id", operation),
2042
- meetingId: asMeetingId(requireText(row, "meeting_id", operation)),
2043
- userId: userId === null ? null : asUserId(userId),
2044
- email: text(row, "email"),
2045
- displayName: text(row, "display_name"),
2046
- role: text(row, "role") === "cohost" ? "cohost" : "invitee",
2047
- rsvpState: rsvpState(text(row, "rsvp_state")),
2048
- respondedAt: when(row, "responded_at"),
2049
- rsvpNote: text(row, "rsvp_note"),
2050
- lastSentSequence: num2(row, "last_sent_sequence"),
2051
- lastSentAt: when(row, "last_sent_at")
2052
- };
2053
- }
2054
- function projectOccurrence(row) {
2055
- const operation = "meet.projectOccurrence";
2056
- const state = text(row, "state");
2057
- return {
2058
- meetingId: asMeetingId(requireText(row, "meeting_id", operation)),
2059
- originalStart: when(row, "original_start") ?? requireText(row, "original_start", operation),
2060
- occurrenceStart: when(row, "occurrence_start") ?? requireText(row, "occurrence_start", operation),
2061
- durationMinutes: num2(row, "duration_minutes"),
2062
- // An unknown state is shown as scheduled, never dropped: the row is real.
2063
- state: OCCURRENCE_STATES.includes(state) ? state : "scheduled",
2064
- sequence: num2(row, "sequence") ?? 0
2065
- };
2066
- }
2067
- function projectUpcoming(row) {
2068
- const operation = "meet.projectUpcoming";
2069
- const kind = text(row, "kind");
2070
- const myRole = text(row, "my_role");
2071
- const myRsvp = text(row, "my_rsvp");
2072
- return {
2073
- ...projectOccurrence(row),
2074
- organizationId: asOrganizationId(requireText(row, "organization_id", operation)),
2075
- title: text(row, "title") ?? "Meeting",
2076
- slug: requireText(row, "slug", operation),
2077
- kind: kind === "scheduled" || kind === "recurring" ? kind : "instant",
2078
- hostUserId: asUserId(requireText(row, "host_user_id", operation)),
2079
- timeZone: text(row, "time_zone") ?? "UTC",
2080
- meetingCancelled: bool(row, "meeting_cancelled", false),
2081
- myRsvp: myRsvp === null ? null : rsvpState(myRsvp),
2082
- myRole: myRole === "host" || myRole === "cohost" || myRole === "invitee" ? myRole : null
2083
- };
2084
- }
2085
- function projectOverride(row) {
2086
- const operation = "meet.projectOverride";
2087
- return {
2088
- id: requireText(row, "id", operation),
2089
- meetingId: asMeetingId(requireText(row, "meeting_id", operation)),
2090
- originalStart: when(row, "original_start") ?? requireText(row, "original_start", operation),
2091
- state: text(row, "state") === "moved" ? "moved" : "cancelled",
2092
- newStart: when(row, "new_start"),
2093
- newDurationMinutes: num2(row, "new_duration_minutes"),
2094
- reason: text(row, "reason"),
2095
- calendarSequence: num2(row, "calendar_sequence") ?? 0,
2096
- deletedAt: when(row, "deleted_at")
2097
- };
2098
- }
2099
- function projectRsvpInvitation(body) {
2100
- const operation = "meet.projectRsvpInvitation";
2101
- if (typeof body !== "object" || body === null) {
2102
- throw invalidResponse(operation, "the RSVP door returned no object");
2103
- }
2104
- const row = body;
2105
- if (row["ok"] !== true) {
1971
+ function projectAttendee(row) {
1972
+ const operation = "meet.projectAttendee";
1973
+ const role = text(row, "role");
1974
+ const admission = text(row, "admission_state");
1975
+ const known = ["host", "cohost", "presenter", "participant", "viewer"];
1976
+ const states = [
1977
+ "waiting_host",
1978
+ "knocking",
1979
+ "admitted",
1980
+ "denied",
1981
+ "expired",
1982
+ "withdrawn",
1983
+ "removed",
1984
+ "ended"
1985
+ ];
1986
+ const userId = text(row, "participant_user_id");
1987
+ const identity = requireText(row, "identity", operation);
2106
1988
  return {
2107
- ok: false,
2108
- message: text(row, "message") ?? "This invitation link is no longer valid \u2014 ask the host to send it again."
1989
+ id: requireText(row, "id", operation),
1990
+ meetingId: asMeetingId(requireText(row, "meeting_id", operation)),
1991
+ identity: asParticipantIdentity(identity),
1992
+ // A nameless attendee is a row nobody can read. The identity is a worse
1993
+ // name than a real one and an infinitely better one than a blank.
1994
+ displayName: text(row, "display_name") ?? identity,
1995
+ avatarUrl: text(row, "avatar_url"),
1996
+ userId: userId === null ? null : asUserId(userId),
1997
+ role: known.includes(role) ? role : "participant",
1998
+ admissionState: states.includes(admission) ? admission : "admitted",
1999
+ knockedAt: text(row, "knocked_at"),
2000
+ knockExpiresAt: text(row, "knock_expires_at"),
2001
+ isAgent: bool(row, "is_agent", false),
2002
+ joinedAt: text(row, "joined_at"),
2003
+ leftAt: text(row, "left_at"),
2004
+ sessions: projectStays(row["sessions"])
2109
2005
  };
2110
2006
  }
2111
- const meeting = row["meeting"] ?? {};
2112
- const invitee = row["invitee"] ?? {};
2113
- return {
2114
- ok: true,
2115
- meeting: {
2116
- title: text(meeting, "title") ?? "Meeting",
2117
- slug: requireText(meeting, "slug", operation),
2118
- scheduledFor: when(meeting, "scheduled_for"),
2119
- durationMinutes: num2(meeting, "duration_minutes"),
2120
- timeZone: text(meeting, "time_zone") ?? "UTC",
2121
- agenda: text(meeting, "agenda"),
2122
- recurrenceRule: text(meeting, "recurrence_rule"),
2123
- cancelledAt: when(meeting, "cancelled_at"),
2124
- cancellationReason: text(meeting, "cancellation_reason"),
2125
- endedAt: when(meeting, "ended_at")
2126
- },
2127
- invitee: {
2128
- displayName: text(invitee, "display_name"),
2129
- email: text(invitee, "email"),
2130
- rsvpState: rsvpState(text(invitee, "rsvp_state")),
2131
- respondedAt: when(invitee, "responded_at")
2132
- }
2133
- };
2134
- }
2135
- function settingsPatch(settings) {
2136
- const patch = {};
2137
- if (settings.lobbyEnabled !== void 0) patch["lobby_enabled"] = settings.lobbyEnabled;
2138
- if (settings.recordingPolicy !== void 0) patch["recording_policy"] = settings.recordingPolicy;
2139
- if (settings.aiEnabled !== void 0) patch["ai_enabled"] = settings.aiEnabled;
2140
- if (settings.joinBeforeHost !== void 0) patch["join_before_host"] = settings.joinBeforeHost;
2141
- return patch;
2142
- }
2143
- function changesPatch(changes) {
2144
- const patch = settingsPatch(changes);
2145
- if (changes.title !== void 0) patch["title"] = changes.title;
2146
- if (changes.agenda !== void 0) patch["agenda"] = changes.agenda;
2147
- if (changes.timeZone !== void 0) patch["time_zone"] = changes.timeZone;
2148
- if (changes.scheduledFor !== void 0) patch["scheduled_for"] = changes.scheduledFor;
2149
- if (changes.scheduledDurationMinutes !== void 0) {
2150
- patch["scheduled_duration_minutes"] = changes.scheduledDurationMinutes;
2151
- }
2152
- if (changes.recurrenceRule !== void 0) patch["recurrence_rule"] = changes.recurrenceRule;
2153
- if (changes.keepExceptions !== void 0) patch["keep_exceptions"] = changes.keepExceptions;
2154
- return patch;
2155
- }
2156
- function createMeetRepository(options) {
2157
- const db = options.client.schema(MEET_SCHEMA);
2158
- async function guardVoid(operation, run) {
2159
- let response = await run();
2160
- if (response.error !== null) {
2161
- const normalized = normalizeMeetError(response.error, operation);
2162
- if (normalized.code === "session-unavailable" && options.session !== void 0) {
2163
- await options.session();
2164
- response = await run();
2165
- }
2166
- if (response.error !== null) throw normalizeMeetError(response.error, operation);
2167
- }
2168
- }
2169
- async function guard(operation, run) {
2170
- let response = await run();
2171
- if (response.error !== null) {
2172
- const normalized = normalizeMeetError(response.error, operation);
2173
- if (normalized.code === "session-unavailable" && options.session !== void 0) {
2174
- await options.session();
2175
- response = await run();
2176
- }
2177
- if (response.error !== null) throw normalizeMeetError(response.error, operation);
2178
- }
2179
- if (response.data === null) {
2180
- throw invalidResponse(operation, "the database returned no row");
2181
- }
2182
- return response.data;
2183
- }
2184
- function projectInvite(row) {
2185
- const operation = "meet.projectInvite";
2186
- const mode = text(row, "mode");
2187
- return {
2188
- id: asCallId(requireText(row, "id", operation)),
2189
- organizationId: asOrganizationId(requireText(row, "organization_id", operation)),
2190
- roomName: asRoomName(requireText(row, "room_name", operation)),
2191
- mode: mode === "video" ? "video" : "audio",
2192
- callerUserId: asUserId(requireText(row, "caller_user_id", operation)),
2193
- callerName: text(row, "caller_name") ?? "Someone",
2194
- callerAvatarUrl: text(row, "caller_avatar_url"),
2195
- calleeUserIds: stringArray(row, "callee_ids").map(asUserId),
2196
- conversationId: text(row, "conversation_id"),
2197
- createdAt: requireText(row, "created_at", operation),
2198
- expiresAt: text(row, "expires_at") ?? requireText(row, "created_at", operation),
2199
- state: callState(text(row, "state")),
2200
- declineMessage: text(row, "decline_message")
2201
- };
2202
- }
2203
- function projectMeeting(row) {
2204
- const operation = "meet.projectMeeting";
2205
- const kind = text(row, "kind");
2206
- const policy = text(row, "recording_policy");
2207
- const duration = row["scheduled_duration_minutes"];
2208
- const metadata = row["metadata"];
2209
- return {
2210
- id: asMeetingId(requireText(row, "id", operation)),
2211
- organizationId: asOrganizationId(requireText(row, "organization_id", operation)),
2212
- roomName: asRoomName(requireText(row, "room_name", operation)),
2213
- slug: requireText(row, "slug", operation),
2214
- title: text(row, "title") ?? "Meeting",
2215
- kind: kind === "scheduled" || kind === "recurring" ? kind : "instant",
2216
- hostUserId: asUserId(requireText(row, "host_user_id", operation)),
2217
- scheduledFor: text(row, "scheduled_for"),
2218
- scheduledDurationMinutes: typeof duration === "number" ? duration : null,
2219
- startedAt: text(row, "started_at"),
2220
- endedAt: text(row, "ended_at"),
2221
- locked: bool(row, "locked", false),
2222
- lobbyEnabled: bool(row, "lobby_enabled", true),
2223
- recordingPolicy: policy === "disabled" || policy === "always-on" ? policy : "host-controlled",
2224
- aiEnabled: bool(row, "ai_enabled", true),
2225
- metadata: typeof metadata === "object" && metadata !== null && !Array.isArray(metadata) ? metadata : {},
2226
- timeZone: text(row, "time_zone") ?? "UTC",
2227
- agenda: text(row, "agenda"),
2228
- recurrenceRule: text(row, "recurrence_rule"),
2229
- joinBeforeHost: bool(row, "join_before_host", true),
2230
- cancelledAt: text(row, "cancelled_at"),
2231
- cancellationReason: text(row, "cancellation_reason"),
2232
- deletedAt: text(row, "deleted_at"),
2233
- calendarSequence: typeof row["calendar_sequence"] === "number" ? row["calendar_sequence"] : 0,
2234
- version: typeof row["version"] === "number" ? row["version"] : 1
2235
- };
2236
- }
2237
- function projectRecording(row) {
2238
- const operation = "meet.projectRecording";
2239
- const state = text(row, "state");
2240
- const known = [
2241
- "idle",
2242
- "starting",
2243
- "recording",
2244
- "stopping",
2245
- "processing",
2246
- "available",
2247
- "expired",
2248
- "failed"
2249
- ];
2250
- return {
2251
- id: requireText(row, "id", operation),
2252
- meetingId: asMeetingId(requireText(row, "meeting_id", operation)),
2253
- state: known.includes(state) ? state : "failed",
2254
- startedAt: text(row, "started_at"),
2255
- endedAt: text(row, "ended_at"),
2256
- startedBy: text(row, "started_by") === null ? null : asUserId(text(row, "started_by")),
2257
- // A PLATFORM file id, never a provider URL (R3). A recording that only
2258
- // exists as a LiveKit URL is orphaned the day the room is garbage
2259
- // collected, which is the whole reason this column is not `url`.
2260
- fileId: text(row, "file_id"),
2261
- failureReason: text(row, "failure_reason"),
2262
- retention: projectRecordingRetention(row["metadata"])
2263
- };
2264
- }
2265
- function projectRecordingRetention(value) {
2266
- if (typeof value !== "object" || value === null || Array.isArray(value)) return null;
2267
- const retention = value["retention"];
2268
- if (typeof retention !== "object" || retention === null || Array.isArray(retention)) return null;
2269
- const row = retention;
2270
- const phase = row["phase"];
2271
- const operationId = row["operation_id"];
2272
- const policyId = row["policy_id"];
2273
- const policyUpdatedAt = row["policy_updated_at"];
2274
- const dueAt = row["due_at"];
2275
- const expiredAt = row["expired_at"];
2276
- if (phase !== "warning" && phase !== "expired" || typeof operationId !== "string" || typeof policyId !== "string" || typeof policyUpdatedAt !== "string" || typeof dueAt !== "string" || expiredAt !== null && typeof expiredAt !== "string") return null;
2277
- return { operationId, policyId, policyUpdatedAt, phase, dueAt, expiredAt };
2278
- }
2279
- function projectAttendee(row) {
2280
- const operation = "meet.projectAttendee";
2281
- const role = text(row, "role");
2282
- const admission = text(row, "admission_state");
2283
- const known = ["host", "cohost", "presenter", "participant", "viewer"];
2284
- const states = ["knocking", "admitted", "denied", "removed"];
2285
- const userId = text(row, "participant_user_id");
2286
- const identity = requireText(row, "identity", operation);
2287
- return {
2288
- id: requireText(row, "id", operation),
2289
- meetingId: asMeetingId(requireText(row, "meeting_id", operation)),
2290
- identity: asParticipantIdentity(identity),
2291
- // A nameless attendee is a row nobody can read. The identity is a worse
2292
- // name than a real one and an infinitely better one than a blank.
2293
- displayName: text(row, "display_name") ?? identity,
2294
- avatarUrl: text(row, "avatar_url"),
2295
- userId: userId === null ? null : asUserId(userId),
2296
- role: known.includes(role) ? role : "participant",
2297
- admissionState: states.includes(admission) ? admission : "admitted",
2298
- isAgent: bool(row, "is_agent", false),
2299
- joinedAt: text(row, "joined_at"),
2300
- leftAt: text(row, "left_at"),
2301
- sessions: projectStays(row["sessions"])
2302
- };
2303
- }
2304
- function requireOrg(organizationId, operation) {
2305
- if (typeof organizationId !== "string" || organizationId.length === 0) {
2306
- throw new MeetError(
2307
- "misconfigured",
2308
- `${operation}: no organization_id`,
2309
- "Every meet write carries an explicit organization_id (R5). The package refuses the write rather than letting an unscoped row reach the database."
2310
- );
2007
+ function requireOrg(organizationId, operation) {
2008
+ if (typeof organizationId !== "string" || organizationId.length === 0) {
2009
+ throw new MeetError(
2010
+ "misconfigured",
2011
+ `${operation}: no organization_id`,
2012
+ "Every meet write carries an explicit organization_id (R5). The package refuses the write rather than letting an unscoped row reach the database."
2013
+ );
2311
2014
  }
2312
2015
  }
2313
2016
  return {
@@ -2333,7 +2036,7 @@ function createMeetRepository(options) {
2333
2036
  p_duration_minutes: input.durationMinutes ?? null,
2334
2037
  p_agenda: input.agenda ?? null,
2335
2038
  p_recurrence_rule: input.recurrenceRule ?? null,
2336
- p_settings: settingsPatch(input.settings ?? {})
2039
+ p_settings: scheduleSettingsPatch(input.settings ?? {}, input.appPanel, input.observers)
2337
2040
  })
2338
2041
  );
2339
2042
  return projectMeeting(row);
@@ -2573,16 +2276,26 @@ function createMeetRepository(options) {
2573
2276
  hasMore: rows.length > limit
2574
2277
  };
2575
2278
  },
2576
- async admit({ meetingId, identity, admitted, byUserId }) {
2577
- await guardVoid(
2578
- "meet.admit",
2579
- () => db.rpc(RPCS.admit, {
2279
+ async decideAdmission({ meetingId, identity, decision, byUserId }) {
2280
+ const fn = decision === "admit" ? RPCS.admit : decision === "deny" ? RPCS.deny : RPCS.undeny;
2281
+ const row = await guard(
2282
+ `meet.${decision}`,
2283
+ () => db.rpc(fn, {
2580
2284
  p_meeting_id: meetingId,
2581
2285
  p_identity: identity,
2582
- p_admitted: admitted,
2583
2286
  p_by_user_id: byUserId
2584
2287
  })
2585
2288
  );
2289
+ return projectAttendee(row);
2290
+ },
2291
+ async admitAll(meetingId, byUserId) {
2292
+ const response = await db.rpc(RPCS.admitAll, {
2293
+ p_meeting_id: meetingId,
2294
+ p_by_user_id: byUserId
2295
+ });
2296
+ if (response.error !== null) throw normalizeMeetError(response.error, "meet.admitAll");
2297
+ const rows = Array.isArray(response.data) ? response.data : [];
2298
+ return rows.map(projectAttendee);
2586
2299
  },
2587
2300
  async setLock(meetingId, locked, byUserId) {
2588
2301
  const row = await guard(
@@ -2656,76 +2369,449 @@ function createMeetRepository(options) {
2656
2369
  );
2657
2370
  }
2658
2371
  }
2659
- };
2372
+ };
2373
+ }
2374
+ var LIBRARY_STATES = [
2375
+ "idle",
2376
+ "starting",
2377
+ "recording",
2378
+ "stopping",
2379
+ "processing",
2380
+ "available",
2381
+ "expired",
2382
+ "failed"
2383
+ ];
2384
+ function projectLibraryRecording(row) {
2385
+ const operation = "meet.projectLibraryRecording";
2386
+ const state = text(row, "state");
2387
+ const role = text(row, "my_role");
2388
+ return {
2389
+ id: requireText(row, "recording_id", operation),
2390
+ meetingId: asMeetingId(requireText(row, "meeting_id", operation)),
2391
+ meetingTitle: text(row, "meeting_title") ?? "Meeting",
2392
+ meetingSlug: requireText(row, "meeting_slug", operation),
2393
+ role: role === "host" || role === "cohost" || role === "invitee" ? role : "attendee",
2394
+ state: LIBRARY_STATES.includes(state) ? state : "failed",
2395
+ title: text(row, "recording_title"),
2396
+ fileId: text(row, "file_id"),
2397
+ startedAt: text(row, "started_at"),
2398
+ endedAt: text(row, "ended_at"),
2399
+ durationMs: num(row, "duration_ms"),
2400
+ sizeBytes: num(row, "size_bytes"),
2401
+ failureReason: text(row, "failure_reason"),
2402
+ archivedAt: text(row, "deleted_at"),
2403
+ organizationId: asOrganizationId(requireText(row, "organization_id", operation))
2404
+ };
2405
+ }
2406
+ var SEARCH_SOURCES = [
2407
+ "title",
2408
+ "summary",
2409
+ "decision",
2410
+ "action_item",
2411
+ "note",
2412
+ "transcript"
2413
+ ];
2414
+ function projectSearchHit(row) {
2415
+ const operation = "meet.projectSearchHit";
2416
+ const source = text(row, "source");
2417
+ return {
2418
+ meetingId: asMeetingId(requireText(row, "meeting_id", operation)),
2419
+ meetingTitle: text(row, "meeting_title") ?? "Meeting",
2420
+ meetingSlug: requireText(row, "meeting_slug", operation),
2421
+ meetingAt: text(row, "meeting_at"),
2422
+ source: SEARCH_SOURCES.includes(source) ? source : "note",
2423
+ refId: requireText(row, "ref_id", operation),
2424
+ speaker: text(row, "speaker"),
2425
+ snippet: text(row, "snippet") ?? "",
2426
+ at: text(row, "said_at"),
2427
+ rank: num(row, "rank") ?? 0
2428
+ };
2429
+ }
2430
+ function snippetRuns(snippet) {
2431
+ const runs = [];
2432
+ const pattern = /⟦([^⟧]*)⟧/g;
2433
+ let last = 0;
2434
+ for (const match of snippet.matchAll(pattern)) {
2435
+ const start = match.index ?? 0;
2436
+ if (start > last) runs.push({ text: snippet.slice(last, start), hit: false });
2437
+ runs.push({ text: match[1] ?? "", hit: true });
2438
+ last = start + match[0].length;
2439
+ }
2440
+ if (last < snippet.length) runs.push({ text: snippet.slice(last), hit: false });
2441
+ return runs;
2442
+ }
2443
+
2444
+ // src/core/app-panel.ts
2445
+ function appPanelOf(meeting) {
2446
+ const raw = meeting?.metadata?.["app_panel"];
2447
+ if (typeof raw !== "object" || raw === null || Array.isArray(raw)) return null;
2448
+ const key = raw["key"];
2449
+ const recordId = raw["record_id"];
2450
+ if (typeof key !== "string" || key.length === 0) return null;
2451
+ if (typeof recordId !== "string" || recordId.length === 0) return null;
2452
+ return { key, recordId };
2453
+ }
2454
+ function scheduleSettingsPatch(settings, appPanel, observers) {
2455
+ const patch = settingsPatch(settings);
2456
+ if (appPanel !== void 0) patch["app_panel"] = { key: appPanel.key, record_id: appPanel.recordId };
2457
+ if (observers !== void 0 && observers.length > 0) patch["observers"] = [...observers];
2458
+ return patch;
2459
+ }
2460
+ function focusFromRow(value) {
2461
+ if (typeof value !== "object" || value === null || Array.isArray(value)) return null;
2462
+ const row = value;
2463
+ if (typeof row["panel"] !== "string") return null;
2464
+ return {
2465
+ panel: row["panel"],
2466
+ section: typeof row["section"] === "string" ? row["section"] : "",
2467
+ byName: typeof row["by_name"] === "string" ? row["by_name"] : "",
2468
+ at: typeof row["at"] === "string" ? row["at"] : ""
2469
+ };
2470
+ }
2471
+
2472
+ // src/core/collab.ts
2473
+ var EMPTY_COLLAB = {
2474
+ loaded: false,
2475
+ error: null,
2476
+ settings: null,
2477
+ polls: [],
2478
+ questions: [],
2479
+ strokes: [],
2480
+ breakout: null,
2481
+ broadcast: null,
2482
+ moveRequest: null,
2483
+ panelRequest: null,
2484
+ captionLanguage: null,
2485
+ captionTranslations: {},
2486
+ captionTranslationError: null,
2487
+ focus: null
2488
+ };
2489
+ function obj(value) {
2490
+ return typeof value === "object" && value !== null && !Array.isArray(value) ? value : null;
2491
+ }
2492
+ function str(value) {
2493
+ return typeof value === "string" ? value : null;
2494
+ }
2495
+ function num2(value) {
2496
+ return typeof value === "number" && Number.isFinite(value) ? value : null;
2497
+ }
2498
+ function strings(value) {
2499
+ return Array.isArray(value) ? value.filter((v) => typeof v === "string") : [];
2500
+ }
2501
+ function pollFromRow(row) {
2502
+ const id = str(row["id"]);
2503
+ if (id === null) return null;
2504
+ const options = [];
2505
+ for (const raw of Array.isArray(row["options"]) ? row["options"] : []) {
2506
+ const option = obj(raw);
2507
+ const optionId = option === null ? null : str(option["id"]);
2508
+ if (option === null || optionId === null) continue;
2509
+ const votes = num2(option["votes"]);
2510
+ options.push({
2511
+ id: optionId,
2512
+ text: str(option["text"]) ?? "",
2513
+ ...votes !== null ? { votes } : {},
2514
+ ...Array.isArray(option["voters"]) ? { voters: strings(option["voters"]) } : {}
2515
+ });
2516
+ }
2517
+ const state = str(row["state"]);
2518
+ return {
2519
+ id,
2520
+ question: str(row["question"]) ?? "",
2521
+ options,
2522
+ multiple: row["multiple"] === true,
2523
+ anonymous: row["anonymous"] === true,
2524
+ state: state === "live" || state === "closed" ? state : "draft",
2525
+ resultsShared: row["results_shared"] === true,
2526
+ voterCount: num2(row["voter_count"]),
2527
+ myVote: strings(row["my_vote"]),
2528
+ createdByName: str(row["created_by_name"]) ?? "",
2529
+ createdAt: str(row["created_at"])
2530
+ };
2531
+ }
2532
+ function questionFromRow(row) {
2533
+ const id = str(row["id"]);
2534
+ if (id === null) return null;
2535
+ const state = str(row["state"]);
2536
+ return {
2537
+ id,
2538
+ text: str(row["text"]) ?? "",
2539
+ askerName: str(row["asker_name"]) ?? "",
2540
+ anonymous: row["anonymous"] === true,
2541
+ state: state === "answered" || state === "dismissed" ? state : "open",
2542
+ answer: str(row["answer"]),
2543
+ answeredByName: str(row["answered_by_name"]),
2544
+ upvotes: num2(row["upvotes"]) ?? 0,
2545
+ upvotedByMe: row["upvoted_by_me"] === true,
2546
+ mine: row["mine"] === true,
2547
+ createdAt: str(row["created_at"])
2548
+ };
2549
+ }
2550
+ function strokeFromRow(row) {
2551
+ const id = str(row["id"]);
2552
+ if (id === null || !Array.isArray(row["points"])) return null;
2553
+ const points = [];
2554
+ for (const raw of row["points"]) {
2555
+ if (Array.isArray(raw) && typeof raw[0] === "number" && typeof raw[1] === "number") {
2556
+ points.push([raw[0], raw[1]]);
2557
+ }
2558
+ }
2559
+ return {
2560
+ id,
2561
+ points,
2562
+ color: str(row["color"]) ?? "#111827",
2563
+ width: num2(row["width"]) ?? 3,
2564
+ tool: row["tool"] === "highlighter" ? "highlighter" : "pen",
2565
+ by: str(row["by"]) ?? "",
2566
+ byName: str(row["by_name"]) ?? ""
2567
+ };
2568
+ }
2569
+ function broadcastFrom(value) {
2570
+ const row = obj(value);
2571
+ const text3 = row === null ? null : str(row["text"]);
2572
+ if (row === null || text3 === null) return null;
2573
+ return { text: text3, by: str(row["by"]) ?? "", at: str(row["at"]) ?? "" };
2574
+ }
2575
+ function planFromRow(row) {
2576
+ const id = str(row["id"]);
2577
+ if (id === null) return null;
2578
+ const rooms = [];
2579
+ for (const raw of Array.isArray(row["rooms"]) ? row["rooms"] : []) {
2580
+ const room = obj(raw);
2581
+ const roomId = room === null ? null : str(room["id"]);
2582
+ if (room === null || roomId === null) continue;
2583
+ rooms.push({ id: roomId, name: str(room["name"]) ?? roomId, identities: strings(room["identities"]) });
2584
+ }
2585
+ return {
2586
+ id,
2587
+ state: row["state"] === "open" ? "open" : "closed",
2588
+ rooms,
2589
+ allowChoose: row["allow_choose"] === true,
2590
+ minutes: num2(row["minutes"]) ?? 0,
2591
+ openedAt: str(row["opened_at"]),
2592
+ endsAt: str(row["ends_at"]),
2593
+ closedAt: str(row["closed_at"]),
2594
+ broadcasts: (Array.isArray(row["broadcasts"]) ? row["broadcasts"] : []).map(broadcastFrom).filter((b) => b !== null)
2595
+ };
2596
+ }
2597
+ function settingsFromRow(row) {
2598
+ if (row === null) return null;
2599
+ return {
2600
+ pollsEnabled: row["polls_enabled"] === true,
2601
+ pollMaxOptions: num2(row["poll_max_options"]) ?? 10,
2602
+ qaEnabled: row["qa_enabled"] === true,
2603
+ qaAnonymousAllowed: row["qa_anonymous_allowed"] === true,
2604
+ whiteboardEnabled: row["whiteboard_enabled"] === true,
2605
+ breakoutsEnabled: row["breakouts_enabled"] === true,
2606
+ breakoutMaxRooms: num2(row["breakout_max_rooms"]) ?? 20,
2607
+ breakoutDefaultMinutes: num2(row["breakout_default_minutes"]) ?? 15,
2608
+ captionTranslationEnabled: row["caption_translation_enabled"] === true,
2609
+ captionTranslationLanguage: str(row["caption_translation_language"]) ?? ""
2610
+ };
2611
+ }
2612
+ function collabFromState(body, previous) {
2613
+ const list = (key, parse) => (Array.isArray(body[key]) ? body[key] : []).map((raw) => {
2614
+ const row = obj(raw);
2615
+ return row === null ? null : parse(row);
2616
+ }).filter((item) => item !== null);
2617
+ const breakoutRow = obj(body["breakout"]);
2618
+ return {
2619
+ ...previous,
2620
+ loaded: true,
2621
+ error: null,
2622
+ settings: settingsFromRow(obj(body["settings"])),
2623
+ polls: list("polls", pollFromRow),
2624
+ questions: list("questions", questionFromRow),
2625
+ strokes: list("strokes", strokeFromRow),
2626
+ breakout: breakoutRow === null ? null : planFromRow(breakoutRow),
2627
+ focus: focusFromRow(body["focus"])
2628
+ };
2629
+ }
2630
+ function upsert(items, next) {
2631
+ const index = items.findIndex((item) => item.id === next.id);
2632
+ if (index === -1) return [...items, next];
2633
+ const copy = items.slice();
2634
+ copy[index] = next;
2635
+ return copy;
2636
+ }
2637
+ function withItem(collab, row, fromRoom = false) {
2638
+ const kind = str(row["kind"]);
2639
+ if (kind === "poll") {
2640
+ const poll = pollFromRow(row);
2641
+ if (poll === null) return collab;
2642
+ const known = collab.polls.find((p) => p.id === poll.id);
2643
+ let merged = fromRoom && known !== void 0 ? { ...poll, myVote: known.myVote } : poll;
2644
+ if (fromRoom && known !== void 0 && !poll.options.some((o) => o.votes !== void 0)) {
2645
+ const counted = new Map(known.options.map((o) => [o.id, o]));
2646
+ merged = {
2647
+ ...merged,
2648
+ voterCount: known.voterCount,
2649
+ options: merged.options.map((o) => {
2650
+ const had = counted.get(o.id);
2651
+ return had === void 0 ? o : { ...o, votes: had.votes, voters: had.voters };
2652
+ })
2653
+ };
2654
+ }
2655
+ return { ...collab, polls: upsert(collab.polls, merged) };
2656
+ }
2657
+ if (kind === "question") {
2658
+ const question = questionFromRow(row);
2659
+ if (question === null) return collab;
2660
+ const known = collab.questions.find((q) => q.id === question.id);
2661
+ const merged = fromRoom && known !== void 0 ? { ...question, mine: known.mine, upvotedByMe: known.upvotedByMe } : question;
2662
+ return { ...collab, questions: upsert(collab.questions, merged) };
2663
+ }
2664
+ if (kind === "stroke") {
2665
+ const stroke = strokeFromRow(row);
2666
+ if (stroke === null) return collab;
2667
+ return { ...collab, strokes: upsert(collab.strokes, stroke) };
2668
+ }
2669
+ if (kind === "breakout") {
2670
+ const plan = planFromRow(row);
2671
+ if (plan === null) return collab;
2672
+ return { ...collab, breakout: plan };
2673
+ }
2674
+ return collab;
2675
+ }
2676
+ function without(collab, ids) {
2677
+ const gone = new Set(ids);
2678
+ return {
2679
+ ...collab,
2680
+ polls: collab.polls.filter((p) => !gone.has(p.id)),
2681
+ questions: collab.questions.filter((q) => !gone.has(q.id)),
2682
+ strokes: collab.strokes.filter((s) => !gone.has(s.id))
2683
+ };
2684
+ }
2685
+ function applyCollabPacket(collab, payload, context) {
2686
+ const item = obj(payload["item"]);
2687
+ if (item !== null) {
2688
+ const kind = str(item["kind"]);
2689
+ let next = withItem(collab, item, true);
2690
+ if (kind === "poll" && str(item["state"]) === "live") {
2691
+ const before = collab.polls.find((p) => p.id === str(item["id"]));
2692
+ if (before === void 0 || before.state !== "live") {
2693
+ next = { ...next, panelRequest: { panel: "polls", at: context.now } };
2694
+ }
2695
+ }
2696
+ if (kind === "breakout") {
2697
+ const plan = planFromRow(item);
2698
+ if (plan !== null && plan.state === "closed") {
2699
+ next = { ...next, moveRequest: { roomId: null, at: context.now } };
2700
+ }
2701
+ }
2702
+ const move = obj(payload["move"]);
2703
+ if (move !== null && str(move["identity"]) === context.localIdentity) {
2704
+ next = { ...next, moveRequest: { roomId: str(move["room_id"]), at: context.now } };
2705
+ }
2706
+ return { next };
2707
+ }
2708
+ if (payload["focus"] !== void 0) {
2709
+ const focus = focusFromRow(payload["focus"]);
2710
+ return focus === null ? { next: null, dropped: "a focus packet without a panel" } : { next: { ...collab, focus } };
2711
+ }
2712
+ if (Array.isArray(payload["removed"])) {
2713
+ return { next: without(collab, strings(payload["removed"])) };
2714
+ }
2715
+ if (Array.isArray(payload["hidden"])) {
2716
+ return context.isHost ? { next: null, dropped: "hosts keep dismissed questions" } : { next: without(collab, strings(payload["hidden"])) };
2717
+ }
2718
+ if (payload["cleared"] === "whiteboard") {
2719
+ return { next: { ...collab, strokes: [] } };
2720
+ }
2721
+ const broadcast = broadcastFrom(payload["broadcast"]);
2722
+ if (broadcast !== null) {
2723
+ return { next: { ...collab, broadcast } };
2724
+ }
2725
+ return { next: null, dropped: "a collab packet with nothing this version understands" };
2660
2726
  }
2661
- var LIBRARY_STATES = [
2662
- "idle",
2663
- "starting",
2664
- "recording",
2665
- "stopping",
2666
- "processing",
2667
- "available",
2668
- "expired",
2669
- "failed"
2670
- ];
2671
- function projectLibraryRecording(row) {
2672
- const operation = "meet.projectLibraryRecording";
2673
- const state = text(row, "state");
2674
- const role = text(row, "my_role");
2675
- return {
2676
- id: requireText(row, "recording_id", operation),
2677
- meetingId: asMeetingId(requireText(row, "meeting_id", operation)),
2678
- meetingTitle: text(row, "meeting_title") ?? "Meeting",
2679
- meetingSlug: requireText(row, "meeting_slug", operation),
2680
- role: role === "host" || role === "cohost" || role === "invitee" ? role : "attendee",
2681
- state: LIBRARY_STATES.includes(state) ? state : "failed",
2682
- title: text(row, "recording_title"),
2683
- fileId: text(row, "file_id"),
2684
- startedAt: text(row, "started_at"),
2685
- endedAt: text(row, "ended_at"),
2686
- durationMs: num2(row, "duration_ms"),
2687
- sizeBytes: num2(row, "size_bytes"),
2688
- failureReason: text(row, "failure_reason"),
2689
- archivedAt: text(row, "deleted_at"),
2690
- organizationId: asOrganizationId(requireText(row, "organization_id", operation))
2691
- };
2727
+ function assignedRoom(plan, identity) {
2728
+ if (plan === null || plan.state !== "open" || identity === null) return null;
2729
+ return plan.rooms.find((room) => room.identities.includes(identity)) ?? null;
2692
2730
  }
2693
- var SEARCH_SOURCES = [
2694
- "title",
2695
- "summary",
2696
- "decision",
2697
- "action_item",
2698
- "note",
2699
- "transcript"
2700
- ];
2701
- function projectSearchHit(row) {
2702
- const operation = "meet.projectSearchHit";
2703
- const source = text(row, "source");
2704
- return {
2705
- meetingId: asMeetingId(requireText(row, "meeting_id", operation)),
2706
- meetingTitle: text(row, "meeting_title") ?? "Meeting",
2707
- meetingSlug: requireText(row, "meeting_slug", operation),
2708
- meetingAt: text(row, "meeting_at"),
2709
- source: SEARCH_SOURCES.includes(source) ? source : "note",
2710
- refId: requireText(row, "ref_id", operation),
2711
- speaker: text(row, "speaker"),
2712
- snippet: text(row, "snippet") ?? "",
2713
- at: text(row, "said_at"),
2714
- rank: num2(row, "rank") ?? 0
2715
- };
2731
+ function autoAssign(identities, rooms, exclude = []) {
2732
+ const count2 = Math.max(1, Math.floor(rooms));
2733
+ const out = Array.from({ length: count2 }, () => []);
2734
+ const skip = new Set(exclude);
2735
+ let index = 0;
2736
+ for (const identity of identities) {
2737
+ if (skip.has(identity)) continue;
2738
+ out[index % count2].push(identity);
2739
+ index += 1;
2740
+ }
2741
+ return out;
2716
2742
  }
2717
- function snippetRuns(snippet) {
2718
- const runs = [];
2719
- const pattern = /⟦([^⟧]*)⟧/g;
2720
- let last = 0;
2721
- for (const match of snippet.matchAll(pattern)) {
2722
- const start = match.index ?? 0;
2723
- if (start > last) runs.push({ text: snippet.slice(last, start), hit: false });
2724
- runs.push({ text: match[1] ?? "", hit: true });
2725
- last = start + match[0].length;
2743
+ function breakoutRemainingMs(plan, now) {
2744
+ if (plan === null || plan.state !== "open" || plan.endsAt === null) return null;
2745
+ const end = Date.parse(plan.endsAt);
2746
+ return Number.isNaN(end) ? null : Math.max(0, end - now);
2747
+ }
2748
+ function defaultCaptionLanguage(settings, viewerLanguage) {
2749
+ const configured = settings?.captionTranslationLanguage.trim() ?? "";
2750
+ if (configured.length > 0) return configured;
2751
+ const own = (viewerLanguage ?? "").trim();
2752
+ return own.length > 0 ? own : "en";
2753
+ }
2754
+ var CAPTION_LANGUAGES = [
2755
+ { tag: "en", name: "English" },
2756
+ { tag: "es", name: "Spanish" },
2757
+ { tag: "fr", name: "French" },
2758
+ { tag: "de", name: "German" },
2759
+ { tag: "it", name: "Italian" },
2760
+ { tag: "pt", name: "Portuguese" },
2761
+ { tag: "nl", name: "Dutch" },
2762
+ { tag: "pl", name: "Polish" },
2763
+ { tag: "ru", name: "Russian" },
2764
+ { tag: "uk", name: "Ukrainian" },
2765
+ { tag: "tr", name: "Turkish" },
2766
+ { tag: "ar", name: "Arabic" },
2767
+ { tag: "he", name: "Hebrew" },
2768
+ { tag: "fa", name: "Persian" },
2769
+ { tag: "hi", name: "Hindi" },
2770
+ { tag: "bn", name: "Bengali" },
2771
+ { tag: "zh", name: "Chinese" },
2772
+ { tag: "ja", name: "Japanese" },
2773
+ { tag: "ko", name: "Korean" },
2774
+ { tag: "vi", name: "Vietnamese" },
2775
+ { tag: "th", name: "Thai" },
2776
+ { tag: "id", name: "Indonesian" },
2777
+ { tag: "sv", name: "Swedish" }
2778
+ ];
2779
+ async function loadMeetingActivities(api, meetingId, meetingPass = null) {
2780
+ let response;
2781
+ try {
2782
+ response = await api.request({
2783
+ path: `${MEET_ROUTES.collab}?meeting_id=${encodeURIComponent(meetingId)}`,
2784
+ method: "GET",
2785
+ operation: "meet.loadMeetingActivities",
2786
+ requireAuth: meetingPass === null,
2787
+ ...meetingPass !== null ? { headers: { [MEETING_PASS_HEADER]: meetingPass } } : {}
2788
+ });
2789
+ } catch (error) {
2790
+ return {
2791
+ collab: null,
2792
+ refusal: {
2793
+ message: `Polls, questions and the whiteboard could not be read (${error.message}).`,
2794
+ remedy: error.remedy ?? "Reload the page."
2795
+ }
2796
+ };
2726
2797
  }
2727
- if (last < snippet.length) runs.push({ text: snippet.slice(last), hit: false });
2728
- return runs;
2798
+ if (!response.ok) {
2799
+ let body = null;
2800
+ try {
2801
+ body = await response.json();
2802
+ } catch {
2803
+ body = null;
2804
+ }
2805
+ const said = readServerRefusal(body);
2806
+ return {
2807
+ collab: null,
2808
+ refusal: {
2809
+ message: said?.reason ?? `Polls and questions could not be read (the server answered ${response.status}).`,
2810
+ remedy: said?.remedy || "Reload the page."
2811
+ }
2812
+ };
2813
+ }
2814
+ return { collab: collabFromState(await response.json(), EMPTY_COLLAB), refusal: null };
2729
2815
  }
2730
2816
 
2731
2817
  // src/core/names.ts
@@ -2930,6 +3016,7 @@ var NO_GRANTS = {
2930
3016
  };
2931
3017
  var EMPTY = {
2932
3018
  phase: "idle",
3019
+ waiting: null,
2933
3020
  meeting: null,
2934
3021
  localIdentity: null,
2935
3022
  grants: NO_GRANTS,
@@ -3797,7 +3884,10 @@ function orderParticipants(participants, activeSpeakers) {
3797
3884
  }
3798
3885
  function computeStage(args) {
3799
3886
  const pageSize = Math.max(1, args.pageSize ?? DEFAULT_PAGE_SIZE);
3800
- const ordered = orderParticipants(args.participants, args.activeSpeakers);
3887
+ const ordered = orderParticipants(
3888
+ args.participants.filter((participant) => !isObserverRole(participant.role)),
3889
+ args.activeSpeakers
3890
+ );
3801
3891
  const sharer = ordered.find((participant) => participant.screenSharing) ?? null;
3802
3892
  const pinnedParticipant = args.pinned === null ? null : ordered.find((participant) => participant.identity === args.pinned) ?? null;
3803
3893
  const spotlit = args.spotlight == null ? null : ordered.find((participant) => participant.identity === args.spotlight) ?? null;
@@ -4207,19 +4297,25 @@ function roleOf(participants, identity) {
4207
4297
  return participants.find((p) => p.identity === identity)?.role ?? "participant";
4208
4298
  }
4209
4299
  var END_MEETING_NOT_SERVED = "Ending a meeting is not available on this server yet";
4210
- function lobbyEntryFrom(attendee) {
4211
- if (attendee.admissionState !== "knocking") return null;
4300
+ function lobbyEntryFrom(attendee, now = Date.now()) {
4301
+ if (!WAITING_ADMISSION_STATES.includes(attendee.admissionState)) return null;
4302
+ const expires = attendee.knockExpiresAt ? Date.parse(attendee.knockExpiresAt) : Number.NaN;
4303
+ if (!Number.isNaN(expires) && expires <= now) return null;
4212
4304
  return {
4213
4305
  identity: attendee.identity,
4214
4306
  displayName: attendee.displayName,
4215
4307
  avatarUrl: attendee.avatarUrl,
4216
4308
  userId: attendee.userId,
4217
4309
  state: "knocking",
4218
- knockedAt: attendee.joinedAt ?? (/* @__PURE__ */ new Date()).toISOString(),
4310
+ knockedAt: attendee.knockedAt ?? attendee.joinedAt ?? (/* @__PURE__ */ new Date()).toISOString(),
4311
+ knockExpiresAt: attendee.knockExpiresAt ?? null,
4219
4312
  decidedAt: null,
4220
4313
  decidedBy: null
4221
4314
  };
4222
4315
  }
4316
+ function decidedState(state) {
4317
+ return WAITING_ADMISSION_STATES.includes(state) ? "expired" : state;
4318
+ }
4223
4319
  function createMeetingSession(options) {
4224
4320
  const { manager, repository, store, identity } = options;
4225
4321
  let channel = null;
@@ -4243,11 +4339,6 @@ function createMeetingSession(options) {
4243
4339
  }
4244
4340
  return identity.userId;
4245
4341
  }
4246
- function knockIdentity() {
4247
- return asParticipantIdentity(
4248
- identity.userId !== null ? `user:${identity.userId}` : `guest:${identity.deviceId}`
4249
- );
4250
- }
4251
4342
  function requireMeeting(operation) {
4252
4343
  const meeting = store.snapshot().meeting;
4253
4344
  if (meeting === null) {
@@ -4259,6 +4350,191 @@ function createMeetingSession(options) {
4259
4350
  }
4260
4351
  return meeting;
4261
4352
  }
4353
+ let polling = false;
4354
+ let pollTimer = null;
4355
+ let reading = null;
4356
+ let admittedOnce = false;
4357
+ let unlisten = null;
4358
+ function report(level, message, remedy) {
4359
+ options.onDiagnostic?.(remedy === void 0 ? { level, message } : { level, message, remedy });
4360
+ }
4361
+ function pollMs() {
4362
+ const seconds = store.snapshot().waiting?.pollSeconds ?? 3;
4363
+ return Math.max(1, Math.min(60, seconds)) * 1e3;
4364
+ }
4365
+ function schedule(ms2) {
4366
+ if (pollTimer !== null) clearTimeout(pollTimer);
4367
+ if (!polling) return;
4368
+ pollTimer = setTimeout(() => {
4369
+ pollTimer = null;
4370
+ void readState2().finally(() => schedule(pollMs()));
4371
+ }, ms2);
4372
+ }
4373
+ function listen() {
4374
+ if (unlisten !== null || typeof window === "undefined") return;
4375
+ const nudge = () => {
4376
+ if (typeof document !== "undefined" && document.visibilityState === "hidden") return;
4377
+ void readState2();
4378
+ };
4379
+ const goodbye = () => {
4380
+ const meeting = store.snapshot().meeting;
4381
+ const pass = options.resolvePass?.() ?? null;
4382
+ const waiting = store.snapshot().waiting;
4383
+ if (meeting === null || pass === null || waiting === null) return;
4384
+ if (!WAITING_ADMISSION_STATES.includes(waiting.admission)) return;
4385
+ try {
4386
+ navigator.sendBeacon?.(
4387
+ options.api.url(MEET_ROUTES.leave),
4388
+ new Blob([JSON.stringify({ meeting_id: meeting.id, intent: "withdraw", meeting_pass: pass })], {
4389
+ type: "application/json"
4390
+ })
4391
+ );
4392
+ } catch {
4393
+ }
4394
+ };
4395
+ window.addEventListener("online", nudge);
4396
+ window.addEventListener("pagehide", goodbye);
4397
+ document.addEventListener("visibilitychange", nudge);
4398
+ unlisten = () => {
4399
+ window.removeEventListener("online", nudge);
4400
+ window.removeEventListener("pagehide", goodbye);
4401
+ document.removeEventListener("visibilitychange", nudge);
4402
+ };
4403
+ }
4404
+ function stopWaiting() {
4405
+ polling = false;
4406
+ if (pollTimer !== null) clearTimeout(pollTimer);
4407
+ pollTimer = null;
4408
+ unlisten?.();
4409
+ unlisten = null;
4410
+ }
4411
+ function patchWaiting(next) {
4412
+ const current = store.snapshot().waiting;
4413
+ const base = current ?? {
4414
+ admission: "knocking",
4415
+ admittersPresent: null,
4416
+ reknockAllowed: true,
4417
+ knockExpiresAt: null,
4418
+ pollSeconds: 3,
4419
+ liveVersion: 0
4420
+ };
4421
+ store.patch({ waiting: { ...base, ...next } });
4422
+ }
4423
+ function apply(read) {
4424
+ const snapshot = store.snapshot();
4425
+ if (snapshot.phase !== "lobby") return;
4426
+ const admission = read.admission_state ?? null;
4427
+ const ended = admission === "ended" || read.session_phase === "ended" || read.meeting_phase === "ended";
4428
+ if (admission === "admitted" && !ended) {
4429
+ if (admittedOnce) return;
4430
+ admittedOnce = true;
4431
+ stopWaiting();
4432
+ options.onAdmitted?.();
4433
+ return;
4434
+ }
4435
+ if (ended) {
4436
+ stopWaiting();
4437
+ patchWaiting({ admission: "ended" });
4438
+ const meeting = snapshot.meeting;
4439
+ if (read.meeting_phase === "ended" && meeting !== null && meeting.endedAt === null) {
4440
+ store.patch({ meeting: { ...meeting, endedAt: (/* @__PURE__ */ new Date()).toISOString() } });
4441
+ }
4442
+ return;
4443
+ }
4444
+ if (admission === null) return;
4445
+ patchWaiting({
4446
+ admission,
4447
+ admittersPresent: read.admitters_present ?? null,
4448
+ reknockAllowed: read.reknock_allowed ?? true,
4449
+ knockExpiresAt: read.knock_expires_at ?? null,
4450
+ pollSeconds: read.poll_seconds ?? snapshot.waiting?.pollSeconds ?? 3,
4451
+ liveVersion: read.live_version ?? 0
4452
+ });
4453
+ if (admission === "denied" || admission === "expired" || admission === "withdrawn" || admission === "removed") {
4454
+ stopWaiting();
4455
+ }
4456
+ }
4457
+ async function readState2() {
4458
+ if (reading !== null) return reading;
4459
+ const meeting = store.snapshot().meeting;
4460
+ const pass = options.resolvePass?.() ?? null;
4461
+ if (meeting === null || pass === null) return;
4462
+ reading = (async () => {
4463
+ try {
4464
+ const response = await options.api.request({
4465
+ path: `${MEET_ROUTES.state}?meeting_id=${encodeURIComponent(meeting.id)}`,
4466
+ method: "GET",
4467
+ requireAuth: false,
4468
+ operation: "meet.state",
4469
+ headers: { [MEETING_PASS_HEADER]: pass }
4470
+ });
4471
+ if (!response.ok) {
4472
+ let body = null;
4473
+ try {
4474
+ body = await response.json();
4475
+ } catch {
4476
+ body = null;
4477
+ }
4478
+ const refusal2 = readServerRefusal(body);
4479
+ report(
4480
+ "warn",
4481
+ refusal2?.reason ?? `meet.state: the server answered ${response.status}`,
4482
+ refusal2?.remedy ?? "The waiting room keeps asking; nothing on this screen changes until the server answers."
4483
+ );
4484
+ return;
4485
+ }
4486
+ apply(await response.json());
4487
+ } catch (error) {
4488
+ report("info", `meet.state: ${String(error)}`, "Waiting continues; the state is read again shortly.");
4489
+ } finally {
4490
+ reading = null;
4491
+ }
4492
+ })();
4493
+ return reading;
4494
+ }
4495
+ async function postLeave(intent) {
4496
+ const meeting = requireMeeting(`meet.${intent}`);
4497
+ const pass = options.resolvePass?.() ?? null;
4498
+ const response = await options.api.request({
4499
+ path: MEET_ROUTES.leave,
4500
+ method: "POST",
4501
+ requireAuth: false,
4502
+ operation: `meet.${intent}`,
4503
+ body: { meeting_id: meeting.id, intent },
4504
+ ...pass !== null ? { headers: { [MEETING_PASS_HEADER]: pass } } : {}
4505
+ });
4506
+ if (!response.ok) {
4507
+ let body = null;
4508
+ try {
4509
+ body = await response.json();
4510
+ } catch {
4511
+ body = null;
4512
+ }
4513
+ const refusal2 = readServerRefusal(body);
4514
+ throw new MeetError(
4515
+ "transport",
4516
+ refusal2?.reason ?? `meet.${intent}: the server answered ${response.status}`,
4517
+ refusal2?.remedy ?? "You are still in the waiting room. Try again."
4518
+ );
4519
+ }
4520
+ }
4521
+ function hint() {
4522
+ channel?.send(MEET_EVENTS.stateChanged, { v: null });
4523
+ }
4524
+ async function refreshQueue() {
4525
+ const meeting = store.snapshot().meeting;
4526
+ if (meeting === null || isGuest(identity) || !isHostRole(effectiveRole(store.snapshot()))) return;
4527
+ try {
4528
+ const attendees = await repository.attendees(meeting.id);
4529
+ store.learn(attendees);
4530
+ for (const attendee of attendees) {
4531
+ const entry = lobbyEntryFrom(attendee);
4532
+ if (entry !== null) store.upsertLobby(entry);
4533
+ else store.setAdmission(attendee.identity, decidedState(attendee.admissionState), null);
4534
+ }
4535
+ } catch {
4536
+ }
4537
+ }
4262
4538
  return {
4263
4539
  watch(roomName) {
4264
4540
  channel?.close();
@@ -4266,58 +4542,22 @@ function createMeetingSession(options) {
4266
4542
  topic: roomTopic(roomName),
4267
4543
  broadcast: [
4268
4544
  {
4269
- event: MEET_EVENTS.knock,
4270
- onMessage: (message) => {
4271
- store.upsertLobby(message.data);
4272
- }
4273
- },
4274
- {
4275
- event: MEET_EVENTS.admission,
4276
- onMessage: (message) => {
4277
- const verdict = message.data;
4278
- const decided = asParticipantIdentity(verdict.identity);
4279
- store.setAdmission(
4280
- decided,
4281
- verdict.admitted ? "admitted" : "denied",
4282
- verdict.by === null ? null : verdict.by
4283
- );
4284
- if (decided === knockIdentity()) {
4285
- if (verdict.admitted) options.onAdmitted?.();
4286
- else options.onDenied?.();
4287
- }
4288
- }
4289
- },
4290
- {
4291
- event: MEET_EVENTS.lock,
4292
- onMessage: (message) => {
4293
- const { locked } = message.data;
4294
- const meeting = store.snapshot().meeting;
4295
- store.patch({
4296
- locked,
4297
- ...meeting !== null ? { meeting: { ...meeting, locked } } : {}
4298
- });
4299
- }
4300
- },
4301
- {
4302
- event: MEET_EVENTS.recording,
4303
- onMessage: (message) => {
4304
- const payload = message.data;
4305
- store.setRecording(
4306
- payload.state,
4307
- payload.by === null ? null : payload.by,
4308
- "room"
4309
- );
4545
+ // 🚨 PAYLOAD-FREE. Every receiver reads again; a spoofed hint only causes a read.
4546
+ event: MEET_EVENTS.stateChanged,
4547
+ onMessage: () => {
4548
+ if (store.snapshot().phase === "lobby") void readState2();
4549
+ void refreshQueue();
4310
4550
  }
4311
4551
  }
4312
4552
  ],
4313
- // THE BACKFILL DOOR. A host whose socket blipped must not miss a knock:
4314
- // re-reading the meeting restores lock/recording truth, and knockers
4315
- // re-announce on their own timer.
4553
+ // THE BACKFILL DOOR. A socket blip must not lose a decision: re-read the meeting
4554
+ // row, and a waiting tab re-reads its own state.
4316
4555
  onBackfill: async () => {
4317
4556
  const meeting = store.snapshot().meeting;
4318
4557
  if (meeting === null) return;
4319
4558
  const fresh = isGuest(identity) ? await repository.meetingBySlug(meeting.slug) : await readMeeting(meeting.id);
4320
4559
  store.patch({ meeting: fresh, locked: fresh.locked });
4560
+ if (store.snapshot().phase === "lobby") void readState2();
4321
4561
  }
4322
4562
  });
4323
4563
  },
@@ -4378,7 +4618,7 @@ function createMeetingSession(options) {
4378
4618
  store.upsertLobby(entry);
4379
4619
  return;
4380
4620
  }
4381
- store.setAdmission(attendee.identity, attendee.admissionState, null);
4621
+ store.setAdmission(attendee.identity, decidedState(attendee.admissionState), null);
4382
4622
  }
4383
4623
  },
4384
4624
  {
@@ -4430,62 +4670,75 @@ function createMeetingSession(options) {
4430
4670
  if (entry !== null) {
4431
4671
  store.upsertLobby(entry);
4432
4672
  } else {
4433
- store.setAdmission(attendee.identity, attendee.admissionState, null);
4673
+ store.setAdmission(attendee.identity, decidedState(attendee.admissionState), null);
4434
4674
  }
4435
4675
  }
4436
4676
  }
4437
4677
  });
4438
4678
  meetingChannel.requestBackfill("manual");
4439
4679
  },
4440
- knock(roomName) {
4441
- const handle = channel ?? manager.open({ topic: roomTopic(roomName) });
4442
- channel = handle;
4443
- const entry = {
4444
- identity: knockIdentity(),
4445
- displayName: identity.displayName,
4446
- avatarUrl: identity.avatarUrl,
4447
- userId: identity.userId,
4448
- state: "knocking",
4449
- knockedAt: (/* @__PURE__ */ new Date()).toISOString(),
4450
- decidedAt: null,
4451
- decidedBy: null
4452
- };
4453
- store.upsertLobby(entry);
4454
- handle.send(MEET_EVENTS.knock, entry);
4455
- },
4456
4680
  async admit(entry, admitted) {
4457
4681
  const by = requireHostUser("meet.admit");
4458
4682
  const meeting = requireMeeting("meet.admit");
4459
- await repository.admit({
4683
+ const row = await repository.decideAdmission({
4460
4684
  meetingId: meeting.id,
4461
4685
  identity: entry.identity,
4462
- admitted,
4686
+ decision: admitted ? "admit" : "deny",
4463
4687
  byUserId: by
4464
4688
  });
4465
4689
  store.upsertLobby({
4466
4690
  ...entry,
4467
- state: admitted ? "admitted" : "denied",
4691
+ state: decidedState(row.admissionState),
4468
4692
  decidedAt: (/* @__PURE__ */ new Date()).toISOString(),
4469
4693
  decidedBy: by
4470
4694
  });
4471
- channel?.send(MEET_EVENTS.admission, {
4695
+ hint();
4696
+ },
4697
+ async undeny(entry) {
4698
+ const by = requireHostUser("meet.undeny");
4699
+ const meeting = requireMeeting("meet.undeny");
4700
+ const row = await repository.decideAdmission({
4701
+ meetingId: meeting.id,
4472
4702
  identity: entry.identity,
4473
- admitted,
4474
- by
4703
+ decision: "undeny",
4704
+ byUserId: by
4475
4705
  });
4706
+ store.upsertLobby({ ...entry, state: decidedState(row.admissionState), decidedAt: (/* @__PURE__ */ new Date()).toISOString(), decidedBy: by });
4707
+ hint();
4476
4708
  },
4477
4709
  async admitAll() {
4478
- const knocking = store.snapshot().lobby.filter((entry) => entry.state === "knocking");
4479
- for (const entry of knocking) {
4480
- await this.admit(entry, true);
4710
+ const by = requireHostUser("meet.admitAll");
4711
+ const meeting = requireMeeting("meet.admitAll");
4712
+ const rows = await repository.admitAll(meeting.id, by);
4713
+ for (const row of rows) {
4714
+ store.setAdmission(row.identity, row.admissionState, by);
4481
4715
  }
4716
+ hint();
4717
+ },
4718
+ startWaiting() {
4719
+ if (polling) return;
4720
+ polling = true;
4721
+ admittedOnce = false;
4722
+ listen();
4723
+ schedule(0);
4724
+ },
4725
+ stopWaiting,
4726
+ readState: readState2,
4727
+ async withdraw() {
4728
+ stopWaiting();
4729
+ await postLeave("withdraw");
4730
+ patchWaiting({ admission: "withdrawn" });
4731
+ hint();
4732
+ },
4733
+ async askAgain() {
4734
+ await options.rejoin?.();
4482
4735
  },
4483
4736
  async setLock(locked) {
4484
4737
  const by = requireHostUser("meet.setLock");
4485
4738
  const meeting = requireMeeting("meet.setLock");
4486
4739
  const updated = await repository.setLock(meeting.id, locked, by);
4487
4740
  store.patch({ meeting: updated, locked: updated.locked });
4488
- channel?.send(MEET_EVENTS.lock, { locked: updated.locked });
4741
+ hint();
4489
4742
  return updated;
4490
4743
  },
4491
4744
  /**
@@ -4543,6 +4796,7 @@ function createMeetingSession(options) {
4543
4796
  return { endedAt, served: true };
4544
4797
  },
4545
4798
  dispose() {
4799
+ stopWaiting();
4546
4800
  channel?.close();
4547
4801
  channel = null;
4548
4802
  meetingChannel?.close();
@@ -5403,13 +5657,14 @@ function createRoomEngine(options) {
5403
5657
  }
5404
5658
  };
5405
5659
  async function connect(request) {
5406
- store.patch({ phase: "requesting-token", failure: null });
5660
+ store.patch({ phase: "requesting-token", failure: null, waiting: null });
5407
5661
  const token = await tokens.get({
5408
5662
  roomName: request.roomName,
5409
5663
  identity,
5410
5664
  ...request.meetingId !== void 0 ? { meetingId: request.meetingId } : {},
5411
5665
  ...request.slug !== void 0 ? { slug: request.slug } : {},
5412
- ...request.breakoutRoomId != null ? { breakoutRoomId: request.breakoutRoomId } : {}
5666
+ ...request.breakoutRoomId != null ? { breakoutRoomId: request.breakoutRoomId } : {},
5667
+ ...request.observe === true ? { observe: true } : {}
5413
5668
  });
5414
5669
  current = token;
5415
5670
  store.patch({
@@ -5420,14 +5675,25 @@ function createRoomEngine(options) {
5420
5675
  standing: token.standing,
5421
5676
  canRecord: token.canRecord,
5422
5677
  canPublish: token.canPublish,
5423
- canSubscribe: token.canSubscribe
5678
+ canSubscribe: token.canSubscribe,
5679
+ observersVisibleTo: token.observersVisibleTo ?? "everyone"
5424
5680
  }
5425
5681
  });
5426
- if (token.lobby) {
5427
- store.patch({ phase: "lobby" });
5682
+ if (token.lobby || !token.canSubscribe) {
5683
+ store.patch({
5684
+ phase: "lobby",
5685
+ waiting: {
5686
+ admission: token.admissionState !== void 0 && token.admissionState !== "admitted" ? token.admissionState : "knocking",
5687
+ admittersPresent: null,
5688
+ reknockAllowed: true,
5689
+ knockExpiresAt: null,
5690
+ pollSeconds: token.pollSeconds ?? 3,
5691
+ liveVersion: 0
5692
+ }
5693
+ });
5428
5694
  return;
5429
5695
  }
5430
- store.patch({ phase: "connecting" });
5696
+ store.patch({ phase: "connecting", waiting: null });
5431
5697
  const created = await options.driverFactory(handlers);
5432
5698
  if (disposed) {
5433
5699
  await created.disconnect();
@@ -5910,7 +6176,9 @@ function createInRoomClient(options) {
5910
6176
  ...request.allowChoose !== void 0 ? { allow_choose: request.allowChoose } : {},
5911
6177
  ...request.minutes !== void 0 ? { minutes: request.minutes } : {},
5912
6178
  ...request.identity !== void 0 ? { identity: request.identity } : {},
5913
- ...request.roomId !== void 0 ? { room_id: request.roomId } : {}
6179
+ ...request.roomId !== void 0 ? { room_id: request.roomId } : {},
6180
+ ...request.panel !== void 0 ? { panel: request.panel } : {},
6181
+ ...request.section !== void 0 ? { section: request.section } : {}
5914
6182
  },
5915
6183
  ...hdrs !== void 0 ? { headers: hdrs } : {}
5916
6184
  });
@@ -6686,7 +6954,7 @@ function observedPhase(input) {
6686
6954
  case "left":
6687
6955
  return "prejoin";
6688
6956
  case "lobby":
6689
- return "waiting:knocking";
6957
+ return waitingPhase(snapshot);
6690
6958
  case "requesting-token":
6691
6959
  case "connecting":
6692
6960
  return "joining";
@@ -6695,6 +6963,24 @@ function observedPhase(input) {
6695
6963
  return "in_call";
6696
6964
  }
6697
6965
  }
6966
+ function waitingPhase(snapshot) {
6967
+ switch (snapshot.waiting?.admission) {
6968
+ case "waiting_host":
6969
+ return "waiting:host_not_started";
6970
+ case "denied":
6971
+ return "denied";
6972
+ case "expired":
6973
+ return "knock_expired";
6974
+ case "withdrawn":
6975
+ return "left";
6976
+ case "ended":
6977
+ return "ended";
6978
+ case "removed":
6979
+ return "refused:removed";
6980
+ default:
6981
+ return "waiting:knocking";
6982
+ }
6983
+ }
6698
6984
  var ADMITTER_ROLES = /* @__PURE__ */ new Set(["host", "cohost"]);
6699
6985
  function observe(input) {
6700
6986
  const { snapshot } = input;
@@ -6709,7 +6995,7 @@ function observe(input) {
6709
6995
  reason: phase.startsWith("refused:") ? phase.slice("refused:".length) : null,
6710
6996
  role,
6711
6997
  connection: snapshot?.phase === "reconnecting" ? "reconnecting" : "stable",
6712
- hostPresent: inCall && snapshot !== null ? snapshot.participants.some((p) => ADMITTER_ROLES.has(p.role)) : null,
6998
+ hostPresent: inCall && snapshot !== null ? snapshot.participants.some((p) => ADMITTER_ROLES.has(p.role)) : snapshot?.phase === "lobby" ? snapshot.waiting?.admittersPresent ?? null : null,
6713
6999
  lobbyCount: canAdmit && snapshot !== null ? snapshot.lobby.filter((entry) => entry.state === "knocking").length : 0,
6714
7000
  recording: snapshot?.recording === "recording" || snapshot?.recording === "stopping",
6715
7001
  audioBlocked: null,