@ai-matrx/meet 0.11.32 → 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.cjs CHANGED
@@ -288,12 +288,18 @@ var MeetError = class extends Error {
288
288
  * renders `refused:<reason>` (or `ended`) from it — never from the HTTP status.
289
289
  */
290
290
  reason;
291
- constructor(code, message, remedy, cause, reason) {
291
+ /**
292
+ * A refusal that lifts by itself says when: whole seconds from the moment the server answered
293
+ * (a denial's cooldown, `reknock_cooldown_seconds`). Null for every refusal that does not.
294
+ */
295
+ retryAfterSeconds;
296
+ constructor(code, message, remedy, cause, reason, retryAfterSeconds) {
292
297
  super(message);
293
298
  this.name = "MeetError";
294
299
  this.code = code;
295
300
  this.remedy = remedy;
296
301
  this.reason = reason ?? null;
302
+ this.retryAfterSeconds = retryAfterSeconds ?? null;
297
303
  if (cause !== void 0) this.cause = cause;
298
304
  }
299
305
  /** True when retrying after the host re-establishes a session is the fix. */
@@ -973,7 +979,8 @@ function parseRoomToken(body, roomName) {
973
979
  const serverUrl = readString(payload, "server_url") ?? readString(payload, "serverUrl");
974
980
  const identity = readString(payload, "identity");
975
981
  const expiresAt = readString(payload, "expires_at") ?? readString(payload, "expiresAt");
976
- if (token === null || serverUrl === null || identity === null) {
982
+ const secondDeviceChoice = readString(payload, "second_device_choice");
983
+ if (token === null && secondDeviceChoice === null || serverUrl === null || identity === null) {
977
984
  throw invalidResponse(
978
985
  "meet.token",
979
986
  "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"
@@ -985,11 +992,15 @@ function parseRoomToken(body, roomName) {
985
992
  "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"
986
993
  );
987
994
  }
995
+ const personKey = readString(payload, "person_key") ?? identity;
988
996
  return {
989
- token,
997
+ token: token ?? "",
990
998
  serverUrl,
991
999
  roomName,
992
- identity,
1000
+ identity: personKey,
1001
+ connectionIdentity: readString(payload, "connection_identity") ?? personKey,
1002
+ connectionKind: readString(payload, "connection_kind") === "companion" ? "companion" : "main",
1003
+ secondDeviceChoice,
993
1004
  displayName: readString(payload, "display_name") ?? readString(payload, "displayName") ?? identity,
994
1005
  expiresAt,
995
1006
  canPublish: readBoolean(payload, "can_publish", true),
@@ -1064,7 +1075,9 @@ function createTokenClient(options) {
1064
1075
  organization_id: input.identity.organizationId,
1065
1076
  display_name: input.identity.displayName,
1066
1077
  device_id: input.identity.deviceId,
1067
- guest
1078
+ guest,
1079
+ ...input.tabId !== void 0 ? { tab_id: input.tabId } : {},
1080
+ ...input.secondDevice !== void 0 ? { second_device: input.secondDevice } : {}
1068
1081
  }
1069
1082
  });
1070
1083
  } catch (error) {
@@ -1081,7 +1094,8 @@ function createTokenClient(options) {
1081
1094
  said?.message ?? "You can't join this meeting.",
1082
1095
  said?.remedy ?? "Ask the person who shared the link to let you in, then open the link again.",
1083
1096
  void 0,
1084
- said?.reason ?? null
1097
+ said?.reason ?? null,
1098
+ said?.retryAfterSeconds ?? null
1085
1099
  );
1086
1100
  }
1087
1101
  if (response.status === 404) {
@@ -1139,6 +1153,10 @@ function createTokenClient(options) {
1139
1153
  if (!expired(entry)) return Promise.resolve(entry.token);
1140
1154
  }
1141
1155
  const pending = request(input, true).then((token) => {
1156
+ if (token.secondDeviceChoice != null) {
1157
+ cache.delete(input.roomName);
1158
+ return token;
1159
+ }
1142
1160
  cache.set(input.roomName, { token, mintedAt: now(), inFlight: null });
1143
1161
  remember(token);
1144
1162
  return token;
@@ -1173,6 +1191,9 @@ function placeholder(roomName) {
1173
1191
  serverUrl: "",
1174
1192
  roomName,
1175
1193
  identity: "",
1194
+ connectionIdentity: "",
1195
+ connectionKind: "main",
1196
+ secondDeviceChoice: null,
1176
1197
  displayName: "",
1177
1198
  expiresAt: (/* @__PURE__ */ new Date(0)).toISOString(),
1178
1199
  canPublish: false,
@@ -1197,7 +1218,9 @@ async function serverSentence(response) {
1197
1218
  if (message === null) return null;
1198
1219
  const remedy = typeof source.remedy === "string" && source.remedy.trim() !== "" ? source.remedy : void 0;
1199
1220
  const reason = typeof source.reason === "string" && source.reason !== "" ? source.reason : null;
1200
- return { message, remedy, reason };
1221
+ const retry = source.retry_after_seconds;
1222
+ const retryAfterSeconds = typeof retry === "number" && Number.isFinite(retry) && retry > 0 ? Math.ceil(retry) : null;
1223
+ return { message, remedy, reason, retryAfterSeconds };
1201
1224
  } catch {
1202
1225
  return null;
1203
1226
  }
@@ -1673,7 +1696,10 @@ var JOINABLE_FROM = /* @__PURE__ */ new Set([
1673
1696
  "superseded",
1674
1697
  "inactive",
1675
1698
  "denied",
1676
- "knock_expired"
1699
+ "knock_expired",
1700
+ // A refused knock whose rule lets the person ask again (a cooldown that ran out): the machine
1701
+ // allows the press; the server's gate decides whether it knocks or is refused again.
1702
+ "refused:lobby_rejects"
1677
1703
  ]);
1678
1704
  var NOT_ENDABLE = /* @__PURE__ */ new Set([
1679
1705
  "ended",
@@ -1757,6 +1783,8 @@ function nextPhase(from, event) {
1757
1783
  return WAITING_PHASES.has(from) ? "joining" : null;
1758
1784
  case "connected":
1759
1785
  return from === "joining" ? "in_call" : null;
1786
+ case "second_device":
1787
+ return from === "joining" ? "prejoin" : null;
1760
1788
  case "join_failed":
1761
1789
  return from === "joining" ? refusalPhase(event.reason) : null;
1762
1790
  case "exit":
@@ -1774,7 +1802,7 @@ function nextPhase(from, event) {
1774
1802
  case "call_outcome":
1775
1803
  return RINGING.has(from) ? event.outcome : null;
1776
1804
  case "reset":
1777
- return LIVE_PHASES.has(from) ? null : "resolving";
1805
+ return LIVE_PHASES.has(from) && event.teardown !== true ? null : "resolving";
1778
1806
  }
1779
1807
  }
1780
1808
  function resolveExit(input) {
@@ -2080,8 +2108,8 @@ function createCallCenter(options) {
2080
2108
  "A late accept must not resurrect a settled call. Close the ringing UI; the caller has already moved on."
2081
2109
  );
2082
2110
  }
2083
- if (options.store.snapshot().phase === "ringing_in") options.store.dispatch({ type: "call_answered" });
2084
2111
  const settled = await repository.settleCallInvite(id, "connecting", null);
2112
+ if (options.store.snapshot().phase === "ringing_in") options.store.dispatch({ type: "call_answered" });
2085
2113
  options.store.patch({ call: { invite: settled, direction: "incoming" } });
2086
2114
  settleLocal(settled);
2087
2115
  channel?.send(MEET_EVENTS.settled, settled);
@@ -3452,11 +3480,17 @@ async function loadMeetingActivities(api, meetingId, meetingPass = null) {
3452
3480
  }
3453
3481
 
3454
3482
  // src/core/names.ts
3483
+ var CONNECTION_SEPARATOR = "~";
3484
+ function personOf(identity) {
3485
+ const value = String(identity ?? "").trim();
3486
+ const cut = value.indexOf(CONNECTION_SEPARATOR);
3487
+ return cut === -1 ? value : value.slice(0, cut);
3488
+ }
3455
3489
  function isGuestIdentity(identity) {
3456
- return String(identity).startsWith("guest:");
3490
+ return personOf(identity).startsWith("guest:");
3457
3491
  }
3458
3492
  function isAgentIdentity(identity) {
3459
- return String(identity).startsWith("agent:");
3493
+ return personOf(identity).startsWith("agent:");
3460
3494
  }
3461
3495
  var NOTE_TAKER_PRODUCT_NAME = "Matrx note-taker";
3462
3496
  var AGENT_PRODUCT_NAME = "Matrx agent";
@@ -3670,6 +3704,7 @@ var EMPTY = {
3670
3704
  waiting: null,
3671
3705
  meeting: null,
3672
3706
  localIdentity: null,
3707
+ secondDevice: null,
3673
3708
  grants: NO_GRANTS,
3674
3709
  participants: [],
3675
3710
  activeSpeakers: [],
@@ -3920,7 +3955,8 @@ function createMeetStore(initial = {}) {
3920
3955
  commit({ ...state, media: { ...state.media, [kind]: next } });
3921
3956
  },
3922
3957
  reset() {
3923
- commit({ ...EMPTY, effects: state.effects });
3958
+ const phase = nextPhase(state.phase, { type: "reset", teardown: true }) ?? EMPTY.phase;
3959
+ commit({ ...EMPTY, effects: state.effects, phase, reason: reasonOf(phase), phaseNotice: null });
3924
3960
  }
3925
3961
  };
3926
3962
  }
@@ -4999,6 +5035,28 @@ function lobbyEntryFrom(attendee, now = Date.now()) {
4999
5035
  decidedBy: null
5000
5036
  };
5001
5037
  }
5038
+ function deniedEntryFrom(attendee) {
5039
+ if (attendee.admissionState !== "denied") return null;
5040
+ return {
5041
+ identity: attendee.identity,
5042
+ displayName: attendee.displayName,
5043
+ avatarUrl: attendee.avatarUrl,
5044
+ userId: attendee.userId,
5045
+ state: "denied",
5046
+ knockedAt: attendee.knockedAt ?? attendee.joinedAt ?? (/* @__PURE__ */ new Date()).toISOString(),
5047
+ knockExpiresAt: null,
5048
+ decidedAt: null,
5049
+ decidedBy: null
5050
+ };
5051
+ }
5052
+ function applyDoorRow(store, attendee) {
5053
+ const entry = lobbyEntryFrom(attendee) ?? deniedEntryFrom(attendee);
5054
+ if (entry !== null) {
5055
+ store.upsertLobby(entry);
5056
+ return;
5057
+ }
5058
+ store.setAdmission(attendee.identity, decidedState(attendee.admissionState), null);
5059
+ }
5002
5060
  function decidedState(state) {
5003
5061
  return WAITING_ADMISSION_STATES.includes(state) ? "expired" : state;
5004
5062
  }
@@ -5133,6 +5191,7 @@ function createMeetingSession(options) {
5133
5191
  admission,
5134
5192
  admittersPresent: read.admitters_present ?? null,
5135
5193
  reknockAllowed: read.reknock_allowed ?? true,
5194
+ reknockAt: typeof read.reknock_in_seconds === "number" && read.reknock_in_seconds > 0 ? Date.now() + read.reknock_in_seconds * 1e3 : null,
5136
5195
  knockExpiresAt: read.knock_expires_at ?? null,
5137
5196
  pollSeconds: read.poll_seconds ?? snapshot.waiting?.pollSeconds ?? 3,
5138
5197
  liveVersion: read.live_version ?? 0
@@ -5215,11 +5274,7 @@ function createMeetingSession(options) {
5215
5274
  try {
5216
5275
  const attendees = await repository.attendees(meeting.id);
5217
5276
  store.learn(attendees);
5218
- for (const attendee of attendees) {
5219
- const entry = lobbyEntryFrom(attendee);
5220
- if (entry !== null) store.upsertLobby(entry);
5221
- else store.setAdmission(attendee.identity, decidedState(attendee.admissionState), null);
5222
- }
5277
+ for (const attendee of attendees) applyDoorRow(store, attendee);
5223
5278
  } catch {
5224
5279
  }
5225
5280
  }
@@ -5301,12 +5356,7 @@ function createMeetingSession(options) {
5301
5356
  if (row === null) return;
5302
5357
  const attendee = repository.projectAttendee(row);
5303
5358
  store.learn([attendee]);
5304
- const entry = lobbyEntryFrom(attendee);
5305
- if (entry !== null) {
5306
- store.upsertLobby(entry);
5307
- return;
5308
- }
5309
- store.setAdmission(attendee.identity, decidedState(attendee.admissionState), null);
5359
+ applyDoorRow(store, attendee);
5310
5360
  }
5311
5361
  },
5312
5362
  {
@@ -5353,14 +5403,7 @@ function createMeetingSession(options) {
5353
5403
  store.setRecording(latest.state, latest.startedBy, "record", latest.id);
5354
5404
  }
5355
5405
  store.learn(attendees);
5356
- for (const attendee of attendees) {
5357
- const entry = lobbyEntryFrom(attendee);
5358
- if (entry !== null) {
5359
- store.upsertLobby(entry);
5360
- } else {
5361
- store.setAdmission(attendee.identity, decidedState(attendee.admissionState), null);
5362
- }
5363
- }
5406
+ for (const attendee of attendees) applyDoorRow(store, attendee);
5364
5407
  }
5365
5408
  });
5366
5409
  meetingChannel.requestBackfill("manual");
@@ -6404,6 +6447,7 @@ function SCREEN_SHARE_BLOCKED_REMEDY_FOR(remedy) {
6404
6447
  }
6405
6448
  var PARTICIPANT_ROLE_ATTRIBUTE = "matrx.participant.role";
6406
6449
  var PARTICIPANT_CAN_RECORD_ATTRIBUTE = "matrx.participant.can_record";
6450
+ var CONNECTION_KIND_ATTRIBUTE = "matrx.participant.connection_kind";
6407
6451
  var PARTICIPANT_STANDING_ATTRIBUTE = "matrx.participant.standing";
6408
6452
  var KNOWN_STANDINGS = ["member", "invited", "outsider", "guest"];
6409
6453
  function parseStanding(value) {
@@ -6427,7 +6471,8 @@ function projectParticipant(driver, joinedAt) {
6427
6471
  }
6428
6472
  const userId = typeof metadata.user_id === "string" ? asUserId(metadata.user_id) : null;
6429
6473
  return {
6430
- identity: asParticipantIdentity(driver.identity),
6474
+ // S7: a tile is a PERSON; LiveKit names the tab's connection (`<person>~<tab>`).
6475
+ identity: personOf(driver.identity),
6431
6476
  userId,
6432
6477
  // 🚨 A RAW LIVEKIT IDENTITY IS NEVER A NAME (MRI-A18). Through 0.4.12 this
6433
6478
  // line read `driver.name.length > 0 ? driver.name : driver.identity`, and
@@ -6474,6 +6519,11 @@ function createRoomEngine(options) {
6474
6519
  let ghosts = /* @__PURE__ */ new Map();
6475
6520
  let ghostTimer = null;
6476
6521
  let liveRoster = [];
6522
+ let personConnections = /* @__PURE__ */ new Map();
6523
+ let mainConnection = /* @__PURE__ */ new Map();
6524
+ function connectionsOf(who) {
6525
+ return personConnections.get(personOf(who)) ?? [who];
6526
+ }
6477
6527
  const departures = /* @__PURE__ */ new Map();
6478
6528
  function requestAsNow() {
6479
6529
  if (lastRequest === null) return null;
@@ -6538,7 +6588,12 @@ function createRoomEngine(options) {
6538
6588
  function failJoin(error, operation) {
6539
6589
  const normalized = normalizeMeetError(error, operation);
6540
6590
  store.patch({
6541
- failure: { message: normalized.message, remedy: normalized.remedy, reason: normalized.reason }
6591
+ failure: {
6592
+ message: normalized.message,
6593
+ remedy: normalized.remedy,
6594
+ reason: normalized.reason,
6595
+ retryAt: normalized.retryAfterSeconds === null ? null : now() + normalized.retryAfterSeconds * 1e3
6596
+ }
6542
6597
  });
6543
6598
  store.dispatch({ type: "join_failed", reason: normalized.reason ?? null });
6544
6599
  report(
@@ -6610,10 +6665,10 @@ function createRoomEngine(options) {
6610
6665
  if (store.snapshot().spotlight !== spotlight) store.patch({ spotlight });
6611
6666
  },
6612
6667
  activeSpeakers(identities) {
6613
- store.setSpeaking(identities.map(asParticipantIdentity));
6668
+ store.setSpeaking([...new Set(identities.map((who) => personOf(who)))]);
6614
6669
  },
6615
6670
  quality(who, value) {
6616
- store.setQuality(asParticipantIdentity(who), value);
6671
+ store.setQuality(personOf(who), value);
6617
6672
  },
6618
6673
  connectionState(state) {
6619
6674
  supervisor.driverState(state);
@@ -6633,7 +6688,7 @@ function createRoomEngine(options) {
6633
6688
  );
6634
6689
  return;
6635
6690
  }
6636
- const speakerIdentity = asParticipantIdentity(event.speakerIdentity);
6691
+ const speakerIdentity = personOf(event.speakerIdentity);
6637
6692
  const speaker = snapshot.participants.find(
6638
6693
  (participant) => participant.identity === speakerIdentity
6639
6694
  );
@@ -6707,7 +6762,7 @@ function createRoomEngine(options) {
6707
6762
  );
6708
6763
  return;
6709
6764
  }
6710
- const sender = asParticipantIdentity(from ?? "");
6765
+ const sender = personOf(from ?? "");
6711
6766
  switch (topic) {
6712
6767
  case DATA_TOPICS.hand:
6713
6768
  store.setHand(
@@ -6749,7 +6804,7 @@ function createRoomEngine(options) {
6749
6804
  }
6750
6805
  };
6751
6806
  async function resolveExitAndApply(hint) {
6752
- const connection = current?.identity ?? store.snapshot().localIdentity ?? null;
6807
+ const connection = current?.connectionIdentity ?? current?.identity ?? store.snapshot().localIdentity ?? null;
6753
6808
  lastRequest = requestAsNow();
6754
6809
  await dropRoom();
6755
6810
  let server;
@@ -6787,14 +6842,29 @@ function createRoomEngine(options) {
6787
6842
  }
6788
6843
  async function connect(request) {
6789
6844
  store.patch({ failure: null, waiting: null, roomName: request.roomName, share: NO_SHARE, connection: "stable" });
6845
+ const tabId = options.tabClaim !== void 0 ? await options.tabClaim.tabId : null;
6790
6846
  const token = await tokens.get({
6791
6847
  roomName: request.roomName,
6792
6848
  identity,
6793
6849
  ...request.meetingId !== void 0 ? { meetingId: request.meetingId } : {},
6794
6850
  ...request.slug !== void 0 ? { slug: request.slug } : {},
6795
6851
  ...request.breakoutRoomId != null ? { breakoutRoomId: request.breakoutRoomId } : {},
6796
- ...request.observe === true ? { observe: true } : {}
6852
+ ...request.observe === true ? { observe: true } : {},
6853
+ ...tabId !== null ? { tabId } : {},
6854
+ ...request.secondDevice !== void 0 ? { secondDevice: request.secondDevice } : {}
6797
6855
  });
6856
+ if (token.secondDeviceChoice != null) {
6857
+ store.patch({ secondDevice: { rule: token.secondDeviceChoice }, localIdentity: token.identity });
6858
+ store.dispatch({ type: "second_device" });
6859
+ report("info", `meet: already in this meeting elsewhere \u2014 offering the choice (${token.secondDeviceChoice})`);
6860
+ return;
6861
+ }
6862
+ if (store.snapshot().secondDevice !== null) store.patch({ secondDevice: null });
6863
+ if (request.secondDevice === "switch_here" && lastRequest !== null) {
6864
+ const { secondDevice: _answered, ...rest } = lastRequest;
6865
+ lastRequest = rest;
6866
+ }
6867
+ options.tabClaim?.inRoom(String(request.roomName), String(token.identity));
6798
6868
  current = token;
6799
6869
  lastServerUrl = token.serverUrl;
6800
6870
  connectionPolicy = token.connectionPolicy ?? DEFAULT_CONNECTION_POLICY;
@@ -6859,7 +6929,7 @@ function createRoomEngine(options) {
6859
6929
  if (request.mode === "video") {
6860
6930
  await enableTrack(created, "camera", true);
6861
6931
  }
6862
- if (request.microphone !== false) {
6932
+ if (request.microphone !== false && current?.connectionKind !== "companion") {
6863
6933
  await enableTrack(created, "microphone", true);
6864
6934
  }
6865
6935
  media.onConnected();
@@ -6881,7 +6951,8 @@ function createRoomEngine(options) {
6881
6951
  }
6882
6952
  async function publish(topic, body, to) {
6883
6953
  if (driver === null) return;
6884
- await driver.publishData(topic, JSON.stringify(body), to);
6954
+ const addresses = to === void 0 ? void 0 : to.flatMap((who) => connectionsOf(who));
6955
+ await driver.publishData(topic, JSON.stringify(body), addresses);
6885
6956
  }
6886
6957
  async function applyEffects(target, effects) {
6887
6958
  try {
@@ -6919,8 +6990,19 @@ function createRoomEngine(options) {
6919
6990
  liveRoster = list;
6920
6991
  const merged = mergeGhosts(list, ghosts, at);
6921
6992
  ghosts = merged.kept;
6993
+ const people = /* @__PURE__ */ new Map();
6994
+ const connections = /* @__PURE__ */ new Map();
6995
+ for (const entry of merged.roster) {
6996
+ const person = personOf(entry.identity);
6997
+ connections.set(person, [...connections.get(person) ?? [], entry.identity]);
6998
+ 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);
6999
+ const held = people.get(person);
7000
+ if (held === void 0 || rank > held.rank) people.set(person, { entry, rank });
7001
+ }
7002
+ personConnections = connections;
7003
+ mainConnection = new Map([...people].map(([person, { entry }]) => [person, entry.identity]));
6922
7004
  store.setParticipants(
6923
- merged.roster.map((entry) => {
7005
+ [...people.values()].map(({ entry }) => {
6924
7006
  const projected = projectParticipant(entry, joinedAt);
6925
7007
  return entry.reconnecting === true ? { ...projected, reconnecting: true, quality: "lost" } : projected;
6926
7008
  })
@@ -7097,7 +7179,13 @@ function createRoomEngine(options) {
7097
7179
  },
7098
7180
  async setScreenShareEnabled(enabled) {
7099
7181
  if (driver === null) return;
7100
- if (enabled && driver.supportsScreenShare?.() === false) {
7182
+ let unsupported = false;
7183
+ try {
7184
+ unsupported = enabled && driver.supportsScreenShare?.() === false;
7185
+ } catch {
7186
+ unsupported = false;
7187
+ }
7188
+ if (unsupported) {
7101
7189
  store.patch({ share: { state: "refused", reason: "unsupported", remedy: null, at: now() } });
7102
7190
  report("warn", "meet: this browser cannot share a screen", "Share from a desktop browser (Chrome, Edge, Firefox or Safari).");
7103
7191
  return;
@@ -7196,7 +7284,7 @@ function createRoomEngine(options) {
7196
7284
  },
7197
7285
  attach(who, kind, element) {
7198
7286
  if (driver === null) return () => void 0;
7199
- return driver.attach(who, kind, element);
7287
+ return driver.attach(mainConnection.get(personOf(who)) ?? who, kind, element);
7200
7288
  },
7201
7289
  token: () => current,
7202
7290
  async dispose() {