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