@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/core.js CHANGED
@@ -301,7 +301,17 @@ var MEET_ROUTES = {
301
301
  */
302
302
  collab: "/api/v1/meet/collab",
303
303
  /** Meet wave 5: translate finished caption lines into this viewer's language. */
304
- captionsTranslate: "/api/v1/meet/captions/translate"
304
+ captionsTranslate: "/api/v1/meet/captions/translate",
305
+ /**
306
+ * S4 — THE STATE READ (CORE-DESIGN §2.6): `GET ?meeting_id=` with `x-meet-pass`. This
307
+ * person's admission (lazy expiry applied), whether anyone who can admit is here, the run's
308
+ * phase. Read every `waiting_poll_seconds` while waiting and on every `mx.state.changed`.
309
+ */
310
+ state: "/api/v1/meet/state",
311
+ /** S4: `withdraw` (stop waiting) or `leave` (close this tab); the pass may ride in the body. */
312
+ leave: "/api/v1/meet/leave",
313
+ /** S4: a pass holder (guests included) saw the recording notice, for this run. */
314
+ consent: "/api/v1/meet/consent"
305
315
  };
306
316
  function meetingPath(template, meetingId) {
307
317
  return template.replace("{meeting_id}", encodeURIComponent(meetingId));
@@ -375,7 +385,10 @@ function parseRoomToken(body, roomName) {
375
385
  meetingPassExpiresAt: readString(payload, "meeting_pass_expires_at"),
376
386
  sessionId: readString(payload, "session_id"),
377
387
  lobby: readBoolean(payload, "lobby", false),
378
- breakoutRoomId: readString(payload, "breakout_room_id") ?? null
388
+ admissionState: readString(payload, "admission_state") ?? void 0,
389
+ pollSeconds: typeof payload["poll_seconds"] === "number" ? payload["poll_seconds"] : void 0,
390
+ breakoutRoomId: readString(payload, "breakout_room_id") ?? null,
391
+ observersVisibleTo: readString(payload, "observers_visible_to") === "hosts" ? "hosts" : "everyone"
379
392
  };
380
393
  }
381
394
  function createTokenClient(options) {
@@ -424,6 +437,7 @@ function createTokenClient(options) {
424
437
  ...input.meetingId !== void 0 ? { meeting_id: input.meetingId } : {},
425
438
  ...input.slug !== void 0 ? { slug: input.slug } : {},
426
439
  ...input.breakoutRoomId != null ? { breakout_room_id: input.breakoutRoomId } : {},
440
+ ...input.observe === true ? { observe: true } : {},
427
441
  organization_id: input.identity.organizationId,
428
442
  display_name: input.identity.displayName,
429
443
  device_id: input.identity.deviceId,
@@ -495,7 +509,8 @@ function createTokenClient(options) {
495
509
  return {
496
510
  get(input) {
497
511
  const entry = cache.get(input.roomName);
498
- const sameRoom = entry !== void 0 && (entry.token.breakoutRoomId ?? null) === (input.breakoutRoomId ?? null);
512
+ 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).
513
+ entry.token.role === "viewer" === (input.observe === true);
499
514
  if (entry !== void 0 && sameRoom) {
500
515
  if (entry.inFlight !== null) return entry.inFlight;
501
516
  if (!expired(entry)) return Promise.resolve(entry.token);
@@ -987,14 +1002,15 @@ var MEET_EVENTS = {
987
1002
  cancel: "mx.call.cancel",
988
1003
  /** You answered (or declined) on another device. Stop ringing here. */
989
1004
  settled: "mx.call.settled",
990
- /** Someone is in the lobby. Hosts render the knock. */
991
- knock: "mx.lobby.knock",
992
- /** A host decided. The knocker learns it here — they have no DB read. */
993
- admission: "mx.lobby.admission",
994
- /** The room was locked or unlocked. */
995
- lock: "mx.room.lock",
996
- /** Recording started or stopped. EVERY participant renders the indicator. */
997
- recording: "mx.room.recording"
1005
+ /**
1006
+ * 🚨 S4 (CORE-DESIGN §2.6): the ONE room-channel event — payload-free "something changed,
1007
+ * read again". Nothing a person must see rides it: a waiting person learns admitted /
1008
+ * denied / expired / ended by READING their row (`GET /v1/meet/state`) on a timer and on
1009
+ * every hint. The four payload broadcasts (`mx.lobby.knock`, `mx.lobby.admission`,
1010
+ * `mx.room.lock`, `mx.room.recording`) are gone with their senders and receivers. The
1011
+ * server publishes the same event on the same topic (aidream `admission.room_topic`).
1012
+ */
1013
+ stateChanged: "mx.state.changed"
998
1014
  };
999
1015
 
1000
1016
  // src/core/types.ts
@@ -1020,6 +1036,10 @@ function isLinkFollower(standing) {
1020
1036
  function isHostRole(role) {
1021
1037
  return role === "host" || role === "cohost";
1022
1038
  }
1039
+ function isObserverRole(role) {
1040
+ return role === "viewer";
1041
+ }
1042
+ var WAITING_ADMISSION_STATES = ["waiting_host", "knocking"];
1023
1043
 
1024
1044
  // src/core/calls.ts
1025
1045
  var TRANSITIONS = {
@@ -1268,813 +1288,492 @@ function createCallCenter(options) {
1268
1288
  };
1269
1289
  }
1270
1290
 
1271
- // src/core/collab.ts
1272
- var EMPTY_COLLAB = {
1273
- loaded: false,
1274
- error: null,
1275
- settings: null,
1276
- polls: [],
1277
- questions: [],
1278
- strokes: [],
1279
- breakout: null,
1280
- broadcast: null,
1281
- moveRequest: null,
1282
- panelRequest: null,
1283
- captionLanguage: null,
1284
- captionTranslations: {},
1285
- captionTranslationError: null
1291
+ // src/core/repository.ts
1292
+ var MEET_SCHEMA = "communication";
1293
+ var TABLES = {
1294
+ meetings: "meet_meetings",
1295
+ participants: "meet_participants",
1296
+ invites: "meet_call_invites",
1297
+ recordings: "meet_recordings",
1298
+ transcripts: "meet_transcript_segments",
1299
+ notes: "meet_notes",
1300
+ /** Wave 1 (0.8): who a meeting was sent to, and what they answered. */
1301
+ invitees: "meet_invitees",
1302
+ /** Wave 1 (0.8): one occurrence of a series cancelled or moved. */
1303
+ occurrenceOverrides: "meet_occurrence_overrides"
1286
1304
  };
1287
- function obj(value) {
1288
- return typeof value === "object" && value !== null && !Array.isArray(value) ? value : null;
1289
- }
1290
- function str(value) {
1291
- return typeof value === "string" ? value : null;
1292
- }
1293
- function num(value) {
1294
- return typeof value === "number" && Number.isFinite(value) ? value : null;
1305
+ var RPCS = {
1306
+ /** Atomic: create the invite row and stamp its expiry in one statement. */
1307
+ createCallInvite: "meet_create_call_invite",
1308
+ /** Atomic settle. Refuses a transition out of a terminal state. */
1309
+ settleCallInvite: "meet_settle_call_invite",
1310
+ /** Every invite still ringing this user — the reconnect backfill door. */
1311
+ pendingCallInvites: "meet_pending_call_invites",
1312
+ /** Instant meeting, or the durable one behind a recurring link. */
1313
+ getOrCreateMeeting: "meet_get_or_create_meeting",
1314
+ /** Resolve a durable public link, before/during/after its scheduled time. */
1315
+ meetingBySlug: "meet_meeting_by_slug",
1316
+ /*
1317
+ * ── S4 ADMISSION (CORE-DESIGN §2.4). Each returns the decided row; the caller then
1318
+ * sends the payload-free `mx.state.changed` hint. Authorized by `who_may_admit`.
1319
+ */
1320
+ admit: "meet_admit",
1321
+ deny: "meet_deny",
1322
+ undeny: "meet_undeny",
1323
+ admitAll: "meet_admit_all",
1324
+ setLock: "meet_set_lock",
1325
+ recordConsent: "meet_record_consent",
1326
+ /*
1327
+ * ── MEET WAVE 1 — management (0.8). Every write is a SECURITY DEFINER door
1328
+ * authorized by the meeting's host or a real iam grant (editor to change,
1329
+ * admin = co-host to archive or name a co-host) — never an organization
1330
+ * match. After any of these writes, call `announceMeeting` so the server
1331
+ * sends invitations / updates / cancellations and re-queues reminders.
1332
+ */
1333
+ /** Schedule with every setting at create; unset settings come from the knobs. */
1334
+ scheduleMeeting: "meet_schedule_meeting",
1335
+ /** Edit / reschedule — a JSON patch with a version compare-and-swap. */
1336
+ updateMeeting: "meet_update_meeting",
1337
+ /** Cancel is a state (`cancelled_at`); the link keeps resolving. */
1338
+ cancelMeeting: "meet_cancel_meeting",
1339
+ /** Archive (soft delete) — the invitations and exceptions cascade. */
1340
+ archiveMeeting: "meet_archive_meeting",
1341
+ restoreMeeting: "meet_restore_meeting",
1342
+ /** Cancel, move or restore ONE occurrence of a series by its original start. */
1343
+ setOccurrence: "meet_set_occurrence",
1344
+ addInvitees: "meet_add_invitees",
1345
+ setInviteeRole: "meet_set_invitee_role",
1346
+ removeInvitee: "meet_remove_invitee",
1347
+ /** A signed-in invitee answers for themselves. */
1348
+ respond: "meet_respond",
1349
+ /** The emailed RSVP link (no account): read and answer. */
1350
+ invitationByToken: "meet_invitation_by_token",
1351
+ respondByToken: "meet_respond_by_token",
1352
+ /** One meeting's occurrences between two instants (INVOKER — the reader's RLS). */
1353
+ meetingOccurrences: "meet_meeting_occurrences",
1354
+ /** Every occurrence the reader can see between two instants — the Upcoming list. */
1355
+ occurrencesBetween: "meet_occurrences_between",
1356
+ /*
1357
+ * ── MEET WAVE 3 — after the meeting. Both INVOKER (the reader's RLS) and both
1358
+ * scoped to the reader's OWN meetings (host, invitee, attended) — RLS is the
1359
+ * ceiling, never the view.
1360
+ */
1361
+ /** The recordings library. */
1362
+ myRecordings: "meet_my_recordings",
1363
+ /** Titles, agendas, current notes and final transcript lines; snippets mark hits with ⟦ ⟧. */
1364
+ search: "meet_search"
1365
+ };
1366
+ var VOID_RPCS = [RPCS.recordConsent];
1367
+ 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";
1368
+ 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";
1369
+ var RECORDING_COLUMNS = "id,meeting_id,state,started_at,ended_at,started_by,file_id,failure_reason";
1370
+ var SEGMENT_COLUMNS = "id,meeting_id,identity,speaker,text,started_at,is_final,metadata";
1371
+ var NOTE_COLUMNS = "id,meeting_id,text,kind,assignee_user_id,created_at,metadata";
1372
+ 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";
1373
+ function projectStays(value) {
1374
+ if (!Array.isArray(value)) return [];
1375
+ const stays = [];
1376
+ for (const item of value) {
1377
+ if (item === null || typeof item !== "object") continue;
1378
+ const joined = item["joined_at"];
1379
+ const left = item["left_at"];
1380
+ if (typeof joined !== "string" || joined.length === 0) continue;
1381
+ stays.push({ joinedAt: joined, leftAt: typeof left === "string" && left.length > 0 ? left : null });
1382
+ }
1383
+ return stays;
1295
1384
  }
1296
- function strings(value) {
1297
- return Array.isArray(value) ? value.filter((v) => typeof v === "string") : [];
1385
+ function text(row, key) {
1386
+ const value = row[key];
1387
+ return typeof value === "string" && value.length > 0 ? value : null;
1298
1388
  }
1299
- function pollFromRow(row) {
1300
- const id = str(row["id"]);
1301
- if (id === null) return null;
1302
- const options = [];
1303
- for (const raw of Array.isArray(row["options"]) ? row["options"] : []) {
1304
- const option = obj(raw);
1305
- const optionId = option === null ? null : str(option["id"]);
1306
- if (option === null || optionId === null) continue;
1307
- const votes = num(option["votes"]);
1308
- options.push({
1309
- id: optionId,
1310
- text: str(option["text"]) ?? "",
1311
- ...votes !== null ? { votes } : {},
1312
- ...Array.isArray(option["voters"]) ? { voters: strings(option["voters"]) } : {}
1313
- });
1389
+ function requireText(row, key, operation) {
1390
+ const value = text(row, key);
1391
+ if (value === null) {
1392
+ throw invalidResponse(operation, `the row is missing "${key}"`);
1314
1393
  }
1315
- const state = str(row["state"]);
1316
- return {
1317
- id,
1318
- question: str(row["question"]) ?? "",
1319
- options,
1320
- multiple: row["multiple"] === true,
1321
- anonymous: row["anonymous"] === true,
1322
- state: state === "live" || state === "closed" ? state : "draft",
1323
- resultsShared: row["results_shared"] === true,
1324
- voterCount: num(row["voter_count"]),
1325
- myVote: strings(row["my_vote"]),
1326
- createdByName: str(row["created_by_name"]) ?? "",
1327
- createdAt: str(row["created_at"])
1328
- };
1394
+ return value;
1329
1395
  }
1330
- function questionFromRow(row) {
1331
- const id = str(row["id"]);
1332
- if (id === null) return null;
1333
- const state = str(row["state"]);
1334
- return {
1335
- id,
1336
- text: str(row["text"]) ?? "",
1337
- askerName: str(row["asker_name"]) ?? "",
1338
- anonymous: row["anonymous"] === true,
1339
- state: state === "answered" || state === "dismissed" ? state : "open",
1340
- answer: str(row["answer"]),
1341
- answeredByName: str(row["answered_by_name"]),
1342
- upvotes: num(row["upvotes"]) ?? 0,
1343
- upvotedByMe: row["upvoted_by_me"] === true,
1344
- mine: row["mine"] === true,
1345
- createdAt: str(row["created_at"])
1346
- };
1396
+ function bool(row, key, fallback) {
1397
+ const value = row[key];
1398
+ return typeof value === "boolean" ? value : fallback;
1347
1399
  }
1348
- function strokeFromRow(row) {
1349
- const id = str(row["id"]);
1350
- if (id === null || !Array.isArray(row["points"])) return null;
1351
- const points = [];
1352
- for (const raw of row["points"]) {
1353
- if (Array.isArray(raw) && typeof raw[0] === "number" && typeof raw[1] === "number") {
1354
- points.push([raw[0], raw[1]]);
1355
- }
1356
- }
1357
- return {
1358
- id,
1359
- points,
1360
- color: str(row["color"]) ?? "#111827",
1361
- width: num(row["width"]) ?? 3,
1362
- tool: row["tool"] === "highlighter" ? "highlighter" : "pen",
1363
- by: str(row["by"]) ?? "",
1364
- byName: str(row["by_name"]) ?? ""
1365
- };
1400
+ function stringArray(row, key) {
1401
+ const value = row[key];
1402
+ if (!Array.isArray(value)) return [];
1403
+ return value.filter((entry) => typeof entry === "string");
1366
1404
  }
1367
- function broadcastFrom(value) {
1368
- const row = obj(value);
1369
- const text3 = row === null ? null : str(row["text"]);
1370
- if (row === null || text3 === null) return null;
1371
- return { text: text3, by: str(row["by"]) ?? "", at: str(row["at"]) ?? "" };
1405
+ var CALL_STATES = [
1406
+ "idle",
1407
+ "dialing",
1408
+ "ringing",
1409
+ "connecting",
1410
+ "active",
1411
+ "declined",
1412
+ "missed",
1413
+ "busy",
1414
+ "cancelled",
1415
+ "ended",
1416
+ "failed"
1417
+ ];
1418
+ function callState(value) {
1419
+ return CALL_STATES.includes(value) ? value : "failed";
1372
1420
  }
1373
- function planFromRow(row) {
1374
- const id = str(row["id"]);
1375
- if (id === null) return null;
1376
- const rooms = [];
1377
- for (const raw of Array.isArray(row["rooms"]) ? row["rooms"] : []) {
1378
- const room = obj(raw);
1379
- const roomId = room === null ? null : str(room["id"]);
1380
- if (room === null || roomId === null) continue;
1381
- rooms.push({ id: roomId, name: str(room["name"]) ?? roomId, identities: strings(room["identities"]) });
1382
- }
1421
+ function projectSegment(row) {
1422
+ const operation = "meet.projectSegment";
1423
+ const id = requireText(row, "id", operation);
1383
1424
  return {
1384
1425
  id,
1385
- state: row["state"] === "open" ? "open" : "closed",
1386
- rooms,
1387
- allowChoose: row["allow_choose"] === true,
1388
- minutes: num(row["minutes"]) ?? 0,
1389
- openedAt: str(row["opened_at"]),
1390
- endsAt: str(row["ends_at"]),
1391
- closedAt: str(row["closed_at"]),
1392
- broadcasts: (Array.isArray(row["broadcasts"]) ? row["broadcasts"] : []).map(broadcastFrom).filter((b) => b !== null)
1426
+ meetingId: asMeetingId(requireText(row, "meeting_id", operation)),
1427
+ identity: asParticipantIdentity(text(row, "identity") ?? "unknown"),
1428
+ // A nameless line is a line nobody can act on. The identity is a worse
1429
+ // name than a real one and a far better one than an empty string.
1430
+ speaker: text(row, "speaker") ?? text(row, "identity") ?? "Someone",
1431
+ text: text(row, "text") ?? "",
1432
+ startedAt: text(row, "started_at") ?? (/* @__PURE__ */ new Date(0)).toISOString(),
1433
+ // The table holds finals only; a row that somehow says otherwise is still
1434
+ // a durable record and is rendered as one.
1435
+ final: bool(row, "is_final", true),
1436
+ source: "record",
1437
+ // The worker publishes the caption under THIS row's id (`lk.segment_id` =
1438
+ // the durable segment id), which is what lets a record replace the exact
1439
+ // caption it supersedes instead of doubling the line (D16).
1440
+ captionId: id
1393
1441
  };
1394
1442
  }
1395
- function settingsFromRow(row) {
1396
- if (row === null) return null;
1443
+ function projectNote(row) {
1444
+ const operation = "meet.projectNote";
1445
+ const kind = text(row, "kind");
1446
+ const rawMetadata = row["metadata"];
1447
+ const metadata = typeof rawMetadata === "object" && rawMetadata !== null && !Array.isArray(rawMetadata) ? rawMetadata : {};
1448
+ const assignee = metadata["assignee"];
1449
+ const assigneeRow = typeof assignee === "object" && assignee !== null && !Array.isArray(assignee) ? assignee : {};
1450
+ const windowValue = metadata["window"];
1451
+ const windowRow = typeof windowValue === "object" && windowValue !== null && !Array.isArray(windowValue) ? windowValue : {};
1452
+ const assigneeUserId = text(row, "assignee_user_id");
1453
+ const displayName = assigneeRow["display_name"];
1397
1454
  return {
1398
- pollsEnabled: row["polls_enabled"] === true,
1399
- pollMaxOptions: num(row["poll_max_options"]) ?? 10,
1400
- qaEnabled: row["qa_enabled"] === true,
1401
- qaAnonymousAllowed: row["qa_anonymous_allowed"] === true,
1402
- whiteboardEnabled: row["whiteboard_enabled"] === true,
1403
- breakoutsEnabled: row["breakouts_enabled"] === true,
1404
- breakoutMaxRooms: num(row["breakout_max_rooms"]) ?? 20,
1405
- breakoutDefaultMinutes: num(row["breakout_default_minutes"]) ?? 15,
1406
- captionTranslationEnabled: row["caption_translation_enabled"] === true,
1407
- captionTranslationLanguage: str(row["caption_translation_language"]) ?? ""
1455
+ id: requireText(row, "id", operation),
1456
+ meetingId: asMeetingId(requireText(row, "meeting_id", operation)),
1457
+ text: (text(row, "text") ?? "").trim(),
1458
+ createdAt: text(row, "created_at") ?? (/* @__PURE__ */ new Date()).toISOString(),
1459
+ // An unknown kind is a NOTE, never dropped: a row the database accepted
1460
+ // is a row a person wrote down, whatever a future migration calls it.
1461
+ kind: kind === "decision" || kind === "action-item" ? kind : "note",
1462
+ assigneeUserId: assigneeUserId === null ? null : asUserId(assigneeUserId),
1463
+ // D18: a guest never gets an `assignee_user_id` (the FK targets
1464
+ // `auth.users`), so their name lives in the metadata and is read here —
1465
+ // an action item whose owner has no name is an action item nobody owns.
1466
+ assigneeDisplayName: typeof displayName === "string" && displayName.length > 0 ? displayName : null,
1467
+ role: metadata["role"] === "summary" ? "summary" : null,
1468
+ window: typeof windowRow["index"] === "number" ? windowRow["index"] : null,
1469
+ // D20. `superseded_by` is in `NOTE_PUBLIC_METADATA_KEYS` server-side, so a
1470
+ // session-less guest's room-borne copy carries it too — otherwise the guest
1471
+ // would be the one lane still reading a decision twice.
1472
+ supersededBy: typeof metadata["superseded_by"] === "string" && metadata["superseded_by"].length > 0 ? metadata["superseded_by"] : null,
1473
+ // THE DATABASE ROW. `core/room-events.ts` marks its copy of the same row
1474
+ // `"room"`, and the store's precedence rule reads this field (MRI-A12).
1475
+ source: "record"
1408
1476
  };
1409
1477
  }
1410
- function collabFromState(body, previous) {
1411
- const list = (key, parse) => (Array.isArray(body[key]) ? body[key] : []).map((raw) => {
1412
- const row = obj(raw);
1413
- return row === null ? null : parse(row);
1414
- }).filter((item) => item !== null);
1415
- const breakoutRow = obj(body["breakout"]);
1478
+ var RSVP_STATES = ["needs_action", "accepted", "declined", "tentative"];
1479
+ var OCCURRENCE_STATES = ["scheduled", "moved", "cancelled"];
1480
+ function rsvpState(value) {
1481
+ return RSVP_STATES.includes(value) ? value : "needs_action";
1482
+ }
1483
+ function num(row, key) {
1484
+ const value = row[key];
1485
+ return typeof value === "number" ? value : null;
1486
+ }
1487
+ function when(row, key) {
1488
+ const value = row[key];
1489
+ if (value instanceof Date) return value.toISOString();
1490
+ return text(row, key);
1491
+ }
1492
+ function projectInvitee(row) {
1493
+ const operation = "meet.projectInvitee";
1494
+ const userId = text(row, "invitee_user_id");
1416
1495
  return {
1417
- ...previous,
1418
- loaded: true,
1419
- error: null,
1420
- settings: settingsFromRow(obj(body["settings"])),
1421
- polls: list("polls", pollFromRow),
1422
- questions: list("questions", questionFromRow),
1423
- strokes: list("strokes", strokeFromRow),
1424
- breakout: breakoutRow === null ? null : planFromRow(breakoutRow)
1496
+ id: requireText(row, "id", operation),
1497
+ meetingId: asMeetingId(requireText(row, "meeting_id", operation)),
1498
+ userId: userId === null ? null : asUserId(userId),
1499
+ email: text(row, "email"),
1500
+ displayName: text(row, "display_name"),
1501
+ role: text(row, "role") === "cohost" ? "cohost" : "invitee",
1502
+ rsvpState: rsvpState(text(row, "rsvp_state")),
1503
+ respondedAt: when(row, "responded_at"),
1504
+ rsvpNote: text(row, "rsvp_note"),
1505
+ lastSentSequence: num(row, "last_sent_sequence"),
1506
+ lastSentAt: when(row, "last_sent_at")
1425
1507
  };
1426
1508
  }
1427
- function upsert(items, next) {
1428
- const index = items.findIndex((item) => item.id === next.id);
1429
- if (index === -1) return [...items, next];
1430
- const copy = items.slice();
1431
- copy[index] = next;
1432
- return copy;
1509
+ function projectOccurrence(row) {
1510
+ const operation = "meet.projectOccurrence";
1511
+ const state = text(row, "state");
1512
+ return {
1513
+ meetingId: asMeetingId(requireText(row, "meeting_id", operation)),
1514
+ originalStart: when(row, "original_start") ?? requireText(row, "original_start", operation),
1515
+ occurrenceStart: when(row, "occurrence_start") ?? requireText(row, "occurrence_start", operation),
1516
+ durationMinutes: num(row, "duration_minutes"),
1517
+ // An unknown state is shown as scheduled, never dropped: the row is real.
1518
+ state: OCCURRENCE_STATES.includes(state) ? state : "scheduled",
1519
+ sequence: num(row, "sequence") ?? 0
1520
+ };
1433
1521
  }
1434
- function withItem(collab, row, fromRoom = false) {
1435
- const kind = str(row["kind"]);
1436
- if (kind === "poll") {
1437
- const poll = pollFromRow(row);
1438
- if (poll === null) return collab;
1439
- const known = collab.polls.find((p) => p.id === poll.id);
1440
- let merged = fromRoom && known !== void 0 ? { ...poll, myVote: known.myVote } : poll;
1441
- if (fromRoom && known !== void 0 && !poll.options.some((o) => o.votes !== void 0)) {
1442
- const counted = new Map(known.options.map((o) => [o.id, o]));
1443
- merged = {
1444
- ...merged,
1445
- voterCount: known.voterCount,
1446
- options: merged.options.map((o) => {
1447
- const had = counted.get(o.id);
1448
- return had === void 0 ? o : { ...o, votes: had.votes, voters: had.voters };
1449
- })
1450
- };
1451
- }
1452
- return { ...collab, polls: upsert(collab.polls, merged) };
1453
- }
1454
- if (kind === "question") {
1455
- const question = questionFromRow(row);
1456
- if (question === null) return collab;
1457
- const known = collab.questions.find((q) => q.id === question.id);
1458
- const merged = fromRoom && known !== void 0 ? { ...question, mine: known.mine, upvotedByMe: known.upvotedByMe } : question;
1459
- return { ...collab, questions: upsert(collab.questions, merged) };
1460
- }
1461
- if (kind === "stroke") {
1462
- const stroke = strokeFromRow(row);
1463
- if (stroke === null) return collab;
1464
- return { ...collab, strokes: upsert(collab.strokes, stroke) };
1522
+ function projectUpcoming(row) {
1523
+ const operation = "meet.projectUpcoming";
1524
+ const kind = text(row, "kind");
1525
+ const myRole = text(row, "my_role");
1526
+ const myRsvp = text(row, "my_rsvp");
1527
+ return {
1528
+ ...projectOccurrence(row),
1529
+ organizationId: asOrganizationId(requireText(row, "organization_id", operation)),
1530
+ title: text(row, "title") ?? "Meeting",
1531
+ slug: requireText(row, "slug", operation),
1532
+ kind: kind === "scheduled" || kind === "recurring" ? kind : "instant",
1533
+ hostUserId: asUserId(requireText(row, "host_user_id", operation)),
1534
+ timeZone: text(row, "time_zone") ?? "UTC",
1535
+ meetingCancelled: bool(row, "meeting_cancelled", false),
1536
+ myRsvp: myRsvp === null ? null : rsvpState(myRsvp),
1537
+ myRole: myRole === "host" || myRole === "cohost" || myRole === "invitee" ? myRole : null
1538
+ };
1539
+ }
1540
+ function projectOverride(row) {
1541
+ const operation = "meet.projectOverride";
1542
+ return {
1543
+ id: requireText(row, "id", operation),
1544
+ meetingId: asMeetingId(requireText(row, "meeting_id", operation)),
1545
+ originalStart: when(row, "original_start") ?? requireText(row, "original_start", operation),
1546
+ state: text(row, "state") === "moved" ? "moved" : "cancelled",
1547
+ newStart: when(row, "new_start"),
1548
+ newDurationMinutes: num(row, "new_duration_minutes"),
1549
+ reason: text(row, "reason"),
1550
+ calendarSequence: num(row, "calendar_sequence") ?? 0,
1551
+ deletedAt: when(row, "deleted_at")
1552
+ };
1553
+ }
1554
+ function projectRsvpInvitation(body) {
1555
+ const operation = "meet.projectRsvpInvitation";
1556
+ if (typeof body !== "object" || body === null) {
1557
+ throw invalidResponse(operation, "the RSVP door returned no object");
1465
1558
  }
1466
- if (kind === "breakout") {
1467
- const plan = planFromRow(row);
1468
- if (plan === null) return collab;
1469
- return { ...collab, breakout: plan };
1559
+ const row = body;
1560
+ if (row["ok"] !== true) {
1561
+ return {
1562
+ ok: false,
1563
+ message: text(row, "message") ?? "This invitation link is no longer valid \u2014 ask the host to send it again."
1564
+ };
1470
1565
  }
1471
- return collab;
1472
- }
1473
- function without(collab, ids) {
1474
- const gone = new Set(ids);
1566
+ const meeting = row["meeting"] ?? {};
1567
+ const invitee = row["invitee"] ?? {};
1475
1568
  return {
1476
- ...collab,
1477
- polls: collab.polls.filter((p) => !gone.has(p.id)),
1478
- questions: collab.questions.filter((q) => !gone.has(q.id)),
1479
- strokes: collab.strokes.filter((s) => !gone.has(s.id))
1569
+ ok: true,
1570
+ meeting: {
1571
+ title: text(meeting, "title") ?? "Meeting",
1572
+ slug: requireText(meeting, "slug", operation),
1573
+ scheduledFor: when(meeting, "scheduled_for"),
1574
+ durationMinutes: num(meeting, "duration_minutes"),
1575
+ timeZone: text(meeting, "time_zone") ?? "UTC",
1576
+ agenda: text(meeting, "agenda"),
1577
+ recurrenceRule: text(meeting, "recurrence_rule"),
1578
+ cancelledAt: when(meeting, "cancelled_at"),
1579
+ cancellationReason: text(meeting, "cancellation_reason"),
1580
+ endedAt: when(meeting, "ended_at")
1581
+ },
1582
+ invitee: {
1583
+ displayName: text(invitee, "display_name"),
1584
+ email: text(invitee, "email"),
1585
+ rsvpState: rsvpState(text(invitee, "rsvp_state")),
1586
+ respondedAt: when(invitee, "responded_at")
1587
+ }
1480
1588
  };
1481
1589
  }
1482
- function applyCollabPacket(collab, payload, context) {
1483
- const item = obj(payload["item"]);
1484
- if (item !== null) {
1485
- const kind = str(item["kind"]);
1486
- let next = withItem(collab, item, true);
1487
- if (kind === "poll" && str(item["state"]) === "live") {
1488
- const before = collab.polls.find((p) => p.id === str(item["id"]));
1489
- if (before === void 0 || before.state !== "live") {
1490
- next = { ...next, panelRequest: { panel: "polls", at: context.now } };
1590
+ function settingsPatch(settings) {
1591
+ const patch = {};
1592
+ if (settings.lobbyEnabled !== void 0) patch["lobby_enabled"] = settings.lobbyEnabled;
1593
+ if (settings.recordingPolicy !== void 0) patch["recording_policy"] = settings.recordingPolicy;
1594
+ if (settings.aiEnabled !== void 0) patch["ai_enabled"] = settings.aiEnabled;
1595
+ if (settings.joinBeforeHost !== void 0) patch["join_before_host"] = settings.joinBeforeHost;
1596
+ return patch;
1597
+ }
1598
+ function changesPatch(changes) {
1599
+ const patch = settingsPatch(changes);
1600
+ if (changes.title !== void 0) patch["title"] = changes.title;
1601
+ if (changes.agenda !== void 0) patch["agenda"] = changes.agenda;
1602
+ if (changes.timeZone !== void 0) patch["time_zone"] = changes.timeZone;
1603
+ if (changes.scheduledFor !== void 0) patch["scheduled_for"] = changes.scheduledFor;
1604
+ if (changes.scheduledDurationMinutes !== void 0) {
1605
+ patch["scheduled_duration_minutes"] = changes.scheduledDurationMinutes;
1606
+ }
1607
+ if (changes.recurrenceRule !== void 0) patch["recurrence_rule"] = changes.recurrenceRule;
1608
+ if (changes.keepExceptions !== void 0) patch["keep_exceptions"] = changes.keepExceptions;
1609
+ return patch;
1610
+ }
1611
+ function createMeetRepository(options) {
1612
+ const db = options.client.schema(MEET_SCHEMA);
1613
+ async function guardVoid(operation, run) {
1614
+ let response = await run();
1615
+ if (response.error !== null) {
1616
+ const normalized = normalizeMeetError(response.error, operation);
1617
+ if (normalized.code === "session-unavailable" && options.session !== void 0) {
1618
+ await options.session();
1619
+ response = await run();
1491
1620
  }
1621
+ if (response.error !== null) throw normalizeMeetError(response.error, operation);
1492
1622
  }
1493
- if (kind === "breakout") {
1494
- const plan = planFromRow(item);
1495
- if (plan !== null && plan.state === "closed") {
1496
- next = { ...next, moveRequest: { roomId: null, at: context.now } };
1623
+ }
1624
+ async function guard(operation, run) {
1625
+ let response = await run();
1626
+ if (response.error !== null) {
1627
+ const normalized = normalizeMeetError(response.error, operation);
1628
+ if (normalized.code === "session-unavailable" && options.session !== void 0) {
1629
+ await options.session();
1630
+ response = await run();
1497
1631
  }
1632
+ if (response.error !== null) throw normalizeMeetError(response.error, operation);
1498
1633
  }
1499
- const move = obj(payload["move"]);
1500
- if (move !== null && str(move["identity"]) === context.localIdentity) {
1501
- next = { ...next, moveRequest: { roomId: str(move["room_id"]), at: context.now } };
1634
+ if (response.data === null) {
1635
+ throw invalidResponse(operation, "the database returned no row");
1502
1636
  }
1503
- return { next };
1504
- }
1505
- if (Array.isArray(payload["removed"])) {
1506
- return { next: without(collab, strings(payload["removed"])) };
1507
- }
1508
- if (Array.isArray(payload["hidden"])) {
1509
- return context.isHost ? { next: null, dropped: "hosts keep dismissed questions" } : { next: without(collab, strings(payload["hidden"])) };
1510
- }
1511
- if (payload["cleared"] === "whiteboard") {
1512
- return { next: { ...collab, strokes: [] } };
1637
+ return response.data;
1513
1638
  }
1514
- const broadcast = broadcastFrom(payload["broadcast"]);
1515
- if (broadcast !== null) {
1516
- return { next: { ...collab, broadcast } };
1517
- }
1518
- return { next: null, dropped: "a collab packet with nothing this version understands" };
1519
- }
1520
- function assignedRoom(plan, identity) {
1521
- if (plan === null || plan.state !== "open" || identity === null) return null;
1522
- return plan.rooms.find((room) => room.identities.includes(identity)) ?? null;
1523
- }
1524
- function autoAssign(identities, rooms, exclude = []) {
1525
- const count2 = Math.max(1, Math.floor(rooms));
1526
- const out = Array.from({ length: count2 }, () => []);
1527
- const skip = new Set(exclude);
1528
- let index = 0;
1529
- for (const identity of identities) {
1530
- if (skip.has(identity)) continue;
1531
- out[index % count2].push(identity);
1532
- index += 1;
1639
+ function projectInvite(row) {
1640
+ const operation = "meet.projectInvite";
1641
+ const mode = text(row, "mode");
1642
+ return {
1643
+ id: asCallId(requireText(row, "id", operation)),
1644
+ organizationId: asOrganizationId(requireText(row, "organization_id", operation)),
1645
+ roomName: asRoomName(requireText(row, "room_name", operation)),
1646
+ mode: mode === "video" ? "video" : "audio",
1647
+ callerUserId: asUserId(requireText(row, "caller_user_id", operation)),
1648
+ callerName: text(row, "caller_name") ?? "Someone",
1649
+ callerAvatarUrl: text(row, "caller_avatar_url"),
1650
+ calleeUserIds: stringArray(row, "callee_ids").map(asUserId),
1651
+ conversationId: text(row, "conversation_id"),
1652
+ createdAt: requireText(row, "created_at", operation),
1653
+ expiresAt: text(row, "expires_at") ?? requireText(row, "created_at", operation),
1654
+ state: callState(text(row, "state")),
1655
+ declineMessage: text(row, "decline_message")
1656
+ };
1533
1657
  }
1534
- return out;
1535
- }
1536
- function breakoutRemainingMs(plan, now) {
1537
- if (plan === null || plan.state !== "open" || plan.endsAt === null) return null;
1538
- const end = Date.parse(plan.endsAt);
1539
- return Number.isNaN(end) ? null : Math.max(0, end - now);
1540
- }
1541
- function defaultCaptionLanguage(settings, viewerLanguage) {
1542
- const configured = settings?.captionTranslationLanguage.trim() ?? "";
1543
- if (configured.length > 0) return configured;
1544
- const own = (viewerLanguage ?? "").trim();
1545
- return own.length > 0 ? own : "en";
1546
- }
1547
- var CAPTION_LANGUAGES = [
1548
- { tag: "en", name: "English" },
1549
- { tag: "es", name: "Spanish" },
1550
- { tag: "fr", name: "French" },
1551
- { tag: "de", name: "German" },
1552
- { tag: "it", name: "Italian" },
1553
- { tag: "pt", name: "Portuguese" },
1554
- { tag: "nl", name: "Dutch" },
1555
- { tag: "pl", name: "Polish" },
1556
- { tag: "ru", name: "Russian" },
1557
- { tag: "uk", name: "Ukrainian" },
1558
- { tag: "tr", name: "Turkish" },
1559
- { tag: "ar", name: "Arabic" },
1560
- { tag: "he", name: "Hebrew" },
1561
- { tag: "fa", name: "Persian" },
1562
- { tag: "hi", name: "Hindi" },
1563
- { tag: "bn", name: "Bengali" },
1564
- { tag: "zh", name: "Chinese" },
1565
- { tag: "ja", name: "Japanese" },
1566
- { tag: "ko", name: "Korean" },
1567
- { tag: "vi", name: "Vietnamese" },
1568
- { tag: "th", name: "Thai" },
1569
- { tag: "id", name: "Indonesian" },
1570
- { tag: "sv", name: "Swedish" }
1571
- ];
1572
- async function loadMeetingActivities(api, meetingId, meetingPass = null) {
1573
- let response;
1574
- try {
1575
- response = await api.request({
1576
- path: `${MEET_ROUTES.collab}?meeting_id=${encodeURIComponent(meetingId)}`,
1577
- method: "GET",
1578
- operation: "meet.loadMeetingActivities",
1579
- requireAuth: meetingPass === null,
1580
- ...meetingPass !== null ? { headers: { [MEETING_PASS_HEADER]: meetingPass } } : {}
1581
- });
1582
- } catch (error) {
1658
+ function projectMeeting(row) {
1659
+ const operation = "meet.projectMeeting";
1660
+ const kind = text(row, "kind");
1661
+ const policy = text(row, "recording_policy");
1662
+ const duration = row["scheduled_duration_minutes"];
1663
+ const metadata = row["metadata"];
1583
1664
  return {
1584
- collab: null,
1585
- refusal: {
1586
- message: `Polls, questions and the whiteboard could not be read (${error.message}).`,
1587
- remedy: error.remedy ?? "Reload the page."
1588
- }
1665
+ id: asMeetingId(requireText(row, "id", operation)),
1666
+ organizationId: asOrganizationId(requireText(row, "organization_id", operation)),
1667
+ roomName: asRoomName(requireText(row, "room_name", operation)),
1668
+ slug: requireText(row, "slug", operation),
1669
+ title: text(row, "title") ?? "Meeting",
1670
+ kind: kind === "scheduled" || kind === "recurring" ? kind : "instant",
1671
+ hostUserId: asUserId(requireText(row, "host_user_id", operation)),
1672
+ scheduledFor: text(row, "scheduled_for"),
1673
+ scheduledDurationMinutes: typeof duration === "number" ? duration : null,
1674
+ startedAt: text(row, "started_at"),
1675
+ endedAt: text(row, "ended_at"),
1676
+ locked: bool(row, "locked", false),
1677
+ lobbyEnabled: bool(row, "lobby_enabled", true),
1678
+ recordingPolicy: policy === "disabled" || policy === "always-on" ? policy : "host-controlled",
1679
+ aiEnabled: bool(row, "ai_enabled", true),
1680
+ metadata: typeof metadata === "object" && metadata !== null && !Array.isArray(metadata) ? metadata : {},
1681
+ timeZone: text(row, "time_zone") ?? "UTC",
1682
+ agenda: text(row, "agenda"),
1683
+ recurrenceRule: text(row, "recurrence_rule"),
1684
+ joinBeforeHost: bool(row, "join_before_host", true),
1685
+ cancelledAt: text(row, "cancelled_at"),
1686
+ cancellationReason: text(row, "cancellation_reason"),
1687
+ deletedAt: text(row, "deleted_at"),
1688
+ calendarSequence: typeof row["calendar_sequence"] === "number" ? row["calendar_sequence"] : 0,
1689
+ version: typeof row["version"] === "number" ? row["version"] : 1
1589
1690
  };
1590
1691
  }
1591
- if (!response.ok) {
1592
- let body = null;
1593
- try {
1594
- body = await response.json();
1595
- } catch {
1596
- body = null;
1597
- }
1598
- const said = readServerRefusal(body);
1692
+ function projectRecording(row) {
1693
+ const operation = "meet.projectRecording";
1694
+ const state = text(row, "state");
1695
+ const known = [
1696
+ "idle",
1697
+ "starting",
1698
+ "recording",
1699
+ "stopping",
1700
+ "processing",
1701
+ "available",
1702
+ "expired",
1703
+ "failed"
1704
+ ];
1599
1705
  return {
1600
- collab: null,
1601
- refusal: {
1602
- message: said?.reason ?? `Polls and questions could not be read (the server answered ${response.status}).`,
1603
- remedy: said?.remedy || "Reload the page."
1604
- }
1706
+ id: requireText(row, "id", operation),
1707
+ meetingId: asMeetingId(requireText(row, "meeting_id", operation)),
1708
+ state: known.includes(state) ? state : "failed",
1709
+ startedAt: text(row, "started_at"),
1710
+ endedAt: text(row, "ended_at"),
1711
+ startedBy: text(row, "started_by") === null ? null : asUserId(text(row, "started_by")),
1712
+ // A PLATFORM file id, never a provider URL (R3). A recording that only
1713
+ // exists as a LiveKit URL is orphaned the day the room is garbage
1714
+ // collected, which is the whole reason this column is not `url`.
1715
+ fileId: text(row, "file_id"),
1716
+ failureReason: text(row, "failure_reason"),
1717
+ retention: projectRecordingRetention(row["metadata"])
1605
1718
  };
1606
1719
  }
1607
- return { collab: collabFromState(await response.json(), EMPTY_COLLAB), refusal: null };
1608
- }
1609
-
1610
- // src/core/repository.ts
1611
- var MEET_SCHEMA = "communication";
1612
- var TABLES = {
1613
- meetings: "meet_meetings",
1614
- participants: "meet_participants",
1615
- invites: "meet_call_invites",
1616
- recordings: "meet_recordings",
1617
- transcripts: "meet_transcript_segments",
1618
- notes: "meet_notes",
1619
- /** Wave 1 (0.8): who a meeting was sent to, and what they answered. */
1620
- invitees: "meet_invitees",
1621
- /** Wave 1 (0.8): one occurrence of a series cancelled or moved. */
1622
- occurrenceOverrides: "meet_occurrence_overrides"
1623
- };
1624
- var RPCS = {
1625
- /** Atomic: create the invite row and stamp its expiry in one statement. */
1626
- createCallInvite: "meet_create_call_invite",
1627
- /** Atomic settle. Refuses a transition out of a terminal state. */
1628
- settleCallInvite: "meet_settle_call_invite",
1629
- /** Every invite still ringing this user — the reconnect backfill door. */
1630
- pendingCallInvites: "meet_pending_call_invites",
1631
- /** Instant meeting, or the durable one behind a recurring link. */
1632
- getOrCreateMeeting: "meet_get_or_create_meeting",
1633
- /** Resolve a durable public link, before/during/after its scheduled time. */
1634
- meetingBySlug: "meet_meeting_by_slug",
1635
- admit: "meet_admit_participant",
1636
- setLock: "meet_set_lock",
1637
- recordConsent: "meet_record_consent",
1638
- /*
1639
- * ── MEET WAVE 1 — management (0.8). Every write is a SECURITY DEFINER door
1640
- * authorized by the meeting's host or a real iam grant (editor to change,
1641
- * admin = co-host to archive or name a co-host) — never an organization
1642
- * match. After any of these writes, call `announceMeeting` so the server
1643
- * sends invitations / updates / cancellations and re-queues reminders.
1644
- */
1645
- /** Schedule with every setting at create; unset settings come from the knobs. */
1646
- scheduleMeeting: "meet_schedule_meeting",
1647
- /** Edit / reschedule — a JSON patch with a version compare-and-swap. */
1648
- updateMeeting: "meet_update_meeting",
1649
- /** Cancel is a state (`cancelled_at`); the link keeps resolving. */
1650
- cancelMeeting: "meet_cancel_meeting",
1651
- /** Archive (soft delete) — the invitations and exceptions cascade. */
1652
- archiveMeeting: "meet_archive_meeting",
1653
- restoreMeeting: "meet_restore_meeting",
1654
- /** Cancel, move or restore ONE occurrence of a series by its original start. */
1655
- setOccurrence: "meet_set_occurrence",
1656
- addInvitees: "meet_add_invitees",
1657
- setInviteeRole: "meet_set_invitee_role",
1658
- removeInvitee: "meet_remove_invitee",
1659
- /** A signed-in invitee answers for themselves. */
1660
- respond: "meet_respond",
1661
- /** The emailed RSVP link (no account): read and answer. */
1662
- invitationByToken: "meet_invitation_by_token",
1663
- respondByToken: "meet_respond_by_token",
1664
- /** One meeting's occurrences between two instants (INVOKER — the reader's RLS). */
1665
- meetingOccurrences: "meet_meeting_occurrences",
1666
- /** Every occurrence the reader can see between two instants — the Upcoming list. */
1667
- occurrencesBetween: "meet_occurrences_between",
1668
- /*
1669
- * ── MEET WAVE 3 — after the meeting. Both INVOKER (the reader's RLS) and both
1670
- * scoped to the reader's OWN meetings (host, invitee, attended) — RLS is the
1671
- * ceiling, never the view.
1672
- */
1673
- /** The recordings library. */
1674
- myRecordings: "meet_my_recordings",
1675
- /** Titles, agendas, current notes and final transcript lines; snippets mark hits with ⟦ ⟧. */
1676
- search: "meet_search"
1677
- };
1678
- var VOID_RPCS = [RPCS.admit, RPCS.recordConsent];
1679
- 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";
1680
- 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";
1681
- var RECORDING_COLUMNS = "id,meeting_id,state,started_at,ended_at,started_by,file_id,failure_reason";
1682
- var SEGMENT_COLUMNS = "id,meeting_id,identity,speaker,text,started_at,is_final,metadata";
1683
- var NOTE_COLUMNS = "id,meeting_id,text,kind,assignee_user_id,created_at,metadata";
1684
- 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";
1685
- function projectStays(value) {
1686
- if (!Array.isArray(value)) return [];
1687
- const stays = [];
1688
- for (const item of value) {
1689
- if (item === null || typeof item !== "object") continue;
1690
- const joined = item["joined_at"];
1691
- const left = item["left_at"];
1692
- if (typeof joined !== "string" || joined.length === 0) continue;
1693
- stays.push({ joinedAt: joined, leftAt: typeof left === "string" && left.length > 0 ? left : null });
1720
+ function projectRecordingRetention(value) {
1721
+ if (typeof value !== "object" || value === null || Array.isArray(value)) return null;
1722
+ const retention = value["retention"];
1723
+ if (typeof retention !== "object" || retention === null || Array.isArray(retention)) return null;
1724
+ const row = retention;
1725
+ const phase = row["phase"];
1726
+ const operationId = row["operation_id"];
1727
+ const policyId = row["policy_id"];
1728
+ const policyUpdatedAt = row["policy_updated_at"];
1729
+ const dueAt = row["due_at"];
1730
+ const expiredAt = row["expired_at"];
1731
+ if (phase !== "warning" && phase !== "expired" || typeof operationId !== "string" || typeof policyId !== "string" || typeof policyUpdatedAt !== "string" || typeof dueAt !== "string" || expiredAt !== null && typeof expiredAt !== "string") return null;
1732
+ return { operationId, policyId, policyUpdatedAt, phase, dueAt, expiredAt };
1694
1733
  }
1695
- return stays;
1696
- }
1697
- function text(row, key) {
1698
- const value = row[key];
1699
- return typeof value === "string" && value.length > 0 ? value : null;
1700
- }
1701
- function requireText(row, key, operation) {
1702
- const value = text(row, key);
1703
- if (value === null) {
1704
- throw invalidResponse(operation, `the row is missing "${key}"`);
1705
- }
1706
- return value;
1707
- }
1708
- function bool(row, key, fallback) {
1709
- const value = row[key];
1710
- return typeof value === "boolean" ? value : fallback;
1711
- }
1712
- function stringArray(row, key) {
1713
- const value = row[key];
1714
- if (!Array.isArray(value)) return [];
1715
- return value.filter((entry) => typeof entry === "string");
1716
- }
1717
- var CALL_STATES = [
1718
- "idle",
1719
- "dialing",
1720
- "ringing",
1721
- "connecting",
1722
- "active",
1723
- "declined",
1724
- "missed",
1725
- "busy",
1726
- "cancelled",
1727
- "ended",
1728
- "failed"
1729
- ];
1730
- function callState(value) {
1731
- return CALL_STATES.includes(value) ? value : "failed";
1732
- }
1733
- function projectSegment(row) {
1734
- const operation = "meet.projectSegment";
1735
- const id = requireText(row, "id", operation);
1736
- return {
1737
- id,
1738
- meetingId: asMeetingId(requireText(row, "meeting_id", operation)),
1739
- identity: asParticipantIdentity(text(row, "identity") ?? "unknown"),
1740
- // A nameless line is a line nobody can act on. The identity is a worse
1741
- // name than a real one and a far better one than an empty string.
1742
- speaker: text(row, "speaker") ?? text(row, "identity") ?? "Someone",
1743
- text: text(row, "text") ?? "",
1744
- startedAt: text(row, "started_at") ?? (/* @__PURE__ */ new Date(0)).toISOString(),
1745
- // The table holds finals only; a row that somehow says otherwise is still
1746
- // a durable record and is rendered as one.
1747
- final: bool(row, "is_final", true),
1748
- source: "record",
1749
- // The worker publishes the caption under THIS row's id (`lk.segment_id` =
1750
- // the durable segment id), which is what lets a record replace the exact
1751
- // caption it supersedes instead of doubling the line (D16).
1752
- captionId: id
1753
- };
1754
- }
1755
- function projectNote(row) {
1756
- const operation = "meet.projectNote";
1757
- const kind = text(row, "kind");
1758
- const rawMetadata = row["metadata"];
1759
- const metadata = typeof rawMetadata === "object" && rawMetadata !== null && !Array.isArray(rawMetadata) ? rawMetadata : {};
1760
- const assignee = metadata["assignee"];
1761
- const assigneeRow = typeof assignee === "object" && assignee !== null && !Array.isArray(assignee) ? assignee : {};
1762
- const windowValue = metadata["window"];
1763
- const windowRow = typeof windowValue === "object" && windowValue !== null && !Array.isArray(windowValue) ? windowValue : {};
1764
- const assigneeUserId = text(row, "assignee_user_id");
1765
- const displayName = assigneeRow["display_name"];
1766
- return {
1767
- id: requireText(row, "id", operation),
1768
- meetingId: asMeetingId(requireText(row, "meeting_id", operation)),
1769
- text: (text(row, "text") ?? "").trim(),
1770
- createdAt: text(row, "created_at") ?? (/* @__PURE__ */ new Date()).toISOString(),
1771
- // An unknown kind is a NOTE, never dropped: a row the database accepted
1772
- // is a row a person wrote down, whatever a future migration calls it.
1773
- kind: kind === "decision" || kind === "action-item" ? kind : "note",
1774
- assigneeUserId: assigneeUserId === null ? null : asUserId(assigneeUserId),
1775
- // D18: a guest never gets an `assignee_user_id` (the FK targets
1776
- // `auth.users`), so their name lives in the metadata and is read here —
1777
- // an action item whose owner has no name is an action item nobody owns.
1778
- assigneeDisplayName: typeof displayName === "string" && displayName.length > 0 ? displayName : null,
1779
- role: metadata["role"] === "summary" ? "summary" : null,
1780
- window: typeof windowRow["index"] === "number" ? windowRow["index"] : null,
1781
- // D20. `superseded_by` is in `NOTE_PUBLIC_METADATA_KEYS` server-side, so a
1782
- // session-less guest's room-borne copy carries it too — otherwise the guest
1783
- // would be the one lane still reading a decision twice.
1784
- supersededBy: typeof metadata["superseded_by"] === "string" && metadata["superseded_by"].length > 0 ? metadata["superseded_by"] : null,
1785
- // THE DATABASE ROW. `core/room-events.ts` marks its copy of the same row
1786
- // `"room"`, and the store's precedence rule reads this field (MRI-A12).
1787
- source: "record"
1788
- };
1789
- }
1790
- var RSVP_STATES = ["needs_action", "accepted", "declined", "tentative"];
1791
- var OCCURRENCE_STATES = ["scheduled", "moved", "cancelled"];
1792
- function rsvpState(value) {
1793
- return RSVP_STATES.includes(value) ? value : "needs_action";
1794
- }
1795
- function num2(row, key) {
1796
- const value = row[key];
1797
- return typeof value === "number" ? value : null;
1798
- }
1799
- function when(row, key) {
1800
- const value = row[key];
1801
- if (value instanceof Date) return value.toISOString();
1802
- return text(row, key);
1803
- }
1804
- function projectInvitee(row) {
1805
- const operation = "meet.projectInvitee";
1806
- const userId = text(row, "invitee_user_id");
1807
- return {
1808
- id: requireText(row, "id", operation),
1809
- meetingId: asMeetingId(requireText(row, "meeting_id", operation)),
1810
- userId: userId === null ? null : asUserId(userId),
1811
- email: text(row, "email"),
1812
- displayName: text(row, "display_name"),
1813
- role: text(row, "role") === "cohost" ? "cohost" : "invitee",
1814
- rsvpState: rsvpState(text(row, "rsvp_state")),
1815
- respondedAt: when(row, "responded_at"),
1816
- rsvpNote: text(row, "rsvp_note"),
1817
- lastSentSequence: num2(row, "last_sent_sequence"),
1818
- lastSentAt: when(row, "last_sent_at")
1819
- };
1820
- }
1821
- function projectOccurrence(row) {
1822
- const operation = "meet.projectOccurrence";
1823
- const state = text(row, "state");
1824
- return {
1825
- meetingId: asMeetingId(requireText(row, "meeting_id", operation)),
1826
- originalStart: when(row, "original_start") ?? requireText(row, "original_start", operation),
1827
- occurrenceStart: when(row, "occurrence_start") ?? requireText(row, "occurrence_start", operation),
1828
- durationMinutes: num2(row, "duration_minutes"),
1829
- // An unknown state is shown as scheduled, never dropped: the row is real.
1830
- state: OCCURRENCE_STATES.includes(state) ? state : "scheduled",
1831
- sequence: num2(row, "sequence") ?? 0
1832
- };
1833
- }
1834
- function projectUpcoming(row) {
1835
- const operation = "meet.projectUpcoming";
1836
- const kind = text(row, "kind");
1837
- const myRole = text(row, "my_role");
1838
- const myRsvp = text(row, "my_rsvp");
1839
- return {
1840
- ...projectOccurrence(row),
1841
- organizationId: asOrganizationId(requireText(row, "organization_id", operation)),
1842
- title: text(row, "title") ?? "Meeting",
1843
- slug: requireText(row, "slug", operation),
1844
- kind: kind === "scheduled" || kind === "recurring" ? kind : "instant",
1845
- hostUserId: asUserId(requireText(row, "host_user_id", operation)),
1846
- timeZone: text(row, "time_zone") ?? "UTC",
1847
- meetingCancelled: bool(row, "meeting_cancelled", false),
1848
- myRsvp: myRsvp === null ? null : rsvpState(myRsvp),
1849
- myRole: myRole === "host" || myRole === "cohost" || myRole === "invitee" ? myRole : null
1850
- };
1851
- }
1852
- function projectOverride(row) {
1853
- const operation = "meet.projectOverride";
1854
- return {
1855
- id: requireText(row, "id", operation),
1856
- meetingId: asMeetingId(requireText(row, "meeting_id", operation)),
1857
- originalStart: when(row, "original_start") ?? requireText(row, "original_start", operation),
1858
- state: text(row, "state") === "moved" ? "moved" : "cancelled",
1859
- newStart: when(row, "new_start"),
1860
- newDurationMinutes: num2(row, "new_duration_minutes"),
1861
- reason: text(row, "reason"),
1862
- calendarSequence: num2(row, "calendar_sequence") ?? 0,
1863
- deletedAt: when(row, "deleted_at")
1864
- };
1865
- }
1866
- function projectRsvpInvitation(body) {
1867
- const operation = "meet.projectRsvpInvitation";
1868
- if (typeof body !== "object" || body === null) {
1869
- throw invalidResponse(operation, "the RSVP door returned no object");
1870
- }
1871
- const row = body;
1872
- if (row["ok"] !== true) {
1734
+ function projectAttendee(row) {
1735
+ const operation = "meet.projectAttendee";
1736
+ const role = text(row, "role");
1737
+ const admission = text(row, "admission_state");
1738
+ const known = ["host", "cohost", "presenter", "participant", "viewer"];
1739
+ const states = [
1740
+ "waiting_host",
1741
+ "knocking",
1742
+ "admitted",
1743
+ "denied",
1744
+ "expired",
1745
+ "withdrawn",
1746
+ "removed",
1747
+ "ended"
1748
+ ];
1749
+ const userId = text(row, "participant_user_id");
1750
+ const identity = requireText(row, "identity", operation);
1873
1751
  return {
1874
- ok: false,
1875
- message: text(row, "message") ?? "This invitation link is no longer valid \u2014 ask the host to send it again."
1752
+ id: requireText(row, "id", operation),
1753
+ meetingId: asMeetingId(requireText(row, "meeting_id", operation)),
1754
+ identity: asParticipantIdentity(identity),
1755
+ // A nameless attendee is a row nobody can read. The identity is a worse
1756
+ // name than a real one and an infinitely better one than a blank.
1757
+ displayName: text(row, "display_name") ?? identity,
1758
+ avatarUrl: text(row, "avatar_url"),
1759
+ userId: userId === null ? null : asUserId(userId),
1760
+ role: known.includes(role) ? role : "participant",
1761
+ admissionState: states.includes(admission) ? admission : "admitted",
1762
+ knockedAt: text(row, "knocked_at"),
1763
+ knockExpiresAt: text(row, "knock_expires_at"),
1764
+ isAgent: bool(row, "is_agent", false),
1765
+ joinedAt: text(row, "joined_at"),
1766
+ leftAt: text(row, "left_at"),
1767
+ sessions: projectStays(row["sessions"])
1876
1768
  };
1877
1769
  }
1878
- const meeting = row["meeting"] ?? {};
1879
- const invitee = row["invitee"] ?? {};
1880
- return {
1881
- ok: true,
1882
- meeting: {
1883
- title: text(meeting, "title") ?? "Meeting",
1884
- slug: requireText(meeting, "slug", operation),
1885
- scheduledFor: when(meeting, "scheduled_for"),
1886
- durationMinutes: num2(meeting, "duration_minutes"),
1887
- timeZone: text(meeting, "time_zone") ?? "UTC",
1888
- agenda: text(meeting, "agenda"),
1889
- recurrenceRule: text(meeting, "recurrence_rule"),
1890
- cancelledAt: when(meeting, "cancelled_at"),
1891
- cancellationReason: text(meeting, "cancellation_reason"),
1892
- endedAt: when(meeting, "ended_at")
1893
- },
1894
- invitee: {
1895
- displayName: text(invitee, "display_name"),
1896
- email: text(invitee, "email"),
1897
- rsvpState: rsvpState(text(invitee, "rsvp_state")),
1898
- respondedAt: when(invitee, "responded_at")
1899
- }
1900
- };
1901
- }
1902
- function settingsPatch(settings) {
1903
- const patch = {};
1904
- if (settings.lobbyEnabled !== void 0) patch["lobby_enabled"] = settings.lobbyEnabled;
1905
- if (settings.recordingPolicy !== void 0) patch["recording_policy"] = settings.recordingPolicy;
1906
- if (settings.aiEnabled !== void 0) patch["ai_enabled"] = settings.aiEnabled;
1907
- if (settings.joinBeforeHost !== void 0) patch["join_before_host"] = settings.joinBeforeHost;
1908
- return patch;
1909
- }
1910
- function changesPatch(changes) {
1911
- const patch = settingsPatch(changes);
1912
- if (changes.title !== void 0) patch["title"] = changes.title;
1913
- if (changes.agenda !== void 0) patch["agenda"] = changes.agenda;
1914
- if (changes.timeZone !== void 0) patch["time_zone"] = changes.timeZone;
1915
- if (changes.scheduledFor !== void 0) patch["scheduled_for"] = changes.scheduledFor;
1916
- if (changes.scheduledDurationMinutes !== void 0) {
1917
- patch["scheduled_duration_minutes"] = changes.scheduledDurationMinutes;
1918
- }
1919
- if (changes.recurrenceRule !== void 0) patch["recurrence_rule"] = changes.recurrenceRule;
1920
- if (changes.keepExceptions !== void 0) patch["keep_exceptions"] = changes.keepExceptions;
1921
- return patch;
1922
- }
1923
- function createMeetRepository(options) {
1924
- const db = options.client.schema(MEET_SCHEMA);
1925
- async function guardVoid(operation, run) {
1926
- let response = await run();
1927
- if (response.error !== null) {
1928
- const normalized = normalizeMeetError(response.error, operation);
1929
- if (normalized.code === "session-unavailable" && options.session !== void 0) {
1930
- await options.session();
1931
- response = await run();
1932
- }
1933
- if (response.error !== null) throw normalizeMeetError(response.error, operation);
1934
- }
1935
- }
1936
- async function guard(operation, run) {
1937
- let response = await run();
1938
- if (response.error !== null) {
1939
- const normalized = normalizeMeetError(response.error, operation);
1940
- if (normalized.code === "session-unavailable" && options.session !== void 0) {
1941
- await options.session();
1942
- response = await run();
1943
- }
1944
- if (response.error !== null) throw normalizeMeetError(response.error, operation);
1945
- }
1946
- if (response.data === null) {
1947
- throw invalidResponse(operation, "the database returned no row");
1948
- }
1949
- return response.data;
1950
- }
1951
- function projectInvite(row) {
1952
- const operation = "meet.projectInvite";
1953
- const mode = text(row, "mode");
1954
- return {
1955
- id: asCallId(requireText(row, "id", operation)),
1956
- organizationId: asOrganizationId(requireText(row, "organization_id", operation)),
1957
- roomName: asRoomName(requireText(row, "room_name", operation)),
1958
- mode: mode === "video" ? "video" : "audio",
1959
- callerUserId: asUserId(requireText(row, "caller_user_id", operation)),
1960
- callerName: text(row, "caller_name") ?? "Someone",
1961
- callerAvatarUrl: text(row, "caller_avatar_url"),
1962
- calleeUserIds: stringArray(row, "callee_ids").map(asUserId),
1963
- conversationId: text(row, "conversation_id"),
1964
- createdAt: requireText(row, "created_at", operation),
1965
- expiresAt: text(row, "expires_at") ?? requireText(row, "created_at", operation),
1966
- state: callState(text(row, "state")),
1967
- declineMessage: text(row, "decline_message")
1968
- };
1969
- }
1970
- function projectMeeting(row) {
1971
- const operation = "meet.projectMeeting";
1972
- const kind = text(row, "kind");
1973
- const policy = text(row, "recording_policy");
1974
- const duration = row["scheduled_duration_minutes"];
1975
- const metadata = row["metadata"];
1976
- return {
1977
- id: asMeetingId(requireText(row, "id", operation)),
1978
- organizationId: asOrganizationId(requireText(row, "organization_id", operation)),
1979
- roomName: asRoomName(requireText(row, "room_name", operation)),
1980
- slug: requireText(row, "slug", operation),
1981
- title: text(row, "title") ?? "Meeting",
1982
- kind: kind === "scheduled" || kind === "recurring" ? kind : "instant",
1983
- hostUserId: asUserId(requireText(row, "host_user_id", operation)),
1984
- scheduledFor: text(row, "scheduled_for"),
1985
- scheduledDurationMinutes: typeof duration === "number" ? duration : null,
1986
- startedAt: text(row, "started_at"),
1987
- endedAt: text(row, "ended_at"),
1988
- locked: bool(row, "locked", false),
1989
- lobbyEnabled: bool(row, "lobby_enabled", true),
1990
- recordingPolicy: policy === "disabled" || policy === "always-on" ? policy : "host-controlled",
1991
- aiEnabled: bool(row, "ai_enabled", true),
1992
- metadata: typeof metadata === "object" && metadata !== null && !Array.isArray(metadata) ? metadata : {},
1993
- timeZone: text(row, "time_zone") ?? "UTC",
1994
- agenda: text(row, "agenda"),
1995
- recurrenceRule: text(row, "recurrence_rule"),
1996
- joinBeforeHost: bool(row, "join_before_host", true),
1997
- cancelledAt: text(row, "cancelled_at"),
1998
- cancellationReason: text(row, "cancellation_reason"),
1999
- deletedAt: text(row, "deleted_at"),
2000
- calendarSequence: typeof row["calendar_sequence"] === "number" ? row["calendar_sequence"] : 0,
2001
- version: typeof row["version"] === "number" ? row["version"] : 1
2002
- };
2003
- }
2004
- function projectRecording(row) {
2005
- const operation = "meet.projectRecording";
2006
- const state = text(row, "state");
2007
- const known = [
2008
- "idle",
2009
- "starting",
2010
- "recording",
2011
- "stopping",
2012
- "processing",
2013
- "available",
2014
- "expired",
2015
- "failed"
2016
- ];
2017
- return {
2018
- id: requireText(row, "id", operation),
2019
- meetingId: asMeetingId(requireText(row, "meeting_id", operation)),
2020
- state: known.includes(state) ? state : "failed",
2021
- startedAt: text(row, "started_at"),
2022
- endedAt: text(row, "ended_at"),
2023
- startedBy: text(row, "started_by") === null ? null : asUserId(text(row, "started_by")),
2024
- // A PLATFORM file id, never a provider URL (R3). A recording that only
2025
- // exists as a LiveKit URL is orphaned the day the room is garbage
2026
- // collected, which is the whole reason this column is not `url`.
2027
- fileId: text(row, "file_id"),
2028
- failureReason: text(row, "failure_reason"),
2029
- retention: projectRecordingRetention(row["metadata"])
2030
- };
2031
- }
2032
- function projectRecordingRetention(value) {
2033
- if (typeof value !== "object" || value === null || Array.isArray(value)) return null;
2034
- const retention = value["retention"];
2035
- if (typeof retention !== "object" || retention === null || Array.isArray(retention)) return null;
2036
- const row = retention;
2037
- const phase = row["phase"];
2038
- const operationId = row["operation_id"];
2039
- const policyId = row["policy_id"];
2040
- const policyUpdatedAt = row["policy_updated_at"];
2041
- const dueAt = row["due_at"];
2042
- const expiredAt = row["expired_at"];
2043
- if (phase !== "warning" && phase !== "expired" || typeof operationId !== "string" || typeof policyId !== "string" || typeof policyUpdatedAt !== "string" || typeof dueAt !== "string" || expiredAt !== null && typeof expiredAt !== "string") return null;
2044
- return { operationId, policyId, policyUpdatedAt, phase, dueAt, expiredAt };
2045
- }
2046
- function projectAttendee(row) {
2047
- const operation = "meet.projectAttendee";
2048
- const role = text(row, "role");
2049
- const admission = text(row, "admission_state");
2050
- const known = ["host", "cohost", "presenter", "participant", "viewer"];
2051
- const states = ["knocking", "admitted", "denied", "removed"];
2052
- const userId = text(row, "participant_user_id");
2053
- const identity = requireText(row, "identity", operation);
2054
- return {
2055
- id: requireText(row, "id", operation),
2056
- meetingId: asMeetingId(requireText(row, "meeting_id", operation)),
2057
- identity: asParticipantIdentity(identity),
2058
- // A nameless attendee is a row nobody can read. The identity is a worse
2059
- // name than a real one and an infinitely better one than a blank.
2060
- displayName: text(row, "display_name") ?? identity,
2061
- avatarUrl: text(row, "avatar_url"),
2062
- userId: userId === null ? null : asUserId(userId),
2063
- role: known.includes(role) ? role : "participant",
2064
- admissionState: states.includes(admission) ? admission : "admitted",
2065
- isAgent: bool(row, "is_agent", false),
2066
- joinedAt: text(row, "joined_at"),
2067
- leftAt: text(row, "left_at"),
2068
- sessions: projectStays(row["sessions"])
2069
- };
2070
- }
2071
- function requireOrg(organizationId, operation) {
2072
- if (typeof organizationId !== "string" || organizationId.length === 0) {
2073
- throw new MeetError(
2074
- "misconfigured",
2075
- `${operation}: no organization_id`,
2076
- "Every meet write carries an explicit organization_id (R5). The package refuses the write rather than letting an unscoped row reach the database."
2077
- );
1770
+ function requireOrg(organizationId, operation) {
1771
+ if (typeof organizationId !== "string" || organizationId.length === 0) {
1772
+ throw new MeetError(
1773
+ "misconfigured",
1774
+ `${operation}: no organization_id`,
1775
+ "Every meet write carries an explicit organization_id (R5). The package refuses the write rather than letting an unscoped row reach the database."
1776
+ );
2078
1777
  }
2079
1778
  }
2080
1779
  return {
@@ -2100,7 +1799,7 @@ function createMeetRepository(options) {
2100
1799
  p_duration_minutes: input.durationMinutes ?? null,
2101
1800
  p_agenda: input.agenda ?? null,
2102
1801
  p_recurrence_rule: input.recurrenceRule ?? null,
2103
- p_settings: settingsPatch(input.settings ?? {})
1802
+ p_settings: scheduleSettingsPatch(input.settings ?? {}, input.appPanel, input.observers)
2104
1803
  })
2105
1804
  );
2106
1805
  return projectMeeting(row);
@@ -2340,16 +2039,26 @@ function createMeetRepository(options) {
2340
2039
  hasMore: rows.length > limit
2341
2040
  };
2342
2041
  },
2343
- async admit({ meetingId, identity, admitted, byUserId }) {
2344
- await guardVoid(
2345
- "meet.admit",
2346
- () => db.rpc(RPCS.admit, {
2042
+ async decideAdmission({ meetingId, identity, decision, byUserId }) {
2043
+ const fn = decision === "admit" ? RPCS.admit : decision === "deny" ? RPCS.deny : RPCS.undeny;
2044
+ const row = await guard(
2045
+ `meet.${decision}`,
2046
+ () => db.rpc(fn, {
2347
2047
  p_meeting_id: meetingId,
2348
2048
  p_identity: identity,
2349
- p_admitted: admitted,
2350
2049
  p_by_user_id: byUserId
2351
2050
  })
2352
2051
  );
2052
+ return projectAttendee(row);
2053
+ },
2054
+ async admitAll(meetingId, byUserId) {
2055
+ const response = await db.rpc(RPCS.admitAll, {
2056
+ p_meeting_id: meetingId,
2057
+ p_by_user_id: byUserId
2058
+ });
2059
+ if (response.error !== null) throw normalizeMeetError(response.error, "meet.admitAll");
2060
+ const rows = Array.isArray(response.data) ? response.data : [];
2061
+ return rows.map(projectAttendee);
2353
2062
  },
2354
2063
  async setLock(meetingId, locked, byUserId) {
2355
2064
  const row = await guard(
@@ -2423,76 +2132,449 @@ function createMeetRepository(options) {
2423
2132
  );
2424
2133
  }
2425
2134
  }
2426
- };
2135
+ };
2136
+ }
2137
+ var LIBRARY_STATES = [
2138
+ "idle",
2139
+ "starting",
2140
+ "recording",
2141
+ "stopping",
2142
+ "processing",
2143
+ "available",
2144
+ "expired",
2145
+ "failed"
2146
+ ];
2147
+ function projectLibraryRecording(row) {
2148
+ const operation = "meet.projectLibraryRecording";
2149
+ const state = text(row, "state");
2150
+ const role = text(row, "my_role");
2151
+ return {
2152
+ id: requireText(row, "recording_id", operation),
2153
+ meetingId: asMeetingId(requireText(row, "meeting_id", operation)),
2154
+ meetingTitle: text(row, "meeting_title") ?? "Meeting",
2155
+ meetingSlug: requireText(row, "meeting_slug", operation),
2156
+ role: role === "host" || role === "cohost" || role === "invitee" ? role : "attendee",
2157
+ state: LIBRARY_STATES.includes(state) ? state : "failed",
2158
+ title: text(row, "recording_title"),
2159
+ fileId: text(row, "file_id"),
2160
+ startedAt: text(row, "started_at"),
2161
+ endedAt: text(row, "ended_at"),
2162
+ durationMs: num(row, "duration_ms"),
2163
+ sizeBytes: num(row, "size_bytes"),
2164
+ failureReason: text(row, "failure_reason"),
2165
+ archivedAt: text(row, "deleted_at"),
2166
+ organizationId: asOrganizationId(requireText(row, "organization_id", operation))
2167
+ };
2168
+ }
2169
+ var SEARCH_SOURCES = [
2170
+ "title",
2171
+ "summary",
2172
+ "decision",
2173
+ "action_item",
2174
+ "note",
2175
+ "transcript"
2176
+ ];
2177
+ function projectSearchHit(row) {
2178
+ const operation = "meet.projectSearchHit";
2179
+ const source = text(row, "source");
2180
+ return {
2181
+ meetingId: asMeetingId(requireText(row, "meeting_id", operation)),
2182
+ meetingTitle: text(row, "meeting_title") ?? "Meeting",
2183
+ meetingSlug: requireText(row, "meeting_slug", operation),
2184
+ meetingAt: text(row, "meeting_at"),
2185
+ source: SEARCH_SOURCES.includes(source) ? source : "note",
2186
+ refId: requireText(row, "ref_id", operation),
2187
+ speaker: text(row, "speaker"),
2188
+ snippet: text(row, "snippet") ?? "",
2189
+ at: text(row, "said_at"),
2190
+ rank: num(row, "rank") ?? 0
2191
+ };
2192
+ }
2193
+ function snippetRuns(snippet) {
2194
+ const runs = [];
2195
+ const pattern = /⟦([^⟧]*)⟧/g;
2196
+ let last = 0;
2197
+ for (const match of snippet.matchAll(pattern)) {
2198
+ const start = match.index ?? 0;
2199
+ if (start > last) runs.push({ text: snippet.slice(last, start), hit: false });
2200
+ runs.push({ text: match[1] ?? "", hit: true });
2201
+ last = start + match[0].length;
2202
+ }
2203
+ if (last < snippet.length) runs.push({ text: snippet.slice(last), hit: false });
2204
+ return runs;
2205
+ }
2206
+
2207
+ // src/core/app-panel.ts
2208
+ function appPanelOf(meeting) {
2209
+ const raw = meeting?.metadata?.["app_panel"];
2210
+ if (typeof raw !== "object" || raw === null || Array.isArray(raw)) return null;
2211
+ const key = raw["key"];
2212
+ const recordId = raw["record_id"];
2213
+ if (typeof key !== "string" || key.length === 0) return null;
2214
+ if (typeof recordId !== "string" || recordId.length === 0) return null;
2215
+ return { key, recordId };
2216
+ }
2217
+ function scheduleSettingsPatch(settings, appPanel, observers) {
2218
+ const patch = settingsPatch(settings);
2219
+ if (appPanel !== void 0) patch["app_panel"] = { key: appPanel.key, record_id: appPanel.recordId };
2220
+ if (observers !== void 0 && observers.length > 0) patch["observers"] = [...observers];
2221
+ return patch;
2222
+ }
2223
+ function focusFromRow(value) {
2224
+ if (typeof value !== "object" || value === null || Array.isArray(value)) return null;
2225
+ const row = value;
2226
+ if (typeof row["panel"] !== "string") return null;
2227
+ return {
2228
+ panel: row["panel"],
2229
+ section: typeof row["section"] === "string" ? row["section"] : "",
2230
+ byName: typeof row["by_name"] === "string" ? row["by_name"] : "",
2231
+ at: typeof row["at"] === "string" ? row["at"] : ""
2232
+ };
2233
+ }
2234
+
2235
+ // src/core/collab.ts
2236
+ var EMPTY_COLLAB = {
2237
+ loaded: false,
2238
+ error: null,
2239
+ settings: null,
2240
+ polls: [],
2241
+ questions: [],
2242
+ strokes: [],
2243
+ breakout: null,
2244
+ broadcast: null,
2245
+ moveRequest: null,
2246
+ panelRequest: null,
2247
+ captionLanguage: null,
2248
+ captionTranslations: {},
2249
+ captionTranslationError: null,
2250
+ focus: null
2251
+ };
2252
+ function obj(value) {
2253
+ return typeof value === "object" && value !== null && !Array.isArray(value) ? value : null;
2254
+ }
2255
+ function str(value) {
2256
+ return typeof value === "string" ? value : null;
2257
+ }
2258
+ function num2(value) {
2259
+ return typeof value === "number" && Number.isFinite(value) ? value : null;
2260
+ }
2261
+ function strings(value) {
2262
+ return Array.isArray(value) ? value.filter((v) => typeof v === "string") : [];
2263
+ }
2264
+ function pollFromRow(row) {
2265
+ const id = str(row["id"]);
2266
+ if (id === null) return null;
2267
+ const options = [];
2268
+ for (const raw of Array.isArray(row["options"]) ? row["options"] : []) {
2269
+ const option = obj(raw);
2270
+ const optionId = option === null ? null : str(option["id"]);
2271
+ if (option === null || optionId === null) continue;
2272
+ const votes = num2(option["votes"]);
2273
+ options.push({
2274
+ id: optionId,
2275
+ text: str(option["text"]) ?? "",
2276
+ ...votes !== null ? { votes } : {},
2277
+ ...Array.isArray(option["voters"]) ? { voters: strings(option["voters"]) } : {}
2278
+ });
2279
+ }
2280
+ const state = str(row["state"]);
2281
+ return {
2282
+ id,
2283
+ question: str(row["question"]) ?? "",
2284
+ options,
2285
+ multiple: row["multiple"] === true,
2286
+ anonymous: row["anonymous"] === true,
2287
+ state: state === "live" || state === "closed" ? state : "draft",
2288
+ resultsShared: row["results_shared"] === true,
2289
+ voterCount: num2(row["voter_count"]),
2290
+ myVote: strings(row["my_vote"]),
2291
+ createdByName: str(row["created_by_name"]) ?? "",
2292
+ createdAt: str(row["created_at"])
2293
+ };
2294
+ }
2295
+ function questionFromRow(row) {
2296
+ const id = str(row["id"]);
2297
+ if (id === null) return null;
2298
+ const state = str(row["state"]);
2299
+ return {
2300
+ id,
2301
+ text: str(row["text"]) ?? "",
2302
+ askerName: str(row["asker_name"]) ?? "",
2303
+ anonymous: row["anonymous"] === true,
2304
+ state: state === "answered" || state === "dismissed" ? state : "open",
2305
+ answer: str(row["answer"]),
2306
+ answeredByName: str(row["answered_by_name"]),
2307
+ upvotes: num2(row["upvotes"]) ?? 0,
2308
+ upvotedByMe: row["upvoted_by_me"] === true,
2309
+ mine: row["mine"] === true,
2310
+ createdAt: str(row["created_at"])
2311
+ };
2312
+ }
2313
+ function strokeFromRow(row) {
2314
+ const id = str(row["id"]);
2315
+ if (id === null || !Array.isArray(row["points"])) return null;
2316
+ const points = [];
2317
+ for (const raw of row["points"]) {
2318
+ if (Array.isArray(raw) && typeof raw[0] === "number" && typeof raw[1] === "number") {
2319
+ points.push([raw[0], raw[1]]);
2320
+ }
2321
+ }
2322
+ return {
2323
+ id,
2324
+ points,
2325
+ color: str(row["color"]) ?? "#111827",
2326
+ width: num2(row["width"]) ?? 3,
2327
+ tool: row["tool"] === "highlighter" ? "highlighter" : "pen",
2328
+ by: str(row["by"]) ?? "",
2329
+ byName: str(row["by_name"]) ?? ""
2330
+ };
2331
+ }
2332
+ function broadcastFrom(value) {
2333
+ const row = obj(value);
2334
+ const text3 = row === null ? null : str(row["text"]);
2335
+ if (row === null || text3 === null) return null;
2336
+ return { text: text3, by: str(row["by"]) ?? "", at: str(row["at"]) ?? "" };
2337
+ }
2338
+ function planFromRow(row) {
2339
+ const id = str(row["id"]);
2340
+ if (id === null) return null;
2341
+ const rooms = [];
2342
+ for (const raw of Array.isArray(row["rooms"]) ? row["rooms"] : []) {
2343
+ const room = obj(raw);
2344
+ const roomId = room === null ? null : str(room["id"]);
2345
+ if (room === null || roomId === null) continue;
2346
+ rooms.push({ id: roomId, name: str(room["name"]) ?? roomId, identities: strings(room["identities"]) });
2347
+ }
2348
+ return {
2349
+ id,
2350
+ state: row["state"] === "open" ? "open" : "closed",
2351
+ rooms,
2352
+ allowChoose: row["allow_choose"] === true,
2353
+ minutes: num2(row["minutes"]) ?? 0,
2354
+ openedAt: str(row["opened_at"]),
2355
+ endsAt: str(row["ends_at"]),
2356
+ closedAt: str(row["closed_at"]),
2357
+ broadcasts: (Array.isArray(row["broadcasts"]) ? row["broadcasts"] : []).map(broadcastFrom).filter((b) => b !== null)
2358
+ };
2359
+ }
2360
+ function settingsFromRow(row) {
2361
+ if (row === null) return null;
2362
+ return {
2363
+ pollsEnabled: row["polls_enabled"] === true,
2364
+ pollMaxOptions: num2(row["poll_max_options"]) ?? 10,
2365
+ qaEnabled: row["qa_enabled"] === true,
2366
+ qaAnonymousAllowed: row["qa_anonymous_allowed"] === true,
2367
+ whiteboardEnabled: row["whiteboard_enabled"] === true,
2368
+ breakoutsEnabled: row["breakouts_enabled"] === true,
2369
+ breakoutMaxRooms: num2(row["breakout_max_rooms"]) ?? 20,
2370
+ breakoutDefaultMinutes: num2(row["breakout_default_minutes"]) ?? 15,
2371
+ captionTranslationEnabled: row["caption_translation_enabled"] === true,
2372
+ captionTranslationLanguage: str(row["caption_translation_language"]) ?? ""
2373
+ };
2374
+ }
2375
+ function collabFromState(body, previous) {
2376
+ const list = (key, parse) => (Array.isArray(body[key]) ? body[key] : []).map((raw) => {
2377
+ const row = obj(raw);
2378
+ return row === null ? null : parse(row);
2379
+ }).filter((item) => item !== null);
2380
+ const breakoutRow = obj(body["breakout"]);
2381
+ return {
2382
+ ...previous,
2383
+ loaded: true,
2384
+ error: null,
2385
+ settings: settingsFromRow(obj(body["settings"])),
2386
+ polls: list("polls", pollFromRow),
2387
+ questions: list("questions", questionFromRow),
2388
+ strokes: list("strokes", strokeFromRow),
2389
+ breakout: breakoutRow === null ? null : planFromRow(breakoutRow),
2390
+ focus: focusFromRow(body["focus"])
2391
+ };
2392
+ }
2393
+ function upsert(items, next) {
2394
+ const index = items.findIndex((item) => item.id === next.id);
2395
+ if (index === -1) return [...items, next];
2396
+ const copy = items.slice();
2397
+ copy[index] = next;
2398
+ return copy;
2399
+ }
2400
+ function withItem(collab, row, fromRoom = false) {
2401
+ const kind = str(row["kind"]);
2402
+ if (kind === "poll") {
2403
+ const poll = pollFromRow(row);
2404
+ if (poll === null) return collab;
2405
+ const known = collab.polls.find((p) => p.id === poll.id);
2406
+ let merged = fromRoom && known !== void 0 ? { ...poll, myVote: known.myVote } : poll;
2407
+ if (fromRoom && known !== void 0 && !poll.options.some((o) => o.votes !== void 0)) {
2408
+ const counted = new Map(known.options.map((o) => [o.id, o]));
2409
+ merged = {
2410
+ ...merged,
2411
+ voterCount: known.voterCount,
2412
+ options: merged.options.map((o) => {
2413
+ const had = counted.get(o.id);
2414
+ return had === void 0 ? o : { ...o, votes: had.votes, voters: had.voters };
2415
+ })
2416
+ };
2417
+ }
2418
+ return { ...collab, polls: upsert(collab.polls, merged) };
2419
+ }
2420
+ if (kind === "question") {
2421
+ const question = questionFromRow(row);
2422
+ if (question === null) return collab;
2423
+ const known = collab.questions.find((q) => q.id === question.id);
2424
+ const merged = fromRoom && known !== void 0 ? { ...question, mine: known.mine, upvotedByMe: known.upvotedByMe } : question;
2425
+ return { ...collab, questions: upsert(collab.questions, merged) };
2426
+ }
2427
+ if (kind === "stroke") {
2428
+ const stroke = strokeFromRow(row);
2429
+ if (stroke === null) return collab;
2430
+ return { ...collab, strokes: upsert(collab.strokes, stroke) };
2431
+ }
2432
+ if (kind === "breakout") {
2433
+ const plan = planFromRow(row);
2434
+ if (plan === null) return collab;
2435
+ return { ...collab, breakout: plan };
2436
+ }
2437
+ return collab;
2438
+ }
2439
+ function without(collab, ids) {
2440
+ const gone = new Set(ids);
2441
+ return {
2442
+ ...collab,
2443
+ polls: collab.polls.filter((p) => !gone.has(p.id)),
2444
+ questions: collab.questions.filter((q) => !gone.has(q.id)),
2445
+ strokes: collab.strokes.filter((s) => !gone.has(s.id))
2446
+ };
2447
+ }
2448
+ function applyCollabPacket(collab, payload, context) {
2449
+ const item = obj(payload["item"]);
2450
+ if (item !== null) {
2451
+ const kind = str(item["kind"]);
2452
+ let next = withItem(collab, item, true);
2453
+ if (kind === "poll" && str(item["state"]) === "live") {
2454
+ const before = collab.polls.find((p) => p.id === str(item["id"]));
2455
+ if (before === void 0 || before.state !== "live") {
2456
+ next = { ...next, panelRequest: { panel: "polls", at: context.now } };
2457
+ }
2458
+ }
2459
+ if (kind === "breakout") {
2460
+ const plan = planFromRow(item);
2461
+ if (plan !== null && plan.state === "closed") {
2462
+ next = { ...next, moveRequest: { roomId: null, at: context.now } };
2463
+ }
2464
+ }
2465
+ const move = obj(payload["move"]);
2466
+ if (move !== null && str(move["identity"]) === context.localIdentity) {
2467
+ next = { ...next, moveRequest: { roomId: str(move["room_id"]), at: context.now } };
2468
+ }
2469
+ return { next };
2470
+ }
2471
+ if (payload["focus"] !== void 0) {
2472
+ const focus = focusFromRow(payload["focus"]);
2473
+ return focus === null ? { next: null, dropped: "a focus packet without a panel" } : { next: { ...collab, focus } };
2474
+ }
2475
+ if (Array.isArray(payload["removed"])) {
2476
+ return { next: without(collab, strings(payload["removed"])) };
2477
+ }
2478
+ if (Array.isArray(payload["hidden"])) {
2479
+ return context.isHost ? { next: null, dropped: "hosts keep dismissed questions" } : { next: without(collab, strings(payload["hidden"])) };
2480
+ }
2481
+ if (payload["cleared"] === "whiteboard") {
2482
+ return { next: { ...collab, strokes: [] } };
2483
+ }
2484
+ const broadcast = broadcastFrom(payload["broadcast"]);
2485
+ if (broadcast !== null) {
2486
+ return { next: { ...collab, broadcast } };
2487
+ }
2488
+ return { next: null, dropped: "a collab packet with nothing this version understands" };
2427
2489
  }
2428
- var LIBRARY_STATES = [
2429
- "idle",
2430
- "starting",
2431
- "recording",
2432
- "stopping",
2433
- "processing",
2434
- "available",
2435
- "expired",
2436
- "failed"
2437
- ];
2438
- function projectLibraryRecording(row) {
2439
- const operation = "meet.projectLibraryRecording";
2440
- const state = text(row, "state");
2441
- const role = text(row, "my_role");
2442
- return {
2443
- id: requireText(row, "recording_id", operation),
2444
- meetingId: asMeetingId(requireText(row, "meeting_id", operation)),
2445
- meetingTitle: text(row, "meeting_title") ?? "Meeting",
2446
- meetingSlug: requireText(row, "meeting_slug", operation),
2447
- role: role === "host" || role === "cohost" || role === "invitee" ? role : "attendee",
2448
- state: LIBRARY_STATES.includes(state) ? state : "failed",
2449
- title: text(row, "recording_title"),
2450
- fileId: text(row, "file_id"),
2451
- startedAt: text(row, "started_at"),
2452
- endedAt: text(row, "ended_at"),
2453
- durationMs: num2(row, "duration_ms"),
2454
- sizeBytes: num2(row, "size_bytes"),
2455
- failureReason: text(row, "failure_reason"),
2456
- archivedAt: text(row, "deleted_at"),
2457
- organizationId: asOrganizationId(requireText(row, "organization_id", operation))
2458
- };
2490
+ function assignedRoom(plan, identity) {
2491
+ if (plan === null || plan.state !== "open" || identity === null) return null;
2492
+ return plan.rooms.find((room) => room.identities.includes(identity)) ?? null;
2459
2493
  }
2460
- var SEARCH_SOURCES = [
2461
- "title",
2462
- "summary",
2463
- "decision",
2464
- "action_item",
2465
- "note",
2466
- "transcript"
2467
- ];
2468
- function projectSearchHit(row) {
2469
- const operation = "meet.projectSearchHit";
2470
- const source = text(row, "source");
2471
- return {
2472
- meetingId: asMeetingId(requireText(row, "meeting_id", operation)),
2473
- meetingTitle: text(row, "meeting_title") ?? "Meeting",
2474
- meetingSlug: requireText(row, "meeting_slug", operation),
2475
- meetingAt: text(row, "meeting_at"),
2476
- source: SEARCH_SOURCES.includes(source) ? source : "note",
2477
- refId: requireText(row, "ref_id", operation),
2478
- speaker: text(row, "speaker"),
2479
- snippet: text(row, "snippet") ?? "",
2480
- at: text(row, "said_at"),
2481
- rank: num2(row, "rank") ?? 0
2482
- };
2494
+ function autoAssign(identities, rooms, exclude = []) {
2495
+ const count2 = Math.max(1, Math.floor(rooms));
2496
+ const out = Array.from({ length: count2 }, () => []);
2497
+ const skip = new Set(exclude);
2498
+ let index = 0;
2499
+ for (const identity of identities) {
2500
+ if (skip.has(identity)) continue;
2501
+ out[index % count2].push(identity);
2502
+ index += 1;
2503
+ }
2504
+ return out;
2483
2505
  }
2484
- function snippetRuns(snippet) {
2485
- const runs = [];
2486
- const pattern = /⟦([^⟧]*)⟧/g;
2487
- let last = 0;
2488
- for (const match of snippet.matchAll(pattern)) {
2489
- const start = match.index ?? 0;
2490
- if (start > last) runs.push({ text: snippet.slice(last, start), hit: false });
2491
- runs.push({ text: match[1] ?? "", hit: true });
2492
- last = start + match[0].length;
2506
+ function breakoutRemainingMs(plan, now) {
2507
+ if (plan === null || plan.state !== "open" || plan.endsAt === null) return null;
2508
+ const end = Date.parse(plan.endsAt);
2509
+ return Number.isNaN(end) ? null : Math.max(0, end - now);
2510
+ }
2511
+ function defaultCaptionLanguage(settings, viewerLanguage) {
2512
+ const configured = settings?.captionTranslationLanguage.trim() ?? "";
2513
+ if (configured.length > 0) return configured;
2514
+ const own = (viewerLanguage ?? "").trim();
2515
+ return own.length > 0 ? own : "en";
2516
+ }
2517
+ var CAPTION_LANGUAGES = [
2518
+ { tag: "en", name: "English" },
2519
+ { tag: "es", name: "Spanish" },
2520
+ { tag: "fr", name: "French" },
2521
+ { tag: "de", name: "German" },
2522
+ { tag: "it", name: "Italian" },
2523
+ { tag: "pt", name: "Portuguese" },
2524
+ { tag: "nl", name: "Dutch" },
2525
+ { tag: "pl", name: "Polish" },
2526
+ { tag: "ru", name: "Russian" },
2527
+ { tag: "uk", name: "Ukrainian" },
2528
+ { tag: "tr", name: "Turkish" },
2529
+ { tag: "ar", name: "Arabic" },
2530
+ { tag: "he", name: "Hebrew" },
2531
+ { tag: "fa", name: "Persian" },
2532
+ { tag: "hi", name: "Hindi" },
2533
+ { tag: "bn", name: "Bengali" },
2534
+ { tag: "zh", name: "Chinese" },
2535
+ { tag: "ja", name: "Japanese" },
2536
+ { tag: "ko", name: "Korean" },
2537
+ { tag: "vi", name: "Vietnamese" },
2538
+ { tag: "th", name: "Thai" },
2539
+ { tag: "id", name: "Indonesian" },
2540
+ { tag: "sv", name: "Swedish" }
2541
+ ];
2542
+ async function loadMeetingActivities(api, meetingId, meetingPass = null) {
2543
+ let response;
2544
+ try {
2545
+ response = await api.request({
2546
+ path: `${MEET_ROUTES.collab}?meeting_id=${encodeURIComponent(meetingId)}`,
2547
+ method: "GET",
2548
+ operation: "meet.loadMeetingActivities",
2549
+ requireAuth: meetingPass === null,
2550
+ ...meetingPass !== null ? { headers: { [MEETING_PASS_HEADER]: meetingPass } } : {}
2551
+ });
2552
+ } catch (error) {
2553
+ return {
2554
+ collab: null,
2555
+ refusal: {
2556
+ message: `Polls, questions and the whiteboard could not be read (${error.message}).`,
2557
+ remedy: error.remedy ?? "Reload the page."
2558
+ }
2559
+ };
2493
2560
  }
2494
- if (last < snippet.length) runs.push({ text: snippet.slice(last), hit: false });
2495
- return runs;
2561
+ if (!response.ok) {
2562
+ let body = null;
2563
+ try {
2564
+ body = await response.json();
2565
+ } catch {
2566
+ body = null;
2567
+ }
2568
+ const said = readServerRefusal(body);
2569
+ return {
2570
+ collab: null,
2571
+ refusal: {
2572
+ message: said?.reason ?? `Polls and questions could not be read (the server answered ${response.status}).`,
2573
+ remedy: said?.remedy || "Reload the page."
2574
+ }
2575
+ };
2576
+ }
2577
+ return { collab: collabFromState(await response.json(), EMPTY_COLLAB), refusal: null };
2496
2578
  }
2497
2579
 
2498
2580
  // src/core/names.ts
@@ -2697,6 +2779,7 @@ var NO_GRANTS = {
2697
2779
  };
2698
2780
  var EMPTY = {
2699
2781
  phase: "idle",
2782
+ waiting: null,
2700
2783
  meeting: null,
2701
2784
  localIdentity: null,
2702
2785
  grants: NO_GRANTS,
@@ -3564,7 +3647,10 @@ function orderParticipants(participants, activeSpeakers) {
3564
3647
  }
3565
3648
  function computeStage(args) {
3566
3649
  const pageSize = Math.max(1, args.pageSize ?? DEFAULT_PAGE_SIZE);
3567
- const ordered = orderParticipants(args.participants, args.activeSpeakers);
3650
+ const ordered = orderParticipants(
3651
+ args.participants.filter((participant) => !isObserverRole(participant.role)),
3652
+ args.activeSpeakers
3653
+ );
3568
3654
  const sharer = ordered.find((participant) => participant.screenSharing) ?? null;
3569
3655
  const pinnedParticipant = args.pinned === null ? null : ordered.find((participant) => participant.identity === args.pinned) ?? null;
3570
3656
  const spotlit = args.spotlight == null ? null : ordered.find((participant) => participant.identity === args.spotlight) ?? null;
@@ -3974,19 +4060,25 @@ function roleOf(participants, identity) {
3974
4060
  return participants.find((p) => p.identity === identity)?.role ?? "participant";
3975
4061
  }
3976
4062
  var END_MEETING_NOT_SERVED = "Ending a meeting is not available on this server yet";
3977
- function lobbyEntryFrom(attendee) {
3978
- if (attendee.admissionState !== "knocking") return null;
4063
+ function lobbyEntryFrom(attendee, now = Date.now()) {
4064
+ if (!WAITING_ADMISSION_STATES.includes(attendee.admissionState)) return null;
4065
+ const expires = attendee.knockExpiresAt ? Date.parse(attendee.knockExpiresAt) : Number.NaN;
4066
+ if (!Number.isNaN(expires) && expires <= now) return null;
3979
4067
  return {
3980
4068
  identity: attendee.identity,
3981
4069
  displayName: attendee.displayName,
3982
4070
  avatarUrl: attendee.avatarUrl,
3983
4071
  userId: attendee.userId,
3984
4072
  state: "knocking",
3985
- knockedAt: attendee.joinedAt ?? (/* @__PURE__ */ new Date()).toISOString(),
4073
+ knockedAt: attendee.knockedAt ?? attendee.joinedAt ?? (/* @__PURE__ */ new Date()).toISOString(),
4074
+ knockExpiresAt: attendee.knockExpiresAt ?? null,
3986
4075
  decidedAt: null,
3987
4076
  decidedBy: null
3988
4077
  };
3989
4078
  }
4079
+ function decidedState(state) {
4080
+ return WAITING_ADMISSION_STATES.includes(state) ? "expired" : state;
4081
+ }
3990
4082
  function createMeetingSession(options) {
3991
4083
  const { manager, repository, store, identity } = options;
3992
4084
  let channel = null;
@@ -4010,11 +4102,6 @@ function createMeetingSession(options) {
4010
4102
  }
4011
4103
  return identity.userId;
4012
4104
  }
4013
- function knockIdentity() {
4014
- return asParticipantIdentity(
4015
- identity.userId !== null ? `user:${identity.userId}` : `guest:${identity.deviceId}`
4016
- );
4017
- }
4018
4105
  function requireMeeting(operation) {
4019
4106
  const meeting = store.snapshot().meeting;
4020
4107
  if (meeting === null) {
@@ -4026,6 +4113,191 @@ function createMeetingSession(options) {
4026
4113
  }
4027
4114
  return meeting;
4028
4115
  }
4116
+ let polling = false;
4117
+ let pollTimer = null;
4118
+ let reading = null;
4119
+ let admittedOnce = false;
4120
+ let unlisten = null;
4121
+ function report(level, message, remedy) {
4122
+ options.onDiagnostic?.(remedy === void 0 ? { level, message } : { level, message, remedy });
4123
+ }
4124
+ function pollMs() {
4125
+ const seconds = store.snapshot().waiting?.pollSeconds ?? 3;
4126
+ return Math.max(1, Math.min(60, seconds)) * 1e3;
4127
+ }
4128
+ function schedule(ms2) {
4129
+ if (pollTimer !== null) clearTimeout(pollTimer);
4130
+ if (!polling) return;
4131
+ pollTimer = setTimeout(() => {
4132
+ pollTimer = null;
4133
+ void readState2().finally(() => schedule(pollMs()));
4134
+ }, ms2);
4135
+ }
4136
+ function listen() {
4137
+ if (unlisten !== null || typeof window === "undefined") return;
4138
+ const nudge = () => {
4139
+ if (typeof document !== "undefined" && document.visibilityState === "hidden") return;
4140
+ void readState2();
4141
+ };
4142
+ const goodbye = () => {
4143
+ const meeting = store.snapshot().meeting;
4144
+ const pass = options.resolvePass?.() ?? null;
4145
+ const waiting = store.snapshot().waiting;
4146
+ if (meeting === null || pass === null || waiting === null) return;
4147
+ if (!WAITING_ADMISSION_STATES.includes(waiting.admission)) return;
4148
+ try {
4149
+ navigator.sendBeacon?.(
4150
+ options.api.url(MEET_ROUTES.leave),
4151
+ new Blob([JSON.stringify({ meeting_id: meeting.id, intent: "withdraw", meeting_pass: pass })], {
4152
+ type: "application/json"
4153
+ })
4154
+ );
4155
+ } catch {
4156
+ }
4157
+ };
4158
+ window.addEventListener("online", nudge);
4159
+ window.addEventListener("pagehide", goodbye);
4160
+ document.addEventListener("visibilitychange", nudge);
4161
+ unlisten = () => {
4162
+ window.removeEventListener("online", nudge);
4163
+ window.removeEventListener("pagehide", goodbye);
4164
+ document.removeEventListener("visibilitychange", nudge);
4165
+ };
4166
+ }
4167
+ function stopWaiting() {
4168
+ polling = false;
4169
+ if (pollTimer !== null) clearTimeout(pollTimer);
4170
+ pollTimer = null;
4171
+ unlisten?.();
4172
+ unlisten = null;
4173
+ }
4174
+ function patchWaiting(next) {
4175
+ const current = store.snapshot().waiting;
4176
+ const base = current ?? {
4177
+ admission: "knocking",
4178
+ admittersPresent: null,
4179
+ reknockAllowed: true,
4180
+ knockExpiresAt: null,
4181
+ pollSeconds: 3,
4182
+ liveVersion: 0
4183
+ };
4184
+ store.patch({ waiting: { ...base, ...next } });
4185
+ }
4186
+ function apply(read) {
4187
+ const snapshot = store.snapshot();
4188
+ if (snapshot.phase !== "lobby") return;
4189
+ const admission = read.admission_state ?? null;
4190
+ const ended = admission === "ended" || read.session_phase === "ended" || read.meeting_phase === "ended";
4191
+ if (admission === "admitted" && !ended) {
4192
+ if (admittedOnce) return;
4193
+ admittedOnce = true;
4194
+ stopWaiting();
4195
+ options.onAdmitted?.();
4196
+ return;
4197
+ }
4198
+ if (ended) {
4199
+ stopWaiting();
4200
+ patchWaiting({ admission: "ended" });
4201
+ const meeting = snapshot.meeting;
4202
+ if (read.meeting_phase === "ended" && meeting !== null && meeting.endedAt === null) {
4203
+ store.patch({ meeting: { ...meeting, endedAt: (/* @__PURE__ */ new Date()).toISOString() } });
4204
+ }
4205
+ return;
4206
+ }
4207
+ if (admission === null) return;
4208
+ patchWaiting({
4209
+ admission,
4210
+ admittersPresent: read.admitters_present ?? null,
4211
+ reknockAllowed: read.reknock_allowed ?? true,
4212
+ knockExpiresAt: read.knock_expires_at ?? null,
4213
+ pollSeconds: read.poll_seconds ?? snapshot.waiting?.pollSeconds ?? 3,
4214
+ liveVersion: read.live_version ?? 0
4215
+ });
4216
+ if (admission === "denied" || admission === "expired" || admission === "withdrawn" || admission === "removed") {
4217
+ stopWaiting();
4218
+ }
4219
+ }
4220
+ async function readState2() {
4221
+ if (reading !== null) return reading;
4222
+ const meeting = store.snapshot().meeting;
4223
+ const pass = options.resolvePass?.() ?? null;
4224
+ if (meeting === null || pass === null) return;
4225
+ reading = (async () => {
4226
+ try {
4227
+ const response = await options.api.request({
4228
+ path: `${MEET_ROUTES.state}?meeting_id=${encodeURIComponent(meeting.id)}`,
4229
+ method: "GET",
4230
+ requireAuth: false,
4231
+ operation: "meet.state",
4232
+ headers: { [MEETING_PASS_HEADER]: pass }
4233
+ });
4234
+ if (!response.ok) {
4235
+ let body = null;
4236
+ try {
4237
+ body = await response.json();
4238
+ } catch {
4239
+ body = null;
4240
+ }
4241
+ const refusal2 = readServerRefusal(body);
4242
+ report(
4243
+ "warn",
4244
+ refusal2?.reason ?? `meet.state: the server answered ${response.status}`,
4245
+ refusal2?.remedy ?? "The waiting room keeps asking; nothing on this screen changes until the server answers."
4246
+ );
4247
+ return;
4248
+ }
4249
+ apply(await response.json());
4250
+ } catch (error) {
4251
+ report("info", `meet.state: ${String(error)}`, "Waiting continues; the state is read again shortly.");
4252
+ } finally {
4253
+ reading = null;
4254
+ }
4255
+ })();
4256
+ return reading;
4257
+ }
4258
+ async function postLeave(intent) {
4259
+ const meeting = requireMeeting(`meet.${intent}`);
4260
+ const pass = options.resolvePass?.() ?? null;
4261
+ const response = await options.api.request({
4262
+ path: MEET_ROUTES.leave,
4263
+ method: "POST",
4264
+ requireAuth: false,
4265
+ operation: `meet.${intent}`,
4266
+ body: { meeting_id: meeting.id, intent },
4267
+ ...pass !== null ? { headers: { [MEETING_PASS_HEADER]: pass } } : {}
4268
+ });
4269
+ if (!response.ok) {
4270
+ let body = null;
4271
+ try {
4272
+ body = await response.json();
4273
+ } catch {
4274
+ body = null;
4275
+ }
4276
+ const refusal2 = readServerRefusal(body);
4277
+ throw new MeetError(
4278
+ "transport",
4279
+ refusal2?.reason ?? `meet.${intent}: the server answered ${response.status}`,
4280
+ refusal2?.remedy ?? "You are still in the waiting room. Try again."
4281
+ );
4282
+ }
4283
+ }
4284
+ function hint() {
4285
+ channel?.send(MEET_EVENTS.stateChanged, { v: null });
4286
+ }
4287
+ async function refreshQueue() {
4288
+ const meeting = store.snapshot().meeting;
4289
+ if (meeting === null || isGuest(identity) || !isHostRole(effectiveRole(store.snapshot()))) return;
4290
+ try {
4291
+ const attendees = await repository.attendees(meeting.id);
4292
+ store.learn(attendees);
4293
+ for (const attendee of attendees) {
4294
+ const entry = lobbyEntryFrom(attendee);
4295
+ if (entry !== null) store.upsertLobby(entry);
4296
+ else store.setAdmission(attendee.identity, decidedState(attendee.admissionState), null);
4297
+ }
4298
+ } catch {
4299
+ }
4300
+ }
4029
4301
  return {
4030
4302
  watch(roomName) {
4031
4303
  channel?.close();
@@ -4033,58 +4305,22 @@ function createMeetingSession(options) {
4033
4305
  topic: roomTopic(roomName),
4034
4306
  broadcast: [
4035
4307
  {
4036
- event: MEET_EVENTS.knock,
4037
- onMessage: (message) => {
4038
- store.upsertLobby(message.data);
4039
- }
4040
- },
4041
- {
4042
- event: MEET_EVENTS.admission,
4043
- onMessage: (message) => {
4044
- const verdict = message.data;
4045
- const decided = asParticipantIdentity(verdict.identity);
4046
- store.setAdmission(
4047
- decided,
4048
- verdict.admitted ? "admitted" : "denied",
4049
- verdict.by === null ? null : verdict.by
4050
- );
4051
- if (decided === knockIdentity()) {
4052
- if (verdict.admitted) options.onAdmitted?.();
4053
- else options.onDenied?.();
4054
- }
4055
- }
4056
- },
4057
- {
4058
- event: MEET_EVENTS.lock,
4059
- onMessage: (message) => {
4060
- const { locked } = message.data;
4061
- const meeting = store.snapshot().meeting;
4062
- store.patch({
4063
- locked,
4064
- ...meeting !== null ? { meeting: { ...meeting, locked } } : {}
4065
- });
4066
- }
4067
- },
4068
- {
4069
- event: MEET_EVENTS.recording,
4070
- onMessage: (message) => {
4071
- const payload = message.data;
4072
- store.setRecording(
4073
- payload.state,
4074
- payload.by === null ? null : payload.by,
4075
- "room"
4076
- );
4308
+ // 🚨 PAYLOAD-FREE. Every receiver reads again; a spoofed hint only causes a read.
4309
+ event: MEET_EVENTS.stateChanged,
4310
+ onMessage: () => {
4311
+ if (store.snapshot().phase === "lobby") void readState2();
4312
+ void refreshQueue();
4077
4313
  }
4078
4314
  }
4079
4315
  ],
4080
- // THE BACKFILL DOOR. A host whose socket blipped must not miss a knock:
4081
- // re-reading the meeting restores lock/recording truth, and knockers
4082
- // re-announce on their own timer.
4316
+ // THE BACKFILL DOOR. A socket blip must not lose a decision: re-read the meeting
4317
+ // row, and a waiting tab re-reads its own state.
4083
4318
  onBackfill: async () => {
4084
4319
  const meeting = store.snapshot().meeting;
4085
4320
  if (meeting === null) return;
4086
4321
  const fresh = isGuest(identity) ? await repository.meetingBySlug(meeting.slug) : await readMeeting(meeting.id);
4087
4322
  store.patch({ meeting: fresh, locked: fresh.locked });
4323
+ if (store.snapshot().phase === "lobby") void readState2();
4088
4324
  }
4089
4325
  });
4090
4326
  },
@@ -4145,7 +4381,7 @@ function createMeetingSession(options) {
4145
4381
  store.upsertLobby(entry);
4146
4382
  return;
4147
4383
  }
4148
- store.setAdmission(attendee.identity, attendee.admissionState, null);
4384
+ store.setAdmission(attendee.identity, decidedState(attendee.admissionState), null);
4149
4385
  }
4150
4386
  },
4151
4387
  {
@@ -4197,62 +4433,75 @@ function createMeetingSession(options) {
4197
4433
  if (entry !== null) {
4198
4434
  store.upsertLobby(entry);
4199
4435
  } else {
4200
- store.setAdmission(attendee.identity, attendee.admissionState, null);
4436
+ store.setAdmission(attendee.identity, decidedState(attendee.admissionState), null);
4201
4437
  }
4202
4438
  }
4203
4439
  }
4204
4440
  });
4205
4441
  meetingChannel.requestBackfill("manual");
4206
4442
  },
4207
- knock(roomName) {
4208
- const handle = channel ?? manager.open({ topic: roomTopic(roomName) });
4209
- channel = handle;
4210
- const entry = {
4211
- identity: knockIdentity(),
4212
- displayName: identity.displayName,
4213
- avatarUrl: identity.avatarUrl,
4214
- userId: identity.userId,
4215
- state: "knocking",
4216
- knockedAt: (/* @__PURE__ */ new Date()).toISOString(),
4217
- decidedAt: null,
4218
- decidedBy: null
4219
- };
4220
- store.upsertLobby(entry);
4221
- handle.send(MEET_EVENTS.knock, entry);
4222
- },
4223
4443
  async admit(entry, admitted) {
4224
4444
  const by = requireHostUser("meet.admit");
4225
4445
  const meeting = requireMeeting("meet.admit");
4226
- await repository.admit({
4446
+ const row = await repository.decideAdmission({
4227
4447
  meetingId: meeting.id,
4228
4448
  identity: entry.identity,
4229
- admitted,
4449
+ decision: admitted ? "admit" : "deny",
4230
4450
  byUserId: by
4231
4451
  });
4232
4452
  store.upsertLobby({
4233
4453
  ...entry,
4234
- state: admitted ? "admitted" : "denied",
4454
+ state: decidedState(row.admissionState),
4235
4455
  decidedAt: (/* @__PURE__ */ new Date()).toISOString(),
4236
4456
  decidedBy: by
4237
4457
  });
4238
- channel?.send(MEET_EVENTS.admission, {
4458
+ hint();
4459
+ },
4460
+ async undeny(entry) {
4461
+ const by = requireHostUser("meet.undeny");
4462
+ const meeting = requireMeeting("meet.undeny");
4463
+ const row = await repository.decideAdmission({
4464
+ meetingId: meeting.id,
4239
4465
  identity: entry.identity,
4240
- admitted,
4241
- by
4466
+ decision: "undeny",
4467
+ byUserId: by
4242
4468
  });
4469
+ store.upsertLobby({ ...entry, state: decidedState(row.admissionState), decidedAt: (/* @__PURE__ */ new Date()).toISOString(), decidedBy: by });
4470
+ hint();
4243
4471
  },
4244
4472
  async admitAll() {
4245
- const knocking = store.snapshot().lobby.filter((entry) => entry.state === "knocking");
4246
- for (const entry of knocking) {
4247
- await this.admit(entry, true);
4473
+ const by = requireHostUser("meet.admitAll");
4474
+ const meeting = requireMeeting("meet.admitAll");
4475
+ const rows = await repository.admitAll(meeting.id, by);
4476
+ for (const row of rows) {
4477
+ store.setAdmission(row.identity, row.admissionState, by);
4248
4478
  }
4479
+ hint();
4480
+ },
4481
+ startWaiting() {
4482
+ if (polling) return;
4483
+ polling = true;
4484
+ admittedOnce = false;
4485
+ listen();
4486
+ schedule(0);
4487
+ },
4488
+ stopWaiting,
4489
+ readState: readState2,
4490
+ async withdraw() {
4491
+ stopWaiting();
4492
+ await postLeave("withdraw");
4493
+ patchWaiting({ admission: "withdrawn" });
4494
+ hint();
4495
+ },
4496
+ async askAgain() {
4497
+ await options.rejoin?.();
4249
4498
  },
4250
4499
  async setLock(locked) {
4251
4500
  const by = requireHostUser("meet.setLock");
4252
4501
  const meeting = requireMeeting("meet.setLock");
4253
4502
  const updated = await repository.setLock(meeting.id, locked, by);
4254
4503
  store.patch({ meeting: updated, locked: updated.locked });
4255
- channel?.send(MEET_EVENTS.lock, { locked: updated.locked });
4504
+ hint();
4256
4505
  return updated;
4257
4506
  },
4258
4507
  /**
@@ -4310,6 +4559,7 @@ function createMeetingSession(options) {
4310
4559
  return { endedAt, served: true };
4311
4560
  },
4312
4561
  dispose() {
4562
+ stopWaiting();
4313
4563
  channel?.close();
4314
4564
  channel = null;
4315
4565
  meetingChannel?.close();
@@ -5170,13 +5420,14 @@ function createRoomEngine(options) {
5170
5420
  }
5171
5421
  };
5172
5422
  async function connect(request) {
5173
- store.patch({ phase: "requesting-token", failure: null });
5423
+ store.patch({ phase: "requesting-token", failure: null, waiting: null });
5174
5424
  const token = await tokens.get({
5175
5425
  roomName: request.roomName,
5176
5426
  identity,
5177
5427
  ...request.meetingId !== void 0 ? { meetingId: request.meetingId } : {},
5178
5428
  ...request.slug !== void 0 ? { slug: request.slug } : {},
5179
- ...request.breakoutRoomId != null ? { breakoutRoomId: request.breakoutRoomId } : {}
5429
+ ...request.breakoutRoomId != null ? { breakoutRoomId: request.breakoutRoomId } : {},
5430
+ ...request.observe === true ? { observe: true } : {}
5180
5431
  });
5181
5432
  current = token;
5182
5433
  store.patch({
@@ -5187,14 +5438,25 @@ function createRoomEngine(options) {
5187
5438
  standing: token.standing,
5188
5439
  canRecord: token.canRecord,
5189
5440
  canPublish: token.canPublish,
5190
- canSubscribe: token.canSubscribe
5441
+ canSubscribe: token.canSubscribe,
5442
+ observersVisibleTo: token.observersVisibleTo ?? "everyone"
5191
5443
  }
5192
5444
  });
5193
- if (token.lobby) {
5194
- store.patch({ phase: "lobby" });
5445
+ if (token.lobby || !token.canSubscribe) {
5446
+ store.patch({
5447
+ phase: "lobby",
5448
+ waiting: {
5449
+ admission: token.admissionState !== void 0 && token.admissionState !== "admitted" ? token.admissionState : "knocking",
5450
+ admittersPresent: null,
5451
+ reknockAllowed: true,
5452
+ knockExpiresAt: null,
5453
+ pollSeconds: token.pollSeconds ?? 3,
5454
+ liveVersion: 0
5455
+ }
5456
+ });
5195
5457
  return;
5196
5458
  }
5197
- store.patch({ phase: "connecting" });
5459
+ store.patch({ phase: "connecting", waiting: null });
5198
5460
  const created = await options.driverFactory(handlers);
5199
5461
  if (disposed) {
5200
5462
  await created.disconnect();
@@ -5677,7 +5939,9 @@ function createInRoomClient(options) {
5677
5939
  ...request.allowChoose !== void 0 ? { allow_choose: request.allowChoose } : {},
5678
5940
  ...request.minutes !== void 0 ? { minutes: request.minutes } : {},
5679
5941
  ...request.identity !== void 0 ? { identity: request.identity } : {},
5680
- ...request.roomId !== void 0 ? { room_id: request.roomId } : {}
5942
+ ...request.roomId !== void 0 ? { room_id: request.roomId } : {},
5943
+ ...request.panel !== void 0 ? { panel: request.panel } : {},
5944
+ ...request.section !== void 0 ? { section: request.section } : {}
5681
5945
  },
5682
5946
  ...hdrs !== void 0 ? { headers: hdrs } : {}
5683
5947
  });
@@ -6453,7 +6717,7 @@ function observedPhase(input) {
6453
6717
  case "left":
6454
6718
  return "prejoin";
6455
6719
  case "lobby":
6456
- return "waiting:knocking";
6720
+ return waitingPhase(snapshot);
6457
6721
  case "requesting-token":
6458
6722
  case "connecting":
6459
6723
  return "joining";
@@ -6462,6 +6726,24 @@ function observedPhase(input) {
6462
6726
  return "in_call";
6463
6727
  }
6464
6728
  }
6729
+ function waitingPhase(snapshot) {
6730
+ switch (snapshot.waiting?.admission) {
6731
+ case "waiting_host":
6732
+ return "waiting:host_not_started";
6733
+ case "denied":
6734
+ return "denied";
6735
+ case "expired":
6736
+ return "knock_expired";
6737
+ case "withdrawn":
6738
+ return "left";
6739
+ case "ended":
6740
+ return "ended";
6741
+ case "removed":
6742
+ return "refused:removed";
6743
+ default:
6744
+ return "waiting:knocking";
6745
+ }
6746
+ }
6465
6747
  var ADMITTER_ROLES = /* @__PURE__ */ new Set(["host", "cohost"]);
6466
6748
  function observe(input) {
6467
6749
  const { snapshot } = input;
@@ -6476,7 +6758,7 @@ function observe(input) {
6476
6758
  reason: phase.startsWith("refused:") ? phase.slice("refused:".length) : null,
6477
6759
  role,
6478
6760
  connection: snapshot?.phase === "reconnecting" ? "reconnecting" : "stable",
6479
- hostPresent: inCall && snapshot !== null ? snapshot.participants.some((p) => ADMITTER_ROLES.has(p.role)) : null,
6761
+ hostPresent: inCall && snapshot !== null ? snapshot.participants.some((p) => ADMITTER_ROLES.has(p.role)) : snapshot?.phase === "lobby" ? snapshot.waiting?.admittersPresent ?? null : null,
6480
6762
  lobbyCount: canAdmit && snapshot !== null ? snapshot.lobby.filter((entry) => entry.state === "knocking").length : 0,
6481
6763
  recording: snapshot?.recording === "recording" || snapshot?.recording === "stopping",
6482
6764
  audioBlocked: null,
@@ -6602,6 +6884,7 @@ export {
6602
6884
  activeWrapUpSummary,
6603
6885
  activityAttention,
6604
6886
  announceMeeting,
6887
+ appPanelOf,
6605
6888
  applyCollabPacket,
6606
6889
  applyRoomEvent,
6607
6890
  asCallId,
@@ -6653,6 +6936,7 @@ export {
6653
6936
  defaultCaptionLanguage,
6654
6937
  displayNameFor,
6655
6938
  fileDownloadPath,
6939
+ focusFromRow,
6656
6940
  gridColumns,
6657
6941
  groupTranscript,
6658
6942
  guestRecordAccessExplanation,
@@ -6667,6 +6951,7 @@ export {
6667
6951
  isLinkFollower,
6668
6952
  isListenOnly,
6669
6953
  isNoteTaker,
6954
+ isObserverRole,
6670
6955
  isRealName,
6671
6956
  isRoomEventTopic,
6672
6957
  learnName,
@@ -6731,6 +7016,7 @@ export {
6731
7016
  roleOf,
6732
7017
  roomTopic,
6733
7018
  rootAttributes,
7019
+ scheduleSettingsPatch,
6734
7020
  searchTranscript,
6735
7021
  sendFollowUp,
6736
7022
  setCaptionLanguage,