@glyphteck/veyl 0.59.0 → 0.60.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -6450,7 +6450,7 @@ var init_notifications = __esm(() => {
6450
6450
  });
6451
6451
 
6452
6452
  // ../../core/config.js
6453
- var MS_PER_SECOND = 1000, MINUTE_MS, HOUR_MS, DAY_MS, KIB_BYTES = 1024, MIB_BYTES, SATS_PER_BITCOIN = 100000000n, USERNAME_MAX_CHARS = 12, PASSWORD_MIN_CHARS = 15, PASSWORD_MAX_CHARS = 64, PASSWORD_MAX_BYTES, AUTOLOCK_MIN_MINUTES = 1, AUTOLOCK_MAX_MINUTES = 60, LOCAL_CACHE_WRITE_DELAY_MS = 350, LOCAL_MEDIA_CACHE_MAX_BYTES, LOCAL_MEDIA_CACHE_MAX_ITEMS = 1000, LOCAL_MEDIA_ACCESS_TOUCH_MIN_MS, LOCAL_PROFILE_CACHE_MAX_ITEMS = 500, LOCAL_PROFILE_CACHE_MAX_AGE_MS, LOCAL_CHAT_CACHE_MAX_ITEMS = 1000, LOCAL_CHAT_MESSAGE_CACHE_MAX_VISIBLE = 1000, LOCAL_CHAT_MESSAGE_CACHE_WRITE_DELAY_MS = 2500, LOCAL_AVATAR_CACHE_MAX_BYTES, LOCAL_AVATAR_CACHE_MAX_AGE_MS, AVATAR_IMAGE_MAX_BYTES, CHAT_AVATAR_IMAGE_MAX_BYTES, CHAT_AVATAR_REF_MAX_CHARS, CHAT_MESSAGE_FILE_CACHE_MAX_BYTES, IDLE_CALLBACK_MIN_TIMEOUT_MS = 50, ATTACHMENT_CACHE_IDLE_TIMEOUT_MS = 2500, ATTACHMENT_CACHE_FALLBACK_DELAY_MS = 250, CHAT_UNSAVED_TTL_DAYS = 21, CHAT_UNSAVED_TTL_MS, CHAT_AFTER_SEEN_MS, CHAT_MEDIA_TTL_DAYS, CHAT_MEDIA_TTL_MS, CHAT_UPLOAD_MAX_BYTES, CHAT_AUDIO_TRANSCODE_BITRATE_BPS = 128000, CHAT_VIDEO_TRANSCODE_VIDEO_BITRATE_BPS = 3000000, CHAT_VIDEO_TRANSCODE_AUDIO_BITRATE_BPS, CHAT_MESSAGE_BATCH_SIZE = 40, CHAT_MESSAGE_QUERY_MAX_DOCS = 120, CHAT_TTL_CLIENT_DELETE_GRACE_MS, CHAT_TTL_DELETE_CHUNK_SIZE = 40, CHAT_DELETE_SCAN_BATCH_SIZE = 200, CHAT_DELETE_WRITE_BATCH_SIZE = 400, CHAT_BATCH_CLEANUP_IDLE_TIMEOUT_MS = 1500, CHAT_BATCH_CLEANUP_IDLE_DELAY_MS = 250, CHAT_LIST_PAGE_SIZE = 20, CHAT_LIST_LIVE_COUNT, CHAT_INBOX_PING_PAGE_SIZE = 25, CHAT_INBOX_CURSOR_OVERLAP_MS, CHAT_LIST_CACHE_WRITE_DELAY_MS = 1500, CHAT_LIST_SNAPSHOT_COALESCE_MS = 80, CHAT_LIST_LISTENER_RETRY_MS = 1000, CHAT_TOP_WARM_COUNT = 0, CHAT_EAGER_WARM_COUNT = 0, CHAT_WARM_DELAY_MS = 3000, CHAT_WARM_BATCH_SIZE, CHAT_MESSAGE_VIEW_CACHE_SIZE = 30, CHAT_MEDIA_WARM_MESSAGES_PER_CHAT, CHAT_MEDIA_WARM_START_DELAY_MS = 600, CHAT_MEDIA_WARM_STEP_DELAY_MS = 120, CHAT_MEDIA_WARM_TYPES, CHAT_MEDIA_WARM_MAX_BYTES = 0, CHAT_READ_WRITE_INTERVAL_MS, CHAT_READ_WRITE_MAX_WAIT_MS, CHAT_LIVE_PING_INTERVAL_MS, CHAT_LIVE_SWEEP_INTERVAL_MS, CHAT_LIVE_STALE_AFTER_MS, CHAT_LIVE_RECONNECT_MAX_MS, CHAT_LIVE_TYPING_IDLE_MS, CHAT_LIVE_TYPING_RENEW_MS, CHAT_LIVE_COMPOSITION_HANDOFF_MS, CHAT_LIVE_READ_SEND_INTERVAL_MS = 120, CHAT_LIVE_READ_SEND_MAX_WAIT_MS = 500, CHAT_LIVE_READ_WRITE_INTERVAL_MS, CHAT_LIVE_READ_WRITE_MAX_WAIT_MS, CHAT_SEND_QUEUE_RATE_LIMIT_COUNT = 12, CHAT_SEND_QUEUE_RATE_LIMIT_WINDOW_MS, CHAT_SETTINGS_BODY_MAX_BYTES, CHAT_MANIFEST_MAX_BYTES, CHAT_RECEIPT_MAX_MEMBERS = 32, CHAT_ORDINARY_DELIVERY_MAX_MEMBERS = 128, CHAT_MAX_MEMBERS = 256, CHAT_MAX_TEXT_CHARS = 2048, CHAT_MAX_REACTIONS, SEARCH_DEBOUNCE_MS = 300, SEARCH_USERNAME_LIMIT = 15, SEARCH_ROLE_LIMIT = 15, RECENT_PEER_REFRESH_LIMIT = 50, RECENT_PEER_REFRESH_DELAY_MS = 250, RECENT_PEER_REFRESH_INTERVAL_MS, RECENT_PEER_REFRESH_THROTTLE_MS = 120, BAN_REFRESH_GRACE_MS = 50, REQUEST_MONEY_MAX_SATS, WALLET_TRANSFER_POLL_MS, WALLET_ACTIVE_TRANSFER_REFRESH_MS, WALLET_IDLE_ACTIVE_TRANSFER_REFRESH_MS, WALLET_VISIBLE_TRANSFER_DISCOVERY_WINDOW_MS, WALLET_ACTIVE_TRANSFER_EMPTY_SCAN_LIMIT = 8, WALLET_CACHE_HYDRATE_DELAY_MS = 0, WALLET_BOOT_REFRESH_DELAY_MS = 500, WALLET_BOOT_CACHED_REFRESH_DELAY_MS = 2500, WALLET_REGTEST_DEPOSIT_CLAIM_POLL_MS, WALLET_MAINNET_DEPOSIT_CLAIM_POLL_MS, WALLET_BALANCE_EVENT_COALESCE_MS, WALLET_INCOMING_UPDATE_COALESCE_MS = 250, WALLET_AUTO_CLAIM_MAX_FEE_SATS = 5000, WALLET_CLAIM_PAGE_SIZE = 100, WALLET_PENDING_TRANSFER_CLAIM_BATCH_SIZE = 50, WALLET_PENDING_TRANSFER_STATUS_BATCH_SIZE = 2, WALLET_PENDING_TRANSFER_COLD_REFRESH_BATCH_SIZE = 50, WALLET_PENDING_TRANSFER_COLD_REFRESH_INTERVAL_MS, WALLET_PENDING_TRANSFER_ADAPTIVE_BATCH_SMALL_QUEUE = 3, WALLET_PENDING_TRANSFER_ADAPTIVE_BATCH_LARGE_QUEUE = 10, WALLET_PENDING_TRANSFER_ADAPTIVE_BATCH_SMALL_SIZE = 2, WALLET_PENDING_TRANSFER_ADAPTIVE_BATCH_LARGE_SIZE = 3, WALLET_PENDING_TRANSFER_SLOW_CLAIM_MS, WALLET_PENDING_TRANSFER_CLAIM_COOLDOWN_MS, WALLET_SDK_BACKGROUND_QUIET_MS, WALLET_RECENT_TRANSFER_LIMIT = 100, WALLET_TRANSFER_PAGE_LIMIT = 100, WALLET_TRANSFER_FETCH_THROTTLE_MS = 150, WALLET_PENDING_TRANSFER_CLAIM_RETRY_MS, WALLET_SENT_TRANSFER_REFRESH_DELAY_MS, WALLET_PENDING_TRANSFER_BOOT_GRACE_MS, WALLET_PENDING_TRANSFER_HOT_AGE_MS, WALLET_PENDING_TRANSFER_WARM_AGE_MS, WALLET_PENDING_TRANSFER_WARM_RETRY_MS, WALLET_PENDING_TRANSFER_STALE_RETRY_MS, WALLET_PENDING_TRANSFER_STUCK_RETRY_MS, WALLET_PENDING_TRANSFER_DORMANT_RETRY_MS, WALLET_TRANSFER_CACHE_WRITE_DELAY_MS;
6453
+ var MS_PER_SECOND = 1000, MINUTE_MS, HOUR_MS, DAY_MS, KIB_BYTES = 1024, MIB_BYTES, SATS_PER_BITCOIN = 100000000n, USERNAME_MAX_CHARS = 12, PASSWORD_MIN_CHARS = 15, PASSWORD_MAX_CHARS = 64, PASSWORD_MAX_BYTES, AUTOLOCK_MIN_MINUTES = 1, AUTOLOCK_MAX_MINUTES = 60, LOCAL_CACHE_WRITE_DELAY_MS = 350, LOCAL_MEDIA_CACHE_MAX_BYTES, LOCAL_MEDIA_CACHE_MAX_ITEMS = 1000, LOCAL_MEDIA_ACCESS_TOUCH_MIN_MS, LOCAL_PROFILE_CACHE_MAX_ITEMS = 500, LOCAL_PROFILE_CACHE_MAX_AGE_MS, LOCAL_CHAT_CACHE_MAX_ITEMS = 1000, LOCAL_CHAT_MESSAGE_CACHE_MAX_VISIBLE = 1000, LOCAL_CHAT_MESSAGE_CACHE_WRITE_DELAY_MS = 2500, LOCAL_AVATAR_CACHE_MAX_BYTES, LOCAL_AVATAR_CACHE_MAX_AGE_MS, AVATAR_IMAGE_MAX_BYTES, AVATAR_IMAGE_QUALITY_ATTEMPTS, CHAT_AVATAR_IMAGE_MAX_BYTES, CHAT_AVATAR_REF_MAX_CHARS = 256, CHAT_MESSAGE_FILE_CACHE_MAX_BYTES, IDLE_CALLBACK_MIN_TIMEOUT_MS = 50, ATTACHMENT_CACHE_IDLE_TIMEOUT_MS = 2500, ATTACHMENT_CACHE_FALLBACK_DELAY_MS = 250, CHAT_UNSAVED_TTL_DAYS = 21, CHAT_UNSAVED_TTL_MS, CHAT_AFTER_SEEN_MS, CHAT_MEDIA_TTL_DAYS, CHAT_MEDIA_TTL_MS, CHAT_UPLOAD_MAX_BYTES, CHAT_AUDIO_TRANSCODE_BITRATE_BPS = 128000, CHAT_VIDEO_TRANSCODE_VIDEO_BITRATE_BPS = 3000000, CHAT_VIDEO_TRANSCODE_AUDIO_BITRATE_BPS, CHAT_MESSAGE_BATCH_SIZE = 40, CHAT_MESSAGE_QUERY_MAX_DOCS = 120, CHAT_TTL_CLIENT_DELETE_GRACE_MS, CHAT_TTL_DELETE_CHUNK_SIZE = 40, CHAT_DELETE_SCAN_BATCH_SIZE = 200, CHAT_DELETE_WRITE_BATCH_SIZE = 400, CHAT_BATCH_CLEANUP_IDLE_TIMEOUT_MS = 1500, CHAT_BATCH_CLEANUP_IDLE_DELAY_MS = 250, CHAT_LIST_PAGE_SIZE = 20, CHAT_LIST_LIVE_COUNT, CHAT_INBOX_PING_PAGE_SIZE = 25, CHAT_INBOX_CURSOR_OVERLAP_MS, CHAT_LIST_CACHE_WRITE_DELAY_MS = 1500, CHAT_LIST_SNAPSHOT_COALESCE_MS = 80, CHAT_LIST_LISTENER_RETRY_MS = 1000, CHAT_TOP_WARM_COUNT = 0, CHAT_EAGER_WARM_COUNT = 0, CHAT_WARM_DELAY_MS = 3000, CHAT_WARM_BATCH_SIZE, CHAT_MESSAGE_VIEW_CACHE_SIZE = 30, CHAT_MEDIA_WARM_MESSAGES_PER_CHAT, CHAT_MEDIA_WARM_START_DELAY_MS = 600, CHAT_MEDIA_WARM_STEP_DELAY_MS = 120, CHAT_MEDIA_WARM_TYPES, CHAT_MEDIA_WARM_MAX_BYTES = 0, CHAT_READ_WRITE_INTERVAL_MS, CHAT_READ_WRITE_MAX_WAIT_MS, CHAT_LIVE_PING_INTERVAL_MS, CHAT_LIVE_SWEEP_INTERVAL_MS, CHAT_LIVE_STALE_AFTER_MS, CHAT_LIVE_RECONNECT_MAX_MS, CHAT_LIVE_TYPING_IDLE_MS, CHAT_LIVE_TYPING_RENEW_MS, CHAT_LIVE_COMPOSITION_HANDOFF_MS, CHAT_LIVE_READ_SEND_INTERVAL_MS = 120, CHAT_LIVE_READ_SEND_MAX_WAIT_MS = 500, CHAT_LIVE_READ_WRITE_INTERVAL_MS, CHAT_LIVE_READ_WRITE_MAX_WAIT_MS, CHAT_SEND_QUEUE_RATE_LIMIT_COUNT = 12, CHAT_SEND_QUEUE_RATE_LIMIT_WINDOW_MS, CHAT_SETTINGS_BODY_MAX_BYTES, CHAT_MANIFEST_MAX_BYTES, CHAT_RECEIPT_MAX_MEMBERS = 32, CHAT_ORDINARY_DELIVERY_MAX_MEMBERS = 128, CHAT_MAX_MEMBERS = 256, CHAT_MAX_TEXT_CHARS = 2048, CHAT_TITLE_MAX_CHARS = 32, CHAT_MAX_REACTIONS, SEARCH_DEBOUNCE_MS = 300, SEARCH_USERNAME_LIMIT = 15, SEARCH_ROLE_LIMIT = 15, RECENT_PEER_REFRESH_LIMIT = 50, RECENT_PEER_REFRESH_DELAY_MS = 250, RECENT_PEER_REFRESH_INTERVAL_MS, RECENT_PEER_REFRESH_THROTTLE_MS = 120, BAN_REFRESH_GRACE_MS = 50, REQUEST_MONEY_MAX_SATS, WALLET_TRANSFER_POLL_MS, WALLET_ACTIVE_TRANSFER_REFRESH_MS, WALLET_IDLE_ACTIVE_TRANSFER_REFRESH_MS, WALLET_VISIBLE_TRANSFER_DISCOVERY_WINDOW_MS, WALLET_ACTIVE_TRANSFER_EMPTY_SCAN_LIMIT = 8, WALLET_CACHE_HYDRATE_DELAY_MS = 0, WALLET_BOOT_REFRESH_DELAY_MS = 500, WALLET_BOOT_CACHED_REFRESH_DELAY_MS = 2500, WALLET_REGTEST_DEPOSIT_CLAIM_POLL_MS, WALLET_MAINNET_DEPOSIT_CLAIM_POLL_MS, WALLET_BALANCE_EVENT_COALESCE_MS, WALLET_INCOMING_UPDATE_COALESCE_MS = 250, WALLET_AUTO_CLAIM_MAX_FEE_SATS = 5000, WALLET_CLAIM_PAGE_SIZE = 100, WALLET_PENDING_TRANSFER_CLAIM_BATCH_SIZE = 50, WALLET_PENDING_TRANSFER_STATUS_BATCH_SIZE = 2, WALLET_PENDING_TRANSFER_COLD_REFRESH_BATCH_SIZE = 50, WALLET_PENDING_TRANSFER_COLD_REFRESH_INTERVAL_MS, WALLET_PENDING_TRANSFER_ADAPTIVE_BATCH_SMALL_QUEUE = 3, WALLET_PENDING_TRANSFER_ADAPTIVE_BATCH_LARGE_QUEUE = 10, WALLET_PENDING_TRANSFER_ADAPTIVE_BATCH_SMALL_SIZE = 2, WALLET_PENDING_TRANSFER_ADAPTIVE_BATCH_LARGE_SIZE = 3, WALLET_PENDING_TRANSFER_SLOW_CLAIM_MS, WALLET_PENDING_TRANSFER_CLAIM_COOLDOWN_MS, WALLET_SDK_BACKGROUND_QUIET_MS, WALLET_RECENT_TRANSFER_LIMIT = 100, WALLET_TRANSFER_PAGE_LIMIT = 100, WALLET_TRANSFER_FETCH_THROTTLE_MS = 150, WALLET_PENDING_TRANSFER_CLAIM_RETRY_MS, WALLET_SENT_TRANSFER_REFRESH_DELAY_MS, WALLET_PENDING_TRANSFER_BOOT_GRACE_MS, WALLET_PENDING_TRANSFER_HOT_AGE_MS, WALLET_PENDING_TRANSFER_WARM_AGE_MS, WALLET_PENDING_TRANSFER_WARM_RETRY_MS, WALLET_PENDING_TRANSFER_STALE_RETRY_MS, WALLET_PENDING_TRANSFER_STUCK_RETRY_MS, WALLET_PENDING_TRANSFER_DORMANT_RETRY_MS, WALLET_TRANSFER_CACHE_WRITE_DELAY_MS;
6454
6454
  var init_config = __esm(() => {
6455
6455
  MINUTE_MS = 60 * MS_PER_SECOND;
6456
6456
  HOUR_MS = 60 * MINUTE_MS;
@@ -6463,8 +6463,8 @@ var init_config = __esm(() => {
6463
6463
  LOCAL_AVATAR_CACHE_MAX_BYTES = 128 * MIB_BYTES;
6464
6464
  LOCAL_AVATAR_CACHE_MAX_AGE_MS = 30 * DAY_MS;
6465
6465
  AVATAR_IMAGE_MAX_BYTES = 256 * KIB_BYTES;
6466
- CHAT_AVATAR_IMAGE_MAX_BYTES = 16 * KIB_BYTES;
6467
- CHAT_AVATAR_REF_MAX_CHARS = 24 * KIB_BYTES;
6466
+ AVATAR_IMAGE_QUALITY_ATTEMPTS = Object.freeze([0.92, 0.84, 0.76, 0.68]);
6467
+ CHAT_AVATAR_IMAGE_MAX_BYTES = AVATAR_IMAGE_MAX_BYTES;
6468
6468
  CHAT_MESSAGE_FILE_CACHE_MAX_BYTES = 64 * MIB_BYTES;
6469
6469
  CHAT_UNSAVED_TTL_MS = CHAT_UNSAVED_TTL_DAYS * DAY_MS;
6470
6470
  CHAT_AFTER_SEEN_MS = DAY_MS;
@@ -9424,6 +9424,430 @@ var init_user = __esm(() => {
9424
9424
  };
9425
9425
  });
9426
9426
 
9427
+ // ../../core/crypto/file.js
9428
+ function createFileKey() {
9429
+ return randomBytes3(32);
9430
+ }
9431
+ function encodeFileKey(key) {
9432
+ return toHex(toBytes32(key, "file key"));
9433
+ }
9434
+ function decodeFileKey(key) {
9435
+ return toBytes32(key, "file key");
9436
+ }
9437
+ function getFileAadForPath(scope) {
9438
+ if (!scope) {
9439
+ throw new Error("file scope required");
9440
+ }
9441
+ return encodeScope(FILE_SCOPE, [scope]);
9442
+ }
9443
+ function getSubtleCrypto() {
9444
+ const subtle = globalThis.crypto?.subtle;
9445
+ if (!subtle) {
9446
+ throw new Error("WebCrypto AES-GCM unavailable");
9447
+ }
9448
+ return subtle;
9449
+ }
9450
+ async function importAesKey(key, usages) {
9451
+ return getSubtleCrypto().importKey("raw", toBytes32(key, "file key"), { name: "AES-GCM" }, false, usages);
9452
+ }
9453
+ async function sealFile(key, bytes, scope) {
9454
+ const fileKey = new Uint8Array(toBytes32(key, "file key"));
9455
+ try {
9456
+ const cryptoKey = await importAesKey(fileKey, ["encrypt"]);
9457
+ const iv = randomBytes3(FILE_IV_BYTES);
9458
+ const ct = await getSubtleCrypto().encrypt({
9459
+ name: "AES-GCM",
9460
+ iv,
9461
+ tagLength: FILE_TAG_BYTES * 8,
9462
+ additionalData: getFileAadForPath(scope)
9463
+ }, cryptoKey, toBytes(bytes, "plaintext"));
9464
+ return packBodyData(iv, new Uint8Array(ct));
9465
+ } finally {
9466
+ cleanBytes(fileKey);
9467
+ }
9468
+ }
9469
+ async function openFileForPath(key, body, scope) {
9470
+ const fileKey = new Uint8Array(toBytes32(key, "file key"));
9471
+ try {
9472
+ const cryptoKey = await importAesKey(fileKey, ["decrypt"]);
9473
+ const { nonce, ct } = unpackBodyData(body, FILE_IV_BYTES);
9474
+ const pt = await getSubtleCrypto().decrypt({
9475
+ name: "AES-GCM",
9476
+ iv: nonce,
9477
+ tagLength: FILE_TAG_BYTES * 8,
9478
+ additionalData: getFileAadForPath(scope)
9479
+ }, cryptoKey, ct);
9480
+ return new Uint8Array(pt);
9481
+ } finally {
9482
+ cleanBytes(fileKey);
9483
+ }
9484
+ }
9485
+ var FILE_SCOPE = "file-body", FILE_IV_BYTES = 12, FILE_TAG_BYTES = 16;
9486
+ var init_file = __esm(() => {
9487
+ init_core();
9488
+ init_kdf();
9489
+ init_pack();
9490
+ "use client";
9491
+ });
9492
+
9493
+ // ../../core/chat/filepayload.js
9494
+ function cleanMediaEpochId(value) {
9495
+ const epochId = String(value || "").trim();
9496
+ if (!new RegExp(`^${CHAT_ID_PATTERN}$`).test(epochId)) {
9497
+ throw new Error("invalid media epoch id");
9498
+ }
9499
+ return epochId.toLowerCase();
9500
+ }
9501
+ function cleanMediaId(value) {
9502
+ const mediaId = String(value || "").trim();
9503
+ if (!new RegExp(`^${MEDIA_ID_PATTERN}$`).test(mediaId)) {
9504
+ throw new Error("invalid media id");
9505
+ }
9506
+ return mediaId.toLowerCase();
9507
+ }
9508
+ function cleanSharedMediaId(value) {
9509
+ const sharedId = String(value || "").trim();
9510
+ if (!new RegExp(`^${SHARED_MEDIA_ID_PATTERN}$`).test(sharedId)) {
9511
+ throw new Error("invalid shared media id");
9512
+ }
9513
+ return sharedId.toLowerCase();
9514
+ }
9515
+ function makeChatMediaId() {
9516
+ return toHex(randomBytes3(16));
9517
+ }
9518
+ function mediaFilePath(epochId, mediaId) {
9519
+ const nextEpochId = cleanMediaEpochId(epochId);
9520
+ const nextMediaId = cleanMediaId(mediaId);
9521
+ return `${CHAT_MEDIA_ROOT}/${nextEpochId}/${nextMediaId}`;
9522
+ }
9523
+ function sharedMediaFilePath(sharedId) {
9524
+ const nextSharedId = cleanSharedMediaId(sharedId);
9525
+ return `${SHARED_MEDIA_ROOT}/${nextSharedId}`;
9526
+ }
9527
+ function makeSharedMediaId() {
9528
+ return toHex(randomBytes3(16));
9529
+ }
9530
+ function getMediaFileRef(path) {
9531
+ const value = String(path || "").trim();
9532
+ const chatMatch = value.match(CHAT_MEDIA_FILE_PATTERN);
9533
+ if (chatMatch?.[1] && chatMatch?.[2]) {
9534
+ return {
9535
+ type: "chat",
9536
+ epochId: chatMatch[1].toLowerCase(),
9537
+ mediaId: chatMatch[2].toLowerCase()
9538
+ };
9539
+ }
9540
+ const sharedMatch = value.match(SHARED_MEDIA_FILE_PATTERN);
9541
+ if (sharedMatch?.[1]) {
9542
+ return {
9543
+ type: "shared",
9544
+ sharedId: sharedMatch[1].toLowerCase()
9545
+ };
9546
+ }
9547
+ throw new Error("invalid media file path");
9548
+ }
9549
+ function getChatMediaFileRef(path) {
9550
+ const ref = getMediaFileRef(path);
9551
+ if (ref?.type !== "chat") {
9552
+ throw new Error("invalid media file path");
9553
+ }
9554
+ return ref;
9555
+ }
9556
+ function getSharedMediaFileRef(path) {
9557
+ const ref = getMediaFileRef(path);
9558
+ if (ref?.type !== "shared") {
9559
+ throw new Error("invalid shared media file path");
9560
+ }
9561
+ return ref;
9562
+ }
9563
+ function uploadByteLength(value) {
9564
+ if (Number.isFinite(value?.byteLength)) {
9565
+ return value.byteLength;
9566
+ }
9567
+ if (Number.isFinite(value?.size)) {
9568
+ return value.size;
9569
+ }
9570
+ if (Number.isFinite(value)) {
9571
+ return value;
9572
+ }
9573
+ return null;
9574
+ }
9575
+ function makeChatUploadTooLargeError(bytes, maxBytes = MAX_CHAT_UPLOAD_BYTES) {
9576
+ const error = new Error("upload too large");
9577
+ error.code = "upload-too-large";
9578
+ error.maxBytes = maxBytes;
9579
+ if (Number.isFinite(bytes)) {
9580
+ error.bytes = bytes;
9581
+ }
9582
+ return error;
9583
+ }
9584
+ function assertChatUploadByteSize(bytes, maxBytes = MAX_CHAT_UPLOAD_BYTES) {
9585
+ const length = uploadByteLength(bytes);
9586
+ if (!Number.isFinite(length)) {
9587
+ throw new Error("upload bytes required");
9588
+ }
9589
+ if (length <= 0 || length > maxBytes) {
9590
+ throw makeChatUploadTooLargeError(length, maxBytes);
9591
+ }
9592
+ return length;
9593
+ }
9594
+ async function toUploadBytes(data) {
9595
+ if (typeof Blob !== "undefined" && data instanceof Blob) {
9596
+ if (typeof data.arrayBuffer === "function") {
9597
+ return new Uint8Array(await data.arrayBuffer());
9598
+ }
9599
+ if (typeof FileReader !== "undefined") {
9600
+ return new Promise((resolve, reject) => {
9601
+ const reader = new FileReader;
9602
+ reader.onload = () => resolve(new Uint8Array(reader.result));
9603
+ reader.onerror = () => reject(reader.error || new Error("blob read failed"));
9604
+ reader.readAsArrayBuffer(data);
9605
+ });
9606
+ }
9607
+ if (typeof Response !== "undefined") {
9608
+ return new Uint8Array(await new Response(data).arrayBuffer());
9609
+ }
9610
+ }
9611
+ if (typeof data?.arrayBuffer === "function") {
9612
+ return new Uint8Array(await data.arrayBuffer());
9613
+ }
9614
+ return toBytes(data, "upload bytes");
9615
+ }
9616
+ async function makeChatFileUploadPayload(epochId, cid, data, { cacheControl = "private, max-age=0, no-transform" } = {}) {
9617
+ const nextEpochId = cleanMediaEpochId(epochId);
9618
+ const mediaId = makeChatMediaId();
9619
+ const path = mediaFilePath(nextEpochId, mediaId);
9620
+ const expiresAt = Date.now() + CHAT_MEDIA_TTL_MS2;
9621
+ const key = createFileKey();
9622
+ try {
9623
+ const uploadBytes = await toUploadBytes(data);
9624
+ assertChatUploadByteSize(uploadBytes);
9625
+ const body = await sealFile(key, uploadBytes, path);
9626
+ assertChatUploadByteSize(body);
9627
+ return {
9628
+ epochId: nextEpochId,
9629
+ mediaId,
9630
+ path,
9631
+ body,
9632
+ metadata: {
9633
+ contentType: "application/octet-stream",
9634
+ cacheControl
9635
+ },
9636
+ file: {
9637
+ p: path,
9638
+ k: encodeFileKey(key),
9639
+ x: expiresAt
9640
+ }
9641
+ };
9642
+ } catch (error) {
9643
+ if (error && typeof error === "object") {
9644
+ error.path = error?.path || path;
9645
+ error.cid = error?.cid || cid;
9646
+ }
9647
+ throw error;
9648
+ } finally {
9649
+ cleanBytes(key);
9650
+ }
9651
+ }
9652
+ async function makeSharedFileUploadPayload(data, { contentType = "application/octet-stream", cacheControl = "private, max-age=0, no-transform" } = {}) {
9653
+ const sharedId = makeSharedMediaId();
9654
+ const path = sharedMediaFilePath(sharedId);
9655
+ const expiresAt = Date.now() + CHAT_MEDIA_TTL_MS2;
9656
+ const key = createFileKey();
9657
+ try {
9658
+ const uploadBytes = await toUploadBytes(data);
9659
+ assertChatUploadByteSize(uploadBytes);
9660
+ const body = await sealFile(key, uploadBytes, path);
9661
+ assertChatUploadByteSize(body);
9662
+ return {
9663
+ sharedId,
9664
+ path,
9665
+ body,
9666
+ metadata: {
9667
+ contentType,
9668
+ cacheControl
9669
+ },
9670
+ file: {
9671
+ p: path,
9672
+ k: encodeFileKey(key),
9673
+ x: expiresAt
9674
+ }
9675
+ };
9676
+ } catch (error) {
9677
+ if (error && typeof error === "object") {
9678
+ error.path = error?.path || path;
9679
+ error.sharedId = error?.sharedId || sharedId;
9680
+ }
9681
+ throw error;
9682
+ } finally {
9683
+ cleanBytes(key);
9684
+ }
9685
+ }
9686
+ var CHAT_MEDIA_ROOT = "chatEpochs", SHARED_MEDIA_ROOT = "shared", CHAT_MEDIA_TTL_MS2, MAX_CHAT_UPLOAD_BYTES, CHAT_VIDEO_TRANSCODE_VIDEO_BITRATE_BPS2, CHAT_VIDEO_TRANSCODE_AUDIO_BITRATE_BPS2, CHAT_VIDEO_TRANSCODE_TOTAL_BITRATE_BPS, CHAT_ID_PATTERN = "[0-9a-fA-F]{64}", SHARED_MEDIA_ID_PATTERN = "[0-9a-fA-F]{32}", MEDIA_ID_PATTERN = "[0-9a-fA-F]{32}", CHAT_MEDIA_FILE_PATTERN, SHARED_MEDIA_FILE_PATTERN;
9687
+ var init_filepayload = __esm(() => {
9688
+ init_file();
9689
+ init_config();
9690
+ init_core();
9691
+ "use client";
9692
+ CHAT_MEDIA_TTL_MS2 = CHAT_MEDIA_TTL_MS;
9693
+ MAX_CHAT_UPLOAD_BYTES = CHAT_UPLOAD_MAX_BYTES;
9694
+ CHAT_VIDEO_TRANSCODE_VIDEO_BITRATE_BPS2 = CHAT_VIDEO_TRANSCODE_VIDEO_BITRATE_BPS;
9695
+ CHAT_VIDEO_TRANSCODE_AUDIO_BITRATE_BPS2 = CHAT_VIDEO_TRANSCODE_AUDIO_BITRATE_BPS;
9696
+ CHAT_VIDEO_TRANSCODE_TOTAL_BITRATE_BPS = CHAT_VIDEO_TRANSCODE_VIDEO_BITRATE_BPS2 + CHAT_VIDEO_TRANSCODE_AUDIO_BITRATE_BPS2;
9697
+ CHAT_MEDIA_FILE_PATTERN = new RegExp(`^${CHAT_MEDIA_ROOT}/(${CHAT_ID_PATTERN})/(${MEDIA_ID_PATTERN})$`);
9698
+ SHARED_MEDIA_FILE_PATTERN = new RegExp(`^${SHARED_MEDIA_ROOT}/(${SHARED_MEDIA_ID_PATTERN})$`);
9699
+ });
9700
+
9701
+ // ../../core/files.js
9702
+ function makeFileId(size = 8) {
9703
+ return toHex(randomBytes3(size));
9704
+ }
9705
+ function setErrorStage(error, stage, extra = {}) {
9706
+ if (!error || typeof error !== "object") {
9707
+ return error;
9708
+ }
9709
+ error.stage = error?.stage || stage;
9710
+ Object.assign(error, extra);
9711
+ return error;
9712
+ }
9713
+ async function makeChatFileUpload(epochId, cid, data, { cacheControl = "private, max-age=0, no-transform" } = {}) {
9714
+ try {
9715
+ return await makeChatFileUploadPayload(epochId, cid, data, {
9716
+ cacheControl
9717
+ });
9718
+ } catch (error) {
9719
+ throw setErrorStage(error, "encrypt", {
9720
+ ...error?.path ? { path: error.path } : {},
9721
+ cid
9722
+ });
9723
+ }
9724
+ }
9725
+ async function putChatFile(epochId, cid, data, options) {
9726
+ const upload = await makeChatFileUpload(epochId, cid, data, options);
9727
+ try {
9728
+ if (typeof options?.uploadChatMedia !== "function") {
9729
+ throw new Error("chat media upload required");
9730
+ }
9731
+ await options.uploadChatMedia(upload);
9732
+ return upload.file;
9733
+ } catch (error) {
9734
+ throw setErrorStage(error, "upload", { path: upload.path, cid });
9735
+ }
9736
+ }
9737
+ async function makeSharedFileUpload(data, { contentType = "application/octet-stream", cacheControl = "private, max-age=0, no-transform" } = {}) {
9738
+ try {
9739
+ return await makeSharedFileUploadPayload(data, {
9740
+ contentType,
9741
+ cacheControl
9742
+ });
9743
+ } catch (error) {
9744
+ throw setErrorStage(error, "encrypt", {
9745
+ ...error?.path ? { path: error.path } : {}
9746
+ });
9747
+ }
9748
+ }
9749
+ async function putSharedFile(data, options = {}) {
9750
+ const upload = await makeSharedFileUpload(data, options);
9751
+ try {
9752
+ if (typeof options?.uploadSharedMedia !== "function") {
9753
+ throw new Error("shared media upload required");
9754
+ }
9755
+ await options.uploadSharedMedia(upload);
9756
+ return upload.file;
9757
+ } catch (error) {
9758
+ throw setErrorStage(error, "upload", { path: upload.path });
9759
+ }
9760
+ }
9761
+ async function readChatFile(readChatMedia, file) {
9762
+ let body;
9763
+ try {
9764
+ if (typeof readChatMedia !== "function") {
9765
+ throw new Error("chat media read required");
9766
+ }
9767
+ body = await readChatMedia(file?.p);
9768
+ } catch (error) {
9769
+ error.path = error?.path || file?.p || null;
9770
+ error.stage = error?.stage || "download";
9771
+ throw error;
9772
+ }
9773
+ try {
9774
+ getMediaFileRef(file?.p);
9775
+ const bytes = await openFileForPath(decodeFileKey(file?.k), body, file?.p);
9776
+ return bytes;
9777
+ } catch (error) {
9778
+ error.path = file?.p || null;
9779
+ error.stage = "decrypt";
9780
+ throw error;
9781
+ }
9782
+ }
9783
+ var init_files = __esm(() => {
9784
+ init_file();
9785
+ init_core();
9786
+ init_filepayload();
9787
+ "use client";
9788
+ });
9789
+
9790
+ // ../../core/chat/avatar.js
9791
+ function chatAvatarFile(value) {
9792
+ const ref = typeof value === "string" ? value.trim() : "";
9793
+ if (!ref.startsWith(CHAT_AVATAR_REF_PREFIX) || ref.length > CHAT_AVATAR_REF_MAX_CHARS)
9794
+ return null;
9795
+ const parts = ref.split(":");
9796
+ const key = parts[2]?.toLowerCase();
9797
+ if (parts.length !== 3 || parts[0] !== "v1" || !FILE_KEY_RE.test(key))
9798
+ return null;
9799
+ let parsed;
9800
+ try {
9801
+ parsed = getChatMediaFileRef(parts[1]);
9802
+ } catch {
9803
+ return null;
9804
+ }
9805
+ return Object.freeze({
9806
+ t: "img",
9807
+ m: "image/webp",
9808
+ p: mediaFilePath(parsed.epochId, parsed.mediaId),
9809
+ k: key
9810
+ });
9811
+ }
9812
+ function makeChatAvatarRef(file) {
9813
+ let parsed;
9814
+ const path = cleanText(file?.p);
9815
+ const key = cleanText(file?.k).toLowerCase();
9816
+ try {
9817
+ parsed = getChatMediaFileRef(path);
9818
+ } catch {
9819
+ throw new Error("invalid chat avatar");
9820
+ }
9821
+ if (!FILE_KEY_RE.test(key))
9822
+ throw new Error("invalid chat avatar");
9823
+ const ref = `${CHAT_AVATAR_REF_PREFIX}${mediaFilePath(parsed.epochId, parsed.mediaId)}:${key}`;
9824
+ if (ref.length > CHAT_AVATAR_REF_MAX_CHARS)
9825
+ throw new Error("invalid chat avatar");
9826
+ return ref;
9827
+ }
9828
+ function cleanChatAvatarRef(value) {
9829
+ if (value == null)
9830
+ return null;
9831
+ const file = chatAvatarFile(value);
9832
+ if (!file)
9833
+ throw new Error("invalid chat avatar");
9834
+ return makeChatAvatarRef(file);
9835
+ }
9836
+ function assertChatAvatarBytes(value) {
9837
+ const bytes = Number(value?.byteLength ?? value?.size);
9838
+ if (!Number.isSafeInteger(bytes) || bytes <= 0 || bytes > CHAT_AVATAR_IMAGE_MAX_BYTES) {
9839
+ throw new Error("invalid chat avatar");
9840
+ }
9841
+ return bytes;
9842
+ }
9843
+ var CHAT_AVATAR_REF_PREFIX = "v1:", FILE_KEY_RE;
9844
+ var init_avatar2 = __esm(() => {
9845
+ init_config();
9846
+ init_filepayload();
9847
+ "use client";
9848
+ FILE_KEY_RE = /^[0-9a-f]{64}$/u;
9849
+ });
9850
+
9427
9851
  // ../../core/chat/state.js
9428
9852
  function makeCid() {
9429
9853
  return `${Date.now().toString(36)}${toHex(randomBytes3(3))}`;
@@ -9689,6 +10113,11 @@ function makeChatUnavailableError() {
9689
10113
  error.code = "permission-denied";
9690
10114
  return error;
9691
10115
  }
10116
+ function isChatEpochConflict(error) {
10117
+ const code = String(error?.code || "").trim().toLowerCase();
10118
+ const message = String(error?.message || "").trim().toLowerCase();
10119
+ return code === "permission-denied" || code.endsWith("/permission-denied") || code === "aborted" || code.endsWith("/aborted") || code === "failed-precondition" || code.endsWith("/failed-precondition") || message.includes("insufficient permissions") || message === "chat owner epoch changed";
10120
+ }
9692
10121
  function makeMessageSaveUnavailableError() {
9693
10122
  const error = new Error("this message can't be saved anymore");
9694
10123
  error.code = "message-unavailable";
@@ -10168,34 +10597,6 @@ var init_chat = __esm(() => {
10168
10597
  MSG_BODY_HEADER_BYTES = 1 + 1 + 1 + BOX_NONCE_BYTES + ED25519_SIGNATURE_BYTES;
10169
10598
  });
10170
10599
 
10171
- // ../../core/chat/avatar.js
10172
- function cleanChatAvatarRef(value) {
10173
- if (value == null)
10174
- return null;
10175
- const ref = cleanText(value);
10176
- if (!ref || ref.length > CHAT_AVATAR_REF_MAX_CHARS) {
10177
- throw new Error("invalid chat avatar");
10178
- }
10179
- return ref;
10180
- }
10181
- function chatAvatarDataUrl(value) {
10182
- const ref = typeof value === "string" ? value.trim() : "";
10183
- if (!ref.startsWith(CHAT_AVATAR_PREFIX) || ref.length > CHAT_AVATAR_REF_MAX_CHARS)
10184
- return "";
10185
- const body = ref.slice(CHAT_AVATAR_PREFIX.length);
10186
- if (!body || body.length % 4 !== 0 || !BASE64_RE.test(body))
10187
- return "";
10188
- const padding2 = body.endsWith("==") ? 2 : body.endsWith("=") ? 1 : 0;
10189
- const bytes = body.length / 4 * 3 - padding2;
10190
- return bytes > 0 && bytes <= CHAT_AVATAR_IMAGE_MAX_BYTES ? ref : "";
10191
- }
10192
- var CHAT_AVATAR_PREFIX = "data:image/webp;base64,", BASE64_RE;
10193
- var init_avatar2 = __esm(() => {
10194
- init_config();
10195
- "use client";
10196
- BASE64_RE = /^[A-Za-z0-9+/]+={0,2}$/u;
10197
- });
10198
-
10199
10600
  // ../../core/chat/epochs/transition.js
10200
10601
  function cleanTime(value) {
10201
10602
  if (!Number.isSafeInteger(value) || value <= 0) {
@@ -10416,7 +10817,7 @@ function normalizeChatStateSettings(value = {}) {
10416
10817
  throw new Error("chat settings required");
10417
10818
  }
10418
10819
  const title = value.title == null ? null : cleanText(value.title);
10419
- if (title != null && (!title || title.length > 128)) {
10820
+ if (title != null && (!title || title.length > CHAT_TITLE_MAX_CHARS)) {
10420
10821
  throw new Error("invalid chat title");
10421
10822
  }
10422
10823
  return {
@@ -11010,369 +11411,6 @@ var init_owner = __esm(() => {
11010
11411
  "use client";
11011
11412
  });
11012
11413
 
11013
- // ../../core/crypto/file.js
11014
- function createFileKey() {
11015
- return randomBytes3(32);
11016
- }
11017
- function encodeFileKey(key) {
11018
- return toHex(toBytes32(key, "file key"));
11019
- }
11020
- function decodeFileKey(key) {
11021
- return toBytes32(key, "file key");
11022
- }
11023
- function getFileAadForPath(scope) {
11024
- if (!scope) {
11025
- throw new Error("file scope required");
11026
- }
11027
- return encodeScope(FILE_SCOPE, [scope]);
11028
- }
11029
- function getSubtleCrypto() {
11030
- const subtle = globalThis.crypto?.subtle;
11031
- if (!subtle) {
11032
- throw new Error("WebCrypto AES-GCM unavailable");
11033
- }
11034
- return subtle;
11035
- }
11036
- async function importAesKey(key, usages) {
11037
- return getSubtleCrypto().importKey("raw", toBytes32(key, "file key"), { name: "AES-GCM" }, false, usages);
11038
- }
11039
- async function sealFile(key, bytes, scope) {
11040
- const fileKey = new Uint8Array(toBytes32(key, "file key"));
11041
- try {
11042
- const cryptoKey = await importAesKey(fileKey, ["encrypt"]);
11043
- const iv = randomBytes3(FILE_IV_BYTES);
11044
- const ct = await getSubtleCrypto().encrypt({
11045
- name: "AES-GCM",
11046
- iv,
11047
- tagLength: FILE_TAG_BYTES * 8,
11048
- additionalData: getFileAadForPath(scope)
11049
- }, cryptoKey, toBytes(bytes, "plaintext"));
11050
- return packBodyData(iv, new Uint8Array(ct));
11051
- } finally {
11052
- cleanBytes(fileKey);
11053
- }
11054
- }
11055
- async function openFileForPath(key, body, scope) {
11056
- const fileKey = new Uint8Array(toBytes32(key, "file key"));
11057
- try {
11058
- const cryptoKey = await importAesKey(fileKey, ["decrypt"]);
11059
- const { nonce, ct } = unpackBodyData(body, FILE_IV_BYTES);
11060
- const pt = await getSubtleCrypto().decrypt({
11061
- name: "AES-GCM",
11062
- iv: nonce,
11063
- tagLength: FILE_TAG_BYTES * 8,
11064
- additionalData: getFileAadForPath(scope)
11065
- }, cryptoKey, ct);
11066
- return new Uint8Array(pt);
11067
- } finally {
11068
- cleanBytes(fileKey);
11069
- }
11070
- }
11071
- var FILE_SCOPE = "file-body", FILE_IV_BYTES = 12, FILE_TAG_BYTES = 16;
11072
- var init_file = __esm(() => {
11073
- init_core();
11074
- init_kdf();
11075
- init_pack();
11076
- "use client";
11077
- });
11078
-
11079
- // ../../core/chat/filepayload.js
11080
- function cleanMediaEpochId(value) {
11081
- const epochId = String(value || "").trim();
11082
- if (!new RegExp(`^${CHAT_ID_PATTERN}$`).test(epochId)) {
11083
- throw new Error("invalid media epoch id");
11084
- }
11085
- return epochId.toLowerCase();
11086
- }
11087
- function cleanMediaId(value) {
11088
- const mediaId = String(value || "").trim();
11089
- if (!new RegExp(`^${MEDIA_ID_PATTERN}$`).test(mediaId)) {
11090
- throw new Error("invalid media id");
11091
- }
11092
- return mediaId.toLowerCase();
11093
- }
11094
- function cleanSharedMediaId(value) {
11095
- const sharedId = String(value || "").trim();
11096
- if (!new RegExp(`^${SHARED_MEDIA_ID_PATTERN}$`).test(sharedId)) {
11097
- throw new Error("invalid shared media id");
11098
- }
11099
- return sharedId.toLowerCase();
11100
- }
11101
- function makeChatMediaId() {
11102
- return toHex(randomBytes3(16));
11103
- }
11104
- function mediaFilePath(epochId, mediaId) {
11105
- const nextEpochId = cleanMediaEpochId(epochId);
11106
- const nextMediaId = cleanMediaId(mediaId);
11107
- return `${CHAT_MEDIA_ROOT}/${nextEpochId}/${nextMediaId}`;
11108
- }
11109
- function sharedMediaFilePath(sharedId) {
11110
- const nextSharedId = cleanSharedMediaId(sharedId);
11111
- return `${SHARED_MEDIA_ROOT}/${nextSharedId}`;
11112
- }
11113
- function makeSharedMediaId() {
11114
- return toHex(randomBytes3(16));
11115
- }
11116
- function getMediaFileRef(path) {
11117
- const value = String(path || "").trim();
11118
- const chatMatch = value.match(CHAT_MEDIA_FILE_PATTERN);
11119
- if (chatMatch?.[1] && chatMatch?.[2]) {
11120
- return {
11121
- type: "chat",
11122
- epochId: chatMatch[1].toLowerCase(),
11123
- mediaId: chatMatch[2].toLowerCase()
11124
- };
11125
- }
11126
- const sharedMatch = value.match(SHARED_MEDIA_FILE_PATTERN);
11127
- if (sharedMatch?.[1]) {
11128
- return {
11129
- type: "shared",
11130
- sharedId: sharedMatch[1].toLowerCase()
11131
- };
11132
- }
11133
- throw new Error("invalid media file path");
11134
- }
11135
- function getChatMediaFileRef(path) {
11136
- const ref = getMediaFileRef(path);
11137
- if (ref?.type !== "chat") {
11138
- throw new Error("invalid media file path");
11139
- }
11140
- return ref;
11141
- }
11142
- function getSharedMediaFileRef(path) {
11143
- const ref = getMediaFileRef(path);
11144
- if (ref?.type !== "shared") {
11145
- throw new Error("invalid shared media file path");
11146
- }
11147
- return ref;
11148
- }
11149
- function uploadByteLength(value) {
11150
- if (Number.isFinite(value?.byteLength)) {
11151
- return value.byteLength;
11152
- }
11153
- if (Number.isFinite(value?.size)) {
11154
- return value.size;
11155
- }
11156
- if (Number.isFinite(value)) {
11157
- return value;
11158
- }
11159
- return null;
11160
- }
11161
- function makeChatUploadTooLargeError(bytes, maxBytes = MAX_CHAT_UPLOAD_BYTES) {
11162
- const error = new Error("upload too large");
11163
- error.code = "upload-too-large";
11164
- error.maxBytes = maxBytes;
11165
- if (Number.isFinite(bytes)) {
11166
- error.bytes = bytes;
11167
- }
11168
- return error;
11169
- }
11170
- function assertChatUploadByteSize(bytes, maxBytes = MAX_CHAT_UPLOAD_BYTES) {
11171
- const length = uploadByteLength(bytes);
11172
- if (!Number.isFinite(length)) {
11173
- throw new Error("upload bytes required");
11174
- }
11175
- if (length <= 0 || length > maxBytes) {
11176
- throw makeChatUploadTooLargeError(length, maxBytes);
11177
- }
11178
- return length;
11179
- }
11180
- async function toUploadBytes(data) {
11181
- if (typeof Blob !== "undefined" && data instanceof Blob) {
11182
- if (typeof data.arrayBuffer === "function") {
11183
- return new Uint8Array(await data.arrayBuffer());
11184
- }
11185
- if (typeof FileReader !== "undefined") {
11186
- return new Promise((resolve, reject) => {
11187
- const reader = new FileReader;
11188
- reader.onload = () => resolve(new Uint8Array(reader.result));
11189
- reader.onerror = () => reject(reader.error || new Error("blob read failed"));
11190
- reader.readAsArrayBuffer(data);
11191
- });
11192
- }
11193
- if (typeof Response !== "undefined") {
11194
- return new Uint8Array(await new Response(data).arrayBuffer());
11195
- }
11196
- }
11197
- if (typeof data?.arrayBuffer === "function") {
11198
- return new Uint8Array(await data.arrayBuffer());
11199
- }
11200
- return toBytes(data, "upload bytes");
11201
- }
11202
- async function makeChatFileUploadPayload(epochId, cid, data, { cacheControl = "private, max-age=0, no-transform" } = {}) {
11203
- const nextEpochId = cleanMediaEpochId(epochId);
11204
- const mediaId = makeChatMediaId();
11205
- const path = mediaFilePath(nextEpochId, mediaId);
11206
- const expiresAt = Date.now() + CHAT_MEDIA_TTL_MS2;
11207
- const key = createFileKey();
11208
- try {
11209
- const uploadBytes = await toUploadBytes(data);
11210
- assertChatUploadByteSize(uploadBytes);
11211
- const body = await sealFile(key, uploadBytes, path);
11212
- assertChatUploadByteSize(body);
11213
- return {
11214
- epochId: nextEpochId,
11215
- mediaId,
11216
- path,
11217
- body,
11218
- metadata: {
11219
- contentType: "application/octet-stream",
11220
- cacheControl
11221
- },
11222
- file: {
11223
- p: path,
11224
- k: encodeFileKey(key),
11225
- x: expiresAt
11226
- }
11227
- };
11228
- } catch (error) {
11229
- if (error && typeof error === "object") {
11230
- error.path = error?.path || path;
11231
- error.cid = error?.cid || cid;
11232
- }
11233
- throw error;
11234
- } finally {
11235
- cleanBytes(key);
11236
- }
11237
- }
11238
- async function makeSharedFileUploadPayload(data, { contentType = "application/octet-stream", cacheControl = "private, max-age=0, no-transform" } = {}) {
11239
- const sharedId = makeSharedMediaId();
11240
- const path = sharedMediaFilePath(sharedId);
11241
- const expiresAt = Date.now() + CHAT_MEDIA_TTL_MS2;
11242
- const key = createFileKey();
11243
- try {
11244
- const uploadBytes = await toUploadBytes(data);
11245
- assertChatUploadByteSize(uploadBytes);
11246
- const body = await sealFile(key, uploadBytes, path);
11247
- assertChatUploadByteSize(body);
11248
- return {
11249
- sharedId,
11250
- path,
11251
- body,
11252
- metadata: {
11253
- contentType,
11254
- cacheControl
11255
- },
11256
- file: {
11257
- p: path,
11258
- k: encodeFileKey(key),
11259
- x: expiresAt
11260
- }
11261
- };
11262
- } catch (error) {
11263
- if (error && typeof error === "object") {
11264
- error.path = error?.path || path;
11265
- error.sharedId = error?.sharedId || sharedId;
11266
- }
11267
- throw error;
11268
- } finally {
11269
- cleanBytes(key);
11270
- }
11271
- }
11272
- var CHAT_MEDIA_ROOT = "chatEpochs", SHARED_MEDIA_ROOT = "shared", CHAT_MEDIA_TTL_MS2, MAX_CHAT_UPLOAD_BYTES, CHAT_VIDEO_TRANSCODE_VIDEO_BITRATE_BPS2, CHAT_VIDEO_TRANSCODE_AUDIO_BITRATE_BPS2, CHAT_VIDEO_TRANSCODE_TOTAL_BITRATE_BPS, CHAT_ID_PATTERN = "[0-9a-fA-F]{64}", SHARED_MEDIA_ID_PATTERN = "[0-9a-fA-F]{32}", MEDIA_ID_PATTERN = "[0-9a-fA-F]{32}", CHAT_MEDIA_FILE_PATTERN, SHARED_MEDIA_FILE_PATTERN;
11273
- var init_filepayload = __esm(() => {
11274
- init_file();
11275
- init_config();
11276
- init_core();
11277
- "use client";
11278
- CHAT_MEDIA_TTL_MS2 = CHAT_MEDIA_TTL_MS;
11279
- MAX_CHAT_UPLOAD_BYTES = CHAT_UPLOAD_MAX_BYTES;
11280
- CHAT_VIDEO_TRANSCODE_VIDEO_BITRATE_BPS2 = CHAT_VIDEO_TRANSCODE_VIDEO_BITRATE_BPS;
11281
- CHAT_VIDEO_TRANSCODE_AUDIO_BITRATE_BPS2 = CHAT_VIDEO_TRANSCODE_AUDIO_BITRATE_BPS;
11282
- CHAT_VIDEO_TRANSCODE_TOTAL_BITRATE_BPS = CHAT_VIDEO_TRANSCODE_VIDEO_BITRATE_BPS2 + CHAT_VIDEO_TRANSCODE_AUDIO_BITRATE_BPS2;
11283
- CHAT_MEDIA_FILE_PATTERN = new RegExp(`^${CHAT_MEDIA_ROOT}/(${CHAT_ID_PATTERN})/(${MEDIA_ID_PATTERN})$`);
11284
- SHARED_MEDIA_FILE_PATTERN = new RegExp(`^${SHARED_MEDIA_ROOT}/(${SHARED_MEDIA_ID_PATTERN})$`);
11285
- });
11286
-
11287
- // ../../core/files.js
11288
- function makeFileId(size = 8) {
11289
- return toHex(randomBytes3(size));
11290
- }
11291
- function setErrorStage(error, stage, extra = {}) {
11292
- if (!error || typeof error !== "object") {
11293
- return error;
11294
- }
11295
- error.stage = error?.stage || stage;
11296
- Object.assign(error, extra);
11297
- return error;
11298
- }
11299
- async function makeChatFileUpload(epochId, cid, data, { cacheControl = "private, max-age=0, no-transform" } = {}) {
11300
- try {
11301
- return await makeChatFileUploadPayload(epochId, cid, data, {
11302
- cacheControl
11303
- });
11304
- } catch (error) {
11305
- throw setErrorStage(error, "encrypt", {
11306
- ...error?.path ? { path: error.path } : {},
11307
- cid
11308
- });
11309
- }
11310
- }
11311
- async function putChatFile(epochId, cid, data, options) {
11312
- const upload = await makeChatFileUpload(epochId, cid, data, options);
11313
- try {
11314
- if (typeof options?.uploadChatMedia !== "function") {
11315
- throw new Error("chat media upload required");
11316
- }
11317
- await options.uploadChatMedia(upload);
11318
- return upload.file;
11319
- } catch (error) {
11320
- throw setErrorStage(error, "upload", { path: upload.path, cid });
11321
- }
11322
- }
11323
- async function makeSharedFileUpload(data, { contentType = "application/octet-stream", cacheControl = "private, max-age=0, no-transform" } = {}) {
11324
- try {
11325
- return await makeSharedFileUploadPayload(data, {
11326
- contentType,
11327
- cacheControl
11328
- });
11329
- } catch (error) {
11330
- throw setErrorStage(error, "encrypt", {
11331
- ...error?.path ? { path: error.path } : {}
11332
- });
11333
- }
11334
- }
11335
- async function putSharedFile(data, options = {}) {
11336
- const upload = await makeSharedFileUpload(data, options);
11337
- try {
11338
- if (typeof options?.uploadSharedMedia !== "function") {
11339
- throw new Error("shared media upload required");
11340
- }
11341
- await options.uploadSharedMedia(upload);
11342
- return upload.file;
11343
- } catch (error) {
11344
- throw setErrorStage(error, "upload", { path: upload.path });
11345
- }
11346
- }
11347
- async function readChatFile(readChatMedia, file) {
11348
- let body;
11349
- try {
11350
- if (typeof readChatMedia !== "function") {
11351
- throw new Error("chat media read required");
11352
- }
11353
- body = await readChatMedia(file?.p);
11354
- } catch (error) {
11355
- error.path = error?.path || file?.p || null;
11356
- error.stage = error?.stage || "download";
11357
- throw error;
11358
- }
11359
- try {
11360
- getMediaFileRef(file?.p);
11361
- const bytes = await openFileForPath(decodeFileKey(file?.k), body, file?.p);
11362
- return bytes;
11363
- } catch (error) {
11364
- error.path = file?.p || null;
11365
- error.stage = "decrypt";
11366
- throw error;
11367
- }
11368
- }
11369
- var init_files = __esm(() => {
11370
- init_file();
11371
- init_core();
11372
- init_filepayload();
11373
- "use client";
11374
- });
11375
-
11376
11414
  // ../../core/chat/messages/types.js
11377
11415
  var ATTACHMENT_MSG_TYPES, MAX_TXT_CHARS, REACTION_MSG_TYPE = "rxn", DELETE_MSG_TYPE = "del", SYSTEM_MSG_TYPE = "sys", EPOCH_TRANSITION_MSG_TYPE = "epoch", EPOCH_PROPOSAL_MSG_TYPE = "epoch_proposal", SYSTEM_SETTINGS_KIND = "settings", DEFAULT_REACTION_EMOJI = "❤️", MAX_REACTIONS, HOLD_VISIBLE_KEY = "__holdVisible", SOURCE_GONE_VISIBLE_KEY = "__sourceGoneVisible";
11378
11416
  var init_types = __esm(() => {
@@ -12320,7 +12358,7 @@ function cleanChatSettingsEventTitle(value) {
12320
12358
  if (value == null)
12321
12359
  return null;
12322
12360
  const title = cleanText(value);
12323
- if (!title || title.length > 128)
12361
+ if (!title || title.length > CHAT_TITLE_MAX_CHARS)
12324
12362
  throw new Error("invalid chat settings event title");
12325
12363
  return title;
12326
12364
  }
@@ -12335,10 +12373,7 @@ function makeChatSettingsMsg(revision, digest, changes = [], values = {}) {
12335
12373
  const titleChanged = cleanChanges.includes("title");
12336
12374
  const avatarChanged = cleanChanges.includes("avatarRef");
12337
12375
  const title = titleChanged ? cleanChatSettingsEventTitle(values?.title) : undefined;
12338
- const avatarRef = avatarChanged ? values?.avatarRef == null ? null : chatAvatarDataUrl(values.avatarRef) : undefined;
12339
- if (avatarChanged && values?.avatarRef != null && !avatarRef) {
12340
- throw new Error("invalid chat settings event avatar");
12341
- }
12376
+ const avatarRef = avatarChanged ? values?.avatarRef == null ? null : cleanChatAvatarRef(values.avatarRef) : undefined;
12342
12377
  return {
12343
12378
  t: SYSTEM_MSG_TYPE,
12344
12379
  sys: SYSTEM_SETTINGS_KIND,
@@ -12361,7 +12396,7 @@ function isChatSettingsMsg(msg) {
12361
12396
  return false;
12362
12397
  }
12363
12398
  }
12364
- return !changes.includes("avatarRef") || msg?.avatarRef == null || !!chatAvatarDataUrl(msg.avatarRef);
12399
+ return !changes.includes("avatarRef") || msg?.avatarRef == null || !!chatAvatarFile(msg.avatarRef);
12365
12400
  }
12366
12401
  function isPendingChatSettingsMsg(msg) {
12367
12402
  return msg?.t === SYSTEM_MSG_TYPE && msg?.sys === SYSTEM_SETTINGS_KIND && msg?.pending === true && String(msg?.id || "").startsWith("local:") && chatSettingsEventChanges(msg).length === 1 && chatSettingsEventChanges(msg)[0] === "retention";
@@ -12369,7 +12404,7 @@ function isPendingChatSettingsMsg(msg) {
12369
12404
  function getChatSettingsEventAvatar(msg) {
12370
12405
  if (!isChatSettingsMsg(msg) || !chatSettingsEventChanges(msg).includes("avatarRef"))
12371
12406
  return "";
12372
- return chatAvatarDataUrl(msg?.avatarRef);
12407
+ return chatAvatarFile(msg?.avatarRef) ? msg.avatarRef : "";
12373
12408
  }
12374
12409
  function isSystemMsg(msg) {
12375
12410
  return !!getSystemMsgText(msg) && (msg?.t === SYSTEM_MSG_TYPE || isMembershipEventMsg(msg));
@@ -12734,6 +12769,7 @@ var init_control = __esm(() => {
12734
12769
  init_messagekeys();
12735
12770
  init_ttl();
12736
12771
  init_actions();
12772
+ init_config();
12737
12773
  init_avatar2();
12738
12774
  init_files2();
12739
12775
  init_text();
@@ -13240,13 +13276,12 @@ function withChatPreviewExpired(preview, expiredAt) {
13240
13276
  sourceTs: getChatPreviewSourceTs(preview)
13241
13277
  }));
13242
13278
  }
13243
- function chatPreviewWantsAttention(preview, chatPK) {
13279
+ function chatPreviewCountsAsUnseen(preview, chatPK) {
13244
13280
  if (!preview || fromSelf(preview, chatPK)) {
13245
13281
  return false;
13246
13282
  }
13247
13283
  if (isReactionMsg(preview)) {
13248
- const targetFrom = cleanText(preview?.targetFrom);
13249
- return !!cleanText(preview?.emoji) && (!targetFrom || targetFrom === chatPK);
13284
+ return !!cleanText(preview?.emoji);
13250
13285
  }
13251
13286
  return canRenderPreviewContent(preview);
13252
13287
  }
@@ -15925,7 +15960,7 @@ function getPeersFromChats(chats) {
15925
15960
  }
15926
15961
  function isChatUnseenForUser(chatData, userChatPK) {
15927
15962
  const last = chatData?.preview;
15928
- if (!last?.ts || !chatPreviewWantsAttention(last, userChatPK))
15963
+ if (!last?.ts || !chatPreviewCountsAsUnseen(last, userChatPK))
15929
15964
  return false;
15930
15965
  const readMs = timestampMs(chatData?.readMs, null);
15931
15966
  const lastMs = timestampMs(last.ts, null);
@@ -18272,11 +18307,13 @@ function createChatDelete({
18272
18307
  listActionsRef,
18273
18308
  closeMessageBatchRef,
18274
18309
  mutationGate,
18310
+ reconcileEpoch,
18275
18311
  diag
18276
18312
  }) {
18277
18313
  const pendingDeleteIdsRef = { current: new Set };
18278
18314
  const locallyDeletedChatIdsRef = { current: new Set };
18279
18315
  const deletedChatIdsRef = { current: new Set };
18316
+ const removedChatVersionsRef = { current: new Map };
18280
18317
  const keepSelectedDeletedChatIdsRef = { current: new Set };
18281
18318
  const isChatPendingDelete = (chatId) => !!chatId && pendingDeleteIdsRef.current.has(chatId);
18282
18319
  const clearDeletedChatState = (chatIds) => {
@@ -18386,20 +18423,46 @@ function createChatDelete({
18386
18423
  listActionsRef.current?.commitServerChats?.(nextServerChats, { warm: false });
18387
18424
  setSelectedChat((current) => current === chatId ? null : current);
18388
18425
  };
18389
- const dropRemovedChat = (chatId) => {
18390
- if (!chatId)
18426
+ const dropRemovedChat = (chatId, epochVersion) => {
18427
+ if (!chatId || !Number.isSafeInteger(epochVersion) || epochVersion < 1)
18391
18428
  return false;
18392
- deletedChatIdsRef.current.add(chatId);
18393
- locallyDeletedChatIdsRef.current.add(chatId);
18429
+ const previousVersion = removedChatVersionsRef.current.get(chatId) || 0;
18430
+ removedChatVersionsRef.current.set(chatId, Math.max(previousVersion, epochVersion));
18394
18431
  dropChat(chatId);
18395
18432
  return true;
18396
18433
  };
18434
+ const commitRemovedRetirement = (chatId, epochVersion) => {
18435
+ if (!chatId || !Number.isSafeInteger(epochVersion) || epochVersion < 1)
18436
+ return false;
18437
+ const previousVersion = removedChatVersionsRef.current.get(chatId) || 0;
18438
+ removedChatVersionsRef.current.set(chatId, Math.max(previousVersion, epochVersion));
18439
+ pendingDeleteIdsRef.current.delete(chatId);
18440
+ locallyDeletedChatIdsRef.current.delete(chatId);
18441
+ deletedChatIdsRef.current.delete(chatId);
18442
+ keepSelectedDeletedChatIdsRef.current.delete(chatId);
18443
+ confirmDeletedChats([chatId]);
18444
+ return true;
18445
+ };
18446
+ const restoreRemovedChat = (chat) => {
18447
+ const chatId = chat?.id;
18448
+ const removedAtVersion = removedChatVersionsRef.current.get(chatId);
18449
+ if (!chatId || removedAtVersion == null)
18450
+ return false;
18451
+ if (!Number.isSafeInteger(chat.epochVersion) || chat.epochVersion <= removedAtVersion || !Array.isArray(chat.memberChatPKs) || !chat.memberChatPKs.includes(chatPK))
18452
+ return false;
18453
+ removedChatVersionsRef.current.delete(chatId);
18454
+ return true;
18455
+ };
18397
18456
  const dropUnavailableChat = (chatId) => {
18398
18457
  if (!chatId)
18399
18458
  return Promise.resolve(false);
18400
18459
  return (async () => {
18401
18460
  const releaseMutation = await mutationGate?.acquireExclusive(chatId) || (() => {});
18402
18461
  try {
18462
+ if (removedChatVersionsRef.current.has(chatId)) {
18463
+ dropChat(chatId);
18464
+ return true;
18465
+ }
18403
18466
  const chat = lastServerChatsRef.current.find((item) => item?.id === chatId);
18404
18467
  if (chat?.deliveryRegistered && chat?.epochVersion && chatPK && chat?.epochState?.stateCapability) {
18405
18468
  await cloud.delivery.revoke(deriveChatDeliveryCapability(chat.epochState.stateCapability, {
@@ -18476,39 +18539,74 @@ function createChatDelete({
18476
18539
  }, target.members, target.expectedEpochId);
18477
18540
  return result.deliveredCount;
18478
18541
  };
18542
+ const deleteCurrentChat = async (input, options = {}) => {
18543
+ const chatId = cleanText(typeof input === "string" ? input : input?.chatId || input?.id);
18544
+ const attemptedEpochs = new Set;
18545
+ while (chatId) {
18546
+ const target = getDeleteTargets(input)[0];
18547
+ if (!target)
18548
+ return null;
18549
+ const peerChatPK = cleanText(typeof input === "object" ? input?.peerChatPK : "") || cleanText(lastServerChatsRef.current.find((item) => item?.id === chatId)?.peerChatPK);
18550
+ const releaseMutation = await mutationGate?.acquireExclusive([chatId, peerChatPK].filter(Boolean)) || (() => {});
18551
+ let released = false;
18552
+ const release = () => {
18553
+ if (released)
18554
+ return;
18555
+ released = true;
18556
+ releaseMutation();
18557
+ };
18558
+ try {
18559
+ const deletion = await cloud.chat.delete(target.chatId, {
18560
+ entryId: target.entryId,
18561
+ expectedEpochId: target.expectedEpochId,
18562
+ stateCapability: target.stateCapability,
18563
+ cleanup: options.cleanup !== false
18564
+ });
18565
+ if (deletion?.newlyDeleted !== false)
18566
+ await wakeDeletedChat(target);
18567
+ return target;
18568
+ } catch (error) {
18569
+ release();
18570
+ const attemptKey = `${target.epochVersion || 0}:${target.expectedEpochId || ""}`;
18571
+ if (!attemptKey || attemptedEpochs.has(attemptKey) || !isChatEpochConflict(error) || typeof reconcileEpoch !== "function")
18572
+ throw error;
18573
+ attemptedEpochs.add(attemptKey);
18574
+ const reconciled = await reconcileEpoch(chatId);
18575
+ const current = lastServerChatsRef.current.find((item) => item?.id === chatId) || null;
18576
+ if (reconciled?.removed || !current)
18577
+ return null;
18578
+ if (!Number.isSafeInteger(current.epochVersion) || current.epochVersion <= target.epochVersion) {
18579
+ throw error;
18580
+ }
18581
+ } finally {
18582
+ release();
18583
+ }
18584
+ }
18585
+ return null;
18586
+ };
18479
18587
  const deleteChat = async (chat, options = {}) => {
18480
- const targets = getDeleteTargets(chat);
18588
+ const inputs = Array.isArray(chat) ? chat : [chat];
18589
+ const targets = getDeleteTargets(inputs);
18481
18590
  if (!targets.length)
18482
18591
  return false;
18483
18592
  const chatIds = targets.map((target) => target.chatId);
18484
- const peerChatPKs = chatIds.map((chatId) => {
18485
- const input = (Array.isArray(chat) ? chat : [chat]).find((item) => cleanText(typeof item === "string" ? item : item?.chatId || item?.id) === chatId);
18486
- return cleanText(typeof input === "object" ? input?.peerChatPK : "") || cleanText(lastServerChatsRef.current.find((item) => item?.id === chatId)?.peerChatPK);
18487
- }).filter(Boolean);
18488
- const releaseMutation = await mutationGate?.acquireExclusive([...chatIds, ...peerChatPKs]) || (() => {});
18593
+ const deletedTargets = [];
18594
+ hideDeletingChats(chatIds, options);
18489
18595
  try {
18490
- hideDeletingChats(chatIds, options);
18491
- try {
18492
- for (const target of targets) {
18493
- await cloud.chat.delete(target.chatId, {
18494
- entryId: target.entryId,
18495
- expectedEpochId: target.expectedEpochId,
18496
- stateCapability: target.stateCapability,
18497
- cleanup: options.cleanup !== false
18498
- });
18499
- await wakeDeletedChat(target);
18500
- }
18501
- await revokeTargetDeliveries(targets);
18502
- await options.onDeliveryRevoked?.();
18503
- confirmDeletedChats(chatIds);
18504
- } catch (error) {
18505
- restoreDeletedChats(chatIds);
18506
- throw error;
18507
- }
18508
- return Array.isArray(chat) ? targets.length : true;
18509
- } finally {
18510
- releaseMutation();
18596
+ for (const target of targets) {
18597
+ const input = inputs.find((item) => cleanText(typeof item === "string" ? item : item?.chatId || item?.id) === target.chatId) || target.chatId;
18598
+ const deleted = await deleteCurrentChat(input, options);
18599
+ if (deleted)
18600
+ deletedTargets.push(deleted);
18601
+ }
18602
+ await revokeTargetDeliveries(deletedTargets);
18603
+ await options.onDeliveryRevoked?.();
18604
+ confirmDeletedChats(chatIds);
18605
+ } catch (error) {
18606
+ restoreDeletedChats(chatIds);
18607
+ throw error;
18511
18608
  }
18609
+ return Array.isArray(chat) ? targets.length : true;
18512
18610
  };
18513
18611
  const wasChatDeletedLocally = (chatId) => !!chatId && locallyDeletedChatIdsRef.current.has(chatId);
18514
18612
  const ackDeletedChat = (chatId) => {
@@ -18519,21 +18617,25 @@ function createChatDelete({
18519
18617
  pendingDeleteIdsRef.current = new Set;
18520
18618
  locallyDeletedChatIdsRef.current = new Set;
18521
18619
  deletedChatIdsRef.current = new Set;
18620
+ removedChatVersionsRef.current = new Map;
18522
18621
  keepSelectedDeletedChatIdsRef.current = new Set;
18523
18622
  };
18524
18623
  return {
18525
18624
  pendingDeleteIdsRef,
18526
18625
  deletedChatIdsRef,
18626
+ removedChatVersionsRef,
18527
18627
  keepSelectedDeletedChatIdsRef,
18528
18628
  resetDeleteState,
18529
18629
  clearDeletedChatState,
18530
18630
  beginRetirement,
18531
18631
  rollbackRetirement,
18532
18632
  commitRetirement,
18633
+ commitRemovedRetirement,
18533
18634
  restoreDeletedChat,
18534
18635
  deleteChat,
18535
18636
  dropChat,
18536
18637
  dropRemovedChat,
18638
+ restoreRemovedChat,
18537
18639
  dropUnavailableChat,
18538
18640
  wasChatDeletedLocally,
18539
18641
  ackDeletedChat,
@@ -20039,7 +20141,73 @@ function withoutChatIdentity(options = {}) {
20039
20141
  }
20040
20142
  function refreshQueuedSendOptions(sendOptionsForPeer, peerChatPK, sendOptionOverrides = null) {
20041
20143
  const chatId = cleanText(sendOptionOverrides?.chatId);
20042
- return mergeSendOptions(withoutChatIdentity(sendOptionOverrides), sendOptionsForPeer(peerChatPK, chatId));
20144
+ const current = sendOptionsForPeer(peerChatPK, chatId);
20145
+ if (chatId) {
20146
+ return {
20147
+ ...withoutChatIdentity(sendOptionOverrides),
20148
+ ...current
20149
+ };
20150
+ }
20151
+ return mergeSendOptions(withoutChatIdentity(sendOptionOverrides), current);
20152
+ }
20153
+ function currentEpoch(options) {
20154
+ const manifest = options?.epochState?.manifest || options?.ownEntry?.current?.manifest || null;
20155
+ const epochId = cleanText(manifest?.epochId);
20156
+ const epochVersion = Number(manifest?.epochVersion);
20157
+ return epochId && Number.isInteger(epochVersion) && epochVersion > 0 ? { epochId, epochVersion } : null;
20158
+ }
20159
+ async function runCurrentEpochWrite({
20160
+ chatId,
20161
+ mutationGate,
20162
+ readOptions,
20163
+ reconcileEpoch,
20164
+ write,
20165
+ onCommitted,
20166
+ onEpochAdvance
20167
+ }) {
20168
+ const attemptedEpochs = new Set;
20169
+ while (true) {
20170
+ const releaseMutation = await mutationGate?.acquireShared(chatId) || (() => {});
20171
+ let released = false;
20172
+ let committed = false;
20173
+ const release = () => {
20174
+ if (released)
20175
+ return;
20176
+ released = true;
20177
+ releaseMutation();
20178
+ };
20179
+ let options;
20180
+ let epoch;
20181
+ try {
20182
+ options = readOptions();
20183
+ epoch = currentEpoch(options);
20184
+ return await write(options, (...args) => {
20185
+ committed = true;
20186
+ release();
20187
+ onCommitted?.(...args);
20188
+ });
20189
+ } catch (error) {
20190
+ release();
20191
+ const attemptKey = epoch ? `${epoch.epochVersion}:${epoch.epochId}` : "";
20192
+ if (committed || options?.chatExists !== true || !attemptKey || attemptedEpochs.has(attemptKey) || !isChatEpochConflict(error) || typeof reconcileEpoch !== "function") {
20193
+ throw error;
20194
+ }
20195
+ attemptedEpochs.add(attemptKey);
20196
+ await reconcileEpoch(chatId);
20197
+ const nextEpoch = currentEpoch(readOptions());
20198
+ if (!nextEpoch || nextEpoch.epochVersion <= epoch.epochVersion || nextEpoch.epochId === epoch.epochId) {
20199
+ throw error;
20200
+ }
20201
+ onEpochAdvance?.({
20202
+ fromEpochId: epoch.epochId,
20203
+ fromEpochVersion: epoch.epochVersion,
20204
+ toEpochId: nextEpoch.epochId,
20205
+ toEpochVersion: nextEpoch.epochVersion
20206
+ });
20207
+ } finally {
20208
+ release();
20209
+ }
20210
+ }
20043
20211
  }
20044
20212
  function markSendPerf(diag, label, startedAt, data = {}, minMs = SEND_CHAT_REDUCER_WARN_MS) {
20045
20213
  const elapsedMs = Date.now() - startedAt;
@@ -20304,7 +20472,7 @@ async function makeSharedMediaAttachment(cloud, media, message, data) {
20304
20472
  ...source?.c ? { caption: source.c } : {}
20305
20473
  }));
20306
20474
  }
20307
- function createChatSend({ cloud, media = {}, uid, chatBanned, chatPK, chatSigningPK, chatSigningSecret, chatPrivateKey, notificationPK, notificationPrivateKey, localCache, localByChatRef, setLocalByChat, setChats, setLastChat, sendOptionsForPeer, selectLocalChat, adoptLocalMessageMedia, readMessageFile, mutationGate, diag = null }) {
20475
+ function createChatSend({ cloud, media = {}, uid, chatBanned, chatPK, chatSigningPK, chatSigningSecret, chatPrivateKey, notificationPK, notificationPrivateKey, localCache, localByChatRef, setLocalByChat, setChats, setLastChat, sendOptionsForPeer, selectLocalChat, adoptLocalMessageMedia, readMessageFile, mutationGate, reconcileEpoch, diag = null }) {
20308
20476
  const adoptedLocalMediaRef = { current: new Set };
20309
20477
  const attachmentPrepareByCidRef = { current: new Map };
20310
20478
  const cachedLocalMediaRef = { current: new Set };
@@ -20664,78 +20832,80 @@ ${cid}` : "";
20664
20832
  };
20665
20833
  const queueSend = async (peerChatPK, message, run, { previewRequired = false, sendOptions: sendOptionOverrides = null } = {}) => {
20666
20834
  const startedAt = Date.now();
20667
- let sendOptions = refreshQueuedSendOptions(sendOptionsForPeer, peerChatPK, sendOptionOverrides);
20668
- let local = await showLocalMessage(peerChatPK, message, sendOptions);
20835
+ const sendOptions = refreshQueuedSendOptions(sendOptionsForPeer, peerChatPK, sendOptionOverrides);
20836
+ const local = await showLocalMessage(peerChatPK, message, sendOptions);
20669
20837
  const mutationTarget = local.chatId || cleanText(sendOptionOverrides?.chatId) || peerChatPK;
20670
- let releaseMutation;
20671
- try {
20672
- releaseMutation = await mutationGate?.acquireShared(mutationTarget) || (() => {});
20673
- } catch (error) {
20674
- markLocalStatus(local.chatId, local.cid, LOCAL_FAILED);
20675
- throw error;
20676
- }
20677
- try {
20678
- sendOptions = refreshQueuedSendOptions(sendOptionsForPeer, peerChatPK, {
20679
- ...sendOptionOverrides,
20680
- chatId: local.chatId
20681
- });
20682
- const requiredPreview = previewRequired || sendOptions.chatExists !== true;
20683
- let committed = false;
20684
- let sent = false;
20685
- const markSent = () => {
20686
- if (sent)
20687
- return;
20688
- sent = true;
20689
- sentChatIdsRef.current.add(local.chatId);
20690
- markLocalStatus(local.chatId, local.cid, LOCAL_SENT);
20691
- };
20692
- const markCommitted = () => {
20693
- if (committed)
20694
- return;
20695
- committed = true;
20696
- markSent();
20697
- markDone(diag, "chat.send.commit", startedAt, { type: local?.t || "txt", previewRequired: !!requiredPreview });
20698
- };
20699
- return await new Promise((resolve, reject) => {
20700
- const job = {
20701
- previewKey: local.chatId,
20702
- previewRequired: requiredPreview,
20703
- resolve,
20704
- reject,
20705
- onSuccess: markSent,
20706
- onError: (error) => {
20707
- markError(diag, "chat.send.job", startedAt, error, { type: local?.t || "txt", previewRequired: !!requiredPreview });
20708
- if (committed)
20709
- markSent();
20710
- else
20711
- markLocalStatus(local.chatId, local.cid, LOCAL_FAILED);
20712
- },
20713
- run: async (context) => {
20714
- const writeStartedAt = Date.now();
20715
- const onCommitted = () => {
20716
- markCommitted();
20717
- context?.releaseTargets?.();
20718
- };
20719
- markDiag(diag, "chat.send.write.start", { type: local?.t || "txt", previewRequired: !!requiredPreview, updatePreview: !!context?.updatePreview });
20720
- try {
20721
- const result = await run({ ...context, local, onCommitted, sendOptions });
20722
- markDone(diag, "chat.send.write", writeStartedAt, { type: local?.t || "txt", previewRequired: !!requiredPreview, updatePreview: !!context?.updatePreview });
20723
- return result;
20724
- } catch (error) {
20725
- markError(diag, "chat.send.write", writeStartedAt, error, {
20726
- type: local?.t || "txt",
20727
- previewRequired: !!requiredPreview,
20728
- updatePreview: !!context?.updatePreview
20729
- });
20730
- throw error;
20731
- }
20838
+ const requiredPreview = previewRequired || sendOptions.chatExists !== true;
20839
+ let committed = false;
20840
+ let sent = false;
20841
+ const markSent = () => {
20842
+ if (sent)
20843
+ return;
20844
+ sent = true;
20845
+ sentChatIdsRef.current.add(local.chatId);
20846
+ markLocalStatus(local.chatId, local.cid, LOCAL_SENT);
20847
+ };
20848
+ const markCommitted = () => {
20849
+ if (committed)
20850
+ return;
20851
+ committed = true;
20852
+ markSent();
20853
+ markDone(diag, "chat.send.commit", startedAt, { type: local?.t || "txt", previewRequired: !!requiredPreview });
20854
+ };
20855
+ return await new Promise((resolve, reject) => {
20856
+ const job = {
20857
+ previewKey: local.chatId,
20858
+ previewRequired: requiredPreview,
20859
+ resolve,
20860
+ reject,
20861
+ onSuccess: markSent,
20862
+ onError: (error) => {
20863
+ markError(diag, "chat.send.job", startedAt, error, { type: local?.t || "txt", previewRequired: !!requiredPreview });
20864
+ if (committed)
20865
+ markSent();
20866
+ else
20867
+ markLocalStatus(local.chatId, local.cid, LOCAL_FAILED);
20868
+ },
20869
+ run: async (context) => {
20870
+ const writeStartedAt = Date.now();
20871
+ markDiag(diag, "chat.send.write.start", { type: local?.t || "txt", previewRequired: !!requiredPreview, updatePreview: !!context?.updatePreview });
20872
+ try {
20873
+ const result = await runCurrentEpochWrite({
20874
+ chatId: mutationTarget,
20875
+ mutationGate,
20876
+ readOptions: () => refreshQueuedSendOptions(sendOptionsForPeer, peerChatPK, {
20877
+ ...sendOptionOverrides,
20878
+ chatId: local.chatId
20879
+ }),
20880
+ reconcileEpoch,
20881
+ write: (currentSendOptions, onCommitted) => run({
20882
+ ...context,
20883
+ local,
20884
+ onCommitted,
20885
+ sendOptions: currentSendOptions
20886
+ }),
20887
+ onCommitted: () => {
20888
+ markCommitted();
20889
+ context?.releaseTargets?.();
20890
+ },
20891
+ onEpochAdvance: ({ fromEpochVersion, toEpochVersion }) => {
20892
+ markDiag(diag, "chat.send.epoch.advance", { fromEpochVersion, toEpochVersion });
20893
+ }
20894
+ });
20895
+ markDone(diag, "chat.send.write", writeStartedAt, { type: local?.t || "txt", previewRequired: !!requiredPreview, updatePreview: !!context?.updatePreview });
20896
+ return result;
20897
+ } catch (error) {
20898
+ markError(diag, "chat.send.write", writeStartedAt, error, {
20899
+ type: local?.t || "txt",
20900
+ previewRequired: !!requiredPreview,
20901
+ updatePreview: !!context?.updatePreview
20902
+ });
20903
+ throw error;
20732
20904
  }
20733
- };
20734
- enqueueSendJob(peerChatPK, job, reject);
20735
- });
20736
- } finally {
20737
- releaseMutation?.();
20738
- }
20905
+ }
20906
+ };
20907
+ enqueueSendJob(peerChatPK, job, reject);
20908
+ });
20739
20909
  };
20740
20910
  const sendOptionsForQueuedWrite = (baseOptions, local, updatePreview, onCommitted) => {
20741
20911
  const chatId = local?.chatId;
@@ -24971,7 +25141,7 @@ async function unlinkBlockedChat(cloud, uid, identity, entry, options) {
24971
25141
  const entryId = ownChatEntryId(identity.chatPrivateKey, manifest.chatId);
24972
25142
  markDiag(options.diag, "chat.owner.unlink", { reason: "blocked" });
24973
25143
  await cloud.user.chats.unlink(uid, entryId);
24974
- options.onRemoved?.(manifest.chatId);
25144
+ options.onRemoved?.(manifest.chatId, manifest.epochVersion);
24975
25145
  options.onPingDelete?.(manifest.chatId);
24976
25146
  return { removed: true, blocked: true, chatId: manifest.chatId };
24977
25147
  }
@@ -24990,6 +25160,25 @@ function isRetryableInboxError(error) {
24990
25160
  ];
24991
25161
  return retryableCodes.some((value) => code === value || code.endsWith(`/${value}`)) || /\b(network|offline|timed? out|temporar(?:y|ily)|connection)\b/.test(message);
24992
25162
  }
25163
+ async function retireConfirmedDeletedChat(cloud, uid, identity, entry, options = {}) {
25164
+ const manifest = entry?.current?.manifest;
25165
+ if (!manifest?.chatId || !identity?.chatPrivateKey || !identity?.chatPK) {
25166
+ throw new Error("deleted chat owner entry required");
25167
+ }
25168
+ const entryId = ownChatEntryId(identity.chatPrivateKey, manifest.chatId);
25169
+ await cloud.user.chats.unlinkDeleted(uid, entryId, manifest.chatId);
25170
+ if (entry.deliveryRegistered) {
25171
+ const capability = deriveChatDeliveryCapability(entry.current.stateCapability, {
25172
+ chatId: manifest.chatId,
25173
+ recipientChatPK: identity.chatPK,
25174
+ generation: manifest.epochVersion
25175
+ });
25176
+ await cloud.delivery.revoke(capability).catch(() => false);
25177
+ }
25178
+ markDiag(options.diag, "chat.owner.unlink", { reason: "confirmed_deleted" });
25179
+ options.onPingDelete?.(manifest.chatId);
25180
+ return { removed: true, deleted: true, chatId: manifest.chatId };
25181
+ }
24993
25182
  function inboxErrorReason(error) {
24994
25183
  const message = cleanText(error?.message).toLowerCase();
24995
25184
  if (message === "ping sender identity mismatch")
@@ -25261,7 +25450,21 @@ async function processOpenedPing(cloud, uid, identity, ping, openedProfile, hint
25261
25450
  return "stale";
25262
25451
  }
25263
25452
  if (entry && payload.epochVersion > entry.current.manifest.epochVersion) {
25264
- const advanced = await advanceEntryToEpoch(cloud, uid, identity, entry, payload.epochVersion, options);
25453
+ let advanced;
25454
+ try {
25455
+ advanced = await advanceEntryToEpoch(cloud, uid, identity, entry, payload.epochVersion, options);
25456
+ } catch (error) {
25457
+ if (payload.kind !== "chat_deleted")
25458
+ throw error;
25459
+ try {
25460
+ await retireConfirmedDeletedChat(cloud, uid, identity, entry, options);
25461
+ return "deleted";
25462
+ } catch (confirmationError) {
25463
+ if (isRetryableInboxError(confirmationError))
25464
+ throw confirmationError;
25465
+ throw error;
25466
+ }
25467
+ }
25265
25468
  if (advanced?.removed) {
25266
25469
  options.onPingDelete?.(payload.chatId);
25267
25470
  return "deleted";
@@ -25317,6 +25520,11 @@ async function processOpenedPing(cloud, uid, identity, ping, openedProfile, hint
25317
25520
  options.onPingDelete?.(payload.chatId);
25318
25521
  return "deleted";
25319
25522
  }
25523
+ const latest = await advanceEntryToLatestEpoch(cloud, uid, identity, transitioned, options);
25524
+ if (latest?.removed) {
25525
+ options.onPingDelete?.(payload.chatId);
25526
+ return "deleted";
25527
+ }
25320
25528
  return "transition";
25321
25529
  }
25322
25530
  }
@@ -25425,8 +25633,15 @@ async function processPing(cloud, uid, identity, doc, options) {
25425
25633
  async function applyEpochTransitionAction(cloud, uid, identityValue, entry, action, options, confirmedValue = null) {
25426
25634
  const parent = entry.current.manifest;
25427
25635
  const sender = manifestMember(parent, action.s || action.certificate?.actor);
25428
- if (!sender)
25636
+ const reject = (stage) => {
25637
+ markDiag(options.diag, "chat.epoch.apply.reject", {
25638
+ stage,
25639
+ epochVersion: parent.epochVersion
25640
+ });
25429
25641
  return null;
25642
+ };
25643
+ if (!sender)
25644
+ return reject("sender");
25430
25645
  const engine = await options.mls?.open?.();
25431
25646
  if (!engine)
25432
25647
  throw new Error("mls engine required");
@@ -25434,25 +25649,25 @@ async function applyEpochTransitionAction(cloud, uid, identityValue, entry, acti
25434
25649
  const expectedCommitment = transitionCommitment(action.certificate);
25435
25650
  const confirmed = confirmedValue || await cloud.chat.state.transition(parent.chatId, parent.epochId, entry.current.stateCapability).catch(() => null);
25436
25651
  if (!confirmed || confirmed.parentEpochVersion !== parent.epochVersion || confirmed.nextEpochId !== action.certificate?.nextEpochId || confirmed.nextEpochVersion !== action.certificate?.nextEpochVersion || confirmed.transitionCommitment !== expectedCommitment || confirmed.transitionMessageId !== messageId || confirmed.mlsPackageId !== action.mls?.packageId || confirmed.mlsPackageDigest !== action.mls?.packageDigest)
25437
- return null;
25652
+ return reject("certificate");
25438
25653
  let leaveProposalId = "";
25439
25654
  if (action.membership?.kind === "leave") {
25440
25655
  const leavingChatPK = cleanText(action.membership?.actor);
25441
25656
  const proposalId = cleanText(action.membership?.proposalId);
25442
25657
  const targetSet = new Set(Array.isArray(action.membership?.targets) ? action.membership.targets.map(cleanText) : []);
25443
25658
  if (!leavingChatPK || !proposalId || targetSet.size !== 1 || !targetSet.has(leavingChatPK))
25444
- return null;
25659
+ return reject("leave-certificate");
25445
25660
  const proposalRecord = await cloud.chat.messages.read(parent.epochId, proposalId, {
25446
25661
  lane: messageLaneForEpoch(entry.current)
25447
25662
  }).catch(() => null);
25448
25663
  const proposal = proposalRecord ? await decryptMsg(proposalRecord, "", "", "", { chatId: parent.chatId, epochState: entry.current }).catch(() => null) : null;
25449
25664
  if (!isLeaveProposalMsg(proposal) || proposal.s !== leavingChatPK)
25450
- return null;
25665
+ return reject("leave-proposal");
25451
25666
  leaveProposalId = proposalId;
25452
25667
  }
25453
25668
  const packageRecord = await cloud.chat.mls.packages.read(parent.chatId, confirmed.mlsPackageId);
25454
25669
  if (!packageRecord || packageRecord.digest !== confirmed.mlsPackageDigest || packageRecord.head.transitionCommitment !== confirmed.transitionCommitment || !verifyChatMlsEpochPackageSignature(packageRecord, sender.chatSigningPK))
25455
- return null;
25670
+ return reject("mls-package");
25456
25671
  const identity = {
25457
25672
  ...identityValue,
25458
25673
  uid,
@@ -25463,8 +25678,11 @@ async function applyEpochTransitionAction(cloud, uid, identityValue, entry, acti
25463
25678
  let parentSnapshot = null;
25464
25679
  let nextSnapshot = null;
25465
25680
  let epochState = null;
25681
+ let openStage = "owner-snapshot";
25682
+ const openStartedAt = Date.now();
25466
25683
  try {
25467
25684
  parentSnapshot = await readOwnerChatMlsSnapshot(cloud, identity, entryId, entry.current);
25685
+ openStage = "mls-commit";
25468
25686
  const processed = engine.processCommit(parentSnapshot, packageRecord.commit);
25469
25687
  nextSnapshot = processed.snapshot;
25470
25688
  if (processed.removed) {
@@ -25483,9 +25701,10 @@ async function applyEpochTransitionAction(cloud, uid, identityValue, entry, acti
25483
25701
  if (leaveProposalId) {
25484
25702
  options.leaveProposalObservedAt?.delete(`${parent.chatId}:${leaveProposalId}`);
25485
25703
  }
25486
- options.onRemoved?.(parent.chatId);
25704
+ options.onRemoved?.(parent.chatId, confirmed.nextEpochVersion);
25487
25705
  return { removed: true, chatId: parent.chatId };
25488
25706
  }
25707
+ openStage = "mls-package-open";
25489
25708
  const opened = await openChatMlsEpochPackage(processed, packageRecord, {
25490
25709
  chatId: parent.chatId,
25491
25710
  senderSigningPK: sender.chatSigningPK,
@@ -25500,8 +25719,13 @@ async function applyEpochTransitionAction(cloud, uid, identityValue, entry, acti
25500
25719
  mlsPackageId: opened.packageId,
25501
25720
  mlsPackageDigest: opened.packageDigest
25502
25721
  };
25722
+ openStage = "settings";
25503
25723
  epochState.settings = await readEpochChatSettings(cloud, epochState);
25504
- } catch {
25724
+ } catch (error) {
25725
+ markError(options.diag, "chat.epoch.apply", openStartedAt, error, {
25726
+ stage: openStage,
25727
+ epochVersion: parent.epochVersion
25728
+ });
25505
25729
  cleanBytes(parentSnapshot, nextSnapshot, epochState?.epochSecret, epochState?.stateCapability);
25506
25730
  return null;
25507
25731
  }
@@ -25588,6 +25812,42 @@ async function advanceEntryToEpoch(cloud, uid, identity, entry, targetEpochVersi
25588
25812
  }).catch(() => null);
25589
25813
  const action = record ? await decryptMsg(record, "", "", "", { chatId: parent.chatId, epochState: current.current }).catch(() => null) : null;
25590
25814
  if (!isEpochTransitionMsg(action)) {
25815
+ markDiag(options.diag, "chat.epoch.advance.pending", {
25816
+ stage: record ? "transition-open" : "transition-record",
25817
+ epochVersion: parent.epochVersion
25818
+ });
25819
+ const error = new Error("chat epoch transition message is not available yet");
25820
+ error.code = "chat/epoch-pending";
25821
+ throw error;
25822
+ }
25823
+ const advanced = await applyEpochTransitionAction(cloud, uid, identity, current, action, options, confirmed);
25824
+ if (advanced?.removed)
25825
+ return advanced;
25826
+ if (!advanced?.current?.manifest || advanced.current.manifest.epochVersion <= parent.epochVersion) {
25827
+ const error = new Error("chat epoch transition could not be verified");
25828
+ error.code = "chat/epoch-pending";
25829
+ throw error;
25830
+ }
25831
+ current = advanced;
25832
+ }
25833
+ return current;
25834
+ }
25835
+ async function advanceEntryToLatestEpoch(cloud, uid, identity, entry, options = {}) {
25836
+ let current = entry;
25837
+ while (current?.current?.manifest) {
25838
+ const parent = current.current.manifest;
25839
+ const confirmed = await cloud.chat.state.transition(parent.chatId, parent.epochId, current.current.stateCapability);
25840
+ if (!confirmed?.transitionMessageId)
25841
+ return current;
25842
+ const record = await cloud.chat.messages.read(parent.epochId, confirmed.transitionMessageId, {
25843
+ lane: messageLaneForEpoch(current.current)
25844
+ }).catch(() => null);
25845
+ const action = record ? await decryptMsg(record, "", "", "", { chatId: parent.chatId, epochState: current.current }).catch(() => null) : null;
25846
+ if (!isEpochTransitionMsg(action)) {
25847
+ markDiag(options.diag, "chat.epoch.advance.pending", {
25848
+ stage: record ? "transition-open" : "transition-record",
25849
+ epochVersion: parent.epochVersion
25850
+ });
25591
25851
  const error = new Error("chat epoch transition message is not available yet");
25592
25852
  error.code = "chat/epoch-pending";
25593
25853
  throw error;
@@ -25724,6 +25984,17 @@ var init_inbox2 = __esm(() => {
25724
25984
  });
25725
25985
 
25726
25986
  // ../../core/chat/list.js
25987
+ function chatOwnerRevision(chat) {
25988
+ return Number.isSafeInteger(chat?.ownEntry?.ownerRevision) ? chat.ownEntry.ownerRevision : 0;
25989
+ }
25990
+ function serverChatCoversOverlay(chat, overlay) {
25991
+ const serverEpoch = Number.isSafeInteger(chat?.epochVersion) ? chat.epochVersion : 0;
25992
+ const overlayEpoch = Number.isSafeInteger(overlay?.epochVersion) ? overlay.epochVersion : 0;
25993
+ if (serverEpoch !== overlayEpoch) {
25994
+ return serverEpoch > overlayEpoch;
25995
+ }
25996
+ return chatOwnerRevision(chat) >= chatOwnerRevision(overlay);
25997
+ }
25727
25998
  function inboxRetryKey(doc) {
25728
25999
  const id = typeof doc?.id === "string" ? doc.id : "";
25729
26000
  const ts = timestampMs(doc?.ts, 0) || 0;
@@ -25796,13 +26067,17 @@ function listenToChats(cloud, uid, userChatPK, userPrivKey, onUpdate, onError, o
25796
26067
  nextAfterChat: null,
25797
26068
  hasMore: false
25798
26069
  };
25799
- const optimisticChats = new Map;
25800
- const mergedChats = (chats) => {
26070
+ const optimisticChats = options.optimisticChats instanceof Map ? options.optimisticChats : new Map;
26071
+ const mergedChats = (chats, mergeOptions = {}) => {
25801
26072
  const nextById = new Map;
25802
26073
  for (const chat of chats || []) {
25803
26074
  const optimistic = optimisticChats.get(chat?.id);
25804
- if (chat?.id)
26075
+ if (chat?.id) {
25805
26076
  nextById.set(chat.id, mergeChatActivity(chat, optimistic, userChatPK));
26077
+ if (mergeOptions.settle === true && optimistic && serverChatCoversOverlay(chat, optimistic)) {
26078
+ optimisticChats.delete(chat.id);
26079
+ }
26080
+ }
25806
26081
  }
25807
26082
  for (const [id, chat] of optimisticChats.entries()) {
25808
26083
  if (!nextById.has(id)) {
@@ -25867,7 +26142,7 @@ function listenToChats(cloud, uid, userChatPK, userPrivKey, onUpdate, onError, o
25867
26142
  if (!closed && runId === run) {
25868
26143
  processedFirst = true;
25869
26144
  const chats = page.chats;
25870
- latestChats = mergedChats(chats);
26145
+ latestChats = mergedChats(chats, { settle: true });
25871
26146
  latestMeta = {
25872
26147
  nextAfterChat: snapshot.nextAfterChat,
25873
26148
  hasMore: snapshot.hasMore
@@ -26249,6 +26524,7 @@ function createChatList({
26249
26524
  localByChatRef,
26250
26525
  pendingDeleteIdsRef,
26251
26526
  deletedChatIdsRef,
26527
+ removedChatVersionsRef,
26252
26528
  keepSelectedDeletedChatIdsRef,
26253
26529
  readCacheRef,
26254
26530
  lastServerChatsRef,
@@ -26266,6 +26542,7 @@ function createChatList({
26266
26542
  onTransition,
26267
26543
  onRemoved,
26268
26544
  wasRemoved,
26545
+ restoreRemovedChat,
26269
26546
  resetExternalChatState,
26270
26547
  mutationGate,
26271
26548
  onMlsKeyPackageConsumed
@@ -26294,6 +26571,8 @@ function createChatList({
26294
26571
  const cachedHistoryEndRef = { current: false };
26295
26572
  const cachedHistoryHeadRef = { current: [] };
26296
26573
  const previewTimerRef = { current: null };
26574
+ const epochReconciliations = new Map;
26575
+ const inboxOverlayChats = new Map;
26297
26576
  let listening = false;
26298
26577
  let unsubscribeChats = null;
26299
26578
  let snapshot = null;
@@ -26302,7 +26581,7 @@ function createChatList({
26302
26581
  let blockedUidSet = new Set(blockedUids);
26303
26582
  let schedulePreviewExpiry = () => {};
26304
26583
  let fillVisibleChatPage = () => {};
26305
- const isHiddenChatId = (chatId) => !!chatId && (pendingDeleteIdsRef.current.has(chatId) || deletedChatIdsRef.current.has(chatId));
26584
+ const isHiddenChatId = (chatId) => !!chatId && (pendingDeleteIdsRef.current.has(chatId) || deletedChatIdsRef.current.has(chatId) || removedChatVersionsRef.current.has(chatId));
26306
26585
  const filterHiddenChats = (nextChats) => (nextChats || []).filter((chatItem) => chatItem?.id && !isHiddenChatId(chatItem.id));
26307
26586
  const nextValue = (current, next) => typeof next === "function" ? next(current) : next;
26308
26587
  const publish = () => {
@@ -26517,7 +26796,13 @@ function createChatList({
26517
26796
  return shownChats;
26518
26797
  };
26519
26798
  const applyInboxChat = (chat) => {
26520
- if (!chat?.id || isHiddenChatId(chat.id)) {
26799
+ if (!chat?.id) {
26800
+ return;
26801
+ }
26802
+ if (removedChatVersionsRef.current.has(chat.id)) {
26803
+ restoreRemovedChat?.(chat);
26804
+ }
26805
+ if (isHiddenChatId(chat.id)) {
26521
26806
  return;
26522
26807
  }
26523
26808
  const updateStartedAt = Date.now();
@@ -26543,13 +26828,113 @@ function createChatList({
26543
26828
  const applyInboxDelete = (chatId) => {
26544
26829
  if (!chatId)
26545
26830
  return;
26831
+ inboxOverlayChats.delete(chatId);
26546
26832
  pendingDeleteIdsRef.current.delete(chatId);
26547
26833
  commitServerChats(lastServerChatsRef.current.filter((chat) => chat?.id !== chatId), {
26548
26834
  warm: false
26549
26835
  });
26550
26836
  setSelectedChat((current) => current === chatId ? null : current);
26551
26837
  };
26838
+ const reconcileChatEpoch = (chatId) => {
26839
+ if (!chatId || !uid || !chatPK || !chatPrivateKey || !chatSigningPK || !chatSigningSecret || !notificationPK) {
26840
+ return Promise.resolve({ advanced: false, chat: null, removed: false });
26841
+ }
26842
+ const active = epochReconciliations.get(chatId);
26843
+ if (active)
26844
+ return active;
26845
+ let task;
26846
+ task = (async () => {
26847
+ const releaseMutation = await mutationGate?.acquireExclusive(chatId) || (() => {});
26848
+ try {
26849
+ const initial = getChatEntry(chatId);
26850
+ if (!initial?.ownEntry)
26851
+ return { advanced: false, chat: initial || null, removed: false };
26852
+ const initialVersion = initial.epochVersion;
26853
+ const identity = {
26854
+ uid,
26855
+ chatPK,
26856
+ chatPrivateKey,
26857
+ chatSigningPK,
26858
+ chatSigningSecret,
26859
+ notificationPK,
26860
+ notificationPrivateKey
26861
+ };
26862
+ const advanceOptions = {
26863
+ blockedUids: blockedUidSet,
26864
+ mls,
26865
+ diag,
26866
+ onMlsKeyPackageConsumed,
26867
+ onRemoved: (removedChatId, epochVersion) => {
26868
+ onRemoved?.(removedChatId, epochVersion);
26869
+ applyInboxDelete(removedChatId);
26870
+ },
26871
+ wasRemoved,
26872
+ onTransition: (chat, message) => {
26873
+ onTransition?.(chat, message);
26874
+ applyInboxChat(chat);
26875
+ }
26876
+ };
26877
+ let working = initial;
26878
+ let result;
26879
+ while (true) {
26880
+ try {
26881
+ result = await advanceEntryToLatestEpoch(cloud, uid, identity, working.ownEntry, advanceOptions);
26882
+ break;
26883
+ } catch (error) {
26884
+ const code = String(error?.code || "").trim().toLowerCase();
26885
+ if (code === "permission-denied" || code.endsWith("/permission-denied")) {
26886
+ result = await retireConfirmedDeletedChat(cloud, uid, identity, working.ownEntry, {
26887
+ diag,
26888
+ onPingDelete: applyInboxDelete
26889
+ });
26890
+ break;
26891
+ }
26892
+ if (code !== "chat/epoch-pending")
26893
+ throw error;
26894
+ const canonical = await getChat(cloud, uid, chatId, chatPK, chatPrivateKey);
26895
+ if (!canonical?.ownEntry || canonical.epochVersion <= working.epochVersion)
26896
+ throw error;
26897
+ markDiag(diag, "chat.epoch.reconcile.rebase", {
26898
+ fromEpochVersion: working.epochVersion,
26899
+ toEpochVersion: canonical.epochVersion
26900
+ });
26901
+ applyInboxChat(canonical);
26902
+ working = canonical;
26903
+ }
26904
+ }
26905
+ if (result?.removed) {
26906
+ markDiag(diag, "chat.epoch.reconcile.done", {
26907
+ fromEpochVersion: initialVersion,
26908
+ removed: true
26909
+ });
26910
+ return { advanced: true, chat: null, removed: true };
26911
+ }
26912
+ const latest = getChatEntry(chatId);
26913
+ const advanced = Number(latest?.epochVersion || 0) > Number(initialVersion || 0);
26914
+ if (advanced) {
26915
+ markDiag(diag, "chat.epoch.reconcile.done", {
26916
+ fromEpochVersion: initialVersion,
26917
+ toEpochVersion: latest.epochVersion,
26918
+ removed: false
26919
+ });
26920
+ }
26921
+ return {
26922
+ advanced,
26923
+ chat: latest || null,
26924
+ removed: false
26925
+ };
26926
+ } finally {
26927
+ releaseMutation();
26928
+ }
26929
+ })().finally(() => {
26930
+ if (epochReconciliations.get(chatId) === task)
26931
+ epochReconciliations.delete(chatId);
26932
+ });
26933
+ epochReconciliations.set(chatId, task);
26934
+ return task;
26935
+ };
26552
26936
  const resetChatList = (ready = false) => {
26937
+ inboxOverlayChats.clear();
26553
26938
  chatSourceReady = ready;
26554
26939
  syncChatDataReady();
26555
26940
  setIsChatListLive(false);
@@ -26673,7 +27058,15 @@ function createChatList({
26673
27058
  };
26674
27059
  const unsubscribe = listenToChats(cloud, uid, chatPK, chatPrivateKey, (nextChats, _nextPeers, meta = {}) => {
26675
27060
  const updateStartedAt = Date.now();
26676
- const rawNextChats = filterHiddenChats(Array.isArray(nextChats) ? nextChats : []);
27061
+ const incomingChats = Array.isArray(nextChats) ? nextChats : [];
27062
+ if (meta?.source === "inbox") {
27063
+ for (const chat of incomingChats) {
27064
+ if (removedChatVersionsRef.current.has(chat?.id)) {
27065
+ restoreRemovedChat?.(chat);
27066
+ }
27067
+ }
27068
+ }
27069
+ const rawNextChats = filterHiddenChats(incomingChats);
26677
27070
  hydrateReadCache(rawNextChats, readCacheRef.current);
26678
27071
  const readAppliedChats = applyReadCache(rawNextChats, chatPK, readCacheRef.current);
26679
27072
  const trimmedChats = trimExpiredChatPreviews(readAppliedChats, { skipChatId: selectedChatIdRef.current });
@@ -26778,6 +27171,7 @@ function createChatList({
26778
27171
  blockedUids: blockedUidSet,
26779
27172
  localCache,
26780
27173
  diag,
27174
+ optimisticChats: inboxOverlayChats,
26781
27175
  getCurrentChat: (chatId) => lastServerChatsRef.current.find((chatItem) => chatItem?.id === chatId) || null,
26782
27176
  mutationGate,
26783
27177
  onMlsKeyPackageConsumed
@@ -26956,12 +27350,16 @@ function createChatList({
26956
27350
  };
26957
27351
  const hasChat = (chatId) => !!chatId && (serverChatIdsSet.has(chatId) || chatLoadsRef.current.has(chatId)) && !isHiddenChatId(chatId);
26958
27352
  const setSources = (nextSources) => {
27353
+ const accountChanged = cloud !== nextSources.cloud || uid !== nextSources.uid || chatPK !== nextSources.chatPK || chatPrivateKey !== nextSources.chatPrivateKey;
26959
27354
  const nextBlocked = sortedUniqueValues(Array.isArray(nextSources.blocked) ? nextSources.blocked : []);
26960
27355
  const nextBlockedUidsKey = nextBlocked.join("|");
26961
27356
  const nextBlockedReady = nextSources.blockedReady === true;
26962
27357
  const blockedFilterChanged = blockedUidsKey !== nextBlockedUidsKey || blockedReady !== nextBlockedReady;
26963
27358
  const listenerChanged = cloud !== nextSources.cloud || uid !== nextSources.uid || chatPK !== nextSources.chatPK || chatSigningPK !== nextSources.chatSigningPK || chatSigningSecret !== nextSources.chatSigningSecret || chatPrivateKey !== nextSources.chatPrivateKey || notificationPK !== nextSources.notificationPK || notificationPrivateKey !== nextSources.notificationPrivateKey || mls !== nextSources.mls || chatBanned !== nextSources.chatBanned || localCache !== nextSources.localCache || isActive !== nextSources.isActive;
26964
27359
  const selectedChatChanged = selectedChatId !== nextSources.selectedChatId;
27360
+ if (accountChanged) {
27361
+ inboxOverlayChats.clear();
27362
+ }
26965
27363
  ({
26966
27364
  cloud,
26967
27365
  uid,
@@ -26983,6 +27381,7 @@ function createChatList({
26983
27381
  localByChatRef,
26984
27382
  pendingDeleteIdsRef,
26985
27383
  deletedChatIdsRef,
27384
+ removedChatVersionsRef,
26986
27385
  keepSelectedDeletedChatIdsRef,
26987
27386
  readCacheRef,
26988
27387
  lastServerChatsRef,
@@ -27000,6 +27399,7 @@ function createChatList({
27000
27399
  onTransition,
27001
27400
  onRemoved,
27002
27401
  wasRemoved,
27402
+ restoreRemovedChat,
27003
27403
  resetExternalChatState,
27004
27404
  mutationGate,
27005
27405
  onMlsKeyPackageConsumed
@@ -27043,6 +27443,8 @@ function createChatList({
27043
27443
  previewTimerRef.current = null;
27044
27444
  }
27045
27445
  flushChatCacheWrite();
27446
+ epochReconciliations.clear();
27447
+ inboxOverlayChats.clear();
27046
27448
  subscribers.clear();
27047
27449
  };
27048
27450
  const subscribe = (subscriber) => {
@@ -27068,6 +27470,7 @@ function createChatList({
27068
27470
  getChatPreviewKey: getChatPreviewKey2,
27069
27471
  getChatRetention,
27070
27472
  getChatEntry,
27473
+ reconcileChatEpoch,
27071
27474
  sendOptionsForPeer,
27072
27475
  hasChat
27073
27476
  };
@@ -27078,6 +27481,7 @@ var init_listsession = __esm(() => {
27078
27481
  init_localdata();
27079
27482
  init_chats();
27080
27483
  init_list();
27484
+ init_inbox2();
27081
27485
  init_ttl();
27082
27486
  init_ids();
27083
27487
  init_time();
@@ -27282,7 +27686,6 @@ function createChatSession({
27282
27686
  const closeMessageBatchRef = { current: null };
27283
27687
  const localMessageDiagRef = { current: "" };
27284
27688
  const epochHistoryRef = { current: new Map };
27285
- const epochRecoveryRef = { current: new Map };
27286
27689
  const settingsUpdateStatesRef = { current: new Map };
27287
27690
  const membershipUpdateStatesRef = { current: new Map };
27288
27691
  const directChatOpensRef = { current: new Map };
@@ -27321,6 +27724,7 @@ function createChatSession({
27321
27724
  kickChatMember,
27322
27725
  leaveChat,
27323
27726
  updateChatSettings: updateChatSettings2,
27727
+ updateChatAvatar,
27324
27728
  dropChat,
27325
27729
  dropUnavailableChat,
27326
27730
  deleteChat,
@@ -27779,51 +28183,15 @@ function createChatSession({
27779
28183
  return true;
27780
28184
  };
27781
28185
  const handleBatchMessages = (chatId, messages) => {
27782
- const transition = [...messages || []].reverse().find(isEpochTransitionMsg);
27783
- const nextEpochId = transition?.certificate?.nextEpochId;
27784
- const chat = getChatEntry(chatId);
27785
- if (!nextEpochId || !chat?.ownEntry || chat.epochId === nextEpochId || epochRecoveryRef.current.has(nextEpochId))
28186
+ if (![...messages || []].reverse().some(isEpochTransitionMsg))
27786
28187
  return;
27787
- const recover = (attempt = 0) => {
27788
- const timer = attempt > 0 ? setTimeout(run, [0, 250, 1000, 3000][attempt] || 3000) : null;
27789
- epochRecoveryRef.current.set(nextEpochId, timer || true);
27790
- if (!timer)
27791
- run();
27792
- async function run() {
27793
- let releaseMutation = () => {};
27794
- try {
27795
- releaseMutation = await mutationGate.acquireExclusive(chatId);
27796
- const current = getChatEntry(chatId);
27797
- if (!current?.ownEntry || current.epochId === nextEpochId)
27798
- return;
27799
- const advanced = await processEpochTransitionMessages(cloud, uid, {
27800
- chatPK,
27801
- chatPrivateKey,
27802
- notificationPK
27803
- }, current.ownEntry, messages, {
27804
- blockedUids: new Set(blocked),
27805
- notificationPrivateKey,
27806
- diag,
27807
- onRemoved: (removedChatId) => deleteActions.dropRemovedChat(removedChatId),
27808
- wasRemoved: (removedChatId) => deleteActions.deletedChatIdsRef.current.has(removedChatId),
27809
- onTransition: stageCommittedMembershipMessage
27810
- });
27811
- if (advanced)
27812
- epochHistoryRef.current.delete(chatId);
27813
- else if (attempt < 3)
27814
- return recover(attempt + 1);
27815
- } catch {
27816
- if (attempt < 3)
27817
- return recover(attempt + 1);
27818
- } finally {
27819
- releaseMutation();
27820
- if (epochRecoveryRef.current.get(nextEpochId) === (timer || true)) {
27821
- epochRecoveryRef.current.delete(nextEpochId);
27822
- }
27823
- }
27824
- }
27825
- };
27826
- recover();
28188
+ const startedAt = Date.now();
28189
+ chatListOwner.reconcileChatEpoch(chatId).then((result) => {
28190
+ if (result.advanced)
28191
+ epochHistoryRef.current.delete(chatId);
28192
+ }).catch((error) => {
28193
+ markError(diag, "chat.epoch.reconcile", startedAt, error);
28194
+ });
27827
28195
  };
27828
28196
  const setChats = (...args) => chatListOwner.setChats(...args);
27829
28197
  const setLastChat = (...args) => chatListOwner.setLastChat(...args);
@@ -27841,11 +28209,6 @@ function createChatSession({
27841
28209
  liveActions?.close?.();
27842
28210
  liveActivityByChat = new Map;
27843
28211
  messageBatchOwner.clear();
27844
- for (const pending of epochRecoveryRef.current.values()) {
27845
- if (typeof pending === "number")
27846
- clearTimeout(pending);
27847
- }
27848
- epochRecoveryRef.current.clear();
27849
28212
  epochHistoryRef.current.clear();
27850
28213
  membershipUpdateStatesRef.current.clear();
27851
28214
  directChatOpensRef.current.clear();
@@ -28299,10 +28662,10 @@ function createChatSession({
28299
28662
  await discardOwnerChatMlsSnapshot(cloud, transitionIdentity(), chat.entryId, chat.epochState);
28300
28663
  await cloud.user.chats.unlink(uid, chat.entryId);
28301
28664
  if (retirementStarted) {
28302
- deleteActions.commitRetirement(chatId);
28665
+ deleteActions.commitRemovedRetirement(chatId, chat.epochVersion + 1);
28303
28666
  retirementStarted = false;
28304
28667
  } else {
28305
- deleteActions.dropChat(chatId);
28668
+ deleteActions.dropRemovedChat(chatId, chat.epochVersion + 1);
28306
28669
  }
28307
28670
  return { left: true, message: proposal.message };
28308
28671
  } catch (error) {
@@ -28364,35 +28727,55 @@ function createChatSession({
28364
28727
  clearLocalMembershipMessage(chatId, cid);
28365
28728
  };
28366
28729
  state.promise = (async () => {
28367
- const releaseMutation = await mutationGate.acquireExclusive(chatId);
28368
28730
  try {
28369
- const chat = requireTransitionChat(chatId);
28370
- const members = prepareMembers ? await prepareMembers(chat) : resolveMembers(chat);
28371
- if (!members) {
28372
- restoreAuthoritativeChat();
28373
- return { entry: chat.ownEntry, unchanged: true };
28374
- }
28375
- if (!members.length) {
28376
- restoreAuthoritativeChat();
28377
- await deleteActions.deleteChat(chat);
28378
- return { deleted: true };
28379
- }
28380
- const result = await transitionChatEpoch(cloud, transitionIdentity(), chat.ownEntry, { members, cid, mls });
28381
- if (result.entry) {
28382
- const projected = projectOwnChatEntry(result.entry, chat.entryId, chatPK, chat.ts);
28383
- chatListOwner.commitServerChats(sortedChats([projected], lastServerChatsRef.current));
28384
- } else {
28385
- chatListOwner.commitServerChats(lastServerChatsRef.current.filter((chatItem) => chatItem?.id !== chatId), { warm: false });
28386
- setSelectedChat((current) => current === chatId ? null : current);
28387
- }
28388
- if (optimisticRoster && result.message) {
28389
- stageCommittedMembershipMessage(chat, result.message);
28390
- } else if (optimisticRoster) {
28391
- clearLocalMembershipMessage(chatId, cid);
28731
+ while (true) {
28732
+ const releaseMutation = await mutationGate.acquireExclusive(chatId);
28733
+ let attemptedEpochVersion = 0;
28734
+ try {
28735
+ const chat = requireTransitionChat(chatId);
28736
+ attemptedEpochVersion = chat.epochVersion;
28737
+ const members = prepareMembers ? await prepareMembers(chat) : resolveMembers(chat);
28738
+ if (!members) {
28739
+ restoreAuthoritativeChat();
28740
+ return { entry: chat.ownEntry, unchanged: true };
28741
+ }
28742
+ if (!members.length) {
28743
+ restoreAuthoritativeChat();
28744
+ await deleteActions.deleteChat(chat);
28745
+ return { deleted: true };
28746
+ }
28747
+ const result = await transitionChatEpoch(cloud, transitionIdentity(), chat.ownEntry, { members, cid, mls });
28748
+ if (result.entry) {
28749
+ const projected = projectOwnChatEntry(result.entry, chat.entryId, chatPK, chat.ts);
28750
+ chatListOwner.commitServerChats(sortedChats([projected], lastServerChatsRef.current));
28751
+ } else {
28752
+ chatListOwner.commitServerChats(lastServerChatsRef.current.filter((chatItem) => chatItem?.id !== chatId), { warm: false });
28753
+ setSelectedChat((current) => current === chatId ? null : current);
28754
+ }
28755
+ if (optimisticRoster && result.message) {
28756
+ stageCommittedMembershipMessage(chat, result.message);
28757
+ } else if (optimisticRoster) {
28758
+ clearLocalMembershipMessage(chatId, cid);
28759
+ }
28760
+ if (optimisticRoster)
28761
+ setPendingChatMembers(chatId, null);
28762
+ return result;
28763
+ } catch (error) {
28764
+ releaseMutation();
28765
+ if (!attemptedEpochVersion || !isChatEpochConflict(error))
28766
+ throw error;
28767
+ await chatListOwner.reconcileChatEpoch(chatId);
28768
+ const currentEpochVersion = getChatEntry(chatId)?.epochVersion || 0;
28769
+ if (currentEpochVersion <= attemptedEpochVersion)
28770
+ throw error;
28771
+ markDiag(diag, "chat.membership.epoch.advance", {
28772
+ fromEpochVersion: attemptedEpochVersion,
28773
+ toEpochVersion: currentEpochVersion
28774
+ });
28775
+ } finally {
28776
+ releaseMutation();
28777
+ }
28392
28778
  }
28393
- if (optimisticRoster)
28394
- setPendingChatMembers(chatId, null);
28395
- return result;
28396
28779
  } catch (error) {
28397
28780
  restoreAuthoritativeChat();
28398
28781
  throw error;
@@ -28400,7 +28783,6 @@ function createChatSession({
28400
28783
  if (membershipUpdateStatesRef.current.get(chatId) === state) {
28401
28784
  membershipUpdateStatesRef.current.delete(chatId);
28402
28785
  }
28403
- releaseMutation();
28404
28786
  }
28405
28787
  })();
28406
28788
  membershipUpdateStatesRef.current.set(chatId, state);
@@ -28523,6 +28905,26 @@ function createChatSession({
28523
28905
  }
28524
28906
  return state.promise;
28525
28907
  };
28908
+ const updateChatAvatar = async (chatId, data) => {
28909
+ assertChatAvatarBytes(data);
28910
+ const chat = requireTransitionChat(chatId);
28911
+ if (chat.lineage !== "group")
28912
+ throw new Error("group chat required");
28913
+ const epochId = chat?.epochState?.manifest?.epochId;
28914
+ if (!epochId)
28915
+ throw new Error("current chat epoch unavailable");
28916
+ const uploadChatMedia = cloud?.chat?.media?.upload;
28917
+ if (typeof uploadChatMedia !== "function")
28918
+ throw new Error("chat media upload unavailable");
28919
+ const cid = makeCid();
28920
+ const file = typeof media?.uploadChatAvatar === "function" ? await media.uploadChatAvatar({ cid, data, epochId, uploadChatMedia }) : await putChatFile(epochId, cid, data, {
28921
+ uploadChatMedia,
28922
+ cacheControl: "private, max-age=31536000, immutable, no-transform"
28923
+ });
28924
+ const avatarRef = makeChatAvatarRef(file);
28925
+ await updateChatSettings2(chatId, { avatarRef });
28926
+ return avatarRef;
28927
+ };
28526
28928
  const adoptCommittedSettingsMessage = ({ chatId, message }) => {
28527
28929
  if (!chatId || !message?.cid)
28528
28930
  return;
@@ -28645,7 +29047,13 @@ function createChatSession({
28645
29047
  const ackDeletedChat = (...args) => deleteActions.ackDeletedChat(...args);
28646
29048
  const markChatReadState = (...args) => seenActions.markChatReadState(...args);
28647
29049
  const markChatRead = (...args) => seenActions.markChatRead(...args);
28648
- const enterChatActivity = (...args) => liveActions.enterChatActivity(...args);
29050
+ const enterChatActivity = (chatId) => {
29051
+ const reconcileStartedAt = Date.now();
29052
+ chatListOwner.reconcileChatEpoch(chatId).catch((error) => {
29053
+ markError(diag, "chat.epoch.reconcile", reconcileStartedAt, error);
29054
+ });
29055
+ return liveActions.enterChatActivity(chatId);
29056
+ };
28649
29057
  const leaveChatActivity = (chatId) => {
28650
29058
  const flush = seenActions.flushChatRead(chatId);
28651
29059
  liveActions.leaveChatActivity(chatId);
@@ -28892,6 +29300,7 @@ function createChatSession({
28892
29300
  listActionsRef: deleteListActionsRef,
28893
29301
  closeMessageBatchRef,
28894
29302
  mutationGate,
29303
+ reconcileEpoch: (chatId) => chatListOwner?.reconcileChatEpoch(chatId),
28895
29304
  diag
28896
29305
  });
28897
29306
  const createActionOwners = () => {
@@ -28971,6 +29380,7 @@ function createChatSession({
28971
29380
  adoptLocalMessageMedia,
28972
29381
  readMessageFile,
28973
29382
  mutationGate,
29383
+ reconcileEpoch: (chatId) => chatListOwner.reconcileChatEpoch(chatId),
28974
29384
  diag
28975
29385
  });
28976
29386
  reactionActions = createChatReaction({
@@ -29051,6 +29461,7 @@ function createChatSession({
29051
29461
  localByChatRef,
29052
29462
  pendingDeleteIdsRef: deleteActions.pendingDeleteIdsRef,
29053
29463
  deletedChatIdsRef: deleteActions.deletedChatIdsRef,
29464
+ removedChatVersionsRef: deleteActions.removedChatVersionsRef,
29054
29465
  keepSelectedDeletedChatIdsRef: deleteActions.keepSelectedDeletedChatIdsRef,
29055
29466
  readCacheRef,
29056
29467
  lastServerChatsRef,
@@ -29066,8 +29477,9 @@ function createChatSession({
29066
29477
  reconcileMessageBatches: (...args) => messageBatchOwner.reconcileChats(...args),
29067
29478
  warmChats: (...args) => messageBatchOwner.warm(...args),
29068
29479
  onTransition: stageCommittedMembershipMessage,
29069
- onRemoved: (removedChatId) => deleteActions.dropRemovedChat(removedChatId),
29070
- wasRemoved: (removedChatId) => deleteActions.deletedChatIdsRef.current.has(removedChatId),
29480
+ onRemoved: (removedChatId, epochVersion) => deleteActions.dropRemovedChat(removedChatId, epochVersion),
29481
+ wasRemoved: (removedChatId) => deleteActions.removedChatVersionsRef.current.has(removedChatId),
29482
+ restoreRemovedChat: (chat) => deleteActions.restoreRemovedChat(chat),
29071
29483
  resetExternalChatState,
29072
29484
  mutationGate,
29073
29485
  onMlsKeyPackageConsumed: markMlsKeyPackageConsumed
@@ -29208,6 +29620,8 @@ function createChatSession({
29208
29620
  var DEFAULT_READ_WRITE_INTERVAL_MS, DIAG_KEY_LEN = 12, EMPTY_LOCAL_MESSAGES;
29209
29621
  var init_session3 = __esm(() => {
29210
29622
  init_config();
29623
+ init_files();
29624
+ init_avatar2();
29211
29625
  init_ids();
29212
29626
  init_direct();
29213
29627
  init_create();
@@ -38087,7 +38501,8 @@ var init_links = __esm(() => {
38087
38501
  ])
38088
38502
  ]);
38089
38503
  storageCorsOrigins = Object.freeze([
38090
- origins.veyl
38504
+ origins.veyl,
38505
+ origins.veylDevWeb
38091
38506
  ]);
38092
38507
  devStorageCorsOrigins = Object.freeze([
38093
38508
  origins.veylDev,
@@ -38103,12 +38518,19 @@ var init_links = __esm(() => {
38103
38518
  });
38104
38519
 
38105
38520
  // ../../product/passkeylabel.js
38521
+ function twoWordLabelAt(index) {
38522
+ if (!Number.isSafeInteger(index) || index < 0) {
38523
+ throw new Error("two-word label index must be a non-negative integer");
38524
+ }
38525
+ const normalized = index % TWO_WORD_LABEL_COUNT;
38526
+ const adjective = adjectives[normalized % adjectives.length];
38527
+ const noun = nouns[Math.floor(normalized / adjectives.length)];
38528
+ return `${adjective} ${noun}`;
38529
+ }
38106
38530
  function randomPasskeyLabel() {
38107
- const adj = adjectives[Math.floor(Math.random() * adjectives.length)];
38108
- const noun = nouns[Math.floor(Math.random() * nouns.length)];
38109
- return `${adj} ${noun}`;
38531
+ return twoWordLabelAt(Math.floor(Math.random() * TWO_WORD_LABEL_COUNT));
38110
38532
  }
38111
- var adjectives, nouns;
38533
+ var adjectives, nouns, TWO_WORD_LABEL_COUNT;
38112
38534
  var init_passkeylabel = __esm(() => {
38113
38535
  adjectives = [
38114
38536
  "Ancient",
@@ -38175,7 +38597,6 @@ var init_passkeylabel = __esm(() => {
38175
38597
  "Grim",
38176
38598
  "Holy",
38177
38599
  "Infernal",
38178
- "Ivory",
38179
38600
  "Jade",
38180
38601
  "Light",
38181
38602
  "Midnight",
@@ -38319,6 +38740,7 @@ var init_passkeylabel = __esm(() => {
38319
38740
  "Wizard",
38320
38741
  "Wyvern"
38321
38742
  ];
38743
+ TWO_WORD_LABEL_COUNT = adjectives.length * nouns.length;
38322
38744
  });
38323
38745
 
38324
38746
  // src/auth.js
@@ -38684,6 +39106,7 @@ function simplifyAccountChat(chat, profiles = null) {
38684
39106
  members,
38685
39107
  title: chat.settings?.title || null,
38686
39108
  avatarRef: chat.settings?.avatarRef || null,
39109
+ retention: chat.settings?.retention || null,
38687
39110
  peerChatPK: chat.peerChatPK,
38688
39111
  peerUid: chat.peerUid || directPeer?.uid || null,
38689
39112
  username: directPeer?.username || null,
@@ -44191,6 +44614,7 @@ function deriveMessageRouteProjection({
44191
44614
  routeActive: routeRetained
44192
44615
  }));
44193
44616
  const { rawMessages, messages, visibleMessages } = routeMessages;
44617
+ const memberStatesByEpoch = activeState.latestServerPage?.memberStatesByEpoch || EMPTY_MEMBER_STATES_BY_EPOCH;
44194
44618
  return {
44195
44619
  activeExists: activeState.exists,
44196
44620
  activeHasOlder: activeState.hasOlder,
@@ -44199,7 +44623,7 @@ function deriveMessageRouteProjection({
44199
44623
  activeLatestMessages: activeState.latestMessages,
44200
44624
  activeLatestServerPage: activeState.latestServerPage,
44201
44625
  activeReady: activeState.ready,
44202
- memberStatesByEpoch: activeState.latestServerPage?.memberStatesByEpoch || new Map,
44626
+ memberStatesByEpoch,
44203
44627
  leaveState: {
44204
44628
  chatId,
44205
44629
  exists: activeState.exists,
@@ -44210,7 +44634,7 @@ function deriveMessageRouteProjection({
44210
44634
  selfChatPublicKey,
44211
44635
  peerChatPublicKey,
44212
44636
  memberChatPKs,
44213
- memberStatesByEpoch: activeState.latestServerPage?.memberStatesByEpoch || new Map,
44637
+ memberStatesByEpoch,
44214
44638
  conversationKeys,
44215
44639
  routeMemory,
44216
44640
  keepKeys: getRouteKeepKeys(refs.visibleMessageKeys.current, refs.heldMessages.current),
@@ -44316,7 +44740,7 @@ function recordRouteProjectionDiagnostics({
44316
44740
  markDiag(diag, "chat.messages.route.visible", visibleData);
44317
44741
  }
44318
44742
  }
44319
- var PASSIVE_OLDER_OVERFLOW_PAGES = 3, ROUTE_VISIBLE_DETAIL_SCAN_LIMIT = 160, DIAG_KEY_LEN2 = 12;
44743
+ var PASSIVE_OLDER_OVERFLOW_PAGES = 3, ROUTE_VISIBLE_DETAIL_SCAN_LIMIT = 160, DIAG_KEY_LEN2 = 12, EMPTY_MEMBER_STATES_BY_EPOCH;
44320
44744
  var init_routeprojection = __esm(() => {
44321
44745
  init_messagekeys();
44322
44746
  init_state();
@@ -44324,6 +44748,7 @@ var init_routeprojection = __esm(() => {
44324
44748
  init_history();
44325
44749
  init_payloadlog();
44326
44750
  init_window();
44751
+ EMPTY_MEMBER_STATES_BY_EPOCH = new Map;
44327
44752
  });
44328
44753
 
44329
44754
  // ../../core/chat/messages/expirycleanup.js
@@ -46706,6 +47131,9 @@ function positiveLimit2(value, fallback = 30) {
46706
47131
  const limit = Math.floor(Number(value) || fallback);
46707
47132
  return Math.max(1, Math.min(limit, 500));
46708
47133
  }
47134
+ function messageIsPermanent(message) {
47135
+ return message?.permanent === true || Object.prototype.hasOwnProperty.call(message || {}, "ttl") && message.ttl == null;
47136
+ }
46709
47137
  function waitFor2(owner, predicate, timeoutMs = DEFAULT_TIMEOUT_MS4) {
46710
47138
  const current = owner.getSnapshot();
46711
47139
  if (predicate(current))
@@ -46873,7 +47301,7 @@ function createRuntimeChatActions({
46873
47301
  const profiles2 = await resolveTargetProfiles(peers, session, runtime);
46874
47302
  const settings = {
46875
47303
  title: cleanText(options.title) || null,
46876
- avatarRef: cleanText(options.avatarRef) || null,
47304
+ avatarRef: null,
46877
47305
  retention: cleanText(options.retention) || "seen"
46878
47306
  };
46879
47307
  const initialText = typeof options.initialMessage === "string" ? options.initialMessage : "";
@@ -46903,11 +47331,25 @@ function createRuntimeChatActions({
46903
47331
  return { left: result?.left === true || result?.deleted === true, chatId: chat.id };
46904
47332
  }
46905
47333
  async function updateSettings(chatId, settings = {}) {
47334
+ if (Object.prototype.hasOwnProperty.call(settings || {}, "avatarRef")) {
47335
+ throw new Error("use chat.updateAvatar for chat pictures");
47336
+ }
46906
47337
  const { runtime, chat } = await chatForId(chatId);
46907
47338
  await runtime.chat.getSnapshot().updateChatSettings(chat.id, settings);
46908
47339
  const next = await chatForId(chat.id);
46909
47340
  return summarizeChat(runtime, next.chat);
46910
47341
  }
47342
+ async function updateAvatar(chatId, data = null) {
47343
+ const { runtime, chat } = await chatForId(chatId);
47344
+ const snapshot2 = runtime.chat.getSnapshot();
47345
+ if (data == null) {
47346
+ await snapshot2.updateChatSettings(chat.id, { avatarRef: null });
47347
+ } else {
47348
+ await snapshot2.updateChatAvatar(chat.id, data);
47349
+ }
47350
+ const next = await chatForId(chat.id);
47351
+ return summarizeChat(runtime, next.chat);
47352
+ }
46911
47353
  async function enterById(chatId, options = {}) {
46912
47354
  const session = await getSession();
46913
47355
  const { runtime, chat, profile } = await chatForId(chatId);
@@ -47078,6 +47520,13 @@ function createRuntimeChatActions({
47078
47520
  }
47079
47521
  async function saveInChat(chatId, messageId, saved = true, options = {}) {
47080
47522
  const target = await messageForChat(chatId, messageId, options);
47523
+ if (saved === messageIsPermanent(target.message)) {
47524
+ return {
47525
+ chat: summarizeChat(target.runtime, target.chat),
47526
+ saved,
47527
+ updated: 0
47528
+ };
47529
+ }
47081
47530
  const result = saved ? await target.runtime.chat.getSnapshot().makeMessagePermanent(target.chat.id, target.message) : await target.runtime.chat.getSnapshot().makeMessageTemporary(target.chat.id, target.message);
47082
47531
  if (!result?.updated)
47083
47532
  throw new Error("message no longer available");
@@ -47572,6 +48021,7 @@ function createRuntimeChatActions({
47572
48021
  addMembers,
47573
48022
  kickMember,
47574
48023
  leaveMembership,
48024
+ updateAvatar,
47575
48025
  updateSettings,
47576
48026
  readById,
47577
48027
  enterById,
@@ -48012,7 +48462,7 @@ var package_default;
48012
48462
  var init_package2 = __esm(() => {
48013
48463
  package_default = {
48014
48464
  name: "veyl",
48015
- version: "0.59.0",
48465
+ version: "0.60.0",
48016
48466
  private: true,
48017
48467
  license: "UNLICENSED",
48018
48468
  workspaces: {
@@ -48341,6 +48791,7 @@ class AccountRuntime {
48341
48791
  addMembers: (chatId, peers) => this.runtimeChat.addMembers(chatId, peers),
48342
48792
  kickMember: (chatId, memberChatPK) => this.runtimeChat.kickMember(chatId, memberChatPK),
48343
48793
  leaveMembership: (chatId) => this.runtimeChat.leaveMembership(chatId),
48794
+ updateAvatar: (chatId, data) => this.runtimeChat.updateAvatar(chatId, data),
48344
48795
  updateSettings: (chatId, payload) => this.runtimeChat.updateSettings(chatId, payload),
48345
48796
  readById: (chatId, payload) => this.runtimeChat.readById(chatId, payload),
48346
48797
  enterById: (chatId, payload) => this.runtimeChat.enterById(chatId, payload),
@@ -48806,7 +49257,9 @@ class AccountRuntime {
48806
49257
  }
48807
49258
  const vault = await this.cloud.user.vault.read(profile.uid);
48808
49259
  if (!vault) {
48809
- throw new Error("vault not found");
49260
+ throw Object.assign(new Error("vault not found"), {
49261
+ code: "not-found"
49262
+ });
48810
49263
  }
48811
49264
  return {
48812
49265
  mnemonic: await this.ports.vaultCrypto.decryptWalletMnemonic(vault, vaultSecret),
@@ -48824,7 +49277,9 @@ class AccountRuntime {
48824
49277
  }
48825
49278
  const vault = await this.cloud.user.vault.read(profile.uid);
48826
49279
  if (!vault) {
48827
- throw new Error("vault not found");
49280
+ throw Object.assign(new Error("vault not found"), {
49281
+ code: "not-found"
49282
+ });
48828
49283
  }
48829
49284
  const session = await this.bootSession(vault, vaultSecret, profile);
48830
49285
  const next = await this.saveProfile({
@@ -181895,7 +182350,7 @@ function firebaseFunctionTarget(environment, name7) {
181895
182350
  if (!/^[A-Za-z][A-Za-z0-9]*$/u.test(target)) {
181896
182351
  throw new Error("Invalid Firebase function target");
181897
182352
  }
181898
- return `${resolved.name}-${target}`;
182353
+ return resolved.name === "dev" ? `dev-${target}` : target;
181899
182354
  }
181900
182355
  var firebaseConfig, firebaseEnvironments, defaultFirebaseEnvironment;
181901
182356
  var init_config3 = __esm(() => {
@@ -182864,14 +183319,14 @@ function createFirebaseCloud({ db, auth: auth2, getAuth: getAuth2, functions: fu
182864
183319
  const { entryId, expectedEpochId, stateCapability, cleanup = true } = options2 || {};
182865
183320
  if (!chatId || !expectedEpochId || !stateCapability)
182866
183321
  throw new Error("current chat state required");
182867
- await callFunction("deleteChat", {
183322
+ const result = await callFunction("deleteChat", {
182868
183323
  chatId,
182869
183324
  expectedEpochId,
182870
183325
  stateCapabilityWitness: stateCapabilityWitness(stateCapability),
182871
183326
  ...entryId ? { entryId } : {},
182872
183327
  cleanup: cleanup !== false
182873
183328
  });
182874
- return true;
183329
+ return { newlyDeleted: result?.newlyDeleted === true };
182875
183330
  }
182876
183331
  function cleanOpaqueState(value) {
182877
183332
  const chat = value?.chat || value || {};
@@ -183013,10 +183468,10 @@ function createFirebaseCloud({ db, auth: auth2, getAuth: getAuth2, functions: fu
183013
183468
  ]).catch(() => []);
183014
183469
  const [epochSnap, settingsSnap, packageSnap, ownerSnap, ownerMlsSnap] = snapshots;
183015
183470
  const outboxSnaps = snapshots.slice(5);
183016
- const currentEpoch = epochSnap?.data?.() || {};
183471
+ const currentEpoch2 = epochSnap?.data?.() || {};
183017
183472
  const currentSettings = settingsSnap?.data?.() || {};
183018
183473
  const currentPackage = packageSnap?.data?.() || {};
183019
- const matches = epochSnap?.exists?.() && settingsSnap?.exists?.() && packageSnap?.exists?.() && currentEpoch.protocol === CHAT_PROTOCOL_VERSION && currentEpoch.chatId === chatId && currentEpoch.epochVersion === 1 && currentEpoch.settingsId === payload.settings.id && currentEpoch.mlsPackageId === payload.mlsPackage.head.packageId && currentEpoch.mlsPackageDigest === payload.mlsPackage.digest && currentEpoch.status === "open" && sameChatSettingsHead(currentSettings.head, settings.data.head) && sameCloudBytes(currentSettings.body, settings.data.body) && currentPackage?.head?.packageId === payload.mlsPackage.head.packageId && currentPackage?.digest === payload.mlsPackage.digest && ownerSnap?.exists?.() && sameCloudBytes(ownerSnap.data()?.body, ownerEntry.data.body) && ownerMlsSnap?.exists?.() && sameCloudBytes(ownerMlsSnap.data()?.body, ownerMlsState.data.body) && membershipOutbox.every((outbox, index) => {
183474
+ const matches = epochSnap?.exists?.() && settingsSnap?.exists?.() && packageSnap?.exists?.() && currentEpoch2.protocol === CHAT_PROTOCOL_VERSION && currentEpoch2.chatId === chatId && currentEpoch2.epochVersion === 1 && currentEpoch2.settingsId === payload.settings.id && currentEpoch2.mlsPackageId === payload.mlsPackage.head.packageId && currentEpoch2.mlsPackageDigest === payload.mlsPackage.digest && currentEpoch2.status === "open" && sameChatSettingsHead(currentSettings.head, settings.data.head) && sameCloudBytes(currentSettings.body, settings.data.body) && currentPackage?.head?.packageId === payload.mlsPackage.head.packageId && currentPackage?.digest === payload.mlsPackage.digest && ownerSnap?.exists?.() && sameCloudBytes(ownerSnap.data()?.body, ownerEntry.data.body) && ownerMlsSnap?.exists?.() && sameCloudBytes(ownerMlsSnap.data()?.body, ownerMlsState.data.body) && membershipOutbox.every((outbox, index) => {
183020
183475
  const current = outboxSnaps[index]?.data?.() || {};
183021
183476
  return outboxSnaps[index]?.exists?.() && sameCloudBytes(current.body, outbox.data.body) && timestampMs(current.ttl, null) === timestampMs(outbox.data.ttl, null);
183022
183477
  });
@@ -183928,6 +184383,15 @@ function createFirebaseCloud({ db, auth: auth2, getAuth: getAuth2, functions: fu
183928
184383
  await callFunction("unlinkChatOwner", { entryId });
183929
184384
  return true;
183930
184385
  }
184386
+ async function unlinkDeletedUserChat(uid, entryId, chatId) {
184387
+ requireUid(uid);
184388
+ if (!entryId)
184389
+ throw new Error("chat entry id required");
184390
+ if (!chatId)
184391
+ throw new Error("chat id required");
184392
+ await callFunction("unlinkChatOwner", { entryId, deletedChatId: chatId });
184393
+ return true;
184394
+ }
183931
184395
  function userChatEpochCollection(uid, entryId) {
183932
184396
  requireUid(uid);
183933
184397
  if (!entryId)
@@ -184231,6 +184695,7 @@ function createFirebaseCloud({ db, auth: auth2, getAuth: getAuth2, functions: fu
184231
184695
  mutate: mutateUserChat,
184232
184696
  delete: deleteUserChat,
184233
184697
  unlink: unlinkUserChat,
184698
+ unlinkDeleted: unlinkDeletedUserChat,
184234
184699
  membershipOutbox: {
184235
184700
  list: listUserChatMembershipOutbox,
184236
184701
  delete: deleteUserChatMembershipOutbox
@@ -186599,6 +187064,7 @@ class FleetOwner {
186599
187064
  this.manifest = options2.manifest;
186600
187065
  this.seed = options2.seed;
186601
187066
  this.homeDir = options2.homeDir || defaultHomeDir();
187067
+ this.profileHomeDir = options2.profileHomeDir || this.homeDir;
186602
187068
  this.openClient = options2.openClient || createAccountRuntime;
186603
187069
  this.policies = [...options2.policies || []];
186604
187070
  this.policyLoader = options2.policyLoader || null;
@@ -186645,7 +187111,7 @@ class FleetOwner {
186645
187111
  try {
186646
187112
  return {
186647
187113
  ...await this.baseClientOptions(profile),
186648
- homeDir: this.homeDir,
187114
+ homeDir: this.profileHomeDir,
186649
187115
  activate: false,
186650
187116
  credential: material.credential,
186651
187117
  vaultKey: material.vaultSecret,
@@ -186781,6 +187247,36 @@ class FleetOwner {
186781
187247
  await this.lock.release();
186782
187248
  }
186783
187249
  }
187250
+ profileStore(profile) {
187251
+ return new ProfileStore({
187252
+ dir: this.profileHomeDir,
187253
+ profile,
187254
+ activate: false
187255
+ });
187256
+ }
187257
+ async hasLocalProfile(profile) {
187258
+ const name7 = cleanProfileName(profile, "");
187259
+ if (!name7)
187260
+ return false;
187261
+ return Boolean(await this.profileStore(name7).load(name7));
187262
+ }
187263
+ async ensureLocalProfile(record, credential) {
187264
+ const store = this.profileStore(record.profile);
187265
+ if (await store.load(record.profile))
187266
+ return store;
187267
+ await store.save({
187268
+ profile: record.profile,
187269
+ username: record.username,
187270
+ network: record.network,
187271
+ credentialId: credential.credentialId,
187272
+ publicKeyPem: credential.publicKeyPem,
187273
+ privateKeyPem: null,
187274
+ auth: "machine",
187275
+ hasVault: false,
187276
+ createdAt: Date.now()
187277
+ });
187278
+ return store;
187279
+ }
186784
187280
  provision(input = {}) {
186785
187281
  return this.mutate(async () => {
186786
187282
  if (this.fleet) {
@@ -186815,32 +187311,75 @@ class FleetOwner {
186815
187311
  });
186816
187312
  });
186817
187313
  }
186818
- async finishProvision(record, options2) {
186819
- const material = deriveFleetAccount(this.seed, record.accountIndex);
186820
- const store = new ProfileStore({
186821
- dir: this.homeDir,
186822
- profile: record.profile,
186823
- activate: false
187314
+ prepare(profile) {
187315
+ return this.mutate(async () => {
187316
+ if (this.fleet) {
187317
+ throw new Error("stop the fleet owner before preparing an account");
187318
+ }
187319
+ return this.withStoppedOwnerLock(async () => {
187320
+ const record = this.slot(profile);
187321
+ if (!record)
187322
+ throw new Error("fleet profile not found");
187323
+ if (record.state !== "ready") {
187324
+ throw new Error("fleet profile must be ready before preparation");
187325
+ }
187326
+ return this.finishProvision(record, {
187327
+ create: false,
187328
+ recoverMissing: false,
187329
+ saveManifest: false,
187330
+ runHook: false
187331
+ });
187332
+ });
186824
187333
  });
186825
- const existing = await store.load(record.profile);
186826
- if (!existing) {
186827
- await store.save({
186828
- profile: record.profile,
186829
- username: record.username,
186830
- network: record.network,
186831
- credentialId: material.credential.credentialId,
186832
- publicKeyPem: material.credential.publicKeyPem,
186833
- privateKeyPem: null,
186834
- auth: "machine",
186835
- hasVault: false,
186836
- createdAt: Date.now()
187334
+ }
187335
+ retire(profile, options2 = {}) {
187336
+ return this.mutate(async () => {
187337
+ if (options2.confirm !== true) {
187338
+ throw new Error("fleet account retirement requires confirm: true");
187339
+ }
187340
+ if (this.fleet) {
187341
+ throw new Error("stop the fleet owner before retiring an account");
187342
+ }
187343
+ return this.withStoppedOwnerLock(async () => {
187344
+ const record = this.slot(profile);
187345
+ if (!record)
187346
+ throw new Error("fleet profile not found");
187347
+ if (record.state !== "ready") {
187348
+ throw new Error("fleet profile must be ready before retirement");
187349
+ }
187350
+ const clientOptions = await this.derivedClientOptions(record);
187351
+ await this.ensureLocalProfile(record, clientOptions.credential);
187352
+ const client2 = await this.openClient({
187353
+ ...clientOptions,
187354
+ profile: record.profile,
187355
+ network: record.network
187356
+ }, record);
187357
+ try {
187358
+ await client2.account.login({
187359
+ credential: clientOptions.credential
187360
+ });
187361
+ await client2.ensureUnlocked();
187362
+ const result = await client2.account.delete({ confirm: true });
187363
+ await this.saveProfiles(this.manifest.profiles.filter((item) => item.profile !== record.profile), this.manifest.nextAccountIndex);
187364
+ return {
187365
+ profile: record.profile,
187366
+ username: record.username,
187367
+ deleted: result?.deleted !== false
187368
+ };
187369
+ } finally {
187370
+ await client2.close();
187371
+ }
186837
187372
  });
186838
- }
187373
+ });
187374
+ }
187375
+ async finishProvision(record, options2) {
187376
+ const material = deriveFleetAccount(this.seed, record.accountIndex);
187377
+ await this.ensureLocalProfile(record, material.credential);
186839
187378
  const client2 = await this.openClient({
186840
187379
  ...await this.baseClientOptions(record),
186841
187380
  profile: record.profile,
186842
187381
  network: record.network,
186843
- homeDir: this.homeDir,
187382
+ homeDir: this.profileHomeDir,
186844
187383
  activate: false,
186845
187384
  credential: material.credential,
186846
187385
  vaultKey: material.vaultSecret,
@@ -186866,6 +187405,8 @@ class FleetOwner {
186866
187405
  } catch (error) {
186867
187406
  if (error?.code !== "not-found")
186868
187407
  throw error;
187408
+ if (options2.recoverMissing === false)
187409
+ throw error;
186869
187410
  await client2.account.create({
186870
187411
  username: record.username,
186871
187412
  network: record.network,
@@ -186880,23 +187421,38 @@ class FleetOwner {
186880
187421
  credential: material.credential
186881
187422
  });
186882
187423
  }
186883
- if (account.hasVault !== true) {
187424
+ try {
187425
+ await client2.vault.unlock({
187426
+ key: material.vaultSecret,
187427
+ saveKey: false
187428
+ });
187429
+ } catch (error) {
187430
+ if (error?.code !== "not-found")
187431
+ throw error;
187432
+ if (options2.recoverMissing === false)
187433
+ throw error;
186884
187434
  await client2.vault.create({
186885
187435
  key: material.vaultSecret,
186886
187436
  masterSeed: material.masterSeed,
186887
187437
  registry: material.registry,
186888
187438
  saveKey: false
186889
187439
  });
186890
- account = await client2.account.me();
186891
- } else {
186892
- await client2.ensureUnlocked();
186893
- account = await client2.account.me();
186894
187440
  }
187441
+ account = await client2.account.me();
186895
187442
  const ready2 = Object.freeze({
186896
187443
  ...record,
186897
187444
  state: "ready"
186898
187445
  });
186899
- await this.saveProfiles(this.manifest.profiles.map((item) => item.profile === ready2.profile ? ready2 : item), this.manifest.nextAccountIndex);
187446
+ if (options2.runHook !== false && typeof this.options.onProvisioned === "function") {
187447
+ await this.options.onProvisioned({
187448
+ account,
187449
+ client: client2,
187450
+ profile: ready2
187451
+ });
187452
+ }
187453
+ if (options2.saveManifest !== false) {
187454
+ await this.saveProfiles(this.manifest.profiles.map((item) => item.profile === ready2.profile ? ready2 : item), this.manifest.nextAccountIndex);
187455
+ }
186900
187456
  return {
186901
187457
  account,
186902
187458
  accountIndex: ready2.accountIndex,