@ai-matrx/meet 0.9.0 → 0.10.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs CHANGED
@@ -534,7 +534,17 @@ var MEET_ROUTES = {
534
534
  */
535
535
  collab: "/api/v1/meet/collab",
536
536
  /** Meet wave 5: translate finished caption lines into this viewer's language. */
537
- captionsTranslate: "/api/v1/meet/captions/translate"
537
+ captionsTranslate: "/api/v1/meet/captions/translate",
538
+ /**
539
+ * S4 — THE STATE READ (CORE-DESIGN §2.6): `GET ?meeting_id=` with `x-meet-pass`. This
540
+ * person's admission (lazy expiry applied), whether anyone who can admit is here, the run's
541
+ * phase. Read every `waiting_poll_seconds` while waiting and on every `mx.state.changed`.
542
+ */
543
+ state: "/api/v1/meet/state",
544
+ /** S4: `withdraw` (stop waiting) or `leave` (close this tab); the pass may ride in the body. */
545
+ leave: "/api/v1/meet/leave",
546
+ /** S4: a pass holder (guests included) saw the recording notice, for this run. */
547
+ consent: "/api/v1/meet/consent"
538
548
  };
539
549
  function meetingPath(template, meetingId) {
540
550
  return template.replace("{meeting_id}", encodeURIComponent(meetingId));
@@ -608,6 +618,8 @@ function parseRoomToken(body, roomName) {
608
618
  meetingPassExpiresAt: readString(payload, "meeting_pass_expires_at"),
609
619
  sessionId: readString(payload, "session_id"),
610
620
  lobby: readBoolean(payload, "lobby", false),
621
+ admissionState: readString(payload, "admission_state") ?? void 0,
622
+ pollSeconds: typeof payload["poll_seconds"] === "number" ? payload["poll_seconds"] : void 0,
611
623
  breakoutRoomId: readString(payload, "breakout_room_id") ?? null
612
624
  };
613
625
  }
@@ -1220,14 +1232,15 @@ var MEET_EVENTS = {
1220
1232
  cancel: "mx.call.cancel",
1221
1233
  /** You answered (or declined) on another device. Stop ringing here. */
1222
1234
  settled: "mx.call.settled",
1223
- /** Someone is in the lobby. Hosts render the knock. */
1224
- knock: "mx.lobby.knock",
1225
- /** A host decided. The knocker learns it here — they have no DB read. */
1226
- admission: "mx.lobby.admission",
1227
- /** The room was locked or unlocked. */
1228
- lock: "mx.room.lock",
1229
- /** Recording started or stopped. EVERY participant renders the indicator. */
1230
- recording: "mx.room.recording"
1235
+ /**
1236
+ * 🚨 S4 (CORE-DESIGN §2.6): the ONE room-channel event — payload-free "something changed,
1237
+ * read again". Nothing a person must see rides it: a waiting person learns admitted /
1238
+ * denied / expired / ended by READING their row (`GET /v1/meet/state`) on a timer and on
1239
+ * every hint. The four payload broadcasts (`mx.lobby.knock`, `mx.lobby.admission`,
1240
+ * `mx.room.lock`, `mx.room.recording`) are gone with their senders and receivers. The
1241
+ * server publishes the same event on the same topic (aidream `admission.room_topic`).
1242
+ */
1243
+ stateChanged: "mx.state.changed"
1231
1244
  };
1232
1245
 
1233
1246
  // src/core/types.ts
@@ -1253,6 +1266,7 @@ function isLinkFollower(standing) {
1253
1266
  function isHostRole(role) {
1254
1267
  return role === "host" || role === "cohost";
1255
1268
  }
1269
+ var WAITING_ADMISSION_STATES = ["waiting_host", "knocking"];
1256
1270
 
1257
1271
  // src/core/calls.ts
1258
1272
  var TRANSITIONS = {
@@ -1865,7 +1879,14 @@ var RPCS = {
1865
1879
  getOrCreateMeeting: "meet_get_or_create_meeting",
1866
1880
  /** Resolve a durable public link, before/during/after its scheduled time. */
1867
1881
  meetingBySlug: "meet_meeting_by_slug",
1868
- admit: "meet_admit_participant",
1882
+ /*
1883
+ * ── S4 ADMISSION (CORE-DESIGN §2.4). Each returns the decided row; the caller then
1884
+ * sends the payload-free `mx.state.changed` hint. Authorized by `who_may_admit`.
1885
+ */
1886
+ admit: "meet_admit",
1887
+ deny: "meet_deny",
1888
+ undeny: "meet_undeny",
1889
+ admitAll: "meet_admit_all",
1869
1890
  setLock: "meet_set_lock",
1870
1891
  recordConsent: "meet_record_consent",
1871
1892
  /*
@@ -1908,13 +1929,13 @@ var RPCS = {
1908
1929
  /** Titles, agendas, current notes and final transcript lines; snippets mark hits with ⟦ ⟧. */
1909
1930
  search: "meet_search"
1910
1931
  };
1911
- var VOID_RPCS = [RPCS.admit, RPCS.recordConsent];
1932
+ var VOID_RPCS = [RPCS.recordConsent];
1912
1933
  var MEETING_COLUMNS = "id,organization_id,room_name,slug,title,kind,host_user_id,scheduled_for,scheduled_duration_minutes,started_at,ended_at,locked,lobby_enabled,recording_policy,ai_enabled,metadata,time_zone,agenda,recurrence_rule,join_before_host,cancelled_at,cancellation_reason,deleted_at,calendar_sequence,version";
1913
1934
  var INVITEE_COLUMNS = "id,meeting_id,invitee_user_id,email,display_name,role,rsvp_state,responded_at,rsvp_note,last_sent_sequence,last_sent_at";
1914
1935
  var RECORDING_COLUMNS = "id,meeting_id,state,started_at,ended_at,started_by,file_id,failure_reason";
1915
1936
  var SEGMENT_COLUMNS = "id,meeting_id,identity,speaker,text,started_at,is_final,metadata";
1916
1937
  var NOTE_COLUMNS = "id,meeting_id,text,kind,assignee_user_id,created_at,metadata";
1917
- var ATTENDEE_COLUMNS = "id,meeting_id,identity,participant_user_id,display_name,avatar_url,role,admission_state,is_agent,joined_at,left_at,sessions:metadata->sessions";
1938
+ var ATTENDEE_COLUMNS = "id,meeting_id,identity,participant_user_id,display_name,avatar_url,role,admission_state,knocked_at,knock_expires_at,is_agent,joined_at,left_at,sessions:metadata->sessions";
1918
1939
  function projectStays(value) {
1919
1940
  if (!Array.isArray(value)) return [];
1920
1941
  const stays = [];
@@ -2281,7 +2302,16 @@ function createMeetRepository(options) {
2281
2302
  const role = text(row, "role");
2282
2303
  const admission = text(row, "admission_state");
2283
2304
  const known = ["host", "cohost", "presenter", "participant", "viewer"];
2284
- const states = ["knocking", "admitted", "denied", "removed"];
2305
+ const states = [
2306
+ "waiting_host",
2307
+ "knocking",
2308
+ "admitted",
2309
+ "denied",
2310
+ "expired",
2311
+ "withdrawn",
2312
+ "removed",
2313
+ "ended"
2314
+ ];
2285
2315
  const userId = text(row, "participant_user_id");
2286
2316
  const identity = requireText(row, "identity", operation);
2287
2317
  return {
@@ -2295,6 +2325,8 @@ function createMeetRepository(options) {
2295
2325
  userId: userId === null ? null : asUserId(userId),
2296
2326
  role: known.includes(role) ? role : "participant",
2297
2327
  admissionState: states.includes(admission) ? admission : "admitted",
2328
+ knockedAt: text(row, "knocked_at"),
2329
+ knockExpiresAt: text(row, "knock_expires_at"),
2298
2330
  isAgent: bool(row, "is_agent", false),
2299
2331
  joinedAt: text(row, "joined_at"),
2300
2332
  leftAt: text(row, "left_at"),
@@ -2573,16 +2605,26 @@ function createMeetRepository(options) {
2573
2605
  hasMore: rows.length > limit
2574
2606
  };
2575
2607
  },
2576
- async admit({ meetingId, identity, admitted, byUserId }) {
2577
- await guardVoid(
2578
- "meet.admit",
2579
- () => db.rpc(RPCS.admit, {
2608
+ async decideAdmission({ meetingId, identity, decision, byUserId }) {
2609
+ const fn = decision === "admit" ? RPCS.admit : decision === "deny" ? RPCS.deny : RPCS.undeny;
2610
+ const row = await guard(
2611
+ `meet.${decision}`,
2612
+ () => db.rpc(fn, {
2580
2613
  p_meeting_id: meetingId,
2581
2614
  p_identity: identity,
2582
- p_admitted: admitted,
2583
2615
  p_by_user_id: byUserId
2584
2616
  })
2585
2617
  );
2618
+ return projectAttendee(row);
2619
+ },
2620
+ async admitAll(meetingId, byUserId) {
2621
+ const response = await db.rpc(RPCS.admitAll, {
2622
+ p_meeting_id: meetingId,
2623
+ p_by_user_id: byUserId
2624
+ });
2625
+ if (response.error !== null) throw normalizeMeetError(response.error, "meet.admitAll");
2626
+ const rows = Array.isArray(response.data) ? response.data : [];
2627
+ return rows.map(projectAttendee);
2586
2628
  },
2587
2629
  async setLock(meetingId, locked, byUserId) {
2588
2630
  const row = await guard(
@@ -2930,6 +2972,7 @@ var NO_GRANTS = {
2930
2972
  };
2931
2973
  var EMPTY = {
2932
2974
  phase: "idle",
2975
+ waiting: null,
2933
2976
  meeting: null,
2934
2977
  localIdentity: null,
2935
2978
  grants: NO_GRANTS,
@@ -4207,19 +4250,25 @@ function roleOf(participants, identity) {
4207
4250
  return participants.find((p) => p.identity === identity)?.role ?? "participant";
4208
4251
  }
4209
4252
  var END_MEETING_NOT_SERVED = "Ending a meeting is not available on this server yet";
4210
- function lobbyEntryFrom(attendee) {
4211
- if (attendee.admissionState !== "knocking") return null;
4253
+ function lobbyEntryFrom(attendee, now = Date.now()) {
4254
+ if (!WAITING_ADMISSION_STATES.includes(attendee.admissionState)) return null;
4255
+ const expires = attendee.knockExpiresAt ? Date.parse(attendee.knockExpiresAt) : Number.NaN;
4256
+ if (!Number.isNaN(expires) && expires <= now) return null;
4212
4257
  return {
4213
4258
  identity: attendee.identity,
4214
4259
  displayName: attendee.displayName,
4215
4260
  avatarUrl: attendee.avatarUrl,
4216
4261
  userId: attendee.userId,
4217
4262
  state: "knocking",
4218
- knockedAt: attendee.joinedAt ?? (/* @__PURE__ */ new Date()).toISOString(),
4263
+ knockedAt: attendee.knockedAt ?? attendee.joinedAt ?? (/* @__PURE__ */ new Date()).toISOString(),
4264
+ knockExpiresAt: attendee.knockExpiresAt ?? null,
4219
4265
  decidedAt: null,
4220
4266
  decidedBy: null
4221
4267
  };
4222
4268
  }
4269
+ function decidedState(state) {
4270
+ return WAITING_ADMISSION_STATES.includes(state) ? "expired" : state;
4271
+ }
4223
4272
  function createMeetingSession(options) {
4224
4273
  const { manager, repository, store, identity } = options;
4225
4274
  let channel = null;
@@ -4243,11 +4292,6 @@ function createMeetingSession(options) {
4243
4292
  }
4244
4293
  return identity.userId;
4245
4294
  }
4246
- function knockIdentity() {
4247
- return asParticipantIdentity(
4248
- identity.userId !== null ? `user:${identity.userId}` : `guest:${identity.deviceId}`
4249
- );
4250
- }
4251
4295
  function requireMeeting(operation) {
4252
4296
  const meeting = store.snapshot().meeting;
4253
4297
  if (meeting === null) {
@@ -4259,6 +4303,191 @@ function createMeetingSession(options) {
4259
4303
  }
4260
4304
  return meeting;
4261
4305
  }
4306
+ let polling = false;
4307
+ let pollTimer = null;
4308
+ let reading = null;
4309
+ let admittedOnce = false;
4310
+ let unlisten = null;
4311
+ function report(level, message, remedy) {
4312
+ options.onDiagnostic?.(remedy === void 0 ? { level, message } : { level, message, remedy });
4313
+ }
4314
+ function pollMs() {
4315
+ const seconds = store.snapshot().waiting?.pollSeconds ?? 3;
4316
+ return Math.max(1, Math.min(60, seconds)) * 1e3;
4317
+ }
4318
+ function schedule(ms2) {
4319
+ if (pollTimer !== null) clearTimeout(pollTimer);
4320
+ if (!polling) return;
4321
+ pollTimer = setTimeout(() => {
4322
+ pollTimer = null;
4323
+ void readState2().finally(() => schedule(pollMs()));
4324
+ }, ms2);
4325
+ }
4326
+ function listen() {
4327
+ if (unlisten !== null || typeof window === "undefined") return;
4328
+ const nudge = () => {
4329
+ if (typeof document !== "undefined" && document.visibilityState === "hidden") return;
4330
+ void readState2();
4331
+ };
4332
+ const goodbye = () => {
4333
+ const meeting = store.snapshot().meeting;
4334
+ const pass = options.resolvePass?.() ?? null;
4335
+ const waiting = store.snapshot().waiting;
4336
+ if (meeting === null || pass === null || waiting === null) return;
4337
+ if (!WAITING_ADMISSION_STATES.includes(waiting.admission)) return;
4338
+ try {
4339
+ navigator.sendBeacon?.(
4340
+ options.api.url(MEET_ROUTES.leave),
4341
+ new Blob([JSON.stringify({ meeting_id: meeting.id, intent: "withdraw", meeting_pass: pass })], {
4342
+ type: "application/json"
4343
+ })
4344
+ );
4345
+ } catch {
4346
+ }
4347
+ };
4348
+ window.addEventListener("online", nudge);
4349
+ window.addEventListener("pagehide", goodbye);
4350
+ document.addEventListener("visibilitychange", nudge);
4351
+ unlisten = () => {
4352
+ window.removeEventListener("online", nudge);
4353
+ window.removeEventListener("pagehide", goodbye);
4354
+ document.removeEventListener("visibilitychange", nudge);
4355
+ };
4356
+ }
4357
+ function stopWaiting() {
4358
+ polling = false;
4359
+ if (pollTimer !== null) clearTimeout(pollTimer);
4360
+ pollTimer = null;
4361
+ unlisten?.();
4362
+ unlisten = null;
4363
+ }
4364
+ function patchWaiting(next) {
4365
+ const current = store.snapshot().waiting;
4366
+ const base = current ?? {
4367
+ admission: "knocking",
4368
+ admittersPresent: null,
4369
+ reknockAllowed: true,
4370
+ knockExpiresAt: null,
4371
+ pollSeconds: 3,
4372
+ liveVersion: 0
4373
+ };
4374
+ store.patch({ waiting: { ...base, ...next } });
4375
+ }
4376
+ function apply(read) {
4377
+ const snapshot = store.snapshot();
4378
+ if (snapshot.phase !== "lobby") return;
4379
+ const admission = read.admission_state ?? null;
4380
+ const ended = admission === "ended" || read.session_phase === "ended" || read.meeting_phase === "ended";
4381
+ if (admission === "admitted" && !ended) {
4382
+ if (admittedOnce) return;
4383
+ admittedOnce = true;
4384
+ stopWaiting();
4385
+ options.onAdmitted?.();
4386
+ return;
4387
+ }
4388
+ if (ended) {
4389
+ stopWaiting();
4390
+ patchWaiting({ admission: "ended" });
4391
+ const meeting = snapshot.meeting;
4392
+ if (read.meeting_phase === "ended" && meeting !== null && meeting.endedAt === null) {
4393
+ store.patch({ meeting: { ...meeting, endedAt: (/* @__PURE__ */ new Date()).toISOString() } });
4394
+ }
4395
+ return;
4396
+ }
4397
+ if (admission === null) return;
4398
+ patchWaiting({
4399
+ admission,
4400
+ admittersPresent: read.admitters_present ?? null,
4401
+ reknockAllowed: read.reknock_allowed ?? true,
4402
+ knockExpiresAt: read.knock_expires_at ?? null,
4403
+ pollSeconds: read.poll_seconds ?? snapshot.waiting?.pollSeconds ?? 3,
4404
+ liveVersion: read.live_version ?? 0
4405
+ });
4406
+ if (admission === "denied" || admission === "expired" || admission === "withdrawn" || admission === "removed") {
4407
+ stopWaiting();
4408
+ }
4409
+ }
4410
+ async function readState2() {
4411
+ if (reading !== null) return reading;
4412
+ const meeting = store.snapshot().meeting;
4413
+ const pass = options.resolvePass?.() ?? null;
4414
+ if (meeting === null || pass === null) return;
4415
+ reading = (async () => {
4416
+ try {
4417
+ const response = await options.api.request({
4418
+ path: `${MEET_ROUTES.state}?meeting_id=${encodeURIComponent(meeting.id)}`,
4419
+ method: "GET",
4420
+ requireAuth: false,
4421
+ operation: "meet.state",
4422
+ headers: { [MEETING_PASS_HEADER]: pass }
4423
+ });
4424
+ if (!response.ok) {
4425
+ let body = null;
4426
+ try {
4427
+ body = await response.json();
4428
+ } catch {
4429
+ body = null;
4430
+ }
4431
+ const refusal2 = readServerRefusal(body);
4432
+ report(
4433
+ "warn",
4434
+ refusal2?.reason ?? `meet.state: the server answered ${response.status}`,
4435
+ refusal2?.remedy ?? "The waiting room keeps asking; nothing on this screen changes until the server answers."
4436
+ );
4437
+ return;
4438
+ }
4439
+ apply(await response.json());
4440
+ } catch (error) {
4441
+ report("info", `meet.state: ${String(error)}`, "Waiting continues; the state is read again shortly.");
4442
+ } finally {
4443
+ reading = null;
4444
+ }
4445
+ })();
4446
+ return reading;
4447
+ }
4448
+ async function postLeave(intent) {
4449
+ const meeting = requireMeeting(`meet.${intent}`);
4450
+ const pass = options.resolvePass?.() ?? null;
4451
+ const response = await options.api.request({
4452
+ path: MEET_ROUTES.leave,
4453
+ method: "POST",
4454
+ requireAuth: false,
4455
+ operation: `meet.${intent}`,
4456
+ body: { meeting_id: meeting.id, intent },
4457
+ ...pass !== null ? { headers: { [MEETING_PASS_HEADER]: pass } } : {}
4458
+ });
4459
+ if (!response.ok) {
4460
+ let body = null;
4461
+ try {
4462
+ body = await response.json();
4463
+ } catch {
4464
+ body = null;
4465
+ }
4466
+ const refusal2 = readServerRefusal(body);
4467
+ throw new MeetError(
4468
+ "transport",
4469
+ refusal2?.reason ?? `meet.${intent}: the server answered ${response.status}`,
4470
+ refusal2?.remedy ?? "You are still in the waiting room. Try again."
4471
+ );
4472
+ }
4473
+ }
4474
+ function hint() {
4475
+ channel?.send(MEET_EVENTS.stateChanged, { v: null });
4476
+ }
4477
+ async function refreshQueue() {
4478
+ const meeting = store.snapshot().meeting;
4479
+ if (meeting === null || isGuest(identity) || !isHostRole(effectiveRole(store.snapshot()))) return;
4480
+ try {
4481
+ const attendees = await repository.attendees(meeting.id);
4482
+ store.learn(attendees);
4483
+ for (const attendee of attendees) {
4484
+ const entry = lobbyEntryFrom(attendee);
4485
+ if (entry !== null) store.upsertLobby(entry);
4486
+ else store.setAdmission(attendee.identity, decidedState(attendee.admissionState), null);
4487
+ }
4488
+ } catch {
4489
+ }
4490
+ }
4262
4491
  return {
4263
4492
  watch(roomName) {
4264
4493
  channel?.close();
@@ -4266,58 +4495,22 @@ function createMeetingSession(options) {
4266
4495
  topic: roomTopic(roomName),
4267
4496
  broadcast: [
4268
4497
  {
4269
- event: MEET_EVENTS.knock,
4270
- onMessage: (message) => {
4271
- store.upsertLobby(message.data);
4272
- }
4273
- },
4274
- {
4275
- event: MEET_EVENTS.admission,
4276
- onMessage: (message) => {
4277
- const verdict = message.data;
4278
- const decided = asParticipantIdentity(verdict.identity);
4279
- store.setAdmission(
4280
- decided,
4281
- verdict.admitted ? "admitted" : "denied",
4282
- verdict.by === null ? null : verdict.by
4283
- );
4284
- if (decided === knockIdentity()) {
4285
- if (verdict.admitted) options.onAdmitted?.();
4286
- else options.onDenied?.();
4287
- }
4288
- }
4289
- },
4290
- {
4291
- event: MEET_EVENTS.lock,
4292
- onMessage: (message) => {
4293
- const { locked } = message.data;
4294
- const meeting = store.snapshot().meeting;
4295
- store.patch({
4296
- locked,
4297
- ...meeting !== null ? { meeting: { ...meeting, locked } } : {}
4298
- });
4299
- }
4300
- },
4301
- {
4302
- event: MEET_EVENTS.recording,
4303
- onMessage: (message) => {
4304
- const payload = message.data;
4305
- store.setRecording(
4306
- payload.state,
4307
- payload.by === null ? null : payload.by,
4308
- "room"
4309
- );
4498
+ // 🚨 PAYLOAD-FREE. Every receiver reads again; a spoofed hint only causes a read.
4499
+ event: MEET_EVENTS.stateChanged,
4500
+ onMessage: () => {
4501
+ if (store.snapshot().phase === "lobby") void readState2();
4502
+ void refreshQueue();
4310
4503
  }
4311
4504
  }
4312
4505
  ],
4313
- // THE BACKFILL DOOR. A host whose socket blipped must not miss a knock:
4314
- // re-reading the meeting restores lock/recording truth, and knockers
4315
- // re-announce on their own timer.
4506
+ // THE BACKFILL DOOR. A socket blip must not lose a decision: re-read the meeting
4507
+ // row, and a waiting tab re-reads its own state.
4316
4508
  onBackfill: async () => {
4317
4509
  const meeting = store.snapshot().meeting;
4318
4510
  if (meeting === null) return;
4319
4511
  const fresh = isGuest(identity) ? await repository.meetingBySlug(meeting.slug) : await readMeeting(meeting.id);
4320
4512
  store.patch({ meeting: fresh, locked: fresh.locked });
4513
+ if (store.snapshot().phase === "lobby") void readState2();
4321
4514
  }
4322
4515
  });
4323
4516
  },
@@ -4378,7 +4571,7 @@ function createMeetingSession(options) {
4378
4571
  store.upsertLobby(entry);
4379
4572
  return;
4380
4573
  }
4381
- store.setAdmission(attendee.identity, attendee.admissionState, null);
4574
+ store.setAdmission(attendee.identity, decidedState(attendee.admissionState), null);
4382
4575
  }
4383
4576
  },
4384
4577
  {
@@ -4430,62 +4623,75 @@ function createMeetingSession(options) {
4430
4623
  if (entry !== null) {
4431
4624
  store.upsertLobby(entry);
4432
4625
  } else {
4433
- store.setAdmission(attendee.identity, attendee.admissionState, null);
4626
+ store.setAdmission(attendee.identity, decidedState(attendee.admissionState), null);
4434
4627
  }
4435
4628
  }
4436
4629
  }
4437
4630
  });
4438
4631
  meetingChannel.requestBackfill("manual");
4439
4632
  },
4440
- knock(roomName) {
4441
- const handle = channel ?? manager.open({ topic: roomTopic(roomName) });
4442
- channel = handle;
4443
- const entry = {
4444
- identity: knockIdentity(),
4445
- displayName: identity.displayName,
4446
- avatarUrl: identity.avatarUrl,
4447
- userId: identity.userId,
4448
- state: "knocking",
4449
- knockedAt: (/* @__PURE__ */ new Date()).toISOString(),
4450
- decidedAt: null,
4451
- decidedBy: null
4452
- };
4453
- store.upsertLobby(entry);
4454
- handle.send(MEET_EVENTS.knock, entry);
4455
- },
4456
4633
  async admit(entry, admitted) {
4457
4634
  const by = requireHostUser("meet.admit");
4458
4635
  const meeting = requireMeeting("meet.admit");
4459
- await repository.admit({
4636
+ const row = await repository.decideAdmission({
4460
4637
  meetingId: meeting.id,
4461
4638
  identity: entry.identity,
4462
- admitted,
4639
+ decision: admitted ? "admit" : "deny",
4463
4640
  byUserId: by
4464
4641
  });
4465
4642
  store.upsertLobby({
4466
4643
  ...entry,
4467
- state: admitted ? "admitted" : "denied",
4644
+ state: decidedState(row.admissionState),
4468
4645
  decidedAt: (/* @__PURE__ */ new Date()).toISOString(),
4469
4646
  decidedBy: by
4470
4647
  });
4471
- channel?.send(MEET_EVENTS.admission, {
4648
+ hint();
4649
+ },
4650
+ async undeny(entry) {
4651
+ const by = requireHostUser("meet.undeny");
4652
+ const meeting = requireMeeting("meet.undeny");
4653
+ const row = await repository.decideAdmission({
4654
+ meetingId: meeting.id,
4472
4655
  identity: entry.identity,
4473
- admitted,
4474
- by
4656
+ decision: "undeny",
4657
+ byUserId: by
4475
4658
  });
4659
+ store.upsertLobby({ ...entry, state: decidedState(row.admissionState), decidedAt: (/* @__PURE__ */ new Date()).toISOString(), decidedBy: by });
4660
+ hint();
4476
4661
  },
4477
4662
  async admitAll() {
4478
- const knocking = store.snapshot().lobby.filter((entry) => entry.state === "knocking");
4479
- for (const entry of knocking) {
4480
- await this.admit(entry, true);
4663
+ const by = requireHostUser("meet.admitAll");
4664
+ const meeting = requireMeeting("meet.admitAll");
4665
+ const rows = await repository.admitAll(meeting.id, by);
4666
+ for (const row of rows) {
4667
+ store.setAdmission(row.identity, row.admissionState, by);
4481
4668
  }
4669
+ hint();
4670
+ },
4671
+ startWaiting() {
4672
+ if (polling) return;
4673
+ polling = true;
4674
+ admittedOnce = false;
4675
+ listen();
4676
+ schedule(0);
4677
+ },
4678
+ stopWaiting,
4679
+ readState: readState2,
4680
+ async withdraw() {
4681
+ stopWaiting();
4682
+ await postLeave("withdraw");
4683
+ patchWaiting({ admission: "withdrawn" });
4684
+ hint();
4685
+ },
4686
+ async askAgain() {
4687
+ await options.rejoin?.();
4482
4688
  },
4483
4689
  async setLock(locked) {
4484
4690
  const by = requireHostUser("meet.setLock");
4485
4691
  const meeting = requireMeeting("meet.setLock");
4486
4692
  const updated = await repository.setLock(meeting.id, locked, by);
4487
4693
  store.patch({ meeting: updated, locked: updated.locked });
4488
- channel?.send(MEET_EVENTS.lock, { locked: updated.locked });
4694
+ hint();
4489
4695
  return updated;
4490
4696
  },
4491
4697
  /**
@@ -4543,6 +4749,7 @@ function createMeetingSession(options) {
4543
4749
  return { endedAt, served: true };
4544
4750
  },
4545
4751
  dispose() {
4752
+ stopWaiting();
4546
4753
  channel?.close();
4547
4754
  channel = null;
4548
4755
  meetingChannel?.close();
@@ -5403,7 +5610,7 @@ function createRoomEngine(options) {
5403
5610
  }
5404
5611
  };
5405
5612
  async function connect(request) {
5406
- store.patch({ phase: "requesting-token", failure: null });
5613
+ store.patch({ phase: "requesting-token", failure: null, waiting: null });
5407
5614
  const token = await tokens.get({
5408
5615
  roomName: request.roomName,
5409
5616
  identity,
@@ -5423,11 +5630,21 @@ function createRoomEngine(options) {
5423
5630
  canSubscribe: token.canSubscribe
5424
5631
  }
5425
5632
  });
5426
- if (token.lobby) {
5427
- store.patch({ phase: "lobby" });
5633
+ if (token.lobby || !token.canSubscribe) {
5634
+ store.patch({
5635
+ phase: "lobby",
5636
+ waiting: {
5637
+ admission: token.admissionState !== void 0 && token.admissionState !== "admitted" ? token.admissionState : "knocking",
5638
+ admittersPresent: null,
5639
+ reknockAllowed: true,
5640
+ knockExpiresAt: null,
5641
+ pollSeconds: token.pollSeconds ?? 3,
5642
+ liveVersion: 0
5643
+ }
5644
+ });
5428
5645
  return;
5429
5646
  }
5430
- store.patch({ phase: "connecting" });
5647
+ store.patch({ phase: "connecting", waiting: null });
5431
5648
  const created = await options.driverFactory(handlers);
5432
5649
  if (disposed) {
5433
5650
  await created.disconnect();
@@ -6686,7 +6903,7 @@ function observedPhase(input) {
6686
6903
  case "left":
6687
6904
  return "prejoin";
6688
6905
  case "lobby":
6689
- return "waiting:knocking";
6906
+ return waitingPhase(snapshot);
6690
6907
  case "requesting-token":
6691
6908
  case "connecting":
6692
6909
  return "joining";
@@ -6695,6 +6912,24 @@ function observedPhase(input) {
6695
6912
  return "in_call";
6696
6913
  }
6697
6914
  }
6915
+ function waitingPhase(snapshot) {
6916
+ switch (snapshot.waiting?.admission) {
6917
+ case "waiting_host":
6918
+ return "waiting:host_not_started";
6919
+ case "denied":
6920
+ return "denied";
6921
+ case "expired":
6922
+ return "knock_expired";
6923
+ case "withdrawn":
6924
+ return "left";
6925
+ case "ended":
6926
+ return "ended";
6927
+ case "removed":
6928
+ return "refused:removed";
6929
+ default:
6930
+ return "waiting:knocking";
6931
+ }
6932
+ }
6698
6933
  var ADMITTER_ROLES = /* @__PURE__ */ new Set(["host", "cohost"]);
6699
6934
  function observe(input) {
6700
6935
  const { snapshot } = input;
@@ -6709,7 +6944,7 @@ function observe(input) {
6709
6944
  reason: phase.startsWith("refused:") ? phase.slice("refused:".length) : null,
6710
6945
  role,
6711
6946
  connection: snapshot?.phase === "reconnecting" ? "reconnecting" : "stable",
6712
- hostPresent: inCall && snapshot !== null ? snapshot.participants.some((p) => ADMITTER_ROLES.has(p.role)) : null,
6947
+ hostPresent: inCall && snapshot !== null ? snapshot.participants.some((p) => ADMITTER_ROLES.has(p.role)) : snapshot?.phase === "lobby" ? snapshot.waiting?.admittersPresent ?? null : null,
6713
6948
  lobbyCount: canAdmit && snapshot !== null ? snapshot.lobby.filter((entry) => entry.state === "knocking").length : 0,
6714
6949
  recording: snapshot?.recording === "recording" || snapshot?.recording === "stopping",
6715
6950
  audioBlocked: null,