@glyphteck/veyl 0.72.0 → 0.74.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/account.js CHANGED
@@ -16676,17 +16676,6 @@ function canonicalBytes(value, label = "value") {
16676
16676
  return encoder.encode(canonicalJson(value, label));
16677
16677
  }
16678
16678
 
16679
- // ../../core/utils/text.js
16680
- function cleanText(value) {
16681
- return typeof value === "string" ? value.trim() : "";
16682
- }
16683
- function lowerText(value) {
16684
- return cleanText(value).toLowerCase();
16685
- }
16686
- function sameText(left, right) {
16687
- return lowerText(left) === lowerText(right);
16688
- }
16689
-
16690
16679
  // ../../core/config.js
16691
16680
  var MS_PER_SECOND = 1000;
16692
16681
  var MINUTE_MS = 60 * MS_PER_SECOND;
@@ -16708,6 +16697,8 @@ var AUTOLOCK_MAX_MINUTES = 60;
16708
16697
  var BTC_PRICE_FALLBACK = 80000;
16709
16698
  var CHAT_MESSAGE_EDIT_WINDOW_MS = 10 * MINUTE_MS;
16710
16699
  var CHAT_SEND_CONNECTION_WAIT_MS = 30 * MS_PER_SECOND;
16700
+ var CALL_SPEAKING_THRESHOLD = 0.02;
16701
+ var CALL_SPEAKING_HOLD_MS = 320;
16711
16702
  var LOCAL_MEDIA_CACHE_MAX_BYTES = 512 * MIB_BYTES;
16712
16703
  var LOCAL_PROFILE_CACHE_MAX_ITEMS = 500;
16713
16704
  var LOCAL_PROFILE_CACHE_MAX_AGE_MS = 30 * DAY_MS;
@@ -16827,6 +16818,341 @@ var WALLET_PENDING_TRANSFER_STALE_RETRY_MS = 2 * MINUTE_MS;
16827
16818
  var WALLET_PENDING_TRANSFER_STUCK_RETRY_MS = 10 * MINUTE_MS;
16828
16819
  var WALLET_PENDING_TRANSFER_DORMANT_RETRY_MS = HOUR_MS;
16829
16820
  var WALLET_TRANSFER_CACHE_WRITE_DELAY_MS = 3 * MS_PER_SECOND;
16821
+ var BITCOIN_FEES_MAX_AGE_MS = 5 * 60000;
16822
+
16823
+ // ../../core/calls/wire.js
16824
+ var CALL_REQUEST_MAX_BYTES = 192 * 1024;
16825
+ var CALL_HEAD_MAX_BYTES = 64 * 1024;
16826
+ var CALL_HEAD_PROOF_MAX_BYTES = 2048;
16827
+ var CALL_EVENT_MAX_BYTES = 48 * 1024;
16828
+ var CALL_LOG_MAX_BYTES = 512 * 1024;
16829
+ var CALL_LOG_MAX_EVENTS = 128;
16830
+ var CALL_RESPONSE_MAX_BYTES = Math.ceil((CALL_HEAD_MAX_BYTES + CALL_HEAD_PROOF_MAX_BYTES + CALL_LOG_MAX_BYTES) / 3) * 4 + CALL_LOG_MAX_EVENTS * 128 + 4096;
16831
+ var CALL_SCOPE_KINDS = ["ownership", "discovery", "signaling"];
16832
+ var SCOPE_SOCKETS = Object.freeze({ ownership: 8, discovery: CHAT_MAX_MEMBERS * 4, signaling: 32 });
16833
+ var SCOPE_LIMITS = Object.freeze(Object.fromEntries(Object.entries(SCOPE_SOCKETS).map(([kind, sockets]) => [
16834
+ kind,
16835
+ Object.freeze({ sockets, readsPerMinute: (kind === "signaling" ? SCOPE_SOCKETS.discovery : sockets) * 4, writesPerMinute: 600 })
16836
+ ])));
16837
+ var HEX_KEY = /^[a-f0-9]{64}$/u;
16838
+ function callWireError(code = "invalid-request") {
16839
+ const error = new Error(`call ${code}`);
16840
+ error.code = `calls/${code}`;
16841
+ return error;
16842
+ }
16843
+ function encodeCallBytes(value) {
16844
+ if (!(value instanceof Uint8Array))
16845
+ throw callWireError();
16846
+ let text = "";
16847
+ for (const byte of value)
16848
+ text += String.fromCharCode(byte);
16849
+ return btoa(text);
16850
+ }
16851
+ function callHeadDigest(value) {
16852
+ if (!(value instanceof Uint8Array) || !value.length || value.length > CALL_HEAD_MAX_BYTES)
16853
+ throw callWireError();
16854
+ return toHex(sha256(value));
16855
+ }
16856
+ function decodeCallBytes(value, maximum = CALL_HEAD_MAX_BYTES) {
16857
+ if (typeof value !== "string" || value.length > Math.ceil(maximum / 3) * 4 || !/^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?$/u.test(value))
16858
+ throw callWireError();
16859
+ const bytes = Uint8Array.from(atob(value), (char) => char.charCodeAt(0));
16860
+ if (bytes.length > maximum || encodeCallBytes(bytes) !== value)
16861
+ throw callWireError();
16862
+ return bytes;
16863
+ }
16864
+ function callScope(realm, kind, publicKey) {
16865
+ if (!["prod", "dev"].includes(realm) || !CALL_SCOPE_KINDS.includes(kind) || !HEX_KEY.test(publicKey))
16866
+ throw callWireError();
16867
+ return toHex(sha256(canonicalBytes(["veyl.call.scope.v1", realm, kind, publicKey])));
16868
+ }
16869
+ function decodeCallRecord(record) {
16870
+ return {
16871
+ ...record,
16872
+ active: record.active ? { ...record.active, payload: decodeCallBytes(record.active.payload, 4096) } : null,
16873
+ pending: record.pending ? { ...record.pending, payload: decodeCallBytes(record.pending.payload, 4096) } : null
16874
+ };
16875
+ }
16876
+
16877
+ // ../../core/calls/identity.js
16878
+ var REALMS = new Set(["prod", "dev"]);
16879
+ var KINDS = new Set(["ownership", "discovery", "signaling"]);
16880
+ var MAX_SEALED_BYTES = 64 * 1024;
16881
+ function createCallCapability(seed, realm, kind, parts = []) {
16882
+ if (!REALMS.has(realm) || !KINDS.has(kind))
16883
+ throw new Error("invalid call capability");
16884
+ const signing = deriveKey(toBytes32(seed, "call root"), "veyl.call.capability.signing.v1", [realm, kind, ...parts]);
16885
+ const sealing = deriveKey(toBytes32(seed, "call root"), "veyl.call.capability.sealing.v1", [realm, kind, ...parts]);
16886
+ const publicKey = toHex(ed25519.getPublicKey(signing));
16887
+ const scope = callScope(realm, kind, publicKey);
16888
+ const aad = canonicalBytes(["veyl.call.sealed.v1", realm, kind, scope]);
16889
+ let owners = 0;
16890
+ function retain() {
16891
+ owners += 1;
16892
+ let closed = false;
16893
+ function current() {
16894
+ if (closed)
16895
+ throw new Error("call capability closed");
16896
+ }
16897
+ return Object.freeze({
16898
+ realm,
16899
+ kind,
16900
+ scope,
16901
+ publicKey,
16902
+ sign(bytes) {
16903
+ current();
16904
+ return toHex(ed25519.sign(toBytes(bytes), signing));
16905
+ },
16906
+ async seal(value) {
16907
+ current();
16908
+ const plain = canonicalBytes(value, "call payload");
16909
+ if (plain.length + 28 > MAX_SEALED_BYTES)
16910
+ throw new Error("call payload too large");
16911
+ try {
16912
+ const { iv, ct } = await sealAes(sealing, plain, aad);
16913
+ current();
16914
+ const result = new Uint8Array(iv.length + ct.length);
16915
+ result.set(iv);
16916
+ result.set(ct, iv.length);
16917
+ return result;
16918
+ } finally {
16919
+ cleanBytes(plain);
16920
+ }
16921
+ },
16922
+ async open(value) {
16923
+ current();
16924
+ const bytes = toBytes(value);
16925
+ if (bytes.length < 28 || bytes.length > MAX_SEALED_BYTES)
16926
+ throw new Error("invalid call payload");
16927
+ const plain = await openAes(sealing, bytes.subarray(0, 12), bytes.subarray(12), aad);
16928
+ try {
16929
+ current();
16930
+ return JSON.parse(decoder.decode(plain));
16931
+ } finally {
16932
+ cleanBytes(plain);
16933
+ }
16934
+ },
16935
+ retain() {
16936
+ current();
16937
+ return retain();
16938
+ },
16939
+ close() {
16940
+ if (closed)
16941
+ return;
16942
+ closed = true;
16943
+ if (--owners === 0)
16944
+ cleanBytes(signing, sealing);
16945
+ }
16946
+ });
16947
+ }
16948
+ return retain();
16949
+ }
16950
+ function createCallIdentity(masterSeed, realm) {
16951
+ const root = deriveKey(toBytes32(masterSeed, "vault root"), "veyl.call.account.v1", [realm]);
16952
+ try {
16953
+ return createCallCapability(root, realm, "ownership");
16954
+ } finally {
16955
+ cleanBytes(root);
16956
+ }
16957
+ }
16958
+ function createCallEndpoint() {
16959
+ const secret = randomBytes3(32);
16960
+ const publicKey = toHex(ed25519.getPublicKey(secret));
16961
+ let closed = false;
16962
+ return Object.freeze({
16963
+ publicKey,
16964
+ holder: toHex(randomBytes3(16)),
16965
+ sign(bytes) {
16966
+ if (closed)
16967
+ throw new Error("call endpoint closed");
16968
+ return toHex(ed25519.sign(toBytes(bytes), secret));
16969
+ },
16970
+ close() {
16971
+ closed = true;
16972
+ cleanBytes(secret);
16973
+ }
16974
+ });
16975
+ }
16976
+ function createCallDiscovery(epochState, realm) {
16977
+ const manifest = epochState?.manifest;
16978
+ if (!manifest?.chatId || !manifest?.epochId || !epochState?.epochSecret)
16979
+ throw new Error("current chat epoch required");
16980
+ return createCallCapability(toBytes32(epochState.epochSecret), realm, "discovery", [manifest.chatId, manifest.epochId]);
16981
+ }
16982
+
16983
+ // ../../core/utils/text.js
16984
+ function cleanText(value) {
16985
+ return typeof value === "string" ? value.trim() : "";
16986
+ }
16987
+ function lowerText(value) {
16988
+ return cleanText(value).toLowerCase();
16989
+ }
16990
+ function sameText(left, right) {
16991
+ return lowerText(left) === lowerText(right);
16992
+ }
16993
+
16994
+ // ../../core/utils/time.js
16995
+ function timestampMs(value, fallback = null, options = {}) {
16996
+ let ms = null;
16997
+ if (typeof value?.toMillis === "function") {
16998
+ ms = value.toMillis();
16999
+ } else if (value instanceof Date) {
17000
+ ms = value.getTime();
17001
+ } else if (typeof value?.seconds === "number") {
17002
+ ms = value.seconds * 1000 + Math.floor((value.nanoseconds || 0) / 1e6);
17003
+ } else if (typeof value?._seconds === "number") {
17004
+ ms = value._seconds * 1000 + Math.floor((value._nanoseconds || 0) / 1e6);
17005
+ } else if (Number.isFinite(value)) {
17006
+ ms = value;
17007
+ } else if (options.parseString && typeof value === "string") {
17008
+ const numberMs = Number(value);
17009
+ ms = Number.isFinite(numberMs) ? numberMs : Date.parse(value);
17010
+ }
17011
+ if (!Number.isFinite(ms) || options.positive && ms <= 0) {
17012
+ return fallback;
17013
+ }
17014
+ return ms;
17015
+ }
17016
+ function timestampKey(value) {
17017
+ if (value == null) {
17018
+ return null;
17019
+ }
17020
+ return timestampMs(value, null) ?? String(value);
17021
+ }
17022
+ function makeTimestamp(ms) {
17023
+ return {
17024
+ toMillis() {
17025
+ return ms;
17026
+ },
17027
+ toDate() {
17028
+ return new Date(ms);
17029
+ }
17030
+ };
17031
+ }
17032
+ function twoDigits(value) {
17033
+ return String(value).padStart(2, "0");
17034
+ }
17035
+ function dayKey(date) {
17036
+ return `${date.getFullYear()}-${twoDigits(date.getMonth() + 1)}-${twoDigits(date.getDate())}`;
17037
+ }
17038
+ function localDayKey(value) {
17039
+ const ms = timestampMs(value, null, { parseString: true });
17040
+ if (!Number.isFinite(ms))
17041
+ return "";
17042
+ return dayKey(new Date(ms));
17043
+ }
17044
+ function hourKey(dateOrHour) {
17045
+ const hour = dateOrHour instanceof Date ? dateOrHour.getHours() : dateOrHour;
17046
+ return twoDigits(hour);
17047
+ }
17048
+ function dayHourKey(date) {
17049
+ return `${dayKey(date)}-${hourKey(date)}`;
17050
+ }
17051
+ var MINUTE_MS2 = 60000;
17052
+ var HOUR_MS2 = 60 * MINUTE_MS2;
17053
+ function nextLocalDayStartMs(ms) {
17054
+ const date = new Date(ms);
17055
+ date.setDate(date.getDate() + 1);
17056
+ date.setHours(0, 0, 0, 0);
17057
+ return date.getTime();
17058
+ }
17059
+ function nextRowDateTimeRefreshMs(value, now = Date.now()) {
17060
+ const ms = timestampMs(value, null, { parseString: true });
17061
+ const nowMs2 = timestampMs(now, Date.now(), { parseString: true });
17062
+ if (!Number.isFinite(ms) || !Number.isFinite(nowMs2))
17063
+ return null;
17064
+ const age = nowMs2 - ms;
17065
+ if (age < -MINUTE_MS2)
17066
+ return ms - MINUTE_MS2;
17067
+ if (age < MINUTE_MS2)
17068
+ return ms + MINUTE_MS2;
17069
+ if (age < HOUR_MS2)
17070
+ return ms + (Math.floor(age / MINUTE_MS2) + 1) * MINUTE_MS2;
17071
+ if (localDayKey(ms) === localDayKey(nowMs2))
17072
+ return nextLocalDayStartMs(nowMs2);
17073
+ return null;
17074
+ }
17075
+
17076
+ // ../../core/chat/state.js
17077
+ function makeCid() {
17078
+ return `${Date.now().toString(36)}${toHex(randomBytes3(3))}`;
17079
+ }
17080
+ function getMessageKey(message) {
17081
+ return message?.cid || message?.id || null;
17082
+ }
17083
+ function getCidMs(cid) {
17084
+ if (typeof cid !== "string" || !/^[0-9a-z]+[0-9a-f]{6}$/u.test(cid)) {
17085
+ return null;
17086
+ }
17087
+ const base = cid.slice(0, -6);
17088
+ const ms = Number.parseInt(base, 36);
17089
+ return Number.isSafeInteger(ms) && ms > 0 ? ms : null;
17090
+ }
17091
+ function getMessageOrderMs(message) {
17092
+ return getCidMs(message?.cid) ?? timestampMs(message?.ts, Infinity);
17093
+ }
17094
+ function sortMessages(messages) {
17095
+ return [...messages].sort((a, b) => {
17096
+ const aMs = getMessageOrderMs(a);
17097
+ const bMs = getMessageOrderMs(b);
17098
+ if (aMs !== bMs) {
17099
+ return aMs - bMs;
17100
+ }
17101
+ return String(a?.id || "").localeCompare(String(b?.id || ""));
17102
+ });
17103
+ }
17104
+ function mergeMessages(...groups) {
17105
+ const merged = new Map;
17106
+ for (const group of groups) {
17107
+ for (const message of group || []) {
17108
+ const key = getMessageKey(message);
17109
+ if (!key) {
17110
+ continue;
17111
+ }
17112
+ merged.set(key, message);
17113
+ }
17114
+ }
17115
+ return sortMessages([...merged.values()]);
17116
+ }
17117
+
17118
+ // ../../core/chat/messages/call.js
17119
+ var CALL_MSG_TYPE = "call";
17120
+ function readCallMessage(message) {
17121
+ if (message?.t !== CALL_MSG_TYPE || typeof message.callId !== "string" || !/^[0-9a-f]{64}$/u.test(message.callId) || !["started", "ended"].includes(message.event))
17122
+ return null;
17123
+ return { callId: message.callId, event: message.event };
17124
+ }
17125
+ function makeCallMessage(callId, event) {
17126
+ const message = { t: CALL_MSG_TYPE, callId, event };
17127
+ if (!readCallMessage(message))
17128
+ throw new Error("invalid call message");
17129
+ return message;
17130
+ }
17131
+ function callMessageText(message) {
17132
+ const call = readCallMessage(message);
17133
+ return call ? `call ${call.event}` : "";
17134
+ }
17135
+ function latestCallStartKey(messages) {
17136
+ for (let index = messages.length - 1;index >= 0; index -= 1) {
17137
+ if (readCallMessage(messages[index])?.event === "started")
17138
+ return getMessageKey(messages[index]);
17139
+ }
17140
+ return "";
17141
+ }
17142
+
17143
+ // ../../core/chat/messages/types.js
17144
+ var ATTACHMENT_MSG_TYPES = ["img", "gif", "m4a", "mp4", "file"];
17145
+ var MAX_TXT_CHARS = CHAT_MAX_TEXT_CHARS;
17146
+ var REACTION_MSG_TYPE = "rxn";
17147
+ var DELETE_MSG_TYPE = "del";
17148
+ var SYSTEM_MSG_TYPE = "sys";
17149
+ var EPOCH_TRANSITION_MSG_TYPE = "epoch";
17150
+ var EPOCH_PROPOSAL_MSG_TYPE = "epoch_proposal";
17151
+ var SYSTEM_SETTINGS_KIND = "settings";
17152
+ var DEFAULT_REACTION_EMOJI = "❤️";
17153
+ var MAX_REACTIONS = CHAT_MAX_REACTIONS;
17154
+ var HOLD_VISIBLE_KEY = "__holdVisible";
17155
+ var SOURCE_GONE_VISIBLE_KEY = "__sourceGoneVisible";
16830
17156
 
16831
17157
  // ../../core/notifications.js
16832
17158
  "use client";
@@ -16843,10 +17169,47 @@ var CHAT_ATTENTION_KINDS = Object.freeze({
16843
17169
  ROTATION: "rotation",
16844
17170
  SILENT: "silent"
16845
17171
  });
17172
+ var NOTIFICATION_PRESENTATION_KINDS = Object.freeze({
17173
+ MESSAGE: 0,
17174
+ REACTION: 1,
17175
+ ACTIVITY: 2,
17176
+ PHOTO: 3,
17177
+ VIDEO: 4,
17178
+ AUDIO: 5,
17179
+ FILE: 6,
17180
+ CALL_STARTED: 7,
17181
+ CALL_ENDED: 8
17182
+ });
17183
+ function notificationMessageKind(message) {
17184
+ switch (message?.t) {
17185
+ case SYSTEM_MSG_TYPE:
17186
+ case EPOCH_TRANSITION_MSG_TYPE:
17187
+ return NOTIFICATION_PRESENTATION_KINDS.ACTIVITY;
17188
+ case "img":
17189
+ case "gif":
17190
+ return NOTIFICATION_PRESENTATION_KINDS.PHOTO;
17191
+ case "mp4":
17192
+ return NOTIFICATION_PRESENTATION_KINDS.VIDEO;
17193
+ case "m4a":
17194
+ return NOTIFICATION_PRESENTATION_KINDS.AUDIO;
17195
+ case "file":
17196
+ return NOTIFICATION_PRESENTATION_KINDS.FILE;
17197
+ case "call": {
17198
+ const call = readCallMessage(message);
17199
+ return !call ? NOTIFICATION_PRESENTATION_KINDS.ACTIVITY : call.event === "started" ? NOTIFICATION_PRESENTATION_KINDS.CALL_STARTED : NOTIFICATION_PRESENTATION_KINDS.CALL_ENDED;
17200
+ }
17201
+ default:
17202
+ return NOTIFICATION_PRESENTATION_KINDS.MESSAGE;
17203
+ }
17204
+ }
17205
+ function notificationMessageIsSilent(message) {
17206
+ return message?.t === SYSTEM_MSG_TYPE && message.sys === SYSTEM_SETTINGS_KIND || readCallMessage(message)?.event === "ended";
17207
+ }
16846
17208
  var HEX_32_RE = /^[0-9a-f]{64}$/u;
16847
17209
  var PEER_TAG_RE = /^[0-9a-f]{32}$/u;
16848
17210
  var ATTENTION_KINDS = new Set(Object.values(CHAT_ATTENTION_KINDS));
16849
17211
  var NOTIFICATION_MODES = new Set(Object.values(CHAT_NOTIFICATION_MODES));
17212
+ var PRESENTATION_KINDS = new Set(Object.values(NOTIFICATION_PRESENTATION_KINDS));
16850
17213
  function hasCurrentChatAttentionRegistration(value) {
16851
17214
  return value?.deliveryRegistered === true && value?.attentionRegistrationVersion === CHAT_ATTENTION_REGISTRATION_VERSION;
16852
17215
  }
@@ -17002,13 +17365,30 @@ function notificationRouteTag(envelope) {
17002
17365
  cleanBytes(input);
17003
17366
  }
17004
17367
  }
17005
- async function sealNotificationDescriptor(recipientNotificationPK, descriptor = {}) {
17006
- const recipientPK = cleanHex32(recipientNotificationPK, "recipient notification key");
17368
+ function notificationDescriptorClaims(descriptor) {
17007
17369
  const peerTag = cleanText(descriptor.peerTag).toLowerCase();
17008
17370
  const eventId = cleanText(descriptor.eventId);
17009
- if (!PEER_TAG_RE.test(peerTag) || !eventId || eventId.length > 256) {
17371
+ const senderChatPK = cleanHex32(descriptor.senderChatPK, "notification sender key");
17372
+ const kind = descriptor.kind;
17373
+ if (!PEER_TAG_RE.test(peerTag) || !eventId || eventId.length > 256 || !PRESENTATION_KINDS.has(kind)) {
17010
17374
  throw new Error("notification descriptor required");
17011
17375
  }
17376
+ return { v: NOTIFICATION_DESCRIPTOR_VERSION, peerTag, eventId, senderChatPK, kind };
17377
+ }
17378
+ function notificationDescriptorSignatureInput(recipientPK, descriptor) {
17379
+ return encoder.encode([
17380
+ "veyl-notification-presentation-v1",
17381
+ recipientPK,
17382
+ descriptor.peerTag,
17383
+ descriptor.senderChatPK,
17384
+ descriptor.kind,
17385
+ descriptor.eventId
17386
+ ].join("\x00"));
17387
+ }
17388
+ async function sealNotificationDescriptor(recipientNotificationPK, descriptor, sender) {
17389
+ const recipientPK = cleanHex32(recipientNotificationPK, "recipient notification key");
17390
+ const claims = notificationDescriptorClaims({ ...descriptor, senderChatPK: sender?.chatPK });
17391
+ const signature = signChatBytes(sender?.signingKey, notificationDescriptorSignatureInput(recipientPK, claims));
17012
17392
  let eph = null;
17013
17393
  let shared = null;
17014
17394
  let key = null;
@@ -17019,9 +17399,8 @@ async function sealNotificationDescriptor(recipientNotificationPK, descriptor =
17019
17399
  const epk = toHex(eph.pub);
17020
17400
  key = deriveKey(shared.subarray(0, 32), "notification-descriptor-v1", [epk, recipientPK]);
17021
17401
  plaintext = encoder.encode(JSON.stringify({
17022
- v: NOTIFICATION_DESCRIPTOR_VERSION,
17023
- peerTag,
17024
- eventId
17402
+ ...claims,
17403
+ signature
17025
17404
  }));
17026
17405
  const { iv, ct } = await sealAes(key, plaintext, descriptorAad(epk, recipientPK));
17027
17406
  return {
@@ -17294,7 +17673,8 @@ var domains = Object.freeze({
17294
17673
  veyl: `veyl.${ROOT_DOMAIN}`,
17295
17674
  veylDev: `dev.veyl.${ROOT_DOMAIN}`,
17296
17675
  live: `live.veyl.${ROOT_DOMAIN}`,
17297
- liveDev: `live.dev.veyl.${ROOT_DOMAIN}`
17676
+ liveDev: `live.dev.veyl.${ROOT_DOMAIN}`,
17677
+ bitcoin: `bitcoin.veyl.${ROOT_DOMAIN}`
17298
17678
  });
17299
17679
  function getVeylDevWebOrigin(port) {
17300
17680
  const value = Number(port);
@@ -17315,6 +17695,7 @@ var liveEndpoints = Object.freeze({
17315
17695
  prod: `wss://${domains.live}`,
17316
17696
  dev: `wss://${domains.liveDev}`
17317
17697
  });
17698
+ var bitcoinEndpoint = `https://${domains.bitcoin}/current`;
17318
17699
  var appDomains = Object.freeze([
17319
17700
  domains.veyl
17320
17701
  ]);
@@ -17348,9 +17729,11 @@ function isAddressOnNetwork(address, network) {
17348
17729
 
17349
17730
  // ../../core/settings.js
17350
17731
  var SEND_ON_SCAN_ENABLED = false;
17732
+ var WEB_LAYOUTS = ["floating", "sidebar"];
17351
17733
  var WALLET_NETWORKS = new Set([MAINNET_NETWORK, REGTEST_NETWORK]);
17352
17734
  var defaultSettings = {
17353
17735
  glass: true,
17736
+ webLayout: "floating",
17354
17737
  moneyFormat: "usd",
17355
17738
  ghostWallet: true,
17356
17739
  showChatPreviews: true,
@@ -17358,6 +17741,8 @@ var defaultSettings = {
17358
17741
  confirmSend: false,
17359
17742
  faceID: null,
17360
17743
  walletNetwork: null,
17744
+ peerAudio: {},
17745
+ callAudio: { muted: false, deafened: false },
17361
17746
  autolock: {
17362
17747
  timer: "never",
17363
17748
  onHide: false,
@@ -17419,9 +17804,14 @@ function normalizeSettings(settings, base = defaultSettings) {
17419
17804
  };
17420
17805
  next.autolock = normalizeAutolock(settings.autolock, current.autolock);
17421
17806
  next.walletNetwork = normalizeWalletNetworkSetting(next.walletNetwork);
17807
+ next.peerAudio = normalizePeerAudio(settings.peerAudio, current.peerAudio);
17808
+ next.callAudio = normalizeCallAudio(settings.callAudio, current.callAudio);
17422
17809
  if (!MONEY_FORMATS.includes(next.moneyFormat)) {
17423
17810
  throw new Error("bad moneyFormat");
17424
17811
  }
17812
+ if (!WEB_LAYOUTS.includes(next.webLayout)) {
17813
+ throw new Error("bad webLayout");
17814
+ }
17425
17815
  if (typeof next.glass !== "boolean") {
17426
17816
  throw new Error("glass must be boolean");
17427
17817
  }
@@ -17445,6 +17835,32 @@ function normalizeSettings(settings, base = defaultSettings) {
17445
17835
  }
17446
17836
  return next;
17447
17837
  }
17838
+ function normalizeCallAudio(patch, base = defaultSettings.callAudio) {
17839
+ if (patch !== undefined && (!patch || typeof patch !== "object" || Array.isArray(patch)))
17840
+ throw new Error("bad call audio settings");
17841
+ const { muted, deafened } = { ...defaultSettings.callAudio, ...base, ...patch };
17842
+ if (typeof muted !== "boolean" || typeof deafened !== "boolean")
17843
+ throw new Error("bad call audio settings");
17844
+ return { muted, deafened };
17845
+ }
17846
+ function normalizePeerAudio(patch, base = {}) {
17847
+ if (patch === undefined)
17848
+ return base;
17849
+ if (!patch || typeof patch !== "object" || Array.isArray(patch))
17850
+ throw new Error("bad peer audio settings");
17851
+ const entries = new Map(Object.entries(base));
17852
+ for (const [chatPK, audio] of Object.entries(patch)) {
17853
+ if (!/^[a-f0-9]{64}$/u.test(chatPK) || !audio || typeof audio !== "object" || Array.isArray(audio)) {
17854
+ throw new Error("bad peer audio settings");
17855
+ }
17856
+ const { volume = 100, muted = false } = { ...entries.get(chatPK), ...audio };
17857
+ if (!Number.isFinite(volume) || volume < 1 || volume > 200 || typeof muted !== "boolean") {
17858
+ throw new Error("bad peer audio settings");
17859
+ }
17860
+ entries.set(chatPK, { volume, muted });
17861
+ }
17862
+ return Object.fromEntries(entries);
17863
+ }
17448
17864
 
17449
17865
  // ../../node_modules/.bun/@noble+ciphers@2.4.0/node_modules/@noble/ciphers/_arx.js
17450
17866
  var encodeStr = (str) => Uint8Array.from(str.split(""), (c) => c.charCodeAt(0));
@@ -18060,7 +18476,7 @@ function packRegistryData(registry) {
18060
18476
  }
18061
18477
  return concatBytes4(new Uint8Array([SECRET_REGISTRY_ENVELOPE_VERSION]), registry.iv, registry.ct);
18062
18478
  }
18063
- var packSeedData = ({ crypto = VAULT_CRYPTO, salt, iv, ciphertext, ct, registry, kdf = VAULT_KDF }) => {
18479
+ var packSeedData = ({ crypto: crypto2 = VAULT_CRYPTO, salt, iv, ciphertext, ct, registry, kdf = VAULT_KDF }) => {
18064
18480
  const body = ciphertext || ct;
18065
18481
  if (!salt || !iv || !body || !registry) {
18066
18482
  throw new Error("seed data missing");
@@ -18068,7 +18484,7 @@ var packSeedData = ({ crypto = VAULT_CRYPTO, salt, iv, ciphertext, ct, registry,
18068
18484
  if (toBytes(salt, "seed salt").byteLength !== VAULT_SALT_BYTES || toBytes(iv, "seed iv").byteLength !== VAULT_IV_BYTES || toBytes(body, "seed ciphertext").byteLength !== VAULT_SEED_CIPHERTEXT_BYTES) {
18069
18485
  throw new Error("invalid seed data");
18070
18486
  }
18071
- const magic = encoder.encode(crypto);
18487
+ const magic = encoder.encode(crypto2);
18072
18488
  const header = new Uint8Array(1 + magic.length + 8);
18073
18489
  header[0] = magic.length;
18074
18490
  header.set(magic, 1);
@@ -18338,6 +18754,9 @@ function normalizeBitcoinPaymentFeeQuote(feeQuote) {
18338
18754
  };
18339
18755
  }
18340
18756
  function getFeeRateSatsPerVbyte(bitcoin, speed = DEFAULT_FEE_RATE_SPEED) {
18757
+ const observedAt = Date.parse(bitcoin?.fees?.updatedAtIso);
18758
+ if (!Number.isFinite(observedAt) || Date.now() - observedAt > BITCOIN_FEES_MAX_AGE_MS || observedAt > Date.now() + 60000)
18759
+ return null;
18341
18760
  const rates = bitcoin?.fees?.satPerVbyte;
18342
18761
  const key = String(speed || DEFAULT_FEE_RATE_SPEED);
18343
18762
  const keys = FEE_RATE_FALLBACKS[key] ?? [key, ...FEE_RATE_FALLBACKS.default];
@@ -18532,7 +18951,7 @@ function walletPubkey(value) {
18532
18951
  }
18533
18952
  return key;
18534
18953
  }
18535
- function closeSessionResources({ wallet, chatPK, chatPrivateKey, chatSigningSecret, notificationPrivateKey, notificationPublicKey, localCache, vaultAccess, vaultSigner } = {}) {
18954
+ function closeSessionResources({ wallet, chatPK, chatPrivateKey, chatSigningSecret, notificationPrivateKey, notificationPublicKey, localCache, vaultAccess, vaultSigner, callIdentity } = {}) {
18536
18955
  const pending = [];
18537
18956
  const close = (owner) => {
18538
18957
  try {
@@ -18543,6 +18962,7 @@ function closeSessionResources({ wallet, chatPK, chatPrivateKey, chatSigningSecr
18543
18962
  };
18544
18963
  close(vaultAccess);
18545
18964
  close(vaultSigner);
18965
+ close(callIdentity);
18546
18966
  close(localCache);
18547
18967
  lockWallet(wallet);
18548
18968
  lockChat(chatPrivateKey, chatPK);
@@ -18598,6 +19018,7 @@ function createAccountSession(resources = {}) {
18598
19018
  session.localCache = null;
18599
19019
  session.vaultAccess = null;
18600
19020
  session.vaultSigner = null;
19021
+ session.callIdentity = null;
18601
19022
  closePromise = Promise.allSettled(pending).then(() => {
18602
19023
  return;
18603
19024
  });
@@ -18737,6 +19158,7 @@ async function openVaultAccountSession(vault, password, options = {}) {
18737
19158
  let cacheKey = null;
18738
19159
  let settingsKey = null;
18739
19160
  let vaultSigner = null;
19161
+ let callIdentity = null;
18740
19162
  let vaultAccess = null;
18741
19163
  let walletBoot = null;
18742
19164
  let network;
@@ -18777,6 +19199,7 @@ async function openVaultAccountSession(vault, password, options = {}) {
18777
19199
  notificationPK = toHex(notificationPublicKey);
18778
19200
  cacheKey = getCacheSeed(registry);
18779
19201
  vaultSigner = createVaultSigner(masterSeed);
19202
+ callIdentity = createCallIdentity(masterSeed, cloud.environment);
18780
19203
  settingsKey = deriveSettingsKey(cacheKey, uid);
18781
19204
  mark(diag, "vault.unlock.derive.done", { elapsedMs: Date.now() - deriveStartedAt, source });
18782
19205
  cleanBytes(masterSeed);
@@ -18825,6 +19248,7 @@ async function openVaultAccountSession(vault, password, options = {}) {
18825
19248
  notificationPublicKey,
18826
19249
  localCache,
18827
19250
  vaultSigner,
19251
+ callIdentity,
18828
19252
  vaultAccess,
18829
19253
  vaultPK: vaultSigner.publicKey
18830
19254
  });
@@ -18925,7 +19349,7 @@ async function openVaultAccountSession(vault, password, options = {}) {
18925
19349
  return session;
18926
19350
  } catch (error) {
18927
19351
  walletBoot?.close();
18928
- await closeSessionResources({ wallet, chatPK, chatPrivateKey, chatSigningSecret, notificationPrivateKey, notificationPublicKey, localCache, vaultAccess, vaultSigner });
19352
+ await closeSessionResources({ wallet, chatPK, chatPrivateKey, chatSigningSecret, notificationPrivateKey, notificationPublicKey, localCache, vaultAccess, vaultSigner, callIdentity });
18929
19353
  throw error;
18930
19354
  } finally {
18931
19355
  cleanBytes(masterSeed, walletEntropy, chatSeed, cacheKey, settingsKey);
@@ -18962,88 +19386,6 @@ function hasAgreement(user, contract = CURRENT_AGREEMENT) {
18962
19386
  return isAgreementAccepted(user?.agreement, contract);
18963
19387
  }
18964
19388
 
18965
- // ../../core/utils/time.js
18966
- function timestampMs(value, fallback = null, options = {}) {
18967
- let ms = null;
18968
- if (typeof value?.toMillis === "function") {
18969
- ms = value.toMillis();
18970
- } else if (value instanceof Date) {
18971
- ms = value.getTime();
18972
- } else if (typeof value?.seconds === "number") {
18973
- ms = value.seconds * 1000 + Math.floor((value.nanoseconds || 0) / 1e6);
18974
- } else if (typeof value?._seconds === "number") {
18975
- ms = value._seconds * 1000 + Math.floor((value._nanoseconds || 0) / 1e6);
18976
- } else if (Number.isFinite(value)) {
18977
- ms = value;
18978
- } else if (options.parseString && typeof value === "string") {
18979
- const numberMs = Number(value);
18980
- ms = Number.isFinite(numberMs) ? numberMs : Date.parse(value);
18981
- }
18982
- if (!Number.isFinite(ms) || options.positive && ms <= 0) {
18983
- return fallback;
18984
- }
18985
- return ms;
18986
- }
18987
- function timestampKey(value) {
18988
- if (value == null) {
18989
- return null;
18990
- }
18991
- return timestampMs(value, null) ?? String(value);
18992
- }
18993
- function makeTimestamp(ms) {
18994
- return {
18995
- toMillis() {
18996
- return ms;
18997
- },
18998
- toDate() {
18999
- return new Date(ms);
19000
- }
19001
- };
19002
- }
19003
- function twoDigits(value) {
19004
- return String(value).padStart(2, "0");
19005
- }
19006
- function dayKey(date) {
19007
- return `${date.getFullYear()}-${twoDigits(date.getMonth() + 1)}-${twoDigits(date.getDate())}`;
19008
- }
19009
- function localDayKey(value) {
19010
- const ms = timestampMs(value, null, { parseString: true });
19011
- if (!Number.isFinite(ms))
19012
- return "";
19013
- return dayKey(new Date(ms));
19014
- }
19015
- function hourKey(dateOrHour) {
19016
- const hour = dateOrHour instanceof Date ? dateOrHour.getHours() : dateOrHour;
19017
- return twoDigits(hour);
19018
- }
19019
- function dayHourKey(date) {
19020
- return `${dayKey(date)}-${hourKey(date)}`;
19021
- }
19022
- var MINUTE_MS2 = 60000;
19023
- var HOUR_MS2 = 60 * MINUTE_MS2;
19024
- function nextLocalDayStartMs(ms) {
19025
- const date = new Date(ms);
19026
- date.setDate(date.getDate() + 1);
19027
- date.setHours(0, 0, 0, 0);
19028
- return date.getTime();
19029
- }
19030
- function nextRowDateTimeRefreshMs(value, now = Date.now()) {
19031
- const ms = timestampMs(value, null, { parseString: true });
19032
- const nowMs2 = timestampMs(now, Date.now(), { parseString: true });
19033
- if (!Number.isFinite(ms) || !Number.isFinite(nowMs2))
19034
- return null;
19035
- const age = nowMs2 - ms;
19036
- if (age < -MINUTE_MS2)
19037
- return ms - MINUTE_MS2;
19038
- if (age < MINUTE_MS2)
19039
- return ms + MINUTE_MS2;
19040
- if (age < HOUR_MS2)
19041
- return ms + (Math.floor(age / MINUTE_MS2) + 1) * MINUTE_MS2;
19042
- if (localDayKey(ms) === localDayKey(nowMs2))
19043
- return nextLocalDayStartMs(nowMs2);
19044
- return null;
19045
- }
19046
-
19047
19389
  // ../../core/moderation.js
19048
19390
  function banUntilMs(ban) {
19049
19391
  if (!ban || typeof ban !== "object" || Array.isArray(ban) || ban.until == null) {
@@ -19445,6 +19787,9 @@ function readAccountType(profile) {
19445
19787
  function hasPeerKeys(profile) {
19446
19788
  return !!(profile?.walletPK || profile?.chatPK);
19447
19789
  }
19790
+ function hasChatIdentity(profile) {
19791
+ return !!(profile?.uid && profile?.chatPK && profile?.chatSigningPK && profile?.notificationPK);
19792
+ }
19448
19793
  function readProfileChatIdentity(profile) {
19449
19794
  const chat = profile?.identities?.chat;
19450
19795
  return {
@@ -19459,7 +19804,7 @@ function peerUid(peer) {
19459
19804
  return typeof peer === "string" ? cleanText(peer) : cleanText(peer?.uid);
19460
19805
  }
19461
19806
  function isFullProfile(profile) {
19462
- return !!(profile?.uid && (("username" in profile) || ("walletPK" in profile) && ("chatPK" in profile)));
19807
+ return !!(profile?.uid && hasPeerKeys(profile) && ["username", "walletPK", "chatPK", "chatSigningPK", "notificationPK"].every((key) => (key in profile)) && (!profile.chatPK || hasChatIdentity(profile)));
19463
19808
  }
19464
19809
  function normalizeProfile(profile, uid = profile?.uid || null) {
19465
19810
  return {
@@ -19560,6 +19905,8 @@ function createUser({ cloud, network, avatarCache = null, diag = null, onSession
19560
19905
  let authSession = 0;
19561
19906
  let userUid = null;
19562
19907
  let settingsKey = null;
19908
+ let settingsWriter = null;
19909
+ let openedSettingsBody;
19563
19910
  let agreementStored = null;
19564
19911
  let agreementOverride = null;
19565
19912
  let agreementAcceptance = null;
@@ -19948,6 +20295,7 @@ function createUser({ cloud, network, avatarCache = null, diag = null, onSession
19948
20295
  settingsBody: privateData.settings ?? null,
19949
20296
  settingsFromCache: false
19950
20297
  }));
20298
+ refreshUnlockedSettings();
19951
20299
  }), current((error) => {
19952
20300
  markError(diag, "user.settings.snapshot", authStartedAt, error);
19953
20301
  if (revokeAuthenticationError(authUser, error, "settings-listener"))
@@ -20163,10 +20511,12 @@ function createUser({ cloud, network, avatarCache = null, diag = null, onSession
20163
20511
  throw new Error("settings key required");
20164
20512
  if (state.settingsBody === undefined)
20165
20513
  throw new Error("settings not available");
20166
- const nextSettings = state.settingsBody === null ? settingsState() : await openSettings(key, uid, state.settingsBody);
20514
+ const body = state.settingsBody;
20515
+ const nextSettings = body === null ? settingsState() : await openSettings(key, uid, body);
20167
20516
  if (!isCurrentUser(uid, session))
20168
20517
  return nextSettings;
20169
20518
  setSettingsKey(key);
20519
+ openedSettingsBody = body;
20170
20520
  setState((user) => ({
20171
20521
  ...user,
20172
20522
  settings: settingsState(nextSettings)
@@ -20187,15 +20537,59 @@ function createUser({ cloud, network, avatarCache = null, diag = null, onSession
20187
20537
  throw new Error("auth");
20188
20538
  if (!settingsKey)
20189
20539
  throw new Error("settings locked");
20190
- const nextSettings = await cloud.user.settings.write(uid, patch, { currentSettings: state.settings, key: settingsKey });
20191
- if (!isCurrentUser(uid, session))
20192
- return nextSettings;
20193
- setState((user) => ({
20194
- ...user,
20195
- settingsReady: true,
20196
- settings: settingsState(nextSettings)
20197
- }));
20198
- return nextSettings;
20540
+ normalizeSettings(patch, state.settings);
20541
+ if (!settingsWriter || settingsWriter.key !== settingsKey) {
20542
+ settingsWriter = { key: settingsKey, uid, session, pending: [], running: false };
20543
+ }
20544
+ const writer = settingsWriter;
20545
+ const result = new Promise((resolve, reject) => writer.pending.push({ patch, resolve, reject }));
20546
+ writeSettings(writer);
20547
+ return result;
20548
+ }
20549
+ async function writeSettings(writer) {
20550
+ if (writer.running)
20551
+ return;
20552
+ writer.running = true;
20553
+ const current = () => settingsKey === writer.key && isCurrentUser(writer.uid, writer.session);
20554
+ while (writer.pending.length) {
20555
+ const batch = writer.pending.splice(0);
20556
+ try {
20557
+ if (!current())
20558
+ throw new Error("settings locked");
20559
+ const desired = batch.reduce((settings2, { patch }) => normalizeSettings(patch, settings2), state.settings);
20560
+ const { settings, body } = await cloud.user.settings.write(writer.uid, desired, { currentSettings: state.settings, key: writer.key });
20561
+ if (!current())
20562
+ throw new Error("settings locked");
20563
+ openedSettingsBody = body;
20564
+ setState((user) => ({ ...user, settingsReady: true, settings: settingsState(settings), settingsBody: body }));
20565
+ for (const request of batch)
20566
+ request.resolve(settings);
20567
+ } catch (error) {
20568
+ for (const request of batch)
20569
+ request.reject(error);
20570
+ }
20571
+ }
20572
+ writer.running = false;
20573
+ if (settingsWriter === writer)
20574
+ settingsWriter = null;
20575
+ refreshUnlockedSettings();
20576
+ }
20577
+ async function refreshUnlockedSettings() {
20578
+ const key = settingsKey;
20579
+ const body = state.settingsBody;
20580
+ if (!key || body === undefined || body === openedSettingsBody || settingsWriter?.key === key)
20581
+ return;
20582
+ const uid = state.uid;
20583
+ try {
20584
+ const settings = body === null ? settingsState() : await openSettings(key, uid, body);
20585
+ if (settingsKey !== key || state.settingsBody !== body || settingsWriter?.key === key)
20586
+ return;
20587
+ openedSettingsBody = body;
20588
+ setState((user) => ({ ...user, settings: settingsState(settings) }));
20589
+ } catch (error) {
20590
+ if (settingsKey === key && state.settingsBody === body)
20591
+ markError(diag, "user.settings.open", Date.now(), error);
20592
+ }
20199
20593
  }
20200
20594
  function isBlocked(peer) {
20201
20595
  const nextPeerUid = peerUid(peer);
@@ -20954,48 +21348,6 @@ function readAccountBootstrap(value, uid) {
20954
21348
  };
20955
21349
  }
20956
21350
 
20957
- // ../../core/chat/state.js
20958
- function makeCid() {
20959
- return `${Date.now().toString(36)}${toHex(randomBytes3(3))}`;
20960
- }
20961
- function getMessageKey(message) {
20962
- return message?.cid || message?.id || null;
20963
- }
20964
- function getCidMs(cid) {
20965
- if (typeof cid !== "string" || !/^[0-9a-z]+[0-9a-f]{6}$/u.test(cid)) {
20966
- return null;
20967
- }
20968
- const base = cid.slice(0, -6);
20969
- const ms = Number.parseInt(base, 36);
20970
- return Number.isSafeInteger(ms) && ms > 0 ? ms : null;
20971
- }
20972
- function getMessageOrderMs(message) {
20973
- return getCidMs(message?.cid) ?? timestampMs(message?.ts, Infinity);
20974
- }
20975
- function sortMessages(messages) {
20976
- return [...messages].sort((a, b) => {
20977
- const aMs = getMessageOrderMs(a);
20978
- const bMs = getMessageOrderMs(b);
20979
- if (aMs !== bMs) {
20980
- return aMs - bMs;
20981
- }
20982
- return String(a?.id || "").localeCompare(String(b?.id || ""));
20983
- });
20984
- }
20985
- function mergeMessages(...groups) {
20986
- const merged = new Map;
20987
- for (const group of groups) {
20988
- for (const message of group || []) {
20989
- const key = getMessageKey(message);
20990
- if (!key) {
20991
- continue;
20992
- }
20993
- merged.set(key, message);
20994
- }
20995
- }
20996
- return sortMessages([...merged.values()]);
20997
- }
20998
-
20999
21351
  // ../../core/chat/ids.js
21000
21352
  function isChatMessageForParticipants(message, chatPK, peerChatPK, memberChatPKs = null) {
21001
21353
  const members = Array.isArray(memberChatPKs) ? new Set(memberChatPKs.filter(Boolean)) : null;
@@ -21381,16 +21733,16 @@ function normalizeOpenedAction(epoch, head, action) {
21381
21733
  };
21382
21734
  }
21383
21735
  async function openMsgPlaintext(epoch, nonce, ct, aad, options) {
21384
- const crypto = options?.crypto;
21385
- if (typeof crypto?.openBox === "function" && (typeof crypto.isAvailable !== "function" || crypto.isAvailable())) {
21386
- return crypto.openBox(epoch.bodyKey, nonce, ct, aad);
21736
+ const crypto2 = options?.crypto;
21737
+ if (typeof crypto2?.openBox === "function" && (typeof crypto2.isAvailable !== "function" || crypto2.isAvailable())) {
21738
+ return crypto2.openBox(epoch.bodyKey, nonce, ct, aad);
21387
21739
  }
21388
21740
  return openBox(epoch.bodyKey, nonce, ct, aad);
21389
21741
  }
21390
21742
  async function verifyMsgSignature(publicKey, sig, bytes, options) {
21391
- const crypto = options?.crypto;
21392
- if (typeof crypto?.verifyChatBytes === "function" && (typeof crypto.isAvailable !== "function" || crypto.isAvailable())) {
21393
- return crypto.verifyChatBytes(publicKey, sig, bytes);
21743
+ const crypto2 = options?.crypto;
21744
+ if (typeof crypto2?.verifyChatBytes === "function" && (typeof crypto2.isAvailable !== "function" || crypto2.isAvailable())) {
21745
+ return crypto2.verifyChatBytes(publicKey, sig, bytes);
21394
21746
  }
21395
21747
  return verifyChatBytes(publicKey, toHex(sig), bytes);
21396
21748
  }
@@ -21473,9 +21825,9 @@ async function openMessageBatchV3(epoch, records, options = {}) {
21473
21825
  if (!epoch?.chatId || !epoch?.epochId || !epoch?.bodyKey || !epoch?.manifest) {
21474
21826
  throw new Error("chat batch epoch required");
21475
21827
  }
21476
- const crypto = options.crypto;
21477
- if (typeof crypto?.openMessageBatchV3 === "function" && (typeof crypto.isAvailable !== "function" || crypto.isAvailable())) {
21478
- const opened = await crypto.openMessageBatchV3(makeBatchOpenRequest(epoch, source));
21828
+ const crypto2 = options.crypto;
21829
+ if (typeof crypto2?.openMessageBatchV3 === "function" && (typeof crypto2.isAvailable !== "function" || crypto2.isAvailable())) {
21830
+ const opened = await crypto2.openMessageBatchV3(makeBatchOpenRequest(epoch, source));
21479
21831
  if (!Array.isArray(opened) || opened.length !== source.length) {
21480
21832
  throw new Error("invalid chat message batch result");
21481
21833
  }
@@ -22831,20 +23183,6 @@ async function readChatFile(readChatMedia, file) {
22831
23183
  }
22832
23184
  }
22833
23185
 
22834
- // ../../core/chat/messages/types.js
22835
- var ATTACHMENT_MSG_TYPES = ["img", "gif", "m4a", "mp4", "file"];
22836
- var MAX_TXT_CHARS = CHAT_MAX_TEXT_CHARS;
22837
- var REACTION_MSG_TYPE = "rxn";
22838
- var DELETE_MSG_TYPE = "del";
22839
- var SYSTEM_MSG_TYPE = "sys";
22840
- var EPOCH_TRANSITION_MSG_TYPE = "epoch";
22841
- var EPOCH_PROPOSAL_MSG_TYPE = "epoch_proposal";
22842
- var SYSTEM_SETTINGS_KIND = "settings";
22843
- var DEFAULT_REACTION_EMOJI = "❤️";
22844
- var MAX_REACTIONS = CHAT_MAX_REACTIONS;
22845
- var HOLD_VISIBLE_KEY = "__holdVisible";
22846
- var SOURCE_GONE_VISIBLE_KEY = "__sourceGoneVisible";
22847
-
22848
23186
  // ../../core/username.js
22849
23187
  var MAX_USERNAME = USERNAME_MAX_CHARS;
22850
23188
  var usernameKeyRegex = /^[a-z0-9]$/i;
@@ -24493,7 +24831,7 @@ function getChatSettingsEventAvatar(msg) {
24493
24831
  return chatAvatarFile(msg?.avatarRef) ? msg.avatarRef : "";
24494
24832
  }
24495
24833
  function isSystemMsg(msg) {
24496
- return !!getSystemMsgText(msg) && (msg?.t === SYSTEM_MSG_TYPE || isMembershipEventMsg(msg));
24834
+ return !!getSystemMsgText(msg) && (msg?.t === SYSTEM_MSG_TYPE || msg?.t === CALL_MSG_TYPE || isMembershipEventMsg(msg));
24497
24835
  }
24498
24836
  function makeChatInviteNotice(profiles) {
24499
24837
  const names = [...new Map(profiles.map((profile) => [profile.chatPK, formatUserDisplay(profile, true)])).values()];
@@ -24528,6 +24866,8 @@ function isMembershipEventMsg(msg) {
24528
24866
  return !!getMembershipEvent(msg);
24529
24867
  }
24530
24868
  function getSystemMsgText(msg) {
24869
+ if (msg?.t === CALL_MSG_TYPE)
24870
+ return callMessageText(msg);
24531
24871
  if (isChatInviteNotice(msg)) {
24532
24872
  const names = msg.names.length < 2 ? msg.names[0] : `${msg.names.slice(0, -1).join(", ")} and ${msg.names.at(-1)}`;
24533
24873
  return `${names} ${msg.names.length === 1 ? "is" : "are"} not accepting chat invitations right now`;
@@ -24625,6 +24965,8 @@ function canShowMsg(msg) {
24625
24965
  return hasText(msg.a);
24626
24966
  case "pay":
24627
24967
  return isChatPayment(msg) || isPendingChatPayment(msg);
24968
+ case CALL_MSG_TYPE:
24969
+ return !!readCallMessage(msg);
24628
24970
  case SYSTEM_MSG_TYPE:
24629
24971
  return !!getSystemMsgText(msg);
24630
24972
  default:
@@ -24810,7 +25152,7 @@ function groupEvidence(messages, byKey, memberStatesByEpoch) {
24810
25152
  const target = byKey.get(reference);
24811
25153
  addEvidence(evidence, messageEpochId(target), actor, messageOrderMs(target), seenAt);
24812
25154
  }
24813
- if (canShowMsg(message) && !isSystemMsg(message)) {
25155
+ if (canShowMsg(message)) {
24814
25156
  addEvidence(evidence, messageEpochId(message), actor, seenAt, seenAt);
24815
25157
  }
24816
25158
  }
@@ -24837,8 +25179,13 @@ function analyzeMessageExpiry(messages, selfChatPublicKey, _peerChatPublicKey, o
24837
25179
  const heldExpired = [];
24838
25180
  const shorten = [];
24839
25181
  let nextExpiryAt = null;
25182
+ const latestStart = latestCallStartKey(projectedMessages.filter(isServerConfirmedMsg));
25183
+ const endedCalls = new Set(projectedMessages.filter(isServerConfirmedMsg).filter((message) => readCallMessage(message)?.event === "ended").map((message) => message.callId));
24840
25184
  for (const message of projectedMessages) {
24841
- if (!isServerConfirmedMsg(message) || isControlMsg(message) || isSystemMsg(message) || !canShowMsg(message) || message.ttl == null)
25185
+ if (!isServerConfirmedMsg(message) || !canShowMsg(message) || message.ttl == null)
25186
+ continue;
25187
+ const call = readCallMessage(message);
25188
+ if (call?.event === "started" && getMessageKey(message) === latestStart && (options.activeCallId === undefined && !endedCalls.has(call.callId) || options.activeCallId === call.callId))
24842
25189
  continue;
24843
25190
  const messageMs = messageOrderMs(message);
24844
25191
  const epochId = messageEpochId(message);
@@ -24875,7 +25222,7 @@ function analyzeMessageExpiry(messages, selfChatPublicKey, _peerChatPublicKey, o
24875
25222
  if (expiresAt == null)
24876
25223
  continue;
24877
25224
  const currentTtlMs = Number.isFinite(message.ttl) ? message.ttl : Number(message.ttl?.toMillis?.());
24878
- if (!Number.isFinite(currentTtlMs) || currentTtlMs > expiresAt)
25225
+ if (!isControlMsg(message) && (!Number.isFinite(currentTtlMs) || currentTtlMs > expiresAt))
24879
25226
  shorten.push({ message, expiresAt });
24880
25227
  if (expiresAt > now) {
24881
25228
  nextExpiryAt = nextExpiryAt == null ? expiresAt : Math.min(nextExpiryAt, expiresAt);
@@ -25028,6 +25375,8 @@ async function compactMessages({ chatId, maintenance, messages, deletedKeys, pro
25028
25375
  function sendableMsgPayload(message) {
25029
25376
  if (isChatInviteNotice(message))
25030
25377
  throw new Error("local chat notices cannot be sent");
25378
+ if (message?.t === CALL_MSG_TYPE && !readCallMessage(message))
25379
+ throw new Error("invalid call message");
25031
25380
  const { localData, localUri, pending, failed, peerChatPK, id, from, ts, chatId, chatDraft, protocol, editedAt, editId, editStatus, editOriginal, ...payload } = message || {};
25032
25381
  return payload;
25033
25382
  }
@@ -25074,6 +25423,8 @@ function canRenderPreviewContent(preview) {
25074
25423
  if (preview.t === "pay") {
25075
25424
  return isChatPayment(preview) || isPendingChatPayment(preview);
25076
25425
  }
25426
+ if (preview.t === CALL_MSG_TYPE)
25427
+ return !!readCallMessage(preview);
25077
25428
  if (isAttachmentMsgType(preview?.t)) {
25078
25429
  return true;
25079
25430
  }
@@ -25701,8 +26052,9 @@ async function makeChatRecipientDeliveries(identity, epochState, routes, message
25701
26052
  const route = routes[member.chatPK] || {};
25702
26053
  const descriptor = await sealNotificationDescriptor(member.notificationPK, {
25703
26054
  peerTag: notificationChatTag(epochState.stateCapability, epochState.manifest.chatId, member.chatPK),
25704
- eventId: messageId
25705
- });
26055
+ eventId: messageId,
26056
+ kind: fields.presentationKind ?? NOTIFICATION_PRESENTATION_KINDS.ACTIVITY
26057
+ }, identity);
25706
26058
  const ping = await sealPing(identity, member.chatPK, {
25707
26059
  kind: fields.kind,
25708
26060
  chatId: epochState.manifest.chatId,
@@ -25776,6 +26128,7 @@ async function prepareMsgRecord(identity, epochState, message, options = {}) {
25776
26128
  cid: head.cid,
25777
26129
  record: { lane: epoch.messageLane, head, body, ttlMs },
25778
26130
  message: ownerPreview(epoch, identity.chatPK, messagePayload, messageId, head, tsMs, ttlMs),
26131
+ actionOp,
25779
26132
  mentionRecipientChatPKs: mentionTargets.recipientChatPKs,
25780
26133
  tsMs
25781
26134
  };
@@ -25809,6 +26162,7 @@ async function sendMsg(cloud, senderChatPK, senderPrivateKey, _receiverChatPK, m
25809
26162
  msgId: messageId,
25810
26163
  record,
25811
26164
  message: sentMessage,
26165
+ actionOp,
25812
26166
  mentionRecipientChatPKs,
25813
26167
  tsMs
25814
26168
  } = prepared;
@@ -25839,8 +26193,9 @@ async function sendMsg(cloud, senderChatPK, senderPrivateKey, _receiverChatPK, m
25839
26193
  shouldPing,
25840
26194
  deliverRecipients: options.deliverRecipients !== false,
25841
26195
  kind: pingKind,
26196
+ presentationKind: isReactionMsg(sentMessage) && sentMessage.emoji ? NOTIFICATION_PRESENTATION_KINDS.REACTION : actionOp !== CHAT_ACTION_OPS.CREATE || isControlMsg(sentMessage) ? NOTIFICATION_PRESENTATION_KINDS.ACTIVITY : notificationMessageKind(sentMessage),
25842
26197
  ownDeliveryCapability: routeState.capability,
25843
- silent: options.notify === false,
26198
+ silent: options.notify === false || notificationMessageIsSilent(sentMessage),
25844
26199
  recipientChatPKs: deliveryRecipientChatPKs,
25845
26200
  attentionRecipientChatPKs: mentionRecipientChatPKs,
25846
26201
  tsMs
@@ -26185,7 +26540,7 @@ function partitionExpiredMessageRecords(records, expiredKeys, cache, now = Date.
26185
26540
  return { activeRecords, expiredRecords };
26186
26541
  }
26187
26542
  function canHideExpiredMessage(message, chatPK) {
26188
- return isServerConfirmedMsg(message) && isPeerMsg(message, chatPK) && canShowMsg(message) && !isControlMsg(message) && !isSystemMsg(message) && message.ttl != null && message.permanent !== true;
26543
+ return isServerConfirmedMsg(message) && isPeerMsg(message, chatPK) && canShowMsg(message) && message.ttl != null && message.permanent !== true;
26189
26544
  }
26190
26545
  function hiddenMessageKeys(messages, chatPK, peerChatPK, options = {}) {
26191
26546
  if (!chatPK || !peerChatPK || !Array.isArray(messages) || !messages.length)
@@ -27806,8 +28161,9 @@ async function makeMlsWelcomeDeliveries(identity, nextState2, addedMembers, opti
27806
28161
  }
27807
28162
  const descriptor = await sealNotificationDescriptor(member.notificationPK, {
27808
28163
  peerTag: notificationChatTag(nextState2.stateCapability, nextState2.manifest.chatId, member.chatPK),
27809
- eventId: nextState2.mlsPackageId
27810
- });
28164
+ eventId: nextState2.mlsPackageId,
28165
+ kind: NOTIFICATION_PRESENTATION_KINDS.ACTIVITY
28166
+ }, identity);
27811
28167
  const ping = await sealPing(identity, member.chatPK, {
27812
28168
  kind: "welcome",
27813
28169
  chatId: nextState2.manifest.chatId,
@@ -37904,6 +38260,8 @@ function createChatListIndex({ cache, filterHiddenChats, getSources, isHiddenCha
37904
38260
  const boundary = authorityBoundaries.get(chat?.id);
37905
38261
  if (boundary?.terminal)
37906
38262
  return false;
38263
+ if (boundary?.leftEpoch != null && !(chat?.epochVersion > boundary.leftEpoch))
38264
+ return false;
37907
38265
  if (!chat?.id || !Number.isSafeInteger(sourceRevision))
37908
38266
  return true;
37909
38267
  if (sourceRevision < sourceRevisionFloor)
@@ -38067,7 +38425,7 @@ function createChatListIndex({ cache, filterHiddenChats, getSources, isHiddenCha
38067
38425
  continue;
38068
38426
  if (!sourceCanRemove(chatId, sourceRevision))
38069
38427
  continue;
38070
- if (entry.announcement && !authorityBoundaries.get(chatId)?.owner) {
38428
+ if (entry.announcement && !ownerCoversAnnouncement(authorityBoundaries.get(chatId)?.owner, entry.announcement)) {
38071
38429
  entry.cache = null;
38072
38430
  continue;
38073
38431
  }
@@ -38153,6 +38511,9 @@ function createChatListIndex({ cache, filterHiddenChats, getSources, isHiddenCha
38153
38511
  return false;
38154
38512
  if (authorityBoundaries.get(announcement.chatId)?.terminal)
38155
38513
  return false;
38514
+ const leftEpoch = authorityBoundaries.get(announcement.chatId)?.leftEpoch;
38515
+ if (leftEpoch != null && !(announcement.epochVersion > leftEpoch))
38516
+ return false;
38156
38517
  const sources = getSources();
38157
38518
  if (sources.pendingDeleteIdsRef.current.has(announcement.chatId))
38158
38519
  return false;
@@ -38211,6 +38572,26 @@ function createChatListIndex({ cache, filterHiddenChats, getSources, isHiddenCha
38211
38572
  return false;
38212
38573
  return removeOwner(chatId, options);
38213
38574
  };
38575
+ const leaveOwner = (chatId, epochVersion) => {
38576
+ if (!chatId || !Number.isSafeInteger(epochVersion) || epochVersion < 1)
38577
+ return false;
38578
+ const previous = authorityBoundaries.get(chatId);
38579
+ if (previous?.terminal)
38580
+ return false;
38581
+ if (previous?.owner?.epochVersion > epochVersion)
38582
+ return false;
38583
+ authorityRevision += 1;
38584
+ authorityBoundaries.set(chatId, {
38585
+ owner: previous?.owner || structuralChat(ownerFor(entries.get(chatId))),
38586
+ revision: authorityRevision,
38587
+ terminal: false,
38588
+ leftEpoch: Math.max(previous?.leftEpoch || 0, epochVersion)
38589
+ });
38590
+ entries.delete(chatId);
38591
+ clearRemovedOwners([chatId]);
38592
+ publishOwners({ warm: false });
38593
+ return true;
38594
+ };
38214
38595
  const getOwnerChat = (chatId) => {
38215
38596
  if (!chatId || isHiddenChatId(chatId))
38216
38597
  return null;
@@ -38397,6 +38778,7 @@ function createChatListIndex({ cache, filterHiddenChats, getSources, isHiddenCha
38397
38778
  observeMessages,
38398
38779
  patchRowSettings,
38399
38780
  removeOwner,
38781
+ leaveOwner,
38400
38782
  removeInboxOwner,
38401
38783
  render,
38402
38784
  reset,
@@ -40398,27 +40780,23 @@ function createChatSessionLaunches({
40398
40780
  const openDirectChat = async (profile) => {
40399
40781
  const operation = operationContext();
40400
40782
  const { chatPK, blocked } = operation.identity;
40401
- const member = profileMember(profile);
40402
- if (member.chatPK === chatPK)
40783
+ if (!profile?.uid || !profile?.chatPK)
40784
+ throw new Error("peer identity required");
40785
+ if (profile.chatPK === chatPK)
40403
40786
  return openNotesChat(operation);
40404
- if (new Set(blocked).has(member.uid))
40787
+ if (new Set(blocked).has(profile.uid))
40405
40788
  throw new Error("blocked peer cannot be opened");
40406
40789
  assertDirectAdmission(profile, operation);
40407
- const existingChatId = getPeerChatId(member.chatPK, operation);
40790
+ const existingChatId = getPeerChatId(profile.chatPK, operation);
40408
40791
  if (existingChatId) {
40409
40792
  setSelectedChat(existingChatId);
40410
40793
  return existingChatId;
40411
40794
  }
40795
+ const member = { uid: profile.uid, chatPK: profile.chatPK };
40412
40796
  const routeId = directRouteIdForPeer(member.chatPK, operation);
40413
40797
  if (operation.pendingOwner.getPendingLaunch()?.id === routeId)
40414
40798
  return routeId;
40415
- operation.pendingOwner.showEmpty(routeId, [{
40416
- ...member,
40417
- username: profile?.username || null,
40418
- avatar: profile?.avatar || null,
40419
- accountType: profile?.accountType || null,
40420
- chatAdmission: normalizeChatAdmission(profile?.chatAdmission)
40421
- }], [member], "direct");
40799
+ operation.pendingOwner.showEmpty(routeId, [profile], [member], "direct");
40422
40800
  const materialization = directMaterializations.get(routeId);
40423
40801
  if (materialization && isCurrent(materialization.operation)) {
40424
40802
  materialization.promise.then((chatId) => {
@@ -41712,10 +42090,10 @@ function createChatSessionMembership({
41712
42090
  await cloud.delivery.revoke(capability).catch(() => false);
41713
42091
  assertCurrent(operation);
41714
42092
  if (retirementStarted) {
41715
- operation.deleteActions.commitLeftChat(chatId);
42093
+ operation.deleteActions.commitLeftChat(chatId, chat.epochVersion);
41716
42094
  retirementStarted = false;
41717
42095
  } else
41718
- operation.deleteActions.dropLeftChat(chatId);
42096
+ operation.deleteActions.dropLeftChat(chatId, chat.epochVersion);
41719
42097
  return { left: true, message: proposal.message };
41720
42098
  } catch (error) {
41721
42099
  if (retirementStarted && isCurrent(operation)) {
@@ -41967,8 +42345,9 @@ async function deliverChatDeletedPings(cloud, identityValue, epochState, members
41967
42345
  continue;
41968
42346
  const descriptor = await sealNotificationDescriptor(member.notificationPK, {
41969
42347
  peerTag: notificationChatTag(epochState.stateCapability, manifest.chatId, member.chatPK),
41970
- eventId
41971
- });
42348
+ eventId,
42349
+ kind: NOTIFICATION_PRESENTATION_KINDS.ACTIVITY
42350
+ }, identity);
41972
42351
  const ping = await sealPing(identity, member.chatPK, {
41973
42352
  kind: "chat_deleted",
41974
42353
  chatId: manifest.chatId,
@@ -42121,20 +42500,20 @@ function createChatDelete({
42121
42500
  return;
42122
42501
  listActionsRef.current?.removeOwner?.(chatId, { warm: false });
42123
42502
  };
42124
- const dropLeftChat = (chatId) => {
42503
+ const dropLeftChat = (chatId, epochVersion) => {
42125
42504
  if (!chatId)
42126
42505
  return false;
42127
- dropChat(chatId);
42506
+ listActionsRef.current?.leaveOwner?.(chatId, epochVersion);
42128
42507
  return true;
42129
42508
  };
42130
- const commitLeftChat = (chatId) => {
42509
+ const commitLeftChat = (chatId, epochVersion) => {
42131
42510
  if (!chatId)
42132
42511
  return false;
42133
42512
  pendingDeleteIdsRef.current.delete(chatId);
42134
42513
  locallyDeletedChatIdsRef.current.delete(chatId);
42135
42514
  deletedChatIdsRef.current.delete(chatId);
42136
42515
  keepSelectedDeletedChatIdsRef.current.delete(chatId);
42137
- confirmDeletedChats([chatId]);
42516
+ dropLeftChat(chatId, epochVersion);
42138
42517
  return true;
42139
42518
  };
42140
42519
  const dropUnavailableChat = (chatId) => {
@@ -43699,6 +44078,7 @@ function createChatSession({
43699
44078
  mls = null,
43700
44079
  live = null,
43701
44080
  maintenance,
44081
+ resolveActiveCall,
43702
44082
  incomingChatDecision = null,
43703
44083
  getPeerProfile = null,
43704
44084
  refreshPeerProfile = null,
@@ -44474,7 +44854,6 @@ function createChatSession({
44474
44854
  assertBulkOperationCurrent(operation);
44475
44855
  await cloud.user.chats.unlink(identity.uid, chat.entryId);
44476
44856
  assertBulkOperationCurrent(operation);
44477
- operation.deleteActions.commitRetirement(chat.id);
44478
44857
  } else {
44479
44858
  await operation.deleteActions.deleteChat(chat, { cleanup: false });
44480
44859
  }
@@ -44561,6 +44940,26 @@ function createChatSession({
44561
44940
  return false;
44562
44941
  }
44563
44942
  };
44943
+ const prepareCall = async (chatId) => {
44944
+ const generation = authGeneration;
44945
+ const current = () => generation === authGeneration && online && pendingOwner.canSendToChat(chatId);
44946
+ if (!current())
44947
+ throw new Error("chat unavailable");
44948
+ await pendingOwner.assertChatAdmission(chatId);
44949
+ if (!getOwnerChat(chatId)?.epochState)
44950
+ await ensureChat(chatId);
44951
+ if (!current())
44952
+ throw new Error("chat unavailable");
44953
+ const reconciliation = await chatListOwner.reconcileChatEpoch(chatId);
44954
+ if (!current() || reconciliation?.removed)
44955
+ throw new Error("chat unavailable");
44956
+ await pendingOwner.assertChatAdmission(chatId);
44957
+ const chat = getVisibleOwnerChat(chatId);
44958
+ if (!current() || !chat?.epochState)
44959
+ throw new Error("chat unavailable");
44960
+ return chat.epochState;
44961
+ };
44962
+ const materializeChat = (chatId) => pendingOwner.runChatMutation(chatId, (canonicalId) => canonicalId);
44564
44963
  const flushChatReadFrontier = (chatId) => {
44565
44964
  if (!online)
44566
44965
  return false;
@@ -44843,6 +45242,8 @@ function createChatSession({
44843
45242
  markChatTyping,
44844
45243
  hasChat: hasVisibleChat,
44845
45244
  getOwnerChat: getVisibleOwnerChat,
45245
+ prepareCall,
45246
+ materializeChat,
44846
45247
  sendOptionsForPeer,
44847
45248
  getLocalMessages,
44848
45249
  wasChatDeletedLocally,
@@ -44876,6 +45277,7 @@ function createChatSession({
44876
45277
  deleteMessage,
44877
45278
  deleteMessages,
44878
45279
  deleteMessageDocs,
45280
+ resolveActiveCall,
44879
45281
  setChatTtl,
44880
45282
  makeMessagePermanent,
44881
45283
  makeMessageTemporary,
@@ -44933,6 +45335,7 @@ function createChatSession({
44933
45335
  stateOwner.setListSnapshot(chatListOwner.getSnapshot());
44934
45336
  deleteListActionsRef.current = {
44935
45337
  removeOwner: (...args) => chatListOwner.removeOwner(...args),
45338
+ leaveOwner: (...args) => chatListOwner.leaveOwner(...args),
44936
45339
  render: (...args) => chatListOwner.render(...args)
44937
45340
  };
44938
45341
  actionOwner.createActionOwners();
@@ -45654,6 +46057,2942 @@ function createPresence({ cloud, diag = null, now = Date.now } = {}) {
45654
46057
  });
45655
46058
  }
45656
46059
 
46060
+ // ../../core/calls/protocol.js
46061
+ var CALL_MAX_PARTICIPANTS = 16;
46062
+ var CALL_TTL_MS = 120000;
46063
+ var CALL_ADMISSION_TIMEOUT_MS = 30000;
46064
+ var HEX16 = /^[0-9a-f]{32}$/u;
46065
+ var HEX32 = /^[0-9a-f]{64}$/u;
46066
+ function bytes(packet) {
46067
+ const { signature: _signature, ...body } = packet;
46068
+ return canonicalBytes(["veyl.call.packet.v1", body]);
46069
+ }
46070
+ function createCallProtocol({ realm: realm2, epochState, identity, endpoint, callId = "" }) {
46071
+ const { manifest } = epochState;
46072
+ const members = new Map(manifest.members.map((member) => [member.chatSigningPK, member]));
46073
+ if (!members.has(identity.chatSigningPK))
46074
+ throw new Error("not a chat member");
46075
+ const actor = identity.chatSigningPK;
46076
+ const secret = identity.chatSigningSecret.slice();
46077
+ let closed = false;
46078
+ const context = { v: 1, realm: realm2, chatId: manifest.chatId, epochId: manifest.epochId, callId };
46079
+ return Object.freeze({
46080
+ sign(kind, data) {
46081
+ if (closed)
46082
+ throw new Error("call protocol closed");
46083
+ const packet = {
46084
+ ...context,
46085
+ actor,
46086
+ holder: endpoint.holder,
46087
+ endpoint: endpoint.publicKey,
46088
+ nonce: toHex(randomBytes3(16)),
46089
+ at: Date.now(),
46090
+ kind,
46091
+ data
46092
+ };
46093
+ return { ...packet, signature: signChatBytes({ secret, publicKey: actor }, bytes(packet)) };
46094
+ },
46095
+ verify(packet, { fresh = true } = {}) {
46096
+ if (!packet || Object.keys(context).some((key) => packet[key] !== context[key]) || !members.has(packet.actor) || !HEX16.test(packet.holder) || !HEX32.test(packet.endpoint) || !HEX16.test(packet.nonce) || !Number.isSafeInteger(packet.at) || fresh && (packet.at > Date.now() + 5000 || packet.at < Date.now() - CALL_TTL_MS) || !verifyChatBytes(packet.actor, packet.signature, bytes(packet)))
46097
+ throw new Error("invalid call participant proof");
46098
+ return members.get(packet.actor);
46099
+ },
46100
+ close() {
46101
+ if (!closed) {
46102
+ closed = true;
46103
+ cleanBytes(secret);
46104
+ }
46105
+ }
46106
+ });
46107
+ }
46108
+ function verifyCallAdmissions(admissions, protocol) {
46109
+ if (!Array.isArray(admissions) || !admissions.length || admissions.length > CALL_MAX_PARTICIPANTS)
46110
+ throw new Error("call membership mismatch");
46111
+ const holders = new Set;
46112
+ const identities = new Set;
46113
+ const members = new Map;
46114
+ for (const admission of admissions) {
46115
+ const identity = protocol.verify(admission, { fresh: false });
46116
+ if (admission.kind !== "join" || !HEX32.test(admission.data?.signaturePK) || admission.data.replaces != null && !HEX16.test(admission.data.replaces) || holders.has(admission.holder) || identities.has(admission.actor))
46117
+ throw new Error("invalid call participant");
46118
+ holders.add(admission.holder);
46119
+ identities.add(admission.actor);
46120
+ members.set(admission.holder, identity);
46121
+ }
46122
+ return members;
46123
+ }
46124
+ function verifyCallMembers(mlsState, admissions, protocol) {
46125
+ const members = verifyCallAdmissions(admissions, protocol);
46126
+ if (mlsState.members.length !== admissions.length)
46127
+ throw new Error("call membership mismatch");
46128
+ for (const admission of admissions) {
46129
+ const member = mlsState.members.find((item) => toHex(item.leafId) === admission.holder);
46130
+ if (!member || toHex(member.signaturePK) !== admission.data.signaturePK)
46131
+ throw new Error("call leaf identity mismatch");
46132
+ }
46133
+ return members;
46134
+ }
46135
+ function verifyCallReplacement(packet, currentHead, protocol) {
46136
+ protocol.verify(packet, { fresh: false });
46137
+ const previous = packet.data?.previous;
46138
+ const replacement = packet.data?.participant;
46139
+ protocol.verify(previous, { fresh: false });
46140
+ if (packet.kind !== "replace" || previous.kind !== "head" || previous.data?.participants?.length !== 1)
46141
+ throw new Error("invalid call replacement");
46142
+ verifyCallAdmissions(previous.data.participants, protocol);
46143
+ verifyCallAdmissions([replacement], protocol);
46144
+ const original = previous.data.participants[0];
46145
+ if (previous.actor !== original.actor || previous.holder !== original.holder || previous.endpoint !== original.endpoint || packet.actor !== original.actor || replacement.actor !== original.actor || packet.holder !== replacement.holder || packet.endpoint !== replacement.endpoint || replacement.holder === original.holder || replacement.data.replaces !== original.holder)
46146
+ throw new Error("invalid call replacement");
46147
+ if (currentHead && (currentHead.epoch !== previous.data.epoch || currentHead.participants.length !== 1 || currentHead.participants[0].signature !== original.signature))
46148
+ throw new Error("stale call replacement");
46149
+ return { epoch: 0, participants: [replacement] };
46150
+ }
46151
+
46152
+ // ../../core/calls/mailbox.js
46153
+ function openCallMailbox({ cloud, capability, endpoint, protocol, onChange, onError, stream = true }) {
46154
+ let closed = false;
46155
+ let record = null;
46156
+ let candidate = null;
46157
+ let tail = Promise.resolve();
46158
+ let cursor = 0;
46159
+ const decode = async (value, fresh = true, maximum) => {
46160
+ const packet = await capability.open(decodeCallBytes(value, maximum));
46161
+ protocol.verify(packet, { fresh });
46162
+ return packet;
46163
+ };
46164
+ const accept = async (value) => {
46165
+ if (closed)
46166
+ return;
46167
+ if (!Number.isSafeInteger(value?.revision) || !Number.isSafeInteger(value?.cursor) || value.revision < 0 || value.cursor < 0 || !Array.isArray(value.events))
46168
+ throw new Error("invalid call mailbox");
46169
+ if (record && value.revision < record.revision)
46170
+ return;
46171
+ if (value.cursor < cursor)
46172
+ throw new Error("invalid call event order");
46173
+ if (value.events.some((item) => !Number.isSafeInteger(item?.cursor) || item.cursor < 1 || item.cursor > value.cursor))
46174
+ throw new Error("invalid call event order");
46175
+ const empty = value.headDigest === null && value.payload === null && value.proof === null && value.events.length === 0;
46176
+ const unseen = value.events.filter((item) => item.cursor > cursor);
46177
+ const historyLost = !!(!empty && cursor && value.cursor > cursor && (!unseen.length || unseen[0].cursor !== cursor + 1));
46178
+ if (historyLost && (stream || value.truncated !== true))
46179
+ throw new Error("call signaling history unavailable");
46180
+ let head = null;
46181
+ let payload = null;
46182
+ if (value.headDigest !== null) {
46183
+ if (typeof value.headDigest !== "string" || !/^[a-f0-9]{64}$/u.test(value.headDigest))
46184
+ throw new Error("invalid call head digest");
46185
+ const proof = await decode(value.proof, true, CALL_HEAD_PROOF_MAX_BYTES);
46186
+ if (proof.kind !== "head-proof" || proof.data?.digest !== value.headDigest)
46187
+ throw new Error("invalid call head proof");
46188
+ if (record?.headDigest === value.headDigest && record.head) {
46189
+ if (Object.hasOwn(value, "payload") && callHeadDigest(decodeCallBytes(value.payload)) !== value.headDigest)
46190
+ throw new Error("invalid call head digest");
46191
+ head = record.head;
46192
+ payload = record.payload;
46193
+ } else {
46194
+ if (!value.payload || callHeadDigest(decodeCallBytes(value.payload)) !== value.headDigest)
46195
+ throw new Error("missing call head");
46196
+ head = await decode(value.payload, false);
46197
+ if (head.kind !== "head")
46198
+ throw new Error("invalid call head");
46199
+ payload = value.payload;
46200
+ }
46201
+ } else if (value.payload !== null || value.proof !== null)
46202
+ throw new Error("invalid empty call head");
46203
+ const events = [];
46204
+ let nextCursor = empty ? value.cursor : historyLost ? 0 : cursor;
46205
+ for (const item of unseen) {
46206
+ if (!Number.isSafeInteger(item.cursor) || item.cursor < 1 || nextCursor && item.cursor !== nextCursor + 1)
46207
+ throw new Error("invalid call event order");
46208
+ const packet = await decode(item.payload, false, CALL_EVENT_MAX_BYTES);
46209
+ events.push(packet);
46210
+ nextCursor = item.cursor;
46211
+ }
46212
+ if (nextCursor !== value.cursor)
46213
+ throw new Error("call signaling history unavailable");
46214
+ if (closed)
46215
+ return;
46216
+ candidate = { ...value, payload, head, events, historyLost };
46217
+ try {
46218
+ await onChange?.(candidate);
46219
+ if (closed)
46220
+ return;
46221
+ cursor = nextCursor;
46222
+ record = candidate;
46223
+ } finally {
46224
+ candidate = null;
46225
+ }
46226
+ };
46227
+ const consume = (value) => {
46228
+ const result = tail.then(() => accept(value));
46229
+ tail = result.catch((error) => {
46230
+ if (!closed)
46231
+ onError?.(error);
46232
+ });
46233
+ return result;
46234
+ };
46235
+ let channel;
46236
+ try {
46237
+ channel = cloud.calls.connect({
46238
+ capability,
46239
+ endpoint,
46240
+ onSnapshot: consume,
46241
+ onError,
46242
+ stream,
46243
+ getCheckpoint: () => ({ cursor, headDigest: record?.headDigest ?? null })
46244
+ });
46245
+ } catch (error) {
46246
+ capability.close();
46247
+ protocol.close?.();
46248
+ throw error;
46249
+ }
46250
+ async function mutate(headDigest, payload, events, { expectedRevision = (candidate || record)?.revision ?? 0, ttlMs = CALL_TTL_MS } = {}) {
46251
+ if (!stream)
46252
+ throw new Error("read-only call mailbox");
46253
+ const proof = encodeCallBytes(await capability.seal(protocol.sign("head-proof", { digest: headDigest })));
46254
+ const packets = await Promise.all(events.map(async (packet) => encodeCallBytes(await capability.seal(packet))));
46255
+ if (closed)
46256
+ throw new Error("call mailbox closed");
46257
+ const command = {
46258
+ type: "commit",
46259
+ expectedRevision,
46260
+ operation: toHex(randomBytes3(16)),
46261
+ headDigest,
46262
+ proof,
46263
+ ...payload !== undefined ? { payload } : {},
46264
+ events: packets,
46265
+ ttlMs
46266
+ };
46267
+ let response;
46268
+ try {
46269
+ response = await channel.request(command);
46270
+ } catch (error) {
46271
+ if (closed || !["calls/connection", "calls/timeout"].includes(error?.code))
46272
+ throw error;
46273
+ response = await channel.request(command);
46274
+ }
46275
+ if (closed)
46276
+ throw new Error("call mailbox closed");
46277
+ consume(response.record).catch(() => {});
46278
+ return response.record;
46279
+ }
46280
+ return Object.freeze({
46281
+ getSnapshot: () => candidate || record,
46282
+ async ready() {
46283
+ const response = await channel.ready();
46284
+ await consume(response.record);
46285
+ return record;
46286
+ },
46287
+ async read() {
46288
+ const response = await channel.request({ type: "read", cursor, headDigest: record?.headDigest ?? null });
46289
+ await consume(response.record);
46290
+ return record;
46291
+ },
46292
+ async commit(head, events = [], options) {
46293
+ const bytes2 = await capability.seal(protocol.sign("head", head));
46294
+ return mutate(callHeadDigest(bytes2), encodeCallBytes(bytes2), events, options);
46295
+ },
46296
+ async append(events, options) {
46297
+ const current = candidate || record;
46298
+ if (!current?.headDigest || !current.head)
46299
+ throw new Error("call ended");
46300
+ return mutate(current.headDigest, undefined, events, options);
46301
+ },
46302
+ close() {
46303
+ closed = true;
46304
+ channel.close();
46305
+ capability.close();
46306
+ protocol.close?.();
46307
+ }
46308
+ });
46309
+ }
46310
+
46311
+ // ../../core/calls/ownership.js
46312
+ var CALL_OWNERSHIP_LEASE_MS = 15000;
46313
+
46314
+ // ../../core/calls/clock.js
46315
+ function callLeaseClock() {
46316
+ return {
46317
+ wall: Date.now(),
46318
+ monotonic: globalThis.performance.timeOrigin + globalThis.performance.now()
46319
+ };
46320
+ }
46321
+
46322
+ // ../../core/calls/leaseattempt.js
46323
+ var STOP_MARGIN_MS = 250;
46324
+ function validTime(value) {
46325
+ return Number.isFinite(value) && value >= 0;
46326
+ }
46327
+ function createCallLeaseAttempt(command, clock = callLeaseClock) {
46328
+ if (!["claim", "activate", "renew"].includes(command?.type))
46329
+ throw new Error("call lease command required");
46330
+ if (command.operation !== undefined)
46331
+ throw new Error("reuse the original call lease attempt");
46332
+ const revision = command.type === "claim" ? command.expectedRevision + 1 : command.revision;
46333
+ const sequence = command.type === "renew" ? command.sequence : 0;
46334
+ if (!/^[a-f0-9]{32}$/u.test(command.holder || "") || !Number.isSafeInteger(revision) || revision < 1 || !Number.isSafeInteger(sequence) || sequence < (command.type === "renew" ? 1 : 0))
46335
+ throw new Error("invalid call lease command");
46336
+ const started = clock();
46337
+ if (!validTime(started?.wall) || !validTime(started?.monotonic))
46338
+ throw new Error("call lease clock unavailable");
46339
+ const deadline = Object.freeze({
46340
+ sessionId: command.holder,
46341
+ revision,
46342
+ wallDeadline: started.wall + CALL_OWNERSHIP_LEASE_MS - STOP_MARGIN_MS,
46343
+ monotonicDeadline: started.monotonic + CALL_OWNERSHIP_LEASE_MS - STOP_MARGIN_MS
46344
+ });
46345
+ const startWall = started.wall;
46346
+ const startMonotonic = started.monotonic;
46347
+ const operation = toHex(randomBytes3(16));
46348
+ const request = Object.freeze({ ...command, ...command.payload ? { payload: command.payload.slice() } : {}, operation });
46349
+ let cancelled2 = false;
46350
+ return Object.freeze({
46351
+ command: request,
46352
+ cancel() {
46353
+ cancelled2 = true;
46354
+ },
46355
+ grant(record) {
46356
+ if (cancelled2)
46357
+ return null;
46358
+ const now = clock();
46359
+ if (!validTime(now?.wall) || !validTime(now?.monotonic) || now.wall < startWall || now.monotonic < startMonotonic || now.wall >= deadline.wallDeadline || now.monotonic >= deadline.monotonicDeadline) {
46360
+ cancelled2 = true;
46361
+ return null;
46362
+ }
46363
+ const active = record?.active;
46364
+ if (record?.pending || record?.revision !== revision || active?.revision !== revision || active.holder !== deadline.sessionId || active.sequence !== sequence || active.operation !== operation)
46365
+ return null;
46366
+ return deadline;
46367
+ }
46368
+ });
46369
+ }
46370
+
46371
+ // ../../core/calls/accountlease.js
46372
+ var transient = (error) => ["calls/connection", "calls/timeout"].includes(error?.code);
46373
+ function openAccountCallLease({ cloud, capability: accountCapability, endpoint, clock = callLeaseClock, onRecord, onGrant, onLost, onError }) {
46374
+ const capability = accountCapability.retain();
46375
+ let closed = false;
46376
+ let record = null;
46377
+ let owned = null;
46378
+ let ownedDeadline = null;
46379
+ let retiring = null;
46380
+ let renewal = null;
46381
+ let expiry = null;
46382
+ let generation = 0;
46383
+ let acquiring = false;
46384
+ let rotating = Promise.resolve();
46385
+ const attempts = new Set;
46386
+ const waiters = new Set;
46387
+ const current = () => !closed;
46388
+ const wake = () => {
46389
+ for (const resolve of [...waiters])
46390
+ resolve();
46391
+ };
46392
+ function waitForChange(milliseconds) {
46393
+ return new Promise((resolve) => {
46394
+ const finish = () => {
46395
+ clearTimeout(timer);
46396
+ waiters.delete(finish);
46397
+ resolve();
46398
+ };
46399
+ const timer = setTimeout(finish, milliseconds);
46400
+ waiters.add(finish);
46401
+ });
46402
+ }
46403
+ function stop() {
46404
+ generation++;
46405
+ clearTimeout(renewal);
46406
+ clearTimeout(expiry);
46407
+ renewal = expiry = null;
46408
+ for (const attempt of attempts)
46409
+ attempt.cancel();
46410
+ attempts.clear();
46411
+ const previous = owned;
46412
+ owned = ownedDeadline = null;
46413
+ wake();
46414
+ return previous;
46415
+ }
46416
+ async function accept(value) {
46417
+ if (closed)
46418
+ return;
46419
+ const next = decodeCallRecord(value);
46420
+ if (!Number.isSafeInteger(next?.version) || next.version < 0)
46421
+ throw new Error("invalid call ownership version");
46422
+ if (record && next.version <= record.version)
46423
+ return;
46424
+ record = next;
46425
+ wake();
46426
+ if (owned && (next.pending || next.active?.holder !== endpoint.holder || next.active?.revision !== owned.revision)) {
46427
+ retiring = stop();
46428
+ onLost?.();
46429
+ }
46430
+ const active = next.pending || next.active;
46431
+ const descriptor = active ? await capability.open(active.payload) : null;
46432
+ if (current() && record === next)
46433
+ onRecord?.(next, descriptor);
46434
+ }
46435
+ let channel;
46436
+ try {
46437
+ channel = cloud.calls.connect({
46438
+ capability,
46439
+ endpoint,
46440
+ onSnapshot: (value) => accept(value).catch(onError),
46441
+ onError(error) {
46442
+ if (!transient(error))
46443
+ onError?.(error);
46444
+ }
46445
+ });
46446
+ } catch (error) {
46447
+ capability.close();
46448
+ throw error;
46449
+ }
46450
+ async function request(command) {
46451
+ const response = await channel.request({ ...command, ...command.payload ? { payload: encodeCallBytes(command.payload) } : {} });
46452
+ await accept(response.record);
46453
+ return decodeCallRecord(response.record);
46454
+ }
46455
+ function renewalRemaining(started) {
46456
+ if (!current() || started !== generation || !owned || !ownedDeadline)
46457
+ return 0;
46458
+ const now = clock();
46459
+ if (!Number.isFinite(now?.wall) || !Number.isFinite(now?.monotonic) || now.wall < 0 || now.monotonic < 0)
46460
+ return 0;
46461
+ return Math.max(0, Math.min(ownedDeadline.wallDeadline - now.wall, ownedDeadline.monotonicDeadline - now.monotonic));
46462
+ }
46463
+ async function requestGrant(attempt, started) {
46464
+ let delay = 250;
46465
+ for (;; ) {
46466
+ if (attempt.command.type === "renew" && !renewalRemaining(started))
46467
+ throw new Error("call ownership expired");
46468
+ try {
46469
+ const value = await request(attempt.command);
46470
+ if (attempt.command.type === "renew" && !renewalRemaining(started))
46471
+ throw new Error("call ownership expired");
46472
+ return value;
46473
+ } catch (error) {
46474
+ const remaining = renewalRemaining(started);
46475
+ if (attempt.command.type !== "renew" || !transient(error) || !remaining)
46476
+ throw error;
46477
+ await waitForChange(Math.min(delay, remaining));
46478
+ delay = Math.min(delay * 2, 1000);
46479
+ }
46480
+ }
46481
+ }
46482
+ async function grant(command) {
46483
+ if (closed)
46484
+ throw new Error("call lease closed");
46485
+ const requestedAt = clock();
46486
+ const attempt = createCallLeaseAttempt(command, clock);
46487
+ const started = generation;
46488
+ attempts.add(attempt);
46489
+ try {
46490
+ const value = await requestGrant(attempt, started);
46491
+ const deadline = attempt.grant(value);
46492
+ if (!current() || started !== generation || !deadline || record.version !== value.version)
46493
+ throw new Error("call ownership unavailable");
46494
+ owned = value.active;
46495
+ await onGrant(deadline);
46496
+ if (!current() || owned !== value.active)
46497
+ throw new Error("call ownership changed");
46498
+ ownedDeadline = deadline;
46499
+ clearTimeout(expiry);
46500
+ expiry = setTimeout(() => {
46501
+ stop();
46502
+ onLost?.();
46503
+ }, Math.max(0, deadline.wallDeadline - Date.now()));
46504
+ clearTimeout(renewal);
46505
+ const now = clock();
46506
+ const elapsed = Math.max(now.wall - requestedAt.wall, now.monotonic - requestedAt.monotonic);
46507
+ renewal = setTimeout(() => {
46508
+ if (!owned)
46509
+ return;
46510
+ const renewing = generation;
46511
+ grant({ type: "renew", holder: endpoint.holder, revision: owned.revision, sequence: owned.sequence + 1 }).catch((error) => {
46512
+ if (closed || renewing !== generation)
46513
+ return;
46514
+ stop();
46515
+ onLost?.();
46516
+ onError?.(error);
46517
+ });
46518
+ }, Math.max(0, 5000 - elapsed));
46519
+ return value;
46520
+ } finally {
46521
+ attempts.delete(attempt);
46522
+ attempt.cancel();
46523
+ }
46524
+ }
46525
+ async function rotateMedia() {
46526
+ if (closed || acquiring || !owned || !renewalRemaining(generation))
46527
+ throw new Error("call ownership unavailable");
46528
+ const previous = stop();
46529
+ const started = generation;
46530
+ await request({ type: "release", holder: endpoint.holder, revision: previous.revision });
46531
+ if (closed || started !== generation)
46532
+ throw new Error("call transfer cancelled");
46533
+ return grant({
46534
+ type: "claim",
46535
+ holder: endpoint.holder,
46536
+ expectedRevision: previous.revision,
46537
+ payload: previous.payload,
46538
+ takeover: false
46539
+ });
46540
+ }
46541
+ return Object.freeze({
46542
+ rotateMedia() {
46543
+ const next = rotating.then(rotateMedia);
46544
+ rotating = next.catch(() => {});
46545
+ return next;
46546
+ },
46547
+ async acquire(descriptor) {
46548
+ if (closed)
46549
+ throw new Error("call lease closed");
46550
+ if (owned || acquiring)
46551
+ throw new Error("already in a call");
46552
+ acquiring = true;
46553
+ const started = generation;
46554
+ try {
46555
+ await accept((await channel.ready()).record);
46556
+ const payload = await capability.seal(descriptor);
46557
+ if (closed || started !== generation)
46558
+ throw new Error("call transfer cancelled");
46559
+ const claim = { type: "claim", holder: endpoint.holder, expectedRevision: record.revision, payload, takeover: true };
46560
+ if (!record.active && !record.pending)
46561
+ return await grant(claim);
46562
+ const pending = createCallLeaseAttempt(claim, clock);
46563
+ attempts.add(pending);
46564
+ try {
46565
+ await request(pending.command);
46566
+ const until = Date.now() + 20000;
46567
+ while (current() && started === generation && record.pending?.holder === endpoint.holder && record.active && Date.now() < until) {
46568
+ await waitForChange(Math.max(1, Math.min(until - Date.now(), record.active.expiresAt - Date.now() + 50)));
46569
+ if (current() && started === generation && record.active?.expiresAt <= Date.now())
46570
+ await request({ type: "read" });
46571
+ }
46572
+ if (!current() || started !== generation || record.pending?.holder !== endpoint.holder)
46573
+ throw new Error("call transfer cancelled");
46574
+ return await grant({ type: "activate", holder: endpoint.holder, revision: record.pending.revision });
46575
+ } finally {
46576
+ attempts.delete(pending);
46577
+ pending.cancel();
46578
+ }
46579
+ } finally {
46580
+ acquiring = false;
46581
+ }
46582
+ },
46583
+ provider(action, payload) {
46584
+ if (!owned || closed)
46585
+ return Promise.reject(new Error("call ownership required"));
46586
+ return channel.provider({ holder: endpoint.holder, revision: owned.revision, action, payload }).then((result) => result.record);
46587
+ },
46588
+ async release() {
46589
+ const previous = stop() || retiring;
46590
+ retiring = null;
46591
+ if (previous && !closed)
46592
+ await request({ type: "release", holder: endpoint.holder, revision: previous.revision });
46593
+ else if (record?.pending?.holder === endpoint.holder && !closed)
46594
+ await request({ type: "cancel", holder: endpoint.holder, revision: record.pending.revision });
46595
+ },
46596
+ retire() {
46597
+ retiring = stop() || retiring;
46598
+ },
46599
+ close() {
46600
+ closed = true;
46601
+ stop();
46602
+ channel.close();
46603
+ capability.close();
46604
+ }
46605
+ });
46606
+ }
46607
+
46608
+ // ../../core/calls/room.js
46609
+ function openCallRoom({
46610
+ cloud,
46611
+ capability,
46612
+ protocol,
46613
+ endpoint,
46614
+ mls,
46615
+ member,
46616
+ admission,
46617
+ callId,
46618
+ onKeys,
46619
+ onParticipants,
46620
+ onSignal,
46621
+ onRemoved,
46622
+ onError
46623
+ }) {
46624
+ let closed = false;
46625
+ let state = null;
46626
+ let head = null;
46627
+ let verifiedHead = null;
46628
+ let admittedIdentities = new Map;
46629
+ let pulseTimer = null;
46630
+ let changing = false;
46631
+ let reconcileAgain = false;
46632
+ let joined = false;
46633
+ let departing = false;
46634
+ let pendingCandidate = null;
46635
+ let ownMedia = null;
46636
+ let audio = { muted: false, deafened: false };
46637
+ let startTask = null;
46638
+ let startPromise = null;
46639
+ let admissionTimer = null;
46640
+ let admissionResult = null;
46641
+ let admissionFailure = null;
46642
+ let pendingInitial = null;
46643
+ let submittedJoin = false;
46644
+ let predecessorHolder = null;
46645
+ let eventTail = Promise.resolve();
46646
+ let retryWait = null;
46647
+ const pendingJoins = new Map;
46648
+ const leaving = new Set;
46649
+ const seen = new Map;
46650
+ const published = new Map;
46651
+ const localHolder = endpoint.holder;
46652
+ function disposeState(value) {
46653
+ cleanBytes(value?.snapshot, value?.baseKey);
46654
+ }
46655
+ const alive = (holder) => !leaving.has(holder) && Date.now() - (seen.get(holder) || 0) < 30000;
46656
+ const replacementFor = (item) => [...pendingJoins.values()].find((packet) => packet.actor === item.actor && packet.holder !== item.holder && packet.data.replaces === item.holder && alive(packet.holder));
46657
+ const leader = () => {
46658
+ const candidates = head?.participants.filter((item) => alive(item.holder)) || [];
46659
+ return (candidates.filter((item) => !replacementFor(item)).length ? candidates.filter((item) => !replacementFor(item)) : candidates).map((item) => item.holder).sort()[0];
46660
+ };
46661
+ function admissionError(code, message) {
46662
+ return Object.assign(new Error(message), { code });
46663
+ }
46664
+ const cancelled2 = () => admissionError("calls/cancelled", "call cancelled");
46665
+ const empty = () => admissionError("calls/room-empty", "this call is empty");
46666
+ function settleAdmission(error) {
46667
+ if (error)
46668
+ admissionFailure ||= error;
46669
+ if (!admissionResult)
46670
+ return;
46671
+ const result = admissionResult;
46672
+ admissionResult = null;
46673
+ clearTimeout(admissionTimer);
46674
+ admissionTimer = null;
46675
+ if (error)
46676
+ result.reject(error);
46677
+ else
46678
+ result.resolve();
46679
+ }
46680
+ function checkAdmission() {
46681
+ if (closed || departing || !head)
46682
+ return;
46683
+ if (!joined && head.participants.every((item) => leaving.has(item.holder)))
46684
+ settleAdmission(empty());
46685
+ else if (joined && head.participants.some((item) => item.holder === localHolder && item.actor === admission.actor))
46686
+ settleAdmission();
46687
+ else if (!joined && head.participants.length >= CALL_MAX_PARTICIPANTS && !head.participants.some((item) => item.actor === admission.actor)) {
46688
+ settleAdmission(admissionError("calls/full", "this call is full"));
46689
+ } else if (!joined && submittedJoin && head.participants.some((item) => item.actor === admission.actor && item.holder !== localHolder && item.holder !== predecessorHolder)) {
46690
+ settleAdmission(admissionError("calls/taken-over", "call moved to another device"));
46691
+ }
46692
+ }
46693
+ async function adopt(next, admissions) {
46694
+ verifyCallMembers(next, admissions, protocol);
46695
+ if (closed) {
46696
+ disposeState(next);
46697
+ return;
46698
+ }
46699
+ const previous = state;
46700
+ state = next;
46701
+ disposeState(previous);
46702
+ const self = next.members.find((item) => toHex(item.leafId) === localHolder);
46703
+ if (!self) {
46704
+ onRemoved?.();
46705
+ return;
46706
+ }
46707
+ if (!admissions.some((item) => item.holder === localHolder && item.actor === admission.actor && item.endpoint === endpoint.publicKey))
46708
+ throw new Error("call endpoint identity mismatch");
46709
+ joined = true;
46710
+ await onKeys({ baseKey: next.baseKey, epoch: next.epoch, members: next.members, leafIndex: self.leafIndex });
46711
+ }
46712
+ function participants() {
46713
+ return (head?.participants || []).filter((item) => !leaving.has(item.holder)).map((item) => ({
46714
+ ...admittedIdentities.get(item.holder),
46715
+ holder: item.holder,
46716
+ muted: published.get(item.holder)?.muted === true || published.get(item.holder)?.deafened === true,
46717
+ deafened: published.get(item.holder)?.deafened === true,
46718
+ media: published.get(item.holder)?.media || null
46719
+ }));
46720
+ }
46721
+ function prune() {
46722
+ const admitted = new Set(head?.participants.map((item) => item.holder));
46723
+ for (const [holder, packet] of pendingJoins) {
46724
+ const predecessor = head?.participants.find((item) => item.actor === packet.actor);
46725
+ if (admitted.has(holder) || Date.now() - packet.at >= CALL_ADMISSION_TIMEOUT_MS || predecessor && packet.data.replaces !== predecessor.holder || !predecessor && packet.data.replaces)
46726
+ pendingJoins.delete(holder);
46727
+ }
46728
+ for (const holder of seen.keys())
46729
+ if (!admitted.has(holder) && !pendingJoins.has(holder))
46730
+ seen.delete(holder);
46731
+ for (const holder of published.keys())
46732
+ if (!admitted.has(holder))
46733
+ published.delete(holder);
46734
+ for (const holder of leaving)
46735
+ if (!admitted.has(holder))
46736
+ leaving.delete(holder);
46737
+ }
46738
+ async function consume(record) {
46739
+ if (closed)
46740
+ return;
46741
+ prune();
46742
+ const nextHead = record.head?.data;
46743
+ const signals = [];
46744
+ let membershipChanged = false;
46745
+ if (nextHead && (!Array.isArray(nextHead.participants) || nextHead.participants.length > CALL_MAX_PARTICIPANTS))
46746
+ throw new Error("invalid call room");
46747
+ for (const packet of record.events) {
46748
+ const roster = head?.participants || nextHead?.participants || [];
46749
+ const bound = roster.some((item) => item.holder === packet.holder && item.actor === packet.actor && item.endpoint === packet.endpoint);
46750
+ const pending = pendingJoins.get(packet.holder);
46751
+ const pendingBound = pending?.actor === packet.actor && pending?.endpoint === packet.endpoint;
46752
+ if (!["join", "replace"].includes(packet.kind) && !bound && !(packet.kind === "leave" && pendingBound))
46753
+ continue;
46754
+ if (packet.kind === "join" && roster.some((item) => item.holder === packet.holder) && !bound)
46755
+ continue;
46756
+ if (packet.kind === "join") {
46757
+ if (Date.now() - packet.at >= CALL_ADMISSION_TIMEOUT_MS)
46758
+ continue;
46759
+ const predecessor = roster.find((item) => item.actor === packet.actor && item.holder !== packet.holder);
46760
+ if (predecessor && packet.data.replaces !== predecessor.holder)
46761
+ continue;
46762
+ if (!predecessor && packet.data.replaces && !bound)
46763
+ continue;
46764
+ const previous = [...pendingJoins.values()].find((item) => item.actor === packet.actor && item.holder !== packet.holder);
46765
+ if (previous) {
46766
+ pendingJoins.delete(previous.holder);
46767
+ seen.delete(previous.holder);
46768
+ }
46769
+ if (!pendingJoins.has(packet.holder) && pendingJoins.size >= CALL_MAX_PARTICIPANTS)
46770
+ continue;
46771
+ pendingJoins.set(packet.holder, packet);
46772
+ } else if (packet.kind === "leave") {
46773
+ if (pendingBound)
46774
+ pendingJoins.delete(packet.holder);
46775
+ if (bound)
46776
+ leaving.add(packet.holder);
46777
+ } else if (packet.kind === "pulse") {
46778
+ published.set(packet.holder, packet.data);
46779
+ } else if (packet.kind === "signal") {
46780
+ if (packet.data.to === localHolder && head?.participants.some((item) => item.holder === packet.holder && item.actor === packet.actor)) {
46781
+ signals.push(packet);
46782
+ }
46783
+ } else if (packet.kind === "replace") {
46784
+ const replacement = verifyCallReplacement(packet, head, protocol);
46785
+ membershipChanged = true;
46786
+ if (state && !replacement.participants.some((item) => item.holder === localHolder)) {
46787
+ settleAdmission(cancelled2());
46788
+ onRemoved?.();
46789
+ return;
46790
+ }
46791
+ head = replacement;
46792
+ } else if (packet.kind === "change") {
46793
+ membershipChanged = true;
46794
+ const change = packet.data;
46795
+ if (!Array.isArray(change.participants) || !Number.isSafeInteger(change.epoch))
46796
+ throw new Error("invalid call epoch");
46797
+ if (state && change.epoch === state.epoch + 1) {
46798
+ if (!bound)
46799
+ throw new Error("unadmitted call commit");
46800
+ const next = pendingCandidate?.commit === change.commit ? pendingCandidate.state : await mls.processCommit(state.snapshot, decodeCallBytes(change.commit));
46801
+ pendingCandidate = null;
46802
+ if (next.removed) {
46803
+ disposeState(next);
46804
+ onRemoved?.();
46805
+ return;
46806
+ }
46807
+ await adopt(next, change.participants);
46808
+ } else if (!state && change.participants.some((item) => item.holder === localHolder && item.actor === admission.actor && item.endpoint === endpoint.publicKey)) {
46809
+ const next = await mls.join(member.snapshot, decodeCallBytes(change.welcome), decodeCallBytes(change.tree), fromHexBytes(callId));
46810
+ await adopt(next, change.participants);
46811
+ }
46812
+ head = { epoch: change.epoch, participants: change.participants };
46813
+ for (const item of change.participants) {
46814
+ pendingJoins.delete(item.holder);
46815
+ if (!seen.has(item.holder))
46816
+ seen.set(item.holder, packet.at);
46817
+ }
46818
+ }
46819
+ seen.set(packet.holder, Math.max(seen.get(packet.holder) || 0, packet.at));
46820
+ }
46821
+ if (!state && pendingInitial && nextHead?.epoch === 0 && nextHead.participants.length === 1 && nextHead.participants[0].signature === pendingInitial.admission.signature) {
46822
+ const initial = pendingInitial.state;
46823
+ pendingInitial = null;
46824
+ await adopt(initial, nextHead.participants);
46825
+ }
46826
+ if (nextHead && (verifiedHead !== record.head || membershipChanged)) {
46827
+ admittedIdentities = state ? verifyCallMembers(state, nextHead.participants, protocol) : verifyCallAdmissions(nextHead.participants, protocol);
46828
+ for (const item of nextHead.participants) {
46829
+ if (!seen.has(item.holder))
46830
+ seen.set(item.holder, record.head.at);
46831
+ }
46832
+ if (state && nextHead.epoch !== state.epoch)
46833
+ throw new Error("call epoch history unavailable");
46834
+ head = nextHead;
46835
+ verifiedHead = record.head;
46836
+ } else if (!nextHead) {
46837
+ head = null;
46838
+ verifiedHead = null;
46839
+ admittedIdentities.clear();
46840
+ if (joined) {
46841
+ onRemoved?.();
46842
+ return;
46843
+ }
46844
+ if (submittedJoin)
46845
+ settleAdmission(empty());
46846
+ }
46847
+ prune();
46848
+ if (closed)
46849
+ return;
46850
+ onParticipants?.(participants());
46851
+ checkAdmission();
46852
+ for (const packet of signals) {
46853
+ try {
46854
+ Promise.resolve(onSignal?.(packet.data.signal, packet.holder)).catch(fail);
46855
+ } catch (error) {
46856
+ fail(error);
46857
+ }
46858
+ }
46859
+ reconcile().catch(fail);
46860
+ }
46861
+ function fail(error) {
46862
+ if (!closed) {
46863
+ if (admissionResult)
46864
+ settleAdmission(error);
46865
+ onError?.(error);
46866
+ }
46867
+ }
46868
+ const mailbox = openCallMailbox({ cloud, capability, protocol, endpoint, onChange: consume, onError: fail });
46869
+ async function reconcile() {
46870
+ reconcileAgain = true;
46871
+ if (changing)
46872
+ return;
46873
+ changing = true;
46874
+ try {
46875
+ do {
46876
+ reconcileAgain = false;
46877
+ await reconcilePass();
46878
+ } while (reconcileAgain && !closed);
46879
+ } finally {
46880
+ changing = false;
46881
+ }
46882
+ }
46883
+ async function reconcilePass() {
46884
+ if (closed || departing || !state || leader() !== localHolder)
46885
+ return;
46886
+ const current = mailbox.getSnapshot();
46887
+ if (!current || !head)
46888
+ return;
46889
+ const removals = head.participants.filter((item) => item.holder !== localHolder && (!alive(item.holder) || replacementFor(item)));
46890
+ const retained = head.participants.filter((item) => !removals.includes(item));
46891
+ const admittedActors = new Set(retained.map((item) => item.actor));
46892
+ const additions = [...pendingJoins.values()].filter((item) => {
46893
+ if (head.participants.some((existing) => existing.holder === item.holder) || admittedActors.has(item.actor) || !alive(item.holder))
46894
+ return false;
46895
+ admittedActors.add(item.actor);
46896
+ return true;
46897
+ }).slice(0, CALL_MAX_PARTICIPANTS - retained.length);
46898
+ if (!removals.length && !additions.length)
46899
+ return;
46900
+ let candidate;
46901
+ try {
46902
+ for (const item of additions)
46903
+ protocol.verify(item);
46904
+ candidate = await mls.stageChange(state.snapshot, {
46905
+ adds: additions.map((item) => decodeCallBytes(item.data.keyPackage)),
46906
+ removes: removals.map((item) => fromHexBytes(item.holder))
46907
+ });
46908
+ if (closed || departing) {
46909
+ disposeState(candidate);
46910
+ return;
46911
+ }
46912
+ const admissions = [...retained, ...additions];
46913
+ verifyCallMembers(candidate, admissions, protocol);
46914
+ const change = {
46915
+ epoch: candidate.epoch,
46916
+ participants: admissions,
46917
+ commit: encodeCallBytes(candidate.commit),
46918
+ welcome: encodeCallBytes(candidate.welcome),
46919
+ tree: encodeCallBytes(candidate.tree)
46920
+ };
46921
+ pendingCandidate = { commit: change.commit, state: candidate };
46922
+ await mailbox.commit({ epoch: candidate.epoch, participants: admissions }, [protocol.sign("change", change)], { expectedRevision: current.revision });
46923
+ candidate = null;
46924
+ } catch (error) {
46925
+ if (pendingCandidate?.state === candidate)
46926
+ pendingCandidate = null;
46927
+ disposeState(candidate);
46928
+ if (String(error.code).endsWith("conflict")) {
46929
+ await mailbox.read();
46930
+ return;
46931
+ }
46932
+ throw error;
46933
+ }
46934
+ }
46935
+ function event(kind, data) {
46936
+ const result = eventTail.then(() => appendEvent(kind, data));
46937
+ eventTail = result.catch(() => {});
46938
+ return result;
46939
+ }
46940
+ async function appendEvent(kind, data) {
46941
+ const deadline = performance.now() + CALL_ADMISSION_TIMEOUT_MS;
46942
+ for (let attempt = 0;!closed && performance.now() < deadline; attempt += 1) {
46943
+ if (kind === "join" && departing)
46944
+ throw cancelled2();
46945
+ const current = mailbox.getSnapshot();
46946
+ if (!current?.head)
46947
+ throw kind === "join" ? empty() : new Error("call ended");
46948
+ if (kind === "join") {
46949
+ checkAdmission();
46950
+ if (admissionFailure)
46951
+ throw admissionFailure;
46952
+ }
46953
+ try {
46954
+ if (kind === "join")
46955
+ submittedJoin = true;
46956
+ return await mailbox.append([protocol.sign(kind, data)], { expectedRevision: current.revision });
46957
+ } catch (error) {
46958
+ if (closed)
46959
+ throw cancelled2();
46960
+ if (!String(error.code).endsWith("conflict"))
46961
+ throw error;
46962
+ const delay = Math.min(250, 25 * 2 ** Math.min(attempt, 4)) * (0.5 + Math.random() / 2);
46963
+ await new Promise((resolve) => {
46964
+ const timer = setTimeout(() => {
46965
+ retryWait = null;
46966
+ resolve();
46967
+ }, delay);
46968
+ retryWait = () => {
46969
+ clearTimeout(timer);
46970
+ retryWait = null;
46971
+ resolve();
46972
+ };
46973
+ });
46974
+ if (closed)
46975
+ throw cancelled2();
46976
+ await mailbox.read();
46977
+ }
46978
+ }
46979
+ if (closed)
46980
+ throw cancelled2();
46981
+ throw admissionError("calls/signaling-conflict", "call changed, try again");
46982
+ }
46983
+ async function pulse() {
46984
+ if (closed || departing || !joined)
46985
+ return;
46986
+ try {
46987
+ await event("pulse", { ...audio, media: ownMedia });
46988
+ } catch (error) {
46989
+ fail(error);
46990
+ }
46991
+ if (!closed && !departing)
46992
+ pulseTimer = setTimeout(pulse, 1e4);
46993
+ }
46994
+ async function beginStart(create) {
46995
+ const record = await mailbox.ready();
46996
+ if (closed || departing)
46997
+ throw cancelled2();
46998
+ if (!create && !record?.head)
46999
+ throw empty();
47000
+ checkAdmission();
47001
+ if (admissionFailure)
47002
+ throw admissionFailure;
47003
+ const solePredecessor = record?.head?.data.participants.length === 1 && record.head.data.participants[0].actor === admission.actor && record.head.data.participants[0].holder !== localHolder;
47004
+ if (create || solePredecessor) {
47005
+ if (create && record?.head)
47006
+ throw new Error("call already exists");
47007
+ let initialMember = member;
47008
+ let initialAdmission = admission;
47009
+ if (solePredecessor)
47010
+ initialMember = await mls.createMember(fromHexBytes(localHolder));
47011
+ let initial;
47012
+ try {
47013
+ if (closed || departing)
47014
+ throw cancelled2();
47015
+ if (solePredecessor)
47016
+ initialAdmission = protocol.sign("join", {
47017
+ signaturePK: toHex(initialMember.signaturePK),
47018
+ keyPackage: "",
47019
+ replaces: record.head.data.participants[0].holder
47020
+ });
47021
+ initial = await mls.createGroup(initialMember.snapshot, fromHexBytes(callId));
47022
+ } finally {
47023
+ if (solePredecessor)
47024
+ disposeState(initialMember);
47025
+ }
47026
+ if (closed || departing) {
47027
+ disposeState(initial);
47028
+ throw cancelled2();
47029
+ }
47030
+ if (admissionFailure) {
47031
+ disposeState(initial);
47032
+ throw admissionFailure;
47033
+ }
47034
+ pendingInitial = { state: initial, admission: initialAdmission };
47035
+ const initialHead = { epoch: initial.epoch, participants: [initialAdmission] };
47036
+ const packet = solePredecessor ? protocol.sign("replace", { previous: record.head, participant: initialAdmission }) : protocol.sign("pulse", { ...audio, media: null });
47037
+ submittedJoin = true;
47038
+ await mailbox.commit(initialHead, [packet], { expectedRevision: record.revision });
47039
+ } else {
47040
+ const predecessor = head.participants.find((item) => item.actor === admission.actor);
47041
+ predecessorHolder = predecessor?.holder || null;
47042
+ await event("join", { ...admission.data, replaces: predecessor?.holder || null });
47043
+ checkAdmission();
47044
+ }
47045
+ }
47046
+ return Object.freeze({
47047
+ start({ create = false } = {}) {
47048
+ if (startPromise)
47049
+ return startPromise;
47050
+ const admitted = new Promise((resolve, reject) => {
47051
+ admissionResult = { resolve, reject };
47052
+ });
47053
+ admissionTimer = setTimeout(() => settleAdmission(admissionError("calls/admission-timeout", "could not join the call")), CALL_ADMISSION_TIMEOUT_MS);
47054
+ startTask = beginStart(create);
47055
+ startPromise = Promise.race([startTask.then(() => admitted), admitted]).then(() => {
47056
+ if (closed || departing)
47057
+ throw cancelled2();
47058
+ pulseTimer = setTimeout(pulse, 1e4);
47059
+ }).catch((error) => {
47060
+ settleAdmission(error);
47061
+ throw error;
47062
+ });
47063
+ return startPromise;
47064
+ },
47065
+ signal(to, signal) {
47066
+ return event("signal", { to, signal });
47067
+ },
47068
+ async publishMedia(media) {
47069
+ ownMedia = media;
47070
+ await event("pulse", { ...audio, media });
47071
+ },
47072
+ async setAudio(value) {
47073
+ audio = { muted: value.muted === true || value.deafened === true, deafened: value.deafened === true };
47074
+ if (joined && !departing && !closed)
47075
+ await event("pulse", { ...audio, media: ownMedia });
47076
+ },
47077
+ async leave() {
47078
+ if (closed)
47079
+ return { ended: false };
47080
+ departing = true;
47081
+ settleAdmission(cancelled2());
47082
+ clearTimeout(pulseTimer);
47083
+ await startTask?.catch(() => {});
47084
+ if (closed || !joined && !submittedJoin || !mailbox.getSnapshot()?.head)
47085
+ return { ended: false };
47086
+ await event("leave", {});
47087
+ await mailbox.read();
47088
+ return { ended: !!head && head.participants.every((item) => !alive(item.holder)) };
47089
+ },
47090
+ close() {
47091
+ if (closed)
47092
+ return;
47093
+ closed = true;
47094
+ retryWait?.();
47095
+ settleAdmission(cancelled2());
47096
+ clearTimeout(pulseTimer);
47097
+ mailbox.close();
47098
+ disposeState(state);
47099
+ disposeState(member);
47100
+ disposeState(pendingCandidate?.state);
47101
+ disposeState(pendingInitial?.state);
47102
+ pendingJoins.clear();
47103
+ published.clear();
47104
+ seen.clear();
47105
+ }
47106
+ });
47107
+ }
47108
+
47109
+ // ../../core/calls/activity.js
47110
+ var positive = (value) => Number.isFinite(value) && value >= CALL_SPEAKING_THRESHOLD && value <= 1;
47111
+ function createCallActivity({ holder, onSpeaking, now = Date.now, setTimer = setTimeout, clearTimer = clearTimeout }) {
47112
+ let closed = false;
47113
+ let timer = null;
47114
+ let local = false;
47115
+ let direct = null;
47116
+ let routes = new Map;
47117
+ let sources = new Map;
47118
+ let published = [];
47119
+ const speaking = new Map;
47120
+ function publish() {
47121
+ const next = [...speaking.keys()].sort();
47122
+ if (published.length === next.length && published.every((value, index) => value === next[index]))
47123
+ return;
47124
+ published = next;
47125
+ onSpeaking?.(next);
47126
+ }
47127
+ function schedule() {
47128
+ clearTimer(timer);
47129
+ timer = null;
47130
+ if (!closed && speaking.size)
47131
+ timer = setTimer(expire, Math.max(0, Math.min(...speaking.values()) - now()));
47132
+ }
47133
+ function expire() {
47134
+ timer = null;
47135
+ if (closed)
47136
+ return;
47137
+ const time = now();
47138
+ for (const [id, deadline] of speaking)
47139
+ if (deadline <= time)
47140
+ speaking.delete(id);
47141
+ publish();
47142
+ schedule();
47143
+ }
47144
+ function update({ peers, received, directPeer, mode, muted, deafened }) {
47145
+ if (closed)
47146
+ return;
47147
+ const admitted = new Map(peers.map((peer) => [peer.holder, peer]));
47148
+ local = admitted.has(holder) && !muted && !deafened;
47149
+ direct = mode === "direct" && directPeer !== holder && admitted.has(directPeer) ? directPeer : null;
47150
+ const nextSources = new Map(local ? [[holder, "local"]] : []);
47151
+ const nextRoutes2 = new Map;
47152
+ const ambiguous = new Set;
47153
+ if (direct)
47154
+ nextSources.set(direct, "direct");
47155
+ if (mode === "group") {
47156
+ for (const [id, mid] of received) {
47157
+ if (id === holder || !admitted.has(id) || typeof mid !== "string" || !mid)
47158
+ continue;
47159
+ if (nextRoutes2.has(mid)) {
47160
+ nextSources.delete(nextRoutes2.get(mid));
47161
+ nextRoutes2.delete(mid);
47162
+ ambiguous.add(mid);
47163
+ }
47164
+ if (ambiguous.has(mid))
47165
+ continue;
47166
+ nextRoutes2.set(mid, id);
47167
+ nextSources.set(id, mid);
47168
+ }
47169
+ }
47170
+ const time = now();
47171
+ for (const [id, deadline] of speaking) {
47172
+ if (deadline <= time || !nextSources.has(id) || nextSources.get(id) !== sources.get(id))
47173
+ speaking.delete(id);
47174
+ }
47175
+ routes = nextRoutes2;
47176
+ sources = nextSources;
47177
+ publish();
47178
+ schedule();
47179
+ }
47180
+ function sample(value) {
47181
+ if (closed)
47182
+ return;
47183
+ const time = now();
47184
+ for (const [id, deadline] of speaking)
47185
+ if (deadline <= time)
47186
+ speaking.delete(id);
47187
+ if (local && positive(value.local))
47188
+ speaking.set(holder, time + CALL_SPEAKING_HOLD_MS);
47189
+ for (const item of value.receive) {
47190
+ if (typeof item.mid !== "string" || !item.mid || !positive(item.level))
47191
+ continue;
47192
+ const id = direct || routes.get(item.mid);
47193
+ if (id)
47194
+ speaking.set(id, time + CALL_SPEAKING_HOLD_MS);
47195
+ }
47196
+ publish();
47197
+ schedule();
47198
+ }
47199
+ function reset() {
47200
+ clearTimer(timer);
47201
+ timer = null;
47202
+ speaking.clear();
47203
+ publish();
47204
+ }
47205
+ function close() {
47206
+ if (closed)
47207
+ return;
47208
+ closed = true;
47209
+ reset();
47210
+ local = false;
47211
+ direct = null;
47212
+ routes.clear();
47213
+ sources.clear();
47214
+ }
47215
+ return Object.freeze({ update, sample, reset, close });
47216
+ }
47217
+
47218
+ // ../../core/calls/media.js
47219
+ var sdp = (value) => ({ type: value.type, sdp: value.sdp });
47220
+ var validMid = (value) => typeof value === "string" && /^[A-Za-z0-9_-]{1,32}$/u.test(value);
47221
+ var MAX_PENDING_SIGNALS = 128;
47222
+ var CONNECTION_TIMEOUT_MS = 15000;
47223
+ var retiredConnection = new Error("call connection retired");
47224
+ function createCallMediaSession({ port, mode, holder, preparation, provider, signal, publish, onState, onError, onExpired, onSpeaking, diag }) {
47225
+ let media = null;
47226
+ let closed = false;
47227
+ let deadline = null;
47228
+ let keys = null;
47229
+ let peers = [];
47230
+ let sessionId = null;
47231
+ let published = false;
47232
+ let started = false;
47233
+ let directPeer = null;
47234
+ let offered = false;
47235
+ let muted = false;
47236
+ let deafened = false;
47237
+ let microphoneVolume = 1;
47238
+ let closing = null;
47239
+ let connectionState = "new";
47240
+ let connectionWait = null;
47241
+ let tail = Promise.resolve();
47242
+ let connectionGeneration = 0;
47243
+ let openingConnection = null;
47244
+ let retiringConnection = Promise.resolve();
47245
+ let iceServers = null;
47246
+ const received = new Map;
47247
+ const peerAudio = new Map;
47248
+ const pendingPeerAudio = new Map;
47249
+ const activity = createCallActivity({ holder, onSpeaking });
47250
+ const updateActivity = () => activity.update({ peers, received, directPeer, mode, muted, deafened });
47251
+ const pendingSignals = [];
47252
+ const incomingSignals = [];
47253
+ function settlePeerAudio(id, error) {
47254
+ const pending = pendingPeerAudio.get(id);
47255
+ if (!pending)
47256
+ return;
47257
+ pendingPeerAudio.delete(id);
47258
+ if (error)
47259
+ pending.reject(error);
47260
+ else
47261
+ pending.resolve();
47262
+ }
47263
+ function current(generation = connectionGeneration) {
47264
+ if (closed)
47265
+ throw new Error("call media closed");
47266
+ if (generation !== connectionGeneration)
47267
+ throw retiredConnection;
47268
+ }
47269
+ async function disposeConnection(previous) {
47270
+ const results = await Promise.allSettled([() => previous?.revoke(), () => previous?.close()].map((operation) => {
47271
+ try {
47272
+ return Promise.resolve(operation());
47273
+ } catch (error) {
47274
+ return Promise.reject(error);
47275
+ }
47276
+ }));
47277
+ const failed = results.find((result) => result.status === "rejected");
47278
+ if (failed)
47279
+ throw failed.reason;
47280
+ }
47281
+ function retireDirectConnection(nextPeer) {
47282
+ connectionGeneration++;
47283
+ const previous = media;
47284
+ media = null;
47285
+ received.clear();
47286
+ directPeer = nextPeer;
47287
+ activity.reset();
47288
+ updateActivity();
47289
+ offered = false;
47290
+ connectionState = "new";
47291
+ pendingSignals.length = 0;
47292
+ const opening = openingConnection;
47293
+ retiringConnection = Promise.all([
47294
+ retiringConnection,
47295
+ disposeConnection(previous),
47296
+ opening?.then(disposeConnection, () => {})
47297
+ ]).then(() => {});
47298
+ retiringConnection.catch(() => {});
47299
+ onState?.({ state: "connecting" });
47300
+ }
47301
+ function settleConnection(error) {
47302
+ if (!connectionWait)
47303
+ return;
47304
+ const wait2 = connectionWait;
47305
+ connectionWait = null;
47306
+ clearTimeout(wait2.timer);
47307
+ if (error)
47308
+ wait2.reject(error);
47309
+ else
47310
+ wait2.resolve();
47311
+ }
47312
+ function connected() {
47313
+ current();
47314
+ if (connectionState === "connected")
47315
+ return Promise.resolve();
47316
+ if (["failed", "closed"].includes(connectionState))
47317
+ return Promise.reject(new Error("call connection failed"));
47318
+ if (!connectionWait) {
47319
+ const wait2 = {};
47320
+ wait2.promise = new Promise((resolve, reject) => {
47321
+ wait2.resolve = resolve;
47322
+ wait2.reject = reject;
47323
+ });
47324
+ wait2.timer = setTimeout(() => settleConnection(new Error("call connection timed out")), CONNECTION_TIMEOUT_MS);
47325
+ connectionWait = wait2;
47326
+ }
47327
+ return connectionWait.promise;
47328
+ }
47329
+ function serialize(work) {
47330
+ const result = tail.then(() => {
47331
+ current();
47332
+ return work();
47333
+ });
47334
+ tail = result.catch((error) => {
47335
+ if (!closed)
47336
+ onError?.(error);
47337
+ });
47338
+ return result;
47339
+ }
47340
+ function close() {
47341
+ if (closed)
47342
+ return closing;
47343
+ closed = true;
47344
+ activity.close();
47345
+ connectionGeneration++;
47346
+ settleConnection(new Error("call media closed"));
47347
+ const previous = media;
47348
+ media = keys = deadline = iceServers = null;
47349
+ received.clear();
47350
+ peerAudio.clear();
47351
+ for (const id of pendingPeerAudio.keys())
47352
+ settlePeerAudio(id, new Error("call media closed"));
47353
+ pendingSignals.length = incomingSignals.length = 0;
47354
+ const operations = [
47355
+ disposeConnection(previous),
47356
+ retiringConnection,
47357
+ openingConnection?.then((opened) => opened !== previous ? disposeConnection(opened) : undefined, () => {})
47358
+ ];
47359
+ closing = Promise.allSettled(operations).then((results) => {
47360
+ const failed = results.find((result) => result.status === "rejected");
47361
+ if (failed)
47362
+ throw failed.reason;
47363
+ });
47364
+ return closing;
47365
+ }
47366
+ async function openConnection() {
47367
+ const generation = connectionGeneration;
47368
+ const live = () => !closed && generation === connectionGeneration;
47369
+ await retiringConnection;
47370
+ current(generation);
47371
+ const startedAt = Date.now();
47372
+ const prepared = preparation;
47373
+ preparation = null;
47374
+ const opening = port.open({
47375
+ id: holder,
47376
+ mode,
47377
+ preparation: prepared,
47378
+ onState: (value) => {
47379
+ if (!live())
47380
+ return;
47381
+ connectionState = value.state;
47382
+ if (connectionState === "connected") {
47383
+ settleConnection();
47384
+ serialize(async () => {
47385
+ if (live())
47386
+ await media?.startSource?.();
47387
+ });
47388
+ } else if (["failed", "closed"].includes(connectionState))
47389
+ settleConnection(new Error("call connection failed"));
47390
+ onState?.(value);
47391
+ },
47392
+ onExpired: () => {
47393
+ if (live())
47394
+ onExpired?.();
47395
+ },
47396
+ onSignal(value) {
47397
+ if (mode !== "direct" || !live())
47398
+ return;
47399
+ if (directPeer)
47400
+ signal(directPeer, value).catch((error) => {
47401
+ if (live())
47402
+ onError?.(error);
47403
+ });
47404
+ else if (pendingSignals.length < MAX_PENDING_SIGNALS)
47405
+ pendingSignals.push(value);
47406
+ else
47407
+ onError?.(new Error("too many pending media signals"));
47408
+ },
47409
+ onTrack({ mid }) {
47410
+ if (!live())
47411
+ return;
47412
+ if (mode === "direct" && directPeer && validMid(mid))
47413
+ received.set(directPeer, mid);
47414
+ Promise.all([updateKeys(), applyPeerAudio()]).catch((error) => {
47415
+ if (live())
47416
+ onError?.(error);
47417
+ });
47418
+ },
47419
+ onActivity(value) {
47420
+ if (live())
47421
+ activity.sample(value);
47422
+ }
47423
+ });
47424
+ openingConnection = opening;
47425
+ const preparing = opening.then(async (opened) => {
47426
+ current(generation);
47427
+ media = opened;
47428
+ await opened.mute(muted);
47429
+ current(generation);
47430
+ await opened.deafen(deafened);
47431
+ current(generation);
47432
+ if (microphoneVolume !== 1) {
47433
+ await opened.setMicrophoneVolume(microphoneVolume);
47434
+ current(generation);
47435
+ }
47436
+ await opened.prepare();
47437
+ current(generation);
47438
+ markDone(diag, "call.media.setup", startedAt, { stage: "local" });
47439
+ return opened;
47440
+ });
47441
+ try {
47442
+ const [opened, configuration] = await Promise.all([
47443
+ preparing,
47444
+ Promise.resolve().then(() => iceServers ? { iceServers, sessionId } : provider(mode === "group" ? "session/new" : "turn", {})).then((value) => {
47445
+ current(generation);
47446
+ markDone(diag, "call.media.setup", startedAt, { stage: "provider" });
47447
+ return value;
47448
+ })
47449
+ ]);
47450
+ current(generation);
47451
+ iceServers = configuration.iceServers;
47452
+ if (mode === "group")
47453
+ sessionId = configuration.sessionId;
47454
+ await opened.configure(iceServers);
47455
+ current(generation);
47456
+ if (deadline) {
47457
+ await opened.grant(deadline);
47458
+ current(generation);
47459
+ }
47460
+ await updateKeys();
47461
+ current(generation);
47462
+ markDone(diag, "call.media.setup", startedAt, { stage: "ready" });
47463
+ } catch (error) {
47464
+ await disposeConnection(await opening.catch(() => null));
47465
+ throw error;
47466
+ } finally {
47467
+ if (openingConnection === opening)
47468
+ openingConnection = null;
47469
+ }
47470
+ }
47471
+ async function applyDescription(result, required = false) {
47472
+ current();
47473
+ if (!result || result.errorCode || result.tracks?.some((track) => track.errorCode))
47474
+ throw new Error("call negotiation failed");
47475
+ const description = result.sessionDescription;
47476
+ if (!description) {
47477
+ if (required || result.requiresImmediateRenegotiation)
47478
+ throw new Error("missing call description");
47479
+ return;
47480
+ }
47481
+ if (!["offer", "answer"].includes(description.type) || typeof description.sdp !== "string" || !description.sdp.length)
47482
+ throw new Error("invalid call description");
47483
+ if (description.type === "offer") {
47484
+ const answer = await media.answer(description);
47485
+ current();
47486
+ await provider("renegotiate", { sessionId, sessionDescription: sdp(answer) });
47487
+ current();
47488
+ } else {
47489
+ await media.remote(description);
47490
+ current();
47491
+ }
47492
+ }
47493
+ async function updateKeys() {
47494
+ if (!media || !keys || mode !== "group")
47495
+ return;
47496
+ const receive = [];
47497
+ for (const peer of peers) {
47498
+ const mid = received.get(peer.holder);
47499
+ const member = keys.members.find((item) => toHex(item.leafId) === peer.holder);
47500
+ if (mid != null && member)
47501
+ receive.push({
47502
+ mid,
47503
+ baseKey: keys.baseKey,
47504
+ epoch: keys.epoch,
47505
+ leafIndex: member.leafIndex,
47506
+ contextId: 0
47507
+ });
47508
+ }
47509
+ await media.setKeys({ send: { baseKey: keys.baseKey, epoch: keys.epoch, leafIndex: keys.leafIndex, contextId: 0 }, receive });
47510
+ }
47511
+ function applyPeerAudio() {
47512
+ return Promise.all([...peerAudio].map(async ([id, entry]) => {
47513
+ const mid = received.get(id);
47514
+ if (!media || mid == null)
47515
+ return;
47516
+ const value = entry.value;
47517
+ try {
47518
+ await media.setPeerAudio({ mid, ...value });
47519
+ settlePeerAudio(id);
47520
+ } catch (error) {
47521
+ if (pendingPeerAudio.has(id))
47522
+ settlePeerAudio(id, error);
47523
+ else
47524
+ throw error;
47525
+ }
47526
+ }));
47527
+ }
47528
+ async function negotiate() {
47529
+ if (!started || !keys)
47530
+ return;
47531
+ if (mode === "group") {
47532
+ if (!published) {
47533
+ const offer = await media.offer();
47534
+ current();
47535
+ const sendMid = media.sendMid;
47536
+ if (!validMid(sendMid))
47537
+ throw new Error("microphone track unavailable");
47538
+ const trackName = toHex(randomBytes3(16));
47539
+ const result = await provider("tracks/new", {
47540
+ sessionId,
47541
+ sessionDescription: sdp(offer),
47542
+ tracks: [{ location: "local", mid: sendMid, trackName }]
47543
+ });
47544
+ current();
47545
+ if (!Array.isArray(result.tracks) || result.tracks.length !== 1 || result.tracks[0].mid !== sendMid || result.tracks[0].trackName !== trackName)
47546
+ throw new Error("microphone publication failed");
47547
+ await applyDescription(result, true);
47548
+ current();
47549
+ await connected();
47550
+ current();
47551
+ await Promise.all([
47552
+ publish({ sessionId, trackName }).then(() => {
47553
+ current();
47554
+ published = true;
47555
+ }),
47556
+ receiveGroupTracks()
47557
+ ]);
47558
+ } else {
47559
+ await receiveGroupTracks();
47560
+ }
47561
+ } else {
47562
+ const peer = peers.find((item) => item.holder !== holder);
47563
+ if (!peer)
47564
+ return;
47565
+ directPeer = peer.holder;
47566
+ updateActivity();
47567
+ const generation = connectionGeneration;
47568
+ try {
47569
+ if (!media)
47570
+ await openConnection();
47571
+ current(generation);
47572
+ if (holder < peer.holder && !offered) {
47573
+ offered = true;
47574
+ const offer = await media.offer();
47575
+ current(generation);
47576
+ await signal(peer.holder, { type: "description", description: sdp(offer) });
47577
+ current(generation);
47578
+ }
47579
+ } catch (error) {
47580
+ if (!closed && generation !== connectionGeneration)
47581
+ return;
47582
+ await close();
47583
+ throw error;
47584
+ }
47585
+ }
47586
+ }
47587
+ async function receiveGroupTracks() {
47588
+ const additions = peers.filter((peer) => peer.holder !== holder && peer.media?.sessionId && peer.media?.trackName && !received.has(peer.holder));
47589
+ if (additions.length) {
47590
+ await connected();
47591
+ current();
47592
+ const result = await provider("tracks/new", { sessionId, tracks: additions.map((peer) => ({
47593
+ location: "remote",
47594
+ sessionId: peer.media.sessionId,
47595
+ trackName: peer.media.trackName
47596
+ })) });
47597
+ current();
47598
+ if (!Array.isArray(result.tracks) || result.tracks.length !== additions.length)
47599
+ throw new Error("peer audio subscription failed");
47600
+ const mappings = [];
47601
+ const occupied = new Set([media.sendMid, ...received.values()]);
47602
+ for (const peer of additions) {
47603
+ const track = result.tracks?.find((item) => item.sessionId === peer.media.sessionId && item.trackName === peer.media.trackName);
47604
+ if (!validMid(track?.mid) || track.errorCode || occupied.has(track.mid))
47605
+ throw new Error("peer audio subscription failed");
47606
+ occupied.add(track.mid);
47607
+ mappings.push([peer.holder, track.mid]);
47608
+ }
47609
+ for (const [peer, mid] of mappings)
47610
+ received.set(peer, mid);
47611
+ updateActivity();
47612
+ await applyPeerAudio();
47613
+ current();
47614
+ await updateKeys();
47615
+ current();
47616
+ await applyDescription(result);
47617
+ }
47618
+ const removals = [...received].filter(([peer]) => !peers.some((item) => item.holder === peer));
47619
+ if (removals.length) {
47620
+ for (const [peer] of removals)
47621
+ received.delete(peer);
47622
+ updateActivity();
47623
+ await updateKeys();
47624
+ current();
47625
+ const result = await provider("tracks/close", { sessionId, tracks: removals.map(([, mid]) => ({ mid })) });
47626
+ await applyDescription(result);
47627
+ }
47628
+ }
47629
+ return Object.freeze({
47630
+ async open() {
47631
+ current();
47632
+ try {
47633
+ await openConnection();
47634
+ started = true;
47635
+ await serialize(negotiate);
47636
+ for (const [value, from] of incomingSignals.splice(0))
47637
+ await handleSignal(value, from);
47638
+ } catch (error) {
47639
+ await close();
47640
+ throw error;
47641
+ }
47642
+ },
47643
+ grant(value) {
47644
+ current();
47645
+ deadline = value;
47646
+ return media?.grant(value);
47647
+ },
47648
+ async setKeys(value) {
47649
+ current();
47650
+ keys = value;
47651
+ await updateKeys();
47652
+ current();
47653
+ return serialize(negotiate);
47654
+ },
47655
+ async setParticipants(value) {
47656
+ current();
47657
+ peers = value;
47658
+ for (const id of peerAudio.keys())
47659
+ if (!peers.some((peer) => peer.holder === id)) {
47660
+ peerAudio.delete(id);
47661
+ settlePeerAudio(id, new Error("call participant unavailable"));
47662
+ }
47663
+ updateActivity();
47664
+ const nextPeer = peers.find((peer) => peer.holder !== holder)?.holder || null;
47665
+ if (mode === "direct" && started && directPeer && nextPeer !== directPeer)
47666
+ retireDirectConnection(nextPeer);
47667
+ await updateKeys();
47668
+ current();
47669
+ return serialize(negotiate);
47670
+ },
47671
+ signal(value, from) {
47672
+ if (closed)
47673
+ return Promise.reject(new Error("call media closed"));
47674
+ if (!started) {
47675
+ if (incomingSignals.length >= MAX_PENDING_SIGNALS)
47676
+ return Promise.reject(new Error("too many pending media signals"));
47677
+ incomingSignals.push([value, from]);
47678
+ return Promise.resolve();
47679
+ }
47680
+ return handleSignal(value, from);
47681
+ },
47682
+ mute(value) {
47683
+ current();
47684
+ const next = value === true;
47685
+ if (muted === next)
47686
+ return;
47687
+ muted = next;
47688
+ updateActivity();
47689
+ return media?.mute(muted);
47690
+ },
47691
+ deafen(value) {
47692
+ current();
47693
+ const next = value === true;
47694
+ if (deafened === next)
47695
+ return;
47696
+ deafened = next;
47697
+ updateActivity();
47698
+ return media?.deafen(deafened);
47699
+ },
47700
+ async setMicrophoneVolume(value) {
47701
+ current();
47702
+ const previous = microphoneVolume;
47703
+ microphoneVolume = value;
47704
+ try {
47705
+ if (media)
47706
+ await media.setMicrophoneVolume(value);
47707
+ } catch (error) {
47708
+ if (microphoneVolume === value)
47709
+ microphoneVolume = previous;
47710
+ throw error;
47711
+ }
47712
+ },
47713
+ async setPeerAudio(id, value) {
47714
+ current();
47715
+ const mid = received.get(id);
47716
+ if (id === holder || !peers.some((peer) => peer.holder === id))
47717
+ throw new Error("call participant unavailable");
47718
+ let entry = peerAudio.get(id);
47719
+ if (!entry) {
47720
+ entry = { value: null, applied: null };
47721
+ peerAudio.set(id, entry);
47722
+ }
47723
+ entry.value = value;
47724
+ try {
47725
+ if (media && mid != null)
47726
+ await media.setPeerAudio({ mid, ...value });
47727
+ else {
47728
+ let pending = pendingPeerAudio.get(id);
47729
+ if (!pending) {
47730
+ pending = {};
47731
+ pending.promise = new Promise((resolve, reject) => {
47732
+ pending.resolve = resolve;
47733
+ pending.reject = reject;
47734
+ });
47735
+ pendingPeerAudio.set(id, pending);
47736
+ }
47737
+ await pending.promise;
47738
+ }
47739
+ if (peerAudio.get(id) === entry && entry.value === value)
47740
+ entry.applied = value;
47741
+ } catch (error) {
47742
+ if (peerAudio.get(id) === entry && entry.value === value) {
47743
+ if (entry.applied)
47744
+ entry.value = entry.applied;
47745
+ else
47746
+ peerAudio.delete(id);
47747
+ }
47748
+ throw error;
47749
+ }
47750
+ },
47751
+ close
47752
+ });
47753
+ function handleSignal(value, from) {
47754
+ return serialize(async () => {
47755
+ if (mode !== "direct")
47756
+ throw new Error("unadmitted media signal");
47757
+ if (from === holder || !peers.some((peer) => peer.holder === from))
47758
+ return;
47759
+ const generation = connectionGeneration;
47760
+ try {
47761
+ await negotiate();
47762
+ current(generation);
47763
+ if (value.type === "description") {
47764
+ if (value.description.type === "offer") {
47765
+ if (holder < from)
47766
+ throw new Error("unexpected direct offer");
47767
+ offered = true;
47768
+ const answer = await media.answer(value.description);
47769
+ current(generation);
47770
+ await signal(from, { type: "description", description: sdp(answer) });
47771
+ } else
47772
+ await media.remote(value.description);
47773
+ } else if (value.type === "ice")
47774
+ await media.ice(value.candidate);
47775
+ else
47776
+ throw new Error("invalid media signal");
47777
+ current(generation);
47778
+ for (const pending of pendingSignals.splice(0)) {
47779
+ await signal(from, pending);
47780
+ current(generation);
47781
+ }
47782
+ } catch (error) {
47783
+ if (!closed && generation !== connectionGeneration)
47784
+ return;
47785
+ throw error;
47786
+ }
47787
+ });
47788
+ }
47789
+ }
47790
+
47791
+ // ../../core/calls/observation.js
47792
+ function openCallObservation({ cloud, capability, endpoint, protocol, onChange, onEmpty, onError }) {
47793
+ let verified = null;
47794
+ let identities = new Map;
47795
+ const audio = new Map;
47796
+ const departed = new Set;
47797
+ let last = "";
47798
+ let pending = null;
47799
+ let dirty = false;
47800
+ let closed = false;
47801
+ const mailbox = openCallMailbox({
47802
+ cloud,
47803
+ capability,
47804
+ endpoint,
47805
+ protocol,
47806
+ onError,
47807
+ stream: false,
47808
+ onChange(record) {
47809
+ const head = record.head;
47810
+ const admissions = head?.data?.participants || [];
47811
+ if (head && head !== verified) {
47812
+ if (!Number.isSafeInteger(head.data?.epoch) || head.data.epoch < 0)
47813
+ throw new Error("invalid call observation");
47814
+ const nextIdentities = verifyCallAdmissions(admissions, protocol);
47815
+ if (!admissions.some((item) => item.actor === head.actor && item.holder === head.holder && item.endpoint === head.endpoint)) {
47816
+ throw new Error("unadmitted call head");
47817
+ }
47818
+ identities = nextIdentities;
47819
+ } else if (!head)
47820
+ identities.clear();
47821
+ verified = head;
47822
+ if (record.historyLost) {
47823
+ audio.clear();
47824
+ departed.clear();
47825
+ }
47826
+ const holders = new Set(admissions.map((item) => item.holder));
47827
+ for (const holder of audio.keys())
47828
+ if (!holders.has(holder))
47829
+ audio.delete(holder);
47830
+ for (const holder of departed)
47831
+ if (!holders.has(holder))
47832
+ departed.delete(holder);
47833
+ for (const packet of record.events) {
47834
+ if (!admissions.some((item) => item.holder === packet.holder && item.actor === packet.actor && item.endpoint === packet.endpoint))
47835
+ continue;
47836
+ if (packet.kind === "leave")
47837
+ departed.add(packet.holder);
47838
+ if (packet.kind === "pulse")
47839
+ audio.set(packet.holder, { muted: packet.data.muted === true, deafened: packet.data.deafened === true });
47840
+ }
47841
+ const roster = admissions.filter((item) => !departed.has(item.holder)).map((item) => ({
47842
+ ...identities.get(item.holder),
47843
+ holder: item.holder,
47844
+ muted: audio.get(item.holder)?.muted === true || audio.get(item.holder)?.deafened === true,
47845
+ deafened: audio.get(item.holder)?.deafened === true
47846
+ }));
47847
+ const fingerprint = JSON.stringify(roster);
47848
+ if (fingerprint !== last) {
47849
+ last = fingerprint;
47850
+ onChange(roster);
47851
+ }
47852
+ if (!head || admissions.length && roster.length === 0)
47853
+ onEmpty?.();
47854
+ }
47855
+ });
47856
+ function read() {
47857
+ if (closed)
47858
+ return Promise.resolve();
47859
+ dirty = true;
47860
+ pending ||= (async () => {
47861
+ while (dirty && !closed) {
47862
+ dirty = false;
47863
+ await mailbox.read();
47864
+ }
47865
+ })().finally(() => {
47866
+ pending = null;
47867
+ });
47868
+ return pending;
47869
+ }
47870
+ return Object.freeze({ read, close() {
47871
+ closed = true;
47872
+ mailbox.close();
47873
+ } });
47874
+ }
47875
+
47876
+ // ../../core/calls/session.js
47877
+ var initial = () => ({ phase: "idle", chatId: null, callId: null, joiningChatId: null, mode: null, transport: null, available: null, elsewhere: null, participants: [], speaking: [], peerAudio: {}, muted: false, deafened: false, microphoneVolume: 100, error: null });
47878
+ var cancelled2 = () => Object.assign(new Error("call cancelled"), { code: "calls/cancelled" });
47879
+ var ended = () => Object.assign(new Error("this call has ended"), { code: "calls/ended" });
47880
+ var callError = (chatId, error) => ({ chatId, code: error?.code || "calls/failed", message: error?.message || "could not connect the call" });
47881
+ function createCallSession({ cloud, chat, media: mediaPort, mls, savePeerAudio, saveAudio, diag }) {
47882
+ const listeners = new Set;
47883
+ const discoveries = new Map;
47884
+ const discoveryReads = new Map;
47885
+ const observers = new Map;
47886
+ const retiring = new Set;
47887
+ let state = initial();
47888
+ const confirmedPeerAudio = new Map;
47889
+ const pendingPeerAudio = new Map;
47890
+ let peerAudioSource = null;
47891
+ let audioSource = normalizeCallAudio();
47892
+ let pendingAudio = null;
47893
+ let identity = null;
47894
+ let endpoint = null;
47895
+ let leaseEndpoint = null;
47896
+ let lease = null;
47897
+ let active = null;
47898
+ let participation = null;
47899
+ let focused = null;
47900
+ let generation = 0;
47901
+ let joinIntent = 0;
47902
+ let joining = null;
47903
+ let closed = false;
47904
+ let foreground = true;
47905
+ let online = true;
47906
+ let leaving = Promise.resolve();
47907
+ let drainingSources = Promise.resolve();
47908
+ let closingSession = null;
47909
+ const emit = (patch) => {
47910
+ if (Object.entries(patch).every(([field, value]) => Object.is(state[field], value)))
47911
+ return;
47912
+ if (patch.phase && patch.phase !== state.phase) {
47913
+ diag?.("call.phase", {
47914
+ phase: patch.phase,
47915
+ previous: state.phase,
47916
+ mode: patch.mode || state.mode,
47917
+ participants: (patch.participants || state.participants).length
47918
+ });
47919
+ }
47920
+ state = { ...state, ...patch };
47921
+ for (const listener of listeners)
47922
+ listener();
47923
+ };
47924
+ function announce(chatId, callId, event) {
47925
+ chat.getSnapshot().sendChatMessage(chatId, makeCallMessage(callId, event)).catch((error) => {
47926
+ diag?.("call.message.error", { event, code: error?.code || "" });
47927
+ });
47928
+ }
47929
+ function current(token) {
47930
+ if (closed || !identity || token !== generation)
47931
+ throw cancelled2();
47932
+ }
47933
+ function fail(error) {
47934
+ if (!identity || closed)
47935
+ return;
47936
+ diag?.("call.error", {
47937
+ code: error?.code || "calls/failed",
47938
+ phase: state.phase,
47939
+ mode: state.mode,
47940
+ participants: state.participants.length,
47941
+ message: error?.message || "call failed"
47942
+ });
47943
+ const chatId = active?.chatId || state.chatId;
47944
+ leave();
47945
+ emit({ phase: "error", chatId, error: callError(chatId, error) });
47946
+ }
47947
+ function available() {
47948
+ const entry = discoveries.get(focused);
47949
+ const value = (chatId, current2) => current2?.descriptor ? {
47950
+ chatId,
47951
+ callId: current2.descriptor.callId,
47952
+ startedAt: current2.descriptor.startedAt,
47953
+ participants: current2.descriptor.participants || 1,
47954
+ roster: (active?.discovery === current2 ? active.participants : current2.roster).filter((peer) => ![...retiring].some((call) => call.callId === current2.descriptor.callId && call.endpoint.holder === peer.holder))
47955
+ } : null;
47956
+ for (const observer of observers.values()) {
47957
+ if (!observer.onAvailable)
47958
+ continue;
47959
+ const next2 = value(observer.chatId, discoveries.get(observer.chatId));
47960
+ const fingerprint = JSON.stringify(next2);
47961
+ if (observer.fingerprint === fingerprint)
47962
+ continue;
47963
+ observer.fingerprint = fingerprint;
47964
+ try {
47965
+ observer.onAvailable(next2);
47966
+ } catch {}
47967
+ }
47968
+ const next = value(focused, entry);
47969
+ if (JSON.stringify(next) !== JSON.stringify(state.available))
47970
+ emit({ available: next });
47971
+ }
47972
+ function closeDiscovery(entry) {
47973
+ if ([...retiring].some((call) => call.discovery === entry))
47974
+ return;
47975
+ entry.mailbox.close();
47976
+ entry.observation?.close();
47977
+ entry.observation = null;
47978
+ entry.observedCallId = null;
47979
+ }
47980
+ function observeRoster(entry, refresh = false) {
47981
+ const descriptor = entry.descriptor;
47982
+ const callId = descriptor?.callId;
47983
+ if (!callId || active?.discovery === entry || ![...observers.values()].some((item) => item.chatId === entry.epochState.manifest.chatId)) {
47984
+ entry.observation?.close();
47985
+ entry.observation = null;
47986
+ entry.observedCallId = null;
47987
+ return;
47988
+ }
47989
+ if (entry.observedCallId === callId) {
47990
+ if (refresh)
47991
+ entry.observation.read().catch((error) => diag?.("call.discovery.error", { code: error?.code || "calls/roster" }));
47992
+ return;
47993
+ }
47994
+ entry.observation?.close();
47995
+ entry.roster = [];
47996
+ entry.observedCallId = callId;
47997
+ const token = generation;
47998
+ const protocol = createCallProtocol({ realm: cloud.environment, epochState: entry.epochState, identity, endpoint, callId });
47999
+ const secret = fromHexBytes(descriptor.secret);
48000
+ const capability = createCallCapability(secret, cloud.environment, "signaling", [callId]);
48001
+ cleanBytes(secret);
48002
+ entry.observation = openCallObservation({
48003
+ cloud,
48004
+ capability,
48005
+ endpoint,
48006
+ protocol,
48007
+ onChange(roster) {
48008
+ if (generation !== token || entry.observedCallId !== callId || discoveries.get(entry.epochState.manifest.chatId) !== entry)
48009
+ return;
48010
+ entry.roster = roster;
48011
+ available();
48012
+ },
48013
+ onError(error) {
48014
+ if (generation === token)
48015
+ diag?.("call.discovery.error", { code: error?.code || "calls/roster" });
48016
+ },
48017
+ onEmpty() {
48018
+ if (generation === token && entry.observedCallId === callId)
48019
+ finishDiscovery(entry, descriptor);
48020
+ }
48021
+ });
48022
+ entry.observation.read().catch(() => {});
48023
+ }
48024
+ function prune() {
48025
+ for (const [chatId, value] of discoveries) {
48026
+ if (![...observers.values()].some((item) => item.chatId === chatId) && chatId !== active?.chatId && chatId !== joining?.chatId && ![...retiring].some((call) => call.chatId === chatId)) {
48027
+ closeDiscovery(value);
48028
+ discoveries.delete(chatId);
48029
+ } else
48030
+ observeRoster(value);
48031
+ }
48032
+ }
48033
+ function discovery(chatId, epochState) {
48034
+ const token = generation;
48035
+ const existing = discoveries.get(chatId);
48036
+ if (existing?.epochState.manifest.epochId === epochState.manifest.epochId)
48037
+ return existing;
48038
+ if (existing)
48039
+ closeDiscovery(existing);
48040
+ const protocol = createCallProtocol({ realm: cloud.environment, epochState, identity, endpoint });
48041
+ const entry = {
48042
+ epochState,
48043
+ protocol,
48044
+ descriptor: null,
48045
+ mailbox: null,
48046
+ observation: null,
48047
+ observedCallId: null,
48048
+ roster: [],
48049
+ revision: null,
48050
+ expiredCallId: null,
48051
+ finishing: new Map
48052
+ };
48053
+ entry.mailbox = openCallMailbox({
48054
+ cloud,
48055
+ capability: createCallDiscovery(epochState, cloud.environment),
48056
+ endpoint,
48057
+ protocol,
48058
+ onChange(record) {
48059
+ if (token !== generation || discoveries.get(chatId) !== entry)
48060
+ return;
48061
+ const descriptor = record.head?.data || null;
48062
+ if (descriptor && (!/^[a-f0-9]{64}$/u.test(descriptor.callId) || !/^[a-f0-9]{64}$/u.test(descriptor.secret) || descriptor.mode !== epochState.manifest.lineage || !Number.isSafeInteger(descriptor.startedAt) || descriptor.startedAt < 0 || descriptor.startedAt > Date.now() + 5000))
48063
+ throw new Error("invalid call discovery");
48064
+ const previous = entry.descriptor;
48065
+ entry.expiredCallId = !record.head ? previous?.callId || entry.expiredCallId : null;
48066
+ entry.descriptor = descriptor;
48067
+ if (!record.head && previous)
48068
+ finishDiscovery(entry, previous);
48069
+ const changed = entry.revision !== record.revision;
48070
+ entry.revision = record.revision;
48071
+ if (active?.discovery === entry && active.published && descriptor?.callId !== active.callId) {
48072
+ leave();
48073
+ }
48074
+ observeRoster(entry, changed);
48075
+ available();
48076
+ },
48077
+ onError(error) {
48078
+ if (token !== generation || discoveries.get(chatId) !== entry)
48079
+ return;
48080
+ diag?.("call.discovery.error", { code: error?.code || "calls/discovery" });
48081
+ }
48082
+ });
48083
+ discoveries.set(chatId, entry);
48084
+ return entry;
48085
+ }
48086
+ function readDiscovery(chatId, preparedEpoch) {
48087
+ const epochState = preparedEpoch || chat.getSnapshot().getOwnerChat(chatId)?.epochState;
48088
+ const epochId = epochState?.manifest.epochId;
48089
+ const pending = discoveryReads.get(chatId);
48090
+ if (pending && pending.epochId === epochId)
48091
+ return pending.promise;
48092
+ const slot = { epochId, promise: null };
48093
+ const token = generation;
48094
+ const reading = Promise.resolve().then(async () => {
48095
+ current(token);
48096
+ if (!online)
48097
+ throw Object.assign(new Error("chat unavailable"), { code: "unavailable" });
48098
+ if (!epochState)
48099
+ throw Object.assign(new Error("chat unavailable"), { code: "calls/chat-not-ready" });
48100
+ if (chat.getSnapshot().getOwnerChat(chatId)?.epochState?.manifest.epochId !== epochId)
48101
+ throw cancelled2();
48102
+ const existing = discoveries.get(chatId);
48103
+ const entry = discovery(chatId, epochState);
48104
+ if (entry === existing)
48105
+ await entry.mailbox.read();
48106
+ else
48107
+ await entry.mailbox.ready();
48108
+ current(token);
48109
+ return entry;
48110
+ }).finally(() => {
48111
+ if (discoveryReads.get(chatId) === slot)
48112
+ discoveryReads.delete(chatId);
48113
+ prune();
48114
+ });
48115
+ slot.promise = reading;
48116
+ discoveryReads.set(chatId, slot);
48117
+ return reading;
48118
+ }
48119
+ function observe(chatId, onAvailable) {
48120
+ if (!chatId || closed)
48121
+ return () => {};
48122
+ const observer = { chatId, onAvailable, fingerprint: undefined };
48123
+ observers.set(observer, observer);
48124
+ focused = chatId;
48125
+ if (state.error?.chatId === chatId)
48126
+ emit({ error: null });
48127
+ available();
48128
+ const entry = discoveries.get(chatId);
48129
+ if (entry)
48130
+ observeRoster(entry);
48131
+ else if (identity && online)
48132
+ readDiscovery(chatId).catch((error) => diag?.("call.discovery.error", { stage: "prepare", code: error?.code || "calls/admission" }));
48133
+ return () => {
48134
+ if (!observers.delete(observer))
48135
+ return;
48136
+ focused = [...observers.values()].at(-1)?.chatId || null;
48137
+ available();
48138
+ prune();
48139
+ };
48140
+ }
48141
+ async function joinAttempt(chatId, intent, expectedCallId, onlyExisting, continuation = null) {
48142
+ if (!identity || !mediaPort || !mls || !cloud.calls)
48143
+ throw new Error("calling unavailable");
48144
+ if (active?.chatId === chatId)
48145
+ return chatId;
48146
+ const token = generation;
48147
+ const startedAt = Date.now();
48148
+ const step = (stage) => diag?.("call.join.stage", { stage, elapsedMs: Date.now() - startedAt });
48149
+ let preflightActive = true;
48150
+ const check = () => {
48151
+ current(token);
48152
+ if (!preflightActive || intent !== joinIntent)
48153
+ throw cancelled2();
48154
+ };
48155
+ const operation = joining;
48156
+ let preparation;
48157
+ let call;
48158
+ try {
48159
+ emit({ ...!active && !continuation ? { phase: "joining", chatId } : {}, error: null });
48160
+ const permission = (async () => {
48161
+ const value = continuation ? undefined : await mediaPort.prepare?.();
48162
+ try {
48163
+ check();
48164
+ } catch (error) {
48165
+ value?.close();
48166
+ throw error;
48167
+ }
48168
+ preparation = value;
48169
+ operation.preparation = value;
48170
+ step("permission");
48171
+ })();
48172
+ const destination = (async () => {
48173
+ if (!onlyExisting && !continuation)
48174
+ chatId = await chat.getSnapshot().materializeChat(chatId);
48175
+ check();
48176
+ joining.chatId = chatId;
48177
+ step("chat");
48178
+ const epochState = continuation ? chat.getSnapshot().getOwnerChat(chatId)?.epochState : await chat.getSnapshot().prepareCall(chatId);
48179
+ check();
48180
+ if (!epochState)
48181
+ throw cancelled2();
48182
+ const entry2 = await readDiscovery(chatId, epochState);
48183
+ check();
48184
+ step("discovery");
48185
+ return entry2;
48186
+ })();
48187
+ const [, entry] = await Promise.all([permission, destination]);
48188
+ check();
48189
+ const discoveryRecord = entry.mailbox.getSnapshot();
48190
+ const create = !entry.descriptor;
48191
+ if (expectedCallId && (onlyExisting || entry.epochState.manifest.lineage === "direct") && entry.descriptor?.callId !== expectedCallId)
48192
+ throw ended();
48193
+ const descriptor = entry.descriptor || {
48194
+ callId: continuation?.callId || toHex(randomBytes3(32)),
48195
+ secret: toHex(randomBytes3(32)),
48196
+ mode: entry.epochState.manifest.lineage,
48197
+ participants: 1,
48198
+ startedAt: continuation?.startedAt ?? Date.now()
48199
+ };
48200
+ if (continuation && descriptor.callId !== continuation.callId)
48201
+ throw ended();
48202
+ const callEndpoint = continuation?.endpoint || createCallEndpoint();
48203
+ call = {
48204
+ chatId,
48205
+ callId: descriptor.callId,
48206
+ discovery: entry,
48207
+ descriptor,
48208
+ endpoint: callEndpoint,
48209
+ published: false,
48210
+ room: null,
48211
+ member: null,
48212
+ media: null,
48213
+ owner: null,
48214
+ participants: [],
48215
+ error: null,
48216
+ timer: null,
48217
+ joinTimer: null,
48218
+ mediaReady: false,
48219
+ leaving: false,
48220
+ muteTail: Promise.resolve()
48221
+ };
48222
+ joining.candidate = call;
48223
+ call.owner = continuation?.owner || createLease(identity, callEndpoint);
48224
+ const protocol = createCallProtocol({ realm: cloud.environment, epochState: entry.epochState, identity, endpoint: callEndpoint, callId: descriptor.callId });
48225
+ call.protocol = protocol;
48226
+ const ownMember = (value) => {
48227
+ try {
48228
+ check();
48229
+ if (call.leaving)
48230
+ throw cancelled2();
48231
+ } catch (error) {
48232
+ cleanBytes(value.snapshot);
48233
+ throw error;
48234
+ }
48235
+ call.member = value;
48236
+ return value;
48237
+ };
48238
+ const callMember = ownMember(await mls.createMember(fromHexBytes(callEndpoint.holder)));
48239
+ const member = create ? callMember : ownMember(await mls.createKeyPackage(callMember.snapshot));
48240
+ if (!create)
48241
+ cleanBytes(callMember.snapshot);
48242
+ check();
48243
+ step("identity");
48244
+ const admission = protocol.sign("join", { keyPackage: member.keyPackage ? encodeCallBytes(member.keyPackage) : "", signaturePK: toHex(member.signaturePK) });
48245
+ const secret = fromHexBytes(descriptor.secret);
48246
+ const capability = createCallCapability(secret, cloud.environment, "signaling", [descriptor.callId]);
48247
+ cleanBytes(secret);
48248
+ const valid = () => token === generation && active === call && !call.leaving;
48249
+ const preparing = () => token === generation && intent === joinIntent && !call.leaving;
48250
+ const checkCall = () => {
48251
+ check();
48252
+ if (call.leaving)
48253
+ throw cancelled2();
48254
+ if (call.error)
48255
+ throw call.error;
48256
+ };
48257
+ const connectionProgress = () => {
48258
+ if (!valid() || state.phase === "connected")
48259
+ return;
48260
+ if (call.mediaReady && descriptor.mode === "direct" && state.participants.length < 2) {
48261
+ clearTimeout(call.joinTimer);
48262
+ call.joinTimer = null;
48263
+ emit({ phase: "waiting" });
48264
+ } else {
48265
+ emit({ phase: "connecting" });
48266
+ if (!call.joinTimer)
48267
+ call.joinTimer = setTimeout(() => {
48268
+ if (valid())
48269
+ fail(new Error("call did not connect"));
48270
+ }, 30000);
48271
+ }
48272
+ };
48273
+ call.media = createCallMediaSession({
48274
+ port: mediaPort,
48275
+ preparation,
48276
+ mode: descriptor.mode,
48277
+ holder: callEndpoint.holder,
48278
+ diag,
48279
+ provider: (...args) => call.owner.provider(...args),
48280
+ signal: (...args) => call.room.signal(...args),
48281
+ publish: (value) => call.room.publishMedia(value),
48282
+ onState(value) {
48283
+ if (!valid())
48284
+ return;
48285
+ diag?.("call.media.state", {
48286
+ state: value.state,
48287
+ reason: value.reason || "",
48288
+ mode: descriptor.mode,
48289
+ transport: value.transport || "",
48290
+ participants: call.participants.length
48291
+ });
48292
+ if (value.state === "connected") {
48293
+ if (state.phase !== "connected")
48294
+ step("connected");
48295
+ clearTimeout(call.joinTimer);
48296
+ call.joinTimer = null;
48297
+ emit({ phase: "connected", transport: value.transport || (descriptor.mode === "group" ? "relay" : null) });
48298
+ } else if (value.state === "connecting") {
48299
+ emit({ phase: "connecting" });
48300
+ connectionProgress();
48301
+ } else if (["failed", "closed"].includes(value.state))
48302
+ fail(Object.assign(new Error("call connection ended"), { code: "calls/media-ended" }));
48303
+ },
48304
+ onExpired() {
48305
+ if (valid())
48306
+ fail(new Error("call connection lost"));
48307
+ },
48308
+ onSpeaking(speaking) {
48309
+ if (valid())
48310
+ emit({ speaking });
48311
+ },
48312
+ onError(error) {
48313
+ if (valid())
48314
+ fail(error);
48315
+ else
48316
+ call.error = error;
48317
+ }
48318
+ });
48319
+ call.room = openCallRoom({
48320
+ cloud,
48321
+ capability,
48322
+ protocol,
48323
+ endpoint: callEndpoint,
48324
+ mls,
48325
+ member,
48326
+ admission,
48327
+ callId: descriptor.callId,
48328
+ onKeys(value) {
48329
+ if (!valid() && !preparing())
48330
+ return;
48331
+ call.media.setKeys(value).catch((error) => {
48332
+ if (valid())
48333
+ fail(error);
48334
+ else
48335
+ call.error = error;
48336
+ });
48337
+ },
48338
+ onParticipants(value) {
48339
+ if (!valid() && !preparing())
48340
+ return;
48341
+ call.participants = value;
48342
+ call.media.setParticipants(value).catch((error) => {
48343
+ if (valid())
48344
+ fail(error);
48345
+ else
48346
+ call.error = error;
48347
+ });
48348
+ if (!valid())
48349
+ return;
48350
+ for (const peer of value) {
48351
+ const preference = state.peerAudio[peer.chatPK];
48352
+ if (preference)
48353
+ call.media.setPeerAudio(peer.holder, { volume: preference.volume / 100, muted: preference.muted }).catch((error) => diag?.("call.peer.audio.error", { code: error?.code || "calls/audio" }));
48354
+ }
48355
+ emit({ participants: value });
48356
+ available();
48357
+ call.refresh?.();
48358
+ if (call.mediaReady)
48359
+ connectionProgress();
48360
+ },
48361
+ onSignal: (value, from) => valid() || preparing() ? call.media.signal(value, from) : undefined,
48362
+ onRemoved() {
48363
+ if (valid())
48364
+ leave();
48365
+ else
48366
+ call.error = cancelled2();
48367
+ },
48368
+ onError(error) {
48369
+ if (valid())
48370
+ fail(error);
48371
+ else
48372
+ call.error = error;
48373
+ }
48374
+ });
48375
+ await applyAudio(call);
48376
+ checkCall();
48377
+ await call.media.setMicrophoneVolume(state.microphoneVolume / 100);
48378
+ checkCall();
48379
+ await call.room.start({ create });
48380
+ checkCall();
48381
+ step("admission");
48382
+ if (create) {
48383
+ try {
48384
+ await entry.mailbox.commit(descriptor, [], { expectedRevision: discoveryRecord.revision });
48385
+ } catch (error) {
48386
+ if (error?.code === "calls/conflict")
48387
+ throw Object.assign(new Error("another call started"), { code: "calls/discovery-conflict" });
48388
+ throw error;
48389
+ }
48390
+ checkCall();
48391
+ if (!continuation)
48392
+ announce(chatId, descriptor.callId, "started");
48393
+ } else {
48394
+ await entry.mailbox.read();
48395
+ checkCall();
48396
+ if (entry.descriptor?.callId !== descriptor.callId) {
48397
+ if (expectedCallId && (onlyExisting || descriptor.mode === "direct"))
48398
+ throw ended();
48399
+ throw Object.assign(new Error("the call room changed"), { code: "calls/discovery-conflict" });
48400
+ }
48401
+ }
48402
+ step("publication");
48403
+ call.published = true;
48404
+ if (!continuation) {
48405
+ await stopCall();
48406
+ checkCall();
48407
+ mountLease(identity, callEndpoint, call.owner);
48408
+ participation = { chatId, callId: descriptor.callId, startedAt: descriptor.startedAt, endpoint: callEndpoint, owner: lease };
48409
+ }
48410
+ step("predecessor");
48411
+ call.owner = participation.owner;
48412
+ active = call;
48413
+ observeRoster(entry);
48414
+ available();
48415
+ emit({
48416
+ phase: continuation ? "connecting" : state.elsewhere ? "transferring" : "joining",
48417
+ chatId,
48418
+ callId: descriptor.callId,
48419
+ mode: descriptor.mode,
48420
+ error: null,
48421
+ speaking: [],
48422
+ participants: call.participants
48423
+ });
48424
+ for (const peer of call.participants) {
48425
+ const preference = state.peerAudio[peer.chatPK];
48426
+ if (preference)
48427
+ call.media.setPeerAudio(peer.holder, { volume: preference.volume / 100, muted: preference.muted }).catch((error) => diag?.("call.peer.audio.error", { code: error?.code || "calls/audio" }));
48428
+ }
48429
+ if (continuation)
48430
+ await participation.owner.rotateMedia();
48431
+ else
48432
+ await lease.acquire({ chatId, callId: descriptor.callId });
48433
+ check();
48434
+ step("ownership");
48435
+ if (!valid())
48436
+ throw cancelled2();
48437
+ connectionProgress();
48438
+ await call.media.open();
48439
+ check();
48440
+ step("media");
48441
+ call.mediaReady = true;
48442
+ connectionProgress();
48443
+ let publishedRoster = "";
48444
+ let refreshing = null;
48445
+ let requested = false;
48446
+ const rosterFingerprint = () => JSON.stringify(call.participants.map(({ holder, muted, deafened }) => ({ holder, muted, deafened })).sort((a, b) => a.holder.localeCompare(b.holder)));
48447
+ const refresh = (heartbeat = false) => {
48448
+ if (!valid() || !heartbeat && publishedRoster === rosterFingerprint())
48449
+ return;
48450
+ requested = true;
48451
+ if (refreshing)
48452
+ return;
48453
+ clearTimeout(call.timer);
48454
+ refreshing = (async () => {
48455
+ while (requested && valid()) {
48456
+ requested = false;
48457
+ const fingerprint = rosterFingerprint();
48458
+ const ordered = call.participants.map((item) => item.holder).sort();
48459
+ if (ordered[0] === callEndpoint.holder) {
48460
+ const snapshot = await entry.mailbox.read();
48461
+ if (valid() && entry.descriptor?.callId === descriptor.callId) {
48462
+ await entry.mailbox.commit({ ...descriptor, participants: call.participants.length }, [], { expectedRevision: snapshot.revision });
48463
+ }
48464
+ }
48465
+ publishedRoster = fingerprint;
48466
+ }
48467
+ })().catch((error) => {
48468
+ if (valid() && error?.code !== "calls/conflict")
48469
+ fail(error);
48470
+ }).finally(() => {
48471
+ refreshing = null;
48472
+ if (valid())
48473
+ call.timer = setTimeout(() => refresh(true), 30000);
48474
+ });
48475
+ };
48476
+ call.refresh = refresh;
48477
+ refresh(true);
48478
+ return chatId;
48479
+ } catch (error) {
48480
+ const cleanup = active === call ? stopCall() : call ? disposeCall(call) : null;
48481
+ if (["calls/room-empty", "calls/discovery-conflict"].includes(error?.code))
48482
+ await cleanup;
48483
+ if (error?.code === "calls/room-empty" && call) {
48484
+ check();
48485
+ if (expectedCallId && (onlyExisting || call.descriptor.mode === "direct"))
48486
+ throw ended();
48487
+ await retireDiscovery(call);
48488
+ check();
48489
+ throw Object.assign(new Error("the call room is restarting"), { code: "calls/discovery-conflict" });
48490
+ }
48491
+ throw error;
48492
+ } finally {
48493
+ preflightActive = false;
48494
+ preparation?.close();
48495
+ if (operation.preparation === preparation)
48496
+ operation.preparation = null;
48497
+ }
48498
+ }
48499
+ async function join(chatId, expectedCallId = null, onlyExisting = false) {
48500
+ if (active?.chatId === chatId) {
48501
+ if (expectedCallId && (onlyExisting || active.descriptor.mode === "direct") && active.callId !== expectedCallId)
48502
+ throw ended();
48503
+ return chatId;
48504
+ }
48505
+ if (!foreground)
48506
+ throw new Error("open veyl to join a call");
48507
+ return runJoin(chatId, expectedCallId, onlyExisting);
48508
+ }
48509
+ async function runJoin(chatId, expectedCallId = null, onlyExisting = false, continuation = null) {
48510
+ const intent = ++joinIntent;
48511
+ const operation = { predecessor: active, chatId, candidate: null, continuation };
48512
+ const previous = joining;
48513
+ joining = operation;
48514
+ previous?.preparation?.close();
48515
+ emit({ joiningChatId: chatId });
48516
+ if (previous?.candidate && previous.candidate !== active)
48517
+ disposeCall(previous.candidate);
48518
+ try {
48519
+ for (let attempt = 0;attempt < 3; attempt += 1) {
48520
+ try {
48521
+ return await joinAttempt(chatId, intent, expectedCallId, onlyExisting, continuation);
48522
+ } catch (error) {
48523
+ if (intent !== joinIntent || !identity || closed)
48524
+ throw cancelled2();
48525
+ if (error?.code !== "calls/discovery-conflict" || attempt === 2) {
48526
+ if (continuation) {
48527
+ fail(error);
48528
+ throw error;
48529
+ }
48530
+ const target = operation.chatId;
48531
+ emit({ ...!active ? { phase: "error", chatId: target } : {}, error: callError(chatId, error) });
48532
+ throw error;
48533
+ }
48534
+ }
48535
+ }
48536
+ } finally {
48537
+ if (joining === operation) {
48538
+ joining = null;
48539
+ emit({ joiningChatId: null });
48540
+ }
48541
+ prune();
48542
+ }
48543
+ }
48544
+ function leave() {
48545
+ joinIntent += 1;
48546
+ joining?.preparation?.close();
48547
+ const candidate = joining?.candidate;
48548
+ joining = null;
48549
+ emit({ joiningChatId: null });
48550
+ const pending = candidate && candidate !== active ? disposeCall(candidate) : null;
48551
+ const stopped = stopCall();
48552
+ return Promise.all([pending, stopped, ...[...retiring].map((call) => call.closing)]).then(() => {
48553
+ return;
48554
+ });
48555
+ }
48556
+ function cancelJoin() {
48557
+ const operation = joining;
48558
+ if (!operation)
48559
+ return Promise.resolve();
48560
+ joinIntent += 1;
48561
+ operation.preparation?.close();
48562
+ joining = null;
48563
+ emit({ joiningChatId: null });
48564
+ const pending = operation.candidate && operation.candidate !== active ? disposeCall(operation.candidate) : null;
48565
+ return Promise.all([pending, active && active === operation.predecessor ? null : stopCall()]).then(() => {
48566
+ return;
48567
+ });
48568
+ }
48569
+ function stopCall() {
48570
+ const call = active;
48571
+ const authority = participation;
48572
+ participation = null;
48573
+ if (!call && authority) {
48574
+ authority.owner.retire();
48575
+ const retired = [...retiring].filter((room) => room.endpoint === authority.endpoint);
48576
+ const released = Promise.all(retired.map((room) => room.stopped)).then(() => authority.owner.release());
48577
+ leaving = retired.length ? Promise.race([released, Promise.all(retired.map((room) => room.closing))]) : released;
48578
+ }
48579
+ if (!call) {
48580
+ emit({ phase: "idle", chatId: null, callId: null, mode: null, transport: null, participants: [], speaking: [] });
48581
+ return leaving;
48582
+ }
48583
+ active = null;
48584
+ leaving = disposeCall(call);
48585
+ if (identity && discoveries.get(call.chatId) === call.discovery)
48586
+ observeRoster(call.discovery);
48587
+ available();
48588
+ emit({ phase: "idle", chatId: null, callId: null, mode: null, transport: null, participants: [], speaking: [] });
48589
+ return leaving;
48590
+ }
48591
+ async function retireDiscovery(call) {
48592
+ const entry = call.discovery;
48593
+ const snapshot = await entry.mailbox.read();
48594
+ if (snapshot.head?.data?.callId !== call.callId && !(snapshot.head === null && entry.expiredCallId === call.callId))
48595
+ return;
48596
+ try {
48597
+ await entry.mailbox.commit(null, [], { expectedRevision: snapshot.revision });
48598
+ if (chat.getSnapshot().getOwnerChat(call.chatId)?.epochState?.manifest.epochId === entry.epochState.manifest.epochId) {
48599
+ announce(call.chatId, call.callId, "ended");
48600
+ }
48601
+ } catch (error) {
48602
+ if (error?.code !== "calls/conflict")
48603
+ throw error;
48604
+ }
48605
+ }
48606
+ function finishDiscovery(entry, descriptor) {
48607
+ if (entry.finishing.has(descriptor.callId))
48608
+ return;
48609
+ const finishing = retireDiscovery({ discovery: entry, callId: descriptor.callId, chatId: entry.epochState.manifest.chatId }).catch((error) => diag?.("call.close.error", { code: error?.code || "calls/discovery" })).finally(() => entry.finishing.delete(descriptor.callId));
48610
+ entry.finishing.set(descriptor.callId, finishing);
48611
+ }
48612
+ function disposeCall(call) {
48613
+ if (call.closing)
48614
+ return call.closing;
48615
+ call.leaving = true;
48616
+ retiring.add(call);
48617
+ clearTimeout(call.timer);
48618
+ clearTimeout(call.joinTimer);
48619
+ const stopping = call.media?.close();
48620
+ call.stopped = Promise.resolve(stopping);
48621
+ const releasedOwner = participation?.endpoint === call.endpoint ? null : call.owner;
48622
+ releasedOwner?.retire();
48623
+ const departing = call.room?.leave();
48624
+ const draining = (async () => {
48625
+ try {
48626
+ const [, departure] = await Promise.allSettled([Promise.resolve(stopping).then(() => releasedOwner?.release()), departing]);
48627
+ if (departure.status === "fulfilled" && departure.value?.ended && call.published && call.discovery.descriptor?.callId === call.callId) {
48628
+ await retireDiscovery(call);
48629
+ }
48630
+ } catch (error) {
48631
+ diag?.("call.close.error", { code: error?.code || "" });
48632
+ }
48633
+ })();
48634
+ let expiry;
48635
+ const expired = new Promise((resolve) => {
48636
+ expiry = setTimeout(() => {
48637
+ diag?.("call.close.error", { code: "calls/retirement-timeout" });
48638
+ resolve();
48639
+ }, CALL_OWNERSHIP_LEASE_MS);
48640
+ });
48641
+ call.closing = Promise.race([draining, expired]).finally(() => {
48642
+ clearTimeout(expiry);
48643
+ call.room?.close();
48644
+ call.protocol?.close();
48645
+ cleanBytes(call.member?.snapshot);
48646
+ if (call.owner !== lease && call.owner !== participation?.owner)
48647
+ call.owner?.close();
48648
+ if (call.endpoint !== leaseEndpoint)
48649
+ call.endpoint.close();
48650
+ retiring.delete(call);
48651
+ if (discoveries.get(call.chatId) !== call.discovery)
48652
+ closeDiscovery(call.discovery);
48653
+ prune();
48654
+ });
48655
+ return call.closing;
48656
+ }
48657
+ function applyAudio(call) {
48658
+ const audio = { muted: state.muted || state.deafened, deafened: state.deafened };
48659
+ const applied = Promise.all([call.media.mute(audio.muted), call.media.deafen(audio.deafened)]);
48660
+ const result = Promise.all([applied, call.muteTail]).then(() => {
48661
+ if (!call.leaving)
48662
+ return call.room.setAudio(audio);
48663
+ });
48664
+ call.muteTail = result.catch(() => {});
48665
+ return result;
48666
+ }
48667
+ async function setAudio(patch) {
48668
+ const value = normalizeCallAudio(patch, state);
48669
+ if (value.muted === state.muted && value.deafened === state.deafened && value.muted === audioSource.muted && value.deafened === audioSource.deafened)
48670
+ return;
48671
+ const token = generation;
48672
+ pendingAudio = value;
48673
+ const saving = Promise.resolve().then(() => {
48674
+ if (generation !== token)
48675
+ throw cancelled2();
48676
+ return saveAudio(value);
48677
+ }).then(() => {
48678
+ if (generation === token)
48679
+ audioSource = value;
48680
+ });
48681
+ try {
48682
+ await Promise.all([saving, applyAudioPreferences(value)]);
48683
+ } finally {
48684
+ if (generation === token && pendingAudio === value)
48685
+ pendingAudio = null;
48686
+ }
48687
+ }
48688
+ async function applyAudioPreferences(patch) {
48689
+ emit(patch);
48690
+ const calls = new Set([active, joining?.candidate].filter((call) => call?.media && call.room && !call.leaving));
48691
+ await Promise.all([...calls].map((call) => applyAudio(call).catch((error) => {
48692
+ if (active === call)
48693
+ fail(error);
48694
+ else if (!call.leaving)
48695
+ call.error = error;
48696
+ throw error;
48697
+ })));
48698
+ }
48699
+ function syncAudio(source) {
48700
+ audioSource = normalizeCallAudio(source);
48701
+ const value = pendingAudio || audioSource;
48702
+ if (state.muted === value.muted && state.deafened === value.deafened)
48703
+ return;
48704
+ applyAudioPreferences(value).catch((error) => diag?.("call.audio.settings.error", { code: error?.code || "calls/audio" }));
48705
+ }
48706
+ async function setPeerAudio(chatPK, patch) {
48707
+ const call = active;
48708
+ if (!identity || typeof chatPK !== "string" || !chatPK || chatPK === identity.chatPK)
48709
+ throw new Error("peer identity required");
48710
+ const token = generation;
48711
+ const peer = call?.participants.find((peer2) => peer2.chatPK === chatPK);
48712
+ const previous = state.peerAudio[chatPK];
48713
+ const value = { volume: 100, muted: false, ...previous, ...patch };
48714
+ pendingPeerAudio.set(chatPK, value);
48715
+ emit({ peerAudio: { ...state.peerAudio, [chatPK]: value } });
48716
+ try {
48717
+ if (peer)
48718
+ await call.media.setPeerAudio(peer.holder, { volume: value.volume / 100, muted: value.muted });
48719
+ if (generation !== token || pendingPeerAudio.get(chatPK) !== value)
48720
+ return;
48721
+ await savePeerAudio(chatPK, value);
48722
+ if (generation === token)
48723
+ confirmedPeerAudio.set(chatPK, value);
48724
+ } catch (error) {
48725
+ if (generation === token && state.peerAudio[chatPK] === value) {
48726
+ const peerAudio = { ...state.peerAudio };
48727
+ const confirmed = confirmedPeerAudio.get(chatPK);
48728
+ if (confirmed)
48729
+ peerAudio[chatPK] = confirmed;
48730
+ else
48731
+ delete peerAudio[chatPK];
48732
+ emit({ peerAudio });
48733
+ if (peer && active === call)
48734
+ call.media.setPeerAudio(peer.holder, {
48735
+ volume: (confirmed?.volume ?? 100) / 100,
48736
+ muted: confirmed?.muted ?? false
48737
+ }).catch(() => {});
48738
+ }
48739
+ throw error;
48740
+ } finally {
48741
+ if (generation === token && pendingPeerAudio.get(chatPK) === value)
48742
+ pendingPeerAudio.delete(chatPK);
48743
+ }
48744
+ }
48745
+ function syncPeerAudio(source = {}) {
48746
+ if (source === peerAudioSource)
48747
+ return;
48748
+ peerAudioSource = source;
48749
+ const peerAudio = { ...source };
48750
+ for (const [chatPK, value] of pendingPeerAudio)
48751
+ peerAudio[chatPK] = value;
48752
+ confirmedPeerAudio.clear();
48753
+ for (const [chatPK, value] of Object.entries(source))
48754
+ confirmedPeerAudio.set(chatPK, value);
48755
+ const changed = new Set([...Object.keys(state.peerAudio), ...Object.keys(peerAudio)].filter((chatPK) => state.peerAudio[chatPK]?.volume !== peerAudio[chatPK]?.volume || state.peerAudio[chatPK]?.muted !== peerAudio[chatPK]?.muted));
48756
+ if (!changed.size)
48757
+ return;
48758
+ emit({ peerAudio });
48759
+ for (const call of new Set([active, joining?.candidate].filter((call2) => call2?.media && !call2.leaving))) {
48760
+ for (const peer of call.participants) {
48761
+ if (!changed.has(peer.chatPK))
48762
+ continue;
48763
+ const audio = peerAudio[peer.chatPK];
48764
+ call.media.setPeerAudio(peer.holder, { volume: (audio?.volume ?? 100) / 100, muted: audio?.muted ?? false }).catch((error) => diag?.("call.peer.audio.error", { code: error?.code || "calls/audio" }));
48765
+ }
48766
+ }
48767
+ }
48768
+ async function setMicrophoneVolume(volume) {
48769
+ if (!Number.isFinite(volume) || volume < 1 || volume > 200)
48770
+ throw new Error("microphone volume must be between 1 and 200");
48771
+ const previous = state.microphoneVolume;
48772
+ const token = generation;
48773
+ emit({ microphoneVolume: volume });
48774
+ const calls = new Set([active, joining?.candidate].filter((call) => call?.media && !call.leaving));
48775
+ try {
48776
+ await Promise.all([...calls].map((call) => call.media.setMicrophoneVolume(volume / 100)));
48777
+ } catch (error) {
48778
+ if (generation === token && state.microphoneVolume === volume)
48779
+ emit({ microphoneVolume: previous });
48780
+ throw error;
48781
+ }
48782
+ }
48783
+ function createLease(session, callEndpoint) {
48784
+ const ownsLease = () => identity === session && leaseEndpoint === callEndpoint;
48785
+ return openAccountCallLease({
48786
+ cloud,
48787
+ capability: session.callIdentity,
48788
+ endpoint: callEndpoint,
48789
+ clock: mediaPort.clock,
48790
+ onRecord(record, descriptor) {
48791
+ if (!ownsLease())
48792
+ return;
48793
+ const holder = (record.pending || record.active)?.holder;
48794
+ const elsewhere = descriptor && holder !== callEndpoint.holder ? descriptor : null;
48795
+ if (elsewhere && state.elsewhere?.chatId === elsewhere.chatId && state.elsewhere.callId === elsewhere.callId)
48796
+ return;
48797
+ emit({ elsewhere });
48798
+ },
48799
+ onGrant: (value) => ownsLease() && active?.endpoint === callEndpoint ? active.media?.grant(value) : undefined,
48800
+ onLost: () => {
48801
+ if (ownsLease())
48802
+ leave();
48803
+ },
48804
+ onError(error) {
48805
+ if (ownsLease() && active)
48806
+ fail(error);
48807
+ }
48808
+ });
48809
+ }
48810
+ function mountLease(session, callEndpoint = endpoint, prepared = null) {
48811
+ lease?.close();
48812
+ if (leaseEndpoint && leaseEndpoint !== endpoint && leaseEndpoint !== callEndpoint)
48813
+ leaseEndpoint.close();
48814
+ leaseEndpoint = callEndpoint;
48815
+ lease = prepared || createLease(session, callEndpoint);
48816
+ }
48817
+ function setSources(session, settings = {}) {
48818
+ const peerAudio = settings.peerAudio || {};
48819
+ if (identity === session) {
48820
+ if (session) {
48821
+ syncPeerAudio(peerAudio);
48822
+ syncAudio(settings.callAudio);
48823
+ }
48824
+ return drainingSources;
48825
+ }
48826
+ const token = ++generation;
48827
+ const previousLease = lease;
48828
+ const previousEndpoint = endpoint;
48829
+ const previousLeaseEndpoint = leaseEndpoint;
48830
+ const previousDiscoveries = [...discoveries.values()];
48831
+ let finishDrain, rejectDrain;
48832
+ const drained = new Promise((resolve, reject) => {
48833
+ finishDrain = resolve;
48834
+ rejectDrain = reject;
48835
+ });
48836
+ const retirement = Promise.all([drainingSources, drained]).then(() => {
48837
+ return;
48838
+ });
48839
+ drainingSources = retirement;
48840
+ lease = null;
48841
+ leaseEndpoint = null;
48842
+ endpoint = null;
48843
+ discoveries.clear();
48844
+ discoveryReads.clear();
48845
+ identity = session;
48846
+ const stopped = leave();
48847
+ Promise.resolve(stopped).finally(() => {
48848
+ previousLease?.close();
48849
+ previousLeaseEndpoint?.close();
48850
+ previousEndpoint?.close();
48851
+ for (const entry of previousDiscoveries)
48852
+ closeDiscovery(entry);
48853
+ }).then(finishDrain, rejectDrain);
48854
+ if (generation !== token || identity !== session)
48855
+ return retirement;
48856
+ confirmedPeerAudio.clear();
48857
+ pendingPeerAudio.clear();
48858
+ pendingAudio = null;
48859
+ audioSource = normalizeCallAudio(settings.callAudio);
48860
+ peerAudioSource = session ? peerAudio : null;
48861
+ for (const [chatPK, value] of Object.entries(peerAudioSource || {}))
48862
+ confirmedPeerAudio.set(chatPK, value);
48863
+ emit({ ...initial(), ...session ? audioSource : {}, peerAudio: peerAudioSource || {} });
48864
+ available();
48865
+ if (generation !== token || identity !== session)
48866
+ return retirement;
48867
+ if (!session || closed || !mediaPort || !mls || !cloud.calls)
48868
+ return retirement;
48869
+ endpoint = createCallEndpoint();
48870
+ mountLease(session);
48871
+ if (online)
48872
+ refreshObserved();
48873
+ return retirement;
48874
+ }
48875
+ function refreshObserved() {
48876
+ for (const chatId of new Set([...observers.values()].map((item) => item.chatId))) {
48877
+ readDiscovery(chatId).catch((error) => diag?.("call.discovery.error", { stage: "prepare", code: error?.code || "calls/admission" }));
48878
+ }
48879
+ }
48880
+ const unsubscribe = chat.subscribe(() => {
48881
+ if (!identity)
48882
+ return;
48883
+ for (const [chatId, entry] of discoveries) {
48884
+ const epochState = chat.getSnapshot().getOwnerChat(chatId)?.epochState;
48885
+ if (epochState?.manifest.epochId === entry.epochState.manifest.epochId)
48886
+ continue;
48887
+ const retained = epochState?.manifest.members.some((member) => member.chatSigningPK === identity.chatSigningPK);
48888
+ if (participation?.chatId === chatId && retained) {
48889
+ const continuation = participation;
48890
+ const intent = ++joinIntent;
48891
+ const previous = active || joining?.candidate;
48892
+ active = null;
48893
+ joining = null;
48894
+ if (previous)
48895
+ disposeCall(previous);
48896
+ const members = new Set(epochState.manifest.members.map((member) => member.chatPK));
48897
+ emit({
48898
+ phase: "connecting",
48899
+ joiningChatId: chatId,
48900
+ speaking: [],
48901
+ participants: state.participants.filter((member) => members.has(member.chatPK))
48902
+ });
48903
+ diag?.("call.epoch.transition", { stage: "starting", participants: state.participants.length });
48904
+ Promise.resolve(previous?.stopped).then(() => {
48905
+ if (intent !== joinIntent || participation !== continuation)
48906
+ return;
48907
+ return runJoin(chatId, null, false, continuation);
48908
+ }).catch((error) => {
48909
+ if (intent === joinIntent && participation === continuation)
48910
+ fail(error);
48911
+ });
48912
+ } else if (active?.chatId === chatId || participation?.chatId === chatId)
48913
+ leave();
48914
+ else if (joining?.chatId === chatId)
48915
+ cancelJoin();
48916
+ closeDiscovery(entry);
48917
+ discoveries.delete(chatId);
48918
+ available();
48919
+ if (online && [...observers.values()].some((item) => item.chatId === chatId))
48920
+ readDiscovery(chatId).catch(() => {});
48921
+ }
48922
+ if (online)
48923
+ for (const chatId of new Set([...observers.values()].map((item) => item.chatId))) {
48924
+ if (!discoveries.has(chatId) && !discoveryReads.has(chatId) && chat.getSnapshot().getOwnerChat(chatId)?.epochState) {
48925
+ readDiscovery(chatId).catch((error) => diag?.("call.discovery.error", { stage: "prepare", code: error?.code || "calls/admission" }));
48926
+ }
48927
+ }
48928
+ });
48929
+ return Object.freeze({
48930
+ getSnapshot: () => state,
48931
+ subscribe(listener) {
48932
+ listeners.add(listener);
48933
+ return () => listeners.delete(listener);
48934
+ },
48935
+ setSources,
48936
+ observe,
48937
+ join,
48938
+ leave,
48939
+ cancelJoin,
48940
+ setMicrophoneVolume,
48941
+ async resolveActiveCall(chatId) {
48942
+ const entry = await readDiscovery(chatId);
48943
+ return entry.descriptor?.callId || null;
48944
+ },
48945
+ joinFromMessage(chatId, callId) {
48946
+ if (!/^[a-f0-9]{64}$/u.test(callId))
48947
+ return Promise.reject(ended());
48948
+ return join(chatId, callId);
48949
+ },
48950
+ joinExisting(chatId, callId) {
48951
+ if (!/^[a-f0-9]{64}$/u.test(callId))
48952
+ return Promise.reject(ended());
48953
+ return join(chatId, callId, true);
48954
+ },
48955
+ setMuted(value) {
48956
+ return setAudio({ muted: value === true });
48957
+ },
48958
+ setDeafened(value) {
48959
+ return setAudio({ deafened: value === true });
48960
+ },
48961
+ setPeerVolume(chatPK, volume) {
48962
+ if (!Number.isFinite(volume) || volume < 1 || volume > 200)
48963
+ return Promise.reject(new Error("peer volume must be between 1 and 200"));
48964
+ return setPeerAudio(chatPK, { volume });
48965
+ },
48966
+ setPeerMuted(chatPK, muted) {
48967
+ return setPeerAudio(chatPK, { muted: muted === true });
48968
+ },
48969
+ setForeground(value) {
48970
+ foreground = value === true;
48971
+ },
48972
+ setOnline(value) {
48973
+ const reconnecting = !online && value === true;
48974
+ online = value === true;
48975
+ if (reconnecting && identity && !closed)
48976
+ refreshObserved();
48977
+ },
48978
+ close() {
48979
+ if (closed)
48980
+ return closingSession;
48981
+ let finish, reject;
48982
+ closingSession = new Promise((resolve, fail2) => {
48983
+ finish = resolve;
48984
+ reject = fail2;
48985
+ });
48986
+ closed = true;
48987
+ const stopped = setSources(null);
48988
+ unsubscribe();
48989
+ listeners.clear();
48990
+ Promise.resolve(stopped).finally(() => Promise.all([mls?.close?.(), mediaPort?.close?.()])).then(finish, reject);
48991
+ return closingSession;
48992
+ }
48993
+ });
48994
+ }
48995
+
45657
48996
  // ../../node_modules/.bun/@zxcvbn-ts+core@4.2.0/node_modules/@zxcvbn-ts/core/dist/utils/helper.mjs
45658
48997
  var extend = (listToExtend, list) => listToExtend.push.apply(listToExtend, list);
45659
48998
  var sorted = (matches) => matches.sort((m1, m2) => m1.i - m2.i || m1.j - m2.j);
@@ -48650,15 +51989,17 @@ var DEFAULT_BITCOIN = Object.freeze({
48650
51989
  ready: false,
48651
51990
  error: null
48652
51991
  });
48653
- function normalizeBitcoinData(data, current = DEFAULT_BITCOIN, info = {}) {
51992
+ function normalizeBitcoinData(data, current = DEFAULT_BITCOIN) {
48654
51993
  if (!data)
48655
51994
  return current;
51995
+ const observedAt = timestampMs(data.priceUpdatedAt, null, { positive: true });
51996
+ const usePrice = hasBitcoinPrice(data.price) && observedAt != null && observedAt >= (current.priceUpdatedAt ?? 0);
48656
51997
  return {
48657
- price: hasBitcoinPrice(data.price) ? data.price : current.price,
48658
- priceUpdatedAt: hasBitcoinPrice(data.price) ? timestampMs(data.updatedAt, Date.now()) : current.priceUpdatedAt,
48659
- priceFromCache: hasBitcoinPrice(data.price) ? info.fromCache === true : current.priceFromCache,
51998
+ price: usePrice ? data.price : current.price,
51999
+ priceUpdatedAt: usePrice ? observedAt : current.priceUpdatedAt,
52000
+ priceFromCache: usePrice ? false : current.priceFromCache,
48660
52001
  block: data.block ?? current?.block ?? null,
48661
- fees: data.fees ?? current?.fees ?? null,
52002
+ fees: data.fees ?? null,
48662
52003
  updatedAt: data.updatedAt ?? current?.updatedAt ?? null,
48663
52004
  ready: true,
48664
52005
  error: null
@@ -48708,20 +52049,15 @@ function createBitcoin({ cloud, priceStorage = null, diag = null }) {
48708
52049
  const priceUpdatedAt = timestampMs(cached.updatedAt, null, { positive: true });
48709
52050
  if (!priceUpdatedAt)
48710
52051
  return;
48711
- if (bitcoin.priceUpdatedAt != null && (!bitcoin.priceFromCache || bitcoin.priceUpdatedAt >= priceUpdatedAt))
52052
+ if (bitcoin.priceUpdatedAt != null && bitcoin.priceUpdatedAt >= priceUpdatedAt)
48712
52053
  return;
48713
52054
  publish({ ...bitcoin, price: cached.price, priceUpdatedAt, priceFromCache: true });
48714
52055
  }).catch((error) => diag?.("bitcoin.price.cache.read.error", { code: error?.code || "" }));
48715
- unsubscribe = cloud.bitcoin.watch((data, info = {}) => {
48716
- if (session !== generation || info.pending)
52056
+ unsubscribe = cloud.bitcoin.watch((data) => {
52057
+ if (session !== generation)
48717
52058
  return;
48718
- const next = normalizeBitcoinData(data, bitcoin, info);
48719
- if (info.fromCache && bitcoin.priceUpdatedAt != null && next.priceUpdatedAt <= bitcoin.priceUpdatedAt) {
48720
- next.price = bitcoin.price;
48721
- next.priceUpdatedAt = bitcoin.priceUpdatedAt;
48722
- next.priceFromCache = bitcoin.priceFromCache;
48723
- }
48724
- if (priceStorage && hasBitcoinPrice(data?.price) && !info.fromCache && (next.price !== bitcoin.price || next.priceUpdatedAt !== bitcoin.priceUpdatedAt || bitcoin.priceFromCache)) {
52059
+ const next = normalizeBitcoinData(data, bitcoin);
52060
+ if (priceStorage && next.priceUpdatedAt != null && !next.priceFromCache && (next.price !== bitcoin.price || next.priceUpdatedAt !== bitcoin.priceUpdatedAt || bitcoin.priceFromCache)) {
48725
52061
  const cached = { price: next.price, updatedAt: next.priceUpdatedAt };
48726
52062
  priceWrite = priceWrite.catch(NOOP3).then(() => priceStorage.write(cached));
48727
52063
  priceWrite.catch((error) => diag?.("bitcoin.price.cache.write.error", { code: error?.code || "" }));
@@ -49657,11 +52993,11 @@ function createPeersApi({ cloud, network, avatarCache = null }) {
49657
52993
  return null;
49658
52994
  }
49659
52995
  }
49660
- async function writeCachedAvatar(uid, version, bytes) {
49661
- if (!uid || version == null || !bytes || typeof avatarCache?.write !== "function")
52996
+ async function writeCachedAvatar(uid, version, bytes2) {
52997
+ if (!uid || version == null || !bytes2 || typeof avatarCache?.write !== "function")
49662
52998
  return null;
49663
52999
  try {
49664
- const cached = await avatarCache.write(uid, { version, bytes });
53000
+ const cached = await avatarCache.write(uid, { version, bytes: bytes2 });
49665
53001
  return typeof cached === "string" && cached ? cached : typeof cached?.url === "string" && cached.url ? cached.url : typeof cached?.source === "string" && cached.source ? cached.source : null;
49666
53002
  } catch {
49667
53003
  return null;
@@ -49675,8 +53011,8 @@ function createPeersApi({ cloud, network, avatarCache = null }) {
49675
53011
  }
49676
53012
  if (avatarVersion != null && typeof cloud.peer.avatar.read === "function") {
49677
53013
  try {
49678
- const bytes = await cloud.peer.avatar.read(uid, { version: avatarVersion });
49679
- const cachedSource = await writeCachedAvatar(uid, avatarVersion, bytes);
53014
+ const bytes2 = await cloud.peer.avatar.read(uid, { version: avatarVersion });
53015
+ const cachedSource = await writeCachedAvatar(uid, avatarVersion, bytes2);
49680
53016
  if (cachedSource) {
49681
53017
  return cachedSource;
49682
53018
  }
@@ -49852,8 +53188,9 @@ function createPeersApi({ cloud, network, avatarCache = null }) {
49852
53188
  async function fetchProfileByUid(uid) {
49853
53189
  if (!uid)
49854
53190
  return null;
49855
- if (profileCache.has(uid))
49856
- return profileCache.get(uid);
53191
+ const cached = profileCache.get(uid);
53192
+ if (isFullProfile(cached))
53193
+ return cached;
49857
53194
  try {
49858
53195
  const record = await cloud.peer.read(uid);
49859
53196
  if (!record)
@@ -49901,11 +53238,11 @@ function createPeersApi({ cloud, network, avatarCache = null }) {
49901
53238
  return null;
49902
53239
  if (field === "walletPK" && walletToUid.has(value)) {
49903
53240
  const uid = walletToUid.get(value);
49904
- return profileCache.get(uid) ?? null;
53241
+ return fetchProfileByUid(uid);
49905
53242
  }
49906
53243
  if (field === "chatPK" && chatToUid.has(value)) {
49907
53244
  const uid = chatToUid.get(value);
49908
- return profileCache.get(uid) ?? null;
53245
+ return fetchProfileByUid(uid);
49909
53246
  }
49910
53247
  try {
49911
53248
  const record = field === "walletPK" ? await cloud.search.peer.byWalletPK(value, { network: walletNetwork }) : field === "chatPK" ? await cloud.search.peer.byChatPK(value) : await cloud.search.peer.byUsername(value);
@@ -55084,12 +58421,12 @@ function createWalletTransferSession({ wallet, walletPK, claimIncomingTransfers,
55084
58421
  reason,
55085
58422
  active: sdkActivityCountRef.current
55086
58423
  });
55087
- let ended = false;
58424
+ let ended2 = false;
55088
58425
  return () => {
55089
- if (ended) {
58426
+ if (ended2) {
55090
58427
  return;
55091
58428
  }
55092
- ended = true;
58429
+ ended2 = true;
55093
58430
  sdkActivityCountRef.current = Math.max(0, sdkActivityCountRef.current - 1);
55094
58431
  sdkActivityQuietUntilRef.current = Math.max(sdkActivityQuietUntilRef.current, Date.now() + SDK_BACKGROUND_QUIET_MS);
55095
58432
  markDiag(diag, "wallet.sdkActivity.done", {
@@ -57304,8 +60641,17 @@ function openAccount(options = {}) {
57304
60641
  getPeerProfile: getChatPeerProfile,
57305
60642
  refreshPeerProfile: refreshChatPeerProfile,
57306
60643
  resolvePeerProfile: resolveChatPeerProfile,
57307
- maintenance: messageMaintenance
60644
+ maintenance: messageMaintenance,
60645
+ resolveActiveCall: (chatId) => calls.resolveActiveCall(chatId)
57308
60646
  }, chatSources());
60647
+ const calls = createCallSession({
60648
+ cloud,
60649
+ chat,
60650
+ ...options.calls,
60651
+ diag,
60652
+ savePeerAudio: (chatPK, audio) => user.getSnapshot().updateSettings({ peerAudio: { [chatPK]: audio } }),
60653
+ saveAudio: (callAudio) => user.getSnapshot().updateSettings({ callAudio })
60654
+ });
57309
60655
  const wallet = openWallet({
57310
60656
  ...options.wallet || {},
57311
60657
  network: state.network,
@@ -57378,6 +60724,8 @@ function openAccount(options = {}) {
57378
60724
  syncWallet();
57379
60725
  syncPresence();
57380
60726
  syncPeers();
60727
+ calls.setOnline(state.online);
60728
+ calls.setSources(state.lockState === "unlocked" ? state.session : null, state.user?.settings);
57381
60729
  }
57382
60730
  function syncPresence() {
57383
60731
  const session = state.session;
@@ -57413,7 +60761,7 @@ function openAccount(options = {}) {
57413
60761
  connected: !closed && state.internetAvailable !== false && state.cloudAvailable !== false && state.user.authSessionReady && state.user.authSessionActive && !state.user.authSessionError && state.user.profileReady && !state.user.profileFromCache && !state.user.profileError
57414
60762
  }),
57415
60763
  preview: (value) => user.previewAvatar(value),
57416
- write: (uid, bytes) => cloud.user.profile.avatar.set(uid, bytes),
60764
+ write: (uid, bytes2) => cloud.user.profile.avatar.set(uid, bytes2),
57417
60765
  commit: (change) => user.confirmAvatar(change),
57418
60766
  onError: (error) => diag?.("account.avatar.update.error", { code: error?.code || "" })
57419
60767
  });
@@ -57427,11 +60775,11 @@ function openAccount(options = {}) {
57427
60775
  await cloud.user.username.get(username);
57428
60776
  return { username };
57429
60777
  },
57430
- async setAvatar(bytes) {
60778
+ async setAvatar(bytes2) {
57431
60779
  profileUid();
57432
- if (!bytes)
60780
+ if (!bytes2)
57433
60781
  throw new Error("avatar data required");
57434
- return avatarUpdate.set(sanitizeAvatar(bytes));
60782
+ return avatarUpdate.set(sanitizeAvatar(bytes2));
57435
60783
  },
57436
60784
  async clearAvatar() {
57437
60785
  profileUid();
@@ -57562,13 +60910,32 @@ function openAccount(options = {}) {
57562
60910
  return;
57563
60911
  foreground = value === true;
57564
60912
  syncPresence();
60913
+ calls.setForeground(foreground);
57565
60914
  }
57566
60915
  function closeSession({ notify: notify2 = true } = {}) {
57567
- avatarUpdate.reset();
57568
- attempt += 1;
57569
- chat.setInboxEnabled(false);
57570
60916
  const session = state.session;
60917
+ attempt += 1;
57571
60918
  chatSession = null;
60919
+ const patch = {
60920
+ session: null,
60921
+ wallet: null,
60922
+ walletError: null,
60923
+ lockState: "locked",
60924
+ online: false,
60925
+ onlineError: null,
60926
+ agreementSession: null
60927
+ };
60928
+ state = { ...state, ...patch };
60929
+ const callRetirement = calls.setSources(null);
60930
+ if (callRetirement) {
60931
+ const work = Promise.resolve(callRetirement).catch((error) => {
60932
+ diag?.("account.calls.close.error", { code: error?.code || "" });
60933
+ });
60934
+ sessionCloseWork.add(work);
60935
+ work.finally(() => sessionCloseWork.delete(work));
60936
+ }
60937
+ avatarUpdate.reset();
60938
+ chat.setInboxEnabled(false);
57572
60939
  closeAccountSession(session);
57573
60940
  if (session && typeof options.onSessionClosed === "function") {
57574
60941
  let result;
@@ -57588,19 +60955,9 @@ function openAccount(options = {}) {
57588
60955
  }
57589
60956
  state.user.lockSettings?.();
57590
60957
  peers.setSources({});
57591
- const patch = {
57592
- session: null,
57593
- wallet: null,
57594
- walletError: null,
57595
- lockState: "locked",
57596
- online: false,
57597
- onlineError: null,
57598
- agreementSession: null
57599
- };
57600
60958
  if (notify2) {
57601
- emitDomains(patch);
60959
+ emitDomains({});
57602
60960
  } else {
57603
- state = { ...state, ...patch };
57604
60961
  syncDomains();
57605
60962
  }
57606
60963
  return session;
@@ -58162,12 +61519,14 @@ function openAccount(options = {}) {
58162
61519
  stopPeerAdmissionProjection();
58163
61520
  stopPresence();
58164
61521
  presence.close();
61522
+ const callClosing = calls.close();
58165
61523
  clearMissingPeers();
58166
61524
  wallet.close();
58167
61525
  chat.close();
58168
61526
  peers.close();
58169
61527
  listeners.clear();
58170
61528
  operationBarrier.finishClose();
61529
+ return callClosing;
58171
61530
  });
58172
61531
  return closePromise;
58173
61532
  }
@@ -58183,6 +61542,7 @@ function openAccount(options = {}) {
58183
61542
  messageMaintenance,
58184
61543
  peers,
58185
61544
  presence,
61545
+ calls,
58186
61546
  closeBarrier: Object.freeze({
58187
61547
  acquire: operationBarrier.acquireClose,
58188
61548
  release: operationBarrier.releaseClose,