@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/index.js CHANGED
@@ -6450,7 +6450,7 @@ var init_notifications = __esm(() => {
6450
6450
  });
6451
6451
 
6452
6452
  // ../../core/config.js
6453
- var MS_PER_SECOND = 1000, MINUTE_MS, HOUR_MS, DAY_MS, KIB_BYTES = 1024, MIB_BYTES, SATS_PER_BITCOIN = 100000000n, USERNAME_MAX_CHARS = 12, PASSWORD_MIN_CHARS = 15, PASSWORD_MAX_CHARS = 64, PASSWORD_MAX_BYTES, AUTOLOCK_MIN_MINUTES = 1, AUTOLOCK_MAX_MINUTES = 60, LOCAL_CACHE_WRITE_DELAY_MS = 350, LOCAL_MEDIA_CACHE_MAX_BYTES, LOCAL_MEDIA_CACHE_MAX_ITEMS = 1000, LOCAL_MEDIA_ACCESS_TOUCH_MIN_MS, LOCAL_PROFILE_CACHE_MAX_ITEMS = 500, LOCAL_PROFILE_CACHE_MAX_AGE_MS, LOCAL_CHAT_CACHE_MAX_ITEMS = 1000, LOCAL_CHAT_MESSAGE_CACHE_MAX_VISIBLE = 1000, LOCAL_CHAT_MESSAGE_CACHE_WRITE_DELAY_MS = 2500, LOCAL_AVATAR_CACHE_MAX_BYTES, LOCAL_AVATAR_CACHE_MAX_AGE_MS, AVATAR_IMAGE_MAX_BYTES, CHAT_AVATAR_IMAGE_MAX_BYTES, CHAT_AVATAR_REF_MAX_CHARS, CHAT_MESSAGE_FILE_CACHE_MAX_BYTES, IDLE_CALLBACK_MIN_TIMEOUT_MS = 50, ATTACHMENT_CACHE_IDLE_TIMEOUT_MS = 2500, ATTACHMENT_CACHE_FALLBACK_DELAY_MS = 250, CHAT_UNSAVED_TTL_DAYS = 21, CHAT_UNSAVED_TTL_MS, CHAT_AFTER_SEEN_MS, CHAT_MEDIA_TTL_DAYS, CHAT_MEDIA_TTL_MS, CHAT_UPLOAD_MAX_BYTES, CHAT_AUDIO_TRANSCODE_BITRATE_BPS = 128000, CHAT_VIDEO_TRANSCODE_VIDEO_BITRATE_BPS = 3000000, CHAT_VIDEO_TRANSCODE_AUDIO_BITRATE_BPS, CHAT_MESSAGE_BATCH_SIZE = 40, CHAT_MESSAGE_QUERY_MAX_DOCS = 120, CHAT_TTL_CLIENT_DELETE_GRACE_MS, CHAT_TTL_DELETE_CHUNK_SIZE = 40, CHAT_DELETE_SCAN_BATCH_SIZE = 200, CHAT_DELETE_WRITE_BATCH_SIZE = 400, CHAT_BATCH_CLEANUP_IDLE_TIMEOUT_MS = 1500, CHAT_BATCH_CLEANUP_IDLE_DELAY_MS = 250, CHAT_LIST_PAGE_SIZE = 20, CHAT_LIST_LIVE_COUNT, CHAT_INBOX_PING_PAGE_SIZE = 25, CHAT_INBOX_CURSOR_OVERLAP_MS, CHAT_LIST_CACHE_WRITE_DELAY_MS = 1500, CHAT_LIST_SNAPSHOT_COALESCE_MS = 80, CHAT_LIST_LISTENER_RETRY_MS = 1000, CHAT_TOP_WARM_COUNT = 0, CHAT_EAGER_WARM_COUNT = 0, CHAT_WARM_DELAY_MS = 3000, CHAT_WARM_BATCH_SIZE, CHAT_MESSAGE_VIEW_CACHE_SIZE = 30, CHAT_MEDIA_WARM_MESSAGES_PER_CHAT, CHAT_MEDIA_WARM_START_DELAY_MS = 600, CHAT_MEDIA_WARM_STEP_DELAY_MS = 120, CHAT_MEDIA_WARM_TYPES, CHAT_MEDIA_WARM_MAX_BYTES = 0, CHAT_READ_WRITE_INTERVAL_MS, CHAT_READ_WRITE_MAX_WAIT_MS, CHAT_LIVE_PING_INTERVAL_MS, CHAT_LIVE_SWEEP_INTERVAL_MS, CHAT_LIVE_STALE_AFTER_MS, CHAT_LIVE_RECONNECT_MAX_MS, CHAT_LIVE_TYPING_IDLE_MS, CHAT_LIVE_TYPING_RENEW_MS, CHAT_LIVE_COMPOSITION_HANDOFF_MS, CHAT_LIVE_READ_SEND_INTERVAL_MS = 120, CHAT_LIVE_READ_SEND_MAX_WAIT_MS = 500, CHAT_LIVE_READ_WRITE_INTERVAL_MS, CHAT_LIVE_READ_WRITE_MAX_WAIT_MS, CHAT_SEND_QUEUE_RATE_LIMIT_COUNT = 12, CHAT_SEND_QUEUE_RATE_LIMIT_WINDOW_MS, CHAT_SETTINGS_BODY_MAX_BYTES, CHAT_MANIFEST_MAX_BYTES, CHAT_RECEIPT_MAX_MEMBERS = 32, CHAT_ORDINARY_DELIVERY_MAX_MEMBERS = 128, CHAT_MAX_MEMBERS = 256, CHAT_MAX_TEXT_CHARS = 2048, CHAT_MAX_REACTIONS, SEARCH_DEBOUNCE_MS = 300, SEARCH_USERNAME_LIMIT = 15, SEARCH_ROLE_LIMIT = 15, RECENT_PEER_REFRESH_LIMIT = 50, RECENT_PEER_REFRESH_DELAY_MS = 250, RECENT_PEER_REFRESH_INTERVAL_MS, RECENT_PEER_REFRESH_THROTTLE_MS = 120, BAN_REFRESH_GRACE_MS = 50, REQUEST_MONEY_MAX_SATS, WALLET_TRANSFER_POLL_MS, WALLET_ACTIVE_TRANSFER_REFRESH_MS, WALLET_IDLE_ACTIVE_TRANSFER_REFRESH_MS, WALLET_VISIBLE_TRANSFER_DISCOVERY_WINDOW_MS, WALLET_ACTIVE_TRANSFER_EMPTY_SCAN_LIMIT = 8, WALLET_CACHE_HYDRATE_DELAY_MS = 0, WALLET_BOOT_REFRESH_DELAY_MS = 500, WALLET_BOOT_CACHED_REFRESH_DELAY_MS = 2500, WALLET_REGTEST_DEPOSIT_CLAIM_POLL_MS, WALLET_MAINNET_DEPOSIT_CLAIM_POLL_MS, WALLET_BALANCE_EVENT_COALESCE_MS, WALLET_INCOMING_UPDATE_COALESCE_MS = 250, WALLET_AUTO_CLAIM_MAX_FEE_SATS = 5000, WALLET_CLAIM_PAGE_SIZE = 100, WALLET_PENDING_TRANSFER_CLAIM_BATCH_SIZE = 50, WALLET_PENDING_TRANSFER_STATUS_BATCH_SIZE = 2, WALLET_PENDING_TRANSFER_COLD_REFRESH_BATCH_SIZE = 50, WALLET_PENDING_TRANSFER_COLD_REFRESH_INTERVAL_MS, WALLET_PENDING_TRANSFER_ADAPTIVE_BATCH_SMALL_QUEUE = 3, WALLET_PENDING_TRANSFER_ADAPTIVE_BATCH_LARGE_QUEUE = 10, WALLET_PENDING_TRANSFER_ADAPTIVE_BATCH_SMALL_SIZE = 2, WALLET_PENDING_TRANSFER_ADAPTIVE_BATCH_LARGE_SIZE = 3, WALLET_PENDING_TRANSFER_SLOW_CLAIM_MS, WALLET_PENDING_TRANSFER_CLAIM_COOLDOWN_MS, WALLET_SDK_BACKGROUND_QUIET_MS, WALLET_RECENT_TRANSFER_LIMIT = 100, WALLET_TRANSFER_PAGE_LIMIT = 100, WALLET_TRANSFER_FETCH_THROTTLE_MS = 150, WALLET_PENDING_TRANSFER_CLAIM_RETRY_MS, WALLET_SENT_TRANSFER_REFRESH_DELAY_MS, WALLET_PENDING_TRANSFER_BOOT_GRACE_MS, WALLET_PENDING_TRANSFER_HOT_AGE_MS, WALLET_PENDING_TRANSFER_WARM_AGE_MS, WALLET_PENDING_TRANSFER_WARM_RETRY_MS, WALLET_PENDING_TRANSFER_STALE_RETRY_MS, WALLET_PENDING_TRANSFER_STUCK_RETRY_MS, WALLET_PENDING_TRANSFER_DORMANT_RETRY_MS, WALLET_TRANSFER_CACHE_WRITE_DELAY_MS;
6453
+ var MS_PER_SECOND = 1000, MINUTE_MS, HOUR_MS, DAY_MS, KIB_BYTES = 1024, MIB_BYTES, SATS_PER_BITCOIN = 100000000n, USERNAME_MAX_CHARS = 12, PASSWORD_MIN_CHARS = 15, PASSWORD_MAX_CHARS = 64, PASSWORD_MAX_BYTES, AUTOLOCK_MIN_MINUTES = 1, AUTOLOCK_MAX_MINUTES = 60, LOCAL_CACHE_WRITE_DELAY_MS = 350, LOCAL_MEDIA_CACHE_MAX_BYTES, LOCAL_MEDIA_CACHE_MAX_ITEMS = 1000, LOCAL_MEDIA_ACCESS_TOUCH_MIN_MS, LOCAL_PROFILE_CACHE_MAX_ITEMS = 500, LOCAL_PROFILE_CACHE_MAX_AGE_MS, LOCAL_CHAT_CACHE_MAX_ITEMS = 1000, LOCAL_CHAT_MESSAGE_CACHE_MAX_VISIBLE = 1000, LOCAL_CHAT_MESSAGE_CACHE_WRITE_DELAY_MS = 2500, LOCAL_AVATAR_CACHE_MAX_BYTES, LOCAL_AVATAR_CACHE_MAX_AGE_MS, AVATAR_IMAGE_MAX_BYTES, AVATAR_IMAGE_QUALITY_ATTEMPTS, CHAT_AVATAR_IMAGE_MAX_BYTES, CHAT_AVATAR_REF_MAX_CHARS = 256, CHAT_MESSAGE_FILE_CACHE_MAX_BYTES, IDLE_CALLBACK_MIN_TIMEOUT_MS = 50, ATTACHMENT_CACHE_IDLE_TIMEOUT_MS = 2500, ATTACHMENT_CACHE_FALLBACK_DELAY_MS = 250, CHAT_UNSAVED_TTL_DAYS = 21, CHAT_UNSAVED_TTL_MS, CHAT_AFTER_SEEN_MS, CHAT_MEDIA_TTL_DAYS, CHAT_MEDIA_TTL_MS, CHAT_UPLOAD_MAX_BYTES, CHAT_AUDIO_TRANSCODE_BITRATE_BPS = 128000, CHAT_VIDEO_TRANSCODE_VIDEO_BITRATE_BPS = 3000000, CHAT_VIDEO_TRANSCODE_AUDIO_BITRATE_BPS, CHAT_MESSAGE_BATCH_SIZE = 40, CHAT_MESSAGE_QUERY_MAX_DOCS = 120, CHAT_TTL_CLIENT_DELETE_GRACE_MS, CHAT_TTL_DELETE_CHUNK_SIZE = 40, CHAT_DELETE_SCAN_BATCH_SIZE = 200, CHAT_DELETE_WRITE_BATCH_SIZE = 400, CHAT_BATCH_CLEANUP_IDLE_TIMEOUT_MS = 1500, CHAT_BATCH_CLEANUP_IDLE_DELAY_MS = 250, CHAT_LIST_PAGE_SIZE = 20, CHAT_LIST_LIVE_COUNT, CHAT_INBOX_PING_PAGE_SIZE = 25, CHAT_INBOX_CURSOR_OVERLAP_MS, CHAT_LIST_CACHE_WRITE_DELAY_MS = 1500, CHAT_LIST_SNAPSHOT_COALESCE_MS = 80, CHAT_LIST_LISTENER_RETRY_MS = 1000, CHAT_TOP_WARM_COUNT = 0, CHAT_EAGER_WARM_COUNT = 0, CHAT_WARM_DELAY_MS = 3000, CHAT_WARM_BATCH_SIZE, CHAT_MESSAGE_VIEW_CACHE_SIZE = 30, CHAT_MEDIA_WARM_MESSAGES_PER_CHAT, CHAT_MEDIA_WARM_START_DELAY_MS = 600, CHAT_MEDIA_WARM_STEP_DELAY_MS = 120, CHAT_MEDIA_WARM_TYPES, CHAT_MEDIA_WARM_MAX_BYTES = 0, CHAT_READ_WRITE_INTERVAL_MS, CHAT_READ_WRITE_MAX_WAIT_MS, CHAT_LIVE_PING_INTERVAL_MS, CHAT_LIVE_SWEEP_INTERVAL_MS, CHAT_LIVE_STALE_AFTER_MS, CHAT_LIVE_RECONNECT_MAX_MS, CHAT_LIVE_TYPING_IDLE_MS, CHAT_LIVE_TYPING_RENEW_MS, CHAT_LIVE_COMPOSITION_HANDOFF_MS, CHAT_LIVE_READ_SEND_INTERVAL_MS = 120, CHAT_LIVE_READ_SEND_MAX_WAIT_MS = 500, CHAT_LIVE_READ_WRITE_INTERVAL_MS, CHAT_LIVE_READ_WRITE_MAX_WAIT_MS, CHAT_SEND_QUEUE_RATE_LIMIT_COUNT = 12, CHAT_SEND_QUEUE_RATE_LIMIT_WINDOW_MS, CHAT_SETTINGS_BODY_MAX_BYTES, CHAT_MANIFEST_MAX_BYTES, CHAT_RECEIPT_MAX_MEMBERS = 32, CHAT_ORDINARY_DELIVERY_MAX_MEMBERS = 128, CHAT_MAX_MEMBERS = 256, CHAT_MAX_TEXT_CHARS = 2048, CHAT_TITLE_MAX_CHARS = 32, CHAT_MAX_REACTIONS, SEARCH_DEBOUNCE_MS = 300, SEARCH_USERNAME_LIMIT = 15, SEARCH_ROLE_LIMIT = 15, RECENT_PEER_REFRESH_LIMIT = 50, RECENT_PEER_REFRESH_DELAY_MS = 250, RECENT_PEER_REFRESH_INTERVAL_MS, RECENT_PEER_REFRESH_THROTTLE_MS = 120, BAN_REFRESH_GRACE_MS = 50, REQUEST_MONEY_MAX_SATS, WALLET_TRANSFER_POLL_MS, WALLET_ACTIVE_TRANSFER_REFRESH_MS, WALLET_IDLE_ACTIVE_TRANSFER_REFRESH_MS, WALLET_VISIBLE_TRANSFER_DISCOVERY_WINDOW_MS, WALLET_ACTIVE_TRANSFER_EMPTY_SCAN_LIMIT = 8, WALLET_CACHE_HYDRATE_DELAY_MS = 0, WALLET_BOOT_REFRESH_DELAY_MS = 500, WALLET_BOOT_CACHED_REFRESH_DELAY_MS = 2500, WALLET_REGTEST_DEPOSIT_CLAIM_POLL_MS, WALLET_MAINNET_DEPOSIT_CLAIM_POLL_MS, WALLET_BALANCE_EVENT_COALESCE_MS, WALLET_INCOMING_UPDATE_COALESCE_MS = 250, WALLET_AUTO_CLAIM_MAX_FEE_SATS = 5000, WALLET_CLAIM_PAGE_SIZE = 100, WALLET_PENDING_TRANSFER_CLAIM_BATCH_SIZE = 50, WALLET_PENDING_TRANSFER_STATUS_BATCH_SIZE = 2, WALLET_PENDING_TRANSFER_COLD_REFRESH_BATCH_SIZE = 50, WALLET_PENDING_TRANSFER_COLD_REFRESH_INTERVAL_MS, WALLET_PENDING_TRANSFER_ADAPTIVE_BATCH_SMALL_QUEUE = 3, WALLET_PENDING_TRANSFER_ADAPTIVE_BATCH_LARGE_QUEUE = 10, WALLET_PENDING_TRANSFER_ADAPTIVE_BATCH_SMALL_SIZE = 2, WALLET_PENDING_TRANSFER_ADAPTIVE_BATCH_LARGE_SIZE = 3, WALLET_PENDING_TRANSFER_SLOW_CLAIM_MS, WALLET_PENDING_TRANSFER_CLAIM_COOLDOWN_MS, WALLET_SDK_BACKGROUND_QUIET_MS, WALLET_RECENT_TRANSFER_LIMIT = 100, WALLET_TRANSFER_PAGE_LIMIT = 100, WALLET_TRANSFER_FETCH_THROTTLE_MS = 150, WALLET_PENDING_TRANSFER_CLAIM_RETRY_MS, WALLET_SENT_TRANSFER_REFRESH_DELAY_MS, WALLET_PENDING_TRANSFER_BOOT_GRACE_MS, WALLET_PENDING_TRANSFER_HOT_AGE_MS, WALLET_PENDING_TRANSFER_WARM_AGE_MS, WALLET_PENDING_TRANSFER_WARM_RETRY_MS, WALLET_PENDING_TRANSFER_STALE_RETRY_MS, WALLET_PENDING_TRANSFER_STUCK_RETRY_MS, WALLET_PENDING_TRANSFER_DORMANT_RETRY_MS, WALLET_TRANSFER_CACHE_WRITE_DELAY_MS;
6454
6454
  var init_config = __esm(() => {
6455
6455
  MINUTE_MS = 60 * MS_PER_SECOND;
6456
6456
  HOUR_MS = 60 * MINUTE_MS;
@@ -6463,8 +6463,8 @@ var init_config = __esm(() => {
6463
6463
  LOCAL_AVATAR_CACHE_MAX_BYTES = 128 * MIB_BYTES;
6464
6464
  LOCAL_AVATAR_CACHE_MAX_AGE_MS = 30 * DAY_MS;
6465
6465
  AVATAR_IMAGE_MAX_BYTES = 256 * KIB_BYTES;
6466
- CHAT_AVATAR_IMAGE_MAX_BYTES = 16 * KIB_BYTES;
6467
- CHAT_AVATAR_REF_MAX_CHARS = 24 * KIB_BYTES;
6466
+ AVATAR_IMAGE_QUALITY_ATTEMPTS = Object.freeze([0.92, 0.84, 0.76, 0.68]);
6467
+ CHAT_AVATAR_IMAGE_MAX_BYTES = AVATAR_IMAGE_MAX_BYTES;
6468
6468
  CHAT_MESSAGE_FILE_CACHE_MAX_BYTES = 64 * MIB_BYTES;
6469
6469
  CHAT_UNSAVED_TTL_MS = CHAT_UNSAVED_TTL_DAYS * DAY_MS;
6470
6470
  CHAT_AFTER_SEEN_MS = DAY_MS;
@@ -9424,6 +9424,430 @@ var init_user = __esm(() => {
9424
9424
  };
9425
9425
  });
9426
9426
 
9427
+ // ../../core/crypto/file.js
9428
+ function createFileKey() {
9429
+ return randomBytes3(32);
9430
+ }
9431
+ function encodeFileKey(key) {
9432
+ return toHex(toBytes32(key, "file key"));
9433
+ }
9434
+ function decodeFileKey(key) {
9435
+ return toBytes32(key, "file key");
9436
+ }
9437
+ function getFileAadForPath(scope) {
9438
+ if (!scope) {
9439
+ throw new Error("file scope required");
9440
+ }
9441
+ return encodeScope(FILE_SCOPE, [scope]);
9442
+ }
9443
+ function getSubtleCrypto() {
9444
+ const subtle = globalThis.crypto?.subtle;
9445
+ if (!subtle) {
9446
+ throw new Error("WebCrypto AES-GCM unavailable");
9447
+ }
9448
+ return subtle;
9449
+ }
9450
+ async function importAesKey(key, usages) {
9451
+ return getSubtleCrypto().importKey("raw", toBytes32(key, "file key"), { name: "AES-GCM" }, false, usages);
9452
+ }
9453
+ async function sealFile(key, bytes, scope) {
9454
+ const fileKey = new Uint8Array(toBytes32(key, "file key"));
9455
+ try {
9456
+ const cryptoKey = await importAesKey(fileKey, ["encrypt"]);
9457
+ const iv = randomBytes3(FILE_IV_BYTES);
9458
+ const ct = await getSubtleCrypto().encrypt({
9459
+ name: "AES-GCM",
9460
+ iv,
9461
+ tagLength: FILE_TAG_BYTES * 8,
9462
+ additionalData: getFileAadForPath(scope)
9463
+ }, cryptoKey, toBytes(bytes, "plaintext"));
9464
+ return packBodyData(iv, new Uint8Array(ct));
9465
+ } finally {
9466
+ cleanBytes(fileKey);
9467
+ }
9468
+ }
9469
+ async function openFileForPath(key, body, scope) {
9470
+ const fileKey = new Uint8Array(toBytes32(key, "file key"));
9471
+ try {
9472
+ const cryptoKey = await importAesKey(fileKey, ["decrypt"]);
9473
+ const { nonce, ct } = unpackBodyData(body, FILE_IV_BYTES);
9474
+ const pt = await getSubtleCrypto().decrypt({
9475
+ name: "AES-GCM",
9476
+ iv: nonce,
9477
+ tagLength: FILE_TAG_BYTES * 8,
9478
+ additionalData: getFileAadForPath(scope)
9479
+ }, cryptoKey, ct);
9480
+ return new Uint8Array(pt);
9481
+ } finally {
9482
+ cleanBytes(fileKey);
9483
+ }
9484
+ }
9485
+ var FILE_SCOPE = "file-body", FILE_IV_BYTES = 12, FILE_TAG_BYTES = 16;
9486
+ var init_file = __esm(() => {
9487
+ init_core();
9488
+ init_kdf();
9489
+ init_pack();
9490
+ "use client";
9491
+ });
9492
+
9493
+ // ../../core/chat/filepayload.js
9494
+ function cleanMediaEpochId(value) {
9495
+ const epochId = String(value || "").trim();
9496
+ if (!new RegExp(`^${CHAT_ID_PATTERN}$`).test(epochId)) {
9497
+ throw new Error("invalid media epoch id");
9498
+ }
9499
+ return epochId.toLowerCase();
9500
+ }
9501
+ function cleanMediaId(value) {
9502
+ const mediaId = String(value || "").trim();
9503
+ if (!new RegExp(`^${MEDIA_ID_PATTERN}$`).test(mediaId)) {
9504
+ throw new Error("invalid media id");
9505
+ }
9506
+ return mediaId.toLowerCase();
9507
+ }
9508
+ function cleanSharedMediaId(value) {
9509
+ const sharedId = String(value || "").trim();
9510
+ if (!new RegExp(`^${SHARED_MEDIA_ID_PATTERN}$`).test(sharedId)) {
9511
+ throw new Error("invalid shared media id");
9512
+ }
9513
+ return sharedId.toLowerCase();
9514
+ }
9515
+ function makeChatMediaId() {
9516
+ return toHex(randomBytes3(16));
9517
+ }
9518
+ function mediaFilePath(epochId, mediaId) {
9519
+ const nextEpochId = cleanMediaEpochId(epochId);
9520
+ const nextMediaId = cleanMediaId(mediaId);
9521
+ return `${CHAT_MEDIA_ROOT}/${nextEpochId}/${nextMediaId}`;
9522
+ }
9523
+ function sharedMediaFilePath(sharedId) {
9524
+ const nextSharedId = cleanSharedMediaId(sharedId);
9525
+ return `${SHARED_MEDIA_ROOT}/${nextSharedId}`;
9526
+ }
9527
+ function makeSharedMediaId() {
9528
+ return toHex(randomBytes3(16));
9529
+ }
9530
+ function getMediaFileRef(path) {
9531
+ const value = String(path || "").trim();
9532
+ const chatMatch = value.match(CHAT_MEDIA_FILE_PATTERN);
9533
+ if (chatMatch?.[1] && chatMatch?.[2]) {
9534
+ return {
9535
+ type: "chat",
9536
+ epochId: chatMatch[1].toLowerCase(),
9537
+ mediaId: chatMatch[2].toLowerCase()
9538
+ };
9539
+ }
9540
+ const sharedMatch = value.match(SHARED_MEDIA_FILE_PATTERN);
9541
+ if (sharedMatch?.[1]) {
9542
+ return {
9543
+ type: "shared",
9544
+ sharedId: sharedMatch[1].toLowerCase()
9545
+ };
9546
+ }
9547
+ throw new Error("invalid media file path");
9548
+ }
9549
+ function getChatMediaFileRef(path) {
9550
+ const ref = getMediaFileRef(path);
9551
+ if (ref?.type !== "chat") {
9552
+ throw new Error("invalid media file path");
9553
+ }
9554
+ return ref;
9555
+ }
9556
+ function getSharedMediaFileRef(path) {
9557
+ const ref = getMediaFileRef(path);
9558
+ if (ref?.type !== "shared") {
9559
+ throw new Error("invalid shared media file path");
9560
+ }
9561
+ return ref;
9562
+ }
9563
+ function uploadByteLength(value) {
9564
+ if (Number.isFinite(value?.byteLength)) {
9565
+ return value.byteLength;
9566
+ }
9567
+ if (Number.isFinite(value?.size)) {
9568
+ return value.size;
9569
+ }
9570
+ if (Number.isFinite(value)) {
9571
+ return value;
9572
+ }
9573
+ return null;
9574
+ }
9575
+ function makeChatUploadTooLargeError(bytes, maxBytes = MAX_CHAT_UPLOAD_BYTES) {
9576
+ const error = new Error("upload too large");
9577
+ error.code = "upload-too-large";
9578
+ error.maxBytes = maxBytes;
9579
+ if (Number.isFinite(bytes)) {
9580
+ error.bytes = bytes;
9581
+ }
9582
+ return error;
9583
+ }
9584
+ function assertChatUploadByteSize(bytes, maxBytes = MAX_CHAT_UPLOAD_BYTES) {
9585
+ const length = uploadByteLength(bytes);
9586
+ if (!Number.isFinite(length)) {
9587
+ throw new Error("upload bytes required");
9588
+ }
9589
+ if (length <= 0 || length > maxBytes) {
9590
+ throw makeChatUploadTooLargeError(length, maxBytes);
9591
+ }
9592
+ return length;
9593
+ }
9594
+ async function toUploadBytes(data) {
9595
+ if (typeof Blob !== "undefined" && data instanceof Blob) {
9596
+ if (typeof data.arrayBuffer === "function") {
9597
+ return new Uint8Array(await data.arrayBuffer());
9598
+ }
9599
+ if (typeof FileReader !== "undefined") {
9600
+ return new Promise((resolve, reject) => {
9601
+ const reader = new FileReader;
9602
+ reader.onload = () => resolve(new Uint8Array(reader.result));
9603
+ reader.onerror = () => reject(reader.error || new Error("blob read failed"));
9604
+ reader.readAsArrayBuffer(data);
9605
+ });
9606
+ }
9607
+ if (typeof Response !== "undefined") {
9608
+ return new Uint8Array(await new Response(data).arrayBuffer());
9609
+ }
9610
+ }
9611
+ if (typeof data?.arrayBuffer === "function") {
9612
+ return new Uint8Array(await data.arrayBuffer());
9613
+ }
9614
+ return toBytes(data, "upload bytes");
9615
+ }
9616
+ async function makeChatFileUploadPayload(epochId, cid, data, { cacheControl = "private, max-age=0, no-transform" } = {}) {
9617
+ const nextEpochId = cleanMediaEpochId(epochId);
9618
+ const mediaId = makeChatMediaId();
9619
+ const path = mediaFilePath(nextEpochId, mediaId);
9620
+ const expiresAt = Date.now() + CHAT_MEDIA_TTL_MS2;
9621
+ const key = createFileKey();
9622
+ try {
9623
+ const uploadBytes = await toUploadBytes(data);
9624
+ assertChatUploadByteSize(uploadBytes);
9625
+ const body = await sealFile(key, uploadBytes, path);
9626
+ assertChatUploadByteSize(body);
9627
+ return {
9628
+ epochId: nextEpochId,
9629
+ mediaId,
9630
+ path,
9631
+ body,
9632
+ metadata: {
9633
+ contentType: "application/octet-stream",
9634
+ cacheControl
9635
+ },
9636
+ file: {
9637
+ p: path,
9638
+ k: encodeFileKey(key),
9639
+ x: expiresAt
9640
+ }
9641
+ };
9642
+ } catch (error) {
9643
+ if (error && typeof error === "object") {
9644
+ error.path = error?.path || path;
9645
+ error.cid = error?.cid || cid;
9646
+ }
9647
+ throw error;
9648
+ } finally {
9649
+ cleanBytes(key);
9650
+ }
9651
+ }
9652
+ async function makeSharedFileUploadPayload(data, { contentType = "application/octet-stream", cacheControl = "private, max-age=0, no-transform" } = {}) {
9653
+ const sharedId = makeSharedMediaId();
9654
+ const path = sharedMediaFilePath(sharedId);
9655
+ const expiresAt = Date.now() + CHAT_MEDIA_TTL_MS2;
9656
+ const key = createFileKey();
9657
+ try {
9658
+ const uploadBytes = await toUploadBytes(data);
9659
+ assertChatUploadByteSize(uploadBytes);
9660
+ const body = await sealFile(key, uploadBytes, path);
9661
+ assertChatUploadByteSize(body);
9662
+ return {
9663
+ sharedId,
9664
+ path,
9665
+ body,
9666
+ metadata: {
9667
+ contentType,
9668
+ cacheControl
9669
+ },
9670
+ file: {
9671
+ p: path,
9672
+ k: encodeFileKey(key),
9673
+ x: expiresAt
9674
+ }
9675
+ };
9676
+ } catch (error) {
9677
+ if (error && typeof error === "object") {
9678
+ error.path = error?.path || path;
9679
+ error.sharedId = error?.sharedId || sharedId;
9680
+ }
9681
+ throw error;
9682
+ } finally {
9683
+ cleanBytes(key);
9684
+ }
9685
+ }
9686
+ var CHAT_MEDIA_ROOT = "chatEpochs", SHARED_MEDIA_ROOT = "shared", CHAT_MEDIA_TTL_MS2, MAX_CHAT_UPLOAD_BYTES, CHAT_VIDEO_TRANSCODE_VIDEO_BITRATE_BPS2, CHAT_VIDEO_TRANSCODE_AUDIO_BITRATE_BPS2, CHAT_VIDEO_TRANSCODE_TOTAL_BITRATE_BPS, CHAT_ID_PATTERN = "[0-9a-fA-F]{64}", SHARED_MEDIA_ID_PATTERN = "[0-9a-fA-F]{32}", MEDIA_ID_PATTERN = "[0-9a-fA-F]{32}", CHAT_MEDIA_FILE_PATTERN, SHARED_MEDIA_FILE_PATTERN;
9687
+ var init_filepayload = __esm(() => {
9688
+ init_file();
9689
+ init_config();
9690
+ init_core();
9691
+ "use client";
9692
+ CHAT_MEDIA_TTL_MS2 = CHAT_MEDIA_TTL_MS;
9693
+ MAX_CHAT_UPLOAD_BYTES = CHAT_UPLOAD_MAX_BYTES;
9694
+ CHAT_VIDEO_TRANSCODE_VIDEO_BITRATE_BPS2 = CHAT_VIDEO_TRANSCODE_VIDEO_BITRATE_BPS;
9695
+ CHAT_VIDEO_TRANSCODE_AUDIO_BITRATE_BPS2 = CHAT_VIDEO_TRANSCODE_AUDIO_BITRATE_BPS;
9696
+ CHAT_VIDEO_TRANSCODE_TOTAL_BITRATE_BPS = CHAT_VIDEO_TRANSCODE_VIDEO_BITRATE_BPS2 + CHAT_VIDEO_TRANSCODE_AUDIO_BITRATE_BPS2;
9697
+ CHAT_MEDIA_FILE_PATTERN = new RegExp(`^${CHAT_MEDIA_ROOT}/(${CHAT_ID_PATTERN})/(${MEDIA_ID_PATTERN})$`);
9698
+ SHARED_MEDIA_FILE_PATTERN = new RegExp(`^${SHARED_MEDIA_ROOT}/(${SHARED_MEDIA_ID_PATTERN})$`);
9699
+ });
9700
+
9701
+ // ../../core/files.js
9702
+ function makeFileId(size = 8) {
9703
+ return toHex(randomBytes3(size));
9704
+ }
9705
+ function setErrorStage(error, stage, extra = {}) {
9706
+ if (!error || typeof error !== "object") {
9707
+ return error;
9708
+ }
9709
+ error.stage = error?.stage || stage;
9710
+ Object.assign(error, extra);
9711
+ return error;
9712
+ }
9713
+ async function makeChatFileUpload(epochId, cid, data, { cacheControl = "private, max-age=0, no-transform" } = {}) {
9714
+ try {
9715
+ return await makeChatFileUploadPayload(epochId, cid, data, {
9716
+ cacheControl
9717
+ });
9718
+ } catch (error) {
9719
+ throw setErrorStage(error, "encrypt", {
9720
+ ...error?.path ? { path: error.path } : {},
9721
+ cid
9722
+ });
9723
+ }
9724
+ }
9725
+ async function putChatFile(epochId, cid, data, options) {
9726
+ const upload = await makeChatFileUpload(epochId, cid, data, options);
9727
+ try {
9728
+ if (typeof options?.uploadChatMedia !== "function") {
9729
+ throw new Error("chat media upload required");
9730
+ }
9731
+ await options.uploadChatMedia(upload);
9732
+ return upload.file;
9733
+ } catch (error) {
9734
+ throw setErrorStage(error, "upload", { path: upload.path, cid });
9735
+ }
9736
+ }
9737
+ async function makeSharedFileUpload(data, { contentType = "application/octet-stream", cacheControl = "private, max-age=0, no-transform" } = {}) {
9738
+ try {
9739
+ return await makeSharedFileUploadPayload(data, {
9740
+ contentType,
9741
+ cacheControl
9742
+ });
9743
+ } catch (error) {
9744
+ throw setErrorStage(error, "encrypt", {
9745
+ ...error?.path ? { path: error.path } : {}
9746
+ });
9747
+ }
9748
+ }
9749
+ async function putSharedFile(data, options = {}) {
9750
+ const upload = await makeSharedFileUpload(data, options);
9751
+ try {
9752
+ if (typeof options?.uploadSharedMedia !== "function") {
9753
+ throw new Error("shared media upload required");
9754
+ }
9755
+ await options.uploadSharedMedia(upload);
9756
+ return upload.file;
9757
+ } catch (error) {
9758
+ throw setErrorStage(error, "upload", { path: upload.path });
9759
+ }
9760
+ }
9761
+ async function readChatFile(readChatMedia, file) {
9762
+ let body;
9763
+ try {
9764
+ if (typeof readChatMedia !== "function") {
9765
+ throw new Error("chat media read required");
9766
+ }
9767
+ body = await readChatMedia(file?.p);
9768
+ } catch (error) {
9769
+ error.path = error?.path || file?.p || null;
9770
+ error.stage = error?.stage || "download";
9771
+ throw error;
9772
+ }
9773
+ try {
9774
+ getMediaFileRef(file?.p);
9775
+ const bytes = await openFileForPath(decodeFileKey(file?.k), body, file?.p);
9776
+ return bytes;
9777
+ } catch (error) {
9778
+ error.path = file?.p || null;
9779
+ error.stage = "decrypt";
9780
+ throw error;
9781
+ }
9782
+ }
9783
+ var init_files = __esm(() => {
9784
+ init_file();
9785
+ init_core();
9786
+ init_filepayload();
9787
+ "use client";
9788
+ });
9789
+
9790
+ // ../../core/chat/avatar.js
9791
+ function chatAvatarFile(value) {
9792
+ const ref = typeof value === "string" ? value.trim() : "";
9793
+ if (!ref.startsWith(CHAT_AVATAR_REF_PREFIX) || ref.length > CHAT_AVATAR_REF_MAX_CHARS)
9794
+ return null;
9795
+ const parts = ref.split(":");
9796
+ const key = parts[2]?.toLowerCase();
9797
+ if (parts.length !== 3 || parts[0] !== "v1" || !FILE_KEY_RE.test(key))
9798
+ return null;
9799
+ let parsed;
9800
+ try {
9801
+ parsed = getChatMediaFileRef(parts[1]);
9802
+ } catch {
9803
+ return null;
9804
+ }
9805
+ return Object.freeze({
9806
+ t: "img",
9807
+ m: "image/webp",
9808
+ p: mediaFilePath(parsed.epochId, parsed.mediaId),
9809
+ k: key
9810
+ });
9811
+ }
9812
+ function makeChatAvatarRef(file) {
9813
+ let parsed;
9814
+ const path = cleanText(file?.p);
9815
+ const key = cleanText(file?.k).toLowerCase();
9816
+ try {
9817
+ parsed = getChatMediaFileRef(path);
9818
+ } catch {
9819
+ throw new Error("invalid chat avatar");
9820
+ }
9821
+ if (!FILE_KEY_RE.test(key))
9822
+ throw new Error("invalid chat avatar");
9823
+ const ref = `${CHAT_AVATAR_REF_PREFIX}${mediaFilePath(parsed.epochId, parsed.mediaId)}:${key}`;
9824
+ if (ref.length > CHAT_AVATAR_REF_MAX_CHARS)
9825
+ throw new Error("invalid chat avatar");
9826
+ return ref;
9827
+ }
9828
+ function cleanChatAvatarRef(value) {
9829
+ if (value == null)
9830
+ return null;
9831
+ const file = chatAvatarFile(value);
9832
+ if (!file)
9833
+ throw new Error("invalid chat avatar");
9834
+ return makeChatAvatarRef(file);
9835
+ }
9836
+ function assertChatAvatarBytes(value) {
9837
+ const bytes = Number(value?.byteLength ?? value?.size);
9838
+ if (!Number.isSafeInteger(bytes) || bytes <= 0 || bytes > CHAT_AVATAR_IMAGE_MAX_BYTES) {
9839
+ throw new Error("invalid chat avatar");
9840
+ }
9841
+ return bytes;
9842
+ }
9843
+ var CHAT_AVATAR_REF_PREFIX = "v1:", FILE_KEY_RE;
9844
+ var init_avatar2 = __esm(() => {
9845
+ init_config();
9846
+ init_filepayload();
9847
+ "use client";
9848
+ FILE_KEY_RE = /^[0-9a-f]{64}$/u;
9849
+ });
9850
+
9427
9851
  // ../../core/chat/state.js
9428
9852
  function makeCid() {
9429
9853
  return `${Date.now().toString(36)}${toHex(randomBytes3(3))}`;
@@ -9689,6 +10113,11 @@ function makeChatUnavailableError() {
9689
10113
  error.code = "permission-denied";
9690
10114
  return error;
9691
10115
  }
10116
+ function isChatEpochConflict(error) {
10117
+ const code = String(error?.code || "").trim().toLowerCase();
10118
+ const message = String(error?.message || "").trim().toLowerCase();
10119
+ return code === "permission-denied" || code.endsWith("/permission-denied") || code === "aborted" || code.endsWith("/aborted") || code === "failed-precondition" || code.endsWith("/failed-precondition") || message.includes("insufficient permissions") || message === "chat owner epoch changed";
10120
+ }
9692
10121
  function makeMessageSaveUnavailableError() {
9693
10122
  const error = new Error("this message can't be saved anymore");
9694
10123
  error.code = "message-unavailable";
@@ -10168,34 +10597,6 @@ var init_chat = __esm(() => {
10168
10597
  MSG_BODY_HEADER_BYTES = 1 + 1 + 1 + BOX_NONCE_BYTES + ED25519_SIGNATURE_BYTES;
10169
10598
  });
10170
10599
 
10171
- // ../../core/chat/avatar.js
10172
- function cleanChatAvatarRef(value) {
10173
- if (value == null)
10174
- return null;
10175
- const ref = cleanText(value);
10176
- if (!ref || ref.length > CHAT_AVATAR_REF_MAX_CHARS) {
10177
- throw new Error("invalid chat avatar");
10178
- }
10179
- return ref;
10180
- }
10181
- function chatAvatarDataUrl(value) {
10182
- const ref = typeof value === "string" ? value.trim() : "";
10183
- if (!ref.startsWith(CHAT_AVATAR_PREFIX) || ref.length > CHAT_AVATAR_REF_MAX_CHARS)
10184
- return "";
10185
- const body = ref.slice(CHAT_AVATAR_PREFIX.length);
10186
- if (!body || body.length % 4 !== 0 || !BASE64_RE.test(body))
10187
- return "";
10188
- const padding2 = body.endsWith("==") ? 2 : body.endsWith("=") ? 1 : 0;
10189
- const bytes = body.length / 4 * 3 - padding2;
10190
- return bytes > 0 && bytes <= CHAT_AVATAR_IMAGE_MAX_BYTES ? ref : "";
10191
- }
10192
- var CHAT_AVATAR_PREFIX = "data:image/webp;base64,", BASE64_RE;
10193
- var init_avatar2 = __esm(() => {
10194
- init_config();
10195
- "use client";
10196
- BASE64_RE = /^[A-Za-z0-9+/]+={0,2}$/u;
10197
- });
10198
-
10199
10600
  // ../../core/chat/epochs/transition.js
10200
10601
  function cleanTime(value) {
10201
10602
  if (!Number.isSafeInteger(value) || value <= 0) {
@@ -10416,7 +10817,7 @@ function normalizeChatStateSettings(value = {}) {
10416
10817
  throw new Error("chat settings required");
10417
10818
  }
10418
10819
  const title = value.title == null ? null : cleanText(value.title);
10419
- if (title != null && (!title || title.length > 128)) {
10820
+ if (title != null && (!title || title.length > CHAT_TITLE_MAX_CHARS)) {
10420
10821
  throw new Error("invalid chat title");
10421
10822
  }
10422
10823
  return {
@@ -10728,6 +11129,20 @@ function normalizeCurrentEpoch(value) {
10728
11129
  settings: normalizeChatSettingsProjection(value.settings)
10729
11130
  };
10730
11131
  }
11132
+ function normalizeOwnerRetirement(value, current) {
11133
+ if (value == null)
11134
+ return null;
11135
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
11136
+ throw new Error("invalid chat owner retirement");
11137
+ }
11138
+ const kind = cleanText(value.kind);
11139
+ const epochVersion = value.epochVersion;
11140
+ if (!Object.values(CHAT_OWNER_RETIREMENT_KINDS).includes(kind) || !Number.isSafeInteger(epochVersion) || epochVersion < current.manifest.epochVersion) {
11141
+ throw new Error("invalid chat owner retirement");
11142
+ }
11143
+ const proposalId = kind === CHAT_OWNER_RETIREMENT_KINDS.LEAVE ? cleanTransitionMessageId(value.proposalId) : null;
11144
+ return { kind, epochVersion, proposalId };
11145
+ }
10731
11146
  function normalizeOwnerEntry(value) {
10732
11147
  if (!value || typeof value !== "object" || Array.isArray(value) || value.v !== CHAT_ENTRY_VERSION || value.protocol !== CHAT_PROTOCOL_VERSION) {
10733
11148
  throw new Error("invalid chat entry");
@@ -10748,7 +11163,8 @@ function normalizeOwnerEntry(value) {
10748
11163
  saved: value.saved || null,
10749
11164
  startMs: Number.isFinite(value.startMs) ? value.startMs : null,
10750
11165
  deliveryRegistered: value.deliveryRegistered === true,
10751
- notificationTag: cleanText(value.notificationTag) || null
11166
+ notificationTag: cleanText(value.notificationTag) || null,
11167
+ retirement: normalizeOwnerRetirement(value.retirement, current)
10752
11168
  };
10753
11169
  }
10754
11170
  async function sealOwnChatEntry(chatPrivateKey, entryId, entry) {
@@ -10789,7 +11205,8 @@ function makeOwnChatEntry(epoch, fields = {}) {
10789
11205
  saved: fields.saved || null,
10790
11206
  startMs: Number.isFinite(fields.startMs) ? fields.startMs : null,
10791
11207
  deliveryRegistered: fields.deliveryRegistered === true,
10792
- notificationTag: cleanText(fields.notificationTag) || null
11208
+ notificationTag: cleanText(fields.notificationTag) || null,
11209
+ retirement: fields.retirement || null
10793
11210
  });
10794
11211
  }
10795
11212
  function normalizeOwnerEpochRecord(value) {
@@ -10797,6 +11214,7 @@ function normalizeOwnerEpochRecord(value) {
10797
11214
  throw new Error("invalid owner epoch entry");
10798
11215
  }
10799
11216
  const manifest = normalizeEpochManifest(value.manifest);
11217
+ const leaveProposalId = value.leaveProposalId == null ? null : cleanTransitionMessageId(value.leaveProposalId);
10800
11218
  return {
10801
11219
  v: CHAT_OWNER_EPOCH_ENTRY_VERSION,
10802
11220
  protocol: CHAT_PROTOCOL_VERSION,
@@ -10806,8 +11224,9 @@ function normalizeOwnerEpochRecord(value) {
10806
11224
  manifest,
10807
11225
  epochSecret: cleanChatHex(value.epochSecret, "epoch secret"),
10808
11226
  cutoffMs: Number.isFinite(value.cutoffMs) ? value.cutoffMs : null,
10809
- successorTransitionCommitment: cleanTransitionCommitment(value.successorTransitionCommitment),
10810
- successorTransitionMessageId: cleanTransitionMessageId(value.successorTransitionMessageId)
11227
+ successorTransitionCommitment: leaveProposalId ? null : cleanTransitionCommitment(value.successorTransitionCommitment),
11228
+ successorTransitionMessageId: leaveProposalId ? null : cleanTransitionMessageId(value.successorTransitionMessageId),
11229
+ leaveProposalId
10811
11230
  };
10812
11231
  }
10813
11232
  function makeOwnerEpochRecord(manifest, epochSecret, fields = {}) {
@@ -10818,7 +11237,8 @@ function makeOwnerEpochRecord(manifest, epochSecret, fields = {}) {
10818
11237
  epochSecret: secretHex(epochSecret, "epoch secret"),
10819
11238
  cutoffMs: Number.isFinite(fields.cutoffMs) ? fields.cutoffMs : null,
10820
11239
  successorTransitionCommitment: fields.successorTransitionCommitment,
10821
- successorTransitionMessageId: fields.successorTransitionMessageId
11240
+ successorTransitionMessageId: fields.successorTransitionMessageId,
11241
+ leaveProposalId: fields.leaveProposalId
10822
11242
  });
10823
11243
  }
10824
11244
  async function sealOwnerEpochRecord(chatPrivateKey, entryId, epochEntryId, record) {
@@ -10842,7 +11262,7 @@ async function openOwnerEpochRecord(chatPrivateKey, entryId, epochEntryId, body)
10842
11262
  function ownEpochEntryId(chatPrivateKey, chatId, epochId) {
10843
11263
  return ownerEpochEntryId(chatPrivateKey, chatId, epochId);
10844
11264
  }
10845
- var CHAT_ENTRY_VERSION = 4, CHAT_OWNER_EPOCH_ENTRY_VERSION = 2;
11265
+ var CHAT_ENTRY_VERSION = 4, CHAT_OWNER_EPOCH_ENTRY_VERSION = 2, CHAT_OWNER_RETIREMENT_KINDS;
10846
11266
  var init_entry = __esm(() => {
10847
11267
  init_canonical();
10848
11268
  init_core();
@@ -10854,6 +11274,10 @@ var init_entry = __esm(() => {
10854
11274
  init_state2();
10855
11275
  init_settings2();
10856
11276
  "use client";
11277
+ CHAT_OWNER_RETIREMENT_KINDS = Object.freeze({
11278
+ LEAVE: "leave",
11279
+ REMOVED: "removed"
11280
+ });
10857
11281
  });
10858
11282
 
10859
11283
  // ../../core/chat/epochs/bootstrap.js
@@ -10994,6 +11418,32 @@ async function mutateOwnChatEntry(cloud, identity, entryId, current, update) {
10994
11418
  const committed = await cloud.user.chats.mutate(identity.uid, entryId, mutation);
10995
11419
  return committed?.result ?? mutation.result ?? null;
10996
11420
  }
11421
+ async function retireOwnChatEntry(cloud, identity, entryId, current, fields = {}) {
11422
+ const expectedEpochVersion = fields.expectedEpochVersion;
11423
+ const retirement = fields.retirement;
11424
+ if (!Number.isSafeInteger(expectedEpochVersion) || expectedEpochVersion < 1 || !retirement?.kind || !Number.isSafeInteger(retirement.epochVersion)) {
11425
+ throw new Error("chat owner retirement required");
11426
+ }
11427
+ return mutateOwnChatEntry(cloud, identity, entryId, current, (latest) => {
11428
+ if (latest?.retirement?.kind === retirement.kind && latest.retirement.epochVersion >= retirement.epochVersion) {
11429
+ return { result: latest };
11430
+ }
11431
+ if (latest?.retirement || latest?.current?.manifest?.epochVersion !== expectedEpochVersion) {
11432
+ throw new Error("chat owner epoch changed");
11433
+ }
11434
+ return {
11435
+ entry: {
11436
+ ...latest,
11437
+ routes: {},
11438
+ deliveryRegistered: false,
11439
+ notificationTag: null,
11440
+ retirement
11441
+ },
11442
+ epochs: fields.epoch ? [fields.epoch] : [],
11443
+ mlsDeletes: fields.mlsStateId ? [fields.mlsStateId] : []
11444
+ };
11445
+ });
11446
+ }
10997
11447
  async function openOwnChatMutationEntry(identity, entryId, record) {
10998
11448
  if (!record)
10999
11449
  return null;
@@ -11010,369 +11460,6 @@ var init_owner = __esm(() => {
11010
11460
  "use client";
11011
11461
  });
11012
11462
 
11013
- // ../../core/crypto/file.js
11014
- function createFileKey() {
11015
- return randomBytes3(32);
11016
- }
11017
- function encodeFileKey(key) {
11018
- return toHex(toBytes32(key, "file key"));
11019
- }
11020
- function decodeFileKey(key) {
11021
- return toBytes32(key, "file key");
11022
- }
11023
- function getFileAadForPath(scope) {
11024
- if (!scope) {
11025
- throw new Error("file scope required");
11026
- }
11027
- return encodeScope(FILE_SCOPE, [scope]);
11028
- }
11029
- function getSubtleCrypto() {
11030
- const subtle = globalThis.crypto?.subtle;
11031
- if (!subtle) {
11032
- throw new Error("WebCrypto AES-GCM unavailable");
11033
- }
11034
- return subtle;
11035
- }
11036
- async function importAesKey(key, usages) {
11037
- return getSubtleCrypto().importKey("raw", toBytes32(key, "file key"), { name: "AES-GCM" }, false, usages);
11038
- }
11039
- async function sealFile(key, bytes, scope) {
11040
- const fileKey = new Uint8Array(toBytes32(key, "file key"));
11041
- try {
11042
- const cryptoKey = await importAesKey(fileKey, ["encrypt"]);
11043
- const iv = randomBytes3(FILE_IV_BYTES);
11044
- const ct = await getSubtleCrypto().encrypt({
11045
- name: "AES-GCM",
11046
- iv,
11047
- tagLength: FILE_TAG_BYTES * 8,
11048
- additionalData: getFileAadForPath(scope)
11049
- }, cryptoKey, toBytes(bytes, "plaintext"));
11050
- return packBodyData(iv, new Uint8Array(ct));
11051
- } finally {
11052
- cleanBytes(fileKey);
11053
- }
11054
- }
11055
- async function openFileForPath(key, body, scope) {
11056
- const fileKey = new Uint8Array(toBytes32(key, "file key"));
11057
- try {
11058
- const cryptoKey = await importAesKey(fileKey, ["decrypt"]);
11059
- const { nonce, ct } = unpackBodyData(body, FILE_IV_BYTES);
11060
- const pt = await getSubtleCrypto().decrypt({
11061
- name: "AES-GCM",
11062
- iv: nonce,
11063
- tagLength: FILE_TAG_BYTES * 8,
11064
- additionalData: getFileAadForPath(scope)
11065
- }, cryptoKey, ct);
11066
- return new Uint8Array(pt);
11067
- } finally {
11068
- cleanBytes(fileKey);
11069
- }
11070
- }
11071
- var FILE_SCOPE = "file-body", FILE_IV_BYTES = 12, FILE_TAG_BYTES = 16;
11072
- var init_file = __esm(() => {
11073
- init_core();
11074
- init_kdf();
11075
- init_pack();
11076
- "use client";
11077
- });
11078
-
11079
- // ../../core/chat/filepayload.js
11080
- function cleanMediaEpochId(value) {
11081
- const epochId = String(value || "").trim();
11082
- if (!new RegExp(`^${CHAT_ID_PATTERN}$`).test(epochId)) {
11083
- throw new Error("invalid media epoch id");
11084
- }
11085
- return epochId.toLowerCase();
11086
- }
11087
- function cleanMediaId(value) {
11088
- const mediaId = String(value || "").trim();
11089
- if (!new RegExp(`^${MEDIA_ID_PATTERN}$`).test(mediaId)) {
11090
- throw new Error("invalid media id");
11091
- }
11092
- return mediaId.toLowerCase();
11093
- }
11094
- function cleanSharedMediaId(value) {
11095
- const sharedId = String(value || "").trim();
11096
- if (!new RegExp(`^${SHARED_MEDIA_ID_PATTERN}$`).test(sharedId)) {
11097
- throw new Error("invalid shared media id");
11098
- }
11099
- return sharedId.toLowerCase();
11100
- }
11101
- function makeChatMediaId() {
11102
- return toHex(randomBytes3(16));
11103
- }
11104
- function mediaFilePath(epochId, mediaId) {
11105
- const nextEpochId = cleanMediaEpochId(epochId);
11106
- const nextMediaId = cleanMediaId(mediaId);
11107
- return `${CHAT_MEDIA_ROOT}/${nextEpochId}/${nextMediaId}`;
11108
- }
11109
- function sharedMediaFilePath(sharedId) {
11110
- const nextSharedId = cleanSharedMediaId(sharedId);
11111
- return `${SHARED_MEDIA_ROOT}/${nextSharedId}`;
11112
- }
11113
- function makeSharedMediaId() {
11114
- return toHex(randomBytes3(16));
11115
- }
11116
- function getMediaFileRef(path) {
11117
- const value = String(path || "").trim();
11118
- const chatMatch = value.match(CHAT_MEDIA_FILE_PATTERN);
11119
- if (chatMatch?.[1] && chatMatch?.[2]) {
11120
- return {
11121
- type: "chat",
11122
- epochId: chatMatch[1].toLowerCase(),
11123
- mediaId: chatMatch[2].toLowerCase()
11124
- };
11125
- }
11126
- const sharedMatch = value.match(SHARED_MEDIA_FILE_PATTERN);
11127
- if (sharedMatch?.[1]) {
11128
- return {
11129
- type: "shared",
11130
- sharedId: sharedMatch[1].toLowerCase()
11131
- };
11132
- }
11133
- throw new Error("invalid media file path");
11134
- }
11135
- function getChatMediaFileRef(path) {
11136
- const ref = getMediaFileRef(path);
11137
- if (ref?.type !== "chat") {
11138
- throw new Error("invalid media file path");
11139
- }
11140
- return ref;
11141
- }
11142
- function getSharedMediaFileRef(path) {
11143
- const ref = getMediaFileRef(path);
11144
- if (ref?.type !== "shared") {
11145
- throw new Error("invalid shared media file path");
11146
- }
11147
- return ref;
11148
- }
11149
- function uploadByteLength(value) {
11150
- if (Number.isFinite(value?.byteLength)) {
11151
- return value.byteLength;
11152
- }
11153
- if (Number.isFinite(value?.size)) {
11154
- return value.size;
11155
- }
11156
- if (Number.isFinite(value)) {
11157
- return value;
11158
- }
11159
- return null;
11160
- }
11161
- function makeChatUploadTooLargeError(bytes, maxBytes = MAX_CHAT_UPLOAD_BYTES) {
11162
- const error = new Error("upload too large");
11163
- error.code = "upload-too-large";
11164
- error.maxBytes = maxBytes;
11165
- if (Number.isFinite(bytes)) {
11166
- error.bytes = bytes;
11167
- }
11168
- return error;
11169
- }
11170
- function assertChatUploadByteSize(bytes, maxBytes = MAX_CHAT_UPLOAD_BYTES) {
11171
- const length = uploadByteLength(bytes);
11172
- if (!Number.isFinite(length)) {
11173
- throw new Error("upload bytes required");
11174
- }
11175
- if (length <= 0 || length > maxBytes) {
11176
- throw makeChatUploadTooLargeError(length, maxBytes);
11177
- }
11178
- return length;
11179
- }
11180
- async function toUploadBytes(data) {
11181
- if (typeof Blob !== "undefined" && data instanceof Blob) {
11182
- if (typeof data.arrayBuffer === "function") {
11183
- return new Uint8Array(await data.arrayBuffer());
11184
- }
11185
- if (typeof FileReader !== "undefined") {
11186
- return new Promise((resolve, reject) => {
11187
- const reader = new FileReader;
11188
- reader.onload = () => resolve(new Uint8Array(reader.result));
11189
- reader.onerror = () => reject(reader.error || new Error("blob read failed"));
11190
- reader.readAsArrayBuffer(data);
11191
- });
11192
- }
11193
- if (typeof Response !== "undefined") {
11194
- return new Uint8Array(await new Response(data).arrayBuffer());
11195
- }
11196
- }
11197
- if (typeof data?.arrayBuffer === "function") {
11198
- return new Uint8Array(await data.arrayBuffer());
11199
- }
11200
- return toBytes(data, "upload bytes");
11201
- }
11202
- async function makeChatFileUploadPayload(epochId, cid, data, { cacheControl = "private, max-age=0, no-transform" } = {}) {
11203
- const nextEpochId = cleanMediaEpochId(epochId);
11204
- const mediaId = makeChatMediaId();
11205
- const path = mediaFilePath(nextEpochId, mediaId);
11206
- const expiresAt = Date.now() + CHAT_MEDIA_TTL_MS2;
11207
- const key = createFileKey();
11208
- try {
11209
- const uploadBytes = await toUploadBytes(data);
11210
- assertChatUploadByteSize(uploadBytes);
11211
- const body = await sealFile(key, uploadBytes, path);
11212
- assertChatUploadByteSize(body);
11213
- return {
11214
- epochId: nextEpochId,
11215
- mediaId,
11216
- path,
11217
- body,
11218
- metadata: {
11219
- contentType: "application/octet-stream",
11220
- cacheControl
11221
- },
11222
- file: {
11223
- p: path,
11224
- k: encodeFileKey(key),
11225
- x: expiresAt
11226
- }
11227
- };
11228
- } catch (error) {
11229
- if (error && typeof error === "object") {
11230
- error.path = error?.path || path;
11231
- error.cid = error?.cid || cid;
11232
- }
11233
- throw error;
11234
- } finally {
11235
- cleanBytes(key);
11236
- }
11237
- }
11238
- async function makeSharedFileUploadPayload(data, { contentType = "application/octet-stream", cacheControl = "private, max-age=0, no-transform" } = {}) {
11239
- const sharedId = makeSharedMediaId();
11240
- const path = sharedMediaFilePath(sharedId);
11241
- const expiresAt = Date.now() + CHAT_MEDIA_TTL_MS2;
11242
- const key = createFileKey();
11243
- try {
11244
- const uploadBytes = await toUploadBytes(data);
11245
- assertChatUploadByteSize(uploadBytes);
11246
- const body = await sealFile(key, uploadBytes, path);
11247
- assertChatUploadByteSize(body);
11248
- return {
11249
- sharedId,
11250
- path,
11251
- body,
11252
- metadata: {
11253
- contentType,
11254
- cacheControl
11255
- },
11256
- file: {
11257
- p: path,
11258
- k: encodeFileKey(key),
11259
- x: expiresAt
11260
- }
11261
- };
11262
- } catch (error) {
11263
- if (error && typeof error === "object") {
11264
- error.path = error?.path || path;
11265
- error.sharedId = error?.sharedId || sharedId;
11266
- }
11267
- throw error;
11268
- } finally {
11269
- cleanBytes(key);
11270
- }
11271
- }
11272
- var CHAT_MEDIA_ROOT = "chatEpochs", SHARED_MEDIA_ROOT = "shared", CHAT_MEDIA_TTL_MS2, MAX_CHAT_UPLOAD_BYTES, CHAT_VIDEO_TRANSCODE_VIDEO_BITRATE_BPS2, CHAT_VIDEO_TRANSCODE_AUDIO_BITRATE_BPS2, CHAT_VIDEO_TRANSCODE_TOTAL_BITRATE_BPS, CHAT_ID_PATTERN = "[0-9a-fA-F]{64}", SHARED_MEDIA_ID_PATTERN = "[0-9a-fA-F]{32}", MEDIA_ID_PATTERN = "[0-9a-fA-F]{32}", CHAT_MEDIA_FILE_PATTERN, SHARED_MEDIA_FILE_PATTERN;
11273
- var init_filepayload = __esm(() => {
11274
- init_file();
11275
- init_config();
11276
- init_core();
11277
- "use client";
11278
- CHAT_MEDIA_TTL_MS2 = CHAT_MEDIA_TTL_MS;
11279
- MAX_CHAT_UPLOAD_BYTES = CHAT_UPLOAD_MAX_BYTES;
11280
- CHAT_VIDEO_TRANSCODE_VIDEO_BITRATE_BPS2 = CHAT_VIDEO_TRANSCODE_VIDEO_BITRATE_BPS;
11281
- CHAT_VIDEO_TRANSCODE_AUDIO_BITRATE_BPS2 = CHAT_VIDEO_TRANSCODE_AUDIO_BITRATE_BPS;
11282
- CHAT_VIDEO_TRANSCODE_TOTAL_BITRATE_BPS = CHAT_VIDEO_TRANSCODE_VIDEO_BITRATE_BPS2 + CHAT_VIDEO_TRANSCODE_AUDIO_BITRATE_BPS2;
11283
- CHAT_MEDIA_FILE_PATTERN = new RegExp(`^${CHAT_MEDIA_ROOT}/(${CHAT_ID_PATTERN})/(${MEDIA_ID_PATTERN})$`);
11284
- SHARED_MEDIA_FILE_PATTERN = new RegExp(`^${SHARED_MEDIA_ROOT}/(${SHARED_MEDIA_ID_PATTERN})$`);
11285
- });
11286
-
11287
- // ../../core/files.js
11288
- function makeFileId(size = 8) {
11289
- return toHex(randomBytes3(size));
11290
- }
11291
- function setErrorStage(error, stage, extra = {}) {
11292
- if (!error || typeof error !== "object") {
11293
- return error;
11294
- }
11295
- error.stage = error?.stage || stage;
11296
- Object.assign(error, extra);
11297
- return error;
11298
- }
11299
- async function makeChatFileUpload(epochId, cid, data, { cacheControl = "private, max-age=0, no-transform" } = {}) {
11300
- try {
11301
- return await makeChatFileUploadPayload(epochId, cid, data, {
11302
- cacheControl
11303
- });
11304
- } catch (error) {
11305
- throw setErrorStage(error, "encrypt", {
11306
- ...error?.path ? { path: error.path } : {},
11307
- cid
11308
- });
11309
- }
11310
- }
11311
- async function putChatFile(epochId, cid, data, options) {
11312
- const upload = await makeChatFileUpload(epochId, cid, data, options);
11313
- try {
11314
- if (typeof options?.uploadChatMedia !== "function") {
11315
- throw new Error("chat media upload required");
11316
- }
11317
- await options.uploadChatMedia(upload);
11318
- return upload.file;
11319
- } catch (error) {
11320
- throw setErrorStage(error, "upload", { path: upload.path, cid });
11321
- }
11322
- }
11323
- async function makeSharedFileUpload(data, { contentType = "application/octet-stream", cacheControl = "private, max-age=0, no-transform" } = {}) {
11324
- try {
11325
- return await makeSharedFileUploadPayload(data, {
11326
- contentType,
11327
- cacheControl
11328
- });
11329
- } catch (error) {
11330
- throw setErrorStage(error, "encrypt", {
11331
- ...error?.path ? { path: error.path } : {}
11332
- });
11333
- }
11334
- }
11335
- async function putSharedFile(data, options = {}) {
11336
- const upload = await makeSharedFileUpload(data, options);
11337
- try {
11338
- if (typeof options?.uploadSharedMedia !== "function") {
11339
- throw new Error("shared media upload required");
11340
- }
11341
- await options.uploadSharedMedia(upload);
11342
- return upload.file;
11343
- } catch (error) {
11344
- throw setErrorStage(error, "upload", { path: upload.path });
11345
- }
11346
- }
11347
- async function readChatFile(readChatMedia, file) {
11348
- let body;
11349
- try {
11350
- if (typeof readChatMedia !== "function") {
11351
- throw new Error("chat media read required");
11352
- }
11353
- body = await readChatMedia(file?.p);
11354
- } catch (error) {
11355
- error.path = error?.path || file?.p || null;
11356
- error.stage = error?.stage || "download";
11357
- throw error;
11358
- }
11359
- try {
11360
- getMediaFileRef(file?.p);
11361
- const bytes = await openFileForPath(decodeFileKey(file?.k), body, file?.p);
11362
- return bytes;
11363
- } catch (error) {
11364
- error.path = file?.p || null;
11365
- error.stage = "decrypt";
11366
- throw error;
11367
- }
11368
- }
11369
- var init_files = __esm(() => {
11370
- init_file();
11371
- init_core();
11372
- init_filepayload();
11373
- "use client";
11374
- });
11375
-
11376
11463
  // ../../core/chat/messages/types.js
11377
11464
  var ATTACHMENT_MSG_TYPES, MAX_TXT_CHARS, REACTION_MSG_TYPE = "rxn", DELETE_MSG_TYPE = "del", SYSTEM_MSG_TYPE = "sys", EPOCH_TRANSITION_MSG_TYPE = "epoch", EPOCH_PROPOSAL_MSG_TYPE = "epoch_proposal", SYSTEM_SETTINGS_KIND = "settings", DEFAULT_REACTION_EMOJI = "❤️", MAX_REACTIONS, HOLD_VISIBLE_KEY = "__holdVisible", SOURCE_GONE_VISIBLE_KEY = "__sourceGoneVisible";
11378
11465
  var init_types = __esm(() => {
@@ -12079,48 +12166,6 @@ function cleanFrontier(value) {
12079
12166
  throw new Error("chat receipt checkpoint frontier time required");
12080
12167
  return Object.freeze({ id, at: value.at, seenAt: value.seenAt });
12081
12168
  }
12082
- function maskBytes(memberCount2) {
12083
- return Math.ceil(memberCount2 / 8);
12084
- }
12085
- function cleanMask(value, memberCount2, label) {
12086
- const mask = cleanText(value).toLowerCase();
12087
- const size = maskBytes(memberCount2);
12088
- if (!new RegExp(`^[0-9a-f]{${size * 2}}$`, "u").test(mask)) {
12089
- throw new Error(`${label} invalid`);
12090
- }
12091
- const unused = size * 8 - memberCount2;
12092
- if (unused > 0 && Number.parseInt(mask.slice(-2), 16) & (1 << unused) - 1 << 8 - unused) {
12093
- throw new Error(`${label} overflow`);
12094
- }
12095
- return mask;
12096
- }
12097
- function makeMask(indices, memberCount2) {
12098
- const bytes = new Uint8Array(maskBytes(memberCount2));
12099
- for (const index of indices)
12100
- bytes[Math.floor(index / 8)] |= 1 << index % 8;
12101
- return [...bytes].map((value) => value.toString(16).padStart(2, "0")).join("");
12102
- }
12103
- function maskHas(mask, index) {
12104
- const offset = Math.floor(index / 8) * 2;
12105
- return (Number.parseInt(mask.slice(offset, offset + 2), 16) & 1 << index % 8) !== 0;
12106
- }
12107
- function maskCount(mask, memberCount2) {
12108
- let count = 0;
12109
- for (let index = 0;index < memberCount2; index += 1) {
12110
- if (maskHas(mask, index))
12111
- count += 1;
12112
- }
12113
- return count;
12114
- }
12115
- function frontierWithSeenAt(frontier, states) {
12116
- if (!frontier)
12117
- return null;
12118
- return {
12119
- id: frontier.id,
12120
- at: frontier.at,
12121
- seenAt: Math.max(frontier.seenAt, ...states.map((state) => Number(state?.readFrontier?.seenAt) || 0))
12122
- };
12123
- }
12124
12169
  function normalizeChatReceiptProjection(value) {
12125
12170
  if (!value || typeof value !== "object" || Array.isArray(value)) {
12126
12171
  throw new Error("chat receipt checkpoint projection required");
@@ -12129,41 +12174,13 @@ function normalizeChatReceiptProjection(value) {
12129
12174
  if (!Array.isArray(value.revisions) || value.revisions.length !== memberCount2) {
12130
12175
  throw new Error("chat receipt checkpoint revisions invalid");
12131
12176
  }
12132
- const lowFrontier = cleanFrontier(value.lowFrontier);
12133
- const nextLowFrontier = cleanFrontier(value.nextLowFrontier);
12134
- const highFrontier = cleanFrontier(value.highFrontier);
12135
- const nextHighFrontier = cleanFrontier(value.nextHighFrontier);
12136
- const lowMask = cleanMask(value.lowMask, memberCount2, "chat receipt checkpoint low mask");
12137
- const highMask = cleanMask(value.highMask, memberCount2, "chat receipt checkpoint high mask");
12138
- const nextHighMask = cleanMask(value.nextHighMask, memberCount2, "chat receipt checkpoint next high mask");
12139
- if (!highFrontier && maskCount(highMask, memberCount2) !== 0) {
12140
- throw new Error("chat receipt checkpoint high frontier missing");
12141
- }
12142
- if (highFrontier && maskCount(highMask, memberCount2) === 0) {
12143
- throw new Error("chat receipt checkpoint high readers missing");
12144
- }
12145
- if (!nextHighFrontier && maskCount(nextHighMask, memberCount2) !== 0) {
12146
- throw new Error("chat receipt checkpoint next high frontier missing");
12147
- }
12148
- if (nextHighFrontier && maskCount(nextHighMask, memberCount2) === 0) {
12149
- throw new Error("chat receipt checkpoint next high readers missing");
12150
- }
12151
- if (lowFrontier && compareReadFrontiers(lowFrontier, highFrontier) > 0) {
12152
- throw new Error("chat receipt checkpoint frontier order invalid");
12153
- }
12154
- if (nextLowFrontier && compareReadFrontiers(nextLowFrontier, highFrontier) > 0) {
12155
- throw new Error("chat receipt checkpoint next frontier order invalid");
12177
+ if (!Array.isArray(value.frontiers) || value.frontiers.length !== memberCount2) {
12178
+ throw new Error("chat receipt checkpoint frontiers invalid");
12156
12179
  }
12157
12180
  return Object.freeze({
12158
12181
  memberCount: memberCount2,
12159
12182
  revisions: Object.freeze(value.revisions.map(cleanRevision3)),
12160
- lowFrontier,
12161
- lowMask,
12162
- nextLowFrontier,
12163
- highFrontier,
12164
- highMask,
12165
- nextHighFrontier,
12166
- nextHighMask
12183
+ frontiers: Object.freeze(value.frontiers.map(cleanFrontier))
12167
12184
  });
12168
12185
  }
12169
12186
  function buildChatReceiptProjection(manifest, states) {
@@ -12171,36 +12188,66 @@ function buildChatReceiptProjection(manifest, states) {
12171
12188
  const memberCount2 = cleanCount(members.length);
12172
12189
  const values = members.map((member) => states?.get?.(member.chatPK) || null);
12173
12190
  const revisions = values.map((state) => Math.max(0, Number(state?.revision) || 0));
12174
- const missing = [];
12175
- const present = [];
12176
- values.forEach((state, index) => {
12177
- if (state?.readFrontier)
12178
- present.push({ index, state, frontier: state.readFrontier });
12179
- else
12180
- missing.push(index);
12181
- });
12182
- present.sort((left, right) => compareReadFrontiers(left.frontier, right.frontier));
12183
- const minimum = present[0]?.frontier || null;
12184
- const maximum = present.at(-1)?.frontier || null;
12185
- const lowIndices = missing.length ? missing : present.filter((item) => compareReadFrontiers(item.frontier, minimum) === 0).map((item) => item.index);
12186
- const nextLowItems = missing.length ? present : present.filter((item) => compareReadFrontiers(item.frontier, minimum) > 0);
12187
- const nextLow = nextLowItems[0]?.frontier || null;
12188
- const highItems = maximum ? present.filter((item) => compareReadFrontiers(item.frontier, maximum) === 0) : [];
12189
- const belowHigh = maximum ? present.filter((item) => compareReadFrontiers(item.frontier, maximum) < 0) : [];
12190
- const nextHigh = belowHigh.at(-1)?.frontier || null;
12191
- const nextHighItems = nextHigh ? belowHigh.filter((item) => compareReadFrontiers(item.frontier, nextHigh) === 0) : [];
12192
12191
  return normalizeChatReceiptProjection({
12193
12192
  memberCount: memberCount2,
12194
12193
  revisions,
12195
- lowFrontier: missing.length ? null : frontierWithSeenAt(minimum, values.filter(Boolean)),
12196
- lowMask: makeMask(lowIndices, memberCount2),
12197
- nextLowFrontier: frontierWithSeenAt(nextLow, nextLowItems.map((item) => item.state)),
12198
- highFrontier: frontierWithSeenAt(maximum, highItems.map((item) => item.state)),
12199
- highMask: makeMask(highItems.map((item) => item.index), memberCount2),
12200
- nextHighFrontier: frontierWithSeenAt(nextHigh, nextHighItems.map((item) => item.state)),
12201
- nextHighMask: makeMask(nextHighItems.map((item) => item.index), memberCount2)
12194
+ frontiers: values.map((state) => state?.readFrontier || null)
12202
12195
  });
12203
12196
  }
12197
+ function mergeChatReceiptProjections(current, incoming) {
12198
+ if (!current)
12199
+ return normalizeChatReceiptProjection(incoming);
12200
+ if (!incoming)
12201
+ return normalizeChatReceiptProjection(current);
12202
+ const left = normalizeChatReceiptProjection(current);
12203
+ const right = normalizeChatReceiptProjection(incoming);
12204
+ if (left.memberCount !== right.memberCount) {
12205
+ throw new Error("chat receipt checkpoint member count mismatch");
12206
+ }
12207
+ return normalizeChatReceiptProjection({
12208
+ memberCount: left.memberCount,
12209
+ revisions: left.revisions.map((revision, index) => Math.max(revision, right.revisions[index])),
12210
+ frontiers: left.frontiers.map((frontier, index) => {
12211
+ const other = right.frontiers[index];
12212
+ const comparison = compareReadFrontiers(frontier, other);
12213
+ if (comparison > 0)
12214
+ return frontier;
12215
+ if (comparison < 0)
12216
+ return other;
12217
+ if (!frontier)
12218
+ return null;
12219
+ return {
12220
+ ...frontier,
12221
+ seenAt: Math.max(frontier.seenAt, other.seenAt)
12222
+ };
12223
+ })
12224
+ });
12225
+ }
12226
+ function mergeChatReceiptCheckpointsByEpoch(current, incoming) {
12227
+ if (!(incoming instanceof Map) || !incoming.size) {
12228
+ return current instanceof Map ? current : new Map;
12229
+ }
12230
+ if (!(current instanceof Map) || !current.size)
12231
+ return new Map(incoming);
12232
+ const merged = new Map(current instanceof Map ? current : []);
12233
+ let changed = false;
12234
+ for (const [epochId, value] of incoming instanceof Map ? incoming : []) {
12235
+ const existing = merged.get(epochId);
12236
+ if (existing === value)
12237
+ continue;
12238
+ 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]);
12239
+ const next = exact ? Object.freeze({
12240
+ ...value,
12241
+ publishedAt: Math.max(Number(existing.publishedAt) || 0, Number(value.publishedAt) || 0) || null,
12242
+ projection: mergeChatReceiptProjections(existing.projection, value.projection)
12243
+ }) : value;
12244
+ if (exact && sameChatReceiptCheckpoint(existing, next) && existing.publishedAt === next.publishedAt)
12245
+ continue;
12246
+ merged.set(epochId, next);
12247
+ changed = true;
12248
+ }
12249
+ return changed ? merged : current;
12250
+ }
12204
12251
  function projectChatReceiptCheckpoint(manifest, value) {
12205
12252
  const projection = normalizeChatReceiptProjection(value?.projection || value);
12206
12253
  if (projection.memberCount !== manifest?.members?.length) {
@@ -12233,13 +12280,14 @@ function chatReceiptCheckpointCoversState(checkpoint, manifest, state) {
12233
12280
  function chatReceiptAllSeenFrontier(checkpoint, authorChatPK) {
12234
12281
  if (!isChatReceiptCheckpoint(checkpoint))
12235
12282
  return null;
12236
- const { lowFrontier, lowMask, nextLowFrontier, memberCount: memberCount2 } = checkpoint.projection;
12237
- const authorIndex = checkpoint.memberChatPKs?.indexOf?.(authorChatPK) ?? -1;
12238
- const lowCount = maskCount(lowMask, memberCount2);
12239
- if (authorIndex >= 0 && lowCount === 1 && maskHas(lowMask, authorIndex)) {
12240
- return nextLowFrontier;
12241
- }
12242
- return lowFrontier;
12283
+ const readers = checkpoint.projection.frontiers.filter((_frontier, index) => checkpoint.memberChatPKs[index] !== authorChatPK);
12284
+ if (!readers.length || readers.some((frontier) => !frontier))
12285
+ return null;
12286
+ const minimum = readers.reduce((current, frontier) => !current || compareReadFrontiers(frontier, current) < 0 ? frontier : current, null);
12287
+ return Object.freeze({
12288
+ ...minimum,
12289
+ seenAt: Math.max(...readers.map((frontier) => frontier.seenAt))
12290
+ });
12243
12291
  }
12244
12292
  function chatReceiptProjectionFingerprint(value) {
12245
12293
  return JSON.stringify(normalizeChatReceiptProjection(value));
@@ -12320,7 +12368,7 @@ function cleanChatSettingsEventTitle(value) {
12320
12368
  if (value == null)
12321
12369
  return null;
12322
12370
  const title = cleanText(value);
12323
- if (!title || title.length > 128)
12371
+ if (!title || title.length > CHAT_TITLE_MAX_CHARS)
12324
12372
  throw new Error("invalid chat settings event title");
12325
12373
  return title;
12326
12374
  }
@@ -12335,10 +12383,7 @@ function makeChatSettingsMsg(revision, digest, changes = [], values = {}) {
12335
12383
  const titleChanged = cleanChanges.includes("title");
12336
12384
  const avatarChanged = cleanChanges.includes("avatarRef");
12337
12385
  const title = titleChanged ? cleanChatSettingsEventTitle(values?.title) : undefined;
12338
- const avatarRef = avatarChanged ? values?.avatarRef == null ? null : chatAvatarDataUrl(values.avatarRef) : undefined;
12339
- if (avatarChanged && values?.avatarRef != null && !avatarRef) {
12340
- throw new Error("invalid chat settings event avatar");
12341
- }
12386
+ const avatarRef = avatarChanged ? values?.avatarRef == null ? null : cleanChatAvatarRef(values.avatarRef) : undefined;
12342
12387
  return {
12343
12388
  t: SYSTEM_MSG_TYPE,
12344
12389
  sys: SYSTEM_SETTINGS_KIND,
@@ -12361,7 +12406,7 @@ function isChatSettingsMsg(msg) {
12361
12406
  return false;
12362
12407
  }
12363
12408
  }
12364
- return !changes.includes("avatarRef") || msg?.avatarRef == null || !!chatAvatarDataUrl(msg.avatarRef);
12409
+ return !changes.includes("avatarRef") || msg?.avatarRef == null || !!chatAvatarFile(msg.avatarRef);
12365
12410
  }
12366
12411
  function isPendingChatSettingsMsg(msg) {
12367
12412
  return msg?.t === SYSTEM_MSG_TYPE && msg?.sys === SYSTEM_SETTINGS_KIND && msg?.pending === true && String(msg?.id || "").startsWith("local:") && chatSettingsEventChanges(msg).length === 1 && chatSettingsEventChanges(msg)[0] === "retention";
@@ -12369,7 +12414,7 @@ function isPendingChatSettingsMsg(msg) {
12369
12414
  function getChatSettingsEventAvatar(msg) {
12370
12415
  if (!isChatSettingsMsg(msg) || !chatSettingsEventChanges(msg).includes("avatarRef"))
12371
12416
  return "";
12372
- return chatAvatarDataUrl(msg?.avatarRef);
12417
+ return chatAvatarFile(msg?.avatarRef) ? msg.avatarRef : "";
12373
12418
  }
12374
12419
  function isSystemMsg(msg) {
12375
12420
  return !!getSystemMsgText(msg) && (msg?.t === SYSTEM_MSG_TYPE || isMembershipEventMsg(msg));
@@ -12601,7 +12646,7 @@ function getLatestLocalReadTarget(messages, chatPK) {
12601
12646
  function getLatestReadStateTarget(messages) {
12602
12647
  for (let i = (messages?.length || 0) - 1;i >= 0; i -= 1) {
12603
12648
  const message = messages[i];
12604
- if (isServerConfirmedMsg(message) && canShowMsg(message) && !isSystemMsg(message))
12649
+ if (isServerConfirmedMsg(message) && !isSystemMsg(message) && (canShowMsg(message) || isReactionMsg(message)))
12605
12650
  return message;
12606
12651
  }
12607
12652
  return null;
@@ -12734,6 +12779,7 @@ var init_control = __esm(() => {
12734
12779
  init_messagekeys();
12735
12780
  init_ttl();
12736
12781
  init_actions();
12782
+ init_config();
12737
12783
  init_avatar2();
12738
12784
  init_files2();
12739
12785
  init_text();
@@ -13240,13 +13286,12 @@ function withChatPreviewExpired(preview, expiredAt) {
13240
13286
  sourceTs: getChatPreviewSourceTs(preview)
13241
13287
  }));
13242
13288
  }
13243
- function chatPreviewWantsAttention(preview, chatPK) {
13289
+ function chatPreviewCountsAsUnseen(preview, chatPK) {
13244
13290
  if (!preview || fromSelf(preview, chatPK)) {
13245
13291
  return false;
13246
13292
  }
13247
13293
  if (isReactionMsg(preview)) {
13248
- const targetFrom = cleanText(preview?.targetFrom);
13249
- return !!cleanText(preview?.emoji) && (!targetFrom || targetFrom === chatPK);
13294
+ return !!cleanText(preview?.emoji);
13250
13295
  }
13251
13296
  return canRenderPreviewContent(preview);
13252
13297
  }
@@ -14751,10 +14796,6 @@ async function prepareOwnerChatMlsSnapshot(identity, entryId, epochState, snapsh
14751
14796
  function ownerChatMlsSnapshotId(identity, entryId, epochState) {
14752
14797
  return stateAddress(identity, entryId, epochState).stateId;
14753
14798
  }
14754
- async function discardOwnerChatMlsSnapshot(cloud, identity, entryId, epochState) {
14755
- const address = stateAddress(identity, entryId, epochState);
14756
- await cloud.user.chats.mls.delete(identity.uid, entryId, address.stateId).catch(() => false);
14757
- }
14758
14799
  var init_owner2 = __esm(() => {
14759
14800
  init_state3();
14760
14801
  "use client";
@@ -15925,7 +15966,7 @@ function getPeersFromChats(chats) {
15925
15966
  }
15926
15967
  function isChatUnseenForUser(chatData, userChatPK) {
15927
15968
  const last = chatData?.preview;
15928
- if (!last?.ts || !chatPreviewWantsAttention(last, userChatPK))
15969
+ if (!last?.ts || !chatPreviewCountsAsUnseen(last, userChatPK))
15929
15970
  return false;
15930
15971
  const readMs = timestampMs(chatData?.readMs, null);
15931
15972
  const lastMs = timestampMs(last.ts, null);
@@ -16560,7 +16601,45 @@ function reviveMarker(marker) {
16560
16601
  function serializeKeys(keys) {
16561
16602
  return [...keySet(keys)].filter(Boolean).sort();
16562
16603
  }
16563
- function serializeBatch(batch, { expiredKeys, deletedKeys } = {}) {
16604
+ function cleanReceiptCheckpoint(value) {
16605
+ if (!isChatReceiptCheckpoint(value))
16606
+ return null;
16607
+ const epochId = cleanText(value.epochId);
16608
+ const memberChatPKs = (value.memberChatPKs || []).map(cleanText).filter(Boolean);
16609
+ if (!epochId || memberChatPKs.length !== value.memberChatPKs?.length)
16610
+ return null;
16611
+ try {
16612
+ return projectChatReceiptCheckpoint({
16613
+ epochId,
16614
+ members: memberChatPKs.map((chatPK) => ({ chatPK }))
16615
+ }, value);
16616
+ } catch {
16617
+ return null;
16618
+ }
16619
+ }
16620
+ function serializeReceiptCheckpoints(statesByEpoch, epochIds) {
16621
+ const allowed = new Set(epochIds || []);
16622
+ const checkpoints = [];
16623
+ for (const [epochId, value] of statesByEpoch instanceof Map ? statesByEpoch : []) {
16624
+ if (allowed.size && !allowed.has(epochId))
16625
+ continue;
16626
+ const checkpoint = cleanReceiptCheckpoint(value);
16627
+ if (checkpoint)
16628
+ checkpoints.push(checkpoint);
16629
+ }
16630
+ return checkpoints.sort((left, right) => left.epochId.localeCompare(right.epochId));
16631
+ }
16632
+ function reviveReceiptCheckpoints(values) {
16633
+ return new Map((Array.isArray(values) ? values : []).map((value) => cleanReceiptCheckpoint(value)).filter(Boolean).map((checkpoint) => [checkpoint.epochId, checkpoint]));
16634
+ }
16635
+ function receiptEpochIds(messages, fallbackEpochId = "") {
16636
+ const ids = new Set((messages || []).map((message) => cleanText(message?.epochId)).filter(Boolean));
16637
+ const fallback = cleanText(fallbackEpochId);
16638
+ if (fallback)
16639
+ ids.add(fallback);
16640
+ return ids;
16641
+ }
16642
+ function serializeBatch(batch, { expiredKeys, deletedKeys, epochIds } = {}) {
16564
16643
  if (!batch && !expiredKeys?.size && !deletedKeys?.size) {
16565
16644
  return null;
16566
16645
  }
@@ -16570,14 +16649,16 @@ function serializeBatch(batch, { expiredKeys, deletedKeys } = {}) {
16570
16649
  ...Number.isSafeInteger(batch?.epochVersion) ? { epochVersion: batch.epochVersion } : {},
16571
16650
  ...Number.isFinite(batch?.firstMs) ? { firstMs: batch.firstMs } : {},
16572
16651
  ...Number.isFinite(batch?.lastMs) ? { lastMs: batch.lastMs } : {},
16652
+ receiptCheckpoints: serializeReceiptCheckpoints(batch?.memberStatesByEpoch, epochIds),
16573
16653
  expiredKeys: serializeKeys(expiredKeys || batch?.expiredKeys),
16574
16654
  deletedKeys: serializeKeys(deletedKeys || batch?.deletedKeys)
16575
16655
  };
16576
16656
  }
16577
- function batchFromLive(latestMessages, expiredKeys, deletedKeys, fallback) {
16657
+ function batchFromLive(latestMessages, expiredKeys, deletedKeys, fallback, receiptMessages = latestMessages) {
16578
16658
  const source = latestMessages || [];
16659
+ const epochIds = receiptEpochIds(receiptMessages, fallback?.epochId);
16579
16660
  if (!source.length) {
16580
- return serializeBatch(fallback, { expiredKeys, deletedKeys });
16661
+ return serializeBatch(fallback, { expiredKeys, deletedKeys, epochIds });
16581
16662
  }
16582
16663
  const firstMs = getMessageOrderMs(source[0]);
16583
16664
  const lastMs = getMessageOrderMs(source[source.length - 1]);
@@ -16585,8 +16666,9 @@ function batchFromLive(latestMessages, expiredKeys, deletedKeys, fallback) {
16585
16666
  firstMs,
16586
16667
  lastMs,
16587
16668
  epochId: fallback?.epochId,
16588
- epochVersion: fallback?.epochVersion
16589
- } : fallback, { expiredKeys, deletedKeys });
16669
+ epochVersion: fallback?.epochVersion,
16670
+ memberStatesByEpoch: fallback?.memberStatesByEpoch
16671
+ } : fallback, { expiredKeys, deletedKeys, epochIds });
16590
16672
  }
16591
16673
  function reviveBatch(batch, expiredKeys, deletedKeys) {
16592
16674
  const hasRange = Number.isFinite(batch?.firstMs) && Number.isFinite(batch?.lastMs);
@@ -16598,6 +16680,7 @@ function reviveBatch(batch, expiredKeys, deletedKeys) {
16598
16680
  ...cleanText(batch?.epochId) ? { epochId: cleanText(batch.epochId) } : {},
16599
16681
  ...Number.isSafeInteger(batch?.epochVersion) ? { epochVersion: batch.epochVersion } : {},
16600
16682
  ...hasRange ? { firstMs: batch.firstMs, lastMs: batch.lastMs } : {},
16683
+ memberStatesByEpoch: reviveReceiptCheckpoints(batch?.receiptCheckpoints),
16601
16684
  expiredKeys,
16602
16685
  deletedKeys
16603
16686
  };
@@ -16614,10 +16697,20 @@ function mergeBatches(left, right) {
16614
16697
  ...Number.isSafeInteger(right?.epochVersion ?? left?.epochVersion) ? { epochVersion: right?.epochVersion ?? left?.epochVersion } : {},
16615
16698
  ...firstValues.length ? { firstMs: Math.min(...firstValues) } : {},
16616
16699
  ...lastValues.length ? { lastMs: Math.max(...lastValues) } : {},
16700
+ memberStatesByEpoch: mergeChatReceiptCheckpointsByEpoch(left?.memberStatesByEpoch, right?.memberStatesByEpoch),
16617
16701
  expiredKeys: keySet([...left?.expiredKeys || [], ...right?.expiredKeys || []]),
16618
16702
  deletedKeys: keySet([...left?.deletedKeys || [], ...right?.deletedKeys || []])
16619
16703
  };
16620
16704
  }
16705
+ function cachedReceiptCheckpointSignature(statesByEpoch) {
16706
+ const values = [];
16707
+ for (const [epochId, value] of statesByEpoch instanceof Map ? statesByEpoch : []) {
16708
+ const checkpoint = cleanReceiptCheckpoint(value);
16709
+ if (checkpoint)
16710
+ values.push(`${epochId}:${chatReceiptProjectionFingerprint(checkpoint.projection)}`);
16711
+ }
16712
+ return values.sort().join("|");
16713
+ }
16621
16714
  function olderMarker(left, right) {
16622
16715
  if (!left)
16623
16716
  return right || null;
@@ -17020,7 +17113,7 @@ function writeCachedMessageState(cache, { stateVersion = null, chatId, selfChatP
17020
17113
  historyStartReached: nextHistoryStartReached === true,
17021
17114
  olderThan: serializeMarker(storedOlderThan),
17022
17115
  olderLoaded: nextOlderLoaded === true || compacted.historyMessages.length > 0,
17023
- latestServerPage: batchFromLive(compacted.latestMessages, expiredKeys, deletedKeys, nextServerBatch),
17116
+ latestServerPage: batchFromLive(compacted.latestMessages, expiredKeys, deletedKeys, nextServerBatch, compacted.all),
17024
17117
  deletedKeys: serializeKeys(deletedKeys),
17025
17118
  expiredKeys: serializeKeys(expiredKeys),
17026
17119
  historyMessages: compacted.historyMessages,
@@ -17049,6 +17142,7 @@ var init_messages2 = __esm(() => {
17049
17142
  init_state();
17050
17143
  init_messagekeys();
17051
17144
  init_compact();
17145
+ init_receipt();
17052
17146
  init_filename();
17053
17147
  init_time();
17054
17148
  init_core();
@@ -18272,11 +18366,13 @@ function createChatDelete({
18272
18366
  listActionsRef,
18273
18367
  closeMessageBatchRef,
18274
18368
  mutationGate,
18369
+ reconcileEpoch,
18275
18370
  diag
18276
18371
  }) {
18277
18372
  const pendingDeleteIdsRef = { current: new Set };
18278
18373
  const locallyDeletedChatIdsRef = { current: new Set };
18279
18374
  const deletedChatIdsRef = { current: new Set };
18375
+ const removedChatVersionsRef = { current: new Map };
18280
18376
  const keepSelectedDeletedChatIdsRef = { current: new Set };
18281
18377
  const isChatPendingDelete = (chatId) => !!chatId && pendingDeleteIdsRef.current.has(chatId);
18282
18378
  const clearDeletedChatState = (chatIds) => {
@@ -18386,20 +18482,46 @@ function createChatDelete({
18386
18482
  listActionsRef.current?.commitServerChats?.(nextServerChats, { warm: false });
18387
18483
  setSelectedChat((current) => current === chatId ? null : current);
18388
18484
  };
18389
- const dropRemovedChat = (chatId) => {
18390
- if (!chatId)
18485
+ const dropRemovedChat = (chatId, epochVersion) => {
18486
+ if (!chatId || !Number.isSafeInteger(epochVersion) || epochVersion < 1)
18391
18487
  return false;
18392
- deletedChatIdsRef.current.add(chatId);
18393
- locallyDeletedChatIdsRef.current.add(chatId);
18488
+ const previousVersion = removedChatVersionsRef.current.get(chatId) || 0;
18489
+ removedChatVersionsRef.current.set(chatId, Math.max(previousVersion, epochVersion));
18394
18490
  dropChat(chatId);
18395
18491
  return true;
18396
18492
  };
18493
+ const commitRemovedRetirement = (chatId, epochVersion) => {
18494
+ if (!chatId || !Number.isSafeInteger(epochVersion) || epochVersion < 1)
18495
+ return false;
18496
+ const previousVersion = removedChatVersionsRef.current.get(chatId) || 0;
18497
+ removedChatVersionsRef.current.set(chatId, Math.max(previousVersion, epochVersion));
18498
+ pendingDeleteIdsRef.current.delete(chatId);
18499
+ locallyDeletedChatIdsRef.current.delete(chatId);
18500
+ deletedChatIdsRef.current.delete(chatId);
18501
+ keepSelectedDeletedChatIdsRef.current.delete(chatId);
18502
+ confirmDeletedChats([chatId]);
18503
+ return true;
18504
+ };
18505
+ const restoreRemovedChat = (chat) => {
18506
+ const chatId = chat?.id;
18507
+ const removedAtVersion = removedChatVersionsRef.current.get(chatId);
18508
+ if (!chatId || removedAtVersion == null)
18509
+ return false;
18510
+ if (!Number.isSafeInteger(chat.epochVersion) || chat.epochVersion <= removedAtVersion || !Array.isArray(chat.memberChatPKs) || !chat.memberChatPKs.includes(chatPK))
18511
+ return false;
18512
+ removedChatVersionsRef.current.delete(chatId);
18513
+ return true;
18514
+ };
18397
18515
  const dropUnavailableChat = (chatId) => {
18398
18516
  if (!chatId)
18399
18517
  return Promise.resolve(false);
18400
18518
  return (async () => {
18401
18519
  const releaseMutation = await mutationGate?.acquireExclusive(chatId) || (() => {});
18402
18520
  try {
18521
+ if (removedChatVersionsRef.current.has(chatId)) {
18522
+ dropChat(chatId);
18523
+ return true;
18524
+ }
18403
18525
  const chat = lastServerChatsRef.current.find((item) => item?.id === chatId);
18404
18526
  if (chat?.deliveryRegistered && chat?.epochVersion && chatPK && chat?.epochState?.stateCapability) {
18405
18527
  await cloud.delivery.revoke(deriveChatDeliveryCapability(chat.epochState.stateCapability, {
@@ -18476,39 +18598,74 @@ function createChatDelete({
18476
18598
  }, target.members, target.expectedEpochId);
18477
18599
  return result.deliveredCount;
18478
18600
  };
18601
+ const deleteCurrentChat = async (input, options = {}) => {
18602
+ const chatId = cleanText(typeof input === "string" ? input : input?.chatId || input?.id);
18603
+ const attemptedEpochs = new Set;
18604
+ while (chatId) {
18605
+ const target = getDeleteTargets(input)[0];
18606
+ if (!target)
18607
+ return null;
18608
+ const peerChatPK = cleanText(typeof input === "object" ? input?.peerChatPK : "") || cleanText(lastServerChatsRef.current.find((item) => item?.id === chatId)?.peerChatPK);
18609
+ const releaseMutation = await mutationGate?.acquireExclusive([chatId, peerChatPK].filter(Boolean)) || (() => {});
18610
+ let released = false;
18611
+ const release = () => {
18612
+ if (released)
18613
+ return;
18614
+ released = true;
18615
+ releaseMutation();
18616
+ };
18617
+ try {
18618
+ const deletion = await cloud.chat.delete(target.chatId, {
18619
+ entryId: target.entryId,
18620
+ expectedEpochId: target.expectedEpochId,
18621
+ stateCapability: target.stateCapability,
18622
+ cleanup: options.cleanup !== false
18623
+ });
18624
+ if (deletion?.newlyDeleted !== false)
18625
+ await wakeDeletedChat(target);
18626
+ return target;
18627
+ } catch (error) {
18628
+ release();
18629
+ const attemptKey = `${target.epochVersion || 0}:${target.expectedEpochId || ""}`;
18630
+ if (!attemptKey || attemptedEpochs.has(attemptKey) || !isChatEpochConflict(error) || typeof reconcileEpoch !== "function")
18631
+ throw error;
18632
+ attemptedEpochs.add(attemptKey);
18633
+ const reconciled = await reconcileEpoch(chatId);
18634
+ const current = lastServerChatsRef.current.find((item) => item?.id === chatId) || null;
18635
+ if (reconciled?.removed || !current)
18636
+ return null;
18637
+ if (!Number.isSafeInteger(current.epochVersion) || current.epochVersion <= target.epochVersion) {
18638
+ throw error;
18639
+ }
18640
+ } finally {
18641
+ release();
18642
+ }
18643
+ }
18644
+ return null;
18645
+ };
18479
18646
  const deleteChat = async (chat, options = {}) => {
18480
- const targets = getDeleteTargets(chat);
18647
+ const inputs = Array.isArray(chat) ? chat : [chat];
18648
+ const targets = getDeleteTargets(inputs);
18481
18649
  if (!targets.length)
18482
18650
  return false;
18483
18651
  const chatIds = targets.map((target) => target.chatId);
18484
- const peerChatPKs = chatIds.map((chatId) => {
18485
- const input = (Array.isArray(chat) ? chat : [chat]).find((item) => cleanText(typeof item === "string" ? item : item?.chatId || item?.id) === chatId);
18486
- return cleanText(typeof input === "object" ? input?.peerChatPK : "") || cleanText(lastServerChatsRef.current.find((item) => item?.id === chatId)?.peerChatPK);
18487
- }).filter(Boolean);
18488
- const releaseMutation = await mutationGate?.acquireExclusive([...chatIds, ...peerChatPKs]) || (() => {});
18652
+ const deletedTargets = [];
18653
+ hideDeletingChats(chatIds, options);
18489
18654
  try {
18490
- hideDeletingChats(chatIds, options);
18491
- try {
18492
- for (const target of targets) {
18493
- await cloud.chat.delete(target.chatId, {
18494
- entryId: target.entryId,
18495
- expectedEpochId: target.expectedEpochId,
18496
- stateCapability: target.stateCapability,
18497
- cleanup: options.cleanup !== false
18498
- });
18499
- await wakeDeletedChat(target);
18500
- }
18501
- await revokeTargetDeliveries(targets);
18502
- await options.onDeliveryRevoked?.();
18503
- confirmDeletedChats(chatIds);
18504
- } catch (error) {
18505
- restoreDeletedChats(chatIds);
18506
- throw error;
18507
- }
18508
- return Array.isArray(chat) ? targets.length : true;
18509
- } finally {
18510
- releaseMutation();
18655
+ for (const target of targets) {
18656
+ const input = inputs.find((item) => cleanText(typeof item === "string" ? item : item?.chatId || item?.id) === target.chatId) || target.chatId;
18657
+ const deleted = await deleteCurrentChat(input, options);
18658
+ if (deleted)
18659
+ deletedTargets.push(deleted);
18660
+ }
18661
+ await revokeTargetDeliveries(deletedTargets);
18662
+ await options.onDeliveryRevoked?.();
18663
+ confirmDeletedChats(chatIds);
18664
+ } catch (error) {
18665
+ restoreDeletedChats(chatIds);
18666
+ throw error;
18511
18667
  }
18668
+ return Array.isArray(chat) ? targets.length : true;
18512
18669
  };
18513
18670
  const wasChatDeletedLocally = (chatId) => !!chatId && locallyDeletedChatIdsRef.current.has(chatId);
18514
18671
  const ackDeletedChat = (chatId) => {
@@ -18519,21 +18676,25 @@ function createChatDelete({
18519
18676
  pendingDeleteIdsRef.current = new Set;
18520
18677
  locallyDeletedChatIdsRef.current = new Set;
18521
18678
  deletedChatIdsRef.current = new Set;
18679
+ removedChatVersionsRef.current = new Map;
18522
18680
  keepSelectedDeletedChatIdsRef.current = new Set;
18523
18681
  };
18524
18682
  return {
18525
18683
  pendingDeleteIdsRef,
18526
18684
  deletedChatIdsRef,
18685
+ removedChatVersionsRef,
18527
18686
  keepSelectedDeletedChatIdsRef,
18528
18687
  resetDeleteState,
18529
18688
  clearDeletedChatState,
18530
18689
  beginRetirement,
18531
18690
  rollbackRetirement,
18532
18691
  commitRetirement,
18692
+ commitRemovedRetirement,
18533
18693
  restoreDeletedChat,
18534
18694
  deleteChat,
18535
18695
  dropChat,
18536
18696
  dropRemovedChat,
18697
+ restoreRemovedChat,
18537
18698
  dropUnavailableChat,
18538
18699
  wasChatDeletedLocally,
18539
18700
  ackDeletedChat,
@@ -19620,6 +19781,9 @@ var init_chatlive = __esm(() => {
19620
19781
  });
19621
19782
 
19622
19783
  // ../../core/chat/live.js
19784
+ function makeChatCompositionId() {
19785
+ return toHex(randomBytes3(16));
19786
+ }
19623
19787
  function chatMessageCompositionId(message) {
19624
19788
  const value = String(message?.compositionId || "").trim().toLowerCase();
19625
19789
  return CHAT_COMPOSITION_ID_RE.test(value) ? value : "";
@@ -19803,10 +19967,16 @@ function createChatLive({
19803
19967
  onActivity?.(entry.chatId, EMPTY_ACTIVITY);
19804
19968
  };
19805
19969
  const enterChatActivity = async (chatId) => {
19806
- if (!enabled || !chatId || rooms.has(chatId))
19970
+ if (!enabled || !chatId)
19807
19971
  return false;
19972
+ const current = rooms.get(chatId);
19973
+ if (current && !current.closed) {
19974
+ current.retainers += 1;
19975
+ return true;
19976
+ }
19808
19977
  const entry = {
19809
19978
  chatId,
19979
+ retainers: 1,
19810
19980
  epochId: "",
19811
19981
  material: null,
19812
19982
  connection: null,
@@ -19882,6 +20052,9 @@ function createChatLive({
19882
20052
  const entry = rooms.get(chatId);
19883
20053
  if (!entry)
19884
20054
  return false;
20055
+ entry.retainers -= 1;
20056
+ if (entry.retainers > 0)
20057
+ return true;
19885
20058
  rooms.delete(chatId);
19886
20059
  destroy(entry);
19887
20060
  return true;
@@ -19929,13 +20102,26 @@ function createChatLive({
19929
20102
  publish(entry);
19930
20103
  return scheduleReadSnapshot(entry);
19931
20104
  };
20105
+ const flushChatReadFrontier = async (chatId) => {
20106
+ const entry = rooms.get(chatId);
20107
+ if (!entry || entry.closed || !entry.connected || !entry.readFrontier)
20108
+ return false;
20109
+ if (!sendSnapshot(entry))
20110
+ return false;
20111
+ const pending = entry.sendChain;
20112
+ await pending;
20113
+ return !entry.closed;
20114
+ };
19932
20115
  const leaveAllChatActivity = () => {
19933
- for (const chatId of [...rooms.keys()])
19934
- leaveChatActivity(chatId);
20116
+ for (const [chatId, entry] of [...rooms]) {
20117
+ rooms.delete(chatId);
20118
+ destroy(entry);
20119
+ }
19935
20120
  };
19936
20121
  return Object.freeze({
19937
20122
  close: leaveAllChatActivity,
19938
20123
  enterChatActivity,
20124
+ flushChatReadFrontier,
19939
20125
  leaveChatActivity,
19940
20126
  leaveAllChatActivity,
19941
20127
  markChatReadFrontier,
@@ -20039,7 +20225,73 @@ function withoutChatIdentity(options = {}) {
20039
20225
  }
20040
20226
  function refreshQueuedSendOptions(sendOptionsForPeer, peerChatPK, sendOptionOverrides = null) {
20041
20227
  const chatId = cleanText(sendOptionOverrides?.chatId);
20042
- return mergeSendOptions(withoutChatIdentity(sendOptionOverrides), sendOptionsForPeer(peerChatPK, chatId));
20228
+ const current = sendOptionsForPeer(peerChatPK, chatId);
20229
+ if (chatId) {
20230
+ return {
20231
+ ...withoutChatIdentity(sendOptionOverrides),
20232
+ ...current
20233
+ };
20234
+ }
20235
+ return mergeSendOptions(withoutChatIdentity(sendOptionOverrides), current);
20236
+ }
20237
+ function currentEpoch(options) {
20238
+ const manifest = options?.epochState?.manifest || options?.ownEntry?.current?.manifest || null;
20239
+ const epochId = cleanText(manifest?.epochId);
20240
+ const epochVersion = Number(manifest?.epochVersion);
20241
+ return epochId && Number.isInteger(epochVersion) && epochVersion > 0 ? { epochId, epochVersion } : null;
20242
+ }
20243
+ async function runCurrentEpochWrite({
20244
+ chatId,
20245
+ mutationGate,
20246
+ readOptions,
20247
+ reconcileEpoch,
20248
+ write,
20249
+ onCommitted,
20250
+ onEpochAdvance
20251
+ }) {
20252
+ const attemptedEpochs = new Set;
20253
+ while (true) {
20254
+ const releaseMutation = await mutationGate?.acquireShared(chatId) || (() => {});
20255
+ let released = false;
20256
+ let committed = false;
20257
+ const release = () => {
20258
+ if (released)
20259
+ return;
20260
+ released = true;
20261
+ releaseMutation();
20262
+ };
20263
+ let options;
20264
+ let epoch;
20265
+ try {
20266
+ options = readOptions();
20267
+ epoch = currentEpoch(options);
20268
+ return await write(options, (...args) => {
20269
+ committed = true;
20270
+ release();
20271
+ onCommitted?.(...args);
20272
+ });
20273
+ } catch (error) {
20274
+ release();
20275
+ const attemptKey = epoch ? `${epoch.epochVersion}:${epoch.epochId}` : "";
20276
+ if (committed || options?.chatExists !== true || !attemptKey || attemptedEpochs.has(attemptKey) || !isChatEpochConflict(error) || typeof reconcileEpoch !== "function") {
20277
+ throw error;
20278
+ }
20279
+ attemptedEpochs.add(attemptKey);
20280
+ await reconcileEpoch(chatId);
20281
+ const nextEpoch = currentEpoch(readOptions());
20282
+ if (!nextEpoch || nextEpoch.epochVersion <= epoch.epochVersion || nextEpoch.epochId === epoch.epochId) {
20283
+ throw error;
20284
+ }
20285
+ onEpochAdvance?.({
20286
+ fromEpochId: epoch.epochId,
20287
+ fromEpochVersion: epoch.epochVersion,
20288
+ toEpochId: nextEpoch.epochId,
20289
+ toEpochVersion: nextEpoch.epochVersion
20290
+ });
20291
+ } finally {
20292
+ release();
20293
+ }
20294
+ }
20043
20295
  }
20044
20296
  function markSendPerf(diag, label, startedAt, data = {}, minMs = SEND_CHAT_REDUCER_WARN_MS) {
20045
20297
  const elapsedMs = Date.now() - startedAt;
@@ -20304,7 +20556,7 @@ async function makeSharedMediaAttachment(cloud, media, message, data) {
20304
20556
  ...source?.c ? { caption: source.c } : {}
20305
20557
  }));
20306
20558
  }
20307
- function createChatSend({ cloud, media = {}, uid, chatBanned, chatPK, chatSigningPK, chatSigningSecret, chatPrivateKey, notificationPK, notificationPrivateKey, localCache, localByChatRef, setLocalByChat, setChats, setLastChat, sendOptionsForPeer, selectLocalChat, adoptLocalMessageMedia, readMessageFile, mutationGate, diag = null }) {
20559
+ function createChatSend({ cloud, media = {}, uid, chatBanned, chatPK, chatSigningPK, chatSigningSecret, chatPrivateKey, notificationPK, notificationPrivateKey, localCache, localByChatRef, setLocalByChat, setChats, setLastChat, sendOptionsForPeer, selectLocalChat, adoptLocalMessageMedia, readMessageFile, mutationGate, reconcileEpoch, diag = null }) {
20308
20560
  const adoptedLocalMediaRef = { current: new Set };
20309
20561
  const attachmentPrepareByCidRef = { current: new Map };
20310
20562
  const cachedLocalMediaRef = { current: new Set };
@@ -20664,78 +20916,80 @@ ${cid}` : "";
20664
20916
  };
20665
20917
  const queueSend = async (peerChatPK, message, run, { previewRequired = false, sendOptions: sendOptionOverrides = null } = {}) => {
20666
20918
  const startedAt = Date.now();
20667
- let sendOptions = refreshQueuedSendOptions(sendOptionsForPeer, peerChatPK, sendOptionOverrides);
20668
- let local = await showLocalMessage(peerChatPK, message, sendOptions);
20919
+ const sendOptions = refreshQueuedSendOptions(sendOptionsForPeer, peerChatPK, sendOptionOverrides);
20920
+ const local = await showLocalMessage(peerChatPK, message, sendOptions);
20669
20921
  const mutationTarget = local.chatId || cleanText(sendOptionOverrides?.chatId) || peerChatPK;
20670
- let releaseMutation;
20671
- try {
20672
- releaseMutation = await mutationGate?.acquireShared(mutationTarget) || (() => {});
20673
- } catch (error) {
20674
- markLocalStatus(local.chatId, local.cid, LOCAL_FAILED);
20675
- throw error;
20676
- }
20677
- try {
20678
- sendOptions = refreshQueuedSendOptions(sendOptionsForPeer, peerChatPK, {
20679
- ...sendOptionOverrides,
20680
- chatId: local.chatId
20681
- });
20682
- const requiredPreview = previewRequired || sendOptions.chatExists !== true;
20683
- let committed = false;
20684
- let sent = false;
20685
- const markSent = () => {
20686
- if (sent)
20687
- return;
20688
- sent = true;
20689
- sentChatIdsRef.current.add(local.chatId);
20690
- markLocalStatus(local.chatId, local.cid, LOCAL_SENT);
20691
- };
20692
- const markCommitted = () => {
20693
- if (committed)
20694
- return;
20695
- committed = true;
20696
- markSent();
20697
- markDone(diag, "chat.send.commit", startedAt, { type: local?.t || "txt", previewRequired: !!requiredPreview });
20698
- };
20699
- return await new Promise((resolve, reject) => {
20700
- const job = {
20701
- previewKey: local.chatId,
20702
- previewRequired: requiredPreview,
20703
- resolve,
20704
- reject,
20705
- onSuccess: markSent,
20706
- onError: (error) => {
20707
- markError(diag, "chat.send.job", startedAt, error, { type: local?.t || "txt", previewRequired: !!requiredPreview });
20708
- if (committed)
20709
- markSent();
20710
- else
20711
- markLocalStatus(local.chatId, local.cid, LOCAL_FAILED);
20712
- },
20713
- run: async (context) => {
20714
- const writeStartedAt = Date.now();
20715
- const onCommitted = () => {
20716
- markCommitted();
20717
- context?.releaseTargets?.();
20718
- };
20719
- markDiag(diag, "chat.send.write.start", { type: local?.t || "txt", previewRequired: !!requiredPreview, updatePreview: !!context?.updatePreview });
20720
- try {
20721
- const result = await run({ ...context, local, onCommitted, sendOptions });
20722
- markDone(diag, "chat.send.write", writeStartedAt, { type: local?.t || "txt", previewRequired: !!requiredPreview, updatePreview: !!context?.updatePreview });
20723
- return result;
20724
- } catch (error) {
20725
- markError(diag, "chat.send.write", writeStartedAt, error, {
20726
- type: local?.t || "txt",
20727
- previewRequired: !!requiredPreview,
20728
- updatePreview: !!context?.updatePreview
20729
- });
20730
- throw error;
20731
- }
20922
+ const requiredPreview = previewRequired || sendOptions.chatExists !== true;
20923
+ let committed = false;
20924
+ let sent = false;
20925
+ const markSent = () => {
20926
+ if (sent)
20927
+ return;
20928
+ sent = true;
20929
+ sentChatIdsRef.current.add(local.chatId);
20930
+ markLocalStatus(local.chatId, local.cid, LOCAL_SENT);
20931
+ };
20932
+ const markCommitted = () => {
20933
+ if (committed)
20934
+ return;
20935
+ committed = true;
20936
+ markSent();
20937
+ markDone(diag, "chat.send.commit", startedAt, { type: local?.t || "txt", previewRequired: !!requiredPreview });
20938
+ };
20939
+ return await new Promise((resolve, reject) => {
20940
+ const job = {
20941
+ previewKey: local.chatId,
20942
+ previewRequired: requiredPreview,
20943
+ resolve,
20944
+ reject,
20945
+ onSuccess: markSent,
20946
+ onError: (error) => {
20947
+ markError(diag, "chat.send.job", startedAt, error, { type: local?.t || "txt", previewRequired: !!requiredPreview });
20948
+ if (committed)
20949
+ markSent();
20950
+ else
20951
+ markLocalStatus(local.chatId, local.cid, LOCAL_FAILED);
20952
+ },
20953
+ run: async (context) => {
20954
+ const writeStartedAt = Date.now();
20955
+ markDiag(diag, "chat.send.write.start", { type: local?.t || "txt", previewRequired: !!requiredPreview, updatePreview: !!context?.updatePreview });
20956
+ try {
20957
+ const result = await runCurrentEpochWrite({
20958
+ chatId: mutationTarget,
20959
+ mutationGate,
20960
+ readOptions: () => refreshQueuedSendOptions(sendOptionsForPeer, peerChatPK, {
20961
+ ...sendOptionOverrides,
20962
+ chatId: local.chatId
20963
+ }),
20964
+ reconcileEpoch,
20965
+ write: (currentSendOptions, onCommitted) => run({
20966
+ ...context,
20967
+ local,
20968
+ onCommitted,
20969
+ sendOptions: currentSendOptions
20970
+ }),
20971
+ onCommitted: () => {
20972
+ markCommitted();
20973
+ context?.releaseTargets?.();
20974
+ },
20975
+ onEpochAdvance: ({ fromEpochVersion, toEpochVersion }) => {
20976
+ markDiag(diag, "chat.send.epoch.advance", { fromEpochVersion, toEpochVersion });
20977
+ }
20978
+ });
20979
+ markDone(diag, "chat.send.write", writeStartedAt, { type: local?.t || "txt", previewRequired: !!requiredPreview, updatePreview: !!context?.updatePreview });
20980
+ return result;
20981
+ } catch (error) {
20982
+ markError(diag, "chat.send.write", writeStartedAt, error, {
20983
+ type: local?.t || "txt",
20984
+ previewRequired: !!requiredPreview,
20985
+ updatePreview: !!context?.updatePreview
20986
+ });
20987
+ throw error;
20732
20988
  }
20733
- };
20734
- enqueueSendJob(peerChatPK, job, reject);
20735
- });
20736
- } finally {
20737
- releaseMutation?.();
20738
- }
20989
+ }
20990
+ };
20991
+ enqueueSendJob(peerChatPK, job, reject);
20992
+ });
20739
20993
  };
20740
20994
  const sendOptionsForQueuedWrite = (baseOptions, local, updatePreview, onCommitted) => {
20741
20995
  const chatId = local?.chatId;
@@ -23489,7 +23743,7 @@ async function openChatReceiptCheckpoint(epoch, record) {
23489
23743
  cleanBytes(keys.key, plaintext);
23490
23744
  }
23491
23745
  }
23492
- 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;
23746
+ 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;
23493
23747
  var init_chatreceipt = __esm(() => {
23494
23748
  init_box();
23495
23749
  init_canonical();
@@ -23501,8 +23755,9 @@ var init_chatreceipt = __esm(() => {
23501
23755
  init_protocol();
23502
23756
  init_receipt();
23503
23757
  "use client";
23758
+ CHAT_RECEIPT_CHECKPOINT_MAX_BYTES = 16 * 1024;
23504
23759
  CHECKPOINT_HEADER_BYTES = 2 + BOX_NONCE_BYTES;
23505
- CHECKPOINT_SIGN_SCOPE = encoder.encode("veyl-chat-receipt-checkpoint-v3-sign");
23760
+ CHECKPOINT_SIGN_SCOPE = encoder.encode("veyl-chat-receipt-checkpoint-v4-sign");
23506
23761
  });
23507
23762
 
23508
23763
  // ../../core/chat/receiptsession.js
@@ -23589,6 +23844,7 @@ function createChatReceiptSession({ cloud, epochState, identity, onUpdate, onErr
23589
23844
  let closed = false;
23590
23845
  let closing = false;
23591
23846
  let checkpoint = null;
23847
+ let displayedProjection = null;
23592
23848
  let sourceProjection = null;
23593
23849
  let sourceStop = null;
23594
23850
  let sourceLeaseTimer = null;
@@ -23654,8 +23910,10 @@ function createChatReceiptSession({ cloud, epochState, identity, onUpdate, onErr
23654
23910
  checkpoint = next;
23655
23911
  if (closing || closed)
23656
23912
  return;
23657
- if (next)
23658
- onUpdate?.(projected(next));
23913
+ if (next) {
23914
+ displayedProjection = mergeChatReceiptProjections(displayedProjection, next.projection);
23915
+ onUpdate?.(projected({ ...next, projection: displayedProjection }));
23916
+ }
23659
23917
  if (coversDesired(next)) {
23660
23918
  desiredState = null;
23661
23919
  clearStalledClaimTimer();
@@ -23668,10 +23926,10 @@ function createChatReceiptSession({ cloud, epochState, identity, onUpdate, onErr
23668
23926
  else
23669
23927
  armSourceExpiry();
23670
23928
  };
23671
- const writeCheckpoint = async (mode, expectedVersion = null) => {
23672
- if (closed || closing)
23929
+ const writeCheckpoint = async (mode, expectedVersion = null, projectionOverride = null) => {
23930
+ if (closed || closing && mode !== "release")
23673
23931
  return false;
23674
- const desiredProjection = mode === "publish" ? sourceProjection : null;
23932
+ const desiredProjection = mode === "publish" || mode === "release" ? projectionOverride || sourceProjection : null;
23675
23933
  const result = await cloud.chat.memberState.write({
23676
23934
  epochId: epoch.epochId,
23677
23935
  lane: address.lane,
@@ -23682,19 +23940,22 @@ function createChatReceiptSession({ cloud, epochState, identity, onUpdate, onErr
23682
23940
  const current = record ? await openChatReceiptCheckpoint(epoch, record).catch(() => null) : null;
23683
23941
  const heldByOther = !holdsLease(current, now) && Number(current?.leaseUntil) > now;
23684
23942
  const stalledTakeover = mode === "claim" && expectedVersion && checkpointVersion(current) === expectedVersion && !coversDesired(current);
23943
+ if (mode === "release" && !holdsLease(current, now)) {
23944
+ return { result: { won: false, checkpoint: current } };
23945
+ }
23685
23946
  if (heldByOther && !stalledTakeover)
23686
23947
  return { result: { won: false, checkpoint: current } };
23687
- const projection = desiredProjection || current?.projection || buildChatReceiptProjection(epoch.manifest, new Map);
23948
+ const projection = desiredProjection ? mergeChatReceiptProjections(current?.projection, desiredProjection) : current?.projection || buildChatReceiptProjection(epoch.manifest, new Map);
23688
23949
  if (mode === "publish" && holdsLease(current, now) && chatReceiptProjectionFingerprint(current.projection) === chatReceiptProjectionFingerprint(projection)) {
23689
23950
  return { result: { won: true, checkpoint: current } };
23690
23951
  }
23691
23952
  const term = current ? current.term + (!holdsLease(current, now) || Number(current.leaseUntil) <= now ? 1 : 0) : 1;
23692
23953
  const sealed = await sealChatReceiptCheckpoint(epoch, {
23693
- v: 1,
23954
+ v: CHAT_RECEIPT_CHECKPOINT_VERSION,
23694
23955
  coordinator: epoch.actor.chatPK,
23695
23956
  holder,
23696
23957
  term,
23697
- leaseUntil: now + RECEIPT_COORDINATOR_LEASE_MS,
23958
+ leaseUntil: mode === "release" ? now : now + RECEIPT_COORDINATOR_LEASE_MS,
23698
23959
  publishedAt: now,
23699
23960
  projection
23700
23961
  });
@@ -23744,10 +24005,10 @@ function createChatReceiptSession({ cloud, epochState, identity, onUpdate, onErr
23744
24005
  sourceStop = watchChatMemberStateEvidence(cloud, epochState, ({ states }) => {
23745
24006
  if (closed || closing || !holdsLease(checkpoint))
23746
24007
  return;
23747
- const nextProjection = buildChatReceiptProjection(epoch.manifest, states);
23748
- sourceProjection = nextProjection;
24008
+ sourceProjection = mergeChatReceiptProjections(checkpoint?.projection, buildChatReceiptProjection(epoch.manifest, states));
24009
+ displayedProjection = mergeChatReceiptProjections(displayedProjection, sourceProjection);
23749
24010
  onUpdate?.(projectChatReceiptCheckpoint(epoch.manifest, {
23750
- projection: sourceProjection,
24011
+ projection: displayedProjection,
23751
24012
  publishedAt: Date.now()
23752
24013
  }));
23753
24014
  schedulePublish();
@@ -23837,6 +24098,8 @@ function createChatReceiptSession({ cloud, epochState, identity, onUpdate, onErr
23837
24098
  if (closed || closing)
23838
24099
  return;
23839
24100
  closing = true;
24101
+ const finalProjection = sourceProjection;
24102
+ const release = holdsLease(checkpoint) ? track(writeCheckpoint("release", null, finalProjection)) : null;
23840
24103
  watchRun += 1;
23841
24104
  clearClaimTimer();
23842
24105
  clearStalledClaimTimer();
@@ -23850,7 +24113,7 @@ function createChatReceiptSession({ cloud, epochState, identity, onUpdate, onErr
23850
24113
  closing = false;
23851
24114
  closeReceiptEpoch(opened);
23852
24115
  };
23853
- Promise.allSettled([...operations]).finally(finish);
24116
+ Promise.allSettled([...operations, release].filter(Boolean)).finally(finish);
23854
24117
  }
23855
24118
  };
23856
24119
  }
@@ -24922,6 +25185,62 @@ var init_session2 = __esm(() => {
24922
25185
  init_receiptsession();
24923
25186
  });
24924
25187
 
25188
+ // ../../core/chat/retirement.js
25189
+ function planChatRetirement(chat, selfChatPK) {
25190
+ const self2 = cleanText(selfChatPK);
25191
+ const members = Array.isArray(chat?.members) ? chat.members : [];
25192
+ const remaining = members.filter((member) => cleanText(member?.chatPK) !== self2);
25193
+ const direct = chat?.lineage === CHAT_LINEAGES.DIRECT && members.length === 2;
25194
+ return {
25195
+ action: direct || !remaining.length ? CHAT_RETIREMENT_ACTIONS.DELETE : CHAT_RETIREMENT_ACTIONS.LEAVE,
25196
+ remaining
25197
+ };
25198
+ }
25199
+ async function retireChatMembershipOwner(cloud, identity, entry, fields = {}) {
25200
+ const parent = entry?.current?.manifest;
25201
+ const kind = fields.kind;
25202
+ const epochVersion = fields.epochVersion;
25203
+ if (!parent?.chatId || !identity?.uid || !identity?.chatPrivateKey || !Object.values(CHAT_OWNER_RETIREMENT_KINDS).includes(kind) || !Number.isSafeInteger(epochVersion)) {
25204
+ throw new Error("chat membership retirement required");
25205
+ }
25206
+ const entryId = ownChatEntryId(identity.chatPrivateKey, parent.chatId);
25207
+ const historyId = ownEpochEntryId(identity.chatPrivateKey, parent.chatId, parent.epochId);
25208
+ const cutoffMs = Number.isFinite(fields.cutoffMs) ? fields.cutoffMs : Date.now();
25209
+ const history = makeOwnerEpochRecord(parent, entry.current.epochSecret, {
25210
+ cutoffMs,
25211
+ successorTransitionCommitment: fields.successorTransitionCommitment,
25212
+ successorTransitionMessageId: fields.successorTransitionMessageId,
25213
+ leaveProposalId: fields.leaveProposalId
25214
+ });
25215
+ return retireOwnChatEntry(cloud, identity, entryId, entry, {
25216
+ expectedEpochVersion: parent.epochVersion,
25217
+ retirement: {
25218
+ kind,
25219
+ epochVersion,
25220
+ ...kind === CHAT_OWNER_RETIREMENT_KINDS.LEAVE ? { proposalId: fields.leaveProposalId } : {}
25221
+ },
25222
+ epoch: {
25223
+ id: historyId,
25224
+ record: {
25225
+ body: await sealOwnerEpochRecord(identity.chatPrivateKey, entryId, historyId, history),
25226
+ tsMs: cutoffMs
25227
+ }
25228
+ },
25229
+ mlsStateId: ownerChatMlsSnapshotId(identity, entryId, entry.current)
25230
+ });
25231
+ }
25232
+ var CHAT_RETIREMENT_ACTIONS;
25233
+ var init_retirement = __esm(() => {
25234
+ init_entry();
25235
+ init_manifest();
25236
+ init_owner2();
25237
+ init_owner();
25238
+ CHAT_RETIREMENT_ACTIONS = Object.freeze({
25239
+ DELETE: "delete",
25240
+ LEAVE: "leave"
25241
+ });
25242
+ });
25243
+
24925
25244
  // ../../core/chat/inbox.js
24926
25245
  function normalizeChatPreview(record, message) {
24927
25246
  if (!message || typeof message !== "object")
@@ -24971,7 +25290,7 @@ async function unlinkBlockedChat(cloud, uid, identity, entry, options) {
24971
25290
  const entryId = ownChatEntryId(identity.chatPrivateKey, manifest.chatId);
24972
25291
  markDiag(options.diag, "chat.owner.unlink", { reason: "blocked" });
24973
25292
  await cloud.user.chats.unlink(uid, entryId);
24974
- options.onRemoved?.(manifest.chatId);
25293
+ options.onRemoved?.(manifest.chatId, manifest.epochVersion);
24975
25294
  options.onPingDelete?.(manifest.chatId);
24976
25295
  return { removed: true, blocked: true, chatId: manifest.chatId };
24977
25296
  }
@@ -24990,6 +25309,25 @@ function isRetryableInboxError(error) {
24990
25309
  ];
24991
25310
  return retryableCodes.some((value) => code === value || code.endsWith(`/${value}`)) || /\b(network|offline|timed? out|temporar(?:y|ily)|connection)\b/.test(message);
24992
25311
  }
25312
+ async function retireConfirmedDeletedChat(cloud, uid, identity, entry, options = {}) {
25313
+ const manifest = entry?.current?.manifest;
25314
+ if (!manifest?.chatId || !identity?.chatPrivateKey || !identity?.chatPK) {
25315
+ throw new Error("deleted chat owner entry required");
25316
+ }
25317
+ const entryId = ownChatEntryId(identity.chatPrivateKey, manifest.chatId);
25318
+ await cloud.user.chats.unlinkDeleted(uid, entryId, manifest.chatId);
25319
+ if (entry.deliveryRegistered) {
25320
+ const capability = deriveChatDeliveryCapability(entry.current.stateCapability, {
25321
+ chatId: manifest.chatId,
25322
+ recipientChatPK: identity.chatPK,
25323
+ generation: manifest.epochVersion
25324
+ });
25325
+ await cloud.delivery.revoke(capability).catch(() => false);
25326
+ }
25327
+ markDiag(options.diag, "chat.owner.unlink", { reason: "confirmed_deleted" });
25328
+ options.onPingDelete?.(manifest.chatId);
25329
+ return { removed: true, deleted: true, chatId: manifest.chatId };
25330
+ }
24993
25331
  function inboxErrorReason(error) {
24994
25332
  const message = cleanText(error?.message).toLowerCase();
24995
25333
  if (message === "ping sender identity mismatch")
@@ -25204,7 +25542,7 @@ function sameRoutes(left, right) {
25204
25542
  return leftKeys.length === rightKeys.length && leftKeys.every((key) => sameRoute(left[key], right[key]));
25205
25543
  }
25206
25544
  function sameStableEntry(left, right) {
25207
- 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;
25545
+ 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;
25208
25546
  }
25209
25547
  function projectOwnChatEntry(entry, entryId, userChatPK, ts, activity = {}) {
25210
25548
  const manifest = entry.current.manifest;
@@ -25254,20 +25592,43 @@ async function processOpenedPing(cloud, uid, identity, ping, openedProfile, hint
25254
25592
  }
25255
25593
  let knownMember = entry?.current?.manifest?.members?.find((member) => member.chatPK === payload.senderChatPK) || null;
25256
25594
  const senderProfile = openedProfile || (knownMember ? { ...knownMember, uid: knownMember.uid } : null) || await resolveSenderProfile(cloud, payload);
25257
- if (entry && payload.epochVersion === entry.current.manifest.epochVersion && cleanText(payload.messageId) && cleanText(currentChat?.inboxMessageId) === cleanText(payload.messageId)) {
25595
+ let restoringMembership = !!entry?.retirement;
25596
+ if (restoringMembership && (payload.kind !== "welcome" || payload.epochVersion <= entry.retirement.epochVersion)) {
25597
+ return "stale";
25598
+ }
25599
+ if (entry && !restoringMembership && payload.epochVersion === entry.current.manifest.epochVersion && cleanText(payload.messageId) && cleanText(currentChat?.inboxMessageId) === cleanText(payload.messageId)) {
25258
25600
  return "duplicate";
25259
25601
  }
25260
- if (entry && payload.epochVersion < entry.current.manifest.epochVersion) {
25602
+ if (entry && !restoringMembership && payload.epochVersion < entry.current.manifest.epochVersion) {
25261
25603
  return "stale";
25262
25604
  }
25263
- if (entry && payload.epochVersion > entry.current.manifest.epochVersion) {
25264
- const advanced = await advanceEntryToEpoch(cloud, uid, identity, entry, payload.epochVersion, options);
25605
+ if (entry && !restoringMembership && payload.epochVersion > entry.current.manifest.epochVersion) {
25606
+ let advanced;
25607
+ try {
25608
+ advanced = await advanceEntryToEpoch(cloud, uid, identity, entry, payload.epochVersion, options);
25609
+ } catch (error) {
25610
+ if (payload.kind !== "chat_deleted")
25611
+ throw error;
25612
+ try {
25613
+ await retireConfirmedDeletedChat(cloud, uid, identity, entry, options);
25614
+ return "deleted";
25615
+ } catch (confirmationError) {
25616
+ if (isRetryableInboxError(confirmationError))
25617
+ throw confirmationError;
25618
+ throw error;
25619
+ }
25620
+ }
25265
25621
  if (advanced?.removed) {
25266
- options.onPingDelete?.(payload.chatId);
25267
- return "deleted";
25622
+ if (payload.kind !== "welcome" || payload.epochVersion <= advanced.entry?.retirement?.epochVersion) {
25623
+ options.onPingDelete?.(payload.chatId);
25624
+ return "deleted";
25625
+ }
25626
+ entry = advanced.entry;
25627
+ restoringMembership = true;
25628
+ } else {
25629
+ entry = advanced;
25630
+ knownMember = entry.current.manifest.members.find((member) => member.chatPK === payload.senderChatPK) || null;
25268
25631
  }
25269
- entry = advanced;
25270
- knownMember = entry.current.manifest.members.find((member) => member.chatPK === payload.senderChatPK) || null;
25271
25632
  }
25272
25633
  if (payload.kind === "chat_deleted") {
25273
25634
  if (!entry || entry.current.manifest.epochId !== payload.epochId || entry.current.manifest.epochVersion !== payload.epochVersion || !knownMember) {
@@ -25287,7 +25648,7 @@ async function processOpenedPing(cloud, uid, identity, ping, openedProfile, hint
25287
25648
  return "deleted";
25288
25649
  }
25289
25650
  let epochState = entry?.current || null;
25290
- if (!entry && payload.kind === "welcome") {
25651
+ if ((!entry || restoringMembership) && payload.kind === "welcome") {
25291
25652
  epochState = await welcomedEpochFromPing(cloud, identity, senderProfile, payload, options);
25292
25653
  } else if (!entry) {
25293
25654
  throw new Error("chat mls welcome required");
@@ -25317,6 +25678,11 @@ async function processOpenedPing(cloud, uid, identity, ping, openedProfile, hint
25317
25678
  options.onPingDelete?.(payload.chatId);
25318
25679
  return "deleted";
25319
25680
  }
25681
+ const latest = await advanceEntryToLatestEpoch(cloud, uid, identity, transitioned, options);
25682
+ if (latest?.removed) {
25683
+ options.onPingDelete?.(payload.chatId);
25684
+ return "deleted";
25685
+ }
25320
25686
  return "transition";
25321
25687
  }
25322
25688
  }
@@ -25330,7 +25696,7 @@ async function processOpenedPing(cloud, uid, identity, ping, openedProfile, hint
25330
25696
  const routes = withSenderRoute(entry, payload, senderProfile, epochState);
25331
25697
  const preview2 = nextPreview(currentChat, record, message, payload);
25332
25698
  const nowMs2 = timestampMs(record?.ts, null) ?? timestampMs(payload.ts, Date.now()) ?? Date.now();
25333
- const membershipWelcome = !entry && payload.kind === "welcome";
25699
+ const membershipWelcome = (!entry || restoringMembership) && payload.kind === "welcome";
25334
25700
  const projectedTsMs = membershipWelcome ? Date.now() : nowMs2;
25335
25701
  let deliveryRegistered = entry?.deliveryRegistered === true;
25336
25702
  if (!deliveryRegistered)
@@ -25348,7 +25714,8 @@ async function processOpenedPing(cloud, uid, identity, ping, openedProfile, hint
25348
25714
  },
25349
25715
  routes,
25350
25716
  deliveryRegistered,
25351
- notificationTag: notificationChatTag(epochState.stateCapability, payload.chatId, identity.chatPK)
25717
+ notificationTag: notificationChatTag(epochState.stateCapability, payload.chatId, identity.chatPK),
25718
+ retirement: null
25352
25719
  } : makeOwnChatEntry(epochState, {
25353
25720
  routes,
25354
25721
  startMs: nowMs2,
@@ -25364,6 +25731,9 @@ async function processOpenedPing(cloud, uid, identity, ping, openedProfile, hint
25364
25731
  ...welcomeMlsState ? { mlsWrites: [{ id: welcomeMlsState.stateId, value: welcomeMlsState.value }] } : {}
25365
25732
  };
25366
25733
  }
25734
+ if (membershipWelcome && current.retirement && epochState.manifest.epochVersion <= current.retirement.epochVersion) {
25735
+ throw new Error("chat welcome is not newer than retirement");
25736
+ }
25367
25737
  if (current.current.manifest.epochVersion > epochState.manifest.epochVersion) {
25368
25738
  return { result: current };
25369
25739
  }
@@ -25372,7 +25742,8 @@ async function processOpenedPing(cloud, uid, identity, ping, openedProfile, hint
25372
25742
  current: nextEntry.current,
25373
25743
  routes: withSenderRoute(current, payload, senderProfile, epochState),
25374
25744
  deliveryRegistered: current.deliveryRegistered || nextEntry.deliveryRegistered,
25375
- notificationTag: nextEntry.notificationTag
25745
+ notificationTag: nextEntry.notificationTag,
25746
+ retirement: membershipWelcome ? null : current.retirement
25376
25747
  };
25377
25748
  if (sameStableEntry(current, candidate) && !welcomeMlsState)
25378
25749
  return { result: current };
@@ -25382,6 +25753,9 @@ async function processOpenedPing(cloud, uid, identity, ping, openedProfile, hint
25382
25753
  ...welcomeMlsState ? { mlsWrites: [{ id: welcomeMlsState.stateId, value: welcomeMlsState.value }] } : {}
25383
25754
  };
25384
25755
  }) : entry;
25756
+ if (committedEntry?.retirement) {
25757
+ throw new Error("chat membership remains retired");
25758
+ }
25385
25759
  if (membershipWelcome && epochState.mlsSnapshot) {
25386
25760
  if (!epochState.mlsKeyPackageLastResort) {
25387
25761
  await cloud.user.mls.keyPackages.deleteState(identity.uid, epochState.mlsKeyPackageId).catch(() => false);
@@ -25425,8 +25799,15 @@ async function processPing(cloud, uid, identity, doc, options) {
25425
25799
  async function applyEpochTransitionAction(cloud, uid, identityValue, entry, action, options, confirmedValue = null) {
25426
25800
  const parent = entry.current.manifest;
25427
25801
  const sender = manifestMember(parent, action.s || action.certificate?.actor);
25428
- if (!sender)
25802
+ const reject = (stage) => {
25803
+ markDiag(options.diag, "chat.epoch.apply.reject", {
25804
+ stage,
25805
+ epochVersion: parent.epochVersion
25806
+ });
25429
25807
  return null;
25808
+ };
25809
+ if (!sender)
25810
+ return reject("sender");
25430
25811
  const engine = await options.mls?.open?.();
25431
25812
  if (!engine)
25432
25813
  throw new Error("mls engine required");
@@ -25434,25 +25815,25 @@ async function applyEpochTransitionAction(cloud, uid, identityValue, entry, acti
25434
25815
  const expectedCommitment = transitionCommitment(action.certificate);
25435
25816
  const confirmed = confirmedValue || await cloud.chat.state.transition(parent.chatId, parent.epochId, entry.current.stateCapability).catch(() => null);
25436
25817
  if (!confirmed || confirmed.parentEpochVersion !== parent.epochVersion || confirmed.nextEpochId !== action.certificate?.nextEpochId || confirmed.nextEpochVersion !== action.certificate?.nextEpochVersion || confirmed.transitionCommitment !== expectedCommitment || confirmed.transitionMessageId !== messageId || confirmed.mlsPackageId !== action.mls?.packageId || confirmed.mlsPackageDigest !== action.mls?.packageDigest)
25437
- return null;
25818
+ return reject("certificate");
25438
25819
  let leaveProposalId = "";
25439
25820
  if (action.membership?.kind === "leave") {
25440
25821
  const leavingChatPK = cleanText(action.membership?.actor);
25441
25822
  const proposalId = cleanText(action.membership?.proposalId);
25442
25823
  const targetSet = new Set(Array.isArray(action.membership?.targets) ? action.membership.targets.map(cleanText) : []);
25443
25824
  if (!leavingChatPK || !proposalId || targetSet.size !== 1 || !targetSet.has(leavingChatPK))
25444
- return null;
25825
+ return reject("leave-certificate");
25445
25826
  const proposalRecord = await cloud.chat.messages.read(parent.epochId, proposalId, {
25446
25827
  lane: messageLaneForEpoch(entry.current)
25447
25828
  }).catch(() => null);
25448
25829
  const proposal = proposalRecord ? await decryptMsg(proposalRecord, "", "", "", { chatId: parent.chatId, epochState: entry.current }).catch(() => null) : null;
25449
25830
  if (!isLeaveProposalMsg(proposal) || proposal.s !== leavingChatPK)
25450
- return null;
25831
+ return reject("leave-proposal");
25451
25832
  leaveProposalId = proposalId;
25452
25833
  }
25453
25834
  const packageRecord = await cloud.chat.mls.packages.read(parent.chatId, confirmed.mlsPackageId);
25454
25835
  if (!packageRecord || packageRecord.digest !== confirmed.mlsPackageDigest || packageRecord.head.transitionCommitment !== confirmed.transitionCommitment || !verifyChatMlsEpochPackageSignature(packageRecord, sender.chatSigningPK))
25455
- return null;
25836
+ return reject("mls-package");
25456
25837
  const identity = {
25457
25838
  ...identityValue,
25458
25839
  uid,
@@ -25463,8 +25844,11 @@ async function applyEpochTransitionAction(cloud, uid, identityValue, entry, acti
25463
25844
  let parentSnapshot = null;
25464
25845
  let nextSnapshot = null;
25465
25846
  let epochState = null;
25847
+ let openStage = "owner-snapshot";
25848
+ const openStartedAt = Date.now();
25466
25849
  try {
25467
25850
  parentSnapshot = await readOwnerChatMlsSnapshot(cloud, identity, entryId, entry.current);
25851
+ openStage = "mls-commit";
25468
25852
  const processed = engine.processCommit(parentSnapshot, packageRecord.commit);
25469
25853
  nextSnapshot = processed.snapshot;
25470
25854
  if (processed.removed) {
@@ -25476,16 +25860,21 @@ async function applyEpochTransitionAction(cloud, uid, identityValue, entry, acti
25476
25860
  });
25477
25861
  await cloud.delivery.revoke(capability).catch(() => false);
25478
25862
  }
25479
- if (options.wasRemoved?.(parent.chatId) !== true) {
25480
- markDiag(options.diag, "chat.owner.unlink", { reason: "membership" });
25481
- await cloud.user.chats.unlink(uid, entryId);
25482
- }
25863
+ markDiag(options.diag, "chat.owner.retire", { reason: "membership" });
25864
+ const retiredEntry = await retireChatMembershipOwner(cloud, identity, entry, {
25865
+ kind: CHAT_OWNER_RETIREMENT_KINDS.REMOVED,
25866
+ epochVersion: confirmed.nextEpochVersion,
25867
+ cutoffMs: Date.now(),
25868
+ successorTransitionCommitment: confirmed.transitionCommitment,
25869
+ successorTransitionMessageId: messageId
25870
+ });
25483
25871
  if (leaveProposalId) {
25484
25872
  options.leaveProposalObservedAt?.delete(`${parent.chatId}:${leaveProposalId}`);
25485
25873
  }
25486
- options.onRemoved?.(parent.chatId);
25487
- return { removed: true, chatId: parent.chatId };
25874
+ options.onRemoved?.(parent.chatId, confirmed.nextEpochVersion);
25875
+ return { removed: true, chatId: parent.chatId, entry: retiredEntry };
25488
25876
  }
25877
+ openStage = "mls-package-open";
25489
25878
  const opened = await openChatMlsEpochPackage(processed, packageRecord, {
25490
25879
  chatId: parent.chatId,
25491
25880
  senderSigningPK: sender.chatSigningPK,
@@ -25500,8 +25889,13 @@ async function applyEpochTransitionAction(cloud, uid, identityValue, entry, acti
25500
25889
  mlsPackageId: opened.packageId,
25501
25890
  mlsPackageDigest: opened.packageDigest
25502
25891
  };
25892
+ openStage = "settings";
25503
25893
  epochState.settings = await readEpochChatSettings(cloud, epochState);
25504
- } catch {
25894
+ } catch (error) {
25895
+ markError(options.diag, "chat.epoch.apply", openStartedAt, error, {
25896
+ stage: openStage,
25897
+ epochVersion: parent.epochVersion
25898
+ });
25505
25899
  cleanBytes(parentSnapshot, nextSnapshot, epochState?.epochSecret, epochState?.stateCapability);
25506
25900
  return null;
25507
25901
  }
@@ -25588,6 +25982,42 @@ async function advanceEntryToEpoch(cloud, uid, identity, entry, targetEpochVersi
25588
25982
  }).catch(() => null);
25589
25983
  const action = record ? await decryptMsg(record, "", "", "", { chatId: parent.chatId, epochState: current.current }).catch(() => null) : null;
25590
25984
  if (!isEpochTransitionMsg(action)) {
25985
+ markDiag(options.diag, "chat.epoch.advance.pending", {
25986
+ stage: record ? "transition-open" : "transition-record",
25987
+ epochVersion: parent.epochVersion
25988
+ });
25989
+ const error = new Error("chat epoch transition message is not available yet");
25990
+ error.code = "chat/epoch-pending";
25991
+ throw error;
25992
+ }
25993
+ const advanced = await applyEpochTransitionAction(cloud, uid, identity, current, action, options, confirmed);
25994
+ if (advanced?.removed)
25995
+ return advanced;
25996
+ if (!advanced?.current?.manifest || advanced.current.manifest.epochVersion <= parent.epochVersion) {
25997
+ const error = new Error("chat epoch transition could not be verified");
25998
+ error.code = "chat/epoch-pending";
25999
+ throw error;
26000
+ }
26001
+ current = advanced;
26002
+ }
26003
+ return current;
26004
+ }
26005
+ async function advanceEntryToLatestEpoch(cloud, uid, identity, entry, options = {}) {
26006
+ let current = entry;
26007
+ while (current?.current?.manifest) {
26008
+ const parent = current.current.manifest;
26009
+ const confirmed = await cloud.chat.state.transition(parent.chatId, parent.epochId, current.current.stateCapability);
26010
+ if (!confirmed?.transitionMessageId)
26011
+ return current;
26012
+ const record = await cloud.chat.messages.read(parent.epochId, confirmed.transitionMessageId, {
26013
+ lane: messageLaneForEpoch(current.current)
26014
+ }).catch(() => null);
26015
+ const action = record ? await decryptMsg(record, "", "", "", { chatId: parent.chatId, epochState: current.current }).catch(() => null) : null;
26016
+ if (!isEpochTransitionMsg(action)) {
26017
+ markDiag(options.diag, "chat.epoch.advance.pending", {
26018
+ stage: record ? "transition-open" : "transition-record",
26019
+ epochVersion: parent.epochVersion
26020
+ });
25591
26021
  const error = new Error("chat epoch transition message is not available yet");
25592
26022
  error.code = "chat/epoch-pending";
25593
26023
  throw error;
@@ -25705,6 +26135,7 @@ var init_inbox2 = __esm(() => {
25705
26135
  init_membership();
25706
26136
  init_entry();
25707
26137
  init_owner();
26138
+ init_retirement();
25708
26139
  init_manifest();
25709
26140
  init_state2();
25710
26141
  init_transition();
@@ -25724,6 +26155,17 @@ var init_inbox2 = __esm(() => {
25724
26155
  });
25725
26156
 
25726
26157
  // ../../core/chat/list.js
26158
+ function chatOwnerRevision(chat) {
26159
+ return Number.isSafeInteger(chat?.ownEntry?.ownerRevision) ? chat.ownEntry.ownerRevision : 0;
26160
+ }
26161
+ function serverChatCoversOverlay(chat, overlay) {
26162
+ const serverEpoch = Number.isSafeInteger(chat?.epochVersion) ? chat.epochVersion : 0;
26163
+ const overlayEpoch = Number.isSafeInteger(overlay?.epochVersion) ? overlay.epochVersion : 0;
26164
+ if (serverEpoch !== overlayEpoch) {
26165
+ return serverEpoch > overlayEpoch;
26166
+ }
26167
+ return chatOwnerRevision(chat) >= chatOwnerRevision(overlay);
26168
+ }
25727
26169
  function inboxRetryKey(doc) {
25728
26170
  const id = typeof doc?.id === "string" ? doc.id : "";
25729
26171
  const ts = timestampMs(doc?.ts, 0) || 0;
@@ -25796,13 +26238,17 @@ function listenToChats(cloud, uid, userChatPK, userPrivKey, onUpdate, onError, o
25796
26238
  nextAfterChat: null,
25797
26239
  hasMore: false
25798
26240
  };
25799
- const optimisticChats = new Map;
25800
- const mergedChats = (chats) => {
26241
+ const optimisticChats = options.optimisticChats instanceof Map ? options.optimisticChats : new Map;
26242
+ const mergedChats = (chats, mergeOptions = {}) => {
25801
26243
  const nextById = new Map;
25802
26244
  for (const chat of chats || []) {
25803
26245
  const optimistic = optimisticChats.get(chat?.id);
25804
- if (chat?.id)
26246
+ if (chat?.id) {
25805
26247
  nextById.set(chat.id, mergeChatActivity(chat, optimistic, userChatPK));
26248
+ if (mergeOptions.settle === true && optimistic && serverChatCoversOverlay(chat, optimistic)) {
26249
+ optimisticChats.delete(chat.id);
26250
+ }
26251
+ }
25806
26252
  }
25807
26253
  for (const [id, chat] of optimisticChats.entries()) {
25808
26254
  if (!nextById.has(id)) {
@@ -25867,7 +26313,7 @@ function listenToChats(cloud, uid, userChatPK, userPrivKey, onUpdate, onError, o
25867
26313
  if (!closed && runId === run) {
25868
26314
  processedFirst = true;
25869
26315
  const chats = page.chats;
25870
- latestChats = mergedChats(chats);
26316
+ latestChats = mergedChats(chats, { settle: true });
25871
26317
  latestMeta = {
25872
26318
  nextAfterChat: snapshot.nextAfterChat,
25873
26319
  hasMore: snapshot.hasMore
@@ -26110,6 +26556,9 @@ async function decryptChatEntry(entryRecord, userChatPK, userPrivKey) {
26110
26556
  if (entry.ownerRevision !== data?.revision) {
26111
26557
  throw new Error("chat owner revision mismatch");
26112
26558
  }
26559
+ if (entry.retirement) {
26560
+ return null;
26561
+ }
26113
26562
  if (entry.protocol !== CHAT_PROTOCOL_VERSION) {
26114
26563
  return null;
26115
26564
  }
@@ -26249,6 +26698,7 @@ function createChatList({
26249
26698
  localByChatRef,
26250
26699
  pendingDeleteIdsRef,
26251
26700
  deletedChatIdsRef,
26701
+ removedChatVersionsRef,
26252
26702
  keepSelectedDeletedChatIdsRef,
26253
26703
  readCacheRef,
26254
26704
  lastServerChatsRef,
@@ -26266,6 +26716,7 @@ function createChatList({
26266
26716
  onTransition,
26267
26717
  onRemoved,
26268
26718
  wasRemoved,
26719
+ restoreRemovedChat,
26269
26720
  resetExternalChatState,
26270
26721
  mutationGate,
26271
26722
  onMlsKeyPackageConsumed
@@ -26294,6 +26745,8 @@ function createChatList({
26294
26745
  const cachedHistoryEndRef = { current: false };
26295
26746
  const cachedHistoryHeadRef = { current: [] };
26296
26747
  const previewTimerRef = { current: null };
26748
+ const epochReconciliations = new Map;
26749
+ const inboxOverlayChats = new Map;
26297
26750
  let listening = false;
26298
26751
  let unsubscribeChats = null;
26299
26752
  let snapshot = null;
@@ -26302,7 +26755,7 @@ function createChatList({
26302
26755
  let blockedUidSet = new Set(blockedUids);
26303
26756
  let schedulePreviewExpiry = () => {};
26304
26757
  let fillVisibleChatPage = () => {};
26305
- const isHiddenChatId = (chatId) => !!chatId && (pendingDeleteIdsRef.current.has(chatId) || deletedChatIdsRef.current.has(chatId));
26758
+ const isHiddenChatId = (chatId) => !!chatId && (pendingDeleteIdsRef.current.has(chatId) || deletedChatIdsRef.current.has(chatId) || removedChatVersionsRef.current.has(chatId));
26306
26759
  const filterHiddenChats = (nextChats) => (nextChats || []).filter((chatItem) => chatItem?.id && !isHiddenChatId(chatItem.id));
26307
26760
  const nextValue = (current, next) => typeof next === "function" ? next(current) : next;
26308
26761
  const publish = () => {
@@ -26517,7 +26970,13 @@ function createChatList({
26517
26970
  return shownChats;
26518
26971
  };
26519
26972
  const applyInboxChat = (chat) => {
26520
- if (!chat?.id || isHiddenChatId(chat.id)) {
26973
+ if (!chat?.id) {
26974
+ return;
26975
+ }
26976
+ if (removedChatVersionsRef.current.has(chat.id)) {
26977
+ restoreRemovedChat?.(chat);
26978
+ }
26979
+ if (isHiddenChatId(chat.id)) {
26521
26980
  return;
26522
26981
  }
26523
26982
  const updateStartedAt = Date.now();
@@ -26543,13 +27002,113 @@ function createChatList({
26543
27002
  const applyInboxDelete = (chatId) => {
26544
27003
  if (!chatId)
26545
27004
  return;
27005
+ inboxOverlayChats.delete(chatId);
26546
27006
  pendingDeleteIdsRef.current.delete(chatId);
26547
27007
  commitServerChats(lastServerChatsRef.current.filter((chat) => chat?.id !== chatId), {
26548
27008
  warm: false
26549
27009
  });
26550
27010
  setSelectedChat((current) => current === chatId ? null : current);
26551
27011
  };
27012
+ const reconcileChatEpoch = (chatId) => {
27013
+ if (!chatId || !uid || !chatPK || !chatPrivateKey || !chatSigningPK || !chatSigningSecret || !notificationPK) {
27014
+ return Promise.resolve({ advanced: false, chat: null, removed: false });
27015
+ }
27016
+ const active = epochReconciliations.get(chatId);
27017
+ if (active)
27018
+ return active;
27019
+ let task;
27020
+ task = (async () => {
27021
+ const releaseMutation = await mutationGate?.acquireExclusive(chatId) || (() => {});
27022
+ try {
27023
+ const initial = getChatEntry(chatId);
27024
+ if (!initial?.ownEntry)
27025
+ return { advanced: false, chat: initial || null, removed: false };
27026
+ const initialVersion = initial.epochVersion;
27027
+ const identity = {
27028
+ uid,
27029
+ chatPK,
27030
+ chatPrivateKey,
27031
+ chatSigningPK,
27032
+ chatSigningSecret,
27033
+ notificationPK,
27034
+ notificationPrivateKey
27035
+ };
27036
+ const advanceOptions = {
27037
+ blockedUids: blockedUidSet,
27038
+ mls,
27039
+ diag,
27040
+ onMlsKeyPackageConsumed,
27041
+ onRemoved: (removedChatId, epochVersion) => {
27042
+ onRemoved?.(removedChatId, epochVersion);
27043
+ applyInboxDelete(removedChatId);
27044
+ },
27045
+ wasRemoved,
27046
+ onTransition: (chat, message) => {
27047
+ onTransition?.(chat, message);
27048
+ applyInboxChat(chat);
27049
+ }
27050
+ };
27051
+ let working = initial;
27052
+ let result;
27053
+ while (true) {
27054
+ try {
27055
+ result = await advanceEntryToLatestEpoch(cloud, uid, identity, working.ownEntry, advanceOptions);
27056
+ break;
27057
+ } catch (error) {
27058
+ const code = String(error?.code || "").trim().toLowerCase();
27059
+ if (code === "permission-denied" || code.endsWith("/permission-denied")) {
27060
+ result = await retireConfirmedDeletedChat(cloud, uid, identity, working.ownEntry, {
27061
+ diag,
27062
+ onPingDelete: applyInboxDelete
27063
+ });
27064
+ break;
27065
+ }
27066
+ if (code !== "chat/epoch-pending")
27067
+ throw error;
27068
+ const canonical = await getChat(cloud, uid, chatId, chatPK, chatPrivateKey);
27069
+ if (!canonical?.ownEntry || canonical.epochVersion <= working.epochVersion)
27070
+ throw error;
27071
+ markDiag(diag, "chat.epoch.reconcile.rebase", {
27072
+ fromEpochVersion: working.epochVersion,
27073
+ toEpochVersion: canonical.epochVersion
27074
+ });
27075
+ applyInboxChat(canonical);
27076
+ working = canonical;
27077
+ }
27078
+ }
27079
+ if (result?.removed) {
27080
+ markDiag(diag, "chat.epoch.reconcile.done", {
27081
+ fromEpochVersion: initialVersion,
27082
+ removed: true
27083
+ });
27084
+ return { advanced: true, chat: null, removed: true };
27085
+ }
27086
+ const latest = getChatEntry(chatId);
27087
+ const advanced = Number(latest?.epochVersion || 0) > Number(initialVersion || 0);
27088
+ if (advanced) {
27089
+ markDiag(diag, "chat.epoch.reconcile.done", {
27090
+ fromEpochVersion: initialVersion,
27091
+ toEpochVersion: latest.epochVersion,
27092
+ removed: false
27093
+ });
27094
+ }
27095
+ return {
27096
+ advanced,
27097
+ chat: latest || null,
27098
+ removed: false
27099
+ };
27100
+ } finally {
27101
+ releaseMutation();
27102
+ }
27103
+ })().finally(() => {
27104
+ if (epochReconciliations.get(chatId) === task)
27105
+ epochReconciliations.delete(chatId);
27106
+ });
27107
+ epochReconciliations.set(chatId, task);
27108
+ return task;
27109
+ };
26552
27110
  const resetChatList = (ready = false) => {
27111
+ inboxOverlayChats.clear();
26553
27112
  chatSourceReady = ready;
26554
27113
  syncChatDataReady();
26555
27114
  setIsChatListLive(false);
@@ -26673,7 +27232,15 @@ function createChatList({
26673
27232
  };
26674
27233
  const unsubscribe = listenToChats(cloud, uid, chatPK, chatPrivateKey, (nextChats, _nextPeers, meta = {}) => {
26675
27234
  const updateStartedAt = Date.now();
26676
- const rawNextChats = filterHiddenChats(Array.isArray(nextChats) ? nextChats : []);
27235
+ const incomingChats = Array.isArray(nextChats) ? nextChats : [];
27236
+ if (meta?.source === "inbox") {
27237
+ for (const chat of incomingChats) {
27238
+ if (removedChatVersionsRef.current.has(chat?.id)) {
27239
+ restoreRemovedChat?.(chat);
27240
+ }
27241
+ }
27242
+ }
27243
+ const rawNextChats = filterHiddenChats(incomingChats);
26677
27244
  hydrateReadCache(rawNextChats, readCacheRef.current);
26678
27245
  const readAppliedChats = applyReadCache(rawNextChats, chatPK, readCacheRef.current);
26679
27246
  const trimmedChats = trimExpiredChatPreviews(readAppliedChats, { skipChatId: selectedChatIdRef.current });
@@ -26778,6 +27345,7 @@ function createChatList({
26778
27345
  blockedUids: blockedUidSet,
26779
27346
  localCache,
26780
27347
  diag,
27348
+ optimisticChats: inboxOverlayChats,
26781
27349
  getCurrentChat: (chatId) => lastServerChatsRef.current.find((chatItem) => chatItem?.id === chatId) || null,
26782
27350
  mutationGate,
26783
27351
  onMlsKeyPackageConsumed
@@ -26956,12 +27524,16 @@ function createChatList({
26956
27524
  };
26957
27525
  const hasChat = (chatId) => !!chatId && (serverChatIdsSet.has(chatId) || chatLoadsRef.current.has(chatId)) && !isHiddenChatId(chatId);
26958
27526
  const setSources = (nextSources) => {
27527
+ const accountChanged = cloud !== nextSources.cloud || uid !== nextSources.uid || chatPK !== nextSources.chatPK || chatPrivateKey !== nextSources.chatPrivateKey;
26959
27528
  const nextBlocked = sortedUniqueValues(Array.isArray(nextSources.blocked) ? nextSources.blocked : []);
26960
27529
  const nextBlockedUidsKey = nextBlocked.join("|");
26961
27530
  const nextBlockedReady = nextSources.blockedReady === true;
26962
27531
  const blockedFilterChanged = blockedUidsKey !== nextBlockedUidsKey || blockedReady !== nextBlockedReady;
26963
27532
  const listenerChanged = cloud !== nextSources.cloud || uid !== nextSources.uid || chatPK !== nextSources.chatPK || chatSigningPK !== nextSources.chatSigningPK || chatSigningSecret !== nextSources.chatSigningSecret || chatPrivateKey !== nextSources.chatPrivateKey || notificationPK !== nextSources.notificationPK || notificationPrivateKey !== nextSources.notificationPrivateKey || mls !== nextSources.mls || chatBanned !== nextSources.chatBanned || localCache !== nextSources.localCache || isActive !== nextSources.isActive;
26964
27533
  const selectedChatChanged = selectedChatId !== nextSources.selectedChatId;
27534
+ if (accountChanged) {
27535
+ inboxOverlayChats.clear();
27536
+ }
26965
27537
  ({
26966
27538
  cloud,
26967
27539
  uid,
@@ -26983,6 +27555,7 @@ function createChatList({
26983
27555
  localByChatRef,
26984
27556
  pendingDeleteIdsRef,
26985
27557
  deletedChatIdsRef,
27558
+ removedChatVersionsRef,
26986
27559
  keepSelectedDeletedChatIdsRef,
26987
27560
  readCacheRef,
26988
27561
  lastServerChatsRef,
@@ -27000,6 +27573,7 @@ function createChatList({
27000
27573
  onTransition,
27001
27574
  onRemoved,
27002
27575
  wasRemoved,
27576
+ restoreRemovedChat,
27003
27577
  resetExternalChatState,
27004
27578
  mutationGate,
27005
27579
  onMlsKeyPackageConsumed
@@ -27043,6 +27617,8 @@ function createChatList({
27043
27617
  previewTimerRef.current = null;
27044
27618
  }
27045
27619
  flushChatCacheWrite();
27620
+ epochReconciliations.clear();
27621
+ inboxOverlayChats.clear();
27046
27622
  subscribers.clear();
27047
27623
  };
27048
27624
  const subscribe = (subscriber) => {
@@ -27068,6 +27644,7 @@ function createChatList({
27068
27644
  getChatPreviewKey: getChatPreviewKey2,
27069
27645
  getChatRetention,
27070
27646
  getChatEntry,
27647
+ reconcileChatEpoch,
27071
27648
  sendOptionsForPeer,
27072
27649
  hasChat
27073
27650
  };
@@ -27078,32 +27655,13 @@ var init_listsession = __esm(() => {
27078
27655
  init_localdata();
27079
27656
  init_chats();
27080
27657
  init_list();
27658
+ init_inbox2();
27081
27659
  init_ttl();
27082
27660
  init_ids();
27083
27661
  init_time();
27084
27662
  CHAT_LIST_RESET_ERROR_CODES = new Set(["permission-denied", "unauthenticated"]);
27085
27663
  });
27086
27664
 
27087
- // ../../core/chat/retirement.js
27088
- function planChatRetirement(chat, selfChatPK) {
27089
- const self2 = cleanText(selfChatPK);
27090
- const members = Array.isArray(chat?.members) ? chat.members : [];
27091
- const remaining = members.filter((member) => cleanText(member?.chatPK) !== self2);
27092
- const direct = chat?.lineage === CHAT_LINEAGES.DIRECT && members.length === 2;
27093
- return {
27094
- action: direct || !remaining.length ? CHAT_RETIREMENT_ACTIONS.DELETE : CHAT_RETIREMENT_ACTIONS.LEAVE,
27095
- remaining
27096
- };
27097
- }
27098
- var CHAT_RETIREMENT_ACTIONS;
27099
- var init_retirement = __esm(() => {
27100
- init_manifest();
27101
- CHAT_RETIREMENT_ACTIONS = Object.freeze({
27102
- DELETE: "delete",
27103
- LEAVE: "leave"
27104
- });
27105
- });
27106
-
27107
27665
  // ../../core/utils/appstate.js
27108
27666
  function isForegroundAppState(state) {
27109
27667
  const value = String(state || "").trim().toLowerCase();
@@ -27282,7 +27840,6 @@ function createChatSession({
27282
27840
  const closeMessageBatchRef = { current: null };
27283
27841
  const localMessageDiagRef = { current: "" };
27284
27842
  const epochHistoryRef = { current: new Map };
27285
- const epochRecoveryRef = { current: new Map };
27286
27843
  const settingsUpdateStatesRef = { current: new Map };
27287
27844
  const membershipUpdateStatesRef = { current: new Map };
27288
27845
  const directChatOpensRef = { current: new Map };
@@ -27321,6 +27878,7 @@ function createChatSession({
27321
27878
  kickChatMember,
27322
27879
  leaveChat,
27323
27880
  updateChatSettings: updateChatSettings2,
27881
+ updateChatAvatar,
27324
27882
  dropChat,
27325
27883
  dropUnavailableChat,
27326
27884
  deleteChat,
@@ -27328,6 +27886,7 @@ function createChatSession({
27328
27886
  deleteAllChats,
27329
27887
  restoreDeletedChat,
27330
27888
  enterChatActivity,
27889
+ flushChatReadFrontier,
27331
27890
  leaveChatActivity,
27332
27891
  markChatReadState,
27333
27892
  markChatRead,
@@ -27779,51 +28338,15 @@ function createChatSession({
27779
28338
  return true;
27780
28339
  };
27781
28340
  const handleBatchMessages = (chatId, messages) => {
27782
- const transition = [...messages || []].reverse().find(isEpochTransitionMsg);
27783
- const nextEpochId = transition?.certificate?.nextEpochId;
27784
- const chat = getChatEntry(chatId);
27785
- if (!nextEpochId || !chat?.ownEntry || chat.epochId === nextEpochId || epochRecoveryRef.current.has(nextEpochId))
28341
+ if (![...messages || []].reverse().some(isEpochTransitionMsg))
27786
28342
  return;
27787
- const recover = (attempt = 0) => {
27788
- const timer = attempt > 0 ? setTimeout(run, [0, 250, 1000, 3000][attempt] || 3000) : null;
27789
- epochRecoveryRef.current.set(nextEpochId, timer || true);
27790
- if (!timer)
27791
- run();
27792
- async function run() {
27793
- let releaseMutation = () => {};
27794
- try {
27795
- releaseMutation = await mutationGate.acquireExclusive(chatId);
27796
- const current = getChatEntry(chatId);
27797
- if (!current?.ownEntry || current.epochId === nextEpochId)
27798
- return;
27799
- const advanced = await processEpochTransitionMessages(cloud, uid, {
27800
- chatPK,
27801
- chatPrivateKey,
27802
- notificationPK
27803
- }, current.ownEntry, messages, {
27804
- blockedUids: new Set(blocked),
27805
- notificationPrivateKey,
27806
- diag,
27807
- onRemoved: (removedChatId) => deleteActions.dropRemovedChat(removedChatId),
27808
- wasRemoved: (removedChatId) => deleteActions.deletedChatIdsRef.current.has(removedChatId),
27809
- onTransition: stageCommittedMembershipMessage
27810
- });
27811
- if (advanced)
27812
- epochHistoryRef.current.delete(chatId);
27813
- else if (attempt < 3)
27814
- return recover(attempt + 1);
27815
- } catch {
27816
- if (attempt < 3)
27817
- return recover(attempt + 1);
27818
- } finally {
27819
- releaseMutation();
27820
- if (epochRecoveryRef.current.get(nextEpochId) === (timer || true)) {
27821
- epochRecoveryRef.current.delete(nextEpochId);
27822
- }
27823
- }
27824
- }
27825
- };
27826
- recover();
28343
+ const startedAt = Date.now();
28344
+ chatListOwner.reconcileChatEpoch(chatId).then((result) => {
28345
+ if (result.advanced)
28346
+ epochHistoryRef.current.delete(chatId);
28347
+ }).catch((error) => {
28348
+ markError(diag, "chat.epoch.reconcile", startedAt, error);
28349
+ });
27827
28350
  };
27828
28351
  const setChats = (...args) => chatListOwner.setChats(...args);
27829
28352
  const setLastChat = (...args) => chatListOwner.setLastChat(...args);
@@ -27841,11 +28364,6 @@ function createChatSession({
27841
28364
  liveActions?.close?.();
27842
28365
  liveActivityByChat = new Map;
27843
28366
  messageBatchOwner.clear();
27844
- for (const pending of epochRecoveryRef.current.values()) {
27845
- if (typeof pending === "number")
27846
- clearTimeout(pending);
27847
- }
27848
- epochRecoveryRef.current.clear();
27849
28367
  epochHistoryRef.current.clear();
27850
28368
  membershipUpdateStatesRef.current.clear();
27851
28369
  directChatOpensRef.current.clear();
@@ -28295,14 +28813,18 @@ function createChatSession({
28295
28813
  recipientChatPK: chatPK,
28296
28814
  generation: chat.epochVersion
28297
28815
  });
28816
+ await retireChatMembershipOwner(cloud, transitionIdentity(), chat.ownEntry, {
28817
+ kind: CHAT_OWNER_RETIREMENT_KINDS.LEAVE,
28818
+ epochVersion: chat.epochVersion,
28819
+ cutoffMs: Date.now(),
28820
+ leaveProposalId: proposal.message.cid
28821
+ });
28298
28822
  await cloud.delivery.revoke(capability).catch(() => false);
28299
- await discardOwnerChatMlsSnapshot(cloud, transitionIdentity(), chat.entryId, chat.epochState);
28300
- await cloud.user.chats.unlink(uid, chat.entryId);
28301
28823
  if (retirementStarted) {
28302
- deleteActions.commitRetirement(chatId);
28824
+ deleteActions.commitRemovedRetirement(chatId, chat.epochVersion + 1);
28303
28825
  retirementStarted = false;
28304
28826
  } else {
28305
- deleteActions.dropChat(chatId);
28827
+ deleteActions.dropRemovedChat(chatId, chat.epochVersion + 1);
28306
28828
  }
28307
28829
  return { left: true, message: proposal.message };
28308
28830
  } catch (error) {
@@ -28364,35 +28886,55 @@ function createChatSession({
28364
28886
  clearLocalMembershipMessage(chatId, cid);
28365
28887
  };
28366
28888
  state.promise = (async () => {
28367
- const releaseMutation = await mutationGate.acquireExclusive(chatId);
28368
28889
  try {
28369
- const chat = requireTransitionChat(chatId);
28370
- const members = prepareMembers ? await prepareMembers(chat) : resolveMembers(chat);
28371
- if (!members) {
28372
- restoreAuthoritativeChat();
28373
- return { entry: chat.ownEntry, unchanged: true };
28374
- }
28375
- if (!members.length) {
28376
- restoreAuthoritativeChat();
28377
- await deleteActions.deleteChat(chat);
28378
- return { deleted: true };
28379
- }
28380
- const result = await transitionChatEpoch(cloud, transitionIdentity(), chat.ownEntry, { members, cid, mls });
28381
- if (result.entry) {
28382
- const projected = projectOwnChatEntry(result.entry, chat.entryId, chatPK, chat.ts);
28383
- chatListOwner.commitServerChats(sortedChats([projected], lastServerChatsRef.current));
28384
- } else {
28385
- chatListOwner.commitServerChats(lastServerChatsRef.current.filter((chatItem) => chatItem?.id !== chatId), { warm: false });
28386
- setSelectedChat((current) => current === chatId ? null : current);
28387
- }
28388
- if (optimisticRoster && result.message) {
28389
- stageCommittedMembershipMessage(chat, result.message);
28390
- } else if (optimisticRoster) {
28391
- clearLocalMembershipMessage(chatId, cid);
28890
+ while (true) {
28891
+ const releaseMutation = await mutationGate.acquireExclusive(chatId);
28892
+ let attemptedEpochVersion = 0;
28893
+ try {
28894
+ const chat = requireTransitionChat(chatId);
28895
+ attemptedEpochVersion = chat.epochVersion;
28896
+ const members = prepareMembers ? await prepareMembers(chat) : resolveMembers(chat);
28897
+ if (!members) {
28898
+ restoreAuthoritativeChat();
28899
+ return { entry: chat.ownEntry, unchanged: true };
28900
+ }
28901
+ if (!members.length) {
28902
+ restoreAuthoritativeChat();
28903
+ await deleteActions.deleteChat(chat);
28904
+ return { deleted: true };
28905
+ }
28906
+ const result = await transitionChatEpoch(cloud, transitionIdentity(), chat.ownEntry, { members, cid, mls });
28907
+ if (result.entry) {
28908
+ const projected = projectOwnChatEntry(result.entry, chat.entryId, chatPK, chat.ts);
28909
+ chatListOwner.commitServerChats(sortedChats([projected], lastServerChatsRef.current));
28910
+ } else {
28911
+ chatListOwner.commitServerChats(lastServerChatsRef.current.filter((chatItem) => chatItem?.id !== chatId), { warm: false });
28912
+ setSelectedChat((current) => current === chatId ? null : current);
28913
+ }
28914
+ if (optimisticRoster && result.message) {
28915
+ stageCommittedMembershipMessage(chat, result.message);
28916
+ } else if (optimisticRoster) {
28917
+ clearLocalMembershipMessage(chatId, cid);
28918
+ }
28919
+ if (optimisticRoster)
28920
+ setPendingChatMembers(chatId, null);
28921
+ return result;
28922
+ } catch (error) {
28923
+ releaseMutation();
28924
+ if (!attemptedEpochVersion || !isChatEpochConflict(error))
28925
+ throw error;
28926
+ await chatListOwner.reconcileChatEpoch(chatId);
28927
+ const currentEpochVersion = getChatEntry(chatId)?.epochVersion || 0;
28928
+ if (currentEpochVersion <= attemptedEpochVersion)
28929
+ throw error;
28930
+ markDiag(diag, "chat.membership.epoch.advance", {
28931
+ fromEpochVersion: attemptedEpochVersion,
28932
+ toEpochVersion: currentEpochVersion
28933
+ });
28934
+ } finally {
28935
+ releaseMutation();
28936
+ }
28392
28937
  }
28393
- if (optimisticRoster)
28394
- setPendingChatMembers(chatId, null);
28395
- return result;
28396
28938
  } catch (error) {
28397
28939
  restoreAuthoritativeChat();
28398
28940
  throw error;
@@ -28400,7 +28942,6 @@ function createChatSession({
28400
28942
  if (membershipUpdateStatesRef.current.get(chatId) === state) {
28401
28943
  membershipUpdateStatesRef.current.delete(chatId);
28402
28944
  }
28403
- releaseMutation();
28404
28945
  }
28405
28946
  })();
28406
28947
  membershipUpdateStatesRef.current.set(chatId, state);
@@ -28523,6 +29064,26 @@ function createChatSession({
28523
29064
  }
28524
29065
  return state.promise;
28525
29066
  };
29067
+ const updateChatAvatar = async (chatId, data) => {
29068
+ assertChatAvatarBytes(data);
29069
+ const chat = requireTransitionChat(chatId);
29070
+ if (chat.lineage !== "group")
29071
+ throw new Error("group chat required");
29072
+ const epochId = chat?.epochState?.manifest?.epochId;
29073
+ if (!epochId)
29074
+ throw new Error("current chat epoch unavailable");
29075
+ const uploadChatMedia = cloud?.chat?.media?.upload;
29076
+ if (typeof uploadChatMedia !== "function")
29077
+ throw new Error("chat media upload unavailable");
29078
+ const cid = makeCid();
29079
+ const file = typeof media?.uploadChatAvatar === "function" ? await media.uploadChatAvatar({ cid, data, epochId, uploadChatMedia }) : await putChatFile(epochId, cid, data, {
29080
+ uploadChatMedia,
29081
+ cacheControl: "private, max-age=31536000, immutable, no-transform"
29082
+ });
29083
+ const avatarRef = makeChatAvatarRef(file);
29084
+ await updateChatSettings2(chatId, { avatarRef });
29085
+ return avatarRef;
29086
+ };
28526
29087
  const adoptCommittedSettingsMessage = ({ chatId, message }) => {
28527
29088
  if (!chatId || !message?.cid)
28528
29089
  return;
@@ -28615,11 +29176,12 @@ function createChatSession({
28615
29176
  const dropChat = (...args) => deleteActions.dropChat(...args);
28616
29177
  const dropUnavailableChat = (...args) => deleteActions.dropUnavailableChat(...args);
28617
29178
  const deleteChat = (...args) => deleteActions.deleteChat(...args);
28618
- const retireChats = async (affected) => {
29179
+ const purgePeerChats = async (affected) => {
28619
29180
  for (const chat of affected) {
28620
29181
  const retirement = planChatRetirement(chat, chatPK);
28621
29182
  if (retirement.action === CHAT_RETIREMENT_ACTIONS.LEAVE) {
28622
29183
  await leaveOwnedChat(chat, { optimistic: false });
29184
+ await cloud.user.chats.unlink(uid, chat.entryId);
28623
29185
  } else {
28624
29186
  await deleteChat(chat, { cleanup: false });
28625
29187
  }
@@ -28629,7 +29191,7 @@ function createChatSession({
28629
29191
  const retirePeerChat = async (peerChatPK, options = {}) => {
28630
29192
  const allChats = await loadAllChats(cloud, uid, chatPK, chatPrivateKey);
28631
29193
  const affected = allChats.filter((chat) => chat?.members?.some((member) => member.chatPK === peerChatPK));
28632
- await retireChats(affected);
29194
+ await purgePeerChats(affected);
28633
29195
  await options.onDeliveryRevoked?.();
28634
29196
  return affected.length > 0;
28635
29197
  };
@@ -28638,14 +29200,21 @@ function createChatSession({
28638
29200
  return 0;
28639
29201
  const allChats = await loadAllChats(cloud, uid, chatPK, chatPrivateKey);
28640
29202
  const affected = allChats.filter((chat) => chat?.members?.some((member) => member.chatPK !== chatPK && blockedUids.has(member.uid)));
28641
- return retireChats(affected);
29203
+ return purgePeerChats(affected);
28642
29204
  };
28643
29205
  const restoreDeletedChat = (...args) => deleteActions.restoreDeletedChat(...args);
28644
29206
  const wasChatDeletedLocally = (...args) => deleteActions.wasChatDeletedLocally(...args);
28645
29207
  const ackDeletedChat = (...args) => deleteActions.ackDeletedChat(...args);
28646
29208
  const markChatReadState = (...args) => seenActions.markChatReadState(...args);
28647
29209
  const markChatRead = (...args) => seenActions.markChatRead(...args);
28648
- const enterChatActivity = (...args) => liveActions.enterChatActivity(...args);
29210
+ const enterChatActivity = (chatId) => {
29211
+ const reconcileStartedAt = Date.now();
29212
+ chatListOwner.reconcileChatEpoch(chatId).catch((error) => {
29213
+ markError(diag, "chat.epoch.reconcile", reconcileStartedAt, error);
29214
+ });
29215
+ return liveActions.enterChatActivity(chatId);
29216
+ };
29217
+ const flushChatReadFrontier = (chatId) => liveActions.flushChatReadFrontier(chatId);
28649
29218
  const leaveChatActivity = (chatId) => {
28650
29219
  const flush = seenActions.flushChatRead(chatId);
28651
29220
  liveActions.leaveChatActivity(chatId);
@@ -28892,6 +29461,7 @@ function createChatSession({
28892
29461
  listActionsRef: deleteListActionsRef,
28893
29462
  closeMessageBatchRef,
28894
29463
  mutationGate,
29464
+ reconcileEpoch: (chatId) => chatListOwner?.reconcileChatEpoch(chatId),
28895
29465
  diag
28896
29466
  });
28897
29467
  const createActionOwners = () => {
@@ -28971,6 +29541,7 @@ function createChatSession({
28971
29541
  adoptLocalMessageMedia,
28972
29542
  readMessageFile,
28973
29543
  mutationGate,
29544
+ reconcileEpoch: (chatId) => chatListOwner.reconcileChatEpoch(chatId),
28974
29545
  diag
28975
29546
  });
28976
29547
  reactionActions = createChatReaction({
@@ -29051,6 +29622,7 @@ function createChatSession({
29051
29622
  localByChatRef,
29052
29623
  pendingDeleteIdsRef: deleteActions.pendingDeleteIdsRef,
29053
29624
  deletedChatIdsRef: deleteActions.deletedChatIdsRef,
29625
+ removedChatVersionsRef: deleteActions.removedChatVersionsRef,
29054
29626
  keepSelectedDeletedChatIdsRef: deleteActions.keepSelectedDeletedChatIdsRef,
29055
29627
  readCacheRef,
29056
29628
  lastServerChatsRef,
@@ -29066,8 +29638,9 @@ function createChatSession({
29066
29638
  reconcileMessageBatches: (...args) => messageBatchOwner.reconcileChats(...args),
29067
29639
  warmChats: (...args) => messageBatchOwner.warm(...args),
29068
29640
  onTransition: stageCommittedMembershipMessage,
29069
- onRemoved: (removedChatId) => deleteActions.dropRemovedChat(removedChatId),
29070
- wasRemoved: (removedChatId) => deleteActions.deletedChatIdsRef.current.has(removedChatId),
29641
+ onRemoved: (removedChatId, epochVersion) => deleteActions.dropRemovedChat(removedChatId, epochVersion),
29642
+ wasRemoved: (removedChatId) => deleteActions.removedChatVersionsRef.current.has(removedChatId),
29643
+ restoreRemovedChat: (chat) => deleteActions.restoreRemovedChat(chat),
29071
29644
  resetExternalChatState,
29072
29645
  mutationGate,
29073
29646
  onMlsKeyPackageConsumed: markMlsKeyPackageConsumed
@@ -29208,12 +29781,13 @@ function createChatSession({
29208
29781
  var DEFAULT_READ_WRITE_INTERVAL_MS, DIAG_KEY_LEN = 12, EMPTY_LOCAL_MESSAGES;
29209
29782
  var init_session3 = __esm(() => {
29210
29783
  init_config();
29784
+ init_files();
29785
+ init_avatar2();
29211
29786
  init_ids();
29212
29787
  init_direct();
29213
29788
  init_create();
29214
29789
  init_membership();
29215
29790
  init_pool();
29216
- init_owner2();
29217
29791
  init_manifest();
29218
29792
  init_notifications();
29219
29793
  init_state2();
@@ -38087,7 +38661,8 @@ var init_links = __esm(() => {
38087
38661
  ])
38088
38662
  ]);
38089
38663
  storageCorsOrigins = Object.freeze([
38090
- origins.veyl
38664
+ origins.veyl,
38665
+ origins.veylDevWeb
38091
38666
  ]);
38092
38667
  devStorageCorsOrigins = Object.freeze([
38093
38668
  origins.veylDev,
@@ -38103,12 +38678,19 @@ var init_links = __esm(() => {
38103
38678
  });
38104
38679
 
38105
38680
  // ../../product/passkeylabel.js
38681
+ function twoWordLabelAt(index) {
38682
+ if (!Number.isSafeInteger(index) || index < 0) {
38683
+ throw new Error("two-word label index must be a non-negative integer");
38684
+ }
38685
+ const normalized = index % TWO_WORD_LABEL_COUNT;
38686
+ const adjective = adjectives[normalized % adjectives.length];
38687
+ const noun = nouns[Math.floor(normalized / adjectives.length)];
38688
+ return `${adjective} ${noun}`;
38689
+ }
38106
38690
  function randomPasskeyLabel() {
38107
- const adj = adjectives[Math.floor(Math.random() * adjectives.length)];
38108
- const noun = nouns[Math.floor(Math.random() * nouns.length)];
38109
- return `${adj} ${noun}`;
38691
+ return twoWordLabelAt(Math.floor(Math.random() * TWO_WORD_LABEL_COUNT));
38110
38692
  }
38111
- var adjectives, nouns;
38693
+ var adjectives, nouns, TWO_WORD_LABEL_COUNT;
38112
38694
  var init_passkeylabel = __esm(() => {
38113
38695
  adjectives = [
38114
38696
  "Ancient",
@@ -38175,7 +38757,6 @@ var init_passkeylabel = __esm(() => {
38175
38757
  "Grim",
38176
38758
  "Holy",
38177
38759
  "Infernal",
38178
- "Ivory",
38179
38760
  "Jade",
38180
38761
  "Light",
38181
38762
  "Midnight",
@@ -38319,6 +38900,7 @@ var init_passkeylabel = __esm(() => {
38319
38900
  "Wizard",
38320
38901
  "Wyvern"
38321
38902
  ];
38903
+ TWO_WORD_LABEL_COUNT = adjectives.length * nouns.length;
38322
38904
  });
38323
38905
 
38324
38906
  // src/auth.js
@@ -38684,6 +39266,7 @@ function simplifyAccountChat(chat, profiles = null) {
38684
39266
  members,
38685
39267
  title: chat.settings?.title || null,
38686
39268
  avatarRef: chat.settings?.avatarRef || null,
39269
+ retention: chat.settings?.retention || null,
38687
39270
  peerChatPK: chat.peerChatPK,
38688
39271
  peerUid: chat.peerUid || directPeer?.uid || null,
38689
39272
  username: directPeer?.username || null,
@@ -38936,6 +39519,8 @@ async function listenAccount(client, options = {}) {
38936
39519
  const includeChats = options.chats !== false;
38937
39520
  const includeTransactions = options.transactions !== false;
38938
39521
  const markRead = options.read !== false;
39522
+ const relayReads = markRead && options.relayReads === true;
39523
+ const readLiveIdleMs = Number.isFinite(Number(options.readLiveIdleMs)) ? Math.max(0, Math.floor(Number(options.readLiveIdleMs))) : 15000;
38939
39524
  const replay = options.replay === true;
38940
39525
  const persistentChatIdleMs = Number.isFinite(Number(options.persistentChatIdleMs)) ? Math.max(0, Math.floor(Number(options.persistentChatIdleMs))) : 0;
38941
39526
  const emit2 = typeof options.onEvent === "function" ? options.onEvent : () => {};
@@ -38946,8 +39531,12 @@ async function listenAccount(client, options = {}) {
38946
39531
  const chatActivityTimers = new Map;
38947
39532
  const subscribedWakeVersions = new Map;
38948
39533
  const idleWakeVersions = new Map;
39534
+ const messageBatchTasks = new Map;
38949
39535
  const pendingChats = new Map;
38950
39536
  const processedWakeVersions = new Map;
39537
+ const readLiveChats = new Set;
39538
+ const readLiveEntering = new Map;
39539
+ const readLiveTimers = new Map;
38951
39540
  const wantedWakeVersions = new Map;
38952
39541
  let activeChatIds = new Set;
38953
39542
  let chatSyncRequested = false;
@@ -38992,6 +39581,44 @@ async function listenAccount(client, options = {}) {
38992
39581
  const account = client.accountSummary();
38993
39582
  const runtime = client.runtime;
38994
39583
  const session = client.session;
39584
+ async function enterReadLive(chatId) {
39585
+ if (!relayReads)
39586
+ return false;
39587
+ if (readLiveChats.has(chatId))
39588
+ return true;
39589
+ const current = readLiveEntering.get(chatId);
39590
+ if (current)
39591
+ return current;
39592
+ const entering = runtime.chat.getSnapshot().enterChatActivity(chatId).then(async (entered) => {
39593
+ if (!entered)
39594
+ return false;
39595
+ if (closed) {
39596
+ await runtime.chat.getSnapshot().leaveChatActivity(chatId);
39597
+ return false;
39598
+ }
39599
+ readLiveChats.add(chatId);
39600
+ return true;
39601
+ }).finally(() => readLiveEntering.delete(chatId));
39602
+ readLiveEntering.set(chatId, entering);
39603
+ return entering;
39604
+ }
39605
+ function releaseReadLiveSoon(chatId) {
39606
+ if (!readLiveChats.has(chatId))
39607
+ return;
39608
+ const current = readLiveTimers.get(chatId);
39609
+ if (current)
39610
+ clearTimeout(current);
39611
+ const timer = setTimeout(() => {
39612
+ if (readLiveTimers.get(chatId) !== timer)
39613
+ return;
39614
+ readLiveTimers.delete(chatId);
39615
+ if (!readLiveChats.delete(chatId))
39616
+ return;
39617
+ runtime.chat.getSnapshot().leaveChatActivity(chatId);
39618
+ }, readLiveIdleMs);
39619
+ timer.unref?.();
39620
+ readLiveTimers.set(chatId, timer);
39621
+ }
38995
39622
  function projectedChat(chat) {
38996
39623
  const peerChatPK = getChatPeerPK(chat);
38997
39624
  const peerByChatPK = runtime.peers.getSnapshot().peerByChatPK;
@@ -39011,13 +39638,15 @@ async function listenAccount(client, options = {}) {
39011
39638
  const sourceMessages = batch?.sourceMessages?.length ? batch.sourceMessages : batch?.messages || [];
39012
39639
  return projectMessages(sourceMessages, session.chatPK, peerChatPK || session.chatPK, chat?.memberChatPKs).map((message) => simplifyAccountMessage(message, chat, session.chatPK, { raw: true }));
39013
39640
  }
39014
- function applyMessageBatch(chat, batch, emitChanges) {
39641
+ async function applyMessageBatch(chat, batch, emitChanges) {
39015
39642
  if (!batch?.ready)
39016
39643
  return;
39017
39644
  touchChatActivity(chat.id);
39018
39645
  const targetChat = projectedChat(chat);
39646
+ const rawMessages = batch?.sourceMessages?.length ? batch.sourceMessages : batch?.messages || [];
39019
39647
  const messages = projectMessages2(chat, batch);
39020
39648
  const currentKeys = new Set;
39649
+ const messageEvents = [];
39021
39650
  for (const message of messages) {
39022
39651
  const key = messageKey(message);
39023
39652
  if (!key)
@@ -39028,16 +39657,37 @@ async function listenAccount(client, options = {}) {
39028
39657
  seenMessages.set(key, version);
39029
39658
  if (!emitChanges || previous === version || incomingOnly && message.from !== "peer")
39030
39659
  continue;
39031
- emit2(formatMessageEvent(targetChat, message, { compact: compact2 }));
39032
- }
39033
- const latestPeer = [...messages].reverse().find((message) => message.from === "peer");
39034
- const latestPeerKey = latestPeer ? messageKey(latestPeer) : "";
39035
- if (markRead && latestPeerKey && readMessages.get(chat.id) !== latestPeerKey) {
39036
- readMessages.set(chat.id, latestPeerKey);
39037
- runtime.chat.getSnapshot().markChatReadState(chat.id, latestPeer.raw, {
39038
- messages: messages.map((message) => message.raw)
39039
- });
39660
+ messageEvents.push(formatMessageEvent(targetChat, message, { compact: compact2 }));
39661
+ }
39662
+ const latestPeer = [...rawMessages].reverse().find((message) => isPeerMsg(message, session.chatPK));
39663
+ const latestPeerKey = latestPeer ? messageKey({ ...latestPeer, chatId: chat.id }) : "";
39664
+ const latestPeerEvent = latestPeer ? formatMessageEvent(targetChat, simplifyAccountMessage(latestPeer, chat, session.chatPK, { raw: true }), { compact: compact2 }) : null;
39665
+ const readTarget = getLatestReadStateTarget(rawMessages);
39666
+ const readTargetKey = readTarget ? messageKey({ ...readTarget, chatId: chat.id }) : "";
39667
+ const alreadyProcessed = latestPeerEvent && options.isProcessedEvent?.(latestPeerEvent) === true;
39668
+ if (markRead && readTargetKey && !(alreadyProcessed && readTargetKey === latestPeerKey) && readMessages.get(chat.id) !== readTargetKey) {
39669
+ readMessages.set(chat.id, readTargetKey);
39670
+ if (relayReads) {
39671
+ try {
39672
+ await enterReadLive(chat.id);
39673
+ await runtime.chat.getSnapshot().markChatReadState(chat.id, readTarget, { messages: rawMessages });
39674
+ await runtime.chat.getSnapshot().flushChatReadFrontier?.(chat.id);
39675
+ } catch (error) {
39676
+ emit2(event("error", {
39677
+ peer: peerTarget(targetChat),
39678
+ message: error?.message || String(error)
39679
+ }));
39680
+ } finally {
39681
+ releaseReadLiveSoon(chat.id);
39682
+ }
39683
+ } else {
39684
+ runtime.chat.getSnapshot().markChatReadState(chat.id, readTarget, {
39685
+ messages: rawMessages
39686
+ });
39687
+ }
39040
39688
  }
39689
+ for (const messageEvent of messageEvents)
39690
+ emit2(messageEvent);
39041
39691
  const previousForChat = [...seenMessages.keys()].filter((key) => key.startsWith(`${chat.id}:`));
39042
39692
  for (const key of previousForChat) {
39043
39693
  if (currentKeys.has(key))
@@ -39056,6 +39706,21 @@ async function listenAccount(client, options = {}) {
39056
39706
  }
39057
39707
  }
39058
39708
  }
39709
+ function queueMessageBatch(chat, batch, emitChanges) {
39710
+ const previous = messageBatchTasks.get(chat.id) || Promise.resolve();
39711
+ const task = previous.catch(() => {}).then(() => applyMessageBatch(chat, batch, emitChanges));
39712
+ messageBatchTasks.set(chat.id, task);
39713
+ task.then(() => {
39714
+ if (messageBatchTasks.get(chat.id) === task) {
39715
+ messageBatchTasks.delete(chat.id);
39716
+ }
39717
+ }, () => {
39718
+ if (messageBatchTasks.get(chat.id) === task) {
39719
+ messageBatchTasks.delete(chat.id);
39720
+ }
39721
+ });
39722
+ return task;
39723
+ }
39059
39724
  function checkpointBatch(chat, page) {
39060
39725
  const sourceMessages = page?.history?.sourceMessages?.length ? page.history.sourceMessages : page?.batch?.sourceMessages?.length ? page.batch.sourceMessages : page?.batch?.messages || [];
39061
39726
  const batch = {
@@ -39142,13 +39807,15 @@ async function listenAccount(client, options = {}) {
39142
39807
  if (closed || !activeChatIds.has(chat.id) || !page.chat || !transientChats && chatSubscriptions.has(chat.id)) {
39143
39808
  return;
39144
39809
  }
39145
- applyMessageBatch(chat, page.batch, emitInitial);
39810
+ await queueMessageBatch(chat, page.batch, emitInitial);
39146
39811
  if (transientChats) {
39147
39812
  processedWakeVersions.set(chat.id, wakeVersion);
39148
39813
  return;
39149
39814
  }
39150
39815
  const batches = runtime.chat.getSnapshot().messageBatches;
39151
- const unsubscribe = batches.subscribeMessageBatch(chat.id, (batch) => applyMessageBatch(chat, batch, ready));
39816
+ const unsubscribe = batches.subscribeMessageBatch(chat.id, (batch) => {
39817
+ queueMessageBatch(chat, batch, ready).catch((error) => emit2(event("error", { peer: peerTarget(targetChat), message: error?.message || String(error) })));
39818
+ });
39152
39819
  chatSubscriptions.set(chat.id, () => {
39153
39820
  unsubscribe();
39154
39821
  page.release?.();
@@ -39282,6 +39949,13 @@ async function listenAccount(client, options = {}) {
39282
39949
  for (const timer of chatActivityTimers.values())
39283
39950
  clearTimeout(timer);
39284
39951
  chatActivityTimers.clear();
39952
+ for (const timer of readLiveTimers.values())
39953
+ clearTimeout(timer);
39954
+ readLiveTimers.clear();
39955
+ await Promise.allSettled(messageBatchTasks.values());
39956
+ await Promise.allSettled(readLiveEntering.values());
39957
+ await Promise.allSettled([...readLiveChats].map((chatId) => runtime.chat.getSnapshot().leaveChatActivity(chatId)));
39958
+ readLiveChats.clear();
39285
39959
  processedWakeVersions.clear();
39286
39960
  wantedWakeVersions.clear();
39287
39961
  }
@@ -39290,6 +39964,7 @@ var DEFAULT_CHAT_COUNT = 20, DEFAULT_MESSAGE_COUNT = 10, DEFAULT_REPLAY_MESSAGE_
39290
39964
  var init_listen = __esm(() => {
39291
39965
  init_actions2();
39292
39966
  init_ids();
39967
+ init_messages();
39293
39968
  init_presentation();
39294
39969
  init_messages3();
39295
39970
  });
@@ -41149,6 +41824,7 @@ function mergeLatestServerPages(primary, secondary) {
41149
41824
  return {
41150
41825
  ...secondary || {},
41151
41826
  ...primary || {},
41827
+ memberStatesByEpoch: mergeChatReceiptCheckpointsByEpoch(secondary?.memberStatesByEpoch, primary?.memberStatesByEpoch),
41152
41828
  expiredKeys: keySet([...primary?.expiredKeys || [], ...secondary?.expiredKeys || []]),
41153
41829
  deletedKeys: keySet([...primary?.deletedKeys || [], ...secondary?.deletedKeys || []])
41154
41830
  };
@@ -41296,6 +41972,7 @@ var init_screen = __esm(() => {
41296
41972
  init_messagekeys();
41297
41973
  init_ids();
41298
41974
  init_state();
41975
+ init_receipt();
41299
41976
  init_time();
41300
41977
  init_control();
41301
41978
  init_window();
@@ -41697,10 +42374,7 @@ var init_resolve = __esm(() => {
41697
42374
 
41698
42375
  // ../../core/chat/messages/serversync.js
41699
42376
  function mergeMemberStates(previous, current) {
41700
- return new Map([
41701
- ...previous instanceof Map ? previous : [],
41702
- ...current instanceof Map ? current : []
41703
- ]);
42377
+ return mergeChatReceiptCheckpointsByEpoch(previous, current);
41704
42378
  }
41705
42379
  function isEpochBatchCutover(latestServerPage, msgBatch) {
41706
42380
  return !!(latestServerPage?.epochId && msgBatch?.epochId && latestServerPage.epochId !== msgBatch.epochId);
@@ -41924,6 +42598,7 @@ var init_serversync = __esm(() => {
41924
42598
  init_messagekeys();
41925
42599
  init_messages();
41926
42600
  init_state();
42601
+ init_receipt();
41927
42602
  init_resolve();
41928
42603
  init_history();
41929
42604
  init_window();
@@ -42352,6 +43027,7 @@ function conversationStateSignature(state) {
42352
43027
  cacheMessageListSignature(state.latestMessages),
42353
43028
  cacheKeySignature(state.latestServerPage?.expiredKeys),
42354
43029
  cacheKeySignature(state.latestServerPage?.deletedKeys),
43030
+ cachedReceiptCheckpointSignature(state.latestServerPage?.memberStatesByEpoch),
42355
43031
  reactionControlCacheSignature(state)
42356
43032
  ].join(`
42357
43033
  `);
@@ -44191,6 +44867,7 @@ function deriveMessageRouteProjection({
44191
44867
  routeActive: routeRetained
44192
44868
  }));
44193
44869
  const { rawMessages, messages, visibleMessages } = routeMessages;
44870
+ const memberStatesByEpoch = activeState.latestServerPage?.memberStatesByEpoch || EMPTY_MEMBER_STATES_BY_EPOCH;
44194
44871
  return {
44195
44872
  activeExists: activeState.exists,
44196
44873
  activeHasOlder: activeState.hasOlder,
@@ -44199,7 +44876,7 @@ function deriveMessageRouteProjection({
44199
44876
  activeLatestMessages: activeState.latestMessages,
44200
44877
  activeLatestServerPage: activeState.latestServerPage,
44201
44878
  activeReady: activeState.ready,
44202
- memberStatesByEpoch: activeState.latestServerPage?.memberStatesByEpoch || new Map,
44879
+ memberStatesByEpoch,
44203
44880
  leaveState: {
44204
44881
  chatId,
44205
44882
  exists: activeState.exists,
@@ -44210,7 +44887,7 @@ function deriveMessageRouteProjection({
44210
44887
  selfChatPublicKey,
44211
44888
  peerChatPublicKey,
44212
44889
  memberChatPKs,
44213
- memberStatesByEpoch: activeState.latestServerPage?.memberStatesByEpoch || new Map,
44890
+ memberStatesByEpoch,
44214
44891
  conversationKeys,
44215
44892
  routeMemory,
44216
44893
  keepKeys: getRouteKeepKeys(refs.visibleMessageKeys.current, refs.heldMessages.current),
@@ -44316,7 +44993,7 @@ function recordRouteProjectionDiagnostics({
44316
44993
  markDiag(diag, "chat.messages.route.visible", visibleData);
44317
44994
  }
44318
44995
  }
44319
- var PASSIVE_OLDER_OVERFLOW_PAGES = 3, ROUTE_VISIBLE_DETAIL_SCAN_LIMIT = 160, DIAG_KEY_LEN2 = 12;
44996
+ var PASSIVE_OLDER_OVERFLOW_PAGES = 3, ROUTE_VISIBLE_DETAIL_SCAN_LIMIT = 160, DIAG_KEY_LEN2 = 12, EMPTY_MEMBER_STATES_BY_EPOCH;
44320
44997
  var init_routeprojection = __esm(() => {
44321
44998
  init_messagekeys();
44322
44999
  init_state();
@@ -44324,6 +45001,7 @@ var init_routeprojection = __esm(() => {
44324
45001
  init_history();
44325
45002
  init_payloadlog();
44326
45003
  init_window();
45004
+ EMPTY_MEMBER_STATES_BY_EPOCH = new Map;
44327
45005
  });
44328
45006
 
44329
45007
  // ../../core/chat/messages/expirycleanup.js
@@ -45349,7 +46027,7 @@ function createMessageRouteSession({
45349
46027
  return;
45350
46028
  const localTarget = getLatestLocalReadTarget(viewedMessages, current.selfChatPublicKey);
45351
46029
  const localTargetKey = messageKey2(localTarget);
45352
- const target = getLatestReadStateTarget(viewedMessages);
46030
+ const target = getLatestReadStateTarget(current.rawMessages?.length ? current.rawMessages : viewedMessages);
45353
46031
  const targetKey = messageKey2(target);
45354
46032
  const localChanged = !!localTargetKey && localTargetKey !== refs.lastSelfReadTarget.current;
45355
46033
  const stateChanged = !!targetKey && targetKey !== refs.lastSharedReadTarget.current;
@@ -46706,6 +47384,9 @@ function positiveLimit2(value, fallback = 30) {
46706
47384
  const limit = Math.floor(Number(value) || fallback);
46707
47385
  return Math.max(1, Math.min(limit, 500));
46708
47386
  }
47387
+ function messageIsPermanent(message) {
47388
+ return message?.permanent === true || Object.prototype.hasOwnProperty.call(message || {}, "ttl") && message.ttl == null;
47389
+ }
46709
47390
  function waitFor2(owner, predicate, timeoutMs = DEFAULT_TIMEOUT_MS4) {
46710
47391
  const current = owner.getSnapshot();
46711
47392
  if (predicate(current))
@@ -46873,7 +47554,7 @@ function createRuntimeChatActions({
46873
47554
  const profiles2 = await resolveTargetProfiles(peers, session, runtime);
46874
47555
  const settings = {
46875
47556
  title: cleanText(options.title) || null,
46876
- avatarRef: cleanText(options.avatarRef) || null,
47557
+ avatarRef: null,
46877
47558
  retention: cleanText(options.retention) || "seen"
46878
47559
  };
46879
47560
  const initialText = typeof options.initialMessage === "string" ? options.initialMessage : "";
@@ -46903,11 +47584,25 @@ function createRuntimeChatActions({
46903
47584
  return { left: result?.left === true || result?.deleted === true, chatId: chat.id };
46904
47585
  }
46905
47586
  async function updateSettings(chatId, settings = {}) {
47587
+ if (Object.prototype.hasOwnProperty.call(settings || {}, "avatarRef")) {
47588
+ throw new Error("use chat.updateAvatar for chat pictures");
47589
+ }
46906
47590
  const { runtime, chat } = await chatForId(chatId);
46907
47591
  await runtime.chat.getSnapshot().updateChatSettings(chat.id, settings);
46908
47592
  const next = await chatForId(chat.id);
46909
47593
  return summarizeChat(runtime, next.chat);
46910
47594
  }
47595
+ async function updateAvatar(chatId, data = null) {
47596
+ const { runtime, chat } = await chatForId(chatId);
47597
+ const snapshot2 = runtime.chat.getSnapshot();
47598
+ if (data == null) {
47599
+ await snapshot2.updateChatSettings(chat.id, { avatarRef: null });
47600
+ } else {
47601
+ await snapshot2.updateChatAvatar(chat.id, data);
47602
+ }
47603
+ const next = await chatForId(chat.id);
47604
+ return summarizeChat(runtime, next.chat);
47605
+ }
46911
47606
  async function enterById(chatId, options = {}) {
46912
47607
  const session = await getSession();
46913
47608
  const { runtime, chat, profile } = await chatForId(chatId);
@@ -46934,6 +47629,31 @@ function createRuntimeChatActions({
46934
47629
  const route = enteredChats.get(id);
46935
47630
  return { exited: route?.leave() === true, chatId: id || null };
46936
47631
  }
47632
+ async function enterLive(chatId) {
47633
+ const { runtime, chat } = await chatForId(chatId);
47634
+ const entered2 = await runtime.chat.getSnapshot().enterChatActivity(chat.id);
47635
+ return { entered: entered2 === true, chatId: chat.id };
47636
+ }
47637
+ async function leaveLive(chatId) {
47638
+ const id = cleanText(chatId).toLowerCase();
47639
+ if (!/^[0-9a-f]{64}$/u.test(id))
47640
+ throw new Error("chat id required");
47641
+ const runtime = await getRuntime();
47642
+ await runtime.chat.getSnapshot().leaveChatActivity(id);
47643
+ return { left: true, chatId: id };
47644
+ }
47645
+ async function markTyping(chatId, typing = true, compositionId = "") {
47646
+ const { runtime, chat } = await chatForId(chatId);
47647
+ const nextTyping = typing === true;
47648
+ const nextCompositionId = cleanText(compositionId).toLowerCase() || (nextTyping ? makeChatCompositionId() : "");
47649
+ const marked = runtime.chat.getSnapshot().markChatTyping(chat.id, nextTyping, nextCompositionId);
47650
+ return {
47651
+ marked: marked === true,
47652
+ typing: nextTyping,
47653
+ compositionId: nextCompositionId,
47654
+ chatId: chat.id
47655
+ };
47656
+ }
46937
47657
  async function readById(chatId, options = {}) {
46938
47658
  const window2 = await messageWindowById(chatId, options);
46939
47659
  try {
@@ -47078,6 +47798,13 @@ function createRuntimeChatActions({
47078
47798
  }
47079
47799
  async function saveInChat(chatId, messageId, saved = true, options = {}) {
47080
47800
  const target = await messageForChat(chatId, messageId, options);
47801
+ if (saved === messageIsPermanent(target.message)) {
47802
+ return {
47803
+ chat: summarizeChat(target.runtime, target.chat),
47804
+ saved,
47805
+ updated: 0
47806
+ };
47807
+ }
47081
47808
  const result = saved ? await target.runtime.chat.getSnapshot().makeMessagePermanent(target.chat.id, target.message) : await target.runtime.chat.getSnapshot().makeMessageTemporary(target.chat.id, target.message);
47082
47809
  if (!result?.updated)
47083
47810
  throw new Error("message no longer available");
@@ -47572,10 +48299,14 @@ function createRuntimeChatActions({
47572
48299
  addMembers,
47573
48300
  kickMember,
47574
48301
  leaveMembership,
48302
+ updateAvatar,
47575
48303
  updateSettings,
47576
48304
  readById,
47577
48305
  enterById,
47578
48306
  exitById,
48307
+ enterLive,
48308
+ leaveLive,
48309
+ markTyping,
47579
48310
  sendToChat,
47580
48311
  sendAttachmentToChat,
47581
48312
  readAttachmentInChat,
@@ -47995,6 +48726,7 @@ var DEFAULT_TIMEOUT_MS4 = 15000, HISTORY_LOAD_ATTEMPTS = 4, messageWindowSequenc
47995
48726
  var init_runtime_actions = __esm(() => {
47996
48727
  init_messages();
47997
48728
  init_ids();
48729
+ init_live();
47998
48730
  init_state();
47999
48731
  init_actions2();
48000
48732
  init_fees();
@@ -48012,7 +48744,7 @@ var package_default;
48012
48744
  var init_package2 = __esm(() => {
48013
48745
  package_default = {
48014
48746
  name: "veyl",
48015
- version: "0.59.1",
48747
+ version: "0.61.0",
48016
48748
  private: true,
48017
48749
  license: "UNLICENSED",
48018
48750
  workspaces: {
@@ -48341,10 +49073,14 @@ class AccountRuntime {
48341
49073
  addMembers: (chatId, peers) => this.runtimeChat.addMembers(chatId, peers),
48342
49074
  kickMember: (chatId, memberChatPK) => this.runtimeChat.kickMember(chatId, memberChatPK),
48343
49075
  leaveMembership: (chatId) => this.runtimeChat.leaveMembership(chatId),
49076
+ updateAvatar: (chatId, data) => this.runtimeChat.updateAvatar(chatId, data),
48344
49077
  updateSettings: (chatId, payload) => this.runtimeChat.updateSettings(chatId, payload),
48345
49078
  readById: (chatId, payload) => this.runtimeChat.readById(chatId, payload),
48346
49079
  enterById: (chatId, payload) => this.runtimeChat.enterById(chatId, payload),
48347
49080
  exitById: (chatId) => this.runtimeChat.exitById(chatId),
49081
+ enterLive: (chatId) => this.runtimeChat.enterLive(chatId),
49082
+ leaveLive: (chatId) => this.runtimeChat.leaveLive(chatId),
49083
+ markTyping: (chatId, typing, compositionId) => this.runtimeChat.markTyping(chatId, typing, compositionId),
48348
49084
  sendTo: (chatId, message, payload) => this.runtimeChat.sendToChat(chatId, message, payload),
48349
49085
  replyIn: (chatId, messageId, message, payload) => this.runtimeChat.replyInChat(chatId, messageId, message, payload),
48350
49086
  reactIn: (chatId, messageId, emoji, payload) => this.runtimeChat.reactInChat(chatId, messageId, emoji, payload),
@@ -48806,7 +49542,9 @@ class AccountRuntime {
48806
49542
  }
48807
49543
  const vault = await this.cloud.user.vault.read(profile.uid);
48808
49544
  if (!vault) {
48809
- throw new Error("vault not found");
49545
+ throw Object.assign(new Error("vault not found"), {
49546
+ code: "not-found"
49547
+ });
48810
49548
  }
48811
49549
  return {
48812
49550
  mnemonic: await this.ports.vaultCrypto.decryptWalletMnemonic(vault, vaultSecret),
@@ -48824,7 +49562,9 @@ class AccountRuntime {
48824
49562
  }
48825
49563
  const vault = await this.cloud.user.vault.read(profile.uid);
48826
49564
  if (!vault) {
48827
- throw new Error("vault not found");
49565
+ throw Object.assign(new Error("vault not found"), {
49566
+ code: "not-found"
49567
+ });
48828
49568
  }
48829
49569
  const session = await this.bootSession(vault, vaultSecret, profile);
48830
49570
  const next = await this.saveProfile({
@@ -181895,7 +182635,7 @@ function firebaseFunctionTarget(environment, name7) {
181895
182635
  if (!/^[A-Za-z][A-Za-z0-9]*$/u.test(target)) {
181896
182636
  throw new Error("Invalid Firebase function target");
181897
182637
  }
181898
- return `${resolved.name}-${target}`;
182638
+ return resolved.name === "dev" ? `dev-${target}` : target;
181899
182639
  }
181900
182640
  var firebaseConfig, firebaseEnvironments, defaultFirebaseEnvironment;
181901
182641
  var init_config3 = __esm(() => {
@@ -182864,14 +183604,14 @@ function createFirebaseCloud({ db, auth: auth2, getAuth: getAuth2, functions: fu
182864
183604
  const { entryId, expectedEpochId, stateCapability, cleanup = true } = options2 || {};
182865
183605
  if (!chatId || !expectedEpochId || !stateCapability)
182866
183606
  throw new Error("current chat state required");
182867
- await callFunction("deleteChat", {
183607
+ const result = await callFunction("deleteChat", {
182868
183608
  chatId,
182869
183609
  expectedEpochId,
182870
183610
  stateCapabilityWitness: stateCapabilityWitness(stateCapability),
182871
183611
  ...entryId ? { entryId } : {},
182872
183612
  cleanup: cleanup !== false
182873
183613
  });
182874
- return true;
183614
+ return { newlyDeleted: result?.newlyDeleted === true };
182875
183615
  }
182876
183616
  function cleanOpaqueState(value) {
182877
183617
  const chat = value?.chat || value || {};
@@ -183013,10 +183753,10 @@ function createFirebaseCloud({ db, auth: auth2, getAuth: getAuth2, functions: fu
183013
183753
  ]).catch(() => []);
183014
183754
  const [epochSnap, settingsSnap, packageSnap, ownerSnap, ownerMlsSnap] = snapshots;
183015
183755
  const outboxSnaps = snapshots.slice(5);
183016
- const currentEpoch = epochSnap?.data?.() || {};
183756
+ const currentEpoch2 = epochSnap?.data?.() || {};
183017
183757
  const currentSettings = settingsSnap?.data?.() || {};
183018
183758
  const currentPackage = packageSnap?.data?.() || {};
183019
- const matches = epochSnap?.exists?.() && settingsSnap?.exists?.() && packageSnap?.exists?.() && currentEpoch.protocol === CHAT_PROTOCOL_VERSION && currentEpoch.chatId === chatId && currentEpoch.epochVersion === 1 && currentEpoch.settingsId === payload.settings.id && currentEpoch.mlsPackageId === payload.mlsPackage.head.packageId && currentEpoch.mlsPackageDigest === payload.mlsPackage.digest && currentEpoch.status === "open" && sameChatSettingsHead(currentSettings.head, settings.data.head) && sameCloudBytes(currentSettings.body, settings.data.body) && currentPackage?.head?.packageId === payload.mlsPackage.head.packageId && currentPackage?.digest === payload.mlsPackage.digest && ownerSnap?.exists?.() && sameCloudBytes(ownerSnap.data()?.body, ownerEntry.data.body) && ownerMlsSnap?.exists?.() && sameCloudBytes(ownerMlsSnap.data()?.body, ownerMlsState.data.body) && membershipOutbox.every((outbox, index) => {
183759
+ const matches = epochSnap?.exists?.() && settingsSnap?.exists?.() && packageSnap?.exists?.() && currentEpoch2.protocol === CHAT_PROTOCOL_VERSION && currentEpoch2.chatId === chatId && currentEpoch2.epochVersion === 1 && currentEpoch2.settingsId === payload.settings.id && currentEpoch2.mlsPackageId === payload.mlsPackage.head.packageId && currentEpoch2.mlsPackageDigest === payload.mlsPackage.digest && currentEpoch2.status === "open" && sameChatSettingsHead(currentSettings.head, settings.data.head) && sameCloudBytes(currentSettings.body, settings.data.body) && currentPackage?.head?.packageId === payload.mlsPackage.head.packageId && currentPackage?.digest === payload.mlsPackage.digest && ownerSnap?.exists?.() && sameCloudBytes(ownerSnap.data()?.body, ownerEntry.data.body) && ownerMlsSnap?.exists?.() && sameCloudBytes(ownerMlsSnap.data()?.body, ownerMlsState.data.body) && membershipOutbox.every((outbox, index) => {
183020
183760
  const current = outboxSnaps[index]?.data?.() || {};
183021
183761
  return outboxSnaps[index]?.exists?.() && sameCloudBytes(current.body, outbox.data.body) && timestampMs(current.ttl, null) === timestampMs(outbox.data.ttl, null);
183022
183762
  });
@@ -183928,6 +184668,15 @@ function createFirebaseCloud({ db, auth: auth2, getAuth: getAuth2, functions: fu
183928
184668
  await callFunction("unlinkChatOwner", { entryId });
183929
184669
  return true;
183930
184670
  }
184671
+ async function unlinkDeletedUserChat(uid, entryId, chatId) {
184672
+ requireUid(uid);
184673
+ if (!entryId)
184674
+ throw new Error("chat entry id required");
184675
+ if (!chatId)
184676
+ throw new Error("chat id required");
184677
+ await callFunction("unlinkChatOwner", { entryId, deletedChatId: chatId });
184678
+ return true;
184679
+ }
183931
184680
  function userChatEpochCollection(uid, entryId) {
183932
184681
  requireUid(uid);
183933
184682
  if (!entryId)
@@ -184231,6 +184980,7 @@ function createFirebaseCloud({ db, auth: auth2, getAuth: getAuth2, functions: fu
184231
184980
  mutate: mutateUserChat,
184232
184981
  delete: deleteUserChat,
184233
184982
  unlink: unlinkUserChat,
184983
+ unlinkDeleted: unlinkDeletedUserChat,
184234
184984
  membershipOutbox: {
184235
184985
  list: listUserChatMembershipOutbox,
184236
184986
  delete: deleteUserChatMembershipOutbox
@@ -186599,6 +187349,7 @@ class FleetOwner {
186599
187349
  this.manifest = options2.manifest;
186600
187350
  this.seed = options2.seed;
186601
187351
  this.homeDir = options2.homeDir || defaultHomeDir();
187352
+ this.profileHomeDir = options2.profileHomeDir || this.homeDir;
186602
187353
  this.openClient = options2.openClient || createAccountRuntime;
186603
187354
  this.policies = [...options2.policies || []];
186604
187355
  this.policyLoader = options2.policyLoader || null;
@@ -186645,7 +187396,7 @@ class FleetOwner {
186645
187396
  try {
186646
187397
  return {
186647
187398
  ...await this.baseClientOptions(profile),
186648
- homeDir: this.homeDir,
187399
+ homeDir: this.profileHomeDir,
186649
187400
  activate: false,
186650
187401
  credential: material.credential,
186651
187402
  vaultKey: material.vaultSecret,
@@ -186781,6 +187532,36 @@ class FleetOwner {
186781
187532
  await this.lock.release();
186782
187533
  }
186783
187534
  }
187535
+ profileStore(profile) {
187536
+ return new ProfileStore({
187537
+ dir: this.profileHomeDir,
187538
+ profile,
187539
+ activate: false
187540
+ });
187541
+ }
187542
+ async hasLocalProfile(profile) {
187543
+ const name7 = cleanProfileName(profile, "");
187544
+ if (!name7)
187545
+ return false;
187546
+ return Boolean(await this.profileStore(name7).load(name7));
187547
+ }
187548
+ async ensureLocalProfile(record, credential) {
187549
+ const store = this.profileStore(record.profile);
187550
+ if (await store.load(record.profile))
187551
+ return store;
187552
+ await store.save({
187553
+ profile: record.profile,
187554
+ username: record.username,
187555
+ network: record.network,
187556
+ credentialId: credential.credentialId,
187557
+ publicKeyPem: credential.publicKeyPem,
187558
+ privateKeyPem: null,
187559
+ auth: "machine",
187560
+ hasVault: false,
187561
+ createdAt: Date.now()
187562
+ });
187563
+ return store;
187564
+ }
186784
187565
  provision(input = {}) {
186785
187566
  return this.mutate(async () => {
186786
187567
  if (this.fleet) {
@@ -186815,32 +187596,75 @@ class FleetOwner {
186815
187596
  });
186816
187597
  });
186817
187598
  }
186818
- async finishProvision(record, options2) {
186819
- const material = deriveFleetAccount(this.seed, record.accountIndex);
186820
- const store = new ProfileStore({
186821
- dir: this.homeDir,
186822
- profile: record.profile,
186823
- activate: false
187599
+ prepare(profile) {
187600
+ return this.mutate(async () => {
187601
+ if (this.fleet) {
187602
+ throw new Error("stop the fleet owner before preparing an account");
187603
+ }
187604
+ return this.withStoppedOwnerLock(async () => {
187605
+ const record = this.slot(profile);
187606
+ if (!record)
187607
+ throw new Error("fleet profile not found");
187608
+ if (record.state !== "ready") {
187609
+ throw new Error("fleet profile must be ready before preparation");
187610
+ }
187611
+ return this.finishProvision(record, {
187612
+ create: false,
187613
+ recoverMissing: false,
187614
+ saveManifest: false,
187615
+ runHook: false
187616
+ });
187617
+ });
186824
187618
  });
186825
- const existing = await store.load(record.profile);
186826
- if (!existing) {
186827
- await store.save({
186828
- profile: record.profile,
186829
- username: record.username,
186830
- network: record.network,
186831
- credentialId: material.credential.credentialId,
186832
- publicKeyPem: material.credential.publicKeyPem,
186833
- privateKeyPem: null,
186834
- auth: "machine",
186835
- hasVault: false,
186836
- createdAt: Date.now()
187619
+ }
187620
+ retire(profile, options2 = {}) {
187621
+ return this.mutate(async () => {
187622
+ if (options2.confirm !== true) {
187623
+ throw new Error("fleet account retirement requires confirm: true");
187624
+ }
187625
+ if (this.fleet) {
187626
+ throw new Error("stop the fleet owner before retiring an account");
187627
+ }
187628
+ return this.withStoppedOwnerLock(async () => {
187629
+ const record = this.slot(profile);
187630
+ if (!record)
187631
+ throw new Error("fleet profile not found");
187632
+ if (record.state !== "ready") {
187633
+ throw new Error("fleet profile must be ready before retirement");
187634
+ }
187635
+ const clientOptions = await this.derivedClientOptions(record);
187636
+ await this.ensureLocalProfile(record, clientOptions.credential);
187637
+ const client2 = await this.openClient({
187638
+ ...clientOptions,
187639
+ profile: record.profile,
187640
+ network: record.network
187641
+ }, record);
187642
+ try {
187643
+ await client2.account.login({
187644
+ credential: clientOptions.credential
187645
+ });
187646
+ await client2.ensureUnlocked();
187647
+ const result = await client2.account.delete({ confirm: true });
187648
+ await this.saveProfiles(this.manifest.profiles.filter((item) => item.profile !== record.profile), this.manifest.nextAccountIndex);
187649
+ return {
187650
+ profile: record.profile,
187651
+ username: record.username,
187652
+ deleted: result?.deleted !== false
187653
+ };
187654
+ } finally {
187655
+ await client2.close();
187656
+ }
186837
187657
  });
186838
- }
187658
+ });
187659
+ }
187660
+ async finishProvision(record, options2) {
187661
+ const material = deriveFleetAccount(this.seed, record.accountIndex);
187662
+ await this.ensureLocalProfile(record, material.credential);
186839
187663
  const client2 = await this.openClient({
186840
187664
  ...await this.baseClientOptions(record),
186841
187665
  profile: record.profile,
186842
187666
  network: record.network,
186843
- homeDir: this.homeDir,
187667
+ homeDir: this.profileHomeDir,
186844
187668
  activate: false,
186845
187669
  credential: material.credential,
186846
187670
  vaultKey: material.vaultSecret,
@@ -186866,6 +187690,8 @@ class FleetOwner {
186866
187690
  } catch (error) {
186867
187691
  if (error?.code !== "not-found")
186868
187692
  throw error;
187693
+ if (options2.recoverMissing === false)
187694
+ throw error;
186869
187695
  await client2.account.create({
186870
187696
  username: record.username,
186871
187697
  network: record.network,
@@ -186880,23 +187706,38 @@ class FleetOwner {
186880
187706
  credential: material.credential
186881
187707
  });
186882
187708
  }
186883
- if (account.hasVault !== true) {
187709
+ try {
187710
+ await client2.vault.unlock({
187711
+ key: material.vaultSecret,
187712
+ saveKey: false
187713
+ });
187714
+ } catch (error) {
187715
+ if (error?.code !== "not-found")
187716
+ throw error;
187717
+ if (options2.recoverMissing === false)
187718
+ throw error;
186884
187719
  await client2.vault.create({
186885
187720
  key: material.vaultSecret,
186886
187721
  masterSeed: material.masterSeed,
186887
187722
  registry: material.registry,
186888
187723
  saveKey: false
186889
187724
  });
186890
- account = await client2.account.me();
186891
- } else {
186892
- await client2.ensureUnlocked();
186893
- account = await client2.account.me();
186894
187725
  }
187726
+ account = await client2.account.me();
186895
187727
  const ready2 = Object.freeze({
186896
187728
  ...record,
186897
187729
  state: "ready"
186898
187730
  });
186899
- await this.saveProfiles(this.manifest.profiles.map((item) => item.profile === ready2.profile ? ready2 : item), this.manifest.nextAccountIndex);
187731
+ if (options2.runHook !== false && typeof this.options.onProvisioned === "function") {
187732
+ await this.options.onProvisioned({
187733
+ account,
187734
+ client: client2,
187735
+ profile: ready2
187736
+ });
187737
+ }
187738
+ if (options2.saveManifest !== false) {
187739
+ await this.saveProfiles(this.manifest.profiles.map((item) => item.profile === ready2.profile ? ready2 : item), this.manifest.nextAccountIndex);
187740
+ }
186900
187741
  return {
186901
187742
  account,
186902
187743
  accountIndex: ready2.accountIndex,