@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 +2175 -1764
- package/dist/auth.js +13 -5
- package/dist/cli.js +1226 -654
- package/dist/index.js +1209 -653
- package/docs/api.md +3 -3
- package/package.json +1 -1
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
|
|
6267
|
-
var
|
|
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/
|
|
8734
|
-
|
|
8735
|
-
|
|
8736
|
-
|
|
8737
|
-
|
|
8738
|
-
|
|
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
|
|
8749
|
-
return
|
|
8743
|
+
function encodeFileKey(key) {
|
|
8744
|
+
return toHex(toBytes32(key, "file key"));
|
|
8750
8745
|
}
|
|
8751
|
-
function
|
|
8752
|
-
return
|
|
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
|
|
8762
|
-
|
|
8763
|
-
|
|
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
|
|
8753
|
+
return encodeScope(FILE_SCOPE, [scope]);
|
|
8773
8754
|
}
|
|
8774
|
-
|
|
8775
|
-
|
|
8776
|
-
|
|
8777
|
-
|
|
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
|
-
|
|
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
|
|
8786
|
-
return
|
|
8762
|
+
async function importAesKey(key, usages) {
|
|
8763
|
+
return getSubtleCrypto().importKey("raw", toBytes32(key, "file key"), { name: "AES-GCM" }, false, usages);
|
|
8787
8764
|
}
|
|
8788
|
-
function
|
|
8789
|
-
|
|
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
|
|
8792
|
-
const
|
|
8793
|
-
|
|
8794
|
-
|
|
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/
|
|
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
|
|
8810
|
-
|
|
8811
|
-
|
|
8812
|
-
|
|
8813
|
-
|
|
8814
|
-
var
|
|
8815
|
-
var
|
|
8816
|
-
|
|
8817
|
-
|
|
8818
|
-
|
|
8819
|
-
|
|
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
|
|
8817
|
+
return epochId.toLowerCase();
|
|
8822
8818
|
}
|
|
8823
|
-
function
|
|
8824
|
-
const
|
|
8825
|
-
if (!
|
|
8826
|
-
throw new Error("
|
|
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
|
|
8824
|
+
return mediaId.toLowerCase();
|
|
8829
8825
|
}
|
|
8830
|
-
function
|
|
8831
|
-
|
|
8832
|
-
|
|
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
|
|
8831
|
+
return sharedId.toLowerCase();
|
|
8835
8832
|
}
|
|
8836
|
-
function
|
|
8837
|
-
|
|
8838
|
-
throw new Error("chat epoch version required");
|
|
8839
|
-
}
|
|
8840
|
-
return value;
|
|
8833
|
+
function makeChatMediaId() {
|
|
8834
|
+
return toHex(randomBytes3(16));
|
|
8841
8835
|
}
|
|
8842
|
-
function
|
|
8843
|
-
|
|
8844
|
-
|
|
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
|
|
8855
|
-
const
|
|
8856
|
-
|
|
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
|
|
8870
|
-
|
|
8871
|
-
|
|
8872
|
-
|
|
8873
|
-
|
|
8874
|
-
|
|
8875
|
-
|
|
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
|
-
|
|
8878
|
-
|
|
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
|
-
|
|
8881
|
-
|
|
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
|
|
8885
|
-
|
|
8886
|
-
|
|
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
|
-
|
|
8889
|
-
|
|
8879
|
+
return ref;
|
|
8880
|
+
}
|
|
8881
|
+
function uploadByteLength(value) {
|
|
8882
|
+
if (Number.isFinite(value?.byteLength)) {
|
|
8883
|
+
return value.byteLength;
|
|
8890
8884
|
}
|
|
8891
|
-
if (
|
|
8892
|
-
|
|
8885
|
+
if (Number.isFinite(value?.size)) {
|
|
8886
|
+
return value.size;
|
|
8893
8887
|
}
|
|
8894
|
-
|
|
8895
|
-
|
|
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
|
-
|
|
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
|
-
|
|
8933
|
-
|
|
8934
|
-
|
|
8935
|
-
|
|
8936
|
-
|
|
8937
|
-
|
|
8938
|
-
|
|
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
|
|
8941
|
-
const
|
|
8942
|
-
|
|
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
|
|
8945
|
-
|
|
8946
|
-
|
|
8947
|
-
|
|
8948
|
-
|
|
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
|
-
|
|
8951
|
-
|
|
8952
|
-
|
|
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
|
-
|
|
8955
|
-
|
|
8956
|
-
|
|
8957
|
-
|
|
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(
|
|
8967
|
+
cleanBytes(key);
|
|
8960
8968
|
}
|
|
8961
8969
|
}
|
|
8962
|
-
function
|
|
8963
|
-
const
|
|
8964
|
-
|
|
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
|
-
|
|
8967
|
-
|
|
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(
|
|
9001
|
+
cleanBytes(key);
|
|
8970
9002
|
}
|
|
8971
9003
|
}
|
|
8972
9004
|
|
|
8973
|
-
// ../../core/
|
|
8974
|
-
|
|
8975
|
-
|
|
8976
|
-
|
|
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
|
|
8988
|
-
if (
|
|
9010
|
+
function setErrorStage(error, stage, extra = {}) {
|
|
9011
|
+
if (!error || typeof error !== "object") {
|
|
8989
9012
|
return error;
|
|
8990
9013
|
}
|
|
8991
|
-
|
|
8992
|
-
|
|
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
|
-
|
|
9026
|
-
|
|
9027
|
-
|
|
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
|
-
|
|
9039
|
-
|
|
9040
|
-
|
|
9041
|
-
|
|
9024
|
+
throw setErrorStage(error, "encrypt", {
|
|
9025
|
+
...error?.path ? { path: error.path } : {},
|
|
9026
|
+
cid
|
|
9027
|
+
});
|
|
9042
9028
|
}
|
|
9043
9029
|
}
|
|
9044
|
-
function
|
|
9045
|
-
|
|
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
|
-
|
|
9054
|
-
|
|
9055
|
-
|
|
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
|
|
9059
|
-
|
|
9060
|
-
|
|
9061
|
-
|
|
9062
|
-
|
|
9063
|
-
}
|
|
9064
|
-
|
|
9065
|
-
|
|
9066
|
-
|
|
9067
|
-
|
|
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
|
|
9088
|
-
const
|
|
9089
|
-
|
|
9090
|
-
|
|
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
|
-
|
|
9093
|
-
|
|
9094
|
-
let
|
|
9095
|
-
|
|
9096
|
-
|
|
9097
|
-
|
|
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/
|
|
9089
|
+
// ../../core/chat/avatar.js
|
|
9112
9090
|
"use client";
|
|
9113
|
-
var
|
|
9114
|
-
var
|
|
9115
|
-
|
|
9116
|
-
|
|
9117
|
-
|
|
9118
|
-
|
|
9119
|
-
|
|
9120
|
-
|
|
9121
|
-
|
|
9122
|
-
|
|
9123
|
-
|
|
9124
|
-
|
|
9125
|
-
|
|
9126
|
-
|
|
9127
|
-
|
|
9128
|
-
|
|
9129
|
-
|
|
9130
|
-
|
|
9131
|
-
|
|
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
|
|
9134
|
-
|
|
9135
|
-
|
|
9136
|
-
|
|
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
|
-
|
|
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
|
|
9141
|
-
|
|
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
|
-
|
|
9145
|
-
|
|
9146
|
-
|
|
9147
|
-
|
|
9148
|
-
|
|
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
|
-
|
|
9156
|
-
|
|
9145
|
+
|
|
9146
|
+
// ../../core/chat/state.js
|
|
9147
|
+
function makeCid() {
|
|
9148
|
+
return `${Date.now().toString(36)}${toHex(randomBytes3(3))}`;
|
|
9157
9149
|
}
|
|
9158
|
-
function
|
|
9159
|
-
return
|
|
9150
|
+
function getMessageKey(message) {
|
|
9151
|
+
return message?.cid || message?.id || null;
|
|
9160
9152
|
}
|
|
9161
|
-
function
|
|
9162
|
-
|
|
9163
|
-
|
|
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
|
-
|
|
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
|
|
9170
|
-
|
|
9171
|
-
|
|
9172
|
-
|
|
9173
|
-
|
|
9174
|
-
|
|
9175
|
-
|
|
9176
|
-
|
|
9177
|
-
|
|
9178
|
-
|
|
9179
|
-
|
|
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
|
-
|
|
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
|
-
|
|
9198
|
-
|
|
9199
|
-
|
|
9200
|
-
|
|
9201
|
-
|
|
9202
|
-
|
|
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
|
-
|
|
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
|
|
9208
|
-
|
|
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
|
|
9215
|
-
return
|
|
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
|
|
9227
|
-
|
|
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
|
-
|
|
9230
|
-
|
|
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
|
|
9233
|
-
|
|
9234
|
-
|
|
9235
|
-
|
|
9236
|
-
|
|
9237
|
-
|
|
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
|
|
9249
|
-
if (
|
|
9250
|
-
throw new Error("chat
|
|
9243
|
+
function cleanCreatedAt(value) {
|
|
9244
|
+
if (!Number.isSafeInteger(value) || value <= 0) {
|
|
9245
|
+
throw new Error("chat epoch createdAt required");
|
|
9251
9246
|
}
|
|
9252
|
-
return
|
|
9247
|
+
return value;
|
|
9253
9248
|
}
|
|
9254
|
-
function
|
|
9255
|
-
if (
|
|
9256
|
-
|
|
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
|
|
9253
|
+
return value;
|
|
9261
9254
|
}
|
|
9262
|
-
function
|
|
9263
|
-
if (!
|
|
9264
|
-
throw new Error("chat
|
|
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
|
-
|
|
9269
|
-
|
|
9270
|
-
|
|
9271
|
-
|
|
9272
|
-
|
|
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
|
|
9278
|
-
const
|
|
9279
|
-
const
|
|
9280
|
-
const
|
|
9281
|
-
|
|
9282
|
-
|
|
9283
|
-
|
|
9284
|
-
|
|
9285
|
-
|
|
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
|
|
9297
|
-
const
|
|
9298
|
-
if (
|
|
9299
|
-
throw new Error("
|
|
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 (
|
|
9302
|
-
|
|
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
|
|
9313
|
-
if (!
|
|
9314
|
-
throw new Error("
|
|
9297
|
+
function normalizeEpochManifest(value, options = {}) {
|
|
9298
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
9299
|
+
throw new Error("chat manifest required");
|
|
9315
9300
|
}
|
|
9316
|
-
|
|
9317
|
-
|
|
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
|
-
|
|
9321
|
-
|
|
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
|
-
|
|
9326
|
-
|
|
9327
|
-
|
|
9328
|
-
|
|
9329
|
-
|
|
9330
|
-
|
|
9331
|
-
|
|
9332
|
-
|
|
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
|
-
|
|
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
|
-
|
|
9341
|
-
|
|
9342
|
-
|
|
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
|
-
|
|
9331
|
+
assertLineage(manifest, options.parent || null);
|
|
9332
|
+
return manifest;
|
|
9348
9333
|
}
|
|
9349
|
-
|
|
9350
|
-
|
|
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
|
-
|
|
9360
|
-
|
|
9361
|
-
|
|
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
|
-
|
|
9376
|
-
|
|
9377
|
-
|
|
9378
|
-
|
|
9379
|
-
|
|
9380
|
-
|
|
9381
|
-
|
|
9382
|
-
|
|
9383
|
-
|
|
9384
|
-
|
|
9385
|
-
|
|
9386
|
-
|
|
9387
|
-
|
|
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
|
-
|
|
9390
|
-
|
|
9391
|
-
|
|
9392
|
-
|
|
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(
|
|
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
|
-
|
|
9422
|
-
const
|
|
9423
|
-
|
|
9424
|
-
|
|
9425
|
-
|
|
9426
|
-
|
|
9427
|
-
}
|
|
9428
|
-
|
|
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/
|
|
9440
|
-
|
|
9441
|
-
|
|
9442
|
-
|
|
9443
|
-
|
|
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
|
|
9453
|
-
const
|
|
9454
|
-
|
|
9455
|
-
|
|
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
|
-
|
|
9465
|
-
|
|
9466
|
-
|
|
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
|
|
9475
|
-
|
|
9476
|
-
|
|
9477
|
-
|
|
9478
|
-
if (
|
|
9479
|
-
|
|
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
|
|
9482
|
-
|
|
9483
|
-
|
|
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
|
-
|
|
9501
|
-
|
|
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
|
|
9504
|
-
return
|
|
9423
|
+
function hashText(value) {
|
|
9424
|
+
return toHex(sha256(encoder.encode(value)));
|
|
9505
9425
|
}
|
|
9506
|
-
function
|
|
9507
|
-
|
|
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
|
|
9534
|
-
|
|
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
|
|
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
|
-
|
|
9547
|
-
|
|
9548
|
-
|
|
9549
|
-
|
|
9550
|
-
|
|
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
|
|
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
|
-
|
|
9556
|
-
|
|
9557
|
-
|
|
9558
|
-
} catch {
|
|
9559
|
-
return false;
|
|
9471
|
+
return toHex(value);
|
|
9472
|
+
} finally {
|
|
9473
|
+
cleanBytes(value);
|
|
9560
9474
|
}
|
|
9561
9475
|
}
|
|
9562
|
-
function
|
|
9563
|
-
return
|
|
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/
|
|
9483
|
+
// ../../core/crypto/base64.js
|
|
9567
9484
|
"use client";
|
|
9568
|
-
var
|
|
9569
|
-
var
|
|
9570
|
-
|
|
9571
|
-
|
|
9572
|
-
|
|
9573
|
-
|
|
9574
|
-
|
|
9575
|
-
|
|
9576
|
-
|
|
9577
|
-
|
|
9578
|
-
|
|
9579
|
-
|
|
9580
|
-
|
|
9581
|
-
|
|
9582
|
-
|
|
9583
|
-
|
|
9584
|
-
|
|
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
|
|
9597
|
-
}
|
|
9598
|
-
function retentionPatch(message) {
|
|
9599
|
-
return hasChatRetention(message?.retention) ? { retention: cleanChatRetention(message.retention) } : {};
|
|
9503
|
+
return output;
|
|
9600
9504
|
}
|
|
9601
|
-
function
|
|
9602
|
-
|
|
9603
|
-
|
|
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
|
|
9606
|
-
|
|
9607
|
-
|
|
9608
|
-
|
|
9609
|
-
|
|
9610
|
-
|
|
9611
|
-
|
|
9612
|
-
|
|
9613
|
-
|
|
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
|
-
|
|
9616
|
-
|
|
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
|
|
9619
|
-
|
|
9620
|
-
|
|
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
|
|
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
|
|
9562
|
+
// ../../core/crypto/chat.js
|
|
9629
9563
|
"use client";
|
|
9630
|
-
var
|
|
9631
|
-
var
|
|
9632
|
-
|
|
9633
|
-
|
|
9634
|
-
|
|
9635
|
-
|
|
9636
|
-
|
|
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
|
|
9639
|
-
return
|
|
9573
|
+
function hasMsgBody(data) {
|
|
9574
|
+
return data?.body != null;
|
|
9640
9575
|
}
|
|
9641
|
-
function
|
|
9642
|
-
|
|
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
|
|
9649
|
-
|
|
9650
|
-
|
|
9651
|
-
if (!
|
|
9652
|
-
throw new Error("
|
|
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
|
-
|
|
9655
|
-
|
|
9656
|
-
|
|
9657
|
-
|
|
9658
|
-
|
|
9659
|
-
|
|
9660
|
-
|
|
9661
|
-
|
|
9662
|
-
|
|
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
|
|
9667
|
-
|
|
9615
|
+
function closeChatEpoch(epoch) {
|
|
9616
|
+
closeEpochKeys(epoch);
|
|
9668
9617
|
}
|
|
9669
|
-
function
|
|
9670
|
-
|
|
9671
|
-
|
|
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
|
|
9684
|
-
const
|
|
9685
|
-
|
|
9686
|
-
|
|
9687
|
-
|
|
9688
|
-
|
|
9689
|
-
|
|
9690
|
-
|
|
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:
|
|
9693
|
-
epochId:
|
|
9694
|
-
|
|
9695
|
-
|
|
9696
|
-
|
|
9697
|
-
|
|
9698
|
-
|
|
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
|
|
9702
|
-
|
|
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
|
|
9724
|
-
return
|
|
9647
|
+
function batchRecordId(record) {
|
|
9648
|
+
return cleanText(record?.id) || cleanText(record?.recordId) || cleanText(record?.head?.cid);
|
|
9725
9649
|
}
|
|
9726
|
-
function
|
|
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
|
-
|
|
9732
|
-
|
|
9733
|
-
|
|
9734
|
-
|
|
9735
|
-
|
|
9736
|
-
|
|
9737
|
-
|
|
9738
|
-
|
|
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
|
|
9742
|
-
|
|
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
|
|
9745
|
-
|
|
9746
|
-
|
|
9747
|
-
|
|
9748
|
-
|
|
9749
|
-
|
|
9750
|
-
|
|
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
|
-
|
|
9757
|
-
|
|
9758
|
-
|
|
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
|
-
|
|
9776
|
-
|
|
9777
|
-
|
|
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
|
-
|
|
9780
|
-
|
|
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
|
|
9784
|
-
|
|
9785
|
-
|
|
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
|
-
|
|
9788
|
-
|
|
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
|
|
9804
|
-
|
|
9805
|
-
|
|
9806
|
-
|
|
9807
|
-
|
|
9808
|
-
|
|
9809
|
-
|
|
9810
|
-
|
|
9811
|
-
|
|
9812
|
-
|
|
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
|
-
|
|
9816
|
-
|
|
9817
|
-
|
|
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
|
|
9821
|
-
const
|
|
9822
|
-
|
|
9823
|
-
|
|
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
|
|
9840
|
-
const
|
|
9841
|
-
|
|
9842
|
-
|
|
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
|
|
9867
|
-
|
|
9868
|
-
|
|
9869
|
-
|
|
9870
|
-
|
|
9871
|
-
|
|
9872
|
-
|
|
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
|
|
9882
|
-
|
|
9883
|
-
|
|
9884
|
-
|
|
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
|
-
|
|
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
|
-
|
|
9790
|
+
cleanBytes(plaintext);
|
|
9890
9791
|
}
|
|
9891
9792
|
}
|
|
9892
|
-
|
|
9893
|
-
|
|
9894
|
-
"
|
|
9895
|
-
|
|
9896
|
-
|
|
9897
|
-
|
|
9898
|
-
const
|
|
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
|
-
|
|
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(
|
|
9824
|
+
cleanBytes(plaintext);
|
|
9903
9825
|
}
|
|
9904
9826
|
}
|
|
9905
|
-
function
|
|
9906
|
-
|
|
9907
|
-
|
|
9908
|
-
|
|
9909
|
-
|
|
9910
|
-
}
|
|
9911
|
-
|
|
9912
|
-
|
|
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
|
|
9837
|
+
return opened;
|
|
9931
9838
|
}
|
|
9932
|
-
function
|
|
9933
|
-
|
|
9934
|
-
|
|
9935
|
-
|
|
9936
|
-
|
|
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
|
|
9939
|
-
|
|
9940
|
-
const
|
|
9941
|
-
if (!
|
|
9942
|
-
throw new Error("invalid chat
|
|
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
|
-
|
|
9852
|
+
return opened;
|
|
9952
9853
|
}
|
|
9953
|
-
return
|
|
9854
|
+
return openMessageBatchV3Default(epoch, source, options);
|
|
9954
9855
|
}
|
|
9955
|
-
|
|
9956
|
-
|
|
9957
|
-
|
|
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
|
-
|
|
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
|
|
9971
|
-
if (!value || typeof value !== "object" || Array.isArray(value)
|
|
9972
|
-
throw new Error("
|
|
9867
|
+
function normalizeTransitionCore(value) {
|
|
9868
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
9869
|
+
throw new Error("chat transition required");
|
|
9973
9870
|
}
|
|
9974
|
-
|
|
9975
|
-
|
|
9976
|
-
|
|
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:
|
|
9880
|
+
v: CHAT_TRANSITION_VERSION,
|
|
9980
9881
|
protocol: CHAT_PROTOCOL_VERSION,
|
|
9981
|
-
|
|
9982
|
-
|
|
9983
|
-
|
|
9984
|
-
|
|
9985
|
-
|
|
9986
|
-
|
|
9987
|
-
|
|
9988
|
-
|
|
9989
|
-
|
|
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
|
-
|
|
9994
|
-
|
|
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
|
|
10012
|
-
|
|
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
|
|
10035
|
-
|
|
10036
|
-
|
|
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
|
|
10039
|
-
|
|
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
|
-
|
|
10057
|
-
|
|
10058
|
-
|
|
10059
|
-
|
|
10060
|
-
|
|
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
|
-
|
|
10064
|
-
const
|
|
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
|
|
10067
|
-
|
|
10068
|
-
|
|
10069
|
-
|
|
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
|
-
|
|
10073
|
-
const key = ownerEpochKey(chatPrivateKey, entryId, epochEntryId);
|
|
9946
|
+
function verifyTransitionSignature(certificate, signingPublicKey) {
|
|
10074
9947
|
try {
|
|
10075
|
-
const
|
|
10076
|
-
|
|
10077
|
-
|
|
10078
|
-
|
|
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
|
|
10082
|
-
return
|
|
9955
|
+
function transitionCore(certificate) {
|
|
9956
|
+
return normalizeTransitionCore(certificate);
|
|
10083
9957
|
}
|
|
10084
9958
|
|
|
10085
|
-
// ../../core/chat/
|
|
9959
|
+
// ../../core/chat/ttl.js
|
|
10086
9960
|
"use client";
|
|
10087
|
-
var
|
|
10088
|
-
|
|
10089
|
-
|
|
10090
|
-
|
|
10091
|
-
|
|
10092
|
-
|
|
10093
|
-
|
|
10094
|
-
|
|
10095
|
-
|
|
10096
|
-
|
|
10097
|
-
|
|
10098
|
-
|
|
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
|
|
10101
|
-
|
|
10102
|
-
|
|
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
|
-
|
|
10115
|
-
|
|
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
|
|
10132
|
-
|
|
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
|
-
|
|
10135
|
-
|
|
10136
|
-
|
|
10137
|
-
|
|
10138
|
-
|
|
10139
|
-
|
|
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
|
-
|
|
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/
|
|
10021
|
+
// ../../core/chat/settings.js
|
|
10154
10022
|
"use client";
|
|
10155
|
-
|
|
10156
|
-
|
|
10157
|
-
|
|
10158
|
-
|
|
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
|
-
|
|
10161
|
-
|
|
10162
|
-
|
|
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
|
-
|
|
10166
|
-
|
|
10167
|
-
|
|
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
|
-
|
|
10180
|
-
|
|
10181
|
-
|
|
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
|
-
|
|
10186
|
-
|
|
10187
|
-
|
|
10188
|
-
|
|
10189
|
-
|
|
10190
|
-
|
|
10191
|
-
|
|
10192
|
-
|
|
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
|
-
|
|
10203
|
-
|
|
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
|
-
|
|
10213
|
-
|
|
10214
|
-
|
|
10215
|
-
|
|
10216
|
-
|
|
10217
|
-
|
|
10218
|
-
|
|
10219
|
-
|
|
10220
|
-
|
|
10221
|
-
|
|
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
|
-
|
|
10225
|
-
|
|
10226
|
-
|
|
10227
|
-
|
|
10228
|
-
|
|
10229
|
-
|
|
10230
|
-
|
|
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
|
|
10233
|
-
|
|
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
|
|
10236
|
-
return
|
|
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
|
|
10239
|
-
if (!
|
|
10240
|
-
throw new Error("
|
|
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
|
|
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
|
|
10245
|
-
const
|
|
10246
|
-
|
|
10247
|
-
|
|
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
|
|
10252
|
-
|
|
10253
|
-
|
|
10254
|
-
|
|
10255
|
-
|
|
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
|
|
10258
|
-
|
|
10259
|
-
|
|
10260
|
-
|
|
10261
|
-
|
|
10262
|
-
|
|
10263
|
-
|
|
10264
|
-
}
|
|
10265
|
-
return
|
|
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
|
-
|
|
10256
|
+
closeChatEpoch(epoch);
|
|
10268
10257
|
}
|
|
10269
10258
|
}
|
|
10270
|
-
async function
|
|
10271
|
-
const
|
|
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
|
|
10274
|
-
|
|
10275
|
-
|
|
10276
|
-
|
|
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
|
-
|
|
10271
|
+
closeChatEpoch(epoch);
|
|
10284
10272
|
}
|
|
10285
10273
|
}
|
|
10286
|
-
|
|
10287
|
-
|
|
10288
|
-
|
|
10289
|
-
|
|
10290
|
-
|
|
10291
|
-
|
|
10292
|
-
|
|
10293
|
-
|
|
10294
|
-
|
|
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
|
-
|
|
10309
|
-
|
|
10310
|
-
|
|
10311
|
-
|
|
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
|
|
10316
|
-
|
|
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
|
|
10323
|
-
return
|
|
10301
|
+
function entryAad(entryId) {
|
|
10302
|
+
return canonicalBytes({ v: CHAT_ENTRY_VERSION, protocol: CHAT_PROTOCOL_VERSION, entryId }, "chat entry aad");
|
|
10324
10303
|
}
|
|
10325
|
-
function
|
|
10326
|
-
|
|
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
|
|
10331
|
-
|
|
10332
|
-
|
|
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
|
|
10335
|
-
return
|
|
10315
|
+
function cleanTransitionCommitment(value) {
|
|
10316
|
+
return cleanChatHex(value, "transition commitment");
|
|
10336
10317
|
}
|
|
10337
|
-
function
|
|
10338
|
-
const
|
|
10339
|
-
|
|
10340
|
-
|
|
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
|
-
|
|
10323
|
+
return messageId;
|
|
10355
10324
|
}
|
|
10356
|
-
function
|
|
10357
|
-
|
|
10358
|
-
|
|
10359
|
-
|
|
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
|
-
|
|
10362
|
-
|
|
10363
|
-
|
|
10364
|
-
|
|
10365
|
-
|
|
10366
|
-
|
|
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
|
|
10346
|
+
return routes;
|
|
10369
10347
|
}
|
|
10370
|
-
function
|
|
10371
|
-
if (
|
|
10372
|
-
|
|
10348
|
+
function normalizeCurrentEpoch(value) {
|
|
10349
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
10350
|
+
throw new Error("current chat epoch required");
|
|
10373
10351
|
}
|
|
10374
|
-
|
|
10375
|
-
|
|
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
|
-
|
|
10378
|
-
|
|
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
|
|
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
|
|
10383
|
-
const
|
|
10384
|
-
|
|
10385
|
-
|
|
10386
|
-
|
|
10387
|
-
|
|
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
|
|
10392
|
-
const
|
|
10393
|
-
|
|
10394
|
-
|
|
10395
|
-
|
|
10396
|
-
|
|
10397
|
-
|
|
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
|
-
|
|
10402
|
-
|
|
10403
|
-
|
|
10404
|
-
|
|
10405
|
-
|
|
10406
|
-
|
|
10407
|
-
|
|
10408
|
-
|
|
10409
|
-
|
|
10410
|
-
|
|
10411
|
-
|
|
10412
|
-
|
|
10413
|
-
|
|
10414
|
-
|
|
10415
|
-
|
|
10416
|
-
}
|
|
10417
|
-
|
|
10418
|
-
|
|
10419
|
-
|
|
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
|
-
|
|
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
|
-
|
|
10424
|
-
|
|
10425
|
-
|
|
10426
|
-
|
|
10427
|
-
|
|
10428
|
-
|
|
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
|
|
10431
|
-
|
|
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
|
|
10460
|
-
const
|
|
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
|
|
10466
|
-
|
|
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/
|
|
10478
|
+
// ../../core/chat/epochs/bootstrap.js
|
|
10495
10479
|
"use client";
|
|
10496
|
-
|
|
10497
|
-
|
|
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
|
|
10500
|
-
|
|
10501
|
-
|
|
10502
|
-
|
|
10503
|
-
|
|
10504
|
-
|
|
10505
|
-
|
|
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
|
-
|
|
10508
|
-
|
|
10509
|
-
|
|
10510
|
-
|
|
10511
|
-
|
|
10512
|
-
|
|
10513
|
-
|
|
10514
|
-
|
|
10515
|
-
|
|
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
|
-
|
|
10520
|
-
|
|
10521
|
-
|
|
10522
|
-
|
|
10523
|
-
|
|
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
|
-
|
|
10532
|
-
|
|
10533
|
-
|
|
10534
|
-
|
|
10535
|
-
|
|
10536
|
-
|
|
10537
|
-
|
|
10538
|
-
|
|
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
|
|
10544
|
-
|
|
10545
|
-
|
|
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
|
-
|
|
10556
|
-
|
|
10557
|
-
|
|
10558
|
-
|
|
10559
|
-
|
|
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
|
-
|
|
10562
|
-
|
|
10563
|
-
|
|
10564
|
-
|
|
10565
|
-
throw
|
|
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
|
-
|
|
10568
|
-
|
|
10569
|
-
|
|
10570
|
-
|
|
10571
|
-
|
|
10572
|
-
|
|
10573
|
-
|
|
10574
|
-
|
|
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 >
|
|
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 :
|
|
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 || !!
|
|
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
|
|
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
|
|
12281
|
+
function chatPreviewCountsAsUnseen(preview, chatPK) {
|
|
12246
12282
|
if (!preview || fromSelf(preview, chatPK)) {
|
|
12247
12283
|
return false;
|
|
12248
12284
|
}
|
|
12249
12285
|
if (isReactionMsg(preview)) {
|
|
12250
|
-
|
|
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 || !
|
|
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
|
-
|
|
16301
|
-
|
|
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
|
|
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
|
|
16393
|
-
|
|
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
|
-
|
|
16399
|
-
|
|
16400
|
-
|
|
16401
|
-
|
|
16402
|
-
|
|
16403
|
-
|
|
16404
|
-
|
|
16405
|
-
|
|
16406
|
-
|
|
16407
|
-
|
|
16408
|
-
|
|
16409
|
-
|
|
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
|
-
|
|
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
|
-
|
|
18531
|
-
|
|
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
|
-
|
|
18534
|
-
|
|
18535
|
-
|
|
18536
|
-
|
|
18537
|
-
|
|
18538
|
-
|
|
18539
|
-
|
|
18540
|
-
|
|
18541
|
-
|
|
18542
|
-
|
|
18543
|
-
|
|
18544
|
-
|
|
18545
|
-
|
|
18546
|
-
|
|
18547
|
-
|
|
18548
|
-
|
|
18549
|
-
|
|
18550
|
-
|
|
18551
|
-
|
|
18552
|
-
|
|
18553
|
-
|
|
18554
|
-
|
|
18555
|
-
|
|
18556
|
-
|
|
18557
|
-
|
|
18558
|
-
|
|
18559
|
-
|
|
18560
|
-
|
|
18561
|
-
|
|
18562
|
-
|
|
18563
|
-
|
|
18564
|
-
|
|
18565
|
-
|
|
18566
|
-
|
|
18567
|
-
|
|
18568
|
-
|
|
18569
|
-
|
|
18570
|
-
|
|
18571
|
-
|
|
18572
|
-
|
|
18573
|
-
|
|
18574
|
-
|
|
18575
|
-
|
|
18576
|
-
|
|
18577
|
-
|
|
18578
|
-
|
|
18579
|
-
|
|
18580
|
-
|
|
18581
|
-
|
|
18582
|
-
|
|
18583
|
-
|
|
18584
|
-
|
|
18585
|
-
|
|
18586
|
-
|
|
18587
|
-
|
|
18588
|
-
|
|
18589
|
-
|
|
18590
|
-
|
|
18591
|
-
|
|
18592
|
-
|
|
18593
|
-
|
|
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
|
-
|
|
18598
|
-
|
|
18599
|
-
}
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
|
25488
|
-
|
|
25489
|
-
|
|
25490
|
-
|
|
25491
|
-
|
|
25492
|
-
|
|
25493
|
-
|
|
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.
|
|
26364
|
+
deleteActions.commitRemovedRetirement(chatId, chat.epochVersion + 1);
|
|
26003
26365
|
retirementStarted = false;
|
|
26004
26366
|
} else {
|
|
26005
|
-
deleteActions.
|
|
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
|
-
|
|
26070
|
-
|
|
26071
|
-
|
|
26072
|
-
|
|
26073
|
-
|
|
26074
|
-
|
|
26075
|
-
|
|
26076
|
-
|
|
26077
|
-
|
|
26078
|
-
|
|
26079
|
-
|
|
26080
|
-
|
|
26081
|
-
|
|
26082
|
-
|
|
26083
|
-
|
|
26084
|
-
|
|
26085
|
-
|
|
26086
|
-
|
|
26087
|
-
|
|
26088
|
-
|
|
26089
|
-
|
|
26090
|
-
|
|
26091
|
-
|
|
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 = (
|
|
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.
|
|
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
|