@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.js CHANGED
@@ -28,11 +28,18 @@ var MeetError = class extends Error {
28
28
  /** What to DO. Rendered to the user for the device codes. */
29
29
  remedy;
30
30
  cause;
31
- constructor(code, message, remedy, cause) {
31
+ /**
32
+ * The join gate's reason code (CORE-DESIGN §2.5) when the server named one: `not_found`,
33
+ * `cancelled`, `ended`, `locked`, `removed`, `lobby_rejects`, `not_invited`… The phase
34
+ * renders `refused:<reason>` (or `ended`) from it — never from the HTTP status.
35
+ */
36
+ reason;
37
+ constructor(code, message, remedy, cause, reason) {
32
38
  super(message);
33
39
  this.name = "MeetError";
34
40
  this.code = code;
35
41
  this.remedy = remedy;
42
+ this.reason = reason ?? null;
36
43
  if (cause !== void 0) this.cause = cause;
37
44
  }
38
45
  /** True when retrying after the host re-establishes a session is the fix. */
@@ -250,6 +257,17 @@ function createWebRoomTokenStorage(onDiagnostic) {
250
257
  }
251
258
  };
252
259
  }
260
+ function rememberedMeetingPass(roomName, storage = createWebRoomTokenStorage()) {
261
+ const raw = storage.read(roomName);
262
+ if (raw === null) return null;
263
+ try {
264
+ const parsed = JSON.parse(raw);
265
+ if (parsed.standing !== "guest") return null;
266
+ return typeof parsed.meetingPass === "string" && parsed.meetingPass.length > 0 ? parsed.meetingPass : null;
267
+ } catch {
268
+ return null;
269
+ }
270
+ }
253
271
  var MEET_ROUTES = {
254
272
  token: "/api/v1/meet/token",
255
273
  recordingStart: "/api/v1/meet/recording/start",
@@ -263,7 +281,7 @@ var MEET_ROUTES = {
263
281
  /**
264
282
  * THE POST-MEETING RECORD FOR A READER WITH NO DATABASE (MRF-1). A `GET`,
265
283
  * authorized by the room token this device already holds, carried in the
266
- * `x-meet-room-token` header — never a query parameter, because a bearer
284
+ * `x-meet-pass` header — never a query parameter, because a bearer
267
285
  * credential in a URL lands in access logs, referrers and history.
268
286
  */
269
287
  record: "/api/v1/meet/record",
@@ -271,7 +289,7 @@ var MEET_ROUTES = {
271
289
  recordAccess: "/api/v1/meet/record/access",
272
290
  /**
273
291
  * KEEP YOUR NOTES (2026-10-01): a guest who just created an account presents
274
- * their kept pass in `x-meet-room-token` with their new session, and the
292
+ * their kept pass in `x-meet-pass` with their new session, and the
275
293
  * meeting they attended becomes theirs to read. Idempotent; never a grant.
276
294
  */
277
295
  claim: "/api/v1/meet/claim",
@@ -293,7 +311,7 @@ var MEET_ROUTES = {
293
311
  * Meet wave 2: EVERY in-meeting host power (mute, stop video, mute all, ask to
294
312
  * unmute, spotlight, lower hands, co-host, hand over, rename, remove, report).
295
313
  * Decided by the server from the meeting itself; a guest's pass rides
296
- * `x-meet-room-token`.
314
+ * `x-meet-pass`.
297
315
  */
298
316
  moderate: "/api/v1/meet/moderate",
299
317
  /** Meet wave 2: the SAVED chat — `POST` sends to everyone, `GET ?meeting_id=` reads history. */
@@ -304,12 +322,26 @@ var MEET_ROUTES = {
304
322
  */
305
323
  collab: "/api/v1/meet/collab",
306
324
  /** Meet wave 5: translate finished caption lines into this viewer's language. */
307
- captionsTranslate: "/api/v1/meet/captions/translate"
325
+ captionsTranslate: "/api/v1/meet/captions/translate",
326
+ /**
327
+ * S4 — THE STATE READ (CORE-DESIGN §2.6): `GET ?meeting_id=` with `x-meet-pass`. This
328
+ * person's admission (lazy expiry applied), whether anyone who can admit is here, the run's
329
+ * phase. Read every `waiting_poll_seconds` while waiting and on every `mx.state.changed`.
330
+ */
331
+ state: "/api/v1/meet/state",
332
+ /** S4: `withdraw` (stop waiting) or `leave` (close this tab); the pass may ride in the body. */
333
+ leave: "/api/v1/meet/leave",
334
+ /** S4: a pass holder (guests included) saw the recording notice, for this run. */
335
+ consent: "/api/v1/meet/consent"
308
336
  };
309
337
  function meetingPath(template, meetingId) {
310
338
  return template.replace("{meeting_id}", encodeURIComponent(meetingId));
311
339
  }
312
- var ROOM_TOKEN_HEADER = "x-meet-room-token";
340
+ var MEETING_PASS_HEADER = "x-meet-pass";
341
+ function meetingPassHeaders(token) {
342
+ const pass = token?.meetingPass ?? null;
343
+ return pass !== null && pass.length > 0 ? { [MEETING_PASS_HEADER]: pass } : {};
344
+ }
313
345
  var REFRESH_RATIO = 0.8;
314
346
  function readString(source, key) {
315
347
  const value = source[key];
@@ -319,7 +351,12 @@ function readBoolean(source, key, fallback) {
319
351
  const value = source[key];
320
352
  return typeof value === "boolean" ? value : fallback;
321
353
  }
322
- var TOKEN_ROLES = ["host", "cohost", "participant", "guest"];
354
+ var TOKEN_STANDINGS = ["member", "invited", "outsider", "guest"];
355
+ function readStanding(source, key) {
356
+ const value = source[key];
357
+ return TOKEN_STANDINGS.includes(value) ? value : null;
358
+ }
359
+ var TOKEN_ROLES = ["host", "cohost", "presenter", "participant", "viewer"];
323
360
  function readRole(source, key) {
324
361
  const value = source[key];
325
362
  return TOKEN_ROLES.includes(value) ? value : null;
@@ -362,8 +399,15 @@ function parseRoomToken(body, roomName) {
362
399
  // Deriving the role from what DID arrive keeps a host a host against a
363
400
  // server that has not landed MRI-A11 — and never invents a host from
364
401
  // silence, because both fallbacks below grant nothing.
365
- role: readRole(payload, "role") ?? (isHost ? "host" : identity.startsWith("guest:") ? "guest" : "participant"),
402
+ // A word this package does not know grants nothing: `participant`, never a host.
403
+ role: readRole(payload, "role") ?? (isHost ? "host" : "participant"),
404
+ standing: readStanding(payload, "standing"),
405
+ meetingPass: readString(payload, "meeting_pass"),
406
+ meetingPassExpiresAt: readString(payload, "meeting_pass_expires_at"),
407
+ sessionId: readString(payload, "session_id"),
366
408
  lobby: readBoolean(payload, "lobby", false),
409
+ admissionState: readString(payload, "admission_state") ?? void 0,
410
+ pollSeconds: typeof payload["poll_seconds"] === "number" ? payload["poll_seconds"] : void 0,
367
411
  breakoutRoomId: readString(payload, "breakout_room_id") ?? null
368
412
  };
369
413
  }
@@ -373,7 +417,7 @@ function createTokenClient(options) {
373
417
  const storage = options.storage;
374
418
  function remember(token) {
375
419
  if (storage === void 0) return;
376
- if (!token.identity.startsWith("guest:")) return;
420
+ if (token.standing !== "guest") return;
377
421
  storage.write(token.roomName, JSON.stringify(token));
378
422
  }
379
423
  function recall(roomName) {
@@ -431,22 +475,28 @@ function createTokenClient(options) {
431
475
  throw new MeetError(
432
476
  "forbidden",
433
477
  said?.message ?? "You can't join this meeting.",
434
- said?.remedy ?? "Ask the person who shared the link to let you in, then open the link again."
478
+ said?.remedy ?? "Ask the person who shared the link to let you in, then open the link again.",
479
+ void 0,
480
+ said?.reason ?? null
435
481
  );
436
482
  }
437
483
  if (response.status === 404) {
438
484
  throw new MeetError(
439
485
  "not-found",
440
486
  "meet.token: no such meeting",
441
- "The link is wrong or the meeting was deleted. Show the 'meeting not found' screen; a durable link that 404s is never a retry."
487
+ "The link is wrong or the meeting was deleted. Show the 'meeting not found' screen; a durable link that 404s is never a retry.",
488
+ void 0,
489
+ "not_found"
442
490
  );
443
491
  }
444
492
  if (response.status === 410) {
445
- const reason = (await serverSentence(response))?.message ?? "This meeting was cancelled.";
493
+ const said = await serverSentence(response);
446
494
  throw new MeetError(
447
495
  "cancelled",
448
- reason,
449
- "Nobody can join a cancelled meeting. Show the cancelled screen with this sentence; ask the host to schedule a new one."
496
+ said?.message ?? "This meeting was cancelled.",
497
+ "Nobody can join a cancelled meeting. Show the cancelled screen with this sentence; ask the host to schedule a new one.",
498
+ void 0,
499
+ said?.reason ?? "cancelled"
450
500
  );
451
501
  }
452
502
  if (response.status === 423) {
@@ -454,7 +504,9 @@ function createTokenClient(options) {
454
504
  throw new MeetError(
455
505
  "room-locked",
456
506
  said?.message ?? "The meeting is locked.",
457
- said?.remedy ?? "The host locked the room. Ask them to unlock it, then join again."
507
+ said?.remedy ?? "The host locked the room. Ask them to unlock it, then join again.",
508
+ void 0,
509
+ said?.reason ?? "locked"
458
510
  );
459
511
  }
460
512
  if (response.status === 503) {
@@ -523,6 +575,10 @@ function placeholder(roomName) {
523
575
  canRecord: false,
524
576
  isHost: false,
525
577
  role: "participant",
578
+ standing: null,
579
+ meetingPass: null,
580
+ meetingPassExpiresAt: null,
581
+ sessionId: null,
526
582
  lobby: false,
527
583
  breakoutRoomId: null
528
584
  };
@@ -535,7 +591,8 @@ async function serverSentence(response) {
535
591
  const message = typeof source.user_message === "string" && source.user_message.trim() !== "" ? source.user_message : typeof source.message === "string" && source.message.trim() !== "" ? source.message : null;
536
592
  if (message === null) return null;
537
593
  const remedy = typeof source.remedy === "string" && source.remedy.trim() !== "" ? source.remedy : void 0;
538
- return { message, remedy };
594
+ const reason = typeof source.reason === "string" && source.reason !== "" ? source.reason : null;
595
+ return { message, remedy, reason };
539
596
  } catch {
540
597
  return null;
541
598
  }
@@ -724,7 +781,7 @@ function createMeetAi(options) {
724
781
  operation: "meet.ai.ask",
725
782
  // A GUEST MAY ASK. The server decides whether it can answer.
726
783
  requireAuth: false,
727
- ...roomToken !== null ? { headers: { [ROOM_TOKEN_HEADER]: roomToken } } : {},
784
+ ...roomToken !== null ? { headers: { [MEETING_PASS_HEADER]: roomToken } } : {},
728
785
  ...signal !== void 0 ? { signal } : {}
729
786
  });
730
787
  if (!response.ok) {
@@ -938,14 +995,15 @@ var MEET_EVENTS = {
938
995
  cancel: "mx.call.cancel",
939
996
  /** You answered (or declined) on another device. Stop ringing here. */
940
997
  settled: "mx.call.settled",
941
- /** Someone is in the lobby. Hosts render the knock. */
942
- knock: "mx.lobby.knock",
943
- /** A host decided. The knocker learns it here — they have no DB read. */
944
- admission: "mx.lobby.admission",
945
- /** The room was locked or unlocked. */
946
- lock: "mx.room.lock",
947
- /** Recording started or stopped. EVERY participant renders the indicator. */
948
- recording: "mx.room.recording"
998
+ /**
999
+ * 🚨 S4 (CORE-DESIGN §2.6): the ONE room-channel event — payload-free "something changed,
1000
+ * read again". Nothing a person must see rides it: a waiting person learns admitted /
1001
+ * denied / expired / ended by READING their row (`GET /v1/meet/state`) on a timer and on
1002
+ * every hint. The four payload broadcasts (`mx.lobby.knock`, `mx.lobby.admission`,
1003
+ * `mx.room.lock`, `mx.room.recording`) are gone with their senders and receivers. The
1004
+ * server publishes the same event on the same topic (aidream `admission.room_topic`).
1005
+ */
1006
+ stateChanged: "mx.state.changed"
949
1007
  };
950
1008
 
951
1009
  // src/core/types.ts
@@ -959,14 +1017,19 @@ function isGuest(identity) {
959
1017
  return identity.userId === null;
960
1018
  }
961
1019
  var ROLE_RANK = {
962
- host: 3,
963
- cohost: 2,
1020
+ host: 4,
1021
+ cohost: 3,
1022
+ presenter: 2,
964
1023
  participant: 1,
965
- guest: 0
1024
+ viewer: 0
966
1025
  };
1026
+ function isLinkFollower(standing) {
1027
+ return standing === "guest" || standing === "outsider";
1028
+ }
967
1029
  function isHostRole(role) {
968
1030
  return role === "host" || role === "cohost";
969
1031
  }
1032
+ var WAITING_ADMISSION_STATES = ["waiting_host", "knocking"];
970
1033
 
971
1034
  // src/core/calls.ts
972
1035
  var TRANSITIONS = {
@@ -1870,15 +1933,15 @@ var CAPTION_LANGUAGES = [
1870
1933
  { tag: "id", name: "Indonesian" },
1871
1934
  { tag: "sv", name: "Swedish" }
1872
1935
  ];
1873
- async function loadMeetingActivities(api, meetingId, roomToken = null) {
1936
+ async function loadMeetingActivities(api, meetingId, meetingPass = null) {
1874
1937
  let response;
1875
1938
  try {
1876
1939
  response = await api.request({
1877
1940
  path: `${MEET_ROUTES.collab}?meeting_id=${encodeURIComponent(meetingId)}`,
1878
1941
  method: "GET",
1879
1942
  operation: "meet.loadMeetingActivities",
1880
- requireAuth: roomToken === null,
1881
- ...roomToken !== null ? { headers: { [ROOM_TOKEN_HEADER]: roomToken } } : {}
1943
+ requireAuth: meetingPass === null,
1944
+ ...meetingPass !== null ? { headers: { [MEETING_PASS_HEADER]: meetingPass } } : {}
1882
1945
  });
1883
1946
  } catch (error) {
1884
1947
  return {
@@ -1975,12 +2038,14 @@ var DEFAULT_EFFECTS = {
1975
2038
  };
1976
2039
  var NO_GRANTS = {
1977
2040
  role: "participant",
2041
+ standing: null,
1978
2042
  canRecord: false,
1979
2043
  canPublish: false,
1980
2044
  canSubscribe: false
1981
2045
  };
1982
2046
  var EMPTY = {
1983
2047
  phase: "idle",
2048
+ waiting: null,
1984
2049
  meeting: null,
1985
2050
  localIdentity: null,
1986
2051
  grants: NO_GRANTS,
@@ -2021,6 +2086,11 @@ function effectiveRole(snapshot) {
2021
2086
  const fromToken = snapshot.grants.role;
2022
2087
  return ROLE_RANK[fromRoster] >= ROLE_RANK[fromToken] ? fromRoster : fromToken;
2023
2088
  }
2089
+ function effectiveStanding(snapshot) {
2090
+ if (snapshot.grants.standing != null) return snapshot.grants.standing;
2091
+ const me = snapshot.participants.find((p) => p.identity === snapshot.localIdentity);
2092
+ return me?.standing ?? null;
2093
+ }
2024
2094
  var REACTION_TTL_MS = 5e3;
2025
2095
  var CHAT_LIMIT = 200;
2026
2096
  function createMeetStore(initial = {}) {
@@ -2232,19 +2302,25 @@ function roleOf(participants, identity) {
2232
2302
  return participants.find((p) => p.identity === identity)?.role ?? "participant";
2233
2303
  }
2234
2304
  var END_MEETING_NOT_SERVED = "Ending a meeting is not available on this server yet";
2235
- function lobbyEntryFrom(attendee) {
2236
- if (attendee.admissionState !== "knocking") return null;
2305
+ function lobbyEntryFrom(attendee, now = Date.now()) {
2306
+ if (!WAITING_ADMISSION_STATES.includes(attendee.admissionState)) return null;
2307
+ const expires = attendee.knockExpiresAt ? Date.parse(attendee.knockExpiresAt) : Number.NaN;
2308
+ if (!Number.isNaN(expires) && expires <= now) return null;
2237
2309
  return {
2238
2310
  identity: attendee.identity,
2239
2311
  displayName: attendee.displayName,
2240
2312
  avatarUrl: attendee.avatarUrl,
2241
2313
  userId: attendee.userId,
2242
2314
  state: "knocking",
2243
- knockedAt: attendee.joinedAt ?? (/* @__PURE__ */ new Date()).toISOString(),
2315
+ knockedAt: attendee.knockedAt ?? attendee.joinedAt ?? (/* @__PURE__ */ new Date()).toISOString(),
2316
+ knockExpiresAt: attendee.knockExpiresAt ?? null,
2244
2317
  decidedAt: null,
2245
2318
  decidedBy: null
2246
2319
  };
2247
2320
  }
2321
+ function decidedState(state) {
2322
+ return WAITING_ADMISSION_STATES.includes(state) ? "expired" : state;
2323
+ }
2248
2324
  function createMeetingSession(options) {
2249
2325
  const { manager, repository, store, identity } = options;
2250
2326
  let channel = null;
@@ -2268,11 +2344,6 @@ function createMeetingSession(options) {
2268
2344
  }
2269
2345
  return identity.userId;
2270
2346
  }
2271
- function knockIdentity() {
2272
- return asParticipantIdentity(
2273
- identity.userId !== null ? `user:${identity.userId}` : `guest:${identity.deviceId}`
2274
- );
2275
- }
2276
2347
  function requireMeeting(operation) {
2277
2348
  const meeting = store.snapshot().meeting;
2278
2349
  if (meeting === null) {
@@ -2284,6 +2355,191 @@ function createMeetingSession(options) {
2284
2355
  }
2285
2356
  return meeting;
2286
2357
  }
2358
+ let polling = false;
2359
+ let pollTimer = null;
2360
+ let reading = null;
2361
+ let admittedOnce = false;
2362
+ let unlisten = null;
2363
+ function report(level, message, remedy) {
2364
+ options.onDiagnostic?.(remedy === void 0 ? { level, message } : { level, message, remedy });
2365
+ }
2366
+ function pollMs() {
2367
+ const seconds = store.snapshot().waiting?.pollSeconds ?? 3;
2368
+ return Math.max(1, Math.min(60, seconds)) * 1e3;
2369
+ }
2370
+ function schedule(ms2) {
2371
+ if (pollTimer !== null) clearTimeout(pollTimer);
2372
+ if (!polling) return;
2373
+ pollTimer = setTimeout(() => {
2374
+ pollTimer = null;
2375
+ void readState2().finally(() => schedule(pollMs()));
2376
+ }, ms2);
2377
+ }
2378
+ function listen() {
2379
+ if (unlisten !== null || typeof window === "undefined") return;
2380
+ const nudge = () => {
2381
+ if (typeof document !== "undefined" && document.visibilityState === "hidden") return;
2382
+ void readState2();
2383
+ };
2384
+ const goodbye = () => {
2385
+ const meeting = store.snapshot().meeting;
2386
+ const pass = options.resolvePass?.() ?? null;
2387
+ const waiting = store.snapshot().waiting;
2388
+ if (meeting === null || pass === null || waiting === null) return;
2389
+ if (!WAITING_ADMISSION_STATES.includes(waiting.admission)) return;
2390
+ try {
2391
+ navigator.sendBeacon?.(
2392
+ options.api.url(MEET_ROUTES.leave),
2393
+ new Blob([JSON.stringify({ meeting_id: meeting.id, intent: "withdraw", meeting_pass: pass })], {
2394
+ type: "application/json"
2395
+ })
2396
+ );
2397
+ } catch {
2398
+ }
2399
+ };
2400
+ window.addEventListener("online", nudge);
2401
+ window.addEventListener("pagehide", goodbye);
2402
+ document.addEventListener("visibilitychange", nudge);
2403
+ unlisten = () => {
2404
+ window.removeEventListener("online", nudge);
2405
+ window.removeEventListener("pagehide", goodbye);
2406
+ document.removeEventListener("visibilitychange", nudge);
2407
+ };
2408
+ }
2409
+ function stopWaiting() {
2410
+ polling = false;
2411
+ if (pollTimer !== null) clearTimeout(pollTimer);
2412
+ pollTimer = null;
2413
+ unlisten?.();
2414
+ unlisten = null;
2415
+ }
2416
+ function patchWaiting(next) {
2417
+ const current = store.snapshot().waiting;
2418
+ const base = current ?? {
2419
+ admission: "knocking",
2420
+ admittersPresent: null,
2421
+ reknockAllowed: true,
2422
+ knockExpiresAt: null,
2423
+ pollSeconds: 3,
2424
+ liveVersion: 0
2425
+ };
2426
+ store.patch({ waiting: { ...base, ...next } });
2427
+ }
2428
+ function apply(read) {
2429
+ const snapshot = store.snapshot();
2430
+ if (snapshot.phase !== "lobby") return;
2431
+ const admission = read.admission_state ?? null;
2432
+ const ended = admission === "ended" || read.session_phase === "ended" || read.meeting_phase === "ended";
2433
+ if (admission === "admitted" && !ended) {
2434
+ if (admittedOnce) return;
2435
+ admittedOnce = true;
2436
+ stopWaiting();
2437
+ options.onAdmitted?.();
2438
+ return;
2439
+ }
2440
+ if (ended) {
2441
+ stopWaiting();
2442
+ patchWaiting({ admission: "ended" });
2443
+ const meeting = snapshot.meeting;
2444
+ if (read.meeting_phase === "ended" && meeting !== null && meeting.endedAt === null) {
2445
+ store.patch({ meeting: { ...meeting, endedAt: (/* @__PURE__ */ new Date()).toISOString() } });
2446
+ }
2447
+ return;
2448
+ }
2449
+ if (admission === null) return;
2450
+ patchWaiting({
2451
+ admission,
2452
+ admittersPresent: read.admitters_present ?? null,
2453
+ reknockAllowed: read.reknock_allowed ?? true,
2454
+ knockExpiresAt: read.knock_expires_at ?? null,
2455
+ pollSeconds: read.poll_seconds ?? snapshot.waiting?.pollSeconds ?? 3,
2456
+ liveVersion: read.live_version ?? 0
2457
+ });
2458
+ if (admission === "denied" || admission === "expired" || admission === "withdrawn" || admission === "removed") {
2459
+ stopWaiting();
2460
+ }
2461
+ }
2462
+ async function readState2() {
2463
+ if (reading !== null) return reading;
2464
+ const meeting = store.snapshot().meeting;
2465
+ const pass = options.resolvePass?.() ?? null;
2466
+ if (meeting === null || pass === null) return;
2467
+ reading = (async () => {
2468
+ try {
2469
+ const response = await options.api.request({
2470
+ path: `${MEET_ROUTES.state}?meeting_id=${encodeURIComponent(meeting.id)}`,
2471
+ method: "GET",
2472
+ requireAuth: false,
2473
+ operation: "meet.state",
2474
+ headers: { [MEETING_PASS_HEADER]: pass }
2475
+ });
2476
+ if (!response.ok) {
2477
+ let body = null;
2478
+ try {
2479
+ body = await response.json();
2480
+ } catch {
2481
+ body = null;
2482
+ }
2483
+ const refusal2 = readServerRefusal(body);
2484
+ report(
2485
+ "warn",
2486
+ refusal2?.reason ?? `meet.state: the server answered ${response.status}`,
2487
+ refusal2?.remedy ?? "The waiting room keeps asking; nothing on this screen changes until the server answers."
2488
+ );
2489
+ return;
2490
+ }
2491
+ apply(await response.json());
2492
+ } catch (error) {
2493
+ report("info", `meet.state: ${String(error)}`, "Waiting continues; the state is read again shortly.");
2494
+ } finally {
2495
+ reading = null;
2496
+ }
2497
+ })();
2498
+ return reading;
2499
+ }
2500
+ async function postLeave(intent) {
2501
+ const meeting = requireMeeting(`meet.${intent}`);
2502
+ const pass = options.resolvePass?.() ?? null;
2503
+ const response = await options.api.request({
2504
+ path: MEET_ROUTES.leave,
2505
+ method: "POST",
2506
+ requireAuth: false,
2507
+ operation: `meet.${intent}`,
2508
+ body: { meeting_id: meeting.id, intent },
2509
+ ...pass !== null ? { headers: { [MEETING_PASS_HEADER]: pass } } : {}
2510
+ });
2511
+ if (!response.ok) {
2512
+ let body = null;
2513
+ try {
2514
+ body = await response.json();
2515
+ } catch {
2516
+ body = null;
2517
+ }
2518
+ const refusal2 = readServerRefusal(body);
2519
+ throw new MeetError(
2520
+ "transport",
2521
+ refusal2?.reason ?? `meet.${intent}: the server answered ${response.status}`,
2522
+ refusal2?.remedy ?? "You are still in the waiting room. Try again."
2523
+ );
2524
+ }
2525
+ }
2526
+ function hint() {
2527
+ channel?.send(MEET_EVENTS.stateChanged, { v: null });
2528
+ }
2529
+ async function refreshQueue() {
2530
+ const meeting = store.snapshot().meeting;
2531
+ if (meeting === null || isGuest(identity) || !isHostRole(effectiveRole(store.snapshot()))) return;
2532
+ try {
2533
+ const attendees = await repository.attendees(meeting.id);
2534
+ store.learn(attendees);
2535
+ for (const attendee of attendees) {
2536
+ const entry = lobbyEntryFrom(attendee);
2537
+ if (entry !== null) store.upsertLobby(entry);
2538
+ else store.setAdmission(attendee.identity, decidedState(attendee.admissionState), null);
2539
+ }
2540
+ } catch {
2541
+ }
2542
+ }
2287
2543
  return {
2288
2544
  watch(roomName) {
2289
2545
  channel?.close();
@@ -2291,58 +2547,22 @@ function createMeetingSession(options) {
2291
2547
  topic: roomTopic(roomName),
2292
2548
  broadcast: [
2293
2549
  {
2294
- event: MEET_EVENTS.knock,
2295
- onMessage: (message) => {
2296
- store.upsertLobby(message.data);
2297
- }
2298
- },
2299
- {
2300
- event: MEET_EVENTS.admission,
2301
- onMessage: (message) => {
2302
- const verdict = message.data;
2303
- const decided = asParticipantIdentity(verdict.identity);
2304
- store.setAdmission(
2305
- decided,
2306
- verdict.admitted ? "admitted" : "denied",
2307
- verdict.by === null ? null : verdict.by
2308
- );
2309
- if (decided === knockIdentity()) {
2310
- if (verdict.admitted) options.onAdmitted?.();
2311
- else options.onDenied?.();
2312
- }
2313
- }
2314
- },
2315
- {
2316
- event: MEET_EVENTS.lock,
2317
- onMessage: (message) => {
2318
- const { locked } = message.data;
2319
- const meeting = store.snapshot().meeting;
2320
- store.patch({
2321
- locked,
2322
- ...meeting !== null ? { meeting: { ...meeting, locked } } : {}
2323
- });
2324
- }
2325
- },
2326
- {
2327
- event: MEET_EVENTS.recording,
2328
- onMessage: (message) => {
2329
- const payload = message.data;
2330
- store.setRecording(
2331
- payload.state,
2332
- payload.by === null ? null : payload.by,
2333
- "room"
2334
- );
2550
+ // 🚨 PAYLOAD-FREE. Every receiver reads again; a spoofed hint only causes a read.
2551
+ event: MEET_EVENTS.stateChanged,
2552
+ onMessage: () => {
2553
+ if (store.snapshot().phase === "lobby") void readState2();
2554
+ void refreshQueue();
2335
2555
  }
2336
2556
  }
2337
2557
  ],
2338
- // THE BACKFILL DOOR. A host whose socket blipped must not miss a knock:
2339
- // re-reading the meeting restores lock/recording truth, and knockers
2340
- // re-announce on their own timer.
2558
+ // THE BACKFILL DOOR. A socket blip must not lose a decision: re-read the meeting
2559
+ // row, and a waiting tab re-reads its own state.
2341
2560
  onBackfill: async () => {
2342
2561
  const meeting = store.snapshot().meeting;
2343
2562
  if (meeting === null) return;
2344
2563
  const fresh = isGuest(identity) ? await repository.meetingBySlug(meeting.slug) : await readMeeting(meeting.id);
2345
2564
  store.patch({ meeting: fresh, locked: fresh.locked });
2565
+ if (store.snapshot().phase === "lobby") void readState2();
2346
2566
  }
2347
2567
  });
2348
2568
  },
@@ -2403,7 +2623,7 @@ function createMeetingSession(options) {
2403
2623
  store.upsertLobby(entry);
2404
2624
  return;
2405
2625
  }
2406
- store.setAdmission(attendee.identity, attendee.admissionState, null);
2626
+ store.setAdmission(attendee.identity, decidedState(attendee.admissionState), null);
2407
2627
  }
2408
2628
  },
2409
2629
  {
@@ -2455,62 +2675,75 @@ function createMeetingSession(options) {
2455
2675
  if (entry !== null) {
2456
2676
  store.upsertLobby(entry);
2457
2677
  } else {
2458
- store.setAdmission(attendee.identity, attendee.admissionState, null);
2678
+ store.setAdmission(attendee.identity, decidedState(attendee.admissionState), null);
2459
2679
  }
2460
2680
  }
2461
2681
  }
2462
2682
  });
2463
2683
  meetingChannel.requestBackfill("manual");
2464
2684
  },
2465
- knock(roomName) {
2466
- const handle = channel ?? manager.open({ topic: roomTopic(roomName) });
2467
- channel = handle;
2468
- const entry = {
2469
- identity: knockIdentity(),
2470
- displayName: identity.displayName,
2471
- avatarUrl: identity.avatarUrl,
2472
- userId: identity.userId,
2473
- state: "knocking",
2474
- knockedAt: (/* @__PURE__ */ new Date()).toISOString(),
2475
- decidedAt: null,
2476
- decidedBy: null
2477
- };
2478
- store.upsertLobby(entry);
2479
- handle.send(MEET_EVENTS.knock, entry);
2480
- },
2481
2685
  async admit(entry, admitted) {
2482
2686
  const by = requireHostUser("meet.admit");
2483
2687
  const meeting = requireMeeting("meet.admit");
2484
- await repository.admit({
2688
+ const row = await repository.decideAdmission({
2485
2689
  meetingId: meeting.id,
2486
2690
  identity: entry.identity,
2487
- admitted,
2691
+ decision: admitted ? "admit" : "deny",
2488
2692
  byUserId: by
2489
2693
  });
2490
2694
  store.upsertLobby({
2491
2695
  ...entry,
2492
- state: admitted ? "admitted" : "denied",
2696
+ state: decidedState(row.admissionState),
2493
2697
  decidedAt: (/* @__PURE__ */ new Date()).toISOString(),
2494
2698
  decidedBy: by
2495
2699
  });
2496
- channel?.send(MEET_EVENTS.admission, {
2700
+ hint();
2701
+ },
2702
+ async undeny(entry) {
2703
+ const by = requireHostUser("meet.undeny");
2704
+ const meeting = requireMeeting("meet.undeny");
2705
+ const row = await repository.decideAdmission({
2706
+ meetingId: meeting.id,
2497
2707
  identity: entry.identity,
2498
- admitted,
2499
- by
2708
+ decision: "undeny",
2709
+ byUserId: by
2500
2710
  });
2711
+ store.upsertLobby({ ...entry, state: decidedState(row.admissionState), decidedAt: (/* @__PURE__ */ new Date()).toISOString(), decidedBy: by });
2712
+ hint();
2501
2713
  },
2502
2714
  async admitAll() {
2503
- const knocking = store.snapshot().lobby.filter((entry) => entry.state === "knocking");
2504
- for (const entry of knocking) {
2505
- await this.admit(entry, true);
2506
- }
2715
+ const by = requireHostUser("meet.admitAll");
2716
+ const meeting = requireMeeting("meet.admitAll");
2717
+ const rows = await repository.admitAll(meeting.id, by);
2718
+ for (const row of rows) {
2719
+ store.setAdmission(row.identity, row.admissionState, by);
2720
+ }
2721
+ hint();
2722
+ },
2723
+ startWaiting() {
2724
+ if (polling) return;
2725
+ polling = true;
2726
+ admittedOnce = false;
2727
+ listen();
2728
+ schedule(0);
2729
+ },
2730
+ stopWaiting,
2731
+ readState: readState2,
2732
+ async withdraw() {
2733
+ stopWaiting();
2734
+ await postLeave("withdraw");
2735
+ patchWaiting({ admission: "withdrawn" });
2736
+ hint();
2737
+ },
2738
+ async askAgain() {
2739
+ await options.rejoin?.();
2507
2740
  },
2508
2741
  async setLock(locked) {
2509
2742
  const by = requireHostUser("meet.setLock");
2510
2743
  const meeting = requireMeeting("meet.setLock");
2511
2744
  const updated = await repository.setLock(meeting.id, locked, by);
2512
2745
  store.patch({ meeting: updated, locked: updated.locked });
2513
- channel?.send(MEET_EVENTS.lock, { locked: updated.locked });
2746
+ hint();
2514
2747
  return updated;
2515
2748
  },
2516
2749
  /**
@@ -2568,6 +2801,7 @@ function createMeetingSession(options) {
2568
2801
  return { endedAt, served: true };
2569
2802
  },
2570
2803
  dispose() {
2804
+ stopWaiting();
2571
2805
  channel?.close();
2572
2806
  channel = null;
2573
2807
  meetingChannel?.close();
@@ -2612,9 +2846,15 @@ var RPCS = {
2612
2846
  getOrCreateMeeting: "meet_get_or_create_meeting",
2613
2847
  /** Resolve a durable public link, before/during/after its scheduled time. */
2614
2848
  meetingBySlug: "meet_meeting_by_slug",
2615
- admit: "meet_admit_participant",
2849
+ /*
2850
+ * ── S4 ADMISSION (CORE-DESIGN §2.4). Each returns the decided row; the caller then
2851
+ * sends the payload-free `mx.state.changed` hint. Authorized by `who_may_admit`.
2852
+ */
2853
+ admit: "meet_admit",
2854
+ deny: "meet_deny",
2855
+ undeny: "meet_undeny",
2856
+ admitAll: "meet_admit_all",
2616
2857
  setLock: "meet_set_lock",
2617
- endMeeting: "meet_end_meeting",
2618
2858
  recordConsent: "meet_record_consent",
2619
2859
  /*
2620
2860
  * ── MEET WAVE 1 — management (0.8). Every write is a SECURITY DEFINER door
@@ -2656,13 +2896,13 @@ var RPCS = {
2656
2896
  /** Titles, agendas, current notes and final transcript lines; snippets mark hits with ⟦ ⟧. */
2657
2897
  search: "meet_search"
2658
2898
  };
2659
- var VOID_RPCS = [RPCS.admit, RPCS.recordConsent];
2899
+ var VOID_RPCS = [RPCS.recordConsent];
2660
2900
  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";
2661
2901
  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";
2662
2902
  var RECORDING_COLUMNS = "id,meeting_id,state,started_at,ended_at,started_by,file_id,failure_reason";
2663
2903
  var SEGMENT_COLUMNS = "id,meeting_id,identity,speaker,text,started_at,is_final,metadata";
2664
2904
  var NOTE_COLUMNS = "id,meeting_id,text,kind,assignee_user_id,created_at,metadata";
2665
- 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";
2905
+ 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";
2666
2906
  function projectStays(value) {
2667
2907
  if (!Array.isArray(value)) return [];
2668
2908
  const stays = [];
@@ -3028,8 +3268,17 @@ function createMeetRepository(options) {
3028
3268
  const operation = "meet.projectAttendee";
3029
3269
  const role = text(row, "role");
3030
3270
  const admission = text(row, "admission_state");
3031
- const known = ["host", "cohost", "participant", "guest"];
3032
- const states = ["knocking", "admitted", "denied", "removed"];
3271
+ const known = ["host", "cohost", "presenter", "participant", "viewer"];
3272
+ const states = [
3273
+ "waiting_host",
3274
+ "knocking",
3275
+ "admitted",
3276
+ "denied",
3277
+ "expired",
3278
+ "withdrawn",
3279
+ "removed",
3280
+ "ended"
3281
+ ];
3033
3282
  const userId = text(row, "participant_user_id");
3034
3283
  const identity = requireText(row, "identity", operation);
3035
3284
  return {
@@ -3043,6 +3292,8 @@ function createMeetRepository(options) {
3043
3292
  userId: userId === null ? null : asUserId(userId),
3044
3293
  role: known.includes(role) ? role : "participant",
3045
3294
  admissionState: states.includes(admission) ? admission : "admitted",
3295
+ knockedAt: text(row, "knocked_at"),
3296
+ knockExpiresAt: text(row, "knock_expires_at"),
3046
3297
  isAgent: bool(row, "is_agent", false),
3047
3298
  joinedAt: text(row, "joined_at"),
3048
3299
  leftAt: text(row, "left_at"),
@@ -3321,16 +3572,26 @@ function createMeetRepository(options) {
3321
3572
  hasMore: rows.length > limit
3322
3573
  };
3323
3574
  },
3324
- async admit({ meetingId, identity, admitted, byUserId }) {
3325
- await guardVoid(
3326
- "meet.admit",
3327
- () => db.rpc(RPCS.admit, {
3575
+ async decideAdmission({ meetingId, identity, decision, byUserId }) {
3576
+ const fn = decision === "admit" ? RPCS.admit : decision === "deny" ? RPCS.deny : RPCS.undeny;
3577
+ const row = await guard(
3578
+ `meet.${decision}`,
3579
+ () => db.rpc(fn, {
3328
3580
  p_meeting_id: meetingId,
3329
3581
  p_identity: identity,
3330
- p_admitted: admitted,
3331
3582
  p_by_user_id: byUserId
3332
3583
  })
3333
3584
  );
3585
+ return projectAttendee(row);
3586
+ },
3587
+ async admitAll(meetingId, byUserId) {
3588
+ const response = await db.rpc(RPCS.admitAll, {
3589
+ p_meeting_id: meetingId,
3590
+ p_by_user_id: byUserId
3591
+ });
3592
+ if (response.error !== null) throw normalizeMeetError(response.error, "meet.admitAll");
3593
+ const rows = Array.isArray(response.data) ? response.data : [];
3594
+ return rows.map(projectAttendee);
3334
3595
  },
3335
3596
  async setLock(meetingId, locked, byUserId) {
3336
3597
  const row = await guard(
@@ -3343,16 +3604,6 @@ function createMeetRepository(options) {
3343
3604
  );
3344
3605
  return projectMeeting(row);
3345
3606
  },
3346
- async endMeeting(meetingId, byUserId) {
3347
- const row = await guard(
3348
- "meet.endMeeting",
3349
- () => db.rpc(RPCS.endMeeting, {
3350
- p_meeting_id: meetingId,
3351
- p_by_user_id: byUserId
3352
- })
3353
- );
3354
- return projectMeeting(row);
3355
- },
3356
3607
  async transcript(meetingId, limit = 1e3) {
3357
3608
  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);
3358
3609
  if (response.error !== null) throw normalizeMeetError(response.error, "meet.transcript");
@@ -3798,7 +4049,7 @@ function applyModeration(store, payload, now) {
3798
4049
  return ignored(`a moderation kind this version does not know: ${String(kind)}`);
3799
4050
  }
3800
4051
  }
3801
- var ROLES = ["host", "cohost", "participant", "guest"];
4052
+ var ROLES = ["host", "cohost", "presenter", "participant", "viewer"];
3802
4053
 
3803
4054
  // src/core/note-taker.ts
3804
4055
  var NOTE_TAKER_IDENTITY_PREFIX = "agent:note_taker:";
@@ -3883,7 +4134,12 @@ var TRANSCRIPTION_ATTRIBUTES = {
3883
4134
  // src/core/room.ts
3884
4135
  var PARTICIPANT_ROLE_ATTRIBUTE = "matrx.participant.role";
3885
4136
  var PARTICIPANT_CAN_RECORD_ATTRIBUTE = "matrx.participant.can_record";
3886
- var KNOWN_ROLES = ["host", "cohost", "participant", "guest"];
4137
+ var PARTICIPANT_STANDING_ATTRIBUTE = "matrx.participant.standing";
4138
+ var KNOWN_STANDINGS = ["member", "invited", "outsider", "guest"];
4139
+ function parseStanding(value) {
4140
+ return KNOWN_STANDINGS.includes(value) ? value : null;
4141
+ }
4142
+ var KNOWN_ROLES = ["host", "cohost", "presenter", "participant", "viewer"];
3887
4143
  function parseRole(value) {
3888
4144
  return KNOWN_ROLES.includes(value) ? value : "participant";
3889
4145
  }
@@ -3917,6 +4173,9 @@ function projectParticipant(driver, joinedAt) {
3917
4173
  // ATTRIBUTE FIRST, metadata second — see the constants above. Both are the
3918
4174
  // server's; the attribute is simply the one that always arrives.
3919
4175
  role: parseRole(driver.attributes[PARTICIPANT_ROLE_ATTRIBUTE] ?? metadata.role),
4176
+ standing: parseStanding(
4177
+ driver.attributes[PARTICIPANT_STANDING_ATTRIBUTE] ?? metadata.standing
4178
+ ),
3920
4179
  canRecord: driver.attributes[PARTICIPANT_CAN_RECORD_ATTRIBUTE] === "true" || metadata.can_record === true,
3921
4180
  isLocal: driver.isLocal,
3922
4181
  isAgent: metadata.is_agent === true,
@@ -3989,7 +4248,7 @@ function createRoomEngine(options) {
3989
4248
  const normalized = normalizeMeetError(error, operation);
3990
4249
  store.patch({
3991
4250
  phase: "failed",
3992
- failure: { message: normalized.message, remedy: normalized.remedy }
4251
+ failure: { message: normalized.message, remedy: normalized.remedy, reason: normalized.reason }
3993
4252
  });
3994
4253
  report(
3995
4254
  normalized.code === "session-unavailable" ? "warn" : "error",
@@ -4179,7 +4438,7 @@ function createRoomEngine(options) {
4179
4438
  }
4180
4439
  };
4181
4440
  async function connect(request) {
4182
- store.patch({ phase: "requesting-token", failure: null });
4441
+ store.patch({ phase: "requesting-token", failure: null, waiting: null });
4183
4442
  const token = await tokens.get({
4184
4443
  roomName: request.roomName,
4185
4444
  identity,
@@ -4193,16 +4452,27 @@ function createRoomEngine(options) {
4193
4452
  breakoutRoomId: token.breakoutRoomId ?? null,
4194
4453
  grants: {
4195
4454
  role: token.role,
4455
+ standing: token.standing,
4196
4456
  canRecord: token.canRecord,
4197
4457
  canPublish: token.canPublish,
4198
4458
  canSubscribe: token.canSubscribe
4199
4459
  }
4200
4460
  });
4201
- if (token.lobby) {
4202
- store.patch({ phase: "lobby" });
4461
+ if (token.lobby || !token.canSubscribe) {
4462
+ store.patch({
4463
+ phase: "lobby",
4464
+ waiting: {
4465
+ admission: token.admissionState !== void 0 && token.admissionState !== "admitted" ? token.admissionState : "knocking",
4466
+ admittersPresent: null,
4467
+ reknockAllowed: true,
4468
+ knockExpiresAt: null,
4469
+ pollSeconds: token.pollSeconds ?? 3,
4470
+ liveVersion: 0
4471
+ }
4472
+ });
4203
4473
  return;
4204
4474
  }
4205
- store.patch({ phase: "connecting" });
4475
+ store.patch({ phase: "connecting", waiting: null });
4206
4476
  const created = await options.driverFactory(handlers);
4207
4477
  if (disposed) {
4208
4478
  await created.disconnect();
@@ -4538,7 +4808,7 @@ function createInRoomClient(options) {
4538
4808
  }
4539
4809
  function headers() {
4540
4810
  const token = options.resolveRoomToken();
4541
- return token === null ? void 0 : { [ROOM_TOKEN_HEADER]: token };
4811
+ return token === null ? void 0 : { [MEETING_PASS_HEADER]: token };
4542
4812
  }
4543
4813
  function refusal2(response, operation, remedy) {
4544
4814
  return refusalFromResponse(response, operation, remedy);
@@ -5285,25 +5555,37 @@ function MeetRuntime(props) {
5285
5555
  api,
5286
5556
  onDiagnostic: report,
5287
5557
  // ADMISSION IS A JOIN, NOT A RE-RENDER. The lobby token grants neither
5288
- // publish nor subscribe, so the host saying yes has to mint a new one.
5558
+ // publish nor subscribe, so the host saying yes has to mint a new one. The
5559
+ // "yes" is read from the server (the state read), never a broadcast (S4).
5289
5560
  onAdmitted: () => {
5290
- void engine.rejoin();
5291
- },
5292
- // A "no" is ALSO a mint: the server refuses it with its own sentence, which
5293
- // turns the waiting room into the pre-join screen showing that sentence.
5294
- onDenied: () => {
5295
5561
  void engine.rejoin().catch(() => void 0);
5562
+ },
5563
+ rejoin: () => engine.rejoin(),
5564
+ // THE MEETING PASS this device holds — what the state read and `/leave` carry.
5565
+ resolvePass: () => {
5566
+ const roomName = store.snapshot().meeting?.roomName;
5567
+ const active = engine.token();
5568
+ if (active?.meetingPass) return active.meetingPass;
5569
+ return roomName === void 0 ? null : tokens.remembered(roomName)?.meetingPass ?? null;
5296
5570
  }
5297
5571
  });
5298
5572
  const resolveRoomToken = () => {
5299
5573
  const roomName = store.snapshot().meeting?.roomName;
5300
5574
  if (roomName === void 0) return null;
5301
5575
  const active = engine.token();
5302
- if (active?.roomName === roomName && active.token.length > 0) return active.token;
5303
- return tokens.remembered(roomName)?.token ?? null;
5576
+ if (active?.roomName === roomName && active.meetingPass) return active.meetingPass;
5577
+ return tokens.remembered(roomName)?.meetingPass ?? null;
5304
5578
  };
5305
5579
  const ai = createMeetAi({ api, store, resolveRoomToken });
5306
5580
  const inRoom = createInRoomClient({ api, store, resolveRoomToken });
5581
+ store.subscribe(() => {
5582
+ const snapshot = store.snapshot();
5583
+ if (snapshot.phase === "lobby" && snapshot.waiting !== null && (snapshot.waiting.admission === "knocking" || snapshot.waiting.admission === "waiting_host")) {
5584
+ meetings.startWaiting();
5585
+ } else if (snapshot.phase !== "lobby") {
5586
+ meetings.stopWaiting();
5587
+ }
5588
+ });
5307
5589
  let chatReadFor = null;
5308
5590
  store.subscribe(() => {
5309
5591
  const snapshot = store.snapshot();
@@ -5472,6 +5754,10 @@ function useLocalUserId() {
5472
5754
  import { useEffect as useEffect2, useMemo as useMemo2, useRef as useRef2 } from "react";
5473
5755
 
5474
5756
  // src/core/observation.ts
5757
+ function observedRole(role, standing) {
5758
+ if (role === "host" || role === "cohost") return role;
5759
+ return isLinkFollower(standing) ? "guest" : "participant";
5760
+ }
5475
5761
  function observedDevice(track) {
5476
5762
  if (track.enabled) return "on";
5477
5763
  const refused = track.unavailable;
@@ -5499,13 +5785,15 @@ function observedPhase(input) {
5499
5785
  const raw = snapshot.phase;
5500
5786
  const live = raw === "connected" || raw === "reconnecting";
5501
5787
  if (endedRecord(snapshot, input.meeting ?? null) && !live) return "ended";
5788
+ const reason = raw === "failed" ? snapshot.failure?.reason ?? null : null;
5789
+ if (reason !== null) return reason === "ended" ? "ended" : `refused:${reason}`;
5502
5790
  switch (raw) {
5503
5791
  case "idle":
5504
5792
  case "failed":
5505
5793
  case "left":
5506
5794
  return "prejoin";
5507
5795
  case "lobby":
5508
- return "waiting:knocking";
5796
+ return waitingPhase(snapshot);
5509
5797
  case "requesting-token":
5510
5798
  case "connecting":
5511
5799
  return "joining";
@@ -5514,20 +5802,39 @@ function observedPhase(input) {
5514
5802
  return "in_call";
5515
5803
  }
5516
5804
  }
5805
+ function waitingPhase(snapshot) {
5806
+ switch (snapshot.waiting?.admission) {
5807
+ case "waiting_host":
5808
+ return "waiting:host_not_started";
5809
+ case "denied":
5810
+ return "denied";
5811
+ case "expired":
5812
+ return "knock_expired";
5813
+ case "withdrawn":
5814
+ return "left";
5815
+ case "ended":
5816
+ return "ended";
5817
+ case "removed":
5818
+ return "refused:removed";
5819
+ default:
5820
+ return "waiting:knocking";
5821
+ }
5822
+ }
5517
5823
  var ADMITTER_ROLES = /* @__PURE__ */ new Set(["host", "cohost"]);
5518
5824
  function observe(input) {
5519
5825
  const { snapshot } = input;
5520
5826
  const phase = observedPhase(input);
5521
5827
  const inCall = phase === "in_call";
5522
- const role = inCall && snapshot !== null ? effectiveRole(snapshot) : "none";
5523
- const canAdmit = role !== "none" && ADMITTER_ROLES.has(role);
5828
+ const actual = inCall && snapshot !== null ? effectiveRole(snapshot) : "none";
5829
+ const canAdmit = actual !== "none" && ADMITTER_ROLES.has(actual);
5830
+ const role = actual === "none" || snapshot === null ? "none" : observedRole(actual, effectiveStanding(snapshot));
5524
5831
  const deviceInPlay = phase === "joining" || inCall;
5525
5832
  return {
5526
5833
  phase,
5527
- reason: null,
5834
+ reason: phase.startsWith("refused:") ? phase.slice("refused:".length) : null,
5528
5835
  role,
5529
5836
  connection: snapshot?.phase === "reconnecting" ? "reconnecting" : "stable",
5530
- hostPresent: inCall && snapshot !== null ? snapshot.participants.some((p) => ADMITTER_ROLES.has(p.role)) : null,
5837
+ hostPresent: inCall && snapshot !== null ? snapshot.participants.some((p) => ADMITTER_ROLES.has(p.role)) : snapshot?.phase === "lobby" ? snapshot.waiting?.admittersPresent ?? null : null,
5531
5838
  lobbyCount: canAdmit && snapshot !== null ? snapshot.lobby.filter((entry) => entry.state === "knocking").length : 0,
5532
5839
  recording: snapshot?.recording === "recording" || snapshot?.recording === "stopping",
5533
5840
  audioBlocked: null,
@@ -5590,7 +5897,7 @@ function participantAttributes(p) {
5590
5897
  "data-meet-participant": "",
5591
5898
  "data-meet-participant-identity": p.identity,
5592
5899
  "data-meet-participant-name": p.displayName,
5593
- "data-meet-participant-role": p.role,
5900
+ "data-meet-participant-role": observedRole(p.role, p.standing),
5594
5901
  "data-meet-participant-self": String(p.isLocal)
5595
5902
  };
5596
5903
  const quality = observedQuality(p.quality);
@@ -5919,7 +6226,7 @@ function summarizeMeetingRecordRestrictions(restrictions) {
5919
6226
  async function loadGuestMeetingRecord(args) {
5920
6227
  const { api, repository, meetingId, meeting, roomToken } = args;
5921
6228
  const session = args.session === true;
5922
- if (roomToken === null && !session) {
6229
+ if (roomToken?.meetingPass == null && !session) {
5923
6230
  return refusedBundle(
5924
6231
  meeting,
5925
6232
  "This meeting's record is only readable by the people who were in it.",
@@ -5929,12 +6236,12 @@ async function loadGuestMeetingRecord(args) {
5929
6236
  let response;
5930
6237
  try {
5931
6238
  response = await api.request({
5932
- path: `${MEET_ROUTES.record}?meeting_id=${encodeURIComponent(meetingId)}`,
6239
+ path: `${MEET_ROUTES.record}?meeting_id=${encodeURIComponent(meetingId)}` + (args.sessionId ? `&session_id=${encodeURIComponent(args.sessionId)}` : ""),
5933
6240
  method: "GET",
5934
6241
  // A GUEST HAS NO SESSION AND IS STILL A FIRST-CLASS READER (D6).
5935
6242
  requireAuth: session,
5936
6243
  operation: "meet.record",
5937
- headers: roomToken === null ? {} : { [ROOM_TOKEN_HEADER]: roomToken.token }
6244
+ headers: roomToken?.meetingPass != null ? { [MEETING_PASS_HEADER]: roomToken.meetingPass } : {}
5938
6245
  });
5939
6246
  } catch (error) {
5940
6247
  return refusedBundle(
@@ -6488,12 +6795,22 @@ function useLobby() {
6488
6795
  async () => requireSession(session, "admitAll").admitAll(),
6489
6796
  [session]
6490
6797
  ),
6491
- knock: useCallback2(
6492
- (roomName) => requireSession(session, "knock").knock(roomName),
6798
+ undeny: useCallback2(
6799
+ async (entry) => requireSession(session, "undeny").undeny(entry),
6493
6800
  [session]
6494
6801
  )
6495
6802
  };
6496
6803
  }
6804
+ function useWaiting() {
6805
+ const host = useMeetHost();
6806
+ const snapshot = useMeetSnapshot();
6807
+ const session = host?.meetings ?? null;
6808
+ return {
6809
+ waiting: snapshot?.waiting ?? null,
6810
+ withdraw: useCallback2(async () => requireSession(session, "withdraw").withdraw(), [session]),
6811
+ askAgain: useCallback2(async () => requireSession(session, "askAgain").askAgain(), [session])
6812
+ };
6813
+ }
6497
6814
  function useRecording() {
6498
6815
  const host = useMeetHost();
6499
6816
  const snapshot = useMeetSnapshot();
@@ -7141,7 +7458,6 @@ function kindOf(person) {
7141
7458
  if (person.isAgent) return "assistant";
7142
7459
  if (person.role === "host") return "host";
7143
7460
  if (person.role === "cohost") return "cohost";
7144
- if (person.role === "guest") return person.userId !== null ? "signed-in guest" : "guest";
7145
7461
  if (person.userId === null) return "guest";
7146
7462
  return "member";
7147
7463
  }
@@ -7426,7 +7742,7 @@ function meetingLength(meeting) {
7426
7742
  if (start === null || end === null || end < start) return null;
7427
7743
  return formatDurationMs2(end - start, { style: "long" });
7428
7744
  }
7429
- async function loadMeetingChat(api, meetingId, roomToken = null) {
7745
+ async function loadMeetingChat(api, meetingId, meetingPass = null) {
7430
7746
  const operation = "meet.loadMeetingChat";
7431
7747
  let response;
7432
7748
  try {
@@ -7434,8 +7750,8 @@ async function loadMeetingChat(api, meetingId, roomToken = null) {
7434
7750
  path: `${MEET_ROUTES.chat}?meeting_id=${encodeURIComponent(meetingId)}`,
7435
7751
  method: "GET",
7436
7752
  operation,
7437
- requireAuth: roomToken === null,
7438
- ...roomToken !== null ? { headers: { [ROOM_TOKEN_HEADER]: roomToken } } : {}
7753
+ requireAuth: meetingPass === null,
7754
+ ...meetingPass !== null ? { headers: { [MEETING_PASS_HEADER]: meetingPass } } : {}
7439
7755
  });
7440
7756
  } catch (error) {
7441
7757
  return {
@@ -8908,10 +9224,13 @@ function RecordingIndicator() {
8908
9224
  }
8909
9225
  );
8910
9226
  }
8911
- function RoleBadge({ role }) {
9227
+ function RoleBadge({
9228
+ role,
9229
+ standing
9230
+ }) {
8912
9231
  if (role === "host") return /* @__PURE__ */ jsx5("span", { className: "mx-meet__badge", children: "Host" });
8913
9232
  if (role === "cohost") return /* @__PURE__ */ jsx5("span", { className: "mx-meet__badge", children: "Co-host" });
8914
- if (role === "guest") return /* @__PURE__ */ jsx5("span", { className: "mx-meet__badge", children: "Guest" });
9233
+ if (isLinkFollower(standing)) return /* @__PURE__ */ jsx5("span", { className: "mx-meet__badge", children: "Guest" });
8915
9234
  return null;
8916
9235
  }
8917
9236
  function ParticipantTile({
@@ -8961,7 +9280,7 @@ function ParticipantTile({
8961
9280
  participant.displayName,
8962
9281
  participant.isLocal ? " (you)" : "",
8963
9282
  participant.isAgent ? /* @__PURE__ */ jsx5("span", { className: "mx-meet__badge", children: "AI" }) : null,
8964
- /* @__PURE__ */ jsx5(RoleBadge, { role: participant.role })
9283
+ /* @__PURE__ */ jsx5(RoleBadge, { role: participant.role, standing: participant.standing })
8965
9284
  ] }),
8966
9285
  !participant.micEnabled ? /* @__PURE__ */ jsx5(MicOffIcon, { width: 14, height: 14 }) : null,
8967
9286
  /* @__PURE__ */ jsx5(QualityBars, { quality: participant.quality })
@@ -9700,7 +10019,13 @@ function PeoplePanel({
9700
10019
  /* @__PURE__ */ jsxs3("span", { className: "mx-meet__person-name", children: [
9701
10020
  participant.displayName,
9702
10021
  participant.identity === local ? /* @__PURE__ */ jsx5("span", { className: "mx-meet__you", children: " (you)" }) : null,
9703
- /* @__PURE__ */ jsx5(RoleBadge, { role: participant.identity === local ? myRole : participant.role }),
10022
+ /* @__PURE__ */ jsx5(
10023
+ RoleBadge,
10024
+ {
10025
+ role: participant.identity === local ? myRole : participant.role,
10026
+ standing: participant.standing
10027
+ }
10028
+ ),
9704
10029
  isNoteTaker(participant) ? /* @__PURE__ */ jsx5("span", { className: "mx-meet__badge", children: "Meeting assistant" }) : null,
9705
10030
  snapshot?.spotlight === participant.identity ? /* @__PURE__ */ jsx5("span", { className: "mx-meet__badge", children: "Spotlight" }) : null
9706
10031
  ] }),
@@ -10616,7 +10941,7 @@ function MeetSkinRoom({
10616
10941
  ) });
10617
10942
  }
10618
10943
  if (phase === "lobby") {
10619
- return /* @__PURE__ */ jsx5("div", { className: "mx-meet mx-meet--lobby", children: /* @__PURE__ */ jsx5(WaitingRoom, { roomName, host }) });
10944
+ return /* @__PURE__ */ jsx5("div", { className: "mx-meet mx-meet--lobby", children: /* @__PURE__ */ jsx5(WaitingRoom, { host, ...onLeave !== void 0 ? { onLeave } : {} }) });
10620
10945
  }
10621
10946
  return /* @__PURE__ */ jsxs3("div", { className: "mx-meet", children: [
10622
10947
  /* @__PURE__ */ jsx5(ConsentNotice, {}),
@@ -10641,21 +10966,58 @@ function MeetSkinRoom({
10641
10966
  /* @__PURE__ */ jsx5(ControlBar, { ...onLeave !== void 0 ? { onLeave } : {} })
10642
10967
  ] });
10643
10968
  }
10644
- function WaitingRoom({ roomName, host }) {
10645
- const knocked = useRef6(false);
10646
- useEffect6(() => {
10647
- if (knocked.current) return;
10648
- knocked.current = true;
10649
- host.meetings.knock(roomName);
10650
- }, [host, roomName]);
10969
+ function WaitingRoom({ host, onLeave }) {
10970
+ const { waiting, withdraw, askAgain } = useWaiting();
10971
+ const [busy, setBusy] = useState4(false);
10972
+ const admission = waiting?.admission ?? "knocking";
10973
+ const run = (action) => () => {
10974
+ setBusy(true);
10975
+ void action().catch(() => void 0).finally(() => setBusy(false));
10976
+ };
10977
+ const home = /* @__PURE__ */ jsx5(Button, { type: "button", variant: "outline", onClick: () => onLeave !== void 0 ? onLeave() : window.location.assign("/"), children: "Back to home" });
10978
+ let title;
10979
+ let line = null;
10980
+ let actions;
10981
+ switch (admission) {
10982
+ case "waiting_host":
10983
+ title = "Waiting for the host to start";
10984
+ actions = /* @__PURE__ */ jsx5(Button, { type: "button", variant: "outline", disabled: busy, onClick: run(withdraw), children: "Leave" });
10985
+ break;
10986
+ case "denied":
10987
+ title = "The host declined your request";
10988
+ actions = /* @__PURE__ */ jsxs3(Fragment, { children: [
10989
+ waiting?.reknockAllowed === true ? /* @__PURE__ */ jsx5(Button, { type: "button", disabled: busy, onClick: run(askAgain), children: "Ask to join again" }) : null,
10990
+ home
10991
+ ] });
10992
+ break;
10993
+ case "expired":
10994
+ title = "No one responded";
10995
+ actions = /* @__PURE__ */ jsxs3(Fragment, { children: [
10996
+ /* @__PURE__ */ jsx5(Button, { type: "button", disabled: busy, onClick: run(askAgain), children: "Ask again" }),
10997
+ home
10998
+ ] });
10999
+ break;
11000
+ case "withdrawn":
11001
+ title = "You left the waiting room";
11002
+ actions = /* @__PURE__ */ jsxs3(Fragment, { children: [
11003
+ /* @__PURE__ */ jsx5(Button, { type: "button", disabled: busy, onClick: run(askAgain), children: "Rejoin" }),
11004
+ home
11005
+ ] });
11006
+ break;
11007
+ case "ended":
11008
+ title = "The meeting has ended";
11009
+ actions = home;
11010
+ break;
11011
+ default:
11012
+ title = "Waiting for the host to let you in";
11013
+ line = waiting?.admittersPresent === false ? "The host isn't here yet" : null;
11014
+ actions = /* @__PURE__ */ jsx5(Button, { type: "button", variant: "outline", disabled: busy, onClick: run(withdraw), children: "Cancel request" });
11015
+ }
10651
11016
  return /* @__PURE__ */ jsxs3("div", { className: "mx-meet__waiting", children: [
10652
11017
  /* @__PURE__ */ jsx5(Avatar, { name: host.identity.displayName, url: host.identity.avatarUrl, size: 72 }),
10653
- /* @__PURE__ */ jsx5("h1", { className: "mx-meet__prejoin-title", children: "Waiting for the host to let you in" }),
10654
- /* @__PURE__ */ jsxs3("p", { className: "mx-meet__empty-body", children: [
10655
- "You are in the waiting room as ",
10656
- host.identity.displayName,
10657
- ". You will join automatically once you are admitted."
10658
- ] })
11018
+ /* @__PURE__ */ jsx5("h1", { className: "mx-meet__prejoin-title", children: title }),
11019
+ line !== null ? /* @__PURE__ */ jsx5("p", { className: "mx-meet__empty-body", role: "status", "data-meet-notice": "host-absent", children: line }) : null,
11020
+ /* @__PURE__ */ jsx5("div", { className: "mx-meet__waiting-actions", children: actions })
10659
11021
  ] });
10660
11022
  }
10661
11023
  function HostMenu() {
@@ -10862,7 +11224,7 @@ function MeetingRecordBody({
10862
11224
  /* @__PURE__ */ jsxs4("span", { className: "mx-meet__person-name", children: [
10863
11225
  displayNameFor(bundle.names, person.identity, person.displayName),
10864
11226
  person.role === "host" ? /* @__PURE__ */ jsx6("span", { className: "mx-meet__badge", children: "Host" }) : null,
10865
- person.role === "guest" ? /* @__PURE__ */ jsx6("span", { className: "mx-meet__badge", children: "Guest" }) : null,
11227
+ person.userId === null && !person.isAgent ? /* @__PURE__ */ jsx6("span", { className: "mx-meet__badge", children: "Guest" }) : null,
10866
11228
  person.isAgent ? /* @__PURE__ */ jsx6("span", { className: "mx-meet__badge", children: "Meeting assistant" }) : null
10867
11229
  ] })
10868
11230
  ] }, person.id)) })
@@ -11381,6 +11743,7 @@ export {
11381
11743
  LISTEN_ONLY_NOTICE,
11382
11744
  MEETING_INVITE_KIND,
11383
11745
  MEETING_INVITE_VERSION,
11746
+ MEETING_PASS_HEADER,
11384
11747
  MEETING_RECORD_GRACE_MS,
11385
11748
  MEET_ANSWER_CONTEXT_EVENT,
11386
11749
  MEET_ANSWER_EVENTS,
@@ -11401,12 +11764,12 @@ export {
11401
11764
  ORGANIZATION_HEADER,
11402
11765
  PARTICIPANT_CAN_RECORD_ATTRIBUTE,
11403
11766
  PARTICIPANT_ROLE_ATTRIBUTE,
11767
+ PARTICIPANT_STANDING_ATTRIBUTE,
11404
11768
  REACTION_TTL_MS,
11405
11769
  REFUSED_ABSENCE,
11406
11770
  ROLE_RANK,
11407
11771
  ROOM_EVENTS,
11408
11772
  ROOM_EVENT_PREFIX,
11409
- ROOM_TOKEN_HEADER,
11410
11773
  RPCS,
11411
11774
  RecordingPlayer,
11412
11775
  TABLES,
@@ -11478,6 +11841,7 @@ export {
11478
11841
  isGuest,
11479
11842
  isGuestIdentity,
11480
11843
  isHostRole,
11844
+ isLinkFollower,
11481
11845
  isListenOnly,
11482
11846
  isNoteTaker,
11483
11847
  isRealName,
@@ -11496,6 +11860,7 @@ export {
11496
11860
  meetingInviteAction,
11497
11861
  meetingLength,
11498
11862
  meetingLink,
11863
+ meetingPassHeaders,
11499
11864
  meetingPath,
11500
11865
  meetingRecordCache,
11501
11866
  meetingSlug,
@@ -11540,6 +11905,7 @@ export {
11540
11905
  recordingLabel,
11541
11906
  recordingNotice,
11542
11907
  recordingOffsetMs,
11908
+ rememberedMeetingPass,
11543
11909
  roleOf,
11544
11910
  roomTopic,
11545
11911
  rootAttributes,