@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/core.js CHANGED
@@ -7,11 +7,18 @@ var MeetError = class extends Error {
7
7
  /** What to DO. Rendered to the user for the device codes. */
8
8
  remedy;
9
9
  cause;
10
- constructor(code, message, remedy, cause) {
10
+ /**
11
+ * The join gate's reason code (CORE-DESIGN §2.5) when the server named one: `not_found`,
12
+ * `cancelled`, `ended`, `locked`, `removed`, `lobby_rejects`, `not_invited`… The phase
13
+ * renders `refused:<reason>` (or `ended`) from it — never from the HTTP status.
14
+ */
15
+ reason;
16
+ constructor(code, message, remedy, cause, reason) {
11
17
  super(message);
12
18
  this.name = "MeetError";
13
19
  this.code = code;
14
20
  this.remedy = remedy;
21
+ this.reason = reason ?? null;
15
22
  if (cause !== void 0) this.cause = cause;
16
23
  }
17
24
  /** True when retrying after the host re-establishes a session is the fix. */
@@ -229,6 +236,17 @@ function createWebRoomTokenStorage(onDiagnostic) {
229
236
  }
230
237
  };
231
238
  }
239
+ function rememberedMeetingPass(roomName, storage = createWebRoomTokenStorage()) {
240
+ const raw = storage.read(roomName);
241
+ if (raw === null) return null;
242
+ try {
243
+ const parsed = JSON.parse(raw);
244
+ if (parsed.standing !== "guest") return null;
245
+ return typeof parsed.meetingPass === "string" && parsed.meetingPass.length > 0 ? parsed.meetingPass : null;
246
+ } catch {
247
+ return null;
248
+ }
249
+ }
232
250
  var MEET_ROUTES = {
233
251
  token: "/api/v1/meet/token",
234
252
  recordingStart: "/api/v1/meet/recording/start",
@@ -242,7 +260,7 @@ var MEET_ROUTES = {
242
260
  /**
243
261
  * THE POST-MEETING RECORD FOR A READER WITH NO DATABASE (MRF-1). A `GET`,
244
262
  * authorized by the room token this device already holds, carried in the
245
- * `x-meet-room-token` header — never a query parameter, because a bearer
263
+ * `x-meet-pass` header — never a query parameter, because a bearer
246
264
  * credential in a URL lands in access logs, referrers and history.
247
265
  */
248
266
  record: "/api/v1/meet/record",
@@ -250,7 +268,7 @@ var MEET_ROUTES = {
250
268
  recordAccess: "/api/v1/meet/record/access",
251
269
  /**
252
270
  * KEEP YOUR NOTES (2026-10-01): a guest who just created an account presents
253
- * their kept pass in `x-meet-room-token` with their new session, and the
271
+ * their kept pass in `x-meet-pass` with their new session, and the
254
272
  * meeting they attended becomes theirs to read. Idempotent; never a grant.
255
273
  */
256
274
  claim: "/api/v1/meet/claim",
@@ -272,7 +290,7 @@ var MEET_ROUTES = {
272
290
  * Meet wave 2: EVERY in-meeting host power (mute, stop video, mute all, ask to
273
291
  * unmute, spotlight, lower hands, co-host, hand over, rename, remove, report).
274
292
  * Decided by the server from the meeting itself; a guest's pass rides
275
- * `x-meet-room-token`.
293
+ * `x-meet-pass`.
276
294
  */
277
295
  moderate: "/api/v1/meet/moderate",
278
296
  /** Meet wave 2: the SAVED chat — `POST` sends to everyone, `GET ?meeting_id=` reads history. */
@@ -283,12 +301,26 @@ var MEET_ROUTES = {
283
301
  */
284
302
  collab: "/api/v1/meet/collab",
285
303
  /** Meet wave 5: translate finished caption lines into this viewer's language. */
286
- captionsTranslate: "/api/v1/meet/captions/translate"
304
+ captionsTranslate: "/api/v1/meet/captions/translate",
305
+ /**
306
+ * S4 — THE STATE READ (CORE-DESIGN §2.6): `GET ?meeting_id=` with `x-meet-pass`. This
307
+ * person's admission (lazy expiry applied), whether anyone who can admit is here, the run's
308
+ * phase. Read every `waiting_poll_seconds` while waiting and on every `mx.state.changed`.
309
+ */
310
+ state: "/api/v1/meet/state",
311
+ /** S4: `withdraw` (stop waiting) or `leave` (close this tab); the pass may ride in the body. */
312
+ leave: "/api/v1/meet/leave",
313
+ /** S4: a pass holder (guests included) saw the recording notice, for this run. */
314
+ consent: "/api/v1/meet/consent"
287
315
  };
288
316
  function meetingPath(template, meetingId) {
289
317
  return template.replace("{meeting_id}", encodeURIComponent(meetingId));
290
318
  }
291
- var ROOM_TOKEN_HEADER = "x-meet-room-token";
319
+ var MEETING_PASS_HEADER = "x-meet-pass";
320
+ function meetingPassHeaders(token) {
321
+ const pass = token?.meetingPass ?? null;
322
+ return pass !== null && pass.length > 0 ? { [MEETING_PASS_HEADER]: pass } : {};
323
+ }
292
324
  var REFRESH_RATIO = 0.8;
293
325
  function readString(source, key) {
294
326
  const value = source[key];
@@ -298,7 +330,12 @@ function readBoolean(source, key, fallback) {
298
330
  const value = source[key];
299
331
  return typeof value === "boolean" ? value : fallback;
300
332
  }
301
- var TOKEN_ROLES = ["host", "cohost", "participant", "guest"];
333
+ var TOKEN_STANDINGS = ["member", "invited", "outsider", "guest"];
334
+ function readStanding(source, key) {
335
+ const value = source[key];
336
+ return TOKEN_STANDINGS.includes(value) ? value : null;
337
+ }
338
+ var TOKEN_ROLES = ["host", "cohost", "presenter", "participant", "viewer"];
302
339
  function readRole(source, key) {
303
340
  const value = source[key];
304
341
  return TOKEN_ROLES.includes(value) ? value : null;
@@ -341,8 +378,15 @@ function parseRoomToken(body, roomName) {
341
378
  // Deriving the role from what DID arrive keeps a host a host against a
342
379
  // server that has not landed MRI-A11 — and never invents a host from
343
380
  // silence, because both fallbacks below grant nothing.
344
- role: readRole(payload, "role") ?? (isHost ? "host" : identity.startsWith("guest:") ? "guest" : "participant"),
381
+ // A word this package does not know grants nothing: `participant`, never a host.
382
+ role: readRole(payload, "role") ?? (isHost ? "host" : "participant"),
383
+ standing: readStanding(payload, "standing"),
384
+ meetingPass: readString(payload, "meeting_pass"),
385
+ meetingPassExpiresAt: readString(payload, "meeting_pass_expires_at"),
386
+ sessionId: readString(payload, "session_id"),
345
387
  lobby: readBoolean(payload, "lobby", false),
388
+ admissionState: readString(payload, "admission_state") ?? void 0,
389
+ pollSeconds: typeof payload["poll_seconds"] === "number" ? payload["poll_seconds"] : void 0,
346
390
  breakoutRoomId: readString(payload, "breakout_room_id") ?? null
347
391
  };
348
392
  }
@@ -352,7 +396,7 @@ function createTokenClient(options) {
352
396
  const storage = options.storage;
353
397
  function remember(token) {
354
398
  if (storage === void 0) return;
355
- if (!token.identity.startsWith("guest:")) return;
399
+ if (token.standing !== "guest") return;
356
400
  storage.write(token.roomName, JSON.stringify(token));
357
401
  }
358
402
  function recall(roomName) {
@@ -410,22 +454,28 @@ function createTokenClient(options) {
410
454
  throw new MeetError(
411
455
  "forbidden",
412
456
  said?.message ?? "You can't join this meeting.",
413
- said?.remedy ?? "Ask the person who shared the link to let you in, then open the link again."
457
+ said?.remedy ?? "Ask the person who shared the link to let you in, then open the link again.",
458
+ void 0,
459
+ said?.reason ?? null
414
460
  );
415
461
  }
416
462
  if (response.status === 404) {
417
463
  throw new MeetError(
418
464
  "not-found",
419
465
  "meet.token: no such meeting",
420
- "The link is wrong or the meeting was deleted. Show the 'meeting not found' screen; a durable link that 404s is never a retry."
466
+ "The link is wrong or the meeting was deleted. Show the 'meeting not found' screen; a durable link that 404s is never a retry.",
467
+ void 0,
468
+ "not_found"
421
469
  );
422
470
  }
423
471
  if (response.status === 410) {
424
- const reason = (await serverSentence(response))?.message ?? "This meeting was cancelled.";
472
+ const said = await serverSentence(response);
425
473
  throw new MeetError(
426
474
  "cancelled",
427
- reason,
428
- "Nobody can join a cancelled meeting. Show the cancelled screen with this sentence; ask the host to schedule a new one."
475
+ said?.message ?? "This meeting was cancelled.",
476
+ "Nobody can join a cancelled meeting. Show the cancelled screen with this sentence; ask the host to schedule a new one.",
477
+ void 0,
478
+ said?.reason ?? "cancelled"
429
479
  );
430
480
  }
431
481
  if (response.status === 423) {
@@ -433,7 +483,9 @@ function createTokenClient(options) {
433
483
  throw new MeetError(
434
484
  "room-locked",
435
485
  said?.message ?? "The meeting is locked.",
436
- said?.remedy ?? "The host locked the room. Ask them to unlock it, then join again."
486
+ said?.remedy ?? "The host locked the room. Ask them to unlock it, then join again.",
487
+ void 0,
488
+ said?.reason ?? "locked"
437
489
  );
438
490
  }
439
491
  if (response.status === 503) {
@@ -502,6 +554,10 @@ function placeholder(roomName) {
502
554
  canRecord: false,
503
555
  isHost: false,
504
556
  role: "participant",
557
+ standing: null,
558
+ meetingPass: null,
559
+ meetingPassExpiresAt: null,
560
+ sessionId: null,
505
561
  lobby: false,
506
562
  breakoutRoomId: null
507
563
  };
@@ -514,7 +570,8 @@ async function serverSentence(response) {
514
570
  const message = typeof source.user_message === "string" && source.user_message.trim() !== "" ? source.user_message : typeof source.message === "string" && source.message.trim() !== "" ? source.message : null;
515
571
  if (message === null) return null;
516
572
  const remedy = typeof source.remedy === "string" && source.remedy.trim() !== "" ? source.remedy : void 0;
517
- return { message, remedy };
573
+ const reason = typeof source.reason === "string" && source.reason !== "" ? source.reason : null;
574
+ return { message, remedy, reason };
518
575
  } catch {
519
576
  return null;
520
577
  }
@@ -703,7 +760,7 @@ function createMeetAi(options) {
703
760
  operation: "meet.ai.ask",
704
761
  // A GUEST MAY ASK. The server decides whether it can answer.
705
762
  requireAuth: false,
706
- ...roomToken !== null ? { headers: { [ROOM_TOKEN_HEADER]: roomToken } } : {},
763
+ ...roomToken !== null ? { headers: { [MEETING_PASS_HEADER]: roomToken } } : {},
707
764
  ...signal !== void 0 ? { signal } : {}
708
765
  });
709
766
  if (!response.ok) {
@@ -942,14 +999,15 @@ var MEET_EVENTS = {
942
999
  cancel: "mx.call.cancel",
943
1000
  /** You answered (or declined) on another device. Stop ringing here. */
944
1001
  settled: "mx.call.settled",
945
- /** Someone is in the lobby. Hosts render the knock. */
946
- knock: "mx.lobby.knock",
947
- /** A host decided. The knocker learns it here — they have no DB read. */
948
- admission: "mx.lobby.admission",
949
- /** The room was locked or unlocked. */
950
- lock: "mx.room.lock",
951
- /** Recording started or stopped. EVERY participant renders the indicator. */
952
- recording: "mx.room.recording"
1002
+ /**
1003
+ * 🚨 S4 (CORE-DESIGN §2.6): the ONE room-channel event — payload-free "something changed,
1004
+ * read again". Nothing a person must see rides it: a waiting person learns admitted /
1005
+ * denied / expired / ended by READING their row (`GET /v1/meet/state`) on a timer and on
1006
+ * every hint. The four payload broadcasts (`mx.lobby.knock`, `mx.lobby.admission`,
1007
+ * `mx.room.lock`, `mx.room.recording`) are gone with their senders and receivers. The
1008
+ * server publishes the same event on the same topic (aidream `admission.room_topic`).
1009
+ */
1010
+ stateChanged: "mx.state.changed"
953
1011
  };
954
1012
 
955
1013
  // src/core/types.ts
@@ -963,14 +1021,19 @@ function isGuest(identity) {
963
1021
  return identity.userId === null;
964
1022
  }
965
1023
  var ROLE_RANK = {
966
- host: 3,
967
- cohost: 2,
1024
+ host: 4,
1025
+ cohost: 3,
1026
+ presenter: 2,
968
1027
  participant: 1,
969
- guest: 0
1028
+ viewer: 0
970
1029
  };
1030
+ function isLinkFollower(standing) {
1031
+ return standing === "guest" || standing === "outsider";
1032
+ }
971
1033
  function isHostRole(role) {
972
1034
  return role === "host" || role === "cohost";
973
1035
  }
1036
+ var WAITING_ADMISSION_STATES = ["waiting_host", "knocking"];
974
1037
 
975
1038
  // src/core/calls.ts
976
1039
  var TRANSITIONS = {
@@ -1520,15 +1583,15 @@ var CAPTION_LANGUAGES = [
1520
1583
  { tag: "id", name: "Indonesian" },
1521
1584
  { tag: "sv", name: "Swedish" }
1522
1585
  ];
1523
- async function loadMeetingActivities(api, meetingId, roomToken = null) {
1586
+ async function loadMeetingActivities(api, meetingId, meetingPass = null) {
1524
1587
  let response;
1525
1588
  try {
1526
1589
  response = await api.request({
1527
1590
  path: `${MEET_ROUTES.collab}?meeting_id=${encodeURIComponent(meetingId)}`,
1528
1591
  method: "GET",
1529
1592
  operation: "meet.loadMeetingActivities",
1530
- requireAuth: roomToken === null,
1531
- ...roomToken !== null ? { headers: { [ROOM_TOKEN_HEADER]: roomToken } } : {}
1593
+ requireAuth: meetingPass === null,
1594
+ ...meetingPass !== null ? { headers: { [MEETING_PASS_HEADER]: meetingPass } } : {}
1532
1595
  });
1533
1596
  } catch (error) {
1534
1597
  return {
@@ -1583,9 +1646,15 @@ var RPCS = {
1583
1646
  getOrCreateMeeting: "meet_get_or_create_meeting",
1584
1647
  /** Resolve a durable public link, before/during/after its scheduled time. */
1585
1648
  meetingBySlug: "meet_meeting_by_slug",
1586
- admit: "meet_admit_participant",
1649
+ /*
1650
+ * ── S4 ADMISSION (CORE-DESIGN §2.4). Each returns the decided row; the caller then
1651
+ * sends the payload-free `mx.state.changed` hint. Authorized by `who_may_admit`.
1652
+ */
1653
+ admit: "meet_admit",
1654
+ deny: "meet_deny",
1655
+ undeny: "meet_undeny",
1656
+ admitAll: "meet_admit_all",
1587
1657
  setLock: "meet_set_lock",
1588
- endMeeting: "meet_end_meeting",
1589
1658
  recordConsent: "meet_record_consent",
1590
1659
  /*
1591
1660
  * ── MEET WAVE 1 — management (0.8). Every write is a SECURITY DEFINER door
@@ -1627,13 +1696,13 @@ var RPCS = {
1627
1696
  /** Titles, agendas, current notes and final transcript lines; snippets mark hits with ⟦ ⟧. */
1628
1697
  search: "meet_search"
1629
1698
  };
1630
- var VOID_RPCS = [RPCS.admit, RPCS.recordConsent];
1699
+ var VOID_RPCS = [RPCS.recordConsent];
1631
1700
  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";
1632
1701
  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";
1633
1702
  var RECORDING_COLUMNS = "id,meeting_id,state,started_at,ended_at,started_by,file_id,failure_reason";
1634
1703
  var SEGMENT_COLUMNS = "id,meeting_id,identity,speaker,text,started_at,is_final,metadata";
1635
1704
  var NOTE_COLUMNS = "id,meeting_id,text,kind,assignee_user_id,created_at,metadata";
1636
- 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";
1705
+ 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";
1637
1706
  function projectStays(value) {
1638
1707
  if (!Array.isArray(value)) return [];
1639
1708
  const stays = [];
@@ -1999,8 +2068,17 @@ function createMeetRepository(options) {
1999
2068
  const operation = "meet.projectAttendee";
2000
2069
  const role = text(row, "role");
2001
2070
  const admission = text(row, "admission_state");
2002
- const known = ["host", "cohost", "participant", "guest"];
2003
- const states = ["knocking", "admitted", "denied", "removed"];
2071
+ const known = ["host", "cohost", "presenter", "participant", "viewer"];
2072
+ const states = [
2073
+ "waiting_host",
2074
+ "knocking",
2075
+ "admitted",
2076
+ "denied",
2077
+ "expired",
2078
+ "withdrawn",
2079
+ "removed",
2080
+ "ended"
2081
+ ];
2004
2082
  const userId = text(row, "participant_user_id");
2005
2083
  const identity = requireText(row, "identity", operation);
2006
2084
  return {
@@ -2014,6 +2092,8 @@ function createMeetRepository(options) {
2014
2092
  userId: userId === null ? null : asUserId(userId),
2015
2093
  role: known.includes(role) ? role : "participant",
2016
2094
  admissionState: states.includes(admission) ? admission : "admitted",
2095
+ knockedAt: text(row, "knocked_at"),
2096
+ knockExpiresAt: text(row, "knock_expires_at"),
2017
2097
  isAgent: bool(row, "is_agent", false),
2018
2098
  joinedAt: text(row, "joined_at"),
2019
2099
  leftAt: text(row, "left_at"),
@@ -2292,16 +2372,26 @@ function createMeetRepository(options) {
2292
2372
  hasMore: rows.length > limit
2293
2373
  };
2294
2374
  },
2295
- async admit({ meetingId, identity, admitted, byUserId }) {
2296
- await guardVoid(
2297
- "meet.admit",
2298
- () => db.rpc(RPCS.admit, {
2375
+ async decideAdmission({ meetingId, identity, decision, byUserId }) {
2376
+ const fn = decision === "admit" ? RPCS.admit : decision === "deny" ? RPCS.deny : RPCS.undeny;
2377
+ const row = await guard(
2378
+ `meet.${decision}`,
2379
+ () => db.rpc(fn, {
2299
2380
  p_meeting_id: meetingId,
2300
2381
  p_identity: identity,
2301
- p_admitted: admitted,
2302
2382
  p_by_user_id: byUserId
2303
2383
  })
2304
2384
  );
2385
+ return projectAttendee(row);
2386
+ },
2387
+ async admitAll(meetingId, byUserId) {
2388
+ const response = await db.rpc(RPCS.admitAll, {
2389
+ p_meeting_id: meetingId,
2390
+ p_by_user_id: byUserId
2391
+ });
2392
+ if (response.error !== null) throw normalizeMeetError(response.error, "meet.admitAll");
2393
+ const rows = Array.isArray(response.data) ? response.data : [];
2394
+ return rows.map(projectAttendee);
2305
2395
  },
2306
2396
  async setLock(meetingId, locked, byUserId) {
2307
2397
  const row = await guard(
@@ -2314,16 +2404,6 @@ function createMeetRepository(options) {
2314
2404
  );
2315
2405
  return projectMeeting(row);
2316
2406
  },
2317
- async endMeeting(meetingId, byUserId) {
2318
- const row = await guard(
2319
- "meet.endMeeting",
2320
- () => db.rpc(RPCS.endMeeting, {
2321
- p_meeting_id: meetingId,
2322
- p_by_user_id: byUserId
2323
- })
2324
- );
2325
- return projectMeeting(row);
2326
- },
2327
2407
  async transcript(meetingId, limit = 1e3) {
2328
2408
  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);
2329
2409
  if (response.error !== null) throw normalizeMeetError(response.error, "meet.transcript");
@@ -2652,12 +2732,14 @@ var DEFAULT_EFFECTS = {
2652
2732
  };
2653
2733
  var NO_GRANTS = {
2654
2734
  role: "participant",
2735
+ standing: null,
2655
2736
  canRecord: false,
2656
2737
  canPublish: false,
2657
2738
  canSubscribe: false
2658
2739
  };
2659
2740
  var EMPTY = {
2660
2741
  phase: "idle",
2742
+ waiting: null,
2661
2743
  meeting: null,
2662
2744
  localIdentity: null,
2663
2745
  grants: NO_GRANTS,
@@ -2698,6 +2780,11 @@ function effectiveRole(snapshot) {
2698
2780
  const fromToken = snapshot.grants.role;
2699
2781
  return ROLE_RANK[fromRoster] >= ROLE_RANK[fromToken] ? fromRoster : fromToken;
2700
2782
  }
2783
+ function effectiveStanding(snapshot) {
2784
+ if (snapshot.grants.standing != null) return snapshot.grants.standing;
2785
+ const me = snapshot.participants.find((p) => p.identity === snapshot.localIdentity);
2786
+ return me?.standing ?? null;
2787
+ }
2701
2788
  var REACTION_TTL_MS = 5e3;
2702
2789
  var CHAT_LIMIT = 200;
2703
2790
  function createMeetStore(initial = {}) {
@@ -3190,7 +3277,7 @@ function applyModeration(store, payload, now) {
3190
3277
  return ignored(`a moderation kind this version does not know: ${String(kind)}`);
3191
3278
  }
3192
3279
  }
3193
- var ROLES = ["host", "cohost", "participant", "guest"];
3280
+ var ROLES = ["host", "cohost", "presenter", "participant", "viewer"];
3194
3281
 
3195
3282
  // src/core/devices.ts
3196
3283
  var KINDS = ["audioinput", "videoinput", "audiooutput"];
@@ -3930,19 +4017,25 @@ function roleOf(participants, identity) {
3930
4017
  return participants.find((p) => p.identity === identity)?.role ?? "participant";
3931
4018
  }
3932
4019
  var END_MEETING_NOT_SERVED = "Ending a meeting is not available on this server yet";
3933
- function lobbyEntryFrom(attendee) {
3934
- if (attendee.admissionState !== "knocking") return null;
4020
+ function lobbyEntryFrom(attendee, now = Date.now()) {
4021
+ if (!WAITING_ADMISSION_STATES.includes(attendee.admissionState)) return null;
4022
+ const expires = attendee.knockExpiresAt ? Date.parse(attendee.knockExpiresAt) : Number.NaN;
4023
+ if (!Number.isNaN(expires) && expires <= now) return null;
3935
4024
  return {
3936
4025
  identity: attendee.identity,
3937
4026
  displayName: attendee.displayName,
3938
4027
  avatarUrl: attendee.avatarUrl,
3939
4028
  userId: attendee.userId,
3940
4029
  state: "knocking",
3941
- knockedAt: attendee.joinedAt ?? (/* @__PURE__ */ new Date()).toISOString(),
4030
+ knockedAt: attendee.knockedAt ?? attendee.joinedAt ?? (/* @__PURE__ */ new Date()).toISOString(),
4031
+ knockExpiresAt: attendee.knockExpiresAt ?? null,
3942
4032
  decidedAt: null,
3943
4033
  decidedBy: null
3944
4034
  };
3945
4035
  }
4036
+ function decidedState(state) {
4037
+ return WAITING_ADMISSION_STATES.includes(state) ? "expired" : state;
4038
+ }
3946
4039
  function createMeetingSession(options) {
3947
4040
  const { manager, repository, store, identity } = options;
3948
4041
  let channel = null;
@@ -3966,11 +4059,6 @@ function createMeetingSession(options) {
3966
4059
  }
3967
4060
  return identity.userId;
3968
4061
  }
3969
- function knockIdentity() {
3970
- return asParticipantIdentity(
3971
- identity.userId !== null ? `user:${identity.userId}` : `guest:${identity.deviceId}`
3972
- );
3973
- }
3974
4062
  function requireMeeting(operation) {
3975
4063
  const meeting = store.snapshot().meeting;
3976
4064
  if (meeting === null) {
@@ -3982,6 +4070,191 @@ function createMeetingSession(options) {
3982
4070
  }
3983
4071
  return meeting;
3984
4072
  }
4073
+ let polling = false;
4074
+ let pollTimer = null;
4075
+ let reading = null;
4076
+ let admittedOnce = false;
4077
+ let unlisten = null;
4078
+ function report(level, message, remedy) {
4079
+ options.onDiagnostic?.(remedy === void 0 ? { level, message } : { level, message, remedy });
4080
+ }
4081
+ function pollMs() {
4082
+ const seconds = store.snapshot().waiting?.pollSeconds ?? 3;
4083
+ return Math.max(1, Math.min(60, seconds)) * 1e3;
4084
+ }
4085
+ function schedule(ms2) {
4086
+ if (pollTimer !== null) clearTimeout(pollTimer);
4087
+ if (!polling) return;
4088
+ pollTimer = setTimeout(() => {
4089
+ pollTimer = null;
4090
+ void readState2().finally(() => schedule(pollMs()));
4091
+ }, ms2);
4092
+ }
4093
+ function listen() {
4094
+ if (unlisten !== null || typeof window === "undefined") return;
4095
+ const nudge = () => {
4096
+ if (typeof document !== "undefined" && document.visibilityState === "hidden") return;
4097
+ void readState2();
4098
+ };
4099
+ const goodbye = () => {
4100
+ const meeting = store.snapshot().meeting;
4101
+ const pass = options.resolvePass?.() ?? null;
4102
+ const waiting = store.snapshot().waiting;
4103
+ if (meeting === null || pass === null || waiting === null) return;
4104
+ if (!WAITING_ADMISSION_STATES.includes(waiting.admission)) return;
4105
+ try {
4106
+ navigator.sendBeacon?.(
4107
+ options.api.url(MEET_ROUTES.leave),
4108
+ new Blob([JSON.stringify({ meeting_id: meeting.id, intent: "withdraw", meeting_pass: pass })], {
4109
+ type: "application/json"
4110
+ })
4111
+ );
4112
+ } catch {
4113
+ }
4114
+ };
4115
+ window.addEventListener("online", nudge);
4116
+ window.addEventListener("pagehide", goodbye);
4117
+ document.addEventListener("visibilitychange", nudge);
4118
+ unlisten = () => {
4119
+ window.removeEventListener("online", nudge);
4120
+ window.removeEventListener("pagehide", goodbye);
4121
+ document.removeEventListener("visibilitychange", nudge);
4122
+ };
4123
+ }
4124
+ function stopWaiting() {
4125
+ polling = false;
4126
+ if (pollTimer !== null) clearTimeout(pollTimer);
4127
+ pollTimer = null;
4128
+ unlisten?.();
4129
+ unlisten = null;
4130
+ }
4131
+ function patchWaiting(next) {
4132
+ const current = store.snapshot().waiting;
4133
+ const base = current ?? {
4134
+ admission: "knocking",
4135
+ admittersPresent: null,
4136
+ reknockAllowed: true,
4137
+ knockExpiresAt: null,
4138
+ pollSeconds: 3,
4139
+ liveVersion: 0
4140
+ };
4141
+ store.patch({ waiting: { ...base, ...next } });
4142
+ }
4143
+ function apply(read) {
4144
+ const snapshot = store.snapshot();
4145
+ if (snapshot.phase !== "lobby") return;
4146
+ const admission = read.admission_state ?? null;
4147
+ const ended = admission === "ended" || read.session_phase === "ended" || read.meeting_phase === "ended";
4148
+ if (admission === "admitted" && !ended) {
4149
+ if (admittedOnce) return;
4150
+ admittedOnce = true;
4151
+ stopWaiting();
4152
+ options.onAdmitted?.();
4153
+ return;
4154
+ }
4155
+ if (ended) {
4156
+ stopWaiting();
4157
+ patchWaiting({ admission: "ended" });
4158
+ const meeting = snapshot.meeting;
4159
+ if (read.meeting_phase === "ended" && meeting !== null && meeting.endedAt === null) {
4160
+ store.patch({ meeting: { ...meeting, endedAt: (/* @__PURE__ */ new Date()).toISOString() } });
4161
+ }
4162
+ return;
4163
+ }
4164
+ if (admission === null) return;
4165
+ patchWaiting({
4166
+ admission,
4167
+ admittersPresent: read.admitters_present ?? null,
4168
+ reknockAllowed: read.reknock_allowed ?? true,
4169
+ knockExpiresAt: read.knock_expires_at ?? null,
4170
+ pollSeconds: read.poll_seconds ?? snapshot.waiting?.pollSeconds ?? 3,
4171
+ liveVersion: read.live_version ?? 0
4172
+ });
4173
+ if (admission === "denied" || admission === "expired" || admission === "withdrawn" || admission === "removed") {
4174
+ stopWaiting();
4175
+ }
4176
+ }
4177
+ async function readState2() {
4178
+ if (reading !== null) return reading;
4179
+ const meeting = store.snapshot().meeting;
4180
+ const pass = options.resolvePass?.() ?? null;
4181
+ if (meeting === null || pass === null) return;
4182
+ reading = (async () => {
4183
+ try {
4184
+ const response = await options.api.request({
4185
+ path: `${MEET_ROUTES.state}?meeting_id=${encodeURIComponent(meeting.id)}`,
4186
+ method: "GET",
4187
+ requireAuth: false,
4188
+ operation: "meet.state",
4189
+ headers: { [MEETING_PASS_HEADER]: pass }
4190
+ });
4191
+ if (!response.ok) {
4192
+ let body = null;
4193
+ try {
4194
+ body = await response.json();
4195
+ } catch {
4196
+ body = null;
4197
+ }
4198
+ const refusal2 = readServerRefusal(body);
4199
+ report(
4200
+ "warn",
4201
+ refusal2?.reason ?? `meet.state: the server answered ${response.status}`,
4202
+ refusal2?.remedy ?? "The waiting room keeps asking; nothing on this screen changes until the server answers."
4203
+ );
4204
+ return;
4205
+ }
4206
+ apply(await response.json());
4207
+ } catch (error) {
4208
+ report("info", `meet.state: ${String(error)}`, "Waiting continues; the state is read again shortly.");
4209
+ } finally {
4210
+ reading = null;
4211
+ }
4212
+ })();
4213
+ return reading;
4214
+ }
4215
+ async function postLeave(intent) {
4216
+ const meeting = requireMeeting(`meet.${intent}`);
4217
+ const pass = options.resolvePass?.() ?? null;
4218
+ const response = await options.api.request({
4219
+ path: MEET_ROUTES.leave,
4220
+ method: "POST",
4221
+ requireAuth: false,
4222
+ operation: `meet.${intent}`,
4223
+ body: { meeting_id: meeting.id, intent },
4224
+ ...pass !== null ? { headers: { [MEETING_PASS_HEADER]: pass } } : {}
4225
+ });
4226
+ if (!response.ok) {
4227
+ let body = null;
4228
+ try {
4229
+ body = await response.json();
4230
+ } catch {
4231
+ body = null;
4232
+ }
4233
+ const refusal2 = readServerRefusal(body);
4234
+ throw new MeetError(
4235
+ "transport",
4236
+ refusal2?.reason ?? `meet.${intent}: the server answered ${response.status}`,
4237
+ refusal2?.remedy ?? "You are still in the waiting room. Try again."
4238
+ );
4239
+ }
4240
+ }
4241
+ function hint() {
4242
+ channel?.send(MEET_EVENTS.stateChanged, { v: null });
4243
+ }
4244
+ async function refreshQueue() {
4245
+ const meeting = store.snapshot().meeting;
4246
+ if (meeting === null || isGuest(identity) || !isHostRole(effectiveRole(store.snapshot()))) return;
4247
+ try {
4248
+ const attendees = await repository.attendees(meeting.id);
4249
+ store.learn(attendees);
4250
+ for (const attendee of attendees) {
4251
+ const entry = lobbyEntryFrom(attendee);
4252
+ if (entry !== null) store.upsertLobby(entry);
4253
+ else store.setAdmission(attendee.identity, decidedState(attendee.admissionState), null);
4254
+ }
4255
+ } catch {
4256
+ }
4257
+ }
3985
4258
  return {
3986
4259
  watch(roomName) {
3987
4260
  channel?.close();
@@ -3989,58 +4262,22 @@ function createMeetingSession(options) {
3989
4262
  topic: roomTopic(roomName),
3990
4263
  broadcast: [
3991
4264
  {
3992
- event: MEET_EVENTS.knock,
3993
- onMessage: (message) => {
3994
- store.upsertLobby(message.data);
3995
- }
3996
- },
3997
- {
3998
- event: MEET_EVENTS.admission,
3999
- onMessage: (message) => {
4000
- const verdict = message.data;
4001
- const decided = asParticipantIdentity(verdict.identity);
4002
- store.setAdmission(
4003
- decided,
4004
- verdict.admitted ? "admitted" : "denied",
4005
- verdict.by === null ? null : verdict.by
4006
- );
4007
- if (decided === knockIdentity()) {
4008
- if (verdict.admitted) options.onAdmitted?.();
4009
- else options.onDenied?.();
4010
- }
4011
- }
4012
- },
4013
- {
4014
- event: MEET_EVENTS.lock,
4015
- onMessage: (message) => {
4016
- const { locked } = message.data;
4017
- const meeting = store.snapshot().meeting;
4018
- store.patch({
4019
- locked,
4020
- ...meeting !== null ? { meeting: { ...meeting, locked } } : {}
4021
- });
4022
- }
4023
- },
4024
- {
4025
- event: MEET_EVENTS.recording,
4026
- onMessage: (message) => {
4027
- const payload = message.data;
4028
- store.setRecording(
4029
- payload.state,
4030
- payload.by === null ? null : payload.by,
4031
- "room"
4032
- );
4265
+ // 🚨 PAYLOAD-FREE. Every receiver reads again; a spoofed hint only causes a read.
4266
+ event: MEET_EVENTS.stateChanged,
4267
+ onMessage: () => {
4268
+ if (store.snapshot().phase === "lobby") void readState2();
4269
+ void refreshQueue();
4033
4270
  }
4034
4271
  }
4035
4272
  ],
4036
- // THE BACKFILL DOOR. A host whose socket blipped must not miss a knock:
4037
- // re-reading the meeting restores lock/recording truth, and knockers
4038
- // re-announce on their own timer.
4273
+ // THE BACKFILL DOOR. A socket blip must not lose a decision: re-read the meeting
4274
+ // row, and a waiting tab re-reads its own state.
4039
4275
  onBackfill: async () => {
4040
4276
  const meeting = store.snapshot().meeting;
4041
4277
  if (meeting === null) return;
4042
4278
  const fresh = isGuest(identity) ? await repository.meetingBySlug(meeting.slug) : await readMeeting(meeting.id);
4043
4279
  store.patch({ meeting: fresh, locked: fresh.locked });
4280
+ if (store.snapshot().phase === "lobby") void readState2();
4044
4281
  }
4045
4282
  });
4046
4283
  },
@@ -4101,7 +4338,7 @@ function createMeetingSession(options) {
4101
4338
  store.upsertLobby(entry);
4102
4339
  return;
4103
4340
  }
4104
- store.setAdmission(attendee.identity, attendee.admissionState, null);
4341
+ store.setAdmission(attendee.identity, decidedState(attendee.admissionState), null);
4105
4342
  }
4106
4343
  },
4107
4344
  {
@@ -4153,62 +4390,75 @@ function createMeetingSession(options) {
4153
4390
  if (entry !== null) {
4154
4391
  store.upsertLobby(entry);
4155
4392
  } else {
4156
- store.setAdmission(attendee.identity, attendee.admissionState, null);
4393
+ store.setAdmission(attendee.identity, decidedState(attendee.admissionState), null);
4157
4394
  }
4158
4395
  }
4159
4396
  }
4160
4397
  });
4161
4398
  meetingChannel.requestBackfill("manual");
4162
4399
  },
4163
- knock(roomName) {
4164
- const handle = channel ?? manager.open({ topic: roomTopic(roomName) });
4165
- channel = handle;
4166
- const entry = {
4167
- identity: knockIdentity(),
4168
- displayName: identity.displayName,
4169
- avatarUrl: identity.avatarUrl,
4170
- userId: identity.userId,
4171
- state: "knocking",
4172
- knockedAt: (/* @__PURE__ */ new Date()).toISOString(),
4173
- decidedAt: null,
4174
- decidedBy: null
4175
- };
4176
- store.upsertLobby(entry);
4177
- handle.send(MEET_EVENTS.knock, entry);
4178
- },
4179
4400
  async admit(entry, admitted) {
4180
4401
  const by = requireHostUser("meet.admit");
4181
4402
  const meeting = requireMeeting("meet.admit");
4182
- await repository.admit({
4403
+ const row = await repository.decideAdmission({
4183
4404
  meetingId: meeting.id,
4184
4405
  identity: entry.identity,
4185
- admitted,
4406
+ decision: admitted ? "admit" : "deny",
4186
4407
  byUserId: by
4187
4408
  });
4188
4409
  store.upsertLobby({
4189
4410
  ...entry,
4190
- state: admitted ? "admitted" : "denied",
4411
+ state: decidedState(row.admissionState),
4191
4412
  decidedAt: (/* @__PURE__ */ new Date()).toISOString(),
4192
4413
  decidedBy: by
4193
4414
  });
4194
- channel?.send(MEET_EVENTS.admission, {
4415
+ hint();
4416
+ },
4417
+ async undeny(entry) {
4418
+ const by = requireHostUser("meet.undeny");
4419
+ const meeting = requireMeeting("meet.undeny");
4420
+ const row = await repository.decideAdmission({
4421
+ meetingId: meeting.id,
4195
4422
  identity: entry.identity,
4196
- admitted,
4197
- by
4423
+ decision: "undeny",
4424
+ byUserId: by
4198
4425
  });
4426
+ store.upsertLobby({ ...entry, state: decidedState(row.admissionState), decidedAt: (/* @__PURE__ */ new Date()).toISOString(), decidedBy: by });
4427
+ hint();
4199
4428
  },
4200
4429
  async admitAll() {
4201
- const knocking = store.snapshot().lobby.filter((entry) => entry.state === "knocking");
4202
- for (const entry of knocking) {
4203
- await this.admit(entry, true);
4430
+ const by = requireHostUser("meet.admitAll");
4431
+ const meeting = requireMeeting("meet.admitAll");
4432
+ const rows = await repository.admitAll(meeting.id, by);
4433
+ for (const row of rows) {
4434
+ store.setAdmission(row.identity, row.admissionState, by);
4204
4435
  }
4436
+ hint();
4437
+ },
4438
+ startWaiting() {
4439
+ if (polling) return;
4440
+ polling = true;
4441
+ admittedOnce = false;
4442
+ listen();
4443
+ schedule(0);
4444
+ },
4445
+ stopWaiting,
4446
+ readState: readState2,
4447
+ async withdraw() {
4448
+ stopWaiting();
4449
+ await postLeave("withdraw");
4450
+ patchWaiting({ admission: "withdrawn" });
4451
+ hint();
4452
+ },
4453
+ async askAgain() {
4454
+ await options.rejoin?.();
4205
4455
  },
4206
4456
  async setLock(locked) {
4207
4457
  const by = requireHostUser("meet.setLock");
4208
4458
  const meeting = requireMeeting("meet.setLock");
4209
4459
  const updated = await repository.setLock(meeting.id, locked, by);
4210
4460
  store.patch({ meeting: updated, locked: updated.locked });
4211
- channel?.send(MEET_EVENTS.lock, { locked: updated.locked });
4461
+ hint();
4212
4462
  return updated;
4213
4463
  },
4214
4464
  /**
@@ -4266,6 +4516,7 @@ function createMeetingSession(options) {
4266
4516
  return { endedAt, served: true };
4267
4517
  },
4268
4518
  dispose() {
4519
+ stopWaiting();
4269
4520
  channel?.close();
4270
4521
  channel = null;
4271
4522
  meetingChannel?.close();
@@ -4385,7 +4636,6 @@ function kindOf(person) {
4385
4636
  if (person.isAgent) return "assistant";
4386
4637
  if (person.role === "host") return "host";
4387
4638
  if (person.role === "cohost") return "cohost";
4388
- if (person.role === "guest") return person.userId !== null ? "signed-in guest" : "guest";
4389
4639
  if (person.userId === null) return "guest";
4390
4640
  return "member";
4391
4641
  }
@@ -4670,7 +4920,7 @@ function meetingLength(meeting) {
4670
4920
  if (start === null || end === null || end < start) return null;
4671
4921
  return formatDurationMs(end - start, { style: "long" });
4672
4922
  }
4673
- async function loadMeetingChat(api, meetingId, roomToken = null) {
4923
+ async function loadMeetingChat(api, meetingId, meetingPass = null) {
4674
4924
  const operation = "meet.loadMeetingChat";
4675
4925
  let response;
4676
4926
  try {
@@ -4678,8 +4928,8 @@ async function loadMeetingChat(api, meetingId, roomToken = null) {
4678
4928
  path: `${MEET_ROUTES.chat}?meeting_id=${encodeURIComponent(meetingId)}`,
4679
4929
  method: "GET",
4680
4930
  operation,
4681
- requireAuth: roomToken === null,
4682
- ...roomToken !== null ? { headers: { [ROOM_TOKEN_HEADER]: roomToken } } : {}
4931
+ requireAuth: meetingPass === null,
4932
+ ...meetingPass !== null ? { headers: { [MEETING_PASS_HEADER]: meetingPass } } : {}
4683
4933
  });
4684
4934
  } catch (error) {
4685
4935
  return {
@@ -4823,7 +5073,12 @@ async function pushMeetingToGoogleCalendar(api, meetingId, target) {
4823
5073
  // src/core/room.ts
4824
5074
  var PARTICIPANT_ROLE_ATTRIBUTE = "matrx.participant.role";
4825
5075
  var PARTICIPANT_CAN_RECORD_ATTRIBUTE = "matrx.participant.can_record";
4826
- var KNOWN_ROLES = ["host", "cohost", "participant", "guest"];
5076
+ var PARTICIPANT_STANDING_ATTRIBUTE = "matrx.participant.standing";
5077
+ var KNOWN_STANDINGS = ["member", "invited", "outsider", "guest"];
5078
+ function parseStanding(value) {
5079
+ return KNOWN_STANDINGS.includes(value) ? value : null;
5080
+ }
5081
+ var KNOWN_ROLES = ["host", "cohost", "presenter", "participant", "viewer"];
4827
5082
  function parseRole(value) {
4828
5083
  return KNOWN_ROLES.includes(value) ? value : "participant";
4829
5084
  }
@@ -4857,6 +5112,9 @@ function projectParticipant(driver, joinedAt) {
4857
5112
  // ATTRIBUTE FIRST, metadata second — see the constants above. Both are the
4858
5113
  // server's; the attribute is simply the one that always arrives.
4859
5114
  role: parseRole(driver.attributes[PARTICIPANT_ROLE_ATTRIBUTE] ?? metadata.role),
5115
+ standing: parseStanding(
5116
+ driver.attributes[PARTICIPANT_STANDING_ATTRIBUTE] ?? metadata.standing
5117
+ ),
4860
5118
  canRecord: driver.attributes[PARTICIPANT_CAN_RECORD_ATTRIBUTE] === "true" || metadata.can_record === true,
4861
5119
  isLocal: driver.isLocal,
4862
5120
  isAgent: metadata.is_agent === true,
@@ -4929,7 +5187,7 @@ function createRoomEngine(options) {
4929
5187
  const normalized = normalizeMeetError(error, operation);
4930
5188
  store.patch({
4931
5189
  phase: "failed",
4932
- failure: { message: normalized.message, remedy: normalized.remedy }
5190
+ failure: { message: normalized.message, remedy: normalized.remedy, reason: normalized.reason }
4933
5191
  });
4934
5192
  report(
4935
5193
  normalized.code === "session-unavailable" ? "warn" : "error",
@@ -5119,7 +5377,7 @@ function createRoomEngine(options) {
5119
5377
  }
5120
5378
  };
5121
5379
  async function connect(request) {
5122
- store.patch({ phase: "requesting-token", failure: null });
5380
+ store.patch({ phase: "requesting-token", failure: null, waiting: null });
5123
5381
  const token = await tokens.get({
5124
5382
  roomName: request.roomName,
5125
5383
  identity,
@@ -5133,16 +5391,27 @@ function createRoomEngine(options) {
5133
5391
  breakoutRoomId: token.breakoutRoomId ?? null,
5134
5392
  grants: {
5135
5393
  role: token.role,
5394
+ standing: token.standing,
5136
5395
  canRecord: token.canRecord,
5137
5396
  canPublish: token.canPublish,
5138
5397
  canSubscribe: token.canSubscribe
5139
5398
  }
5140
5399
  });
5141
- if (token.lobby) {
5142
- store.patch({ phase: "lobby" });
5400
+ if (token.lobby || !token.canSubscribe) {
5401
+ store.patch({
5402
+ phase: "lobby",
5403
+ waiting: {
5404
+ admission: token.admissionState !== void 0 && token.admissionState !== "admitted" ? token.admissionState : "knocking",
5405
+ admittersPresent: null,
5406
+ reknockAllowed: true,
5407
+ knockExpiresAt: null,
5408
+ pollSeconds: token.pollSeconds ?? 3,
5409
+ liveVersion: 0
5410
+ }
5411
+ });
5143
5412
  return;
5144
5413
  }
5145
- store.patch({ phase: "connecting" });
5414
+ store.patch({ phase: "connecting", waiting: null });
5146
5415
  const created = await options.driverFactory(handlers);
5147
5416
  if (disposed) {
5148
5417
  await created.disconnect();
@@ -5478,7 +5747,7 @@ function createInRoomClient(options) {
5478
5747
  }
5479
5748
  function headers() {
5480
5749
  const token = options.resolveRoomToken();
5481
- return token === null ? void 0 : { [ROOM_TOKEN_HEADER]: token };
5750
+ return token === null ? void 0 : { [MEETING_PASS_HEADER]: token };
5482
5751
  }
5483
5752
  function refusal2(response, operation, remedy) {
5484
5753
  return refusalFromResponse(response, operation, remedy);
@@ -5959,7 +6228,7 @@ function summarizeMeetingRecordRestrictions(restrictions) {
5959
6228
  async function loadGuestMeetingRecord(args) {
5960
6229
  const { api, repository, meetingId, meeting, roomToken } = args;
5961
6230
  const session = args.session === true;
5962
- if (roomToken === null && !session) {
6231
+ if (roomToken?.meetingPass == null && !session) {
5963
6232
  return refusedBundle(
5964
6233
  meeting,
5965
6234
  "This meeting's record is only readable by the people who were in it.",
@@ -5969,12 +6238,12 @@ async function loadGuestMeetingRecord(args) {
5969
6238
  let response;
5970
6239
  try {
5971
6240
  response = await api.request({
5972
- path: `${MEET_ROUTES.record}?meeting_id=${encodeURIComponent(meetingId)}`,
6241
+ path: `${MEET_ROUTES.record}?meeting_id=${encodeURIComponent(meetingId)}` + (args.sessionId ? `&session_id=${encodeURIComponent(args.sessionId)}` : ""),
5973
6242
  method: "GET",
5974
6243
  // A GUEST HAS NO SESSION AND IS STILL A FIRST-CLASS READER (D6).
5975
6244
  requireAuth: session,
5976
6245
  operation: "meet.record",
5977
- headers: roomToken === null ? {} : { [ROOM_TOKEN_HEADER]: roomToken.token }
6246
+ headers: roomToken?.meetingPass != null ? { [MEETING_PASS_HEADER]: roomToken.meetingPass } : {}
5978
6247
  });
5979
6248
  } catch (error) {
5980
6249
  return refusedBundle(
@@ -6362,6 +6631,10 @@ function takeMoveRequest(store) {
6362
6631
  }
6363
6632
 
6364
6633
  // src/core/observation.ts
6634
+ function observedRole(role, standing) {
6635
+ if (role === "host" || role === "cohost") return role;
6636
+ return isLinkFollower(standing) ? "guest" : "participant";
6637
+ }
6365
6638
  function observedDevice(track) {
6366
6639
  if (track.enabled) return "on";
6367
6640
  const refused = track.unavailable;
@@ -6389,13 +6662,15 @@ function observedPhase(input) {
6389
6662
  const raw = snapshot.phase;
6390
6663
  const live = raw === "connected" || raw === "reconnecting";
6391
6664
  if (endedRecord(snapshot, input.meeting ?? null) && !live) return "ended";
6665
+ const reason = raw === "failed" ? snapshot.failure?.reason ?? null : null;
6666
+ if (reason !== null) return reason === "ended" ? "ended" : `refused:${reason}`;
6392
6667
  switch (raw) {
6393
6668
  case "idle":
6394
6669
  case "failed":
6395
6670
  case "left":
6396
6671
  return "prejoin";
6397
6672
  case "lobby":
6398
- return "waiting:knocking";
6673
+ return waitingPhase(snapshot);
6399
6674
  case "requesting-token":
6400
6675
  case "connecting":
6401
6676
  return "joining";
@@ -6404,20 +6679,39 @@ function observedPhase(input) {
6404
6679
  return "in_call";
6405
6680
  }
6406
6681
  }
6682
+ function waitingPhase(snapshot) {
6683
+ switch (snapshot.waiting?.admission) {
6684
+ case "waiting_host":
6685
+ return "waiting:host_not_started";
6686
+ case "denied":
6687
+ return "denied";
6688
+ case "expired":
6689
+ return "knock_expired";
6690
+ case "withdrawn":
6691
+ return "left";
6692
+ case "ended":
6693
+ return "ended";
6694
+ case "removed":
6695
+ return "refused:removed";
6696
+ default:
6697
+ return "waiting:knocking";
6698
+ }
6699
+ }
6407
6700
  var ADMITTER_ROLES = /* @__PURE__ */ new Set(["host", "cohost"]);
6408
6701
  function observe(input) {
6409
6702
  const { snapshot } = input;
6410
6703
  const phase = observedPhase(input);
6411
6704
  const inCall = phase === "in_call";
6412
- const role = inCall && snapshot !== null ? effectiveRole(snapshot) : "none";
6413
- const canAdmit = role !== "none" && ADMITTER_ROLES.has(role);
6705
+ const actual = inCall && snapshot !== null ? effectiveRole(snapshot) : "none";
6706
+ const canAdmit = actual !== "none" && ADMITTER_ROLES.has(actual);
6707
+ const role = actual === "none" || snapshot === null ? "none" : observedRole(actual, effectiveStanding(snapshot));
6414
6708
  const deviceInPlay = phase === "joining" || inCall;
6415
6709
  return {
6416
6710
  phase,
6417
- reason: null,
6711
+ reason: phase.startsWith("refused:") ? phase.slice("refused:".length) : null,
6418
6712
  role,
6419
6713
  connection: snapshot?.phase === "reconnecting" ? "reconnecting" : "stable",
6420
- hostPresent: inCall && snapshot !== null ? snapshot.participants.some((p) => ADMITTER_ROLES.has(p.role)) : null,
6714
+ hostPresent: inCall && snapshot !== null ? snapshot.participants.some((p) => ADMITTER_ROLES.has(p.role)) : snapshot?.phase === "lobby" ? snapshot.waiting?.admittersPresent ?? null : null,
6421
6715
  lobbyCount: canAdmit && snapshot !== null ? snapshot.lobby.filter((entry) => entry.state === "knocking").length : 0,
6422
6716
  recording: snapshot?.recording === "recording" || snapshot?.recording === "stopping",
6423
6717
  audioBlocked: null,
@@ -6480,7 +6774,7 @@ function participantAttributes(p) {
6480
6774
  "data-meet-participant": "",
6481
6775
  "data-meet-participant-identity": p.identity,
6482
6776
  "data-meet-participant-name": p.displayName,
6483
- "data-meet-participant-role": p.role,
6777
+ "data-meet-participant-role": observedRole(p.role, p.standing),
6484
6778
  "data-meet-participant-self": String(p.isLocal)
6485
6779
  };
6486
6780
  const quality = observedQuality(p.quality);
@@ -6513,6 +6807,7 @@ export {
6513
6807
  LISTEN_ONLY_NOTICE,
6514
6808
  MEETING_INVITE_KIND,
6515
6809
  MEETING_INVITE_VERSION,
6810
+ MEETING_PASS_HEADER,
6516
6811
  MEETING_RECORD_GRACE_MS,
6517
6812
  MEET_ANSWER_CONTEXT_EVENT,
6518
6813
  MEET_ANSWER_EVENTS,
@@ -6528,12 +6823,12 @@ export {
6528
6823
  ORGANIZATION_HEADER,
6529
6824
  PARTICIPANT_CAN_RECORD_ATTRIBUTE,
6530
6825
  PARTICIPANT_ROLE_ATTRIBUTE,
6826
+ PARTICIPANT_STANDING_ATTRIBUTE,
6531
6827
  REACTION_TTL_MS,
6532
6828
  REFUSED_ABSENCE,
6533
6829
  ROLE_RANK,
6534
6830
  ROOM_EVENTS,
6535
6831
  ROOM_EVENT_PREFIX,
6536
- ROOM_TOKEN_HEADER,
6537
6832
  RPCS,
6538
6833
  TABLES,
6539
6834
  TRANSCRIPTION_ATTRIBUTES,
@@ -6604,6 +6899,7 @@ export {
6604
6899
  isGuest,
6605
6900
  isGuestIdentity,
6606
6901
  isHostRole,
6902
+ isLinkFollower,
6607
6903
  isListenOnly,
6608
6904
  isNoteTaker,
6609
6905
  isRealName,
@@ -6622,6 +6918,7 @@ export {
6622
6918
  meetingInviteAction,
6623
6919
  meetingLength,
6624
6920
  meetingLink,
6921
+ meetingPassHeaders,
6625
6922
  meetingPath,
6626
6923
  meetingSlug,
6627
6924
  meetingTopic,
@@ -6665,6 +6962,7 @@ export {
6665
6962
  recordingLabel,
6666
6963
  recordingNotice,
6667
6964
  recordingOffsetMs,
6965
+ rememberedMeetingPass,
6668
6966
  roleOf,
6669
6967
  roomTopic,
6670
6968
  rootAttributes,