@agentunion/fastaun-browser 0.3.0 → 0.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +16 -0
- package/README.md +0 -1
- package/_packed_docs/CHANGELOG.md +16 -0
- package/_packed_docs/design/2026-05-22-aun-rpc-trace-enhancement.md +542 -0
- package/_packed_docs/protocol/06-/346/234/215/345/212/241/345/215/217/350/256/256.md +1 -24
- package/_packed_docs/sdk/06-API/346/211/213/345/206/214.md +41 -0
- package/_packed_docs/sdk/09-message-rpc-manual.md +30 -67
- package/dist/auth.d.ts.map +1 -1
- package/dist/auth.js +5 -2
- package/dist/auth.js.map +1 -1
- package/dist/bundle.js +775 -33
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js +185 -25
- package/dist/client.js.map +1 -1
- package/dist/namespaces/auth.d.ts +9 -0
- package/dist/namespaces/auth.d.ts.map +1 -1
- package/dist/namespaces/auth.js +243 -0
- package/dist/namespaces/auth.js.map +1 -1
- package/dist/seq-tracker.d.ts.map +1 -1
- package/dist/seq-tracker.js +2 -3
- package/dist/seq-tracker.js.map +1 -1
- package/dist/transport.d.ts +9 -1
- package/dist/transport.d.ts.map +1 -1
- package/dist/transport.js +262 -10
- package/dist/transport.js.map +1 -1
- package/dist/v2/session/keystore.d.ts +9 -0
- package/dist/v2/session/keystore.d.ts.map +1 -1
- package/dist/v2/session/keystore.js +60 -0
- package/dist/v2/session/keystore.js.map +1 -1
- package/dist/v2/session/session.d.ts +25 -2
- package/dist/v2/session/session.d.ts.map +1 -1
- package/dist/v2/session/session.js +77 -3
- package/dist/v2/session/session.js.map +1 -1
- package/package.json +1 -1
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(
|
|
1171
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 已覆盖、超过
|
|
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
|
-
|
|
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 =
|
|
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(
|
|
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.
|
|
13044
|
-
|
|
13045
|
-
|
|
13046
|
-
|
|
13047
|
-
|
|
13048
|
-
|
|
13049
|
-
|
|
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
|
-
|
|
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,79 @@ 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
|
+
const 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 (pushSeq <= contigBefore) {
|
|
14820
|
+
this._clientLog.debug(
|
|
14821
|
+
`_onV2PushNotification: push_seq=${pushSeq} <= contiguous_seq=${contigBefore}, \u56DE\u64AD\u6709\u5E8F\u961F\u5217`
|
|
14822
|
+
);
|
|
14823
|
+
try {
|
|
14824
|
+
await this._drainOrderedMessages(ns);
|
|
14825
|
+
} catch (exc) {
|
|
14826
|
+
this._clientLog.warn(`V2 push drain ordered messages failed: ${exc}`);
|
|
14827
|
+
}
|
|
14828
|
+
return;
|
|
14829
|
+
} else {
|
|
14830
|
+
this._seqTracker.onMessageSeq(ns, pushSeq);
|
|
14831
|
+
this._clientLog.debug(
|
|
14832
|
+
`_onV2PushNotification: \u7EAF\u901A\u77E5 push_seq=${pushSeq} > contiguous_seq=${contigBefore}, \u6807\u8BB0\u4E0A\u754C(seq-1=${pushSeq - 1}) \u89E6\u53D1 pull`
|
|
14833
|
+
);
|
|
14834
|
+
}
|
|
14835
|
+
}
|
|
14100
14836
|
if (this._v2PullInflight) {
|
|
14101
14837
|
this._v2PullPending = true;
|
|
14102
14838
|
return;
|
|
14103
14839
|
}
|
|
14104
14840
|
this._v2PullInflight = true;
|
|
14105
|
-
const ns = this._aid ? `p2p:${this._aid}` : "";
|
|
14106
14841
|
const dedupKey = `p2p_pull:${ns}`;
|
|
14107
14842
|
this._gapFillDone.add(dedupKey);
|
|
14108
14843
|
try {
|
|
14109
14844
|
do {
|
|
14110
14845
|
this._v2PullPending = false;
|
|
14111
14846
|
await this.pullV2();
|
|
14847
|
+
const newContig = ns ? this._seqTracker.getContiguousSeq(ns) : -1;
|
|
14848
|
+
this._clientLog.debug(
|
|
14849
|
+
`_onV2PushNotification pull done: contiguous_seq=${contigBefore}->${newContig} (push_seq=${pushSeq || "null"})`
|
|
14850
|
+
);
|
|
14112
14851
|
} while (this._v2PullPending);
|
|
14113
14852
|
} catch (exc) {
|
|
14114
|
-
this.
|
|
14853
|
+
const newContig = ns ? this._seqTracker.getContiguousSeq(ns) : -1;
|
|
14854
|
+
this._clientLog.warn(
|
|
14855
|
+
`V2 push auto-pull failed: contiguous_seq=${contigBefore}->${newContig} err=${exc}`
|
|
14856
|
+
);
|
|
14115
14857
|
} finally {
|
|
14116
14858
|
this._v2PullInflight = false;
|
|
14117
14859
|
this._gapFillDone.delete(dedupKey);
|