@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.
Files changed (73) hide show
  1. package/CHANGELOG.md +41 -0
  2. package/_packed_docs/CHANGELOG.md +41 -0
  3. package/_packed_docs/INDEX.md +2 -2
  4. package/_packed_docs/KITE_DOCS_GUIDE.md +1 -1
  5. 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
  6. package/_packed_docs/sdk/01-/345/277/253/351/200/237/345/274/200/345/247/213.md +16 -15
  7. package/_packed_docs/sdk/02-WebSocket/345/215/217/350/256/256.md +2 -2
  8. package/_packed_docs/sdk/03-/346/240/270/345/277/203/346/246/202/345/277/265.md +22 -5
  9. package/_packed_docs/sdk/04-/350/277/236/346/216/245/344/270/216/350/256/244/350/257/201.md +42 -26
  10. package/_packed_docs/sdk/05-E2EE/345/212/240/345/257/206/351/200/232/344/277/241.md +2 -2
  11. package/_packed_docs/sdk/06-API/346/211/213/345/206/214.md +61 -35
  12. package/_packed_docs/sdk/08-/346/234/200/344/275/263/345/256/236/350/267/265.md +3 -3
  13. package/_packed_docs/sdk/09-message-rpc-manual.md +6 -6
  14. package/_packed_docs/sdk/AUN_DOCS_GUIDE.md +6 -4
  15. package/_packed_docs/sdk/INDEX.md +2 -2
  16. package/_packed_docs/sdk/README.md +3 -3
  17. package/dist/agent-md.d.ts +111 -0
  18. package/dist/agent-md.d.ts.map +1 -0
  19. package/dist/agent-md.js +656 -0
  20. package/dist/agent-md.js.map +1 -0
  21. package/dist/aid-store.d.ts +8 -40
  22. package/dist/aid-store.d.ts.map +1 -1
  23. package/dist/aid-store.js +89 -172
  24. package/dist/aid-store.js.map +1 -1
  25. package/dist/auth.d.ts +8 -13
  26. package/dist/auth.d.ts.map +1 -1
  27. package/dist/auth.js +37 -130
  28. package/dist/auth.js.map +1 -1
  29. package/dist/bundle.js +6540 -6033
  30. package/dist/client.d.ts +8 -65
  31. package/dist/client.d.ts.map +1 -1
  32. package/dist/client.js +193 -762
  33. package/dist/client.js.map +1 -1
  34. package/dist/index.d.ts +4 -2
  35. package/dist/index.d.ts.map +1 -1
  36. package/dist/index.js +3 -1
  37. package/dist/index.js.map +1 -1
  38. package/dist/keystore/index.d.ts +49 -22
  39. package/dist/keystore/index.d.ts.map +1 -1
  40. package/dist/keystore/index.js +6 -1
  41. package/dist/keystore/index.js.map +1 -1
  42. package/dist/keystore/indexeddb-identity-store.d.ts +59 -0
  43. package/dist/keystore/indexeddb-identity-store.d.ts.map +1 -0
  44. package/dist/keystore/indexeddb-identity-store.js +489 -0
  45. package/dist/keystore/indexeddb-identity-store.js.map +1 -0
  46. package/dist/keystore/indexeddb-shared.d.ts +76 -0
  47. package/dist/keystore/indexeddb-shared.d.ts.map +1 -0
  48. package/dist/keystore/indexeddb-shared.js +382 -0
  49. package/dist/keystore/indexeddb-shared.js.map +1 -0
  50. package/dist/keystore/indexeddb-token-store.d.ts +119 -0
  51. package/dist/keystore/indexeddb-token-store.d.ts.map +1 -0
  52. package/dist/keystore/indexeddb-token-store.js +1086 -0
  53. package/dist/keystore/indexeddb-token-store.js.map +1 -0
  54. package/dist/keystore/indexeddb.d.ts +11 -1
  55. package/dist/keystore/indexeddb.d.ts.map +1 -1
  56. package/dist/keystore/indexeddb.js +167 -18
  57. package/dist/keystore/indexeddb.js.map +1 -1
  58. package/dist/register-flow.d.ts +53 -0
  59. package/dist/register-flow.d.ts.map +1 -0
  60. package/dist/register-flow.js +401 -0
  61. package/dist/register-flow.js.map +1 -0
  62. package/dist/v2/session/keystore.d.ts +5 -0
  63. package/dist/v2/session/keystore.d.ts.map +1 -1
  64. package/dist/v2/session/keystore.js +29 -0
  65. package/dist/v2/session/keystore.js.map +1 -1
  66. package/dist/version.d.ts +1 -1
  67. package/dist/version.js +1 -1
  68. package/package.json +1 -1
  69. 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
  70. 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
  71. 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
  72. 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
  73. 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, normalizeInstanceId, normalizeSlotId, slotIsolationKey } from './config.js';
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 { IndexedDBKeyStore } from './keystore/indexeddb.js';
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 agentMdHttpScheme(gatewayUrl) {
276
- const raw = String(gatewayUrl ?? '').trim().toLowerCase();
277
- return raw.startsWith('ws://') ? 'http' : 'https';
278
- }
279
- function agentMdAuthority(aid) {
280
- return String(aid ?? '').trim();
281
- }
282
- async function fetchWithTimeout(input, init, timeoutMs = AGENT_MD_HTTP_TIMEOUT_MS) {
283
- const controller = new AbortController();
284
- const timer = globalThis.setTimeout(() => controller.abort(), timeoutMs);
285
- try {
286
- return await fetch(input, { ...init, signal: controller.signal });
287
- }
288
- catch (error) {
289
- if (controller.signal.aborted) {
290
- throw new AUNError(`agent.md request timed out after ${timeoutMs}ms`);
291
- }
292
- throw error;
293
- }
294
- finally {
295
- globalThis.clearTimeout(timer);
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
- _keystore;
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
- * 本地 agent.md 内容对应的 etag(quoted sha256 hex,与服务端 _agent_md_etag 一致)。
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
- _logKeystore;
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._logKeystore = this._logger.for('aun_core.keystore');
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._keystore = new IndexedDBKeyStore({});
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
- keystore: this._keystore,
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._keystore.setLogger === 'function') {
831
- this._keystore.setLogger(this._logKeystore);
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
- _setAgentMdRoot(root) {
884
- const next = String(root ?? '').trim() || this._agentMdDefaultRoot();
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
- if (!isJsonObject(envelope))
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
- if (!isJsonObject(meta))
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._logKeystore = this._logger.for('aun_core.keystore');
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._keystore = new IndexedDBKeyStore({});
1032
+ this._tokenStore = new IndexedDBTokenStore();
1618
1033
  this._auth = new AuthFlow({
1619
- keystore: this._keystore,
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._keystore.setLogger === 'function') {
1644
- this._keystore.setLogger(this._logKeystore);
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 localEtag = this._localAgentMdEtag || '';
2760
- const remoteEtag = this._remoteAgentMdEtag || '';
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._keystore.loadGroupState;
2487
+ const loadFn = this._tokenStore.loadGroupState;
3052
2488
  const localState = loadFn
3053
- ? await loadFn.call(this._keystore, groupId)
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._keystore.saveGroupState;
2516
+ const saveFn = this._tokenStore.saveGroupState;
3081
2517
  if (saveFn) {
3082
- await saveFn.call(this._keystore, groupId, {
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._keystore.saveGroupState;
2547
+ const saveFn = this._tokenStore.saveGroupState;
3112
2548
  if (saveFn) {
3113
- await saveFn.call(this._keystore, groupId, {
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._keystore.saveCert(aid, certPem, certFingerprint, { makeActive: false });
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._keystore.loadCert(aid, certFingerprint);
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._keystore.saveCert(aid, certPem, certFingerprint, { makeActive: false });
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._keystore.getMetadata;
3225
+ const getMetadata = this._tokenStore.getMetadata;
3790
3226
  const raw = typeof getMetadata === 'function'
3791
- ? String(await getMetadata.call(this._keystore, target, 'gateway_url') ?? '').trim()
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._keystore.setMetadata;
3253
+ const setMetadata = this._tokenStore.setMetadata;
3818
3254
  if (typeof setMetadata === 'function') {
3819
- await setMetadata.call(this._keystore, target, 'gateway_url', gateway);
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
- let identity = null;
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 = normalizeInstanceId(params.slot_id ?? this._slotId, 'slot_id', { allowEmpty: true });
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
- * 创建命名群:本地生成 P-256 keypair,调用 group.create 传入 public_key,
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._keystore.saveIdentity(groupAid, {
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._keystore.saveCert(groupAid, certPem);
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._keystore.saveIdentity(groupAid, {
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._keystore.saveCert(groupAid, certPem);
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._keystore.loadAllSeqs?.bind(this._keystore);
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._keystore.loadInstanceState?.bind(this._keystore);
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._keystore.saveSeq?.bind(this._keystore);
4568
- const deleter = this._keystore.deleteSeq?.bind(this._keystore);
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._keystore.saveSeq?.bind(this._keystore);
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._keystore.updateInstanceState === 'function') {
4649
- this._keystore.updateInstanceState(this._aid, this._deviceId, this._slotId, (current) => {
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._keystore.saveSeq === 'function') {
4679
- this._keystore.saveSeq(this._aid, this._deviceId, this._slotId, ns, seq).catch((exc) => {
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._keystore.deleteSeq;
4101
+ const deleteSeq = this._tokenStore.deleteSeq;
4685
4102
  if (seq <= 0 && typeof deleteSeq === 'function') {
4686
- deleteSeq.call(this._keystore, this._aid, this._deviceId, this._slotId, ns).catch((exc) => {
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
- // 私钥由 AIDStore 管理,直接从 _currentAid 读取明文私钥
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)