@ai-matrx/meet 0.8.1 → 0.10.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs CHANGED
@@ -50,6 +50,7 @@ __export(src_exports, {
50
50
  LISTEN_ONLY_NOTICE: () => LISTEN_ONLY_NOTICE,
51
51
  MEETING_INVITE_KIND: () => MEETING_INVITE_KIND,
52
52
  MEETING_INVITE_VERSION: () => MEETING_INVITE_VERSION,
53
+ MEETING_PASS_HEADER: () => MEETING_PASS_HEADER,
53
54
  MEETING_RECORD_GRACE_MS: () => MEETING_RECORD_GRACE_MS,
54
55
  MEET_ANSWER_CONTEXT_EVENT: () => MEET_ANSWER_CONTEXT_EVENT,
55
56
  MEET_ANSWER_EVENTS: () => MEET_ANSWER_EVENTS,
@@ -65,12 +66,12 @@ __export(src_exports, {
65
66
  ORGANIZATION_HEADER: () => ORGANIZATION_HEADER,
66
67
  PARTICIPANT_CAN_RECORD_ATTRIBUTE: () => PARTICIPANT_CAN_RECORD_ATTRIBUTE,
67
68
  PARTICIPANT_ROLE_ATTRIBUTE: () => PARTICIPANT_ROLE_ATTRIBUTE,
69
+ PARTICIPANT_STANDING_ATTRIBUTE: () => PARTICIPANT_STANDING_ATTRIBUTE,
68
70
  REACTION_TTL_MS: () => REACTION_TTL_MS,
69
71
  REFUSED_ABSENCE: () => REFUSED_ABSENCE,
70
72
  ROLE_RANK: () => ROLE_RANK,
71
73
  ROOM_EVENTS: () => ROOM_EVENTS,
72
74
  ROOM_EVENT_PREFIX: () => ROOM_EVENT_PREFIX,
73
- ROOM_TOKEN_HEADER: () => ROOM_TOKEN_HEADER,
74
75
  RPCS: () => RPCS,
75
76
  TABLES: () => TABLES,
76
77
  TRANSCRIPTION_ATTRIBUTES: () => TRANSCRIPTION_ATTRIBUTES,
@@ -141,6 +142,7 @@ __export(src_exports, {
141
142
  isGuest: () => isGuest,
142
143
  isGuestIdentity: () => isGuestIdentity,
143
144
  isHostRole: () => isHostRole,
145
+ isLinkFollower: () => isLinkFollower,
144
146
  isListenOnly: () => isListenOnly,
145
147
  isNoteTaker: () => isNoteTaker,
146
148
  isRealName: () => isRealName,
@@ -159,6 +161,7 @@ __export(src_exports, {
159
161
  meetingInviteAction: () => meetingInviteAction,
160
162
  meetingLength: () => meetingLength,
161
163
  meetingLink: () => meetingLink,
164
+ meetingPassHeaders: () => meetingPassHeaders,
162
165
  meetingPath: () => meetingPath,
163
166
  meetingSlug: () => meetingSlug,
164
167
  meetingTopic: () => meetingTopic,
@@ -202,6 +205,7 @@ __export(src_exports, {
202
205
  recordingLabel: () => recordingLabel,
203
206
  recordingNotice: () => recordingNotice,
204
207
  recordingOffsetMs: () => recordingOffsetMs,
208
+ rememberedMeetingPass: () => rememberedMeetingPass,
205
209
  roleOf: () => roleOf,
206
210
  roomTopic: () => roomTopic,
207
211
  rootAttributes: () => rootAttributes,
@@ -236,11 +240,18 @@ var MeetError = class extends Error {
236
240
  /** What to DO. Rendered to the user for the device codes. */
237
241
  remedy;
238
242
  cause;
239
- constructor(code, message, remedy, cause) {
243
+ /**
244
+ * The join gate's reason code (CORE-DESIGN §2.5) when the server named one: `not_found`,
245
+ * `cancelled`, `ended`, `locked`, `removed`, `lobby_rejects`, `not_invited`… The phase
246
+ * renders `refused:<reason>` (or `ended`) from it — never from the HTTP status.
247
+ */
248
+ reason;
249
+ constructor(code, message, remedy, cause, reason) {
240
250
  super(message);
241
251
  this.name = "MeetError";
242
252
  this.code = code;
243
253
  this.remedy = remedy;
254
+ this.reason = reason ?? null;
244
255
  if (cause !== void 0) this.cause = cause;
245
256
  }
246
257
  /** True when retrying after the host re-establishes a session is the fix. */
@@ -458,6 +469,17 @@ function createWebRoomTokenStorage(onDiagnostic) {
458
469
  }
459
470
  };
460
471
  }
472
+ function rememberedMeetingPass(roomName, storage = createWebRoomTokenStorage()) {
473
+ const raw = storage.read(roomName);
474
+ if (raw === null) return null;
475
+ try {
476
+ const parsed = JSON.parse(raw);
477
+ if (parsed.standing !== "guest") return null;
478
+ return typeof parsed.meetingPass === "string" && parsed.meetingPass.length > 0 ? parsed.meetingPass : null;
479
+ } catch {
480
+ return null;
481
+ }
482
+ }
461
483
  var MEET_ROUTES = {
462
484
  token: "/api/v1/meet/token",
463
485
  recordingStart: "/api/v1/meet/recording/start",
@@ -471,7 +493,7 @@ var MEET_ROUTES = {
471
493
  /**
472
494
  * THE POST-MEETING RECORD FOR A READER WITH NO DATABASE (MRF-1). A `GET`,
473
495
  * authorized by the room token this device already holds, carried in the
474
- * `x-meet-room-token` header — never a query parameter, because a bearer
496
+ * `x-meet-pass` header — never a query parameter, because a bearer
475
497
  * credential in a URL lands in access logs, referrers and history.
476
498
  */
477
499
  record: "/api/v1/meet/record",
@@ -479,7 +501,7 @@ var MEET_ROUTES = {
479
501
  recordAccess: "/api/v1/meet/record/access",
480
502
  /**
481
503
  * KEEP YOUR NOTES (2026-10-01): a guest who just created an account presents
482
- * their kept pass in `x-meet-room-token` with their new session, and the
504
+ * their kept pass in `x-meet-pass` with their new session, and the
483
505
  * meeting they attended becomes theirs to read. Idempotent; never a grant.
484
506
  */
485
507
  claim: "/api/v1/meet/claim",
@@ -501,7 +523,7 @@ var MEET_ROUTES = {
501
523
  * Meet wave 2: EVERY in-meeting host power (mute, stop video, mute all, ask to
502
524
  * unmute, spotlight, lower hands, co-host, hand over, rename, remove, report).
503
525
  * Decided by the server from the meeting itself; a guest's pass rides
504
- * `x-meet-room-token`.
526
+ * `x-meet-pass`.
505
527
  */
506
528
  moderate: "/api/v1/meet/moderate",
507
529
  /** Meet wave 2: the SAVED chat — `POST` sends to everyone, `GET ?meeting_id=` reads history. */
@@ -512,12 +534,26 @@ var MEET_ROUTES = {
512
534
  */
513
535
  collab: "/api/v1/meet/collab",
514
536
  /** Meet wave 5: translate finished caption lines into this viewer's language. */
515
- captionsTranslate: "/api/v1/meet/captions/translate"
537
+ captionsTranslate: "/api/v1/meet/captions/translate",
538
+ /**
539
+ * S4 — THE STATE READ (CORE-DESIGN §2.6): `GET ?meeting_id=` with `x-meet-pass`. This
540
+ * person's admission (lazy expiry applied), whether anyone who can admit is here, the run's
541
+ * phase. Read every `waiting_poll_seconds` while waiting and on every `mx.state.changed`.
542
+ */
543
+ state: "/api/v1/meet/state",
544
+ /** S4: `withdraw` (stop waiting) or `leave` (close this tab); the pass may ride in the body. */
545
+ leave: "/api/v1/meet/leave",
546
+ /** S4: a pass holder (guests included) saw the recording notice, for this run. */
547
+ consent: "/api/v1/meet/consent"
516
548
  };
517
549
  function meetingPath(template, meetingId) {
518
550
  return template.replace("{meeting_id}", encodeURIComponent(meetingId));
519
551
  }
520
- var ROOM_TOKEN_HEADER = "x-meet-room-token";
552
+ var MEETING_PASS_HEADER = "x-meet-pass";
553
+ function meetingPassHeaders(token) {
554
+ const pass = token?.meetingPass ?? null;
555
+ return pass !== null && pass.length > 0 ? { [MEETING_PASS_HEADER]: pass } : {};
556
+ }
521
557
  var REFRESH_RATIO = 0.8;
522
558
  function readString(source, key) {
523
559
  const value = source[key];
@@ -527,7 +563,12 @@ function readBoolean(source, key, fallback) {
527
563
  const value = source[key];
528
564
  return typeof value === "boolean" ? value : fallback;
529
565
  }
530
- var TOKEN_ROLES = ["host", "cohost", "participant", "guest"];
566
+ var TOKEN_STANDINGS = ["member", "invited", "outsider", "guest"];
567
+ function readStanding(source, key) {
568
+ const value = source[key];
569
+ return TOKEN_STANDINGS.includes(value) ? value : null;
570
+ }
571
+ var TOKEN_ROLES = ["host", "cohost", "presenter", "participant", "viewer"];
531
572
  function readRole(source, key) {
532
573
  const value = source[key];
533
574
  return TOKEN_ROLES.includes(value) ? value : null;
@@ -570,8 +611,15 @@ function parseRoomToken(body, roomName) {
570
611
  // Deriving the role from what DID arrive keeps a host a host against a
571
612
  // server that has not landed MRI-A11 — and never invents a host from
572
613
  // silence, because both fallbacks below grant nothing.
573
- role: readRole(payload, "role") ?? (isHost ? "host" : identity.startsWith("guest:") ? "guest" : "participant"),
614
+ // A word this package does not know grants nothing: `participant`, never a host.
615
+ role: readRole(payload, "role") ?? (isHost ? "host" : "participant"),
616
+ standing: readStanding(payload, "standing"),
617
+ meetingPass: readString(payload, "meeting_pass"),
618
+ meetingPassExpiresAt: readString(payload, "meeting_pass_expires_at"),
619
+ sessionId: readString(payload, "session_id"),
574
620
  lobby: readBoolean(payload, "lobby", false),
621
+ admissionState: readString(payload, "admission_state") ?? void 0,
622
+ pollSeconds: typeof payload["poll_seconds"] === "number" ? payload["poll_seconds"] : void 0,
575
623
  breakoutRoomId: readString(payload, "breakout_room_id") ?? null
576
624
  };
577
625
  }
@@ -581,7 +629,7 @@ function createTokenClient(options) {
581
629
  const storage = options.storage;
582
630
  function remember(token) {
583
631
  if (storage === void 0) return;
584
- if (!token.identity.startsWith("guest:")) return;
632
+ if (token.standing !== "guest") return;
585
633
  storage.write(token.roomName, JSON.stringify(token));
586
634
  }
587
635
  function recall(roomName) {
@@ -639,22 +687,28 @@ function createTokenClient(options) {
639
687
  throw new MeetError(
640
688
  "forbidden",
641
689
  said?.message ?? "You can't join this meeting.",
642
- said?.remedy ?? "Ask the person who shared the link to let you in, then open the link again."
690
+ said?.remedy ?? "Ask the person who shared the link to let you in, then open the link again.",
691
+ void 0,
692
+ said?.reason ?? null
643
693
  );
644
694
  }
645
695
  if (response.status === 404) {
646
696
  throw new MeetError(
647
697
  "not-found",
648
698
  "meet.token: no such meeting",
649
- "The link is wrong or the meeting was deleted. Show the 'meeting not found' screen; a durable link that 404s is never a retry."
699
+ "The link is wrong or the meeting was deleted. Show the 'meeting not found' screen; a durable link that 404s is never a retry.",
700
+ void 0,
701
+ "not_found"
650
702
  );
651
703
  }
652
704
  if (response.status === 410) {
653
- const reason = (await serverSentence(response))?.message ?? "This meeting was cancelled.";
705
+ const said = await serverSentence(response);
654
706
  throw new MeetError(
655
707
  "cancelled",
656
- reason,
657
- "Nobody can join a cancelled meeting. Show the cancelled screen with this sentence; ask the host to schedule a new one."
708
+ said?.message ?? "This meeting was cancelled.",
709
+ "Nobody can join a cancelled meeting. Show the cancelled screen with this sentence; ask the host to schedule a new one.",
710
+ void 0,
711
+ said?.reason ?? "cancelled"
658
712
  );
659
713
  }
660
714
  if (response.status === 423) {
@@ -662,7 +716,9 @@ function createTokenClient(options) {
662
716
  throw new MeetError(
663
717
  "room-locked",
664
718
  said?.message ?? "The meeting is locked.",
665
- said?.remedy ?? "The host locked the room. Ask them to unlock it, then join again."
719
+ said?.remedy ?? "The host locked the room. Ask them to unlock it, then join again.",
720
+ void 0,
721
+ said?.reason ?? "locked"
666
722
  );
667
723
  }
668
724
  if (response.status === 503) {
@@ -731,6 +787,10 @@ function placeholder(roomName) {
731
787
  canRecord: false,
732
788
  isHost: false,
733
789
  role: "participant",
790
+ standing: null,
791
+ meetingPass: null,
792
+ meetingPassExpiresAt: null,
793
+ sessionId: null,
734
794
  lobby: false,
735
795
  breakoutRoomId: null
736
796
  };
@@ -743,7 +803,8 @@ async function serverSentence(response) {
743
803
  const message = typeof source.user_message === "string" && source.user_message.trim() !== "" ? source.user_message : typeof source.message === "string" && source.message.trim() !== "" ? source.message : null;
744
804
  if (message === null) return null;
745
805
  const remedy = typeof source.remedy === "string" && source.remedy.trim() !== "" ? source.remedy : void 0;
746
- return { message, remedy };
806
+ const reason = typeof source.reason === "string" && source.reason !== "" ? source.reason : null;
807
+ return { message, remedy, reason };
747
808
  } catch {
748
809
  return null;
749
810
  }
@@ -932,7 +993,7 @@ function createMeetAi(options) {
932
993
  operation: "meet.ai.ask",
933
994
  // A GUEST MAY ASK. The server decides whether it can answer.
934
995
  requireAuth: false,
935
- ...roomToken !== null ? { headers: { [ROOM_TOKEN_HEADER]: roomToken } } : {},
996
+ ...roomToken !== null ? { headers: { [MEETING_PASS_HEADER]: roomToken } } : {},
936
997
  ...signal !== void 0 ? { signal } : {}
937
998
  });
938
999
  if (!response.ok) {
@@ -1171,14 +1232,15 @@ var MEET_EVENTS = {
1171
1232
  cancel: "mx.call.cancel",
1172
1233
  /** You answered (or declined) on another device. Stop ringing here. */
1173
1234
  settled: "mx.call.settled",
1174
- /** Someone is in the lobby. Hosts render the knock. */
1175
- knock: "mx.lobby.knock",
1176
- /** A host decided. The knocker learns it here — they have no DB read. */
1177
- admission: "mx.lobby.admission",
1178
- /** The room was locked or unlocked. */
1179
- lock: "mx.room.lock",
1180
- /** Recording started or stopped. EVERY participant renders the indicator. */
1181
- recording: "mx.room.recording"
1235
+ /**
1236
+ * 🚨 S4 (CORE-DESIGN §2.6): the ONE room-channel event — payload-free "something changed,
1237
+ * read again". Nothing a person must see rides it: a waiting person learns admitted /
1238
+ * denied / expired / ended by READING their row (`GET /v1/meet/state`) on a timer and on
1239
+ * every hint. The four payload broadcasts (`mx.lobby.knock`, `mx.lobby.admission`,
1240
+ * `mx.room.lock`, `mx.room.recording`) are gone with their senders and receivers. The
1241
+ * server publishes the same event on the same topic (aidream `admission.room_topic`).
1242
+ */
1243
+ stateChanged: "mx.state.changed"
1182
1244
  };
1183
1245
 
1184
1246
  // src/core/types.ts
@@ -1192,14 +1254,19 @@ function isGuest(identity) {
1192
1254
  return identity.userId === null;
1193
1255
  }
1194
1256
  var ROLE_RANK = {
1195
- host: 3,
1196
- cohost: 2,
1257
+ host: 4,
1258
+ cohost: 3,
1259
+ presenter: 2,
1197
1260
  participant: 1,
1198
- guest: 0
1261
+ viewer: 0
1199
1262
  };
1263
+ function isLinkFollower(standing) {
1264
+ return standing === "guest" || standing === "outsider";
1265
+ }
1200
1266
  function isHostRole(role) {
1201
1267
  return role === "host" || role === "cohost";
1202
1268
  }
1269
+ var WAITING_ADMISSION_STATES = ["waiting_host", "knocking"];
1203
1270
 
1204
1271
  // src/core/calls.ts
1205
1272
  var TRANSITIONS = {
@@ -1749,15 +1816,15 @@ var CAPTION_LANGUAGES = [
1749
1816
  { tag: "id", name: "Indonesian" },
1750
1817
  { tag: "sv", name: "Swedish" }
1751
1818
  ];
1752
- async function loadMeetingActivities(api, meetingId, roomToken = null) {
1819
+ async function loadMeetingActivities(api, meetingId, meetingPass = null) {
1753
1820
  let response;
1754
1821
  try {
1755
1822
  response = await api.request({
1756
1823
  path: `${MEET_ROUTES.collab}?meeting_id=${encodeURIComponent(meetingId)}`,
1757
1824
  method: "GET",
1758
1825
  operation: "meet.loadMeetingActivities",
1759
- requireAuth: roomToken === null,
1760
- ...roomToken !== null ? { headers: { [ROOM_TOKEN_HEADER]: roomToken } } : {}
1826
+ requireAuth: meetingPass === null,
1827
+ ...meetingPass !== null ? { headers: { [MEETING_PASS_HEADER]: meetingPass } } : {}
1761
1828
  });
1762
1829
  } catch (error) {
1763
1830
  return {
@@ -1812,9 +1879,15 @@ var RPCS = {
1812
1879
  getOrCreateMeeting: "meet_get_or_create_meeting",
1813
1880
  /** Resolve a durable public link, before/during/after its scheduled time. */
1814
1881
  meetingBySlug: "meet_meeting_by_slug",
1815
- admit: "meet_admit_participant",
1882
+ /*
1883
+ * ── S4 ADMISSION (CORE-DESIGN §2.4). Each returns the decided row; the caller then
1884
+ * sends the payload-free `mx.state.changed` hint. Authorized by `who_may_admit`.
1885
+ */
1886
+ admit: "meet_admit",
1887
+ deny: "meet_deny",
1888
+ undeny: "meet_undeny",
1889
+ admitAll: "meet_admit_all",
1816
1890
  setLock: "meet_set_lock",
1817
- endMeeting: "meet_end_meeting",
1818
1891
  recordConsent: "meet_record_consent",
1819
1892
  /*
1820
1893
  * ── MEET WAVE 1 — management (0.8). Every write is a SECURITY DEFINER door
@@ -1856,13 +1929,13 @@ var RPCS = {
1856
1929
  /** Titles, agendas, current notes and final transcript lines; snippets mark hits with ⟦ ⟧. */
1857
1930
  search: "meet_search"
1858
1931
  };
1859
- var VOID_RPCS = [RPCS.admit, RPCS.recordConsent];
1932
+ var VOID_RPCS = [RPCS.recordConsent];
1860
1933
  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";
1861
1934
  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";
1862
1935
  var RECORDING_COLUMNS = "id,meeting_id,state,started_at,ended_at,started_by,file_id,failure_reason";
1863
1936
  var SEGMENT_COLUMNS = "id,meeting_id,identity,speaker,text,started_at,is_final,metadata";
1864
1937
  var NOTE_COLUMNS = "id,meeting_id,text,kind,assignee_user_id,created_at,metadata";
1865
- 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";
1938
+ 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";
1866
1939
  function projectStays(value) {
1867
1940
  if (!Array.isArray(value)) return [];
1868
1941
  const stays = [];
@@ -2228,8 +2301,17 @@ function createMeetRepository(options) {
2228
2301
  const operation = "meet.projectAttendee";
2229
2302
  const role = text(row, "role");
2230
2303
  const admission = text(row, "admission_state");
2231
- const known = ["host", "cohost", "participant", "guest"];
2232
- const states = ["knocking", "admitted", "denied", "removed"];
2304
+ const known = ["host", "cohost", "presenter", "participant", "viewer"];
2305
+ const states = [
2306
+ "waiting_host",
2307
+ "knocking",
2308
+ "admitted",
2309
+ "denied",
2310
+ "expired",
2311
+ "withdrawn",
2312
+ "removed",
2313
+ "ended"
2314
+ ];
2233
2315
  const userId = text(row, "participant_user_id");
2234
2316
  const identity = requireText(row, "identity", operation);
2235
2317
  return {
@@ -2243,6 +2325,8 @@ function createMeetRepository(options) {
2243
2325
  userId: userId === null ? null : asUserId(userId),
2244
2326
  role: known.includes(role) ? role : "participant",
2245
2327
  admissionState: states.includes(admission) ? admission : "admitted",
2328
+ knockedAt: text(row, "knocked_at"),
2329
+ knockExpiresAt: text(row, "knock_expires_at"),
2246
2330
  isAgent: bool(row, "is_agent", false),
2247
2331
  joinedAt: text(row, "joined_at"),
2248
2332
  leftAt: text(row, "left_at"),
@@ -2521,16 +2605,26 @@ function createMeetRepository(options) {
2521
2605
  hasMore: rows.length > limit
2522
2606
  };
2523
2607
  },
2524
- async admit({ meetingId, identity, admitted, byUserId }) {
2525
- await guardVoid(
2526
- "meet.admit",
2527
- () => db.rpc(RPCS.admit, {
2608
+ async decideAdmission({ meetingId, identity, decision, byUserId }) {
2609
+ const fn = decision === "admit" ? RPCS.admit : decision === "deny" ? RPCS.deny : RPCS.undeny;
2610
+ const row = await guard(
2611
+ `meet.${decision}`,
2612
+ () => db.rpc(fn, {
2528
2613
  p_meeting_id: meetingId,
2529
2614
  p_identity: identity,
2530
- p_admitted: admitted,
2531
2615
  p_by_user_id: byUserId
2532
2616
  })
2533
2617
  );
2618
+ return projectAttendee(row);
2619
+ },
2620
+ async admitAll(meetingId, byUserId) {
2621
+ const response = await db.rpc(RPCS.admitAll, {
2622
+ p_meeting_id: meetingId,
2623
+ p_by_user_id: byUserId
2624
+ });
2625
+ if (response.error !== null) throw normalizeMeetError(response.error, "meet.admitAll");
2626
+ const rows = Array.isArray(response.data) ? response.data : [];
2627
+ return rows.map(projectAttendee);
2534
2628
  },
2535
2629
  async setLock(meetingId, locked, byUserId) {
2536
2630
  const row = await guard(
@@ -2543,16 +2637,6 @@ function createMeetRepository(options) {
2543
2637
  );
2544
2638
  return projectMeeting(row);
2545
2639
  },
2546
- async endMeeting(meetingId, byUserId) {
2547
- const row = await guard(
2548
- "meet.endMeeting",
2549
- () => db.rpc(RPCS.endMeeting, {
2550
- p_meeting_id: meetingId,
2551
- p_by_user_id: byUserId
2552
- })
2553
- );
2554
- return projectMeeting(row);
2555
- },
2556
2640
  async transcript(meetingId, limit = 1e3) {
2557
2641
  const response = await db.from(TABLES.transcripts).select(SEGMENT_COLUMNS).eq("meeting_id", meetingId).order("started_at", { ascending: true }).order("id", { ascending: true }).limit(limit);
2558
2642
  if (response.error !== null) throw normalizeMeetError(response.error, "meet.transcript");
@@ -2881,12 +2965,14 @@ var DEFAULT_EFFECTS = {
2881
2965
  };
2882
2966
  var NO_GRANTS = {
2883
2967
  role: "participant",
2968
+ standing: null,
2884
2969
  canRecord: false,
2885
2970
  canPublish: false,
2886
2971
  canSubscribe: false
2887
2972
  };
2888
2973
  var EMPTY = {
2889
2974
  phase: "idle",
2975
+ waiting: null,
2890
2976
  meeting: null,
2891
2977
  localIdentity: null,
2892
2978
  grants: NO_GRANTS,
@@ -2927,6 +3013,11 @@ function effectiveRole(snapshot) {
2927
3013
  const fromToken = snapshot.grants.role;
2928
3014
  return ROLE_RANK[fromRoster] >= ROLE_RANK[fromToken] ? fromRoster : fromToken;
2929
3015
  }
3016
+ function effectiveStanding(snapshot) {
3017
+ if (snapshot.grants.standing != null) return snapshot.grants.standing;
3018
+ const me = snapshot.participants.find((p) => p.identity === snapshot.localIdentity);
3019
+ return me?.standing ?? null;
3020
+ }
2930
3021
  var REACTION_TTL_MS = 5e3;
2931
3022
  var CHAT_LIMIT = 200;
2932
3023
  function createMeetStore(initial = {}) {
@@ -3419,7 +3510,7 @@ function applyModeration(store, payload, now) {
3419
3510
  return ignored(`a moderation kind this version does not know: ${String(kind)}`);
3420
3511
  }
3421
3512
  }
3422
- var ROLES = ["host", "cohost", "participant", "guest"];
3513
+ var ROLES = ["host", "cohost", "presenter", "participant", "viewer"];
3423
3514
 
3424
3515
  // src/core/devices.ts
3425
3516
  var KINDS = ["audioinput", "videoinput", "audiooutput"];
@@ -4159,19 +4250,25 @@ function roleOf(participants, identity) {
4159
4250
  return participants.find((p) => p.identity === identity)?.role ?? "participant";
4160
4251
  }
4161
4252
  var END_MEETING_NOT_SERVED = "Ending a meeting is not available on this server yet";
4162
- function lobbyEntryFrom(attendee) {
4163
- if (attendee.admissionState !== "knocking") return null;
4253
+ function lobbyEntryFrom(attendee, now = Date.now()) {
4254
+ if (!WAITING_ADMISSION_STATES.includes(attendee.admissionState)) return null;
4255
+ const expires = attendee.knockExpiresAt ? Date.parse(attendee.knockExpiresAt) : Number.NaN;
4256
+ if (!Number.isNaN(expires) && expires <= now) return null;
4164
4257
  return {
4165
4258
  identity: attendee.identity,
4166
4259
  displayName: attendee.displayName,
4167
4260
  avatarUrl: attendee.avatarUrl,
4168
4261
  userId: attendee.userId,
4169
4262
  state: "knocking",
4170
- knockedAt: attendee.joinedAt ?? (/* @__PURE__ */ new Date()).toISOString(),
4263
+ knockedAt: attendee.knockedAt ?? attendee.joinedAt ?? (/* @__PURE__ */ new Date()).toISOString(),
4264
+ knockExpiresAt: attendee.knockExpiresAt ?? null,
4171
4265
  decidedAt: null,
4172
4266
  decidedBy: null
4173
4267
  };
4174
4268
  }
4269
+ function decidedState(state) {
4270
+ return WAITING_ADMISSION_STATES.includes(state) ? "expired" : state;
4271
+ }
4175
4272
  function createMeetingSession(options) {
4176
4273
  const { manager, repository, store, identity } = options;
4177
4274
  let channel = null;
@@ -4195,11 +4292,6 @@ function createMeetingSession(options) {
4195
4292
  }
4196
4293
  return identity.userId;
4197
4294
  }
4198
- function knockIdentity() {
4199
- return asParticipantIdentity(
4200
- identity.userId !== null ? `user:${identity.userId}` : `guest:${identity.deviceId}`
4201
- );
4202
- }
4203
4295
  function requireMeeting(operation) {
4204
4296
  const meeting = store.snapshot().meeting;
4205
4297
  if (meeting === null) {
@@ -4211,6 +4303,191 @@ function createMeetingSession(options) {
4211
4303
  }
4212
4304
  return meeting;
4213
4305
  }
4306
+ let polling = false;
4307
+ let pollTimer = null;
4308
+ let reading = null;
4309
+ let admittedOnce = false;
4310
+ let unlisten = null;
4311
+ function report(level, message, remedy) {
4312
+ options.onDiagnostic?.(remedy === void 0 ? { level, message } : { level, message, remedy });
4313
+ }
4314
+ function pollMs() {
4315
+ const seconds = store.snapshot().waiting?.pollSeconds ?? 3;
4316
+ return Math.max(1, Math.min(60, seconds)) * 1e3;
4317
+ }
4318
+ function schedule(ms2) {
4319
+ if (pollTimer !== null) clearTimeout(pollTimer);
4320
+ if (!polling) return;
4321
+ pollTimer = setTimeout(() => {
4322
+ pollTimer = null;
4323
+ void readState2().finally(() => schedule(pollMs()));
4324
+ }, ms2);
4325
+ }
4326
+ function listen() {
4327
+ if (unlisten !== null || typeof window === "undefined") return;
4328
+ const nudge = () => {
4329
+ if (typeof document !== "undefined" && document.visibilityState === "hidden") return;
4330
+ void readState2();
4331
+ };
4332
+ const goodbye = () => {
4333
+ const meeting = store.snapshot().meeting;
4334
+ const pass = options.resolvePass?.() ?? null;
4335
+ const waiting = store.snapshot().waiting;
4336
+ if (meeting === null || pass === null || waiting === null) return;
4337
+ if (!WAITING_ADMISSION_STATES.includes(waiting.admission)) return;
4338
+ try {
4339
+ navigator.sendBeacon?.(
4340
+ options.api.url(MEET_ROUTES.leave),
4341
+ new Blob([JSON.stringify({ meeting_id: meeting.id, intent: "withdraw", meeting_pass: pass })], {
4342
+ type: "application/json"
4343
+ })
4344
+ );
4345
+ } catch {
4346
+ }
4347
+ };
4348
+ window.addEventListener("online", nudge);
4349
+ window.addEventListener("pagehide", goodbye);
4350
+ document.addEventListener("visibilitychange", nudge);
4351
+ unlisten = () => {
4352
+ window.removeEventListener("online", nudge);
4353
+ window.removeEventListener("pagehide", goodbye);
4354
+ document.removeEventListener("visibilitychange", nudge);
4355
+ };
4356
+ }
4357
+ function stopWaiting() {
4358
+ polling = false;
4359
+ if (pollTimer !== null) clearTimeout(pollTimer);
4360
+ pollTimer = null;
4361
+ unlisten?.();
4362
+ unlisten = null;
4363
+ }
4364
+ function patchWaiting(next) {
4365
+ const current = store.snapshot().waiting;
4366
+ const base = current ?? {
4367
+ admission: "knocking",
4368
+ admittersPresent: null,
4369
+ reknockAllowed: true,
4370
+ knockExpiresAt: null,
4371
+ pollSeconds: 3,
4372
+ liveVersion: 0
4373
+ };
4374
+ store.patch({ waiting: { ...base, ...next } });
4375
+ }
4376
+ function apply(read) {
4377
+ const snapshot = store.snapshot();
4378
+ if (snapshot.phase !== "lobby") return;
4379
+ const admission = read.admission_state ?? null;
4380
+ const ended = admission === "ended" || read.session_phase === "ended" || read.meeting_phase === "ended";
4381
+ if (admission === "admitted" && !ended) {
4382
+ if (admittedOnce) return;
4383
+ admittedOnce = true;
4384
+ stopWaiting();
4385
+ options.onAdmitted?.();
4386
+ return;
4387
+ }
4388
+ if (ended) {
4389
+ stopWaiting();
4390
+ patchWaiting({ admission: "ended" });
4391
+ const meeting = snapshot.meeting;
4392
+ if (read.meeting_phase === "ended" && meeting !== null && meeting.endedAt === null) {
4393
+ store.patch({ meeting: { ...meeting, endedAt: (/* @__PURE__ */ new Date()).toISOString() } });
4394
+ }
4395
+ return;
4396
+ }
4397
+ if (admission === null) return;
4398
+ patchWaiting({
4399
+ admission,
4400
+ admittersPresent: read.admitters_present ?? null,
4401
+ reknockAllowed: read.reknock_allowed ?? true,
4402
+ knockExpiresAt: read.knock_expires_at ?? null,
4403
+ pollSeconds: read.poll_seconds ?? snapshot.waiting?.pollSeconds ?? 3,
4404
+ liveVersion: read.live_version ?? 0
4405
+ });
4406
+ if (admission === "denied" || admission === "expired" || admission === "withdrawn" || admission === "removed") {
4407
+ stopWaiting();
4408
+ }
4409
+ }
4410
+ async function readState2() {
4411
+ if (reading !== null) return reading;
4412
+ const meeting = store.snapshot().meeting;
4413
+ const pass = options.resolvePass?.() ?? null;
4414
+ if (meeting === null || pass === null) return;
4415
+ reading = (async () => {
4416
+ try {
4417
+ const response = await options.api.request({
4418
+ path: `${MEET_ROUTES.state}?meeting_id=${encodeURIComponent(meeting.id)}`,
4419
+ method: "GET",
4420
+ requireAuth: false,
4421
+ operation: "meet.state",
4422
+ headers: { [MEETING_PASS_HEADER]: pass }
4423
+ });
4424
+ if (!response.ok) {
4425
+ let body = null;
4426
+ try {
4427
+ body = await response.json();
4428
+ } catch {
4429
+ body = null;
4430
+ }
4431
+ const refusal2 = readServerRefusal(body);
4432
+ report(
4433
+ "warn",
4434
+ refusal2?.reason ?? `meet.state: the server answered ${response.status}`,
4435
+ refusal2?.remedy ?? "The waiting room keeps asking; nothing on this screen changes until the server answers."
4436
+ );
4437
+ return;
4438
+ }
4439
+ apply(await response.json());
4440
+ } catch (error) {
4441
+ report("info", `meet.state: ${String(error)}`, "Waiting continues; the state is read again shortly.");
4442
+ } finally {
4443
+ reading = null;
4444
+ }
4445
+ })();
4446
+ return reading;
4447
+ }
4448
+ async function postLeave(intent) {
4449
+ const meeting = requireMeeting(`meet.${intent}`);
4450
+ const pass = options.resolvePass?.() ?? null;
4451
+ const response = await options.api.request({
4452
+ path: MEET_ROUTES.leave,
4453
+ method: "POST",
4454
+ requireAuth: false,
4455
+ operation: `meet.${intent}`,
4456
+ body: { meeting_id: meeting.id, intent },
4457
+ ...pass !== null ? { headers: { [MEETING_PASS_HEADER]: pass } } : {}
4458
+ });
4459
+ if (!response.ok) {
4460
+ let body = null;
4461
+ try {
4462
+ body = await response.json();
4463
+ } catch {
4464
+ body = null;
4465
+ }
4466
+ const refusal2 = readServerRefusal(body);
4467
+ throw new MeetError(
4468
+ "transport",
4469
+ refusal2?.reason ?? `meet.${intent}: the server answered ${response.status}`,
4470
+ refusal2?.remedy ?? "You are still in the waiting room. Try again."
4471
+ );
4472
+ }
4473
+ }
4474
+ function hint() {
4475
+ channel?.send(MEET_EVENTS.stateChanged, { v: null });
4476
+ }
4477
+ async function refreshQueue() {
4478
+ const meeting = store.snapshot().meeting;
4479
+ if (meeting === null || isGuest(identity) || !isHostRole(effectiveRole(store.snapshot()))) return;
4480
+ try {
4481
+ const attendees = await repository.attendees(meeting.id);
4482
+ store.learn(attendees);
4483
+ for (const attendee of attendees) {
4484
+ const entry = lobbyEntryFrom(attendee);
4485
+ if (entry !== null) store.upsertLobby(entry);
4486
+ else store.setAdmission(attendee.identity, decidedState(attendee.admissionState), null);
4487
+ }
4488
+ } catch {
4489
+ }
4490
+ }
4214
4491
  return {
4215
4492
  watch(roomName) {
4216
4493
  channel?.close();
@@ -4218,58 +4495,22 @@ function createMeetingSession(options) {
4218
4495
  topic: roomTopic(roomName),
4219
4496
  broadcast: [
4220
4497
  {
4221
- event: MEET_EVENTS.knock,
4222
- onMessage: (message) => {
4223
- store.upsertLobby(message.data);
4224
- }
4225
- },
4226
- {
4227
- event: MEET_EVENTS.admission,
4228
- onMessage: (message) => {
4229
- const verdict = message.data;
4230
- const decided = asParticipantIdentity(verdict.identity);
4231
- store.setAdmission(
4232
- decided,
4233
- verdict.admitted ? "admitted" : "denied",
4234
- verdict.by === null ? null : verdict.by
4235
- );
4236
- if (decided === knockIdentity()) {
4237
- if (verdict.admitted) options.onAdmitted?.();
4238
- else options.onDenied?.();
4239
- }
4240
- }
4241
- },
4242
- {
4243
- event: MEET_EVENTS.lock,
4244
- onMessage: (message) => {
4245
- const { locked } = message.data;
4246
- const meeting = store.snapshot().meeting;
4247
- store.patch({
4248
- locked,
4249
- ...meeting !== null ? { meeting: { ...meeting, locked } } : {}
4250
- });
4251
- }
4252
- },
4253
- {
4254
- event: MEET_EVENTS.recording,
4255
- onMessage: (message) => {
4256
- const payload = message.data;
4257
- store.setRecording(
4258
- payload.state,
4259
- payload.by === null ? null : payload.by,
4260
- "room"
4261
- );
4498
+ // 🚨 PAYLOAD-FREE. Every receiver reads again; a spoofed hint only causes a read.
4499
+ event: MEET_EVENTS.stateChanged,
4500
+ onMessage: () => {
4501
+ if (store.snapshot().phase === "lobby") void readState2();
4502
+ void refreshQueue();
4262
4503
  }
4263
4504
  }
4264
4505
  ],
4265
- // THE BACKFILL DOOR. A host whose socket blipped must not miss a knock:
4266
- // re-reading the meeting restores lock/recording truth, and knockers
4267
- // re-announce on their own timer.
4506
+ // THE BACKFILL DOOR. A socket blip must not lose a decision: re-read the meeting
4507
+ // row, and a waiting tab re-reads its own state.
4268
4508
  onBackfill: async () => {
4269
4509
  const meeting = store.snapshot().meeting;
4270
4510
  if (meeting === null) return;
4271
4511
  const fresh = isGuest(identity) ? await repository.meetingBySlug(meeting.slug) : await readMeeting(meeting.id);
4272
4512
  store.patch({ meeting: fresh, locked: fresh.locked });
4513
+ if (store.snapshot().phase === "lobby") void readState2();
4273
4514
  }
4274
4515
  });
4275
4516
  },
@@ -4330,7 +4571,7 @@ function createMeetingSession(options) {
4330
4571
  store.upsertLobby(entry);
4331
4572
  return;
4332
4573
  }
4333
- store.setAdmission(attendee.identity, attendee.admissionState, null);
4574
+ store.setAdmission(attendee.identity, decidedState(attendee.admissionState), null);
4334
4575
  }
4335
4576
  },
4336
4577
  {
@@ -4382,62 +4623,75 @@ function createMeetingSession(options) {
4382
4623
  if (entry !== null) {
4383
4624
  store.upsertLobby(entry);
4384
4625
  } else {
4385
- store.setAdmission(attendee.identity, attendee.admissionState, null);
4626
+ store.setAdmission(attendee.identity, decidedState(attendee.admissionState), null);
4386
4627
  }
4387
4628
  }
4388
4629
  }
4389
4630
  });
4390
4631
  meetingChannel.requestBackfill("manual");
4391
4632
  },
4392
- knock(roomName) {
4393
- const handle = channel ?? manager.open({ topic: roomTopic(roomName) });
4394
- channel = handle;
4395
- const entry = {
4396
- identity: knockIdentity(),
4397
- displayName: identity.displayName,
4398
- avatarUrl: identity.avatarUrl,
4399
- userId: identity.userId,
4400
- state: "knocking",
4401
- knockedAt: (/* @__PURE__ */ new Date()).toISOString(),
4402
- decidedAt: null,
4403
- decidedBy: null
4404
- };
4405
- store.upsertLobby(entry);
4406
- handle.send(MEET_EVENTS.knock, entry);
4407
- },
4408
4633
  async admit(entry, admitted) {
4409
4634
  const by = requireHostUser("meet.admit");
4410
4635
  const meeting = requireMeeting("meet.admit");
4411
- await repository.admit({
4636
+ const row = await repository.decideAdmission({
4412
4637
  meetingId: meeting.id,
4413
4638
  identity: entry.identity,
4414
- admitted,
4639
+ decision: admitted ? "admit" : "deny",
4415
4640
  byUserId: by
4416
4641
  });
4417
4642
  store.upsertLobby({
4418
4643
  ...entry,
4419
- state: admitted ? "admitted" : "denied",
4644
+ state: decidedState(row.admissionState),
4420
4645
  decidedAt: (/* @__PURE__ */ new Date()).toISOString(),
4421
4646
  decidedBy: by
4422
4647
  });
4423
- channel?.send(MEET_EVENTS.admission, {
4648
+ hint();
4649
+ },
4650
+ async undeny(entry) {
4651
+ const by = requireHostUser("meet.undeny");
4652
+ const meeting = requireMeeting("meet.undeny");
4653
+ const row = await repository.decideAdmission({
4654
+ meetingId: meeting.id,
4424
4655
  identity: entry.identity,
4425
- admitted,
4426
- by
4656
+ decision: "undeny",
4657
+ byUserId: by
4427
4658
  });
4659
+ store.upsertLobby({ ...entry, state: decidedState(row.admissionState), decidedAt: (/* @__PURE__ */ new Date()).toISOString(), decidedBy: by });
4660
+ hint();
4428
4661
  },
4429
4662
  async admitAll() {
4430
- const knocking = store.snapshot().lobby.filter((entry) => entry.state === "knocking");
4431
- for (const entry of knocking) {
4432
- await this.admit(entry, true);
4663
+ const by = requireHostUser("meet.admitAll");
4664
+ const meeting = requireMeeting("meet.admitAll");
4665
+ const rows = await repository.admitAll(meeting.id, by);
4666
+ for (const row of rows) {
4667
+ store.setAdmission(row.identity, row.admissionState, by);
4433
4668
  }
4669
+ hint();
4670
+ },
4671
+ startWaiting() {
4672
+ if (polling) return;
4673
+ polling = true;
4674
+ admittedOnce = false;
4675
+ listen();
4676
+ schedule(0);
4677
+ },
4678
+ stopWaiting,
4679
+ readState: readState2,
4680
+ async withdraw() {
4681
+ stopWaiting();
4682
+ await postLeave("withdraw");
4683
+ patchWaiting({ admission: "withdrawn" });
4684
+ hint();
4685
+ },
4686
+ async askAgain() {
4687
+ await options.rejoin?.();
4434
4688
  },
4435
4689
  async setLock(locked) {
4436
4690
  const by = requireHostUser("meet.setLock");
4437
4691
  const meeting = requireMeeting("meet.setLock");
4438
4692
  const updated = await repository.setLock(meeting.id, locked, by);
4439
4693
  store.patch({ meeting: updated, locked: updated.locked });
4440
- channel?.send(MEET_EVENTS.lock, { locked: updated.locked });
4694
+ hint();
4441
4695
  return updated;
4442
4696
  },
4443
4697
  /**
@@ -4495,6 +4749,7 @@ function createMeetingSession(options) {
4495
4749
  return { endedAt, served: true };
4496
4750
  },
4497
4751
  dispose() {
4752
+ stopWaiting();
4498
4753
  channel?.close();
4499
4754
  channel = null;
4500
4755
  meetingChannel?.close();
@@ -4614,7 +4869,6 @@ function kindOf(person) {
4614
4869
  if (person.isAgent) return "assistant";
4615
4870
  if (person.role === "host") return "host";
4616
4871
  if (person.role === "cohost") return "cohost";
4617
- if (person.role === "guest") return person.userId !== null ? "signed-in guest" : "guest";
4618
4872
  if (person.userId === null) return "guest";
4619
4873
  return "member";
4620
4874
  }
@@ -4899,7 +5153,7 @@ function meetingLength(meeting) {
4899
5153
  if (start === null || end === null || end < start) return null;
4900
5154
  return (0, import_format.formatDurationMs)(end - start, { style: "long" });
4901
5155
  }
4902
- async function loadMeetingChat(api, meetingId, roomToken = null) {
5156
+ async function loadMeetingChat(api, meetingId, meetingPass = null) {
4903
5157
  const operation = "meet.loadMeetingChat";
4904
5158
  let response;
4905
5159
  try {
@@ -4907,8 +5161,8 @@ async function loadMeetingChat(api, meetingId, roomToken = null) {
4907
5161
  path: `${MEET_ROUTES.chat}?meeting_id=${encodeURIComponent(meetingId)}`,
4908
5162
  method: "GET",
4909
5163
  operation,
4910
- requireAuth: roomToken === null,
4911
- ...roomToken !== null ? { headers: { [ROOM_TOKEN_HEADER]: roomToken } } : {}
5164
+ requireAuth: meetingPass === null,
5165
+ ...meetingPass !== null ? { headers: { [MEETING_PASS_HEADER]: meetingPass } } : {}
4912
5166
  });
4913
5167
  } catch (error) {
4914
5168
  return {
@@ -5052,7 +5306,12 @@ async function pushMeetingToGoogleCalendar(api, meetingId, target) {
5052
5306
  // src/core/room.ts
5053
5307
  var PARTICIPANT_ROLE_ATTRIBUTE = "matrx.participant.role";
5054
5308
  var PARTICIPANT_CAN_RECORD_ATTRIBUTE = "matrx.participant.can_record";
5055
- var KNOWN_ROLES = ["host", "cohost", "participant", "guest"];
5309
+ var PARTICIPANT_STANDING_ATTRIBUTE = "matrx.participant.standing";
5310
+ var KNOWN_STANDINGS = ["member", "invited", "outsider", "guest"];
5311
+ function parseStanding(value) {
5312
+ return KNOWN_STANDINGS.includes(value) ? value : null;
5313
+ }
5314
+ var KNOWN_ROLES = ["host", "cohost", "presenter", "participant", "viewer"];
5056
5315
  function parseRole(value) {
5057
5316
  return KNOWN_ROLES.includes(value) ? value : "participant";
5058
5317
  }
@@ -5086,6 +5345,9 @@ function projectParticipant(driver, joinedAt) {
5086
5345
  // ATTRIBUTE FIRST, metadata second — see the constants above. Both are the
5087
5346
  // server's; the attribute is simply the one that always arrives.
5088
5347
  role: parseRole(driver.attributes[PARTICIPANT_ROLE_ATTRIBUTE] ?? metadata.role),
5348
+ standing: parseStanding(
5349
+ driver.attributes[PARTICIPANT_STANDING_ATTRIBUTE] ?? metadata.standing
5350
+ ),
5089
5351
  canRecord: driver.attributes[PARTICIPANT_CAN_RECORD_ATTRIBUTE] === "true" || metadata.can_record === true,
5090
5352
  isLocal: driver.isLocal,
5091
5353
  isAgent: metadata.is_agent === true,
@@ -5158,7 +5420,7 @@ function createRoomEngine(options) {
5158
5420
  const normalized = normalizeMeetError(error, operation);
5159
5421
  store.patch({
5160
5422
  phase: "failed",
5161
- failure: { message: normalized.message, remedy: normalized.remedy }
5423
+ failure: { message: normalized.message, remedy: normalized.remedy, reason: normalized.reason }
5162
5424
  });
5163
5425
  report(
5164
5426
  normalized.code === "session-unavailable" ? "warn" : "error",
@@ -5348,7 +5610,7 @@ function createRoomEngine(options) {
5348
5610
  }
5349
5611
  };
5350
5612
  async function connect(request) {
5351
- store.patch({ phase: "requesting-token", failure: null });
5613
+ store.patch({ phase: "requesting-token", failure: null, waiting: null });
5352
5614
  const token = await tokens.get({
5353
5615
  roomName: request.roomName,
5354
5616
  identity,
@@ -5362,16 +5624,27 @@ function createRoomEngine(options) {
5362
5624
  breakoutRoomId: token.breakoutRoomId ?? null,
5363
5625
  grants: {
5364
5626
  role: token.role,
5627
+ standing: token.standing,
5365
5628
  canRecord: token.canRecord,
5366
5629
  canPublish: token.canPublish,
5367
5630
  canSubscribe: token.canSubscribe
5368
5631
  }
5369
5632
  });
5370
- if (token.lobby) {
5371
- store.patch({ phase: "lobby" });
5633
+ if (token.lobby || !token.canSubscribe) {
5634
+ store.patch({
5635
+ phase: "lobby",
5636
+ waiting: {
5637
+ admission: token.admissionState !== void 0 && token.admissionState !== "admitted" ? token.admissionState : "knocking",
5638
+ admittersPresent: null,
5639
+ reknockAllowed: true,
5640
+ knockExpiresAt: null,
5641
+ pollSeconds: token.pollSeconds ?? 3,
5642
+ liveVersion: 0
5643
+ }
5644
+ });
5372
5645
  return;
5373
5646
  }
5374
- store.patch({ phase: "connecting" });
5647
+ store.patch({ phase: "connecting", waiting: null });
5375
5648
  const created = await options.driverFactory(handlers);
5376
5649
  if (disposed) {
5377
5650
  await created.disconnect();
@@ -5707,7 +5980,7 @@ function createInRoomClient(options) {
5707
5980
  }
5708
5981
  function headers() {
5709
5982
  const token = options.resolveRoomToken();
5710
- return token === null ? void 0 : { [ROOM_TOKEN_HEADER]: token };
5983
+ return token === null ? void 0 : { [MEETING_PASS_HEADER]: token };
5711
5984
  }
5712
5985
  function refusal2(response, operation, remedy) {
5713
5986
  return refusalFromResponse(response, operation, remedy);
@@ -6188,7 +6461,7 @@ function summarizeMeetingRecordRestrictions(restrictions) {
6188
6461
  async function loadGuestMeetingRecord(args) {
6189
6462
  const { api, repository, meetingId, meeting, roomToken } = args;
6190
6463
  const session = args.session === true;
6191
- if (roomToken === null && !session) {
6464
+ if (roomToken?.meetingPass == null && !session) {
6192
6465
  return refusedBundle(
6193
6466
  meeting,
6194
6467
  "This meeting's record is only readable by the people who were in it.",
@@ -6198,12 +6471,12 @@ async function loadGuestMeetingRecord(args) {
6198
6471
  let response;
6199
6472
  try {
6200
6473
  response = await api.request({
6201
- path: `${MEET_ROUTES.record}?meeting_id=${encodeURIComponent(meetingId)}`,
6474
+ path: `${MEET_ROUTES.record}?meeting_id=${encodeURIComponent(meetingId)}` + (args.sessionId ? `&session_id=${encodeURIComponent(args.sessionId)}` : ""),
6202
6475
  method: "GET",
6203
6476
  // A GUEST HAS NO SESSION AND IS STILL A FIRST-CLASS READER (D6).
6204
6477
  requireAuth: session,
6205
6478
  operation: "meet.record",
6206
- headers: roomToken === null ? {} : { [ROOM_TOKEN_HEADER]: roomToken.token }
6479
+ headers: roomToken?.meetingPass != null ? { [MEETING_PASS_HEADER]: roomToken.meetingPass } : {}
6207
6480
  });
6208
6481
  } catch (error) {
6209
6482
  return refusedBundle(
@@ -6591,6 +6864,10 @@ function takeMoveRequest(store) {
6591
6864
  }
6592
6865
 
6593
6866
  // src/core/observation.ts
6867
+ function observedRole(role, standing) {
6868
+ if (role === "host" || role === "cohost") return role;
6869
+ return isLinkFollower(standing) ? "guest" : "participant";
6870
+ }
6594
6871
  function observedDevice(track) {
6595
6872
  if (track.enabled) return "on";
6596
6873
  const refused = track.unavailable;
@@ -6618,13 +6895,15 @@ function observedPhase(input) {
6618
6895
  const raw = snapshot.phase;
6619
6896
  const live = raw === "connected" || raw === "reconnecting";
6620
6897
  if (endedRecord(snapshot, input.meeting ?? null) && !live) return "ended";
6898
+ const reason = raw === "failed" ? snapshot.failure?.reason ?? null : null;
6899
+ if (reason !== null) return reason === "ended" ? "ended" : `refused:${reason}`;
6621
6900
  switch (raw) {
6622
6901
  case "idle":
6623
6902
  case "failed":
6624
6903
  case "left":
6625
6904
  return "prejoin";
6626
6905
  case "lobby":
6627
- return "waiting:knocking";
6906
+ return waitingPhase(snapshot);
6628
6907
  case "requesting-token":
6629
6908
  case "connecting":
6630
6909
  return "joining";
@@ -6633,20 +6912,39 @@ function observedPhase(input) {
6633
6912
  return "in_call";
6634
6913
  }
6635
6914
  }
6915
+ function waitingPhase(snapshot) {
6916
+ switch (snapshot.waiting?.admission) {
6917
+ case "waiting_host":
6918
+ return "waiting:host_not_started";
6919
+ case "denied":
6920
+ return "denied";
6921
+ case "expired":
6922
+ return "knock_expired";
6923
+ case "withdrawn":
6924
+ return "left";
6925
+ case "ended":
6926
+ return "ended";
6927
+ case "removed":
6928
+ return "refused:removed";
6929
+ default:
6930
+ return "waiting:knocking";
6931
+ }
6932
+ }
6636
6933
  var ADMITTER_ROLES = /* @__PURE__ */ new Set(["host", "cohost"]);
6637
6934
  function observe(input) {
6638
6935
  const { snapshot } = input;
6639
6936
  const phase = observedPhase(input);
6640
6937
  const inCall = phase === "in_call";
6641
- const role = inCall && snapshot !== null ? effectiveRole(snapshot) : "none";
6642
- const canAdmit = role !== "none" && ADMITTER_ROLES.has(role);
6938
+ const actual = inCall && snapshot !== null ? effectiveRole(snapshot) : "none";
6939
+ const canAdmit = actual !== "none" && ADMITTER_ROLES.has(actual);
6940
+ const role = actual === "none" || snapshot === null ? "none" : observedRole(actual, effectiveStanding(snapshot));
6643
6941
  const deviceInPlay = phase === "joining" || inCall;
6644
6942
  return {
6645
6943
  phase,
6646
- reason: null,
6944
+ reason: phase.startsWith("refused:") ? phase.slice("refused:".length) : null,
6647
6945
  role,
6648
6946
  connection: snapshot?.phase === "reconnecting" ? "reconnecting" : "stable",
6649
- hostPresent: inCall && snapshot !== null ? snapshot.participants.some((p) => ADMITTER_ROLES.has(p.role)) : null,
6947
+ hostPresent: inCall && snapshot !== null ? snapshot.participants.some((p) => ADMITTER_ROLES.has(p.role)) : snapshot?.phase === "lobby" ? snapshot.waiting?.admittersPresent ?? null : null,
6650
6948
  lobbyCount: canAdmit && snapshot !== null ? snapshot.lobby.filter((entry) => entry.state === "knocking").length : 0,
6651
6949
  recording: snapshot?.recording === "recording" || snapshot?.recording === "stopping",
6652
6950
  audioBlocked: null,
@@ -6709,7 +7007,7 @@ function participantAttributes(p) {
6709
7007
  "data-meet-participant": "",
6710
7008
  "data-meet-participant-identity": p.identity,
6711
7009
  "data-meet-participant-name": p.displayName,
6712
- "data-meet-participant-role": p.role,
7010
+ "data-meet-participant-role": observedRole(p.role, p.standing),
6713
7011
  "data-meet-participant-self": String(p.isLocal)
6714
7012
  };
6715
7013
  const quality = observedQuality(p.quality);