@agentunion/fastaun-browser 0.3.6 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (69) hide show
  1. package/_packed_docs/AUN_SDK_/351/207/215/346/236/204/345/256/236/346/226/275/350/256/241/345/210/222.md +596 -0
  2. package/_packed_docs/AUN_SDK_/351/207/215/346/236/204/350/256/276/350/256/241/346/226/271/346/241/210_v3.md +1633 -0
  3. package/_packed_docs/INDEX.md +17 -11
  4. package/_packed_docs/KITE_DOCS_GUIDE.md +11 -10
  5. package/_packed_docs/sdk/01-/345/277/253/351/200/237/345/274/200/345/247/213.md +134 -158
  6. package/_packed_docs/sdk/02-WebSocket/345/215/217/350/256/256.md +11 -7
  7. package/_packed_docs/sdk/03-/346/240/270/345/277/203/346/246/202/345/277/265.md +98 -119
  8. package/_packed_docs/sdk/04-/350/277/236/346/216/245/344/270/216/350/256/244/350/257/201.md +147 -374
  9. package/_packed_docs/sdk/05-E2EE/345/212/240/345/257/206/351/200/232/344/277/241.md +153 -153
  10. package/_packed_docs/sdk/06-API/346/211/213/345/206/214.md +163 -1383
  11. package/_packed_docs/sdk/07-/351/224/231/350/257/257/345/244/204/347/220/206.md +71 -91
  12. package/_packed_docs/sdk/08-/346/234/200/344/275/263/345/256/236/350/267/265.md +76 -63
  13. package/_packed_docs/sdk/09-custody-api-manual.md +7 -6
  14. package/_packed_docs/sdk/09-meta-rpc-manual.md +13 -14
  15. package/_packed_docs/sdk/AUN_DOCS_GUIDE.md +37 -49
  16. package/_packed_docs/sdk/INDEX.md +72 -98
  17. package/_packed_docs/sdk/README.md +85 -266
  18. package/dist/aid-store.d.ts +64 -0
  19. package/dist/aid-store.d.ts.map +1 -0
  20. package/dist/aid-store.js +855 -0
  21. package/dist/aid-store.js.map +1 -0
  22. package/dist/aid.d.ts +50 -0
  23. package/dist/aid.d.ts.map +1 -0
  24. package/dist/aid.js +106 -0
  25. package/dist/aid.js.map +1 -0
  26. package/dist/auth.js +1 -1
  27. package/dist/auth.js.map +1 -1
  28. package/dist/bundle.js +1626 -1885
  29. package/dist/cert-utils.d.ts +26 -0
  30. package/dist/cert-utils.d.ts.map +1 -0
  31. package/dist/cert-utils.js +221 -0
  32. package/dist/cert-utils.js.map +1 -0
  33. package/dist/client.d.ts +89 -60
  34. package/dist/client.d.ts.map +1 -1
  35. package/dist/client.js +558 -154
  36. package/dist/client.js.map +1 -1
  37. package/dist/error-codes.d.ts +25 -0
  38. package/dist/error-codes.d.ts.map +1 -0
  39. package/dist/error-codes.js +31 -0
  40. package/dist/error-codes.js.map +1 -0
  41. package/dist/errors.d.ts +4 -0
  42. package/dist/errors.d.ts.map +1 -1
  43. package/dist/errors.js +4 -0
  44. package/dist/errors.js.map +1 -1
  45. package/dist/index.d.ts +6 -6
  46. package/dist/index.d.ts.map +1 -1
  47. package/dist/index.js +5 -5
  48. package/dist/index.js.map +1 -1
  49. package/dist/keystore/index.d.ts +1 -1
  50. package/dist/keystore/index.d.ts.map +1 -1
  51. package/dist/result.d.ts +19 -0
  52. package/dist/result.d.ts.map +1 -0
  53. package/dist/result.js +10 -0
  54. package/dist/result.js.map +1 -0
  55. package/dist/transport.d.ts +3 -0
  56. package/dist/transport.d.ts.map +1 -1
  57. package/dist/transport.js +16 -1
  58. package/dist/transport.js.map +1 -1
  59. package/dist/types.d.ts +13 -2
  60. package/dist/types.d.ts.map +1 -1
  61. package/dist/types.js +22 -0
  62. package/dist/types.js.map +1 -1
  63. package/dist/v2/e2ee/encrypt-p2p.js +1 -1
  64. package/dist/v2/e2ee/encrypt-p2p.js.map +1 -1
  65. package/dist/version.d.ts +2 -0
  66. package/dist/version.d.ts.map +1 -0
  67. package/dist/version.js +5 -0
  68. package/dist/version.js.map +1 -0
  69. package/package.json +1 -1
package/dist/client.js CHANGED
@@ -11,9 +11,6 @@ import { GatewayDiscovery } from './discovery.js';
11
11
  import { RPCTransport } from './transport.js';
12
12
  import { AuthFlow } from './auth.js';
13
13
  import { SeqTracker } from './seq-tracker.js';
14
- import { AuthNamespace } from './namespaces/auth.js';
15
- import { CustodyNamespace } from './namespaces/custody.js';
16
- import { MetaNamespace } from './namespaces/meta.js';
17
14
  import { CryptoProvider, uint8ToBase64, base64ToUint8, pemToArrayBuffer, p1363ToDer, certificateSha256Fingerprint, ecdsaSignDer, ecdsaVerifyDer, importCertPublicKeyEcdsa, importPrivateKeyEcdsa, } from './crypto.js';
18
15
  import { IndexedDBKeyStore } from './keystore/indexeddb.js';
19
16
  import { V2Session, V2KeyStore } from './v2/session/index.js';
@@ -21,8 +18,9 @@ import { encryptP2PMessage, encryptGroupMessage, decryptMessage, } from './v2/e2
21
18
  import { ecdsaVerifyRaw } from './v2/crypto/ecdsa.js';
22
19
  import { computeStateCommitment } from './v2/state/index.js';
23
20
  import { AUNLogger } from './logger.js';
24
- import { AUNError, AuthError, ConnectionError, E2EEError, PermissionError, StateError, ValidationError, } from './errors.js';
25
- import { isJsonObject, } from './types.js';
21
+ import { AUNError, AuthError, ConnectionError, E2EEError, NotFoundError, PermissionError, StateError, ValidationError, } from './errors.js';
22
+ import { isJsonObject, ConnectionState, STATE_TO_PUBLIC, } from './types.js';
23
+ import { AID } from './aid.js';
26
24
  /**
27
25
  * 递归排序键的 JSON 序列化(Canonical JSON for AUN)
28
26
  * 等价于 Python json.dumps(sort_keys=True, separators=(",",":"), ensure_ascii=False)
@@ -171,6 +169,12 @@ const DEFAULT_SESSION_OPTIONS = {
171
169
  http: 30.0,
172
170
  },
173
171
  };
172
+ const PROTECTED_HEADERS_METHODS = new Set([
173
+ 'message.send',
174
+ 'group.send',
175
+ 'message.thought.put',
176
+ 'group.thought.put',
177
+ ]);
174
178
  const RECONNECT_MIN_BASE_DELAY_SECONDS = 1.0;
175
179
  const RECONNECT_MAX_BASE_DELAY_SECONDS = 64.0;
176
180
  const TOKEN_REFRESH_CHECK_INTERVAL_MS = 30_000;
@@ -213,6 +217,7 @@ function reconnectSleepDelaySeconds(baseDelay, maxBaseDelay) {
213
217
  /** 对端证书缓存 TTL(秒) */
214
218
  const PEER_CERT_CACHE_TTL = 3600;
215
219
  const PEER_PREKEYS_CACHE_TTL = 3600;
220
+ const AGENT_MD_HTTP_TIMEOUT_MS = 30_000;
216
221
  /**
217
222
  * 将 WebSocket URL 转为对应的 HTTP URL
218
223
  */
@@ -245,6 +250,34 @@ function buildCertUrl(gatewayUrl, aid, certFingerprint) {
245
250
  }
246
251
  return url.toString();
247
252
  }
253
+ function agentMdHttpScheme(gatewayUrl) {
254
+ const raw = String(gatewayUrl ?? '').trim().toLowerCase();
255
+ return raw.startsWith('ws://') ? 'http' : 'https';
256
+ }
257
+ function agentMdAuthority(aid, discoveryPort) {
258
+ const host = String(aid ?? '').trim();
259
+ if (!host)
260
+ return '';
261
+ if (discoveryPort && !host.includes(':'))
262
+ return `${host}:${discoveryPort}`;
263
+ return host;
264
+ }
265
+ async function fetchWithTimeout(input, init, timeoutMs = AGENT_MD_HTTP_TIMEOUT_MS) {
266
+ const controller = new AbortController();
267
+ const timer = globalThis.setTimeout(() => controller.abort(), timeoutMs);
268
+ try {
269
+ return await fetch(input, { ...init, signal: controller.signal });
270
+ }
271
+ catch (error) {
272
+ if (controller.signal.aborted) {
273
+ throw new AUNError(`agent.md request timed out after ${timeoutMs}ms`);
274
+ }
275
+ throw error;
276
+ }
277
+ finally {
278
+ globalThis.clearTimeout(timer);
279
+ }
280
+ }
248
281
  /**
249
282
  * 跨域时将 Gateway URL 替换为 peer 所在域的 Gateway URL。
250
283
  *
@@ -581,6 +614,22 @@ function normalizeDeliveryModeConfig(raw, opts = {}) {
581
614
  affinity_ttl_ms: affinityTtlMs,
582
615
  };
583
616
  }
617
+ function assertClientOptions(value, label) {
618
+ if (value == null)
619
+ return;
620
+ if (typeof value !== 'object' || Array.isArray(value) || value instanceof AID) {
621
+ throw new ValidationError(`${label} must be an options object`);
622
+ }
623
+ }
624
+ function clientOptionsConfig(options) {
625
+ const raw = { ...(options ?? {}) };
626
+ if (Object.prototype.hasOwnProperty.call(raw, 'aid')) {
627
+ throw new ValidationError('AUNClient options must not include aid; pass an AID object as the first argument');
628
+ }
629
+ delete raw.debug;
630
+ delete raw.protected_headers;
631
+ return raw;
632
+ }
584
633
  /**
585
634
  * AUN Core SDK 客户端 — 浏览器版本。
586
635
  *
@@ -601,6 +650,8 @@ export class AUNClient {
601
650
  _aid = null;
602
651
  _identity = null;
603
652
  _state = 'idle';
653
+ _currentAid = null;
654
+ _instanceProtectedHeaders = null;
604
655
  _gatewayUrl = null;
605
656
  _deviceId;
606
657
  _slotId;
@@ -615,12 +666,6 @@ export class AUNClient {
615
666
  _keystore;
616
667
  _auth;
617
668
  _transport;
618
- /** 认证命名空间 */
619
- auth;
620
- /** AID 托管命名空间 */
621
- custody;
622
- /** 元数据命名空间(心跳、状态、信任根管理) */
623
- meta;
624
669
  // E2EE 编排状态(内存缓存)
625
670
  _certCache = new Map();
626
671
  // 后台任务 handle(浏览器 setInterval/setTimeout)
@@ -661,7 +706,7 @@ export class AUNClient {
661
706
  _localAgentMdEtag = '';
662
707
  /** gateway 在 RPC envelope._meta.agent_md_etag 注入的服务端 etag;纯观察,无下游依赖。 */
663
708
  _remoteAgentMdEtag = '';
664
- /** 浏览器侧 AgentMDs 逻辑根目录,正文映射到 IndexedDB 里的 {aid}/agent.md。 */
709
+ /** 浏览器侧 AIDs 逻辑根目录,正文映射到 IndexedDB 里的 {aid}/agent.md。 */
665
710
  _agentMdPath = '';
666
711
  _agentMdCache = new Map();
667
712
  _agentMdFetchInflight = new Set();
@@ -686,6 +731,14 @@ export class AUNClient {
686
731
  _reconnectActive = false;
687
732
  _reconnectAbort = null;
688
733
  _serverKicked = false;
734
+ // 重连状态追踪(对齐 Python client.py)
735
+ _nextRetryAt = null;
736
+ _retryAttempt = 0;
737
+ _retryMaxAttempts = 0;
738
+ _lastError = null;
739
+ _lastErrorCode = null;
740
+ /** 对端 AID 缓存(aid string → AID 对象) */
741
+ _peerCache = new Map();
689
742
  /**
690
743
  * 缓存最近一次服务端 gateway.disconnect 信息(含 code/reason/detail),
691
744
  * 让后续 connection.state(terminal_failed) 也能携带 detail(如配额超限信息)。
@@ -699,17 +752,32 @@ export class AUNClient {
699
752
  _logKeystore;
700
753
  _logDiscovery;
701
754
  _logEvents;
702
- constructor(config, _debug = false) {
703
- const rawConfig = config ?? {};
755
+ constructor(first, second) {
756
+ if (typeof first === 'string') {
757
+ throw new ValidationError('AUNClient aid must be an AID object, not a string');
758
+ }
759
+ if (typeof second === 'boolean') {
760
+ throw new ValidationError('AUNClient debug must be passed as options.debug');
761
+ }
762
+ const inputAid = first instanceof AID ? first : null;
763
+ if (!inputAid && second !== undefined) {
764
+ throw new ValidationError('AUNClient options-only construction accepts a single options object');
765
+ }
766
+ const options = inputAid ? (second ?? {}) : (first ?? {});
767
+ assertClientOptions(options, 'AUNClient options');
768
+ const rawConfig = clientOptionsConfig(options);
769
+ if (inputAid)
770
+ rawConfig.aun_path = inputAid.aunPath;
771
+ const _debug = !!options?.debug;
704
772
  this.configModel = createConfig(rawConfig);
705
- const initAid = String(rawConfig.aid ?? '').trim() || null;
773
+ const initAid = inputAid ? inputAid.aid : null;
706
774
  this.config = {
707
775
  aun_path: this.configModel.aunPath,
708
776
  root_ca_path: this.configModel.rootCaPem,
709
777
  seed_password: this.configModel.seedPassword,
710
778
  };
711
779
  this._agentMdPath = this._agentMdDefaultRoot();
712
- this._deviceId = getDeviceId();
780
+ this._deviceId = (inputAid?.deviceId) || getDeviceId();
713
781
  // Logger 必须最早初始化(其他子模块构造时通过 logger 输出)
714
782
  this._logger = new AUNLogger({ debug: _debug, aunPath: this.configModel.aunPath });
715
783
  this._logger.bindDeviceId(this._deviceId);
@@ -723,7 +791,7 @@ export class AUNClient {
723
791
  this._dispatcher = new EventDispatcher();
724
792
  this._discovery = new GatewayDiscovery();
725
793
  this._keystore = new IndexedDBKeyStore({ encryptionSeed: this.configModel.seedPassword ?? undefined });
726
- this._slotId = '';
794
+ this._slotId = inputAid?.slotId || 'default';
727
795
  this._connectDeliveryMode = normalizeDeliveryModeConfig({ mode: 'fanout' });
728
796
  this._defaultConnectDeliveryMode = { ...this._connectDeliveryMode };
729
797
  this._auth = new AuthFlow({
@@ -746,9 +814,21 @@ export class AUNClient {
746
814
  this._clientLog.debug(`agent.md meta observer skipped: ${String(exc)}`);
747
815
  });
748
816
  });
749
- this.auth = new AuthNamespace(this);
750
- this.custody = new CustodyNamespace(this);
751
- this.meta = new MetaNamespace(this);
817
+ if (inputAid) {
818
+ if (!inputAid.isPrivateKeyValid())
819
+ throw new StateError('AUNClient requires an AID with a valid private key');
820
+ this._currentAid = inputAid;
821
+ this._identity = {
822
+ aid: inputAid.aid,
823
+ private_key_pem: inputAid._privateKeyPem ?? '',
824
+ public_key_der_b64: inputAid.publicKey,
825
+ cert: inputAid.certPem,
826
+ };
827
+ this._state = 'disconnected';
828
+ }
829
+ if (options?.protected_headers !== undefined) {
830
+ this.setProtectedHeaders(options.protected_headers);
831
+ }
752
832
  // 注入 logger 到各子模块(构造时未传 logger,构造后通过 setLogger 注入)
753
833
  this._auth.setLogger(this._logAuth);
754
834
  this._transport.setLogger(this._logTransport);
@@ -756,18 +836,9 @@ export class AUNClient {
756
836
  if (typeof this._discovery.setLogger === 'function') {
757
837
  this._discovery.setLogger(this._logger.for('aun_core.discovery'));
758
838
  }
759
- if (typeof this.auth.setLogger === 'function') {
760
- this.auth.setLogger(this._logger.for('aun_core.namespace.auth'));
761
- }
762
- if (typeof this.custody.setLogger === 'function') {
763
- this.custody.setLogger(this._logger.for('aun_core.namespace.custody'));
764
- }
765
839
  if (typeof this._keystore.setLogger === 'function') {
766
840
  this._keystore.setLogger(this._logKeystore);
767
841
  }
768
- if (typeof this.meta.setLogger === 'function') {
769
- this.meta.setLogger(this._logger.for('aun_core.namespace.meta'));
770
- }
771
842
  // 内部订阅:推送消息 re-publish 给用户(V2 加密消息走 _raw.peer.v2.message_received)
772
843
  this._dispatcher.subscribe('_raw.message.received', (data) => {
773
844
  this._onRawMessageReceived(data);
@@ -818,17 +889,182 @@ export class AUNClient {
818
889
  get aid() {
819
890
  return this._aid;
820
891
  }
821
- setAgentMdPath(root) {
892
+ _setAgentMdRoot(root) {
822
893
  const next = String(root ?? '').trim() || this._agentMdDefaultRoot();
823
894
  this._agentMdPath = next;
824
895
  this._agentMdCache.clear();
825
896
  return next;
826
897
  }
827
- setAgentMDPath(root) {
828
- return this.setAgentMdPath(root);
898
+ async _resolveAgentMdUrl(aid) {
899
+ const target = String(aid ?? '').trim();
900
+ if (!target)
901
+ throw new ValidationError('agent.md requires non-empty aid');
902
+ let gatewayUrl = String(this._gatewayUrl ?? '').trim();
903
+ if (!gatewayUrl) {
904
+ try {
905
+ gatewayUrl = await this._resolveGatewayForAid(target);
906
+ }
907
+ catch {
908
+ gatewayUrl = '';
909
+ }
910
+ }
911
+ const authority = agentMdAuthority(target, this.configModel.discoveryPort);
912
+ return `${agentMdHttpScheme(gatewayUrl)}://${authority}/agent.md`;
913
+ }
914
+ async _ensureAgentMdUploadToken(aid, gatewayUrl) {
915
+ let identity = await this._auth.loadIdentityOrNone(aid);
916
+ if (!identity && this._identity && String(this._identity.aid ?? '') === aid) {
917
+ identity = this._identity;
918
+ }
919
+ if (!identity) {
920
+ throw new StateError('no local identity found, register or load an AID first');
921
+ }
922
+ const cachedToken = String(identity.access_token ?? '');
923
+ const expiresAt = this._auth.getAccessTokenExpiry(identity);
924
+ if (cachedToken && (expiresAt === null || expiresAt > Date.now() / 1000 + 30)) {
925
+ return cachedToken;
926
+ }
927
+ if (identity.refresh_token) {
928
+ try {
929
+ const refreshed = await this._auth.refreshCachedTokens(gatewayUrl, identity);
930
+ const refreshedToken = String(refreshed.access_token ?? '');
931
+ const refreshedExpiry = this._auth.getAccessTokenExpiry(refreshed);
932
+ if (refreshedToken && (refreshedExpiry === null || refreshedExpiry > Date.now() / 1000 + 30)) {
933
+ this._identity = refreshed;
934
+ return refreshedToken;
935
+ }
936
+ }
937
+ catch {
938
+ // refresh 失败时回退到完整 authenticate。
939
+ }
940
+ }
941
+ const result = await this._auth.authenticate(gatewayUrl, aid);
942
+ const token = String(result.access_token ?? '');
943
+ if (!token)
944
+ throw new StateError('authenticate did not return access_token');
945
+ const fallbackIdentity = {
946
+ ...identity,
947
+ access_token: token,
948
+ refresh_token: String(result.refresh_token ?? identity.refresh_token ?? ''),
949
+ };
950
+ const fallbackExpiresAt = Number(result.expires_at ?? identity.expires_at ?? NaN);
951
+ if (Number.isFinite(fallbackExpiresAt))
952
+ fallbackIdentity.expires_at = fallbackExpiresAt;
953
+ this._identity = await this._auth.loadIdentityOrNone(aid) ?? fallbackIdentity;
954
+ return token;
955
+ }
956
+ async _uploadAgentMd(content) {
957
+ const target = String(this._aid ?? this._currentAid?.aid ?? '').trim();
958
+ if (!target)
959
+ throw new StateError('uploadAgentMd requires local AID');
960
+ const gatewayUrl = await this._resolveGatewayForAid(target);
961
+ this._gatewayUrl = gatewayUrl;
962
+ const token = await this._ensureAgentMdUploadToken(target, gatewayUrl);
963
+ const response = await fetchWithTimeout(await this._resolveAgentMdUrl(target), {
964
+ method: 'PUT',
965
+ headers: {
966
+ Authorization: `Bearer ${token}`,
967
+ 'Content-Type': 'text/markdown; charset=utf-8',
968
+ },
969
+ body: content,
970
+ });
971
+ if (response.status === 404) {
972
+ throw new NotFoundError(`agent.md endpoint not found for aid: ${target}`);
973
+ }
974
+ if (!response.ok) {
975
+ const message = (await response.text()).trim();
976
+ throw new AUNError(`upload agent.md failed: HTTP ${response.status}${message ? ` - ${message}` : ''}`);
977
+ }
978
+ const payload = await response.json();
979
+ if (!isJsonObject(payload))
980
+ throw new AUNError('upload agent.md returned invalid JSON payload');
981
+ return payload;
982
+ }
983
+ async _downloadAgentMd(aid) {
984
+ const target = String(aid ?? '').trim();
985
+ if (!target)
986
+ throw new ValidationError('downloadAgentMd requires non-empty aid');
987
+ const cached = this._agentMdCache.get(target);
988
+ const url = await this._resolveAgentMdUrl(target);
989
+ const response = await fetchWithTimeout(url, {
990
+ method: 'GET',
991
+ headers: { Accept: 'text/markdown' },
992
+ redirect: 'follow',
993
+ });
994
+ if (response.status === 304 && typeof cached?.text === 'string') {
995
+ return String(cached.text);
996
+ }
997
+ if (response.status === 404) {
998
+ throw new NotFoundError(`agent.md not found for aid: ${target}`);
999
+ }
1000
+ if (!response.ok) {
1001
+ const message = (await response.text()).trim();
1002
+ throw new AUNError(`download agent.md failed: HTTP ${response.status}${message ? ` - ${message}` : ''}`);
1003
+ }
1004
+ const text = await response.text();
1005
+ const etag = String(response.headers?.get('ETag') ?? response.headers?.get('etag') ?? '').trim();
1006
+ const lastModified = String(response.headers?.get('Last-Modified') ?? response.headers?.get('last-modified') ?? '').trim();
1007
+ this._agentMdCache.set(target, {
1008
+ ...(cached ?? {}),
1009
+ text,
1010
+ etag,
1011
+ lastModified,
1012
+ remote_etag: etag,
1013
+ last_modified: lastModified,
1014
+ });
1015
+ return text;
829
1016
  }
830
- SetAgentMDPath(root) {
831
- return this.setAgentMdPath(root);
1017
+ async _headAgentMd(aid) {
1018
+ const target = String(aid ?? '').trim();
1019
+ if (!target)
1020
+ throw new ValidationError('headAgentMd requires non-empty aid');
1021
+ const response = await fetchWithTimeout(await this._resolveAgentMdUrl(target), {
1022
+ method: 'HEAD',
1023
+ headers: { Accept: 'text/markdown' },
1024
+ });
1025
+ const etag = String(response.headers?.get('ETag') ?? response.headers?.get('etag') ?? '').trim();
1026
+ const lastModified = String(response.headers?.get('Last-Modified') ?? response.headers?.get('last-modified') ?? '').trim();
1027
+ if (response.status === 404) {
1028
+ return { aid: target, found: false, etag: '', last_modified: '', status: 404 };
1029
+ }
1030
+ if (!response.ok) {
1031
+ throw new AUNError(`head agent.md failed: HTTP ${response.status}`);
1032
+ }
1033
+ const cached = this._agentMdCache.get(target) ?? {};
1034
+ this._agentMdCache.set(target, {
1035
+ ...cached,
1036
+ etag,
1037
+ lastModified,
1038
+ remote_etag: etag,
1039
+ last_modified: lastModified,
1040
+ });
1041
+ return { aid: target, found: true, etag, last_modified: lastModified, status: response.status };
1042
+ }
1043
+ async _verifyAgentMd(content, aid) {
1044
+ const target = String(aid ?? '').trim();
1045
+ if (!target)
1046
+ throw new ValidationError('verifyAgentMd requires non-empty aid');
1047
+ let peer = target === this._currentAid?.aid ? this._currentAid : null;
1048
+ if (!peer) {
1049
+ let certPem = String(await this._keystore.loadCert(target) ?? '').trim();
1050
+ if (!certPem) {
1051
+ certPem = String(await this._fetchPeerCert(target) ?? '').trim();
1052
+ }
1053
+ if (!certPem)
1054
+ throw new NotFoundError(`certificate not found for aid: ${target}`);
1055
+ peer = await AID.create({
1056
+ aid: target,
1057
+ aunPath: this.configModel.aunPath,
1058
+ certPem,
1059
+ privateKeyPem: null,
1060
+ certValid: true,
1061
+ privateKeyValid: false,
1062
+ });
1063
+ }
1064
+ const result = await peer.verifyAgentMd(content);
1065
+ if (!result.ok)
1066
+ throw new AUNError(result.error.message);
1067
+ return { ...result.data, verified: result.data.status === 'verified' };
832
1068
  }
833
1069
  /**
834
1070
  * 浏览器版本 publishAgentMd。默认从 {agentMdPath}/{self_aid}/agent.md 的等价 IndexedDB 正文读取,
@@ -856,8 +1092,12 @@ export class AUNClient {
856
1092
  if (localContent === null || localContent.length === 0) {
857
1093
  throw new ValidationError('publishAgentMd requires local agent.md content');
858
1094
  }
859
- const signed = await this.auth.signAgentMd(localContent);
860
- const result = await this.auth.uploadAgentMd(signed);
1095
+ const signedResult = await this._currentAid?.signAgentMd(localContent);
1096
+ if (!signedResult?.ok) {
1097
+ throw new StateError(signedResult?.error.message ?? 'publishAgentMd requires a valid local AID private key');
1098
+ }
1099
+ const signed = signedResult.data.signed;
1100
+ const result = await this._uploadAgentMd(signed);
861
1101
  this._localAgentMdEtag = await this._agentMdContentEtag(signed);
862
1102
  const remoteEtag = isJsonObject(result) ? String(result.etag ?? '').trim() : '';
863
1103
  if (remoteEtag)
@@ -877,13 +1117,13 @@ export class AUNClient {
877
1117
  * 浏览器版本 fetchAgentMd。aid 缺省时取自身;下载后的正文固定写入
878
1118
  * {agentMdPath}/{aid}/agent.md 的等价 IndexedDB 正文,agentmd.json 只保存元数据。
879
1119
  */
880
- async fetchAgentMd(aid) {
1120
+ async _fetchAgentMdCache(aid) {
881
1121
  const target = String(aid ?? this._aid ?? '').trim();
882
1122
  if (!target) {
883
1123
  throw new ValidationError('fetchAgentMd requires aid (or local AID)');
884
1124
  }
885
- const content = await this.auth.downloadAgentMd(target);
886
- const signature = await this.auth.verifyAgentMd(content, { aid: target });
1125
+ const content = await this._downloadAgentMd(target);
1126
+ const signature = await this._verifyAgentMd(content, target);
887
1127
  const isSelf = target === (this._aid ?? '');
888
1128
  const localEtag = await this._agentMdContentEtag(content);
889
1129
  const cacheMeta = this._agentMdAuthCacheMeta(target);
@@ -932,7 +1172,7 @@ export class AUNClient {
932
1172
  return String(this._aid ?? '').trim();
933
1173
  }
934
1174
  _agentMdDefaultRoot() {
935
- return this._joinAgentMdPath(this.configModel.aunPath || '.', 'AgentMDs');
1175
+ return this._joinAgentMdPath(this.configModel.aunPath || '.', 'AIDs');
936
1176
  }
937
1177
  _joinAgentMdPath(base, name) {
938
1178
  const left = String(base ?? '').trim().replace(/[\\/]+$/g, '');
@@ -1039,8 +1279,7 @@ export class AUNClient {
1039
1279
  }
1040
1280
  _agentMdAuthCacheMeta(aid) {
1041
1281
  try {
1042
- const store = this.auth._agentMdCache;
1043
- const record = store?.get(String(aid ?? '').trim());
1282
+ const record = this._agentMdCache.get(String(aid ?? '').trim());
1044
1283
  return record && typeof record === 'object' ? { ...record } : {};
1045
1284
  }
1046
1285
  catch {
@@ -1165,7 +1404,7 @@ export class AUNClient {
1165
1404
  return;
1166
1405
  this._agentMdFetchInflight.add(target);
1167
1406
  try {
1168
- await this.fetchAgentMd(target);
1407
+ await this._fetchAgentMdCache(target);
1169
1408
  }
1170
1409
  catch (err) {
1171
1410
  await this._saveAgentMdRecord(target, {
@@ -1226,7 +1465,7 @@ export class AUNClient {
1226
1465
  }
1227
1466
  await this._observeAgentMdMeta(senderAid, String(sender.etag ?? '').trim(), String(sender.last_modified ?? sender.lastModified ?? '').trim(), 'envelope');
1228
1467
  }
1229
- async checkAgentMd(aid, maxUnsyncedDays = 0) {
1468
+ async _checkAgentMdCache(aid, maxUnsyncedDays = 0) {
1230
1469
  const target = String(aid ?? this._aid ?? '').trim();
1231
1470
  if (!target)
1232
1471
  throw new ValidationError('checkAgentMd requires aid (or local AID)');
@@ -1256,7 +1495,7 @@ export class AUNClient {
1256
1495
  const now = Date.now();
1257
1496
  let remote;
1258
1497
  try {
1259
- remote = await this.auth.headAgentMd(target);
1498
+ remote = await this._headAgentMd(target);
1260
1499
  }
1261
1500
  catch (err) {
1262
1501
  await this._saveAgentMdRecord(target, { checked_at: now, remote_status: 'error', last_error: err instanceof Error ? err.message : String(err) });
@@ -1310,7 +1549,116 @@ export class AUNClient {
1310
1549
  }
1311
1550
  }
1312
1551
  get state() {
1313
- return this._state;
1552
+ return this._publicState(this._state);
1553
+ }
1554
+ _publicState(state) {
1555
+ return STATE_TO_PUBLIC[state] ?? state;
1556
+ }
1557
+ get currentAid() {
1558
+ return this._currentAid;
1559
+ }
1560
+ get hasIdentity() {
1561
+ return this._currentAid !== null && this.state !== ConnectionState.CLOSED;
1562
+ }
1563
+ get canSign() {
1564
+ return this.hasIdentity && !!this._currentAid?.isPrivateKeyValid();
1565
+ }
1566
+ get canConnect() {
1567
+ return this.hasIdentity && this.state !== ConnectionState.CLOSED;
1568
+ }
1569
+ get canSend() {
1570
+ return this.state === ConnectionState.READY;
1571
+ }
1572
+ get isReady() { return this.canSend; }
1573
+ get isOnline() {
1574
+ return this.state === ConnectionState.READY
1575
+ || this.state === ConnectionState.RECONNECTING
1576
+ || this.state === ConnectionState.RETRY_BACKOFF;
1577
+ }
1578
+ get isClosed() { return this.state === ConnectionState.CLOSED; }
1579
+ get aunPath() { return this.hasIdentity ? this._currentAid?.aunPath ?? this.configModel.aunPath : null; }
1580
+ /** 下次重连时间(仅在 retry_backoff 状态时非 null,对齐 Python next_retry_at) */
1581
+ get nextRetryAt() {
1582
+ return this.state === ConnectionState.RETRY_BACKOFF ? this._nextRetryAt : null;
1583
+ }
1584
+ /** 距下次重连的剩余秒数(仅在 retry_backoff 状态时非 null,对齐 Python next_retry_in_seconds) */
1585
+ get nextRetryInSeconds() {
1586
+ const t = this.nextRetryAt;
1587
+ if (t === null)
1588
+ return null;
1589
+ return Math.max(0, (t.getTime() - Date.now()) / 1000);
1590
+ }
1591
+ /** 当前重连尝试次数(对齐 Python retry_attempt) */
1592
+ get retryAttempt() { return this._retryAttempt; }
1593
+ /** 最大重连次数(0 = 无限,对齐 Python retry_max_attempts) */
1594
+ get retryMaxAttempts() { return this._retryMaxAttempts; }
1595
+ /** 最近一次错误(对齐 Python last_error) */
1596
+ get lastError() { return this._lastError; }
1597
+ /** 最近一次错误码(对齐 Python last_error_code) */
1598
+ get lastErrorCode() { return this._lastErrorCode; }
1599
+ loadIdentity(aid) {
1600
+ if (!aid?.isPrivateKeyValid())
1601
+ throw new StateError('loadIdentity requires an AID with a valid private key');
1602
+ const publicState = this.state;
1603
+ if (publicState !== ConnectionState.NO_IDENTITY && publicState !== ConnectionState.CLOSED) {
1604
+ throw new StateError(`loadIdentity not allowed in state ${publicState}`);
1605
+ }
1606
+ this._currentAid = aid;
1607
+ this._aid = aid.aid;
1608
+ this._identity = {
1609
+ aid: aid.aid,
1610
+ private_key_pem: aid._privateKeyPem ?? '',
1611
+ public_key_der_b64: aid.publicKey,
1612
+ cert: aid.certPem,
1613
+ };
1614
+ this._auth._aid = aid.aid;
1615
+ this._state = 'disconnected';
1616
+ this._closing = false;
1617
+ }
1618
+ setProtectedHeaders(headers) {
1619
+ if (!headers) {
1620
+ this._instanceProtectedHeaders = null;
1621
+ return;
1622
+ }
1623
+ const cleaned = {};
1624
+ for (const [key, value] of Object.entries(headers)) {
1625
+ if (key === '_auth')
1626
+ continue;
1627
+ cleaned[String(key)] = String(value);
1628
+ }
1629
+ this._instanceProtectedHeaders = Object.keys(cleaned).length ? cleaned : null;
1630
+ }
1631
+ getProtectedHeaders() {
1632
+ return this._instanceProtectedHeaders ? { ...this._instanceProtectedHeaders } : null;
1633
+ }
1634
+ cachePeer(aid) {
1635
+ if (!this.hasIdentity)
1636
+ throw new StateError('cachePeer requires a loaded identity');
1637
+ if (!aid.isCertValid())
1638
+ throw new ValidationError('cachePeer requires an AID with a valid certificate');
1639
+ this._peerCache.set(aid.aid, aid);
1640
+ return aid;
1641
+ }
1642
+ getPeer(aid) {
1643
+ if (!this.hasIdentity)
1644
+ throw new StateError('getPeer requires a loaded identity');
1645
+ return this._peerCache.get(String(aid ?? '').trim()) ?? null;
1646
+ }
1647
+ async lookupPeer(aid) {
1648
+ if (!this.hasIdentity)
1649
+ throw new StateError('lookupPeer requires a loaded identity');
1650
+ const target = String(aid ?? '').trim();
1651
+ if (!target)
1652
+ throw new ValidationError('lookupPeer requires non-empty aid');
1653
+ const cached = this._peerCache.get(target);
1654
+ if (cached)
1655
+ return cached;
1656
+ throw new NotFoundError(`peer not found in cache: ${target}`);
1657
+ }
1658
+ peers() {
1659
+ if (!this.hasIdentity)
1660
+ throw new StateError('peers requires a loaded identity');
1661
+ return [...this._peerCache.entries()].sort(([a], [b]) => a.localeCompare(b)).map(([, v]) => v);
1314
1662
  }
1315
1663
  get gatewayUrl() {
1316
1664
  return this._gatewayUrl;
@@ -1325,36 +1673,65 @@ export class AUNClient {
1325
1673
  get gatewayHealth() {
1326
1674
  return this._discovery.lastHealthy;
1327
1675
  }
1328
- /** 主动检查 gateway 可用性(GET /health) */
1329
- async checkGatewayHealth(gatewayUrl, timeout = 5000) {
1676
+ // ── 生命周期 ──────────────────────────────────────
1677
+ /** 仅认证当前身份,获取/刷新 token,但不建立长连接。 */
1678
+ async authenticate(options = {}) {
1330
1679
  const tStart = Date.now();
1331
- this._clientLog.debug(`checkGatewayHealth enter: gateway=${gatewayUrl} timeout=${timeout}`);
1680
+ const target = this._currentAid?.aid ?? this._aid ?? '';
1681
+ if (!target || !this._currentAid?.isPrivateKeyValid()) {
1682
+ throw new StateError('authenticate requires a loaded AID with a valid private key');
1683
+ }
1684
+ const publicState = this.state;
1685
+ if (publicState !== ConnectionState.STANDBY && publicState !== ConnectionState.AUTHENTICATED) {
1686
+ throw new StateError(`authenticate not allowed in state ${publicState}`);
1687
+ }
1688
+ if ('aid' in options || 'access_token' in options || 'token' in options || 'kite_token' in options) {
1689
+ throw new ValidationError('authenticate options must not include aid or token fields; load an AID object first');
1690
+ }
1691
+ this._state = 'connecting';
1332
1692
  try {
1333
- const result = await this._discovery.checkHealth(gatewayUrl, timeout);
1334
- this._clientLog.debug(`checkGatewayHealth exit: elapsed=${Date.now() - tStart}ms healthy=${result}`);
1693
+ const gateway = String(options.gateway ?? this._gatewayUrl ?? await this._resolveGatewayForAid(target)).trim();
1694
+ const result = await this._auth.authenticate(gateway, target);
1695
+ this._gatewayUrl = String(result.gateway ?? gateway);
1696
+ this._identity = await this._auth.loadIdentityOrNone(target);
1697
+ this._state = 'authenticated';
1698
+ this._clientLog.debug(`authenticate exit: elapsed=${Date.now() - tStart}ms aid=${target}`);
1335
1699
  return result;
1336
1700
  }
1337
1701
  catch (err) {
1338
- this._clientLog.debug(`checkGatewayHealth exit (error): elapsed=${Date.now() - tStart}ms err=${err instanceof Error ? err.message : String(err)}`);
1702
+ this._state = 'disconnected';
1703
+ this._clientLog.debug(`authenticate exit (error): elapsed=${Date.now() - tStart}ms err=${err instanceof Error ? err.message : String(err)}`);
1339
1704
  throw err;
1340
1705
  }
1341
1706
  }
1342
- // ── 生命周期 ──────────────────────────────────────
1343
- /**
1344
- * 连接到 Gateway。
1345
- *
1346
- * @param auth - 认证参数,必须包含 access_token 和 gateway
1347
- * @param options - 可选的会话选项(auto_reconnect, heartbeat_interval 等)
1348
- */
1349
- async connect(auth, options) {
1707
+ /** 连接到 Gateway;身份来自构造函数或 loadIdentity(aid),认证由 SDK 内部自动完成。 */
1708
+ async connect(options = {}) {
1350
1709
  const tStart = Date.now();
1351
1710
  this._clientLog.debug(`connect enter: state=${this._state} aid=${this._aid ?? '-'}`);
1352
- if (this._state !== 'idle' && this._state !== 'closed' && this._state !== 'disconnected') {
1711
+ if (arguments.length > 1) {
1712
+ throw new ValidationError('connect accepts a single options object');
1713
+ }
1714
+ if ('aid' in options || 'access_token' in options || 'token' in options || 'kite_token' in options) {
1715
+ throw new ValidationError('connect options must not include aid or token fields; load an AID object first');
1716
+ }
1717
+ const target = this._currentAid?.aid ?? this._aid ?? '';
1718
+ if (!target || !this._currentAid?.isPrivateKeyValid()) {
1719
+ throw new StateError('connect requires a loaded AID with a valid private key');
1720
+ }
1721
+ const publicState = this.state;
1722
+ const allowed = new Set([
1723
+ ConnectionState.STANDBY,
1724
+ ConnectionState.AUTHENTICATED,
1725
+ ConnectionState.RETRY_BACKOFF,
1726
+ ConnectionState.CONNECTION_FAILED,
1727
+ ]);
1728
+ if (!allowed.has(publicState)) {
1353
1729
  this._clientLog.debug(`connect exit (error): elapsed=${Date.now() - tStart}ms err=invalid_state state=${this._state}`);
1354
- throw new StateError(`connect not allowed in state ${this._state}`);
1730
+ throw new StateError(`connect not allowed in state ${publicState}`);
1355
1731
  }
1356
1732
  this._state = 'connecting';
1357
- const params = { ...auth, ...options };
1733
+ const gateway = String(options.gateway ?? this._gatewayUrl ?? await this._resolveGatewayForAid(target)).trim();
1734
+ const params = { ...options, gateway };
1358
1735
  const normalized = this._normalizeConnectParams(params);
1359
1736
  this._sessionParams = normalized;
1360
1737
  this._sessionOptions = this._buildSessionOptions(normalized);
@@ -1365,7 +1742,7 @@ export class AUNClient {
1365
1742
  for (const gw of gateways) {
1366
1743
  try {
1367
1744
  const gwParams = { ...normalized, gateway: gw };
1368
- await this._connectOnce(gwParams, false);
1745
+ await this._connectOnce(gwParams, true);
1369
1746
  this._clientLog.debug(`connect exit: elapsed=${Date.now() - tStart}ms state=${this._state}`);
1370
1747
  return;
1371
1748
  }
@@ -1380,7 +1757,7 @@ export class AUNClient {
1380
1757
  }
1381
1758
  }
1382
1759
  if (this._state === 'connecting' || this._state === 'authenticating') {
1383
- this._state = 'disconnected';
1760
+ this._state = 'terminal_failed';
1384
1761
  }
1385
1762
  this._clientLog.debug(`connect exit (error): elapsed=${Date.now() - tStart}ms err=${lastErr instanceof Error ? lastErr.message : String(lastErr)}`);
1386
1763
  throw lastErr;
@@ -1402,56 +1779,9 @@ export class AUNClient {
1402
1779
  }
1403
1780
  await this._transport.close();
1404
1781
  this._state = 'disconnected';
1405
- await this._dispatcher.publish('connection.state', { state: this._state });
1782
+ await this._dispatcher.publish('connection.state', { state: this._publicState(this._state) });
1406
1783
  this._clientLog.debug(`disconnect exit: elapsed=${Date.now() - tStart}ms`);
1407
1784
  }
1408
- /** 列出本地所有已存储的身份摘要(仅返回有有效私钥的 AID) */
1409
- async listIdentities() {
1410
- const tStart = Date.now();
1411
- this._clientLog.debug('listIdentities enter');
1412
- try {
1413
- const listFn = this._keystore.listIdentities;
1414
- if (typeof listFn !== 'function') {
1415
- this._clientLog.debug(`listIdentities exit: elapsed=${Date.now() - tStart}ms count=0 reason=keystore_no_list`);
1416
- return [];
1417
- }
1418
- const aids = await listFn.call(this._keystore);
1419
- const summaries = [];
1420
- for (const aid of [...aids].sort()) {
1421
- const identity = await this._keystore.loadIdentity(aid);
1422
- if (!identity || !identity.private_key_pem)
1423
- continue;
1424
- const summary = { aid };
1425
- // 优先从 loadMetadata 获取
1426
- const loadMeta = this._keystore.loadMetadata;
1427
- if (typeof loadMeta === 'function') {
1428
- const md = await loadMeta.call(this._keystore, aid);
1429
- if (md && Object.keys(md).length > 0) {
1430
- summary.metadata = md;
1431
- }
1432
- }
1433
- // 回退:从 identity 中提取非核心字段
1434
- if (!summary.metadata) {
1435
- const metadata = {};
1436
- for (const [key, value] of Object.entries(identity)) {
1437
- if (!['aid', 'private_key_pem', 'public_key_der_b64', 'curve', 'cert'].includes(key)) {
1438
- metadata[key] = value;
1439
- }
1440
- }
1441
- if (Object.keys(metadata).length > 0) {
1442
- summary.metadata = metadata;
1443
- }
1444
- }
1445
- summaries.push(summary);
1446
- }
1447
- this._clientLog.debug(`listIdentities exit: elapsed=${Date.now() - tStart}ms count=${summaries.length}`);
1448
- return summaries;
1449
- }
1450
- catch (err) {
1451
- this._clientLog.debug(`listIdentities exit (error): elapsed=${Date.now() - tStart}ms err=${err instanceof Error ? err.message : String(err)}`);
1452
- throw err;
1453
- }
1454
- }
1455
1785
  /** 关闭连接 */
1456
1786
  async close() {
1457
1787
  const tStart = Date.now();
@@ -1480,7 +1810,7 @@ export class AUNClient {
1480
1810
  }
1481
1811
  await this._transport.close();
1482
1812
  this._state = 'closed';
1483
- await this._dispatcher.publish('connection.state', { state: this._state });
1813
+ await this._dispatcher.publish('connection.state', { state: this._publicState(this._state) });
1484
1814
  this._resetSeqTrackingState();
1485
1815
  this._clientLog.debug(`close exit: elapsed=${Date.now() - tStart}ms`);
1486
1816
  }
@@ -1515,6 +1845,10 @@ export class AUNClient {
1515
1845
  throw new PermissionError(`legacy E2EE method is removed in this SDK: ${method}`);
1516
1846
  }
1517
1847
  const p = { ...(params ?? {}) };
1848
+ if (this._instanceProtectedHeaders && PROTECTED_HEADERS_METHODS.has(method)) {
1849
+ const existing = isJsonObject(p.protected_headers) ? p.protected_headers : {};
1850
+ p.protected_headers = { ...this._instanceProtectedHeaders, ...existing };
1851
+ }
1518
1852
  if (method === 'message.send' || method === 'group.send') {
1519
1853
  this._normalizeOutboundMessagePayload(p, method);
1520
1854
  }
@@ -1545,7 +1879,7 @@ export class AUNClient {
1545
1879
  throw new StateError('V2 session not initialized; encrypted message.send requires V2 (V1 E2EE removed)');
1546
1880
  }
1547
1881
  this._clientLog.debug('call route: message.send → V2 encrypted send');
1548
- return await this.sendV2(String(p.to ?? ''), p.payload ?? {}, {
1882
+ return await this._sendV2(String(p.to ?? ''), p.payload ?? {}, {
1549
1883
  messageId: String(p.message_id ?? '') || undefined,
1550
1884
  timestamp: p.timestamp,
1551
1885
  protectedHeaders: this._protectedHeadersFromParams(p),
@@ -1564,7 +1898,7 @@ export class AUNClient {
1564
1898
  throw new StateError('V2 session not initialized; encrypted group.send requires V2 (V1 E2EE removed)');
1565
1899
  }
1566
1900
  this._clientLog.debug('call route: group.send → V2 encrypted send');
1567
- return await this.sendGroupV2(String(p.group_id ?? ''), p.payload ?? {}, {
1901
+ return await this._sendGroupV2(String(p.group_id ?? ''), p.payload ?? {}, {
1568
1902
  messageId: String(p.message_id ?? '') || undefined,
1569
1903
  timestamp: p.timestamp,
1570
1904
  protectedHeaders: this._protectedHeadersFromParams(p),
@@ -1612,24 +1946,24 @@ export class AUNClient {
1612
1946
  // message.pull:V2 就绪时走 V2 pull
1613
1947
  if (method === 'message.pull' && this._v2Session) {
1614
1948
  this._clientLog.debug('call route: message.pull → V2 pull');
1615
- const messages = await this.pullV2(Number(p.after_seq ?? 0) || 0, Number(p.limit ?? 50) || 50, { force: p.force === true });
1949
+ const messages = await this._pullV2(Number(p.after_seq ?? 0) || 0, Number(p.limit ?? 50) || 50, { force: p.force === true });
1616
1950
  return { messages };
1617
1951
  }
1618
1952
  // message.ack:V2 就绪时走 V2 ack
1619
1953
  if (method === 'message.ack' && this._v2Session) {
1620
1954
  this._clientLog.debug('call route: message.ack → V2 ack');
1621
- return await this.ackV2(Number(p.seq ?? p.up_to_seq ?? 0) || undefined);
1955
+ return await this._ackV2(Number(p.seq ?? p.up_to_seq ?? 0) || undefined);
1622
1956
  }
1623
1957
  // group.pull:V2 就绪时走 V2 pull
1624
1958
  if (method === 'group.pull' && this._v2Session && p.group_id) {
1625
1959
  this._clientLog.debug('call route: group.pull → V2 pull');
1626
- const messages = await this.pullGroupV2(String(p.group_id), Number(p.after_seq ?? p.after_message_seq ?? 0) || 0, Number(p.limit ?? 50) || 50);
1960
+ const messages = await this._pullGroupV2(String(p.group_id), Number(p.after_seq ?? p.after_message_seq ?? 0) || 0, Number(p.limit ?? 50) || 50);
1627
1961
  return { messages };
1628
1962
  }
1629
1963
  // group.ack_messages:V2 就绪时走 V2 ack
1630
1964
  if (method === 'group.ack_messages' && this._v2Session && p.group_id) {
1631
1965
  this._clientLog.debug('call route: group.ack_messages → V2 ack');
1632
- return await this.ackGroupV2(String(p.group_id), Number(p.seq ?? p.msg_seq ?? p.up_to_seq ?? 0) || undefined);
1966
+ return await this._ackGroupV2(String(p.group_id), Number(p.seq ?? p.msg_seq ?? p.up_to_seq ?? 0) || undefined);
1633
1967
  }
1634
1968
  // 关键操作自动附加客户端签名
1635
1969
  if (SIGNED_METHODS.has(method)) {
@@ -1754,16 +2088,6 @@ export class AUNClient {
1754
2088
  }
1755
2089
  return await this._transport.call(method, p);
1756
2090
  }
1757
- // ── 便利方法 ──────────────────────────────────────
1758
- async ping(params) {
1759
- return this.meta.ping(params);
1760
- }
1761
- async status(params) {
1762
- return this.meta.status(params);
1763
- }
1764
- async trustRoots(params) {
1765
- return this.meta.trustRoots(params);
1766
- }
1767
2091
  // ── 事件 ──────────────────────────────────────────
1768
2092
  /**
1769
2093
  * 订阅事件。
@@ -1901,7 +2225,7 @@ export class AUNClient {
1901
2225
  this._gapFillDone.add(dedupKey);
1902
2226
  try {
1903
2227
  this._clientLog.debug(`_onRawGroupV2MessageCreated -> group.v2.pull group=${groupId} after_seq=${afterSeq}`);
1904
- const messages = await this.pullGroupV2(groupId, afterSeq, 50);
2228
+ const messages = await this._pullGroupV2(groupId, afterSeq, 50);
1905
2229
  this._clientLog.debug(`_onRawGroupV2MessageCreated pulled ${messages.length} msgs for group=${groupId}`);
1906
2230
  }
1907
2231
  finally {
@@ -3373,7 +3697,7 @@ export class AUNClient {
3373
3697
  this._state = 'connected';
3374
3698
  this._connectedAt = Date.now();
3375
3699
  await this._dispatcher.publish('connection.state', {
3376
- state: this._state,
3700
+ state: this._publicState(this._state),
3377
3701
  gateway: gatewayUrl,
3378
3702
  });
3379
3703
  if (this._seqTrackerContext !== this._currentSeqTrackerContext()) {
@@ -3383,7 +3707,7 @@ export class AUNClient {
3383
3707
  this._startBackgroundTasks();
3384
3708
  // V2 E2EE: 初始化 session 并注册设备 SPK(与 Python `_init_v2_session` 对齐)
3385
3709
  try {
3386
- await this.initV2Session();
3710
+ await this._initV2Session();
3387
3711
  }
3388
3712
  catch (exc) {
3389
3713
  this._clientLog.warn(`V2 session init failed (non-fatal): ${String(exc)}`);
@@ -3402,6 +3726,57 @@ export class AUNClient {
3402
3726
  const gateways = this._resolveGateways(params);
3403
3727
  return gateways[0];
3404
3728
  }
3729
+ async _resolveGatewayForAid(aid) {
3730
+ const target = String(aid ?? this._aid ?? '').trim();
3731
+ if (!target)
3732
+ throw new StateError('gateway discovery requires a loaded AID');
3733
+ if (this._gatewayUrl)
3734
+ return this._gatewayUrl;
3735
+ try {
3736
+ const getMetadata = this._keystore.getMetadata;
3737
+ const raw = typeof getMetadata === 'function'
3738
+ ? String(await getMetadata.call(this._keystore, target, 'gateway_url') ?? '').trim()
3739
+ : '';
3740
+ if (raw) {
3741
+ const gateway = raw.startsWith('"') && raw.endsWith('"') ? String(JSON.parse(raw)).trim() : raw;
3742
+ if (gateway) {
3743
+ this._gatewayUrl = gateway;
3744
+ return gateway;
3745
+ }
3746
+ }
3747
+ }
3748
+ catch {
3749
+ // 缓存读取失败不影响发现流程。
3750
+ }
3751
+ const dotIdx = target.indexOf('.');
3752
+ const issuerDomain = dotIdx >= 0 ? target.slice(dotIdx + 1) : target;
3753
+ const portSuffix = this.configModel.discoveryPort ? `:${this.configModel.discoveryPort}` : '';
3754
+ const candidates = [
3755
+ `https://${target}${portSuffix}/.well-known/aun-gateway`,
3756
+ `https://gateway.${issuerDomain}${portSuffix}/.well-known/aun-gateway`,
3757
+ ];
3758
+ let lastError = null;
3759
+ for (const url of candidates) {
3760
+ try {
3761
+ const gateway = await this._discovery.discover(url);
3762
+ this._gatewayUrl = gateway;
3763
+ try {
3764
+ const setMetadata = this._keystore.setMetadata;
3765
+ if (typeof setMetadata === 'function') {
3766
+ await setMetadata.call(this._keystore, target, 'gateway_url', gateway);
3767
+ }
3768
+ }
3769
+ catch {
3770
+ // 缓存写入失败不影响连接。
3771
+ }
3772
+ return gateway;
3773
+ }
3774
+ catch (err) {
3775
+ lastError = err;
3776
+ }
3777
+ }
3778
+ throw lastError instanceof Error ? lastError : new ConnectionError(`gateway discovery failed for ${target}`);
3779
+ }
3405
3780
  _resolveGateways(params) {
3406
3781
  const topology = isJsonObject(params.topology) ? params.topology : null;
3407
3782
  if (topology) {
@@ -3451,12 +3826,13 @@ export class AUNClient {
3451
3826
  _normalizeConnectParams(params) {
3452
3827
  const request = { ...params };
3453
3828
  const accessToken = String(request.access_token ?? '');
3454
- if (!accessToken)
3455
- throw new StateError('connect requires non-empty access_token');
3456
3829
  const gateway = String(request.gateway ?? this._gatewayUrl ?? '');
3457
3830
  if (!gateway)
3458
3831
  throw new StateError('connect requires non-empty gateway');
3459
- request.access_token = accessToken;
3832
+ if (accessToken)
3833
+ request.access_token = accessToken;
3834
+ else
3835
+ delete request.access_token;
3460
3836
  request.gateway = gateway;
3461
3837
  request.device_id = this._deviceId;
3462
3838
  request.slot_id = normalizeInstanceId(request.slot_id ?? this._slotId, 'slot_id', { allowEmpty: true });
@@ -3676,6 +4052,11 @@ export class AUNClient {
3676
4052
  }
3677
4053
  try {
3678
4054
  identity = await this._auth.refreshCachedTokens(this._gatewayUrl, identity);
4055
+ // 刷新期间可能已断线,复检状态,避免写回 stale identity
4056
+ if (this._state !== 'connected') {
4057
+ scheduleRefresh();
4058
+ return;
4059
+ }
3679
4060
  this._identity = identity;
3680
4061
  if (this._sessionParams && identity.access_token) {
3681
4062
  this._sessionParams.access_token = identity.access_token;
@@ -3816,7 +4197,7 @@ export class AUNClient {
3816
4197
  // 先停止后台任务,避免心跳/token刷新在重连期间继续触发
3817
4198
  this._stopBackgroundTasks();
3818
4199
  await this._dispatcher.publish('connection.state', {
3819
- state: this._state,
4200
+ state: this._publicState(this._state),
3820
4201
  error,
3821
4202
  });
3822
4203
  if (!this._sessionOptions.auto_reconnect)
@@ -3830,7 +4211,7 @@ export class AUNClient {
3830
4211
  this._clientLog.warn(`suppress auto-reconnect: ${reason}`);
3831
4212
  const disconnectInfo = this._lastDisconnectInfo ?? {};
3832
4213
  const eventPayload = {
3833
- state: this._state, error, reason,
4214
+ state: this._publicState(this._state), error, reason,
3834
4215
  };
3835
4216
  // 把服务端附带的结构化 detail(如配额超限信息)也带给应用层
3836
4217
  const detail = disconnectInfo.detail;
@@ -3858,34 +4239,51 @@ export class AUNClient {
3858
4239
  const maxAttempts = Number.isFinite(maxAttemptsRaw) && maxAttemptsRaw > 0 ? Math.floor(maxAttemptsRaw) : 0;
3859
4240
  // 服务端主动关闭时从 16s 起跳,避免重连风暴;网络断开从 initial_delay 起跳
3860
4241
  let delay = clampReconnectDelaySeconds(serverInitiated ? 16.0 : retry.initial_delay, serverInitiated ? 16.0 : 1.0, maxBaseDelay);
4242
+ this._retryAttempt = 0;
4243
+ this._retryMaxAttempts = maxAttempts;
3861
4244
  for (let attempt = 1; !this._reconnectAbort?.signal.aborted; attempt++) {
3862
4245
  // R1 fix: max_attempts 检查在循环顶部,覆盖所有路径(含 health-fail)
3863
4246
  if (maxAttempts > 0 && attempt > maxAttempts) {
3864
4247
  this._state = 'terminal_failed';
4248
+ this._nextRetryAt = null;
3865
4249
  this._reconnectActive = false;
3866
4250
  this._reconnectAbort = null;
3867
4251
  await this._dispatcher.publish('connection.state', {
3868
- state: this._state,
4252
+ state: this._publicState(this._state),
3869
4253
  attempt: attempt - 1,
3870
4254
  reason: 'max_attempts_exhausted',
3871
4255
  });
3872
4256
  return;
3873
4257
  }
3874
- this._state = 'reconnecting';
4258
+ // 先进入 retry_backoff 状态(对齐 Python:先退避再重连)
4259
+ this._retryAttempt = attempt;
4260
+ const sleepMs = reconnectSleepDelaySeconds(delay, maxBaseDelay) * 1000;
4261
+ this._nextRetryAt = new Date(Date.now() + sleepMs);
4262
+ this._state = 'retry_backoff';
3875
4263
  await this._dispatcher.publish('connection.state', {
3876
- state: this._state,
4264
+ state: this._publicState(this._state),
3877
4265
  attempt,
4266
+ next_retry_at: this._nextRetryAt.getTime() / 1000,
3878
4267
  });
3879
4268
  try {
3880
- await this._sleep(reconnectSleepDelaySeconds(delay, maxBaseDelay) * 1000);
4269
+ await this._sleep(sleepMs);
4270
+ this._nextRetryAt = null;
3881
4271
  if (this._reconnectAbort?.signal.aborted) {
3882
4272
  this._reconnectActive = false;
3883
4273
  return;
3884
4274
  }
4275
+ // 退避结束,进入 reconnecting 状态
4276
+ this._state = 'reconnecting';
4277
+ await this._dispatcher.publish('connection.state', {
4278
+ state: this._publicState(this._state),
4279
+ attempt,
4280
+ });
3885
4281
  // 重连前先 GET /health 探测,不健康则跳过本轮
3886
4282
  if (this._gatewayUrl) {
3887
4283
  const healthy = await this._discovery.checkHealth(this._gatewayUrl, 5000);
3888
4284
  if (!healthy) {
4285
+ this._lastError = new Error('gateway health check failed');
4286
+ this._lastErrorCode = 'gateway_unhealthy';
3889
4287
  delay = Math.min(delay * 2, maxBaseDelay);
3890
4288
  continue;
3891
4289
  }
@@ -3895,21 +4293,27 @@ export class AUNClient {
3895
4293
  throw new StateError('missing connect params for reconnect');
3896
4294
  }
3897
4295
  await this._connectOnce(this._sessionParams, true);
4296
+ this._lastError = null;
4297
+ this._lastErrorCode = null;
4298
+ this._nextRetryAt = null;
3898
4299
  this._reconnectActive = false;
3899
4300
  this._reconnectAbort = null;
3900
4301
  return;
3901
4302
  }
3902
4303
  catch (exc) {
4304
+ this._lastError = exc instanceof Error ? exc : new Error(String(exc));
4305
+ this._lastErrorCode = 'reconnect_failed';
3903
4306
  await this._dispatcher.publish('connection.error', {
3904
4307
  error: formatCaughtError(exc),
3905
4308
  attempt,
3906
4309
  });
3907
4310
  if (!this._shouldRetryReconnect(exc)) {
3908
4311
  this._state = 'terminal_failed';
4312
+ this._nextRetryAt = null;
3909
4313
  this._reconnectActive = false;
3910
4314
  this._reconnectAbort = null;
3911
4315
  await this._dispatcher.publish('connection.state', {
3912
- state: this._state,
4316
+ state: this._publicState(this._state),
3913
4317
  error: formatCaughtError(exc),
3914
4318
  attempt,
3915
4319
  });
@@ -4259,7 +4663,7 @@ export class AUNClient {
4259
4663
  *
4260
4664
  * connect 成功后自动调用,可幂等手动调用。
4261
4665
  */
4262
- async initV2Session() {
4666
+ async _initV2Session() {
4263
4667
  if (!this._aid)
4264
4668
  return;
4265
4669
  let identity = this._identity;
@@ -4564,7 +4968,7 @@ export class AUNClient {
4564
4968
  * @param opts 可选 messageId / timestamp(与 Python 行为一致)
4565
4969
  * @returns 服务端响应
4566
4970
  */
4567
- async sendV2(to, payload, opts) {
4971
+ async _sendV2(to, payload, opts) {
4568
4972
  if (!this._v2Session) {
4569
4973
  throw new StateError('V2 session not initialized (not connected?)');
4570
4974
  }
@@ -4608,7 +5012,7 @@ export class AUNClient {
4608
5012
  * @param afterSeq 从此 seq 之后开始拉取(0/省略 = 从当前 contiguous 开始)
4609
5013
  * @param limit 最多拉取条数
4610
5014
  */
4611
- async pullV2(afterSeq = 0, limit = 50, opts) {
5015
+ async _pullV2(afterSeq = 0, limit = 50, opts) {
4612
5016
  if (!this._v2Session) {
4613
5017
  throw new StateError('V2 session not initialized (not connected?)');
4614
5018
  }
@@ -4704,7 +5108,7 @@ export class AUNClient {
4704
5108
  this._saveSeqTrackerState();
4705
5109
  }
4706
5110
  if (messages.length > 0 && contigAdvanced && ackSeq > 0) {
4707
- this._safeAsync(this.ackV2(ackSeq).then(() => undefined));
5111
+ this._safeAsync(this._ackV2(ackSeq).then(() => undefined));
4708
5112
  }
4709
5113
  }
4710
5114
  const nextAfter = Math.max(pageMaxSeq, nextAfterSeq);
@@ -4722,7 +5126,7 @@ export class AUNClient {
4722
5126
  *
4723
5127
  * @param upToSeq 确认到此 seq;省略则用当前 contiguous
4724
5128
  */
4725
- async ackV2(upToSeq) {
5129
+ async _ackV2(upToSeq) {
4726
5130
  const ns = this._aid ? `p2p:${this._aid}` : '';
4727
5131
  let seq = upToSeq ?? (ns ? this._seqTracker.getContiguousSeq(ns) : 0);
4728
5132
  if (seq <= 0)
@@ -4959,7 +5363,7 @@ export class AUNClient {
4959
5363
  * @param opts 可选 messageId / timestamp
4960
5364
  * @returns 服务端响应
4961
5365
  */
4962
- async sendGroupV2(groupId, payload, opts) {
5366
+ async _sendGroupV2(groupId, payload, opts) {
4963
5367
  if (!this._v2Session) {
4964
5368
  throw new StateError('V2 session not initialized (not connected?)');
4965
5369
  }
@@ -5018,7 +5422,7 @@ export class AUNClient {
5018
5422
  }
5019
5423
  }
5020
5424
  async _pullGroupV2Internal(params) {
5021
- await this.pullGroupV2(params.group_id, params.after_seq, params.limit);
5425
+ await this._pullGroupV2(params.group_id, params.after_seq, params.limit);
5022
5426
  }
5023
5427
  /**
5024
5428
  * 拉取并解密 V2 Group 消息。
@@ -5027,7 +5431,7 @@ export class AUNClient {
5027
5431
  * @param afterSeq 从此 seq 之后开始拉取(0/省略 = 从当前 contiguous 开始)
5028
5432
  * @param limit 最多拉取条数
5029
5433
  */
5030
- async pullGroupV2(groupId, afterSeq = 0, limit = 50) {
5434
+ async _pullGroupV2(groupId, afterSeq = 0, limit = 50) {
5031
5435
  if (!this._v2Session) {
5032
5436
  throw new StateError('V2 session not initialized (not connected?)');
5033
5437
  }
@@ -5126,7 +5530,7 @@ export class AUNClient {
5126
5530
  this._saveSeqTrackerState();
5127
5531
  }
5128
5532
  if (messages.length > 0 && contigAdvanced && ackSeq > 0) {
5129
- this._safeAsync(this.ackGroupV2(gid, ackSeq).then(() => undefined));
5533
+ this._safeAsync(this._ackGroupV2(gid, ackSeq).then(() => undefined));
5130
5534
  }
5131
5535
  const nextAfter = Math.max(pageMaxSeq, nextAfterSeq);
5132
5536
  if (messages.length === 0 || nextAfter <= nextAfterSeq || result.has_more === false)
@@ -5144,7 +5548,7 @@ export class AUNClient {
5144
5548
  * @param groupId 群 ID
5145
5549
  * @param upToSeq 确认到此 seq;省略则用当前 contiguous
5146
5550
  */
5147
- async ackGroupV2(groupId, upToSeq) {
5551
+ async _ackGroupV2(groupId, upToSeq) {
5148
5552
  const gid = normalizeGroupId(groupId) || String(groupId ?? '').trim();
5149
5553
  if (!gid)
5150
5554
  throw new ValidationError('group.ack_messages requires group_id');
@@ -6221,7 +6625,7 @@ export class AUNClient {
6221
6625
  try {
6222
6626
  do {
6223
6627
  this._v2PullPending = false;
6224
- await this.pullV2();
6628
+ await this._pullV2();
6225
6629
  const newContig = ns ? this._seqTracker.getContiguousSeq(ns) : -1;
6226
6630
  this._clientLog.debug(`_onV2PushNotification pull done: contiguous_seq=${contigBefore}->${newContig} (push_seq=${pushSeq || 'null'})`);
6227
6631
  } while (this._v2PullPending);