@agentunion/fastaun 0.4.4 → 0.4.6
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 +41 -0
- package/_packed_docs/CHANGELOG.md +41 -0
- package/_packed_docs/INDEX.md +2 -2
- package/_packed_docs/KITE_DOCS_GUIDE.md +1 -1
- package/_packed_docs/agent.md//350/277/234/347/250/213agent.md/347/274/223/345/255/230/344/270/216etag/351/200/217/344/274/240/346/226/271/346/241/210.md +73 -84
- package/_packed_docs/sdk/01-/345/277/253/351/200/237/345/274/200/345/247/213.md +16 -15
- package/_packed_docs/sdk/02-WebSocket/345/215/217/350/256/256.md +2 -2
- package/_packed_docs/sdk/03-/346/240/270/345/277/203/346/246/202/345/277/265.md +22 -5
- package/_packed_docs/sdk/04-/350/277/236/346/216/245/344/270/216/350/256/244/350/257/201.md +42 -26
- package/_packed_docs/sdk/05-E2EE/345/212/240/345/257/206/351/200/232/344/277/241.md +2 -2
- package/_packed_docs/sdk/06-API/346/211/213/345/206/214.md +61 -35
- package/_packed_docs/sdk/08-/346/234/200/344/275/263/345/256/236/350/267/265.md +3 -3
- package/_packed_docs/sdk/09-message-rpc-manual.md +6 -6
- package/_packed_docs/sdk/AUN_DOCS_GUIDE.md +6 -4
- package/_packed_docs/sdk/INDEX.md +2 -2
- package/_packed_docs/sdk/README.md +3 -3
- package/dist/agent-md.d.ts +101 -0
- package/dist/agent-md.js +778 -0
- package/dist/agent-md.js.map +1 -0
- package/dist/aid-store.d.ts +7 -39
- package/dist/aid-store.js +74 -141
- package/dist/aid-store.js.map +1 -1
- package/dist/auth.d.ts +17 -32
- package/dist/auth.js +42 -295
- package/dist/auth.js.map +1 -1
- package/dist/client.d.ts +6 -65
- package/dist/client.js +213 -913
- package/dist/client.js.map +1 -1
- package/dist/crypto.d.ts +1 -1
- package/dist/crypto.js +1 -1
- package/dist/index.d.ts +4 -2
- package/dist/index.js +3 -1
- package/dist/index.js.map +1 -1
- package/dist/keystore/aid-db.d.ts +0 -4
- package/dist/keystore/aid-db.js +4 -95
- package/dist/keystore/aid-db.js.map +1 -1
- package/dist/keystore/file.d.ts +8 -3
- package/dist/keystore/file.js +103 -24
- package/dist/keystore/file.js.map +1 -1
- package/dist/keystore/index.d.ts +43 -36
- package/dist/keystore/index.js +3 -2
- package/dist/keystore/index.js.map +1 -1
- package/dist/keystore/local-identity-store.d.ts +70 -0
- package/dist/keystore/local-identity-store.js +525 -0
- package/dist/keystore/local-identity-store.js.map +1 -0
- package/dist/keystore/local-token-store.d.ts +68 -0
- package/dist/keystore/local-token-store.js +368 -0
- package/dist/keystore/local-token-store.js.map +1 -0
- package/dist/register-flow.d.ts +57 -0
- package/dist/register-flow.js +433 -0
- package/dist/register-flow.js.map +1 -0
- package/dist/secret-store/file-store.js +6 -1
- package/dist/secret-store/file-store.js.map +1 -1
- package/dist/v2/session/keystore.d.ts +5 -0
- package/dist/v2/session/keystore.js +21 -3
- package/dist/v2/session/keystore.js.map +1 -1
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +1 -1
- package/_packed_docs/0.4.0_/345/267/256/345/274/202/346/240/270/345/256/236/345/206/263/347/255/226/350/256/260/345/275/225.md +0 -302
- package/_packed_docs/AUN_SDK_0.4.0_/350/256/276/350/256/241/345/257/271/346/257/224/345/210/206/346/236/220.md +0 -194
- package/_packed_docs/AUN_SDK_/351/207/215/346/236/204/345/256/236/346/226/275/350/256/241/345/210/222.md +0 -596
- 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 +0 -1698
- package/_packed_docs/python-sdk-v2-only-changelog.md +0 -189
package/dist/client.js
CHANGED
|
@@ -11,22 +11,21 @@
|
|
|
11
11
|
* - 群组 E2EE 全自动编排(建群/加人/踢人/退出)
|
|
12
12
|
*/
|
|
13
13
|
import * as crypto from 'node:crypto';
|
|
14
|
-
import * as fs from 'node:fs';
|
|
15
14
|
import * as http from 'node:http';
|
|
16
15
|
import * as https from 'node:https';
|
|
17
|
-
import * as path from 'node:path';
|
|
18
16
|
import { URL } from 'node:url';
|
|
19
|
-
import { configFromMap, getDeviceId,
|
|
17
|
+
import { configFromMap, getDeviceId, normalizeSlotId, slotIsolationKey } from './config.js';
|
|
20
18
|
import { CryptoProvider } from './crypto.js';
|
|
21
19
|
import { GatewayDiscovery } from './discovery.js';
|
|
22
20
|
import { DnsResilientNet } from './net.js';
|
|
23
21
|
import { AUNError, AuthError, ConnectionError, E2EEError, NotFoundError, PermissionError, StateError, TimeoutError, ValidationError, } from './errors.js';
|
|
24
22
|
import { EventDispatcher } from './events.js';
|
|
25
|
-
import {
|
|
23
|
+
import { LocalTokenStore } from './keystore/local-token-store.js';
|
|
26
24
|
import { AUNLogger } from './logger.js';
|
|
27
25
|
import { normalizeGroupId } from './group-id.js';
|
|
28
26
|
import { RPCTransport } from './transport.js';
|
|
29
27
|
import { AuthFlow } from './auth.js';
|
|
28
|
+
import { AgentMdManager } from './agent-md.js';
|
|
30
29
|
import { SeqTracker } from './seq-tracker.js';
|
|
31
30
|
import { V2Session } from './v2/session/index.js';
|
|
32
31
|
import { encryptP2PMessage, encryptGroupMessage, decryptMessage, } from './v2/e2ee/index.js';
|
|
@@ -226,7 +225,6 @@ const SIGNED_METHODS = new Set([
|
|
|
226
225
|
]);
|
|
227
226
|
/** peer 证书缓存 TTL(1 小时) */
|
|
228
227
|
const PEER_CERT_CACHE_TTL = 3600;
|
|
229
|
-
const AGENT_MD_HTTP_TIMEOUT_MS = 30_000;
|
|
230
228
|
function normalizeV2WrapPolicy(raw) {
|
|
231
229
|
if (!raw || typeof raw !== 'object' || Array.isArray(raw)) {
|
|
232
230
|
return { explicit: false, version: '', protocol: '', scope: 'device' };
|
|
@@ -404,33 +402,93 @@ function lengthPrefixedBytesKey(...parts) {
|
|
|
404
402
|
}
|
|
405
403
|
return Buffer.concat(chunks);
|
|
406
404
|
}
|
|
407
|
-
function
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
405
|
+
function createAgentMdManagerForRuntime(opts) {
|
|
406
|
+
return new AgentMdManager({
|
|
407
|
+
aunPath: opts.config().aunPath,
|
|
408
|
+
verifySsl: opts.config().verifySsl,
|
|
409
|
+
discoveryPort: opts.config().discoveryPort,
|
|
410
|
+
logger: opts.logger(),
|
|
411
|
+
ownerAidGetter: opts.ownerAid,
|
|
412
|
+
currentAidGetter: opts.currentAid,
|
|
413
|
+
gatewayResolver: async (aid) => {
|
|
414
|
+
const gatewayUrl = await opts.gateway.resolve(aid);
|
|
415
|
+
opts.gateway.set(gatewayUrl);
|
|
416
|
+
return gatewayUrl;
|
|
417
|
+
},
|
|
418
|
+
accessTokenResolver: async (aid, gatewayUrl) => {
|
|
419
|
+
const target = String(aid ?? '').trim();
|
|
420
|
+
let identity = opts.identity.get();
|
|
421
|
+
if (!identity || String(identity.aid ?? '') !== target) {
|
|
422
|
+
throw new StateError('no local identity found, register or load an AID first');
|
|
423
|
+
}
|
|
424
|
+
const auth = opts.auth();
|
|
425
|
+
const cachedToken = String(identity.access_token ?? '');
|
|
426
|
+
const expiresAt = auth.getAccessTokenExpiry(identity);
|
|
427
|
+
if (cachedToken && (expiresAt === null || expiresAt > Date.now() / 1000 + 30)) {
|
|
428
|
+
return cachedToken;
|
|
429
|
+
}
|
|
430
|
+
if (identity.refresh_token) {
|
|
431
|
+
try {
|
|
432
|
+
const refreshed = await auth.refreshCachedTokens(gatewayUrl, identity);
|
|
433
|
+
const refreshedToken = String(refreshed.access_token ?? '');
|
|
434
|
+
const refreshedExpiry = auth.getAccessTokenExpiry(refreshed);
|
|
435
|
+
if (refreshedToken && (refreshedExpiry === null || refreshedExpiry > Date.now() / 1000 + 30)) {
|
|
436
|
+
opts.identity.set(refreshed);
|
|
437
|
+
return refreshedToken;
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
catch {
|
|
441
|
+
// refresh 失败时回退到完整 authenticate。
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
const result = await auth.authenticate(gatewayUrl, { aid: target });
|
|
445
|
+
const token = String(result.access_token ?? '');
|
|
446
|
+
if (!token)
|
|
447
|
+
throw new StateError('authenticate did not return access_token');
|
|
448
|
+
identity = auth.loadIdentityOrNone(target) ?? {
|
|
449
|
+
...identity,
|
|
450
|
+
access_token: token,
|
|
451
|
+
refresh_token: String(result.refresh_token ?? identity.refresh_token ?? ''),
|
|
452
|
+
access_token_expires_at: typeof result.expires_at === 'number' ? result.expires_at : identity.access_token_expires_at,
|
|
453
|
+
token_exp: typeof result.expires_at === 'number' ? result.expires_at : identity.token_exp,
|
|
454
|
+
expires_at: typeof result.expires_at === 'number' ? result.expires_at : identity.expires_at,
|
|
455
|
+
};
|
|
456
|
+
opts.identity.set(identity);
|
|
457
|
+
return token;
|
|
458
|
+
},
|
|
459
|
+
peerResolver: async (aid) => {
|
|
460
|
+
const target = String(aid ?? '').trim();
|
|
461
|
+
const current = opts.currentAid();
|
|
462
|
+
if (current?.aid === target)
|
|
463
|
+
return current;
|
|
464
|
+
let certPem = '';
|
|
465
|
+
try {
|
|
466
|
+
certPem = String(opts.tokenStore().loadCert(target) ?? '').trim();
|
|
467
|
+
}
|
|
468
|
+
catch {
|
|
469
|
+
certPem = '';
|
|
470
|
+
}
|
|
471
|
+
if (!certPem) {
|
|
472
|
+
if (!opts.gateway.get()) {
|
|
473
|
+
try {
|
|
474
|
+
opts.gateway.set(await opts.gateway.resolve(target));
|
|
475
|
+
}
|
|
476
|
+
catch { /* best effort before cert fetch */ }
|
|
477
|
+
}
|
|
478
|
+
certPem = String(await opts.fetchPeerCert(target) ?? '').trim();
|
|
479
|
+
}
|
|
480
|
+
if (!certPem)
|
|
481
|
+
throw new NotFoundError(`certificate not found for aid: ${target}`);
|
|
482
|
+
return AID._create({
|
|
483
|
+
aid: target,
|
|
484
|
+
aunPath: opts.config().aunPath,
|
|
485
|
+
certPem,
|
|
486
|
+
privateKeyPem: null,
|
|
487
|
+
certValid: true,
|
|
488
|
+
privateKeyValid: false,
|
|
489
|
+
});
|
|
490
|
+
},
|
|
491
|
+
});
|
|
434
492
|
}
|
|
435
493
|
export class AUNClient {
|
|
436
494
|
/** 原始配置 */
|
|
@@ -466,7 +524,7 @@ export class AUNClient {
|
|
|
466
524
|
/** 认证流程 */
|
|
467
525
|
_auth;
|
|
468
526
|
/** 密钥存储 */
|
|
469
|
-
|
|
527
|
+
_tokenStore;
|
|
470
528
|
/** 会话参数(重连用) */
|
|
471
529
|
_sessionParams = null;
|
|
472
530
|
/** 会话选项 */
|
|
@@ -479,17 +537,7 @@ export class AUNClient {
|
|
|
479
537
|
_defaultConnectDeliveryMode;
|
|
480
538
|
/** peer 证书缓存 */
|
|
481
539
|
_certCache = new Map();
|
|
482
|
-
|
|
483
|
-
_agentMdPath = '';
|
|
484
|
-
_localAgentMdPath = '';
|
|
485
|
-
_localAgentMdEtag = '';
|
|
486
|
-
// gateway 在 RPC envelope._meta.agent_md_etag 注入的服务端 etag;纯观察,无下游依赖。
|
|
487
|
-
_remoteAgentMdEtag = '';
|
|
488
|
-
_agentMdCache = new Map();
|
|
489
|
-
_agentMdFetchInflight = new Map();
|
|
490
|
-
_agentMdDownloadInflight = new Map();
|
|
491
|
-
_agentMdDownloadActive = 0;
|
|
492
|
-
_agentMdDownloadWaiters = [];
|
|
540
|
+
_agentMdManager;
|
|
493
541
|
/** 消息序列号跟踪器(群消息 + P2P 空洞检测) */
|
|
494
542
|
_seqTracker = new SeqTracker();
|
|
495
543
|
_seqTrackerContext = null;
|
|
@@ -541,7 +589,6 @@ export class AUNClient {
|
|
|
541
589
|
_peerCache = new Map();
|
|
542
590
|
static V2_SIG_CACHE_TTL_MS = 60 * 60 * 1000;
|
|
543
591
|
static V2_SIG_CACHE_MAX = 16_384;
|
|
544
|
-
static AGENT_MD_DOWNLOAD_CONCURRENCY = 8;
|
|
545
592
|
_reconnectActive = false;
|
|
546
593
|
_reconnectAbort = null;
|
|
547
594
|
_serverKicked = false;
|
|
@@ -564,7 +611,6 @@ export class AUNClient {
|
|
|
564
611
|
}
|
|
565
612
|
this._configModel = configFromMap(rawConfig);
|
|
566
613
|
const initAid = (inputAid && inputAid.isPrivateKeyValid()) ? inputAid.aid : null;
|
|
567
|
-
this._agentMdPath = path.join(this._configModel.aunPath, 'AIDs');
|
|
568
614
|
this.config = {
|
|
569
615
|
aun_path: this._configModel.aunPath,
|
|
570
616
|
root_ca_path: this._configModel.rootCaPath,
|
|
@@ -588,29 +634,15 @@ export class AUNClient {
|
|
|
588
634
|
logger: this._clientLog,
|
|
589
635
|
});
|
|
590
636
|
this._discovery = new GatewayDiscovery({ verifySsl: this._configModel.verifySsl, logger: this._clientLog, net: dnsNet });
|
|
591
|
-
const
|
|
637
|
+
const tokenStore = new LocalTokenStore(this._configModel.aunPath, {
|
|
592
638
|
logger: this._logger.for('aun_core.keystore'),
|
|
593
|
-
secretStoreLogger: this._logger.for('aun_core.secret-store'),
|
|
594
639
|
});
|
|
595
|
-
this.
|
|
596
|
-
// 启动时被动清理 registerAid 留下的孤儿临时目录(>10 分钟)
|
|
597
|
-
try {
|
|
598
|
-
const cleanup = keystore.cleanupPendingDirs;
|
|
599
|
-
if (typeof cleanup === 'function') {
|
|
600
|
-
const removed = cleanup.call(keystore, 600_000);
|
|
601
|
-
if (removed > 0) {
|
|
602
|
-
this._clientLog.info(`_pending cleanup removed=${removed}`);
|
|
603
|
-
}
|
|
604
|
-
}
|
|
605
|
-
}
|
|
606
|
-
catch (err) {
|
|
607
|
-
this._clientLog.warn(`_pending cleanup failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
608
|
-
}
|
|
640
|
+
this._tokenStore = tokenStore;
|
|
609
641
|
this._slotId = inputAid?.slotId || 'default';
|
|
610
642
|
this._connectDeliveryMode = normalizeDeliveryModeConfig({ mode: 'fanout' });
|
|
611
643
|
this._defaultConnectDeliveryMode = { ...this._connectDeliveryMode };
|
|
612
644
|
this._auth = new AuthFlow({
|
|
613
|
-
|
|
645
|
+
tokenStore,
|
|
614
646
|
crypto: new CryptoProvider(),
|
|
615
647
|
aid: initAid,
|
|
616
648
|
deviceId: this._deviceId,
|
|
@@ -621,6 +653,24 @@ export class AUNClient {
|
|
|
621
653
|
net: dnsNet,
|
|
622
654
|
});
|
|
623
655
|
this._aid = initAid;
|
|
656
|
+
this._agentMdManager = createAgentMdManagerForRuntime({
|
|
657
|
+
config: () => this._configModel,
|
|
658
|
+
logger: () => this._logger.for('aun_core.agent_md'),
|
|
659
|
+
ownerAid: () => this._aid,
|
|
660
|
+
currentAid: () => this._currentAid,
|
|
661
|
+
gateway: {
|
|
662
|
+
resolve: (target) => this._resolveGatewayForAid(target),
|
|
663
|
+
get: () => this._gatewayUrl,
|
|
664
|
+
set: (gatewayUrl) => { this._gatewayUrl = gatewayUrl; },
|
|
665
|
+
},
|
|
666
|
+
identity: {
|
|
667
|
+
get: () => this._identity,
|
|
668
|
+
set: (identity) => { this._identity = identity; },
|
|
669
|
+
},
|
|
670
|
+
auth: () => this._auth,
|
|
671
|
+
tokenStore: () => this._tokenStore,
|
|
672
|
+
fetchPeerCert: (target) => this._fetchPeerCert(target),
|
|
673
|
+
});
|
|
624
674
|
this._transport = new RPCTransport({
|
|
625
675
|
eventDispatcher: this._dispatcher,
|
|
626
676
|
timeout: 10_000,
|
|
@@ -640,6 +690,7 @@ export class AUNClient {
|
|
|
640
690
|
public_key_der_b64: inputAid.publicKey,
|
|
641
691
|
cert: inputAid.certPem,
|
|
642
692
|
};
|
|
693
|
+
this._auth.setIdentity(this._identity);
|
|
643
694
|
this._state = 'standby';
|
|
644
695
|
}
|
|
645
696
|
}
|
|
@@ -729,21 +780,17 @@ export class AUNClient {
|
|
|
729
780
|
rawConfig.root_ca_path = aid.rootCaPath;
|
|
730
781
|
const nextConfig = configFromMap(rawConfig);
|
|
731
782
|
try {
|
|
732
|
-
const close = this.
|
|
783
|
+
const close = this._tokenStore.close;
|
|
733
784
|
if (typeof close === 'function')
|
|
734
|
-
close.call(this.
|
|
785
|
+
close.call(this._tokenStore);
|
|
735
786
|
}
|
|
736
787
|
catch {
|
|
737
|
-
// best-effort cleanup before switching
|
|
788
|
+
// best-effort cleanup before switching tokenStore roots
|
|
738
789
|
}
|
|
739
790
|
this._configModel = nextConfig;
|
|
740
791
|
this.config.aun_path = nextConfig.aunPath;
|
|
741
792
|
this.config.root_ca_path = nextConfig.rootCaPath;
|
|
742
793
|
this.config.seed_password = nextConfig.seedPassword;
|
|
743
|
-
this._agentMdPath = path.join(nextConfig.aunPath, 'AIDs');
|
|
744
|
-
this._agentMdCache.clear();
|
|
745
|
-
this._agentMdFetchInflight.clear();
|
|
746
|
-
this._agentMdDownloadInflight.clear();
|
|
747
794
|
this._peerCache.clear();
|
|
748
795
|
this._certCache.clear();
|
|
749
796
|
this._gatewayUrl = null;
|
|
@@ -758,13 +805,12 @@ export class AUNClient {
|
|
|
758
805
|
logger: this._clientLog,
|
|
759
806
|
});
|
|
760
807
|
this._discovery = new GatewayDiscovery({ verifySsl: nextConfig.verifySsl, logger: this._clientLog, net: dnsNet });
|
|
761
|
-
const
|
|
808
|
+
const tokenStore = new LocalTokenStore(nextConfig.aunPath, {
|
|
762
809
|
logger: this._logger.for('aun_core.keystore'),
|
|
763
|
-
secretStoreLogger: this._logger.for('aun_core.secret-store'),
|
|
764
810
|
});
|
|
765
|
-
this.
|
|
811
|
+
this._tokenStore = tokenStore;
|
|
766
812
|
this._auth = new AuthFlow({
|
|
767
|
-
|
|
813
|
+
tokenStore,
|
|
768
814
|
crypto: new CryptoProvider(),
|
|
769
815
|
aid: aid.aid,
|
|
770
816
|
deviceId: this._deviceId,
|
|
@@ -774,6 +820,24 @@ export class AUNClient {
|
|
|
774
820
|
logger: this._logger.for('aun_core.auth'),
|
|
775
821
|
net: dnsNet,
|
|
776
822
|
});
|
|
823
|
+
this._agentMdManager = createAgentMdManagerForRuntime({
|
|
824
|
+
config: () => this._configModel,
|
|
825
|
+
logger: () => this._logger.for('aun_core.agent_md'),
|
|
826
|
+
ownerAid: () => this._aid,
|
|
827
|
+
currentAid: () => this._currentAid,
|
|
828
|
+
gateway: {
|
|
829
|
+
resolve: (target) => this._resolveGatewayForAid(target),
|
|
830
|
+
get: () => this._gatewayUrl,
|
|
831
|
+
set: (gatewayUrl) => { this._gatewayUrl = gatewayUrl; },
|
|
832
|
+
},
|
|
833
|
+
identity: {
|
|
834
|
+
get: () => this._identity,
|
|
835
|
+
set: (identity) => { this._identity = identity; },
|
|
836
|
+
},
|
|
837
|
+
auth: () => this._auth,
|
|
838
|
+
tokenStore: () => this._tokenStore,
|
|
839
|
+
fetchPeerCert: (target) => this._fetchPeerCert(target),
|
|
840
|
+
});
|
|
777
841
|
this._transport = new RPCTransport({
|
|
778
842
|
eventDispatcher: this._dispatcher,
|
|
779
843
|
timeout: 10_000,
|
|
@@ -801,6 +865,8 @@ export class AUNClient {
|
|
|
801
865
|
public_key_der_b64: aid.publicKey,
|
|
802
866
|
cert: aid.certPem,
|
|
803
867
|
};
|
|
868
|
+
// 注入内存私钥到 AuthFlow,禁止 AuthFlow 内部再走 keystore 解密
|
|
869
|
+
this._auth.setIdentity(this._identity);
|
|
804
870
|
this._state = 'standby';
|
|
805
871
|
this._closing = false;
|
|
806
872
|
this._lastError = null;
|
|
@@ -858,772 +924,12 @@ export class AUNClient {
|
|
|
858
924
|
throw new StateError('peers requires a loaded identity');
|
|
859
925
|
return [...this._peerCache.entries()].sort(([a], [b]) => a.localeCompare(b)).map(([, v]) => v);
|
|
860
926
|
}
|
|
861
|
-
async
|
|
862
|
-
|
|
863
|
-
if (!target)
|
|
864
|
-
throw new ValidationError('agent.md requires non-empty aid');
|
|
865
|
-
let gatewayUrl = String(this._gatewayUrl ?? '').trim();
|
|
866
|
-
if (!gatewayUrl) {
|
|
867
|
-
try {
|
|
868
|
-
gatewayUrl = await this._resolveGatewayForAid(target);
|
|
869
|
-
}
|
|
870
|
-
catch {
|
|
871
|
-
gatewayUrl = '';
|
|
872
|
-
}
|
|
873
|
-
}
|
|
874
|
-
return `${agentMdHttpScheme(gatewayUrl)}://${agentMdAuthority(target, this._configModel.discoveryPort)}/agent.md`;
|
|
875
|
-
}
|
|
876
|
-
async _ensureAgentMdUploadToken(aid, gatewayUrl) {
|
|
877
|
-
let identity = this._auth.loadIdentityOrNone(aid);
|
|
878
|
-
if (!identity && this._identity && String(this._identity.aid ?? '') === aid) {
|
|
879
|
-
identity = this._identity;
|
|
880
|
-
}
|
|
881
|
-
if (!identity) {
|
|
882
|
-
throw new StateError('no local identity found, register or load an AID first');
|
|
883
|
-
}
|
|
884
|
-
const cachedToken = String(identity.access_token ?? '');
|
|
885
|
-
const expiresAt = this._auth.getAccessTokenExpiry(identity);
|
|
886
|
-
if (cachedToken && (expiresAt === null || expiresAt > Date.now() / 1000 + 30)) {
|
|
887
|
-
return cachedToken;
|
|
888
|
-
}
|
|
889
|
-
if (identity.refresh_token) {
|
|
890
|
-
try {
|
|
891
|
-
const refreshed = await this._auth.refreshCachedTokens(gatewayUrl, identity);
|
|
892
|
-
const refreshedToken = String(refreshed.access_token ?? '');
|
|
893
|
-
const refreshedExpiry = this._auth.getAccessTokenExpiry(refreshed);
|
|
894
|
-
if (refreshedToken && (refreshedExpiry === null || refreshedExpiry > Date.now() / 1000 + 30)) {
|
|
895
|
-
this._identity = refreshed;
|
|
896
|
-
return refreshedToken;
|
|
897
|
-
}
|
|
898
|
-
}
|
|
899
|
-
catch {
|
|
900
|
-
// refresh 失败时回退到完整 authenticate。
|
|
901
|
-
}
|
|
902
|
-
}
|
|
903
|
-
const result = await this._auth.authenticate(gatewayUrl, { aid });
|
|
904
|
-
const token = String(result.access_token ?? '');
|
|
905
|
-
if (!token)
|
|
906
|
-
throw new StateError('authenticate did not return access_token');
|
|
907
|
-
this._identity = this._auth.loadIdentityOrNone(aid) ?? {
|
|
908
|
-
...identity,
|
|
909
|
-
access_token: token,
|
|
910
|
-
refresh_token: String(result.refresh_token ?? identity.refresh_token ?? ''),
|
|
911
|
-
access_token_expires_at: typeof result.expires_at === 'number' ? result.expires_at : identity.access_token_expires_at,
|
|
912
|
-
token_exp: typeof result.expires_at === 'number' ? result.expires_at : identity.token_exp,
|
|
913
|
-
expires_at: typeof result.expires_at === 'number' ? result.expires_at : identity.expires_at,
|
|
914
|
-
};
|
|
915
|
-
return token;
|
|
916
|
-
}
|
|
917
|
-
async _uploadAgentMd(content) {
|
|
918
|
-
const target = String(this._aid ?? this._currentAid?.aid ?? '').trim();
|
|
919
|
-
if (!target)
|
|
920
|
-
throw new StateError('uploadAgentMd requires local AID');
|
|
921
|
-
const gatewayUrl = await this._resolveGatewayForAid(target);
|
|
922
|
-
this._gatewayUrl = gatewayUrl;
|
|
923
|
-
const token = await this._ensureAgentMdUploadToken(target, gatewayUrl);
|
|
924
|
-
const response = await fetchWithTimeout(await this._resolveAgentMdUrl(target), {
|
|
925
|
-
method: 'PUT',
|
|
926
|
-
headers: {
|
|
927
|
-
Authorization: `Bearer ${token}`,
|
|
928
|
-
'Content-Type': 'text/markdown; charset=utf-8',
|
|
929
|
-
},
|
|
930
|
-
body: content,
|
|
931
|
-
});
|
|
932
|
-
if (response.status === 404) {
|
|
933
|
-
throw new NotFoundError(`agent.md endpoint not found for aid: ${target}`);
|
|
934
|
-
}
|
|
935
|
-
if (!response.ok) {
|
|
936
|
-
const message = (await response.text()).trim();
|
|
937
|
-
throw new AUNError(`upload agent.md failed: HTTP ${response.status}${message ? ` - ${message}` : ''}`);
|
|
938
|
-
}
|
|
939
|
-
const payload = await response.json();
|
|
940
|
-
if (!isJsonObject(payload))
|
|
941
|
-
throw new AUNError('upload agent.md returned invalid JSON payload');
|
|
942
|
-
return payload;
|
|
943
|
-
}
|
|
944
|
-
async _acquireAgentMdDownloadSlot() {
|
|
945
|
-
if (this._agentMdDownloadActive < AUNClient.AGENT_MD_DOWNLOAD_CONCURRENCY) {
|
|
946
|
-
this._agentMdDownloadActive += 1;
|
|
947
|
-
return () => this._releaseAgentMdDownloadSlot();
|
|
948
|
-
}
|
|
949
|
-
await new Promise((resolve) => {
|
|
950
|
-
this._agentMdDownloadWaiters.push(resolve);
|
|
951
|
-
});
|
|
952
|
-
return () => this._releaseAgentMdDownloadSlot();
|
|
953
|
-
}
|
|
954
|
-
_releaseAgentMdDownloadSlot() {
|
|
955
|
-
const next = this._agentMdDownloadWaiters.shift();
|
|
956
|
-
if (next) {
|
|
957
|
-
next();
|
|
958
|
-
return;
|
|
959
|
-
}
|
|
960
|
-
if (this._agentMdDownloadActive > 0)
|
|
961
|
-
this._agentMdDownloadActive -= 1;
|
|
962
|
-
}
|
|
963
|
-
async _downloadAgentMd(aid) {
|
|
964
|
-
const target = String(aid ?? '').trim();
|
|
965
|
-
if (!target)
|
|
966
|
-
throw new ValidationError('downloadAgentMd requires non-empty aid');
|
|
967
|
-
const existing = this._agentMdDownloadInflight.get(target);
|
|
968
|
-
if (existing)
|
|
969
|
-
return await existing;
|
|
970
|
-
const task = (async () => {
|
|
971
|
-
const release = await this._acquireAgentMdDownloadSlot();
|
|
972
|
-
try {
|
|
973
|
-
return await this._downloadAgentMdOnce(target);
|
|
974
|
-
}
|
|
975
|
-
finally {
|
|
976
|
-
release();
|
|
977
|
-
}
|
|
978
|
-
})();
|
|
979
|
-
this._agentMdDownloadInflight.set(target, task);
|
|
980
|
-
task.finally(() => {
|
|
981
|
-
if (this._agentMdDownloadInflight.get(target) === task) {
|
|
982
|
-
this._agentMdDownloadInflight.delete(target);
|
|
983
|
-
}
|
|
984
|
-
}).catch(() => undefined);
|
|
985
|
-
return await task;
|
|
986
|
-
}
|
|
987
|
-
async _downloadAgentMdOnce(target) {
|
|
988
|
-
const cached = this._agentMdCache.get(target);
|
|
989
|
-
const url = await this._resolveAgentMdUrl(target);
|
|
990
|
-
let response = await fetchWithTimeout(url, {
|
|
991
|
-
method: 'GET',
|
|
992
|
-
headers: { Accept: 'text/markdown' },
|
|
993
|
-
redirect: 'follow',
|
|
994
|
-
});
|
|
995
|
-
if (response.status === 304 && typeof cached?.text === 'string') {
|
|
996
|
-
return String(cached.text);
|
|
997
|
-
}
|
|
998
|
-
if (response.status === 304) {
|
|
999
|
-
response = await fetchWithTimeout(url, {
|
|
1000
|
-
method: 'GET',
|
|
1001
|
-
headers: { Accept: 'text/markdown' },
|
|
1002
|
-
cache: 'reload',
|
|
1003
|
-
redirect: 'follow',
|
|
1004
|
-
});
|
|
1005
|
-
}
|
|
1006
|
-
if (response.status === 404) {
|
|
1007
|
-
throw new NotFoundError(`agent.md not found for aid: ${target}`);
|
|
1008
|
-
}
|
|
1009
|
-
if (!response.ok) {
|
|
1010
|
-
const message = (await response.text()).trim();
|
|
1011
|
-
throw new AUNError(`download agent.md failed: HTTP ${response.status}${message ? ` - ${message}` : ''}`);
|
|
1012
|
-
}
|
|
1013
|
-
const text = await response.text();
|
|
1014
|
-
const etag = String(response.headers?.get('ETag') ?? response.headers?.get('etag') ?? '').trim();
|
|
1015
|
-
const lastModified = String(response.headers?.get('Last-Modified') ?? response.headers?.get('last-modified') ?? '').trim();
|
|
1016
|
-
this._agentMdCache.set(target, {
|
|
1017
|
-
...(cached ?? {}),
|
|
1018
|
-
text,
|
|
1019
|
-
etag,
|
|
1020
|
-
lastModified,
|
|
1021
|
-
remote_etag: etag,
|
|
1022
|
-
last_modified: lastModified,
|
|
1023
|
-
});
|
|
1024
|
-
return text;
|
|
1025
|
-
}
|
|
1026
|
-
async _headAgentMd(aid) {
|
|
1027
|
-
const target = String(aid ?? '').trim();
|
|
1028
|
-
if (!target)
|
|
1029
|
-
throw new ValidationError('headAgentMd requires non-empty aid');
|
|
1030
|
-
const response = await fetchWithTimeout(await this._resolveAgentMdUrl(target), {
|
|
1031
|
-
method: 'HEAD',
|
|
1032
|
-
headers: { Accept: 'text/markdown' },
|
|
1033
|
-
redirect: 'follow',
|
|
1034
|
-
}, 15_000);
|
|
1035
|
-
const cached = this._agentMdCache.get(target) ?? {};
|
|
1036
|
-
const etag = String(response.headers?.get('ETag') ?? response.headers?.get('etag') ?? '').trim();
|
|
1037
|
-
const lastModified = String(response.headers?.get('Last-Modified') ?? response.headers?.get('last-modified') ?? '').trim();
|
|
1038
|
-
if (response.status === 404) {
|
|
1039
|
-
return { aid: target, found: false, etag: '', last_modified: '', status: 404 };
|
|
1040
|
-
}
|
|
1041
|
-
const resultEtag = response.status === 304 ? (etag || String(cached.etag ?? cached.remote_etag ?? '')) : etag;
|
|
1042
|
-
const resultLastModified = response.status === 304 ? (lastModified || String(cached.lastModified ?? cached.last_modified ?? '')) : lastModified;
|
|
1043
|
-
if (response.status < 200 || (response.status >= 300 && response.status !== 304)) {
|
|
1044
|
-
throw new AUNError(`head agent.md failed: HTTP ${response.status}`);
|
|
1045
|
-
}
|
|
1046
|
-
this._agentMdCache.set(target, {
|
|
1047
|
-
...cached,
|
|
1048
|
-
etag: resultEtag,
|
|
1049
|
-
lastModified: resultLastModified,
|
|
1050
|
-
remote_etag: resultEtag,
|
|
1051
|
-
last_modified: resultLastModified,
|
|
1052
|
-
});
|
|
1053
|
-
return { aid: target, found: true, etag: resultEtag, last_modified: resultLastModified, status: response.status };
|
|
1054
|
-
}
|
|
1055
|
-
async _verifyAgentMd(content, aid, certPem) {
|
|
1056
|
-
const target = String(aid ?? '').trim();
|
|
1057
|
-
if (!target)
|
|
1058
|
-
throw new ValidationError('verifyAgentMd requires non-empty aid');
|
|
1059
|
-
let peer = target === this._currentAid?.aid ? this._currentAid : null;
|
|
1060
|
-
if (!peer) {
|
|
1061
|
-
let resolvedCert = String(certPem ?? '').trim();
|
|
1062
|
-
if (!resolvedCert) {
|
|
1063
|
-
try {
|
|
1064
|
-
resolvedCert = String(this._keystore.loadCert(target) ?? '').trim();
|
|
1065
|
-
}
|
|
1066
|
-
catch {
|
|
1067
|
-
resolvedCert = '';
|
|
1068
|
-
}
|
|
1069
|
-
}
|
|
1070
|
-
if (!resolvedCert) {
|
|
1071
|
-
if (!this._gatewayUrl) {
|
|
1072
|
-
try {
|
|
1073
|
-
this._gatewayUrl = await this._resolveGatewayForAid(target);
|
|
1074
|
-
}
|
|
1075
|
-
catch { /* best-effort before cert fetch */ }
|
|
1076
|
-
}
|
|
1077
|
-
resolvedCert = String(await this._fetchPeerCert(target) ?? '').trim();
|
|
1078
|
-
}
|
|
1079
|
-
if (!resolvedCert)
|
|
1080
|
-
throw new NotFoundError(`certificate not found for aid: ${target}`);
|
|
1081
|
-
peer = AID._create({
|
|
1082
|
-
aid: target,
|
|
1083
|
-
aunPath: this._configModel.aunPath,
|
|
1084
|
-
certPem: resolvedCert,
|
|
1085
|
-
privateKeyPem: null,
|
|
1086
|
-
certValid: true,
|
|
1087
|
-
privateKeyValid: false,
|
|
1088
|
-
});
|
|
1089
|
-
}
|
|
1090
|
-
const result = peer.verifyAgentMd(content);
|
|
1091
|
-
if (!result.ok)
|
|
1092
|
-
throw new AUNError(result.error.message);
|
|
1093
|
-
const vd = result.data;
|
|
1094
|
-
return { ...vd, verified: vd.status === 'verified' };
|
|
1095
|
-
}
|
|
1096
|
-
/**
|
|
1097
|
-
* 读取 {agentMdPath}/{self_aid}/agent.md,签名后上传,并把签名结果原子写回本地。
|
|
1098
|
-
*/
|
|
1099
|
-
async publishAgentMd() {
|
|
1100
|
-
const target = this._agentMdOwnerAid();
|
|
1101
|
-
if (!target || !this._currentAid) {
|
|
1102
|
-
throw new ValidationError('publishAgentMd requires local AID');
|
|
1103
|
-
}
|
|
1104
|
-
const content = this._readAgentMdContent(target);
|
|
1105
|
-
const signed = this._currentAid?.signAgentMd(content);
|
|
1106
|
-
if (!signed?.ok) {
|
|
1107
|
-
throw new StateError(signed?.error.message ?? 'publishAgentMd requires a valid local AID private key');
|
|
1108
|
-
}
|
|
1109
|
-
const signedContent = signed.data.signed;
|
|
1110
|
-
const result = await this._uploadAgentMd(signedContent);
|
|
1111
|
-
this._localAgentMdEtag = this._agentMdContentEtag(signedContent);
|
|
1112
|
-
const remoteEtag = isJsonObject(result) ? String(result.etag ?? '').trim() : '';
|
|
1113
|
-
if (remoteEtag)
|
|
1114
|
-
this._remoteAgentMdEtag = remoteEtag;
|
|
1115
|
-
this._saveAgentMdRecord(target, {
|
|
1116
|
-
content: signedContent,
|
|
1117
|
-
local_etag: this._localAgentMdEtag,
|
|
1118
|
-
remote_etag: remoteEtag || undefined,
|
|
1119
|
-
last_modified: isJsonObject(result) ? String(result.last_modified ?? '').trim() : '',
|
|
1120
|
-
fetched_at: Date.now(),
|
|
1121
|
-
remote_status: remoteEtag ? 'found' : 'unknown',
|
|
1122
|
-
last_error: '',
|
|
1123
|
-
});
|
|
1124
|
-
return result;
|
|
1125
|
-
}
|
|
1126
|
-
async _startAgentMdFetchTask(target) {
|
|
1127
|
-
const existing = this._agentMdFetchInflight.get(target);
|
|
1128
|
-
if (existing) {
|
|
1129
|
-
return await existing;
|
|
1130
|
-
}
|
|
1131
|
-
const task = this._fetchAgentMdOnce(target);
|
|
1132
|
-
this._agentMdFetchInflight.set(target, task);
|
|
1133
|
-
task.finally(() => {
|
|
1134
|
-
if (this._agentMdFetchInflight.get(target) === task) {
|
|
1135
|
-
this._agentMdFetchInflight.delete(target);
|
|
1136
|
-
}
|
|
1137
|
-
}).catch(() => undefined);
|
|
1138
|
-
return await task;
|
|
1139
|
-
}
|
|
1140
|
-
async _fetchAgentMdOnce(target) {
|
|
1141
|
-
const content = await this._downloadAgentMd(target);
|
|
1142
|
-
const signature = await this._verifyAgentMd(content, target);
|
|
1143
|
-
const isSelf = target === (this._aid ?? '');
|
|
1144
|
-
const localEtag = this._agentMdContentEtag(content);
|
|
1145
|
-
const cacheMeta = this._agentMdAuthCacheMeta(target);
|
|
1146
|
-
const remoteEtag = String(cacheMeta.etag ?? '').trim();
|
|
1147
|
-
const lastModified = String(cacheMeta.lastModified ?? cacheMeta.last_modified ?? '').trim();
|
|
1148
|
-
if (isSelf) {
|
|
1149
|
-
this._localAgentMdEtag = localEtag;
|
|
1150
|
-
if (remoteEtag)
|
|
1151
|
-
this._remoteAgentMdEtag = remoteEtag;
|
|
1152
|
-
}
|
|
1153
|
-
const saved = this._saveAgentMdRecord(target, {
|
|
1154
|
-
content,
|
|
1155
|
-
local_etag: localEtag,
|
|
1156
|
-
remote_etag: remoteEtag || undefined,
|
|
1157
|
-
last_modified: lastModified || undefined,
|
|
1158
|
-
fetched_at: Date.now(),
|
|
1159
|
-
remote_status: 'found',
|
|
1160
|
-
verify_status: isJsonObject(signature) ? String(signature.status ?? '') : '',
|
|
1161
|
-
verify_error: isJsonObject(signature) ? String(signature.reason ?? '') : '',
|
|
1162
|
-
last_error: '',
|
|
1163
|
-
});
|
|
1164
|
-
let inSync = null;
|
|
1165
|
-
if (isSelf) {
|
|
1166
|
-
const remote = remoteEtag || this._remoteAgentMdEtag || '';
|
|
1167
|
-
inSync = localEtag && remote ? localEtag === remote : false;
|
|
1168
|
-
}
|
|
1169
|
-
return {
|
|
1170
|
-
aid: target,
|
|
1171
|
-
content,
|
|
1172
|
-
signature: signature,
|
|
1173
|
-
in_sync: inSync,
|
|
1174
|
-
saved_to: String(saved.saved_to ?? this._agentMdFilePath(target)),
|
|
1175
|
-
save_error: null,
|
|
1176
|
-
};
|
|
1177
|
-
}
|
|
1178
|
-
/**
|
|
1179
|
-
* 设置 agent.md 本地存储根目录;为空时恢复默认 {aun_path}/AIDs。
|
|
1180
|
-
*/
|
|
1181
|
-
_setAgentMdRoot(root) {
|
|
1182
|
-
const raw = String(root ?? '').trim();
|
|
1183
|
-
const next = raw || path.join(this._configModel.aunPath, 'AIDs');
|
|
1184
|
-
fs.mkdirSync(next, { recursive: true });
|
|
1185
|
-
this._agentMdPath = next;
|
|
1186
|
-
this._agentMdCache.clear();
|
|
1187
|
-
return this._agentMdPath;
|
|
1188
|
-
}
|
|
1189
|
-
/** 返回本地 agent.md 文件的 etag;未设置或读取失败时返回空串。 */
|
|
1190
|
-
getLocalAgentMdEtag() {
|
|
1191
|
-
return this._localAgentMdEtag;
|
|
1192
|
-
}
|
|
1193
|
-
/**
|
|
1194
|
-
* 返回 gateway 在最近一次 RPC envelope._meta 注入的服务端 agent.md etag。
|
|
1195
|
-
*
|
|
1196
|
-
* 未收到过则为空串;不阻塞调用,纯内存读。
|
|
1197
|
-
*/
|
|
1198
|
-
getRemoteAgentMdEtag() {
|
|
1199
|
-
return this._remoteAgentMdEtag;
|
|
1200
|
-
}
|
|
1201
|
-
_agentMdContentEtag(content) {
|
|
1202
|
-
return `"${crypto.createHash('sha256').update(String(content ?? ''), 'utf-8').digest('hex')}"`;
|
|
1203
|
-
}
|
|
1204
|
-
_agentMdOwnerAid() {
|
|
1205
|
-
return String(this._aid ?? '').trim();
|
|
1206
|
-
}
|
|
1207
|
-
_agentMdSafeAid(aid) {
|
|
1208
|
-
const target = String(aid ?? '').trim();
|
|
1209
|
-
if (!target || target.includes('/') || target.includes('\\') || target.includes('\0')) {
|
|
1210
|
-
throw new ValidationError('agent.md aid is empty or contains path separators');
|
|
1211
|
-
}
|
|
1212
|
-
return target;
|
|
1213
|
-
}
|
|
1214
|
-
_agentMdRoot() {
|
|
1215
|
-
const root = this._agentMdPath || path.join(this._configModel.aunPath, 'AIDs');
|
|
1216
|
-
fs.mkdirSync(root, { recursive: true });
|
|
1217
|
-
return root;
|
|
1218
|
-
}
|
|
1219
|
-
_agentMdFilePath(aid) {
|
|
1220
|
-
return path.join(this._agentMdRoot(), this._agentMdSafeAid(aid), 'agent.md');
|
|
1221
|
-
}
|
|
1222
|
-
_agentMdMetaPath(aid) {
|
|
1223
|
-
return path.join(this._agentMdRoot(), this._agentMdSafeAid(aid), 'agentmd.json');
|
|
1224
|
-
}
|
|
1225
|
-
_atomicWriteText(filePath, content) {
|
|
1226
|
-
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
|
1227
|
-
const tmp = path.join(path.dirname(filePath), `.${path.basename(filePath)}.${process.pid}.${crypto.randomUUID()}.tmp`);
|
|
1228
|
-
let fd = null;
|
|
1229
|
-
try {
|
|
1230
|
-
fd = fs.openSync(tmp, 'w');
|
|
1231
|
-
fs.writeFileSync(fd, content, 'utf-8');
|
|
1232
|
-
fs.fsyncSync(fd);
|
|
1233
|
-
fs.closeSync(fd);
|
|
1234
|
-
fd = null;
|
|
1235
|
-
fs.renameSync(tmp, filePath);
|
|
1236
|
-
try {
|
|
1237
|
-
const dirFd = fs.openSync(path.dirname(filePath), 'r');
|
|
1238
|
-
try {
|
|
1239
|
-
fs.fsyncSync(dirFd);
|
|
1240
|
-
}
|
|
1241
|
-
finally {
|
|
1242
|
-
fs.closeSync(dirFd);
|
|
1243
|
-
}
|
|
1244
|
-
}
|
|
1245
|
-
catch { /* best effort */ }
|
|
1246
|
-
}
|
|
1247
|
-
finally {
|
|
1248
|
-
if (fd !== null) {
|
|
1249
|
-
try {
|
|
1250
|
-
fs.closeSync(fd);
|
|
1251
|
-
}
|
|
1252
|
-
catch { /* ignore */ }
|
|
1253
|
-
}
|
|
1254
|
-
if (fs.existsSync(tmp)) {
|
|
1255
|
-
try {
|
|
1256
|
-
fs.unlinkSync(tmp);
|
|
1257
|
-
}
|
|
1258
|
-
catch { /* ignore */ }
|
|
1259
|
-
}
|
|
1260
|
-
}
|
|
1261
|
-
}
|
|
1262
|
-
_sleepSync(ms) {
|
|
1263
|
-
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms);
|
|
1264
|
-
}
|
|
1265
|
-
_withAgentMdRecordLock(aid, fn) {
|
|
1266
|
-
const lockPath = path.join(path.dirname(this._agentMdMetaPath(aid)), 'agentmd.json.lock');
|
|
1267
|
-
fs.mkdirSync(path.dirname(lockPath), { recursive: true });
|
|
1268
|
-
const deadline = Date.now() + 5000;
|
|
1269
|
-
let fd = null;
|
|
1270
|
-
while (fd === null) {
|
|
1271
|
-
try {
|
|
1272
|
-
fd = fs.openSync(lockPath, 'wx');
|
|
1273
|
-
fs.writeFileSync(fd, `${process.pid}\n`, 'utf-8');
|
|
1274
|
-
}
|
|
1275
|
-
catch (err) {
|
|
1276
|
-
if (err?.code !== 'EEXIST' || Date.now() >= deadline)
|
|
1277
|
-
throw err;
|
|
1278
|
-
try {
|
|
1279
|
-
const st = fs.statSync(lockPath);
|
|
1280
|
-
if (Date.now() - st.mtimeMs > 30000)
|
|
1281
|
-
fs.unlinkSync(lockPath);
|
|
1282
|
-
}
|
|
1283
|
-
catch { /* ignore */ }
|
|
1284
|
-
this._sleepSync(25);
|
|
1285
|
-
}
|
|
1286
|
-
}
|
|
1287
|
-
try {
|
|
1288
|
-
return fn();
|
|
1289
|
-
}
|
|
1290
|
-
finally {
|
|
1291
|
-
if (fd !== null) {
|
|
1292
|
-
try {
|
|
1293
|
-
fs.closeSync(fd);
|
|
1294
|
-
}
|
|
1295
|
-
catch { /* ignore */ }
|
|
1296
|
-
}
|
|
1297
|
-
try {
|
|
1298
|
-
fs.unlinkSync(lockPath);
|
|
1299
|
-
}
|
|
1300
|
-
catch { /* ignore */ }
|
|
1301
|
-
}
|
|
1302
|
-
}
|
|
1303
|
-
_writeAgentMdRecordUnlocked(aid, record) {
|
|
1304
|
-
const payload = {};
|
|
1305
|
-
for (const [key, value] of Object.entries(record)) {
|
|
1306
|
-
if (key !== 'content' && value !== undefined && value !== null)
|
|
1307
|
-
payload[key] = value;
|
|
1308
|
-
}
|
|
1309
|
-
payload.aid = this._agentMdSafeAid(aid);
|
|
1310
|
-
this._atomicWriteText(this._agentMdMetaPath(aid), `${JSON.stringify(payload, null, 2)}\n`);
|
|
1311
|
-
}
|
|
1312
|
-
_normalizeAgentMdRecord(aid, data) {
|
|
1313
|
-
if (!isJsonObject(data))
|
|
1314
|
-
return {};
|
|
1315
|
-
const record = {};
|
|
1316
|
-
for (const [key, value] of Object.entries(data)) {
|
|
1317
|
-
if (key !== 'content')
|
|
1318
|
-
record[key] = value;
|
|
1319
|
-
}
|
|
1320
|
-
record.aid = this._agentMdSafeAid(String(record.aid ?? aid));
|
|
1321
|
-
for (const key of ['fetched_at', 'observed_at', 'checked_at', 'updated_at']) {
|
|
1322
|
-
record[key] = Number(record[key] ?? 0) || 0;
|
|
1323
|
-
}
|
|
1324
|
-
return record;
|
|
1325
|
-
}
|
|
1326
|
-
_readAgentMdRecordUnlocked(aid) {
|
|
1327
|
-
const filePath = this._agentMdMetaPath(aid);
|
|
1328
|
-
if (!fs.existsSync(filePath))
|
|
1329
|
-
return {};
|
|
1330
|
-
try {
|
|
1331
|
-
return this._normalizeAgentMdRecord(aid, JSON.parse(fs.readFileSync(filePath, 'utf-8')));
|
|
1332
|
-
}
|
|
1333
|
-
catch (err) {
|
|
1334
|
-
this._clientLog.warn(`agent.md metadata damaged, ignoring: aid=${aid} err=${err instanceof Error ? err.message : String(err)}`);
|
|
1335
|
-
return {};
|
|
1336
|
-
}
|
|
1337
|
-
}
|
|
1338
|
-
_readAgentMdContent(aid) {
|
|
1339
|
-
return fs.readFileSync(this._agentMdFilePath(aid), 'utf-8');
|
|
1340
|
-
}
|
|
1341
|
-
_writeAgentMdContent(aid, content) {
|
|
1342
|
-
const filePath = this._agentMdFilePath(aid);
|
|
1343
|
-
this._atomicWriteText(filePath, String(content ?? ''));
|
|
1344
|
-
return filePath;
|
|
1345
|
-
}
|
|
1346
|
-
_agentMdAuthCacheMeta(aid) {
|
|
1347
|
-
try {
|
|
1348
|
-
const record = this._agentMdCache.get(String(aid ?? '').trim());
|
|
1349
|
-
return record && typeof record === 'object' ? { ...record } : {};
|
|
1350
|
-
}
|
|
1351
|
-
catch {
|
|
1352
|
-
return {};
|
|
1353
|
-
}
|
|
1354
|
-
}
|
|
1355
|
-
_loadAgentMdRecord(aid) {
|
|
1356
|
-
const target = String(aid ?? '').trim();
|
|
1357
|
-
if (!target)
|
|
1358
|
-
return null;
|
|
1359
|
-
try {
|
|
1360
|
-
const loaded = this._withAgentMdRecordLock(target, () => {
|
|
1361
|
-
const record = this._readAgentMdRecordUnlocked(target);
|
|
1362
|
-
const next = Object.keys(record).length > 0 ? { ...record, aid: target } : { aid: target };
|
|
1363
|
-
try {
|
|
1364
|
-
const content = this._readAgentMdContent(target);
|
|
1365
|
-
next.content = content;
|
|
1366
|
-
next.local_etag = this._agentMdContentEtag(content);
|
|
1367
|
-
}
|
|
1368
|
-
catch (err) {
|
|
1369
|
-
if (fs.existsSync(this._agentMdMetaPath(target))) {
|
|
1370
|
-
this._clientLog.warn(`agent.md content read failed: aid=${target} err=${err instanceof Error ? err.message : String(err)}`);
|
|
1371
|
-
}
|
|
1372
|
-
}
|
|
1373
|
-
return next;
|
|
1374
|
-
});
|
|
1375
|
-
if (Object.keys(loaded).length <= 1)
|
|
1376
|
-
return null;
|
|
1377
|
-
this._agentMdCache.set(target, { ...loaded });
|
|
1378
|
-
return { ...loaded };
|
|
1379
|
-
}
|
|
1380
|
-
catch (err) {
|
|
1381
|
-
this._clientLog.debug(`agent.md cache load skipped: aid=${target} err=${err instanceof Error ? err.message : String(err)}`);
|
|
1382
|
-
}
|
|
1383
|
-
return null;
|
|
1384
|
-
}
|
|
1385
|
-
_saveAgentMdRecord(aid, fields) {
|
|
1386
|
-
const target = String(aid ?? '').trim();
|
|
1387
|
-
if (!target)
|
|
1388
|
-
return {};
|
|
1389
|
-
try {
|
|
1390
|
-
const inputFields = { ...fields };
|
|
1391
|
-
const hasContent = Object.prototype.hasOwnProperty.call(inputFields, 'content') && inputFields.content !== undefined && inputFields.content !== null;
|
|
1392
|
-
let savedTo = '';
|
|
1393
|
-
const record = this._withAgentMdRecordLock(target, () => {
|
|
1394
|
-
if (hasContent) {
|
|
1395
|
-
const content = String(inputFields.content ?? '');
|
|
1396
|
-
savedTo = this._writeAgentMdContent(target, content);
|
|
1397
|
-
if (!inputFields.local_etag)
|
|
1398
|
-
inputFields.local_etag = this._agentMdContentEtag(content);
|
|
1399
|
-
if (!inputFields.fetched_at)
|
|
1400
|
-
inputFields.fetched_at = Date.now();
|
|
1401
|
-
}
|
|
1402
|
-
delete inputFields.content;
|
|
1403
|
-
const next = { ...this._readAgentMdRecordUnlocked(target), aid: target };
|
|
1404
|
-
for (const [key, value] of Object.entries(inputFields)) {
|
|
1405
|
-
if (value !== undefined && value !== null)
|
|
1406
|
-
next[key] = value;
|
|
1407
|
-
}
|
|
1408
|
-
next.updated_at = Date.now();
|
|
1409
|
-
this._writeAgentMdRecordUnlocked(target, next);
|
|
1410
|
-
return next;
|
|
1411
|
-
});
|
|
1412
|
-
const loaded = { ...record };
|
|
1413
|
-
if (hasContent) {
|
|
1414
|
-
loaded.content = String(fields.content ?? '');
|
|
1415
|
-
if (savedTo)
|
|
1416
|
-
loaded.saved_to = savedTo;
|
|
1417
|
-
}
|
|
1418
|
-
this._agentMdCache.set(target, { ...loaded });
|
|
1419
|
-
const owner = this._agentMdOwnerAid();
|
|
1420
|
-
if (target === owner) {
|
|
1421
|
-
const localEtag = String(loaded.local_etag ?? '').trim();
|
|
1422
|
-
const remoteEtag = String(loaded.remote_etag ?? '').trim();
|
|
1423
|
-
if (localEtag)
|
|
1424
|
-
this._localAgentMdEtag = localEtag;
|
|
1425
|
-
if (remoteEtag)
|
|
1426
|
-
this._remoteAgentMdEtag = remoteEtag;
|
|
1427
|
-
}
|
|
1428
|
-
return { ...loaded };
|
|
1429
|
-
}
|
|
1430
|
-
catch (err) {
|
|
1431
|
-
this._clientLog.debug(`agent.md cache save skipped: aid=${target} err=${err instanceof Error ? err.message : String(err)}`);
|
|
1432
|
-
}
|
|
1433
|
-
return {};
|
|
1434
|
-
}
|
|
1435
|
-
_agentMdHasLocalContent(aid, record) {
|
|
1436
|
-
if (record && typeof record.content === 'string' && record.content.length > 0)
|
|
1437
|
-
return true;
|
|
1438
|
-
try {
|
|
1439
|
-
return fs.existsSync(this._agentMdFilePath(aid));
|
|
1440
|
-
}
|
|
1441
|
-
catch {
|
|
1442
|
-
return false;
|
|
1443
|
-
}
|
|
1444
|
-
}
|
|
1445
|
-
_agentMdCheckedAtFresh(checkedAtMs, maxUnsyncedDays) {
|
|
1446
|
-
const days = Number(maxUnsyncedDays || 0);
|
|
1447
|
-
if (!Number.isFinite(days) || days <= 0)
|
|
1448
|
-
return false;
|
|
1449
|
-
if (!Number.isFinite(checkedAtMs) || checkedAtMs <= 0)
|
|
1450
|
-
return false;
|
|
1451
|
-
return Date.now() - checkedAtMs <= days * 86400000;
|
|
1452
|
-
}
|
|
1453
|
-
_agentMdLastModifiedFresh(lastModified, maxUnsyncedDays) {
|
|
1454
|
-
const days = Number(maxUnsyncedDays || 0);
|
|
1455
|
-
if (!Number.isFinite(days) || days <= 0)
|
|
1456
|
-
return false;
|
|
1457
|
-
const ts = Date.parse(String(lastModified ?? '').trim());
|
|
1458
|
-
if (!Number.isFinite(ts))
|
|
1459
|
-
return false;
|
|
1460
|
-
return Date.now() <= ts + days * 86400000;
|
|
1461
|
-
}
|
|
1462
|
-
_scheduleAgentMdFetchIfMissing(aid, record, source = '') {
|
|
1463
|
-
const target = String(aid ?? '').trim();
|
|
1464
|
-
if (!target || this._agentMdHasLocalContent(target, record))
|
|
1465
|
-
return;
|
|
1466
|
-
if (this._agentMdFetchInflight.has(target))
|
|
1467
|
-
return;
|
|
1468
|
-
void this._startAgentMdFetchTask(target).catch((err) => {
|
|
1469
|
-
this._saveAgentMdRecord(target, {
|
|
1470
|
-
last_error: err instanceof Error ? err.message : String(err),
|
|
1471
|
-
remote_status: 'found',
|
|
1472
|
-
});
|
|
1473
|
-
this._clientLog.debug(`agent.md auto fetch failed: aid=${target} source=${source || '-'} err=${err instanceof Error ? err.message : String(err)}`);
|
|
1474
|
-
});
|
|
1475
|
-
}
|
|
1476
|
-
_observeAgentMdMeta(aid, etag = '', lastModified = '', source = '') {
|
|
1477
|
-
const target = String(aid ?? '').trim();
|
|
1478
|
-
const remoteEtag = String(etag ?? '').trim();
|
|
1479
|
-
const remoteLastModified = String(lastModified ?? '').trim();
|
|
1480
|
-
if (!target || (!remoteEtag && !remoteLastModified))
|
|
1481
|
-
return;
|
|
1482
|
-
let before = this._agentMdCache.get(target);
|
|
1483
|
-
if (!before || typeof before !== 'object')
|
|
1484
|
-
before = this._loadAgentMdRecord(target) ?? {};
|
|
1485
|
-
const same = (!remoteEtag || String(before.remote_etag ?? '').trim() === remoteEtag) &&
|
|
1486
|
-
(!remoteLastModified || String(before.last_modified ?? '').trim() === remoteLastModified);
|
|
1487
|
-
let record = { ...before };
|
|
1488
|
-
if (!same || Object.keys(before).length === 0) {
|
|
1489
|
-
const fields = {
|
|
1490
|
-
observed_at: Date.now(),
|
|
1491
|
-
remote_status: 'found',
|
|
1492
|
-
};
|
|
1493
|
-
if (remoteEtag)
|
|
1494
|
-
fields.remote_etag = remoteEtag;
|
|
1495
|
-
if (remoteLastModified)
|
|
1496
|
-
fields.last_modified = remoteLastModified;
|
|
1497
|
-
record = this._saveAgentMdRecord(target, fields) || record;
|
|
1498
|
-
}
|
|
1499
|
-
if (target === this._agentMdOwnerAid() && remoteEtag)
|
|
1500
|
-
this._remoteAgentMdEtag = remoteEtag;
|
|
1501
|
-
this._scheduleAgentMdFetchIfMissing(target, record, source);
|
|
1502
|
-
this._clientLog.debug(`agent.md meta observed: aid=${target} etag=${remoteEtag || '-'} last_modified=${remoteLastModified || '-'} source=${source || '-'}`);
|
|
1503
|
-
}
|
|
1504
|
-
_observeAgentMdEtag(aid, etag, source = '') {
|
|
1505
|
-
this._observeAgentMdMeta(aid, etag, '', source);
|
|
1506
|
-
}
|
|
1507
|
-
_observeAgentMdFromEnvelope(envelope) {
|
|
1508
|
-
if (!isJsonObject(envelope))
|
|
1509
|
-
return;
|
|
1510
|
-
const env = envelope;
|
|
1511
|
-
if (!isJsonObject(env.agent_md))
|
|
1512
|
-
return;
|
|
1513
|
-
const agentMd = env.agent_md;
|
|
1514
|
-
if (!isJsonObject(agentMd.sender))
|
|
1515
|
-
return;
|
|
1516
|
-
const sender = agentMd.sender;
|
|
1517
|
-
let senderAid = String(sender.aid ?? '').trim();
|
|
1518
|
-
if (!senderAid) {
|
|
1519
|
-
const aad = isJsonObject(env.aad) ? env.aad : {};
|
|
1520
|
-
senderAid = String(aad.from ?? env.from ?? '').trim();
|
|
1521
|
-
}
|
|
1522
|
-
this._observeAgentMdMeta(senderAid, String(sender.etag ?? '').trim(), String(sender.last_modified ?? sender.lastModified ?? '').trim(), 'envelope');
|
|
1523
|
-
}
|
|
1524
|
-
async _checkAgentMdCache(aid, maxUnsyncedDays = 1) {
|
|
1525
|
-
const target = String(aid ?? this._aid ?? '').trim();
|
|
1526
|
-
if (!target)
|
|
1527
|
-
throw new ValidationError('checkAgentMd requires aid (or local AID)');
|
|
1528
|
-
const before = this._loadAgentMdRecord(target) ?? {};
|
|
1529
|
-
const localEtag = String(before.local_etag ?? '').trim();
|
|
1530
|
-
const localFound = !!(Object.keys(before).length > 0 && (String(before.content ?? '') || localEtag));
|
|
1531
|
-
const remoteEtagCached = String(before.remote_etag ?? '').trim();
|
|
1532
|
-
const lastModifiedCached = String(before.last_modified ?? '').trim();
|
|
1533
|
-
const checkedAt = Number(before.checked_at ?? 0);
|
|
1534
|
-
const fetchedAt = Number(before.fetched_at ?? 0);
|
|
1535
|
-
const checkedAtCached = checkedAt > 0 ? checkedAt : fetchedAt;
|
|
1536
|
-
const cachedInSync = !!(localFound && localEtag && remoteEtagCached && localEtag === remoteEtagCached);
|
|
1537
|
-
// max_unsynced_days > 0 且距上次 HEAD 在窗口内 → 直接返回缓存;否则强制 HEAD。
|
|
1538
|
-
if (cachedInSync && this._agentMdCheckedAtFresh(checkedAtCached, maxUnsyncedDays)) {
|
|
1539
|
-
return {
|
|
1540
|
-
aid: target,
|
|
1541
|
-
local_found: true,
|
|
1542
|
-
remote_found: true,
|
|
1543
|
-
local_etag: localEtag,
|
|
1544
|
-
remote_etag: remoteEtagCached,
|
|
1545
|
-
in_sync: true,
|
|
1546
|
-
last_modified: lastModifiedCached,
|
|
1547
|
-
status: 200,
|
|
1548
|
-
cached: true,
|
|
1549
|
-
verify_status: String(before.verify_status ?? ''),
|
|
1550
|
-
verify_error: String(before.verify_error ?? ''),
|
|
1551
|
-
};
|
|
1552
|
-
}
|
|
1553
|
-
const remoteFoundCached = !!(remoteEtagCached || String(before.remote_status ?? '') === 'found');
|
|
1554
|
-
if (!localFound &&
|
|
1555
|
-
!remoteFoundCached &&
|
|
1556
|
-
String(before.remote_status ?? '') === 'missing' &&
|
|
1557
|
-
this._agentMdCheckedAtFresh(checkedAtCached, maxUnsyncedDays)) {
|
|
1558
|
-
return {
|
|
1559
|
-
aid: target,
|
|
1560
|
-
local_found: false,
|
|
1561
|
-
remote_found: false,
|
|
1562
|
-
local_etag: '',
|
|
1563
|
-
remote_etag: '',
|
|
1564
|
-
in_sync: false,
|
|
1565
|
-
last_modified: '',
|
|
1566
|
-
status: 404,
|
|
1567
|
-
cached: true,
|
|
1568
|
-
verify_status: '',
|
|
1569
|
-
verify_error: '',
|
|
1570
|
-
};
|
|
1571
|
-
}
|
|
1572
|
-
const now = Date.now();
|
|
1573
|
-
let remote;
|
|
1574
|
-
try {
|
|
1575
|
-
remote = await this._headAgentMd(target);
|
|
1576
|
-
}
|
|
1577
|
-
catch (err) {
|
|
1578
|
-
this._saveAgentMdRecord(target, { checked_at: now, remote_status: 'error', last_error: err instanceof Error ? err.message : String(err) });
|
|
1579
|
-
throw err;
|
|
1580
|
-
}
|
|
1581
|
-
const remoteFound = !!remote.found;
|
|
1582
|
-
const remoteEtag = String(remote.etag ?? '').trim();
|
|
1583
|
-
const lastModified = String(remote.last_modified ?? remote.lastModified ?? '').trim();
|
|
1584
|
-
const saved = this._saveAgentMdRecord(target, {
|
|
1585
|
-
remote_etag: remoteFound ? remoteEtag : '',
|
|
1586
|
-
last_modified: lastModified,
|
|
1587
|
-
checked_at: now,
|
|
1588
|
-
remote_status: remoteFound ? 'found' : 'missing',
|
|
1589
|
-
last_error: '',
|
|
1590
|
-
});
|
|
1591
|
-
if (target === this._agentMdOwnerAid() && remoteEtag)
|
|
1592
|
-
this._remoteAgentMdEtag = remoteEtag;
|
|
1593
|
-
const inSync = !!(localFound && remoteFound && localEtag && remoteEtag && localEtag === remoteEtag);
|
|
1594
|
-
return {
|
|
1595
|
-
aid: target,
|
|
1596
|
-
local_found: localFound,
|
|
1597
|
-
remote_found: remoteFound,
|
|
1598
|
-
local_etag: localEtag,
|
|
1599
|
-
remote_etag: remoteEtag,
|
|
1600
|
-
in_sync: inSync,
|
|
1601
|
-
last_modified: lastModified,
|
|
1602
|
-
status: Number(remote.status ?? (remoteFound ? 200 : 404)),
|
|
1603
|
-
cached: false,
|
|
1604
|
-
verify_status: String(saved.verify_status ?? before.verify_status ?? ''),
|
|
1605
|
-
verify_error: String(saved.verify_error ?? before.verify_error ?? ''),
|
|
1606
|
-
};
|
|
927
|
+
async uploadAgentMd(content) {
|
|
928
|
+
return await this._agentMdManager.upload(content);
|
|
1607
929
|
}
|
|
1608
930
|
/** transport 的 meta observer:吸收 gateway 注入的 _meta 字段。失败不影响业务。 */
|
|
1609
931
|
_observeRpcMeta(meta) {
|
|
1610
|
-
|
|
1611
|
-
return;
|
|
1612
|
-
const etag = String(meta.agent_md_etag ?? '').trim();
|
|
1613
|
-
if (etag) {
|
|
1614
|
-
this._remoteAgentMdEtag = etag;
|
|
1615
|
-
this._observeAgentMdMeta(this._aid ?? '', etag, '', 'rpc.self');
|
|
1616
|
-
}
|
|
1617
|
-
const etags = meta.agent_md_etags;
|
|
1618
|
-
if (isJsonObject(etags)) {
|
|
1619
|
-
// role key 优先级:requester / peer 是新规范,其余是兼容旧 SDK 的别名。
|
|
1620
|
-
for (const key of ['requester', 'peer', 'receiver', 'target', 'to', 'sender', 'from']) {
|
|
1621
|
-
const item = etags[key];
|
|
1622
|
-
if (!isJsonObject(item))
|
|
1623
|
-
continue;
|
|
1624
|
-
this._observeAgentMdMeta(String(item.aid ?? ''), String(item.etag ?? ''), String(item.last_modified ?? item.lastModified ?? ''), `rpc.${key}`);
|
|
1625
|
-
}
|
|
1626
|
-
}
|
|
932
|
+
this._agentMdManager.observeRpcMeta(meta, this._aid);
|
|
1627
933
|
}
|
|
1628
934
|
/** 连接状态 */
|
|
1629
935
|
get state() {
|
|
@@ -1781,8 +1087,8 @@ export class AUNClient {
|
|
|
1781
1087
|
this._stopBackgroundTasks();
|
|
1782
1088
|
this._stopReconnect();
|
|
1783
1089
|
if (this.state === ConnectionState.NO_IDENTITY || this.state === ConnectionState.CLOSED) {
|
|
1784
|
-
const
|
|
1785
|
-
|
|
1090
|
+
const closableStore = this._tokenStore;
|
|
1091
|
+
closableStore.close?.();
|
|
1786
1092
|
this._state = 'closed';
|
|
1787
1093
|
this._logger.close();
|
|
1788
1094
|
this._resetSeqTrackingState();
|
|
@@ -1790,8 +1096,8 @@ export class AUNClient {
|
|
|
1790
1096
|
return;
|
|
1791
1097
|
}
|
|
1792
1098
|
await this._transport.close();
|
|
1793
|
-
const
|
|
1794
|
-
|
|
1099
|
+
const closableStore = this._tokenStore;
|
|
1100
|
+
closableStore.close?.();
|
|
1795
1101
|
this._state = 'closed';
|
|
1796
1102
|
this._logger.close();
|
|
1797
1103
|
await this._dispatcher.publish('state_change', { state: this._publicState(this._state) });
|
|
@@ -2548,15 +1854,11 @@ export class AUNClient {
|
|
|
2548
1854
|
// 注入本地/远端 agent.md etag,让应用层判断版本一致性;失败不影响业务。
|
|
2549
1855
|
if (isJsonObject(payload)) {
|
|
2550
1856
|
try {
|
|
2551
|
-
const
|
|
2552
|
-
|
|
2553
|
-
if (localEtag || remoteEtag) {
|
|
1857
|
+
const snapshot = this._agentMdManager.eventSnapshot();
|
|
1858
|
+
if (snapshot) {
|
|
2554
1859
|
const obj = payload;
|
|
2555
1860
|
if (!('_agent_md' in obj)) {
|
|
2556
|
-
obj._agent_md =
|
|
2557
|
-
local_etag: localEtag,
|
|
2558
|
-
remote_etag: remoteEtag,
|
|
2559
|
-
};
|
|
1861
|
+
obj._agent_md = snapshot;
|
|
2560
1862
|
}
|
|
2561
1863
|
}
|
|
2562
1864
|
}
|
|
@@ -3369,8 +2671,8 @@ export class AUNClient {
|
|
|
3369
2671
|
const membershipSnapshot = String(d.membership_snapshot ?? '').trim();
|
|
3370
2672
|
const policySnapshot = String(d.policy_snapshot ?? '').trim();
|
|
3371
2673
|
// 1. 验证 prev_state_hash 连续性
|
|
3372
|
-
const loadFn = this.
|
|
3373
|
-
const localState = loadFn ? loadFn.call(this.
|
|
2674
|
+
const loadFn = this._tokenStore.loadGroupState;
|
|
2675
|
+
const localState = loadFn ? loadFn.call(this._tokenStore, groupId) : null;
|
|
3374
2676
|
if (localState && localState.state_hash && localState.state_hash !== prevStateHash) {
|
|
3375
2677
|
this._clientLog.warn(`state_hash chain discontinuous group=${groupId} local_sv=${localState.state_version} event_sv=${stateVersion}`);
|
|
3376
2678
|
// 回源同步
|
|
@@ -3396,9 +2698,9 @@ export class AUNClient {
|
|
|
3396
2698
|
return;
|
|
3397
2699
|
}
|
|
3398
2700
|
}
|
|
3399
|
-
const saveFn = this.
|
|
2701
|
+
const saveFn = this._tokenStore.saveGroupState;
|
|
3400
2702
|
if (saveFn) {
|
|
3401
|
-
saveFn.call(this.
|
|
2703
|
+
saveFn.call(this._tokenStore, groupId, sv, sHash, sEpoch, sMembersJson || membershipSnapshot, sPolicyJson || policySnapshot);
|
|
3402
2704
|
}
|
|
3403
2705
|
}
|
|
3404
2706
|
}
|
|
@@ -3419,9 +2721,9 @@ export class AUNClient {
|
|
|
3419
2721
|
return;
|
|
3420
2722
|
}
|
|
3421
2723
|
// 3. 更新本地存储
|
|
3422
|
-
const saveFn = this.
|
|
2724
|
+
const saveFn = this._tokenStore.saveGroupState;
|
|
3423
2725
|
if (saveFn) {
|
|
3424
|
-
saveFn.call(this.
|
|
2726
|
+
saveFn.call(this._tokenStore, groupId, stateVersion, stateHash, keyEpoch, membershipSnapshot, policySnapshot);
|
|
3425
2727
|
}
|
|
3426
2728
|
this._clientLog.debug(`_onGroupStateCommitted exit: elapsed=${Date.now() - tStart}ms group=${groupId}`);
|
|
3427
2729
|
}
|
|
@@ -3581,7 +2883,7 @@ export class AUNClient {
|
|
|
3581
2883
|
}
|
|
3582
2884
|
try {
|
|
3583
2885
|
// peer 证书只存版本目录,不覆盖 cert.pem
|
|
3584
|
-
this.
|
|
2886
|
+
this._tokenStore.saveCert(aid, certPem, certFingerprint, { makeActive: false });
|
|
3585
2887
|
}
|
|
3586
2888
|
catch (exc) {
|
|
3587
2889
|
this._clientLog.error(`failed to write cert to keystore (aid=${aid}, fp=${certFingerprint ?? ''}): ${formatCaughtError(exc)}`, exc instanceof Error ? exc : undefined);
|
|
@@ -3852,9 +3154,9 @@ export class AUNClient {
|
|
|
3852
3154
|
return;
|
|
3853
3155
|
try {
|
|
3854
3156
|
// 优先从 seq_tracker 表按行读取
|
|
3855
|
-
const loadAll = this.
|
|
3157
|
+
const loadAll = this._tokenStore.loadAllSeqs;
|
|
3856
3158
|
if (typeof loadAll === 'function') {
|
|
3857
|
-
let state = loadAll.call(this.
|
|
3159
|
+
let state = loadAll.call(this._tokenStore, this._aid, this._deviceId, this._slotId);
|
|
3858
3160
|
if (state && Object.keys(state).length > 0) {
|
|
3859
3161
|
state = this._migrateSeqStateGroupIds(state);
|
|
3860
3162
|
this._seqTracker.restoreState(state);
|
|
@@ -3862,9 +3164,9 @@ export class AUNClient {
|
|
|
3862
3164
|
}
|
|
3863
3165
|
}
|
|
3864
3166
|
// fallback: 从旧 instance_state JSON blob 恢复
|
|
3865
|
-
const loader = this.
|
|
3167
|
+
const loader = this._tokenStore.loadInstanceState;
|
|
3866
3168
|
if (typeof loader === 'function') {
|
|
3867
|
-
const instanceState = loader.call(this.
|
|
3169
|
+
const instanceState = loader.call(this._tokenStore, this._aid, this._deviceId, this._slotId);
|
|
3868
3170
|
if (instanceState && typeof instanceState.seq_tracker_state === 'object') {
|
|
3869
3171
|
let state = instanceState.seq_tracker_state;
|
|
3870
3172
|
state = this._migrateSeqStateGroupIds(state);
|
|
@@ -3915,20 +3217,20 @@ export class AUNClient {
|
|
|
3915
3217
|
}
|
|
3916
3218
|
this._clientLog.info(`SeqTracker group_id migration: ${Object.keys(renameMap).length} namespaces rewritten`);
|
|
3917
3219
|
// 落盘
|
|
3918
|
-
const saver = this.
|
|
3919
|
-
const deleter = this.
|
|
3220
|
+
const saver = this._tokenStore.saveSeq;
|
|
3221
|
+
const deleter = this._tokenStore.deleteSeq;
|
|
3920
3222
|
if (typeof saver === 'function' && this._aid) {
|
|
3921
3223
|
for (const [oldNs, newNs] of Object.entries(renameMap)) {
|
|
3922
3224
|
if (typeof deleter === 'function') {
|
|
3923
3225
|
try {
|
|
3924
|
-
deleter.call(this.
|
|
3226
|
+
deleter.call(this._tokenStore, this._aid, this._deviceId, this._slotId, oldNs);
|
|
3925
3227
|
}
|
|
3926
3228
|
catch (e) {
|
|
3927
3229
|
this._clientLog.debug(`delete old seq ns failed: ns=${oldNs} err=${formatCaughtError(e)}`);
|
|
3928
3230
|
}
|
|
3929
3231
|
}
|
|
3930
3232
|
try {
|
|
3931
|
-
saver.call(this.
|
|
3233
|
+
saver.call(this._tokenStore, this._aid, this._deviceId, this._slotId, newNs, newState[newNs]);
|
|
3932
3234
|
}
|
|
3933
3235
|
catch (e) {
|
|
3934
3236
|
this._clientLog.debug(`write new seq ns failed: ns=${newNs} err=${formatCaughtError(e)}`);
|
|
@@ -3976,17 +3278,17 @@ export class AUNClient {
|
|
|
3976
3278
|
return;
|
|
3977
3279
|
try {
|
|
3978
3280
|
// 优先按行写入 seq_tracker 表
|
|
3979
|
-
const saveFn = this.
|
|
3281
|
+
const saveFn = this._tokenStore.saveSeq;
|
|
3980
3282
|
if (typeof saveFn === 'function') {
|
|
3981
3283
|
for (const [ns, seq] of Object.entries(state)) {
|
|
3982
|
-
saveFn.call(this.
|
|
3284
|
+
saveFn.call(this._tokenStore, this._aid, this._deviceId, this._slotId, ns, seq);
|
|
3983
3285
|
}
|
|
3984
3286
|
return;
|
|
3985
3287
|
}
|
|
3986
3288
|
// fallback: 旧版 updateInstanceState JSON blob
|
|
3987
|
-
const updater = this.
|
|
3289
|
+
const updater = this._tokenStore.updateInstanceState;
|
|
3988
3290
|
if (typeof updater === 'function') {
|
|
3989
|
-
updater.call(this.
|
|
3291
|
+
updater.call(this._tokenStore, this._aid, this._deviceId, this._slotId, (metadata) => {
|
|
3990
3292
|
metadata.seq_tracker_state = state;
|
|
3991
3293
|
return metadata;
|
|
3992
3294
|
});
|
|
@@ -4009,13 +3311,13 @@ export class AUNClient {
|
|
|
4009
3311
|
return;
|
|
4010
3312
|
const seq = this._seqTracker.getContiguousSeq(ns);
|
|
4011
3313
|
try {
|
|
4012
|
-
if (seq > 0 && typeof this.
|
|
4013
|
-
this.
|
|
3314
|
+
if (seq > 0 && typeof this._tokenStore.saveSeq === 'function') {
|
|
3315
|
+
this._tokenStore.saveSeq(this._aid, this._deviceId, this._slotId, ns, seq);
|
|
4014
3316
|
return;
|
|
4015
3317
|
}
|
|
4016
|
-
const deleteSeq = this.
|
|
3318
|
+
const deleteSeq = this._tokenStore.deleteSeq;
|
|
4017
3319
|
if (seq <= 0 && typeof deleteSeq === 'function') {
|
|
4018
|
-
deleteSeq.call(this.
|
|
3320
|
+
deleteSeq.call(this._tokenStore, this._aid, this._deviceId, this._slotId, ns);
|
|
4019
3321
|
return;
|
|
4020
3322
|
}
|
|
4021
3323
|
if (seq > 0) {
|
|
@@ -4247,17 +3549,7 @@ export class AUNClient {
|
|
|
4247
3549
|
this._v2BootstrapCache.clear();
|
|
4248
3550
|
}
|
|
4249
3551
|
let identity = this._identity;
|
|
4250
|
-
|
|
4251
|
-
try {
|
|
4252
|
-
identity = this._keystore.loadIdentity(this._aid);
|
|
4253
|
-
if (identity)
|
|
4254
|
-
this._identity = identity;
|
|
4255
|
-
}
|
|
4256
|
-
catch {
|
|
4257
|
-
identity = null;
|
|
4258
|
-
}
|
|
4259
|
-
}
|
|
4260
|
-
// 私钥由 AIDStore 管理,直接从 _currentAid 读取明文私钥
|
|
3552
|
+
// 私钥来自当前 AID 值对象,AUNClient 不从持久化存储读取私钥。
|
|
4261
3553
|
const currentAid = this._currentAid;
|
|
4262
3554
|
if (!currentAid?.privateKeyPem) {
|
|
4263
3555
|
this._clientLog.warn('V2 session init skipped: no AID private key');
|
|
@@ -4271,8 +3563,8 @@ export class AUNClient {
|
|
|
4271
3563
|
const aidPriv = _v2LeftPad32(_v2B64uToBytes(jwk.d));
|
|
4272
3564
|
const pubDer = crypto.createPublicKey(privateKey).export({ format: 'der', type: 'spki' });
|
|
4273
3565
|
const aidPubDer = new Uint8Array(pubDer);
|
|
4274
|
-
const storeProvider = this.
|
|
4275
|
-
const v2Store = storeProvider.getV2KeyStore?.call(this.
|
|
3566
|
+
const storeProvider = this._tokenStore;
|
|
3567
|
+
const v2Store = storeProvider.getV2KeyStore?.call(this._tokenStore, this._aid);
|
|
4276
3568
|
if (!v2Store) {
|
|
4277
3569
|
throw new StateError('V2 key store is unavailable for current keystore');
|
|
4278
3570
|
}
|
|
@@ -4282,6 +3574,28 @@ export class AUNClient {
|
|
|
4282
3574
|
this._clientLog.debug(`V2 session initialized aid=${this._aid} device=${this._deviceId}`);
|
|
4283
3575
|
// 群 state proposal 由服务端在 client.online 时定向通知。
|
|
4284
3576
|
}
|
|
3577
|
+
_currentV2KeyStore() {
|
|
3578
|
+
if (this._v2KeyStore)
|
|
3579
|
+
return this._v2KeyStore;
|
|
3580
|
+
if (!this._aid)
|
|
3581
|
+
throw new StateError('V2 key store requires a loaded AID');
|
|
3582
|
+
const storeProvider = this._tokenStore;
|
|
3583
|
+
const v2Store = storeProvider.getV2KeyStore?.call(this._tokenStore, this._aid);
|
|
3584
|
+
if (!v2Store) {
|
|
3585
|
+
throw new StateError('V2 key store is unavailable for current identity');
|
|
3586
|
+
}
|
|
3587
|
+
this._v2KeyStore = v2Store;
|
|
3588
|
+
return v2Store;
|
|
3589
|
+
}
|
|
3590
|
+
_saveGroupIdentityToV2(groupAid, identity) {
|
|
3591
|
+
const privateKeyPem = String(identity.private_key_pem ?? '').trim();
|
|
3592
|
+
const publicKeyDerB64 = String(identity.public_key_der_b64 ?? '').trim();
|
|
3593
|
+
if (!groupAid || !privateKeyPem || !publicKeyDerB64) {
|
|
3594
|
+
throw new StateError('group identity is incomplete');
|
|
3595
|
+
}
|
|
3596
|
+
const pubDer = new Uint8Array(Buffer.from(publicKeyDerB64, 'base64'));
|
|
3597
|
+
this._currentV2KeyStore().saveGroupIdentity(this._deviceId, groupAid, privateKeyPem, pubDer);
|
|
3598
|
+
}
|
|
4285
3599
|
async _v2TrustedIKPubDer(aid) {
|
|
4286
3600
|
const normalizedAid = String(aid ?? '').trim();
|
|
4287
3601
|
if (!normalizedAid)
|
|
@@ -5263,7 +4577,7 @@ export class AUNClient {
|
|
|
5263
4577
|
return null;
|
|
5264
4578
|
}
|
|
5265
4579
|
const e2eeMeta = this._v2E2eeMeta(envelope);
|
|
5266
|
-
this.
|
|
4580
|
+
this._agentMdManager.observeEnvelope(envelope);
|
|
5267
4581
|
let spkId = '';
|
|
5268
4582
|
let recipientKeySource = '';
|
|
5269
4583
|
if (isJsonObject(envelope.recipient)) {
|
|
@@ -5468,7 +4782,7 @@ export class AUNClient {
|
|
|
5468
4782
|
_attachV2EnvelopeMetadataFromSource(message, source) {
|
|
5469
4783
|
const envelope = this._extractV2EnvelopeFromSource(source);
|
|
5470
4784
|
if (envelope) {
|
|
5471
|
-
this.
|
|
4785
|
+
this._agentMdManager.observeEnvelope(envelope);
|
|
5472
4786
|
this._attachV2EnvelopeMetadata(message, this._v2E2eeMeta(envelope));
|
|
5473
4787
|
}
|
|
5474
4788
|
}
|
|
@@ -6555,9 +5869,9 @@ export class AUNClient {
|
|
|
6555
5869
|
if (this._gatewayUrl)
|
|
6556
5870
|
return this._gatewayUrl;
|
|
6557
5871
|
try {
|
|
6558
|
-
const loadMetadata = this.
|
|
5872
|
+
const loadMetadata = this._tokenStore.loadMetadata;
|
|
6559
5873
|
const cachedGateway = typeof loadMetadata === 'function'
|
|
6560
|
-
? String(loadMetadata.call(this.
|
|
5874
|
+
? String(loadMetadata.call(this._tokenStore, resolvedAid)?.gateway_url ?? '').trim()
|
|
6561
5875
|
: '';
|
|
6562
5876
|
if (cachedGateway) {
|
|
6563
5877
|
this._gatewayUrl = cachedGateway;
|
|
@@ -6579,9 +5893,9 @@ export class AUNClient {
|
|
|
6579
5893
|
const gateway = await this._discovery.discover(url);
|
|
6580
5894
|
this._gatewayUrl = gateway;
|
|
6581
5895
|
try {
|
|
6582
|
-
const saveMetadata = this.
|
|
5896
|
+
const saveMetadata = this._tokenStore.saveMetadata;
|
|
6583
5897
|
if (typeof saveMetadata === 'function') {
|
|
6584
|
-
saveMetadata.call(this.
|
|
5898
|
+
saveMetadata.call(this._tokenStore, resolvedAid, { gateway_url: gateway, gateway_cached_at: Date.now() });
|
|
6585
5899
|
}
|
|
6586
5900
|
}
|
|
6587
5901
|
catch {
|
|
@@ -6627,9 +5941,8 @@ export class AUNClient {
|
|
|
6627
5941
|
}
|
|
6628
5942
|
/** 连接后同步身份信息 */
|
|
6629
5943
|
_syncIdentityAfterConnect(accessToken) {
|
|
6630
|
-
const identity = this.
|
|
5944
|
+
const identity = this._identity;
|
|
6631
5945
|
if (identity === null) {
|
|
6632
|
-
this._identity = null;
|
|
6633
5946
|
return;
|
|
6634
5947
|
}
|
|
6635
5948
|
identity.access_token = accessToken;
|
|
@@ -6640,9 +5953,7 @@ export class AUNClient {
|
|
|
6640
5953
|
const persistIdentity = this._auth._persistIdentity;
|
|
6641
5954
|
if (typeof persistIdentity === 'function') {
|
|
6642
5955
|
persistIdentity.call(this._auth, identity);
|
|
6643
|
-
return;
|
|
6644
5956
|
}
|
|
6645
|
-
this._keystore.saveIdentity(String(identity.aid), identity);
|
|
6646
5957
|
}
|
|
6647
5958
|
// ── 内部:参数处理 ────────────────────────────────────────
|
|
6648
5959
|
/** 规范化连接参数 */
|
|
@@ -6826,7 +6137,7 @@ export class AUNClient {
|
|
|
6826
6137
|
scheduleNext();
|
|
6827
6138
|
return;
|
|
6828
6139
|
}
|
|
6829
|
-
let identity = this._identity
|
|
6140
|
+
let identity = this._identity;
|
|
6830
6141
|
if (identity === null) {
|
|
6831
6142
|
scheduleNext();
|
|
6832
6143
|
return;
|
|
@@ -6954,8 +6265,8 @@ export class AUNClient {
|
|
|
6954
6265
|
if ('device_id' in params && String(params.device_id ?? '').trim() !== this._deviceId) {
|
|
6955
6266
|
throw new ValidationError('message.pull/message.ack device_id must match the current client instance');
|
|
6956
6267
|
}
|
|
6957
|
-
const slotId =
|
|
6958
|
-
if (slotId !== this._slotId) {
|
|
6268
|
+
const slotId = normalizeSlotId(params.slot_id ?? this._slotId, this._slotId);
|
|
6269
|
+
if (slotIsolationKey(slotId) !== slotIsolationKey(this._slotId)) {
|
|
6959
6270
|
throw new ValidationError('message.pull/message.ack slot_id must match the current client instance');
|
|
6960
6271
|
}
|
|
6961
6272
|
params.device_id = this._deviceId;
|
|
@@ -7175,8 +6486,7 @@ export class AUNClient {
|
|
|
7175
6486
|
}
|
|
7176
6487
|
// ── Named Group(命名群)高层 API ────────────────────────────
|
|
7177
6488
|
/**
|
|
7178
|
-
*
|
|
7179
|
-
* 服务端签发群 AID 证书,返回后将证书和私钥存入 keystore。
|
|
6489
|
+
* 创建命名群:群/P2P 私钥由 V2 数据库存储,不写入 AID 身份私钥存储。
|
|
7180
6490
|
*/
|
|
7181
6491
|
async createNamedGroup(groupName, opts = {}) {
|
|
7182
6492
|
const tStart = Date.now();
|
|
@@ -7196,15 +6506,10 @@ export class AUNClient {
|
|
|
7196
6506
|
const aidCert = result?.aid_cert;
|
|
7197
6507
|
const groupAid = String(groupInfo?.group_aid ?? '');
|
|
7198
6508
|
if (groupAid && aidCert) {
|
|
7199
|
-
this.
|
|
7200
|
-
private_key_pem: identity.private_key_pem,
|
|
7201
|
-
public_key: identity.public_key_der_b64,
|
|
7202
|
-
curve: 'P-256',
|
|
7203
|
-
type: 'group_identity',
|
|
7204
|
-
});
|
|
6509
|
+
this._saveGroupIdentityToV2(groupAid, identity);
|
|
7205
6510
|
const certPem = String(aidCert.cert ?? '');
|
|
7206
6511
|
if (certPem) {
|
|
7207
|
-
this.
|
|
6512
|
+
this._tokenStore.saveCert(groupAid, certPem);
|
|
7208
6513
|
}
|
|
7209
6514
|
}
|
|
7210
6515
|
this._clientLog.debug(`createNamedGroup exit: elapsed=${Date.now() - tStart}ms groupAid=${groupAid}`);
|
|
@@ -7235,15 +6540,10 @@ export class AUNClient {
|
|
|
7235
6540
|
const aidCert = result?.aid_cert;
|
|
7236
6541
|
const groupAid = String(groupInfo?.group_aid ?? '');
|
|
7237
6542
|
if (groupAid && aidCert) {
|
|
7238
|
-
this.
|
|
7239
|
-
private_key_pem: identity.private_key_pem,
|
|
7240
|
-
public_key: identity.public_key_der_b64,
|
|
7241
|
-
curve: 'P-256',
|
|
7242
|
-
type: 'group_identity',
|
|
7243
|
-
});
|
|
6543
|
+
this._saveGroupIdentityToV2(groupAid, identity);
|
|
7244
6544
|
const certPem = String(aidCert.cert ?? '');
|
|
7245
6545
|
if (certPem) {
|
|
7246
|
-
this.
|
|
6546
|
+
this._tokenStore.saveCert(groupAid, certPem);
|
|
7247
6547
|
}
|
|
7248
6548
|
}
|
|
7249
6549
|
this._clientLog.debug(`bindGroupAid exit: elapsed=${Date.now() - tStart}ms groupAid=${groupAid}`);
|