@agentunion/fastaun-browser 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 +111 -0
- package/dist/agent-md.d.ts.map +1 -0
- package/dist/agent-md.js +656 -0
- package/dist/agent-md.js.map +1 -0
- package/dist/aid-store.d.ts +8 -40
- package/dist/aid-store.d.ts.map +1 -1
- package/dist/aid-store.js +89 -172
- package/dist/aid-store.js.map +1 -1
- package/dist/auth.d.ts +8 -13
- package/dist/auth.d.ts.map +1 -1
- package/dist/auth.js +37 -130
- package/dist/auth.js.map +1 -1
- package/dist/bundle.js +6540 -6033
- package/dist/client.d.ts +8 -65
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js +193 -762
- package/dist/client.js.map +1 -1
- package/dist/index.d.ts +4 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -1
- package/dist/index.js.map +1 -1
- package/dist/keystore/index.d.ts +49 -22
- package/dist/keystore/index.d.ts.map +1 -1
- package/dist/keystore/index.js +6 -1
- package/dist/keystore/index.js.map +1 -1
- package/dist/keystore/indexeddb-identity-store.d.ts +59 -0
- package/dist/keystore/indexeddb-identity-store.d.ts.map +1 -0
- package/dist/keystore/indexeddb-identity-store.js +489 -0
- package/dist/keystore/indexeddb-identity-store.js.map +1 -0
- package/dist/keystore/indexeddb-shared.d.ts +76 -0
- package/dist/keystore/indexeddb-shared.d.ts.map +1 -0
- package/dist/keystore/indexeddb-shared.js +382 -0
- package/dist/keystore/indexeddb-shared.js.map +1 -0
- package/dist/keystore/indexeddb-token-store.d.ts +119 -0
- package/dist/keystore/indexeddb-token-store.d.ts.map +1 -0
- package/dist/keystore/indexeddb-token-store.js +1086 -0
- package/dist/keystore/indexeddb-token-store.js.map +1 -0
- package/dist/keystore/indexeddb.d.ts +11 -1
- package/dist/keystore/indexeddb.d.ts.map +1 -1
- package/dist/keystore/indexeddb.js +167 -18
- package/dist/keystore/indexeddb.js.map +1 -1
- package/dist/register-flow.d.ts +53 -0
- package/dist/register-flow.d.ts.map +1 -0
- package/dist/register-flow.js +401 -0
- package/dist/register-flow.js.map +1 -0
- package/dist/v2/session/keystore.d.ts +5 -0
- package/dist/v2/session/keystore.d.ts.map +1 -1
- package/dist/v2/session/keystore.js +29 -0
- 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
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
// - HTTP 使用 fetch() 而非 Node http
|
|
5
5
|
// - 无文件系统(IndexedDB via keystore)
|
|
6
6
|
// - 后台任务使用 setTimeout/setInterval
|
|
7
|
-
import { createConfig, getDeviceId,
|
|
7
|
+
import { createConfig, getDeviceId, normalizeSlotId, slotIsolationKey } from './config.js';
|
|
8
8
|
import { EventDispatcher } from './events.js';
|
|
9
9
|
import { normalizeGroupId } from './group-id.js';
|
|
10
10
|
import { GatewayDiscovery } from './discovery.js';
|
|
@@ -12,12 +12,13 @@ import { RPCTransport } from './transport.js';
|
|
|
12
12
|
import { AuthFlow } from './auth.js';
|
|
13
13
|
import { SeqTracker } from './seq-tracker.js';
|
|
14
14
|
import { CryptoProvider, uint8ToBase64, base64ToUint8, pemToArrayBuffer, p1363ToDer, certificateSha256Fingerprint, ecdsaSignDer, ecdsaVerifyDer, importCertPublicKeyEcdsa, importPrivateKeyEcdsa, } from './crypto.js';
|
|
15
|
-
import {
|
|
15
|
+
import { IndexedDBTokenStore } from './keystore/indexeddb-token-store.js';
|
|
16
16
|
import { V2Session, V2KeyStore } from './v2/session/index.js';
|
|
17
17
|
import { encryptP2PMessage, encryptGroupMessage, decryptMessage, } from './v2/e2ee/index.js';
|
|
18
18
|
import { ecdsaVerifyRaw } from './v2/crypto/ecdsa.js';
|
|
19
19
|
import { computeStateCommitment } from './v2/state/index.js';
|
|
20
20
|
import { AUNLogger } from './logger.js';
|
|
21
|
+
import { AgentMdManager } from './agent-md.js';
|
|
21
22
|
import { AUNError, AuthError, ConnectionError, E2EEError, NotFoundError, PermissionError, StateError, ValidationError, } from './errors.js';
|
|
22
23
|
import { isJsonObject, ConnectionState, STATE_TO_PUBLIC, } from './types.js';
|
|
23
24
|
import { AID } from './aid.js';
|
|
@@ -239,7 +240,6 @@ function reconnectSleepDelaySeconds(baseDelay, maxBaseDelay) {
|
|
|
239
240
|
/** 对端证书缓存 TTL(秒) */
|
|
240
241
|
const PEER_CERT_CACHE_TTL = 3600;
|
|
241
242
|
const PEER_PREKEYS_CACHE_TTL = 3600;
|
|
242
|
-
const AGENT_MD_HTTP_TIMEOUT_MS = 30_000;
|
|
243
243
|
/**
|
|
244
244
|
* 将 WebSocket URL 转为对应的 HTTP URL
|
|
245
245
|
*/
|
|
@@ -272,28 +272,89 @@ function buildCertUrl(gatewayUrl, aid, certFingerprint) {
|
|
|
272
272
|
}
|
|
273
273
|
return url.toString();
|
|
274
274
|
}
|
|
275
|
-
function
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
async
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
275
|
+
function createAgentMdManagerForRuntime(opts) {
|
|
276
|
+
return new AgentMdManager({
|
|
277
|
+
aunPath: opts.config().aunPath,
|
|
278
|
+
tokenStore: opts.tokenStore(),
|
|
279
|
+
logger: opts.logger(),
|
|
280
|
+
ownerAidGetter: opts.ownerAid,
|
|
281
|
+
currentAidGetter: opts.currentAid,
|
|
282
|
+
gatewayResolver: async (aid) => {
|
|
283
|
+
const gatewayUrl = await opts.gateway.resolve(aid);
|
|
284
|
+
opts.gateway.set(gatewayUrl);
|
|
285
|
+
return gatewayUrl;
|
|
286
|
+
},
|
|
287
|
+
accessTokenResolver: async (aid, gatewayUrl) => {
|
|
288
|
+
const target = String(aid ?? '').trim();
|
|
289
|
+
let identity = await opts.auth().loadIdentityOrNone(target);
|
|
290
|
+
if (!identity && opts.identity.get() && String(opts.identity.get()?.aid ?? '') === target) {
|
|
291
|
+
identity = opts.identity.get();
|
|
292
|
+
}
|
|
293
|
+
if (!identity) {
|
|
294
|
+
throw new StateError('no local identity found, register or load an AID first');
|
|
295
|
+
}
|
|
296
|
+
const auth = opts.auth();
|
|
297
|
+
const cachedToken = String(identity.access_token ?? '');
|
|
298
|
+
const expiresAt = auth.getAccessTokenExpiry(identity);
|
|
299
|
+
if (cachedToken && (expiresAt === null || expiresAt > Date.now() / 1000 + 30)) {
|
|
300
|
+
return cachedToken;
|
|
301
|
+
}
|
|
302
|
+
if (identity.refresh_token) {
|
|
303
|
+
try {
|
|
304
|
+
const refreshed = await auth.refreshCachedTokens(gatewayUrl, identity);
|
|
305
|
+
const refreshedToken = String(refreshed.access_token ?? '');
|
|
306
|
+
const refreshedExpiry = auth.getAccessTokenExpiry(refreshed);
|
|
307
|
+
if (refreshedToken && (refreshedExpiry === null || refreshedExpiry > Date.now() / 1000 + 30)) {
|
|
308
|
+
opts.identity.set(refreshed);
|
|
309
|
+
return refreshedToken;
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
catch {
|
|
313
|
+
// refresh 失败时回退到完整 authenticate。
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
const result = await auth.authenticate(gatewayUrl, target);
|
|
317
|
+
const token = String(result.access_token ?? '');
|
|
318
|
+
if (!token)
|
|
319
|
+
throw new StateError('authenticate did not return access_token');
|
|
320
|
+
const fallbackIdentity = {
|
|
321
|
+
...identity,
|
|
322
|
+
access_token: token,
|
|
323
|
+
refresh_token: String(result.refresh_token ?? identity.refresh_token ?? ''),
|
|
324
|
+
};
|
|
325
|
+
const fallbackExpiresAt = Number(result.expires_at ?? identity.expires_at ?? NaN);
|
|
326
|
+
if (Number.isFinite(fallbackExpiresAt))
|
|
327
|
+
fallbackIdentity.expires_at = fallbackExpiresAt;
|
|
328
|
+
opts.identity.set(await auth.loadIdentityOrNone(target) ?? fallbackIdentity);
|
|
329
|
+
return token;
|
|
330
|
+
},
|
|
331
|
+
peerResolver: async (aid) => {
|
|
332
|
+
const target = String(aid ?? '').trim();
|
|
333
|
+
const current = opts.currentAid();
|
|
334
|
+
if (current?.aid === target)
|
|
335
|
+
return current;
|
|
336
|
+
let certPem = String(await opts.tokenStore().loadCert(target) ?? '').trim();
|
|
337
|
+
if (!certPem) {
|
|
338
|
+
if (!opts.gateway.get()) {
|
|
339
|
+
try {
|
|
340
|
+
opts.gateway.set(await opts.gateway.resolve(target));
|
|
341
|
+
}
|
|
342
|
+
catch { /* best effort */ }
|
|
343
|
+
}
|
|
344
|
+
certPem = String(await opts.fetchPeerCert(target) ?? '').trim();
|
|
345
|
+
}
|
|
346
|
+
if (!certPem)
|
|
347
|
+
throw new NotFoundError(`certificate not found for aid: ${target}`);
|
|
348
|
+
return await AID.create({
|
|
349
|
+
aid: target,
|
|
350
|
+
aunPath: opts.config().aunPath,
|
|
351
|
+
certPem,
|
|
352
|
+
privateKeyPem: null,
|
|
353
|
+
certValid: true,
|
|
354
|
+
privateKeyValid: false,
|
|
355
|
+
});
|
|
356
|
+
},
|
|
357
|
+
});
|
|
297
358
|
}
|
|
298
359
|
/**
|
|
299
360
|
* 跨域时将 Gateway URL 替换为 peer 所在域的 Gateway URL。
|
|
@@ -664,7 +725,7 @@ export class AUNClient {
|
|
|
664
725
|
_sessionOptions = { ...DEFAULT_SESSION_OPTIONS };
|
|
665
726
|
_dispatcher;
|
|
666
727
|
_discovery;
|
|
667
|
-
|
|
728
|
+
_tokenStore;
|
|
668
729
|
_auth;
|
|
669
730
|
_transport;
|
|
670
731
|
// E2EE 编排状态(内存缓存)
|
|
@@ -699,20 +760,8 @@ export class AUNClient {
|
|
|
699
760
|
/** 最近一次已成功确认的 membership_snapshot;相同快照直接跳过。 */
|
|
700
761
|
_v2AutoProposeLastSnapshot = new Map();
|
|
701
762
|
_v2LazyProposeTriggered = new Map();
|
|
702
|
-
/**
|
|
703
|
-
|
|
704
|
-
*
|
|
705
|
-
* 由 publishAgentMd() / fetchAgentMd(自身 aid) 写入;用于跟服务端 RPC 注入的 _meta.agent_md_etag
|
|
706
|
-
* 比对,触发"本地未发布到服务端"或"服务端版本更新"的 UI 提示。
|
|
707
|
-
*/
|
|
708
|
-
_localAgentMdEtag = '';
|
|
709
|
-
/** gateway 在 RPC envelope._meta.agent_md_etag 注入的服务端 etag;纯观察,无下游依赖。 */
|
|
710
|
-
_remoteAgentMdEtag = '';
|
|
711
|
-
/** 浏览器侧 AIDs 逻辑根目录,正文映射到 IndexedDB 里的 {aid}/agent.md。 */
|
|
712
|
-
_agentMdPath = '';
|
|
713
|
-
_agentMdCache = new Map();
|
|
714
|
-
_agentMdFetchInflight = new Set();
|
|
715
|
-
_agentMdLock = Promise.resolve();
|
|
763
|
+
/** agent.md 运行时管理器,负责上传、下载、缓存和 RPC 元数据观察。 */
|
|
764
|
+
_agentMdManager;
|
|
716
765
|
/** 消息序列号跟踪器(群消息 + P2P 空洞检测) */
|
|
717
766
|
_seqTracker = new SeqTracker();
|
|
718
767
|
_seqTrackerContext = null;
|
|
@@ -751,7 +800,7 @@ export class AUNClient {
|
|
|
751
800
|
_clientLog;
|
|
752
801
|
_logAuth;
|
|
753
802
|
_logTransport;
|
|
754
|
-
|
|
803
|
+
_tokenStoreLog;
|
|
755
804
|
_logDiscovery;
|
|
756
805
|
_logEvents;
|
|
757
806
|
constructor(aid) {
|
|
@@ -770,7 +819,6 @@ export class AUNClient {
|
|
|
770
819
|
root_ca_path: this.configModel.rootCaPem,
|
|
771
820
|
seed_password: this.configModel.seedPassword,
|
|
772
821
|
};
|
|
773
|
-
this._agentMdPath = this._agentMdDefaultRoot();
|
|
774
822
|
this._deviceId = (inputAid?.deviceId) || getDeviceId();
|
|
775
823
|
// Logger 必须最早初始化(其他子模块构造时通过 logger 输出)
|
|
776
824
|
this._logger = new AUNLogger({ debug: _debug, aunPath: this.configModel.aunPath });
|
|
@@ -778,18 +826,18 @@ export class AUNClient {
|
|
|
778
826
|
this._clientLog = this._logger.for('aun_core.client');
|
|
779
827
|
this._logAuth = this._logger.for('aun_core.auth');
|
|
780
828
|
this._logTransport = this._logger.for('aun_core.transport');
|
|
781
|
-
this.
|
|
829
|
+
this._tokenStoreLog = this._logger.for('aun_core.keystore');
|
|
782
830
|
this._logDiscovery = this._logger.for('aun_core.discovery');
|
|
783
831
|
this._logEvents = this._logger.for('aun_core.events');
|
|
784
832
|
this._clientLog.info(`AUNClient initialized: debug=${_debug} aunPath=${this.configModel.aunPath} aid=${initAid ?? '-'}`);
|
|
785
833
|
this._dispatcher = new EventDispatcher();
|
|
786
834
|
this._discovery = new GatewayDiscovery();
|
|
787
|
-
this.
|
|
835
|
+
this._tokenStore = new IndexedDBTokenStore();
|
|
788
836
|
this._slotId = inputAid?.slotId || 'default';
|
|
789
837
|
this._connectDeliveryMode = normalizeDeliveryModeConfig({ mode: 'fanout' });
|
|
790
838
|
this._defaultConnectDeliveryMode = { ...this._connectDeliveryMode };
|
|
791
839
|
this._auth = new AuthFlow({
|
|
792
|
-
|
|
840
|
+
tokenStore: this._tokenStore,
|
|
793
841
|
crypto: new CryptoProvider(),
|
|
794
842
|
aid: initAid,
|
|
795
843
|
deviceId: this._deviceId,
|
|
@@ -798,6 +846,24 @@ export class AUNClient {
|
|
|
798
846
|
verifySsl: this.configModel.verifySsl,
|
|
799
847
|
});
|
|
800
848
|
this._aid = initAid;
|
|
849
|
+
this._agentMdManager = createAgentMdManagerForRuntime({
|
|
850
|
+
config: () => this.configModel,
|
|
851
|
+
logger: () => this._logger.for('aun_core.agent_md'),
|
|
852
|
+
ownerAid: () => this._aid,
|
|
853
|
+
currentAid: () => this._currentAid,
|
|
854
|
+
gateway: {
|
|
855
|
+
resolve: (target) => this._resolveGatewayForAid(target),
|
|
856
|
+
get: () => this._gatewayUrl,
|
|
857
|
+
set: (gatewayUrl) => { this._gatewayUrl = gatewayUrl; },
|
|
858
|
+
},
|
|
859
|
+
identity: {
|
|
860
|
+
get: () => this._identity,
|
|
861
|
+
set: (identity) => { this._identity = identity; },
|
|
862
|
+
},
|
|
863
|
+
auth: () => this._auth,
|
|
864
|
+
tokenStore: () => this._tokenStore,
|
|
865
|
+
fetchPeerCert: (target) => this._fetchPeerCert(target),
|
|
866
|
+
});
|
|
801
867
|
this._transport = new RPCTransport({
|
|
802
868
|
eventDispatcher: this._dispatcher,
|
|
803
869
|
timeout: DEFAULT_SESSION_OPTIONS.timeouts.call,
|
|
@@ -817,6 +883,7 @@ export class AUNClient {
|
|
|
817
883
|
public_key_der_b64: inputAid.publicKey,
|
|
818
884
|
cert: inputAid.certPem,
|
|
819
885
|
};
|
|
886
|
+
this._auth.setIdentity(this._identity);
|
|
820
887
|
this._state = 'disconnected';
|
|
821
888
|
}
|
|
822
889
|
}
|
|
@@ -827,8 +894,8 @@ export class AUNClient {
|
|
|
827
894
|
if (typeof this._discovery.setLogger === 'function') {
|
|
828
895
|
this._discovery.setLogger(this._logger.for('aun_core.discovery'));
|
|
829
896
|
}
|
|
830
|
-
if (typeof this.
|
|
831
|
-
this.
|
|
897
|
+
if (typeof this._tokenStore.setLogger === 'function') {
|
|
898
|
+
this._tokenStore.setLogger(this._tokenStoreLog);
|
|
832
899
|
}
|
|
833
900
|
// 内部订阅:推送消息 re-publish 给用户(V2 加密消息走 _raw.peer.v2.message_received)
|
|
834
901
|
this._dispatcher.subscribe('_raw.message.received', (data) => {
|
|
@@ -880,664 +947,15 @@ export class AUNClient {
|
|
|
880
947
|
get aid() {
|
|
881
948
|
return this._aid;
|
|
882
949
|
}
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
this._agentMdPath = next;
|
|
886
|
-
this._agentMdCache.clear();
|
|
887
|
-
return next;
|
|
888
|
-
}
|
|
889
|
-
async _resolveAgentMdUrl(aid) {
|
|
890
|
-
const target = String(aid ?? '').trim();
|
|
891
|
-
if (!target)
|
|
892
|
-
throw new ValidationError('agent.md requires non-empty aid');
|
|
893
|
-
let gatewayUrl = String(this._gatewayUrl ?? '').trim();
|
|
894
|
-
if (!gatewayUrl) {
|
|
895
|
-
try {
|
|
896
|
-
gatewayUrl = await this._resolveGatewayForAid(target);
|
|
897
|
-
}
|
|
898
|
-
catch {
|
|
899
|
-
gatewayUrl = '';
|
|
900
|
-
}
|
|
901
|
-
}
|
|
902
|
-
const authority = agentMdAuthority(target);
|
|
903
|
-
return `${agentMdHttpScheme(gatewayUrl)}://${authority}/agent.md`;
|
|
904
|
-
}
|
|
905
|
-
async _ensureAgentMdUploadToken(aid, gatewayUrl) {
|
|
906
|
-
let identity = await this._auth.loadIdentityOrNone(aid);
|
|
907
|
-
if (!identity && this._identity && String(this._identity.aid ?? '') === aid) {
|
|
908
|
-
identity = this._identity;
|
|
909
|
-
}
|
|
910
|
-
if (!identity) {
|
|
911
|
-
throw new StateError('no local identity found, register or load an AID first');
|
|
912
|
-
}
|
|
913
|
-
const cachedToken = String(identity.access_token ?? '');
|
|
914
|
-
const expiresAt = this._auth.getAccessTokenExpiry(identity);
|
|
915
|
-
if (cachedToken && (expiresAt === null || expiresAt > Date.now() / 1000 + 30)) {
|
|
916
|
-
return cachedToken;
|
|
917
|
-
}
|
|
918
|
-
if (identity.refresh_token) {
|
|
919
|
-
try {
|
|
920
|
-
const refreshed = await this._auth.refreshCachedTokens(gatewayUrl, identity);
|
|
921
|
-
const refreshedToken = String(refreshed.access_token ?? '');
|
|
922
|
-
const refreshedExpiry = this._auth.getAccessTokenExpiry(refreshed);
|
|
923
|
-
if (refreshedToken && (refreshedExpiry === null || refreshedExpiry > Date.now() / 1000 + 30)) {
|
|
924
|
-
this._identity = refreshed;
|
|
925
|
-
return refreshedToken;
|
|
926
|
-
}
|
|
927
|
-
}
|
|
928
|
-
catch {
|
|
929
|
-
// refresh 失败时回退到完整 authenticate。
|
|
930
|
-
}
|
|
931
|
-
}
|
|
932
|
-
const result = await this._auth.authenticate(gatewayUrl, aid);
|
|
933
|
-
const token = String(result.access_token ?? '');
|
|
934
|
-
if (!token)
|
|
935
|
-
throw new StateError('authenticate did not return access_token');
|
|
936
|
-
const fallbackIdentity = {
|
|
937
|
-
...identity,
|
|
938
|
-
access_token: token,
|
|
939
|
-
refresh_token: String(result.refresh_token ?? identity.refresh_token ?? ''),
|
|
940
|
-
};
|
|
941
|
-
const fallbackExpiresAt = Number(result.expires_at ?? identity.expires_at ?? NaN);
|
|
942
|
-
if (Number.isFinite(fallbackExpiresAt))
|
|
943
|
-
fallbackIdentity.expires_at = fallbackExpiresAt;
|
|
944
|
-
this._identity = await this._auth.loadIdentityOrNone(aid) ?? fallbackIdentity;
|
|
945
|
-
return token;
|
|
946
|
-
}
|
|
947
|
-
async _uploadAgentMd(content) {
|
|
948
|
-
const target = String(this._aid ?? this._currentAid?.aid ?? '').trim();
|
|
949
|
-
if (!target)
|
|
950
|
-
throw new StateError('uploadAgentMd requires local AID');
|
|
951
|
-
const gatewayUrl = await this._resolveGatewayForAid(target);
|
|
952
|
-
this._gatewayUrl = gatewayUrl;
|
|
953
|
-
const token = await this._ensureAgentMdUploadToken(target, gatewayUrl);
|
|
954
|
-
const response = await fetchWithTimeout(await this._resolveAgentMdUrl(target), {
|
|
955
|
-
method: 'PUT',
|
|
956
|
-
headers: {
|
|
957
|
-
Authorization: `Bearer ${token}`,
|
|
958
|
-
'Content-Type': 'text/markdown; charset=utf-8',
|
|
959
|
-
},
|
|
960
|
-
body: content,
|
|
961
|
-
});
|
|
962
|
-
if (response.status === 404) {
|
|
963
|
-
throw new NotFoundError(`agent.md endpoint not found for aid: ${target}`);
|
|
964
|
-
}
|
|
965
|
-
if (!response.ok) {
|
|
966
|
-
const message = (await response.text()).trim();
|
|
967
|
-
throw new AUNError(`upload agent.md failed: HTTP ${response.status}${message ? ` - ${message}` : ''}`);
|
|
968
|
-
}
|
|
969
|
-
const payload = await response.json();
|
|
970
|
-
if (!isJsonObject(payload))
|
|
971
|
-
throw new AUNError('upload agent.md returned invalid JSON payload');
|
|
972
|
-
return payload;
|
|
973
|
-
}
|
|
974
|
-
async _downloadAgentMd(aid) {
|
|
975
|
-
const target = String(aid ?? '').trim();
|
|
976
|
-
if (!target)
|
|
977
|
-
throw new ValidationError('downloadAgentMd requires non-empty aid');
|
|
978
|
-
const cached = this._agentMdCache.get(target);
|
|
979
|
-
const url = await this._resolveAgentMdUrl(target);
|
|
980
|
-
const response = await fetchWithTimeout(url, {
|
|
981
|
-
method: 'GET',
|
|
982
|
-
headers: { Accept: 'text/markdown' },
|
|
983
|
-
redirect: 'follow',
|
|
984
|
-
});
|
|
985
|
-
if (response.status === 304 && typeof cached?.text === 'string') {
|
|
986
|
-
return String(cached.text);
|
|
987
|
-
}
|
|
988
|
-
if (response.status === 404) {
|
|
989
|
-
throw new NotFoundError(`agent.md not found for aid: ${target}`);
|
|
990
|
-
}
|
|
991
|
-
if (!response.ok) {
|
|
992
|
-
const message = (await response.text()).trim();
|
|
993
|
-
throw new AUNError(`download agent.md failed: HTTP ${response.status}${message ? ` - ${message}` : ''}`);
|
|
994
|
-
}
|
|
995
|
-
const text = await response.text();
|
|
996
|
-
const etag = String(response.headers?.get('ETag') ?? response.headers?.get('etag') ?? '').trim();
|
|
997
|
-
const lastModified = String(response.headers?.get('Last-Modified') ?? response.headers?.get('last-modified') ?? '').trim();
|
|
998
|
-
this._agentMdCache.set(target, {
|
|
999
|
-
...(cached ?? {}),
|
|
1000
|
-
text,
|
|
1001
|
-
etag,
|
|
1002
|
-
lastModified,
|
|
1003
|
-
remote_etag: etag,
|
|
1004
|
-
last_modified: lastModified,
|
|
1005
|
-
});
|
|
1006
|
-
return text;
|
|
1007
|
-
}
|
|
1008
|
-
async _headAgentMd(aid) {
|
|
1009
|
-
const target = String(aid ?? '').trim();
|
|
1010
|
-
if (!target)
|
|
1011
|
-
throw new ValidationError('headAgentMd requires non-empty aid');
|
|
1012
|
-
const response = await fetchWithTimeout(await this._resolveAgentMdUrl(target), {
|
|
1013
|
-
method: 'HEAD',
|
|
1014
|
-
headers: { Accept: 'text/markdown' },
|
|
1015
|
-
});
|
|
1016
|
-
const etag = String(response.headers?.get('ETag') ?? response.headers?.get('etag') ?? '').trim();
|
|
1017
|
-
const lastModified = String(response.headers?.get('Last-Modified') ?? response.headers?.get('last-modified') ?? '').trim();
|
|
1018
|
-
if (response.status === 404) {
|
|
1019
|
-
return { aid: target, found: false, etag: '', last_modified: '', status: 404 };
|
|
1020
|
-
}
|
|
1021
|
-
if (!response.ok) {
|
|
1022
|
-
throw new AUNError(`head agent.md failed: HTTP ${response.status}`);
|
|
1023
|
-
}
|
|
1024
|
-
const cached = this._agentMdCache.get(target) ?? {};
|
|
1025
|
-
this._agentMdCache.set(target, {
|
|
1026
|
-
...cached,
|
|
1027
|
-
etag,
|
|
1028
|
-
lastModified,
|
|
1029
|
-
remote_etag: etag,
|
|
1030
|
-
last_modified: lastModified,
|
|
1031
|
-
});
|
|
1032
|
-
return { aid: target, found: true, etag, last_modified: lastModified, status: response.status };
|
|
1033
|
-
}
|
|
1034
|
-
async _verifyAgentMd(content, aid) {
|
|
1035
|
-
const target = String(aid ?? '').trim();
|
|
1036
|
-
if (!target)
|
|
1037
|
-
throw new ValidationError('verifyAgentMd requires non-empty aid');
|
|
1038
|
-
let peer = target === this._currentAid?.aid ? this._currentAid : null;
|
|
1039
|
-
if (!peer) {
|
|
1040
|
-
let certPem = String(await this._keystore.loadCert(target) ?? '').trim();
|
|
1041
|
-
if (!certPem) {
|
|
1042
|
-
certPem = String(await this._fetchPeerCert(target) ?? '').trim();
|
|
1043
|
-
}
|
|
1044
|
-
if (!certPem)
|
|
1045
|
-
throw new NotFoundError(`certificate not found for aid: ${target}`);
|
|
1046
|
-
peer = await AID.create({
|
|
1047
|
-
aid: target,
|
|
1048
|
-
aunPath: this.configModel.aunPath,
|
|
1049
|
-
certPem,
|
|
1050
|
-
privateKeyPem: null,
|
|
1051
|
-
certValid: true,
|
|
1052
|
-
privateKeyValid: false,
|
|
1053
|
-
});
|
|
1054
|
-
}
|
|
1055
|
-
const result = await peer.verifyAgentMd(content);
|
|
1056
|
-
if (!result.ok)
|
|
1057
|
-
throw new AUNError(result.error.message);
|
|
1058
|
-
return { ...result.data, verified: result.data.status === 'verified' };
|
|
1059
|
-
}
|
|
1060
|
-
/**
|
|
1061
|
-
* 浏览器版本 publishAgentMd。默认从 {agentMdPath}/{self_aid}/agent.md 的等价 IndexedDB 正文读取,
|
|
1062
|
-
* 然后签名、上传,并刷新 agentmd.json 元数据。
|
|
1063
|
-
*
|
|
1064
|
-
* 兼容旧浏览器调用:传入 content 时会先写入等价正文,再从该正文发布。
|
|
1065
|
-
*/
|
|
1066
|
-
async publishAgentMd(content) {
|
|
1067
|
-
const target = this._agentMdOwnerAid();
|
|
1068
|
-
if (!target || !this._currentAid) {
|
|
1069
|
-
throw new ValidationError('publishAgentMd requires local AID');
|
|
1070
|
-
}
|
|
1071
|
-
if (content !== undefined && content !== null) {
|
|
1072
|
-
const text = String(content ?? '');
|
|
1073
|
-
if (text.length === 0) {
|
|
1074
|
-
throw new ValidationError('publishAgentMd requires non-empty content');
|
|
1075
|
-
}
|
|
1076
|
-
await this._saveAgentMdRecord(target, {
|
|
1077
|
-
content: text,
|
|
1078
|
-
local_etag: await this._agentMdContentEtag(text),
|
|
1079
|
-
fetched_at: Date.now(),
|
|
1080
|
-
});
|
|
1081
|
-
}
|
|
1082
|
-
const localContent = await this._readAgentMdContent(target);
|
|
1083
|
-
if (localContent === null || localContent.length === 0) {
|
|
1084
|
-
throw new ValidationError('publishAgentMd requires local agent.md content');
|
|
1085
|
-
}
|
|
1086
|
-
const signedResult = await this._currentAid?.signAgentMd(localContent);
|
|
1087
|
-
if (!signedResult?.ok) {
|
|
1088
|
-
throw new StateError(signedResult?.error.message ?? 'publishAgentMd requires a valid local AID private key');
|
|
1089
|
-
}
|
|
1090
|
-
const signed = signedResult.data.signed;
|
|
1091
|
-
const result = await this._uploadAgentMd(signed);
|
|
1092
|
-
this._localAgentMdEtag = await this._agentMdContentEtag(signed);
|
|
1093
|
-
const remoteEtag = isJsonObject(result) ? String(result.etag ?? '').trim() : '';
|
|
1094
|
-
if (remoteEtag)
|
|
1095
|
-
this._remoteAgentMdEtag = remoteEtag;
|
|
1096
|
-
await this._saveAgentMdRecord(target, {
|
|
1097
|
-
content: signed,
|
|
1098
|
-
local_etag: this._localAgentMdEtag,
|
|
1099
|
-
remote_etag: remoteEtag || undefined,
|
|
1100
|
-
last_modified: isJsonObject(result) ? String(result.last_modified ?? '').trim() : '',
|
|
1101
|
-
fetched_at: Date.now(),
|
|
1102
|
-
remote_status: remoteEtag ? 'found' : 'unknown',
|
|
1103
|
-
last_error: '',
|
|
1104
|
-
});
|
|
1105
|
-
return result;
|
|
1106
|
-
}
|
|
1107
|
-
/**
|
|
1108
|
-
* 浏览器版本 fetchAgentMd。aid 缺省时取自身;下载后的正文固定写入
|
|
1109
|
-
* {agentMdPath}/{aid}/agent.md 的等价 IndexedDB 正文,agentmd.json 只保存元数据。
|
|
1110
|
-
*/
|
|
1111
|
-
async _fetchAgentMdCache(aid) {
|
|
1112
|
-
const target = String(aid ?? this._aid ?? '').trim();
|
|
1113
|
-
if (!target) {
|
|
1114
|
-
throw new ValidationError('fetchAgentMd requires aid (or local AID)');
|
|
1115
|
-
}
|
|
1116
|
-
const content = await this._downloadAgentMd(target);
|
|
1117
|
-
const signature = await this._verifyAgentMd(content, target);
|
|
1118
|
-
const isSelf = target === (this._aid ?? '');
|
|
1119
|
-
const localEtag = await this._agentMdContentEtag(content);
|
|
1120
|
-
const cacheMeta = this._agentMdAuthCacheMeta(target);
|
|
1121
|
-
const remoteEtag = String(cacheMeta.etag ?? '').trim();
|
|
1122
|
-
const lastModified = String(cacheMeta.lastModified ?? cacheMeta.last_modified ?? '').trim();
|
|
1123
|
-
if (isSelf) {
|
|
1124
|
-
this._localAgentMdEtag = localEtag;
|
|
1125
|
-
if (remoteEtag)
|
|
1126
|
-
this._remoteAgentMdEtag = remoteEtag;
|
|
1127
|
-
}
|
|
1128
|
-
await this._saveAgentMdRecord(target, {
|
|
1129
|
-
content,
|
|
1130
|
-
local_etag: localEtag,
|
|
1131
|
-
remote_etag: remoteEtag || undefined,
|
|
1132
|
-
last_modified: lastModified || undefined,
|
|
1133
|
-
fetched_at: Date.now(),
|
|
1134
|
-
remote_status: 'found',
|
|
1135
|
-
verify_status: isJsonObject(signature) ? String(signature.status ?? '') : '',
|
|
1136
|
-
verify_error: isJsonObject(signature) ? String(signature.reason ?? '') : '',
|
|
1137
|
-
last_error: '',
|
|
1138
|
-
});
|
|
1139
|
-
let in_sync = null;
|
|
1140
|
-
if (isSelf) {
|
|
1141
|
-
const remote = remoteEtag || this._remoteAgentMdEtag || '';
|
|
1142
|
-
in_sync = localEtag && remote ? localEtag === remote : false;
|
|
1143
|
-
}
|
|
1144
|
-
return {
|
|
1145
|
-
aid: target,
|
|
1146
|
-
content,
|
|
1147
|
-
signature: signature,
|
|
1148
|
-
in_sync,
|
|
1149
|
-
};
|
|
1150
|
-
}
|
|
1151
|
-
getLocalAgentMdEtag() {
|
|
1152
|
-
return this._localAgentMdEtag;
|
|
1153
|
-
}
|
|
1154
|
-
getRemoteAgentMdEtag() {
|
|
1155
|
-
return this._remoteAgentMdEtag;
|
|
1156
|
-
}
|
|
1157
|
-
async _agentMdContentEtag(content) {
|
|
1158
|
-
const digest = await crypto.subtle.digest('SHA-256', new TextEncoder().encode(String(content ?? '')));
|
|
1159
|
-
const hex = Array.from(new Uint8Array(digest)).map((b) => b.toString(16).padStart(2, '0')).join('');
|
|
1160
|
-
return `"${hex}"`;
|
|
1161
|
-
}
|
|
1162
|
-
_agentMdOwnerAid() {
|
|
1163
|
-
return String(this._aid ?? '').trim();
|
|
1164
|
-
}
|
|
1165
|
-
_agentMdDefaultRoot() {
|
|
1166
|
-
return this._joinAgentMdPath(this.configModel.aunPath || '.', 'AIDs');
|
|
1167
|
-
}
|
|
1168
|
-
_joinAgentMdPath(base, name) {
|
|
1169
|
-
const left = String(base ?? '').trim().replace(/[\\/]+$/g, '');
|
|
1170
|
-
return left ? `${left}/${name}` : name;
|
|
1171
|
-
}
|
|
1172
|
-
_agentMdRoot() {
|
|
1173
|
-
return this._agentMdPath || this._agentMdDefaultRoot();
|
|
1174
|
-
}
|
|
1175
|
-
_agentMdSafeAid(aid) {
|
|
1176
|
-
const target = String(aid ?? '').trim();
|
|
1177
|
-
if (!target || target.includes('/') || target.includes('\\') || target.includes('\0')) {
|
|
1178
|
-
throw new ValidationError('agent.md aid is empty or contains path separators');
|
|
1179
|
-
}
|
|
1180
|
-
return target;
|
|
1181
|
-
}
|
|
1182
|
-
_agentMdMetaKey(aid) {
|
|
1183
|
-
return `${this._agentMdSafeAid(aid)}/agentmd.json`;
|
|
1184
|
-
}
|
|
1185
|
-
_agentMdContentKey(aid) {
|
|
1186
|
-
return `${this._agentMdSafeAid(aid)}/agent.md`;
|
|
1187
|
-
}
|
|
1188
|
-
async _readAgentMdStorage(logicalKey) {
|
|
1189
|
-
const key = String(logicalKey ?? '').trim();
|
|
1190
|
-
if (!key)
|
|
1191
|
-
return null;
|
|
1192
|
-
const load = this._keystore.loadAgentMdCache;
|
|
1193
|
-
if (typeof load !== 'function') {
|
|
1194
|
-
throw new Error('IndexedDB agent.md storage unavailable');
|
|
1195
|
-
}
|
|
1196
|
-
const record = await load.call(this._keystore, this._agentMdRoot(), key);
|
|
1197
|
-
if (record && Object.prototype.hasOwnProperty.call(record, 'content')) {
|
|
1198
|
-
return String(record.content ?? '');
|
|
1199
|
-
}
|
|
1200
|
-
return null;
|
|
1201
|
-
}
|
|
1202
|
-
async _writeAgentMdStorage(logicalKey, content) {
|
|
1203
|
-
const key = String(logicalKey ?? '').trim();
|
|
1204
|
-
if (!key)
|
|
1205
|
-
return;
|
|
1206
|
-
const save = this._keystore.upsertAgentMdCache;
|
|
1207
|
-
if (typeof save !== 'function') {
|
|
1208
|
-
throw new Error('IndexedDB agent.md storage unavailable');
|
|
1209
|
-
}
|
|
1210
|
-
const text = String(content ?? '');
|
|
1211
|
-
await save.call(this._keystore, this._agentMdRoot(), key, {
|
|
1212
|
-
content: text,
|
|
1213
|
-
local_etag: await this._agentMdContentEtag(text),
|
|
1214
|
-
fetched_at: Date.now(),
|
|
1215
|
-
});
|
|
1216
|
-
}
|
|
1217
|
-
async _withAgentMdLock(fn) {
|
|
1218
|
-
const previous = this._agentMdLock.catch(() => undefined);
|
|
1219
|
-
let release;
|
|
1220
|
-
const current = new Promise((resolve) => { release = resolve; });
|
|
1221
|
-
this._agentMdLock = previous.then(() => current);
|
|
1222
|
-
await previous;
|
|
1223
|
-
try {
|
|
1224
|
-
return await fn();
|
|
1225
|
-
}
|
|
1226
|
-
finally {
|
|
1227
|
-
release();
|
|
1228
|
-
}
|
|
1229
|
-
}
|
|
1230
|
-
_normalizeAgentMdRecord(aid, data) {
|
|
1231
|
-
if (!isJsonObject(data))
|
|
1232
|
-
return {};
|
|
1233
|
-
const record = {};
|
|
1234
|
-
for (const [key, value] of Object.entries(data)) {
|
|
1235
|
-
if (key !== 'content')
|
|
1236
|
-
record[key] = value;
|
|
1237
|
-
}
|
|
1238
|
-
record.aid = this._agentMdSafeAid(String(record.aid ?? aid));
|
|
1239
|
-
for (const key of ['fetched_at', 'observed_at', 'checked_at', 'updated_at']) {
|
|
1240
|
-
record[key] = Number(record[key] ?? 0) || 0;
|
|
1241
|
-
}
|
|
1242
|
-
return record;
|
|
1243
|
-
}
|
|
1244
|
-
async _writeAgentMdRecordUnlocked(aid, record) {
|
|
1245
|
-
const payload = {};
|
|
1246
|
-
for (const [key, value] of Object.entries(record)) {
|
|
1247
|
-
if (key !== 'content' && value !== undefined && value !== null)
|
|
1248
|
-
payload[key] = value;
|
|
1249
|
-
}
|
|
1250
|
-
payload.aid = this._agentMdSafeAid(aid);
|
|
1251
|
-
await this._writeAgentMdStorage(this._agentMdMetaKey(aid), `${JSON.stringify(payload, null, 2)}\n`);
|
|
1252
|
-
}
|
|
1253
|
-
async _readAgentMdRecordUnlocked(aid) {
|
|
1254
|
-
const raw = await this._readAgentMdStorage(this._agentMdMetaKey(aid));
|
|
1255
|
-
if (raw === null)
|
|
1256
|
-
return {};
|
|
1257
|
-
try {
|
|
1258
|
-
return this._normalizeAgentMdRecord(aid, JSON.parse(raw));
|
|
1259
|
-
}
|
|
1260
|
-
catch (err) {
|
|
1261
|
-
this._clientLog.warn(`agent.md metadata damaged, ignoring: aid=${aid} err=${err instanceof Error ? err.message : String(err)}`);
|
|
1262
|
-
return {};
|
|
1263
|
-
}
|
|
1264
|
-
}
|
|
1265
|
-
async _readAgentMdContent(aid) {
|
|
1266
|
-
return await this._readAgentMdStorage(this._agentMdContentKey(aid));
|
|
1267
|
-
}
|
|
1268
|
-
async _writeAgentMdContent(aid, content) {
|
|
1269
|
-
await this._writeAgentMdStorage(this._agentMdContentKey(aid), String(content ?? ''));
|
|
1270
|
-
}
|
|
1271
|
-
_agentMdAuthCacheMeta(aid) {
|
|
1272
|
-
try {
|
|
1273
|
-
const record = this._agentMdCache.get(String(aid ?? '').trim());
|
|
1274
|
-
return record && typeof record === 'object' ? { ...record } : {};
|
|
1275
|
-
}
|
|
1276
|
-
catch {
|
|
1277
|
-
return {};
|
|
1278
|
-
}
|
|
1279
|
-
}
|
|
1280
|
-
async _loadAgentMdRecord(aid) {
|
|
1281
|
-
const target = String(aid ?? '').trim();
|
|
1282
|
-
if (!target)
|
|
1283
|
-
return null;
|
|
1284
|
-
try {
|
|
1285
|
-
const loaded = await this._withAgentMdLock(async () => {
|
|
1286
|
-
const record = await this._readAgentMdRecordUnlocked(target);
|
|
1287
|
-
const next = Object.keys(record).length > 0 ? { ...record, aid: target } : { aid: target };
|
|
1288
|
-
try {
|
|
1289
|
-
const content = await this._readAgentMdContent(target);
|
|
1290
|
-
if (content !== null) {
|
|
1291
|
-
next.content = content;
|
|
1292
|
-
next.local_etag = await this._agentMdContentEtag(content);
|
|
1293
|
-
}
|
|
1294
|
-
else {
|
|
1295
|
-
// 元数据存在但正文缺失
|
|
1296
|
-
const metaRaw = await this._readAgentMdStorage(this._agentMdMetaKey(target));
|
|
1297
|
-
if (metaRaw !== null) {
|
|
1298
|
-
this._clientLog.warn(`agent.md content read failed: aid=${target}`);
|
|
1299
|
-
}
|
|
1300
|
-
}
|
|
1301
|
-
}
|
|
1302
|
-
catch (err) {
|
|
1303
|
-
this._clientLog.warn(`agent.md content read failed: aid=${target} err=${err instanceof Error ? err.message : String(err)}`);
|
|
1304
|
-
}
|
|
1305
|
-
return next;
|
|
1306
|
-
});
|
|
1307
|
-
if (Object.keys(loaded).length <= 1)
|
|
1308
|
-
return null;
|
|
1309
|
-
this._agentMdCache.set(target, { ...loaded });
|
|
1310
|
-
return { ...loaded };
|
|
1311
|
-
}
|
|
1312
|
-
catch (err) {
|
|
1313
|
-
this._clientLog.debug(`agent.md cache load skipped: aid=${target} err=${err instanceof Error ? err.message : String(err)}`);
|
|
1314
|
-
}
|
|
1315
|
-
return null;
|
|
1316
|
-
}
|
|
1317
|
-
async _saveAgentMdRecord(aid, fields) {
|
|
1318
|
-
const target = String(aid ?? '').trim();
|
|
1319
|
-
if (!target)
|
|
1320
|
-
return {};
|
|
1321
|
-
try {
|
|
1322
|
-
const inputFields = { ...fields };
|
|
1323
|
-
const hasContent = Object.prototype.hasOwnProperty.call(inputFields, 'content') && inputFields.content !== undefined && inputFields.content !== null;
|
|
1324
|
-
if (hasContent) {
|
|
1325
|
-
const text = String(inputFields.content ?? '');
|
|
1326
|
-
await this._writeAgentMdContent(target, text);
|
|
1327
|
-
if (!inputFields.local_etag)
|
|
1328
|
-
inputFields.local_etag = await this._agentMdContentEtag(text);
|
|
1329
|
-
if (!inputFields.fetched_at)
|
|
1330
|
-
inputFields.fetched_at = Date.now();
|
|
1331
|
-
}
|
|
1332
|
-
delete inputFields.content;
|
|
1333
|
-
const record = await this._withAgentMdLock(async () => {
|
|
1334
|
-
const next = { ...(await this._readAgentMdRecordUnlocked(target)), aid: target };
|
|
1335
|
-
for (const [key, value] of Object.entries(inputFields)) {
|
|
1336
|
-
if (value !== undefined && value !== null)
|
|
1337
|
-
next[key] = value;
|
|
1338
|
-
}
|
|
1339
|
-
next.updated_at = Date.now();
|
|
1340
|
-
await this._writeAgentMdRecordUnlocked(target, next);
|
|
1341
|
-
return next;
|
|
1342
|
-
});
|
|
1343
|
-
const loaded = { ...record };
|
|
1344
|
-
if (hasContent)
|
|
1345
|
-
loaded.content = String(fields.content ?? '');
|
|
1346
|
-
this._agentMdCache.set(target, { ...loaded });
|
|
1347
|
-
const owner = this._agentMdOwnerAid();
|
|
1348
|
-
if (target === owner) {
|
|
1349
|
-
const localEtag = String(loaded.local_etag ?? '').trim();
|
|
1350
|
-
const remoteEtag = String(loaded.remote_etag ?? '').trim();
|
|
1351
|
-
if (localEtag)
|
|
1352
|
-
this._localAgentMdEtag = localEtag;
|
|
1353
|
-
if (remoteEtag)
|
|
1354
|
-
this._remoteAgentMdEtag = remoteEtag;
|
|
1355
|
-
}
|
|
1356
|
-
return { ...loaded };
|
|
1357
|
-
}
|
|
1358
|
-
catch (err) {
|
|
1359
|
-
this._clientLog.debug(`agent.md cache save skipped: aid=${target} err=${err instanceof Error ? err.message : String(err)}`);
|
|
1360
|
-
}
|
|
1361
|
-
return {};
|
|
1362
|
-
}
|
|
1363
|
-
async _agentMdHasLocalContent(aid, record) {
|
|
1364
|
-
if (record && typeof record.content === 'string' && record.content.length > 0)
|
|
1365
|
-
return true;
|
|
1366
|
-
try {
|
|
1367
|
-
return (await this._readAgentMdContent(aid)) !== null;
|
|
1368
|
-
}
|
|
1369
|
-
catch {
|
|
1370
|
-
return false;
|
|
1371
|
-
}
|
|
1372
|
-
}
|
|
1373
|
-
_agentMdCheckedAtFresh(checkedAtMs, maxUnsyncedDays) {
|
|
1374
|
-
const days = Number(maxUnsyncedDays || 0);
|
|
1375
|
-
if (!Number.isFinite(days) || days <= 0)
|
|
1376
|
-
return false;
|
|
1377
|
-
if (!Number.isFinite(checkedAtMs) || checkedAtMs <= 0)
|
|
1378
|
-
return false;
|
|
1379
|
-
return Date.now() - checkedAtMs <= days * 86400000;
|
|
1380
|
-
}
|
|
1381
|
-
_agentMdLastModifiedFresh(lastModified, maxUnsyncedDays) {
|
|
1382
|
-
const days = Number(maxUnsyncedDays || 0);
|
|
1383
|
-
if (!Number.isFinite(days) || days <= 0)
|
|
1384
|
-
return false;
|
|
1385
|
-
const ts = Date.parse(String(lastModified ?? '').trim());
|
|
1386
|
-
if (!Number.isFinite(ts))
|
|
1387
|
-
return false;
|
|
1388
|
-
return Date.now() <= ts + days * 86400000;
|
|
1389
|
-
}
|
|
1390
|
-
async _scheduleAgentMdFetchIfMissing(aid, record, source = '') {
|
|
1391
|
-
const target = String(aid ?? '').trim();
|
|
1392
|
-
if (!target || await this._agentMdHasLocalContent(target, record))
|
|
1393
|
-
return;
|
|
1394
|
-
if (this._agentMdFetchInflight.has(target))
|
|
1395
|
-
return;
|
|
1396
|
-
this._agentMdFetchInflight.add(target);
|
|
1397
|
-
try {
|
|
1398
|
-
await this._fetchAgentMdCache(target);
|
|
1399
|
-
}
|
|
1400
|
-
catch (err) {
|
|
1401
|
-
await this._saveAgentMdRecord(target, {
|
|
1402
|
-
last_error: err instanceof Error ? err.message : String(err),
|
|
1403
|
-
remote_status: 'found',
|
|
1404
|
-
});
|
|
1405
|
-
this._clientLog.debug(`agent.md auto fetch failed: aid=${target} source=${source || '-'} err=${err instanceof Error ? err.message : String(err)}`);
|
|
1406
|
-
}
|
|
1407
|
-
finally {
|
|
1408
|
-
this._agentMdFetchInflight.delete(target);
|
|
1409
|
-
}
|
|
1410
|
-
}
|
|
1411
|
-
async _observeAgentMdMeta(aid, etag = '', lastModified = '', source = '') {
|
|
1412
|
-
const target = String(aid ?? '').trim();
|
|
1413
|
-
const remoteEtag = String(etag ?? '').trim();
|
|
1414
|
-
const remoteLastModified = String(lastModified ?? '').trim();
|
|
1415
|
-
if (!target || (!remoteEtag && !remoteLastModified))
|
|
1416
|
-
return;
|
|
1417
|
-
let before = this._agentMdCache.get(target);
|
|
1418
|
-
if (!before || typeof before !== 'object')
|
|
1419
|
-
before = await this._loadAgentMdRecord(target) ?? {};
|
|
1420
|
-
const same = (!remoteEtag || String(before.remote_etag ?? '').trim() === remoteEtag) &&
|
|
1421
|
-
(!remoteLastModified || String(before.last_modified ?? '').trim() === remoteLastModified);
|
|
1422
|
-
let record = { ...before };
|
|
1423
|
-
if (!same || Object.keys(before).length === 0) {
|
|
1424
|
-
const fields = {
|
|
1425
|
-
observed_at: Date.now(),
|
|
1426
|
-
remote_status: 'found',
|
|
1427
|
-
};
|
|
1428
|
-
if (remoteEtag)
|
|
1429
|
-
fields.remote_etag = remoteEtag;
|
|
1430
|
-
if (remoteLastModified)
|
|
1431
|
-
fields.last_modified = remoteLastModified;
|
|
1432
|
-
record = await this._saveAgentMdRecord(target, fields) || record;
|
|
1433
|
-
}
|
|
1434
|
-
if (target === this._agentMdOwnerAid() && remoteEtag)
|
|
1435
|
-
this._remoteAgentMdEtag = remoteEtag;
|
|
1436
|
-
await this._scheduleAgentMdFetchIfMissing(target, record, source);
|
|
1437
|
-
this._clientLog.debug(`agent.md meta observed: aid=${target} etag=${remoteEtag || '-'} last_modified=${remoteLastModified || '-'} source=${source || '-'}`);
|
|
1438
|
-
}
|
|
1439
|
-
async _observeAgentMdEtag(aid, etag, source = '') {
|
|
1440
|
-
await this._observeAgentMdMeta(aid, etag, '', source);
|
|
950
|
+
async uploadAgentMd(content) {
|
|
951
|
+
return await this._agentMdManager.upload(content);
|
|
1441
952
|
}
|
|
1442
953
|
async _observeAgentMdFromEnvelope(envelope) {
|
|
1443
|
-
|
|
1444
|
-
return;
|
|
1445
|
-
const env = envelope;
|
|
1446
|
-
if (!isJsonObject(env.agent_md))
|
|
1447
|
-
return;
|
|
1448
|
-
const agentMd = env.agent_md;
|
|
1449
|
-
if (!isJsonObject(agentMd.sender))
|
|
1450
|
-
return;
|
|
1451
|
-
const sender = agentMd.sender;
|
|
1452
|
-
let senderAid = String(sender.aid ?? '').trim();
|
|
1453
|
-
if (!senderAid) {
|
|
1454
|
-
const aad = isJsonObject(env.aad) ? env.aad : {};
|
|
1455
|
-
senderAid = String(aad.from ?? env.from ?? '').trim();
|
|
1456
|
-
}
|
|
1457
|
-
await this._observeAgentMdMeta(senderAid, String(sender.etag ?? '').trim(), String(sender.last_modified ?? sender.lastModified ?? '').trim(), 'envelope');
|
|
1458
|
-
}
|
|
1459
|
-
async _checkAgentMdCache(aid, maxUnsyncedDays = 0) {
|
|
1460
|
-
const target = String(aid ?? this._aid ?? '').trim();
|
|
1461
|
-
if (!target)
|
|
1462
|
-
throw new ValidationError('checkAgentMd requires aid (or local AID)');
|
|
1463
|
-
const before = await this._loadAgentMdRecord(target) ?? {};
|
|
1464
|
-
const localEtag = String(before.local_etag ?? '').trim();
|
|
1465
|
-
const localFound = !!(Object.keys(before).length > 0 && (String(before.content ?? '') || localEtag));
|
|
1466
|
-
const remoteEtagCached = String(before.remote_etag ?? '').trim();
|
|
1467
|
-
const lastModifiedCached = String(before.last_modified ?? '').trim();
|
|
1468
|
-
const checkedAtCached = Number(before.checked_at ?? 0);
|
|
1469
|
-
const cachedInSync = !!(localFound && localEtag && remoteEtagCached && localEtag === remoteEtagCached);
|
|
1470
|
-
// max_unsynced_days > 0 且距上次 HEAD 在窗口内 → 直接返回缓存;否则强制 HEAD。
|
|
1471
|
-
if (cachedInSync && this._agentMdCheckedAtFresh(checkedAtCached, maxUnsyncedDays)) {
|
|
1472
|
-
return {
|
|
1473
|
-
aid: target,
|
|
1474
|
-
local_found: true,
|
|
1475
|
-
remote_found: true,
|
|
1476
|
-
local_etag: localEtag,
|
|
1477
|
-
remote_etag: remoteEtagCached,
|
|
1478
|
-
in_sync: true,
|
|
1479
|
-
last_modified: lastModifiedCached,
|
|
1480
|
-
status: 200,
|
|
1481
|
-
cached: true,
|
|
1482
|
-
verify_status: String(before.verify_status ?? ''),
|
|
1483
|
-
verify_error: String(before.verify_error ?? ''),
|
|
1484
|
-
};
|
|
1485
|
-
}
|
|
1486
|
-
const now = Date.now();
|
|
1487
|
-
let remote;
|
|
1488
|
-
try {
|
|
1489
|
-
remote = await this._headAgentMd(target);
|
|
1490
|
-
}
|
|
1491
|
-
catch (err) {
|
|
1492
|
-
await this._saveAgentMdRecord(target, { checked_at: now, remote_status: 'error', last_error: err instanceof Error ? err.message : String(err) });
|
|
1493
|
-
throw err;
|
|
1494
|
-
}
|
|
1495
|
-
const remoteFound = !!remote.found;
|
|
1496
|
-
const remoteEtag = String(remote.etag ?? '').trim();
|
|
1497
|
-
const lastModified = String(remote.last_modified ?? remote.lastModified ?? '').trim();
|
|
1498
|
-
const saved = await this._saveAgentMdRecord(target, {
|
|
1499
|
-
remote_etag: remoteFound ? remoteEtag : '',
|
|
1500
|
-
last_modified: lastModified,
|
|
1501
|
-
checked_at: now,
|
|
1502
|
-
remote_status: remoteFound ? 'found' : 'missing',
|
|
1503
|
-
last_error: '',
|
|
1504
|
-
});
|
|
1505
|
-
if (target === this._agentMdOwnerAid() && remoteEtag)
|
|
1506
|
-
this._remoteAgentMdEtag = remoteEtag;
|
|
1507
|
-
const inSync = !!(localFound && remoteFound && localEtag && remoteEtag && localEtag === remoteEtag);
|
|
1508
|
-
return {
|
|
1509
|
-
aid: target,
|
|
1510
|
-
local_found: localFound,
|
|
1511
|
-
remote_found: remoteFound,
|
|
1512
|
-
local_etag: localEtag,
|
|
1513
|
-
remote_etag: remoteEtag,
|
|
1514
|
-
in_sync: inSync,
|
|
1515
|
-
last_modified: lastModified,
|
|
1516
|
-
status: Number(remote.status ?? (remoteFound ? 200 : 404)),
|
|
1517
|
-
cached: false,
|
|
1518
|
-
verify_status: String(saved.verify_status ?? before.verify_status ?? ''),
|
|
1519
|
-
verify_error: String(saved.verify_error ?? before.verify_error ?? ''),
|
|
1520
|
-
};
|
|
954
|
+
await this._agentMdManager.observeEnvelope(envelope);
|
|
1521
955
|
}
|
|
1522
956
|
/** transport 的 meta observer:吸收 gateway 注入的 _meta 字段。失败不影响业务。 */
|
|
1523
957
|
async _observeRpcMeta(meta) {
|
|
1524
|
-
|
|
1525
|
-
return;
|
|
1526
|
-
const etag = String(meta.agent_md_etag ?? '').trim();
|
|
1527
|
-
if (etag) {
|
|
1528
|
-
this._remoteAgentMdEtag = etag;
|
|
1529
|
-
await this._observeAgentMdMeta(this._aid ?? '', etag, '', 'rpc.self');
|
|
1530
|
-
}
|
|
1531
|
-
const etags = meta.agent_md_etags;
|
|
1532
|
-
if (isJsonObject(etags)) {
|
|
1533
|
-
// role key 优先级:requester / peer 是新规范,其余是兼容旧 SDK 的别名。
|
|
1534
|
-
for (const key of ['requester', 'peer', 'receiver', 'target', 'to', 'sender', 'from']) {
|
|
1535
|
-
const item = etags[key];
|
|
1536
|
-
if (!isJsonObject(item))
|
|
1537
|
-
continue;
|
|
1538
|
-
await this._observeAgentMdMeta(String(item.aid ?? ''), String(item.etag ?? ''), String(item.last_modified ?? item.lastModified ?? ''), `rpc.${key}`);
|
|
1539
|
-
}
|
|
1540
|
-
}
|
|
958
|
+
await this._agentMdManager.observeRpcMeta(meta, this._aid);
|
|
1541
959
|
}
|
|
1542
960
|
get state() {
|
|
1543
961
|
return this._publicState(this._state);
|
|
@@ -1597,9 +1015,6 @@ export class AUNClient {
|
|
|
1597
1015
|
this.config.aun_path = nextConfig.aunPath;
|
|
1598
1016
|
this.config.root_ca_path = nextConfig.rootCaPem;
|
|
1599
1017
|
this.config.seed_password = nextConfig.seedPassword;
|
|
1600
|
-
this._agentMdPath = this._agentMdDefaultRoot();
|
|
1601
|
-
this._agentMdCache.clear();
|
|
1602
|
-
this._agentMdFetchInflight.clear();
|
|
1603
1018
|
this._peerCache.clear();
|
|
1604
1019
|
this._certCache.clear();
|
|
1605
1020
|
this._gatewayUrl = null;
|
|
@@ -1610,13 +1025,13 @@ export class AUNClient {
|
|
|
1610
1025
|
this._clientLog = this._logger.for('aun_core.client');
|
|
1611
1026
|
this._logAuth = this._logger.for('aun_core.auth');
|
|
1612
1027
|
this._logTransport = this._logger.for('aun_core.transport');
|
|
1613
|
-
this.
|
|
1028
|
+
this._tokenStoreLog = this._logger.for('aun_core.keystore');
|
|
1614
1029
|
this._logDiscovery = this._logger.for('aun_core.discovery');
|
|
1615
1030
|
this._logEvents = this._logger.for('aun_core.events');
|
|
1616
1031
|
this._discovery = new GatewayDiscovery();
|
|
1617
|
-
this.
|
|
1032
|
+
this._tokenStore = new IndexedDBTokenStore();
|
|
1618
1033
|
this._auth = new AuthFlow({
|
|
1619
|
-
|
|
1034
|
+
tokenStore: this._tokenStore,
|
|
1620
1035
|
crypto: new CryptoProvider(),
|
|
1621
1036
|
aid: aid.aid,
|
|
1622
1037
|
deviceId: this._deviceId,
|
|
@@ -1624,6 +1039,24 @@ export class AUNClient {
|
|
|
1624
1039
|
rootCaPem: nextConfig.rootCaPem,
|
|
1625
1040
|
verifySsl: nextConfig.verifySsl,
|
|
1626
1041
|
});
|
|
1042
|
+
this._agentMdManager = createAgentMdManagerForRuntime({
|
|
1043
|
+
config: () => this.configModel,
|
|
1044
|
+
logger: () => this._logger.for('aun_core.agent_md'),
|
|
1045
|
+
ownerAid: () => this._aid,
|
|
1046
|
+
currentAid: () => this._currentAid,
|
|
1047
|
+
gateway: {
|
|
1048
|
+
resolve: (target) => this._resolveGatewayForAid(target),
|
|
1049
|
+
get: () => this._gatewayUrl,
|
|
1050
|
+
set: (gatewayUrl) => { this._gatewayUrl = gatewayUrl; },
|
|
1051
|
+
},
|
|
1052
|
+
identity: {
|
|
1053
|
+
get: () => this._identity,
|
|
1054
|
+
set: (identity) => { this._identity = identity; },
|
|
1055
|
+
},
|
|
1056
|
+
auth: () => this._auth,
|
|
1057
|
+
tokenStore: () => this._tokenStore,
|
|
1058
|
+
fetchPeerCert: (target) => this._fetchPeerCert(target),
|
|
1059
|
+
});
|
|
1627
1060
|
this._transport = new RPCTransport({
|
|
1628
1061
|
eventDispatcher: this._dispatcher,
|
|
1629
1062
|
timeout: DEFAULT_SESSION_OPTIONS.timeouts.call,
|
|
@@ -1640,8 +1073,8 @@ export class AUNClient {
|
|
|
1640
1073
|
if (typeof this._discovery.setLogger === 'function') {
|
|
1641
1074
|
this._discovery.setLogger(this._logDiscovery);
|
|
1642
1075
|
}
|
|
1643
|
-
if (typeof this.
|
|
1644
|
-
this.
|
|
1076
|
+
if (typeof this._tokenStore.setLogger === 'function') {
|
|
1077
|
+
this._tokenStore.setLogger(this._tokenStoreLog);
|
|
1645
1078
|
}
|
|
1646
1079
|
}
|
|
1647
1080
|
loadIdentity(aid) {
|
|
@@ -1660,6 +1093,8 @@ export class AUNClient {
|
|
|
1660
1093
|
public_key_der_b64: aid.publicKey,
|
|
1661
1094
|
cert: aid.certPem,
|
|
1662
1095
|
};
|
|
1096
|
+
// 注入内存私钥到 AuthFlow,禁止 AuthFlow 内部再走 keystore 解密
|
|
1097
|
+
this._auth.setIdentity(this._identity);
|
|
1663
1098
|
this._state = 'disconnected';
|
|
1664
1099
|
this._closing = false;
|
|
1665
1100
|
}
|
|
@@ -2756,8 +2191,9 @@ export class AUNClient {
|
|
|
2756
2191
|
// 注入本地/远端 agent.md etag,让应用层判断版本一致性;失败不影响业务。
|
|
2757
2192
|
if (isJsonObject(payload)) {
|
|
2758
2193
|
try {
|
|
2759
|
-
const
|
|
2760
|
-
const
|
|
2194
|
+
const snapshot = this._agentMdManager.eventSnapshot();
|
|
2195
|
+
const localEtag = snapshot?.local_etag || '';
|
|
2196
|
+
const remoteEtag = snapshot?.remote_etag || '';
|
|
2761
2197
|
if ((localEtag || remoteEtag) && payload._agent_md === undefined) {
|
|
2762
2198
|
payload._agent_md = {
|
|
2763
2199
|
local_etag: localEtag,
|
|
@@ -3048,9 +2484,9 @@ export class AUNClient {
|
|
|
3048
2484
|
const membershipSnapshot = String(d.membership_snapshot ?? '').trim();
|
|
3049
2485
|
const policySnapshot = String(d.policy_snapshot ?? '').trim();
|
|
3050
2486
|
// 1. 验证 prev_state_hash 连续性
|
|
3051
|
-
const loadFn = this.
|
|
2487
|
+
const loadFn = this._tokenStore.loadGroupState;
|
|
3052
2488
|
const localState = loadFn
|
|
3053
|
-
? await loadFn.call(this.
|
|
2489
|
+
? await loadFn.call(this._tokenStore, groupId)
|
|
3054
2490
|
: null;
|
|
3055
2491
|
if (localState && localState.state_hash && localState.state_hash !== prevStateHash) {
|
|
3056
2492
|
this._clientLog.warn('[aun_core] state_hash 链不连续 group=%s local_sv=%d event_sv=%d', groupId, localState.state_version, stateVersion);
|
|
@@ -3077,9 +2513,9 @@ export class AUNClient {
|
|
|
3077
2513
|
return;
|
|
3078
2514
|
}
|
|
3079
2515
|
}
|
|
3080
|
-
const saveFn = this.
|
|
2516
|
+
const saveFn = this._tokenStore.saveGroupState;
|
|
3081
2517
|
if (saveFn) {
|
|
3082
|
-
await saveFn.call(this.
|
|
2518
|
+
await saveFn.call(this._tokenStore, groupId, {
|
|
3083
2519
|
group_id: groupId,
|
|
3084
2520
|
state_version: sv,
|
|
3085
2521
|
state_hash: sHash,
|
|
@@ -3108,9 +2544,9 @@ export class AUNClient {
|
|
|
3108
2544
|
return;
|
|
3109
2545
|
}
|
|
3110
2546
|
// 3. 更新本地存储
|
|
3111
|
-
const saveFn = this.
|
|
2547
|
+
const saveFn = this._tokenStore.saveGroupState;
|
|
3112
2548
|
if (saveFn) {
|
|
3113
|
-
await saveFn.call(this.
|
|
2549
|
+
await saveFn.call(this._tokenStore, groupId, {
|
|
3114
2550
|
group_id: groupId,
|
|
3115
2551
|
state_version: stateVersion,
|
|
3116
2552
|
state_hash: stateHash,
|
|
@@ -3543,7 +2979,7 @@ export class AUNClient {
|
|
|
3543
2979
|
});
|
|
3544
2980
|
try {
|
|
3545
2981
|
// peer 证书只存版本目录,不覆盖 cert.pem
|
|
3546
|
-
await this.
|
|
2982
|
+
await this._tokenStore.saveCert(aid, certPem, certFingerprint, { makeActive: false });
|
|
3547
2983
|
}
|
|
3548
2984
|
catch (exc) {
|
|
3549
2985
|
this._clientLog.error(`write cert to keystore failed (aid=${aid}): ${String(exc)}`, exc instanceof Error ? exc : undefined);
|
|
@@ -3563,7 +2999,7 @@ export class AUNClient {
|
|
|
3563
2999
|
const now = Date.now() / 1000;
|
|
3564
3000
|
if (cached && now < cached.refreshAfter)
|
|
3565
3001
|
return true;
|
|
3566
|
-
const localCert = await this.
|
|
3002
|
+
const localCert = await this._tokenStore.loadCert(aid, certFingerprint);
|
|
3567
3003
|
if (localCert) {
|
|
3568
3004
|
if (certFingerprint) {
|
|
3569
3005
|
const actualFingerprint = await this._certFingerprint(localCert);
|
|
@@ -3588,7 +3024,7 @@ export class AUNClient {
|
|
|
3588
3024
|
try {
|
|
3589
3025
|
const certPem = await this._fetchPeerCert(aid, certFingerprint);
|
|
3590
3026
|
// peer 证书只存版本目录,不覆盖 cert.pem
|
|
3591
|
-
await this.
|
|
3027
|
+
await this._tokenStore.saveCert(aid, certPem, certFingerprint, { makeActive: false });
|
|
3592
3028
|
return true;
|
|
3593
3029
|
}
|
|
3594
3030
|
catch (exc) {
|
|
@@ -3786,9 +3222,9 @@ export class AUNClient {
|
|
|
3786
3222
|
if (this._gatewayUrl)
|
|
3787
3223
|
return this._gatewayUrl;
|
|
3788
3224
|
try {
|
|
3789
|
-
const getMetadata = this.
|
|
3225
|
+
const getMetadata = this._tokenStore.getMetadata;
|
|
3790
3226
|
const raw = typeof getMetadata === 'function'
|
|
3791
|
-
? String(await getMetadata.call(this.
|
|
3227
|
+
? String(await getMetadata.call(this._tokenStore, target, 'gateway_url') ?? '').trim()
|
|
3792
3228
|
: '';
|
|
3793
3229
|
if (raw) {
|
|
3794
3230
|
const gateway = raw.startsWith('"') && raw.endsWith('"') ? String(JSON.parse(raw)).trim() : raw;
|
|
@@ -3814,9 +3250,9 @@ export class AUNClient {
|
|
|
3814
3250
|
const gateway = await this._discovery.discover(url);
|
|
3815
3251
|
this._gatewayUrl = gateway;
|
|
3816
3252
|
try {
|
|
3817
|
-
const setMetadata = this.
|
|
3253
|
+
const setMetadata = this._tokenStore.setMetadata;
|
|
3818
3254
|
if (typeof setMetadata === 'function') {
|
|
3819
|
-
await setMetadata.call(this.
|
|
3255
|
+
await setMetadata.call(this._tokenStore, target, 'gateway_url', gateway);
|
|
3820
3256
|
}
|
|
3821
3257
|
}
|
|
3822
3258
|
catch {
|
|
@@ -3853,13 +3289,8 @@ export class AUNClient {
|
|
|
3853
3289
|
return [gateway];
|
|
3854
3290
|
}
|
|
3855
3291
|
async _syncIdentityAfterConnect(accessToken) {
|
|
3856
|
-
|
|
3857
|
-
try {
|
|
3858
|
-
identity = await this._auth.loadIdentityOrNone(this._aid ?? undefined);
|
|
3859
|
-
}
|
|
3860
|
-
catch { /* 忽略 */ }
|
|
3292
|
+
const identity = this._identity;
|
|
3861
3293
|
if (!identity) {
|
|
3862
|
-
this._identity = null;
|
|
3863
3294
|
return;
|
|
3864
3295
|
}
|
|
3865
3296
|
identity.access_token = accessToken;
|
|
@@ -3870,9 +3301,6 @@ export class AUNClient {
|
|
|
3870
3301
|
if (typeof persistIdentity === 'function') {
|
|
3871
3302
|
await persistIdentity.call(this._auth, identity);
|
|
3872
3303
|
}
|
|
3873
|
-
else {
|
|
3874
|
-
await this._keystore.saveIdentity(String(identity.aid), identity);
|
|
3875
|
-
}
|
|
3876
3304
|
}
|
|
3877
3305
|
}
|
|
3878
3306
|
// ── 内部:参数处理 ────────────────────────────────
|
|
@@ -4213,8 +3641,8 @@ export class AUNClient {
|
|
|
4213
3641
|
if ('device_id' in params && String(params.device_id ?? '').trim() !== this._deviceId) {
|
|
4214
3642
|
throw new ValidationError('message.pull/message.ack device_id must match the current client instance');
|
|
4215
3643
|
}
|
|
4216
|
-
const slotId =
|
|
4217
|
-
if (slotId !== this._slotId) {
|
|
3644
|
+
const slotId = normalizeSlotId(params.slot_id ?? this._slotId, this._slotId);
|
|
3645
|
+
if (slotIsolationKey(slotId) !== slotIsolationKey(this._slotId)) {
|
|
4218
3646
|
throw new ValidationError('message.pull/message.ack slot_id must match the current client instance');
|
|
4219
3647
|
}
|
|
4220
3648
|
params.device_id = this._deviceId;
|
|
@@ -4382,8 +3810,7 @@ export class AUNClient {
|
|
|
4382
3810
|
}
|
|
4383
3811
|
// ── Named Group(命名群)高层 API ────────────────────────────
|
|
4384
3812
|
/**
|
|
4385
|
-
*
|
|
4386
|
-
* 服务端签发群 AID 证书,返回后将证书和私钥存入 keystore。
|
|
3813
|
+
* 创建命名群:群/P2P 私钥由 V2 数据库存储,不写入 AID 身份私钥存储。
|
|
4387
3814
|
*/
|
|
4388
3815
|
async createNamedGroup(groupName, opts = {}) {
|
|
4389
3816
|
const tStart = Date.now();
|
|
@@ -4403,15 +3830,10 @@ export class AUNClient {
|
|
|
4403
3830
|
const aidCert = result?.aid_cert;
|
|
4404
3831
|
const groupAid = String(groupInfo?.group_aid ?? '');
|
|
4405
3832
|
if (groupAid && aidCert) {
|
|
4406
|
-
await this.
|
|
4407
|
-
private_key_pem: identity.private_key_pem,
|
|
4408
|
-
public_key: identity.public_key_der_b64,
|
|
4409
|
-
curve: 'P-256',
|
|
4410
|
-
type: 'group_identity',
|
|
4411
|
-
});
|
|
3833
|
+
await this._saveGroupIdentityToV2(groupAid, identity);
|
|
4412
3834
|
const certPem = String(aidCert.cert ?? '');
|
|
4413
3835
|
if (certPem) {
|
|
4414
|
-
await this.
|
|
3836
|
+
await this._tokenStore.saveCert(groupAid, certPem);
|
|
4415
3837
|
}
|
|
4416
3838
|
}
|
|
4417
3839
|
this._clientLog.debug(`createNamedGroup exit: elapsed=${Date.now() - tStart}ms group_aid=${groupAid}`);
|
|
@@ -4442,15 +3864,10 @@ export class AUNClient {
|
|
|
4442
3864
|
const aidCert = result?.aid_cert;
|
|
4443
3865
|
const groupAid = String(groupInfo?.group_aid ?? '');
|
|
4444
3866
|
if (groupAid && aidCert) {
|
|
4445
|
-
await this.
|
|
4446
|
-
private_key_pem: identity.private_key_pem,
|
|
4447
|
-
public_key: identity.public_key_der_b64,
|
|
4448
|
-
curve: 'P-256',
|
|
4449
|
-
type: 'group_identity',
|
|
4450
|
-
});
|
|
3867
|
+
await this._saveGroupIdentityToV2(groupAid, identity);
|
|
4451
3868
|
const certPem = String(aidCert.cert ?? '');
|
|
4452
3869
|
if (certPem) {
|
|
4453
|
-
await this.
|
|
3870
|
+
await this._tokenStore.saveCert(groupAid, certPem);
|
|
4454
3871
|
}
|
|
4455
3872
|
}
|
|
4456
3873
|
this._clientLog.debug(`bindGroupAid exit: elapsed=${Date.now() - tStart}ms group_aid=${groupAid}`);
|
|
@@ -4494,7 +3911,7 @@ export class AUNClient {
|
|
|
4494
3911
|
const slotId = this._slotId;
|
|
4495
3912
|
try {
|
|
4496
3913
|
// 优先从 seq_tracker 表按行读取
|
|
4497
|
-
const loadAll = this.
|
|
3914
|
+
const loadAll = this._tokenStore.loadAllSeqs?.bind(this._tokenStore);
|
|
4498
3915
|
if (typeof loadAll === 'function') {
|
|
4499
3916
|
let state = await loadAll(aid, deviceId, slotId);
|
|
4500
3917
|
if (this._seqTrackerContext !== context)
|
|
@@ -4506,7 +3923,7 @@ export class AUNClient {
|
|
|
4506
3923
|
return;
|
|
4507
3924
|
}
|
|
4508
3925
|
// fallback: 从旧 instance_state JSON blob 恢复
|
|
4509
|
-
const loader = this.
|
|
3926
|
+
const loader = this._tokenStore.loadInstanceState?.bind(this._tokenStore);
|
|
4510
3927
|
if (typeof loader !== 'function')
|
|
4511
3928
|
return;
|
|
4512
3929
|
const stateHolder = await loader(aid, deviceId, slotId);
|
|
@@ -4564,8 +3981,8 @@ export class AUNClient {
|
|
|
4564
3981
|
const aid = this._aid;
|
|
4565
3982
|
const deviceId = this._deviceId;
|
|
4566
3983
|
const slotId = this._slotId;
|
|
4567
|
-
const saver = this.
|
|
4568
|
-
const deleter = this.
|
|
3984
|
+
const saver = this._tokenStore.saveSeq?.bind(this._tokenStore);
|
|
3985
|
+
const deleter = this._tokenStore.deleteSeq?.bind(this._tokenStore);
|
|
4569
3986
|
if (typeof saver === 'function') {
|
|
4570
3987
|
for (const [oldNs, newNs] of Object.entries(renameMap)) {
|
|
4571
3988
|
if (typeof deleter === 'function') {
|
|
@@ -4629,7 +4046,7 @@ export class AUNClient {
|
|
|
4629
4046
|
return;
|
|
4630
4047
|
try {
|
|
4631
4048
|
// 优先按行写入 seq_tracker 表
|
|
4632
|
-
const saveFn = this.
|
|
4049
|
+
const saveFn = this._tokenStore.saveSeq?.bind(this._tokenStore);
|
|
4633
4050
|
if (typeof saveFn === 'function') {
|
|
4634
4051
|
for (const [ns, seq] of Object.entries(state)) {
|
|
4635
4052
|
saveFn(this._aid, this._deviceId, this._slotId, ns, seq).catch((exc) => {
|
|
@@ -4645,8 +4062,8 @@ export class AUNClient {
|
|
|
4645
4062
|
return;
|
|
4646
4063
|
}
|
|
4647
4064
|
// fallback: 旧版 updateInstanceState JSON blob
|
|
4648
|
-
if (typeof this.
|
|
4649
|
-
this.
|
|
4065
|
+
if (typeof this._tokenStore.updateInstanceState === 'function') {
|
|
4066
|
+
this._tokenStore.updateInstanceState(this._aid, this._deviceId, this._slotId, (current) => {
|
|
4650
4067
|
current.seq_tracker_state = state;
|
|
4651
4068
|
return current;
|
|
4652
4069
|
}).catch((exc) => {
|
|
@@ -4675,15 +4092,15 @@ export class AUNClient {
|
|
|
4675
4092
|
return;
|
|
4676
4093
|
const seq = this._seqTracker.getContiguousSeq(ns);
|
|
4677
4094
|
try {
|
|
4678
|
-
if (seq > 0 && typeof this.
|
|
4679
|
-
this.
|
|
4095
|
+
if (seq > 0 && typeof this._tokenStore.saveSeq === 'function') {
|
|
4096
|
+
this._tokenStore.saveSeq(this._aid, this._deviceId, this._slotId, ns, seq).catch((exc) => {
|
|
4680
4097
|
this._clientLog.debug(`persist repaired seq failed: ns=${ns} err=${formatCaughtError(exc)}`);
|
|
4681
4098
|
});
|
|
4682
4099
|
return;
|
|
4683
4100
|
}
|
|
4684
|
-
const deleteSeq = this.
|
|
4101
|
+
const deleteSeq = this._tokenStore.deleteSeq;
|
|
4685
4102
|
if (seq <= 0 && typeof deleteSeq === 'function') {
|
|
4686
|
-
deleteSeq.call(this.
|
|
4103
|
+
deleteSeq.call(this._tokenStore, this._aid, this._deviceId, this._slotId, ns).catch((exc) => {
|
|
4687
4104
|
this._clientLog.debug(`delete repaired seq failed: ns=${ns} err=${formatCaughtError(exc)}`);
|
|
4688
4105
|
});
|
|
4689
4106
|
return;
|
|
@@ -4735,7 +4152,7 @@ export class AUNClient {
|
|
|
4735
4152
|
async _initV2Session() {
|
|
4736
4153
|
if (!this._aid)
|
|
4737
4154
|
return;
|
|
4738
|
-
//
|
|
4155
|
+
// 私钥来自当前 AID 值对象,AUNClient 不从持久化存储读取私钥。
|
|
4739
4156
|
const currentAid = this._currentAid;
|
|
4740
4157
|
if (!currentAid?.privateKeyPem) {
|
|
4741
4158
|
this._clientLog.warn('V2 session init skipped: no AID private key');
|
|
@@ -4776,6 +4193,20 @@ export class AUNClient {
|
|
|
4776
4193
|
// 上线时自动确认 pending state proposals
|
|
4777
4194
|
this._safeAsync(this._v2AutoConfirmPendingProposals());
|
|
4778
4195
|
}
|
|
4196
|
+
_v2StoreDeviceId() {
|
|
4197
|
+
return `aid:${encodeURIComponent(String(this._aid ?? ''))}|device:${encodeURIComponent(String(this._deviceId ?? ''))}`;
|
|
4198
|
+
}
|
|
4199
|
+
async _saveGroupIdentityToV2(groupAid, identity) {
|
|
4200
|
+
const privateKeyPem = String(identity.private_key_pem ?? '').trim();
|
|
4201
|
+
const publicKeyDerB64 = String(identity.public_key_der_b64 ?? '').trim();
|
|
4202
|
+
if (!groupAid || !privateKeyPem || !publicKeyDerB64) {
|
|
4203
|
+
throw new StateError('group identity is incomplete');
|
|
4204
|
+
}
|
|
4205
|
+
if (!this._v2KeyStore) {
|
|
4206
|
+
this._v2KeyStore = await V2KeyStore.open();
|
|
4207
|
+
}
|
|
4208
|
+
await this._v2KeyStore.saveGroupIdentity(this._v2StoreDeviceId(), groupAid, privateKeyPem, base64ToUint8(publicKeyDerB64));
|
|
4209
|
+
}
|
|
4779
4210
|
async _v2TrustedIKPubDer(aid) {
|
|
4780
4211
|
const normalizedAid = String(aid ?? '').trim();
|
|
4781
4212
|
if (!normalizedAid)
|