@glyphteck/veyl 0.59.1 → 0.61.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 {
@@ -10729,6 +11130,20 @@ function normalizeCurrentEpoch(value) {
10729
11130
  settings: normalizeChatSettingsProjection(value.settings)
10730
11131
  };
10731
11132
  }
11133
+ function normalizeOwnerRetirement(value, current) {
11134
+ if (value == null)
11135
+ return null;
11136
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
11137
+ throw new Error("invalid chat owner retirement");
11138
+ }
11139
+ const kind = cleanText(value.kind);
11140
+ const epochVersion = value.epochVersion;
11141
+ if (!Object.values(CHAT_OWNER_RETIREMENT_KINDS).includes(kind) || !Number.isSafeInteger(epochVersion) || epochVersion < current.manifest.epochVersion) {
11142
+ throw new Error("invalid chat owner retirement");
11143
+ }
11144
+ const proposalId = kind === CHAT_OWNER_RETIREMENT_KINDS.LEAVE ? cleanTransitionMessageId(value.proposalId) : null;
11145
+ return { kind, epochVersion, proposalId };
11146
+ }
10732
11147
  function normalizeOwnerEntry(value) {
10733
11148
  if (!value || typeof value !== "object" || Array.isArray(value) || value.v !== CHAT_ENTRY_VERSION || value.protocol !== CHAT_PROTOCOL_VERSION) {
10734
11149
  throw new Error("invalid chat entry");
@@ -10749,7 +11164,8 @@ function normalizeOwnerEntry(value) {
10749
11164
  saved: value.saved || null,
10750
11165
  startMs: Number.isFinite(value.startMs) ? value.startMs : null,
10751
11166
  deliveryRegistered: value.deliveryRegistered === true,
10752
- notificationTag: cleanText(value.notificationTag) || null
11167
+ notificationTag: cleanText(value.notificationTag) || null,
11168
+ retirement: normalizeOwnerRetirement(value.retirement, current)
10753
11169
  };
10754
11170
  }
10755
11171
  async function sealOwnChatEntry(chatPrivateKey, entryId, entry) {
@@ -10790,7 +11206,8 @@ function makeOwnChatEntry(epoch, fields = {}) {
10790
11206
  saved: fields.saved || null,
10791
11207
  startMs: Number.isFinite(fields.startMs) ? fields.startMs : null,
10792
11208
  deliveryRegistered: fields.deliveryRegistered === true,
10793
- notificationTag: cleanText(fields.notificationTag) || null
11209
+ notificationTag: cleanText(fields.notificationTag) || null,
11210
+ retirement: fields.retirement || null
10794
11211
  });
10795
11212
  }
10796
11213
  function normalizeOwnerEpochRecord(value) {
@@ -10798,6 +11215,7 @@ function normalizeOwnerEpochRecord(value) {
10798
11215
  throw new Error("invalid owner epoch entry");
10799
11216
  }
10800
11217
  const manifest = normalizeEpochManifest(value.manifest);
11218
+ const leaveProposalId = value.leaveProposalId == null ? null : cleanTransitionMessageId(value.leaveProposalId);
10801
11219
  return {
10802
11220
  v: CHAT_OWNER_EPOCH_ENTRY_VERSION,
10803
11221
  protocol: CHAT_PROTOCOL_VERSION,
@@ -10807,8 +11225,9 @@ function normalizeOwnerEpochRecord(value) {
10807
11225
  manifest,
10808
11226
  epochSecret: cleanChatHex(value.epochSecret, "epoch secret"),
10809
11227
  cutoffMs: Number.isFinite(value.cutoffMs) ? value.cutoffMs : null,
10810
- successorTransitionCommitment: cleanTransitionCommitment(value.successorTransitionCommitment),
10811
- successorTransitionMessageId: cleanTransitionMessageId(value.successorTransitionMessageId)
11228
+ successorTransitionCommitment: leaveProposalId ? null : cleanTransitionCommitment(value.successorTransitionCommitment),
11229
+ successorTransitionMessageId: leaveProposalId ? null : cleanTransitionMessageId(value.successorTransitionMessageId),
11230
+ leaveProposalId
10812
11231
  };
10813
11232
  }
10814
11233
  function makeOwnerEpochRecord(manifest, epochSecret, fields = {}) {
@@ -10819,7 +11238,8 @@ function makeOwnerEpochRecord(manifest, epochSecret, fields = {}) {
10819
11238
  epochSecret: secretHex(epochSecret, "epoch secret"),
10820
11239
  cutoffMs: Number.isFinite(fields.cutoffMs) ? fields.cutoffMs : null,
10821
11240
  successorTransitionCommitment: fields.successorTransitionCommitment,
10822
- successorTransitionMessageId: fields.successorTransitionMessageId
11241
+ successorTransitionMessageId: fields.successorTransitionMessageId,
11242
+ leaveProposalId: fields.leaveProposalId
10823
11243
  });
10824
11244
  }
10825
11245
  async function sealOwnerEpochRecord(chatPrivateKey, entryId, epochEntryId, record) {
@@ -10843,7 +11263,7 @@ async function openOwnerEpochRecord(chatPrivateKey, entryId, epochEntryId, body)
10843
11263
  function ownEpochEntryId(chatPrivateKey, chatId, epochId) {
10844
11264
  return ownerEpochEntryId(chatPrivateKey, chatId, epochId);
10845
11265
  }
10846
- var CHAT_ENTRY_VERSION = 4, CHAT_OWNER_EPOCH_ENTRY_VERSION = 2;
11266
+ var CHAT_ENTRY_VERSION = 4, CHAT_OWNER_EPOCH_ENTRY_VERSION = 2, CHAT_OWNER_RETIREMENT_KINDS;
10847
11267
  var init_entry = __esm(() => {
10848
11268
  init_canonical();
10849
11269
  init_core();
@@ -10855,6 +11275,10 @@ var init_entry = __esm(() => {
10855
11275
  init_state2();
10856
11276
  init_settings2();
10857
11277
  "use client";
11278
+ CHAT_OWNER_RETIREMENT_KINDS = Object.freeze({
11279
+ LEAVE: "leave",
11280
+ REMOVED: "removed"
11281
+ });
10858
11282
  });
10859
11283
 
10860
11284
  // ../../core/chat/epochs/bootstrap.js
@@ -10995,6 +11419,32 @@ async function mutateOwnChatEntry(cloud, identity, entryId, current, update) {
10995
11419
  const committed = await cloud.user.chats.mutate(identity.uid, entryId, mutation);
10996
11420
  return committed?.result ?? mutation.result ?? null;
10997
11421
  }
11422
+ async function retireOwnChatEntry(cloud, identity, entryId, current, fields = {}) {
11423
+ const expectedEpochVersion = fields.expectedEpochVersion;
11424
+ const retirement = fields.retirement;
11425
+ if (!Number.isSafeInteger(expectedEpochVersion) || expectedEpochVersion < 1 || !retirement?.kind || !Number.isSafeInteger(retirement.epochVersion)) {
11426
+ throw new Error("chat owner retirement required");
11427
+ }
11428
+ return mutateOwnChatEntry(cloud, identity, entryId, current, (latest) => {
11429
+ if (latest?.retirement?.kind === retirement.kind && latest.retirement.epochVersion >= retirement.epochVersion) {
11430
+ return { result: latest };
11431
+ }
11432
+ if (latest?.retirement || latest?.current?.manifest?.epochVersion !== expectedEpochVersion) {
11433
+ throw new Error("chat owner epoch changed");
11434
+ }
11435
+ return {
11436
+ entry: {
11437
+ ...latest,
11438
+ routes: {},
11439
+ deliveryRegistered: false,
11440
+ notificationTag: null,
11441
+ retirement
11442
+ },
11443
+ epochs: fields.epoch ? [fields.epoch] : [],
11444
+ mlsDeletes: fields.mlsStateId ? [fields.mlsStateId] : []
11445
+ };
11446
+ });
11447
+ }
10998
11448
  async function openOwnChatMutationEntry(identity, entryId, record) {
10999
11449
  if (!record)
11000
11450
  return null;
@@ -11011,369 +11461,6 @@ var init_owner = __esm(() => {
11011
11461
  "use client";
11012
11462
  });
11013
11463
 
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
11464
  // ../../core/chat/messages/types.js
11378
11465
  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
11466
  var init_types = __esm(() => {
@@ -12080,48 +12167,6 @@ function cleanFrontier(value) {
12080
12167
  throw new Error("chat receipt checkpoint frontier time required");
12081
12168
  return Object.freeze({ id, at: value.at, seenAt: value.seenAt });
12082
12169
  }
12083
- function maskBytes(memberCount2) {
12084
- return Math.ceil(memberCount2 / 8);
12085
- }
12086
- function cleanMask(value, memberCount2, label) {
12087
- const mask = cleanText(value).toLowerCase();
12088
- const size = maskBytes(memberCount2);
12089
- if (!new RegExp(`^[0-9a-f]{${size * 2}}$`, "u").test(mask)) {
12090
- throw new Error(`${label} invalid`);
12091
- }
12092
- const unused = size * 8 - memberCount2;
12093
- if (unused > 0 && Number.parseInt(mask.slice(-2), 16) & (1 << unused) - 1 << 8 - unused) {
12094
- throw new Error(`${label} overflow`);
12095
- }
12096
- return mask;
12097
- }
12098
- function makeMask(indices, memberCount2) {
12099
- const bytes = new Uint8Array(maskBytes(memberCount2));
12100
- for (const index of indices)
12101
- bytes[Math.floor(index / 8)] |= 1 << index % 8;
12102
- return [...bytes].map((value) => value.toString(16).padStart(2, "0")).join("");
12103
- }
12104
- function maskHas(mask, index) {
12105
- const offset = Math.floor(index / 8) * 2;
12106
- return (Number.parseInt(mask.slice(offset, offset + 2), 16) & 1 << index % 8) !== 0;
12107
- }
12108
- function maskCount(mask, memberCount2) {
12109
- let count = 0;
12110
- for (let index = 0;index < memberCount2; index += 1) {
12111
- if (maskHas(mask, index))
12112
- count += 1;
12113
- }
12114
- return count;
12115
- }
12116
- function frontierWithSeenAt(frontier, states) {
12117
- if (!frontier)
12118
- return null;
12119
- return {
12120
- id: frontier.id,
12121
- at: frontier.at,
12122
- seenAt: Math.max(frontier.seenAt, ...states.map((state) => Number(state?.readFrontier?.seenAt) || 0))
12123
- };
12124
- }
12125
12170
  function normalizeChatReceiptProjection(value) {
12126
12171
  if (!value || typeof value !== "object" || Array.isArray(value)) {
12127
12172
  throw new Error("chat receipt checkpoint projection required");
@@ -12130,41 +12175,13 @@ function normalizeChatReceiptProjection(value) {
12130
12175
  if (!Array.isArray(value.revisions) || value.revisions.length !== memberCount2) {
12131
12176
  throw new Error("chat receipt checkpoint revisions invalid");
12132
12177
  }
12133
- const lowFrontier = cleanFrontier(value.lowFrontier);
12134
- const nextLowFrontier = cleanFrontier(value.nextLowFrontier);
12135
- const highFrontier = cleanFrontier(value.highFrontier);
12136
- const nextHighFrontier = cleanFrontier(value.nextHighFrontier);
12137
- const lowMask = cleanMask(value.lowMask, memberCount2, "chat receipt checkpoint low mask");
12138
- const highMask = cleanMask(value.highMask, memberCount2, "chat receipt checkpoint high mask");
12139
- const nextHighMask = cleanMask(value.nextHighMask, memberCount2, "chat receipt checkpoint next high mask");
12140
- if (!highFrontier && maskCount(highMask, memberCount2) !== 0) {
12141
- throw new Error("chat receipt checkpoint high frontier missing");
12142
- }
12143
- if (highFrontier && maskCount(highMask, memberCount2) === 0) {
12144
- throw new Error("chat receipt checkpoint high readers missing");
12145
- }
12146
- if (!nextHighFrontier && maskCount(nextHighMask, memberCount2) !== 0) {
12147
- throw new Error("chat receipt checkpoint next high frontier missing");
12148
- }
12149
- if (nextHighFrontier && maskCount(nextHighMask, memberCount2) === 0) {
12150
- throw new Error("chat receipt checkpoint next high readers missing");
12151
- }
12152
- if (lowFrontier && compareReadFrontiers(lowFrontier, highFrontier) > 0) {
12153
- throw new Error("chat receipt checkpoint frontier order invalid");
12154
- }
12155
- if (nextLowFrontier && compareReadFrontiers(nextLowFrontier, highFrontier) > 0) {
12156
- throw new Error("chat receipt checkpoint next frontier order invalid");
12178
+ if (!Array.isArray(value.frontiers) || value.frontiers.length !== memberCount2) {
12179
+ throw new Error("chat receipt checkpoint frontiers invalid");
12157
12180
  }
12158
12181
  return Object.freeze({
12159
12182
  memberCount: memberCount2,
12160
12183
  revisions: Object.freeze(value.revisions.map(cleanRevision3)),
12161
- lowFrontier,
12162
- lowMask,
12163
- nextLowFrontier,
12164
- highFrontier,
12165
- highMask,
12166
- nextHighFrontier,
12167
- nextHighMask
12184
+ frontiers: Object.freeze(value.frontiers.map(cleanFrontier))
12168
12185
  });
12169
12186
  }
12170
12187
  function buildChatReceiptProjection(manifest, states) {
@@ -12172,36 +12189,66 @@ function buildChatReceiptProjection(manifest, states) {
12172
12189
  const memberCount2 = cleanCount(members.length);
12173
12190
  const values = members.map((member) => states?.get?.(member.chatPK) || null);
12174
12191
  const revisions = values.map((state) => Math.max(0, Number(state?.revision) || 0));
12175
- const missing = [];
12176
- const present = [];
12177
- values.forEach((state, index) => {
12178
- if (state?.readFrontier)
12179
- present.push({ index, state, frontier: state.readFrontier });
12180
- else
12181
- missing.push(index);
12182
- });
12183
- present.sort((left, right) => compareReadFrontiers(left.frontier, right.frontier));
12184
- const minimum = present[0]?.frontier || null;
12185
- const maximum = present.at(-1)?.frontier || null;
12186
- const lowIndices = missing.length ? missing : present.filter((item) => compareReadFrontiers(item.frontier, minimum) === 0).map((item) => item.index);
12187
- const nextLowItems = missing.length ? present : present.filter((item) => compareReadFrontiers(item.frontier, minimum) > 0);
12188
- const nextLow = nextLowItems[0]?.frontier || null;
12189
- const highItems = maximum ? present.filter((item) => compareReadFrontiers(item.frontier, maximum) === 0) : [];
12190
- const belowHigh = maximum ? present.filter((item) => compareReadFrontiers(item.frontier, maximum) < 0) : [];
12191
- const nextHigh = belowHigh.at(-1)?.frontier || null;
12192
- const nextHighItems = nextHigh ? belowHigh.filter((item) => compareReadFrontiers(item.frontier, nextHigh) === 0) : [];
12193
12192
  return normalizeChatReceiptProjection({
12194
12193
  memberCount: memberCount2,
12195
12194
  revisions,
12196
- lowFrontier: missing.length ? null : frontierWithSeenAt(minimum, values.filter(Boolean)),
12197
- lowMask: makeMask(lowIndices, memberCount2),
12198
- nextLowFrontier: frontierWithSeenAt(nextLow, nextLowItems.map((item) => item.state)),
12199
- highFrontier: frontierWithSeenAt(maximum, highItems.map((item) => item.state)),
12200
- highMask: makeMask(highItems.map((item) => item.index), memberCount2),
12201
- nextHighFrontier: frontierWithSeenAt(nextHigh, nextHighItems.map((item) => item.state)),
12202
- nextHighMask: makeMask(nextHighItems.map((item) => item.index), memberCount2)
12195
+ frontiers: values.map((state) => state?.readFrontier || null)
12203
12196
  });
12204
12197
  }
12198
+ function mergeChatReceiptProjections(current, incoming) {
12199
+ if (!current)
12200
+ return normalizeChatReceiptProjection(incoming);
12201
+ if (!incoming)
12202
+ return normalizeChatReceiptProjection(current);
12203
+ const left = normalizeChatReceiptProjection(current);
12204
+ const right = normalizeChatReceiptProjection(incoming);
12205
+ if (left.memberCount !== right.memberCount) {
12206
+ throw new Error("chat receipt checkpoint member count mismatch");
12207
+ }
12208
+ return normalizeChatReceiptProjection({
12209
+ memberCount: left.memberCount,
12210
+ revisions: left.revisions.map((revision, index) => Math.max(revision, right.revisions[index])),
12211
+ frontiers: left.frontiers.map((frontier, index) => {
12212
+ const other = right.frontiers[index];
12213
+ const comparison = compareReadFrontiers(frontier, other);
12214
+ if (comparison > 0)
12215
+ return frontier;
12216
+ if (comparison < 0)
12217
+ return other;
12218
+ if (!frontier)
12219
+ return null;
12220
+ return {
12221
+ ...frontier,
12222
+ seenAt: Math.max(frontier.seenAt, other.seenAt)
12223
+ };
12224
+ })
12225
+ });
12226
+ }
12227
+ function mergeChatReceiptCheckpointsByEpoch(current, incoming) {
12228
+ if (!(incoming instanceof Map) || !incoming.size) {
12229
+ return current instanceof Map ? current : new Map;
12230
+ }
12231
+ if (!(current instanceof Map) || !current.size)
12232
+ return new Map(incoming);
12233
+ const merged = new Map(current instanceof Map ? current : []);
12234
+ let changed = false;
12235
+ for (const [epochId, value] of incoming instanceof Map ? incoming : []) {
12236
+ const existing = merged.get(epochId);
12237
+ if (existing === value)
12238
+ continue;
12239
+ const exact = isChatReceiptCheckpoint(existing) && isChatReceiptCheckpoint(value) && existing.epochId === value.epochId && existing.memberChatPKs.length === value.memberChatPKs.length && existing.memberChatPKs.every((chatPK, index) => chatPK === value.memberChatPKs[index]);
12240
+ const next = exact ? Object.freeze({
12241
+ ...value,
12242
+ publishedAt: Math.max(Number(existing.publishedAt) || 0, Number(value.publishedAt) || 0) || null,
12243
+ projection: mergeChatReceiptProjections(existing.projection, value.projection)
12244
+ }) : value;
12245
+ if (exact && sameChatReceiptCheckpoint(existing, next) && existing.publishedAt === next.publishedAt)
12246
+ continue;
12247
+ merged.set(epochId, next);
12248
+ changed = true;
12249
+ }
12250
+ return changed ? merged : current;
12251
+ }
12205
12252
  function projectChatReceiptCheckpoint(manifest, value) {
12206
12253
  const projection = normalizeChatReceiptProjection(value?.projection || value);
12207
12254
  if (projection.memberCount !== manifest?.members?.length) {
@@ -12234,13 +12281,14 @@ function chatReceiptCheckpointCoversState(checkpoint, manifest, state) {
12234
12281
  function chatReceiptAllSeenFrontier(checkpoint, authorChatPK) {
12235
12282
  if (!isChatReceiptCheckpoint(checkpoint))
12236
12283
  return null;
12237
- const { lowFrontier, lowMask, nextLowFrontier, memberCount: memberCount2 } = checkpoint.projection;
12238
- const authorIndex = checkpoint.memberChatPKs?.indexOf?.(authorChatPK) ?? -1;
12239
- const lowCount = maskCount(lowMask, memberCount2);
12240
- if (authorIndex >= 0 && lowCount === 1 && maskHas(lowMask, authorIndex)) {
12241
- return nextLowFrontier;
12242
- }
12243
- return lowFrontier;
12284
+ const readers = checkpoint.projection.frontiers.filter((_frontier, index) => checkpoint.memberChatPKs[index] !== authorChatPK);
12285
+ if (!readers.length || readers.some((frontier) => !frontier))
12286
+ return null;
12287
+ const minimum = readers.reduce((current, frontier) => !current || compareReadFrontiers(frontier, current) < 0 ? frontier : current, null);
12288
+ return Object.freeze({
12289
+ ...minimum,
12290
+ seenAt: Math.max(...readers.map((frontier) => frontier.seenAt))
12291
+ });
12244
12292
  }
12245
12293
  function chatReceiptProjectionFingerprint(value) {
12246
12294
  return JSON.stringify(normalizeChatReceiptProjection(value));
@@ -12321,7 +12369,7 @@ function cleanChatSettingsEventTitle(value) {
12321
12369
  if (value == null)
12322
12370
  return null;
12323
12371
  const title = cleanText(value);
12324
- if (!title || title.length > 128)
12372
+ if (!title || title.length > CHAT_TITLE_MAX_CHARS)
12325
12373
  throw new Error("invalid chat settings event title");
12326
12374
  return title;
12327
12375
  }
@@ -12336,10 +12384,7 @@ function makeChatSettingsMsg(revision, digest, changes = [], values = {}) {
12336
12384
  const titleChanged = cleanChanges.includes("title");
12337
12385
  const avatarChanged = cleanChanges.includes("avatarRef");
12338
12386
  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
- }
12387
+ const avatarRef = avatarChanged ? values?.avatarRef == null ? null : cleanChatAvatarRef(values.avatarRef) : undefined;
12343
12388
  return {
12344
12389
  t: SYSTEM_MSG_TYPE,
12345
12390
  sys: SYSTEM_SETTINGS_KIND,
@@ -12362,7 +12407,7 @@ function isChatSettingsMsg(msg) {
12362
12407
  return false;
12363
12408
  }
12364
12409
  }
12365
- return !changes.includes("avatarRef") || msg?.avatarRef == null || !!chatAvatarDataUrl(msg.avatarRef);
12410
+ return !changes.includes("avatarRef") || msg?.avatarRef == null || !!chatAvatarFile(msg.avatarRef);
12366
12411
  }
12367
12412
  function isPendingChatSettingsMsg(msg) {
12368
12413
  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 +12415,7 @@ function isPendingChatSettingsMsg(msg) {
12370
12415
  function getChatSettingsEventAvatar(msg) {
12371
12416
  if (!isChatSettingsMsg(msg) || !chatSettingsEventChanges(msg).includes("avatarRef"))
12372
12417
  return "";
12373
- return chatAvatarDataUrl(msg?.avatarRef);
12418
+ return chatAvatarFile(msg?.avatarRef) ? msg.avatarRef : "";
12374
12419
  }
12375
12420
  function isSystemMsg(msg) {
12376
12421
  return !!getSystemMsgText(msg) && (msg?.t === SYSTEM_MSG_TYPE || isMembershipEventMsg(msg));
@@ -12602,7 +12647,7 @@ function getLatestLocalReadTarget(messages, chatPK) {
12602
12647
  function getLatestReadStateTarget(messages) {
12603
12648
  for (let i = (messages?.length || 0) - 1;i >= 0; i -= 1) {
12604
12649
  const message = messages[i];
12605
- if (isServerConfirmedMsg(message) && canShowMsg(message) && !isSystemMsg(message))
12650
+ if (isServerConfirmedMsg(message) && !isSystemMsg(message) && (canShowMsg(message) || isReactionMsg(message)))
12606
12651
  return message;
12607
12652
  }
12608
12653
  return null;
@@ -12735,6 +12780,7 @@ var init_control = __esm(() => {
12735
12780
  init_messagekeys();
12736
12781
  init_ttl();
12737
12782
  init_actions();
12783
+ init_config();
12738
12784
  init_avatar2();
12739
12785
  init_files2();
12740
12786
  init_text();
@@ -13241,13 +13287,12 @@ function withChatPreviewExpired(preview, expiredAt) {
13241
13287
  sourceTs: getChatPreviewSourceTs(preview)
13242
13288
  }));
13243
13289
  }
13244
- function chatPreviewWantsAttention(preview, chatPK) {
13290
+ function chatPreviewCountsAsUnseen(preview, chatPK) {
13245
13291
  if (!preview || fromSelf(preview, chatPK)) {
13246
13292
  return false;
13247
13293
  }
13248
13294
  if (isReactionMsg(preview)) {
13249
- const targetFrom = cleanText(preview?.targetFrom);
13250
- return !!cleanText(preview?.emoji) && (!targetFrom || targetFrom === chatPK);
13295
+ return !!cleanText(preview?.emoji);
13251
13296
  }
13252
13297
  return canRenderPreviewContent(preview);
13253
13298
  }
@@ -14752,10 +14797,6 @@ async function prepareOwnerChatMlsSnapshot(identity, entryId, epochState, snapsh
14752
14797
  function ownerChatMlsSnapshotId(identity, entryId, epochState) {
14753
14798
  return stateAddress(identity, entryId, epochState).stateId;
14754
14799
  }
14755
- async function discardOwnerChatMlsSnapshot(cloud, identity, entryId, epochState) {
14756
- const address = stateAddress(identity, entryId, epochState);
14757
- await cloud.user.chats.mls.delete(identity.uid, entryId, address.stateId).catch(() => false);
14758
- }
14759
14800
  var init_owner2 = __esm(() => {
14760
14801
  init_state3();
14761
14802
  "use client";
@@ -15926,7 +15967,7 @@ function getPeersFromChats(chats) {
15926
15967
  }
15927
15968
  function isChatUnseenForUser(chatData, userChatPK) {
15928
15969
  const last = chatData?.preview;
15929
- if (!last?.ts || !chatPreviewWantsAttention(last, userChatPK))
15970
+ if (!last?.ts || !chatPreviewCountsAsUnseen(last, userChatPK))
15930
15971
  return false;
15931
15972
  const readMs = timestampMs(chatData?.readMs, null);
15932
15973
  const lastMs = timestampMs(last.ts, null);
@@ -16561,7 +16602,45 @@ function reviveMarker(marker) {
16561
16602
  function serializeKeys(keys) {
16562
16603
  return [...keySet(keys)].filter(Boolean).sort();
16563
16604
  }
16564
- function serializeBatch(batch, { expiredKeys, deletedKeys } = {}) {
16605
+ function cleanReceiptCheckpoint(value) {
16606
+ if (!isChatReceiptCheckpoint(value))
16607
+ return null;
16608
+ const epochId = cleanText(value.epochId);
16609
+ const memberChatPKs = (value.memberChatPKs || []).map(cleanText).filter(Boolean);
16610
+ if (!epochId || memberChatPKs.length !== value.memberChatPKs?.length)
16611
+ return null;
16612
+ try {
16613
+ return projectChatReceiptCheckpoint({
16614
+ epochId,
16615
+ members: memberChatPKs.map((chatPK) => ({ chatPK }))
16616
+ }, value);
16617
+ } catch {
16618
+ return null;
16619
+ }
16620
+ }
16621
+ function serializeReceiptCheckpoints(statesByEpoch, epochIds) {
16622
+ const allowed = new Set(epochIds || []);
16623
+ const checkpoints = [];
16624
+ for (const [epochId, value] of statesByEpoch instanceof Map ? statesByEpoch : []) {
16625
+ if (allowed.size && !allowed.has(epochId))
16626
+ continue;
16627
+ const checkpoint = cleanReceiptCheckpoint(value);
16628
+ if (checkpoint)
16629
+ checkpoints.push(checkpoint);
16630
+ }
16631
+ return checkpoints.sort((left, right) => left.epochId.localeCompare(right.epochId));
16632
+ }
16633
+ function reviveReceiptCheckpoints(values) {
16634
+ return new Map((Array.isArray(values) ? values : []).map((value) => cleanReceiptCheckpoint(value)).filter(Boolean).map((checkpoint) => [checkpoint.epochId, checkpoint]));
16635
+ }
16636
+ function receiptEpochIds(messages, fallbackEpochId = "") {
16637
+ const ids = new Set((messages || []).map((message) => cleanText(message?.epochId)).filter(Boolean));
16638
+ const fallback = cleanText(fallbackEpochId);
16639
+ if (fallback)
16640
+ ids.add(fallback);
16641
+ return ids;
16642
+ }
16643
+ function serializeBatch(batch, { expiredKeys, deletedKeys, epochIds } = {}) {
16565
16644
  if (!batch && !expiredKeys?.size && !deletedKeys?.size) {
16566
16645
  return null;
16567
16646
  }
@@ -16571,14 +16650,16 @@ function serializeBatch(batch, { expiredKeys, deletedKeys } = {}) {
16571
16650
  ...Number.isSafeInteger(batch?.epochVersion) ? { epochVersion: batch.epochVersion } : {},
16572
16651
  ...Number.isFinite(batch?.firstMs) ? { firstMs: batch.firstMs } : {},
16573
16652
  ...Number.isFinite(batch?.lastMs) ? { lastMs: batch.lastMs } : {},
16653
+ receiptCheckpoints: serializeReceiptCheckpoints(batch?.memberStatesByEpoch, epochIds),
16574
16654
  expiredKeys: serializeKeys(expiredKeys || batch?.expiredKeys),
16575
16655
  deletedKeys: serializeKeys(deletedKeys || batch?.deletedKeys)
16576
16656
  };
16577
16657
  }
16578
- function batchFromLive(latestMessages, expiredKeys, deletedKeys, fallback) {
16658
+ function batchFromLive(latestMessages, expiredKeys, deletedKeys, fallback, receiptMessages = latestMessages) {
16579
16659
  const source = latestMessages || [];
16660
+ const epochIds = receiptEpochIds(receiptMessages, fallback?.epochId);
16580
16661
  if (!source.length) {
16581
- return serializeBatch(fallback, { expiredKeys, deletedKeys });
16662
+ return serializeBatch(fallback, { expiredKeys, deletedKeys, epochIds });
16582
16663
  }
16583
16664
  const firstMs = getMessageOrderMs(source[0]);
16584
16665
  const lastMs = getMessageOrderMs(source[source.length - 1]);
@@ -16586,8 +16667,9 @@ function batchFromLive(latestMessages, expiredKeys, deletedKeys, fallback) {
16586
16667
  firstMs,
16587
16668
  lastMs,
16588
16669
  epochId: fallback?.epochId,
16589
- epochVersion: fallback?.epochVersion
16590
- } : fallback, { expiredKeys, deletedKeys });
16670
+ epochVersion: fallback?.epochVersion,
16671
+ memberStatesByEpoch: fallback?.memberStatesByEpoch
16672
+ } : fallback, { expiredKeys, deletedKeys, epochIds });
16591
16673
  }
16592
16674
  function reviveBatch(batch, expiredKeys, deletedKeys) {
16593
16675
  const hasRange = Number.isFinite(batch?.firstMs) && Number.isFinite(batch?.lastMs);
@@ -16599,6 +16681,7 @@ function reviveBatch(batch, expiredKeys, deletedKeys) {
16599
16681
  ...cleanText(batch?.epochId) ? { epochId: cleanText(batch.epochId) } : {},
16600
16682
  ...Number.isSafeInteger(batch?.epochVersion) ? { epochVersion: batch.epochVersion } : {},
16601
16683
  ...hasRange ? { firstMs: batch.firstMs, lastMs: batch.lastMs } : {},
16684
+ memberStatesByEpoch: reviveReceiptCheckpoints(batch?.receiptCheckpoints),
16602
16685
  expiredKeys,
16603
16686
  deletedKeys
16604
16687
  };
@@ -16615,10 +16698,20 @@ function mergeBatches(left, right) {
16615
16698
  ...Number.isSafeInteger(right?.epochVersion ?? left?.epochVersion) ? { epochVersion: right?.epochVersion ?? left?.epochVersion } : {},
16616
16699
  ...firstValues.length ? { firstMs: Math.min(...firstValues) } : {},
16617
16700
  ...lastValues.length ? { lastMs: Math.max(...lastValues) } : {},
16701
+ memberStatesByEpoch: mergeChatReceiptCheckpointsByEpoch(left?.memberStatesByEpoch, right?.memberStatesByEpoch),
16618
16702
  expiredKeys: keySet([...left?.expiredKeys || [], ...right?.expiredKeys || []]),
16619
16703
  deletedKeys: keySet([...left?.deletedKeys || [], ...right?.deletedKeys || []])
16620
16704
  };
16621
16705
  }
16706
+ function cachedReceiptCheckpointSignature(statesByEpoch) {
16707
+ const values = [];
16708
+ for (const [epochId, value] of statesByEpoch instanceof Map ? statesByEpoch : []) {
16709
+ const checkpoint = cleanReceiptCheckpoint(value);
16710
+ if (checkpoint)
16711
+ values.push(`${epochId}:${chatReceiptProjectionFingerprint(checkpoint.projection)}`);
16712
+ }
16713
+ return values.sort().join("|");
16714
+ }
16622
16715
  function olderMarker(left, right) {
16623
16716
  if (!left)
16624
16717
  return right || null;
@@ -17021,7 +17114,7 @@ function writeCachedMessageState(cache, { stateVersion = null, chatId, selfChatP
17021
17114
  historyStartReached: nextHistoryStartReached === true,
17022
17115
  olderThan: serializeMarker(storedOlderThan),
17023
17116
  olderLoaded: nextOlderLoaded === true || compacted.historyMessages.length > 0,
17024
- latestServerPage: batchFromLive(compacted.latestMessages, expiredKeys, deletedKeys, nextServerBatch),
17117
+ latestServerPage: batchFromLive(compacted.latestMessages, expiredKeys, deletedKeys, nextServerBatch, compacted.all),
17025
17118
  deletedKeys: serializeKeys(deletedKeys),
17026
17119
  expiredKeys: serializeKeys(expiredKeys),
17027
17120
  historyMessages: compacted.historyMessages,
@@ -17050,6 +17143,7 @@ var init_messages2 = __esm(() => {
17050
17143
  init_state();
17051
17144
  init_messagekeys();
17052
17145
  init_compact();
17146
+ init_receipt();
17053
17147
  init_filename();
17054
17148
  init_time();
17055
17149
  init_core();
@@ -18273,11 +18367,13 @@ function createChatDelete({
18273
18367
  listActionsRef,
18274
18368
  closeMessageBatchRef,
18275
18369
  mutationGate,
18370
+ reconcileEpoch,
18276
18371
  diag
18277
18372
  }) {
18278
18373
  const pendingDeleteIdsRef = { current: new Set };
18279
18374
  const locallyDeletedChatIdsRef = { current: new Set };
18280
18375
  const deletedChatIdsRef = { current: new Set };
18376
+ const removedChatVersionsRef = { current: new Map };
18281
18377
  const keepSelectedDeletedChatIdsRef = { current: new Set };
18282
18378
  const isChatPendingDelete = (chatId) => !!chatId && pendingDeleteIdsRef.current.has(chatId);
18283
18379
  const clearDeletedChatState = (chatIds) => {
@@ -18387,20 +18483,46 @@ function createChatDelete({
18387
18483
  listActionsRef.current?.commitServerChats?.(nextServerChats, { warm: false });
18388
18484
  setSelectedChat((current) => current === chatId ? null : current);
18389
18485
  };
18390
- const dropRemovedChat = (chatId) => {
18391
- if (!chatId)
18486
+ const dropRemovedChat = (chatId, epochVersion) => {
18487
+ if (!chatId || !Number.isSafeInteger(epochVersion) || epochVersion < 1)
18392
18488
  return false;
18393
- deletedChatIdsRef.current.add(chatId);
18394
- locallyDeletedChatIdsRef.current.add(chatId);
18489
+ const previousVersion = removedChatVersionsRef.current.get(chatId) || 0;
18490
+ removedChatVersionsRef.current.set(chatId, Math.max(previousVersion, epochVersion));
18395
18491
  dropChat(chatId);
18396
18492
  return true;
18397
18493
  };
18494
+ const commitRemovedRetirement = (chatId, epochVersion) => {
18495
+ if (!chatId || !Number.isSafeInteger(epochVersion) || epochVersion < 1)
18496
+ return false;
18497
+ const previousVersion = removedChatVersionsRef.current.get(chatId) || 0;
18498
+ removedChatVersionsRef.current.set(chatId, Math.max(previousVersion, epochVersion));
18499
+ pendingDeleteIdsRef.current.delete(chatId);
18500
+ locallyDeletedChatIdsRef.current.delete(chatId);
18501
+ deletedChatIdsRef.current.delete(chatId);
18502
+ keepSelectedDeletedChatIdsRef.current.delete(chatId);
18503
+ confirmDeletedChats([chatId]);
18504
+ return true;
18505
+ };
18506
+ const restoreRemovedChat = (chat) => {
18507
+ const chatId = chat?.id;
18508
+ const removedAtVersion = removedChatVersionsRef.current.get(chatId);
18509
+ if (!chatId || removedAtVersion == null)
18510
+ return false;
18511
+ if (!Number.isSafeInteger(chat.epochVersion) || chat.epochVersion <= removedAtVersion || !Array.isArray(chat.memberChatPKs) || !chat.memberChatPKs.includes(chatPK))
18512
+ return false;
18513
+ removedChatVersionsRef.current.delete(chatId);
18514
+ return true;
18515
+ };
18398
18516
  const dropUnavailableChat = (chatId) => {
18399
18517
  if (!chatId)
18400
18518
  return Promise.resolve(false);
18401
18519
  return (async () => {
18402
18520
  const releaseMutation = await mutationGate?.acquireExclusive(chatId) || (() => {});
18403
18521
  try {
18522
+ if (removedChatVersionsRef.current.has(chatId)) {
18523
+ dropChat(chatId);
18524
+ return true;
18525
+ }
18404
18526
  const chat = lastServerChatsRef.current.find((item) => item?.id === chatId);
18405
18527
  if (chat?.deliveryRegistered && chat?.epochVersion && chatPK && chat?.epochState?.stateCapability) {
18406
18528
  await cloud.delivery.revoke(deriveChatDeliveryCapability(chat.epochState.stateCapability, {
@@ -18477,39 +18599,74 @@ function createChatDelete({
18477
18599
  }, target.members, target.expectedEpochId);
18478
18600
  return result.deliveredCount;
18479
18601
  };
18602
+ const deleteCurrentChat = async (input, options = {}) => {
18603
+ const chatId = cleanText(typeof input === "string" ? input : input?.chatId || input?.id);
18604
+ const attemptedEpochs = new Set;
18605
+ while (chatId) {
18606
+ const target = getDeleteTargets(input)[0];
18607
+ if (!target)
18608
+ return null;
18609
+ const peerChatPK = cleanText(typeof input === "object" ? input?.peerChatPK : "") || cleanText(lastServerChatsRef.current.find((item) => item?.id === chatId)?.peerChatPK);
18610
+ const releaseMutation = await mutationGate?.acquireExclusive([chatId, peerChatPK].filter(Boolean)) || (() => {});
18611
+ let released = false;
18612
+ const release = () => {
18613
+ if (released)
18614
+ return;
18615
+ released = true;
18616
+ releaseMutation();
18617
+ };
18618
+ try {
18619
+ const deletion = await cloud.chat.delete(target.chatId, {
18620
+ entryId: target.entryId,
18621
+ expectedEpochId: target.expectedEpochId,
18622
+ stateCapability: target.stateCapability,
18623
+ cleanup: options.cleanup !== false
18624
+ });
18625
+ if (deletion?.newlyDeleted !== false)
18626
+ await wakeDeletedChat(target);
18627
+ return target;
18628
+ } catch (error) {
18629
+ release();
18630
+ const attemptKey = `${target.epochVersion || 0}:${target.expectedEpochId || ""}`;
18631
+ if (!attemptKey || attemptedEpochs.has(attemptKey) || !isChatEpochConflict(error) || typeof reconcileEpoch !== "function")
18632
+ throw error;
18633
+ attemptedEpochs.add(attemptKey);
18634
+ const reconciled = await reconcileEpoch(chatId);
18635
+ const current = lastServerChatsRef.current.find((item) => item?.id === chatId) || null;
18636
+ if (reconciled?.removed || !current)
18637
+ return null;
18638
+ if (!Number.isSafeInteger(current.epochVersion) || current.epochVersion <= target.epochVersion) {
18639
+ throw error;
18640
+ }
18641
+ } finally {
18642
+ release();
18643
+ }
18644
+ }
18645
+ return null;
18646
+ };
18480
18647
  const deleteChat = async (chat, options = {}) => {
18481
- const targets = getDeleteTargets(chat);
18648
+ const inputs = Array.isArray(chat) ? chat : [chat];
18649
+ const targets = getDeleteTargets(inputs);
18482
18650
  if (!targets.length)
18483
18651
  return false;
18484
18652
  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]) || (() => {});
18653
+ const deletedTargets = [];
18654
+ hideDeletingChats(chatIds, options);
18490
18655
  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();
18656
+ for (const target of targets) {
18657
+ const input = inputs.find((item) => cleanText(typeof item === "string" ? item : item?.chatId || item?.id) === target.chatId) || target.chatId;
18658
+ const deleted = await deleteCurrentChat(input, options);
18659
+ if (deleted)
18660
+ deletedTargets.push(deleted);
18661
+ }
18662
+ await revokeTargetDeliveries(deletedTargets);
18663
+ await options.onDeliveryRevoked?.();
18664
+ confirmDeletedChats(chatIds);
18665
+ } catch (error) {
18666
+ restoreDeletedChats(chatIds);
18667
+ throw error;
18512
18668
  }
18669
+ return Array.isArray(chat) ? targets.length : true;
18513
18670
  };
18514
18671
  const wasChatDeletedLocally = (chatId) => !!chatId && locallyDeletedChatIdsRef.current.has(chatId);
18515
18672
  const ackDeletedChat = (chatId) => {
@@ -18520,21 +18677,25 @@ function createChatDelete({
18520
18677
  pendingDeleteIdsRef.current = new Set;
18521
18678
  locallyDeletedChatIdsRef.current = new Set;
18522
18679
  deletedChatIdsRef.current = new Set;
18680
+ removedChatVersionsRef.current = new Map;
18523
18681
  keepSelectedDeletedChatIdsRef.current = new Set;
18524
18682
  };
18525
18683
  return {
18526
18684
  pendingDeleteIdsRef,
18527
18685
  deletedChatIdsRef,
18686
+ removedChatVersionsRef,
18528
18687
  keepSelectedDeletedChatIdsRef,
18529
18688
  resetDeleteState,
18530
18689
  clearDeletedChatState,
18531
18690
  beginRetirement,
18532
18691
  rollbackRetirement,
18533
18692
  commitRetirement,
18693
+ commitRemovedRetirement,
18534
18694
  restoreDeletedChat,
18535
18695
  deleteChat,
18536
18696
  dropChat,
18537
18697
  dropRemovedChat,
18698
+ restoreRemovedChat,
18538
18699
  dropUnavailableChat,
18539
18700
  wasChatDeletedLocally,
18540
18701
  ackDeletedChat,
@@ -19621,6 +19782,9 @@ var init_chatlive = __esm(() => {
19621
19782
  });
19622
19783
 
19623
19784
  // ../../core/chat/live.js
19785
+ function makeChatCompositionId() {
19786
+ return toHex(randomBytes3(16));
19787
+ }
19624
19788
  function chatMessageCompositionId(message) {
19625
19789
  const value = String(message?.compositionId || "").trim().toLowerCase();
19626
19790
  return CHAT_COMPOSITION_ID_RE.test(value) ? value : "";
@@ -19804,10 +19968,16 @@ function createChatLive({
19804
19968
  onActivity?.(entry.chatId, EMPTY_ACTIVITY);
19805
19969
  };
19806
19970
  const enterChatActivity = async (chatId) => {
19807
- if (!enabled || !chatId || rooms.has(chatId))
19971
+ if (!enabled || !chatId)
19808
19972
  return false;
19973
+ const current = rooms.get(chatId);
19974
+ if (current && !current.closed) {
19975
+ current.retainers += 1;
19976
+ return true;
19977
+ }
19809
19978
  const entry = {
19810
19979
  chatId,
19980
+ retainers: 1,
19811
19981
  epochId: "",
19812
19982
  material: null,
19813
19983
  connection: null,
@@ -19883,6 +20053,9 @@ function createChatLive({
19883
20053
  const entry = rooms.get(chatId);
19884
20054
  if (!entry)
19885
20055
  return false;
20056
+ entry.retainers -= 1;
20057
+ if (entry.retainers > 0)
20058
+ return true;
19886
20059
  rooms.delete(chatId);
19887
20060
  destroy(entry);
19888
20061
  return true;
@@ -19930,13 +20103,26 @@ function createChatLive({
19930
20103
  publish(entry);
19931
20104
  return scheduleReadSnapshot(entry);
19932
20105
  };
20106
+ const flushChatReadFrontier = async (chatId) => {
20107
+ const entry = rooms.get(chatId);
20108
+ if (!entry || entry.closed || !entry.connected || !entry.readFrontier)
20109
+ return false;
20110
+ if (!sendSnapshot(entry))
20111
+ return false;
20112
+ const pending = entry.sendChain;
20113
+ await pending;
20114
+ return !entry.closed;
20115
+ };
19933
20116
  const leaveAllChatActivity = () => {
19934
- for (const chatId of [...rooms.keys()])
19935
- leaveChatActivity(chatId);
20117
+ for (const [chatId, entry] of [...rooms]) {
20118
+ rooms.delete(chatId);
20119
+ destroy(entry);
20120
+ }
19936
20121
  };
19937
20122
  return Object.freeze({
19938
20123
  close: leaveAllChatActivity,
19939
20124
  enterChatActivity,
20125
+ flushChatReadFrontier,
19940
20126
  leaveChatActivity,
19941
20127
  leaveAllChatActivity,
19942
20128
  markChatReadFrontier,
@@ -20040,7 +20226,73 @@ function withoutChatIdentity(options = {}) {
20040
20226
  }
20041
20227
  function refreshQueuedSendOptions(sendOptionsForPeer, peerChatPK, sendOptionOverrides = null) {
20042
20228
  const chatId = cleanText(sendOptionOverrides?.chatId);
20043
- return mergeSendOptions(withoutChatIdentity(sendOptionOverrides), sendOptionsForPeer(peerChatPK, chatId));
20229
+ const current = sendOptionsForPeer(peerChatPK, chatId);
20230
+ if (chatId) {
20231
+ return {
20232
+ ...withoutChatIdentity(sendOptionOverrides),
20233
+ ...current
20234
+ };
20235
+ }
20236
+ return mergeSendOptions(withoutChatIdentity(sendOptionOverrides), current);
20237
+ }
20238
+ function currentEpoch(options) {
20239
+ const manifest = options?.epochState?.manifest || options?.ownEntry?.current?.manifest || null;
20240
+ const epochId = cleanText(manifest?.epochId);
20241
+ const epochVersion = Number(manifest?.epochVersion);
20242
+ return epochId && Number.isInteger(epochVersion) && epochVersion > 0 ? { epochId, epochVersion } : null;
20243
+ }
20244
+ async function runCurrentEpochWrite({
20245
+ chatId,
20246
+ mutationGate,
20247
+ readOptions,
20248
+ reconcileEpoch,
20249
+ write,
20250
+ onCommitted,
20251
+ onEpochAdvance
20252
+ }) {
20253
+ const attemptedEpochs = new Set;
20254
+ while (true) {
20255
+ const releaseMutation = await mutationGate?.acquireShared(chatId) || (() => {});
20256
+ let released = false;
20257
+ let committed = false;
20258
+ const release = () => {
20259
+ if (released)
20260
+ return;
20261
+ released = true;
20262
+ releaseMutation();
20263
+ };
20264
+ let options;
20265
+ let epoch;
20266
+ try {
20267
+ options = readOptions();
20268
+ epoch = currentEpoch(options);
20269
+ return await write(options, (...args) => {
20270
+ committed = true;
20271
+ release();
20272
+ onCommitted?.(...args);
20273
+ });
20274
+ } catch (error) {
20275
+ release();
20276
+ const attemptKey = epoch ? `${epoch.epochVersion}:${epoch.epochId}` : "";
20277
+ if (committed || options?.chatExists !== true || !attemptKey || attemptedEpochs.has(attemptKey) || !isChatEpochConflict(error) || typeof reconcileEpoch !== "function") {
20278
+ throw error;
20279
+ }
20280
+ attemptedEpochs.add(attemptKey);
20281
+ await reconcileEpoch(chatId);
20282
+ const nextEpoch = currentEpoch(readOptions());
20283
+ if (!nextEpoch || nextEpoch.epochVersion <= epoch.epochVersion || nextEpoch.epochId === epoch.epochId) {
20284
+ throw error;
20285
+ }
20286
+ onEpochAdvance?.({
20287
+ fromEpochId: epoch.epochId,
20288
+ fromEpochVersion: epoch.epochVersion,
20289
+ toEpochId: nextEpoch.epochId,
20290
+ toEpochVersion: nextEpoch.epochVersion
20291
+ });
20292
+ } finally {
20293
+ release();
20294
+ }
20295
+ }
20044
20296
  }
20045
20297
  function markSendPerf(diag, label, startedAt, data = {}, minMs = SEND_CHAT_REDUCER_WARN_MS) {
20046
20298
  const elapsedMs = Date.now() - startedAt;
@@ -20305,7 +20557,7 @@ async function makeSharedMediaAttachment(cloud, media, message, data) {
20305
20557
  ...source?.c ? { caption: source.c } : {}
20306
20558
  }));
20307
20559
  }
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 }) {
20560
+ 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
20561
  const adoptedLocalMediaRef = { current: new Set };
20310
20562
  const attachmentPrepareByCidRef = { current: new Map };
20311
20563
  const cachedLocalMediaRef = { current: new Set };
@@ -20665,78 +20917,80 @@ ${cid}` : "";
20665
20917
  };
20666
20918
  const queueSend = async (peerChatPK, message, run, { previewRequired = false, sendOptions: sendOptionOverrides = null } = {}) => {
20667
20919
  const startedAt = Date.now();
20668
- let sendOptions = refreshQueuedSendOptions(sendOptionsForPeer, peerChatPK, sendOptionOverrides);
20669
- let local = await showLocalMessage(peerChatPK, message, sendOptions);
20920
+ const sendOptions = refreshQueuedSendOptions(sendOptionsForPeer, peerChatPK, sendOptionOverrides);
20921
+ const local = await showLocalMessage(peerChatPK, message, sendOptions);
20670
20922
  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
- }
20923
+ const requiredPreview = previewRequired || sendOptions.chatExists !== true;
20924
+ let committed = false;
20925
+ let sent = false;
20926
+ const markSent = () => {
20927
+ if (sent)
20928
+ return;
20929
+ sent = true;
20930
+ sentChatIdsRef.current.add(local.chatId);
20931
+ markLocalStatus(local.chatId, local.cid, LOCAL_SENT);
20932
+ };
20933
+ const markCommitted = () => {
20934
+ if (committed)
20935
+ return;
20936
+ committed = true;
20937
+ markSent();
20938
+ markDone(diag, "chat.send.commit", startedAt, { type: local?.t || "txt", previewRequired: !!requiredPreview });
20939
+ };
20940
+ return await new Promise((resolve, reject) => {
20941
+ const job = {
20942
+ previewKey: local.chatId,
20943
+ previewRequired: requiredPreview,
20944
+ resolve,
20945
+ reject,
20946
+ onSuccess: markSent,
20947
+ onError: (error) => {
20948
+ markError(diag, "chat.send.job", startedAt, error, { type: local?.t || "txt", previewRequired: !!requiredPreview });
20949
+ if (committed)
20950
+ markSent();
20951
+ else
20952
+ markLocalStatus(local.chatId, local.cid, LOCAL_FAILED);
20953
+ },
20954
+ run: async (context) => {
20955
+ const writeStartedAt = Date.now();
20956
+ markDiag(diag, "chat.send.write.start", { type: local?.t || "txt", previewRequired: !!requiredPreview, updatePreview: !!context?.updatePreview });
20957
+ try {
20958
+ const result = await runCurrentEpochWrite({
20959
+ chatId: mutationTarget,
20960
+ mutationGate,
20961
+ readOptions: () => refreshQueuedSendOptions(sendOptionsForPeer, peerChatPK, {
20962
+ ...sendOptionOverrides,
20963
+ chatId: local.chatId
20964
+ }),
20965
+ reconcileEpoch,
20966
+ write: (currentSendOptions, onCommitted) => run({
20967
+ ...context,
20968
+ local,
20969
+ onCommitted,
20970
+ sendOptions: currentSendOptions
20971
+ }),
20972
+ onCommitted: () => {
20973
+ markCommitted();
20974
+ context?.releaseTargets?.();
20975
+ },
20976
+ onEpochAdvance: ({ fromEpochVersion, toEpochVersion }) => {
20977
+ markDiag(diag, "chat.send.epoch.advance", { fromEpochVersion, toEpochVersion });
20978
+ }
20979
+ });
20980
+ markDone(diag, "chat.send.write", writeStartedAt, { type: local?.t || "txt", previewRequired: !!requiredPreview, updatePreview: !!context?.updatePreview });
20981
+ return result;
20982
+ } catch (error) {
20983
+ markError(diag, "chat.send.write", writeStartedAt, error, {
20984
+ type: local?.t || "txt",
20985
+ previewRequired: !!requiredPreview,
20986
+ updatePreview: !!context?.updatePreview
20987
+ });
20988
+ throw error;
20733
20989
  }
20734
- };
20735
- enqueueSendJob(peerChatPK, job, reject);
20736
- });
20737
- } finally {
20738
- releaseMutation?.();
20739
- }
20990
+ }
20991
+ };
20992
+ enqueueSendJob(peerChatPK, job, reject);
20993
+ });
20740
20994
  };
20741
20995
  const sendOptionsForQueuedWrite = (baseOptions, local, updatePreview, onCommitted) => {
20742
20996
  const chatId = local?.chatId;
@@ -23490,7 +23744,7 @@ async function openChatReceiptCheckpoint(epoch, record) {
23490
23744
  cleanBytes(keys.key, plaintext);
23491
23745
  }
23492
23746
  }
23493
- var CHAT_RECEIPT_CHECKPOINT_VERSION = 1, CHAT_RECEIPT_CHECKPOINT_MAX_BYTES = 4096, CHECKPOINT_BODY_VERSION = 1, CHECKPOINT_BODY_SUITE = 1, CHECKPOINT_HEADER_BYTES, CHECKPOINT_SIGN_SCOPE;
23747
+ var CHAT_RECEIPT_CHECKPOINT_VERSION = 2, CHAT_RECEIPT_CHECKPOINT_MAX_BYTES, CHECKPOINT_BODY_VERSION = 1, CHECKPOINT_BODY_SUITE = 1, CHECKPOINT_HEADER_BYTES, CHECKPOINT_SIGN_SCOPE;
23494
23748
  var init_chatreceipt = __esm(() => {
23495
23749
  init_box();
23496
23750
  init_canonical();
@@ -23502,8 +23756,9 @@ var init_chatreceipt = __esm(() => {
23502
23756
  init_protocol();
23503
23757
  init_receipt();
23504
23758
  "use client";
23759
+ CHAT_RECEIPT_CHECKPOINT_MAX_BYTES = 16 * 1024;
23505
23760
  CHECKPOINT_HEADER_BYTES = 2 + BOX_NONCE_BYTES;
23506
- CHECKPOINT_SIGN_SCOPE = encoder.encode("veyl-chat-receipt-checkpoint-v3-sign");
23761
+ CHECKPOINT_SIGN_SCOPE = encoder.encode("veyl-chat-receipt-checkpoint-v4-sign");
23507
23762
  });
23508
23763
 
23509
23764
  // ../../core/chat/receiptsession.js
@@ -23590,6 +23845,7 @@ function createChatReceiptSession({ cloud, epochState, identity, onUpdate, onErr
23590
23845
  let closed = false;
23591
23846
  let closing = false;
23592
23847
  let checkpoint = null;
23848
+ let displayedProjection = null;
23593
23849
  let sourceProjection = null;
23594
23850
  let sourceStop = null;
23595
23851
  let sourceLeaseTimer = null;
@@ -23655,8 +23911,10 @@ function createChatReceiptSession({ cloud, epochState, identity, onUpdate, onErr
23655
23911
  checkpoint = next;
23656
23912
  if (closing || closed)
23657
23913
  return;
23658
- if (next)
23659
- onUpdate?.(projected(next));
23914
+ if (next) {
23915
+ displayedProjection = mergeChatReceiptProjections(displayedProjection, next.projection);
23916
+ onUpdate?.(projected({ ...next, projection: displayedProjection }));
23917
+ }
23660
23918
  if (coversDesired(next)) {
23661
23919
  desiredState = null;
23662
23920
  clearStalledClaimTimer();
@@ -23669,10 +23927,10 @@ function createChatReceiptSession({ cloud, epochState, identity, onUpdate, onErr
23669
23927
  else
23670
23928
  armSourceExpiry();
23671
23929
  };
23672
- const writeCheckpoint = async (mode, expectedVersion = null) => {
23673
- if (closed || closing)
23930
+ const writeCheckpoint = async (mode, expectedVersion = null, projectionOverride = null) => {
23931
+ if (closed || closing && mode !== "release")
23674
23932
  return false;
23675
- const desiredProjection = mode === "publish" ? sourceProjection : null;
23933
+ const desiredProjection = mode === "publish" || mode === "release" ? projectionOverride || sourceProjection : null;
23676
23934
  const result = await cloud.chat.memberState.write({
23677
23935
  epochId: epoch.epochId,
23678
23936
  lane: address.lane,
@@ -23683,19 +23941,22 @@ function createChatReceiptSession({ cloud, epochState, identity, onUpdate, onErr
23683
23941
  const current = record ? await openChatReceiptCheckpoint(epoch, record).catch(() => null) : null;
23684
23942
  const heldByOther = !holdsLease(current, now) && Number(current?.leaseUntil) > now;
23685
23943
  const stalledTakeover = mode === "claim" && expectedVersion && checkpointVersion(current) === expectedVersion && !coversDesired(current);
23944
+ if (mode === "release" && !holdsLease(current, now)) {
23945
+ return { result: { won: false, checkpoint: current } };
23946
+ }
23686
23947
  if (heldByOther && !stalledTakeover)
23687
23948
  return { result: { won: false, checkpoint: current } };
23688
- const projection = desiredProjection || current?.projection || buildChatReceiptProjection(epoch.manifest, new Map);
23949
+ const projection = desiredProjection ? mergeChatReceiptProjections(current?.projection, desiredProjection) : current?.projection || buildChatReceiptProjection(epoch.manifest, new Map);
23689
23950
  if (mode === "publish" && holdsLease(current, now) && chatReceiptProjectionFingerprint(current.projection) === chatReceiptProjectionFingerprint(projection)) {
23690
23951
  return { result: { won: true, checkpoint: current } };
23691
23952
  }
23692
23953
  const term = current ? current.term + (!holdsLease(current, now) || Number(current.leaseUntil) <= now ? 1 : 0) : 1;
23693
23954
  const sealed = await sealChatReceiptCheckpoint(epoch, {
23694
- v: 1,
23955
+ v: CHAT_RECEIPT_CHECKPOINT_VERSION,
23695
23956
  coordinator: epoch.actor.chatPK,
23696
23957
  holder,
23697
23958
  term,
23698
- leaseUntil: now + RECEIPT_COORDINATOR_LEASE_MS,
23959
+ leaseUntil: mode === "release" ? now : now + RECEIPT_COORDINATOR_LEASE_MS,
23699
23960
  publishedAt: now,
23700
23961
  projection
23701
23962
  });
@@ -23745,10 +24006,10 @@ function createChatReceiptSession({ cloud, epochState, identity, onUpdate, onErr
23745
24006
  sourceStop = watchChatMemberStateEvidence(cloud, epochState, ({ states }) => {
23746
24007
  if (closed || closing || !holdsLease(checkpoint))
23747
24008
  return;
23748
- const nextProjection = buildChatReceiptProjection(epoch.manifest, states);
23749
- sourceProjection = nextProjection;
24009
+ sourceProjection = mergeChatReceiptProjections(checkpoint?.projection, buildChatReceiptProjection(epoch.manifest, states));
24010
+ displayedProjection = mergeChatReceiptProjections(displayedProjection, sourceProjection);
23750
24011
  onUpdate?.(projectChatReceiptCheckpoint(epoch.manifest, {
23751
- projection: sourceProjection,
24012
+ projection: displayedProjection,
23752
24013
  publishedAt: Date.now()
23753
24014
  }));
23754
24015
  schedulePublish();
@@ -23838,6 +24099,8 @@ function createChatReceiptSession({ cloud, epochState, identity, onUpdate, onErr
23838
24099
  if (closed || closing)
23839
24100
  return;
23840
24101
  closing = true;
24102
+ const finalProjection = sourceProjection;
24103
+ const release = holdsLease(checkpoint) ? track(writeCheckpoint("release", null, finalProjection)) : null;
23841
24104
  watchRun += 1;
23842
24105
  clearClaimTimer();
23843
24106
  clearStalledClaimTimer();
@@ -23851,7 +24114,7 @@ function createChatReceiptSession({ cloud, epochState, identity, onUpdate, onErr
23851
24114
  closing = false;
23852
24115
  closeReceiptEpoch(opened);
23853
24116
  };
23854
- Promise.allSettled([...operations]).finally(finish);
24117
+ Promise.allSettled([...operations, release].filter(Boolean)).finally(finish);
23855
24118
  }
23856
24119
  };
23857
24120
  }
@@ -24923,6 +25186,62 @@ var init_session2 = __esm(() => {
24923
25186
  init_receiptsession();
24924
25187
  });
24925
25188
 
25189
+ // ../../core/chat/retirement.js
25190
+ function planChatRetirement(chat, selfChatPK) {
25191
+ const self2 = cleanText(selfChatPK);
25192
+ const members = Array.isArray(chat?.members) ? chat.members : [];
25193
+ const remaining = members.filter((member) => cleanText(member?.chatPK) !== self2);
25194
+ const direct = chat?.lineage === CHAT_LINEAGES.DIRECT && members.length === 2;
25195
+ return {
25196
+ action: direct || !remaining.length ? CHAT_RETIREMENT_ACTIONS.DELETE : CHAT_RETIREMENT_ACTIONS.LEAVE,
25197
+ remaining
25198
+ };
25199
+ }
25200
+ async function retireChatMembershipOwner(cloud, identity, entry, fields = {}) {
25201
+ const parent = entry?.current?.manifest;
25202
+ const kind = fields.kind;
25203
+ const epochVersion = fields.epochVersion;
25204
+ if (!parent?.chatId || !identity?.uid || !identity?.chatPrivateKey || !Object.values(CHAT_OWNER_RETIREMENT_KINDS).includes(kind) || !Number.isSafeInteger(epochVersion)) {
25205
+ throw new Error("chat membership retirement required");
25206
+ }
25207
+ const entryId = ownChatEntryId(identity.chatPrivateKey, parent.chatId);
25208
+ const historyId = ownEpochEntryId(identity.chatPrivateKey, parent.chatId, parent.epochId);
25209
+ const cutoffMs = Number.isFinite(fields.cutoffMs) ? fields.cutoffMs : Date.now();
25210
+ const history = makeOwnerEpochRecord(parent, entry.current.epochSecret, {
25211
+ cutoffMs,
25212
+ successorTransitionCommitment: fields.successorTransitionCommitment,
25213
+ successorTransitionMessageId: fields.successorTransitionMessageId,
25214
+ leaveProposalId: fields.leaveProposalId
25215
+ });
25216
+ return retireOwnChatEntry(cloud, identity, entryId, entry, {
25217
+ expectedEpochVersion: parent.epochVersion,
25218
+ retirement: {
25219
+ kind,
25220
+ epochVersion,
25221
+ ...kind === CHAT_OWNER_RETIREMENT_KINDS.LEAVE ? { proposalId: fields.leaveProposalId } : {}
25222
+ },
25223
+ epoch: {
25224
+ id: historyId,
25225
+ record: {
25226
+ body: await sealOwnerEpochRecord(identity.chatPrivateKey, entryId, historyId, history),
25227
+ tsMs: cutoffMs
25228
+ }
25229
+ },
25230
+ mlsStateId: ownerChatMlsSnapshotId(identity, entryId, entry.current)
25231
+ });
25232
+ }
25233
+ var CHAT_RETIREMENT_ACTIONS;
25234
+ var init_retirement = __esm(() => {
25235
+ init_entry();
25236
+ init_manifest();
25237
+ init_owner2();
25238
+ init_owner();
25239
+ CHAT_RETIREMENT_ACTIONS = Object.freeze({
25240
+ DELETE: "delete",
25241
+ LEAVE: "leave"
25242
+ });
25243
+ });
25244
+
24926
25245
  // ../../core/chat/inbox.js
24927
25246
  function normalizeChatPreview(record, message) {
24928
25247
  if (!message || typeof message !== "object")
@@ -24972,7 +25291,7 @@ async function unlinkBlockedChat(cloud, uid, identity, entry, options) {
24972
25291
  const entryId = ownChatEntryId(identity.chatPrivateKey, manifest.chatId);
24973
25292
  markDiag(options.diag, "chat.owner.unlink", { reason: "blocked" });
24974
25293
  await cloud.user.chats.unlink(uid, entryId);
24975
- options.onRemoved?.(manifest.chatId);
25294
+ options.onRemoved?.(manifest.chatId, manifest.epochVersion);
24976
25295
  options.onPingDelete?.(manifest.chatId);
24977
25296
  return { removed: true, blocked: true, chatId: manifest.chatId };
24978
25297
  }
@@ -24991,6 +25310,25 @@ function isRetryableInboxError(error) {
24991
25310
  ];
24992
25311
  return retryableCodes.some((value) => code === value || code.endsWith(`/${value}`)) || /\b(network|offline|timed? out|temporar(?:y|ily)|connection)\b/.test(message);
24993
25312
  }
25313
+ async function retireConfirmedDeletedChat(cloud, uid, identity, entry, options = {}) {
25314
+ const manifest = entry?.current?.manifest;
25315
+ if (!manifest?.chatId || !identity?.chatPrivateKey || !identity?.chatPK) {
25316
+ throw new Error("deleted chat owner entry required");
25317
+ }
25318
+ const entryId = ownChatEntryId(identity.chatPrivateKey, manifest.chatId);
25319
+ await cloud.user.chats.unlinkDeleted(uid, entryId, manifest.chatId);
25320
+ if (entry.deliveryRegistered) {
25321
+ const capability = deriveChatDeliveryCapability(entry.current.stateCapability, {
25322
+ chatId: manifest.chatId,
25323
+ recipientChatPK: identity.chatPK,
25324
+ generation: manifest.epochVersion
25325
+ });
25326
+ await cloud.delivery.revoke(capability).catch(() => false);
25327
+ }
25328
+ markDiag(options.diag, "chat.owner.unlink", { reason: "confirmed_deleted" });
25329
+ options.onPingDelete?.(manifest.chatId);
25330
+ return { removed: true, deleted: true, chatId: manifest.chatId };
25331
+ }
24994
25332
  function inboxErrorReason(error) {
24995
25333
  const message = cleanText(error?.message).toLowerCase();
24996
25334
  if (message === "ping sender identity mismatch")
@@ -25205,7 +25543,7 @@ function sameRoutes(left, right) {
25205
25543
  return leftKeys.length === rightKeys.length && leftKeys.every((key) => sameRoute(left[key], right[key]));
25206
25544
  }
25207
25545
  function sameStableEntry(left, right) {
25208
- return !!left && !!right && left.current?.manifest?.epochId === right.current?.manifest?.epochId && left.current?.settings?.digest === right.current?.settings?.digest && sameRoutes(left.routes, right.routes) && left.deliveryRegistered === right.deliveryRegistered && left.notificationTag === right.notificationTag;
25546
+ return !!left && !!right && left.current?.manifest?.epochId === right.current?.manifest?.epochId && left.current?.settings?.digest === right.current?.settings?.digest && sameRoutes(left.routes, right.routes) && left.deliveryRegistered === right.deliveryRegistered && left.notificationTag === right.notificationTag && left.retirement?.kind === right.retirement?.kind && left.retirement?.epochVersion === right.retirement?.epochVersion;
25209
25547
  }
25210
25548
  function projectOwnChatEntry(entry, entryId, userChatPK, ts, activity = {}) {
25211
25549
  const manifest = entry.current.manifest;
@@ -25255,20 +25593,43 @@ async function processOpenedPing(cloud, uid, identity, ping, openedProfile, hint
25255
25593
  }
25256
25594
  let knownMember = entry?.current?.manifest?.members?.find((member) => member.chatPK === payload.senderChatPK) || null;
25257
25595
  const senderProfile = openedProfile || (knownMember ? { ...knownMember, uid: knownMember.uid } : null) || await resolveSenderProfile(cloud, payload);
25258
- if (entry && payload.epochVersion === entry.current.manifest.epochVersion && cleanText(payload.messageId) && cleanText(currentChat?.inboxMessageId) === cleanText(payload.messageId)) {
25596
+ let restoringMembership = !!entry?.retirement;
25597
+ if (restoringMembership && (payload.kind !== "welcome" || payload.epochVersion <= entry.retirement.epochVersion)) {
25598
+ return "stale";
25599
+ }
25600
+ if (entry && !restoringMembership && payload.epochVersion === entry.current.manifest.epochVersion && cleanText(payload.messageId) && cleanText(currentChat?.inboxMessageId) === cleanText(payload.messageId)) {
25259
25601
  return "duplicate";
25260
25602
  }
25261
- if (entry && payload.epochVersion < entry.current.manifest.epochVersion) {
25603
+ if (entry && !restoringMembership && payload.epochVersion < entry.current.manifest.epochVersion) {
25262
25604
  return "stale";
25263
25605
  }
25264
- if (entry && payload.epochVersion > entry.current.manifest.epochVersion) {
25265
- const advanced = await advanceEntryToEpoch(cloud, uid, identity, entry, payload.epochVersion, options);
25606
+ if (entry && !restoringMembership && payload.epochVersion > entry.current.manifest.epochVersion) {
25607
+ let advanced;
25608
+ try {
25609
+ advanced = await advanceEntryToEpoch(cloud, uid, identity, entry, payload.epochVersion, options);
25610
+ } catch (error) {
25611
+ if (payload.kind !== "chat_deleted")
25612
+ throw error;
25613
+ try {
25614
+ await retireConfirmedDeletedChat(cloud, uid, identity, entry, options);
25615
+ return "deleted";
25616
+ } catch (confirmationError) {
25617
+ if (isRetryableInboxError(confirmationError))
25618
+ throw confirmationError;
25619
+ throw error;
25620
+ }
25621
+ }
25266
25622
  if (advanced?.removed) {
25267
- options.onPingDelete?.(payload.chatId);
25268
- return "deleted";
25623
+ if (payload.kind !== "welcome" || payload.epochVersion <= advanced.entry?.retirement?.epochVersion) {
25624
+ options.onPingDelete?.(payload.chatId);
25625
+ return "deleted";
25626
+ }
25627
+ entry = advanced.entry;
25628
+ restoringMembership = true;
25629
+ } else {
25630
+ entry = advanced;
25631
+ knownMember = entry.current.manifest.members.find((member) => member.chatPK === payload.senderChatPK) || null;
25269
25632
  }
25270
- entry = advanced;
25271
- knownMember = entry.current.manifest.members.find((member) => member.chatPK === payload.senderChatPK) || null;
25272
25633
  }
25273
25634
  if (payload.kind === "chat_deleted") {
25274
25635
  if (!entry || entry.current.manifest.epochId !== payload.epochId || entry.current.manifest.epochVersion !== payload.epochVersion || !knownMember) {
@@ -25288,7 +25649,7 @@ async function processOpenedPing(cloud, uid, identity, ping, openedProfile, hint
25288
25649
  return "deleted";
25289
25650
  }
25290
25651
  let epochState = entry?.current || null;
25291
- if (!entry && payload.kind === "welcome") {
25652
+ if ((!entry || restoringMembership) && payload.kind === "welcome") {
25292
25653
  epochState = await welcomedEpochFromPing(cloud, identity, senderProfile, payload, options);
25293
25654
  } else if (!entry) {
25294
25655
  throw new Error("chat mls welcome required");
@@ -25318,6 +25679,11 @@ async function processOpenedPing(cloud, uid, identity, ping, openedProfile, hint
25318
25679
  options.onPingDelete?.(payload.chatId);
25319
25680
  return "deleted";
25320
25681
  }
25682
+ const latest = await advanceEntryToLatestEpoch(cloud, uid, identity, transitioned, options);
25683
+ if (latest?.removed) {
25684
+ options.onPingDelete?.(payload.chatId);
25685
+ return "deleted";
25686
+ }
25321
25687
  return "transition";
25322
25688
  }
25323
25689
  }
@@ -25331,7 +25697,7 @@ async function processOpenedPing(cloud, uid, identity, ping, openedProfile, hint
25331
25697
  const routes = withSenderRoute(entry, payload, senderProfile, epochState);
25332
25698
  const preview2 = nextPreview(currentChat, record, message, payload);
25333
25699
  const nowMs2 = timestampMs(record?.ts, null) ?? timestampMs(payload.ts, Date.now()) ?? Date.now();
25334
- const membershipWelcome = !entry && payload.kind === "welcome";
25700
+ const membershipWelcome = (!entry || restoringMembership) && payload.kind === "welcome";
25335
25701
  const projectedTsMs = membershipWelcome ? Date.now() : nowMs2;
25336
25702
  let deliveryRegistered = entry?.deliveryRegistered === true;
25337
25703
  if (!deliveryRegistered)
@@ -25349,7 +25715,8 @@ async function processOpenedPing(cloud, uid, identity, ping, openedProfile, hint
25349
25715
  },
25350
25716
  routes,
25351
25717
  deliveryRegistered,
25352
- notificationTag: notificationChatTag(epochState.stateCapability, payload.chatId, identity.chatPK)
25718
+ notificationTag: notificationChatTag(epochState.stateCapability, payload.chatId, identity.chatPK),
25719
+ retirement: null
25353
25720
  } : makeOwnChatEntry(epochState, {
25354
25721
  routes,
25355
25722
  startMs: nowMs2,
@@ -25365,6 +25732,9 @@ async function processOpenedPing(cloud, uid, identity, ping, openedProfile, hint
25365
25732
  ...welcomeMlsState ? { mlsWrites: [{ id: welcomeMlsState.stateId, value: welcomeMlsState.value }] } : {}
25366
25733
  };
25367
25734
  }
25735
+ if (membershipWelcome && current.retirement && epochState.manifest.epochVersion <= current.retirement.epochVersion) {
25736
+ throw new Error("chat welcome is not newer than retirement");
25737
+ }
25368
25738
  if (current.current.manifest.epochVersion > epochState.manifest.epochVersion) {
25369
25739
  return { result: current };
25370
25740
  }
@@ -25373,7 +25743,8 @@ async function processOpenedPing(cloud, uid, identity, ping, openedProfile, hint
25373
25743
  current: nextEntry.current,
25374
25744
  routes: withSenderRoute(current, payload, senderProfile, epochState),
25375
25745
  deliveryRegistered: current.deliveryRegistered || nextEntry.deliveryRegistered,
25376
- notificationTag: nextEntry.notificationTag
25746
+ notificationTag: nextEntry.notificationTag,
25747
+ retirement: membershipWelcome ? null : current.retirement
25377
25748
  };
25378
25749
  if (sameStableEntry(current, candidate) && !welcomeMlsState)
25379
25750
  return { result: current };
@@ -25383,6 +25754,9 @@ async function processOpenedPing(cloud, uid, identity, ping, openedProfile, hint
25383
25754
  ...welcomeMlsState ? { mlsWrites: [{ id: welcomeMlsState.stateId, value: welcomeMlsState.value }] } : {}
25384
25755
  };
25385
25756
  }) : entry;
25757
+ if (committedEntry?.retirement) {
25758
+ throw new Error("chat membership remains retired");
25759
+ }
25386
25760
  if (membershipWelcome && epochState.mlsSnapshot) {
25387
25761
  if (!epochState.mlsKeyPackageLastResort) {
25388
25762
  await cloud.user.mls.keyPackages.deleteState(identity.uid, epochState.mlsKeyPackageId).catch(() => false);
@@ -25426,8 +25800,15 @@ async function processPing(cloud, uid, identity, doc, options) {
25426
25800
  async function applyEpochTransitionAction(cloud, uid, identityValue, entry, action, options, confirmedValue = null) {
25427
25801
  const parent = entry.current.manifest;
25428
25802
  const sender = manifestMember(parent, action.s || action.certificate?.actor);
25429
- if (!sender)
25803
+ const reject = (stage) => {
25804
+ markDiag(options.diag, "chat.epoch.apply.reject", {
25805
+ stage,
25806
+ epochVersion: parent.epochVersion
25807
+ });
25430
25808
  return null;
25809
+ };
25810
+ if (!sender)
25811
+ return reject("sender");
25431
25812
  const engine = await options.mls?.open?.();
25432
25813
  if (!engine)
25433
25814
  throw new Error("mls engine required");
@@ -25435,25 +25816,25 @@ async function applyEpochTransitionAction(cloud, uid, identityValue, entry, acti
25435
25816
  const expectedCommitment = transitionCommitment(action.certificate);
25436
25817
  const confirmed = confirmedValue || await cloud.chat.state.transition(parent.chatId, parent.epochId, entry.current.stateCapability).catch(() => null);
25437
25818
  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;
25819
+ return reject("certificate");
25439
25820
  let leaveProposalId = "";
25440
25821
  if (action.membership?.kind === "leave") {
25441
25822
  const leavingChatPK = cleanText(action.membership?.actor);
25442
25823
  const proposalId = cleanText(action.membership?.proposalId);
25443
25824
  const targetSet = new Set(Array.isArray(action.membership?.targets) ? action.membership.targets.map(cleanText) : []);
25444
25825
  if (!leavingChatPK || !proposalId || targetSet.size !== 1 || !targetSet.has(leavingChatPK))
25445
- return null;
25826
+ return reject("leave-certificate");
25446
25827
  const proposalRecord = await cloud.chat.messages.read(parent.epochId, proposalId, {
25447
25828
  lane: messageLaneForEpoch(entry.current)
25448
25829
  }).catch(() => null);
25449
25830
  const proposal = proposalRecord ? await decryptMsg(proposalRecord, "", "", "", { chatId: parent.chatId, epochState: entry.current }).catch(() => null) : null;
25450
25831
  if (!isLeaveProposalMsg(proposal) || proposal.s !== leavingChatPK)
25451
- return null;
25832
+ return reject("leave-proposal");
25452
25833
  leaveProposalId = proposalId;
25453
25834
  }
25454
25835
  const packageRecord = await cloud.chat.mls.packages.read(parent.chatId, confirmed.mlsPackageId);
25455
25836
  if (!packageRecord || packageRecord.digest !== confirmed.mlsPackageDigest || packageRecord.head.transitionCommitment !== confirmed.transitionCommitment || !verifyChatMlsEpochPackageSignature(packageRecord, sender.chatSigningPK))
25456
- return null;
25837
+ return reject("mls-package");
25457
25838
  const identity = {
25458
25839
  ...identityValue,
25459
25840
  uid,
@@ -25464,8 +25845,11 @@ async function applyEpochTransitionAction(cloud, uid, identityValue, entry, acti
25464
25845
  let parentSnapshot = null;
25465
25846
  let nextSnapshot = null;
25466
25847
  let epochState = null;
25848
+ let openStage = "owner-snapshot";
25849
+ const openStartedAt = Date.now();
25467
25850
  try {
25468
25851
  parentSnapshot = await readOwnerChatMlsSnapshot(cloud, identity, entryId, entry.current);
25852
+ openStage = "mls-commit";
25469
25853
  const processed = engine.processCommit(parentSnapshot, packageRecord.commit);
25470
25854
  nextSnapshot = processed.snapshot;
25471
25855
  if (processed.removed) {
@@ -25477,16 +25861,21 @@ async function applyEpochTransitionAction(cloud, uid, identityValue, entry, acti
25477
25861
  });
25478
25862
  await cloud.delivery.revoke(capability).catch(() => false);
25479
25863
  }
25480
- if (options.wasRemoved?.(parent.chatId) !== true) {
25481
- markDiag(options.diag, "chat.owner.unlink", { reason: "membership" });
25482
- await cloud.user.chats.unlink(uid, entryId);
25483
- }
25864
+ markDiag(options.diag, "chat.owner.retire", { reason: "membership" });
25865
+ const retiredEntry = await retireChatMembershipOwner(cloud, identity, entry, {
25866
+ kind: CHAT_OWNER_RETIREMENT_KINDS.REMOVED,
25867
+ epochVersion: confirmed.nextEpochVersion,
25868
+ cutoffMs: Date.now(),
25869
+ successorTransitionCommitment: confirmed.transitionCommitment,
25870
+ successorTransitionMessageId: messageId
25871
+ });
25484
25872
  if (leaveProposalId) {
25485
25873
  options.leaveProposalObservedAt?.delete(`${parent.chatId}:${leaveProposalId}`);
25486
25874
  }
25487
- options.onRemoved?.(parent.chatId);
25488
- return { removed: true, chatId: parent.chatId };
25875
+ options.onRemoved?.(parent.chatId, confirmed.nextEpochVersion);
25876
+ return { removed: true, chatId: parent.chatId, entry: retiredEntry };
25489
25877
  }
25878
+ openStage = "mls-package-open";
25490
25879
  const opened = await openChatMlsEpochPackage(processed, packageRecord, {
25491
25880
  chatId: parent.chatId,
25492
25881
  senderSigningPK: sender.chatSigningPK,
@@ -25501,8 +25890,13 @@ async function applyEpochTransitionAction(cloud, uid, identityValue, entry, acti
25501
25890
  mlsPackageId: opened.packageId,
25502
25891
  mlsPackageDigest: opened.packageDigest
25503
25892
  };
25893
+ openStage = "settings";
25504
25894
  epochState.settings = await readEpochChatSettings(cloud, epochState);
25505
- } catch {
25895
+ } catch (error) {
25896
+ markError(options.diag, "chat.epoch.apply", openStartedAt, error, {
25897
+ stage: openStage,
25898
+ epochVersion: parent.epochVersion
25899
+ });
25506
25900
  cleanBytes(parentSnapshot, nextSnapshot, epochState?.epochSecret, epochState?.stateCapability);
25507
25901
  return null;
25508
25902
  }
@@ -25589,6 +25983,42 @@ async function advanceEntryToEpoch(cloud, uid, identity, entry, targetEpochVersi
25589
25983
  }).catch(() => null);
25590
25984
  const action = record ? await decryptMsg(record, "", "", "", { chatId: parent.chatId, epochState: current.current }).catch(() => null) : null;
25591
25985
  if (!isEpochTransitionMsg(action)) {
25986
+ markDiag(options.diag, "chat.epoch.advance.pending", {
25987
+ stage: record ? "transition-open" : "transition-record",
25988
+ epochVersion: parent.epochVersion
25989
+ });
25990
+ const error = new Error("chat epoch transition message is not available yet");
25991
+ error.code = "chat/epoch-pending";
25992
+ throw error;
25993
+ }
25994
+ const advanced = await applyEpochTransitionAction(cloud, uid, identity, current, action, options, confirmed);
25995
+ if (advanced?.removed)
25996
+ return advanced;
25997
+ if (!advanced?.current?.manifest || advanced.current.manifest.epochVersion <= parent.epochVersion) {
25998
+ const error = new Error("chat epoch transition could not be verified");
25999
+ error.code = "chat/epoch-pending";
26000
+ throw error;
26001
+ }
26002
+ current = advanced;
26003
+ }
26004
+ return current;
26005
+ }
26006
+ async function advanceEntryToLatestEpoch(cloud, uid, identity, entry, options = {}) {
26007
+ let current = entry;
26008
+ while (current?.current?.manifest) {
26009
+ const parent = current.current.manifest;
26010
+ const confirmed = await cloud.chat.state.transition(parent.chatId, parent.epochId, current.current.stateCapability);
26011
+ if (!confirmed?.transitionMessageId)
26012
+ return current;
26013
+ const record = await cloud.chat.messages.read(parent.epochId, confirmed.transitionMessageId, {
26014
+ lane: messageLaneForEpoch(current.current)
26015
+ }).catch(() => null);
26016
+ const action = record ? await decryptMsg(record, "", "", "", { chatId: parent.chatId, epochState: current.current }).catch(() => null) : null;
26017
+ if (!isEpochTransitionMsg(action)) {
26018
+ markDiag(options.diag, "chat.epoch.advance.pending", {
26019
+ stage: record ? "transition-open" : "transition-record",
26020
+ epochVersion: parent.epochVersion
26021
+ });
25592
26022
  const error = new Error("chat epoch transition message is not available yet");
25593
26023
  error.code = "chat/epoch-pending";
25594
26024
  throw error;
@@ -25706,6 +26136,7 @@ var init_inbox2 = __esm(() => {
25706
26136
  init_membership();
25707
26137
  init_entry();
25708
26138
  init_owner();
26139
+ init_retirement();
25709
26140
  init_manifest();
25710
26141
  init_state2();
25711
26142
  init_transition();
@@ -25725,6 +26156,17 @@ var init_inbox2 = __esm(() => {
25725
26156
  });
25726
26157
 
25727
26158
  // ../../core/chat/list.js
26159
+ function chatOwnerRevision(chat) {
26160
+ return Number.isSafeInteger(chat?.ownEntry?.ownerRevision) ? chat.ownEntry.ownerRevision : 0;
26161
+ }
26162
+ function serverChatCoversOverlay(chat, overlay) {
26163
+ const serverEpoch = Number.isSafeInteger(chat?.epochVersion) ? chat.epochVersion : 0;
26164
+ const overlayEpoch = Number.isSafeInteger(overlay?.epochVersion) ? overlay.epochVersion : 0;
26165
+ if (serverEpoch !== overlayEpoch) {
26166
+ return serverEpoch > overlayEpoch;
26167
+ }
26168
+ return chatOwnerRevision(chat) >= chatOwnerRevision(overlay);
26169
+ }
25728
26170
  function inboxRetryKey(doc) {
25729
26171
  const id = typeof doc?.id === "string" ? doc.id : "";
25730
26172
  const ts = timestampMs(doc?.ts, 0) || 0;
@@ -25797,13 +26239,17 @@ function listenToChats(cloud, uid, userChatPK, userPrivKey, onUpdate, onError, o
25797
26239
  nextAfterChat: null,
25798
26240
  hasMore: false
25799
26241
  };
25800
- const optimisticChats = new Map;
25801
- const mergedChats = (chats) => {
26242
+ const optimisticChats = options.optimisticChats instanceof Map ? options.optimisticChats : new Map;
26243
+ const mergedChats = (chats, mergeOptions = {}) => {
25802
26244
  const nextById = new Map;
25803
26245
  for (const chat of chats || []) {
25804
26246
  const optimistic = optimisticChats.get(chat?.id);
25805
- if (chat?.id)
26247
+ if (chat?.id) {
25806
26248
  nextById.set(chat.id, mergeChatActivity(chat, optimistic, userChatPK));
26249
+ if (mergeOptions.settle === true && optimistic && serverChatCoversOverlay(chat, optimistic)) {
26250
+ optimisticChats.delete(chat.id);
26251
+ }
26252
+ }
25807
26253
  }
25808
26254
  for (const [id, chat] of optimisticChats.entries()) {
25809
26255
  if (!nextById.has(id)) {
@@ -25868,7 +26314,7 @@ function listenToChats(cloud, uid, userChatPK, userPrivKey, onUpdate, onError, o
25868
26314
  if (!closed && runId === run) {
25869
26315
  processedFirst = true;
25870
26316
  const chats = page.chats;
25871
- latestChats = mergedChats(chats);
26317
+ latestChats = mergedChats(chats, { settle: true });
25872
26318
  latestMeta = {
25873
26319
  nextAfterChat: snapshot.nextAfterChat,
25874
26320
  hasMore: snapshot.hasMore
@@ -26111,6 +26557,9 @@ async function decryptChatEntry(entryRecord, userChatPK, userPrivKey) {
26111
26557
  if (entry.ownerRevision !== data?.revision) {
26112
26558
  throw new Error("chat owner revision mismatch");
26113
26559
  }
26560
+ if (entry.retirement) {
26561
+ return null;
26562
+ }
26114
26563
  if (entry.protocol !== CHAT_PROTOCOL_VERSION) {
26115
26564
  return null;
26116
26565
  }
@@ -26250,6 +26699,7 @@ function createChatList({
26250
26699
  localByChatRef,
26251
26700
  pendingDeleteIdsRef,
26252
26701
  deletedChatIdsRef,
26702
+ removedChatVersionsRef,
26253
26703
  keepSelectedDeletedChatIdsRef,
26254
26704
  readCacheRef,
26255
26705
  lastServerChatsRef,
@@ -26267,6 +26717,7 @@ function createChatList({
26267
26717
  onTransition,
26268
26718
  onRemoved,
26269
26719
  wasRemoved,
26720
+ restoreRemovedChat,
26270
26721
  resetExternalChatState,
26271
26722
  mutationGate,
26272
26723
  onMlsKeyPackageConsumed
@@ -26295,6 +26746,8 @@ function createChatList({
26295
26746
  const cachedHistoryEndRef = { current: false };
26296
26747
  const cachedHistoryHeadRef = { current: [] };
26297
26748
  const previewTimerRef = { current: null };
26749
+ const epochReconciliations = new Map;
26750
+ const inboxOverlayChats = new Map;
26298
26751
  let listening = false;
26299
26752
  let unsubscribeChats = null;
26300
26753
  let snapshot = null;
@@ -26303,7 +26756,7 @@ function createChatList({
26303
26756
  let blockedUidSet = new Set(blockedUids);
26304
26757
  let schedulePreviewExpiry = () => {};
26305
26758
  let fillVisibleChatPage = () => {};
26306
- const isHiddenChatId = (chatId) => !!chatId && (pendingDeleteIdsRef.current.has(chatId) || deletedChatIdsRef.current.has(chatId));
26759
+ const isHiddenChatId = (chatId) => !!chatId && (pendingDeleteIdsRef.current.has(chatId) || deletedChatIdsRef.current.has(chatId) || removedChatVersionsRef.current.has(chatId));
26307
26760
  const filterHiddenChats = (nextChats) => (nextChats || []).filter((chatItem) => chatItem?.id && !isHiddenChatId(chatItem.id));
26308
26761
  const nextValue = (current, next) => typeof next === "function" ? next(current) : next;
26309
26762
  const publish = () => {
@@ -26518,7 +26971,13 @@ function createChatList({
26518
26971
  return shownChats;
26519
26972
  };
26520
26973
  const applyInboxChat = (chat) => {
26521
- if (!chat?.id || isHiddenChatId(chat.id)) {
26974
+ if (!chat?.id) {
26975
+ return;
26976
+ }
26977
+ if (removedChatVersionsRef.current.has(chat.id)) {
26978
+ restoreRemovedChat?.(chat);
26979
+ }
26980
+ if (isHiddenChatId(chat.id)) {
26522
26981
  return;
26523
26982
  }
26524
26983
  const updateStartedAt = Date.now();
@@ -26544,13 +27003,113 @@ function createChatList({
26544
27003
  const applyInboxDelete = (chatId) => {
26545
27004
  if (!chatId)
26546
27005
  return;
27006
+ inboxOverlayChats.delete(chatId);
26547
27007
  pendingDeleteIdsRef.current.delete(chatId);
26548
27008
  commitServerChats(lastServerChatsRef.current.filter((chat) => chat?.id !== chatId), {
26549
27009
  warm: false
26550
27010
  });
26551
27011
  setSelectedChat((current) => current === chatId ? null : current);
26552
27012
  };
27013
+ const reconcileChatEpoch = (chatId) => {
27014
+ if (!chatId || !uid || !chatPK || !chatPrivateKey || !chatSigningPK || !chatSigningSecret || !notificationPK) {
27015
+ return Promise.resolve({ advanced: false, chat: null, removed: false });
27016
+ }
27017
+ const active = epochReconciliations.get(chatId);
27018
+ if (active)
27019
+ return active;
27020
+ let task;
27021
+ task = (async () => {
27022
+ const releaseMutation = await mutationGate?.acquireExclusive(chatId) || (() => {});
27023
+ try {
27024
+ const initial = getChatEntry(chatId);
27025
+ if (!initial?.ownEntry)
27026
+ return { advanced: false, chat: initial || null, removed: false };
27027
+ const initialVersion = initial.epochVersion;
27028
+ const identity = {
27029
+ uid,
27030
+ chatPK,
27031
+ chatPrivateKey,
27032
+ chatSigningPK,
27033
+ chatSigningSecret,
27034
+ notificationPK,
27035
+ notificationPrivateKey
27036
+ };
27037
+ const advanceOptions = {
27038
+ blockedUids: blockedUidSet,
27039
+ mls,
27040
+ diag,
27041
+ onMlsKeyPackageConsumed,
27042
+ onRemoved: (removedChatId, epochVersion) => {
27043
+ onRemoved?.(removedChatId, epochVersion);
27044
+ applyInboxDelete(removedChatId);
27045
+ },
27046
+ wasRemoved,
27047
+ onTransition: (chat, message) => {
27048
+ onTransition?.(chat, message);
27049
+ applyInboxChat(chat);
27050
+ }
27051
+ };
27052
+ let working = initial;
27053
+ let result;
27054
+ while (true) {
27055
+ try {
27056
+ result = await advanceEntryToLatestEpoch(cloud, uid, identity, working.ownEntry, advanceOptions);
27057
+ break;
27058
+ } catch (error) {
27059
+ const code = String(error?.code || "").trim().toLowerCase();
27060
+ if (code === "permission-denied" || code.endsWith("/permission-denied")) {
27061
+ result = await retireConfirmedDeletedChat(cloud, uid, identity, working.ownEntry, {
27062
+ diag,
27063
+ onPingDelete: applyInboxDelete
27064
+ });
27065
+ break;
27066
+ }
27067
+ if (code !== "chat/epoch-pending")
27068
+ throw error;
27069
+ const canonical = await getChat(cloud, uid, chatId, chatPK, chatPrivateKey);
27070
+ if (!canonical?.ownEntry || canonical.epochVersion <= working.epochVersion)
27071
+ throw error;
27072
+ markDiag(diag, "chat.epoch.reconcile.rebase", {
27073
+ fromEpochVersion: working.epochVersion,
27074
+ toEpochVersion: canonical.epochVersion
27075
+ });
27076
+ applyInboxChat(canonical);
27077
+ working = canonical;
27078
+ }
27079
+ }
27080
+ if (result?.removed) {
27081
+ markDiag(diag, "chat.epoch.reconcile.done", {
27082
+ fromEpochVersion: initialVersion,
27083
+ removed: true
27084
+ });
27085
+ return { advanced: true, chat: null, removed: true };
27086
+ }
27087
+ const latest = getChatEntry(chatId);
27088
+ const advanced = Number(latest?.epochVersion || 0) > Number(initialVersion || 0);
27089
+ if (advanced) {
27090
+ markDiag(diag, "chat.epoch.reconcile.done", {
27091
+ fromEpochVersion: initialVersion,
27092
+ toEpochVersion: latest.epochVersion,
27093
+ removed: false
27094
+ });
27095
+ }
27096
+ return {
27097
+ advanced,
27098
+ chat: latest || null,
27099
+ removed: false
27100
+ };
27101
+ } finally {
27102
+ releaseMutation();
27103
+ }
27104
+ })().finally(() => {
27105
+ if (epochReconciliations.get(chatId) === task)
27106
+ epochReconciliations.delete(chatId);
27107
+ });
27108
+ epochReconciliations.set(chatId, task);
27109
+ return task;
27110
+ };
26553
27111
  const resetChatList = (ready = false) => {
27112
+ inboxOverlayChats.clear();
26554
27113
  chatSourceReady = ready;
26555
27114
  syncChatDataReady();
26556
27115
  setIsChatListLive(false);
@@ -26674,7 +27233,15 @@ function createChatList({
26674
27233
  };
26675
27234
  const unsubscribe = listenToChats(cloud, uid, chatPK, chatPrivateKey, (nextChats, _nextPeers, meta = {}) => {
26676
27235
  const updateStartedAt = Date.now();
26677
- const rawNextChats = filterHiddenChats(Array.isArray(nextChats) ? nextChats : []);
27236
+ const incomingChats = Array.isArray(nextChats) ? nextChats : [];
27237
+ if (meta?.source === "inbox") {
27238
+ for (const chat of incomingChats) {
27239
+ if (removedChatVersionsRef.current.has(chat?.id)) {
27240
+ restoreRemovedChat?.(chat);
27241
+ }
27242
+ }
27243
+ }
27244
+ const rawNextChats = filterHiddenChats(incomingChats);
26678
27245
  hydrateReadCache(rawNextChats, readCacheRef.current);
26679
27246
  const readAppliedChats = applyReadCache(rawNextChats, chatPK, readCacheRef.current);
26680
27247
  const trimmedChats = trimExpiredChatPreviews(readAppliedChats, { skipChatId: selectedChatIdRef.current });
@@ -26779,6 +27346,7 @@ function createChatList({
26779
27346
  blockedUids: blockedUidSet,
26780
27347
  localCache,
26781
27348
  diag,
27349
+ optimisticChats: inboxOverlayChats,
26782
27350
  getCurrentChat: (chatId) => lastServerChatsRef.current.find((chatItem) => chatItem?.id === chatId) || null,
26783
27351
  mutationGate,
26784
27352
  onMlsKeyPackageConsumed
@@ -26957,12 +27525,16 @@ function createChatList({
26957
27525
  };
26958
27526
  const hasChat = (chatId) => !!chatId && (serverChatIdsSet.has(chatId) || chatLoadsRef.current.has(chatId)) && !isHiddenChatId(chatId);
26959
27527
  const setSources = (nextSources) => {
27528
+ const accountChanged = cloud !== nextSources.cloud || uid !== nextSources.uid || chatPK !== nextSources.chatPK || chatPrivateKey !== nextSources.chatPrivateKey;
26960
27529
  const nextBlocked = sortedUniqueValues(Array.isArray(nextSources.blocked) ? nextSources.blocked : []);
26961
27530
  const nextBlockedUidsKey = nextBlocked.join("|");
26962
27531
  const nextBlockedReady = nextSources.blockedReady === true;
26963
27532
  const blockedFilterChanged = blockedUidsKey !== nextBlockedUidsKey || blockedReady !== nextBlockedReady;
26964
27533
  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
27534
  const selectedChatChanged = selectedChatId !== nextSources.selectedChatId;
27535
+ if (accountChanged) {
27536
+ inboxOverlayChats.clear();
27537
+ }
26966
27538
  ({
26967
27539
  cloud,
26968
27540
  uid,
@@ -26984,6 +27556,7 @@ function createChatList({
26984
27556
  localByChatRef,
26985
27557
  pendingDeleteIdsRef,
26986
27558
  deletedChatIdsRef,
27559
+ removedChatVersionsRef,
26987
27560
  keepSelectedDeletedChatIdsRef,
26988
27561
  readCacheRef,
26989
27562
  lastServerChatsRef,
@@ -27001,6 +27574,7 @@ function createChatList({
27001
27574
  onTransition,
27002
27575
  onRemoved,
27003
27576
  wasRemoved,
27577
+ restoreRemovedChat,
27004
27578
  resetExternalChatState,
27005
27579
  mutationGate,
27006
27580
  onMlsKeyPackageConsumed
@@ -27044,6 +27618,8 @@ function createChatList({
27044
27618
  previewTimerRef.current = null;
27045
27619
  }
27046
27620
  flushChatCacheWrite();
27621
+ epochReconciliations.clear();
27622
+ inboxOverlayChats.clear();
27047
27623
  subscribers.clear();
27048
27624
  };
27049
27625
  const subscribe = (subscriber) => {
@@ -27069,6 +27645,7 @@ function createChatList({
27069
27645
  getChatPreviewKey: getChatPreviewKey2,
27070
27646
  getChatRetention,
27071
27647
  getChatEntry,
27648
+ reconcileChatEpoch,
27072
27649
  sendOptionsForPeer,
27073
27650
  hasChat
27074
27651
  };
@@ -27079,32 +27656,13 @@ var init_listsession = __esm(() => {
27079
27656
  init_localdata();
27080
27657
  init_chats();
27081
27658
  init_list();
27659
+ init_inbox2();
27082
27660
  init_ttl();
27083
27661
  init_ids();
27084
27662
  init_time();
27085
27663
  CHAT_LIST_RESET_ERROR_CODES = new Set(["permission-denied", "unauthenticated"]);
27086
27664
  });
27087
27665
 
27088
- // ../../core/chat/retirement.js
27089
- function planChatRetirement(chat, selfChatPK) {
27090
- const self2 = cleanText(selfChatPK);
27091
- const members = Array.isArray(chat?.members) ? chat.members : [];
27092
- const remaining = members.filter((member) => cleanText(member?.chatPK) !== self2);
27093
- const direct = chat?.lineage === CHAT_LINEAGES.DIRECT && members.length === 2;
27094
- return {
27095
- action: direct || !remaining.length ? CHAT_RETIREMENT_ACTIONS.DELETE : CHAT_RETIREMENT_ACTIONS.LEAVE,
27096
- remaining
27097
- };
27098
- }
27099
- var CHAT_RETIREMENT_ACTIONS;
27100
- var init_retirement = __esm(() => {
27101
- init_manifest();
27102
- CHAT_RETIREMENT_ACTIONS = Object.freeze({
27103
- DELETE: "delete",
27104
- LEAVE: "leave"
27105
- });
27106
- });
27107
-
27108
27666
  // ../../core/utils/appstate.js
27109
27667
  function isForegroundAppState(state) {
27110
27668
  const value = String(state || "").trim().toLowerCase();
@@ -27283,7 +27841,6 @@ function createChatSession({
27283
27841
  const closeMessageBatchRef = { current: null };
27284
27842
  const localMessageDiagRef = { current: "" };
27285
27843
  const epochHistoryRef = { current: new Map };
27286
- const epochRecoveryRef = { current: new Map };
27287
27844
  const settingsUpdateStatesRef = { current: new Map };
27288
27845
  const membershipUpdateStatesRef = { current: new Map };
27289
27846
  const directChatOpensRef = { current: new Map };
@@ -27322,6 +27879,7 @@ function createChatSession({
27322
27879
  kickChatMember,
27323
27880
  leaveChat,
27324
27881
  updateChatSettings: updateChatSettings2,
27882
+ updateChatAvatar,
27325
27883
  dropChat,
27326
27884
  dropUnavailableChat,
27327
27885
  deleteChat,
@@ -27329,6 +27887,7 @@ function createChatSession({
27329
27887
  deleteAllChats,
27330
27888
  restoreDeletedChat,
27331
27889
  enterChatActivity,
27890
+ flushChatReadFrontier,
27332
27891
  leaveChatActivity,
27333
27892
  markChatReadState,
27334
27893
  markChatRead,
@@ -27780,51 +28339,15 @@ function createChatSession({
27780
28339
  return true;
27781
28340
  };
27782
28341
  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))
28342
+ if (![...messages || []].reverse().some(isEpochTransitionMsg))
27787
28343
  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();
28344
+ const startedAt = Date.now();
28345
+ chatListOwner.reconcileChatEpoch(chatId).then((result) => {
28346
+ if (result.advanced)
28347
+ epochHistoryRef.current.delete(chatId);
28348
+ }).catch((error) => {
28349
+ markError(diag, "chat.epoch.reconcile", startedAt, error);
28350
+ });
27828
28351
  };
27829
28352
  const setChats = (...args) => chatListOwner.setChats(...args);
27830
28353
  const setLastChat = (...args) => chatListOwner.setLastChat(...args);
@@ -27842,11 +28365,6 @@ function createChatSession({
27842
28365
  liveActions?.close?.();
27843
28366
  liveActivityByChat = new Map;
27844
28367
  messageBatchOwner.clear();
27845
- for (const pending of epochRecoveryRef.current.values()) {
27846
- if (typeof pending === "number")
27847
- clearTimeout(pending);
27848
- }
27849
- epochRecoveryRef.current.clear();
27850
28368
  epochHistoryRef.current.clear();
27851
28369
  membershipUpdateStatesRef.current.clear();
27852
28370
  directChatOpensRef.current.clear();
@@ -28296,14 +28814,18 @@ function createChatSession({
28296
28814
  recipientChatPK: chatPK,
28297
28815
  generation: chat.epochVersion
28298
28816
  });
28817
+ await retireChatMembershipOwner(cloud, transitionIdentity(), chat.ownEntry, {
28818
+ kind: CHAT_OWNER_RETIREMENT_KINDS.LEAVE,
28819
+ epochVersion: chat.epochVersion,
28820
+ cutoffMs: Date.now(),
28821
+ leaveProposalId: proposal.message.cid
28822
+ });
28299
28823
  await cloud.delivery.revoke(capability).catch(() => false);
28300
- await discardOwnerChatMlsSnapshot(cloud, transitionIdentity(), chat.entryId, chat.epochState);
28301
- await cloud.user.chats.unlink(uid, chat.entryId);
28302
28824
  if (retirementStarted) {
28303
- deleteActions.commitRetirement(chatId);
28825
+ deleteActions.commitRemovedRetirement(chatId, chat.epochVersion + 1);
28304
28826
  retirementStarted = false;
28305
28827
  } else {
28306
- deleteActions.dropChat(chatId);
28828
+ deleteActions.dropRemovedChat(chatId, chat.epochVersion + 1);
28307
28829
  }
28308
28830
  return { left: true, message: proposal.message };
28309
28831
  } catch (error) {
@@ -28365,35 +28887,55 @@ function createChatSession({
28365
28887
  clearLocalMembershipMessage(chatId, cid);
28366
28888
  };
28367
28889
  state.promise = (async () => {
28368
- const releaseMutation = await mutationGate.acquireExclusive(chatId);
28369
28890
  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);
28891
+ while (true) {
28892
+ const releaseMutation = await mutationGate.acquireExclusive(chatId);
28893
+ let attemptedEpochVersion = 0;
28894
+ try {
28895
+ const chat = requireTransitionChat(chatId);
28896
+ attemptedEpochVersion = chat.epochVersion;
28897
+ const members = prepareMembers ? await prepareMembers(chat) : resolveMembers(chat);
28898
+ if (!members) {
28899
+ restoreAuthoritativeChat();
28900
+ return { entry: chat.ownEntry, unchanged: true };
28901
+ }
28902
+ if (!members.length) {
28903
+ restoreAuthoritativeChat();
28904
+ await deleteActions.deleteChat(chat);
28905
+ return { deleted: true };
28906
+ }
28907
+ const result = await transitionChatEpoch(cloud, transitionIdentity(), chat.ownEntry, { members, cid, mls });
28908
+ if (result.entry) {
28909
+ const projected = projectOwnChatEntry(result.entry, chat.entryId, chatPK, chat.ts);
28910
+ chatListOwner.commitServerChats(sortedChats([projected], lastServerChatsRef.current));
28911
+ } else {
28912
+ chatListOwner.commitServerChats(lastServerChatsRef.current.filter((chatItem) => chatItem?.id !== chatId), { warm: false });
28913
+ setSelectedChat((current) => current === chatId ? null : current);
28914
+ }
28915
+ if (optimisticRoster && result.message) {
28916
+ stageCommittedMembershipMessage(chat, result.message);
28917
+ } else if (optimisticRoster) {
28918
+ clearLocalMembershipMessage(chatId, cid);
28919
+ }
28920
+ if (optimisticRoster)
28921
+ setPendingChatMembers(chatId, null);
28922
+ return result;
28923
+ } catch (error) {
28924
+ releaseMutation();
28925
+ if (!attemptedEpochVersion || !isChatEpochConflict(error))
28926
+ throw error;
28927
+ await chatListOwner.reconcileChatEpoch(chatId);
28928
+ const currentEpochVersion = getChatEntry(chatId)?.epochVersion || 0;
28929
+ if (currentEpochVersion <= attemptedEpochVersion)
28930
+ throw error;
28931
+ markDiag(diag, "chat.membership.epoch.advance", {
28932
+ fromEpochVersion: attemptedEpochVersion,
28933
+ toEpochVersion: currentEpochVersion
28934
+ });
28935
+ } finally {
28936
+ releaseMutation();
28937
+ }
28393
28938
  }
28394
- if (optimisticRoster)
28395
- setPendingChatMembers(chatId, null);
28396
- return result;
28397
28939
  } catch (error) {
28398
28940
  restoreAuthoritativeChat();
28399
28941
  throw error;
@@ -28401,7 +28943,6 @@ function createChatSession({
28401
28943
  if (membershipUpdateStatesRef.current.get(chatId) === state) {
28402
28944
  membershipUpdateStatesRef.current.delete(chatId);
28403
28945
  }
28404
- releaseMutation();
28405
28946
  }
28406
28947
  })();
28407
28948
  membershipUpdateStatesRef.current.set(chatId, state);
@@ -28524,6 +29065,26 @@ function createChatSession({
28524
29065
  }
28525
29066
  return state.promise;
28526
29067
  };
29068
+ const updateChatAvatar = async (chatId, data) => {
29069
+ assertChatAvatarBytes(data);
29070
+ const chat = requireTransitionChat(chatId);
29071
+ if (chat.lineage !== "group")
29072
+ throw new Error("group chat required");
29073
+ const epochId = chat?.epochState?.manifest?.epochId;
29074
+ if (!epochId)
29075
+ throw new Error("current chat epoch unavailable");
29076
+ const uploadChatMedia = cloud?.chat?.media?.upload;
29077
+ if (typeof uploadChatMedia !== "function")
29078
+ throw new Error("chat media upload unavailable");
29079
+ const cid = makeCid();
29080
+ const file = typeof media?.uploadChatAvatar === "function" ? await media.uploadChatAvatar({ cid, data, epochId, uploadChatMedia }) : await putChatFile(epochId, cid, data, {
29081
+ uploadChatMedia,
29082
+ cacheControl: "private, max-age=31536000, immutable, no-transform"
29083
+ });
29084
+ const avatarRef = makeChatAvatarRef(file);
29085
+ await updateChatSettings2(chatId, { avatarRef });
29086
+ return avatarRef;
29087
+ };
28527
29088
  const adoptCommittedSettingsMessage = ({ chatId, message }) => {
28528
29089
  if (!chatId || !message?.cid)
28529
29090
  return;
@@ -28616,11 +29177,12 @@ function createChatSession({
28616
29177
  const dropChat = (...args) => deleteActions.dropChat(...args);
28617
29178
  const dropUnavailableChat = (...args) => deleteActions.dropUnavailableChat(...args);
28618
29179
  const deleteChat = (...args) => deleteActions.deleteChat(...args);
28619
- const retireChats = async (affected) => {
29180
+ const purgePeerChats = async (affected) => {
28620
29181
  for (const chat of affected) {
28621
29182
  const retirement = planChatRetirement(chat, chatPK);
28622
29183
  if (retirement.action === CHAT_RETIREMENT_ACTIONS.LEAVE) {
28623
29184
  await leaveOwnedChat(chat, { optimistic: false });
29185
+ await cloud.user.chats.unlink(uid, chat.entryId);
28624
29186
  } else {
28625
29187
  await deleteChat(chat, { cleanup: false });
28626
29188
  }
@@ -28630,7 +29192,7 @@ function createChatSession({
28630
29192
  const retirePeerChat = async (peerChatPK, options = {}) => {
28631
29193
  const allChats = await loadAllChats(cloud, uid, chatPK, chatPrivateKey);
28632
29194
  const affected = allChats.filter((chat) => chat?.members?.some((member) => member.chatPK === peerChatPK));
28633
- await retireChats(affected);
29195
+ await purgePeerChats(affected);
28634
29196
  await options.onDeliveryRevoked?.();
28635
29197
  return affected.length > 0;
28636
29198
  };
@@ -28639,14 +29201,21 @@ function createChatSession({
28639
29201
  return 0;
28640
29202
  const allChats = await loadAllChats(cloud, uid, chatPK, chatPrivateKey);
28641
29203
  const affected = allChats.filter((chat) => chat?.members?.some((member) => member.chatPK !== chatPK && blockedUids.has(member.uid)));
28642
- return retireChats(affected);
29204
+ return purgePeerChats(affected);
28643
29205
  };
28644
29206
  const restoreDeletedChat = (...args) => deleteActions.restoreDeletedChat(...args);
28645
29207
  const wasChatDeletedLocally = (...args) => deleteActions.wasChatDeletedLocally(...args);
28646
29208
  const ackDeletedChat = (...args) => deleteActions.ackDeletedChat(...args);
28647
29209
  const markChatReadState = (...args) => seenActions.markChatReadState(...args);
28648
29210
  const markChatRead = (...args) => seenActions.markChatRead(...args);
28649
- const enterChatActivity = (...args) => liveActions.enterChatActivity(...args);
29211
+ const enterChatActivity = (chatId) => {
29212
+ const reconcileStartedAt = Date.now();
29213
+ chatListOwner.reconcileChatEpoch(chatId).catch((error) => {
29214
+ markError(diag, "chat.epoch.reconcile", reconcileStartedAt, error);
29215
+ });
29216
+ return liveActions.enterChatActivity(chatId);
29217
+ };
29218
+ const flushChatReadFrontier = (chatId) => liveActions.flushChatReadFrontier(chatId);
28650
29219
  const leaveChatActivity = (chatId) => {
28651
29220
  const flush = seenActions.flushChatRead(chatId);
28652
29221
  liveActions.leaveChatActivity(chatId);
@@ -28893,6 +29462,7 @@ function createChatSession({
28893
29462
  listActionsRef: deleteListActionsRef,
28894
29463
  closeMessageBatchRef,
28895
29464
  mutationGate,
29465
+ reconcileEpoch: (chatId) => chatListOwner?.reconcileChatEpoch(chatId),
28896
29466
  diag
28897
29467
  });
28898
29468
  const createActionOwners = () => {
@@ -28972,6 +29542,7 @@ function createChatSession({
28972
29542
  adoptLocalMessageMedia,
28973
29543
  readMessageFile,
28974
29544
  mutationGate,
29545
+ reconcileEpoch: (chatId) => chatListOwner.reconcileChatEpoch(chatId),
28975
29546
  diag
28976
29547
  });
28977
29548
  reactionActions = createChatReaction({
@@ -29052,6 +29623,7 @@ function createChatSession({
29052
29623
  localByChatRef,
29053
29624
  pendingDeleteIdsRef: deleteActions.pendingDeleteIdsRef,
29054
29625
  deletedChatIdsRef: deleteActions.deletedChatIdsRef,
29626
+ removedChatVersionsRef: deleteActions.removedChatVersionsRef,
29055
29627
  keepSelectedDeletedChatIdsRef: deleteActions.keepSelectedDeletedChatIdsRef,
29056
29628
  readCacheRef,
29057
29629
  lastServerChatsRef,
@@ -29067,8 +29639,9 @@ function createChatSession({
29067
29639
  reconcileMessageBatches: (...args) => messageBatchOwner.reconcileChats(...args),
29068
29640
  warmChats: (...args) => messageBatchOwner.warm(...args),
29069
29641
  onTransition: stageCommittedMembershipMessage,
29070
- onRemoved: (removedChatId) => deleteActions.dropRemovedChat(removedChatId),
29071
- wasRemoved: (removedChatId) => deleteActions.deletedChatIdsRef.current.has(removedChatId),
29642
+ onRemoved: (removedChatId, epochVersion) => deleteActions.dropRemovedChat(removedChatId, epochVersion),
29643
+ wasRemoved: (removedChatId) => deleteActions.removedChatVersionsRef.current.has(removedChatId),
29644
+ restoreRemovedChat: (chat) => deleteActions.restoreRemovedChat(chat),
29072
29645
  resetExternalChatState,
29073
29646
  mutationGate,
29074
29647
  onMlsKeyPackageConsumed: markMlsKeyPackageConsumed
@@ -29209,12 +29782,13 @@ function createChatSession({
29209
29782
  var DEFAULT_READ_WRITE_INTERVAL_MS, DIAG_KEY_LEN = 12, EMPTY_LOCAL_MESSAGES;
29210
29783
  var init_session3 = __esm(() => {
29211
29784
  init_config();
29785
+ init_files();
29786
+ init_avatar2();
29212
29787
  init_ids();
29213
29788
  init_direct();
29214
29789
  init_create();
29215
29790
  init_membership();
29216
29791
  init_pool();
29217
- init_owner2();
29218
29792
  init_manifest();
29219
29793
  init_notifications();
29220
29794
  init_state2();
@@ -38088,7 +38662,8 @@ var init_links = __esm(() => {
38088
38662
  ])
38089
38663
  ]);
38090
38664
  storageCorsOrigins = Object.freeze([
38091
- origins.veyl
38665
+ origins.veyl,
38666
+ origins.veylDevWeb
38092
38667
  ]);
38093
38668
  devStorageCorsOrigins = Object.freeze([
38094
38669
  origins.veylDev,
@@ -38104,12 +38679,19 @@ var init_links = __esm(() => {
38104
38679
  });
38105
38680
 
38106
38681
  // ../../product/passkeylabel.js
38682
+ function twoWordLabelAt(index) {
38683
+ if (!Number.isSafeInteger(index) || index < 0) {
38684
+ throw new Error("two-word label index must be a non-negative integer");
38685
+ }
38686
+ const normalized = index % TWO_WORD_LABEL_COUNT;
38687
+ const adjective = adjectives[normalized % adjectives.length];
38688
+ const noun = nouns[Math.floor(normalized / adjectives.length)];
38689
+ return `${adjective} ${noun}`;
38690
+ }
38107
38691
  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}`;
38692
+ return twoWordLabelAt(Math.floor(Math.random() * TWO_WORD_LABEL_COUNT));
38111
38693
  }
38112
- var adjectives, nouns;
38694
+ var adjectives, nouns, TWO_WORD_LABEL_COUNT;
38113
38695
  var init_passkeylabel = __esm(() => {
38114
38696
  adjectives = [
38115
38697
  "Ancient",
@@ -38176,7 +38758,6 @@ var init_passkeylabel = __esm(() => {
38176
38758
  "Grim",
38177
38759
  "Holy",
38178
38760
  "Infernal",
38179
- "Ivory",
38180
38761
  "Jade",
38181
38762
  "Light",
38182
38763
  "Midnight",
@@ -38320,6 +38901,7 @@ var init_passkeylabel = __esm(() => {
38320
38901
  "Wizard",
38321
38902
  "Wyvern"
38322
38903
  ];
38904
+ TWO_WORD_LABEL_COUNT = adjectives.length * nouns.length;
38323
38905
  });
38324
38906
 
38325
38907
  // src/auth.js
@@ -38685,6 +39267,7 @@ function simplifyAccountChat(chat, profiles = null) {
38685
39267
  members,
38686
39268
  title: chat.settings?.title || null,
38687
39269
  avatarRef: chat.settings?.avatarRef || null,
39270
+ retention: chat.settings?.retention || null,
38688
39271
  peerChatPK: chat.peerChatPK,
38689
39272
  peerUid: chat.peerUid || directPeer?.uid || null,
38690
39273
  username: directPeer?.username || null,
@@ -38937,6 +39520,8 @@ async function listenAccount(client, options = {}) {
38937
39520
  const includeChats = options.chats !== false;
38938
39521
  const includeTransactions = options.transactions !== false;
38939
39522
  const markRead = options.read !== false;
39523
+ const relayReads = markRead && options.relayReads === true;
39524
+ const readLiveIdleMs = Number.isFinite(Number(options.readLiveIdleMs)) ? Math.max(0, Math.floor(Number(options.readLiveIdleMs))) : 15000;
38940
39525
  const replay = options.replay === true;
38941
39526
  const persistentChatIdleMs = Number.isFinite(Number(options.persistentChatIdleMs)) ? Math.max(0, Math.floor(Number(options.persistentChatIdleMs))) : 0;
38942
39527
  const emit2 = typeof options.onEvent === "function" ? options.onEvent : () => {};
@@ -38947,8 +39532,12 @@ async function listenAccount(client, options = {}) {
38947
39532
  const chatActivityTimers = new Map;
38948
39533
  const subscribedWakeVersions = new Map;
38949
39534
  const idleWakeVersions = new Map;
39535
+ const messageBatchTasks = new Map;
38950
39536
  const pendingChats = new Map;
38951
39537
  const processedWakeVersions = new Map;
39538
+ const readLiveChats = new Set;
39539
+ const readLiveEntering = new Map;
39540
+ const readLiveTimers = new Map;
38952
39541
  const wantedWakeVersions = new Map;
38953
39542
  let activeChatIds = new Set;
38954
39543
  let chatSyncRequested = false;
@@ -38993,6 +39582,44 @@ async function listenAccount(client, options = {}) {
38993
39582
  const account = client.accountSummary();
38994
39583
  const runtime = client.runtime;
38995
39584
  const session = client.session;
39585
+ async function enterReadLive(chatId) {
39586
+ if (!relayReads)
39587
+ return false;
39588
+ if (readLiveChats.has(chatId))
39589
+ return true;
39590
+ const current = readLiveEntering.get(chatId);
39591
+ if (current)
39592
+ return current;
39593
+ const entering = runtime.chat.getSnapshot().enterChatActivity(chatId).then(async (entered) => {
39594
+ if (!entered)
39595
+ return false;
39596
+ if (closed) {
39597
+ await runtime.chat.getSnapshot().leaveChatActivity(chatId);
39598
+ return false;
39599
+ }
39600
+ readLiveChats.add(chatId);
39601
+ return true;
39602
+ }).finally(() => readLiveEntering.delete(chatId));
39603
+ readLiveEntering.set(chatId, entering);
39604
+ return entering;
39605
+ }
39606
+ function releaseReadLiveSoon(chatId) {
39607
+ if (!readLiveChats.has(chatId))
39608
+ return;
39609
+ const current = readLiveTimers.get(chatId);
39610
+ if (current)
39611
+ clearTimeout(current);
39612
+ const timer = setTimeout(() => {
39613
+ if (readLiveTimers.get(chatId) !== timer)
39614
+ return;
39615
+ readLiveTimers.delete(chatId);
39616
+ if (!readLiveChats.delete(chatId))
39617
+ return;
39618
+ runtime.chat.getSnapshot().leaveChatActivity(chatId);
39619
+ }, readLiveIdleMs);
39620
+ timer.unref?.();
39621
+ readLiveTimers.set(chatId, timer);
39622
+ }
38996
39623
  function projectedChat(chat) {
38997
39624
  const peerChatPK = getChatPeerPK(chat);
38998
39625
  const peerByChatPK = runtime.peers.getSnapshot().peerByChatPK;
@@ -39012,13 +39639,15 @@ async function listenAccount(client, options = {}) {
39012
39639
  const sourceMessages = batch?.sourceMessages?.length ? batch.sourceMessages : batch?.messages || [];
39013
39640
  return projectMessages(sourceMessages, session.chatPK, peerChatPK || session.chatPK, chat?.memberChatPKs).map((message) => simplifyAccountMessage(message, chat, session.chatPK, { raw: true }));
39014
39641
  }
39015
- function applyMessageBatch(chat, batch, emitChanges) {
39642
+ async function applyMessageBatch(chat, batch, emitChanges) {
39016
39643
  if (!batch?.ready)
39017
39644
  return;
39018
39645
  touchChatActivity(chat.id);
39019
39646
  const targetChat = projectedChat(chat);
39647
+ const rawMessages = batch?.sourceMessages?.length ? batch.sourceMessages : batch?.messages || [];
39020
39648
  const messages = projectMessages2(chat, batch);
39021
39649
  const currentKeys = new Set;
39650
+ const messageEvents = [];
39022
39651
  for (const message of messages) {
39023
39652
  const key = messageKey(message);
39024
39653
  if (!key)
@@ -39029,16 +39658,37 @@ async function listenAccount(client, options = {}) {
39029
39658
  seenMessages.set(key, version);
39030
39659
  if (!emitChanges || previous === version || incomingOnly && message.from !== "peer")
39031
39660
  continue;
39032
- emit2(formatMessageEvent(targetChat, message, { compact: compact2 }));
39033
- }
39034
- const latestPeer = [...messages].reverse().find((message) => message.from === "peer");
39035
- const latestPeerKey = latestPeer ? messageKey(latestPeer) : "";
39036
- if (markRead && latestPeerKey && readMessages.get(chat.id) !== latestPeerKey) {
39037
- readMessages.set(chat.id, latestPeerKey);
39038
- runtime.chat.getSnapshot().markChatReadState(chat.id, latestPeer.raw, {
39039
- messages: messages.map((message) => message.raw)
39040
- });
39661
+ messageEvents.push(formatMessageEvent(targetChat, message, { compact: compact2 }));
39662
+ }
39663
+ const latestPeer = [...rawMessages].reverse().find((message) => isPeerMsg(message, session.chatPK));
39664
+ const latestPeerKey = latestPeer ? messageKey({ ...latestPeer, chatId: chat.id }) : "";
39665
+ const latestPeerEvent = latestPeer ? formatMessageEvent(targetChat, simplifyAccountMessage(latestPeer, chat, session.chatPK, { raw: true }), { compact: compact2 }) : null;
39666
+ const readTarget = getLatestReadStateTarget(rawMessages);
39667
+ const readTargetKey = readTarget ? messageKey({ ...readTarget, chatId: chat.id }) : "";
39668
+ const alreadyProcessed = latestPeerEvent && options.isProcessedEvent?.(latestPeerEvent) === true;
39669
+ if (markRead && readTargetKey && !(alreadyProcessed && readTargetKey === latestPeerKey) && readMessages.get(chat.id) !== readTargetKey) {
39670
+ readMessages.set(chat.id, readTargetKey);
39671
+ if (relayReads) {
39672
+ try {
39673
+ await enterReadLive(chat.id);
39674
+ await runtime.chat.getSnapshot().markChatReadState(chat.id, readTarget, { messages: rawMessages });
39675
+ await runtime.chat.getSnapshot().flushChatReadFrontier?.(chat.id);
39676
+ } catch (error) {
39677
+ emit2(event("error", {
39678
+ peer: peerTarget(targetChat),
39679
+ message: error?.message || String(error)
39680
+ }));
39681
+ } finally {
39682
+ releaseReadLiveSoon(chat.id);
39683
+ }
39684
+ } else {
39685
+ runtime.chat.getSnapshot().markChatReadState(chat.id, readTarget, {
39686
+ messages: rawMessages
39687
+ });
39688
+ }
39041
39689
  }
39690
+ for (const messageEvent of messageEvents)
39691
+ emit2(messageEvent);
39042
39692
  const previousForChat = [...seenMessages.keys()].filter((key) => key.startsWith(`${chat.id}:`));
39043
39693
  for (const key of previousForChat) {
39044
39694
  if (currentKeys.has(key))
@@ -39057,6 +39707,21 @@ async function listenAccount(client, options = {}) {
39057
39707
  }
39058
39708
  }
39059
39709
  }
39710
+ function queueMessageBatch(chat, batch, emitChanges) {
39711
+ const previous = messageBatchTasks.get(chat.id) || Promise.resolve();
39712
+ const task = previous.catch(() => {}).then(() => applyMessageBatch(chat, batch, emitChanges));
39713
+ messageBatchTasks.set(chat.id, task);
39714
+ task.then(() => {
39715
+ if (messageBatchTasks.get(chat.id) === task) {
39716
+ messageBatchTasks.delete(chat.id);
39717
+ }
39718
+ }, () => {
39719
+ if (messageBatchTasks.get(chat.id) === task) {
39720
+ messageBatchTasks.delete(chat.id);
39721
+ }
39722
+ });
39723
+ return task;
39724
+ }
39060
39725
  function checkpointBatch(chat, page) {
39061
39726
  const sourceMessages = page?.history?.sourceMessages?.length ? page.history.sourceMessages : page?.batch?.sourceMessages?.length ? page.batch.sourceMessages : page?.batch?.messages || [];
39062
39727
  const batch = {
@@ -39143,13 +39808,15 @@ async function listenAccount(client, options = {}) {
39143
39808
  if (closed || !activeChatIds.has(chat.id) || !page.chat || !transientChats && chatSubscriptions.has(chat.id)) {
39144
39809
  return;
39145
39810
  }
39146
- applyMessageBatch(chat, page.batch, emitInitial);
39811
+ await queueMessageBatch(chat, page.batch, emitInitial);
39147
39812
  if (transientChats) {
39148
39813
  processedWakeVersions.set(chat.id, wakeVersion);
39149
39814
  return;
39150
39815
  }
39151
39816
  const batches = runtime.chat.getSnapshot().messageBatches;
39152
- const unsubscribe = batches.subscribeMessageBatch(chat.id, (batch) => applyMessageBatch(chat, batch, ready));
39817
+ const unsubscribe = batches.subscribeMessageBatch(chat.id, (batch) => {
39818
+ queueMessageBatch(chat, batch, ready).catch((error) => emit2(event("error", { peer: peerTarget(targetChat), message: error?.message || String(error) })));
39819
+ });
39153
39820
  chatSubscriptions.set(chat.id, () => {
39154
39821
  unsubscribe();
39155
39822
  page.release?.();
@@ -39283,6 +39950,13 @@ async function listenAccount(client, options = {}) {
39283
39950
  for (const timer of chatActivityTimers.values())
39284
39951
  clearTimeout(timer);
39285
39952
  chatActivityTimers.clear();
39953
+ for (const timer of readLiveTimers.values())
39954
+ clearTimeout(timer);
39955
+ readLiveTimers.clear();
39956
+ await Promise.allSettled(messageBatchTasks.values());
39957
+ await Promise.allSettled(readLiveEntering.values());
39958
+ await Promise.allSettled([...readLiveChats].map((chatId) => runtime.chat.getSnapshot().leaveChatActivity(chatId)));
39959
+ readLiveChats.clear();
39286
39960
  processedWakeVersions.clear();
39287
39961
  wantedWakeVersions.clear();
39288
39962
  }
@@ -39291,6 +39965,7 @@ var DEFAULT_CHAT_COUNT = 20, DEFAULT_MESSAGE_COUNT = 10, DEFAULT_REPLAY_MESSAGE_
39291
39965
  var init_listen = __esm(() => {
39292
39966
  init_actions2();
39293
39967
  init_ids();
39968
+ init_messages();
39294
39969
  init_presentation();
39295
39970
  init_messages3();
39296
39971
  });
@@ -41150,6 +41825,7 @@ function mergeLatestServerPages(primary, secondary) {
41150
41825
  return {
41151
41826
  ...secondary || {},
41152
41827
  ...primary || {},
41828
+ memberStatesByEpoch: mergeChatReceiptCheckpointsByEpoch(secondary?.memberStatesByEpoch, primary?.memberStatesByEpoch),
41153
41829
  expiredKeys: keySet([...primary?.expiredKeys || [], ...secondary?.expiredKeys || []]),
41154
41830
  deletedKeys: keySet([...primary?.deletedKeys || [], ...secondary?.deletedKeys || []])
41155
41831
  };
@@ -41297,6 +41973,7 @@ var init_screen = __esm(() => {
41297
41973
  init_messagekeys();
41298
41974
  init_ids();
41299
41975
  init_state();
41976
+ init_receipt();
41300
41977
  init_time();
41301
41978
  init_control();
41302
41979
  init_window();
@@ -41698,10 +42375,7 @@ var init_resolve = __esm(() => {
41698
42375
 
41699
42376
  // ../../core/chat/messages/serversync.js
41700
42377
  function mergeMemberStates(previous, current) {
41701
- return new Map([
41702
- ...previous instanceof Map ? previous : [],
41703
- ...current instanceof Map ? current : []
41704
- ]);
42378
+ return mergeChatReceiptCheckpointsByEpoch(previous, current);
41705
42379
  }
41706
42380
  function isEpochBatchCutover(latestServerPage, msgBatch) {
41707
42381
  return !!(latestServerPage?.epochId && msgBatch?.epochId && latestServerPage.epochId !== msgBatch.epochId);
@@ -41925,6 +42599,7 @@ var init_serversync = __esm(() => {
41925
42599
  init_messagekeys();
41926
42600
  init_messages();
41927
42601
  init_state();
42602
+ init_receipt();
41928
42603
  init_resolve();
41929
42604
  init_history();
41930
42605
  init_window();
@@ -42353,6 +43028,7 @@ function conversationStateSignature(state) {
42353
43028
  cacheMessageListSignature(state.latestMessages),
42354
43029
  cacheKeySignature(state.latestServerPage?.expiredKeys),
42355
43030
  cacheKeySignature(state.latestServerPage?.deletedKeys),
43031
+ cachedReceiptCheckpointSignature(state.latestServerPage?.memberStatesByEpoch),
42356
43032
  reactionControlCacheSignature(state)
42357
43033
  ].join(`
42358
43034
  `);
@@ -44192,6 +44868,7 @@ function deriveMessageRouteProjection({
44192
44868
  routeActive: routeRetained
44193
44869
  }));
44194
44870
  const { rawMessages, messages, visibleMessages } = routeMessages;
44871
+ const memberStatesByEpoch = activeState.latestServerPage?.memberStatesByEpoch || EMPTY_MEMBER_STATES_BY_EPOCH;
44195
44872
  return {
44196
44873
  activeExists: activeState.exists,
44197
44874
  activeHasOlder: activeState.hasOlder,
@@ -44200,7 +44877,7 @@ function deriveMessageRouteProjection({
44200
44877
  activeLatestMessages: activeState.latestMessages,
44201
44878
  activeLatestServerPage: activeState.latestServerPage,
44202
44879
  activeReady: activeState.ready,
44203
- memberStatesByEpoch: activeState.latestServerPage?.memberStatesByEpoch || new Map,
44880
+ memberStatesByEpoch,
44204
44881
  leaveState: {
44205
44882
  chatId,
44206
44883
  exists: activeState.exists,
@@ -44211,7 +44888,7 @@ function deriveMessageRouteProjection({
44211
44888
  selfChatPublicKey,
44212
44889
  peerChatPublicKey,
44213
44890
  memberChatPKs,
44214
- memberStatesByEpoch: activeState.latestServerPage?.memberStatesByEpoch || new Map,
44891
+ memberStatesByEpoch,
44215
44892
  conversationKeys,
44216
44893
  routeMemory,
44217
44894
  keepKeys: getRouteKeepKeys(refs.visibleMessageKeys.current, refs.heldMessages.current),
@@ -44317,7 +44994,7 @@ function recordRouteProjectionDiagnostics({
44317
44994
  markDiag(diag, "chat.messages.route.visible", visibleData);
44318
44995
  }
44319
44996
  }
44320
- var PASSIVE_OLDER_OVERFLOW_PAGES = 3, ROUTE_VISIBLE_DETAIL_SCAN_LIMIT = 160, DIAG_KEY_LEN2 = 12;
44997
+ var PASSIVE_OLDER_OVERFLOW_PAGES = 3, ROUTE_VISIBLE_DETAIL_SCAN_LIMIT = 160, DIAG_KEY_LEN2 = 12, EMPTY_MEMBER_STATES_BY_EPOCH;
44321
44998
  var init_routeprojection = __esm(() => {
44322
44999
  init_messagekeys();
44323
45000
  init_state();
@@ -44325,6 +45002,7 @@ var init_routeprojection = __esm(() => {
44325
45002
  init_history();
44326
45003
  init_payloadlog();
44327
45004
  init_window();
45005
+ EMPTY_MEMBER_STATES_BY_EPOCH = new Map;
44328
45006
  });
44329
45007
 
44330
45008
  // ../../core/chat/messages/expirycleanup.js
@@ -45350,7 +46028,7 @@ function createMessageRouteSession({
45350
46028
  return;
45351
46029
  const localTarget = getLatestLocalReadTarget(viewedMessages, current.selfChatPublicKey);
45352
46030
  const localTargetKey = messageKey2(localTarget);
45353
- const target = getLatestReadStateTarget(viewedMessages);
46031
+ const target = getLatestReadStateTarget(current.rawMessages?.length ? current.rawMessages : viewedMessages);
45354
46032
  const targetKey = messageKey2(target);
45355
46033
  const localChanged = !!localTargetKey && localTargetKey !== refs.lastSelfReadTarget.current;
45356
46034
  const stateChanged = !!targetKey && targetKey !== refs.lastSharedReadTarget.current;
@@ -46707,6 +47385,9 @@ function positiveLimit2(value, fallback = 30) {
46707
47385
  const limit = Math.floor(Number(value) || fallback);
46708
47386
  return Math.max(1, Math.min(limit, 500));
46709
47387
  }
47388
+ function messageIsPermanent(message) {
47389
+ return message?.permanent === true || Object.prototype.hasOwnProperty.call(message || {}, "ttl") && message.ttl == null;
47390
+ }
46710
47391
  function waitFor2(owner, predicate, timeoutMs = DEFAULT_TIMEOUT_MS4) {
46711
47392
  const current = owner.getSnapshot();
46712
47393
  if (predicate(current))
@@ -46874,7 +47555,7 @@ function createRuntimeChatActions({
46874
47555
  const profiles2 = await resolveTargetProfiles(peers, session, runtime);
46875
47556
  const settings = {
46876
47557
  title: cleanText(options.title) || null,
46877
- avatarRef: cleanText(options.avatarRef) || null,
47558
+ avatarRef: null,
46878
47559
  retention: cleanText(options.retention) || "seen"
46879
47560
  };
46880
47561
  const initialText = typeof options.initialMessage === "string" ? options.initialMessage : "";
@@ -46904,11 +47585,25 @@ function createRuntimeChatActions({
46904
47585
  return { left: result?.left === true || result?.deleted === true, chatId: chat.id };
46905
47586
  }
46906
47587
  async function updateSettings(chatId, settings = {}) {
47588
+ if (Object.prototype.hasOwnProperty.call(settings || {}, "avatarRef")) {
47589
+ throw new Error("use chat.updateAvatar for chat pictures");
47590
+ }
46907
47591
  const { runtime, chat } = await chatForId(chatId);
46908
47592
  await runtime.chat.getSnapshot().updateChatSettings(chat.id, settings);
46909
47593
  const next = await chatForId(chat.id);
46910
47594
  return summarizeChat(runtime, next.chat);
46911
47595
  }
47596
+ async function updateAvatar(chatId, data = null) {
47597
+ const { runtime, chat } = await chatForId(chatId);
47598
+ const snapshot2 = runtime.chat.getSnapshot();
47599
+ if (data == null) {
47600
+ await snapshot2.updateChatSettings(chat.id, { avatarRef: null });
47601
+ } else {
47602
+ await snapshot2.updateChatAvatar(chat.id, data);
47603
+ }
47604
+ const next = await chatForId(chat.id);
47605
+ return summarizeChat(runtime, next.chat);
47606
+ }
46912
47607
  async function enterById(chatId, options = {}) {
46913
47608
  const session = await getSession();
46914
47609
  const { runtime, chat, profile } = await chatForId(chatId);
@@ -46935,6 +47630,31 @@ function createRuntimeChatActions({
46935
47630
  const route = enteredChats.get(id);
46936
47631
  return { exited: route?.leave() === true, chatId: id || null };
46937
47632
  }
47633
+ async function enterLive(chatId) {
47634
+ const { runtime, chat } = await chatForId(chatId);
47635
+ const entered2 = await runtime.chat.getSnapshot().enterChatActivity(chat.id);
47636
+ return { entered: entered2 === true, chatId: chat.id };
47637
+ }
47638
+ async function leaveLive(chatId) {
47639
+ const id = cleanText(chatId).toLowerCase();
47640
+ if (!/^[0-9a-f]{64}$/u.test(id))
47641
+ throw new Error("chat id required");
47642
+ const runtime = await getRuntime();
47643
+ await runtime.chat.getSnapshot().leaveChatActivity(id);
47644
+ return { left: true, chatId: id };
47645
+ }
47646
+ async function markTyping(chatId, typing = true, compositionId = "") {
47647
+ const { runtime, chat } = await chatForId(chatId);
47648
+ const nextTyping = typing === true;
47649
+ const nextCompositionId = cleanText(compositionId).toLowerCase() || (nextTyping ? makeChatCompositionId() : "");
47650
+ const marked = runtime.chat.getSnapshot().markChatTyping(chat.id, nextTyping, nextCompositionId);
47651
+ return {
47652
+ marked: marked === true,
47653
+ typing: nextTyping,
47654
+ compositionId: nextCompositionId,
47655
+ chatId: chat.id
47656
+ };
47657
+ }
46938
47658
  async function readById(chatId, options = {}) {
46939
47659
  const window2 = await messageWindowById(chatId, options);
46940
47660
  try {
@@ -47079,6 +47799,13 @@ function createRuntimeChatActions({
47079
47799
  }
47080
47800
  async function saveInChat(chatId, messageId, saved = true, options = {}) {
47081
47801
  const target = await messageForChat(chatId, messageId, options);
47802
+ if (saved === messageIsPermanent(target.message)) {
47803
+ return {
47804
+ chat: summarizeChat(target.runtime, target.chat),
47805
+ saved,
47806
+ updated: 0
47807
+ };
47808
+ }
47082
47809
  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
47810
  if (!result?.updated)
47084
47811
  throw new Error("message no longer available");
@@ -47573,10 +48300,14 @@ function createRuntimeChatActions({
47573
48300
  addMembers,
47574
48301
  kickMember,
47575
48302
  leaveMembership,
48303
+ updateAvatar,
47576
48304
  updateSettings,
47577
48305
  readById,
47578
48306
  enterById,
47579
48307
  exitById,
48308
+ enterLive,
48309
+ leaveLive,
48310
+ markTyping,
47580
48311
  sendToChat,
47581
48312
  sendAttachmentToChat,
47582
48313
  readAttachmentInChat,
@@ -47996,6 +48727,7 @@ var DEFAULT_TIMEOUT_MS4 = 15000, HISTORY_LOAD_ATTEMPTS = 4, messageWindowSequenc
47996
48727
  var init_runtime_actions = __esm(() => {
47997
48728
  init_messages();
47998
48729
  init_ids();
48730
+ init_live();
47999
48731
  init_state();
48000
48732
  init_actions2();
48001
48733
  init_fees();
@@ -48013,7 +48745,7 @@ var package_default;
48013
48745
  var init_package2 = __esm(() => {
48014
48746
  package_default = {
48015
48747
  name: "veyl",
48016
- version: "0.59.1",
48748
+ version: "0.61.0",
48017
48749
  private: true,
48018
48750
  license: "UNLICENSED",
48019
48751
  workspaces: {
@@ -48342,10 +49074,14 @@ class AccountRuntime {
48342
49074
  addMembers: (chatId, peers) => this.runtimeChat.addMembers(chatId, peers),
48343
49075
  kickMember: (chatId, memberChatPK) => this.runtimeChat.kickMember(chatId, memberChatPK),
48344
49076
  leaveMembership: (chatId) => this.runtimeChat.leaveMembership(chatId),
49077
+ updateAvatar: (chatId, data) => this.runtimeChat.updateAvatar(chatId, data),
48345
49078
  updateSettings: (chatId, payload) => this.runtimeChat.updateSettings(chatId, payload),
48346
49079
  readById: (chatId, payload) => this.runtimeChat.readById(chatId, payload),
48347
49080
  enterById: (chatId, payload) => this.runtimeChat.enterById(chatId, payload),
48348
49081
  exitById: (chatId) => this.runtimeChat.exitById(chatId),
49082
+ enterLive: (chatId) => this.runtimeChat.enterLive(chatId),
49083
+ leaveLive: (chatId) => this.runtimeChat.leaveLive(chatId),
49084
+ markTyping: (chatId, typing, compositionId) => this.runtimeChat.markTyping(chatId, typing, compositionId),
48349
49085
  sendTo: (chatId, message, payload) => this.runtimeChat.sendToChat(chatId, message, payload),
48350
49086
  replyIn: (chatId, messageId, message, payload) => this.runtimeChat.replyInChat(chatId, messageId, message, payload),
48351
49087
  reactIn: (chatId, messageId, emoji, payload) => this.runtimeChat.reactInChat(chatId, messageId, emoji, payload),
@@ -48807,7 +49543,9 @@ class AccountRuntime {
48807
49543
  }
48808
49544
  const vault = await this.cloud.user.vault.read(profile.uid);
48809
49545
  if (!vault) {
48810
- throw new Error("vault not found");
49546
+ throw Object.assign(new Error("vault not found"), {
49547
+ code: "not-found"
49548
+ });
48811
49549
  }
48812
49550
  return {
48813
49551
  mnemonic: await this.ports.vaultCrypto.decryptWalletMnemonic(vault, vaultSecret),
@@ -48825,7 +49563,9 @@ class AccountRuntime {
48825
49563
  }
48826
49564
  const vault = await this.cloud.user.vault.read(profile.uid);
48827
49565
  if (!vault) {
48828
- throw new Error("vault not found");
49566
+ throw Object.assign(new Error("vault not found"), {
49567
+ code: "not-found"
49568
+ });
48829
49569
  }
48830
49570
  const session = await this.bootSession(vault, vaultSecret, profile);
48831
49571
  const next = await this.saveProfile({
@@ -181896,7 +182636,7 @@ function firebaseFunctionTarget(environment, name7) {
181896
182636
  if (!/^[A-Za-z][A-Za-z0-9]*$/u.test(target)) {
181897
182637
  throw new Error("Invalid Firebase function target");
181898
182638
  }
181899
- return `${resolved.name}-${target}`;
182639
+ return resolved.name === "dev" ? `dev-${target}` : target;
181900
182640
  }
181901
182641
  var firebaseConfig, firebaseEnvironments, defaultFirebaseEnvironment;
181902
182642
  var init_config3 = __esm(() => {
@@ -182865,14 +183605,14 @@ function createFirebaseCloud({ db, auth: auth2, getAuth: getAuth2, functions: fu
182865
183605
  const { entryId, expectedEpochId, stateCapability, cleanup = true } = options2 || {};
182866
183606
  if (!chatId || !expectedEpochId || !stateCapability)
182867
183607
  throw new Error("current chat state required");
182868
- await callFunction("deleteChat", {
183608
+ const result = await callFunction("deleteChat", {
182869
183609
  chatId,
182870
183610
  expectedEpochId,
182871
183611
  stateCapabilityWitness: stateCapabilityWitness(stateCapability),
182872
183612
  ...entryId ? { entryId } : {},
182873
183613
  cleanup: cleanup !== false
182874
183614
  });
182875
- return true;
183615
+ return { newlyDeleted: result?.newlyDeleted === true };
182876
183616
  }
182877
183617
  function cleanOpaqueState(value) {
182878
183618
  const chat = value?.chat || value || {};
@@ -183014,10 +183754,10 @@ function createFirebaseCloud({ db, auth: auth2, getAuth: getAuth2, functions: fu
183014
183754
  ]).catch(() => []);
183015
183755
  const [epochSnap, settingsSnap, packageSnap, ownerSnap, ownerMlsSnap] = snapshots;
183016
183756
  const outboxSnaps = snapshots.slice(5);
183017
- const currentEpoch = epochSnap?.data?.() || {};
183757
+ const currentEpoch2 = epochSnap?.data?.() || {};
183018
183758
  const currentSettings = settingsSnap?.data?.() || {};
183019
183759
  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) => {
183760
+ 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
183761
  const current = outboxSnaps[index]?.data?.() || {};
183022
183762
  return outboxSnaps[index]?.exists?.() && sameCloudBytes(current.body, outbox.data.body) && timestampMs(current.ttl, null) === timestampMs(outbox.data.ttl, null);
183023
183763
  });
@@ -183929,6 +184669,15 @@ function createFirebaseCloud({ db, auth: auth2, getAuth: getAuth2, functions: fu
183929
184669
  await callFunction("unlinkChatOwner", { entryId });
183930
184670
  return true;
183931
184671
  }
184672
+ async function unlinkDeletedUserChat(uid, entryId, chatId) {
184673
+ requireUid(uid);
184674
+ if (!entryId)
184675
+ throw new Error("chat entry id required");
184676
+ if (!chatId)
184677
+ throw new Error("chat id required");
184678
+ await callFunction("unlinkChatOwner", { entryId, deletedChatId: chatId });
184679
+ return true;
184680
+ }
183932
184681
  function userChatEpochCollection(uid, entryId) {
183933
184682
  requireUid(uid);
183934
184683
  if (!entryId)
@@ -184232,6 +184981,7 @@ function createFirebaseCloud({ db, auth: auth2, getAuth: getAuth2, functions: fu
184232
184981
  mutate: mutateUserChat,
184233
184982
  delete: deleteUserChat,
184234
184983
  unlink: unlinkUserChat,
184984
+ unlinkDeleted: unlinkDeletedUserChat,
184235
184985
  membershipOutbox: {
184236
184986
  list: listUserChatMembershipOutbox,
184237
184987
  delete: deleteUserChatMembershipOutbox
@@ -186600,6 +187350,7 @@ class FleetOwner {
186600
187350
  this.manifest = options2.manifest;
186601
187351
  this.seed = options2.seed;
186602
187352
  this.homeDir = options2.homeDir || defaultHomeDir();
187353
+ this.profileHomeDir = options2.profileHomeDir || this.homeDir;
186603
187354
  this.openClient = options2.openClient || createAccountRuntime;
186604
187355
  this.policies = [...options2.policies || []];
186605
187356
  this.policyLoader = options2.policyLoader || null;
@@ -186646,7 +187397,7 @@ class FleetOwner {
186646
187397
  try {
186647
187398
  return {
186648
187399
  ...await this.baseClientOptions(profile),
186649
- homeDir: this.homeDir,
187400
+ homeDir: this.profileHomeDir,
186650
187401
  activate: false,
186651
187402
  credential: material.credential,
186652
187403
  vaultKey: material.vaultSecret,
@@ -186782,6 +187533,36 @@ class FleetOwner {
186782
187533
  await this.lock.release();
186783
187534
  }
186784
187535
  }
187536
+ profileStore(profile) {
187537
+ return new ProfileStore({
187538
+ dir: this.profileHomeDir,
187539
+ profile,
187540
+ activate: false
187541
+ });
187542
+ }
187543
+ async hasLocalProfile(profile) {
187544
+ const name7 = cleanProfileName(profile, "");
187545
+ if (!name7)
187546
+ return false;
187547
+ return Boolean(await this.profileStore(name7).load(name7));
187548
+ }
187549
+ async ensureLocalProfile(record, credential) {
187550
+ const store = this.profileStore(record.profile);
187551
+ if (await store.load(record.profile))
187552
+ return store;
187553
+ await store.save({
187554
+ profile: record.profile,
187555
+ username: record.username,
187556
+ network: record.network,
187557
+ credentialId: credential.credentialId,
187558
+ publicKeyPem: credential.publicKeyPem,
187559
+ privateKeyPem: null,
187560
+ auth: "machine",
187561
+ hasVault: false,
187562
+ createdAt: Date.now()
187563
+ });
187564
+ return store;
187565
+ }
186785
187566
  provision(input = {}) {
186786
187567
  return this.mutate(async () => {
186787
187568
  if (this.fleet) {
@@ -186816,32 +187597,75 @@ class FleetOwner {
186816
187597
  });
186817
187598
  });
186818
187599
  }
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
187600
+ prepare(profile) {
187601
+ return this.mutate(async () => {
187602
+ if (this.fleet) {
187603
+ throw new Error("stop the fleet owner before preparing an account");
187604
+ }
187605
+ return this.withStoppedOwnerLock(async () => {
187606
+ const record = this.slot(profile);
187607
+ if (!record)
187608
+ throw new Error("fleet profile not found");
187609
+ if (record.state !== "ready") {
187610
+ throw new Error("fleet profile must be ready before preparation");
187611
+ }
187612
+ return this.finishProvision(record, {
187613
+ create: false,
187614
+ recoverMissing: false,
187615
+ saveManifest: false,
187616
+ runHook: false
187617
+ });
187618
+ });
186825
187619
  });
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()
187620
+ }
187621
+ retire(profile, options2 = {}) {
187622
+ return this.mutate(async () => {
187623
+ if (options2.confirm !== true) {
187624
+ throw new Error("fleet account retirement requires confirm: true");
187625
+ }
187626
+ if (this.fleet) {
187627
+ throw new Error("stop the fleet owner before retiring an account");
187628
+ }
187629
+ return this.withStoppedOwnerLock(async () => {
187630
+ const record = this.slot(profile);
187631
+ if (!record)
187632
+ throw new Error("fleet profile not found");
187633
+ if (record.state !== "ready") {
187634
+ throw new Error("fleet profile must be ready before retirement");
187635
+ }
187636
+ const clientOptions = await this.derivedClientOptions(record);
187637
+ await this.ensureLocalProfile(record, clientOptions.credential);
187638
+ const client2 = await this.openClient({
187639
+ ...clientOptions,
187640
+ profile: record.profile,
187641
+ network: record.network
187642
+ }, record);
187643
+ try {
187644
+ await client2.account.login({
187645
+ credential: clientOptions.credential
187646
+ });
187647
+ await client2.ensureUnlocked();
187648
+ const result = await client2.account.delete({ confirm: true });
187649
+ await this.saveProfiles(this.manifest.profiles.filter((item) => item.profile !== record.profile), this.manifest.nextAccountIndex);
187650
+ return {
187651
+ profile: record.profile,
187652
+ username: record.username,
187653
+ deleted: result?.deleted !== false
187654
+ };
187655
+ } finally {
187656
+ await client2.close();
187657
+ }
186838
187658
  });
186839
- }
187659
+ });
187660
+ }
187661
+ async finishProvision(record, options2) {
187662
+ const material = deriveFleetAccount(this.seed, record.accountIndex);
187663
+ await this.ensureLocalProfile(record, material.credential);
186840
187664
  const client2 = await this.openClient({
186841
187665
  ...await this.baseClientOptions(record),
186842
187666
  profile: record.profile,
186843
187667
  network: record.network,
186844
- homeDir: this.homeDir,
187668
+ homeDir: this.profileHomeDir,
186845
187669
  activate: false,
186846
187670
  credential: material.credential,
186847
187671
  vaultKey: material.vaultSecret,
@@ -186867,6 +187691,8 @@ class FleetOwner {
186867
187691
  } catch (error) {
186868
187692
  if (error?.code !== "not-found")
186869
187693
  throw error;
187694
+ if (options2.recoverMissing === false)
187695
+ throw error;
186870
187696
  await client2.account.create({
186871
187697
  username: record.username,
186872
187698
  network: record.network,
@@ -186881,23 +187707,38 @@ class FleetOwner {
186881
187707
  credential: material.credential
186882
187708
  });
186883
187709
  }
186884
- if (account.hasVault !== true) {
187710
+ try {
187711
+ await client2.vault.unlock({
187712
+ key: material.vaultSecret,
187713
+ saveKey: false
187714
+ });
187715
+ } catch (error) {
187716
+ if (error?.code !== "not-found")
187717
+ throw error;
187718
+ if (options2.recoverMissing === false)
187719
+ throw error;
186885
187720
  await client2.vault.create({
186886
187721
  key: material.vaultSecret,
186887
187722
  masterSeed: material.masterSeed,
186888
187723
  registry: material.registry,
186889
187724
  saveKey: false
186890
187725
  });
186891
- account = await client2.account.me();
186892
- } else {
186893
- await client2.ensureUnlocked();
186894
- account = await client2.account.me();
186895
187726
  }
187727
+ account = await client2.account.me();
186896
187728
  const ready2 = Object.freeze({
186897
187729
  ...record,
186898
187730
  state: "ready"
186899
187731
  });
186900
- await this.saveProfiles(this.manifest.profiles.map((item) => item.profile === ready2.profile ? ready2 : item), this.manifest.nextAccountIndex);
187732
+ if (options2.runHook !== false && typeof this.options.onProvisioned === "function") {
187733
+ await this.options.onProvisioned({
187734
+ account,
187735
+ client: client2,
187736
+ profile: ready2
187737
+ });
187738
+ }
187739
+ if (options2.saveManifest !== false) {
187740
+ await this.saveProfiles(this.manifest.profiles.map((item) => item.profile === ready2.profile ? ready2 : item), this.manifest.nextAccountIndex);
187741
+ }
186901
187742
  return {
186902
187743
  account,
186903
187744
  accountIndex: ready2.accountIndex,
@@ -187571,7 +188412,23 @@ var CLI_COMMANDS = Object.freeze([
187571
188412
  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
188413
  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
188414
  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) }),
188415
+ 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) }),
188416
+ command({
188417
+ name: "chat_avatar_set",
188418
+ group: "chat",
188419
+ apiName: "setAvatar",
188420
+ clientMethods: ["updateAvatar"],
188421
+ description: "Encrypt and set a prepared WebP group picture.",
188422
+ properties: { chatId: stringProp, dataBase64: stringProp },
188423
+ required: ["chatId", "dataBase64"],
188424
+ usage: "chat avatar-set chat-id <webp-path>",
188425
+ cliInput: (args, context) => ({ ...context.commandOptions, chatId: args[0], path: args[1] }),
188426
+ access: "write",
188427
+ invoke: (client2, input) => client2.chat.updateAvatar(input.chatId, assertWebp(avatarBytes(input))),
188428
+ invokeCli: async (client2, input) => client2.chat.updateAvatar(input.chatId, assertWebp(new Uint8Array(await readFile10(resolve8(String(input.path || "")))))),
188429
+ prepareDispatch: dispatchAvatar
188430
+ }),
188431
+ 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
188432
  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
188433
  command({
187577
188434
  name: "chat_enter_by_id",