@glyphteck/veyl 0.72.0 → 0.73.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;
@@ -16828,6 +16819,340 @@ 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;
16830
16821
 
16822
+ // ../../core/calls/wire.js
16823
+ var CALL_REQUEST_MAX_BYTES = 192 * 1024;
16824
+ var CALL_HEAD_MAX_BYTES = 64 * 1024;
16825
+ var CALL_HEAD_PROOF_MAX_BYTES = 2048;
16826
+ var CALL_EVENT_MAX_BYTES = 48 * 1024;
16827
+ var CALL_LOG_MAX_BYTES = 512 * 1024;
16828
+ var CALL_LOG_MAX_EVENTS = 128;
16829
+ 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;
16830
+ var CALL_SCOPE_KINDS = ["ownership", "discovery", "signaling"];
16831
+ var SCOPE_SOCKETS = Object.freeze({ ownership: 8, discovery: CHAT_MAX_MEMBERS * 4, signaling: 32 });
16832
+ var SCOPE_LIMITS = Object.freeze(Object.fromEntries(Object.entries(SCOPE_SOCKETS).map(([kind, sockets]) => [
16833
+ kind,
16834
+ Object.freeze({ sockets, readsPerMinute: (kind === "signaling" ? SCOPE_SOCKETS.discovery : sockets) * 4, writesPerMinute: 600 })
16835
+ ])));
16836
+ var HEX_KEY = /^[a-f0-9]{64}$/u;
16837
+ function callWireError(code = "invalid-request") {
16838
+ const error = new Error(`call ${code}`);
16839
+ error.code = `calls/${code}`;
16840
+ return error;
16841
+ }
16842
+ function encodeCallBytes(value) {
16843
+ if (!(value instanceof Uint8Array))
16844
+ throw callWireError();
16845
+ let text = "";
16846
+ for (const byte of value)
16847
+ text += String.fromCharCode(byte);
16848
+ return btoa(text);
16849
+ }
16850
+ function callHeadDigest(value) {
16851
+ if (!(value instanceof Uint8Array) || !value.length || value.length > CALL_HEAD_MAX_BYTES)
16852
+ throw callWireError();
16853
+ return toHex(sha256(value));
16854
+ }
16855
+ function decodeCallBytes(value, maximum = CALL_HEAD_MAX_BYTES) {
16856
+ 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))
16857
+ throw callWireError();
16858
+ const bytes = Uint8Array.from(atob(value), (char) => char.charCodeAt(0));
16859
+ if (bytes.length > maximum || encodeCallBytes(bytes) !== value)
16860
+ throw callWireError();
16861
+ return bytes;
16862
+ }
16863
+ function callScope(realm, kind, publicKey) {
16864
+ if (!["prod", "dev"].includes(realm) || !CALL_SCOPE_KINDS.includes(kind) || !HEX_KEY.test(publicKey))
16865
+ throw callWireError();
16866
+ return toHex(sha256(canonicalBytes(["veyl.call.scope.v1", realm, kind, publicKey])));
16867
+ }
16868
+ function decodeCallRecord(record) {
16869
+ return {
16870
+ ...record,
16871
+ active: record.active ? { ...record.active, payload: decodeCallBytes(record.active.payload, 4096) } : null,
16872
+ pending: record.pending ? { ...record.pending, payload: decodeCallBytes(record.pending.payload, 4096) } : null
16873
+ };
16874
+ }
16875
+
16876
+ // ../../core/calls/identity.js
16877
+ var REALMS = new Set(["prod", "dev"]);
16878
+ var KINDS = new Set(["ownership", "discovery", "signaling"]);
16879
+ var MAX_SEALED_BYTES = 64 * 1024;
16880
+ function createCallCapability(seed, realm, kind, parts = []) {
16881
+ if (!REALMS.has(realm) || !KINDS.has(kind))
16882
+ throw new Error("invalid call capability");
16883
+ const signing = deriveKey(toBytes32(seed, "call root"), "veyl.call.capability.signing.v1", [realm, kind, ...parts]);
16884
+ const sealing = deriveKey(toBytes32(seed, "call root"), "veyl.call.capability.sealing.v1", [realm, kind, ...parts]);
16885
+ const publicKey = toHex(ed25519.getPublicKey(signing));
16886
+ const scope = callScope(realm, kind, publicKey);
16887
+ const aad = canonicalBytes(["veyl.call.sealed.v1", realm, kind, scope]);
16888
+ let owners = 0;
16889
+ function retain() {
16890
+ owners += 1;
16891
+ let closed = false;
16892
+ function current() {
16893
+ if (closed)
16894
+ throw new Error("call capability closed");
16895
+ }
16896
+ return Object.freeze({
16897
+ realm,
16898
+ kind,
16899
+ scope,
16900
+ publicKey,
16901
+ sign(bytes) {
16902
+ current();
16903
+ return toHex(ed25519.sign(toBytes(bytes), signing));
16904
+ },
16905
+ async seal(value) {
16906
+ current();
16907
+ const plain = canonicalBytes(value, "call payload");
16908
+ if (plain.length + 28 > MAX_SEALED_BYTES)
16909
+ throw new Error("call payload too large");
16910
+ try {
16911
+ const { iv, ct } = await sealAes(sealing, plain, aad);
16912
+ current();
16913
+ const result = new Uint8Array(iv.length + ct.length);
16914
+ result.set(iv);
16915
+ result.set(ct, iv.length);
16916
+ return result;
16917
+ } finally {
16918
+ cleanBytes(plain);
16919
+ }
16920
+ },
16921
+ async open(value) {
16922
+ current();
16923
+ const bytes = toBytes(value);
16924
+ if (bytes.length < 28 || bytes.length > MAX_SEALED_BYTES)
16925
+ throw new Error("invalid call payload");
16926
+ const plain = await openAes(sealing, bytes.subarray(0, 12), bytes.subarray(12), aad);
16927
+ try {
16928
+ current();
16929
+ return JSON.parse(decoder.decode(plain));
16930
+ } finally {
16931
+ cleanBytes(plain);
16932
+ }
16933
+ },
16934
+ retain() {
16935
+ current();
16936
+ return retain();
16937
+ },
16938
+ close() {
16939
+ if (closed)
16940
+ return;
16941
+ closed = true;
16942
+ if (--owners === 0)
16943
+ cleanBytes(signing, sealing);
16944
+ }
16945
+ });
16946
+ }
16947
+ return retain();
16948
+ }
16949
+ function createCallIdentity(masterSeed, realm) {
16950
+ const root = deriveKey(toBytes32(masterSeed, "vault root"), "veyl.call.account.v1", [realm]);
16951
+ try {
16952
+ return createCallCapability(root, realm, "ownership");
16953
+ } finally {
16954
+ cleanBytes(root);
16955
+ }
16956
+ }
16957
+ function createCallEndpoint() {
16958
+ const secret = randomBytes3(32);
16959
+ const publicKey = toHex(ed25519.getPublicKey(secret));
16960
+ let closed = false;
16961
+ return Object.freeze({
16962
+ publicKey,
16963
+ holder: toHex(randomBytes3(16)),
16964
+ sign(bytes) {
16965
+ if (closed)
16966
+ throw new Error("call endpoint closed");
16967
+ return toHex(ed25519.sign(toBytes(bytes), secret));
16968
+ },
16969
+ close() {
16970
+ closed = true;
16971
+ cleanBytes(secret);
16972
+ }
16973
+ });
16974
+ }
16975
+ function createCallDiscovery(epochState, realm) {
16976
+ const manifest = epochState?.manifest;
16977
+ if (!manifest?.chatId || !manifest?.epochId || !epochState?.epochSecret)
16978
+ throw new Error("current chat epoch required");
16979
+ return createCallCapability(toBytes32(epochState.epochSecret), realm, "discovery", [manifest.chatId, manifest.epochId]);
16980
+ }
16981
+
16982
+ // ../../core/utils/text.js
16983
+ function cleanText(value) {
16984
+ return typeof value === "string" ? value.trim() : "";
16985
+ }
16986
+ function lowerText(value) {
16987
+ return cleanText(value).toLowerCase();
16988
+ }
16989
+ function sameText(left, right) {
16990
+ return lowerText(left) === lowerText(right);
16991
+ }
16992
+
16993
+ // ../../core/utils/time.js
16994
+ function timestampMs(value, fallback = null, options = {}) {
16995
+ let ms = null;
16996
+ if (typeof value?.toMillis === "function") {
16997
+ ms = value.toMillis();
16998
+ } else if (value instanceof Date) {
16999
+ ms = value.getTime();
17000
+ } else if (typeof value?.seconds === "number") {
17001
+ ms = value.seconds * 1000 + Math.floor((value.nanoseconds || 0) / 1e6);
17002
+ } else if (typeof value?._seconds === "number") {
17003
+ ms = value._seconds * 1000 + Math.floor((value._nanoseconds || 0) / 1e6);
17004
+ } else if (Number.isFinite(value)) {
17005
+ ms = value;
17006
+ } else if (options.parseString && typeof value === "string") {
17007
+ const numberMs = Number(value);
17008
+ ms = Number.isFinite(numberMs) ? numberMs : Date.parse(value);
17009
+ }
17010
+ if (!Number.isFinite(ms) || options.positive && ms <= 0) {
17011
+ return fallback;
17012
+ }
17013
+ return ms;
17014
+ }
17015
+ function timestampKey(value) {
17016
+ if (value == null) {
17017
+ return null;
17018
+ }
17019
+ return timestampMs(value, null) ?? String(value);
17020
+ }
17021
+ function makeTimestamp(ms) {
17022
+ return {
17023
+ toMillis() {
17024
+ return ms;
17025
+ },
17026
+ toDate() {
17027
+ return new Date(ms);
17028
+ }
17029
+ };
17030
+ }
17031
+ function twoDigits(value) {
17032
+ return String(value).padStart(2, "0");
17033
+ }
17034
+ function dayKey(date) {
17035
+ return `${date.getFullYear()}-${twoDigits(date.getMonth() + 1)}-${twoDigits(date.getDate())}`;
17036
+ }
17037
+ function localDayKey(value) {
17038
+ const ms = timestampMs(value, null, { parseString: true });
17039
+ if (!Number.isFinite(ms))
17040
+ return "";
17041
+ return dayKey(new Date(ms));
17042
+ }
17043
+ function hourKey(dateOrHour) {
17044
+ const hour = dateOrHour instanceof Date ? dateOrHour.getHours() : dateOrHour;
17045
+ return twoDigits(hour);
17046
+ }
17047
+ function dayHourKey(date) {
17048
+ return `${dayKey(date)}-${hourKey(date)}`;
17049
+ }
17050
+ var MINUTE_MS2 = 60000;
17051
+ var HOUR_MS2 = 60 * MINUTE_MS2;
17052
+ function nextLocalDayStartMs(ms) {
17053
+ const date = new Date(ms);
17054
+ date.setDate(date.getDate() + 1);
17055
+ date.setHours(0, 0, 0, 0);
17056
+ return date.getTime();
17057
+ }
17058
+ function nextRowDateTimeRefreshMs(value, now = Date.now()) {
17059
+ const ms = timestampMs(value, null, { parseString: true });
17060
+ const nowMs2 = timestampMs(now, Date.now(), { parseString: true });
17061
+ if (!Number.isFinite(ms) || !Number.isFinite(nowMs2))
17062
+ return null;
17063
+ const age = nowMs2 - ms;
17064
+ if (age < -MINUTE_MS2)
17065
+ return ms - MINUTE_MS2;
17066
+ if (age < MINUTE_MS2)
17067
+ return ms + MINUTE_MS2;
17068
+ if (age < HOUR_MS2)
17069
+ return ms + (Math.floor(age / MINUTE_MS2) + 1) * MINUTE_MS2;
17070
+ if (localDayKey(ms) === localDayKey(nowMs2))
17071
+ return nextLocalDayStartMs(nowMs2);
17072
+ return null;
17073
+ }
17074
+
17075
+ // ../../core/chat/state.js
17076
+ function makeCid() {
17077
+ return `${Date.now().toString(36)}${toHex(randomBytes3(3))}`;
17078
+ }
17079
+ function getMessageKey(message) {
17080
+ return message?.cid || message?.id || null;
17081
+ }
17082
+ function getCidMs(cid) {
17083
+ if (typeof cid !== "string" || !/^[0-9a-z]+[0-9a-f]{6}$/u.test(cid)) {
17084
+ return null;
17085
+ }
17086
+ const base = cid.slice(0, -6);
17087
+ const ms = Number.parseInt(base, 36);
17088
+ return Number.isSafeInteger(ms) && ms > 0 ? ms : null;
17089
+ }
17090
+ function getMessageOrderMs(message) {
17091
+ return getCidMs(message?.cid) ?? timestampMs(message?.ts, Infinity);
17092
+ }
17093
+ function sortMessages(messages) {
17094
+ return [...messages].sort((a, b) => {
17095
+ const aMs = getMessageOrderMs(a);
17096
+ const bMs = getMessageOrderMs(b);
17097
+ if (aMs !== bMs) {
17098
+ return aMs - bMs;
17099
+ }
17100
+ return String(a?.id || "").localeCompare(String(b?.id || ""));
17101
+ });
17102
+ }
17103
+ function mergeMessages(...groups) {
17104
+ const merged = new Map;
17105
+ for (const group of groups) {
17106
+ for (const message of group || []) {
17107
+ const key = getMessageKey(message);
17108
+ if (!key) {
17109
+ continue;
17110
+ }
17111
+ merged.set(key, message);
17112
+ }
17113
+ }
17114
+ return sortMessages([...merged.values()]);
17115
+ }
17116
+
17117
+ // ../../core/chat/messages/call.js
17118
+ var CALL_MSG_TYPE = "call";
17119
+ function readCallMessage(message) {
17120
+ if (message?.t !== CALL_MSG_TYPE || typeof message.callId !== "string" || !/^[0-9a-f]{64}$/u.test(message.callId) || !["started", "ended"].includes(message.event))
17121
+ return null;
17122
+ return { callId: message.callId, event: message.event };
17123
+ }
17124
+ function makeCallMessage(callId, event) {
17125
+ const message = { t: CALL_MSG_TYPE, callId, event };
17126
+ if (!readCallMessage(message))
17127
+ throw new Error("invalid call message");
17128
+ return message;
17129
+ }
17130
+ function callMessageText(message) {
17131
+ const call = readCallMessage(message);
17132
+ return call ? `call ${call.event}` : "";
17133
+ }
17134
+ function latestCallStartKey(messages) {
17135
+ for (let index = messages.length - 1;index >= 0; index -= 1) {
17136
+ if (readCallMessage(messages[index])?.event === "started")
17137
+ return getMessageKey(messages[index]);
17138
+ }
17139
+ return "";
17140
+ }
17141
+
17142
+ // ../../core/chat/messages/types.js
17143
+ var ATTACHMENT_MSG_TYPES = ["img", "gif", "m4a", "mp4", "file"];
17144
+ var MAX_TXT_CHARS = CHAT_MAX_TEXT_CHARS;
17145
+ var REACTION_MSG_TYPE = "rxn";
17146
+ var DELETE_MSG_TYPE = "del";
17147
+ var SYSTEM_MSG_TYPE = "sys";
17148
+ var EPOCH_TRANSITION_MSG_TYPE = "epoch";
17149
+ var EPOCH_PROPOSAL_MSG_TYPE = "epoch_proposal";
17150
+ var SYSTEM_SETTINGS_KIND = "settings";
17151
+ var DEFAULT_REACTION_EMOJI = "❤️";
17152
+ var MAX_REACTIONS = CHAT_MAX_REACTIONS;
17153
+ var HOLD_VISIBLE_KEY = "__holdVisible";
17154
+ var SOURCE_GONE_VISIBLE_KEY = "__sourceGoneVisible";
17155
+
16831
17156
  // ../../core/notifications.js
16832
17157
  "use client";
16833
17158
  var NOTIFICATION_DESCRIPTOR_VERSION = 1;
@@ -16843,10 +17168,47 @@ var CHAT_ATTENTION_KINDS = Object.freeze({
16843
17168
  ROTATION: "rotation",
16844
17169
  SILENT: "silent"
16845
17170
  });
17171
+ var NOTIFICATION_PRESENTATION_KINDS = Object.freeze({
17172
+ MESSAGE: 0,
17173
+ REACTION: 1,
17174
+ ACTIVITY: 2,
17175
+ PHOTO: 3,
17176
+ VIDEO: 4,
17177
+ AUDIO: 5,
17178
+ FILE: 6,
17179
+ CALL_STARTED: 7,
17180
+ CALL_ENDED: 8
17181
+ });
17182
+ function notificationMessageKind(message) {
17183
+ switch (message?.t) {
17184
+ case SYSTEM_MSG_TYPE:
17185
+ case EPOCH_TRANSITION_MSG_TYPE:
17186
+ return NOTIFICATION_PRESENTATION_KINDS.ACTIVITY;
17187
+ case "img":
17188
+ case "gif":
17189
+ return NOTIFICATION_PRESENTATION_KINDS.PHOTO;
17190
+ case "mp4":
17191
+ return NOTIFICATION_PRESENTATION_KINDS.VIDEO;
17192
+ case "m4a":
17193
+ return NOTIFICATION_PRESENTATION_KINDS.AUDIO;
17194
+ case "file":
17195
+ return NOTIFICATION_PRESENTATION_KINDS.FILE;
17196
+ case "call": {
17197
+ const call = readCallMessage(message);
17198
+ return !call ? NOTIFICATION_PRESENTATION_KINDS.ACTIVITY : call.event === "started" ? NOTIFICATION_PRESENTATION_KINDS.CALL_STARTED : NOTIFICATION_PRESENTATION_KINDS.CALL_ENDED;
17199
+ }
17200
+ default:
17201
+ return NOTIFICATION_PRESENTATION_KINDS.MESSAGE;
17202
+ }
17203
+ }
17204
+ function notificationMessageIsSilent(message) {
17205
+ return message?.t === SYSTEM_MSG_TYPE && message.sys === SYSTEM_SETTINGS_KIND || readCallMessage(message)?.event === "ended";
17206
+ }
16846
17207
  var HEX_32_RE = /^[0-9a-f]{64}$/u;
16847
17208
  var PEER_TAG_RE = /^[0-9a-f]{32}$/u;
16848
17209
  var ATTENTION_KINDS = new Set(Object.values(CHAT_ATTENTION_KINDS));
16849
17210
  var NOTIFICATION_MODES = new Set(Object.values(CHAT_NOTIFICATION_MODES));
17211
+ var PRESENTATION_KINDS = new Set(Object.values(NOTIFICATION_PRESENTATION_KINDS));
16850
17212
  function hasCurrentChatAttentionRegistration(value) {
16851
17213
  return value?.deliveryRegistered === true && value?.attentionRegistrationVersion === CHAT_ATTENTION_REGISTRATION_VERSION;
16852
17214
  }
@@ -17002,13 +17364,30 @@ function notificationRouteTag(envelope) {
17002
17364
  cleanBytes(input);
17003
17365
  }
17004
17366
  }
17005
- async function sealNotificationDescriptor(recipientNotificationPK, descriptor = {}) {
17006
- const recipientPK = cleanHex32(recipientNotificationPK, "recipient notification key");
17367
+ function notificationDescriptorClaims(descriptor) {
17007
17368
  const peerTag = cleanText(descriptor.peerTag).toLowerCase();
17008
17369
  const eventId = cleanText(descriptor.eventId);
17009
- if (!PEER_TAG_RE.test(peerTag) || !eventId || eventId.length > 256) {
17370
+ const senderChatPK = cleanHex32(descriptor.senderChatPK, "notification sender key");
17371
+ const kind = descriptor.kind;
17372
+ if (!PEER_TAG_RE.test(peerTag) || !eventId || eventId.length > 256 || !PRESENTATION_KINDS.has(kind)) {
17010
17373
  throw new Error("notification descriptor required");
17011
17374
  }
17375
+ return { v: NOTIFICATION_DESCRIPTOR_VERSION, peerTag, eventId, senderChatPK, kind };
17376
+ }
17377
+ function notificationDescriptorSignatureInput(recipientPK, descriptor) {
17378
+ return encoder.encode([
17379
+ "veyl-notification-presentation-v1",
17380
+ recipientPK,
17381
+ descriptor.peerTag,
17382
+ descriptor.senderChatPK,
17383
+ descriptor.kind,
17384
+ descriptor.eventId
17385
+ ].join("\x00"));
17386
+ }
17387
+ async function sealNotificationDescriptor(recipientNotificationPK, descriptor, sender) {
17388
+ const recipientPK = cleanHex32(recipientNotificationPK, "recipient notification key");
17389
+ const claims = notificationDescriptorClaims({ ...descriptor, senderChatPK: sender?.chatPK });
17390
+ const signature = signChatBytes(sender?.signingKey, notificationDescriptorSignatureInput(recipientPK, claims));
17012
17391
  let eph = null;
17013
17392
  let shared = null;
17014
17393
  let key = null;
@@ -17019,9 +17398,8 @@ async function sealNotificationDescriptor(recipientNotificationPK, descriptor =
17019
17398
  const epk = toHex(eph.pub);
17020
17399
  key = deriveKey(shared.subarray(0, 32), "notification-descriptor-v1", [epk, recipientPK]);
17021
17400
  plaintext = encoder.encode(JSON.stringify({
17022
- v: NOTIFICATION_DESCRIPTOR_VERSION,
17023
- peerTag,
17024
- eventId
17401
+ ...claims,
17402
+ signature
17025
17403
  }));
17026
17404
  const { iv, ct } = await sealAes(key, plaintext, descriptorAad(epk, recipientPK));
17027
17405
  return {
@@ -18060,7 +18438,7 @@ function packRegistryData(registry) {
18060
18438
  }
18061
18439
  return concatBytes4(new Uint8Array([SECRET_REGISTRY_ENVELOPE_VERSION]), registry.iv, registry.ct);
18062
18440
  }
18063
- var packSeedData = ({ crypto = VAULT_CRYPTO, salt, iv, ciphertext, ct, registry, kdf = VAULT_KDF }) => {
18441
+ var packSeedData = ({ crypto: crypto2 = VAULT_CRYPTO, salt, iv, ciphertext, ct, registry, kdf = VAULT_KDF }) => {
18064
18442
  const body = ciphertext || ct;
18065
18443
  if (!salt || !iv || !body || !registry) {
18066
18444
  throw new Error("seed data missing");
@@ -18068,7 +18446,7 @@ var packSeedData = ({ crypto = VAULT_CRYPTO, salt, iv, ciphertext, ct, registry,
18068
18446
  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
18447
  throw new Error("invalid seed data");
18070
18448
  }
18071
- const magic = encoder.encode(crypto);
18449
+ const magic = encoder.encode(crypto2);
18072
18450
  const header = new Uint8Array(1 + magic.length + 8);
18073
18451
  header[0] = magic.length;
18074
18452
  header.set(magic, 1);
@@ -18532,7 +18910,7 @@ function walletPubkey(value) {
18532
18910
  }
18533
18911
  return key;
18534
18912
  }
18535
- function closeSessionResources({ wallet, chatPK, chatPrivateKey, chatSigningSecret, notificationPrivateKey, notificationPublicKey, localCache, vaultAccess, vaultSigner } = {}) {
18913
+ function closeSessionResources({ wallet, chatPK, chatPrivateKey, chatSigningSecret, notificationPrivateKey, notificationPublicKey, localCache, vaultAccess, vaultSigner, callIdentity } = {}) {
18536
18914
  const pending = [];
18537
18915
  const close = (owner) => {
18538
18916
  try {
@@ -18543,6 +18921,7 @@ function closeSessionResources({ wallet, chatPK, chatPrivateKey, chatSigningSecr
18543
18921
  };
18544
18922
  close(vaultAccess);
18545
18923
  close(vaultSigner);
18924
+ close(callIdentity);
18546
18925
  close(localCache);
18547
18926
  lockWallet(wallet);
18548
18927
  lockChat(chatPrivateKey, chatPK);
@@ -18598,6 +18977,7 @@ function createAccountSession(resources = {}) {
18598
18977
  session.localCache = null;
18599
18978
  session.vaultAccess = null;
18600
18979
  session.vaultSigner = null;
18980
+ session.callIdentity = null;
18601
18981
  closePromise = Promise.allSettled(pending).then(() => {
18602
18982
  return;
18603
18983
  });
@@ -18737,6 +19117,7 @@ async function openVaultAccountSession(vault, password, options = {}) {
18737
19117
  let cacheKey = null;
18738
19118
  let settingsKey = null;
18739
19119
  let vaultSigner = null;
19120
+ let callIdentity = null;
18740
19121
  let vaultAccess = null;
18741
19122
  let walletBoot = null;
18742
19123
  let network;
@@ -18777,6 +19158,7 @@ async function openVaultAccountSession(vault, password, options = {}) {
18777
19158
  notificationPK = toHex(notificationPublicKey);
18778
19159
  cacheKey = getCacheSeed(registry);
18779
19160
  vaultSigner = createVaultSigner(masterSeed);
19161
+ callIdentity = createCallIdentity(masterSeed, cloud.environment);
18780
19162
  settingsKey = deriveSettingsKey(cacheKey, uid);
18781
19163
  mark(diag, "vault.unlock.derive.done", { elapsedMs: Date.now() - deriveStartedAt, source });
18782
19164
  cleanBytes(masterSeed);
@@ -18825,6 +19207,7 @@ async function openVaultAccountSession(vault, password, options = {}) {
18825
19207
  notificationPublicKey,
18826
19208
  localCache,
18827
19209
  vaultSigner,
19210
+ callIdentity,
18828
19211
  vaultAccess,
18829
19212
  vaultPK: vaultSigner.publicKey
18830
19213
  });
@@ -18925,7 +19308,7 @@ async function openVaultAccountSession(vault, password, options = {}) {
18925
19308
  return session;
18926
19309
  } catch (error) {
18927
19310
  walletBoot?.close();
18928
- await closeSessionResources({ wallet, chatPK, chatPrivateKey, chatSigningSecret, notificationPrivateKey, notificationPublicKey, localCache, vaultAccess, vaultSigner });
19311
+ await closeSessionResources({ wallet, chatPK, chatPrivateKey, chatSigningSecret, notificationPrivateKey, notificationPublicKey, localCache, vaultAccess, vaultSigner, callIdentity });
18929
19312
  throw error;
18930
19313
  } finally {
18931
19314
  cleanBytes(masterSeed, walletEntropy, chatSeed, cacheKey, settingsKey);
@@ -18962,88 +19345,6 @@ function hasAgreement(user, contract = CURRENT_AGREEMENT) {
18962
19345
  return isAgreementAccepted(user?.agreement, contract);
18963
19346
  }
18964
19347
 
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
19348
  // ../../core/moderation.js
19048
19349
  function banUntilMs(ban) {
19049
19350
  if (!ban || typeof ban !== "object" || Array.isArray(ban) || ban.until == null) {
@@ -20954,48 +21255,6 @@ function readAccountBootstrap(value, uid) {
20954
21255
  };
20955
21256
  }
20956
21257
 
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
21258
  // ../../core/chat/ids.js
21000
21259
  function isChatMessageForParticipants(message, chatPK, peerChatPK, memberChatPKs = null) {
21001
21260
  const members = Array.isArray(memberChatPKs) ? new Set(memberChatPKs.filter(Boolean)) : null;
@@ -21381,16 +21640,16 @@ function normalizeOpenedAction(epoch, head, action) {
21381
21640
  };
21382
21641
  }
21383
21642
  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);
21643
+ const crypto2 = options?.crypto;
21644
+ if (typeof crypto2?.openBox === "function" && (typeof crypto2.isAvailable !== "function" || crypto2.isAvailable())) {
21645
+ return crypto2.openBox(epoch.bodyKey, nonce, ct, aad);
21387
21646
  }
21388
21647
  return openBox(epoch.bodyKey, nonce, ct, aad);
21389
21648
  }
21390
21649
  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);
21650
+ const crypto2 = options?.crypto;
21651
+ if (typeof crypto2?.verifyChatBytes === "function" && (typeof crypto2.isAvailable !== "function" || crypto2.isAvailable())) {
21652
+ return crypto2.verifyChatBytes(publicKey, sig, bytes);
21394
21653
  }
21395
21654
  return verifyChatBytes(publicKey, toHex(sig), bytes);
21396
21655
  }
@@ -21473,9 +21732,9 @@ async function openMessageBatchV3(epoch, records, options = {}) {
21473
21732
  if (!epoch?.chatId || !epoch?.epochId || !epoch?.bodyKey || !epoch?.manifest) {
21474
21733
  throw new Error("chat batch epoch required");
21475
21734
  }
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));
21735
+ const crypto2 = options.crypto;
21736
+ if (typeof crypto2?.openMessageBatchV3 === "function" && (typeof crypto2.isAvailable !== "function" || crypto2.isAvailable())) {
21737
+ const opened = await crypto2.openMessageBatchV3(makeBatchOpenRequest(epoch, source));
21479
21738
  if (!Array.isArray(opened) || opened.length !== source.length) {
21480
21739
  throw new Error("invalid chat message batch result");
21481
21740
  }
@@ -22831,20 +23090,6 @@ async function readChatFile(readChatMedia, file) {
22831
23090
  }
22832
23091
  }
22833
23092
 
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
23093
  // ../../core/username.js
22849
23094
  var MAX_USERNAME = USERNAME_MAX_CHARS;
22850
23095
  var usernameKeyRegex = /^[a-z0-9]$/i;
@@ -24493,7 +24738,7 @@ function getChatSettingsEventAvatar(msg) {
24493
24738
  return chatAvatarFile(msg?.avatarRef) ? msg.avatarRef : "";
24494
24739
  }
24495
24740
  function isSystemMsg(msg) {
24496
- return !!getSystemMsgText(msg) && (msg?.t === SYSTEM_MSG_TYPE || isMembershipEventMsg(msg));
24741
+ return !!getSystemMsgText(msg) && (msg?.t === SYSTEM_MSG_TYPE || msg?.t === CALL_MSG_TYPE || isMembershipEventMsg(msg));
24497
24742
  }
24498
24743
  function makeChatInviteNotice(profiles) {
24499
24744
  const names = [...new Map(profiles.map((profile) => [profile.chatPK, formatUserDisplay(profile, true)])).values()];
@@ -24528,6 +24773,8 @@ function isMembershipEventMsg(msg) {
24528
24773
  return !!getMembershipEvent(msg);
24529
24774
  }
24530
24775
  function getSystemMsgText(msg) {
24776
+ if (msg?.t === CALL_MSG_TYPE)
24777
+ return callMessageText(msg);
24531
24778
  if (isChatInviteNotice(msg)) {
24532
24779
  const names = msg.names.length < 2 ? msg.names[0] : `${msg.names.slice(0, -1).join(", ")} and ${msg.names.at(-1)}`;
24533
24780
  return `${names} ${msg.names.length === 1 ? "is" : "are"} not accepting chat invitations right now`;
@@ -24625,6 +24872,8 @@ function canShowMsg(msg) {
24625
24872
  return hasText(msg.a);
24626
24873
  case "pay":
24627
24874
  return isChatPayment(msg) || isPendingChatPayment(msg);
24875
+ case CALL_MSG_TYPE:
24876
+ return !!readCallMessage(msg);
24628
24877
  case SYSTEM_MSG_TYPE:
24629
24878
  return !!getSystemMsgText(msg);
24630
24879
  default:
@@ -24837,8 +25086,12 @@ function analyzeMessageExpiry(messages, selfChatPublicKey, _peerChatPublicKey, o
24837
25086
  const heldExpired = [];
24838
25087
  const shorten = [];
24839
25088
  let nextExpiryAt = null;
25089
+ const latestStart = latestCallStartKey(projectedMessages.filter(isServerConfirmedMsg));
24840
25090
  for (const message of projectedMessages) {
24841
- if (!isServerConfirmedMsg(message) || isControlMsg(message) || isSystemMsg(message) || !canShowMsg(message) || message.ttl == null)
25091
+ if (!isServerConfirmedMsg(message) || !canShowMsg(message) || message.ttl == null)
25092
+ continue;
25093
+ const call = readCallMessage(message);
25094
+ if (call?.event === "started" && getMessageKey(message) === latestStart && (options.activeCallId === undefined || options.activeCallId === call.callId))
24842
25095
  continue;
24843
25096
  const messageMs = messageOrderMs(message);
24844
25097
  const epochId = messageEpochId(message);
@@ -24875,7 +25128,7 @@ function analyzeMessageExpiry(messages, selfChatPublicKey, _peerChatPublicKey, o
24875
25128
  if (expiresAt == null)
24876
25129
  continue;
24877
25130
  const currentTtlMs = Number.isFinite(message.ttl) ? message.ttl : Number(message.ttl?.toMillis?.());
24878
- if (!Number.isFinite(currentTtlMs) || currentTtlMs > expiresAt)
25131
+ if (!isControlMsg(message) && (!Number.isFinite(currentTtlMs) || currentTtlMs > expiresAt))
24879
25132
  shorten.push({ message, expiresAt });
24880
25133
  if (expiresAt > now) {
24881
25134
  nextExpiryAt = nextExpiryAt == null ? expiresAt : Math.min(nextExpiryAt, expiresAt);
@@ -25028,6 +25281,8 @@ async function compactMessages({ chatId, maintenance, messages, deletedKeys, pro
25028
25281
  function sendableMsgPayload(message) {
25029
25282
  if (isChatInviteNotice(message))
25030
25283
  throw new Error("local chat notices cannot be sent");
25284
+ if (message?.t === CALL_MSG_TYPE && !readCallMessage(message))
25285
+ throw new Error("invalid call message");
25031
25286
  const { localData, localUri, pending, failed, peerChatPK, id, from, ts, chatId, chatDraft, protocol, editedAt, editId, editStatus, editOriginal, ...payload } = message || {};
25032
25287
  return payload;
25033
25288
  }
@@ -25074,6 +25329,8 @@ function canRenderPreviewContent(preview) {
25074
25329
  if (preview.t === "pay") {
25075
25330
  return isChatPayment(preview) || isPendingChatPayment(preview);
25076
25331
  }
25332
+ if (preview.t === CALL_MSG_TYPE)
25333
+ return !!readCallMessage(preview);
25077
25334
  if (isAttachmentMsgType(preview?.t)) {
25078
25335
  return true;
25079
25336
  }
@@ -25701,8 +25958,9 @@ async function makeChatRecipientDeliveries(identity, epochState, routes, message
25701
25958
  const route = routes[member.chatPK] || {};
25702
25959
  const descriptor = await sealNotificationDescriptor(member.notificationPK, {
25703
25960
  peerTag: notificationChatTag(epochState.stateCapability, epochState.manifest.chatId, member.chatPK),
25704
- eventId: messageId
25705
- });
25961
+ eventId: messageId,
25962
+ kind: fields.presentationKind ?? NOTIFICATION_PRESENTATION_KINDS.ACTIVITY
25963
+ }, identity);
25706
25964
  const ping = await sealPing(identity, member.chatPK, {
25707
25965
  kind: fields.kind,
25708
25966
  chatId: epochState.manifest.chatId,
@@ -25776,6 +26034,7 @@ async function prepareMsgRecord(identity, epochState, message, options = {}) {
25776
26034
  cid: head.cid,
25777
26035
  record: { lane: epoch.messageLane, head, body, ttlMs },
25778
26036
  message: ownerPreview(epoch, identity.chatPK, messagePayload, messageId, head, tsMs, ttlMs),
26037
+ actionOp,
25779
26038
  mentionRecipientChatPKs: mentionTargets.recipientChatPKs,
25780
26039
  tsMs
25781
26040
  };
@@ -25809,6 +26068,7 @@ async function sendMsg(cloud, senderChatPK, senderPrivateKey, _receiverChatPK, m
25809
26068
  msgId: messageId,
25810
26069
  record,
25811
26070
  message: sentMessage,
26071
+ actionOp,
25812
26072
  mentionRecipientChatPKs,
25813
26073
  tsMs
25814
26074
  } = prepared;
@@ -25839,8 +26099,9 @@ async function sendMsg(cloud, senderChatPK, senderPrivateKey, _receiverChatPK, m
25839
26099
  shouldPing,
25840
26100
  deliverRecipients: options.deliverRecipients !== false,
25841
26101
  kind: pingKind,
26102
+ presentationKind: isReactionMsg(sentMessage) && sentMessage.emoji ? NOTIFICATION_PRESENTATION_KINDS.REACTION : actionOp !== CHAT_ACTION_OPS.CREATE || isControlMsg(sentMessage) ? NOTIFICATION_PRESENTATION_KINDS.ACTIVITY : notificationMessageKind(sentMessage),
25842
26103
  ownDeliveryCapability: routeState.capability,
25843
- silent: options.notify === false,
26104
+ silent: options.notify === false || notificationMessageIsSilent(sentMessage),
25844
26105
  recipientChatPKs: deliveryRecipientChatPKs,
25845
26106
  attentionRecipientChatPKs: mentionRecipientChatPKs,
25846
26107
  tsMs
@@ -27806,8 +28067,9 @@ async function makeMlsWelcomeDeliveries(identity, nextState2, addedMembers, opti
27806
28067
  }
27807
28068
  const descriptor = await sealNotificationDescriptor(member.notificationPK, {
27808
28069
  peerTag: notificationChatTag(nextState2.stateCapability, nextState2.manifest.chatId, member.chatPK),
27809
- eventId: nextState2.mlsPackageId
27810
- });
28070
+ eventId: nextState2.mlsPackageId,
28071
+ kind: NOTIFICATION_PRESENTATION_KINDS.ACTIVITY
28072
+ }, identity);
27811
28073
  const ping = await sealPing(identity, member.chatPK, {
27812
28074
  kind: "welcome",
27813
28075
  chatId: nextState2.manifest.chatId,
@@ -37904,6 +38166,8 @@ function createChatListIndex({ cache, filterHiddenChats, getSources, isHiddenCha
37904
38166
  const boundary = authorityBoundaries.get(chat?.id);
37905
38167
  if (boundary?.terminal)
37906
38168
  return false;
38169
+ if (boundary?.leftEpoch != null && !(chat?.epochVersion > boundary.leftEpoch))
38170
+ return false;
37907
38171
  if (!chat?.id || !Number.isSafeInteger(sourceRevision))
37908
38172
  return true;
37909
38173
  if (sourceRevision < sourceRevisionFloor)
@@ -38067,7 +38331,7 @@ function createChatListIndex({ cache, filterHiddenChats, getSources, isHiddenCha
38067
38331
  continue;
38068
38332
  if (!sourceCanRemove(chatId, sourceRevision))
38069
38333
  continue;
38070
- if (entry.announcement && !authorityBoundaries.get(chatId)?.owner) {
38334
+ if (entry.announcement && !ownerCoversAnnouncement(authorityBoundaries.get(chatId)?.owner, entry.announcement)) {
38071
38335
  entry.cache = null;
38072
38336
  continue;
38073
38337
  }
@@ -38153,6 +38417,9 @@ function createChatListIndex({ cache, filterHiddenChats, getSources, isHiddenCha
38153
38417
  return false;
38154
38418
  if (authorityBoundaries.get(announcement.chatId)?.terminal)
38155
38419
  return false;
38420
+ const leftEpoch = authorityBoundaries.get(announcement.chatId)?.leftEpoch;
38421
+ if (leftEpoch != null && !(announcement.epochVersion > leftEpoch))
38422
+ return false;
38156
38423
  const sources = getSources();
38157
38424
  if (sources.pendingDeleteIdsRef.current.has(announcement.chatId))
38158
38425
  return false;
@@ -38211,6 +38478,26 @@ function createChatListIndex({ cache, filterHiddenChats, getSources, isHiddenCha
38211
38478
  return false;
38212
38479
  return removeOwner(chatId, options);
38213
38480
  };
38481
+ const leaveOwner = (chatId, epochVersion) => {
38482
+ if (!chatId || !Number.isSafeInteger(epochVersion) || epochVersion < 1)
38483
+ return false;
38484
+ const previous = authorityBoundaries.get(chatId);
38485
+ if (previous?.terminal)
38486
+ return false;
38487
+ if (previous?.owner?.epochVersion > epochVersion)
38488
+ return false;
38489
+ authorityRevision += 1;
38490
+ authorityBoundaries.set(chatId, {
38491
+ owner: previous?.owner || structuralChat(ownerFor(entries.get(chatId))),
38492
+ revision: authorityRevision,
38493
+ terminal: false,
38494
+ leftEpoch: Math.max(previous?.leftEpoch || 0, epochVersion)
38495
+ });
38496
+ entries.delete(chatId);
38497
+ clearRemovedOwners([chatId]);
38498
+ publishOwners({ warm: false });
38499
+ return true;
38500
+ };
38214
38501
  const getOwnerChat = (chatId) => {
38215
38502
  if (!chatId || isHiddenChatId(chatId))
38216
38503
  return null;
@@ -38397,6 +38684,7 @@ function createChatListIndex({ cache, filterHiddenChats, getSources, isHiddenCha
38397
38684
  observeMessages,
38398
38685
  patchRowSettings,
38399
38686
  removeOwner,
38687
+ leaveOwner,
38400
38688
  removeInboxOwner,
38401
38689
  render,
38402
38690
  reset,
@@ -41712,10 +42000,10 @@ function createChatSessionMembership({
41712
42000
  await cloud.delivery.revoke(capability).catch(() => false);
41713
42001
  assertCurrent(operation);
41714
42002
  if (retirementStarted) {
41715
- operation.deleteActions.commitLeftChat(chatId);
42003
+ operation.deleteActions.commitLeftChat(chatId, chat.epochVersion);
41716
42004
  retirementStarted = false;
41717
42005
  } else
41718
- operation.deleteActions.dropLeftChat(chatId);
42006
+ operation.deleteActions.dropLeftChat(chatId, chat.epochVersion);
41719
42007
  return { left: true, message: proposal.message };
41720
42008
  } catch (error) {
41721
42009
  if (retirementStarted && isCurrent(operation)) {
@@ -41967,8 +42255,9 @@ async function deliverChatDeletedPings(cloud, identityValue, epochState, members
41967
42255
  continue;
41968
42256
  const descriptor = await sealNotificationDescriptor(member.notificationPK, {
41969
42257
  peerTag: notificationChatTag(epochState.stateCapability, manifest.chatId, member.chatPK),
41970
- eventId
41971
- });
42258
+ eventId,
42259
+ kind: NOTIFICATION_PRESENTATION_KINDS.ACTIVITY
42260
+ }, identity);
41972
42261
  const ping = await sealPing(identity, member.chatPK, {
41973
42262
  kind: "chat_deleted",
41974
42263
  chatId: manifest.chatId,
@@ -42121,20 +42410,20 @@ function createChatDelete({
42121
42410
  return;
42122
42411
  listActionsRef.current?.removeOwner?.(chatId, { warm: false });
42123
42412
  };
42124
- const dropLeftChat = (chatId) => {
42413
+ const dropLeftChat = (chatId, epochVersion) => {
42125
42414
  if (!chatId)
42126
42415
  return false;
42127
- dropChat(chatId);
42416
+ listActionsRef.current?.leaveOwner?.(chatId, epochVersion);
42128
42417
  return true;
42129
42418
  };
42130
- const commitLeftChat = (chatId) => {
42419
+ const commitLeftChat = (chatId, epochVersion) => {
42131
42420
  if (!chatId)
42132
42421
  return false;
42133
42422
  pendingDeleteIdsRef.current.delete(chatId);
42134
42423
  locallyDeletedChatIdsRef.current.delete(chatId);
42135
42424
  deletedChatIdsRef.current.delete(chatId);
42136
42425
  keepSelectedDeletedChatIdsRef.current.delete(chatId);
42137
- confirmDeletedChats([chatId]);
42426
+ dropLeftChat(chatId, epochVersion);
42138
42427
  return true;
42139
42428
  };
42140
42429
  const dropUnavailableChat = (chatId) => {
@@ -43699,6 +43988,7 @@ function createChatSession({
43699
43988
  mls = null,
43700
43989
  live = null,
43701
43990
  maintenance,
43991
+ resolveActiveCall,
43702
43992
  incomingChatDecision = null,
43703
43993
  getPeerProfile = null,
43704
43994
  refreshPeerProfile = null,
@@ -44474,7 +44764,6 @@ function createChatSession({
44474
44764
  assertBulkOperationCurrent(operation);
44475
44765
  await cloud.user.chats.unlink(identity.uid, chat.entryId);
44476
44766
  assertBulkOperationCurrent(operation);
44477
- operation.deleteActions.commitRetirement(chat.id);
44478
44767
  } else {
44479
44768
  await operation.deleteActions.deleteChat(chat, { cleanup: false });
44480
44769
  }
@@ -44561,6 +44850,26 @@ function createChatSession({
44561
44850
  return false;
44562
44851
  }
44563
44852
  };
44853
+ const prepareCall = async (chatId) => {
44854
+ const generation = authGeneration;
44855
+ const current = () => generation === authGeneration && online && pendingOwner.canSendToChat(chatId);
44856
+ if (!current())
44857
+ throw new Error("chat unavailable");
44858
+ await pendingOwner.assertChatAdmission(chatId);
44859
+ if (!getOwnerChat(chatId)?.epochState)
44860
+ await ensureChat(chatId);
44861
+ if (!current())
44862
+ throw new Error("chat unavailable");
44863
+ const reconciliation = await chatListOwner.reconcileChatEpoch(chatId);
44864
+ if (!current() || reconciliation?.removed)
44865
+ throw new Error("chat unavailable");
44866
+ await pendingOwner.assertChatAdmission(chatId);
44867
+ const chat = getVisibleOwnerChat(chatId);
44868
+ if (!current() || !chat?.epochState)
44869
+ throw new Error("chat unavailable");
44870
+ return chat.epochState;
44871
+ };
44872
+ const materializeChat = (chatId) => pendingOwner.runChatMutation(chatId, (canonicalId) => canonicalId);
44564
44873
  const flushChatReadFrontier = (chatId) => {
44565
44874
  if (!online)
44566
44875
  return false;
@@ -44843,6 +45152,8 @@ function createChatSession({
44843
45152
  markChatTyping,
44844
45153
  hasChat: hasVisibleChat,
44845
45154
  getOwnerChat: getVisibleOwnerChat,
45155
+ prepareCall,
45156
+ materializeChat,
44846
45157
  sendOptionsForPeer,
44847
45158
  getLocalMessages,
44848
45159
  wasChatDeletedLocally,
@@ -44876,6 +45187,7 @@ function createChatSession({
44876
45187
  deleteMessage,
44877
45188
  deleteMessages,
44878
45189
  deleteMessageDocs,
45190
+ resolveActiveCall,
44879
45191
  setChatTtl,
44880
45192
  makeMessagePermanent,
44881
45193
  makeMessageTemporary,
@@ -44933,6 +45245,7 @@ function createChatSession({
44933
45245
  stateOwner.setListSnapshot(chatListOwner.getSnapshot());
44934
45246
  deleteListActionsRef.current = {
44935
45247
  removeOwner: (...args) => chatListOwner.removeOwner(...args),
45248
+ leaveOwner: (...args) => chatListOwner.leaveOwner(...args),
44936
45249
  render: (...args) => chatListOwner.render(...args)
44937
45250
  };
44938
45251
  actionOwner.createActionOwners();
@@ -45654,6 +45967,2695 @@ function createPresence({ cloud, diag = null, now = Date.now } = {}) {
45654
45967
  });
45655
45968
  }
45656
45969
 
45970
+ // ../../core/calls/protocol.js
45971
+ var CALL_MAX_PARTICIPANTS = 16;
45972
+ var CALL_TTL_MS = 120000;
45973
+ var CALL_ADMISSION_TIMEOUT_MS = 30000;
45974
+ var HEX16 = /^[0-9a-f]{32}$/u;
45975
+ var HEX32 = /^[0-9a-f]{64}$/u;
45976
+ function bytes(packet) {
45977
+ const { signature: _signature, ...body } = packet;
45978
+ return canonicalBytes(["veyl.call.packet.v1", body]);
45979
+ }
45980
+ function createCallProtocol({ realm: realm2, epochState, identity, endpoint, callId = "" }) {
45981
+ const { manifest } = epochState;
45982
+ const members = new Map(manifest.members.map((member) => [member.chatSigningPK, member]));
45983
+ if (!members.has(identity.chatSigningPK))
45984
+ throw new Error("not a chat member");
45985
+ const actor = identity.chatSigningPK;
45986
+ const secret = identity.chatSigningSecret.slice();
45987
+ let closed = false;
45988
+ const context = { v: 1, realm: realm2, chatId: manifest.chatId, epochId: manifest.epochId, callId };
45989
+ return Object.freeze({
45990
+ sign(kind, data) {
45991
+ if (closed)
45992
+ throw new Error("call protocol closed");
45993
+ const packet = {
45994
+ ...context,
45995
+ actor,
45996
+ holder: endpoint.holder,
45997
+ endpoint: endpoint.publicKey,
45998
+ nonce: toHex(randomBytes3(16)),
45999
+ at: Date.now(),
46000
+ kind,
46001
+ data
46002
+ };
46003
+ return { ...packet, signature: signChatBytes({ secret, publicKey: actor }, bytes(packet)) };
46004
+ },
46005
+ verify(packet, { fresh = true } = {}) {
46006
+ 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)))
46007
+ throw new Error("invalid call participant proof");
46008
+ return members.get(packet.actor);
46009
+ },
46010
+ close() {
46011
+ if (!closed) {
46012
+ closed = true;
46013
+ cleanBytes(secret);
46014
+ }
46015
+ }
46016
+ });
46017
+ }
46018
+ function verifyCallAdmissions(admissions, protocol) {
46019
+ if (!Array.isArray(admissions) || !admissions.length || admissions.length > CALL_MAX_PARTICIPANTS)
46020
+ throw new Error("call membership mismatch");
46021
+ const holders = new Set;
46022
+ const identities = new Set;
46023
+ const members = new Map;
46024
+ for (const admission of admissions) {
46025
+ const identity = protocol.verify(admission, { fresh: false });
46026
+ 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))
46027
+ throw new Error("invalid call participant");
46028
+ holders.add(admission.holder);
46029
+ identities.add(admission.actor);
46030
+ members.set(admission.holder, identity);
46031
+ }
46032
+ return members;
46033
+ }
46034
+ function verifyCallMembers(mlsState, admissions, protocol) {
46035
+ const members = verifyCallAdmissions(admissions, protocol);
46036
+ if (mlsState.members.length !== admissions.length)
46037
+ throw new Error("call membership mismatch");
46038
+ for (const admission of admissions) {
46039
+ const member = mlsState.members.find((item) => toHex(item.leafId) === admission.holder);
46040
+ if (!member || toHex(member.signaturePK) !== admission.data.signaturePK)
46041
+ throw new Error("call leaf identity mismatch");
46042
+ }
46043
+ return members;
46044
+ }
46045
+ function verifyCallReplacement(packet, currentHead, protocol) {
46046
+ protocol.verify(packet, { fresh: false });
46047
+ const previous = packet.data?.previous;
46048
+ const replacement = packet.data?.participant;
46049
+ protocol.verify(previous, { fresh: false });
46050
+ if (packet.kind !== "replace" || previous.kind !== "head" || previous.data?.participants?.length !== 1)
46051
+ throw new Error("invalid call replacement");
46052
+ verifyCallAdmissions(previous.data.participants, protocol);
46053
+ verifyCallAdmissions([replacement], protocol);
46054
+ const original = previous.data.participants[0];
46055
+ 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)
46056
+ throw new Error("invalid call replacement");
46057
+ if (currentHead && (currentHead.epoch !== previous.data.epoch || currentHead.participants.length !== 1 || currentHead.participants[0].signature !== original.signature))
46058
+ throw new Error("stale call replacement");
46059
+ return { epoch: 0, participants: [replacement] };
46060
+ }
46061
+
46062
+ // ../../core/calls/mailbox.js
46063
+ function openCallMailbox({ cloud, capability, endpoint, protocol, onChange, onError, stream = true }) {
46064
+ let closed = false;
46065
+ let record = null;
46066
+ let candidate = null;
46067
+ let tail = Promise.resolve();
46068
+ let cursor = 0;
46069
+ const decode = async (value, fresh = true, maximum) => {
46070
+ const packet = await capability.open(decodeCallBytes(value, maximum));
46071
+ protocol.verify(packet, { fresh });
46072
+ return packet;
46073
+ };
46074
+ const accept = async (value) => {
46075
+ if (closed)
46076
+ return;
46077
+ if (!Number.isSafeInteger(value?.revision) || !Number.isSafeInteger(value?.cursor) || value.revision < 0 || value.cursor < 0 || !Array.isArray(value.events))
46078
+ throw new Error("invalid call mailbox");
46079
+ if (record && value.revision < record.revision)
46080
+ return;
46081
+ if (value.cursor < cursor)
46082
+ throw new Error("invalid call event order");
46083
+ if (value.events.some((item) => !Number.isSafeInteger(item?.cursor) || item.cursor < 1 || item.cursor > value.cursor))
46084
+ throw new Error("invalid call event order");
46085
+ const empty = value.headDigest === null && value.payload === null && value.proof === null && value.events.length === 0;
46086
+ const unseen = value.events.filter((item) => item.cursor > cursor);
46087
+ const historyLost = !!(!empty && cursor && value.cursor > cursor && (!unseen.length || unseen[0].cursor !== cursor + 1));
46088
+ if (historyLost && (stream || value.truncated !== true))
46089
+ throw new Error("call signaling history unavailable");
46090
+ let head = null;
46091
+ let payload = null;
46092
+ if (value.headDigest !== null) {
46093
+ if (typeof value.headDigest !== "string" || !/^[a-f0-9]{64}$/u.test(value.headDigest))
46094
+ throw new Error("invalid call head digest");
46095
+ const proof = await decode(value.proof, true, CALL_HEAD_PROOF_MAX_BYTES);
46096
+ if (proof.kind !== "head-proof" || proof.data?.digest !== value.headDigest)
46097
+ throw new Error("invalid call head proof");
46098
+ if (record?.headDigest === value.headDigest && record.head) {
46099
+ if (Object.hasOwn(value, "payload") && callHeadDigest(decodeCallBytes(value.payload)) !== value.headDigest)
46100
+ throw new Error("invalid call head digest");
46101
+ head = record.head;
46102
+ payload = record.payload;
46103
+ } else {
46104
+ if (!value.payload || callHeadDigest(decodeCallBytes(value.payload)) !== value.headDigest)
46105
+ throw new Error("missing call head");
46106
+ head = await decode(value.payload, false);
46107
+ if (head.kind !== "head")
46108
+ throw new Error("invalid call head");
46109
+ payload = value.payload;
46110
+ }
46111
+ } else if (value.payload !== null || value.proof !== null)
46112
+ throw new Error("invalid empty call head");
46113
+ const events = [];
46114
+ let nextCursor = empty ? value.cursor : historyLost ? 0 : cursor;
46115
+ for (const item of unseen) {
46116
+ if (!Number.isSafeInteger(item.cursor) || item.cursor < 1 || nextCursor && item.cursor !== nextCursor + 1)
46117
+ throw new Error("invalid call event order");
46118
+ const packet = await decode(item.payload, false, CALL_EVENT_MAX_BYTES);
46119
+ events.push(packet);
46120
+ nextCursor = item.cursor;
46121
+ }
46122
+ if (nextCursor !== value.cursor)
46123
+ throw new Error("call signaling history unavailable");
46124
+ if (closed)
46125
+ return;
46126
+ candidate = { ...value, payload, head, events, historyLost };
46127
+ try {
46128
+ await onChange?.(candidate);
46129
+ if (closed)
46130
+ return;
46131
+ cursor = nextCursor;
46132
+ record = candidate;
46133
+ } finally {
46134
+ candidate = null;
46135
+ }
46136
+ };
46137
+ const consume = (value) => {
46138
+ const result = tail.then(() => accept(value));
46139
+ tail = result.catch((error) => {
46140
+ if (!closed)
46141
+ onError?.(error);
46142
+ });
46143
+ return result;
46144
+ };
46145
+ let channel;
46146
+ try {
46147
+ channel = cloud.calls.connect({
46148
+ capability,
46149
+ endpoint,
46150
+ onSnapshot: consume,
46151
+ onError,
46152
+ stream,
46153
+ getCheckpoint: () => ({ cursor, headDigest: record?.headDigest ?? null })
46154
+ });
46155
+ } catch (error) {
46156
+ capability.close();
46157
+ protocol.close?.();
46158
+ throw error;
46159
+ }
46160
+ async function mutate(headDigest, payload, events, { expectedRevision = (candidate || record)?.revision ?? 0, ttlMs = CALL_TTL_MS } = {}) {
46161
+ if (!stream)
46162
+ throw new Error("read-only call mailbox");
46163
+ const proof = encodeCallBytes(await capability.seal(protocol.sign("head-proof", { digest: headDigest })));
46164
+ const packets = await Promise.all(events.map(async (packet) => encodeCallBytes(await capability.seal(packet))));
46165
+ if (closed)
46166
+ throw new Error("call mailbox closed");
46167
+ const command = {
46168
+ type: "commit",
46169
+ expectedRevision,
46170
+ operation: toHex(randomBytes3(16)),
46171
+ headDigest,
46172
+ proof,
46173
+ ...payload !== undefined ? { payload } : {},
46174
+ events: packets,
46175
+ ttlMs
46176
+ };
46177
+ let response;
46178
+ try {
46179
+ response = await channel.request(command);
46180
+ } catch (error) {
46181
+ if (closed || !["calls/connection", "calls/timeout"].includes(error?.code))
46182
+ throw error;
46183
+ response = await channel.request(command);
46184
+ }
46185
+ if (closed)
46186
+ throw new Error("call mailbox closed");
46187
+ consume(response.record).catch(() => {});
46188
+ return response.record;
46189
+ }
46190
+ return Object.freeze({
46191
+ getSnapshot: () => candidate || record,
46192
+ async read() {
46193
+ const response = await channel.request({ type: "read", cursor, headDigest: record?.headDigest ?? null });
46194
+ await consume(response.record);
46195
+ return record;
46196
+ },
46197
+ async commit(head, events = [], options) {
46198
+ const bytes2 = await capability.seal(protocol.sign("head", head));
46199
+ return mutate(callHeadDigest(bytes2), encodeCallBytes(bytes2), events, options);
46200
+ },
46201
+ async append(events, options) {
46202
+ const current = candidate || record;
46203
+ if (!current?.headDigest || !current.head)
46204
+ throw new Error("call ended");
46205
+ return mutate(current.headDigest, undefined, events, options);
46206
+ },
46207
+ close() {
46208
+ closed = true;
46209
+ channel.close();
46210
+ capability.close();
46211
+ protocol.close?.();
46212
+ }
46213
+ });
46214
+ }
46215
+
46216
+ // ../../core/calls/ownership.js
46217
+ var CALL_OWNERSHIP_LEASE_MS = 15000;
46218
+
46219
+ // ../../core/calls/clock.js
46220
+ function callLeaseClock() {
46221
+ return {
46222
+ wall: Date.now(),
46223
+ monotonic: globalThis.performance.timeOrigin + globalThis.performance.now()
46224
+ };
46225
+ }
46226
+
46227
+ // ../../core/calls/leaseattempt.js
46228
+ var STOP_MARGIN_MS = 250;
46229
+ function validTime(value) {
46230
+ return Number.isFinite(value) && value >= 0;
46231
+ }
46232
+ function createCallLeaseAttempt(command, clock = callLeaseClock) {
46233
+ if (!["claim", "activate", "renew"].includes(command?.type))
46234
+ throw new Error("call lease command required");
46235
+ if (command.operation !== undefined)
46236
+ throw new Error("reuse the original call lease attempt");
46237
+ const revision = command.type === "claim" ? command.expectedRevision + 1 : command.revision;
46238
+ const sequence = command.type === "renew" ? command.sequence : 0;
46239
+ if (!/^[a-f0-9]{32}$/u.test(command.holder || "") || !Number.isSafeInteger(revision) || revision < 1 || !Number.isSafeInteger(sequence) || sequence < (command.type === "renew" ? 1 : 0))
46240
+ throw new Error("invalid call lease command");
46241
+ const started = clock();
46242
+ if (!validTime(started?.wall) || !validTime(started?.monotonic))
46243
+ throw new Error("call lease clock unavailable");
46244
+ const deadline = Object.freeze({
46245
+ sessionId: command.holder,
46246
+ revision,
46247
+ wallDeadline: started.wall + CALL_OWNERSHIP_LEASE_MS - STOP_MARGIN_MS,
46248
+ monotonicDeadline: started.monotonic + CALL_OWNERSHIP_LEASE_MS - STOP_MARGIN_MS
46249
+ });
46250
+ const startWall = started.wall;
46251
+ const startMonotonic = started.monotonic;
46252
+ const operation = toHex(randomBytes3(16));
46253
+ const request = Object.freeze({ ...command, ...command.payload ? { payload: command.payload.slice() } : {}, operation });
46254
+ let cancelled2 = false;
46255
+ return Object.freeze({
46256
+ command: request,
46257
+ cancel() {
46258
+ cancelled2 = true;
46259
+ },
46260
+ grant(record) {
46261
+ if (cancelled2)
46262
+ return null;
46263
+ const now = clock();
46264
+ if (!validTime(now?.wall) || !validTime(now?.monotonic) || now.wall < startWall || now.monotonic < startMonotonic || now.wall >= deadline.wallDeadline || now.monotonic >= deadline.monotonicDeadline) {
46265
+ cancelled2 = true;
46266
+ return null;
46267
+ }
46268
+ const active = record?.active;
46269
+ if (record?.pending || record?.revision !== revision || active?.revision !== revision || active.holder !== deadline.sessionId || active.sequence !== sequence || active.operation !== operation)
46270
+ return null;
46271
+ return deadline;
46272
+ }
46273
+ });
46274
+ }
46275
+
46276
+ // ../../core/calls/accountlease.js
46277
+ var transient = (error) => ["calls/connection", "calls/timeout"].includes(error?.code);
46278
+ function openAccountCallLease({ cloud, capability: accountCapability, endpoint, clock = callLeaseClock, onRecord, onGrant, onLost, onError }) {
46279
+ const capability = accountCapability.retain();
46280
+ let closed = false;
46281
+ let record = null;
46282
+ let owned = null;
46283
+ let ownedDeadline = null;
46284
+ let retiring = null;
46285
+ let renewal = null;
46286
+ let expiry = null;
46287
+ let generation = 0;
46288
+ let acquiring = false;
46289
+ const attempts = new Set;
46290
+ const waiters = new Set;
46291
+ const current = () => !closed;
46292
+ const wake = () => {
46293
+ for (const resolve of [...waiters])
46294
+ resolve();
46295
+ };
46296
+ function waitForChange(milliseconds) {
46297
+ return new Promise((resolve) => {
46298
+ const finish = () => {
46299
+ clearTimeout(timer);
46300
+ waiters.delete(finish);
46301
+ resolve();
46302
+ };
46303
+ const timer = setTimeout(finish, milliseconds);
46304
+ waiters.add(finish);
46305
+ });
46306
+ }
46307
+ function stop() {
46308
+ generation++;
46309
+ clearTimeout(renewal);
46310
+ clearTimeout(expiry);
46311
+ renewal = expiry = null;
46312
+ for (const attempt of attempts)
46313
+ attempt.cancel();
46314
+ attempts.clear();
46315
+ const previous = owned;
46316
+ owned = ownedDeadline = null;
46317
+ wake();
46318
+ return previous;
46319
+ }
46320
+ async function accept(value) {
46321
+ if (closed)
46322
+ return;
46323
+ const next = decodeCallRecord(value);
46324
+ if (!Number.isSafeInteger(next?.version) || next.version < 0)
46325
+ throw new Error("invalid call ownership version");
46326
+ if (record && next.version <= record.version)
46327
+ return;
46328
+ record = next;
46329
+ wake();
46330
+ if (owned && (next.pending || next.active?.holder !== endpoint.holder || next.active?.revision !== owned.revision)) {
46331
+ retiring = stop();
46332
+ onLost?.();
46333
+ }
46334
+ const active = next.pending || next.active;
46335
+ const descriptor = active ? await capability.open(active.payload) : null;
46336
+ if (current() && record === next)
46337
+ onRecord?.(next, descriptor);
46338
+ }
46339
+ let channel;
46340
+ try {
46341
+ channel = cloud.calls.connect({
46342
+ capability,
46343
+ endpoint,
46344
+ onSnapshot: (value) => accept(value).catch(onError),
46345
+ onError(error) {
46346
+ if (!transient(error))
46347
+ onError?.(error);
46348
+ }
46349
+ });
46350
+ } catch (error) {
46351
+ capability.close();
46352
+ throw error;
46353
+ }
46354
+ async function request(command) {
46355
+ const response = await channel.request({ ...command, ...command.payload ? { payload: encodeCallBytes(command.payload) } : {} });
46356
+ await accept(response.record);
46357
+ return decodeCallRecord(response.record);
46358
+ }
46359
+ function renewalRemaining(started) {
46360
+ if (!current() || started !== generation || !owned || !ownedDeadline)
46361
+ return 0;
46362
+ const now = clock();
46363
+ if (!Number.isFinite(now?.wall) || !Number.isFinite(now?.monotonic) || now.wall < 0 || now.monotonic < 0)
46364
+ return 0;
46365
+ return Math.max(0, Math.min(ownedDeadline.wallDeadline - now.wall, ownedDeadline.monotonicDeadline - now.monotonic));
46366
+ }
46367
+ async function requestGrant(attempt, started) {
46368
+ let delay = 250;
46369
+ for (;; ) {
46370
+ if (attempt.command.type === "renew" && !renewalRemaining(started))
46371
+ throw new Error("call ownership expired");
46372
+ try {
46373
+ const value = await request(attempt.command);
46374
+ if (attempt.command.type === "renew" && !renewalRemaining(started))
46375
+ throw new Error("call ownership expired");
46376
+ return value;
46377
+ } catch (error) {
46378
+ const remaining = renewalRemaining(started);
46379
+ if (attempt.command.type !== "renew" || !transient(error) || !remaining)
46380
+ throw error;
46381
+ await waitForChange(Math.min(delay, remaining));
46382
+ delay = Math.min(delay * 2, 1000);
46383
+ }
46384
+ }
46385
+ }
46386
+ async function grant(command) {
46387
+ if (closed)
46388
+ throw new Error("call lease closed");
46389
+ const attempt = createCallLeaseAttempt(command, clock);
46390
+ const started = generation;
46391
+ attempts.add(attempt);
46392
+ try {
46393
+ const value = await requestGrant(attempt, started);
46394
+ const deadline = attempt.grant(value);
46395
+ if (!current() || started !== generation || !deadline || record.version !== value.version)
46396
+ throw new Error("call ownership unavailable");
46397
+ owned = value.active;
46398
+ await onGrant(deadline);
46399
+ if (!current() || owned !== value.active)
46400
+ throw new Error("call ownership changed");
46401
+ ownedDeadline = deadline;
46402
+ clearTimeout(expiry);
46403
+ expiry = setTimeout(() => {
46404
+ stop();
46405
+ onLost?.();
46406
+ }, Math.max(0, deadline.wallDeadline - Date.now()));
46407
+ clearTimeout(renewal);
46408
+ renewal = setTimeout(() => {
46409
+ if (!owned)
46410
+ return;
46411
+ const renewing = generation;
46412
+ grant({ type: "renew", holder: endpoint.holder, revision: owned.revision, sequence: owned.sequence + 1 }).catch((error) => {
46413
+ if (closed || renewing !== generation)
46414
+ return;
46415
+ stop();
46416
+ onLost?.();
46417
+ onError?.(error);
46418
+ });
46419
+ }, 5000);
46420
+ return value;
46421
+ } finally {
46422
+ attempts.delete(attempt);
46423
+ attempt.cancel();
46424
+ }
46425
+ }
46426
+ return Object.freeze({
46427
+ async acquire(descriptor) {
46428
+ if (closed)
46429
+ throw new Error("call lease closed");
46430
+ if (owned || acquiring)
46431
+ throw new Error("already in a call");
46432
+ acquiring = true;
46433
+ const started = generation;
46434
+ try {
46435
+ await request({ type: "read" });
46436
+ const payload = await capability.seal(descriptor);
46437
+ if (closed || started !== generation)
46438
+ throw new Error("call transfer cancelled");
46439
+ const claim = { type: "claim", holder: endpoint.holder, expectedRevision: record.revision, payload, takeover: true };
46440
+ if (!record.active && !record.pending)
46441
+ return await grant(claim);
46442
+ const pending = createCallLeaseAttempt(claim, clock);
46443
+ attempts.add(pending);
46444
+ try {
46445
+ await request(pending.command);
46446
+ const until = Date.now() + 20000;
46447
+ while (current() && started === generation && record.pending?.holder === endpoint.holder && record.active && Date.now() < until) {
46448
+ await waitForChange(Math.max(1, Math.min(until - Date.now(), record.active.expiresAt - Date.now() + 50)));
46449
+ if (current() && started === generation && record.active?.expiresAt <= Date.now())
46450
+ await request({ type: "read" });
46451
+ }
46452
+ if (!current() || started !== generation || record.pending?.holder !== endpoint.holder)
46453
+ throw new Error("call transfer cancelled");
46454
+ return await grant({ type: "activate", holder: endpoint.holder, revision: record.pending.revision });
46455
+ } finally {
46456
+ attempts.delete(pending);
46457
+ pending.cancel();
46458
+ }
46459
+ } finally {
46460
+ acquiring = false;
46461
+ }
46462
+ },
46463
+ provider(action, payload) {
46464
+ if (!owned || closed)
46465
+ return Promise.reject(new Error("call ownership required"));
46466
+ return channel.provider({ holder: endpoint.holder, revision: owned.revision, action, payload }).then((result) => result.record);
46467
+ },
46468
+ async release() {
46469
+ const previous = stop() || retiring;
46470
+ retiring = null;
46471
+ if (previous && !closed)
46472
+ await request({ type: "release", holder: endpoint.holder, revision: previous.revision });
46473
+ else if (record?.pending?.holder === endpoint.holder && !closed)
46474
+ await request({ type: "cancel", holder: endpoint.holder, revision: record.pending.revision });
46475
+ },
46476
+ retire() {
46477
+ retiring = stop() || retiring;
46478
+ },
46479
+ close() {
46480
+ closed = true;
46481
+ stop();
46482
+ channel.close();
46483
+ capability.close();
46484
+ }
46485
+ });
46486
+ }
46487
+
46488
+ // ../../core/calls/room.js
46489
+ function openCallRoom({
46490
+ cloud,
46491
+ capability,
46492
+ protocol,
46493
+ endpoint,
46494
+ mls,
46495
+ member,
46496
+ admission,
46497
+ callId,
46498
+ onKeys,
46499
+ onParticipants,
46500
+ onSignal,
46501
+ onRemoved,
46502
+ onError
46503
+ }) {
46504
+ let closed = false;
46505
+ let state = null;
46506
+ let head = null;
46507
+ let verifiedHead = null;
46508
+ let admittedIdentities = new Map;
46509
+ let pulseTimer = null;
46510
+ let changing = false;
46511
+ let reconcileAgain = false;
46512
+ let joined = false;
46513
+ let departing = false;
46514
+ let pendingCandidate = null;
46515
+ let ownMedia = null;
46516
+ let audio = { muted: false, deafened: false };
46517
+ let startTask = null;
46518
+ let startPromise = null;
46519
+ let admissionTimer = null;
46520
+ let admissionResult = null;
46521
+ let admissionFailure = null;
46522
+ let pendingInitial = null;
46523
+ let submittedJoin = false;
46524
+ let predecessorHolder = null;
46525
+ let eventTail = Promise.resolve();
46526
+ let retryWait = null;
46527
+ const pendingJoins = new Map;
46528
+ const leaving = new Set;
46529
+ const seen = new Map;
46530
+ const published = new Map;
46531
+ const localHolder = endpoint.holder;
46532
+ function disposeState(value) {
46533
+ cleanBytes(value?.snapshot, value?.baseKey);
46534
+ }
46535
+ const alive = (holder) => !leaving.has(holder) && Date.now() - (seen.get(holder) || 0) < 30000;
46536
+ const replacementFor = (item) => [...pendingJoins.values()].find((packet) => packet.actor === item.actor && packet.holder !== item.holder && packet.data.replaces === item.holder && alive(packet.holder));
46537
+ const leader = () => {
46538
+ const candidates = head?.participants.filter((item) => alive(item.holder)) || [];
46539
+ return (candidates.filter((item) => !replacementFor(item)).length ? candidates.filter((item) => !replacementFor(item)) : candidates).map((item) => item.holder).sort()[0];
46540
+ };
46541
+ function admissionError(code, message) {
46542
+ return Object.assign(new Error(message), { code });
46543
+ }
46544
+ const cancelled2 = () => admissionError("calls/cancelled", "call cancelled");
46545
+ const empty = () => admissionError("calls/room-empty", "this call is empty");
46546
+ function settleAdmission(error) {
46547
+ if (error)
46548
+ admissionFailure ||= error;
46549
+ if (!admissionResult)
46550
+ return;
46551
+ const result = admissionResult;
46552
+ admissionResult = null;
46553
+ clearTimeout(admissionTimer);
46554
+ admissionTimer = null;
46555
+ if (error)
46556
+ result.reject(error);
46557
+ else
46558
+ result.resolve();
46559
+ }
46560
+ function checkAdmission() {
46561
+ if (closed || departing || !head)
46562
+ return;
46563
+ if (!joined && head.participants.every((item) => leaving.has(item.holder)))
46564
+ settleAdmission(empty());
46565
+ else if (joined && head.participants.some((item) => item.holder === localHolder && item.actor === admission.actor))
46566
+ settleAdmission();
46567
+ else if (!joined && head.participants.length >= CALL_MAX_PARTICIPANTS && !head.participants.some((item) => item.actor === admission.actor)) {
46568
+ settleAdmission(admissionError("calls/full", "this call is full"));
46569
+ } else if (!joined && submittedJoin && head.participants.some((item) => item.actor === admission.actor && item.holder !== localHolder && item.holder !== predecessorHolder)) {
46570
+ settleAdmission(admissionError("calls/taken-over", "call moved to another device"));
46571
+ }
46572
+ }
46573
+ async function adopt(next, admissions) {
46574
+ verifyCallMembers(next, admissions, protocol);
46575
+ if (closed) {
46576
+ disposeState(next);
46577
+ return;
46578
+ }
46579
+ const previous = state;
46580
+ state = next;
46581
+ disposeState(previous);
46582
+ const self = next.members.find((item) => toHex(item.leafId) === localHolder);
46583
+ if (!self) {
46584
+ onRemoved?.();
46585
+ return;
46586
+ }
46587
+ if (!admissions.some((item) => item.holder === localHolder && item.actor === admission.actor && item.endpoint === endpoint.publicKey))
46588
+ throw new Error("call endpoint identity mismatch");
46589
+ joined = true;
46590
+ await onKeys({ baseKey: next.baseKey, epoch: next.epoch, members: next.members, leafIndex: self.leafIndex });
46591
+ }
46592
+ function participants() {
46593
+ return (head?.participants || []).filter((item) => !leaving.has(item.holder)).map((item) => ({
46594
+ ...admittedIdentities.get(item.holder),
46595
+ holder: item.holder,
46596
+ muted: published.get(item.holder)?.muted === true || published.get(item.holder)?.deafened === true,
46597
+ deafened: published.get(item.holder)?.deafened === true,
46598
+ media: published.get(item.holder)?.media || null
46599
+ }));
46600
+ }
46601
+ function prune() {
46602
+ const admitted = new Set(head?.participants.map((item) => item.holder));
46603
+ for (const [holder, packet] of pendingJoins) {
46604
+ const predecessor = head?.participants.find((item) => item.actor === packet.actor);
46605
+ if (admitted.has(holder) || Date.now() - packet.at >= CALL_ADMISSION_TIMEOUT_MS || predecessor && packet.data.replaces !== predecessor.holder || !predecessor && packet.data.replaces)
46606
+ pendingJoins.delete(holder);
46607
+ }
46608
+ for (const holder of seen.keys())
46609
+ if (!admitted.has(holder) && !pendingJoins.has(holder))
46610
+ seen.delete(holder);
46611
+ for (const holder of published.keys())
46612
+ if (!admitted.has(holder))
46613
+ published.delete(holder);
46614
+ for (const holder of leaving)
46615
+ if (!admitted.has(holder))
46616
+ leaving.delete(holder);
46617
+ }
46618
+ async function consume(record) {
46619
+ if (closed)
46620
+ return;
46621
+ prune();
46622
+ const nextHead = record.head?.data;
46623
+ const signals = [];
46624
+ let membershipChanged = false;
46625
+ if (nextHead && (!Array.isArray(nextHead.participants) || nextHead.participants.length > CALL_MAX_PARTICIPANTS))
46626
+ throw new Error("invalid call room");
46627
+ for (const packet of record.events) {
46628
+ const roster = head?.participants || nextHead?.participants || [];
46629
+ const bound = roster.some((item) => item.holder === packet.holder && item.actor === packet.actor && item.endpoint === packet.endpoint);
46630
+ const pending = pendingJoins.get(packet.holder);
46631
+ const pendingBound = pending?.actor === packet.actor && pending?.endpoint === packet.endpoint;
46632
+ if (!["join", "replace"].includes(packet.kind) && !bound && !(packet.kind === "leave" && pendingBound))
46633
+ continue;
46634
+ if (packet.kind === "join" && roster.some((item) => item.holder === packet.holder) && !bound)
46635
+ continue;
46636
+ if (packet.kind === "join") {
46637
+ if (Date.now() - packet.at >= CALL_ADMISSION_TIMEOUT_MS)
46638
+ continue;
46639
+ const predecessor = roster.find((item) => item.actor === packet.actor && item.holder !== packet.holder);
46640
+ if (predecessor && packet.data.replaces !== predecessor.holder)
46641
+ continue;
46642
+ if (!predecessor && packet.data.replaces && !bound)
46643
+ continue;
46644
+ const previous = [...pendingJoins.values()].find((item) => item.actor === packet.actor && item.holder !== packet.holder);
46645
+ if (previous) {
46646
+ pendingJoins.delete(previous.holder);
46647
+ seen.delete(previous.holder);
46648
+ }
46649
+ if (!pendingJoins.has(packet.holder) && pendingJoins.size >= CALL_MAX_PARTICIPANTS)
46650
+ continue;
46651
+ pendingJoins.set(packet.holder, packet);
46652
+ } else if (packet.kind === "leave") {
46653
+ if (pendingBound)
46654
+ pendingJoins.delete(packet.holder);
46655
+ if (bound)
46656
+ leaving.add(packet.holder);
46657
+ } else if (packet.kind === "pulse") {
46658
+ published.set(packet.holder, packet.data);
46659
+ } else if (packet.kind === "signal") {
46660
+ if (packet.data.to === localHolder && head?.participants.some((item) => item.holder === packet.holder && item.actor === packet.actor)) {
46661
+ signals.push(packet);
46662
+ }
46663
+ } else if (packet.kind === "replace") {
46664
+ const replacement = verifyCallReplacement(packet, head, protocol);
46665
+ membershipChanged = true;
46666
+ if (state && !replacement.participants.some((item) => item.holder === localHolder)) {
46667
+ settleAdmission(cancelled2());
46668
+ onRemoved?.();
46669
+ return;
46670
+ }
46671
+ head = replacement;
46672
+ } else if (packet.kind === "change") {
46673
+ membershipChanged = true;
46674
+ const change = packet.data;
46675
+ if (!Array.isArray(change.participants) || !Number.isSafeInteger(change.epoch))
46676
+ throw new Error("invalid call epoch");
46677
+ if (state && change.epoch === state.epoch + 1) {
46678
+ if (!bound)
46679
+ throw new Error("unadmitted call commit");
46680
+ const next = pendingCandidate?.commit === change.commit ? pendingCandidate.state : await mls.processCommit(state.snapshot, decodeCallBytes(change.commit));
46681
+ pendingCandidate = null;
46682
+ if (next.removed) {
46683
+ disposeState(next);
46684
+ onRemoved?.();
46685
+ return;
46686
+ }
46687
+ await adopt(next, change.participants);
46688
+ } else if (!state && change.participants.some((item) => item.holder === localHolder && item.actor === admission.actor && item.endpoint === endpoint.publicKey)) {
46689
+ const next = await mls.join(member.snapshot, decodeCallBytes(change.welcome), decodeCallBytes(change.tree), fromHexBytes(callId));
46690
+ await adopt(next, change.participants);
46691
+ }
46692
+ head = { epoch: change.epoch, participants: change.participants };
46693
+ for (const item of change.participants) {
46694
+ pendingJoins.delete(item.holder);
46695
+ if (!seen.has(item.holder))
46696
+ seen.set(item.holder, packet.at);
46697
+ }
46698
+ }
46699
+ seen.set(packet.holder, Math.max(seen.get(packet.holder) || 0, packet.at));
46700
+ }
46701
+ if (!state && pendingInitial && nextHead?.epoch === 0 && nextHead.participants.length === 1 && nextHead.participants[0].signature === pendingInitial.admission.signature) {
46702
+ const initial = pendingInitial.state;
46703
+ pendingInitial = null;
46704
+ await adopt(initial, nextHead.participants);
46705
+ }
46706
+ if (nextHead && (verifiedHead !== record.head || membershipChanged)) {
46707
+ admittedIdentities = state ? verifyCallMembers(state, nextHead.participants, protocol) : verifyCallAdmissions(nextHead.participants, protocol);
46708
+ for (const item of nextHead.participants) {
46709
+ if (!seen.has(item.holder))
46710
+ seen.set(item.holder, record.head.at);
46711
+ }
46712
+ if (state && nextHead.epoch !== state.epoch)
46713
+ throw new Error("call epoch history unavailable");
46714
+ head = nextHead;
46715
+ verifiedHead = record.head;
46716
+ } else if (!nextHead) {
46717
+ head = null;
46718
+ verifiedHead = null;
46719
+ admittedIdentities.clear();
46720
+ if (joined) {
46721
+ onRemoved?.();
46722
+ return;
46723
+ }
46724
+ if (submittedJoin)
46725
+ settleAdmission(empty());
46726
+ }
46727
+ prune();
46728
+ if (closed)
46729
+ return;
46730
+ onParticipants?.(participants());
46731
+ checkAdmission();
46732
+ for (const packet of signals) {
46733
+ try {
46734
+ Promise.resolve(onSignal?.(packet.data.signal, packet.holder)).catch(fail);
46735
+ } catch (error) {
46736
+ fail(error);
46737
+ }
46738
+ }
46739
+ reconcile().catch(fail);
46740
+ }
46741
+ function fail(error) {
46742
+ if (!closed) {
46743
+ if (admissionResult)
46744
+ settleAdmission(error);
46745
+ onError?.(error);
46746
+ }
46747
+ }
46748
+ const mailbox = openCallMailbox({ cloud, capability, protocol, endpoint, onChange: consume, onError: fail });
46749
+ async function reconcile() {
46750
+ reconcileAgain = true;
46751
+ if (changing)
46752
+ return;
46753
+ changing = true;
46754
+ try {
46755
+ do {
46756
+ reconcileAgain = false;
46757
+ await reconcilePass();
46758
+ } while (reconcileAgain && !closed);
46759
+ } finally {
46760
+ changing = false;
46761
+ }
46762
+ }
46763
+ async function reconcilePass() {
46764
+ if (closed || departing || !state || leader() !== localHolder)
46765
+ return;
46766
+ const current = mailbox.getSnapshot();
46767
+ if (!current || !head)
46768
+ return;
46769
+ const removals = head.participants.filter((item) => item.holder !== localHolder && (!alive(item.holder) || replacementFor(item)));
46770
+ const retained = head.participants.filter((item) => !removals.includes(item));
46771
+ const admittedActors = new Set(retained.map((item) => item.actor));
46772
+ const additions = [...pendingJoins.values()].filter((item) => {
46773
+ if (head.participants.some((existing) => existing.holder === item.holder) || admittedActors.has(item.actor) || !alive(item.holder))
46774
+ return false;
46775
+ admittedActors.add(item.actor);
46776
+ return true;
46777
+ }).slice(0, CALL_MAX_PARTICIPANTS - retained.length);
46778
+ if (!removals.length && !additions.length)
46779
+ return;
46780
+ let candidate;
46781
+ try {
46782
+ for (const item of additions)
46783
+ protocol.verify(item);
46784
+ candidate = await mls.stageChange(state.snapshot, {
46785
+ adds: additions.map((item) => decodeCallBytes(item.data.keyPackage)),
46786
+ removes: removals.map((item) => fromHexBytes(item.holder))
46787
+ });
46788
+ if (closed || departing) {
46789
+ disposeState(candidate);
46790
+ return;
46791
+ }
46792
+ const admissions = [...retained, ...additions];
46793
+ verifyCallMembers(candidate, admissions, protocol);
46794
+ const change = {
46795
+ epoch: candidate.epoch,
46796
+ participants: admissions,
46797
+ commit: encodeCallBytes(candidate.commit),
46798
+ welcome: encodeCallBytes(candidate.welcome),
46799
+ tree: encodeCallBytes(candidate.tree)
46800
+ };
46801
+ pendingCandidate = { commit: change.commit, state: candidate };
46802
+ await mailbox.commit({ epoch: candidate.epoch, participants: admissions }, [protocol.sign("change", change)], { expectedRevision: current.revision });
46803
+ candidate = null;
46804
+ } catch (error) {
46805
+ if (pendingCandidate?.state === candidate)
46806
+ pendingCandidate = null;
46807
+ disposeState(candidate);
46808
+ if (String(error.code).endsWith("conflict")) {
46809
+ await mailbox.read();
46810
+ return;
46811
+ }
46812
+ throw error;
46813
+ }
46814
+ }
46815
+ function event(kind, data) {
46816
+ const result = eventTail.then(() => appendEvent(kind, data));
46817
+ eventTail = result.catch(() => {});
46818
+ return result;
46819
+ }
46820
+ async function appendEvent(kind, data) {
46821
+ const deadline = performance.now() + CALL_ADMISSION_TIMEOUT_MS;
46822
+ for (let attempt = 0;!closed && performance.now() < deadline; attempt += 1) {
46823
+ if (kind === "join" && departing)
46824
+ throw cancelled2();
46825
+ const current = mailbox.getSnapshot();
46826
+ if (!current?.head)
46827
+ throw kind === "join" ? empty() : new Error("call ended");
46828
+ if (kind === "join") {
46829
+ checkAdmission();
46830
+ if (admissionFailure)
46831
+ throw admissionFailure;
46832
+ }
46833
+ try {
46834
+ if (kind === "join")
46835
+ submittedJoin = true;
46836
+ return await mailbox.append([protocol.sign(kind, data)], { expectedRevision: current.revision });
46837
+ } catch (error) {
46838
+ if (closed)
46839
+ throw cancelled2();
46840
+ if (!String(error.code).endsWith("conflict"))
46841
+ throw error;
46842
+ const delay = Math.min(250, 25 * 2 ** Math.min(attempt, 4)) * (0.5 + Math.random() / 2);
46843
+ await new Promise((resolve) => {
46844
+ const timer = setTimeout(() => {
46845
+ retryWait = null;
46846
+ resolve();
46847
+ }, delay);
46848
+ retryWait = () => {
46849
+ clearTimeout(timer);
46850
+ retryWait = null;
46851
+ resolve();
46852
+ };
46853
+ });
46854
+ if (closed)
46855
+ throw cancelled2();
46856
+ await mailbox.read();
46857
+ }
46858
+ }
46859
+ if (closed)
46860
+ throw cancelled2();
46861
+ throw admissionError("calls/signaling-conflict", "call changed, try again");
46862
+ }
46863
+ async function pulse() {
46864
+ if (closed || departing || !joined)
46865
+ return;
46866
+ try {
46867
+ await event("pulse", { ...audio, media: ownMedia });
46868
+ } catch (error) {
46869
+ fail(error);
46870
+ }
46871
+ if (!closed && !departing)
46872
+ pulseTimer = setTimeout(pulse, 1e4);
46873
+ }
46874
+ async function beginStart(create) {
46875
+ const record = await mailbox.read();
46876
+ if (closed || departing)
46877
+ throw cancelled2();
46878
+ if (!create && !record?.head)
46879
+ throw empty();
46880
+ checkAdmission();
46881
+ if (admissionFailure)
46882
+ throw admissionFailure;
46883
+ const solePredecessor = record?.head?.data.participants.length === 1 && record.head.data.participants[0].actor === admission.actor && record.head.data.participants[0].holder !== localHolder;
46884
+ if (create || solePredecessor) {
46885
+ if (create && record?.head)
46886
+ throw new Error("call already exists");
46887
+ let initialMember = member;
46888
+ let initialAdmission = admission;
46889
+ if (solePredecessor)
46890
+ initialMember = await mls.createMember(fromHexBytes(localHolder));
46891
+ let initial;
46892
+ try {
46893
+ if (closed || departing)
46894
+ throw cancelled2();
46895
+ if (solePredecessor)
46896
+ initialAdmission = protocol.sign("join", {
46897
+ signaturePK: toHex(initialMember.signaturePK),
46898
+ keyPackage: "",
46899
+ replaces: record.head.data.participants[0].holder
46900
+ });
46901
+ initial = await mls.createGroup(initialMember.snapshot, fromHexBytes(callId));
46902
+ } finally {
46903
+ if (solePredecessor)
46904
+ disposeState(initialMember);
46905
+ }
46906
+ if (closed || departing) {
46907
+ disposeState(initial);
46908
+ throw cancelled2();
46909
+ }
46910
+ if (admissionFailure) {
46911
+ disposeState(initial);
46912
+ throw admissionFailure;
46913
+ }
46914
+ pendingInitial = { state: initial, admission: initialAdmission };
46915
+ const initialHead = { epoch: initial.epoch, participants: [initialAdmission] };
46916
+ const packet = solePredecessor ? protocol.sign("replace", { previous: record.head, participant: initialAdmission }) : protocol.sign("pulse", { ...audio, media: null });
46917
+ submittedJoin = true;
46918
+ await mailbox.commit(initialHead, [packet], { expectedRevision: record.revision });
46919
+ } else {
46920
+ const predecessor = head.participants.find((item) => item.actor === admission.actor);
46921
+ predecessorHolder = predecessor?.holder || null;
46922
+ await event("join", { ...admission.data, replaces: predecessor?.holder || null });
46923
+ checkAdmission();
46924
+ }
46925
+ }
46926
+ return Object.freeze({
46927
+ start({ create = false } = {}) {
46928
+ if (startPromise)
46929
+ return startPromise;
46930
+ const admitted = new Promise((resolve, reject) => {
46931
+ admissionResult = { resolve, reject };
46932
+ });
46933
+ admissionTimer = setTimeout(() => settleAdmission(admissionError("calls/admission-timeout", "could not join the call")), CALL_ADMISSION_TIMEOUT_MS);
46934
+ startTask = beginStart(create);
46935
+ startPromise = Promise.race([startTask.then(() => admitted), admitted]).then(() => {
46936
+ if (closed || departing)
46937
+ throw cancelled2();
46938
+ pulseTimer = setTimeout(pulse, 1e4);
46939
+ }).catch((error) => {
46940
+ settleAdmission(error);
46941
+ throw error;
46942
+ });
46943
+ return startPromise;
46944
+ },
46945
+ signal(to, signal) {
46946
+ return event("signal", { to, signal });
46947
+ },
46948
+ async publishMedia(media) {
46949
+ ownMedia = media;
46950
+ await event("pulse", { ...audio, media });
46951
+ },
46952
+ async setAudio(value) {
46953
+ audio = { muted: value.muted === true || value.deafened === true, deafened: value.deafened === true };
46954
+ if (joined && !departing && !closed)
46955
+ await event("pulse", { ...audio, media: ownMedia });
46956
+ },
46957
+ async leave() {
46958
+ if (closed)
46959
+ return { ended: false };
46960
+ departing = true;
46961
+ settleAdmission(cancelled2());
46962
+ clearTimeout(pulseTimer);
46963
+ await startTask?.catch(() => {});
46964
+ if (closed || !joined && !submittedJoin || !mailbox.getSnapshot()?.head)
46965
+ return { ended: false };
46966
+ await event("leave", {});
46967
+ await mailbox.read();
46968
+ return { ended: !!head && head.participants.every((item) => !alive(item.holder)) };
46969
+ },
46970
+ close() {
46971
+ if (closed)
46972
+ return;
46973
+ closed = true;
46974
+ retryWait?.();
46975
+ settleAdmission(cancelled2());
46976
+ clearTimeout(pulseTimer);
46977
+ mailbox.close();
46978
+ disposeState(state);
46979
+ disposeState(member);
46980
+ disposeState(pendingCandidate?.state);
46981
+ disposeState(pendingInitial?.state);
46982
+ pendingJoins.clear();
46983
+ published.clear();
46984
+ seen.clear();
46985
+ }
46986
+ });
46987
+ }
46988
+
46989
+ // ../../core/calls/activity.js
46990
+ var positive = (value) => Number.isFinite(value) && value >= CALL_SPEAKING_THRESHOLD && value <= 1;
46991
+ function createCallActivity({ holder, onSpeaking, now = Date.now, setTimer = setTimeout, clearTimer = clearTimeout }) {
46992
+ let closed = false;
46993
+ let timer = null;
46994
+ let local = false;
46995
+ let direct = null;
46996
+ let routes = new Map;
46997
+ let sources = new Map;
46998
+ let published = [];
46999
+ const speaking = new Map;
47000
+ function publish() {
47001
+ const next = [...speaking.keys()].sort();
47002
+ if (published.length === next.length && published.every((value, index) => value === next[index]))
47003
+ return;
47004
+ published = next;
47005
+ onSpeaking?.(next);
47006
+ }
47007
+ function schedule() {
47008
+ clearTimer(timer);
47009
+ timer = null;
47010
+ if (!closed && speaking.size)
47011
+ timer = setTimer(expire, Math.max(0, Math.min(...speaking.values()) - now()));
47012
+ }
47013
+ function expire() {
47014
+ timer = null;
47015
+ if (closed)
47016
+ return;
47017
+ const time = now();
47018
+ for (const [id, deadline] of speaking)
47019
+ if (deadline <= time)
47020
+ speaking.delete(id);
47021
+ publish();
47022
+ schedule();
47023
+ }
47024
+ function update({ peers, received, directPeer, mode, muted, deafened }) {
47025
+ if (closed)
47026
+ return;
47027
+ const admitted = new Map(peers.map((peer) => [peer.holder, peer]));
47028
+ local = admitted.has(holder) && !muted && !deafened;
47029
+ direct = mode === "direct" && directPeer !== holder && admitted.has(directPeer) && !admitted.get(directPeer).muted ? directPeer : null;
47030
+ const nextSources = new Map(local ? [[holder, "local"]] : []);
47031
+ const nextRoutes2 = new Map;
47032
+ const ambiguous = new Set;
47033
+ if (direct)
47034
+ nextSources.set(direct, "direct");
47035
+ if (mode === "group") {
47036
+ for (const [id, mid] of received) {
47037
+ if (id === holder || !admitted.has(id) || admitted.get(id).muted || typeof mid !== "string" || !mid)
47038
+ continue;
47039
+ if (nextRoutes2.has(mid)) {
47040
+ nextSources.delete(nextRoutes2.get(mid));
47041
+ nextRoutes2.delete(mid);
47042
+ ambiguous.add(mid);
47043
+ }
47044
+ if (ambiguous.has(mid))
47045
+ continue;
47046
+ nextRoutes2.set(mid, id);
47047
+ nextSources.set(id, mid);
47048
+ }
47049
+ }
47050
+ const time = now();
47051
+ for (const [id, deadline] of speaking) {
47052
+ if (deadline <= time || !nextSources.has(id) || nextSources.get(id) !== sources.get(id))
47053
+ speaking.delete(id);
47054
+ }
47055
+ routes = nextRoutes2;
47056
+ sources = nextSources;
47057
+ publish();
47058
+ schedule();
47059
+ }
47060
+ function sample(value) {
47061
+ if (closed)
47062
+ return;
47063
+ const time = now();
47064
+ for (const [id, deadline] of speaking)
47065
+ if (deadline <= time)
47066
+ speaking.delete(id);
47067
+ if (local && positive(value.local))
47068
+ speaking.set(holder, time + CALL_SPEAKING_HOLD_MS);
47069
+ for (const item of value.receive) {
47070
+ if (typeof item.mid !== "string" || !item.mid || !positive(item.level))
47071
+ continue;
47072
+ const id = direct || routes.get(item.mid);
47073
+ if (id)
47074
+ speaking.set(id, time + CALL_SPEAKING_HOLD_MS);
47075
+ }
47076
+ publish();
47077
+ schedule();
47078
+ }
47079
+ function reset() {
47080
+ clearTimer(timer);
47081
+ timer = null;
47082
+ speaking.clear();
47083
+ publish();
47084
+ }
47085
+ function close() {
47086
+ if (closed)
47087
+ return;
47088
+ closed = true;
47089
+ reset();
47090
+ local = false;
47091
+ direct = null;
47092
+ routes.clear();
47093
+ sources.clear();
47094
+ }
47095
+ return Object.freeze({ update, sample, reset, close });
47096
+ }
47097
+
47098
+ // ../../core/calls/media.js
47099
+ var sdp = (value) => ({ type: value.type, sdp: value.sdp });
47100
+ var validMid = (value) => typeof value === "string" && /^[A-Za-z0-9_-]{1,32}$/u.test(value);
47101
+ var MAX_PENDING_SIGNALS = 128;
47102
+ var CONNECTION_TIMEOUT_MS = 15000;
47103
+ var retiredConnection = new Error("call connection retired");
47104
+ function createCallMediaSession({ port, mode, holder, provider, signal, publish, onState, onError, onExpired, onSpeaking }) {
47105
+ let media = null;
47106
+ let closed = false;
47107
+ let deadline = null;
47108
+ let keys = null;
47109
+ let peers = [];
47110
+ let sessionId = null;
47111
+ let started = false;
47112
+ let directPeer = null;
47113
+ let offered = false;
47114
+ let muted = false;
47115
+ let deafened = false;
47116
+ let microphoneVolume = 1;
47117
+ let closing = null;
47118
+ let connectionState = "new";
47119
+ let connectionWait = null;
47120
+ let tail = Promise.resolve();
47121
+ let connectionGeneration = 0;
47122
+ let openingConnection = null;
47123
+ let retiringConnection = Promise.resolve();
47124
+ let iceServers = null;
47125
+ const received = new Map;
47126
+ const peerAudio = new Map;
47127
+ const pendingPeerAudio = new Map;
47128
+ const activity = createCallActivity({ holder, onSpeaking });
47129
+ const updateActivity = () => activity.update({ peers, received, directPeer, mode, muted, deafened });
47130
+ const pendingSignals = [];
47131
+ const incomingSignals = [];
47132
+ function settlePeerAudio(id, error) {
47133
+ const pending = pendingPeerAudio.get(id);
47134
+ if (!pending)
47135
+ return;
47136
+ pendingPeerAudio.delete(id);
47137
+ if (error)
47138
+ pending.reject(error);
47139
+ else
47140
+ pending.resolve();
47141
+ }
47142
+ function current(generation = connectionGeneration) {
47143
+ if (closed)
47144
+ throw new Error("call media closed");
47145
+ if (generation !== connectionGeneration)
47146
+ throw retiredConnection;
47147
+ }
47148
+ async function disposeConnection(previous) {
47149
+ const results = await Promise.allSettled([() => previous?.revoke(), () => previous?.close()].map((operation) => {
47150
+ try {
47151
+ return Promise.resolve(operation());
47152
+ } catch (error) {
47153
+ return Promise.reject(error);
47154
+ }
47155
+ }));
47156
+ const failed = results.find((result) => result.status === "rejected");
47157
+ if (failed)
47158
+ throw failed.reason;
47159
+ }
47160
+ function retireDirectConnection(nextPeer) {
47161
+ connectionGeneration++;
47162
+ const previous = media;
47163
+ media = null;
47164
+ received.clear();
47165
+ directPeer = nextPeer;
47166
+ activity.reset();
47167
+ updateActivity();
47168
+ offered = false;
47169
+ connectionState = "new";
47170
+ pendingSignals.length = 0;
47171
+ const opening = openingConnection;
47172
+ retiringConnection = Promise.all([
47173
+ retiringConnection,
47174
+ disposeConnection(previous),
47175
+ opening?.then(disposeConnection, () => {})
47176
+ ]).then(() => {});
47177
+ retiringConnection.catch(() => {});
47178
+ onState?.({ state: "connecting" });
47179
+ }
47180
+ function settleConnection(error) {
47181
+ if (!connectionWait)
47182
+ return;
47183
+ const wait2 = connectionWait;
47184
+ connectionWait = null;
47185
+ clearTimeout(wait2.timer);
47186
+ if (error)
47187
+ wait2.reject(error);
47188
+ else
47189
+ wait2.resolve();
47190
+ }
47191
+ function connected() {
47192
+ current();
47193
+ if (connectionState === "connected")
47194
+ return Promise.resolve();
47195
+ if (["failed", "closed"].includes(connectionState))
47196
+ return Promise.reject(new Error("call connection failed"));
47197
+ if (!connectionWait) {
47198
+ const wait2 = {};
47199
+ wait2.promise = new Promise((resolve, reject) => {
47200
+ wait2.resolve = resolve;
47201
+ wait2.reject = reject;
47202
+ });
47203
+ wait2.timer = setTimeout(() => settleConnection(new Error("call connection timed out")), CONNECTION_TIMEOUT_MS);
47204
+ connectionWait = wait2;
47205
+ }
47206
+ return connectionWait.promise;
47207
+ }
47208
+ function serialize(work) {
47209
+ const result = tail.then(() => {
47210
+ current();
47211
+ return work();
47212
+ });
47213
+ tail = result.catch((error) => {
47214
+ if (!closed)
47215
+ onError?.(error);
47216
+ });
47217
+ return result;
47218
+ }
47219
+ function close() {
47220
+ if (closed)
47221
+ return closing;
47222
+ closed = true;
47223
+ activity.close();
47224
+ connectionGeneration++;
47225
+ settleConnection(new Error("call media closed"));
47226
+ const previous = media;
47227
+ media = keys = deadline = iceServers = null;
47228
+ received.clear();
47229
+ peerAudio.clear();
47230
+ for (const id of pendingPeerAudio.keys())
47231
+ settlePeerAudio(id, new Error("call media closed"));
47232
+ pendingSignals.length = incomingSignals.length = 0;
47233
+ const operations = [disposeConnection(previous), retiringConnection];
47234
+ closing = Promise.allSettled(operations).then((results) => {
47235
+ const failed = results.find((result) => result.status === "rejected");
47236
+ if (failed)
47237
+ throw failed.reason;
47238
+ });
47239
+ return closing;
47240
+ }
47241
+ async function openConnection() {
47242
+ const generation = connectionGeneration;
47243
+ const live = () => !closed && generation === connectionGeneration;
47244
+ await retiringConnection;
47245
+ current(generation);
47246
+ if (!iceServers) {
47247
+ const configuration = await provider("turn", {});
47248
+ current(generation);
47249
+ iceServers = configuration.iceServers;
47250
+ }
47251
+ const opening = port.open({
47252
+ id: holder,
47253
+ mode,
47254
+ iceServers,
47255
+ onState: (value) => {
47256
+ if (!live())
47257
+ return;
47258
+ connectionState = value.state;
47259
+ if (connectionState === "connected")
47260
+ settleConnection();
47261
+ else if (["failed", "closed"].includes(connectionState))
47262
+ settleConnection(new Error("call connection failed"));
47263
+ onState?.(value);
47264
+ },
47265
+ onExpired: () => {
47266
+ if (live())
47267
+ onExpired?.();
47268
+ },
47269
+ onSignal(value) {
47270
+ if (mode !== "direct" || !live())
47271
+ return;
47272
+ if (directPeer)
47273
+ signal(directPeer, value).catch((error) => {
47274
+ if (live())
47275
+ onError?.(error);
47276
+ });
47277
+ else if (pendingSignals.length < MAX_PENDING_SIGNALS)
47278
+ pendingSignals.push(value);
47279
+ else
47280
+ onError?.(new Error("too many pending media signals"));
47281
+ },
47282
+ onTrack({ mid }) {
47283
+ if (!live())
47284
+ return;
47285
+ if (mode === "direct" && directPeer && validMid(mid))
47286
+ received.set(directPeer, mid);
47287
+ Promise.all([updateKeys(), applyPeerAudio()]).catch((error) => {
47288
+ if (live())
47289
+ onError?.(error);
47290
+ });
47291
+ },
47292
+ onActivity(value) {
47293
+ if (live())
47294
+ activity.sample(value);
47295
+ }
47296
+ });
47297
+ openingConnection = opening;
47298
+ let opened;
47299
+ try {
47300
+ opened = await opening;
47301
+ } finally {
47302
+ if (openingConnection === opening)
47303
+ openingConnection = null;
47304
+ }
47305
+ if (!live()) {
47306
+ await disposeConnection(opened);
47307
+ current(generation);
47308
+ }
47309
+ media = opened;
47310
+ await opened.mute(muted);
47311
+ current(generation);
47312
+ await opened.deafen(deafened);
47313
+ current(generation);
47314
+ if (microphoneVolume !== 1) {
47315
+ await opened.setMicrophoneVolume(microphoneVolume);
47316
+ current(generation);
47317
+ }
47318
+ await opened.prepare();
47319
+ current(generation);
47320
+ if (deadline) {
47321
+ await opened.grant(deadline);
47322
+ current(generation);
47323
+ }
47324
+ await updateKeys();
47325
+ current(generation);
47326
+ }
47327
+ async function applyDescription(result, required = false) {
47328
+ current();
47329
+ if (!result || result.errorCode || result.tracks?.some((track) => track.errorCode))
47330
+ throw new Error("call negotiation failed");
47331
+ const description = result.sessionDescription;
47332
+ if (!description) {
47333
+ if (required || result.requiresImmediateRenegotiation)
47334
+ throw new Error("missing call description");
47335
+ return;
47336
+ }
47337
+ if (!["offer", "answer"].includes(description.type) || typeof description.sdp !== "string" || !description.sdp.length)
47338
+ throw new Error("invalid call description");
47339
+ if (description.type === "offer") {
47340
+ const answer = await media.answer(description);
47341
+ current();
47342
+ await provider("renegotiate", { sessionId, sessionDescription: sdp(answer) });
47343
+ current();
47344
+ } else {
47345
+ await media.remote(description);
47346
+ current();
47347
+ }
47348
+ }
47349
+ async function updateKeys() {
47350
+ if (!media || !keys || mode !== "group")
47351
+ return;
47352
+ const receive = [];
47353
+ for (const peer of peers) {
47354
+ const mid = received.get(peer.holder);
47355
+ const member = keys.members.find((item) => toHex(item.leafId) === peer.holder);
47356
+ if (mid != null && member)
47357
+ receive.push({
47358
+ mid,
47359
+ baseKey: keys.baseKey,
47360
+ epoch: keys.epoch,
47361
+ leafIndex: member.leafIndex,
47362
+ contextId: 0
47363
+ });
47364
+ }
47365
+ await media.setKeys({ send: { baseKey: keys.baseKey, epoch: keys.epoch, leafIndex: keys.leafIndex, contextId: 0 }, receive });
47366
+ }
47367
+ function applyPeerAudio() {
47368
+ return Promise.all([...peerAudio].map(async ([id, entry]) => {
47369
+ const mid = received.get(id);
47370
+ if (!media || mid == null)
47371
+ return;
47372
+ const value = entry.value;
47373
+ try {
47374
+ await media.setPeerAudio({ mid, ...value });
47375
+ settlePeerAudio(id);
47376
+ } catch (error) {
47377
+ if (pendingPeerAudio.has(id))
47378
+ settlePeerAudio(id, error);
47379
+ else
47380
+ throw error;
47381
+ }
47382
+ }));
47383
+ }
47384
+ async function negotiate() {
47385
+ if (!started || !keys)
47386
+ return;
47387
+ if (mode === "group") {
47388
+ if (!sessionId) {
47389
+ const session = await provider("session/new", {});
47390
+ current();
47391
+ sessionId = session.sessionId;
47392
+ const offer = await media.offer();
47393
+ current();
47394
+ const sendMid = media.sendMid;
47395
+ if (!validMid(sendMid))
47396
+ throw new Error("microphone track unavailable");
47397
+ const trackName = toHex(randomBytes3(16));
47398
+ const result = await provider("tracks/new", {
47399
+ sessionId,
47400
+ sessionDescription: sdp(offer),
47401
+ tracks: [{ location: "local", mid: sendMid, trackName }]
47402
+ });
47403
+ current();
47404
+ if (!Array.isArray(result.tracks) || result.tracks.length !== 1 || result.tracks[0].mid !== sendMid || result.tracks[0].trackName !== trackName)
47405
+ throw new Error("microphone publication failed");
47406
+ await applyDescription(result, true);
47407
+ current();
47408
+ await connected();
47409
+ current();
47410
+ await publish({ sessionId, trackName });
47411
+ current();
47412
+ }
47413
+ const additions = peers.filter((peer) => peer.holder !== holder && peer.media?.sessionId && peer.media?.trackName && !received.has(peer.holder));
47414
+ if (additions.length) {
47415
+ await connected();
47416
+ current();
47417
+ const result = await provider("tracks/new", { sessionId, tracks: additions.map((peer) => ({
47418
+ location: "remote",
47419
+ sessionId: peer.media.sessionId,
47420
+ trackName: peer.media.trackName
47421
+ })) });
47422
+ current();
47423
+ if (!Array.isArray(result.tracks) || result.tracks.length !== additions.length)
47424
+ throw new Error("peer audio subscription failed");
47425
+ const mappings = [];
47426
+ const occupied = new Set([media.sendMid, ...received.values()]);
47427
+ for (const peer of additions) {
47428
+ const track = result.tracks?.find((item) => item.sessionId === peer.media.sessionId && item.trackName === peer.media.trackName);
47429
+ if (!validMid(track?.mid) || track.errorCode || occupied.has(track.mid))
47430
+ throw new Error("peer audio subscription failed");
47431
+ occupied.add(track.mid);
47432
+ mappings.push([peer.holder, track.mid]);
47433
+ }
47434
+ for (const [peer, mid] of mappings)
47435
+ received.set(peer, mid);
47436
+ updateActivity();
47437
+ await applyPeerAudio();
47438
+ current();
47439
+ await updateKeys();
47440
+ current();
47441
+ await applyDescription(result);
47442
+ }
47443
+ const removals = [...received].filter(([peer]) => !peers.some((item) => item.holder === peer));
47444
+ if (removals.length) {
47445
+ for (const [peer] of removals)
47446
+ received.delete(peer);
47447
+ updateActivity();
47448
+ await updateKeys();
47449
+ current();
47450
+ const result = await provider("tracks/close", { sessionId, tracks: removals.map(([, mid]) => ({ mid })) });
47451
+ await applyDescription(result);
47452
+ }
47453
+ } else {
47454
+ const peer = peers.find((item) => item.holder !== holder);
47455
+ if (!peer)
47456
+ return;
47457
+ directPeer = peer.holder;
47458
+ updateActivity();
47459
+ const generation = connectionGeneration;
47460
+ try {
47461
+ if (!media)
47462
+ await openConnection();
47463
+ current(generation);
47464
+ if (holder < peer.holder && !offered) {
47465
+ offered = true;
47466
+ const offer = await media.offer();
47467
+ current(generation);
47468
+ await signal(peer.holder, { type: "description", description: sdp(offer) });
47469
+ current(generation);
47470
+ }
47471
+ } catch (error) {
47472
+ if (!closed && generation !== connectionGeneration)
47473
+ return;
47474
+ await close();
47475
+ throw error;
47476
+ }
47477
+ }
47478
+ }
47479
+ return Object.freeze({
47480
+ async open() {
47481
+ current();
47482
+ try {
47483
+ await openConnection();
47484
+ started = true;
47485
+ await serialize(negotiate);
47486
+ for (const [value, from] of incomingSignals.splice(0))
47487
+ await handleSignal(value, from);
47488
+ } catch (error) {
47489
+ await close();
47490
+ throw error;
47491
+ }
47492
+ },
47493
+ grant(value) {
47494
+ current();
47495
+ deadline = value;
47496
+ return media?.grant(value);
47497
+ },
47498
+ async setKeys(value) {
47499
+ current();
47500
+ keys = value;
47501
+ await updateKeys();
47502
+ current();
47503
+ return serialize(negotiate);
47504
+ },
47505
+ async setParticipants(value) {
47506
+ current();
47507
+ peers = value;
47508
+ for (const id of peerAudio.keys())
47509
+ if (!peers.some((peer) => peer.holder === id)) {
47510
+ peerAudio.delete(id);
47511
+ settlePeerAudio(id, new Error("call participant unavailable"));
47512
+ }
47513
+ updateActivity();
47514
+ const nextPeer = peers.find((peer) => peer.holder !== holder)?.holder || null;
47515
+ if (mode === "direct" && started && directPeer && nextPeer !== directPeer)
47516
+ retireDirectConnection(nextPeer);
47517
+ await updateKeys();
47518
+ current();
47519
+ return serialize(negotiate);
47520
+ },
47521
+ signal(value, from) {
47522
+ if (closed)
47523
+ return Promise.reject(new Error("call media closed"));
47524
+ if (!started) {
47525
+ if (incomingSignals.length >= MAX_PENDING_SIGNALS)
47526
+ return Promise.reject(new Error("too many pending media signals"));
47527
+ incomingSignals.push([value, from]);
47528
+ return Promise.resolve();
47529
+ }
47530
+ return handleSignal(value, from);
47531
+ },
47532
+ mute(value) {
47533
+ current();
47534
+ muted = value === true;
47535
+ updateActivity();
47536
+ return media?.mute(muted);
47537
+ },
47538
+ deafen(value) {
47539
+ current();
47540
+ deafened = value === true;
47541
+ updateActivity();
47542
+ return media?.deafen(deafened);
47543
+ },
47544
+ async setMicrophoneVolume(value) {
47545
+ current();
47546
+ const previous = microphoneVolume;
47547
+ microphoneVolume = value;
47548
+ try {
47549
+ if (media)
47550
+ await media.setMicrophoneVolume(value);
47551
+ } catch (error) {
47552
+ if (microphoneVolume === value)
47553
+ microphoneVolume = previous;
47554
+ throw error;
47555
+ }
47556
+ },
47557
+ async setPeerAudio(id, value) {
47558
+ current();
47559
+ const mid = received.get(id);
47560
+ if (id === holder || !peers.some((peer) => peer.holder === id))
47561
+ throw new Error("call participant unavailable");
47562
+ let entry = peerAudio.get(id);
47563
+ if (!entry) {
47564
+ entry = { value: null, applied: null };
47565
+ peerAudio.set(id, entry);
47566
+ }
47567
+ entry.value = value;
47568
+ try {
47569
+ if (media && mid != null)
47570
+ await media.setPeerAudio({ mid, ...value });
47571
+ else {
47572
+ let pending = pendingPeerAudio.get(id);
47573
+ if (!pending) {
47574
+ pending = {};
47575
+ pending.promise = new Promise((resolve, reject) => {
47576
+ pending.resolve = resolve;
47577
+ pending.reject = reject;
47578
+ });
47579
+ pendingPeerAudio.set(id, pending);
47580
+ }
47581
+ await pending.promise;
47582
+ }
47583
+ if (peerAudio.get(id) === entry && entry.value === value)
47584
+ entry.applied = value;
47585
+ } catch (error) {
47586
+ if (peerAudio.get(id) === entry && entry.value === value) {
47587
+ if (entry.applied)
47588
+ entry.value = entry.applied;
47589
+ else
47590
+ peerAudio.delete(id);
47591
+ }
47592
+ throw error;
47593
+ }
47594
+ },
47595
+ close
47596
+ });
47597
+ function handleSignal(value, from) {
47598
+ return serialize(async () => {
47599
+ if (mode !== "direct")
47600
+ throw new Error("unadmitted media signal");
47601
+ if (from === holder || !peers.some((peer) => peer.holder === from))
47602
+ return;
47603
+ const generation = connectionGeneration;
47604
+ try {
47605
+ await negotiate();
47606
+ current(generation);
47607
+ if (value.type === "description") {
47608
+ if (value.description.type === "offer") {
47609
+ if (holder < from)
47610
+ throw new Error("unexpected direct offer");
47611
+ offered = true;
47612
+ const answer = await media.answer(value.description);
47613
+ current(generation);
47614
+ await signal(from, { type: "description", description: sdp(answer) });
47615
+ } else
47616
+ await media.remote(value.description);
47617
+ } else if (value.type === "ice")
47618
+ await media.ice(value.candidate);
47619
+ else
47620
+ throw new Error("invalid media signal");
47621
+ current(generation);
47622
+ for (const pending of pendingSignals.splice(0)) {
47623
+ await signal(from, pending);
47624
+ current(generation);
47625
+ }
47626
+ } catch (error) {
47627
+ if (!closed && generation !== connectionGeneration)
47628
+ return;
47629
+ throw error;
47630
+ }
47631
+ });
47632
+ }
47633
+ }
47634
+
47635
+ // ../../core/calls/observation.js
47636
+ function openCallObservation({ cloud, capability, endpoint, protocol, onChange, onEmpty, onError }) {
47637
+ let verified = null;
47638
+ let identities = new Map;
47639
+ const audio = new Map;
47640
+ const departed = new Set;
47641
+ let last = "";
47642
+ let pending = null;
47643
+ let dirty = false;
47644
+ let closed = false;
47645
+ const mailbox = openCallMailbox({
47646
+ cloud,
47647
+ capability,
47648
+ endpoint,
47649
+ protocol,
47650
+ onError,
47651
+ stream: false,
47652
+ onChange(record) {
47653
+ const head = record.head;
47654
+ const admissions = head?.data?.participants || [];
47655
+ if (head && head !== verified) {
47656
+ if (!Number.isSafeInteger(head.data?.epoch) || head.data.epoch < 0)
47657
+ throw new Error("invalid call observation");
47658
+ const nextIdentities = verifyCallAdmissions(admissions, protocol);
47659
+ if (!admissions.some((item) => item.actor === head.actor && item.holder === head.holder && item.endpoint === head.endpoint)) {
47660
+ throw new Error("unadmitted call head");
47661
+ }
47662
+ identities = nextIdentities;
47663
+ } else if (!head)
47664
+ identities.clear();
47665
+ verified = head;
47666
+ if (record.historyLost) {
47667
+ audio.clear();
47668
+ departed.clear();
47669
+ }
47670
+ const holders = new Set(admissions.map((item) => item.holder));
47671
+ for (const holder of audio.keys())
47672
+ if (!holders.has(holder))
47673
+ audio.delete(holder);
47674
+ for (const holder of departed)
47675
+ if (!holders.has(holder))
47676
+ departed.delete(holder);
47677
+ for (const packet of record.events) {
47678
+ if (!admissions.some((item) => item.holder === packet.holder && item.actor === packet.actor && item.endpoint === packet.endpoint))
47679
+ continue;
47680
+ if (packet.kind === "leave")
47681
+ departed.add(packet.holder);
47682
+ if (packet.kind === "pulse")
47683
+ audio.set(packet.holder, { muted: packet.data.muted === true, deafened: packet.data.deafened === true });
47684
+ }
47685
+ const roster = admissions.filter((item) => !departed.has(item.holder)).map((item) => ({
47686
+ ...identities.get(item.holder),
47687
+ holder: item.holder,
47688
+ muted: audio.get(item.holder)?.muted === true || audio.get(item.holder)?.deafened === true,
47689
+ deafened: audio.get(item.holder)?.deafened === true
47690
+ }));
47691
+ const fingerprint = JSON.stringify(roster);
47692
+ if (fingerprint !== last) {
47693
+ last = fingerprint;
47694
+ onChange(roster);
47695
+ }
47696
+ if (!head || admissions.length && roster.length === 0)
47697
+ onEmpty?.();
47698
+ }
47699
+ });
47700
+ function read() {
47701
+ if (closed)
47702
+ return Promise.resolve();
47703
+ dirty = true;
47704
+ pending ||= (async () => {
47705
+ while (dirty && !closed) {
47706
+ dirty = false;
47707
+ await mailbox.read();
47708
+ }
47709
+ })().finally(() => {
47710
+ pending = null;
47711
+ });
47712
+ return pending;
47713
+ }
47714
+ return Object.freeze({ read, close() {
47715
+ closed = true;
47716
+ mailbox.close();
47717
+ } });
47718
+ }
47719
+
47720
+ // ../../core/calls/session.js
47721
+ 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 });
47722
+ var cancelled2 = () => Object.assign(new Error("call cancelled"), { code: "calls/cancelled" });
47723
+ var ended = () => Object.assign(new Error("this call has ended"), { code: "calls/ended" });
47724
+ var callError = (chatId, error) => ({ chatId, code: error?.code || "calls/failed", message: error?.message || "could not connect the call" });
47725
+ function createCallSession({ cloud, chat, media: mediaPort, mls, diag }) {
47726
+ const listeners = new Set;
47727
+ const discoveries = new Map;
47728
+ const discoveryReads = new Map;
47729
+ const observers = new Map;
47730
+ const retiring = new Set;
47731
+ let state = initial();
47732
+ const confirmedPeerAudio = new Map;
47733
+ let identity = null;
47734
+ let endpoint = null;
47735
+ let leaseEndpoint = null;
47736
+ let lease = null;
47737
+ let active = null;
47738
+ let focused = null;
47739
+ let generation = 0;
47740
+ let joinIntent = 0;
47741
+ let joining = null;
47742
+ let closed = false;
47743
+ let foreground = true;
47744
+ let online = true;
47745
+ let leaving = Promise.resolve();
47746
+ let drainingSources = Promise.resolve();
47747
+ let closingSession = null;
47748
+ const emit = (patch) => {
47749
+ if (Object.entries(patch).every(([field, value]) => Object.is(state[field], value)))
47750
+ return;
47751
+ if (patch.phase && patch.phase !== state.phase) {
47752
+ diag?.("call.phase", {
47753
+ phase: patch.phase,
47754
+ previous: state.phase,
47755
+ mode: patch.mode || state.mode,
47756
+ participants: (patch.participants || state.participants).length
47757
+ });
47758
+ }
47759
+ state = { ...state, ...patch };
47760
+ for (const listener of listeners)
47761
+ listener();
47762
+ };
47763
+ function announce(chatId, callId, event) {
47764
+ chat.getSnapshot().sendChatMessage(chatId, makeCallMessage(callId, event)).catch((error) => {
47765
+ diag?.("call.message.error", { event, code: error?.code || "" });
47766
+ });
47767
+ }
47768
+ function current(token) {
47769
+ if (closed || !identity || token !== generation)
47770
+ throw cancelled2();
47771
+ }
47772
+ function fail(error) {
47773
+ if (!identity || closed)
47774
+ return;
47775
+ diag?.("call.error", {
47776
+ code: error?.code || "calls/failed",
47777
+ phase: state.phase,
47778
+ mode: state.mode,
47779
+ participants: state.participants.length,
47780
+ message: error?.message || "call failed"
47781
+ });
47782
+ const chatId = active?.chatId || state.chatId;
47783
+ leave();
47784
+ emit({ phase: "error", chatId, error: callError(chatId, error) });
47785
+ }
47786
+ function available() {
47787
+ const entry = discoveries.get(focused);
47788
+ const value = (chatId, current2) => current2?.descriptor ? {
47789
+ chatId,
47790
+ callId: current2.descriptor.callId,
47791
+ startedAt: current2.descriptor.startedAt,
47792
+ participants: current2.descriptor.participants || 1,
47793
+ roster: (active?.discovery === current2 ? active.participants : current2.roster).filter((peer) => ![...retiring].some((call) => call.callId === current2.descriptor.callId && call.endpoint.holder === peer.holder))
47794
+ } : null;
47795
+ for (const observer of observers.values()) {
47796
+ if (!observer.onAvailable)
47797
+ continue;
47798
+ const next2 = value(observer.chatId, discoveries.get(observer.chatId));
47799
+ const fingerprint = JSON.stringify(next2);
47800
+ if (observer.fingerprint === fingerprint)
47801
+ continue;
47802
+ observer.fingerprint = fingerprint;
47803
+ try {
47804
+ observer.onAvailable(next2);
47805
+ } catch {}
47806
+ }
47807
+ const next = value(focused, entry);
47808
+ if (JSON.stringify(next) !== JSON.stringify(state.available))
47809
+ emit({ available: next });
47810
+ }
47811
+ function closeDiscovery(entry) {
47812
+ entry.mailbox.close();
47813
+ entry.observation?.close();
47814
+ entry.observation = null;
47815
+ entry.observedCallId = null;
47816
+ }
47817
+ function observeRoster(entry, refresh = false) {
47818
+ const descriptor = entry.descriptor;
47819
+ const callId = descriptor?.callId;
47820
+ if (!callId || active?.discovery === entry || ![...observers.values()].some((item) => item.chatId === entry.epochState.manifest.chatId)) {
47821
+ entry.observation?.close();
47822
+ entry.observation = null;
47823
+ entry.observedCallId = null;
47824
+ return;
47825
+ }
47826
+ if (entry.observedCallId === callId) {
47827
+ if (refresh)
47828
+ entry.observation.read().catch((error) => diag?.("call.discovery.error", { code: error?.code || "calls/roster" }));
47829
+ return;
47830
+ }
47831
+ entry.observation?.close();
47832
+ entry.roster = [];
47833
+ entry.observedCallId = callId;
47834
+ const token = generation;
47835
+ const protocol = createCallProtocol({ realm: cloud.environment, epochState: entry.epochState, identity, endpoint, callId });
47836
+ const secret = fromHexBytes(descriptor.secret);
47837
+ const capability = createCallCapability(secret, cloud.environment, "signaling", [callId]);
47838
+ cleanBytes(secret);
47839
+ entry.observation = openCallObservation({
47840
+ cloud,
47841
+ capability,
47842
+ endpoint,
47843
+ protocol,
47844
+ onChange(roster) {
47845
+ if (generation !== token || entry.observedCallId !== callId || discoveries.get(entry.epochState.manifest.chatId) !== entry)
47846
+ return;
47847
+ entry.roster = roster;
47848
+ available();
47849
+ },
47850
+ onError(error) {
47851
+ if (generation === token)
47852
+ diag?.("call.discovery.error", { code: error?.code || "calls/roster" });
47853
+ },
47854
+ onEmpty() {
47855
+ if (generation === token && entry.observedCallId === callId)
47856
+ finishDiscovery(entry, descriptor);
47857
+ }
47858
+ });
47859
+ entry.observation.read().catch(() => {});
47860
+ }
47861
+ function prune() {
47862
+ for (const [chatId, value] of discoveries) {
47863
+ if (![...observers.values()].some((item) => item.chatId === chatId) && chatId !== active?.chatId && chatId !== joining?.chatId && ![...retiring].some((call) => call.chatId === chatId)) {
47864
+ closeDiscovery(value);
47865
+ discoveries.delete(chatId);
47866
+ } else
47867
+ observeRoster(value);
47868
+ }
47869
+ }
47870
+ function discovery(chatId, epochState) {
47871
+ const token = generation;
47872
+ const existing = discoveries.get(chatId);
47873
+ if (existing?.epochState.manifest.epochId === epochState.manifest.epochId)
47874
+ return existing;
47875
+ if (existing)
47876
+ closeDiscovery(existing);
47877
+ const protocol = createCallProtocol({ realm: cloud.environment, epochState, identity, endpoint });
47878
+ const entry = {
47879
+ epochState,
47880
+ protocol,
47881
+ descriptor: null,
47882
+ mailbox: null,
47883
+ observation: null,
47884
+ observedCallId: null,
47885
+ roster: [],
47886
+ revision: null,
47887
+ expiredCallId: null,
47888
+ finishing: new Map
47889
+ };
47890
+ entry.mailbox = openCallMailbox({
47891
+ cloud,
47892
+ capability: createCallDiscovery(epochState, cloud.environment),
47893
+ endpoint,
47894
+ protocol,
47895
+ onChange(record) {
47896
+ if (token !== generation || discoveries.get(chatId) !== entry)
47897
+ return;
47898
+ const descriptor = record.head?.data || null;
47899
+ 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))
47900
+ throw new Error("invalid call discovery");
47901
+ const previous = entry.descriptor;
47902
+ entry.expiredCallId = !record.head ? previous?.callId || entry.expiredCallId : null;
47903
+ entry.descriptor = descriptor;
47904
+ if (!record.head && previous)
47905
+ finishDiscovery(entry, previous);
47906
+ const changed = entry.revision !== record.revision;
47907
+ entry.revision = record.revision;
47908
+ if (active?.discovery === entry && active.published && descriptor?.callId !== active.callId) {
47909
+ leave();
47910
+ }
47911
+ observeRoster(entry, changed);
47912
+ available();
47913
+ },
47914
+ onError(error) {
47915
+ if (token !== generation || discoveries.get(chatId) !== entry)
47916
+ return;
47917
+ diag?.("call.discovery.error", { code: error?.code || "calls/discovery" });
47918
+ }
47919
+ });
47920
+ discoveries.set(chatId, entry);
47921
+ return entry;
47922
+ }
47923
+ function readDiscovery(chatId, preparedEpoch) {
47924
+ const epochState = preparedEpoch || chat.getSnapshot().getOwnerChat(chatId)?.epochState;
47925
+ const epochId = epochState?.manifest.epochId;
47926
+ const pending = discoveryReads.get(chatId);
47927
+ if (pending && pending.epochId === epochId)
47928
+ return pending.promise;
47929
+ const slot = { epochId, promise: null };
47930
+ const token = generation;
47931
+ const reading = Promise.resolve().then(async () => {
47932
+ current(token);
47933
+ if (!online)
47934
+ throw Object.assign(new Error("chat unavailable"), { code: "unavailable" });
47935
+ if (!epochState)
47936
+ throw Object.assign(new Error("chat unavailable"), { code: "calls/chat-not-ready" });
47937
+ const entry = discovery(chatId, epochState);
47938
+ await entry.mailbox.read();
47939
+ current(token);
47940
+ return entry;
47941
+ }).finally(() => {
47942
+ if (discoveryReads.get(chatId) === slot)
47943
+ discoveryReads.delete(chatId);
47944
+ prune();
47945
+ });
47946
+ slot.promise = reading;
47947
+ discoveryReads.set(chatId, slot);
47948
+ return reading;
47949
+ }
47950
+ function observe(chatId, onAvailable) {
47951
+ if (!chatId || closed)
47952
+ return () => {};
47953
+ const observer = { chatId, onAvailable, fingerprint: undefined };
47954
+ observers.set(observer, observer);
47955
+ focused = chatId;
47956
+ if (state.error?.chatId === chatId)
47957
+ emit({ error: null });
47958
+ available();
47959
+ const entry = discoveries.get(chatId);
47960
+ if (entry)
47961
+ observeRoster(entry);
47962
+ else if (identity && online)
47963
+ readDiscovery(chatId).catch((error) => diag?.("call.discovery.error", { stage: "prepare", code: error?.code || "calls/admission" }));
47964
+ return () => {
47965
+ if (!observers.delete(observer))
47966
+ return;
47967
+ focused = [...observers.values()].at(-1)?.chatId || null;
47968
+ available();
47969
+ prune();
47970
+ };
47971
+ }
47972
+ async function joinAttempt(chatId, intent, expectedCallId, onlyExisting) {
47973
+ if (!identity || !mediaPort || !mls || !cloud.calls)
47974
+ throw new Error("calling unavailable");
47975
+ if (!foreground)
47976
+ throw new Error("open veyl to join a call");
47977
+ if (active?.chatId === chatId)
47978
+ return chatId;
47979
+ const token = generation;
47980
+ const startedAt = Date.now();
47981
+ const step = (stage) => diag?.("call.join.stage", { stage, elapsedMs: Date.now() - startedAt });
47982
+ const check = () => {
47983
+ current(token);
47984
+ if (intent !== joinIntent || !foreground)
47985
+ throw cancelled2();
47986
+ };
47987
+ emit({ ...!active ? { phase: "joining", chatId } : {}, error: null });
47988
+ await mediaPort.prepare?.();
47989
+ check();
47990
+ step("permission");
47991
+ if (!onlyExisting)
47992
+ chatId = await chat.getSnapshot().materializeChat(chatId);
47993
+ check();
47994
+ joining.chatId = chatId;
47995
+ step("chat");
47996
+ let call;
47997
+ try {
47998
+ const epochState = await chat.getSnapshot().prepareCall(chatId);
47999
+ check();
48000
+ const entry = await readDiscovery(chatId, epochState);
48001
+ check();
48002
+ const discoveryRecord = entry.mailbox.getSnapshot();
48003
+ step("discovery");
48004
+ const create = !entry.descriptor;
48005
+ if (expectedCallId && (onlyExisting || entry.epochState.manifest.lineage === "direct") && entry.descriptor?.callId !== expectedCallId)
48006
+ throw ended();
48007
+ const descriptor = entry.descriptor || {
48008
+ callId: toHex(randomBytes3(32)),
48009
+ secret: toHex(randomBytes3(32)),
48010
+ mode: entry.epochState.manifest.lineage,
48011
+ participants: 1,
48012
+ startedAt: Date.now()
48013
+ };
48014
+ const callEndpoint = createCallEndpoint();
48015
+ call = {
48016
+ chatId,
48017
+ callId: descriptor.callId,
48018
+ discovery: entry,
48019
+ descriptor,
48020
+ endpoint: callEndpoint,
48021
+ published: false,
48022
+ room: null,
48023
+ member: null,
48024
+ media: null,
48025
+ owner: null,
48026
+ participants: [],
48027
+ error: null,
48028
+ timer: null,
48029
+ joinTimer: null,
48030
+ mediaReady: false,
48031
+ leaving: false,
48032
+ muteTail: Promise.resolve()
48033
+ };
48034
+ joining.candidate = call;
48035
+ const protocol = createCallProtocol({ realm: cloud.environment, epochState: entry.epochState, identity, endpoint: callEndpoint, callId: descriptor.callId });
48036
+ call.protocol = protocol;
48037
+ const ownMember = (value) => {
48038
+ try {
48039
+ check();
48040
+ if (call.leaving)
48041
+ throw cancelled2();
48042
+ } catch (error) {
48043
+ cleanBytes(value.snapshot);
48044
+ throw error;
48045
+ }
48046
+ call.member = value;
48047
+ return value;
48048
+ };
48049
+ const callMember = ownMember(await mls.createMember(fromHexBytes(callEndpoint.holder)));
48050
+ const member = create ? callMember : ownMember(await mls.createKeyPackage(callMember.snapshot));
48051
+ if (!create)
48052
+ cleanBytes(callMember.snapshot);
48053
+ check();
48054
+ const admission = protocol.sign("join", { keyPackage: member.keyPackage ? encodeCallBytes(member.keyPackage) : "", signaturePK: toHex(member.signaturePK) });
48055
+ const secret = fromHexBytes(descriptor.secret);
48056
+ const capability = createCallCapability(secret, cloud.environment, "signaling", [descriptor.callId]);
48057
+ cleanBytes(secret);
48058
+ const valid = () => token === generation && active === call && !call.leaving;
48059
+ const preparing = () => token === generation && intent === joinIntent && !call.leaving;
48060
+ const checkCall = () => {
48061
+ check();
48062
+ if (call.leaving)
48063
+ throw cancelled2();
48064
+ if (call.error)
48065
+ throw call.error;
48066
+ };
48067
+ const connectionProgress = () => {
48068
+ if (!valid() || state.phase === "connected")
48069
+ return;
48070
+ if (call.mediaReady && descriptor.mode === "direct" && state.participants.length < 2) {
48071
+ clearTimeout(call.joinTimer);
48072
+ call.joinTimer = null;
48073
+ emit({ phase: "waiting" });
48074
+ } else {
48075
+ emit({ phase: "connecting" });
48076
+ if (!call.joinTimer)
48077
+ call.joinTimer = setTimeout(() => {
48078
+ if (valid())
48079
+ fail(new Error("call did not connect"));
48080
+ }, 30000);
48081
+ }
48082
+ };
48083
+ call.media = createCallMediaSession({
48084
+ port: mediaPort,
48085
+ mode: descriptor.mode,
48086
+ holder: callEndpoint.holder,
48087
+ provider: (...args) => call.owner.provider(...args),
48088
+ signal: (...args) => call.room.signal(...args),
48089
+ publish: (value) => call.room.publishMedia(value),
48090
+ onState(value) {
48091
+ if (!valid())
48092
+ return;
48093
+ diag?.("call.media.state", {
48094
+ state: value.state,
48095
+ reason: value.reason || "",
48096
+ mode: descriptor.mode,
48097
+ transport: value.transport || "",
48098
+ participants: call.participants.length
48099
+ });
48100
+ if (value.state === "connected") {
48101
+ clearTimeout(call.joinTimer);
48102
+ call.joinTimer = null;
48103
+ emit({ phase: "connected", transport: value.transport || (descriptor.mode === "group" ? "relay" : null) });
48104
+ } else if (value.state === "connecting") {
48105
+ emit({ phase: "connecting" });
48106
+ connectionProgress();
48107
+ } else if (["failed", "closed"].includes(value.state))
48108
+ fail(Object.assign(new Error("call connection ended"), { code: "calls/media-ended" }));
48109
+ },
48110
+ onExpired() {
48111
+ if (valid())
48112
+ fail(new Error("call connection lost"));
48113
+ },
48114
+ onSpeaking(speaking) {
48115
+ if (valid())
48116
+ emit({ speaking });
48117
+ },
48118
+ onError(error) {
48119
+ if (valid())
48120
+ fail(error);
48121
+ else
48122
+ call.error = error;
48123
+ }
48124
+ });
48125
+ call.room = openCallRoom({
48126
+ cloud,
48127
+ capability,
48128
+ protocol,
48129
+ endpoint: callEndpoint,
48130
+ mls,
48131
+ member,
48132
+ admission,
48133
+ callId: descriptor.callId,
48134
+ onKeys(value) {
48135
+ if (!valid() && !preparing())
48136
+ return;
48137
+ call.media.setKeys(value).catch((error) => {
48138
+ if (valid())
48139
+ fail(error);
48140
+ else
48141
+ call.error = error;
48142
+ });
48143
+ },
48144
+ onParticipants(value) {
48145
+ if (!valid() && !preparing())
48146
+ return;
48147
+ call.participants = value;
48148
+ call.media.setParticipants(value).catch((error) => {
48149
+ if (valid())
48150
+ fail(error);
48151
+ else
48152
+ call.error = error;
48153
+ });
48154
+ if (!valid())
48155
+ return;
48156
+ for (const peer of value) {
48157
+ const preference = state.peerAudio[peer.chatPK];
48158
+ if (preference)
48159
+ call.media.setPeerAudio(peer.holder, { volume: preference.volume / 100, muted: preference.muted }).catch((error) => diag?.("call.peer.audio.error", { code: error?.code || "calls/audio" }));
48160
+ }
48161
+ emit({ participants: value });
48162
+ available();
48163
+ call.refresh?.();
48164
+ if (call.mediaReady)
48165
+ connectionProgress();
48166
+ },
48167
+ onSignal: (value, from) => valid() || preparing() ? call.media.signal(value, from) : undefined,
48168
+ onRemoved() {
48169
+ if (valid())
48170
+ leave();
48171
+ else
48172
+ call.error = cancelled2();
48173
+ },
48174
+ onError(error) {
48175
+ if (valid())
48176
+ fail(error);
48177
+ else
48178
+ call.error = error;
48179
+ }
48180
+ });
48181
+ await applyAudio(call);
48182
+ checkCall();
48183
+ await call.media.setMicrophoneVolume(state.microphoneVolume / 100);
48184
+ checkCall();
48185
+ await call.room.start({ create });
48186
+ checkCall();
48187
+ step("admission");
48188
+ if (create) {
48189
+ try {
48190
+ await entry.mailbox.commit(descriptor, [], { expectedRevision: discoveryRecord.revision });
48191
+ } catch (error) {
48192
+ if (error?.code === "calls/conflict")
48193
+ throw Object.assign(new Error("another call started"), { code: "calls/discovery-conflict" });
48194
+ throw error;
48195
+ }
48196
+ checkCall();
48197
+ announce(chatId, descriptor.callId, "started");
48198
+ } else {
48199
+ await entry.mailbox.read();
48200
+ checkCall();
48201
+ if (entry.descriptor?.callId !== descriptor.callId) {
48202
+ if (expectedCallId && (onlyExisting || descriptor.mode === "direct"))
48203
+ throw ended();
48204
+ throw Object.assign(new Error("the call room changed"), { code: "calls/discovery-conflict" });
48205
+ }
48206
+ }
48207
+ call.published = true;
48208
+ await stopCall();
48209
+ checkCall();
48210
+ mountLease(identity, callEndpoint);
48211
+ call.owner = lease;
48212
+ active = call;
48213
+ observeRoster(entry);
48214
+ available();
48215
+ emit({
48216
+ phase: state.elsewhere ? "transferring" : "joining",
48217
+ chatId,
48218
+ callId: descriptor.callId,
48219
+ mode: descriptor.mode,
48220
+ error: null,
48221
+ speaking: [],
48222
+ participants: call.participants
48223
+ });
48224
+ for (const peer of call.participants) {
48225
+ const preference = state.peerAudio[peer.chatPK];
48226
+ if (preference)
48227
+ call.media.setPeerAudio(peer.holder, { volume: preference.volume / 100, muted: preference.muted }).catch((error) => diag?.("call.peer.audio.error", { code: error?.code || "calls/audio" }));
48228
+ }
48229
+ await lease.acquire({ chatId, callId: descriptor.callId });
48230
+ check();
48231
+ step("ownership");
48232
+ if (!valid())
48233
+ throw cancelled2();
48234
+ connectionProgress();
48235
+ await call.media.open();
48236
+ check();
48237
+ step("media");
48238
+ call.mediaReady = true;
48239
+ connectionProgress();
48240
+ let publishedRoster = "";
48241
+ let refreshing = null;
48242
+ let requested = false;
48243
+ const rosterFingerprint = () => JSON.stringify(call.participants.map(({ holder, muted, deafened }) => ({ holder, muted, deafened })).sort((a, b) => a.holder.localeCompare(b.holder)));
48244
+ const refresh = (heartbeat = false) => {
48245
+ if (!valid() || !heartbeat && publishedRoster === rosterFingerprint())
48246
+ return;
48247
+ requested = true;
48248
+ if (refreshing)
48249
+ return;
48250
+ clearTimeout(call.timer);
48251
+ refreshing = (async () => {
48252
+ while (requested && valid()) {
48253
+ requested = false;
48254
+ const fingerprint = rosterFingerprint();
48255
+ const ordered = call.participants.map((item) => item.holder).sort();
48256
+ if (ordered[0] === callEndpoint.holder) {
48257
+ const snapshot = await entry.mailbox.read();
48258
+ if (valid() && entry.descriptor?.callId === descriptor.callId) {
48259
+ await entry.mailbox.commit({ ...descriptor, participants: call.participants.length }, [], { expectedRevision: snapshot.revision });
48260
+ }
48261
+ }
48262
+ publishedRoster = fingerprint;
48263
+ }
48264
+ })().catch((error) => {
48265
+ if (valid() && error?.code !== "calls/conflict")
48266
+ fail(error);
48267
+ }).finally(() => {
48268
+ refreshing = null;
48269
+ if (valid())
48270
+ call.timer = setTimeout(() => refresh(true), 30000);
48271
+ });
48272
+ };
48273
+ call.refresh = refresh;
48274
+ refresh(true);
48275
+ return chatId;
48276
+ } catch (error) {
48277
+ const cleanup = active === call ? stopCall() : call ? disposeCall(call) : null;
48278
+ if (["calls/room-empty", "calls/discovery-conflict"].includes(error?.code))
48279
+ await cleanup;
48280
+ if (error?.code === "calls/room-empty" && call) {
48281
+ check();
48282
+ if (expectedCallId && (onlyExisting || call.descriptor.mode === "direct"))
48283
+ throw ended();
48284
+ await retireDiscovery(call);
48285
+ check();
48286
+ throw Object.assign(new Error("the call room is restarting"), { code: "calls/discovery-conflict" });
48287
+ }
48288
+ throw error;
48289
+ }
48290
+ }
48291
+ async function join(chatId, expectedCallId = null, onlyExisting = false) {
48292
+ if (active?.chatId === chatId) {
48293
+ if (expectedCallId && (onlyExisting || active.descriptor.mode === "direct") && active.callId !== expectedCallId)
48294
+ throw ended();
48295
+ return chatId;
48296
+ }
48297
+ const intent = ++joinIntent;
48298
+ const operation = { predecessor: active, chatId, candidate: null };
48299
+ const previous = joining;
48300
+ joining = operation;
48301
+ emit({ joiningChatId: chatId });
48302
+ if (previous?.candidate && previous.candidate !== active)
48303
+ disposeCall(previous.candidate);
48304
+ try {
48305
+ for (let attempt = 0;attempt < 3; attempt += 1) {
48306
+ try {
48307
+ return await joinAttempt(chatId, intent, expectedCallId, onlyExisting);
48308
+ } catch (error) {
48309
+ if (intent !== joinIntent || !identity || closed)
48310
+ throw cancelled2();
48311
+ if (error?.code !== "calls/discovery-conflict" || attempt === 2) {
48312
+ const target = operation.chatId;
48313
+ emit({ ...!active ? { phase: "error", chatId: target } : {}, error: callError(chatId, error) });
48314
+ throw error;
48315
+ }
48316
+ }
48317
+ }
48318
+ } finally {
48319
+ if (joining === operation) {
48320
+ joining = null;
48321
+ emit({ joiningChatId: null });
48322
+ }
48323
+ prune();
48324
+ }
48325
+ }
48326
+ function leave() {
48327
+ joinIntent += 1;
48328
+ const candidate = joining?.candidate;
48329
+ joining = null;
48330
+ emit({ joiningChatId: null });
48331
+ const pending = candidate && candidate !== active ? disposeCall(candidate) : null;
48332
+ const stopped = stopCall();
48333
+ return Promise.all([pending, stopped, ...[...retiring].map((call) => call.closing)]).then(() => {
48334
+ return;
48335
+ });
48336
+ }
48337
+ function cancelJoin() {
48338
+ const operation = joining;
48339
+ if (!operation)
48340
+ return Promise.resolve();
48341
+ joinIntent += 1;
48342
+ joining = null;
48343
+ emit({ joiningChatId: null });
48344
+ const pending = operation.candidate && operation.candidate !== active ? disposeCall(operation.candidate) : null;
48345
+ return Promise.all([pending, active && active === operation.predecessor ? null : stopCall()]).then(() => {
48346
+ return;
48347
+ });
48348
+ }
48349
+ function stopCall() {
48350
+ const call = active;
48351
+ if (!call) {
48352
+ emit({ phase: "idle", chatId: null, callId: null, mode: null, transport: null, participants: [], speaking: [] });
48353
+ return leaving;
48354
+ }
48355
+ active = null;
48356
+ leaving = disposeCall(call);
48357
+ if (identity && discoveries.get(call.chatId) === call.discovery)
48358
+ observeRoster(call.discovery);
48359
+ available();
48360
+ emit({ phase: "idle", chatId: null, callId: null, mode: null, transport: null, participants: [], speaking: [] });
48361
+ return leaving;
48362
+ }
48363
+ async function retireDiscovery(call) {
48364
+ const entry = call.discovery;
48365
+ const snapshot = await entry.mailbox.read();
48366
+ if (snapshot.head?.data?.callId !== call.callId && !(snapshot.head === null && entry.expiredCallId === call.callId))
48367
+ return;
48368
+ try {
48369
+ await entry.mailbox.commit(null, [], { expectedRevision: snapshot.revision });
48370
+ announce(call.chatId, call.callId, "ended");
48371
+ } catch (error) {
48372
+ if (error?.code !== "calls/conflict")
48373
+ throw error;
48374
+ }
48375
+ }
48376
+ function finishDiscovery(entry, descriptor) {
48377
+ if (entry.finishing.has(descriptor.callId))
48378
+ return;
48379
+ 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));
48380
+ entry.finishing.set(descriptor.callId, finishing);
48381
+ }
48382
+ function disposeCall(call) {
48383
+ if (call.closing)
48384
+ return call.closing;
48385
+ call.leaving = true;
48386
+ retiring.add(call);
48387
+ clearTimeout(call.timer);
48388
+ clearTimeout(call.joinTimer);
48389
+ const stopping = call.media?.close();
48390
+ call.owner?.retire();
48391
+ const departing = call.room?.leave();
48392
+ const draining = (async () => {
48393
+ try {
48394
+ const [, departure] = await Promise.allSettled([Promise.resolve(stopping).then(() => call.owner?.release()), departing]);
48395
+ if (departure.status === "fulfilled" && departure.value?.ended && call.published && call.discovery.descriptor?.callId === call.callId) {
48396
+ await retireDiscovery(call);
48397
+ }
48398
+ } catch (error) {
48399
+ diag?.("call.close.error", { code: error?.code || "" });
48400
+ }
48401
+ })();
48402
+ let expiry;
48403
+ const expired = new Promise((resolve) => {
48404
+ expiry = setTimeout(() => {
48405
+ diag?.("call.close.error", { code: "calls/retirement-timeout" });
48406
+ resolve();
48407
+ }, CALL_OWNERSHIP_LEASE_MS);
48408
+ });
48409
+ call.closing = Promise.race([draining, expired]).finally(() => {
48410
+ clearTimeout(expiry);
48411
+ call.room?.close();
48412
+ call.protocol?.close();
48413
+ cleanBytes(call.member?.snapshot);
48414
+ if (call.endpoint !== leaseEndpoint)
48415
+ call.endpoint.close();
48416
+ retiring.delete(call);
48417
+ prune();
48418
+ });
48419
+ return call.closing;
48420
+ }
48421
+ function applyAudio(call) {
48422
+ const audio = { muted: state.muted || state.deafened, deafened: state.deafened };
48423
+ const applied = Promise.all([call.media.mute(audio.muted), call.media.deafen(audio.deafened)]);
48424
+ const result = Promise.all([applied, call.muteTail]).then(() => {
48425
+ if (!call.leaving)
48426
+ return call.room.setAudio(audio);
48427
+ });
48428
+ call.muteTail = result.catch(() => {});
48429
+ return result;
48430
+ }
48431
+ async function setAudio(patch) {
48432
+ emit(patch);
48433
+ const calls = new Set([active, joining?.candidate].filter((call) => call?.media && call.room && !call.leaving));
48434
+ await Promise.all([...calls].map((call) => applyAudio(call).catch((error) => {
48435
+ if (active === call)
48436
+ fail(error);
48437
+ else if (!call.leaving)
48438
+ call.error = error;
48439
+ throw error;
48440
+ })));
48441
+ }
48442
+ async function setPeerAudio(chatPK, patch) {
48443
+ const call = active;
48444
+ if (!identity || typeof chatPK !== "string" || !chatPK || chatPK === identity.chatPK)
48445
+ throw new Error("peer identity required");
48446
+ const token = generation;
48447
+ const peer = call?.participants.find((peer2) => peer2.chatPK === chatPK);
48448
+ const previous = state.peerAudio[chatPK];
48449
+ const value = { volume: 100, muted: false, ...previous, ...patch };
48450
+ emit({ peerAudio: { ...state.peerAudio, [chatPK]: value } });
48451
+ try {
48452
+ if (peer)
48453
+ await call.media.setPeerAudio(peer.holder, { volume: value.volume / 100, muted: value.muted });
48454
+ if (generation === token && state.peerAudio[chatPK] === value)
48455
+ confirmedPeerAudio.set(chatPK, value);
48456
+ } catch (error) {
48457
+ if (generation === token && state.peerAudio[chatPK] === value) {
48458
+ const peerAudio = { ...state.peerAudio };
48459
+ const confirmed = confirmedPeerAudio.get(chatPK);
48460
+ if (confirmed)
48461
+ peerAudio[chatPK] = confirmed;
48462
+ else
48463
+ delete peerAudio[chatPK];
48464
+ emit({ peerAudio });
48465
+ }
48466
+ throw error;
48467
+ }
48468
+ }
48469
+ async function setMicrophoneVolume(volume) {
48470
+ if (!Number.isFinite(volume) || volume < 1 || volume > 200)
48471
+ throw new Error("microphone volume must be between 1 and 200");
48472
+ const previous = state.microphoneVolume;
48473
+ const token = generation;
48474
+ emit({ microphoneVolume: volume });
48475
+ const calls = new Set([active, joining?.candidate].filter((call) => call?.media && !call.leaving));
48476
+ try {
48477
+ await Promise.all([...calls].map((call) => call.media.setMicrophoneVolume(volume / 100)));
48478
+ } catch (error) {
48479
+ if (generation === token && state.microphoneVolume === volume)
48480
+ emit({ microphoneVolume: previous });
48481
+ throw error;
48482
+ }
48483
+ }
48484
+ function mountLease(session, callEndpoint = endpoint) {
48485
+ lease?.close();
48486
+ if (leaseEndpoint && leaseEndpoint !== endpoint && leaseEndpoint !== callEndpoint)
48487
+ leaseEndpoint.close();
48488
+ leaseEndpoint = callEndpoint;
48489
+ const ownsLease = () => identity === session && leaseEndpoint === callEndpoint;
48490
+ lease = openAccountCallLease({
48491
+ cloud,
48492
+ capability: session.callIdentity,
48493
+ endpoint: callEndpoint,
48494
+ clock: mediaPort.clock,
48495
+ onRecord(record, descriptor) {
48496
+ if (!ownsLease())
48497
+ return;
48498
+ const holder = (record.pending || record.active)?.holder;
48499
+ const elsewhere = descriptor && holder !== callEndpoint.holder ? descriptor : null;
48500
+ if (elsewhere && state.elsewhere?.chatId === elsewhere.chatId && state.elsewhere.callId === elsewhere.callId)
48501
+ return;
48502
+ emit({ elsewhere });
48503
+ },
48504
+ onGrant: (value) => ownsLease() && active?.endpoint === callEndpoint ? active.media?.grant(value) : undefined,
48505
+ onLost: () => {
48506
+ if (ownsLease())
48507
+ leave();
48508
+ },
48509
+ onError(error) {
48510
+ if (ownsLease() && active)
48511
+ fail(error);
48512
+ }
48513
+ });
48514
+ }
48515
+ function setSources(session) {
48516
+ if (identity === session)
48517
+ return drainingSources;
48518
+ const token = ++generation;
48519
+ const previousLease = lease;
48520
+ const previousEndpoint = endpoint;
48521
+ const previousLeaseEndpoint = leaseEndpoint;
48522
+ const previousDiscoveries = [...discoveries.values()];
48523
+ let finishDrain, rejectDrain;
48524
+ const drained = new Promise((resolve, reject) => {
48525
+ finishDrain = resolve;
48526
+ rejectDrain = reject;
48527
+ });
48528
+ const retirement = Promise.all([drainingSources, drained]).then(() => {
48529
+ return;
48530
+ });
48531
+ drainingSources = retirement;
48532
+ lease = null;
48533
+ leaseEndpoint = null;
48534
+ endpoint = null;
48535
+ discoveries.clear();
48536
+ discoveryReads.clear();
48537
+ identity = session;
48538
+ const stopped = leave();
48539
+ Promise.resolve(stopped).finally(() => {
48540
+ previousLease?.close();
48541
+ previousLeaseEndpoint?.close();
48542
+ previousEndpoint?.close();
48543
+ for (const entry of previousDiscoveries)
48544
+ closeDiscovery(entry);
48545
+ }).then(finishDrain, rejectDrain);
48546
+ if (generation !== token || identity !== session)
48547
+ return retirement;
48548
+ confirmedPeerAudio.clear();
48549
+ emit(initial());
48550
+ available();
48551
+ if (generation !== token || identity !== session)
48552
+ return retirement;
48553
+ if (!session || closed || !mediaPort || !mls || !cloud.calls)
48554
+ return retirement;
48555
+ endpoint = createCallEndpoint();
48556
+ mountLease(session);
48557
+ if (online)
48558
+ refreshObserved();
48559
+ return retirement;
48560
+ }
48561
+ function refreshObserved() {
48562
+ for (const chatId of new Set([...observers.values()].map((item) => item.chatId))) {
48563
+ readDiscovery(chatId).catch((error) => diag?.("call.discovery.error", { stage: "prepare", code: error?.code || "calls/admission" }));
48564
+ }
48565
+ }
48566
+ const unsubscribe = chat.subscribe(() => {
48567
+ if (!identity)
48568
+ return;
48569
+ for (const [chatId, entry] of discoveries) {
48570
+ const epoch = chat.getSnapshot().getOwnerChat(chatId)?.epochState?.manifest.epochId;
48571
+ if (epoch === entry.epochState.manifest.epochId)
48572
+ continue;
48573
+ if (active?.chatId === chatId)
48574
+ leave();
48575
+ else if (joining?.chatId === chatId)
48576
+ cancelJoin();
48577
+ closeDiscovery(entry);
48578
+ discoveries.delete(chatId);
48579
+ available();
48580
+ if (online && [...observers.values()].some((item) => item.chatId === chatId))
48581
+ readDiscovery(chatId).catch(() => {});
48582
+ }
48583
+ if (online)
48584
+ for (const chatId of new Set([...observers.values()].map((item) => item.chatId))) {
48585
+ if (!discoveries.has(chatId) && !discoveryReads.has(chatId) && chat.getSnapshot().getOwnerChat(chatId)?.epochState) {
48586
+ readDiscovery(chatId).catch((error) => diag?.("call.discovery.error", { stage: "prepare", code: error?.code || "calls/admission" }));
48587
+ }
48588
+ }
48589
+ });
48590
+ return Object.freeze({
48591
+ getSnapshot: () => state,
48592
+ subscribe(listener) {
48593
+ listeners.add(listener);
48594
+ return () => listeners.delete(listener);
48595
+ },
48596
+ setSources,
48597
+ observe,
48598
+ join,
48599
+ leave,
48600
+ cancelJoin,
48601
+ setMicrophoneVolume,
48602
+ async resolveActiveCall(chatId) {
48603
+ const entry = await readDiscovery(chatId);
48604
+ return entry.descriptor?.callId || null;
48605
+ },
48606
+ joinFromMessage(chatId, callId) {
48607
+ if (!/^[a-f0-9]{64}$/u.test(callId))
48608
+ return Promise.reject(ended());
48609
+ return join(chatId, callId);
48610
+ },
48611
+ joinExisting(chatId, callId) {
48612
+ if (!/^[a-f0-9]{64}$/u.test(callId))
48613
+ return Promise.reject(ended());
48614
+ return join(chatId, callId, true);
48615
+ },
48616
+ setMuted(value) {
48617
+ return setAudio({ muted: value === true });
48618
+ },
48619
+ setDeafened(value) {
48620
+ return setAudio({ deafened: value === true });
48621
+ },
48622
+ setPeerVolume(chatPK, volume) {
48623
+ if (!Number.isFinite(volume) || volume < 1 || volume > 200)
48624
+ return Promise.reject(new Error("peer volume must be between 1 and 200"));
48625
+ return setPeerAudio(chatPK, { volume });
48626
+ },
48627
+ setPeerMuted(chatPK, muted) {
48628
+ return setPeerAudio(chatPK, { muted: muted === true });
48629
+ },
48630
+ setForeground(value) {
48631
+ foreground = value === true;
48632
+ if (!foreground && joining)
48633
+ cancelJoin();
48634
+ },
48635
+ setOnline(value) {
48636
+ const reconnecting = !online && value === true;
48637
+ online = value === true;
48638
+ if (reconnecting && identity && !closed)
48639
+ refreshObserved();
48640
+ },
48641
+ close() {
48642
+ if (closed)
48643
+ return closingSession;
48644
+ let finish, reject;
48645
+ closingSession = new Promise((resolve, fail2) => {
48646
+ finish = resolve;
48647
+ reject = fail2;
48648
+ });
48649
+ closed = true;
48650
+ const stopped = setSources(null);
48651
+ unsubscribe();
48652
+ listeners.clear();
48653
+ Promise.resolve(stopped).finally(() => Promise.all([mls?.close?.(), mediaPort?.close?.()])).then(finish, reject);
48654
+ return closingSession;
48655
+ }
48656
+ });
48657
+ }
48658
+
45657
48659
  // ../../node_modules/.bun/@zxcvbn-ts+core@4.2.0/node_modules/@zxcvbn-ts/core/dist/utils/helper.mjs
45658
48660
  var extend = (listToExtend, list) => listToExtend.push.apply(listToExtend, list);
45659
48661
  var sorted = (matches) => matches.sort((m1, m2) => m1.i - m2.i || m1.j - m2.j);
@@ -49657,11 +52659,11 @@ function createPeersApi({ cloud, network, avatarCache = null }) {
49657
52659
  return null;
49658
52660
  }
49659
52661
  }
49660
- async function writeCachedAvatar(uid, version, bytes) {
49661
- if (!uid || version == null || !bytes || typeof avatarCache?.write !== "function")
52662
+ async function writeCachedAvatar(uid, version, bytes2) {
52663
+ if (!uid || version == null || !bytes2 || typeof avatarCache?.write !== "function")
49662
52664
  return null;
49663
52665
  try {
49664
- const cached = await avatarCache.write(uid, { version, bytes });
52666
+ const cached = await avatarCache.write(uid, { version, bytes: bytes2 });
49665
52667
  return typeof cached === "string" && cached ? cached : typeof cached?.url === "string" && cached.url ? cached.url : typeof cached?.source === "string" && cached.source ? cached.source : null;
49666
52668
  } catch {
49667
52669
  return null;
@@ -49675,8 +52677,8 @@ function createPeersApi({ cloud, network, avatarCache = null }) {
49675
52677
  }
49676
52678
  if (avatarVersion != null && typeof cloud.peer.avatar.read === "function") {
49677
52679
  try {
49678
- const bytes = await cloud.peer.avatar.read(uid, { version: avatarVersion });
49679
- const cachedSource = await writeCachedAvatar(uid, avatarVersion, bytes);
52680
+ const bytes2 = await cloud.peer.avatar.read(uid, { version: avatarVersion });
52681
+ const cachedSource = await writeCachedAvatar(uid, avatarVersion, bytes2);
49680
52682
  if (cachedSource) {
49681
52683
  return cachedSource;
49682
52684
  }
@@ -55084,12 +58086,12 @@ function createWalletTransferSession({ wallet, walletPK, claimIncomingTransfers,
55084
58086
  reason,
55085
58087
  active: sdkActivityCountRef.current
55086
58088
  });
55087
- let ended = false;
58089
+ let ended2 = false;
55088
58090
  return () => {
55089
- if (ended) {
58091
+ if (ended2) {
55090
58092
  return;
55091
58093
  }
55092
- ended = true;
58094
+ ended2 = true;
55093
58095
  sdkActivityCountRef.current = Math.max(0, sdkActivityCountRef.current - 1);
55094
58096
  sdkActivityQuietUntilRef.current = Math.max(sdkActivityQuietUntilRef.current, Date.now() + SDK_BACKGROUND_QUIET_MS);
55095
58097
  markDiag(diag, "wallet.sdkActivity.done", {
@@ -57304,8 +60306,10 @@ function openAccount(options = {}) {
57304
60306
  getPeerProfile: getChatPeerProfile,
57305
60307
  refreshPeerProfile: refreshChatPeerProfile,
57306
60308
  resolvePeerProfile: resolveChatPeerProfile,
57307
- maintenance: messageMaintenance
60309
+ maintenance: messageMaintenance,
60310
+ resolveActiveCall: (chatId) => calls.resolveActiveCall(chatId)
57308
60311
  }, chatSources());
60312
+ const calls = createCallSession({ cloud, chat, ...options.calls, diag });
57309
60313
  const wallet = openWallet({
57310
60314
  ...options.wallet || {},
57311
60315
  network: state.network,
@@ -57378,6 +60382,8 @@ function openAccount(options = {}) {
57378
60382
  syncWallet();
57379
60383
  syncPresence();
57380
60384
  syncPeers();
60385
+ calls.setOnline(state.online);
60386
+ calls.setSources(state.lockState === "unlocked" ? state.session : null);
57381
60387
  }
57382
60388
  function syncPresence() {
57383
60389
  const session = state.session;
@@ -57413,7 +60419,7 @@ function openAccount(options = {}) {
57413
60419
  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
60420
  }),
57415
60421
  preview: (value) => user.previewAvatar(value),
57416
- write: (uid, bytes) => cloud.user.profile.avatar.set(uid, bytes),
60422
+ write: (uid, bytes2) => cloud.user.profile.avatar.set(uid, bytes2),
57417
60423
  commit: (change) => user.confirmAvatar(change),
57418
60424
  onError: (error) => diag?.("account.avatar.update.error", { code: error?.code || "" })
57419
60425
  });
@@ -57427,11 +60433,11 @@ function openAccount(options = {}) {
57427
60433
  await cloud.user.username.get(username);
57428
60434
  return { username };
57429
60435
  },
57430
- async setAvatar(bytes) {
60436
+ async setAvatar(bytes2) {
57431
60437
  profileUid();
57432
- if (!bytes)
60438
+ if (!bytes2)
57433
60439
  throw new Error("avatar data required");
57434
- return avatarUpdate.set(sanitizeAvatar(bytes));
60440
+ return avatarUpdate.set(sanitizeAvatar(bytes2));
57435
60441
  },
57436
60442
  async clearAvatar() {
57437
60443
  profileUid();
@@ -57562,13 +60568,32 @@ function openAccount(options = {}) {
57562
60568
  return;
57563
60569
  foreground = value === true;
57564
60570
  syncPresence();
60571
+ calls.setForeground(foreground);
57565
60572
  }
57566
60573
  function closeSession({ notify: notify2 = true } = {}) {
57567
- avatarUpdate.reset();
57568
- attempt += 1;
57569
- chat.setInboxEnabled(false);
57570
60574
  const session = state.session;
60575
+ attempt += 1;
57571
60576
  chatSession = null;
60577
+ const patch = {
60578
+ session: null,
60579
+ wallet: null,
60580
+ walletError: null,
60581
+ lockState: "locked",
60582
+ online: false,
60583
+ onlineError: null,
60584
+ agreementSession: null
60585
+ };
60586
+ state = { ...state, ...patch };
60587
+ const callRetirement = calls.setSources(null);
60588
+ if (callRetirement) {
60589
+ const work = Promise.resolve(callRetirement).catch((error) => {
60590
+ diag?.("account.calls.close.error", { code: error?.code || "" });
60591
+ });
60592
+ sessionCloseWork.add(work);
60593
+ work.finally(() => sessionCloseWork.delete(work));
60594
+ }
60595
+ avatarUpdate.reset();
60596
+ chat.setInboxEnabled(false);
57572
60597
  closeAccountSession(session);
57573
60598
  if (session && typeof options.onSessionClosed === "function") {
57574
60599
  let result;
@@ -57588,19 +60613,9 @@ function openAccount(options = {}) {
57588
60613
  }
57589
60614
  state.user.lockSettings?.();
57590
60615
  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
60616
  if (notify2) {
57601
- emitDomains(patch);
60617
+ emitDomains({});
57602
60618
  } else {
57603
- state = { ...state, ...patch };
57604
60619
  syncDomains();
57605
60620
  }
57606
60621
  return session;
@@ -58162,12 +61177,14 @@ function openAccount(options = {}) {
58162
61177
  stopPeerAdmissionProjection();
58163
61178
  stopPresence();
58164
61179
  presence.close();
61180
+ const callClosing = calls.close();
58165
61181
  clearMissingPeers();
58166
61182
  wallet.close();
58167
61183
  chat.close();
58168
61184
  peers.close();
58169
61185
  listeners.clear();
58170
61186
  operationBarrier.finishClose();
61187
+ return callClosing;
58171
61188
  });
58172
61189
  return closePromise;
58173
61190
  }
@@ -58183,6 +61200,7 @@ function openAccount(options = {}) {
58183
61200
  messageMaintenance,
58184
61201
  peers,
58185
61202
  presence,
61203
+ calls,
58186
61204
  closeBarrier: Object.freeze({
58187
61205
  acquire: operationBarrier.acquireClose,
58188
61206
  release: operationBarrier.releaseClose,