@glyphteck/veyl 0.67.0 → 0.68.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/account.js CHANGED
@@ -5618,11 +5618,158 @@ function sameText(left, right) {
5618
5618
  return lowerText(left) === lowerText(right);
5619
5619
  }
5620
5620
 
5621
+ // ../../core/config.js
5622
+ var MS_PER_SECOND = 1000;
5623
+ var MINUTE_MS = 60 * MS_PER_SECOND;
5624
+ var HOUR_MS = 60 * MINUTE_MS;
5625
+ var DAY_MS = 24 * HOUR_MS;
5626
+ var KIB_BYTES = 1024;
5627
+ var MIB_BYTES = 1024 * 1024;
5628
+ var SATS_PER_BITCOIN = 100000000n;
5629
+ var USERNAME_MAX_CHARS = 12;
5630
+ var PASSWORD_MIN_CHARS = 15;
5631
+ var PASSWORD_MAX_CHARS = 64;
5632
+ var PASSWORD_MAX_BYTES = 4 * 1024;
5633
+ var LOCAL_MEDIA_CACHE_MAX_BYTES = 512 * MIB_BYTES;
5634
+ var LOCAL_PROFILE_CACHE_MAX_ITEMS = 500;
5635
+ var LOCAL_PROFILE_CACHE_MAX_AGE_MS = 30 * DAY_MS;
5636
+ var LOCAL_CHAT_CACHE_MAX_ITEMS = 1000;
5637
+ var LOCAL_CHAT_MESSAGE_CACHE_MAX_VISIBLE = 1000;
5638
+ var LOCAL_AVATAR_CACHE_MAX_BYTES = 128 * MIB_BYTES;
5639
+ var LOCAL_AVATAR_CACHE_MAX_AGE_MS = 30 * DAY_MS;
5640
+ var AVATAR_IMAGE_MAX_BYTES = 256 * KIB_BYTES;
5641
+ var AVATAR_IMAGE_QUALITY_ATTEMPTS = Object.freeze([0.92, 0.84, 0.76, 0.68]);
5642
+ var CHAT_AVATAR_IMAGE_MAX_BYTES = AVATAR_IMAGE_MAX_BYTES;
5643
+ var CHAT_AVATAR_REF_MAX_CHARS = 256;
5644
+ var CHAT_MESSAGE_FILE_CACHE_MAX_BYTES = 64 * MIB_BYTES;
5645
+ var IDLE_CALLBACK_MIN_TIMEOUT_MS = 50;
5646
+ var ATTACHMENT_CACHE_IDLE_TIMEOUT_MS = 2500;
5647
+ var ATTACHMENT_CACHE_FALLBACK_DELAY_MS = 250;
5648
+ var CHAT_UNSAVED_TTL_DAYS = 21;
5649
+ var CHAT_UNSAVED_TTL_MS = CHAT_UNSAVED_TTL_DAYS * DAY_MS;
5650
+ var CHAT_AFTER_SEEN_MS = DAY_MS;
5651
+ var CHAT_MEDIA_TTL_DAYS = CHAT_UNSAVED_TTL_DAYS;
5652
+ var CHAT_MEDIA_TTL_MS = CHAT_MEDIA_TTL_DAYS * DAY_MS;
5653
+ var CHAT_UPLOAD_MAX_BYTES = 64 * MIB_BYTES;
5654
+ var CHAT_AUDIO_TRANSCODE_BITRATE_BPS = 128000;
5655
+ var CHAT_VIDEO_TRANSCODE_VIDEO_BITRATE_BPS = 3000000;
5656
+ var CHAT_VIDEO_TRANSCODE_AUDIO_BITRATE_BPS = CHAT_AUDIO_TRANSCODE_BITRATE_BPS;
5657
+ var CHAT_MESSAGE_BATCH_SIZE = 40;
5658
+ var CHAT_MESSAGE_QUERY_MAX_DOCS = 120;
5659
+ var CHAT_MESSAGE_MUTATION_MAX_ITEMS = 256;
5660
+ var CHAT_TTL_CLIENT_DELETE_GRACE_MS = MINUTE_MS;
5661
+ var CHAT_BATCH_CLEANUP_IDLE_TIMEOUT_MS = 1500;
5662
+ var CHAT_BATCH_CLEANUP_IDLE_DELAY_MS = 250;
5663
+ var CHAT_LIST_PAGE_SIZE = 20;
5664
+ var CHAT_LIST_LIVE_COUNT = CHAT_LIST_PAGE_SIZE;
5665
+ var CHAT_INBOX_PING_PAGE_SIZE = 25;
5666
+ var CHAT_INBOX_DISCOVERY_PARALLEL_CHATS = CHAT_INBOX_PING_PAGE_SIZE;
5667
+ var CHAT_INBOX_SETTLEMENT_PARALLEL_CHATS = 4;
5668
+ var CHAT_INBOX_CURSOR_OVERLAP_MS = 5 * MS_PER_SECOND;
5669
+ var CHAT_LIST_CACHE_WRITE_DELAY_MS = 1500;
5670
+ var CHAT_LIST_SNAPSHOT_COALESCE_MS = 80;
5671
+ var CHAT_LIST_COMPLETE_SYNC_DELAY_MS = 250;
5672
+ var CHAT_LIST_LISTENER_RETRY_MS = 1000;
5673
+ var CHAT_TOP_WARM_COUNT = 0;
5674
+ var CHAT_EAGER_WARM_COUNT = 0;
5675
+ var CHAT_WARM_DELAY_MS = 3000;
5676
+ var CHAT_WARM_BATCH_SIZE = CHAT_MESSAGE_BATCH_SIZE;
5677
+ var CHAT_MESSAGE_VIEW_CACHE_SIZE = 30;
5678
+ var CHAT_MEDIA_WARM_MESSAGES_PER_CHAT = CHAT_MESSAGE_BATCH_SIZE;
5679
+ var CHAT_MEDIA_WARM_START_DELAY_MS = 600;
5680
+ var CHAT_MEDIA_WARM_STEP_DELAY_MS = 120;
5681
+ var CHAT_MEDIA_WARM_TYPES = Object.freeze(["img", "gif", "mp4"]);
5682
+ var CHAT_MEDIA_WARM_MAX_BYTES = 0;
5683
+ var CHAT_READ_WRITE_INTERVAL_MS = 2 * MS_PER_SECOND;
5684
+ var CHAT_READ_WRITE_MAX_WAIT_MS = 8 * MS_PER_SECOND;
5685
+ var CHAT_LIVE_PING_INTERVAL_MS = 5 * MS_PER_SECOND;
5686
+ var CHAT_LIVE_SWEEP_INTERVAL_MS = 15 * MS_PER_SECOND;
5687
+ var CHAT_LIVE_STALE_AFTER_MS = 15 * MS_PER_SECOND;
5688
+ var CHAT_LIVE_RECONNECT_MAX_MS = 5 * MS_PER_SECOND;
5689
+ var CHAT_LIVE_TYPING_IDLE_MS = 8 * MS_PER_SECOND;
5690
+ var CHAT_LIVE_TYPING_RENEW_MS = 5 * MS_PER_SECOND;
5691
+ var CHAT_LIVE_COMPOSITION_HANDOFF_MS = 15 * MS_PER_SECOND;
5692
+ var CHAT_LIVE_READ_SEND_INTERVAL_MS = 120;
5693
+ var CHAT_LIVE_READ_SEND_MAX_WAIT_MS = 500;
5694
+ var CHAT_LIVE_READ_WRITE_INTERVAL_MS = 12 * MS_PER_SECOND;
5695
+ var CHAT_LIVE_READ_WRITE_MAX_WAIT_MS = 30 * MS_PER_SECOND;
5696
+ var CHAT_SEND_QUEUE_RATE_LIMIT_COUNT = 12;
5697
+ var CHAT_SEND_QUEUE_RATE_LIMIT_WINDOW_MS = 10 * MS_PER_SECOND;
5698
+ var CHAT_SETTINGS_BODY_MAX_BYTES = 32 * KIB_BYTES;
5699
+ var CHAT_TITLE_STATE_MAX_CHARS = 128;
5700
+ var CHAT_MANIFEST_MAX_BYTES = 256 * KIB_BYTES;
5701
+ var CHAT_RECEIPT_MAX_MEMBERS = 32;
5702
+ var CHAT_ORDINARY_DELIVERY_MAX_MEMBERS = 128;
5703
+ var CHAT_MAX_MEMBERS = 256;
5704
+ var CHAT_LARGE_GROUP_MENTION_MAX_TARGETS = 16;
5705
+ var CHAT_MAX_TEXT_CHARS = 2048;
5706
+ var CHAT_TITLE_INPUT_MAX_CHARS = 16;
5707
+ var CHAT_MAX_REACTIONS = CHAT_MAX_MEMBERS;
5708
+ var SEARCH_DEBOUNCE_MS = 300;
5709
+ var RECENT_PEER_REFRESH_LIMIT = 50;
5710
+ var RECENT_PEER_REFRESH_DELAY_MS = 250;
5711
+ var RECENT_PEER_REFRESH_INTERVAL_MS = 5 * MINUTE_MS;
5712
+ var RECENT_PEER_REFRESH_THROTTLE_MS = 120;
5713
+ var BAN_REFRESH_GRACE_MS = 50;
5714
+ var REQUEST_MONEY_MAX_SATS = SATS_PER_BITCOIN * 100000n;
5715
+ var WALLET_TRANSFER_POLL_MS = 3 * MS_PER_SECOND;
5716
+ var WALLET_ACTIVE_TRANSFER_REFRESH_MS = MS_PER_SECOND;
5717
+ var WALLET_CACHE_HYDRATE_DELAY_MS = 0;
5718
+ var WALLET_BOOT_REFRESH_DELAY_MS = 500;
5719
+ var WALLET_BOOT_CACHED_REFRESH_DELAY_MS = 2500;
5720
+ var WALLET_REGTEST_DEPOSIT_CLAIM_POLL_MS = 20 * MS_PER_SECOND;
5721
+ var WALLET_MAINNET_DEPOSIT_CLAIM_POLL_MS = MINUTE_MS;
5722
+ var WALLET_BALANCE_EVENT_COALESCE_MS = 2 * MS_PER_SECOND;
5723
+ var WALLET_INCOMING_UPDATE_COALESCE_MS = 250;
5724
+ var WALLET_AUTO_CLAIM_MAX_FEE_SATS = 5000;
5725
+ var WALLET_CLAIM_PAGE_SIZE = 100;
5726
+ var WALLET_PENDING_TRANSFER_CLAIM_BATCH_SIZE = 50;
5727
+ var WALLET_PENDING_TRANSFER_STATUS_BATCH_SIZE = 2;
5728
+ var WALLET_PENDING_TRANSFER_COLD_REFRESH_BATCH_SIZE = 50;
5729
+ var WALLET_PENDING_TRANSFER_COLD_REFRESH_INTERVAL_MS = 15 * MS_PER_SECOND;
5730
+ var WALLET_PENDING_TRANSFER_ADAPTIVE_BATCH_SMALL_QUEUE = 3;
5731
+ var WALLET_PENDING_TRANSFER_ADAPTIVE_BATCH_LARGE_QUEUE = 10;
5732
+ var WALLET_PENDING_TRANSFER_ADAPTIVE_BATCH_SMALL_SIZE = 2;
5733
+ var WALLET_PENDING_TRANSFER_ADAPTIVE_BATCH_LARGE_SIZE = 3;
5734
+ var WALLET_PENDING_TRANSFER_SLOW_CLAIM_MS = 4 * MS_PER_SECOND;
5735
+ var WALLET_PENDING_TRANSFER_CLAIM_COOLDOWN_MS = 15 * MS_PER_SECOND;
5736
+ var WALLET_SDK_BACKGROUND_QUIET_MS = MS_PER_SECOND;
5737
+ var WALLET_RECENT_TRANSFER_LIMIT = 100;
5738
+ var WALLET_TRANSFER_PAGE_LIMIT = 100;
5739
+ var WALLET_TRANSFER_FETCH_THROTTLE_MS = 150;
5740
+ var WALLET_PENDING_TRANSFER_CLAIM_RETRY_MS = 3 * MS_PER_SECOND;
5741
+ var WALLET_SENT_TRANSFER_REFRESH_DELAY_MS = 3 * MS_PER_SECOND;
5742
+ var WALLET_PENDING_TRANSFER_BOOT_GRACE_MS = 15 * MS_PER_SECOND;
5743
+ var WALLET_PENDING_TRANSFER_HOT_AGE_MS = MINUTE_MS;
5744
+ var WALLET_PENDING_TRANSFER_WARM_AGE_MS = 10 * MINUTE_MS;
5745
+ var WALLET_PENDING_TRANSFER_WARM_RETRY_MS = 15 * MS_PER_SECOND;
5746
+ var WALLET_PENDING_TRANSFER_STALE_RETRY_MS = 2 * MINUTE_MS;
5747
+ var WALLET_PENDING_TRANSFER_STUCK_RETRY_MS = 10 * MINUTE_MS;
5748
+ var WALLET_PENDING_TRANSFER_DORMANT_RETRY_MS = HOUR_MS;
5749
+ var WALLET_TRANSFER_CACHE_WRITE_DELAY_MS = 3 * MS_PER_SECOND;
5750
+
5621
5751
  // ../../core/notifications.js
5622
5752
  "use client";
5623
5753
  var NOTIFICATION_DESCRIPTOR_VERSION = 1;
5754
+ var CHAT_ATTENTION_REGISTRATION_VERSION = 1;
5755
+ var CHAT_NOTIFICATION_MODES = Object.freeze({
5756
+ ALL: "all",
5757
+ MENTIONS: "mentions",
5758
+ NONE: "none"
5759
+ });
5760
+ var CHAT_ATTENTION_KINDS = Object.freeze({
5761
+ GENERAL: "general",
5762
+ MENTION: "mention",
5763
+ ROTATION: "rotation",
5764
+ SILENT: "silent"
5765
+ });
5624
5766
  var HEX_32_RE = /^[0-9a-f]{64}$/u;
5625
5767
  var PEER_TAG_RE = /^[0-9a-f]{32}$/u;
5768
+ var ATTENTION_KINDS = new Set(Object.values(CHAT_ATTENTION_KINDS));
5769
+ var NOTIFICATION_MODES = new Set(Object.values(CHAT_NOTIFICATION_MODES));
5770
+ function hasCurrentChatAttentionRegistration(value) {
5771
+ return value?.deliveryRegistered === true && value?.attentionRegistrationVersion === CHAT_ATTENTION_REGISTRATION_VERSION;
5772
+ }
5626
5773
  function cleanHex32(value, label) {
5627
5774
  const text = cleanText(value).toLowerCase();
5628
5775
  if (!HEX_32_RE.test(text)) {
@@ -5656,6 +5803,75 @@ function deriveChatDeliveryCapability(stateCapability, fields = {}) {
5656
5803
  cleanBytes(key);
5657
5804
  }
5658
5805
  }
5806
+ function cleanAttentionFields(fields = {}) {
5807
+ return {
5808
+ chatId: cleanHex32(fields.chatId, "attention chat id"),
5809
+ recipientChatPK: cleanHex32(fields.recipientChatPK, "attention recipient chat key"),
5810
+ generation: Number.isSafeInteger(fields.generation) && fields.generation > 0 ? fields.generation : 1
5811
+ };
5812
+ }
5813
+ function deriveChatAttentionMaterial(attentionSecret, fields, kind) {
5814
+ const normalized = cleanAttentionFields(fields);
5815
+ const attentionKind = cleanText(kind);
5816
+ if (!ATTENTION_KINDS.has(attentionKind)) {
5817
+ throw new Error("chat attention kind required");
5818
+ }
5819
+ const key = deriveKey(toBytes32(attentionSecret, "chat attention secret"), "epoch-chat-attention-capability-v1", [normalized.chatId, normalized.recipientChatPK, normalized.generation, attentionKind]);
5820
+ try {
5821
+ return toHex(key);
5822
+ } finally {
5823
+ cleanBytes(key);
5824
+ }
5825
+ }
5826
+ function deriveChatAttentionCapability(attentionSecret, fields = {}) {
5827
+ return deriveChatAttentionMaterial(attentionSecret, fields, fields.kind);
5828
+ }
5829
+ function defaultChatNotificationMode(manifest) {
5830
+ if (manifest?.lineage !== "group")
5831
+ return CHAT_NOTIFICATION_MODES.ALL;
5832
+ return manifest.members?.length > CHAT_ORDINARY_DELIVERY_MAX_MEMBERS ? CHAT_NOTIFICATION_MODES.MENTIONS : CHAT_NOTIFICATION_MODES.ALL;
5833
+ }
5834
+ function normalizeChatNotificationMode(value, manifest) {
5835
+ const mode = cleanText(value);
5836
+ if (!NOTIFICATION_MODES.has(mode))
5837
+ throw new Error("chat notification mode required");
5838
+ if (manifest?.lineage !== "group")
5839
+ return CHAT_NOTIFICATION_MODES.ALL;
5840
+ if (mode === CHAT_NOTIFICATION_MODES.ALL && manifest.members?.length > CHAT_ORDINARY_DELIVERY_MAX_MEMBERS) {
5841
+ return CHAT_NOTIFICATION_MODES.MENTIONS;
5842
+ }
5843
+ return mode;
5844
+ }
5845
+ function chatAttentionCommitments(attentionSecret, fields = {}) {
5846
+ const mode = normalizeChatNotificationMode(fields.notificationMode, fields.manifest);
5847
+ const acceptedKinds = mode === CHAT_NOTIFICATION_MODES.ALL ? [CHAT_ATTENTION_KINDS.GENERAL, CHAT_ATTENTION_KINDS.MENTION] : mode === CHAT_NOTIFICATION_MODES.MENTIONS ? [CHAT_ATTENTION_KINDS.MENTION] : [];
5848
+ const commitments = [...acceptedKinds, CHAT_ATTENTION_KINDS.ROTATION].map((kind) => deliveryCapabilityHash(deriveChatAttentionMaterial(attentionSecret, fields, kind)));
5849
+ while (commitments.length < 3) {
5850
+ const padding = toHex(randomBytes3(32));
5851
+ if (!commitments.includes(padding))
5852
+ commitments.push(padding);
5853
+ }
5854
+ return commitments.sort();
5855
+ }
5856
+ function chatDeliveryRegistration(stateCapability, fields = {}) {
5857
+ const attentionFields = {
5858
+ ...fields,
5859
+ notificationMode: fields.notificationMode ?? defaultChatNotificationMode(fields.manifest)
5860
+ };
5861
+ return {
5862
+ capability: deriveChatDeliveryCapability(stateCapability, fields),
5863
+ attentionCommitments: chatAttentionCommitments(fields.attentionSecret, attentionFields),
5864
+ singleUseAttentionCommitment: deliveryCapabilityHash(deriveChatAttentionMaterial(fields.attentionSecret, attentionFields, CHAT_ATTENTION_KINDS.ROTATION))
5865
+ };
5866
+ }
5867
+ function deliveryCapabilityHash(value) {
5868
+ const capability = fromHex(cleanHex32(value, "delivery capability"), "delivery capability");
5869
+ try {
5870
+ return toHex(sha256(capability));
5871
+ } finally {
5872
+ cleanBytes(capability);
5873
+ }
5874
+ }
5659
5875
  function deriveChatInboxDeliveryId(stateCapability, fields = {}) {
5660
5876
  const chatId = cleanHex32(fields.chatId, "inbox delivery chat id");
5661
5877
  const recipientChatPK = cleanHex32(fields.recipientChatPK, "inbox delivery recipient chat key");
@@ -5900,135 +6116,6 @@ function isExtendedPictographic(cp) {
5900
6116
  return findUnicodeRangeCategory(cp, EXTENDED_PICTOGRAPHIC_S, EXTENDED_PICTOGRAPHIC_E) !== 0;
5901
6117
  }
5902
6118
 
5903
- // ../../core/config.js
5904
- var MS_PER_SECOND = 1000;
5905
- var MINUTE_MS = 60 * MS_PER_SECOND;
5906
- var HOUR_MS = 60 * MINUTE_MS;
5907
- var DAY_MS = 24 * HOUR_MS;
5908
- var KIB_BYTES = 1024;
5909
- var MIB_BYTES = 1024 * 1024;
5910
- var SATS_PER_BITCOIN = 100000000n;
5911
- var USERNAME_MAX_CHARS = 12;
5912
- var PASSWORD_MIN_CHARS = 15;
5913
- var PASSWORD_MAX_CHARS = 64;
5914
- var PASSWORD_MAX_BYTES = 4 * 1024;
5915
- var LOCAL_MEDIA_CACHE_MAX_BYTES = 512 * MIB_BYTES;
5916
- var LOCAL_PROFILE_CACHE_MAX_ITEMS = 500;
5917
- var LOCAL_PROFILE_CACHE_MAX_AGE_MS = 30 * DAY_MS;
5918
- var LOCAL_CHAT_CACHE_MAX_ITEMS = 1000;
5919
- var LOCAL_CHAT_MESSAGE_CACHE_MAX_VISIBLE = 1000;
5920
- var LOCAL_AVATAR_CACHE_MAX_BYTES = 128 * MIB_BYTES;
5921
- var LOCAL_AVATAR_CACHE_MAX_AGE_MS = 30 * DAY_MS;
5922
- var AVATAR_IMAGE_MAX_BYTES = 256 * KIB_BYTES;
5923
- var AVATAR_IMAGE_QUALITY_ATTEMPTS = Object.freeze([0.92, 0.84, 0.76, 0.68]);
5924
- var CHAT_AVATAR_IMAGE_MAX_BYTES = AVATAR_IMAGE_MAX_BYTES;
5925
- var CHAT_AVATAR_REF_MAX_CHARS = 256;
5926
- var CHAT_MESSAGE_FILE_CACHE_MAX_BYTES = 64 * MIB_BYTES;
5927
- var IDLE_CALLBACK_MIN_TIMEOUT_MS = 50;
5928
- var ATTACHMENT_CACHE_IDLE_TIMEOUT_MS = 2500;
5929
- var ATTACHMENT_CACHE_FALLBACK_DELAY_MS = 250;
5930
- var CHAT_UNSAVED_TTL_DAYS = 21;
5931
- var CHAT_UNSAVED_TTL_MS = CHAT_UNSAVED_TTL_DAYS * DAY_MS;
5932
- var CHAT_AFTER_SEEN_MS = DAY_MS;
5933
- var CHAT_MEDIA_TTL_DAYS = CHAT_UNSAVED_TTL_DAYS;
5934
- var CHAT_MEDIA_TTL_MS = CHAT_MEDIA_TTL_DAYS * DAY_MS;
5935
- var CHAT_UPLOAD_MAX_BYTES = 64 * MIB_BYTES;
5936
- var CHAT_AUDIO_TRANSCODE_BITRATE_BPS = 128000;
5937
- var CHAT_VIDEO_TRANSCODE_VIDEO_BITRATE_BPS = 3000000;
5938
- var CHAT_VIDEO_TRANSCODE_AUDIO_BITRATE_BPS = CHAT_AUDIO_TRANSCODE_BITRATE_BPS;
5939
- var CHAT_MESSAGE_BATCH_SIZE = 40;
5940
- var CHAT_MESSAGE_QUERY_MAX_DOCS = 120;
5941
- var CHAT_MESSAGE_MUTATION_MAX_ITEMS = 256;
5942
- var CHAT_TTL_CLIENT_DELETE_GRACE_MS = MINUTE_MS;
5943
- var CHAT_BATCH_CLEANUP_IDLE_TIMEOUT_MS = 1500;
5944
- var CHAT_BATCH_CLEANUP_IDLE_DELAY_MS = 250;
5945
- var CHAT_LIST_PAGE_SIZE = 20;
5946
- var CHAT_LIST_LIVE_COUNT = CHAT_LIST_PAGE_SIZE;
5947
- var CHAT_INBOX_PING_PAGE_SIZE = 25;
5948
- var CHAT_INBOX_DISCOVERY_PARALLEL_CHATS = CHAT_INBOX_PING_PAGE_SIZE;
5949
- var CHAT_INBOX_SETTLEMENT_PARALLEL_CHATS = 4;
5950
- var CHAT_INBOX_CURSOR_OVERLAP_MS = 5 * MS_PER_SECOND;
5951
- var CHAT_LIST_CACHE_WRITE_DELAY_MS = 1500;
5952
- var CHAT_LIST_SNAPSHOT_COALESCE_MS = 80;
5953
- var CHAT_LIST_COMPLETE_SYNC_DELAY_MS = 250;
5954
- var CHAT_LIST_LISTENER_RETRY_MS = 1000;
5955
- var CHAT_TOP_WARM_COUNT = 0;
5956
- var CHAT_EAGER_WARM_COUNT = 0;
5957
- var CHAT_WARM_DELAY_MS = 3000;
5958
- var CHAT_WARM_BATCH_SIZE = CHAT_MESSAGE_BATCH_SIZE;
5959
- var CHAT_MESSAGE_VIEW_CACHE_SIZE = 30;
5960
- var CHAT_MEDIA_WARM_MESSAGES_PER_CHAT = CHAT_MESSAGE_BATCH_SIZE;
5961
- var CHAT_MEDIA_WARM_START_DELAY_MS = 600;
5962
- var CHAT_MEDIA_WARM_STEP_DELAY_MS = 120;
5963
- var CHAT_MEDIA_WARM_TYPES = Object.freeze(["img", "gif", "mp4"]);
5964
- var CHAT_MEDIA_WARM_MAX_BYTES = 0;
5965
- var CHAT_READ_WRITE_INTERVAL_MS = 2 * MS_PER_SECOND;
5966
- var CHAT_READ_WRITE_MAX_WAIT_MS = 8 * MS_PER_SECOND;
5967
- var CHAT_LIVE_PING_INTERVAL_MS = 5 * MS_PER_SECOND;
5968
- var CHAT_LIVE_SWEEP_INTERVAL_MS = 15 * MS_PER_SECOND;
5969
- var CHAT_LIVE_STALE_AFTER_MS = 15 * MS_PER_SECOND;
5970
- var CHAT_LIVE_RECONNECT_MAX_MS = 5 * MS_PER_SECOND;
5971
- var CHAT_LIVE_TYPING_IDLE_MS = 8 * MS_PER_SECOND;
5972
- var CHAT_LIVE_TYPING_RENEW_MS = 5 * MS_PER_SECOND;
5973
- var CHAT_LIVE_COMPOSITION_HANDOFF_MS = 15 * MS_PER_SECOND;
5974
- var CHAT_LIVE_READ_SEND_INTERVAL_MS = 120;
5975
- var CHAT_LIVE_READ_SEND_MAX_WAIT_MS = 500;
5976
- var CHAT_LIVE_READ_WRITE_INTERVAL_MS = 12 * MS_PER_SECOND;
5977
- var CHAT_LIVE_READ_WRITE_MAX_WAIT_MS = 30 * MS_PER_SECOND;
5978
- var CHAT_SEND_QUEUE_RATE_LIMIT_COUNT = 12;
5979
- var CHAT_SEND_QUEUE_RATE_LIMIT_WINDOW_MS = 10 * MS_PER_SECOND;
5980
- var CHAT_SETTINGS_BODY_MAX_BYTES = 32 * KIB_BYTES;
5981
- var CHAT_TITLE_STATE_MAX_CHARS = 128;
5982
- var CHAT_MANIFEST_MAX_BYTES = 256 * KIB_BYTES;
5983
- var CHAT_RECEIPT_MAX_MEMBERS = 32;
5984
- var CHAT_ORDINARY_DELIVERY_MAX_MEMBERS = 128;
5985
- var CHAT_MAX_MEMBERS = 256;
5986
- var CHAT_MAX_TEXT_CHARS = 2048;
5987
- var CHAT_TITLE_INPUT_MAX_CHARS = 16;
5988
- var CHAT_MAX_REACTIONS = CHAT_MAX_MEMBERS;
5989
- var SEARCH_DEBOUNCE_MS = 300;
5990
- var RECENT_PEER_REFRESH_LIMIT = 50;
5991
- var RECENT_PEER_REFRESH_DELAY_MS = 250;
5992
- var RECENT_PEER_REFRESH_INTERVAL_MS = 5 * MINUTE_MS;
5993
- var RECENT_PEER_REFRESH_THROTTLE_MS = 120;
5994
- var BAN_REFRESH_GRACE_MS = 50;
5995
- var REQUEST_MONEY_MAX_SATS = SATS_PER_BITCOIN * 100000n;
5996
- var WALLET_TRANSFER_POLL_MS = 3 * MS_PER_SECOND;
5997
- var WALLET_ACTIVE_TRANSFER_REFRESH_MS = MS_PER_SECOND;
5998
- var WALLET_CACHE_HYDRATE_DELAY_MS = 0;
5999
- var WALLET_BOOT_REFRESH_DELAY_MS = 500;
6000
- var WALLET_BOOT_CACHED_REFRESH_DELAY_MS = 2500;
6001
- var WALLET_REGTEST_DEPOSIT_CLAIM_POLL_MS = 20 * MS_PER_SECOND;
6002
- var WALLET_MAINNET_DEPOSIT_CLAIM_POLL_MS = MINUTE_MS;
6003
- var WALLET_BALANCE_EVENT_COALESCE_MS = 2 * MS_PER_SECOND;
6004
- var WALLET_INCOMING_UPDATE_COALESCE_MS = 250;
6005
- var WALLET_AUTO_CLAIM_MAX_FEE_SATS = 5000;
6006
- var WALLET_CLAIM_PAGE_SIZE = 100;
6007
- var WALLET_PENDING_TRANSFER_CLAIM_BATCH_SIZE = 50;
6008
- var WALLET_PENDING_TRANSFER_STATUS_BATCH_SIZE = 2;
6009
- var WALLET_PENDING_TRANSFER_COLD_REFRESH_BATCH_SIZE = 50;
6010
- var WALLET_PENDING_TRANSFER_COLD_REFRESH_INTERVAL_MS = 15 * MS_PER_SECOND;
6011
- var WALLET_PENDING_TRANSFER_ADAPTIVE_BATCH_SMALL_QUEUE = 3;
6012
- var WALLET_PENDING_TRANSFER_ADAPTIVE_BATCH_LARGE_QUEUE = 10;
6013
- var WALLET_PENDING_TRANSFER_ADAPTIVE_BATCH_SMALL_SIZE = 2;
6014
- var WALLET_PENDING_TRANSFER_ADAPTIVE_BATCH_LARGE_SIZE = 3;
6015
- var WALLET_PENDING_TRANSFER_SLOW_CLAIM_MS = 4 * MS_PER_SECOND;
6016
- var WALLET_PENDING_TRANSFER_CLAIM_COOLDOWN_MS = 15 * MS_PER_SECOND;
6017
- var WALLET_SDK_BACKGROUND_QUIET_MS = MS_PER_SECOND;
6018
- var WALLET_RECENT_TRANSFER_LIMIT = 100;
6019
- var WALLET_TRANSFER_PAGE_LIMIT = 100;
6020
- var WALLET_TRANSFER_FETCH_THROTTLE_MS = 150;
6021
- var WALLET_PENDING_TRANSFER_CLAIM_RETRY_MS = 3 * MS_PER_SECOND;
6022
- var WALLET_SENT_TRANSFER_REFRESH_DELAY_MS = 3 * MS_PER_SECOND;
6023
- var WALLET_PENDING_TRANSFER_BOOT_GRACE_MS = 15 * MS_PER_SECOND;
6024
- var WALLET_PENDING_TRANSFER_HOT_AGE_MS = MINUTE_MS;
6025
- var WALLET_PENDING_TRANSFER_WARM_AGE_MS = 10 * MINUTE_MS;
6026
- var WALLET_PENDING_TRANSFER_WARM_RETRY_MS = 15 * MS_PER_SECOND;
6027
- var WALLET_PENDING_TRANSFER_STALE_RETRY_MS = 2 * MINUTE_MS;
6028
- var WALLET_PENDING_TRANSFER_STUCK_RETRY_MS = 10 * MINUTE_MS;
6029
- var WALLET_PENDING_TRANSFER_DORMANT_RETRY_MS = HOUR_MS;
6030
- var WALLET_TRANSFER_CACHE_WRITE_DELAY_MS = 3 * MS_PER_SECOND;
6031
-
6032
6119
  // ../../core/password.js
6033
6120
  var MIN_PASSWORD = PASSWORD_MIN_CHARS;
6034
6121
  var MAX_PASSWORD = PASSWORD_MAX_CHARS;
@@ -7670,7 +7757,7 @@ function cleanAvatarUsername(value) {
7670
7757
  }
7671
7758
 
7672
7759
  // ../../core/agreement.js
7673
- var TERMS_VERSION = "2026-08-22.4";
7760
+ var TERMS_VERSION = "2026-09-01.1";
7674
7761
  var CURRENT_AGREEMENT = Object.freeze({
7675
7762
  termsVersion: TERMS_VERSION
7676
7763
  });
@@ -7802,6 +7889,312 @@ function nextBanRefreshMs(banned, keys = ["full", "chat"], now = Date.now()) {
7802
7889
  return times[0] ?? null;
7803
7890
  }
7804
7891
 
7892
+ // ../../core/chat/protocol.js
7893
+ "use client";
7894
+ var CHAT_PROTOCOL_VERSION = 3;
7895
+ var CHAT_MANIFEST_VERSION = 2;
7896
+ var CHAT_TRANSITION_VERSION = 2;
7897
+ var CHAT_MESSAGE_ENVELOPE_VERSION = 3;
7898
+ var CHAT_SETTINGS_VERSION = 1;
7899
+
7900
+ // ../../core/chat/epochs/manifest.js
7901
+ "use client";
7902
+ var CHAT_LINEAGES = Object.freeze({
7903
+ SELF: "self",
7904
+ DIRECT: "direct",
7905
+ GROUP: "group"
7906
+ });
7907
+ var LINEAGES = new Set(Object.values(CHAT_LINEAGES));
7908
+ var HEX_32_RE2 = /^[0-9a-f]{64}$/u;
7909
+ function cleanChatHex(value, label = "chat value") {
7910
+ const text = cleanText(value).toLowerCase();
7911
+ if (!HEX_32_RE2.test(text)) {
7912
+ throw new Error(`${label} required`);
7913
+ }
7914
+ return text;
7915
+ }
7916
+ function cleanUid(value) {
7917
+ const uid = cleanText(value);
7918
+ if (!uid || uid.length > 128) {
7919
+ throw new Error("chat member uid required");
7920
+ }
7921
+ return uid;
7922
+ }
7923
+ function cleanCreatedAt(value) {
7924
+ if (!Number.isSafeInteger(value) || value <= 0) {
7925
+ throw new Error("chat epoch createdAt required");
7926
+ }
7927
+ return value;
7928
+ }
7929
+ function cleanEpochVersion(value) {
7930
+ if (!Number.isSafeInteger(value) || value <= 0) {
7931
+ throw new Error("chat epoch version required");
7932
+ }
7933
+ return value;
7934
+ }
7935
+ function cleanMember(value) {
7936
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
7937
+ throw new Error("invalid chat member");
7938
+ }
7939
+ return {
7940
+ uid: cleanUid(value.uid),
7941
+ chatPK: cleanChatHex(value.chatPK, "member chat key"),
7942
+ chatSigningPK: cleanChatHex(value.chatSigningPK, "member signing key"),
7943
+ notificationPK: cleanChatHex(value.notificationPK, "member notification key"),
7944
+ mlsLeafId: cleanChatHex(value.mlsLeafId, "member mls leaf id")
7945
+ };
7946
+ }
7947
+ function assertUniqueMembers(members) {
7948
+ const uids = new Set;
7949
+ const chatKeys = new Set;
7950
+ const signingKeys = new Set;
7951
+ const mlsLeafIds = new Set;
7952
+ for (const member of members) {
7953
+ if (uids.has(member.uid) || chatKeys.has(member.chatPK) || signingKeys.has(member.chatSigningPK) || mlsLeafIds.has(member.mlsLeafId)) {
7954
+ throw new Error("duplicate chat member");
7955
+ }
7956
+ uids.add(member.uid);
7957
+ chatKeys.add(member.chatPK);
7958
+ signingKeys.add(member.chatSigningPK);
7959
+ mlsLeafIds.add(member.mlsLeafId);
7960
+ }
7961
+ }
7962
+ function assertLineage(manifest, parent) {
7963
+ const memberCount = manifest.members.length;
7964
+ if (memberCount > 2 && manifest.lineage !== CHAT_LINEAGES.GROUP) {
7965
+ throw new Error("group lineage required");
7966
+ }
7967
+ if (!parent) {
7968
+ return;
7969
+ }
7970
+ if (manifest.chatId !== parent.chatId || manifest.parentEpochId !== parent.epochId || manifest.epochVersion !== parent.epochVersion + 1) {
7971
+ throw new Error("invalid chat epoch successor");
7972
+ }
7973
+ if (parent.lineage === CHAT_LINEAGES.GROUP && manifest.lineage !== CHAT_LINEAGES.GROUP) {
7974
+ throw new Error("chat group lineage is permanent");
7975
+ }
7976
+ }
7977
+ function normalizeEpochManifest(value, options = {}) {
7978
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
7979
+ throw new Error("chat manifest required");
7980
+ }
7981
+ if (value.v !== CHAT_MANIFEST_VERSION || value.protocol !== CHAT_PROTOCOL_VERSION) {
7982
+ throw new Error("unsupported chat manifest");
7983
+ }
7984
+ if (!Array.isArray(value.members) || value.members.length < 1 || value.members.length > CHAT_MAX_MEMBERS) {
7985
+ throw new Error("invalid chat member count");
7986
+ }
7987
+ const lineage = cleanText(value.lineage);
7988
+ if (!LINEAGES.has(lineage)) {
7989
+ throw new Error("invalid chat lineage");
7990
+ }
7991
+ const members = value.members.map(cleanMember).sort((a, b) => a.chatPK.localeCompare(b.chatPK));
7992
+ assertUniqueMembers(members);
7993
+ const manifest = {
7994
+ v: CHAT_MANIFEST_VERSION,
7995
+ protocol: CHAT_PROTOCOL_VERSION,
7996
+ chatId: cleanChatHex(value.chatId, "chat id"),
7997
+ epochId: cleanChatHex(value.epochId, "chat epoch id"),
7998
+ epochVersion: cleanEpochVersion(value.epochVersion),
7999
+ parentEpochId: value.parentEpochId == null ? null : cleanChatHex(value.parentEpochId, "parent epoch id"),
8000
+ createdAt: cleanCreatedAt(value.createdAt),
8001
+ lineage,
8002
+ members
8003
+ };
8004
+ if (manifest.epochVersion === 1 !== (manifest.parentEpochId === null)) {
8005
+ throw new Error("invalid parent epoch");
8006
+ }
8007
+ const bytes = canonicalBytes(manifest, "chat manifest");
8008
+ if (bytes.length > CHAT_MANIFEST_MAX_BYTES) {
8009
+ throw new Error("chat manifest too large");
8010
+ }
8011
+ assertLineage(manifest, options.parent || null);
8012
+ return manifest;
8013
+ }
8014
+ function epochManifestDigest(manifest) {
8015
+ return toHex(sha256(canonicalBytes(normalizeEpochManifest(manifest), "chat manifest digest")));
8016
+ }
8017
+ function manifestMember(manifest, chatPK) {
8018
+ const key = cleanChatHex(chatPK, "member chat key");
8019
+ return normalizeEpochManifest(manifest).members.find((member) => member.chatPK === key) || null;
8020
+ }
8021
+ function manifestSigningKeys(manifest) {
8022
+ return Object.fromEntries(normalizeEpochManifest(manifest).members.map((member) => [member.chatPK, member.chatSigningPK]));
8023
+ }
8024
+
8025
+ // ../../core/chat/admission.js
8026
+ "use client";
8027
+ var CHAT_ADMISSION_VERSION = 1;
8028
+ var CHAT_ADMISSION_GRANT_COUNT = 8;
8029
+ var CHAT_ADMISSION_DECISIONS = Object.freeze({
8030
+ ACCEPT: "accept",
8031
+ REQUEST: "request",
8032
+ REJECT: "reject"
8033
+ });
8034
+ var CHAT_DIRECT_ADMISSION_MODES = Object.freeze({
8035
+ OPEN: "open",
8036
+ REQUESTS: "requests",
8037
+ CLOSED: "closed"
8038
+ });
8039
+ var CHAT_GROUP_ADMISSION_MODES = Object.freeze({
8040
+ OPEN: "open",
8041
+ CLOSED: "closed"
8042
+ });
8043
+ var HEX_32_RE3 = /^[0-9a-f]{64}$/u;
8044
+ var DIRECT_MODES = new Set(Object.values(CHAT_DIRECT_ADMISSION_MODES));
8045
+ var GROUP_MODES = new Set(Object.values(CHAT_GROUP_ADMISSION_MODES));
8046
+ var DECISION_RANK = Object.freeze({ accept: 0, request: 1, reject: 2 });
8047
+ var DEFAULT_ADMISSION = Object.freeze({
8048
+ v: CHAT_ADMISSION_VERSION,
8049
+ direct: CHAT_DIRECT_ADMISSION_MODES.OPEN,
8050
+ groups: CHAT_GROUP_ADMISSION_MODES.OPEN,
8051
+ directGrants: Object.freeze([])
8052
+ });
8053
+ var CLOSED_ADMISSION = Object.freeze({
8054
+ v: CHAT_ADMISSION_VERSION,
8055
+ direct: CHAT_DIRECT_ADMISSION_MODES.CLOSED,
8056
+ groups: CHAT_GROUP_ADMISSION_MODES.CLOSED,
8057
+ directGrants: Object.freeze([])
8058
+ });
8059
+ function validGrantList(value) {
8060
+ return Array.isArray(value) && value.length === CHAT_ADMISSION_GRANT_COUNT && value.every((item) => typeof item === "string" && HEX_32_RE3.test(item)) && new Set(value).size === value.length && value.every((item, index) => index === 0 || value[index - 1] < item);
8061
+ }
8062
+ function normalizeChatAdmission(value) {
8063
+ if (value == null)
8064
+ return DEFAULT_ADMISSION;
8065
+ if (typeof value !== "object" || Array.isArray(value) || value.v !== CHAT_ADMISSION_VERSION || !DIRECT_MODES.has(value.direct) || !GROUP_MODES.has(value.groups) || !validGrantList(value.directGrants))
8066
+ return CLOSED_ADMISSION;
8067
+ return Object.freeze({
8068
+ v: CHAT_ADMISSION_VERSION,
8069
+ direct: value.direct,
8070
+ groups: value.groups,
8071
+ directGrants: Object.freeze([...value.directGrants])
8072
+ });
8073
+ }
8074
+ function normalizeChatAdmissionDecision(value) {
8075
+ return Object.values(CHAT_ADMISSION_DECISIONS).includes(value) ? value : CHAT_ADMISSION_DECISIONS.REJECT;
8076
+ }
8077
+ function strictestChatAdmissionDecision(...values) {
8078
+ return values.map(normalizeChatAdmissionDecision).reduce((strictest, value) => DECISION_RANK[value] > DECISION_RANK[strictest] ? value : strictest, CHAT_ADMISSION_DECISIONS.ACCEPT);
8079
+ }
8080
+ function orderedChatKeys(first, second) {
8081
+ return [
8082
+ cleanChatHex(first, "chat public key"),
8083
+ cleanChatHex(second, "peer chat public key")
8084
+ ].sort();
8085
+ }
8086
+ function deriveDirectAdmissionCapability(chatPrivateKey, chatPK, peerChatPK, recipientChatPK) {
8087
+ const ownChatPK = cleanChatHex(chatPK, "chat public key");
8088
+ const otherChatPK = cleanChatHex(peerChatPK, "peer chat public key");
8089
+ const recipient = cleanChatHex(recipientChatPK, "recipient chat public key");
8090
+ if (ownChatPK === otherChatPK || ![ownChatPK, otherChatPK].includes(recipient)) {
8091
+ throw new Error("direct admission peer required");
8092
+ }
8093
+ let peerKey = null;
8094
+ let shared = null;
8095
+ let capability = null;
8096
+ try {
8097
+ peerKey = fromHex(otherChatPK, "peer chat public key");
8098
+ shared = x25519.getSharedSecret(toBytes32(chatPrivateKey, "chat private key"), peerKey);
8099
+ capability = deriveKey(shared.subarray(0, 32), "direct-chat-admission-capability-v1", [...orderedChatKeys(ownChatPK, otherChatPK), recipient]);
8100
+ return toHex(capability);
8101
+ } finally {
8102
+ cleanBytes(peerKey, shared, capability);
8103
+ }
8104
+ }
8105
+ function chatAdmissionCapabilityCommitment(value) {
8106
+ let capability = null;
8107
+ try {
8108
+ capability = fromHex(value, "chat admission capability");
8109
+ return toHex(sha256(capability));
8110
+ } finally {
8111
+ cleanBytes(capability);
8112
+ }
8113
+ }
8114
+ function directAdmissionCapabilityForPeer(chatPrivateKey, chatPK, profile) {
8115
+ return deriveDirectAdmissionCapability(chatPrivateKey, chatPK, profile?.chatPK, profile?.chatPK);
8116
+ }
8117
+ function hasDirectAdmissionGrant(chatPrivateKey, chatPK, profile) {
8118
+ const policy = normalizeChatAdmission(profile?.chatAdmission);
8119
+ if (!policy.directGrants.length)
8120
+ return false;
8121
+ const capability = directAdmissionCapabilityForPeer(chatPrivateKey, chatPK, profile);
8122
+ try {
8123
+ return policy.directGrants.includes(chatAdmissionCapabilityCommitment(capability));
8124
+ } finally {
8125
+ cleanBytes(capability);
8126
+ }
8127
+ }
8128
+ function directAdmissionForPeer(chatPrivateKey, chatPK, profile) {
8129
+ if (hasDirectAdmissionGrant(chatPrivateKey, chatPK, profile)) {
8130
+ return CHAT_ADMISSION_DECISIONS.ACCEPT;
8131
+ }
8132
+ const mode = normalizeChatAdmission(profile?.chatAdmission).direct;
8133
+ if (mode === CHAT_DIRECT_ADMISSION_MODES.OPEN)
8134
+ return CHAT_ADMISSION_DECISIONS.ACCEPT;
8135
+ if (mode === CHAT_DIRECT_ADMISSION_MODES.REQUESTS)
8136
+ return CHAT_ADMISSION_DECISIONS.REQUEST;
8137
+ return CHAT_ADMISSION_DECISIONS.REJECT;
8138
+ }
8139
+ function canStartDirectChat(chatPrivateKey, chatPK, profile) {
8140
+ return directAdmissionForPeer(chatPrivateKey, chatPK, profile) !== CHAT_ADMISSION_DECISIONS.REJECT;
8141
+ }
8142
+ function canInviteToGroup(profile) {
8143
+ return normalizeChatAdmission(profile?.chatAdmission).groups === CHAT_GROUP_ADMISSION_MODES.OPEN;
8144
+ }
8145
+ function incomingChatAdmissionDecision(chatPrivateKey, chatPK, senderProfile, recipientAdmission, lineage) {
8146
+ const policy = normalizeChatAdmission(recipientAdmission);
8147
+ if (lineage === "direct") {
8148
+ const capability = deriveDirectAdmissionCapability(chatPrivateKey, chatPK, senderProfile?.chatPK, chatPK);
8149
+ try {
8150
+ if (policy.directGrants.includes(chatAdmissionCapabilityCommitment(capability))) {
8151
+ return CHAT_ADMISSION_DECISIONS.ACCEPT;
8152
+ }
8153
+ } finally {
8154
+ cleanBytes(capability);
8155
+ }
8156
+ if (policy.direct === CHAT_DIRECT_ADMISSION_MODES.OPEN)
8157
+ return CHAT_ADMISSION_DECISIONS.ACCEPT;
8158
+ if (policy.direct === CHAT_DIRECT_ADMISSION_MODES.REQUESTS)
8159
+ return CHAT_ADMISSION_DECISIONS.REQUEST;
8160
+ return CHAT_ADMISSION_DECISIONS.REJECT;
8161
+ }
8162
+ return lineage === "group" && policy.groups === CHAT_GROUP_ADMISSION_MODES.OPEN ? CHAT_ADMISSION_DECISIONS.ACCEPT : CHAT_ADMISSION_DECISIONS.REJECT;
8163
+ }
8164
+ function makeChatAdmission(chatPrivateKey, chatPK, options = {}) {
8165
+ const direct = options.direct || CHAT_DIRECT_ADMISSION_MODES.OPEN;
8166
+ const groups = options.groups || CHAT_GROUP_ADMISSION_MODES.OPEN;
8167
+ if (!DIRECT_MODES.has(direct) || !GROUP_MODES.has(groups)) {
8168
+ throw new Error("valid chat admission modes required");
8169
+ }
8170
+ const peers = [...new Map((options.allow || []).map((profile) => {
8171
+ const peerChatPK = cleanChatHex(profile?.chatPK, "allowed peer chat public key");
8172
+ return [peerChatPK, profile];
8173
+ })).values()];
8174
+ if (peers.length > CHAT_ADMISSION_GRANT_COUNT) {
8175
+ throw new Error(`chat admission allows at most ${CHAT_ADMISSION_GRANT_COUNT} direct peers`);
8176
+ }
8177
+ const commitments = peers.map((profile) => {
8178
+ const capability = deriveDirectAdmissionCapability(chatPrivateKey, chatPK, profile.chatPK, chatPK);
8179
+ try {
8180
+ return chatAdmissionCapabilityCommitment(capability);
8181
+ } finally {
8182
+ cleanBytes(capability);
8183
+ }
8184
+ });
8185
+ while (commitments.length < CHAT_ADMISSION_GRANT_COUNT) {
8186
+ const padding = toHex(randomBytes3(32));
8187
+ if (!commitments.includes(padding))
8188
+ commitments.push(padding);
8189
+ }
8190
+ return Object.freeze({
8191
+ v: CHAT_ADMISSION_VERSION,
8192
+ direct,
8193
+ groups,
8194
+ directGrants: Object.freeze(commitments.sort())
8195
+ });
8196
+ }
8197
+
7805
8198
  // ../../core/profile.js
7806
8199
  var BOT_PROFILE_MARKER = "glyphteck";
7807
8200
  function readBotMarker(profile) {
@@ -7834,6 +8227,7 @@ function normalizeProfile(profile, uid = profile?.uid || null) {
7834
8227
  chatPK: profile?.chatPK || null,
7835
8228
  chatSigningPK: profile?.chatSigningPK || null,
7836
8229
  notificationPK: profile?.notificationPK || null,
8230
+ chatAdmission: normalizeChatAdmission(profile?.chatAdmission),
7837
8231
  active: profile?.active ?? false,
7838
8232
  bot: readBotMarker(profile),
7839
8233
  avatarVersion: readAvatarVersion(profile?.avatarVersion),
@@ -7872,6 +8266,7 @@ var defaultUser = {
7872
8266
  chatPK: null,
7873
8267
  chatSigningPK: null,
7874
8268
  notificationPK: null,
8269
+ chatAdmission: normalizeChatAdmission(null),
7875
8270
  active: false,
7876
8271
  banned: null,
7877
8272
  agreement: null,
@@ -7986,7 +8381,7 @@ function createUser({ cloud, network, avatarCache = null, diag = null, onSession
7986
8381
  if (!uid || !avatarCache)
7987
8382
  return null;
7988
8383
  try {
7989
- const cached = await avatarCache.read(uid);
8384
+ const cached = await avatarCache.read(uid, expectedVersion);
7990
8385
  const version = readAvatarVersion(cached?.version);
7991
8386
  if (expectedVersion != null && version !== expectedVersion) {
7992
8387
  return null;
@@ -8296,14 +8691,15 @@ function createUser({ cloud, network, avatarCache = null, diag = null, onSession
8296
8691
  const walletPK = resolveWalletPK(profileData, activeNetwork);
8297
8692
  const { chatPK, chatSigningPK } = readProfileChatIdentity(profileData);
8298
8693
  const notificationPK = readProfileNotificationPK(profileData);
8694
+ const chatAdmission = normalizeChatAdmission(profileData.chatAdmission);
8299
8695
  const active = profileData.active ?? false;
8300
8696
  const hasAvatarEntry = !!info.exists && Object.prototype.hasOwnProperty.call(profileData, "avatar");
8301
8697
  const avatarVersion2 = readAvatarVersion(profileData.avatar);
8302
8698
  const avatar = avatarVersion2 == null ? null : user.avatar;
8303
- if (user.profileReady && user.uid === authUser.uid && user.username === username && user.identities === identities && user.walletPK === walletPK && user.chatPK === chatPK && user.chatSigningPK === chatSigningPK && user.notificationPK === notificationPK && user.active === active && user.hasAvatarEntry === hasAvatarEntry && user.avatarVersion === avatarVersion2 && user.avatar === avatar) {
8699
+ if (user.profileReady && user.uid === authUser.uid && user.username === username && user.identities === identities && user.walletPK === walletPK && user.chatPK === chatPK && user.chatSigningPK === chatSigningPK && user.notificationPK === notificationPK && user.chatAdmission === chatAdmission && user.active === active && user.hasAvatarEntry === hasAvatarEntry && user.avatarVersion === avatarVersion2 && user.avatar === avatar) {
8304
8700
  return user;
8305
8701
  }
8306
- return { ...user, uid: authUser.uid, profileReady: true, username, identities, walletPK, chatPK, chatSigningPK, notificationPK, active, hasAvatarEntry, avatarVersion: avatarVersion2, avatar };
8702
+ return { ...user, uid: authUser.uid, profileReady: true, username, identities, walletPK, chatPK, chatSigningPK, notificationPK, chatAdmission, active, hasAvatarEntry, avatarVersion: avatarVersion2, avatar };
8307
8703
  });
8308
8704
  const avatarVersion = readAvatarVersion(profileData.avatar);
8309
8705
  if (avatarVersion == null) {
@@ -8342,6 +8738,7 @@ function createUser({ cloud, network, avatarCache = null, diag = null, onSession
8342
8738
  chatPK: null,
8343
8739
  chatSigningPK: null,
8344
8740
  notificationPK: null,
8741
+ chatAdmission: normalizeChatAdmission(null),
8345
8742
  active: false,
8346
8743
  avatarVersion: null,
8347
8744
  avatar: null,
@@ -8684,139 +9081,6 @@ async function pushCriticalInbox(cloud, recipientUid, ping, options = {}, retry
8684
9081
  }
8685
9082
  var criticalDeliveryInternals = Object.freeze({ retryableDeliveryError });
8686
9083
 
8687
- // ../../core/chat/protocol.js
8688
- "use client";
8689
- var CHAT_PROTOCOL_VERSION = 3;
8690
- var CHAT_MANIFEST_VERSION = 2;
8691
- var CHAT_TRANSITION_VERSION = 2;
8692
- var CHAT_MESSAGE_ENVELOPE_VERSION = 3;
8693
- var CHAT_SETTINGS_VERSION = 1;
8694
-
8695
- // ../../core/chat/epochs/manifest.js
8696
- "use client";
8697
- var CHAT_LINEAGES = Object.freeze({
8698
- SELF: "self",
8699
- DIRECT: "direct",
8700
- GROUP: "group"
8701
- });
8702
- var LINEAGES = new Set(Object.values(CHAT_LINEAGES));
8703
- var HEX_32_RE2 = /^[0-9a-f]{64}$/u;
8704
- function cleanChatHex(value, label = "chat value") {
8705
- const text = cleanText(value).toLowerCase();
8706
- if (!HEX_32_RE2.test(text)) {
8707
- throw new Error(`${label} required`);
8708
- }
8709
- return text;
8710
- }
8711
- function cleanUid(value) {
8712
- const uid = cleanText(value);
8713
- if (!uid || uid.length > 128) {
8714
- throw new Error("chat member uid required");
8715
- }
8716
- return uid;
8717
- }
8718
- function cleanCreatedAt(value) {
8719
- if (!Number.isSafeInteger(value) || value <= 0) {
8720
- throw new Error("chat epoch createdAt required");
8721
- }
8722
- return value;
8723
- }
8724
- function cleanEpochVersion(value) {
8725
- if (!Number.isSafeInteger(value) || value <= 0) {
8726
- throw new Error("chat epoch version required");
8727
- }
8728
- return value;
8729
- }
8730
- function cleanMember(value) {
8731
- if (!value || typeof value !== "object" || Array.isArray(value)) {
8732
- throw new Error("invalid chat member");
8733
- }
8734
- return {
8735
- uid: cleanUid(value.uid),
8736
- chatPK: cleanChatHex(value.chatPK, "member chat key"),
8737
- chatSigningPK: cleanChatHex(value.chatSigningPK, "member signing key"),
8738
- notificationPK: cleanChatHex(value.notificationPK, "member notification key"),
8739
- mlsLeafId: cleanChatHex(value.mlsLeafId, "member mls leaf id")
8740
- };
8741
- }
8742
- function assertUniqueMembers(members) {
8743
- const uids = new Set;
8744
- const chatKeys = new Set;
8745
- const signingKeys = new Set;
8746
- const mlsLeafIds = new Set;
8747
- for (const member of members) {
8748
- if (uids.has(member.uid) || chatKeys.has(member.chatPK) || signingKeys.has(member.chatSigningPK) || mlsLeafIds.has(member.mlsLeafId)) {
8749
- throw new Error("duplicate chat member");
8750
- }
8751
- uids.add(member.uid);
8752
- chatKeys.add(member.chatPK);
8753
- signingKeys.add(member.chatSigningPK);
8754
- mlsLeafIds.add(member.mlsLeafId);
8755
- }
8756
- }
8757
- function assertLineage(manifest, parent) {
8758
- const memberCount = manifest.members.length;
8759
- if (memberCount > 2 && manifest.lineage !== CHAT_LINEAGES.GROUP) {
8760
- throw new Error("group lineage required");
8761
- }
8762
- if (!parent) {
8763
- return;
8764
- }
8765
- if (manifest.chatId !== parent.chatId || manifest.parentEpochId !== parent.epochId || manifest.epochVersion !== parent.epochVersion + 1) {
8766
- throw new Error("invalid chat epoch successor");
8767
- }
8768
- if (parent.lineage === CHAT_LINEAGES.GROUP && manifest.lineage !== CHAT_LINEAGES.GROUP) {
8769
- throw new Error("chat group lineage is permanent");
8770
- }
8771
- }
8772
- function normalizeEpochManifest(value, options = {}) {
8773
- if (!value || typeof value !== "object" || Array.isArray(value)) {
8774
- throw new Error("chat manifest required");
8775
- }
8776
- if (value.v !== CHAT_MANIFEST_VERSION || value.protocol !== CHAT_PROTOCOL_VERSION) {
8777
- throw new Error("unsupported chat manifest");
8778
- }
8779
- if (!Array.isArray(value.members) || value.members.length < 1 || value.members.length > CHAT_MAX_MEMBERS) {
8780
- throw new Error("invalid chat member count");
8781
- }
8782
- const lineage = cleanText(value.lineage);
8783
- if (!LINEAGES.has(lineage)) {
8784
- throw new Error("invalid chat lineage");
8785
- }
8786
- const members = value.members.map(cleanMember).sort((a, b) => a.chatPK.localeCompare(b.chatPK));
8787
- assertUniqueMembers(members);
8788
- const manifest = {
8789
- v: CHAT_MANIFEST_VERSION,
8790
- protocol: CHAT_PROTOCOL_VERSION,
8791
- chatId: cleanChatHex(value.chatId, "chat id"),
8792
- epochId: cleanChatHex(value.epochId, "chat epoch id"),
8793
- epochVersion: cleanEpochVersion(value.epochVersion),
8794
- parentEpochId: value.parentEpochId == null ? null : cleanChatHex(value.parentEpochId, "parent epoch id"),
8795
- createdAt: cleanCreatedAt(value.createdAt),
8796
- lineage,
8797
- members
8798
- };
8799
- if (manifest.epochVersion === 1 !== (manifest.parentEpochId === null)) {
8800
- throw new Error("invalid parent epoch");
8801
- }
8802
- const bytes = canonicalBytes(manifest, "chat manifest");
8803
- if (bytes.length > CHAT_MANIFEST_MAX_BYTES) {
8804
- throw new Error("chat manifest too large");
8805
- }
8806
- assertLineage(manifest, options.parent || null);
8807
- return manifest;
8808
- }
8809
- function epochManifestDigest(manifest) {
8810
- return toHex(sha256(canonicalBytes(normalizeEpochManifest(manifest), "chat manifest digest")));
8811
- }
8812
- function manifestMember(manifest, chatPK) {
8813
- const key = cleanChatHex(chatPK, "member chat key");
8814
- return normalizeEpochManifest(manifest).members.find((member) => member.chatPK === key) || null;
8815
- }
8816
- function manifestSigningKeys(manifest) {
8817
- return Object.fromEntries(normalizeEpochManifest(manifest).members.map((member) => [member.chatPK, member.chatSigningPK]));
8818
- }
8819
-
8820
9084
  // ../../core/chat/epochs/state.js
8821
9085
  "use client";
8822
9086
  var CAPABILITY_WITNESS_SCOPE = "veyl-chat-state-witness-v3:";
@@ -10028,6 +10292,7 @@ function epochChatSettingsId(epochState) {
10028
10292
  "use client";
10029
10293
  var CHAT_ENTRY_VERSION = 4;
10030
10294
  var CHAT_OWNER_EPOCH_ENTRY_VERSION = 2;
10295
+ var CHAT_NOTIFICATION_PREFERENCE_VERSION = 1;
10031
10296
  var CHAT_OWNER_RETIREMENT_KINDS = Object.freeze({
10032
10297
  LEAVE: "leave",
10033
10298
  REMOVED: "removed"
@@ -10049,6 +10314,16 @@ function entryKey(chatPrivateKey, entryId) {
10049
10314
  function entryAad(entryId) {
10050
10315
  return canonicalBytes({ v: CHAT_ENTRY_VERSION, protocol: CHAT_PROTOCOL_VERSION, entryId }, "chat entry aad");
10051
10316
  }
10317
+ function notificationPreferenceKey(chatPrivateKey, entryId) {
10318
+ return deriveKey(toBytes32(chatPrivateKey, "chat private key"), "user-chat-notification-preference-v1", [cleanText(entryId)]);
10319
+ }
10320
+ function notificationPreferenceAad(entryId) {
10321
+ return canonicalBytes({
10322
+ v: CHAT_NOTIFICATION_PREFERENCE_VERSION,
10323
+ protocol: CHAT_PROTOCOL_VERSION,
10324
+ entryId
10325
+ }, "chat notification preference aad");
10326
+ }
10052
10327
  function ownerEpochKey(chatPrivateKey, entryId, epochEntryId) {
10053
10328
  return deriveKey(toBytes32(chatPrivateKey, "chat private key"), "user-chat-epoch-entry-v3", [entryId, epochEntryId]);
10054
10329
  }
@@ -10084,12 +10359,13 @@ function normalizeRoutes(value) {
10084
10359
  }
10085
10360
  const uid = cleanText(route.uid);
10086
10361
  const deliveryCapability = route.deliveryCapability == null ? null : cleanChatHex(route.deliveryCapability, "delivery capability");
10362
+ const admissionCapability = route.admissionCapability == null ? null : cleanChatHex(route.admissionCapability, "admission capability");
10087
10363
  const notificationPK = route.notificationPK == null ? null : cleanChatHex(route.notificationPK, "notification key");
10088
10364
  const generation = Number.isSafeInteger(route.generation) && route.generation > 0 ? route.generation : 1;
10089
10365
  if (!uid || uid.length > 128) {
10090
10366
  throw new Error("chat route uid required");
10091
10367
  }
10092
- routes[chatPK] = { uid, deliveryCapability, notificationPK, generation };
10368
+ routes[chatPK] = { uid, deliveryCapability, admissionCapability, notificationPK, generation };
10093
10369
  }
10094
10370
  return routes;
10095
10371
  }
@@ -10146,6 +10422,32 @@ function normalizeOwnerEntry(value) {
10146
10422
  retirement: normalizeOwnerRetirement(value.retirement, current)
10147
10423
  };
10148
10424
  }
10425
+ function withNotificationPreference(entry, value = {}) {
10426
+ const manifest = entry.current.manifest;
10427
+ const notificationMode = normalizeChatNotificationMode(value.notificationMode ?? defaultChatNotificationMode(manifest), manifest);
10428
+ const registrationMatches = value.registeredEpochId === manifest.epochId && value.attentionRegistrationVersion === CHAT_ATTENTION_REGISTRATION_VERSION;
10429
+ return {
10430
+ ...entry,
10431
+ attentionRegistrationVersion: registrationMatches ? CHAT_ATTENTION_REGISTRATION_VERSION : 0,
10432
+ notificationMode
10433
+ };
10434
+ }
10435
+ function normalizeNotificationPreference(value, entry) {
10436
+ if (!value || typeof value !== "object" || Array.isArray(value) || value.v !== CHAT_NOTIFICATION_PREFERENCE_VERSION || value.protocol !== CHAT_PROTOCOL_VERSION) {
10437
+ throw new Error("invalid chat notification preference");
10438
+ }
10439
+ const manifest = entry.current.manifest;
10440
+ const notificationMode = normalizeChatNotificationMode(value.notificationMode, manifest);
10441
+ const registeredEpochId = value.registeredEpochId == null ? null : cleanChatHex(value.registeredEpochId, "registered chat epoch id");
10442
+ const attentionRegistrationVersion = registeredEpochId === manifest.epochId && value.attentionRegistrationVersion === CHAT_ATTENTION_REGISTRATION_VERSION ? CHAT_ATTENTION_REGISTRATION_VERSION : 0;
10443
+ return {
10444
+ v: CHAT_NOTIFICATION_PREFERENCE_VERSION,
10445
+ protocol: CHAT_PROTOCOL_VERSION,
10446
+ notificationMode,
10447
+ registeredEpochId: attentionRegistrationVersion ? registeredEpochId : null,
10448
+ attentionRegistrationVersion
10449
+ };
10450
+ }
10149
10451
  async function sealOwnChatEntry(chatPrivateKey, entryId, entry) {
10150
10452
  const key = entryKey(chatPrivateKey, entryId);
10151
10453
  try {
@@ -10159,14 +10461,14 @@ async function openOwnChatEntry(chatPrivateKey, entryId, body) {
10159
10461
  const key = entryKey(chatPrivateKey, entryId);
10160
10462
  try {
10161
10463
  const { nonce, ct } = unpackBodyData(body);
10162
- return normalizeOwnerEntry(await openJson(key, nonce, ct, entryAad(entryId)));
10464
+ return withNotificationPreference(normalizeOwnerEntry(await openJson(key, nonce, ct, entryAad(entryId))));
10163
10465
  } finally {
10164
10466
  cleanBytes(key);
10165
10467
  }
10166
10468
  }
10167
10469
  function makeOwnChatEntry(epoch, fields = {}) {
10168
10470
  const manifest = normalizeEpochManifest(epoch?.manifest);
10169
- return normalizeOwnerEntry({
10471
+ const entry = normalizeOwnerEntry({
10170
10472
  v: CHAT_ENTRY_VERSION,
10171
10473
  protocol: CHAT_PROTOCOL_VERSION,
10172
10474
  ownerRevision: Number.isSafeInteger(fields.ownerRevision) && fields.ownerRevision > 0 ? fields.ownerRevision : 1,
@@ -10187,6 +10489,48 @@ function makeOwnChatEntry(epoch, fields = {}) {
10187
10489
  notificationTag: cleanText(fields.notificationTag) || null,
10188
10490
  retirement: fields.retirement || null
10189
10491
  });
10492
+ return withNotificationPreference(entry, {
10493
+ attentionRegistrationVersion: fields.deliveryRegistered === true ? fields.attentionRegistrationVersion ?? CHAT_ATTENTION_REGISTRATION_VERSION : 0,
10494
+ notificationMode: fields.notificationMode,
10495
+ registeredEpochId: fields.deliveryRegistered === true ? manifest.epochId : null
10496
+ });
10497
+ }
10498
+ async function sealOwnChatNotificationPreference(chatPrivateKey, entryId, entry) {
10499
+ const key = notificationPreferenceKey(chatPrivateKey, entryId);
10500
+ try {
10501
+ const manifest = entry?.current?.manifest;
10502
+ if (!manifest)
10503
+ throw new Error("chat notification preference entry required");
10504
+ const preference = normalizeNotificationPreference({
10505
+ v: CHAT_NOTIFICATION_PREFERENCE_VERSION,
10506
+ protocol: CHAT_PROTOCOL_VERSION,
10507
+ notificationMode: entry.notificationMode,
10508
+ registeredEpochId: entry.attentionRegistrationVersion === CHAT_ATTENTION_REGISTRATION_VERSION ? manifest.epochId : null,
10509
+ attentionRegistrationVersion: entry.attentionRegistrationVersion
10510
+ }, entry);
10511
+ const { nonce, ct } = await sealJson(key, preference, notificationPreferenceAad(entryId));
10512
+ return packBodyData(nonce, ct);
10513
+ } finally {
10514
+ cleanBytes(key);
10515
+ }
10516
+ }
10517
+ async function openOwnChatNotificationPreference(chatPrivateKey, entryId, body, entry) {
10518
+ if (!body)
10519
+ return withNotificationPreference(entry);
10520
+ const key = notificationPreferenceKey(chatPrivateKey, entryId);
10521
+ try {
10522
+ const { nonce, ct } = unpackBodyData(body);
10523
+ const preference = normalizeNotificationPreference(await openJson(key, nonce, ct, notificationPreferenceAad(entryId)), entry);
10524
+ return withNotificationPreference(entry, preference);
10525
+ } finally {
10526
+ cleanBytes(key);
10527
+ }
10528
+ }
10529
+ async function openOwnChatRecord(chatPrivateKey, entryId, record) {
10530
+ if (!record?.body)
10531
+ throw new Error("chat owner record required");
10532
+ const entry = await openOwnChatEntry(chatPrivateKey, entryId, record.body);
10533
+ return openOwnChatNotificationPreference(chatPrivateKey, entryId, record.notificationBody, entry);
10190
10534
  }
10191
10535
  function normalizeOwnerEpochRecord(value) {
10192
10536
  if (!value || typeof value !== "object" || Array.isArray(value) || value.v !== CHAT_OWNER_EPOCH_ENTRY_VERSION || value.protocol !== CHAT_PROTOCOL_VERSION) {
@@ -10258,6 +10602,7 @@ async function prepareMutation(identity, entryId, current, update) {
10258
10602
  entryId,
10259
10603
  record: {
10260
10604
  body: await sealOwnChatEntry(identity.chatPrivateKey, entryId, entry),
10605
+ notificationBody: await sealOwnChatNotificationPreference(identity.chatPrivateKey, entryId, entry),
10261
10606
  revision: entry.ownerRevision,
10262
10607
  ...Number.isFinite(prepared.tsMs) ? { tsMs: prepared.tsMs } : {},
10263
10608
  ...prepared.touchTs === true ? { touchTs: true } : {}
@@ -10285,7 +10630,7 @@ async function prepareOwnChatMutation(identity, entryId, current, update) {
10285
10630
  if (!record?.body || !Number.isSafeInteger(record.revision) || record.revision < 1) {
10286
10631
  throw new Error("current chat owner record required");
10287
10632
  }
10288
- const opened = await openOwnChatEntry(identity.chatPrivateKey, entryId, record.body);
10633
+ const opened = await openOwnChatRecord(identity.chatPrivateKey, entryId, record);
10289
10634
  if (opened.ownerRevision !== record.revision) {
10290
10635
  throw new Error("chat owner revision mismatch");
10291
10636
  }
@@ -10321,6 +10666,7 @@ async function retireOwnChatEntry(cloud, identity, entryId, current, fields = {}
10321
10666
  ...latest,
10322
10667
  routes: {},
10323
10668
  deliveryRegistered: false,
10669
+ attentionRegistrationVersion: 0,
10324
10670
  notificationTag: null,
10325
10671
  retirement
10326
10672
  },
@@ -10334,7 +10680,7 @@ async function openOwnChatMutationEntry(identity, entryId, record) {
10334
10680
  return null;
10335
10681
  if (!record?.body || !Number.isSafeInteger(record.revision) || record.revision < 1)
10336
10682
  throw new Error("current chat owner record required");
10337
- const current = await openOwnChatEntry(identity.chatPrivateKey, entryId, record.body);
10683
+ const current = await openOwnChatRecord(identity.chatPrivateKey, entryId, record);
10338
10684
  if (current.ownerRevision !== record.revision) {
10339
10685
  throw new Error("chat owner revision mismatch");
10340
10686
  }
@@ -10439,6 +10785,166 @@ var MAX_REACTIONS = CHAT_MAX_REACTIONS;
10439
10785
  var HOLD_VISIBLE_KEY = "__holdVisible";
10440
10786
  var SOURCE_GONE_VISIBLE_KEY = "__sourceGoneVisible";
10441
10787
 
10788
+ // ../../core/username.js
10789
+ var MAX_USERNAME = USERNAME_MAX_CHARS;
10790
+ var usernameKeyRegex = /^[a-z0-9]$/i;
10791
+ var usernameStripRegex = /[^a-z0-9]/g;
10792
+ var usernameRegex = new RegExp(`^[a-z0-9]{1,${MAX_USERNAME}}$`);
10793
+ function normalizeUsername(value = "") {
10794
+ return String(value).trim().toLowerCase();
10795
+ }
10796
+ function cleanUsername(value = "") {
10797
+ return normalizeUsername(value).replace(usernameStripRegex, "").slice(0, MAX_USERNAME);
10798
+ }
10799
+ function isUsername(value = "") {
10800
+ return usernameRegex.test(value);
10801
+ }
10802
+ function isUsernameKey(value = "") {
10803
+ return usernameKeyRegex.test(value);
10804
+ }
10805
+
10806
+ // ../../core/chat/messages/mentions.js
10807
+ "use client";
10808
+ var CHAT_MENTION_KINDS = Object.freeze({
10809
+ MEMBER: "member",
10810
+ EVERYONE: "everyone"
10811
+ });
10812
+ var CHAT_PK_RE = /^[0-9a-f]{64}$/u;
10813
+ function cleanChatPK(value) {
10814
+ return typeof value === "string" && CHAT_PK_RE.test(value) ? value : "";
10815
+ }
10816
+ function sourceMention(value, allowBinding = false) {
10817
+ const target = allowBinding && value?.target && typeof value.target === "object" ? value.target : value;
10818
+ if (target?.kind === CHAT_MENTION_KINDS.EVERYONE) {
10819
+ return {
10820
+ start: value?.start,
10821
+ end: value?.end,
10822
+ kind: CHAT_MENTION_KINDS.EVERYONE
10823
+ };
10824
+ }
10825
+ if (target?.kind !== CHAT_MENTION_KINDS.MEMBER)
10826
+ return null;
10827
+ return {
10828
+ start: value?.start,
10829
+ end: value?.end,
10830
+ kind: CHAT_MENTION_KINDS.MEMBER,
10831
+ chatPK: target.chatPK
10832
+ };
10833
+ }
10834
+ function mentionToken(text, mention) {
10835
+ if (!Number.isSafeInteger(mention?.start) || !Number.isSafeInteger(mention?.end) || mention.start < 0 || mention.end <= mention.start || mention.end > text.length) {
10836
+ return "";
10837
+ }
10838
+ return text.slice(mention.start, mention.end);
10839
+ }
10840
+ function canonicalMention(text, value, allowBinding = false) {
10841
+ const mention = sourceMention(value, allowBinding);
10842
+ const token = mentionToken(text, mention);
10843
+ if (!token)
10844
+ return null;
10845
+ const before = text[mention.start - 1] || "";
10846
+ const after = text[mention.end] || "";
10847
+ if (before && (before === "@" || isUsernameKey(before)) || after && isUsernameKey(after)) {
10848
+ return null;
10849
+ }
10850
+ if (mention.kind === CHAT_MENTION_KINDS.EVERYONE) {
10851
+ return token === "@everyone" ? mention : null;
10852
+ }
10853
+ if (!cleanChatPK(mention.chatPK) || token[0] !== "@" || !isUsername(token.slice(1))) {
10854
+ return null;
10855
+ }
10856
+ return mention;
10857
+ }
10858
+ function invalidMentions(message, strict) {
10859
+ if (strict)
10860
+ throw new Error(message);
10861
+ return [];
10862
+ }
10863
+ function manifestContext(manifest) {
10864
+ const members = Array.isArray(manifest?.members) ? manifest.members : [];
10865
+ const memberChatPKs = new Set(members.map((member) => cleanChatPK(member?.chatPK)).filter(Boolean));
10866
+ return {
10867
+ group: manifest?.lineage === "group" && members.length > 0,
10868
+ memberCount: members.length,
10869
+ memberChatPKs
10870
+ };
10871
+ }
10872
+ function normalizeMentionList(message, manifest, { strict = false, senderChatPK = "" } = {}) {
10873
+ const raw = message?.mentions;
10874
+ if (raw == null)
10875
+ return [];
10876
+ if (!Array.isArray(raw))
10877
+ return invalidMentions("invalid chat mentions", strict);
10878
+ if (!raw.length)
10879
+ return [];
10880
+ if (message?.t !== "txt" || typeof message?.c !== "string") {
10881
+ return invalidMentions("text message required for mentions", strict);
10882
+ }
10883
+ const context = manifestContext(manifest);
10884
+ if (!context.group)
10885
+ return invalidMentions("group chat required for mentions", strict);
10886
+ const sender = cleanChatPK(senderChatPK || message?.s || message?.from);
10887
+ const mentions = [];
10888
+ const uniqueTargets = new Set;
10889
+ let previousEnd = 0;
10890
+ for (const value of raw) {
10891
+ const mention = canonicalMention(message.c, value);
10892
+ if (!mention || mention.start < previousEnd) {
10893
+ if (strict)
10894
+ throw new Error("invalid chat mention");
10895
+ continue;
10896
+ }
10897
+ if (mention.kind === CHAT_MENTION_KINDS.EVERYONE) {
10898
+ if (context.memberCount > CHAT_ORDINARY_DELIVERY_MAX_MEMBERS) {
10899
+ if (strict)
10900
+ throw new Error("everyone mentions are unavailable in large groups");
10901
+ continue;
10902
+ }
10903
+ } else {
10904
+ if (!context.memberChatPKs.has(mention.chatPK) || mention.chatPK === sender) {
10905
+ if (strict)
10906
+ throw new Error("mention target must be a current chat member");
10907
+ continue;
10908
+ }
10909
+ if (context.memberCount > CHAT_ORDINARY_DELIVERY_MAX_MEMBERS && !uniqueTargets.has(mention.chatPK) && uniqueTargets.size >= CHAT_LARGE_GROUP_MENTION_MAX_TARGETS) {
10910
+ if (strict)
10911
+ throw new Error(`large group mentions support up to ${CHAT_LARGE_GROUP_MENTION_MAX_TARGETS} members`);
10912
+ continue;
10913
+ }
10914
+ uniqueTargets.add(mention.chatPK);
10915
+ }
10916
+ mentions.push(mention);
10917
+ previousEnd = mention.end;
10918
+ }
10919
+ return mentions;
10920
+ }
10921
+ function normalizeChatMessageMentions(message, manifest, options = {}) {
10922
+ if (!message || typeof message !== "object" || Array.isArray(message))
10923
+ return message;
10924
+ if (message.t !== "txt") {
10925
+ const { mentions: raw, ...rest } = message;
10926
+ if (options.strict && raw != null) {
10927
+ throw new Error("text message required for mentions");
10928
+ }
10929
+ return rest;
10930
+ }
10931
+ return {
10932
+ ...message,
10933
+ mentions: normalizeMentionList(message, manifest, options)
10934
+ };
10935
+ }
10936
+ function hasChatMessageMentions(message) {
10937
+ return Array.isArray(message?.mentions) && message.mentions.length > 0;
10938
+ }
10939
+ function chatMessageMentionTargets(message, manifest, options = {}) {
10940
+ const normalized = normalizeChatMessageMentions(message, manifest, { ...options, strict: true });
10941
+ const sender = cleanChatPK(options.senderChatPK || normalized?.s || normalized?.from);
10942
+ const memberChatPKs = [...new Set(normalized.mentions.filter((mention) => mention.kind === CHAT_MENTION_KINDS.MEMBER).map((mention) => mention.chatPK))].sort();
10943
+ const everyone = normalized.mentions.some((mention) => mention.kind === CHAT_MENTION_KINDS.EVERYONE);
10944
+ const recipientChatPKs = everyone ? [...new Set((manifest?.members || []).map((member) => cleanChatPK(member?.chatPK)).filter((chatPK) => chatPK && chatPK !== sender))].sort() : memberChatPKs;
10945
+ return { everyone, memberChatPKs, recipientChatPKs };
10946
+ }
10947
+
10442
10948
  // ../../core/chat/messages/text.js
10443
10949
  var DOMAIN_LABEL_PATTERN = "[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?";
10444
10950
  var DOMAIN_TLD_PATTERN = "(?:[a-z]{2,63}|xn--[a-z0-9-]{2,59})";
@@ -12287,7 +12793,6 @@ function getDisplayMessages(messages, selfChatPublicKey, peerChatPublicKey, opti
12287
12793
 
12288
12794
  // ../../core/chat/messages/compact.js
12289
12795
  "use client";
12290
- var pendingCompactKeys = new Set;
12291
12796
  function ownsMessageCompaction(memberChatPKs, chatPK) {
12292
12797
  const members = [...new Set((memberChatPKs || []).filter(Boolean))].sort();
12293
12798
  return !!chatPK && members[0] === chatPK;
@@ -12363,39 +12868,45 @@ function compactKey(chatId, message) {
12363
12868
  const id = cleanText(message?.id);
12364
12869
  return chatId && id ? `${chatId}/${id}` : "";
12365
12870
  }
12366
- function claimMessages(chatId, messages) {
12871
+ function claimMessages(maintenance, chatId, messages) {
12367
12872
  const claimed = [];
12368
12873
  for (const message of messages || []) {
12369
12874
  const key = compactKey(chatId, message);
12370
- if (!key || pendingCompactKeys.has(key))
12875
+ if (!key)
12876
+ continue;
12877
+ const lease = maintenance.claim("message-compaction", key);
12878
+ if (!lease)
12371
12879
  continue;
12372
- pendingCompactKeys.add(key);
12373
- claimed.push(message);
12880
+ claimed.push({ lease, message });
12374
12881
  }
12375
12882
  return claimed;
12376
12883
  }
12377
- function releaseMessages(chatId, messages) {
12378
- for (const message of messages || []) {
12379
- const key = compactKey(chatId, message);
12380
- if (key)
12381
- pendingCompactKeys.delete(key);
12382
- }
12884
+ function releaseMessages(claimed) {
12885
+ for (const item of claimed || [])
12886
+ item.lease.release();
12383
12887
  }
12384
- async function compactMessages({ chatId, messages, deletedKeys, protectedKeys, deleteMessages, scannedKeys, scanComplete }) {
12888
+ async function compactMessages({ chatId, maintenance, messages, deletedKeys, protectedKeys, deleteMessages, scannedKeys, scanComplete }) {
12385
12889
  if (!chatId || typeof deleteMessages !== "function")
12386
12890
  return [];
12387
- const targets = claimMessages(chatId, getCompactMessages(messages, { deletedKeys, protectedKeys, scannedKeys, scanComplete }));
12388
- if (!targets.length)
12891
+ if (!maintenance?.claim)
12892
+ throw new Error("account message maintenance required");
12893
+ const claimed = claimMessages(maintenance, chatId, getCompactMessages(messages, { deletedKeys, protectedKeys, scannedKeys, scanComplete }));
12894
+ if (!claimed.length)
12389
12895
  return [];
12896
+ const targets = claimed.map((item) => item.message);
12390
12897
  try {
12898
+ for (const item of claimed)
12899
+ item.lease.assertCurrent();
12391
12900
  await deleteMessages(targets);
12901
+ for (const item of claimed)
12902
+ item.lease.assertCurrent();
12392
12903
  if (deletedKeys?.add) {
12393
12904
  for (const message of targets)
12394
12905
  addMessageKeys(deletedKeys, message);
12395
12906
  }
12396
12907
  return targets;
12397
12908
  } finally {
12398
- releaseMessages(chatId, targets);
12909
+ releaseMessages(claimed);
12399
12910
  }
12400
12911
  }
12401
12912
 
@@ -12927,20 +13438,23 @@ function ownerPreview(senderChatPK, message, messageId, head, tsMs, ttlMs) {
12927
13438
  }
12928
13439
  async function ensureDeliveryRoute(cloud, identity, epochState, entry, shouldPing) {
12929
13440
  if (!shouldPing)
12930
- return { capability: "", registered: entry?.deliveryRegistered === true };
13441
+ return { capability: "", registered: hasCurrentChatAttentionRegistration(entry) };
12931
13442
  if (!identity.notificationPK) {
12932
13443
  throw new Error("notification identity required");
12933
13444
  }
12934
- const capability = deriveChatDeliveryCapability(epochState.stateCapability, {
13445
+ const registration = chatDeliveryRegistration(epochState.stateCapability, {
13446
+ attentionSecret: epochState.epochSecret,
12935
13447
  chatId: epochState.manifest.chatId,
12936
13448
  recipientChatPK: identity.chatPK,
12937
- generation: epochState.manifest.epochVersion
13449
+ generation: epochState.manifest.epochVersion,
13450
+ manifest: epochState.manifest,
13451
+ notificationMode: entry?.notificationMode
12938
13452
  });
12939
- if (entry?.deliveryRegistered === true) {
12940
- return { capability, registered: true };
13453
+ if (hasCurrentChatAttentionRegistration(entry)) {
13454
+ return { capability: registration.capability, registered: true };
12941
13455
  }
12942
- const registered = await cloud.delivery?.register?.(capability).then(() => true, () => false) || false;
12943
- return { capability: registered ? capability : "", registered };
13456
+ const registered = await cloud.delivery?.register?.(registration).then(() => true, () => false) || false;
13457
+ return { capability: registered ? registration.capability : "", registered };
12944
13458
  }
12945
13459
  function mergedRoutes(entry, options, epochState) {
12946
13460
  const routes = {
@@ -12954,6 +13468,7 @@ function mergedRoutes(entry, options, epochState) {
12954
13468
  ...routes[member.chatPK] || {},
12955
13469
  uid: member.uid,
12956
13470
  notificationPK: member.notificationPK,
13471
+ admissionCapability: routes[member.chatPK]?.admissionCapability || null,
12957
13472
  deliveryCapability: deriveChatDeliveryCapability(epochState.stateCapability, {
12958
13473
  chatId: manifest.chatId,
12959
13474
  recipientChatPK: member.chatPK,
@@ -12972,6 +13487,7 @@ async function makeOwnerEntryMutation(identity, epochState, existing, routes, fi
12972
13487
  ...existing,
12973
13488
  routes,
12974
13489
  deliveryRegistered: fields.deliveryRegistered || existing.deliveryRegistered,
13490
+ attentionRegistrationVersion: fields.deliveryRegistered ? CHAT_ATTENTION_REGISTRATION_VERSION : existing.attentionRegistrationVersion,
12975
13491
  notificationTag: notificationChatTag(epochState.stateCapability, chatId, identity.chatPK),
12976
13492
  ...fields.settings ? {
12977
13493
  current: { ...existing.current, settings: normalizeChatSettingsProjection(fields.settings) }
@@ -12995,12 +13511,13 @@ async function makeOwnerEntryMutation(identity, epochState, existing, routes, fi
12995
13511
  ...current,
12996
13512
  routes: { ...current.routes, ...routes },
12997
13513
  deliveryRegistered: fields.deliveryRegistered || current.deliveryRegistered,
13514
+ attentionRegistrationVersion: fields.deliveryRegistered ? CHAT_ATTENTION_REGISTRATION_VERSION : current.attentionRegistrationVersion,
12998
13515
  notificationTag: notificationChatTag(epochState.stateCapability, chatId, identity.chatPK),
12999
13516
  ...fields.settings ? {
13000
13517
  current: { ...current.current, settings: normalizeChatSettingsProjection(fields.settings) }
13001
13518
  } : {}
13002
13519
  };
13003
- const unchanged = JSON.stringify(next.routes) === JSON.stringify(current.routes) && next.deliveryRegistered === current.deliveryRegistered && next.notificationTag === current.notificationTag && next.current.settings.digest === current.current.settings.digest;
13520
+ const unchanged = JSON.stringify(next.routes) === JSON.stringify(current.routes) && next.deliveryRegistered === current.deliveryRegistered && next.attentionRegistrationVersion === current.attentionRegistrationVersion && next.notificationTag === current.notificationTag && next.current.settings.digest === current.current.settings.digest;
13004
13521
  return unchanged ? { result: current } : {
13005
13522
  entry: next,
13006
13523
  ...Number.isFinite(fields.tsMs) ? { tsMs: fields.tsMs } : {}
@@ -13040,16 +13557,20 @@ async function makeChatRecipientDeliveries(identity, epochState, routes, message
13040
13557
  if (!fields.shouldPing || fields.deliverRecipients === false)
13041
13558
  return [];
13042
13559
  const capabilities = chatEpochCapabilities(epochState);
13043
- const targetedRecipient = (fields.recipientChatPKs || []).map(cleanText).find(Boolean) || "";
13044
- const critical = fields.deliveryClass === "critical";
13045
- if (!critical && !capabilities.ordinaryDelivery && !targetedRecipient)
13560
+ const currentMemberChatPKs = new Set(epochState.manifest.members.map((member) => member.chatPK));
13561
+ const targetedRecipients = new Set((fields.recipientChatPKs || []).map((value) => cleanText(value).toLowerCase()).filter((chatPK) => chatPK !== identity.chatPK && currentMemberChatPKs.has(chatPK)));
13562
+ const attentionRecipients = new Set((fields.attentionRecipientChatPKs || []).map((value) => cleanText(value).toLowerCase()).filter((chatPK) => chatPK !== identity.chatPK && currentMemberChatPKs.has(chatPK)));
13563
+ const rotation = fields.deliveryClass === "rotation";
13564
+ if (!rotation && !capabilities.ordinaryDelivery && !targetedRecipients.size)
13046
13565
  return [];
13047
13566
  const deliveries = [];
13048
13567
  for (const member of epochState.manifest.members) {
13049
13568
  if (member.chatPK === identity.chatPK)
13050
13569
  continue;
13051
- if (!critical && !capabilities.ordinaryDelivery && member.chatPK !== targetedRecipient)
13570
+ if (!rotation && !capabilities.ordinaryDelivery && !targetedRecipients.has(member.chatPK))
13052
13571
  continue;
13572
+ const attentionKind = rotation ? CHAT_ATTENTION_KINDS.ROTATION : attentionRecipients.has(member.chatPK) ? CHAT_ATTENTION_KINDS.MENTION : fields.silent === true || !capabilities.ordinaryDelivery ? CHAT_ATTENTION_KINDS.SILENT : CHAT_ATTENTION_KINDS.GENERAL;
13573
+ const attentionPriority = attentionKind === CHAT_ATTENTION_KINDS.ROTATION ? 3 : attentionKind === CHAT_ATTENTION_KINDS.MENTION ? 2 : attentionKind === CHAT_ATTENTION_KINDS.GENERAL ? 1 : 0;
13053
13574
  const route = routes[member.chatPK] || {};
13054
13575
  const descriptor = await sealNotificationDescriptor(member.notificationPK, {
13055
13576
  peerTag: notificationChatTag(epochState.stateCapability, epochState.manifest.chatId, member.chatPK),
@@ -13069,7 +13590,10 @@ async function makeChatRecipientDeliveries(identity, epochState, routes, message
13069
13590
  });
13070
13591
  deliveries.push({
13071
13592
  recipientChatPK: member.chatPK,
13072
- ...route.deliveryCapability ? { capability: route.deliveryCapability } : { recipientUid: member.uid },
13593
+ ...route.deliveryCapability ? { capability: route.deliveryCapability } : {
13594
+ recipientUid: member.uid,
13595
+ admissionCapability: route.admissionCapability || null
13596
+ },
13073
13597
  deliveryId: deriveChatInboxDeliveryId(epochState.stateCapability, {
13074
13598
  chatId: epochState.manifest.chatId,
13075
13599
  recipientChatPK: member.chatPK,
@@ -13078,7 +13602,14 @@ async function makeChatRecipientDeliveries(identity, epochState, routes, message
13078
13602
  ping,
13079
13603
  descriptor,
13080
13604
  routeTag: notificationRouteTag(descriptor),
13081
- notify: fields.notify
13605
+ attentionProof: deriveChatAttentionCapability(epochState.epochSecret, {
13606
+ chatId: epochState.manifest.chatId,
13607
+ recipientChatPK: member.chatPK,
13608
+ generation: epochState.manifest.epochVersion,
13609
+ kind: attentionKind
13610
+ }),
13611
+ attentionPriority,
13612
+ notify: attentionKind !== CHAT_ATTENTION_KINDS.SILENT
13082
13613
  });
13083
13614
  }
13084
13615
  return deliveries;
@@ -13096,9 +13627,14 @@ async function prepareMsgRecord(identity, epochState, message, options = {}) {
13096
13627
  }
13097
13628
  const chatRetention = cleanChatRetention(options.retention ?? epochState.settings.values.retention);
13098
13629
  const tsMs = Date.now();
13099
- const messagePayload = withMessageRetention(message, chatRetention);
13630
+ const retainedMessage = withMessageRetention(message, chatRetention);
13631
+ const messagePayload = normalizeChatMessageMentions(retainedMessage, epochState.manifest, {
13632
+ strict: true,
13633
+ senderChatPK: identity.chatPK
13634
+ });
13100
13635
  const retention = getMessageRetention(messagePayload, chatRetention);
13101
13636
  const actionOp = options.actionOp || actionOpForPayload(messagePayload);
13637
+ const mentionTargets = actionOp === CHAT_ACTION_OPS.CREATE && messagePayload?.t === "txt" ? chatMessageMentionTargets(messagePayload, epochState.manifest, { senderChatPK: identity.chatPK }) : { recipientChatPKs: [] };
13102
13638
  const { head, body } = await sealMsg(epoch, messagePayload, {
13103
13639
  op: actionOp,
13104
13640
  target: options.actionTarget,
@@ -13113,6 +13649,7 @@ async function prepareMsgRecord(identity, epochState, message, options = {}) {
13113
13649
  cid: head.cid,
13114
13650
  record: { lane: epoch.messageLane, head, body, ttlMs },
13115
13651
  message: ownerPreview(identity.chatPK, messagePayload, messageId, head, tsMs, ttlMs),
13652
+ mentionRecipientChatPKs: mentionTargets.recipientChatPKs,
13116
13653
  tsMs
13117
13654
  };
13118
13655
  } finally {
@@ -13145,6 +13682,7 @@ async function sendMsg(cloud, senderChatPK, senderPrivateKey, _receiverChatPK, m
13145
13682
  msgId: messageId,
13146
13683
  record,
13147
13684
  message: sentMessage,
13685
+ mentionRecipientChatPKs,
13148
13686
  tsMs
13149
13687
  } = prepared;
13150
13688
  const updatePreview = options.updatePreview !== false;
@@ -13153,7 +13691,7 @@ async function sendMsg(cloud, senderChatPK, senderPrivateKey, _receiverChatPK, m
13153
13691
  const routes = mergedRoutes(options.ownEntry, options, epochState);
13154
13692
  const preview = updatePreview ? sentMessage : null;
13155
13693
  const settingsProjection = options.settingsProjection ? normalizeChatSettingsProjection(options.settingsProjection) : null;
13156
- const ownerStateChanged = !options.ownEntry || settingsProjection || shouldPing && routeState.registered && options.ownEntry?.deliveryRegistered !== true;
13694
+ const ownerStateChanged = !options.ownEntry || settingsProjection || shouldPing && routeState.registered && !hasCurrentChatAttentionRegistration(options.ownEntry);
13157
13695
  const ownerMutation = ownerStateChanged ? await makeOwnerEntryMutation(identity, epochState, options.ownEntry, routes, {
13158
13696
  settings: settingsProjection,
13159
13697
  immediate: !!settingsProjection || options.ownerImmediate === true,
@@ -13166,13 +13704,18 @@ async function sendMsg(cloud, senderChatPK, senderPrivateKey, _receiverChatPK, m
13166
13704
  kind: pingKind,
13167
13705
  tsMs
13168
13706
  });
13707
+ const deliveryRecipientChatPKs = [
13708
+ ...options.deliveryRecipientChatPKs || [],
13709
+ ...mentionRecipientChatPKs
13710
+ ];
13169
13711
  const deliveries = await makeChatRecipientDeliveries(identity, epochState, routes, messageId, {
13170
13712
  shouldPing,
13171
13713
  deliverRecipients: options.deliverRecipients !== false,
13172
13714
  kind: pingKind,
13173
13715
  ownDeliveryCapability: routeState.capability,
13174
- notify: options.notify !== false,
13175
- recipientChatPKs: options.deliveryRecipientChatPKs,
13716
+ silent: options.notify === false,
13717
+ recipientChatPKs: deliveryRecipientChatPKs,
13718
+ attentionRecipientChatPKs: mentionRecipientChatPKs,
13176
13719
  tsMs
13177
13720
  });
13178
13721
  const write = await cloud.chat.messages.send({
@@ -13794,14 +14337,19 @@ function normalizeDelivery(value) {
13794
14337
  throw new Error("invalid chat membership delivery mode");
13795
14338
  const recipientUid = cleanText(value.recipientUid);
13796
14339
  const capability = cleanText(value.capability).toLowerCase();
14340
+ const admissionCapability = cleanText(value.admissionCapability).toLowerCase();
13797
14341
  if (mode === "critical" && !recipientUid || mode === "established" && !/^[0-9a-f]{64}$/u.test(capability)) {
13798
14342
  throw new Error("invalid chat membership delivery target");
13799
14343
  }
14344
+ if (admissionCapability && !/^[0-9a-f]{64}$/u.test(admissionCapability)) {
14345
+ throw new Error("invalid chat admission capability");
14346
+ }
13800
14347
  return {
13801
14348
  mode,
13802
14349
  recipientUid,
13803
14350
  recipientChatPK: cleanChatHex(value.recipientChatPK, "membership recipient"),
13804
14351
  capability,
14352
+ admissionCapability,
13805
14353
  deliveryId: cleanChatHex(value.deliveryId, "membership delivery id"),
13806
14354
  ping: openPing(value.ping),
13807
14355
  descriptor: openDescriptor(value.descriptor),
@@ -14012,9 +14560,11 @@ function successorManifest(parent, members, epochId) {
14012
14560
  members
14013
14561
  }, { parent, official: true });
14014
14562
  }
14015
- function nextRoutes(entry, manifest, stateCapability) {
14563
+ function nextRoutes(entry, manifest, stateCapability, privateMembers = []) {
14564
+ const privateByChatPK = new Map(privateMembers.map((member) => [member.chatPK, member]));
14016
14565
  return Object.fromEntries(manifest.members.map((member) => {
14017
14566
  const current = entry.routes?.[member.chatPK] || {};
14567
+ const privateMember = privateByChatPK.get(member.chatPK) || {};
14018
14568
  return [member.chatPK, {
14019
14569
  uid: member.uid,
14020
14570
  deliveryCapability: deriveChatDeliveryCapability(stateCapability, {
@@ -14023,6 +14573,7 @@ function nextRoutes(entry, manifest, stateCapability) {
14023
14573
  generation: manifest.epochVersion
14024
14574
  }),
14025
14575
  notificationPK: member.notificationPK || current.notificationPK || null,
14576
+ admissionCapability: privateMember.admissionCapability || current.admissionCapability || null,
14026
14577
  generation: manifest.epochVersion
14027
14578
  }];
14028
14579
  }));
@@ -14071,7 +14622,11 @@ async function makeMlsWelcomeDeliveries(identity, nextState2, addedMembers, opti
14071
14622
  senderUid: identity.uid,
14072
14623
  messageId,
14073
14624
  transitionMessageId: nextState2.transitionMessageId,
14074
- deliveryCapability: null,
14625
+ deliveryCapability: deriveChatDeliveryCapability(nextState2.stateCapability, {
14626
+ chatId: nextState2.manifest.chatId,
14627
+ recipientChatPK: identity.chatPK,
14628
+ generation: nextState2.manifest.epochVersion
14629
+ }),
14075
14630
  notificationPK: identity.notificationPK,
14076
14631
  transitionCommitment: nextState2.transitionCommitment,
14077
14632
  settingsId,
@@ -14092,6 +14647,7 @@ async function makeMlsWelcomeDeliveries(identity, nextState2, addedMembers, opti
14092
14647
  ping,
14093
14648
  descriptor,
14094
14649
  routeTag,
14650
+ admissionCapability: member.admissionCapability || null,
14095
14651
  member
14096
14652
  });
14097
14653
  }
@@ -14106,7 +14662,7 @@ async function makeTransitionWakes(identity, parentState, entry, messageId, tsMs
14106
14662
  shouldPing: true,
14107
14663
  deliverRecipients: true,
14108
14664
  kind: "transition",
14109
- deliveryClass: "critical",
14665
+ deliveryClass: "rotation",
14110
14666
  ownDeliveryCapability: "",
14111
14667
  notify: true,
14112
14668
  tsMs
@@ -14118,14 +14674,18 @@ async function makeTransitionWakes(identity, parentState, entry, messageId, tsMs
14118
14674
  }
14119
14675
  async function deliverMembershipRecord(cloud, delivery) {
14120
14676
  if (delivery.mode === "established") {
14677
+ if (!/^[0-9a-f]{64}$/u.test(delivery.attentionProof || "")) {
14678
+ throw new Error("chat membership attention proof required");
14679
+ }
14121
14680
  await cloud.inbox.deliver(delivery.capability, delivery.ping, {
14681
+ attentionProof: delivery.attentionProof,
14122
14682
  descriptor: delivery.descriptor,
14123
- routeTag: delivery.routeTag,
14124
- notify: true
14683
+ routeTag: delivery.routeTag
14125
14684
  });
14126
14685
  return;
14127
14686
  }
14128
14687
  await pushCriticalInbox(cloud, delivery.recipientUid || delivery.member?.uid, delivery.ping, {
14688
+ admissionCapability: delivery.admissionCapability || null,
14129
14689
  deliveryId: delivery.deliveryId,
14130
14690
  descriptor: delivery.descriptor,
14131
14691
  routeTag: delivery.routeTag,
@@ -14155,6 +14715,46 @@ async function deliverQueuedMembership(cloud, identity, deliveries) {
14155
14715
  deliveredMembers: delivered.map((delivery) => delivery.member).filter(Boolean)
14156
14716
  };
14157
14717
  }
14718
+ async function recoverMembershipAttentionSecret(cloud, identity, record, mlsPackage) {
14719
+ const parentEpochVersion = Number(mlsPackage?.head?.epochVersion) - 1;
14720
+ if (!Number.isSafeInteger(parentEpochVersion) || parentEpochVersion < 1) {
14721
+ throw new Error("chat membership parent epoch required");
14722
+ }
14723
+ const entryId = ownChatEntryId(identity.chatPrivateKey, record.chatId);
14724
+ let afterEpoch = null;
14725
+ while (true) {
14726
+ const page = await cloud.user.chats.epochs.list(identity.uid, entryId, {
14727
+ count: 50,
14728
+ ...afterEpoch ? { afterEpoch } : {}
14729
+ });
14730
+ for (const epochRecord of page?.records || []) {
14731
+ const epoch = await openOwnerEpochRecord(identity.chatPrivateKey, entryId, epochRecord.id, epochRecord.body).catch(() => null);
14732
+ if (epoch?.epochVersion === parentEpochVersion && epoch.successorTransitionCommitment === mlsPackage.head.transitionCommitment) {
14733
+ return epoch.epochSecret;
14734
+ }
14735
+ }
14736
+ afterEpoch = page?.nextAfterEpoch ?? null;
14737
+ if (!page?.hasMore || !afterEpoch)
14738
+ break;
14739
+ }
14740
+ throw new Error("chat membership parent attention secret unavailable");
14741
+ }
14742
+ async function recoverMembershipAttentionProofs(cloud, identity, record, mlsPackage) {
14743
+ if (!record.deliveries.some((delivery) => delivery.mode === "established")) {
14744
+ return record.deliveries;
14745
+ }
14746
+ const attentionSecret = await recoverMembershipAttentionSecret(cloud, identity, record, mlsPackage);
14747
+ const generation = mlsPackage.head.epochVersion - 1;
14748
+ return record.deliveries.map((delivery) => delivery.mode === "established" ? {
14749
+ ...delivery,
14750
+ attentionProof: deriveChatAttentionCapability(attentionSecret, {
14751
+ chatId: record.chatId,
14752
+ recipientChatPK: delivery.recipientChatPK,
14753
+ generation,
14754
+ kind: CHAT_ATTENTION_KINDS.ROTATION
14755
+ })
14756
+ } : delivery);
14757
+ }
14158
14758
  async function drainChatMembershipOutbox(cloud, identity, options = {}) {
14159
14759
  const records = await cloud.user.chats.membershipOutbox.list(identity.uid, { count: options.count || 20 });
14160
14760
  const committedPackages = new Map;
@@ -14167,13 +14767,15 @@ async function drainChatMembershipOutbox(cloud, identity, options = {}) {
14167
14767
  }
14168
14768
  const packageKey = `${opened.chatId}:${opened.packageId}:${opened.packageDigest}`;
14169
14769
  if (!committedPackages.has(packageKey)) {
14170
- const mlsPackage = await cloud.chat.mls.packages.read(opened.chatId, opened.packageId).catch(() => null);
14171
- committedPackages.set(packageKey, mlsPackage?.digest === opened.packageDigest);
14770
+ const mlsPackage2 = await cloud.chat.mls.packages.read(opened.chatId, opened.packageId).catch(() => null);
14771
+ committedPackages.set(packageKey, mlsPackage2?.digest === opened.packageDigest ? mlsPackage2 : null);
14172
14772
  }
14173
- if (!committedPackages.get(packageKey))
14773
+ const mlsPackage = committedPackages.get(packageKey);
14774
+ if (!mlsPackage)
14174
14775
  continue;
14175
14776
  try {
14176
- const results = await Promise.allSettled(opened.deliveries.map((delivery) => deliverMembershipRecord(cloud, delivery)));
14777
+ const deliveries = await recoverMembershipAttentionProofs(cloud, identity, opened, mlsPackage);
14778
+ const results = await Promise.allSettled(deliveries.map((delivery) => deliverMembershipRecord(cloud, delivery)));
14177
14779
  delivered += results.filter((result) => result.status === "fulfilled").length;
14178
14780
  if (results.every((result) => result.status === "fulfilled")) {
14179
14781
  await cloud.user.chats.membershipOutbox.delete(identity.uid, record.id);
@@ -14305,13 +14907,6 @@ async function prepareMembershipTransition(context) {
14305
14907
  });
14306
14908
  if (transitionMessage.msgId !== transitionMessageId)
14307
14909
  throw new Error("chat transition message id mismatch");
14308
- runtime.ownDeliveryCapability = deriveChatDeliveryCapability(runtime.stateCapability, {
14309
- chatId: parent.chatId,
14310
- recipientChatPK: identity.chatPK,
14311
- generation: next.epochVersion
14312
- });
14313
- runtime.stage = "register-delivery";
14314
- await cloud.delivery.register(runtime.ownDeliveryCapability);
14315
14910
  runtime.stage = "prepare-deliveries";
14316
14911
  const [wakeDeliveries, welcomeDeliveries] = await Promise.all([
14317
14912
  makeTransitionWakes(identity, parentState, entry, transitionMessage.msgId, transitionMessage.tsMs),
@@ -14338,16 +14933,26 @@ async function prepareMembershipTransition(context) {
14338
14933
  if (current.current.manifest.epochId !== parent.epochId || current.current.manifest.epochVersion !== parent.epochVersion) {
14339
14934
  throw new Error("chat owner epoch changed");
14340
14935
  }
14936
+ const notificationMode = normalizeChatNotificationMode(current.notificationMode, next);
14341
14937
  const nextEntry = makeOwnChatEntry(candidateState, {
14342
14938
  ownerRevision: current.ownerRevision,
14343
- routes: nextRoutes(current, next, runtime.stateCapability),
14939
+ routes: nextRoutes(current, next, runtime.stateCapability, delta.members),
14344
14940
  saved: current.saved,
14345
14941
  startMs: current.startMs,
14346
14942
  deliveryRegistered: true,
14943
+ notificationMode,
14347
14944
  notificationTag: notificationChatTag(runtime.stateCapability, parent.chatId, identity.chatPK)
14348
14945
  });
14349
14946
  return {
14350
14947
  entry: nextEntry,
14948
+ deliveryRegistration: chatDeliveryRegistration(runtime.stateCapability, {
14949
+ attentionSecret: runtime.epochSecret,
14950
+ chatId: parent.chatId,
14951
+ recipientChatPK: identity.chatPK,
14952
+ generation: next.epochVersion,
14953
+ manifest: next,
14954
+ notificationMode
14955
+ }),
14351
14956
  ...Number.isFinite(fields.ownerTsMs) ? { tsMs: fields.ownerTsMs } : { touchTs: true },
14352
14957
  epochs: [{ id: historyId, record: historyRecord }],
14353
14958
  mlsWrites: [{ id: ownerMlsState.stateId, value: ownerMlsState.value }],
@@ -14490,7 +15095,6 @@ async function transitionChatEpoch(cloud, identityValue, entry, fields = {}) {
14490
15095
  epochSecret: null,
14491
15096
  metrics: {},
14492
15097
  nextSnapshot: null,
14493
- ownDeliveryCapability: "",
14494
15098
  parentSnapshot: null,
14495
15099
  stage: "read-owner-mls",
14496
15100
  stateCapability: null
@@ -14518,9 +15122,6 @@ async function transitionChatEpoch(cloud, identityValue, entry, fields = {}) {
14518
15122
  });
14519
15123
  throw error;
14520
15124
  } finally {
14521
- if (runtime.ownDeliveryCapability && !runtime.committed) {
14522
- await cloud.delivery.revoke(runtime.ownDeliveryCapability).catch(() => false);
14523
- }
14524
15125
  cleanBytes(runtime.parentSnapshot, runtime.nextSnapshot, runtime.epochSecret, runtime.stateCapability);
14525
15126
  }
14526
15127
  }
@@ -14913,7 +15514,7 @@ function sortedUniqueValues(items) {
14913
15514
 
14914
15515
  // ../../core/chat/chats.js
14915
15516
  "use client";
14916
- var HEX_32_RE3 = /^[0-9a-f]{32}$/i;
15517
+ var HEX_32_RE4 = /^[0-9a-f]{32}$/i;
14917
15518
  var HEX_64_RE = /^[0-9a-f]{64}$/i;
14918
15519
  function sameSigningKeys(left, right) {
14919
15520
  if (left === right)
@@ -14937,7 +15538,7 @@ function sameMembers(left, right) {
14937
15538
  function sameChatShape(a, b) {
14938
15539
  if (!a || !b)
14939
15540
  return a === b;
14940
- return a.id === b.id && a.protocol === b.protocol && a.entryId === b.entryId && a.epochId === b.epochId && a.epochVersion === b.epochVersion && a.lineage === b.lineage && a.memberCount === b.memberCount && sameMembers(a.members, b.members) && a.peerChatPK === b.peerChatPK && a.peerUid === b.peerUid && sameSigningKeys(a.signingKeysByChatKey, b.signingKeysByChatKey) && a.signerShared === b.signerShared && a.deliveryRegistered === b.deliveryRegistered && a.peerDeliveryCapability === b.peerDeliveryCapability && a.peerNotificationPK === b.peerNotificationPK && a.notificationTag === b.notificationTag && a.ts === b.ts && a.startMs === b.startMs && a.readMs === b.readMs && a.inboxMessageId === b.inboxMessageId && a.inboxMessageAt === b.inboxMessageAt && a.inboxMessage === b.inboxMessage && a.unseen === b.unseen && a.settings?.retention === b.settings?.retention && a.settings?.title === b.settings?.title && a.settings?.avatarRef === b.settings?.avatarRef && a.membershipRemoved === true === (b.membershipRemoved === true) && sameChatPreview(a.preview, b.preview);
15541
+ return a.id === b.id && a.protocol === b.protocol && a.entryId === b.entryId && a.epochId === b.epochId && a.epochVersion === b.epochVersion && a.lineage === b.lineage && a.memberCount === b.memberCount && sameMembers(a.members, b.members) && a.peerChatPK === b.peerChatPK && a.peerUid === b.peerUid && sameSigningKeys(a.signingKeysByChatKey, b.signingKeysByChatKey) && a.signerShared === b.signerShared && a.deliveryRegistered === b.deliveryRegistered && a.attentionRegistrationVersion === b.attentionRegistrationVersion && a.notificationMode === b.notificationMode && a.peerDeliveryCapability === b.peerDeliveryCapability && a.peerNotificationPK === b.peerNotificationPK && a.notificationTag === b.notificationTag && a.ts === b.ts && a.startMs === b.startMs && a.readMs === b.readMs && a.inboxMessageId === b.inboxMessageId && a.inboxMessageAt === b.inboxMessageAt && a.inboxMessage === b.inboxMessage && a.unseen === b.unseen && a.settings?.retention === b.settings?.retention && a.settings?.title === b.settings?.title && a.settings?.avatarRef === b.settings?.avatarRef && a.membershipRemoved === true === (b.membershipRemoved === true) && sameChatPreview(a.preview, b.preview);
14941
15542
  }
14942
15543
  function sameChats(prev, next) {
14943
15544
  if (prev.length !== next.length)
@@ -15011,7 +15612,7 @@ function chatVersionKey(chat) {
15011
15612
  return "";
15012
15613
  }
15013
15614
  function isHex32(value) {
15014
- return HEX_32_RE3.test(cleanText(value));
15615
+ return HEX_32_RE4.test(cleanText(value));
15015
15616
  }
15016
15617
  function isHex64(value) {
15017
15618
  return HEX_64_RE.test(cleanText(value));
@@ -16067,7 +16668,9 @@ function serializeChat(chat) {
16067
16668
  ts: timestampMs(chat.ts, null, { positive: true }) || 0,
16068
16669
  lastUsedAt: Date.now(),
16069
16670
  unseen: !!chat.unseen,
16070
- membershipRemoved: chat.membershipRemoved === true
16671
+ membershipRemoved: chat.membershipRemoved === true,
16672
+ attentionRegistrationVersion: Number(chat.attentionRegistrationVersion) || 0,
16673
+ notificationMode: chat.notificationMode || null
16071
16674
  };
16072
16675
  }
16073
16676
  function reviveChat(chat) {
@@ -16098,7 +16701,9 @@ function reviveChat(chat) {
16098
16701
  ts: timestampMs(chat.ts, null, { positive: true }) || 0,
16099
16702
  lastUsedAt: timestampMs(chat.lastUsedAt, null, { positive: true }) || 0,
16100
16703
  unseen: !!chat.unseen,
16101
- membershipRemoved: chat.membershipRemoved === true
16704
+ membershipRemoved: chat.membershipRemoved === true,
16705
+ attentionRegistrationVersion: Number(chat.attentionRegistrationVersion) || 0,
16706
+ notificationMode: chat.notificationMode || null
16102
16707
  };
16103
16708
  }
16104
16709
  function isReadableCachedChat(chat) {
@@ -17501,7 +18106,7 @@ function createChatLive({
17501
18106
  return sendSnapshot(entry);
17502
18107
  }
17503
18108
  if (!nextCompositionId) {
17504
- if (!entry.typing)
18109
+ if (!entry.typing && !entry.compositionId)
17505
18110
  return false;
17506
18111
  entry.typing = false;
17507
18112
  entry.compositionId = "";
@@ -18104,7 +18709,10 @@ ${cid}` : "";
18104
18709
  }
18105
18710
  if (adopt && !adoptedLocalMediaRef.current.has(mediaKey)) {
18106
18711
  adoptedLocalMediaRef.current.add(mediaKey);
18107
- adoptLocalMessageMedia?.(message, local, { chatId });
18712
+ adoptLocalMessageMedia?.(message, local, {
18713
+ chatId,
18714
+ peerChatPK: local.peerChatPK
18715
+ });
18108
18716
  }
18109
18717
  }
18110
18718
  if (changed) {
@@ -18325,6 +18933,9 @@ ${cid}` : "";
18325
18933
  const sendOptions = mergeSendOptions(sendOptionsForPeer(peerChatPK), options);
18326
18934
  const nextMessage = withMessageRetention(message, sendOptions.retention);
18327
18935
  if (isLongTxt(nextMessage)) {
18936
+ if (hasChatMessageMentions(nextMessage)) {
18937
+ throw new Error("messages with mentions must fit in a text message");
18938
+ }
18328
18939
  const cid = makeSendCid(nextMessage);
18329
18940
  const attachment = makeTxtFileAttachment(nextMessage);
18330
18941
  const localMessage = makeLongTxtLocalMessage(chatPK, cid, attachment, nextMessage);
@@ -19384,9 +19995,12 @@ function normalizeDecryptedMsg(msgData, message, epoch, readContext) {
19384
19995
  if (!message || typeof message !== "object")
19385
19996
  return null;
19386
19997
  const epochSigningKeysVersion = signingKeysFingerprint(readContext?.signingKeysByChatKey);
19998
+ const content = normalizeChatMessageMentions(message, epoch?.manifest, {
19999
+ senderChatPK: message.s || message.from
20000
+ });
19387
20001
  const normalized = {
19388
- ...message,
19389
- id: message.id || msgData?.id || null,
20002
+ ...content,
20003
+ id: content.id || msgData?.id || null,
19390
20004
  ts: msgData?.ts ?? null,
19391
20005
  ttl: msgData?.ttl ?? null,
19392
20006
  ...decryptedEpochFields(epoch),
@@ -20193,11 +20807,171 @@ async function readMsg({ cloud, messageId, options = {} }) {
20193
20807
  deletedKeys: []
20194
20808
  };
20195
20809
  }
20810
+ // ../../core/account/scope.js
20811
+ var ACCOUNT_SCOPE_STALE_CODE = "account-scope/stale";
20812
+ var PROFILE_ID_RE = /^[0-9a-f]{32}$/;
20813
+ var ACCOUNT_SCOPE_CAPABILITIES = new WeakSet;
20814
+ function requireGeneration(value) {
20815
+ const generation = Number(value);
20816
+ if (!Number.isSafeInteger(generation) || generation < 0) {
20817
+ throw new Error("account scope generation required");
20818
+ }
20819
+ return generation;
20820
+ }
20821
+ function requireProfileId(value) {
20822
+ const profileId = lowerText(value);
20823
+ if (!PROFILE_ID_RE.test(profileId))
20824
+ throw new Error("account scope profile id required");
20825
+ return profileId;
20826
+ }
20827
+ function requireText(value, label) {
20828
+ const text = cleanText(value);
20829
+ if (!text)
20830
+ throw new Error(`${label} required`);
20831
+ return text;
20832
+ }
20833
+ function staleAccountScopeError(scope = null) {
20834
+ const error = new Error("account scope is no longer active");
20835
+ error.code = ACCOUNT_SCOPE_STALE_CODE;
20836
+ error.profileId = scope?.profileId || null;
20837
+ error.generation = Number.isSafeInteger(scope?.generation) ? scope.generation : null;
20838
+ return error;
20839
+ }
20840
+ function createAccountScope({
20841
+ environment,
20842
+ generation,
20843
+ isCurrent,
20844
+ profileId,
20845
+ uid
20846
+ } = {}) {
20847
+ if (typeof isCurrent !== "function")
20848
+ throw new Error("account scope current-state port required");
20849
+ const normalizedEnvironment = lowerText(requireText(environment, "account scope environment"));
20850
+ const normalizedGeneration = requireGeneration(generation);
20851
+ const normalizedProfileId = requireProfileId(profileId);
20852
+ const normalizedUid = requireText(uid, "account scope uid");
20853
+ const accountOwnerKey = `${normalizedEnvironment}:${normalizedProfileId}:${normalizedUid}`;
20854
+ const scope = {
20855
+ accountOwnerKey,
20856
+ environment: normalizedEnvironment,
20857
+ generation: normalizedGeneration,
20858
+ key: `${accountOwnerKey}:${normalizedGeneration}`,
20859
+ profileId: normalizedProfileId,
20860
+ uid: normalizedUid,
20861
+ isCurrent: () => isCurrent() === true,
20862
+ assertCurrent() {
20863
+ if (!scope.isCurrent())
20864
+ throw staleAccountScopeError(scope);
20865
+ return scope;
20866
+ }
20867
+ };
20868
+ ACCOUNT_SCOPE_CAPABILITIES.add(scope);
20869
+ return Object.freeze(scope);
20870
+ }
20871
+ function isAccountScope(value) {
20872
+ return !!value && typeof value === "object" && ACCOUNT_SCOPE_CAPABILITIES.has(value);
20873
+ }
20874
+
20875
+ // ../../core/chat/messages/maintenance.js
20876
+ var STALE_MAINTENANCE_CODE = "chat-maintenance/stale";
20877
+ function staleError() {
20878
+ const error = new Error("chat maintenance owner is no longer current");
20879
+ error.code = STALE_MAINTENANCE_CODE;
20880
+ return error;
20881
+ }
20882
+ function cleanKey(value) {
20883
+ return typeof value === "string" ? value.trim() : "";
20884
+ }
20885
+ function isStaleMessageMaintenance(error) {
20886
+ return error?.code === STALE_MAINTENANCE_CODE;
20887
+ }
20888
+ function createAccountMessageMaintenance({ isAccountCurrent = null } = {}) {
20889
+ const claims = new Set;
20890
+ const tasks = new Set;
20891
+ let generation = 0;
20892
+ let open = true;
20893
+ let scope = null;
20894
+ const accountCurrent = () => scope?.isCurrent?.() === true || !scope && (typeof isAccountCurrent !== "function" || isAccountCurrent() === true);
20895
+ const isCurrent = (expectedGeneration = generation) => open && expectedGeneration === generation && accountCurrent();
20896
+ const assertCurrent = (expectedGeneration = generation) => {
20897
+ if (!isCurrent(expectedGeneration))
20898
+ throw staleError();
20899
+ return true;
20900
+ };
20901
+ const owner = {
20902
+ assertCurrent,
20903
+ isCurrent,
20904
+ bindScope(nextScope) {
20905
+ if (!isAccountScope(nextScope)) {
20906
+ throw new Error("account message maintenance scope required");
20907
+ }
20908
+ if (scope && scope !== nextScope) {
20909
+ throw new Error("account message maintenance scope already bound");
20910
+ }
20911
+ scope = nextScope;
20912
+ return scope;
20913
+ },
20914
+ open() {
20915
+ if (open)
20916
+ return generation;
20917
+ generation += 1;
20918
+ open = true;
20919
+ claims.clear();
20920
+ return generation;
20921
+ },
20922
+ claim(kind, value) {
20923
+ const type = cleanKey(kind);
20924
+ const key = cleanKey(value);
20925
+ if (!type || !key)
20926
+ throw new Error("chat maintenance claim key required");
20927
+ assertCurrent();
20928
+ const claimKey = `${type}:${key}`;
20929
+ if (claims.has(claimKey))
20930
+ return null;
20931
+ const claimGeneration = generation;
20932
+ claims.add(claimKey);
20933
+ let released = false;
20934
+ return Object.freeze({
20935
+ assertCurrent: () => {
20936
+ if (released || !claims.has(claimKey))
20937
+ throw staleError();
20938
+ return assertCurrent(claimGeneration);
20939
+ },
20940
+ isCurrent: () => !released && claims.has(claimKey) && isCurrent(claimGeneration),
20941
+ release() {
20942
+ if (released)
20943
+ return false;
20944
+ released = true;
20945
+ claims.delete(claimKey);
20946
+ return true;
20947
+ }
20948
+ });
20949
+ },
20950
+ track(value) {
20951
+ const taskGeneration = generation;
20952
+ assertCurrent(taskGeneration);
20953
+ const task = Promise.resolve(value).finally(() => tasks.delete(task));
20954
+ tasks.add(task);
20955
+ return task;
20956
+ },
20957
+ close() {
20958
+ if (open) {
20959
+ open = false;
20960
+ generation += 1;
20961
+ claims.clear();
20962
+ }
20963
+ return Promise.allSettled([...tasks]);
20964
+ },
20965
+ drain: () => Promise.allSettled([...tasks])
20966
+ };
20967
+ return Object.freeze(owner);
20968
+ }
20969
+
20196
20970
  // ../../core/chat/messages/batches/cleanup.js
20197
20971
  function isDenied(error) {
20198
20972
  return error?.code === "permission-denied";
20199
20973
  }
20200
- function runMessageBatchCleanup({ entry, options = {}, chatBanned, isActive, chatPK, localCache, deleteMessageDocs, diag, getCurrentEntry, notify }) {
20974
+ function runMessageBatchCleanup({ entry, options = {}, chatBanned, isActive, chatPK, localCache, maintenance, deleteMessageDocs, diag, getCurrentEntry, notify }) {
20201
20975
  if (chatBanned || !isActive || !ownsMessageCompaction(entry?.memberChatPKs, chatPK) || !entry?.chatId || !entry.peerChatPK || !entry.ready || entry.exists === false || !entry.messages?.length || typeof deleteMessageDocs !== "function") {
20202
20976
  return null;
20203
20977
  }
@@ -20209,18 +20983,25 @@ function runMessageBatchCleanup({ entry, options = {}, chatBanned, isActive, cha
20209
20983
  const peerChatPK = entry.peerChatPK;
20210
20984
  const generation = entry.generation;
20211
20985
  const scheduledAt = Date.now();
20986
+ if (!maintenance?.claim)
20987
+ throw new Error("account message maintenance required");
20988
+ const lease = maintenance.claim("batch-cleanup", `${chatId}:${generation}`);
20989
+ if (!lease)
20990
+ return null;
20212
20991
  markDiag(diag, "chat.message.cleanup.schedule", {
20213
20992
  chatId,
20214
20993
  compactControls,
20215
20994
  idle: options.idle !== false,
20216
20995
  messages: entry.messages?.length || 0
20217
20996
  });
20218
- return Promise.resolve().then(async () => {
20997
+ const operation = Promise.resolve().then(async () => {
20998
+ lease.assertCurrent();
20219
20999
  if (options.idle !== false) {
20220
21000
  await waitForIdle({
20221
21001
  timeout: CHAT_BATCH_CLEANUP_IDLE_TIMEOUT_MS,
20222
21002
  delay: CHAT_BATCH_CLEANUP_IDLE_DELAY_MS
20223
21003
  });
21004
+ lease.assertCurrent();
20224
21005
  }
20225
21006
  let current = getCurrentEntry?.(chatId);
20226
21007
  if (current !== entry || current.generation !== generation || current.route) {
@@ -20243,8 +21024,10 @@ function runMessageBatchCleanup({ entry, options = {}, chatBanned, isActive, cha
20243
21024
  return;
20244
21025
  }
20245
21026
  if (compactControls) {
21027
+ lease.assertCurrent();
20246
21028
  const compacted = await compactMessages({
20247
21029
  chatId,
21030
+ maintenance,
20248
21031
  messages: current.messages.slice(),
20249
21032
  deletedKeys,
20250
21033
  protectedKeys: compactProtectedKeys,
@@ -20255,6 +21038,7 @@ function runMessageBatchCleanup({ entry, options = {}, chatBanned, isActive, cha
20255
21038
  if (compacted.length) {
20256
21039
  dropped.push(...compacted);
20257
21040
  }
21041
+ lease.assertCurrent();
20258
21042
  }
20259
21043
  if (dropped.length) {
20260
21044
  const cacheStartedAt = Date.now();
@@ -20267,7 +21051,12 @@ function runMessageBatchCleanup({ entry, options = {}, chatBanned, isActive, cha
20267
21051
  startMs: entry.startMs,
20268
21052
  messages: dropped,
20269
21053
  mode: "confirmed"
21054
+ }).then((result) => {
21055
+ lease.assertCurrent();
21056
+ return result;
20270
21057
  }).catch((error) => {
21058
+ if (isStaleMessageMaintenance(error))
21059
+ throw error;
20271
21060
  markError(diag, "chat.message.cleanup.cache", cacheStartedAt, error, {
20272
21061
  droppedCount: dropped.length
20273
21062
  });
@@ -20289,14 +21078,16 @@ function runMessageBatchCleanup({ entry, options = {}, chatBanned, isActive, cha
20289
21078
  if (current !== entry || current.generation !== generation || current.route || !dropped.length) {
20290
21079
  return;
20291
21080
  }
21081
+ lease.assertCurrent();
20292
21082
  notify(current);
20293
21083
  }).catch((error) => {
20294
- if (!isDenied(error)) {
21084
+ if (!isDenied(error) && !isStaleMessageMaintenance(error)) {
20295
21085
  markError(diag, "chat.message.cleanup", Date.now(), error, {
20296
21086
  compactControls
20297
21087
  });
20298
21088
  }
20299
- });
21089
+ }).finally(() => lease.release());
21090
+ return maintenance.track(operation);
20300
21091
  }
20301
21092
 
20302
21093
  // ../../core/chat/messages/epochhistory.js
@@ -20862,6 +21653,7 @@ function runBatchCleanup(owner, entry, options = {}) {
20862
21653
  isActive: sources.isActive,
20863
21654
  chatPK: sources.chatPK,
20864
21655
  localCache: sources.localCache,
21656
+ maintenance: sources.maintenance,
20865
21657
  deleteMessageDocs: sources.deleteMessageDocs,
20866
21658
  diag: sources.diag,
20867
21659
  getCurrentEntry: (chatId) => owner.batches.get(chatId),
@@ -21821,6 +22613,9 @@ function messageBatchSources(input = {}) {
21821
22613
  }
21822
22614
  function createChatMessageBatches(input) {
21823
22615
  let sources = messageBatchSources(input);
22616
+ if (!sources.maintenance?.claim) {
22617
+ throw new Error("message batches require account maintenance");
22618
+ }
21824
22619
  let warming = null;
21825
22620
  const routeMemory = createRouteMemory(MESSAGE_VIEW_CACHE_SIZE);
21826
22621
  const collection = createMessageBatchCollection({
@@ -21841,7 +22636,11 @@ function createChatMessageBatches(input) {
21841
22636
  collection.clear();
21842
22637
  };
21843
22638
  const setSources = (next) => {
21844
- sources = messageBatchSources(next);
22639
+ const nextSources = messageBatchSources(next);
22640
+ if (nextSources.maintenance !== sources.maintenance) {
22641
+ throw new Error("message batch maintenance owner cannot change");
22642
+ }
22643
+ sources = nextSources;
21845
22644
  warming.sync();
21846
22645
  };
21847
22646
  return Object.freeze({
@@ -22128,7 +22927,7 @@ function epochMessageReadContext(epochState) {
22128
22927
  async function readEntry(cloud, uid, chatPrivateKey, chatId) {
22129
22928
  const entryId = ownChatEntryId(chatPrivateKey, chatId);
22130
22929
  const record = await cloud.user.chats.read(uid, entryId);
22131
- const entry = record ? await openOwnChatEntry(chatPrivateKey, entryId, record.body).catch(() => null) : null;
22930
+ const entry = record ? await openOwnChatRecord(chatPrivateKey, entryId, record).catch(() => null) : null;
22132
22931
  if (entry && entry.ownerRevision !== record?.revision)
22133
22932
  throw new Error("chat owner revision mismatch");
22134
22933
  return { entryId, entry };
@@ -22168,6 +22967,50 @@ function cachedInboxRead(cache, key, read) {
22168
22967
  function hasBlockedMembers(manifest, blockedUids) {
22169
22968
  return blockedUids instanceof Set && blockedUids.size > 0 && manifest.members.some((member) => blockedUids.has(member.uid));
22170
22969
  }
22970
+ function publicIncomingChatMember(member) {
22971
+ return Object.freeze({
22972
+ uid: cleanText(member?.uid),
22973
+ chatPK: cleanText(member?.chatPK),
22974
+ chatSigningPK: cleanText(member?.chatSigningPK),
22975
+ notificationPK: cleanText(member?.notificationPK)
22976
+ });
22977
+ }
22978
+ function incomingChatAdmissionContext(context, epochState) {
22979
+ const sender = context.senderProfile || {};
22980
+ return Object.freeze({
22981
+ sender: Object.freeze({
22982
+ uid: cleanText(sender.uid),
22983
+ username: cleanText(sender.username),
22984
+ chatPK: cleanText(sender.chatPK),
22985
+ chatSigningPK: cleanText(sender.chatSigningPK),
22986
+ notificationPK: cleanText(sender.notificationPK)
22987
+ }),
22988
+ chat: Object.freeze({
22989
+ id: cleanText(epochState?.manifest?.chatId),
22990
+ lineage: cleanText(epochState?.manifest?.lineage),
22991
+ members: Object.freeze((epochState?.manifest?.members || []).map(publicIncomingChatMember))
22992
+ }),
22993
+ self: Object.freeze({
22994
+ uid: cleanText(context.identity?.uid),
22995
+ chatPK: cleanText(context.identity?.chatPK)
22996
+ })
22997
+ });
22998
+ }
22999
+ async function decideIncomingChat(context, epochState) {
23000
+ const resolution = context.options.chatRequestResolutions?.get?.(context.options.requestToken);
23001
+ if (resolution?.decision)
23002
+ return normalizeChatAdmissionDecision(resolution.decision);
23003
+ const policyDecision = incomingChatAdmissionDecision(context.identity.chatPrivateKey, context.identity.chatPK, context.senderProfile, context.options.chatAdmission, epochState?.manifest?.lineage);
23004
+ if (typeof context.options.incomingChatDecision !== "function")
23005
+ return policyDecision;
23006
+ try {
23007
+ const harnessDecision = normalizeChatAdmissionDecision(await context.options.incomingChatDecision(incomingChatAdmissionContext(context, epochState)));
23008
+ return strictestChatAdmissionDecision(policyDecision, harnessDecision);
23009
+ } catch {
23010
+ markDiag(context.options.diag, "chat.inbox.admission.failed", {});
23011
+ return CHAT_ADMISSION_DECISIONS.REJECT;
23012
+ }
23013
+ }
22171
23014
  function inboxLeaseError() {
22172
23015
  const error = new Error("chat inbox session changed");
22173
23016
  error.code = "chat/session-stale";
@@ -22377,27 +23220,16 @@ function routesForManifest(entry, epochState) {
22377
23220
  }];
22378
23221
  }));
22379
23222
  }
22380
- function epochDeliveryCapability(identity, epochState) {
22381
- return deriveChatDeliveryCapability(epochState.stateCapability, {
23223
+ function epochDeliveryRegistration(identity, epochState, notificationMode) {
23224
+ return chatDeliveryRegistration(epochState.stateCapability, {
23225
+ attentionSecret: epochState.epochSecret,
22382
23226
  chatId: epochState.manifest.chatId,
22383
23227
  recipientChatPK: identity.chatPK,
22384
- generation: epochState.manifest.epochVersion
23228
+ generation: epochState.manifest.epochVersion,
23229
+ manifest: epochState.manifest,
23230
+ notificationMode
22385
23231
  });
22386
23232
  }
22387
- async function registerEpochDelivery(cloud, identity, epochState, previousState, wasRegistered, options) {
22388
- const capability = epochDeliveryCapability(identity, epochState);
22389
- assertInboxLease(options);
22390
- await cloud.delivery.register(capability);
22391
- if (previousState && wasRegistered) {
22392
- const oldCapability = deriveChatDeliveryCapability(previousState.stateCapability, {
22393
- chatId: previousState.manifest.chatId,
22394
- recipientChatPK: identity.chatPK,
22395
- generation: previousState.manifest.epochVersion
22396
- });
22397
- await cloud.delivery.revoke(oldCapability).catch(() => false);
22398
- }
22399
- return true;
22400
- }
22401
23233
  async function readPingMessage(cloud, payload, epochState) {
22402
23234
  const messageId = cleanText(payload.messageId);
22403
23235
  if (!messageId)
@@ -22484,7 +23316,7 @@ function sameEntryEpoch(left, right) {
22484
23316
  return left?.current?.manifest?.epochId === right?.current?.manifest?.epochId && left?.current?.settings?.digest === right?.current?.settings?.digest;
22485
23317
  }
22486
23318
  function sameEntryRegistration(left, right) {
22487
- return left?.deliveryRegistered === right?.deliveryRegistered && left?.notificationTag === right?.notificationTag;
23319
+ return left?.deliveryRegistered === right?.deliveryRegistered && left?.attentionRegistrationVersion === right?.attentionRegistrationVersion && left?.notificationMode === right?.notificationMode && left?.notificationTag === right?.notificationTag;
22488
23320
  }
22489
23321
  function sameEntryRetirement(left, right) {
22490
23322
  return left?.retirement?.kind === right?.retirement?.kind && left?.retirement?.epochVersion === right?.retirement?.epochVersion;
@@ -22516,6 +23348,8 @@ function projectOwnChatEntry(entry, entryId, userChatPK, ts, activity = {}) {
22516
23348
  settings,
22517
23349
  routes: entry.routes,
22518
23350
  deliveryRegistered: entry.deliveryRegistered === true,
23351
+ attentionRegistrationVersion: entry.attentionRegistrationVersion,
23352
+ notificationMode: entry.notificationMode,
22519
23353
  notificationTag: entry.notificationTag,
22520
23354
  epochState: entry.current,
22521
23355
  ownEntry: entry,
@@ -22634,14 +23468,22 @@ async function verifyOpenedPingEpoch(context) {
22634
23468
  if (epochState.manifest.epochId !== payload.epochId || epochState.manifest.epochVersion !== payload.epochVersion) {
22635
23469
  throw new Error("ping epoch mismatch");
22636
23470
  }
23471
+ context.epochState = epochState;
22637
23472
  if (hasBlockedMembers(epochState.manifest, options.blockedUids)) {
22638
23473
  if (context.entry) {
22639
23474
  await unlinkBlockedChat(cloud, uid, identity, context.entry, options);
22640
23475
  return "deleted";
22641
23476
  }
22642
- throw new Error("blocked chat member");
23477
+ return "blocked";
23478
+ }
23479
+ const incomingMembership = payload.kind === "welcome" && (!context.entry || context.restoringMembership);
23480
+ if (incomingMembership) {
23481
+ const decision = await decideIncomingChat(context, epochState);
23482
+ if (decision === CHAT_ADMISSION_DECISIONS.REJECT)
23483
+ return "rejected";
23484
+ if (decision === CHAT_ADMISSION_DECISIONS.REQUEST)
23485
+ return "request";
22643
23486
  }
22644
- context.epochState = epochState;
22645
23487
  return null;
22646
23488
  }
22647
23489
  function startOpenedWelcomeHydration(context) {
@@ -22649,7 +23491,6 @@ function startOpenedWelcomeHydration(context) {
22649
23491
  if (!membershipWelcome)
22650
23492
  return;
22651
23493
  context.prefetchedMessage = captureAsync(readPingMessage(context.cloud, context.payload, context.epochState));
22652
- context.deliveryRegistration = captureAsync(!context.entry || context.entry?.deliveryRegistered === true ? true : registerEpochDelivery(context.cloud, context.identity, context.epochState, context.entry?.current || null, context.entry?.deliveryRegistered === true, context.options));
22653
23494
  }
22654
23495
  async function applyOpenedPingMessage(context) {
22655
23496
  const { cloud, identity, options, payload, uid } = context;
@@ -22695,19 +23536,12 @@ async function applyOpenedPingMessage(context) {
22695
23536
  return null;
22696
23537
  }
22697
23538
  async function prepareOpenedPingOwner(context) {
22698
- const { cloud, identity, payload, senderProfile } = context;
23539
+ const { identity, payload, senderProfile } = context;
22699
23540
  const routes = withSenderRoute(context.entry, payload, senderProfile, context.epochState);
22700
23541
  const nowMs2 = timestampMs(payload.ts, Date.now()) ?? Date.now();
22701
23542
  const membershipWelcome = (!context.entry || context.restoringMembership) && payload.kind === "welcome";
22702
- const initialDeliveryCapability = !context.entry && membershipWelcome ? epochDeliveryCapability(identity, context.epochState) : null;
22703
- let deliveryRegistered = context.entry?.deliveryRegistered === true;
22704
- if (initialDeliveryCapability)
22705
- deliveryRegistered = true;
22706
- if (!deliveryRegistered && context.deliveryRegistration) {
22707
- deliveryRegistered = await unwrapAsync(context.deliveryRegistration);
22708
- }
22709
- if (!deliveryRegistered)
22710
- deliveryRegistered = await registerEpochDelivery(cloud, identity, context.epochState, null, context.entry?.deliveryRegistered === true, context.options);
23543
+ const initialDeliveryRegistration = membershipWelcome ? epochDeliveryRegistration(identity, context.epochState) : null;
23544
+ const deliveryRegistered = true;
22711
23545
  const nextEntry = context.entry ? {
22712
23546
  ...context.entry,
22713
23547
  current: {
@@ -22721,6 +23555,8 @@ async function prepareOpenedPingOwner(context) {
22721
23555
  },
22722
23556
  routes,
22723
23557
  deliveryRegistered,
23558
+ attentionRegistrationVersion: CHAT_ATTENTION_REGISTRATION_VERSION,
23559
+ notificationMode: normalizeChatNotificationMode(context.entry.notificationMode, context.epochState.manifest),
22724
23560
  notificationTag: notificationChatTag(context.epochState.stateCapability, payload.chatId, identity.chatPK),
22725
23561
  retirement: null
22726
23562
  } : makeOwnChatEntry(context.epochState, {
@@ -22731,7 +23567,7 @@ async function prepareOpenedPingOwner(context) {
22731
23567
  });
22732
23568
  const welcomeMlsState = membershipWelcome && context.epochState.mlsSnapshot ? await prepareOwnerChatMlsSnapshot(identity, context.entryId, context.epochState, context.epochState.mlsSnapshot) : null;
22733
23569
  Object.assign(context, {
22734
- initialDeliveryCapability,
23570
+ initialDeliveryRegistration,
22735
23571
  membershipWelcome,
22736
23572
  nextEntry,
22737
23573
  nowMs: nowMs2,
@@ -22745,7 +23581,7 @@ async function commitOpenedPingOwner(context) {
22745
23581
  const {
22746
23582
  cloud,
22747
23583
  identity,
22748
- initialDeliveryCapability,
23584
+ initialDeliveryRegistration,
22749
23585
  membershipWelcome,
22750
23586
  nextEntry,
22751
23587
  nowMs: nowMs2,
@@ -22759,7 +23595,7 @@ async function commitOpenedPingOwner(context) {
22759
23595
  return {
22760
23596
  entry: nextEntry,
22761
23597
  tsMs: nowMs2,
22762
- deliveryRegistration: initialDeliveryCapability,
23598
+ deliveryRegistration: initialDeliveryRegistration,
22763
23599
  ...welcomeMlsState ? { mlsWrites: [{ id: welcomeMlsState.stateId, value: welcomeMlsState.value }] } : {}
22764
23600
  };
22765
23601
  }
@@ -22774,13 +23610,18 @@ async function commitOpenedPingOwner(context) {
22774
23610
  current: nextEntry.current,
22775
23611
  routes: withSenderRoute(current, payload, senderProfile, context.epochState),
22776
23612
  deliveryRegistered: current.deliveryRegistered || nextEntry.deliveryRegistered,
23613
+ attentionRegistrationVersion: CHAT_ATTENTION_REGISTRATION_VERSION,
23614
+ notificationMode: normalizeChatNotificationMode(current.notificationMode, context.epochState.manifest),
22777
23615
  notificationTag: nextEntry.notificationTag,
22778
23616
  retirement: membershipWelcome ? null : current.retirement
22779
23617
  };
22780
23618
  if (sameStableEntry(current, candidate) && !welcomeMlsState)
22781
23619
  return { result: current };
23620
+ const registrationMatchesEpoch = current.current.manifest.epochId === context.epochState.manifest.epochId && hasCurrentChatAttentionRegistration(current);
23621
+ const deliveryRegistration = registrationMatchesEpoch ? null : epochDeliveryRegistration(identity, context.epochState, candidate.notificationMode);
22782
23622
  return {
22783
23623
  entry: candidate,
23624
+ deliveryRegistration,
22784
23625
  ...membershipWelcome ? { tsMs: nowMs2 } : {},
22785
23626
  ...welcomeMlsState ? { mlsWrites: [{ id: welcomeMlsState.stateId, value: welcomeMlsState.value }] } : {}
22786
23627
  };
@@ -22792,10 +23633,18 @@ async function commitOpenedPingOwner(context) {
22792
23633
  }
22793
23634
  async function cleanupOpenedPing(context) {
22794
23635
  const { cloud, identity, options } = context;
22795
- if (!context.membershipWelcome || !context.epochState.mlsSnapshot || context.epochState.mlsKeyPackageLastResort)
23636
+ const membershipWelcome = context.payload.kind === "welcome" && (!context.entry || context.restoringMembership);
23637
+ if (!membershipWelcome || !context.epochState?.mlsSnapshot || context.epochState.mlsKeyPackageLastResort)
23638
+ return;
23639
+ const keyPackageId = cleanText(context.epochState.mlsKeyPackageId);
23640
+ if (!keyPackageId)
22796
23641
  return;
23642
+ const consumed = options.consumedMlsKeyPackageIds;
23643
+ if (consumed?.has(keyPackageId))
23644
+ return;
23645
+ consumed?.add(keyPackageId);
22797
23646
  assertInboxLease(options);
22798
- await cloud.user.mls.keyPackages.deleteState(identity.uid, context.epochState.mlsKeyPackageId).catch(() => false);
23647
+ await cloud.user.mls.keyPackages.deleteState(identity.uid, keyPackageId).catch(() => false);
22799
23648
  options.onMlsKeyPackageConsumed?.();
22800
23649
  }
22801
23650
  function openedPingProjection(context, entry) {
@@ -22862,9 +23711,13 @@ async function persistOpenedPingOwner(context) {
22862
23711
  });
22863
23712
  }
22864
23713
  async function settleOpenedPing(prepared, onAuthoritative) {
22865
- if (prepared.result)
22866
- return prepared.result;
22867
23714
  const { context } = prepared;
23715
+ if (prepared.result === "request")
23716
+ return "request";
23717
+ if (prepared.result) {
23718
+ await cleanupOpenedPing(context);
23719
+ return prepared.result;
23720
+ }
22868
23721
  startOpenedWelcomeHydration(context);
22869
23722
  const createsMembership = !context.entry && context.payload.kind === "welcome";
22870
23723
  const ownerPersistence = createsMembership ? captureAsync(persistOpenedPingOwner(context)) : null;
@@ -22889,6 +23742,15 @@ async function settleOpenedPing(prepared, onAuthoritative) {
22889
23742
  });
22890
23743
  return true;
22891
23744
  }
23745
+ async function hydrateOpenedChatRequest(context) {
23746
+ const { message, record } = await readPingMessage(context.cloud, context.payload, context.epochState);
23747
+ if (!message || !record || !cleanText(context.payload.messageId)) {
23748
+ throw new Error("chat request message required");
23749
+ }
23750
+ context.message = message;
23751
+ context.record = record;
23752
+ context.preview = nextPreview(null, record, message, context.payload);
23753
+ }
22892
23754
  function senderProfilePromise(cache, cloud, payload) {
22893
23755
  const key = `${cleanText(payload?.senderUid)}:${cleanText(payload?.senderChatPK)}`;
22894
23756
  return cachedInboxRead(cache, key, () => resolveSenderProfile(cloud, payload));
@@ -22961,13 +23823,15 @@ function welcomeAnnouncementFromPreparedPing(authenticated, prepared, document)
22961
23823
  members: manifest.members,
22962
23824
  settings,
22963
23825
  messageId: payload.messageId || null,
23826
+ messageRequest: prepared?.opened?.result === "request",
23827
+ preview: prepared?.opened?.context?.preview || null,
22964
23828
  at: ts
22965
23829
  };
22966
23830
  }
22967
23831
  function announcePreparedWelcome(context, authenticated, prepared, document, startedAt) {
22968
23832
  const { blockedUids, options, state } = context;
22969
23833
  const payload = authenticated?.ping?.payload;
22970
- if (payload?.kind !== "welcome" || state.currentChats.some((chat) => chat?.id === payload.chatId) || blockedUids.has(payload.senderUid)) {
23834
+ if (prepared?.opened?.result && prepared.opened.result !== "request" || payload?.kind !== "welcome" || state.currentChats.some((chat) => chat?.id === payload.chatId) || blockedUids.has(payload.senderUid)) {
22971
23835
  return () => {};
22972
23836
  }
22973
23837
  markDiag(options.diag, "chat.inbox.chat.announced", {
@@ -23119,12 +23983,11 @@ async function commitRemovedEpochTransition(context) {
23119
23983
  return { removed: true, chatId: parent.chatId, entry: retiredEntry };
23120
23984
  }
23121
23985
  async function prepareReceivedEpochOwner(context) {
23122
- const { cloud, entry, identity, options, parent } = context;
23986
+ const { entry, identity, options, parent } = context;
23123
23987
  if (hasBlockedMembers(context.epochState.manifest, options.blockedUids)) {
23124
- context.terminal = await unlinkBlockedChat(cloud, context.uid, identity, entry, options);
23988
+ context.terminal = await unlinkBlockedChat(context.cloud, context.uid, identity, entry, options);
23125
23989
  return;
23126
23990
  }
23127
- const deliveryRegistered = await registerEpochDelivery(cloud, identity, context.epochState, entry.current, entry.deliveryRegistered === true, options);
23128
23991
  context.nextEntry = {
23129
23992
  ...entry,
23130
23993
  current: {
@@ -23137,7 +24000,9 @@ async function prepareReceivedEpochOwner(context) {
23137
24000
  settings: context.epochState.settings
23138
24001
  },
23139
24002
  routes: routesForManifest(entry, context.epochState),
23140
- deliveryRegistered,
24003
+ deliveryRegistered: true,
24004
+ attentionRegistrationVersion: CHAT_ATTENTION_REGISTRATION_VERSION,
24005
+ notificationMode: normalizeChatNotificationMode(entry.notificationMode, context.epochState.manifest),
23141
24006
  notificationTag: notificationChatTag(context.epochState.stateCapability, parent.chatId, identity.chatPK)
23142
24007
  };
23143
24008
  context.historyId = ownEpochEntryId(identity.chatPrivateKey, parent.chatId, parent.epochId);
@@ -23166,8 +24031,11 @@ async function commitReceivedEpochOwner(context) {
23166
24031
  current: nextEntry.current,
23167
24032
  routes: routesForManifest(current, epochState),
23168
24033
  deliveryRegistered: current.deliveryRegistered || nextEntry.deliveryRegistered,
24034
+ attentionRegistrationVersion: CHAT_ATTENTION_REGISTRATION_VERSION,
24035
+ notificationMode: normalizeChatNotificationMode(current.notificationMode, epochState.manifest),
23169
24036
  notificationTag: nextEntry.notificationTag
23170
24037
  },
24038
+ deliveryRegistration: epochDeliveryRegistration(identity, epochState, current.notificationMode),
23171
24039
  touchTs: true,
23172
24040
  epochs: [{
23173
24041
  id: historyId,
@@ -23181,8 +24049,17 @@ async function commitReceivedEpochOwner(context) {
23181
24049
  };
23182
24050
  });
23183
24051
  }
23184
- function publishReceivedEpochTransition(context) {
24052
+ async function publishReceivedEpochTransition(context) {
23185
24053
  assertInboxLease(context.options);
24054
+ if (context.entry.deliveryRegistered) {
24055
+ const oldCapability = deriveChatDeliveryCapability(context.entry.current.stateCapability, {
24056
+ chatId: context.parent.chatId,
24057
+ recipientChatPK: context.identity.chatPK,
24058
+ generation: context.parent.epochVersion
24059
+ });
24060
+ await context.cloud.delivery.revoke(oldCapability).catch(() => false);
24061
+ assertInboxLease(context.options);
24062
+ }
23186
24063
  context.options.onTransition?.(projectOwnChatEntry(context.committedEntry, context.entryId, context.identity.chatPK, context.nowMs), {
23187
24064
  ...context.action,
23188
24065
  membershipCommitted: true
@@ -23445,12 +24322,17 @@ async function prepareInboxWork(context, prepared, processingOptions = {}) {
23445
24322
  currentChats: state.currentChats,
23446
24323
  discoveryStartedAt: prepared.startedAt,
23447
24324
  mlsKeyPackageStateCache: context.mlsKeyPackageStateCache,
24325
+ consumedMlsKeyPackageIds: context.consumedMlsKeyPackageIds,
23448
24326
  mlsPackageCache: context.mlsPackageCache,
23449
24327
  settingsRecordCache: context.settingsRecordCache,
23450
24328
  onInboxCommit: state.commitOwner,
23451
24329
  onTransition: state.publishTransition,
23452
- onInboxDelete: state.publishDelete
24330
+ onInboxDelete: state.publishDelete,
24331
+ requestToken: `${document?.slot === true ? "slot" : "ping"}:${cleanText(document?.id)}`
23453
24332
  });
24333
+ if (ping.opened?.result === "request") {
24334
+ await hydrateOpenedChatRequest(ping.opened.context);
24335
+ }
23454
24336
  announcement = announcePreparedWelcome(context, authenticated, ping, document, prepared.startedAt);
23455
24337
  return { announcement, document, ping, prepared, processingOptions, status: null };
23456
24338
  } catch (error) {
@@ -23475,6 +24357,11 @@ async function settleInboxWork(context, work, onAuthoritative) {
23475
24357
  result: result === true ? "applied" : cleanText(result) || "ignored",
23476
24358
  slot: document?.slot === true
23477
24359
  });
24360
+ if (result === "request") {
24361
+ options.onInboxRequest?.(work.announcement, document);
24362
+ options.onPingSettled?.(document, result);
24363
+ return "request";
24364
+ }
23478
24365
  if (result !== "duplicate")
23479
24366
  state.processed += 1;
23480
24367
  if (!document.slot || result === "deleted") {
@@ -23778,6 +24665,7 @@ async function processInbox(cloud, uid, userChatPK, userPrivKey, options = {}) {
23778
24665
  const context = {
23779
24666
  blockedUids,
23780
24667
  cloud,
24668
+ consumedMlsKeyPackageIds: new Set,
23781
24669
  discoveryPool: createInboxWorkPool(CHAT_INBOX_DISCOVERY_PARALLEL_CHATS),
23782
24670
  identity,
23783
24671
  mlsKeyPackageStateCache: new Map,
@@ -23948,6 +24836,8 @@ function listenToChats(cloud, uid, userChatPK, userPrivKey, onUpdate, onError, o
23948
24836
  let inboxRequestedRetryDelayMs = null;
23949
24837
  const priorityInboxDocuments = new Map;
23950
24838
  const activeInboxDocuments = new Map;
24839
+ const pendingChatRequests = new Map;
24840
+ const chatRequestResolutions = new Map;
23951
24841
  const inboxPendingAttempts = new Map;
23952
24842
  const leaveProposalObservedAt = new Map;
23953
24843
  let closed = false;
@@ -23976,8 +24866,17 @@ function listenToChats(cloud, uid, userChatPK, userPrivKey, onUpdate, onError, o
23976
24866
  const revokeInboxAnnouncement = (chatId, token) => {
23977
24867
  if (closed)
23978
24868
  return false;
24869
+ if (pendingChatRequests.get(chatId)?.announcement?.token === token) {
24870
+ pendingChatRequests.delete(chatId);
24871
+ }
23979
24872
  return options.onInboxRevoke?.(chatId, token) ?? false;
23980
24873
  };
24874
+ const retainInboxRequest = (announcement, document) => {
24875
+ if (closed || !announcement?.messageRequest || !announcement?.chatId || !announcement?.token)
24876
+ return false;
24877
+ pendingChatRequests.set(announcement.chatId, { announcement, document });
24878
+ return true;
24879
+ };
23981
24880
  const publishInboxTransition = (chat, message) => {
23982
24881
  if (closed)
23983
24882
  return false;
@@ -24055,6 +24954,10 @@ function listenToChats(cloud, uid, userChatPK, userPrivKey, onUpdate, onError, o
24055
24954
  notificationPrivateKey: options.notificationPrivateKey,
24056
24955
  mls: options.mls,
24057
24956
  blockedUids: options.blockedUids,
24957
+ chatAdmission: options.chatAdmission,
24958
+ incomingChatDecision: options.incomingChatDecision,
24959
+ chatRequestResolutions,
24960
+ onInboxRequest: retainInboxRequest,
24058
24961
  sinceMs: inboxSinceMs,
24059
24962
  priorityDocuments,
24060
24963
  settlementController,
@@ -24100,7 +25003,7 @@ function listenToChats(cloud, uid, userChatPK, userPrivKey, onUpdate, onError, o
24100
25003
  retryRequested = true;
24101
25004
  return true;
24102
25005
  },
24103
- onPingSettled: (doc) => {
25006
+ onPingSettled: (doc, result) => {
24104
25007
  const key = inboxRetryKey(doc);
24105
25008
  if (key)
24106
25009
  inboxPendingAttempts.delete(key);
@@ -24108,6 +25011,12 @@ function listenToChats(cloud, uid, userChatPK, userPrivKey, onUpdate, onError, o
24108
25011
  if (priorityKey && chatDocDataEqual(priorityInboxDocuments.get(priorityKey), doc)) {
24109
25012
  priorityInboxDocuments.delete(priorityKey);
24110
25013
  }
25014
+ const resolution = chatRequestResolutions.get(priorityKey);
25015
+ if (resolution && result !== "request") {
25016
+ chatRequestResolutions.delete(priorityKey);
25017
+ pendingChatRequests.delete(resolution.chatId);
25018
+ resolution.resolve(result === true || result === "rejected");
25019
+ }
24111
25020
  },
24112
25021
  onPingStarted: (doc) => {
24113
25022
  const key = inboxDocumentKey2(doc);
@@ -24183,6 +25092,27 @@ function listenToChats(cloud, uid, userChatPK, userPrivKey, onUpdate, onError, o
24183
25092
  setTimeout(processQueuedInbox, 0);
24184
25093
  }
24185
25094
  };
25095
+ const resolveChatRequest = (chatId, decision) => {
25096
+ const request = pendingChatRequests.get(chatId);
25097
+ if (!request?.document || !["accept", "reject"].includes(decision))
25098
+ return Promise.resolve(false);
25099
+ const token = request.announcement.token;
25100
+ const existing = chatRequestResolutions.get(token);
25101
+ if (existing)
25102
+ return existing.promise;
25103
+ let resolve;
25104
+ let reject;
25105
+ const promise = new Promise((resolveValue, rejectValue) => {
25106
+ resolve = resolveValue;
25107
+ reject = rejectValue;
25108
+ });
25109
+ chatRequestResolutions.set(token, { chatId, decision, promise, reject, resolve });
25110
+ priorityInboxDocuments.set(token, request.document);
25111
+ inboxQueued = true;
25112
+ if (!inboxProcessing)
25113
+ setTimeout(processQueuedInbox, 0);
25114
+ return promise;
25115
+ };
24186
25116
  const schedule = (delayMs) => {
24187
25117
  if (timer || processing || closed) {
24188
25118
  return;
@@ -24242,13 +25172,19 @@ function listenToChats(cloud, uid, userChatPK, userPrivKey, onUpdate, onError, o
24242
25172
  inboxQueued = false;
24243
25173
  priorityInboxDocuments.clear();
24244
25174
  activeInboxDocuments.clear();
25175
+ for (const resolution of chatRequestResolutions.values()) {
25176
+ resolution.reject(new Error("chat inbox closed"));
25177
+ }
25178
+ chatRequestResolutions.clear();
25179
+ pendingChatRequests.clear();
24245
25180
  unsub?.();
24246
25181
  tokenUnsub?.();
24247
25182
  inboxUnsub?.();
24248
25183
  };
24249
25184
  return Object.freeze({
24250
25185
  close,
24251
- prioritizeChat: (chatId) => settlementController.prioritizeChat(chatId)
25186
+ prioritizeChat: (chatId) => settlementController.prioritizeChat(chatId),
25187
+ resolveChatRequest
24252
25188
  });
24253
25189
  }
24254
25190
  async function loadMoreChats(cloud, uid, userChatPK, userPrivKey, afterChat, pageSize) {
@@ -24330,7 +25266,7 @@ async function getChat(cloud, uid, chatId, userChatPK, userPrivKey) {
24330
25266
  }
24331
25267
  async function decryptChatEntry(entryRecord, userChatPK, userPrivKey) {
24332
25268
  const data = entryRecord;
24333
- const entry = await openOwnChatEntry(userPrivKey, entryRecord.id, data?.body);
25269
+ const entry = await openOwnChatRecord(userPrivKey, entryRecord.id, data);
24334
25270
  if (entry.ownerRevision !== data?.revision) {
24335
25271
  throw new Error("chat owner revision mismatch");
24336
25272
  }
@@ -24368,6 +25304,8 @@ async function decryptChatEntry(entryRecord, userChatPK, userPrivKey) {
24368
25304
  readMs: null,
24369
25305
  startMs,
24370
25306
  deliveryRegistered: entry.deliveryRegistered === true,
25307
+ attentionRegistrationVersion: entry.attentionRegistrationVersion,
25308
+ notificationMode: entry.notificationMode,
24371
25309
  routes: entry.routes,
24372
25310
  peerDeliveryCapability: directPeer ? entry.routes?.[directPeer.chatPK]?.deliveryCapability || null : null,
24373
25311
  peerNotificationPK: directPeer?.notificationPK || null,
@@ -24645,12 +25583,14 @@ function announcementRow(announcement, selfChatPK) {
24645
25583
  peerChatPK: directPeer?.chatPK || null,
24646
25584
  peerUid: directPeer?.uid || null,
24647
25585
  settings: announcement.settings || {},
24648
- preview: null,
25586
+ preview: announcement.preview || null,
24649
25587
  readMs: null,
24650
25588
  inboxMessageId: announcement.messageId || null,
24651
25589
  inboxMessageAt: at,
24652
25590
  ts: at,
24653
- unseen: false
25591
+ unseen: announcement.messageRequest === true,
25592
+ messageRequest: announcement.messageRequest === true,
25593
+ requestToken: announcement.token
24654
25594
  };
24655
25595
  }
24656
25596
  function sourceEntry() {
@@ -25012,6 +25952,8 @@ function createChatListIndex({ cache, filterHiddenChats, getSources, isHiddenCha
25012
25952
  settings: retiredEntry.current.settings.values,
25013
25953
  routes: retiredEntry.routes,
25014
25954
  deliveryRegistered: retiredEntry.deliveryRegistered === true,
25955
+ attentionRegistrationVersion: retiredEntry.attentionRegistrationVersion,
25956
+ notificationMode: retiredEntry.notificationMode,
25015
25957
  notificationTag: retiredEntry.notificationTag || null,
25016
25958
  epochState: retiredEntry.current,
25017
25959
  ownEntry: retiredEntry,
@@ -25098,6 +26040,8 @@ function createChatListIndex({ cache, filterHiddenChats, getSources, isHiddenCha
25098
26040
  peerSigningPublicKey: owner?.signingKeysByChatKey?.[peerChatPK] || "",
25099
26041
  signerShared: owner?.signerShared === true,
25100
26042
  deliveryRegistered: owner?.deliveryRegistered === true,
26043
+ attentionRegistrationVersion: owner?.attentionRegistrationVersion || 0,
26044
+ notificationMode: owner?.notificationMode || null,
25101
26045
  peerDeliveryCapability: owner?.peerDeliveryCapability || "",
25102
26046
  peerNotificationPK: owner?.peerNotificationPK || "",
25103
26047
  notificationTag: owner?.notificationTag || "",
@@ -25179,12 +26123,14 @@ function createChatListListener({
25179
26123
  let listening = false;
25180
26124
  let unsubscribeChats = null;
25181
26125
  let prioritizeInboxChat = null;
26126
+ let resolveInboxChatRequest = null;
25182
26127
  let retryTimer = null;
25183
26128
  let updateSequence = 0;
25184
26129
  const stop = () => {
25185
26130
  unsubscribeChats?.();
25186
26131
  unsubscribeChats = null;
25187
26132
  prioritizeInboxChat = null;
26133
+ resolveInboxChatRequest = null;
25188
26134
  };
25189
26135
  const retainAfterError = (error) => {
25190
26136
  const sources = getSources();
@@ -25343,6 +26289,8 @@ function createChatListListener({
25343
26289
  inboxTransport: sources.inboxTransport,
25344
26290
  mls: sources.mls,
25345
26291
  blockedUids: sources.blockedUidSet,
26292
+ chatAdmission: sources.chatAdmission,
26293
+ incomingChatDecision: sources.incomingChatDecision,
25346
26294
  localCache: sources.localCache,
25347
26295
  diag: sources.diag,
25348
26296
  getCurrentChat: index.getOwnerChat,
@@ -25353,6 +26301,7 @@ function createChatListListener({
25353
26301
  throw new Error("chat list subscription lifecycle required");
25354
26302
  }
25355
26303
  prioritizeInboxChat = subscription.prioritizeChat;
26304
+ resolveInboxChatRequest = subscription.resolveChatRequest;
25356
26305
  unsubscribeChats = () => {
25357
26306
  cancelled = true;
25358
26307
  markDiag(getSources().diag, "chat.provider.listen.stop", { elapsedMs: Date.now() - listenStartedAt });
@@ -25370,6 +26319,7 @@ function createChatListListener({
25370
26319
  },
25371
26320
  isListening: () => listening,
25372
26321
  prioritizeChat: (chatId) => prioritizeInboxChat?.(chatId) || Promise.resolve(false),
26322
+ resolveChatRequest: (chatId, decision) => resolveInboxChatRequest?.(chatId, decision) || Promise.resolve(false),
25373
26323
  start: () => {
25374
26324
  if (listening)
25375
26325
  return;
@@ -25970,6 +26920,7 @@ function createChatList(initialSources) {
25970
26920
  getRow: index.getRow,
25971
26921
  markChatRead: index.markChatRead,
25972
26922
  reconcileChatEpoch: epoch.reconcileChatEpoch,
26923
+ resolveChatRequest: listener.resolveChatRequest,
25973
26924
  sendOptionsForPeer: index.sendOptionsForPeer,
25974
26925
  hasChat: paging.hasChat
25975
26926
  };
@@ -26538,20 +27489,24 @@ function memberFromChatIdentity(value) {
26538
27489
  uid: identity.uid,
26539
27490
  chatPK: identity.chatPK,
26540
27491
  chatSigningPK: identity.chatSigningPK,
26541
- notificationPK: identity.notificationPK
27492
+ notificationPK: identity.notificationPK,
27493
+ ...value?.admissionCapability ? { admissionCapability: cleanText(value.admissionCapability).toLowerCase() } : {}
26542
27494
  };
26543
27495
  const mlsLeafId = cleanText(value?.mlsLeafId).toLowerCase();
26544
27496
  return /^[0-9a-f]{64}$/u.test(mlsLeafId) ? { ...member, mlsLeafId } : member;
26545
27497
  }
26546
27498
  async function registerInitialDelivery(cloud, identity, epochState) {
26547
- const capability = deriveChatDeliveryCapability(epochState.stateCapability, {
27499
+ const registration = chatDeliveryRegistration(epochState.stateCapability, {
27500
+ attentionSecret: epochState.epochSecret,
26548
27501
  chatId: epochState.manifest.chatId,
26549
27502
  recipientChatPK: identity.chatPK,
26550
- generation: epochState.manifest.epochVersion
27503
+ generation: epochState.manifest.epochVersion,
27504
+ manifest: epochState.manifest
26551
27505
  });
26552
- return cloud.delivery.register(capability).then(() => true, () => false);
27506
+ return cloud.delivery.register(registration).then(() => true, () => false);
26553
27507
  }
26554
- function initialRoutes(manifest, stateCapability, ownerChatPK) {
27508
+ function initialRoutes(manifest, stateCapability, ownerChatPK, memberValues = []) {
27509
+ const privateByChatPK = new Map(memberValues.map((member) => [member.chatPK, member]));
26555
27510
  return Object.fromEntries(manifest.members.map((member) => [member.chatPK, {
26556
27511
  uid: member.uid,
26557
27512
  deliveryCapability: manifest.lineage === CHAT_LINEAGES.GROUP || member.chatPK === ownerChatPK ? deriveChatDeliveryCapability(stateCapability, {
@@ -26560,13 +27515,14 @@ function initialRoutes(manifest, stateCapability, ownerChatPK) {
26560
27515
  generation: manifest.epochVersion
26561
27516
  }) : null,
26562
27517
  notificationPK: member.notificationPK,
27518
+ admissionCapability: privateByChatPK.get(member.chatPK)?.admissionCapability || null,
26563
27519
  generation: manifest.epochVersion
26564
27520
  }]));
26565
27521
  }
26566
27522
  async function prepareInitialOwner(identity, epochState, fields = {}) {
26567
27523
  const entryId = ownChatEntryId(identity.chatPrivateKey, epochState.manifest.chatId);
26568
27524
  const entry = makeOwnChatEntry(epochState, {
26569
- routes: initialRoutes(epochState.manifest, epochState.stateCapability, identity.chatPK),
27525
+ routes: initialRoutes(epochState.manifest, epochState.stateCapability, identity.chatPK, fields.members),
26570
27526
  startMs: Date.now(),
26571
27527
  deliveryRegistered: fields.deliveryRegistered === true,
26572
27528
  notificationTag: notificationChatTag(epochState.stateCapability, epochState.manifest.chatId, identity.chatPK)
@@ -26581,6 +27537,7 @@ async function prepareInitialOwner(identity, epochState, fields = {}) {
26581
27537
  entryId,
26582
27538
  record: {
26583
27539
  body: await sealOwnChatEntry(identity.chatPrivateKey, entryId, entry),
27540
+ notificationBody: await sealOwnChatNotificationPreference(identity.chatPrivateKey, entryId, entry),
26584
27541
  revision: entry.ownerRevision,
26585
27542
  tsMs
26586
27543
  }
@@ -26680,7 +27637,8 @@ async function createGroupChat(cloud, identityValue, memberValues, settings = {}
26680
27637
  const stateId = chatMlsStateId(identity.chatPrivateKey, chatId, initial.transitionCommitment);
26681
27638
  const ownerMlsState = await sealChatMlsState(identity.chatPrivateKey, stateId, { v: 1, chatId, transitionCommitment: initial.transitionCommitment }, mlsGroup.snapshot);
26682
27639
  const owner = await prepareInitialOwner(identity, initialState, {
26683
- deliveryRegistered: true
27640
+ deliveryRegistered: true,
27641
+ members
26684
27642
  });
26685
27643
  let stateError = null;
26686
27644
  try {
@@ -26794,7 +27752,7 @@ async function createGroupChat(cloud, identityValue, memberValues, settings = {}
26794
27752
 
26795
27753
  // ../../core/chat/direct.js
26796
27754
  "use client";
26797
- function orderedChatKeys(first, second) {
27755
+ function orderedChatKeys2(first, second) {
26798
27756
  return [
26799
27757
  cleanChatHex(first, "chat public key"),
26800
27758
  cleanChatHex(second, "peer chat public key")
@@ -26816,7 +27774,7 @@ function deriveDirectRouteId(chatPrivateKey, chatPK, peerChatPK) {
26816
27774
  try {
26817
27775
  peerKey = fromHex(otherChatPK, "peer chat public key");
26818
27776
  shared = x25519.getSharedSecret(toBytes32(chatPrivateKey, "chat private key"), peerKey);
26819
- routeId = deriveKey(shared, "direct-route-id-v3", orderedChatKeys(ownChatPK, otherChatPK));
27777
+ routeId = deriveKey(shared, "direct-route-id-v3", orderedChatKeys2(ownChatPK, otherChatPK));
26820
27778
  return toHex(routeId);
26821
27779
  } finally {
26822
27780
  cleanBytes(peerKey, shared, routeId);
@@ -26966,6 +27924,9 @@ function createChatSessionLaunches({
26966
27924
  setSelectedChat(existingChatId);
26967
27925
  return existingChatId;
26968
27926
  }
27927
+ if (directAdmissionForPeer(operation.identity.chatPrivateKey, chatPK, profile) === CHAT_ADMISSION_DECISIONS.REJECT) {
27928
+ throw new Error("peer is not accepting direct chats");
27929
+ }
26969
27930
  const routeId = directRouteIdForPeer(member.chatPK, operation);
26970
27931
  if (operation.pendingOwner.getPendingLaunch()?.id === routeId)
26971
27932
  return routeId;
@@ -27034,6 +27995,13 @@ function createChatSessionLaunches({
27034
27995
  const profileList = Array.isArray(profiles) ? profiles : [profiles];
27035
27996
  const blockedSet = new Set(blocked);
27036
27997
  const requestedMembers = profileList.map(profileMember);
27998
+ if (profileList.length === 1) {
27999
+ if (directAdmissionForPeer(chatPrivateKey, chatPK, profileList[0]) === CHAT_ADMISSION_DECISIONS.REJECT) {
28000
+ throw new Error("peer is not accepting direct chats");
28001
+ }
28002
+ } else if (profileList.length > 1 && profileList.some((profile) => !canInviteToGroup(profile))) {
28003
+ throw new Error("peer is not accepting group invitations");
28004
+ }
27037
28005
  for (const member of requestedMembers) {
27038
28006
  if (member.chatPK === chatPK)
27039
28007
  throw new Error("current user is already a chat member");
@@ -27162,6 +28130,9 @@ function createChatSessionLaunches({
27162
28130
  directOpens.delete(routeId);
27163
28131
  return runExistingDirectLifecycle(existingChatId, lifecycle, operation);
27164
28132
  }
28133
+ if (directAdmissionForPeer(operation.identity.chatPrivateKey, chatPK, profile) === CHAT_ADMISSION_DECISIONS.REJECT) {
28134
+ throw new Error("peer is not accepting direct chats");
28135
+ }
27165
28136
  const resolution = startDirectResolution(routeId, member.chatPK, operation);
27166
28137
  const state = { operation, promise: null };
27167
28138
  state.promise = resolution.promise.then((resolvedChatId) => {
@@ -27243,6 +28214,7 @@ function createChatSessionPending({
27243
28214
  setSelectedChat,
27244
28215
  publish,
27245
28216
  getOwnerChat,
28217
+ getChatRow,
27246
28218
  ensureChat,
27247
28219
  profileMember,
27248
28220
  transitionIdentity,
@@ -27607,7 +28579,7 @@ function createChatSessionPending({
27607
28579
  setSelectedChat(chatId);
27608
28580
  return true;
27609
28581
  };
27610
- if (pendingMatches || getOwnerChat(chatId))
28582
+ if (pendingMatches || getOwnerChat(chatId) || getChatRow(chatId)?.messageRequest)
27611
28583
  return selectResolved();
27612
28584
  return Promise.resolve(ensureChat(chatId)).then((ready) => ready === true && selectResolved());
27613
28585
  },
@@ -27680,11 +28652,13 @@ function createChatSessionMembership({
27680
28652
  if (!profile?.uid || !profile?.chatPK || !profile?.chatSigningPK || !profile?.notificationPK) {
27681
28653
  throw new Error("complete peer chat identity required");
27682
28654
  }
28655
+ const identity = getIdentity();
27683
28656
  return {
27684
28657
  uid: profile.uid,
27685
28658
  chatPK: profile.chatPK,
27686
28659
  chatSigningPK: profile.chatSigningPK,
27687
- notificationPK: profile.notificationPK
28660
+ notificationPK: profile.notificationPK,
28661
+ admissionCapability: deriveDirectAdmissionCapability(identity.chatPrivateKey, identity.chatPK, profile.chatPK, profile.chatPK)
27688
28662
  };
27689
28663
  };
27690
28664
  const prepareMlsAdditions = async (profiles, operation = operationContext()) => {
@@ -27928,7 +28902,12 @@ function createChatSessionMembership({
27928
28902
  return state.promise;
27929
28903
  };
27930
28904
  const addChatMembers = (chatId, profiles) => {
27931
- const additions = (Array.isArray(profiles) ? profiles : [profiles]).map(profileMember);
28905
+ const profileList = Array.isArray(profiles) ? profiles : [profiles];
28906
+ for (const profile of profileList) {
28907
+ if (!canInviteToGroup(profile))
28908
+ throw new Error("peer is not accepting group invitations");
28909
+ }
28910
+ const additions = profileList.map(profileMember);
27932
28911
  const blockedSet = new Set(getIdentity().blocked);
27933
28912
  for (const member of additions) {
27934
28913
  if (blockedSet.has(member.uid))
@@ -27986,7 +28965,7 @@ function createChatSessionMembership({
27986
28965
 
27987
28966
  // ../../core/chat/deletiondelivery.js
27988
28967
  "use client";
27989
- async function deliverChatDeletedPings(cloud, identityValue, epochState, members = [], eventIdValue = "") {
28968
+ async function deliverChatDeletedPings(cloud, identityValue, epochState, members = [], eventIdValue = "", routes = {}) {
27990
28969
  const identity = {
27991
28970
  ...identityValue,
27992
28971
  uid: cleanText(identityValue?.uid),
@@ -28024,6 +29003,7 @@ async function deliverChatDeletedPings(cloud, identityValue, epochState, members
28024
29003
  deliveries.push({
28025
29004
  member,
28026
29005
  promise: pushCriticalInbox(cloud, member.uid, ping, {
29006
+ admissionCapability: routes?.[member.chatPK]?.admissionCapability || null,
28027
29007
  descriptor,
28028
29008
  routeTag: notificationRouteTag(descriptor),
28029
29009
  notify: true
@@ -28227,6 +29207,7 @@ function createChatDelete({
28227
29207
  stateCapability: serverChat?.epochState?.stateCapability,
28228
29208
  epochVersion: serverChat?.epochVersion,
28229
29209
  members: serverChat?.members || [],
29210
+ routes: sourceChat?.routes || {},
28230
29211
  transitionCommitment: serverChat?.epochState?.transitionCommitment,
28231
29212
  ownEntry: sourceChat?.ownEntry || null,
28232
29213
  membershipRemoved: sourceChat?.membershipRemoved === true,
@@ -28273,7 +29254,7 @@ function createChatDelete({
28273
29254
  },
28274
29255
  stateCapability: target.stateCapability,
28275
29256
  transitionCommitment: target.transitionCommitment
28276
- }, target.members, target.expectedEpochId);
29257
+ }, target.members, target.expectedEpochId, target.routes);
28277
29258
  return result.deliveredCount;
28278
29259
  };
28279
29260
  const deleteCurrentChat = async (input, options = {}) => {
@@ -29404,6 +30385,188 @@ function createChatSessionSettings({
29404
30385
  };
29405
30386
  }
29406
30387
 
30388
+ // ../../core/chat/sessionnotifications.js
30389
+ var CHAT_ATTENTION_REFRESH_CONCURRENCY = 4;
30390
+ function currentNotificationRegistration(identity, entry, notificationMode = entry?.notificationMode) {
30391
+ const manifest = entry?.current?.manifest;
30392
+ if (!manifest || !identity?.chatPK)
30393
+ throw makeChatUnavailableError();
30394
+ const mode = normalizeChatNotificationMode(notificationMode, manifest);
30395
+ return {
30396
+ deliveryRegistration: chatDeliveryRegistration(entry.current.stateCapability, {
30397
+ attentionSecret: entry.current.epochSecret,
30398
+ chatId: manifest.chatId,
30399
+ recipientChatPK: identity.chatPK,
30400
+ generation: manifest.epochVersion,
30401
+ manifest,
30402
+ notificationMode: mode
30403
+ }),
30404
+ notificationMode: mode
30405
+ };
30406
+ }
30407
+ function entryWithCurrentNotificationRegistration(entry, notificationMode) {
30408
+ return {
30409
+ ...entry,
30410
+ deliveryRegistered: true,
30411
+ attentionRegistrationVersion: CHAT_ATTENTION_REGISTRATION_VERSION,
30412
+ notificationMode
30413
+ };
30414
+ }
30415
+ async function setOwnChatNotificationMode(cloud, identity, entryId, entry, value) {
30416
+ if (!entryId || !entry?.current || entry.retirement)
30417
+ throw makeChatUnavailableError();
30418
+ return mutateOwnChatEntry(cloud, identity, entryId, entry, (current) => {
30419
+ if (!current?.current || current.retirement)
30420
+ throw makeChatUnavailableError();
30421
+ const manifest = current.current.manifest;
30422
+ if (manifest.lineage !== "group")
30423
+ throw new Error("group chat required");
30424
+ const requestedMode = cleanText(value);
30425
+ const notificationMode = normalizeChatNotificationMode(requestedMode, manifest);
30426
+ if (notificationMode !== requestedMode) {
30427
+ throw new Error("all notifications are unavailable in large groups");
30428
+ }
30429
+ if (current.notificationMode === notificationMode && hasCurrentChatAttentionRegistration(current)) {
30430
+ return { result: current };
30431
+ }
30432
+ const { deliveryRegistration } = currentNotificationRegistration(identity, current, notificationMode);
30433
+ return {
30434
+ entry: entryWithCurrentNotificationRegistration(current, notificationMode),
30435
+ deliveryRegistration
30436
+ };
30437
+ });
30438
+ }
30439
+ async function refreshOwnChatAttentionRegistration(cloud, identity, entryId, entry) {
30440
+ if (!entryId || !entry?.current || entry.retirement)
30441
+ throw makeChatUnavailableError();
30442
+ return mutateOwnChatEntry(cloud, identity, entryId, entry, (current) => {
30443
+ if (!current?.current || current.retirement)
30444
+ throw makeChatUnavailableError();
30445
+ if (hasCurrentChatAttentionRegistration(current))
30446
+ return { result: current };
30447
+ const { deliveryRegistration, notificationMode } = currentNotificationRegistration(identity, current);
30448
+ return {
30449
+ entry: entryWithCurrentNotificationRegistration(current, notificationMode),
30450
+ deliveryRegistration
30451
+ };
30452
+ });
30453
+ }
30454
+ function createChatSessionNotifications({
30455
+ cloud,
30456
+ getGeneration,
30457
+ getIdentity,
30458
+ getOwnerChat,
30459
+ getMutationGate,
30460
+ getChatListOwner,
30461
+ loadOwnerChats = async () => [],
30462
+ writeMode = setOwnChatNotificationMode,
30463
+ writeRegistration = refreshOwnChatAttentionRegistration
30464
+ }) {
30465
+ let refreshState = null;
30466
+ let refreshedGeneration = null;
30467
+ let refreshedListOwner = null;
30468
+ const operationContext = () => ({
30469
+ generation: getGeneration(),
30470
+ identity: getIdentity(),
30471
+ listOwner: getChatListOwner(),
30472
+ mutationGate: getMutationGate()
30473
+ });
30474
+ const assertCurrent = (operation) => {
30475
+ if (getGeneration() !== operation.generation || getChatListOwner() !== operation.listOwner) {
30476
+ throw makeChatUnavailableError();
30477
+ }
30478
+ };
30479
+ const requireOwnerChat = (chatId, operation, { group = false } = {}) => {
30480
+ assertCurrent(operation);
30481
+ const chat = getOwnerChat(chatId);
30482
+ if (operation.identity.chatBanned || group && chat?.lineage !== "group" || !chat.ownEntry || chat.ownEntry.retirement || !chat.entryId) {
30483
+ throw makeChatUnavailableError();
30484
+ }
30485
+ return chat;
30486
+ };
30487
+ const setChatNotificationMode = async (chatId, mode) => {
30488
+ const operation = operationContext();
30489
+ requireOwnerChat(chatId, operation, { group: true });
30490
+ const releaseMutation = await operation.mutationGate.acquireExclusive(chatId);
30491
+ try {
30492
+ const chat = requireOwnerChat(chatId, operation, { group: true });
30493
+ const committed = await writeMode(cloud, operation.identity, chat.entryId, chat.ownEntry, mode);
30494
+ assertCurrent(operation);
30495
+ const projected = projectOwnChatEntry(committed, chat.entryId, operation.identity.chatPK, chat.ts);
30496
+ operation.listOwner.commitOwner(projected);
30497
+ return projected.notificationMode;
30498
+ } finally {
30499
+ releaseMutation();
30500
+ }
30501
+ };
30502
+ const refreshLoadedChat = async (chat, operation) => {
30503
+ assertCurrent(operation);
30504
+ if (!chat?.id || !chat.entryId || !chat.ownEntry?.current || chat.ownEntry.retirement || hasCurrentChatAttentionRegistration(chat.ownEntry)) {
30505
+ return false;
30506
+ }
30507
+ const releaseMutation = await operation.mutationGate.acquireExclusive(chat.id);
30508
+ try {
30509
+ assertCurrent(operation);
30510
+ const committed = await writeRegistration(cloud, operation.identity, chat.entryId, chat.ownEntry);
30511
+ assertCurrent(operation);
30512
+ const projected = projectOwnChatEntry(committed, chat.entryId, operation.identity.chatPK, chat.ts);
30513
+ operation.listOwner.commitOwner(projected);
30514
+ return true;
30515
+ } finally {
30516
+ releaseMutation();
30517
+ }
30518
+ };
30519
+ const startAttentionRegistrationRefresh = (operation) => {
30520
+ const state = { operation, promise: null };
30521
+ state.promise = (async () => {
30522
+ const { chatBanned, chatPK, chatPrivateKey, uid } = operation.identity;
30523
+ if (chatBanned || !uid || !chatPK || !chatPrivateKey)
30524
+ throw makeChatUnavailableError();
30525
+ const chats = await loadOwnerChats(cloud, uid, chatPK, chatPrivateKey);
30526
+ assertCurrent(operation);
30527
+ const pending = (chats || []).filter((chat) => chat?.id && chat.entryId && chat.ownEntry?.current && !chat.ownEntry.retirement && !hasCurrentChatAttentionRegistration(chat.ownEntry));
30528
+ let cursor = 0;
30529
+ let refreshed = 0;
30530
+ const failures = [];
30531
+ const worker = async () => {
30532
+ while (cursor < pending.length) {
30533
+ const chat = pending[cursor];
30534
+ cursor += 1;
30535
+ try {
30536
+ if (await refreshLoadedChat(chat, operation))
30537
+ refreshed += 1;
30538
+ } catch (error) {
30539
+ failures.push(error);
30540
+ }
30541
+ }
30542
+ };
30543
+ await Promise.all(Array.from({ length: Math.min(CHAT_ATTENTION_REFRESH_CONCURRENCY, pending.length) }, worker));
30544
+ assertCurrent(operation);
30545
+ if (failures.length)
30546
+ throw failures[0];
30547
+ refreshedGeneration = operation.generation;
30548
+ refreshedListOwner = operation.listOwner;
30549
+ return refreshed;
30550
+ })().finally(() => {
30551
+ if (refreshState === state)
30552
+ refreshState = null;
30553
+ });
30554
+ refreshState = state;
30555
+ return state.promise;
30556
+ };
30557
+ const refreshChatAttentionRegistrations = () => {
30558
+ const operation = operationContext();
30559
+ if (refreshedGeneration === operation.generation && refreshedListOwner === operation.listOwner) {
30560
+ return Promise.resolve(0);
30561
+ }
30562
+ if (refreshState && refreshState.operation.generation === operation.generation && refreshState.operation.listOwner === operation.listOwner) {
30563
+ return refreshState.promise;
30564
+ }
30565
+ return startAttentionRegistrationRefresh(operation);
30566
+ };
30567
+ return { refreshChatAttentionRegistrations, setChatNotificationMode };
30568
+ }
30569
+
29407
30570
  // ../../core/chat/sessionsources.js
29408
30571
  function normalizeChatSessionSources(sources = {}) {
29409
30572
  return {
@@ -29417,11 +30580,12 @@ function normalizeChatSessionSources(sources = {}) {
29417
30580
  chatSigningSecret: sources.chatSigningSecret || null,
29418
30581
  notificationPK: sources.notificationPK || "",
29419
30582
  notificationPrivateKey: sources.notificationPrivateKey || "",
30583
+ chatAdmission: sources.chatAdmission || null,
29420
30584
  localCache: sources.localCache || null
29421
30585
  };
29422
30586
  }
29423
30587
  function chatSessionAuthChanged(previous, next) {
29424
- return previous.uid !== next.uid || previous.chatPK !== next.chatPK || previous.chatBanned !== next.chatBanned || previous.chatPrivateKey !== next.chatPrivateKey || previous.chatSigningPK !== next.chatSigningPK || previous.chatSigningSecret !== next.chatSigningSecret || previous.notificationPK !== next.notificationPK || previous.notificationPrivateKey !== next.notificationPrivateKey || previous.localCache !== next.localCache;
30588
+ return previous.uid !== next.uid || previous.chatPK !== next.chatPK || previous.chatBanned !== next.chatBanned || previous.chatPrivateKey !== next.chatPrivateKey || previous.chatSigningPK !== next.chatSigningPK || previous.chatSigningSecret !== next.chatSigningSecret || previous.notificationPK !== next.notificationPK || previous.notificationPrivateKey !== next.notificationPrivateKey || previous.chatAdmission !== next.chatAdmission || previous.localCache !== next.localCache;
29425
30589
  }
29426
30590
 
29427
30591
  // ../../core/chat/session.js
@@ -29451,15 +30615,21 @@ function createChatSession({
29451
30615
  chatWarming = false,
29452
30616
  preloadMessageMedia,
29453
30617
  adoptLocalMessageMedia,
30618
+ refreshAttentionRegistrationsOnStart = false,
29454
30619
  chatCrypto = null,
29455
30620
  mls = null,
29456
30621
  live = null,
30622
+ maintenance,
30623
+ incomingChatDecision = null,
29457
30624
  diag = null,
29458
30625
  loadOwnerChats = loadAllChats
29459
30626
  }, initialSources = {}) {
29460
30627
  if (!cloud) {
29461
30628
  throw new Error("createChatSession requires cloud");
29462
30629
  }
30630
+ if (!maintenance?.claim || !maintenance?.open || !maintenance?.close) {
30631
+ throw new Error("createChatSession requires account message maintenance");
30632
+ }
29463
30633
  let {
29464
30634
  uid = "",
29465
30635
  blocked = [],
@@ -29471,6 +30641,7 @@ function createChatSession({
29471
30641
  chatPrivateKey = "",
29472
30642
  notificationPK = "",
29473
30643
  notificationPrivateKey = "",
30644
+ chatAdmission = null,
29474
30645
  localCache = null
29475
30646
  } = initialSources;
29476
30647
  let isActive = isForegroundAppState(appState?.currentState);
@@ -29483,6 +30654,9 @@ function createChatSession({
29483
30654
  let mlsPoolLastCheckedAt = 0;
29484
30655
  let mlsPoolGeneration = 0;
29485
30656
  let authGeneration = 0;
30657
+ let attentionRegistrationPreparation = null;
30658
+ let attentionRegistrationReadyGeneration = null;
30659
+ let attentionRegistrationReadyOwner = null;
29486
30660
  const inboxSource = () => ({
29487
30661
  uid,
29488
30662
  chatPK,
@@ -29514,6 +30688,7 @@ function createChatSession({
29514
30688
  let pendingOwner = null;
29515
30689
  let membershipOwner = null;
29516
30690
  let settingsOwner = null;
30691
+ let notificationsOwner = null;
29517
30692
  let chatListOwner = null;
29518
30693
  let unsubscribeChatList = null;
29519
30694
  let selectionIntent = 0;
@@ -29696,11 +30871,48 @@ function createChatSession({
29696
30871
  diag
29697
30872
  });
29698
30873
  const drainMembershipOutbox = () => {
29699
- if (!uid || !chatPK || !chatPrivateKey || !chatSigningPK || !chatSigningSecret)
29700
- return;
29701
- drainChatMembershipOutbox(cloud, transitionIdentity()).catch((error) => {
30874
+ if (!uid || !chatPK || !chatPrivateKey || !chatSigningPK || !chatSigningSecret) {
30875
+ return Promise.resolve(0);
30876
+ }
30877
+ return drainChatMembershipOutbox(cloud, transitionIdentity()).catch((error) => {
29702
30878
  markDiag(diag, "chat.membership.outbox.failed", { message: error?.message || String(error) });
30879
+ return 0;
30880
+ });
30881
+ };
30882
+ const refreshAttentionRegistrations = () => {
30883
+ if (!refreshAttentionRegistrationsOnStart || !isActive || chatBanned || !notificationsOwner)
30884
+ return null;
30885
+ const generation = authGeneration;
30886
+ const owner = notificationsOwner;
30887
+ if (attentionRegistrationReadyGeneration === generation && attentionRegistrationReadyOwner === owner) {
30888
+ return Promise.resolve(0);
30889
+ }
30890
+ if (attentionRegistrationPreparation?.generation === generation && attentionRegistrationPreparation.owner === owner) {
30891
+ return attentionRegistrationPreparation.promise;
30892
+ }
30893
+ const state = { generation, owner, promise: null };
30894
+ state.promise = owner.refreshChatAttentionRegistrations().then((refreshed) => {
30895
+ if (authGeneration !== generation || notificationsOwner !== owner || !isActive)
30896
+ return 0;
30897
+ attentionRegistrationReadyGeneration = generation;
30898
+ attentionRegistrationReadyOwner = owner;
30899
+ if (refreshed > 0) {
30900
+ markDiag(diag, "chat.notifications.routes.refreshed", { count: refreshed });
30901
+ }
30902
+ return refreshed;
30903
+ }).catch((error) => {
30904
+ if (authGeneration === generation && notificationsOwner === owner) {
30905
+ markDiag(diag, "chat.notifications.routes.failed", {
30906
+ message: error?.message || String(error)
30907
+ });
30908
+ }
30909
+ return 0;
30910
+ }).finally(() => {
30911
+ if (attentionRegistrationPreparation === state)
30912
+ attentionRegistrationPreparation = null;
29703
30913
  });
30914
+ attentionRegistrationPreparation = state;
30915
+ return state.promise;
29704
30916
  };
29705
30917
  const replenishMlsPool = ({ force = false } = {}) => {
29706
30918
  if (!uid || !chatPK || !chatPrivateKey || !chatSigningPK || !chatSigningSecret)
@@ -29779,6 +30991,20 @@ function createChatSession({
29779
30991
  getChatListOwner: () => chatListOwner,
29780
30992
  setLocalByChat
29781
30993
  });
30994
+ notificationsOwner = createChatSessionNotifications({
30995
+ cloud,
30996
+ getGeneration: () => authGeneration,
30997
+ getIdentity: () => ({
30998
+ uid,
30999
+ chatPK,
31000
+ chatPrivateKey,
31001
+ chatBanned
31002
+ }),
31003
+ getOwnerChat,
31004
+ getMutationGate: () => actionOwner.mutationGate,
31005
+ getChatListOwner: () => chatListOwner,
31006
+ loadOwnerChats
31007
+ });
29782
31008
  pendingOwner = createChatSessionPending({
29783
31009
  beginSelection: beginChatSelection,
29784
31010
  getIdentity: () => ({ uid, blocked, chatPK, chatPrivateKey, chatBanned }),
@@ -29786,6 +31012,7 @@ function createChatSession({
29786
31012
  setSelectedChat,
29787
31013
  publish,
29788
31014
  getOwnerChat,
31015
+ getChatRow: (chatId) => chatListOwner.getRow(chatId),
29789
31016
  ensureChat,
29790
31017
  profileMember: membershipOwner.profileMember,
29791
31018
  transitionIdentity,
@@ -29817,12 +31044,16 @@ function createChatSession({
29817
31044
  const selectChat = (chatId) => pendingOwner.selectChat(chatId, {
29818
31045
  flushPrevious: (previousChatId) => actionOwner.seenActions.flushChatRead(previousChatId)
29819
31046
  });
31047
+ const resolveChatRequest = (chatId, decision) => chatListOwner.resolveChatRequest(chatId, decision);
29820
31048
  const getPeerChatId = (...args) => launchOwner.getPeerChatId(...args);
29821
31049
  const resolvePeerChatId = (...args) => launchOwner.resolvePeerChatId(...args);
29822
31050
  const selectPeerChat = (...args) => launchOwner.selectPeerChat(...args);
29823
31051
  const openDirectChat = (...args) => launchOwner.openDirectChat(...args);
29824
31052
  const openNotesChat = (...args) => launchOwner.openNotesChat(...args);
29825
31053
  const openNewChat = (...args) => pendingOwner.openNewChat(...args);
31054
+ const directAdmissionForPeer2 = (profile) => directAdmissionForPeer(chatPrivateKey, chatPK, profile);
31055
+ const canStartDirectChat2 = (profile) => canStartDirectChat(chatPrivateKey, chatPK, profile);
31056
+ const canInviteToGroup2 = (profile) => canInviteToGroup(profile);
29826
31057
  const createGroupChat2 = (...args) => launchOwner.createGroupChat(...args);
29827
31058
  const selectLocalChat = (...args) => launchOwner.selectLocalChat(...args);
29828
31059
  const addChatMembers = (chatId, profiles) => {
@@ -29840,6 +31071,7 @@ function createChatSession({
29840
31071
  const leaveOwnedChat = (...args) => membershipOwner.leaveOwnedChat(...args);
29841
31072
  const updateChatSettings2 = gateChatMutation((...args) => settingsOwner.updateChatSettings(...args));
29842
31073
  const updateChatAvatar = gateChatMutation((...args) => settingsOwner.updateChatAvatar(...args));
31074
+ const setChatNotificationMode = gateChatMutation((...args) => notificationsOwner.setChatNotificationMode(...args));
29843
31075
  const updateMessage = (chatId, msgId, newMessage, peerChatPK) => {
29844
31076
  if (chatBanned)
29845
31077
  throw makeChatUnavailableError();
@@ -29962,6 +31194,8 @@ function createChatSession({
29962
31194
  notificationPrivateKey: identity.notificationPrivateKey,
29963
31195
  mls,
29964
31196
  blockedUids: new Set(identity.blocked),
31197
+ chatAdmission,
31198
+ incomingChatDecision,
29965
31199
  scanAllSlots: true,
29966
31200
  requireComplete: true
29967
31201
  });
@@ -30165,7 +31399,8 @@ function createChatSession({
30165
31399
  getChatPreviewKey: getChatPreviewKey2,
30166
31400
  writeChatPreview,
30167
31401
  chatCrypto,
30168
- diag
31402
+ diag,
31403
+ maintenance
30169
31404
  };
30170
31405
  const batchSources = () => ({
30171
31406
  cloud,
@@ -30179,6 +31414,7 @@ function createChatSession({
30179
31414
  chatBanned,
30180
31415
  isActive,
30181
31416
  localCache,
31417
+ maintenance,
30182
31418
  listRef: lastServerChatsRef,
30183
31419
  pendingDeleteIdsRef: actionOwner.deleteActions.pendingDeleteIdsRef,
30184
31420
  config: chatWarming,
@@ -30205,6 +31441,8 @@ function createChatSession({
30205
31441
  localCache,
30206
31442
  isActive,
30207
31443
  inboxTransport,
31444
+ chatAdmission,
31445
+ incomingChatDecision,
30208
31446
  diag,
30209
31447
  selectedChatId: stateOwner.getSelectedChatId(),
30210
31448
  selectedChatIdRef,
@@ -30260,18 +31498,23 @@ function createChatSession({
30260
31498
  stateOwner.setActions({
30261
31499
  loadMoreChats: loadMoreChats2,
30262
31500
  selectChat,
31501
+ resolveChatRequest,
30263
31502
  getPeerChatId,
30264
31503
  resolvePeerChatId,
30265
31504
  selectPeerChat,
30266
31505
  openDirectChat,
30267
31506
  openNotesChat,
30268
31507
  openNewChat,
31508
+ directAdmissionForPeer: directAdmissionForPeer2,
31509
+ canStartDirectChat: canStartDirectChat2,
31510
+ canInviteToGroup: canInviteToGroup2,
30269
31511
  createGroupChat: createGroupChat2,
30270
31512
  addChatMembers,
30271
31513
  kickChatMember,
30272
31514
  leaveChat,
30273
31515
  updateChatSettings: updateChatSettings2,
30274
31516
  updateChatAvatar,
31517
+ setChatNotificationMode,
30275
31518
  dropChat,
30276
31519
  dropUnavailableChat,
30277
31520
  deleteChat,
@@ -30393,6 +31636,7 @@ function createChatSession({
30393
31636
  if (value) {
30394
31637
  if (selectedChatIdRef.current)
30395
31638
  enterChatActivity(selectedChatIdRef.current);
31639
+ refreshAttentionRegistrations();
30396
31640
  drainMembershipOutbox();
30397
31641
  replenishMlsPool();
30398
31642
  }
@@ -30406,7 +31650,7 @@ function createChatSession({
30406
31650
  };
30407
31651
  const setSources = (nextSources = {}) => {
30408
31652
  const previousBlockedKey = [...blocked].sort().join("|");
30409
- const current = { uid, chatPK, chatBanned, chatPrivateKey, chatSigningPK, chatSigningSecret, notificationPK, notificationPrivateKey, localCache };
31653
+ const current = { uid, chatPK, chatBanned, chatPrivateKey, chatSigningPK, chatSigningSecret, notificationPK, notificationPrivateKey, chatAdmission, localCache };
30410
31654
  const next = normalizeChatSessionSources(nextSources);
30411
31655
  const authChanged = chatSessionAuthChanged(current, next);
30412
31656
  if (authChanged) {
@@ -30424,6 +31668,7 @@ function createChatSession({
30424
31668
  chatSigningSecret,
30425
31669
  notificationPK,
30426
31670
  notificationPrivateKey,
31671
+ chatAdmission,
30427
31672
  localCache
30428
31673
  } = next);
30429
31674
  inboxTransport.setSource(inboxSource());
@@ -30441,6 +31686,7 @@ function createChatSession({
30441
31686
  }
30442
31687
  publish();
30443
31688
  if (authChanged && isActive) {
31689
+ refreshAttentionRegistrations();
30444
31690
  drainMembershipOutbox();
30445
31691
  replenishMlsPool({ force: true });
30446
31692
  }
@@ -30454,10 +31700,13 @@ function createChatSession({
30454
31700
  const start = () => {
30455
31701
  if (started)
30456
31702
  return;
31703
+ maintenance.open();
30457
31704
  started = true;
30458
31705
  unsubscribeChatList = chatListOwner.subscribe(() => {
30459
31706
  stateOwner.setListSnapshot(chatListOwner.getSnapshot());
30460
31707
  publish();
31708
+ if (serverChatsReadyRef.current)
31709
+ refreshAttentionRegistrations();
30461
31710
  });
30462
31711
  stateOwner.setListSnapshot(chatListOwner.getSnapshot());
30463
31712
  publish();
@@ -30468,6 +31717,7 @@ function createChatSession({
30468
31717
  markDiag(diag, "chat.block.retire.failed", { message: error?.message || String(error) });
30469
31718
  });
30470
31719
  }
31720
+ refreshAttentionRegistrations();
30471
31721
  drainMembershipOutbox();
30472
31722
  replenishMlsPool({ force: true });
30473
31723
  if (appState?.addEventListener) {
@@ -30476,6 +31726,7 @@ function createChatSession({
30476
31726
  };
30477
31727
  const close = () => {
30478
31728
  authGeneration += 1;
31729
+ maintenance.close();
30479
31730
  settingsOwner.reset();
30480
31731
  mls?.close?.();
30481
31732
  if (!started)
@@ -33228,18 +34479,261 @@ function isPasswordStrengthAcceptable(feedback) {
33228
34479
  return feedback?.version === PASSWORD_STRENGTH_VERSION && feedback.acceptable === true;
33229
34480
  }
33230
34481
 
33231
- // ../../core/username.js
33232
- var MAX_USERNAME = USERNAME_MAX_CHARS;
33233
- var usernameStripRegex = /[^a-z0-9]/g;
33234
- var usernameRegex = new RegExp(`^[a-z0-9]{1,${MAX_USERNAME}}$`);
33235
- function normalizeUsername(value = "") {
33236
- return String(value).trim().toLowerCase();
34482
+ // ../../core/wallet/invoice.js
34483
+ var import_bech322 = __toESM(require_dist(), 1);
34484
+ var externalInvoiceType = Object.freeze({
34485
+ lightning: "lightning",
34486
+ spark: "spark"
34487
+ });
34488
+ function lightningValue(value) {
34489
+ const raw = cleanText(typeof value === "string" ? value : value?.invoice ?? value?.encodedInvoice ?? value?.bolt11);
34490
+ const lower = lowerText(raw);
34491
+ if (!lower)
34492
+ return null;
34493
+ const invoice = lower.startsWith("lightning://") ? raw.slice(12).trim() : lower.startsWith("lightning:") ? raw.slice(10).trim() : raw;
34494
+ return /^ln[a-z0-9]+$/.test(lowerText(invoice)) ? invoice : null;
33237
34495
  }
33238
- function cleanUsername(value = "") {
33239
- return normalizeUsername(value).replace(usernameStripRegex, "").slice(0, MAX_USERNAME);
34496
+ function ceilDiv(value, divisor) {
34497
+ return (value + divisor - 1n) / divisor;
33240
34498
  }
33241
- function isUsername(value = "") {
33242
- return usernameRegex.test(value);
34499
+ function wordsValue(words) {
34500
+ let value = 0;
34501
+ for (const word of words) {
34502
+ value = value * 32 + word;
34503
+ if (!Number.isSafeInteger(value))
34504
+ return null;
34505
+ }
34506
+ return value;
34507
+ }
34508
+ function lightningFields(value) {
34509
+ const invoice = lightningValue(value);
34510
+ if (!invoice)
34511
+ return null;
34512
+ try {
34513
+ const decoded = import_bech322.bech32.decode(lowerText(invoice), 5000);
34514
+ if (decoded.words.length < 7 + 104)
34515
+ return null;
34516
+ const timestampSeconds = wordsValue(decoded.words.slice(0, 7));
34517
+ if (timestampSeconds == null)
34518
+ return null;
34519
+ return {
34520
+ invoice,
34521
+ timestampSeconds,
34522
+ taggedFields: decoded.words.slice(7, -104)
34523
+ };
34524
+ } catch {
34525
+ return null;
34526
+ }
34527
+ }
34528
+ function readTaggedField(words, expectedTag) {
34529
+ let offset = 0;
34530
+ while (offset + 3 <= words.length) {
34531
+ const tag = words[offset];
34532
+ const length = words[offset + 1] * 32 + words[offset + 2];
34533
+ const start = offset + 3;
34534
+ const end = start + length;
34535
+ if (end > words.length)
34536
+ return null;
34537
+ if (tag === expectedTag)
34538
+ return words.slice(start, end);
34539
+ offset = end;
34540
+ }
34541
+ return;
34542
+ }
34543
+ function getLightningInvoiceAmountMsats(invoice) {
34544
+ const value = lowerText(invoice);
34545
+ const match = value.match(/^ln(?:bcrt|bc|tb|sb|tbs)(\d+[munp]?)?1/);
34546
+ const amount = match?.[1];
34547
+ if (!amount)
34548
+ return null;
34549
+ const unit = /[munp]$/.test(amount) ? amount.slice(-1) : "";
34550
+ const raw = unit ? amount.slice(0, -1) : amount;
34551
+ if (!/^\d+$/.test(raw))
34552
+ return null;
34553
+ const n = BigInt(raw);
34554
+ switch (unit) {
34555
+ case "m":
34556
+ return n * 100000000n;
34557
+ case "u":
34558
+ return n * 100000n;
34559
+ case "n":
34560
+ return n * 100n;
34561
+ case "p":
34562
+ return n % 10n === 0n ? n / 10n : null;
34563
+ default:
34564
+ return n * 100000000000n;
34565
+ }
34566
+ }
34567
+ function lightningAmountSats(invoice) {
34568
+ const amountMsats = getLightningInvoiceAmountMsats(invoice);
34569
+ return amountMsats == null ? null : ceilDiv(amountMsats, 1000n);
34570
+ }
34571
+ function getLightningInvoiceExpiresAtMs(value) {
34572
+ const fields = lightningFields(value);
34573
+ if (!fields)
34574
+ return null;
34575
+ const expiryWords = readTaggedField(fields.taggedFields, 6);
34576
+ if (expiryWords === null)
34577
+ return null;
34578
+ const expirySeconds = expiryWords === undefined ? 3600 : wordsValue(expiryWords);
34579
+ if (expirySeconds == null)
34580
+ return null;
34581
+ const expiresAtMs = (fields.timestampSeconds + expirySeconds) * 1000;
34582
+ return Number.isSafeInteger(expiresAtMs) ? expiresAtMs : null;
34583
+ }
34584
+ function readLightningInvoice(value) {
34585
+ const invoice = lightningValue(value);
34586
+ if (!invoice)
34587
+ return null;
34588
+ const amountSats = lightningAmountSats(invoice);
34589
+ return {
34590
+ type: externalInvoiceType.lightning,
34591
+ invoice,
34592
+ ...amountSats != null && amountSats > 0n ? { amount: amountSats.toString() } : {}
34593
+ };
34594
+ }
34595
+ function getLightningInvoiceNetwork(value) {
34596
+ const lower = lowerText(readLightningInvoice(value)?.invoice);
34597
+ if (!lower)
34598
+ return null;
34599
+ if (lower.startsWith("lnbcrt"))
34600
+ return "REGTEST";
34601
+ if (lower.startsWith("lntbs") || lower.startsWith("lnsb"))
34602
+ return "SIGNET";
34603
+ if (lower.startsWith("lntb"))
34604
+ return "TESTNET";
34605
+ if (lower.startsWith("lnbc"))
34606
+ return "MAINNET";
34607
+ return null;
34608
+ }
34609
+
34610
+ // ../../core/paymentrequest.js
34611
+ "use client";
34612
+ var PUBLIC_PAYMENT_REQUEST_VERSION = "1";
34613
+ var PUBLIC_PAYMENT_REQUEST_PATH = "/pay/";
34614
+ var PUBLIC_PAYMENT_REQUEST_EXPIRY_SECONDS = 24 * 60 * 60;
34615
+ var PUBLIC_PAYMENT_REQUEST_MAX_EXPIRY_SECONDS = 7 * 24 * 60 * 60;
34616
+ var REQUEST_ID_RE = /^[0-9a-f]{64}$/u;
34617
+ var UID_RE = /^[A-Za-z0-9_-]{1,128}$/u;
34618
+ var WALLET_PK_RE = /^(02|03)[0-9a-f]{64}$/u;
34619
+ var SIGNING_PK_RE = /^[0-9a-f]{64}$/u;
34620
+ var SIGNATURE_RE2 = /^[0-9a-f]{128}$/u;
34621
+ var INTEGER_RE = /^[1-9][0-9]*$/u;
34622
+ var ALLOWED_HOSTS = new Set([
34623
+ new URL(origins.veyl).hostname,
34624
+ new URL(origins.veylDev).hostname
34625
+ ]);
34626
+ function cleanString(value) {
34627
+ return typeof value === "string" ? value.trim() : "";
34628
+ }
34629
+ function cleanHex(value) {
34630
+ return cleanString(value).toLowerCase();
34631
+ }
34632
+ function cleanIntegerString(value) {
34633
+ const text = cleanString(typeof value === "bigint" || typeof value === "number" ? String(value) : value);
34634
+ if (!INTEGER_RE.test(text))
34635
+ return null;
34636
+ const number = Number(text);
34637
+ return Number.isSafeInteger(number) && number > 0 ? text : null;
34638
+ }
34639
+ function cleanRequestData(value = {}) {
34640
+ const v = cleanString(value.v);
34641
+ const id = cleanHex(value.id);
34642
+ const uid = cleanString(value.uid);
34643
+ const username = normalizeUsername(value.username);
34644
+ const walletPK = cleanHex(value.walletPK);
34645
+ const signingPK = cleanHex(value.signingPK);
34646
+ const amount = cleanIntegerString(value.amount);
34647
+ const invoice = readLightningInvoice(value.invoice)?.invoice?.toLowerCase() || "";
34648
+ const expiresAt = cleanIntegerString(value.expiresAt);
34649
+ let network;
34650
+ try {
34651
+ network = normalizeWalletNetwork(value.network);
34652
+ } catch {
34653
+ return null;
34654
+ }
34655
+ if (v !== PUBLIC_PAYMENT_REQUEST_VERSION || !REQUEST_ID_RE.test(id) || !UID_RE.test(uid) || !isUsername(username) || !WALLET_PK_RE.test(walletPK) || !SIGNING_PK_RE.test(signingPK) || !amount || !invoice || !expiresAt || getLightningInvoiceNetwork(invoice) !== network || readLightningInvoice(invoice)?.amount !== amount || String(Math.floor((getLightningInvoiceExpiresAtMs(invoice) || 0) / 1000)) !== expiresAt) {
34656
+ return null;
34657
+ }
34658
+ return {
34659
+ v,
34660
+ id,
34661
+ uid,
34662
+ username,
34663
+ walletPK,
34664
+ signingPK,
34665
+ network,
34666
+ amount,
34667
+ invoice,
34668
+ expiresAt
34669
+ };
34670
+ }
34671
+ function publicPaymentRequestSigningBytes(value) {
34672
+ const data = cleanRequestData(value);
34673
+ if (!data)
34674
+ throw new Error("invalid public payment request");
34675
+ return encoder.encode([
34676
+ "veyl-public-payment-request-v1",
34677
+ data.id,
34678
+ data.uid,
34679
+ data.username,
34680
+ data.walletPK,
34681
+ data.signingPK,
34682
+ data.network,
34683
+ data.amount,
34684
+ data.invoice,
34685
+ data.expiresAt
34686
+ ].join(`
34687
+ `));
34688
+ }
34689
+ function createPublicPaymentRequest(value = {}) {
34690
+ const signingKey = value.signingKey;
34691
+ const signingPK = cleanHex(signingKey?.publicKey);
34692
+ const invoiceExpiryMs = getLightningInvoiceExpiresAtMs(value.invoice);
34693
+ const data = cleanRequestData({
34694
+ v: PUBLIC_PAYMENT_REQUEST_VERSION,
34695
+ id: value.id || toHex(randomBytes3(32)),
34696
+ uid: value.uid,
34697
+ username: value.username,
34698
+ walletPK: value.walletPK,
34699
+ signingPK,
34700
+ network: value.network,
34701
+ amount: value.amountSats,
34702
+ invoice: value.invoice,
34703
+ expiresAt: invoiceExpiryMs ? String(Math.floor(invoiceExpiryMs / 1000)) : ""
34704
+ });
34705
+ if (!data)
34706
+ throw new Error("invalid public payment request");
34707
+ if (Number(data.expiresAt) * 1000 <= Number(value.nowMs ?? Date.now())) {
34708
+ throw new Error("public payment request invoice expired");
34709
+ }
34710
+ return Object.freeze({
34711
+ ...data,
34712
+ sig: signChatBytes(signingKey, publicPaymentRequestSigningBytes(data))
34713
+ });
34714
+ }
34715
+ function verifyPublicPaymentRequest(value) {
34716
+ const data = cleanRequestData(value);
34717
+ const sig = cleanHex(value?.sig);
34718
+ if (!data || !SIGNATURE_RE2.test(sig))
34719
+ return null;
34720
+ if (!verifyChatBytes(data.signingPK, sig, publicPaymentRequestSigningBytes(data)))
34721
+ return null;
34722
+ return Object.freeze({ ...data, sig });
34723
+ }
34724
+ function encodePublicPaymentRequest(value) {
34725
+ const request = verifyPublicPaymentRequest(value);
34726
+ if (!request)
34727
+ throw new Error("verified public payment request required");
34728
+ return bytesBase64Url(encoder.encode(JSON.stringify(request)));
34729
+ }
34730
+ function makePublicPaymentLink(value, options = {}) {
34731
+ const origin = cleanString(options.origin) || origins.veyl;
34732
+ const parsedOrigin = new URL(origin);
34733
+ if (parsedOrigin.protocol !== "https:" || !ALLOWED_HOSTS.has(parsedOrigin.hostname)) {
34734
+ throw new Error("invalid public payment origin");
34735
+ }
34736
+ return `${parsedOrigin.origin}${PUBLIC_PAYMENT_REQUEST_PATH}${encodePublicPaymentRequest(value)}`;
33243
34737
  }
33244
34738
 
33245
34739
  // ../../core/wallet/bitcoin.js
@@ -34152,7 +35646,7 @@ function createPeersApi({ cloud, network, avatarCache = null }) {
34152
35646
  if (!uid || expectedVersion == null || typeof avatarCache?.read !== "function")
34153
35647
  return null;
34154
35648
  try {
34155
- const cached = await avatarCache.read(uid);
35649
+ const cached = await avatarCache.read(uid, expectedVersion);
34156
35650
  const version = readAvatarVersion(cached?.version);
34157
35651
  const url = typeof cached?.url === "string" && cached.url ? cached.url : typeof cached?.source === "string" && cached.source ? cached.source : null;
34158
35652
  return version === expectedVersion && url ? url : null;
@@ -41653,8 +43147,6 @@ function requirePorts(options) {
41653
43147
  throw new Error("account cloud port required");
41654
43148
  if (!options?.defaultNetwork)
41655
43149
  throw new Error("account default network required");
41656
- if (typeof options?.setNetwork !== "function")
41657
- throw new Error("account network port required");
41658
43150
  if (!options?.vaultCrypto)
41659
43151
  throw new Error("account vault crypto port required");
41660
43152
  if (typeof options?.bootWallet !== "function")
@@ -41690,8 +43182,7 @@ function openAccount(options = {}) {
41690
43182
  vaultCrypto,
41691
43183
  bootWallet,
41692
43184
  bootChat,
41693
- openLocalCache,
41694
- setNetwork: writeNetwork
43185
+ openLocalCache
41695
43186
  } = ports;
41696
43187
  let closed = false;
41697
43188
  let attempt = 0;
@@ -41708,6 +43199,9 @@ function openAccount(options = {}) {
41708
43199
  let activeWriteTail = Promise.resolve();
41709
43200
  const sessionCloseWork = new Set;
41710
43201
  const operationBarrier = createAccountOperationCloseBarrier();
43202
+ const messageMaintenance = createAccountMessageMaintenance({
43203
+ isAccountCurrent: () => !closed
43204
+ });
41711
43205
  function revokeSession(details = {}) {
41712
43206
  const { uid = null } = details;
41713
43207
  const accountUid = uid || state?.uid || null;
@@ -41736,7 +43230,8 @@ function openAccount(options = {}) {
41736
43230
  const chat = createChatSession({
41737
43231
  ...options.chat || {},
41738
43232
  cloud,
41739
- diag: options.chat?.diag || diag
43233
+ diag: options.chat?.diag || diag,
43234
+ maintenance: messageMaintenance
41740
43235
  }, chatSources());
41741
43236
  const wallet = openWallet({
41742
43237
  ...options.wallet || {},
@@ -41765,6 +43260,7 @@ function openAccount(options = {}) {
41765
43260
  chatPrivateKey: session?.chatPrivateKey || "",
41766
43261
  notificationPK: session?.notificationPK || "",
41767
43262
  notificationPrivateKey: session?.notificationPrivateKey || "",
43263
+ chatAdmission: currentUser.chatAdmission,
41768
43264
  localCache: session?.localCache || null
41769
43265
  };
41770
43266
  }
@@ -41844,6 +43340,15 @@ function openAccount(options = {}) {
41844
43340
  await cloud.user.profile.avatar.set(uid, null);
41845
43341
  user.getSnapshot().clearAvatar?.();
41846
43342
  return { deleted: true };
43343
+ },
43344
+ async setChatAdmission(options2 = {}) {
43345
+ const uid = profileUid();
43346
+ const session = state.session;
43347
+ if (!session?.chatPrivateKey || !session?.chatPK)
43348
+ throw new Error("vault must be unlocked");
43349
+ const chatAdmission = makeChatAdmission(session.chatPrivateKey, session.chatPK, options2);
43350
+ await cloud.user.profile.admission.set(uid, chatAdmission);
43351
+ return chatAdmission;
41847
43352
  }
41848
43353
  });
41849
43354
  const push = Object.freeze({
@@ -41856,6 +43361,56 @@ function openAccount(options = {}) {
41856
43361
  return cloud.user.push.drop(payload);
41857
43362
  }
41858
43363
  });
43364
+ const payment = Object.freeze({
43365
+ async createRequest(options2 = {}) {
43366
+ const amountSats = toSafeSats(options2.amountSats);
43367
+ if (BigInt(amountSats) > REQUEST_MONEY_MAX_SATS) {
43368
+ throw new Error("payment request amount too large");
43369
+ }
43370
+ const expirySeconds = options2.expirySeconds == null ? PUBLIC_PAYMENT_REQUEST_EXPIRY_SECONDS : Number(options2.expirySeconds);
43371
+ if (!Number.isSafeInteger(expirySeconds) || expirySeconds < 60 || expirySeconds > PUBLIC_PAYMENT_REQUEST_MAX_EXPIRY_SECONDS) {
43372
+ throw new Error("payment request expiry must be between one minute and seven days");
43373
+ }
43374
+ const uid = profileUid();
43375
+ const session = state.session;
43376
+ const username = normalizeUsername(state.user?.username);
43377
+ const walletPK = session?.walletPK;
43378
+ const signingKey = session?.chatSigningSecret && session?.chatSigningPK ? { secret: session.chatSigningSecret, publicKey: session.chatSigningPK } : null;
43379
+ if (state.lockState !== "unlocked" || !session || session.closed) {
43380
+ throw new Error("unlocked vault required");
43381
+ }
43382
+ if (!isUsername(username) || !walletPK || !signingKey) {
43383
+ throw new Error("payment request identity unavailable");
43384
+ }
43385
+ const createInvoice = wallet.getSnapshot().value.createLightningInvoice;
43386
+ const result = await createInvoice({
43387
+ amountSats,
43388
+ expirySeconds,
43389
+ includeSparkInvoice: true
43390
+ });
43391
+ if (closed || operationBarrier.blocksOperations || state.uid !== uid || state.session !== session || session.closed) {
43392
+ throw new Error("account changed during payment request");
43393
+ }
43394
+ if (!result?.success || !result.invoice?.encodedInvoice) {
43395
+ throw result?.error || new Error("lightning invoice unavailable");
43396
+ }
43397
+ const request = createPublicPaymentRequest({
43398
+ uid,
43399
+ username,
43400
+ walletPK,
43401
+ signingKey,
43402
+ network: session.network || state.network,
43403
+ amountSats,
43404
+ invoice: result.invoice.encodedInvoice
43405
+ });
43406
+ const origin = cloud.environment === "dev" ? origins.veylDev : origins.veyl;
43407
+ return Object.freeze({
43408
+ request,
43409
+ link: makePublicPaymentLink(request, { origin }),
43410
+ receiveRequest: result.invoice
43411
+ });
43412
+ }
43413
+ });
41859
43414
  function emit2(patch) {
41860
43415
  if (closed || operationBarrier.suppressPublications)
41861
43416
  return state;
@@ -41913,7 +43468,13 @@ function openAccount(options = {}) {
41913
43468
  chatSession = null;
41914
43469
  closeAccountSession(session);
41915
43470
  if (session && typeof options.onSessionClosed === "function") {
41916
- const work = Promise.resolve().then(() => options.onSessionClosed({ session, uid: state.uid })).catch((error) => {
43471
+ let result;
43472
+ try {
43473
+ result = options.onSessionClosed({ session, uid: state.uid });
43474
+ } catch (error) {
43475
+ result = Promise.reject(error);
43476
+ }
43477
+ const work = Promise.resolve(result).catch((error) => {
41917
43478
  diag?.("account.session.closed.error", {
41918
43479
  code: error?.code || "",
41919
43480
  message: error?.message || String(error)
@@ -41939,13 +43500,17 @@ function openAccount(options = {}) {
41939
43500
  }
41940
43501
  return session;
41941
43502
  }
43503
+ async function drainSessionCloseWork() {
43504
+ while (sessionCloseWork.size) {
43505
+ await Promise.allSettled([...sessionCloseWork]);
43506
+ }
43507
+ }
41942
43508
  function lock() {
41943
43509
  const session = closeSession();
41944
43510
  setActive(false);
41945
43511
  return session;
41946
43512
  }
41947
43513
  async function applyNetwork(network) {
41948
- await writeNetwork(network);
41949
43514
  user.setNetwork(network);
41950
43515
  emitDomains({ network });
41951
43516
  peers.refreshNetwork();
@@ -42028,7 +43593,6 @@ function openAccount(options = {}) {
42028
43593
  writeActive(previousUid, false);
42029
43594
  active = { uid: null, value: false };
42030
43595
  user.setNetwork(defaultNetwork);
42031
- Promise.resolve(writeNetwork(defaultNetwork)).catch(() => {});
42032
43596
  const currentUser = user.getSnapshot();
42033
43597
  const currentUid = currentUser.uid || null;
42034
43598
  state = {
@@ -42046,6 +43610,11 @@ function openAccount(options = {}) {
42046
43610
  const stopUser = user.subscribe(syncUser);
42047
43611
  watchVault(state.uid);
42048
43612
  async function unlock(password, unlockOptions = {}) {
43613
+ if (closed)
43614
+ throw new Error("account closed");
43615
+ if (state.lockState !== "locked")
43616
+ throw new Error("unlock in progress");
43617
+ await drainSessionCloseWork();
42049
43618
  if (closed)
42050
43619
  throw new Error("account closed");
42051
43620
  if (state.lockState !== "locked")
@@ -42389,10 +43958,12 @@ function openAccount(options = {}) {
42389
43958
  user,
42390
43959
  profile,
42391
43960
  push,
43961
+ payment,
42392
43962
  support,
42393
43963
  chat,
42394
43964
  wallet,
42395
43965
  bitcoin,
43966
+ messageMaintenance,
42396
43967
  peers,
42397
43968
  closeBarrier: Object.freeze({
42398
43969
  acquire: operationBarrier.acquireClose,