@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/react.cjs CHANGED
@@ -55,6 +55,7 @@ __export(react_exports, {
55
55
  LISTEN_ONLY_NOTICE: () => LISTEN_ONLY_NOTICE,
56
56
  MEETING_INVITE_KIND: () => MEETING_INVITE_KIND,
57
57
  MEETING_INVITE_VERSION: () => MEETING_INVITE_VERSION,
58
+ MEETING_PASS_HEADER: () => MEETING_PASS_HEADER,
58
59
  MEETING_RECORD_GRACE_MS: () => MEETING_RECORD_GRACE_MS,
59
60
  MEET_ANSWER_CONTEXT_EVENT: () => MEET_ANSWER_CONTEXT_EVENT,
60
61
  MEET_ANSWER_EVENTS: () => MEET_ANSWER_EVENTS,
@@ -75,12 +76,12 @@ __export(react_exports, {
75
76
  ORGANIZATION_HEADER: () => ORGANIZATION_HEADER,
76
77
  PARTICIPANT_CAN_RECORD_ATTRIBUTE: () => PARTICIPANT_CAN_RECORD_ATTRIBUTE,
77
78
  PARTICIPANT_ROLE_ATTRIBUTE: () => PARTICIPANT_ROLE_ATTRIBUTE,
79
+ PARTICIPANT_STANDING_ATTRIBUTE: () => PARTICIPANT_STANDING_ATTRIBUTE,
78
80
  REACTION_TTL_MS: () => REACTION_TTL_MS,
79
81
  REFUSED_ABSENCE: () => REFUSED_ABSENCE,
80
82
  ROLE_RANK: () => ROLE_RANK,
81
83
  ROOM_EVENTS: () => ROOM_EVENTS,
82
84
  ROOM_EVENT_PREFIX: () => ROOM_EVENT_PREFIX,
83
- ROOM_TOKEN_HEADER: () => ROOM_TOKEN_HEADER,
84
85
  RPCS: () => RPCS,
85
86
  RecordingPlayer: () => RecordingPlayer,
86
87
  TABLES: () => TABLES,
@@ -152,6 +153,7 @@ __export(react_exports, {
152
153
  isGuest: () => isGuest,
153
154
  isGuestIdentity: () => isGuestIdentity,
154
155
  isHostRole: () => isHostRole,
156
+ isLinkFollower: () => isLinkFollower,
155
157
  isListenOnly: () => isListenOnly,
156
158
  isNoteTaker: () => isNoteTaker,
157
159
  isRealName: () => isRealName,
@@ -170,6 +172,7 @@ __export(react_exports, {
170
172
  meetingInviteAction: () => meetingInviteAction,
171
173
  meetingLength: () => meetingLength,
172
174
  meetingLink: () => meetingLink,
175
+ meetingPassHeaders: () => meetingPassHeaders,
173
176
  meetingPath: () => meetingPath,
174
177
  meetingRecordCache: () => meetingRecordCache,
175
178
  meetingSlug: () => meetingSlug,
@@ -214,6 +217,7 @@ __export(react_exports, {
214
217
  recordingLabel: () => recordingLabel,
215
218
  recordingNotice: () => recordingNotice,
216
219
  recordingOffsetMs: () => recordingOffsetMs,
220
+ rememberedMeetingPass: () => rememberedMeetingPass,
217
221
  roleOf: () => roleOf,
218
222
  roomTopic: () => roomTopic,
219
223
  rootAttributes: () => rootAttributes,
@@ -294,11 +298,18 @@ var MeetError = class extends Error {
294
298
  /** What to DO. Rendered to the user for the device codes. */
295
299
  remedy;
296
300
  cause;
297
- constructor(code, message, remedy, cause) {
301
+ /**
302
+ * The join gate's reason code (CORE-DESIGN §2.5) when the server named one: `not_found`,
303
+ * `cancelled`, `ended`, `locked`, `removed`, `lobby_rejects`, `not_invited`… The phase
304
+ * renders `refused:<reason>` (or `ended`) from it — never from the HTTP status.
305
+ */
306
+ reason;
307
+ constructor(code, message, remedy, cause, reason) {
298
308
  super(message);
299
309
  this.name = "MeetError";
300
310
  this.code = code;
301
311
  this.remedy = remedy;
312
+ this.reason = reason ?? null;
302
313
  if (cause !== void 0) this.cause = cause;
303
314
  }
304
315
  /** True when retrying after the host re-establishes a session is the fix. */
@@ -516,6 +527,17 @@ function createWebRoomTokenStorage(onDiagnostic) {
516
527
  }
517
528
  };
518
529
  }
530
+ function rememberedMeetingPass(roomName, storage = createWebRoomTokenStorage()) {
531
+ const raw = storage.read(roomName);
532
+ if (raw === null) return null;
533
+ try {
534
+ const parsed = JSON.parse(raw);
535
+ if (parsed.standing !== "guest") return null;
536
+ return typeof parsed.meetingPass === "string" && parsed.meetingPass.length > 0 ? parsed.meetingPass : null;
537
+ } catch {
538
+ return null;
539
+ }
540
+ }
519
541
  var MEET_ROUTES = {
520
542
  token: "/api/v1/meet/token",
521
543
  recordingStart: "/api/v1/meet/recording/start",
@@ -529,7 +551,7 @@ var MEET_ROUTES = {
529
551
  /**
530
552
  * THE POST-MEETING RECORD FOR A READER WITH NO DATABASE (MRF-1). A `GET`,
531
553
  * authorized by the room token this device already holds, carried in the
532
- * `x-meet-room-token` header — never a query parameter, because a bearer
554
+ * `x-meet-pass` header — never a query parameter, because a bearer
533
555
  * credential in a URL lands in access logs, referrers and history.
534
556
  */
535
557
  record: "/api/v1/meet/record",
@@ -537,7 +559,7 @@ var MEET_ROUTES = {
537
559
  recordAccess: "/api/v1/meet/record/access",
538
560
  /**
539
561
  * KEEP YOUR NOTES (2026-10-01): a guest who just created an account presents
540
- * their kept pass in `x-meet-room-token` with their new session, and the
562
+ * their kept pass in `x-meet-pass` with their new session, and the
541
563
  * meeting they attended becomes theirs to read. Idempotent; never a grant.
542
564
  */
543
565
  claim: "/api/v1/meet/claim",
@@ -559,7 +581,7 @@ var MEET_ROUTES = {
559
581
  * Meet wave 2: EVERY in-meeting host power (mute, stop video, mute all, ask to
560
582
  * unmute, spotlight, lower hands, co-host, hand over, rename, remove, report).
561
583
  * Decided by the server from the meeting itself; a guest's pass rides
562
- * `x-meet-room-token`.
584
+ * `x-meet-pass`.
563
585
  */
564
586
  moderate: "/api/v1/meet/moderate",
565
587
  /** Meet wave 2: the SAVED chat — `POST` sends to everyone, `GET ?meeting_id=` reads history. */
@@ -570,12 +592,26 @@ var MEET_ROUTES = {
570
592
  */
571
593
  collab: "/api/v1/meet/collab",
572
594
  /** Meet wave 5: translate finished caption lines into this viewer's language. */
573
- captionsTranslate: "/api/v1/meet/captions/translate"
595
+ captionsTranslate: "/api/v1/meet/captions/translate",
596
+ /**
597
+ * S4 — THE STATE READ (CORE-DESIGN §2.6): `GET ?meeting_id=` with `x-meet-pass`. This
598
+ * person's admission (lazy expiry applied), whether anyone who can admit is here, the run's
599
+ * phase. Read every `waiting_poll_seconds` while waiting and on every `mx.state.changed`.
600
+ */
601
+ state: "/api/v1/meet/state",
602
+ /** S4: `withdraw` (stop waiting) or `leave` (close this tab); the pass may ride in the body. */
603
+ leave: "/api/v1/meet/leave",
604
+ /** S4: a pass holder (guests included) saw the recording notice, for this run. */
605
+ consent: "/api/v1/meet/consent"
574
606
  };
575
607
  function meetingPath(template, meetingId) {
576
608
  return template.replace("{meeting_id}", encodeURIComponent(meetingId));
577
609
  }
578
- var ROOM_TOKEN_HEADER = "x-meet-room-token";
610
+ var MEETING_PASS_HEADER = "x-meet-pass";
611
+ function meetingPassHeaders(token) {
612
+ const pass = token?.meetingPass ?? null;
613
+ return pass !== null && pass.length > 0 ? { [MEETING_PASS_HEADER]: pass } : {};
614
+ }
579
615
  var REFRESH_RATIO = 0.8;
580
616
  function readString(source, key) {
581
617
  const value = source[key];
@@ -585,7 +621,12 @@ function readBoolean(source, key, fallback) {
585
621
  const value = source[key];
586
622
  return typeof value === "boolean" ? value : fallback;
587
623
  }
588
- var TOKEN_ROLES = ["host", "cohost", "participant", "guest"];
624
+ var TOKEN_STANDINGS = ["member", "invited", "outsider", "guest"];
625
+ function readStanding(source, key) {
626
+ const value = source[key];
627
+ return TOKEN_STANDINGS.includes(value) ? value : null;
628
+ }
629
+ var TOKEN_ROLES = ["host", "cohost", "presenter", "participant", "viewer"];
589
630
  function readRole(source, key) {
590
631
  const value = source[key];
591
632
  return TOKEN_ROLES.includes(value) ? value : null;
@@ -628,8 +669,15 @@ function parseRoomToken(body, roomName) {
628
669
  // Deriving the role from what DID arrive keeps a host a host against a
629
670
  // server that has not landed MRI-A11 — and never invents a host from
630
671
  // silence, because both fallbacks below grant nothing.
631
- role: readRole(payload, "role") ?? (isHost ? "host" : identity.startsWith("guest:") ? "guest" : "participant"),
672
+ // A word this package does not know grants nothing: `participant`, never a host.
673
+ role: readRole(payload, "role") ?? (isHost ? "host" : "participant"),
674
+ standing: readStanding(payload, "standing"),
675
+ meetingPass: readString(payload, "meeting_pass"),
676
+ meetingPassExpiresAt: readString(payload, "meeting_pass_expires_at"),
677
+ sessionId: readString(payload, "session_id"),
632
678
  lobby: readBoolean(payload, "lobby", false),
679
+ admissionState: readString(payload, "admission_state") ?? void 0,
680
+ pollSeconds: typeof payload["poll_seconds"] === "number" ? payload["poll_seconds"] : void 0,
633
681
  breakoutRoomId: readString(payload, "breakout_room_id") ?? null
634
682
  };
635
683
  }
@@ -639,7 +687,7 @@ function createTokenClient(options) {
639
687
  const storage = options.storage;
640
688
  function remember(token) {
641
689
  if (storage === void 0) return;
642
- if (!token.identity.startsWith("guest:")) return;
690
+ if (token.standing !== "guest") return;
643
691
  storage.write(token.roomName, JSON.stringify(token));
644
692
  }
645
693
  function recall(roomName) {
@@ -697,22 +745,28 @@ function createTokenClient(options) {
697
745
  throw new MeetError(
698
746
  "forbidden",
699
747
  said?.message ?? "You can't join this meeting.",
700
- said?.remedy ?? "Ask the person who shared the link to let you in, then open the link again."
748
+ said?.remedy ?? "Ask the person who shared the link to let you in, then open the link again.",
749
+ void 0,
750
+ said?.reason ?? null
701
751
  );
702
752
  }
703
753
  if (response.status === 404) {
704
754
  throw new MeetError(
705
755
  "not-found",
706
756
  "meet.token: no such meeting",
707
- "The link is wrong or the meeting was deleted. Show the 'meeting not found' screen; a durable link that 404s is never a retry."
757
+ "The link is wrong or the meeting was deleted. Show the 'meeting not found' screen; a durable link that 404s is never a retry.",
758
+ void 0,
759
+ "not_found"
708
760
  );
709
761
  }
710
762
  if (response.status === 410) {
711
- const reason = (await serverSentence(response))?.message ?? "This meeting was cancelled.";
763
+ const said = await serverSentence(response);
712
764
  throw new MeetError(
713
765
  "cancelled",
714
- reason,
715
- "Nobody can join a cancelled meeting. Show the cancelled screen with this sentence; ask the host to schedule a new one."
766
+ said?.message ?? "This meeting was cancelled.",
767
+ "Nobody can join a cancelled meeting. Show the cancelled screen with this sentence; ask the host to schedule a new one.",
768
+ void 0,
769
+ said?.reason ?? "cancelled"
716
770
  );
717
771
  }
718
772
  if (response.status === 423) {
@@ -720,7 +774,9 @@ function createTokenClient(options) {
720
774
  throw new MeetError(
721
775
  "room-locked",
722
776
  said?.message ?? "The meeting is locked.",
723
- said?.remedy ?? "The host locked the room. Ask them to unlock it, then join again."
777
+ said?.remedy ?? "The host locked the room. Ask them to unlock it, then join again.",
778
+ void 0,
779
+ said?.reason ?? "locked"
724
780
  );
725
781
  }
726
782
  if (response.status === 503) {
@@ -789,6 +845,10 @@ function placeholder(roomName) {
789
845
  canRecord: false,
790
846
  isHost: false,
791
847
  role: "participant",
848
+ standing: null,
849
+ meetingPass: null,
850
+ meetingPassExpiresAt: null,
851
+ sessionId: null,
792
852
  lobby: false,
793
853
  breakoutRoomId: null
794
854
  };
@@ -801,7 +861,8 @@ async function serverSentence(response) {
801
861
  const message = typeof source.user_message === "string" && source.user_message.trim() !== "" ? source.user_message : typeof source.message === "string" && source.message.trim() !== "" ? source.message : null;
802
862
  if (message === null) return null;
803
863
  const remedy = typeof source.remedy === "string" && source.remedy.trim() !== "" ? source.remedy : void 0;
804
- return { message, remedy };
864
+ const reason = typeof source.reason === "string" && source.reason !== "" ? source.reason : null;
865
+ return { message, remedy, reason };
805
866
  } catch {
806
867
  return null;
807
868
  }
@@ -990,7 +1051,7 @@ function createMeetAi(options) {
990
1051
  operation: "meet.ai.ask",
991
1052
  // A GUEST MAY ASK. The server decides whether it can answer.
992
1053
  requireAuth: false,
993
- ...roomToken !== null ? { headers: { [ROOM_TOKEN_HEADER]: roomToken } } : {},
1054
+ ...roomToken !== null ? { headers: { [MEETING_PASS_HEADER]: roomToken } } : {},
994
1055
  ...signal !== void 0 ? { signal } : {}
995
1056
  });
996
1057
  if (!response.ok) {
@@ -1204,14 +1265,15 @@ var MEET_EVENTS = {
1204
1265
  cancel: "mx.call.cancel",
1205
1266
  /** You answered (or declined) on another device. Stop ringing here. */
1206
1267
  settled: "mx.call.settled",
1207
- /** Someone is in the lobby. Hosts render the knock. */
1208
- knock: "mx.lobby.knock",
1209
- /** A host decided. The knocker learns it here — they have no DB read. */
1210
- admission: "mx.lobby.admission",
1211
- /** The room was locked or unlocked. */
1212
- lock: "mx.room.lock",
1213
- /** Recording started or stopped. EVERY participant renders the indicator. */
1214
- recording: "mx.room.recording"
1268
+ /**
1269
+ * 🚨 S4 (CORE-DESIGN §2.6): the ONE room-channel event — payload-free "something changed,
1270
+ * read again". Nothing a person must see rides it: a waiting person learns admitted /
1271
+ * denied / expired / ended by READING their row (`GET /v1/meet/state`) on a timer and on
1272
+ * every hint. The four payload broadcasts (`mx.lobby.knock`, `mx.lobby.admission`,
1273
+ * `mx.room.lock`, `mx.room.recording`) are gone with their senders and receivers. The
1274
+ * server publishes the same event on the same topic (aidream `admission.room_topic`).
1275
+ */
1276
+ stateChanged: "mx.state.changed"
1215
1277
  };
1216
1278
 
1217
1279
  // src/core/types.ts
@@ -1225,14 +1287,19 @@ function isGuest(identity) {
1225
1287
  return identity.userId === null;
1226
1288
  }
1227
1289
  var ROLE_RANK = {
1228
- host: 3,
1229
- cohost: 2,
1290
+ host: 4,
1291
+ cohost: 3,
1292
+ presenter: 2,
1230
1293
  participant: 1,
1231
- guest: 0
1294
+ viewer: 0
1232
1295
  };
1296
+ function isLinkFollower(standing) {
1297
+ return standing === "guest" || standing === "outsider";
1298
+ }
1233
1299
  function isHostRole(role) {
1234
1300
  return role === "host" || role === "cohost";
1235
1301
  }
1302
+ var WAITING_ADMISSION_STATES = ["waiting_host", "knocking"];
1236
1303
 
1237
1304
  // src/core/calls.ts
1238
1305
  var TRANSITIONS = {
@@ -2136,15 +2203,15 @@ var CAPTION_LANGUAGES = [
2136
2203
  { tag: "id", name: "Indonesian" },
2137
2204
  { tag: "sv", name: "Swedish" }
2138
2205
  ];
2139
- async function loadMeetingActivities(api, meetingId, roomToken = null) {
2206
+ async function loadMeetingActivities(api, meetingId, meetingPass = null) {
2140
2207
  let response;
2141
2208
  try {
2142
2209
  response = await api.request({
2143
2210
  path: `${MEET_ROUTES.collab}?meeting_id=${encodeURIComponent(meetingId)}`,
2144
2211
  method: "GET",
2145
2212
  operation: "meet.loadMeetingActivities",
2146
- requireAuth: roomToken === null,
2147
- ...roomToken !== null ? { headers: { [ROOM_TOKEN_HEADER]: roomToken } } : {}
2213
+ requireAuth: meetingPass === null,
2214
+ ...meetingPass !== null ? { headers: { [MEETING_PASS_HEADER]: meetingPass } } : {}
2148
2215
  });
2149
2216
  } catch (error) {
2150
2217
  return {
@@ -2241,12 +2308,14 @@ var DEFAULT_EFFECTS = {
2241
2308
  };
2242
2309
  var NO_GRANTS = {
2243
2310
  role: "participant",
2311
+ standing: null,
2244
2312
  canRecord: false,
2245
2313
  canPublish: false,
2246
2314
  canSubscribe: false
2247
2315
  };
2248
2316
  var EMPTY = {
2249
2317
  phase: "idle",
2318
+ waiting: null,
2250
2319
  meeting: null,
2251
2320
  localIdentity: null,
2252
2321
  grants: NO_GRANTS,
@@ -2287,6 +2356,11 @@ function effectiveRole(snapshot) {
2287
2356
  const fromToken = snapshot.grants.role;
2288
2357
  return ROLE_RANK[fromRoster] >= ROLE_RANK[fromToken] ? fromRoster : fromToken;
2289
2358
  }
2359
+ function effectiveStanding(snapshot) {
2360
+ if (snapshot.grants.standing != null) return snapshot.grants.standing;
2361
+ const me = snapshot.participants.find((p) => p.identity === snapshot.localIdentity);
2362
+ return me?.standing ?? null;
2363
+ }
2290
2364
  var REACTION_TTL_MS = 5e3;
2291
2365
  var CHAT_LIMIT = 200;
2292
2366
  function createMeetStore(initial = {}) {
@@ -2498,19 +2572,25 @@ function roleOf(participants, identity) {
2498
2572
  return participants.find((p) => p.identity === identity)?.role ?? "participant";
2499
2573
  }
2500
2574
  var END_MEETING_NOT_SERVED = "Ending a meeting is not available on this server yet";
2501
- function lobbyEntryFrom(attendee) {
2502
- if (attendee.admissionState !== "knocking") return null;
2575
+ function lobbyEntryFrom(attendee, now = Date.now()) {
2576
+ if (!WAITING_ADMISSION_STATES.includes(attendee.admissionState)) return null;
2577
+ const expires = attendee.knockExpiresAt ? Date.parse(attendee.knockExpiresAt) : Number.NaN;
2578
+ if (!Number.isNaN(expires) && expires <= now) return null;
2503
2579
  return {
2504
2580
  identity: attendee.identity,
2505
2581
  displayName: attendee.displayName,
2506
2582
  avatarUrl: attendee.avatarUrl,
2507
2583
  userId: attendee.userId,
2508
2584
  state: "knocking",
2509
- knockedAt: attendee.joinedAt ?? (/* @__PURE__ */ new Date()).toISOString(),
2585
+ knockedAt: attendee.knockedAt ?? attendee.joinedAt ?? (/* @__PURE__ */ new Date()).toISOString(),
2586
+ knockExpiresAt: attendee.knockExpiresAt ?? null,
2510
2587
  decidedAt: null,
2511
2588
  decidedBy: null
2512
2589
  };
2513
2590
  }
2591
+ function decidedState(state) {
2592
+ return WAITING_ADMISSION_STATES.includes(state) ? "expired" : state;
2593
+ }
2514
2594
  function createMeetingSession(options) {
2515
2595
  const { manager, repository, store, identity } = options;
2516
2596
  let channel = null;
@@ -2534,11 +2614,6 @@ function createMeetingSession(options) {
2534
2614
  }
2535
2615
  return identity.userId;
2536
2616
  }
2537
- function knockIdentity() {
2538
- return asParticipantIdentity(
2539
- identity.userId !== null ? `user:${identity.userId}` : `guest:${identity.deviceId}`
2540
- );
2541
- }
2542
2617
  function requireMeeting(operation) {
2543
2618
  const meeting = store.snapshot().meeting;
2544
2619
  if (meeting === null) {
@@ -2550,6 +2625,191 @@ function createMeetingSession(options) {
2550
2625
  }
2551
2626
  return meeting;
2552
2627
  }
2628
+ let polling = false;
2629
+ let pollTimer = null;
2630
+ let reading = null;
2631
+ let admittedOnce = false;
2632
+ let unlisten = null;
2633
+ function report(level, message, remedy) {
2634
+ options.onDiagnostic?.(remedy === void 0 ? { level, message } : { level, message, remedy });
2635
+ }
2636
+ function pollMs() {
2637
+ const seconds = store.snapshot().waiting?.pollSeconds ?? 3;
2638
+ return Math.max(1, Math.min(60, seconds)) * 1e3;
2639
+ }
2640
+ function schedule(ms2) {
2641
+ if (pollTimer !== null) clearTimeout(pollTimer);
2642
+ if (!polling) return;
2643
+ pollTimer = setTimeout(() => {
2644
+ pollTimer = null;
2645
+ void readState2().finally(() => schedule(pollMs()));
2646
+ }, ms2);
2647
+ }
2648
+ function listen() {
2649
+ if (unlisten !== null || typeof window === "undefined") return;
2650
+ const nudge = () => {
2651
+ if (typeof document !== "undefined" && document.visibilityState === "hidden") return;
2652
+ void readState2();
2653
+ };
2654
+ const goodbye = () => {
2655
+ const meeting = store.snapshot().meeting;
2656
+ const pass = options.resolvePass?.() ?? null;
2657
+ const waiting = store.snapshot().waiting;
2658
+ if (meeting === null || pass === null || waiting === null) return;
2659
+ if (!WAITING_ADMISSION_STATES.includes(waiting.admission)) return;
2660
+ try {
2661
+ navigator.sendBeacon?.(
2662
+ options.api.url(MEET_ROUTES.leave),
2663
+ new Blob([JSON.stringify({ meeting_id: meeting.id, intent: "withdraw", meeting_pass: pass })], {
2664
+ type: "application/json"
2665
+ })
2666
+ );
2667
+ } catch {
2668
+ }
2669
+ };
2670
+ window.addEventListener("online", nudge);
2671
+ window.addEventListener("pagehide", goodbye);
2672
+ document.addEventListener("visibilitychange", nudge);
2673
+ unlisten = () => {
2674
+ window.removeEventListener("online", nudge);
2675
+ window.removeEventListener("pagehide", goodbye);
2676
+ document.removeEventListener("visibilitychange", nudge);
2677
+ };
2678
+ }
2679
+ function stopWaiting() {
2680
+ polling = false;
2681
+ if (pollTimer !== null) clearTimeout(pollTimer);
2682
+ pollTimer = null;
2683
+ unlisten?.();
2684
+ unlisten = null;
2685
+ }
2686
+ function patchWaiting(next) {
2687
+ const current = store.snapshot().waiting;
2688
+ const base = current ?? {
2689
+ admission: "knocking",
2690
+ admittersPresent: null,
2691
+ reknockAllowed: true,
2692
+ knockExpiresAt: null,
2693
+ pollSeconds: 3,
2694
+ liveVersion: 0
2695
+ };
2696
+ store.patch({ waiting: { ...base, ...next } });
2697
+ }
2698
+ function apply(read) {
2699
+ const snapshot = store.snapshot();
2700
+ if (snapshot.phase !== "lobby") return;
2701
+ const admission = read.admission_state ?? null;
2702
+ const ended = admission === "ended" || read.session_phase === "ended" || read.meeting_phase === "ended";
2703
+ if (admission === "admitted" && !ended) {
2704
+ if (admittedOnce) return;
2705
+ admittedOnce = true;
2706
+ stopWaiting();
2707
+ options.onAdmitted?.();
2708
+ return;
2709
+ }
2710
+ if (ended) {
2711
+ stopWaiting();
2712
+ patchWaiting({ admission: "ended" });
2713
+ const meeting = snapshot.meeting;
2714
+ if (read.meeting_phase === "ended" && meeting !== null && meeting.endedAt === null) {
2715
+ store.patch({ meeting: { ...meeting, endedAt: (/* @__PURE__ */ new Date()).toISOString() } });
2716
+ }
2717
+ return;
2718
+ }
2719
+ if (admission === null) return;
2720
+ patchWaiting({
2721
+ admission,
2722
+ admittersPresent: read.admitters_present ?? null,
2723
+ reknockAllowed: read.reknock_allowed ?? true,
2724
+ knockExpiresAt: read.knock_expires_at ?? null,
2725
+ pollSeconds: read.poll_seconds ?? snapshot.waiting?.pollSeconds ?? 3,
2726
+ liveVersion: read.live_version ?? 0
2727
+ });
2728
+ if (admission === "denied" || admission === "expired" || admission === "withdrawn" || admission === "removed") {
2729
+ stopWaiting();
2730
+ }
2731
+ }
2732
+ async function readState2() {
2733
+ if (reading !== null) return reading;
2734
+ const meeting = store.snapshot().meeting;
2735
+ const pass = options.resolvePass?.() ?? null;
2736
+ if (meeting === null || pass === null) return;
2737
+ reading = (async () => {
2738
+ try {
2739
+ const response = await options.api.request({
2740
+ path: `${MEET_ROUTES.state}?meeting_id=${encodeURIComponent(meeting.id)}`,
2741
+ method: "GET",
2742
+ requireAuth: false,
2743
+ operation: "meet.state",
2744
+ headers: { [MEETING_PASS_HEADER]: pass }
2745
+ });
2746
+ if (!response.ok) {
2747
+ let body = null;
2748
+ try {
2749
+ body = await response.json();
2750
+ } catch {
2751
+ body = null;
2752
+ }
2753
+ const refusal2 = readServerRefusal(body);
2754
+ report(
2755
+ "warn",
2756
+ refusal2?.reason ?? `meet.state: the server answered ${response.status}`,
2757
+ refusal2?.remedy ?? "The waiting room keeps asking; nothing on this screen changes until the server answers."
2758
+ );
2759
+ return;
2760
+ }
2761
+ apply(await response.json());
2762
+ } catch (error) {
2763
+ report("info", `meet.state: ${String(error)}`, "Waiting continues; the state is read again shortly.");
2764
+ } finally {
2765
+ reading = null;
2766
+ }
2767
+ })();
2768
+ return reading;
2769
+ }
2770
+ async function postLeave(intent) {
2771
+ const meeting = requireMeeting(`meet.${intent}`);
2772
+ const pass = options.resolvePass?.() ?? null;
2773
+ const response = await options.api.request({
2774
+ path: MEET_ROUTES.leave,
2775
+ method: "POST",
2776
+ requireAuth: false,
2777
+ operation: `meet.${intent}`,
2778
+ body: { meeting_id: meeting.id, intent },
2779
+ ...pass !== null ? { headers: { [MEETING_PASS_HEADER]: pass } } : {}
2780
+ });
2781
+ if (!response.ok) {
2782
+ let body = null;
2783
+ try {
2784
+ body = await response.json();
2785
+ } catch {
2786
+ body = null;
2787
+ }
2788
+ const refusal2 = readServerRefusal(body);
2789
+ throw new MeetError(
2790
+ "transport",
2791
+ refusal2?.reason ?? `meet.${intent}: the server answered ${response.status}`,
2792
+ refusal2?.remedy ?? "You are still in the waiting room. Try again."
2793
+ );
2794
+ }
2795
+ }
2796
+ function hint() {
2797
+ channel?.send(MEET_EVENTS.stateChanged, { v: null });
2798
+ }
2799
+ async function refreshQueue() {
2800
+ const meeting = store.snapshot().meeting;
2801
+ if (meeting === null || isGuest(identity) || !isHostRole(effectiveRole(store.snapshot()))) return;
2802
+ try {
2803
+ const attendees = await repository.attendees(meeting.id);
2804
+ store.learn(attendees);
2805
+ for (const attendee of attendees) {
2806
+ const entry = lobbyEntryFrom(attendee);
2807
+ if (entry !== null) store.upsertLobby(entry);
2808
+ else store.setAdmission(attendee.identity, decidedState(attendee.admissionState), null);
2809
+ }
2810
+ } catch {
2811
+ }
2812
+ }
2553
2813
  return {
2554
2814
  watch(roomName) {
2555
2815
  channel?.close();
@@ -2557,58 +2817,22 @@ function createMeetingSession(options) {
2557
2817
  topic: roomTopic(roomName),
2558
2818
  broadcast: [
2559
2819
  {
2560
- event: MEET_EVENTS.knock,
2561
- onMessage: (message) => {
2562
- store.upsertLobby(message.data);
2563
- }
2564
- },
2565
- {
2566
- event: MEET_EVENTS.admission,
2567
- onMessage: (message) => {
2568
- const verdict = message.data;
2569
- const decided = asParticipantIdentity(verdict.identity);
2570
- store.setAdmission(
2571
- decided,
2572
- verdict.admitted ? "admitted" : "denied",
2573
- verdict.by === null ? null : verdict.by
2574
- );
2575
- if (decided === knockIdentity()) {
2576
- if (verdict.admitted) options.onAdmitted?.();
2577
- else options.onDenied?.();
2578
- }
2579
- }
2580
- },
2581
- {
2582
- event: MEET_EVENTS.lock,
2583
- onMessage: (message) => {
2584
- const { locked } = message.data;
2585
- const meeting = store.snapshot().meeting;
2586
- store.patch({
2587
- locked,
2588
- ...meeting !== null ? { meeting: { ...meeting, locked } } : {}
2589
- });
2590
- }
2591
- },
2592
- {
2593
- event: MEET_EVENTS.recording,
2594
- onMessage: (message) => {
2595
- const payload = message.data;
2596
- store.setRecording(
2597
- payload.state,
2598
- payload.by === null ? null : payload.by,
2599
- "room"
2600
- );
2820
+ // 🚨 PAYLOAD-FREE. Every receiver reads again; a spoofed hint only causes a read.
2821
+ event: MEET_EVENTS.stateChanged,
2822
+ onMessage: () => {
2823
+ if (store.snapshot().phase === "lobby") void readState2();
2824
+ void refreshQueue();
2601
2825
  }
2602
2826
  }
2603
2827
  ],
2604
- // THE BACKFILL DOOR. A host whose socket blipped must not miss a knock:
2605
- // re-reading the meeting restores lock/recording truth, and knockers
2606
- // re-announce on their own timer.
2828
+ // THE BACKFILL DOOR. A socket blip must not lose a decision: re-read the meeting
2829
+ // row, and a waiting tab re-reads its own state.
2607
2830
  onBackfill: async () => {
2608
2831
  const meeting = store.snapshot().meeting;
2609
2832
  if (meeting === null) return;
2610
2833
  const fresh = isGuest(identity) ? await repository.meetingBySlug(meeting.slug) : await readMeeting(meeting.id);
2611
2834
  store.patch({ meeting: fresh, locked: fresh.locked });
2835
+ if (store.snapshot().phase === "lobby") void readState2();
2612
2836
  }
2613
2837
  });
2614
2838
  },
@@ -2669,7 +2893,7 @@ function createMeetingSession(options) {
2669
2893
  store.upsertLobby(entry);
2670
2894
  return;
2671
2895
  }
2672
- store.setAdmission(attendee.identity, attendee.admissionState, null);
2896
+ store.setAdmission(attendee.identity, decidedState(attendee.admissionState), null);
2673
2897
  }
2674
2898
  },
2675
2899
  {
@@ -2721,62 +2945,75 @@ function createMeetingSession(options) {
2721
2945
  if (entry !== null) {
2722
2946
  store.upsertLobby(entry);
2723
2947
  } else {
2724
- store.setAdmission(attendee.identity, attendee.admissionState, null);
2948
+ store.setAdmission(attendee.identity, decidedState(attendee.admissionState), null);
2725
2949
  }
2726
2950
  }
2727
2951
  }
2728
2952
  });
2729
2953
  meetingChannel.requestBackfill("manual");
2730
2954
  },
2731
- knock(roomName) {
2732
- const handle = channel ?? manager.open({ topic: roomTopic(roomName) });
2733
- channel = handle;
2734
- const entry = {
2735
- identity: knockIdentity(),
2736
- displayName: identity.displayName,
2737
- avatarUrl: identity.avatarUrl,
2738
- userId: identity.userId,
2739
- state: "knocking",
2740
- knockedAt: (/* @__PURE__ */ new Date()).toISOString(),
2741
- decidedAt: null,
2742
- decidedBy: null
2743
- };
2744
- store.upsertLobby(entry);
2745
- handle.send(MEET_EVENTS.knock, entry);
2746
- },
2747
2955
  async admit(entry, admitted) {
2748
2956
  const by = requireHostUser("meet.admit");
2749
2957
  const meeting = requireMeeting("meet.admit");
2750
- await repository.admit({
2958
+ const row = await repository.decideAdmission({
2751
2959
  meetingId: meeting.id,
2752
2960
  identity: entry.identity,
2753
- admitted,
2961
+ decision: admitted ? "admit" : "deny",
2754
2962
  byUserId: by
2755
2963
  });
2756
2964
  store.upsertLobby({
2757
2965
  ...entry,
2758
- state: admitted ? "admitted" : "denied",
2966
+ state: decidedState(row.admissionState),
2759
2967
  decidedAt: (/* @__PURE__ */ new Date()).toISOString(),
2760
2968
  decidedBy: by
2761
2969
  });
2762
- channel?.send(MEET_EVENTS.admission, {
2970
+ hint();
2971
+ },
2972
+ async undeny(entry) {
2973
+ const by = requireHostUser("meet.undeny");
2974
+ const meeting = requireMeeting("meet.undeny");
2975
+ const row = await repository.decideAdmission({
2976
+ meetingId: meeting.id,
2763
2977
  identity: entry.identity,
2764
- admitted,
2765
- by
2978
+ decision: "undeny",
2979
+ byUserId: by
2766
2980
  });
2981
+ store.upsertLobby({ ...entry, state: decidedState(row.admissionState), decidedAt: (/* @__PURE__ */ new Date()).toISOString(), decidedBy: by });
2982
+ hint();
2767
2983
  },
2768
2984
  async admitAll() {
2769
- const knocking = store.snapshot().lobby.filter((entry) => entry.state === "knocking");
2770
- for (const entry of knocking) {
2771
- await this.admit(entry, true);
2772
- }
2985
+ const by = requireHostUser("meet.admitAll");
2986
+ const meeting = requireMeeting("meet.admitAll");
2987
+ const rows = await repository.admitAll(meeting.id, by);
2988
+ for (const row of rows) {
2989
+ store.setAdmission(row.identity, row.admissionState, by);
2990
+ }
2991
+ hint();
2992
+ },
2993
+ startWaiting() {
2994
+ if (polling) return;
2995
+ polling = true;
2996
+ admittedOnce = false;
2997
+ listen();
2998
+ schedule(0);
2999
+ },
3000
+ stopWaiting,
3001
+ readState: readState2,
3002
+ async withdraw() {
3003
+ stopWaiting();
3004
+ await postLeave("withdraw");
3005
+ patchWaiting({ admission: "withdrawn" });
3006
+ hint();
3007
+ },
3008
+ async askAgain() {
3009
+ await options.rejoin?.();
2773
3010
  },
2774
3011
  async setLock(locked) {
2775
3012
  const by = requireHostUser("meet.setLock");
2776
3013
  const meeting = requireMeeting("meet.setLock");
2777
3014
  const updated = await repository.setLock(meeting.id, locked, by);
2778
3015
  store.patch({ meeting: updated, locked: updated.locked });
2779
- channel?.send(MEET_EVENTS.lock, { locked: updated.locked });
3016
+ hint();
2780
3017
  return updated;
2781
3018
  },
2782
3019
  /**
@@ -2834,6 +3071,7 @@ function createMeetingSession(options) {
2834
3071
  return { endedAt, served: true };
2835
3072
  },
2836
3073
  dispose() {
3074
+ stopWaiting();
2837
3075
  channel?.close();
2838
3076
  channel = null;
2839
3077
  meetingChannel?.close();
@@ -2878,9 +3116,15 @@ var RPCS = {
2878
3116
  getOrCreateMeeting: "meet_get_or_create_meeting",
2879
3117
  /** Resolve a durable public link, before/during/after its scheduled time. */
2880
3118
  meetingBySlug: "meet_meeting_by_slug",
2881
- admit: "meet_admit_participant",
3119
+ /*
3120
+ * ── S4 ADMISSION (CORE-DESIGN §2.4). Each returns the decided row; the caller then
3121
+ * sends the payload-free `mx.state.changed` hint. Authorized by `who_may_admit`.
3122
+ */
3123
+ admit: "meet_admit",
3124
+ deny: "meet_deny",
3125
+ undeny: "meet_undeny",
3126
+ admitAll: "meet_admit_all",
2882
3127
  setLock: "meet_set_lock",
2883
- endMeeting: "meet_end_meeting",
2884
3128
  recordConsent: "meet_record_consent",
2885
3129
  /*
2886
3130
  * ── MEET WAVE 1 — management (0.8). Every write is a SECURITY DEFINER door
@@ -2922,13 +3166,13 @@ var RPCS = {
2922
3166
  /** Titles, agendas, current notes and final transcript lines; snippets mark hits with ⟦ ⟧. */
2923
3167
  search: "meet_search"
2924
3168
  };
2925
- var VOID_RPCS = [RPCS.admit, RPCS.recordConsent];
3169
+ var VOID_RPCS = [RPCS.recordConsent];
2926
3170
  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";
2927
3171
  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";
2928
3172
  var RECORDING_COLUMNS = "id,meeting_id,state,started_at,ended_at,started_by,file_id,failure_reason";
2929
3173
  var SEGMENT_COLUMNS = "id,meeting_id,identity,speaker,text,started_at,is_final,metadata";
2930
3174
  var NOTE_COLUMNS = "id,meeting_id,text,kind,assignee_user_id,created_at,metadata";
2931
- 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";
3175
+ 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";
2932
3176
  function projectStays(value) {
2933
3177
  if (!Array.isArray(value)) return [];
2934
3178
  const stays = [];
@@ -3294,8 +3538,17 @@ function createMeetRepository(options) {
3294
3538
  const operation = "meet.projectAttendee";
3295
3539
  const role = text(row, "role");
3296
3540
  const admission = text(row, "admission_state");
3297
- const known = ["host", "cohost", "participant", "guest"];
3298
- const states = ["knocking", "admitted", "denied", "removed"];
3541
+ const known = ["host", "cohost", "presenter", "participant", "viewer"];
3542
+ const states = [
3543
+ "waiting_host",
3544
+ "knocking",
3545
+ "admitted",
3546
+ "denied",
3547
+ "expired",
3548
+ "withdrawn",
3549
+ "removed",
3550
+ "ended"
3551
+ ];
3299
3552
  const userId = text(row, "participant_user_id");
3300
3553
  const identity = requireText(row, "identity", operation);
3301
3554
  return {
@@ -3309,6 +3562,8 @@ function createMeetRepository(options) {
3309
3562
  userId: userId === null ? null : asUserId(userId),
3310
3563
  role: known.includes(role) ? role : "participant",
3311
3564
  admissionState: states.includes(admission) ? admission : "admitted",
3565
+ knockedAt: text(row, "knocked_at"),
3566
+ knockExpiresAt: text(row, "knock_expires_at"),
3312
3567
  isAgent: bool(row, "is_agent", false),
3313
3568
  joinedAt: text(row, "joined_at"),
3314
3569
  leftAt: text(row, "left_at"),
@@ -3587,16 +3842,26 @@ function createMeetRepository(options) {
3587
3842
  hasMore: rows.length > limit
3588
3843
  };
3589
3844
  },
3590
- async admit({ meetingId, identity, admitted, byUserId }) {
3591
- await guardVoid(
3592
- "meet.admit",
3593
- () => db.rpc(RPCS.admit, {
3845
+ async decideAdmission({ meetingId, identity, decision, byUserId }) {
3846
+ const fn = decision === "admit" ? RPCS.admit : decision === "deny" ? RPCS.deny : RPCS.undeny;
3847
+ const row = await guard(
3848
+ `meet.${decision}`,
3849
+ () => db.rpc(fn, {
3594
3850
  p_meeting_id: meetingId,
3595
3851
  p_identity: identity,
3596
- p_admitted: admitted,
3597
3852
  p_by_user_id: byUserId
3598
3853
  })
3599
3854
  );
3855
+ return projectAttendee(row);
3856
+ },
3857
+ async admitAll(meetingId, byUserId) {
3858
+ const response = await db.rpc(RPCS.admitAll, {
3859
+ p_meeting_id: meetingId,
3860
+ p_by_user_id: byUserId
3861
+ });
3862
+ if (response.error !== null) throw normalizeMeetError(response.error, "meet.admitAll");
3863
+ const rows = Array.isArray(response.data) ? response.data : [];
3864
+ return rows.map(projectAttendee);
3600
3865
  },
3601
3866
  async setLock(meetingId, locked, byUserId) {
3602
3867
  const row = await guard(
@@ -3609,16 +3874,6 @@ function createMeetRepository(options) {
3609
3874
  );
3610
3875
  return projectMeeting(row);
3611
3876
  },
3612
- async endMeeting(meetingId, byUserId) {
3613
- const row = await guard(
3614
- "meet.endMeeting",
3615
- () => db.rpc(RPCS.endMeeting, {
3616
- p_meeting_id: meetingId,
3617
- p_by_user_id: byUserId
3618
- })
3619
- );
3620
- return projectMeeting(row);
3621
- },
3622
3877
  async transcript(meetingId, limit = 1e3) {
3623
3878
  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);
3624
3879
  if (response.error !== null) throw normalizeMeetError(response.error, "meet.transcript");
@@ -4062,7 +4317,7 @@ function applyModeration(store, payload, now) {
4062
4317
  return ignored(`a moderation kind this version does not know: ${String(kind)}`);
4063
4318
  }
4064
4319
  }
4065
- var ROLES = ["host", "cohost", "participant", "guest"];
4320
+ var ROLES = ["host", "cohost", "presenter", "participant", "viewer"];
4066
4321
 
4067
4322
  // src/core/note-taker.ts
4068
4323
  var NOTE_TAKER_IDENTITY_PREFIX = "agent:note_taker:";
@@ -4147,7 +4402,12 @@ var TRANSCRIPTION_ATTRIBUTES = {
4147
4402
  // src/core/room.ts
4148
4403
  var PARTICIPANT_ROLE_ATTRIBUTE = "matrx.participant.role";
4149
4404
  var PARTICIPANT_CAN_RECORD_ATTRIBUTE = "matrx.participant.can_record";
4150
- var KNOWN_ROLES = ["host", "cohost", "participant", "guest"];
4405
+ var PARTICIPANT_STANDING_ATTRIBUTE = "matrx.participant.standing";
4406
+ var KNOWN_STANDINGS = ["member", "invited", "outsider", "guest"];
4407
+ function parseStanding(value) {
4408
+ return KNOWN_STANDINGS.includes(value) ? value : null;
4409
+ }
4410
+ var KNOWN_ROLES = ["host", "cohost", "presenter", "participant", "viewer"];
4151
4411
  function parseRole(value) {
4152
4412
  return KNOWN_ROLES.includes(value) ? value : "participant";
4153
4413
  }
@@ -4181,6 +4441,9 @@ function projectParticipant(driver, joinedAt) {
4181
4441
  // ATTRIBUTE FIRST, metadata second — see the constants above. Both are the
4182
4442
  // server's; the attribute is simply the one that always arrives.
4183
4443
  role: parseRole(driver.attributes[PARTICIPANT_ROLE_ATTRIBUTE] ?? metadata.role),
4444
+ standing: parseStanding(
4445
+ driver.attributes[PARTICIPANT_STANDING_ATTRIBUTE] ?? metadata.standing
4446
+ ),
4184
4447
  canRecord: driver.attributes[PARTICIPANT_CAN_RECORD_ATTRIBUTE] === "true" || metadata.can_record === true,
4185
4448
  isLocal: driver.isLocal,
4186
4449
  isAgent: metadata.is_agent === true,
@@ -4253,7 +4516,7 @@ function createRoomEngine(options) {
4253
4516
  const normalized = normalizeMeetError(error, operation);
4254
4517
  store.patch({
4255
4518
  phase: "failed",
4256
- failure: { message: normalized.message, remedy: normalized.remedy }
4519
+ failure: { message: normalized.message, remedy: normalized.remedy, reason: normalized.reason }
4257
4520
  });
4258
4521
  report(
4259
4522
  normalized.code === "session-unavailable" ? "warn" : "error",
@@ -4443,7 +4706,7 @@ function createRoomEngine(options) {
4443
4706
  }
4444
4707
  };
4445
4708
  async function connect(request) {
4446
- store.patch({ phase: "requesting-token", failure: null });
4709
+ store.patch({ phase: "requesting-token", failure: null, waiting: null });
4447
4710
  const token = await tokens.get({
4448
4711
  roomName: request.roomName,
4449
4712
  identity,
@@ -4457,16 +4720,27 @@ function createRoomEngine(options) {
4457
4720
  breakoutRoomId: token.breakoutRoomId ?? null,
4458
4721
  grants: {
4459
4722
  role: token.role,
4723
+ standing: token.standing,
4460
4724
  canRecord: token.canRecord,
4461
4725
  canPublish: token.canPublish,
4462
4726
  canSubscribe: token.canSubscribe
4463
4727
  }
4464
4728
  });
4465
- if (token.lobby) {
4466
- store.patch({ phase: "lobby" });
4729
+ if (token.lobby || !token.canSubscribe) {
4730
+ store.patch({
4731
+ phase: "lobby",
4732
+ waiting: {
4733
+ admission: token.admissionState !== void 0 && token.admissionState !== "admitted" ? token.admissionState : "knocking",
4734
+ admittersPresent: null,
4735
+ reknockAllowed: true,
4736
+ knockExpiresAt: null,
4737
+ pollSeconds: token.pollSeconds ?? 3,
4738
+ liveVersion: 0
4739
+ }
4740
+ });
4467
4741
  return;
4468
4742
  }
4469
- store.patch({ phase: "connecting" });
4743
+ store.patch({ phase: "connecting", waiting: null });
4470
4744
  const created = await options.driverFactory(handlers);
4471
4745
  if (disposed) {
4472
4746
  await created.disconnect();
@@ -4802,7 +5076,7 @@ function createInRoomClient(options) {
4802
5076
  }
4803
5077
  function headers() {
4804
5078
  const token = options.resolveRoomToken();
4805
- return token === null ? void 0 : { [ROOM_TOKEN_HEADER]: token };
5079
+ return token === null ? void 0 : { [MEETING_PASS_HEADER]: token };
4806
5080
  }
4807
5081
  function refusal2(response, operation, remedy) {
4808
5082
  return refusalFromResponse(response, operation, remedy);
@@ -5549,25 +5823,37 @@ function MeetRuntime(props) {
5549
5823
  api,
5550
5824
  onDiagnostic: report,
5551
5825
  // ADMISSION IS A JOIN, NOT A RE-RENDER. The lobby token grants neither
5552
- // publish nor subscribe, so the host saying yes has to mint a new one.
5826
+ // publish nor subscribe, so the host saying yes has to mint a new one. The
5827
+ // "yes" is read from the server (the state read), never a broadcast (S4).
5553
5828
  onAdmitted: () => {
5554
- void engine.rejoin();
5555
- },
5556
- // A "no" is ALSO a mint: the server refuses it with its own sentence, which
5557
- // turns the waiting room into the pre-join screen showing that sentence.
5558
- onDenied: () => {
5559
5829
  void engine.rejoin().catch(() => void 0);
5830
+ },
5831
+ rejoin: () => engine.rejoin(),
5832
+ // THE MEETING PASS this device holds — what the state read and `/leave` carry.
5833
+ resolvePass: () => {
5834
+ const roomName = store.snapshot().meeting?.roomName;
5835
+ const active = engine.token();
5836
+ if (active?.meetingPass) return active.meetingPass;
5837
+ return roomName === void 0 ? null : tokens.remembered(roomName)?.meetingPass ?? null;
5560
5838
  }
5561
5839
  });
5562
5840
  const resolveRoomToken = () => {
5563
5841
  const roomName = store.snapshot().meeting?.roomName;
5564
5842
  if (roomName === void 0) return null;
5565
5843
  const active = engine.token();
5566
- if (active?.roomName === roomName && active.token.length > 0) return active.token;
5567
- return tokens.remembered(roomName)?.token ?? null;
5844
+ if (active?.roomName === roomName && active.meetingPass) return active.meetingPass;
5845
+ return tokens.remembered(roomName)?.meetingPass ?? null;
5568
5846
  };
5569
5847
  const ai = createMeetAi({ api, store, resolveRoomToken });
5570
5848
  const inRoom = createInRoomClient({ api, store, resolveRoomToken });
5849
+ store.subscribe(() => {
5850
+ const snapshot = store.snapshot();
5851
+ if (snapshot.phase === "lobby" && snapshot.waiting !== null && (snapshot.waiting.admission === "knocking" || snapshot.waiting.admission === "waiting_host")) {
5852
+ meetings.startWaiting();
5853
+ } else if (snapshot.phase !== "lobby") {
5854
+ meetings.stopWaiting();
5855
+ }
5856
+ });
5571
5857
  let chatReadFor = null;
5572
5858
  store.subscribe(() => {
5573
5859
  const snapshot = store.snapshot();
@@ -5736,6 +6022,10 @@ function useLocalUserId() {
5736
6022
  var import_react3 = require("react");
5737
6023
 
5738
6024
  // src/core/observation.ts
6025
+ function observedRole(role, standing) {
6026
+ if (role === "host" || role === "cohost") return role;
6027
+ return isLinkFollower(standing) ? "guest" : "participant";
6028
+ }
5739
6029
  function observedDevice(track) {
5740
6030
  if (track.enabled) return "on";
5741
6031
  const refused = track.unavailable;
@@ -5763,13 +6053,15 @@ function observedPhase(input) {
5763
6053
  const raw = snapshot.phase;
5764
6054
  const live = raw === "connected" || raw === "reconnecting";
5765
6055
  if (endedRecord(snapshot, input.meeting ?? null) && !live) return "ended";
6056
+ const reason = raw === "failed" ? snapshot.failure?.reason ?? null : null;
6057
+ if (reason !== null) return reason === "ended" ? "ended" : `refused:${reason}`;
5766
6058
  switch (raw) {
5767
6059
  case "idle":
5768
6060
  case "failed":
5769
6061
  case "left":
5770
6062
  return "prejoin";
5771
6063
  case "lobby":
5772
- return "waiting:knocking";
6064
+ return waitingPhase(snapshot);
5773
6065
  case "requesting-token":
5774
6066
  case "connecting":
5775
6067
  return "joining";
@@ -5778,20 +6070,39 @@ function observedPhase(input) {
5778
6070
  return "in_call";
5779
6071
  }
5780
6072
  }
6073
+ function waitingPhase(snapshot) {
6074
+ switch (snapshot.waiting?.admission) {
6075
+ case "waiting_host":
6076
+ return "waiting:host_not_started";
6077
+ case "denied":
6078
+ return "denied";
6079
+ case "expired":
6080
+ return "knock_expired";
6081
+ case "withdrawn":
6082
+ return "left";
6083
+ case "ended":
6084
+ return "ended";
6085
+ case "removed":
6086
+ return "refused:removed";
6087
+ default:
6088
+ return "waiting:knocking";
6089
+ }
6090
+ }
5781
6091
  var ADMITTER_ROLES = /* @__PURE__ */ new Set(["host", "cohost"]);
5782
6092
  function observe(input) {
5783
6093
  const { snapshot } = input;
5784
6094
  const phase = observedPhase(input);
5785
6095
  const inCall = phase === "in_call";
5786
- const role = inCall && snapshot !== null ? effectiveRole(snapshot) : "none";
5787
- const canAdmit = role !== "none" && ADMITTER_ROLES.has(role);
6096
+ const actual = inCall && snapshot !== null ? effectiveRole(snapshot) : "none";
6097
+ const canAdmit = actual !== "none" && ADMITTER_ROLES.has(actual);
6098
+ const role = actual === "none" || snapshot === null ? "none" : observedRole(actual, effectiveStanding(snapshot));
5788
6099
  const deviceInPlay = phase === "joining" || inCall;
5789
6100
  return {
5790
6101
  phase,
5791
- reason: null,
6102
+ reason: phase.startsWith("refused:") ? phase.slice("refused:".length) : null,
5792
6103
  role,
5793
6104
  connection: snapshot?.phase === "reconnecting" ? "reconnecting" : "stable",
5794
- hostPresent: inCall && snapshot !== null ? snapshot.participants.some((p) => ADMITTER_ROLES.has(p.role)) : null,
6105
+ hostPresent: inCall && snapshot !== null ? snapshot.participants.some((p) => ADMITTER_ROLES.has(p.role)) : snapshot?.phase === "lobby" ? snapshot.waiting?.admittersPresent ?? null : null,
5795
6106
  lobbyCount: canAdmit && snapshot !== null ? snapshot.lobby.filter((entry) => entry.state === "knocking").length : 0,
5796
6107
  recording: snapshot?.recording === "recording" || snapshot?.recording === "stopping",
5797
6108
  audioBlocked: null,
@@ -5854,7 +6165,7 @@ function participantAttributes(p) {
5854
6165
  "data-meet-participant": "",
5855
6166
  "data-meet-participant-identity": p.identity,
5856
6167
  "data-meet-participant-name": p.displayName,
5857
- "data-meet-participant-role": p.role,
6168
+ "data-meet-participant-role": observedRole(p.role, p.standing),
5858
6169
  "data-meet-participant-self": String(p.isLocal)
5859
6170
  };
5860
6171
  const quality = observedQuality(p.quality);
@@ -6183,7 +6494,7 @@ function summarizeMeetingRecordRestrictions(restrictions) {
6183
6494
  async function loadGuestMeetingRecord(args) {
6184
6495
  const { api, repository, meetingId, meeting, roomToken } = args;
6185
6496
  const session = args.session === true;
6186
- if (roomToken === null && !session) {
6497
+ if (roomToken?.meetingPass == null && !session) {
6187
6498
  return refusedBundle(
6188
6499
  meeting,
6189
6500
  "This meeting's record is only readable by the people who were in it.",
@@ -6193,12 +6504,12 @@ async function loadGuestMeetingRecord(args) {
6193
6504
  let response;
6194
6505
  try {
6195
6506
  response = await api.request({
6196
- path: `${MEET_ROUTES.record}?meeting_id=${encodeURIComponent(meetingId)}`,
6507
+ path: `${MEET_ROUTES.record}?meeting_id=${encodeURIComponent(meetingId)}` + (args.sessionId ? `&session_id=${encodeURIComponent(args.sessionId)}` : ""),
6197
6508
  method: "GET",
6198
6509
  // A GUEST HAS NO SESSION AND IS STILL A FIRST-CLASS READER (D6).
6199
6510
  requireAuth: session,
6200
6511
  operation: "meet.record",
6201
- headers: roomToken === null ? {} : { [ROOM_TOKEN_HEADER]: roomToken.token }
6512
+ headers: roomToken?.meetingPass != null ? { [MEETING_PASS_HEADER]: roomToken.meetingPass } : {}
6202
6513
  });
6203
6514
  } catch (error) {
6204
6515
  return refusedBundle(
@@ -6752,12 +7063,22 @@ function useLobby() {
6752
7063
  async () => requireSession(session, "admitAll").admitAll(),
6753
7064
  [session]
6754
7065
  ),
6755
- knock: (0, import_react4.useCallback)(
6756
- (roomName) => requireSession(session, "knock").knock(roomName),
7066
+ undeny: (0, import_react4.useCallback)(
7067
+ async (entry) => requireSession(session, "undeny").undeny(entry),
6757
7068
  [session]
6758
7069
  )
6759
7070
  };
6760
7071
  }
7072
+ function useWaiting() {
7073
+ const host = useMeetHost();
7074
+ const snapshot = useMeetSnapshot();
7075
+ const session = host?.meetings ?? null;
7076
+ return {
7077
+ waiting: snapshot?.waiting ?? null,
7078
+ withdraw: (0, import_react4.useCallback)(async () => requireSession(session, "withdraw").withdraw(), [session]),
7079
+ askAgain: (0, import_react4.useCallback)(async () => requireSession(session, "askAgain").askAgain(), [session])
7080
+ };
7081
+ }
6761
7082
  function useRecording() {
6762
7083
  const host = useMeetHost();
6763
7084
  const snapshot = useMeetSnapshot();
@@ -7405,7 +7726,6 @@ function kindOf(person) {
7405
7726
  if (person.isAgent) return "assistant";
7406
7727
  if (person.role === "host") return "host";
7407
7728
  if (person.role === "cohost") return "cohost";
7408
- if (person.role === "guest") return person.userId !== null ? "signed-in guest" : "guest";
7409
7729
  if (person.userId === null) return "guest";
7410
7730
  return "member";
7411
7731
  }
@@ -7690,7 +8010,7 @@ function meetingLength(meeting) {
7690
8010
  if (start === null || end === null || end < start) return null;
7691
8011
  return (0, import_format2.formatDurationMs)(end - start, { style: "long" });
7692
8012
  }
7693
- async function loadMeetingChat(api, meetingId, roomToken = null) {
8013
+ async function loadMeetingChat(api, meetingId, meetingPass = null) {
7694
8014
  const operation = "meet.loadMeetingChat";
7695
8015
  let response;
7696
8016
  try {
@@ -7698,8 +8018,8 @@ async function loadMeetingChat(api, meetingId, roomToken = null) {
7698
8018
  path: `${MEET_ROUTES.chat}?meeting_id=${encodeURIComponent(meetingId)}`,
7699
8019
  method: "GET",
7700
8020
  operation,
7701
- requireAuth: roomToken === null,
7702
- ...roomToken !== null ? { headers: { [ROOM_TOKEN_HEADER]: roomToken } } : {}
8021
+ requireAuth: meetingPass === null,
8022
+ ...meetingPass !== null ? { headers: { [MEETING_PASS_HEADER]: meetingPass } } : {}
7703
8023
  });
7704
8024
  } catch (error) {
7705
8025
  return {
@@ -9172,10 +9492,13 @@ function RecordingIndicator() {
9172
9492
  }
9173
9493
  );
9174
9494
  }
9175
- function RoleBadge({ role }) {
9495
+ function RoleBadge({
9496
+ role,
9497
+ standing
9498
+ }) {
9176
9499
  if (role === "host") return /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("span", { className: "mx-meet__badge", children: "Host" });
9177
9500
  if (role === "cohost") return /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("span", { className: "mx-meet__badge", children: "Co-host" });
9178
- if (role === "guest") return /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("span", { className: "mx-meet__badge", children: "Guest" });
9501
+ if (isLinkFollower(standing)) return /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("span", { className: "mx-meet__badge", children: "Guest" });
9179
9502
  return null;
9180
9503
  }
9181
9504
  function ParticipantTile({
@@ -9225,7 +9548,7 @@ function ParticipantTile({
9225
9548
  participant.displayName,
9226
9549
  participant.isLocal ? " (you)" : "",
9227
9550
  participant.isAgent ? /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("span", { className: "mx-meet__badge", children: "AI" }) : null,
9228
- /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(RoleBadge, { role: participant.role })
9551
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(RoleBadge, { role: participant.role, standing: participant.standing })
9229
9552
  ] }),
9230
9553
  !participant.micEnabled ? /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(MicOffIcon, { width: 14, height: 14 }) : null,
9231
9554
  /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(QualityBars, { quality: participant.quality })
@@ -9964,7 +10287,13 @@ function PeoplePanel({
9964
10287
  /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("span", { className: "mx-meet__person-name", children: [
9965
10288
  participant.displayName,
9966
10289
  participant.identity === local ? /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("span", { className: "mx-meet__you", children: " (you)" }) : null,
9967
- /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(RoleBadge, { role: participant.identity === local ? myRole : participant.role }),
10290
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
10291
+ RoleBadge,
10292
+ {
10293
+ role: participant.identity === local ? myRole : participant.role,
10294
+ standing: participant.standing
10295
+ }
10296
+ ),
9968
10297
  isNoteTaker(participant) ? /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("span", { className: "mx-meet__badge", children: "Meeting assistant" }) : null,
9969
10298
  snapshot?.spotlight === participant.identity ? /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("span", { className: "mx-meet__badge", children: "Spotlight" }) : null
9970
10299
  ] }),
@@ -10880,7 +11209,7 @@ function MeetSkinRoom({
10880
11209
  ) });
10881
11210
  }
10882
11211
  if (phase === "lobby") {
10883
- return /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("div", { className: "mx-meet mx-meet--lobby", children: /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(WaitingRoom, { roomName, host }) });
11212
+ return /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("div", { className: "mx-meet mx-meet--lobby", children: /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(WaitingRoom, { host, ...onLeave !== void 0 ? { onLeave } : {} }) });
10884
11213
  }
10885
11214
  return /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("div", { className: "mx-meet", children: [
10886
11215
  /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(ConsentNotice, {}),
@@ -10905,21 +11234,58 @@ function MeetSkinRoom({
10905
11234
  /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(ControlBar, { ...onLeave !== void 0 ? { onLeave } : {} })
10906
11235
  ] });
10907
11236
  }
10908
- function WaitingRoom({ roomName, host }) {
10909
- const knocked = (0, import_react9.useRef)(false);
10910
- (0, import_react9.useEffect)(() => {
10911
- if (knocked.current) return;
10912
- knocked.current = true;
10913
- host.meetings.knock(roomName);
10914
- }, [host, roomName]);
11237
+ function WaitingRoom({ host, onLeave }) {
11238
+ const { waiting, withdraw, askAgain } = useWaiting();
11239
+ const [busy, setBusy] = (0, import_react9.useState)(false);
11240
+ const admission = waiting?.admission ?? "knocking";
11241
+ const run = (action) => () => {
11242
+ setBusy(true);
11243
+ void action().catch(() => void 0).finally(() => setBusy(false));
11244
+ };
11245
+ const home = /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(import_design_system.Button, { type: "button", variant: "outline", onClick: () => onLeave !== void 0 ? onLeave() : window.location.assign("/"), children: "Back to home" });
11246
+ let title;
11247
+ let line = null;
11248
+ let actions;
11249
+ switch (admission) {
11250
+ case "waiting_host":
11251
+ title = "Waiting for the host to start";
11252
+ actions = /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(import_design_system.Button, { type: "button", variant: "outline", disabled: busy, onClick: run(withdraw), children: "Leave" });
11253
+ break;
11254
+ case "denied":
11255
+ title = "The host declined your request";
11256
+ actions = /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)(import_jsx_runtime5.Fragment, { children: [
11257
+ waiting?.reknockAllowed === true ? /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(import_design_system.Button, { type: "button", disabled: busy, onClick: run(askAgain), children: "Ask to join again" }) : null,
11258
+ home
11259
+ ] });
11260
+ break;
11261
+ case "expired":
11262
+ title = "No one responded";
11263
+ actions = /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)(import_jsx_runtime5.Fragment, { children: [
11264
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(import_design_system.Button, { type: "button", disabled: busy, onClick: run(askAgain), children: "Ask again" }),
11265
+ home
11266
+ ] });
11267
+ break;
11268
+ case "withdrawn":
11269
+ title = "You left the waiting room";
11270
+ actions = /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)(import_jsx_runtime5.Fragment, { children: [
11271
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(import_design_system.Button, { type: "button", disabled: busy, onClick: run(askAgain), children: "Rejoin" }),
11272
+ home
11273
+ ] });
11274
+ break;
11275
+ case "ended":
11276
+ title = "The meeting has ended";
11277
+ actions = home;
11278
+ break;
11279
+ default:
11280
+ title = "Waiting for the host to let you in";
11281
+ line = waiting?.admittersPresent === false ? "The host isn't here yet" : null;
11282
+ actions = /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(import_design_system.Button, { type: "button", variant: "outline", disabled: busy, onClick: run(withdraw), children: "Cancel request" });
11283
+ }
10915
11284
  return /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("div", { className: "mx-meet__waiting", children: [
10916
11285
  /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(Avatar, { name: host.identity.displayName, url: host.identity.avatarUrl, size: 72 }),
10917
- /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("h1", { className: "mx-meet__prejoin-title", children: "Waiting for the host to let you in" }),
10918
- /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("p", { className: "mx-meet__empty-body", children: [
10919
- "You are in the waiting room as ",
10920
- host.identity.displayName,
10921
- ". You will join automatically once you are admitted."
10922
- ] })
11286
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("h1", { className: "mx-meet__prejoin-title", children: title }),
11287
+ line !== null ? /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("p", { className: "mx-meet__empty-body", role: "status", "data-meet-notice": "host-absent", children: line }) : null,
11288
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("div", { className: "mx-meet__waiting-actions", children: actions })
10923
11289
  ] });
10924
11290
  }
10925
11291
  function HostMenu() {
@@ -11126,7 +11492,7 @@ function MeetingRecordBody({
11126
11492
  /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("span", { className: "mx-meet__person-name", children: [
11127
11493
  displayNameFor(bundle.names, person.identity, person.displayName),
11128
11494
  person.role === "host" ? /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("span", { className: "mx-meet__badge", children: "Host" }) : null,
11129
- person.role === "guest" ? /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("span", { className: "mx-meet__badge", children: "Guest" }) : null,
11495
+ person.userId === null && !person.isAgent ? /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("span", { className: "mx-meet__badge", children: "Guest" }) : null,
11130
11496
  person.isAgent ? /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("span", { className: "mx-meet__badge", children: "Meeting assistant" }) : null
11131
11497
  ] })
11132
11498
  ] }, person.id)) })