@glyphteck/veyl 0.59.0 → 0.60.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/account.js CHANGED
@@ -6263,8 +6263,9 @@ var LOCAL_CHAT_MESSAGE_CACHE_MAX_VISIBLE = 1000;
6263
6263
  var LOCAL_AVATAR_CACHE_MAX_BYTES = 128 * MIB_BYTES;
6264
6264
  var LOCAL_AVATAR_CACHE_MAX_AGE_MS = 30 * DAY_MS;
6265
6265
  var AVATAR_IMAGE_MAX_BYTES = 256 * KIB_BYTES;
6266
- var CHAT_AVATAR_IMAGE_MAX_BYTES = 16 * KIB_BYTES;
6267
- var CHAT_AVATAR_REF_MAX_CHARS = 24 * KIB_BYTES;
6266
+ var AVATAR_IMAGE_QUALITY_ATTEMPTS = Object.freeze([0.92, 0.84, 0.76, 0.68]);
6267
+ var CHAT_AVATAR_IMAGE_MAX_BYTES = AVATAR_IMAGE_MAX_BYTES;
6268
+ var CHAT_AVATAR_REF_MAX_CHARS = 256;
6268
6269
  var CHAT_MESSAGE_FILE_CACHE_MAX_BYTES = 64 * MIB_BYTES;
6269
6270
  var IDLE_CALLBACK_MIN_TIMEOUT_MS = 50;
6270
6271
  var ATTACHMENT_CACHE_IDLE_TIMEOUT_MS = 2500;
@@ -6323,6 +6324,7 @@ var CHAT_RECEIPT_MAX_MEMBERS = 32;
6323
6324
  var CHAT_ORDINARY_DELIVERY_MAX_MEMBERS = 128;
6324
6325
  var CHAT_MAX_MEMBERS = 256;
6325
6326
  var CHAT_MAX_TEXT_CHARS = 2048;
6327
+ var CHAT_TITLE_MAX_CHARS = 32;
6326
6328
  var CHAT_MAX_REACTIONS = CHAT_MAX_MEMBERS;
6327
6329
  var SEARCH_DEBOUNCE_MS = 300;
6328
6330
  var RECENT_PEER_REFRESH_LIMIT = 50;
@@ -8730,1849 +8732,1886 @@ function createUser({ cloud, network, avatarCache = null, diag = null, onSession
8730
8732
  };
8731
8733
  }
8732
8734
 
8733
- // ../../core/chat/state.js
8734
- function makeCid() {
8735
- return `${Date.now().toString(36)}${toHex(randomBytes3(3))}`;
8736
- }
8737
- function getMessageKey(message) {
8738
- return message?.cid || message?.id || null;
8739
- }
8740
- function getCidMs(cid) {
8741
- if (typeof cid !== "string" || cid.length <= 6) {
8742
- return null;
8743
- }
8744
- const base = cid.slice(0, -6);
8745
- const ms = Number.parseInt(base, 36);
8746
- return Number.isFinite(ms) ? ms : null;
8735
+ // ../../core/crypto/file.js
8736
+ "use client";
8737
+ var FILE_SCOPE = "file-body";
8738
+ var FILE_IV_BYTES = 12;
8739
+ var FILE_TAG_BYTES = 16;
8740
+ function createFileKey() {
8741
+ return randomBytes3(32);
8747
8742
  }
8748
- function getMessageOrderMs(message) {
8749
- return getCidMs(message?.cid) ?? timestampMs(message?.ts, Infinity);
8743
+ function encodeFileKey(key) {
8744
+ return toHex(toBytes32(key, "file key"));
8750
8745
  }
8751
- function sortMessages(messages) {
8752
- return [...messages].sort((a, b) => {
8753
- const aMs = getMessageOrderMs(a);
8754
- const bMs = getMessageOrderMs(b);
8755
- if (aMs !== bMs) {
8756
- return aMs - bMs;
8757
- }
8758
- return String(a?.id || "").localeCompare(String(b?.id || ""));
8759
- });
8746
+ function decodeFileKey(key) {
8747
+ return toBytes32(key, "file key");
8760
8748
  }
8761
- function mergeMessages(...groups) {
8762
- const merged = new Map;
8763
- for (const group of groups) {
8764
- for (const message of group || []) {
8765
- const key = getMessageKey(message);
8766
- if (!key) {
8767
- continue;
8768
- }
8769
- merged.set(key, message);
8770
- }
8749
+ function getFileAadForPath(scope) {
8750
+ if (!scope) {
8751
+ throw new Error("file scope required");
8771
8752
  }
8772
- return sortMessages([...merged.values()]);
8753
+ return encodeScope(FILE_SCOPE, [scope]);
8773
8754
  }
8774
-
8775
- // ../../core/chat/ids.js
8776
- function isChatMessageForParticipants(message, chatPK, peerChatPK, memberChatPKs = null) {
8777
- const members = Array.isArray(memberChatPKs) ? new Set(memberChatPKs.filter(Boolean)) : null;
8778
- if (!chatPK && !peerChatPK && !members?.size) {
8779
- return true;
8755
+ function getSubtleCrypto() {
8756
+ const subtle = globalThis.crypto?.subtle;
8757
+ if (!subtle) {
8758
+ throw new Error("WebCrypto AES-GCM unavailable");
8780
8759
  }
8781
- const sender = typeof message?.s === "string" && message.s ? message.s : typeof message?.from === "string" ? message.from : "";
8782
- const epochMembers = Array.isArray(message?.epochMemberChatPKs) ? new Set(message.epochMemberChatPKs.filter(Boolean)) : null;
8783
- return !sender || epochMembers?.has(sender) || members?.has(sender) || sender === chatPK || sender === peerChatPK;
8760
+ return subtle;
8784
8761
  }
8785
- function filterChatMessages(messages, chatPK, peerChatPK, memberChatPKs = null) {
8786
- return chatPK || peerChatPK || memberChatPKs?.length ? (messages || []).filter((message) => isChatMessageForParticipants(message, chatPK, peerChatPK, memberChatPKs)) : messages || [];
8762
+ async function importAesKey(key, usages) {
8763
+ return getSubtleCrypto().importKey("raw", toBytes32(key, "file key"), { name: "AES-GCM" }, false, usages);
8787
8764
  }
8788
- function getChatPeerPK(chatItem) {
8789
- return chatItem?.lineage === "direct" && chatItem?.memberCount === 2 ? chatItem?.peerChatPK || null : null;
8765
+ async function sealFile(key, bytes, scope) {
8766
+ const fileKey = new Uint8Array(toBytes32(key, "file key"));
8767
+ try {
8768
+ const cryptoKey = await importAesKey(fileKey, ["encrypt"]);
8769
+ const iv = randomBytes3(FILE_IV_BYTES);
8770
+ const ct = await getSubtleCrypto().encrypt({
8771
+ name: "AES-GCM",
8772
+ iv,
8773
+ tagLength: FILE_TAG_BYTES * 8,
8774
+ additionalData: getFileAadForPath(scope)
8775
+ }, cryptoKey, toBytes(bytes, "plaintext"));
8776
+ return packBodyData(iv, new Uint8Array(ct));
8777
+ } finally {
8778
+ cleanBytes(fileKey);
8779
+ }
8790
8780
  }
8791
- function getChatPreviewKey(chatItem) {
8792
- const preview = chatItem?.preview;
8793
- if (!preview || preview.pending || preview.failed || String(preview?.id || "").startsWith("local:")) {
8794
- return null;
8781
+ async function openFileForPath(key, body, scope) {
8782
+ const fileKey = new Uint8Array(toBytes32(key, "file key"));
8783
+ try {
8784
+ const cryptoKey = await importAesKey(fileKey, ["decrypt"]);
8785
+ const { nonce, ct } = unpackBodyData(body, FILE_IV_BYTES);
8786
+ const pt = await getSubtleCrypto().decrypt({
8787
+ name: "AES-GCM",
8788
+ iv: nonce,
8789
+ tagLength: FILE_TAG_BYTES * 8,
8790
+ additionalData: getFileAadForPath(scope)
8791
+ }, cryptoKey, ct);
8792
+ return new Uint8Array(pt);
8793
+ } finally {
8794
+ cleanBytes(fileKey);
8795
8795
  }
8796
- return getMessageKey(preview);
8797
8796
  }
8798
8797
 
8799
- // ../../core/chat/protocol.js
8800
- "use client";
8801
- var CHAT_PROTOCOL_VERSION = 3;
8802
- var CHAT_MANIFEST_VERSION = 2;
8803
- var CHAT_TRANSITION_VERSION = 2;
8804
- var CHAT_MESSAGE_ENVELOPE_VERSION = 3;
8805
- var CHAT_SETTINGS_VERSION = 1;
8806
-
8807
- // ../../core/chat/epochs/manifest.js
8798
+ // ../../core/chat/filepayload.js
8808
8799
  "use client";
8809
- var CHAT_LINEAGES = Object.freeze({
8810
- SELF: "self",
8811
- DIRECT: "direct",
8812
- GROUP: "group"
8813
- });
8814
- var LINEAGES = new Set(Object.values(CHAT_LINEAGES));
8815
- var HEX_32_RE2 = /^[0-9a-f]{64}$/u;
8816
- function cleanChatHex(value, label = "chat value") {
8817
- const text = cleanText(value).toLowerCase();
8818
- if (!HEX_32_RE2.test(text)) {
8819
- throw new Error(`${label} required`);
8800
+ var CHAT_MEDIA_ROOT = "chatEpochs";
8801
+ var SHARED_MEDIA_ROOT = "shared";
8802
+ var CHAT_MEDIA_TTL_MS2 = CHAT_MEDIA_TTL_MS;
8803
+ var MAX_CHAT_UPLOAD_BYTES = CHAT_UPLOAD_MAX_BYTES;
8804
+ var CHAT_VIDEO_TRANSCODE_VIDEO_BITRATE_BPS2 = CHAT_VIDEO_TRANSCODE_VIDEO_BITRATE_BPS;
8805
+ var CHAT_VIDEO_TRANSCODE_AUDIO_BITRATE_BPS2 = CHAT_VIDEO_TRANSCODE_AUDIO_BITRATE_BPS;
8806
+ var CHAT_VIDEO_TRANSCODE_TOTAL_BITRATE_BPS = CHAT_VIDEO_TRANSCODE_VIDEO_BITRATE_BPS2 + CHAT_VIDEO_TRANSCODE_AUDIO_BITRATE_BPS2;
8807
+ var CHAT_ID_PATTERN = "[0-9a-fA-F]{64}";
8808
+ var SHARED_MEDIA_ID_PATTERN = "[0-9a-fA-F]{32}";
8809
+ var MEDIA_ID_PATTERN = "[0-9a-fA-F]{32}";
8810
+ var CHAT_MEDIA_FILE_PATTERN = new RegExp(`^${CHAT_MEDIA_ROOT}/(${CHAT_ID_PATTERN})/(${MEDIA_ID_PATTERN})$`);
8811
+ var SHARED_MEDIA_FILE_PATTERN = new RegExp(`^${SHARED_MEDIA_ROOT}/(${SHARED_MEDIA_ID_PATTERN})$`);
8812
+ function cleanMediaEpochId(value) {
8813
+ const epochId = String(value || "").trim();
8814
+ if (!new RegExp(`^${CHAT_ID_PATTERN}$`).test(epochId)) {
8815
+ throw new Error("invalid media epoch id");
8820
8816
  }
8821
- return text;
8817
+ return epochId.toLowerCase();
8822
8818
  }
8823
- function cleanUid(value) {
8824
- const uid = cleanText(value);
8825
- if (!uid || uid.length > 128) {
8826
- throw new Error("chat member uid required");
8819
+ function cleanMediaId(value) {
8820
+ const mediaId = String(value || "").trim();
8821
+ if (!new RegExp(`^${MEDIA_ID_PATTERN}$`).test(mediaId)) {
8822
+ throw new Error("invalid media id");
8827
8823
  }
8828
- return uid;
8824
+ return mediaId.toLowerCase();
8829
8825
  }
8830
- function cleanCreatedAt(value) {
8831
- if (!Number.isSafeInteger(value) || value <= 0) {
8832
- throw new Error("chat epoch createdAt required");
8826
+ function cleanSharedMediaId(value) {
8827
+ const sharedId = String(value || "").trim();
8828
+ if (!new RegExp(`^${SHARED_MEDIA_ID_PATTERN}$`).test(sharedId)) {
8829
+ throw new Error("invalid shared media id");
8833
8830
  }
8834
- return value;
8831
+ return sharedId.toLowerCase();
8835
8832
  }
8836
- function cleanEpochVersion(value) {
8837
- if (!Number.isSafeInteger(value) || value <= 0) {
8838
- throw new Error("chat epoch version required");
8839
- }
8840
- return value;
8833
+ function makeChatMediaId() {
8834
+ return toHex(randomBytes3(16));
8841
8835
  }
8842
- function cleanMember(value) {
8843
- if (!value || typeof value !== "object" || Array.isArray(value)) {
8844
- throw new Error("invalid chat member");
8845
- }
8846
- return {
8847
- uid: cleanUid(value.uid),
8848
- chatPK: cleanChatHex(value.chatPK, "member chat key"),
8849
- chatSigningPK: cleanChatHex(value.chatSigningPK, "member signing key"),
8850
- notificationPK: cleanChatHex(value.notificationPK, "member notification key"),
8851
- mlsLeafId: cleanChatHex(value.mlsLeafId, "member mls leaf id")
8852
- };
8836
+ function mediaFilePath(epochId, mediaId) {
8837
+ const nextEpochId = cleanMediaEpochId(epochId);
8838
+ const nextMediaId = cleanMediaId(mediaId);
8839
+ return `${CHAT_MEDIA_ROOT}/${nextEpochId}/${nextMediaId}`;
8853
8840
  }
8854
- function assertUniqueMembers(members) {
8855
- const uids = new Set;
8856
- const chatKeys = new Set;
8857
- const signingKeys = new Set;
8858
- const mlsLeafIds = new Set;
8859
- for (const member of members) {
8860
- if (uids.has(member.uid) || chatKeys.has(member.chatPK) || signingKeys.has(member.chatSigningPK) || mlsLeafIds.has(member.mlsLeafId)) {
8861
- throw new Error("duplicate chat member");
8862
- }
8863
- uids.add(member.uid);
8864
- chatKeys.add(member.chatPK);
8865
- signingKeys.add(member.chatSigningPK);
8866
- mlsLeafIds.add(member.mlsLeafId);
8867
- }
8841
+ function sharedMediaFilePath(sharedId) {
8842
+ const nextSharedId = cleanSharedMediaId(sharedId);
8843
+ return `${SHARED_MEDIA_ROOT}/${nextSharedId}`;
8868
8844
  }
8869
- function assertLineage(manifest, parent) {
8870
- const memberCount = manifest.members.length;
8871
- if (memberCount > 2 && manifest.lineage !== CHAT_LINEAGES.GROUP) {
8872
- throw new Error("group lineage required");
8873
- }
8874
- if (!parent) {
8875
- return;
8845
+ function makeSharedMediaId() {
8846
+ return toHex(randomBytes3(16));
8847
+ }
8848
+ function getMediaFileRef(path) {
8849
+ const value = String(path || "").trim();
8850
+ const chatMatch = value.match(CHAT_MEDIA_FILE_PATTERN);
8851
+ if (chatMatch?.[1] && chatMatch?.[2]) {
8852
+ return {
8853
+ type: "chat",
8854
+ epochId: chatMatch[1].toLowerCase(),
8855
+ mediaId: chatMatch[2].toLowerCase()
8856
+ };
8876
8857
  }
8877
- if (manifest.chatId !== parent.chatId || manifest.parentEpochId !== parent.epochId || manifest.epochVersion !== parent.epochVersion + 1) {
8878
- throw new Error("invalid chat epoch successor");
8858
+ const sharedMatch = value.match(SHARED_MEDIA_FILE_PATTERN);
8859
+ if (sharedMatch?.[1]) {
8860
+ return {
8861
+ type: "shared",
8862
+ sharedId: sharedMatch[1].toLowerCase()
8863
+ };
8879
8864
  }
8880
- if (parent.lineage === CHAT_LINEAGES.GROUP && manifest.lineage !== CHAT_LINEAGES.GROUP) {
8881
- throw new Error("chat group lineage is permanent");
8865
+ throw new Error("invalid media file path");
8866
+ }
8867
+ function getChatMediaFileRef(path) {
8868
+ const ref = getMediaFileRef(path);
8869
+ if (ref?.type !== "chat") {
8870
+ throw new Error("invalid media file path");
8882
8871
  }
8872
+ return ref;
8883
8873
  }
8884
- function normalizeEpochManifest(value, options = {}) {
8885
- if (!value || typeof value !== "object" || Array.isArray(value)) {
8886
- throw new Error("chat manifest required");
8874
+ function getSharedMediaFileRef(path) {
8875
+ const ref = getMediaFileRef(path);
8876
+ if (ref?.type !== "shared") {
8877
+ throw new Error("invalid shared media file path");
8887
8878
  }
8888
- if (value.v !== CHAT_MANIFEST_VERSION || value.protocol !== CHAT_PROTOCOL_VERSION) {
8889
- throw new Error("unsupported chat manifest");
8879
+ return ref;
8880
+ }
8881
+ function uploadByteLength(value) {
8882
+ if (Number.isFinite(value?.byteLength)) {
8883
+ return value.byteLength;
8890
8884
  }
8891
- if (!Array.isArray(value.members) || value.members.length < 1 || value.members.length > CHAT_MAX_MEMBERS) {
8892
- throw new Error("invalid chat member count");
8885
+ if (Number.isFinite(value?.size)) {
8886
+ return value.size;
8893
8887
  }
8894
- const lineage = cleanText(value.lineage);
8895
- if (!LINEAGES.has(lineage)) {
8896
- throw new Error("invalid chat lineage");
8897
- }
8898
- const members = value.members.map(cleanMember).sort((a, b) => a.chatPK.localeCompare(b.chatPK));
8899
- assertUniqueMembers(members);
8900
- const manifest = {
8901
- v: CHAT_MANIFEST_VERSION,
8902
- protocol: CHAT_PROTOCOL_VERSION,
8903
- chatId: cleanChatHex(value.chatId, "chat id"),
8904
- epochId: cleanChatHex(value.epochId, "chat epoch id"),
8905
- epochVersion: cleanEpochVersion(value.epochVersion),
8906
- parentEpochId: value.parentEpochId == null ? null : cleanChatHex(value.parentEpochId, "parent epoch id"),
8907
- createdAt: cleanCreatedAt(value.createdAt),
8908
- lineage,
8909
- members
8910
- };
8911
- if (manifest.epochVersion === 1 !== (manifest.parentEpochId === null)) {
8912
- throw new Error("invalid parent epoch");
8913
- }
8914
- const bytes = canonicalBytes(manifest, "chat manifest");
8915
- if (bytes.length > CHAT_MANIFEST_MAX_BYTES) {
8916
- throw new Error("chat manifest too large");
8888
+ if (Number.isFinite(value)) {
8889
+ return value;
8917
8890
  }
8918
- assertLineage(manifest, options.parent || null);
8919
- return manifest;
8920
- }
8921
- function epochManifestDigest(manifest) {
8922
- return toHex(sha256(canonicalBytes(normalizeEpochManifest(manifest), "chat manifest digest")));
8923
- }
8924
- function manifestMember(manifest, chatPK) {
8925
- const key = cleanChatHex(chatPK, "member chat key");
8926
- return normalizeEpochManifest(manifest).members.find((member) => member.chatPK === key) || null;
8927
- }
8928
- function manifestSigningKeys(manifest) {
8929
- return Object.fromEntries(normalizeEpochManifest(manifest).members.map((member) => [member.chatPK, member.chatSigningPK]));
8891
+ return null;
8930
8892
  }
8931
-
8932
- // ../../core/chat/direct.js
8933
- "use client";
8934
- function orderedChatKeys(first, second) {
8935
- return [
8936
- cleanChatHex(first, "chat public key"),
8937
- cleanChatHex(second, "peer chat public key")
8938
- ].sort();
8893
+ function makeChatUploadTooLargeError(bytes, maxBytes = MAX_CHAT_UPLOAD_BYTES) {
8894
+ const error = new Error("upload too large");
8895
+ error.code = "upload-too-large";
8896
+ error.maxBytes = maxBytes;
8897
+ if (Number.isFinite(bytes)) {
8898
+ error.bytes = bytes;
8899
+ }
8900
+ return error;
8939
8901
  }
8940
- function findDirectChat(chats, peerChatPK) {
8941
- const peer = cleanChatHex(peerChatPK, "peer chat public key");
8942
- return (chats || []).find((chat) => chat?.lineage === "direct" && chat?.peerChatPK === peer) || null;
8902
+ function assertChatUploadByteSize(bytes, maxBytes = MAX_CHAT_UPLOAD_BYTES) {
8903
+ const length = uploadByteLength(bytes);
8904
+ if (!Number.isFinite(length)) {
8905
+ throw new Error("upload bytes required");
8906
+ }
8907
+ if (length <= 0 || length > maxBytes) {
8908
+ throw makeChatUploadTooLargeError(length, maxBytes);
8909
+ }
8910
+ return length;
8943
8911
  }
8944
- function deriveDirectRouteId(chatPrivateKey, chatPK, peerChatPK) {
8945
- const ownChatPK = cleanChatHex(chatPK, "chat public key");
8946
- const otherChatPK = cleanChatHex(peerChatPK, "peer chat public key");
8947
- if (ownChatPK === otherChatPK) {
8948
- throw new Error("direct peer chat key required");
8912
+ async function toUploadBytes(data) {
8913
+ if (typeof Blob !== "undefined" && data instanceof Blob) {
8914
+ if (typeof data.arrayBuffer === "function") {
8915
+ return new Uint8Array(await data.arrayBuffer());
8916
+ }
8917
+ if (typeof FileReader !== "undefined") {
8918
+ return new Promise((resolve, reject) => {
8919
+ const reader = new FileReader;
8920
+ reader.onload = () => resolve(new Uint8Array(reader.result));
8921
+ reader.onerror = () => reject(reader.error || new Error("blob read failed"));
8922
+ reader.readAsArrayBuffer(data);
8923
+ });
8924
+ }
8925
+ if (typeof Response !== "undefined") {
8926
+ return new Uint8Array(await new Response(data).arrayBuffer());
8927
+ }
8949
8928
  }
8950
- let peerKey = null;
8951
- let shared = null;
8952
- let routeId = null;
8929
+ if (typeof data?.arrayBuffer === "function") {
8930
+ return new Uint8Array(await data.arrayBuffer());
8931
+ }
8932
+ return toBytes(data, "upload bytes");
8933
+ }
8934
+ async function makeChatFileUploadPayload(epochId, cid, data, { cacheControl = "private, max-age=0, no-transform" } = {}) {
8935
+ const nextEpochId = cleanMediaEpochId(epochId);
8936
+ const mediaId = makeChatMediaId();
8937
+ const path = mediaFilePath(nextEpochId, mediaId);
8938
+ const expiresAt = Date.now() + CHAT_MEDIA_TTL_MS2;
8939
+ const key = createFileKey();
8953
8940
  try {
8954
- peerKey = fromHex(otherChatPK, "peer chat public key");
8955
- shared = x25519.getSharedSecret(toBytes32(chatPrivateKey, "chat private key"), peerKey);
8956
- routeId = deriveKey(shared, "direct-route-id-v3", orderedChatKeys(ownChatPK, otherChatPK));
8957
- return toHex(routeId);
8941
+ const uploadBytes = await toUploadBytes(data);
8942
+ assertChatUploadByteSize(uploadBytes);
8943
+ const body = await sealFile(key, uploadBytes, path);
8944
+ assertChatUploadByteSize(body);
8945
+ return {
8946
+ epochId: nextEpochId,
8947
+ mediaId,
8948
+ path,
8949
+ body,
8950
+ metadata: {
8951
+ contentType: "application/octet-stream",
8952
+ cacheControl
8953
+ },
8954
+ file: {
8955
+ p: path,
8956
+ k: encodeFileKey(key),
8957
+ x: expiresAt
8958
+ }
8959
+ };
8960
+ } catch (error) {
8961
+ if (error && typeof error === "object") {
8962
+ error.path = error?.path || path;
8963
+ error.cid = error?.cid || cid;
8964
+ }
8965
+ throw error;
8958
8966
  } finally {
8959
- cleanBytes(peerKey, shared, routeId);
8967
+ cleanBytes(key);
8960
8968
  }
8961
8969
  }
8962
- function deriveSelfChatId(chatPrivateKey, chatPK) {
8963
- const ownChatPK = cleanChatHex(chatPK, "chat public key");
8964
- let chatId = null;
8970
+ async function makeSharedFileUploadPayload(data, { contentType = "application/octet-stream", cacheControl = "private, max-age=0, no-transform" } = {}) {
8971
+ const sharedId = makeSharedMediaId();
8972
+ const path = sharedMediaFilePath(sharedId);
8973
+ const expiresAt = Date.now() + CHAT_MEDIA_TTL_MS2;
8974
+ const key = createFileKey();
8965
8975
  try {
8966
- chatId = deriveKey(toBytes32(chatPrivateKey, "chat private key"), "self-chat-id-v3", [ownChatPK]);
8967
- return toHex(chatId);
8976
+ const uploadBytes = await toUploadBytes(data);
8977
+ assertChatUploadByteSize(uploadBytes);
8978
+ const body = await sealFile(key, uploadBytes, path);
8979
+ assertChatUploadByteSize(body);
8980
+ return {
8981
+ sharedId,
8982
+ path,
8983
+ body,
8984
+ metadata: {
8985
+ contentType,
8986
+ cacheControl
8987
+ },
8988
+ file: {
8989
+ p: path,
8990
+ k: encodeFileKey(key),
8991
+ x: expiresAt
8992
+ }
8993
+ };
8994
+ } catch (error) {
8995
+ if (error && typeof error === "object") {
8996
+ error.path = error?.path || path;
8997
+ error.sharedId = error?.sharedId || sharedId;
8998
+ }
8999
+ throw error;
8968
9000
  } finally {
8969
- cleanBytes(chatId);
9001
+ cleanBytes(key);
8970
9002
  }
8971
9003
  }
8972
9004
 
8973
- // ../../core/chat/errors.js
8974
- function makeChatUnavailableError() {
8975
- const error = new Error("chat unavailable");
8976
- error.code = "permission-denied";
8977
- return error;
8978
- }
8979
- function makeMessageSaveUnavailableError() {
8980
- const error = new Error("this message can't be saved anymore");
8981
- error.code = "message-unavailable";
8982
- return error;
8983
- }
8984
- function isMessageSaveUnavailableError(error) {
8985
- return String(error?.code || "").toLowerCase() === "message-unavailable";
9005
+ // ../../core/files.js
9006
+ "use client";
9007
+ function makeFileId(size = 8) {
9008
+ return toHex(randomBytes3(size));
8986
9009
  }
8987
- function normalizeMessageSaveError(error) {
8988
- if (isMessageSaveUnavailableError(error)) {
9010
+ function setErrorStage(error, stage, extra = {}) {
9011
+ if (!error || typeof error !== "object") {
8989
9012
  return error;
8990
9013
  }
8991
- const code = String(error?.code || "").toLowerCase();
8992
- if (code === "7" || code === "not-found" || code.endsWith("/not-found") || code === "permission-denied" || code.endsWith("/permission-denied")) {
8993
- return makeMessageSaveUnavailableError();
8994
- }
9014
+ error.stage = error?.stage || stage;
9015
+ Object.assign(error, extra);
8995
9016
  return error;
8996
9017
  }
8997
-
8998
- // ../../core/chat/epochs/state.js
8999
- "use client";
9000
- var CAPABILITY_WITNESS_SCOPE = "veyl-chat-state-witness-v3:";
9001
- var CAPABILITY_COMMITMENT_SCOPE = "veyl-chat-state-commitment-v3:";
9002
- function randomChatId() {
9003
- return toHex(randomBytes3(32));
9004
- }
9005
- function hashText(value) {
9006
- return toHex(sha256(encoder.encode(value)));
9007
- }
9008
- function stateCapabilityWitness(value) {
9009
- return hashText(`${CAPABILITY_WITNESS_SCOPE}${toHex(toBytes32(value, "state capability"))}`);
9010
- }
9011
- function stateCapabilityCommitment(value) {
9012
- return hashText(`${CAPABILITY_COMMITMENT_SCOPE}${stateCapabilityWitness(value)}`);
9013
- }
9014
- function deriveEpochKeys(epochSecret, manifestValue) {
9015
- const manifest = normalizeEpochManifest(manifestValue);
9016
- const root = deriveKey(toBytes32(epochSecret, "epoch secret"), "chat-epoch-root-v3", [
9017
- manifest.chatId,
9018
- manifest.epochId,
9019
- manifest.epochVersion
9020
- ]);
9021
- let settingsIdBytes = null;
9022
- let messageLaneBytes = null;
9023
- let stateLaneBytes = null;
9018
+ async function makeChatFileUpload(epochId, cid, data, { cacheControl = "private, max-age=0, no-transform" } = {}) {
9024
9019
  try {
9025
- settingsIdBytes = deriveKey(root, "chat-epoch-settings-id-v3");
9026
- messageLaneBytes = deriveKey(root, "chat-epoch-message-lane-v3");
9027
- stateLaneBytes = deriveKey(root, "chat-epoch-member-state-lane-v3");
9028
- return {
9029
- root,
9030
- bodyKey: deriveKey(root, "chat-epoch-body-v3"),
9031
- settingsKey: deriveKey(root, "chat-epoch-settings-v3"),
9032
- settingsId: toHex(settingsIdBytes),
9033
- messageLane: toHex(messageLaneBytes),
9034
- stateLane: toHex(stateLaneBytes),
9035
- stateKey: deriveKey(root, "chat-epoch-member-state-v3")
9036
- };
9020
+ return await makeChatFileUploadPayload(epochId, cid, data, {
9021
+ cacheControl
9022
+ });
9037
9023
  } catch (error) {
9038
- cleanBytes(root);
9039
- throw error;
9040
- } finally {
9041
- cleanBytes(settingsIdBytes, messageLaneBytes, stateLaneBytes);
9024
+ throw setErrorStage(error, "encrypt", {
9025
+ ...error?.path ? { path: error.path } : {},
9026
+ cid
9027
+ });
9042
9028
  }
9043
9029
  }
9044
- function closeEpochKeys(keys) {
9045
- cleanBytes(keys?.root, keys?.bodyKey, keys?.settingsKey, keys?.stateKey);
9046
- }
9047
- function ownerEpochEntryId(chatPrivateKey, chatId, epochId) {
9048
- const value = deriveKey(toBytes32(chatPrivateKey, "chat private key"), "user-chat-epoch-entry-id-v3", [
9049
- cleanChatHex(chatId, "chat id"),
9050
- cleanChatHex(epochId, "chat epoch id")
9051
- ], 16);
9030
+ async function putChatFile(epochId, cid, data, options) {
9031
+ const upload = await makeChatFileUpload(epochId, cid, data, options);
9052
9032
  try {
9053
- return toHex(value);
9054
- } finally {
9055
- cleanBytes(value);
9033
+ if (typeof options?.uploadChatMedia !== "function") {
9034
+ throw new Error("chat media upload required");
9035
+ }
9036
+ await options.uploadChatMedia(upload);
9037
+ return upload.file;
9038
+ } catch (error) {
9039
+ throw setErrorStage(error, "upload", { path: upload.path, cid });
9056
9040
  }
9057
9041
  }
9058
- function secretHex(value, label) {
9059
- return typeof value === "string" ? cleanChatHex(value, label) : toHex(toBytes32(value, label));
9060
- }
9061
- function secretBytes(value, label) {
9062
- return fromHex(cleanChatHex(value, label), label);
9063
- }
9064
-
9065
- // ../../core/crypto/base64.js
9066
- "use client";
9067
- var ALPHABET = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_";
9068
- var VALUE_BY_CHAR = new Map([...ALPHABET].map((char, index) => [char, index]));
9069
- function bytesBase64Url(value, label = "base64url bytes") {
9070
- const bytes = toBytes(value, label);
9071
- let output = "";
9072
- for (let index = 0;index < bytes.length; index += 3) {
9073
- const a = bytes[index];
9074
- const hasB = index + 1 < bytes.length;
9075
- const hasC = index + 2 < bytes.length;
9076
- const b = hasB ? bytes[index + 1] : 0;
9077
- const c = hasC ? bytes[index + 2] : 0;
9078
- output += ALPHABET[a >>> 2];
9079
- output += ALPHABET[(a & 3) << 4 | b >>> 4];
9080
- if (hasB)
9081
- output += ALPHABET[(b & 15) << 2 | c >>> 6];
9082
- if (hasC)
9083
- output += ALPHABET[c & 63];
9042
+ async function makeSharedFileUpload(data, { contentType = "application/octet-stream", cacheControl = "private, max-age=0, no-transform" } = {}) {
9043
+ try {
9044
+ return await makeSharedFileUploadPayload(data, {
9045
+ contentType,
9046
+ cacheControl
9047
+ });
9048
+ } catch (error) {
9049
+ throw setErrorStage(error, "encrypt", {
9050
+ ...error?.path ? { path: error.path } : {}
9051
+ });
9084
9052
  }
9085
- return output;
9086
9053
  }
9087
- function base64UrlBytes(value, label = "base64url") {
9088
- const text = typeof value === "string" ? value.trim() : "";
9089
- if (!text || text.length % 4 === 1 || !/^[A-Za-z0-9_-]+$/u.test(text)) {
9090
- throw new Error(`invalid ${label}`);
9054
+ async function putSharedFile(data, options = {}) {
9055
+ const upload = await makeSharedFileUpload(data, options);
9056
+ try {
9057
+ if (typeof options?.uploadSharedMedia !== "function") {
9058
+ throw new Error("shared media upload required");
9059
+ }
9060
+ await options.uploadSharedMedia(upload);
9061
+ return upload.file;
9062
+ } catch (error) {
9063
+ throw setErrorStage(error, "upload", { path: upload.path });
9091
9064
  }
9092
- const output = new Uint8Array(Math.floor(text.length * 6 / 8));
9093
- let bits = 0;
9094
- let bitCount = 0;
9095
- let offset = 0;
9096
- for (const char of text) {
9097
- const next = VALUE_BY_CHAR.get(char);
9098
- if (next == null)
9099
- throw new Error(`invalid ${label}`);
9100
- bits = bits << 6 | next;
9101
- bitCount += 6;
9102
- if (bitCount >= 8) {
9103
- bitCount -= 8;
9104
- output[offset++] = bits >>> bitCount & 255;
9105
- bits &= bitCount ? (1 << bitCount) - 1 : 0;
9065
+ }
9066
+ async function readChatFile(readChatMedia, file) {
9067
+ let body;
9068
+ try {
9069
+ if (typeof readChatMedia !== "function") {
9070
+ throw new Error("chat media read required");
9106
9071
  }
9072
+ body = await readChatMedia(file?.p);
9073
+ } catch (error) {
9074
+ error.path = error?.path || file?.p || null;
9075
+ error.stage = error?.stage || "download";
9076
+ throw error;
9077
+ }
9078
+ try {
9079
+ getMediaFileRef(file?.p);
9080
+ const bytes = await openFileForPath(decodeFileKey(file?.k), body, file?.p);
9081
+ return bytes;
9082
+ } catch (error) {
9083
+ error.path = file?.p || null;
9084
+ error.stage = "decrypt";
9085
+ throw error;
9107
9086
  }
9108
- return output;
9109
9087
  }
9110
9088
 
9111
- // ../../core/chat/messages/actions.js
9089
+ // ../../core/chat/avatar.js
9112
9090
  "use client";
9113
- var CHAT_ACTION_VERSION = 3;
9114
- var CHAT_ACTION_OPS = Object.freeze({
9115
- CREATE: "create",
9116
- EDIT: "edit",
9117
- PAY_CONFIRM: "pay_confirm",
9118
- REACTION: "rxn",
9119
- DELETE: "del",
9120
- SYSTEM: "sys",
9121
- EPOCH: "epoch"
9122
- });
9123
- var CHAT_ACTION_OP_SET = new Set(Object.values(CHAT_ACTION_OPS));
9124
- var CONTROL_OP_BY_TYPE = Object.freeze({
9125
- rxn: CHAT_ACTION_OPS.REACTION,
9126
- del: CHAT_ACTION_OPS.DELETE,
9127
- sys: CHAT_ACTION_OPS.SYSTEM,
9128
- epoch: CHAT_ACTION_OPS.EPOCH
9129
- });
9130
- function isChatActionOp(value) {
9131
- return CHAT_ACTION_OP_SET.has(value);
9091
+ var CHAT_AVATAR_REF_PREFIX = "v1:";
9092
+ var FILE_KEY_RE = /^[0-9a-f]{64}$/u;
9093
+ function chatAvatarFile(value) {
9094
+ const ref = typeof value === "string" ? value.trim() : "";
9095
+ if (!ref.startsWith(CHAT_AVATAR_REF_PREFIX) || ref.length > CHAT_AVATAR_REF_MAX_CHARS)
9096
+ return null;
9097
+ const parts = ref.split(":");
9098
+ const key = parts[2]?.toLowerCase();
9099
+ if (parts.length !== 3 || parts[0] !== "v1" || !FILE_KEY_RE.test(key))
9100
+ return null;
9101
+ let parsed;
9102
+ try {
9103
+ parsed = getChatMediaFileRef(parts[1]);
9104
+ } catch {
9105
+ return null;
9106
+ }
9107
+ return Object.freeze({
9108
+ t: "img",
9109
+ m: "image/webp",
9110
+ p: mediaFilePath(parsed.epochId, parsed.mediaId),
9111
+ k: key
9112
+ });
9132
9113
  }
9133
- function cleanChatActionOp(value) {
9134
- const op = cleanText(value);
9135
- if (!isChatActionOp(op)) {
9136
- throw new Error("unsupported chat action op");
9114
+ function makeChatAvatarRef(file) {
9115
+ let parsed;
9116
+ const path = cleanText(file?.p);
9117
+ const key = cleanText(file?.k).toLowerCase();
9118
+ try {
9119
+ parsed = getChatMediaFileRef(path);
9120
+ } catch {
9121
+ throw new Error("invalid chat avatar");
9137
9122
  }
9138
- return op;
9123
+ if (!FILE_KEY_RE.test(key))
9124
+ throw new Error("invalid chat avatar");
9125
+ const ref = `${CHAT_AVATAR_REF_PREFIX}${mediaFilePath(parsed.epochId, parsed.mediaId)}:${key}`;
9126
+ if (ref.length > CHAT_AVATAR_REF_MAX_CHARS)
9127
+ throw new Error("invalid chat avatar");
9128
+ return ref;
9139
9129
  }
9140
- function actionOpForPayload(payload, fallback = CHAT_ACTION_OPS.CREATE) {
9141
- return CONTROL_OP_BY_TYPE[cleanText(payload?.t)] || fallback;
9130
+ function cleanChatAvatarRef(value) {
9131
+ if (value == null)
9132
+ return null;
9133
+ const file = chatAvatarFile(value);
9134
+ if (!file)
9135
+ throw new Error("invalid chat avatar");
9136
+ return makeChatAvatarRef(file);
9142
9137
  }
9143
-
9144
- // ../../core/crypto/chat.js
9145
- "use client";
9146
- var MSG_BODY_VERSION = CHAT_MESSAGE_ENVELOPE_VERSION;
9147
- var MSG_BODY_SUITE_XCHACHA_ED25519 = 1;
9148
- var MSG_SIGN_SCOPE_BYTES = encoder.encode("veyl-chat-msg-v3-sign");
9149
- var MSG_BODY_FLAGS_NONE = 0;
9150
- var ED25519_SIGNATURE_BYTES = 64;
9151
- var MSG_BODY_HEADER_BYTES = 1 + 1 + 1 + BOX_NONCE_BYTES + ED25519_SIGNATURE_BYTES;
9152
- function hasMsgHead(data) {
9153
- return typeof data?.head?.cid === "string" && !!data.head.cid;
9138
+ function assertChatAvatarBytes(value) {
9139
+ const bytes = Number(value?.byteLength ?? value?.size);
9140
+ if (!Number.isSafeInteger(bytes) || bytes <= 0 || bytes > CHAT_AVATAR_IMAGE_MAX_BYTES) {
9141
+ throw new Error("invalid chat avatar");
9142
+ }
9143
+ return bytes;
9154
9144
  }
9155
- function hasMsgBody(data) {
9156
- return data?.body != null;
9145
+
9146
+ // ../../core/chat/state.js
9147
+ function makeCid() {
9148
+ return `${Date.now().toString(36)}${toHex(randomBytes3(3))}`;
9157
9149
  }
9158
- function hasMsgData(data) {
9159
- return hasMsgHead(data) && hasMsgBody(data);
9150
+ function getMessageKey(message) {
9151
+ return message?.cid || message?.id || null;
9160
9152
  }
9161
- function cleanSigningKey(value) {
9162
- const secret = value?.secret;
9163
- const publicKey = cleanChatHex(value?.publicKey, "chat signing public key");
9164
- if (!secret) {
9165
- throw new Error("chat signing secret required");
9153
+ function getCidMs(cid) {
9154
+ if (typeof cid !== "string" || cid.length <= 6) {
9155
+ return null;
9166
9156
  }
9167
- return { secret, publicKey };
9157
+ const base = cid.slice(0, -6);
9158
+ const ms = Number.parseInt(base, 36);
9159
+ return Number.isFinite(ms) ? ms : null;
9168
9160
  }
9169
- function openChatEpoch({ manifest: manifestValue, epochSecret, actor = null } = {}) {
9170
- const manifest = normalizeEpochManifest(manifestValue);
9171
- const keys = deriveEpochKeys(epochSecret, manifest);
9172
- let normalizedActor = null;
9173
- try {
9174
- if (actor) {
9175
- const chatPK = cleanChatHex(actor.chatPK, "chat actor key");
9176
- const signingKey = cleanSigningKey(actor.signingKey);
9177
- const member = manifestMember(manifest, chatPK);
9178
- if (!member || member.chatSigningPK !== signingKey.publicKey) {
9179
- throw new Error("chat actor is not an epoch member");
9161
+ function getMessageOrderMs(message) {
9162
+ return getCidMs(message?.cid) ?? timestampMs(message?.ts, Infinity);
9163
+ }
9164
+ function sortMessages(messages) {
9165
+ return [...messages].sort((a, b) => {
9166
+ const aMs = getMessageOrderMs(a);
9167
+ const bMs = getMessageOrderMs(b);
9168
+ if (aMs !== bMs) {
9169
+ return aMs - bMs;
9170
+ }
9171
+ return String(a?.id || "").localeCompare(String(b?.id || ""));
9172
+ });
9173
+ }
9174
+ function mergeMessages(...groups) {
9175
+ const merged = new Map;
9176
+ for (const group of groups) {
9177
+ for (const message of group || []) {
9178
+ const key = getMessageKey(message);
9179
+ if (!key) {
9180
+ continue;
9180
9181
  }
9181
- normalizedActor = { chatPK, signingKey };
9182
+ merged.set(key, message);
9182
9183
  }
9183
- return {
9184
- protocol: CHAT_PROTOCOL_VERSION,
9185
- chatId: manifest.chatId,
9186
- epochId: manifest.epochId,
9187
- epochVersion: manifest.epochVersion,
9188
- manifest,
9189
- actor: normalizedActor,
9190
- ...keys
9191
- };
9192
- } catch (error) {
9193
- closeEpochKeys(keys);
9194
- throw error;
9195
9184
  }
9185
+ return sortMessages([...merged.values()]);
9196
9186
  }
9197
- function closeChatEpoch(epoch) {
9198
- closeEpochKeys(epoch);
9199
- }
9200
- function getHead(message) {
9201
- const cid = cleanText(message?.cid);
9202
- if (!cid || cid.length > 256) {
9203
- throw new Error("message cid required");
9187
+
9188
+ // ../../core/chat/ids.js
9189
+ function isChatMessageForParticipants(message, chatPK, peerChatPK, memberChatPKs = null) {
9190
+ const members = Array.isArray(memberChatPKs) ? new Set(memberChatPKs.filter(Boolean)) : null;
9191
+ if (!chatPK && !peerChatPK && !members?.size) {
9192
+ return true;
9204
9193
  }
9205
- return { cid };
9194
+ const sender = typeof message?.s === "string" && message.s ? message.s : typeof message?.from === "string" ? message.from : "";
9195
+ const epochMembers = Array.isArray(message?.epochMemberChatPKs) ? new Set(message.epochMemberChatPKs.filter(Boolean)) : null;
9196
+ return !sender || epochMembers?.has(sender) || members?.has(sender) || sender === chatPK || sender === peerChatPK;
9206
9197
  }
9207
- function getPayload(message) {
9208
- const payload = { ...message || {} };
9209
- delete payload.cid;
9210
- delete payload.from;
9211
- delete payload.s;
9212
- return payload;
9198
+ function filterChatMessages(messages, chatPK, peerChatPK, memberChatPKs = null) {
9199
+ return chatPK || peerChatPK || memberChatPKs?.length ? (messages || []).filter((message) => isChatMessageForParticipants(message, chatPK, peerChatPK, memberChatPKs)) : messages || [];
9213
9200
  }
9214
- function getMsgAad(epoch, head, suite = MSG_BODY_SUITE_XCHACHA_ED25519, flags = MSG_BODY_FLAGS_NONE) {
9215
- return canonicalBytes({
9216
- v: MSG_BODY_VERSION,
9217
- protocol: CHAT_PROTOCOL_VERSION,
9218
- chatId: epoch.chatId,
9219
- epochId: epoch.epochId,
9220
- epochVersion: epoch.epochVersion,
9221
- cid: head.cid,
9222
- suite,
9223
- flags
9224
- }, "chat message aad");
9201
+ function getChatPeerPK(chatItem) {
9202
+ return chatItem?.lineage === "direct" && chatItem?.memberCount === 2 ? chatItem?.peerChatPK || null : null;
9225
9203
  }
9226
- function getSignatureInput(aad, nonce, ct) {
9227
- return concatBytes4(MSG_SIGN_SCOPE_BYTES, aad, nonce, ct);
9204
+ function getChatPreviewKey(chatItem) {
9205
+ const preview = chatItem?.preview;
9206
+ if (!preview || preview.pending || preview.failed || String(preview?.id || "").startsWith("local:")) {
9207
+ return null;
9208
+ }
9209
+ return getMessageKey(preview);
9228
9210
  }
9229
- function batchRecordId(record) {
9230
- return cleanText(record?.id) || cleanText(record?.recordId) || cleanText(record?.head?.cid);
9211
+
9212
+ // ../../core/chat/protocol.js
9213
+ "use client";
9214
+ var CHAT_PROTOCOL_VERSION = 3;
9215
+ var CHAT_MANIFEST_VERSION = 2;
9216
+ var CHAT_TRANSITION_VERSION = 2;
9217
+ var CHAT_MESSAGE_ENVELOPE_VERSION = 3;
9218
+ var CHAT_SETTINGS_VERSION = 1;
9219
+
9220
+ // ../../core/chat/epochs/manifest.js
9221
+ "use client";
9222
+ var CHAT_LINEAGES = Object.freeze({
9223
+ SELF: "self",
9224
+ DIRECT: "direct",
9225
+ GROUP: "group"
9226
+ });
9227
+ var LINEAGES = new Set(Object.values(CHAT_LINEAGES));
9228
+ var HEX_32_RE2 = /^[0-9a-f]{64}$/u;
9229
+ function cleanChatHex(value, label = "chat value") {
9230
+ const text = cleanText(value).toLowerCase();
9231
+ if (!HEX_32_RE2.test(text)) {
9232
+ throw new Error(`${label} required`);
9233
+ }
9234
+ return text;
9231
9235
  }
9232
- function makeBatchOpenRequest(epoch, records) {
9233
- return {
9234
- records: (records || []).map((record) => ({
9235
- id: batchRecordId(record),
9236
- head: record?.head,
9237
- body: record?.body
9238
- })),
9239
- protocol: CHAT_PROTOCOL_VERSION,
9240
- chatId: epoch.chatId,
9241
- epochId: epoch.epochId,
9242
- epochVersion: epoch.epochVersion,
9243
- bodyKey: epoch.bodyKey,
9244
- manifest: epoch.manifest,
9245
- signingKeysByChatKey: manifestSigningKeys(epoch.manifest)
9246
- };
9236
+ function cleanUid(value) {
9237
+ const uid = cleanText(value);
9238
+ if (!uid || uid.length > 128) {
9239
+ throw new Error("chat member uid required");
9240
+ }
9241
+ return uid;
9247
9242
  }
9248
- function cleanActionTimestamp(value) {
9249
- if (typeof value !== "number" || !Number.isFinite(value)) {
9250
- throw new Error("chat action ts required");
9243
+ function cleanCreatedAt(value) {
9244
+ if (!Number.isSafeInteger(value) || value <= 0) {
9245
+ throw new Error("chat epoch createdAt required");
9251
9246
  }
9252
- return Object.is(value, -0) ? 0 : value;
9247
+ return value;
9253
9248
  }
9254
- function normalizePlainPayload(payload) {
9255
- if (payload == null)
9256
- return {};
9257
- if (typeof payload !== "object" || Array.isArray(payload)) {
9258
- throw new Error("invalid chat action payload");
9249
+ function cleanEpochVersion(value) {
9250
+ if (!Number.isSafeInteger(value) || value <= 0) {
9251
+ throw new Error("chat epoch version required");
9259
9252
  }
9260
- return payload;
9253
+ return value;
9261
9254
  }
9262
- function makeMsgAction(epoch, head, message, options = {}) {
9263
- if (!epoch?.actor?.chatPK || !epoch?.actor?.signingKey) {
9264
- throw new Error("chat epoch actor required");
9255
+ function cleanMember(value) {
9256
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
9257
+ throw new Error("invalid chat member");
9265
9258
  }
9266
- const target = cleanText(options.target);
9267
9259
  return {
9268
- v: CHAT_ACTION_VERSION,
9269
- op: cleanChatActionOp(cleanText(options.op) || actionOpForPayload(message)),
9270
- id: cleanText(options.id) || head.cid,
9271
- ...target ? { target } : {},
9272
- actor: epoch.actor.chatPK,
9273
- ts: Number.isFinite(options.ts) ? options.ts : Date.now(),
9274
- p: getPayload(message)
9260
+ uid: cleanUid(value.uid),
9261
+ chatPK: cleanChatHex(value.chatPK, "member chat key"),
9262
+ chatSigningPK: cleanChatHex(value.chatSigningPK, "member signing key"),
9263
+ notificationPK: cleanChatHex(value.notificationPK, "member notification key"),
9264
+ mlsLeafId: cleanChatHex(value.mlsLeafId, "member mls leaf id")
9275
9265
  };
9276
9266
  }
9277
- function packMsgBody({ suite = MSG_BODY_SUITE_XCHACHA_ED25519, flags = MSG_BODY_FLAGS_NONE, nonce, sig, ct }) {
9278
- const nonceBytes = toBytes(nonce, "chat message nonce");
9279
- const sigBytes = typeof sig === "string" ? fromHexBytes(sig, "chat message signature") : toBytes(sig, "chat message signature");
9280
- const ctBytes = toBytes(ct, "chat message ciphertext");
9281
- if (suite !== MSG_BODY_SUITE_XCHACHA_ED25519 || flags !== MSG_BODY_FLAGS_NONE) {
9282
- throw new Error("unsupported chat message suite");
9283
- }
9284
- if (nonceBytes.length !== BOX_NONCE_BYTES || sigBytes.length !== ED25519_SIGNATURE_BYTES) {
9285
- throw new Error("invalid chat message signature envelope");
9267
+ function assertUniqueMembers(members) {
9268
+ const uids = new Set;
9269
+ const chatKeys = new Set;
9270
+ const signingKeys = new Set;
9271
+ const mlsLeafIds = new Set;
9272
+ for (const member of members) {
9273
+ if (uids.has(member.uid) || chatKeys.has(member.chatPK) || signingKeys.has(member.chatSigningPK) || mlsLeafIds.has(member.mlsLeafId)) {
9274
+ throw new Error("duplicate chat member");
9275
+ }
9276
+ uids.add(member.uid);
9277
+ chatKeys.add(member.chatPK);
9278
+ signingKeys.add(member.chatSigningPK);
9279
+ mlsLeafIds.add(member.mlsLeafId);
9286
9280
  }
9287
- const out = new Uint8Array(MSG_BODY_HEADER_BYTES + ctBytes.length);
9288
- out[0] = MSG_BODY_VERSION;
9289
- out[1] = suite;
9290
- out[2] = flags;
9291
- out.set(nonceBytes, 3);
9292
- out.set(sigBytes, 3 + BOX_NONCE_BYTES);
9293
- out.set(ctBytes, MSG_BODY_HEADER_BYTES);
9294
- return out;
9295
9281
  }
9296
- function unpackMsgBody(body) {
9297
- const bytes = toBytes(body, "chat message body");
9298
- if (bytes.length <= MSG_BODY_HEADER_BYTES || bytes[0] !== MSG_BODY_VERSION) {
9299
- throw new Error("unsupported chat message body");
9282
+ function assertLineage(manifest, parent) {
9283
+ const memberCount = manifest.members.length;
9284
+ if (memberCount > 2 && manifest.lineage !== CHAT_LINEAGES.GROUP) {
9285
+ throw new Error("group lineage required");
9300
9286
  }
9301
- if (bytes[1] !== MSG_BODY_SUITE_XCHACHA_ED25519 || bytes[2] !== MSG_BODY_FLAGS_NONE) {
9302
- throw new Error("unsupported chat message suite");
9287
+ if (!parent) {
9288
+ return;
9289
+ }
9290
+ if (manifest.chatId !== parent.chatId || manifest.parentEpochId !== parent.epochId || manifest.epochVersion !== parent.epochVersion + 1) {
9291
+ throw new Error("invalid chat epoch successor");
9292
+ }
9293
+ if (parent.lineage === CHAT_LINEAGES.GROUP && manifest.lineage !== CHAT_LINEAGES.GROUP) {
9294
+ throw new Error("chat group lineage is permanent");
9303
9295
  }
9304
- return {
9305
- suite: bytes[1],
9306
- flags: bytes[2],
9307
- nonce: bytes.subarray(3, 3 + BOX_NONCE_BYTES),
9308
- sig: bytes.subarray(3 + BOX_NONCE_BYTES, MSG_BODY_HEADER_BYTES),
9309
- ct: bytes.subarray(MSG_BODY_HEADER_BYTES)
9310
- };
9311
9296
  }
9312
- function normalizeOpenedAction(epoch, head, action) {
9313
- if (!action || typeof action !== "object" || Array.isArray(action) || action.v !== CHAT_ACTION_VERSION) {
9314
- throw new Error("unsupported chat action");
9297
+ function normalizeEpochManifest(value, options = {}) {
9298
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
9299
+ throw new Error("chat manifest required");
9315
9300
  }
9316
- const id = cleanText(action.id);
9317
- if (!id || id !== head.cid) {
9318
- throw new Error("chat action id mismatch");
9301
+ if (value.v !== CHAT_MANIFEST_VERSION || value.protocol !== CHAT_PROTOCOL_VERSION) {
9302
+ throw new Error("unsupported chat manifest");
9319
9303
  }
9320
- const sender = cleanChatHex(action.actor, "chat action actor");
9321
- const member = manifestMember(epoch.manifest, sender);
9322
- if (!member) {
9323
- throw new Error("chat action actor is not an epoch member");
9304
+ if (!Array.isArray(value.members) || value.members.length < 1 || value.members.length > CHAT_MAX_MEMBERS) {
9305
+ throw new Error("invalid chat member count");
9324
9306
  }
9325
- return {
9326
- op: cleanChatActionOp(action.op),
9327
- id,
9328
- target: cleanText(action.target),
9329
- sender,
9330
- signingPublicKey: member.chatSigningPK,
9331
- ts: cleanActionTimestamp(action.ts),
9332
- payload: normalizePlainPayload(action.p)
9307
+ const lineage = cleanText(value.lineage);
9308
+ if (!LINEAGES.has(lineage)) {
9309
+ throw new Error("invalid chat lineage");
9310
+ }
9311
+ const members = value.members.map(cleanMember).sort((a, b) => a.chatPK.localeCompare(b.chatPK));
9312
+ assertUniqueMembers(members);
9313
+ const manifest = {
9314
+ v: CHAT_MANIFEST_VERSION,
9315
+ protocol: CHAT_PROTOCOL_VERSION,
9316
+ chatId: cleanChatHex(value.chatId, "chat id"),
9317
+ epochId: cleanChatHex(value.epochId, "chat epoch id"),
9318
+ epochVersion: cleanEpochVersion(value.epochVersion),
9319
+ parentEpochId: value.parentEpochId == null ? null : cleanChatHex(value.parentEpochId, "parent epoch id"),
9320
+ createdAt: cleanCreatedAt(value.createdAt),
9321
+ lineage,
9322
+ members
9333
9323
  };
9334
- }
9335
- async function openMsgPlaintext(epoch, nonce, ct, aad, options) {
9336
- const crypto = options?.crypto;
9337
- if (typeof crypto?.openBox === "function" && (typeof crypto.isAvailable !== "function" || crypto.isAvailable())) {
9338
- return crypto.openBox(epoch.bodyKey, nonce, ct, aad);
9324
+ if (manifest.epochVersion === 1 !== (manifest.parentEpochId === null)) {
9325
+ throw new Error("invalid parent epoch");
9339
9326
  }
9340
- return openBox(epoch.bodyKey, nonce, ct, aad);
9341
- }
9342
- async function verifyMsgSignature(publicKey, sig, bytes, options) {
9343
- const crypto = options?.crypto;
9344
- if (typeof crypto?.verifyChatBytes === "function" && (typeof crypto.isAvailable !== "function" || crypto.isAvailable())) {
9345
- return crypto.verifyChatBytes(publicKey, sig, bytes);
9327
+ const bytes = canonicalBytes(manifest, "chat manifest");
9328
+ if (bytes.length > CHAT_MANIFEST_MAX_BYTES) {
9329
+ throw new Error("chat manifest too large");
9346
9330
  }
9347
- return verifyChatBytes(publicKey, toHex(sig), bytes);
9331
+ assertLineage(manifest, options.parent || null);
9332
+ return manifest;
9348
9333
  }
9349
- async function preverifySigner(epoch, sig, bytes, options) {
9350
- if (!options?.crypto?.preverifyBeforeOpen)
9351
- return "";
9352
- for (const member of epoch.manifest.members) {
9353
- if (await verifyMsgSignature(member.chatSigningPK, sig, bytes, options)) {
9354
- return member.chatSigningPK;
9355
- }
9356
- }
9357
- return null;
9334
+ function epochManifestDigest(manifest) {
9335
+ return toHex(sha256(canonicalBytes(normalizeEpochManifest(manifest), "chat manifest digest")));
9358
9336
  }
9359
- async function sealMsg(epoch, message, options = {}) {
9360
- if (!epoch?.bodyKey || !epoch?.actor?.signingKey) {
9361
- throw new Error("chat epoch sender required");
9362
- }
9363
- const head = getHead(message);
9364
- const action = makeMsgAction(epoch, head, message, options);
9365
- const aad = getMsgAad(epoch, head);
9366
- const plaintext = encoder.encode(JSON.stringify(action));
9367
- try {
9368
- const { nonce, ct } = await sealBox(epoch.bodyKey, plaintext, aad);
9369
- const sig = signChatBytes(epoch.actor.signingKey, getSignatureInput(aad, nonce, ct));
9370
- return { head, body: packMsgBody({ nonce, sig, ct }) };
9371
- } finally {
9372
- cleanBytes(plaintext);
9373
- }
9337
+ function manifestMember(manifest, chatPK) {
9338
+ const key = cleanChatHex(chatPK, "member chat key");
9339
+ return normalizeEpochManifest(manifest).members.find((member) => member.chatPK === key) || null;
9374
9340
  }
9375
- async function openMsg(epoch, data, options = {}) {
9376
- if (!hasMsgData(data) || !epoch?.bodyKey || !epoch?.manifest) {
9377
- throw new Error("invalid chat message");
9378
- }
9379
- const head = data.head;
9380
- const { suite, flags, nonce, sig, ct } = unpackMsgBody(data.body);
9381
- const aad = getMsgAad(epoch, head, suite, flags);
9382
- const signedBytes = getSignatureInput(aad, nonce, ct);
9383
- const preverified = await preverifySigner(epoch, sig, signedBytes, options);
9384
- if (preverified == null) {
9385
- throw new Error("invalid chat action signature");
9386
- }
9387
- const plaintext = await openMsgPlaintext(epoch, nonce, ct, aad, options);
9341
+ function manifestSigningKeys(manifest) {
9342
+ return Object.fromEntries(normalizeEpochManifest(manifest).members.map((member) => [member.chatPK, member.chatSigningPK]));
9343
+ }
9344
+
9345
+ // ../../core/chat/direct.js
9346
+ "use client";
9347
+ function orderedChatKeys(first, second) {
9348
+ return [
9349
+ cleanChatHex(first, "chat public key"),
9350
+ cleanChatHex(second, "peer chat public key")
9351
+ ].sort();
9352
+ }
9353
+ function findDirectChat(chats, peerChatPK) {
9354
+ const peer = cleanChatHex(peerChatPK, "peer chat public key");
9355
+ return (chats || []).find((chat) => chat?.lineage === "direct" && chat?.peerChatPK === peer) || null;
9356
+ }
9357
+ function deriveDirectRouteId(chatPrivateKey, chatPK, peerChatPK) {
9358
+ const ownChatPK = cleanChatHex(chatPK, "chat public key");
9359
+ const otherChatPK = cleanChatHex(peerChatPK, "peer chat public key");
9360
+ if (ownChatPK === otherChatPK) {
9361
+ throw new Error("direct peer chat key required");
9362
+ }
9363
+ let peerKey = null;
9364
+ let shared = null;
9365
+ let routeId = null;
9388
9366
  try {
9389
- const normalized = normalizeOpenedAction(epoch, head, JSON.parse(decoder.decode(plaintext)));
9390
- const valid = preverified ? normalized.signingPublicKey === preverified : await verifyMsgSignature(normalized.signingPublicKey, sig, signedBytes, options);
9391
- if (!valid) {
9392
- throw new Error("invalid chat action signature");
9393
- }
9394
- return {
9395
- ...normalized.payload,
9396
- id: normalized.target && normalized.op !== CHAT_ACTION_OPS.CREATE ? normalized.target : undefined,
9397
- cid: normalized.id,
9398
- actionId: normalized.id,
9399
- actionOp: normalized.op,
9400
- actionTarget: normalized.target,
9401
- signingPublicKey: normalized.signingPublicKey,
9402
- from: normalized.sender,
9403
- s: normalized.sender
9404
- };
9367
+ peerKey = fromHex(otherChatPK, "peer chat public key");
9368
+ shared = x25519.getSharedSecret(toBytes32(chatPrivateKey, "chat private key"), peerKey);
9369
+ routeId = deriveKey(shared, "direct-route-id-v3", orderedChatKeys(ownChatPK, otherChatPK));
9370
+ return toHex(routeId);
9405
9371
  } finally {
9406
- cleanBytes(plaintext);
9407
- }
9408
- }
9409
- async function openMessageBatchV3Default(epoch, records, options) {
9410
- const opened = [];
9411
- for (const record of records || []) {
9412
- try {
9413
- const message = await openMsg(epoch, record, options);
9414
- opened.push({ recordId: batchRecordId(record), message });
9415
- } catch {
9416
- opened.push(null);
9417
- }
9372
+ cleanBytes(peerKey, shared, routeId);
9418
9373
  }
9419
- return opened;
9420
9374
  }
9421
- async function openMessageBatchV3(epoch, records, options = {}) {
9422
- const source = Array.isArray(records) ? records : [];
9423
- if (!source.length)
9424
- return [];
9425
- if (!epoch?.chatId || !epoch?.epochId || !epoch?.bodyKey || !epoch?.manifest) {
9426
- throw new Error("chat batch epoch required");
9427
- }
9428
- const crypto = options.crypto;
9429
- if (typeof crypto?.openMessageBatchV3 === "function" && (typeof crypto.isAvailable !== "function" || crypto.isAvailable())) {
9430
- const opened = await crypto.openMessageBatchV3(makeBatchOpenRequest(epoch, source));
9431
- if (!Array.isArray(opened) || opened.length !== source.length) {
9432
- throw new Error("invalid chat message batch result");
9433
- }
9434
- return opened;
9375
+ function deriveSelfChatId(chatPrivateKey, chatPK) {
9376
+ const ownChatPK = cleanChatHex(chatPK, "chat public key");
9377
+ let chatId = null;
9378
+ try {
9379
+ chatId = deriveKey(toBytes32(chatPrivateKey, "chat private key"), "self-chat-id-v3", [ownChatPK]);
9380
+ return toHex(chatId);
9381
+ } finally {
9382
+ cleanBytes(chatId);
9435
9383
  }
9436
- return openMessageBatchV3Default(epoch, source, options);
9437
9384
  }
9438
9385
 
9439
- // ../../core/chat/avatar.js
9440
- "use client";
9441
- var CHAT_AVATAR_PREFIX = "data:image/webp;base64,";
9442
- var BASE64_RE = /^[A-Za-z0-9+/]+={0,2}$/u;
9443
- function cleanChatAvatarRef(value) {
9444
- if (value == null)
9445
- return null;
9446
- const ref = cleanText(value);
9447
- if (!ref || ref.length > CHAT_AVATAR_REF_MAX_CHARS) {
9448
- throw new Error("invalid chat avatar");
9449
- }
9450
- return ref;
9386
+ // ../../core/chat/errors.js
9387
+ function makeChatUnavailableError() {
9388
+ const error = new Error("chat unavailable");
9389
+ error.code = "permission-denied";
9390
+ return error;
9451
9391
  }
9452
- function chatAvatarDataUrl(value) {
9453
- const ref = typeof value === "string" ? value.trim() : "";
9454
- if (!ref.startsWith(CHAT_AVATAR_PREFIX) || ref.length > CHAT_AVATAR_REF_MAX_CHARS)
9455
- return "";
9456
- const body = ref.slice(CHAT_AVATAR_PREFIX.length);
9457
- if (!body || body.length % 4 !== 0 || !BASE64_RE.test(body))
9458
- return "";
9459
- const padding2 = body.endsWith("==") ? 2 : body.endsWith("=") ? 1 : 0;
9460
- const bytes = body.length / 4 * 3 - padding2;
9461
- return bytes > 0 && bytes <= CHAT_AVATAR_IMAGE_MAX_BYTES ? ref : "";
9392
+ function isChatEpochConflict(error) {
9393
+ const code = String(error?.code || "").trim().toLowerCase();
9394
+ const message = String(error?.message || "").trim().toLowerCase();
9395
+ return code === "permission-denied" || code.endsWith("/permission-denied") || code === "aborted" || code.endsWith("/aborted") || code === "failed-precondition" || code.endsWith("/failed-precondition") || message.includes("insufficient permissions") || message === "chat owner epoch changed";
9462
9396
  }
9463
-
9464
- // ../../core/chat/epochs/transition.js
9465
- "use client";
9466
- var TRANSITION_SIGN_SCOPE = encoder.encode("veyl-chat-transition-v3");
9467
- var TRANSITION_MEMBER_SCOPE = encoder.encode("veyl-chat-transition-member-v3");
9468
- function cleanTime(value) {
9469
- if (!Number.isSafeInteger(value) || value <= 0) {
9470
- throw new Error("chat transition time required");
9471
- }
9472
- return value;
9397
+ function makeMessageSaveUnavailableError() {
9398
+ const error = new Error("this message can't be saved anymore");
9399
+ error.code = "message-unavailable";
9400
+ return error;
9473
9401
  }
9474
- function normalizeTransitionCore(value) {
9475
- if (!value || typeof value !== "object" || Array.isArray(value)) {
9476
- throw new Error("chat transition required");
9477
- }
9478
- if (value.v !== CHAT_TRANSITION_VERSION || value.protocol !== CHAT_PROTOCOL_VERSION) {
9479
- throw new Error("unsupported chat transition");
9402
+ function isMessageSaveUnavailableError(error) {
9403
+ return String(error?.code || "").toLowerCase() === "message-unavailable";
9404
+ }
9405
+ function normalizeMessageSaveError(error) {
9406
+ if (isMessageSaveUnavailableError(error)) {
9407
+ return error;
9480
9408
  }
9481
- const parentEpochVersion = value.parentEpochVersion;
9482
- const nextEpochVersion = value.nextEpochVersion;
9483
- if (!Number.isSafeInteger(parentEpochVersion) || parentEpochVersion <= 0 || nextEpochVersion !== parentEpochVersion + 1) {
9484
- throw new Error("invalid chat transition version");
9409
+ const code = String(error?.code || "").toLowerCase();
9410
+ if (code === "7" || code === "not-found" || code.endsWith("/not-found") || code === "permission-denied" || code.endsWith("/permission-denied")) {
9411
+ return makeMessageSaveUnavailableError();
9485
9412
  }
9486
- return {
9487
- v: CHAT_TRANSITION_VERSION,
9488
- protocol: CHAT_PROTOCOL_VERSION,
9489
- chatId: cleanChatHex(value.chatId, "chat id"),
9490
- parentEpochId: cleanChatHex(value.parentEpochId, "parent epoch id"),
9491
- parentEpochVersion,
9492
- nextEpochId: cleanChatHex(value.nextEpochId, "next epoch id"),
9493
- nextEpochVersion,
9494
- nextManifestDigest: cleanChatHex(value.nextManifestDigest, "manifest digest"),
9495
- nextStateCapabilityCommitment: cleanChatHex(value.nextStateCapabilityCommitment, "state capability commitment"),
9496
- proposalId: cleanChatHex(value.proposalId, "transition proposal id"),
9497
- createdAt: cleanTime(value.createdAt)
9498
- };
9413
+ return error;
9499
9414
  }
9500
- function transitionCommitment(value) {
9501
- return toHex(sha256(canonicalBytes(normalizeTransitionCore(value), "chat transition commitment")));
9415
+
9416
+ // ../../core/chat/epochs/state.js
9417
+ "use client";
9418
+ var CAPABILITY_WITNESS_SCOPE = "veyl-chat-state-witness-v3:";
9419
+ var CAPABILITY_COMMITMENT_SCOPE = "veyl-chat-state-commitment-v3:";
9420
+ function randomChatId() {
9421
+ return toHex(randomBytes3(32));
9502
9422
  }
9503
- function signatureInput(core) {
9504
- return concatBytes4(TRANSITION_SIGN_SCOPE, canonicalBytes(core, "chat transition signature"));
9423
+ function hashText(value) {
9424
+ return toHex(sha256(encoder.encode(value)));
9505
9425
  }
9506
- function createTransitionCertificate(parentValue, nextValue, nextStateCapability, actor, options = {}) {
9507
- const parent = normalizeEpochManifest(parentValue);
9508
- const next = normalizeEpochManifest(nextValue, { parent });
9509
- const actorChatPK = cleanChatHex(actor?.chatPK, "transition actor");
9510
- const parentMember = manifestMember(parent, actorChatPK);
9511
- if (!parentMember || parentMember.chatSigningPK !== cleanText(actor?.signingKey?.publicKey).toLowerCase()) {
9512
- throw new Error("transition actor is not a parent member");
9513
- }
9514
- const core = normalizeTransitionCore({
9515
- v: CHAT_TRANSITION_VERSION,
9516
- protocol: CHAT_PROTOCOL_VERSION,
9517
- chatId: parent.chatId,
9518
- parentEpochId: parent.epochId,
9519
- parentEpochVersion: parent.epochVersion,
9520
- nextEpochId: next.epochId,
9521
- nextEpochVersion: next.epochVersion,
9522
- nextManifestDigest: epochManifestDigest(next),
9523
- nextStateCapabilityCommitment: stateCapabilityCommitment(nextStateCapability),
9524
- proposalId: options.proposalId || toHex(randomBytes3(32)),
9525
- createdAt: options.createdAt || Date.now()
9526
- });
9527
- return {
9528
- ...core,
9529
- actor: actorChatPK,
9530
- signature: signChatBytes(actor.signingKey, signatureInput(core))
9531
- };
9426
+ function stateCapabilityWitness(value) {
9427
+ return hashText(`${CAPABILITY_WITNESS_SCOPE}${toHex(toBytes32(value, "state capability"))}`);
9532
9428
  }
9533
- function verifyTransitionCertificate(parentValue, nextValue, certificate) {
9534
- const parent = normalizeEpochManifest(parentValue);
9535
- const next = normalizeEpochManifest(nextValue, { parent });
9536
- const core = normalizeTransitionCore(certificate);
9537
- const actor = cleanChatHex(certificate?.actor, "transition actor");
9538
- const member = manifestMember(parent, actor);
9539
- if (!member || core.chatId !== parent.chatId || core.parentEpochId !== parent.epochId || core.parentEpochVersion !== parent.epochVersion || core.nextEpochId !== next.epochId || core.nextEpochVersion !== next.epochVersion || core.nextManifestDigest !== epochManifestDigest(next)) {
9540
- return false;
9541
- }
9542
- return verifyTransitionSignature(certificate, member.chatSigningPK);
9429
+ function stateCapabilityCommitment(value) {
9430
+ return hashText(`${CAPABILITY_COMMITMENT_SCOPE}${stateCapabilityWitness(value)}`);
9543
9431
  }
9544
- function verifyTransitionSuccessor(nextValue, certificate) {
9432
+ function deriveEpochKeys(epochSecret, manifestValue) {
9433
+ const manifest = normalizeEpochManifest(manifestValue);
9434
+ const root = deriveKey(toBytes32(epochSecret, "epoch secret"), "chat-epoch-root-v3", [
9435
+ manifest.chatId,
9436
+ manifest.epochId,
9437
+ manifest.epochVersion
9438
+ ]);
9439
+ let settingsIdBytes = null;
9440
+ let messageLaneBytes = null;
9441
+ let stateLaneBytes = null;
9545
9442
  try {
9546
- const next = normalizeEpochManifest(nextValue);
9547
- const core = normalizeTransitionCore(certificate);
9548
- return core.chatId === next.chatId && core.nextEpochId === next.epochId && core.nextEpochVersion === next.epochVersion && core.nextManifestDigest === epochManifestDigest(next);
9549
- } catch {
9550
- return false;
9443
+ settingsIdBytes = deriveKey(root, "chat-epoch-settings-id-v3");
9444
+ messageLaneBytes = deriveKey(root, "chat-epoch-message-lane-v3");
9445
+ stateLaneBytes = deriveKey(root, "chat-epoch-member-state-lane-v3");
9446
+ return {
9447
+ root,
9448
+ bodyKey: deriveKey(root, "chat-epoch-body-v3"),
9449
+ settingsKey: deriveKey(root, "chat-epoch-settings-v3"),
9450
+ settingsId: toHex(settingsIdBytes),
9451
+ messageLane: toHex(messageLaneBytes),
9452
+ stateLane: toHex(stateLaneBytes),
9453
+ stateKey: deriveKey(root, "chat-epoch-member-state-v3")
9454
+ };
9455
+ } catch (error) {
9456
+ cleanBytes(root);
9457
+ throw error;
9458
+ } finally {
9459
+ cleanBytes(settingsIdBytes, messageLaneBytes, stateLaneBytes);
9551
9460
  }
9552
9461
  }
9553
- function verifyTransitionSignature(certificate, signingPublicKey) {
9462
+ function closeEpochKeys(keys) {
9463
+ cleanBytes(keys?.root, keys?.bodyKey, keys?.settingsKey, keys?.stateKey);
9464
+ }
9465
+ function ownerEpochEntryId(chatPrivateKey, chatId, epochId) {
9466
+ const value = deriveKey(toBytes32(chatPrivateKey, "chat private key"), "user-chat-epoch-entry-id-v3", [
9467
+ cleanChatHex(chatId, "chat id"),
9468
+ cleanChatHex(epochId, "chat epoch id")
9469
+ ], 16);
9554
9470
  try {
9555
- const core = normalizeTransitionCore(certificate);
9556
- const signature = cleanText(certificate?.signature).toLowerCase();
9557
- return verifyChatBytes(cleanChatHex(signingPublicKey, "transition signing key"), signature, signatureInput(core));
9558
- } catch {
9559
- return false;
9471
+ return toHex(value);
9472
+ } finally {
9473
+ cleanBytes(value);
9560
9474
  }
9561
9475
  }
9562
- function transitionCore(certificate) {
9563
- return normalizeTransitionCore(certificate);
9476
+ function secretHex(value, label) {
9477
+ return typeof value === "string" ? cleanChatHex(value, label) : toHex(toBytes32(value, label));
9478
+ }
9479
+ function secretBytes(value, label) {
9480
+ return fromHex(cleanChatHex(value, label), label);
9564
9481
  }
9565
9482
 
9566
- // ../../core/chat/ttl.js
9483
+ // ../../core/crypto/base64.js
9567
9484
  "use client";
9568
- var CHAT_RETENTION_SEEN = "seen";
9569
- var CHAT_RETENTION_24H = "24h";
9570
- var CHAT_RETENTION_FOREVER = "forever";
9571
- var DEFAULT_CHAT_RETENTION = CHAT_RETENTION_SEEN;
9572
- var CHAT_RETENTION_VALUES = Object.freeze([CHAT_RETENTION_SEEN, CHAT_RETENTION_24H]);
9573
- var CHAT_PROTOCOL_RETENTION_VALUES = new Set([...CHAT_RETENTION_VALUES, CHAT_RETENTION_FOREVER]);
9574
- var CHAT_RETENTION_LABELS = Object.freeze({
9575
- [CHAT_RETENTION_SEEN]: "delete after seen",
9576
- [CHAT_RETENTION_24H]: "keep for 24h"
9577
- });
9578
- var MESSAGE_STORAGE_TTL_MS = CHAT_UNSAVED_TTL_MS;
9579
- var AFTER_SEEN_MS = CHAT_AFTER_SEEN_MS;
9580
- function hasChatRetention(value) {
9581
- const retention = cleanText(value);
9582
- return CHAT_PROTOCOL_RETENTION_VALUES.has(retention);
9583
- }
9584
- function cleanChatRetention(value) {
9585
- const retention = cleanText(value);
9586
- return hasChatRetention(retention) ? retention : DEFAULT_CHAT_RETENTION;
9587
- }
9588
- function normalizeChatSettings(settings) {
9589
- const retention = cleanChatRetention(settings?.retention);
9590
- return { retention };
9591
- }
9592
- function getMessageRetention(message, fallback = DEFAULT_CHAT_RETENTION) {
9593
- if (hasChatRetention(message?.retention)) {
9594
- return cleanChatRetention(message.retention);
9485
+ var ALPHABET = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_";
9486
+ var VALUE_BY_CHAR = new Map([...ALPHABET].map((char, index) => [char, index]));
9487
+ function bytesBase64Url(value, label = "base64url bytes") {
9488
+ const bytes = toBytes(value, label);
9489
+ let output = "";
9490
+ for (let index = 0;index < bytes.length; index += 3) {
9491
+ const a = bytes[index];
9492
+ const hasB = index + 1 < bytes.length;
9493
+ const hasC = index + 2 < bytes.length;
9494
+ const b = hasB ? bytes[index + 1] : 0;
9495
+ const c = hasC ? bytes[index + 2] : 0;
9496
+ output += ALPHABET[a >>> 2];
9497
+ output += ALPHABET[(a & 3) << 4 | b >>> 4];
9498
+ if (hasB)
9499
+ output += ALPHABET[(b & 15) << 2 | c >>> 6];
9500
+ if (hasC)
9501
+ output += ALPHABET[c & 63];
9595
9502
  }
9596
- return cleanChatRetention(fallback);
9597
- }
9598
- function retentionPatch(message) {
9599
- return hasChatRetention(message?.retention) ? { retention: cleanChatRetention(message.retention) } : {};
9503
+ return output;
9600
9504
  }
9601
- function withMessageRetention(message, retention = DEFAULT_CHAT_RETENTION) {
9602
- if (!message || typeof message !== "object" || Array.isArray(message)) {
9603
- return message;
9505
+ function base64UrlBytes(value, label = "base64url") {
9506
+ const text = typeof value === "string" ? value.trim() : "";
9507
+ if (!text || text.length % 4 === 1 || !/^[A-Za-z0-9_-]+$/u.test(text)) {
9508
+ throw new Error(`invalid ${label}`);
9604
9509
  }
9605
- const nextRetention = getMessageRetention(message, retention);
9606
- return message.retention === nextRetention ? message : { ...message, retention: nextRetention };
9607
- }
9608
- function ttlMillis(value) {
9609
- return timestampMs(value, null, { positive: true });
9610
- }
9611
- function isTtlExpired(value, now = Date.now()) {
9612
- const ms = ttlMillis(value);
9613
- return ms != null && ms <= now;
9510
+ const output = new Uint8Array(Math.floor(text.length * 6 / 8));
9511
+ let bits = 0;
9512
+ let bitCount = 0;
9513
+ let offset = 0;
9514
+ for (const char of text) {
9515
+ const next = VALUE_BY_CHAR.get(char);
9516
+ if (next == null)
9517
+ throw new Error(`invalid ${label}`);
9518
+ bits = bits << 6 | next;
9519
+ bitCount += 6;
9520
+ if (bitCount >= 8) {
9521
+ bitCount -= 8;
9522
+ output[offset++] = bits >>> bitCount & 255;
9523
+ bits &= bitCount ? (1 << bitCount) - 1 : 0;
9524
+ }
9525
+ }
9526
+ return output;
9614
9527
  }
9615
- function newMessageStorageTtlMs(now = Date.now()) {
9616
- return now + MESSAGE_STORAGE_TTL_MS;
9528
+
9529
+ // ../../core/chat/messages/actions.js
9530
+ "use client";
9531
+ var CHAT_ACTION_VERSION = 3;
9532
+ var CHAT_ACTION_OPS = Object.freeze({
9533
+ CREATE: "create",
9534
+ EDIT: "edit",
9535
+ PAY_CONFIRM: "pay_confirm",
9536
+ REACTION: "rxn",
9537
+ DELETE: "del",
9538
+ SYSTEM: "sys",
9539
+ EPOCH: "epoch"
9540
+ });
9541
+ var CHAT_ACTION_OP_SET = new Set(Object.values(CHAT_ACTION_OPS));
9542
+ var CONTROL_OP_BY_TYPE = Object.freeze({
9543
+ rxn: CHAT_ACTION_OPS.REACTION,
9544
+ del: CHAT_ACTION_OPS.DELETE,
9545
+ sys: CHAT_ACTION_OPS.SYSTEM,
9546
+ epoch: CHAT_ACTION_OPS.EPOCH
9547
+ });
9548
+ function isChatActionOp(value) {
9549
+ return CHAT_ACTION_OP_SET.has(value);
9617
9550
  }
9618
- function messageExpiryMs(retention, seenAt) {
9619
- if (cleanChatRetention(retention) === CHAT_RETENTION_FOREVER) {
9620
- return null;
9621
- }
9622
- if (!Number.isFinite(seenAt)) {
9623
- return null;
9551
+ function cleanChatActionOp(value) {
9552
+ const op = cleanText(value);
9553
+ if (!isChatActionOp(op)) {
9554
+ throw new Error("unsupported chat action op");
9624
9555
  }
9625
- return cleanChatRetention(retention) === CHAT_RETENTION_24H ? seenAt + AFTER_SEEN_MS : seenAt;
9556
+ return op;
9557
+ }
9558
+ function actionOpForPayload(payload, fallback = CHAT_ACTION_OPS.CREATE) {
9559
+ return CONTROL_OP_BY_TYPE[cleanText(payload?.t)] || fallback;
9626
9560
  }
9627
9561
 
9628
- // ../../core/chat/settings.js
9562
+ // ../../core/crypto/chat.js
9629
9563
  "use client";
9630
- var SETTINGS_SIGN_SCOPE = encoder.encode("veyl-chat-settings-v3-sign");
9631
- var SIGNATURE_RE = /^[0-9a-f]{128}$/u;
9632
- function cleanRevision(value) {
9633
- if (!Number.isSafeInteger(value) || value <= 0) {
9634
- throw new Error("chat settings revision required");
9635
- }
9636
- return value;
9564
+ var MSG_BODY_VERSION = CHAT_MESSAGE_ENVELOPE_VERSION;
9565
+ var MSG_BODY_SUITE_XCHACHA_ED25519 = 1;
9566
+ var MSG_SIGN_SCOPE_BYTES = encoder.encode("veyl-chat-msg-v3-sign");
9567
+ var MSG_BODY_FLAGS_NONE = 0;
9568
+ var ED25519_SIGNATURE_BYTES = 64;
9569
+ var MSG_BODY_HEADER_BYTES = 1 + 1 + 1 + BOX_NONCE_BYTES + ED25519_SIGNATURE_BYTES;
9570
+ function hasMsgHead(data) {
9571
+ return typeof data?.head?.cid === "string" && !!data.head.cid;
9637
9572
  }
9638
- function cleanPreviousDigest(value) {
9639
- return value == null ? null : cleanChatHex(value, "previous chat settings digest");
9573
+ function hasMsgBody(data) {
9574
+ return data?.body != null;
9640
9575
  }
9641
- function cleanEventId(value) {
9642
- const eventId = value == null ? null : cleanText(value);
9643
- if (eventId != null && (!eventId || eventId.length > 128 || eventId.includes("/"))) {
9644
- throw new Error("invalid chat settings event");
9645
- }
9646
- return eventId;
9576
+ function hasMsgData(data) {
9577
+ return hasMsgHead(data) && hasMsgBody(data);
9647
9578
  }
9648
- function normalizeTransitionAuthorization(value, epoch, actor) {
9649
- if (value == null)
9650
- return null;
9651
- if (!value || typeof value !== "object" || Array.isArray(value) || value.kind !== "transition") {
9652
- throw new Error("invalid chat settings authorization");
9579
+ function cleanSigningKey(value) {
9580
+ const secret = value?.secret;
9581
+ const publicKey = cleanChatHex(value?.publicKey, "chat signing public key");
9582
+ if (!secret) {
9583
+ throw new Error("chat signing secret required");
9653
9584
  }
9654
- const signingPK = cleanChatHex(value.signingPK, "chat settings transition signing key");
9655
- const certificateCore = transitionCore(value.certificate);
9656
- const certificate = {
9657
- ...certificateCore,
9658
- actor: cleanChatHex(value.certificate?.actor, "transition actor"),
9659
- signature: cleanText(value.certificate?.signature).toLowerCase()
9660
- };
9661
- if (certificate.actor !== actor || !SIGNATURE_RE.test(certificate.signature) || transitionCommitment(certificate) !== cleanChatHex(epoch?.transitionCommitment, "transition commitment") || !verifyTransitionSuccessor(epoch.manifest, certificate) || !verifyTransitionSignature(certificate, signingPK)) {
9662
- throw new Error("invalid chat settings transition authorization");
9585
+ return { secret, publicKey };
9586
+ }
9587
+ function openChatEpoch({ manifest: manifestValue, epochSecret, actor = null } = {}) {
9588
+ const manifest = normalizeEpochManifest(manifestValue);
9589
+ const keys = deriveEpochKeys(epochSecret, manifest);
9590
+ let normalizedActor = null;
9591
+ try {
9592
+ if (actor) {
9593
+ const chatPK = cleanChatHex(actor.chatPK, "chat actor key");
9594
+ const signingKey = cleanSigningKey(actor.signingKey);
9595
+ const member = manifestMember(manifest, chatPK);
9596
+ if (!member || member.chatSigningPK !== signingKey.publicKey) {
9597
+ throw new Error("chat actor is not an epoch member");
9598
+ }
9599
+ normalizedActor = { chatPK, signingKey };
9600
+ }
9601
+ return {
9602
+ protocol: CHAT_PROTOCOL_VERSION,
9603
+ chatId: manifest.chatId,
9604
+ epochId: manifest.epochId,
9605
+ epochVersion: manifest.epochVersion,
9606
+ manifest,
9607
+ actor: normalizedActor,
9608
+ ...keys
9609
+ };
9610
+ } catch (error) {
9611
+ closeEpochKeys(keys);
9612
+ throw error;
9663
9613
  }
9664
- return { kind: "transition", signingPK, certificate };
9665
9614
  }
9666
- function withAuthorization(core, authorization) {
9667
- return authorization ? { ...core, authorization } : core;
9615
+ function closeChatEpoch(epoch) {
9616
+ closeEpochKeys(epoch);
9668
9617
  }
9669
- function normalizeChatStateSettings(value = {}) {
9670
- if (!value || typeof value !== "object" || Array.isArray(value)) {
9671
- throw new Error("chat settings required");
9672
- }
9673
- const title = value.title == null ? null : cleanText(value.title);
9674
- if (title != null && (!title || title.length > 128)) {
9675
- throw new Error("invalid chat title");
9618
+ function getHead(message) {
9619
+ const cid = cleanText(message?.cid);
9620
+ if (!cid || cid.length > 256) {
9621
+ throw new Error("message cid required");
9676
9622
  }
9677
- return {
9678
- title,
9679
- avatarRef: cleanChatAvatarRef(value.avatarRef),
9680
- retention: cleanChatRetention(value.retention)
9681
- };
9623
+ return { cid };
9682
9624
  }
9683
- function settingsCore(epoch, settings, options = {}) {
9684
- const actor = cleanChatHex(epoch?.actor?.chatPK, "chat settings actor");
9685
- const authorization = normalizeTransitionAuthorization(options.authorization, epoch, actor);
9686
- if (!epoch?.settingsKey || !epoch?.settingsId || !epoch?.actor?.signingKey || !manifestMember(epoch.manifest, actor) && !authorization) {
9687
- throw new Error("chat settings epoch actor required");
9688
- }
9689
- return withAuthorization({
9690
- v: CHAT_SETTINGS_VERSION,
9625
+ function getPayload(message) {
9626
+ const payload = { ...message || {} };
9627
+ delete payload.cid;
9628
+ delete payload.from;
9629
+ delete payload.s;
9630
+ return payload;
9631
+ }
9632
+ function getMsgAad(epoch, head, suite = MSG_BODY_SUITE_XCHACHA_ED25519, flags = MSG_BODY_FLAGS_NONE) {
9633
+ return canonicalBytes({
9634
+ v: MSG_BODY_VERSION,
9691
9635
  protocol: CHAT_PROTOCOL_VERSION,
9692
- chatId: cleanChatHex(epoch.chatId, "chat id"),
9693
- epochId: cleanChatHex(epoch.epochId, "chat epoch id"),
9694
- revision: cleanRevision(options.revision),
9695
- previousDigest: cleanPreviousDigest(options.previousDigest),
9696
- eventId: cleanEventId(options.eventId),
9697
- actor,
9698
- settings: normalizeChatStateSettings(settings)
9699
- }, authorization);
9636
+ chatId: epoch.chatId,
9637
+ epochId: epoch.epochId,
9638
+ epochVersion: epoch.epochVersion,
9639
+ cid: head.cid,
9640
+ suite,
9641
+ flags
9642
+ }, "chat message aad");
9700
9643
  }
9701
- function normalizeSettingsCore(value, epoch) {
9702
- if (!value || typeof value !== "object" || Array.isArray(value)) {
9703
- throw new Error("invalid chat settings state");
9704
- }
9705
- const actor = cleanChatHex(value.actor, "chat settings actor");
9706
- const authorization = normalizeTransitionAuthorization(value.authorization, epoch, actor);
9707
- const core = withAuthorization({
9708
- v: value.v,
9709
- protocol: value.protocol,
9710
- chatId: cleanChatHex(value.chatId, "chat id"),
9711
- epochId: cleanChatHex(value.epochId, "chat epoch id"),
9712
- revision: cleanRevision(value.revision),
9713
- previousDigest: cleanPreviousDigest(value.previousDigest),
9714
- eventId: cleanEventId(value.eventId),
9715
- actor,
9716
- settings: normalizeChatStateSettings(value.settings)
9717
- }, authorization);
9718
- if (core.v !== CHAT_SETTINGS_VERSION || core.protocol !== CHAT_PROTOCOL_VERSION || core.chatId !== epoch.chatId || core.epochId !== epoch.epochId || !manifestMember(epoch.manifest, core.actor) && !authorization) {
9719
- throw new Error("chat settings binding mismatch");
9720
- }
9721
- return core;
9644
+ function getSignatureInput(aad, nonce, ct) {
9645
+ return concatBytes4(MSG_SIGN_SCOPE_BYTES, aad, nonce, ct);
9722
9646
  }
9723
- function coreDigest(core) {
9724
- return toHex(sha256(canonicalBytes(core, "chat settings digest")));
9647
+ function batchRecordId(record) {
9648
+ return cleanText(record?.id) || cleanText(record?.recordId) || cleanText(record?.head?.cid);
9725
9649
  }
9726
- function settingsHead(value) {
9727
- if (!value || typeof value !== "object" || Array.isArray(value)) {
9728
- throw new Error("chat settings record required");
9729
- }
9650
+ function makeBatchOpenRequest(epoch, records) {
9730
9651
  return {
9731
- v: value.v,
9732
- protocol: value.protocol,
9733
- chatId: cleanChatHex(value.chatId, "chat id"),
9734
- epochId: cleanChatHex(value.epochId, "chat epoch id"),
9735
- revision: cleanRevision(value.revision),
9736
- previousDigest: cleanPreviousDigest(value.previousDigest),
9737
- eventId: cleanEventId(value.eventId),
9738
- digest: cleanChatHex(value.digest, "chat settings digest")
9652
+ records: (records || []).map((record) => ({
9653
+ id: batchRecordId(record),
9654
+ head: record?.head,
9655
+ body: record?.body
9656
+ })),
9657
+ protocol: CHAT_PROTOCOL_VERSION,
9658
+ chatId: epoch.chatId,
9659
+ epochId: epoch.epochId,
9660
+ epochVersion: epoch.epochVersion,
9661
+ bodyKey: epoch.bodyKey,
9662
+ manifest: epoch.manifest,
9663
+ signingKeysByChatKey: manifestSigningKeys(epoch.manifest)
9739
9664
  };
9740
9665
  }
9741
- function signatureInput2(core) {
9742
- return concatBytes4(SETTINGS_SIGN_SCOPE, canonicalBytes(core, "chat settings signature"));
9666
+ function cleanActionTimestamp(value) {
9667
+ if (typeof value !== "number" || !Number.isFinite(value)) {
9668
+ throw new Error("chat action ts required");
9669
+ }
9670
+ return Object.is(value, -0) ? 0 : value;
9743
9671
  }
9744
- function settingsAad(head) {
9745
- return canonicalBytes({
9746
- v: head.v,
9747
- protocol: head.protocol,
9748
- chatId: head.chatId,
9749
- epochId: head.epochId,
9750
- revision: head.revision,
9751
- previousDigest: head.previousDigest,
9752
- eventId: head.eventId,
9753
- digest: head.digest
9754
- }, "chat settings aad");
9672
+ function normalizePlainPayload(payload) {
9673
+ if (payload == null)
9674
+ return {};
9675
+ if (typeof payload !== "object" || Array.isArray(payload)) {
9676
+ throw new Error("invalid chat action payload");
9677
+ }
9678
+ return payload;
9755
9679
  }
9756
- async function sealChatSettings(epoch, settings, options = {}) {
9757
- const core = settingsCore(epoch, settings, options);
9758
- const digest = coreDigest(core);
9759
- const head = settingsHead({ ...core, digest });
9760
- const plaintext = encoder.encode(JSON.stringify({
9761
- core,
9762
- signature: signChatBytes(epoch.actor.signingKey, signatureInput2(core))
9763
- }));
9764
- try {
9765
- const { nonce, ct } = await sealBox(epoch.settingsKey, plaintext, settingsAad(head));
9766
- const body = concatBytes4(nonce, ct);
9767
- if (body.length > CHAT_SETTINGS_BODY_MAX_BYTES) {
9768
- throw new Error("chat settings too large");
9769
- }
9770
- return { id: epoch.settingsId, head, body, actor: core.actor, settings: core.settings };
9771
- } finally {
9772
- cleanBytes(plaintext);
9680
+ function makeMsgAction(epoch, head, message, options = {}) {
9681
+ if (!epoch?.actor?.chatPK || !epoch?.actor?.signingKey) {
9682
+ throw new Error("chat epoch actor required");
9773
9683
  }
9684
+ const target = cleanText(options.target);
9685
+ return {
9686
+ v: CHAT_ACTION_VERSION,
9687
+ op: cleanChatActionOp(cleanText(options.op) || actionOpForPayload(message)),
9688
+ id: cleanText(options.id) || head.cid,
9689
+ ...target ? { target } : {},
9690
+ actor: epoch.actor.chatPK,
9691
+ ts: Number.isFinite(options.ts) ? options.ts : Date.now(),
9692
+ p: getPayload(message)
9693
+ };
9774
9694
  }
9775
- async function openChatSettings(epoch, record) {
9776
- if (!epoch?.settingsKey || !epoch?.settingsId || record?.id !== epoch.settingsId) {
9777
- throw new Error("chat settings address mismatch");
9695
+ function packMsgBody({ suite = MSG_BODY_SUITE_XCHACHA_ED25519, flags = MSG_BODY_FLAGS_NONE, nonce, sig, ct }) {
9696
+ const nonceBytes = toBytes(nonce, "chat message nonce");
9697
+ const sigBytes = typeof sig === "string" ? fromHexBytes(sig, "chat message signature") : toBytes(sig, "chat message signature");
9698
+ const ctBytes = toBytes(ct, "chat message ciphertext");
9699
+ if (suite !== MSG_BODY_SUITE_XCHACHA_ED25519 || flags !== MSG_BODY_FLAGS_NONE) {
9700
+ throw new Error("unsupported chat message suite");
9778
9701
  }
9779
- const head = settingsHead(record.head);
9780
- if (head.v !== CHAT_SETTINGS_VERSION || head.protocol !== CHAT_PROTOCOL_VERSION || head.chatId !== epoch.chatId || head.epochId !== epoch.epochId) {
9781
- throw new Error("chat settings binding mismatch");
9702
+ if (nonceBytes.length !== BOX_NONCE_BYTES || sigBytes.length !== ED25519_SIGNATURE_BYTES) {
9703
+ throw new Error("invalid chat message signature envelope");
9782
9704
  }
9783
- const body = toBytes(record.body, "chat settings body");
9784
- if (body.length <= 24 || body.length > CHAT_SETTINGS_BODY_MAX_BYTES) {
9785
- throw new Error("invalid chat settings body");
9705
+ const out = new Uint8Array(MSG_BODY_HEADER_BYTES + ctBytes.length);
9706
+ out[0] = MSG_BODY_VERSION;
9707
+ out[1] = suite;
9708
+ out[2] = flags;
9709
+ out.set(nonceBytes, 3);
9710
+ out.set(sigBytes, 3 + BOX_NONCE_BYTES);
9711
+ out.set(ctBytes, MSG_BODY_HEADER_BYTES);
9712
+ return out;
9713
+ }
9714
+ function unpackMsgBody(body) {
9715
+ const bytes = toBytes(body, "chat message body");
9716
+ if (bytes.length <= MSG_BODY_HEADER_BYTES || bytes[0] !== MSG_BODY_VERSION) {
9717
+ throw new Error("unsupported chat message body");
9786
9718
  }
9787
- let plaintext = null;
9788
- try {
9789
- plaintext = await openBox(epoch.settingsKey, body.subarray(0, 24), body.subarray(24), settingsAad(head));
9790
- const opened = JSON.parse(decoder.decode(plaintext));
9791
- const core = normalizeSettingsCore(opened?.core, epoch);
9792
- const signature = cleanText(opened?.signature).toLowerCase();
9793
- const member = manifestMember(epoch.manifest, core.actor);
9794
- const signingPK = member?.chatSigningPK || core.authorization?.signingPK;
9795
- if (core.revision !== head.revision || core.previousDigest !== head.previousDigest || core.eventId !== head.eventId || coreDigest(core) !== head.digest || !SIGNATURE_RE.test(signature) || !signingPK || !verifyChatBytes(signingPK, signature, signatureInput2(core))) {
9796
- throw new Error("invalid chat settings signature");
9797
- }
9798
- return { id: epoch.settingsId, head, actor: core.actor, settings: core.settings };
9799
- } finally {
9800
- cleanBytes(plaintext);
9719
+ if (bytes[1] !== MSG_BODY_SUITE_XCHACHA_ED25519 || bytes[2] !== MSG_BODY_FLAGS_NONE) {
9720
+ throw new Error("unsupported chat message suite");
9801
9721
  }
9722
+ return {
9723
+ suite: bytes[1],
9724
+ flags: bytes[2],
9725
+ nonce: bytes.subarray(3, 3 + BOX_NONCE_BYTES),
9726
+ sig: bytes.subarray(3 + BOX_NONCE_BYTES, MSG_BODY_HEADER_BYTES),
9727
+ ct: bytes.subarray(MSG_BODY_HEADER_BYTES)
9728
+ };
9802
9729
  }
9803
- function chatSettingsProjection(snapshot) {
9804
- return normalizeChatSettingsProjection({
9805
- revision: cleanRevision(snapshot?.head?.revision),
9806
- digest: cleanChatHex(snapshot?.head?.digest, "chat settings digest"),
9807
- values: normalizeChatStateSettings(snapshot?.settings)
9808
- });
9809
- }
9810
- function normalizeChatSettingsProjection(value) {
9811
- if (!value || typeof value !== "object" || Array.isArray(value)) {
9812
- throw new Error("chat settings projection required");
9730
+ function normalizeOpenedAction(epoch, head, action) {
9731
+ if (!action || typeof action !== "object" || Array.isArray(action) || action.v !== CHAT_ACTION_VERSION) {
9732
+ throw new Error("unsupported chat action");
9733
+ }
9734
+ const id = cleanText(action.id);
9735
+ if (!id || id !== head.cid) {
9736
+ throw new Error("chat action id mismatch");
9737
+ }
9738
+ const sender = cleanChatHex(action.actor, "chat action actor");
9739
+ const member = manifestMember(epoch.manifest, sender);
9740
+ if (!member) {
9741
+ throw new Error("chat action actor is not an epoch member");
9813
9742
  }
9814
9743
  return {
9815
- revision: cleanRevision(value.revision),
9816
- digest: cleanChatHex(value.digest, "chat settings digest"),
9817
- values: normalizeChatStateSettings(value.values)
9744
+ op: cleanChatActionOp(action.op),
9745
+ id,
9746
+ target: cleanText(action.target),
9747
+ sender,
9748
+ signingPublicKey: member.chatSigningPK,
9749
+ ts: cleanActionTimestamp(action.ts),
9750
+ payload: normalizePlainPayload(action.p)
9818
9751
  };
9819
9752
  }
9820
- async function createEpochChatSettings(epochState, identity, values, options = {}) {
9821
- const epoch = openChatEpoch({
9822
- manifest: epochState?.manifest,
9823
- epochSecret: epochState?.epochSecret,
9824
- actor: {
9825
- chatPK: identity?.chatPK,
9826
- signingKey: {
9827
- publicKey: identity?.chatSigningPK,
9828
- secret: identity?.chatSigningSecret
9829
- }
9830
- }
9831
- });
9832
- try {
9833
- const sealed = await sealChatSettings(epoch, values, options);
9834
- return { sealed, projection: chatSettingsProjection(sealed) };
9835
- } finally {
9836
- closeChatEpoch(epoch);
9753
+ async function openMsgPlaintext(epoch, nonce, ct, aad, options) {
9754
+ const crypto = options?.crypto;
9755
+ if (typeof crypto?.openBox === "function" && (typeof crypto.isAvailable !== "function" || crypto.isAvailable())) {
9756
+ return crypto.openBox(epoch.bodyKey, nonce, ct, aad);
9837
9757
  }
9758
+ return openBox(epoch.bodyKey, nonce, ct, aad);
9838
9759
  }
9839
- async function createTransitionEpochChatSettings(epochState, identity, certificate, values, options = {}) {
9840
- const epoch = openChatEpoch({
9841
- manifest: epochState?.manifest,
9842
- epochSecret: epochState?.epochSecret
9843
- });
9844
- epoch.actor = {
9845
- chatPK: cleanChatHex(identity?.chatPK, "chat settings actor"),
9846
- signingKey: {
9847
- publicKey: cleanChatHex(identity?.chatSigningPK, "chat settings signing key"),
9848
- secret: identity?.chatSigningSecret
9849
- }
9850
- };
9851
- epoch.transitionCommitment = transitionCommitment(certificate);
9852
- try {
9853
- const sealed = await sealChatSettings(epoch, values, {
9854
- ...options,
9855
- authorization: {
9856
- kind: "transition",
9857
- signingPK: identity?.chatSigningPK,
9858
- certificate
9859
- }
9860
- });
9861
- return { sealed, projection: chatSettingsProjection(sealed) };
9862
- } finally {
9863
- closeChatEpoch(epoch);
9760
+ async function verifyMsgSignature(publicKey, sig, bytes, options) {
9761
+ const crypto = options?.crypto;
9762
+ if (typeof crypto?.verifyChatBytes === "function" && (typeof crypto.isAvailable !== "function" || crypto.isAvailable())) {
9763
+ return crypto.verifyChatBytes(publicKey, sig, bytes);
9864
9764
  }
9765
+ return verifyChatBytes(publicKey, toHex(sig), bytes);
9865
9766
  }
9866
- async function readEpochChatSettings(cloud, epochState) {
9867
- const epoch = openChatEpoch({
9868
- manifest: epochState?.manifest,
9869
- epochSecret: epochState?.epochSecret
9870
- });
9871
- epoch.transitionCommitment = cleanChatHex(epochState?.transitionCommitment, "transition commitment");
9872
- try {
9873
- const record = await cloud?.chat?.settings?.read?.(epoch.settingsId);
9874
- if (!record)
9875
- throw new Error("chat settings unavailable");
9876
- return chatSettingsProjection(await openChatSettings(epoch, record));
9877
- } finally {
9878
- closeChatEpoch(epoch);
9767
+ async function preverifySigner(epoch, sig, bytes, options) {
9768
+ if (!options?.crypto?.preverifyBeforeOpen)
9769
+ return "";
9770
+ for (const member of epoch.manifest.members) {
9771
+ if (await verifyMsgSignature(member.chatSigningPK, sig, bytes, options)) {
9772
+ return member.chatSigningPK;
9773
+ }
9879
9774
  }
9775
+ return null;
9880
9776
  }
9881
- function epochChatSettingsId(epochState) {
9882
- const epoch = openChatEpoch({
9883
- manifest: epochState?.manifest,
9884
- epochSecret: epochState?.epochSecret
9885
- });
9777
+ async function sealMsg(epoch, message, options = {}) {
9778
+ if (!epoch?.bodyKey || !epoch?.actor?.signingKey) {
9779
+ throw new Error("chat epoch sender required");
9780
+ }
9781
+ const head = getHead(message);
9782
+ const action = makeMsgAction(epoch, head, message, options);
9783
+ const aad = getMsgAad(epoch, head);
9784
+ const plaintext = encoder.encode(JSON.stringify(action));
9886
9785
  try {
9887
- return epoch.settingsId;
9786
+ const { nonce, ct } = await sealBox(epoch.bodyKey, plaintext, aad);
9787
+ const sig = signChatBytes(epoch.actor.signingKey, getSignatureInput(aad, nonce, ct));
9788
+ return { head, body: packMsgBody({ nonce, sig, ct }) };
9888
9789
  } finally {
9889
- closeChatEpoch(epoch);
9790
+ cleanBytes(plaintext);
9890
9791
  }
9891
9792
  }
9892
-
9893
- // ../../core/chat/entry.js
9894
- "use client";
9895
- var CHAT_ENTRY_VERSION = 4;
9896
- var CHAT_OWNER_EPOCH_ENTRY_VERSION = 2;
9897
- function ownChatEntryId(chatPrivateKey, chatId) {
9898
- const key = deriveKey(toBytes32(chatPrivateKey, "chat private key"), "user-chat-entry-id-v4", [cleanChatHex(chatId, "chat id")], 16);
9793
+ async function openMsg(epoch, data, options = {}) {
9794
+ if (!hasMsgData(data) || !epoch?.bodyKey || !epoch?.manifest) {
9795
+ throw new Error("invalid chat message");
9796
+ }
9797
+ const head = data.head;
9798
+ const { suite, flags, nonce, sig, ct } = unpackMsgBody(data.body);
9799
+ const aad = getMsgAad(epoch, head, suite, flags);
9800
+ const signedBytes = getSignatureInput(aad, nonce, ct);
9801
+ const preverified = await preverifySigner(epoch, sig, signedBytes, options);
9802
+ if (preverified == null) {
9803
+ throw new Error("invalid chat action signature");
9804
+ }
9805
+ const plaintext = await openMsgPlaintext(epoch, nonce, ct, aad, options);
9899
9806
  try {
9900
- return toHex(key);
9807
+ const normalized = normalizeOpenedAction(epoch, head, JSON.parse(decoder.decode(plaintext)));
9808
+ const valid = preverified ? normalized.signingPublicKey === preverified : await verifyMsgSignature(normalized.signingPublicKey, sig, signedBytes, options);
9809
+ if (!valid) {
9810
+ throw new Error("invalid chat action signature");
9811
+ }
9812
+ return {
9813
+ ...normalized.payload,
9814
+ id: normalized.target && normalized.op !== CHAT_ACTION_OPS.CREATE ? normalized.target : undefined,
9815
+ cid: normalized.id,
9816
+ actionId: normalized.id,
9817
+ actionOp: normalized.op,
9818
+ actionTarget: normalized.target,
9819
+ signingPublicKey: normalized.signingPublicKey,
9820
+ from: normalized.sender,
9821
+ s: normalized.sender
9822
+ };
9901
9823
  } finally {
9902
- cleanBytes(key);
9824
+ cleanBytes(plaintext);
9903
9825
  }
9904
9826
  }
9905
- function entryKey(chatPrivateKey, entryId) {
9906
- return deriveKey(toBytes32(chatPrivateKey, "chat private key"), "user-chat-entry-v4", [cleanText(entryId)]);
9907
- }
9908
- function entryAad(entryId) {
9909
- return canonicalBytes({ v: CHAT_ENTRY_VERSION, protocol: CHAT_PROTOCOL_VERSION, entryId }, "chat entry aad");
9910
- }
9911
- function ownerEpochKey(chatPrivateKey, entryId, epochEntryId) {
9912
- return deriveKey(toBytes32(chatPrivateKey, "chat private key"), "user-chat-epoch-entry-v3", [entryId, epochEntryId]);
9913
- }
9914
- function ownerEpochAad(entryId, epochEntryId) {
9915
- return canonicalBytes({
9916
- v: CHAT_OWNER_EPOCH_ENTRY_VERSION,
9917
- protocol: CHAT_PROTOCOL_VERSION,
9918
- entryId,
9919
- epochEntryId
9920
- }, "chat owner epoch aad");
9921
- }
9922
- function cleanTransitionCommitment(value) {
9923
- return cleanChatHex(value, "transition commitment");
9924
- }
9925
- function cleanTransitionMessageId(value) {
9926
- const messageId = cleanText(value);
9927
- if (!messageId || messageId.length > 128 || messageId.includes("/")) {
9928
- throw new Error("invalid transition message id");
9827
+ async function openMessageBatchV3Default(epoch, records, options) {
9828
+ const opened = [];
9829
+ for (const record of records || []) {
9830
+ try {
9831
+ const message = await openMsg(epoch, record, options);
9832
+ opened.push({ recordId: batchRecordId(record), message });
9833
+ } catch {
9834
+ opened.push(null);
9835
+ }
9929
9836
  }
9930
- return messageId;
9837
+ return opened;
9931
9838
  }
9932
- function normalizeRoutes(value) {
9933
- if (value == null)
9934
- return {};
9935
- if (typeof value !== "object" || Array.isArray(value)) {
9936
- throw new Error("invalid chat routes");
9839
+ async function openMessageBatchV3(epoch, records, options = {}) {
9840
+ const source = Array.isArray(records) ? records : [];
9841
+ if (!source.length)
9842
+ return [];
9843
+ if (!epoch?.chatId || !epoch?.epochId || !epoch?.bodyKey || !epoch?.manifest) {
9844
+ throw new Error("chat batch epoch required");
9937
9845
  }
9938
- const routes = {};
9939
- for (const [rawChatPK, route] of Object.entries(value)) {
9940
- const chatPK = cleanChatHex(rawChatPK, "chat route member");
9941
- if (!route || typeof route !== "object" || Array.isArray(route)) {
9942
- throw new Error("invalid chat route");
9943
- }
9944
- const uid = cleanText(route.uid);
9945
- const deliveryCapability = route.deliveryCapability == null ? null : cleanChatHex(route.deliveryCapability, "delivery capability");
9946
- const notificationPK = route.notificationPK == null ? null : cleanChatHex(route.notificationPK, "notification key");
9947
- const generation = Number.isSafeInteger(route.generation) && route.generation > 0 ? route.generation : 1;
9948
- if (!uid || uid.length > 128) {
9949
- throw new Error("chat route uid required");
9846
+ const crypto = options.crypto;
9847
+ if (typeof crypto?.openMessageBatchV3 === "function" && (typeof crypto.isAvailable !== "function" || crypto.isAvailable())) {
9848
+ const opened = await crypto.openMessageBatchV3(makeBatchOpenRequest(epoch, source));
9849
+ if (!Array.isArray(opened) || opened.length !== source.length) {
9850
+ throw new Error("invalid chat message batch result");
9950
9851
  }
9951
- routes[chatPK] = { uid, deliveryCapability, notificationPK, generation };
9852
+ return opened;
9952
9853
  }
9953
- return routes;
9854
+ return openMessageBatchV3Default(epoch, source, options);
9954
9855
  }
9955
- function normalizeCurrentEpoch(value) {
9956
- if (!value || typeof value !== "object" || Array.isArray(value)) {
9957
- throw new Error("current chat epoch required");
9856
+
9857
+ // ../../core/chat/epochs/transition.js
9858
+ "use client";
9859
+ var TRANSITION_SIGN_SCOPE = encoder.encode("veyl-chat-transition-v3");
9860
+ var TRANSITION_MEMBER_SCOPE = encoder.encode("veyl-chat-transition-member-v3");
9861
+ function cleanTime(value) {
9862
+ if (!Number.isSafeInteger(value) || value <= 0) {
9863
+ throw new Error("chat transition time required");
9958
9864
  }
9959
- const manifest = normalizeEpochManifest(value.manifest);
9960
- return {
9961
- manifest,
9962
- epochSecret: cleanChatHex(value.epochSecret, "epoch secret"),
9963
- stateCapability: cleanChatHex(value.stateCapability, "state capability"),
9964
- transitionCommitment: cleanTransitionCommitment(value.transitionCommitment),
9965
- mlsPackageId: cleanChatHex(value.mlsPackageId, "mls epoch package id"),
9966
- mlsPackageDigest: cleanChatHex(value.mlsPackageDigest, "mls epoch package digest"),
9967
- settings: normalizeChatSettingsProjection(value.settings)
9968
- };
9865
+ return value;
9969
9866
  }
9970
- function normalizeOwnerEntry(value) {
9971
- if (!value || typeof value !== "object" || Array.isArray(value) || value.v !== CHAT_ENTRY_VERSION || value.protocol !== CHAT_PROTOCOL_VERSION) {
9972
- throw new Error("invalid chat entry");
9867
+ function normalizeTransitionCore(value) {
9868
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
9869
+ throw new Error("chat transition required");
9973
9870
  }
9974
- const current = normalizeCurrentEpoch(value.current);
9975
- if (current.manifest.chatId !== cleanChatHex(value.chatId, "chat id")) {
9976
- throw new Error("chat entry id mismatch");
9871
+ if (value.v !== CHAT_TRANSITION_VERSION || value.protocol !== CHAT_PROTOCOL_VERSION) {
9872
+ throw new Error("unsupported chat transition");
9873
+ }
9874
+ const parentEpochVersion = value.parentEpochVersion;
9875
+ const nextEpochVersion = value.nextEpochVersion;
9876
+ if (!Number.isSafeInteger(parentEpochVersion) || parentEpochVersion <= 0 || nextEpochVersion !== parentEpochVersion + 1) {
9877
+ throw new Error("invalid chat transition version");
9977
9878
  }
9978
9879
  return {
9979
- v: CHAT_ENTRY_VERSION,
9880
+ v: CHAT_TRANSITION_VERSION,
9980
9881
  protocol: CHAT_PROTOCOL_VERSION,
9981
- ownerRevision: Number.isSafeInteger(value.ownerRevision) && value.ownerRevision > 0 ? value.ownerRevision : (() => {
9982
- throw new Error("chat owner revision required");
9983
- })(),
9984
- chatId: current.manifest.chatId,
9985
- current,
9986
- routes: normalizeRoutes(value.routes),
9987
- saved: value.saved || null,
9988
- startMs: Number.isFinite(value.startMs) ? value.startMs : null,
9989
- deliveryRegistered: value.deliveryRegistered === true,
9990
- notificationTag: cleanText(value.notificationTag) || null
9882
+ chatId: cleanChatHex(value.chatId, "chat id"),
9883
+ parentEpochId: cleanChatHex(value.parentEpochId, "parent epoch id"),
9884
+ parentEpochVersion,
9885
+ nextEpochId: cleanChatHex(value.nextEpochId, "next epoch id"),
9886
+ nextEpochVersion,
9887
+ nextManifestDigest: cleanChatHex(value.nextManifestDigest, "manifest digest"),
9888
+ nextStateCapabilityCommitment: cleanChatHex(value.nextStateCapabilityCommitment, "state capability commitment"),
9889
+ proposalId: cleanChatHex(value.proposalId, "transition proposal id"),
9890
+ createdAt: cleanTime(value.createdAt)
9991
9891
  };
9992
9892
  }
9993
- async function sealOwnChatEntry(chatPrivateKey, entryId, entry) {
9994
- const key = entryKey(chatPrivateKey, entryId);
9995
- try {
9996
- const { nonce, ct } = await sealJson(key, normalizeOwnerEntry(entry), entryAad(entryId));
9997
- return packBodyData(nonce, ct);
9998
- } finally {
9999
- cleanBytes(key);
10000
- }
10001
- }
10002
- async function openOwnChatEntry(chatPrivateKey, entryId, body) {
10003
- const key = entryKey(chatPrivateKey, entryId);
10004
- try {
10005
- const { nonce, ct } = unpackBodyData(body);
10006
- return normalizeOwnerEntry(await openJson(key, nonce, ct, entryAad(entryId)));
10007
- } finally {
10008
- cleanBytes(key);
10009
- }
9893
+ function transitionCommitment(value) {
9894
+ return toHex(sha256(canonicalBytes(normalizeTransitionCore(value), "chat transition commitment")));
10010
9895
  }
10011
- function makeOwnChatEntry(epoch, fields = {}) {
10012
- const manifest = normalizeEpochManifest(epoch?.manifest);
10013
- return normalizeOwnerEntry({
10014
- v: CHAT_ENTRY_VERSION,
10015
- protocol: CHAT_PROTOCOL_VERSION,
10016
- ownerRevision: Number.isSafeInteger(fields.ownerRevision) && fields.ownerRevision > 0 ? fields.ownerRevision : 1,
10017
- chatId: manifest.chatId,
10018
- current: {
10019
- manifest,
10020
- epochSecret: secretHex(epoch.epochSecret, "epoch secret"),
10021
- stateCapability: secretHex(epoch.stateCapability, "state capability"),
10022
- transitionCommitment: cleanTransitionCommitment(epoch.transitionCommitment),
10023
- mlsPackageId: cleanChatHex(epoch.mlsPackageId, "mls epoch package id"),
10024
- mlsPackageDigest: cleanChatHex(epoch.mlsPackageDigest, "mls epoch package digest"),
10025
- settings: normalizeChatSettingsProjection(epoch.settings)
10026
- },
10027
- routes: fields.routes || {},
10028
- saved: fields.saved || null,
10029
- startMs: Number.isFinite(fields.startMs) ? fields.startMs : null,
10030
- deliveryRegistered: fields.deliveryRegistered === true,
10031
- notificationTag: cleanText(fields.notificationTag) || null
10032
- });
9896
+ function signatureInput(core) {
9897
+ return concatBytes4(TRANSITION_SIGN_SCOPE, canonicalBytes(core, "chat transition signature"));
10033
9898
  }
10034
- function normalizeOwnerEpochRecord(value) {
10035
- if (!value || typeof value !== "object" || Array.isArray(value) || value.v !== CHAT_OWNER_EPOCH_ENTRY_VERSION || value.protocol !== CHAT_PROTOCOL_VERSION) {
10036
- throw new Error("invalid owner epoch entry");
9899
+ function createTransitionCertificate(parentValue, nextValue, nextStateCapability, actor, options = {}) {
9900
+ const parent = normalizeEpochManifest(parentValue);
9901
+ const next = normalizeEpochManifest(nextValue, { parent });
9902
+ const actorChatPK = cleanChatHex(actor?.chatPK, "transition actor");
9903
+ const parentMember = manifestMember(parent, actorChatPK);
9904
+ if (!parentMember || parentMember.chatSigningPK !== cleanText(actor?.signingKey?.publicKey).toLowerCase()) {
9905
+ throw new Error("transition actor is not a parent member");
10037
9906
  }
10038
- const manifest = normalizeEpochManifest(value.manifest);
10039
- return {
10040
- v: CHAT_OWNER_EPOCH_ENTRY_VERSION,
10041
- protocol: CHAT_PROTOCOL_VERSION,
10042
- chatId: manifest.chatId,
10043
- epochId: manifest.epochId,
10044
- epochVersion: manifest.epochVersion,
10045
- manifest,
10046
- epochSecret: cleanChatHex(value.epochSecret, "epoch secret"),
10047
- cutoffMs: Number.isFinite(value.cutoffMs) ? value.cutoffMs : null,
10048
- successorTransitionCommitment: cleanTransitionCommitment(value.successorTransitionCommitment),
10049
- successorTransitionMessageId: cleanTransitionMessageId(value.successorTransitionMessageId)
10050
- };
10051
- }
10052
- function makeOwnerEpochRecord(manifest, epochSecret, fields = {}) {
10053
- return normalizeOwnerEpochRecord({
10054
- v: CHAT_OWNER_EPOCH_ENTRY_VERSION,
9907
+ const core = normalizeTransitionCore({
9908
+ v: CHAT_TRANSITION_VERSION,
10055
9909
  protocol: CHAT_PROTOCOL_VERSION,
10056
- manifest,
10057
- epochSecret: secretHex(epochSecret, "epoch secret"),
10058
- cutoffMs: Number.isFinite(fields.cutoffMs) ? fields.cutoffMs : null,
10059
- successorTransitionCommitment: fields.successorTransitionCommitment,
10060
- successorTransitionMessageId: fields.successorTransitionMessageId
9910
+ chatId: parent.chatId,
9911
+ parentEpochId: parent.epochId,
9912
+ parentEpochVersion: parent.epochVersion,
9913
+ nextEpochId: next.epochId,
9914
+ nextEpochVersion: next.epochVersion,
9915
+ nextManifestDigest: epochManifestDigest(next),
9916
+ nextStateCapabilityCommitment: stateCapabilityCommitment(nextStateCapability),
9917
+ proposalId: options.proposalId || toHex(randomBytes3(32)),
9918
+ createdAt: options.createdAt || Date.now()
10061
9919
  });
9920
+ return {
9921
+ ...core,
9922
+ actor: actorChatPK,
9923
+ signature: signChatBytes(actor.signingKey, signatureInput(core))
9924
+ };
10062
9925
  }
10063
- async function sealOwnerEpochRecord(chatPrivateKey, entryId, epochEntryId, record) {
10064
- const key = ownerEpochKey(chatPrivateKey, entryId, epochEntryId);
9926
+ function verifyTransitionCertificate(parentValue, nextValue, certificate) {
9927
+ const parent = normalizeEpochManifest(parentValue);
9928
+ const next = normalizeEpochManifest(nextValue, { parent });
9929
+ const core = normalizeTransitionCore(certificate);
9930
+ const actor = cleanChatHex(certificate?.actor, "transition actor");
9931
+ const member = manifestMember(parent, actor);
9932
+ if (!member || core.chatId !== parent.chatId || core.parentEpochId !== parent.epochId || core.parentEpochVersion !== parent.epochVersion || core.nextEpochId !== next.epochId || core.nextEpochVersion !== next.epochVersion || core.nextManifestDigest !== epochManifestDigest(next)) {
9933
+ return false;
9934
+ }
9935
+ return verifyTransitionSignature(certificate, member.chatSigningPK);
9936
+ }
9937
+ function verifyTransitionSuccessor(nextValue, certificate) {
10065
9938
  try {
10066
- const { nonce, ct } = await sealJson(key, normalizeOwnerEpochRecord(record), ownerEpochAad(entryId, epochEntryId));
10067
- return packBodyData(nonce, ct);
10068
- } finally {
10069
- cleanBytes(key);
9939
+ const next = normalizeEpochManifest(nextValue);
9940
+ const core = normalizeTransitionCore(certificate);
9941
+ return core.chatId === next.chatId && core.nextEpochId === next.epochId && core.nextEpochVersion === next.epochVersion && core.nextManifestDigest === epochManifestDigest(next);
9942
+ } catch {
9943
+ return false;
10070
9944
  }
10071
9945
  }
10072
- async function openOwnerEpochRecord(chatPrivateKey, entryId, epochEntryId, body) {
10073
- const key = ownerEpochKey(chatPrivateKey, entryId, epochEntryId);
9946
+ function verifyTransitionSignature(certificate, signingPublicKey) {
10074
9947
  try {
10075
- const { nonce, ct } = unpackBodyData(body);
10076
- return normalizeOwnerEpochRecord(await openJson(key, nonce, ct, ownerEpochAad(entryId, epochEntryId)));
10077
- } finally {
10078
- cleanBytes(key);
9948
+ const core = normalizeTransitionCore(certificate);
9949
+ const signature = cleanText(certificate?.signature).toLowerCase();
9950
+ return verifyChatBytes(cleanChatHex(signingPublicKey, "transition signing key"), signature, signatureInput(core));
9951
+ } catch {
9952
+ return false;
10079
9953
  }
10080
9954
  }
10081
- function ownEpochEntryId(chatPrivateKey, chatId, epochId) {
10082
- return ownerEpochEntryId(chatPrivateKey, chatId, epochId);
9955
+ function transitionCore(certificate) {
9956
+ return normalizeTransitionCore(certificate);
10083
9957
  }
10084
9958
 
10085
- // ../../core/chat/epochs/bootstrap.js
9959
+ // ../../core/chat/ttl.js
10086
9960
  "use client";
10087
- var CHAT_INITIAL_EPOCH_CREATED_AT = 1;
10088
- function initialEpochCommitment(manifestValue, stateCapability) {
10089
- const manifest = normalizeEpochManifest(manifestValue);
10090
- return toHex(sha256(canonicalBytes({
10091
- v: 2,
10092
- protocol: CHAT_PROTOCOL_VERSION,
10093
- chatId: manifest.chatId,
10094
- epochId: manifest.epochId,
10095
- epochVersion: manifest.epochVersion,
10096
- manifestDigest: epochManifestDigest(manifest),
10097
- stateCapabilityCommitment: stateCapabilityCommitment(stateCapability)
10098
- }, "initial chat epoch commitment")));
9961
+ var CHAT_RETENTION_SEEN = "seen";
9962
+ var CHAT_RETENTION_24H = "24h";
9963
+ var CHAT_RETENTION_FOREVER = "forever";
9964
+ var DEFAULT_CHAT_RETENTION = CHAT_RETENTION_SEEN;
9965
+ var CHAT_RETENTION_VALUES = Object.freeze([CHAT_RETENTION_SEEN, CHAT_RETENTION_24H]);
9966
+ var CHAT_PROTOCOL_RETENTION_VALUES = new Set([...CHAT_RETENTION_VALUES, CHAT_RETENTION_FOREVER]);
9967
+ var CHAT_RETENTION_LABELS = Object.freeze({
9968
+ [CHAT_RETENTION_SEEN]: "delete after seen",
9969
+ [CHAT_RETENTION_24H]: "keep for 24h"
9970
+ });
9971
+ var MESSAGE_STORAGE_TTL_MS = CHAT_UNSAVED_TTL_MS;
9972
+ var AFTER_SEEN_MS = CHAT_AFTER_SEEN_MS;
9973
+ function hasChatRetention(value) {
9974
+ const retention = cleanText(value);
9975
+ return CHAT_PROTOCOL_RETENTION_VALUES.has(retention);
10099
9976
  }
10100
- function makeInitialEpochManifest({ chatId, epochId, members, lineage }, options = {}) {
10101
- return normalizeEpochManifest({
10102
- v: CHAT_MANIFEST_VERSION,
10103
- protocol: CHAT_PROTOCOL_VERSION,
10104
- chatId,
10105
- epochId,
10106
- epochVersion: 1,
10107
- parentEpochId: null,
10108
- createdAt: CHAT_INITIAL_EPOCH_CREATED_AT,
10109
- lineage,
10110
- members
10111
- }, options);
9977
+ function cleanChatRetention(value) {
9978
+ const retention = cleanText(value);
9979
+ return hasChatRetention(retention) ? retention : DEFAULT_CHAT_RETENTION;
10112
9980
  }
10113
-
10114
- // ../../core/chat/criticaldelivery.js
10115
- "use client";
10116
- var RETRY_DELAYS_MS = Object.freeze([150, 600]);
10117
- function retryableDeliveryError(error) {
10118
- const code = cleanText(error?.code).toLowerCase();
10119
- const message = cleanText(error?.message).toLowerCase();
10120
- const retryableCodes = [
10121
- "aborted",
10122
- "deadline-exceeded",
10123
- "internal",
10124
- "network-request-failed",
10125
- "resource-exhausted",
10126
- "unavailable",
10127
- "unknown"
10128
- ];
10129
- return retryableCodes.some((value) => code === value || code.endsWith(`/${value}`)) || /\b(network|offline|timed? out|temporar(?:y|ily)|connection)\b/u.test(message);
9981
+ function normalizeChatSettings(settings) {
9982
+ const retention = cleanChatRetention(settings?.retention);
9983
+ return { retention };
10130
9984
  }
10131
- function wait(ms) {
10132
- return ms > 0 ? new Promise((resolve) => setTimeout(resolve, ms)) : Promise.resolve();
9985
+ function getMessageRetention(message, fallback = DEFAULT_CHAT_RETENTION) {
9986
+ if (hasChatRetention(message?.retention)) {
9987
+ return cleanChatRetention(message.retention);
9988
+ }
9989
+ return cleanChatRetention(fallback);
10133
9990
  }
10134
- async function pushCriticalInbox(cloud, recipientUid, ping, options = {}, retry = {}) {
10135
- const deliveryId = cleanText(options.deliveryId).toLowerCase() || toHex(randomBytes3(32));
10136
- const delays = Array.isArray(retry.delaysMs) ? retry.delaysMs : RETRY_DELAYS_MS;
10137
- let lastError = null;
10138
- for (let attempt = 0;attempt <= delays.length; attempt += 1) {
10139
- try {
10140
- await cloud.inbox.push(recipientUid, ping, { ...options, deliveryId });
10141
- return { deliveryId, attempts: attempt + 1 };
10142
- } catch (error) {
10143
- lastError = error;
10144
- if (attempt >= delays.length || !retryableDeliveryError(error))
10145
- break;
10146
- await wait(delays[attempt]);
10147
- }
9991
+ function retentionPatch(message) {
9992
+ return hasChatRetention(message?.retention) ? { retention: cleanChatRetention(message.retention) } : {};
9993
+ }
9994
+ function withMessageRetention(message, retention = DEFAULT_CHAT_RETENTION) {
9995
+ if (!message || typeof message !== "object" || Array.isArray(message)) {
9996
+ return message;
10148
9997
  }
10149
- throw lastError || new Error("critical inbox delivery failed");
9998
+ const nextRetention = getMessageRetention(message, retention);
9999
+ return message.retention === nextRetention ? message : { ...message, retention: nextRetention };
10000
+ }
10001
+ function ttlMillis(value) {
10002
+ return timestampMs(value, null, { positive: true });
10003
+ }
10004
+ function isTtlExpired(value, now = Date.now()) {
10005
+ const ms = ttlMillis(value);
10006
+ return ms != null && ms <= now;
10007
+ }
10008
+ function newMessageStorageTtlMs(now = Date.now()) {
10009
+ return now + MESSAGE_STORAGE_TTL_MS;
10010
+ }
10011
+ function messageExpiryMs(retention, seenAt) {
10012
+ if (cleanChatRetention(retention) === CHAT_RETENTION_FOREVER) {
10013
+ return null;
10014
+ }
10015
+ if (!Number.isFinite(seenAt)) {
10016
+ return null;
10017
+ }
10018
+ return cleanChatRetention(retention) === CHAT_RETENTION_24H ? seenAt + AFTER_SEEN_MS : seenAt;
10150
10019
  }
10151
- var criticalDeliveryInternals = Object.freeze({ retryableDeliveryError });
10152
10020
 
10153
- // ../../core/chat/owner.js
10021
+ // ../../core/chat/settings.js
10154
10022
  "use client";
10155
- async function prepareMutation(identity, entryId, current, update) {
10156
- const prepared = await update(current);
10157
- if (!prepared?.entry) {
10158
- return { unchanged: true, result: prepared?.result ?? null };
10023
+ var SETTINGS_SIGN_SCOPE = encoder.encode("veyl-chat-settings-v3-sign");
10024
+ var SIGNATURE_RE = /^[0-9a-f]{128}$/u;
10025
+ function cleanRevision(value) {
10026
+ if (!Number.isSafeInteger(value) || value <= 0) {
10027
+ throw new Error("chat settings revision required");
10159
10028
  }
10160
- const entry = {
10161
- ...prepared.entry,
10162
- ownerRevision: (current?.ownerRevision || 0) + 1
10029
+ return value;
10030
+ }
10031
+ function cleanPreviousDigest(value) {
10032
+ return value == null ? null : cleanChatHex(value, "previous chat settings digest");
10033
+ }
10034
+ function cleanEventId(value) {
10035
+ const eventId = value == null ? null : cleanText(value);
10036
+ if (eventId != null && (!eventId || eventId.length > 128 || eventId.includes("/"))) {
10037
+ throw new Error("invalid chat settings event");
10038
+ }
10039
+ return eventId;
10040
+ }
10041
+ function normalizeTransitionAuthorization(value, epoch, actor) {
10042
+ if (value == null)
10043
+ return null;
10044
+ if (!value || typeof value !== "object" || Array.isArray(value) || value.kind !== "transition") {
10045
+ throw new Error("invalid chat settings authorization");
10046
+ }
10047
+ const signingPK = cleanChatHex(value.signingPK, "chat settings transition signing key");
10048
+ const certificateCore = transitionCore(value.certificate);
10049
+ const certificate = {
10050
+ ...certificateCore,
10051
+ actor: cleanChatHex(value.certificate?.actor, "transition actor"),
10052
+ signature: cleanText(value.certificate?.signature).toLowerCase()
10163
10053
  };
10054
+ if (certificate.actor !== actor || !SIGNATURE_RE.test(certificate.signature) || transitionCommitment(certificate) !== cleanChatHex(epoch?.transitionCommitment, "transition commitment") || !verifyTransitionSuccessor(epoch.manifest, certificate) || !verifyTransitionSignature(certificate, signingPK)) {
10055
+ throw new Error("invalid chat settings transition authorization");
10056
+ }
10057
+ return { kind: "transition", signingPK, certificate };
10058
+ }
10059
+ function withAuthorization(core, authorization) {
10060
+ return authorization ? { ...core, authorization } : core;
10061
+ }
10062
+ function normalizeChatStateSettings(value = {}) {
10063
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
10064
+ throw new Error("chat settings required");
10065
+ }
10066
+ const title = value.title == null ? null : cleanText(value.title);
10067
+ if (title != null && (!title || title.length > CHAT_TITLE_MAX_CHARS)) {
10068
+ throw new Error("invalid chat title");
10069
+ }
10164
10070
  return {
10165
- uid: identity.uid,
10166
- entryId,
10167
- record: {
10168
- body: await sealOwnChatEntry(identity.chatPrivateKey, entryId, entry),
10169
- revision: entry.ownerRevision,
10170
- ...Number.isFinite(prepared.tsMs) ? { tsMs: prepared.tsMs } : {},
10171
- ...prepared.touchTs === true ? { touchTs: true } : {}
10172
- },
10173
- epochs: prepared.epochs || [],
10174
- mlsWrites: prepared.mlsWrites || [],
10175
- mlsDeletes: prepared.mlsDeletes || [],
10176
- result: prepared.result === prepared.entry || prepared.result == null ? entry : prepared.result
10071
+ title,
10072
+ avatarRef: cleanChatAvatarRef(value.avatarRef),
10073
+ retention: cleanChatRetention(value.retention)
10177
10074
  };
10178
10075
  }
10179
- async function prepareOwnChatMutation(identity, entryId, current, update) {
10180
- if (!identity?.uid || !identity?.chatPrivateKey || !entryId || typeof update !== "function") {
10181
- throw new Error("chat owner mutation required");
10076
+ function settingsCore(epoch, settings, options = {}) {
10077
+ const actor = cleanChatHex(epoch?.actor?.chatPK, "chat settings actor");
10078
+ const authorization = normalizeTransitionAuthorization(options.authorization, epoch, actor);
10079
+ if (!epoch?.settingsKey || !epoch?.settingsId || !epoch?.actor?.signingKey || !manifestMember(epoch.manifest, actor) && !authorization) {
10080
+ throw new Error("chat settings epoch actor required");
10081
+ }
10082
+ return withAuthorization({
10083
+ v: CHAT_SETTINGS_VERSION,
10084
+ protocol: CHAT_PROTOCOL_VERSION,
10085
+ chatId: cleanChatHex(epoch.chatId, "chat id"),
10086
+ epochId: cleanChatHex(epoch.epochId, "chat epoch id"),
10087
+ revision: cleanRevision(options.revision),
10088
+ previousDigest: cleanPreviousDigest(options.previousDigest),
10089
+ eventId: cleanEventId(options.eventId),
10090
+ actor,
10091
+ settings: normalizeChatStateSettings(settings)
10092
+ }, authorization);
10093
+ }
10094
+ function normalizeSettingsCore(value, epoch) {
10095
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
10096
+ throw new Error("invalid chat settings state");
10097
+ }
10098
+ const actor = cleanChatHex(value.actor, "chat settings actor");
10099
+ const authorization = normalizeTransitionAuthorization(value.authorization, epoch, actor);
10100
+ const core = withAuthorization({
10101
+ v: value.v,
10102
+ protocol: value.protocol,
10103
+ chatId: cleanChatHex(value.chatId, "chat id"),
10104
+ epochId: cleanChatHex(value.epochId, "chat epoch id"),
10105
+ revision: cleanRevision(value.revision),
10106
+ previousDigest: cleanPreviousDigest(value.previousDigest),
10107
+ eventId: cleanEventId(value.eventId),
10108
+ actor,
10109
+ settings: normalizeChatStateSettings(value.settings)
10110
+ }, authorization);
10111
+ if (core.v !== CHAT_SETTINGS_VERSION || core.protocol !== CHAT_PROTOCOL_VERSION || core.chatId !== epoch.chatId || core.epochId !== epoch.epochId || !manifestMember(epoch.manifest, core.actor) && !authorization) {
10112
+ throw new Error("chat settings binding mismatch");
10113
+ }
10114
+ return core;
10115
+ }
10116
+ function coreDigest(core) {
10117
+ return toHex(sha256(canonicalBytes(core, "chat settings digest")));
10118
+ }
10119
+ function settingsHead(value) {
10120
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
10121
+ throw new Error("chat settings record required");
10182
10122
  }
10183
- const initial = await prepareMutation(identity, entryId, current, update);
10184
10123
  return {
10185
- ...initial,
10186
- open: (record) => openOwnChatMutationEntry(identity, entryId, record),
10187
- prepare: async (record) => {
10188
- if (!record) {
10189
- return prepareMutation(identity, entryId, null, update);
10190
- }
10191
- if (!record?.body || !Number.isSafeInteger(record.revision) || record.revision < 1) {
10192
- throw new Error("current chat owner record required");
10193
- }
10194
- const opened = await openOwnChatEntry(identity.chatPrivateKey, entryId, record.body);
10195
- if (opened.ownerRevision !== record.revision) {
10196
- throw new Error("chat owner revision mismatch");
10197
- }
10198
- return prepareMutation(identity, entryId, opened, update);
10199
- }
10124
+ v: value.v,
10125
+ protocol: value.protocol,
10126
+ chatId: cleanChatHex(value.chatId, "chat id"),
10127
+ epochId: cleanChatHex(value.epochId, "chat epoch id"),
10128
+ revision: cleanRevision(value.revision),
10129
+ previousDigest: cleanPreviousDigest(value.previousDigest),
10130
+ eventId: cleanEventId(value.eventId),
10131
+ digest: cleanChatHex(value.digest, "chat settings digest")
10200
10132
  };
10201
10133
  }
10202
- async function mutateOwnChatEntry(cloud, identity, entryId, current, update) {
10203
- if (!cloud?.user?.chats?.mutate) {
10204
- throw new Error("chat owner mutation required");
10205
- }
10206
- const mutation = await prepareOwnChatMutation(identity, entryId, current, update);
10207
- if (mutation.unchanged)
10208
- return mutation.result;
10209
- const committed = await cloud.user.chats.mutate(identity.uid, entryId, mutation);
10210
- return committed?.result ?? mutation.result ?? null;
10134
+ function signatureInput2(core) {
10135
+ return concatBytes4(SETTINGS_SIGN_SCOPE, canonicalBytes(core, "chat settings signature"));
10211
10136
  }
10212
- async function openOwnChatMutationEntry(identity, entryId, record) {
10213
- if (!record)
10214
- return null;
10215
- if (!record?.body || !Number.isSafeInteger(record.revision) || record.revision < 1)
10216
- throw new Error("current chat owner record required");
10217
- const current = await openOwnChatEntry(identity.chatPrivateKey, entryId, record.body);
10218
- if (current.ownerRevision !== record.revision) {
10219
- throw new Error("chat owner revision mismatch");
10220
- }
10221
- return current;
10137
+ function settingsAad(head) {
10138
+ return canonicalBytes({
10139
+ v: head.v,
10140
+ protocol: head.protocol,
10141
+ chatId: head.chatId,
10142
+ epochId: head.epochId,
10143
+ revision: head.revision,
10144
+ previousDigest: head.previousDigest,
10145
+ eventId: head.eventId,
10146
+ digest: head.digest
10147
+ }, "chat settings aad");
10222
10148
  }
10223
-
10224
- // ../../core/crypto/file.js
10225
- "use client";
10226
- var FILE_SCOPE = "file-body";
10227
- var FILE_IV_BYTES = 12;
10228
- var FILE_TAG_BYTES = 16;
10229
- function createFileKey() {
10230
- return randomBytes3(32);
10149
+ async function sealChatSettings(epoch, settings, options = {}) {
10150
+ const core = settingsCore(epoch, settings, options);
10151
+ const digest = coreDigest(core);
10152
+ const head = settingsHead({ ...core, digest });
10153
+ const plaintext = encoder.encode(JSON.stringify({
10154
+ core,
10155
+ signature: signChatBytes(epoch.actor.signingKey, signatureInput2(core))
10156
+ }));
10157
+ try {
10158
+ const { nonce, ct } = await sealBox(epoch.settingsKey, plaintext, settingsAad(head));
10159
+ const body = concatBytes4(nonce, ct);
10160
+ if (body.length > CHAT_SETTINGS_BODY_MAX_BYTES) {
10161
+ throw new Error("chat settings too large");
10162
+ }
10163
+ return { id: epoch.settingsId, head, body, actor: core.actor, settings: core.settings };
10164
+ } finally {
10165
+ cleanBytes(plaintext);
10166
+ }
10231
10167
  }
10232
- function encodeFileKey(key) {
10233
- return toHex(toBytes32(key, "file key"));
10168
+ async function openChatSettings(epoch, record) {
10169
+ if (!epoch?.settingsKey || !epoch?.settingsId || record?.id !== epoch.settingsId) {
10170
+ throw new Error("chat settings address mismatch");
10171
+ }
10172
+ const head = settingsHead(record.head);
10173
+ if (head.v !== CHAT_SETTINGS_VERSION || head.protocol !== CHAT_PROTOCOL_VERSION || head.chatId !== epoch.chatId || head.epochId !== epoch.epochId) {
10174
+ throw new Error("chat settings binding mismatch");
10175
+ }
10176
+ const body = toBytes(record.body, "chat settings body");
10177
+ if (body.length <= 24 || body.length > CHAT_SETTINGS_BODY_MAX_BYTES) {
10178
+ throw new Error("invalid chat settings body");
10179
+ }
10180
+ let plaintext = null;
10181
+ try {
10182
+ plaintext = await openBox(epoch.settingsKey, body.subarray(0, 24), body.subarray(24), settingsAad(head));
10183
+ const opened = JSON.parse(decoder.decode(plaintext));
10184
+ const core = normalizeSettingsCore(opened?.core, epoch);
10185
+ const signature = cleanText(opened?.signature).toLowerCase();
10186
+ const member = manifestMember(epoch.manifest, core.actor);
10187
+ const signingPK = member?.chatSigningPK || core.authorization?.signingPK;
10188
+ if (core.revision !== head.revision || core.previousDigest !== head.previousDigest || core.eventId !== head.eventId || coreDigest(core) !== head.digest || !SIGNATURE_RE.test(signature) || !signingPK || !verifyChatBytes(signingPK, signature, signatureInput2(core))) {
10189
+ throw new Error("invalid chat settings signature");
10190
+ }
10191
+ return { id: epoch.settingsId, head, actor: core.actor, settings: core.settings };
10192
+ } finally {
10193
+ cleanBytes(plaintext);
10194
+ }
10234
10195
  }
10235
- function decodeFileKey(key) {
10236
- return toBytes32(key, "file key");
10196
+ function chatSettingsProjection(snapshot) {
10197
+ return normalizeChatSettingsProjection({
10198
+ revision: cleanRevision(snapshot?.head?.revision),
10199
+ digest: cleanChatHex(snapshot?.head?.digest, "chat settings digest"),
10200
+ values: normalizeChatStateSettings(snapshot?.settings)
10201
+ });
10237
10202
  }
10238
- function getFileAadForPath(scope) {
10239
- if (!scope) {
10240
- throw new Error("file scope required");
10203
+ function normalizeChatSettingsProjection(value) {
10204
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
10205
+ throw new Error("chat settings projection required");
10241
10206
  }
10242
- return encodeScope(FILE_SCOPE, [scope]);
10207
+ return {
10208
+ revision: cleanRevision(value.revision),
10209
+ digest: cleanChatHex(value.digest, "chat settings digest"),
10210
+ values: normalizeChatStateSettings(value.values)
10211
+ };
10243
10212
  }
10244
- function getSubtleCrypto() {
10245
- const subtle = globalThis.crypto?.subtle;
10246
- if (!subtle) {
10247
- throw new Error("WebCrypto AES-GCM unavailable");
10213
+ async function createEpochChatSettings(epochState, identity, values, options = {}) {
10214
+ const epoch = openChatEpoch({
10215
+ manifest: epochState?.manifest,
10216
+ epochSecret: epochState?.epochSecret,
10217
+ actor: {
10218
+ chatPK: identity?.chatPK,
10219
+ signingKey: {
10220
+ publicKey: identity?.chatSigningPK,
10221
+ secret: identity?.chatSigningSecret
10222
+ }
10223
+ }
10224
+ });
10225
+ try {
10226
+ const sealed = await sealChatSettings(epoch, values, options);
10227
+ return { sealed, projection: chatSettingsProjection(sealed) };
10228
+ } finally {
10229
+ closeChatEpoch(epoch);
10248
10230
  }
10249
- return subtle;
10250
10231
  }
10251
- async function importAesKey(key, usages) {
10252
- return getSubtleCrypto().importKey("raw", toBytes32(key, "file key"), { name: "AES-GCM" }, false, usages);
10253
- }
10254
- async function sealFile(key, bytes, scope) {
10255
- const fileKey = new Uint8Array(toBytes32(key, "file key"));
10232
+ async function createTransitionEpochChatSettings(epochState, identity, certificate, values, options = {}) {
10233
+ const epoch = openChatEpoch({
10234
+ manifest: epochState?.manifest,
10235
+ epochSecret: epochState?.epochSecret
10236
+ });
10237
+ epoch.actor = {
10238
+ chatPK: cleanChatHex(identity?.chatPK, "chat settings actor"),
10239
+ signingKey: {
10240
+ publicKey: cleanChatHex(identity?.chatSigningPK, "chat settings signing key"),
10241
+ secret: identity?.chatSigningSecret
10242
+ }
10243
+ };
10244
+ epoch.transitionCommitment = transitionCommitment(certificate);
10256
10245
  try {
10257
- const cryptoKey = await importAesKey(fileKey, ["encrypt"]);
10258
- const iv = randomBytes3(FILE_IV_BYTES);
10259
- const ct = await getSubtleCrypto().encrypt({
10260
- name: "AES-GCM",
10261
- iv,
10262
- tagLength: FILE_TAG_BYTES * 8,
10263
- additionalData: getFileAadForPath(scope)
10264
- }, cryptoKey, toBytes(bytes, "plaintext"));
10265
- return packBodyData(iv, new Uint8Array(ct));
10246
+ const sealed = await sealChatSettings(epoch, values, {
10247
+ ...options,
10248
+ authorization: {
10249
+ kind: "transition",
10250
+ signingPK: identity?.chatSigningPK,
10251
+ certificate
10252
+ }
10253
+ });
10254
+ return { sealed, projection: chatSettingsProjection(sealed) };
10266
10255
  } finally {
10267
- cleanBytes(fileKey);
10256
+ closeChatEpoch(epoch);
10268
10257
  }
10269
10258
  }
10270
- async function openFileForPath(key, body, scope) {
10271
- const fileKey = new Uint8Array(toBytes32(key, "file key"));
10259
+ async function readEpochChatSettings(cloud, epochState) {
10260
+ const epoch = openChatEpoch({
10261
+ manifest: epochState?.manifest,
10262
+ epochSecret: epochState?.epochSecret
10263
+ });
10264
+ epoch.transitionCommitment = cleanChatHex(epochState?.transitionCommitment, "transition commitment");
10272
10265
  try {
10273
- const cryptoKey = await importAesKey(fileKey, ["decrypt"]);
10274
- const { nonce, ct } = unpackBodyData(body, FILE_IV_BYTES);
10275
- const pt = await getSubtleCrypto().decrypt({
10276
- name: "AES-GCM",
10277
- iv: nonce,
10278
- tagLength: FILE_TAG_BYTES * 8,
10279
- additionalData: getFileAadForPath(scope)
10280
- }, cryptoKey, ct);
10281
- return new Uint8Array(pt);
10266
+ const record = await cloud?.chat?.settings?.read?.(epoch.settingsId);
10267
+ if (!record)
10268
+ throw new Error("chat settings unavailable");
10269
+ return chatSettingsProjection(await openChatSettings(epoch, record));
10282
10270
  } finally {
10283
- cleanBytes(fileKey);
10271
+ closeChatEpoch(epoch);
10284
10272
  }
10285
10273
  }
10286
-
10287
- // ../../core/chat/filepayload.js
10288
- "use client";
10289
- var CHAT_MEDIA_ROOT = "chatEpochs";
10290
- var SHARED_MEDIA_ROOT = "shared";
10291
- var CHAT_MEDIA_TTL_MS2 = CHAT_MEDIA_TTL_MS;
10292
- var MAX_CHAT_UPLOAD_BYTES = CHAT_UPLOAD_MAX_BYTES;
10293
- var CHAT_VIDEO_TRANSCODE_VIDEO_BITRATE_BPS2 = CHAT_VIDEO_TRANSCODE_VIDEO_BITRATE_BPS;
10294
- var CHAT_VIDEO_TRANSCODE_AUDIO_BITRATE_BPS2 = CHAT_VIDEO_TRANSCODE_AUDIO_BITRATE_BPS;
10295
- var CHAT_VIDEO_TRANSCODE_TOTAL_BITRATE_BPS = CHAT_VIDEO_TRANSCODE_VIDEO_BITRATE_BPS2 + CHAT_VIDEO_TRANSCODE_AUDIO_BITRATE_BPS2;
10296
- var CHAT_ID_PATTERN = "[0-9a-fA-F]{64}";
10297
- var SHARED_MEDIA_ID_PATTERN = "[0-9a-fA-F]{32}";
10298
- var MEDIA_ID_PATTERN = "[0-9a-fA-F]{32}";
10299
- var CHAT_MEDIA_FILE_PATTERN = new RegExp(`^${CHAT_MEDIA_ROOT}/(${CHAT_ID_PATTERN})/(${MEDIA_ID_PATTERN})$`);
10300
- var SHARED_MEDIA_FILE_PATTERN = new RegExp(`^${SHARED_MEDIA_ROOT}/(${SHARED_MEDIA_ID_PATTERN})$`);
10301
- function cleanMediaEpochId(value) {
10302
- const epochId = String(value || "").trim();
10303
- if (!new RegExp(`^${CHAT_ID_PATTERN}$`).test(epochId)) {
10304
- throw new Error("invalid media epoch id");
10274
+ function epochChatSettingsId(epochState) {
10275
+ const epoch = openChatEpoch({
10276
+ manifest: epochState?.manifest,
10277
+ epochSecret: epochState?.epochSecret
10278
+ });
10279
+ try {
10280
+ return epoch.settingsId;
10281
+ } finally {
10282
+ closeChatEpoch(epoch);
10305
10283
  }
10306
- return epochId.toLowerCase();
10307
10284
  }
10308
- function cleanMediaId(value) {
10309
- const mediaId = String(value || "").trim();
10310
- if (!new RegExp(`^${MEDIA_ID_PATTERN}$`).test(mediaId)) {
10311
- throw new Error("invalid media id");
10285
+
10286
+ // ../../core/chat/entry.js
10287
+ "use client";
10288
+ var CHAT_ENTRY_VERSION = 4;
10289
+ var CHAT_OWNER_EPOCH_ENTRY_VERSION = 2;
10290
+ function ownChatEntryId(chatPrivateKey, chatId) {
10291
+ const key = deriveKey(toBytes32(chatPrivateKey, "chat private key"), "user-chat-entry-id-v4", [cleanChatHex(chatId, "chat id")], 16);
10292
+ try {
10293
+ return toHex(key);
10294
+ } finally {
10295
+ cleanBytes(key);
10312
10296
  }
10313
- return mediaId.toLowerCase();
10314
10297
  }
10315
- function cleanSharedMediaId(value) {
10316
- const sharedId = String(value || "").trim();
10317
- if (!new RegExp(`^${SHARED_MEDIA_ID_PATTERN}$`).test(sharedId)) {
10318
- throw new Error("invalid shared media id");
10319
- }
10320
- return sharedId.toLowerCase();
10298
+ function entryKey(chatPrivateKey, entryId) {
10299
+ return deriveKey(toBytes32(chatPrivateKey, "chat private key"), "user-chat-entry-v4", [cleanText(entryId)]);
10321
10300
  }
10322
- function makeChatMediaId() {
10323
- return toHex(randomBytes3(16));
10301
+ function entryAad(entryId) {
10302
+ return canonicalBytes({ v: CHAT_ENTRY_VERSION, protocol: CHAT_PROTOCOL_VERSION, entryId }, "chat entry aad");
10324
10303
  }
10325
- function mediaFilePath(epochId, mediaId) {
10326
- const nextEpochId = cleanMediaEpochId(epochId);
10327
- const nextMediaId = cleanMediaId(mediaId);
10328
- return `${CHAT_MEDIA_ROOT}/${nextEpochId}/${nextMediaId}`;
10304
+ function ownerEpochKey(chatPrivateKey, entryId, epochEntryId) {
10305
+ return deriveKey(toBytes32(chatPrivateKey, "chat private key"), "user-chat-epoch-entry-v3", [entryId, epochEntryId]);
10329
10306
  }
10330
- function sharedMediaFilePath(sharedId) {
10331
- const nextSharedId = cleanSharedMediaId(sharedId);
10332
- return `${SHARED_MEDIA_ROOT}/${nextSharedId}`;
10307
+ function ownerEpochAad(entryId, epochEntryId) {
10308
+ return canonicalBytes({
10309
+ v: CHAT_OWNER_EPOCH_ENTRY_VERSION,
10310
+ protocol: CHAT_PROTOCOL_VERSION,
10311
+ entryId,
10312
+ epochEntryId
10313
+ }, "chat owner epoch aad");
10333
10314
  }
10334
- function makeSharedMediaId() {
10335
- return toHex(randomBytes3(16));
10315
+ function cleanTransitionCommitment(value) {
10316
+ return cleanChatHex(value, "transition commitment");
10336
10317
  }
10337
- function getMediaFileRef(path) {
10338
- const value = String(path || "").trim();
10339
- const chatMatch = value.match(CHAT_MEDIA_FILE_PATTERN);
10340
- if (chatMatch?.[1] && chatMatch?.[2]) {
10341
- return {
10342
- type: "chat",
10343
- epochId: chatMatch[1].toLowerCase(),
10344
- mediaId: chatMatch[2].toLowerCase()
10345
- };
10346
- }
10347
- const sharedMatch = value.match(SHARED_MEDIA_FILE_PATTERN);
10348
- if (sharedMatch?.[1]) {
10349
- return {
10350
- type: "shared",
10351
- sharedId: sharedMatch[1].toLowerCase()
10352
- };
10318
+ function cleanTransitionMessageId(value) {
10319
+ const messageId = cleanText(value);
10320
+ if (!messageId || messageId.length > 128 || messageId.includes("/")) {
10321
+ throw new Error("invalid transition message id");
10353
10322
  }
10354
- throw new Error("invalid media file path");
10323
+ return messageId;
10355
10324
  }
10356
- function getChatMediaFileRef(path) {
10357
- const ref = getMediaFileRef(path);
10358
- if (ref?.type !== "chat") {
10359
- throw new Error("invalid media file path");
10325
+ function normalizeRoutes(value) {
10326
+ if (value == null)
10327
+ return {};
10328
+ if (typeof value !== "object" || Array.isArray(value)) {
10329
+ throw new Error("invalid chat routes");
10360
10330
  }
10361
- return ref;
10362
- }
10363
- function getSharedMediaFileRef(path) {
10364
- const ref = getMediaFileRef(path);
10365
- if (ref?.type !== "shared") {
10366
- throw new Error("invalid shared media file path");
10331
+ const routes = {};
10332
+ for (const [rawChatPK, route] of Object.entries(value)) {
10333
+ const chatPK = cleanChatHex(rawChatPK, "chat route member");
10334
+ if (!route || typeof route !== "object" || Array.isArray(route)) {
10335
+ throw new Error("invalid chat route");
10336
+ }
10337
+ const uid = cleanText(route.uid);
10338
+ const deliveryCapability = route.deliveryCapability == null ? null : cleanChatHex(route.deliveryCapability, "delivery capability");
10339
+ const notificationPK = route.notificationPK == null ? null : cleanChatHex(route.notificationPK, "notification key");
10340
+ const generation = Number.isSafeInteger(route.generation) && route.generation > 0 ? route.generation : 1;
10341
+ if (!uid || uid.length > 128) {
10342
+ throw new Error("chat route uid required");
10343
+ }
10344
+ routes[chatPK] = { uid, deliveryCapability, notificationPK, generation };
10367
10345
  }
10368
- return ref;
10346
+ return routes;
10369
10347
  }
10370
- function uploadByteLength(value) {
10371
- if (Number.isFinite(value?.byteLength)) {
10372
- return value.byteLength;
10348
+ function normalizeCurrentEpoch(value) {
10349
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
10350
+ throw new Error("current chat epoch required");
10373
10351
  }
10374
- if (Number.isFinite(value?.size)) {
10375
- return value.size;
10352
+ const manifest = normalizeEpochManifest(value.manifest);
10353
+ return {
10354
+ manifest,
10355
+ epochSecret: cleanChatHex(value.epochSecret, "epoch secret"),
10356
+ stateCapability: cleanChatHex(value.stateCapability, "state capability"),
10357
+ transitionCommitment: cleanTransitionCommitment(value.transitionCommitment),
10358
+ mlsPackageId: cleanChatHex(value.mlsPackageId, "mls epoch package id"),
10359
+ mlsPackageDigest: cleanChatHex(value.mlsPackageDigest, "mls epoch package digest"),
10360
+ settings: normalizeChatSettingsProjection(value.settings)
10361
+ };
10362
+ }
10363
+ function normalizeOwnerEntry(value) {
10364
+ if (!value || typeof value !== "object" || Array.isArray(value) || value.v !== CHAT_ENTRY_VERSION || value.protocol !== CHAT_PROTOCOL_VERSION) {
10365
+ throw new Error("invalid chat entry");
10376
10366
  }
10377
- if (Number.isFinite(value)) {
10378
- return value;
10367
+ const current = normalizeCurrentEpoch(value.current);
10368
+ if (current.manifest.chatId !== cleanChatHex(value.chatId, "chat id")) {
10369
+ throw new Error("chat entry id mismatch");
10379
10370
  }
10380
- return null;
10371
+ return {
10372
+ v: CHAT_ENTRY_VERSION,
10373
+ protocol: CHAT_PROTOCOL_VERSION,
10374
+ ownerRevision: Number.isSafeInteger(value.ownerRevision) && value.ownerRevision > 0 ? value.ownerRevision : (() => {
10375
+ throw new Error("chat owner revision required");
10376
+ })(),
10377
+ chatId: current.manifest.chatId,
10378
+ current,
10379
+ routes: normalizeRoutes(value.routes),
10380
+ saved: value.saved || null,
10381
+ startMs: Number.isFinite(value.startMs) ? value.startMs : null,
10382
+ deliveryRegistered: value.deliveryRegistered === true,
10383
+ notificationTag: cleanText(value.notificationTag) || null
10384
+ };
10381
10385
  }
10382
- function makeChatUploadTooLargeError(bytes, maxBytes = MAX_CHAT_UPLOAD_BYTES) {
10383
- const error = new Error("upload too large");
10384
- error.code = "upload-too-large";
10385
- error.maxBytes = maxBytes;
10386
- if (Number.isFinite(bytes)) {
10387
- error.bytes = bytes;
10386
+ async function sealOwnChatEntry(chatPrivateKey, entryId, entry) {
10387
+ const key = entryKey(chatPrivateKey, entryId);
10388
+ try {
10389
+ const { nonce, ct } = await sealJson(key, normalizeOwnerEntry(entry), entryAad(entryId));
10390
+ return packBodyData(nonce, ct);
10391
+ } finally {
10392
+ cleanBytes(key);
10388
10393
  }
10389
- return error;
10390
10394
  }
10391
- function assertChatUploadByteSize(bytes, maxBytes = MAX_CHAT_UPLOAD_BYTES) {
10392
- const length = uploadByteLength(bytes);
10393
- if (!Number.isFinite(length)) {
10394
- throw new Error("upload bytes required");
10395
- }
10396
- if (length <= 0 || length > maxBytes) {
10397
- throw makeChatUploadTooLargeError(length, maxBytes);
10395
+ async function openOwnChatEntry(chatPrivateKey, entryId, body) {
10396
+ const key = entryKey(chatPrivateKey, entryId);
10397
+ try {
10398
+ const { nonce, ct } = unpackBodyData(body);
10399
+ return normalizeOwnerEntry(await openJson(key, nonce, ct, entryAad(entryId)));
10400
+ } finally {
10401
+ cleanBytes(key);
10398
10402
  }
10399
- return length;
10400
10403
  }
10401
- async function toUploadBytes(data) {
10402
- if (typeof Blob !== "undefined" && data instanceof Blob) {
10403
- if (typeof data.arrayBuffer === "function") {
10404
- return new Uint8Array(await data.arrayBuffer());
10405
- }
10406
- if (typeof FileReader !== "undefined") {
10407
- return new Promise((resolve, reject) => {
10408
- const reader = new FileReader;
10409
- reader.onload = () => resolve(new Uint8Array(reader.result));
10410
- reader.onerror = () => reject(reader.error || new Error("blob read failed"));
10411
- reader.readAsArrayBuffer(data);
10412
- });
10413
- }
10414
- if (typeof Response !== "undefined") {
10415
- return new Uint8Array(await new Response(data).arrayBuffer());
10416
- }
10417
- }
10418
- if (typeof data?.arrayBuffer === "function") {
10419
- return new Uint8Array(await data.arrayBuffer());
10404
+ function makeOwnChatEntry(epoch, fields = {}) {
10405
+ const manifest = normalizeEpochManifest(epoch?.manifest);
10406
+ return normalizeOwnerEntry({
10407
+ v: CHAT_ENTRY_VERSION,
10408
+ protocol: CHAT_PROTOCOL_VERSION,
10409
+ ownerRevision: Number.isSafeInteger(fields.ownerRevision) && fields.ownerRevision > 0 ? fields.ownerRevision : 1,
10410
+ chatId: manifest.chatId,
10411
+ current: {
10412
+ manifest,
10413
+ epochSecret: secretHex(epoch.epochSecret, "epoch secret"),
10414
+ stateCapability: secretHex(epoch.stateCapability, "state capability"),
10415
+ transitionCommitment: cleanTransitionCommitment(epoch.transitionCommitment),
10416
+ mlsPackageId: cleanChatHex(epoch.mlsPackageId, "mls epoch package id"),
10417
+ mlsPackageDigest: cleanChatHex(epoch.mlsPackageDigest, "mls epoch package digest"),
10418
+ settings: normalizeChatSettingsProjection(epoch.settings)
10419
+ },
10420
+ routes: fields.routes || {},
10421
+ saved: fields.saved || null,
10422
+ startMs: Number.isFinite(fields.startMs) ? fields.startMs : null,
10423
+ deliveryRegistered: fields.deliveryRegistered === true,
10424
+ notificationTag: cleanText(fields.notificationTag) || null
10425
+ });
10426
+ }
10427
+ function normalizeOwnerEpochRecord(value) {
10428
+ if (!value || typeof value !== "object" || Array.isArray(value) || value.v !== CHAT_OWNER_EPOCH_ENTRY_VERSION || value.protocol !== CHAT_PROTOCOL_VERSION) {
10429
+ throw new Error("invalid owner epoch entry");
10420
10430
  }
10421
- return toBytes(data, "upload bytes");
10431
+ const manifest = normalizeEpochManifest(value.manifest);
10432
+ return {
10433
+ v: CHAT_OWNER_EPOCH_ENTRY_VERSION,
10434
+ protocol: CHAT_PROTOCOL_VERSION,
10435
+ chatId: manifest.chatId,
10436
+ epochId: manifest.epochId,
10437
+ epochVersion: manifest.epochVersion,
10438
+ manifest,
10439
+ epochSecret: cleanChatHex(value.epochSecret, "epoch secret"),
10440
+ cutoffMs: Number.isFinite(value.cutoffMs) ? value.cutoffMs : null,
10441
+ successorTransitionCommitment: cleanTransitionCommitment(value.successorTransitionCommitment),
10442
+ successorTransitionMessageId: cleanTransitionMessageId(value.successorTransitionMessageId)
10443
+ };
10422
10444
  }
10423
- async function makeChatFileUploadPayload(epochId, cid, data, { cacheControl = "private, max-age=0, no-transform" } = {}) {
10424
- const nextEpochId = cleanMediaEpochId(epochId);
10425
- const mediaId = makeChatMediaId();
10426
- const path = mediaFilePath(nextEpochId, mediaId);
10427
- const expiresAt = Date.now() + CHAT_MEDIA_TTL_MS2;
10428
- const key = createFileKey();
10445
+ function makeOwnerEpochRecord(manifest, epochSecret, fields = {}) {
10446
+ return normalizeOwnerEpochRecord({
10447
+ v: CHAT_OWNER_EPOCH_ENTRY_VERSION,
10448
+ protocol: CHAT_PROTOCOL_VERSION,
10449
+ manifest,
10450
+ epochSecret: secretHex(epochSecret, "epoch secret"),
10451
+ cutoffMs: Number.isFinite(fields.cutoffMs) ? fields.cutoffMs : null,
10452
+ successorTransitionCommitment: fields.successorTransitionCommitment,
10453
+ successorTransitionMessageId: fields.successorTransitionMessageId
10454
+ });
10455
+ }
10456
+ async function sealOwnerEpochRecord(chatPrivateKey, entryId, epochEntryId, record) {
10457
+ const key = ownerEpochKey(chatPrivateKey, entryId, epochEntryId);
10429
10458
  try {
10430
- const uploadBytes = await toUploadBytes(data);
10431
- assertChatUploadByteSize(uploadBytes);
10432
- const body = await sealFile(key, uploadBytes, path);
10433
- assertChatUploadByteSize(body);
10434
- return {
10435
- epochId: nextEpochId,
10436
- mediaId,
10437
- path,
10438
- body,
10439
- metadata: {
10440
- contentType: "application/octet-stream",
10441
- cacheControl
10442
- },
10443
- file: {
10444
- p: path,
10445
- k: encodeFileKey(key),
10446
- x: expiresAt
10447
- }
10448
- };
10449
- } catch (error) {
10450
- if (error && typeof error === "object") {
10451
- error.path = error?.path || path;
10452
- error.cid = error?.cid || cid;
10453
- }
10454
- throw error;
10459
+ const { nonce, ct } = await sealJson(key, normalizeOwnerEpochRecord(record), ownerEpochAad(entryId, epochEntryId));
10460
+ return packBodyData(nonce, ct);
10455
10461
  } finally {
10456
10462
  cleanBytes(key);
10457
10463
  }
10458
10464
  }
10459
- async function makeSharedFileUploadPayload(data, { contentType = "application/octet-stream", cacheControl = "private, max-age=0, no-transform" } = {}) {
10460
- const sharedId = makeSharedMediaId();
10461
- const path = sharedMediaFilePath(sharedId);
10462
- const expiresAt = Date.now() + CHAT_MEDIA_TTL_MS2;
10463
- const key = createFileKey();
10465
+ async function openOwnerEpochRecord(chatPrivateKey, entryId, epochEntryId, body) {
10466
+ const key = ownerEpochKey(chatPrivateKey, entryId, epochEntryId);
10464
10467
  try {
10465
- const uploadBytes = await toUploadBytes(data);
10466
- assertChatUploadByteSize(uploadBytes);
10467
- const body = await sealFile(key, uploadBytes, path);
10468
- assertChatUploadByteSize(body);
10469
- return {
10470
- sharedId,
10471
- path,
10472
- body,
10473
- metadata: {
10474
- contentType,
10475
- cacheControl
10476
- },
10477
- file: {
10478
- p: path,
10479
- k: encodeFileKey(key),
10480
- x: expiresAt
10481
- }
10482
- };
10483
- } catch (error) {
10484
- if (error && typeof error === "object") {
10485
- error.path = error?.path || path;
10486
- error.sharedId = error?.sharedId || sharedId;
10487
- }
10488
- throw error;
10468
+ const { nonce, ct } = unpackBodyData(body);
10469
+ return normalizeOwnerEpochRecord(await openJson(key, nonce, ct, ownerEpochAad(entryId, epochEntryId)));
10489
10470
  } finally {
10490
10471
  cleanBytes(key);
10491
10472
  }
10492
10473
  }
10474
+ function ownEpochEntryId(chatPrivateKey, chatId, epochId) {
10475
+ return ownerEpochEntryId(chatPrivateKey, chatId, epochId);
10476
+ }
10493
10477
 
10494
- // ../../core/files.js
10478
+ // ../../core/chat/epochs/bootstrap.js
10495
10479
  "use client";
10496
- function makeFileId(size = 8) {
10497
- return toHex(randomBytes3(size));
10480
+ var CHAT_INITIAL_EPOCH_CREATED_AT = 1;
10481
+ function initialEpochCommitment(manifestValue, stateCapability) {
10482
+ const manifest = normalizeEpochManifest(manifestValue);
10483
+ return toHex(sha256(canonicalBytes({
10484
+ v: 2,
10485
+ protocol: CHAT_PROTOCOL_VERSION,
10486
+ chatId: manifest.chatId,
10487
+ epochId: manifest.epochId,
10488
+ epochVersion: manifest.epochVersion,
10489
+ manifestDigest: epochManifestDigest(manifest),
10490
+ stateCapabilityCommitment: stateCapabilityCommitment(stateCapability)
10491
+ }, "initial chat epoch commitment")));
10498
10492
  }
10499
- function setErrorStage(error, stage, extra = {}) {
10500
- if (!error || typeof error !== "object") {
10501
- return error;
10502
- }
10503
- error.stage = error?.stage || stage;
10504
- Object.assign(error, extra);
10505
- return error;
10493
+ function makeInitialEpochManifest({ chatId, epochId, members, lineage }, options = {}) {
10494
+ return normalizeEpochManifest({
10495
+ v: CHAT_MANIFEST_VERSION,
10496
+ protocol: CHAT_PROTOCOL_VERSION,
10497
+ chatId,
10498
+ epochId,
10499
+ epochVersion: 1,
10500
+ parentEpochId: null,
10501
+ createdAt: CHAT_INITIAL_EPOCH_CREATED_AT,
10502
+ lineage,
10503
+ members
10504
+ }, options);
10506
10505
  }
10507
- async function makeChatFileUpload(epochId, cid, data, { cacheControl = "private, max-age=0, no-transform" } = {}) {
10508
- try {
10509
- return await makeChatFileUploadPayload(epochId, cid, data, {
10510
- cacheControl
10511
- });
10512
- } catch (error) {
10513
- throw setErrorStage(error, "encrypt", {
10514
- ...error?.path ? { path: error.path } : {},
10515
- cid
10516
- });
10517
- }
10506
+
10507
+ // ../../core/chat/criticaldelivery.js
10508
+ "use client";
10509
+ var RETRY_DELAYS_MS = Object.freeze([150, 600]);
10510
+ function retryableDeliveryError(error) {
10511
+ const code = cleanText(error?.code).toLowerCase();
10512
+ const message = cleanText(error?.message).toLowerCase();
10513
+ const retryableCodes = [
10514
+ "aborted",
10515
+ "deadline-exceeded",
10516
+ "internal",
10517
+ "network-request-failed",
10518
+ "resource-exhausted",
10519
+ "unavailable",
10520
+ "unknown"
10521
+ ];
10522
+ return retryableCodes.some((value) => code === value || code.endsWith(`/${value}`)) || /\b(network|offline|timed? out|temporar(?:y|ily)|connection)\b/u.test(message);
10518
10523
  }
10519
- async function putChatFile(epochId, cid, data, options) {
10520
- const upload = await makeChatFileUpload(epochId, cid, data, options);
10521
- try {
10522
- if (typeof options?.uploadChatMedia !== "function") {
10523
- throw new Error("chat media upload required");
10524
+ function wait(ms) {
10525
+ return ms > 0 ? new Promise((resolve) => setTimeout(resolve, ms)) : Promise.resolve();
10526
+ }
10527
+ async function pushCriticalInbox(cloud, recipientUid, ping, options = {}, retry = {}) {
10528
+ const deliveryId = cleanText(options.deliveryId).toLowerCase() || toHex(randomBytes3(32));
10529
+ const delays = Array.isArray(retry.delaysMs) ? retry.delaysMs : RETRY_DELAYS_MS;
10530
+ let lastError = null;
10531
+ for (let attempt = 0;attempt <= delays.length; attempt += 1) {
10532
+ try {
10533
+ await cloud.inbox.push(recipientUid, ping, { ...options, deliveryId });
10534
+ return { deliveryId, attempts: attempt + 1 };
10535
+ } catch (error) {
10536
+ lastError = error;
10537
+ if (attempt >= delays.length || !retryableDeliveryError(error))
10538
+ break;
10539
+ await wait(delays[attempt]);
10524
10540
  }
10525
- await options.uploadChatMedia(upload);
10526
- return upload.file;
10527
- } catch (error) {
10528
- throw setErrorStage(error, "upload", { path: upload.path, cid });
10529
10541
  }
10542
+ throw lastError || new Error("critical inbox delivery failed");
10530
10543
  }
10531
- async function makeSharedFileUpload(data, { contentType = "application/octet-stream", cacheControl = "private, max-age=0, no-transform" } = {}) {
10532
- try {
10533
- return await makeSharedFileUploadPayload(data, {
10534
- contentType,
10535
- cacheControl
10536
- });
10537
- } catch (error) {
10538
- throw setErrorStage(error, "encrypt", {
10539
- ...error?.path ? { path: error.path } : {}
10540
- });
10544
+ var criticalDeliveryInternals = Object.freeze({ retryableDeliveryError });
10545
+
10546
+ // ../../core/chat/owner.js
10547
+ "use client";
10548
+ async function prepareMutation(identity, entryId, current, update) {
10549
+ const prepared = await update(current);
10550
+ if (!prepared?.entry) {
10551
+ return { unchanged: true, result: prepared?.result ?? null };
10541
10552
  }
10553
+ const entry = {
10554
+ ...prepared.entry,
10555
+ ownerRevision: (current?.ownerRevision || 0) + 1
10556
+ };
10557
+ return {
10558
+ uid: identity.uid,
10559
+ entryId,
10560
+ record: {
10561
+ body: await sealOwnChatEntry(identity.chatPrivateKey, entryId, entry),
10562
+ revision: entry.ownerRevision,
10563
+ ...Number.isFinite(prepared.tsMs) ? { tsMs: prepared.tsMs } : {},
10564
+ ...prepared.touchTs === true ? { touchTs: true } : {}
10565
+ },
10566
+ epochs: prepared.epochs || [],
10567
+ mlsWrites: prepared.mlsWrites || [],
10568
+ mlsDeletes: prepared.mlsDeletes || [],
10569
+ result: prepared.result === prepared.entry || prepared.result == null ? entry : prepared.result
10570
+ };
10542
10571
  }
10543
- async function putSharedFile(data, options = {}) {
10544
- const upload = await makeSharedFileUpload(data, options);
10545
- try {
10546
- if (typeof options?.uploadSharedMedia !== "function") {
10547
- throw new Error("shared media upload required");
10548
- }
10549
- await options.uploadSharedMedia(upload);
10550
- return upload.file;
10551
- } catch (error) {
10552
- throw setErrorStage(error, "upload", { path: upload.path });
10572
+ async function prepareOwnChatMutation(identity, entryId, current, update) {
10573
+ if (!identity?.uid || !identity?.chatPrivateKey || !entryId || typeof update !== "function") {
10574
+ throw new Error("chat owner mutation required");
10553
10575
  }
10554
- }
10555
- async function readChatFile(readChatMedia, file) {
10556
- let body;
10557
- try {
10558
- if (typeof readChatMedia !== "function") {
10559
- throw new Error("chat media read required");
10576
+ const initial = await prepareMutation(identity, entryId, current, update);
10577
+ return {
10578
+ ...initial,
10579
+ open: (record) => openOwnChatMutationEntry(identity, entryId, record),
10580
+ prepare: async (record) => {
10581
+ if (!record) {
10582
+ return prepareMutation(identity, entryId, null, update);
10583
+ }
10584
+ if (!record?.body || !Number.isSafeInteger(record.revision) || record.revision < 1) {
10585
+ throw new Error("current chat owner record required");
10586
+ }
10587
+ const opened = await openOwnChatEntry(identity.chatPrivateKey, entryId, record.body);
10588
+ if (opened.ownerRevision !== record.revision) {
10589
+ throw new Error("chat owner revision mismatch");
10590
+ }
10591
+ return prepareMutation(identity, entryId, opened, update);
10560
10592
  }
10561
- body = await readChatMedia(file?.p);
10562
- } catch (error) {
10563
- error.path = error?.path || file?.p || null;
10564
- error.stage = error?.stage || "download";
10565
- throw error;
10593
+ };
10594
+ }
10595
+ async function mutateOwnChatEntry(cloud, identity, entryId, current, update) {
10596
+ if (!cloud?.user?.chats?.mutate) {
10597
+ throw new Error("chat owner mutation required");
10566
10598
  }
10567
- try {
10568
- getMediaFileRef(file?.p);
10569
- const bytes = await openFileForPath(decodeFileKey(file?.k), body, file?.p);
10570
- return bytes;
10571
- } catch (error) {
10572
- error.path = file?.p || null;
10573
- error.stage = "decrypt";
10574
- throw error;
10599
+ const mutation = await prepareOwnChatMutation(identity, entryId, current, update);
10600
+ if (mutation.unchanged)
10601
+ return mutation.result;
10602
+ const committed = await cloud.user.chats.mutate(identity.uid, entryId, mutation);
10603
+ return committed?.result ?? mutation.result ?? null;
10604
+ }
10605
+ async function openOwnChatMutationEntry(identity, entryId, record) {
10606
+ if (!record)
10607
+ return null;
10608
+ if (!record?.body || !Number.isSafeInteger(record.revision) || record.revision < 1)
10609
+ throw new Error("current chat owner record required");
10610
+ const current = await openOwnChatEntry(identity.chatPrivateKey, entryId, record.body);
10611
+ if (current.ownerRevision !== record.revision) {
10612
+ throw new Error("chat owner revision mismatch");
10575
10613
  }
10614
+ return current;
10576
10615
  }
10577
10616
 
10578
10617
  // ../../core/chat/messages/types.js
@@ -11484,7 +11523,7 @@ function cleanChatSettingsEventTitle(value) {
11484
11523
  if (value == null)
11485
11524
  return null;
11486
11525
  const title = cleanText(value);
11487
- if (!title || title.length > 128)
11526
+ if (!title || title.length > CHAT_TITLE_MAX_CHARS)
11488
11527
  throw new Error("invalid chat settings event title");
11489
11528
  return title;
11490
11529
  }
@@ -11499,10 +11538,7 @@ function makeChatSettingsMsg(revision, digest, changes = [], values = {}) {
11499
11538
  const titleChanged = cleanChanges.includes("title");
11500
11539
  const avatarChanged = cleanChanges.includes("avatarRef");
11501
11540
  const title = titleChanged ? cleanChatSettingsEventTitle(values?.title) : undefined;
11502
- const avatarRef = avatarChanged ? values?.avatarRef == null ? null : chatAvatarDataUrl(values.avatarRef) : undefined;
11503
- if (avatarChanged && values?.avatarRef != null && !avatarRef) {
11504
- throw new Error("invalid chat settings event avatar");
11505
- }
11541
+ const avatarRef = avatarChanged ? values?.avatarRef == null ? null : cleanChatAvatarRef(values.avatarRef) : undefined;
11506
11542
  return {
11507
11543
  t: SYSTEM_MSG_TYPE,
11508
11544
  sys: SYSTEM_SETTINGS_KIND,
@@ -11525,7 +11561,7 @@ function isChatSettingsMsg(msg) {
11525
11561
  return false;
11526
11562
  }
11527
11563
  }
11528
- return !changes.includes("avatarRef") || msg?.avatarRef == null || !!chatAvatarDataUrl(msg.avatarRef);
11564
+ return !changes.includes("avatarRef") || msg?.avatarRef == null || !!chatAvatarFile(msg.avatarRef);
11529
11565
  }
11530
11566
  function isPendingChatSettingsMsg(msg) {
11531
11567
  return msg?.t === SYSTEM_MSG_TYPE && msg?.sys === SYSTEM_SETTINGS_KIND && msg?.pending === true && String(msg?.id || "").startsWith("local:") && chatSettingsEventChanges(msg).length === 1 && chatSettingsEventChanges(msg)[0] === "retention";
@@ -11533,7 +11569,7 @@ function isPendingChatSettingsMsg(msg) {
11533
11569
  function getChatSettingsEventAvatar(msg) {
11534
11570
  if (!isChatSettingsMsg(msg) || !chatSettingsEventChanges(msg).includes("avatarRef"))
11535
11571
  return "";
11536
- return chatAvatarDataUrl(msg?.avatarRef);
11572
+ return chatAvatarFile(msg?.avatarRef) ? msg.avatarRef : "";
11537
11573
  }
11538
11574
  function isSystemMsg(msg) {
11539
11575
  return !!getSystemMsgText(msg) && (msg?.t === SYSTEM_MSG_TYPE || isMembershipEventMsg(msg));
@@ -12242,13 +12278,12 @@ function withChatPreviewExpired(preview, expiredAt) {
12242
12278
  sourceTs: getChatPreviewSourceTs(preview)
12243
12279
  }));
12244
12280
  }
12245
- function chatPreviewWantsAttention(preview, chatPK) {
12281
+ function chatPreviewCountsAsUnseen(preview, chatPK) {
12246
12282
  if (!preview || fromSelf(preview, chatPK)) {
12247
12283
  return false;
12248
12284
  }
12249
12285
  if (isReactionMsg(preview)) {
12250
- const targetFrom = cleanText(preview?.targetFrom);
12251
- return !!cleanText(preview?.emoji) && (!targetFrom || targetFrom === chatPK);
12286
+ return !!cleanText(preview?.emoji);
12252
12287
  }
12253
12288
  return canRenderPreviewContent(preview);
12254
12289
  }
@@ -14656,7 +14691,7 @@ function getPeersFromChats(chats) {
14656
14691
  }
14657
14692
  function isChatUnseenForUser(chatData, userChatPK) {
14658
14693
  const last = chatData?.preview;
14659
- if (!last?.ts || !chatPreviewWantsAttention(last, userChatPK))
14694
+ if (!last?.ts || !chatPreviewCountsAsUnseen(last, userChatPK))
14660
14695
  return false;
14661
14696
  const readMs = timestampMs(chatData?.readMs, null);
14662
14697
  const lastMs = timestampMs(last.ts, null);
@@ -16180,11 +16215,13 @@ function createChatDelete({
16180
16215
  listActionsRef,
16181
16216
  closeMessageBatchRef,
16182
16217
  mutationGate,
16218
+ reconcileEpoch,
16183
16219
  diag
16184
16220
  }) {
16185
16221
  const pendingDeleteIdsRef = { current: new Set };
16186
16222
  const locallyDeletedChatIdsRef = { current: new Set };
16187
16223
  const deletedChatIdsRef = { current: new Set };
16224
+ const removedChatVersionsRef = { current: new Map };
16188
16225
  const keepSelectedDeletedChatIdsRef = { current: new Set };
16189
16226
  const isChatPendingDelete = (chatId) => !!chatId && pendingDeleteIdsRef.current.has(chatId);
16190
16227
  const clearDeletedChatState = (chatIds) => {
@@ -16294,20 +16331,46 @@ function createChatDelete({
16294
16331
  listActionsRef.current?.commitServerChats?.(nextServerChats, { warm: false });
16295
16332
  setSelectedChat((current) => current === chatId ? null : current);
16296
16333
  };
16297
- const dropRemovedChat = (chatId) => {
16298
- if (!chatId)
16334
+ const dropRemovedChat = (chatId, epochVersion) => {
16335
+ if (!chatId || !Number.isSafeInteger(epochVersion) || epochVersion < 1)
16299
16336
  return false;
16300
- deletedChatIdsRef.current.add(chatId);
16301
- locallyDeletedChatIdsRef.current.add(chatId);
16337
+ const previousVersion = removedChatVersionsRef.current.get(chatId) || 0;
16338
+ removedChatVersionsRef.current.set(chatId, Math.max(previousVersion, epochVersion));
16302
16339
  dropChat(chatId);
16303
16340
  return true;
16304
16341
  };
16342
+ const commitRemovedRetirement = (chatId, epochVersion) => {
16343
+ if (!chatId || !Number.isSafeInteger(epochVersion) || epochVersion < 1)
16344
+ return false;
16345
+ const previousVersion = removedChatVersionsRef.current.get(chatId) || 0;
16346
+ removedChatVersionsRef.current.set(chatId, Math.max(previousVersion, epochVersion));
16347
+ pendingDeleteIdsRef.current.delete(chatId);
16348
+ locallyDeletedChatIdsRef.current.delete(chatId);
16349
+ deletedChatIdsRef.current.delete(chatId);
16350
+ keepSelectedDeletedChatIdsRef.current.delete(chatId);
16351
+ confirmDeletedChats([chatId]);
16352
+ return true;
16353
+ };
16354
+ const restoreRemovedChat = (chat) => {
16355
+ const chatId = chat?.id;
16356
+ const removedAtVersion = removedChatVersionsRef.current.get(chatId);
16357
+ if (!chatId || removedAtVersion == null)
16358
+ return false;
16359
+ if (!Number.isSafeInteger(chat.epochVersion) || chat.epochVersion <= removedAtVersion || !Array.isArray(chat.memberChatPKs) || !chat.memberChatPKs.includes(chatPK))
16360
+ return false;
16361
+ removedChatVersionsRef.current.delete(chatId);
16362
+ return true;
16363
+ };
16305
16364
  const dropUnavailableChat = (chatId) => {
16306
16365
  if (!chatId)
16307
16366
  return Promise.resolve(false);
16308
16367
  return (async () => {
16309
16368
  const releaseMutation = await mutationGate?.acquireExclusive(chatId) || (() => {});
16310
16369
  try {
16370
+ if (removedChatVersionsRef.current.has(chatId)) {
16371
+ dropChat(chatId);
16372
+ return true;
16373
+ }
16311
16374
  const chat = lastServerChatsRef.current.find((item) => item?.id === chatId);
16312
16375
  if (chat?.deliveryRegistered && chat?.epochVersion && chatPK && chat?.epochState?.stateCapability) {
16313
16376
  await cloud.delivery.revoke(deriveChatDeliveryCapability(chat.epochState.stateCapability, {
@@ -16384,39 +16447,74 @@ function createChatDelete({
16384
16447
  }, target.members, target.expectedEpochId);
16385
16448
  return result.deliveredCount;
16386
16449
  };
16450
+ const deleteCurrentChat = async (input, options = {}) => {
16451
+ const chatId = cleanText(typeof input === "string" ? input : input?.chatId || input?.id);
16452
+ const attemptedEpochs = new Set;
16453
+ while (chatId) {
16454
+ const target = getDeleteTargets(input)[0];
16455
+ if (!target)
16456
+ return null;
16457
+ const peerChatPK = cleanText(typeof input === "object" ? input?.peerChatPK : "") || cleanText(lastServerChatsRef.current.find((item) => item?.id === chatId)?.peerChatPK);
16458
+ const releaseMutation = await mutationGate?.acquireExclusive([chatId, peerChatPK].filter(Boolean)) || (() => {});
16459
+ let released = false;
16460
+ const release = () => {
16461
+ if (released)
16462
+ return;
16463
+ released = true;
16464
+ releaseMutation();
16465
+ };
16466
+ try {
16467
+ const deletion = await cloud.chat.delete(target.chatId, {
16468
+ entryId: target.entryId,
16469
+ expectedEpochId: target.expectedEpochId,
16470
+ stateCapability: target.stateCapability,
16471
+ cleanup: options.cleanup !== false
16472
+ });
16473
+ if (deletion?.newlyDeleted !== false)
16474
+ await wakeDeletedChat(target);
16475
+ return target;
16476
+ } catch (error) {
16477
+ release();
16478
+ const attemptKey = `${target.epochVersion || 0}:${target.expectedEpochId || ""}`;
16479
+ if (!attemptKey || attemptedEpochs.has(attemptKey) || !isChatEpochConflict(error) || typeof reconcileEpoch !== "function")
16480
+ throw error;
16481
+ attemptedEpochs.add(attemptKey);
16482
+ const reconciled = await reconcileEpoch(chatId);
16483
+ const current = lastServerChatsRef.current.find((item) => item?.id === chatId) || null;
16484
+ if (reconciled?.removed || !current)
16485
+ return null;
16486
+ if (!Number.isSafeInteger(current.epochVersion) || current.epochVersion <= target.epochVersion) {
16487
+ throw error;
16488
+ }
16489
+ } finally {
16490
+ release();
16491
+ }
16492
+ }
16493
+ return null;
16494
+ };
16387
16495
  const deleteChat = async (chat, options = {}) => {
16388
- const targets = getDeleteTargets(chat);
16496
+ const inputs = Array.isArray(chat) ? chat : [chat];
16497
+ const targets = getDeleteTargets(inputs);
16389
16498
  if (!targets.length)
16390
16499
  return false;
16391
16500
  const chatIds = targets.map((target) => target.chatId);
16392
- const peerChatPKs = chatIds.map((chatId) => {
16393
- const input = (Array.isArray(chat) ? chat : [chat]).find((item) => cleanText(typeof item === "string" ? item : item?.chatId || item?.id) === chatId);
16394
- return cleanText(typeof input === "object" ? input?.peerChatPK : "") || cleanText(lastServerChatsRef.current.find((item) => item?.id === chatId)?.peerChatPK);
16395
- }).filter(Boolean);
16396
- const releaseMutation = await mutationGate?.acquireExclusive([...chatIds, ...peerChatPKs]) || (() => {});
16501
+ const deletedTargets = [];
16502
+ hideDeletingChats(chatIds, options);
16397
16503
  try {
16398
- hideDeletingChats(chatIds, options);
16399
- try {
16400
- for (const target of targets) {
16401
- await cloud.chat.delete(target.chatId, {
16402
- entryId: target.entryId,
16403
- expectedEpochId: target.expectedEpochId,
16404
- stateCapability: target.stateCapability,
16405
- cleanup: options.cleanup !== false
16406
- });
16407
- await wakeDeletedChat(target);
16408
- }
16409
- await revokeTargetDeliveries(targets);
16410
- await options.onDeliveryRevoked?.();
16411
- confirmDeletedChats(chatIds);
16412
- } catch (error) {
16413
- restoreDeletedChats(chatIds);
16414
- throw error;
16415
- }
16416
- return Array.isArray(chat) ? targets.length : true;
16417
- } finally {
16418
- releaseMutation();
16504
+ for (const target of targets) {
16505
+ const input = inputs.find((item) => cleanText(typeof item === "string" ? item : item?.chatId || item?.id) === target.chatId) || target.chatId;
16506
+ const deleted = await deleteCurrentChat(input, options);
16507
+ if (deleted)
16508
+ deletedTargets.push(deleted);
16509
+ }
16510
+ await revokeTargetDeliveries(deletedTargets);
16511
+ await options.onDeliveryRevoked?.();
16512
+ confirmDeletedChats(chatIds);
16513
+ } catch (error) {
16514
+ restoreDeletedChats(chatIds);
16515
+ throw error;
16419
16516
  }
16517
+ return Array.isArray(chat) ? targets.length : true;
16420
16518
  };
16421
16519
  const wasChatDeletedLocally = (chatId) => !!chatId && locallyDeletedChatIdsRef.current.has(chatId);
16422
16520
  const ackDeletedChat = (chatId) => {
@@ -16427,21 +16525,25 @@ function createChatDelete({
16427
16525
  pendingDeleteIdsRef.current = new Set;
16428
16526
  locallyDeletedChatIdsRef.current = new Set;
16429
16527
  deletedChatIdsRef.current = new Set;
16528
+ removedChatVersionsRef.current = new Map;
16430
16529
  keepSelectedDeletedChatIdsRef.current = new Set;
16431
16530
  };
16432
16531
  return {
16433
16532
  pendingDeleteIdsRef,
16434
16533
  deletedChatIdsRef,
16534
+ removedChatVersionsRef,
16435
16535
  keepSelectedDeletedChatIdsRef,
16436
16536
  resetDeleteState,
16437
16537
  clearDeletedChatState,
16438
16538
  beginRetirement,
16439
16539
  rollbackRetirement,
16440
16540
  commitRetirement,
16541
+ commitRemovedRetirement,
16441
16542
  restoreDeletedChat,
16442
16543
  deleteChat,
16443
16544
  dropChat,
16444
16545
  dropRemovedChat,
16546
+ restoreRemovedChat,
16445
16547
  dropUnavailableChat,
16446
16548
  wasChatDeletedLocally,
16447
16549
  ackDeletedChat,
@@ -17902,7 +18004,73 @@ function withoutChatIdentity(options = {}) {
17902
18004
  }
17903
18005
  function refreshQueuedSendOptions(sendOptionsForPeer, peerChatPK, sendOptionOverrides = null) {
17904
18006
  const chatId = cleanText(sendOptionOverrides?.chatId);
17905
- return mergeSendOptions(withoutChatIdentity(sendOptionOverrides), sendOptionsForPeer(peerChatPK, chatId));
18007
+ const current = sendOptionsForPeer(peerChatPK, chatId);
18008
+ if (chatId) {
18009
+ return {
18010
+ ...withoutChatIdentity(sendOptionOverrides),
18011
+ ...current
18012
+ };
18013
+ }
18014
+ return mergeSendOptions(withoutChatIdentity(sendOptionOverrides), current);
18015
+ }
18016
+ function currentEpoch(options) {
18017
+ const manifest = options?.epochState?.manifest || options?.ownEntry?.current?.manifest || null;
18018
+ const epochId = cleanText(manifest?.epochId);
18019
+ const epochVersion = Number(manifest?.epochVersion);
18020
+ return epochId && Number.isInteger(epochVersion) && epochVersion > 0 ? { epochId, epochVersion } : null;
18021
+ }
18022
+ async function runCurrentEpochWrite({
18023
+ chatId,
18024
+ mutationGate,
18025
+ readOptions,
18026
+ reconcileEpoch,
18027
+ write,
18028
+ onCommitted,
18029
+ onEpochAdvance
18030
+ }) {
18031
+ const attemptedEpochs = new Set;
18032
+ while (true) {
18033
+ const releaseMutation = await mutationGate?.acquireShared(chatId) || (() => {});
18034
+ let released = false;
18035
+ let committed = false;
18036
+ const release = () => {
18037
+ if (released)
18038
+ return;
18039
+ released = true;
18040
+ releaseMutation();
18041
+ };
18042
+ let options;
18043
+ let epoch;
18044
+ try {
18045
+ options = readOptions();
18046
+ epoch = currentEpoch(options);
18047
+ return await write(options, (...args) => {
18048
+ committed = true;
18049
+ release();
18050
+ onCommitted?.(...args);
18051
+ });
18052
+ } catch (error) {
18053
+ release();
18054
+ const attemptKey = epoch ? `${epoch.epochVersion}:${epoch.epochId}` : "";
18055
+ if (committed || options?.chatExists !== true || !attemptKey || attemptedEpochs.has(attemptKey) || !isChatEpochConflict(error) || typeof reconcileEpoch !== "function") {
18056
+ throw error;
18057
+ }
18058
+ attemptedEpochs.add(attemptKey);
18059
+ await reconcileEpoch(chatId);
18060
+ const nextEpoch = currentEpoch(readOptions());
18061
+ if (!nextEpoch || nextEpoch.epochVersion <= epoch.epochVersion || nextEpoch.epochId === epoch.epochId) {
18062
+ throw error;
18063
+ }
18064
+ onEpochAdvance?.({
18065
+ fromEpochId: epoch.epochId,
18066
+ fromEpochVersion: epoch.epochVersion,
18067
+ toEpochId: nextEpoch.epochId,
18068
+ toEpochVersion: nextEpoch.epochVersion
18069
+ });
18070
+ } finally {
18071
+ release();
18072
+ }
18073
+ }
17906
18074
  }
17907
18075
  function markSendPerf(diag, label, startedAt, data = {}, minMs = SEND_CHAT_REDUCER_WARN_MS) {
17908
18076
  const elapsedMs = Date.now() - startedAt;
@@ -18167,7 +18335,7 @@ async function makeSharedMediaAttachment(cloud, media, message, data) {
18167
18335
  ...source?.c ? { caption: source.c } : {}
18168
18336
  }));
18169
18337
  }
18170
- function createChatSend({ cloud, media = {}, uid, chatBanned, chatPK, chatSigningPK, chatSigningSecret, chatPrivateKey, notificationPK, notificationPrivateKey, localCache, localByChatRef, setLocalByChat, setChats, setLastChat, sendOptionsForPeer, selectLocalChat, adoptLocalMessageMedia, readMessageFile, mutationGate, diag = null }) {
18338
+ function createChatSend({ cloud, media = {}, uid, chatBanned, chatPK, chatSigningPK, chatSigningSecret, chatPrivateKey, notificationPK, notificationPrivateKey, localCache, localByChatRef, setLocalByChat, setChats, setLastChat, sendOptionsForPeer, selectLocalChat, adoptLocalMessageMedia, readMessageFile, mutationGate, reconcileEpoch, diag = null }) {
18171
18339
  const adoptedLocalMediaRef = { current: new Set };
18172
18340
  const attachmentPrepareByCidRef = { current: new Map };
18173
18341
  const cachedLocalMediaRef = { current: new Set };
@@ -18527,78 +18695,80 @@ ${cid}` : "";
18527
18695
  };
18528
18696
  const queueSend = async (peerChatPK, message, run, { previewRequired = false, sendOptions: sendOptionOverrides = null } = {}) => {
18529
18697
  const startedAt = Date.now();
18530
- let sendOptions = refreshQueuedSendOptions(sendOptionsForPeer, peerChatPK, sendOptionOverrides);
18531
- let local = await showLocalMessage(peerChatPK, message, sendOptions);
18698
+ const sendOptions = refreshQueuedSendOptions(sendOptionsForPeer, peerChatPK, sendOptionOverrides);
18699
+ const local = await showLocalMessage(peerChatPK, message, sendOptions);
18532
18700
  const mutationTarget = local.chatId || cleanText(sendOptionOverrides?.chatId) || peerChatPK;
18533
- let releaseMutation;
18534
- try {
18535
- releaseMutation = await mutationGate?.acquireShared(mutationTarget) || (() => {});
18536
- } catch (error) {
18537
- markLocalStatus(local.chatId, local.cid, LOCAL_FAILED);
18538
- throw error;
18539
- }
18540
- try {
18541
- sendOptions = refreshQueuedSendOptions(sendOptionsForPeer, peerChatPK, {
18542
- ...sendOptionOverrides,
18543
- chatId: local.chatId
18544
- });
18545
- const requiredPreview = previewRequired || sendOptions.chatExists !== true;
18546
- let committed = false;
18547
- let sent = false;
18548
- const markSent = () => {
18549
- if (sent)
18550
- return;
18551
- sent = true;
18552
- sentChatIdsRef.current.add(local.chatId);
18553
- markLocalStatus(local.chatId, local.cid, LOCAL_SENT);
18554
- };
18555
- const markCommitted = () => {
18556
- if (committed)
18557
- return;
18558
- committed = true;
18559
- markSent();
18560
- markDone(diag, "chat.send.commit", startedAt, { type: local?.t || "txt", previewRequired: !!requiredPreview });
18561
- };
18562
- return await new Promise((resolve, reject) => {
18563
- const job = {
18564
- previewKey: local.chatId,
18565
- previewRequired: requiredPreview,
18566
- resolve,
18567
- reject,
18568
- onSuccess: markSent,
18569
- onError: (error) => {
18570
- markError(diag, "chat.send.job", startedAt, error, { type: local?.t || "txt", previewRequired: !!requiredPreview });
18571
- if (committed)
18572
- markSent();
18573
- else
18574
- markLocalStatus(local.chatId, local.cid, LOCAL_FAILED);
18575
- },
18576
- run: async (context) => {
18577
- const writeStartedAt = Date.now();
18578
- const onCommitted = () => {
18579
- markCommitted();
18580
- context?.releaseTargets?.();
18581
- };
18582
- markDiag(diag, "chat.send.write.start", { type: local?.t || "txt", previewRequired: !!requiredPreview, updatePreview: !!context?.updatePreview });
18583
- try {
18584
- const result = await run({ ...context, local, onCommitted, sendOptions });
18585
- markDone(diag, "chat.send.write", writeStartedAt, { type: local?.t || "txt", previewRequired: !!requiredPreview, updatePreview: !!context?.updatePreview });
18586
- return result;
18587
- } catch (error) {
18588
- markError(diag, "chat.send.write", writeStartedAt, error, {
18589
- type: local?.t || "txt",
18590
- previewRequired: !!requiredPreview,
18591
- updatePreview: !!context?.updatePreview
18592
- });
18593
- throw error;
18594
- }
18701
+ const requiredPreview = previewRequired || sendOptions.chatExists !== true;
18702
+ let committed = false;
18703
+ let sent = false;
18704
+ const markSent = () => {
18705
+ if (sent)
18706
+ return;
18707
+ sent = true;
18708
+ sentChatIdsRef.current.add(local.chatId);
18709
+ markLocalStatus(local.chatId, local.cid, LOCAL_SENT);
18710
+ };
18711
+ const markCommitted = () => {
18712
+ if (committed)
18713
+ return;
18714
+ committed = true;
18715
+ markSent();
18716
+ markDone(diag, "chat.send.commit", startedAt, { type: local?.t || "txt", previewRequired: !!requiredPreview });
18717
+ };
18718
+ return await new Promise((resolve, reject) => {
18719
+ const job = {
18720
+ previewKey: local.chatId,
18721
+ previewRequired: requiredPreview,
18722
+ resolve,
18723
+ reject,
18724
+ onSuccess: markSent,
18725
+ onError: (error) => {
18726
+ markError(diag, "chat.send.job", startedAt, error, { type: local?.t || "txt", previewRequired: !!requiredPreview });
18727
+ if (committed)
18728
+ markSent();
18729
+ else
18730
+ markLocalStatus(local.chatId, local.cid, LOCAL_FAILED);
18731
+ },
18732
+ run: async (context) => {
18733
+ const writeStartedAt = Date.now();
18734
+ markDiag(diag, "chat.send.write.start", { type: local?.t || "txt", previewRequired: !!requiredPreview, updatePreview: !!context?.updatePreview });
18735
+ try {
18736
+ const result = await runCurrentEpochWrite({
18737
+ chatId: mutationTarget,
18738
+ mutationGate,
18739
+ readOptions: () => refreshQueuedSendOptions(sendOptionsForPeer, peerChatPK, {
18740
+ ...sendOptionOverrides,
18741
+ chatId: local.chatId
18742
+ }),
18743
+ reconcileEpoch,
18744
+ write: (currentSendOptions, onCommitted) => run({
18745
+ ...context,
18746
+ local,
18747
+ onCommitted,
18748
+ sendOptions: currentSendOptions
18749
+ }),
18750
+ onCommitted: () => {
18751
+ markCommitted();
18752
+ context?.releaseTargets?.();
18753
+ },
18754
+ onEpochAdvance: ({ fromEpochVersion, toEpochVersion }) => {
18755
+ markDiag(diag, "chat.send.epoch.advance", { fromEpochVersion, toEpochVersion });
18756
+ }
18757
+ });
18758
+ markDone(diag, "chat.send.write", writeStartedAt, { type: local?.t || "txt", previewRequired: !!requiredPreview, updatePreview: !!context?.updatePreview });
18759
+ return result;
18760
+ } catch (error) {
18761
+ markError(diag, "chat.send.write", writeStartedAt, error, {
18762
+ type: local?.t || "txt",
18763
+ previewRequired: !!requiredPreview,
18764
+ updatePreview: !!context?.updatePreview
18765
+ });
18766
+ throw error;
18595
18767
  }
18596
- };
18597
- enqueueSendJob(peerChatPK, job, reject);
18598
- });
18599
- } finally {
18600
- releaseMutation?.();
18601
- }
18768
+ }
18769
+ };
18770
+ enqueueSendJob(peerChatPK, job, reject);
18771
+ });
18602
18772
  };
18603
18773
  const sendOptionsForQueuedWrite = (baseOptions, local, updatePreview, onCommitted) => {
18604
18774
  const chatId = local?.chatId;
@@ -22715,7 +22885,7 @@ async function unlinkBlockedChat(cloud, uid, identity, entry, options) {
22715
22885
  const entryId = ownChatEntryId(identity.chatPrivateKey, manifest.chatId);
22716
22886
  markDiag(options.diag, "chat.owner.unlink", { reason: "blocked" });
22717
22887
  await cloud.user.chats.unlink(uid, entryId);
22718
- options.onRemoved?.(manifest.chatId);
22888
+ options.onRemoved?.(manifest.chatId, manifest.epochVersion);
22719
22889
  options.onPingDelete?.(manifest.chatId);
22720
22890
  return { removed: true, blocked: true, chatId: manifest.chatId };
22721
22891
  }
@@ -22734,6 +22904,25 @@ function isRetryableInboxError(error) {
22734
22904
  ];
22735
22905
  return retryableCodes.some((value) => code === value || code.endsWith(`/${value}`)) || /\b(network|offline|timed? out|temporar(?:y|ily)|connection)\b/.test(message);
22736
22906
  }
22907
+ async function retireConfirmedDeletedChat(cloud, uid, identity, entry, options = {}) {
22908
+ const manifest = entry?.current?.manifest;
22909
+ if (!manifest?.chatId || !identity?.chatPrivateKey || !identity?.chatPK) {
22910
+ throw new Error("deleted chat owner entry required");
22911
+ }
22912
+ const entryId = ownChatEntryId(identity.chatPrivateKey, manifest.chatId);
22913
+ await cloud.user.chats.unlinkDeleted(uid, entryId, manifest.chatId);
22914
+ if (entry.deliveryRegistered) {
22915
+ const capability = deriveChatDeliveryCapability(entry.current.stateCapability, {
22916
+ chatId: manifest.chatId,
22917
+ recipientChatPK: identity.chatPK,
22918
+ generation: manifest.epochVersion
22919
+ });
22920
+ await cloud.delivery.revoke(capability).catch(() => false);
22921
+ }
22922
+ markDiag(options.diag, "chat.owner.unlink", { reason: "confirmed_deleted" });
22923
+ options.onPingDelete?.(manifest.chatId);
22924
+ return { removed: true, deleted: true, chatId: manifest.chatId };
22925
+ }
22737
22926
  function inboxErrorReason(error) {
22738
22927
  const message = cleanText(error?.message).toLowerCase();
22739
22928
  if (message === "ping sender identity mismatch")
@@ -23006,7 +23195,21 @@ async function processOpenedPing(cloud, uid, identity, ping, openedProfile, hint
23006
23195
  return "stale";
23007
23196
  }
23008
23197
  if (entry && payload.epochVersion > entry.current.manifest.epochVersion) {
23009
- const advanced = await advanceEntryToEpoch(cloud, uid, identity, entry, payload.epochVersion, options);
23198
+ let advanced;
23199
+ try {
23200
+ advanced = await advanceEntryToEpoch(cloud, uid, identity, entry, payload.epochVersion, options);
23201
+ } catch (error) {
23202
+ if (payload.kind !== "chat_deleted")
23203
+ throw error;
23204
+ try {
23205
+ await retireConfirmedDeletedChat(cloud, uid, identity, entry, options);
23206
+ return "deleted";
23207
+ } catch (confirmationError) {
23208
+ if (isRetryableInboxError(confirmationError))
23209
+ throw confirmationError;
23210
+ throw error;
23211
+ }
23212
+ }
23010
23213
  if (advanced?.removed) {
23011
23214
  options.onPingDelete?.(payload.chatId);
23012
23215
  return "deleted";
@@ -23062,6 +23265,11 @@ async function processOpenedPing(cloud, uid, identity, ping, openedProfile, hint
23062
23265
  options.onPingDelete?.(payload.chatId);
23063
23266
  return "deleted";
23064
23267
  }
23268
+ const latest = await advanceEntryToLatestEpoch(cloud, uid, identity, transitioned, options);
23269
+ if (latest?.removed) {
23270
+ options.onPingDelete?.(payload.chatId);
23271
+ return "deleted";
23272
+ }
23065
23273
  return "transition";
23066
23274
  }
23067
23275
  }
@@ -23170,8 +23378,15 @@ async function processPing(cloud, uid, identity, doc, options) {
23170
23378
  async function applyEpochTransitionAction(cloud, uid, identityValue, entry, action, options, confirmedValue = null) {
23171
23379
  const parent = entry.current.manifest;
23172
23380
  const sender = manifestMember(parent, action.s || action.certificate?.actor);
23173
- if (!sender)
23381
+ const reject = (stage) => {
23382
+ markDiag(options.diag, "chat.epoch.apply.reject", {
23383
+ stage,
23384
+ epochVersion: parent.epochVersion
23385
+ });
23174
23386
  return null;
23387
+ };
23388
+ if (!sender)
23389
+ return reject("sender");
23175
23390
  const engine = await options.mls?.open?.();
23176
23391
  if (!engine)
23177
23392
  throw new Error("mls engine required");
@@ -23179,25 +23394,25 @@ async function applyEpochTransitionAction(cloud, uid, identityValue, entry, acti
23179
23394
  const expectedCommitment = transitionCommitment(action.certificate);
23180
23395
  const confirmed = confirmedValue || await cloud.chat.state.transition(parent.chatId, parent.epochId, entry.current.stateCapability).catch(() => null);
23181
23396
  if (!confirmed || confirmed.parentEpochVersion !== parent.epochVersion || confirmed.nextEpochId !== action.certificate?.nextEpochId || confirmed.nextEpochVersion !== action.certificate?.nextEpochVersion || confirmed.transitionCommitment !== expectedCommitment || confirmed.transitionMessageId !== messageId || confirmed.mlsPackageId !== action.mls?.packageId || confirmed.mlsPackageDigest !== action.mls?.packageDigest)
23182
- return null;
23397
+ return reject("certificate");
23183
23398
  let leaveProposalId = "";
23184
23399
  if (action.membership?.kind === "leave") {
23185
23400
  const leavingChatPK = cleanText(action.membership?.actor);
23186
23401
  const proposalId = cleanText(action.membership?.proposalId);
23187
23402
  const targetSet = new Set(Array.isArray(action.membership?.targets) ? action.membership.targets.map(cleanText) : []);
23188
23403
  if (!leavingChatPK || !proposalId || targetSet.size !== 1 || !targetSet.has(leavingChatPK))
23189
- return null;
23404
+ return reject("leave-certificate");
23190
23405
  const proposalRecord = await cloud.chat.messages.read(parent.epochId, proposalId, {
23191
23406
  lane: messageLaneForEpoch(entry.current)
23192
23407
  }).catch(() => null);
23193
23408
  const proposal = proposalRecord ? await decryptMsg(proposalRecord, "", "", "", { chatId: parent.chatId, epochState: entry.current }).catch(() => null) : null;
23194
23409
  if (!isLeaveProposalMsg(proposal) || proposal.s !== leavingChatPK)
23195
- return null;
23410
+ return reject("leave-proposal");
23196
23411
  leaveProposalId = proposalId;
23197
23412
  }
23198
23413
  const packageRecord = await cloud.chat.mls.packages.read(parent.chatId, confirmed.mlsPackageId);
23199
23414
  if (!packageRecord || packageRecord.digest !== confirmed.mlsPackageDigest || packageRecord.head.transitionCommitment !== confirmed.transitionCommitment || !verifyChatMlsEpochPackageSignature(packageRecord, sender.chatSigningPK))
23200
- return null;
23415
+ return reject("mls-package");
23201
23416
  const identity = {
23202
23417
  ...identityValue,
23203
23418
  uid,
@@ -23208,8 +23423,11 @@ async function applyEpochTransitionAction(cloud, uid, identityValue, entry, acti
23208
23423
  let parentSnapshot = null;
23209
23424
  let nextSnapshot = null;
23210
23425
  let epochState = null;
23426
+ let openStage = "owner-snapshot";
23427
+ const openStartedAt = Date.now();
23211
23428
  try {
23212
23429
  parentSnapshot = await readOwnerChatMlsSnapshot(cloud, identity, entryId, entry.current);
23430
+ openStage = "mls-commit";
23213
23431
  const processed = engine.processCommit(parentSnapshot, packageRecord.commit);
23214
23432
  nextSnapshot = processed.snapshot;
23215
23433
  if (processed.removed) {
@@ -23228,9 +23446,10 @@ async function applyEpochTransitionAction(cloud, uid, identityValue, entry, acti
23228
23446
  if (leaveProposalId) {
23229
23447
  options.leaveProposalObservedAt?.delete(`${parent.chatId}:${leaveProposalId}`);
23230
23448
  }
23231
- options.onRemoved?.(parent.chatId);
23449
+ options.onRemoved?.(parent.chatId, confirmed.nextEpochVersion);
23232
23450
  return { removed: true, chatId: parent.chatId };
23233
23451
  }
23452
+ openStage = "mls-package-open";
23234
23453
  const opened = await openChatMlsEpochPackage(processed, packageRecord, {
23235
23454
  chatId: parent.chatId,
23236
23455
  senderSigningPK: sender.chatSigningPK,
@@ -23245,8 +23464,13 @@ async function applyEpochTransitionAction(cloud, uid, identityValue, entry, acti
23245
23464
  mlsPackageId: opened.packageId,
23246
23465
  mlsPackageDigest: opened.packageDigest
23247
23466
  };
23467
+ openStage = "settings";
23248
23468
  epochState.settings = await readEpochChatSettings(cloud, epochState);
23249
- } catch {
23469
+ } catch (error) {
23470
+ markError(options.diag, "chat.epoch.apply", openStartedAt, error, {
23471
+ stage: openStage,
23472
+ epochVersion: parent.epochVersion
23473
+ });
23250
23474
  cleanBytes(parentSnapshot, nextSnapshot, epochState?.epochSecret, epochState?.stateCapability);
23251
23475
  return null;
23252
23476
  }
@@ -23333,6 +23557,42 @@ async function advanceEntryToEpoch(cloud, uid, identity, entry, targetEpochVersi
23333
23557
  }).catch(() => null);
23334
23558
  const action = record ? await decryptMsg(record, "", "", "", { chatId: parent.chatId, epochState: current.current }).catch(() => null) : null;
23335
23559
  if (!isEpochTransitionMsg(action)) {
23560
+ markDiag(options.diag, "chat.epoch.advance.pending", {
23561
+ stage: record ? "transition-open" : "transition-record",
23562
+ epochVersion: parent.epochVersion
23563
+ });
23564
+ const error = new Error("chat epoch transition message is not available yet");
23565
+ error.code = "chat/epoch-pending";
23566
+ throw error;
23567
+ }
23568
+ const advanced = await applyEpochTransitionAction(cloud, uid, identity, current, action, options, confirmed);
23569
+ if (advanced?.removed)
23570
+ return advanced;
23571
+ if (!advanced?.current?.manifest || advanced.current.manifest.epochVersion <= parent.epochVersion) {
23572
+ const error = new Error("chat epoch transition could not be verified");
23573
+ error.code = "chat/epoch-pending";
23574
+ throw error;
23575
+ }
23576
+ current = advanced;
23577
+ }
23578
+ return current;
23579
+ }
23580
+ async function advanceEntryToLatestEpoch(cloud, uid, identity, entry, options = {}) {
23581
+ let current = entry;
23582
+ while (current?.current?.manifest) {
23583
+ const parent = current.current.manifest;
23584
+ const confirmed = await cloud.chat.state.transition(parent.chatId, parent.epochId, current.current.stateCapability);
23585
+ if (!confirmed?.transitionMessageId)
23586
+ return current;
23587
+ const record = await cloud.chat.messages.read(parent.epochId, confirmed.transitionMessageId, {
23588
+ lane: messageLaneForEpoch(current.current)
23589
+ }).catch(() => null);
23590
+ const action = record ? await decryptMsg(record, "", "", "", { chatId: parent.chatId, epochState: current.current }).catch(() => null) : null;
23591
+ if (!isEpochTransitionMsg(action)) {
23592
+ markDiag(options.diag, "chat.epoch.advance.pending", {
23593
+ stage: record ? "transition-open" : "transition-record",
23594
+ epochVersion: parent.epochVersion
23595
+ });
23336
23596
  const error = new Error("chat epoch transition message is not available yet");
23337
23597
  error.code = "chat/epoch-pending";
23338
23598
  throw error;
@@ -23449,6 +23709,17 @@ async function processInbox(cloud, uid, userChatPK, userPrivKey, options = {}) {
23449
23709
  var CHAT_LIST_DECRYPT_YIELD_EVERY = 2;
23450
23710
  var CHAT_INBOX_RETRY_DELAYS_MS = Object.freeze([1000, 3000, 1e4, 30000]);
23451
23711
  var CHAT_EPOCH_PENDING_MAX_ATTEMPTS = CHAT_INBOX_RETRY_DELAYS_MS.length + 1;
23712
+ function chatOwnerRevision(chat) {
23713
+ return Number.isSafeInteger(chat?.ownEntry?.ownerRevision) ? chat.ownEntry.ownerRevision : 0;
23714
+ }
23715
+ function serverChatCoversOverlay(chat, overlay) {
23716
+ const serverEpoch = Number.isSafeInteger(chat?.epochVersion) ? chat.epochVersion : 0;
23717
+ const overlayEpoch = Number.isSafeInteger(overlay?.epochVersion) ? overlay.epochVersion : 0;
23718
+ if (serverEpoch !== overlayEpoch) {
23719
+ return serverEpoch > overlayEpoch;
23720
+ }
23721
+ return chatOwnerRevision(chat) >= chatOwnerRevision(overlay);
23722
+ }
23452
23723
  function inboxRetryKey(doc) {
23453
23724
  const id = typeof doc?.id === "string" ? doc.id : "";
23454
23725
  const ts = timestampMs(doc?.ts, 0) || 0;
@@ -23521,13 +23792,17 @@ function listenToChats(cloud, uid, userChatPK, userPrivKey, onUpdate, onError, o
23521
23792
  nextAfterChat: null,
23522
23793
  hasMore: false
23523
23794
  };
23524
- const optimisticChats = new Map;
23525
- const mergedChats = (chats) => {
23795
+ const optimisticChats = options.optimisticChats instanceof Map ? options.optimisticChats : new Map;
23796
+ const mergedChats = (chats, mergeOptions = {}) => {
23526
23797
  const nextById = new Map;
23527
23798
  for (const chat of chats || []) {
23528
23799
  const optimistic = optimisticChats.get(chat?.id);
23529
- if (chat?.id)
23800
+ if (chat?.id) {
23530
23801
  nextById.set(chat.id, mergeChatActivity(chat, optimistic, userChatPK));
23802
+ if (mergeOptions.settle === true && optimistic && serverChatCoversOverlay(chat, optimistic)) {
23803
+ optimisticChats.delete(chat.id);
23804
+ }
23805
+ }
23531
23806
  }
23532
23807
  for (const [id, chat] of optimisticChats.entries()) {
23533
23808
  if (!nextById.has(id)) {
@@ -23592,7 +23867,7 @@ function listenToChats(cloud, uid, userChatPK, userPrivKey, onUpdate, onError, o
23592
23867
  if (!closed && runId === run) {
23593
23868
  processedFirst = true;
23594
23869
  const chats = page.chats;
23595
- latestChats = mergedChats(chats);
23870
+ latestChats = mergedChats(chats, { settle: true });
23596
23871
  latestMeta = {
23597
23872
  nextAfterChat: snapshot.nextAfterChat,
23598
23873
  hasMore: snapshot.hasMore
@@ -23961,6 +24236,7 @@ function createChatList({
23961
24236
  localByChatRef,
23962
24237
  pendingDeleteIdsRef,
23963
24238
  deletedChatIdsRef,
24239
+ removedChatVersionsRef,
23964
24240
  keepSelectedDeletedChatIdsRef,
23965
24241
  readCacheRef,
23966
24242
  lastServerChatsRef,
@@ -23978,6 +24254,7 @@ function createChatList({
23978
24254
  onTransition,
23979
24255
  onRemoved,
23980
24256
  wasRemoved,
24257
+ restoreRemovedChat,
23981
24258
  resetExternalChatState,
23982
24259
  mutationGate,
23983
24260
  onMlsKeyPackageConsumed
@@ -24006,6 +24283,8 @@ function createChatList({
24006
24283
  const cachedHistoryEndRef = { current: false };
24007
24284
  const cachedHistoryHeadRef = { current: [] };
24008
24285
  const previewTimerRef = { current: null };
24286
+ const epochReconciliations = new Map;
24287
+ const inboxOverlayChats = new Map;
24009
24288
  let listening = false;
24010
24289
  let unsubscribeChats = null;
24011
24290
  let snapshot = null;
@@ -24014,7 +24293,7 @@ function createChatList({
24014
24293
  let blockedUidSet = new Set(blockedUids);
24015
24294
  let schedulePreviewExpiry = () => {};
24016
24295
  let fillVisibleChatPage = () => {};
24017
- const isHiddenChatId = (chatId) => !!chatId && (pendingDeleteIdsRef.current.has(chatId) || deletedChatIdsRef.current.has(chatId));
24296
+ const isHiddenChatId = (chatId) => !!chatId && (pendingDeleteIdsRef.current.has(chatId) || deletedChatIdsRef.current.has(chatId) || removedChatVersionsRef.current.has(chatId));
24018
24297
  const filterHiddenChats = (nextChats) => (nextChats || []).filter((chatItem) => chatItem?.id && !isHiddenChatId(chatItem.id));
24019
24298
  const nextValue = (current, next) => typeof next === "function" ? next(current) : next;
24020
24299
  const publish = () => {
@@ -24229,7 +24508,13 @@ function createChatList({
24229
24508
  return shownChats;
24230
24509
  };
24231
24510
  const applyInboxChat = (chat) => {
24232
- if (!chat?.id || isHiddenChatId(chat.id)) {
24511
+ if (!chat?.id) {
24512
+ return;
24513
+ }
24514
+ if (removedChatVersionsRef.current.has(chat.id)) {
24515
+ restoreRemovedChat?.(chat);
24516
+ }
24517
+ if (isHiddenChatId(chat.id)) {
24233
24518
  return;
24234
24519
  }
24235
24520
  const updateStartedAt = Date.now();
@@ -24255,13 +24540,113 @@ function createChatList({
24255
24540
  const applyInboxDelete = (chatId) => {
24256
24541
  if (!chatId)
24257
24542
  return;
24543
+ inboxOverlayChats.delete(chatId);
24258
24544
  pendingDeleteIdsRef.current.delete(chatId);
24259
24545
  commitServerChats(lastServerChatsRef.current.filter((chat) => chat?.id !== chatId), {
24260
24546
  warm: false
24261
24547
  });
24262
24548
  setSelectedChat((current) => current === chatId ? null : current);
24263
24549
  };
24550
+ const reconcileChatEpoch = (chatId) => {
24551
+ if (!chatId || !uid || !chatPK || !chatPrivateKey || !chatSigningPK || !chatSigningSecret || !notificationPK) {
24552
+ return Promise.resolve({ advanced: false, chat: null, removed: false });
24553
+ }
24554
+ const active = epochReconciliations.get(chatId);
24555
+ if (active)
24556
+ return active;
24557
+ let task;
24558
+ task = (async () => {
24559
+ const releaseMutation = await mutationGate?.acquireExclusive(chatId) || (() => {});
24560
+ try {
24561
+ const initial = getChatEntry(chatId);
24562
+ if (!initial?.ownEntry)
24563
+ return { advanced: false, chat: initial || null, removed: false };
24564
+ const initialVersion = initial.epochVersion;
24565
+ const identity = {
24566
+ uid,
24567
+ chatPK,
24568
+ chatPrivateKey,
24569
+ chatSigningPK,
24570
+ chatSigningSecret,
24571
+ notificationPK,
24572
+ notificationPrivateKey
24573
+ };
24574
+ const advanceOptions = {
24575
+ blockedUids: blockedUidSet,
24576
+ mls,
24577
+ diag,
24578
+ onMlsKeyPackageConsumed,
24579
+ onRemoved: (removedChatId, epochVersion) => {
24580
+ onRemoved?.(removedChatId, epochVersion);
24581
+ applyInboxDelete(removedChatId);
24582
+ },
24583
+ wasRemoved,
24584
+ onTransition: (chat, message) => {
24585
+ onTransition?.(chat, message);
24586
+ applyInboxChat(chat);
24587
+ }
24588
+ };
24589
+ let working = initial;
24590
+ let result;
24591
+ while (true) {
24592
+ try {
24593
+ result = await advanceEntryToLatestEpoch(cloud, uid, identity, working.ownEntry, advanceOptions);
24594
+ break;
24595
+ } catch (error) {
24596
+ const code = String(error?.code || "").trim().toLowerCase();
24597
+ if (code === "permission-denied" || code.endsWith("/permission-denied")) {
24598
+ result = await retireConfirmedDeletedChat(cloud, uid, identity, working.ownEntry, {
24599
+ diag,
24600
+ onPingDelete: applyInboxDelete
24601
+ });
24602
+ break;
24603
+ }
24604
+ if (code !== "chat/epoch-pending")
24605
+ throw error;
24606
+ const canonical = await getChat(cloud, uid, chatId, chatPK, chatPrivateKey);
24607
+ if (!canonical?.ownEntry || canonical.epochVersion <= working.epochVersion)
24608
+ throw error;
24609
+ markDiag(diag, "chat.epoch.reconcile.rebase", {
24610
+ fromEpochVersion: working.epochVersion,
24611
+ toEpochVersion: canonical.epochVersion
24612
+ });
24613
+ applyInboxChat(canonical);
24614
+ working = canonical;
24615
+ }
24616
+ }
24617
+ if (result?.removed) {
24618
+ markDiag(diag, "chat.epoch.reconcile.done", {
24619
+ fromEpochVersion: initialVersion,
24620
+ removed: true
24621
+ });
24622
+ return { advanced: true, chat: null, removed: true };
24623
+ }
24624
+ const latest = getChatEntry(chatId);
24625
+ const advanced = Number(latest?.epochVersion || 0) > Number(initialVersion || 0);
24626
+ if (advanced) {
24627
+ markDiag(diag, "chat.epoch.reconcile.done", {
24628
+ fromEpochVersion: initialVersion,
24629
+ toEpochVersion: latest.epochVersion,
24630
+ removed: false
24631
+ });
24632
+ }
24633
+ return {
24634
+ advanced,
24635
+ chat: latest || null,
24636
+ removed: false
24637
+ };
24638
+ } finally {
24639
+ releaseMutation();
24640
+ }
24641
+ })().finally(() => {
24642
+ if (epochReconciliations.get(chatId) === task)
24643
+ epochReconciliations.delete(chatId);
24644
+ });
24645
+ epochReconciliations.set(chatId, task);
24646
+ return task;
24647
+ };
24264
24648
  const resetChatList = (ready = false) => {
24649
+ inboxOverlayChats.clear();
24265
24650
  chatSourceReady = ready;
24266
24651
  syncChatDataReady();
24267
24652
  setIsChatListLive(false);
@@ -24385,7 +24770,15 @@ function createChatList({
24385
24770
  };
24386
24771
  const unsubscribe = listenToChats(cloud, uid, chatPK, chatPrivateKey, (nextChats, _nextPeers, meta = {}) => {
24387
24772
  const updateStartedAt = Date.now();
24388
- const rawNextChats = filterHiddenChats(Array.isArray(nextChats) ? nextChats : []);
24773
+ const incomingChats = Array.isArray(nextChats) ? nextChats : [];
24774
+ if (meta?.source === "inbox") {
24775
+ for (const chat of incomingChats) {
24776
+ if (removedChatVersionsRef.current.has(chat?.id)) {
24777
+ restoreRemovedChat?.(chat);
24778
+ }
24779
+ }
24780
+ }
24781
+ const rawNextChats = filterHiddenChats(incomingChats);
24389
24782
  hydrateReadCache(rawNextChats, readCacheRef.current);
24390
24783
  const readAppliedChats = applyReadCache(rawNextChats, chatPK, readCacheRef.current);
24391
24784
  const trimmedChats = trimExpiredChatPreviews(readAppliedChats, { skipChatId: selectedChatIdRef.current });
@@ -24490,6 +24883,7 @@ function createChatList({
24490
24883
  blockedUids: blockedUidSet,
24491
24884
  localCache,
24492
24885
  diag,
24886
+ optimisticChats: inboxOverlayChats,
24493
24887
  getCurrentChat: (chatId) => lastServerChatsRef.current.find((chatItem) => chatItem?.id === chatId) || null,
24494
24888
  mutationGate,
24495
24889
  onMlsKeyPackageConsumed
@@ -24668,12 +25062,16 @@ function createChatList({
24668
25062
  };
24669
25063
  const hasChat = (chatId) => !!chatId && (serverChatIdsSet.has(chatId) || chatLoadsRef.current.has(chatId)) && !isHiddenChatId(chatId);
24670
25064
  const setSources = (nextSources) => {
25065
+ const accountChanged = cloud !== nextSources.cloud || uid !== nextSources.uid || chatPK !== nextSources.chatPK || chatPrivateKey !== nextSources.chatPrivateKey;
24671
25066
  const nextBlocked = sortedUniqueValues(Array.isArray(nextSources.blocked) ? nextSources.blocked : []);
24672
25067
  const nextBlockedUidsKey = nextBlocked.join("|");
24673
25068
  const nextBlockedReady = nextSources.blockedReady === true;
24674
25069
  const blockedFilterChanged = blockedUidsKey !== nextBlockedUidsKey || blockedReady !== nextBlockedReady;
24675
25070
  const listenerChanged = cloud !== nextSources.cloud || uid !== nextSources.uid || chatPK !== nextSources.chatPK || chatSigningPK !== nextSources.chatSigningPK || chatSigningSecret !== nextSources.chatSigningSecret || chatPrivateKey !== nextSources.chatPrivateKey || notificationPK !== nextSources.notificationPK || notificationPrivateKey !== nextSources.notificationPrivateKey || mls !== nextSources.mls || chatBanned !== nextSources.chatBanned || localCache !== nextSources.localCache || isActive !== nextSources.isActive;
24676
25071
  const selectedChatChanged = selectedChatId !== nextSources.selectedChatId;
25072
+ if (accountChanged) {
25073
+ inboxOverlayChats.clear();
25074
+ }
24677
25075
  ({
24678
25076
  cloud,
24679
25077
  uid,
@@ -24695,6 +25093,7 @@ function createChatList({
24695
25093
  localByChatRef,
24696
25094
  pendingDeleteIdsRef,
24697
25095
  deletedChatIdsRef,
25096
+ removedChatVersionsRef,
24698
25097
  keepSelectedDeletedChatIdsRef,
24699
25098
  readCacheRef,
24700
25099
  lastServerChatsRef,
@@ -24712,6 +25111,7 @@ function createChatList({
24712
25111
  onTransition,
24713
25112
  onRemoved,
24714
25113
  wasRemoved,
25114
+ restoreRemovedChat,
24715
25115
  resetExternalChatState,
24716
25116
  mutationGate,
24717
25117
  onMlsKeyPackageConsumed
@@ -24755,6 +25155,8 @@ function createChatList({
24755
25155
  previewTimerRef.current = null;
24756
25156
  }
24757
25157
  flushChatCacheWrite();
25158
+ epochReconciliations.clear();
25159
+ inboxOverlayChats.clear();
24758
25160
  subscribers.clear();
24759
25161
  };
24760
25162
  const subscribe = (subscriber) => {
@@ -24780,6 +25182,7 @@ function createChatList({
24780
25182
  getChatPreviewKey: getChatPreviewKey2,
24781
25183
  getChatRetention,
24782
25184
  getChatEntry,
25185
+ reconcileChatEpoch,
24783
25186
  sendOptionsForPeer,
24784
25187
  hasChat
24785
25188
  };
@@ -24982,7 +25385,6 @@ function createChatSession({
24982
25385
  const closeMessageBatchRef = { current: null };
24983
25386
  const localMessageDiagRef = { current: "" };
24984
25387
  const epochHistoryRef = { current: new Map };
24985
- const epochRecoveryRef = { current: new Map };
24986
25388
  const settingsUpdateStatesRef = { current: new Map };
24987
25389
  const membershipUpdateStatesRef = { current: new Map };
24988
25390
  const directChatOpensRef = { current: new Map };
@@ -25021,6 +25423,7 @@ function createChatSession({
25021
25423
  kickChatMember,
25022
25424
  leaveChat,
25023
25425
  updateChatSettings: updateChatSettings2,
25426
+ updateChatAvatar,
25024
25427
  dropChat,
25025
25428
  dropUnavailableChat,
25026
25429
  deleteChat,
@@ -25479,51 +25882,15 @@ function createChatSession({
25479
25882
  return true;
25480
25883
  };
25481
25884
  const handleBatchMessages = (chatId, messages) => {
25482
- const transition = [...messages || []].reverse().find(isEpochTransitionMsg);
25483
- const nextEpochId = transition?.certificate?.nextEpochId;
25484
- const chat = getChatEntry(chatId);
25485
- if (!nextEpochId || !chat?.ownEntry || chat.epochId === nextEpochId || epochRecoveryRef.current.has(nextEpochId))
25885
+ if (![...messages || []].reverse().some(isEpochTransitionMsg))
25486
25886
  return;
25487
- const recover = (attempt = 0) => {
25488
- const timer = attempt > 0 ? setTimeout(run, [0, 250, 1000, 3000][attempt] || 3000) : null;
25489
- epochRecoveryRef.current.set(nextEpochId, timer || true);
25490
- if (!timer)
25491
- run();
25492
- async function run() {
25493
- let releaseMutation = () => {};
25494
- try {
25495
- releaseMutation = await mutationGate.acquireExclusive(chatId);
25496
- const current = getChatEntry(chatId);
25497
- if (!current?.ownEntry || current.epochId === nextEpochId)
25498
- return;
25499
- const advanced = await processEpochTransitionMessages(cloud, uid, {
25500
- chatPK,
25501
- chatPrivateKey,
25502
- notificationPK
25503
- }, current.ownEntry, messages, {
25504
- blockedUids: new Set(blocked),
25505
- notificationPrivateKey,
25506
- diag,
25507
- onRemoved: (removedChatId) => deleteActions.dropRemovedChat(removedChatId),
25508
- wasRemoved: (removedChatId) => deleteActions.deletedChatIdsRef.current.has(removedChatId),
25509
- onTransition: stageCommittedMembershipMessage
25510
- });
25511
- if (advanced)
25512
- epochHistoryRef.current.delete(chatId);
25513
- else if (attempt < 3)
25514
- return recover(attempt + 1);
25515
- } catch {
25516
- if (attempt < 3)
25517
- return recover(attempt + 1);
25518
- } finally {
25519
- releaseMutation();
25520
- if (epochRecoveryRef.current.get(nextEpochId) === (timer || true)) {
25521
- epochRecoveryRef.current.delete(nextEpochId);
25522
- }
25523
- }
25524
- }
25525
- };
25526
- recover();
25887
+ const startedAt = Date.now();
25888
+ chatListOwner.reconcileChatEpoch(chatId).then((result) => {
25889
+ if (result.advanced)
25890
+ epochHistoryRef.current.delete(chatId);
25891
+ }).catch((error) => {
25892
+ markError(diag, "chat.epoch.reconcile", startedAt, error);
25893
+ });
25527
25894
  };
25528
25895
  const setChats = (...args) => chatListOwner.setChats(...args);
25529
25896
  const setLastChat = (...args) => chatListOwner.setLastChat(...args);
@@ -25541,11 +25908,6 @@ function createChatSession({
25541
25908
  liveActions?.close?.();
25542
25909
  liveActivityByChat = new Map;
25543
25910
  messageBatchOwner.clear();
25544
- for (const pending of epochRecoveryRef.current.values()) {
25545
- if (typeof pending === "number")
25546
- clearTimeout(pending);
25547
- }
25548
- epochRecoveryRef.current.clear();
25549
25911
  epochHistoryRef.current.clear();
25550
25912
  membershipUpdateStatesRef.current.clear();
25551
25913
  directChatOpensRef.current.clear();
@@ -25999,10 +26361,10 @@ function createChatSession({
25999
26361
  await discardOwnerChatMlsSnapshot(cloud, transitionIdentity(), chat.entryId, chat.epochState);
26000
26362
  await cloud.user.chats.unlink(uid, chat.entryId);
26001
26363
  if (retirementStarted) {
26002
- deleteActions.commitRetirement(chatId);
26364
+ deleteActions.commitRemovedRetirement(chatId, chat.epochVersion + 1);
26003
26365
  retirementStarted = false;
26004
26366
  } else {
26005
- deleteActions.dropChat(chatId);
26367
+ deleteActions.dropRemovedChat(chatId, chat.epochVersion + 1);
26006
26368
  }
26007
26369
  return { left: true, message: proposal.message };
26008
26370
  } catch (error) {
@@ -26064,35 +26426,55 @@ function createChatSession({
26064
26426
  clearLocalMembershipMessage(chatId, cid);
26065
26427
  };
26066
26428
  state.promise = (async () => {
26067
- const releaseMutation = await mutationGate.acquireExclusive(chatId);
26068
26429
  try {
26069
- const chat = requireTransitionChat(chatId);
26070
- const members = prepareMembers ? await prepareMembers(chat) : resolveMembers(chat);
26071
- if (!members) {
26072
- restoreAuthoritativeChat();
26073
- return { entry: chat.ownEntry, unchanged: true };
26074
- }
26075
- if (!members.length) {
26076
- restoreAuthoritativeChat();
26077
- await deleteActions.deleteChat(chat);
26078
- return { deleted: true };
26079
- }
26080
- const result = await transitionChatEpoch(cloud, transitionIdentity(), chat.ownEntry, { members, cid, mls });
26081
- if (result.entry) {
26082
- const projected = projectOwnChatEntry(result.entry, chat.entryId, chatPK, chat.ts);
26083
- chatListOwner.commitServerChats(sortedChats([projected], lastServerChatsRef.current));
26084
- } else {
26085
- chatListOwner.commitServerChats(lastServerChatsRef.current.filter((chatItem) => chatItem?.id !== chatId), { warm: false });
26086
- setSelectedChat((current) => current === chatId ? null : current);
26087
- }
26088
- if (optimisticRoster && result.message) {
26089
- stageCommittedMembershipMessage(chat, result.message);
26090
- } else if (optimisticRoster) {
26091
- clearLocalMembershipMessage(chatId, cid);
26430
+ while (true) {
26431
+ const releaseMutation = await mutationGate.acquireExclusive(chatId);
26432
+ let attemptedEpochVersion = 0;
26433
+ try {
26434
+ const chat = requireTransitionChat(chatId);
26435
+ attemptedEpochVersion = chat.epochVersion;
26436
+ const members = prepareMembers ? await prepareMembers(chat) : resolveMembers(chat);
26437
+ if (!members) {
26438
+ restoreAuthoritativeChat();
26439
+ return { entry: chat.ownEntry, unchanged: true };
26440
+ }
26441
+ if (!members.length) {
26442
+ restoreAuthoritativeChat();
26443
+ await deleteActions.deleteChat(chat);
26444
+ return { deleted: true };
26445
+ }
26446
+ const result = await transitionChatEpoch(cloud, transitionIdentity(), chat.ownEntry, { members, cid, mls });
26447
+ if (result.entry) {
26448
+ const projected = projectOwnChatEntry(result.entry, chat.entryId, chatPK, chat.ts);
26449
+ chatListOwner.commitServerChats(sortedChats([projected], lastServerChatsRef.current));
26450
+ } else {
26451
+ chatListOwner.commitServerChats(lastServerChatsRef.current.filter((chatItem) => chatItem?.id !== chatId), { warm: false });
26452
+ setSelectedChat((current) => current === chatId ? null : current);
26453
+ }
26454
+ if (optimisticRoster && result.message) {
26455
+ stageCommittedMembershipMessage(chat, result.message);
26456
+ } else if (optimisticRoster) {
26457
+ clearLocalMembershipMessage(chatId, cid);
26458
+ }
26459
+ if (optimisticRoster)
26460
+ setPendingChatMembers(chatId, null);
26461
+ return result;
26462
+ } catch (error) {
26463
+ releaseMutation();
26464
+ if (!attemptedEpochVersion || !isChatEpochConflict(error))
26465
+ throw error;
26466
+ await chatListOwner.reconcileChatEpoch(chatId);
26467
+ const currentEpochVersion = getChatEntry(chatId)?.epochVersion || 0;
26468
+ if (currentEpochVersion <= attemptedEpochVersion)
26469
+ throw error;
26470
+ markDiag(diag, "chat.membership.epoch.advance", {
26471
+ fromEpochVersion: attemptedEpochVersion,
26472
+ toEpochVersion: currentEpochVersion
26473
+ });
26474
+ } finally {
26475
+ releaseMutation();
26476
+ }
26092
26477
  }
26093
- if (optimisticRoster)
26094
- setPendingChatMembers(chatId, null);
26095
- return result;
26096
26478
  } catch (error) {
26097
26479
  restoreAuthoritativeChat();
26098
26480
  throw error;
@@ -26100,7 +26482,6 @@ function createChatSession({
26100
26482
  if (membershipUpdateStatesRef.current.get(chatId) === state) {
26101
26483
  membershipUpdateStatesRef.current.delete(chatId);
26102
26484
  }
26103
- releaseMutation();
26104
26485
  }
26105
26486
  })();
26106
26487
  membershipUpdateStatesRef.current.set(chatId, state);
@@ -26223,6 +26604,26 @@ function createChatSession({
26223
26604
  }
26224
26605
  return state.promise;
26225
26606
  };
26607
+ const updateChatAvatar = async (chatId, data) => {
26608
+ assertChatAvatarBytes(data);
26609
+ const chat = requireTransitionChat(chatId);
26610
+ if (chat.lineage !== "group")
26611
+ throw new Error("group chat required");
26612
+ const epochId = chat?.epochState?.manifest?.epochId;
26613
+ if (!epochId)
26614
+ throw new Error("current chat epoch unavailable");
26615
+ const uploadChatMedia = cloud?.chat?.media?.upload;
26616
+ if (typeof uploadChatMedia !== "function")
26617
+ throw new Error("chat media upload unavailable");
26618
+ const cid = makeCid();
26619
+ const file = typeof media?.uploadChatAvatar === "function" ? await media.uploadChatAvatar({ cid, data, epochId, uploadChatMedia }) : await putChatFile(epochId, cid, data, {
26620
+ uploadChatMedia,
26621
+ cacheControl: "private, max-age=31536000, immutable, no-transform"
26622
+ });
26623
+ const avatarRef = makeChatAvatarRef(file);
26624
+ await updateChatSettings2(chatId, { avatarRef });
26625
+ return avatarRef;
26626
+ };
26226
26627
  const adoptCommittedSettingsMessage = ({ chatId, message }) => {
26227
26628
  if (!chatId || !message?.cid)
26228
26629
  return;
@@ -26345,7 +26746,13 @@ function createChatSession({
26345
26746
  const ackDeletedChat = (...args) => deleteActions.ackDeletedChat(...args);
26346
26747
  const markChatReadState = (...args) => seenActions.markChatReadState(...args);
26347
26748
  const markChatRead = (...args) => seenActions.markChatRead(...args);
26348
- const enterChatActivity = (...args) => liveActions.enterChatActivity(...args);
26749
+ const enterChatActivity = (chatId) => {
26750
+ const reconcileStartedAt = Date.now();
26751
+ chatListOwner.reconcileChatEpoch(chatId).catch((error) => {
26752
+ markError(diag, "chat.epoch.reconcile", reconcileStartedAt, error);
26753
+ });
26754
+ return liveActions.enterChatActivity(chatId);
26755
+ };
26349
26756
  const leaveChatActivity = (chatId) => {
26350
26757
  const flush = seenActions.flushChatRead(chatId);
26351
26758
  liveActions.leaveChatActivity(chatId);
@@ -26592,6 +26999,7 @@ function createChatSession({
26592
26999
  listActionsRef: deleteListActionsRef,
26593
27000
  closeMessageBatchRef,
26594
27001
  mutationGate,
27002
+ reconcileEpoch: (chatId) => chatListOwner?.reconcileChatEpoch(chatId),
26595
27003
  diag
26596
27004
  });
26597
27005
  const createActionOwners = () => {
@@ -26671,6 +27079,7 @@ function createChatSession({
26671
27079
  adoptLocalMessageMedia,
26672
27080
  readMessageFile,
26673
27081
  mutationGate,
27082
+ reconcileEpoch: (chatId) => chatListOwner.reconcileChatEpoch(chatId),
26674
27083
  diag
26675
27084
  });
26676
27085
  reactionActions = createChatReaction({
@@ -26751,6 +27160,7 @@ function createChatSession({
26751
27160
  localByChatRef,
26752
27161
  pendingDeleteIdsRef: deleteActions.pendingDeleteIdsRef,
26753
27162
  deletedChatIdsRef: deleteActions.deletedChatIdsRef,
27163
+ removedChatVersionsRef: deleteActions.removedChatVersionsRef,
26754
27164
  keepSelectedDeletedChatIdsRef: deleteActions.keepSelectedDeletedChatIdsRef,
26755
27165
  readCacheRef,
26756
27166
  lastServerChatsRef,
@@ -26766,8 +27176,9 @@ function createChatSession({
26766
27176
  reconcileMessageBatches: (...args) => messageBatchOwner.reconcileChats(...args),
26767
27177
  warmChats: (...args) => messageBatchOwner.warm(...args),
26768
27178
  onTransition: stageCommittedMembershipMessage,
26769
- onRemoved: (removedChatId) => deleteActions.dropRemovedChat(removedChatId),
26770
- wasRemoved: (removedChatId) => deleteActions.deletedChatIdsRef.current.has(removedChatId),
27179
+ onRemoved: (removedChatId, epochVersion) => deleteActions.dropRemovedChat(removedChatId, epochVersion),
27180
+ wasRemoved: (removedChatId) => deleteActions.removedChatVersionsRef.current.has(removedChatId),
27181
+ restoreRemovedChat: (chat) => deleteActions.restoreRemovedChat(chat),
26771
27182
  resetExternalChatState,
26772
27183
  mutationGate,
26773
27184
  onMlsKeyPackageConsumed: markMlsKeyPackageConsumed