@agentunion/fastaun-browser 0.3.2 → 0.3.3
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/CHANGELOG.md +19 -0
- package/_packed_docs/CHANGELOG.md +19 -0
- package/_packed_docs/sdk/04-/350/277/236/346/216/245/344/270/216/350/256/244/350/257/201.md +48 -15
- package/_packed_docs/sdk/06-API/346/211/213/345/206/214.md +182 -28
- package/_packed_docs/sdk/AUN_DOCS_GUIDE.md +7 -5
- package/_packed_docs/sdk/INDEX.md +17 -12
- package/dist/auth.d.ts.map +1 -1
- package/dist/auth.js +1 -4
- package/dist/auth.js.map +1 -1
- package/dist/bundle.js +2093 -602
- package/dist/client.d.ts +64 -7
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js +1441 -476
- package/dist/client.js.map +1 -1
- package/dist/crypto.d.ts.map +1 -1
- package/dist/crypto.js +45 -31
- package/dist/crypto.js.map +1 -1
- package/dist/discovery.d.ts +4 -0
- package/dist/discovery.d.ts.map +1 -1
- package/dist/discovery.js +16 -11
- package/dist/discovery.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/keystore/index.d.ts +22 -0
- package/dist/keystore/index.d.ts.map +1 -1
- package/dist/keystore/indexeddb.d.ts +4 -1
- package/dist/keystore/indexeddb.d.ts.map +1 -1
- package/dist/keystore/indexeddb.js +104 -1
- package/dist/keystore/indexeddb.js.map +1 -1
- package/dist/logger.d.ts +5 -1
- package/dist/logger.d.ts.map +1 -1
- package/dist/logger.js +8 -2
- package/dist/logger.js.map +1 -1
- package/dist/namespaces/auth.d.ts +1 -0
- package/dist/namespaces/auth.d.ts.map +1 -1
- package/dist/namespaces/auth.js +38 -0
- package/dist/namespaces/auth.js.map +1 -1
- package/dist/seq-tracker.d.ts +5 -3
- package/dist/seq-tracker.d.ts.map +1 -1
- package/dist/seq-tracker.js +30 -3
- package/dist/seq-tracker.js.map +1 -1
- package/dist/transport.d.ts.map +1 -1
- package/dist/transport.js +18 -0
- package/dist/transport.js.map +1 -1
- package/dist/v2/crypto/canonical.d.ts +1 -1
- package/dist/v2/crypto/canonical.d.ts.map +1 -1
- package/dist/v2/crypto/canonical.js +42 -13
- package/dist/v2/crypto/canonical.js.map +1 -1
- package/dist/v2/crypto/ecdh.d.ts.map +1 -1
- package/dist/v2/crypto/ecdh.js +18 -1
- package/dist/v2/crypto/ecdh.js.map +1 -1
- package/dist/v2/e2ee/decrypt.d.ts.map +1 -1
- package/dist/v2/e2ee/decrypt.js +56 -2
- package/dist/v2/e2ee/decrypt.js.map +1 -1
- package/dist/v2/e2ee/encrypt-group.d.ts.map +1 -1
- package/dist/v2/e2ee/encrypt-group.js +16 -6
- package/dist/v2/e2ee/encrypt-group.js.map +1 -1
- package/dist/v2/e2ee/encrypt-p2p.d.ts.map +1 -1
- package/dist/v2/e2ee/encrypt-p2p.js +39 -11
- package/dist/v2/e2ee/encrypt-p2p.js.map +1 -1
- package/dist/v2/e2ee/metadata-auth.d.ts +1 -0
- package/dist/v2/e2ee/metadata-auth.d.ts.map +1 -1
- package/dist/v2/e2ee/metadata-auth.js +51 -0
- package/dist/v2/e2ee/metadata-auth.js.map +1 -1
- package/dist/v2/e2ee/types.d.ts +2 -2
- package/dist/v2/e2ee/types.d.ts.map +1 -1
- package/dist/v2/session/keystore.d.ts +12 -4
- package/dist/v2/session/keystore.d.ts.map +1 -1
- package/dist/v2/session/keystore.js +177 -35
- package/dist/v2/session/keystore.js.map +1 -1
- package/dist/v2/session/session.d.ts +10 -3
- package/dist/v2/session/session.d.ts.map +1 -1
- package/dist/v2/session/session.js +91 -17
- package/dist/v2/session/session.js.map +1 -1
- package/dist/v2/state/commitment.d.ts.map +1 -1
- package/dist/v2/state/commitment.js +4 -1
- package/dist/v2/state/commitment.js.map +1 -1
- package/package.json +1 -1
package/dist/bundle.js
CHANGED
|
@@ -109,41 +109,55 @@ function parseDerLength(data, offset) {
|
|
|
109
109
|
}
|
|
110
110
|
return { value, lenBytes: 1 + numBytes };
|
|
111
111
|
}
|
|
112
|
+
function readDerTlvRange(data, offset, expectedTag) {
|
|
113
|
+
if (offset >= data.length) return null;
|
|
114
|
+
const tag = data[offset];
|
|
115
|
+
if (expectedTag !== void 0 && tag !== expectedTag) return null;
|
|
116
|
+
const len = parseDerLength(data, offset + 1);
|
|
117
|
+
if (!len) return null;
|
|
118
|
+
const valueStart = offset + 1 + len.lenBytes;
|
|
119
|
+
const valueEnd = valueStart + len.value;
|
|
120
|
+
if (valueStart > data.length || valueEnd > data.length) return null;
|
|
121
|
+
return { fullStart: offset, valueStart, valueEnd, fullEnd: valueEnd, tag };
|
|
122
|
+
}
|
|
123
|
+
function skipDerTlv(data, offset, expectedTag) {
|
|
124
|
+
const tlv = readDerTlvRange(data, offset, expectedTag);
|
|
125
|
+
return tlv?.fullEnd ?? null;
|
|
126
|
+
}
|
|
112
127
|
function extractSpkiFromCertPem(certPem) {
|
|
113
128
|
const certDer = new Uint8Array(pemToArrayBuffer(certPem));
|
|
114
|
-
const
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
129
|
+
const cert = readDerTlvRange(certDer, 0, 48);
|
|
130
|
+
if (!cert) {
|
|
131
|
+
throw new Error("unable to extract SPKI public key from certificate");
|
|
132
|
+
}
|
|
133
|
+
const tbs = readDerTlvRange(certDer, cert.valueStart, 48);
|
|
134
|
+
if (!tbs) {
|
|
135
|
+
throw new Error("unable to extract SPKI public key from certificate");
|
|
136
|
+
}
|
|
137
|
+
let pos = tbs.valueStart;
|
|
138
|
+
if (certDer[pos] === 160) {
|
|
139
|
+
const next = skipDerTlv(certDer, pos, 160);
|
|
140
|
+
if (next === null || next > tbs.valueEnd) {
|
|
141
|
+
throw new Error("unable to extract SPKI public key from certificate");
|
|
122
142
|
}
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
if (totalLen < 50 || totalLen > 140) continue;
|
|
130
|
-
const spkiCandidate = certDer.slice(seqStart, seqStart + totalLen);
|
|
131
|
-
let hasBitString = false;
|
|
132
|
-
for (let k = 20; k < spkiCandidate.length - 10; k++) {
|
|
133
|
-
if (spkiCandidate[k] === 3 && spkiCandidate[k + 2] === 0) {
|
|
134
|
-
hasBitString = true;
|
|
135
|
-
break;
|
|
136
|
-
}
|
|
137
|
-
}
|
|
138
|
-
if (hasBitString) {
|
|
139
|
-
return spkiCandidate.buffer.slice(
|
|
140
|
-
spkiCandidate.byteOffset,
|
|
141
|
-
spkiCandidate.byteOffset + spkiCandidate.byteLength
|
|
142
|
-
);
|
|
143
|
-
}
|
|
143
|
+
pos = next;
|
|
144
|
+
}
|
|
145
|
+
for (const tag of [2, 48, 48, 48, 48]) {
|
|
146
|
+
const next = skipDerTlv(certDer, pos, tag);
|
|
147
|
+
if (next === null || next > tbs.valueEnd) {
|
|
148
|
+
throw new Error("unable to extract SPKI public key from certificate");
|
|
144
149
|
}
|
|
150
|
+
pos = next;
|
|
151
|
+
}
|
|
152
|
+
const spki = readDerTlvRange(certDer, pos, 48);
|
|
153
|
+
if (!spki || spki.fullEnd > tbs.valueEnd) {
|
|
154
|
+
throw new Error("unable to extract SPKI public key from certificate");
|
|
145
155
|
}
|
|
146
|
-
|
|
156
|
+
const spkiDer = certDer.slice(spki.fullStart, spki.fullEnd);
|
|
157
|
+
return spkiDer.buffer.slice(
|
|
158
|
+
spkiDer.byteOffset,
|
|
159
|
+
spkiDer.byteOffset + spkiDer.byteLength
|
|
160
|
+
);
|
|
147
161
|
}
|
|
148
162
|
async function certificateSha256Fingerprint(certPem) {
|
|
149
163
|
const der = pemToArrayBuffer(certPem);
|
|
@@ -902,8 +916,15 @@ var GatewayDiscovery = class {
|
|
|
902
916
|
* 选择 priority 最小的网关。
|
|
903
917
|
*/
|
|
904
918
|
async discover(wellKnownUrl, timeout = 5e3) {
|
|
919
|
+
const urls = await this.discoverAll(wellKnownUrl, timeout);
|
|
920
|
+
return urls[0];
|
|
921
|
+
}
|
|
922
|
+
/**
|
|
923
|
+
* 从 well-known URL 发现所有 Gateway WebSocket 地址(按 priority 排序)。
|
|
924
|
+
*/
|
|
925
|
+
async discoverAll(wellKnownUrl, timeout = 5e3) {
|
|
905
926
|
const tStart = Date.now();
|
|
906
|
-
this._log.debug(`
|
|
927
|
+
this._log.debug(`discoverAll enter: url=${wellKnownUrl}`);
|
|
907
928
|
let payload;
|
|
908
929
|
try {
|
|
909
930
|
const controller = new AbortController();
|
|
@@ -921,7 +942,7 @@ var GatewayDiscovery = class {
|
|
|
921
942
|
}
|
|
922
943
|
payload = rawPayload;
|
|
923
944
|
} catch (exc) {
|
|
924
|
-
this._log.debug(`
|
|
945
|
+
this._log.debug(`discoverAll exit (error): elapsed=${Date.now() - tStart}ms err=${exc instanceof Error ? exc.message : String(exc)}`);
|
|
925
946
|
throw new ConnectionError(
|
|
926
947
|
`gateway discovery failed for ${wellKnownUrl}: ${exc}`,
|
|
927
948
|
{ retryable: true }
|
|
@@ -929,21 +950,21 @@ var GatewayDiscovery = class {
|
|
|
929
950
|
}
|
|
930
951
|
const gateways = payload.gateways;
|
|
931
952
|
if (!Array.isArray(gateways) || gateways.length === 0) {
|
|
932
|
-
this._log.debug(`
|
|
953
|
+
this._log.debug(`discoverAll exit (error): elapsed=${Date.now() - tStart}ms err=empty_gateways`);
|
|
933
954
|
throw new ValidationError("well-known returned empty gateways");
|
|
934
955
|
}
|
|
935
956
|
const sorted = [...gateways].sort(
|
|
936
957
|
(a, b) => Number(a.priority ?? 999) - Number(b.priority ?? 999)
|
|
937
958
|
);
|
|
938
|
-
const
|
|
939
|
-
if (
|
|
940
|
-
this._log.debug(`
|
|
959
|
+
const urls = sorted.map((g) => String(g.url ?? "")).filter((u) => u.length > 0);
|
|
960
|
+
if (urls.length === 0) {
|
|
961
|
+
this._log.debug(`discoverAll exit (error): elapsed=${Date.now() - tStart}ms err=missing_url`);
|
|
941
962
|
throw new ValidationError("well-known missing gateway url");
|
|
942
963
|
}
|
|
943
|
-
this.checkHealth(
|
|
964
|
+
this.checkHealth(urls[0], timeout).catch(() => {
|
|
944
965
|
});
|
|
945
|
-
this._log.debug(`
|
|
946
|
-
return
|
|
966
|
+
this._log.debug(`discoverAll exit: elapsed=${Date.now() - tStart}ms gateways=${JSON.stringify(urls)}`);
|
|
967
|
+
return urls;
|
|
947
968
|
}
|
|
948
969
|
};
|
|
949
970
|
|
|
@@ -1490,6 +1511,14 @@ var RPCTransport = class {
|
|
|
1490
1511
|
const protocolEvent = method.slice(6);
|
|
1491
1512
|
const sdkEvent = EVENT_NAME_MAP[protocolEvent] ?? protocolEvent;
|
|
1492
1513
|
this._log.debug(`event recv: event=${sdkEvent} ${summarizeDict(message.params, DIAG_RESULT_FIELDS)}`);
|
|
1514
|
+
const meta2 = message._meta;
|
|
1515
|
+
if (this._metaObserver !== null && isJsonObject(meta2)) {
|
|
1516
|
+
try {
|
|
1517
|
+
this._metaObserver(meta2);
|
|
1518
|
+
} catch (exc) {
|
|
1519
|
+
this._log.debug(`event meta_observer raised: ${String(exc)}`);
|
|
1520
|
+
}
|
|
1521
|
+
}
|
|
1493
1522
|
const params = message.params ?? {};
|
|
1494
1523
|
if ("_trace" in params) {
|
|
1495
1524
|
const eventTrace = params._trace;
|
|
@@ -1508,6 +1537,14 @@ var RPCTransport = class {
|
|
|
1508
1537
|
this._dispatcher.publish(`_raw.${sdkEvent}`, params);
|
|
1509
1538
|
return;
|
|
1510
1539
|
}
|
|
1540
|
+
const meta = message._meta;
|
|
1541
|
+
if (this._metaObserver !== null && isJsonObject(meta)) {
|
|
1542
|
+
try {
|
|
1543
|
+
this._metaObserver(meta);
|
|
1544
|
+
} catch (exc) {
|
|
1545
|
+
this._log.debug(`notification meta_observer raised: ${String(exc)}`);
|
|
1546
|
+
}
|
|
1547
|
+
}
|
|
1511
1548
|
this._log.debug(`notification recv: method=${method || "<no-method>"}`);
|
|
1512
1549
|
this._dispatcher.publish("notification", message);
|
|
1513
1550
|
}
|
|
@@ -2854,7 +2891,7 @@ var _AuthFlow = class _AuthFlow {
|
|
|
2854
2891
|
}
|
|
2855
2892
|
}
|
|
2856
2893
|
async _loadInstanceState(aid) {
|
|
2857
|
-
if (
|
|
2894
|
+
if (typeof this._keystore.loadInstanceState !== "function") {
|
|
2858
2895
|
return null;
|
|
2859
2896
|
}
|
|
2860
2897
|
return await this._keystore.loadInstanceState(aid, this._deviceId, this._slotId);
|
|
@@ -2875,9 +2912,6 @@ var _AuthFlow = class _AuthFlow {
|
|
|
2875
2912
|
}
|
|
2876
2913
|
}
|
|
2877
2914
|
await this._keystore.saveIdentity(aid, persisted);
|
|
2878
|
-
if (!this._deviceId) {
|
|
2879
|
-
return;
|
|
2880
|
-
}
|
|
2881
2915
|
if (Object.keys(instanceState).length === 0 || typeof this._keystore.updateInstanceState !== "function") {
|
|
2882
2916
|
return;
|
|
2883
2917
|
}
|
|
@@ -2952,6 +2986,14 @@ var SeqTracker = class {
|
|
|
2952
2986
|
getMaxSeenSeq(ns) {
|
|
2953
2987
|
return this._get(ns).maxSeenSeq;
|
|
2954
2988
|
}
|
|
2989
|
+
/** Push 专用:只扩展上界 maxSeenSeq,不动 contiguousSeq。 */
|
|
2990
|
+
updateMaxSeen(ns, seq) {
|
|
2991
|
+
if (seq <= 0) return;
|
|
2992
|
+
const t = this._get(ns);
|
|
2993
|
+
if (seq > t.maxSeenSeq) {
|
|
2994
|
+
t.maxSeenSeq = seq;
|
|
2995
|
+
}
|
|
2996
|
+
}
|
|
2955
2997
|
/** S2: 从持久化(keystore 最近 ack seq)恢复 baseline,
|
|
2956
2998
|
* 以便首条 push 消息能构造 [baseline+1, seq-1] 的历史 gap。
|
|
2957
2999
|
* 必须在收到首条消息前调用。 */
|
|
@@ -3128,10 +3170,9 @@ var SeqTracker = class {
|
|
|
3128
3170
|
removeNamespace(ns) {
|
|
3129
3171
|
this._trackers.delete(ns);
|
|
3130
3172
|
}
|
|
3131
|
-
/**
|
|
3132
|
-
* 当服务端返回 server_ack_seq 且本地 contiguousSeq 落后时调用,
|
|
3133
|
-
* 跳过 [contiguousSeq, server_ack_seq) 这段不连续区间。 */
|
|
3173
|
+
/** Pull 专用:强制推进 contiguousSeq(已连续到达的下界)。 */
|
|
3134
3174
|
forceContiguousSeq(ns, seq) {
|
|
3175
|
+
if (seq <= 0) return;
|
|
3135
3176
|
const t = this._get(ns);
|
|
3136
3177
|
if (seq > t.contiguousSeq) {
|
|
3137
3178
|
for (const [key, probe] of t.pendingGaps) {
|
|
@@ -3147,6 +3188,22 @@ var SeqTracker = class {
|
|
|
3147
3188
|
this._tryAdvance(t);
|
|
3148
3189
|
}
|
|
3149
3190
|
}
|
|
3191
|
+
/** 脏数据修复:允许 contiguousSeq 倒退到指定值。 */
|
|
3192
|
+
repairContiguousSeq(ns, seq) {
|
|
3193
|
+
if (seq < 0) seq = 0;
|
|
3194
|
+
const t = this._get(ns);
|
|
3195
|
+
if (seq < t.contiguousSeq) {
|
|
3196
|
+
for (const s of t.receivedSeqs) {
|
|
3197
|
+
if (s <= seq) t.receivedSeqs.delete(s);
|
|
3198
|
+
}
|
|
3199
|
+
for (const [key, probe] of t.pendingGaps) {
|
|
3200
|
+
if (probe.gapStart <= seq) {
|
|
3201
|
+
t.pendingGaps.delete(key);
|
|
3202
|
+
}
|
|
3203
|
+
}
|
|
3204
|
+
t.contiguousSeq = seq;
|
|
3205
|
+
}
|
|
3206
|
+
}
|
|
3150
3207
|
/** 导出各命名空间的 contiguousSeq,用于持久化 */
|
|
3151
3208
|
exportState() {
|
|
3152
3209
|
const result = {};
|
|
@@ -3286,7 +3343,7 @@ function extractCommonNameFromCertPem(certPem) {
|
|
|
3286
3343
|
try {
|
|
3287
3344
|
const bytes = new Uint8Array(pemToArrayBuffer(certPem));
|
|
3288
3345
|
const oid = [6, 3, 85, 4, 3];
|
|
3289
|
-
const
|
|
3346
|
+
const decoder2 = new TextDecoder();
|
|
3290
3347
|
let commonName = "";
|
|
3291
3348
|
for (let i = 0; i <= bytes.length - oid.length - 2; i++) {
|
|
3292
3349
|
let matched = true;
|
|
@@ -3305,9 +3362,9 @@ function extractCommonNameFromCertPem(certPem) {
|
|
|
3305
3362
|
if (end > bytes.length) continue;
|
|
3306
3363
|
const valueBytes = bytes.slice(start, end);
|
|
3307
3364
|
if (tag === 12 || tag === 19 || tag === 22 || tag === 20 || tag === 30) {
|
|
3308
|
-
commonName =
|
|
3365
|
+
commonName = decoder2.decode(valueBytes).trim();
|
|
3309
3366
|
} else {
|
|
3310
|
-
commonName =
|
|
3367
|
+
commonName = decoder2.decode(valueBytes).trim();
|
|
3311
3368
|
}
|
|
3312
3369
|
}
|
|
3313
3370
|
return commonName;
|
|
@@ -3725,6 +3782,43 @@ var AuthNamespace = class {
|
|
|
3725
3782
|
throw err;
|
|
3726
3783
|
}
|
|
3727
3784
|
}
|
|
3785
|
+
async headAgentMd(aid) {
|
|
3786
|
+
const tStart = Date.now();
|
|
3787
|
+
const targetAid = String(aid ?? "").trim();
|
|
3788
|
+
if (!targetAid) {
|
|
3789
|
+
throw new ValidationError("headAgentMd requires non-empty aid");
|
|
3790
|
+
}
|
|
3791
|
+
this._log.debug(`headAgentMd enter: aid=${targetAid}`);
|
|
3792
|
+
try {
|
|
3793
|
+
const response = await fetchWithTimeout(await this._resolveAgentMdUrl(targetAid), {
|
|
3794
|
+
method: "HEAD",
|
|
3795
|
+
headers: { Accept: "text/markdown" }
|
|
3796
|
+
});
|
|
3797
|
+
const respHeaders = response.headers;
|
|
3798
|
+
const etag = respHeaders ? String(respHeaders.get("ETag") ?? "").trim() : "";
|
|
3799
|
+
const lastModified = respHeaders ? String(respHeaders.get("Last-Modified") ?? "").trim() : "";
|
|
3800
|
+
if (response.status === 404) {
|
|
3801
|
+
this._log.debug(`headAgentMd exit (not_found): elapsed=${Date.now() - tStart}ms aid=${targetAid}`);
|
|
3802
|
+
return { aid: targetAid, found: false, etag: "", last_modified: "", status: 404 };
|
|
3803
|
+
}
|
|
3804
|
+
if (response.status < 200 || response.status >= 300) {
|
|
3805
|
+
throw new AUNError(`head agent.md failed: HTTP ${response.status}`);
|
|
3806
|
+
}
|
|
3807
|
+
if (etag || lastModified) {
|
|
3808
|
+
const cached = this._agentMdCache.get(targetAid) ?? { text: "", etag: "", lastModified: "" };
|
|
3809
|
+
this._agentMdCache.set(targetAid, {
|
|
3810
|
+
text: cached.text ?? "",
|
|
3811
|
+
etag,
|
|
3812
|
+
lastModified
|
|
3813
|
+
});
|
|
3814
|
+
}
|
|
3815
|
+
this._log.debug(`headAgentMd exit: elapsed=${Date.now() - tStart}ms aid=${targetAid} status=${response.status} etag=${etag || "-"}`);
|
|
3816
|
+
return { aid: targetAid, found: true, etag, last_modified: lastModified, status: response.status };
|
|
3817
|
+
} catch (err) {
|
|
3818
|
+
this._log.debug(`headAgentMd exit (error): elapsed=${Date.now() - tStart}ms aid=${targetAid} err=${err instanceof Error ? err.message : String(err)}`);
|
|
3819
|
+
throw err;
|
|
3820
|
+
}
|
|
3821
|
+
}
|
|
3728
3822
|
async downloadAgentMd(aid) {
|
|
3729
3823
|
const tStart = Date.now();
|
|
3730
3824
|
this._log.debug(`downloadAgentMd enter: aid=${aid}`);
|
|
@@ -4907,7 +5001,7 @@ async function _decryptPEM(enc, seed) {
|
|
|
4907
5001
|
return new TextDecoder().decode(pt);
|
|
4908
5002
|
}
|
|
4909
5003
|
var DB_NAME = "aun-keystore";
|
|
4910
|
-
var DB_VERSION =
|
|
5004
|
+
var DB_VERSION = 6;
|
|
4911
5005
|
var STORE_KEY_PAIRS = "key_pairs";
|
|
4912
5006
|
var STORE_CERTS = "certs";
|
|
4913
5007
|
var STORE_METADATA = "metadata";
|
|
@@ -4917,6 +5011,7 @@ var STORE_GROUP_CURRENT = "group_current";
|
|
|
4917
5011
|
var STORE_GROUP_OLD_EPOCHS = "group_old_epochs";
|
|
4918
5012
|
var STORE_SESSIONS = "e2ee_sessions";
|
|
4919
5013
|
var STORE_GROUP_STATE = "group_state";
|
|
5014
|
+
var STORE_AGENT_MD_CACHE = "agent_md_cache";
|
|
4920
5015
|
var STRUCTURED_RECOVERY_RETENTION_MS = 7 * 24 * 3600 * 1e3;
|
|
4921
5016
|
function metadataStoreKey(aid) {
|
|
4922
5017
|
return safeAid(aid);
|
|
@@ -4987,6 +5082,59 @@ function seqTrackerPrefix(aid, deviceId, slotId) {
|
|
|
4987
5082
|
function seqTrackerStoreKey(aid, deviceId, slotId, namespace) {
|
|
4988
5083
|
return `${seqTrackerPrefix(aid, deviceId, slotId)}${encodePart(namespace)}`;
|
|
4989
5084
|
}
|
|
5085
|
+
function agentMdCachePrefix(ownerAid) {
|
|
5086
|
+
return `${safeAid(ownerAid)}|`;
|
|
5087
|
+
}
|
|
5088
|
+
function agentMdCacheStoreKey(ownerAid, targetAid) {
|
|
5089
|
+
return `${agentMdCachePrefix(ownerAid)}${encodePart(targetAid)}`;
|
|
5090
|
+
}
|
|
5091
|
+
function defaultAgentMdCacheRecord(aid) {
|
|
5092
|
+
return {
|
|
5093
|
+
aid,
|
|
5094
|
+
content: "",
|
|
5095
|
+
local_etag: "",
|
|
5096
|
+
remote_etag: "",
|
|
5097
|
+
last_modified: "",
|
|
5098
|
+
fetched_at: 0,
|
|
5099
|
+
observed_at: 0,
|
|
5100
|
+
checked_at: 0,
|
|
5101
|
+
remote_status: "",
|
|
5102
|
+
verify_status: "",
|
|
5103
|
+
verify_error: "",
|
|
5104
|
+
last_error: "",
|
|
5105
|
+
updated_at: 0
|
|
5106
|
+
};
|
|
5107
|
+
}
|
|
5108
|
+
function normalizeAgentMdCacheRecord(aid, value) {
|
|
5109
|
+
if (!isRecord(value)) return null;
|
|
5110
|
+
const out = defaultAgentMdCacheRecord(aid);
|
|
5111
|
+
for (const key of ["content", "local_etag", "remote_etag", "last_modified", "remote_status", "verify_status", "verify_error", "last_error"]) {
|
|
5112
|
+
out[key] = String(value[key] ?? "");
|
|
5113
|
+
}
|
|
5114
|
+
for (const key of ["fetched_at", "observed_at", "checked_at", "updated_at"]) {
|
|
5115
|
+
const n = Number(value[key] ?? 0);
|
|
5116
|
+
out[key] = Number.isFinite(n) ? Math.trunc(n) : 0;
|
|
5117
|
+
}
|
|
5118
|
+
out.aid = String(value.aid ?? aid).trim() || aid;
|
|
5119
|
+
return out;
|
|
5120
|
+
}
|
|
5121
|
+
function mergeAgentMdCacheRecord(aid, current, fields) {
|
|
5122
|
+
const out = current ? { ...current } : defaultAgentMdCacheRecord(aid);
|
|
5123
|
+
out.aid = aid;
|
|
5124
|
+
for (const key of ["content", "local_etag", "remote_etag", "last_modified", "remote_status", "verify_status", "verify_error", "last_error"]) {
|
|
5125
|
+
if (Object.prototype.hasOwnProperty.call(fields, key) && fields[key] !== void 0 && fields[key] !== null) {
|
|
5126
|
+
out[key] = String(fields[key] ?? "");
|
|
5127
|
+
}
|
|
5128
|
+
}
|
|
5129
|
+
for (const key of ["fetched_at", "observed_at", "checked_at"]) {
|
|
5130
|
+
if (Object.prototype.hasOwnProperty.call(fields, key) && fields[key] !== void 0 && fields[key] !== null) {
|
|
5131
|
+
const n = Number(fields[key] ?? 0);
|
|
5132
|
+
out[key] = Number.isFinite(n) ? Math.trunc(n) : 0;
|
|
5133
|
+
}
|
|
5134
|
+
}
|
|
5135
|
+
out.updated_at = Date.now();
|
|
5136
|
+
return out;
|
|
5137
|
+
}
|
|
4990
5138
|
function stripStructuredFields(metadata) {
|
|
4991
5139
|
const plain = deepClone(metadata);
|
|
4992
5140
|
delete plain.e2ee_prekeys;
|
|
@@ -5030,6 +5178,9 @@ function openDB() {
|
|
|
5030
5178
|
if (!db.objectStoreNames.contains(STORE_GROUP_STATE)) {
|
|
5031
5179
|
db.createObjectStore(STORE_GROUP_STATE);
|
|
5032
5180
|
}
|
|
5181
|
+
if (!db.objectStoreNames.contains(STORE_AGENT_MD_CACHE)) {
|
|
5182
|
+
db.createObjectStore(STORE_AGENT_MD_CACHE);
|
|
5183
|
+
}
|
|
5033
5184
|
};
|
|
5034
5185
|
request.onsuccess = () => {
|
|
5035
5186
|
const db = request.result;
|
|
@@ -6260,6 +6411,46 @@ var _IndexedDBKeyStore = class _IndexedDBKeyStore {
|
|
|
6260
6411
|
const key = seqTrackerStoreKey(aid, deviceId, slotId, namespace);
|
|
6261
6412
|
await idbDelete(STORE_INSTANCE_STATE, key);
|
|
6262
6413
|
}
|
|
6414
|
+
// ── agent.md Cache ───────────────────────────────────────
|
|
6415
|
+
async loadAgentMdCache(ownerAid, targetAid) {
|
|
6416
|
+
const owner = String(ownerAid ?? "").trim();
|
|
6417
|
+
const target = String(targetAid ?? "").trim();
|
|
6418
|
+
if (!owner || !target) return null;
|
|
6419
|
+
const data = await idbGet(STORE_AGENT_MD_CACHE, agentMdCacheStoreKey(owner, target));
|
|
6420
|
+
const record = normalizeAgentMdCacheRecord(target, data);
|
|
6421
|
+
return record ? deepClone(record) : null;
|
|
6422
|
+
}
|
|
6423
|
+
async upsertAgentMdCache(ownerAid, targetAid, fields) {
|
|
6424
|
+
const owner = String(ownerAid ?? "").trim();
|
|
6425
|
+
const target = String(targetAid ?? "").trim();
|
|
6426
|
+
if (!owner || !target) {
|
|
6427
|
+
throw new Error("upsertAgentMdCache requires ownerAid and targetAid");
|
|
6428
|
+
}
|
|
6429
|
+
const current = await this.loadAgentMdCache(owner, target);
|
|
6430
|
+
const record = mergeAgentMdCacheRecord(target, current, fields ?? {});
|
|
6431
|
+
await idbPut(STORE_AGENT_MD_CACHE, agentMdCacheStoreKey(owner, target), record);
|
|
6432
|
+
return deepClone(record);
|
|
6433
|
+
}
|
|
6434
|
+
async listAgentMdContentAids(agentMdPath) {
|
|
6435
|
+
const root = String(agentMdPath ?? "").trim();
|
|
6436
|
+
if (!root) return [];
|
|
6437
|
+
const prefix = agentMdCachePrefix(root);
|
|
6438
|
+
const suffix = "/agent.md";
|
|
6439
|
+
const aids = /* @__PURE__ */ new Set();
|
|
6440
|
+
for (const item of await idbGetAllByPrefix(STORE_AGENT_MD_CACHE, prefix)) {
|
|
6441
|
+
const encodedTarget = item.key.slice(prefix.length);
|
|
6442
|
+
let target = "";
|
|
6443
|
+
try {
|
|
6444
|
+
target = decodeURIComponent(encodedTarget);
|
|
6445
|
+
} catch {
|
|
6446
|
+
target = encodedTarget;
|
|
6447
|
+
}
|
|
6448
|
+
if (!target.endsWith(suffix)) continue;
|
|
6449
|
+
const aid = target.slice(0, -suffix.length).trim();
|
|
6450
|
+
if (aid) aids.add(aid);
|
|
6451
|
+
}
|
|
6452
|
+
return [...aids].sort();
|
|
6453
|
+
}
|
|
6263
6454
|
// ── Group State(群组状态快照) ─────────────────────────────
|
|
6264
6455
|
async saveGroupState(groupId, state) {
|
|
6265
6456
|
const key = encodePart(groupId);
|
|
@@ -6359,9 +6550,53 @@ var IndexedDBKeyStore = _IndexedDBKeyStore;
|
|
|
6359
6550
|
|
|
6360
6551
|
// src/v2/session/keystore.ts
|
|
6361
6552
|
var V2_DB_NAME = "aun_v2";
|
|
6362
|
-
var V2_DB_VERSION =
|
|
6553
|
+
var V2_DB_VERSION = 3;
|
|
6363
6554
|
var V2_STORE_NAME = "v2_device_keys";
|
|
6364
6555
|
var V2_INDEX_BY_DEVICE_TYPE_CREATED = "by_device_type_created";
|
|
6556
|
+
async function spkIdForPubDer(pubDer) {
|
|
6557
|
+
const buf = await crypto.subtle.digest("SHA-256", pubDer.slice().buffer);
|
|
6558
|
+
const arr = new Uint8Array(buf);
|
|
6559
|
+
let hex = "";
|
|
6560
|
+
for (let i = 0; i < arr.length; i++) hex += arr[i].toString(16).padStart(2, "0");
|
|
6561
|
+
return `sha256:${hex.slice(0, 16)}`;
|
|
6562
|
+
}
|
|
6563
|
+
function migrateRecord(raw) {
|
|
6564
|
+
let groupId = String(raw.group_id ?? "");
|
|
6565
|
+
let keyId = String(raw.key_id ?? "");
|
|
6566
|
+
if ((raw.key_type === "group_spk" || raw.key_type === "group_spk_uploaded") && !groupId && keyId.includes("\0")) {
|
|
6567
|
+
const parts = keyId.split("\0");
|
|
6568
|
+
groupId = parts[0] ?? "";
|
|
6569
|
+
keyId = parts.slice(1).join("\0");
|
|
6570
|
+
}
|
|
6571
|
+
return {
|
|
6572
|
+
device_id: String(raw.device_id ?? ""),
|
|
6573
|
+
key_type: raw.key_type,
|
|
6574
|
+
group_id: groupId,
|
|
6575
|
+
key_id: keyId,
|
|
6576
|
+
private_key: new Uint8Array(raw.private_key ?? new Uint8Array()),
|
|
6577
|
+
public_key: new Uint8Array(raw.public_key ?? new Uint8Array()),
|
|
6578
|
+
created_at: Number(raw.created_at ?? Date.now())
|
|
6579
|
+
};
|
|
6580
|
+
}
|
|
6581
|
+
function createV2Store(db) {
|
|
6582
|
+
const store = db.createObjectStore(V2_STORE_NAME, {
|
|
6583
|
+
keyPath: ["device_id", "key_type", "group_id", "key_id"]
|
|
6584
|
+
});
|
|
6585
|
+
store.createIndex(
|
|
6586
|
+
V2_INDEX_BY_DEVICE_TYPE_CREATED,
|
|
6587
|
+
["device_id", "key_type", "group_id", "created_at"]
|
|
6588
|
+
);
|
|
6589
|
+
return store;
|
|
6590
|
+
}
|
|
6591
|
+
function recreateScopeCreatedIndex(store) {
|
|
6592
|
+
if (store.indexNames.contains(V2_INDEX_BY_DEVICE_TYPE_CREATED)) {
|
|
6593
|
+
store.deleteIndex(V2_INDEX_BY_DEVICE_TYPE_CREATED);
|
|
6594
|
+
}
|
|
6595
|
+
store.createIndex(
|
|
6596
|
+
V2_INDEX_BY_DEVICE_TYPE_CREATED,
|
|
6597
|
+
["device_id", "key_type", "group_id", "created_at"]
|
|
6598
|
+
);
|
|
6599
|
+
}
|
|
6365
6600
|
var V2KeyStore = class _V2KeyStore {
|
|
6366
6601
|
constructor(db) {
|
|
6367
6602
|
this.db = db;
|
|
@@ -6370,16 +6605,28 @@ var V2KeyStore = class _V2KeyStore {
|
|
|
6370
6605
|
static async open(dbName = V2_DB_NAME) {
|
|
6371
6606
|
return new Promise((resolve, reject) => {
|
|
6372
6607
|
const req = indexedDB.open(dbName, V2_DB_VERSION);
|
|
6373
|
-
req.onupgradeneeded = () => {
|
|
6608
|
+
req.onupgradeneeded = (event) => {
|
|
6374
6609
|
const db = req.result;
|
|
6375
6610
|
if (!db.objectStoreNames.contains(V2_STORE_NAME)) {
|
|
6376
|
-
|
|
6377
|
-
|
|
6378
|
-
|
|
6379
|
-
|
|
6380
|
-
|
|
6381
|
-
|
|
6382
|
-
);
|
|
6611
|
+
createV2Store(db);
|
|
6612
|
+
return;
|
|
6613
|
+
}
|
|
6614
|
+
if (event.oldVersion < 2) {
|
|
6615
|
+
const tx = req.transaction;
|
|
6616
|
+
if (!tx) throw new Error("V2KeyStore.open: missing upgrade transaction");
|
|
6617
|
+
const oldStore = tx.objectStore(V2_STORE_NAME);
|
|
6618
|
+
const getAllReq = oldStore.getAll();
|
|
6619
|
+
getAllReq.onsuccess = () => {
|
|
6620
|
+
const records = getAllReq.result.map(migrateRecord);
|
|
6621
|
+
db.deleteObjectStore(V2_STORE_NAME);
|
|
6622
|
+
const store = createV2Store(db);
|
|
6623
|
+
for (const record of records) store.put(record);
|
|
6624
|
+
};
|
|
6625
|
+
getAllReq.onerror = () => reject(getAllReq.error);
|
|
6626
|
+
} else if (event.oldVersion < 3) {
|
|
6627
|
+
const tx = req.transaction;
|
|
6628
|
+
if (!tx) throw new Error("V2KeyStore.open: missing upgrade transaction");
|
|
6629
|
+
recreateScopeCreatedIndex(tx.objectStore(V2_STORE_NAME));
|
|
6383
6630
|
}
|
|
6384
6631
|
};
|
|
6385
6632
|
req.onsuccess = () => resolve(new _V2KeyStore(req.result));
|
|
@@ -6394,11 +6641,33 @@ var V2KeyStore = class _V2KeyStore {
|
|
|
6394
6641
|
store(mode) {
|
|
6395
6642
|
return this.db.transaction(V2_STORE_NAME, mode).objectStore(V2_STORE_NAME);
|
|
6396
6643
|
}
|
|
6644
|
+
async _listRecordsByTypeNewestFirst(deviceId, keyType, groupId) {
|
|
6645
|
+
return new Promise((resolve, reject) => {
|
|
6646
|
+
const idx = this.store("readonly").index(V2_INDEX_BY_DEVICE_TYPE_CREATED);
|
|
6647
|
+
const range = IDBKeyRange.bound(
|
|
6648
|
+
[deviceId, keyType, groupId, -Infinity],
|
|
6649
|
+
[deviceId, keyType, groupId, Infinity]
|
|
6650
|
+
);
|
|
6651
|
+
const req = idx.openCursor(range, "prev");
|
|
6652
|
+
const out = [];
|
|
6653
|
+
req.onsuccess = () => {
|
|
6654
|
+
const cursor = req.result;
|
|
6655
|
+
if (cursor) {
|
|
6656
|
+
out.push(cursor.value);
|
|
6657
|
+
cursor.continue();
|
|
6658
|
+
} else {
|
|
6659
|
+
resolve(out);
|
|
6660
|
+
}
|
|
6661
|
+
};
|
|
6662
|
+
req.onerror = () => reject(req.error);
|
|
6663
|
+
});
|
|
6664
|
+
}
|
|
6397
6665
|
// ---------- SPK ----------
|
|
6398
6666
|
async saveSPK(deviceId, spkId, priv, pubDer) {
|
|
6399
6667
|
const record = {
|
|
6400
6668
|
device_id: deviceId,
|
|
6401
6669
|
key_type: "spk",
|
|
6670
|
+
group_id: "",
|
|
6402
6671
|
key_id: spkId,
|
|
6403
6672
|
private_key: priv,
|
|
6404
6673
|
public_key: pubDer,
|
|
@@ -6412,7 +6681,7 @@ var V2KeyStore = class _V2KeyStore {
|
|
|
6412
6681
|
}
|
|
6413
6682
|
async loadSPK(deviceId, spkId) {
|
|
6414
6683
|
return new Promise((resolve, reject) => {
|
|
6415
|
-
const req = this.store("readonly").get([deviceId, "spk", spkId]);
|
|
6684
|
+
const req = this.store("readonly").get([deviceId, "spk", "", spkId]);
|
|
6416
6685
|
req.onsuccess = () => {
|
|
6417
6686
|
const r = req.result;
|
|
6418
6687
|
resolve(r ? new Uint8Array(r.private_key) : null);
|
|
@@ -6425,8 +6694,8 @@ var V2KeyStore = class _V2KeyStore {
|
|
|
6425
6694
|
return new Promise((resolve, reject) => {
|
|
6426
6695
|
const idx = this.store("readonly").index(V2_INDEX_BY_DEVICE_TYPE_CREATED);
|
|
6427
6696
|
const range = IDBKeyRange.bound(
|
|
6428
|
-
[deviceId, "spk", -Infinity],
|
|
6429
|
-
[deviceId, "spk", Infinity]
|
|
6697
|
+
[deviceId, "spk", "", -Infinity],
|
|
6698
|
+
[deviceId, "spk", "", Infinity]
|
|
6430
6699
|
);
|
|
6431
6700
|
const req = idx.openCursor(range, "prev");
|
|
6432
6701
|
req.onsuccess = () => {
|
|
@@ -6446,20 +6715,49 @@ var V2KeyStore = class _V2KeyStore {
|
|
|
6446
6715
|
});
|
|
6447
6716
|
}
|
|
6448
6717
|
async deleteSPK(deviceId, spkId) {
|
|
6718
|
+
await new Promise((resolve, reject) => {
|
|
6719
|
+
const req = this.store("readwrite").delete([deviceId, "spk", "", spkId]);
|
|
6720
|
+
req.onsuccess = () => resolve();
|
|
6721
|
+
req.onerror = () => reject(req.error);
|
|
6722
|
+
});
|
|
6723
|
+
await new Promise((resolve, reject) => {
|
|
6724
|
+
const req = this.store("readwrite").delete([deviceId, "spk_uploaded", "", spkId]);
|
|
6725
|
+
req.onsuccess = () => resolve();
|
|
6726
|
+
req.onerror = () => reject(req.error);
|
|
6727
|
+
});
|
|
6728
|
+
}
|
|
6729
|
+
async markSPKUploaded(deviceId, spkId) {
|
|
6730
|
+
const record = {
|
|
6731
|
+
device_id: deviceId,
|
|
6732
|
+
key_type: "spk_uploaded",
|
|
6733
|
+
group_id: "",
|
|
6734
|
+
key_id: spkId,
|
|
6735
|
+
private_key: new Uint8Array(),
|
|
6736
|
+
public_key: new Uint8Array(),
|
|
6737
|
+
created_at: Date.now()
|
|
6738
|
+
};
|
|
6449
6739
|
return new Promise((resolve, reject) => {
|
|
6450
|
-
const req = this.store("readwrite").
|
|
6740
|
+
const req = this.store("readwrite").put(record);
|
|
6451
6741
|
req.onsuccess = () => resolve();
|
|
6452
6742
|
req.onerror = () => reject(req.error);
|
|
6453
6743
|
});
|
|
6454
6744
|
}
|
|
6745
|
+
async loadLatestUploadedSPKId(deviceId) {
|
|
6746
|
+
const records = await this._listRecordsByTypeNewestFirst(deviceId, "spk_uploaded", "");
|
|
6747
|
+
for (const record of records) {
|
|
6748
|
+
const spkId = record.key_id;
|
|
6749
|
+
if (await this.loadSPK(deviceId, spkId)) return spkId;
|
|
6750
|
+
}
|
|
6751
|
+
return null;
|
|
6752
|
+
}
|
|
6455
6753
|
/** 返回最近 N 代 SPK 的 spk_id(按 created_at DESC)。 */
|
|
6456
6754
|
async listRecentSPKIds(deviceId, n) {
|
|
6457
6755
|
if (n <= 0) return [];
|
|
6458
6756
|
return new Promise((resolve, reject) => {
|
|
6459
6757
|
const idx = this.store("readonly").index(V2_INDEX_BY_DEVICE_TYPE_CREATED);
|
|
6460
6758
|
const range = IDBKeyRange.bound(
|
|
6461
|
-
[deviceId, "spk", -Infinity],
|
|
6462
|
-
[deviceId, "spk", Infinity]
|
|
6759
|
+
[deviceId, "spk", "", -Infinity],
|
|
6760
|
+
[deviceId, "spk", "", Infinity]
|
|
6463
6761
|
);
|
|
6464
6762
|
const req = idx.openCursor(range, "prev");
|
|
6465
6763
|
const out = [];
|
|
@@ -6480,8 +6778,8 @@ var V2KeyStore = class _V2KeyStore {
|
|
|
6480
6778
|
return new Promise((resolve, reject) => {
|
|
6481
6779
|
const idx = this.store("readonly").index(V2_INDEX_BY_DEVICE_TYPE_CREATED);
|
|
6482
6780
|
const range = IDBKeyRange.bound(
|
|
6483
|
-
[deviceId, "spk", -Infinity],
|
|
6484
|
-
[deviceId, "spk", cutoff],
|
|
6781
|
+
[deviceId, "spk", "", -Infinity],
|
|
6782
|
+
[deviceId, "spk", "", cutoff],
|
|
6485
6783
|
false,
|
|
6486
6784
|
true
|
|
6487
6785
|
);
|
|
@@ -6500,14 +6798,12 @@ var V2KeyStore = class _V2KeyStore {
|
|
|
6500
6798
|
});
|
|
6501
6799
|
}
|
|
6502
6800
|
// ---------- Group SPK ----------
|
|
6503
|
-
static _groupSpkKeyId(groupId, spkId) {
|
|
6504
|
-
return `${groupId}\0${spkId}`;
|
|
6505
|
-
}
|
|
6506
6801
|
async saveGroupSPK(deviceId, groupId, spkId, priv, pubDer) {
|
|
6507
6802
|
const record = {
|
|
6508
6803
|
device_id: deviceId,
|
|
6509
6804
|
key_type: "group_spk",
|
|
6510
|
-
|
|
6805
|
+
group_id: groupId,
|
|
6806
|
+
key_id: spkId,
|
|
6511
6807
|
private_key: priv,
|
|
6512
6808
|
public_key: pubDer,
|
|
6513
6809
|
created_at: Date.now()
|
|
@@ -6519,9 +6815,8 @@ var V2KeyStore = class _V2KeyStore {
|
|
|
6519
6815
|
});
|
|
6520
6816
|
}
|
|
6521
6817
|
async loadGroupSPK(deviceId, groupId, spkId) {
|
|
6522
|
-
const keyId = _V2KeyStore._groupSpkKeyId(groupId, spkId);
|
|
6523
6818
|
return new Promise((resolve, reject) => {
|
|
6524
|
-
const req = this.store("readonly").get([deviceId, "group_spk",
|
|
6819
|
+
const req = this.store("readonly").get([deviceId, "group_spk", groupId, spkId]);
|
|
6525
6820
|
req.onsuccess = () => {
|
|
6526
6821
|
const r = req.result;
|
|
6527
6822
|
resolve(r ? new Uint8Array(r.private_key) : null);
|
|
@@ -6529,14 +6824,13 @@ var V2KeyStore = class _V2KeyStore {
|
|
|
6529
6824
|
req.onerror = () => reject(req.error);
|
|
6530
6825
|
});
|
|
6531
6826
|
}
|
|
6532
|
-
/** 取指定群最新 group SPK(按 created_at DESC
|
|
6827
|
+
/** 取指定群最新 group SPK(按 created_at DESC)。 */
|
|
6533
6828
|
async loadCurrentGroupSPK(deviceId, groupId) {
|
|
6534
|
-
const prefix = `${groupId}\0`;
|
|
6535
6829
|
return new Promise((resolve, reject) => {
|
|
6536
6830
|
const idx = this.store("readonly").index(V2_INDEX_BY_DEVICE_TYPE_CREATED);
|
|
6537
6831
|
const range = IDBKeyRange.bound(
|
|
6538
|
-
[deviceId, "group_spk", -Infinity],
|
|
6539
|
-
[deviceId, "group_spk", Infinity]
|
|
6832
|
+
[deviceId, "group_spk", groupId, -Infinity],
|
|
6833
|
+
[deviceId, "group_spk", groupId, Infinity]
|
|
6540
6834
|
);
|
|
6541
6835
|
const req = idx.openCursor(range, "prev");
|
|
6542
6836
|
req.onsuccess = () => {
|
|
@@ -6546,39 +6840,80 @@ var V2KeyStore = class _V2KeyStore {
|
|
|
6546
6840
|
return;
|
|
6547
6841
|
}
|
|
6548
6842
|
const r = cursor.value;
|
|
6549
|
-
|
|
6550
|
-
|
|
6551
|
-
|
|
6552
|
-
|
|
6553
|
-
|
|
6554
|
-
pubDer: new Uint8Array(r.public_key)
|
|
6555
|
-
});
|
|
6556
|
-
} else {
|
|
6557
|
-
cursor.continue();
|
|
6558
|
-
}
|
|
6843
|
+
resolve({
|
|
6844
|
+
spkId: r.key_id,
|
|
6845
|
+
priv: new Uint8Array(r.private_key),
|
|
6846
|
+
pubDer: new Uint8Array(r.public_key)
|
|
6847
|
+
});
|
|
6559
6848
|
};
|
|
6560
6849
|
req.onerror = () => reject(req.error);
|
|
6561
6850
|
});
|
|
6562
6851
|
}
|
|
6852
|
+
async markGroupSPKUploaded(deviceId, groupId, spkId) {
|
|
6853
|
+
const record = {
|
|
6854
|
+
device_id: deviceId,
|
|
6855
|
+
key_type: "group_spk_uploaded",
|
|
6856
|
+
group_id: groupId,
|
|
6857
|
+
key_id: spkId,
|
|
6858
|
+
private_key: new Uint8Array(),
|
|
6859
|
+
public_key: new Uint8Array(),
|
|
6860
|
+
created_at: Date.now()
|
|
6861
|
+
};
|
|
6862
|
+
return new Promise((resolve, reject) => {
|
|
6863
|
+
const req = this.store("readwrite").put(record);
|
|
6864
|
+
req.onsuccess = () => resolve();
|
|
6865
|
+
req.onerror = () => reject(req.error);
|
|
6866
|
+
});
|
|
6867
|
+
}
|
|
6868
|
+
async loadLatestUploadedGroupSPKId(deviceId, groupId) {
|
|
6869
|
+
const records = await this._listRecordsByTypeNewestFirst(deviceId, "group_spk_uploaded", groupId);
|
|
6870
|
+
for (const record of records) {
|
|
6871
|
+
const spkId = record.key_id;
|
|
6872
|
+
if (await this.loadGroupSPK(deviceId, groupId, spkId)) return spkId;
|
|
6873
|
+
}
|
|
6874
|
+
return null;
|
|
6875
|
+
}
|
|
6563
6876
|
// ---------- IK ----------
|
|
6564
6877
|
async saveIK(deviceId, priv, pubDer) {
|
|
6565
6878
|
const record = {
|
|
6566
6879
|
device_id: deviceId,
|
|
6567
6880
|
key_type: "ik",
|
|
6881
|
+
group_id: "",
|
|
6568
6882
|
key_id: "",
|
|
6569
6883
|
private_key: priv,
|
|
6570
6884
|
public_key: pubDer,
|
|
6571
6885
|
created_at: Date.now()
|
|
6572
6886
|
};
|
|
6573
|
-
|
|
6887
|
+
await new Promise((resolve, reject) => {
|
|
6574
6888
|
const req = this.store("readwrite").put(record);
|
|
6575
6889
|
req.onsuccess = () => resolve();
|
|
6576
6890
|
req.onerror = () => reject(req.error);
|
|
6577
6891
|
});
|
|
6892
|
+
const alias = {
|
|
6893
|
+
...record,
|
|
6894
|
+
key_id: await spkIdForPubDer(pubDer)
|
|
6895
|
+
};
|
|
6896
|
+
return new Promise((resolve, reject) => {
|
|
6897
|
+
const req = this.store("readwrite").put(alias);
|
|
6898
|
+
req.onsuccess = () => resolve();
|
|
6899
|
+
req.onerror = () => reject(req.error);
|
|
6900
|
+
});
|
|
6578
6901
|
}
|
|
6579
6902
|
async loadIK(deviceId) {
|
|
6580
6903
|
return new Promise((resolve, reject) => {
|
|
6581
|
-
const req = this.store("readonly").get([deviceId, "ik", ""]);
|
|
6904
|
+
const req = this.store("readonly").get([deviceId, "ik", "", ""]);
|
|
6905
|
+
req.onsuccess = () => {
|
|
6906
|
+
const r = req.result;
|
|
6907
|
+
resolve(
|
|
6908
|
+
r ? { priv: new Uint8Array(r.private_key), pubDer: new Uint8Array(r.public_key) } : null
|
|
6909
|
+
);
|
|
6910
|
+
};
|
|
6911
|
+
req.onerror = () => reject(req.error);
|
|
6912
|
+
});
|
|
6913
|
+
}
|
|
6914
|
+
async loadIKSPK(deviceId, spkId) {
|
|
6915
|
+
return new Promise((resolve, reject) => {
|
|
6916
|
+
const req = this.store("readonly").get([deviceId, "ik", "", spkId]);
|
|
6582
6917
|
req.onsuccess = () => {
|
|
6583
6918
|
const r = req.result;
|
|
6584
6919
|
resolve(
|
|
@@ -6601,6 +6936,7 @@ var V2KeyStore = class _V2KeyStore {
|
|
|
6601
6936
|
|
|
6602
6937
|
// src/v2/crypto/canonical.ts
|
|
6603
6938
|
var encoder = new TextEncoder();
|
|
6939
|
+
var MAX_SAFE_JSON_INTEGER = 9007199254740991;
|
|
6604
6940
|
function canonicalJson(obj) {
|
|
6605
6941
|
return encoder.encode(serialize(obj));
|
|
6606
6942
|
}
|
|
@@ -6626,17 +6962,34 @@ function serializeNumber(n) {
|
|
|
6626
6962
|
if (!isFinite(n)) {
|
|
6627
6963
|
throw new RangeError("canonicalJson: Infinity and NaN not allowed");
|
|
6628
6964
|
}
|
|
6965
|
+
if (Object.is(n, -0)) return "0";
|
|
6629
6966
|
if (Number.isInteger(n)) {
|
|
6630
|
-
|
|
6631
|
-
|
|
6632
|
-
let s = String(n);
|
|
6633
|
-
if (s.includes("e") || s.includes("E")) {
|
|
6634
|
-
s = n.toFixed(20).replace(/0+$/, "");
|
|
6635
|
-
if (s.endsWith(".")) {
|
|
6636
|
-
s += "0";
|
|
6967
|
+
if (Math.abs(n) > MAX_SAFE_JSON_INTEGER) {
|
|
6968
|
+
throw new RangeError(`canonicalJson: integer outside safe range ${n}`);
|
|
6637
6969
|
}
|
|
6970
|
+
return String(n);
|
|
6638
6971
|
}
|
|
6639
|
-
return
|
|
6972
|
+
return expandExponent(String(n));
|
|
6973
|
+
}
|
|
6974
|
+
function expandExponent(s) {
|
|
6975
|
+
if (!/[eE]/.test(s)) return s;
|
|
6976
|
+
const match = /^(-?)(\d+)(?:\.(\d+))?[eE]([+-]?\d+)$/.exec(s);
|
|
6977
|
+
if (!match) {
|
|
6978
|
+
throw new TypeError(`canonicalJson: invalid number ${s}`);
|
|
6979
|
+
}
|
|
6980
|
+
const sign = match[1] ?? "";
|
|
6981
|
+
const intPart = match[2] ?? "";
|
|
6982
|
+
const fracPart = match[3] ?? "";
|
|
6983
|
+
const exp = Number(match[4]);
|
|
6984
|
+
const digits = intPart + fracPart;
|
|
6985
|
+
const point = intPart.length + exp;
|
|
6986
|
+
if (point <= 0) {
|
|
6987
|
+
return `${sign}0.${"0".repeat(-point)}${digits}`;
|
|
6988
|
+
}
|
|
6989
|
+
if (point >= digits.length) {
|
|
6990
|
+
return `${sign}${digits}${"0".repeat(point - digits.length)}`;
|
|
6991
|
+
}
|
|
6992
|
+
return `${sign}${digits.slice(0, point)}.${digits.slice(point)}`;
|
|
6640
6993
|
}
|
|
6641
6994
|
function serializeString(s) {
|
|
6642
6995
|
let result = '"';
|
|
@@ -6671,12 +7024,23 @@ function serializeArray(arr) {
|
|
|
6671
7024
|
return "[" + items.join(",") + "]";
|
|
6672
7025
|
}
|
|
6673
7026
|
function serializeObject(obj) {
|
|
6674
|
-
const sortedKeys = Object.keys(obj).sort();
|
|
7027
|
+
const sortedKeys = Object.keys(obj).sort(compareCodePoints);
|
|
6675
7028
|
const pairs = sortedKeys.map(
|
|
6676
7029
|
(key) => serializeString(key) + ":" + serialize(obj[key])
|
|
6677
7030
|
);
|
|
6678
7031
|
return "{" + pairs.join(",") + "}";
|
|
6679
7032
|
}
|
|
7033
|
+
function compareCodePoints(a, b) {
|
|
7034
|
+
const ac = Array.from(a);
|
|
7035
|
+
const bc = Array.from(b);
|
|
7036
|
+
const n = Math.min(ac.length, bc.length);
|
|
7037
|
+
for (let i = 0; i < n; i++) {
|
|
7038
|
+
const av = ac[i].codePointAt(0) ?? 0;
|
|
7039
|
+
const bv = bc[i].codePointAt(0) ?? 0;
|
|
7040
|
+
if (av !== bv) return av - bv;
|
|
7041
|
+
}
|
|
7042
|
+
return ac.length - bc.length;
|
|
7043
|
+
}
|
|
6680
7044
|
|
|
6681
7045
|
// node_modules/@noble/hashes/utils.js
|
|
6682
7046
|
function isBytes(a) {
|
|
@@ -8901,6 +9265,22 @@ function b64UrlToBytes(s) {
|
|
|
8901
9265
|
for (let i = 0; i < bin.length; i++) out[i] = bin.charCodeAt(i);
|
|
8902
9266
|
return out;
|
|
8903
9267
|
}
|
|
9268
|
+
function b64UrlToFixed32(s) {
|
|
9269
|
+
const raw = b64UrlToBytes(s);
|
|
9270
|
+
if (raw.length === 32) return raw;
|
|
9271
|
+
if (raw.length < 32) {
|
|
9272
|
+
const out = new Uint8Array(32);
|
|
9273
|
+
out.set(raw, 32 - raw.length);
|
|
9274
|
+
return out;
|
|
9275
|
+
}
|
|
9276
|
+
const extra = raw.length - 32;
|
|
9277
|
+
for (let i = 0; i < extra; i++) {
|
|
9278
|
+
if (raw[i] !== 0) {
|
|
9279
|
+
throw new Error(`invalid P-256 private scalar length=${raw.length}`);
|
|
9280
|
+
}
|
|
9281
|
+
}
|
|
9282
|
+
return raw.slice(extra);
|
|
9283
|
+
}
|
|
8904
9284
|
function p256PublicXY(privateKeyScalar) {
|
|
8905
9285
|
const pub = p256.getPublicKey(privateKeyScalar, false);
|
|
8906
9286
|
if (pub.length !== 65 || pub[0] !== 4) {
|
|
@@ -8957,7 +9337,7 @@ async function generateP256Keypair() {
|
|
|
8957
9337
|
if (!jwk.d) {
|
|
8958
9338
|
throw new Error("exportKey(jwk) returned no private component");
|
|
8959
9339
|
}
|
|
8960
|
-
const priv =
|
|
9340
|
+
const priv = b64UrlToFixed32(jwk.d);
|
|
8961
9341
|
const pubDerBuf = await crypto.subtle.exportKey("spki", keyPair.publicKey);
|
|
8962
9342
|
return [priv, new Uint8Array(pubDerBuf)];
|
|
8963
9343
|
}
|
|
@@ -9254,10 +9634,14 @@ function concatBytes3(...parts) {
|
|
|
9254
9634
|
}
|
|
9255
9635
|
return out;
|
|
9256
9636
|
}
|
|
9637
|
+
function scopedDeviceId(aid, deviceId) {
|
|
9638
|
+
return `aid:${encodeURIComponent(String(aid ?? ""))}|device:${encodeURIComponent(String(deviceId ?? ""))}`;
|
|
9639
|
+
}
|
|
9257
9640
|
var V2Session = class {
|
|
9258
9641
|
constructor(store, deviceId, aid, ikPriv, ikPubDer) {
|
|
9259
9642
|
__publicField(this, "_store");
|
|
9260
9643
|
__publicField(this, "_deviceId");
|
|
9644
|
+
__publicField(this, "_storeDeviceId");
|
|
9261
9645
|
__publicField(this, "_aid");
|
|
9262
9646
|
__publicField(this, "_ikPriv");
|
|
9263
9647
|
__publicField(this, "_ikPubDer");
|
|
@@ -9276,6 +9660,7 @@ var V2Session = class {
|
|
|
9276
9660
|
}
|
|
9277
9661
|
this._store = store;
|
|
9278
9662
|
this._deviceId = deviceId;
|
|
9663
|
+
this._storeDeviceId = scopedDeviceId(aid, deviceId);
|
|
9279
9664
|
this._aid = aid;
|
|
9280
9665
|
this._ikPriv = ikPriv;
|
|
9281
9666
|
this._ikPubDer = ikPubDer;
|
|
@@ -9302,8 +9687,9 @@ var V2Session = class {
|
|
|
9302
9687
|
}
|
|
9303
9688
|
/** 加载或生成当前 SPK;IK 由构造函数注入,无需加载。 */
|
|
9304
9689
|
async ensureKeys() {
|
|
9690
|
+
await this._store.saveIK(this._storeDeviceId, this._ikPriv, this._ikPubDer);
|
|
9305
9691
|
if (this._spkPriv) return;
|
|
9306
|
-
const cur = await this._store.loadCurrentSPK(this.
|
|
9692
|
+
const cur = await this._store.loadCurrentSPK(this._storeDeviceId);
|
|
9307
9693
|
if (cur) {
|
|
9308
9694
|
this._spkId = cur.spkId;
|
|
9309
9695
|
this._spkPriv = cur.priv;
|
|
@@ -9316,11 +9702,20 @@ var V2Session = class {
|
|
|
9316
9702
|
const [priv, pubDer] = await generateP256Keypair();
|
|
9317
9703
|
const hex = await sha256Hex(pubDer);
|
|
9318
9704
|
const spkId = `sha256:${hex.substring(0, 16)}`;
|
|
9319
|
-
await this._store.saveSPK(this.
|
|
9705
|
+
await this._store.saveSPK(this._storeDeviceId, spkId, priv, pubDer);
|
|
9320
9706
|
this._spkId = spkId;
|
|
9321
9707
|
this._spkPriv = priv;
|
|
9322
9708
|
this._spkPubDer = pubDer;
|
|
9323
9709
|
}
|
|
9710
|
+
async _ikSPKId() {
|
|
9711
|
+
const hex = await sha256Hex(this._ikPubDer);
|
|
9712
|
+
return `sha256:${hex.substring(0, 16)}`;
|
|
9713
|
+
}
|
|
9714
|
+
_normalizeGroupSPKLookup(groupId, spkId) {
|
|
9715
|
+
const nul = spkId.indexOf("\0");
|
|
9716
|
+
if (nul < 0) return { groupId, spkId };
|
|
9717
|
+
return { groupId: spkId.slice(0, nul).trim(), spkId: spkId.slice(nul + 1) };
|
|
9718
|
+
}
|
|
9324
9719
|
/** SPK 由 AID 私钥(IK)签名背书并上报到 message.v2.put_peer_pk。 */
|
|
9325
9720
|
async _registerSPK(callFn) {
|
|
9326
9721
|
const spkTimestamp = Math.floor(this._nowFn() / 1e3);
|
|
@@ -9344,7 +9739,14 @@ var V2Session = class {
|
|
|
9344
9739
|
async ensureRegistered(callFn) {
|
|
9345
9740
|
if (this._registered) return;
|
|
9346
9741
|
await this.ensureKeys();
|
|
9742
|
+
const uploadedSPKId = await this._store.loadLatestUploadedSPKId(this._storeDeviceId);
|
|
9743
|
+
if (uploadedSPKId) {
|
|
9744
|
+
this._registered = true;
|
|
9745
|
+
this._lastUploadedSPKId = uploadedSPKId;
|
|
9746
|
+
return;
|
|
9747
|
+
}
|
|
9347
9748
|
await this._registerSPK(callFn);
|
|
9749
|
+
await this._store.markSPKUploaded(this._storeDeviceId, this._spkId);
|
|
9348
9750
|
this._registered = true;
|
|
9349
9751
|
this._lastUploadedSPKId = this._spkId;
|
|
9350
9752
|
}
|
|
@@ -9361,16 +9763,47 @@ var V2Session = class {
|
|
|
9361
9763
|
/**
|
|
9362
9764
|
* 返回解密所需的私钥。
|
|
9363
9765
|
* - spkId 空:1DH(仅 IK)
|
|
9364
|
-
* - spkId ==
|
|
9365
|
-
* -
|
|
9766
|
+
* - spkId == 当前/历史 device SPK:对应 spkPriv
|
|
9767
|
+
* - spkId == IK 指纹:走 IK 特殊 fallback,返回 IK 私钥作为 spkPriv
|
|
9768
|
+
* - 否则:显式报 spk_missing
|
|
9366
9769
|
*/
|
|
9367
9770
|
async getDecryptKeys(spkId) {
|
|
9368
9771
|
await this.ensureKeys();
|
|
9369
9772
|
if (!spkId) return { ikPriv: this._ikPriv };
|
|
9370
9773
|
if (spkId === this._spkId) return { ikPriv: this._ikPriv, spkPriv: this._spkPriv };
|
|
9371
|
-
const oldSPK = await this.
|
|
9372
|
-
if (
|
|
9373
|
-
|
|
9774
|
+
const oldSPK = await this._loadSPK(spkId);
|
|
9775
|
+
if (oldSPK) return { ikPriv: this._ikPriv, spkPriv: oldSPK };
|
|
9776
|
+
const ikAlias = await this._loadIKSPK(spkId);
|
|
9777
|
+
if (ikAlias) return { ikPriv: ikAlias.priv, spkPriv: ikAlias.priv };
|
|
9778
|
+
if (spkId === await this._ikSPKId()) {
|
|
9779
|
+
await this._store.saveIK(this._storeDeviceId, this._ikPriv, this._ikPubDer);
|
|
9780
|
+
return { ikPriv: this._ikPriv, spkPriv: this._ikPriv };
|
|
9781
|
+
}
|
|
9782
|
+
throw new Error(`spk_missing: spk_id=${spkId}`);
|
|
9783
|
+
}
|
|
9784
|
+
async _loadSPK(spkId) {
|
|
9785
|
+
const scoped = await this._store.loadSPK(this._storeDeviceId, spkId);
|
|
9786
|
+
if (scoped) return scoped;
|
|
9787
|
+
if (this._storeDeviceId !== this._deviceId) {
|
|
9788
|
+
return this._store.loadSPK(this._deviceId, spkId);
|
|
9789
|
+
}
|
|
9790
|
+
return null;
|
|
9791
|
+
}
|
|
9792
|
+
async _loadIKSPK(spkId) {
|
|
9793
|
+
const scoped = await this._store.loadIKSPK(this._storeDeviceId, spkId);
|
|
9794
|
+
if (scoped) return scoped;
|
|
9795
|
+
if (this._storeDeviceId !== this._deviceId) {
|
|
9796
|
+
return this._store.loadIKSPK(this._deviceId, spkId);
|
|
9797
|
+
}
|
|
9798
|
+
return null;
|
|
9799
|
+
}
|
|
9800
|
+
async _loadGroupSPK(groupId, spkId) {
|
|
9801
|
+
const scoped = await this._store.loadGroupSPK(this._storeDeviceId, groupId, spkId);
|
|
9802
|
+
if (scoped) return scoped;
|
|
9803
|
+
if (this._storeDeviceId !== this._deviceId) {
|
|
9804
|
+
return this._store.loadGroupSPK(this._deviceId, groupId, spkId);
|
|
9805
|
+
}
|
|
9806
|
+
return null;
|
|
9374
9807
|
}
|
|
9375
9808
|
/** 判断 spkId 是否命中当前活跃 SPK。 */
|
|
9376
9809
|
isCurrentSPK(spkId) {
|
|
@@ -9399,7 +9832,7 @@ var V2Session = class {
|
|
|
9399
9832
|
let recentKeep;
|
|
9400
9833
|
try {
|
|
9401
9834
|
recentKeep = new Set(
|
|
9402
|
-
await this._store.listRecentSPKIds(this.
|
|
9835
|
+
await this._store.listRecentSPKIds(this._storeDeviceId, RECENT_GENERATIONS)
|
|
9403
9836
|
);
|
|
9404
9837
|
} catch {
|
|
9405
9838
|
recentKeep = /* @__PURE__ */ new Set();
|
|
@@ -9411,6 +9844,7 @@ var V2Session = class {
|
|
|
9411
9844
|
if (recentKeep.has(spkId)) continue;
|
|
9412
9845
|
try {
|
|
9413
9846
|
await this._store.deleteSPK(this._deviceId, spkId);
|
|
9847
|
+
await this._store.deleteSPK(this._storeDeviceId, spkId);
|
|
9414
9848
|
} catch (err) {
|
|
9415
9849
|
console.warn("[V2Session] deleteSPK failed", { spkId, err });
|
|
9416
9850
|
continue;
|
|
@@ -9420,9 +9854,14 @@ var V2Session = class {
|
|
|
9420
9854
|
}
|
|
9421
9855
|
try {
|
|
9422
9856
|
const expired = await this._store.listExpiredSPKIds(this._deviceId, HARD_LIMIT_MS);
|
|
9857
|
+
const scopedExpired = await this._store.listExpiredSPKIds(this._storeDeviceId, HARD_LIMIT_MS);
|
|
9858
|
+
for (const spkId of scopedExpired) {
|
|
9859
|
+
if (!expired.includes(spkId)) expired.push(spkId);
|
|
9860
|
+
}
|
|
9423
9861
|
for (const spkId of expired) {
|
|
9424
9862
|
if (spkId === this._spkId) continue;
|
|
9425
9863
|
try {
|
|
9864
|
+
await this._store.deleteSPK(this._storeDeviceId, spkId);
|
|
9426
9865
|
await this._store.deleteSPK(this._deviceId, spkId);
|
|
9427
9866
|
} catch {
|
|
9428
9867
|
continue;
|
|
@@ -9438,6 +9877,7 @@ var V2Session = class {
|
|
|
9438
9877
|
async rotateSPK(callFn) {
|
|
9439
9878
|
await this._generateNewSPK();
|
|
9440
9879
|
await this._registerSPK(callFn);
|
|
9880
|
+
await this._store.markSPKUploaded(this._storeDeviceId, this._spkId);
|
|
9441
9881
|
this._lastUploadedSPKId = this._spkId;
|
|
9442
9882
|
}
|
|
9443
9883
|
/** 判断 spkId 是否为本进程最后一次成功上传的 P2P SPK。 */
|
|
@@ -9448,25 +9888,31 @@ var V2Session = class {
|
|
|
9448
9888
|
isLastUploadedGroupSPK(groupId, spkId) {
|
|
9449
9889
|
if (!spkId) return false;
|
|
9450
9890
|
const gk = (groupId || "").trim();
|
|
9451
|
-
|
|
9891
|
+
const lookup = this._normalizeGroupSPKLookup(gk, spkId);
|
|
9892
|
+
return this._lastUploadedGroupSPKIds.get(lookup.groupId) === lookup.spkId;
|
|
9452
9893
|
}
|
|
9453
9894
|
// ---------- Group SPK ----------
|
|
9454
9895
|
/** 确保指定群有独立 group SPK,返回 { spkId, priv, pubDer }。 */
|
|
9455
9896
|
async ensureGroupSPK(groupId) {
|
|
9456
9897
|
await this.ensureKeys();
|
|
9457
9898
|
const gk = (groupId || "").trim();
|
|
9458
|
-
const existing = await this._store.loadCurrentGroupSPK(this.
|
|
9899
|
+
const existing = await this._store.loadCurrentGroupSPK(this._storeDeviceId, gk);
|
|
9459
9900
|
if (existing) return existing;
|
|
9460
9901
|
const [priv, pubDer] = await generateP256Keypair();
|
|
9461
9902
|
const hex = await sha256Hex(pubDer);
|
|
9462
9903
|
const spkId = `sha256:${hex.substring(0, 16)}`;
|
|
9463
|
-
await this._store.saveGroupSPK(this.
|
|
9904
|
+
await this._store.saveGroupSPK(this._storeDeviceId, gk, spkId, priv, pubDer);
|
|
9464
9905
|
return { spkId, priv, pubDer };
|
|
9465
9906
|
}
|
|
9466
9907
|
/** 注册指定群的 group SPK。group 服务负责成员鉴权。 */
|
|
9467
9908
|
async ensureGroupRegistered(groupId, callFn) {
|
|
9468
9909
|
await this.ensureKeys();
|
|
9469
9910
|
const gk = (groupId || "").trim();
|
|
9911
|
+
const uploadedSPKId = await this._store.loadLatestUploadedGroupSPKId(this._storeDeviceId, gk);
|
|
9912
|
+
if (uploadedSPKId) {
|
|
9913
|
+
this._lastUploadedGroupSPKIds.set(gk, uploadedSPKId);
|
|
9914
|
+
return;
|
|
9915
|
+
}
|
|
9470
9916
|
const { spkId, pubDer } = await this.ensureGroupSPK(gk);
|
|
9471
9917
|
await this._publishGroupSPK(gk, spkId, pubDer, callFn);
|
|
9472
9918
|
}
|
|
@@ -9477,18 +9923,19 @@ var V2Session = class {
|
|
|
9477
9923
|
const [priv, pubDer] = await generateP256Keypair();
|
|
9478
9924
|
const hex = await sha256Hex(pubDer);
|
|
9479
9925
|
const spkId = `sha256:${hex.substring(0, 16)}`;
|
|
9480
|
-
await this._store.saveGroupSPK(this.
|
|
9926
|
+
await this._store.saveGroupSPK(this._storeDeviceId, gk, spkId, priv, pubDer);
|
|
9481
9927
|
await this._publishGroupSPK(gk, spkId, pubDer, callFn);
|
|
9482
9928
|
return { spkId, priv, pubDer };
|
|
9483
9929
|
}
|
|
9484
|
-
/**
|
|
9930
|
+
/** 群消息解密按 group SPK -> device SPK -> IK fallback;仍找不到则显式报错。 */
|
|
9485
9931
|
async getGroupDecryptKeys(groupId, spkId) {
|
|
9486
9932
|
await this.ensureKeys();
|
|
9487
9933
|
const gk = (groupId || "").trim();
|
|
9488
9934
|
if (!spkId) return { ikPriv: this._ikPriv };
|
|
9489
|
-
const
|
|
9935
|
+
const lookup = this._normalizeGroupSPKLookup(gk, spkId);
|
|
9936
|
+
const groupSpk = await this._loadGroupSPK(lookup.groupId, lookup.spkId);
|
|
9490
9937
|
if (groupSpk) return { ikPriv: this._ikPriv, spkPriv: groupSpk };
|
|
9491
|
-
return this.getDecryptKeys(spkId);
|
|
9938
|
+
return this.getDecryptKeys(lookup.spkId);
|
|
9492
9939
|
}
|
|
9493
9940
|
async _publishGroupSPK(groupId, spkId, spkPubDer, callFn) {
|
|
9494
9941
|
const spkTimestamp = Math.floor(this._nowFn() / 1e3);
|
|
@@ -9507,6 +9954,7 @@ var V2Session = class {
|
|
|
9507
9954
|
spk_signature: bytesToBase64(signature),
|
|
9508
9955
|
spk_timestamp: spkTimestamp
|
|
9509
9956
|
});
|
|
9957
|
+
await this._store.markGroupSPKUploaded(this._storeDeviceId, groupId, spkId);
|
|
9510
9958
|
this._lastUploadedGroupSPKIds.set(groupId, spkId);
|
|
9511
9959
|
}
|
|
9512
9960
|
cachePeerIK(peerAid, deviceId, ikPubDer) {
|
|
@@ -9546,6 +9994,18 @@ function bytesToBase642(b) {
|
|
|
9546
9994
|
for (let i = 0; i < b.length; i++) bin += String.fromCharCode(b[i]);
|
|
9547
9995
|
return btoa(bin);
|
|
9548
9996
|
}
|
|
9997
|
+
function base64ToBytes(s) {
|
|
9998
|
+
const bin = atob(s);
|
|
9999
|
+
const out = new Uint8Array(bin.length);
|
|
10000
|
+
for (let i = 0; i < bin.length; i++) out[i] = bin.charCodeAt(i);
|
|
10001
|
+
return out;
|
|
10002
|
+
}
|
|
10003
|
+
function bytesEqual(a, b) {
|
|
10004
|
+
if (a.length !== b.length) return false;
|
|
10005
|
+
let diff = 0;
|
|
10006
|
+
for (let i = 0; i < a.length; i++) diff |= a[i] ^ b[i];
|
|
10007
|
+
return diff === 0;
|
|
10008
|
+
}
|
|
9549
10009
|
async function hmacSha256(key, data) {
|
|
9550
10010
|
const hmacKey = await crypto.subtle.importKey(
|
|
9551
10011
|
"raw",
|
|
@@ -9581,9 +10041,45 @@ async function withMetadataAuth(metadata, key, domain) {
|
|
|
9581
10041
|
}
|
|
9582
10042
|
};
|
|
9583
10043
|
}
|
|
10044
|
+
async function verifyMetadataAuth(metadata, key, domain, fieldName) {
|
|
10045
|
+
if (metadata == null) return;
|
|
10046
|
+
if (!isPlainObject(metadata)) {
|
|
10047
|
+
throw new Error(`${fieldName} must be an object`);
|
|
10048
|
+
}
|
|
10049
|
+
const body = {};
|
|
10050
|
+
for (const [k, v] of Object.entries(metadata)) {
|
|
10051
|
+
if (k !== "_auth") body[k] = v;
|
|
10052
|
+
}
|
|
10053
|
+
if (Object.keys(body).length === 0) return;
|
|
10054
|
+
const auth = metadata._auth;
|
|
10055
|
+
if (!isPlainObject(auth)) {
|
|
10056
|
+
throw new Error(`${fieldName} missing _auth`);
|
|
10057
|
+
}
|
|
10058
|
+
if (auth.alg !== "HMAC-SHA256") {
|
|
10059
|
+
throw new Error(`${fieldName} unsupported _auth alg`);
|
|
10060
|
+
}
|
|
10061
|
+
if (typeof auth.tag !== "string" || auth.tag.length === 0) {
|
|
10062
|
+
throw new Error(`${fieldName} missing _auth tag`);
|
|
10063
|
+
}
|
|
10064
|
+
const actual = base64ToBytes(auth.tag);
|
|
10065
|
+
const expected = await metadataAuthTag(key, domain, body);
|
|
10066
|
+
if (!bytesEqual(actual, expected)) {
|
|
10067
|
+
throw new Error(`${fieldName} _auth verification failed`);
|
|
10068
|
+
}
|
|
10069
|
+
}
|
|
10070
|
+
function isPlainObject(value) {
|
|
10071
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
10072
|
+
return false;
|
|
10073
|
+
}
|
|
10074
|
+
const proto = Object.getPrototypeOf(value);
|
|
10075
|
+
return proto === Object.prototype || proto === null;
|
|
10076
|
+
}
|
|
9584
10077
|
|
|
9585
10078
|
// src/v2/e2ee/encrypt-p2p.ts
|
|
9586
10079
|
var encoder3 = new TextEncoder();
|
|
10080
|
+
var decoder = new TextDecoder();
|
|
10081
|
+
var E2EE_SDK_LANG = "javascript";
|
|
10082
|
+
var E2EE_SDK_VERSION = "0.3.2";
|
|
9587
10083
|
async function sha2563(data) {
|
|
9588
10084
|
const buf = await crypto.subtle.digest("SHA-256", data.slice().buffer);
|
|
9589
10085
|
return new Uint8Array(buf);
|
|
@@ -9642,7 +10138,7 @@ async function encryptP2PMessage(sender, targetSet, payload, opts = {}) {
|
|
|
9642
10138
|
];
|
|
9643
10139
|
const protocolSet = /* @__PURE__ */ new Set();
|
|
9644
10140
|
for (const t of allTargetsForProto) {
|
|
9645
|
-
if (
|
|
10141
|
+
if (usesSPKWrap(t)) {
|
|
9646
10142
|
protocolSet.add("3DH");
|
|
9647
10143
|
} else {
|
|
9648
10144
|
protocolSet.add("1DH");
|
|
@@ -9727,6 +10223,10 @@ async function encryptP2PMessage(sender, targetSet, payload, opts = {}) {
|
|
|
9727
10223
|
recipients: sortedRows,
|
|
9728
10224
|
aad
|
|
9729
10225
|
};
|
|
10226
|
+
const payloadType = payload?.type == null ? "" : String(payload.type);
|
|
10227
|
+
if (payloadType) {
|
|
10228
|
+
envelope.payload_type = payloadType;
|
|
10229
|
+
}
|
|
9730
10230
|
const normalizedHeaders = normalizeProtectedHeaders(opts.protectedHeaders, payload);
|
|
9731
10231
|
if (Object.keys(normalizedHeaders).length > 0) {
|
|
9732
10232
|
envelope.protected_headers = await withMetadataAuth(normalizedHeaders, masterKey, PROTECTED_HEADERS_DOMAIN);
|
|
@@ -9741,25 +10241,43 @@ function normalizeProtectedHeaders(headers, payload) {
|
|
|
9741
10241
|
const normalized = {};
|
|
9742
10242
|
if (headers && typeof headers === "object") {
|
|
9743
10243
|
for (const [k, v] of Object.entries(headers)) {
|
|
9744
|
-
|
|
9745
|
-
const sv = v != null ? String(v) : "";
|
|
9746
|
-
if (sv) normalized[k] = sv;
|
|
10244
|
+
normalized[normalizeProtectedHeaderKey(k)] = normalizeProtectedHeaderValue(v);
|
|
9747
10245
|
}
|
|
9748
10246
|
}
|
|
9749
10247
|
const payloadType = typeof payload?.type === "string" ? payload.type : "";
|
|
9750
10248
|
if (payloadType && !("payload_type" in normalized)) {
|
|
9751
10249
|
normalized["payload_type"] = payloadType;
|
|
9752
10250
|
}
|
|
10251
|
+
normalized.sdk_lang = E2EE_SDK_LANG;
|
|
10252
|
+
normalized.sdk_vesion = E2EE_SDK_VERSION;
|
|
9753
10253
|
return normalized;
|
|
9754
10254
|
}
|
|
10255
|
+
function normalizeProtectedHeaderKey(key) {
|
|
10256
|
+
const value = String(key ?? "").trim().toLowerCase();
|
|
10257
|
+
if (!value || !/^[a-z0-9_-]+$/.test(value)) {
|
|
10258
|
+
throw new Error("protected header key must match [a-z0-9_-]+");
|
|
10259
|
+
}
|
|
10260
|
+
if (value === "_auth") {
|
|
10261
|
+
throw new Error("protected header key is reserved");
|
|
10262
|
+
}
|
|
10263
|
+
return value;
|
|
10264
|
+
}
|
|
10265
|
+
function normalizeProtectedHeaderValue(value) {
|
|
10266
|
+
if (value == null) return "";
|
|
10267
|
+
if (typeof value === "string") return value;
|
|
10268
|
+
return decoder.decode(canonicalJson(value));
|
|
10269
|
+
}
|
|
9755
10270
|
async function wrapForRecipient(target, masterKey, senderSessionPriv, senderMasterPriv, wrapSalt, defaultRole) {
|
|
9756
10271
|
const role = target.role ?? defaultRole;
|
|
9757
10272
|
const keySource = target.keySource ?? "aid_master";
|
|
9758
10273
|
const fpHash = bytesToHex4(await sha2563(target.ikPkDer));
|
|
9759
10274
|
const fp = `sha256:${fpHash.substring(0, 16)}`;
|
|
9760
10275
|
const wrapNonce = randomBytes3(12);
|
|
10276
|
+
const use3DH = usesSPKWrap(target);
|
|
10277
|
+
const rowKeySource = use3DH ? keySource : "aid_master";
|
|
10278
|
+
const rowSpkId = use3DH ? target.spkId ?? "" : "";
|
|
9761
10279
|
let wrapKey;
|
|
9762
|
-
if (
|
|
10280
|
+
if (use3DH) {
|
|
9763
10281
|
wrapKey = await compute3DHWrap(
|
|
9764
10282
|
senderSessionPriv,
|
|
9765
10283
|
senderMasterPriv,
|
|
@@ -9783,13 +10301,18 @@ async function wrapForRecipient(target, masterKey, senderSessionPriv, senderMast
|
|
|
9783
10301
|
target.aid,
|
|
9784
10302
|
target.deviceId,
|
|
9785
10303
|
role,
|
|
9786
|
-
|
|
10304
|
+
rowKeySource,
|
|
9787
10305
|
fp,
|
|
9788
|
-
|
|
10306
|
+
rowSpkId,
|
|
9789
10307
|
bytesToBase643(wrapNonce),
|
|
9790
10308
|
bytesToBase643(wrappedKey)
|
|
9791
10309
|
];
|
|
9792
10310
|
}
|
|
10311
|
+
function usesSPKWrap(target) {
|
|
10312
|
+
return Boolean(
|
|
10313
|
+
target.spkId && target.spkPkDer && (target.keySource === "peer_device_prekey" || target.keySource === "group_device_prekey")
|
|
10314
|
+
);
|
|
10315
|
+
}
|
|
9793
10316
|
|
|
9794
10317
|
// src/v2/e2ee/encrypt-group.ts
|
|
9795
10318
|
var encoder4 = new TextEncoder();
|
|
@@ -9840,7 +10363,7 @@ async function encryptGroupMessage(sender, groupId, epoch, targets, payload, opt
|
|
|
9840
10363
|
const timestamp = opts.timestamp ?? Date.now();
|
|
9841
10364
|
const protocolSet = /* @__PURE__ */ new Set();
|
|
9842
10365
|
for (const t of targets) {
|
|
9843
|
-
if (
|
|
10366
|
+
if (usesSPKWrap2(t)) {
|
|
9844
10367
|
protocolSet.add("3DH");
|
|
9845
10368
|
} else {
|
|
9846
10369
|
protocolSet.add("1DH");
|
|
@@ -9918,6 +10441,10 @@ async function encryptGroupMessage(sender, groupId, epoch, targets, payload, opt
|
|
|
9918
10441
|
recipients: sortedRows,
|
|
9919
10442
|
aad
|
|
9920
10443
|
};
|
|
10444
|
+
const payloadType = payload?.type == null ? "" : String(payload.type);
|
|
10445
|
+
if (payloadType) {
|
|
10446
|
+
envelope.payload_type = payloadType;
|
|
10447
|
+
}
|
|
9921
10448
|
const { context } = opts;
|
|
9922
10449
|
const normalizedHeaders = normalizeProtectedHeaders(opts.protectedHeaders, payload);
|
|
9923
10450
|
if (Object.keys(normalizedHeaders).length > 0) {
|
|
@@ -9934,8 +10461,11 @@ async function wrapForRecipient2(target, masterKey, senderSessionPriv, senderMas
|
|
|
9934
10461
|
const fpHash = bytesToHex5(await sha2564(target.ikPkDer));
|
|
9935
10462
|
const fp = `sha256:${fpHash.substring(0, 16)}`;
|
|
9936
10463
|
const wrapNonce = randomBytes4(12);
|
|
10464
|
+
const use3DH = usesSPKWrap2(target);
|
|
10465
|
+
const rowKeySource = use3DH ? keySource : "aid_master";
|
|
10466
|
+
const rowSpkId = use3DH ? target.spkId ?? "" : "";
|
|
9937
10467
|
let wrapKey;
|
|
9938
|
-
if (
|
|
10468
|
+
if (use3DH) {
|
|
9939
10469
|
wrapKey = await compute3DHWrap(
|
|
9940
10470
|
senderSessionPriv,
|
|
9941
10471
|
senderMasterPriv,
|
|
@@ -9959,13 +10489,18 @@ async function wrapForRecipient2(target, masterKey, senderSessionPriv, senderMas
|
|
|
9959
10489
|
target.aid,
|
|
9960
10490
|
target.deviceId,
|
|
9961
10491
|
role,
|
|
9962
|
-
|
|
10492
|
+
rowKeySource,
|
|
9963
10493
|
fp,
|
|
9964
|
-
|
|
10494
|
+
rowSpkId,
|
|
9965
10495
|
bytesToBase644(wrapNonce),
|
|
9966
10496
|
bytesToBase644(wrappedKey)
|
|
9967
10497
|
];
|
|
9968
10498
|
}
|
|
10499
|
+
function usesSPKWrap2(target) {
|
|
10500
|
+
return Boolean(
|
|
10501
|
+
target.spkId && target.spkPkDer && (target.keySource === "peer_device_prekey" || target.keySource === "group_device_prekey")
|
|
10502
|
+
);
|
|
10503
|
+
}
|
|
9969
10504
|
|
|
9970
10505
|
// src/v2/e2ee/decrypt.ts
|
|
9971
10506
|
var encoder5 = new TextEncoder();
|
|
@@ -9975,7 +10510,7 @@ async function sha2565(data) {
|
|
|
9975
10510
|
const buf = await crypto.subtle.digest("SHA-256", data.slice().buffer);
|
|
9976
10511
|
return new Uint8Array(buf);
|
|
9977
10512
|
}
|
|
9978
|
-
function
|
|
10513
|
+
function base64ToBytes2(s) {
|
|
9979
10514
|
const bin = atob(s);
|
|
9980
10515
|
const out = new Uint8Array(bin.length);
|
|
9981
10516
|
for (let i = 0; i < bin.length; i++) out[i] = bin.charCodeAt(i);
|
|
@@ -10030,7 +10565,7 @@ async function decryptMessage(envelope, selfAid, selfDeviceId, selfIkPriv, selfS
|
|
|
10030
10565
|
} else {
|
|
10031
10566
|
return null;
|
|
10032
10567
|
}
|
|
10033
|
-
const senderSessionPkDer =
|
|
10568
|
+
const senderSessionPkDer = base64ToBytes2(env.sender_session_pk);
|
|
10034
10569
|
const aadBytes = canonicalJson(env.aad);
|
|
10035
10570
|
const suiteStr = env.suite ?? SUITE_NAME;
|
|
10036
10571
|
const suiteBytes = encoder5.encode(suiteStr);
|
|
@@ -10049,33 +10584,49 @@ async function decryptMessage(envelope, selfAid, selfDeviceId, selfIkPriv, selfS
|
|
|
10049
10584
|
senderPubDer,
|
|
10050
10585
|
wrapSalt
|
|
10051
10586
|
);
|
|
10052
|
-
const wrapNonce =
|
|
10053
|
-
const wrappedKey =
|
|
10587
|
+
const wrapNonce = base64ToBytes2(row[6]);
|
|
10588
|
+
const wrappedKey = base64ToBytes2(row[7]);
|
|
10054
10589
|
if (wrappedKey.length < 16) {
|
|
10055
10590
|
throw new Error(`wrapped_key too short: ${wrappedKey.length}`);
|
|
10056
10591
|
}
|
|
10057
10592
|
const wrappedCt = wrappedKey.subarray(0, wrappedKey.length - 16);
|
|
10058
10593
|
const wrappedTag = wrappedKey.subarray(wrappedKey.length - 16);
|
|
10059
|
-
|
|
10060
|
-
|
|
10061
|
-
|
|
10062
|
-
|
|
10063
|
-
|
|
10064
|
-
|
|
10065
|
-
|
|
10066
|
-
|
|
10067
|
-
|
|
10068
|
-
|
|
10069
|
-
|
|
10070
|
-
|
|
10071
|
-
|
|
10072
|
-
|
|
10073
|
-
|
|
10074
|
-
|
|
10075
|
-
const
|
|
10076
|
-
const
|
|
10077
|
-
const
|
|
10078
|
-
|
|
10594
|
+
let masterKey;
|
|
10595
|
+
try {
|
|
10596
|
+
masterKey = await aesGcmDecrypt(
|
|
10597
|
+
wrapKey,
|
|
10598
|
+
wrapNonce,
|
|
10599
|
+
wrappedCt,
|
|
10600
|
+
wrappedTag,
|
|
10601
|
+
new Uint8Array(0)
|
|
10602
|
+
);
|
|
10603
|
+
} catch (exc) {
|
|
10604
|
+
throw new Error(
|
|
10605
|
+
`wrap_key_decrypt_failed: ${rowContext(row)}; master_key unwrap AEAD authentication failed; likely wrong local SPK/IK, stale sender bootstrap, or tampered recipient wrap; cause=${formatCaught(exc)}`
|
|
10606
|
+
);
|
|
10607
|
+
}
|
|
10608
|
+
await verifyMetadataAuth(env.protected_headers, masterKey, PROTECTED_HEADERS_DOMAIN, "protected_headers");
|
|
10609
|
+
await verifyMetadataAuth(env.context, masterKey, PROTECTED_CONTEXT_DOMAIN, "context");
|
|
10610
|
+
const msgNonce = base64ToBytes2(env.nonce);
|
|
10611
|
+
const ct = base64ToBytes2(env.ciphertext);
|
|
10612
|
+
const tag = base64ToBytes2(env.tag);
|
|
10613
|
+
let plaintext;
|
|
10614
|
+
try {
|
|
10615
|
+
plaintext = await aesGcmDecrypt(masterKey, msgNonce, ct, tag, aadBytes);
|
|
10616
|
+
} catch (exc) {
|
|
10617
|
+
throw new Error(
|
|
10618
|
+
`body_decrypt_failed: ${envelopeContext(env, row)}; message body AEAD authentication failed after master_key unwrap; likely AAD/ciphertext/tag mismatch or envelope body corruption; cause=${formatCaught(exc)}`
|
|
10619
|
+
);
|
|
10620
|
+
}
|
|
10621
|
+
return JSON.parse(new TextDecoder().decode(plaintext));
|
|
10622
|
+
}
|
|
10623
|
+
async function verifySenderSignature(env, senderPubDer) {
|
|
10624
|
+
const sig = base64ToBytes2(env.sender_signature);
|
|
10625
|
+
const ct = base64ToBytes2(env.ciphertext);
|
|
10626
|
+
const tag = base64ToBytes2(env.tag);
|
|
10627
|
+
const aadBytes = canonicalJson(env.aad);
|
|
10628
|
+
const digestBytes = hexToBytes6(env.recipients_digest);
|
|
10629
|
+
const signInput = new Uint8Array(
|
|
10079
10630
|
ct.length + tag.length + aadBytes.length + digestBytes.length
|
|
10080
10631
|
);
|
|
10081
10632
|
let pos = 0;
|
|
@@ -10094,8 +10645,39 @@ function findMyRow(recipients, selfAid, selfDeviceId) {
|
|
|
10094
10645
|
}
|
|
10095
10646
|
return null;
|
|
10096
10647
|
}
|
|
10648
|
+
function formatCaught(exc) {
|
|
10649
|
+
if (exc instanceof Error) {
|
|
10650
|
+
return exc.message ? `${exc.name}: ${exc.message}` : exc.name;
|
|
10651
|
+
}
|
|
10652
|
+
return String(exc);
|
|
10653
|
+
}
|
|
10654
|
+
function rowContext(row) {
|
|
10655
|
+
return [
|
|
10656
|
+
`recipient=${String(row[0] ?? "")}/${String(row[1] ?? "")}`,
|
|
10657
|
+
`role=${String(row[2] ?? "")}`,
|
|
10658
|
+
`key_source=${String(row[3] ?? "")}`,
|
|
10659
|
+
`spk_id=${String(row[5] ?? "") || "<empty>"}`
|
|
10660
|
+
].join("; ");
|
|
10661
|
+
}
|
|
10662
|
+
function envelopeContext(env, row) {
|
|
10663
|
+
const aad = env.aad && typeof env.aad === "object" && !Array.isArray(env.aad) ? env.aad : {};
|
|
10664
|
+
const messageId = String(aad.message_id ?? "");
|
|
10665
|
+
const groupId = String(aad.group_id ?? "") || "<p2p>";
|
|
10666
|
+
const from = String(aad.from ?? "");
|
|
10667
|
+
const fromDevice = String(aad.from_device ?? "");
|
|
10668
|
+
return [
|
|
10669
|
+
`message_id=${messageId}`,
|
|
10670
|
+
`group_id=${groupId}`,
|
|
10671
|
+
`from=${from}`,
|
|
10672
|
+
`from_device=${fromDevice}`,
|
|
10673
|
+
rowContext(row)
|
|
10674
|
+
].join("; ");
|
|
10675
|
+
}
|
|
10097
10676
|
async function computeWrapKey(row, selfIkPriv, selfSpkPriv, senderSessionPkDer, senderMasterPkDer, salt) {
|
|
10098
10677
|
const spkId = row[5];
|
|
10678
|
+
if (spkId && !selfSpkPriv) {
|
|
10679
|
+
throw new Error(`spk_missing: spk_id=${spkId}`);
|
|
10680
|
+
}
|
|
10099
10681
|
if (spkId && selfSpkPriv) {
|
|
10100
10682
|
const dh12 = await ecdhComputeShared(selfIkPriv, senderSessionPkDer);
|
|
10101
10683
|
const dh2 = await ecdhComputeShared(selfSpkPriv, senderMasterPkDer);
|
|
@@ -10146,11 +10728,14 @@ function sortPayload(payload) {
|
|
|
10146
10728
|
}
|
|
10147
10729
|
}
|
|
10148
10730
|
async function computeStateCommitment(groupId, epoch, statePayload) {
|
|
10731
|
+
if (!Number.isInteger(epoch) || epoch < 0 || epoch > 4294967295) {
|
|
10732
|
+
throw new Error(`epoch out of uint32 range: ${epoch}`);
|
|
10733
|
+
}
|
|
10149
10734
|
const sorted = deepClone2(statePayload);
|
|
10150
10735
|
sortPayload(sorted);
|
|
10151
10736
|
const groupBytes = new TextEncoder().encode(groupId);
|
|
10152
10737
|
const epochBytes = new Uint8Array(4);
|
|
10153
|
-
new DataView(epochBytes.buffer).setUint32(0, epoch
|
|
10738
|
+
new DataView(epochBytes.buffer).setUint32(0, epoch, false);
|
|
10154
10739
|
const payloadBytes = canonicalJson(sorted);
|
|
10155
10740
|
const total = STATE_PREFIX.length + groupBytes.length + 4 + payloadBytes.length;
|
|
10156
10741
|
const data = new Uint8Array(total);
|
|
@@ -10195,9 +10780,12 @@ function formatMessage(template, args) {
|
|
|
10195
10780
|
var AUNLogger = class {
|
|
10196
10781
|
constructor(opts) {
|
|
10197
10782
|
__publicField(this, "_debug");
|
|
10783
|
+
__publicField(this, "_aunPath");
|
|
10784
|
+
__publicField(this, "_deviceId", "-");
|
|
10198
10785
|
__publicField(this, "_aid", null);
|
|
10199
10786
|
__publicField(this, "_minLevel");
|
|
10200
10787
|
this._debug = opts.debug;
|
|
10788
|
+
this._aunPath = String(opts.aunPath || "-");
|
|
10201
10789
|
this._minLevel = this._debug ? LEVEL_ORDER.DEBUG : LEVEL_ORDER.INFO;
|
|
10202
10790
|
}
|
|
10203
10791
|
for(module) {
|
|
@@ -10211,13 +10799,16 @@ var AUNLogger = class {
|
|
|
10211
10799
|
bindAid(aid) {
|
|
10212
10800
|
this._aid = aid || null;
|
|
10213
10801
|
}
|
|
10802
|
+
bindDeviceId(deviceId) {
|
|
10803
|
+
this._deviceId = String(deviceId || "").trim() || "-";
|
|
10804
|
+
}
|
|
10214
10805
|
close() {
|
|
10215
10806
|
}
|
|
10216
10807
|
_emit(level, module, msg, args) {
|
|
10217
10808
|
if (LEVEL_ORDER[level] < this._minLevel) return;
|
|
10218
10809
|
if (level === "DEBUG" && !this._debug) return;
|
|
10219
10810
|
const { date, time, ms } = this._now();
|
|
10220
|
-
const head = `[${date} ${time}.${ms}][${level}][${module}]`;
|
|
10811
|
+
const head = `[${date} ${time}.${ms}][${level}][${module}][aun_path=${this._aunPath || "-"}][device_id=${this._deviceId || "-"}]`;
|
|
10221
10812
|
const aidPart = this._aid ? ` [${this._aid}]` : "";
|
|
10222
10813
|
const formatted = formatMessage(msg, args);
|
|
10223
10814
|
const line = `${head}${aidPart} ${formatted}`;
|
|
@@ -10272,6 +10863,15 @@ function stableStringify(obj) {
|
|
|
10272
10863
|
}
|
|
10273
10864
|
return JSON.stringify(obj);
|
|
10274
10865
|
}
|
|
10866
|
+
function getV2DeviceId(dev) {
|
|
10867
|
+
if (Object.prototype.hasOwnProperty.call(dev, "device_id")) {
|
|
10868
|
+
return { present: true, value: String(dev.device_id ?? "").trim() };
|
|
10869
|
+
}
|
|
10870
|
+
if (Object.prototype.hasOwnProperty.call(dev, "owner_device_id")) {
|
|
10871
|
+
return { present: true, value: String(dev.owner_device_id ?? "").trim() };
|
|
10872
|
+
}
|
|
10873
|
+
return { present: false, value: "" };
|
|
10874
|
+
}
|
|
10275
10875
|
function sortObjectKeys(obj) {
|
|
10276
10876
|
if (obj === null || obj === void 0 || typeof obj !== "object") return obj;
|
|
10277
10877
|
if (Array.isArray(obj)) return obj.map(sortObjectKeys);
|
|
@@ -10342,7 +10942,21 @@ var REMOVED_E2EE_METHODS = /* @__PURE__ */ new Set([
|
|
|
10342
10942
|
"group.rotate_epoch"
|
|
10343
10943
|
]);
|
|
10344
10944
|
var SIGNED_METHODS = /* @__PURE__ */ new Set([
|
|
10945
|
+
"message.send",
|
|
10946
|
+
"message.v2.put_peer_pk",
|
|
10947
|
+
"message.v2.bootstrap",
|
|
10948
|
+
"message.v2.group_bootstrap",
|
|
10949
|
+
"message.v2.pull",
|
|
10950
|
+
"message.v2.ack",
|
|
10345
10951
|
"group.send",
|
|
10952
|
+
"group.v2.put_group_pk",
|
|
10953
|
+
"group.v2.bootstrap",
|
|
10954
|
+
"group.v2.send",
|
|
10955
|
+
"group.v2.pull",
|
|
10956
|
+
"group.v2.ack",
|
|
10957
|
+
"group.v2.propose_state",
|
|
10958
|
+
"group.v2.confirm_state",
|
|
10959
|
+
"group.v2.get_proposal",
|
|
10346
10960
|
"group.kick",
|
|
10347
10961
|
"group.add_member",
|
|
10348
10962
|
"group.leave",
|
|
@@ -10491,6 +11105,41 @@ function _v2B64ToBytes(s) {
|
|
|
10491
11105
|
for (let i = 0; i < bin.length; i++) out[i] = bin.charCodeAt(i);
|
|
10492
11106
|
return out;
|
|
10493
11107
|
}
|
|
11108
|
+
function _v2B64ToBytesStrict(s) {
|
|
11109
|
+
const text = String(s ?? "").trim();
|
|
11110
|
+
if (!text || text.length % 4 === 1 || !/^[A-Za-z0-9+/]*={0,2}$/.test(text)) {
|
|
11111
|
+
throw new Error("invalid base64");
|
|
11112
|
+
}
|
|
11113
|
+
return _v2B64ToBytes(text);
|
|
11114
|
+
}
|
|
11115
|
+
function _v2BytesEqual(a, b) {
|
|
11116
|
+
if (a.length !== b.length) return false;
|
|
11117
|
+
let diff = 0;
|
|
11118
|
+
for (let i = 0; i < a.length; i++) diff |= a[i] ^ b[i];
|
|
11119
|
+
return diff === 0;
|
|
11120
|
+
}
|
|
11121
|
+
function _v2ConcatBytes(...parts) {
|
|
11122
|
+
const total = parts.reduce((sum, part) => sum + part.length, 0);
|
|
11123
|
+
const out = new Uint8Array(total);
|
|
11124
|
+
let offset = 0;
|
|
11125
|
+
for (const part of parts) {
|
|
11126
|
+
out.set(part, offset);
|
|
11127
|
+
offset += part.length;
|
|
11128
|
+
}
|
|
11129
|
+
return out;
|
|
11130
|
+
}
|
|
11131
|
+
function _v2LengthPrefixedTextKey(...parts) {
|
|
11132
|
+
const enc = new TextEncoder();
|
|
11133
|
+
return parts.map((part) => `${enc.encode(part).length}:${part};`).join("");
|
|
11134
|
+
}
|
|
11135
|
+
function _v2LengthPrefixedBytes(...parts) {
|
|
11136
|
+
const enc = new TextEncoder();
|
|
11137
|
+
const framed = [];
|
|
11138
|
+
for (const part of parts) {
|
|
11139
|
+
framed.push(enc.encode(`${part.length}:`), part, enc.encode(";"));
|
|
11140
|
+
}
|
|
11141
|
+
return _v2ConcatBytes(...framed);
|
|
11142
|
+
}
|
|
10494
11143
|
function _v2B64uToBytes(s) {
|
|
10495
11144
|
const std = s.replace(/-/g, "+").replace(/_/g, "/");
|
|
10496
11145
|
const pad = std.length % 4 === 0 ? "" : "=".repeat(4 - std.length % 4);
|
|
@@ -10502,12 +11151,65 @@ function formatCaughtError(error) {
|
|
|
10502
11151
|
function v2E2eeMeta(envelope) {
|
|
10503
11152
|
const suite = String(envelope.suite ?? "");
|
|
10504
11153
|
const modeSuite = String(envelope.suite ?? "unknown");
|
|
10505
|
-
|
|
11154
|
+
const meta = {
|
|
10506
11155
|
version: "v2",
|
|
10507
11156
|
suite,
|
|
10508
11157
|
encryption_mode: `v2_${modeSuite}`,
|
|
10509
11158
|
forward_secrecy: true
|
|
10510
11159
|
};
|
|
11160
|
+
const protectedHeaders = metadataWithoutAuth(envelope.protected_headers);
|
|
11161
|
+
if (protectedHeaders && Object.keys(protectedHeaders).length > 0) {
|
|
11162
|
+
meta.protected_headers = protectedHeaders;
|
|
11163
|
+
}
|
|
11164
|
+
const payloadType = String(envelope.payload_type ?? protectedHeaders?.payload_type ?? "").trim();
|
|
11165
|
+
if (payloadType) {
|
|
11166
|
+
meta.payload_type = payloadType;
|
|
11167
|
+
}
|
|
11168
|
+
const context = metadataWithoutAuth(envelope.context);
|
|
11169
|
+
if (context && Object.keys(context).length > 0) {
|
|
11170
|
+
meta.context = context;
|
|
11171
|
+
}
|
|
11172
|
+
const agentMd = metadataWithoutAuth(envelope.agent_md);
|
|
11173
|
+
if (agentMd && Object.keys(agentMd).length > 0) {
|
|
11174
|
+
meta.agent_md = agentMd;
|
|
11175
|
+
}
|
|
11176
|
+
return meta;
|
|
11177
|
+
}
|
|
11178
|
+
function attachV2EnvelopeMetadata(message, meta) {
|
|
11179
|
+
if (!meta) return;
|
|
11180
|
+
const payloadType = typeof meta.payload_type === "string" ? meta.payload_type.trim() : "";
|
|
11181
|
+
if (payloadType) message.payload_type = payloadType;
|
|
11182
|
+
if (isJsonObject(meta.protected_headers)) {
|
|
11183
|
+
message.protected_headers = { ...meta.protected_headers };
|
|
11184
|
+
}
|
|
11185
|
+
if (isJsonObject(meta.agent_md)) {
|
|
11186
|
+
message.agent_md = { ...meta.agent_md };
|
|
11187
|
+
}
|
|
11188
|
+
}
|
|
11189
|
+
function attachV2EnvelopeMetadataFromSource(message, source) {
|
|
11190
|
+
const envelope = extractV2EnvelopeFromSource(source);
|
|
11191
|
+
if (envelope) attachV2EnvelopeMetadata(message, v2E2eeMeta(envelope));
|
|
11192
|
+
}
|
|
11193
|
+
function extractV2EnvelopeFromSource(source) {
|
|
11194
|
+
if (!isJsonObject(source)) return null;
|
|
11195
|
+
if (isJsonObject(source.payload)) return source.payload;
|
|
11196
|
+
if (typeof source.envelope_json === "string" && source.envelope_json) {
|
|
11197
|
+
try {
|
|
11198
|
+
const parsed = JSON.parse(source.envelope_json);
|
|
11199
|
+
if (isJsonObject(parsed)) return parsed;
|
|
11200
|
+
} catch {
|
|
11201
|
+
return null;
|
|
11202
|
+
}
|
|
11203
|
+
}
|
|
11204
|
+
return null;
|
|
11205
|
+
}
|
|
11206
|
+
function metadataWithoutAuth(value) {
|
|
11207
|
+
if (!isJsonObject(value)) return null;
|
|
11208
|
+
const body = {};
|
|
11209
|
+
for (const [key, item] of Object.entries(value)) {
|
|
11210
|
+
if (key !== "_auth") body[key] = item;
|
|
11211
|
+
}
|
|
11212
|
+
return body;
|
|
10511
11213
|
}
|
|
10512
11214
|
function normalizeDeliveryModeConfig(raw, opts = {}) {
|
|
10513
11215
|
const defaultMode = String(opts.defaultMode ?? "fanout").trim().toLowerCase() || "fanout";
|
|
@@ -10585,6 +11287,8 @@ var _AUNClient = class _AUNClient {
|
|
|
10585
11287
|
__publicField(this, "_v2Session");
|
|
10586
11288
|
__publicField(this, "_v2KeyStore");
|
|
10587
11289
|
__publicField(this, "_v2BootstrapCache", /* @__PURE__ */ new Map());
|
|
11290
|
+
__publicField(this, "_v2SenderIKPending", /* @__PURE__ */ new Map());
|
|
11291
|
+
__publicField(this, "_v2SenderIKFetching", /* @__PURE__ */ new Set());
|
|
10588
11292
|
/** V2 state 签名验证缓存:cacheKey(hex) → expiry_unix_ms */
|
|
10589
11293
|
__publicField(this, "_v2SigCache", /* @__PURE__ */ new Map());
|
|
10590
11294
|
/** V2 state chain 本地记录:group_id → [state_version, chain_hash] */
|
|
@@ -10607,6 +11311,11 @@ var _AUNClient = class _AUNClient {
|
|
|
10607
11311
|
__publicField(this, "_localAgentMdEtag", "");
|
|
10608
11312
|
/** gateway 在 RPC envelope._meta.agent_md_etag 注入的服务端 etag;纯观察,无下游依赖。 */
|
|
10609
11313
|
__publicField(this, "_remoteAgentMdEtag", "");
|
|
11314
|
+
/** 浏览器侧 AgentMDs 逻辑根目录,正文映射到 IndexedDB 里的 {aid}/agent.md。 */
|
|
11315
|
+
__publicField(this, "_agentMdPath", "");
|
|
11316
|
+
__publicField(this, "_agentMdCache", /* @__PURE__ */ new Map());
|
|
11317
|
+
__publicField(this, "_agentMdFetchInflight", /* @__PURE__ */ new Set());
|
|
11318
|
+
__publicField(this, "_agentMdListLock", Promise.resolve());
|
|
10610
11319
|
/** 消息序列号跟踪器(群消息 + P2P 空洞检测) */
|
|
10611
11320
|
__publicField(this, "_seqTracker", new SeqTracker());
|
|
10612
11321
|
__publicField(this, "_seqTrackerContext", null);
|
|
@@ -10650,7 +11359,10 @@ var _AUNClient = class _AUNClient {
|
|
|
10650
11359
|
root_ca_path: this.configModel.rootCaPem,
|
|
10651
11360
|
seed_password: this.configModel.seedPassword
|
|
10652
11361
|
};
|
|
10653
|
-
this.
|
|
11362
|
+
this._agentMdPath = this._agentMdDefaultRoot();
|
|
11363
|
+
this._deviceId = getDeviceId();
|
|
11364
|
+
this._logger = new AUNLogger({ debug: _debug, aunPath: this.configModel.aunPath });
|
|
11365
|
+
this._logger.bindDeviceId(this._deviceId);
|
|
10654
11366
|
this._clientLog = this._logger.for("aun_core.client");
|
|
10655
11367
|
this._logAuth = this._logger.for("aun_core.auth");
|
|
10656
11368
|
this._logTransport = this._logger.for("aun_core.transport");
|
|
@@ -10661,7 +11373,6 @@ var _AUNClient = class _AUNClient {
|
|
|
10661
11373
|
this._dispatcher = new EventDispatcher();
|
|
10662
11374
|
this._discovery = new GatewayDiscovery();
|
|
10663
11375
|
this._keystore = new IndexedDBKeyStore();
|
|
10664
|
-
this._deviceId = getDeviceId();
|
|
10665
11376
|
this._slotId = "";
|
|
10666
11377
|
this._connectDeliveryMode = normalizeDeliveryModeConfig({ mode: "fanout" });
|
|
10667
11378
|
this._defaultConnectDeliveryMode = { ...this._connectDeliveryMode };
|
|
@@ -10680,7 +11391,11 @@ var _AUNClient = class _AUNClient {
|
|
|
10680
11391
|
timeout: DEFAULT_SESSION_OPTIONS.timeouts.call,
|
|
10681
11392
|
onDisconnect: (error, closeCode) => this._handleTransportDisconnect(error, closeCode)
|
|
10682
11393
|
});
|
|
10683
|
-
this._transport.setMetaObserver((meta) =>
|
|
11394
|
+
this._transport.setMetaObserver((meta) => {
|
|
11395
|
+
void this._observeRpcMeta(meta).catch((exc) => {
|
|
11396
|
+
this._clientLog.debug(`agent.md meta observer skipped: ${String(exc)}`);
|
|
11397
|
+
});
|
|
11398
|
+
});
|
|
10684
11399
|
this.auth = new AuthNamespace(this);
|
|
10685
11400
|
this.custody = new CustodyNamespace(this);
|
|
10686
11401
|
this.meta = new MetaNamespace(this);
|
|
@@ -10745,28 +11460,63 @@ var _AUNClient = class _AUNClient {
|
|
|
10745
11460
|
get aid() {
|
|
10746
11461
|
return this._aid;
|
|
10747
11462
|
}
|
|
11463
|
+
setAgentMdPath(root) {
|
|
11464
|
+
const next = String(root ?? "").trim() || this._agentMdDefaultRoot();
|
|
11465
|
+
this._agentMdPath = next;
|
|
11466
|
+
this._agentMdCache.clear();
|
|
11467
|
+
return next;
|
|
11468
|
+
}
|
|
11469
|
+
setAgentMDPath(root) {
|
|
11470
|
+
return this.setAgentMdPath(root);
|
|
11471
|
+
}
|
|
11472
|
+
SetAgentMDPath(root) {
|
|
11473
|
+
return this.setAgentMdPath(root);
|
|
11474
|
+
}
|
|
10748
11475
|
/**
|
|
10749
|
-
* 浏览器版本 publishAgentMd
|
|
10750
|
-
*
|
|
11476
|
+
* 浏览器版本 publishAgentMd。默认从 {agentMdPath}/{self_aid}/agent.md 的等价 IndexedDB 正文读取,
|
|
11477
|
+
* 然后签名、上传,并刷新 list.json 元数据。
|
|
10751
11478
|
*
|
|
10752
|
-
*
|
|
11479
|
+
* 兼容旧浏览器调用:传入 content 时会先写入等价正文,再从该正文发布。
|
|
10753
11480
|
*/
|
|
10754
11481
|
async publishAgentMd(content) {
|
|
10755
|
-
const
|
|
10756
|
-
if (
|
|
10757
|
-
throw new ValidationError("publishAgentMd requires
|
|
11482
|
+
const target = this._agentMdOwnerAid();
|
|
11483
|
+
if (!target) {
|
|
11484
|
+
throw new ValidationError("publishAgentMd requires local AID");
|
|
10758
11485
|
}
|
|
10759
|
-
|
|
11486
|
+
if (content !== void 0 && content !== null) {
|
|
11487
|
+
const text = String(content ?? "");
|
|
11488
|
+
if (text.length === 0) {
|
|
11489
|
+
throw new ValidationError("publishAgentMd requires non-empty content");
|
|
11490
|
+
}
|
|
11491
|
+
await this._saveAgentMdRecord(target, {
|
|
11492
|
+
content: text,
|
|
11493
|
+
local_etag: await this._agentMdContentEtag(text),
|
|
11494
|
+
fetched_at: Date.now()
|
|
11495
|
+
});
|
|
11496
|
+
}
|
|
11497
|
+
const localContent = await this._readAgentMdContent(target);
|
|
11498
|
+
if (localContent === null || localContent.length === 0) {
|
|
11499
|
+
throw new ValidationError("publishAgentMd requires local agent.md content");
|
|
11500
|
+
}
|
|
11501
|
+
const signed = await this.auth.signAgentMd(localContent);
|
|
10760
11502
|
const result = await this.auth.uploadAgentMd(signed);
|
|
10761
|
-
|
|
10762
|
-
const
|
|
10763
|
-
|
|
10764
|
-
this.
|
|
11503
|
+
this._localAgentMdEtag = await this._agentMdContentEtag(signed);
|
|
11504
|
+
const remoteEtag = isJsonObject(result) ? String(result.etag ?? "").trim() : "";
|
|
11505
|
+
if (remoteEtag) this._remoteAgentMdEtag = remoteEtag;
|
|
11506
|
+
await this._saveAgentMdRecord(target, {
|
|
11507
|
+
content: signed,
|
|
11508
|
+
local_etag: this._localAgentMdEtag,
|
|
11509
|
+
remote_etag: remoteEtag || void 0,
|
|
11510
|
+
last_modified: isJsonObject(result) ? String(result.last_modified ?? "").trim() : "",
|
|
11511
|
+
fetched_at: Date.now(),
|
|
11512
|
+
remote_status: remoteEtag ? "found" : "unknown",
|
|
11513
|
+
last_error: ""
|
|
11514
|
+
});
|
|
10765
11515
|
return result;
|
|
10766
11516
|
}
|
|
10767
11517
|
/**
|
|
10768
|
-
* 浏览器版本 fetchAgentMd。aid
|
|
10769
|
-
*
|
|
11518
|
+
* 浏览器版本 fetchAgentMd。aid 缺省时取自身;下载后的正文固定写入
|
|
11519
|
+
* {agentMdPath}/{aid}/agent.md 的等价 IndexedDB 正文,list.json 只保存元数据。
|
|
10770
11520
|
*/
|
|
10771
11521
|
async fetchAgentMd(aid) {
|
|
10772
11522
|
const target = String(aid ?? this._aid ?? "").trim();
|
|
@@ -10776,15 +11526,29 @@ var _AUNClient = class _AUNClient {
|
|
|
10776
11526
|
const content = await this.auth.downloadAgentMd(target);
|
|
10777
11527
|
const signature = await this.auth.verifyAgentMd(content, { aid: target });
|
|
10778
11528
|
const isSelf = target === (this._aid ?? "");
|
|
11529
|
+
const localEtag = await this._agentMdContentEtag(content);
|
|
11530
|
+
const cacheMeta = this._agentMdAuthCacheMeta(target);
|
|
11531
|
+
const remoteEtag = String(cacheMeta.etag ?? "").trim();
|
|
11532
|
+
const lastModified = String(cacheMeta.lastModified ?? cacheMeta.last_modified ?? "").trim();
|
|
11533
|
+
if (isSelf) {
|
|
11534
|
+
this._localAgentMdEtag = localEtag;
|
|
11535
|
+
if (remoteEtag) this._remoteAgentMdEtag = remoteEtag;
|
|
11536
|
+
}
|
|
11537
|
+
await this._saveAgentMdRecord(target, {
|
|
11538
|
+
content,
|
|
11539
|
+
local_etag: localEtag,
|
|
11540
|
+
remote_etag: remoteEtag || void 0,
|
|
11541
|
+
last_modified: lastModified || void 0,
|
|
11542
|
+
fetched_at: Date.now(),
|
|
11543
|
+
remote_status: "found",
|
|
11544
|
+
verify_status: isJsonObject(signature) ? String(signature.status ?? "") : "",
|
|
11545
|
+
verify_error: isJsonObject(signature) ? String(signature.reason ?? "") : "",
|
|
11546
|
+
last_error: ""
|
|
11547
|
+
});
|
|
10779
11548
|
let in_sync = null;
|
|
10780
11549
|
if (isSelf) {
|
|
10781
|
-
const
|
|
10782
|
-
|
|
10783
|
-
const hex = Array.from(new Uint8Array(digest)).map((b) => b.toString(16).padStart(2, "0")).join("");
|
|
10784
|
-
this._localAgentMdEtag = `"${hex}"`;
|
|
10785
|
-
const local = this._localAgentMdEtag || "";
|
|
10786
|
-
const remote = this._remoteAgentMdEtag || "";
|
|
10787
|
-
in_sync = local && remote ? local === remote : false;
|
|
11550
|
+
const remote = remoteEtag || this._remoteAgentMdEtag || "";
|
|
11551
|
+
in_sync = localEtag && remote ? localEtag === remote : false;
|
|
10788
11552
|
}
|
|
10789
11553
|
return {
|
|
10790
11554
|
aid: target,
|
|
@@ -10793,12 +11557,396 @@ var _AUNClient = class _AUNClient {
|
|
|
10793
11557
|
in_sync
|
|
10794
11558
|
};
|
|
10795
11559
|
}
|
|
11560
|
+
getLocalAgentMdEtag() {
|
|
11561
|
+
return this._localAgentMdEtag;
|
|
11562
|
+
}
|
|
11563
|
+
getRemoteAgentMdEtag() {
|
|
11564
|
+
return this._remoteAgentMdEtag;
|
|
11565
|
+
}
|
|
11566
|
+
async _agentMdContentEtag(content) {
|
|
11567
|
+
const digest = await crypto.subtle.digest("SHA-256", new TextEncoder().encode(String(content ?? "")));
|
|
11568
|
+
const hex = Array.from(new Uint8Array(digest)).map((b) => b.toString(16).padStart(2, "0")).join("");
|
|
11569
|
+
return `"${hex}"`;
|
|
11570
|
+
}
|
|
11571
|
+
_agentMdOwnerAid() {
|
|
11572
|
+
return String(this._aid ?? "").trim();
|
|
11573
|
+
}
|
|
11574
|
+
_agentMdDefaultRoot() {
|
|
11575
|
+
return this._joinAgentMdPath(this.configModel.aunPath || ".", "AgentMDs");
|
|
11576
|
+
}
|
|
11577
|
+
_joinAgentMdPath(base, name) {
|
|
11578
|
+
const left = String(base ?? "").trim().replace(/[\\/]+$/g, "");
|
|
11579
|
+
return left ? `${left}/${name}` : name;
|
|
11580
|
+
}
|
|
11581
|
+
_agentMdRoot() {
|
|
11582
|
+
return this._agentMdPath || this._agentMdDefaultRoot();
|
|
11583
|
+
}
|
|
11584
|
+
_agentMdSafeAid(aid) {
|
|
11585
|
+
const target = String(aid ?? "").trim();
|
|
11586
|
+
if (!target || target.includes("/") || target.includes("\\") || target.includes("\0")) {
|
|
11587
|
+
throw new ValidationError("agent.md aid is empty or contains path separators");
|
|
11588
|
+
}
|
|
11589
|
+
return target;
|
|
11590
|
+
}
|
|
11591
|
+
_agentMdListKey() {
|
|
11592
|
+
return "list.json";
|
|
11593
|
+
}
|
|
11594
|
+
_agentMdContentKey(aid) {
|
|
11595
|
+
return `${this._agentMdSafeAid(aid)}/agent.md`;
|
|
11596
|
+
}
|
|
11597
|
+
async _readAgentMdStorage(logicalKey) {
|
|
11598
|
+
const key = String(logicalKey ?? "").trim();
|
|
11599
|
+
if (!key) return null;
|
|
11600
|
+
const load = this._keystore.loadAgentMdCache;
|
|
11601
|
+
if (typeof load !== "function") {
|
|
11602
|
+
throw new Error("IndexedDB agent.md storage unavailable");
|
|
11603
|
+
}
|
|
11604
|
+
const record = await load.call(this._keystore, this._agentMdRoot(), key);
|
|
11605
|
+
if (record && Object.prototype.hasOwnProperty.call(record, "content")) {
|
|
11606
|
+
return String(record.content ?? "");
|
|
11607
|
+
}
|
|
11608
|
+
return null;
|
|
11609
|
+
}
|
|
11610
|
+
async _writeAgentMdStorage(logicalKey, content) {
|
|
11611
|
+
const key = String(logicalKey ?? "").trim();
|
|
11612
|
+
if (!key) return;
|
|
11613
|
+
const save = this._keystore.upsertAgentMdCache;
|
|
11614
|
+
if (typeof save !== "function") {
|
|
11615
|
+
throw new Error("IndexedDB agent.md storage unavailable");
|
|
11616
|
+
}
|
|
11617
|
+
const text = String(content ?? "");
|
|
11618
|
+
await save.call(this._keystore, this._agentMdRoot(), key, {
|
|
11619
|
+
content: text,
|
|
11620
|
+
local_etag: await this._agentMdContentEtag(text),
|
|
11621
|
+
fetched_at: Date.now()
|
|
11622
|
+
});
|
|
11623
|
+
}
|
|
11624
|
+
async _listAgentMdContentAids() {
|
|
11625
|
+
const list = this._keystore.listAgentMdContentAids;
|
|
11626
|
+
if (typeof list !== "function") {
|
|
11627
|
+
throw new Error("IndexedDB agent.md storage unavailable");
|
|
11628
|
+
}
|
|
11629
|
+
return await list.call(this._keystore, this._agentMdRoot());
|
|
11630
|
+
}
|
|
11631
|
+
async _withAgentMdListLock(fn) {
|
|
11632
|
+
const previous = this._agentMdListLock.catch(() => void 0);
|
|
11633
|
+
let release;
|
|
11634
|
+
const current = new Promise((resolve) => {
|
|
11635
|
+
release = resolve;
|
|
11636
|
+
});
|
|
11637
|
+
this._agentMdListLock = previous.then(() => current);
|
|
11638
|
+
await previous;
|
|
11639
|
+
try {
|
|
11640
|
+
return await fn();
|
|
11641
|
+
} finally {
|
|
11642
|
+
release();
|
|
11643
|
+
}
|
|
11644
|
+
}
|
|
11645
|
+
_normalizeAgentMdList(data) {
|
|
11646
|
+
const records = {};
|
|
11647
|
+
let iterable = [];
|
|
11648
|
+
if (isJsonObject(data)) {
|
|
11649
|
+
const payload = data;
|
|
11650
|
+
if (Array.isArray(payload.records)) iterable = payload.records;
|
|
11651
|
+
else if (isJsonObject(payload.records)) iterable = Object.values(payload.records);
|
|
11652
|
+
} else if (Array.isArray(data)) {
|
|
11653
|
+
iterable = data;
|
|
11654
|
+
}
|
|
11655
|
+
for (const item of iterable) {
|
|
11656
|
+
if (!isJsonObject(item)) continue;
|
|
11657
|
+
const raw = item;
|
|
11658
|
+
const aid = String(raw.aid ?? "").trim();
|
|
11659
|
+
if (!aid) continue;
|
|
11660
|
+
const record = {};
|
|
11661
|
+
for (const [key, value] of Object.entries(raw)) {
|
|
11662
|
+
if (key !== "content") record[key] = value;
|
|
11663
|
+
}
|
|
11664
|
+
record.aid = aid;
|
|
11665
|
+
for (const key of ["fetched_at", "observed_at", "checked_at", "updated_at"]) {
|
|
11666
|
+
record[key] = Number(record[key] ?? 0) || 0;
|
|
11667
|
+
}
|
|
11668
|
+
records[aid] = record;
|
|
11669
|
+
}
|
|
11670
|
+
return records;
|
|
11671
|
+
}
|
|
11672
|
+
async _writeAgentMdListUnlocked(records) {
|
|
11673
|
+
const sorted = {};
|
|
11674
|
+
for (const aid of Object.keys(records).sort()) sorted[aid] = records[aid];
|
|
11675
|
+
await this._writeAgentMdStorage(this._agentMdListKey(), `${JSON.stringify({ version: 1, updated_at: Date.now(), records: sorted }, null, 2)}
|
|
11676
|
+
`);
|
|
11677
|
+
}
|
|
11678
|
+
async _rebuildAgentMdListUnlocked() {
|
|
11679
|
+
const records = {};
|
|
11680
|
+
const now = Date.now();
|
|
11681
|
+
for (const aid of await this._listAgentMdContentAids()) {
|
|
11682
|
+
try {
|
|
11683
|
+
const content = await this._readAgentMdStorage(this._agentMdContentKey(aid));
|
|
11684
|
+
if (content === null) continue;
|
|
11685
|
+
records[aid] = {
|
|
11686
|
+
aid,
|
|
11687
|
+
local_etag: await this._agentMdContentEtag(content),
|
|
11688
|
+
fetched_at: now,
|
|
11689
|
+
updated_at: now
|
|
11690
|
+
};
|
|
11691
|
+
} catch (err) {
|
|
11692
|
+
this._clientLog.warn(`agent.md rebuild skipped unreadable file aid=${aid} err=${err instanceof Error ? err.message : String(err)}`);
|
|
11693
|
+
}
|
|
11694
|
+
}
|
|
11695
|
+
await this._writeAgentMdListUnlocked(records);
|
|
11696
|
+
this._agentMdCache.clear();
|
|
11697
|
+
return records;
|
|
11698
|
+
}
|
|
11699
|
+
async _readAgentMdListUnlocked() {
|
|
11700
|
+
const raw = await this._readAgentMdStorage(this._agentMdListKey());
|
|
11701
|
+
if (raw === null) {
|
|
11702
|
+
return await this._rebuildAgentMdListUnlocked();
|
|
11703
|
+
}
|
|
11704
|
+
try {
|
|
11705
|
+
return this._normalizeAgentMdList(JSON.parse(raw));
|
|
11706
|
+
} catch (err) {
|
|
11707
|
+
this._clientLog.warn(`agent.md list.json damaged, rebuilding: ${err instanceof Error ? err.message : String(err)}`);
|
|
11708
|
+
return await this._rebuildAgentMdListUnlocked();
|
|
11709
|
+
}
|
|
11710
|
+
}
|
|
11711
|
+
async _readAgentMdContent(aid) {
|
|
11712
|
+
return await this._readAgentMdStorage(this._agentMdContentKey(aid));
|
|
11713
|
+
}
|
|
11714
|
+
async _writeAgentMdContent(aid, content) {
|
|
11715
|
+
await this._writeAgentMdStorage(this._agentMdContentKey(aid), String(content ?? ""));
|
|
11716
|
+
}
|
|
11717
|
+
_agentMdAuthCacheMeta(aid) {
|
|
11718
|
+
try {
|
|
11719
|
+
const store = this.auth._agentMdCache;
|
|
11720
|
+
const record = store?.get(String(aid ?? "").trim());
|
|
11721
|
+
return record && typeof record === "object" ? { ...record } : {};
|
|
11722
|
+
} catch {
|
|
11723
|
+
return {};
|
|
11724
|
+
}
|
|
11725
|
+
}
|
|
11726
|
+
async _loadAgentMdRecord(aid) {
|
|
11727
|
+
const target = String(aid ?? "").trim();
|
|
11728
|
+
if (!target) return null;
|
|
11729
|
+
try {
|
|
11730
|
+
const records = await this._withAgentMdListLock(async () => await this._readAgentMdListUnlocked());
|
|
11731
|
+
const record = records[target];
|
|
11732
|
+
if (record && typeof record === "object") {
|
|
11733
|
+
const loaded = { ...record, aid: target };
|
|
11734
|
+
const content = await this._readAgentMdContent(target);
|
|
11735
|
+
if (content !== null) {
|
|
11736
|
+
loaded.content = content;
|
|
11737
|
+
loaded.local_etag = await this._agentMdContentEtag(content);
|
|
11738
|
+
} else {
|
|
11739
|
+
this._clientLog.warn(`agent.md content read failed: aid=${target}`);
|
|
11740
|
+
}
|
|
11741
|
+
this._agentMdCache.set(target, { ...loaded });
|
|
11742
|
+
return { ...loaded };
|
|
11743
|
+
}
|
|
11744
|
+
} catch (err) {
|
|
11745
|
+
this._clientLog.debug(`agent.md cache load skipped: aid=${target} err=${err instanceof Error ? err.message : String(err)}`);
|
|
11746
|
+
}
|
|
11747
|
+
return null;
|
|
11748
|
+
}
|
|
11749
|
+
async _saveAgentMdRecord(aid, fields) {
|
|
11750
|
+
const target = String(aid ?? "").trim();
|
|
11751
|
+
if (!target) return {};
|
|
11752
|
+
try {
|
|
11753
|
+
const inputFields = { ...fields };
|
|
11754
|
+
const hasContent = Object.prototype.hasOwnProperty.call(inputFields, "content") && inputFields.content !== void 0 && inputFields.content !== null;
|
|
11755
|
+
if (hasContent) {
|
|
11756
|
+
const text = String(inputFields.content ?? "");
|
|
11757
|
+
await this._writeAgentMdContent(target, text);
|
|
11758
|
+
if (!inputFields.local_etag) inputFields.local_etag = await this._agentMdContentEtag(text);
|
|
11759
|
+
if (!inputFields.fetched_at) inputFields.fetched_at = Date.now();
|
|
11760
|
+
}
|
|
11761
|
+
delete inputFields.content;
|
|
11762
|
+
const record = await this._withAgentMdListLock(async () => {
|
|
11763
|
+
const records = await this._readAgentMdListUnlocked();
|
|
11764
|
+
const next = { ...records[target] ?? {}, aid: target };
|
|
11765
|
+
for (const [key, value] of Object.entries(inputFields)) {
|
|
11766
|
+
if (value !== void 0 && value !== null) next[key] = value;
|
|
11767
|
+
}
|
|
11768
|
+
next.updated_at = Date.now();
|
|
11769
|
+
records[target] = { ...next };
|
|
11770
|
+
await this._writeAgentMdListUnlocked(records);
|
|
11771
|
+
return next;
|
|
11772
|
+
});
|
|
11773
|
+
const loaded = { ...record };
|
|
11774
|
+
if (hasContent) loaded.content = String(fields.content ?? "");
|
|
11775
|
+
this._agentMdCache.set(target, { ...loaded });
|
|
11776
|
+
const owner = this._agentMdOwnerAid();
|
|
11777
|
+
if (target === owner) {
|
|
11778
|
+
const localEtag = String(loaded.local_etag ?? "").trim();
|
|
11779
|
+
const remoteEtag = String(loaded.remote_etag ?? "").trim();
|
|
11780
|
+
if (localEtag) this._localAgentMdEtag = localEtag;
|
|
11781
|
+
if (remoteEtag) this._remoteAgentMdEtag = remoteEtag;
|
|
11782
|
+
}
|
|
11783
|
+
return { ...loaded };
|
|
11784
|
+
} catch (err) {
|
|
11785
|
+
this._clientLog.debug(`agent.md cache save skipped: aid=${target} err=${err instanceof Error ? err.message : String(err)}`);
|
|
11786
|
+
}
|
|
11787
|
+
return {};
|
|
11788
|
+
}
|
|
11789
|
+
async _agentMdHasLocalContent(aid, record) {
|
|
11790
|
+
if (record && typeof record.content === "string" && record.content.length > 0) return true;
|
|
11791
|
+
try {
|
|
11792
|
+
return await this._readAgentMdContent(aid) !== null;
|
|
11793
|
+
} catch {
|
|
11794
|
+
return false;
|
|
11795
|
+
}
|
|
11796
|
+
}
|
|
11797
|
+
_agentMdCheckedAtFresh(checkedAtMs, maxUnsyncedDays) {
|
|
11798
|
+
const days = Number(maxUnsyncedDays || 0);
|
|
11799
|
+
if (!Number.isFinite(days) || days <= 0) return false;
|
|
11800
|
+
if (!Number.isFinite(checkedAtMs) || checkedAtMs <= 0) return false;
|
|
11801
|
+
return Date.now() - checkedAtMs <= days * 864e5;
|
|
11802
|
+
}
|
|
11803
|
+
_agentMdLastModifiedFresh(lastModified, maxUnsyncedDays) {
|
|
11804
|
+
const days = Number(maxUnsyncedDays || 0);
|
|
11805
|
+
if (!Number.isFinite(days) || days <= 0) return false;
|
|
11806
|
+
const ts = Date.parse(String(lastModified ?? "").trim());
|
|
11807
|
+
if (!Number.isFinite(ts)) return false;
|
|
11808
|
+
return Date.now() <= ts + days * 864e5;
|
|
11809
|
+
}
|
|
11810
|
+
async _scheduleAgentMdFetchIfMissing(aid, record, source = "") {
|
|
11811
|
+
const target = String(aid ?? "").trim();
|
|
11812
|
+
if (!target || await this._agentMdHasLocalContent(target, record)) return;
|
|
11813
|
+
if (this._agentMdFetchInflight.has(target)) return;
|
|
11814
|
+
this._agentMdFetchInflight.add(target);
|
|
11815
|
+
try {
|
|
11816
|
+
await this.fetchAgentMd(target);
|
|
11817
|
+
} catch (err) {
|
|
11818
|
+
await this._saveAgentMdRecord(target, {
|
|
11819
|
+
last_error: err instanceof Error ? err.message : String(err),
|
|
11820
|
+
remote_status: "found"
|
|
11821
|
+
});
|
|
11822
|
+
this._clientLog.debug(`agent.md auto fetch failed: aid=${target} source=${source || "-"} err=${err instanceof Error ? err.message : String(err)}`);
|
|
11823
|
+
} finally {
|
|
11824
|
+
this._agentMdFetchInflight.delete(target);
|
|
11825
|
+
}
|
|
11826
|
+
}
|
|
11827
|
+
async _observeAgentMdMeta(aid, etag = "", lastModified = "", source = "") {
|
|
11828
|
+
const target = String(aid ?? "").trim();
|
|
11829
|
+
const remoteEtag = String(etag ?? "").trim();
|
|
11830
|
+
const remoteLastModified = String(lastModified ?? "").trim();
|
|
11831
|
+
if (!target || !remoteEtag && !remoteLastModified) return;
|
|
11832
|
+
let before = this._agentMdCache.get(target);
|
|
11833
|
+
if (!before || typeof before !== "object") before = await this._loadAgentMdRecord(target) ?? {};
|
|
11834
|
+
const same = (!remoteEtag || String(before.remote_etag ?? "").trim() === remoteEtag) && (!remoteLastModified || String(before.last_modified ?? "").trim() === remoteLastModified);
|
|
11835
|
+
let record = { ...before };
|
|
11836
|
+
if (!same || Object.keys(before).length === 0) {
|
|
11837
|
+
const fields = {
|
|
11838
|
+
observed_at: Date.now(),
|
|
11839
|
+
remote_status: "found"
|
|
11840
|
+
};
|
|
11841
|
+
if (remoteEtag) fields.remote_etag = remoteEtag;
|
|
11842
|
+
if (remoteLastModified) fields.last_modified = remoteLastModified;
|
|
11843
|
+
record = await this._saveAgentMdRecord(target, fields) || record;
|
|
11844
|
+
}
|
|
11845
|
+
if (target === this._agentMdOwnerAid() && remoteEtag) this._remoteAgentMdEtag = remoteEtag;
|
|
11846
|
+
await this._scheduleAgentMdFetchIfMissing(target, record, source);
|
|
11847
|
+
this._clientLog.debug(`agent.md meta observed: aid=${target} etag=${remoteEtag || "-"} last_modified=${remoteLastModified || "-"} source=${source || "-"}`);
|
|
11848
|
+
}
|
|
11849
|
+
async _observeAgentMdEtag(aid, etag, source = "") {
|
|
11850
|
+
await this._observeAgentMdMeta(aid, etag, "", source);
|
|
11851
|
+
}
|
|
11852
|
+
async _observeAgentMdFromEnvelope(envelope) {
|
|
11853
|
+
if (!isJsonObject(envelope)) return;
|
|
11854
|
+
const env = envelope;
|
|
11855
|
+
if (!isJsonObject(env.agent_md)) return;
|
|
11856
|
+
const agentMd = env.agent_md;
|
|
11857
|
+
if (!isJsonObject(agentMd.sender)) return;
|
|
11858
|
+
const sender = agentMd.sender;
|
|
11859
|
+
let senderAid = String(sender.aid ?? "").trim();
|
|
11860
|
+
if (!senderAid) {
|
|
11861
|
+
const aad = isJsonObject(env.aad) ? env.aad : {};
|
|
11862
|
+
senderAid = String(aad.from ?? env.from ?? "").trim();
|
|
11863
|
+
}
|
|
11864
|
+
await this._observeAgentMdMeta(
|
|
11865
|
+
senderAid,
|
|
11866
|
+
String(sender.etag ?? "").trim(),
|
|
11867
|
+
String(sender.last_modified ?? sender.lastModified ?? "").trim(),
|
|
11868
|
+
"envelope"
|
|
11869
|
+
);
|
|
11870
|
+
}
|
|
11871
|
+
async checkAgentMd(aid, maxUnsyncedDays = 0) {
|
|
11872
|
+
const target = String(aid ?? this._aid ?? "").trim();
|
|
11873
|
+
if (!target) throw new ValidationError("checkAgentMd requires aid (or local AID)");
|
|
11874
|
+
const before = await this._loadAgentMdRecord(target) ?? {};
|
|
11875
|
+
const localEtag = String(before.local_etag ?? "").trim();
|
|
11876
|
+
const localFound = !!(Object.keys(before).length > 0 && (String(before.content ?? "") || localEtag));
|
|
11877
|
+
const remoteEtagCached = String(before.remote_etag ?? "").trim();
|
|
11878
|
+
const lastModifiedCached = String(before.last_modified ?? "").trim();
|
|
11879
|
+
const checkedAtCached = Number(before.checked_at ?? 0);
|
|
11880
|
+
const cachedInSync = !!(localFound && localEtag && remoteEtagCached && localEtag === remoteEtagCached);
|
|
11881
|
+
if (cachedInSync && this._agentMdCheckedAtFresh(checkedAtCached, maxUnsyncedDays)) {
|
|
11882
|
+
return {
|
|
11883
|
+
aid: target,
|
|
11884
|
+
local_found: true,
|
|
11885
|
+
remote_found: true,
|
|
11886
|
+
local_etag: localEtag,
|
|
11887
|
+
remote_etag: remoteEtagCached,
|
|
11888
|
+
in_sync: true,
|
|
11889
|
+
last_modified: lastModifiedCached,
|
|
11890
|
+
status: 200,
|
|
11891
|
+
cached: true,
|
|
11892
|
+
verify_status: String(before.verify_status ?? ""),
|
|
11893
|
+
verify_error: String(before.verify_error ?? "")
|
|
11894
|
+
};
|
|
11895
|
+
}
|
|
11896
|
+
const now = Date.now();
|
|
11897
|
+
let remote;
|
|
11898
|
+
try {
|
|
11899
|
+
remote = await this.auth.headAgentMd(target);
|
|
11900
|
+
} catch (err) {
|
|
11901
|
+
await this._saveAgentMdRecord(target, { checked_at: now, remote_status: "error", last_error: err instanceof Error ? err.message : String(err) });
|
|
11902
|
+
throw err;
|
|
11903
|
+
}
|
|
11904
|
+
const remoteFound = !!remote.found;
|
|
11905
|
+
const remoteEtag = String(remote.etag ?? "").trim();
|
|
11906
|
+
const lastModified = String(remote.last_modified ?? remote.lastModified ?? "").trim();
|
|
11907
|
+
const saved = await this._saveAgentMdRecord(target, {
|
|
11908
|
+
remote_etag: remoteFound ? remoteEtag : "",
|
|
11909
|
+
last_modified: lastModified,
|
|
11910
|
+
checked_at: now,
|
|
11911
|
+
remote_status: remoteFound ? "found" : "missing",
|
|
11912
|
+
last_error: ""
|
|
11913
|
+
});
|
|
11914
|
+
if (target === this._agentMdOwnerAid() && remoteEtag) this._remoteAgentMdEtag = remoteEtag;
|
|
11915
|
+
const inSync = !!(localFound && remoteFound && localEtag && remoteEtag && localEtag === remoteEtag);
|
|
11916
|
+
return {
|
|
11917
|
+
aid: target,
|
|
11918
|
+
local_found: localFound,
|
|
11919
|
+
remote_found: remoteFound,
|
|
11920
|
+
local_etag: localEtag,
|
|
11921
|
+
remote_etag: remoteEtag,
|
|
11922
|
+
in_sync: inSync,
|
|
11923
|
+
last_modified: lastModified,
|
|
11924
|
+
status: Number(remote.status ?? (remoteFound ? 200 : 404)),
|
|
11925
|
+
cached: false,
|
|
11926
|
+
verify_status: String(saved.verify_status ?? before.verify_status ?? ""),
|
|
11927
|
+
verify_error: String(saved.verify_error ?? before.verify_error ?? "")
|
|
11928
|
+
};
|
|
11929
|
+
}
|
|
10796
11930
|
/** transport 的 meta observer:吸收 gateway 注入的 _meta 字段。失败不影响业务。 */
|
|
10797
|
-
_observeRpcMeta(meta) {
|
|
11931
|
+
async _observeRpcMeta(meta) {
|
|
10798
11932
|
if (!isJsonObject(meta)) return;
|
|
10799
11933
|
const etag = String(meta.agent_md_etag ?? "").trim();
|
|
10800
11934
|
if (etag) {
|
|
10801
11935
|
this._remoteAgentMdEtag = etag;
|
|
11936
|
+
await this._observeAgentMdMeta(this._aid ?? "", etag, "", "rpc.self");
|
|
11937
|
+
}
|
|
11938
|
+
const etags = meta.agent_md_etags;
|
|
11939
|
+
if (isJsonObject(etags)) {
|
|
11940
|
+
for (const key of ["requester", "peer", "receiver", "target", "to", "sender", "from"]) {
|
|
11941
|
+
const item = etags[key];
|
|
11942
|
+
if (!isJsonObject(item)) continue;
|
|
11943
|
+
await this._observeAgentMdMeta(
|
|
11944
|
+
String(item.aid ?? ""),
|
|
11945
|
+
String(item.etag ?? ""),
|
|
11946
|
+
String(item.last_modified ?? item.lastModified ?? ""),
|
|
11947
|
+
`rpc.${key}`
|
|
11948
|
+
);
|
|
11949
|
+
}
|
|
10802
11950
|
}
|
|
10803
11951
|
}
|
|
10804
11952
|
get state() {
|
|
@@ -10851,16 +11999,29 @@ var _AUNClient = class _AUNClient {
|
|
|
10851
11999
|
this._sessionOptions = this._buildSessionOptions(normalized);
|
|
10852
12000
|
this._transport.setTimeout(this._sessionOptions.timeouts.call);
|
|
10853
12001
|
this._closing = false;
|
|
10854
|
-
|
|
10855
|
-
|
|
10856
|
-
|
|
10857
|
-
|
|
10858
|
-
|
|
10859
|
-
this.
|
|
12002
|
+
const gateways = this._resolveGateways(normalized);
|
|
12003
|
+
let lastErr = null;
|
|
12004
|
+
for (const gw of gateways) {
|
|
12005
|
+
try {
|
|
12006
|
+
const gwParams = { ...normalized, gateway: gw };
|
|
12007
|
+
await this._connectOnce(gwParams, false);
|
|
12008
|
+
this._clientLog.debug(`connect exit: elapsed=${Date.now() - tStart}ms state=${this._state}`);
|
|
12009
|
+
return;
|
|
12010
|
+
} catch (err) {
|
|
12011
|
+
lastErr = err;
|
|
12012
|
+
if (gateways.length > 1) {
|
|
12013
|
+
this._clientLog.warn(`connect: gateway ${gw} failed, trying next: ${err instanceof Error ? err.message : String(err)}`);
|
|
12014
|
+
}
|
|
12015
|
+
if (this._state === "connecting" || this._state === "authenticating") {
|
|
12016
|
+
this._state = "connecting";
|
|
12017
|
+
}
|
|
10860
12018
|
}
|
|
10861
|
-
this._clientLog.debug(`connect exit (error): elapsed=${Date.now() - tStart}ms err=${err instanceof Error ? err.message : String(err)}`);
|
|
10862
|
-
throw err;
|
|
10863
12019
|
}
|
|
12020
|
+
if (this._state === "connecting" || this._state === "authenticating") {
|
|
12021
|
+
this._state = "disconnected";
|
|
12022
|
+
}
|
|
12023
|
+
this._clientLog.debug(`connect exit (error): elapsed=${Date.now() - tStart}ms err=${lastErr instanceof Error ? lastErr.message : String(lastErr)}`);
|
|
12024
|
+
throw lastErr;
|
|
10864
12025
|
}
|
|
10865
12026
|
/** 断开连接但保留本地状态,可再次 connect */
|
|
10866
12027
|
async disconnect() {
|
|
@@ -10983,6 +12144,9 @@ var _AUNClient = class _AUNClient {
|
|
|
10983
12144
|
throw new PermissionError(`legacy E2EE method is removed in this SDK: ${method}`);
|
|
10984
12145
|
}
|
|
10985
12146
|
const p = { ...params ?? {} };
|
|
12147
|
+
if (method === "message.send" || method === "group.send") {
|
|
12148
|
+
this._normalizeOutboundMessagePayload(p, method);
|
|
12149
|
+
}
|
|
10986
12150
|
this._validateOutboundCall(method, p);
|
|
10987
12151
|
this._injectMessageCursorContext(method, p);
|
|
10988
12152
|
if (method.startsWith("group.") && p.group_id !== void 0 && p.group_id !== null) {
|
|
@@ -10993,7 +12157,7 @@ var _AUNClient = class _AUNClient {
|
|
|
10993
12157
|
}
|
|
10994
12158
|
p.group_id = normalizedGroupId;
|
|
10995
12159
|
}
|
|
10996
|
-
if (method.startsWith("group.") &&
|
|
12160
|
+
if (method.startsWith("group.") && p.device_id === void 0) {
|
|
10997
12161
|
p.device_id = this._deviceId;
|
|
10998
12162
|
}
|
|
10999
12163
|
if (method.startsWith("group.") && p.slot_id === void 0) {
|
|
@@ -11081,7 +12245,11 @@ var _AUNClient = class _AUNClient {
|
|
|
11081
12245
|
);
|
|
11082
12246
|
}
|
|
11083
12247
|
if (SIGNED_METHODS.has(method)) {
|
|
11084
|
-
|
|
12248
|
+
if (this._shouldSkipClientSignature(method, p)) {
|
|
12249
|
+
delete p.client_signature;
|
|
12250
|
+
} else {
|
|
12251
|
+
await this._signClientOperation(method, p);
|
|
12252
|
+
}
|
|
11085
12253
|
}
|
|
11086
12254
|
const callTimeout = NON_IDEMPOTENT_METHODS.has(method) ? NON_IDEMPOTENT_TIMEOUT : void 0;
|
|
11087
12255
|
let result = callTimeout ? await this._transport.call(method, p, callTimeout) : await this._transport.call(method, p);
|
|
@@ -11211,14 +12379,17 @@ var _AUNClient = class _AUNClient {
|
|
|
11211
12379
|
const seq = msg.seq;
|
|
11212
12380
|
if (seq !== void 0 && seq !== null && this._aid) {
|
|
11213
12381
|
const ns = `p2p:${this._aid}`;
|
|
12382
|
+
if (seq > 0) this._seqTracker.updateMaxSeen(ns, seq);
|
|
11214
12383
|
const needPull = this._seqTracker.onMessageSeq(ns, seq);
|
|
11215
12384
|
if (needPull) {
|
|
11216
12385
|
this._safeAsync(this._fillP2pGap());
|
|
11217
12386
|
}
|
|
11218
12387
|
const contig = this._seqTracker.getContiguousSeq(ns);
|
|
11219
12388
|
if (contig > 0) {
|
|
12389
|
+
const maxSeen = this._seqTracker.getMaxSeenSeq(ns);
|
|
12390
|
+
const ackSeq = maxSeen > 0 ? Math.min(contig, maxSeen) : contig;
|
|
11220
12391
|
this._transport.call("message.ack", {
|
|
11221
|
-
seq:
|
|
12392
|
+
seq: ackSeq,
|
|
11222
12393
|
device_id: this._deviceId,
|
|
11223
12394
|
slot_id: this._slotId
|
|
11224
12395
|
}).catch((e) => {
|
|
@@ -11245,6 +12416,7 @@ var _AUNClient = class _AUNClient {
|
|
|
11245
12416
|
timestamp: src.timestamp ?? null,
|
|
11246
12417
|
_decrypt_error: String(exc)
|
|
11247
12418
|
};
|
|
12419
|
+
attachV2EnvelopeMetadataFromSource(safeEvent, data);
|
|
11248
12420
|
await this._publishAppEvent("message.undecryptable", safeEvent);
|
|
11249
12421
|
}
|
|
11250
12422
|
}
|
|
@@ -11277,6 +12449,18 @@ var _AUNClient = class _AUNClient {
|
|
|
11277
12449
|
}
|
|
11278
12450
|
try {
|
|
11279
12451
|
const ns = `group:${groupId}`;
|
|
12452
|
+
this._seqTracker.updateMaxSeen(ns, seq);
|
|
12453
|
+
const contigBefore = this._seqTracker.getContiguousSeq(ns);
|
|
12454
|
+
if (contigBefore === seq) {
|
|
12455
|
+
this._clientLog.debug(`_onRawGroupV2MessageCreated duplicate push already covered: group=${groupId} seq=${seq}`);
|
|
12456
|
+
return;
|
|
12457
|
+
}
|
|
12458
|
+
const afterSeq = this._repairPushContiguousBound(
|
|
12459
|
+
ns,
|
|
12460
|
+
seq,
|
|
12461
|
+
false,
|
|
12462
|
+
"_raw.group.v2.message_created"
|
|
12463
|
+
);
|
|
11280
12464
|
const dedupKey = `group_pull:${ns}`;
|
|
11281
12465
|
if (this._gapFillDone.has(dedupKey)) {
|
|
11282
12466
|
this._clientLog.debug(`_onRawGroupV2MessageCreated skipped: dedupKey=${dedupKey} in flight`);
|
|
@@ -11284,7 +12468,6 @@ var _AUNClient = class _AUNClient {
|
|
|
11284
12468
|
}
|
|
11285
12469
|
this._gapFillDone.add(dedupKey);
|
|
11286
12470
|
try {
|
|
11287
|
-
const afterSeq = Math.max(0, this._seqTracker.getContiguousSeq(ns));
|
|
11288
12471
|
this._clientLog.debug(`_onRawGroupV2MessageCreated -> group.v2.pull group=${groupId} after_seq=${afterSeq}`);
|
|
11289
12472
|
const messages = await this.pullGroupV2(groupId, afterSeq, 50);
|
|
11290
12473
|
this._clientLog.debug(`_onRawGroupV2MessageCreated pulled ${messages.length} msgs for group=${groupId}`);
|
|
@@ -11322,15 +12505,18 @@ var _AUNClient = class _AUNClient {
|
|
|
11322
12505
|
}
|
|
11323
12506
|
if (groupId && seq !== void 0 && seq !== null) {
|
|
11324
12507
|
const ns = `group:${groupId}`;
|
|
12508
|
+
if (seq > 0) this._seqTracker.updateMaxSeen(ns, seq);
|
|
11325
12509
|
const needPull = this._seqTracker.onMessageSeq(ns, seq);
|
|
11326
12510
|
if (needPull) {
|
|
11327
12511
|
this._safeAsync(this._fillGroupGap(groupId));
|
|
11328
12512
|
}
|
|
11329
12513
|
const contig = this._seqTracker.getContiguousSeq(ns);
|
|
11330
12514
|
if (contig > 0) {
|
|
12515
|
+
const maxSeen = this._seqTracker.getMaxSeenSeq(ns);
|
|
12516
|
+
const ackSeq = maxSeen > 0 ? Math.min(contig, maxSeen) : contig;
|
|
11331
12517
|
this._transport.call("group.ack_messages", {
|
|
11332
12518
|
group_id: groupId,
|
|
11333
|
-
msg_seq:
|
|
12519
|
+
msg_seq: ackSeq,
|
|
11334
12520
|
device_id: this._deviceId,
|
|
11335
12521
|
slot_id: this._slotId
|
|
11336
12522
|
}).catch((e) => {
|
|
@@ -11357,6 +12543,7 @@ var _AUNClient = class _AUNClient {
|
|
|
11357
12543
|
timestamp: src.timestamp ?? null,
|
|
11358
12544
|
_decrypt_error: String(exc)
|
|
11359
12545
|
};
|
|
12546
|
+
attachV2EnvelopeMetadataFromSource(safeEvent, data);
|
|
11360
12547
|
await this._publishAppEvent("group.message_undecryptable", safeEvent);
|
|
11361
12548
|
}
|
|
11362
12549
|
}
|
|
@@ -11470,50 +12657,73 @@ var _AUNClient = class _AUNClient {
|
|
|
11470
12657
|
this._gapFillDone.add(dedupKey);
|
|
11471
12658
|
this._gapFillActive = true;
|
|
11472
12659
|
try {
|
|
11473
|
-
|
|
11474
|
-
|
|
11475
|
-
|
|
11476
|
-
|
|
11477
|
-
|
|
11478
|
-
|
|
11479
|
-
|
|
12660
|
+
let nextAfterSeq = afterSeq;
|
|
12661
|
+
const maxPages = 100;
|
|
12662
|
+
let pageCount = 0;
|
|
12663
|
+
while (pageCount < maxPages) {
|
|
12664
|
+
pageCount += 1;
|
|
12665
|
+
const result = await this.call("group.pull_events", {
|
|
12666
|
+
group_id: groupId,
|
|
12667
|
+
after_event_seq: nextAfterSeq,
|
|
12668
|
+
device_id: this._deviceId,
|
|
12669
|
+
limit: 50
|
|
12670
|
+
});
|
|
12671
|
+
if (!isJsonObject(result)) return;
|
|
11480
12672
|
const events = result.events;
|
|
11481
|
-
if (Array.isArray(events))
|
|
11482
|
-
|
|
11483
|
-
|
|
11484
|
-
|
|
11485
|
-
|
|
11486
|
-
|
|
11487
|
-
|
|
11488
|
-
|
|
11489
|
-
|
|
11490
|
-
|
|
11491
|
-
|
|
11492
|
-
|
|
11493
|
-
|
|
11494
|
-
if (contig > 0 && (events.length > 0 || serverAck > 0)) {
|
|
11495
|
-
this._transport.call("group.ack_events", {
|
|
11496
|
-
group_id: groupId,
|
|
11497
|
-
event_seq: contig,
|
|
11498
|
-
device_id: this._deviceId,
|
|
11499
|
-
slot_id: this._slotId
|
|
11500
|
-
}).catch((e) => {
|
|
11501
|
-
this._clientLog.warn("group event auto-ack failed: group=" + groupId, e);
|
|
11502
|
-
});
|
|
12673
|
+
if (!Array.isArray(events)) return;
|
|
12674
|
+
const pageContigBefore = this._seqTracker.getContiguousSeq(ns);
|
|
12675
|
+
const eventObjects = events.filter(isJsonObject);
|
|
12676
|
+
if (eventObjects.length > 0) {
|
|
12677
|
+
this._seqTracker.onPullResult(ns, eventObjects, nextAfterSeq);
|
|
12678
|
+
}
|
|
12679
|
+
const cursor = isJsonObject(result.cursor) ? result.cursor : null;
|
|
12680
|
+
const serverAck = cursor ? Number(cursor.current_seq ?? 0) : 0;
|
|
12681
|
+
if (serverAck > 0) {
|
|
12682
|
+
const contigBeforeFloor = this._seqTracker.getContiguousSeq(ns);
|
|
12683
|
+
if (contigBeforeFloor < serverAck) {
|
|
12684
|
+
this._clientLog.info("group.pull_events retention-floor advance: ns=" + ns + " contiguous=" + contigBeforeFloor + " -> cursor.current_seq=" + serverAck);
|
|
12685
|
+
this._seqTracker.forceContiguousSeq(ns, serverAck);
|
|
11503
12686
|
}
|
|
11504
|
-
|
|
11505
|
-
|
|
11506
|
-
|
|
11507
|
-
|
|
11508
|
-
|
|
11509
|
-
|
|
11510
|
-
|
|
11511
|
-
|
|
11512
|
-
|
|
11513
|
-
|
|
12687
|
+
}
|
|
12688
|
+
const eventSeqs = [];
|
|
12689
|
+
for (const evt of eventObjects) {
|
|
12690
|
+
const eventSeq = Number(evt.event_seq ?? 0);
|
|
12691
|
+
if (Number.isFinite(eventSeq) && eventSeq > 0) eventSeqs.push(eventSeq);
|
|
12692
|
+
evt._from_gap_fill = true;
|
|
12693
|
+
const et = String(evt.event_type ?? "");
|
|
12694
|
+
if (et === "group.message_created") continue;
|
|
12695
|
+
const cs = evt.client_signature;
|
|
12696
|
+
if (cs && typeof cs === "object") {
|
|
12697
|
+
if (this._shouldSkipEventSignature(evt)) {
|
|
12698
|
+
delete evt.client_signature;
|
|
12699
|
+
} else {
|
|
12700
|
+
evt._verified = await this._verifyEventSignature(evt, cs);
|
|
11514
12701
|
}
|
|
11515
12702
|
}
|
|
12703
|
+
await this._dispatcher.publish("group.changed", evt);
|
|
12704
|
+
}
|
|
12705
|
+
const contig = this._seqTracker.getContiguousSeq(ns);
|
|
12706
|
+
if (contig !== pageContigBefore) {
|
|
12707
|
+
this._saveSeqTrackerState();
|
|
12708
|
+
}
|
|
12709
|
+
if (eventObjects.length > 0 && contig > 0 && contig !== pageContigBefore) {
|
|
12710
|
+
const maxSeen = this._seqTracker.getMaxSeenSeq(ns);
|
|
12711
|
+
const ackSeq = maxSeen > 0 ? Math.min(contig, maxSeen) : contig;
|
|
12712
|
+
this._transport.call("group.ack_events", {
|
|
12713
|
+
group_id: groupId,
|
|
12714
|
+
event_seq: ackSeq,
|
|
12715
|
+
device_id: this._deviceId,
|
|
12716
|
+
slot_id: this._slotId
|
|
12717
|
+
}).catch((e) => {
|
|
12718
|
+
this._clientLog.warn("group event auto-ack failed: group=" + groupId, e);
|
|
12719
|
+
});
|
|
11516
12720
|
}
|
|
12721
|
+
const nextAfter = Math.max(eventSeqs.length > 0 ? Math.max(...eventSeqs) : nextAfterSeq, nextAfterSeq);
|
|
12722
|
+
if (eventObjects.length === 0 || nextAfter <= nextAfterSeq || result.has_more === false) break;
|
|
12723
|
+
nextAfterSeq = nextAfter;
|
|
12724
|
+
}
|
|
12725
|
+
if (pageCount >= maxPages) {
|
|
12726
|
+
this._clientLog.warn(`group event gap fill reached max_pages=${maxPages} group=${groupId} after_seq=${nextAfterSeq}`);
|
|
11517
12727
|
}
|
|
11518
12728
|
} catch (exc) {
|
|
11519
12729
|
this._clientLog.warn(`group event gap-fill failed:${String(exc)}`);
|
|
@@ -11611,10 +12821,10 @@ var _AUNClient = class _AUNClient {
|
|
|
11611
12821
|
_attachCurrentInstanceContext(payload) {
|
|
11612
12822
|
if (!isJsonObject(payload)) return payload;
|
|
11613
12823
|
const result = { ...payload };
|
|
11614
|
-
if (
|
|
12824
|
+
if (!("device_id" in result)) {
|
|
11615
12825
|
result.device_id = this._deviceId;
|
|
11616
12826
|
}
|
|
11617
|
-
if (
|
|
12827
|
+
if (!("slot_id" in result)) {
|
|
11618
12828
|
result.slot_id = this._slotId;
|
|
11619
12829
|
}
|
|
11620
12830
|
return result;
|
|
@@ -11664,6 +12874,15 @@ var _AUNClient = class _AUNClient {
|
|
|
11664
12874
|
const trace = `${this._echoTimestamp()} [AUN-SDK.send] aid=${this._aid ?? "-"} conn_uptime=${uptime}s`;
|
|
11665
12875
|
params.payload = { ...payload, text: payload.text + "\n" + trace };
|
|
11666
12876
|
}
|
|
12877
|
+
_shouldSkipClientSignature(method, params) {
|
|
12878
|
+
if (method !== "message.send" && method !== "group.send") return false;
|
|
12879
|
+
if (params.encrypted || params.encrypt) return false;
|
|
12880
|
+
return this._isEchoPayload(params.payload);
|
|
12881
|
+
}
|
|
12882
|
+
_shouldSkipEventSignature(event) {
|
|
12883
|
+
if (event.encrypted || event.encrypt) return false;
|
|
12884
|
+
return this._isEchoPayload(event.payload);
|
|
12885
|
+
}
|
|
11667
12886
|
_maybeAppendEchoTraceReceive(msg) {
|
|
11668
12887
|
if (msg.encrypted) return;
|
|
11669
12888
|
const payload = msg.payload;
|
|
@@ -11674,13 +12893,17 @@ var _AUNClient = class _AUNClient {
|
|
|
11674
12893
|
}
|
|
11675
12894
|
_messageTargetsCurrentInstance(message) {
|
|
11676
12895
|
if (!isJsonObject(message)) return true;
|
|
11677
|
-
|
|
11678
|
-
|
|
11679
|
-
|
|
12896
|
+
if ("device_id" in message) {
|
|
12897
|
+
const targetDeviceId = String(message.device_id ?? "").trim();
|
|
12898
|
+
if (targetDeviceId !== this._deviceId) {
|
|
12899
|
+
return false;
|
|
12900
|
+
}
|
|
11680
12901
|
}
|
|
11681
|
-
|
|
11682
|
-
|
|
11683
|
-
|
|
12902
|
+
if ("slot_id" in message) {
|
|
12903
|
+
const targetSlotId = String(message.slot_id ?? "").trim();
|
|
12904
|
+
if (targetSlotId !== this._slotId) {
|
|
12905
|
+
return false;
|
|
12906
|
+
}
|
|
11684
12907
|
}
|
|
11685
12908
|
return true;
|
|
11686
12909
|
}
|
|
@@ -11762,26 +12985,36 @@ var _AUNClient = class _AUNClient {
|
|
|
11762
12985
|
const d = data;
|
|
11763
12986
|
const cs = d.client_signature;
|
|
11764
12987
|
if (cs && isJsonObject(cs)) {
|
|
11765
|
-
|
|
12988
|
+
if (this._shouldSkipEventSignature(d)) {
|
|
12989
|
+
delete d.client_signature;
|
|
12990
|
+
} else {
|
|
12991
|
+
d._verified = await this._verifyEventSignature(d, cs);
|
|
12992
|
+
}
|
|
11766
12993
|
}
|
|
11767
12994
|
await this._dispatcher.publish("group.changed", d);
|
|
11768
12995
|
const groupId = d.group_id ?? "";
|
|
11769
12996
|
if (groupId) {
|
|
11770
12997
|
this._v2BootstrapCache.delete(`group:${groupId}`);
|
|
11771
12998
|
}
|
|
12999
|
+
const membershipActions = /* @__PURE__ */ new Set([
|
|
13000
|
+
"member_added",
|
|
13001
|
+
"member_left",
|
|
13002
|
+
"member_removed",
|
|
13003
|
+
"role_changed",
|
|
13004
|
+
"owner_transferred",
|
|
13005
|
+
"joined",
|
|
13006
|
+
"join_approved",
|
|
13007
|
+
"invite_code_used"
|
|
13008
|
+
]);
|
|
11772
13009
|
if (this._v2Session && groupId) {
|
|
11773
|
-
const membershipActions = /* @__PURE__ */ new Set([
|
|
11774
|
-
"member_added",
|
|
11775
|
-
"member_left",
|
|
11776
|
-
"member_removed",
|
|
11777
|
-
"role_changed",
|
|
11778
|
-
"owner_transferred",
|
|
11779
|
-
"joined",
|
|
11780
|
-
"join_approved"
|
|
11781
|
-
]);
|
|
11782
13010
|
if (membershipActions.has(action)) {
|
|
11783
13011
|
const callFn = async (method, params) => this.call(method, params);
|
|
11784
|
-
|
|
13012
|
+
const joinedAid = String(d.joined_aid ?? d.member_aid ?? d.aid ?? "").trim();
|
|
13013
|
+
const actorAid = String(d.actor_aid ?? "").trim();
|
|
13014
|
+
const selfAid = String(this._aid ?? "").trim();
|
|
13015
|
+
const joinActions = /* @__PURE__ */ new Set(["member_added", "joined", "join_approved", "invite_code_used"]);
|
|
13016
|
+
const isSelfJoin = joinActions.has(action) && !!selfAid && (joinedAid === selfAid || !joinedAid && (action === "joined" || action === "invite_code_used") && actorAid === selfAid);
|
|
13017
|
+
if (isSelfJoin) {
|
|
11785
13018
|
this._v2Session.ensureGroupRegistered?.(groupId, callFn)?.catch((exc) => {
|
|
11786
13019
|
this._clientLog.debug(`group SPK registration failed (non-fatal): group=${groupId} action=${action} err=${exc}`);
|
|
11787
13020
|
});
|
|
@@ -11792,7 +13025,7 @@ var _AUNClient = class _AUNClient {
|
|
|
11792
13025
|
}
|
|
11793
13026
|
}
|
|
11794
13027
|
}
|
|
11795
|
-
if (groupId && action === "upsert"
|
|
13028
|
+
if (groupId && this._v2Session && (action === "upsert" || membershipActions.has(action))) {
|
|
11796
13029
|
this._safeAsync(this._v2AutoProposeState(groupId, { leaderDelay: true }));
|
|
11797
13030
|
}
|
|
11798
13031
|
let needPull = false;
|
|
@@ -11849,12 +13082,16 @@ var _AUNClient = class _AUNClient {
|
|
|
11849
13082
|
async _onGroupStateCommittedImpl(d, groupId) {
|
|
11850
13083
|
const cs = d.client_signature;
|
|
11851
13084
|
if (cs && isJsonObject(cs)) {
|
|
11852
|
-
|
|
11853
|
-
|
|
11854
|
-
|
|
11855
|
-
|
|
13085
|
+
if (this._shouldSkipEventSignature(d)) {
|
|
13086
|
+
delete d.client_signature;
|
|
13087
|
+
} else {
|
|
13088
|
+
const verified = await this._verifyEventSignature(d, cs);
|
|
13089
|
+
if (verified === false) {
|
|
13090
|
+
this._clientLog.warn(`state_committed committer signature verify failed group=%s${String(groupId)}`);
|
|
13091
|
+
return;
|
|
13092
|
+
}
|
|
13093
|
+
d._verified = verified;
|
|
11856
13094
|
}
|
|
11857
|
-
d._verified = verified;
|
|
11858
13095
|
}
|
|
11859
13096
|
const stateVersion = Number(d.state_version ?? 0);
|
|
11860
13097
|
const stateHash = String(d.state_hash ?? "").trim();
|
|
@@ -12110,6 +13347,7 @@ var _AUNClient = class _AUNClient {
|
|
|
12110
13347
|
let e2eeMeta = null;
|
|
12111
13348
|
let decryptFailed = false;
|
|
12112
13349
|
if (isV2Envelope) {
|
|
13350
|
+
e2eeMeta = v2E2eeMeta(payload);
|
|
12113
13351
|
const plaintext = await this._decryptV2EnvelopeForThought({
|
|
12114
13352
|
envelope: payload,
|
|
12115
13353
|
fromAid: senderAid
|
|
@@ -12120,7 +13358,7 @@ var _AUNClient = class _AUNClient {
|
|
|
12120
13358
|
decryptedPayload = payload;
|
|
12121
13359
|
} else {
|
|
12122
13360
|
decryptedPayload = plaintext;
|
|
12123
|
-
const e2eeObj =
|
|
13361
|
+
const e2eeObj = e2eeMeta;
|
|
12124
13362
|
const ph = payload.protected_headers;
|
|
12125
13363
|
if (isJsonObject(ph)) {
|
|
12126
13364
|
const phBody = {};
|
|
@@ -12150,6 +13388,7 @@ var _AUNClient = class _AUNClient {
|
|
|
12150
13388
|
created_at: item.created_at,
|
|
12151
13389
|
e2ee: e2eeMeta
|
|
12152
13390
|
};
|
|
13391
|
+
if (isJsonObject(e2eeMeta)) attachV2EnvelopeMetadata(thought, e2eeMeta);
|
|
12153
13392
|
if (decryptFailed) thought.decrypt_failed = true;
|
|
12154
13393
|
if ("context" in item) thought.context = item.context;
|
|
12155
13394
|
thoughts.push(thought);
|
|
@@ -12184,6 +13423,8 @@ var _AUNClient = class _AUNClient {
|
|
|
12184
13423
|
let decrypted = message;
|
|
12185
13424
|
let decryptFailed = false;
|
|
12186
13425
|
if (payload?.type === "e2ee.p2p_encrypted") {
|
|
13426
|
+
const e2eeObj = v2E2eeMeta(payload);
|
|
13427
|
+
message.e2ee = e2eeObj;
|
|
12187
13428
|
const plaintext = await this._decryptV2EnvelopeForThought({
|
|
12188
13429
|
envelope: payload,
|
|
12189
13430
|
fromAid
|
|
@@ -12194,7 +13435,6 @@ var _AUNClient = class _AUNClient {
|
|
|
12194
13435
|
} else {
|
|
12195
13436
|
decrypted = { ...message };
|
|
12196
13437
|
decrypted.payload = plaintext;
|
|
12197
|
-
const e2eeObj = v2E2eeMeta(payload);
|
|
12198
13438
|
const ph = payload.protected_headers;
|
|
12199
13439
|
if (isJsonObject(ph)) {
|
|
12200
13440
|
const phBody = {};
|
|
@@ -12216,6 +13456,7 @@ var _AUNClient = class _AUNClient {
|
|
|
12216
13456
|
} else if (payload?.type === "e2ee.encrypted") {
|
|
12217
13457
|
decryptFailed = true;
|
|
12218
13458
|
}
|
|
13459
|
+
const exposedE2ee = (decrypted ?? message).e2ee;
|
|
12219
13460
|
const thought = {
|
|
12220
13461
|
thought_id: thoughtId,
|
|
12221
13462
|
message_id: thoughtId,
|
|
@@ -12223,8 +13464,9 @@ var _AUNClient = class _AUNClient {
|
|
|
12223
13464
|
to: toAid,
|
|
12224
13465
|
payload: (decrypted ?? message).payload,
|
|
12225
13466
|
created_at: item.created_at,
|
|
12226
|
-
e2ee:
|
|
13467
|
+
e2ee: exposedE2ee
|
|
12227
13468
|
};
|
|
13469
|
+
if (isJsonObject(exposedE2ee)) attachV2EnvelopeMetadata(thought, exposedE2ee);
|
|
12228
13470
|
if (decryptFailed) thought.decrypt_failed = true;
|
|
12229
13471
|
if ("context" in item) thought.context = item.context;
|
|
12230
13472
|
thoughts.push(thought);
|
|
@@ -12236,7 +13478,7 @@ var _AUNClient = class _AUNClient {
|
|
|
12236
13478
|
* 获取对方证书(带缓存 + 完整 PKI 验证:链 + CRL + OCSP + AID 绑定)。
|
|
12237
13479
|
* 跨域时自动将请求路由到 peer 所在域的 Gateway。
|
|
12238
13480
|
*/
|
|
12239
|
-
async _fetchPeerCert(aid, certFingerprint) {
|
|
13481
|
+
async _fetchPeerCert(aid, certFingerprint, timeoutMs = 5e3) {
|
|
12240
13482
|
const tStart = Date.now();
|
|
12241
13483
|
this._clientLog.debug(`_fetchPeerCert enter: aid=${aid} fingerprint=${certFingerprint ?? "<none>"}`);
|
|
12242
13484
|
try {
|
|
@@ -12256,7 +13498,7 @@ var _AUNClient = class _AUNClient {
|
|
|
12256
13498
|
try {
|
|
12257
13499
|
const certUrl = buildCertUrl(peerGatewayUrl, aid, certFingerprint);
|
|
12258
13500
|
const controller = new AbortController();
|
|
12259
|
-
const timeoutId = setTimeout(() => controller.abort(),
|
|
13501
|
+
const timeoutId = setTimeout(() => controller.abort(), timeoutMs);
|
|
12260
13502
|
try {
|
|
12261
13503
|
const resp = await fetch(certUrl, { signal: controller.signal });
|
|
12262
13504
|
if (!resp.ok) throw new ValidationError(`failed to fetch peer cert for ${aid}: HTTP ${resp.status}`);
|
|
@@ -12269,7 +13511,7 @@ var _AUNClient = class _AUNClient {
|
|
|
12269
13511
|
throw exc;
|
|
12270
13512
|
}
|
|
12271
13513
|
const fallbackController = new AbortController();
|
|
12272
|
-
const fallbackTimeoutId = setTimeout(() => fallbackController.abort(),
|
|
13514
|
+
const fallbackTimeoutId = setTimeout(() => fallbackController.abort(), timeoutMs);
|
|
12273
13515
|
try {
|
|
12274
13516
|
const fallbackResp = await fetch(buildCertUrl(peerGatewayUrl, aid), { signal: fallbackController.signal });
|
|
12275
13517
|
if (!fallbackResp.ok) {
|
|
@@ -12526,6 +13768,10 @@ var _AUNClient = class _AUNClient {
|
|
|
12526
13768
|
}
|
|
12527
13769
|
}
|
|
12528
13770
|
_resolveGateway(params) {
|
|
13771
|
+
const gateways = this._resolveGateways(params);
|
|
13772
|
+
return gateways[0];
|
|
13773
|
+
}
|
|
13774
|
+
_resolveGateways(params) {
|
|
12529
13775
|
const topology = isJsonObject(params.topology) ? params.topology : null;
|
|
12530
13776
|
if (topology) {
|
|
12531
13777
|
const mode = String(topology.mode ?? "gateway");
|
|
@@ -12536,9 +13782,14 @@ var _AUNClient = class _AUNClient {
|
|
|
12536
13782
|
throw new ValidationError("relay topology is not implemented in the Browser SDK");
|
|
12537
13783
|
}
|
|
12538
13784
|
}
|
|
12539
|
-
const
|
|
13785
|
+
const gw = params.gateway ?? params.gateways;
|
|
13786
|
+
if (Array.isArray(gw)) {
|
|
13787
|
+
const urls = gw.map((g) => String(g ?? "")).filter((u) => u.length > 0);
|
|
13788
|
+
if (urls.length > 0) return urls;
|
|
13789
|
+
}
|
|
13790
|
+
const gateway = String(gw ?? this._gatewayUrl ?? "");
|
|
12540
13791
|
if (!gateway) throw new StateError("missing gateway in connect params");
|
|
12541
|
-
return gateway;
|
|
13792
|
+
return [gateway];
|
|
12542
13793
|
}
|
|
12543
13794
|
async _syncIdentityAfterConnect(accessToken) {
|
|
12544
13795
|
let identity = null;
|
|
@@ -12798,6 +14049,16 @@ var _AUNClient = class _AUNClient {
|
|
|
12798
14049
|
};
|
|
12799
14050
|
scheduleRefresh(0);
|
|
12800
14051
|
}
|
|
14052
|
+
_normalizeOutboundMessagePayload(params, method = "") {
|
|
14053
|
+
if (!Object.prototype.hasOwnProperty.call(params, "payload") && Object.prototype.hasOwnProperty.call(params, "content")) {
|
|
14054
|
+
params.payload = params.content;
|
|
14055
|
+
delete params.content;
|
|
14056
|
+
}
|
|
14057
|
+
const payload = params.payload;
|
|
14058
|
+
if (isJsonObject(payload) && !Object.prototype.hasOwnProperty.call(payload, "type") && typeof payload.text === "string") {
|
|
14059
|
+
params.payload = { type: "text", ...payload };
|
|
14060
|
+
}
|
|
14061
|
+
}
|
|
12801
14062
|
_validateMessageRecipient(toAid) {
|
|
12802
14063
|
if (isGroupServiceAid(toAid)) {
|
|
12803
14064
|
throw new ValidationError("message.send receiver cannot be group.{issuer}; use group.send instead");
|
|
@@ -13204,6 +14465,8 @@ var _AUNClient = class _AUNClient {
|
|
|
13204
14465
|
this._gapFillDone.clear();
|
|
13205
14466
|
this._pushedSeqs.clear();
|
|
13206
14467
|
this._pendingOrderedMsgs.clear();
|
|
14468
|
+
this._v2SenderIKPending.clear();
|
|
14469
|
+
this._v2SenderIKFetching.clear();
|
|
13207
14470
|
this._groupSynced.clear();
|
|
13208
14471
|
}
|
|
13209
14472
|
_refreshSeqTrackerContext() {
|
|
@@ -13213,6 +14476,8 @@ var _AUNClient = class _AUNClient {
|
|
|
13213
14476
|
this._gapFillDone.clear();
|
|
13214
14477
|
this._pushedSeqs.clear();
|
|
13215
14478
|
this._pendingOrderedMsgs.clear();
|
|
14479
|
+
this._v2SenderIKPending.clear();
|
|
14480
|
+
this._v2SenderIKFetching.clear();
|
|
13216
14481
|
this._groupSynced.clear();
|
|
13217
14482
|
this._seqTrackerContext = nextContext;
|
|
13218
14483
|
}
|
|
@@ -13264,6 +14529,46 @@ var _AUNClient = class _AUNClient {
|
|
|
13264
14529
|
});
|
|
13265
14530
|
}
|
|
13266
14531
|
}
|
|
14532
|
+
_persistRepairedSeq(ns) {
|
|
14533
|
+
if (!this._aid || !ns) return;
|
|
14534
|
+
const seq = this._seqTracker.getContiguousSeq(ns);
|
|
14535
|
+
try {
|
|
14536
|
+
if (seq > 0 && typeof this._keystore.saveSeq === "function") {
|
|
14537
|
+
this._keystore.saveSeq(this._aid, this._deviceId, this._slotId, ns, seq).catch((exc) => {
|
|
14538
|
+
this._clientLog.debug(`persist repaired seq failed: ns=${ns} err=${formatCaughtError(exc)}`);
|
|
14539
|
+
});
|
|
14540
|
+
return;
|
|
14541
|
+
}
|
|
14542
|
+
const deleteSeq = this._keystore.deleteSeq;
|
|
14543
|
+
if (seq <= 0 && typeof deleteSeq === "function") {
|
|
14544
|
+
deleteSeq.call(this._keystore, this._aid, this._deviceId, this._slotId, ns).catch((exc) => {
|
|
14545
|
+
this._clientLog.debug(`delete repaired seq failed: ns=${ns} err=${formatCaughtError(exc)}`);
|
|
14546
|
+
});
|
|
14547
|
+
return;
|
|
14548
|
+
}
|
|
14549
|
+
if (seq > 0) {
|
|
14550
|
+
this._saveSeqTrackerState();
|
|
14551
|
+
}
|
|
14552
|
+
} catch (exc) {
|
|
14553
|
+
this._clientLog.debug(`persist repaired seq failed: ns=${ns} err=${formatCaughtError(exc)}`);
|
|
14554
|
+
}
|
|
14555
|
+
}
|
|
14556
|
+
_repairPushContiguousBound(ns, pushSeq, hasPayload, label) {
|
|
14557
|
+
if (!ns || !Number.isFinite(pushSeq) || pushSeq <= 0) {
|
|
14558
|
+
return ns ? this._seqTracker.getContiguousSeq(ns) : 0;
|
|
14559
|
+
}
|
|
14560
|
+
const contig = this._seqTracker.getContiguousSeq(ns);
|
|
14561
|
+
const shouldRepair = contig > pushSeq;
|
|
14562
|
+
if (!shouldRepair) return contig;
|
|
14563
|
+
const repairedTo = Math.max(0, pushSeq - 1);
|
|
14564
|
+
this._seqTracker.repairContiguousSeq(ns, repairedTo);
|
|
14565
|
+
const repaired = this._seqTracker.getContiguousSeq(ns);
|
|
14566
|
+
this._persistRepairedSeq(ns);
|
|
14567
|
+
this._clientLog.warn(
|
|
14568
|
+
`${label} push repaired contiguous_seq: ns=${ns} payload=${hasPayload} push_seq=${pushSeq} contiguous=${contig}->${repaired}`
|
|
14569
|
+
);
|
|
14570
|
+
return repaired;
|
|
14571
|
+
}
|
|
13267
14572
|
// ── V2 E2EE API(async,与 Python `client.py` `_init_v2_session` / `send_v2` / `pull_v2` / `ack_v2` 对齐) ──
|
|
13268
14573
|
/**
|
|
13269
14574
|
* 初始化 V2 session:从 AID PEM 私钥提取 raw scalar + DER 公钥,
|
|
@@ -13322,39 +14627,212 @@ var _AUNClient = class _AUNClient {
|
|
|
13322
14627
|
this._clientLog.debug(`V2 session initialized aid=${this._aid} device=${this._deviceId}`);
|
|
13323
14628
|
this._safeAsync(this._v2AutoConfirmPendingProposals());
|
|
13324
14629
|
}
|
|
14630
|
+
async _v2TrustedIKPubDer(aid) {
|
|
14631
|
+
const normalizedAid = String(aid ?? "").trim();
|
|
14632
|
+
if (!normalizedAid) throw new E2EEError("spk_aid_missing");
|
|
14633
|
+
if (this._aid && normalizedAid === this._aid) {
|
|
14634
|
+
if (!this._v2Session) throw new E2EEError("V2 session not initialized");
|
|
14635
|
+
return this._v2Session.currentIkPubDer;
|
|
14636
|
+
}
|
|
14637
|
+
const certPem = await this._fetchPeerCert(normalizedAid);
|
|
14638
|
+
const pubKey = await importCertPublicKeyEcdsa(certPem);
|
|
14639
|
+
return new Uint8Array(await crypto.subtle.exportKey("spki", pubKey));
|
|
14640
|
+
}
|
|
14641
|
+
_v2SPKTimestampText(value, aid, deviceId, spkId) {
|
|
14642
|
+
if (value === null || value === void 0 || value === "") {
|
|
14643
|
+
throw new E2EEError(`spk_timestamp_missing: aid=${aid} device_id=${deviceId} spk_id=${spkId}`);
|
|
14644
|
+
}
|
|
14645
|
+
if (typeof value === "boolean") {
|
|
14646
|
+
throw new E2EEError(`spk_timestamp_invalid: aid=${aid} device_id=${deviceId} spk_id=${spkId}`);
|
|
14647
|
+
}
|
|
14648
|
+
if (typeof value === "number") {
|
|
14649
|
+
if (!Number.isSafeInteger(value)) {
|
|
14650
|
+
throw new E2EEError(`spk_timestamp_invalid: aid=${aid} device_id=${deviceId} spk_id=${spkId}`);
|
|
14651
|
+
}
|
|
14652
|
+
return String(value);
|
|
14653
|
+
}
|
|
14654
|
+
const text = String(value).trim();
|
|
14655
|
+
if (!/^\d+$/.test(text)) {
|
|
14656
|
+
throw new E2EEError(`spk_timestamp_invalid: aid=${aid} device_id=${deviceId} spk_id=${spkId}`);
|
|
14657
|
+
}
|
|
14658
|
+
return BigInt(text).toString();
|
|
14659
|
+
}
|
|
14660
|
+
async _v2VerifySPKDevice(args) {
|
|
14661
|
+
if (!this._v2Session) throw new E2EEError("V2 session not initialized");
|
|
14662
|
+
const spkId = String(args.dev.spk_id ?? "").trim();
|
|
14663
|
+
if (!spkId) return;
|
|
14664
|
+
if (args.keySource !== "peer_device_prekey" && args.keySource !== "group_device_prekey") {
|
|
14665
|
+
throw new E2EEError(`spk_key_source_invalid: aid=${args.aid} device_id=${args.deviceId} spk_id=${spkId} key_source=${args.keySource}`);
|
|
14666
|
+
}
|
|
14667
|
+
if (!args.spkPkDer || args.spkPkDer.length === 0) {
|
|
14668
|
+
throw new E2EEError(`spk_public_key_missing: aid=${args.aid} device_id=${args.deviceId} spk_id=${spkId}`);
|
|
14669
|
+
}
|
|
14670
|
+
const spkHash = bytesToHex6(new Uint8Array(await crypto.subtle.digest("SHA-256", args.spkPkDer.slice().buffer)));
|
|
14671
|
+
const expectedSpkId = `sha256:${spkHash.substring(0, 16)}`;
|
|
14672
|
+
if (spkId !== expectedSpkId) {
|
|
14673
|
+
throw new E2EEError(`spk_id_mismatch: aid=${args.aid} device_id=${args.deviceId} spk_id=${spkId} expected=${expectedSpkId}`);
|
|
14674
|
+
}
|
|
14675
|
+
const trustedIK = await this._v2TrustedIKPubDer(args.aid);
|
|
14676
|
+
if (!_v2BytesEqual(trustedIK, args.ikPkDer)) {
|
|
14677
|
+
throw new E2EEError(`spk_ik_mismatch: aid=${args.aid} device_id=${args.deviceId} spk_id=${spkId}`);
|
|
14678
|
+
}
|
|
14679
|
+
if (_v2BytesEqual(args.spkPkDer, trustedIK)) {
|
|
14680
|
+
this._v2Session.markPeerSPKVerified(args.aid, args.deviceId, spkId);
|
|
14681
|
+
return;
|
|
14682
|
+
}
|
|
14683
|
+
const sigB64 = String(args.dev.spk_signature ?? "").trim();
|
|
14684
|
+
if (!sigB64) {
|
|
14685
|
+
throw new E2EEError(`spk_signature_missing: aid=${args.aid} device_id=${args.deviceId} spk_id=${spkId}`);
|
|
14686
|
+
}
|
|
14687
|
+
let signature;
|
|
14688
|
+
try {
|
|
14689
|
+
signature = _v2B64ToBytesStrict(sigB64);
|
|
14690
|
+
} catch {
|
|
14691
|
+
throw new E2EEError(`spk_signature_invalid_base64: aid=${args.aid} device_id=${args.deviceId} spk_id=${spkId}`);
|
|
14692
|
+
}
|
|
14693
|
+
const encoder6 = new TextEncoder();
|
|
14694
|
+
const tsText = this._v2SPKTimestampText(args.dev.spk_timestamp, args.aid, args.deviceId, spkId);
|
|
14695
|
+
const signData = _v2ConcatBytes(args.spkPkDer, encoder6.encode(spkId), encoder6.encode(tsText));
|
|
14696
|
+
if (!await ecdsaVerifyRaw(trustedIK, signature, signData)) {
|
|
14697
|
+
throw new E2EEError(`spk_signature_invalid: aid=${args.aid} device_id=${args.deviceId} spk_id=${spkId}`);
|
|
14698
|
+
}
|
|
14699
|
+
this._v2Session.markPeerSPKVerified(args.aid, args.deviceId, spkId);
|
|
14700
|
+
}
|
|
14701
|
+
async _v2BuildTargetFromDevice(args) {
|
|
14702
|
+
const aid = String(args.aid ?? "").trim();
|
|
14703
|
+
const devId = getV2DeviceId(args.dev);
|
|
14704
|
+
const deviceId = devId.present ? devId.value : String(args.deviceId ?? "").trim();
|
|
14705
|
+
const ikPk = String(args.dev.ik_pk ?? "").trim();
|
|
14706
|
+
if (!aid || !devId.present || !ikPk) return null;
|
|
14707
|
+
const ikPkDer = _v2B64ToBytes(ikPk);
|
|
14708
|
+
const spkPkDer = args.dev.spk_pk ? _v2B64ToBytes(String(args.dev.spk_pk)) : void 0;
|
|
14709
|
+
const keySource = String(args.dev.key_source ?? args.defaultKeySource).trim() || args.defaultKeySource;
|
|
14710
|
+
await this._v2VerifySPKDevice({ dev: args.dev, aid, deviceId, ikPkDer, spkPkDer, keySource });
|
|
14711
|
+
this._v2Session?.cachePeerIK(aid, deviceId, ikPkDer);
|
|
14712
|
+
return {
|
|
14713
|
+
aid,
|
|
14714
|
+
deviceId,
|
|
14715
|
+
role: args.role,
|
|
14716
|
+
keySource,
|
|
14717
|
+
ikPkDer,
|
|
14718
|
+
spkPkDer,
|
|
14719
|
+
spkId: String(args.dev.spk_id ?? "").trim()
|
|
14720
|
+
};
|
|
14721
|
+
}
|
|
13325
14722
|
async _getV2SenderPubDer(fromAid, senderDeviceId) {
|
|
13326
14723
|
const session = this._v2Session;
|
|
13327
14724
|
if (!session || !fromAid) return null;
|
|
13328
14725
|
let senderPubDer = session.getPeerIK(fromAid, senderDeviceId);
|
|
13329
14726
|
if (senderPubDer) return senderPubDer;
|
|
13330
14727
|
try {
|
|
13331
|
-
const
|
|
13332
|
-
const peers = Array.isArray(bs?.peer_devices) ? bs.peer_devices : [];
|
|
13333
|
-
for (const dev of peers) {
|
|
13334
|
-
const devId = String(dev.device_id ?? dev.owner_device_id ?? "");
|
|
13335
|
-
const ikPk = String(dev.ik_pk ?? "");
|
|
13336
|
-
if (!devId || !ikPk) continue;
|
|
13337
|
-
session.cachePeerIK(fromAid, devId, _v2B64ToBytes(ikPk));
|
|
13338
|
-
}
|
|
13339
|
-
senderPubDer = session.getPeerIK(fromAid, senderDeviceId);
|
|
13340
|
-
if (senderPubDer) return senderPubDer;
|
|
13341
|
-
} catch (exc) {
|
|
13342
|
-
this._clientLog.warn(`V2 decrypt: bootstrap for sender ${fromAid} failed: ${String(formatCaughtError(exc))}`);
|
|
13343
|
-
}
|
|
13344
|
-
try {
|
|
13345
|
-
const certPem = await this._fetchPeerCert(fromAid);
|
|
14728
|
+
const certPem = await this._fetchPeerCert(fromAid, void 0, 3e3);
|
|
13346
14729
|
const pubKey = await importCertPublicKeyEcdsa(certPem);
|
|
13347
14730
|
senderPubDer = new Uint8Array(await crypto.subtle.exportKey("spki", pubKey));
|
|
13348
|
-
|
|
13349
|
-
|
|
13350
|
-
}
|
|
13351
|
-
this._clientLog.debug(`V2 decrypt: sender IK fallback from CA cert for ${fromAid}`);
|
|
14731
|
+
session.cachePeerIK(fromAid, senderDeviceId, senderPubDer);
|
|
14732
|
+
this._clientLog.debug(`V2 decrypt: sender IK fallback from PKI cert for ${fromAid}`);
|
|
13352
14733
|
return senderPubDer;
|
|
13353
14734
|
} catch (exc) {
|
|
13354
|
-
this._clientLog.warn(`V2 decrypt:
|
|
14735
|
+
this._clientLog.warn(`V2 decrypt: PKI cert sender IK fallback failed for ${fromAid}: ${String(formatCaughtError(exc))}`);
|
|
13355
14736
|
return null;
|
|
13356
14737
|
}
|
|
13357
14738
|
}
|
|
14739
|
+
_v2PendingSenderIKMessageKey(msg, groupId) {
|
|
14740
|
+
const messageId = String(msg.message_id ?? "").trim();
|
|
14741
|
+
const seq = String(msg.seq ?? "").trim();
|
|
14742
|
+
const prefix = groupId ? `group:${groupId}` : `p2p:${this._aid ?? ""}`;
|
|
14743
|
+
return `${prefix}:${messageId || seq || Math.random().toString(36).slice(2)}`;
|
|
14744
|
+
}
|
|
14745
|
+
_v2PendingSenderIKFetchKey(fromAid, senderDeviceId, groupId) {
|
|
14746
|
+
return `${fromAid}#${senderDeviceId}#${groupId || ""}`;
|
|
14747
|
+
}
|
|
14748
|
+
_cacheV2PeerIKFromDevice(dev, fallbackAid = "") {
|
|
14749
|
+
const session = this._v2Session;
|
|
14750
|
+
if (!session || !isJsonObject(dev)) return;
|
|
14751
|
+
const device = dev;
|
|
14752
|
+
const devId = getV2DeviceId(device);
|
|
14753
|
+
const aid = String(device.aid ?? fallbackAid ?? "").trim();
|
|
14754
|
+
const ikPk = String(device.ik_pk ?? "").trim();
|
|
14755
|
+
if (!devId.present || !aid || !ikPk) return;
|
|
14756
|
+
try {
|
|
14757
|
+
session.cachePeerIK(aid, devId.value, _v2B64ToBytes(ikPk));
|
|
14758
|
+
} catch (exc) {
|
|
14759
|
+
this._clientLog.debug(`V2 sender IK cache from bootstrap skipped aid=${aid} dev=${devId.value}: ${String(formatCaughtError(exc))}`);
|
|
14760
|
+
}
|
|
14761
|
+
}
|
|
14762
|
+
_scheduleV2SenderIKPending(args) {
|
|
14763
|
+
const fromAid = String(args.fromAid ?? "").trim();
|
|
14764
|
+
if (!fromAid) return;
|
|
14765
|
+
const senderDeviceId = String(args.senderDeviceId ?? "");
|
|
14766
|
+
const groupId = String(args.groupId ?? "").trim();
|
|
14767
|
+
const messageKey = this._v2PendingSenderIKMessageKey(args.msg, groupId);
|
|
14768
|
+
this._v2SenderIKPending.set(messageKey, {
|
|
14769
|
+
msg: { ...args.msg },
|
|
14770
|
+
fromAid,
|
|
14771
|
+
senderDeviceId,
|
|
14772
|
+
groupId,
|
|
14773
|
+
createdAt: Date.now()
|
|
14774
|
+
});
|
|
14775
|
+
this._clientLog.debug(`V2 decrypt pending sender IK: key=${messageKey} from=${fromAid} device=${senderDeviceId || "-"} group=${groupId || "<p2p>"} pending=${this._v2SenderIKPending.size}`);
|
|
14776
|
+
this._scheduleV2SenderIKFetch(fromAid, senderDeviceId, groupId);
|
|
14777
|
+
}
|
|
14778
|
+
_scheduleV2SenderIKFetch(fromAid, senderDeviceId, groupId) {
|
|
14779
|
+
const fetchKey = this._v2PendingSenderIKFetchKey(fromAid, senderDeviceId, groupId);
|
|
14780
|
+
if (!fromAid || this._v2SenderIKFetching.has(fetchKey)) return;
|
|
14781
|
+
this._v2SenderIKFetching.add(fetchKey);
|
|
14782
|
+
this._safeAsync(this._resolveV2SenderIKPending(fromAid, senderDeviceId, groupId, fetchKey));
|
|
14783
|
+
}
|
|
14784
|
+
async _resolveV2SenderIKPending(fromAid, senderDeviceId, groupId, fetchKey) {
|
|
14785
|
+
try {
|
|
14786
|
+
const session = this._v2Session;
|
|
14787
|
+
if (session && fromAid) {
|
|
14788
|
+
try {
|
|
14789
|
+
const bs = await this.call("message.v2.bootstrap", { peer_aid: fromAid });
|
|
14790
|
+
const peers = Array.isArray(bs?.peer_devices) ? bs.peer_devices : [];
|
|
14791
|
+
for (const dev of peers) this._cacheV2PeerIKFromDevice(dev, fromAid);
|
|
14792
|
+
} catch (exc) {
|
|
14793
|
+
this._clientLog.warn(`V2 sender IK pending bootstrap failed peer=${fromAid}: ${String(formatCaughtError(exc))}`);
|
|
14794
|
+
}
|
|
14795
|
+
if (groupId) {
|
|
14796
|
+
try {
|
|
14797
|
+
const gbs = await this.call("group.v2.bootstrap", { group_id: groupId });
|
|
14798
|
+
const devices = Array.isArray(gbs?.devices) ? gbs.devices : [];
|
|
14799
|
+
const audit = Array.isArray(gbs?.audit_recipients) ? gbs.audit_recipients : [];
|
|
14800
|
+
for (const dev of devices) this._cacheV2PeerIKFromDevice(dev);
|
|
14801
|
+
for (const dev of audit) this._cacheV2PeerIKFromDevice(dev);
|
|
14802
|
+
} catch (exc) {
|
|
14803
|
+
this._clientLog.warn(`V2 sender IK pending group bootstrap failed group=${groupId}: ${String(formatCaughtError(exc))}`);
|
|
14804
|
+
}
|
|
14805
|
+
}
|
|
14806
|
+
if (!session.getPeerIK(fromAid, senderDeviceId)) {
|
|
14807
|
+
await this._getV2SenderPubDer(fromAid, senderDeviceId);
|
|
14808
|
+
}
|
|
14809
|
+
}
|
|
14810
|
+
const pendingItems = [...this._v2SenderIKPending.entries()].filter(([, entry]) => entry.fromAid === fromAid && entry.senderDeviceId === senderDeviceId && entry.groupId === groupId);
|
|
14811
|
+
for (const [key, entry] of pendingItems) {
|
|
14812
|
+
let plaintext = null;
|
|
14813
|
+
try {
|
|
14814
|
+
plaintext = await this._decryptV2Message(entry.msg, false);
|
|
14815
|
+
} catch (exc) {
|
|
14816
|
+
this._clientLog.warn(`V2 sender IK pending retry raised: key=${key} err=${String(formatCaughtError(exc))}`);
|
|
14817
|
+
}
|
|
14818
|
+
this._v2SenderIKPending.delete(key);
|
|
14819
|
+
if (plaintext === null) {
|
|
14820
|
+
this._clientLog.debug(`V2 sender IK pending retry failed: key=${key}`);
|
|
14821
|
+
continue;
|
|
14822
|
+
}
|
|
14823
|
+
const seq = Number(entry.msg.seq ?? 0);
|
|
14824
|
+
if (entry.groupId) {
|
|
14825
|
+
plaintext.group_id = entry.groupId;
|
|
14826
|
+
await this._publishPulledMessage("group.message_created", `group:${entry.groupId}`, seq, plaintext);
|
|
14827
|
+
} else {
|
|
14828
|
+
await this._publishPulledMessage("message.received", `p2p:${this._aid ?? ""}`, seq, plaintext);
|
|
14829
|
+
}
|
|
14830
|
+
this._clientLog.debug(`V2 sender IK pending retry delivered: key=${key}`);
|
|
14831
|
+
}
|
|
14832
|
+
} finally {
|
|
14833
|
+
this._v2SenderIKFetching.delete(fetchKey);
|
|
14834
|
+
}
|
|
14835
|
+
}
|
|
13358
14836
|
/**
|
|
13359
14837
|
* V2 P2P 加密发送(推测性:用缓存 bootstrap 直接发,失败刷新重试一次)。
|
|
13360
14838
|
*
|
|
@@ -13367,105 +14845,19 @@ var _AUNClient = class _AUNClient {
|
|
|
13367
14845
|
if (!this._v2Session) {
|
|
13368
14846
|
throw new StateError("V2 session not initialized (not connected?)");
|
|
13369
14847
|
}
|
|
13370
|
-
const session = this._v2Session;
|
|
13371
14848
|
const toAid = String(to ?? "").trim();
|
|
13372
14849
|
if (!toAid) throw new ValidationError("message.send requires 'to'");
|
|
13373
14850
|
if (!isJsonObject(payload)) throw new ValidationError("message.send payload must be a dict for V2 encryption");
|
|
13374
14851
|
const attempt = async (useCache) => {
|
|
13375
|
-
|
|
13376
|
-
|
|
13377
|
-
const cached = useCache ? this._v2BootstrapCache.get(toAid) : void 0;
|
|
13378
|
-
if (cached && Date.now() - cached.cachedAt < _AUNClient.V2_BOOTSTRAP_TTL_MS) {
|
|
13379
|
-
peerDevices = cached.devices;
|
|
13380
|
-
auditRaw = cached.auditRecipients;
|
|
13381
|
-
} else {
|
|
13382
|
-
const bs = await this.call("message.v2.bootstrap", { peer_aid: toAid });
|
|
13383
|
-
peerDevices = Array.isArray(bs?.peer_devices) ? bs.peer_devices : [];
|
|
13384
|
-
auditRaw = Array.isArray(bs?.audit_recipients) ? bs.audit_recipients : [];
|
|
13385
|
-
if (peerDevices.length > 0) {
|
|
13386
|
-
this._v2BootstrapCache.set(toAid, {
|
|
13387
|
-
devices: peerDevices,
|
|
13388
|
-
auditRecipients: auditRaw,
|
|
13389
|
-
cachedAt: Date.now()
|
|
13390
|
-
});
|
|
13391
|
-
}
|
|
13392
|
-
}
|
|
13393
|
-
if (peerDevices.length === 0) {
|
|
13394
|
-
throw new E2EEError(`V2 bootstrap: no devices found for ${toAid}`);
|
|
13395
|
-
}
|
|
13396
|
-
const targets = [];
|
|
13397
|
-
for (const dev of peerDevices) {
|
|
13398
|
-
const ikDer = _v2B64ToBytes(String(dev.ik_pk ?? ""));
|
|
13399
|
-
const spkDer = dev.spk_pk ? _v2B64ToBytes(String(dev.spk_pk)) : void 0;
|
|
13400
|
-
session.cachePeerIK(toAid, String(dev.device_id ?? ""), ikDer);
|
|
13401
|
-
targets.push({
|
|
13402
|
-
aid: toAid,
|
|
13403
|
-
deviceId: String(dev.device_id ?? ""),
|
|
13404
|
-
role: "peer",
|
|
13405
|
-
keySource: String(dev.key_source ?? "peer_device_prekey"),
|
|
13406
|
-
ikPkDer: ikDer,
|
|
13407
|
-
spkPkDer: spkDer,
|
|
13408
|
-
spkId: String(dev.spk_id ?? "")
|
|
13409
|
-
});
|
|
13410
|
-
}
|
|
13411
|
-
const auditTargets = [];
|
|
13412
|
-
for (const dev of auditRaw) {
|
|
13413
|
-
if (!dev.ik_pk) continue;
|
|
13414
|
-
const ikDer = _v2B64ToBytes(String(dev.ik_pk));
|
|
13415
|
-
const spkDer = dev.spk_pk ? _v2B64ToBytes(String(dev.spk_pk)) : void 0;
|
|
13416
|
-
auditTargets.push({
|
|
13417
|
-
aid: String(dev.aid ?? ""),
|
|
13418
|
-
deviceId: String(dev.device_id ?? ""),
|
|
13419
|
-
role: "audit",
|
|
13420
|
-
keySource: String(dev.key_source ?? "peer_device_prekey"),
|
|
13421
|
-
ikPkDer: ikDer,
|
|
13422
|
-
spkPkDer: spkDer,
|
|
13423
|
-
spkId: String(dev.spk_id ?? "")
|
|
13424
|
-
});
|
|
13425
|
-
}
|
|
13426
|
-
if (this._aid && this._aid !== toAid) {
|
|
13427
|
-
try {
|
|
13428
|
-
const selfCached = this._v2BootstrapCache.get(this._aid);
|
|
13429
|
-
let selfDevices = [];
|
|
13430
|
-
if (selfCached && Date.now() - selfCached.cachedAt < _AUNClient.V2_BOOTSTRAP_TTL_MS) {
|
|
13431
|
-
selfDevices = selfCached.devices;
|
|
13432
|
-
} else {
|
|
13433
|
-
const selfBs = await this.call("message.v2.bootstrap", { peer_aid: this._aid });
|
|
13434
|
-
selfDevices = Array.isArray(selfBs?.peer_devices) ? selfBs.peer_devices : [];
|
|
13435
|
-
if (selfDevices.length > 0) {
|
|
13436
|
-
this._v2BootstrapCache.set(this._aid, {
|
|
13437
|
-
devices: selfDevices,
|
|
13438
|
-
auditRecipients: [],
|
|
13439
|
-
cachedAt: Date.now()
|
|
13440
|
-
});
|
|
13441
|
-
}
|
|
13442
|
-
}
|
|
13443
|
-
for (const dev of selfDevices) {
|
|
13444
|
-
const devId = String(dev.owner_device_id ?? dev.device_id ?? "");
|
|
13445
|
-
if (devId === this._deviceId) continue;
|
|
13446
|
-
const ikDer = _v2B64ToBytes(String(dev.ik_pk ?? ""));
|
|
13447
|
-
const spkDer = dev.spk_pk ? _v2B64ToBytes(String(dev.spk_pk)) : void 0;
|
|
13448
|
-
targets.push({
|
|
13449
|
-
aid: this._aid,
|
|
13450
|
-
deviceId: devId,
|
|
13451
|
-
role: "self_sync",
|
|
13452
|
-
keySource: String(dev.key_source ?? "peer_device_prekey"),
|
|
13453
|
-
ikPkDer: ikDer,
|
|
13454
|
-
spkPkDer: spkDer,
|
|
13455
|
-
spkId: String(dev.spk_id ?? "")
|
|
13456
|
-
});
|
|
13457
|
-
}
|
|
13458
|
-
} catch (exc) {
|
|
13459
|
-
this._clientLog.debug(`V2 self-sync bootstrap failed (non-fatal): ${String(exc)}`);
|
|
13460
|
-
}
|
|
13461
|
-
}
|
|
13462
|
-
const sender = await session.getSenderIdentity();
|
|
13463
|
-
const envelope = await encryptP2PMessage(
|
|
13464
|
-
sender,
|
|
13465
|
-
{ targets, auditRecipients: auditTargets },
|
|
14852
|
+
const envelope = await this._buildV2P2PEnvelope({
|
|
14853
|
+
to: toAid,
|
|
13466
14854
|
payload,
|
|
13467
|
-
opts
|
|
13468
|
-
|
|
14855
|
+
messageId: opts?.messageId,
|
|
14856
|
+
timestamp: opts?.timestamp,
|
|
14857
|
+
protectedHeaders: opts?.protectedHeaders,
|
|
14858
|
+
context: opts?.context,
|
|
14859
|
+
useCache
|
|
14860
|
+
});
|
|
13469
14861
|
return this.call("message.send", {
|
|
13470
14862
|
to: toAid,
|
|
13471
14863
|
payload: envelope,
|
|
@@ -13495,77 +14887,93 @@ var _AUNClient = class _AUNClient {
|
|
|
13495
14887
|
throw new StateError("V2 session not initialized (not connected?)");
|
|
13496
14888
|
}
|
|
13497
14889
|
const ns = this._aid ? `p2p:${this._aid}` : "";
|
|
13498
|
-
const effective = afterSeq || (ns ? this._seqTracker.getContiguousSeq(ns) : 0);
|
|
13499
|
-
const result = await this.call("message.v2.pull", {
|
|
13500
|
-
after_seq: effective,
|
|
13501
|
-
limit
|
|
13502
|
-
});
|
|
13503
|
-
const messages = Array.isArray(result?.messages) ? result.messages : [];
|
|
13504
14890
|
const decrypted = [];
|
|
13505
|
-
|
|
13506
|
-
|
|
13507
|
-
|
|
13508
|
-
|
|
13509
|
-
|
|
13510
|
-
|
|
13511
|
-
|
|
13512
|
-
|
|
13513
|
-
|
|
13514
|
-
|
|
13515
|
-
|
|
13516
|
-
|
|
13517
|
-
|
|
13518
|
-
|
|
13519
|
-
|
|
13520
|
-
|
|
13521
|
-
|
|
13522
|
-
|
|
13523
|
-
|
|
13524
|
-
|
|
13525
|
-
|
|
13526
|
-
|
|
13527
|
-
|
|
13528
|
-
|
|
13529
|
-
if (
|
|
13530
|
-
|
|
13531
|
-
|
|
14891
|
+
let nextAfterSeq = afterSeq || (ns ? this._seqTracker.getContiguousSeq(ns) : 0);
|
|
14892
|
+
let pageCount = 0;
|
|
14893
|
+
const maxPages = 100;
|
|
14894
|
+
while (pageCount < maxPages) {
|
|
14895
|
+
pageCount += 1;
|
|
14896
|
+
const result = await this.call("message.v2.pull", {
|
|
14897
|
+
after_seq: nextAfterSeq,
|
|
14898
|
+
limit
|
|
14899
|
+
});
|
|
14900
|
+
const messages = Array.isArray(result?.messages) ? result.messages : [];
|
|
14901
|
+
const seqs = messages.map((msg) => Number(msg.seq ?? 0)).filter((seq) => Number.isFinite(seq) && seq > 0);
|
|
14902
|
+
const pageContigBefore = ns ? this._seqTracker.getContiguousSeq(ns) : 0;
|
|
14903
|
+
const pageMaxSeq = seqs.length > 0 ? Math.max(...seqs) : nextAfterSeq;
|
|
14904
|
+
if (ns && seqs.length > 0) {
|
|
14905
|
+
this._seqTracker.forceContiguousSeq(ns, pageMaxSeq);
|
|
14906
|
+
}
|
|
14907
|
+
for (const msg of messages) {
|
|
14908
|
+
const seq = Number(msg.seq ?? 0);
|
|
14909
|
+
if (!Number.isFinite(seq) || seq <= 0) continue;
|
|
14910
|
+
const version = String(msg.version ?? "v2");
|
|
14911
|
+
if (version === "v1") {
|
|
14912
|
+
const legacy = isJsonObject(msg.legacy_v1) ? msg.legacy_v1 : {};
|
|
14913
|
+
const legacyPayload = legacy.payload;
|
|
14914
|
+
const payloadType = isJsonObject(legacyPayload) ? String(legacyPayload.type ?? "").trim() : "";
|
|
14915
|
+
if (legacyPayload !== void 0 && legacyPayload !== null && payloadType !== "e2ee.encrypted" && payloadType !== "e2ee.group_encrypted") {
|
|
14916
|
+
const v1Msg = {
|
|
14917
|
+
message_id: String(msg.message_id ?? ""),
|
|
14918
|
+
from: String(msg.from_aid ?? ""),
|
|
14919
|
+
to: String(legacy.to ?? this._aid ?? ""),
|
|
14920
|
+
seq: msg.seq,
|
|
14921
|
+
type: String(msg.type ?? ""),
|
|
14922
|
+
timestamp: msg.t_server,
|
|
14923
|
+
payload: legacyPayload,
|
|
14924
|
+
encrypted: false
|
|
14925
|
+
};
|
|
14926
|
+
if (ns) await this._publishPulledMessage("message.received", ns, seq, v1Msg);
|
|
14927
|
+
else await this._publishAppEvent("message.received", v1Msg);
|
|
14928
|
+
decrypted.push(v1Msg);
|
|
14929
|
+
} else {
|
|
14930
|
+
this._clientLog.debug(`message.v2.pull skipping V1 envelope seq=${seq} payload_type=${payloadType || "<none>"} (V1 E2EE removed)`);
|
|
14931
|
+
}
|
|
14932
|
+
continue;
|
|
14933
|
+
}
|
|
14934
|
+
if (version !== "v2") {
|
|
14935
|
+
this._clientLog.debug(`message.v2.pull skipping non-V2 row seq=${seq} version=${String(msg.version ?? "")}`);
|
|
14936
|
+
continue;
|
|
14937
|
+
}
|
|
14938
|
+
const msgSpkId = String(msg.spk_id ?? "");
|
|
14939
|
+
if (msgSpkId && this._v2Session && !this._v2Session.isCurrentSPK(msgSpkId)) {
|
|
14940
|
+
this._v2Session.trackOldSPKMaxSeq(msgSpkId, seq);
|
|
14941
|
+
}
|
|
14942
|
+
const plaintext = await this._decryptV2Message(msg);
|
|
14943
|
+
if (plaintext === null) continue;
|
|
14944
|
+
if (ns) {
|
|
14945
|
+
await this._publishPulledMessage("message.received", ns, seq, plaintext);
|
|
14946
|
+
decrypted.push(plaintext);
|
|
13532
14947
|
} else {
|
|
13533
|
-
this.
|
|
14948
|
+
await this._publishAppEvent("message.received", plaintext);
|
|
14949
|
+
decrypted.push(plaintext);
|
|
13534
14950
|
}
|
|
13535
|
-
continue;
|
|
13536
14951
|
}
|
|
13537
|
-
|
|
13538
|
-
|
|
13539
|
-
|
|
13540
|
-
|
|
13541
|
-
|
|
13542
|
-
|
|
13543
|
-
|
|
14952
|
+
const serverAckSeq = Number(result.server_ack_seq ?? 0);
|
|
14953
|
+
if (ns && Number.isFinite(serverAckSeq) && serverAckSeq > 0) {
|
|
14954
|
+
const contig = this._seqTracker.getContiguousSeq(ns);
|
|
14955
|
+
if (contig < serverAckSeq) {
|
|
14956
|
+
this._clientLog.info(`message.v2.pull retention-floor advance: ns=${ns} contiguous=${contig} -> server_ack_seq=${serverAckSeq}`);
|
|
14957
|
+
this._seqTracker.forceContiguousSeq(ns, serverAckSeq);
|
|
14958
|
+
}
|
|
13544
14959
|
}
|
|
13545
|
-
const plaintext = await this._decryptV2Message(msg);
|
|
13546
|
-
if (plaintext === null) continue;
|
|
13547
14960
|
if (ns) {
|
|
13548
|
-
|
|
13549
|
-
|
|
13550
|
-
|
|
13551
|
-
|
|
13552
|
-
|
|
13553
|
-
|
|
13554
|
-
|
|
13555
|
-
if (ns && seqs.length > 0) {
|
|
13556
|
-
const maxSeq = Math.max(...seqs);
|
|
13557
|
-
const contig = this._seqTracker.getContiguousSeq(ns);
|
|
13558
|
-
if (maxSeq > contig) {
|
|
13559
|
-
this._seqTracker.forceContiguousSeq(ns, maxSeq);
|
|
13560
|
-
await this._drainOrderedMessages(ns);
|
|
13561
|
-
}
|
|
13562
|
-
const ackSeq = this._seqTracker.getContiguousSeq(ns);
|
|
13563
|
-
if (ackSeq !== contigBefore) {
|
|
13564
|
-
this._saveSeqTrackerState();
|
|
13565
|
-
if (ackSeq > 0) {
|
|
14961
|
+
const ackSeq = this._seqTracker.getContiguousSeq(ns);
|
|
14962
|
+
const contigAdvanced = ackSeq !== pageContigBefore;
|
|
14963
|
+
if (contigAdvanced) {
|
|
14964
|
+
await this._drainOrderedMessages(ns);
|
|
14965
|
+
this._saveSeqTrackerState();
|
|
14966
|
+
}
|
|
14967
|
+
if (messages.length > 0 && contigAdvanced && ackSeq > 0) {
|
|
13566
14968
|
this._safeAsync(this.ackV2(ackSeq).then(() => void 0));
|
|
13567
14969
|
}
|
|
13568
14970
|
}
|
|
14971
|
+
const nextAfter = Math.max(pageMaxSeq, nextAfterSeq);
|
|
14972
|
+
if (messages.length === 0 || nextAfter <= nextAfterSeq || result.has_more === false) break;
|
|
14973
|
+
nextAfterSeq = nextAfter;
|
|
14974
|
+
}
|
|
14975
|
+
if (pageCount >= maxPages) {
|
|
14976
|
+
this._clientLog.warn(`message.v2.pull reached max_pages=${maxPages} after_seq=${nextAfterSeq}`);
|
|
13569
14977
|
}
|
|
13570
14978
|
return decrypted;
|
|
13571
14979
|
}
|
|
@@ -13576,8 +14984,15 @@ var _AUNClient = class _AUNClient {
|
|
|
13576
14984
|
*/
|
|
13577
14985
|
async ackV2(upToSeq) {
|
|
13578
14986
|
const ns = this._aid ? `p2p:${this._aid}` : "";
|
|
13579
|
-
|
|
14987
|
+
let seq = upToSeq ?? (ns ? this._seqTracker.getContiguousSeq(ns) : 0);
|
|
13580
14988
|
if (seq <= 0) return { acked: 0 };
|
|
14989
|
+
if (ns) {
|
|
14990
|
+
const maxSeen = this._seqTracker.getMaxSeenSeq(ns);
|
|
14991
|
+
if (maxSeen > 0 && seq > maxSeen) {
|
|
14992
|
+
this._clientLog.warn(`ackV2 clamp: up_to_seq=${seq} > max_seen=${maxSeen}, clamp`);
|
|
14993
|
+
seq = maxSeen;
|
|
14994
|
+
}
|
|
14995
|
+
}
|
|
13581
14996
|
const raw = await this.call("message.v2.ack", { up_to_seq: seq });
|
|
13582
14997
|
const result = isJsonObject(raw) ? { ...raw } : { result: raw };
|
|
13583
14998
|
let actualAckSeq = seq;
|
|
@@ -13600,8 +15015,8 @@ var _AUNClient = class _AUNClient {
|
|
|
13600
15015
|
}
|
|
13601
15016
|
return result;
|
|
13602
15017
|
}
|
|
13603
|
-
/** 解密单条 V2 消息(与 Python `_decrypt_v2_message`
|
|
13604
|
-
async _decryptV2Message(msg) {
|
|
15018
|
+
/** 解密单条 V2 消息(与 Python `_decrypt_v2_message` 对齐)。缺 sender IK 时先入 pending,后台补齐后重试。 */
|
|
15019
|
+
async _decryptV2Message(msg, allowPending = true) {
|
|
13605
15020
|
const session = this._v2Session;
|
|
13606
15021
|
if (!session) return null;
|
|
13607
15022
|
const envJson = msg.envelope_json;
|
|
@@ -13613,6 +15028,8 @@ var _AUNClient = class _AUNClient {
|
|
|
13613
15028
|
this._clientLog.warn(`V2 decrypt: invalid envelope_json for msg seq=${String(msg.seq)}`);
|
|
13614
15029
|
return null;
|
|
13615
15030
|
}
|
|
15031
|
+
const e2eeMeta = v2E2eeMeta(envelope);
|
|
15032
|
+
await this._observeAgentMdFromEnvelope(envelope);
|
|
13616
15033
|
let spkId = "";
|
|
13617
15034
|
let recipientKeySource = "";
|
|
13618
15035
|
const recipientObj = envelope.recipient;
|
|
@@ -13640,29 +15057,68 @@ var _AUNClient = class _AUNClient {
|
|
|
13640
15057
|
}
|
|
13641
15058
|
const aad = isJsonObject(envelope.aad) ? envelope.aad : {};
|
|
13642
15059
|
const groupIdForKeys = String(msg.group_id ?? aad.group_id ?? envelope.group_id ?? "").trim();
|
|
15060
|
+
const undecryptableEvent = groupIdForKeys ? "group.message_undecryptable" : "message.undecryptable";
|
|
13643
15061
|
let ikPriv;
|
|
13644
15062
|
let spkPriv;
|
|
13645
|
-
|
|
13646
|
-
|
|
13647
|
-
|
|
13648
|
-
|
|
13649
|
-
|
|
13650
|
-
|
|
13651
|
-
|
|
13652
|
-
|
|
15063
|
+
try {
|
|
15064
|
+
if (groupIdForKeys) {
|
|
15065
|
+
const keys = await session.getGroupDecryptKeys(groupIdForKeys, spkId);
|
|
15066
|
+
ikPriv = keys.ikPriv;
|
|
15067
|
+
spkPriv = keys.spkPriv;
|
|
15068
|
+
} else {
|
|
15069
|
+
const keys = await session.getDecryptKeys(spkId);
|
|
15070
|
+
ikPriv = keys.ikPriv;
|
|
15071
|
+
spkPriv = keys.spkPriv;
|
|
15072
|
+
}
|
|
15073
|
+
} catch (exc) {
|
|
15074
|
+
this._clientLog.warn(`V2 decrypt: SPK lookup failed seq=${String(msg.seq)} spk_id=${spkId}: ${String(exc)}`);
|
|
15075
|
+
try {
|
|
15076
|
+
const event = {
|
|
15077
|
+
message_id: String(msg.message_id ?? ""),
|
|
15078
|
+
from: String(msg.from_aid ?? ""),
|
|
15079
|
+
to: String(msg.to ?? ""),
|
|
15080
|
+
seq: msg.seq,
|
|
15081
|
+
timestamp: msg.t_server ?? msg.timestamp,
|
|
15082
|
+
device_id: String(msg.device_id ?? ""),
|
|
15083
|
+
slot_id: String(msg.slot_id ?? ""),
|
|
15084
|
+
_decrypt_error: String(exc),
|
|
15085
|
+
_decrypt_stage: "spk_lookup",
|
|
15086
|
+
_envelope_type: String(envelope.type ?? ""),
|
|
15087
|
+
_suite: String(envelope.suite ?? ""),
|
|
15088
|
+
_spk_id: spkId
|
|
15089
|
+
};
|
|
15090
|
+
attachV2EnvelopeMetadata(event, e2eeMeta);
|
|
15091
|
+
await this._dispatcher.publish(undecryptableEvent, event);
|
|
15092
|
+
} catch {
|
|
15093
|
+
}
|
|
15094
|
+
return null;
|
|
13653
15095
|
}
|
|
13654
15096
|
const fromAid = String(msg.from_aid ?? "");
|
|
13655
15097
|
const senderDeviceId = String(aad.from_device ?? "");
|
|
13656
15098
|
const senderPubDer = await this._getV2SenderPubDer(fromAid, senderDeviceId);
|
|
13657
15099
|
if (!senderPubDer) {
|
|
13658
|
-
this._clientLog.warn(`V2 decrypt: no sender IK for ${fromAid}
|
|
15100
|
+
this._clientLog.warn(`V2 decrypt: no sender IK for ${fromAid} device=${senderDeviceId}`);
|
|
15101
|
+
if (allowPending) {
|
|
15102
|
+
this._scheduleV2SenderIKPending({ msg, fromAid, senderDeviceId, groupId: groupIdForKeys });
|
|
15103
|
+
return null;
|
|
15104
|
+
}
|
|
13659
15105
|
try {
|
|
13660
|
-
|
|
15106
|
+
const event = {
|
|
13661
15107
|
message_id: String(msg.message_id ?? ""),
|
|
13662
15108
|
from: fromAid,
|
|
15109
|
+
to: String(msg.to ?? ""),
|
|
13663
15110
|
seq: msg.seq,
|
|
13664
|
-
|
|
13665
|
-
|
|
15111
|
+
timestamp: msg.t_server ?? msg.timestamp,
|
|
15112
|
+
device_id: String(msg.device_id ?? ""),
|
|
15113
|
+
slot_id: String(msg.slot_id ?? ""),
|
|
15114
|
+
_decrypt_error: "sender_ik_not_found",
|
|
15115
|
+
_decrypt_stage: "sender_ik",
|
|
15116
|
+
_envelope_type: String(envelope.type ?? ""),
|
|
15117
|
+
_suite: String(envelope.suite ?? ""),
|
|
15118
|
+
_sender_device_id: String(aad.from_device ?? "")
|
|
15119
|
+
};
|
|
15120
|
+
attachV2EnvelopeMetadata(event, e2eeMeta);
|
|
15121
|
+
await this._dispatcher.publish(undecryptableEvent, event);
|
|
13666
15122
|
} catch {
|
|
13667
15123
|
}
|
|
13668
15124
|
return null;
|
|
@@ -13680,12 +15136,22 @@ var _AUNClient = class _AUNClient {
|
|
|
13680
15136
|
} catch (exc) {
|
|
13681
15137
|
this._clientLog.warn(`V2 decrypt failed for msg seq=${String(msg.seq)}: ${String(exc)}`);
|
|
13682
15138
|
try {
|
|
13683
|
-
|
|
15139
|
+
const event = {
|
|
13684
15140
|
message_id: String(msg.message_id ?? ""),
|
|
13685
15141
|
from: fromAid,
|
|
15142
|
+
to: String(msg.to ?? ""),
|
|
13686
15143
|
seq: msg.seq,
|
|
13687
|
-
|
|
13688
|
-
|
|
15144
|
+
timestamp: msg.t_server ?? msg.timestamp,
|
|
15145
|
+
device_id: String(msg.device_id ?? ""),
|
|
15146
|
+
slot_id: String(msg.slot_id ?? ""),
|
|
15147
|
+
_decrypt_error: String(exc),
|
|
15148
|
+
_decrypt_stage: "decrypt",
|
|
15149
|
+
_envelope_type: String(envelope.type ?? ""),
|
|
15150
|
+
_suite: String(envelope.suite ?? ""),
|
|
15151
|
+
_sender_device_id: String(aad.from_device ?? "")
|
|
15152
|
+
};
|
|
15153
|
+
attachV2EnvelopeMetadata(event, e2eeMeta);
|
|
15154
|
+
await this._dispatcher.publish(undecryptableEvent, event);
|
|
13689
15155
|
} catch {
|
|
13690
15156
|
}
|
|
13691
15157
|
return null;
|
|
@@ -13707,7 +15173,8 @@ var _AUNClient = class _AUNClient {
|
|
|
13707
15173
|
this._clientLog.debug(`V2 SPK rotation failed (non-fatal): ${exc}`);
|
|
13708
15174
|
});
|
|
13709
15175
|
}
|
|
13710
|
-
|
|
15176
|
+
const e2ee = v2E2eeMeta(envelope);
|
|
15177
|
+
const result = {
|
|
13711
15178
|
message_id: String(msg.message_id ?? ""),
|
|
13712
15179
|
from: fromAid,
|
|
13713
15180
|
to: this._aid ?? "",
|
|
@@ -13715,8 +15182,10 @@ var _AUNClient = class _AUNClient {
|
|
|
13715
15182
|
t_server: msg.t_server,
|
|
13716
15183
|
payload: plaintext,
|
|
13717
15184
|
encrypted: true,
|
|
13718
|
-
e2ee
|
|
15185
|
+
e2ee
|
|
13719
15186
|
};
|
|
15187
|
+
attachV2EnvelopeMetadata(result, e2ee);
|
|
15188
|
+
return result;
|
|
13720
15189
|
}
|
|
13721
15190
|
/**
|
|
13722
15191
|
* V2 Group 加密发送(推测性:用缓存 bootstrap 直接发,失败刷新重试一次)。
|
|
@@ -13797,29 +15266,49 @@ var _AUNClient = class _AUNClient {
|
|
|
13797
15266
|
const gid = normalizeGroupId(groupId) || String(groupId ?? "").trim();
|
|
13798
15267
|
if (!gid) throw new ValidationError("group.pull requires group_id");
|
|
13799
15268
|
const ns = `group:${gid}`;
|
|
13800
|
-
const effective = afterSeq || this._seqTracker.getContiguousSeq(ns);
|
|
13801
|
-
const result = await this.call("group.v2.pull", {
|
|
13802
|
-
group_id: gid,
|
|
13803
|
-
after_seq: effective,
|
|
13804
|
-
limit
|
|
13805
|
-
});
|
|
13806
|
-
const messages = Array.isArray(result?.messages) ? result.messages : [];
|
|
13807
15269
|
const decrypted = [];
|
|
13808
|
-
|
|
13809
|
-
|
|
13810
|
-
|
|
13811
|
-
|
|
13812
|
-
|
|
13813
|
-
|
|
13814
|
-
|
|
13815
|
-
|
|
13816
|
-
|
|
13817
|
-
|
|
13818
|
-
|
|
13819
|
-
|
|
13820
|
-
|
|
13821
|
-
|
|
13822
|
-
|
|
15270
|
+
let nextAfterSeq = afterSeq || this._seqTracker.getContiguousSeq(ns);
|
|
15271
|
+
let pageCount = 0;
|
|
15272
|
+
const maxPages = 100;
|
|
15273
|
+
while (pageCount < maxPages) {
|
|
15274
|
+
pageCount += 1;
|
|
15275
|
+
const result = await this.call("group.v2.pull", {
|
|
15276
|
+
group_id: gid,
|
|
15277
|
+
after_seq: nextAfterSeq,
|
|
15278
|
+
limit
|
|
15279
|
+
});
|
|
15280
|
+
const messages = Array.isArray(result?.messages) ? result.messages : [];
|
|
15281
|
+
const seqs = messages.map((msg) => Number(msg.seq ?? 0)).filter((seq) => Number.isFinite(seq) && seq > 0);
|
|
15282
|
+
const pageContigBefore = this._seqTracker.getContiguousSeq(ns);
|
|
15283
|
+
const pageMaxSeq = seqs.length > 0 ? Math.max(...seqs) : nextAfterSeq;
|
|
15284
|
+
if (seqs.length > 0) {
|
|
15285
|
+
this._seqTracker.forceContiguousSeq(ns, pageMaxSeq);
|
|
15286
|
+
}
|
|
15287
|
+
for (const msg of messages) {
|
|
15288
|
+
const seq = Number(msg.seq ?? 0);
|
|
15289
|
+
if (!Number.isFinite(seq) || seq <= 0) continue;
|
|
15290
|
+
const version = String(msg.version ?? "v2");
|
|
15291
|
+
if (version === "v1") {
|
|
15292
|
+
const payload = msg.payload;
|
|
15293
|
+
const payloadObj = isJsonObject(payload) ? payload : null;
|
|
15294
|
+
if (payloadObj) {
|
|
15295
|
+
const payloadType = String(payloadObj.type ?? "").trim();
|
|
15296
|
+
if (payloadType !== "e2ee.encrypted" && payloadType !== "e2ee.group_encrypted") {
|
|
15297
|
+
const v1Msg = {
|
|
15298
|
+
message_id: String(msg.message_id ?? ""),
|
|
15299
|
+
from: String(msg.from_aid ?? ""),
|
|
15300
|
+
group_id: gid,
|
|
15301
|
+
seq: msg.seq,
|
|
15302
|
+
type: String(msg.type ?? ""),
|
|
15303
|
+
timestamp: msg.t_server,
|
|
15304
|
+
payload,
|
|
15305
|
+
encrypted: false
|
|
15306
|
+
};
|
|
15307
|
+
await this._publishPulledMessage("group.message_created", ns, seq, v1Msg);
|
|
15308
|
+
decrypted.push(v1Msg);
|
|
15309
|
+
continue;
|
|
15310
|
+
}
|
|
15311
|
+
} else if (payload !== void 0 && payload !== null) {
|
|
13823
15312
|
const v1Msg = {
|
|
13824
15313
|
message_id: String(msg.message_id ?? ""),
|
|
13825
15314
|
from: String(msg.from_aid ?? ""),
|
|
@@ -13834,48 +15323,43 @@ var _AUNClient = class _AUNClient {
|
|
|
13834
15323
|
decrypted.push(v1Msg);
|
|
13835
15324
|
continue;
|
|
13836
15325
|
}
|
|
13837
|
-
|
|
13838
|
-
const v1Msg = {
|
|
13839
|
-
message_id: String(msg.message_id ?? ""),
|
|
13840
|
-
from: String(msg.from_aid ?? ""),
|
|
13841
|
-
group_id: gid,
|
|
13842
|
-
seq: msg.seq,
|
|
13843
|
-
type: String(msg.type ?? ""),
|
|
13844
|
-
timestamp: msg.t_server,
|
|
13845
|
-
payload,
|
|
13846
|
-
encrypted: false
|
|
13847
|
-
};
|
|
13848
|
-
await this._publishPulledMessage("group.message_created", ns, seq, v1Msg);
|
|
13849
|
-
decrypted.push(v1Msg);
|
|
15326
|
+
this._clientLog.debug(`group.v2.pull skipping V1 envelope group=${gid} seq=${seq} payload_type=${payloadObj ? String(payloadObj.type ?? "") : "<none>"} (V1 E2EE removed)`);
|
|
13850
15327
|
continue;
|
|
13851
15328
|
}
|
|
13852
|
-
|
|
13853
|
-
|
|
13854
|
-
|
|
13855
|
-
|
|
13856
|
-
|
|
13857
|
-
continue;
|
|
15329
|
+
if (version !== "v2") {
|
|
15330
|
+
this._clientLog.debug(`group.v2.pull skipping non-V2 row group=${gid} seq=${seq} version=${String(msg.version ?? "")}`);
|
|
15331
|
+
continue;
|
|
15332
|
+
}
|
|
15333
|
+
const plaintext = await this._decryptV2Message(msg);
|
|
15334
|
+
if (plaintext === null) continue;
|
|
15335
|
+
plaintext.group_id = gid;
|
|
15336
|
+
await this._publishPulledMessage("group.message_created", ns, seq, plaintext);
|
|
15337
|
+
decrypted.push(plaintext);
|
|
13858
15338
|
}
|
|
13859
|
-
const
|
|
13860
|
-
|
|
13861
|
-
|
|
13862
|
-
|
|
13863
|
-
|
|
13864
|
-
|
|
13865
|
-
|
|
13866
|
-
|
|
13867
|
-
const contig = this._seqTracker.getContiguousSeq(ns);
|
|
13868
|
-
if (maxSeq > contig) {
|
|
13869
|
-
this._seqTracker.forceContiguousSeq(ns, maxSeq);
|
|
13870
|
-
await this._drainOrderedMessages(ns);
|
|
15339
|
+
const cursor = isJsonObject(result.cursor) ? result.cursor : null;
|
|
15340
|
+
const serverAckSeq = Number(cursor?.current_seq ?? 0);
|
|
15341
|
+
if (Number.isFinite(serverAckSeq) && serverAckSeq > 0) {
|
|
15342
|
+
const contig = this._seqTracker.getContiguousSeq(ns);
|
|
15343
|
+
if (contig < serverAckSeq) {
|
|
15344
|
+
this._clientLog.info(`group.v2.pull retention-floor advance: ns=${ns} contiguous=${contig} -> cursor.current_seq=${serverAckSeq}`);
|
|
15345
|
+
this._seqTracker.forceContiguousSeq(ns, serverAckSeq);
|
|
15346
|
+
}
|
|
13871
15347
|
}
|
|
13872
15348
|
const ackSeq = this._seqTracker.getContiguousSeq(ns);
|
|
13873
|
-
|
|
15349
|
+
const contigAdvanced = ackSeq !== pageContigBefore;
|
|
15350
|
+
if (contigAdvanced) {
|
|
15351
|
+
await this._drainOrderedMessages(ns);
|
|
13874
15352
|
this._saveSeqTrackerState();
|
|
13875
|
-
if (ackSeq > 0) {
|
|
13876
|
-
this._safeAsync(this.ackGroupV2(gid, ackSeq).then(() => void 0));
|
|
13877
|
-
}
|
|
13878
15353
|
}
|
|
15354
|
+
if (messages.length > 0 && contigAdvanced && ackSeq > 0) {
|
|
15355
|
+
this._safeAsync(this.ackGroupV2(gid, ackSeq).then(() => void 0));
|
|
15356
|
+
}
|
|
15357
|
+
const nextAfter = Math.max(pageMaxSeq, nextAfterSeq);
|
|
15358
|
+
if (messages.length === 0 || nextAfter <= nextAfterSeq || result.has_more === false) break;
|
|
15359
|
+
nextAfterSeq = nextAfter;
|
|
15360
|
+
}
|
|
15361
|
+
if (pageCount >= maxPages) {
|
|
15362
|
+
this._clientLog.warn(`group.v2.pull reached max_pages=${maxPages} group=${gid} after_seq=${nextAfterSeq}`);
|
|
13879
15363
|
}
|
|
13880
15364
|
return decrypted;
|
|
13881
15365
|
}
|
|
@@ -13889,8 +15373,13 @@ var _AUNClient = class _AUNClient {
|
|
|
13889
15373
|
const gid = normalizeGroupId(groupId) || String(groupId ?? "").trim();
|
|
13890
15374
|
if (!gid) throw new ValidationError("group.ack_messages requires group_id");
|
|
13891
15375
|
const ns = `group:${gid}`;
|
|
13892
|
-
|
|
15376
|
+
let seq = upToSeq ?? this._seqTracker.getContiguousSeq(ns);
|
|
13893
15377
|
if (seq <= 0) return { acked: 0 };
|
|
15378
|
+
const maxSeen = this._seqTracker.getMaxSeenSeq(ns);
|
|
15379
|
+
if (maxSeen > 0 && seq > maxSeen) {
|
|
15380
|
+
this._clientLog.warn(`ackGroupV2 clamp: group=${gid} up_to_seq=${seq} > max_seen=${maxSeen}, clamp`);
|
|
15381
|
+
seq = maxSeen;
|
|
15382
|
+
}
|
|
13894
15383
|
return this.call("group.v2.ack", { group_id: gid, up_to_seq: seq });
|
|
13895
15384
|
}
|
|
13896
15385
|
// ── V2 thought(per-device wrap,服务端透传,不持久化)──────────
|
|
@@ -13929,32 +15418,25 @@ var _AUNClient = class _AUNClient {
|
|
|
13929
15418
|
}
|
|
13930
15419
|
const targets = [];
|
|
13931
15420
|
for (const dev of peerDevices) {
|
|
13932
|
-
const
|
|
13933
|
-
const
|
|
13934
|
-
|
|
13935
|
-
targets.push({
|
|
15421
|
+
const devId = getV2DeviceId(dev);
|
|
15422
|
+
const target = await this._v2BuildTargetFromDevice({
|
|
15423
|
+
dev,
|
|
13936
15424
|
aid: to,
|
|
13937
|
-
deviceId:
|
|
15425
|
+
deviceId: devId.value,
|
|
13938
15426
|
role: "peer",
|
|
13939
|
-
|
|
13940
|
-
ikPkDer: ikDer,
|
|
13941
|
-
spkPkDer: spkDer,
|
|
13942
|
-
spkId: String(dev.spk_id ?? "")
|
|
15427
|
+
defaultKeySource: "peer_device_prekey"
|
|
13943
15428
|
});
|
|
15429
|
+
if (target) targets.push(target);
|
|
13944
15430
|
}
|
|
13945
15431
|
for (const dev of auditRaw) {
|
|
13946
|
-
|
|
13947
|
-
|
|
13948
|
-
const spkDer = dev.spk_pk ? _v2B64ToBytes(String(dev.spk_pk)) : void 0;
|
|
13949
|
-
targets.push({
|
|
15432
|
+
const target = await this._v2BuildTargetFromDevice({
|
|
15433
|
+
dev,
|
|
13950
15434
|
aid: String(dev.aid ?? ""),
|
|
13951
15435
|
deviceId: String(dev.device_id ?? ""),
|
|
13952
15436
|
role: "audit",
|
|
13953
|
-
|
|
13954
|
-
ikPkDer: ikDer,
|
|
13955
|
-
spkPkDer: spkDer,
|
|
13956
|
-
spkId: String(dev.spk_id ?? "")
|
|
15437
|
+
defaultKeySource: "peer_device_prekey"
|
|
13957
15438
|
});
|
|
15439
|
+
if (target) targets.push(target);
|
|
13958
15440
|
}
|
|
13959
15441
|
if (this._aid && this._aid !== to) {
|
|
13960
15442
|
try {
|
|
@@ -13974,19 +15456,16 @@ var _AUNClient = class _AUNClient {
|
|
|
13974
15456
|
}
|
|
13975
15457
|
}
|
|
13976
15458
|
for (const dev of selfDevices) {
|
|
13977
|
-
const devId =
|
|
13978
|
-
if (devId === this._deviceId) continue;
|
|
13979
|
-
const
|
|
13980
|
-
|
|
13981
|
-
targets.push({
|
|
15459
|
+
const devId = getV2DeviceId(dev);
|
|
15460
|
+
if (!devId.present || devId.value === this._deviceId) continue;
|
|
15461
|
+
const target = await this._v2BuildTargetFromDevice({
|
|
15462
|
+
dev,
|
|
13982
15463
|
aid: this._aid,
|
|
13983
|
-
deviceId: devId,
|
|
15464
|
+
deviceId: devId.value,
|
|
13984
15465
|
role: "self_sync",
|
|
13985
|
-
|
|
13986
|
-
ikPkDer: ikDer,
|
|
13987
|
-
spkPkDer: spkDer,
|
|
13988
|
-
spkId: String(dev.spk_id ?? "")
|
|
15466
|
+
defaultKeySource: "peer_device_prekey"
|
|
13989
15467
|
});
|
|
15468
|
+
if (target) targets.push(target);
|
|
13990
15469
|
}
|
|
13991
15470
|
} catch (exc) {
|
|
13992
15471
|
this._clientLog.debug(`V2 thought self-sync bootstrap failed (non-fatal): ${String(exc)}`);
|
|
@@ -14019,14 +15498,14 @@ var _AUNClient = class _AUNClient {
|
|
|
14019
15498
|
const thoughtId = String(params.thought_id ?? "") || `mt-${_uuidV4()}`;
|
|
14020
15499
|
const timestamp = Number(params.timestamp ?? Date.now());
|
|
14021
15500
|
const attempt = async (useCache) => {
|
|
14022
|
-
const
|
|
15501
|
+
const envelopeContext2 = params.context && typeof params.context === "object" && !Array.isArray(params.context) ? params.context : void 0;
|
|
14023
15502
|
const envelope = await this._buildV2P2PEnvelope({
|
|
14024
15503
|
to: toAid,
|
|
14025
15504
|
payload,
|
|
14026
15505
|
messageId: thoughtId,
|
|
14027
15506
|
timestamp,
|
|
14028
15507
|
useCache,
|
|
14029
|
-
context:
|
|
15508
|
+
context: envelopeContext2
|
|
14030
15509
|
});
|
|
14031
15510
|
const sendParams = {
|
|
14032
15511
|
to: toAid,
|
|
@@ -14079,6 +15558,9 @@ var _AUNClient = class _AUNClient {
|
|
|
14079
15558
|
allDevices = Array.isArray(bs?.devices) ? bs.devices : [];
|
|
14080
15559
|
epoch = Number(bs?.epoch ?? 0);
|
|
14081
15560
|
auditRecipientsRaw = Array.isArray(bs?.audit_recipients) ? bs.audit_recipients : [];
|
|
15561
|
+
await this._v2CheckFork(groupId, String(bs?.state_chain ?? ""));
|
|
15562
|
+
await this._v2VerifyStateSignature(groupId, bs);
|
|
15563
|
+
await this._publishV2GroupSecurityLevel(groupId, bs);
|
|
14082
15564
|
stateCommitment = {
|
|
14083
15565
|
state_version: Number(bs?.state_version ?? 0) || 0,
|
|
14084
15566
|
state_hash: String(bs?.state_hash_signed ?? bs?.state_hash ?? ""),
|
|
@@ -14093,9 +15575,6 @@ var _AUNClient = class _AUNClient {
|
|
|
14093
15575
|
stateCommitment
|
|
14094
15576
|
});
|
|
14095
15577
|
}
|
|
14096
|
-
await this._v2CheckFork(groupId, String(bs?.state_chain ?? ""));
|
|
14097
|
-
await this._v2VerifyStateSignature(groupId, bs);
|
|
14098
|
-
await this._publishV2GroupSecurityLevel(groupId, bs);
|
|
14099
15578
|
const pendingAdds = Array.isArray(bs?.pending_adds) ? bs.pending_adds : [];
|
|
14100
15579
|
if (pendingAdds.length > 0 && this._v2Session) {
|
|
14101
15580
|
this._v2MaybeTriggerAutoPropose(groupId);
|
|
@@ -14107,34 +15586,27 @@ var _AUNClient = class _AUNClient {
|
|
|
14107
15586
|
const targets = [];
|
|
14108
15587
|
for (const dev of allDevices) {
|
|
14109
15588
|
const devAid = String(dev.aid ?? "");
|
|
14110
|
-
const devId =
|
|
14111
|
-
if (devAid === this._aid && devId === this._deviceId) continue;
|
|
14112
|
-
const ikDer = _v2B64ToBytes(String(dev.ik_pk ?? ""));
|
|
14113
|
-
const spkDer = dev.spk_pk ? _v2B64ToBytes(String(dev.spk_pk)) : void 0;
|
|
15589
|
+
const devId = getV2DeviceId(dev);
|
|
15590
|
+
if (devAid === this._aid && devId.present && devId.value === this._deviceId) continue;
|
|
14114
15591
|
const role = devAid === this._aid ? "self_sync" : "member";
|
|
14115
|
-
|
|
15592
|
+
const target = await this._v2BuildTargetFromDevice({
|
|
15593
|
+
dev,
|
|
14116
15594
|
aid: devAid,
|
|
14117
|
-
deviceId: devId,
|
|
15595
|
+
deviceId: devId.value,
|
|
14118
15596
|
role,
|
|
14119
|
-
|
|
14120
|
-
ikPkDer: ikDer,
|
|
14121
|
-
spkPkDer: spkDer,
|
|
14122
|
-
spkId: String(dev.spk_id ?? "")
|
|
15597
|
+
defaultKeySource: "peer_device_prekey"
|
|
14123
15598
|
});
|
|
15599
|
+
if (target) targets.push(target);
|
|
14124
15600
|
}
|
|
14125
15601
|
for (const dev of auditRecipientsRaw) {
|
|
14126
|
-
|
|
14127
|
-
|
|
14128
|
-
const spkDer = dev.spk_pk ? _v2B64ToBytes(String(dev.spk_pk)) : void 0;
|
|
14129
|
-
targets.push({
|
|
15602
|
+
const target = await this._v2BuildTargetFromDevice({
|
|
15603
|
+
dev,
|
|
14130
15604
|
aid: String(dev.aid ?? ""),
|
|
14131
15605
|
deviceId: String(dev.device_id ?? ""),
|
|
14132
15606
|
role: "audit",
|
|
14133
|
-
|
|
14134
|
-
ikPkDer: ikDer,
|
|
14135
|
-
spkPkDer: spkDer,
|
|
14136
|
-
spkId: String(dev.spk_id ?? "")
|
|
15607
|
+
defaultKeySource: "peer_device_prekey"
|
|
14137
15608
|
});
|
|
15609
|
+
if (target) targets.push(target);
|
|
14138
15610
|
}
|
|
14139
15611
|
if (targets.length === 0) {
|
|
14140
15612
|
throw new E2EEError(`V2 group: no target devices for ${groupId}`);
|
|
@@ -14179,14 +15651,14 @@ var _AUNClient = class _AUNClient {
|
|
|
14179
15651
|
const thoughtId = String(params.thought_id ?? "") || `gt-${_uuidV4()}`;
|
|
14180
15652
|
const timestamp = Number(params.timestamp ?? Date.now());
|
|
14181
15653
|
const attempt = async (useCache) => {
|
|
14182
|
-
const
|
|
15654
|
+
const envelopeContext2 = params.context && typeof params.context === "object" && !Array.isArray(params.context) ? params.context : void 0;
|
|
14183
15655
|
const envelope = await this._buildV2GroupEnvelope({
|
|
14184
15656
|
groupId,
|
|
14185
15657
|
payload,
|
|
14186
15658
|
messageId: thoughtId,
|
|
14187
15659
|
timestamp,
|
|
14188
15660
|
useCache,
|
|
14189
|
-
context:
|
|
15661
|
+
context: envelopeContext2
|
|
14190
15662
|
});
|
|
14191
15663
|
const sendParams = {
|
|
14192
15664
|
group_id: groupId,
|
|
@@ -14237,20 +15709,26 @@ var _AUNClient = class _AUNClient {
|
|
|
14237
15709
|
const groupIdForKeys = String(aad.group_id ?? opts.envelope.group_id ?? "").trim();
|
|
14238
15710
|
let ikPriv;
|
|
14239
15711
|
let spkPriv;
|
|
14240
|
-
|
|
14241
|
-
|
|
14242
|
-
|
|
14243
|
-
|
|
14244
|
-
|
|
14245
|
-
|
|
14246
|
-
|
|
14247
|
-
|
|
15712
|
+
try {
|
|
15713
|
+
if (groupIdForKeys) {
|
|
15714
|
+
const keys = await session.getGroupDecryptKeys(groupIdForKeys, spkId);
|
|
15715
|
+
ikPriv = keys.ikPriv;
|
|
15716
|
+
spkPriv = keys.spkPriv;
|
|
15717
|
+
} else {
|
|
15718
|
+
const keys = await session.getDecryptKeys(spkId);
|
|
15719
|
+
ikPriv = keys.ikPriv;
|
|
15720
|
+
spkPriv = keys.spkPriv;
|
|
15721
|
+
}
|
|
15722
|
+
} catch (exc) {
|
|
15723
|
+
this._clientLog.warn(`V2 thought decrypt: SPK lookup failed from=${opts.fromAid}, group=${groupIdForKeys || "<p2p>"}, spk_id=${spkId || "<empty>"}: ${exc}`);
|
|
15724
|
+
return null;
|
|
14248
15725
|
}
|
|
14249
15726
|
const fromAid = String(opts.fromAid || aad.from || "").trim();
|
|
14250
15727
|
const senderDeviceId = String(aad.from_device ?? "");
|
|
14251
15728
|
const senderPubDer = await this._getV2SenderPubDer(fromAid, senderDeviceId);
|
|
14252
15729
|
if (!senderPubDer) {
|
|
14253
15730
|
this._clientLog.warn(`V2 thought decrypt: no sender IK for ${fromAid} device=${senderDeviceId}`);
|
|
15731
|
+
this._scheduleV2SenderIKFetch(fromAid, senderDeviceId, groupIdForKeys);
|
|
14254
15732
|
return null;
|
|
14255
15733
|
}
|
|
14256
15734
|
try {
|
|
@@ -14304,11 +15782,12 @@ var _AUNClient = class _AUNClient {
|
|
|
14304
15782
|
const signPayload = stableStringify(signPayloadObj);
|
|
14305
15783
|
const signPayloadBytes = new TextEncoder().encode(signPayload);
|
|
14306
15784
|
const sigBytes = base64ToUint8(stateSignature);
|
|
14307
|
-
const
|
|
14308
|
-
|
|
14309
|
-
|
|
14310
|
-
|
|
14311
|
-
|
|
15785
|
+
const cacheData = _v2LengthPrefixedBytes(
|
|
15786
|
+
new TextEncoder().encode(actorAid),
|
|
15787
|
+
signPayloadBytes,
|
|
15788
|
+
sigBytes
|
|
15789
|
+
);
|
|
15790
|
+
const cacheHashBuf = await crypto.subtle.digest("SHA-256", cacheData.slice().buffer);
|
|
14312
15791
|
const cacheHashArr = new Uint8Array(cacheHashBuf);
|
|
14313
15792
|
let cacheKey = "";
|
|
14314
15793
|
for (let i = 0; i < cacheHashArr.length; i++) cacheKey += cacheHashArr[i].toString(16).padStart(2, "0");
|
|
@@ -14502,8 +15981,9 @@ var _AUNClient = class _AUNClient {
|
|
|
14502
15981
|
const candidates = [];
|
|
14503
15982
|
for (const dev of devices) {
|
|
14504
15983
|
const aid = String(dev.aid ?? "").trim();
|
|
15984
|
+
const hasDeviceId = "device_id" in dev;
|
|
14505
15985
|
const deviceId = String(dev.device_id ?? "").trim();
|
|
14506
|
-
if (aid &&
|
|
15986
|
+
if (aid && hasDeviceId && onlineAdminAids.has(aid)) {
|
|
14507
15987
|
candidates.push(`${aid}${deviceId}`);
|
|
14508
15988
|
}
|
|
14509
15989
|
}
|
|
@@ -14517,7 +15997,7 @@ var _AUNClient = class _AUNClient {
|
|
|
14517
15997
|
this._clientLog.debug(`V2 auto propose leader elected: group=${groupId} leader=${leader}`);
|
|
14518
15998
|
return true;
|
|
14519
15999
|
}
|
|
14520
|
-
const delayMs = await this._v2LeaderDelayMs(
|
|
16000
|
+
const delayMs = await this._v2LeaderDelayMs(_v2LengthPrefixedTextKey(groupId, myKey));
|
|
14521
16001
|
this._clientLog.debug(`V2 auto propose non-leader delay: group=${groupId} leader=${leader} self=${myKey} delay_ms=${delayMs}`);
|
|
14522
16002
|
await this._sleep(delayMs);
|
|
14523
16003
|
return true;
|
|
@@ -14785,45 +16265,57 @@ var _AUNClient = class _AUNClient {
|
|
|
14785
16265
|
const pushFrom = isJsonObject(data) ? String(data.from_aid ?? "") : "";
|
|
14786
16266
|
const pushMsgId = isJsonObject(data) ? String(data.message_id ?? "") : "";
|
|
14787
16267
|
const envelopeJson = isJsonObject(data) ? data.envelope_json : void 0;
|
|
16268
|
+
const hasPayload = !!envelopeJson;
|
|
14788
16269
|
const ns = this._aid ? `p2p:${this._aid}` : "";
|
|
14789
16270
|
let contigBefore = ns ? this._seqTracker.getContiguousSeq(ns) : 0;
|
|
14790
16271
|
this._clientLog.debug(
|
|
14791
|
-
`_onV2PushNotification: push_seq=${pushSeq || "null"} push_from=${pushFrom} push_msg_id=${pushMsgId} has_payload=${
|
|
16272
|
+
`_onV2PushNotification: push_seq=${pushSeq || "null"} push_from=${pushFrom} push_msg_id=${pushMsgId} has_payload=${hasPayload} contiguous_seq=${contigBefore}`
|
|
14792
16273
|
);
|
|
14793
|
-
if (
|
|
16274
|
+
if (pushSeq > 0 && ns) {
|
|
16275
|
+
this._seqTracker.updateMaxSeen(ns, pushSeq);
|
|
16276
|
+
if (contigBefore === pushSeq) {
|
|
16277
|
+
this._clientLog.debug(
|
|
16278
|
+
`_onV2PushNotification: push seq=${pushSeq} already covered by contiguous_seq=${contigBefore}, ignore duplicate push`
|
|
16279
|
+
);
|
|
16280
|
+
return;
|
|
16281
|
+
}
|
|
16282
|
+
contigBefore = this._repairPushContiguousBound(
|
|
16283
|
+
ns,
|
|
16284
|
+
pushSeq,
|
|
16285
|
+
hasPayload,
|
|
16286
|
+
"_raw.peer.v2.message_received"
|
|
16287
|
+
);
|
|
16288
|
+
}
|
|
16289
|
+
if (hasPayload && pushSeq > 0 && ns) {
|
|
14794
16290
|
try {
|
|
14795
16291
|
const decrypted = await this._decryptV2Message(data);
|
|
14796
16292
|
if (decrypted) {
|
|
14797
|
-
this._seqTracker.onMessageSeq(ns, pushSeq);
|
|
14798
|
-
|
|
14799
|
-
this._seqTracker.forceContiguousSeq(ns, pushSeq);
|
|
14800
|
-
}
|
|
14801
|
-
await this._publishOrderedMessage("message.received", ns, pushSeq, decrypted);
|
|
16293
|
+
const needPull = this._seqTracker.onMessageSeq(ns, pushSeq);
|
|
16294
|
+
const published = await this._publishOrderedMessage("message.received", ns, pushSeq, decrypted);
|
|
14802
16295
|
const newContig = this._seqTracker.getContiguousSeq(ns);
|
|
14803
16296
|
if (newContig !== contigBefore) {
|
|
14804
16297
|
this._saveSeqTrackerState();
|
|
14805
16298
|
}
|
|
14806
16299
|
if (newContig > 0 && newContig !== contigBefore) {
|
|
14807
|
-
|
|
16300
|
+
const maxSeen = this._seqTracker.getMaxSeenSeq(ns);
|
|
16301
|
+
const ackSeq = maxSeen > 0 ? Math.min(newContig, maxSeen) : newContig;
|
|
16302
|
+
this.call("message.v2.ack", { up_to_seq: ackSeq }).catch((e) => this._clientLog.debug(`V2 P2P push-ack failed: ${e}`));
|
|
14808
16303
|
}
|
|
14809
16304
|
this._clientLog.debug(
|
|
14810
16305
|
`_onV2PushNotification: push \u5E26 payload \u89E3\u5BC6\u6210\u529F, contiguous_seq=${contigBefore}->${newContig} push_seq=${pushSeq}`
|
|
14811
16306
|
);
|
|
14812
|
-
|
|
16307
|
+
if (!needPull && (published || newContig >= pushSeq || pushSeq <= contigBefore)) {
|
|
16308
|
+
return;
|
|
16309
|
+
}
|
|
16310
|
+
this._clientLog.debug(
|
|
16311
|
+
`_onV2PushNotification: payload push seq=${pushSeq} \u56E0\u7A7A\u6D1E\u6302\u8D77\uFF0C\u7EE7\u7EED pull \u8865\u9F50 after_seq=${newContig}`
|
|
16312
|
+
);
|
|
14813
16313
|
}
|
|
14814
16314
|
} catch (exc) {
|
|
14815
16315
|
this._clientLog.debug(`_onV2PushNotification: push payload \u89E3\u5BC6\u5931\u8D25, fallback to pull: ${exc}`);
|
|
14816
16316
|
}
|
|
14817
16317
|
}
|
|
14818
16318
|
if (pushSeq > 0 && ns) {
|
|
14819
|
-
if (contigBefore >= pushSeq) {
|
|
14820
|
-
this._clientLog.warn(
|
|
14821
|
-
`_onV2PushNotification: contiguous_seq=${contigBefore} \u8D8A\u754C\uFF08>= push_seq=${pushSeq}\uFF09\uFF0C\u5F3A\u5236\u4FEE\u590D\u4E3A ${pushSeq - 1}`
|
|
14822
|
-
);
|
|
14823
|
-
this._seqTracker.forceContiguousSeq(ns, pushSeq - 1);
|
|
14824
|
-
this._saveSeqTrackerState();
|
|
14825
|
-
contigBefore = pushSeq - 1;
|
|
14826
|
-
}
|
|
14827
16319
|
this._clientLog.debug(
|
|
14828
16320
|
`_onV2PushNotification: \u7EAF\u901A\u77E5 push_seq=${pushSeq} > contiguous_seq=${contigBefore}, \u89E6\u53D1 pull(after_seq=${contigBefore})`
|
|
14829
16321
|
);
|
|
@@ -14891,8 +16383,7 @@ var _AUNClient = class _AUNClient {
|
|
|
14891
16383
|
};
|
|
14892
16384
|
__publicField(_AUNClient, "V2_BOOTSTRAP_TTL_MS", 60 * 60 * 1e3);
|
|
14893
16385
|
__publicField(_AUNClient, "V2_RETRYABLE_CODES", /* @__PURE__ */ new Set([-33011, -33012, -33050, -33052, -33054]));
|
|
14894
|
-
__publicField(_AUNClient, "_V2_SIG_CACHE_TTL",
|
|
14895
|
-
// 10 min
|
|
16386
|
+
__publicField(_AUNClient, "_V2_SIG_CACHE_TTL", 60 * 60 * 1e3);
|
|
14896
16387
|
__publicField(_AUNClient, "_V2_SIG_CACHE_MAX", 16384);
|
|
14897
16388
|
// ── 内部:断线重连 ────────────────────────────────
|
|
14898
16389
|
/** 不重连 close code 集合:认证失败/权限错误/被踢等,重连无意义 */
|
|
@@ -14964,7 +16455,7 @@ var ProtectedHeaders = class _ProtectedHeaders {
|
|
|
14964
16455
|
};
|
|
14965
16456
|
|
|
14966
16457
|
// src/index.ts
|
|
14967
|
-
var __version__ = "0.2
|
|
16458
|
+
var __version__ = "0.3.2";
|
|
14968
16459
|
export {
|
|
14969
16460
|
AUNClient,
|
|
14970
16461
|
AUNError,
|