@agentunion/fastaun-browser 0.3.0 → 0.3.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/bundle.js CHANGED
@@ -948,12 +948,172 @@ var GatewayDiscovery = class {
948
948
  };
949
949
 
950
950
  // src/transport.ts
951
+ var MAX_WS_PAYLOAD_SIZE = 1e6;
951
952
  var _noopLog3 = { error: () => {
952
953
  }, warn: () => {
953
954
  }, info: () => {
954
955
  }, debug: () => {
955
956
  } };
956
957
  var _rpcIdCounter = 0;
958
+ var TRACE_SPAN_DETAIL_FIELDS = [
959
+ "method",
960
+ "route",
961
+ "namespace",
962
+ "instance_id",
963
+ "aid",
964
+ "caller_aid",
965
+ "peer_aid",
966
+ "to_aid",
967
+ "from_aid",
968
+ "group_id",
969
+ "message_id",
970
+ "event",
971
+ "status",
972
+ "error_code",
973
+ "error_msg",
974
+ "found",
975
+ "delivered_count",
976
+ "success",
977
+ "created",
978
+ "connection_id",
979
+ "device_id",
980
+ "slot_id",
981
+ "key_source",
982
+ "spk_id",
983
+ "curve",
984
+ "lifecycle_state",
985
+ "auth_method"
986
+ ];
987
+ function sortTraceSpansForDisplay(spans) {
988
+ const indexed = spans.map((span, idx) => ({ idx, span })).filter(({ span }) => span !== null && typeof span === "object");
989
+ indexed.sort((a, b) => {
990
+ const stageA = _spanStage(a.span);
991
+ const stageB = _spanStage(b.span);
992
+ if (stageA !== stageB) return stageA - stageB;
993
+ return a.idx - b.idx;
994
+ });
995
+ return indexed.map(({ span }) => span);
996
+ }
997
+ function _spanStage(span) {
998
+ const node = String(span.node ?? "");
999
+ const action = String(span.action ?? "process");
1000
+ if (node === "sdk" && action === "send") return 0;
1001
+ if (node === "gateway" && (action === "relay_in" || action === "enter")) return 10;
1002
+ if (node === "gateway" && (action === "relay_out" || action === "exit")) return 90;
1003
+ if (node === "sdk" && action === "recv") return 100;
1004
+ return 50;
1005
+ }
1006
+ function traceLogicalOffsets(spans) {
1007
+ let totalMs = null;
1008
+ for (const span of spans) {
1009
+ if (span.node === "sdk" && span.action === "recv") {
1010
+ const ms = span.ms;
1011
+ if (typeof ms === "number" && ms >= 0) {
1012
+ totalMs = Math.round(ms);
1013
+ break;
1014
+ }
1015
+ }
1016
+ }
1017
+ const serverTs = [];
1018
+ for (const span of spans) {
1019
+ if (span.node !== "sdk" && typeof span.ts === "number" && span.ts > 0) {
1020
+ serverTs.push(Math.round(span.ts));
1021
+ }
1022
+ }
1023
+ const serverMin = serverTs.length > 0 ? Math.min(...serverTs) : null;
1024
+ const serverMax = serverTs.length > 0 ? Math.max(...serverTs) : null;
1025
+ const serverDur = serverMin !== null && serverMax !== null ? serverMax - serverMin : 0;
1026
+ let serverBase;
1027
+ let serverScale;
1028
+ if (totalMs !== null && serverMin !== null) {
1029
+ if (serverDur > totalMs && serverDur > 0) {
1030
+ serverBase = 0;
1031
+ serverScale = totalMs / serverDur;
1032
+ } else {
1033
+ serverBase = Math.max(0, totalMs - serverDur);
1034
+ serverScale = 1;
1035
+ }
1036
+ } else {
1037
+ serverBase = 0;
1038
+ serverScale = 1;
1039
+ }
1040
+ const offsets = [];
1041
+ let lastOffset = 0;
1042
+ for (let idx = 0; idx < spans.length; idx++) {
1043
+ const span = spans[idx];
1044
+ const node = span.node;
1045
+ const action = span.action;
1046
+ const ts = span.ts;
1047
+ let offset;
1048
+ if (node === "sdk" && action === "send") {
1049
+ offset = 0;
1050
+ } else if (node === "sdk" && action === "recv" && totalMs !== null) {
1051
+ offset = totalMs;
1052
+ } else if (node !== "sdk" && serverMin !== null && typeof ts === "number" && ts > 0) {
1053
+ offset = Math.round(serverBase + (Math.round(ts) - serverMin) * serverScale);
1054
+ } else {
1055
+ offset = idx === 0 ? 0 : lastOffset;
1056
+ }
1057
+ if (offset < lastOffset) offset = lastOffset;
1058
+ offsets.push(offset);
1059
+ lastOffset = offset;
1060
+ }
1061
+ return offsets;
1062
+ }
1063
+ function formatTraceFields(span) {
1064
+ const parts = [];
1065
+ for (const key of TRACE_SPAN_DETAIL_FIELDS) {
1066
+ const value = span[key];
1067
+ if (value === void 0 || value === null || value === "") continue;
1068
+ let s = String(value);
1069
+ if (s.length > 48) s = s.slice(0, 45) + "...";
1070
+ parts.push(`${key}=${s}`);
1071
+ }
1072
+ return parts.join(" ");
1073
+ }
1074
+ function formatTraceTree(spans) {
1075
+ if (!spans || spans.length === 0) return "";
1076
+ const sortedSpans = sortTraceSpansForDisplay(spans);
1077
+ const offsets = traceLogicalOffsets(sortedSpans);
1078
+ const lines = [];
1079
+ const stack = [];
1080
+ for (let idx = 0; idx < sortedSpans.length; idx++) {
1081
+ const span = sortedSpans[idx];
1082
+ const node = String(span.node ?? "?");
1083
+ const action = String(span.action ?? "process");
1084
+ const timePart = ` +${offsets[idx]}ms`;
1085
+ if (action === "enter") {
1086
+ const indent = " ".repeat(stack.length);
1087
+ const fieldsStr = formatTraceFields(span);
1088
+ const detail = fieldsStr ? ` ${fieldsStr}` : "";
1089
+ lines.push(`${indent}\u251C\u2500 ${node}.enter${detail}${timePart}`);
1090
+ stack.push({ node, span });
1091
+ } else if (action === "exit") {
1092
+ if (stack.length > 0 && stack[stack.length - 1].node === node) {
1093
+ stack.pop();
1094
+ }
1095
+ const indent = " ".repeat(stack.length);
1096
+ const dur = span.ms ?? 0;
1097
+ const fieldsStr = formatTraceFields(span);
1098
+ const detail = fieldsStr ? ` ${fieldsStr}` : "";
1099
+ lines.push(`${indent}\u2514\u2500 ${node}.exit${detail} dur=${dur}ms${timePart}`);
1100
+ } else {
1101
+ const indent = " ".repeat(stack.length);
1102
+ const fieldsStr = formatTraceFields(span);
1103
+ const dur = span.ms;
1104
+ const durPart = dur !== void 0 && dur !== null ? ` dur=${dur}ms` : "";
1105
+ const detail = fieldsStr ? ` ${fieldsStr}` : "";
1106
+ lines.push(`${indent}\u251C\u2500 ${node}.${action}${detail}${durPart}${timePart}`);
1107
+ }
1108
+ }
1109
+ return lines.join("\n");
1110
+ }
1111
+ function traceDisplay(method, status, durationMs, trace, spans) {
1112
+ const tree = formatTraceTree(spans);
1113
+ const header = `[TRACE][${method}][${status}] total=${durationMs}ms trace_id=${String(trace.trace_id ?? "")}`;
1114
+ return tree ? `${header}
1115
+ ${tree}` : header;
1116
+ }
957
1117
  var EVENT_NAME_MAP = {
958
1118
  "message.received": "message.received",
959
1119
  "message.recalled": "message.recalled",
@@ -1048,6 +1208,10 @@ var RPCTransport = class {
1048
1208
  // Gateway 在 RPC envelope 注入 _meta 字段(与 result 同级),由 client 层 observer 接收。
1049
1209
  // 注入失败 / 字段缺失时 observer 不会被调用,不影响业务路径。
1050
1210
  __publicField(this, "_metaObserver", null);
1211
+ // Trace 模式:off / log / diag
1212
+ __publicField(this, "_traceMode", "off");
1213
+ // Trace observer:observer(traceInfo) 在每次 RPC/事件携带 _trace 时调用
1214
+ __publicField(this, "_traceObserver", null);
1051
1215
  this._dispatcher = opts.eventDispatcher;
1052
1216
  this._timeout = opts.timeout ?? 10;
1053
1217
  this._onDisconnect = opts.onDisconnect ?? null;
@@ -1068,6 +1232,17 @@ var RPCTransport = class {
1068
1232
  setMetaObserver(observer) {
1069
1233
  this._metaObserver = observer;
1070
1234
  }
1235
+ /** 设置 trace 模式:off / log / diag */
1236
+ setTraceMode(mode) {
1237
+ if (mode !== "off" && mode !== "log" && mode !== "diag") {
1238
+ throw new ValidationError(`invalid trace mode: ${mode}, must be off/log/diag`);
1239
+ }
1240
+ this._traceMode = mode;
1241
+ }
1242
+ /** 注册 trace observer;observer(traceInfo) 在每次 RPC/事件携带 _trace 时调用。 */
1243
+ setTraceObserver(observer) {
1244
+ this._traceObserver = observer;
1245
+ }
1071
1246
  /** 获取连接时收到的 challenge */
1072
1247
  get challenge() {
1073
1248
  return this._challenge;
@@ -1156,24 +1331,43 @@ var RPCTransport = class {
1156
1331
  * 发起 JSON-RPC 2.0 调用。
1157
1332
  * 返回 result 字段的值;若有 error 字段则抛出映射后的错误。
1158
1333
  */
1159
- async call(method, params, timeout) {
1334
+ async call(method, params, timeout, trace) {
1160
1335
  if (this._closed || !this._ws) {
1161
1336
  throw new ConnectionError("transport not connected");
1162
1337
  }
1163
1338
  const rpcId = `rpc-${String(++_rpcIdCounter).padStart(6, "0")}`;
1164
1339
  const effectiveTimeout = (timeout ?? this._timeout) * 1e3;
1165
1340
  const tStart = Date.now();
1341
+ const effectiveTraceMode = trace === "off" || trace === "log" || trace === "diag" ? trace : this._traceMode;
1342
+ let traceId = "";
1343
+ let sendParams = params ?? {};
1344
+ if (effectiveTraceMode !== "off") {
1345
+ traceId = typeof crypto !== "undefined" && crypto.randomUUID ? crypto.randomUUID().replace(/-/g, "") : Array.from({ length: 32 }, () => Math.floor(Math.random() * 16).toString(16)).join("");
1346
+ sendParams = { ...params ?? {} };
1347
+ const tracePayload = { trace_id: traceId, mode: effectiveTraceMode };
1348
+ if (effectiveTraceMode === "diag") {
1349
+ tracePayload.spans = [{ node: "sdk", ts: tStart, action: "send" }];
1350
+ }
1351
+ sendParams._trace = tracePayload;
1352
+ this._log.info(`[trace=${traceId}] rpc_send method=${method} rpc_id=${rpcId}`);
1353
+ }
1166
1354
  const promise = new Promise((resolve, reject) => {
1167
1355
  this._pending.set(rpcId, { resolve, reject });
1168
1356
  });
1357
+ const payload = JSON.stringify({
1358
+ jsonrpc: "2.0",
1359
+ id: rpcId,
1360
+ method,
1361
+ params: sendParams
1362
+ });
1363
+ const payloadSize = new TextEncoder().encode(payload).length;
1364
+ if (payloadSize > MAX_WS_PAYLOAD_SIZE) {
1365
+ this._pending.delete(rpcId);
1366
+ throw new ValidationError("payload is too large");
1367
+ }
1169
1368
  try {
1170
- this._ws.send(JSON.stringify({
1171
- jsonrpc: "2.0",
1172
- id: rpcId,
1173
- method,
1174
- params: params ?? {}
1175
- }));
1176
- this._log.debug(`RPC request sent: method=${method}, id=${rpcId} ${summarizeDict(params, DIAG_PARAM_FIELDS)}`);
1369
+ this._ws.send(payload);
1370
+ this._log.debug(`RPC request sent: method=${method}, id=${rpcId} ${summarizeDict(sendParams, DIAG_PARAM_FIELDS)}`);
1177
1371
  } catch (exc) {
1178
1372
  this._pending.delete(rpcId);
1179
1373
  this._log.error(`RPC send failed: method=${method}, id=${rpcId}, error=${String(exc)}`, exc instanceof Error ? exc : void 0);
@@ -1192,12 +1386,22 @@ var RPCTransport = class {
1192
1386
  const elapsed = Date.now() - tStart;
1193
1387
  if (response.error !== void 0) {
1194
1388
  this._log.debug(`RPC error response: method=${method}, id=${rpcId}, elapsed=${elapsed}ms, error=${JSON.stringify(response.error)}`);
1389
+ if (traceId) {
1390
+ this._log.info(`[trace=${traceId}] rpc_recv method=${method} rpc_id=${rpcId} duration_ms=${elapsed} status=error`);
1391
+ }
1392
+ const respTrace2 = response._trace;
1393
+ if (respTrace2 && typeof respTrace2 === "object" && !Array.isArray(respTrace2)) {
1394
+ this._handleResponseTrace(method, "error", elapsed, respTrace2);
1395
+ }
1195
1396
  throw mapRemoteError(response.error);
1196
1397
  }
1197
1398
  if (response.result === void 0) {
1198
1399
  throw new SerializationError(`rpc response missing result and error: ${method}`);
1199
1400
  }
1200
1401
  this._log.debug(`RPC response ok: method=${method}, id=${rpcId}, elapsed=${elapsed}ms ${summarizeDict(response.result, DIAG_RESULT_FIELDS)}`);
1402
+ if (traceId) {
1403
+ this._log.info(`[trace=${traceId}] rpc_recv method=${method} rpc_id=${rpcId} duration_ms=${elapsed} status=ok`);
1404
+ }
1201
1405
  if (this._metaObserver !== null) {
1202
1406
  const meta = response._meta;
1203
1407
  if (isJsonObject(meta)) {
@@ -1208,6 +1412,10 @@ var RPCTransport = class {
1208
1412
  }
1209
1413
  }
1210
1414
  }
1415
+ const respTrace = response._trace;
1416
+ if (respTrace && typeof respTrace === "object" && !Array.isArray(respTrace)) {
1417
+ this._handleResponseTrace(method, "ok", elapsed, respTrace);
1418
+ }
1211
1419
  return response.result;
1212
1420
  } finally {
1213
1421
  if (timeoutHandle !== null) {
@@ -1215,6 +1423,26 @@ var RPCTransport = class {
1215
1423
  }
1216
1424
  }
1217
1425
  }
1426
+ /** 处理 RPC 响应中的 _trace 字段:追加 sdk.recv span,格式化输出,通知 observer */
1427
+ _handleResponseTrace(method, status, elapsedMs, respTrace) {
1428
+ try {
1429
+ const sdkRecvSpan = {
1430
+ node: "sdk",
1431
+ ts: Date.now(),
1432
+ action: "recv",
1433
+ ms: elapsedMs
1434
+ };
1435
+ const existingSpans = Array.isArray(respTrace.spans) ? respTrace.spans : [];
1436
+ const spans = [...existingSpans, sdkRecvSpan];
1437
+ const enriched = { ...respTrace, spans };
1438
+ this._log.info(traceDisplay(method, status, elapsedMs, respTrace, spans));
1439
+ if (this._traceObserver !== null) {
1440
+ this._traceObserver({ type: "rpc", method, trace: enriched, status, duration_ms: elapsedMs });
1441
+ }
1442
+ } catch (err) {
1443
+ this._log.debug(`trace handling raised: ${err instanceof Error ? err.message : String(err)}`);
1444
+ }
1445
+ }
1218
1446
  // ── 内部消息处理 ──────────────────────────────────
1219
1447
  _handleMessage(data) {
1220
1448
  try {
@@ -1262,7 +1490,22 @@ var RPCTransport = class {
1262
1490
  const protocolEvent = method.slice(6);
1263
1491
  const sdkEvent = EVENT_NAME_MAP[protocolEvent] ?? protocolEvent;
1264
1492
  this._log.debug(`event recv: event=${sdkEvent} ${summarizeDict(message.params, DIAG_RESULT_FIELDS)}`);
1265
- this._dispatcher.publish(`_raw.${sdkEvent}`, message.params ?? {});
1493
+ const params = message.params ?? {};
1494
+ if ("_trace" in params) {
1495
+ const eventTrace = params._trace;
1496
+ delete params._trace;
1497
+ if (eventTrace && typeof eventTrace === "object" && !Array.isArray(eventTrace)) {
1498
+ if (this._traceObserver !== null) {
1499
+ try {
1500
+ this._traceObserver({ type: "event", event: sdkEvent, trace: eventTrace });
1501
+ } catch {
1502
+ }
1503
+ }
1504
+ const traceObj = eventTrace;
1505
+ this._log.info(`[trace=${String(traceObj.trace_id ?? "")}] event_recv event=${sdkEvent}`);
1506
+ }
1507
+ }
1508
+ this._dispatcher.publish(`_raw.${sdkEvent}`, params);
1266
1509
  return;
1267
1510
  }
1268
1511
  this._log.debug(`notification recv: method=${method || "<no-method>"}`);
@@ -2024,15 +2267,18 @@ var _AuthFlow = class _AuthFlow {
2024
2267
  const msg = typeof event.data === "string" ? JSON.parse(event.data) : event.data;
2025
2268
  if (!receivedChallenge) {
2026
2269
  receivedChallenge = true;
2027
- ws.send(JSON.stringify({
2270
+ const requestPayload = JSON.stringify({
2028
2271
  jsonrpc: "2.0",
2029
2272
  id: `pre-${method}`,
2030
2273
  method,
2031
2274
  params
2032
- }));
2275
+ });
2276
+ this._log.debug(`short RPC request full: ${requestPayload}`);
2277
+ ws.send(requestPayload);
2033
2278
  return;
2034
2279
  }
2035
2280
  globalThis.clearTimeout(timeout);
2281
+ this._log.debug(`short RPC response full: method=${method} ${JSON.stringify(msg)}`);
2036
2282
  try {
2037
2283
  ws.close();
2038
2284
  } catch {
@@ -2834,8 +3080,7 @@ var SeqTracker = class {
2834
3080
  if (s < minSeq) minSeq = s;
2835
3081
  }
2836
3082
  if (minSeq === Infinity) return;
2837
- t.contiguousSeq = minSeq;
2838
- t.receivedSeqs.delete(minSeq);
3083
+ t.contiguousSeq = minSeq - 1;
2839
3084
  for (const [key, probe] of t.pendingGaps) {
2840
3085
  if (probe.gapEnd <= t.contiguousSeq) {
2841
3086
  t.pendingGaps.delete(key);
@@ -3070,6 +3315,59 @@ function extractCommonNameFromCertPem(certPem) {
3070
3315
  return "";
3071
3316
  }
3072
3317
  }
3318
+ function parseCertValidity(certPem) {
3319
+ try {
3320
+ const der = new Uint8Array(pemToArrayBuffer(certPem));
3321
+ const dates = [];
3322
+ for (let i = 0; i < der.length - 2 && dates.length < 2; i++) {
3323
+ const tag = der[i];
3324
+ if (tag !== 23 && tag !== 24) continue;
3325
+ const len = der[i + 1];
3326
+ if (len === 0 || len > 20 || i + 2 + len > der.length) continue;
3327
+ const str = new TextDecoder().decode(der.slice(i + 2, i + 2 + len));
3328
+ const ts = parseAsn1Time2(tag, str);
3329
+ if (ts !== null) {
3330
+ dates.push(ts);
3331
+ i += 1 + len;
3332
+ }
3333
+ }
3334
+ if (dates.length >= 2) {
3335
+ return { notBefore: dates[0], notAfter: dates[1] };
3336
+ }
3337
+ return null;
3338
+ } catch {
3339
+ return null;
3340
+ }
3341
+ }
3342
+ function parseAsn1Time2(tag, str) {
3343
+ try {
3344
+ if (tag === 23) {
3345
+ const cleaned = str.replace(/Z$/i, "");
3346
+ if (cleaned.length < 12) return null;
3347
+ let year = parseInt(cleaned.slice(0, 2), 10);
3348
+ year += year >= 50 ? 1900 : 2e3;
3349
+ const month = parseInt(cleaned.slice(2, 4), 10) - 1;
3350
+ const day = parseInt(cleaned.slice(4, 6), 10);
3351
+ const hour = parseInt(cleaned.slice(6, 8), 10);
3352
+ const min = parseInt(cleaned.slice(8, 10), 10);
3353
+ const sec = parseInt(cleaned.slice(10, 12), 10);
3354
+ return Date.UTC(year, month, day, hour, min, sec);
3355
+ } else if (tag === 24) {
3356
+ const cleaned = str.replace(/Z$/i, "");
3357
+ if (cleaned.length < 14) return null;
3358
+ const year = parseInt(cleaned.slice(0, 4), 10);
3359
+ const month = parseInt(cleaned.slice(4, 6), 10) - 1;
3360
+ const day = parseInt(cleaned.slice(6, 8), 10);
3361
+ const hour = parseInt(cleaned.slice(8, 10), 10);
3362
+ const min = parseInt(cleaned.slice(10, 12), 10);
3363
+ const sec = parseInt(cleaned.slice(12, 14), 10);
3364
+ return Date.UTC(year, month, day, hour, min, sec);
3365
+ }
3366
+ return null;
3367
+ } catch {
3368
+ return null;
3369
+ }
3370
+ }
3073
3371
  async function fetchWithTimeout(input, init, timeoutMs = AGENT_MD_HTTP_TIMEOUT_MS) {
3074
3372
  const controller = new AbortController();
3075
3373
  const timer = setTimeout(() => controller.abort(), timeoutMs);
@@ -3535,6 +3833,162 @@ var AuthNamespace = class {
3535
3833
  throw err;
3536
3834
  }
3537
3835
  }
3836
+ /**
3837
+ * 检查指定 AID 的本地和远程状态。
3838
+ * 与 Python SDK namespaces/auth_namespace.py:check_aid 对应。
3839
+ */
3840
+ async checkAid(params) {
3841
+ const tStart = Date.now();
3842
+ const aid = String(params?.aid ?? "").trim();
3843
+ if (!aid) throw new ValidationError("auth.check_aid requires 'aid'");
3844
+ this._log.debug(`checkAid enter: aid=${aid}`);
3845
+ try {
3846
+ const result = await this._checkLocalAid(aid);
3847
+ const local = result.local;
3848
+ if (!local?.complete) {
3849
+ const remote = await this._checkRemoteAidRegistration(aid);
3850
+ result.remote = remote;
3851
+ const remoteStatus = remote?.status;
3852
+ if (remoteStatus === "available") {
3853
+ result.status = "available";
3854
+ result.can_register = true;
3855
+ } else if (remoteStatus === "registered") {
3856
+ result.status = "registered_remote";
3857
+ result.can_register = false;
3858
+ } else {
3859
+ result.status = "unknown";
3860
+ result.can_register = false;
3861
+ }
3862
+ }
3863
+ this._log.debug(`checkAid exit: elapsed=${Date.now() - tStart}ms aid=${aid} status=${String(result.status)}`);
3864
+ return result;
3865
+ } catch (err) {
3866
+ this._log.debug(`checkAid exit (error): elapsed=${Date.now() - tStart}ms aid=${aid} err=${err instanceof Error ? err.message : String(err)}`);
3867
+ throw err;
3868
+ }
3869
+ }
3870
+ async _checkLocalAid(aid) {
3871
+ const client = this._internal;
3872
+ const ks = client._keystore;
3873
+ const identity = await client._auth.loadIdentityOrNone(aid);
3874
+ let keyPair = null;
3875
+ let keyError = "";
3876
+ try {
3877
+ if (ks && typeof ks.loadKeyPair === "function") {
3878
+ keyPair = await ks.loadKeyPair(aid);
3879
+ }
3880
+ } catch (e) {
3881
+ keyError = e instanceof Error ? e.message : String(e);
3882
+ }
3883
+ let certPem = null;
3884
+ let certError = "";
3885
+ try {
3886
+ if (ks && typeof ks.loadCert === "function") {
3887
+ certPem = await ks.loadCert(aid);
3888
+ }
3889
+ } catch (e) {
3890
+ certError = e instanceof Error ? e.message : String(e);
3891
+ }
3892
+ const privateKeyPresent = !!(keyPair && keyPair.private_key_pem);
3893
+ const publicKeyPresent = !!(keyPair && keyPair.public_key_der_b64);
3894
+ const certPresent = !!certPem;
3895
+ const certInfo = certPresent ? await this._inspectCertBrowser(aid, certPem) : { present: false, valid: false, expired: false };
3896
+ const certValid = !!certInfo.valid;
3897
+ const localComplete = privateKeyPresent && publicKeyPresent && certPresent && certValid;
3898
+ const issues = [];
3899
+ if (!identity) issues.push("local identity not found");
3900
+ if (!privateKeyPresent) issues.push("private key missing");
3901
+ if (!publicKeyPresent) issues.push("public key missing");
3902
+ if (!certPresent) {
3903
+ issues.push("certificate missing");
3904
+ } else if (certInfo.parse_error) {
3905
+ issues.push(`certificate invalid: ${certInfo.parse_error}`);
3906
+ } else if (certInfo.expired) {
3907
+ issues.push("certificate expired");
3908
+ } else if (!certValid) {
3909
+ issues.push("certificate not currently valid");
3910
+ }
3911
+ if (keyError) issues.push(`key load error: ${keyError}`);
3912
+ if (certError) issues.push(`certificate load error: ${certError}`);
3913
+ return {
3914
+ aid,
3915
+ status: localComplete ? "local_ready" : "local_incomplete",
3916
+ can_register: localComplete ? false : null,
3917
+ local: {
3918
+ exists: identity !== null,
3919
+ complete: localComplete,
3920
+ private_key: privateKeyPresent,
3921
+ public_key: publicKeyPresent,
3922
+ certificate: certInfo,
3923
+ issues
3924
+ },
3925
+ remote: {
3926
+ status: localComplete ? "not_checked" : "pending"
3927
+ }
3928
+ };
3929
+ }
3930
+ /** 浏览器环境证书检查(无 node:crypto X509Certificate) */
3931
+ async _inspectCertBrowser(aid, certPem) {
3932
+ const result = { present: true, valid: false, expired: false };
3933
+ try {
3934
+ const fingerprint = await certificateSha256Fingerprint(certPem);
3935
+ const cn = extractCommonNameFromCertPem(certPem);
3936
+ const aidMatches = !cn || cn === aid;
3937
+ const validity = parseCertValidity(certPem);
3938
+ if (validity) {
3939
+ const now = Date.now();
3940
+ const valid = now >= validity.notBefore && now <= validity.notAfter && aidMatches;
3941
+ const expired = now > validity.notAfter;
3942
+ result.valid = valid;
3943
+ result.expired = expired;
3944
+ result.not_before = new Date(validity.notBefore).toISOString();
3945
+ result.not_after = new Date(validity.notAfter).toISOString();
3946
+ result.expires_at = Math.floor(validity.notAfter / 1e3);
3947
+ result.seconds_until_expiry = Math.floor((validity.notAfter - now) / 1e3);
3948
+ } else {
3949
+ result.valid = aidMatches;
3950
+ }
3951
+ result.fingerprint = fingerprint;
3952
+ result.subject_cn = cn;
3953
+ result.aid_matches = aidMatches;
3954
+ if (cn && cn !== aid) {
3955
+ result.valid = false;
3956
+ result.parse_error = `certificate CN mismatch: ${cn}`;
3957
+ }
3958
+ } catch (e) {
3959
+ result.parse_error = e instanceof Error ? e.message : String(e);
3960
+ }
3961
+ return result;
3962
+ }
3963
+ async _checkRemoteAidRegistration(aid) {
3964
+ try {
3965
+ const content = await this.downloadAgentMd(aid);
3966
+ return {
3967
+ status: "registered",
3968
+ registered: true,
3969
+ available: false,
3970
+ source: "agent.md",
3971
+ agent_md_bytes: new TextEncoder().encode(content).length,
3972
+ agent_md_aid: extractAgentMDAid(content)
3973
+ };
3974
+ } catch (err) {
3975
+ if (err instanceof NotFoundError) {
3976
+ return {
3977
+ status: "available",
3978
+ registered: false,
3979
+ available: true,
3980
+ source: "agent.md"
3981
+ };
3982
+ }
3983
+ return {
3984
+ status: "unknown",
3985
+ registered: null,
3986
+ available: null,
3987
+ source: "agent.md",
3988
+ error: err instanceof Error ? err.message : String(err)
3989
+ };
3990
+ }
3991
+ }
3538
3992
  };
3539
3993
 
3540
3994
  // src/namespaces/custody.ts
@@ -6045,6 +6499,67 @@ var V2KeyStore = class _V2KeyStore {
6045
6499
  req.onerror = () => reject(req.error);
6046
6500
  });
6047
6501
  }
6502
+ // ---------- Group SPK ----------
6503
+ static _groupSpkKeyId(groupId, spkId) {
6504
+ return `${groupId}\0${spkId}`;
6505
+ }
6506
+ async saveGroupSPK(deviceId, groupId, spkId, priv, pubDer) {
6507
+ const record = {
6508
+ device_id: deviceId,
6509
+ key_type: "group_spk",
6510
+ key_id: _V2KeyStore._groupSpkKeyId(groupId, spkId),
6511
+ private_key: priv,
6512
+ public_key: pubDer,
6513
+ created_at: Date.now()
6514
+ };
6515
+ return new Promise((resolve, reject) => {
6516
+ const req = this.store("readwrite").put(record);
6517
+ req.onsuccess = () => resolve();
6518
+ req.onerror = () => reject(req.error);
6519
+ });
6520
+ }
6521
+ async loadGroupSPK(deviceId, groupId, spkId) {
6522
+ const keyId = _V2KeyStore._groupSpkKeyId(groupId, spkId);
6523
+ return new Promise((resolve, reject) => {
6524
+ const req = this.store("readonly").get([deviceId, "group_spk", keyId]);
6525
+ req.onsuccess = () => {
6526
+ const r = req.result;
6527
+ resolve(r ? new Uint8Array(r.private_key) : null);
6528
+ };
6529
+ req.onerror = () => reject(req.error);
6530
+ });
6531
+ }
6532
+ /** 取指定群最新 group SPK(按 created_at DESC,key_id 前缀匹配)。 */
6533
+ async loadCurrentGroupSPK(deviceId, groupId) {
6534
+ const prefix = `${groupId}\0`;
6535
+ return new Promise((resolve, reject) => {
6536
+ const idx = this.store("readonly").index(V2_INDEX_BY_DEVICE_TYPE_CREATED);
6537
+ const range = IDBKeyRange.bound(
6538
+ [deviceId, "group_spk", -Infinity],
6539
+ [deviceId, "group_spk", Infinity]
6540
+ );
6541
+ const req = idx.openCursor(range, "prev");
6542
+ req.onsuccess = () => {
6543
+ const cursor = req.result;
6544
+ if (!cursor) {
6545
+ resolve(null);
6546
+ return;
6547
+ }
6548
+ 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
+ }
6559
+ };
6560
+ req.onerror = () => reject(req.error);
6561
+ });
6562
+ }
6048
6563
  // ---------- IK ----------
6049
6564
  async saveIK(deviceId, priv, pubDer) {
6050
6565
  const record = {
@@ -8713,7 +9228,7 @@ async function computeRecipientsDigest(rows) {
8713
9228
 
8714
9229
  // src/v2/session/session.ts
8715
9230
  var PEER_KEY_CACHE_TTL_MS = 60 * 60 * 1e3;
8716
- var DESTROY_DELAY_MS = 7 * 60 * 60 * 1e3;
9231
+ var DESTROY_DELAY_MS = 7 * 24 * 60 * 60 * 1e3;
8717
9232
  var RECENT_GENERATIONS = 7;
8718
9233
  var HARD_LIMIT_MS = 180 * 24 * 60 * 60 * 1e3;
8719
9234
  async function sha256Hex(data) {
@@ -8750,6 +9265,8 @@ var V2Session = class {
8750
9265
  __publicField(this, "_spkPriv");
8751
9266
  __publicField(this, "_spkPubDer");
8752
9267
  __publicField(this, "_registered", false);
9268
+ __publicField(this, "_lastUploadedSPKId", "");
9269
+ __publicField(this, "_lastUploadedGroupSPKIds", /* @__PURE__ */ new Map());
8753
9270
  __publicField(this, "_peerIKCache", /* @__PURE__ */ new Map());
8754
9271
  __publicField(this, "_verifiedSPKs", /* @__PURE__ */ new Set());
8755
9272
  __publicField(this, "_oldSPKMaxSeq", /* @__PURE__ */ new Map());
@@ -8829,6 +9346,7 @@ var V2Session = class {
8829
9346
  await this.ensureKeys();
8830
9347
  await this._registerSPK(callFn);
8831
9348
  this._registered = true;
9349
+ this._lastUploadedSPKId = this._spkId;
8832
9350
  }
8833
9351
  /** 返回加密所需的 sender 结构。 */
8834
9352
  async getSenderIdentity() {
@@ -8868,11 +9386,11 @@ var V2Session = class {
8868
9386
  }
8869
9387
  }
8870
9388
  /**
8871
- * contig_seq 已覆盖、超过 7h 安全窗口、且不在最近 7 代保留窗口内时销毁。
9389
+ * contig_seq 已覆盖、超过 7 天安全窗口、且不在最近 7 代保留窗口内时销毁。
8872
9390
  *
8873
9391
  * 销毁条件(全部满足才销毁):
8874
9392
  * - contig_seq >= 该 SPK 引用的最大 seq
8875
- * - 自最后一次见到该 spk_id 引用 >= 7 小时
9393
+ * - 自最后一次见到该 spk_id 引用 >= 7
8876
9394
  * - 不在最近 7 代 SPK 保留窗口内
8877
9395
  */
8878
9396
  async maybeDestroyOldSPKs(contigSeq) {
@@ -8920,6 +9438,76 @@ var V2Session = class {
8920
9438
  async rotateSPK(callFn) {
8921
9439
  await this._generateNewSPK();
8922
9440
  await this._registerSPK(callFn);
9441
+ this._lastUploadedSPKId = this._spkId;
9442
+ }
9443
+ /** 判断 spkId 是否为本进程最后一次成功上传的 P2P SPK。 */
9444
+ isLastUploadedSPK(spkId) {
9445
+ return Boolean(spkId) && spkId === this._lastUploadedSPKId;
9446
+ }
9447
+ /** 判断 spkId 是否为本进程在该群最后一次成功上传的 group SPK。 */
9448
+ isLastUploadedGroupSPK(groupId, spkId) {
9449
+ if (!spkId) return false;
9450
+ const gk = (groupId || "").trim();
9451
+ return this._lastUploadedGroupSPKIds.get(gk) === spkId;
9452
+ }
9453
+ // ---------- Group SPK ----------
9454
+ /** 确保指定群有独立 group SPK,返回 { spkId, priv, pubDer }。 */
9455
+ async ensureGroupSPK(groupId) {
9456
+ await this.ensureKeys();
9457
+ const gk = (groupId || "").trim();
9458
+ const existing = await this._store.loadCurrentGroupSPK(this._deviceId, gk);
9459
+ if (existing) return existing;
9460
+ const [priv, pubDer] = await generateP256Keypair();
9461
+ const hex = await sha256Hex(pubDer);
9462
+ const spkId = `sha256:${hex.substring(0, 16)}`;
9463
+ await this._store.saveGroupSPK(this._deviceId, gk, spkId, priv, pubDer);
9464
+ return { spkId, priv, pubDer };
9465
+ }
9466
+ /** 注册指定群的 group SPK。group 服务负责成员鉴权。 */
9467
+ async ensureGroupRegistered(groupId, callFn) {
9468
+ await this.ensureKeys();
9469
+ const gk = (groupId || "").trim();
9470
+ const { spkId, pubDer } = await this.ensureGroupSPK(gk);
9471
+ await this._publishGroupSPK(gk, spkId, pubDer, callFn);
9472
+ }
9473
+ /** 轮换指定群的 group SPK,保留旧私钥用于缓存窗口内的历史 wrap 解密。 */
9474
+ async rotateGroupSPK(groupId, callFn) {
9475
+ await this.ensureKeys();
9476
+ const gk = (groupId || "").trim();
9477
+ const [priv, pubDer] = await generateP256Keypair();
9478
+ const hex = await sha256Hex(pubDer);
9479
+ const spkId = `sha256:${hex.substring(0, 16)}`;
9480
+ await this._store.saveGroupSPK(this._deviceId, gk, spkId, priv, pubDer);
9481
+ await this._publishGroupSPK(gk, spkId, pubDer, callFn);
9482
+ return { spkId, priv, pubDer };
9483
+ }
9484
+ /** 群消息解密优先查 group SPK;找不到时 fallback 旧 P2P SPK 兼容历史消息。 */
9485
+ async getGroupDecryptKeys(groupId, spkId) {
9486
+ await this.ensureKeys();
9487
+ const gk = (groupId || "").trim();
9488
+ if (!spkId) return { ikPriv: this._ikPriv };
9489
+ const groupSpk = await this._store.loadGroupSPK(this._deviceId, gk, spkId);
9490
+ if (groupSpk) return { ikPriv: this._ikPriv, spkPriv: groupSpk };
9491
+ return this.getDecryptKeys(spkId);
9492
+ }
9493
+ async _publishGroupSPK(groupId, spkId, spkPubDer, callFn) {
9494
+ const spkTimestamp = Math.floor(this._nowFn() / 1e3);
9495
+ const enc = new TextEncoder();
9496
+ const signData = concatBytes3(
9497
+ spkPubDer,
9498
+ enc.encode(spkId),
9499
+ enc.encode(String(spkTimestamp))
9500
+ );
9501
+ const signature = await ecdsaSignRaw(this._ikPriv, signData);
9502
+ await callFn("group.v2.put_group_pk", {
9503
+ group_id: groupId,
9504
+ key_source: "group_device_prekey",
9505
+ spk_id: spkId,
9506
+ spk_pk: bytesToBase64(spkPubDer),
9507
+ spk_signature: bytesToBase64(signature),
9508
+ spk_timestamp: spkTimestamp
9509
+ });
9510
+ this._lastUploadedGroupSPKIds.set(groupId, spkId);
8923
9511
  }
8924
9512
  cachePeerIK(peerAid, deviceId, ikPubDer) {
8925
9513
  this._peerIKCache.set(`${peerAid}#${deviceId}`, {
@@ -10522,6 +11110,12 @@ var _AUNClient = class _AUNClient {
10522
11110
  } catch (exc) {
10523
11111
  this._clientLog.debug(`V2 post-membership propose failed (non-fatal): group=${groupId} err=${formatCaughtError(exc)}`);
10524
11112
  }
11113
+ if (method === "group.create" || method === "group.use_invite_code") {
11114
+ const callFn = async (m, ps) => this.call(m, ps);
11115
+ this._v2Session.ensureGroupRegistered?.(groupId, callFn)?.catch((exc) => {
11116
+ this._clientLog.debug(`group SPK registration after ${method} failed (non-fatal): group=${groupId} err=${exc}`);
11117
+ });
11118
+ }
10525
11119
  }
10526
11120
  }
10527
11121
  if (method === "message.pull" && isJsonObject(result)) {
@@ -11175,6 +11769,29 @@ var _AUNClient = class _AUNClient {
11175
11769
  if (groupId) {
11176
11770
  this._v2BootstrapCache.delete(`group:${groupId}`);
11177
11771
  }
11772
+ 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
+ if (membershipActions.has(action)) {
11783
+ const callFn = async (method, params) => this.call(method, params);
11784
+ if (action === "joined" || action === "join_approved") {
11785
+ this._v2Session.ensureGroupRegistered?.(groupId, callFn)?.catch((exc) => {
11786
+ this._clientLog.debug(`group SPK registration failed (non-fatal): group=${groupId} action=${action} err=${exc}`);
11787
+ });
11788
+ } else {
11789
+ this._v2Session.rotateGroupSPK?.(groupId, callFn)?.catch((exc) => {
11790
+ this._clientLog.debug(`group SPK rotation failed (non-fatal): group=${groupId} action=${action} err=${exc}`);
11791
+ });
11792
+ }
11793
+ }
11794
+ }
11178
11795
  if (groupId && action === "upsert" && this._v2Session) {
11179
11796
  this._safeAsync(this._v2AutoProposeState(groupId, { leaderDelay: true }));
11180
11797
  }
@@ -12963,12 +13580,17 @@ var _AUNClient = class _AUNClient {
12963
13580
  if (seq <= 0) return { acked: 0 };
12964
13581
  const raw = await this.call("message.v2.ack", { up_to_seq: seq });
12965
13582
  const result = isJsonObject(raw) ? { ...raw } : { result: raw };
12966
- result.ack_seq = seq;
13583
+ let actualAckSeq = seq;
13584
+ if ("effective_ack_seq" in result) actualAckSeq = Number(result.effective_ack_seq ?? 0);
13585
+ else if ("ack_seq" in result) actualAckSeq = Number(result.ack_seq ?? 0);
13586
+ else if ("cursor" in result) actualAckSeq = Number(result.cursor ?? 0);
13587
+ if (!Number.isFinite(actualAckSeq)) actualAckSeq = seq;
13588
+ result.ack_seq = actualAckSeq;
12967
13589
  result.success = true;
12968
- if (Number(result.acked ?? 0) === 0) result.acked = seq;
13590
+ if (Number(result.acked ?? 0) === 0) result.acked = actualAckSeq;
12969
13591
  if (this._v2Session) {
12970
13592
  try {
12971
- const destroyed = await this._v2Session.maybeDestroyOldSPKs(seq);
13593
+ const destroyed = await this._v2Session.maybeDestroyOldSPKs(actualAckSeq);
12972
13594
  if (destroyed.length > 0) {
12973
13595
  this._clientLog.info(`V2 destroyed old SPKs after ack: ${destroyed.slice(0, 3)} (PFS)`);
12974
13596
  }
@@ -12992,15 +13614,44 @@ var _AUNClient = class _AUNClient {
12992
13614
  return null;
12993
13615
  }
12994
13616
  let spkId = "";
13617
+ let recipientKeySource = "";
12995
13618
  const recipientObj = envelope.recipient;
12996
13619
  if (recipientObj && typeof recipientObj === "object") {
12997
13620
  spkId = String(recipientObj.spk_id ?? "");
13621
+ recipientKeySource = String(recipientObj.key_source ?? "");
12998
13622
  } else if (Array.isArray(envelope.recipients)) {
12999
13623
  spkId = String(msg.spk_id ?? "");
13624
+ if (!spkId) {
13625
+ for (const row of envelope.recipients) {
13626
+ if (Array.isArray(row) && row.length >= 6 && row[0] === this._aid && row[1] === this._deviceId) {
13627
+ spkId = String(row[5] ?? "");
13628
+ recipientKeySource = row.length > 3 ? String(row[3] ?? "") : "";
13629
+ break;
13630
+ }
13631
+ }
13632
+ } else {
13633
+ for (const row of envelope.recipients) {
13634
+ if (Array.isArray(row) && row.length >= 6 && row[0] === this._aid && row[1] === this._deviceId) {
13635
+ recipientKeySource = row.length > 3 ? String(row[3] ?? "") : "";
13636
+ break;
13637
+ }
13638
+ }
13639
+ }
13640
+ }
13641
+ const aad = isJsonObject(envelope.aad) ? envelope.aad : {};
13642
+ const groupIdForKeys = String(msg.group_id ?? aad.group_id ?? envelope.group_id ?? "").trim();
13643
+ let ikPriv;
13644
+ 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;
13000
13653
  }
13001
- const { ikPriv, spkPriv } = await session.getDecryptKeys(spkId);
13002
13654
  const fromAid = String(msg.from_aid ?? "");
13003
- const aad = envelope.aad ?? {};
13004
13655
  const senderDeviceId = String(aad.from_device ?? "");
13005
13656
  const senderPubDer = await this._getV2SenderPubDer(fromAid, senderDeviceId);
13006
13657
  if (!senderPubDer) {
@@ -13040,14 +13691,20 @@ var _AUNClient = class _AUNClient {
13040
13691
  return null;
13041
13692
  }
13042
13693
  if (plaintext == null) return null;
13043
- if (session.isCurrentSPK(spkId)) {
13044
- queueMicrotask(() => {
13045
- const callFn = async (method, params) => {
13046
- return this.call(method, params);
13047
- };
13048
- session.rotateSPK(callFn).catch((exc) => {
13049
- this._clientLog.warn(`V2 SPK rotation failed (non-fatal): ${String(exc)}`);
13050
- });
13694
+ if (groupIdForKeys && recipientKeySource === "group_device_prekey" && session.isLastUploadedGroupSPK(groupIdForKeys, spkId)) {
13695
+ const callFn = async (method, params) => this.call(method, params);
13696
+ session.rotateGroupSPK(groupIdForKeys, callFn).catch((exc) => {
13697
+ this._clientLog.debug(`V2 group SPK rotation failed (non-fatal): group=${groupIdForKeys} err=${exc}`);
13698
+ });
13699
+ } else if (groupIdForKeys && recipientKeySource === "peer_device_prekey") {
13700
+ const callFn = async (method, params) => this.call(method, params);
13701
+ session.ensureGroupRegistered(groupIdForKeys, callFn).catch((exc) => {
13702
+ this._clientLog.debug(`V2 group SPK registration after peer fallback failed (non-fatal): group=${groupIdForKeys} err=${exc}`);
13703
+ });
13704
+ } else if (!groupIdForKeys && session.isLastUploadedSPK(spkId)) {
13705
+ const callFn = async (method, params) => this.call(method, params);
13706
+ session.rotateSPK(callFn).catch((exc) => {
13707
+ this._clientLog.debug(`V2 SPK rotation failed (non-fatal): ${exc}`);
13051
13708
  });
13052
13709
  }
13053
13710
  return {
@@ -13563,19 +14220,32 @@ var _AUNClient = class _AUNClient {
13563
14220
  const session = this._v2Session;
13564
14221
  if (!session || !opts.envelope) return null;
13565
14222
  let spkId = "";
14223
+ let recipientKeySource = "";
13566
14224
  const recipients = opts.envelope.recipients;
13567
14225
  if (Array.isArray(recipients)) {
13568
14226
  for (const row of recipients) {
13569
14227
  if (Array.isArray(row) && row.length >= 6) {
13570
14228
  if (row[0] === this._aid && row[1] === this._deviceId) {
13571
14229
  spkId = String(row[5] ?? "");
14230
+ recipientKeySource = row.length > 3 ? String(row[3] ?? "") : "";
13572
14231
  break;
13573
14232
  }
13574
14233
  }
13575
14234
  }
13576
14235
  }
13577
- const { ikPriv, spkPriv } = await session.getDecryptKeys(spkId);
13578
14236
  const aad = opts.envelope.aad ?? {};
14237
+ const groupIdForKeys = String(aad.group_id ?? opts.envelope.group_id ?? "").trim();
14238
+ let ikPriv;
14239
+ 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;
14248
+ }
13579
14249
  const fromAid = String(opts.fromAid || aad.from || "").trim();
13580
14250
  const senderDeviceId = String(aad.from_device ?? "");
13581
14251
  const senderPubDer = await this._getV2SenderPubDer(fromAid, senderDeviceId);
@@ -13584,7 +14254,7 @@ var _AUNClient = class _AUNClient {
13584
14254
  return null;
13585
14255
  }
13586
14256
  try {
13587
- return await decryptMessage(
14257
+ const plaintext = await decryptMessage(
13588
14258
  opts.envelope,
13589
14259
  this._aid ?? "",
13590
14260
  this._deviceId,
@@ -13592,6 +14262,20 @@ var _AUNClient = class _AUNClient {
13592
14262
  spkPriv,
13593
14263
  senderPubDer
13594
14264
  );
14265
+ if (plaintext != null) {
14266
+ if (groupIdForKeys && recipientKeySource === "group_device_prekey" && session.isLastUploadedGroupSPK(groupIdForKeys, spkId)) {
14267
+ const callFn = async (method, params) => this.call(method, params);
14268
+ session.rotateGroupSPK(groupIdForKeys, callFn).catch((exc) => {
14269
+ this._clientLog.debug(`V2 thought group SPK rotation failed (non-fatal): group=${groupIdForKeys} err=${exc}`);
14270
+ });
14271
+ } else if (groupIdForKeys && recipientKeySource === "peer_device_prekey") {
14272
+ const callFn = async (method, params) => this.call(method, params);
14273
+ session.ensureGroupRegistered(groupIdForKeys, callFn).catch((exc) => {
14274
+ this._clientLog.debug(`V2 thought group SPK registration after peer fallback failed (non-fatal): group=${groupIdForKeys} err=${exc}`);
14275
+ });
14276
+ }
14277
+ }
14278
+ return plaintext;
13595
14279
  } catch (exc) {
13596
14280
  this._clientLog.warn(`V2 thought decrypt failed from=${fromAid}: ${String(exc)}`);
13597
14281
  return null;
@@ -14097,21 +14781,74 @@ var _AUNClient = class _AUNClient {
14097
14781
  }
14098
14782
  async _onV2PushNotification(data) {
14099
14783
  if (!this._v2Session) return;
14784
+ const pushSeq = isJsonObject(data) ? Number(data.seq ?? 0) || 0 : 0;
14785
+ const pushFrom = isJsonObject(data) ? String(data.from_aid ?? "") : "";
14786
+ const pushMsgId = isJsonObject(data) ? String(data.message_id ?? "") : "";
14787
+ const envelopeJson = isJsonObject(data) ? data.envelope_json : void 0;
14788
+ const ns = this._aid ? `p2p:${this._aid}` : "";
14789
+ let contigBefore = ns ? this._seqTracker.getContiguousSeq(ns) : 0;
14790
+ this._clientLog.debug(
14791
+ `_onV2PushNotification: push_seq=${pushSeq || "null"} push_from=${pushFrom} push_msg_id=${pushMsgId} has_payload=${!!envelopeJson} contiguous_seq=${contigBefore}`
14792
+ );
14793
+ if (envelopeJson && pushSeq > 0 && ns) {
14794
+ try {
14795
+ const decrypted = await this._decryptV2Message(data);
14796
+ 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);
14802
+ const newContig = this._seqTracker.getContiguousSeq(ns);
14803
+ if (newContig !== contigBefore) {
14804
+ this._saveSeqTrackerState();
14805
+ }
14806
+ 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}`));
14808
+ }
14809
+ this._clientLog.debug(
14810
+ `_onV2PushNotification: push \u5E26 payload \u89E3\u5BC6\u6210\u529F, contiguous_seq=${contigBefore}->${newContig} push_seq=${pushSeq}`
14811
+ );
14812
+ return;
14813
+ }
14814
+ } catch (exc) {
14815
+ this._clientLog.debug(`_onV2PushNotification: push payload \u89E3\u5BC6\u5931\u8D25, fallback to pull: ${exc}`);
14816
+ }
14817
+ }
14818
+ 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
+ this._clientLog.debug(
14828
+ `_onV2PushNotification: \u7EAF\u901A\u77E5 push_seq=${pushSeq} > contiguous_seq=${contigBefore}, \u89E6\u53D1 pull(after_seq=${contigBefore})`
14829
+ );
14830
+ }
14100
14831
  if (this._v2PullInflight) {
14101
14832
  this._v2PullPending = true;
14102
14833
  return;
14103
14834
  }
14104
14835
  this._v2PullInflight = true;
14105
- const ns = this._aid ? `p2p:${this._aid}` : "";
14106
14836
  const dedupKey = `p2p_pull:${ns}`;
14107
14837
  this._gapFillDone.add(dedupKey);
14108
14838
  try {
14109
14839
  do {
14110
14840
  this._v2PullPending = false;
14111
14841
  await this.pullV2();
14842
+ const newContig = ns ? this._seqTracker.getContiguousSeq(ns) : -1;
14843
+ this._clientLog.debug(
14844
+ `_onV2PushNotification pull done: contiguous_seq=${contigBefore}->${newContig} (push_seq=${pushSeq || "null"})`
14845
+ );
14112
14846
  } while (this._v2PullPending);
14113
14847
  } catch (exc) {
14114
- this._clientLog.warn(`V2 push auto-pull failed: ${exc}`);
14848
+ const newContig = ns ? this._seqTracker.getContiguousSeq(ns) : -1;
14849
+ this._clientLog.warn(
14850
+ `V2 push auto-pull failed: contiguous_seq=${contigBefore}->${newContig} err=${exc}`
14851
+ );
14115
14852
  } finally {
14116
14853
  this._v2PullInflight = false;
14117
14854
  this._gapFillDone.delete(dedupKey);