@agentunion/fastaun 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 (60) 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 +66 -0
  19. package/dist/aid-store.js +539 -0
  20. package/dist/aid-store.js.map +1 -0
  21. package/dist/aid.d.ts +52 -0
  22. package/dist/aid.js +140 -0
  23. package/dist/aid.js.map +1 -0
  24. package/dist/auth.js +1 -1
  25. package/dist/auth.js.map +1 -1
  26. package/dist/cert-utils.d.ts +29 -0
  27. package/dist/cert-utils.js +142 -0
  28. package/dist/cert-utils.js.map +1 -0
  29. package/dist/client.d.ts +93 -90
  30. package/dist/client.js +681 -250
  31. package/dist/client.js.map +1 -1
  32. package/dist/error-codes.d.ts +25 -0
  33. package/dist/error-codes.js +26 -0
  34. package/dist/error-codes.js.map +1 -0
  35. package/dist/errors.d.ts +4 -1
  36. package/dist/errors.js +4 -1
  37. package/dist/errors.js.map +1 -1
  38. package/dist/index.d.ts +6 -5
  39. package/dist/index.js +5 -4
  40. package/dist/index.js.map +1 -1
  41. package/dist/keystore/aid-db.js +33 -0
  42. package/dist/keystore/aid-db.js.map +1 -1
  43. package/dist/keystore/file.d.ts +17 -0
  44. package/dist/keystore/file.js +194 -0
  45. package/dist/keystore/file.js.map +1 -1
  46. package/dist/keystore/index.d.ts +2 -0
  47. package/dist/result.d.ts +17 -0
  48. package/dist/result.js +10 -0
  49. package/dist/result.js.map +1 -0
  50. package/dist/tools/cross-sdk-agent.js +24 -12
  51. package/dist/tools/cross-sdk-agent.js.map +1 -1
  52. package/dist/types.d.ts +14 -0
  53. package/dist/types.js +30 -0
  54. package/dist/types.js.map +1 -1
  55. package/dist/v2/e2ee/encrypt-p2p.js +1 -1
  56. package/dist/v2/e2ee/encrypt-p2p.js.map +1 -1
  57. package/dist/version.d.ts +1 -0
  58. package/dist/version.js +5 -0
  59. package/dist/version.js.map +1 -0
  60. package/package.json +1 -1
package/dist/client.js CHANGED
@@ -20,14 +20,11 @@ import { configFromMap, getDeviceId, normalizeInstanceId } from './config.js';
20
20
  import { CryptoProvider } from './crypto.js';
21
21
  import { GatewayDiscovery } from './discovery.js';
22
22
  import { DnsResilientNet } from './net.js';
23
- import { AUNError, AuthError, ConnectionError, E2EEError, PermissionError, StateError, TimeoutError, ValidationError, } from './errors.js';
23
+ import { AUNError, AuthError, ConnectionError, E2EEError, NotFoundError, PermissionError, StateError, TimeoutError, ValidationError, } from './errors.js';
24
24
  import { EventDispatcher } from './events.js';
25
25
  import { FileKeyStore } from './keystore/file.js';
26
26
  import { AUNLogger } from './logger.js';
27
27
  import { normalizeGroupId } from './group-id.js';
28
- import { AuthNamespace } from './namespaces/auth.js';
29
- import { CustodyNamespace } from './namespaces/custody.js';
30
- import { MetaNamespace } from './namespaces/meta.js';
31
28
  import { RPCTransport } from './transport.js';
32
29
  import { AuthFlow } from './auth.js';
33
30
  import { SeqTracker } from './seq-tracker.js';
@@ -35,7 +32,8 @@ import { V2Session } from './v2/session/index.js';
35
32
  import { encryptP2PMessage, encryptGroupMessage, decryptMessage, } from './v2/e2ee/index.js';
36
33
  import { ecdsaVerifyRaw } from './v2/crypto/ecdsa.js';
37
34
  import { computeStateCommitment } from './v2/state/index.js';
38
- import { isJsonObject, } from './types.js';
35
+ import { isJsonObject, ConnectionState, STATE_TO_PUBLIC, } from './types.js';
36
+ import { AID } from './aid.js';
39
37
  function isPromiseLike(value) {
40
38
  return Boolean(value && typeof value.then === 'function');
41
39
  }
@@ -130,6 +128,12 @@ const DEFAULT_SESSION_OPTIONS = {
130
128
  http: 30.0,
131
129
  },
132
130
  };
131
+ const PROTECTED_HEADERS_METHODS = new Set([
132
+ 'message.send',
133
+ 'group.send',
134
+ 'message.thought.put',
135
+ 'group.thought.put',
136
+ ]);
133
137
  const RECONNECT_MIN_BASE_DELAY_MS = 1_000;
134
138
  const RECONNECT_MAX_BASE_DELAY_MS = 64_000;
135
139
  const TOKEN_REFRESH_CHECK_INTERVAL_MS = 30_000;
@@ -200,6 +204,7 @@ const SIGNED_METHODS = new Set([
200
204
  ]);
201
205
  /** peer 证书缓存 TTL(1 小时) */
202
206
  const PEER_CERT_CACHE_TTL = 3600;
207
+ const AGENT_MD_HTTP_TIMEOUT_MS = 30_000;
203
208
  function normalizeV2WrapPolicy(raw) {
204
209
  if (!raw || typeof raw !== 'object' || Array.isArray(raw)) {
205
210
  return { explicit: false, version: '', protocol: '', scope: 'device' };
@@ -377,6 +382,50 @@ function lengthPrefixedBytesKey(...parts) {
377
382
  }
378
383
  return Buffer.concat(chunks);
379
384
  }
385
+ function agentMdHttpScheme(gatewayUrl) {
386
+ const raw = String(gatewayUrl ?? '').trim().toLowerCase();
387
+ return raw.startsWith('ws://') ? 'http' : 'https';
388
+ }
389
+ function agentMdAuthority(aid, discoveryPort) {
390
+ const host = String(aid ?? '').trim();
391
+ if (!host)
392
+ return '';
393
+ if (discoveryPort && !host.includes(':'))
394
+ return `${host}:${discoveryPort}`;
395
+ return host;
396
+ }
397
+ async function fetchWithTimeout(input, init, timeoutMs = AGENT_MD_HTTP_TIMEOUT_MS) {
398
+ const controller = new AbortController();
399
+ const timer = setTimeout(() => controller.abort(), timeoutMs);
400
+ try {
401
+ return await fetch(input, { ...init, signal: controller.signal });
402
+ }
403
+ catch (error) {
404
+ if (controller.signal.aborted) {
405
+ throw new AUNError(`agent.md request timed out after ${timeoutMs}ms`);
406
+ }
407
+ throw error;
408
+ }
409
+ finally {
410
+ clearTimeout(timer);
411
+ }
412
+ }
413
+ function assertClientOptions(value, label) {
414
+ if (value == null)
415
+ return;
416
+ if (typeof value !== 'object' || Array.isArray(value) || value instanceof AID) {
417
+ throw new ValidationError(`${label} must be an options object`);
418
+ }
419
+ }
420
+ function clientOptionsConfig(options) {
421
+ const raw = { ...(options ?? {}) };
422
+ if (Object.prototype.hasOwnProperty.call(raw, 'aid')) {
423
+ throw new ValidationError('AUNClient options must not include aid; pass an AID object as the first argument');
424
+ }
425
+ delete raw.debug;
426
+ delete raw.protected_headers;
427
+ return raw;
428
+ }
380
429
  export class AUNClient {
381
430
  /** 原始配置 */
382
431
  config;
@@ -387,7 +436,17 @@ export class AUNClient {
387
436
  /** 当前身份信息(内存缓存) */
388
437
  _identity = null;
389
438
  /** 连接状态 */
390
- _state = 'idle';
439
+ _state = 'no_identity';
440
+ /** 当前 AID 值对象(新 API) */
441
+ _currentAid = null;
442
+ /** 实例级 protected_headers */
443
+ _instanceProtectedHeaders = null;
444
+ /** 重连退避时间戳(ms) */
445
+ _nextRetryAt = null;
446
+ _retryAttempt = 0;
447
+ _retryMaxAttempts = 0;
448
+ _lastError = null;
449
+ _lastErrorCode = null;
391
450
  /** Gateway URL */
392
451
  _gatewayUrl = null;
393
452
  /** 是否正在关闭 */
@@ -402,12 +461,6 @@ export class AUNClient {
402
461
  _auth;
403
462
  /** 密钥存储 */
404
463
  _keystore;
405
- /** Auth 命名空间 */
406
- auth;
407
- /** AID 托管命名空间 */
408
- custody;
409
- /** Meta 命名空间(心跳、状态、信任根管理) */
410
- meta;
411
464
  /** 会话参数(重连用) */
412
465
  _sessionParams = null;
413
466
  /** 会话选项 */
@@ -428,6 +481,9 @@ export class AUNClient {
428
481
  _remoteAgentMdEtag = '';
429
482
  _agentMdCache = new Map();
430
483
  _agentMdFetchInflight = new Map();
484
+ _agentMdDownloadInflight = new Map();
485
+ _agentMdDownloadActive = 0;
486
+ _agentMdDownloadWaiters = [];
431
487
  /** 消息序列号跟踪器(群消息 + P2P 空洞检测) */
432
488
  _seqTracker = new SeqTracker();
433
489
  _seqTrackerContext = null;
@@ -474,8 +530,11 @@ export class AUNClient {
474
530
  static V2_BOOTSTRAP_TTL_MS = 60 * 60 * 1000;
475
531
  static V2_RETRYABLE_CODES = new Set([-33011, -33012, -33050, -33052, -33054]);
476
532
  static PULL_GATE_STALE_MS = 3000;
533
+ /** 对端 AID 缓存(aid string → AID 对象) */
534
+ _peerCache = new Map();
477
535
  static V2_SIG_CACHE_TTL_MS = 60 * 60 * 1000;
478
536
  static V2_SIG_CACHE_MAX = 16_384;
537
+ static AGENT_MD_DOWNLOAD_CONCURRENCY = 8;
479
538
  _reconnectActive = false;
480
539
  _reconnectAbort = null;
481
540
  _serverKicked = false;
@@ -483,17 +542,32 @@ export class AUNClient {
483
542
  _lastDisconnectInfo = null;
484
543
  _logger;
485
544
  _clientLog;
486
- constructor(config, debug = false) {
487
- const rawConfig = { ...(config ?? {}) };
545
+ constructor(first, second) {
546
+ if (typeof first === 'string') {
547
+ throw new ValidationError('AUNClient aid must be an AID object, not a string');
548
+ }
549
+ if (typeof second === 'boolean') {
550
+ throw new ValidationError('AUNClient debug must be passed as options.debug');
551
+ }
552
+ const inputAid = first instanceof AID ? first : null;
553
+ if (!inputAid && second !== undefined) {
554
+ throw new ValidationError('AUNClient options-only construction accepts a single options object');
555
+ }
556
+ const options = inputAid ? (second ?? {}) : (first ?? {});
557
+ assertClientOptions(options, 'AUNClient options');
558
+ const rawConfig = clientOptionsConfig(options);
559
+ if (inputAid)
560
+ rawConfig.aun_path = inputAid.aunPath;
561
+ const debug = !!options?.debug;
488
562
  this._configModel = configFromMap(rawConfig);
489
- const initAid = String(rawConfig.aid ?? '').trim() || null;
563
+ const initAid = inputAid ? inputAid.aid : null;
490
564
  this._agentMdPath = path.join(this._configModel.aunPath, 'AIDs');
491
565
  this.config = {
492
566
  aun_path: this._configModel.aunPath,
493
567
  root_ca_path: this._configModel.rootCaPath,
494
568
  seed_password: this._configModel.seedPassword,
495
569
  };
496
- this._deviceId = getDeviceId(this._configModel.aunPath);
570
+ this._deviceId = (inputAid?.deviceId) || getDeviceId(this._configModel.aunPath);
497
571
  // 初始化 Logger(per-client 单例,必须最早创建)
498
572
  const debugFlag = this._configModel.debug || debug;
499
573
  this._logger = new AUNLogger({
@@ -530,7 +604,7 @@ export class AUNClient {
530
604
  catch (err) {
531
605
  this._clientLog.warn(`_pending cleanup failed: ${err instanceof Error ? err.message : String(err)}`);
532
606
  }
533
- this._slotId = '';
607
+ this._slotId = inputAid?.slotId || 'default';
534
608
  this._connectDeliveryMode = normalizeDeliveryModeConfig({ mode: 'fanout' });
535
609
  this._defaultConnectDeliveryMode = { ...this._connectDeliveryMode };
536
610
  this._auth = new AuthFlow({
@@ -554,9 +628,22 @@ export class AUNClient {
554
628
  dnsNet,
555
629
  });
556
630
  this._transport.setMetaObserver((meta) => this._observeRpcMeta(meta));
557
- this.auth = new AuthNamespace(this);
558
- this.custody = new CustodyNamespace(this);
559
- this.meta = new MetaNamespace(this);
631
+ if (inputAid) {
632
+ if (!inputAid.isPrivateKeyValid()) {
633
+ throw new StateError('AUNClient requires an AID with a valid private key');
634
+ }
635
+ this._currentAid = inputAid;
636
+ this._identity = {
637
+ aid: inputAid.aid,
638
+ private_key_pem: inputAid._privateKeyPem ?? '',
639
+ public_key_der_b64: inputAid.publicKey,
640
+ cert: inputAid.certPem,
641
+ };
642
+ this._state = 'standby';
643
+ }
644
+ if (options?.protected_headers !== undefined) {
645
+ this.setProtectedHeaders(options.protected_headers);
646
+ }
560
647
  // 内部订阅:推送消息自动解密后 re-publish 给用户
561
648
  this._dispatcher.subscribe('_raw.message.received', (data) => this._onRawMessageReceived(data));
562
649
  // V2 P2P 推送通知:收到通知后自动走 message.v2.pull 拉取并解密
@@ -587,6 +674,360 @@ export class AUNClient {
587
674
  get aid() {
588
675
  return this._aid;
589
676
  }
677
+ /** 当前 AID 值对象 */
678
+ get currentAid() {
679
+ return this._currentAid;
680
+ }
681
+ get hasIdentity() {
682
+ return this._currentAid !== null && this.state !== ConnectionState.CLOSED;
683
+ }
684
+ get canSign() {
685
+ return this.hasIdentity && !!this._currentAid?.isPrivateKeyValid();
686
+ }
687
+ get canConnect() {
688
+ return this.hasIdentity && this.state !== ConnectionState.CLOSED;
689
+ }
690
+ get canSend() {
691
+ return this.state === ConnectionState.READY;
692
+ }
693
+ get isReady() {
694
+ return this.canSend;
695
+ }
696
+ get isOnline() {
697
+ return this.state === ConnectionState.READY || this.state === ConnectionState.RETRY_BACKOFF || this.state === ConnectionState.RECONNECTING;
698
+ }
699
+ get isClosed() {
700
+ return this.state === ConnectionState.CLOSED;
701
+ }
702
+ get aunPath() {
703
+ return this.hasIdentity ? this._currentAid?.aunPath ?? this._configModel.aunPath : null;
704
+ }
705
+ get nextRetryAt() {
706
+ return this.state === ConnectionState.RETRY_BACKOFF && this._nextRetryAt ? new Date(this._nextRetryAt) : null;
707
+ }
708
+ get nextRetryInSeconds() {
709
+ return this.state === ConnectionState.RETRY_BACKOFF && this._nextRetryAt ? Math.max(0, Math.ceil((this._nextRetryAt - Date.now()) / 1000)) : null;
710
+ }
711
+ get retryAttempt() {
712
+ return this._retryAttempt;
713
+ }
714
+ get retryMaxAttempts() {
715
+ return this._retryMaxAttempts;
716
+ }
717
+ get lastError() {
718
+ return this._lastError;
719
+ }
720
+ get lastErrorCode() {
721
+ return this._lastErrorCode;
722
+ }
723
+ loadIdentity(aid) {
724
+ if (!aid?.isPrivateKeyValid()) {
725
+ throw new StateError('loadIdentity requires an AID with a valid private key');
726
+ }
727
+ const publicState = this.state;
728
+ if (publicState !== ConnectionState.NO_IDENTITY && publicState !== ConnectionState.CLOSED) {
729
+ throw new StateError(`loadIdentity not allowed in state ${publicState}`);
730
+ }
731
+ this._currentAid = aid;
732
+ this._aid = aid.aid;
733
+ this._identity = {
734
+ aid: aid.aid,
735
+ private_key_pem: aid._privateKeyPem ?? '',
736
+ public_key_der_b64: aid.publicKey,
737
+ cert: aid.certPem,
738
+ };
739
+ this._auth._aid = aid.aid;
740
+ this._state = 'standby';
741
+ this._closing = false;
742
+ this._lastError = null;
743
+ this._lastErrorCode = null;
744
+ this._retryAttempt = 0;
745
+ this._nextRetryAt = null;
746
+ }
747
+ setProtectedHeaders(headers) {
748
+ if (!headers) {
749
+ this._instanceProtectedHeaders = null;
750
+ return;
751
+ }
752
+ // 字段规范:key 限 [a-z0-9_-],_auth 为保留键不可设置。
753
+ // 非法 key 静默跳过(不报错),值强转 str。
754
+ const cleaned = {};
755
+ for (const [key, value] of Object.entries(headers)) {
756
+ const keyStr = String(key);
757
+ if (keyStr === '_auth')
758
+ continue;
759
+ if (!/^[a-z0-9_-]+$/.test(keyStr))
760
+ continue;
761
+ cleaned[keyStr] = value == null ? '' : String(value);
762
+ }
763
+ this._instanceProtectedHeaders = Object.keys(cleaned).length ? cleaned : null;
764
+ }
765
+ getProtectedHeaders() {
766
+ return this._instanceProtectedHeaders ? { ...this._instanceProtectedHeaders } : null;
767
+ }
768
+ cachePeer(aid) {
769
+ if (!this.hasIdentity)
770
+ throw new StateError('cachePeer requires a loaded identity');
771
+ if (!aid.isCertValid())
772
+ throw new ValidationError('cachePeer requires an AID with a valid certificate');
773
+ this._peerCache.set(aid.aid, aid);
774
+ return aid;
775
+ }
776
+ getPeer(aid) {
777
+ if (!this.hasIdentity)
778
+ throw new StateError('getPeer requires a loaded identity');
779
+ return this._peerCache.get(String(aid ?? '').trim()) ?? null;
780
+ }
781
+ async lookupPeer(aid) {
782
+ if (!this.hasIdentity)
783
+ throw new StateError('lookupPeer requires a loaded identity');
784
+ const target = String(aid ?? '').trim();
785
+ if (!target)
786
+ throw new ValidationError('lookupPeer requires non-empty aid');
787
+ const cached = this._peerCache.get(target);
788
+ if (cached)
789
+ return cached;
790
+ throw new NotFoundError(`peer not found in cache: ${target}`);
791
+ }
792
+ peers() {
793
+ if (!this.hasIdentity)
794
+ throw new StateError('peers requires a loaded identity');
795
+ return [...this._peerCache.entries()].sort(([a], [b]) => a.localeCompare(b)).map(([, v]) => v);
796
+ }
797
+ async _resolveAgentMdUrl(aid) {
798
+ const target = String(aid ?? '').trim();
799
+ if (!target)
800
+ throw new ValidationError('agent.md requires non-empty aid');
801
+ let gatewayUrl = String(this._gatewayUrl ?? '').trim();
802
+ if (!gatewayUrl) {
803
+ try {
804
+ gatewayUrl = await this._resolveGatewayForAid(target);
805
+ }
806
+ catch {
807
+ gatewayUrl = '';
808
+ }
809
+ }
810
+ return `${agentMdHttpScheme(gatewayUrl)}://${agentMdAuthority(target, this._configModel.discoveryPort)}/agent.md`;
811
+ }
812
+ async _ensureAgentMdUploadToken(aid, gatewayUrl) {
813
+ let identity = this._auth.loadIdentityOrNone(aid);
814
+ if (!identity && this._identity && String(this._identity.aid ?? '') === aid) {
815
+ identity = this._identity;
816
+ }
817
+ if (!identity) {
818
+ throw new StateError('no local identity found, register or load an AID first');
819
+ }
820
+ const cachedToken = String(identity.access_token ?? '');
821
+ const expiresAt = this._auth.getAccessTokenExpiry(identity);
822
+ if (cachedToken && (expiresAt === null || expiresAt > Date.now() / 1000 + 30)) {
823
+ return cachedToken;
824
+ }
825
+ if (identity.refresh_token) {
826
+ try {
827
+ const refreshed = await this._auth.refreshCachedTokens(gatewayUrl, identity);
828
+ const refreshedToken = String(refreshed.access_token ?? '');
829
+ const refreshedExpiry = this._auth.getAccessTokenExpiry(refreshed);
830
+ if (refreshedToken && (refreshedExpiry === null || refreshedExpiry > Date.now() / 1000 + 30)) {
831
+ this._identity = refreshed;
832
+ return refreshedToken;
833
+ }
834
+ }
835
+ catch {
836
+ // refresh 失败时回退到完整 authenticate。
837
+ }
838
+ }
839
+ const result = await this._auth.authenticate(gatewayUrl, { aid });
840
+ const token = String(result.access_token ?? '');
841
+ if (!token)
842
+ throw new StateError('authenticate did not return access_token');
843
+ this._identity = this._auth.loadIdentityOrNone(aid) ?? {
844
+ ...identity,
845
+ access_token: token,
846
+ refresh_token: String(result.refresh_token ?? identity.refresh_token ?? ''),
847
+ access_token_expires_at: typeof result.expires_at === 'number' ? result.expires_at : identity.access_token_expires_at,
848
+ token_exp: typeof result.expires_at === 'number' ? result.expires_at : identity.token_exp,
849
+ expires_at: typeof result.expires_at === 'number' ? result.expires_at : identity.expires_at,
850
+ };
851
+ return token;
852
+ }
853
+ async _uploadAgentMd(content) {
854
+ const target = String(this._aid ?? this._currentAid?.aid ?? '').trim();
855
+ if (!target)
856
+ throw new StateError('uploadAgentMd requires local AID');
857
+ const gatewayUrl = await this._resolveGatewayForAid(target);
858
+ this._gatewayUrl = gatewayUrl;
859
+ const token = await this._ensureAgentMdUploadToken(target, gatewayUrl);
860
+ const response = await fetchWithTimeout(await this._resolveAgentMdUrl(target), {
861
+ method: 'PUT',
862
+ headers: {
863
+ Authorization: `Bearer ${token}`,
864
+ 'Content-Type': 'text/markdown; charset=utf-8',
865
+ },
866
+ body: content,
867
+ });
868
+ if (response.status === 404) {
869
+ throw new NotFoundError(`agent.md endpoint not found for aid: ${target}`);
870
+ }
871
+ if (!response.ok) {
872
+ const message = (await response.text()).trim();
873
+ throw new AUNError(`upload agent.md failed: HTTP ${response.status}${message ? ` - ${message}` : ''}`);
874
+ }
875
+ const payload = await response.json();
876
+ if (!isJsonObject(payload))
877
+ throw new AUNError('upload agent.md returned invalid JSON payload');
878
+ return payload;
879
+ }
880
+ async _acquireAgentMdDownloadSlot() {
881
+ if (this._agentMdDownloadActive < AUNClient.AGENT_MD_DOWNLOAD_CONCURRENCY) {
882
+ this._agentMdDownloadActive += 1;
883
+ return () => this._releaseAgentMdDownloadSlot();
884
+ }
885
+ await new Promise((resolve) => {
886
+ this._agentMdDownloadWaiters.push(resolve);
887
+ });
888
+ return () => this._releaseAgentMdDownloadSlot();
889
+ }
890
+ _releaseAgentMdDownloadSlot() {
891
+ const next = this._agentMdDownloadWaiters.shift();
892
+ if (next) {
893
+ next();
894
+ return;
895
+ }
896
+ if (this._agentMdDownloadActive > 0)
897
+ this._agentMdDownloadActive -= 1;
898
+ }
899
+ async _downloadAgentMd(aid) {
900
+ const target = String(aid ?? '').trim();
901
+ if (!target)
902
+ throw new ValidationError('downloadAgentMd requires non-empty aid');
903
+ const existing = this._agentMdDownloadInflight.get(target);
904
+ if (existing)
905
+ return await existing;
906
+ const task = (async () => {
907
+ const release = await this._acquireAgentMdDownloadSlot();
908
+ try {
909
+ return await this._downloadAgentMdOnce(target);
910
+ }
911
+ finally {
912
+ release();
913
+ }
914
+ })();
915
+ this._agentMdDownloadInflight.set(target, task);
916
+ task.finally(() => {
917
+ if (this._agentMdDownloadInflight.get(target) === task) {
918
+ this._agentMdDownloadInflight.delete(target);
919
+ }
920
+ }).catch(() => undefined);
921
+ return await task;
922
+ }
923
+ async _downloadAgentMdOnce(target) {
924
+ const cached = this._agentMdCache.get(target);
925
+ const url = await this._resolveAgentMdUrl(target);
926
+ let response = await fetchWithTimeout(url, {
927
+ method: 'GET',
928
+ headers: { Accept: 'text/markdown' },
929
+ redirect: 'follow',
930
+ });
931
+ if (response.status === 304 && typeof cached?.text === 'string') {
932
+ return String(cached.text);
933
+ }
934
+ if (response.status === 304) {
935
+ response = await fetchWithTimeout(url, {
936
+ method: 'GET',
937
+ headers: { Accept: 'text/markdown' },
938
+ cache: 'reload',
939
+ redirect: 'follow',
940
+ });
941
+ }
942
+ if (response.status === 404) {
943
+ throw new NotFoundError(`agent.md not found for aid: ${target}`);
944
+ }
945
+ if (!response.ok) {
946
+ const message = (await response.text()).trim();
947
+ throw new AUNError(`download agent.md failed: HTTP ${response.status}${message ? ` - ${message}` : ''}`);
948
+ }
949
+ const text = await response.text();
950
+ const etag = String(response.headers?.get('ETag') ?? response.headers?.get('etag') ?? '').trim();
951
+ const lastModified = String(response.headers?.get('Last-Modified') ?? response.headers?.get('last-modified') ?? '').trim();
952
+ this._agentMdCache.set(target, {
953
+ ...(cached ?? {}),
954
+ text,
955
+ etag,
956
+ lastModified,
957
+ remote_etag: etag,
958
+ last_modified: lastModified,
959
+ });
960
+ return text;
961
+ }
962
+ async _headAgentMd(aid) {
963
+ const target = String(aid ?? '').trim();
964
+ if (!target)
965
+ throw new ValidationError('headAgentMd requires non-empty aid');
966
+ const response = await fetchWithTimeout(await this._resolveAgentMdUrl(target), {
967
+ method: 'HEAD',
968
+ headers: { Accept: 'text/markdown' },
969
+ redirect: 'follow',
970
+ }, 15_000);
971
+ const cached = this._agentMdCache.get(target) ?? {};
972
+ const etag = String(response.headers?.get('ETag') ?? response.headers?.get('etag') ?? '').trim();
973
+ const lastModified = String(response.headers?.get('Last-Modified') ?? response.headers?.get('last-modified') ?? '').trim();
974
+ if (response.status === 404) {
975
+ return { aid: target, found: false, etag: '', last_modified: '', status: 404 };
976
+ }
977
+ const resultEtag = response.status === 304 ? (etag || String(cached.etag ?? cached.remote_etag ?? '')) : etag;
978
+ const resultLastModified = response.status === 304 ? (lastModified || String(cached.lastModified ?? cached.last_modified ?? '')) : lastModified;
979
+ if (response.status < 200 || (response.status >= 300 && response.status !== 304)) {
980
+ throw new AUNError(`head agent.md failed: HTTP ${response.status}`);
981
+ }
982
+ this._agentMdCache.set(target, {
983
+ ...cached,
984
+ etag: resultEtag,
985
+ lastModified: resultLastModified,
986
+ remote_etag: resultEtag,
987
+ last_modified: resultLastModified,
988
+ });
989
+ return { aid: target, found: true, etag: resultEtag, last_modified: resultLastModified, status: response.status };
990
+ }
991
+ async _verifyAgentMd(content, aid, certPem) {
992
+ const target = String(aid ?? '').trim();
993
+ if (!target)
994
+ throw new ValidationError('verifyAgentMd requires non-empty aid');
995
+ let peer = target === this._currentAid?.aid ? this._currentAid : null;
996
+ if (!peer) {
997
+ let resolvedCert = String(certPem ?? '').trim();
998
+ if (!resolvedCert) {
999
+ try {
1000
+ resolvedCert = String(this._keystore.loadCert(target) ?? '').trim();
1001
+ }
1002
+ catch {
1003
+ resolvedCert = '';
1004
+ }
1005
+ }
1006
+ if (!resolvedCert) {
1007
+ if (!this._gatewayUrl) {
1008
+ try {
1009
+ this._gatewayUrl = await this._resolveGatewayForAid(target);
1010
+ }
1011
+ catch { /* best-effort before cert fetch */ }
1012
+ }
1013
+ resolvedCert = String(await this._fetchPeerCert(target) ?? '').trim();
1014
+ }
1015
+ if (!resolvedCert)
1016
+ throw new NotFoundError(`certificate not found for aid: ${target}`);
1017
+ peer = AID._create({
1018
+ aid: target,
1019
+ aunPath: this._configModel.aunPath,
1020
+ certPem: resolvedCert,
1021
+ privateKeyPem: null,
1022
+ certValid: true,
1023
+ privateKeyValid: false,
1024
+ });
1025
+ }
1026
+ const result = peer.verifyAgentMd(content);
1027
+ if (!result.ok)
1028
+ throw new AUNError(result.error.message);
1029
+ return { ...result.data, verified: result.data.status === 'verified' };
1030
+ }
590
1031
  /**
591
1032
  * 读取 {agentMdPath}/{self_aid}/agent.md,签名后上传,并把签名结果原子写回本地。
592
1033
  */
@@ -596,14 +1037,18 @@ export class AUNClient {
596
1037
  throw new ValidationError('publishAgentMd requires local AID');
597
1038
  }
598
1039
  const content = this._readAgentMdContent(target);
599
- const signed = await this.auth.signAgentMd(content);
600
- const result = await this.auth.uploadAgentMd(signed);
601
- this._localAgentMdEtag = this._agentMdContentEtag(signed);
1040
+ const signed = this._currentAid?.signAgentMd(content);
1041
+ if (!signed?.ok) {
1042
+ throw new StateError(signed?.error.message ?? 'publishAgentMd requires a valid local AID private key');
1043
+ }
1044
+ const signedContent = signed.data.signed;
1045
+ const result = await this._uploadAgentMd(signedContent);
1046
+ this._localAgentMdEtag = this._agentMdContentEtag(signedContent);
602
1047
  const remoteEtag = isJsonObject(result) ? String(result.etag ?? '').trim() : '';
603
1048
  if (remoteEtag)
604
1049
  this._remoteAgentMdEtag = remoteEtag;
605
1050
  this._saveAgentMdRecord(target, {
606
- content: signed,
1051
+ content: signedContent,
607
1052
  local_etag: this._localAgentMdEtag,
608
1053
  remote_etag: remoteEtag || undefined,
609
1054
  last_modified: isJsonObject(result) ? String(result.last_modified ?? '').trim() : '',
@@ -613,16 +1058,6 @@ export class AUNClient {
613
1058
  });
614
1059
  return result;
615
1060
  }
616
- /**
617
- * 下载 agent.md 并自动验签;内容固定保存到 {agentMdPath}/{aid}/agent.md。
618
- */
619
- async fetchAgentMd(aid) {
620
- const target = String(aid ?? this._aid ?? '').trim();
621
- if (!target) {
622
- throw new ValidationError('fetchAgentMd requires aid (or local AID)');
623
- }
624
- return await this._startAgentMdFetchTask(target);
625
- }
626
1061
  async _startAgentMdFetchTask(target) {
627
1062
  const existing = this._agentMdFetchInflight.get(target);
628
1063
  if (existing) {
@@ -638,8 +1073,8 @@ export class AUNClient {
638
1073
  return await task;
639
1074
  }
640
1075
  async _fetchAgentMdOnce(target) {
641
- const content = await this.auth.downloadAgentMd(target);
642
- const signature = await this.auth.verifyAgentMd(content, { aid: target });
1076
+ const content = await this._downloadAgentMd(target);
1077
+ const signature = await this._verifyAgentMd(content, target);
643
1078
  const isSelf = target === (this._aid ?? '');
644
1079
  const localEtag = this._agentMdContentEtag(content);
645
1080
  const cacheMeta = this._agentMdAuthCacheMeta(target);
@@ -678,7 +1113,7 @@ export class AUNClient {
678
1113
  /**
679
1114
  * 设置 agent.md 本地存储根目录;为空时恢复默认 {aun_path}/AIDs。
680
1115
  */
681
- setAgentMdPath(root) {
1116
+ _setAgentMdRoot(root) {
682
1117
  const raw = String(root ?? '').trim();
683
1118
  const next = raw || path.join(this._configModel.aunPath, 'AIDs');
684
1119
  fs.mkdirSync(next, { recursive: true });
@@ -686,9 +1121,6 @@ export class AUNClient {
686
1121
  this._agentMdCache.clear();
687
1122
  return this._agentMdPath;
688
1123
  }
689
- SetAgentMDPath(root) {
690
- return this.setAgentMdPath(root);
691
- }
692
1124
  /**
693
1125
  * 记录本地 agent.md 文件路径并一次性计算 etag(quoted sha256,与服务端一致)。
694
1126
  *
@@ -886,8 +1318,7 @@ export class AUNClient {
886
1318
  }
887
1319
  _agentMdAuthCacheMeta(aid) {
888
1320
  try {
889
- const store = this.auth._agentMdCache;
890
- const record = store?.get(String(aid ?? '').trim());
1321
+ const record = this._agentMdCache.get(String(aid ?? '').trim());
891
1322
  return record && typeof record === 'object' ? { ...record } : {};
892
1323
  }
893
1324
  catch {
@@ -1007,7 +1438,7 @@ export class AUNClient {
1007
1438
  return;
1008
1439
  if (this._agentMdFetchInflight.has(target))
1009
1440
  return;
1010
- void this.fetchAgentMd(target).catch((err) => {
1441
+ void this._startAgentMdFetchTask(target).catch((err) => {
1011
1442
  this._saveAgentMdRecord(target, {
1012
1443
  last_error: err instanceof Error ? err.message : String(err),
1013
1444
  remote_status: 'found',
@@ -1063,7 +1494,7 @@ export class AUNClient {
1063
1494
  }
1064
1495
  this._observeAgentMdMeta(senderAid, String(sender.etag ?? '').trim(), String(sender.last_modified ?? sender.lastModified ?? '').trim(), 'envelope');
1065
1496
  }
1066
- async checkAgentMd(aid, maxUnsyncedDays = 1) {
1497
+ async _checkAgentMdCache(aid, maxUnsyncedDays = 1) {
1067
1498
  const target = String(aid ?? this._aid ?? '').trim();
1068
1499
  if (!target)
1069
1500
  throw new ValidationError('checkAgentMd requires aid (or local AID)');
@@ -1114,7 +1545,7 @@ export class AUNClient {
1114
1545
  const now = Date.now();
1115
1546
  let remote;
1116
1547
  try {
1117
- remote = await this.auth.headAgentMd(target);
1548
+ remote = await this._headAgentMd(target);
1118
1549
  }
1119
1550
  catch (err) {
1120
1551
  this._saveAgentMdRecord(target, { checked_at: now, remote_status: 'error', last_error: err instanceof Error ? err.message : String(err) });
@@ -1169,42 +1600,79 @@ export class AUNClient {
1169
1600
  }
1170
1601
  /** 连接状态 */
1171
1602
  get state() {
1172
- return this._state;
1603
+ return this._publicState(this._state);
1604
+ }
1605
+ _publicState(state) {
1606
+ return STATE_TO_PUBLIC[state] ?? state;
1173
1607
  }
1174
1608
  /** 最近一次 gateway health check 结果,null 表示尚未检查 */
1175
1609
  get gatewayHealth() {
1176
1610
  return this._discovery.lastHealthy;
1177
1611
  }
1178
- /** gatewayUrl 的 /health 端点发送 GET 请求,检查网关可用性 */
1179
- async checkGatewayHealth(gatewayUrl, timeout = 5_000) {
1612
+ // ── 生命周期 ──────────────────────────────────────────────
1613
+ /** 仅认证当前身份,获取/刷新 token,但不建立长连接。 */
1614
+ async authenticate(options = {}) {
1180
1615
  const tStart = Date.now();
1181
- this._clientLog.debug(`checkGatewayHealth enter: gatewayUrl=${gatewayUrl}`);
1616
+ const target = this._currentAid?.aid ?? this._aid ?? '';
1617
+ if (!target || !this._currentAid?.isPrivateKeyValid()) {
1618
+ throw new StateError('authenticate requires a loaded AID with a valid private key');
1619
+ }
1620
+ const publicState = this.state;
1621
+ if (publicState !== ConnectionState.STANDBY && publicState !== ConnectionState.AUTHENTICATED) {
1622
+ throw new StateError(`authenticate not allowed in state ${publicState}`);
1623
+ }
1624
+ if ('aid' in options || 'access_token' in options || 'token' in options || 'kite_token' in options) {
1625
+ throw new ValidationError('authenticate options must not include aid or token fields; load an AID object first');
1626
+ }
1627
+ this._state = 'connecting';
1182
1628
  try {
1183
- const result = await this._discovery.checkHealth(gatewayUrl, timeout);
1184
- this._clientLog.debug(`checkGatewayHealth exit: elapsed=${Date.now() - tStart}ms healthy=${result}`);
1629
+ const gateway = String(options.gateway ?? this._gatewayUrl ?? await this._resolveGatewayForAid(target)).trim();
1630
+ const result = await this._auth.authenticate(gateway, { aid: target });
1631
+ this._gatewayUrl = String(result.gateway ?? gateway);
1632
+ this._identity = this._auth.loadIdentityOrNone(target);
1633
+ this._state = 'authenticated';
1634
+ this._lastError = null;
1635
+ this._lastErrorCode = null;
1636
+ this._clientLog.debug(`authenticate exit: elapsed=${Date.now() - tStart}ms aid=${target}`);
1185
1637
  return result;
1186
1638
  }
1187
1639
  catch (err) {
1188
- this._clientLog.debug(`checkGatewayHealth exit (error): elapsed=${Date.now() - tStart}ms err=${err instanceof Error ? err.message : String(err)}`);
1640
+ this._state = 'standby';
1641
+ this._lastError = err instanceof Error ? err : new Error(String(err));
1642
+ this._lastErrorCode = 'AUTHENTICATE_FAILED';
1643
+ this._clientLog.debug(`authenticate exit (error): elapsed=${Date.now() - tStart}ms err=${err instanceof Error ? err.message : String(err)}`);
1189
1644
  throw err;
1190
1645
  }
1191
1646
  }
1192
- // ── 生命周期 ──────────────────────────────────────────────
1193
- /**
1194
- * 连接到 Gateway。
1195
- *
1196
- * @param auth - 认证参数(必须包含 access_token 和 gateway)
1197
- * @param options - 会话选项(auto_reconnect、heartbeat_interval 等)
1198
- */
1199
- async connect(auth, options) {
1647
+ /** 连接到 Gateway;身份来自构造函数或 loadIdentity(aid),认证由 SDK 内部自动完成。 */
1648
+ async connect(options = {}) {
1200
1649
  const tStart = Date.now();
1201
- if (this._state !== 'idle' && this._state !== 'closed' && this._state !== 'disconnected') {
1202
- throw new StateError(`connect not allowed in state ${this._state}`);
1650
+ if (arguments.length > 1) {
1651
+ throw new ValidationError('connect accepts a single options object');
1652
+ }
1653
+ if ('aid' in options || 'access_token' in options || 'token' in options || 'kite_token' in options) {
1654
+ throw new ValidationError('connect options must not include aid or token fields; load an AID object first');
1655
+ }
1656
+ const target = this._currentAid?.aid ?? this._aid ?? '';
1657
+ if (!target || !this._currentAid?.isPrivateKeyValid()) {
1658
+ throw new StateError('connect requires a loaded AID with a valid private key');
1659
+ }
1660
+ const publicState = this.state;
1661
+ const allowed = new Set([
1662
+ ConnectionState.STANDBY,
1663
+ ConnectionState.AUTHENTICATED,
1664
+ ConnectionState.RETRY_BACKOFF,
1665
+ ConnectionState.CONNECTION_FAILED,
1666
+ ]);
1667
+ if (!allowed.has(publicState)) {
1668
+ throw new StateError(`connect not allowed in state ${publicState}`);
1669
+ }
1670
+ if (publicState === ConnectionState.RETRY_BACKOFF) {
1671
+ this._stopReconnect();
1203
1672
  }
1204
1673
  this._state = 'connecting';
1205
- const params = { ...auth };
1206
- if (options)
1207
- Object.assign(params, options);
1674
+ const gateway = String(options.gateway ?? this._gatewayUrl ?? await this._resolveGatewayForAid(target)).trim();
1675
+ const params = { ...options, gateway };
1208
1676
  const normalized = this._normalizeConnectParams(params);
1209
1677
  this._captureCapabilitiesFromConnect(normalized);
1210
1678
  this._sessionParams = normalized;
@@ -1218,7 +1686,9 @@ export class AUNClient {
1218
1686
  for (const gw of gateways) {
1219
1687
  try {
1220
1688
  const gwParams = { ...normalized, gateway: gw };
1221
- await this._connectOnce(gwParams, false);
1689
+ await this._connectOnce(gwParams, true);
1690
+ this._lastError = null;
1691
+ this._lastErrorCode = null;
1222
1692
  this._clientLog.debug(`connect exit: elapsed=${Date.now() - tStart}ms aid=${this._aid ?? ''}, state=${this._state}`);
1223
1693
  return;
1224
1694
  }
@@ -1227,14 +1697,15 @@ export class AUNClient {
1227
1697
  if (gateways.length > 1) {
1228
1698
  this._clientLog.warn(`connect: gateway ${gw} failed, trying next: ${formatCaughtError(err)}`);
1229
1699
  }
1230
- if (this._state === 'connecting' || this._state === 'authenticating') {
1700
+ if (this._state !== 'closed')
1231
1701
  this._state = 'connecting';
1232
- }
1233
1702
  }
1234
1703
  }
1235
1704
  if (this._state === 'connecting' || this._state === 'authenticating') {
1236
- this._state = 'disconnected';
1705
+ this._state = 'connection_failed';
1237
1706
  }
1707
+ this._lastError = lastErr instanceof Error ? lastErr : new Error(String(lastErr));
1708
+ this._lastErrorCode = 'CONNECT_FAILED';
1238
1709
  this._clientLog.error(`connect failed: ${formatCaughtError(lastErr)}`, lastErr instanceof Error ? lastErr : undefined);
1239
1710
  this._clientLog.debug(`connect exit (error): elapsed=${Date.now() - tStart}ms err=${lastErr instanceof Error ? lastErr.message : String(lastErr)}`);
1240
1711
  throw lastErr;
@@ -1248,13 +1719,13 @@ export class AUNClient {
1248
1719
  this._saveSeqTrackerState();
1249
1720
  this._stopBackgroundTasks();
1250
1721
  this._stopReconnect();
1251
- if (this._state === 'idle' || this._state === 'closed') {
1722
+ if (this.state === ConnectionState.NO_IDENTITY || this.state === ConnectionState.CLOSED) {
1252
1723
  const closableKeyStore = this._keystore;
1253
1724
  closableKeyStore.close?.();
1254
1725
  this._state = 'closed';
1255
1726
  this._logger.close();
1256
1727
  this._resetSeqTrackingState();
1257
- this._clientLog.debug(`close exit: elapsed=${Date.now() - tStart}ms (was idle/closed)`);
1728
+ this._clientLog.debug(`close exit: elapsed=${Date.now() - tStart}ms (was no_identity/closed)`);
1258
1729
  return;
1259
1730
  }
1260
1731
  await this._transport.close();
@@ -1262,7 +1733,7 @@ export class AUNClient {
1262
1733
  closableKeyStore.close?.();
1263
1734
  this._state = 'closed';
1264
1735
  this._logger.close();
1265
- await this._dispatcher.publish('connection.state', { state: this._state });
1736
+ await this._dispatcher.publish('connection.state', { state: this._publicState(this._state) });
1266
1737
  this._resetSeqTrackingState();
1267
1738
  this._clientLog.debug(`close exit: elapsed=${Date.now() - tStart}ms`);
1268
1739
  }
@@ -1284,7 +1755,14 @@ export class AUNClient {
1284
1755
  this._clientLog.debug(`disconnect exit: elapsed=${Date.now() - tStart}ms (closing)`);
1285
1756
  return;
1286
1757
  }
1287
- if (this._state !== 'connected' && this._state !== 'reconnecting') {
1758
+ if (![
1759
+ ConnectionState.AUTHENTICATED,
1760
+ ConnectionState.CONNECTING,
1761
+ ConnectionState.READY,
1762
+ ConnectionState.RETRY_BACKOFF,
1763
+ ConnectionState.RECONNECTING,
1764
+ ConnectionState.CONNECTION_FAILED,
1765
+ ].includes(this.state)) {
1288
1766
  this._clientLog.debug(`disconnect exit: elapsed=${Date.now() - tStart}ms (state=${this._state})`);
1289
1767
  return;
1290
1768
  }
@@ -1292,8 +1770,8 @@ export class AUNClient {
1292
1770
  this._stopBackgroundTasks();
1293
1771
  this._stopReconnect();
1294
1772
  await this._transport.close();
1295
- this._state = 'disconnected';
1296
- await this._dispatcher.publish('connection.state', { state: this._state });
1773
+ this._state = 'standby';
1774
+ await this._dispatcher.publish('connection.state', { state: this._publicState(this._state) });
1297
1775
  this._clientLog.debug(`disconnect exit: elapsed=${Date.now() - tStart}ms`);
1298
1776
  }
1299
1777
  catch (err) {
@@ -1301,83 +1779,6 @@ export class AUNClient {
1301
1779
  throw err;
1302
1780
  }
1303
1781
  }
1304
- /**
1305
- * 列出本地身份摘要。
1306
- *
1307
- * @param opts.all=false(默认):仅返回严格校验通过的可用身份——
1308
- * keypair 完整 + cert 公钥 == keypair 公钥 + cert 时间窗口有效
1309
- * @param opts.all=true:返回所有 AIDs/ 子目录(不含 _pending/);
1310
- * 每项含 valid=bool 和 reason=string 字段
1311
- */
1312
- listIdentities(opts) {
1313
- const tStart = Date.now();
1314
- const includeAll = !!opts?.all;
1315
- this._clientLog.debug(`listIdentities enter all=${includeAll}`);
1316
- try {
1317
- const listFn = this._keystore.listIdentities;
1318
- if (typeof listFn !== 'function') {
1319
- this._clientLog.debug(`listIdentities exit: elapsed=${Date.now() - tStart}ms (no_list_fn)`);
1320
- return [];
1321
- }
1322
- const aids = listFn.call(this._keystore);
1323
- const summaries = [];
1324
- for (const aid of [...aids].sort()) {
1325
- const { valid, reason } = this._validateLocalIdentity(aid);
1326
- if (!includeAll && !valid)
1327
- continue;
1328
- const summary = { aid, valid };
1329
- if (reason)
1330
- summary.reason = reason;
1331
- const loadMetadata = this._keystore.loadMetadata;
1332
- if (typeof loadMetadata === 'function') {
1333
- const md = loadMetadata.call(this._keystore, aid);
1334
- if (md)
1335
- summary.metadata = md;
1336
- }
1337
- summaries.push(summary);
1338
- }
1339
- this._clientLog.debug(`listIdentities exit: elapsed=${Date.now() - tStart}ms all=${includeAll} count=${summaries.length}`);
1340
- return summaries;
1341
- }
1342
- catch (err) {
1343
- this._clientLog.debug(`listIdentities exit (error): elapsed=${Date.now() - tStart}ms err=${err instanceof Error ? err.message : String(err)}`);
1344
- throw err;
1345
- }
1346
- }
1347
- /**
1348
- * 严格校验本地身份的可用性。返回 {valid, reason}。
1349
- * 4 项校验:keypair 完整 + cert 存在 + cert 公钥 == keypair 公钥 + cert 时间窗口有效。
1350
- */
1351
- _validateLocalIdentity(aid) {
1352
- const identity = this._keystore.loadIdentity(aid);
1353
- if (!identity)
1354
- return { valid: false, reason: 'no identity record' };
1355
- const priv = String(identity.private_key_pem ?? '');
1356
- const pubB64 = String(identity.public_key_der_b64 ?? '');
1357
- const certPem = String(identity.cert ?? '');
1358
- if (!priv || !pubB64)
1359
- return { valid: false, reason: 'missing keypair' };
1360
- if (!certPem)
1361
- return { valid: false, reason: 'missing certificate' };
1362
- try {
1363
- const crypto = require('node:crypto');
1364
- const cert = new crypto.X509Certificate(certPem);
1365
- const certPubDer = cert.publicKey.export({ type: 'spki', format: 'der' });
1366
- const localPubDer = Buffer.from(pubB64, 'base64');
1367
- if (!certPubDer.equals(localPubDer)) {
1368
- return { valid: false, reason: 'cert public key does not match keypair' };
1369
- }
1370
- const now = Date.now();
1371
- if (now < new Date(cert.validFrom).getTime())
1372
- return { valid: false, reason: 'cert not yet valid' };
1373
- if (now > new Date(cert.validTo).getTime())
1374
- return { valid: false, reason: 'cert expired' };
1375
- return { valid: true, reason: '' };
1376
- }
1377
- catch (e) {
1378
- return { valid: false, reason: `cert parse error: ${e instanceof Error ? e.message : String(e)}` };
1379
- }
1380
- }
1381
1782
  // ── RPC ───────────────────────────────────────────────────
1382
1783
  /**
1383
1784
  * 发送 JSON-RPC 调用。
@@ -1387,7 +1788,7 @@ export class AUNClient {
1387
1788
  const tStart = Date.now();
1388
1789
  this._clientLog.debug(`call enter: method=${method}`);
1389
1790
  try {
1390
- if (this._state !== 'connected') {
1791
+ if (this.state !== ConnectionState.READY) {
1391
1792
  throw new ConnectionError('client is not connected');
1392
1793
  }
1393
1794
  if (INTERNAL_ONLY_METHODS.has(method)) {
@@ -1397,6 +1798,10 @@ export class AUNClient {
1397
1798
  throw new PermissionError(`legacy E2EE method is removed in this SDK: ${method}`);
1398
1799
  }
1399
1800
  const p = { ...(params ?? {}) };
1801
+ if (this._instanceProtectedHeaders && PROTECTED_HEADERS_METHODS.has(method)) {
1802
+ const existing = isJsonObject(p.protected_headers) ? p.protected_headers : {};
1803
+ p.protected_headers = { ...this._instanceProtectedHeaders, ...existing };
1804
+ }
1400
1805
  const rpcBackground = Boolean(p._rpc_background) || this._backgroundRpcDepth > 0;
1401
1806
  delete p._rpc_background;
1402
1807
  const runWithRpcPriority = async (operation) => {
@@ -1446,7 +1851,7 @@ export class AUNClient {
1446
1851
  const encrypt = p.encrypt ?? true;
1447
1852
  delete p.encrypt;
1448
1853
  if (encrypt) {
1449
- return await runWithRpcPriority(() => this.sendV2(String(p.to ?? ''), p.payload, {
1854
+ return await runWithRpcPriority(() => this._sendV2(String(p.to ?? ''), p.payload, {
1450
1855
  messageId: String(p.message_id ?? '') || undefined,
1451
1856
  timestamp: p.timestamp,
1452
1857
  protectedHeaders: this._protectedHeadersFromParams(p),
@@ -1461,7 +1866,7 @@ export class AUNClient {
1461
1866
  const encrypt = p.encrypt ?? true;
1462
1867
  delete p.encrypt;
1463
1868
  if (encrypt) {
1464
- return await runWithRpcPriority(() => this.sendGroupV2(String(p.group_id ?? ''), p.payload, {
1869
+ return await runWithRpcPriority(() => this._sendGroupV2(String(p.group_id ?? ''), p.payload, {
1465
1870
  messageId: String(p.message_id ?? '') || undefined,
1466
1871
  timestamp: p.timestamp,
1467
1872
  protectedHeaders: this._protectedHeadersFromParams(p),
@@ -1497,20 +1902,20 @@ export class AUNClient {
1497
1902
  const afterSeq = Number(p.after_seq ?? 0) || 0;
1498
1903
  const limit = Number(p.limit ?? 50) || 50;
1499
1904
  const messages = skipAutoAck
1500
- ? await runWithRpcPriority(() => this.pullV2(afterSeq, limit, { skipAutoAck: true, gateLocked: true, force }))
1501
- : await runWithRpcPriority(() => this.pullV2(afterSeq, limit, { gateLocked: true, force }));
1905
+ ? await runWithRpcPriority(() => this._pullV2(afterSeq, limit, { skipAutoAck: true, gateLocked: true, force }))
1906
+ : await runWithRpcPriority(() => this._pullV2(afterSeq, limit, { gateLocked: true, force }));
1502
1907
  return { messages };
1503
1908
  }
1504
1909
  if (method === 'message.ack' || method === 'message.v2.ack') {
1505
1910
  await this._ensureV2SessionReady('message.ack');
1506
- return await runWithRpcPriority(() => this.ackV2(Number(p.seq ?? p.up_to_seq ?? 0) || undefined));
1911
+ return await runWithRpcPriority(() => this._ackV2(Number(p.seq ?? p.up_to_seq ?? 0) || undefined));
1507
1912
  }
1508
1913
  if (method === 'group.pull' || method === 'group.v2.pull') {
1509
1914
  if (!String(p.group_id ?? '').trim()) {
1510
1915
  throw new ValidationError('group.pull requires group_id');
1511
1916
  }
1512
1917
  await this._ensureV2SessionReady('group.pull');
1513
- const messages = await runWithRpcPriority(() => this.pullGroupV2(String(p.group_id), Number(p.after_seq ?? p.after_message_seq ?? 0) || 0, Number(p.limit ?? 50) || 50, { gateLocked: true }));
1918
+ const messages = await runWithRpcPriority(() => this._pullGroupV2(String(p.group_id), Number(p.after_seq ?? p.after_message_seq ?? 0) || 0, Number(p.limit ?? 50) || 50, { gateLocked: true }));
1514
1919
  return { messages };
1515
1920
  }
1516
1921
  if (method === 'group.ack_messages' || method === 'group.v2.ack') {
@@ -1518,7 +1923,7 @@ export class AUNClient {
1518
1923
  throw new ValidationError('group.ack_messages requires group_id');
1519
1924
  }
1520
1925
  await this._ensureV2SessionReady('group.ack_messages');
1521
- return await runWithRpcPriority(() => this.ackGroupV2(String(p.group_id), Number(p.seq ?? p.msg_seq ?? p.up_to_seq ?? 0) || undefined));
1926
+ return await runWithRpcPriority(() => this._ackGroupV2(String(p.group_id), Number(p.seq ?? p.msg_seq ?? p.up_to_seq ?? 0) || undefined));
1522
1927
  }
1523
1928
  if (method === 'message.pull') {
1524
1929
  delete p._skip_auto_ack;
@@ -1578,49 +1983,6 @@ export class AUNClient {
1578
1983
  throw err;
1579
1984
  }
1580
1985
  }
1581
- // ── 便利方法 ──────────────────────────────────────────────
1582
- /** 心跳检测 */
1583
- async ping(params) {
1584
- const tStart = Date.now();
1585
- this._clientLog.debug(`ping enter`);
1586
- try {
1587
- const result = await this.call('meta.ping', params ?? {});
1588
- this._clientLog.debug(`ping exit: elapsed=${Date.now() - tStart}ms`);
1589
- return result;
1590
- }
1591
- catch (err) {
1592
- this._clientLog.debug(`ping exit (error): elapsed=${Date.now() - tStart}ms err=${err instanceof Error ? err.message : String(err)}`);
1593
- throw err;
1594
- }
1595
- }
1596
- /** 获取服务端状态 */
1597
- async status(params) {
1598
- const tStart = Date.now();
1599
- this._clientLog.debug(`status enter`);
1600
- try {
1601
- const result = await this.call('meta.status', params ?? {});
1602
- this._clientLog.debug(`status exit: elapsed=${Date.now() - tStart}ms`);
1603
- return result;
1604
- }
1605
- catch (err) {
1606
- this._clientLog.debug(`status exit (error): elapsed=${Date.now() - tStart}ms err=${err instanceof Error ? err.message : String(err)}`);
1607
- throw err;
1608
- }
1609
- }
1610
- /** 获取信任根证书列表 */
1611
- async trustRoots(params) {
1612
- const tStart = Date.now();
1613
- this._clientLog.debug(`trustRoots enter`);
1614
- try {
1615
- const result = await this.call('meta.trust_roots', params ?? {});
1616
- this._clientLog.debug(`trustRoots exit: elapsed=${Date.now() - tStart}ms`);
1617
- return result;
1618
- }
1619
- catch (err) {
1620
- this._clientLog.debug(`trustRoots exit (error): elapsed=${Date.now() - tStart}ms err=${err instanceof Error ? err.message : String(err)}`);
1621
- throw err;
1622
- }
1623
- }
1624
1986
  // ── 事件 ──────────────────────────────────────────────────
1625
1987
  /** 订阅事件 */
1626
1988
  on(event, handler) {
@@ -1787,7 +2149,7 @@ export class AUNClient {
1787
2149
  const maxSeen = this._seqTracker.getMaxSeenSeq(ns);
1788
2150
  const ackSeq = maxSeen > 0 ? Math.min(contig, maxSeen) : contig;
1789
2151
  this._clientLog.debug(`P2P push auto-ack send: ns=${ns}, seq=${ackSeq}, contiguous=${contig}, max_seen=${maxSeen}`);
1790
- this._withBackgroundRpc(() => this.ackV2(ackSeq))
2152
+ this._withBackgroundRpc(() => this._ackV2(ackSeq))
1791
2153
  .then(() => { this._clientLog.debug(`P2P push auto-ack ok: ns=${ns}, seq=${ackSeq}`); })
1792
2154
  .catch((e) => { this._clientLog.debug(`P2P auto-ack failed: ${formatCaughtError(e)}`); });
1793
2155
  }
@@ -1881,7 +2243,7 @@ export class AUNClient {
1881
2243
  const maxSeen = this._seqTracker.getMaxSeenSeq(ns);
1882
2244
  const ackSeq = maxSeen > 0 ? Math.min(contig, maxSeen) : contig;
1883
2245
  this._clientLog.debug(`group push auto-ack send: group=${groupId}, ns=${ns}, seq=${ackSeq}, contiguous=${contig}, max_seen=${maxSeen}`);
1884
- this._withBackgroundRpc(() => this.ackGroupV2(groupId, ackSeq))
2246
+ this._withBackgroundRpc(() => this._ackGroupV2(groupId, ackSeq))
1885
2247
  .then(() => { this._clientLog.debug(`group push auto-ack ok: group=${groupId}, seq=${ackSeq}`); })
1886
2248
  .catch((e) => { this._clientLog.debug(`group message auto-ack failed: group=${groupId} ${formatCaughtError(e)}`); });
1887
2249
  }
@@ -1912,7 +2274,7 @@ export class AUNClient {
1912
2274
  this._clientLog.debug(`auto pull group messages start: group=${groupId}, after_seq=${afterSeq}, seq=${String(notification.seq ?? '')}`);
1913
2275
  const started = await this._tryRunBackgroundPull(ns, async () => {
1914
2276
  const pullAfterSeq = this._seqTracker.getContiguousSeq(ns);
1915
- const messages = await this.pullGroupV2(groupId, pullAfterSeq, 50, { gateLocked: true });
2277
+ const messages = await this._pullGroupV2(groupId, pullAfterSeq, 50, { gateLocked: true });
1916
2278
  this._prunePushedSeqs(ns);
1917
2279
  return messages.length;
1918
2280
  }, true);
@@ -1940,7 +2302,7 @@ export class AUNClient {
1940
2302
  this._clientLog.debug(`group message gap fill start: group=${groupId}, after_seq=${afterSeq}`);
1941
2303
  let filled = 0;
1942
2304
  try {
1943
- const messages = await this._withBackgroundRpc(() => this.pullGroupV2(groupId, afterSeq, 50, { gateLocked: true }));
2305
+ const messages = await this._withBackgroundRpc(() => this._pullGroupV2(groupId, afterSeq, 50, { gateLocked: true }));
1944
2306
  filled = messages.length;
1945
2307
  this._prunePushedSeqs(ns);
1946
2308
  if (this._seqTracker.getContiguousSeq(ns) !== afterSeq) {
@@ -1979,7 +2341,7 @@ export class AUNClient {
1979
2341
  this._clientLog.debug(`P2P message gap fill start: after_seq=${afterSeq}`);
1980
2342
  let filled = 0;
1981
2343
  try {
1982
- const messages = await this._withBackgroundRpc(() => this.pullV2(afterSeq, 50, { skipAutoAck: true, gateLocked: true }));
2344
+ const messages = await this._withBackgroundRpc(() => this._pullV2(afterSeq, 50, { skipAutoAck: true, gateLocked: true }));
1983
2345
  filled = messages.length;
1984
2346
  this._prunePushedSeqs(ns);
1985
2347
  if (this._seqTracker.getContiguousSeq(ns) !== afterSeq) {
@@ -1988,7 +2350,7 @@ export class AUNClient {
1988
2350
  }
1989
2351
  const contig = this._seqTracker.getContiguousSeq(ns);
1990
2352
  if (contig > 0 && contig !== afterSeq) {
1991
- await this._withBackgroundRpc(() => this.ackV2(contig));
2353
+ await this._withBackgroundRpc(() => this._ackV2(contig));
1992
2354
  }
1993
2355
  this._clientLog.debug(`P2P message gap fill done: after_seq=${afterSeq}, filled=${filled}`);
1994
2356
  }
@@ -2036,7 +2398,7 @@ export class AUNClient {
2036
2398
  this._clientLog.debug(`P2P pending pull upper already covered: ns=${ns}, upper_seq=${upperSeq}, contiguous=${contig}, reason=${reason}`);
2037
2399
  return false;
2038
2400
  }
2039
- if (this._state !== 'connected' || this._closing) {
2401
+ if (this.state !== ConnectionState.READY || this._closing) {
2040
2402
  this._clientLog.debug(`P2P pending pull postponed: ns=${ns}, upper_seq=${upperSeq}, contiguous=${contig}, state=${this._state}, closing=${this._closing}, reason=${reason}`);
2041
2403
  return false;
2042
2404
  }
@@ -2427,14 +2789,14 @@ export class AUNClient {
2427
2789
  try {
2428
2790
  await this._withBackgroundRpc(async () => {
2429
2791
  if (method === 'message.pull' || method === 'message.v2.pull') {
2430
- await this.pullV2(Number(next.after_seq ?? 0) || 0, Number(next.limit ?? 50) || 50);
2792
+ await this._pullV2(Number(next.after_seq ?? 0) || 0, Number(next.limit ?? 50) || 50);
2431
2793
  return;
2432
2794
  }
2433
2795
  if (method === 'group.pull' || method === 'group.v2.pull') {
2434
2796
  const groupId = String(next.group_id ?? '').trim();
2435
2797
  if (!groupId)
2436
2798
  return;
2437
- await this.pullGroupV2(groupId, Number(next.after_seq ?? next.after_message_seq ?? 0) || 0, Number(next.limit ?? 50) || 50);
2799
+ await this._pullGroupV2(groupId, Number(next.after_seq ?? next.after_message_seq ?? 0) || 0, Number(next.limit ?? 50) || 50);
2438
2800
  return;
2439
2801
  }
2440
2802
  await this.call(method, next);
@@ -3679,10 +4041,10 @@ export class AUNClient {
3679
4041
  this._applyServerHeartbeatInterval(hello.heartbeat_interval, 'auth');
3680
4042
  }
3681
4043
  }
3682
- this._state = 'connected';
4044
+ this._state = 'ready';
3683
4045
  this._connectedAt = Date.now();
3684
4046
  this._clientLog.debug(`auth complete, connection ready: aid=${this._aid ?? ''}, gateway=${gatewayUrl}`);
3685
- await this._dispatcher.publish('connection.state', { state: this._state, gateway: gatewayUrl });
4047
+ await this._dispatcher.publish('connection.state', { state: this._publicState(this._state), gateway: gatewayUrl });
3686
4048
  // auth 阶段 aid 可能被 identity 覆盖(上方 this._aid = identity.aid);
3687
4049
  // 若 context 发生变化,重新 refresh + restore,保持 tracker 与真实身份一致。
3688
4050
  if (this._seqTrackerContext !== this._currentSeqTrackerContext()) {
@@ -3692,7 +4054,7 @@ export class AUNClient {
3692
4054
  this._startBackgroundTasks();
3693
4055
  // V2 E2EE:初始化 session 并注册本设备 SPK。
3694
4056
  try {
3695
- await this.initV2Session();
4057
+ await this._initV2Session();
3696
4058
  }
3697
4059
  catch (exc) {
3698
4060
  this._clientLog.warn(`V2 session init failed (non-fatal): ${formatCaughtError(exc)}`);
@@ -3705,7 +4067,7 @@ export class AUNClient {
3705
4067
  this._clientLog.debug(`_connectOnce exit: elapsed=${Date.now() - tStart}ms gateway=${gatewayUrl}, aid=${this._aid ?? ''}`);
3706
4068
  }
3707
4069
  catch (err) {
3708
- this._state = prevState === 'connected' ? 'disconnected' : 'idle';
4070
+ this._state = (prevState === 'connected' || prevState === 'ready') ? 'standby' : (this._currentAid ? 'standby' : 'no_identity');
3709
4071
  this._clientLog.debug(`_connectOnce exit (error): elapsed=${Date.now() - tStart}ms gateway=${gatewayUrl} err=${err instanceof Error ? err.message : String(err)}`);
3710
4072
  throw err;
3711
4073
  }
@@ -3747,7 +4109,7 @@ export class AUNClient {
3747
4109
  * 初始化 V2 session:IK 使用 AID 长期私钥,SPK 存储在 per-AID SQLite 的 v2_device_keys 表。
3748
4110
  * connect 成功后会自动调用;重复调用幂等。
3749
4111
  */
3750
- async initV2Session() {
4112
+ async _initV2Session() {
3751
4113
  if (!this._aid)
3752
4114
  return;
3753
4115
  const existing = this._v2Session;
@@ -4195,7 +4557,7 @@ export class AUNClient {
4195
4557
  return envelope;
4196
4558
  }
4197
4559
  /** V2 P2P 加密发送,推测性缓存失败后刷新 bootstrap 重试一次。 */
4198
- async sendV2(to, payload, opts) {
4560
+ async _sendV2(to, payload, opts) {
4199
4561
  await this._ensureV2SessionReady('message.send', 'V2 session not initialized; encrypted message.send requires V2 (V1 E2EE removed)');
4200
4562
  const toAid = String(to ?? '').trim();
4201
4563
  if (!toAid)
@@ -4240,11 +4602,11 @@ export class AUNClient {
4240
4602
  }
4241
4603
  }
4242
4604
  /** V2 P2P 拉取并解密;直接方法返回消息数组,call("message.pull") 会包装为 {messages}. */
4243
- async pullV2(afterSeq = 0, limit = 50, opts) {
4605
+ async _pullV2(afterSeq = 0, limit = 50, opts) {
4244
4606
  await this._ensureV2SessionReady('message.pull');
4245
4607
  const ns = this._aid ? `p2p:${this._aid}` : '';
4246
4608
  if (ns && !opts?.gateLocked) {
4247
- return await this._runPullSerialized(ns, async () => this.pullV2(afterSeq, limit, {
4609
+ return await this._runPullSerialized(ns, async () => this._pullV2(afterSeq, limit, {
4248
4610
  ...(opts ?? {}),
4249
4611
  gateLocked: true,
4250
4612
  scheduleFollowup: true,
@@ -4356,7 +4718,7 @@ export class AUNClient {
4356
4718
  }
4357
4719
  if (messages.length > 0 && contigAdvanced && ackSeq > 0 && !opts?.skipAutoAck) {
4358
4720
  this._clientLog.debug(`message.v2.pull scheduling auto-ack: ns=${ns}, ack_seq=${ackSeq}, raw_count=${messages.length}`);
4359
- this._safeAsync(this.ackV2(ackSeq).then(() => undefined));
4721
+ this._safeAsync(this._ackV2(ackSeq).then(() => undefined));
4360
4722
  }
4361
4723
  }
4362
4724
  const nextAfter = Math.max(pageMaxSeq, nextAfterSeq);
@@ -4371,7 +4733,7 @@ export class AUNClient {
4371
4733
  return decrypted;
4372
4734
  }
4373
4735
  /** V2 P2P ack,并触发旧 SPK 销毁自检。 */
4374
- async ackV2(upToSeq) {
4736
+ async _ackV2(upToSeq) {
4375
4737
  const ns = this._aid ? `p2p:${this._aid}` : '';
4376
4738
  let seq = Number(upToSeq ?? (ns ? this._seqTracker.getContiguousSeq(ns) : 0));
4377
4739
  if (!Number.isFinite(seq) || seq <= 0) {
@@ -4419,7 +4781,7 @@ export class AUNClient {
4419
4781
  return result;
4420
4782
  }
4421
4783
  /** V2 Group 加密发送,推测性缓存失败后刷新 bootstrap 重试一次。 */
4422
- async sendGroupV2(groupId, payload, opts) {
4784
+ async _sendGroupV2(groupId, payload, opts) {
4423
4785
  await this._ensureV2SessionReady('group.send', 'V2 session not initialized; encrypted group.send requires V2 (V1 E2EE removed)');
4424
4786
  const gid = normalizeGroupId(groupId) || String(groupId ?? '').trim();
4425
4787
  if (!gid)
@@ -4599,17 +4961,17 @@ export class AUNClient {
4599
4961
  return envelope;
4600
4962
  }
4601
4963
  async _pullGroupV2Internal(params) {
4602
- await this.pullGroupV2(params.group_id, params.after_seq, params.limit, { gateLocked: true });
4964
+ await this._pullGroupV2(params.group_id, params.after_seq, params.limit, { gateLocked: true });
4603
4965
  }
4604
4966
  /** V2 Group 拉取并解密;直接方法返回消息数组,call("group.pull") 会包装为 {messages}. */
4605
- async pullGroupV2(groupId, afterSeq = 0, limit = 50, opts) {
4967
+ async _pullGroupV2(groupId, afterSeq = 0, limit = 50, opts) {
4606
4968
  await this._ensureV2SessionReady('group.pull');
4607
4969
  const gid = normalizeGroupId(groupId) || String(groupId ?? '').trim();
4608
4970
  if (!gid)
4609
4971
  throw new ValidationError('group.pull requires group_id');
4610
4972
  const ns = `group:${gid}`;
4611
4973
  if (!opts?.gateLocked) {
4612
- return await this._runPullSerialized(ns, async () => this.pullGroupV2(gid, afterSeq, limit, {
4974
+ return await this._runPullSerialized(ns, async () => this._pullGroupV2(gid, afterSeq, limit, {
4613
4975
  ...(opts ?? {}),
4614
4976
  gateLocked: true,
4615
4977
  scheduleFollowup: true,
@@ -4721,7 +5083,7 @@ export class AUNClient {
4721
5083
  }
4722
5084
  if (messages.length > 0 && contigAdvanced && ackSeq > 0) {
4723
5085
  this._clientLog.debug(`group.v2.pull scheduling auto-ack: group=${gid}, ns=${ns}, ack_seq=${ackSeq}, raw_count=${messages.length}`);
4724
- this._safeAsync(this.ackGroupV2(gid, ackSeq).then(() => undefined));
5086
+ this._safeAsync(this._ackGroupV2(gid, ackSeq).then(() => undefined));
4725
5087
  }
4726
5088
  const nextAfter = Math.max(pageMaxSeq, nextAfterSeq);
4727
5089
  if (messages.length === 0 || nextAfter <= nextAfterSeq || result.has_more === false)
@@ -4735,7 +5097,7 @@ export class AUNClient {
4735
5097
  return decrypted;
4736
5098
  }
4737
5099
  /** V2 Group ack。 */
4738
- async ackGroupV2(groupId, upToSeq) {
5100
+ async _ackGroupV2(groupId, upToSeq) {
4739
5101
  const gid = normalizeGroupId(groupId) || String(groupId ?? '').trim();
4740
5102
  if (!gid)
4741
5103
  throw new ValidationError('group.ack_messages requires group_id');
@@ -5930,7 +6292,7 @@ export class AUNClient {
5930
6292
  }
5931
6293
  this._gapFillDone.set(dedupKey, Date.now());
5932
6294
  try {
5933
- const pulled = await this.pullV2(0, 50, { gateLocked: true });
6295
+ const pulled = await this._pullV2(0, 50, { gateLocked: true });
5934
6296
  const newContig = this._seqTracker.getContiguousSeq(ns);
5935
6297
  this._clientLog.debug(`_onV2PushNotification pull done: contiguous_seq=${contigBefore}->${newContig} (push_seq=${pushSeq || 'null'})`);
5936
6298
  if (newContig <= operationBefore)
@@ -6019,7 +6381,7 @@ export class AUNClient {
6019
6381
  this._gapFillDone.set(dedupKey, Date.now());
6020
6382
  try {
6021
6383
  this._clientLog.debug(`_onRawGroupV2MessageCreated auto-pull start: group=${groupId}, after_seq=${pullAfterSeq}, push_seq=${seq}`);
6022
- const pulled = await this.pullGroupV2(groupId, pullAfterSeq, 50, { gateLocked: true });
6384
+ const pulled = await this._pullGroupV2(groupId, pullAfterSeq, 50, { gateLocked: true });
6023
6385
  const newContig = this._seqTracker.getContiguousSeq(ns);
6024
6386
  this._clientLog.debug(`_onRawGroupV2MessageCreated auto-pull done: group=${groupId}, after_seq=${pullAfterSeq}, push_seq=${seq}, contiguous=${newContig}`);
6025
6387
  if (newContig <= pullAfterSeq)
@@ -6056,6 +6418,56 @@ export class AUNClient {
6056
6418
  this._clientLog.debug(`SPK rotation after V2 epoch change failed (non-fatal): ${formatCaughtError(exc)}`);
6057
6419
  }
6058
6420
  }
6421
+ /** 按当前 AID 发现 Gateway;用于 authenticate()/connect() 的新入口。 */
6422
+ async _resolveGatewayForAid(aid) {
6423
+ const resolvedAid = String(aid ?? this._aid ?? '').trim();
6424
+ if (!resolvedAid) {
6425
+ throw new StateError('gateway discovery requires a loaded AID');
6426
+ }
6427
+ if (this._gatewayUrl)
6428
+ return this._gatewayUrl;
6429
+ try {
6430
+ const loadMetadata = this._keystore.loadMetadata;
6431
+ const cachedGateway = typeof loadMetadata === 'function'
6432
+ ? String(loadMetadata.call(this._keystore, resolvedAid)?.gateway_url ?? '').trim()
6433
+ : '';
6434
+ if (cachedGateway) {
6435
+ this._gatewayUrl = cachedGateway;
6436
+ return cachedGateway;
6437
+ }
6438
+ }
6439
+ catch {
6440
+ // 缓存读取失败不影响发现流程。
6441
+ }
6442
+ const dotIdx = resolvedAid.indexOf('.');
6443
+ const issuerDomain = dotIdx >= 0 ? resolvedAid.slice(dotIdx + 1) : resolvedAid;
6444
+ const portSuffix = this._configModel.discoveryPort ? `:${this._configModel.discoveryPort}` : '';
6445
+ const aidUrl = `https://${resolvedAid}${portSuffix}/.well-known/aun-gateway`;
6446
+ const gatewayDomainUrl = `https://gateway.${issuerDomain}${portSuffix}/.well-known/aun-gateway`;
6447
+ const candidates = this._configModel.verifySsl ? [aidUrl, gatewayDomainUrl] : [gatewayDomainUrl, aidUrl];
6448
+ let lastErr = null;
6449
+ for (const url of candidates) {
6450
+ try {
6451
+ const gateway = await this._discovery.discover(url);
6452
+ this._gatewayUrl = gateway;
6453
+ try {
6454
+ const saveMetadata = this._keystore.saveMetadata;
6455
+ if (typeof saveMetadata === 'function') {
6456
+ saveMetadata.call(this._keystore, resolvedAid, { gateway_url: gateway, gateway_cached_at: Date.now() });
6457
+ }
6458
+ }
6459
+ catch {
6460
+ // 缓存写入失败不影响连接。
6461
+ }
6462
+ return gateway;
6463
+ }
6464
+ catch (err) {
6465
+ lastErr = err;
6466
+ this._clientLog.warn(`gateway discovery failed: aid=${resolvedAid} url=${url} err=${formatCaughtError(err)}`);
6467
+ }
6468
+ }
6469
+ throw lastErr instanceof Error ? lastErr : new ConnectionError(`gateway discovery failed for ${resolvedAid}`);
6470
+ }
6059
6471
  /** 从参数中解析 Gateway URL */
6060
6472
  _resolveGateway(params) {
6061
6473
  const gateways = this._resolveGateways(params);
@@ -6106,15 +6518,19 @@ export class AUNClient {
6106
6518
  }
6107
6519
  // ── 内部:参数处理 ────────────────────────────────────────
6108
6520
  /** 规范化连接参数 */
6109
- _normalizeConnectParams(params) {
6521
+ _normalizeConnectParams(params, opts = {}) {
6110
6522
  const request = { ...params };
6111
6523
  const accessToken = String(request.access_token ?? '');
6112
- if (!accessToken)
6524
+ if (!accessToken && opts.requireAccessToken === true) {
6113
6525
  throw new StateError('connect requires non-empty access_token');
6526
+ }
6114
6527
  const gateway = String(request.gateway ?? this._gatewayUrl ?? '');
6115
6528
  if (!gateway)
6116
6529
  throw new StateError('connect requires non-empty gateway');
6117
- request.access_token = accessToken;
6530
+ if (accessToken)
6531
+ request.access_token = accessToken;
6532
+ else
6533
+ delete request.access_token;
6118
6534
  request.gateway = gateway;
6119
6535
  request.device_id = this._deviceId;
6120
6536
  request.slot_id = normalizeInstanceId(request.slot_id ?? this._slotId, 'slot_id', { allowEmpty: true });
@@ -6222,7 +6638,7 @@ export class AUNClient {
6222
6638
  let consecutiveFailures = 0;
6223
6639
  const maxFailures = 2;
6224
6640
  this._heartbeatTimer = setInterval(() => {
6225
- if (this._closing || this._state !== 'connected')
6641
+ if (this._closing || this.state !== ConnectionState.READY)
6226
6642
  return;
6227
6643
  this._transport.call('meta.ping', {}).then((pong) => {
6228
6644
  consecutiveFailures = 0;
@@ -6257,7 +6673,7 @@ export class AUNClient {
6257
6673
  clearInterval(this._heartbeatTimer);
6258
6674
  this._heartbeatTimer = null;
6259
6675
  }
6260
- if (newInterval > 0 && this._state === 'connected' && !this._closing) {
6676
+ if (newInterval > 0 && this.state === ConnectionState.READY && !this._closing) {
6261
6677
  this._startHeartbeatTask();
6262
6678
  }
6263
6679
  }
@@ -6276,7 +6692,7 @@ export class AUNClient {
6276
6692
  if (this._closing)
6277
6693
  return;
6278
6694
  this._tokenRefreshTimer = null;
6279
- if (this._state !== 'connected' || !this._gatewayUrl) {
6695
+ if (this.state !== ConnectionState.READY || !this._gatewayUrl) {
6280
6696
  scheduleNext();
6281
6697
  return;
6282
6698
  }
@@ -6295,12 +6711,17 @@ export class AUNClient {
6295
6711
  scheduleNext();
6296
6712
  return;
6297
6713
  }
6298
- if (this._closing || this._state !== 'connected' || !this._gatewayUrl) {
6714
+ if (this._closing || this.state !== ConnectionState.READY || !this._gatewayUrl) {
6299
6715
  scheduleNext();
6300
6716
  return;
6301
6717
  }
6302
6718
  try {
6303
6719
  identity = await this._auth.refreshCachedTokens(this._gatewayUrl, identity);
6720
+ // 刷新期间可能已断线,复检状态,避免写回 stale identity
6721
+ if (this.state !== ConnectionState.READY) {
6722
+ scheduleNext();
6723
+ return;
6724
+ }
6304
6725
  this._identity = identity;
6305
6726
  if (this._sessionParams !== null && identity.access_token) {
6306
6727
  this._sessionParams.access_token = identity.access_token;
@@ -6465,7 +6886,7 @@ export class AUNClient {
6465
6886
  : {};
6466
6887
  this._clientLog.warn(`server initiated disconnect: code=${code}, reason=${reason}, detail=${JSON.stringify(detail)}`);
6467
6888
  this._serverKicked = true;
6468
- // 缓存最近一次 disconnect 信息,让后续 connection.state(terminal_failed) 也能带 detail
6889
+ // 缓存最近一次 disconnect 信息,让后续 connection.state(connection_failed) 也能带 detail
6469
6890
  this._lastDisconnectInfo = { code, reason, detail };
6470
6891
  // 透传给应用层订阅者
6471
6892
  try {
@@ -6481,27 +6902,27 @@ export class AUNClient {
6481
6902
  }
6482
6903
  /** 传输层断线回调 */
6483
6904
  async _handleTransportDisconnect(error, closeCode) {
6484
- if (this._closing || this._state === 'closed')
6905
+ if (this._closing || this.state === ConnectionState.CLOSED)
6485
6906
  return;
6486
6907
  // 已在重连中则跳过,避免心跳超时和 transport 断线回调重复触发
6487
6908
  if (this._reconnectActive)
6488
6909
  return;
6489
6910
  this._clientLog.warn(`transport disconnected: closeCode=${closeCode ?? 'none'}, error=${error ? formatCaughtError(error) : 'none'}`);
6490
- this._state = 'disconnected';
6911
+ this._state = 'standby';
6491
6912
  this._stopBackgroundTasks();
6492
- await this._dispatcher.publish('connection.state', { state: this._state, error });
6913
+ await this._dispatcher.publish('connection.state', { state: this._publicState(this._state), error });
6493
6914
  if (!this._sessionOptions.auto_reconnect)
6494
6915
  return;
6495
6916
  if (this._reconnectActive)
6496
6917
  return;
6497
6918
  // 不重连 close code(认证失败/权限错误/被踢等)或服务端通知断开:抑制重连
6498
6919
  if (this._serverKicked || (closeCode !== undefined && AUNClient._NO_RECONNECT_CODES.has(closeCode))) {
6499
- this._state = 'terminal_failed';
6920
+ this._state = 'connection_failed';
6500
6921
  const reason = this._serverKicked ? 'server kicked' : `close code ${closeCode}`;
6501
6922
  this._clientLog.warn(`suppressing auto-reconnect: ${reason}`);
6502
6923
  const disconnectInfo = this._lastDisconnectInfo ?? {};
6503
6924
  const eventPayload = {
6504
- state: this._state, error, reason,
6925
+ state: this._publicState(this._state), error, reason,
6505
6926
  };
6506
6927
  // 把服务端附带的结构化 detail(如配额超限信息)也带给应用层
6507
6928
  if (disconnectInfo.detail && Object.keys(disconnectInfo.detail).length > 0) {
@@ -6534,30 +6955,39 @@ export class AUNClient {
6534
6955
  const maxBaseDelay = clampReconnectDelayMs(Number(retry.max_delay ?? 64.0) * 1000, RECONNECT_MAX_BASE_DELAY_MS);
6535
6956
  const maxAttemptsRaw = Number(retry.max_attempts ?? 0);
6536
6957
  const maxAttempts = Number.isFinite(maxAttemptsRaw) && maxAttemptsRaw > 0 ? Math.floor(maxAttemptsRaw) : 0;
6958
+ this._retryMaxAttempts = maxAttempts;
6537
6959
  let delay = clampReconnectDelayMs(serverInitiated ? 16_000 : Number(retry.initial_delay ?? 1.0) * 1000, serverInitiated ? 16_000 : RECONNECT_MIN_BASE_DELAY_MS, maxBaseDelay);
6538
6960
  for (let attempt = 1; !this._reconnectAbort?.signal.aborted; attempt++) {
6539
6961
  if (this._closing)
6540
6962
  break;
6541
6963
  // max_attempts 检查在循环顶部,覆盖所有路径(含 health-fail)
6542
6964
  if (maxAttempts > 0 && attempt > maxAttempts) {
6543
- this._state = 'terminal_failed';
6965
+ this._state = 'connection_failed';
6544
6966
  await this._dispatcher.publish('connection.state', {
6545
- state: this._state,
6967
+ state: this._publicState(this._state),
6546
6968
  attempt: attempt - 1,
6547
6969
  reason: 'max_attempts_exhausted',
6548
6970
  });
6549
6971
  break;
6550
6972
  }
6551
- this._state = 'reconnecting';
6973
+ this._retryAttempt = attempt;
6974
+ this._nextRetryAt = Date.now() + reconnectSleepDelayMs(delay, maxBaseDelay);
6975
+ this._state = 'retry_backoff';
6552
6976
  await this._dispatcher.publish('connection.state', {
6553
- state: this._state,
6977
+ state: this._publicState(this._state),
6554
6978
  attempt,
6979
+ next_retry_at: this._nextRetryAt,
6555
6980
  });
6556
6981
  try {
6557
6982
  // 固定上限抖动:base=[1s, max_base],delay=base+rand(0..max_base)。
6558
- await this._sleep(reconnectSleepDelayMs(delay, maxBaseDelay));
6983
+ await this._sleep(Math.max(0, this._nextRetryAt - Date.now()));
6559
6984
  if (this._reconnectAbort?.signal.aborted || this._closing)
6560
6985
  break;
6986
+ this._state = 'reconnecting';
6987
+ await this._dispatcher.publish('connection.state', {
6988
+ state: this._publicState(this._state),
6989
+ attempt,
6990
+ });
6561
6991
  // 重连前先 GET /health 探测,不健康则跳过本轮
6562
6992
  if (this._gatewayUrl) {
6563
6993
  const healthy = await this._discovery.checkHealth(this._gatewayUrl, 5_000);
@@ -6573,6 +7003,7 @@ export class AUNClient {
6573
7003
  await this._connectOnce(this._sessionParams, true);
6574
7004
  // 重连成功,退出循环
6575
7005
  this._clientLog.debug(`reconnect success: attempt=${attempt}, aid=${this._aid ?? ''}`);
7006
+ this._nextRetryAt = null;
6576
7007
  this._reconnectActive = false;
6577
7008
  this._reconnectAbort = null;
6578
7009
  return;
@@ -6583,9 +7014,9 @@ export class AUNClient {
6583
7014
  attempt,
6584
7015
  });
6585
7016
  if (!AUNClient._shouldRetryReconnect(exc)) {
6586
- this._state = 'terminal_failed';
7017
+ this._state = 'connection_failed';
6587
7018
  await this._dispatcher.publish('connection.state', {
6588
- state: this._state,
7019
+ state: this._publicState(this._state),
6589
7020
  error: formatCaughtError(exc),
6590
7021
  attempt,
6591
7022
  });