@ai-matrx/meet 0.11.31 → 0.11.33

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.js CHANGED
@@ -13,12 +13,18 @@ var MeetError = class extends Error {
13
13
  * renders `refused:<reason>` (or `ended`) from it — never from the HTTP status.
14
14
  */
15
15
  reason;
16
- constructor(code, message, remedy, cause, reason) {
16
+ /**
17
+ * A refusal that lifts by itself says when: whole seconds from the moment the server answered
18
+ * (a denial's cooldown, `reknock_cooldown_seconds`). Null for every refusal that does not.
19
+ */
20
+ retryAfterSeconds;
21
+ constructor(code, message, remedy, cause, reason, retryAfterSeconds) {
17
22
  super(message);
18
23
  this.name = "MeetError";
19
24
  this.code = code;
20
25
  this.remedy = remedy;
21
26
  this.reason = reason ?? null;
27
+ this.retryAfterSeconds = retryAfterSeconds ?? null;
22
28
  if (cause !== void 0) this.cause = cause;
23
29
  }
24
30
  /** True when retrying after the host re-establishes a session is the fix. */
@@ -698,7 +704,8 @@ function parseRoomToken(body, roomName) {
698
704
  const serverUrl = readString(payload, "server_url") ?? readString(payload, "serverUrl");
699
705
  const identity = readString(payload, "identity");
700
706
  const expiresAt = readString(payload, "expires_at") ?? readString(payload, "expiresAt");
701
- if (token === null || serverUrl === null || identity === null) {
707
+ const secondDeviceChoice = readString(payload, "second_device_choice");
708
+ if (token === null && secondDeviceChoice === null || serverUrl === null || identity === null) {
702
709
  throw invalidResponse(
703
710
  "meet.token",
704
711
  "the response is missing token, server_url, or identity \u2014 joining with a partial token would fail inside LiveKit with an opaque error instead of here"
@@ -710,11 +717,15 @@ function parseRoomToken(body, roomName) {
710
717
  "the response carries no expires_at. Without it the package cannot refresh before the token dies, and a long meeting silently loses its ability to reconnect"
711
718
  );
712
719
  }
720
+ const personKey = readString(payload, "person_key") ?? identity;
713
721
  return {
714
- token,
722
+ token: token ?? "",
715
723
  serverUrl,
716
724
  roomName,
717
- identity,
725
+ identity: personKey,
726
+ connectionIdentity: readString(payload, "connection_identity") ?? personKey,
727
+ connectionKind: readString(payload, "connection_kind") === "companion" ? "companion" : "main",
728
+ secondDeviceChoice,
718
729
  displayName: readString(payload, "display_name") ?? readString(payload, "displayName") ?? identity,
719
730
  expiresAt,
720
731
  canPublish: readBoolean(payload, "can_publish", true),
@@ -789,7 +800,9 @@ function createTokenClient(options) {
789
800
  organization_id: input.identity.organizationId,
790
801
  display_name: input.identity.displayName,
791
802
  device_id: input.identity.deviceId,
792
- guest
803
+ guest,
804
+ ...input.tabId !== void 0 ? { tab_id: input.tabId } : {},
805
+ ...input.secondDevice !== void 0 ? { second_device: input.secondDevice } : {}
793
806
  }
794
807
  });
795
808
  } catch (error) {
@@ -806,7 +819,8 @@ function createTokenClient(options) {
806
819
  said?.message ?? "You can't join this meeting.",
807
820
  said?.remedy ?? "Ask the person who shared the link to let you in, then open the link again.",
808
821
  void 0,
809
- said?.reason ?? null
822
+ said?.reason ?? null,
823
+ said?.retryAfterSeconds ?? null
810
824
  );
811
825
  }
812
826
  if (response.status === 404) {
@@ -864,6 +878,10 @@ function createTokenClient(options) {
864
878
  if (!expired(entry)) return Promise.resolve(entry.token);
865
879
  }
866
880
  const pending = request(input, true).then((token) => {
881
+ if (token.secondDeviceChoice != null) {
882
+ cache.delete(input.roomName);
883
+ return token;
884
+ }
867
885
  cache.set(input.roomName, { token, mintedAt: now(), inFlight: null });
868
886
  remember(token);
869
887
  return token;
@@ -898,6 +916,9 @@ function placeholder(roomName) {
898
916
  serverUrl: "",
899
917
  roomName,
900
918
  identity: "",
919
+ connectionIdentity: "",
920
+ connectionKind: "main",
921
+ secondDeviceChoice: null,
901
922
  displayName: "",
902
923
  expiresAt: (/* @__PURE__ */ new Date(0)).toISOString(),
903
924
  canPublish: false,
@@ -922,7 +943,9 @@ async function serverSentence(response) {
922
943
  if (message === null) return null;
923
944
  const remedy = typeof source.remedy === "string" && source.remedy.trim() !== "" ? source.remedy : void 0;
924
945
  const reason = typeof source.reason === "string" && source.reason !== "" ? source.reason : null;
925
- return { message, remedy, reason };
946
+ const retry = source.retry_after_seconds;
947
+ const retryAfterSeconds = typeof retry === "number" && Number.isFinite(retry) && retry > 0 ? Math.ceil(retry) : null;
948
+ return { message, remedy, reason, retryAfterSeconds };
926
949
  } catch {
927
950
  return null;
928
951
  }
@@ -1398,7 +1421,10 @@ var JOINABLE_FROM = /* @__PURE__ */ new Set([
1398
1421
  "superseded",
1399
1422
  "inactive",
1400
1423
  "denied",
1401
- "knock_expired"
1424
+ "knock_expired",
1425
+ // A refused knock whose rule lets the person ask again (a cooldown that ran out): the machine
1426
+ // allows the press; the server's gate decides whether it knocks or is refused again.
1427
+ "refused:lobby_rejects"
1402
1428
  ]);
1403
1429
  var NOT_ENDABLE = /* @__PURE__ */ new Set([
1404
1430
  "ended",
@@ -1482,6 +1508,8 @@ function nextPhase(from, event) {
1482
1508
  return WAITING_PHASES.has(from) ? "joining" : null;
1483
1509
  case "connected":
1484
1510
  return from === "joining" ? "in_call" : null;
1511
+ case "second_device":
1512
+ return from === "joining" ? "prejoin" : null;
1485
1513
  case "join_failed":
1486
1514
  return from === "joining" ? refusalPhase(event.reason) : null;
1487
1515
  case "exit":
@@ -1499,7 +1527,7 @@ function nextPhase(from, event) {
1499
1527
  case "call_outcome":
1500
1528
  return RINGING.has(from) ? event.outcome : null;
1501
1529
  case "reset":
1502
- return LIVE_PHASES.has(from) ? null : "resolving";
1530
+ return LIVE_PHASES.has(from) && event.teardown !== true ? null : "resolving";
1503
1531
  }
1504
1532
  }
1505
1533
  function resolveExit(input) {
@@ -1805,8 +1833,8 @@ function createCallCenter(options) {
1805
1833
  "A late accept must not resurrect a settled call. Close the ringing UI; the caller has already moved on."
1806
1834
  );
1807
1835
  }
1808
- if (options.store.snapshot().phase === "ringing_in") options.store.dispatch({ type: "call_answered" });
1809
1836
  const settled = await repository.settleCallInvite(id, "connecting", null);
1837
+ if (options.store.snapshot().phase === "ringing_in") options.store.dispatch({ type: "call_answered" });
1810
1838
  options.store.patch({ call: { invite: settled, direction: "incoming" } });
1811
1839
  settleLocal(settled);
1812
1840
  channel?.send(MEET_EVENTS.settled, settled);
@@ -3177,11 +3205,17 @@ async function loadMeetingActivities(api, meetingId, meetingPass = null) {
3177
3205
  }
3178
3206
 
3179
3207
  // src/core/names.ts
3208
+ var CONNECTION_SEPARATOR = "~";
3209
+ function personOf(identity) {
3210
+ const value = String(identity ?? "").trim();
3211
+ const cut = value.indexOf(CONNECTION_SEPARATOR);
3212
+ return cut === -1 ? value : value.slice(0, cut);
3213
+ }
3180
3214
  function isGuestIdentity(identity) {
3181
- return String(identity).startsWith("guest:");
3215
+ return personOf(identity).startsWith("guest:");
3182
3216
  }
3183
3217
  function isAgentIdentity(identity) {
3184
- return String(identity).startsWith("agent:");
3218
+ return personOf(identity).startsWith("agent:");
3185
3219
  }
3186
3220
  var NOTE_TAKER_PRODUCT_NAME = "Matrx note-taker";
3187
3221
  var AGENT_PRODUCT_NAME = "Matrx agent";
@@ -3395,6 +3429,7 @@ var EMPTY = {
3395
3429
  waiting: null,
3396
3430
  meeting: null,
3397
3431
  localIdentity: null,
3432
+ secondDevice: null,
3398
3433
  grants: NO_GRANTS,
3399
3434
  participants: [],
3400
3435
  activeSpeakers: [],
@@ -3645,7 +3680,8 @@ function createMeetStore(initial = {}) {
3645
3680
  commit({ ...state, media: { ...state.media, [kind]: next } });
3646
3681
  },
3647
3682
  reset() {
3648
- commit({ ...EMPTY, effects: state.effects });
3683
+ const phase = nextPhase(state.phase, { type: "reset", teardown: true }) ?? EMPTY.phase;
3684
+ commit({ ...EMPTY, effects: state.effects, phase, reason: reasonOf(phase), phaseNotice: null });
3649
3685
  }
3650
3686
  };
3651
3687
  }
@@ -4724,6 +4760,28 @@ function lobbyEntryFrom(attendee, now = Date.now()) {
4724
4760
  decidedBy: null
4725
4761
  };
4726
4762
  }
4763
+ function deniedEntryFrom(attendee) {
4764
+ if (attendee.admissionState !== "denied") return null;
4765
+ return {
4766
+ identity: attendee.identity,
4767
+ displayName: attendee.displayName,
4768
+ avatarUrl: attendee.avatarUrl,
4769
+ userId: attendee.userId,
4770
+ state: "denied",
4771
+ knockedAt: attendee.knockedAt ?? attendee.joinedAt ?? (/* @__PURE__ */ new Date()).toISOString(),
4772
+ knockExpiresAt: null,
4773
+ decidedAt: null,
4774
+ decidedBy: null
4775
+ };
4776
+ }
4777
+ function applyDoorRow(store, attendee) {
4778
+ const entry = lobbyEntryFrom(attendee) ?? deniedEntryFrom(attendee);
4779
+ if (entry !== null) {
4780
+ store.upsertLobby(entry);
4781
+ return;
4782
+ }
4783
+ store.setAdmission(attendee.identity, decidedState(attendee.admissionState), null);
4784
+ }
4727
4785
  function decidedState(state) {
4728
4786
  return WAITING_ADMISSION_STATES.includes(state) ? "expired" : state;
4729
4787
  }
@@ -4858,6 +4916,7 @@ function createMeetingSession(options) {
4858
4916
  admission,
4859
4917
  admittersPresent: read.admitters_present ?? null,
4860
4918
  reknockAllowed: read.reknock_allowed ?? true,
4919
+ reknockAt: typeof read.reknock_in_seconds === "number" && read.reknock_in_seconds > 0 ? Date.now() + read.reknock_in_seconds * 1e3 : null,
4861
4920
  knockExpiresAt: read.knock_expires_at ?? null,
4862
4921
  pollSeconds: read.poll_seconds ?? snapshot.waiting?.pollSeconds ?? 3,
4863
4922
  liveVersion: read.live_version ?? 0
@@ -4940,11 +4999,7 @@ function createMeetingSession(options) {
4940
4999
  try {
4941
5000
  const attendees = await repository.attendees(meeting.id);
4942
5001
  store.learn(attendees);
4943
- for (const attendee of attendees) {
4944
- const entry = lobbyEntryFrom(attendee);
4945
- if (entry !== null) store.upsertLobby(entry);
4946
- else store.setAdmission(attendee.identity, decidedState(attendee.admissionState), null);
4947
- }
5002
+ for (const attendee of attendees) applyDoorRow(store, attendee);
4948
5003
  } catch {
4949
5004
  }
4950
5005
  }
@@ -5026,12 +5081,7 @@ function createMeetingSession(options) {
5026
5081
  if (row === null) return;
5027
5082
  const attendee = repository.projectAttendee(row);
5028
5083
  store.learn([attendee]);
5029
- const entry = lobbyEntryFrom(attendee);
5030
- if (entry !== null) {
5031
- store.upsertLobby(entry);
5032
- return;
5033
- }
5034
- store.setAdmission(attendee.identity, decidedState(attendee.admissionState), null);
5084
+ applyDoorRow(store, attendee);
5035
5085
  }
5036
5086
  },
5037
5087
  {
@@ -5078,14 +5128,7 @@ function createMeetingSession(options) {
5078
5128
  store.setRecording(latest.state, latest.startedBy, "record", latest.id);
5079
5129
  }
5080
5130
  store.learn(attendees);
5081
- for (const attendee of attendees) {
5082
- const entry = lobbyEntryFrom(attendee);
5083
- if (entry !== null) {
5084
- store.upsertLobby(entry);
5085
- } else {
5086
- store.setAdmission(attendee.identity, decidedState(attendee.admissionState), null);
5087
- }
5088
- }
5131
+ for (const attendee of attendees) applyDoorRow(store, attendee);
5089
5132
  }
5090
5133
  });
5091
5134
  meetingChannel.requestBackfill("manual");
@@ -6129,6 +6172,7 @@ function SCREEN_SHARE_BLOCKED_REMEDY_FOR(remedy) {
6129
6172
  }
6130
6173
  var PARTICIPANT_ROLE_ATTRIBUTE = "matrx.participant.role";
6131
6174
  var PARTICIPANT_CAN_RECORD_ATTRIBUTE = "matrx.participant.can_record";
6175
+ var CONNECTION_KIND_ATTRIBUTE = "matrx.participant.connection_kind";
6132
6176
  var PARTICIPANT_STANDING_ATTRIBUTE = "matrx.participant.standing";
6133
6177
  var KNOWN_STANDINGS = ["member", "invited", "outsider", "guest"];
6134
6178
  function parseStanding(value) {
@@ -6152,7 +6196,8 @@ function projectParticipant(driver, joinedAt) {
6152
6196
  }
6153
6197
  const userId = typeof metadata.user_id === "string" ? asUserId(metadata.user_id) : null;
6154
6198
  return {
6155
- identity: asParticipantIdentity(driver.identity),
6199
+ // S7: a tile is a PERSON; LiveKit names the tab's connection (`<person>~<tab>`).
6200
+ identity: personOf(driver.identity),
6156
6201
  userId,
6157
6202
  // 🚨 A RAW LIVEKIT IDENTITY IS NEVER A NAME (MRI-A18). Through 0.4.12 this
6158
6203
  // line read `driver.name.length > 0 ? driver.name : driver.identity`, and
@@ -6199,6 +6244,11 @@ function createRoomEngine(options) {
6199
6244
  let ghosts = /* @__PURE__ */ new Map();
6200
6245
  let ghostTimer = null;
6201
6246
  let liveRoster = [];
6247
+ let personConnections = /* @__PURE__ */ new Map();
6248
+ let mainConnection = /* @__PURE__ */ new Map();
6249
+ function connectionsOf(who) {
6250
+ return personConnections.get(personOf(who)) ?? [who];
6251
+ }
6202
6252
  const departures = /* @__PURE__ */ new Map();
6203
6253
  function requestAsNow() {
6204
6254
  if (lastRequest === null) return null;
@@ -6263,7 +6313,12 @@ function createRoomEngine(options) {
6263
6313
  function failJoin(error, operation) {
6264
6314
  const normalized = normalizeMeetError(error, operation);
6265
6315
  store.patch({
6266
- failure: { message: normalized.message, remedy: normalized.remedy, reason: normalized.reason }
6316
+ failure: {
6317
+ message: normalized.message,
6318
+ remedy: normalized.remedy,
6319
+ reason: normalized.reason,
6320
+ retryAt: normalized.retryAfterSeconds === null ? null : now() + normalized.retryAfterSeconds * 1e3
6321
+ }
6267
6322
  });
6268
6323
  store.dispatch({ type: "join_failed", reason: normalized.reason ?? null });
6269
6324
  report(
@@ -6335,10 +6390,10 @@ function createRoomEngine(options) {
6335
6390
  if (store.snapshot().spotlight !== spotlight) store.patch({ spotlight });
6336
6391
  },
6337
6392
  activeSpeakers(identities) {
6338
- store.setSpeaking(identities.map(asParticipantIdentity));
6393
+ store.setSpeaking([...new Set(identities.map((who) => personOf(who)))]);
6339
6394
  },
6340
6395
  quality(who, value) {
6341
- store.setQuality(asParticipantIdentity(who), value);
6396
+ store.setQuality(personOf(who), value);
6342
6397
  },
6343
6398
  connectionState(state) {
6344
6399
  supervisor.driverState(state);
@@ -6358,7 +6413,7 @@ function createRoomEngine(options) {
6358
6413
  );
6359
6414
  return;
6360
6415
  }
6361
- const speakerIdentity = asParticipantIdentity(event.speakerIdentity);
6416
+ const speakerIdentity = personOf(event.speakerIdentity);
6362
6417
  const speaker = snapshot.participants.find(
6363
6418
  (participant) => participant.identity === speakerIdentity
6364
6419
  );
@@ -6432,7 +6487,7 @@ function createRoomEngine(options) {
6432
6487
  );
6433
6488
  return;
6434
6489
  }
6435
- const sender = asParticipantIdentity(from ?? "");
6490
+ const sender = personOf(from ?? "");
6436
6491
  switch (topic) {
6437
6492
  case DATA_TOPICS.hand:
6438
6493
  store.setHand(
@@ -6474,7 +6529,7 @@ function createRoomEngine(options) {
6474
6529
  }
6475
6530
  };
6476
6531
  async function resolveExitAndApply(hint) {
6477
- const connection = current?.identity ?? store.snapshot().localIdentity ?? null;
6532
+ const connection = current?.connectionIdentity ?? current?.identity ?? store.snapshot().localIdentity ?? null;
6478
6533
  lastRequest = requestAsNow();
6479
6534
  await dropRoom();
6480
6535
  let server;
@@ -6512,14 +6567,29 @@ function createRoomEngine(options) {
6512
6567
  }
6513
6568
  async function connect(request) {
6514
6569
  store.patch({ failure: null, waiting: null, roomName: request.roomName, share: NO_SHARE, connection: "stable" });
6570
+ const tabId = options.tabClaim !== void 0 ? await options.tabClaim.tabId : null;
6515
6571
  const token = await tokens.get({
6516
6572
  roomName: request.roomName,
6517
6573
  identity,
6518
6574
  ...request.meetingId !== void 0 ? { meetingId: request.meetingId } : {},
6519
6575
  ...request.slug !== void 0 ? { slug: request.slug } : {},
6520
6576
  ...request.breakoutRoomId != null ? { breakoutRoomId: request.breakoutRoomId } : {},
6521
- ...request.observe === true ? { observe: true } : {}
6577
+ ...request.observe === true ? { observe: true } : {},
6578
+ ...tabId !== null ? { tabId } : {},
6579
+ ...request.secondDevice !== void 0 ? { secondDevice: request.secondDevice } : {}
6522
6580
  });
6581
+ if (token.secondDeviceChoice != null) {
6582
+ store.patch({ secondDevice: { rule: token.secondDeviceChoice }, localIdentity: token.identity });
6583
+ store.dispatch({ type: "second_device" });
6584
+ report("info", `meet: already in this meeting elsewhere \u2014 offering the choice (${token.secondDeviceChoice})`);
6585
+ return;
6586
+ }
6587
+ if (store.snapshot().secondDevice !== null) store.patch({ secondDevice: null });
6588
+ if (request.secondDevice === "switch_here" && lastRequest !== null) {
6589
+ const { secondDevice: _answered, ...rest } = lastRequest;
6590
+ lastRequest = rest;
6591
+ }
6592
+ options.tabClaim?.inRoom(String(request.roomName), String(token.identity));
6523
6593
  current = token;
6524
6594
  lastServerUrl = token.serverUrl;
6525
6595
  connectionPolicy = token.connectionPolicy ?? DEFAULT_CONNECTION_POLICY;
@@ -6584,7 +6654,7 @@ function createRoomEngine(options) {
6584
6654
  if (request.mode === "video") {
6585
6655
  await enableTrack(created, "camera", true);
6586
6656
  }
6587
- if (request.microphone !== false) {
6657
+ if (request.microphone !== false && current?.connectionKind !== "companion") {
6588
6658
  await enableTrack(created, "microphone", true);
6589
6659
  }
6590
6660
  media.onConnected();
@@ -6606,7 +6676,8 @@ function createRoomEngine(options) {
6606
6676
  }
6607
6677
  async function publish(topic, body, to) {
6608
6678
  if (driver === null) return;
6609
- await driver.publishData(topic, JSON.stringify(body), to);
6679
+ const addresses = to === void 0 ? void 0 : to.flatMap((who) => connectionsOf(who));
6680
+ await driver.publishData(topic, JSON.stringify(body), addresses);
6610
6681
  }
6611
6682
  async function applyEffects(target, effects) {
6612
6683
  try {
@@ -6644,8 +6715,19 @@ function createRoomEngine(options) {
6644
6715
  liveRoster = list;
6645
6716
  const merged = mergeGhosts(list, ghosts, at);
6646
6717
  ghosts = merged.kept;
6718
+ const people = /* @__PURE__ */ new Map();
6719
+ const connections = /* @__PURE__ */ new Map();
6720
+ for (const entry of merged.roster) {
6721
+ const person = personOf(entry.identity);
6722
+ connections.set(person, [...connections.get(person) ?? [], entry.identity]);
6723
+ const rank = (entry.isLocal ? 8 : 0) + (entry.reconnecting === true ? 0 : 4) + (entry.attributes[CONNECTION_KIND_ATTRIBUTE] === "companion" ? 0 : 2) + (entry.cameraEnabled || entry.micEnabled ? 1 : 0);
6724
+ const held = people.get(person);
6725
+ if (held === void 0 || rank > held.rank) people.set(person, { entry, rank });
6726
+ }
6727
+ personConnections = connections;
6728
+ mainConnection = new Map([...people].map(([person, { entry }]) => [person, entry.identity]));
6647
6729
  store.setParticipants(
6648
- merged.roster.map((entry) => {
6730
+ [...people.values()].map(({ entry }) => {
6649
6731
  const projected = projectParticipant(entry, joinedAt);
6650
6732
  return entry.reconnecting === true ? { ...projected, reconnecting: true, quality: "lost" } : projected;
6651
6733
  })
@@ -6822,7 +6904,13 @@ function createRoomEngine(options) {
6822
6904
  },
6823
6905
  async setScreenShareEnabled(enabled) {
6824
6906
  if (driver === null) return;
6825
- if (enabled && driver.supportsScreenShare?.() === false) {
6907
+ let unsupported = false;
6908
+ try {
6909
+ unsupported = enabled && driver.supportsScreenShare?.() === false;
6910
+ } catch {
6911
+ unsupported = false;
6912
+ }
6913
+ if (unsupported) {
6826
6914
  store.patch({ share: { state: "refused", reason: "unsupported", remedy: null, at: now() } });
6827
6915
  report("warn", "meet: this browser cannot share a screen", "Share from a desktop browser (Chrome, Edge, Firefox or Safari).");
6828
6916
  return;
@@ -6921,7 +7009,7 @@ function createRoomEngine(options) {
6921
7009
  },
6922
7010
  attach(who, kind, element) {
6923
7011
  if (driver === null) return () => void 0;
6924
- return driver.attach(who, kind, element);
7012
+ return driver.attach(mainConnection.get(personOf(who)) ?? who, kind, element);
6925
7013
  },
6926
7014
  token: () => current,
6927
7015
  async dispose() {