@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.
Files changed (80) hide show
  1. package/CHANGELOG.md +19 -0
  2. package/_packed_docs/CHANGELOG.md +19 -0
  3. package/_packed_docs/sdk/04-/350/277/236/346/216/245/344/270/216/350/256/244/350/257/201.md +48 -15
  4. package/_packed_docs/sdk/06-API/346/211/213/345/206/214.md +182 -28
  5. package/_packed_docs/sdk/AUN_DOCS_GUIDE.md +7 -5
  6. package/_packed_docs/sdk/INDEX.md +17 -12
  7. package/dist/auth.d.ts.map +1 -1
  8. package/dist/auth.js +1 -4
  9. package/dist/auth.js.map +1 -1
  10. package/dist/bundle.js +2093 -602
  11. package/dist/client.d.ts +64 -7
  12. package/dist/client.d.ts.map +1 -1
  13. package/dist/client.js +1441 -476
  14. package/dist/client.js.map +1 -1
  15. package/dist/crypto.d.ts.map +1 -1
  16. package/dist/crypto.js +45 -31
  17. package/dist/crypto.js.map +1 -1
  18. package/dist/discovery.d.ts +4 -0
  19. package/dist/discovery.d.ts.map +1 -1
  20. package/dist/discovery.js +16 -11
  21. package/dist/discovery.js.map +1 -1
  22. package/dist/index.d.ts +1 -1
  23. package/dist/index.d.ts.map +1 -1
  24. package/dist/index.js +1 -1
  25. package/dist/index.js.map +1 -1
  26. package/dist/keystore/index.d.ts +22 -0
  27. package/dist/keystore/index.d.ts.map +1 -1
  28. package/dist/keystore/indexeddb.d.ts +4 -1
  29. package/dist/keystore/indexeddb.d.ts.map +1 -1
  30. package/dist/keystore/indexeddb.js +104 -1
  31. package/dist/keystore/indexeddb.js.map +1 -1
  32. package/dist/logger.d.ts +5 -1
  33. package/dist/logger.d.ts.map +1 -1
  34. package/dist/logger.js +8 -2
  35. package/dist/logger.js.map +1 -1
  36. package/dist/namespaces/auth.d.ts +1 -0
  37. package/dist/namespaces/auth.d.ts.map +1 -1
  38. package/dist/namespaces/auth.js +38 -0
  39. package/dist/namespaces/auth.js.map +1 -1
  40. package/dist/seq-tracker.d.ts +5 -3
  41. package/dist/seq-tracker.d.ts.map +1 -1
  42. package/dist/seq-tracker.js +30 -3
  43. package/dist/seq-tracker.js.map +1 -1
  44. package/dist/transport.d.ts.map +1 -1
  45. package/dist/transport.js +18 -0
  46. package/dist/transport.js.map +1 -1
  47. package/dist/v2/crypto/canonical.d.ts +1 -1
  48. package/dist/v2/crypto/canonical.d.ts.map +1 -1
  49. package/dist/v2/crypto/canonical.js +42 -13
  50. package/dist/v2/crypto/canonical.js.map +1 -1
  51. package/dist/v2/crypto/ecdh.d.ts.map +1 -1
  52. package/dist/v2/crypto/ecdh.js +18 -1
  53. package/dist/v2/crypto/ecdh.js.map +1 -1
  54. package/dist/v2/e2ee/decrypt.d.ts.map +1 -1
  55. package/dist/v2/e2ee/decrypt.js +56 -2
  56. package/dist/v2/e2ee/decrypt.js.map +1 -1
  57. package/dist/v2/e2ee/encrypt-group.d.ts.map +1 -1
  58. package/dist/v2/e2ee/encrypt-group.js +16 -6
  59. package/dist/v2/e2ee/encrypt-group.js.map +1 -1
  60. package/dist/v2/e2ee/encrypt-p2p.d.ts.map +1 -1
  61. package/dist/v2/e2ee/encrypt-p2p.js +39 -11
  62. package/dist/v2/e2ee/encrypt-p2p.js.map +1 -1
  63. package/dist/v2/e2ee/metadata-auth.d.ts +1 -0
  64. package/dist/v2/e2ee/metadata-auth.d.ts.map +1 -1
  65. package/dist/v2/e2ee/metadata-auth.js +51 -0
  66. package/dist/v2/e2ee/metadata-auth.js.map +1 -1
  67. package/dist/v2/e2ee/types.d.ts +2 -2
  68. package/dist/v2/e2ee/types.d.ts.map +1 -1
  69. package/dist/v2/session/keystore.d.ts +12 -4
  70. package/dist/v2/session/keystore.d.ts.map +1 -1
  71. package/dist/v2/session/keystore.js +177 -35
  72. package/dist/v2/session/keystore.js.map +1 -1
  73. package/dist/v2/session/session.d.ts +10 -3
  74. package/dist/v2/session/session.d.ts.map +1 -1
  75. package/dist/v2/session/session.js +91 -17
  76. package/dist/v2/session/session.js.map +1 -1
  77. package/dist/v2/state/commitment.d.ts.map +1 -1
  78. package/dist/v2/state/commitment.js +4 -1
  79. package/dist/v2/state/commitment.js.map +1 -1
  80. 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 p256Oid = [6, 8, 42, 134, 72, 206, 61, 3, 1, 7];
115
- for (let i = 0; i <= certDer.length - p256Oid.length; i++) {
116
- let match = true;
117
- for (let j = 0; j < p256Oid.length; j++) {
118
- if (certDer[i + j] !== p256Oid[j]) {
119
- match = false;
120
- break;
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
- if (!match) continue;
124
- for (let seqStart = Math.max(0, i - 32); seqStart <= i; seqStart++) {
125
- if (certDer[seqStart] !== 48) continue;
126
- const seqLen = parseDerLength(certDer, seqStart + 1);
127
- if (seqLen === null) continue;
128
- const totalLen = 1 + seqLen.lenBytes + seqLen.value;
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
- throw new Error("unable to extract SPKI public key from certificate");
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(`discover enter: url=${wellKnownUrl}`);
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(`discover exit (error): elapsed=${Date.now() - tStart}ms err=${exc instanceof Error ? exc.message : String(exc)}`);
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(`discover exit (error): elapsed=${Date.now() - tStart}ms err=empty_gateways`);
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 url = sorted[0]?.url;
939
- if (!url) {
940
- this._log.debug(`discover exit (error): elapsed=${Date.now() - tStart}ms err=missing_url`);
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(String(url), timeout).catch(() => {
964
+ this.checkHealth(urls[0], timeout).catch(() => {
944
965
  });
945
- this._log.debug(`discover exit: elapsed=${Date.now() - tStart}ms gateway=${String(url)} candidates=${gateways.length}`);
946
- return String(url);
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 (!this._deviceId || typeof this._keystore.loadInstanceState !== "function") {
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
- /** 强制跳过不连续区间,将 contiguousSeq 拨到指定位置。
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 decoder = new TextDecoder();
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 = decoder.decode(valueBytes).trim();
3365
+ commonName = decoder2.decode(valueBytes).trim();
3309
3366
  } else {
3310
- commonName = decoder.decode(valueBytes).trim();
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 = 5;
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 = 1;
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
- const store = db.createObjectStore(V2_STORE_NAME, {
6377
- keyPath: ["device_id", "key_type", "key_id"]
6378
- });
6379
- store.createIndex(
6380
- V2_INDEX_BY_DEVICE_TYPE_CREATED,
6381
- ["device_id", "key_type", "created_at"]
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").delete([deviceId, "spk", spkId]);
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
- key_id: _V2KeyStore._groupSpkKeyId(groupId, spkId),
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", keyId]);
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,key_id 前缀匹配)。 */
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
- if (r.key_id.startsWith(prefix)) {
6550
- const spkId = r.key_id.slice(prefix.length);
6551
- resolve({
6552
- spkId,
6553
- priv: new Uint8Array(r.private_key),
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
- return new Promise((resolve, reject) => {
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
- return n.toString(10);
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 s;
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 = b64UrlToBytes(jwk.d);
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._deviceId);
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._deviceId, spkId, priv, pubDer);
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 == 当前 SPK:当前 spkPriv
9365
- * - 否则:从 store 加载旧 SPK 私钥(可能 undefined = 已销毁)
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._store.loadSPK(this._deviceId, spkId);
9372
- if (!oldSPK) return { ikPriv: this._ikPriv };
9373
- return { ikPriv: this._ikPriv, spkPriv: oldSPK };
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._deviceId, RECENT_GENERATIONS)
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
- return this._lastUploadedGroupSPKIds.get(gk) === spkId;
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._deviceId, gk);
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._deviceId, gk, spkId, priv, pubDer);
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._deviceId, gk, spkId, priv, pubDer);
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
- /** 群消息解密优先查 group SPK;找不到时 fallback P2P SPK 兼容历史消息。 */
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 groupSpk = await this._store.loadGroupSPK(this._deviceId, gk, spkId);
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 (t.spkPkDer && (t.keySource === "peer_device_prekey" || t.keySource === "group_device_prekey")) {
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
- if (k === "_auth") continue;
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 (target.spkPkDer && (keySource === "peer_device_prekey" || keySource === "group_device_prekey")) {
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
- keySource,
10304
+ rowKeySource,
9787
10305
  fp,
9788
- target.spkId ?? "",
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 (t.spkPkDer && (t.keySource === "peer_device_prekey" || t.keySource === "group_device_prekey")) {
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 (target.spkPkDer && (keySource === "peer_device_prekey" || keySource === "group_device_prekey")) {
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
- keySource,
10492
+ rowKeySource,
9963
10493
  fp,
9964
- target.spkId ?? "",
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 base64ToBytes(s) {
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 = base64ToBytes(env.sender_session_pk);
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 = base64ToBytes(row[6]);
10053
- const wrappedKey = base64ToBytes(row[7]);
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
- const masterKey = await aesGcmDecrypt(
10060
- wrapKey,
10061
- wrapNonce,
10062
- wrappedCt,
10063
- wrappedTag,
10064
- new Uint8Array(0)
10065
- );
10066
- const msgNonce = base64ToBytes(env.nonce);
10067
- const ct = base64ToBytes(env.ciphertext);
10068
- const tag = base64ToBytes(env.tag);
10069
- const plaintext = await aesGcmDecrypt(masterKey, msgNonce, ct, tag, aadBytes);
10070
- return JSON.parse(new TextDecoder().decode(plaintext));
10071
- }
10072
- async function verifySenderSignature(env, senderPubDer) {
10073
- const sig = base64ToBytes(env.sender_signature);
10074
- const ct = base64ToBytes(env.ciphertext);
10075
- const tag = base64ToBytes(env.tag);
10076
- const aadBytes = canonicalJson(env.aad);
10077
- const digestBytes = hexToBytes6(env.recipients_digest);
10078
- const signInput = new Uint8Array(
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 >>> 0, false);
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
- return {
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._logger = new AUNLogger({ debug: _debug });
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) => this._observeRpcMeta(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。接收 agent.md 文本(应用层用 `<input type=file>` 等读出文本传入),
10750
- * 内部签名 上传 → 更新 _localAgentMdEtag(quoted sha256,与服务端一致)。
11476
+ * 浏览器版本 publishAgentMd。默认从 {agentMdPath}/{self_aid}/agent.md 的等价 IndexedDB 正文读取,
11477
+ * 然后签名、上传,并刷新 list.json 元数据。
10751
11478
  *
10752
- * @throws ValidationError content 为空
11479
+ * 兼容旧浏览器调用:传入 content 时会先写入等价正文,再从该正文发布。
10753
11480
  */
10754
11481
  async publishAgentMd(content) {
10755
- const text = String(content ?? "");
10756
- if (text.length === 0) {
10757
- throw new ValidationError("publishAgentMd requires non-empty content");
11482
+ const target = this._agentMdOwnerAid();
11483
+ if (!target) {
11484
+ throw new ValidationError("publishAgentMd requires local AID");
10758
11485
  }
10759
- const signed = await this.auth.signAgentMd(text);
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
- const buf = new TextEncoder().encode(signed);
10762
- const digest = await crypto.subtle.digest("SHA-256", buf);
10763
- const hex = Array.from(new Uint8Array(digest)).map((b) => b.toString(16).padStart(2, "0")).join("");
10764
- this._localAgentMdEtag = `"${hex}"`;
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 缺省时取自身;不接受 savePath(浏览器无文件系统);
10769
- * aid 是自己则同步刷新 _localAgentMdEtag in_sync。
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 buf = new TextEncoder().encode(content);
10782
- const digest = await crypto.subtle.digest("SHA-256", buf);
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
- try {
10855
- await this._connectOnce(normalized, false);
10856
- this._clientLog.debug(`connect exit: elapsed=${Date.now() - tStart}ms state=${this._state}`);
10857
- } catch (err) {
10858
- if (this._state === "connecting" || this._state === "authenticating") {
10859
- this._state = "disconnected";
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.") && this._deviceId && p.device_id === void 0) {
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
- await this._signClientOperation(method, p);
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: contig,
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: contig,
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
- const result = await this.call("group.pull_events", {
11474
- group_id: groupId,
11475
- after_event_seq: afterSeq,
11476
- device_id: this._deviceId,
11477
- limit: 50
11478
- });
11479
- if (isJsonObject(result)) {
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
- this._seqTracker.onPullResult(ns, events.filter(isJsonObject), afterSeq);
11483
- const cursor = isJsonObject(result.cursor) ? result.cursor : null;
11484
- const serverAck = cursor ? Number(cursor.current_seq ?? 0) : 0;
11485
- if (serverAck > 0) {
11486
- const contigBefore = this._seqTracker.getContiguousSeq(ns);
11487
- if (contigBefore < serverAck) {
11488
- this._clientLog.info("group.pull_events retention-floor advance: ns=" + ns + " contiguous=" + contigBefore + " -> cursor.current_seq=" + serverAck);
11489
- this._seqTracker.forceContiguousSeq(ns, serverAck);
11490
- }
11491
- }
11492
- this._saveSeqTrackerState();
11493
- const contig = this._seqTracker.getContiguousSeq(ns);
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
- for (const evt of events) {
11505
- if (isJsonObject(evt)) {
11506
- evt._from_gap_fill = true;
11507
- const et = String(evt.event_type ?? "");
11508
- if (et === "group.message_created") continue;
11509
- const cs = evt.client_signature;
11510
- if (cs && typeof cs === "object") {
11511
- evt._verified = await this._verifyEventSignature(evt, cs);
11512
- }
11513
- await this._dispatcher.publish("group.changed", evt);
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 (this._deviceId && !String(result.device_id ?? "").trim()) {
12824
+ if (!("device_id" in result)) {
11615
12825
  result.device_id = this._deviceId;
11616
12826
  }
11617
- if (this._slotId && !String(result.slot_id ?? "").trim()) {
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
- const targetDeviceId = String(message.device_id ?? "").trim();
11678
- if (targetDeviceId && this._deviceId && targetDeviceId !== this._deviceId) {
11679
- return false;
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
- const targetSlotId = String(message.slot_id ?? "").trim();
11682
- if (targetSlotId && this._slotId && targetSlotId !== this._slotId) {
11683
- return false;
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
- d._verified = await this._verifyEventSignature(d, cs);
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
- if (action === "joined" || action === "join_approved") {
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" && this._v2Session) {
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
- const verified = await this._verifyEventSignature(d, cs);
11853
- if (verified === false) {
11854
- this._clientLog.warn(`state_committed committer signature verify failed group=%s${String(groupId)}`);
11855
- return;
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 = v2E2eeMeta(payload);
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: (decrypted ?? message).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(), 5e3);
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(), 5e3);
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 gateway = String(params.gateway ?? this._gatewayUrl ?? "");
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 bs = await this.call("message.v2.bootstrap", { peer_aid: fromAid });
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
- if (senderDeviceId) {
13349
- session.cachePeerIK(fromAid, senderDeviceId, senderPubDer);
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: CA fallback for ${fromAid} failed: ${String(formatCaughtError(exc))}`);
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
- let peerDevices = [];
13376
- let auditRaw = [];
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
- const seqs = messages.map((msg) => Number(msg.seq ?? 0)).filter((seq) => Number.isFinite(seq) && seq > 0);
13506
- const contigBefore = ns ? this._seqTracker.getContiguousSeq(ns) : 0;
13507
- if (ns && seqs.length > 0 && seqs[0] > contigBefore) {
13508
- this._seqTracker.forceContiguousSeq(ns, seqs[0]);
13509
- }
13510
- for (const msg of messages) {
13511
- const seq = Number(msg.seq ?? 0);
13512
- if (!Number.isFinite(seq) || seq <= 0) continue;
13513
- const version = String(msg.version ?? "v2");
13514
- if (version === "v1") {
13515
- const legacy = isJsonObject(msg.legacy_v1) ? msg.legacy_v1 : {};
13516
- const legacyPayload = legacy.payload;
13517
- const payloadType = isJsonObject(legacyPayload) ? String(legacyPayload.type ?? "").trim() : "";
13518
- if (legacyPayload !== void 0 && legacyPayload !== null && payloadType !== "e2ee.encrypted" && payloadType !== "e2ee.group_encrypted") {
13519
- const v1Msg = {
13520
- message_id: String(msg.message_id ?? ""),
13521
- from: String(msg.from_aid ?? ""),
13522
- to: String(legacy.to ?? this._aid ?? ""),
13523
- seq: msg.seq,
13524
- type: String(msg.type ?? ""),
13525
- timestamp: msg.t_server,
13526
- payload: legacyPayload,
13527
- encrypted: false
13528
- };
13529
- if (ns) await this._publishPulledMessage("message.received", ns, seq, v1Msg);
13530
- else await this._publishAppEvent("message.received", v1Msg);
13531
- decrypted.push(v1Msg);
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._clientLog.debug(`message.v2.pull skipping V1 envelope seq=${seq} payload_type=${payloadType || "<none>"} (V1 E2EE removed)`);
14948
+ await this._publishAppEvent("message.received", plaintext);
14949
+ decrypted.push(plaintext);
13534
14950
  }
13535
- continue;
13536
14951
  }
13537
- if (version !== "v2") {
13538
- this._clientLog.debug(`message.v2.pull skipping non-V2 row seq=${seq} version=${String(msg.version ?? "")}`);
13539
- continue;
13540
- }
13541
- const msgSpkId = String(msg.spk_id ?? "");
13542
- if (msgSpkId && this._v2Session && !this._v2Session.isCurrentSPK(msgSpkId)) {
13543
- this._v2Session.trackOldSPKMaxSeq(msgSpkId, seq);
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
- await this._publishPulledMessage("message.received", ns, seq, plaintext);
13549
- decrypted.push(plaintext);
13550
- } else {
13551
- await this._publishAppEvent("message.received", plaintext);
13552
- decrypted.push(plaintext);
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
- const seq = upToSeq ?? (ns ? this._seqTracker.getContiguousSeq(ns) : 0);
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
- if (groupIdForKeys) {
13646
- const keys = await session.getGroupDecryptKeys(groupIdForKeys, spkId);
13647
- ikPriv = keys.ikPriv;
13648
- spkPriv = keys.spkPriv;
13649
- } else {
13650
- const keys = await session.getDecryptKeys(spkId);
13651
- ikPriv = keys.ikPriv;
13652
- spkPriv = keys.spkPriv;
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}, cannot verify signature`);
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
- await this._dispatcher.publish("message.undecryptable", {
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
- _decrypt_error: "sender_ik_not_found"
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
- await this._dispatcher.publish("message.undecryptable", {
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
- _decrypt_error: String(exc)
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
- return {
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: v2E2eeMeta(envelope)
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
- const seqs = messages.map((msg) => Number(msg.seq ?? 0)).filter((seq) => Number.isFinite(seq) && seq > 0);
13809
- const contigBefore = this._seqTracker.getContiguousSeq(ns);
13810
- if (seqs.length > 0 && seqs[0] > contigBefore) {
13811
- this._seqTracker.forceContiguousSeq(ns, seqs[0]);
13812
- }
13813
- for (const msg of messages) {
13814
- const seq = Number(msg.seq ?? 0);
13815
- if (!Number.isFinite(seq) || seq <= 0) continue;
13816
- const version = String(msg.version ?? "v2");
13817
- if (version === "v1") {
13818
- const payload = msg.payload;
13819
- const payloadObj = isJsonObject(payload) ? payload : null;
13820
- if (payloadObj) {
13821
- const payloadType = String(payloadObj.type ?? "").trim();
13822
- if (payloadType !== "e2ee.encrypted" && payloadType !== "e2ee.group_encrypted") {
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
- } else if (payload !== void 0 && payload !== null) {
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
- this._clientLog.debug(`group.v2.pull skipping V1 envelope group=${gid} seq=${seq} payload_type=${payloadObj ? String(payloadObj.type ?? "") : "<none>"} (V1 E2EE removed)`);
13853
- continue;
13854
- }
13855
- if (version !== "v2") {
13856
- this._clientLog.debug(`group.v2.pull skipping non-V2 row group=${gid} seq=${seq} version=${String(msg.version ?? "")}`);
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 plaintext = await this._decryptV2Message(msg);
13860
- if (plaintext === null) continue;
13861
- plaintext.group_id = gid;
13862
- await this._publishPulledMessage("group.message_created", ns, seq, plaintext);
13863
- decrypted.push(plaintext);
13864
- }
13865
- if (seqs.length > 0) {
13866
- const maxSeq = Math.max(...seqs);
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
- if (ackSeq !== contigBefore) {
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
- const seq = upToSeq ?? this._seqTracker.getContiguousSeq(ns);
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 ikDer = _v2B64ToBytes(String(dev.ik_pk ?? ""));
13933
- const spkDer = dev.spk_pk ? _v2B64ToBytes(String(dev.spk_pk)) : void 0;
13934
- session.cachePeerIK(to, String(dev.device_id ?? ""), ikDer);
13935
- targets.push({
15421
+ const devId = getV2DeviceId(dev);
15422
+ const target = await this._v2BuildTargetFromDevice({
15423
+ dev,
13936
15424
  aid: to,
13937
- deviceId: String(dev.device_id ?? ""),
15425
+ deviceId: devId.value,
13938
15426
  role: "peer",
13939
- keySource: String(dev.key_source ?? "peer_device_prekey"),
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
- if (!dev.ik_pk) continue;
13947
- const ikDer = _v2B64ToBytes(String(dev.ik_pk));
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
- keySource: String(dev.key_source ?? "peer_device_prekey"),
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 = String(dev.owner_device_id ?? dev.device_id ?? "");
13978
- if (devId === this._deviceId) continue;
13979
- const ikDer = _v2B64ToBytes(String(dev.ik_pk ?? ""));
13980
- const spkDer = dev.spk_pk ? _v2B64ToBytes(String(dev.spk_pk)) : void 0;
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
- keySource: String(dev.key_source ?? "peer_device_prekey"),
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 envelopeContext = params.context && typeof params.context === "object" && !Array.isArray(params.context) ? params.context : void 0;
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: envelopeContext
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 = String(dev.device_id ?? "");
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
- targets.push({
15592
+ const target = await this._v2BuildTargetFromDevice({
15593
+ dev,
14116
15594
  aid: devAid,
14117
- deviceId: devId,
15595
+ deviceId: devId.value,
14118
15596
  role,
14119
- keySource: String(dev.key_source ?? "peer_device_prekey"),
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
- if (!dev.ik_pk) continue;
14127
- const ikDer = _v2B64ToBytes(String(dev.ik_pk));
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
- keySource: String(dev.key_source ?? "peer_device_prekey"),
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 envelopeContext = params.context && typeof params.context === "object" && !Array.isArray(params.context) ? params.context : void 0;
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: envelopeContext
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
- if (groupIdForKeys) {
14241
- const keys = await session.getGroupDecryptKeys(groupIdForKeys, spkId);
14242
- ikPriv = keys.ikPriv;
14243
- spkPriv = keys.spkPriv;
14244
- } else {
14245
- const keys = await session.getDecryptKeys(spkId);
14246
- ikPriv = keys.ikPriv;
14247
- spkPriv = keys.spkPriv;
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 cacheInput = new TextEncoder().encode(actorAid + "\0" + signPayload + "\0");
14308
- const cacheData = new Uint8Array(cacheInput.length + sigBytes.length);
14309
- cacheData.set(cacheInput, 0);
14310
- cacheData.set(sigBytes, cacheInput.length);
14311
- const cacheHashBuf = await crypto.subtle.digest("SHA-256", cacheData.buffer);
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 && deviceId && onlineAdminAids.has(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(`${groupId}\0${myKey}`);
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=${!!envelopeJson} contiguous_seq=${contigBefore}`
16272
+ `_onV2PushNotification: push_seq=${pushSeq || "null"} push_from=${pushFrom} push_msg_id=${pushMsgId} has_payload=${hasPayload} contiguous_seq=${contigBefore}`
14792
16273
  );
14793
- if (envelopeJson && pushSeq > 0 && ns) {
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
- if (pushSeq === contigBefore + 1) {
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
- this._transport.call("message.v2.ack", { up_to_seq: newContig }).catch((e) => this._clientLog.debug(`V2 P2P push-ack failed: ${e}`));
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
- return;
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", 6e5);
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.13";
16458
+ var __version__ = "0.3.2";
14968
16459
  export {
14969
16460
  AUNClient,
14970
16461
  AUNError,