@agentunion/fastaun-browser 0.3.5 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +14 -0
- package/_packed_docs/AUN_SDK_/351/207/215/346/236/204/345/256/236/346/226/275/350/256/241/345/210/222.md +596 -0
- package/_packed_docs/AUN_SDK_/351/207/215/346/236/204/350/256/276/350/256/241/346/226/271/346/241/210_v3.md +1633 -0
- package/_packed_docs/CHANGELOG.md +14 -0
- package/_packed_docs/INDEX.md +17 -11
- package/_packed_docs/KITE_DOCS_GUIDE.md +11 -10
- package/_packed_docs/sdk/01-/345/277/253/351/200/237/345/274/200/345/247/213.md +134 -158
- package/_packed_docs/sdk/02-WebSocket/345/215/217/350/256/256.md +11 -7
- package/_packed_docs/sdk/03-/346/240/270/345/277/203/346/246/202/345/277/265.md +98 -119
- package/_packed_docs/sdk/04-/350/277/236/346/216/245/344/270/216/350/256/244/350/257/201.md +147 -374
- package/_packed_docs/sdk/05-E2EE/345/212/240/345/257/206/351/200/232/344/277/241.md +153 -153
- package/_packed_docs/sdk/06-API/346/211/213/345/206/214.md +163 -1364
- package/_packed_docs/sdk/07-/351/224/231/350/257/257/345/244/204/347/220/206.md +71 -91
- package/_packed_docs/sdk/08-/346/234/200/344/275/263/345/256/236/350/267/265.md +76 -63
- package/_packed_docs/sdk/09-custody-api-manual.md +7 -6
- package/_packed_docs/sdk/09-meta-rpc-manual.md +13 -14
- package/_packed_docs/sdk/09-storage-rpc-manual.md +89 -0
- package/_packed_docs/sdk/AUN_DOCS_GUIDE.md +37 -49
- package/_packed_docs/sdk/INDEX.md +72 -98
- package/_packed_docs/sdk/README.md +85 -266
- package/dist/aid-store.d.ts +64 -0
- package/dist/aid-store.d.ts.map +1 -0
- package/dist/aid-store.js +855 -0
- package/dist/aid-store.js.map +1 -0
- package/dist/aid.d.ts +50 -0
- package/dist/aid.d.ts.map +1 -0
- package/dist/aid.js +106 -0
- package/dist/aid.js.map +1 -0
- package/dist/auth.d.ts +17 -1
- package/dist/auth.d.ts.map +1 -1
- package/dist/auth.js +27 -4
- package/dist/auth.js.map +1 -1
- package/dist/bundle.js +1981 -2048
- package/dist/cert-utils.d.ts +26 -0
- package/dist/cert-utils.d.ts.map +1 -0
- package/dist/cert-utils.js +221 -0
- package/dist/cert-utils.js.map +1 -0
- package/dist/client.d.ts +93 -58
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js +775 -170
- package/dist/client.js.map +1 -1
- package/dist/error-codes.d.ts +25 -0
- package/dist/error-codes.d.ts.map +1 -0
- package/dist/error-codes.js +31 -0
- package/dist/error-codes.js.map +1 -0
- package/dist/errors.d.ts +4 -0
- package/dist/errors.d.ts.map +1 -1
- package/dist/errors.js +4 -0
- package/dist/errors.js.map +1 -1
- package/dist/index.d.ts +6 -6
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +5 -5
- package/dist/index.js.map +1 -1
- package/dist/keystore/index.d.ts +1 -1
- package/dist/keystore/index.d.ts.map +1 -1
- package/dist/result.d.ts +19 -0
- package/dist/result.d.ts.map +1 -0
- package/dist/result.js +10 -0
- package/dist/result.js.map +1 -0
- package/dist/transport.d.ts +3 -0
- package/dist/transport.d.ts.map +1 -1
- package/dist/transport.js +17 -2
- package/dist/transport.js.map +1 -1
- package/dist/types.d.ts +13 -2
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js +22 -0
- package/dist/types.js.map +1 -1
- package/dist/v2/e2ee/encrypt-p2p.js +1 -1
- package/dist/v2/e2ee/encrypt-p2p.js.map +1 -1
- package/dist/version.d.ts +2 -0
- package/dist/version.d.ts.map +1 -0
- package/dist/version.js +5 -0
- package/dist/version.js.map +1 -0
- package/package.json +1 -1
package/dist/bundle.js
CHANGED
|
@@ -225,10 +225,10 @@ var init_crypto = __esm({
|
|
|
225
225
|
const pkcs8 = await crypto.subtle.exportKey("pkcs8", keyPair.privateKey);
|
|
226
226
|
const privateKeyPem = arrayBufferToPem(pkcs8, "PRIVATE KEY");
|
|
227
227
|
const spki = await crypto.subtle.exportKey("spki", keyPair.publicKey);
|
|
228
|
-
const
|
|
228
|
+
const publicKeyDerB642 = uint8ToBase64(new Uint8Array(spki));
|
|
229
229
|
return {
|
|
230
230
|
private_key_pem: privateKeyPem,
|
|
231
|
-
public_key_der_b64:
|
|
231
|
+
public_key_der_b64: publicKeyDerB642,
|
|
232
232
|
curve: _CryptoProvider.curveName
|
|
233
233
|
};
|
|
234
234
|
}
|
|
@@ -314,12 +314,12 @@ async function secretPut(storeName, key, value) {
|
|
|
314
314
|
tx.oncomplete = () => db.close();
|
|
315
315
|
});
|
|
316
316
|
}
|
|
317
|
-
var
|
|
317
|
+
var _noopLog6, SECRET_DB_NAME, SECRET_DB_VERSION, STORE_SECRETS, STORE_MASTER, IndexedDBSecretStore;
|
|
318
318
|
var init_indexeddb_store = __esm({
|
|
319
319
|
"src/secret-store/indexeddb-store.ts"() {
|
|
320
320
|
"use strict";
|
|
321
321
|
init_crypto();
|
|
322
|
-
|
|
322
|
+
_noopLog6 = { error: () => {
|
|
323
323
|
}, warn: () => {
|
|
324
324
|
}, info: () => {
|
|
325
325
|
}, debug: () => {
|
|
@@ -332,7 +332,7 @@ var init_indexeddb_store = __esm({
|
|
|
332
332
|
constructor(encryptionSeed) {
|
|
333
333
|
__publicField(this, "_encryptionSeed");
|
|
334
334
|
__publicField(this, "_masterKeyPromise", null);
|
|
335
|
-
__publicField(this, "_log",
|
|
335
|
+
__publicField(this, "_log", _noopLog6);
|
|
336
336
|
this._encryptionSeed = encryptionSeed;
|
|
337
337
|
}
|
|
338
338
|
setLogger(log) {
|
|
@@ -453,7 +453,31 @@ var init_indexeddb_store = __esm({
|
|
|
453
453
|
}
|
|
454
454
|
});
|
|
455
455
|
|
|
456
|
+
// src/version.ts
|
|
457
|
+
var VERSION = "0.4.0";
|
|
458
|
+
|
|
456
459
|
// src/types.ts
|
|
460
|
+
var ConnectionState = /* @__PURE__ */ ((ConnectionState2) => {
|
|
461
|
+
ConnectionState2["NO_IDENTITY"] = "no_identity";
|
|
462
|
+
ConnectionState2["STANDBY"] = "standby";
|
|
463
|
+
ConnectionState2["AUTHENTICATED"] = "authenticated";
|
|
464
|
+
ConnectionState2["CONNECTING"] = "connecting";
|
|
465
|
+
ConnectionState2["READY"] = "ready";
|
|
466
|
+
ConnectionState2["RETRY_BACKOFF"] = "retry_backoff";
|
|
467
|
+
ConnectionState2["RECONNECTING"] = "reconnecting";
|
|
468
|
+
ConnectionState2["CONNECTION_FAILED"] = "connection_failed";
|
|
469
|
+
ConnectionState2["CLOSED"] = "closed";
|
|
470
|
+
return ConnectionState2;
|
|
471
|
+
})(ConnectionState || {});
|
|
472
|
+
var STATE_TO_PUBLIC = {
|
|
473
|
+
idle: "no_identity" /* NO_IDENTITY */,
|
|
474
|
+
authenticating: "connecting" /* CONNECTING */,
|
|
475
|
+
connected: "ready" /* READY */,
|
|
476
|
+
disconnected: "standby" /* STANDBY */,
|
|
477
|
+
terminal_failed: "connection_failed" /* CONNECTION_FAILED */,
|
|
478
|
+
retry_backoff: "retry_backoff" /* RETRY_BACKOFF */,
|
|
479
|
+
reconnecting: "reconnecting" /* RECONNECTING */
|
|
480
|
+
};
|
|
457
481
|
function isJsonObject(value) {
|
|
458
482
|
if (value === null || typeof value !== "object" || Array.isArray(value)) {
|
|
459
483
|
return false;
|
|
@@ -466,12 +490,16 @@ function isJsonObject(value) {
|
|
|
466
490
|
var AUNError = class extends Error {
|
|
467
491
|
constructor(message, opts) {
|
|
468
492
|
super(message);
|
|
493
|
+
/** JSON-RPC 错误码(数字,用于 RPC 层) */
|
|
469
494
|
__publicField(this, "code");
|
|
495
|
+
/** 业务字符串错误码(与 Python/TS SDK error_codes 对齐) */
|
|
496
|
+
__publicField(this, "stringCode");
|
|
470
497
|
__publicField(this, "data");
|
|
471
498
|
__publicField(this, "retryable");
|
|
472
499
|
__publicField(this, "traceId");
|
|
473
500
|
this.name = "AUNError";
|
|
474
501
|
this.code = opts?.code ?? -1;
|
|
502
|
+
this.stringCode = opts?.stringCode ?? "";
|
|
475
503
|
this.data = opts?.data ?? null;
|
|
476
504
|
this.retryable = opts?.retryable ?? false;
|
|
477
505
|
this.traceId = opts?.traceId ?? null;
|
|
@@ -1185,7 +1213,8 @@ var DIAG_PARAM_FIELDS = [
|
|
|
1185
1213
|
"request_id",
|
|
1186
1214
|
"owner_aid",
|
|
1187
1215
|
"rotated_by",
|
|
1188
|
-
"action"
|
|
1216
|
+
"action",
|
|
1217
|
+
"force"
|
|
1189
1218
|
];
|
|
1190
1219
|
var DIAG_RESULT_FIELDS = [
|
|
1191
1220
|
...DIAG_PARAM_FIELDS,
|
|
@@ -1232,6 +1261,8 @@ var RPCTransport = class {
|
|
|
1232
1261
|
__publicField(this, "_onDisconnect");
|
|
1233
1262
|
__publicField(this, "_ws", null);
|
|
1234
1263
|
__publicField(this, "_closed", true);
|
|
1264
|
+
__publicField(this, "_lastCloseCode", null);
|
|
1265
|
+
__publicField(this, "_lastCloseReason", "");
|
|
1235
1266
|
__publicField(this, "_challenge", null);
|
|
1236
1267
|
__publicField(this, "_pending", /* @__PURE__ */ new Map());
|
|
1237
1268
|
__publicField(this, "_pendingBackground", /* @__PURE__ */ new Set());
|
|
@@ -1287,6 +1318,8 @@ var RPCTransport = class {
|
|
|
1287
1318
|
async connect(url) {
|
|
1288
1319
|
const tStart = Date.now();
|
|
1289
1320
|
this._log.debug(`connect enter: url=${url}`);
|
|
1321
|
+
this._lastCloseCode = null;
|
|
1322
|
+
this._lastCloseReason = "";
|
|
1290
1323
|
return new Promise((resolve, reject) => {
|
|
1291
1324
|
const ws = new WebSocket(url);
|
|
1292
1325
|
this._ws = ws;
|
|
@@ -1329,6 +1362,8 @@ var RPCTransport = class {
|
|
|
1329
1362
|
}
|
|
1330
1363
|
};
|
|
1331
1364
|
ws.onclose = (event) => {
|
|
1365
|
+
this._lastCloseCode = event.code;
|
|
1366
|
+
this._lastCloseReason = event.reason ?? "";
|
|
1332
1367
|
if (!initialResolved) {
|
|
1333
1368
|
initialResolved = true;
|
|
1334
1369
|
this._log.debug(`connect exit (error): elapsed=${Date.now() - tStart}ms err=closed_before_initial code=${event.code}`);
|
|
@@ -1379,7 +1414,7 @@ var RPCTransport = class {
|
|
|
1379
1414
|
*/
|
|
1380
1415
|
async call(method, params, timeout, trace, background = false) {
|
|
1381
1416
|
if (this._closed || !this._ws) {
|
|
1382
|
-
throw
|
|
1417
|
+
throw this._notConnectedError();
|
|
1383
1418
|
}
|
|
1384
1419
|
const rpcId = `rpc-${String(++_rpcIdCounter).padStart(6, "0")}`;
|
|
1385
1420
|
const effectiveTimeout = (timeout ?? this._timeout) * 1e3;
|
|
@@ -1571,6 +1606,8 @@ var RPCTransport = class {
|
|
|
1571
1606
|
_handleClose(event) {
|
|
1572
1607
|
const wasClosed = this._closed;
|
|
1573
1608
|
this._closed = true;
|
|
1609
|
+
this._lastCloseCode = event.code;
|
|
1610
|
+
this._lastCloseReason = event.reason ?? "";
|
|
1574
1611
|
const err = new ConnectionError(`websocket closed: code=${event.code}`);
|
|
1575
1612
|
for (const [, pending] of this._pending) {
|
|
1576
1613
|
clearTimeout(pending.timer);
|
|
@@ -1596,6 +1633,13 @@ var RPCTransport = class {
|
|
|
1596
1633
|
}
|
|
1597
1634
|
}
|
|
1598
1635
|
}
|
|
1636
|
+
_notConnectedError() {
|
|
1637
|
+
if (this._lastCloseCode !== null) {
|
|
1638
|
+
const reason = this._lastCloseReason ? ` reason=${this._lastCloseReason}` : "";
|
|
1639
|
+
return new ConnectionError(`transport not connected: last websocket close code=${this._lastCloseCode}${reason}`);
|
|
1640
|
+
}
|
|
1641
|
+
return new ConnectionError("transport not connected");
|
|
1642
|
+
}
|
|
1599
1643
|
_routeMessage(message) {
|
|
1600
1644
|
if ("id" in message) {
|
|
1601
1645
|
const rpcId = String(message.id);
|
|
@@ -1702,7 +1746,6 @@ var _noopLog4 = { error: () => {
|
|
|
1702
1746
|
}, debug: () => {
|
|
1703
1747
|
} };
|
|
1704
1748
|
var AUN_SDK_LANG = "javascript";
|
|
1705
|
-
var AUN_SDK_VERSION = "0.3.5";
|
|
1706
1749
|
function splitPemBundle(bundle) {
|
|
1707
1750
|
const marker = "-----END CERTIFICATE-----";
|
|
1708
1751
|
const certs = [];
|
|
@@ -2013,7 +2056,13 @@ var _AuthFlow = class _AuthFlow {
|
|
|
2013
2056
|
const cert = await this._keystore.loadCert(identity.aid);
|
|
2014
2057
|
if (cert) identity.cert = cert;
|
|
2015
2058
|
const instanceState = await this._loadInstanceState(identity.aid);
|
|
2016
|
-
if (instanceState)
|
|
2059
|
+
if (instanceState) {
|
|
2060
|
+
for (const key of _AuthFlow._INSTANCE_STATE_FIELDS) {
|
|
2061
|
+
if (key in instanceState) {
|
|
2062
|
+
identity[key] = instanceState[key];
|
|
2063
|
+
}
|
|
2064
|
+
}
|
|
2065
|
+
}
|
|
2017
2066
|
this._log.debug(`loadIdentity exit: elapsed=${Date.now() - tStart}ms aid=${identity.aid} has_cert=${!!identity.cert}`);
|
|
2018
2067
|
return identity;
|
|
2019
2068
|
} catch (err) {
|
|
@@ -2532,7 +2581,25 @@ var _AuthFlow = class _AuthFlow {
|
|
|
2532
2581
|
});
|
|
2533
2582
|
return { cert: response.cert };
|
|
2534
2583
|
}
|
|
2535
|
-
/**
|
|
2584
|
+
/**
|
|
2585
|
+
* 从服务端下载指定 AID 的证书(公开 API)。
|
|
2586
|
+
*
|
|
2587
|
+
* @param gatewayUrl - Gateway WebSocket URL
|
|
2588
|
+
* @param aid - 目标 AID
|
|
2589
|
+
* @returns 证书 PEM 字符串,如果 AID 未注册则返回 null
|
|
2590
|
+
*
|
|
2591
|
+
* @example
|
|
2592
|
+
* ```typescript
|
|
2593
|
+
* const cert = await auth.fetchPeerCert('wss://gateway.example.com', 'alice.aid.com');
|
|
2594
|
+
* if (cert) {
|
|
2595
|
+
* console.log('Alice is registered');
|
|
2596
|
+
* }
|
|
2597
|
+
* ```
|
|
2598
|
+
*/
|
|
2599
|
+
async fetchPeerCert(gatewayUrl, aid) {
|
|
2600
|
+
return this._downloadRegisteredCert(gatewayUrl, aid);
|
|
2601
|
+
}
|
|
2602
|
+
/** 下载服务端当前登记的证书(内部实现);未注册返回 null */
|
|
2536
2603
|
async _downloadRegisteredCert(gatewayUrl, aid) {
|
|
2537
2604
|
const certUrl = gatewayHttpUrl(gatewayUrl, `/pki/cert/${aid}`);
|
|
2538
2605
|
try {
|
|
@@ -2631,7 +2698,7 @@ var _AuthFlow = class _AuthFlow {
|
|
|
2631
2698
|
client: {
|
|
2632
2699
|
slot_id: String(opts?.slotId ?? this._slotId ?? ""),
|
|
2633
2700
|
sdk_lang: AUN_SDK_LANG,
|
|
2634
|
-
sdk_version:
|
|
2701
|
+
sdk_version: VERSION
|
|
2635
2702
|
},
|
|
2636
2703
|
delivery_mode: opts?.deliveryMode ?? { mode: "fanout" },
|
|
2637
2704
|
capabilities
|
|
@@ -3376,1886 +3443,169 @@ var SeqTracker = class {
|
|
|
3376
3443
|
}
|
|
3377
3444
|
};
|
|
3378
3445
|
|
|
3379
|
-
// src/
|
|
3446
|
+
// src/client.ts
|
|
3447
|
+
init_crypto();
|
|
3448
|
+
|
|
3449
|
+
// src/keystore/indexeddb.ts
|
|
3380
3450
|
init_crypto();
|
|
3381
3451
|
var _noopLog5 = { error: () => {
|
|
3382
3452
|
}, warn: () => {
|
|
3383
3453
|
}, info: () => {
|
|
3384
3454
|
}, debug: () => {
|
|
3385
3455
|
} };
|
|
3386
|
-
|
|
3387
|
-
|
|
3388
|
-
var AGENT_MD_SIGNATURE_RE = /^<!-- AUN-SIGNATURE\r?\n(?<body>[\s\S]*?)\r?\n-->\s*$/;
|
|
3389
|
-
var AGENT_MD_FINGERPRINT_RE = /^sha256:[0-9a-f]{64}$/;
|
|
3390
|
-
function agentMdHttpScheme(gatewayUrl) {
|
|
3391
|
-
const raw = String(gatewayUrl ?? "").trim().toLowerCase();
|
|
3392
|
-
return raw.startsWith("ws://") ? "http" : "https";
|
|
3456
|
+
function safeAid(aid) {
|
|
3457
|
+
return aid.replace(/[/\\:]/g, "_");
|
|
3393
3458
|
}
|
|
3394
|
-
function
|
|
3395
|
-
|
|
3396
|
-
|
|
3397
|
-
|
|
3398
|
-
|
|
3459
|
+
function extractSpkiB64FromCertPem(certPem) {
|
|
3460
|
+
try {
|
|
3461
|
+
const der = new Uint8Array(pemToArrayBuffer(certPem));
|
|
3462
|
+
const tlv = (d, o) => {
|
|
3463
|
+
let lo = o + 1, len;
|
|
3464
|
+
if (d[lo] & 128) {
|
|
3465
|
+
const n = d[lo] & 127;
|
|
3466
|
+
len = 0;
|
|
3467
|
+
for (let i = 0; i < n; i++) len = len << 8 | d[lo + 1 + i];
|
|
3468
|
+
lo += 1 + n;
|
|
3469
|
+
} else {
|
|
3470
|
+
len = d[lo];
|
|
3471
|
+
lo += 1;
|
|
3472
|
+
}
|
|
3473
|
+
return [lo, len];
|
|
3474
|
+
};
|
|
3475
|
+
const skip = (d, o) => {
|
|
3476
|
+
const [vs2, vl] = tlv(d, o);
|
|
3477
|
+
return vs2 + vl;
|
|
3478
|
+
};
|
|
3479
|
+
let [vs] = tlv(der, 0);
|
|
3480
|
+
let pos = vs;
|
|
3481
|
+
const [tbsVs] = tlv(der, pos);
|
|
3482
|
+
pos = tbsVs;
|
|
3483
|
+
if (der[pos] === 160) pos = skip(der, pos);
|
|
3484
|
+
for (let i = 0; i < 5; i++) pos = skip(der, pos);
|
|
3485
|
+
const spkiEnd = skip(der, pos);
|
|
3486
|
+
const spkiBytes = der.slice(pos, spkiEnd);
|
|
3487
|
+
let b = "";
|
|
3488
|
+
for (let i = 0; i < spkiBytes.length; i++) b += String.fromCharCode(spkiBytes[i]);
|
|
3489
|
+
return btoa(b);
|
|
3490
|
+
} catch {
|
|
3491
|
+
return "";
|
|
3399
3492
|
}
|
|
3400
|
-
return host;
|
|
3401
3493
|
}
|
|
3402
|
-
function
|
|
3403
|
-
|
|
3404
|
-
if (idx < 0) {
|
|
3405
|
-
return { payload: content, fields: null };
|
|
3406
|
-
}
|
|
3407
|
-
if (idx > 0 && content[idx - 1] !== "\n" && content[idx - 1] !== "\r") {
|
|
3408
|
-
return { payload: content, fields: null };
|
|
3409
|
-
}
|
|
3410
|
-
const tail = content.slice(idx);
|
|
3411
|
-
const match = tail.match(AGENT_MD_SIGNATURE_RE);
|
|
3412
|
-
if (!match) {
|
|
3413
|
-
return { payload: content.slice(0, idx), fields: null, parseError: "malformed signature block" };
|
|
3414
|
-
}
|
|
3415
|
-
const fields = {};
|
|
3416
|
-
for (const rawLine of match.groups?.body?.split(/\r?\n/) ?? []) {
|
|
3417
|
-
const line = rawLine.trim();
|
|
3418
|
-
if (!line) continue;
|
|
3419
|
-
const colon = line.indexOf(":");
|
|
3420
|
-
if (colon < 0) {
|
|
3421
|
-
return { payload: content.slice(0, idx), fields: null, parseError: `malformed signature field: ${line}` };
|
|
3422
|
-
}
|
|
3423
|
-
const key = line.slice(0, colon).trim().toLowerCase();
|
|
3424
|
-
const value = line.slice(colon + 1).trim();
|
|
3425
|
-
fields[key] = value;
|
|
3426
|
-
}
|
|
3427
|
-
for (const required of ["cert_fingerprint", "timestamp", "signature"]) {
|
|
3428
|
-
if (!fields[required]) {
|
|
3429
|
-
return { payload: content.slice(0, idx), fields: null, parseError: `signature block missing ${required}` };
|
|
3430
|
-
}
|
|
3431
|
-
}
|
|
3432
|
-
if (!AGENT_MD_FINGERPRINT_RE.test(fields.cert_fingerprint.toLowerCase())) {
|
|
3433
|
-
return { payload: content.slice(0, idx), fields: null, parseError: "invalid cert_fingerprint" };
|
|
3434
|
-
}
|
|
3435
|
-
if (!Number.isFinite(Number(fields.timestamp))) {
|
|
3436
|
-
return { payload: content.slice(0, idx), fields: null, parseError: "invalid timestamp" };
|
|
3437
|
-
}
|
|
3438
|
-
return { payload: content.slice(0, idx), fields, parseError: void 0 };
|
|
3494
|
+
function encodePart(value) {
|
|
3495
|
+
return encodeURIComponent(value);
|
|
3439
3496
|
}
|
|
3440
|
-
function
|
|
3441
|
-
|
|
3442
|
-
|
|
3443
|
-
|
|
3444
|
-
|
|
3445
|
-
|
|
3446
|
-
|
|
3447
|
-
|
|
3448
|
-
|
|
3449
|
-
|
|
3450
|
-
|
|
3451
|
-
|
|
3452
|
-
|
|
3453
|
-
|
|
3454
|
-
|
|
3497
|
+
function deepClone(value) {
|
|
3498
|
+
return JSON.parse(JSON.stringify(value));
|
|
3499
|
+
}
|
|
3500
|
+
function isRecord(value) {
|
|
3501
|
+
return isJsonObject(value);
|
|
3502
|
+
}
|
|
3503
|
+
function toBufferSource2(bytes) {
|
|
3504
|
+
return bytes.slice().buffer;
|
|
3505
|
+
}
|
|
3506
|
+
function sameJson(a, b) {
|
|
3507
|
+
return JSON.stringify(a) === JSON.stringify(b);
|
|
3508
|
+
}
|
|
3509
|
+
var _ENC_ALGO = "AES-GCM";
|
|
3510
|
+
var _PBKDF2_ITERATIONS = 1e5;
|
|
3511
|
+
var SeedMigrationError = class extends Error {
|
|
3512
|
+
constructor(message) {
|
|
3513
|
+
super(message);
|
|
3514
|
+
this.name = "SeedMigrationError";
|
|
3455
3515
|
}
|
|
3456
|
-
|
|
3516
|
+
};
|
|
3517
|
+
function _uint8ToBase64(bytes) {
|
|
3518
|
+
let b = "";
|
|
3519
|
+
for (let i = 0; i < bytes.length; i++) b += String.fromCharCode(bytes[i]);
|
|
3520
|
+
return btoa(b);
|
|
3521
|
+
}
|
|
3522
|
+
function _base64ToUint8(b64) {
|
|
3523
|
+
return Uint8Array.from(atob(b64), (c) => c.charCodeAt(0));
|
|
3524
|
+
}
|
|
3525
|
+
async function _deriveEncKey(seed, salt) {
|
|
3526
|
+
const raw = new TextEncoder().encode(seed);
|
|
3527
|
+
const base = await crypto.subtle.importKey("raw", raw, "PBKDF2", false, ["deriveKey"]);
|
|
3528
|
+
return crypto.subtle.deriveKey(
|
|
3529
|
+
{ name: "PBKDF2", salt, iterations: _PBKDF2_ITERATIONS, hash: "SHA-256" },
|
|
3530
|
+
base,
|
|
3531
|
+
{ name: _ENC_ALGO, length: 256 },
|
|
3532
|
+
false,
|
|
3533
|
+
["encrypt", "decrypt"]
|
|
3534
|
+
);
|
|
3457
3535
|
}
|
|
3458
|
-
function
|
|
3459
|
-
const
|
|
3460
|
-
|
|
3461
|
-
|
|
3462
|
-
|
|
3536
|
+
async function _encryptPEM(pem, seed) {
|
|
3537
|
+
const salt = crypto.getRandomValues(new Uint8Array(16));
|
|
3538
|
+
const iv = crypto.getRandomValues(new Uint8Array(12));
|
|
3539
|
+
const key = await _deriveEncKey(seed, salt);
|
|
3540
|
+
const ct = await crypto.subtle.encrypt({ name: _ENC_ALGO, iv }, key, new TextEncoder().encode(pem));
|
|
3541
|
+
return {
|
|
3542
|
+
ct: _uint8ToBase64(new Uint8Array(ct)),
|
|
3543
|
+
iv: _uint8ToBase64(iv),
|
|
3544
|
+
salt: _uint8ToBase64(salt)
|
|
3463
3545
|
};
|
|
3464
|
-
if (reason) result.reason = reason;
|
|
3465
|
-
if (aid) result.aid = aid;
|
|
3466
|
-
if (certFingerprint) result.cert_fingerprint = certFingerprint;
|
|
3467
|
-
if (timestamp !== void 0) result.timestamp = timestamp;
|
|
3468
|
-
return result;
|
|
3469
3546
|
}
|
|
3470
|
-
function
|
|
3471
|
-
|
|
3472
|
-
|
|
3547
|
+
async function _decryptPEM(enc, seed) {
|
|
3548
|
+
const salt = _base64ToUint8(enc.salt);
|
|
3549
|
+
const iv = _base64ToUint8(enc.iv);
|
|
3550
|
+
const ct = _base64ToUint8(enc.ct);
|
|
3551
|
+
const key = await _deriveEncKey(seed, salt);
|
|
3552
|
+
const pt = await crypto.subtle.decrypt({ name: _ENC_ALGO, iv: toBufferSource2(iv) }, key, toBufferSource2(ct));
|
|
3553
|
+
return new TextDecoder().decode(pt);
|
|
3473
3554
|
}
|
|
3474
|
-
function
|
|
3475
|
-
|
|
3476
|
-
return Number.isFinite(parsed) ? Math.trunc(parsed) : void 0;
|
|
3555
|
+
function hasEncryptionSeed(seed) {
|
|
3556
|
+
return seed !== void 0;
|
|
3477
3557
|
}
|
|
3478
|
-
|
|
3479
|
-
|
|
3480
|
-
|
|
3481
|
-
|
|
3482
|
-
|
|
3483
|
-
|
|
3484
|
-
|
|
3485
|
-
|
|
3486
|
-
|
|
3487
|
-
|
|
3488
|
-
|
|
3489
|
-
|
|
3490
|
-
|
|
3491
|
-
|
|
3492
|
-
return
|
|
3558
|
+
var DB_NAME = "aun-keystore";
|
|
3559
|
+
var DB_VERSION = 6;
|
|
3560
|
+
var STORE_KEY_PAIRS = "key_pairs";
|
|
3561
|
+
var STORE_CERTS = "certs";
|
|
3562
|
+
var STORE_METADATA = "metadata";
|
|
3563
|
+
var STORE_INSTANCE_STATE = "instance_state";
|
|
3564
|
+
var STORE_PREKEYS = "prekeys";
|
|
3565
|
+
var STORE_GROUP_CURRENT = "group_current";
|
|
3566
|
+
var STORE_GROUP_OLD_EPOCHS = "group_old_epochs";
|
|
3567
|
+
var STORE_SESSIONS = "e2ee_sessions";
|
|
3568
|
+
var STORE_GROUP_STATE = "group_state";
|
|
3569
|
+
var STORE_AGENT_MD_CACHE = "agent_md_cache";
|
|
3570
|
+
var STRUCTURED_RECOVERY_RETENTION_MS = 7 * 24 * 3600 * 1e3;
|
|
3571
|
+
function metadataStoreKey(aid) {
|
|
3572
|
+
return safeAid(aid);
|
|
3573
|
+
}
|
|
3574
|
+
function normalizeCertFingerprint(certFingerprint) {
|
|
3575
|
+
const normalized = String(certFingerprint ?? "").trim().toLowerCase();
|
|
3576
|
+
if (!normalized) return "";
|
|
3577
|
+
if (!normalized.startsWith("sha256:")) return "";
|
|
3578
|
+
const hexPart = normalized.slice(7);
|
|
3579
|
+
if (hexPart.length !== 64 || /[^0-9a-f]/.test(hexPart)) return "";
|
|
3580
|
+
return normalized;
|
|
3493
3581
|
}
|
|
3494
|
-
function
|
|
3582
|
+
async function fingerprintFromCertPem(certPem) {
|
|
3495
3583
|
try {
|
|
3496
|
-
const
|
|
3497
|
-
const
|
|
3498
|
-
const
|
|
3499
|
-
|
|
3500
|
-
for (let i = 0; i <= bytes.length - oid.length - 2; i++) {
|
|
3501
|
-
let matched = true;
|
|
3502
|
-
for (let j = 0; j < oid.length; j++) {
|
|
3503
|
-
if (bytes[i + j] !== oid[j]) {
|
|
3504
|
-
matched = false;
|
|
3505
|
-
break;
|
|
3506
|
-
}
|
|
3507
|
-
}
|
|
3508
|
-
if (!matched) continue;
|
|
3509
|
-
const tag = bytes[i + oid.length];
|
|
3510
|
-
const lengthInfo = readDerLength(bytes, i + oid.length + 1);
|
|
3511
|
-
if (!lengthInfo) continue;
|
|
3512
|
-
const start = i + oid.length + 1 + lengthInfo.lengthBytes;
|
|
3513
|
-
const end = start + lengthInfo.length;
|
|
3514
|
-
if (end > bytes.length) continue;
|
|
3515
|
-
const valueBytes = bytes.slice(start, end);
|
|
3516
|
-
if (tag === 12 || tag === 19 || tag === 22 || tag === 20 || tag === 30) {
|
|
3517
|
-
commonName = decoder2.decode(valueBytes).trim();
|
|
3518
|
-
} else {
|
|
3519
|
-
commonName = decoder2.decode(valueBytes).trim();
|
|
3520
|
-
}
|
|
3521
|
-
}
|
|
3522
|
-
return commonName;
|
|
3584
|
+
const der = pemToArrayBuffer(certPem);
|
|
3585
|
+
const hash = await crypto.subtle.digest("SHA-256", der);
|
|
3586
|
+
const hex = Array.from(new Uint8Array(hash)).map((b) => b.toString(16).padStart(2, "0")).join("");
|
|
3587
|
+
return `sha256:${hex}`;
|
|
3523
3588
|
} catch {
|
|
3524
3589
|
return "";
|
|
3525
3590
|
}
|
|
3526
3591
|
}
|
|
3527
|
-
function
|
|
3528
|
-
|
|
3529
|
-
|
|
3530
|
-
|
|
3531
|
-
for (let i = 0; i < der.length - 2 && dates.length < 2; i++) {
|
|
3532
|
-
const tag = der[i];
|
|
3533
|
-
if (tag !== 23 && tag !== 24) continue;
|
|
3534
|
-
const len = der[i + 1];
|
|
3535
|
-
if (len === 0 || len > 20 || i + 2 + len > der.length) continue;
|
|
3536
|
-
const str = new TextDecoder().decode(der.slice(i + 2, i + 2 + len));
|
|
3537
|
-
const ts = parseAsn1Time2(tag, str);
|
|
3538
|
-
if (ts !== null) {
|
|
3539
|
-
dates.push(ts);
|
|
3540
|
-
i += 1 + len;
|
|
3541
|
-
}
|
|
3542
|
-
}
|
|
3543
|
-
if (dates.length >= 2) {
|
|
3544
|
-
return { notBefore: dates[0], notAfter: dates[1] };
|
|
3545
|
-
}
|
|
3546
|
-
return null;
|
|
3547
|
-
} catch {
|
|
3548
|
-
return null;
|
|
3549
|
-
}
|
|
3592
|
+
function certStoreKey(aid, certFingerprint) {
|
|
3593
|
+
const normalized = normalizeCertFingerprint(certFingerprint);
|
|
3594
|
+
if (!normalized) return safeAid(aid);
|
|
3595
|
+
return `${safeAid(aid)}|${encodePart(normalized)}`;
|
|
3550
3596
|
}
|
|
3551
|
-
function
|
|
3552
|
-
|
|
3553
|
-
|
|
3554
|
-
|
|
3555
|
-
if (cleaned.length < 12) return null;
|
|
3556
|
-
let year = parseInt(cleaned.slice(0, 2), 10);
|
|
3557
|
-
year += year >= 50 ? 1900 : 2e3;
|
|
3558
|
-
const month = parseInt(cleaned.slice(2, 4), 10) - 1;
|
|
3559
|
-
const day = parseInt(cleaned.slice(4, 6), 10);
|
|
3560
|
-
const hour = parseInt(cleaned.slice(6, 8), 10);
|
|
3561
|
-
const min = parseInt(cleaned.slice(8, 10), 10);
|
|
3562
|
-
const sec = parseInt(cleaned.slice(10, 12), 10);
|
|
3563
|
-
return Date.UTC(year, month, day, hour, min, sec);
|
|
3564
|
-
} else if (tag === 24) {
|
|
3565
|
-
const cleaned = str.replace(/Z$/i, "");
|
|
3566
|
-
if (cleaned.length < 14) return null;
|
|
3567
|
-
const year = parseInt(cleaned.slice(0, 4), 10);
|
|
3568
|
-
const month = parseInt(cleaned.slice(4, 6), 10) - 1;
|
|
3569
|
-
const day = parseInt(cleaned.slice(6, 8), 10);
|
|
3570
|
-
const hour = parseInt(cleaned.slice(8, 10), 10);
|
|
3571
|
-
const min = parseInt(cleaned.slice(10, 12), 10);
|
|
3572
|
-
const sec = parseInt(cleaned.slice(12, 14), 10);
|
|
3573
|
-
return Date.UTC(year, month, day, hour, min, sec);
|
|
3574
|
-
}
|
|
3575
|
-
return null;
|
|
3576
|
-
} catch {
|
|
3577
|
-
return null;
|
|
3578
|
-
}
|
|
3597
|
+
function instanceStateStoreKey(aid, deviceId, slotId = "") {
|
|
3598
|
+
const normalizedDevice = normalizeInstanceId(deviceId, "device_id");
|
|
3599
|
+
const normalizedSlot = normalizeInstanceId(slotId, "slot_id", { allowEmpty: true }) || "_singleton";
|
|
3600
|
+
return `${safeAid(aid)}|${encodePart(normalizedDevice)}|${encodePart(normalizedSlot)}`;
|
|
3579
3601
|
}
|
|
3580
|
-
|
|
3581
|
-
|
|
3582
|
-
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
3583
|
-
try {
|
|
3584
|
-
return await fetch(input, { ...init, signal: controller.signal });
|
|
3585
|
-
} catch (error) {
|
|
3586
|
-
if (controller.signal.aborted) {
|
|
3587
|
-
throw new AUNError(`agent.md request timed out after ${timeoutMs}ms`);
|
|
3588
|
-
}
|
|
3589
|
-
throw error;
|
|
3590
|
-
} finally {
|
|
3591
|
-
clearTimeout(timer);
|
|
3592
|
-
}
|
|
3602
|
+
function prekeyPrefix(aid) {
|
|
3603
|
+
return `${safeAid(aid)}|`;
|
|
3593
3604
|
}
|
|
3594
|
-
|
|
3595
|
-
|
|
3596
|
-
|
|
3597
|
-
|
|
3598
|
-
__publicField(this, "_agentMdCache", /* @__PURE__ */ new Map());
|
|
3599
|
-
this._client = client;
|
|
3600
|
-
}
|
|
3601
|
-
setLogger(log) {
|
|
3602
|
-
this._log = log;
|
|
3603
|
-
}
|
|
3604
|
-
get _internal() {
|
|
3605
|
-
return this._client;
|
|
3606
|
-
}
|
|
3607
|
-
/**
|
|
3608
|
-
* 解析 gateway URL。
|
|
3609
|
-
* 优先使用已预置的 gatewayUrl,否则基于 AID 自动发现。
|
|
3610
|
-
*
|
|
3611
|
-
* 发现流程:
|
|
3612
|
-
* 1. 若 gatewayUrl 已预置(内存),直接返回
|
|
3613
|
-
* 2. 从 keystore metadata 读 cached gateway_url(跨进程/会话复用)
|
|
3614
|
-
* 3. https://{aid}/.well-known/aun-gateway(泛域名 nameservice)
|
|
3615
|
-
* 4. https://gateway.{issuer}/.well-known/aun-gateway(Gateway 直连)
|
|
3616
|
-
* 发现成功后写入 keystore metadata,避免每次都做 well-known discovery。
|
|
3617
|
-
*/
|
|
3618
|
-
async _resolveGateway(aid) {
|
|
3619
|
-
if (this._client.gatewayUrl) {
|
|
3620
|
-
return this._client.gatewayUrl;
|
|
3621
|
-
}
|
|
3622
|
-
const resolvedAid = aid ?? this._client.aid;
|
|
3623
|
-
if (resolvedAid) {
|
|
3624
|
-
try {
|
|
3625
|
-
const cached = await this._loadCachedGatewayUrl(resolvedAid);
|
|
3626
|
-
if (cached) {
|
|
3627
|
-
this._log.debug(`_resolveGateway from keystore cache aid=${resolvedAid} gateway=${cached}`);
|
|
3628
|
-
this._client.gatewayUrl = cached;
|
|
3629
|
-
return cached;
|
|
3630
|
-
}
|
|
3631
|
-
} catch (exc) {
|
|
3632
|
-
this._log.debug(`_resolveGateway load cache failed: ${exc instanceof Error ? exc.message : String(exc)}`);
|
|
3633
|
-
}
|
|
3634
|
-
const parts = resolvedAid.split(".");
|
|
3635
|
-
const issuerDomain = parts.length > 1 ? parts.slice(1).join(".") : resolvedAid;
|
|
3636
|
-
const port = this._client.configModel.discoveryPort;
|
|
3637
|
-
const portSuffix = port ? `:${port}` : "";
|
|
3638
|
-
const primaryUrl = `https://${resolvedAid}${portSuffix}/.well-known/aun-gateway`;
|
|
3639
|
-
try {
|
|
3640
|
-
const discovered2 = await this._client.discovery.discover(primaryUrl);
|
|
3641
|
-
await this._persistGatewayUrl(resolvedAid, discovered2);
|
|
3642
|
-
return discovered2;
|
|
3643
|
-
} catch {
|
|
3644
|
-
}
|
|
3645
|
-
const fallbackUrl = `https://gateway.${issuerDomain}${portSuffix}/.well-known/aun-gateway`;
|
|
3646
|
-
const discovered = await this._client.discovery.discover(fallbackUrl);
|
|
3647
|
-
await this._persistGatewayUrl(resolvedAid, discovered);
|
|
3648
|
-
return discovered;
|
|
3649
|
-
}
|
|
3650
|
-
throw new ValidationError(
|
|
3651
|
-
"unable to resolve gateway: set client.gatewayUrl or provide 'aid' for auto-discovery"
|
|
3652
|
-
);
|
|
3653
|
-
}
|
|
3654
|
-
/** 从 keystore metadata 读取 cached gateway_url(跨进程/会话复用) */
|
|
3655
|
-
async _loadCachedGatewayUrl(aid) {
|
|
3656
|
-
if (!aid) return "";
|
|
3657
|
-
const ks = this._internal._keystore;
|
|
3658
|
-
if (!ks || typeof ks.getMetadata !== "function") return "";
|
|
3659
|
-
try {
|
|
3660
|
-
const value = await ks.getMetadata(aid, "gateway_url");
|
|
3661
|
-
const raw = String(value ?? "").trim();
|
|
3662
|
-
if (!raw) return "";
|
|
3663
|
-
if (raw.startsWith('"') && raw.endsWith('"')) {
|
|
3664
|
-
try {
|
|
3665
|
-
const parsed = JSON.parse(raw);
|
|
3666
|
-
if (typeof parsed === "string") return parsed.trim();
|
|
3667
|
-
} catch {
|
|
3668
|
-
}
|
|
3669
|
-
}
|
|
3670
|
-
return raw;
|
|
3671
|
-
} catch {
|
|
3672
|
-
return "";
|
|
3673
|
-
}
|
|
3674
|
-
}
|
|
3675
|
-
/** 持久化 gateway_url 到 keystore metadata,供跨进程复用 */
|
|
3676
|
-
async _persistGatewayUrl(aid, gatewayUrl) {
|
|
3677
|
-
if (!gatewayUrl || !aid) return;
|
|
3678
|
-
const ks = this._internal._keystore;
|
|
3679
|
-
if (!ks || typeof ks.setMetadata !== "function") return;
|
|
3680
|
-
try {
|
|
3681
|
-
await ks.setMetadata(aid, "gateway_url", gatewayUrl);
|
|
3682
|
-
} catch (exc) {
|
|
3683
|
-
this._log.debug(`persist gateway_url failed aid=${aid} err=${exc instanceof Error ? exc.message : String(exc)}`);
|
|
3684
|
-
}
|
|
3685
|
-
}
|
|
3686
|
-
/** 内部访问 client 私有属性 */
|
|
3687
|
-
/**
|
|
3688
|
-
* 严格注册新 AID(对齐 TS registerAid)。
|
|
3689
|
-
* 通过 well-known 发现 gateway → 调用 AuthFlow.registerAid 注册。
|
|
3690
|
-
*/
|
|
3691
|
-
async registerAid(params) {
|
|
3692
|
-
const tStart = Date.now();
|
|
3693
|
-
const aid = String(params?.aid ?? "");
|
|
3694
|
-
this._log.debug(`registerAid enter: aid=${aid}`);
|
|
3695
|
-
try {
|
|
3696
|
-
if (!aid) throw new ValidationError("auth.registerAid requires 'aid'");
|
|
3697
|
-
const gatewayUrl = await this._resolveGateway(aid);
|
|
3698
|
-
this._client.gatewayUrl = gatewayUrl;
|
|
3699
|
-
const auth = this._internal._auth;
|
|
3700
|
-
const result = await auth.registerAid(gatewayUrl, aid);
|
|
3701
|
-
this._internal._aid = aid;
|
|
3702
|
-
this._internal._identity = await auth.loadIdentityOrNone(aid);
|
|
3703
|
-
this._log.debug(`registerAid exit: elapsed=${Date.now() - tStart}ms aid=${aid}`);
|
|
3704
|
-
return {
|
|
3705
|
-
aid,
|
|
3706
|
-
cert_pem: result.cert,
|
|
3707
|
-
gateway: gatewayUrl
|
|
3708
|
-
};
|
|
3709
|
-
} catch (err) {
|
|
3710
|
-
this._log.debug(`registerAid exit (error): elapsed=${Date.now() - tStart}ms aid=${aid} err=${err instanceof Error ? err.message : String(err)}`);
|
|
3711
|
-
throw err;
|
|
3712
|
-
}
|
|
3713
|
-
}
|
|
3714
|
-
/**
|
|
3715
|
-
* 认证已有 AID(login1 + login2 两阶段认证)。
|
|
3716
|
-
* 通过 well-known 发现 gateway → 调用 AuthFlow.authenticate。
|
|
3717
|
-
*/
|
|
3718
|
-
async authenticate(params) {
|
|
3719
|
-
const tStart = Date.now();
|
|
3720
|
-
const aid = params?.aid ?? "";
|
|
3721
|
-
this._log.debug(`authenticate enter: aid=${aid || "<current>"}`);
|
|
3722
|
-
try {
|
|
3723
|
-
const request = { ...params ?? {} };
|
|
3724
|
-
const requestAid = request.aid;
|
|
3725
|
-
const gatewayUrl = await this._resolveGateway(requestAid);
|
|
3726
|
-
this._client.gatewayUrl = gatewayUrl;
|
|
3727
|
-
const auth = this._internal._auth;
|
|
3728
|
-
const result = await auth.authenticate(gatewayUrl, requestAid);
|
|
3729
|
-
this._internal._aid = result.aid ?? null;
|
|
3730
|
-
this._internal._identity = await auth.loadIdentityOrNone(String(result.aid));
|
|
3731
|
-
this._log.debug(`authenticate exit: elapsed=${Date.now() - tStart}ms aid=${result.aid}`);
|
|
3732
|
-
return result;
|
|
3733
|
-
} catch (err) {
|
|
3734
|
-
this._log.debug(`authenticate exit (error): elapsed=${Date.now() - tStart}ms err=${err instanceof Error ? err.message : String(err)}`);
|
|
3735
|
-
throw err;
|
|
3736
|
-
}
|
|
3737
|
-
}
|
|
3738
|
-
/** 只读加载本地已注册身份(密钥对 + 证书 + 实例状态)。无副作用,不触发网络请求。 */
|
|
3739
|
-
async loadIdentity(params) {
|
|
3740
|
-
const aid = String((params ?? {})?.aid ?? "").trim() || void 0;
|
|
3741
|
-
const identity = await this._internal._auth.loadIdentityOrNone(aid);
|
|
3742
|
-
if (!identity) {
|
|
3743
|
-
throw new StateError(`identity not found for aid: ${aid ?? "<default>"}`);
|
|
3744
|
-
}
|
|
3745
|
-
return identity;
|
|
3746
|
-
}
|
|
3747
|
-
/** 只读加载本地已注册身份,不存在时返回 null。 */
|
|
3748
|
-
async loadIdentityOrNull(params) {
|
|
3749
|
-
const aid = String((params ?? {})?.aid ?? "").trim() || void 0;
|
|
3750
|
-
return await this._internal._auth.loadIdentityOrNone(aid);
|
|
3751
|
-
}
|
|
3752
|
-
/** 获取对端 AID 的证书 PEM(本地缓存优先,未命中走 PKI HTTP + 链验证)。 */
|
|
3753
|
-
async fetchPeerCert(params) {
|
|
3754
|
-
const aid = String(params?.aid ?? "").trim();
|
|
3755
|
-
if (!aid) throw new Error("auth.fetchPeerCert requires 'aid'");
|
|
3756
|
-
const fp = String(params?.cert_fingerprint ?? "").trim() || void 0;
|
|
3757
|
-
if (typeof this._internal._fetchPeerCert !== "function") {
|
|
3758
|
-
throw new Error("client does not support _fetchPeerCert");
|
|
3759
|
-
}
|
|
3760
|
-
return String(await this._internal._fetchPeerCert(aid, fp)).trim();
|
|
3761
|
-
}
|
|
3762
|
-
async signAgentMd(content, opts) {
|
|
3763
|
-
const tStart = Date.now();
|
|
3764
|
-
this._log.debug(`signAgentMd enter: aid=${opts?.aid ?? "<current>"} content_len=${String(content ?? "").length}`);
|
|
3765
|
-
try {
|
|
3766
|
-
const client = this._internal;
|
|
3767
|
-
const targetAid = String(opts?.aid ?? client._aid ?? "").trim();
|
|
3768
|
-
const identity = await client._auth.loadIdentityOrNone(targetAid || void 0);
|
|
3769
|
-
if (!identity) {
|
|
3770
|
-
throw new StateError("no local identity found, call auth.registerAid() first");
|
|
3771
|
-
}
|
|
3772
|
-
const privateKeyPem = String(identity.private_key_pem ?? "").trim();
|
|
3773
|
-
const certPem = normalizeIdentityCertPem(identity);
|
|
3774
|
-
if (!privateKeyPem || !certPem) {
|
|
3775
|
-
throw new StateError("local identity missing private key or certificate");
|
|
3776
|
-
}
|
|
3777
|
-
let payload = parseAgentMdTailSignature(String(content ?? "")).payload;
|
|
3778
|
-
if (payload && !payload.endsWith("\n") && !payload.endsWith("\r")) {
|
|
3779
|
-
payload += "\n";
|
|
3780
|
-
}
|
|
3781
|
-
const privateKey = await importPrivateKeyEcdsa(privateKeyPem);
|
|
3782
|
-
const signatureDer = await ecdsaSignDer(privateKey, new TextEncoder().encode(payload));
|
|
3783
|
-
const fingerprint = await certificateSha256Fingerprint(certPem);
|
|
3784
|
-
if (!fingerprint) {
|
|
3785
|
-
throw new StateError("agent.md cert fingerprint failed");
|
|
3786
|
-
}
|
|
3787
|
-
const signedBlock = [
|
|
3788
|
-
AGENT_MD_SIGNATURE_MARKER,
|
|
3789
|
-
`cert_fingerprint: ${fingerprint}`,
|
|
3790
|
-
`timestamp: ${Math.floor(Date.now() / 1e3)}`,
|
|
3791
|
-
`signature: ${uint8ToBase64(signatureDer)}`,
|
|
3792
|
-
"-->"
|
|
3793
|
-
].join("\n");
|
|
3794
|
-
this._log.debug(`signAgentMd exit: elapsed=${Date.now() - tStart}ms aid=${targetAid}`);
|
|
3795
|
-
return payload + signedBlock;
|
|
3796
|
-
} catch (err) {
|
|
3797
|
-
this._log.debug(`signAgentMd exit (error): elapsed=${Date.now() - tStart}ms err=${err instanceof Error ? err.message : String(err)}`);
|
|
3798
|
-
throw err;
|
|
3799
|
-
}
|
|
3800
|
-
}
|
|
3801
|
-
async verifyAgentMd(content, opts) {
|
|
3802
|
-
const tStart = Date.now();
|
|
3803
|
-
this._log.debug(`verifyAgentMd enter: aid=${opts?.aid ?? "<infer>"} content_len=${String(content ?? "").length}`);
|
|
3804
|
-
try {
|
|
3805
|
-
const result = await this._verifyAgentMdImpl(content, opts);
|
|
3806
|
-
this._log.debug(`verifyAgentMd exit: elapsed=${Date.now() - tStart}ms status=${result.status} verified=${!!result.verified}`);
|
|
3807
|
-
return result;
|
|
3808
|
-
} catch (err) {
|
|
3809
|
-
this._log.debug(`verifyAgentMd exit (error): elapsed=${Date.now() - tStart}ms err=${err instanceof Error ? err.message : String(err)}`);
|
|
3810
|
-
throw err;
|
|
3811
|
-
}
|
|
3812
|
-
}
|
|
3813
|
-
async _verifyAgentMdImpl(content, opts) {
|
|
3814
|
-
const { payload, fields, parseError } = parseAgentMdTailSignature(String(content ?? ""));
|
|
3815
|
-
if (!fields) {
|
|
3816
|
-
if (!parseError) {
|
|
3817
|
-
return agentMdResult("unsigned", payload);
|
|
3818
|
-
}
|
|
3819
|
-
return agentMdResult("invalid", payload, parseError);
|
|
3820
|
-
}
|
|
3821
|
-
let expectedAid = String(opts?.aid ?? "").trim();
|
|
3822
|
-
const payloadAid = extractAgentMDAid(payload);
|
|
3823
|
-
if (expectedAid && payloadAid && expectedAid !== payloadAid) {
|
|
3824
|
-
return agentMdResult("invalid", payload, "aid mismatch", payloadAid);
|
|
3825
|
-
}
|
|
3826
|
-
if (!expectedAid) {
|
|
3827
|
-
expectedAid = payloadAid;
|
|
3828
|
-
}
|
|
3829
|
-
let certPem = String(opts?.certPem ?? opts?.cert_pem ?? "").trim();
|
|
3830
|
-
if (!certPem) {
|
|
3831
|
-
if (!expectedAid) {
|
|
3832
|
-
return agentMdResult("invalid", payload, "aid required to verify agent.md");
|
|
3833
|
-
}
|
|
3834
|
-
if (typeof this._internal._fetchPeerCert !== "function") {
|
|
3835
|
-
return agentMdResult("invalid", payload, "aid required to verify agent.md", expectedAid);
|
|
3836
|
-
}
|
|
3837
|
-
try {
|
|
3838
|
-
certPem = String(await this._internal._fetchPeerCert(expectedAid, fields.cert_fingerprint)).trim();
|
|
3839
|
-
} catch (error) {
|
|
3840
|
-
return agentMdResult("invalid", payload, String(error), expectedAid, fields.cert_fingerprint);
|
|
3841
|
-
}
|
|
3842
|
-
}
|
|
3843
|
-
let publicKey;
|
|
3844
|
-
try {
|
|
3845
|
-
publicKey = await importCertPublicKeyEcdsa(certPem);
|
|
3846
|
-
} catch (error) {
|
|
3847
|
-
return agentMdResult("invalid", payload, `invalid certificate: ${String(error)}`, expectedAid, fields.cert_fingerprint);
|
|
3848
|
-
}
|
|
3849
|
-
const actualFingerprint = await certificateSha256Fingerprint(certPem);
|
|
3850
|
-
if (!actualFingerprint || actualFingerprint.toLowerCase() !== fields.cert_fingerprint.toLowerCase()) {
|
|
3851
|
-
return agentMdResult("invalid", payload, "certificate fingerprint mismatch", expectedAid, fields.cert_fingerprint);
|
|
3852
|
-
}
|
|
3853
|
-
const cn = extractCommonNameFromCertPem(certPem);
|
|
3854
|
-
if (expectedAid && cn && cn !== expectedAid) {
|
|
3855
|
-
return agentMdResult("invalid", payload, "certificate aid mismatch", expectedAid, fields.cert_fingerprint);
|
|
3856
|
-
}
|
|
3857
|
-
let signature;
|
|
3858
|
-
try {
|
|
3859
|
-
signature = base64ToUint8(fields.signature);
|
|
3860
|
-
} catch {
|
|
3861
|
-
return agentMdResult("invalid", payload, "invalid signature", expectedAid, fields.cert_fingerprint);
|
|
3862
|
-
}
|
|
3863
|
-
if (!signature.length) {
|
|
3864
|
-
return agentMdResult("invalid", payload, "invalid signature", expectedAid, fields.cert_fingerprint);
|
|
3865
|
-
}
|
|
3866
|
-
const ok = await ecdsaVerifyDer(publicKey, signature, new TextEncoder().encode(payload));
|
|
3867
|
-
if (!ok) {
|
|
3868
|
-
return agentMdResult("invalid", payload, "signature verification failed", expectedAid, fields.cert_fingerprint, parseAgentMdTimestamp(fields.timestamp));
|
|
3869
|
-
}
|
|
3870
|
-
return agentMdResult("verified", payload, "", expectedAid || payloadAid, fields.cert_fingerprint, parseAgentMdTimestamp(fields.timestamp));
|
|
3871
|
-
}
|
|
3872
|
-
async _resolveAgentMdUrl(aid) {
|
|
3873
|
-
const resolvedAid = String(aid ?? "").trim();
|
|
3874
|
-
if (!resolvedAid) {
|
|
3875
|
-
throw new ValidationError("agent.md requires non-empty aid");
|
|
3876
|
-
}
|
|
3877
|
-
let gatewayUrl = this._client.gatewayUrl ?? "";
|
|
3878
|
-
if (!gatewayUrl) {
|
|
3879
|
-
try {
|
|
3880
|
-
gatewayUrl = await this._resolveGateway(resolvedAid);
|
|
3881
|
-
} catch {
|
|
3882
|
-
gatewayUrl = "";
|
|
3883
|
-
}
|
|
3884
|
-
}
|
|
3885
|
-
const authority = agentMdAuthority(resolvedAid, this._client.configModel.discoveryPort);
|
|
3886
|
-
return `${agentMdHttpScheme(gatewayUrl)}://${authority}/agent.md`;
|
|
3887
|
-
}
|
|
3888
|
-
async _ensureAgentMdUploadToken(aid, gatewayUrl) {
|
|
3889
|
-
const auth = this._internal._auth;
|
|
3890
|
-
let identity = await auth.loadIdentityOrNone(aid);
|
|
3891
|
-
if (!identity) {
|
|
3892
|
-
throw new StateError("no local identity found, call auth.registerAid() first");
|
|
3893
|
-
}
|
|
3894
|
-
const cachedToken = String(identity.access_token ?? "");
|
|
3895
|
-
const expiresAt = auth.getAccessTokenExpiry(identity);
|
|
3896
|
-
if (cachedToken && (expiresAt === null || expiresAt > Date.now() / 1e3 + 30)) {
|
|
3897
|
-
return cachedToken;
|
|
3898
|
-
}
|
|
3899
|
-
if (typeof auth.refreshCachedTokens === "function" && identity.refresh_token) {
|
|
3900
|
-
try {
|
|
3901
|
-
identity = await auth.refreshCachedTokens(gatewayUrl, identity);
|
|
3902
|
-
const refreshedToken = String(identity.access_token ?? "");
|
|
3903
|
-
const refreshedExpiry = auth.getAccessTokenExpiry(identity);
|
|
3904
|
-
if (refreshedToken && (refreshedExpiry === null || refreshedExpiry > Date.now() / 1e3 + 30)) {
|
|
3905
|
-
return refreshedToken;
|
|
3906
|
-
}
|
|
3907
|
-
} catch {
|
|
3908
|
-
}
|
|
3909
|
-
}
|
|
3910
|
-
const result = await this.authenticate({ aid });
|
|
3911
|
-
const token = String(result.access_token ?? "");
|
|
3912
|
-
if (!token) {
|
|
3913
|
-
throw new StateError("authenticate did not return access_token");
|
|
3914
|
-
}
|
|
3915
|
-
return token;
|
|
3916
|
-
}
|
|
3917
|
-
async uploadAgentMd(content) {
|
|
3918
|
-
const tStart = Date.now();
|
|
3919
|
-
this._log.debug(`uploadAgentMd enter: content_len=${String(content ?? "").length}`);
|
|
3920
|
-
try {
|
|
3921
|
-
const auth = this._internal._auth;
|
|
3922
|
-
const identity = await auth.loadIdentityOrNone(this._client.aid ?? void 0);
|
|
3923
|
-
if (!identity) {
|
|
3924
|
-
throw new StateError("no local identity found, call auth.registerAid() first");
|
|
3925
|
-
}
|
|
3926
|
-
const aid = String(identity.aid ?? this._client.aid ?? "").trim();
|
|
3927
|
-
if (!aid) {
|
|
3928
|
-
throw new StateError("no local identity found, call auth.registerAid() first");
|
|
3929
|
-
}
|
|
3930
|
-
const gatewayUrl = await this._resolveGateway(aid);
|
|
3931
|
-
this._client.gatewayUrl = gatewayUrl;
|
|
3932
|
-
const token = await this._ensureAgentMdUploadToken(aid, gatewayUrl);
|
|
3933
|
-
const response = await fetchWithTimeout(await this._resolveAgentMdUrl(aid), {
|
|
3934
|
-
method: "PUT",
|
|
3935
|
-
headers: {
|
|
3936
|
-
Authorization: `Bearer ${token}`,
|
|
3937
|
-
"Content-Type": "text/markdown; charset=utf-8"
|
|
3938
|
-
},
|
|
3939
|
-
body: content
|
|
3940
|
-
});
|
|
3941
|
-
if (response.status === 404) {
|
|
3942
|
-
throw new NotFoundError(`agent.md endpoint not found for aid: ${aid}`);
|
|
3943
|
-
}
|
|
3944
|
-
if (!response.ok) {
|
|
3945
|
-
const message = (await response.text()).trim();
|
|
3946
|
-
throw new AUNError(
|
|
3947
|
-
`upload agent.md failed: HTTP ${response.status}${message ? ` - ${message}` : ""}`
|
|
3948
|
-
);
|
|
3949
|
-
}
|
|
3950
|
-
const payload = await response.json();
|
|
3951
|
-
if (!isJsonObject(payload)) {
|
|
3952
|
-
throw new AUNError("upload agent.md returned invalid JSON payload");
|
|
3953
|
-
}
|
|
3954
|
-
this._log.debug(`uploadAgentMd exit: elapsed=${Date.now() - tStart}ms aid=${aid}`);
|
|
3955
|
-
return payload;
|
|
3956
|
-
} catch (err) {
|
|
3957
|
-
this._log.debug(`uploadAgentMd exit (error): elapsed=${Date.now() - tStart}ms err=${err instanceof Error ? err.message : String(err)}`);
|
|
3958
|
-
throw err;
|
|
3959
|
-
}
|
|
3960
|
-
}
|
|
3961
|
-
async headAgentMd(aid) {
|
|
3962
|
-
const tStart = Date.now();
|
|
3963
|
-
const targetAid = String(aid ?? "").trim();
|
|
3964
|
-
if (!targetAid) {
|
|
3965
|
-
throw new ValidationError("headAgentMd requires non-empty aid");
|
|
3966
|
-
}
|
|
3967
|
-
this._log.debug(`headAgentMd enter: aid=${targetAid}`);
|
|
3968
|
-
try {
|
|
3969
|
-
const response = await fetchWithTimeout(await this._resolveAgentMdUrl(targetAid), {
|
|
3970
|
-
method: "HEAD",
|
|
3971
|
-
headers: { Accept: "text/markdown" }
|
|
3972
|
-
});
|
|
3973
|
-
const respHeaders = response.headers;
|
|
3974
|
-
const etag = respHeaders ? String(respHeaders.get("ETag") ?? "").trim() : "";
|
|
3975
|
-
const lastModified = respHeaders ? String(respHeaders.get("Last-Modified") ?? "").trim() : "";
|
|
3976
|
-
if (response.status === 404) {
|
|
3977
|
-
this._log.debug(`headAgentMd exit (not_found): elapsed=${Date.now() - tStart}ms aid=${targetAid}`);
|
|
3978
|
-
return { aid: targetAid, found: false, etag: "", last_modified: "", status: 404 };
|
|
3979
|
-
}
|
|
3980
|
-
if (response.status < 200 || response.status >= 300) {
|
|
3981
|
-
throw new AUNError(`head agent.md failed: HTTP ${response.status}`);
|
|
3982
|
-
}
|
|
3983
|
-
if (etag || lastModified) {
|
|
3984
|
-
const cached = this._agentMdCache.get(targetAid) ?? { text: "", etag: "", lastModified: "" };
|
|
3985
|
-
this._agentMdCache.set(targetAid, {
|
|
3986
|
-
text: cached.text ?? "",
|
|
3987
|
-
etag,
|
|
3988
|
-
lastModified
|
|
3989
|
-
});
|
|
3990
|
-
}
|
|
3991
|
-
this._log.debug(`headAgentMd exit: elapsed=${Date.now() - tStart}ms aid=${targetAid} status=${response.status} etag=${etag || "-"}`);
|
|
3992
|
-
return { aid: targetAid, found: true, etag, last_modified: lastModified, status: response.status };
|
|
3993
|
-
} catch (err) {
|
|
3994
|
-
this._log.debug(`headAgentMd exit (error): elapsed=${Date.now() - tStart}ms aid=${targetAid} err=${err instanceof Error ? err.message : String(err)}`);
|
|
3995
|
-
throw err;
|
|
3996
|
-
}
|
|
3997
|
-
}
|
|
3998
|
-
async downloadAgentMd(aid) {
|
|
3999
|
-
const tStart = Date.now();
|
|
4000
|
-
this._log.debug(`downloadAgentMd enter: aid=${aid}`);
|
|
4001
|
-
try {
|
|
4002
|
-
const targetAid = String(aid ?? "").trim();
|
|
4003
|
-
if (!targetAid) {
|
|
4004
|
-
throw new ValidationError("downloadAgentMd requires non-empty aid");
|
|
4005
|
-
}
|
|
4006
|
-
const cached = this._agentMdCache.get(targetAid);
|
|
4007
|
-
const requestHeaders = { Accept: "text/markdown" };
|
|
4008
|
-
const agentMdUrl = await this._resolveAgentMdUrl(targetAid);
|
|
4009
|
-
const response = await fetchWithTimeout(agentMdUrl, {
|
|
4010
|
-
method: "GET",
|
|
4011
|
-
headers: requestHeaders,
|
|
4012
|
-
redirect: "follow"
|
|
4013
|
-
});
|
|
4014
|
-
if (response.status === 304) {
|
|
4015
|
-
if (cached?.text) {
|
|
4016
|
-
this._log.debug(`downloadAgentMd exit (not_modified): elapsed=${Date.now() - tStart}ms aid=${targetAid}`);
|
|
4017
|
-
return cached.text;
|
|
4018
|
-
}
|
|
4019
|
-
this._log.warn(`downloadAgentMd got 304 but no local cache, retrying unconditional GET: aid=${targetAid}`);
|
|
4020
|
-
const retryResp = await fetchWithTimeout(agentMdUrl, {
|
|
4021
|
-
method: "GET",
|
|
4022
|
-
headers: { Accept: "text/markdown" },
|
|
4023
|
-
redirect: "follow"
|
|
4024
|
-
});
|
|
4025
|
-
if (retryResp.status === 404) {
|
|
4026
|
-
throw new NotFoundError(`agent.md not found for aid: ${targetAid}`);
|
|
4027
|
-
}
|
|
4028
|
-
if (!retryResp.ok) {
|
|
4029
|
-
const message = (await retryResp.text()).trim();
|
|
4030
|
-
throw new AUNError(
|
|
4031
|
-
`download agent.md failed (retry): HTTP ${retryResp.status}${message ? ` - ${message}` : ""}`
|
|
4032
|
-
);
|
|
4033
|
-
}
|
|
4034
|
-
const retryText = await retryResp.text();
|
|
4035
|
-
this._log.debug(`downloadAgentMd exit (retry): elapsed=${Date.now() - tStart}ms aid=${targetAid} bytes=${retryText.length}`);
|
|
4036
|
-
return retryText;
|
|
4037
|
-
}
|
|
4038
|
-
if (response.status === 404) {
|
|
4039
|
-
throw new NotFoundError(`agent.md not found for aid: ${targetAid}`);
|
|
4040
|
-
}
|
|
4041
|
-
if (!response.ok) {
|
|
4042
|
-
const message = (await response.text()).trim();
|
|
4043
|
-
throw new AUNError(
|
|
4044
|
-
`download agent.md failed: HTTP ${response.status}${message ? ` - ${message}` : ""}`
|
|
4045
|
-
);
|
|
4046
|
-
}
|
|
4047
|
-
const text = await response.text();
|
|
4048
|
-
const respHeaders = response.headers;
|
|
4049
|
-
const etag = respHeaders ? String(respHeaders.get("ETag") ?? "").trim() : "";
|
|
4050
|
-
const lastModified = respHeaders ? String(respHeaders.get("Last-Modified") ?? "").trim() : "";
|
|
4051
|
-
if (etag || lastModified) {
|
|
4052
|
-
this._agentMdCache.set(targetAid, { text, etag, lastModified });
|
|
4053
|
-
}
|
|
4054
|
-
this._log.debug(`downloadAgentMd exit: elapsed=${Date.now() - tStart}ms aid=${targetAid} bytes=${text.length}`);
|
|
4055
|
-
return text;
|
|
4056
|
-
} catch (err) {
|
|
4057
|
-
this._log.debug(`downloadAgentMd exit (error): elapsed=${Date.now() - tStart}ms err=${err instanceof Error ? err.message : String(err)}`);
|
|
4058
|
-
throw err;
|
|
4059
|
-
}
|
|
4060
|
-
}
|
|
4061
|
-
/** 下载证书(透传 RPC) */
|
|
4062
|
-
async downloadCert(params) {
|
|
4063
|
-
const tStart = Date.now();
|
|
4064
|
-
this._log.debug("downloadCert enter");
|
|
4065
|
-
try {
|
|
4066
|
-
const r = await this._client.call("auth.download_cert", params ?? {});
|
|
4067
|
-
this._log.debug(`downloadCert exit: elapsed=${Date.now() - tStart}ms`);
|
|
4068
|
-
return r;
|
|
4069
|
-
} catch (err) {
|
|
4070
|
-
this._log.debug(`downloadCert exit (error): elapsed=${Date.now() - tStart}ms err=${err instanceof Error ? err.message : String(err)}`);
|
|
4071
|
-
throw err;
|
|
4072
|
-
}
|
|
4073
|
-
}
|
|
4074
|
-
/** 请求签发证书(透传 RPC) */
|
|
4075
|
-
async requestCert(params) {
|
|
4076
|
-
const tStart = Date.now();
|
|
4077
|
-
this._log.debug("requestCert enter");
|
|
4078
|
-
try {
|
|
4079
|
-
const r = await this._client.call("auth.request_cert", params);
|
|
4080
|
-
this._log.debug(`requestCert exit: elapsed=${Date.now() - tStart}ms`);
|
|
4081
|
-
return r;
|
|
4082
|
-
} catch (err) {
|
|
4083
|
-
this._log.debug(`requestCert exit (error): elapsed=${Date.now() - tStart}ms err=${err instanceof Error ? err.message : String(err)}`);
|
|
4084
|
-
throw err;
|
|
4085
|
-
}
|
|
4086
|
-
}
|
|
4087
|
-
/** 续期证书(透传 RPC) */
|
|
4088
|
-
async renewCert(params) {
|
|
4089
|
-
const tStart = Date.now();
|
|
4090
|
-
this._log.debug("renewCert enter");
|
|
4091
|
-
try {
|
|
4092
|
-
const r = await this._client.call("auth.renew_cert", params ?? {});
|
|
4093
|
-
this._log.debug(`renewCert exit: elapsed=${Date.now() - tStart}ms`);
|
|
4094
|
-
return r;
|
|
4095
|
-
} catch (err) {
|
|
4096
|
-
this._log.debug(`renewCert exit (error): elapsed=${Date.now() - tStart}ms err=${err instanceof Error ? err.message : String(err)}`);
|
|
4097
|
-
throw err;
|
|
4098
|
-
}
|
|
4099
|
-
}
|
|
4100
|
-
/** 密钥轮换(透传 RPC) */
|
|
4101
|
-
async rekey(params) {
|
|
4102
|
-
const tStart = Date.now();
|
|
4103
|
-
this._log.debug("rekey enter");
|
|
4104
|
-
try {
|
|
4105
|
-
const r = await this._client.call("auth.rekey", params ?? {});
|
|
4106
|
-
this._log.debug(`rekey exit: elapsed=${Date.now() - tStart}ms`);
|
|
4107
|
-
return r;
|
|
4108
|
-
} catch (err) {
|
|
4109
|
-
this._log.debug(`rekey exit (error): elapsed=${Date.now() - tStart}ms err=${err instanceof Error ? err.message : String(err)}`);
|
|
4110
|
-
throw err;
|
|
4111
|
-
}
|
|
4112
|
-
}
|
|
4113
|
-
/** 获取信任根(透传 RPC) */
|
|
4114
|
-
async trustRoots(params) {
|
|
4115
|
-
const tStart = Date.now();
|
|
4116
|
-
this._log.debug("trustRoots enter");
|
|
4117
|
-
try {
|
|
4118
|
-
const r = await this._client.call("meta.trust_roots", params ?? {});
|
|
4119
|
-
this._log.debug(`trustRoots exit: elapsed=${Date.now() - tStart}ms`);
|
|
4120
|
-
return r;
|
|
4121
|
-
} catch (err) {
|
|
4122
|
-
this._log.debug(`trustRoots exit (error): elapsed=${Date.now() - tStart}ms err=${err instanceof Error ? err.message : String(err)}`);
|
|
4123
|
-
throw err;
|
|
4124
|
-
}
|
|
4125
|
-
}
|
|
4126
|
-
/**
|
|
4127
|
-
* 检查指定 AID 的本地和远程状态。
|
|
4128
|
-
* 与 Python SDK namespaces/auth_namespace.py:check_aid 对应。
|
|
4129
|
-
*/
|
|
4130
|
-
async checkAid(params) {
|
|
4131
|
-
const tStart = Date.now();
|
|
4132
|
-
const aid = String(params?.aid ?? "").trim();
|
|
4133
|
-
if (!aid) throw new ValidationError("auth.check_aid requires 'aid'");
|
|
4134
|
-
this._log.debug(`checkAid enter: aid=${aid}`);
|
|
4135
|
-
try {
|
|
4136
|
-
const result = await this._checkLocalAid(aid);
|
|
4137
|
-
const local = result.local;
|
|
4138
|
-
if (!local?.complete) {
|
|
4139
|
-
const remote = await this._checkRemoteAidRegistration(aid);
|
|
4140
|
-
result.remote = remote;
|
|
4141
|
-
const remoteStatus = remote?.status;
|
|
4142
|
-
if (remoteStatus === "available") {
|
|
4143
|
-
result.status = "available";
|
|
4144
|
-
result.can_register = true;
|
|
4145
|
-
} else if (remoteStatus === "registered") {
|
|
4146
|
-
result.status = "registered_remote";
|
|
4147
|
-
result.can_register = false;
|
|
4148
|
-
} else {
|
|
4149
|
-
result.status = "unknown";
|
|
4150
|
-
result.can_register = false;
|
|
4151
|
-
}
|
|
4152
|
-
}
|
|
4153
|
-
this._log.debug(`checkAid exit: elapsed=${Date.now() - tStart}ms aid=${aid} status=${String(result.status)}`);
|
|
4154
|
-
return result;
|
|
4155
|
-
} catch (err) {
|
|
4156
|
-
this._log.debug(`checkAid exit (error): elapsed=${Date.now() - tStart}ms aid=${aid} err=${err instanceof Error ? err.message : String(err)}`);
|
|
4157
|
-
throw err;
|
|
4158
|
-
}
|
|
4159
|
-
}
|
|
4160
|
-
async _checkLocalAid(aid) {
|
|
4161
|
-
const client = this._internal;
|
|
4162
|
-
const ks = client._keystore;
|
|
4163
|
-
const identity = await client._auth.loadIdentityOrNone(aid);
|
|
4164
|
-
let keyPair = null;
|
|
4165
|
-
let keyError = "";
|
|
4166
|
-
try {
|
|
4167
|
-
if (ks && typeof ks.loadKeyPair === "function") {
|
|
4168
|
-
keyPair = await ks.loadKeyPair(aid);
|
|
4169
|
-
}
|
|
4170
|
-
} catch (e) {
|
|
4171
|
-
keyError = e instanceof Error ? e.message : String(e);
|
|
4172
|
-
}
|
|
4173
|
-
let certPem = null;
|
|
4174
|
-
let certError = "";
|
|
4175
|
-
try {
|
|
4176
|
-
if (ks && typeof ks.loadCert === "function") {
|
|
4177
|
-
certPem = await ks.loadCert(aid);
|
|
4178
|
-
}
|
|
4179
|
-
} catch (e) {
|
|
4180
|
-
certError = e instanceof Error ? e.message : String(e);
|
|
4181
|
-
}
|
|
4182
|
-
const privateKeyPresent = !!(keyPair && keyPair.private_key_pem);
|
|
4183
|
-
const publicKeyPresent = !!(keyPair && keyPair.public_key_der_b64);
|
|
4184
|
-
const certPresent = !!certPem;
|
|
4185
|
-
const certInfo = certPresent ? await this._inspectCertBrowser(aid, certPem) : { present: false, valid: false, expired: false };
|
|
4186
|
-
const certValid = !!certInfo.valid;
|
|
4187
|
-
const localComplete = privateKeyPresent && publicKeyPresent && certPresent && certValid;
|
|
4188
|
-
const issues = [];
|
|
4189
|
-
if (!identity) issues.push("local identity not found");
|
|
4190
|
-
if (!privateKeyPresent) issues.push("private key missing");
|
|
4191
|
-
if (!publicKeyPresent) issues.push("public key missing");
|
|
4192
|
-
if (!certPresent) {
|
|
4193
|
-
issues.push("certificate missing");
|
|
4194
|
-
} else if (certInfo.parse_error) {
|
|
4195
|
-
issues.push(`certificate invalid: ${certInfo.parse_error}`);
|
|
4196
|
-
} else if (certInfo.expired) {
|
|
4197
|
-
issues.push("certificate expired");
|
|
4198
|
-
} else if (!certValid) {
|
|
4199
|
-
issues.push("certificate not currently valid");
|
|
4200
|
-
}
|
|
4201
|
-
if (keyError) issues.push(`key load error: ${keyError}`);
|
|
4202
|
-
if (certError) issues.push(`certificate load error: ${certError}`);
|
|
4203
|
-
return {
|
|
4204
|
-
aid,
|
|
4205
|
-
status: localComplete ? "local_ready" : "local_incomplete",
|
|
4206
|
-
can_register: localComplete ? false : null,
|
|
4207
|
-
local: {
|
|
4208
|
-
exists: identity !== null,
|
|
4209
|
-
complete: localComplete,
|
|
4210
|
-
private_key: privateKeyPresent,
|
|
4211
|
-
public_key: publicKeyPresent,
|
|
4212
|
-
certificate: certInfo,
|
|
4213
|
-
issues
|
|
4214
|
-
},
|
|
4215
|
-
remote: {
|
|
4216
|
-
status: localComplete ? "not_checked" : "pending"
|
|
4217
|
-
}
|
|
4218
|
-
};
|
|
4219
|
-
}
|
|
4220
|
-
/** 浏览器环境证书检查(无 node:crypto X509Certificate) */
|
|
4221
|
-
async _inspectCertBrowser(aid, certPem) {
|
|
4222
|
-
const result = { present: true, valid: false, expired: false };
|
|
4223
|
-
try {
|
|
4224
|
-
const fingerprint = await certificateSha256Fingerprint(certPem);
|
|
4225
|
-
const cn = extractCommonNameFromCertPem(certPem);
|
|
4226
|
-
const aidMatches = !cn || cn === aid;
|
|
4227
|
-
const validity = parseCertValidity(certPem);
|
|
4228
|
-
if (validity) {
|
|
4229
|
-
const now = Date.now();
|
|
4230
|
-
const valid = now >= validity.notBefore && now <= validity.notAfter && aidMatches;
|
|
4231
|
-
const expired = now > validity.notAfter;
|
|
4232
|
-
result.valid = valid;
|
|
4233
|
-
result.expired = expired;
|
|
4234
|
-
result.not_before = new Date(validity.notBefore).toISOString();
|
|
4235
|
-
result.not_after = new Date(validity.notAfter).toISOString();
|
|
4236
|
-
result.expires_at = Math.floor(validity.notAfter / 1e3);
|
|
4237
|
-
result.seconds_until_expiry = Math.floor((validity.notAfter - now) / 1e3);
|
|
4238
|
-
} else {
|
|
4239
|
-
result.valid = aidMatches;
|
|
4240
|
-
}
|
|
4241
|
-
result.fingerprint = fingerprint;
|
|
4242
|
-
result.subject_cn = cn;
|
|
4243
|
-
result.aid_matches = aidMatches;
|
|
4244
|
-
if (cn && cn !== aid) {
|
|
4245
|
-
result.valid = false;
|
|
4246
|
-
result.parse_error = `certificate CN mismatch: ${cn}`;
|
|
4247
|
-
}
|
|
4248
|
-
} catch (e) {
|
|
4249
|
-
result.parse_error = e instanceof Error ? e.message : String(e);
|
|
4250
|
-
}
|
|
4251
|
-
return result;
|
|
4252
|
-
}
|
|
4253
|
-
async _checkRemoteAidRegistration(aid) {
|
|
4254
|
-
try {
|
|
4255
|
-
const content = await this.downloadAgentMd(aid);
|
|
4256
|
-
return {
|
|
4257
|
-
status: "registered",
|
|
4258
|
-
registered: true,
|
|
4259
|
-
available: false,
|
|
4260
|
-
source: "agent.md",
|
|
4261
|
-
agent_md_bytes: new TextEncoder().encode(content).length,
|
|
4262
|
-
agent_md_aid: extractAgentMDAid(content)
|
|
4263
|
-
};
|
|
4264
|
-
} catch (err) {
|
|
4265
|
-
if (err instanceof NotFoundError) {
|
|
4266
|
-
return {
|
|
4267
|
-
status: "available",
|
|
4268
|
-
registered: false,
|
|
4269
|
-
available: true,
|
|
4270
|
-
source: "agent.md"
|
|
4271
|
-
};
|
|
4272
|
-
}
|
|
4273
|
-
return {
|
|
4274
|
-
status: "unknown",
|
|
4275
|
-
registered: null,
|
|
4276
|
-
available: null,
|
|
4277
|
-
source: "agent.md",
|
|
4278
|
-
error: err instanceof Error ? err.message : String(err)
|
|
4279
|
-
};
|
|
4280
|
-
}
|
|
4281
|
-
}
|
|
4282
|
-
};
|
|
4283
|
-
|
|
4284
|
-
// src/namespaces/custody.ts
|
|
4285
|
-
var _noopLog6 = { error: () => {
|
|
4286
|
-
}, warn: () => {
|
|
4287
|
-
}, info: () => {
|
|
4288
|
-
}, debug: () => {
|
|
4289
|
-
} };
|
|
4290
|
-
var CUSTODY_HTTP_TIMEOUT_MS = 3e4;
|
|
4291
|
-
function issuerDomainFromAid(aid) {
|
|
4292
|
-
const parts = String(aid || "").trim().split(".", 2);
|
|
4293
|
-
return parts.length > 1 ? parts[1] : parts[0] || "";
|
|
4294
|
-
}
|
|
4295
|
-
function custodyWellKnownUrls(aid, discoveryPort, verifySsl) {
|
|
4296
|
-
const portSuffix = discoveryPort ? `:${discoveryPort}` : "";
|
|
4297
|
-
const issuerDomain = issuerDomainFromAid(aid);
|
|
4298
|
-
const aidUrl = `https://${aid}${portSuffix}/.well-known/aun-custody`;
|
|
4299
|
-
const fallbackUrl = `https://aid_custody.${issuerDomain}${portSuffix}/.well-known/aun-custody`;
|
|
4300
|
-
const urls = verifySsl ? [aidUrl, fallbackUrl] : [fallbackUrl, aidUrl];
|
|
4301
|
-
return [...new Set(urls)];
|
|
4302
|
-
}
|
|
4303
|
-
function extractCustodyUrl(payload) {
|
|
4304
|
-
for (const key of ["custody_url", "custodyUrl", "url"]) {
|
|
4305
|
-
const value = String(payload[key] ?? "").trim();
|
|
4306
|
-
if (value) return value;
|
|
4307
|
-
}
|
|
4308
|
-
if (isJsonObject(payload.custody)) {
|
|
4309
|
-
const value = String(payload.custody.url ?? "").trim();
|
|
4310
|
-
if (value) return value;
|
|
4311
|
-
}
|
|
4312
|
-
for (const key of ["custody_services", "custodyServices", "services"]) {
|
|
4313
|
-
const items = payload[key];
|
|
4314
|
-
if (Array.isArray(items)) {
|
|
4315
|
-
const candidates = items.filter(isJsonObject).sort((a, b) => Number(a.priority ?? 999) - Number(b.priority ?? 999));
|
|
4316
|
-
for (const item of candidates) {
|
|
4317
|
-
const value = String(item.url ?? "").trim();
|
|
4318
|
-
if (value) return value;
|
|
4319
|
-
}
|
|
4320
|
-
}
|
|
4321
|
-
}
|
|
4322
|
-
throw new ValidationError("custody well-known missing custody url");
|
|
4323
|
-
}
|
|
4324
|
-
function normalizeCustodyUrl(url) {
|
|
4325
|
-
const value = String(url ?? "").trim().replace(/\/+$/, "");
|
|
4326
|
-
if (!value) return null;
|
|
4327
|
-
try {
|
|
4328
|
-
const parsed = new URL(value);
|
|
4329
|
-
if (parsed.protocol !== "http:" && parsed.protocol !== "https:" || !parsed.hostname) {
|
|
4330
|
-
return null;
|
|
4331
|
-
}
|
|
4332
|
-
return value;
|
|
4333
|
-
} catch {
|
|
4334
|
-
return null;
|
|
4335
|
-
}
|
|
4336
|
-
}
|
|
4337
|
-
async function fetchJsonWithTimeout(input, init, timeoutMs = CUSTODY_HTTP_TIMEOUT_MS) {
|
|
4338
|
-
const controller = new AbortController();
|
|
4339
|
-
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
4340
|
-
try {
|
|
4341
|
-
const response = await fetch(input, { ...init, signal: controller.signal });
|
|
4342
|
-
const payload = await response.json();
|
|
4343
|
-
if (response.ok) {
|
|
4344
|
-
return payload;
|
|
4345
|
-
}
|
|
4346
|
-
const error = isJsonObject(payload) && isJsonObject(payload.error) ? payload.error : null;
|
|
4347
|
-
const code = String(error?.code ?? "");
|
|
4348
|
-
const message = String(error?.message ?? "");
|
|
4349
|
-
throw new AUNError(
|
|
4350
|
-
message ? `custody ${code || response.status}: ${message}` : `custody HTTP ${response.status}`
|
|
4351
|
-
);
|
|
4352
|
-
} catch (error) {
|
|
4353
|
-
if (controller.signal.aborted) {
|
|
4354
|
-
throw new AUNError(`custody request timed out after ${timeoutMs}ms`);
|
|
4355
|
-
}
|
|
4356
|
-
throw error;
|
|
4357
|
-
} finally {
|
|
4358
|
-
clearTimeout(timer);
|
|
4359
|
-
}
|
|
4360
|
-
}
|
|
4361
|
-
var CustodyNamespace = class {
|
|
4362
|
-
constructor(client) {
|
|
4363
|
-
__publicField(this, "_client");
|
|
4364
|
-
__publicField(this, "_custodyUrl", "");
|
|
4365
|
-
__publicField(this, "_log", _noopLog6);
|
|
4366
|
-
this._client = client;
|
|
4367
|
-
}
|
|
4368
|
-
setLogger(log) {
|
|
4369
|
-
this._log = log;
|
|
4370
|
-
}
|
|
4371
|
-
get _internal() {
|
|
4372
|
-
return this._client;
|
|
4373
|
-
}
|
|
4374
|
-
setUrl(url) {
|
|
4375
|
-
this._custodyUrl = String(url ?? "").trim().replace(/\/+$/, "");
|
|
4376
|
-
}
|
|
4377
|
-
configureUrl(url) {
|
|
4378
|
-
this.setUrl(url);
|
|
4379
|
-
}
|
|
4380
|
-
async discoverUrl(params = {}) {
|
|
4381
|
-
const tStart = Date.now();
|
|
4382
|
-
this._log.debug(`discoverUrl enter: aid=${params.aid ?? "<current>"}`);
|
|
4383
|
-
try {
|
|
4384
|
-
const aid = String(params.aid ?? this._client.aid ?? "").trim();
|
|
4385
|
-
if (!aid) {
|
|
4386
|
-
throw new ValidationError("custody.discoverUrl requires aid or authenticated client");
|
|
4387
|
-
}
|
|
4388
|
-
let lastError = null;
|
|
4389
|
-
const urls = custodyWellKnownUrls(aid, this._client.configModel.discoveryPort, this._client.configModel.verifySsl);
|
|
4390
|
-
for (const wellKnownUrl of urls) {
|
|
4391
|
-
try {
|
|
4392
|
-
const payload = await fetchJsonWithTimeout(wellKnownUrl, { method: "GET" }, params.timeout ?? 5e3);
|
|
4393
|
-
if (!isJsonObject(payload)) {
|
|
4394
|
-
throw new ValidationError("custody well-known returned invalid payload");
|
|
4395
|
-
}
|
|
4396
|
-
const custodyUrl = normalizeCustodyUrl(extractCustodyUrl(payload));
|
|
4397
|
-
if (!custodyUrl) {
|
|
4398
|
-
throw new ValidationError("custody well-known returned invalid custody url");
|
|
4399
|
-
}
|
|
4400
|
-
this._custodyUrl = custodyUrl;
|
|
4401
|
-
this._log.debug(`discoverUrl exit: elapsed=${Date.now() - tStart}ms aid=${aid} url=${custodyUrl}`);
|
|
4402
|
-
return custodyUrl;
|
|
4403
|
-
} catch (error) {
|
|
4404
|
-
lastError = error;
|
|
4405
|
-
}
|
|
4406
|
-
}
|
|
4407
|
-
throw new AUNError(`custody discovery failed for ${aid}: ${lastError instanceof Error ? lastError.message : String(lastError)}`);
|
|
4408
|
-
} catch (err) {
|
|
4409
|
-
this._log.debug(`discoverUrl exit (error): elapsed=${Date.now() - tStart}ms err=${err instanceof Error ? err.message : String(err)}`);
|
|
4410
|
-
throw err;
|
|
4411
|
-
}
|
|
4412
|
-
}
|
|
4413
|
-
async _resolveCustodyUrl(aid) {
|
|
4414
|
-
const custodyUrl = normalizeCustodyUrl(this._custodyUrl);
|
|
4415
|
-
if (custodyUrl) {
|
|
4416
|
-
if (custodyUrl !== this._custodyUrl) {
|
|
4417
|
-
this._custodyUrl = custodyUrl;
|
|
4418
|
-
}
|
|
4419
|
-
return custodyUrl;
|
|
4420
|
-
}
|
|
4421
|
-
return this.discoverUrl({ aid });
|
|
4422
|
-
}
|
|
4423
|
-
_getAccessToken() {
|
|
4424
|
-
const identity = this._internal._identity;
|
|
4425
|
-
if (identity) {
|
|
4426
|
-
const token = String(identity.access_token ?? "").trim();
|
|
4427
|
-
if (token) return token;
|
|
4428
|
-
}
|
|
4429
|
-
throw new ValidationError("no access_token available: call auth.authenticate() first");
|
|
4430
|
-
}
|
|
4431
|
-
async _post(path, body, opts = {}) {
|
|
4432
|
-
const headers = { "Content-Type": "application/json" };
|
|
4433
|
-
const token = String(opts.token ?? "").trim();
|
|
4434
|
-
if (token) {
|
|
4435
|
-
headers.Authorization = `Bearer ${token}`;
|
|
4436
|
-
}
|
|
4437
|
-
const payload = await fetchJsonWithTimeout(
|
|
4438
|
-
`${await this._resolveCustodyUrl(String(body.aid ?? "") || this._client.aid)}${path}`,
|
|
4439
|
-
{
|
|
4440
|
-
method: "POST",
|
|
4441
|
-
headers,
|
|
4442
|
-
body: JSON.stringify(body)
|
|
4443
|
-
}
|
|
4444
|
-
);
|
|
4445
|
-
if (!isJsonObject(payload)) {
|
|
4446
|
-
throw new AUNError("custody returned invalid JSON payload");
|
|
4447
|
-
}
|
|
4448
|
-
return payload;
|
|
4449
|
-
}
|
|
4450
|
-
async sendCode(params) {
|
|
4451
|
-
const tStart = Date.now();
|
|
4452
|
-
this._log.debug(`sendCode enter: aid=${params.aid ?? "<current>"}`);
|
|
4453
|
-
try {
|
|
4454
|
-
const phone = String(params.phone ?? "").trim();
|
|
4455
|
-
const aid = String(params.aid ?? "").trim();
|
|
4456
|
-
if (!phone) {
|
|
4457
|
-
throw new ValidationError("custody.sendCode requires non-empty phone");
|
|
4458
|
-
}
|
|
4459
|
-
const body = { phone };
|
|
4460
|
-
let token = null;
|
|
4461
|
-
if (aid) {
|
|
4462
|
-
body.aid = aid;
|
|
4463
|
-
} else {
|
|
4464
|
-
token = this._getAccessToken();
|
|
4465
|
-
}
|
|
4466
|
-
const r = await this._post("/custody/accounts/send-code", body, { token });
|
|
4467
|
-
this._log.debug(`sendCode exit: elapsed=${Date.now() - tStart}ms`);
|
|
4468
|
-
return r;
|
|
4469
|
-
} catch (err) {
|
|
4470
|
-
this._log.debug(`sendCode exit (error): elapsed=${Date.now() - tStart}ms err=${err instanceof Error ? err.message : String(err)}`);
|
|
4471
|
-
throw err;
|
|
4472
|
-
}
|
|
4473
|
-
}
|
|
4474
|
-
async bindPhone(params) {
|
|
4475
|
-
const tStart = Date.now();
|
|
4476
|
-
this._log.debug("bindPhone enter");
|
|
4477
|
-
try {
|
|
4478
|
-
const phone = String(params.phone ?? "").trim();
|
|
4479
|
-
const code = String(params.code ?? "").trim();
|
|
4480
|
-
const cert = String(params.cert ?? "").trim();
|
|
4481
|
-
const key = String(params.key ?? "").trim();
|
|
4482
|
-
if (!phone || !code || !cert || !key) {
|
|
4483
|
-
throw new ValidationError("custody.bindPhone requires phone, code, cert and key");
|
|
4484
|
-
}
|
|
4485
|
-
const body = { phone, code, cert, key };
|
|
4486
|
-
if (params.metadata && isJsonObject(params.metadata)) {
|
|
4487
|
-
body.metadata = params.metadata;
|
|
4488
|
-
}
|
|
4489
|
-
const r = await this._post("/custody/accounts/bind-phone", body, {
|
|
4490
|
-
token: this._getAccessToken()
|
|
4491
|
-
});
|
|
4492
|
-
this._log.debug(`bindPhone exit: elapsed=${Date.now() - tStart}ms`);
|
|
4493
|
-
return r;
|
|
4494
|
-
} catch (err) {
|
|
4495
|
-
this._log.debug(`bindPhone exit (error): elapsed=${Date.now() - tStart}ms err=${err instanceof Error ? err.message : String(err)}`);
|
|
4496
|
-
throw err;
|
|
4497
|
-
}
|
|
4498
|
-
}
|
|
4499
|
-
async restorePhone(params) {
|
|
4500
|
-
const tStart = Date.now();
|
|
4501
|
-
this._log.debug(`restorePhone enter: aid=${params.aid}`);
|
|
4502
|
-
try {
|
|
4503
|
-
const phone = String(params.phone ?? "").trim();
|
|
4504
|
-
const code = String(params.code ?? "").trim();
|
|
4505
|
-
const aid = String(params.aid ?? "").trim();
|
|
4506
|
-
if (!phone || !code || !aid) {
|
|
4507
|
-
throw new ValidationError("custody.restorePhone requires phone, code and aid");
|
|
4508
|
-
}
|
|
4509
|
-
const r = await this._post("/custody/accounts/restore-phone", { phone, code, aid });
|
|
4510
|
-
this._log.debug(`restorePhone exit: elapsed=${Date.now() - tStart}ms aid=${aid}`);
|
|
4511
|
-
return r;
|
|
4512
|
-
} catch (err) {
|
|
4513
|
-
this._log.debug(`restorePhone exit (error): elapsed=${Date.now() - tStart}ms err=${err instanceof Error ? err.message : String(err)}`);
|
|
4514
|
-
throw err;
|
|
4515
|
-
}
|
|
4516
|
-
}
|
|
4517
|
-
async createDeviceCopy(params = {}) {
|
|
4518
|
-
const tStart = Date.now();
|
|
4519
|
-
this._log.debug(`createDeviceCopy enter: aid=${params.aid ?? "<current>"}`);
|
|
4520
|
-
try {
|
|
4521
|
-
const aid = String(params.aid ?? this._client.aid ?? "").trim();
|
|
4522
|
-
if (!aid) {
|
|
4523
|
-
throw new ValidationError("custody.createDeviceCopy requires aid or authenticated client");
|
|
4524
|
-
}
|
|
4525
|
-
const r = await this._post("/custody/transfers", { aid }, { token: this._getAccessToken() });
|
|
4526
|
-
this._log.debug(`createDeviceCopy exit: elapsed=${Date.now() - tStart}ms aid=${aid}`);
|
|
4527
|
-
return r;
|
|
4528
|
-
} catch (err) {
|
|
4529
|
-
this._log.debug(`createDeviceCopy exit (error): elapsed=${Date.now() - tStart}ms err=${err instanceof Error ? err.message : String(err)}`);
|
|
4530
|
-
throw err;
|
|
4531
|
-
}
|
|
4532
|
-
}
|
|
4533
|
-
async uploadDeviceCopyMaterials(params) {
|
|
4534
|
-
const tStart = Date.now();
|
|
4535
|
-
this._log.debug(`uploadDeviceCopyMaterials enter: aid=${params.aid ?? "<current>"}`);
|
|
4536
|
-
try {
|
|
4537
|
-
const transferCode = String(params.transferCode ?? "").trim();
|
|
4538
|
-
const aid = String(params.aid ?? this._client.aid ?? "").trim();
|
|
4539
|
-
const cert = String(params.cert ?? "").trim();
|
|
4540
|
-
const key = String(params.key ?? "").trim();
|
|
4541
|
-
if (!transferCode || !aid || !cert || !key) {
|
|
4542
|
-
throw new ValidationError("custody.uploadDeviceCopyMaterials requires transferCode, aid, cert and key");
|
|
4543
|
-
}
|
|
4544
|
-
const body = { aid, cert, key };
|
|
4545
|
-
if (params.metadata && isJsonObject(params.metadata)) {
|
|
4546
|
-
body.metadata = params.metadata;
|
|
4547
|
-
}
|
|
4548
|
-
const r = await this._post(`/custody/transfers/${encodeURIComponent(transferCode)}/materials`, body, {
|
|
4549
|
-
token: this._getAccessToken()
|
|
4550
|
-
});
|
|
4551
|
-
this._log.debug(`uploadDeviceCopyMaterials exit: elapsed=${Date.now() - tStart}ms aid=${aid}`);
|
|
4552
|
-
return r;
|
|
4553
|
-
} catch (err) {
|
|
4554
|
-
this._log.debug(`uploadDeviceCopyMaterials exit (error): elapsed=${Date.now() - tStart}ms err=${err instanceof Error ? err.message : String(err)}`);
|
|
4555
|
-
throw err;
|
|
4556
|
-
}
|
|
4557
|
-
}
|
|
4558
|
-
async claimDeviceCopy(params) {
|
|
4559
|
-
const tStart = Date.now();
|
|
4560
|
-
this._log.debug(`claimDeviceCopy enter: aid=${params.aid}`);
|
|
4561
|
-
try {
|
|
4562
|
-
const aid = String(params.aid ?? "").trim();
|
|
4563
|
-
const transferCode = String(params.transferCode ?? "").trim();
|
|
4564
|
-
if (!aid || !transferCode) {
|
|
4565
|
-
throw new ValidationError("custody.claimDeviceCopy requires aid and transferCode");
|
|
4566
|
-
}
|
|
4567
|
-
const r = await this._post("/custody/transfers/claim", { aid, transfer_code: transferCode });
|
|
4568
|
-
this._log.debug(`claimDeviceCopy exit: elapsed=${Date.now() - tStart}ms aid=${aid}`);
|
|
4569
|
-
return r;
|
|
4570
|
-
} catch (err) {
|
|
4571
|
-
this._log.debug(`claimDeviceCopy exit (error): elapsed=${Date.now() - tStart}ms err=${err instanceof Error ? err.message : String(err)}`);
|
|
4572
|
-
throw err;
|
|
4573
|
-
}
|
|
4574
|
-
}
|
|
4575
|
-
};
|
|
4576
|
-
|
|
4577
|
-
// src/namespaces/meta.ts
|
|
4578
|
-
init_crypto();
|
|
4579
|
-
var _noopLog7 = { error: () => {
|
|
4580
|
-
}, warn: () => {
|
|
4581
|
-
}, info: () => {
|
|
4582
|
-
}, debug: () => {
|
|
4583
|
-
} };
|
|
4584
|
-
var AUTHORITY_ENDPOINT = "https://trust.aun.network/.well-known/aun/trust-roots.json";
|
|
4585
|
-
var MAX_CLOCK_SKEW = 300;
|
|
4586
|
-
var MetaNamespace = class _MetaNamespace {
|
|
4587
|
-
constructor(client) {
|
|
4588
|
-
__publicField(this, "_log", _noopLog7);
|
|
4589
|
-
__publicField(this, "_client");
|
|
4590
|
-
this._client = client;
|
|
4591
|
-
}
|
|
4592
|
-
setLogger(log) {
|
|
4593
|
-
this._log = log;
|
|
4594
|
-
}
|
|
4595
|
-
get _internal() {
|
|
4596
|
-
return this._client;
|
|
4597
|
-
}
|
|
4598
|
-
// ── RPC 直通 ──────────────────────────────────────────────
|
|
4599
|
-
async ping(params) {
|
|
4600
|
-
const tStart = Date.now();
|
|
4601
|
-
this._log.debug("ping enter");
|
|
4602
|
-
try {
|
|
4603
|
-
const r = await this._client.call("meta.ping", params ?? {});
|
|
4604
|
-
this._log.debug(`ping exit: elapsed=${Date.now() - tStart}ms`);
|
|
4605
|
-
return r;
|
|
4606
|
-
} catch (err) {
|
|
4607
|
-
this._log.debug(`ping exit (error): elapsed=${Date.now() - tStart}ms err=${err instanceof Error ? err.message : String(err)}`);
|
|
4608
|
-
throw err;
|
|
4609
|
-
}
|
|
4610
|
-
}
|
|
4611
|
-
async status(params) {
|
|
4612
|
-
const tStart = Date.now();
|
|
4613
|
-
this._log.debug("status enter");
|
|
4614
|
-
try {
|
|
4615
|
-
const r = await this._client.call("meta.status", params ?? {});
|
|
4616
|
-
this._log.debug(`status exit: elapsed=${Date.now() - tStart}ms`);
|
|
4617
|
-
return r;
|
|
4618
|
-
} catch (err) {
|
|
4619
|
-
this._log.debug(`status exit (error): elapsed=${Date.now() - tStart}ms err=${err instanceof Error ? err.message : String(err)}`);
|
|
4620
|
-
throw err;
|
|
4621
|
-
}
|
|
4622
|
-
}
|
|
4623
|
-
async trustRoots(params) {
|
|
4624
|
-
const tStart = Date.now();
|
|
4625
|
-
this._log.debug("trustRoots enter");
|
|
4626
|
-
try {
|
|
4627
|
-
const r = await this._client.call("meta.trust_roots", params ?? {});
|
|
4628
|
-
this._log.debug(`trustRoots exit: elapsed=${Date.now() - tStart}ms`);
|
|
4629
|
-
return r;
|
|
4630
|
-
} catch (err) {
|
|
4631
|
-
this._log.debug(`trustRoots exit (error): elapsed=${Date.now() - tStart}ms err=${err instanceof Error ? err.message : String(err)}`);
|
|
4632
|
-
throw err;
|
|
4633
|
-
}
|
|
4634
|
-
}
|
|
4635
|
-
// ── 信任根下载 ────────────────────────────────────────────
|
|
4636
|
-
async downloadTrustRoots(opts) {
|
|
4637
|
-
const tStart = Date.now();
|
|
4638
|
-
this._log.debug(`downloadTrustRoots enter: issuer=${opts?.issuer ?? "-"}`);
|
|
4639
|
-
try {
|
|
4640
|
-
const target = this._resolveTrustRootsUrl(
|
|
4641
|
-
opts?.url,
|
|
4642
|
-
opts?.issuer,
|
|
4643
|
-
opts?.gateway_url
|
|
4644
|
-
);
|
|
4645
|
-
if (!target.toLowerCase().startsWith("https://") && !target.toLowerCase().startsWith("http://")) {
|
|
4646
|
-
throw new ValidationError("trust roots url must be http(s)");
|
|
4647
|
-
}
|
|
4648
|
-
const timeoutMs = (opts?.timeout ?? 10) * 1e3;
|
|
4649
|
-
const controller = new AbortController();
|
|
4650
|
-
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
4651
|
-
try {
|
|
4652
|
-
const response = await fetch(target, {
|
|
4653
|
-
headers: { Accept: "application/json" },
|
|
4654
|
-
signal: controller.signal
|
|
4655
|
-
});
|
|
4656
|
-
if (!response.ok) {
|
|
4657
|
-
throw new ValidationError(`trust roots download failed: HTTP ${response.status}`);
|
|
4658
|
-
}
|
|
4659
|
-
const payload = await response.json();
|
|
4660
|
-
if (!payload || typeof payload !== "object" || Array.isArray(payload)) {
|
|
4661
|
-
throw new ValidationError("trust roots endpoint returned non-object JSON");
|
|
4662
|
-
}
|
|
4663
|
-
this._log.debug(`downloadTrustRoots exit: elapsed=${Date.now() - tStart}ms`);
|
|
4664
|
-
return payload;
|
|
4665
|
-
} finally {
|
|
4666
|
-
clearTimeout(timer);
|
|
4667
|
-
}
|
|
4668
|
-
} catch (err) {
|
|
4669
|
-
this._log.debug(`downloadTrustRoots exit (error): elapsed=${Date.now() - tStart}ms err=${err instanceof Error ? err.message : String(err)}`);
|
|
4670
|
-
throw err;
|
|
4671
|
-
}
|
|
4672
|
-
}
|
|
4673
|
-
async downloadIssuerRootCert(issuer, opts) {
|
|
4674
|
-
const tStart = Date.now();
|
|
4675
|
-
this._log.debug(`downloadIssuerRootCert enter: issuer=${issuer}`);
|
|
4676
|
-
try {
|
|
4677
|
-
const normalizedIssuer = _MetaNamespace._validateIssuer(issuer);
|
|
4678
|
-
const target = opts?.url?.trim() || this._issuerRootCertUrl(normalizedIssuer);
|
|
4679
|
-
if (!target.toLowerCase().startsWith("https://") && !target.toLowerCase().startsWith("http://")) {
|
|
4680
|
-
throw new ValidationError("issuer root certificate url must be http(s)");
|
|
4681
|
-
}
|
|
4682
|
-
const timeoutMs = (opts?.timeout ?? 10) * 1e3;
|
|
4683
|
-
const controller = new AbortController();
|
|
4684
|
-
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
4685
|
-
try {
|
|
4686
|
-
const response = await fetch(target, {
|
|
4687
|
-
headers: { Accept: "application/x-pem-file,text/plain" },
|
|
4688
|
-
signal: controller.signal
|
|
4689
|
-
});
|
|
4690
|
-
if (!response.ok) {
|
|
4691
|
-
throw new ValidationError(`issuer root cert download failed: HTTP ${response.status}`);
|
|
4692
|
-
}
|
|
4693
|
-
const certPem = (await response.text()).trim();
|
|
4694
|
-
_MetaNamespace._validateCertPem(certPem, normalizedIssuer);
|
|
4695
|
-
this._log.debug(`downloadIssuerRootCert exit: elapsed=${Date.now() - tStart}ms issuer=${normalizedIssuer}`);
|
|
4696
|
-
return certPem + "\n";
|
|
4697
|
-
} finally {
|
|
4698
|
-
clearTimeout(timer);
|
|
4699
|
-
}
|
|
4700
|
-
} catch (err) {
|
|
4701
|
-
this._log.debug(`downloadIssuerRootCert exit (error): elapsed=${Date.now() - tStart}ms err=${err instanceof Error ? err.message : String(err)}`);
|
|
4702
|
-
throw err;
|
|
4703
|
-
}
|
|
4704
|
-
}
|
|
4705
|
-
// ── 信任根验证 ────────────────────────────────────────────
|
|
4706
|
-
async verifyTrustRoots(trustList, opts) {
|
|
4707
|
-
if (!trustList || typeof trustList !== "object") {
|
|
4708
|
-
throw new ValidationError("trust roots list must be a JSON object");
|
|
4709
|
-
}
|
|
4710
|
-
const signature = String(trustList.authority_signature ?? "").trim();
|
|
4711
|
-
if (!signature && !opts?.allow_unsigned) {
|
|
4712
|
-
throw new ValidationError("trust roots list missing authority_signature");
|
|
4713
|
-
}
|
|
4714
|
-
_MetaNamespace._validateListMetadata(trustList);
|
|
4715
|
-
if (signature) {
|
|
4716
|
-
const publicKey = await this._loadAuthorityPublicKey(
|
|
4717
|
-
opts?.authority_cert_pem,
|
|
4718
|
-
opts?.authority_public_key_pem,
|
|
4719
|
-
trustList
|
|
4720
|
-
);
|
|
4721
|
-
const signedPayload = _MetaNamespace._canonicalSignedPayload(trustList);
|
|
4722
|
-
const sigBytes = _MetaNamespace._decodeSignature(signature);
|
|
4723
|
-
const ok = await crypto.subtle.verify(
|
|
4724
|
-
{ name: "ECDSA", hash: "SHA-256" },
|
|
4725
|
-
publicKey,
|
|
4726
|
-
toBufferSource(sigBytes),
|
|
4727
|
-
toBufferSource(signedPayload)
|
|
4728
|
-
);
|
|
4729
|
-
if (!ok) {
|
|
4730
|
-
throw new ValidationError("trust roots authority_signature verification failed");
|
|
4731
|
-
}
|
|
4732
|
-
}
|
|
4733
|
-
const roots = _MetaNamespace._extractRootEntries(trustList);
|
|
4734
|
-
const imported = [];
|
|
4735
|
-
const skipped = [];
|
|
4736
|
-
for (const item of roots) {
|
|
4737
|
-
const status = String(item.status ?? "active").trim().toLowerCase();
|
|
4738
|
-
const certPem = String(item.certificate ?? item.cert_pem ?? "").trim();
|
|
4739
|
-
const rootId = String(item.id ?? item.agentid ?? "").trim();
|
|
4740
|
-
if (status !== "active") {
|
|
4741
|
-
skipped.push({ id: rootId, reason: `status=${status}` });
|
|
4742
|
-
continue;
|
|
4743
|
-
}
|
|
4744
|
-
_MetaNamespace._validateCertPem(certPem, rootId || "root");
|
|
4745
|
-
const fingerprint = await _MetaNamespace._certFingerprint(certPem);
|
|
4746
|
-
const expectedFp = _MetaNamespace._normalizeFingerprint(item.fingerprint_sha256);
|
|
4747
|
-
if (expectedFp.length !== 64) {
|
|
4748
|
-
throw new ValidationError(`root certificate missing or invalid fingerprint_sha256: ${rootId || fingerprint}`);
|
|
4749
|
-
}
|
|
4750
|
-
if (expectedFp !== fingerprint) {
|
|
4751
|
-
throw new ValidationError(`root certificate fingerprint mismatch: ${rootId || fingerprint}`);
|
|
4752
|
-
}
|
|
4753
|
-
imported.push({ id: rootId || fingerprint, cert_pem: certPem, fingerprint_sha256: fingerprint });
|
|
4754
|
-
}
|
|
4755
|
-
if (imported.length === 0) {
|
|
4756
|
-
throw new ValidationError("trust roots list contains no active root certificates");
|
|
4757
|
-
}
|
|
4758
|
-
return { imported, skipped, count: imported.length };
|
|
4759
|
-
}
|
|
4760
|
-
// ── 信任根导入 ────────────────────────────────────────────
|
|
4761
|
-
async importTrustRoots(trustList, opts) {
|
|
4762
|
-
const verified = await this.verifyTrustRoots(trustList, opts);
|
|
4763
|
-
await this._enforceMonotonicVersion(trustList);
|
|
4764
|
-
const ks = this._internal._keystore;
|
|
4765
|
-
let bundlePath = "";
|
|
4766
|
-
if (typeof ks.saveTrustRoots === "function") {
|
|
4767
|
-
bundlePath = await ks.saveTrustRoots(trustList, verified.imported);
|
|
4768
|
-
} else {
|
|
4769
|
-
this._log.warn("[MetaNamespace] keystore missing saveTrustRoots method, skip persist");
|
|
4770
|
-
}
|
|
4771
|
-
const auth = this._internal._auth;
|
|
4772
|
-
const reloaded = auth.reloadTrustedRoots?.() ?? auth.reload_trusted_roots?.() ?? 0;
|
|
4773
|
-
return {
|
|
4774
|
-
imported: verified.count,
|
|
4775
|
-
skipped: verified.skipped,
|
|
4776
|
-
bundle_path: bundlePath,
|
|
4777
|
-
reloaded_roots: reloaded,
|
|
4778
|
-
fingerprints: verified.imported.map((i) => i.fingerprint_sha256)
|
|
4779
|
-
};
|
|
4780
|
-
}
|
|
4781
|
-
async refreshTrustRoots(opts) {
|
|
4782
|
-
const sourceUrl = this._resolveTrustRootsUrl(opts?.url, opts?.issuer, opts?.gateway_url);
|
|
4783
|
-
const trustList = await this.downloadTrustRoots({ url: sourceUrl, timeout: opts?.timeout });
|
|
4784
|
-
const result = await this.importTrustRoots(trustList, {
|
|
4785
|
-
authority_cert_pem: opts?.authority_cert_pem,
|
|
4786
|
-
authority_public_key_pem: opts?.authority_public_key_pem,
|
|
4787
|
-
allow_unsigned: opts?.allow_unsigned
|
|
4788
|
-
});
|
|
4789
|
-
result.source_url = sourceUrl;
|
|
4790
|
-
return result;
|
|
4791
|
-
}
|
|
4792
|
-
async updateIssuerRootCert(issuer, opts) {
|
|
4793
|
-
const normalizedIssuer = _MetaNamespace._validateIssuer(issuer);
|
|
4794
|
-
const sourceUrl = opts?.url?.trim() || this._issuerRootCertUrl(normalizedIssuer);
|
|
4795
|
-
let rootPem = opts?.cert_pem?.trim() ? opts.cert_pem.trim() + "\n" : "";
|
|
4796
|
-
if (!rootPem) {
|
|
4797
|
-
rootPem = await this.downloadIssuerRootCert(normalizedIssuer, { url: sourceUrl, timeout: opts?.timeout });
|
|
4798
|
-
}
|
|
4799
|
-
_MetaNamespace._validateCertPem(rootPem, normalizedIssuer);
|
|
4800
|
-
const fingerprint = await _MetaNamespace._certFingerprint(rootPem);
|
|
4801
|
-
let effectiveTrustList = opts?.trust_list ?? await this._loadLocalTrustList();
|
|
4802
|
-
let trustSource = "local";
|
|
4803
|
-
if (!effectiveTrustList) {
|
|
4804
|
-
effectiveTrustList = await this.downloadTrustRoots({ issuer: normalizedIssuer, timeout: opts?.timeout });
|
|
4805
|
-
trustSource = this._issuerTrustRootUrl(normalizedIssuer);
|
|
4806
|
-
}
|
|
4807
|
-
const verified = await this.verifyTrustRoots(effectiveTrustList, {
|
|
4808
|
-
authority_cert_pem: opts?.authority_cert_pem,
|
|
4809
|
-
authority_public_key_pem: opts?.authority_public_key_pem,
|
|
4810
|
-
allow_unsigned: opts?.allow_unsigned
|
|
4811
|
-
});
|
|
4812
|
-
await this._enforceMonotonicVersion(effectiveTrustList);
|
|
4813
|
-
const trustedFingerprints = new Set(verified.imported.map((i) => i.fingerprint_sha256));
|
|
4814
|
-
if (!trustedFingerprints.has(fingerprint)) {
|
|
4815
|
-
throw new ValidationError("issuer root certificate is not in trusted root list");
|
|
4816
|
-
}
|
|
4817
|
-
const ks = this._internal._keystore;
|
|
4818
|
-
let certPath = "";
|
|
4819
|
-
let bundlePath = "";
|
|
4820
|
-
if (typeof ks.saveIssuerRootCert === "function") {
|
|
4821
|
-
[certPath, bundlePath] = await ks.saveIssuerRootCert(normalizedIssuer, rootPem, fingerprint);
|
|
4822
|
-
} else {
|
|
4823
|
-
this._log.warn("[MetaNamespace] keystore missing saveIssuerRootCert method, skip persist");
|
|
4824
|
-
}
|
|
4825
|
-
const auth = this._internal._auth;
|
|
4826
|
-
const reloaded = auth.reloadTrustedRoots?.() ?? auth.reload_trusted_roots?.() ?? 0;
|
|
4827
|
-
return {
|
|
4828
|
-
issuer: normalizedIssuer,
|
|
4829
|
-
fingerprint_sha256: fingerprint,
|
|
4830
|
-
cert_path: certPath,
|
|
4831
|
-
bundle_path: bundlePath,
|
|
4832
|
-
reloaded_roots: reloaded,
|
|
4833
|
-
source_url: sourceUrl,
|
|
4834
|
-
trust_source: trustSource
|
|
4835
|
-
};
|
|
4836
|
-
}
|
|
4837
|
-
// ── URL 解析 ──────────────────────────────────────────────
|
|
4838
|
-
_resolveTrustRootsUrl(url, issuer, gatewayUrl) {
|
|
4839
|
-
const target = (url ?? "").trim();
|
|
4840
|
-
if (target) return target;
|
|
4841
|
-
if (issuer) return this._issuerTrustRootUrl(issuer);
|
|
4842
|
-
const gw = (gatewayUrl ?? this._internal._gatewayUrl ?? "").trim();
|
|
4843
|
-
return gw ? this._gatewayTrustRootsUrl(gw) : AUTHORITY_ENDPOINT;
|
|
4844
|
-
}
|
|
4845
|
-
_issuerTrustRootUrl(issuer) {
|
|
4846
|
-
const authority = this._pkiAuthority(issuer);
|
|
4847
|
-
return `https://${authority}/trust-root.json`;
|
|
4848
|
-
}
|
|
4849
|
-
_issuerRootCertUrl(issuer) {
|
|
4850
|
-
const authority = this._pkiAuthority(issuer);
|
|
4851
|
-
return `https://${authority}/root.crt`;
|
|
4852
|
-
}
|
|
4853
|
-
_pkiAuthority(issuer) {
|
|
4854
|
-
const normalized = _MetaNamespace._validateIssuer(issuer);
|
|
4855
|
-
const port = this._internal.configModel.discoveryPort;
|
|
4856
|
-
const portSuffix = port && !normalized.includes(":") ? `:${port}` : "";
|
|
4857
|
-
return `pki.${normalized}${portSuffix}`;
|
|
4858
|
-
}
|
|
4859
|
-
_gatewayTrustRootsUrl(gatewayUrl) {
|
|
4860
|
-
let parsed;
|
|
4861
|
-
try {
|
|
4862
|
-
parsed = new URL(gatewayUrl);
|
|
4863
|
-
} catch {
|
|
4864
|
-
throw new ValidationError("gateway_url must include scheme and host");
|
|
4865
|
-
}
|
|
4866
|
-
if (!parsed.host) {
|
|
4867
|
-
throw new ValidationError("gateway_url must include scheme and host");
|
|
4868
|
-
}
|
|
4869
|
-
const scheme = ["wss:", "https:"].includes(parsed.protocol) ? "https" : "http";
|
|
4870
|
-
return `${scheme}://${parsed.host}/pki/trust-roots.json`;
|
|
4871
|
-
}
|
|
4872
|
-
// ── 内部辅助 ──────────────────────────────────────────────
|
|
4873
|
-
/** 校验 issuer 域名格式 */
|
|
4874
|
-
static _validateIssuer(issuer) {
|
|
4875
|
-
const value = (issuer ?? "").trim().toLowerCase();
|
|
4876
|
-
if (!value || value.includes("://") || value.includes("/") || value.includes("\\") || value.startsWith(".")) {
|
|
4877
|
-
throw new ValidationError("issuer must be a domain name");
|
|
4878
|
-
}
|
|
4879
|
-
return value;
|
|
4880
|
-
}
|
|
4881
|
-
/** 校验信任列表元数据(版本、时间戳) */
|
|
4882
|
-
static _validateListMetadata(trustList) {
|
|
4883
|
-
const version = trustList.version;
|
|
4884
|
-
if (typeof version !== "number" || !Number.isInteger(version) || version < 0) {
|
|
4885
|
-
throw new ValidationError("trust roots list version must be a non-negative integer");
|
|
4886
|
-
}
|
|
4887
|
-
const issuedAt = _MetaNamespace._parseTimestamp(trustList.issued_at, "issued_at");
|
|
4888
|
-
const nextUpdate = _MetaNamespace._parseTimestamp(trustList.next_update, "next_update");
|
|
4889
|
-
if (nextUpdate < issuedAt) {
|
|
4890
|
-
throw new ValidationError("trust roots list next_update must not be earlier than issued_at");
|
|
4891
|
-
}
|
|
4892
|
-
if (issuedAt > Date.now() + MAX_CLOCK_SKEW * 1e3) {
|
|
4893
|
-
throw new ValidationError("trust roots list issued_at is too far in the future");
|
|
4894
|
-
}
|
|
4895
|
-
}
|
|
4896
|
-
static _parseTimestamp(value, field) {
|
|
4897
|
-
const text = String(value ?? "").trim();
|
|
4898
|
-
if (!text) throw new ValidationError(`trust roots list missing ${field}`);
|
|
4899
|
-
const ts = new Date(text).getTime();
|
|
4900
|
-
if (isNaN(ts)) throw new ValidationError(`trust roots list ${field} must be ISO-8601`);
|
|
4901
|
-
return ts;
|
|
4902
|
-
}
|
|
4903
|
-
/** 构建用于签名验证的规范化 JSON 载荷 */
|
|
4904
|
-
static _canonicalSignedPayload(trustList) {
|
|
4905
|
-
const payload = { ...trustList };
|
|
4906
|
-
delete payload.authority_signature;
|
|
4907
|
-
const canonical = _MetaNamespace._stableStringify(payload);
|
|
4908
|
-
return new TextEncoder().encode(canonical);
|
|
4909
|
-
}
|
|
4910
|
-
/** 递归稳定排序 JSON 序列化 */
|
|
4911
|
-
static _stableStringify(obj) {
|
|
4912
|
-
if (obj === null || obj === void 0) return JSON.stringify(obj);
|
|
4913
|
-
if (typeof obj !== "object") return JSON.stringify(obj);
|
|
4914
|
-
if (Array.isArray(obj)) {
|
|
4915
|
-
return "[" + obj.map((v) => _MetaNamespace._stableStringify(v)).join(",") + "]";
|
|
4916
|
-
}
|
|
4917
|
-
const keys = Object.keys(obj).sort();
|
|
4918
|
-
const pairs = keys.map((k) => JSON.stringify(k) + ":" + _MetaNamespace._stableStringify(obj[k]));
|
|
4919
|
-
return "{" + pairs.join(",") + "}";
|
|
4920
|
-
}
|
|
4921
|
-
/** 解码 base64 编码的签名 */
|
|
4922
|
-
static _decodeSignature(signature) {
|
|
4923
|
-
let value = signature.trim();
|
|
4924
|
-
if (value.startsWith("base64:")) {
|
|
4925
|
-
value = value.slice(7);
|
|
4926
|
-
}
|
|
4927
|
-
const normalized = value.replace(/-/g, "+").replace(/_/g, "/");
|
|
4928
|
-
const padding = "=".repeat((4 - normalized.length % 4) % 4);
|
|
4929
|
-
return base64ToUint8(normalized + padding);
|
|
4930
|
-
}
|
|
4931
|
-
/** 提取信任列表中的根 CA 条目 */
|
|
4932
|
-
static _extractRootEntries(trustList) {
|
|
4933
|
-
const roots = trustList.root_cas;
|
|
4934
|
-
if (Array.isArray(roots)) {
|
|
4935
|
-
return roots.filter((item) => item && typeof item === "object");
|
|
4936
|
-
}
|
|
4937
|
-
const legacy = trustList.roots;
|
|
4938
|
-
if (Array.isArray(legacy)) {
|
|
4939
|
-
return legacy.filter((item) => item && typeof item === "object").map((item) => ({
|
|
4940
|
-
id: item.agentid ?? item.id ?? item.cert_sn,
|
|
4941
|
-
certificate: item.cert_pem ?? item.certificate,
|
|
4942
|
-
fingerprint_sha256: item.fingerprint_sha256,
|
|
4943
|
-
status: item.status ?? "active"
|
|
4944
|
-
}));
|
|
4945
|
-
}
|
|
4946
|
-
throw new ValidationError("trust roots list missing root_cas");
|
|
4947
|
-
}
|
|
4948
|
-
/** 规范化指纹字符串(去除前缀和分隔符) */
|
|
4949
|
-
static _normalizeFingerprint(value) {
|
|
4950
|
-
let text = String(value ?? "").trim().toLowerCase();
|
|
4951
|
-
if (text.startsWith("sha256:")) text = text.slice(7);
|
|
4952
|
-
return text.replace(/[^0-9a-f]/g, "");
|
|
4953
|
-
}
|
|
4954
|
-
/** 计算证书 PEM 的 SHA-256 指纹(纯 hex,不含 sha256: 前缀) */
|
|
4955
|
-
static async _certFingerprint(certPem) {
|
|
4956
|
-
const der = pemToArrayBuffer(certPem);
|
|
4957
|
-
const hash = await crypto.subtle.digest("SHA-256", der);
|
|
4958
|
-
return Array.from(new Uint8Array(hash)).map((b) => b.toString(16).padStart(2, "0")).join("");
|
|
4959
|
-
}
|
|
4960
|
-
/** 校验 PEM 格式是否包含 BEGIN CERTIFICATE */
|
|
4961
|
-
static _validateCertPem(certPem, rootId) {
|
|
4962
|
-
if (!certPem.includes("BEGIN CERTIFICATE")) {
|
|
4963
|
-
throw new ValidationError(`root certificate missing PEM data: ${rootId}`);
|
|
4964
|
-
}
|
|
4965
|
-
}
|
|
4966
|
-
/**
|
|
4967
|
-
* 加载权威公钥用于签名验证。
|
|
4968
|
-
* 浏览器环境:从 PEM 提取 SPKI 并导入为 CryptoKey。
|
|
4969
|
-
* 支持 P-256 和 P-384 曲线。
|
|
4970
|
-
*/
|
|
4971
|
-
async _loadAuthorityPublicKey(authorityCertPem, authorityPublicKeyPem, trustList) {
|
|
4972
|
-
if (authorityPublicKeyPem) {
|
|
4973
|
-
return this._importPublicKeyPem(authorityPublicKeyPem);
|
|
4974
|
-
}
|
|
4975
|
-
const candidateCert = authorityCertPem || String(trustList?.authority_cert_pem ?? "");
|
|
4976
|
-
if (!candidateCert) {
|
|
4977
|
-
throw new ValidationError("authority certificate/public key is required to verify trust roots");
|
|
4978
|
-
}
|
|
4979
|
-
return this._importCertPublicKey(candidateCert);
|
|
4980
|
-
}
|
|
4981
|
-
/** 从 SPKI PEM 导入公钥(尝试 P-384,降级 P-256) */
|
|
4982
|
-
async _importPublicKeyPem(pem) {
|
|
4983
|
-
const der = pemToArrayBuffer(pem);
|
|
4984
|
-
for (const curve of ["P-384", "P-256"]) {
|
|
4985
|
-
try {
|
|
4986
|
-
return await crypto.subtle.importKey(
|
|
4987
|
-
"spki",
|
|
4988
|
-
der,
|
|
4989
|
-
{ name: "ECDSA", namedCurve: curve },
|
|
4990
|
-
true,
|
|
4991
|
-
["verify"]
|
|
4992
|
-
);
|
|
4993
|
-
} catch {
|
|
4994
|
-
}
|
|
4995
|
-
}
|
|
4996
|
-
throw new ValidationError("invalid authority public key PEM");
|
|
4997
|
-
}
|
|
4998
|
-
/**
|
|
4999
|
-
* 从证书 PEM 提取公钥并导入。
|
|
5000
|
-
* 使用简化 ASN.1 解析提取 SPKI 段。
|
|
5001
|
-
*/
|
|
5002
|
-
async _importCertPublicKey(certPem) {
|
|
5003
|
-
if (!certPem.includes("BEGIN CERTIFICATE")) {
|
|
5004
|
-
throw new ValidationError("invalid authority certificate PEM");
|
|
5005
|
-
}
|
|
5006
|
-
const certDer = new Uint8Array(pemToArrayBuffer(certPem));
|
|
5007
|
-
const spki = this._extractSpkiFromDer(certDer);
|
|
5008
|
-
if (!spki) {
|
|
5009
|
-
throw new ValidationError("invalid authority certificate PEM");
|
|
5010
|
-
}
|
|
5011
|
-
for (const curve of ["P-384", "P-256"]) {
|
|
5012
|
-
try {
|
|
5013
|
-
return await crypto.subtle.importKey(
|
|
5014
|
-
"spki",
|
|
5015
|
-
spki,
|
|
5016
|
-
{ name: "ECDSA", namedCurve: curve },
|
|
5017
|
-
true,
|
|
5018
|
-
["verify"]
|
|
5019
|
-
);
|
|
5020
|
-
} catch {
|
|
5021
|
-
}
|
|
5022
|
-
}
|
|
5023
|
-
throw new ValidationError("invalid authority certificate PEM");
|
|
5024
|
-
}
|
|
5025
|
-
/**
|
|
5026
|
-
* 从 X.509 DER 证书中提取 SubjectPublicKeyInfo。
|
|
5027
|
-
* 搜索 EC 公钥 OID (1.2.840.10045.2.1)。
|
|
5028
|
-
*/
|
|
5029
|
-
_extractSpkiFromDer(certDer) {
|
|
5030
|
-
const ecOid = [6, 7, 42, 134, 72, 206, 61, 2, 1];
|
|
5031
|
-
for (let i = 0; i < certDer.length - ecOid.length; i++) {
|
|
5032
|
-
let match = true;
|
|
5033
|
-
for (let j = 0; j < ecOid.length; j++) {
|
|
5034
|
-
if (certDer[i + j] !== ecOid[j]) {
|
|
5035
|
-
match = false;
|
|
5036
|
-
break;
|
|
5037
|
-
}
|
|
5038
|
-
}
|
|
5039
|
-
if (!match) continue;
|
|
5040
|
-
for (let back = 1; back <= 4; back++) {
|
|
5041
|
-
const seqStart = i - back;
|
|
5042
|
-
if (seqStart < 0) continue;
|
|
5043
|
-
if (certDer[seqStart] !== 48) continue;
|
|
5044
|
-
const seqLen = this._parseDerLength(certDer, seqStart + 1);
|
|
5045
|
-
if (!seqLen) continue;
|
|
5046
|
-
const totalLen = 1 + seqLen.lenBytes + seqLen.value;
|
|
5047
|
-
if (totalLen < 50 || totalLen > 200) continue;
|
|
5048
|
-
return certDer.slice(seqStart, seqStart + totalLen).buffer;
|
|
5049
|
-
}
|
|
5050
|
-
}
|
|
5051
|
-
return null;
|
|
5052
|
-
}
|
|
5053
|
-
/** 解析 DER 长度字段 */
|
|
5054
|
-
_parseDerLength(data, offset) {
|
|
5055
|
-
if (offset >= data.length) return null;
|
|
5056
|
-
const first = data[offset];
|
|
5057
|
-
if (first < 128) return { value: first, lenBytes: 1 };
|
|
5058
|
-
const numBytes = first & 127;
|
|
5059
|
-
if (numBytes === 0 || numBytes > 4 || offset + 1 + numBytes > data.length) return null;
|
|
5060
|
-
let value = 0;
|
|
5061
|
-
for (let i = 0; i < numBytes; i++) {
|
|
5062
|
-
value = value << 8 | data[offset + 1 + i];
|
|
5063
|
-
}
|
|
5064
|
-
return { value, lenBytes: 1 + numBytes };
|
|
5065
|
-
}
|
|
5066
|
-
/** 版本单调递增检查(通过 keystore 加载已有版本) */
|
|
5067
|
-
async _enforceMonotonicVersion(trustList) {
|
|
5068
|
-
const version = trustList.version;
|
|
5069
|
-
if (typeof version !== "number") return;
|
|
5070
|
-
const ks = this._internal._keystore;
|
|
5071
|
-
if (typeof ks.loadTrustRoots !== "function") return;
|
|
5072
|
-
try {
|
|
5073
|
-
const current = await ks.loadTrustRoots();
|
|
5074
|
-
const currentVersion = current?.version;
|
|
5075
|
-
if (typeof currentVersion === "number" && version < currentVersion) {
|
|
5076
|
-
throw new ValidationError("trust roots list version rollback is not allowed");
|
|
5077
|
-
}
|
|
5078
|
-
} catch (e) {
|
|
5079
|
-
if (e instanceof ValidationError) throw e;
|
|
5080
|
-
}
|
|
5081
|
-
}
|
|
5082
|
-
/** 从 keystore 加载本地已存储的信任列表 */
|
|
5083
|
-
async _loadLocalTrustList() {
|
|
5084
|
-
const ks = this._internal._keystore;
|
|
5085
|
-
if (typeof ks.loadTrustRoots !== "function") return null;
|
|
5086
|
-
try {
|
|
5087
|
-
const payload = await ks.loadTrustRoots();
|
|
5088
|
-
if (!payload || typeof payload !== "object") return null;
|
|
5089
|
-
return payload;
|
|
5090
|
-
} catch {
|
|
5091
|
-
return null;
|
|
5092
|
-
}
|
|
5093
|
-
}
|
|
5094
|
-
};
|
|
5095
|
-
|
|
5096
|
-
// src/client.ts
|
|
5097
|
-
init_crypto();
|
|
5098
|
-
|
|
5099
|
-
// src/keystore/indexeddb.ts
|
|
5100
|
-
init_crypto();
|
|
5101
|
-
var _noopLog8 = { error: () => {
|
|
5102
|
-
}, warn: () => {
|
|
5103
|
-
}, info: () => {
|
|
5104
|
-
}, debug: () => {
|
|
5105
|
-
} };
|
|
5106
|
-
function safeAid(aid) {
|
|
5107
|
-
return aid.replace(/[/\\:]/g, "_");
|
|
5108
|
-
}
|
|
5109
|
-
function extractSpkiB64FromCertPem(certPem) {
|
|
5110
|
-
try {
|
|
5111
|
-
const der = new Uint8Array(pemToArrayBuffer(certPem));
|
|
5112
|
-
const tlv = (d, o) => {
|
|
5113
|
-
let lo = o + 1, len;
|
|
5114
|
-
if (d[lo] & 128) {
|
|
5115
|
-
const n = d[lo] & 127;
|
|
5116
|
-
len = 0;
|
|
5117
|
-
for (let i = 0; i < n; i++) len = len << 8 | d[lo + 1 + i];
|
|
5118
|
-
lo += 1 + n;
|
|
5119
|
-
} else {
|
|
5120
|
-
len = d[lo];
|
|
5121
|
-
lo += 1;
|
|
5122
|
-
}
|
|
5123
|
-
return [lo, len];
|
|
5124
|
-
};
|
|
5125
|
-
const skip = (d, o) => {
|
|
5126
|
-
const [vs2, vl] = tlv(d, o);
|
|
5127
|
-
return vs2 + vl;
|
|
5128
|
-
};
|
|
5129
|
-
let [vs] = tlv(der, 0);
|
|
5130
|
-
let pos = vs;
|
|
5131
|
-
const [tbsVs] = tlv(der, pos);
|
|
5132
|
-
pos = tbsVs;
|
|
5133
|
-
if (der[pos] === 160) pos = skip(der, pos);
|
|
5134
|
-
for (let i = 0; i < 5; i++) pos = skip(der, pos);
|
|
5135
|
-
const spkiEnd = skip(der, pos);
|
|
5136
|
-
const spkiBytes = der.slice(pos, spkiEnd);
|
|
5137
|
-
let b = "";
|
|
5138
|
-
for (let i = 0; i < spkiBytes.length; i++) b += String.fromCharCode(spkiBytes[i]);
|
|
5139
|
-
return btoa(b);
|
|
5140
|
-
} catch {
|
|
5141
|
-
return "";
|
|
5142
|
-
}
|
|
5143
|
-
}
|
|
5144
|
-
function encodePart(value) {
|
|
5145
|
-
return encodeURIComponent(value);
|
|
5146
|
-
}
|
|
5147
|
-
function deepClone(value) {
|
|
5148
|
-
return JSON.parse(JSON.stringify(value));
|
|
5149
|
-
}
|
|
5150
|
-
function isRecord(value) {
|
|
5151
|
-
return isJsonObject(value);
|
|
5152
|
-
}
|
|
5153
|
-
function toBufferSource2(bytes) {
|
|
5154
|
-
return bytes.slice().buffer;
|
|
5155
|
-
}
|
|
5156
|
-
function sameJson(a, b) {
|
|
5157
|
-
return JSON.stringify(a) === JSON.stringify(b);
|
|
5158
|
-
}
|
|
5159
|
-
var _ENC_ALGO = "AES-GCM";
|
|
5160
|
-
var _PBKDF2_ITERATIONS = 1e5;
|
|
5161
|
-
var SeedMigrationError = class extends Error {
|
|
5162
|
-
constructor(message) {
|
|
5163
|
-
super(message);
|
|
5164
|
-
this.name = "SeedMigrationError";
|
|
5165
|
-
}
|
|
5166
|
-
};
|
|
5167
|
-
function _uint8ToBase64(bytes) {
|
|
5168
|
-
let b = "";
|
|
5169
|
-
for (let i = 0; i < bytes.length; i++) b += String.fromCharCode(bytes[i]);
|
|
5170
|
-
return btoa(b);
|
|
5171
|
-
}
|
|
5172
|
-
function _base64ToUint8(b64) {
|
|
5173
|
-
return Uint8Array.from(atob(b64), (c) => c.charCodeAt(0));
|
|
5174
|
-
}
|
|
5175
|
-
async function _deriveEncKey(seed, salt) {
|
|
5176
|
-
const raw = new TextEncoder().encode(seed);
|
|
5177
|
-
const base = await crypto.subtle.importKey("raw", raw, "PBKDF2", false, ["deriveKey"]);
|
|
5178
|
-
return crypto.subtle.deriveKey(
|
|
5179
|
-
{ name: "PBKDF2", salt, iterations: _PBKDF2_ITERATIONS, hash: "SHA-256" },
|
|
5180
|
-
base,
|
|
5181
|
-
{ name: _ENC_ALGO, length: 256 },
|
|
5182
|
-
false,
|
|
5183
|
-
["encrypt", "decrypt"]
|
|
5184
|
-
);
|
|
5185
|
-
}
|
|
5186
|
-
async function _encryptPEM(pem, seed) {
|
|
5187
|
-
const salt = crypto.getRandomValues(new Uint8Array(16));
|
|
5188
|
-
const iv = crypto.getRandomValues(new Uint8Array(12));
|
|
5189
|
-
const key = await _deriveEncKey(seed, salt);
|
|
5190
|
-
const ct = await crypto.subtle.encrypt({ name: _ENC_ALGO, iv }, key, new TextEncoder().encode(pem));
|
|
5191
|
-
return {
|
|
5192
|
-
ct: _uint8ToBase64(new Uint8Array(ct)),
|
|
5193
|
-
iv: _uint8ToBase64(iv),
|
|
5194
|
-
salt: _uint8ToBase64(salt)
|
|
5195
|
-
};
|
|
5196
|
-
}
|
|
5197
|
-
async function _decryptPEM(enc, seed) {
|
|
5198
|
-
const salt = _base64ToUint8(enc.salt);
|
|
5199
|
-
const iv = _base64ToUint8(enc.iv);
|
|
5200
|
-
const ct = _base64ToUint8(enc.ct);
|
|
5201
|
-
const key = await _deriveEncKey(seed, salt);
|
|
5202
|
-
const pt = await crypto.subtle.decrypt({ name: _ENC_ALGO, iv: toBufferSource2(iv) }, key, toBufferSource2(ct));
|
|
5203
|
-
return new TextDecoder().decode(pt);
|
|
5204
|
-
}
|
|
5205
|
-
function hasEncryptionSeed(seed) {
|
|
5206
|
-
return seed !== void 0;
|
|
5207
|
-
}
|
|
5208
|
-
var DB_NAME = "aun-keystore";
|
|
5209
|
-
var DB_VERSION = 6;
|
|
5210
|
-
var STORE_KEY_PAIRS = "key_pairs";
|
|
5211
|
-
var STORE_CERTS = "certs";
|
|
5212
|
-
var STORE_METADATA = "metadata";
|
|
5213
|
-
var STORE_INSTANCE_STATE = "instance_state";
|
|
5214
|
-
var STORE_PREKEYS = "prekeys";
|
|
5215
|
-
var STORE_GROUP_CURRENT = "group_current";
|
|
5216
|
-
var STORE_GROUP_OLD_EPOCHS = "group_old_epochs";
|
|
5217
|
-
var STORE_SESSIONS = "e2ee_sessions";
|
|
5218
|
-
var STORE_GROUP_STATE = "group_state";
|
|
5219
|
-
var STORE_AGENT_MD_CACHE = "agent_md_cache";
|
|
5220
|
-
var STRUCTURED_RECOVERY_RETENTION_MS = 7 * 24 * 3600 * 1e3;
|
|
5221
|
-
function metadataStoreKey(aid) {
|
|
5222
|
-
return safeAid(aid);
|
|
5223
|
-
}
|
|
5224
|
-
function normalizeCertFingerprint(certFingerprint) {
|
|
5225
|
-
const normalized = String(certFingerprint ?? "").trim().toLowerCase();
|
|
5226
|
-
if (!normalized) return "";
|
|
5227
|
-
if (!normalized.startsWith("sha256:")) return "";
|
|
5228
|
-
const hexPart = normalized.slice(7);
|
|
5229
|
-
if (hexPart.length !== 64 || /[^0-9a-f]/.test(hexPart)) return "";
|
|
5230
|
-
return normalized;
|
|
5231
|
-
}
|
|
5232
|
-
async function fingerprintFromCertPem(certPem) {
|
|
5233
|
-
try {
|
|
5234
|
-
const der = pemToArrayBuffer(certPem);
|
|
5235
|
-
const hash = await crypto.subtle.digest("SHA-256", der);
|
|
5236
|
-
const hex = Array.from(new Uint8Array(hash)).map((b) => b.toString(16).padStart(2, "0")).join("");
|
|
5237
|
-
return `sha256:${hex}`;
|
|
5238
|
-
} catch {
|
|
5239
|
-
return "";
|
|
5240
|
-
}
|
|
5241
|
-
}
|
|
5242
|
-
function certStoreKey(aid, certFingerprint) {
|
|
5243
|
-
const normalized = normalizeCertFingerprint(certFingerprint);
|
|
5244
|
-
if (!normalized) return safeAid(aid);
|
|
5245
|
-
return `${safeAid(aid)}|${encodePart(normalized)}`;
|
|
5246
|
-
}
|
|
5247
|
-
function instanceStateStoreKey(aid, deviceId, slotId = "") {
|
|
5248
|
-
const normalizedDevice = normalizeInstanceId(deviceId, "device_id");
|
|
5249
|
-
const normalizedSlot = normalizeInstanceId(slotId, "slot_id", { allowEmpty: true }) || "_singleton";
|
|
5250
|
-
return `${safeAid(aid)}|${encodePart(normalizedDevice)}|${encodePart(normalizedSlot)}`;
|
|
5251
|
-
}
|
|
5252
|
-
function prekeyPrefix(aid) {
|
|
5253
|
-
return `${safeAid(aid)}|`;
|
|
5254
|
-
}
|
|
5255
|
-
function prekeyStoreKey(aid, prekeyId, deviceId = "") {
|
|
5256
|
-
const normalizedDeviceId = String(deviceId ?? "").trim();
|
|
5257
|
-
if (!normalizedDeviceId) {
|
|
5258
|
-
return `${safeAid(aid)}|${encodePart(prekeyId)}`;
|
|
3605
|
+
function prekeyStoreKey(aid, prekeyId, deviceId = "") {
|
|
3606
|
+
const normalizedDeviceId = String(deviceId ?? "").trim();
|
|
3607
|
+
if (!normalizedDeviceId) {
|
|
3608
|
+
return `${safeAid(aid)}|${encodePart(prekeyId)}`;
|
|
5259
3609
|
}
|
|
5260
3610
|
return `${safeAid(aid)}|${encodePart(normalizedDeviceId)}|${encodePart(prekeyId)}`;
|
|
5261
3611
|
}
|
|
@@ -5519,7 +3869,7 @@ async function idbDelete(storeName, key) {
|
|
|
5519
3869
|
}
|
|
5520
3870
|
var _IndexedDBKeyStore = class _IndexedDBKeyStore {
|
|
5521
3871
|
constructor(opts) {
|
|
5522
|
-
__publicField(this, "_log",
|
|
3872
|
+
__publicField(this, "_log", _noopLog5);
|
|
5523
3873
|
/** 私钥加密种子;为空时降级为明文存储(向后兼容) */
|
|
5524
3874
|
__publicField(this, "_encryptionSeed");
|
|
5525
3875
|
this._encryptionSeed = opts?.encryptionSeed;
|
|
@@ -10336,7 +8686,6 @@ function isPlainObject(value) {
|
|
|
10336
8686
|
var encoder3 = new TextEncoder();
|
|
10337
8687
|
var decoder = new TextDecoder();
|
|
10338
8688
|
var E2EE_SDK_LANG = "javascript";
|
|
10339
|
-
var E2EE_SDK_VERSION = "0.3.5";
|
|
10340
8689
|
async function sha2563(data) {
|
|
10341
8690
|
const buf = await crypto.subtle.digest("SHA-256", data.slice().buffer);
|
|
10342
8691
|
return new Uint8Array(buf);
|
|
@@ -10507,7 +8856,7 @@ function normalizeProtectedHeaders(headers, payload) {
|
|
|
10507
8856
|
}
|
|
10508
8857
|
normalized.sdk_lang = E2EE_SDK_LANG;
|
|
10509
8858
|
delete normalized.sdk_vesion;
|
|
10510
|
-
normalized.sdk_version =
|
|
8859
|
+
normalized.sdk_version = VERSION;
|
|
10511
8860
|
return normalized;
|
|
10512
8861
|
}
|
|
10513
8862
|
function normalizeProtectedHeaderKey(key) {
|
|
@@ -11096,13 +9445,325 @@ var AUNLogger = class {
|
|
|
11096
9445
|
break;
|
|
11097
9446
|
}
|
|
11098
9447
|
}
|
|
11099
|
-
_now() {
|
|
11100
|
-
const d = /* @__PURE__ */ new Date();
|
|
11101
|
-
const pad = (n, w = 2) => String(n).padStart(w, "0");
|
|
11102
|
-
const date = `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}`;
|
|
11103
|
-
const time = `${pad(d.getHours())}:${pad(d.getMinutes())}:${pad(d.getSeconds())}`;
|
|
11104
|
-
const ms = pad(d.getMilliseconds(), 3);
|
|
11105
|
-
return { date, time, ms };
|
|
9448
|
+
_now() {
|
|
9449
|
+
const d = /* @__PURE__ */ new Date();
|
|
9450
|
+
const pad = (n, w = 2) => String(n).padStart(w, "0");
|
|
9451
|
+
const date = `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}`;
|
|
9452
|
+
const time = `${pad(d.getHours())}:${pad(d.getMinutes())}:${pad(d.getSeconds())}`;
|
|
9453
|
+
const ms = pad(d.getMilliseconds(), 3);
|
|
9454
|
+
return { date, time, ms };
|
|
9455
|
+
}
|
|
9456
|
+
};
|
|
9457
|
+
|
|
9458
|
+
// src/error-codes.ts
|
|
9459
|
+
var CERT_NOT_FOUND = "CERT_NOT_FOUND";
|
|
9460
|
+
var CERT_PARSE_ERROR = "CERT_PARSE_ERROR";
|
|
9461
|
+
var CERT_EXPIRED = "CERT_EXPIRED";
|
|
9462
|
+
var CERT_NOT_YET_VALID = "CERT_NOT_YET_VALID";
|
|
9463
|
+
var CERT_CHAIN_BROKEN = "CERT_CHAIN_BROKEN";
|
|
9464
|
+
var KEYPAIR_MISMATCH = "KEYPAIR_MISMATCH";
|
|
9465
|
+
var PRIVATE_KEY_PARSE_ERROR = "PRIVATE_KEY_PARSE_ERROR";
|
|
9466
|
+
var IDENTITY_CONFLICT = "IDENTITY_CONFLICT";
|
|
9467
|
+
var INVALID_AID_FORMAT = "INVALID_AID_FORMAT";
|
|
9468
|
+
var NETWORK_ERROR = "NETWORK_ERROR";
|
|
9469
|
+
var SERVER_ERROR = "SERVER_ERROR";
|
|
9470
|
+
var AGENTMD_NOT_FOUND = "AGENTMD_NOT_FOUND";
|
|
9471
|
+
var CERT_RENEWAL_FAILED = "CERT_RENEWAL_FAILED";
|
|
9472
|
+
var REKEY_FAILED = "REKEY_FAILED";
|
|
9473
|
+
var PRIVATE_KEY_REQUIRED = "PRIVATE_KEY_REQUIRED";
|
|
9474
|
+
var SIGNATURE_OPERATION_ERROR = "SIGNATURE_OPERATION_ERROR";
|
|
9475
|
+
var VERIFICATION_OPERATION_ERROR = "VERIFICATION_OPERATION_ERROR";
|
|
9476
|
+
var CERT_NOT_VALID = "CERT_NOT_VALID";
|
|
9477
|
+
var PRIVATE_KEY_NOT_VALID = "PRIVATE_KEY_NOT_VALID";
|
|
9478
|
+
|
|
9479
|
+
// src/cert-utils.ts
|
|
9480
|
+
init_crypto();
|
|
9481
|
+
var AGENT_MD_SIGNATURE_MARKER = "<!-- AUN-SIGNATURE";
|
|
9482
|
+
var AGENT_MD_SIGNATURE_RE = /^<!-- AUN-SIGNATURE\r?\n(?<body>[\s\S]*?)\r?\n-->\s*$/;
|
|
9483
|
+
var AGENT_MD_FINGERPRINT_RE = /^sha256:[0-9a-f]{64}$/;
|
|
9484
|
+
function parseAgentMdTailSignature(content) {
|
|
9485
|
+
const idx = content.lastIndexOf(AGENT_MD_SIGNATURE_MARKER);
|
|
9486
|
+
if (idx < 0) return { payload: content, fields: null };
|
|
9487
|
+
if (idx > 0 && content[idx - 1] !== "\n" && content[idx - 1] !== "\r") return { payload: content, fields: null };
|
|
9488
|
+
const tail = content.slice(idx);
|
|
9489
|
+
const match = tail.match(AGENT_MD_SIGNATURE_RE);
|
|
9490
|
+
if (!match) return { payload: content.slice(0, idx), fields: null, parseError: "malformed signature block" };
|
|
9491
|
+
const fields = {};
|
|
9492
|
+
for (const rawLine of match.groups?.body?.split(/\r?\n/) ?? []) {
|
|
9493
|
+
const line = rawLine.trim();
|
|
9494
|
+
if (!line) continue;
|
|
9495
|
+
const colon = line.indexOf(":");
|
|
9496
|
+
if (colon < 0) return { payload: content.slice(0, idx), fields: null, parseError: `malformed signature field: ${line}` };
|
|
9497
|
+
fields[line.slice(0, colon).trim().toLowerCase()] = line.slice(colon + 1).trim();
|
|
9498
|
+
}
|
|
9499
|
+
for (const req of ["cert_fingerprint", "timestamp", "signature"]) {
|
|
9500
|
+
if (!fields[req]) return { payload: content.slice(0, idx), fields: null, parseError: `signature block missing ${req}` };
|
|
9501
|
+
}
|
|
9502
|
+
if (!AGENT_MD_FINGERPRINT_RE.test(fields.cert_fingerprint.toLowerCase())) {
|
|
9503
|
+
return { payload: content.slice(0, idx), fields: null, parseError: "invalid cert_fingerprint" };
|
|
9504
|
+
}
|
|
9505
|
+
if (!Number.isFinite(Number(fields.timestamp))) {
|
|
9506
|
+
return { payload: content.slice(0, idx), fields: null, parseError: "invalid timestamp" };
|
|
9507
|
+
}
|
|
9508
|
+
return { payload: content.slice(0, idx), fields };
|
|
9509
|
+
}
|
|
9510
|
+
function normalizeAgentMdPayload(content) {
|
|
9511
|
+
let payload = parseAgentMdTailSignature(String(content ?? "")).payload;
|
|
9512
|
+
if (payload && !payload.endsWith("\n") && !payload.endsWith("\r")) payload += "\n";
|
|
9513
|
+
return payload;
|
|
9514
|
+
}
|
|
9515
|
+
function buildAgentMdSignatureBlock(certFingerprint, timestamp, signatureB64) {
|
|
9516
|
+
return [
|
|
9517
|
+
AGENT_MD_SIGNATURE_MARKER,
|
|
9518
|
+
`cert_fingerprint: ${certFingerprint}`,
|
|
9519
|
+
`timestamp: ${Math.trunc(timestamp)}`,
|
|
9520
|
+
`signature: ${signatureB64}`,
|
|
9521
|
+
"-->"
|
|
9522
|
+
].join("\n");
|
|
9523
|
+
}
|
|
9524
|
+
function extractAgentMdAid(payload) {
|
|
9525
|
+
const lines = payload.replace(/^\ufeff/, "").split(/\r?\n/);
|
|
9526
|
+
if (!lines.length || lines[0].trim() !== "---") return "";
|
|
9527
|
+
for (const line of lines.slice(1)) {
|
|
9528
|
+
const t = line.trim();
|
|
9529
|
+
if (t === "---") break;
|
|
9530
|
+
if (t.startsWith("aid:")) {
|
|
9531
|
+
let v = t.slice(4).trim();
|
|
9532
|
+
if (v.length >= 2 && v[0] === v[v.length - 1] && (v[0] === '"' || v[0] === "'")) v = v.slice(1, -1);
|
|
9533
|
+
return v.trim();
|
|
9534
|
+
}
|
|
9535
|
+
}
|
|
9536
|
+
return "";
|
|
9537
|
+
}
|
|
9538
|
+
function readDerLength(data, offset) {
|
|
9539
|
+
if (offset >= data.length) return null;
|
|
9540
|
+
const first = data[offset];
|
|
9541
|
+
if (first < 128) return { value: first, lenBytes: 1 };
|
|
9542
|
+
const numBytes = first & 127;
|
|
9543
|
+
if (numBytes === 0 || numBytes > 4) return null;
|
|
9544
|
+
let value = 0;
|
|
9545
|
+
for (let i = 0; i < numBytes; i++) value = value << 8 | data[offset + 1 + i];
|
|
9546
|
+
return { value, lenBytes: 1 + numBytes };
|
|
9547
|
+
}
|
|
9548
|
+
function readDerTlv(data, offset) {
|
|
9549
|
+
const len = readDerLength(data, offset + 1);
|
|
9550
|
+
if (!len) return null;
|
|
9551
|
+
const valueStart = offset + 1 + len.lenBytes;
|
|
9552
|
+
const valueEnd = valueStart + len.value;
|
|
9553
|
+
if (valueEnd > data.length) return null;
|
|
9554
|
+
return { tag: data[offset], valueStart, valueEnd, fullEnd: valueEnd };
|
|
9555
|
+
}
|
|
9556
|
+
function publicKeyDerB64(certPem) {
|
|
9557
|
+
const certDer = new Uint8Array(pemToArrayBuffer(certPem));
|
|
9558
|
+
const cert = readDerTlv(certDer, 0);
|
|
9559
|
+
if (!cert || cert.tag !== 48) return "";
|
|
9560
|
+
const tbs = readDerTlv(certDer, cert.valueStart);
|
|
9561
|
+
if (!tbs || tbs.tag !== 48) return "";
|
|
9562
|
+
let pos = tbs.valueStart;
|
|
9563
|
+
if (certDer[pos] === 160) pos = readDerTlv(certDer, pos)?.fullEnd ?? pos;
|
|
9564
|
+
for (let i = 0; i < 5; i++) pos = readDerTlv(certDer, pos)?.fullEnd ?? pos;
|
|
9565
|
+
const spki = readDerTlv(certDer, pos);
|
|
9566
|
+
if (!spki || spki.tag !== 48) return "";
|
|
9567
|
+
return uint8ToBase64(certDer.slice(pos, spki.fullEnd));
|
|
9568
|
+
}
|
|
9569
|
+
function extractCommonName(der, nameStart, nameEnd) {
|
|
9570
|
+
let pos = nameStart;
|
|
9571
|
+
while (pos < nameEnd) {
|
|
9572
|
+
const set = readDerTlv(der, pos);
|
|
9573
|
+
if (!set || set.tag !== 49) break;
|
|
9574
|
+
let rpos = set.valueStart;
|
|
9575
|
+
while (rpos < set.valueEnd) {
|
|
9576
|
+
const seq = readDerTlv(der, rpos);
|
|
9577
|
+
if (!seq || seq.tag !== 48) break;
|
|
9578
|
+
const oid = readDerTlv(der, seq.valueStart);
|
|
9579
|
+
if (oid && oid.tag === 6) {
|
|
9580
|
+
const isCN = oid.valueEnd - oid.valueStart === 3 && der[oid.valueStart] === 85 && der[oid.valueStart + 1] === 4 && der[oid.valueStart + 2] === 3;
|
|
9581
|
+
if (isCN) {
|
|
9582
|
+
const val = readDerTlv(der, oid.fullEnd);
|
|
9583
|
+
if (val) return new TextDecoder().decode(der.slice(val.valueStart, val.valueEnd));
|
|
9584
|
+
}
|
|
9585
|
+
}
|
|
9586
|
+
rpos = seq.fullEnd;
|
|
9587
|
+
}
|
|
9588
|
+
pos = set.fullEnd;
|
|
9589
|
+
}
|
|
9590
|
+
return "";
|
|
9591
|
+
}
|
|
9592
|
+
function parseDerTime(der, offset) {
|
|
9593
|
+
const tlv = readDerTlv(der, offset);
|
|
9594
|
+
if (!tlv) return null;
|
|
9595
|
+
const raw = new TextDecoder().decode(der.slice(tlv.valueStart, tlv.valueEnd));
|
|
9596
|
+
if (tlv.tag === 23) {
|
|
9597
|
+
const yy = parseInt(raw.slice(0, 2), 10);
|
|
9598
|
+
const year = yy >= 50 ? 1900 + yy : 2e3 + yy;
|
|
9599
|
+
const ms = Date.UTC(
|
|
9600
|
+
year,
|
|
9601
|
+
parseInt(raw.slice(2, 4), 10) - 1,
|
|
9602
|
+
parseInt(raw.slice(4, 6), 10),
|
|
9603
|
+
parseInt(raw.slice(6, 8), 10),
|
|
9604
|
+
parseInt(raw.slice(8, 10), 10),
|
|
9605
|
+
parseInt(raw.slice(10, 12), 10)
|
|
9606
|
+
);
|
|
9607
|
+
return Number.isNaN(ms) ? null : new Date(ms);
|
|
9608
|
+
}
|
|
9609
|
+
if (tlv.tag === 24) {
|
|
9610
|
+
const ms = Date.UTC(
|
|
9611
|
+
parseInt(raw.slice(0, 4), 10),
|
|
9612
|
+
parseInt(raw.slice(4, 6), 10) - 1,
|
|
9613
|
+
parseInt(raw.slice(6, 8), 10),
|
|
9614
|
+
parseInt(raw.slice(8, 10), 10),
|
|
9615
|
+
parseInt(raw.slice(10, 12), 10),
|
|
9616
|
+
parseInt(raw.slice(12, 14), 10)
|
|
9617
|
+
);
|
|
9618
|
+
return Number.isNaN(ms) ? null : new Date(ms);
|
|
9619
|
+
}
|
|
9620
|
+
return null;
|
|
9621
|
+
}
|
|
9622
|
+
function parseCertMetadata(certPem) {
|
|
9623
|
+
const fallback = { subject: "", issuer: "", notBefore: /* @__PURE__ */ new Date(0), notAfter: /* @__PURE__ */ new Date(0) };
|
|
9624
|
+
try {
|
|
9625
|
+
const der = new Uint8Array(pemToArrayBuffer(certPem));
|
|
9626
|
+
const cert = readDerTlv(der, 0);
|
|
9627
|
+
if (!cert || cert.tag !== 48) return fallback;
|
|
9628
|
+
const tbs = readDerTlv(der, cert.valueStart);
|
|
9629
|
+
if (!tbs || tbs.tag !== 48) return fallback;
|
|
9630
|
+
let pos = tbs.valueStart;
|
|
9631
|
+
if (der[pos] === 160) pos = readDerTlv(der, pos)?.fullEnd ?? pos;
|
|
9632
|
+
pos = readDerTlv(der, pos)?.fullEnd ?? pos;
|
|
9633
|
+
pos = readDerTlv(der, pos)?.fullEnd ?? pos;
|
|
9634
|
+
const issuer = readDerTlv(der, pos);
|
|
9635
|
+
if (!issuer || issuer.tag !== 48) return fallback;
|
|
9636
|
+
const issuerCN = extractCommonName(der, issuer.valueStart, issuer.valueEnd);
|
|
9637
|
+
pos = issuer.fullEnd;
|
|
9638
|
+
const validity = readDerTlv(der, pos);
|
|
9639
|
+
if (!validity || validity.tag !== 48) return fallback;
|
|
9640
|
+
const notBefore = parseDerTime(der, validity.valueStart);
|
|
9641
|
+
const nb = readDerTlv(der, validity.valueStart);
|
|
9642
|
+
const notAfter = nb ? parseDerTime(der, nb.fullEnd) : null;
|
|
9643
|
+
pos = validity.fullEnd;
|
|
9644
|
+
const subject = readDerTlv(der, pos);
|
|
9645
|
+
if (!subject || subject.tag !== 48) return fallback;
|
|
9646
|
+
const subjectCN = extractCommonName(der, subject.valueStart, subject.valueEnd);
|
|
9647
|
+
return {
|
|
9648
|
+
subject: subjectCN,
|
|
9649
|
+
issuer: issuerCN,
|
|
9650
|
+
notBefore: notBefore ?? /* @__PURE__ */ new Date(0),
|
|
9651
|
+
notAfter: notAfter ?? /* @__PURE__ */ new Date(0)
|
|
9652
|
+
};
|
|
9653
|
+
} catch {
|
|
9654
|
+
return fallback;
|
|
9655
|
+
}
|
|
9656
|
+
}
|
|
9657
|
+
async function signBytes(privateKeyPem, payload) {
|
|
9658
|
+
const key = await importPrivateKeyEcdsa(privateKeyPem);
|
|
9659
|
+
return uint8ToBase64(await ecdsaSignDer(key, payload));
|
|
9660
|
+
}
|
|
9661
|
+
async function verifySignatureWithCert(certPem, signatureB64, data) {
|
|
9662
|
+
try {
|
|
9663
|
+
const key = await importCertPublicKeyEcdsa(certPem);
|
|
9664
|
+
return await ecdsaVerifyDer(key, base64ToUint8(signatureB64), data);
|
|
9665
|
+
} catch {
|
|
9666
|
+
return false;
|
|
9667
|
+
}
|
|
9668
|
+
}
|
|
9669
|
+
|
|
9670
|
+
// src/result.ts
|
|
9671
|
+
function resultOk(data) {
|
|
9672
|
+
return { ok: true, data };
|
|
9673
|
+
}
|
|
9674
|
+
function resultErr(code, message, cause) {
|
|
9675
|
+
return { ok: false, error: { code, message, ...cause !== void 0 ? { cause } : {} } };
|
|
9676
|
+
}
|
|
9677
|
+
|
|
9678
|
+
// src/aid.ts
|
|
9679
|
+
var AID = class _AID {
|
|
9680
|
+
constructor(params) {
|
|
9681
|
+
__publicField(this, "aid");
|
|
9682
|
+
__publicField(this, "aunPath");
|
|
9683
|
+
__publicField(this, "certPem");
|
|
9684
|
+
__publicField(this, "publicKey");
|
|
9685
|
+
__publicField(this, "certSubject");
|
|
9686
|
+
__publicField(this, "certNotBefore");
|
|
9687
|
+
__publicField(this, "certNotAfter");
|
|
9688
|
+
__publicField(this, "certIssuer");
|
|
9689
|
+
__publicField(this, "deviceId");
|
|
9690
|
+
__publicField(this, "slotId");
|
|
9691
|
+
__publicField(this, "_privateKeyPem");
|
|
9692
|
+
__publicField(this, "_certValid");
|
|
9693
|
+
__publicField(this, "_privateKeyValid");
|
|
9694
|
+
__publicField(this, "_certFingerprint", "");
|
|
9695
|
+
this.aid = params.aid;
|
|
9696
|
+
this.aunPath = params.aunPath;
|
|
9697
|
+
this.deviceId = params.deviceId ?? "";
|
|
9698
|
+
this.slotId = params.slotId ?? "default";
|
|
9699
|
+
this.certPem = params.certPem;
|
|
9700
|
+
this.publicKey = publicKeyDerB64(params.certPem);
|
|
9701
|
+
const meta = parseCertMetadata(params.certPem);
|
|
9702
|
+
this.certSubject = meta.subject;
|
|
9703
|
+
this.certIssuer = meta.issuer;
|
|
9704
|
+
this.certNotBefore = meta.notBefore;
|
|
9705
|
+
this.certNotAfter = meta.notAfter;
|
|
9706
|
+
this._privateKeyPem = params.privateKeyPem;
|
|
9707
|
+
this._certValid = params.certValid;
|
|
9708
|
+
this._privateKeyValid = params.privateKeyValid;
|
|
9709
|
+
}
|
|
9710
|
+
static async create(params) {
|
|
9711
|
+
const aid = new _AID(params);
|
|
9712
|
+
aid._certFingerprint = await certificateSha256Fingerprint(params.certPem);
|
|
9713
|
+
return aid;
|
|
9714
|
+
}
|
|
9715
|
+
get certFingerprint() {
|
|
9716
|
+
return this._certFingerprint;
|
|
9717
|
+
}
|
|
9718
|
+
isCertValid() {
|
|
9719
|
+
return this._certValid;
|
|
9720
|
+
}
|
|
9721
|
+
isPrivateKeyValid() {
|
|
9722
|
+
return this._privateKeyValid;
|
|
9723
|
+
}
|
|
9724
|
+
async sign(payload) {
|
|
9725
|
+
if (!this._privateKeyValid || !this._privateKeyPem) return resultErr(PRIVATE_KEY_NOT_VALID, "private key is not valid");
|
|
9726
|
+
try {
|
|
9727
|
+
const data = typeof payload === "string" ? new TextEncoder().encode(payload) : payload;
|
|
9728
|
+
return resultOk({ signature: await signBytes(this._privateKeyPem, data) });
|
|
9729
|
+
} catch (exc) {
|
|
9730
|
+
return resultErr(SIGNATURE_OPERATION_ERROR, String(exc), exc);
|
|
9731
|
+
}
|
|
9732
|
+
}
|
|
9733
|
+
async verify(payload, signature) {
|
|
9734
|
+
if (!this._certValid) return resultErr(CERT_NOT_VALID, "certificate is not valid");
|
|
9735
|
+
try {
|
|
9736
|
+
const data = typeof payload === "string" ? new TextEncoder().encode(payload) : payload;
|
|
9737
|
+
return resultOk({ valid: await verifySignatureWithCert(this.certPem, signature, data) });
|
|
9738
|
+
} catch (exc) {
|
|
9739
|
+
return resultErr(VERIFICATION_OPERATION_ERROR, String(exc), exc);
|
|
9740
|
+
}
|
|
9741
|
+
}
|
|
9742
|
+
async signAgentMd(content) {
|
|
9743
|
+
if (!this._privateKeyValid || !this._privateKeyPem) return resultErr(PRIVATE_KEY_NOT_VALID, "private key is not valid");
|
|
9744
|
+
try {
|
|
9745
|
+
const payload = normalizeAgentMdPayload(content);
|
|
9746
|
+
const signature = await signBytes(this._privateKeyPem, new TextEncoder().encode(payload));
|
|
9747
|
+
return resultOk({ signed: payload + buildAgentMdSignatureBlock(this.certFingerprint, Date.now() / 1e3, signature) });
|
|
9748
|
+
} catch (exc) {
|
|
9749
|
+
return resultErr(SIGNATURE_OPERATION_ERROR, String(exc), exc);
|
|
9750
|
+
}
|
|
9751
|
+
}
|
|
9752
|
+
async verifyAgentMd(content) {
|
|
9753
|
+
if (!this._certValid) return resultErr(CERT_NOT_VALID, "certificate is not valid");
|
|
9754
|
+
try {
|
|
9755
|
+
const { payload, fields, parseError } = parseAgentMdTailSignature(String(content ?? ""));
|
|
9756
|
+
if (!fields) return resultOk(parseError ? { status: "invalid", payload, reason: parseError } : { status: "unsigned", payload });
|
|
9757
|
+
const payloadAid = extractAgentMdAid(payload);
|
|
9758
|
+
if (payloadAid && payloadAid !== this.aid) return resultOk({ status: "invalid", payload, aid: payloadAid, reason: "aid mismatch" });
|
|
9759
|
+
if (fields.cert_fingerprint.toLowerCase() !== this.certFingerprint.toLowerCase()) {
|
|
9760
|
+
return resultOk({ status: "invalid", payload, aid: this.aid, reason: "certificate fingerprint mismatch" });
|
|
9761
|
+
}
|
|
9762
|
+
const valid = await verifySignatureWithCert(this.certPem, fields.signature, new TextEncoder().encode(payload));
|
|
9763
|
+
return resultOk(valid ? { status: "verified", payload, aid: this.aid, cert_fingerprint: fields.cert_fingerprint, timestamp: Number(fields.timestamp) } : { status: "invalid", payload, aid: this.aid, cert_fingerprint: fields.cert_fingerprint, timestamp: Number(fields.timestamp), reason: "signature verification failed" });
|
|
9764
|
+
} catch (exc) {
|
|
9765
|
+
return resultErr(VERIFICATION_OPERATION_ERROR, String(exc), exc);
|
|
9766
|
+
}
|
|
11106
9767
|
}
|
|
11107
9768
|
};
|
|
11108
9769
|
|
|
@@ -11262,6 +9923,12 @@ var DEFAULT_SESSION_OPTIONS = {
|
|
|
11262
9923
|
http: 30
|
|
11263
9924
|
}
|
|
11264
9925
|
};
|
|
9926
|
+
var PROTECTED_HEADERS_METHODS = /* @__PURE__ */ new Set([
|
|
9927
|
+
"message.send",
|
|
9928
|
+
"group.send",
|
|
9929
|
+
"message.thought.put",
|
|
9930
|
+
"group.thought.put"
|
|
9931
|
+
]);
|
|
11265
9932
|
var RECONNECT_MIN_BASE_DELAY_SECONDS = 1;
|
|
11266
9933
|
var RECONNECT_MAX_BASE_DELAY_SECONDS = 64;
|
|
11267
9934
|
var TOKEN_REFRESH_CHECK_INTERVAL_MS = 3e4;
|
|
@@ -11309,6 +9976,7 @@ function reconnectSleepDelaySeconds(baseDelay, maxBaseDelay) {
|
|
|
11309
9976
|
return baseDelay + Math.random() * maxBaseDelay;
|
|
11310
9977
|
}
|
|
11311
9978
|
var PEER_CERT_CACHE_TTL = 3600;
|
|
9979
|
+
var AGENT_MD_HTTP_TIMEOUT_MS = 3e4;
|
|
11312
9980
|
function gatewayHttpUrl2(gatewayUrl, path) {
|
|
11313
9981
|
try {
|
|
11314
9982
|
const parsed = new URL(gatewayUrl);
|
|
@@ -11335,6 +10003,30 @@ function buildCertUrl(gatewayUrl, aid, certFingerprint) {
|
|
|
11335
10003
|
}
|
|
11336
10004
|
return url.toString();
|
|
11337
10005
|
}
|
|
10006
|
+
function agentMdHttpScheme(gatewayUrl) {
|
|
10007
|
+
const raw = String(gatewayUrl ?? "").trim().toLowerCase();
|
|
10008
|
+
return raw.startsWith("ws://") ? "http" : "https";
|
|
10009
|
+
}
|
|
10010
|
+
function agentMdAuthority(aid, discoveryPort) {
|
|
10011
|
+
const host = String(aid ?? "").trim();
|
|
10012
|
+
if (!host) return "";
|
|
10013
|
+
if (discoveryPort && !host.includes(":")) return `${host}:${discoveryPort}`;
|
|
10014
|
+
return host;
|
|
10015
|
+
}
|
|
10016
|
+
async function fetchWithTimeout(input, init, timeoutMs = AGENT_MD_HTTP_TIMEOUT_MS) {
|
|
10017
|
+
const controller = new AbortController();
|
|
10018
|
+
const timer = globalThis.setTimeout(() => controller.abort(), timeoutMs);
|
|
10019
|
+
try {
|
|
10020
|
+
return await fetch(input, { ...init, signal: controller.signal });
|
|
10021
|
+
} catch (error) {
|
|
10022
|
+
if (controller.signal.aborted) {
|
|
10023
|
+
throw new AUNError(`agent.md request timed out after ${timeoutMs}ms`);
|
|
10024
|
+
}
|
|
10025
|
+
throw error;
|
|
10026
|
+
} finally {
|
|
10027
|
+
globalThis.clearTimeout(timer);
|
|
10028
|
+
}
|
|
10029
|
+
}
|
|
11338
10030
|
function resolvePeerGatewayUrl(localGatewayUrl, peerAid) {
|
|
11339
10031
|
if (!peerAid.includes(".")) return localGatewayUrl;
|
|
11340
10032
|
const peerIssuer = peerAid.split(".").slice(1).join(".");
|
|
@@ -11509,6 +10201,69 @@ function extractV2EnvelopeFromSource(source) {
|
|
|
11509
10201
|
}
|
|
11510
10202
|
return null;
|
|
11511
10203
|
}
|
|
10204
|
+
function truthyBool(value) {
|
|
10205
|
+
if (value === true || value === 1) return true;
|
|
10206
|
+
if (typeof value === "string") {
|
|
10207
|
+
const normalized = value.trim().toLowerCase();
|
|
10208
|
+
return normalized === "true" || normalized === "1" || normalized === "yes" || normalized === "on";
|
|
10209
|
+
}
|
|
10210
|
+
return false;
|
|
10211
|
+
}
|
|
10212
|
+
function isEncryptedEnvelopePayload(payload) {
|
|
10213
|
+
if (!isJsonObject(payload)) return false;
|
|
10214
|
+
const payloadType = String(payload.type ?? "").trim();
|
|
10215
|
+
if (payloadType.startsWith("e2ee.")) return true;
|
|
10216
|
+
if (!String(payload.ciphertext ?? "").trim()) return false;
|
|
10217
|
+
return payload.nonce !== void 0 || payload.tag !== void 0 || payload.recipient !== void 0 || payload.recipients !== void 0 || payload.wrapped_key !== void 0 || payload.recipients_digest !== void 0;
|
|
10218
|
+
}
|
|
10219
|
+
function encryptedPushEnvelope(msg) {
|
|
10220
|
+
if (isEncryptedEnvelopePayload(msg.payload)) return msg.payload;
|
|
10221
|
+
if (typeof msg.envelope_json === "string" && msg.envelope_json.trim()) {
|
|
10222
|
+
try {
|
|
10223
|
+
const parsed = JSON.parse(msg.envelope_json);
|
|
10224
|
+
if (isEncryptedEnvelopePayload(parsed)) return parsed;
|
|
10225
|
+
} catch {
|
|
10226
|
+
return null;
|
|
10227
|
+
}
|
|
10228
|
+
}
|
|
10229
|
+
return null;
|
|
10230
|
+
}
|
|
10231
|
+
function isEncryptedPushMessage(msg) {
|
|
10232
|
+
if (truthyBool(msg.encrypted)) return true;
|
|
10233
|
+
return encryptedPushEnvelope(msg) !== null;
|
|
10234
|
+
}
|
|
10235
|
+
function isV2EncryptedEnvelopePayload(envelope) {
|
|
10236
|
+
if (!envelope) return false;
|
|
10237
|
+
const payloadType = String(envelope.type ?? "").trim();
|
|
10238
|
+
if (payloadType === "e2ee.p2p_encrypted" || payloadType === "e2ee.group_encrypted") return true;
|
|
10239
|
+
return String(envelope.version ?? "").trim().toLowerCase() === "v2" && payloadType.startsWith("e2ee.");
|
|
10240
|
+
}
|
|
10241
|
+
function safeUndecryptablePushEvent(msg, group) {
|
|
10242
|
+
const event = {
|
|
10243
|
+
message_id: msg.message_id ?? null,
|
|
10244
|
+
from: msg.from ?? null,
|
|
10245
|
+
seq: msg.seq ?? null,
|
|
10246
|
+
timestamp: msg.timestamp ?? msg.t_server ?? null,
|
|
10247
|
+
device_id: msg.device_id ?? null,
|
|
10248
|
+
slot_id: msg.slot_id ?? null,
|
|
10249
|
+
_decrypt_error: "encrypted push payload is not decryptable on raw push path",
|
|
10250
|
+
_decrypt_stage: "push_envelope"
|
|
10251
|
+
};
|
|
10252
|
+
if (group) {
|
|
10253
|
+
event.group_id = msg.group_id ?? null;
|
|
10254
|
+
} else {
|
|
10255
|
+
event.to = msg.to ?? null;
|
|
10256
|
+
}
|
|
10257
|
+
const envelope = encryptedPushEnvelope(msg);
|
|
10258
|
+
if (envelope) {
|
|
10259
|
+
event._envelope_type = String(envelope.type ?? "");
|
|
10260
|
+
event._suite = String(envelope.suite ?? "");
|
|
10261
|
+
if (isV2EncryptedEnvelopePayload(envelope)) {
|
|
10262
|
+
attachV2EnvelopeMetadata(event, v2E2eeMeta(envelope));
|
|
10263
|
+
}
|
|
10264
|
+
}
|
|
10265
|
+
return event;
|
|
10266
|
+
}
|
|
11512
10267
|
function metadataWithoutAuth(value) {
|
|
11513
10268
|
if (!isJsonObject(value)) return null;
|
|
11514
10269
|
const body = {};
|
|
@@ -11552,8 +10307,23 @@ function normalizeDeliveryModeConfig(raw, opts = {}) {
|
|
|
11552
10307
|
affinity_ttl_ms: affinityTtlMs
|
|
11553
10308
|
};
|
|
11554
10309
|
}
|
|
10310
|
+
function assertClientOptions(value, label) {
|
|
10311
|
+
if (value == null) return;
|
|
10312
|
+
if (typeof value !== "object" || Array.isArray(value) || value instanceof AID) {
|
|
10313
|
+
throw new ValidationError(`${label} must be an options object`);
|
|
10314
|
+
}
|
|
10315
|
+
}
|
|
10316
|
+
function clientOptionsConfig(options) {
|
|
10317
|
+
const raw = { ...options ?? {} };
|
|
10318
|
+
if (Object.prototype.hasOwnProperty.call(raw, "aid")) {
|
|
10319
|
+
throw new ValidationError("AUNClient options must not include aid; pass an AID object as the first argument");
|
|
10320
|
+
}
|
|
10321
|
+
delete raw.debug;
|
|
10322
|
+
delete raw.protected_headers;
|
|
10323
|
+
return raw;
|
|
10324
|
+
}
|
|
11555
10325
|
var _AUNClient = class _AUNClient {
|
|
11556
|
-
constructor(
|
|
10326
|
+
constructor(first, second) {
|
|
11557
10327
|
/** SDK 配置模型 */
|
|
11558
10328
|
__publicField(this, "configModel");
|
|
11559
10329
|
/** 原始配置字典 */
|
|
@@ -11561,6 +10331,8 @@ var _AUNClient = class _AUNClient {
|
|
|
11561
10331
|
__publicField(this, "_aid", null);
|
|
11562
10332
|
__publicField(this, "_identity", null);
|
|
11563
10333
|
__publicField(this, "_state", "idle");
|
|
10334
|
+
__publicField(this, "_currentAid", null);
|
|
10335
|
+
__publicField(this, "_instanceProtectedHeaders", null);
|
|
11564
10336
|
__publicField(this, "_gatewayUrl", null);
|
|
11565
10337
|
__publicField(this, "_deviceId");
|
|
11566
10338
|
__publicField(this, "_slotId");
|
|
@@ -11575,12 +10347,6 @@ var _AUNClient = class _AUNClient {
|
|
|
11575
10347
|
__publicField(this, "_keystore");
|
|
11576
10348
|
__publicField(this, "_auth");
|
|
11577
10349
|
__publicField(this, "_transport");
|
|
11578
|
-
/** 认证命名空间 */
|
|
11579
|
-
__publicField(this, "auth");
|
|
11580
|
-
/** AID 托管命名空间 */
|
|
11581
|
-
__publicField(this, "custody");
|
|
11582
|
-
/** 元数据命名空间(心跳、状态、信任根管理) */
|
|
11583
|
-
__publicField(this, "meta");
|
|
11584
10350
|
// E2EE 编排状态(内存缓存)
|
|
11585
10351
|
__publicField(this, "_certCache", /* @__PURE__ */ new Map());
|
|
11586
10352
|
// 后台任务 handle(浏览器 setInterval/setTimeout)
|
|
@@ -11617,7 +10383,7 @@ var _AUNClient = class _AUNClient {
|
|
|
11617
10383
|
__publicField(this, "_localAgentMdEtag", "");
|
|
11618
10384
|
/** gateway 在 RPC envelope._meta.agent_md_etag 注入的服务端 etag;纯观察,无下游依赖。 */
|
|
11619
10385
|
__publicField(this, "_remoteAgentMdEtag", "");
|
|
11620
|
-
/** 浏览器侧
|
|
10386
|
+
/** 浏览器侧 AIDs 逻辑根目录,正文映射到 IndexedDB 里的 {aid}/agent.md。 */
|
|
11621
10387
|
__publicField(this, "_agentMdPath", "");
|
|
11622
10388
|
__publicField(this, "_agentMdCache", /* @__PURE__ */ new Map());
|
|
11623
10389
|
__publicField(this, "_agentMdFetchInflight", /* @__PURE__ */ new Set());
|
|
@@ -11641,6 +10407,14 @@ var _AUNClient = class _AUNClient {
|
|
|
11641
10407
|
__publicField(this, "_reconnectActive", false);
|
|
11642
10408
|
__publicField(this, "_reconnectAbort", null);
|
|
11643
10409
|
__publicField(this, "_serverKicked", false);
|
|
10410
|
+
// 重连状态追踪(对齐 Python client.py)
|
|
10411
|
+
__publicField(this, "_nextRetryAt", null);
|
|
10412
|
+
__publicField(this, "_retryAttempt", 0);
|
|
10413
|
+
__publicField(this, "_retryMaxAttempts", 0);
|
|
10414
|
+
__publicField(this, "_lastError", null);
|
|
10415
|
+
__publicField(this, "_lastErrorCode", null);
|
|
10416
|
+
/** 对端 AID 缓存(aid string → AID 对象) */
|
|
10417
|
+
__publicField(this, "_peerCache", /* @__PURE__ */ new Map());
|
|
11644
10418
|
/**
|
|
11645
10419
|
* 缓存最近一次服务端 gateway.disconnect 信息(含 code/reason/detail),
|
|
11646
10420
|
* 让后续 connection.state(terminal_failed) 也能携带 detail(如配额超限信息)。
|
|
@@ -11659,16 +10433,30 @@ var _AUNClient = class _AUNClient {
|
|
|
11659
10433
|
*/
|
|
11660
10434
|
__publicField(this, "_v2PullInflight", false);
|
|
11661
10435
|
__publicField(this, "_v2PullPending", false);
|
|
11662
|
-
|
|
10436
|
+
if (typeof first === "string") {
|
|
10437
|
+
throw new ValidationError("AUNClient aid must be an AID object, not a string");
|
|
10438
|
+
}
|
|
10439
|
+
if (typeof second === "boolean") {
|
|
10440
|
+
throw new ValidationError("AUNClient debug must be passed as options.debug");
|
|
10441
|
+
}
|
|
10442
|
+
const inputAid = first instanceof AID ? first : null;
|
|
10443
|
+
if (!inputAid && second !== void 0) {
|
|
10444
|
+
throw new ValidationError("AUNClient options-only construction accepts a single options object");
|
|
10445
|
+
}
|
|
10446
|
+
const options = inputAid ? second ?? {} : first ?? {};
|
|
10447
|
+
assertClientOptions(options, "AUNClient options");
|
|
10448
|
+
const rawConfig = clientOptionsConfig(options);
|
|
10449
|
+
if (inputAid) rawConfig.aun_path = inputAid.aunPath;
|
|
10450
|
+
const _debug = !!options?.debug;
|
|
11663
10451
|
this.configModel = createConfig(rawConfig);
|
|
11664
|
-
const initAid =
|
|
10452
|
+
const initAid = inputAid ? inputAid.aid : null;
|
|
11665
10453
|
this.config = {
|
|
11666
10454
|
aun_path: this.configModel.aunPath,
|
|
11667
10455
|
root_ca_path: this.configModel.rootCaPem,
|
|
11668
10456
|
seed_password: this.configModel.seedPassword
|
|
11669
10457
|
};
|
|
11670
10458
|
this._agentMdPath = this._agentMdDefaultRoot();
|
|
11671
|
-
this._deviceId = getDeviceId();
|
|
10459
|
+
this._deviceId = inputAid?.deviceId || getDeviceId();
|
|
11672
10460
|
this._logger = new AUNLogger({ debug: _debug, aunPath: this.configModel.aunPath });
|
|
11673
10461
|
this._logger.bindDeviceId(this._deviceId);
|
|
11674
10462
|
this._clientLog = this._logger.for("aun_core.client");
|
|
@@ -11681,7 +10469,7 @@ var _AUNClient = class _AUNClient {
|
|
|
11681
10469
|
this._dispatcher = new EventDispatcher();
|
|
11682
10470
|
this._discovery = new GatewayDiscovery();
|
|
11683
10471
|
this._keystore = new IndexedDBKeyStore({ encryptionSeed: this.configModel.seedPassword ?? void 0 });
|
|
11684
|
-
this._slotId = "";
|
|
10472
|
+
this._slotId = inputAid?.slotId || "default";
|
|
11685
10473
|
this._connectDeliveryMode = normalizeDeliveryModeConfig({ mode: "fanout" });
|
|
11686
10474
|
this._defaultConnectDeliveryMode = { ...this._connectDeliveryMode };
|
|
11687
10475
|
this._auth = new AuthFlow({
|
|
@@ -11704,27 +10492,29 @@ var _AUNClient = class _AUNClient {
|
|
|
11704
10492
|
this._clientLog.debug(`agent.md meta observer skipped: ${String(exc)}`);
|
|
11705
10493
|
});
|
|
11706
10494
|
});
|
|
11707
|
-
|
|
11708
|
-
|
|
11709
|
-
|
|
10495
|
+
if (inputAid) {
|
|
10496
|
+
if (!inputAid.isPrivateKeyValid()) throw new StateError("AUNClient requires an AID with a valid private key");
|
|
10497
|
+
this._currentAid = inputAid;
|
|
10498
|
+
this._identity = {
|
|
10499
|
+
aid: inputAid.aid,
|
|
10500
|
+
private_key_pem: inputAid._privateKeyPem ?? "",
|
|
10501
|
+
public_key_der_b64: inputAid.publicKey,
|
|
10502
|
+
cert: inputAid.certPem
|
|
10503
|
+
};
|
|
10504
|
+
this._state = "disconnected";
|
|
10505
|
+
}
|
|
10506
|
+
if (options?.protected_headers !== void 0) {
|
|
10507
|
+
this.setProtectedHeaders(options.protected_headers);
|
|
10508
|
+
}
|
|
11710
10509
|
this._auth.setLogger(this._logAuth);
|
|
11711
10510
|
this._transport.setLogger(this._logTransport);
|
|
11712
10511
|
this._dispatcher.setLogger(this._logEvents);
|
|
11713
10512
|
if (typeof this._discovery.setLogger === "function") {
|
|
11714
10513
|
this._discovery.setLogger(this._logger.for("aun_core.discovery"));
|
|
11715
10514
|
}
|
|
11716
|
-
if (typeof this.auth.setLogger === "function") {
|
|
11717
|
-
this.auth.setLogger(this._logger.for("aun_core.namespace.auth"));
|
|
11718
|
-
}
|
|
11719
|
-
if (typeof this.custody.setLogger === "function") {
|
|
11720
|
-
this.custody.setLogger(this._logger.for("aun_core.namespace.custody"));
|
|
11721
|
-
}
|
|
11722
10515
|
if (typeof this._keystore.setLogger === "function") {
|
|
11723
10516
|
this._keystore.setLogger(this._logKeystore);
|
|
11724
10517
|
}
|
|
11725
|
-
if (typeof this.meta.setLogger === "function") {
|
|
11726
|
-
this.meta.setLogger(this._logger.for("aun_core.namespace.meta"));
|
|
11727
|
-
}
|
|
11728
10518
|
this._dispatcher.subscribe("_raw.message.received", (data) => {
|
|
11729
10519
|
this._onRawMessageReceived(data);
|
|
11730
10520
|
});
|
|
@@ -11768,17 +10558,169 @@ var _AUNClient = class _AUNClient {
|
|
|
11768
10558
|
get aid() {
|
|
11769
10559
|
return this._aid;
|
|
11770
10560
|
}
|
|
11771
|
-
|
|
10561
|
+
_setAgentMdRoot(root) {
|
|
11772
10562
|
const next = String(root ?? "").trim() || this._agentMdDefaultRoot();
|
|
11773
10563
|
this._agentMdPath = next;
|
|
11774
10564
|
this._agentMdCache.clear();
|
|
11775
10565
|
return next;
|
|
11776
10566
|
}
|
|
11777
|
-
|
|
11778
|
-
|
|
10567
|
+
async _resolveAgentMdUrl(aid) {
|
|
10568
|
+
const target = String(aid ?? "").trim();
|
|
10569
|
+
if (!target) throw new ValidationError("agent.md requires non-empty aid");
|
|
10570
|
+
let gatewayUrl = String(this._gatewayUrl ?? "").trim();
|
|
10571
|
+
if (!gatewayUrl) {
|
|
10572
|
+
try {
|
|
10573
|
+
gatewayUrl = await this._resolveGatewayForAid(target);
|
|
10574
|
+
} catch {
|
|
10575
|
+
gatewayUrl = "";
|
|
10576
|
+
}
|
|
10577
|
+
}
|
|
10578
|
+
const authority = agentMdAuthority(target, this.configModel.discoveryPort);
|
|
10579
|
+
return `${agentMdHttpScheme(gatewayUrl)}://${authority}/agent.md`;
|
|
10580
|
+
}
|
|
10581
|
+
async _ensureAgentMdUploadToken(aid, gatewayUrl) {
|
|
10582
|
+
let identity = await this._auth.loadIdentityOrNone(aid);
|
|
10583
|
+
if (!identity && this._identity && String(this._identity.aid ?? "") === aid) {
|
|
10584
|
+
identity = this._identity;
|
|
10585
|
+
}
|
|
10586
|
+
if (!identity) {
|
|
10587
|
+
throw new StateError("no local identity found, register or load an AID first");
|
|
10588
|
+
}
|
|
10589
|
+
const cachedToken = String(identity.access_token ?? "");
|
|
10590
|
+
const expiresAt = this._auth.getAccessTokenExpiry(identity);
|
|
10591
|
+
if (cachedToken && (expiresAt === null || expiresAt > Date.now() / 1e3 + 30)) {
|
|
10592
|
+
return cachedToken;
|
|
10593
|
+
}
|
|
10594
|
+
if (identity.refresh_token) {
|
|
10595
|
+
try {
|
|
10596
|
+
const refreshed = await this._auth.refreshCachedTokens(gatewayUrl, identity);
|
|
10597
|
+
const refreshedToken = String(refreshed.access_token ?? "");
|
|
10598
|
+
const refreshedExpiry = this._auth.getAccessTokenExpiry(refreshed);
|
|
10599
|
+
if (refreshedToken && (refreshedExpiry === null || refreshedExpiry > Date.now() / 1e3 + 30)) {
|
|
10600
|
+
this._identity = refreshed;
|
|
10601
|
+
return refreshedToken;
|
|
10602
|
+
}
|
|
10603
|
+
} catch {
|
|
10604
|
+
}
|
|
10605
|
+
}
|
|
10606
|
+
const result = await this._auth.authenticate(gatewayUrl, aid);
|
|
10607
|
+
const token = String(result.access_token ?? "");
|
|
10608
|
+
if (!token) throw new StateError("authenticate did not return access_token");
|
|
10609
|
+
const fallbackIdentity = {
|
|
10610
|
+
...identity,
|
|
10611
|
+
access_token: token,
|
|
10612
|
+
refresh_token: String(result.refresh_token ?? identity.refresh_token ?? "")
|
|
10613
|
+
};
|
|
10614
|
+
const fallbackExpiresAt = Number(result.expires_at ?? identity.expires_at ?? NaN);
|
|
10615
|
+
if (Number.isFinite(fallbackExpiresAt)) fallbackIdentity.expires_at = fallbackExpiresAt;
|
|
10616
|
+
this._identity = await this._auth.loadIdentityOrNone(aid) ?? fallbackIdentity;
|
|
10617
|
+
return token;
|
|
10618
|
+
}
|
|
10619
|
+
async _uploadAgentMd(content) {
|
|
10620
|
+
const target = String(this._aid ?? this._currentAid?.aid ?? "").trim();
|
|
10621
|
+
if (!target) throw new StateError("uploadAgentMd requires local AID");
|
|
10622
|
+
const gatewayUrl = await this._resolveGatewayForAid(target);
|
|
10623
|
+
this._gatewayUrl = gatewayUrl;
|
|
10624
|
+
const token = await this._ensureAgentMdUploadToken(target, gatewayUrl);
|
|
10625
|
+
const response = await fetchWithTimeout(await this._resolveAgentMdUrl(target), {
|
|
10626
|
+
method: "PUT",
|
|
10627
|
+
headers: {
|
|
10628
|
+
Authorization: `Bearer ${token}`,
|
|
10629
|
+
"Content-Type": "text/markdown; charset=utf-8"
|
|
10630
|
+
},
|
|
10631
|
+
body: content
|
|
10632
|
+
});
|
|
10633
|
+
if (response.status === 404) {
|
|
10634
|
+
throw new NotFoundError(`agent.md endpoint not found for aid: ${target}`);
|
|
10635
|
+
}
|
|
10636
|
+
if (!response.ok) {
|
|
10637
|
+
const message = (await response.text()).trim();
|
|
10638
|
+
throw new AUNError(`upload agent.md failed: HTTP ${response.status}${message ? ` - ${message}` : ""}`);
|
|
10639
|
+
}
|
|
10640
|
+
const payload = await response.json();
|
|
10641
|
+
if (!isJsonObject(payload)) throw new AUNError("upload agent.md returned invalid JSON payload");
|
|
10642
|
+
return payload;
|
|
10643
|
+
}
|
|
10644
|
+
async _downloadAgentMd(aid) {
|
|
10645
|
+
const target = String(aid ?? "").trim();
|
|
10646
|
+
if (!target) throw new ValidationError("downloadAgentMd requires non-empty aid");
|
|
10647
|
+
const cached = this._agentMdCache.get(target);
|
|
10648
|
+
const url = await this._resolveAgentMdUrl(target);
|
|
10649
|
+
const response = await fetchWithTimeout(url, {
|
|
10650
|
+
method: "GET",
|
|
10651
|
+
headers: { Accept: "text/markdown" },
|
|
10652
|
+
redirect: "follow"
|
|
10653
|
+
});
|
|
10654
|
+
if (response.status === 304 && typeof cached?.text === "string") {
|
|
10655
|
+
return String(cached.text);
|
|
10656
|
+
}
|
|
10657
|
+
if (response.status === 404) {
|
|
10658
|
+
throw new NotFoundError(`agent.md not found for aid: ${target}`);
|
|
10659
|
+
}
|
|
10660
|
+
if (!response.ok) {
|
|
10661
|
+
const message = (await response.text()).trim();
|
|
10662
|
+
throw new AUNError(`download agent.md failed: HTTP ${response.status}${message ? ` - ${message}` : ""}`);
|
|
10663
|
+
}
|
|
10664
|
+
const text = await response.text();
|
|
10665
|
+
const etag = String(response.headers?.get("ETag") ?? response.headers?.get("etag") ?? "").trim();
|
|
10666
|
+
const lastModified = String(response.headers?.get("Last-Modified") ?? response.headers?.get("last-modified") ?? "").trim();
|
|
10667
|
+
this._agentMdCache.set(target, {
|
|
10668
|
+
...cached ?? {},
|
|
10669
|
+
text,
|
|
10670
|
+
etag,
|
|
10671
|
+
lastModified,
|
|
10672
|
+
remote_etag: etag,
|
|
10673
|
+
last_modified: lastModified
|
|
10674
|
+
});
|
|
10675
|
+
return text;
|
|
10676
|
+
}
|
|
10677
|
+
async _headAgentMd(aid) {
|
|
10678
|
+
const target = String(aid ?? "").trim();
|
|
10679
|
+
if (!target) throw new ValidationError("headAgentMd requires non-empty aid");
|
|
10680
|
+
const response = await fetchWithTimeout(await this._resolveAgentMdUrl(target), {
|
|
10681
|
+
method: "HEAD",
|
|
10682
|
+
headers: { Accept: "text/markdown" }
|
|
10683
|
+
});
|
|
10684
|
+
const etag = String(response.headers?.get("ETag") ?? response.headers?.get("etag") ?? "").trim();
|
|
10685
|
+
const lastModified = String(response.headers?.get("Last-Modified") ?? response.headers?.get("last-modified") ?? "").trim();
|
|
10686
|
+
if (response.status === 404) {
|
|
10687
|
+
return { aid: target, found: false, etag: "", last_modified: "", status: 404 };
|
|
10688
|
+
}
|
|
10689
|
+
if (!response.ok) {
|
|
10690
|
+
throw new AUNError(`head agent.md failed: HTTP ${response.status}`);
|
|
10691
|
+
}
|
|
10692
|
+
const cached = this._agentMdCache.get(target) ?? {};
|
|
10693
|
+
this._agentMdCache.set(target, {
|
|
10694
|
+
...cached,
|
|
10695
|
+
etag,
|
|
10696
|
+
lastModified,
|
|
10697
|
+
remote_etag: etag,
|
|
10698
|
+
last_modified: lastModified
|
|
10699
|
+
});
|
|
10700
|
+
return { aid: target, found: true, etag, last_modified: lastModified, status: response.status };
|
|
11779
10701
|
}
|
|
11780
|
-
|
|
11781
|
-
|
|
10702
|
+
async _verifyAgentMd(content, aid) {
|
|
10703
|
+
const target = String(aid ?? "").trim();
|
|
10704
|
+
if (!target) throw new ValidationError("verifyAgentMd requires non-empty aid");
|
|
10705
|
+
let peer = target === this._currentAid?.aid ? this._currentAid : null;
|
|
10706
|
+
if (!peer) {
|
|
10707
|
+
let certPem = String(await this._keystore.loadCert(target) ?? "").trim();
|
|
10708
|
+
if (!certPem) {
|
|
10709
|
+
certPem = String(await this._fetchPeerCert(target) ?? "").trim();
|
|
10710
|
+
}
|
|
10711
|
+
if (!certPem) throw new NotFoundError(`certificate not found for aid: ${target}`);
|
|
10712
|
+
peer = await AID.create({
|
|
10713
|
+
aid: target,
|
|
10714
|
+
aunPath: this.configModel.aunPath,
|
|
10715
|
+
certPem,
|
|
10716
|
+
privateKeyPem: null,
|
|
10717
|
+
certValid: true,
|
|
10718
|
+
privateKeyValid: false
|
|
10719
|
+
});
|
|
10720
|
+
}
|
|
10721
|
+
const result = await peer.verifyAgentMd(content);
|
|
10722
|
+
if (!result.ok) throw new AUNError(result.error.message);
|
|
10723
|
+
return { ...result.data, verified: result.data.status === "verified" };
|
|
11782
10724
|
}
|
|
11783
10725
|
/**
|
|
11784
10726
|
* 浏览器版本 publishAgentMd。默认从 {agentMdPath}/{self_aid}/agent.md 的等价 IndexedDB 正文读取,
|
|
@@ -11806,8 +10748,12 @@ var _AUNClient = class _AUNClient {
|
|
|
11806
10748
|
if (localContent === null || localContent.length === 0) {
|
|
11807
10749
|
throw new ValidationError("publishAgentMd requires local agent.md content");
|
|
11808
10750
|
}
|
|
11809
|
-
const
|
|
11810
|
-
|
|
10751
|
+
const signedResult = await this._currentAid?.signAgentMd(localContent);
|
|
10752
|
+
if (!signedResult?.ok) {
|
|
10753
|
+
throw new StateError(signedResult?.error.message ?? "publishAgentMd requires a valid local AID private key");
|
|
10754
|
+
}
|
|
10755
|
+
const signed = signedResult.data.signed;
|
|
10756
|
+
const result = await this._uploadAgentMd(signed);
|
|
11811
10757
|
this._localAgentMdEtag = await this._agentMdContentEtag(signed);
|
|
11812
10758
|
const remoteEtag = isJsonObject(result) ? String(result.etag ?? "").trim() : "";
|
|
11813
10759
|
if (remoteEtag) this._remoteAgentMdEtag = remoteEtag;
|
|
@@ -11826,13 +10772,13 @@ var _AUNClient = class _AUNClient {
|
|
|
11826
10772
|
* 浏览器版本 fetchAgentMd。aid 缺省时取自身;下载后的正文固定写入
|
|
11827
10773
|
* {agentMdPath}/{aid}/agent.md 的等价 IndexedDB 正文,agentmd.json 只保存元数据。
|
|
11828
10774
|
*/
|
|
11829
|
-
async
|
|
10775
|
+
async _fetchAgentMdCache(aid) {
|
|
11830
10776
|
const target = String(aid ?? this._aid ?? "").trim();
|
|
11831
10777
|
if (!target) {
|
|
11832
10778
|
throw new ValidationError("fetchAgentMd requires aid (or local AID)");
|
|
11833
10779
|
}
|
|
11834
|
-
const content = await this.
|
|
11835
|
-
const signature = await this.
|
|
10780
|
+
const content = await this._downloadAgentMd(target);
|
|
10781
|
+
const signature = await this._verifyAgentMd(content, target);
|
|
11836
10782
|
const isSelf = target === (this._aid ?? "");
|
|
11837
10783
|
const localEtag = await this._agentMdContentEtag(content);
|
|
11838
10784
|
const cacheMeta = this._agentMdAuthCacheMeta(target);
|
|
@@ -11880,7 +10826,7 @@ var _AUNClient = class _AUNClient {
|
|
|
11880
10826
|
return String(this._aid ?? "").trim();
|
|
11881
10827
|
}
|
|
11882
10828
|
_agentMdDefaultRoot() {
|
|
11883
|
-
return this._joinAgentMdPath(this.configModel.aunPath || ".", "
|
|
10829
|
+
return this._joinAgentMdPath(this.configModel.aunPath || ".", "AIDs");
|
|
11884
10830
|
}
|
|
11885
10831
|
_joinAgentMdPath(base, name) {
|
|
11886
10832
|
const left = String(base ?? "").trim().replace(/[\\/]+$/g, "");
|
|
@@ -11982,8 +10928,7 @@ var _AUNClient = class _AUNClient {
|
|
|
11982
10928
|
}
|
|
11983
10929
|
_agentMdAuthCacheMeta(aid) {
|
|
11984
10930
|
try {
|
|
11985
|
-
const
|
|
11986
|
-
const record = store?.get(String(aid ?? "").trim());
|
|
10931
|
+
const record = this._agentMdCache.get(String(aid ?? "").trim());
|
|
11987
10932
|
return record && typeof record === "object" ? { ...record } : {};
|
|
11988
10933
|
} catch {
|
|
11989
10934
|
return {};
|
|
@@ -12085,7 +11030,7 @@ var _AUNClient = class _AUNClient {
|
|
|
12085
11030
|
if (this._agentMdFetchInflight.has(target)) return;
|
|
12086
11031
|
this._agentMdFetchInflight.add(target);
|
|
12087
11032
|
try {
|
|
12088
|
-
await this.
|
|
11033
|
+
await this._fetchAgentMdCache(target);
|
|
12089
11034
|
} catch (err) {
|
|
12090
11035
|
await this._saveAgentMdRecord(target, {
|
|
12091
11036
|
last_error: err instanceof Error ? err.message : String(err),
|
|
@@ -12140,7 +11085,7 @@ var _AUNClient = class _AUNClient {
|
|
|
12140
11085
|
"envelope"
|
|
12141
11086
|
);
|
|
12142
11087
|
}
|
|
12143
|
-
async
|
|
11088
|
+
async _checkAgentMdCache(aid, maxUnsyncedDays = 0) {
|
|
12144
11089
|
const target = String(aid ?? this._aid ?? "").trim();
|
|
12145
11090
|
if (!target) throw new ValidationError("checkAgentMd requires aid (or local AID)");
|
|
12146
11091
|
const before = await this._loadAgentMdRecord(target) ?? {};
|
|
@@ -12168,7 +11113,7 @@ var _AUNClient = class _AUNClient {
|
|
|
12168
11113
|
const now = Date.now();
|
|
12169
11114
|
let remote;
|
|
12170
11115
|
try {
|
|
12171
|
-
remote = await this.
|
|
11116
|
+
remote = await this._headAgentMd(target);
|
|
12172
11117
|
} catch (err) {
|
|
12173
11118
|
await this._saveAgentMdRecord(target, { checked_at: now, remote_status: "error", last_error: err instanceof Error ? err.message : String(err) });
|
|
12174
11119
|
throw err;
|
|
@@ -12222,7 +11167,118 @@ var _AUNClient = class _AUNClient {
|
|
|
12222
11167
|
}
|
|
12223
11168
|
}
|
|
12224
11169
|
get state() {
|
|
12225
|
-
return this._state;
|
|
11170
|
+
return this._publicState(this._state);
|
|
11171
|
+
}
|
|
11172
|
+
_publicState(state) {
|
|
11173
|
+
return STATE_TO_PUBLIC[state] ?? state;
|
|
11174
|
+
}
|
|
11175
|
+
get currentAid() {
|
|
11176
|
+
return this._currentAid;
|
|
11177
|
+
}
|
|
11178
|
+
get hasIdentity() {
|
|
11179
|
+
return this._currentAid !== null && this.state !== "closed" /* CLOSED */;
|
|
11180
|
+
}
|
|
11181
|
+
get canSign() {
|
|
11182
|
+
return this.hasIdentity && !!this._currentAid?.isPrivateKeyValid();
|
|
11183
|
+
}
|
|
11184
|
+
get canConnect() {
|
|
11185
|
+
return this.hasIdentity && this.state !== "closed" /* CLOSED */;
|
|
11186
|
+
}
|
|
11187
|
+
get canSend() {
|
|
11188
|
+
return this.state === "ready" /* READY */;
|
|
11189
|
+
}
|
|
11190
|
+
get isReady() {
|
|
11191
|
+
return this.canSend;
|
|
11192
|
+
}
|
|
11193
|
+
get isOnline() {
|
|
11194
|
+
return this.state === "ready" /* READY */ || this.state === "reconnecting" /* RECONNECTING */ || this.state === "retry_backoff" /* RETRY_BACKOFF */;
|
|
11195
|
+
}
|
|
11196
|
+
get isClosed() {
|
|
11197
|
+
return this.state === "closed" /* CLOSED */;
|
|
11198
|
+
}
|
|
11199
|
+
get aunPath() {
|
|
11200
|
+
return this.hasIdentity ? this._currentAid?.aunPath ?? this.configModel.aunPath : null;
|
|
11201
|
+
}
|
|
11202
|
+
/** 下次重连时间(仅在 retry_backoff 状态时非 null,对齐 Python next_retry_at) */
|
|
11203
|
+
get nextRetryAt() {
|
|
11204
|
+
return this.state === "retry_backoff" /* RETRY_BACKOFF */ ? this._nextRetryAt : null;
|
|
11205
|
+
}
|
|
11206
|
+
/** 距下次重连的剩余秒数(仅在 retry_backoff 状态时非 null,对齐 Python next_retry_in_seconds) */
|
|
11207
|
+
get nextRetryInSeconds() {
|
|
11208
|
+
const t = this.nextRetryAt;
|
|
11209
|
+
if (t === null) return null;
|
|
11210
|
+
return Math.max(0, (t.getTime() - Date.now()) / 1e3);
|
|
11211
|
+
}
|
|
11212
|
+
/** 当前重连尝试次数(对齐 Python retry_attempt) */
|
|
11213
|
+
get retryAttempt() {
|
|
11214
|
+
return this._retryAttempt;
|
|
11215
|
+
}
|
|
11216
|
+
/** 最大重连次数(0 = 无限,对齐 Python retry_max_attempts) */
|
|
11217
|
+
get retryMaxAttempts() {
|
|
11218
|
+
return this._retryMaxAttempts;
|
|
11219
|
+
}
|
|
11220
|
+
/** 最近一次错误(对齐 Python last_error) */
|
|
11221
|
+
get lastError() {
|
|
11222
|
+
return this._lastError;
|
|
11223
|
+
}
|
|
11224
|
+
/** 最近一次错误码(对齐 Python last_error_code) */
|
|
11225
|
+
get lastErrorCode() {
|
|
11226
|
+
return this._lastErrorCode;
|
|
11227
|
+
}
|
|
11228
|
+
loadIdentity(aid) {
|
|
11229
|
+
if (!aid?.isPrivateKeyValid()) throw new StateError("loadIdentity requires an AID with a valid private key");
|
|
11230
|
+
const publicState = this.state;
|
|
11231
|
+
if (publicState !== "no_identity" /* NO_IDENTITY */ && publicState !== "closed" /* CLOSED */) {
|
|
11232
|
+
throw new StateError(`loadIdentity not allowed in state ${publicState}`);
|
|
11233
|
+
}
|
|
11234
|
+
this._currentAid = aid;
|
|
11235
|
+
this._aid = aid.aid;
|
|
11236
|
+
this._identity = {
|
|
11237
|
+
aid: aid.aid,
|
|
11238
|
+
private_key_pem: aid._privateKeyPem ?? "",
|
|
11239
|
+
public_key_der_b64: aid.publicKey,
|
|
11240
|
+
cert: aid.certPem
|
|
11241
|
+
};
|
|
11242
|
+
this._auth._aid = aid.aid;
|
|
11243
|
+
this._state = "disconnected";
|
|
11244
|
+
this._closing = false;
|
|
11245
|
+
}
|
|
11246
|
+
setProtectedHeaders(headers) {
|
|
11247
|
+
if (!headers) {
|
|
11248
|
+
this._instanceProtectedHeaders = null;
|
|
11249
|
+
return;
|
|
11250
|
+
}
|
|
11251
|
+
const cleaned = {};
|
|
11252
|
+
for (const [key, value] of Object.entries(headers)) {
|
|
11253
|
+
if (key === "_auth") continue;
|
|
11254
|
+
cleaned[String(key)] = String(value);
|
|
11255
|
+
}
|
|
11256
|
+
this._instanceProtectedHeaders = Object.keys(cleaned).length ? cleaned : null;
|
|
11257
|
+
}
|
|
11258
|
+
getProtectedHeaders() {
|
|
11259
|
+
return this._instanceProtectedHeaders ? { ...this._instanceProtectedHeaders } : null;
|
|
11260
|
+
}
|
|
11261
|
+
cachePeer(aid) {
|
|
11262
|
+
if (!this.hasIdentity) throw new StateError("cachePeer requires a loaded identity");
|
|
11263
|
+
if (!aid.isCertValid()) throw new ValidationError("cachePeer requires an AID with a valid certificate");
|
|
11264
|
+
this._peerCache.set(aid.aid, aid);
|
|
11265
|
+
return aid;
|
|
11266
|
+
}
|
|
11267
|
+
getPeer(aid) {
|
|
11268
|
+
if (!this.hasIdentity) throw new StateError("getPeer requires a loaded identity");
|
|
11269
|
+
return this._peerCache.get(String(aid ?? "").trim()) ?? null;
|
|
11270
|
+
}
|
|
11271
|
+
async lookupPeer(aid) {
|
|
11272
|
+
if (!this.hasIdentity) throw new StateError("lookupPeer requires a loaded identity");
|
|
11273
|
+
const target = String(aid ?? "").trim();
|
|
11274
|
+
if (!target) throw new ValidationError("lookupPeer requires non-empty aid");
|
|
11275
|
+
const cached = this._peerCache.get(target);
|
|
11276
|
+
if (cached) return cached;
|
|
11277
|
+
throw new NotFoundError(`peer not found in cache: ${target}`);
|
|
11278
|
+
}
|
|
11279
|
+
peers() {
|
|
11280
|
+
if (!this.hasIdentity) throw new StateError("peers requires a loaded identity");
|
|
11281
|
+
return [...this._peerCache.entries()].sort(([a], [b]) => a.localeCompare(b)).map(([, v]) => v);
|
|
12226
11282
|
}
|
|
12227
11283
|
get gatewayUrl() {
|
|
12228
11284
|
return this._gatewayUrl;
|
|
@@ -12237,35 +11293,64 @@ var _AUNClient = class _AUNClient {
|
|
|
12237
11293
|
get gatewayHealth() {
|
|
12238
11294
|
return this._discovery.lastHealthy;
|
|
12239
11295
|
}
|
|
12240
|
-
|
|
12241
|
-
|
|
11296
|
+
// ── 生命周期 ──────────────────────────────────────
|
|
11297
|
+
/** 仅认证当前身份,获取/刷新 token,但不建立长连接。 */
|
|
11298
|
+
async authenticate(options = {}) {
|
|
12242
11299
|
const tStart = Date.now();
|
|
12243
|
-
this.
|
|
11300
|
+
const target = this._currentAid?.aid ?? this._aid ?? "";
|
|
11301
|
+
if (!target || !this._currentAid?.isPrivateKeyValid()) {
|
|
11302
|
+
throw new StateError("authenticate requires a loaded AID with a valid private key");
|
|
11303
|
+
}
|
|
11304
|
+
const publicState = this.state;
|
|
11305
|
+
if (publicState !== "standby" /* STANDBY */ && publicState !== "authenticated" /* AUTHENTICATED */) {
|
|
11306
|
+
throw new StateError(`authenticate not allowed in state ${publicState}`);
|
|
11307
|
+
}
|
|
11308
|
+
if ("aid" in options || "access_token" in options || "token" in options || "kite_token" in options) {
|
|
11309
|
+
throw new ValidationError("authenticate options must not include aid or token fields; load an AID object first");
|
|
11310
|
+
}
|
|
11311
|
+
this._state = "connecting";
|
|
12244
11312
|
try {
|
|
12245
|
-
const
|
|
12246
|
-
this.
|
|
11313
|
+
const gateway = String(options.gateway ?? this._gatewayUrl ?? await this._resolveGatewayForAid(target)).trim();
|
|
11314
|
+
const result = await this._auth.authenticate(gateway, target);
|
|
11315
|
+
this._gatewayUrl = String(result.gateway ?? gateway);
|
|
11316
|
+
this._identity = await this._auth.loadIdentityOrNone(target);
|
|
11317
|
+
this._state = "authenticated";
|
|
11318
|
+
this._clientLog.debug(`authenticate exit: elapsed=${Date.now() - tStart}ms aid=${target}`);
|
|
12247
11319
|
return result;
|
|
12248
11320
|
} catch (err) {
|
|
12249
|
-
this.
|
|
11321
|
+
this._state = "disconnected";
|
|
11322
|
+
this._clientLog.debug(`authenticate exit (error): elapsed=${Date.now() - tStart}ms err=${err instanceof Error ? err.message : String(err)}`);
|
|
12250
11323
|
throw err;
|
|
12251
11324
|
}
|
|
12252
11325
|
}
|
|
12253
|
-
|
|
12254
|
-
|
|
12255
|
-
* 连接到 Gateway。
|
|
12256
|
-
*
|
|
12257
|
-
* @param auth - 认证参数,必须包含 access_token 和 gateway
|
|
12258
|
-
* @param options - 可选的会话选项(auto_reconnect, heartbeat_interval 等)
|
|
12259
|
-
*/
|
|
12260
|
-
async connect(auth, options) {
|
|
11326
|
+
/** 连接到 Gateway;身份来自构造函数或 loadIdentity(aid),认证由 SDK 内部自动完成。 */
|
|
11327
|
+
async connect(options = {}) {
|
|
12261
11328
|
const tStart = Date.now();
|
|
12262
11329
|
this._clientLog.debug(`connect enter: state=${this._state} aid=${this._aid ?? "-"}`);
|
|
12263
|
-
if (
|
|
11330
|
+
if (arguments.length > 1) {
|
|
11331
|
+
throw new ValidationError("connect accepts a single options object");
|
|
11332
|
+
}
|
|
11333
|
+
if ("aid" in options || "access_token" in options || "token" in options || "kite_token" in options) {
|
|
11334
|
+
throw new ValidationError("connect options must not include aid or token fields; load an AID object first");
|
|
11335
|
+
}
|
|
11336
|
+
const target = this._currentAid?.aid ?? this._aid ?? "";
|
|
11337
|
+
if (!target || !this._currentAid?.isPrivateKeyValid()) {
|
|
11338
|
+
throw new StateError("connect requires a loaded AID with a valid private key");
|
|
11339
|
+
}
|
|
11340
|
+
const publicState = this.state;
|
|
11341
|
+
const allowed = /* @__PURE__ */ new Set([
|
|
11342
|
+
"standby" /* STANDBY */,
|
|
11343
|
+
"authenticated" /* AUTHENTICATED */,
|
|
11344
|
+
"retry_backoff" /* RETRY_BACKOFF */,
|
|
11345
|
+
"connection_failed" /* CONNECTION_FAILED */
|
|
11346
|
+
]);
|
|
11347
|
+
if (!allowed.has(publicState)) {
|
|
12264
11348
|
this._clientLog.debug(`connect exit (error): elapsed=${Date.now() - tStart}ms err=invalid_state state=${this._state}`);
|
|
12265
|
-
throw new StateError(`connect not allowed in state ${
|
|
11349
|
+
throw new StateError(`connect not allowed in state ${publicState}`);
|
|
12266
11350
|
}
|
|
12267
11351
|
this._state = "connecting";
|
|
12268
|
-
const
|
|
11352
|
+
const gateway = String(options.gateway ?? this._gatewayUrl ?? await this._resolveGatewayForAid(target)).trim();
|
|
11353
|
+
const params = { ...options, gateway };
|
|
12269
11354
|
const normalized = this._normalizeConnectParams(params);
|
|
12270
11355
|
this._sessionParams = normalized;
|
|
12271
11356
|
this._sessionOptions = this._buildSessionOptions(normalized);
|
|
@@ -12276,7 +11361,7 @@ var _AUNClient = class _AUNClient {
|
|
|
12276
11361
|
for (const gw of gateways) {
|
|
12277
11362
|
try {
|
|
12278
11363
|
const gwParams = { ...normalized, gateway: gw };
|
|
12279
|
-
await this._connectOnce(gwParams,
|
|
11364
|
+
await this._connectOnce(gwParams, true);
|
|
12280
11365
|
this._clientLog.debug(`connect exit: elapsed=${Date.now() - tStart}ms state=${this._state}`);
|
|
12281
11366
|
return;
|
|
12282
11367
|
} catch (err) {
|
|
@@ -12290,7 +11375,7 @@ var _AUNClient = class _AUNClient {
|
|
|
12290
11375
|
}
|
|
12291
11376
|
}
|
|
12292
11377
|
if (this._state === "connecting" || this._state === "authenticating") {
|
|
12293
|
-
this._state = "
|
|
11378
|
+
this._state = "terminal_failed";
|
|
12294
11379
|
}
|
|
12295
11380
|
this._clientLog.debug(`connect exit (error): elapsed=${Date.now() - tStart}ms err=${lastErr instanceof Error ? lastErr.message : String(lastErr)}`);
|
|
12296
11381
|
throw lastErr;
|
|
@@ -12312,52 +11397,9 @@ var _AUNClient = class _AUNClient {
|
|
|
12312
11397
|
}
|
|
12313
11398
|
await this._transport.close();
|
|
12314
11399
|
this._state = "disconnected";
|
|
12315
|
-
await this._dispatcher.publish("connection.state", { state: this._state });
|
|
11400
|
+
await this._dispatcher.publish("connection.state", { state: this._publicState(this._state) });
|
|
12316
11401
|
this._clientLog.debug(`disconnect exit: elapsed=${Date.now() - tStart}ms`);
|
|
12317
11402
|
}
|
|
12318
|
-
/** 列出本地所有已存储的身份摘要(仅返回有有效私钥的 AID) */
|
|
12319
|
-
async listIdentities() {
|
|
12320
|
-
const tStart = Date.now();
|
|
12321
|
-
this._clientLog.debug("listIdentities enter");
|
|
12322
|
-
try {
|
|
12323
|
-
const listFn = this._keystore.listIdentities;
|
|
12324
|
-
if (typeof listFn !== "function") {
|
|
12325
|
-
this._clientLog.debug(`listIdentities exit: elapsed=${Date.now() - tStart}ms count=0 reason=keystore_no_list`);
|
|
12326
|
-
return [];
|
|
12327
|
-
}
|
|
12328
|
-
const aids = await listFn.call(this._keystore);
|
|
12329
|
-
const summaries = [];
|
|
12330
|
-
for (const aid of [...aids].sort()) {
|
|
12331
|
-
const identity = await this._keystore.loadIdentity(aid);
|
|
12332
|
-
if (!identity || !identity.private_key_pem) continue;
|
|
12333
|
-
const summary = { aid };
|
|
12334
|
-
const loadMeta = this._keystore.loadMetadata;
|
|
12335
|
-
if (typeof loadMeta === "function") {
|
|
12336
|
-
const md = await loadMeta.call(this._keystore, aid);
|
|
12337
|
-
if (md && Object.keys(md).length > 0) {
|
|
12338
|
-
summary.metadata = md;
|
|
12339
|
-
}
|
|
12340
|
-
}
|
|
12341
|
-
if (!summary.metadata) {
|
|
12342
|
-
const metadata = {};
|
|
12343
|
-
for (const [key, value] of Object.entries(identity)) {
|
|
12344
|
-
if (!["aid", "private_key_pem", "public_key_der_b64", "curve", "cert"].includes(key)) {
|
|
12345
|
-
metadata[key] = value;
|
|
12346
|
-
}
|
|
12347
|
-
}
|
|
12348
|
-
if (Object.keys(metadata).length > 0) {
|
|
12349
|
-
summary.metadata = metadata;
|
|
12350
|
-
}
|
|
12351
|
-
}
|
|
12352
|
-
summaries.push(summary);
|
|
12353
|
-
}
|
|
12354
|
-
this._clientLog.debug(`listIdentities exit: elapsed=${Date.now() - tStart}ms count=${summaries.length}`);
|
|
12355
|
-
return summaries;
|
|
12356
|
-
} catch (err) {
|
|
12357
|
-
this._clientLog.debug(`listIdentities exit (error): elapsed=${Date.now() - tStart}ms err=${err instanceof Error ? err.message : String(err)}`);
|
|
12358
|
-
throw err;
|
|
12359
|
-
}
|
|
12360
|
-
}
|
|
12361
11403
|
/** 关闭连接 */
|
|
12362
11404
|
async close() {
|
|
12363
11405
|
const tStart = Date.now();
|
|
@@ -12382,7 +11424,7 @@ var _AUNClient = class _AUNClient {
|
|
|
12382
11424
|
}
|
|
12383
11425
|
await this._transport.close();
|
|
12384
11426
|
this._state = "closed";
|
|
12385
|
-
await this._dispatcher.publish("connection.state", { state: this._state });
|
|
11427
|
+
await this._dispatcher.publish("connection.state", { state: this._publicState(this._state) });
|
|
12386
11428
|
this._resetSeqTrackingState();
|
|
12387
11429
|
this._clientLog.debug(`close exit: elapsed=${Date.now() - tStart}ms`);
|
|
12388
11430
|
}
|
|
@@ -12416,6 +11458,10 @@ var _AUNClient = class _AUNClient {
|
|
|
12416
11458
|
throw new PermissionError(`legacy E2EE method is removed in this SDK: ${method}`);
|
|
12417
11459
|
}
|
|
12418
11460
|
const p = { ...params ?? {} };
|
|
11461
|
+
if (this._instanceProtectedHeaders && PROTECTED_HEADERS_METHODS.has(method)) {
|
|
11462
|
+
const existing = isJsonObject(p.protected_headers) ? p.protected_headers : {};
|
|
11463
|
+
p.protected_headers = { ...this._instanceProtectedHeaders, ...existing };
|
|
11464
|
+
}
|
|
12419
11465
|
if (method === "message.send" || method === "group.send") {
|
|
12420
11466
|
this._normalizeOutboundMessagePayload(p, method);
|
|
12421
11467
|
}
|
|
@@ -12443,7 +11489,7 @@ var _AUNClient = class _AUNClient {
|
|
|
12443
11489
|
throw new StateError("V2 session not initialized; encrypted message.send requires V2 (V1 E2EE removed)");
|
|
12444
11490
|
}
|
|
12445
11491
|
this._clientLog.debug("call route: message.send \u2192 V2 encrypted send");
|
|
12446
|
-
return await this.
|
|
11492
|
+
return await this._sendV2(String(p.to ?? ""), p.payload ?? {}, {
|
|
12447
11493
|
messageId: String(p.message_id ?? "") || void 0,
|
|
12448
11494
|
timestamp: p.timestamp,
|
|
12449
11495
|
protectedHeaders: this._protectedHeadersFromParams(p),
|
|
@@ -12460,7 +11506,7 @@ var _AUNClient = class _AUNClient {
|
|
|
12460
11506
|
throw new StateError("V2 session not initialized; encrypted group.send requires V2 (V1 E2EE removed)");
|
|
12461
11507
|
}
|
|
12462
11508
|
this._clientLog.debug("call route: group.send \u2192 V2 encrypted send");
|
|
12463
|
-
return await this.
|
|
11509
|
+
return await this._sendGroupV2(String(p.group_id ?? ""), p.payload ?? {}, {
|
|
12464
11510
|
messageId: String(p.message_id ?? "") || void 0,
|
|
12465
11511
|
timestamp: p.timestamp,
|
|
12466
11512
|
protectedHeaders: this._protectedHeadersFromParams(p),
|
|
@@ -12506,16 +11552,16 @@ var _AUNClient = class _AUNClient {
|
|
|
12506
11552
|
async _callImplInner(method, p) {
|
|
12507
11553
|
if (method === "message.pull" && this._v2Session) {
|
|
12508
11554
|
this._clientLog.debug("call route: message.pull \u2192 V2 pull");
|
|
12509
|
-
const messages = await this.
|
|
11555
|
+
const messages = await this._pullV2(Number(p.after_seq ?? 0) || 0, Number(p.limit ?? 50) || 50, { force: p.force === true });
|
|
12510
11556
|
return { messages };
|
|
12511
11557
|
}
|
|
12512
11558
|
if (method === "message.ack" && this._v2Session) {
|
|
12513
11559
|
this._clientLog.debug("call route: message.ack \u2192 V2 ack");
|
|
12514
|
-
return await this.
|
|
11560
|
+
return await this._ackV2(Number(p.seq ?? p.up_to_seq ?? 0) || void 0);
|
|
12515
11561
|
}
|
|
12516
11562
|
if (method === "group.pull" && this._v2Session && p.group_id) {
|
|
12517
11563
|
this._clientLog.debug("call route: group.pull \u2192 V2 pull");
|
|
12518
|
-
const messages = await this.
|
|
11564
|
+
const messages = await this._pullGroupV2(
|
|
12519
11565
|
String(p.group_id),
|
|
12520
11566
|
Number(p.after_seq ?? p.after_message_seq ?? 0) || 0,
|
|
12521
11567
|
Number(p.limit ?? 50) || 50
|
|
@@ -12524,7 +11570,7 @@ var _AUNClient = class _AUNClient {
|
|
|
12524
11570
|
}
|
|
12525
11571
|
if (method === "group.ack_messages" && this._v2Session && p.group_id) {
|
|
12526
11572
|
this._clientLog.debug("call route: group.ack_messages \u2192 V2 ack");
|
|
12527
|
-
return await this.
|
|
11573
|
+
return await this._ackGroupV2(
|
|
12528
11574
|
String(p.group_id),
|
|
12529
11575
|
Number(p.seq ?? p.msg_seq ?? p.up_to_seq ?? 0) || void 0
|
|
12530
11576
|
);
|
|
@@ -12619,15 +11665,28 @@ var _AUNClient = class _AUNClient {
|
|
|
12619
11665
|
}
|
|
12620
11666
|
return result;
|
|
12621
11667
|
}
|
|
12622
|
-
|
|
12623
|
-
|
|
12624
|
-
|
|
12625
|
-
|
|
12626
|
-
|
|
12627
|
-
|
|
12628
|
-
|
|
12629
|
-
|
|
12630
|
-
|
|
11668
|
+
async _callRawV2Rpc(method, params) {
|
|
11669
|
+
const p = { ...params ?? {} };
|
|
11670
|
+
delete p._pull_gate_locked;
|
|
11671
|
+
delete p._skip_auto_ack;
|
|
11672
|
+
delete p.skip_auto_ack;
|
|
11673
|
+
if (method.startsWith("group.") && p.group_id !== void 0 && p.group_id !== null) {
|
|
11674
|
+
p.group_id = normalizeGroupId(String(p.group_id)) || String(p.group_id);
|
|
11675
|
+
}
|
|
11676
|
+
if (method.startsWith("group.") && p.device_id === void 0) {
|
|
11677
|
+
p.device_id = this._deviceId;
|
|
11678
|
+
}
|
|
11679
|
+
if (method.startsWith("group.") && p.slot_id === void 0) {
|
|
11680
|
+
p.slot_id = this._slotId;
|
|
11681
|
+
}
|
|
11682
|
+
if (SIGNED_METHODS.has(method)) {
|
|
11683
|
+
if (this._shouldSkipClientSignature(method, p)) {
|
|
11684
|
+
delete p.client_signature;
|
|
11685
|
+
} else {
|
|
11686
|
+
await this._signClientOperation(method, p);
|
|
11687
|
+
}
|
|
11688
|
+
}
|
|
11689
|
+
return await this._transport.call(method, p);
|
|
12631
11690
|
}
|
|
12632
11691
|
// ── 事件 ──────────────────────────────────────────
|
|
12633
11692
|
/**
|
|
@@ -12662,10 +11721,15 @@ var _AUNClient = class _AUNClient {
|
|
|
12662
11721
|
return;
|
|
12663
11722
|
}
|
|
12664
11723
|
const seq = msg.seq;
|
|
11724
|
+
const encryptedPush = isEncryptedPushMessage(msg);
|
|
12665
11725
|
if (seq !== void 0 && seq !== null && this._aid) {
|
|
12666
11726
|
const ns = `p2p:${this._aid}`;
|
|
12667
11727
|
if (seq > 0) this._seqTracker.updateMaxSeen(ns, seq);
|
|
12668
|
-
const
|
|
11728
|
+
const contigBefore = this._seqTracker.getContiguousSeq(ns);
|
|
11729
|
+
const seqNeedsPull = this._seqTracker.onMessageSeq(ns, seq);
|
|
11730
|
+
const published = encryptedPush ? await this._publishEncryptedPushMessage("message.received", "message.undecryptable", ns, seq, msg, false) : await this._publishOrderedMessage("message.received", ns, seq, msg);
|
|
11731
|
+
const contigAfter = this._seqTracker.getContiguousSeq(ns);
|
|
11732
|
+
const needPull = seqNeedsPull && !published;
|
|
12669
11733
|
if (needPull) {
|
|
12670
11734
|
this._safeAsync(this._fillP2pGap());
|
|
12671
11735
|
}
|
|
@@ -12681,12 +11745,13 @@ var _AUNClient = class _AUNClient {
|
|
|
12681
11745
|
this._clientLog.warn(`P2P auto-ack failed:${String(e)}`);
|
|
12682
11746
|
});
|
|
12683
11747
|
}
|
|
12684
|
-
this._saveSeqTrackerState();
|
|
12685
|
-
|
|
12686
|
-
if (seq !== void 0 && seq !== null && this._aid) {
|
|
12687
|
-
const ns = `p2p:${this._aid}`;
|
|
12688
|
-
await this._publishOrderedMessage("message.received", ns, seq, msg);
|
|
11748
|
+
if (contigAfter !== contigBefore) this._saveSeqTrackerState();
|
|
11749
|
+
if (encryptedPush) return;
|
|
12689
11750
|
} else {
|
|
11751
|
+
if (encryptedPush) {
|
|
11752
|
+
await this._publishEncryptedPushMessage("message.received", "message.undecryptable", "", seq ?? 0, msg, false);
|
|
11753
|
+
return;
|
|
11754
|
+
}
|
|
12690
11755
|
await this._publishAppEvent("message.received", msg);
|
|
12691
11756
|
}
|
|
12692
11757
|
} catch (exc) {
|
|
@@ -12754,7 +11819,7 @@ var _AUNClient = class _AUNClient {
|
|
|
12754
11819
|
this._gapFillDone.add(dedupKey);
|
|
12755
11820
|
try {
|
|
12756
11821
|
this._clientLog.debug(`_onRawGroupV2MessageCreated -> group.v2.pull group=${groupId} after_seq=${afterSeq}`);
|
|
12757
|
-
const messages = await this.
|
|
11822
|
+
const messages = await this._pullGroupV2(groupId, afterSeq, 50);
|
|
12758
11823
|
this._clientLog.debug(`_onRawGroupV2MessageCreated pulled ${messages.length} msgs for group=${groupId}`);
|
|
12759
11824
|
} finally {
|
|
12760
11825
|
this._gapFillDone.delete(dedupKey);
|
|
@@ -12788,10 +11853,15 @@ var _AUNClient = class _AUNClient {
|
|
|
12788
11853
|
await this._autoPullGroupMessages(msg);
|
|
12789
11854
|
return;
|
|
12790
11855
|
}
|
|
11856
|
+
const encryptedPush = isEncryptedPushMessage(msg);
|
|
12791
11857
|
if (groupId && seq !== void 0 && seq !== null) {
|
|
12792
11858
|
const ns = `group:${groupId}`;
|
|
12793
11859
|
if (seq > 0) this._seqTracker.updateMaxSeen(ns, seq);
|
|
12794
|
-
const
|
|
11860
|
+
const contigBefore = this._seqTracker.getContiguousSeq(ns);
|
|
11861
|
+
const seqNeedsPull = this._seqTracker.onMessageSeq(ns, seq);
|
|
11862
|
+
const published = encryptedPush ? await this._publishEncryptedPushMessage("group.message_created", "group.message_undecryptable", ns, seq, msg, true) : await this._publishOrderedMessage("group.message_created", ns, seq, msg);
|
|
11863
|
+
const contigAfter = this._seqTracker.getContiguousSeq(ns);
|
|
11864
|
+
const needPull = seqNeedsPull && !published;
|
|
12795
11865
|
if (needPull) {
|
|
12796
11866
|
this._safeAsync(this._fillGroupGap(groupId));
|
|
12797
11867
|
}
|
|
@@ -12808,12 +11878,13 @@ var _AUNClient = class _AUNClient {
|
|
|
12808
11878
|
this._clientLog.warn("group message auto-ack failed: group=" + groupId, e);
|
|
12809
11879
|
});
|
|
12810
11880
|
}
|
|
12811
|
-
this._saveSeqTrackerState();
|
|
12812
|
-
|
|
12813
|
-
if (groupId && seq !== void 0 && seq !== null) {
|
|
12814
|
-
const nsKey = `group:${groupId}`;
|
|
12815
|
-
await this._publishOrderedMessage("group.message_created", nsKey, seq, msg);
|
|
11881
|
+
if (contigAfter !== contigBefore) this._saveSeqTrackerState();
|
|
11882
|
+
if (encryptedPush) return;
|
|
12816
11883
|
} else {
|
|
11884
|
+
if (encryptedPush) {
|
|
11885
|
+
await this._publishEncryptedPushMessage("group.message_created", "group.message_undecryptable", "", seq ?? 0, msg, true);
|
|
11886
|
+
return;
|
|
11887
|
+
}
|
|
12817
11888
|
await this._publishAppEvent("group.message_created", msg);
|
|
12818
11889
|
}
|
|
12819
11890
|
} catch (exc) {
|
|
@@ -12833,6 +11904,52 @@ var _AUNClient = class _AUNClient {
|
|
|
12833
11904
|
}
|
|
12834
11905
|
}
|
|
12835
11906
|
}
|
|
11907
|
+
async _decryptEncryptedPushPayload(msg, group) {
|
|
11908
|
+
const envelope = encryptedPushEnvelope(msg);
|
|
11909
|
+
if (!isV2EncryptedEnvelopePayload(envelope)) return null;
|
|
11910
|
+
const aad = isJsonObject(envelope.aad) ? envelope.aad : {};
|
|
11911
|
+
const fromAid = String(msg.from_aid ?? msg.from ?? msg.sender_aid ?? aad.from ?? "").trim();
|
|
11912
|
+
const plaintext = await this._decryptV2EnvelopeForThought({ envelope, fromAid });
|
|
11913
|
+
if (!plaintext) return null;
|
|
11914
|
+
const e2eeMeta = v2E2eeMeta(envelope);
|
|
11915
|
+
const result = {
|
|
11916
|
+
message_id: String(msg.message_id ?? ""),
|
|
11917
|
+
from: fromAid,
|
|
11918
|
+
seq: msg.seq ?? null,
|
|
11919
|
+
timestamp: msg.t_server ?? msg.timestamp ?? null,
|
|
11920
|
+
payload: plaintext,
|
|
11921
|
+
encrypted: true,
|
|
11922
|
+
e2ee: e2eeMeta
|
|
11923
|
+
};
|
|
11924
|
+
result.direction = fromAid && fromAid === this._aid ? "outbound_sync" : "inbound";
|
|
11925
|
+
if (msg.t_server !== void 0) result.t_server = msg.t_server;
|
|
11926
|
+
if (msg.device_id !== void 0) result.device_id = msg.device_id;
|
|
11927
|
+
if (msg.slot_id !== void 0) result.slot_id = msg.slot_id;
|
|
11928
|
+
if (group) {
|
|
11929
|
+
result.group_id = msg.group_id ?? aad.group_id ?? envelope.group_id ?? null;
|
|
11930
|
+
} else {
|
|
11931
|
+
result.to = msg.to ?? this._aid ?? "";
|
|
11932
|
+
}
|
|
11933
|
+
attachV2EnvelopeMetadata(result, e2eeMeta);
|
|
11934
|
+
return result;
|
|
11935
|
+
}
|
|
11936
|
+
async _publishEncryptedPushAsUndecryptable(event, ns, seq, msg, group) {
|
|
11937
|
+
const safeEvent = safeUndecryptablePushEvent(msg, group);
|
|
11938
|
+
if (ns) {
|
|
11939
|
+
return this._publishOrderedMessage(event, ns, seq, safeEvent);
|
|
11940
|
+
}
|
|
11941
|
+
await this._publishAppEvent(event, safeEvent);
|
|
11942
|
+
return true;
|
|
11943
|
+
}
|
|
11944
|
+
async _publishEncryptedPushMessage(normalEvent, undecryptableEvent, ns, seq, msg, group) {
|
|
11945
|
+
const decrypted = await this._decryptEncryptedPushPayload(msg, group);
|
|
11946
|
+
if (decrypted) {
|
|
11947
|
+
if (ns) return this._publishOrderedMessage(normalEvent, ns, seq, decrypted);
|
|
11948
|
+
await this._publishAppEvent(normalEvent, decrypted);
|
|
11949
|
+
return true;
|
|
11950
|
+
}
|
|
11951
|
+
return this._publishEncryptedPushAsUndecryptable(undecryptableEvent, ns, seq, msg, group);
|
|
11952
|
+
}
|
|
12836
11953
|
/** 收到不带 payload 的 group.message_created 通知后,自动 pull 最新消息 */
|
|
12837
11954
|
async _autoPullGroupMessages(notification) {
|
|
12838
11955
|
const groupId = notification.group_id ?? "";
|
|
@@ -14032,7 +13149,7 @@ var _AUNClient = class _AUNClient {
|
|
|
14032
13149
|
this._state = "connected";
|
|
14033
13150
|
this._connectedAt = Date.now();
|
|
14034
13151
|
await this._dispatcher.publish("connection.state", {
|
|
14035
|
-
state: this._state,
|
|
13152
|
+
state: this._publicState(this._state),
|
|
14036
13153
|
gateway: gatewayUrl
|
|
14037
13154
|
});
|
|
14038
13155
|
if (this._seqTrackerContext !== this._currentSeqTrackerContext()) {
|
|
@@ -14041,7 +13158,7 @@ var _AUNClient = class _AUNClient {
|
|
|
14041
13158
|
}
|
|
14042
13159
|
this._startBackgroundTasks();
|
|
14043
13160
|
try {
|
|
14044
|
-
await this.
|
|
13161
|
+
await this._initV2Session();
|
|
14045
13162
|
} catch (exc) {
|
|
14046
13163
|
this._clientLog.warn(`V2 session init failed (non-fatal): ${String(exc)}`);
|
|
14047
13164
|
}
|
|
@@ -14051,10 +13168,52 @@ var _AUNClient = class _AUNClient {
|
|
|
14051
13168
|
this._clientLog.debug(`_connectOnce exit (error): elapsed=${Date.now() - tStart}ms err=${err instanceof Error ? err.message : String(err)}`);
|
|
14052
13169
|
throw err;
|
|
14053
13170
|
}
|
|
14054
|
-
}
|
|
14055
|
-
_resolveGateway(params) {
|
|
14056
|
-
const gateways = this._resolveGateways(params);
|
|
14057
|
-
return gateways[0];
|
|
13171
|
+
}
|
|
13172
|
+
_resolveGateway(params) {
|
|
13173
|
+
const gateways = this._resolveGateways(params);
|
|
13174
|
+
return gateways[0];
|
|
13175
|
+
}
|
|
13176
|
+
async _resolveGatewayForAid(aid) {
|
|
13177
|
+
const target = String(aid ?? this._aid ?? "").trim();
|
|
13178
|
+
if (!target) throw new StateError("gateway discovery requires a loaded AID");
|
|
13179
|
+
if (this._gatewayUrl) return this._gatewayUrl;
|
|
13180
|
+
try {
|
|
13181
|
+
const getMetadata = this._keystore.getMetadata;
|
|
13182
|
+
const raw = typeof getMetadata === "function" ? String(await getMetadata.call(this._keystore, target, "gateway_url") ?? "").trim() : "";
|
|
13183
|
+
if (raw) {
|
|
13184
|
+
const gateway = raw.startsWith('"') && raw.endsWith('"') ? String(JSON.parse(raw)).trim() : raw;
|
|
13185
|
+
if (gateway) {
|
|
13186
|
+
this._gatewayUrl = gateway;
|
|
13187
|
+
return gateway;
|
|
13188
|
+
}
|
|
13189
|
+
}
|
|
13190
|
+
} catch {
|
|
13191
|
+
}
|
|
13192
|
+
const dotIdx = target.indexOf(".");
|
|
13193
|
+
const issuerDomain = dotIdx >= 0 ? target.slice(dotIdx + 1) : target;
|
|
13194
|
+
const portSuffix = this.configModel.discoveryPort ? `:${this.configModel.discoveryPort}` : "";
|
|
13195
|
+
const candidates = [
|
|
13196
|
+
`https://${target}${portSuffix}/.well-known/aun-gateway`,
|
|
13197
|
+
`https://gateway.${issuerDomain}${portSuffix}/.well-known/aun-gateway`
|
|
13198
|
+
];
|
|
13199
|
+
let lastError = null;
|
|
13200
|
+
for (const url of candidates) {
|
|
13201
|
+
try {
|
|
13202
|
+
const gateway = await this._discovery.discover(url);
|
|
13203
|
+
this._gatewayUrl = gateway;
|
|
13204
|
+
try {
|
|
13205
|
+
const setMetadata = this._keystore.setMetadata;
|
|
13206
|
+
if (typeof setMetadata === "function") {
|
|
13207
|
+
await setMetadata.call(this._keystore, target, "gateway_url", gateway);
|
|
13208
|
+
}
|
|
13209
|
+
} catch {
|
|
13210
|
+
}
|
|
13211
|
+
return gateway;
|
|
13212
|
+
} catch (err) {
|
|
13213
|
+
lastError = err;
|
|
13214
|
+
}
|
|
13215
|
+
}
|
|
13216
|
+
throw lastError instanceof Error ? lastError : new ConnectionError(`gateway discovery failed for ${target}`);
|
|
14058
13217
|
}
|
|
14059
13218
|
_resolveGateways(params) {
|
|
14060
13219
|
const topology = isJsonObject(params.topology) ? params.topology : null;
|
|
@@ -14102,10 +13261,10 @@ var _AUNClient = class _AUNClient {
|
|
|
14102
13261
|
_normalizeConnectParams(params) {
|
|
14103
13262
|
const request = { ...params };
|
|
14104
13263
|
const accessToken = String(request.access_token ?? "");
|
|
14105
|
-
if (!accessToken) throw new StateError("connect requires non-empty access_token");
|
|
14106
13264
|
const gateway = String(request.gateway ?? this._gatewayUrl ?? "");
|
|
14107
13265
|
if (!gateway) throw new StateError("connect requires non-empty gateway");
|
|
14108
|
-
request.access_token = accessToken;
|
|
13266
|
+
if (accessToken) request.access_token = accessToken;
|
|
13267
|
+
else delete request.access_token;
|
|
14109
13268
|
request.gateway = gateway;
|
|
14110
13269
|
request.device_id = this._deviceId;
|
|
14111
13270
|
request.slot_id = normalizeInstanceId(request.slot_id ?? this._slotId, "slot_id", { allowEmpty: true });
|
|
@@ -14301,6 +13460,10 @@ var _AUNClient = class _AUNClient {
|
|
|
14301
13460
|
}
|
|
14302
13461
|
try {
|
|
14303
13462
|
identity = await this._auth.refreshCachedTokens(this._gatewayUrl, identity);
|
|
13463
|
+
if (this._state !== "connected") {
|
|
13464
|
+
scheduleRefresh();
|
|
13465
|
+
return;
|
|
13466
|
+
}
|
|
14304
13467
|
this._identity = identity;
|
|
14305
13468
|
if (this._sessionParams && identity.access_token) {
|
|
14306
13469
|
this._sessionParams.access_token = identity.access_token;
|
|
@@ -14432,7 +13595,7 @@ var _AUNClient = class _AUNClient {
|
|
|
14432
13595
|
this._state = "disconnected";
|
|
14433
13596
|
this._stopBackgroundTasks();
|
|
14434
13597
|
await this._dispatcher.publish("connection.state", {
|
|
14435
|
-
state: this._state,
|
|
13598
|
+
state: this._publicState(this._state),
|
|
14436
13599
|
error
|
|
14437
13600
|
});
|
|
14438
13601
|
if (!this._sessionOptions.auto_reconnect) return;
|
|
@@ -14443,7 +13606,7 @@ var _AUNClient = class _AUNClient {
|
|
|
14443
13606
|
this._clientLog.warn(`suppress auto-reconnect: ${reason}`);
|
|
14444
13607
|
const disconnectInfo = this._lastDisconnectInfo ?? {};
|
|
14445
13608
|
const eventPayload = {
|
|
14446
|
-
state: this._state,
|
|
13609
|
+
state: this._publicState(this._state),
|
|
14447
13610
|
error,
|
|
14448
13611
|
reason
|
|
14449
13612
|
};
|
|
@@ -14476,32 +13639,47 @@ var _AUNClient = class _AUNClient {
|
|
|
14476
13639
|
serverInitiated ? 16 : 1,
|
|
14477
13640
|
maxBaseDelay
|
|
14478
13641
|
);
|
|
13642
|
+
this._retryAttempt = 0;
|
|
13643
|
+
this._retryMaxAttempts = maxAttempts;
|
|
14479
13644
|
for (let attempt = 1; !this._reconnectAbort?.signal.aborted; attempt++) {
|
|
14480
13645
|
if (maxAttempts > 0 && attempt > maxAttempts) {
|
|
14481
13646
|
this._state = "terminal_failed";
|
|
13647
|
+
this._nextRetryAt = null;
|
|
14482
13648
|
this._reconnectActive = false;
|
|
14483
13649
|
this._reconnectAbort = null;
|
|
14484
13650
|
await this._dispatcher.publish("connection.state", {
|
|
14485
|
-
state: this._state,
|
|
13651
|
+
state: this._publicState(this._state),
|
|
14486
13652
|
attempt: attempt - 1,
|
|
14487
13653
|
reason: "max_attempts_exhausted"
|
|
14488
13654
|
});
|
|
14489
13655
|
return;
|
|
14490
13656
|
}
|
|
14491
|
-
this.
|
|
13657
|
+
this._retryAttempt = attempt;
|
|
13658
|
+
const sleepMs = reconnectSleepDelaySeconds(delay, maxBaseDelay) * 1e3;
|
|
13659
|
+
this._nextRetryAt = new Date(Date.now() + sleepMs);
|
|
13660
|
+
this._state = "retry_backoff";
|
|
14492
13661
|
await this._dispatcher.publish("connection.state", {
|
|
14493
|
-
state: this._state,
|
|
14494
|
-
attempt
|
|
13662
|
+
state: this._publicState(this._state),
|
|
13663
|
+
attempt,
|
|
13664
|
+
next_retry_at: this._nextRetryAt.getTime() / 1e3
|
|
14495
13665
|
});
|
|
14496
13666
|
try {
|
|
14497
|
-
await this._sleep(
|
|
13667
|
+
await this._sleep(sleepMs);
|
|
13668
|
+
this._nextRetryAt = null;
|
|
14498
13669
|
if (this._reconnectAbort?.signal.aborted) {
|
|
14499
13670
|
this._reconnectActive = false;
|
|
14500
13671
|
return;
|
|
14501
13672
|
}
|
|
13673
|
+
this._state = "reconnecting";
|
|
13674
|
+
await this._dispatcher.publish("connection.state", {
|
|
13675
|
+
state: this._publicState(this._state),
|
|
13676
|
+
attempt
|
|
13677
|
+
});
|
|
14502
13678
|
if (this._gatewayUrl) {
|
|
14503
13679
|
const healthy = await this._discovery.checkHealth(this._gatewayUrl, 5e3);
|
|
14504
13680
|
if (!healthy) {
|
|
13681
|
+
this._lastError = new Error("gateway health check failed");
|
|
13682
|
+
this._lastErrorCode = "gateway_unhealthy";
|
|
14505
13683
|
delay = Math.min(delay * 2, maxBaseDelay);
|
|
14506
13684
|
continue;
|
|
14507
13685
|
}
|
|
@@ -14511,20 +13689,26 @@ var _AUNClient = class _AUNClient {
|
|
|
14511
13689
|
throw new StateError("missing connect params for reconnect");
|
|
14512
13690
|
}
|
|
14513
13691
|
await this._connectOnce(this._sessionParams, true);
|
|
13692
|
+
this._lastError = null;
|
|
13693
|
+
this._lastErrorCode = null;
|
|
13694
|
+
this._nextRetryAt = null;
|
|
14514
13695
|
this._reconnectActive = false;
|
|
14515
13696
|
this._reconnectAbort = null;
|
|
14516
13697
|
return;
|
|
14517
13698
|
} catch (exc) {
|
|
13699
|
+
this._lastError = exc instanceof Error ? exc : new Error(String(exc));
|
|
13700
|
+
this._lastErrorCode = "reconnect_failed";
|
|
14518
13701
|
await this._dispatcher.publish("connection.error", {
|
|
14519
13702
|
error: formatCaughtError(exc),
|
|
14520
13703
|
attempt
|
|
14521
13704
|
});
|
|
14522
13705
|
if (!this._shouldRetryReconnect(exc)) {
|
|
14523
13706
|
this._state = "terminal_failed";
|
|
13707
|
+
this._nextRetryAt = null;
|
|
14524
13708
|
this._reconnectActive = false;
|
|
14525
13709
|
this._reconnectAbort = null;
|
|
14526
13710
|
await this._dispatcher.publish("connection.state", {
|
|
14527
|
-
state: this._state,
|
|
13711
|
+
state: this._publicState(this._state),
|
|
14528
13712
|
error: formatCaughtError(exc),
|
|
14529
13713
|
attempt
|
|
14530
13714
|
});
|
|
@@ -14861,9 +14045,27 @@ var _AUNClient = class _AUNClient {
|
|
|
14861
14045
|
*
|
|
14862
14046
|
* connect 成功后自动调用,可幂等手动调用。
|
|
14863
14047
|
*/
|
|
14864
|
-
async
|
|
14048
|
+
async _initV2Session() {
|
|
14865
14049
|
if (!this._aid) return;
|
|
14866
|
-
|
|
14050
|
+
let identity = this._identity;
|
|
14051
|
+
if (!identity?.private_key_pem) {
|
|
14052
|
+
try {
|
|
14053
|
+
const reloaded = await this._keystore.loadIdentity(this._aid);
|
|
14054
|
+
if (reloaded?.private_key_pem) {
|
|
14055
|
+
this._identity = reloaded;
|
|
14056
|
+
identity = reloaded;
|
|
14057
|
+
this._clientLog.warn("V2 session init: identity cache was stale, reloaded from keystore");
|
|
14058
|
+
try {
|
|
14059
|
+
const persistIdentity = this._auth._persistIdentity;
|
|
14060
|
+
if (typeof persistIdentity === "function") {
|
|
14061
|
+
await persistIdentity.call(this._auth, reloaded);
|
|
14062
|
+
}
|
|
14063
|
+
} catch {
|
|
14064
|
+
}
|
|
14065
|
+
}
|
|
14066
|
+
} catch {
|
|
14067
|
+
}
|
|
14068
|
+
}
|
|
14867
14069
|
if (!identity?.private_key_pem) {
|
|
14868
14070
|
this._clientLog.warn("V2 session init skipped: no AID private key");
|
|
14869
14071
|
return;
|
|
@@ -15132,7 +14334,7 @@ var _AUNClient = class _AUNClient {
|
|
|
15132
14334
|
* @param opts 可选 messageId / timestamp(与 Python 行为一致)
|
|
15133
14335
|
* @returns 服务端响应
|
|
15134
14336
|
*/
|
|
15135
|
-
async
|
|
14337
|
+
async _sendV2(to, payload, opts) {
|
|
15136
14338
|
if (!this._v2Session) {
|
|
15137
14339
|
throw new StateError("V2 session not initialized (not connected?)");
|
|
15138
14340
|
}
|
|
@@ -15173,20 +14375,21 @@ var _AUNClient = class _AUNClient {
|
|
|
15173
14375
|
* @param afterSeq 从此 seq 之后开始拉取(0/省略 = 从当前 contiguous 开始)
|
|
15174
14376
|
* @param limit 最多拉取条数
|
|
15175
14377
|
*/
|
|
15176
|
-
async
|
|
14378
|
+
async _pullV2(afterSeq = 0, limit = 50, opts) {
|
|
15177
14379
|
if (!this._v2Session) {
|
|
15178
14380
|
throw new StateError("V2 session not initialized (not connected?)");
|
|
15179
14381
|
}
|
|
15180
14382
|
const ns = this._aid ? `p2p:${this._aid}` : "";
|
|
15181
14383
|
const decrypted = [];
|
|
15182
|
-
let nextAfterSeq = afterSeq || (ns ? this._seqTracker.getContiguousSeq(ns) : 0);
|
|
14384
|
+
let nextAfterSeq = opts?.force ? afterSeq : afterSeq || (ns ? this._seqTracker.getContiguousSeq(ns) : 0);
|
|
15183
14385
|
let pageCount = 0;
|
|
15184
14386
|
const maxPages = 100;
|
|
15185
14387
|
while (pageCount < maxPages) {
|
|
15186
14388
|
pageCount += 1;
|
|
15187
|
-
const result = await this.
|
|
14389
|
+
const result = await this._callRawV2Rpc("message.v2.pull", {
|
|
15188
14390
|
after_seq: nextAfterSeq,
|
|
15189
|
-
limit
|
|
14391
|
+
limit,
|
|
14392
|
+
...opts?.force ? { force: true } : {}
|
|
15190
14393
|
});
|
|
15191
14394
|
const messages = Array.isArray(result?.messages) ? result.messages : [];
|
|
15192
14395
|
const seqs = messages.map((msg) => Number(msg.seq ?? 0)).filter((seq) => Number.isFinite(seq) && seq > 0);
|
|
@@ -15256,7 +14459,7 @@ var _AUNClient = class _AUNClient {
|
|
|
15256
14459
|
this._saveSeqTrackerState();
|
|
15257
14460
|
}
|
|
15258
14461
|
if (messages.length > 0 && contigAdvanced && ackSeq > 0) {
|
|
15259
|
-
this._safeAsync(this.
|
|
14462
|
+
this._safeAsync(this._ackV2(ackSeq).then(() => void 0));
|
|
15260
14463
|
}
|
|
15261
14464
|
}
|
|
15262
14465
|
const nextAfter = Math.max(pageMaxSeq, nextAfterSeq);
|
|
@@ -15273,7 +14476,7 @@ var _AUNClient = class _AUNClient {
|
|
|
15273
14476
|
*
|
|
15274
14477
|
* @param upToSeq 确认到此 seq;省略则用当前 contiguous
|
|
15275
14478
|
*/
|
|
15276
|
-
async
|
|
14479
|
+
async _ackV2(upToSeq) {
|
|
15277
14480
|
const ns = this._aid ? `p2p:${this._aid}` : "";
|
|
15278
14481
|
let seq = upToSeq ?? (ns ? this._seqTracker.getContiguousSeq(ns) : 0);
|
|
15279
14482
|
if (seq <= 0) return { acked: 0 };
|
|
@@ -15475,6 +14678,10 @@ var _AUNClient = class _AUNClient {
|
|
|
15475
14678
|
encrypted: true,
|
|
15476
14679
|
e2ee
|
|
15477
14680
|
};
|
|
14681
|
+
const explicitDirection = String(msg.direction ?? "").trim();
|
|
14682
|
+
result.direction = explicitDirection || (fromAid && fromAid === this._aid ? "outbound_sync" : "inbound");
|
|
14683
|
+
if (msg.device_id !== void 0) result.device_id = msg.device_id;
|
|
14684
|
+
if (msg.slot_id !== void 0) result.slot_id = msg.slot_id;
|
|
15478
14685
|
attachV2EnvelopeMetadata(result, e2ee);
|
|
15479
14686
|
return result;
|
|
15480
14687
|
}
|
|
@@ -15486,7 +14693,7 @@ var _AUNClient = class _AUNClient {
|
|
|
15486
14693
|
* @param opts 可选 messageId / timestamp
|
|
15487
14694
|
* @returns 服务端响应
|
|
15488
14695
|
*/
|
|
15489
|
-
async
|
|
14696
|
+
async _sendGroupV2(groupId, payload, opts) {
|
|
15490
14697
|
if (!this._v2Session) {
|
|
15491
14698
|
throw new StateError("V2 session not initialized (not connected?)");
|
|
15492
14699
|
}
|
|
@@ -15541,7 +14748,7 @@ var _AUNClient = class _AUNClient {
|
|
|
15541
14748
|
}
|
|
15542
14749
|
}
|
|
15543
14750
|
async _pullGroupV2Internal(params) {
|
|
15544
|
-
await this.
|
|
14751
|
+
await this._pullGroupV2(params.group_id, params.after_seq, params.limit);
|
|
15545
14752
|
}
|
|
15546
14753
|
/**
|
|
15547
14754
|
* 拉取并解密 V2 Group 消息。
|
|
@@ -15550,7 +14757,7 @@ var _AUNClient = class _AUNClient {
|
|
|
15550
14757
|
* @param afterSeq 从此 seq 之后开始拉取(0/省略 = 从当前 contiguous 开始)
|
|
15551
14758
|
* @param limit 最多拉取条数
|
|
15552
14759
|
*/
|
|
15553
|
-
async
|
|
14760
|
+
async _pullGroupV2(groupId, afterSeq = 0, limit = 50) {
|
|
15554
14761
|
if (!this._v2Session) {
|
|
15555
14762
|
throw new StateError("V2 session not initialized (not connected?)");
|
|
15556
14763
|
}
|
|
@@ -15643,7 +14850,7 @@ var _AUNClient = class _AUNClient {
|
|
|
15643
14850
|
this._saveSeqTrackerState();
|
|
15644
14851
|
}
|
|
15645
14852
|
if (messages.length > 0 && contigAdvanced && ackSeq > 0) {
|
|
15646
|
-
this._safeAsync(this.
|
|
14853
|
+
this._safeAsync(this._ackGroupV2(gid, ackSeq).then(() => void 0));
|
|
15647
14854
|
}
|
|
15648
14855
|
const nextAfter = Math.max(pageMaxSeq, nextAfterSeq);
|
|
15649
14856
|
if (messages.length === 0 || nextAfter <= nextAfterSeq || result.has_more === false) break;
|
|
@@ -15660,7 +14867,7 @@ var _AUNClient = class _AUNClient {
|
|
|
15660
14867
|
* @param groupId 群 ID
|
|
15661
14868
|
* @param upToSeq 确认到此 seq;省略则用当前 contiguous
|
|
15662
14869
|
*/
|
|
15663
|
-
async
|
|
14870
|
+
async _ackGroupV2(groupId, upToSeq) {
|
|
15664
14871
|
const gid = normalizeGroupId(groupId) || String(groupId ?? "").trim();
|
|
15665
14872
|
if (!gid) throw new ValidationError("group.ack_messages requires group_id");
|
|
15666
14873
|
const ns = `group:${gid}`;
|
|
@@ -16646,7 +15853,7 @@ var _AUNClient = class _AUNClient {
|
|
|
16646
15853
|
try {
|
|
16647
15854
|
do {
|
|
16648
15855
|
this._v2PullPending = false;
|
|
16649
|
-
await this.
|
|
15856
|
+
await this._pullV2();
|
|
16650
15857
|
const newContig = ns ? this._seqTracker.getContiguousSeq(ns) : -1;
|
|
16651
15858
|
this._clientLog.debug(
|
|
16652
15859
|
`_onV2PushNotification pull done: contiguous_seq=${contigBefore}->${newContig} (push_seq=${pushSeq || "null"})`
|
|
@@ -16771,6 +15978,733 @@ function _uuidV4() {
|
|
|
16771
15978
|
return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20)}`;
|
|
16772
15979
|
}
|
|
16773
15980
|
|
|
15981
|
+
// src/aid-store.ts
|
|
15982
|
+
init_crypto();
|
|
15983
|
+
function _derReadLength(data, offset) {
|
|
15984
|
+
if (offset >= data.length) return null;
|
|
15985
|
+
const first = data[offset];
|
|
15986
|
+
if (first < 128) return { value: first, lenBytes: 1 };
|
|
15987
|
+
const n = first & 127;
|
|
15988
|
+
if (n === 0 || n > 4) return null;
|
|
15989
|
+
let v = 0;
|
|
15990
|
+
for (let i = 0; i < n; i++) v = v << 8 | data[offset + 1 + i];
|
|
15991
|
+
return { value: v, lenBytes: 1 + n };
|
|
15992
|
+
}
|
|
15993
|
+
function _derSkipTlv(data, offset) {
|
|
15994
|
+
const len = _derReadLength(data, offset + 1);
|
|
15995
|
+
if (!len) return null;
|
|
15996
|
+
return offset + 1 + len.lenBytes + len.value;
|
|
15997
|
+
}
|
|
15998
|
+
function parseCertValidity(certPem) {
|
|
15999
|
+
try {
|
|
16000
|
+
let parseTime2 = function(offset) {
|
|
16001
|
+
const tag = der[offset];
|
|
16002
|
+
const len = _derReadLength(der, offset + 1);
|
|
16003
|
+
if (!len) return null;
|
|
16004
|
+
const raw = new TextDecoder().decode(der.slice(offset + 1 + len.lenBytes, offset + 1 + len.lenBytes + len.value));
|
|
16005
|
+
if (tag === 23) {
|
|
16006
|
+
const y = parseInt(raw.slice(0, 2), 10);
|
|
16007
|
+
const year = y >= 50 ? 1900 + y : 2e3 + y;
|
|
16008
|
+
return Date.UTC(
|
|
16009
|
+
year,
|
|
16010
|
+
parseInt(raw.slice(2, 4), 10) - 1,
|
|
16011
|
+
parseInt(raw.slice(4, 6), 10),
|
|
16012
|
+
parseInt(raw.slice(6, 8), 10),
|
|
16013
|
+
parseInt(raw.slice(8, 10), 10),
|
|
16014
|
+
parseInt(raw.slice(10, 12), 10)
|
|
16015
|
+
);
|
|
16016
|
+
} else if (tag === 24) {
|
|
16017
|
+
return Date.UTC(
|
|
16018
|
+
parseInt(raw.slice(0, 4), 10),
|
|
16019
|
+
parseInt(raw.slice(4, 6), 10) - 1,
|
|
16020
|
+
parseInt(raw.slice(6, 8), 10),
|
|
16021
|
+
parseInt(raw.slice(8, 10), 10),
|
|
16022
|
+
parseInt(raw.slice(10, 12), 10),
|
|
16023
|
+
parseInt(raw.slice(12, 14), 10)
|
|
16024
|
+
);
|
|
16025
|
+
}
|
|
16026
|
+
return null;
|
|
16027
|
+
};
|
|
16028
|
+
var parseTime = parseTime2;
|
|
16029
|
+
const der = new Uint8Array(pemToArrayBuffer(certPem));
|
|
16030
|
+
const cert = _derSkipTlv(der, -1);
|
|
16031
|
+
const certLen = _derReadLength(der, 1);
|
|
16032
|
+
if (!certLen || der[0] !== 48) return null;
|
|
16033
|
+
const tbsStart = 1 + certLen.lenBytes;
|
|
16034
|
+
if (der[tbsStart] !== 48) return null;
|
|
16035
|
+
const tbsLen = _derReadLength(der, tbsStart + 1);
|
|
16036
|
+
if (!tbsLen) return null;
|
|
16037
|
+
let pos = tbsStart + 1 + tbsLen.lenBytes;
|
|
16038
|
+
if (der[pos] === 160) {
|
|
16039
|
+
const e = _derSkipTlv(der, pos);
|
|
16040
|
+
if (e === null) return null;
|
|
16041
|
+
pos = e;
|
|
16042
|
+
}
|
|
16043
|
+
{
|
|
16044
|
+
const e = _derSkipTlv(der, pos);
|
|
16045
|
+
if (e === null) return null;
|
|
16046
|
+
pos = e;
|
|
16047
|
+
}
|
|
16048
|
+
{
|
|
16049
|
+
const e = _derSkipTlv(der, pos);
|
|
16050
|
+
if (e === null) return null;
|
|
16051
|
+
pos = e;
|
|
16052
|
+
}
|
|
16053
|
+
{
|
|
16054
|
+
const e = _derSkipTlv(der, pos);
|
|
16055
|
+
if (e === null) return null;
|
|
16056
|
+
pos = e;
|
|
16057
|
+
}
|
|
16058
|
+
if (der[pos] !== 48) return null;
|
|
16059
|
+
const valLen = _derReadLength(der, pos + 1);
|
|
16060
|
+
if (!valLen) return null;
|
|
16061
|
+
pos = pos + 1 + valLen.lenBytes;
|
|
16062
|
+
const notBefore = parseTime2(pos);
|
|
16063
|
+
if (notBefore === null) return null;
|
|
16064
|
+
const nbLen = _derReadLength(der, pos + 1);
|
|
16065
|
+
if (!nbLen) return null;
|
|
16066
|
+
const notAfterOffset = pos + 1 + nbLen.lenBytes + nbLen.value;
|
|
16067
|
+
const notAfter = parseTime2(notAfterOffset);
|
|
16068
|
+
if (notAfter === null) return null;
|
|
16069
|
+
return { notBefore, notAfter };
|
|
16070
|
+
} catch {
|
|
16071
|
+
return null;
|
|
16072
|
+
}
|
|
16073
|
+
}
|
|
16074
|
+
function parseCertCN(certPem) {
|
|
16075
|
+
try {
|
|
16076
|
+
const der = new Uint8Array(pemToArrayBuffer(certPem));
|
|
16077
|
+
if (der[0] !== 48) return null;
|
|
16078
|
+
const certLen = _derReadLength(der, 1);
|
|
16079
|
+
if (!certLen) return null;
|
|
16080
|
+
const tbsStart = 1 + certLen.lenBytes;
|
|
16081
|
+
if (der[tbsStart] !== 48) return null;
|
|
16082
|
+
const tbsLen = _derReadLength(der, tbsStart + 1);
|
|
16083
|
+
if (!tbsLen) return null;
|
|
16084
|
+
let pos = tbsStart + 1 + tbsLen.lenBytes;
|
|
16085
|
+
if (der[pos] === 160) {
|
|
16086
|
+
const e = _derSkipTlv(der, pos);
|
|
16087
|
+
if (e === null) return null;
|
|
16088
|
+
pos = e;
|
|
16089
|
+
}
|
|
16090
|
+
{
|
|
16091
|
+
const e = _derSkipTlv(der, pos);
|
|
16092
|
+
if (e === null) return null;
|
|
16093
|
+
pos = e;
|
|
16094
|
+
}
|
|
16095
|
+
{
|
|
16096
|
+
const e = _derSkipTlv(der, pos);
|
|
16097
|
+
if (e === null) return null;
|
|
16098
|
+
pos = e;
|
|
16099
|
+
}
|
|
16100
|
+
if (der[pos] !== 48) return null;
|
|
16101
|
+
const issuerLen = _derReadLength(der, pos + 1);
|
|
16102
|
+
if (!issuerLen) return null;
|
|
16103
|
+
const issuerEnd = pos + 1 + issuerLen.lenBytes + issuerLen.value;
|
|
16104
|
+
pos = pos + 1 + issuerLen.lenBytes;
|
|
16105
|
+
pos = issuerEnd;
|
|
16106
|
+
{
|
|
16107
|
+
const e = _derSkipTlv(der, pos);
|
|
16108
|
+
if (e === null) return null;
|
|
16109
|
+
pos = e;
|
|
16110
|
+
}
|
|
16111
|
+
if (der[pos] !== 48) return null;
|
|
16112
|
+
const subjectLen = _derReadLength(der, pos + 1);
|
|
16113
|
+
if (!subjectLen) return null;
|
|
16114
|
+
const subjectEnd = pos + 1 + subjectLen.lenBytes + subjectLen.value;
|
|
16115
|
+
pos = pos + 1 + subjectLen.lenBytes;
|
|
16116
|
+
while (pos < subjectEnd) {
|
|
16117
|
+
if (der[pos] !== 49) break;
|
|
16118
|
+
const setLen = _derReadLength(der, pos + 1);
|
|
16119
|
+
if (!setLen) break;
|
|
16120
|
+
const setEnd = pos + 1 + setLen.lenBytes + setLen.value;
|
|
16121
|
+
let rpos = pos + 1 + setLen.lenBytes;
|
|
16122
|
+
while (rpos < setEnd) {
|
|
16123
|
+
if (der[rpos] !== 48) break;
|
|
16124
|
+
const seqLen = _derReadLength(der, rpos + 1);
|
|
16125
|
+
if (!seqLen) break;
|
|
16126
|
+
const seqEnd = rpos + 1 + seqLen.lenBytes + seqLen.value;
|
|
16127
|
+
let apos = rpos + 1 + seqLen.lenBytes;
|
|
16128
|
+
if (der[apos] !== 6) {
|
|
16129
|
+
rpos = seqEnd;
|
|
16130
|
+
continue;
|
|
16131
|
+
}
|
|
16132
|
+
const oidLen = _derReadLength(der, apos + 1);
|
|
16133
|
+
if (!oidLen) {
|
|
16134
|
+
rpos = seqEnd;
|
|
16135
|
+
continue;
|
|
16136
|
+
}
|
|
16137
|
+
const oidBytes = der.slice(apos + 1 + oidLen.lenBytes, apos + 1 + oidLen.lenBytes + oidLen.value);
|
|
16138
|
+
const isCN = oidBytes.length === 3 && oidBytes[0] === 85 && oidBytes[1] === 4 && oidBytes[2] === 3;
|
|
16139
|
+
apos = apos + 1 + oidLen.lenBytes + oidLen.value;
|
|
16140
|
+
if (isCN && apos < seqEnd) {
|
|
16141
|
+
const valLen = _derReadLength(der, apos + 1);
|
|
16142
|
+
if (valLen) {
|
|
16143
|
+
return new TextDecoder().decode(der.slice(apos + 1 + valLen.lenBytes, apos + 1 + valLen.lenBytes + valLen.value));
|
|
16144
|
+
}
|
|
16145
|
+
}
|
|
16146
|
+
rpos = seqEnd;
|
|
16147
|
+
}
|
|
16148
|
+
pos = setEnd;
|
|
16149
|
+
}
|
|
16150
|
+
return null;
|
|
16151
|
+
} catch {
|
|
16152
|
+
return null;
|
|
16153
|
+
}
|
|
16154
|
+
}
|
|
16155
|
+
function normalizeSlotId(slotId) {
|
|
16156
|
+
const value = String(slotId ?? "default").trim();
|
|
16157
|
+
return value || "default";
|
|
16158
|
+
}
|
|
16159
|
+
function issuerFromAid(aid) {
|
|
16160
|
+
const target = String(aid ?? "").trim();
|
|
16161
|
+
const dotIdx = target.indexOf(".");
|
|
16162
|
+
return dotIdx >= 0 ? target.slice(dotIdx + 1) : target;
|
|
16163
|
+
}
|
|
16164
|
+
function validateRegisterAidName(aid) {
|
|
16165
|
+
if (!aid) throw new ValidationError("AIDStore.register requires 'aid'");
|
|
16166
|
+
const name = aid.includes(".") ? aid.split(".")[0] : aid;
|
|
16167
|
+
if (!/^[a-z0-9_][a-z0-9_-]{3,63}$/.test(name)) {
|
|
16168
|
+
throw new ValidationError(
|
|
16169
|
+
`Invalid AID name '${name}': must be 4-64 characters, only [a-z0-9_-], cannot start with '-'`
|
|
16170
|
+
);
|
|
16171
|
+
}
|
|
16172
|
+
if (name.startsWith("guest")) {
|
|
16173
|
+
throw new ValidationError("AID name must not start with 'guest'");
|
|
16174
|
+
}
|
|
16175
|
+
}
|
|
16176
|
+
function gatewayHttpUrl3(gatewayUrl, path) {
|
|
16177
|
+
const parsed = new URL(gatewayUrl);
|
|
16178
|
+
parsed.protocol = parsed.protocol === "wss:" ? "https:" : "http:";
|
|
16179
|
+
parsed.pathname = path;
|
|
16180
|
+
parsed.search = "";
|
|
16181
|
+
parsed.hash = "";
|
|
16182
|
+
return parsed.toString();
|
|
16183
|
+
}
|
|
16184
|
+
function pkiCertUrl(gatewayUrl, aid) {
|
|
16185
|
+
return gatewayHttpUrl3(gatewayUrl, `/pki/cert/${encodeURIComponent(aid)}`);
|
|
16186
|
+
}
|
|
16187
|
+
function agentMdUrl(aid, gatewayUrl, discoveryPort) {
|
|
16188
|
+
const scheme = String(gatewayUrl ?? "").trim().toLowerCase().startsWith("ws://") ? "http" : "https";
|
|
16189
|
+
let host = String(aid ?? "").trim();
|
|
16190
|
+
if (discoveryPort && !host.includes(":")) host = `${host}:${discoveryPort}`;
|
|
16191
|
+
return `${scheme}://${host}/agent.md`;
|
|
16192
|
+
}
|
|
16193
|
+
async function fetchWithTimeout2(input, init = {}, timeoutMs = 3e4) {
|
|
16194
|
+
const controller = new AbortController();
|
|
16195
|
+
const timer = globalThis.setTimeout(() => controller.abort(), timeoutMs);
|
|
16196
|
+
try {
|
|
16197
|
+
return await fetch(input, { ...init, signal: controller.signal });
|
|
16198
|
+
} finally {
|
|
16199
|
+
globalThis.clearTimeout(timer);
|
|
16200
|
+
}
|
|
16201
|
+
}
|
|
16202
|
+
function resultError(result) {
|
|
16203
|
+
return result.ok ? null : result.error;
|
|
16204
|
+
}
|
|
16205
|
+
var AIDStore = class {
|
|
16206
|
+
constructor(opts) {
|
|
16207
|
+
__publicField(this, "aunPath");
|
|
16208
|
+
__publicField(this, "deviceId");
|
|
16209
|
+
__publicField(this, "slotId");
|
|
16210
|
+
__publicField(this, "_encryptionSeed");
|
|
16211
|
+
__publicField(this, "_keystore");
|
|
16212
|
+
__publicField(this, "_auth");
|
|
16213
|
+
__publicField(this, "_crypto");
|
|
16214
|
+
__publicField(this, "_discovery");
|
|
16215
|
+
__publicField(this, "_verifySsl");
|
|
16216
|
+
__publicField(this, "_discoveryPort");
|
|
16217
|
+
__publicField(this, "_gatewayCache", /* @__PURE__ */ new Map());
|
|
16218
|
+
__publicField(this, "_agentMdCache", /* @__PURE__ */ new Map());
|
|
16219
|
+
this.aunPath = String(opts.aunPath ?? "");
|
|
16220
|
+
this._encryptionSeed = String(opts.encryptionSeed ?? "");
|
|
16221
|
+
this.deviceId = opts.deviceId ? normalizeInstanceId(opts.deviceId, "deviceId", { allowEmpty: true }) : getDeviceId();
|
|
16222
|
+
this.slotId = normalizeSlotId(opts.slotId);
|
|
16223
|
+
this._verifySsl = opts.verifySsl ?? true;
|
|
16224
|
+
this._discoveryPort = opts.discoveryPort ?? null;
|
|
16225
|
+
this._keystore = new IndexedDBKeyStore({ encryptionSeed: this._encryptionSeed || void 0 });
|
|
16226
|
+
this._crypto = new CryptoProvider();
|
|
16227
|
+
this._discovery = new GatewayDiscovery();
|
|
16228
|
+
this._auth = new AuthFlow({
|
|
16229
|
+
keystore: this._keystore,
|
|
16230
|
+
crypto: this._crypto,
|
|
16231
|
+
deviceId: this.deviceId,
|
|
16232
|
+
slotId: this.slotId,
|
|
16233
|
+
rootCaPem: opts.rootCaPem ?? null,
|
|
16234
|
+
verifySsl: this._verifySsl
|
|
16235
|
+
});
|
|
16236
|
+
}
|
|
16237
|
+
close() {
|
|
16238
|
+
const close = this._keystore.close;
|
|
16239
|
+
if (typeof close === "function") close.call(this._keystore);
|
|
16240
|
+
}
|
|
16241
|
+
async load(aid) {
|
|
16242
|
+
const target = String(aid ?? "").trim();
|
|
16243
|
+
const certPem = await this._keystore.loadCert(target);
|
|
16244
|
+
if (!certPem) return resultErr(CERT_NOT_FOUND, `certificate not found for aid: ${target}`);
|
|
16245
|
+
let certPub = "";
|
|
16246
|
+
try {
|
|
16247
|
+
certPub = publicKeyDerB64(certPem);
|
|
16248
|
+
if (!certPub) return resultErr(CERT_PARSE_ERROR, `certificate parse failed for aid: ${target}`);
|
|
16249
|
+
} catch (exc) {
|
|
16250
|
+
return resultErr(CERT_PARSE_ERROR, `certificate parse failed for aid: ${target}`, exc);
|
|
16251
|
+
}
|
|
16252
|
+
const validity = parseCertValidity(certPem);
|
|
16253
|
+
if (validity) {
|
|
16254
|
+
const now = Date.now();
|
|
16255
|
+
if (now > validity.notAfter) {
|
|
16256
|
+
return resultErr(CERT_EXPIRED, `certificate expired for aid: ${target}`);
|
|
16257
|
+
}
|
|
16258
|
+
if (now < validity.notBefore) {
|
|
16259
|
+
return resultErr(CERT_NOT_YET_VALID, `certificate not yet valid for aid: ${target}`);
|
|
16260
|
+
}
|
|
16261
|
+
}
|
|
16262
|
+
const cn = parseCertCN(certPem);
|
|
16263
|
+
if (cn && cn !== target) {
|
|
16264
|
+
return resultErr(CERT_CHAIN_BROKEN, `certificate CN mismatch: expected ${target}, got ${cn}`);
|
|
16265
|
+
}
|
|
16266
|
+
let keyPair;
|
|
16267
|
+
try {
|
|
16268
|
+
keyPair = await this._keystore.loadKeyPair(target);
|
|
16269
|
+
} catch (exc) {
|
|
16270
|
+
return resultErr(PRIVATE_KEY_PARSE_ERROR, `private key load failed for aid: ${target}`, exc);
|
|
16271
|
+
}
|
|
16272
|
+
if (!keyPair?.private_key_pem) {
|
|
16273
|
+
return resultOk({
|
|
16274
|
+
aid: await AID.create({
|
|
16275
|
+
aid: target,
|
|
16276
|
+
aunPath: this.aunPath,
|
|
16277
|
+
certPem,
|
|
16278
|
+
privateKeyPem: null,
|
|
16279
|
+
certValid: true,
|
|
16280
|
+
privateKeyValid: false,
|
|
16281
|
+
deviceId: this.deviceId,
|
|
16282
|
+
slotId: this.slotId
|
|
16283
|
+
})
|
|
16284
|
+
});
|
|
16285
|
+
}
|
|
16286
|
+
try {
|
|
16287
|
+
const privateKey = await importPrivateKeyEcdsa(String(keyPair.private_key_pem));
|
|
16288
|
+
if (keyPair.public_key_der_b64 && keyPair.public_key_der_b64 !== certPub) {
|
|
16289
|
+
return resultErr(KEYPAIR_MISMATCH, `keypair public key mismatch for aid: ${target}`);
|
|
16290
|
+
}
|
|
16291
|
+
const probe = new TextEncoder().encode("aun-aidstore-private-key-self-test");
|
|
16292
|
+
const sig = await ecdsaSignDer(privateKey, probe);
|
|
16293
|
+
const pubKey = await importCertPublicKeyEcdsa(certPem);
|
|
16294
|
+
const ok = await ecdsaVerifyDer(pubKey, sig, probe);
|
|
16295
|
+
if (!ok) {
|
|
16296
|
+
return resultErr(KEYPAIR_MISMATCH, `private key does not match certificate for aid: ${target}`);
|
|
16297
|
+
}
|
|
16298
|
+
} catch (exc) {
|
|
16299
|
+
return resultErr(PRIVATE_KEY_PARSE_ERROR, `private key parse failed for aid: ${target}`, exc);
|
|
16300
|
+
}
|
|
16301
|
+
return resultOk({
|
|
16302
|
+
aid: await AID.create({
|
|
16303
|
+
aid: target,
|
|
16304
|
+
aunPath: this.aunPath,
|
|
16305
|
+
certPem,
|
|
16306
|
+
privateKeyPem: String(keyPair.private_key_pem),
|
|
16307
|
+
certValid: true,
|
|
16308
|
+
privateKeyValid: true,
|
|
16309
|
+
deviceId: this.deviceId,
|
|
16310
|
+
slotId: this.slotId
|
|
16311
|
+
})
|
|
16312
|
+
});
|
|
16313
|
+
}
|
|
16314
|
+
async list() {
|
|
16315
|
+
try {
|
|
16316
|
+
const aids = await this._keystore.listIdentities();
|
|
16317
|
+
const identities = [];
|
|
16318
|
+
for (const aid of aids.sort()) {
|
|
16319
|
+
const loaded = await this.load(aid);
|
|
16320
|
+
if (loaded.ok && loaded.data.aid.isPrivateKeyValid()) {
|
|
16321
|
+
identities.push({
|
|
16322
|
+
aid: loaded.data.aid.aid,
|
|
16323
|
+
certFingerprint: loaded.data.aid.certFingerprint,
|
|
16324
|
+
certNotAfter: loaded.data.aid.certNotAfter,
|
|
16325
|
+
certIssuer: loaded.data.aid.certIssuer
|
|
16326
|
+
});
|
|
16327
|
+
}
|
|
16328
|
+
}
|
|
16329
|
+
return resultOk({ identities });
|
|
16330
|
+
} catch (exc) {
|
|
16331
|
+
return resultErr("LIST_IDENTITIES_FAILED", String(exc), exc);
|
|
16332
|
+
}
|
|
16333
|
+
}
|
|
16334
|
+
async changeSeed(oldSeed, newSeed) {
|
|
16335
|
+
try {
|
|
16336
|
+
const changed = await this._keystore.changeSeed?.(oldSeed, newSeed);
|
|
16337
|
+
this._encryptionSeed = String(newSeed ?? "");
|
|
16338
|
+
return resultOk({ changed: true, count: changed?.privateKeysMigrated ?? changed?.migrated ?? 0 });
|
|
16339
|
+
} catch (exc) {
|
|
16340
|
+
return resultErr(PRIVATE_KEY_PARSE_ERROR, String(exc), exc);
|
|
16341
|
+
}
|
|
16342
|
+
}
|
|
16343
|
+
async register(aid) {
|
|
16344
|
+
const target = String(aid ?? "").trim();
|
|
16345
|
+
try {
|
|
16346
|
+
validateRegisterAidName(target);
|
|
16347
|
+
const gatewayUrl = await this._resolveGateway(target);
|
|
16348
|
+
await this._auth.registerAid(gatewayUrl, target);
|
|
16349
|
+
return resultOk({ registered: true });
|
|
16350
|
+
} catch (exc) {
|
|
16351
|
+
if (exc instanceof IdentityConflictError) {
|
|
16352
|
+
return resultErr(IDENTITY_CONFLICT, String(exc), exc);
|
|
16353
|
+
}
|
|
16354
|
+
if (exc instanceof ValidationError) {
|
|
16355
|
+
return resultErr(INVALID_AID_FORMAT, String(exc), exc);
|
|
16356
|
+
}
|
|
16357
|
+
const message = exc instanceof Error ? exc.message : String(exc);
|
|
16358
|
+
if (/network|connect|timeout|abort/i.test(message)) {
|
|
16359
|
+
return resultErr(NETWORK_ERROR, message, exc);
|
|
16360
|
+
}
|
|
16361
|
+
return resultErr(SERVER_ERROR, message, exc);
|
|
16362
|
+
}
|
|
16363
|
+
}
|
|
16364
|
+
async exists(aid) {
|
|
16365
|
+
const target = String(aid ?? "").trim();
|
|
16366
|
+
try {
|
|
16367
|
+
const gatewayUrl = await this._resolveGateway(target);
|
|
16368
|
+
const response = await fetchWithTimeout2(pkiCertUrl(gatewayUrl, target), { method: "HEAD" }, 1e4);
|
|
16369
|
+
if (response.status === 200) return resultOk({ exists: true });
|
|
16370
|
+
if (response.status === 404) return resultOk({ exists: false });
|
|
16371
|
+
return resultErr(NETWORK_ERROR, `unexpected PKI HEAD status ${response.status}`);
|
|
16372
|
+
} catch (exc) {
|
|
16373
|
+
return resultErr(NETWORK_ERROR, String(exc), exc);
|
|
16374
|
+
}
|
|
16375
|
+
}
|
|
16376
|
+
async resolve(aid, opts) {
|
|
16377
|
+
const target = String(aid ?? "").trim();
|
|
16378
|
+
const forceRefresh = !!opts?.forceRefresh;
|
|
16379
|
+
const skipAgentMd = !!opts?.skipAgentMd;
|
|
16380
|
+
try {
|
|
16381
|
+
let peer;
|
|
16382
|
+
let certFromCache = false;
|
|
16383
|
+
const cached = await this.load(target);
|
|
16384
|
+
if (cached.ok && !forceRefresh) {
|
|
16385
|
+
peer = cached.data.aid;
|
|
16386
|
+
certFromCache = true;
|
|
16387
|
+
} else {
|
|
16388
|
+
const gatewayUrl = await this._resolveGateway(target);
|
|
16389
|
+
const certPem = await this._auth.fetchPeerCert(gatewayUrl, target);
|
|
16390
|
+
if (!certPem) return resultErr(CERT_NOT_FOUND, `certificate not found for aid: ${target}`);
|
|
16391
|
+
await this._keystore.saveCert(target, certPem);
|
|
16392
|
+
const reloaded = await this.load(target);
|
|
16393
|
+
if (!reloaded.ok) return reloaded;
|
|
16394
|
+
peer = reloaded.data.aid;
|
|
16395
|
+
}
|
|
16396
|
+
const source = { certFromCache, cert_from_cache: certFromCache, agentMdFetched: false, agent_md_fetched: false };
|
|
16397
|
+
if (skipAgentMd) return resultOk({ aid: peer, source });
|
|
16398
|
+
const agentMd = await this.fetchAgentMd(target);
|
|
16399
|
+
if (!agentMd.ok) return agentMd;
|
|
16400
|
+
source.agentMdFetched = true;
|
|
16401
|
+
source.agent_md_fetched = true;
|
|
16402
|
+
return resultOk({ aid: peer, agentMd: agentMd.data, agent_md: agentMd.data, source });
|
|
16403
|
+
} catch (exc) {
|
|
16404
|
+
return resultErr(NETWORK_ERROR, String(exc), exc);
|
|
16405
|
+
}
|
|
16406
|
+
}
|
|
16407
|
+
async fetchAgentMd(aid) {
|
|
16408
|
+
const target = String(aid ?? "").trim();
|
|
16409
|
+
try {
|
|
16410
|
+
const gatewayUrl = await this._resolveGateway(target);
|
|
16411
|
+
const cached = this._agentMdCache.get(target) ?? {};
|
|
16412
|
+
const headers = { Accept: "text/markdown" };
|
|
16413
|
+
if (cached.etag) headers["If-None-Match"] = cached.etag;
|
|
16414
|
+
if (cached.lastModified) headers["If-Modified-Since"] = cached.lastModified;
|
|
16415
|
+
const response = await fetchWithTimeout2(agentMdUrl(target, gatewayUrl, this._discoveryPort), {
|
|
16416
|
+
method: "GET",
|
|
16417
|
+
headers
|
|
16418
|
+
}, 3e4);
|
|
16419
|
+
let content = "";
|
|
16420
|
+
if (response.status === 304 && cached.content) {
|
|
16421
|
+
content = cached.content;
|
|
16422
|
+
} else if (response.status === 404) {
|
|
16423
|
+
return resultErr(AGENTMD_NOT_FOUND, `agent.md not found for aid: ${target}`);
|
|
16424
|
+
} else if (!response.ok) {
|
|
16425
|
+
return resultErr(NETWORK_ERROR, `download agent.md failed: HTTP ${response.status}`);
|
|
16426
|
+
} else {
|
|
16427
|
+
content = await response.text();
|
|
16428
|
+
}
|
|
16429
|
+
let peer;
|
|
16430
|
+
const loaded = await this.load(target);
|
|
16431
|
+
if (loaded.ok) {
|
|
16432
|
+
peer = loaded.data.aid;
|
|
16433
|
+
} else {
|
|
16434
|
+
const resolved = await this.resolve(target, { skipAgentMd: true });
|
|
16435
|
+
if (!resolved.ok) return resolved;
|
|
16436
|
+
peer = resolved.data.aid;
|
|
16437
|
+
}
|
|
16438
|
+
const verified = await peer.verifyAgentMd(content);
|
|
16439
|
+
if (!verified.ok) return verified;
|
|
16440
|
+
const signature = verified.data;
|
|
16441
|
+
const verification = { status: signature.status };
|
|
16442
|
+
if (signature.reason) verification.reason = signature.reason;
|
|
16443
|
+
const etag = String(response.headers?.get("ETag") ?? response.headers?.get("etag") ?? cached.etag ?? "").trim();
|
|
16444
|
+
const lastModified = String(response.headers?.get("Last-Modified") ?? response.headers?.get("last-modified") ?? cached.lastModified ?? "").trim();
|
|
16445
|
+
this._agentMdCache.set(target, { content, etag, lastModified, updatedAt: Date.now() });
|
|
16446
|
+
return resultOk({
|
|
16447
|
+
aid: target,
|
|
16448
|
+
content,
|
|
16449
|
+
verification,
|
|
16450
|
+
signature,
|
|
16451
|
+
certPem: peer.certPem,
|
|
16452
|
+
cert_pem: peer.certPem,
|
|
16453
|
+
etag,
|
|
16454
|
+
lastModified,
|
|
16455
|
+
last_modified: lastModified,
|
|
16456
|
+
status: response.status
|
|
16457
|
+
});
|
|
16458
|
+
} catch (exc) {
|
|
16459
|
+
return resultErr(NETWORK_ERROR, String(exc), exc);
|
|
16460
|
+
}
|
|
16461
|
+
}
|
|
16462
|
+
async headAgentMd(aid) {
|
|
16463
|
+
const target = String(aid ?? "").trim();
|
|
16464
|
+
try {
|
|
16465
|
+
const gatewayUrl = await this._resolveGateway(target);
|
|
16466
|
+
const response = await fetchWithTimeout2(agentMdUrl(target, gatewayUrl, this._discoveryPort), {
|
|
16467
|
+
method: "HEAD",
|
|
16468
|
+
headers: { Accept: "text/markdown" }
|
|
16469
|
+
}, 15e3);
|
|
16470
|
+
if (response.status === 404) {
|
|
16471
|
+
return resultErr(AGENTMD_NOT_FOUND, `agent.md not found for aid: ${target}`);
|
|
16472
|
+
}
|
|
16473
|
+
if (!response.ok) {
|
|
16474
|
+
return resultErr(NETWORK_ERROR, `head agent.md failed: HTTP ${response.status}`);
|
|
16475
|
+
}
|
|
16476
|
+
const etag = String(response.headers?.get("ETag") ?? response.headers?.get("etag") ?? "").trim();
|
|
16477
|
+
const lastModified = String(response.headers?.get("Last-Modified") ?? response.headers?.get("last-modified") ?? "").trim();
|
|
16478
|
+
const contentLength = Number.parseInt(String(response.headers?.get("Content-Length") ?? response.headers?.get("content-length") ?? "0"), 10) || 0;
|
|
16479
|
+
const cached = this._agentMdCache.get(target) ?? {};
|
|
16480
|
+
this._agentMdCache.set(target, { ...cached, etag, lastModified, updatedAt: Date.now() });
|
|
16481
|
+
return resultOk({
|
|
16482
|
+
aid: target,
|
|
16483
|
+
found: true,
|
|
16484
|
+
etag,
|
|
16485
|
+
lastModified,
|
|
16486
|
+
last_modified: lastModified,
|
|
16487
|
+
contentLength,
|
|
16488
|
+
content_length: contentLength,
|
|
16489
|
+
status: response.status
|
|
16490
|
+
});
|
|
16491
|
+
} catch (exc) {
|
|
16492
|
+
return resultErr(NETWORK_ERROR, String(exc), exc);
|
|
16493
|
+
}
|
|
16494
|
+
}
|
|
16495
|
+
async checkAgentMd(aid, ttlDays = 1) {
|
|
16496
|
+
const target = String(aid ?? "").trim();
|
|
16497
|
+
const cached = this._agentMdCache.get(target) ?? {};
|
|
16498
|
+
const head = await this.headAgentMd(target);
|
|
16499
|
+
let remote;
|
|
16500
|
+
if (!head.ok) {
|
|
16501
|
+
if (head.error.code === AGENTMD_NOT_FOUND) {
|
|
16502
|
+
remote = { aid: target, found: false, etag: "", last_modified: "", content_length: 0, status: 404 };
|
|
16503
|
+
} else {
|
|
16504
|
+
return head;
|
|
16505
|
+
}
|
|
16506
|
+
} else {
|
|
16507
|
+
remote = head.data;
|
|
16508
|
+
}
|
|
16509
|
+
const localEtag = String(cached.etag ?? "").trim();
|
|
16510
|
+
const localFound = !!cached.content;
|
|
16511
|
+
const remoteFound = !!remote.found;
|
|
16512
|
+
const remoteEtag = String(remote.etag ?? "").trim();
|
|
16513
|
+
const needsUpdate = remoteFound && (!localFound || !!remoteEtag && remoteEtag !== localEtag);
|
|
16514
|
+
return resultOk({
|
|
16515
|
+
aid: target,
|
|
16516
|
+
localFound,
|
|
16517
|
+
local_found: localFound,
|
|
16518
|
+
remoteFound,
|
|
16519
|
+
remote_found: remoteFound,
|
|
16520
|
+
localEtag,
|
|
16521
|
+
local_etag: localEtag,
|
|
16522
|
+
remoteEtag,
|
|
16523
|
+
remote_etag: remoteEtag,
|
|
16524
|
+
lastModified: String(remote.lastModified ?? remote.last_modified ?? ""),
|
|
16525
|
+
last_modified: String(remote.last_modified ?? remote.lastModified ?? ""),
|
|
16526
|
+
needsUpdate,
|
|
16527
|
+
needs_update: needsUpdate,
|
|
16528
|
+
ttlDays,
|
|
16529
|
+
ttl_days: ttlDays
|
|
16530
|
+
});
|
|
16531
|
+
}
|
|
16532
|
+
async diagnose(aid) {
|
|
16533
|
+
const target = String(aid ?? "").trim();
|
|
16534
|
+
const loaded = await this.load(target);
|
|
16535
|
+
const remote = await this.exists(target);
|
|
16536
|
+
const remoteError = resultError(remote);
|
|
16537
|
+
const localError = resultError(loaded);
|
|
16538
|
+
const localAid = loaded.ok ? loaded.data.aid : null;
|
|
16539
|
+
const localCert = !!localAid?.isCertValid();
|
|
16540
|
+
const localPrivateKey = !!localAid?.isPrivateKeyValid();
|
|
16541
|
+
const localValid = localCert && localPrivateKey;
|
|
16542
|
+
const remoteRegistered = !!(remote.ok && remote.data.exists);
|
|
16543
|
+
const suggestions = [];
|
|
16544
|
+
if (!localPrivateKey) suggestions.push("load or register a local identity with a valid private key");
|
|
16545
|
+
if (!remoteRegistered) suggestions.push("register the AID before using it on the network");
|
|
16546
|
+
if (remoteError) suggestions.push(`remote registration check failed: ${remoteError.message}`);
|
|
16547
|
+
const status = localPrivateKey && remoteRegistered ? "ready" : remoteRegistered ? "registered_remote" : remote.ok ? "available" : "unknown";
|
|
16548
|
+
return resultOk({
|
|
16549
|
+
aid: target,
|
|
16550
|
+
status,
|
|
16551
|
+
localValid,
|
|
16552
|
+
local_valid: localValid,
|
|
16553
|
+
remoteRegistered,
|
|
16554
|
+
remote_registered: remoteRegistered,
|
|
16555
|
+
suggestions,
|
|
16556
|
+
local: { cert: localCert, private_key: localPrivateKey, error: localError },
|
|
16557
|
+
remote: { checked: remote.ok, exists: remote.ok ? remoteRegistered : null, error: remoteError }
|
|
16558
|
+
});
|
|
16559
|
+
}
|
|
16560
|
+
async renewCert(aid) {
|
|
16561
|
+
const target = String(aid ?? "").trim();
|
|
16562
|
+
const loaded = await this.load(target);
|
|
16563
|
+
if (!loaded.ok || !loaded.data.aid.isPrivateKeyValid()) {
|
|
16564
|
+
return resultErr(PRIVATE_KEY_REQUIRED, `private key required for aid: ${target}`);
|
|
16565
|
+
}
|
|
16566
|
+
try {
|
|
16567
|
+
const identity = await this._auth.loadIdentityOrNone(target);
|
|
16568
|
+
if (!identity?.cert || !identity.private_key_pem) {
|
|
16569
|
+
return resultErr(PRIVATE_KEY_REQUIRED, `private key required for aid: ${target}`);
|
|
16570
|
+
}
|
|
16571
|
+
const gatewayUrl = await this._resolveGateway(target);
|
|
16572
|
+
const clientNonce = this._crypto.newClientNonce();
|
|
16573
|
+
const phase1 = await this._auth._shortRpc(gatewayUrl, "auth.aid_login1", {
|
|
16574
|
+
aid: target,
|
|
16575
|
+
cert: identity.cert,
|
|
16576
|
+
client_nonce: clientNonce
|
|
16577
|
+
});
|
|
16578
|
+
const [signature, clientTime] = await this._crypto.signLoginNonce(identity.private_key_pem, String(phase1.nonce ?? ""));
|
|
16579
|
+
const response = await this._auth._shortRpc(gatewayUrl, "auth.renew_cert", {
|
|
16580
|
+
aid: target,
|
|
16581
|
+
request_id: String(phase1.request_id ?? ""),
|
|
16582
|
+
nonce: String(phase1.nonce ?? ""),
|
|
16583
|
+
client_time: clientTime,
|
|
16584
|
+
signature
|
|
16585
|
+
});
|
|
16586
|
+
const certPem = String(response.cert ?? response.cert_pem ?? "").trim();
|
|
16587
|
+
if (!certPem) return resultErr(CERT_RENEWAL_FAILED, "server response missing certificate");
|
|
16588
|
+
await this._keystore.saveCert(target, certPem);
|
|
16589
|
+
const refreshed = await this.load(target);
|
|
16590
|
+
if (!refreshed.ok) return resultErr(CERT_RENEWAL_FAILED, "renewed certificate reload failed");
|
|
16591
|
+
return resultOk({
|
|
16592
|
+
renewed: true,
|
|
16593
|
+
newFingerprint: refreshed.data.aid.certFingerprint,
|
|
16594
|
+
new_fingerprint: refreshed.data.aid.certFingerprint
|
|
16595
|
+
});
|
|
16596
|
+
} catch (exc) {
|
|
16597
|
+
return resultErr(CERT_RENEWAL_FAILED, String(exc), exc);
|
|
16598
|
+
}
|
|
16599
|
+
}
|
|
16600
|
+
async rekey(aid) {
|
|
16601
|
+
const target = String(aid ?? "").trim();
|
|
16602
|
+
const loaded = await this.load(target);
|
|
16603
|
+
if (!loaded.ok || !loaded.data.aid.isPrivateKeyValid()) {
|
|
16604
|
+
return resultErr(PRIVATE_KEY_REQUIRED, `private key required for aid: ${target}`);
|
|
16605
|
+
}
|
|
16606
|
+
try {
|
|
16607
|
+
const identity = await this._auth.loadIdentityOrNone(target);
|
|
16608
|
+
if (!identity?.cert || !identity.private_key_pem) {
|
|
16609
|
+
return resultErr(PRIVATE_KEY_REQUIRED, `private key required for aid: ${target}`);
|
|
16610
|
+
}
|
|
16611
|
+
const gatewayUrl = await this._resolveGateway(target);
|
|
16612
|
+
const newIdentity = await this._crypto.generateIdentity();
|
|
16613
|
+
const clientNonce = this._crypto.newClientNonce();
|
|
16614
|
+
const phase1 = await this._auth._shortRpc(gatewayUrl, "auth.aid_login1", {
|
|
16615
|
+
aid: target,
|
|
16616
|
+
cert: identity.cert,
|
|
16617
|
+
client_nonce: clientNonce
|
|
16618
|
+
});
|
|
16619
|
+
const signPayload = `${String(phase1.nonce ?? "")}${newIdentity.public_key_der_b64}`;
|
|
16620
|
+
const signResult = await loaded.data.aid.sign(signPayload);
|
|
16621
|
+
if (!signResult.ok) return resultErr(REKEY_FAILED, signResult.error.message);
|
|
16622
|
+
const response = await this._auth._shortRpc(gatewayUrl, "auth.rekey", {
|
|
16623
|
+
aid: target,
|
|
16624
|
+
request_id: String(phase1.request_id ?? ""),
|
|
16625
|
+
nonce: String(phase1.nonce ?? ""),
|
|
16626
|
+
new_public_key: newIdentity.public_key_der_b64,
|
|
16627
|
+
signature: signResult.data.signature
|
|
16628
|
+
});
|
|
16629
|
+
const certPem = String(response.cert ?? response.cert_pem ?? "").trim();
|
|
16630
|
+
if (!certPem) return resultErr(REKEY_FAILED, "server response missing certificate");
|
|
16631
|
+
const nextIdentity = {
|
|
16632
|
+
aid: target,
|
|
16633
|
+
private_key_pem: newIdentity.private_key_pem,
|
|
16634
|
+
public_key_der_b64: newIdentity.public_key_der_b64,
|
|
16635
|
+
curve: newIdentity.curve,
|
|
16636
|
+
cert: certPem
|
|
16637
|
+
};
|
|
16638
|
+
await this._keystore.saveIdentity(target, nextIdentity);
|
|
16639
|
+
const refreshed = await this.load(target);
|
|
16640
|
+
if (!refreshed.ok) return resultErr(REKEY_FAILED, "rekeyed identity reload failed");
|
|
16641
|
+
return resultOk({
|
|
16642
|
+
rekeyed: true,
|
|
16643
|
+
newFingerprint: refreshed.data.aid.certFingerprint,
|
|
16644
|
+
new_fingerprint: refreshed.data.aid.certFingerprint
|
|
16645
|
+
});
|
|
16646
|
+
} catch (exc) {
|
|
16647
|
+
return resultErr(REKEY_FAILED, String(exc), exc);
|
|
16648
|
+
}
|
|
16649
|
+
}
|
|
16650
|
+
async _resolveGateway(aid) {
|
|
16651
|
+
const target = String(aid ?? "").trim();
|
|
16652
|
+
if (!target) throw new ValidationError("AIDStore requires non-empty aid to resolve gateway");
|
|
16653
|
+
const issuerDomain = issuerFromAid(target);
|
|
16654
|
+
const cached = this._gatewayCache.get(issuerDomain);
|
|
16655
|
+
if (cached) return cached;
|
|
16656
|
+
const persisted = await this._loadCachedGatewayUrl(target);
|
|
16657
|
+
if (persisted) {
|
|
16658
|
+
this._gatewayCache.set(issuerDomain, persisted);
|
|
16659
|
+
return persisted;
|
|
16660
|
+
}
|
|
16661
|
+
const port = this._discoveryPort ? `:${this._discoveryPort}` : "";
|
|
16662
|
+
const candidates = [
|
|
16663
|
+
`https://${target}${port}/.well-known/aun-gateway`,
|
|
16664
|
+
`https://gateway.${issuerDomain}${port}/.well-known/aun-gateway`
|
|
16665
|
+
];
|
|
16666
|
+
let lastError = null;
|
|
16667
|
+
for (const url of candidates) {
|
|
16668
|
+
try {
|
|
16669
|
+
const discovered = await this._discovery.discover(url);
|
|
16670
|
+
this._gatewayCache.set(issuerDomain, discovered);
|
|
16671
|
+
await this._persistGatewayUrl(target, discovered);
|
|
16672
|
+
return discovered;
|
|
16673
|
+
} catch (exc) {
|
|
16674
|
+
lastError = exc;
|
|
16675
|
+
}
|
|
16676
|
+
}
|
|
16677
|
+
throw lastError instanceof Error ? lastError : new Error(String(lastError));
|
|
16678
|
+
}
|
|
16679
|
+
async _loadCachedGatewayUrl(aid) {
|
|
16680
|
+
const getMetadata = this._keystore.getMetadata;
|
|
16681
|
+
if (typeof getMetadata !== "function") return "";
|
|
16682
|
+
try {
|
|
16683
|
+
const raw = String(await getMetadata.call(this._keystore, aid, "gateway_url") ?? "").trim();
|
|
16684
|
+
if (!raw) return "";
|
|
16685
|
+
if (raw.startsWith('"') && raw.endsWith('"')) {
|
|
16686
|
+
try {
|
|
16687
|
+
const parsed = JSON.parse(raw);
|
|
16688
|
+
if (typeof parsed === "string") return parsed.trim();
|
|
16689
|
+
} catch {
|
|
16690
|
+
return raw;
|
|
16691
|
+
}
|
|
16692
|
+
}
|
|
16693
|
+
return raw;
|
|
16694
|
+
} catch {
|
|
16695
|
+
return "";
|
|
16696
|
+
}
|
|
16697
|
+
}
|
|
16698
|
+
async _persistGatewayUrl(aid, gatewayUrl) {
|
|
16699
|
+
const setMetadata = this._keystore.setMetadata;
|
|
16700
|
+
if (typeof setMetadata !== "function" || !gatewayUrl) return;
|
|
16701
|
+
try {
|
|
16702
|
+
await setMetadata.call(this._keystore, aid, "gateway_url", gatewayUrl);
|
|
16703
|
+
} catch {
|
|
16704
|
+
}
|
|
16705
|
+
}
|
|
16706
|
+
};
|
|
16707
|
+
|
|
16774
16708
|
// src/index.ts
|
|
16775
16709
|
init_crypto();
|
|
16776
16710
|
|
|
@@ -16826,20 +16760,18 @@ var ProtectedHeaders = class _ProtectedHeaders {
|
|
|
16826
16760
|
return new _ProtectedHeaders(values ?? {});
|
|
16827
16761
|
}
|
|
16828
16762
|
};
|
|
16829
|
-
|
|
16830
|
-
// src/index.ts
|
|
16831
|
-
var __version__ = "0.3.5";
|
|
16832
16763
|
export {
|
|
16764
|
+
AID,
|
|
16765
|
+
AIDStore,
|
|
16833
16766
|
AUNClient,
|
|
16834
16767
|
AUNError,
|
|
16835
16768
|
AuthError,
|
|
16836
16769
|
AuthFlow,
|
|
16837
|
-
AuthNamespace,
|
|
16838
16770
|
CertificateRevokedError,
|
|
16839
16771
|
ClientSignatureError,
|
|
16840
16772
|
ConnectionError,
|
|
16773
|
+
ConnectionState,
|
|
16841
16774
|
CryptoProvider,
|
|
16842
|
-
CustodyNamespace,
|
|
16843
16775
|
E2EEDecryptFailedError,
|
|
16844
16776
|
E2EEDegradedError,
|
|
16845
16777
|
E2EEError,
|
|
@@ -16856,7 +16788,6 @@ export {
|
|
|
16856
16788
|
IdentityConflictError,
|
|
16857
16789
|
IndexedDBKeyStore,
|
|
16858
16790
|
IndexedDBSecretStore,
|
|
16859
|
-
MetaNamespace,
|
|
16860
16791
|
NotFoundError,
|
|
16861
16792
|
PermissionError,
|
|
16862
16793
|
ProtectedHeaders,
|
|
@@ -16873,7 +16804,7 @@ export {
|
|
|
16873
16804
|
V2KeyStore,
|
|
16874
16805
|
V2Session,
|
|
16875
16806
|
ValidationError,
|
|
16876
|
-
__version__,
|
|
16807
|
+
VERSION as __version__,
|
|
16877
16808
|
computeStateCommitment,
|
|
16878
16809
|
createConfig,
|
|
16879
16810
|
createDefaultSecretStore,
|
|
@@ -16882,7 +16813,9 @@ export {
|
|
|
16882
16813
|
encryptP2PMessage,
|
|
16883
16814
|
getDeviceId,
|
|
16884
16815
|
isJsonObject,
|
|
16885
|
-
mapRemoteError
|
|
16816
|
+
mapRemoteError,
|
|
16817
|
+
resultErr,
|
|
16818
|
+
resultOk
|
|
16886
16819
|
};
|
|
16887
16820
|
/*! Bundled license information:
|
|
16888
16821
|
|