@agentunion/fastaun-browser 0.3.6 → 0.4.1

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 (75) hide show
  1. package/CHANGELOG.md +24 -0
  2. 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
  3. 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 +1697 -0
  4. package/_packed_docs/CHANGELOG.md +24 -0
  5. package/_packed_docs/INDEX.md +17 -11
  6. package/_packed_docs/KITE_DOCS_GUIDE.md +11 -10
  7. package/_packed_docs/sdk/01-/345/277/253/351/200/237/345/274/200/345/247/213.md +134 -158
  8. package/_packed_docs/sdk/02-WebSocket/345/215/217/350/256/256.md +11 -7
  9. package/_packed_docs/sdk/03-/346/240/270/345/277/203/346/246/202/345/277/265.md +98 -119
  10. package/_packed_docs/sdk/04-/350/277/236/346/216/245/344/270/216/350/256/244/350/257/201.md +147 -374
  11. package/_packed_docs/sdk/05-E2EE/345/212/240/345/257/206/351/200/232/344/277/241.md +153 -153
  12. package/_packed_docs/sdk/06-API/346/211/213/345/206/214.md +168 -1383
  13. package/_packed_docs/sdk/07-/351/224/231/350/257/257/345/244/204/347/220/206.md +71 -91
  14. package/_packed_docs/sdk/08-/346/234/200/344/275/263/345/256/236/350/267/265.md +76 -63
  15. package/_packed_docs/sdk/09-custody-api-manual.md +7 -6
  16. package/_packed_docs/sdk/09-meta-rpc-manual.md +13 -14
  17. package/_packed_docs/sdk/AUN_DOCS_GUIDE.md +37 -49
  18. package/_packed_docs/sdk/INDEX.md +72 -98
  19. package/_packed_docs/sdk/README.md +85 -266
  20. package/dist/aid-store.d.ts +125 -0
  21. package/dist/aid-store.d.ts.map +1 -0
  22. package/dist/aid-store.js +841 -0
  23. package/dist/aid-store.js.map +1 -0
  24. package/dist/aid.d.ts +56 -0
  25. package/dist/aid.d.ts.map +1 -0
  26. package/dist/aid.js +112 -0
  27. package/dist/aid.js.map +1 -0
  28. package/dist/auth.js +1 -1
  29. package/dist/auth.js.map +1 -1
  30. package/dist/bundle.js +1630 -1901
  31. package/dist/cert-utils.d.ts +26 -0
  32. package/dist/cert-utils.d.ts.map +1 -0
  33. package/dist/cert-utils.js +221 -0
  34. package/dist/cert-utils.js.map +1 -0
  35. package/dist/client.d.ts +89 -60
  36. package/dist/client.d.ts.map +1 -1
  37. package/dist/client.js +568 -160
  38. package/dist/client.js.map +1 -1
  39. package/dist/config.d.ts +0 -2
  40. package/dist/config.d.ts.map +1 -1
  41. package/dist/config.js +0 -2
  42. package/dist/config.js.map +1 -1
  43. package/dist/error-codes.d.ts +25 -0
  44. package/dist/error-codes.d.ts.map +1 -0
  45. package/dist/error-codes.js +31 -0
  46. package/dist/error-codes.js.map +1 -0
  47. package/dist/errors.d.ts +4 -0
  48. package/dist/errors.d.ts.map +1 -1
  49. package/dist/errors.js +4 -0
  50. package/dist/errors.js.map +1 -1
  51. package/dist/index.d.ts +6 -6
  52. package/dist/index.d.ts.map +1 -1
  53. package/dist/index.js +5 -5
  54. package/dist/index.js.map +1 -1
  55. package/dist/keystore/index.d.ts +1 -1
  56. package/dist/keystore/index.d.ts.map +1 -1
  57. package/dist/result.d.ts +19 -0
  58. package/dist/result.d.ts.map +1 -0
  59. package/dist/result.js +10 -0
  60. package/dist/result.js.map +1 -0
  61. package/dist/transport.d.ts +3 -0
  62. package/dist/transport.d.ts.map +1 -1
  63. package/dist/transport.js +16 -1
  64. package/dist/transport.js.map +1 -1
  65. package/dist/types.d.ts +13 -2
  66. package/dist/types.d.ts.map +1 -1
  67. package/dist/types.js +22 -0
  68. package/dist/types.js.map +1 -1
  69. package/dist/v2/e2ee/encrypt-p2p.js +1 -1
  70. package/dist/v2/e2ee/encrypt-p2p.js.map +1 -1
  71. package/dist/version.d.ts +2 -0
  72. package/dist/version.d.ts.map +1 -0
  73. package/dist/version.js +5 -0
  74. package/dist/version.js.map +1 -0
  75. package/package.json +2 -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,29 @@ 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) {
258
+ return String(aid ?? '').trim();
259
+ }
260
+ async function fetchWithTimeout(input, init, timeoutMs = AGENT_MD_HTTP_TIMEOUT_MS) {
261
+ const controller = new AbortController();
262
+ const timer = globalThis.setTimeout(() => controller.abort(), timeoutMs);
263
+ try {
264
+ return await fetch(input, { ...init, signal: controller.signal });
265
+ }
266
+ catch (error) {
267
+ if (controller.signal.aborted) {
268
+ throw new AUNError(`agent.md request timed out after ${timeoutMs}ms`);
269
+ }
270
+ throw error;
271
+ }
272
+ finally {
273
+ globalThis.clearTimeout(timer);
274
+ }
275
+ }
248
276
  /**
249
277
  * 跨域时将 Gateway URL 替换为 peer 所在域的 Gateway URL。
250
278
  *
@@ -581,6 +609,22 @@ function normalizeDeliveryModeConfig(raw, opts = {}) {
581
609
  affinity_ttl_ms: affinityTtlMs,
582
610
  };
583
611
  }
612
+ function assertClientOptions(value, label) {
613
+ if (value == null)
614
+ return;
615
+ if (typeof value !== 'object' || Array.isArray(value) || value instanceof AID) {
616
+ throw new ValidationError(`${label} must be an options object`);
617
+ }
618
+ }
619
+ function clientOptionsConfig(options) {
620
+ const raw = { ...(options ?? {}) };
621
+ if (Object.prototype.hasOwnProperty.call(raw, 'aid')) {
622
+ throw new ValidationError('AUNClient options must not include aid; pass an AID object as the first argument');
623
+ }
624
+ delete raw.debug;
625
+ delete raw.protected_headers;
626
+ return raw;
627
+ }
584
628
  /**
585
629
  * AUN Core SDK 客户端 — 浏览器版本。
586
630
  *
@@ -601,6 +645,8 @@ export class AUNClient {
601
645
  _aid = null;
602
646
  _identity = null;
603
647
  _state = 'idle';
648
+ _currentAid = null;
649
+ _instanceProtectedHeaders = null;
604
650
  _gatewayUrl = null;
605
651
  _deviceId;
606
652
  _slotId;
@@ -615,12 +661,6 @@ export class AUNClient {
615
661
  _keystore;
616
662
  _auth;
617
663
  _transport;
618
- /** 认证命名空间 */
619
- auth;
620
- /** AID 托管命名空间 */
621
- custody;
622
- /** 元数据命名空间(心跳、状态、信任根管理) */
623
- meta;
624
664
  // E2EE 编排状态(内存缓存)
625
665
  _certCache = new Map();
626
666
  // 后台任务 handle(浏览器 setInterval/setTimeout)
@@ -661,7 +701,7 @@ export class AUNClient {
661
701
  _localAgentMdEtag = '';
662
702
  /** gateway 在 RPC envelope._meta.agent_md_etag 注入的服务端 etag;纯观察,无下游依赖。 */
663
703
  _remoteAgentMdEtag = '';
664
- /** 浏览器侧 AgentMDs 逻辑根目录,正文映射到 IndexedDB 里的 {aid}/agent.md。 */
704
+ /** 浏览器侧 AIDs 逻辑根目录,正文映射到 IndexedDB 里的 {aid}/agent.md。 */
665
705
  _agentMdPath = '';
666
706
  _agentMdCache = new Map();
667
707
  _agentMdFetchInflight = new Set();
@@ -686,6 +726,14 @@ export class AUNClient {
686
726
  _reconnectActive = false;
687
727
  _reconnectAbort = null;
688
728
  _serverKicked = false;
729
+ // 重连状态追踪(对齐 Python client.py)
730
+ _nextRetryAt = null;
731
+ _retryAttempt = 0;
732
+ _retryMaxAttempts = 0;
733
+ _lastError = null;
734
+ _lastErrorCode = null;
735
+ /** 对端 AID 缓存(aid string → AID 对象) */
736
+ _peerCache = new Map();
689
737
  /**
690
738
  * 缓存最近一次服务端 gateway.disconnect 信息(含 code/reason/detail),
691
739
  * 让后续 connection.state(terminal_failed) 也能携带 detail(如配额超限信息)。
@@ -699,17 +747,25 @@ export class AUNClient {
699
747
  _logKeystore;
700
748
  _logDiscovery;
701
749
  _logEvents;
702
- constructor(config, _debug = false) {
703
- const rawConfig = config ?? {};
750
+ constructor(aid) {
751
+ const inputAid = aid instanceof AID ? aid : null;
752
+ if (typeof aid === 'string') {
753
+ throw new ValidationError('AUNClient aid must be an AID object, not a string');
754
+ }
755
+ const options = {};
756
+ const rawConfig = clientOptionsConfig(options);
757
+ if (inputAid)
758
+ rawConfig.aun_path = inputAid.aunPath;
759
+ const _debug = false;
704
760
  this.configModel = createConfig(rawConfig);
705
- const initAid = String(rawConfig.aid ?? '').trim() || null;
761
+ const initAid = inputAid ? inputAid.aid : null;
706
762
  this.config = {
707
763
  aun_path: this.configModel.aunPath,
708
764
  root_ca_path: this.configModel.rootCaPem,
709
765
  seed_password: this.configModel.seedPassword,
710
766
  };
711
767
  this._agentMdPath = this._agentMdDefaultRoot();
712
- this._deviceId = getDeviceId();
768
+ this._deviceId = (inputAid?.deviceId) || getDeviceId();
713
769
  // Logger 必须最早初始化(其他子模块构造时通过 logger 输出)
714
770
  this._logger = new AUNLogger({ debug: _debug, aunPath: this.configModel.aunPath });
715
771
  this._logger.bindDeviceId(this._deviceId);
@@ -723,7 +779,7 @@ export class AUNClient {
723
779
  this._dispatcher = new EventDispatcher();
724
780
  this._discovery = new GatewayDiscovery();
725
781
  this._keystore = new IndexedDBKeyStore({ encryptionSeed: this.configModel.seedPassword ?? undefined });
726
- this._slotId = '';
782
+ this._slotId = inputAid?.slotId || 'default';
727
783
  this._connectDeliveryMode = normalizeDeliveryModeConfig({ mode: 'fanout' });
728
784
  this._defaultConnectDeliveryMode = { ...this._connectDeliveryMode };
729
785
  this._auth = new AuthFlow({
@@ -746,9 +802,21 @@ export class AUNClient {
746
802
  this._clientLog.debug(`agent.md meta observer skipped: ${String(exc)}`);
747
803
  });
748
804
  });
749
- this.auth = new AuthNamespace(this);
750
- this.custody = new CustodyNamespace(this);
751
- this.meta = new MetaNamespace(this);
805
+ if (inputAid) {
806
+ if (!inputAid.isPrivateKeyValid())
807
+ throw new StateError('AUNClient requires an AID with a valid private key');
808
+ this._currentAid = inputAid;
809
+ this._identity = {
810
+ aid: inputAid.aid,
811
+ private_key_pem: inputAid._privateKeyPem ?? '',
812
+ public_key_der_b64: inputAid.publicKey,
813
+ cert: inputAid.certPem,
814
+ };
815
+ this._state = 'disconnected';
816
+ }
817
+ if (options?.protected_headers !== undefined) {
818
+ this.setProtectedHeaders(options.protected_headers);
819
+ }
752
820
  // 注入 logger 到各子模块(构造时未传 logger,构造后通过 setLogger 注入)
753
821
  this._auth.setLogger(this._logAuth);
754
822
  this._transport.setLogger(this._logTransport);
@@ -756,18 +824,9 @@ export class AUNClient {
756
824
  if (typeof this._discovery.setLogger === 'function') {
757
825
  this._discovery.setLogger(this._logger.for('aun_core.discovery'));
758
826
  }
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
827
  if (typeof this._keystore.setLogger === 'function') {
766
828
  this._keystore.setLogger(this._logKeystore);
767
829
  }
768
- if (typeof this.meta.setLogger === 'function') {
769
- this.meta.setLogger(this._logger.for('aun_core.namespace.meta'));
770
- }
771
830
  // 内部订阅:推送消息 re-publish 给用户(V2 加密消息走 _raw.peer.v2.message_received)
772
831
  this._dispatcher.subscribe('_raw.message.received', (data) => {
773
832
  this._onRawMessageReceived(data);
@@ -818,17 +877,182 @@ export class AUNClient {
818
877
  get aid() {
819
878
  return this._aid;
820
879
  }
821
- setAgentMdPath(root) {
880
+ _setAgentMdRoot(root) {
822
881
  const next = String(root ?? '').trim() || this._agentMdDefaultRoot();
823
882
  this._agentMdPath = next;
824
883
  this._agentMdCache.clear();
825
884
  return next;
826
885
  }
827
- setAgentMDPath(root) {
828
- return this.setAgentMdPath(root);
886
+ async _resolveAgentMdUrl(aid) {
887
+ const target = String(aid ?? '').trim();
888
+ if (!target)
889
+ throw new ValidationError('agent.md requires non-empty aid');
890
+ let gatewayUrl = String(this._gatewayUrl ?? '').trim();
891
+ if (!gatewayUrl) {
892
+ try {
893
+ gatewayUrl = await this._resolveGatewayForAid(target);
894
+ }
895
+ catch {
896
+ gatewayUrl = '';
897
+ }
898
+ }
899
+ const authority = agentMdAuthority(target);
900
+ return `${agentMdHttpScheme(gatewayUrl)}://${authority}/agent.md`;
901
+ }
902
+ async _ensureAgentMdUploadToken(aid, gatewayUrl) {
903
+ let identity = await this._auth.loadIdentityOrNone(aid);
904
+ if (!identity && this._identity && String(this._identity.aid ?? '') === aid) {
905
+ identity = this._identity;
906
+ }
907
+ if (!identity) {
908
+ throw new StateError('no local identity found, register or load an AID first');
909
+ }
910
+ const cachedToken = String(identity.access_token ?? '');
911
+ const expiresAt = this._auth.getAccessTokenExpiry(identity);
912
+ if (cachedToken && (expiresAt === null || expiresAt > Date.now() / 1000 + 30)) {
913
+ return cachedToken;
914
+ }
915
+ if (identity.refresh_token) {
916
+ try {
917
+ const refreshed = await this._auth.refreshCachedTokens(gatewayUrl, identity);
918
+ const refreshedToken = String(refreshed.access_token ?? '');
919
+ const refreshedExpiry = this._auth.getAccessTokenExpiry(refreshed);
920
+ if (refreshedToken && (refreshedExpiry === null || refreshedExpiry > Date.now() / 1000 + 30)) {
921
+ this._identity = refreshed;
922
+ return refreshedToken;
923
+ }
924
+ }
925
+ catch {
926
+ // refresh 失败时回退到完整 authenticate。
927
+ }
928
+ }
929
+ const result = await this._auth.authenticate(gatewayUrl, aid);
930
+ const token = String(result.access_token ?? '');
931
+ if (!token)
932
+ throw new StateError('authenticate did not return access_token');
933
+ const fallbackIdentity = {
934
+ ...identity,
935
+ access_token: token,
936
+ refresh_token: String(result.refresh_token ?? identity.refresh_token ?? ''),
937
+ };
938
+ const fallbackExpiresAt = Number(result.expires_at ?? identity.expires_at ?? NaN);
939
+ if (Number.isFinite(fallbackExpiresAt))
940
+ fallbackIdentity.expires_at = fallbackExpiresAt;
941
+ this._identity = await this._auth.loadIdentityOrNone(aid) ?? fallbackIdentity;
942
+ return token;
943
+ }
944
+ async _uploadAgentMd(content) {
945
+ const target = String(this._aid ?? this._currentAid?.aid ?? '').trim();
946
+ if (!target)
947
+ throw new StateError('uploadAgentMd requires local AID');
948
+ const gatewayUrl = await this._resolveGatewayForAid(target);
949
+ this._gatewayUrl = gatewayUrl;
950
+ const token = await this._ensureAgentMdUploadToken(target, gatewayUrl);
951
+ const response = await fetchWithTimeout(await this._resolveAgentMdUrl(target), {
952
+ method: 'PUT',
953
+ headers: {
954
+ Authorization: `Bearer ${token}`,
955
+ 'Content-Type': 'text/markdown; charset=utf-8',
956
+ },
957
+ body: content,
958
+ });
959
+ if (response.status === 404) {
960
+ throw new NotFoundError(`agent.md endpoint not found for aid: ${target}`);
961
+ }
962
+ if (!response.ok) {
963
+ const message = (await response.text()).trim();
964
+ throw new AUNError(`upload agent.md failed: HTTP ${response.status}${message ? ` - ${message}` : ''}`);
965
+ }
966
+ const payload = await response.json();
967
+ if (!isJsonObject(payload))
968
+ throw new AUNError('upload agent.md returned invalid JSON payload');
969
+ return payload;
970
+ }
971
+ async _downloadAgentMd(aid) {
972
+ const target = String(aid ?? '').trim();
973
+ if (!target)
974
+ throw new ValidationError('downloadAgentMd requires non-empty aid');
975
+ const cached = this._agentMdCache.get(target);
976
+ const url = await this._resolveAgentMdUrl(target);
977
+ const response = await fetchWithTimeout(url, {
978
+ method: 'GET',
979
+ headers: { Accept: 'text/markdown' },
980
+ redirect: 'follow',
981
+ });
982
+ if (response.status === 304 && typeof cached?.text === 'string') {
983
+ return String(cached.text);
984
+ }
985
+ if (response.status === 404) {
986
+ throw new NotFoundError(`agent.md not found for aid: ${target}`);
987
+ }
988
+ if (!response.ok) {
989
+ const message = (await response.text()).trim();
990
+ throw new AUNError(`download agent.md failed: HTTP ${response.status}${message ? ` - ${message}` : ''}`);
991
+ }
992
+ const text = await response.text();
993
+ const etag = String(response.headers?.get('ETag') ?? response.headers?.get('etag') ?? '').trim();
994
+ const lastModified = String(response.headers?.get('Last-Modified') ?? response.headers?.get('last-modified') ?? '').trim();
995
+ this._agentMdCache.set(target, {
996
+ ...(cached ?? {}),
997
+ text,
998
+ etag,
999
+ lastModified,
1000
+ remote_etag: etag,
1001
+ last_modified: lastModified,
1002
+ });
1003
+ return text;
1004
+ }
1005
+ async _headAgentMd(aid) {
1006
+ const target = String(aid ?? '').trim();
1007
+ if (!target)
1008
+ throw new ValidationError('headAgentMd requires non-empty aid');
1009
+ const response = await fetchWithTimeout(await this._resolveAgentMdUrl(target), {
1010
+ method: 'HEAD',
1011
+ headers: { Accept: 'text/markdown' },
1012
+ });
1013
+ const etag = String(response.headers?.get('ETag') ?? response.headers?.get('etag') ?? '').trim();
1014
+ const lastModified = String(response.headers?.get('Last-Modified') ?? response.headers?.get('last-modified') ?? '').trim();
1015
+ if (response.status === 404) {
1016
+ return { aid: target, found: false, etag: '', last_modified: '', status: 404 };
1017
+ }
1018
+ if (!response.ok) {
1019
+ throw new AUNError(`head agent.md failed: HTTP ${response.status}`);
1020
+ }
1021
+ const cached = this._agentMdCache.get(target) ?? {};
1022
+ this._agentMdCache.set(target, {
1023
+ ...cached,
1024
+ etag,
1025
+ lastModified,
1026
+ remote_etag: etag,
1027
+ last_modified: lastModified,
1028
+ });
1029
+ return { aid: target, found: true, etag, last_modified: lastModified, status: response.status };
829
1030
  }
830
- SetAgentMDPath(root) {
831
- return this.setAgentMdPath(root);
1031
+ async _verifyAgentMd(content, aid) {
1032
+ const target = String(aid ?? '').trim();
1033
+ if (!target)
1034
+ throw new ValidationError('verifyAgentMd requires non-empty aid');
1035
+ let peer = target === this._currentAid?.aid ? this._currentAid : null;
1036
+ if (!peer) {
1037
+ let certPem = String(await this._keystore.loadCert(target) ?? '').trim();
1038
+ if (!certPem) {
1039
+ certPem = String(await this._fetchPeerCert(target) ?? '').trim();
1040
+ }
1041
+ if (!certPem)
1042
+ throw new NotFoundError(`certificate not found for aid: ${target}`);
1043
+ peer = await AID.create({
1044
+ aid: target,
1045
+ aunPath: this.configModel.aunPath,
1046
+ certPem,
1047
+ privateKeyPem: null,
1048
+ certValid: true,
1049
+ privateKeyValid: false,
1050
+ });
1051
+ }
1052
+ const result = await peer.verifyAgentMd(content);
1053
+ if (!result.ok)
1054
+ throw new AUNError(result.error.message);
1055
+ return { ...result.data, verified: result.data.status === 'verified' };
832
1056
  }
833
1057
  /**
834
1058
  * 浏览器版本 publishAgentMd。默认从 {agentMdPath}/{self_aid}/agent.md 的等价 IndexedDB 正文读取,
@@ -856,8 +1080,12 @@ export class AUNClient {
856
1080
  if (localContent === null || localContent.length === 0) {
857
1081
  throw new ValidationError('publishAgentMd requires local agent.md content');
858
1082
  }
859
- const signed = await this.auth.signAgentMd(localContent);
860
- const result = await this.auth.uploadAgentMd(signed);
1083
+ const signedResult = await this._currentAid?.signAgentMd(localContent);
1084
+ if (!signedResult?.ok) {
1085
+ throw new StateError(signedResult?.error.message ?? 'publishAgentMd requires a valid local AID private key');
1086
+ }
1087
+ const signed = signedResult.data.signed;
1088
+ const result = await this._uploadAgentMd(signed);
861
1089
  this._localAgentMdEtag = await this._agentMdContentEtag(signed);
862
1090
  const remoteEtag = isJsonObject(result) ? String(result.etag ?? '').trim() : '';
863
1091
  if (remoteEtag)
@@ -877,13 +1105,13 @@ export class AUNClient {
877
1105
  * 浏览器版本 fetchAgentMd。aid 缺省时取自身;下载后的正文固定写入
878
1106
  * {agentMdPath}/{aid}/agent.md 的等价 IndexedDB 正文,agentmd.json 只保存元数据。
879
1107
  */
880
- async fetchAgentMd(aid) {
1108
+ async _fetchAgentMdCache(aid) {
881
1109
  const target = String(aid ?? this._aid ?? '').trim();
882
1110
  if (!target) {
883
1111
  throw new ValidationError('fetchAgentMd requires aid (or local AID)');
884
1112
  }
885
- const content = await this.auth.downloadAgentMd(target);
886
- const signature = await this.auth.verifyAgentMd(content, { aid: target });
1113
+ const content = await this._downloadAgentMd(target);
1114
+ const signature = await this._verifyAgentMd(content, target);
887
1115
  const isSelf = target === (this._aid ?? '');
888
1116
  const localEtag = await this._agentMdContentEtag(content);
889
1117
  const cacheMeta = this._agentMdAuthCacheMeta(target);
@@ -932,7 +1160,7 @@ export class AUNClient {
932
1160
  return String(this._aid ?? '').trim();
933
1161
  }
934
1162
  _agentMdDefaultRoot() {
935
- return this._joinAgentMdPath(this.configModel.aunPath || '.', 'AgentMDs');
1163
+ return this._joinAgentMdPath(this.configModel.aunPath || '.', 'AIDs');
936
1164
  }
937
1165
  _joinAgentMdPath(base, name) {
938
1166
  const left = String(base ?? '').trim().replace(/[\\/]+$/g, '');
@@ -1039,8 +1267,7 @@ export class AUNClient {
1039
1267
  }
1040
1268
  _agentMdAuthCacheMeta(aid) {
1041
1269
  try {
1042
- const store = this.auth._agentMdCache;
1043
- const record = store?.get(String(aid ?? '').trim());
1270
+ const record = this._agentMdCache.get(String(aid ?? '').trim());
1044
1271
  return record && typeof record === 'object' ? { ...record } : {};
1045
1272
  }
1046
1273
  catch {
@@ -1165,7 +1392,7 @@ export class AUNClient {
1165
1392
  return;
1166
1393
  this._agentMdFetchInflight.add(target);
1167
1394
  try {
1168
- await this.fetchAgentMd(target);
1395
+ await this._fetchAgentMdCache(target);
1169
1396
  }
1170
1397
  catch (err) {
1171
1398
  await this._saveAgentMdRecord(target, {
@@ -1226,7 +1453,7 @@ export class AUNClient {
1226
1453
  }
1227
1454
  await this._observeAgentMdMeta(senderAid, String(sender.etag ?? '').trim(), String(sender.last_modified ?? sender.lastModified ?? '').trim(), 'envelope');
1228
1455
  }
1229
- async checkAgentMd(aid, maxUnsyncedDays = 0) {
1456
+ async _checkAgentMdCache(aid, maxUnsyncedDays = 0) {
1230
1457
  const target = String(aid ?? this._aid ?? '').trim();
1231
1458
  if (!target)
1232
1459
  throw new ValidationError('checkAgentMd requires aid (or local AID)');
@@ -1256,7 +1483,7 @@ export class AUNClient {
1256
1483
  const now = Date.now();
1257
1484
  let remote;
1258
1485
  try {
1259
- remote = await this.auth.headAgentMd(target);
1486
+ remote = await this._headAgentMd(target);
1260
1487
  }
1261
1488
  catch (err) {
1262
1489
  await this._saveAgentMdRecord(target, { checked_at: now, remote_status: 'error', last_error: err instanceof Error ? err.message : String(err) });
@@ -1310,7 +1537,116 @@ export class AUNClient {
1310
1537
  }
1311
1538
  }
1312
1539
  get state() {
1313
- return this._state;
1540
+ return this._publicState(this._state);
1541
+ }
1542
+ _publicState(state) {
1543
+ return STATE_TO_PUBLIC[state] ?? state;
1544
+ }
1545
+ get currentAid() {
1546
+ return this._currentAid;
1547
+ }
1548
+ get hasIdentity() {
1549
+ return this._currentAid !== null && this.state !== ConnectionState.CLOSED;
1550
+ }
1551
+ get canSign() {
1552
+ return this.hasIdentity && !!this._currentAid?.isPrivateKeyValid();
1553
+ }
1554
+ get canConnect() {
1555
+ return this.hasIdentity && this.state !== ConnectionState.CLOSED;
1556
+ }
1557
+ get canSend() {
1558
+ return this.state === ConnectionState.READY;
1559
+ }
1560
+ get isReady() { return this.canSend; }
1561
+ get isOnline() {
1562
+ return this.state === ConnectionState.READY
1563
+ || this.state === ConnectionState.RECONNECTING
1564
+ || this.state === ConnectionState.RETRY_BACKOFF;
1565
+ }
1566
+ get isClosed() { return this.state === ConnectionState.CLOSED; }
1567
+ get aunPath() { return this.hasIdentity ? this._currentAid?.aunPath ?? this.configModel.aunPath : null; }
1568
+ /** 下次重连时间(仅在 retry_backoff 状态时非 null,对齐 Python next_retry_at) */
1569
+ get nextRetryAt() {
1570
+ return this.state === ConnectionState.RETRY_BACKOFF ? this._nextRetryAt : null;
1571
+ }
1572
+ /** 距下次重连的剩余秒数(仅在 retry_backoff 状态时非 null,对齐 Python next_retry_in_seconds) */
1573
+ get nextRetryInSeconds() {
1574
+ const t = this.nextRetryAt;
1575
+ if (t === null)
1576
+ return null;
1577
+ return Math.max(0, (t.getTime() - Date.now()) / 1000);
1578
+ }
1579
+ /** 当前重连尝试次数(对齐 Python retry_attempt) */
1580
+ get retryAttempt() { return this._retryAttempt; }
1581
+ /** 最大重连次数(0 = 无限,对齐 Python retry_max_attempts) */
1582
+ get retryMaxAttempts() { return this._retryMaxAttempts; }
1583
+ /** 最近一次错误(对齐 Python last_error) */
1584
+ get lastError() { return this._lastError; }
1585
+ /** 最近一次错误码(对齐 Python last_error_code) */
1586
+ get lastErrorCode() { return this._lastErrorCode; }
1587
+ loadIdentity(aid) {
1588
+ if (!aid?.isPrivateKeyValid())
1589
+ throw new StateError('loadIdentity requires an AID with a valid private key');
1590
+ const publicState = this.state;
1591
+ if (publicState !== ConnectionState.NO_IDENTITY && publicState !== ConnectionState.CLOSED) {
1592
+ throw new StateError(`loadIdentity not allowed in state ${publicState}`);
1593
+ }
1594
+ this._currentAid = aid;
1595
+ this._aid = aid.aid;
1596
+ this._identity = {
1597
+ aid: aid.aid,
1598
+ private_key_pem: aid._privateKeyPem ?? '',
1599
+ public_key_der_b64: aid.publicKey,
1600
+ cert: aid.certPem,
1601
+ };
1602
+ this._auth._aid = aid.aid;
1603
+ this._state = 'disconnected';
1604
+ this._closing = false;
1605
+ }
1606
+ setProtectedHeaders(headers) {
1607
+ if (!headers) {
1608
+ this._instanceProtectedHeaders = null;
1609
+ return;
1610
+ }
1611
+ const cleaned = {};
1612
+ for (const [key, value] of Object.entries(headers)) {
1613
+ if (key === '_auth')
1614
+ continue;
1615
+ cleaned[String(key)] = String(value);
1616
+ }
1617
+ this._instanceProtectedHeaders = Object.keys(cleaned).length ? cleaned : null;
1618
+ }
1619
+ getProtectedHeaders() {
1620
+ return this._instanceProtectedHeaders ? { ...this._instanceProtectedHeaders } : null;
1621
+ }
1622
+ cachePeer(aid) {
1623
+ if (!this.hasIdentity)
1624
+ throw new StateError('cachePeer requires a loaded identity');
1625
+ if (!aid.isCertValid())
1626
+ throw new ValidationError('cachePeer requires an AID with a valid certificate');
1627
+ this._peerCache.set(aid.aid, aid);
1628
+ return aid;
1629
+ }
1630
+ getPeer(aid) {
1631
+ if (!this.hasIdentity)
1632
+ throw new StateError('getPeer requires a loaded identity');
1633
+ return this._peerCache.get(String(aid ?? '').trim()) ?? null;
1634
+ }
1635
+ async lookupPeer(aid) {
1636
+ if (!this.hasIdentity)
1637
+ throw new StateError('lookupPeer requires a loaded identity');
1638
+ const target = String(aid ?? '').trim();
1639
+ if (!target)
1640
+ throw new ValidationError('lookupPeer requires non-empty aid');
1641
+ const cached = this._peerCache.get(target);
1642
+ if (cached)
1643
+ return cached;
1644
+ throw new NotFoundError(`peer not found in cache: ${target}`);
1645
+ }
1646
+ peers() {
1647
+ if (!this.hasIdentity)
1648
+ throw new StateError('peers requires a loaded identity');
1649
+ return [...this._peerCache.entries()].sort(([a], [b]) => a.localeCompare(b)).map(([, v]) => v);
1314
1650
  }
1315
1651
  get gatewayUrl() {
1316
1652
  return this._gatewayUrl;
@@ -1325,36 +1661,81 @@ export class AUNClient {
1325
1661
  get gatewayHealth() {
1326
1662
  return this._discovery.lastHealthy;
1327
1663
  }
1328
- /** 主动检查 gateway 可用性(GET /health) */
1329
- async checkGatewayHealth(gatewayUrl, timeout = 5000) {
1664
+ // ── 生命周期 ──────────────────────────────────────
1665
+ /** 仅认证当前身份,获取/刷新 token,但不建立长连接。 */
1666
+ async authenticate(options = {}) {
1330
1667
  const tStart = Date.now();
1331
- this._clientLog.debug(`checkGatewayHealth enter: gateway=${gatewayUrl} timeout=${timeout}`);
1668
+ const target = this._currentAid?.aid ?? this._aid ?? '';
1669
+ if (!target || !this._currentAid?.isPrivateKeyValid()) {
1670
+ throw new StateError('authenticate requires a loaded AID with a valid private key');
1671
+ }
1672
+ const publicState = this.state;
1673
+ if (publicState !== ConnectionState.STANDBY) {
1674
+ throw new StateError(`authenticate not allowed in state ${publicState}`);
1675
+ }
1676
+ if ('aid' in options || 'access_token' in options || 'token' in options || 'kite_token' in options) {
1677
+ throw new ValidationError('authenticate options must not include aid or token fields; load an AID object first');
1678
+ }
1679
+ this._state = 'connecting';
1332
1680
  try {
1333
- const result = await this._discovery.checkHealth(gatewayUrl, timeout);
1334
- this._clientLog.debug(`checkGatewayHealth exit: elapsed=${Date.now() - tStart}ms healthy=${result}`);
1681
+ const gateway = String(options.gateway ?? this._gatewayUrl ?? await this._resolveGatewayForAid(target)).trim();
1682
+ const result = await this._auth.authenticate(gateway, target);
1683
+ this._gatewayUrl = String(result.gateway ?? gateway);
1684
+ this._identity = await this._auth.loadIdentityOrNone(target);
1685
+ this._state = 'authenticated';
1686
+ this._clientLog.debug(`authenticate exit: elapsed=${Date.now() - tStart}ms aid=${target}`);
1335
1687
  return result;
1336
1688
  }
1337
1689
  catch (err) {
1338
- this._clientLog.debug(`checkGatewayHealth exit (error): elapsed=${Date.now() - tStart}ms err=${err instanceof Error ? err.message : String(err)}`);
1690
+ this._state = 'disconnected';
1691
+ this._clientLog.debug(`authenticate exit (error): elapsed=${Date.now() - tStart}ms err=${err instanceof Error ? err.message : String(err)}`);
1339
1692
  throw err;
1340
1693
  }
1341
1694
  }
1342
- // ── 生命周期 ──────────────────────────────────────
1343
- /**
1344
- * 连接到 Gateway。
1345
- *
1346
- * @param auth - 认证参数,必须包含 access_token 和 gateway
1347
- * @param options - 可选的会话选项(auto_reconnect, heartbeat_interval 等)
1348
- */
1349
- async connect(auth, options) {
1695
+ /** 连接到 Gateway;身份来自构造函数或 loadIdentity(aid),认证由 SDK 内部自动完成。 */
1696
+ async connect(opts) {
1350
1697
  const tStart = Date.now();
1698
+ const options = {};
1699
+ if (opts?.auto_reconnect !== undefined)
1700
+ options.auto_reconnect = opts.auto_reconnect;
1701
+ if (opts?.heartbeat_interval !== undefined)
1702
+ options.heartbeat_interval = opts.heartbeat_interval;
1703
+ if (opts?.connect_timeout !== undefined || opts?.call_timeout !== undefined) {
1704
+ options.timeouts = {
1705
+ ...(opts.connect_timeout !== undefined ? { connect: opts.connect_timeout } : {}),
1706
+ ...(opts.call_timeout !== undefined ? { call: opts.call_timeout } : {}),
1707
+ };
1708
+ }
1709
+ if (opts?.retry_initial_delay !== undefined || opts?.retry_max_delay !== undefined || opts?.retry_max_attempts !== undefined) {
1710
+ options.retry = {
1711
+ initial_delay: opts.retry_initial_delay ?? 1,
1712
+ max_delay: opts.retry_max_delay ?? 64,
1713
+ max_attempts: opts.retry_max_attempts ?? 0,
1714
+ };
1715
+ }
1351
1716
  this._clientLog.debug(`connect enter: state=${this._state} aid=${this._aid ?? '-'}`);
1352
- if (this._state !== 'idle' && this._state !== 'closed' && this._state !== 'disconnected') {
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}`);
1731
+ }
1732
+ // gateway 来自 authenticate() 缓存的 this._gatewayUrl;未认证则自动 authenticate()
1733
+ if (!this._gatewayUrl) {
1734
+ await this.authenticate();
1355
1735
  }
1356
1736
  this._state = 'connecting';
1357
- const params = { ...auth, ...options };
1737
+ const gateway = String(this._gatewayUrl ?? '').trim();
1738
+ const params = { ...options, gateway };
1358
1739
  const normalized = this._normalizeConnectParams(params);
1359
1740
  this._sessionParams = normalized;
1360
1741
  this._sessionOptions = this._buildSessionOptions(normalized);
@@ -1365,7 +1746,7 @@ export class AUNClient {
1365
1746
  for (const gw of gateways) {
1366
1747
  try {
1367
1748
  const gwParams = { ...normalized, gateway: gw };
1368
- await this._connectOnce(gwParams, false);
1749
+ await this._connectOnce(gwParams, true);
1369
1750
  this._clientLog.debug(`connect exit: elapsed=${Date.now() - tStart}ms state=${this._state}`);
1370
1751
  return;
1371
1752
  }
@@ -1380,7 +1761,7 @@ export class AUNClient {
1380
1761
  }
1381
1762
  }
1382
1763
  if (this._state === 'connecting' || this._state === 'authenticating') {
1383
- this._state = 'disconnected';
1764
+ this._state = 'terminal_failed';
1384
1765
  }
1385
1766
  this._clientLog.debug(`connect exit (error): elapsed=${Date.now() - tStart}ms err=${lastErr instanceof Error ? lastErr.message : String(lastErr)}`);
1386
1767
  throw lastErr;
@@ -1402,56 +1783,9 @@ export class AUNClient {
1402
1783
  }
1403
1784
  await this._transport.close();
1404
1785
  this._state = 'disconnected';
1405
- await this._dispatcher.publish('connection.state', { state: this._state });
1786
+ await this._dispatcher.publish('state_change', { state: this._publicState(this._state) });
1406
1787
  this._clientLog.debug(`disconnect exit: elapsed=${Date.now() - tStart}ms`);
1407
1788
  }
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
1789
  /** 关闭连接 */
1456
1790
  async close() {
1457
1791
  const tStart = Date.now();
@@ -1480,7 +1814,7 @@ export class AUNClient {
1480
1814
  }
1481
1815
  await this._transport.close();
1482
1816
  this._state = 'closed';
1483
- await this._dispatcher.publish('connection.state', { state: this._state });
1817
+ await this._dispatcher.publish('state_change', { state: this._publicState(this._state) });
1484
1818
  this._resetSeqTrackingState();
1485
1819
  this._clientLog.debug(`close exit: elapsed=${Date.now() - tStart}ms`);
1486
1820
  }
@@ -1515,6 +1849,10 @@ export class AUNClient {
1515
1849
  throw new PermissionError(`legacy E2EE method is removed in this SDK: ${method}`);
1516
1850
  }
1517
1851
  const p = { ...(params ?? {}) };
1852
+ if (this._instanceProtectedHeaders && PROTECTED_HEADERS_METHODS.has(method)) {
1853
+ const existing = isJsonObject(p.protected_headers) ? p.protected_headers : {};
1854
+ p.protected_headers = { ...this._instanceProtectedHeaders, ...existing };
1855
+ }
1518
1856
  if (method === 'message.send' || method === 'group.send') {
1519
1857
  this._normalizeOutboundMessagePayload(p, method);
1520
1858
  }
@@ -1545,7 +1883,7 @@ export class AUNClient {
1545
1883
  throw new StateError('V2 session not initialized; encrypted message.send requires V2 (V1 E2EE removed)');
1546
1884
  }
1547
1885
  this._clientLog.debug('call route: message.send → V2 encrypted send');
1548
- return await this.sendV2(String(p.to ?? ''), p.payload ?? {}, {
1886
+ return await this._sendV2(String(p.to ?? ''), p.payload ?? {}, {
1549
1887
  messageId: String(p.message_id ?? '') || undefined,
1550
1888
  timestamp: p.timestamp,
1551
1889
  protectedHeaders: this._protectedHeadersFromParams(p),
@@ -1564,7 +1902,7 @@ export class AUNClient {
1564
1902
  throw new StateError('V2 session not initialized; encrypted group.send requires V2 (V1 E2EE removed)');
1565
1903
  }
1566
1904
  this._clientLog.debug('call route: group.send → V2 encrypted send');
1567
- return await this.sendGroupV2(String(p.group_id ?? ''), p.payload ?? {}, {
1905
+ return await this._sendGroupV2(String(p.group_id ?? ''), p.payload ?? {}, {
1568
1906
  messageId: String(p.message_id ?? '') || undefined,
1569
1907
  timestamp: p.timestamp,
1570
1908
  protectedHeaders: this._protectedHeadersFromParams(p),
@@ -1612,24 +1950,24 @@ export class AUNClient {
1612
1950
  // message.pull:V2 就绪时走 V2 pull
1613
1951
  if (method === 'message.pull' && this._v2Session) {
1614
1952
  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 });
1953
+ const messages = await this._pullV2(Number(p.after_seq ?? 0) || 0, Number(p.limit ?? 50) || 50, { force: p.force === true });
1616
1954
  return { messages };
1617
1955
  }
1618
1956
  // message.ack:V2 就绪时走 V2 ack
1619
1957
  if (method === 'message.ack' && this._v2Session) {
1620
1958
  this._clientLog.debug('call route: message.ack → V2 ack');
1621
- return await this.ackV2(Number(p.seq ?? p.up_to_seq ?? 0) || undefined);
1959
+ return await this._ackV2(Number(p.seq ?? p.up_to_seq ?? 0) || undefined);
1622
1960
  }
1623
1961
  // group.pull:V2 就绪时走 V2 pull
1624
1962
  if (method === 'group.pull' && this._v2Session && p.group_id) {
1625
1963
  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);
1964
+ 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
1965
  return { messages };
1628
1966
  }
1629
1967
  // group.ack_messages:V2 就绪时走 V2 ack
1630
1968
  if (method === 'group.ack_messages' && this._v2Session && p.group_id) {
1631
1969
  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);
1970
+ return await this._ackGroupV2(String(p.group_id), Number(p.seq ?? p.msg_seq ?? p.up_to_seq ?? 0) || undefined);
1633
1971
  }
1634
1972
  // 关键操作自动附加客户端签名
1635
1973
  if (SIGNED_METHODS.has(method)) {
@@ -1754,16 +2092,6 @@ export class AUNClient {
1754
2092
  }
1755
2093
  return await this._transport.call(method, p);
1756
2094
  }
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
2095
  // ── 事件 ──────────────────────────────────────────
1768
2096
  /**
1769
2097
  * 订阅事件。
@@ -1901,7 +2229,7 @@ export class AUNClient {
1901
2229
  this._gapFillDone.add(dedupKey);
1902
2230
  try {
1903
2231
  this._clientLog.debug(`_onRawGroupV2MessageCreated -> group.v2.pull group=${groupId} after_seq=${afterSeq}`);
1904
- const messages = await this.pullGroupV2(groupId, afterSeq, 50);
2232
+ const messages = await this._pullGroupV2(groupId, afterSeq, 50);
1905
2233
  this._clientLog.debug(`_onRawGroupV2MessageCreated pulled ${messages.length} msgs for group=${groupId}`);
1906
2234
  }
1907
2235
  finally {
@@ -3372,8 +3700,8 @@ export class AUNClient {
3372
3700
  }
3373
3701
  this._state = 'connected';
3374
3702
  this._connectedAt = Date.now();
3375
- await this._dispatcher.publish('connection.state', {
3376
- state: this._state,
3703
+ await this._dispatcher.publish('state_change', {
3704
+ state: this._publicState(this._state),
3377
3705
  gateway: gatewayUrl,
3378
3706
  });
3379
3707
  if (this._seqTrackerContext !== this._currentSeqTrackerContext()) {
@@ -3383,7 +3711,7 @@ export class AUNClient {
3383
3711
  this._startBackgroundTasks();
3384
3712
  // V2 E2EE: 初始化 session 并注册设备 SPK(与 Python `_init_v2_session` 对齐)
3385
3713
  try {
3386
- await this.initV2Session();
3714
+ await this._initV2Session();
3387
3715
  }
3388
3716
  catch (exc) {
3389
3717
  this._clientLog.warn(`V2 session init failed (non-fatal): ${String(exc)}`);
@@ -3402,6 +3730,57 @@ export class AUNClient {
3402
3730
  const gateways = this._resolveGateways(params);
3403
3731
  return gateways[0];
3404
3732
  }
3733
+ async _resolveGatewayForAid(aid) {
3734
+ const target = String(aid ?? this._aid ?? '').trim();
3735
+ if (!target)
3736
+ throw new StateError('gateway discovery requires a loaded AID');
3737
+ if (this._gatewayUrl)
3738
+ return this._gatewayUrl;
3739
+ try {
3740
+ const getMetadata = this._keystore.getMetadata;
3741
+ const raw = typeof getMetadata === 'function'
3742
+ ? String(await getMetadata.call(this._keystore, target, 'gateway_url') ?? '').trim()
3743
+ : '';
3744
+ if (raw) {
3745
+ const gateway = raw.startsWith('"') && raw.endsWith('"') ? String(JSON.parse(raw)).trim() : raw;
3746
+ if (gateway) {
3747
+ this._gatewayUrl = gateway;
3748
+ return gateway;
3749
+ }
3750
+ }
3751
+ }
3752
+ catch {
3753
+ // 缓存读取失败不影响发现流程。
3754
+ }
3755
+ const dotIdx = target.indexOf('.');
3756
+ const issuerDomain = dotIdx >= 0 ? target.slice(dotIdx + 1) : target;
3757
+ const portSuffix = '';
3758
+ const candidates = [
3759
+ `https://${target}${portSuffix}/.well-known/aun-gateway`,
3760
+ `https://gateway.${issuerDomain}${portSuffix}/.well-known/aun-gateway`,
3761
+ ];
3762
+ let lastError = null;
3763
+ for (const url of candidates) {
3764
+ try {
3765
+ const gateway = await this._discovery.discover(url);
3766
+ this._gatewayUrl = gateway;
3767
+ try {
3768
+ const setMetadata = this._keystore.setMetadata;
3769
+ if (typeof setMetadata === 'function') {
3770
+ await setMetadata.call(this._keystore, target, 'gateway_url', gateway);
3771
+ }
3772
+ }
3773
+ catch {
3774
+ // 缓存写入失败不影响连接。
3775
+ }
3776
+ return gateway;
3777
+ }
3778
+ catch (err) {
3779
+ lastError = err;
3780
+ }
3781
+ }
3782
+ throw lastError instanceof Error ? lastError : new ConnectionError(`gateway discovery failed for ${target}`);
3783
+ }
3405
3784
  _resolveGateways(params) {
3406
3785
  const topology = isJsonObject(params.topology) ? params.topology : null;
3407
3786
  if (topology) {
@@ -3451,12 +3830,13 @@ export class AUNClient {
3451
3830
  _normalizeConnectParams(params) {
3452
3831
  const request = { ...params };
3453
3832
  const accessToken = String(request.access_token ?? '');
3454
- if (!accessToken)
3455
- throw new StateError('connect requires non-empty access_token');
3456
3833
  const gateway = String(request.gateway ?? this._gatewayUrl ?? '');
3457
3834
  if (!gateway)
3458
3835
  throw new StateError('connect requires non-empty gateway');
3459
- request.access_token = accessToken;
3836
+ if (accessToken)
3837
+ request.access_token = accessToken;
3838
+ else
3839
+ delete request.access_token;
3460
3840
  request.gateway = gateway;
3461
3841
  request.device_id = this._deviceId;
3462
3842
  request.slot_id = normalizeInstanceId(request.slot_id ?? this._slotId, 'slot_id', { allowEmpty: true });
@@ -3676,6 +4056,11 @@ export class AUNClient {
3676
4056
  }
3677
4057
  try {
3678
4058
  identity = await this._auth.refreshCachedTokens(this._gatewayUrl, identity);
4059
+ // 刷新期间可能已断线,复检状态,避免写回 stale identity
4060
+ if (this._state !== 'connected') {
4061
+ scheduleRefresh();
4062
+ return;
4063
+ }
3679
4064
  this._identity = identity;
3680
4065
  if (this._sessionParams && identity.access_token) {
3681
4066
  this._sessionParams.access_token = identity.access_token;
@@ -3815,8 +4200,8 @@ export class AUNClient {
3815
4200
  this._state = 'disconnected';
3816
4201
  // 先停止后台任务,避免心跳/token刷新在重连期间继续触发
3817
4202
  this._stopBackgroundTasks();
3818
- await this._dispatcher.publish('connection.state', {
3819
- state: this._state,
4203
+ await this._dispatcher.publish('state_change', {
4204
+ state: this._publicState(this._state),
3820
4205
  error,
3821
4206
  });
3822
4207
  if (!this._sessionOptions.auto_reconnect)
@@ -3830,7 +4215,7 @@ export class AUNClient {
3830
4215
  this._clientLog.warn(`suppress auto-reconnect: ${reason}`);
3831
4216
  const disconnectInfo = this._lastDisconnectInfo ?? {};
3832
4217
  const eventPayload = {
3833
- state: this._state, error, reason,
4218
+ state: this._publicState(this._state), error, reason,
3834
4219
  };
3835
4220
  // 把服务端附带的结构化 detail(如配额超限信息)也带给应用层
3836
4221
  const detail = disconnectInfo.detail;
@@ -3840,7 +4225,7 @@ export class AUNClient {
3840
4225
  if (disconnectInfo.code !== undefined && disconnectInfo.code !== null) {
3841
4226
  eventPayload.code = disconnectInfo.code;
3842
4227
  }
3843
- await this._dispatcher.publish('connection.state', eventPayload);
4228
+ await this._dispatcher.publish('state_change', eventPayload);
3844
4229
  return;
3845
4230
  }
3846
4231
  // 1000 = 正常关闭, 1006 = 网络异常断开(无 close frame),其他 code = 服务端主动关闭
@@ -3858,34 +4243,51 @@ export class AUNClient {
3858
4243
  const maxAttempts = Number.isFinite(maxAttemptsRaw) && maxAttemptsRaw > 0 ? Math.floor(maxAttemptsRaw) : 0;
3859
4244
  // 服务端主动关闭时从 16s 起跳,避免重连风暴;网络断开从 initial_delay 起跳
3860
4245
  let delay = clampReconnectDelaySeconds(serverInitiated ? 16.0 : retry.initial_delay, serverInitiated ? 16.0 : 1.0, maxBaseDelay);
4246
+ this._retryAttempt = 0;
4247
+ this._retryMaxAttempts = maxAttempts;
3861
4248
  for (let attempt = 1; !this._reconnectAbort?.signal.aborted; attempt++) {
3862
4249
  // R1 fix: max_attempts 检查在循环顶部,覆盖所有路径(含 health-fail)
3863
4250
  if (maxAttempts > 0 && attempt > maxAttempts) {
3864
4251
  this._state = 'terminal_failed';
4252
+ this._nextRetryAt = null;
3865
4253
  this._reconnectActive = false;
3866
4254
  this._reconnectAbort = null;
3867
- await this._dispatcher.publish('connection.state', {
3868
- state: this._state,
4255
+ await this._dispatcher.publish('state_change', {
4256
+ state: this._publicState(this._state),
3869
4257
  attempt: attempt - 1,
3870
4258
  reason: 'max_attempts_exhausted',
3871
4259
  });
3872
4260
  return;
3873
4261
  }
3874
- this._state = 'reconnecting';
3875
- await this._dispatcher.publish('connection.state', {
3876
- state: this._state,
4262
+ // 先进入 retry_backoff 状态(对齐 Python:先退避再重连)
4263
+ this._retryAttempt = attempt;
4264
+ const sleepMs = reconnectSleepDelaySeconds(delay, maxBaseDelay) * 1000;
4265
+ this._nextRetryAt = new Date(Date.now() + sleepMs);
4266
+ this._state = 'retry_backoff';
4267
+ await this._dispatcher.publish('state_change', {
4268
+ state: this._publicState(this._state),
3877
4269
  attempt,
4270
+ next_retry_at: this._nextRetryAt.getTime() / 1000,
3878
4271
  });
3879
4272
  try {
3880
- await this._sleep(reconnectSleepDelaySeconds(delay, maxBaseDelay) * 1000);
4273
+ await this._sleep(sleepMs);
4274
+ this._nextRetryAt = null;
3881
4275
  if (this._reconnectAbort?.signal.aborted) {
3882
4276
  this._reconnectActive = false;
3883
4277
  return;
3884
4278
  }
4279
+ // 退避结束,进入 reconnecting 状态
4280
+ this._state = 'reconnecting';
4281
+ await this._dispatcher.publish('state_change', {
4282
+ state: this._publicState(this._state),
4283
+ attempt,
4284
+ });
3885
4285
  // 重连前先 GET /health 探测,不健康则跳过本轮
3886
4286
  if (this._gatewayUrl) {
3887
4287
  const healthy = await this._discovery.checkHealth(this._gatewayUrl, 5000);
3888
4288
  if (!healthy) {
4289
+ this._lastError = new Error('gateway health check failed');
4290
+ this._lastErrorCode = 'gateway_unhealthy';
3889
4291
  delay = Math.min(delay * 2, maxBaseDelay);
3890
4292
  continue;
3891
4293
  }
@@ -3895,21 +4297,27 @@ export class AUNClient {
3895
4297
  throw new StateError('missing connect params for reconnect');
3896
4298
  }
3897
4299
  await this._connectOnce(this._sessionParams, true);
4300
+ this._lastError = null;
4301
+ this._lastErrorCode = null;
4302
+ this._nextRetryAt = null;
3898
4303
  this._reconnectActive = false;
3899
4304
  this._reconnectAbort = null;
3900
4305
  return;
3901
4306
  }
3902
4307
  catch (exc) {
4308
+ this._lastError = exc instanceof Error ? exc : new Error(String(exc));
4309
+ this._lastErrorCode = 'reconnect_failed';
3903
4310
  await this._dispatcher.publish('connection.error', {
3904
4311
  error: formatCaughtError(exc),
3905
4312
  attempt,
3906
4313
  });
3907
4314
  if (!this._shouldRetryReconnect(exc)) {
3908
4315
  this._state = 'terminal_failed';
4316
+ this._nextRetryAt = null;
3909
4317
  this._reconnectActive = false;
3910
4318
  this._reconnectAbort = null;
3911
- await this._dispatcher.publish('connection.state', {
3912
- state: this._state,
4319
+ await this._dispatcher.publish('state_change', {
4320
+ state: this._publicState(this._state),
3913
4321
  error: formatCaughtError(exc),
3914
4322
  attempt,
3915
4323
  });
@@ -4259,7 +4667,7 @@ export class AUNClient {
4259
4667
  *
4260
4668
  * connect 成功后自动调用,可幂等手动调用。
4261
4669
  */
4262
- async initV2Session() {
4670
+ async _initV2Session() {
4263
4671
  if (!this._aid)
4264
4672
  return;
4265
4673
  let identity = this._identity;
@@ -4564,7 +4972,7 @@ export class AUNClient {
4564
4972
  * @param opts 可选 messageId / timestamp(与 Python 行为一致)
4565
4973
  * @returns 服务端响应
4566
4974
  */
4567
- async sendV2(to, payload, opts) {
4975
+ async _sendV2(to, payload, opts) {
4568
4976
  if (!this._v2Session) {
4569
4977
  throw new StateError('V2 session not initialized (not connected?)');
4570
4978
  }
@@ -4608,7 +5016,7 @@ export class AUNClient {
4608
5016
  * @param afterSeq 从此 seq 之后开始拉取(0/省略 = 从当前 contiguous 开始)
4609
5017
  * @param limit 最多拉取条数
4610
5018
  */
4611
- async pullV2(afterSeq = 0, limit = 50, opts) {
5019
+ async _pullV2(afterSeq = 0, limit = 50, opts) {
4612
5020
  if (!this._v2Session) {
4613
5021
  throw new StateError('V2 session not initialized (not connected?)');
4614
5022
  }
@@ -4704,7 +5112,7 @@ export class AUNClient {
4704
5112
  this._saveSeqTrackerState();
4705
5113
  }
4706
5114
  if (messages.length > 0 && contigAdvanced && ackSeq > 0) {
4707
- this._safeAsync(this.ackV2(ackSeq).then(() => undefined));
5115
+ this._safeAsync(this._ackV2(ackSeq).then(() => undefined));
4708
5116
  }
4709
5117
  }
4710
5118
  const nextAfter = Math.max(pageMaxSeq, nextAfterSeq);
@@ -4722,7 +5130,7 @@ export class AUNClient {
4722
5130
  *
4723
5131
  * @param upToSeq 确认到此 seq;省略则用当前 contiguous
4724
5132
  */
4725
- async ackV2(upToSeq) {
5133
+ async _ackV2(upToSeq) {
4726
5134
  const ns = this._aid ? `p2p:${this._aid}` : '';
4727
5135
  let seq = upToSeq ?? (ns ? this._seqTracker.getContiguousSeq(ns) : 0);
4728
5136
  if (seq <= 0)
@@ -4959,7 +5367,7 @@ export class AUNClient {
4959
5367
  * @param opts 可选 messageId / timestamp
4960
5368
  * @returns 服务端响应
4961
5369
  */
4962
- async sendGroupV2(groupId, payload, opts) {
5370
+ async _sendGroupV2(groupId, payload, opts) {
4963
5371
  if (!this._v2Session) {
4964
5372
  throw new StateError('V2 session not initialized (not connected?)');
4965
5373
  }
@@ -5018,7 +5426,7 @@ export class AUNClient {
5018
5426
  }
5019
5427
  }
5020
5428
  async _pullGroupV2Internal(params) {
5021
- await this.pullGroupV2(params.group_id, params.after_seq, params.limit);
5429
+ await this._pullGroupV2(params.group_id, params.after_seq, params.limit);
5022
5430
  }
5023
5431
  /**
5024
5432
  * 拉取并解密 V2 Group 消息。
@@ -5027,7 +5435,7 @@ export class AUNClient {
5027
5435
  * @param afterSeq 从此 seq 之后开始拉取(0/省略 = 从当前 contiguous 开始)
5028
5436
  * @param limit 最多拉取条数
5029
5437
  */
5030
- async pullGroupV2(groupId, afterSeq = 0, limit = 50) {
5438
+ async _pullGroupV2(groupId, afterSeq = 0, limit = 50) {
5031
5439
  if (!this._v2Session) {
5032
5440
  throw new StateError('V2 session not initialized (not connected?)');
5033
5441
  }
@@ -5126,7 +5534,7 @@ export class AUNClient {
5126
5534
  this._saveSeqTrackerState();
5127
5535
  }
5128
5536
  if (messages.length > 0 && contigAdvanced && ackSeq > 0) {
5129
- this._safeAsync(this.ackGroupV2(gid, ackSeq).then(() => undefined));
5537
+ this._safeAsync(this._ackGroupV2(gid, ackSeq).then(() => undefined));
5130
5538
  }
5131
5539
  const nextAfter = Math.max(pageMaxSeq, nextAfterSeq);
5132
5540
  if (messages.length === 0 || nextAfter <= nextAfterSeq || result.has_more === false)
@@ -5144,7 +5552,7 @@ export class AUNClient {
5144
5552
  * @param groupId 群 ID
5145
5553
  * @param upToSeq 确认到此 seq;省略则用当前 contiguous
5146
5554
  */
5147
- async ackGroupV2(groupId, upToSeq) {
5555
+ async _ackGroupV2(groupId, upToSeq) {
5148
5556
  const gid = normalizeGroupId(groupId) || String(groupId ?? '').trim();
5149
5557
  if (!gid)
5150
5558
  throw new ValidationError('group.ack_messages requires group_id');
@@ -6221,7 +6629,7 @@ export class AUNClient {
6221
6629
  try {
6222
6630
  do {
6223
6631
  this._v2PullPending = false;
6224
- await this.pullV2();
6632
+ await this._pullV2();
6225
6633
  const newContig = ns ? this._seqTracker.getContiguousSeq(ns) : -1;
6226
6634
  this._clientLog.debug(`_onV2PushNotification pull done: contiguous_seq=${contigBefore}->${newContig} (push_seq=${pushSeq || 'null'})`);
6227
6635
  } while (this._v2PullPending);