@glyphteck/veyl 0.66.3 → 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;
@@ -7281,7 +7368,7 @@ function closeSessionResources({ wallet, chatPK, chatPrivateKey, chatSigningSecr
7281
7368
  close(vaultAccess);
7282
7369
  close(vaultSigner);
7283
7370
  close(localCache);
7284
- pending.push(lockWallet(wallet));
7371
+ lockWallet(wallet);
7285
7372
  lockChat(chatPrivateKey, chatPK);
7286
7373
  cleanBytes(chatSigningSecret, notificationPrivateKey, notificationPublicKey);
7287
7374
  return Promise.allSettled(pending).then(() => {
@@ -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),
@@ -7857,6 +8251,8 @@ function settingsState(settings = defaultSettings) {
7857
8251
  var defaultUser = {
7858
8252
  uid: null,
7859
8253
  authReady: false,
8254
+ authSessionReady: false,
8255
+ authSessionActive: false,
7860
8256
  authCredential: null,
7861
8257
  profileReady: false,
7862
8258
  username: null,
@@ -7870,6 +8266,7 @@ var defaultUser = {
7870
8266
  chatPK: null,
7871
8267
  chatSigningPK: null,
7872
8268
  notificationPK: null,
8269
+ chatAdmission: normalizeChatAdmission(null),
7873
8270
  active: false,
7874
8271
  banned: null,
7875
8272
  agreement: null,
@@ -7878,12 +8275,16 @@ var defaultUser = {
7878
8275
  settingsReady: false,
7879
8276
  settings: settingsState()
7880
8277
  };
8278
+ function isAccountAuthenticationError(error) {
8279
+ const code = typeof error?.code === "string" ? error.code.toLowerCase() : "";
8280
+ return code === "permission-denied" || code.endsWith("/permission-denied") || code === "unauthenticated" || code.endsWith("/unauthenticated");
8281
+ }
7881
8282
  function createUser({ cloud, network, avatarCache = null, diag = null, onSessionRevoked = null }) {
7882
8283
  if (!cloud) {
7883
8284
  throw new Error("createUser requires { cloud }");
7884
8285
  }
7885
- if (avatarCache && [avatarCache.read, avatarCache.write, avatarCache.remove, avatarCache.removeAll].some((method) => typeof method !== "function")) {
7886
- throw new Error("avatarCache requires read, write, remove, and removeAll");
8286
+ if (avatarCache && [avatarCache.read, avatarCache.write, avatarCache.remove].some((method) => typeof method !== "function")) {
8287
+ throw new Error("avatarCache requires read, write, and remove");
7887
8288
  }
7888
8289
  let activeNetwork = network;
7889
8290
  let state = defaultUser;
@@ -7980,7 +8381,7 @@ function createUser({ cloud, network, avatarCache = null, diag = null, onSession
7980
8381
  if (!uid || !avatarCache)
7981
8382
  return null;
7982
8383
  try {
7983
- const cached = await avatarCache.read(uid);
8384
+ const cached = await avatarCache.read(uid, expectedVersion);
7984
8385
  const version = readAvatarVersion(cached?.version);
7985
8386
  if (expectedVersion != null && version !== expectedVersion) {
7986
8387
  return null;
@@ -8021,9 +8422,6 @@ function createUser({ cloud, network, avatarCache = null, diag = null, onSession
8021
8422
  return;
8022
8423
  runCacheTask("user.avatar.cache.remove", () => avatarCache.remove(uid));
8023
8424
  }
8024
- function clearCachedAvatars() {
8025
- runCacheTask("user.avatar.cache.clear", () => avatarCache.removeAll());
8026
- }
8027
8425
  async function fetchAvatar(uid, { version = null, force = false, clear = false, persist = true } = {}) {
8028
8426
  if (!uid)
8029
8427
  return;
@@ -8106,6 +8504,54 @@ function createUser({ cloud, network, avatarCache = null, diag = null, onSession
8106
8504
  function isCurrentUser(uid, session) {
8107
8505
  return started && authSession === session && userUid === uid;
8108
8506
  }
8507
+ function revokeAuthentication(authUser, {
8508
+ code = "",
8509
+ local = false,
8510
+ reason = "session-inactive"
8511
+ } = {}) {
8512
+ if (revokingSession)
8513
+ return;
8514
+ revokingSession = true;
8515
+ let callback;
8516
+ try {
8517
+ callback = onSessionRevoked?.({
8518
+ uid: authUser.uid,
8519
+ code,
8520
+ reason
8521
+ });
8522
+ } catch (error) {
8523
+ callback = Promise.reject(error);
8524
+ }
8525
+ markDiag(diag, "user.auth.revoked", {
8526
+ code,
8527
+ local,
8528
+ reason
8529
+ });
8530
+ setState((user) => ({
8531
+ ...user,
8532
+ authSessionReady: true,
8533
+ authSessionActive: false,
8534
+ authCredential: null
8535
+ }));
8536
+ let operation = Promise.resolve(callback).catch((error) => markError(diag, "user.session.revoke.local", Date.now(), error));
8537
+ if (!local) {
8538
+ operation = operation.then(() => cloud.auth.logout()).catch((error) => markError(diag, "user.session.revoke.auth", Date.now(), error));
8539
+ }
8540
+ operation.finally(() => {
8541
+ if (userUid === authUser.uid) {
8542
+ revokingSession = false;
8543
+ }
8544
+ });
8545
+ }
8546
+ function revokeAuthenticationError(authUser, error, reason) {
8547
+ if (!isAccountAuthenticationError(error))
8548
+ return false;
8549
+ revokeAuthentication(authUser, {
8550
+ code: error?.code || "",
8551
+ reason
8552
+ });
8553
+ return true;
8554
+ }
8109
8555
  function watchUser(authUser) {
8110
8556
  if (!started)
8111
8557
  return;
@@ -8115,7 +8561,6 @@ function createUser({ cloud, network, avatarCache = null, diag = null, onSession
8115
8561
  closeUserWatches();
8116
8562
  markDiag(diag, "user.auth.state", { signedIn: !!authUser });
8117
8563
  if (!authUser) {
8118
- const signedOutUid = userUid;
8119
8564
  revokingSession = false;
8120
8565
  userUid = null;
8121
8566
  clearSettingsKey(settingsKey);
@@ -8124,17 +8569,11 @@ function createUser({ cloud, network, avatarCache = null, diag = null, onSession
8124
8569
  agreementStored = null;
8125
8570
  agreementOverride = null;
8126
8571
  agreementAcceptance = null;
8127
- if (signedOutUid) {
8128
- clearCachedAvatars();
8129
- }
8130
8572
  avatarFetch = { uid: null, key: null, promise: null };
8131
8573
  setState({ ...defaultUser, authReady: true });
8132
8574
  return;
8133
8575
  }
8134
8576
  const authUidChanged = userUid !== authUser.uid;
8135
- if (userUid && authUidChanged) {
8136
- clearCachedAvatars();
8137
- }
8138
8577
  userUid = authUser.uid;
8139
8578
  if (authUidChanged) {
8140
8579
  clearSettingsKey(settingsKey);
@@ -8164,32 +8603,28 @@ function createUser({ cloud, network, avatarCache = null, diag = null, onSession
8164
8603
  userWatches.push(cloud.auth.session.watch(authUser.uid, current((authSessionState) => {
8165
8604
  if (authSessionState?.active) {
8166
8605
  const authCredential = authSessionState.authCredential;
8167
- setState((user) => user.authCredential?.kind === authCredential?.kind && user.authCredential?.id === authCredential?.id ? user : { ...user, authCredential });
8168
- return;
8169
- }
8170
- if (revokingSession)
8171
- return;
8172
- revokingSession = true;
8173
- if (cloud.auth.session.isLocalTermination?.(authUser.uid, authSessionState?.generation)) {
8174
- Promise.resolve(onSessionRevoked?.({ uid: authUser.uid })).catch((error) => markError(diag, "user.session.revoke.local", Date.now(), error)).finally(() => {
8175
- if (userUid === authUser.uid) {
8176
- revokingSession = false;
8177
- }
8606
+ setState((user) => user.authSessionReady && user.authSessionActive && user.authCredential?.kind === authCredential?.kind && user.authCredential?.id === authCredential?.id ? user : {
8607
+ ...user,
8608
+ authSessionReady: true,
8609
+ authSessionActive: true,
8610
+ authCredential
8178
8611
  });
8179
8612
  return;
8180
8613
  }
8181
- Promise.resolve(onSessionRevoked?.({ uid: authUser.uid })).catch((error) => markError(diag, "user.session.revoke.local", Date.now(), error)).then(() => cloud.auth.logout()).catch((error) => markError(diag, "user.session.revoke.auth", Date.now(), error)).finally(() => {
8182
- if (userUid === authUser.uid) {
8183
- revokingSession = false;
8184
- }
8614
+ revokeAuthentication(authUser, {
8615
+ local: cloud.auth.session.isLocalTermination?.(authUser.uid, authSessionState?.generation) === true,
8616
+ reason: "session-inactive"
8185
8617
  });
8186
8618
  }), current((error) => {
8187
8619
  markError(diag, "user.session.listen", authStartedAt, error);
8620
+ revokeAuthenticationError(authUser, error, "session-listener");
8188
8621
  })));
8189
8622
  userWatches.push(cloud.user.admin.watch(authUser.uid, current((allowed) => {
8190
8623
  setState((user) => ({ ...user, isAdmin: allowed, adminReady: true }));
8191
8624
  }), current((error) => {
8192
8625
  markError(diag, "user.admin.listen", authStartedAt, error);
8626
+ if (revokeAuthenticationError(authUser, error, "admin-listener"))
8627
+ return;
8193
8628
  setState((user) => ({ ...user, isAdmin: false, adminReady: true }));
8194
8629
  })));
8195
8630
  userWatches.push(cloud.user.private.watch(authUser.uid, current((privateData, info = {}) => {
@@ -8213,6 +8648,8 @@ function createUser({ cloud, network, avatarCache = null, diag = null, onSession
8213
8648
  }));
8214
8649
  }), current((error) => {
8215
8650
  markError(diag, "user.settings.snapshot", authStartedAt, error);
8651
+ if (revokeAuthenticationError(authUser, error, "settings-listener"))
8652
+ return;
8216
8653
  settingsStored = null;
8217
8654
  agreementStored = null;
8218
8655
  setState((user) => ({
@@ -8225,6 +8662,8 @@ function createUser({ cloud, network, avatarCache = null, diag = null, onSession
8225
8662
  setState((user) => ({ ...user, banned: banned ?? null }));
8226
8663
  }), current((error) => {
8227
8664
  markError(diag, "user.moderation.listen", authStartedAt, error);
8665
+ if (revokeAuthenticationError(authUser, error, "moderation-listener"))
8666
+ return;
8228
8667
  setState((user) => ({ ...user, banned: null }));
8229
8668
  })));
8230
8669
  userWatches.push(cloud.user.blocked.watch(authUser.uid, current((blocked, info = {}) => {
@@ -8237,6 +8676,8 @@ function createUser({ cloud, network, avatarCache = null, diag = null, onSession
8237
8676
  });
8238
8677
  }), current((error) => {
8239
8678
  markError(diag, "user.blocked.listen", authStartedAt, error);
8679
+ if (revokeAuthenticationError(authUser, error, "blocked-listener"))
8680
+ return;
8240
8681
  setState((user) => ({ ...user, blockedReady: false }));
8241
8682
  })));
8242
8683
  userWatches.push(cloud.user.profile.watch(authUser.uid, current((profileData, info = {}) => {
@@ -8250,14 +8691,15 @@ function createUser({ cloud, network, avatarCache = null, diag = null, onSession
8250
8691
  const walletPK = resolveWalletPK(profileData, activeNetwork);
8251
8692
  const { chatPK, chatSigningPK } = readProfileChatIdentity(profileData);
8252
8693
  const notificationPK = readProfileNotificationPK(profileData);
8694
+ const chatAdmission = normalizeChatAdmission(profileData.chatAdmission);
8253
8695
  const active = profileData.active ?? false;
8254
8696
  const hasAvatarEntry = !!info.exists && Object.prototype.hasOwnProperty.call(profileData, "avatar");
8255
8697
  const avatarVersion2 = readAvatarVersion(profileData.avatar);
8256
8698
  const avatar = avatarVersion2 == null ? null : user.avatar;
8257
- 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) {
8258
8700
  return user;
8259
8701
  }
8260
- 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 };
8261
8703
  });
8262
8704
  const avatarVersion = readAvatarVersion(profileData.avatar);
8263
8705
  if (avatarVersion == null) {
@@ -8283,6 +8725,8 @@ function createUser({ cloud, network, avatarCache = null, diag = null, onSession
8283
8725
  }
8284
8726
  }), current((error) => {
8285
8727
  markError(diag, "user.profile.snapshot", authStartedAt, error);
8728
+ if (revokeAuthenticationError(authUser, error, "profile-listener"))
8729
+ return;
8286
8730
  avatarFetch = { uid: null, key: null, promise: null };
8287
8731
  setState((user) => ({
8288
8732
  ...user,
@@ -8294,6 +8738,7 @@ function createUser({ cloud, network, avatarCache = null, diag = null, onSession
8294
8738
  chatPK: null,
8295
8739
  chatSigningPK: null,
8296
8740
  notificationPK: null,
8741
+ chatAdmission: normalizeChatAdmission(null),
8297
8742
  active: false,
8298
8743
  avatarVersion: null,
8299
8744
  avatar: null,
@@ -8636,139 +9081,6 @@ async function pushCriticalInbox(cloud, recipientUid, ping, options = {}, retry
8636
9081
  }
8637
9082
  var criticalDeliveryInternals = Object.freeze({ retryableDeliveryError });
8638
9083
 
8639
- // ../../core/chat/protocol.js
8640
- "use client";
8641
- var CHAT_PROTOCOL_VERSION = 3;
8642
- var CHAT_MANIFEST_VERSION = 2;
8643
- var CHAT_TRANSITION_VERSION = 2;
8644
- var CHAT_MESSAGE_ENVELOPE_VERSION = 3;
8645
- var CHAT_SETTINGS_VERSION = 1;
8646
-
8647
- // ../../core/chat/epochs/manifest.js
8648
- "use client";
8649
- var CHAT_LINEAGES = Object.freeze({
8650
- SELF: "self",
8651
- DIRECT: "direct",
8652
- GROUP: "group"
8653
- });
8654
- var LINEAGES = new Set(Object.values(CHAT_LINEAGES));
8655
- var HEX_32_RE2 = /^[0-9a-f]{64}$/u;
8656
- function cleanChatHex(value, label = "chat value") {
8657
- const text = cleanText(value).toLowerCase();
8658
- if (!HEX_32_RE2.test(text)) {
8659
- throw new Error(`${label} required`);
8660
- }
8661
- return text;
8662
- }
8663
- function cleanUid(value) {
8664
- const uid = cleanText(value);
8665
- if (!uid || uid.length > 128) {
8666
- throw new Error("chat member uid required");
8667
- }
8668
- return uid;
8669
- }
8670
- function cleanCreatedAt(value) {
8671
- if (!Number.isSafeInteger(value) || value <= 0) {
8672
- throw new Error("chat epoch createdAt required");
8673
- }
8674
- return value;
8675
- }
8676
- function cleanEpochVersion(value) {
8677
- if (!Number.isSafeInteger(value) || value <= 0) {
8678
- throw new Error("chat epoch version required");
8679
- }
8680
- return value;
8681
- }
8682
- function cleanMember(value) {
8683
- if (!value || typeof value !== "object" || Array.isArray(value)) {
8684
- throw new Error("invalid chat member");
8685
- }
8686
- return {
8687
- uid: cleanUid(value.uid),
8688
- chatPK: cleanChatHex(value.chatPK, "member chat key"),
8689
- chatSigningPK: cleanChatHex(value.chatSigningPK, "member signing key"),
8690
- notificationPK: cleanChatHex(value.notificationPK, "member notification key"),
8691
- mlsLeafId: cleanChatHex(value.mlsLeafId, "member mls leaf id")
8692
- };
8693
- }
8694
- function assertUniqueMembers(members) {
8695
- const uids = new Set;
8696
- const chatKeys = new Set;
8697
- const signingKeys = new Set;
8698
- const mlsLeafIds = new Set;
8699
- for (const member of members) {
8700
- if (uids.has(member.uid) || chatKeys.has(member.chatPK) || signingKeys.has(member.chatSigningPK) || mlsLeafIds.has(member.mlsLeafId)) {
8701
- throw new Error("duplicate chat member");
8702
- }
8703
- uids.add(member.uid);
8704
- chatKeys.add(member.chatPK);
8705
- signingKeys.add(member.chatSigningPK);
8706
- mlsLeafIds.add(member.mlsLeafId);
8707
- }
8708
- }
8709
- function assertLineage(manifest, parent) {
8710
- const memberCount = manifest.members.length;
8711
- if (memberCount > 2 && manifest.lineage !== CHAT_LINEAGES.GROUP) {
8712
- throw new Error("group lineage required");
8713
- }
8714
- if (!parent) {
8715
- return;
8716
- }
8717
- if (manifest.chatId !== parent.chatId || manifest.parentEpochId !== parent.epochId || manifest.epochVersion !== parent.epochVersion + 1) {
8718
- throw new Error("invalid chat epoch successor");
8719
- }
8720
- if (parent.lineage === CHAT_LINEAGES.GROUP && manifest.lineage !== CHAT_LINEAGES.GROUP) {
8721
- throw new Error("chat group lineage is permanent");
8722
- }
8723
- }
8724
- function normalizeEpochManifest(value, options = {}) {
8725
- if (!value || typeof value !== "object" || Array.isArray(value)) {
8726
- throw new Error("chat manifest required");
8727
- }
8728
- if (value.v !== CHAT_MANIFEST_VERSION || value.protocol !== CHAT_PROTOCOL_VERSION) {
8729
- throw new Error("unsupported chat manifest");
8730
- }
8731
- if (!Array.isArray(value.members) || value.members.length < 1 || value.members.length > CHAT_MAX_MEMBERS) {
8732
- throw new Error("invalid chat member count");
8733
- }
8734
- const lineage = cleanText(value.lineage);
8735
- if (!LINEAGES.has(lineage)) {
8736
- throw new Error("invalid chat lineage");
8737
- }
8738
- const members = value.members.map(cleanMember).sort((a, b) => a.chatPK.localeCompare(b.chatPK));
8739
- assertUniqueMembers(members);
8740
- const manifest = {
8741
- v: CHAT_MANIFEST_VERSION,
8742
- protocol: CHAT_PROTOCOL_VERSION,
8743
- chatId: cleanChatHex(value.chatId, "chat id"),
8744
- epochId: cleanChatHex(value.epochId, "chat epoch id"),
8745
- epochVersion: cleanEpochVersion(value.epochVersion),
8746
- parentEpochId: value.parentEpochId == null ? null : cleanChatHex(value.parentEpochId, "parent epoch id"),
8747
- createdAt: cleanCreatedAt(value.createdAt),
8748
- lineage,
8749
- members
8750
- };
8751
- if (manifest.epochVersion === 1 !== (manifest.parentEpochId === null)) {
8752
- throw new Error("invalid parent epoch");
8753
- }
8754
- const bytes = canonicalBytes(manifest, "chat manifest");
8755
- if (bytes.length > CHAT_MANIFEST_MAX_BYTES) {
8756
- throw new Error("chat manifest too large");
8757
- }
8758
- assertLineage(manifest, options.parent || null);
8759
- return manifest;
8760
- }
8761
- function epochManifestDigest(manifest) {
8762
- return toHex(sha256(canonicalBytes(normalizeEpochManifest(manifest), "chat manifest digest")));
8763
- }
8764
- function manifestMember(manifest, chatPK) {
8765
- const key = cleanChatHex(chatPK, "member chat key");
8766
- return normalizeEpochManifest(manifest).members.find((member) => member.chatPK === key) || null;
8767
- }
8768
- function manifestSigningKeys(manifest) {
8769
- return Object.fromEntries(normalizeEpochManifest(manifest).members.map((member) => [member.chatPK, member.chatSigningPK]));
8770
- }
8771
-
8772
9084
  // ../../core/chat/epochs/state.js
8773
9085
  "use client";
8774
9086
  var CAPABILITY_WITNESS_SCOPE = "veyl-chat-state-witness-v3:";
@@ -9980,6 +10292,7 @@ function epochChatSettingsId(epochState) {
9980
10292
  "use client";
9981
10293
  var CHAT_ENTRY_VERSION = 4;
9982
10294
  var CHAT_OWNER_EPOCH_ENTRY_VERSION = 2;
10295
+ var CHAT_NOTIFICATION_PREFERENCE_VERSION = 1;
9983
10296
  var CHAT_OWNER_RETIREMENT_KINDS = Object.freeze({
9984
10297
  LEAVE: "leave",
9985
10298
  REMOVED: "removed"
@@ -10001,6 +10314,16 @@ function entryKey(chatPrivateKey, entryId) {
10001
10314
  function entryAad(entryId) {
10002
10315
  return canonicalBytes({ v: CHAT_ENTRY_VERSION, protocol: CHAT_PROTOCOL_VERSION, entryId }, "chat entry aad");
10003
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
+ }
10004
10327
  function ownerEpochKey(chatPrivateKey, entryId, epochEntryId) {
10005
10328
  return deriveKey(toBytes32(chatPrivateKey, "chat private key"), "user-chat-epoch-entry-v3", [entryId, epochEntryId]);
10006
10329
  }
@@ -10036,12 +10359,13 @@ function normalizeRoutes(value) {
10036
10359
  }
10037
10360
  const uid = cleanText(route.uid);
10038
10361
  const deliveryCapability = route.deliveryCapability == null ? null : cleanChatHex(route.deliveryCapability, "delivery capability");
10362
+ const admissionCapability = route.admissionCapability == null ? null : cleanChatHex(route.admissionCapability, "admission capability");
10039
10363
  const notificationPK = route.notificationPK == null ? null : cleanChatHex(route.notificationPK, "notification key");
10040
10364
  const generation = Number.isSafeInteger(route.generation) && route.generation > 0 ? route.generation : 1;
10041
10365
  if (!uid || uid.length > 128) {
10042
10366
  throw new Error("chat route uid required");
10043
10367
  }
10044
- routes[chatPK] = { uid, deliveryCapability, notificationPK, generation };
10368
+ routes[chatPK] = { uid, deliveryCapability, admissionCapability, notificationPK, generation };
10045
10369
  }
10046
10370
  return routes;
10047
10371
  }
@@ -10098,6 +10422,32 @@ function normalizeOwnerEntry(value) {
10098
10422
  retirement: normalizeOwnerRetirement(value.retirement, current)
10099
10423
  };
10100
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
+ }
10101
10451
  async function sealOwnChatEntry(chatPrivateKey, entryId, entry) {
10102
10452
  const key = entryKey(chatPrivateKey, entryId);
10103
10453
  try {
@@ -10111,14 +10461,14 @@ async function openOwnChatEntry(chatPrivateKey, entryId, body) {
10111
10461
  const key = entryKey(chatPrivateKey, entryId);
10112
10462
  try {
10113
10463
  const { nonce, ct } = unpackBodyData(body);
10114
- return normalizeOwnerEntry(await openJson(key, nonce, ct, entryAad(entryId)));
10464
+ return withNotificationPreference(normalizeOwnerEntry(await openJson(key, nonce, ct, entryAad(entryId))));
10115
10465
  } finally {
10116
10466
  cleanBytes(key);
10117
10467
  }
10118
10468
  }
10119
10469
  function makeOwnChatEntry(epoch, fields = {}) {
10120
10470
  const manifest = normalizeEpochManifest(epoch?.manifest);
10121
- return normalizeOwnerEntry({
10471
+ const entry = normalizeOwnerEntry({
10122
10472
  v: CHAT_ENTRY_VERSION,
10123
10473
  protocol: CHAT_PROTOCOL_VERSION,
10124
10474
  ownerRevision: Number.isSafeInteger(fields.ownerRevision) && fields.ownerRevision > 0 ? fields.ownerRevision : 1,
@@ -10139,6 +10489,48 @@ function makeOwnChatEntry(epoch, fields = {}) {
10139
10489
  notificationTag: cleanText(fields.notificationTag) || null,
10140
10490
  retirement: fields.retirement || null
10141
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);
10142
10534
  }
10143
10535
  function normalizeOwnerEpochRecord(value) {
10144
10536
  if (!value || typeof value !== "object" || Array.isArray(value) || value.v !== CHAT_OWNER_EPOCH_ENTRY_VERSION || value.protocol !== CHAT_PROTOCOL_VERSION) {
@@ -10210,6 +10602,7 @@ async function prepareMutation(identity, entryId, current, update) {
10210
10602
  entryId,
10211
10603
  record: {
10212
10604
  body: await sealOwnChatEntry(identity.chatPrivateKey, entryId, entry),
10605
+ notificationBody: await sealOwnChatNotificationPreference(identity.chatPrivateKey, entryId, entry),
10213
10606
  revision: entry.ownerRevision,
10214
10607
  ...Number.isFinite(prepared.tsMs) ? { tsMs: prepared.tsMs } : {},
10215
10608
  ...prepared.touchTs === true ? { touchTs: true } : {}
@@ -10237,7 +10630,7 @@ async function prepareOwnChatMutation(identity, entryId, current, update) {
10237
10630
  if (!record?.body || !Number.isSafeInteger(record.revision) || record.revision < 1) {
10238
10631
  throw new Error("current chat owner record required");
10239
10632
  }
10240
- const opened = await openOwnChatEntry(identity.chatPrivateKey, entryId, record.body);
10633
+ const opened = await openOwnChatRecord(identity.chatPrivateKey, entryId, record);
10241
10634
  if (opened.ownerRevision !== record.revision) {
10242
10635
  throw new Error("chat owner revision mismatch");
10243
10636
  }
@@ -10273,6 +10666,7 @@ async function retireOwnChatEntry(cloud, identity, entryId, current, fields = {}
10273
10666
  ...latest,
10274
10667
  routes: {},
10275
10668
  deliveryRegistered: false,
10669
+ attentionRegistrationVersion: 0,
10276
10670
  notificationTag: null,
10277
10671
  retirement
10278
10672
  },
@@ -10286,7 +10680,7 @@ async function openOwnChatMutationEntry(identity, entryId, record) {
10286
10680
  return null;
10287
10681
  if (!record?.body || !Number.isSafeInteger(record.revision) || record.revision < 1)
10288
10682
  throw new Error("current chat owner record required");
10289
- const current = await openOwnChatEntry(identity.chatPrivateKey, entryId, record.body);
10683
+ const current = await openOwnChatRecord(identity.chatPrivateKey, entryId, record);
10290
10684
  if (current.ownerRevision !== record.revision) {
10291
10685
  throw new Error("chat owner revision mismatch");
10292
10686
  }
@@ -10391,6 +10785,166 @@ var MAX_REACTIONS = CHAT_MAX_REACTIONS;
10391
10785
  var HOLD_VISIBLE_KEY = "__holdVisible";
10392
10786
  var SOURCE_GONE_VISIBLE_KEY = "__sourceGoneVisible";
10393
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
+
10394
10948
  // ../../core/chat/messages/text.js
10395
10949
  var DOMAIN_LABEL_PATTERN = "[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?";
10396
10950
  var DOMAIN_TLD_PATTERN = "(?:[a-z]{2,63}|xn--[a-z0-9-]{2,59})";
@@ -12239,7 +12793,6 @@ function getDisplayMessages(messages, selfChatPublicKey, peerChatPublicKey, opti
12239
12793
 
12240
12794
  // ../../core/chat/messages/compact.js
12241
12795
  "use client";
12242
- var pendingCompactKeys = new Set;
12243
12796
  function ownsMessageCompaction(memberChatPKs, chatPK) {
12244
12797
  const members = [...new Set((memberChatPKs || []).filter(Boolean))].sort();
12245
12798
  return !!chatPK && members[0] === chatPK;
@@ -12315,39 +12868,45 @@ function compactKey(chatId, message) {
12315
12868
  const id = cleanText(message?.id);
12316
12869
  return chatId && id ? `${chatId}/${id}` : "";
12317
12870
  }
12318
- function claimMessages(chatId, messages) {
12871
+ function claimMessages(maintenance, chatId, messages) {
12319
12872
  const claimed = [];
12320
12873
  for (const message of messages || []) {
12321
12874
  const key = compactKey(chatId, message);
12322
- if (!key || pendingCompactKeys.has(key))
12875
+ if (!key)
12323
12876
  continue;
12324
- pendingCompactKeys.add(key);
12325
- claimed.push(message);
12877
+ const lease = maintenance.claim("message-compaction", key);
12878
+ if (!lease)
12879
+ continue;
12880
+ claimed.push({ lease, message });
12326
12881
  }
12327
12882
  return claimed;
12328
12883
  }
12329
- function releaseMessages(chatId, messages) {
12330
- for (const message of messages || []) {
12331
- const key = compactKey(chatId, message);
12332
- if (key)
12333
- pendingCompactKeys.delete(key);
12334
- }
12884
+ function releaseMessages(claimed) {
12885
+ for (const item of claimed || [])
12886
+ item.lease.release();
12335
12887
  }
12336
- async function compactMessages({ chatId, messages, deletedKeys, protectedKeys, deleteMessages, scannedKeys, scanComplete }) {
12888
+ async function compactMessages({ chatId, maintenance, messages, deletedKeys, protectedKeys, deleteMessages, scannedKeys, scanComplete }) {
12337
12889
  if (!chatId || typeof deleteMessages !== "function")
12338
12890
  return [];
12339
- const targets = claimMessages(chatId, getCompactMessages(messages, { deletedKeys, protectedKeys, scannedKeys, scanComplete }));
12340
- 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)
12341
12895
  return [];
12896
+ const targets = claimed.map((item) => item.message);
12342
12897
  try {
12898
+ for (const item of claimed)
12899
+ item.lease.assertCurrent();
12343
12900
  await deleteMessages(targets);
12901
+ for (const item of claimed)
12902
+ item.lease.assertCurrent();
12344
12903
  if (deletedKeys?.add) {
12345
12904
  for (const message of targets)
12346
12905
  addMessageKeys(deletedKeys, message);
12347
12906
  }
12348
12907
  return targets;
12349
12908
  } finally {
12350
- releaseMessages(chatId, targets);
12909
+ releaseMessages(claimed);
12351
12910
  }
12352
12911
  }
12353
12912
 
@@ -12879,20 +13438,23 @@ function ownerPreview(senderChatPK, message, messageId, head, tsMs, ttlMs) {
12879
13438
  }
12880
13439
  async function ensureDeliveryRoute(cloud, identity, epochState, entry, shouldPing) {
12881
13440
  if (!shouldPing)
12882
- return { capability: "", registered: entry?.deliveryRegistered === true };
13441
+ return { capability: "", registered: hasCurrentChatAttentionRegistration(entry) };
12883
13442
  if (!identity.notificationPK) {
12884
13443
  throw new Error("notification identity required");
12885
13444
  }
12886
- const capability = deriveChatDeliveryCapability(epochState.stateCapability, {
13445
+ const registration = chatDeliveryRegistration(epochState.stateCapability, {
13446
+ attentionSecret: epochState.epochSecret,
12887
13447
  chatId: epochState.manifest.chatId,
12888
13448
  recipientChatPK: identity.chatPK,
12889
- generation: epochState.manifest.epochVersion
13449
+ generation: epochState.manifest.epochVersion,
13450
+ manifest: epochState.manifest,
13451
+ notificationMode: entry?.notificationMode
12890
13452
  });
12891
- if (entry?.deliveryRegistered === true) {
12892
- return { capability, registered: true };
13453
+ if (hasCurrentChatAttentionRegistration(entry)) {
13454
+ return { capability: registration.capability, registered: true };
12893
13455
  }
12894
- const registered = await cloud.delivery?.register?.(capability).then(() => true, () => false) || false;
12895
- return { capability: registered ? capability : "", registered };
13456
+ const registered = await cloud.delivery?.register?.(registration).then(() => true, () => false) || false;
13457
+ return { capability: registered ? registration.capability : "", registered };
12896
13458
  }
12897
13459
  function mergedRoutes(entry, options, epochState) {
12898
13460
  const routes = {
@@ -12906,6 +13468,7 @@ function mergedRoutes(entry, options, epochState) {
12906
13468
  ...routes[member.chatPK] || {},
12907
13469
  uid: member.uid,
12908
13470
  notificationPK: member.notificationPK,
13471
+ admissionCapability: routes[member.chatPK]?.admissionCapability || null,
12909
13472
  deliveryCapability: deriveChatDeliveryCapability(epochState.stateCapability, {
12910
13473
  chatId: manifest.chatId,
12911
13474
  recipientChatPK: member.chatPK,
@@ -12924,6 +13487,7 @@ async function makeOwnerEntryMutation(identity, epochState, existing, routes, fi
12924
13487
  ...existing,
12925
13488
  routes,
12926
13489
  deliveryRegistered: fields.deliveryRegistered || existing.deliveryRegistered,
13490
+ attentionRegistrationVersion: fields.deliveryRegistered ? CHAT_ATTENTION_REGISTRATION_VERSION : existing.attentionRegistrationVersion,
12927
13491
  notificationTag: notificationChatTag(epochState.stateCapability, chatId, identity.chatPK),
12928
13492
  ...fields.settings ? {
12929
13493
  current: { ...existing.current, settings: normalizeChatSettingsProjection(fields.settings) }
@@ -12947,12 +13511,13 @@ async function makeOwnerEntryMutation(identity, epochState, existing, routes, fi
12947
13511
  ...current,
12948
13512
  routes: { ...current.routes, ...routes },
12949
13513
  deliveryRegistered: fields.deliveryRegistered || current.deliveryRegistered,
13514
+ attentionRegistrationVersion: fields.deliveryRegistered ? CHAT_ATTENTION_REGISTRATION_VERSION : current.attentionRegistrationVersion,
12950
13515
  notificationTag: notificationChatTag(epochState.stateCapability, chatId, identity.chatPK),
12951
13516
  ...fields.settings ? {
12952
13517
  current: { ...current.current, settings: normalizeChatSettingsProjection(fields.settings) }
12953
13518
  } : {}
12954
13519
  };
12955
- 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;
12956
13521
  return unchanged ? { result: current } : {
12957
13522
  entry: next,
12958
13523
  ...Number.isFinite(fields.tsMs) ? { tsMs: fields.tsMs } : {}
@@ -12992,16 +13557,20 @@ async function makeChatRecipientDeliveries(identity, epochState, routes, message
12992
13557
  if (!fields.shouldPing || fields.deliverRecipients === false)
12993
13558
  return [];
12994
13559
  const capabilities = chatEpochCapabilities(epochState);
12995
- const targetedRecipient = (fields.recipientChatPKs || []).map(cleanText).find(Boolean) || "";
12996
- const critical = fields.deliveryClass === "critical";
12997
- 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)
12998
13565
  return [];
12999
13566
  const deliveries = [];
13000
13567
  for (const member of epochState.manifest.members) {
13001
13568
  if (member.chatPK === identity.chatPK)
13002
13569
  continue;
13003
- if (!critical && !capabilities.ordinaryDelivery && member.chatPK !== targetedRecipient)
13570
+ if (!rotation && !capabilities.ordinaryDelivery && !targetedRecipients.has(member.chatPK))
13004
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;
13005
13574
  const route = routes[member.chatPK] || {};
13006
13575
  const descriptor = await sealNotificationDescriptor(member.notificationPK, {
13007
13576
  peerTag: notificationChatTag(epochState.stateCapability, epochState.manifest.chatId, member.chatPK),
@@ -13021,7 +13590,10 @@ async function makeChatRecipientDeliveries(identity, epochState, routes, message
13021
13590
  });
13022
13591
  deliveries.push({
13023
13592
  recipientChatPK: member.chatPK,
13024
- ...route.deliveryCapability ? { capability: route.deliveryCapability } : { recipientUid: member.uid },
13593
+ ...route.deliveryCapability ? { capability: route.deliveryCapability } : {
13594
+ recipientUid: member.uid,
13595
+ admissionCapability: route.admissionCapability || null
13596
+ },
13025
13597
  deliveryId: deriveChatInboxDeliveryId(epochState.stateCapability, {
13026
13598
  chatId: epochState.manifest.chatId,
13027
13599
  recipientChatPK: member.chatPK,
@@ -13030,7 +13602,14 @@ async function makeChatRecipientDeliveries(identity, epochState, routes, message
13030
13602
  ping,
13031
13603
  descriptor,
13032
13604
  routeTag: notificationRouteTag(descriptor),
13033
- 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
13034
13613
  });
13035
13614
  }
13036
13615
  return deliveries;
@@ -13048,9 +13627,14 @@ async function prepareMsgRecord(identity, epochState, message, options = {}) {
13048
13627
  }
13049
13628
  const chatRetention = cleanChatRetention(options.retention ?? epochState.settings.values.retention);
13050
13629
  const tsMs = Date.now();
13051
- 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
+ });
13052
13635
  const retention = getMessageRetention(messagePayload, chatRetention);
13053
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: [] };
13054
13638
  const { head, body } = await sealMsg(epoch, messagePayload, {
13055
13639
  op: actionOp,
13056
13640
  target: options.actionTarget,
@@ -13065,6 +13649,7 @@ async function prepareMsgRecord(identity, epochState, message, options = {}) {
13065
13649
  cid: head.cid,
13066
13650
  record: { lane: epoch.messageLane, head, body, ttlMs },
13067
13651
  message: ownerPreview(identity.chatPK, messagePayload, messageId, head, tsMs, ttlMs),
13652
+ mentionRecipientChatPKs: mentionTargets.recipientChatPKs,
13068
13653
  tsMs
13069
13654
  };
13070
13655
  } finally {
@@ -13097,6 +13682,7 @@ async function sendMsg(cloud, senderChatPK, senderPrivateKey, _receiverChatPK, m
13097
13682
  msgId: messageId,
13098
13683
  record,
13099
13684
  message: sentMessage,
13685
+ mentionRecipientChatPKs,
13100
13686
  tsMs
13101
13687
  } = prepared;
13102
13688
  const updatePreview = options.updatePreview !== false;
@@ -13105,7 +13691,7 @@ async function sendMsg(cloud, senderChatPK, senderPrivateKey, _receiverChatPK, m
13105
13691
  const routes = mergedRoutes(options.ownEntry, options, epochState);
13106
13692
  const preview = updatePreview ? sentMessage : null;
13107
13693
  const settingsProjection = options.settingsProjection ? normalizeChatSettingsProjection(options.settingsProjection) : null;
13108
- const ownerStateChanged = !options.ownEntry || settingsProjection || shouldPing && routeState.registered && options.ownEntry?.deliveryRegistered !== true;
13694
+ const ownerStateChanged = !options.ownEntry || settingsProjection || shouldPing && routeState.registered && !hasCurrentChatAttentionRegistration(options.ownEntry);
13109
13695
  const ownerMutation = ownerStateChanged ? await makeOwnerEntryMutation(identity, epochState, options.ownEntry, routes, {
13110
13696
  settings: settingsProjection,
13111
13697
  immediate: !!settingsProjection || options.ownerImmediate === true,
@@ -13118,13 +13704,18 @@ async function sendMsg(cloud, senderChatPK, senderPrivateKey, _receiverChatPK, m
13118
13704
  kind: pingKind,
13119
13705
  tsMs
13120
13706
  });
13707
+ const deliveryRecipientChatPKs = [
13708
+ ...options.deliveryRecipientChatPKs || [],
13709
+ ...mentionRecipientChatPKs
13710
+ ];
13121
13711
  const deliveries = await makeChatRecipientDeliveries(identity, epochState, routes, messageId, {
13122
13712
  shouldPing,
13123
13713
  deliverRecipients: options.deliverRecipients !== false,
13124
13714
  kind: pingKind,
13125
13715
  ownDeliveryCapability: routeState.capability,
13126
- notify: options.notify !== false,
13127
- recipientChatPKs: options.deliveryRecipientChatPKs,
13716
+ silent: options.notify === false,
13717
+ recipientChatPKs: deliveryRecipientChatPKs,
13718
+ attentionRecipientChatPKs: mentionRecipientChatPKs,
13128
13719
  tsMs
13129
13720
  });
13130
13721
  const write = await cloud.chat.messages.send({
@@ -13746,14 +14337,19 @@ function normalizeDelivery(value) {
13746
14337
  throw new Error("invalid chat membership delivery mode");
13747
14338
  const recipientUid = cleanText(value.recipientUid);
13748
14339
  const capability = cleanText(value.capability).toLowerCase();
14340
+ const admissionCapability = cleanText(value.admissionCapability).toLowerCase();
13749
14341
  if (mode === "critical" && !recipientUid || mode === "established" && !/^[0-9a-f]{64}$/u.test(capability)) {
13750
14342
  throw new Error("invalid chat membership delivery target");
13751
14343
  }
14344
+ if (admissionCapability && !/^[0-9a-f]{64}$/u.test(admissionCapability)) {
14345
+ throw new Error("invalid chat admission capability");
14346
+ }
13752
14347
  return {
13753
14348
  mode,
13754
14349
  recipientUid,
13755
14350
  recipientChatPK: cleanChatHex(value.recipientChatPK, "membership recipient"),
13756
14351
  capability,
14352
+ admissionCapability,
13757
14353
  deliveryId: cleanChatHex(value.deliveryId, "membership delivery id"),
13758
14354
  ping: openPing(value.ping),
13759
14355
  descriptor: openDescriptor(value.descriptor),
@@ -13964,9 +14560,11 @@ function successorManifest(parent, members, epochId) {
13964
14560
  members
13965
14561
  }, { parent, official: true });
13966
14562
  }
13967
- function nextRoutes(entry, manifest, stateCapability) {
14563
+ function nextRoutes(entry, manifest, stateCapability, privateMembers = []) {
14564
+ const privateByChatPK = new Map(privateMembers.map((member) => [member.chatPK, member]));
13968
14565
  return Object.fromEntries(manifest.members.map((member) => {
13969
14566
  const current = entry.routes?.[member.chatPK] || {};
14567
+ const privateMember = privateByChatPK.get(member.chatPK) || {};
13970
14568
  return [member.chatPK, {
13971
14569
  uid: member.uid,
13972
14570
  deliveryCapability: deriveChatDeliveryCapability(stateCapability, {
@@ -13975,6 +14573,7 @@ function nextRoutes(entry, manifest, stateCapability) {
13975
14573
  generation: manifest.epochVersion
13976
14574
  }),
13977
14575
  notificationPK: member.notificationPK || current.notificationPK || null,
14576
+ admissionCapability: privateMember.admissionCapability || current.admissionCapability || null,
13978
14577
  generation: manifest.epochVersion
13979
14578
  }];
13980
14579
  }));
@@ -14023,7 +14622,11 @@ async function makeMlsWelcomeDeliveries(identity, nextState2, addedMembers, opti
14023
14622
  senderUid: identity.uid,
14024
14623
  messageId,
14025
14624
  transitionMessageId: nextState2.transitionMessageId,
14026
- deliveryCapability: null,
14625
+ deliveryCapability: deriveChatDeliveryCapability(nextState2.stateCapability, {
14626
+ chatId: nextState2.manifest.chatId,
14627
+ recipientChatPK: identity.chatPK,
14628
+ generation: nextState2.manifest.epochVersion
14629
+ }),
14027
14630
  notificationPK: identity.notificationPK,
14028
14631
  transitionCommitment: nextState2.transitionCommitment,
14029
14632
  settingsId,
@@ -14044,6 +14647,7 @@ async function makeMlsWelcomeDeliveries(identity, nextState2, addedMembers, opti
14044
14647
  ping,
14045
14648
  descriptor,
14046
14649
  routeTag,
14650
+ admissionCapability: member.admissionCapability || null,
14047
14651
  member
14048
14652
  });
14049
14653
  }
@@ -14058,7 +14662,7 @@ async function makeTransitionWakes(identity, parentState, entry, messageId, tsMs
14058
14662
  shouldPing: true,
14059
14663
  deliverRecipients: true,
14060
14664
  kind: "transition",
14061
- deliveryClass: "critical",
14665
+ deliveryClass: "rotation",
14062
14666
  ownDeliveryCapability: "",
14063
14667
  notify: true,
14064
14668
  tsMs
@@ -14070,14 +14674,18 @@ async function makeTransitionWakes(identity, parentState, entry, messageId, tsMs
14070
14674
  }
14071
14675
  async function deliverMembershipRecord(cloud, delivery) {
14072
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
+ }
14073
14680
  await cloud.inbox.deliver(delivery.capability, delivery.ping, {
14681
+ attentionProof: delivery.attentionProof,
14074
14682
  descriptor: delivery.descriptor,
14075
- routeTag: delivery.routeTag,
14076
- notify: true
14683
+ routeTag: delivery.routeTag
14077
14684
  });
14078
14685
  return;
14079
14686
  }
14080
14687
  await pushCriticalInbox(cloud, delivery.recipientUid || delivery.member?.uid, delivery.ping, {
14688
+ admissionCapability: delivery.admissionCapability || null,
14081
14689
  deliveryId: delivery.deliveryId,
14082
14690
  descriptor: delivery.descriptor,
14083
14691
  routeTag: delivery.routeTag,
@@ -14107,6 +14715,46 @@ async function deliverQueuedMembership(cloud, identity, deliveries) {
14107
14715
  deliveredMembers: delivered.map((delivery) => delivery.member).filter(Boolean)
14108
14716
  };
14109
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
+ }
14110
14758
  async function drainChatMembershipOutbox(cloud, identity, options = {}) {
14111
14759
  const records = await cloud.user.chats.membershipOutbox.list(identity.uid, { count: options.count || 20 });
14112
14760
  const committedPackages = new Map;
@@ -14119,13 +14767,15 @@ async function drainChatMembershipOutbox(cloud, identity, options = {}) {
14119
14767
  }
14120
14768
  const packageKey = `${opened.chatId}:${opened.packageId}:${opened.packageDigest}`;
14121
14769
  if (!committedPackages.has(packageKey)) {
14122
- const mlsPackage = await cloud.chat.mls.packages.read(opened.chatId, opened.packageId).catch(() => null);
14123
- 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);
14124
14772
  }
14125
- if (!committedPackages.get(packageKey))
14773
+ const mlsPackage = committedPackages.get(packageKey);
14774
+ if (!mlsPackage)
14126
14775
  continue;
14127
14776
  try {
14128
- 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)));
14129
14779
  delivered += results.filter((result) => result.status === "fulfilled").length;
14130
14780
  if (results.every((result) => result.status === "fulfilled")) {
14131
14781
  await cloud.user.chats.membershipOutbox.delete(identity.uid, record.id);
@@ -14257,13 +14907,6 @@ async function prepareMembershipTransition(context) {
14257
14907
  });
14258
14908
  if (transitionMessage.msgId !== transitionMessageId)
14259
14909
  throw new Error("chat transition message id mismatch");
14260
- runtime.ownDeliveryCapability = deriveChatDeliveryCapability(runtime.stateCapability, {
14261
- chatId: parent.chatId,
14262
- recipientChatPK: identity.chatPK,
14263
- generation: next.epochVersion
14264
- });
14265
- runtime.stage = "register-delivery";
14266
- await cloud.delivery.register(runtime.ownDeliveryCapability);
14267
14910
  runtime.stage = "prepare-deliveries";
14268
14911
  const [wakeDeliveries, welcomeDeliveries] = await Promise.all([
14269
14912
  makeTransitionWakes(identity, parentState, entry, transitionMessage.msgId, transitionMessage.tsMs),
@@ -14290,16 +14933,26 @@ async function prepareMembershipTransition(context) {
14290
14933
  if (current.current.manifest.epochId !== parent.epochId || current.current.manifest.epochVersion !== parent.epochVersion) {
14291
14934
  throw new Error("chat owner epoch changed");
14292
14935
  }
14936
+ const notificationMode = normalizeChatNotificationMode(current.notificationMode, next);
14293
14937
  const nextEntry = makeOwnChatEntry(candidateState, {
14294
14938
  ownerRevision: current.ownerRevision,
14295
- routes: nextRoutes(current, next, runtime.stateCapability),
14939
+ routes: nextRoutes(current, next, runtime.stateCapability, delta.members),
14296
14940
  saved: current.saved,
14297
14941
  startMs: current.startMs,
14298
14942
  deliveryRegistered: true,
14943
+ notificationMode,
14299
14944
  notificationTag: notificationChatTag(runtime.stateCapability, parent.chatId, identity.chatPK)
14300
14945
  });
14301
14946
  return {
14302
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
+ }),
14303
14956
  ...Number.isFinite(fields.ownerTsMs) ? { tsMs: fields.ownerTsMs } : { touchTs: true },
14304
14957
  epochs: [{ id: historyId, record: historyRecord }],
14305
14958
  mlsWrites: [{ id: ownerMlsState.stateId, value: ownerMlsState.value }],
@@ -14442,7 +15095,6 @@ async function transitionChatEpoch(cloud, identityValue, entry, fields = {}) {
14442
15095
  epochSecret: null,
14443
15096
  metrics: {},
14444
15097
  nextSnapshot: null,
14445
- ownDeliveryCapability: "",
14446
15098
  parentSnapshot: null,
14447
15099
  stage: "read-owner-mls",
14448
15100
  stateCapability: null
@@ -14470,9 +15122,6 @@ async function transitionChatEpoch(cloud, identityValue, entry, fields = {}) {
14470
15122
  });
14471
15123
  throw error;
14472
15124
  } finally {
14473
- if (runtime.ownDeliveryCapability && !runtime.committed) {
14474
- await cloud.delivery.revoke(runtime.ownDeliveryCapability).catch(() => false);
14475
- }
14476
15125
  cleanBytes(runtime.parentSnapshot, runtime.nextSnapshot, runtime.epochSecret, runtime.stateCapability);
14477
15126
  }
14478
15127
  }
@@ -14865,7 +15514,7 @@ function sortedUniqueValues(items) {
14865
15514
 
14866
15515
  // ../../core/chat/chats.js
14867
15516
  "use client";
14868
- var HEX_32_RE3 = /^[0-9a-f]{32}$/i;
15517
+ var HEX_32_RE4 = /^[0-9a-f]{32}$/i;
14869
15518
  var HEX_64_RE = /^[0-9a-f]{64}$/i;
14870
15519
  function sameSigningKeys(left, right) {
14871
15520
  if (left === right)
@@ -14889,7 +15538,7 @@ function sameMembers(left, right) {
14889
15538
  function sameChatShape(a, b) {
14890
15539
  if (!a || !b)
14891
15540
  return a === b;
14892
- 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);
14893
15542
  }
14894
15543
  function sameChats(prev, next) {
14895
15544
  if (prev.length !== next.length)
@@ -14963,7 +15612,7 @@ function chatVersionKey(chat) {
14963
15612
  return "";
14964
15613
  }
14965
15614
  function isHex32(value) {
14966
- return HEX_32_RE3.test(cleanText(value));
15615
+ return HEX_32_RE4.test(cleanText(value));
14967
15616
  }
14968
15617
  function isHex64(value) {
14969
15618
  return HEX_64_RE.test(cleanText(value));
@@ -16019,7 +16668,9 @@ function serializeChat(chat) {
16019
16668
  ts: timestampMs(chat.ts, null, { positive: true }) || 0,
16020
16669
  lastUsedAt: Date.now(),
16021
16670
  unseen: !!chat.unseen,
16022
- membershipRemoved: chat.membershipRemoved === true
16671
+ membershipRemoved: chat.membershipRemoved === true,
16672
+ attentionRegistrationVersion: Number(chat.attentionRegistrationVersion) || 0,
16673
+ notificationMode: chat.notificationMode || null
16023
16674
  };
16024
16675
  }
16025
16676
  function reviveChat(chat) {
@@ -16050,7 +16701,9 @@ function reviveChat(chat) {
16050
16701
  ts: timestampMs(chat.ts, null, { positive: true }) || 0,
16051
16702
  lastUsedAt: timestampMs(chat.lastUsedAt, null, { positive: true }) || 0,
16052
16703
  unseen: !!chat.unseen,
16053
- membershipRemoved: chat.membershipRemoved === true
16704
+ membershipRemoved: chat.membershipRemoved === true,
16705
+ attentionRegistrationVersion: Number(chat.attentionRegistrationVersion) || 0,
16706
+ notificationMode: chat.notificationMode || null
16054
16707
  };
16055
16708
  }
16056
16709
  function isReadableCachedChat(chat) {
@@ -17453,7 +18106,7 @@ function createChatLive({
17453
18106
  return sendSnapshot(entry);
17454
18107
  }
17455
18108
  if (!nextCompositionId) {
17456
- if (!entry.typing)
18109
+ if (!entry.typing && !entry.compositionId)
17457
18110
  return false;
17458
18111
  entry.typing = false;
17459
18112
  entry.compositionId = "";
@@ -18056,7 +18709,10 @@ ${cid}` : "";
18056
18709
  }
18057
18710
  if (adopt && !adoptedLocalMediaRef.current.has(mediaKey)) {
18058
18711
  adoptedLocalMediaRef.current.add(mediaKey);
18059
- adoptLocalMessageMedia?.(message, local, { chatId });
18712
+ adoptLocalMessageMedia?.(message, local, {
18713
+ chatId,
18714
+ peerChatPK: local.peerChatPK
18715
+ });
18060
18716
  }
18061
18717
  }
18062
18718
  if (changed) {
@@ -18277,6 +18933,9 @@ ${cid}` : "";
18277
18933
  const sendOptions = mergeSendOptions(sendOptionsForPeer(peerChatPK), options);
18278
18934
  const nextMessage = withMessageRetention(message, sendOptions.retention);
18279
18935
  if (isLongTxt(nextMessage)) {
18936
+ if (hasChatMessageMentions(nextMessage)) {
18937
+ throw new Error("messages with mentions must fit in a text message");
18938
+ }
18280
18939
  const cid = makeSendCid(nextMessage);
18281
18940
  const attachment = makeTxtFileAttachment(nextMessage);
18282
18941
  const localMessage = makeLongTxtLocalMessage(chatPK, cid, attachment, nextMessage);
@@ -19336,9 +19995,12 @@ function normalizeDecryptedMsg(msgData, message, epoch, readContext) {
19336
19995
  if (!message || typeof message !== "object")
19337
19996
  return null;
19338
19997
  const epochSigningKeysVersion = signingKeysFingerprint(readContext?.signingKeysByChatKey);
19998
+ const content = normalizeChatMessageMentions(message, epoch?.manifest, {
19999
+ senderChatPK: message.s || message.from
20000
+ });
19339
20001
  const normalized = {
19340
- ...message,
19341
- id: message.id || msgData?.id || null,
20002
+ ...content,
20003
+ id: content.id || msgData?.id || null,
19342
20004
  ts: msgData?.ts ?? null,
19343
20005
  ttl: msgData?.ttl ?? null,
19344
20006
  ...decryptedEpochFields(epoch),
@@ -20145,11 +20807,171 @@ async function readMsg({ cloud, messageId, options = {} }) {
20145
20807
  deletedKeys: []
20146
20808
  };
20147
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
+
20148
20970
  // ../../core/chat/messages/batches/cleanup.js
20149
20971
  function isDenied(error) {
20150
20972
  return error?.code === "permission-denied";
20151
20973
  }
20152
- 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 }) {
20153
20975
  if (chatBanned || !isActive || !ownsMessageCompaction(entry?.memberChatPKs, chatPK) || !entry?.chatId || !entry.peerChatPK || !entry.ready || entry.exists === false || !entry.messages?.length || typeof deleteMessageDocs !== "function") {
20154
20976
  return null;
20155
20977
  }
@@ -20161,18 +20983,25 @@ function runMessageBatchCleanup({ entry, options = {}, chatBanned, isActive, cha
20161
20983
  const peerChatPK = entry.peerChatPK;
20162
20984
  const generation = entry.generation;
20163
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;
20164
20991
  markDiag(diag, "chat.message.cleanup.schedule", {
20165
20992
  chatId,
20166
20993
  compactControls,
20167
20994
  idle: options.idle !== false,
20168
20995
  messages: entry.messages?.length || 0
20169
20996
  });
20170
- return Promise.resolve().then(async () => {
20997
+ const operation = Promise.resolve().then(async () => {
20998
+ lease.assertCurrent();
20171
20999
  if (options.idle !== false) {
20172
21000
  await waitForIdle({
20173
21001
  timeout: CHAT_BATCH_CLEANUP_IDLE_TIMEOUT_MS,
20174
21002
  delay: CHAT_BATCH_CLEANUP_IDLE_DELAY_MS
20175
21003
  });
21004
+ lease.assertCurrent();
20176
21005
  }
20177
21006
  let current = getCurrentEntry?.(chatId);
20178
21007
  if (current !== entry || current.generation !== generation || current.route) {
@@ -20195,8 +21024,10 @@ function runMessageBatchCleanup({ entry, options = {}, chatBanned, isActive, cha
20195
21024
  return;
20196
21025
  }
20197
21026
  if (compactControls) {
21027
+ lease.assertCurrent();
20198
21028
  const compacted = await compactMessages({
20199
21029
  chatId,
21030
+ maintenance,
20200
21031
  messages: current.messages.slice(),
20201
21032
  deletedKeys,
20202
21033
  protectedKeys: compactProtectedKeys,
@@ -20207,6 +21038,7 @@ function runMessageBatchCleanup({ entry, options = {}, chatBanned, isActive, cha
20207
21038
  if (compacted.length) {
20208
21039
  dropped.push(...compacted);
20209
21040
  }
21041
+ lease.assertCurrent();
20210
21042
  }
20211
21043
  if (dropped.length) {
20212
21044
  const cacheStartedAt = Date.now();
@@ -20219,7 +21051,12 @@ function runMessageBatchCleanup({ entry, options = {}, chatBanned, isActive, cha
20219
21051
  startMs: entry.startMs,
20220
21052
  messages: dropped,
20221
21053
  mode: "confirmed"
21054
+ }).then((result) => {
21055
+ lease.assertCurrent();
21056
+ return result;
20222
21057
  }).catch((error) => {
21058
+ if (isStaleMessageMaintenance(error))
21059
+ throw error;
20223
21060
  markError(diag, "chat.message.cleanup.cache", cacheStartedAt, error, {
20224
21061
  droppedCount: dropped.length
20225
21062
  });
@@ -20241,14 +21078,16 @@ function runMessageBatchCleanup({ entry, options = {}, chatBanned, isActive, cha
20241
21078
  if (current !== entry || current.generation !== generation || current.route || !dropped.length) {
20242
21079
  return;
20243
21080
  }
21081
+ lease.assertCurrent();
20244
21082
  notify(current);
20245
21083
  }).catch((error) => {
20246
- if (!isDenied(error)) {
21084
+ if (!isDenied(error) && !isStaleMessageMaintenance(error)) {
20247
21085
  markError(diag, "chat.message.cleanup", Date.now(), error, {
20248
21086
  compactControls
20249
21087
  });
20250
21088
  }
20251
- });
21089
+ }).finally(() => lease.release());
21090
+ return maintenance.track(operation);
20252
21091
  }
20253
21092
 
20254
21093
  // ../../core/chat/messages/epochhistory.js
@@ -20814,6 +21653,7 @@ function runBatchCleanup(owner, entry, options = {}) {
20814
21653
  isActive: sources.isActive,
20815
21654
  chatPK: sources.chatPK,
20816
21655
  localCache: sources.localCache,
21656
+ maintenance: sources.maintenance,
20817
21657
  deleteMessageDocs: sources.deleteMessageDocs,
20818
21658
  diag: sources.diag,
20819
21659
  getCurrentEntry: (chatId) => owner.batches.get(chatId),
@@ -21773,6 +22613,9 @@ function messageBatchSources(input = {}) {
21773
22613
  }
21774
22614
  function createChatMessageBatches(input) {
21775
22615
  let sources = messageBatchSources(input);
22616
+ if (!sources.maintenance?.claim) {
22617
+ throw new Error("message batches require account maintenance");
22618
+ }
21776
22619
  let warming = null;
21777
22620
  const routeMemory = createRouteMemory(MESSAGE_VIEW_CACHE_SIZE);
21778
22621
  const collection = createMessageBatchCollection({
@@ -21793,7 +22636,11 @@ function createChatMessageBatches(input) {
21793
22636
  collection.clear();
21794
22637
  };
21795
22638
  const setSources = (next) => {
21796
- 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;
21797
22644
  warming.sync();
21798
22645
  };
21799
22646
  return Object.freeze({
@@ -22080,7 +22927,7 @@ function epochMessageReadContext(epochState) {
22080
22927
  async function readEntry(cloud, uid, chatPrivateKey, chatId) {
22081
22928
  const entryId = ownChatEntryId(chatPrivateKey, chatId);
22082
22929
  const record = await cloud.user.chats.read(uid, entryId);
22083
- const entry = record ? await openOwnChatEntry(chatPrivateKey, entryId, record.body).catch(() => null) : null;
22930
+ const entry = record ? await openOwnChatRecord(chatPrivateKey, entryId, record).catch(() => null) : null;
22084
22931
  if (entry && entry.ownerRevision !== record?.revision)
22085
22932
  throw new Error("chat owner revision mismatch");
22086
22933
  return { entryId, entry };
@@ -22120,6 +22967,50 @@ function cachedInboxRead(cache, key, read) {
22120
22967
  function hasBlockedMembers(manifest, blockedUids) {
22121
22968
  return blockedUids instanceof Set && blockedUids.size > 0 && manifest.members.some((member) => blockedUids.has(member.uid));
22122
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
+ }
22123
23014
  function inboxLeaseError() {
22124
23015
  const error = new Error("chat inbox session changed");
22125
23016
  error.code = "chat/session-stale";
@@ -22329,27 +23220,16 @@ function routesForManifest(entry, epochState) {
22329
23220
  }];
22330
23221
  }));
22331
23222
  }
22332
- function epochDeliveryCapability(identity, epochState) {
22333
- return deriveChatDeliveryCapability(epochState.stateCapability, {
23223
+ function epochDeliveryRegistration(identity, epochState, notificationMode) {
23224
+ return chatDeliveryRegistration(epochState.stateCapability, {
23225
+ attentionSecret: epochState.epochSecret,
22334
23226
  chatId: epochState.manifest.chatId,
22335
23227
  recipientChatPK: identity.chatPK,
22336
- generation: epochState.manifest.epochVersion
23228
+ generation: epochState.manifest.epochVersion,
23229
+ manifest: epochState.manifest,
23230
+ notificationMode
22337
23231
  });
22338
23232
  }
22339
- async function registerEpochDelivery(cloud, identity, epochState, previousState, wasRegistered, options) {
22340
- const capability = epochDeliveryCapability(identity, epochState);
22341
- assertInboxLease(options);
22342
- await cloud.delivery.register(capability);
22343
- if (previousState && wasRegistered) {
22344
- const oldCapability = deriveChatDeliveryCapability(previousState.stateCapability, {
22345
- chatId: previousState.manifest.chatId,
22346
- recipientChatPK: identity.chatPK,
22347
- generation: previousState.manifest.epochVersion
22348
- });
22349
- await cloud.delivery.revoke(oldCapability).catch(() => false);
22350
- }
22351
- return true;
22352
- }
22353
23233
  async function readPingMessage(cloud, payload, epochState) {
22354
23234
  const messageId = cleanText(payload.messageId);
22355
23235
  if (!messageId)
@@ -22436,7 +23316,7 @@ function sameEntryEpoch(left, right) {
22436
23316
  return left?.current?.manifest?.epochId === right?.current?.manifest?.epochId && left?.current?.settings?.digest === right?.current?.settings?.digest;
22437
23317
  }
22438
23318
  function sameEntryRegistration(left, right) {
22439
- 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;
22440
23320
  }
22441
23321
  function sameEntryRetirement(left, right) {
22442
23322
  return left?.retirement?.kind === right?.retirement?.kind && left?.retirement?.epochVersion === right?.retirement?.epochVersion;
@@ -22468,6 +23348,8 @@ function projectOwnChatEntry(entry, entryId, userChatPK, ts, activity = {}) {
22468
23348
  settings,
22469
23349
  routes: entry.routes,
22470
23350
  deliveryRegistered: entry.deliveryRegistered === true,
23351
+ attentionRegistrationVersion: entry.attentionRegistrationVersion,
23352
+ notificationMode: entry.notificationMode,
22471
23353
  notificationTag: entry.notificationTag,
22472
23354
  epochState: entry.current,
22473
23355
  ownEntry: entry,
@@ -22586,14 +23468,22 @@ async function verifyOpenedPingEpoch(context) {
22586
23468
  if (epochState.manifest.epochId !== payload.epochId || epochState.manifest.epochVersion !== payload.epochVersion) {
22587
23469
  throw new Error("ping epoch mismatch");
22588
23470
  }
23471
+ context.epochState = epochState;
22589
23472
  if (hasBlockedMembers(epochState.manifest, options.blockedUids)) {
22590
23473
  if (context.entry) {
22591
23474
  await unlinkBlockedChat(cloud, uid, identity, context.entry, options);
22592
23475
  return "deleted";
22593
23476
  }
22594
- 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";
22595
23486
  }
22596
- context.epochState = epochState;
22597
23487
  return null;
22598
23488
  }
22599
23489
  function startOpenedWelcomeHydration(context) {
@@ -22601,7 +23491,6 @@ function startOpenedWelcomeHydration(context) {
22601
23491
  if (!membershipWelcome)
22602
23492
  return;
22603
23493
  context.prefetchedMessage = captureAsync(readPingMessage(context.cloud, context.payload, context.epochState));
22604
- 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));
22605
23494
  }
22606
23495
  async function applyOpenedPingMessage(context) {
22607
23496
  const { cloud, identity, options, payload, uid } = context;
@@ -22647,19 +23536,12 @@ async function applyOpenedPingMessage(context) {
22647
23536
  return null;
22648
23537
  }
22649
23538
  async function prepareOpenedPingOwner(context) {
22650
- const { cloud, identity, payload, senderProfile } = context;
23539
+ const { identity, payload, senderProfile } = context;
22651
23540
  const routes = withSenderRoute(context.entry, payload, senderProfile, context.epochState);
22652
23541
  const nowMs2 = timestampMs(payload.ts, Date.now()) ?? Date.now();
22653
23542
  const membershipWelcome = (!context.entry || context.restoringMembership) && payload.kind === "welcome";
22654
- const initialDeliveryCapability = !context.entry && membershipWelcome ? epochDeliveryCapability(identity, context.epochState) : null;
22655
- let deliveryRegistered = context.entry?.deliveryRegistered === true;
22656
- if (initialDeliveryCapability)
22657
- deliveryRegistered = true;
22658
- if (!deliveryRegistered && context.deliveryRegistration) {
22659
- deliveryRegistered = await unwrapAsync(context.deliveryRegistration);
22660
- }
22661
- if (!deliveryRegistered)
22662
- 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;
22663
23545
  const nextEntry = context.entry ? {
22664
23546
  ...context.entry,
22665
23547
  current: {
@@ -22673,6 +23555,8 @@ async function prepareOpenedPingOwner(context) {
22673
23555
  },
22674
23556
  routes,
22675
23557
  deliveryRegistered,
23558
+ attentionRegistrationVersion: CHAT_ATTENTION_REGISTRATION_VERSION,
23559
+ notificationMode: normalizeChatNotificationMode(context.entry.notificationMode, context.epochState.manifest),
22676
23560
  notificationTag: notificationChatTag(context.epochState.stateCapability, payload.chatId, identity.chatPK),
22677
23561
  retirement: null
22678
23562
  } : makeOwnChatEntry(context.epochState, {
@@ -22683,7 +23567,7 @@ async function prepareOpenedPingOwner(context) {
22683
23567
  });
22684
23568
  const welcomeMlsState = membershipWelcome && context.epochState.mlsSnapshot ? await prepareOwnerChatMlsSnapshot(identity, context.entryId, context.epochState, context.epochState.mlsSnapshot) : null;
22685
23569
  Object.assign(context, {
22686
- initialDeliveryCapability,
23570
+ initialDeliveryRegistration,
22687
23571
  membershipWelcome,
22688
23572
  nextEntry,
22689
23573
  nowMs: nowMs2,
@@ -22697,7 +23581,7 @@ async function commitOpenedPingOwner(context) {
22697
23581
  const {
22698
23582
  cloud,
22699
23583
  identity,
22700
- initialDeliveryCapability,
23584
+ initialDeliveryRegistration,
22701
23585
  membershipWelcome,
22702
23586
  nextEntry,
22703
23587
  nowMs: nowMs2,
@@ -22711,7 +23595,7 @@ async function commitOpenedPingOwner(context) {
22711
23595
  return {
22712
23596
  entry: nextEntry,
22713
23597
  tsMs: nowMs2,
22714
- deliveryRegistration: initialDeliveryCapability,
23598
+ deliveryRegistration: initialDeliveryRegistration,
22715
23599
  ...welcomeMlsState ? { mlsWrites: [{ id: welcomeMlsState.stateId, value: welcomeMlsState.value }] } : {}
22716
23600
  };
22717
23601
  }
@@ -22726,13 +23610,18 @@ async function commitOpenedPingOwner(context) {
22726
23610
  current: nextEntry.current,
22727
23611
  routes: withSenderRoute(current, payload, senderProfile, context.epochState),
22728
23612
  deliveryRegistered: current.deliveryRegistered || nextEntry.deliveryRegistered,
23613
+ attentionRegistrationVersion: CHAT_ATTENTION_REGISTRATION_VERSION,
23614
+ notificationMode: normalizeChatNotificationMode(current.notificationMode, context.epochState.manifest),
22729
23615
  notificationTag: nextEntry.notificationTag,
22730
23616
  retirement: membershipWelcome ? null : current.retirement
22731
23617
  };
22732
23618
  if (sameStableEntry(current, candidate) && !welcomeMlsState)
22733
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);
22734
23622
  return {
22735
23623
  entry: candidate,
23624
+ deliveryRegistration,
22736
23625
  ...membershipWelcome ? { tsMs: nowMs2 } : {},
22737
23626
  ...welcomeMlsState ? { mlsWrites: [{ id: welcomeMlsState.stateId, value: welcomeMlsState.value }] } : {}
22738
23627
  };
@@ -22744,10 +23633,18 @@ async function commitOpenedPingOwner(context) {
22744
23633
  }
22745
23634
  async function cleanupOpenedPing(context) {
22746
23635
  const { cloud, identity, options } = context;
22747
- 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)
22748
23638
  return;
23639
+ const keyPackageId = cleanText(context.epochState.mlsKeyPackageId);
23640
+ if (!keyPackageId)
23641
+ return;
23642
+ const consumed = options.consumedMlsKeyPackageIds;
23643
+ if (consumed?.has(keyPackageId))
23644
+ return;
23645
+ consumed?.add(keyPackageId);
22749
23646
  assertInboxLease(options);
22750
- 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);
22751
23648
  options.onMlsKeyPackageConsumed?.();
22752
23649
  }
22753
23650
  function openedPingProjection(context, entry) {
@@ -22814,9 +23711,13 @@ async function persistOpenedPingOwner(context) {
22814
23711
  });
22815
23712
  }
22816
23713
  async function settleOpenedPing(prepared, onAuthoritative) {
22817
- if (prepared.result)
22818
- return prepared.result;
22819
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
+ }
22820
23721
  startOpenedWelcomeHydration(context);
22821
23722
  const createsMembership = !context.entry && context.payload.kind === "welcome";
22822
23723
  const ownerPersistence = createsMembership ? captureAsync(persistOpenedPingOwner(context)) : null;
@@ -22841,6 +23742,15 @@ async function settleOpenedPing(prepared, onAuthoritative) {
22841
23742
  });
22842
23743
  return true;
22843
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
+ }
22844
23754
  function senderProfilePromise(cache, cloud, payload) {
22845
23755
  const key = `${cleanText(payload?.senderUid)}:${cleanText(payload?.senderChatPK)}`;
22846
23756
  return cachedInboxRead(cache, key, () => resolveSenderProfile(cloud, payload));
@@ -22913,13 +23823,15 @@ function welcomeAnnouncementFromPreparedPing(authenticated, prepared, document)
22913
23823
  members: manifest.members,
22914
23824
  settings,
22915
23825
  messageId: payload.messageId || null,
23826
+ messageRequest: prepared?.opened?.result === "request",
23827
+ preview: prepared?.opened?.context?.preview || null,
22916
23828
  at: ts
22917
23829
  };
22918
23830
  }
22919
23831
  function announcePreparedWelcome(context, authenticated, prepared, document, startedAt) {
22920
23832
  const { blockedUids, options, state } = context;
22921
23833
  const payload = authenticated?.ping?.payload;
22922
- 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)) {
22923
23835
  return () => {};
22924
23836
  }
22925
23837
  markDiag(options.diag, "chat.inbox.chat.announced", {
@@ -23071,12 +23983,11 @@ async function commitRemovedEpochTransition(context) {
23071
23983
  return { removed: true, chatId: parent.chatId, entry: retiredEntry };
23072
23984
  }
23073
23985
  async function prepareReceivedEpochOwner(context) {
23074
- const { cloud, entry, identity, options, parent } = context;
23986
+ const { entry, identity, options, parent } = context;
23075
23987
  if (hasBlockedMembers(context.epochState.manifest, options.blockedUids)) {
23076
- context.terminal = await unlinkBlockedChat(cloud, context.uid, identity, entry, options);
23988
+ context.terminal = await unlinkBlockedChat(context.cloud, context.uid, identity, entry, options);
23077
23989
  return;
23078
23990
  }
23079
- const deliveryRegistered = await registerEpochDelivery(cloud, identity, context.epochState, entry.current, entry.deliveryRegistered === true, options);
23080
23991
  context.nextEntry = {
23081
23992
  ...entry,
23082
23993
  current: {
@@ -23089,7 +24000,9 @@ async function prepareReceivedEpochOwner(context) {
23089
24000
  settings: context.epochState.settings
23090
24001
  },
23091
24002
  routes: routesForManifest(entry, context.epochState),
23092
- deliveryRegistered,
24003
+ deliveryRegistered: true,
24004
+ attentionRegistrationVersion: CHAT_ATTENTION_REGISTRATION_VERSION,
24005
+ notificationMode: normalizeChatNotificationMode(entry.notificationMode, context.epochState.manifest),
23093
24006
  notificationTag: notificationChatTag(context.epochState.stateCapability, parent.chatId, identity.chatPK)
23094
24007
  };
23095
24008
  context.historyId = ownEpochEntryId(identity.chatPrivateKey, parent.chatId, parent.epochId);
@@ -23118,8 +24031,11 @@ async function commitReceivedEpochOwner(context) {
23118
24031
  current: nextEntry.current,
23119
24032
  routes: routesForManifest(current, epochState),
23120
24033
  deliveryRegistered: current.deliveryRegistered || nextEntry.deliveryRegistered,
24034
+ attentionRegistrationVersion: CHAT_ATTENTION_REGISTRATION_VERSION,
24035
+ notificationMode: normalizeChatNotificationMode(current.notificationMode, epochState.manifest),
23121
24036
  notificationTag: nextEntry.notificationTag
23122
24037
  },
24038
+ deliveryRegistration: epochDeliveryRegistration(identity, epochState, current.notificationMode),
23123
24039
  touchTs: true,
23124
24040
  epochs: [{
23125
24041
  id: historyId,
@@ -23133,8 +24049,17 @@ async function commitReceivedEpochOwner(context) {
23133
24049
  };
23134
24050
  });
23135
24051
  }
23136
- function publishReceivedEpochTransition(context) {
24052
+ async function publishReceivedEpochTransition(context) {
23137
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
+ }
23138
24063
  context.options.onTransition?.(projectOwnChatEntry(context.committedEntry, context.entryId, context.identity.chatPK, context.nowMs), {
23139
24064
  ...context.action,
23140
24065
  membershipCommitted: true
@@ -23397,12 +24322,17 @@ async function prepareInboxWork(context, prepared, processingOptions = {}) {
23397
24322
  currentChats: state.currentChats,
23398
24323
  discoveryStartedAt: prepared.startedAt,
23399
24324
  mlsKeyPackageStateCache: context.mlsKeyPackageStateCache,
24325
+ consumedMlsKeyPackageIds: context.consumedMlsKeyPackageIds,
23400
24326
  mlsPackageCache: context.mlsPackageCache,
23401
24327
  settingsRecordCache: context.settingsRecordCache,
23402
24328
  onInboxCommit: state.commitOwner,
23403
24329
  onTransition: state.publishTransition,
23404
- onInboxDelete: state.publishDelete
24330
+ onInboxDelete: state.publishDelete,
24331
+ requestToken: `${document?.slot === true ? "slot" : "ping"}:${cleanText(document?.id)}`
23405
24332
  });
24333
+ if (ping.opened?.result === "request") {
24334
+ await hydrateOpenedChatRequest(ping.opened.context);
24335
+ }
23406
24336
  announcement = announcePreparedWelcome(context, authenticated, ping, document, prepared.startedAt);
23407
24337
  return { announcement, document, ping, prepared, processingOptions, status: null };
23408
24338
  } catch (error) {
@@ -23427,6 +24357,11 @@ async function settleInboxWork(context, work, onAuthoritative) {
23427
24357
  result: result === true ? "applied" : cleanText(result) || "ignored",
23428
24358
  slot: document?.slot === true
23429
24359
  });
24360
+ if (result === "request") {
24361
+ options.onInboxRequest?.(work.announcement, document);
24362
+ options.onPingSettled?.(document, result);
24363
+ return "request";
24364
+ }
23430
24365
  if (result !== "duplicate")
23431
24366
  state.processed += 1;
23432
24367
  if (!document.slot || result === "deleted") {
@@ -23730,6 +24665,7 @@ async function processInbox(cloud, uid, userChatPK, userPrivKey, options = {}) {
23730
24665
  const context = {
23731
24666
  blockedUids,
23732
24667
  cloud,
24668
+ consumedMlsKeyPackageIds: new Set,
23733
24669
  discoveryPool: createInboxWorkPool(CHAT_INBOX_DISCOVERY_PARALLEL_CHATS),
23734
24670
  identity,
23735
24671
  mlsKeyPackageStateCache: new Map,
@@ -23900,6 +24836,8 @@ function listenToChats(cloud, uid, userChatPK, userPrivKey, onUpdate, onError, o
23900
24836
  let inboxRequestedRetryDelayMs = null;
23901
24837
  const priorityInboxDocuments = new Map;
23902
24838
  const activeInboxDocuments = new Map;
24839
+ const pendingChatRequests = new Map;
24840
+ const chatRequestResolutions = new Map;
23903
24841
  const inboxPendingAttempts = new Map;
23904
24842
  const leaveProposalObservedAt = new Map;
23905
24843
  let closed = false;
@@ -23928,8 +24866,17 @@ function listenToChats(cloud, uid, userChatPK, userPrivKey, onUpdate, onError, o
23928
24866
  const revokeInboxAnnouncement = (chatId, token) => {
23929
24867
  if (closed)
23930
24868
  return false;
24869
+ if (pendingChatRequests.get(chatId)?.announcement?.token === token) {
24870
+ pendingChatRequests.delete(chatId);
24871
+ }
23931
24872
  return options.onInboxRevoke?.(chatId, token) ?? false;
23932
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
+ };
23933
24880
  const publishInboxTransition = (chat, message) => {
23934
24881
  if (closed)
23935
24882
  return false;
@@ -24007,6 +24954,10 @@ function listenToChats(cloud, uid, userChatPK, userPrivKey, onUpdate, onError, o
24007
24954
  notificationPrivateKey: options.notificationPrivateKey,
24008
24955
  mls: options.mls,
24009
24956
  blockedUids: options.blockedUids,
24957
+ chatAdmission: options.chatAdmission,
24958
+ incomingChatDecision: options.incomingChatDecision,
24959
+ chatRequestResolutions,
24960
+ onInboxRequest: retainInboxRequest,
24010
24961
  sinceMs: inboxSinceMs,
24011
24962
  priorityDocuments,
24012
24963
  settlementController,
@@ -24052,7 +25003,7 @@ function listenToChats(cloud, uid, userChatPK, userPrivKey, onUpdate, onError, o
24052
25003
  retryRequested = true;
24053
25004
  return true;
24054
25005
  },
24055
- onPingSettled: (doc) => {
25006
+ onPingSettled: (doc, result) => {
24056
25007
  const key = inboxRetryKey(doc);
24057
25008
  if (key)
24058
25009
  inboxPendingAttempts.delete(key);
@@ -24060,6 +25011,12 @@ function listenToChats(cloud, uid, userChatPK, userPrivKey, onUpdate, onError, o
24060
25011
  if (priorityKey && chatDocDataEqual(priorityInboxDocuments.get(priorityKey), doc)) {
24061
25012
  priorityInboxDocuments.delete(priorityKey);
24062
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
+ }
24063
25020
  },
24064
25021
  onPingStarted: (doc) => {
24065
25022
  const key = inboxDocumentKey2(doc);
@@ -24135,6 +25092,27 @@ function listenToChats(cloud, uid, userChatPK, userPrivKey, onUpdate, onError, o
24135
25092
  setTimeout(processQueuedInbox, 0);
24136
25093
  }
24137
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
+ };
24138
25116
  const schedule = (delayMs) => {
24139
25117
  if (timer || processing || closed) {
24140
25118
  return;
@@ -24194,13 +25172,19 @@ function listenToChats(cloud, uid, userChatPK, userPrivKey, onUpdate, onError, o
24194
25172
  inboxQueued = false;
24195
25173
  priorityInboxDocuments.clear();
24196
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();
24197
25180
  unsub?.();
24198
25181
  tokenUnsub?.();
24199
25182
  inboxUnsub?.();
24200
25183
  };
24201
25184
  return Object.freeze({
24202
25185
  close,
24203
- prioritizeChat: (chatId) => settlementController.prioritizeChat(chatId)
25186
+ prioritizeChat: (chatId) => settlementController.prioritizeChat(chatId),
25187
+ resolveChatRequest
24204
25188
  });
24205
25189
  }
24206
25190
  async function loadMoreChats(cloud, uid, userChatPK, userPrivKey, afterChat, pageSize) {
@@ -24282,7 +25266,7 @@ async function getChat(cloud, uid, chatId, userChatPK, userPrivKey) {
24282
25266
  }
24283
25267
  async function decryptChatEntry(entryRecord, userChatPK, userPrivKey) {
24284
25268
  const data = entryRecord;
24285
- const entry = await openOwnChatEntry(userPrivKey, entryRecord.id, data?.body);
25269
+ const entry = await openOwnChatRecord(userPrivKey, entryRecord.id, data);
24286
25270
  if (entry.ownerRevision !== data?.revision) {
24287
25271
  throw new Error("chat owner revision mismatch");
24288
25272
  }
@@ -24320,6 +25304,8 @@ async function decryptChatEntry(entryRecord, userChatPK, userPrivKey) {
24320
25304
  readMs: null,
24321
25305
  startMs,
24322
25306
  deliveryRegistered: entry.deliveryRegistered === true,
25307
+ attentionRegistrationVersion: entry.attentionRegistrationVersion,
25308
+ notificationMode: entry.notificationMode,
24323
25309
  routes: entry.routes,
24324
25310
  peerDeliveryCapability: directPeer ? entry.routes?.[directPeer.chatPK]?.deliveryCapability || null : null,
24325
25311
  peerNotificationPK: directPeer?.notificationPK || null,
@@ -24597,12 +25583,14 @@ function announcementRow(announcement, selfChatPK) {
24597
25583
  peerChatPK: directPeer?.chatPK || null,
24598
25584
  peerUid: directPeer?.uid || null,
24599
25585
  settings: announcement.settings || {},
24600
- preview: null,
25586
+ preview: announcement.preview || null,
24601
25587
  readMs: null,
24602
25588
  inboxMessageId: announcement.messageId || null,
24603
25589
  inboxMessageAt: at,
24604
25590
  ts: at,
24605
- unseen: false
25591
+ unseen: announcement.messageRequest === true,
25592
+ messageRequest: announcement.messageRequest === true,
25593
+ requestToken: announcement.token
24606
25594
  };
24607
25595
  }
24608
25596
  function sourceEntry() {
@@ -24964,6 +25952,8 @@ function createChatListIndex({ cache, filterHiddenChats, getSources, isHiddenCha
24964
25952
  settings: retiredEntry.current.settings.values,
24965
25953
  routes: retiredEntry.routes,
24966
25954
  deliveryRegistered: retiredEntry.deliveryRegistered === true,
25955
+ attentionRegistrationVersion: retiredEntry.attentionRegistrationVersion,
25956
+ notificationMode: retiredEntry.notificationMode,
24967
25957
  notificationTag: retiredEntry.notificationTag || null,
24968
25958
  epochState: retiredEntry.current,
24969
25959
  ownEntry: retiredEntry,
@@ -25050,6 +26040,8 @@ function createChatListIndex({ cache, filterHiddenChats, getSources, isHiddenCha
25050
26040
  peerSigningPublicKey: owner?.signingKeysByChatKey?.[peerChatPK] || "",
25051
26041
  signerShared: owner?.signerShared === true,
25052
26042
  deliveryRegistered: owner?.deliveryRegistered === true,
26043
+ attentionRegistrationVersion: owner?.attentionRegistrationVersion || 0,
26044
+ notificationMode: owner?.notificationMode || null,
25053
26045
  peerDeliveryCapability: owner?.peerDeliveryCapability || "",
25054
26046
  peerNotificationPK: owner?.peerNotificationPK || "",
25055
26047
  notificationTag: owner?.notificationTag || "",
@@ -25131,12 +26123,14 @@ function createChatListListener({
25131
26123
  let listening = false;
25132
26124
  let unsubscribeChats = null;
25133
26125
  let prioritizeInboxChat = null;
26126
+ let resolveInboxChatRequest = null;
25134
26127
  let retryTimer = null;
25135
26128
  let updateSequence = 0;
25136
26129
  const stop = () => {
25137
26130
  unsubscribeChats?.();
25138
26131
  unsubscribeChats = null;
25139
26132
  prioritizeInboxChat = null;
26133
+ resolveInboxChatRequest = null;
25140
26134
  };
25141
26135
  const retainAfterError = (error) => {
25142
26136
  const sources = getSources();
@@ -25295,6 +26289,8 @@ function createChatListListener({
25295
26289
  inboxTransport: sources.inboxTransport,
25296
26290
  mls: sources.mls,
25297
26291
  blockedUids: sources.blockedUidSet,
26292
+ chatAdmission: sources.chatAdmission,
26293
+ incomingChatDecision: sources.incomingChatDecision,
25298
26294
  localCache: sources.localCache,
25299
26295
  diag: sources.diag,
25300
26296
  getCurrentChat: index.getOwnerChat,
@@ -25305,6 +26301,7 @@ function createChatListListener({
25305
26301
  throw new Error("chat list subscription lifecycle required");
25306
26302
  }
25307
26303
  prioritizeInboxChat = subscription.prioritizeChat;
26304
+ resolveInboxChatRequest = subscription.resolveChatRequest;
25308
26305
  unsubscribeChats = () => {
25309
26306
  cancelled = true;
25310
26307
  markDiag(getSources().diag, "chat.provider.listen.stop", { elapsedMs: Date.now() - listenStartedAt });
@@ -25322,6 +26319,7 @@ function createChatListListener({
25322
26319
  },
25323
26320
  isListening: () => listening,
25324
26321
  prioritizeChat: (chatId) => prioritizeInboxChat?.(chatId) || Promise.resolve(false),
26322
+ resolveChatRequest: (chatId, decision) => resolveInboxChatRequest?.(chatId, decision) || Promise.resolve(false),
25325
26323
  start: () => {
25326
26324
  if (listening)
25327
26325
  return;
@@ -25922,6 +26920,7 @@ function createChatList(initialSources) {
25922
26920
  getRow: index.getRow,
25923
26921
  markChatRead: index.markChatRead,
25924
26922
  reconcileChatEpoch: epoch.reconcileChatEpoch,
26923
+ resolveChatRequest: listener.resolveChatRequest,
25925
26924
  sendOptionsForPeer: index.sendOptionsForPeer,
25926
26925
  hasChat: paging.hasChat
25927
26926
  };
@@ -26490,20 +27489,24 @@ function memberFromChatIdentity(value) {
26490
27489
  uid: identity.uid,
26491
27490
  chatPK: identity.chatPK,
26492
27491
  chatSigningPK: identity.chatSigningPK,
26493
- notificationPK: identity.notificationPK
27492
+ notificationPK: identity.notificationPK,
27493
+ ...value?.admissionCapability ? { admissionCapability: cleanText(value.admissionCapability).toLowerCase() } : {}
26494
27494
  };
26495
27495
  const mlsLeafId = cleanText(value?.mlsLeafId).toLowerCase();
26496
27496
  return /^[0-9a-f]{64}$/u.test(mlsLeafId) ? { ...member, mlsLeafId } : member;
26497
27497
  }
26498
27498
  async function registerInitialDelivery(cloud, identity, epochState) {
26499
- const capability = deriveChatDeliveryCapability(epochState.stateCapability, {
27499
+ const registration = chatDeliveryRegistration(epochState.stateCapability, {
27500
+ attentionSecret: epochState.epochSecret,
26500
27501
  chatId: epochState.manifest.chatId,
26501
27502
  recipientChatPK: identity.chatPK,
26502
- generation: epochState.manifest.epochVersion
27503
+ generation: epochState.manifest.epochVersion,
27504
+ manifest: epochState.manifest
26503
27505
  });
26504
- return cloud.delivery.register(capability).then(() => true, () => false);
27506
+ return cloud.delivery.register(registration).then(() => true, () => false);
26505
27507
  }
26506
- function initialRoutes(manifest, stateCapability, ownerChatPK) {
27508
+ function initialRoutes(manifest, stateCapability, ownerChatPK, memberValues = []) {
27509
+ const privateByChatPK = new Map(memberValues.map((member) => [member.chatPK, member]));
26507
27510
  return Object.fromEntries(manifest.members.map((member) => [member.chatPK, {
26508
27511
  uid: member.uid,
26509
27512
  deliveryCapability: manifest.lineage === CHAT_LINEAGES.GROUP || member.chatPK === ownerChatPK ? deriveChatDeliveryCapability(stateCapability, {
@@ -26512,13 +27515,14 @@ function initialRoutes(manifest, stateCapability, ownerChatPK) {
26512
27515
  generation: manifest.epochVersion
26513
27516
  }) : null,
26514
27517
  notificationPK: member.notificationPK,
27518
+ admissionCapability: privateByChatPK.get(member.chatPK)?.admissionCapability || null,
26515
27519
  generation: manifest.epochVersion
26516
27520
  }]));
26517
27521
  }
26518
27522
  async function prepareInitialOwner(identity, epochState, fields = {}) {
26519
27523
  const entryId = ownChatEntryId(identity.chatPrivateKey, epochState.manifest.chatId);
26520
27524
  const entry = makeOwnChatEntry(epochState, {
26521
- routes: initialRoutes(epochState.manifest, epochState.stateCapability, identity.chatPK),
27525
+ routes: initialRoutes(epochState.manifest, epochState.stateCapability, identity.chatPK, fields.members),
26522
27526
  startMs: Date.now(),
26523
27527
  deliveryRegistered: fields.deliveryRegistered === true,
26524
27528
  notificationTag: notificationChatTag(epochState.stateCapability, epochState.manifest.chatId, identity.chatPK)
@@ -26533,6 +27537,7 @@ async function prepareInitialOwner(identity, epochState, fields = {}) {
26533
27537
  entryId,
26534
27538
  record: {
26535
27539
  body: await sealOwnChatEntry(identity.chatPrivateKey, entryId, entry),
27540
+ notificationBody: await sealOwnChatNotificationPreference(identity.chatPrivateKey, entryId, entry),
26536
27541
  revision: entry.ownerRevision,
26537
27542
  tsMs
26538
27543
  }
@@ -26632,7 +27637,8 @@ async function createGroupChat(cloud, identityValue, memberValues, settings = {}
26632
27637
  const stateId = chatMlsStateId(identity.chatPrivateKey, chatId, initial.transitionCommitment);
26633
27638
  const ownerMlsState = await sealChatMlsState(identity.chatPrivateKey, stateId, { v: 1, chatId, transitionCommitment: initial.transitionCommitment }, mlsGroup.snapshot);
26634
27639
  const owner = await prepareInitialOwner(identity, initialState, {
26635
- deliveryRegistered: true
27640
+ deliveryRegistered: true,
27641
+ members
26636
27642
  });
26637
27643
  let stateError = null;
26638
27644
  try {
@@ -26746,7 +27752,7 @@ async function createGroupChat(cloud, identityValue, memberValues, settings = {}
26746
27752
 
26747
27753
  // ../../core/chat/direct.js
26748
27754
  "use client";
26749
- function orderedChatKeys(first, second) {
27755
+ function orderedChatKeys2(first, second) {
26750
27756
  return [
26751
27757
  cleanChatHex(first, "chat public key"),
26752
27758
  cleanChatHex(second, "peer chat public key")
@@ -26768,7 +27774,7 @@ function deriveDirectRouteId(chatPrivateKey, chatPK, peerChatPK) {
26768
27774
  try {
26769
27775
  peerKey = fromHex(otherChatPK, "peer chat public key");
26770
27776
  shared = x25519.getSharedSecret(toBytes32(chatPrivateKey, "chat private key"), peerKey);
26771
- routeId = deriveKey(shared, "direct-route-id-v3", orderedChatKeys(ownChatPK, otherChatPK));
27777
+ routeId = deriveKey(shared, "direct-route-id-v3", orderedChatKeys2(ownChatPK, otherChatPK));
26772
27778
  return toHex(routeId);
26773
27779
  } finally {
26774
27780
  cleanBytes(peerKey, shared, routeId);
@@ -26918,6 +27924,9 @@ function createChatSessionLaunches({
26918
27924
  setSelectedChat(existingChatId);
26919
27925
  return existingChatId;
26920
27926
  }
27927
+ if (directAdmissionForPeer(operation.identity.chatPrivateKey, chatPK, profile) === CHAT_ADMISSION_DECISIONS.REJECT) {
27928
+ throw new Error("peer is not accepting direct chats");
27929
+ }
26921
27930
  const routeId = directRouteIdForPeer(member.chatPK, operation);
26922
27931
  if (operation.pendingOwner.getPendingLaunch()?.id === routeId)
26923
27932
  return routeId;
@@ -26986,6 +27995,13 @@ function createChatSessionLaunches({
26986
27995
  const profileList = Array.isArray(profiles) ? profiles : [profiles];
26987
27996
  const blockedSet = new Set(blocked);
26988
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
+ }
26989
28005
  for (const member of requestedMembers) {
26990
28006
  if (member.chatPK === chatPK)
26991
28007
  throw new Error("current user is already a chat member");
@@ -27114,6 +28130,9 @@ function createChatSessionLaunches({
27114
28130
  directOpens.delete(routeId);
27115
28131
  return runExistingDirectLifecycle(existingChatId, lifecycle, operation);
27116
28132
  }
28133
+ if (directAdmissionForPeer(operation.identity.chatPrivateKey, chatPK, profile) === CHAT_ADMISSION_DECISIONS.REJECT) {
28134
+ throw new Error("peer is not accepting direct chats");
28135
+ }
27117
28136
  const resolution = startDirectResolution(routeId, member.chatPK, operation);
27118
28137
  const state = { operation, promise: null };
27119
28138
  state.promise = resolution.promise.then((resolvedChatId) => {
@@ -27195,6 +28214,7 @@ function createChatSessionPending({
27195
28214
  setSelectedChat,
27196
28215
  publish,
27197
28216
  getOwnerChat,
28217
+ getChatRow,
27198
28218
  ensureChat,
27199
28219
  profileMember,
27200
28220
  transitionIdentity,
@@ -27559,7 +28579,7 @@ function createChatSessionPending({
27559
28579
  setSelectedChat(chatId);
27560
28580
  return true;
27561
28581
  };
27562
- if (pendingMatches || getOwnerChat(chatId))
28582
+ if (pendingMatches || getOwnerChat(chatId) || getChatRow(chatId)?.messageRequest)
27563
28583
  return selectResolved();
27564
28584
  return Promise.resolve(ensureChat(chatId)).then((ready) => ready === true && selectResolved());
27565
28585
  },
@@ -27632,11 +28652,13 @@ function createChatSessionMembership({
27632
28652
  if (!profile?.uid || !profile?.chatPK || !profile?.chatSigningPK || !profile?.notificationPK) {
27633
28653
  throw new Error("complete peer chat identity required");
27634
28654
  }
28655
+ const identity = getIdentity();
27635
28656
  return {
27636
28657
  uid: profile.uid,
27637
28658
  chatPK: profile.chatPK,
27638
28659
  chatSigningPK: profile.chatSigningPK,
27639
- notificationPK: profile.notificationPK
28660
+ notificationPK: profile.notificationPK,
28661
+ admissionCapability: deriveDirectAdmissionCapability(identity.chatPrivateKey, identity.chatPK, profile.chatPK, profile.chatPK)
27640
28662
  };
27641
28663
  };
27642
28664
  const prepareMlsAdditions = async (profiles, operation = operationContext()) => {
@@ -27880,7 +28902,12 @@ function createChatSessionMembership({
27880
28902
  return state.promise;
27881
28903
  };
27882
28904
  const addChatMembers = (chatId, profiles) => {
27883
- 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);
27884
28911
  const blockedSet = new Set(getIdentity().blocked);
27885
28912
  for (const member of additions) {
27886
28913
  if (blockedSet.has(member.uid))
@@ -27938,7 +28965,7 @@ function createChatSessionMembership({
27938
28965
 
27939
28966
  // ../../core/chat/deletiondelivery.js
27940
28967
  "use client";
27941
- async function deliverChatDeletedPings(cloud, identityValue, epochState, members = [], eventIdValue = "") {
28968
+ async function deliverChatDeletedPings(cloud, identityValue, epochState, members = [], eventIdValue = "", routes = {}) {
27942
28969
  const identity = {
27943
28970
  ...identityValue,
27944
28971
  uid: cleanText(identityValue?.uid),
@@ -27976,6 +29003,7 @@ async function deliverChatDeletedPings(cloud, identityValue, epochState, members
27976
29003
  deliveries.push({
27977
29004
  member,
27978
29005
  promise: pushCriticalInbox(cloud, member.uid, ping, {
29006
+ admissionCapability: routes?.[member.chatPK]?.admissionCapability || null,
27979
29007
  descriptor,
27980
29008
  routeTag: notificationRouteTag(descriptor),
27981
29009
  notify: true
@@ -28179,6 +29207,7 @@ function createChatDelete({
28179
29207
  stateCapability: serverChat?.epochState?.stateCapability,
28180
29208
  epochVersion: serverChat?.epochVersion,
28181
29209
  members: serverChat?.members || [],
29210
+ routes: sourceChat?.routes || {},
28182
29211
  transitionCommitment: serverChat?.epochState?.transitionCommitment,
28183
29212
  ownEntry: sourceChat?.ownEntry || null,
28184
29213
  membershipRemoved: sourceChat?.membershipRemoved === true,
@@ -28225,7 +29254,7 @@ function createChatDelete({
28225
29254
  },
28226
29255
  stateCapability: target.stateCapability,
28227
29256
  transitionCommitment: target.transitionCommitment
28228
- }, target.members, target.expectedEpochId);
29257
+ }, target.members, target.expectedEpochId, target.routes);
28229
29258
  return result.deliveredCount;
28230
29259
  };
28231
29260
  const deleteCurrentChat = async (input, options = {}) => {
@@ -29356,6 +30385,188 @@ function createChatSessionSettings({
29356
30385
  };
29357
30386
  }
29358
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
+
29359
30570
  // ../../core/chat/sessionsources.js
29360
30571
  function normalizeChatSessionSources(sources = {}) {
29361
30572
  return {
@@ -29369,11 +30580,12 @@ function normalizeChatSessionSources(sources = {}) {
29369
30580
  chatSigningSecret: sources.chatSigningSecret || null,
29370
30581
  notificationPK: sources.notificationPK || "",
29371
30582
  notificationPrivateKey: sources.notificationPrivateKey || "",
30583
+ chatAdmission: sources.chatAdmission || null,
29372
30584
  localCache: sources.localCache || null
29373
30585
  };
29374
30586
  }
29375
30587
  function chatSessionAuthChanged(previous, next) {
29376
- 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;
29377
30589
  }
29378
30590
 
29379
30591
  // ../../core/chat/session.js
@@ -29403,15 +30615,21 @@ function createChatSession({
29403
30615
  chatWarming = false,
29404
30616
  preloadMessageMedia,
29405
30617
  adoptLocalMessageMedia,
30618
+ refreshAttentionRegistrationsOnStart = false,
29406
30619
  chatCrypto = null,
29407
30620
  mls = null,
29408
30621
  live = null,
30622
+ maintenance,
30623
+ incomingChatDecision = null,
29409
30624
  diag = null,
29410
30625
  loadOwnerChats = loadAllChats
29411
30626
  }, initialSources = {}) {
29412
30627
  if (!cloud) {
29413
30628
  throw new Error("createChatSession requires cloud");
29414
30629
  }
30630
+ if (!maintenance?.claim || !maintenance?.open || !maintenance?.close) {
30631
+ throw new Error("createChatSession requires account message maintenance");
30632
+ }
29415
30633
  let {
29416
30634
  uid = "",
29417
30635
  blocked = [],
@@ -29423,6 +30641,7 @@ function createChatSession({
29423
30641
  chatPrivateKey = "",
29424
30642
  notificationPK = "",
29425
30643
  notificationPrivateKey = "",
30644
+ chatAdmission = null,
29426
30645
  localCache = null
29427
30646
  } = initialSources;
29428
30647
  let isActive = isForegroundAppState(appState?.currentState);
@@ -29435,6 +30654,9 @@ function createChatSession({
29435
30654
  let mlsPoolLastCheckedAt = 0;
29436
30655
  let mlsPoolGeneration = 0;
29437
30656
  let authGeneration = 0;
30657
+ let attentionRegistrationPreparation = null;
30658
+ let attentionRegistrationReadyGeneration = null;
30659
+ let attentionRegistrationReadyOwner = null;
29438
30660
  const inboxSource = () => ({
29439
30661
  uid,
29440
30662
  chatPK,
@@ -29466,6 +30688,7 @@ function createChatSession({
29466
30688
  let pendingOwner = null;
29467
30689
  let membershipOwner = null;
29468
30690
  let settingsOwner = null;
30691
+ let notificationsOwner = null;
29469
30692
  let chatListOwner = null;
29470
30693
  let unsubscribeChatList = null;
29471
30694
  let selectionIntent = 0;
@@ -29648,11 +30871,48 @@ function createChatSession({
29648
30871
  diag
29649
30872
  });
29650
30873
  const drainMembershipOutbox = () => {
29651
- if (!uid || !chatPK || !chatPrivateKey || !chatSigningPK || !chatSigningSecret)
29652
- return;
29653
- 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) => {
29654
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;
29655
30913
  });
30914
+ attentionRegistrationPreparation = state;
30915
+ return state.promise;
29656
30916
  };
29657
30917
  const replenishMlsPool = ({ force = false } = {}) => {
29658
30918
  if (!uid || !chatPK || !chatPrivateKey || !chatSigningPK || !chatSigningSecret)
@@ -29731,6 +30991,20 @@ function createChatSession({
29731
30991
  getChatListOwner: () => chatListOwner,
29732
30992
  setLocalByChat
29733
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
+ });
29734
31008
  pendingOwner = createChatSessionPending({
29735
31009
  beginSelection: beginChatSelection,
29736
31010
  getIdentity: () => ({ uid, blocked, chatPK, chatPrivateKey, chatBanned }),
@@ -29738,6 +31012,7 @@ function createChatSession({
29738
31012
  setSelectedChat,
29739
31013
  publish,
29740
31014
  getOwnerChat,
31015
+ getChatRow: (chatId) => chatListOwner.getRow(chatId),
29741
31016
  ensureChat,
29742
31017
  profileMember: membershipOwner.profileMember,
29743
31018
  transitionIdentity,
@@ -29769,12 +31044,16 @@ function createChatSession({
29769
31044
  const selectChat = (chatId) => pendingOwner.selectChat(chatId, {
29770
31045
  flushPrevious: (previousChatId) => actionOwner.seenActions.flushChatRead(previousChatId)
29771
31046
  });
31047
+ const resolveChatRequest = (chatId, decision) => chatListOwner.resolveChatRequest(chatId, decision);
29772
31048
  const getPeerChatId = (...args) => launchOwner.getPeerChatId(...args);
29773
31049
  const resolvePeerChatId = (...args) => launchOwner.resolvePeerChatId(...args);
29774
31050
  const selectPeerChat = (...args) => launchOwner.selectPeerChat(...args);
29775
31051
  const openDirectChat = (...args) => launchOwner.openDirectChat(...args);
29776
31052
  const openNotesChat = (...args) => launchOwner.openNotesChat(...args);
29777
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);
29778
31057
  const createGroupChat2 = (...args) => launchOwner.createGroupChat(...args);
29779
31058
  const selectLocalChat = (...args) => launchOwner.selectLocalChat(...args);
29780
31059
  const addChatMembers = (chatId, profiles) => {
@@ -29792,6 +31071,7 @@ function createChatSession({
29792
31071
  const leaveOwnedChat = (...args) => membershipOwner.leaveOwnedChat(...args);
29793
31072
  const updateChatSettings2 = gateChatMutation((...args) => settingsOwner.updateChatSettings(...args));
29794
31073
  const updateChatAvatar = gateChatMutation((...args) => settingsOwner.updateChatAvatar(...args));
31074
+ const setChatNotificationMode = gateChatMutation((...args) => notificationsOwner.setChatNotificationMode(...args));
29795
31075
  const updateMessage = (chatId, msgId, newMessage, peerChatPK) => {
29796
31076
  if (chatBanned)
29797
31077
  throw makeChatUnavailableError();
@@ -29914,6 +31194,8 @@ function createChatSession({
29914
31194
  notificationPrivateKey: identity.notificationPrivateKey,
29915
31195
  mls,
29916
31196
  blockedUids: new Set(identity.blocked),
31197
+ chatAdmission,
31198
+ incomingChatDecision,
29917
31199
  scanAllSlots: true,
29918
31200
  requireComplete: true
29919
31201
  });
@@ -30117,7 +31399,8 @@ function createChatSession({
30117
31399
  getChatPreviewKey: getChatPreviewKey2,
30118
31400
  writeChatPreview,
30119
31401
  chatCrypto,
30120
- diag
31402
+ diag,
31403
+ maintenance
30121
31404
  };
30122
31405
  const batchSources = () => ({
30123
31406
  cloud,
@@ -30131,6 +31414,7 @@ function createChatSession({
30131
31414
  chatBanned,
30132
31415
  isActive,
30133
31416
  localCache,
31417
+ maintenance,
30134
31418
  listRef: lastServerChatsRef,
30135
31419
  pendingDeleteIdsRef: actionOwner.deleteActions.pendingDeleteIdsRef,
30136
31420
  config: chatWarming,
@@ -30157,6 +31441,8 @@ function createChatSession({
30157
31441
  localCache,
30158
31442
  isActive,
30159
31443
  inboxTransport,
31444
+ chatAdmission,
31445
+ incomingChatDecision,
30160
31446
  diag,
30161
31447
  selectedChatId: stateOwner.getSelectedChatId(),
30162
31448
  selectedChatIdRef,
@@ -30212,18 +31498,23 @@ function createChatSession({
30212
31498
  stateOwner.setActions({
30213
31499
  loadMoreChats: loadMoreChats2,
30214
31500
  selectChat,
31501
+ resolveChatRequest,
30215
31502
  getPeerChatId,
30216
31503
  resolvePeerChatId,
30217
31504
  selectPeerChat,
30218
31505
  openDirectChat,
30219
31506
  openNotesChat,
30220
31507
  openNewChat,
31508
+ directAdmissionForPeer: directAdmissionForPeer2,
31509
+ canStartDirectChat: canStartDirectChat2,
31510
+ canInviteToGroup: canInviteToGroup2,
30221
31511
  createGroupChat: createGroupChat2,
30222
31512
  addChatMembers,
30223
31513
  kickChatMember,
30224
31514
  leaveChat,
30225
31515
  updateChatSettings: updateChatSettings2,
30226
31516
  updateChatAvatar,
31517
+ setChatNotificationMode,
30227
31518
  dropChat,
30228
31519
  dropUnavailableChat,
30229
31520
  deleteChat,
@@ -30345,6 +31636,7 @@ function createChatSession({
30345
31636
  if (value) {
30346
31637
  if (selectedChatIdRef.current)
30347
31638
  enterChatActivity(selectedChatIdRef.current);
31639
+ refreshAttentionRegistrations();
30348
31640
  drainMembershipOutbox();
30349
31641
  replenishMlsPool();
30350
31642
  }
@@ -30358,7 +31650,7 @@ function createChatSession({
30358
31650
  };
30359
31651
  const setSources = (nextSources = {}) => {
30360
31652
  const previousBlockedKey = [...blocked].sort().join("|");
30361
- const current = { uid, chatPK, chatBanned, chatPrivateKey, chatSigningPK, chatSigningSecret, notificationPK, notificationPrivateKey, localCache };
31653
+ const current = { uid, chatPK, chatBanned, chatPrivateKey, chatSigningPK, chatSigningSecret, notificationPK, notificationPrivateKey, chatAdmission, localCache };
30362
31654
  const next = normalizeChatSessionSources(nextSources);
30363
31655
  const authChanged = chatSessionAuthChanged(current, next);
30364
31656
  if (authChanged) {
@@ -30376,6 +31668,7 @@ function createChatSession({
30376
31668
  chatSigningSecret,
30377
31669
  notificationPK,
30378
31670
  notificationPrivateKey,
31671
+ chatAdmission,
30379
31672
  localCache
30380
31673
  } = next);
30381
31674
  inboxTransport.setSource(inboxSource());
@@ -30393,6 +31686,7 @@ function createChatSession({
30393
31686
  }
30394
31687
  publish();
30395
31688
  if (authChanged && isActive) {
31689
+ refreshAttentionRegistrations();
30396
31690
  drainMembershipOutbox();
30397
31691
  replenishMlsPool({ force: true });
30398
31692
  }
@@ -30406,10 +31700,13 @@ function createChatSession({
30406
31700
  const start = () => {
30407
31701
  if (started)
30408
31702
  return;
31703
+ maintenance.open();
30409
31704
  started = true;
30410
31705
  unsubscribeChatList = chatListOwner.subscribe(() => {
30411
31706
  stateOwner.setListSnapshot(chatListOwner.getSnapshot());
30412
31707
  publish();
31708
+ if (serverChatsReadyRef.current)
31709
+ refreshAttentionRegistrations();
30413
31710
  });
30414
31711
  stateOwner.setListSnapshot(chatListOwner.getSnapshot());
30415
31712
  publish();
@@ -30420,6 +31717,7 @@ function createChatSession({
30420
31717
  markDiag(diag, "chat.block.retire.failed", { message: error?.message || String(error) });
30421
31718
  });
30422
31719
  }
31720
+ refreshAttentionRegistrations();
30423
31721
  drainMembershipOutbox();
30424
31722
  replenishMlsPool({ force: true });
30425
31723
  if (appState?.addEventListener) {
@@ -30428,6 +31726,7 @@ function createChatSession({
30428
31726
  };
30429
31727
  const close = () => {
30430
31728
  authGeneration += 1;
31729
+ maintenance.close();
30431
31730
  settingsOwner.reset();
30432
31731
  mls?.close?.();
30433
31732
  if (!started)
@@ -33180,18 +34479,261 @@ function isPasswordStrengthAcceptable(feedback) {
33180
34479
  return feedback?.version === PASSWORD_STRENGTH_VERSION && feedback.acceptable === true;
33181
34480
  }
33182
34481
 
33183
- // ../../core/username.js
33184
- var MAX_USERNAME = USERNAME_MAX_CHARS;
33185
- var usernameStripRegex = /[^a-z0-9]/g;
33186
- var usernameRegex = new RegExp(`^[a-z0-9]{1,${MAX_USERNAME}}$`);
33187
- function normalizeUsername(value = "") {
33188
- 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;
33189
34495
  }
33190
- function cleanUsername(value = "") {
33191
- return normalizeUsername(value).replace(usernameStripRegex, "").slice(0, MAX_USERNAME);
34496
+ function ceilDiv(value, divisor) {
34497
+ return (value + divisor - 1n) / divisor;
33192
34498
  }
33193
- function isUsername(value = "") {
33194
- 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)}`;
33195
34737
  }
33196
34738
 
33197
34739
  // ../../core/wallet/bitcoin.js
@@ -34104,7 +35646,7 @@ function createPeersApi({ cloud, network, avatarCache = null }) {
34104
35646
  if (!uid || expectedVersion == null || typeof avatarCache?.read !== "function")
34105
35647
  return null;
34106
35648
  try {
34107
- const cached = await avatarCache.read(uid);
35649
+ const cached = await avatarCache.read(uid, expectedVersion);
34108
35650
  const version = readAvatarVersion(cached?.version);
34109
35651
  const url = typeof cached?.url === "string" && cached.url ? cached.url : typeof cached?.source === "string" && cached.source ? cached.source : null;
34110
35652
  return version === expectedVersion && url ? url : null;
@@ -41605,8 +43147,6 @@ function requirePorts(options) {
41605
43147
  throw new Error("account cloud port required");
41606
43148
  if (!options?.defaultNetwork)
41607
43149
  throw new Error("account default network required");
41608
- if (typeof options?.setNetwork !== "function")
41609
- throw new Error("account network port required");
41610
43150
  if (!options?.vaultCrypto)
41611
43151
  throw new Error("account vault crypto port required");
41612
43152
  if (typeof options?.bootWallet !== "function")
@@ -41642,8 +43182,7 @@ function openAccount(options = {}) {
41642
43182
  vaultCrypto,
41643
43183
  bootWallet,
41644
43184
  bootChat,
41645
- openLocalCache,
41646
- setNetwork: writeNetwork
43185
+ openLocalCache
41647
43186
  } = ports;
41648
43187
  let closed = false;
41649
43188
  let attempt = 0;
@@ -41653,26 +43192,46 @@ function openAccount(options = {}) {
41653
43192
  let closePromise = null;
41654
43193
  let state;
41655
43194
  let chatSession = null;
43195
+ let revokedUid = null;
43196
+ let revocationPromise = null;
41656
43197
  let nextAgreement = agreementContract(options.agreement || CURRENT_AGREEMENT);
41657
43198
  const listeners = new Set;
41658
43199
  let activeWriteTail = Promise.resolve();
41659
43200
  const sessionCloseWork = new Set;
41660
43201
  const operationBarrier = createAccountOperationCloseBarrier();
43202
+ const messageMaintenance = createAccountMessageMaintenance({
43203
+ isAccountCurrent: () => !closed
43204
+ });
43205
+ function revokeSession(details = {}) {
43206
+ const { uid = null } = details;
43207
+ const accountUid = uid || state?.uid || null;
43208
+ if (accountUid && revokedUid === accountUid) {
43209
+ return revocationPromise || Promise.resolve();
43210
+ }
43211
+ revokedUid = accountUid;
43212
+ let callback;
43213
+ try {
43214
+ callback = options.onSessionRevoked?.({ ...details, uid: accountUid });
43215
+ } catch (error) {
43216
+ callback = Promise.reject(error);
43217
+ }
43218
+ lock();
43219
+ revocationPromise = Promise.resolve(callback);
43220
+ return revocationPromise;
43221
+ }
41661
43222
  const user = options.user || createUser({
41662
43223
  cloud,
41663
43224
  network: options.network || defaultNetwork,
41664
43225
  avatarCache,
41665
43226
  diag,
41666
- onSessionRevoked: ({ uid } = {}) => {
41667
- lock();
41668
- options.onSessionRevoked?.({ uid: uid || state?.uid || null });
41669
- }
43227
+ onSessionRevoked: revokeSession
41670
43228
  });
41671
43229
  state = emptyState(user.getSnapshot(), options.network || defaultNetwork);
41672
43230
  const chat = createChatSession({
41673
43231
  ...options.chat || {},
41674
43232
  cloud,
41675
- diag: options.chat?.diag || diag
43233
+ diag: options.chat?.diag || diag,
43234
+ maintenance: messageMaintenance
41676
43235
  }, chatSources());
41677
43236
  const wallet = openWallet({
41678
43237
  ...options.wallet || {},
@@ -41701,6 +43260,7 @@ function openAccount(options = {}) {
41701
43260
  chatPrivateKey: session?.chatPrivateKey || "",
41702
43261
  notificationPK: session?.notificationPK || "",
41703
43262
  notificationPrivateKey: session?.notificationPrivateKey || "",
43263
+ chatAdmission: currentUser.chatAdmission,
41704
43264
  localCache: session?.localCache || null
41705
43265
  };
41706
43266
  }
@@ -41780,6 +43340,15 @@ function openAccount(options = {}) {
41780
43340
  await cloud.user.profile.avatar.set(uid, null);
41781
43341
  user.getSnapshot().clearAvatar?.();
41782
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;
41783
43352
  }
41784
43353
  });
41785
43354
  const push = Object.freeze({
@@ -41792,6 +43361,56 @@ function openAccount(options = {}) {
41792
43361
  return cloud.user.push.drop(payload);
41793
43362
  }
41794
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
+ });
41795
43414
  function emit2(patch) {
41796
43415
  if (closed || operationBarrier.suppressPublications)
41797
43416
  return state;
@@ -41849,7 +43468,13 @@ function openAccount(options = {}) {
41849
43468
  chatSession = null;
41850
43469
  closeAccountSession(session);
41851
43470
  if (session && typeof options.onSessionClosed === "function") {
41852
- 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) => {
41853
43478
  diag?.("account.session.closed.error", {
41854
43479
  code: error?.code || "",
41855
43480
  message: error?.message || String(error)
@@ -41875,13 +43500,17 @@ function openAccount(options = {}) {
41875
43500
  }
41876
43501
  return session;
41877
43502
  }
43503
+ async function drainSessionCloseWork() {
43504
+ while (sessionCloseWork.size) {
43505
+ await Promise.allSettled([...sessionCloseWork]);
43506
+ }
43507
+ }
41878
43508
  function lock() {
41879
43509
  const session = closeSession();
41880
43510
  setActive(false);
41881
43511
  return session;
41882
43512
  }
41883
43513
  async function applyNetwork(network) {
41884
- await writeNetwork(network);
41885
43514
  user.setNetwork(network);
41886
43515
  emitDomains({ network });
41887
43516
  peers.refreshNetwork();
@@ -41915,6 +43544,24 @@ function openAccount(options = {}) {
41915
43544
  code: error?.code || "",
41916
43545
  message: error?.message || String(error)
41917
43546
  });
43547
+ if (isAccountAuthenticationError(error)) {
43548
+ revokeSession({
43549
+ uid,
43550
+ code: error?.code || "",
43551
+ reason: "vault-listener"
43552
+ }).catch((revocationError) => {
43553
+ diag?.("account.session.revoke.error", {
43554
+ code: revocationError?.code || "",
43555
+ message: revocationError?.message || String(revocationError)
43556
+ });
43557
+ }).then(() => cloud.auth.logout()).catch((logoutError) => {
43558
+ diag?.("account.session.revoke.auth.error", {
43559
+ code: logoutError?.code || "",
43560
+ message: logoutError?.message || String(logoutError)
43561
+ });
43562
+ });
43563
+ return;
43564
+ }
41918
43565
  lock();
41919
43566
  emit2({ vault: null, vaultReady: true, vaultError: error });
41920
43567
  });
@@ -41946,7 +43593,6 @@ function openAccount(options = {}) {
41946
43593
  writeActive(previousUid, false);
41947
43594
  active = { uid: null, value: false };
41948
43595
  user.setNetwork(defaultNetwork);
41949
- Promise.resolve(writeNetwork(defaultNetwork)).catch(() => {});
41950
43596
  const currentUser = user.getSnapshot();
41951
43597
  const currentUid = currentUser.uid || null;
41952
43598
  state = {
@@ -41964,6 +43610,11 @@ function openAccount(options = {}) {
41964
43610
  const stopUser = user.subscribe(syncUser);
41965
43611
  watchVault(state.uid);
41966
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();
41967
43618
  if (closed)
41968
43619
  throw new Error("account closed");
41969
43620
  if (state.lockState !== "locked")
@@ -42307,10 +43958,12 @@ function openAccount(options = {}) {
42307
43958
  user,
42308
43959
  profile,
42309
43960
  push,
43961
+ payment,
42310
43962
  support,
42311
43963
  chat,
42312
43964
  wallet,
42313
43965
  bitcoin,
43966
+ messageMaintenance,
42314
43967
  peers,
42315
43968
  closeBarrier: Object.freeze({
42316
43969
  acquire: operationBarrier.acquireClose,