@ai-matrx/meet 0.9.0 → 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
@@ -592,7 +592,17 @@ var MEET_ROUTES = {
592
592
  */
593
593
  collab: "/api/v1/meet/collab",
594
594
  /** Meet wave 5: translate finished caption lines into this viewer's language. */
595
- 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"
596
606
  };
597
607
  function meetingPath(template, meetingId) {
598
608
  return template.replace("{meeting_id}", encodeURIComponent(meetingId));
@@ -666,6 +676,8 @@ function parseRoomToken(body, roomName) {
666
676
  meetingPassExpiresAt: readString(payload, "meeting_pass_expires_at"),
667
677
  sessionId: readString(payload, "session_id"),
668
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,
669
681
  breakoutRoomId: readString(payload, "breakout_room_id") ?? null
670
682
  };
671
683
  }
@@ -1253,14 +1265,15 @@ var MEET_EVENTS = {
1253
1265
  cancel: "mx.call.cancel",
1254
1266
  /** You answered (or declined) on another device. Stop ringing here. */
1255
1267
  settled: "mx.call.settled",
1256
- /** Someone is in the lobby. Hosts render the knock. */
1257
- knock: "mx.lobby.knock",
1258
- /** A host decided. The knocker learns it here — they have no DB read. */
1259
- admission: "mx.lobby.admission",
1260
- /** The room was locked or unlocked. */
1261
- lock: "mx.room.lock",
1262
- /** Recording started or stopped. EVERY participant renders the indicator. */
1263
- 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"
1264
1277
  };
1265
1278
 
1266
1279
  // src/core/types.ts
@@ -1286,6 +1299,7 @@ function isLinkFollower(standing) {
1286
1299
  function isHostRole(role) {
1287
1300
  return role === "host" || role === "cohost";
1288
1301
  }
1302
+ var WAITING_ADMISSION_STATES = ["waiting_host", "knocking"];
1289
1303
 
1290
1304
  // src/core/calls.ts
1291
1305
  var TRANSITIONS = {
@@ -2301,6 +2315,7 @@ var NO_GRANTS = {
2301
2315
  };
2302
2316
  var EMPTY = {
2303
2317
  phase: "idle",
2318
+ waiting: null,
2304
2319
  meeting: null,
2305
2320
  localIdentity: null,
2306
2321
  grants: NO_GRANTS,
@@ -2557,19 +2572,25 @@ function roleOf(participants, identity) {
2557
2572
  return participants.find((p) => p.identity === identity)?.role ?? "participant";
2558
2573
  }
2559
2574
  var END_MEETING_NOT_SERVED = "Ending a meeting is not available on this server yet";
2560
- function lobbyEntryFrom(attendee) {
2561
- 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;
2562
2579
  return {
2563
2580
  identity: attendee.identity,
2564
2581
  displayName: attendee.displayName,
2565
2582
  avatarUrl: attendee.avatarUrl,
2566
2583
  userId: attendee.userId,
2567
2584
  state: "knocking",
2568
- knockedAt: attendee.joinedAt ?? (/* @__PURE__ */ new Date()).toISOString(),
2585
+ knockedAt: attendee.knockedAt ?? attendee.joinedAt ?? (/* @__PURE__ */ new Date()).toISOString(),
2586
+ knockExpiresAt: attendee.knockExpiresAt ?? null,
2569
2587
  decidedAt: null,
2570
2588
  decidedBy: null
2571
2589
  };
2572
2590
  }
2591
+ function decidedState(state) {
2592
+ return WAITING_ADMISSION_STATES.includes(state) ? "expired" : state;
2593
+ }
2573
2594
  function createMeetingSession(options) {
2574
2595
  const { manager, repository, store, identity } = options;
2575
2596
  let channel = null;
@@ -2593,11 +2614,6 @@ function createMeetingSession(options) {
2593
2614
  }
2594
2615
  return identity.userId;
2595
2616
  }
2596
- function knockIdentity() {
2597
- return asParticipantIdentity(
2598
- identity.userId !== null ? `user:${identity.userId}` : `guest:${identity.deviceId}`
2599
- );
2600
- }
2601
2617
  function requireMeeting(operation) {
2602
2618
  const meeting = store.snapshot().meeting;
2603
2619
  if (meeting === null) {
@@ -2609,6 +2625,191 @@ function createMeetingSession(options) {
2609
2625
  }
2610
2626
  return meeting;
2611
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
+ }
2612
2813
  return {
2613
2814
  watch(roomName) {
2614
2815
  channel?.close();
@@ -2616,58 +2817,22 @@ function createMeetingSession(options) {
2616
2817
  topic: roomTopic(roomName),
2617
2818
  broadcast: [
2618
2819
  {
2619
- event: MEET_EVENTS.knock,
2620
- onMessage: (message) => {
2621
- store.upsertLobby(message.data);
2622
- }
2623
- },
2624
- {
2625
- event: MEET_EVENTS.admission,
2626
- onMessage: (message) => {
2627
- const verdict = message.data;
2628
- const decided = asParticipantIdentity(verdict.identity);
2629
- store.setAdmission(
2630
- decided,
2631
- verdict.admitted ? "admitted" : "denied",
2632
- verdict.by === null ? null : verdict.by
2633
- );
2634
- if (decided === knockIdentity()) {
2635
- if (verdict.admitted) options.onAdmitted?.();
2636
- else options.onDenied?.();
2637
- }
2638
- }
2639
- },
2640
- {
2641
- event: MEET_EVENTS.lock,
2642
- onMessage: (message) => {
2643
- const { locked } = message.data;
2644
- const meeting = store.snapshot().meeting;
2645
- store.patch({
2646
- locked,
2647
- ...meeting !== null ? { meeting: { ...meeting, locked } } : {}
2648
- });
2649
- }
2650
- },
2651
- {
2652
- event: MEET_EVENTS.recording,
2653
- onMessage: (message) => {
2654
- const payload = message.data;
2655
- store.setRecording(
2656
- payload.state,
2657
- payload.by === null ? null : payload.by,
2658
- "room"
2659
- );
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();
2660
2825
  }
2661
2826
  }
2662
2827
  ],
2663
- // THE BACKFILL DOOR. A host whose socket blipped must not miss a knock:
2664
- // re-reading the meeting restores lock/recording truth, and knockers
2665
- // 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.
2666
2830
  onBackfill: async () => {
2667
2831
  const meeting = store.snapshot().meeting;
2668
2832
  if (meeting === null) return;
2669
2833
  const fresh = isGuest(identity) ? await repository.meetingBySlug(meeting.slug) : await readMeeting(meeting.id);
2670
2834
  store.patch({ meeting: fresh, locked: fresh.locked });
2835
+ if (store.snapshot().phase === "lobby") void readState2();
2671
2836
  }
2672
2837
  });
2673
2838
  },
@@ -2728,7 +2893,7 @@ function createMeetingSession(options) {
2728
2893
  store.upsertLobby(entry);
2729
2894
  return;
2730
2895
  }
2731
- store.setAdmission(attendee.identity, attendee.admissionState, null);
2896
+ store.setAdmission(attendee.identity, decidedState(attendee.admissionState), null);
2732
2897
  }
2733
2898
  },
2734
2899
  {
@@ -2780,62 +2945,75 @@ function createMeetingSession(options) {
2780
2945
  if (entry !== null) {
2781
2946
  store.upsertLobby(entry);
2782
2947
  } else {
2783
- store.setAdmission(attendee.identity, attendee.admissionState, null);
2948
+ store.setAdmission(attendee.identity, decidedState(attendee.admissionState), null);
2784
2949
  }
2785
2950
  }
2786
2951
  }
2787
2952
  });
2788
2953
  meetingChannel.requestBackfill("manual");
2789
2954
  },
2790
- knock(roomName) {
2791
- const handle = channel ?? manager.open({ topic: roomTopic(roomName) });
2792
- channel = handle;
2793
- const entry = {
2794
- identity: knockIdentity(),
2795
- displayName: identity.displayName,
2796
- avatarUrl: identity.avatarUrl,
2797
- userId: identity.userId,
2798
- state: "knocking",
2799
- knockedAt: (/* @__PURE__ */ new Date()).toISOString(),
2800
- decidedAt: null,
2801
- decidedBy: null
2802
- };
2803
- store.upsertLobby(entry);
2804
- handle.send(MEET_EVENTS.knock, entry);
2805
- },
2806
2955
  async admit(entry, admitted) {
2807
2956
  const by = requireHostUser("meet.admit");
2808
2957
  const meeting = requireMeeting("meet.admit");
2809
- await repository.admit({
2958
+ const row = await repository.decideAdmission({
2810
2959
  meetingId: meeting.id,
2811
2960
  identity: entry.identity,
2812
- admitted,
2961
+ decision: admitted ? "admit" : "deny",
2813
2962
  byUserId: by
2814
2963
  });
2815
2964
  store.upsertLobby({
2816
2965
  ...entry,
2817
- state: admitted ? "admitted" : "denied",
2966
+ state: decidedState(row.admissionState),
2818
2967
  decidedAt: (/* @__PURE__ */ new Date()).toISOString(),
2819
2968
  decidedBy: by
2820
2969
  });
2821
- 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,
2822
2977
  identity: entry.identity,
2823
- admitted,
2824
- by
2978
+ decision: "undeny",
2979
+ byUserId: by
2825
2980
  });
2981
+ store.upsertLobby({ ...entry, state: decidedState(row.admissionState), decidedAt: (/* @__PURE__ */ new Date()).toISOString(), decidedBy: by });
2982
+ hint();
2826
2983
  },
2827
2984
  async admitAll() {
2828
- const knocking = store.snapshot().lobby.filter((entry) => entry.state === "knocking");
2829
- for (const entry of knocking) {
2830
- await this.admit(entry, true);
2831
- }
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?.();
2832
3010
  },
2833
3011
  async setLock(locked) {
2834
3012
  const by = requireHostUser("meet.setLock");
2835
3013
  const meeting = requireMeeting("meet.setLock");
2836
3014
  const updated = await repository.setLock(meeting.id, locked, by);
2837
3015
  store.patch({ meeting: updated, locked: updated.locked });
2838
- channel?.send(MEET_EVENTS.lock, { locked: updated.locked });
3016
+ hint();
2839
3017
  return updated;
2840
3018
  },
2841
3019
  /**
@@ -2893,6 +3071,7 @@ function createMeetingSession(options) {
2893
3071
  return { endedAt, served: true };
2894
3072
  },
2895
3073
  dispose() {
3074
+ stopWaiting();
2896
3075
  channel?.close();
2897
3076
  channel = null;
2898
3077
  meetingChannel?.close();
@@ -2937,7 +3116,14 @@ var RPCS = {
2937
3116
  getOrCreateMeeting: "meet_get_or_create_meeting",
2938
3117
  /** Resolve a durable public link, before/during/after its scheduled time. */
2939
3118
  meetingBySlug: "meet_meeting_by_slug",
2940
- 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",
2941
3127
  setLock: "meet_set_lock",
2942
3128
  recordConsent: "meet_record_consent",
2943
3129
  /*
@@ -2980,13 +3166,13 @@ var RPCS = {
2980
3166
  /** Titles, agendas, current notes and final transcript lines; snippets mark hits with ⟦ ⟧. */
2981
3167
  search: "meet_search"
2982
3168
  };
2983
- var VOID_RPCS = [RPCS.admit, RPCS.recordConsent];
3169
+ var VOID_RPCS = [RPCS.recordConsent];
2984
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";
2985
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";
2986
3172
  var RECORDING_COLUMNS = "id,meeting_id,state,started_at,ended_at,started_by,file_id,failure_reason";
2987
3173
  var SEGMENT_COLUMNS = "id,meeting_id,identity,speaker,text,started_at,is_final,metadata";
2988
3174
  var NOTE_COLUMNS = "id,meeting_id,text,kind,assignee_user_id,created_at,metadata";
2989
- 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";
2990
3176
  function projectStays(value) {
2991
3177
  if (!Array.isArray(value)) return [];
2992
3178
  const stays = [];
@@ -3353,7 +3539,16 @@ function createMeetRepository(options) {
3353
3539
  const role = text(row, "role");
3354
3540
  const admission = text(row, "admission_state");
3355
3541
  const known = ["host", "cohost", "presenter", "participant", "viewer"];
3356
- const states = ["knocking", "admitted", "denied", "removed"];
3542
+ const states = [
3543
+ "waiting_host",
3544
+ "knocking",
3545
+ "admitted",
3546
+ "denied",
3547
+ "expired",
3548
+ "withdrawn",
3549
+ "removed",
3550
+ "ended"
3551
+ ];
3357
3552
  const userId = text(row, "participant_user_id");
3358
3553
  const identity = requireText(row, "identity", operation);
3359
3554
  return {
@@ -3367,6 +3562,8 @@ function createMeetRepository(options) {
3367
3562
  userId: userId === null ? null : asUserId(userId),
3368
3563
  role: known.includes(role) ? role : "participant",
3369
3564
  admissionState: states.includes(admission) ? admission : "admitted",
3565
+ knockedAt: text(row, "knocked_at"),
3566
+ knockExpiresAt: text(row, "knock_expires_at"),
3370
3567
  isAgent: bool(row, "is_agent", false),
3371
3568
  joinedAt: text(row, "joined_at"),
3372
3569
  leftAt: text(row, "left_at"),
@@ -3645,16 +3842,26 @@ function createMeetRepository(options) {
3645
3842
  hasMore: rows.length > limit
3646
3843
  };
3647
3844
  },
3648
- async admit({ meetingId, identity, admitted, byUserId }) {
3649
- await guardVoid(
3650
- "meet.admit",
3651
- () => 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, {
3652
3850
  p_meeting_id: meetingId,
3653
3851
  p_identity: identity,
3654
- p_admitted: admitted,
3655
3852
  p_by_user_id: byUserId
3656
3853
  })
3657
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);
3658
3865
  },
3659
3866
  async setLock(meetingId, locked, byUserId) {
3660
3867
  const row = await guard(
@@ -4499,7 +4706,7 @@ function createRoomEngine(options) {
4499
4706
  }
4500
4707
  };
4501
4708
  async function connect(request) {
4502
- store.patch({ phase: "requesting-token", failure: null });
4709
+ store.patch({ phase: "requesting-token", failure: null, waiting: null });
4503
4710
  const token = await tokens.get({
4504
4711
  roomName: request.roomName,
4505
4712
  identity,
@@ -4519,11 +4726,21 @@ function createRoomEngine(options) {
4519
4726
  canSubscribe: token.canSubscribe
4520
4727
  }
4521
4728
  });
4522
- if (token.lobby) {
4523
- 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
+ });
4524
4741
  return;
4525
4742
  }
4526
- store.patch({ phase: "connecting" });
4743
+ store.patch({ phase: "connecting", waiting: null });
4527
4744
  const created = await options.driverFactory(handlers);
4528
4745
  if (disposed) {
4529
4746
  await created.disconnect();
@@ -5606,14 +5823,18 @@ function MeetRuntime(props) {
5606
5823
  api,
5607
5824
  onDiagnostic: report,
5608
5825
  // ADMISSION IS A JOIN, NOT A RE-RENDER. The lobby token grants neither
5609
- // 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).
5610
5828
  onAdmitted: () => {
5611
- void engine.rejoin();
5612
- },
5613
- // A "no" is ALSO a mint: the server refuses it with its own sentence, which
5614
- // turns the waiting room into the pre-join screen showing that sentence.
5615
- onDenied: () => {
5616
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;
5617
5838
  }
5618
5839
  });
5619
5840
  const resolveRoomToken = () => {
@@ -5625,6 +5846,14 @@ function MeetRuntime(props) {
5625
5846
  };
5626
5847
  const ai = createMeetAi({ api, store, resolveRoomToken });
5627
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
+ });
5628
5857
  let chatReadFor = null;
5629
5858
  store.subscribe(() => {
5630
5859
  const snapshot = store.snapshot();
@@ -5832,7 +6061,7 @@ function observedPhase(input) {
5832
6061
  case "left":
5833
6062
  return "prejoin";
5834
6063
  case "lobby":
5835
- return "waiting:knocking";
6064
+ return waitingPhase(snapshot);
5836
6065
  case "requesting-token":
5837
6066
  case "connecting":
5838
6067
  return "joining";
@@ -5841,6 +6070,24 @@ function observedPhase(input) {
5841
6070
  return "in_call";
5842
6071
  }
5843
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
+ }
5844
6091
  var ADMITTER_ROLES = /* @__PURE__ */ new Set(["host", "cohost"]);
5845
6092
  function observe(input) {
5846
6093
  const { snapshot } = input;
@@ -5855,7 +6102,7 @@ function observe(input) {
5855
6102
  reason: phase.startsWith("refused:") ? phase.slice("refused:".length) : null,
5856
6103
  role,
5857
6104
  connection: snapshot?.phase === "reconnecting" ? "reconnecting" : "stable",
5858
- 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,
5859
6106
  lobbyCount: canAdmit && snapshot !== null ? snapshot.lobby.filter((entry) => entry.state === "knocking").length : 0,
5860
6107
  recording: snapshot?.recording === "recording" || snapshot?.recording === "stopping",
5861
6108
  audioBlocked: null,
@@ -6816,12 +7063,22 @@ function useLobby() {
6816
7063
  async () => requireSession(session, "admitAll").admitAll(),
6817
7064
  [session]
6818
7065
  ),
6819
- knock: (0, import_react4.useCallback)(
6820
- (roomName) => requireSession(session, "knock").knock(roomName),
7066
+ undeny: (0, import_react4.useCallback)(
7067
+ async (entry) => requireSession(session, "undeny").undeny(entry),
6821
7068
  [session]
6822
7069
  )
6823
7070
  };
6824
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
+ }
6825
7082
  function useRecording() {
6826
7083
  const host = useMeetHost();
6827
7084
  const snapshot = useMeetSnapshot();
@@ -10952,7 +11209,7 @@ function MeetSkinRoom({
10952
11209
  ) });
10953
11210
  }
10954
11211
  if (phase === "lobby") {
10955
- 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 } : {} }) });
10956
11213
  }
10957
11214
  return /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("div", { className: "mx-meet", children: [
10958
11215
  /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(ConsentNotice, {}),
@@ -10977,21 +11234,58 @@ function MeetSkinRoom({
10977
11234
  /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(ControlBar, { ...onLeave !== void 0 ? { onLeave } : {} })
10978
11235
  ] });
10979
11236
  }
10980
- function WaitingRoom({ roomName, host }) {
10981
- const knocked = (0, import_react9.useRef)(false);
10982
- (0, import_react9.useEffect)(() => {
10983
- if (knocked.current) return;
10984
- knocked.current = true;
10985
- host.meetings.knock(roomName);
10986
- }, [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
+ }
10987
11284
  return /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("div", { className: "mx-meet__waiting", children: [
10988
11285
  /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(Avatar, { name: host.identity.displayName, url: host.identity.avatarUrl, size: 72 }),
10989
- /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("h1", { className: "mx-meet__prejoin-title", children: "Waiting for the host to let you in" }),
10990
- /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("p", { className: "mx-meet__empty-body", children: [
10991
- "You are in the waiting room as ",
10992
- host.identity.displayName,
10993
- ". You will join automatically once you are admitted."
10994
- ] })
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 })
10995
11289
  ] });
10996
11290
  }
10997
11291
  function HostMenu() {