@agentunion/fastaun 0.5.26 → 0.5.28

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 (94) hide show
  1. package/CHANGELOG.md +77 -0
  2. package/_packed_docs/CHANGELOG.md +77 -0
  3. package/_packed_docs/INDEX.md +37 -14
  4. package/_packed_docs/KITE_DOCS_GUIDE.md +23 -15
  5. package/_packed_docs/aun/345/210/206/345/270/203/345/274/217/346/265/213/350/257/225/350/277/220/350/241/214/346/214/207/345/215/227.md +16 -1
  6. package/_packed_docs/aun/346/265/213/350/257/225/350/277/220/350/241/214/346/214/207/345/215/227.md +37 -9
  7. package/_packed_docs/cli/AUN-CLI/350/256/276/350/256/241/346/226/207/346/241/243.md +20 -9
  8. package/_packed_docs/cli/CLI/346/211/213/345/206/214.md +57 -12
  9. package/_packed_docs/sdk/0.5.22-SDK/345/274/200/345/217/221/344/270/216/347/247/273/346/244/215/346/214/207/345/215/227.md +2 -2
  10. package/_packed_docs/sdk/06-API/346/211/213/345/206/214.md +2 -2
  11. package/_packed_docs/sdk/09-/345/207/255/350/257/201/346/234/215/345/212/241RPC/346/211/213/345/206/214.md +187 -73
  12. package/_packed_docs/sdk/09-/347/273/204/347/273/207/346/234/215/345/212/241RPC/346/211/213/345/206/214.md +1 -1
  13. package/_packed_docs/sdk/AUN_DOCS_GUIDE.md +13 -9
  14. package/_packed_docs/sdk/INDEX.md +23 -13
  15. package/_packed_docs/sdk//345/207/255/350/257/201/344/270/216/347/273/204/347/273/207/346/234/215/345/212/241SDK/345/272/224/347/224/250/351/233/206/346/210/220/346/214/207/345/215/227.md +973 -266
  16. package/_packed_docs/sdk//345/207/255/350/257/201/344/270/216/347/273/204/347/273/207/346/234/215/345/212/241SDK/345/274/200/345/217/221/344/270/216/347/247/273/346/244/215/346/214/207/345/215/227.md +950 -242
  17. package/dist/agent-md-changes.d.ts +42 -0
  18. package/dist/agent-md-changes.js +339 -0
  19. package/dist/agent-md-changes.js.map +1 -0
  20. package/dist/agent-md-schema.d.ts +1 -0
  21. package/dist/agent-md-schema.js +31 -14
  22. package/dist/agent-md-schema.js.map +1 -1
  23. package/dist/agent-md.d.ts +11 -0
  24. package/dist/agent-md.js +91 -15
  25. package/dist/agent-md.js.map +1 -1
  26. package/dist/aid-store.d.ts +11 -0
  27. package/dist/aid-store.js +102 -21
  28. package/dist/aid-store.js.map +1 -1
  29. package/dist/aid.js +3 -3
  30. package/dist/aid.js.map +1 -1
  31. package/dist/auth.d.ts +10 -0
  32. package/dist/auth.js +65 -23
  33. package/dist/auth.js.map +1 -1
  34. package/dist/cert-utils.d.ts +1 -1
  35. package/dist/cert-utils.js +4 -4
  36. package/dist/cert-utils.js.map +1 -1
  37. package/dist/client/control-messages.d.ts +31 -0
  38. package/dist/client/control-messages.js +474 -0
  39. package/dist/client/control-messages.js.map +1 -0
  40. package/dist/client/delivery.js +10 -2
  41. package/dist/client/delivery.js.map +1 -1
  42. package/dist/client/lifecycle.d.ts +1 -1
  43. package/dist/client/lifecycle.js +10 -0
  44. package/dist/client/lifecycle.js.map +1 -1
  45. package/dist/client/rpc-pipeline.js +84 -14
  46. package/dist/client/rpc-pipeline.js.map +1 -1
  47. package/dist/client/v2-e2ee.d.ts +3 -0
  48. package/dist/client/v2-e2ee.js +12 -0
  49. package/dist/client/v2-e2ee.js.map +1 -1
  50. package/dist/client.d.ts +38 -0
  51. package/dist/client.js +572 -36
  52. package/dist/client.js.map +1 -1
  53. package/dist/discovery.d.ts +3 -0
  54. package/dist/discovery.js +52 -2
  55. package/dist/discovery.js.map +1 -1
  56. package/dist/events.js +14 -1
  57. package/dist/events.js.map +1 -1
  58. package/dist/facades.d.ts +38 -0
  59. package/dist/facades.js +49 -5
  60. package/dist/facades.js.map +1 -1
  61. package/dist/index.d.ts +4 -0
  62. package/dist/index.js +3 -0
  63. package/dist/index.js.map +1 -1
  64. package/dist/org-protocol.d.ts +1 -0
  65. package/dist/org-protocol.js +39 -36
  66. package/dist/org-protocol.js.map +1 -1
  67. package/dist/ownership-v21.js +7 -7
  68. package/dist/ownership-v21.js.map +1 -1
  69. package/dist/register-flow.js +2 -2
  70. package/dist/register-flow.js.map +1 -1
  71. package/dist/risk-control.d.ts +68 -0
  72. package/dist/risk-control.js +344 -0
  73. package/dist/risk-control.js.map +1 -0
  74. package/dist/semantic-facade.d.ts +176 -0
  75. package/dist/semantic-facade.js +611 -0
  76. package/dist/semantic-facade.js.map +1 -0
  77. package/dist/semantic-protocol.d.ts +810 -0
  78. package/dist/semantic-protocol.js +894 -0
  79. package/dist/semantic-protocol.js.map +1 -0
  80. package/dist/storage/lowlevel.d.ts +1 -0
  81. package/dist/storage/lowlevel.js +2 -2
  82. package/dist/storage/lowlevel.js.map +1 -1
  83. package/dist/tools/cross-sdk-agent.d.ts +11 -1
  84. package/dist/tools/cross-sdk-agent.js +388 -7
  85. package/dist/tools/cross-sdk-agent.js.map +1 -1
  86. package/dist/transport.d.ts +22 -1
  87. package/dist/transport.js +227 -22
  88. package/dist/transport.js.map +1 -1
  89. package/dist/v2/e2ee/decrypt.d.ts +5 -0
  90. package/dist/v2/e2ee/decrypt.js +22 -2
  91. package/dist/v2/e2ee/decrypt.js.map +1 -1
  92. package/dist/version.d.ts +1 -1
  93. package/dist/version.js +1 -1
  94. package/package.json +1 -1
package/dist/client.js CHANGED
@@ -27,6 +27,7 @@ import { validateAIDFormat, validateGroupIDFormat } from './validators.js';
27
27
  import { RPCTransport } from './transport.js';
28
28
  import { AuthFlow } from './auth.js';
29
29
  import { AgentMdManager, buildDefaultAgentMd } from './agent-md.js';
30
+ import { AgentMdChangeCoordinator } from './agent-md-changes.js';
30
31
  import { GroupIndexMetaCache } from './group-index.js';
31
32
  import { SeqTracker } from './seq-tracker.js';
32
33
  import { ClientRuntime } from './client/runtime.js';
@@ -36,6 +37,8 @@ import { IdentityRuntimeManager } from './client/identity.js';
36
37
  import { LifecycleController } from './client/lifecycle.js';
37
38
  import { PeerDirectory } from './client/peers.js';
38
39
  import { RpcPipeline } from './client/rpc-pipeline.js';
40
+ import { RiskController } from './risk-control.js';
41
+ import { ControlMessageCoordinator, messageClassOf } from './client/control-messages.js';
39
42
  import { CollabClient } from './collab/client.js';
40
43
  import { DebugFacade, CredentialFacade, OrgFacade, GroupFacade, MailFacade, MessageFacade, StreamFacade } from './facades.js';
41
44
  import { StorageVFS } from './storage/vfs.js';
@@ -43,7 +46,7 @@ import { V2E2EECoordinator } from './client/v2-e2ee.js';
43
46
  import { GroupStateCoordinator } from './client/group-state.js';
44
47
  import { decryptMessage, } from './v2/e2ee/index.js';
45
48
  import { ecdsaVerifyRaw } from './v2/crypto/ecdsa.js';
46
- import { decryptedOwnershipJsonText } from './v2/e2ee/decrypt.js';
49
+ import { decryptedOwnershipJsonText, decryptedMessagePayloadBytes } from './v2/e2ee/decrypt.js';
47
50
  import { isJsonObject, ConnectionState, STATE_TO_PUBLIC, } from './types.js';
48
51
  import { AID } from './aid.js';
49
52
  import { certFingerprint, certMatchesFingerprint, normalizeFingerprintHex, publicKeyMatchesFingerprint } from './cert-utils.js';
@@ -51,6 +54,7 @@ import { validateAgentMdCertificate, validateAgentMdDocument } from './agent-md-
51
54
  import { OwnershipFiles, OwnershipStorageAdapter, ownershipAid, ownershipFailure } from './ownership-v21.js';
52
55
  import { OwnershipWorkflow, OWNERSHIP_SDK_MESSAGE_TYPE, parseOwnershipMessage } from './ownership-workflow.js';
53
56
  import { canonicalJson, createCredential, createMemberCredential, signMembershipBinding, digest, validateMembershipBinding, verifyMembershipBinding, verifyCredentialSignature, createRepresentationProof, verifyServiceEvent, buildRequestBinding, validateRequestBinding, CredentialRenewal } from './org-protocol.js';
57
+ import { SemanticProtocolError } from './semantic-protocol.js';
54
58
  function isPromiseLike(value) {
55
59
  return Boolean(value && typeof value.then === 'function');
56
60
  }
@@ -675,12 +679,12 @@ export class AUNClient {
675
679
  this._credentialRenewalAbort = abort;
676
680
  const current = () => this._credentialRenewalAbort === abort && !abort.signal.aborted && aid === this._aid && version === this._serviceEventIdentityVersion && ['ready', 'connected'].includes(this._state) && !this._closing && !this._disconnecting;
677
681
  const guard = { current, signal: abort.signal };
678
- this._nextCredentialRevocationSyncAt ||= Date.now() + 3_600_000;
682
+ this._nextCredentialRevocationSyncAt = Date.now();
679
683
  const scan = async () => {
680
684
  const task = this._credentialRenewalScan.then(async () => {
681
685
  if (!current())
682
686
  return;
683
- const syncRevocations = Date.now() >= this._nextCredentialRevocationSyncAt;
687
+ const syncRevocations = this._sessionOptions.connection_kind !== 'short' && Date.now() >= this._nextCredentialRevocationSyncAt;
684
688
  if (syncRevocations)
685
689
  this._nextCredentialRevocationSyncAt = Date.now() + 3_600_000;
686
690
  await this._resumeCredentialRenewalAfterConnect(guard, syncRevocations);
@@ -710,13 +714,95 @@ export class AUNClient {
710
714
  }
711
715
  this._safeAsync(this._getCredentialRenewal().remember(credential, material));
712
716
  }
717
+ _semanticContextKey() {
718
+ return JSON.stringify([this._aid, this._currentAid?.certFingerprint, this._serviceEventIdentityVersion]);
719
+ }
720
+ _loadSemanticState() {
721
+ const state = (this._tokenStore._loadMetadataStrict?.(this._aid ?? '') ?? this._tokenStore.loadMetadata?.(this._aid ?? ''))?.semantic_credentials_v1;
722
+ return structuredClone(state ?? {});
723
+ }
724
+ _saveSemanticState(state) {
725
+ if (!this._aid)
726
+ throw new ValidationError('semantic credential persistence requires a loaded identity');
727
+ if (!this._tokenStore.saveMetadata)
728
+ throw new ValidationError('semantic credential metadata storage is unavailable');
729
+ this._tokenStore.saveMetadata(this._aid, { semantic_credentials_v1: state });
730
+ }
731
+ _semanticVerificationContext() {
732
+ const scope = this._semanticContextKey();
733
+ const current = () => { if (this._semanticContextKey() !== scope)
734
+ throw new ValidationError('semantic credential identity changed'); };
735
+ return {
736
+ resolveCertificate: async (fingerprint, aid, signedAt) => {
737
+ current();
738
+ const certificate = await this._fetchPeerCert(aid, fingerprint, undefined, signedAt);
739
+ current();
740
+ return certificate;
741
+ },
742
+ verifyCertificateTrust: async (material, context) => {
743
+ current();
744
+ const certificate = typeof material === 'string' && material.startsWith('-----BEGIN CERTIFICATE-----')
745
+ ? material : new crypto.X509Certificate(typeof material === 'string' ? Buffer.from(material, 'base64') : material).toString();
746
+ const gateway = await this._resolveOwnershipCertificateGateway(context.expectedAID);
747
+ await this._auth.verifyPeerCertificate(gateway, certificate, context.expectedAID, context.signedAt);
748
+ await this._auth.verifyPeerCertificate(gateway, certificate, context.expectedAID, undefined, true);
749
+ current();
750
+ },
751
+ };
752
+ }
753
+ _semanticMessageVerificationContext() {
754
+ const actor = this._aid, device = this._deviceId, scope = this._semanticContextKey(), session = this._v2Session;
755
+ const trust = this._semanticVerificationContext();
756
+ const current = () => {
757
+ if (!actor || this._semanticContextKey() !== scope || device !== this._deviceId || session !== this._v2Session)
758
+ throw new SemanticProtocolError('SEMANTIC_IDENTITY_INVALID', 'semantic message verification identity changed');
759
+ };
760
+ const senderCertificate = async (sender, fingerprint, signedAt) => {
761
+ current();
762
+ const certificate = await this._fetchPeerCert(sender, fingerprint, undefined, signedAt);
763
+ if (!publicKeyMatchesFingerprint(certificate, fingerprint))
764
+ throw new SemanticProtocolError('SEMANTIC_IDENTITY_INVALID', 'historical sender key fingerprint differs');
765
+ current();
766
+ return certificate;
767
+ };
768
+ return {
769
+ selfAid: actor ?? '', selfDeviceId: device,
770
+ resolveDecryptionKeys: async (envelope) => {
771
+ current();
772
+ if (!session || session.aid !== actor || session.deviceId !== device)
773
+ throw new SemanticProtocolError('SEMANTIC_STATE_UNAVAILABLE', 'authenticated V2 receiver session is unavailable');
774
+ const recipient = envelope.recipient;
775
+ const row = Array.isArray(envelope.recipients) ? envelope.recipients.find((item) => Array.isArray(item) && item.length >= 6 && item[0] === actor && (item[1] === device || item[1] === '')) : undefined;
776
+ if (!row && (!recipient || recipient.aid !== actor || recipient.device_id !== device && recipient.device_id !== ''))
777
+ throw new SemanticProtocolError('SEMANTIC_IDENTITY_INVALID', 'original receiver row is unavailable');
778
+ const spkId = String(row ? row[5] ?? '' : recipient.spk_id ?? ''), aad = envelope.aad;
779
+ if (envelope.type === 'e2ee.group_encrypted') {
780
+ const keys = session.getGroupDecryptKeys(String(aad.group_id ?? ''), spkId);
781
+ return { ikPriv: keys.ikPriv, spkPriv: keys.spkPriv ?? undefined };
782
+ }
783
+ return session.getDecryptKeys(spkId);
784
+ },
785
+ resolveSenderKey: async (sender, _device, fingerprint, signedAt) => {
786
+ const certificate = await senderCertificate(sender, fingerprint, signedAt);
787
+ return new Uint8Array(new crypto.X509Certificate(certificate).publicKey.export({ type: 'spki', format: 'der' }));
788
+ },
789
+ verifySenderIdentity: async (sender, _device, fingerprint, signedAt, publicKey) => {
790
+ const certificate = await senderCertificate(sender, fingerprint, signedAt);
791
+ await trust.verifyCertificateTrust(certificate, { expectedAID: sender, signedAt, now: Math.trunc(Date.now() / 1000) });
792
+ const expected = new crypto.X509Certificate(certificate).publicKey.export({ type: 'spki', format: 'der' });
793
+ if (!expected.equals(Buffer.from(publicKey)))
794
+ throw new SemanticProtocolError('SEMANTIC_IDENTITY_INVALID', 'historical sender identity differs from message key');
795
+ current();
796
+ },
797
+ };
798
+ }
713
799
  async _verifyIncomingServiceEvent(event, payload) {
714
800
  const targetAid = this._identity?.aid ?? '';
715
801
  const version = this._serviceEventIdentityVersion;
716
802
  const signal = this._serviceEventIdentityAbort.signal;
717
803
  const expiresAt = Number(payload?.expires_at);
718
804
  const transport = this._transport, connectionGeneration = transport?._connectionGeneration, workerGeneration = transport?._workerGeneration;
719
- const current = () => version === this._serviceEventIdentityVersion && targetAid === (this._identity?.aid ?? '') && transport === this._transport && connectionGeneration === this._transport?._connectionGeneration && workerGeneration === this._transport?._workerGeneration && Date.now() / 1000 < expiresAt;
805
+ const current = () => version === this._serviceEventIdentityVersion && targetAid === (this._identity?.aid ?? '') && transport === this._transport && connectionGeneration === this._transport?._connectionGeneration && workerGeneration === this._transport?._workerGeneration && Date.now() / 1000 <= expiresAt + 300;
720
806
  const verified = await verifyServiceEvent(event, payload, { targetAid, resolveEventCertificate: (aid, fingerprint) => this._fetchServiceEventCertificate(aid, fingerprint), resolveCredentialService: this.resolveCredentialService });
721
807
  if (!current())
722
808
  throw new ValidationError('service event receiver identity changed');
@@ -846,6 +932,27 @@ export class AUNClient {
846
932
  _v2KeyStore;
847
933
  _v2SessionInitInFlight = null;
848
934
  _v2RuntimeGeneration = 0;
935
+ _controlMessageCoordinator;
936
+ _agentMdChanges;
937
+ _agentMdCommitUnsubscribe;
938
+ _installAgentMdChangeHooks() {
939
+ this._agentMdCommitUnsubscribe?.();
940
+ this._agentMdCommitUnsubscribe = AgentMdManager.subscribeCommits(this._configModel.aunPath, () => {
941
+ const coordinator = this._getAgentMdChanges();
942
+ if (this._state === 'ready')
943
+ void coordinator.flushNotifications().catch((error) => this._clientLog.debug(String(error)));
944
+ coordinator.schedule();
945
+ });
946
+ }
947
+ _getAgentMdChanges() {
948
+ return this._agentMdChanges ??= new AgentMdChangeCoordinator(this);
949
+ }
950
+ _onSdkControl(protocol, version, payload, senderAid) {
951
+ return this._getAgentMdChanges().handle(protocol, version, payload, senderAid);
952
+ }
953
+ _controlMessages() {
954
+ return this._controlMessageCoordinator ??= new ControlMessageCoordinator(this);
955
+ }
849
956
  /** V2 bootstrap 缓存:aid/group:id → 设备列表 + 时间戳 */
850
957
  _v2BootstrapCache = new Map();
851
958
  _v2SigCache = new Map();
@@ -865,6 +972,30 @@ export class AUNClient {
865
972
  _reconnectAbort = null;
866
973
  _reconnectTask = null;
867
974
  _reconnectEventDispatchDepth = 0;
975
+ _gatewayConnectionId = '';
976
+ _negotiatedCapabilities = {};
977
+ _riskNegotiated = false;
978
+ _riskRestoredAid = '';
979
+ _riskEvents = [];
980
+ _riskApplying = false;
981
+ _riskApplyTail = Promise.resolve();
982
+ _riskScalarReconnectAt = 0;
983
+ _riskHttpRetryAt = new Map();
984
+ _risk = new RiskController({
985
+ persist: async (state) => {
986
+ if (!this._aid || !this._tokenStore.saveMetadata || !isJsonObject(state.target) || state.target.aid !== this._aid || state.target.device_id !== this._deviceId)
987
+ throw new Error('risk persistence unavailable');
988
+ this._tokenStore.saveMetadata(this._aid, { ...(this._tokenStore.loadMetadata?.(this._aid) ?? {}), [`risk_state_v1:${this._deviceId}`]: state });
989
+ },
990
+ onState: (event) => {
991
+ if (this._riskApplying)
992
+ this._riskEvents.push(event);
993
+ else
994
+ this._dispatcher.enqueue('risk_state_changed', event);
995
+ },
996
+ });
997
+ _migrationPending = false;
998
+ _migrationRevision = new Map();
868
999
  _serverKicked = false;
869
1000
  /** 缓存最近一次 gateway.disconnect 信息(含服务端附带的 detail),用于后续 connection.state 透传 */
870
1001
  _lastDisconnectInfo = null;
@@ -973,6 +1104,7 @@ export class AUNClient {
973
1104
  fetchEntityIdentity: (target, type) => this._freshEntityCertificate(target, type),
974
1105
  fetchEntitySigner: (target, fingerprint) => this._freshEntitySigner(target, fingerprint),
975
1106
  });
1107
+ this._installAgentMdChangeHooks();
976
1108
  this._transport = new RPCTransport({
977
1109
  eventDispatcher: this._dispatcher,
978
1110
  timeout: 35_000,
@@ -980,6 +1112,8 @@ export class AUNClient {
980
1112
  logger: this._logger.for('aun_core.transport'),
981
1113
  dnsNet,
982
1114
  });
1115
+ this._installRiskHooks();
1116
+ this._transport.setClassMessageHandler((event, payload) => this._controlMessages().onPush(payload, event.includes('group.')));
983
1117
  this._transport.setProtocolMetaObserver((meta) => this._observeRpcMeta(meta));
984
1118
  this._transport.setServiceEventVerifier((event, payload) => this._verifyIncomingServiceEvent(event, payload), () => this._identity?.aid ?? '', () => this._serviceEventIdentityVersion);
985
1119
  this._transport.setProtocolDisconnectCallback((err, closeCode) => this._handleTransportDisconnect(err, closeCode));
@@ -1014,6 +1148,8 @@ export class AUNClient {
1014
1148
  this._dispatcher.subscribeProtocol('_raw.message.read_changed', (data) => this._onRawReadChanged(data, 'p2p'));
1015
1149
  // V2 P2P 推送通知:收到通知后自动走 message.v2.pull 拉取并解密
1016
1150
  this._dispatcher.subscribeProtocol('_raw.peer.v2.message_received', (data) => {
1151
+ if (this._controlMessages().onPush(data))
1152
+ return;
1017
1153
  const sender = isJsonObject(data) ? String(data.sender_aid ?? '') : '';
1018
1154
  const ns = this._p2pPeerPullEnabled ? (sender ? this._delivery.peerNamespace(sender) : '') : this._aid ? `p2p:${this._aid}` : '';
1019
1155
  const hasInline = isJsonObject(data)
@@ -1025,6 +1161,8 @@ export class AUNClient {
1025
1161
  });
1026
1162
  // V2 群组消息推送通知:收到通知后自动走 group.v2.pull 拉取并解密
1027
1163
  this._dispatcher.subscribeProtocol('_raw.group.v2.message_created', (data) => {
1164
+ if (this._controlMessages().onPush(data, true))
1165
+ return;
1028
1166
  const groupId = isJsonObject(data)
1029
1167
  ? String(data.group_id ?? '').trim()
1030
1168
  : '';
@@ -1058,6 +1196,10 @@ export class AUNClient {
1058
1196
  }
1059
1197
  // 服务端主动断开通知:记录日志并标记不重连
1060
1198
  this._dispatcher.subscribeProtocol('_raw.gateway.disconnect', (data) => this._onGatewayDisconnect(data));
1199
+ this._dispatcher.subscribeProtocol('notification', (data) => {
1200
+ if (isJsonObject(data) && data.method === 'gateway.migrate' && !('id' in data))
1201
+ this._onGatewayMigrate(data.params);
1202
+ });
1061
1203
  }
1062
1204
  // ── 属性 ──────────────────────────────────────────────────
1063
1205
  /** 当前 AID */
@@ -1201,7 +1343,9 @@ export class AUNClient {
1201
1343
  async releaseSubaid(subAid) { return this._getOwnershipWorkflow().releaseSubaid(subAid); }
1202
1344
  async transferSubaid(subAid, newOwnerAid) { return this._getOwnershipWorkflow().transferSubaid(subAid, newOwnerAid); }
1203
1345
  async checkOwnershipConsensus(subAid) {
1204
- return this._createOwnershipFiles((aid) => this._agentMdManager._readOwnershipQuery(aid)).checkConsensus(ownershipAid(subAid ?? this._aid, 'sub_aid'));
1346
+ const target = ownershipAid(subAid ?? this._aid, 'sub_aid');
1347
+ const result = await this._createOwnershipFiles((aid) => this._agentMdManager._readOwnershipQuery(aid)).checkConsensus(target);
1348
+ return result.status === 'consensus' ? result : this._checkOwnershipConsensusFresh(target);
1205
1349
  }
1206
1350
  async _checkOwnershipConsensusFresh(subAid) {
1207
1351
  return this._getOwnershipWorkflow().checkOwnershipConsensus(subAid);
@@ -1301,6 +1445,12 @@ export class AUNClient {
1301
1445
  return session.aid === this._aid && session.deviceId === this._deviceId;
1302
1446
  }
1303
1447
  _resetV2IdentityRuntime() {
1448
+ this._controlMessageCoordinator?.close();
1449
+ this._agentMdChanges?.close();
1450
+ this._agentMdChanges = undefined;
1451
+ this._negotiatedCapabilities = {};
1452
+ this._riskNegotiated = false;
1453
+ this._riskRestoredAid = '';
1304
1454
  this._v2RuntimeGeneration += 1;
1305
1455
  const keyStore = this._v2KeyStore;
1306
1456
  try {
@@ -1416,6 +1566,7 @@ export class AUNClient {
1416
1566
  fetchEntityIdentity: (target, type) => this._freshEntityCertificate(target, type),
1417
1567
  fetchEntitySigner: (target, fingerprint) => this._freshEntitySigner(target, fingerprint),
1418
1568
  });
1569
+ this._installAgentMdChangeHooks();
1419
1570
  this._transport = new RPCTransport({
1420
1571
  eventDispatcher: this._dispatcher,
1421
1572
  timeout: 35_000,
@@ -1423,6 +1574,8 @@ export class AUNClient {
1423
1574
  logger: this._logger.for('aun_core.transport'),
1424
1575
  dnsNet,
1425
1576
  });
1577
+ this._installRiskHooks();
1578
+ this._transport.setClassMessageHandler((event, payload) => this._controlMessages().onPush(payload, event.includes('group.')));
1426
1579
  this._transport.setProtocolMetaObserver((meta) => this._observeRpcMeta(meta));
1427
1580
  this._transport.setServiceEventVerifier((event, payload) => this._verifyIncomingServiceEvent(event, payload), () => this._identity?.aid ?? '', () => this._serviceEventIdentityVersion);
1428
1581
  this._transport.setProtocolDisconnectCallback((err, closeCode) => this._handleTransportDisconnect(err, closeCode));
@@ -1526,6 +1679,7 @@ export class AUNClient {
1526
1679
  }
1527
1680
  /** 关闭连接 */
1528
1681
  async close() {
1682
+ this._risk.close();
1529
1683
  this._peerGatewayCache.clear();
1530
1684
  return this._lifecycle.close();
1531
1685
  }
@@ -1536,6 +1690,155 @@ export class AUNClient {
1536
1690
  async disconnect() {
1537
1691
  return this._lifecycle.disconnect();
1538
1692
  }
1693
+ get riskState() { return Object.freeze(structuredClone(this._risk.state)); }
1694
+ _installRiskHooks() {
1695
+ this._transport.setRiskControlHandler((payload) => this._onRiskControl(payload));
1696
+ this._transport.setRequestAdmission((request) => this._risk.acquire(request));
1697
+ this._transport.setRequestCheck((request) => this._risk.check(request));
1698
+ this._auth.setRequestAdmission((request) => this._risk.acquire(request));
1699
+ this._auth.setRiskSupport(() => this._gatewayUrl?.startsWith('wss://') === true);
1700
+ this._auth.setHttpHandler((url, init, perform) => this._riskFetch(url, init, perform, 'http:gateway:pki'));
1701
+ this._discovery.setHttpHandler((url, init, perform) => this._riskFetch(url, init, perform, new URL(url).pathname === '/health' ? 'http:gateway:health' : 'http:discovery:gateway'));
1702
+ const aid = this._aid;
1703
+ if (aid)
1704
+ this._aidStore?.setRiskController(aid, async () => {
1705
+ if (this._aid !== aid || this._deviceId !== this._aidStore?.deviceId)
1706
+ return null;
1707
+ await this._restoreRiskState();
1708
+ return this._risk.appliesTo(aid, this._deviceId) ? this._risk : null;
1709
+ });
1710
+ this._agentMdManager.setHttpHandler((url, init, perform) => {
1711
+ const headers = new Headers(init.headers);
1712
+ const scope = headers.has('X-AUN-Ownership-Check') || headers.has('X-AUN-Ownership-Stage') ? 'http:nameservice:ownership' : 'http:nameservice:agent_md';
1713
+ return this._riskFetch(url, init, perform, scope);
1714
+ });
1715
+ }
1716
+ async _restoreRiskState() {
1717
+ const aid = this._aid;
1718
+ if (!aid || this._riskRestoredAid === aid)
1719
+ return;
1720
+ this._risk.reset();
1721
+ this._riskHttpRetryAt.clear();
1722
+ this._riskScalarReconnectAt = 0;
1723
+ const raw = this._tokenStore.loadMetadata?.(aid)?.[`risk_state_v1:${this._deviceId}`];
1724
+ if (raw) {
1725
+ try {
1726
+ const state = typeof raw === 'string' ? JSON.parse(raw) : raw;
1727
+ if (!isJsonObject(state) || !isJsonObject(state.target) || state.target.aid !== aid || state.target.device_id !== this._deviceId || !isJsonObject(state.signing_keys))
1728
+ throw new Error('risk state invalid');
1729
+ const [keyId, key] = Object.entries(state.signing_keys)[0] ?? [];
1730
+ if (!keyId || !Array.isArray(key))
1731
+ throw new Error('risk signing key unavailable');
1732
+ this._risk.configure({ aid, device_id: this._deviceId, slot_id: this._slotId, connection_id: '' }, { key_id: keyId, public_key: Buffer.from(key).toString('base64url'), algorithm: 'ECDSA-P256-SHA256' }, Number(state.server_time_ms));
1733
+ await this._risk.restore(state);
1734
+ }
1735
+ catch {
1736
+ throw new StateError('persisted risk state could not be verified', { stringCode: 'risk_state_unavailable', retryable: true });
1737
+ }
1738
+ }
1739
+ this._riskRestoredAid = aid;
1740
+ }
1741
+ _assertRiskReconnect() {
1742
+ const delay = this._riskReconnectDelay();
1743
+ if (delay > 0)
1744
+ throw new ConnectionError('connection postponed by risk policy', { stringCode: 'risk_restricted', retryable: true, data: { retry_after_ms: Math.ceil(delay) } });
1745
+ }
1746
+ _riskReconnectDelay() { return Math.max(this._risk.reconnectDelayMs(), this._riskScalarReconnectAt - (typeof performance !== 'undefined' ? performance.now() : Date.now()), 0); }
1747
+ async _applyRiskHello(hello) {
1748
+ this._riskNegotiated = false;
1749
+ if (!isJsonObject(hello.negotiated_capabilities) || hello.negotiated_capabilities.risk_test_v1 !== true)
1750
+ return;
1751
+ if (!this._gatewayUrl?.startsWith('wss://') || !this._configModel.verifySsl || !this._aid || !isJsonObject(hello.risk_signing_key))
1752
+ return;
1753
+ const key = hello.risk_signing_key;
1754
+ const initial = isJsonObject(hello.risk_state) ? hello.risk_state : {};
1755
+ this._risk.configure({ aid: this._aid, device_id: this._deviceId, slot_id: this._slotId, connection_id: this._gatewayConnectionId }, { key_id: String(key.key_id ?? ''), public_key: String(key.public_key ?? ''), algorithm: String(key.algorithm ?? '') }, Number(hello.server_time_ms ?? initial.server_time_ms));
1756
+ this._riskNegotiated = true;
1757
+ await this._transport.setRiskSnapshot(this._risk.exportState(true));
1758
+ if (Array.isArray(initial.states))
1759
+ for (const state of initial.states)
1760
+ if (isJsonObject(state))
1761
+ await this._onRiskControl(state);
1762
+ }
1763
+ async _onRiskControl(payload) {
1764
+ const identity = JSON.stringify([this._aid, this._deviceId, this._slotId, this._serviceEventIdentityVersion, this._transport._connectionGeneration]);
1765
+ const run = this._riskApplyTail.then(async () => {
1766
+ const current = () => identity === JSON.stringify([this._aid, this._deviceId, this._slotId, this._serviceEventIdentityVersion, this._transport._connectionGeneration]);
1767
+ if (current())
1768
+ await this._applyRiskControl(payload, current);
1769
+ });
1770
+ this._riskApplyTail = run.catch(() => { });
1771
+ await run;
1772
+ }
1773
+ async _applyRiskControl(payload, current) {
1774
+ if (!this._riskNegotiated)
1775
+ return;
1776
+ this._riskApplying = true;
1777
+ let status;
1778
+ try {
1779
+ status = await this._risk.apply(payload);
1780
+ }
1781
+ catch {
1782
+ this._riskApplying = false;
1783
+ this._riskEvents = [];
1784
+ return;
1785
+ }
1786
+ this._riskApplying = false;
1787
+ await this._transport.setRiskSnapshot(this._risk.exportState(true));
1788
+ if (!current()) {
1789
+ this._riskEvents = [];
1790
+ return;
1791
+ }
1792
+ const params = { version: 1, delivery_id: payload.delivery_id, subject: payload.subject, revision: payload.revision, status: this._risk.persistenceFailed ? 'failed' : status };
1793
+ if (this._risk.persistenceFailed)
1794
+ params.error_code = 'persistence_unavailable';
1795
+ const acknowledgement = this._transport.call('gateway.risk_ack', params, 500).catch(() => { });
1796
+ for (const event of this._riskEvents.splice(0))
1797
+ this._dispatcher.enqueue('risk_state_changed', event);
1798
+ if (this._risk.shouldDisconnect()) {
1799
+ await acknowledgement;
1800
+ if (current())
1801
+ void this._handleTransportDisconnect(new ConnectionError('connection restricted by risk policy', { code: 4029, stringCode: 'risk_restricted', retryable: true }), 4029);
1802
+ }
1803
+ }
1804
+ async _riskFetch(input, init = {}, perform = fetch, requestScope) {
1805
+ await this._restoreRiskState();
1806
+ const url = new URL(input);
1807
+ const route = `${url.origin}${url.pathname}`;
1808
+ const monotonicNow = () => typeof performance !== 'undefined' ? performance.now() : Date.now();
1809
+ const retryAt = this._riskHttpRetryAt.get(route) ?? 0;
1810
+ if (retryAt > monotonicNow())
1811
+ throw new StateError('HTTP request postponed by risk response', { stringCode: 'risk_restricted', retryable: true, data: { retry_after_ms: Math.ceil(retryAt - monotonicNow()) } });
1812
+ const release = this._risk.acquire(requestScope ?? (['GET', 'HEAD'].includes(String(init.method ?? 'GET').toUpperCase()) ? 'http:storage:read' : 'http:storage:write'));
1813
+ try {
1814
+ const response = await perform(input, init);
1815
+ if (response.status === 429) {
1816
+ try {
1817
+ const body = await response.clone().json();
1818
+ const detail = isJsonObject(body) && isJsonObject(body.error) && isJsonObject(body.error.data) ? body.error.data : isJsonObject(body) && isJsonObject(body.data) ? body.data : isJsonObject(body) ? body : {};
1819
+ const wrapper = detail.risk_state;
1820
+ if (this._riskNegotiated && isJsonObject(wrapper) && Array.isArray(wrapper.states))
1821
+ for (const state of wrapper.states)
1822
+ if (isJsonObject(state))
1823
+ await this._onRiskControl(state);
1824
+ const delayMs = Number(detail.retry_after_ms);
1825
+ if (Number.isSafeInteger(delayMs) && delayMs > 0)
1826
+ this._riskHttpRetryAt.set(route, monotonicNow() + delayMs);
1827
+ }
1828
+ catch { /* 无签名状态的 HTTP 错误由调用入口处理 */ }
1829
+ const header = response.headers.get('Retry-After');
1830
+ if (header) {
1831
+ const delayMs = /^\d+$/.test(header) ? Number(header) * 1000 : Math.max(0, Date.parse(header) - Date.now());
1832
+ if (Number.isFinite(delayMs) && delayMs > 0)
1833
+ this._riskHttpRetryAt.set(route, Math.max(this._riskHttpRetryAt.get(route) ?? 0, monotonicNow() + delayMs));
1834
+ }
1835
+ }
1836
+ return response;
1837
+ }
1838
+ finally {
1839
+ release();
1840
+ }
1841
+ }
1539
1842
  // ── RPC ───────────────────────────────────────────────────
1540
1843
  /**
1541
1844
  * 发送 JSON-RPC 调用。
@@ -1730,7 +2033,7 @@ export class AUNClient {
1730
2033
  async _resumeOrgRequest(method, params, target) {
1731
2034
  const binding = params.request_binding;
1732
2035
  const now = Math.trunc(Date.now() / 1000);
1733
- if (!isJsonObject(binding) || now < Number(binding.exp))
2036
+ if (!isJsonObject(binding) || now <= Number(binding.exp) + 300)
1734
2037
  return params;
1735
2038
  const credentials = isJsonObject(params.credentials) ? params.credentials : {};
1736
2039
  const credential = ['member_credential', 'approval_credential', 'audit_credential'].map((field) => credentials[field]).find(isJsonObject) ?? params.credential_template;
@@ -1787,7 +2090,7 @@ export class AUNClient {
1787
2090
  checkpoint = { ...checkpoint, result, phase: 'upload' };
1788
2091
  tokens.saveMetadata(orgAid, { org_identity_creation: checkpoint });
1789
2092
  }
1790
- await this._uploadOrgAgentMd(store, orgAid, params);
2093
+ await this._uploadOrgAgentMd(store, orgAid, params, checkpoint.result);
1791
2094
  tokens.saveMetadata(orgAid, { org_identity_creation: null });
1792
2095
  return checkpoint.result;
1793
2096
  }
@@ -1829,7 +2132,7 @@ export class AUNClient {
1829
2132
  const now = Math.trunc(Date.now() / 1000);
1830
2133
  if (binding.org_id !== orgAid || nextBinding.org_id !== orgAid || binding.member_aid === ownerAid || nextBinding.member_aid !== ownerAid
1831
2134
  || binding.role !== 'member' || nextBinding.role !== 'owner' || digest(previous.terms) !== binding.terms_hash || digest(next.terms) !== nextBinding.terms_hash
1832
- || !Number.isSafeInteger(nextBinding.iat) || !Number.isSafeInteger(nextBinding.exp) || Number(nextBinding.iat) > now || Number(nextBinding.exp) <= now
2135
+ || !Number.isSafeInteger(nextBinding.iat) || !Number.isSafeInteger(nextBinding.exp) || Number(nextBinding.iat) > now + 300 || now > Number(nextBinding.exp) + 300
1833
2136
  || credential.iss !== orgAid || credential.sub !== binding.member_aid || credential.parent != null || credential.delegate !== false
1834
2137
  || ext.credential_type !== 'member' || ext.bound_to_membership !== true || ext.org_id !== orgAid || ext.membership_id !== binding.membership_id
1835
2138
  || ext.binding_version !== binding.binding_version || ext.membership_terms_hash !== binding.terms_hash
@@ -1848,7 +2151,7 @@ export class AUNClient {
1848
2151
  const checkedAt = Math.trunc(Date.now() / 1000);
1849
2152
  verifyMembershipBinding(binding, resolve, checkedAt, orgAid);
1850
2153
  verifyCredentialSignature(credential, resolve, checkedAt, orgAid);
1851
- if (Number(nextBinding.exp) <= checkedAt)
2154
+ if (checkedAt > Number(nextBinding.exp) + 300)
1852
2155
  throw ownershipFailure('invalid', 'Org transfer membership materials do not match');
1853
2156
  }
1854
2157
  async completeOrgTransfer(params = {}, options = {}) {
@@ -1938,7 +2241,7 @@ export class AUNClient {
1938
2241
  tokenStore.saveMetadata(orgAid, { org_identity_transfer: pending });
1939
2242
  }
1940
2243
  const result = await this.call('org.lifecycle.transferAccept', await this._resumeOrgRequest('org.lifecycle.transferAccept', pending.finalize_params, target));
1941
- await this._uploadOrgAgentMd(store, orgAid, pending.source_params);
2244
+ await this._uploadOrgAgentMd(store, orgAid, pending.source_params, result);
1942
2245
  tokenStore.saveMetadata(orgAid, { org_identity_transfer: { transfer_id: transferId, owner_aid: owner.aid, input_hash: inputHash, phase: 'completed', result } });
1943
2246
  localStore.clearPendingGroupBind(pendingKey);
1944
2247
  return result;
@@ -1976,15 +2279,18 @@ export class AUNClient {
1976
2279
  }
1977
2280
  return result;
1978
2281
  }
1979
- async _uploadOrgAgentMd(store, orgAid, params) {
2282
+ async _uploadOrgAgentMd(store, orgAid, params, initialResult) {
1980
2283
  const owner = this._currentAid;
1981
2284
  if (!owner)
1982
2285
  throw ownershipFailure('forbidden', 'Org agent.md requires current owner');
1983
- if (await this._resumeGroupAgentMdUpload(store, orgAid))
2286
+ const organization = isJsonObject(initialResult) && isJsonObject(initialResult.organization) ? initialResult.organization : undefined;
2287
+ const bootstrap = organization?.org_id === orgAid && organization.owner_aid === owner.aid && organization.status === 'active'
2288
+ ? { aid: orgAid, ownerAid: owner.aid, certFingerprint: this._localEntityCertificate(orgAid).certFingerprint, completedTransfer: Boolean(params.transfer_id && isJsonObject(initialResult) && initialResult.transfer_id === params.transfer_id) } : undefined;
2289
+ if (await this._resumeGroupAgentMdUpload(store, orgAid, bootstrap))
1984
2290
  return;
1985
2291
  const identity = this._localEntityCertificate(orgAid);
1986
2292
  const content = String(params.org_agent_md ?? buildDefaultAgentMd(orgAid, { type: 'org', owner: owner.aid, entityCertFingerprint: identity.certFingerprint, name: String(params.name ?? orgAid) }));
1987
- const uploaded = await store.uploadAgentMd(orgAid, content, { uploaderAid: owner.aid, entityOwnerResolver: (aid, type) => this._resolveEntityAgentMdOwner(aid, type), entityIdentityGetter: (aid) => this._freshEntityCertificate(aid, 'org') });
2293
+ const uploaded = await store.uploadAgentMd(orgAid, content, { uploaderAid: owner.aid, entityOwnerResolver: (aid, type) => this._resolveEntityAgentMdOwner(aid, type, bootstrap), entityIdentityGetter: (aid) => this._freshEntityCertificate(aid, 'org') });
1988
2294
  if (!uploaded.ok) {
1989
2295
  const code = ['invalid', 'unavailable', 'forbidden', 'conflict', 'persistence_unavailable'].includes(uploaded.error.code) ? uploaded.error.code : uploaded.error.code === 'NETWORK_ERROR' ? 'unavailable' : 'invalid';
1990
2296
  throw ownershipFailure(code, uploaded.error.message, code === 'unavailable');
@@ -2100,7 +2406,7 @@ export class AUNClient {
2100
2406
  if (!imported.ok) {
2101
2407
  throw new ValidationError(imported.error.message);
2102
2408
  }
2103
- await this._uploadGroupAgentMd(store, groupAid, payload, group);
2409
+ await this._uploadGroupAgentMd(store, groupAid, payload, group, certPem);
2104
2410
  if (keystore && typeof keystore.clearPendingGroupBind === 'function') {
2105
2411
  keystore.clearPendingGroupBind(pendingKey);
2106
2412
  }
@@ -2168,7 +2474,7 @@ export class AUNClient {
2168
2474
  '',
2169
2475
  ].join('\n');
2170
2476
  }
2171
- async _resumeGroupAgentMdUpload(store, groupAid) {
2477
+ async _resumeGroupAgentMdUpload(store, groupAid, bootstrap) {
2172
2478
  const identityStore = store;
2173
2479
  const pendingManager = identityStore._agentMdManager ?? this._agentMdManager;
2174
2480
  const pending = pendingManager.pendingUpload(groupAid);
@@ -2188,7 +2494,7 @@ export class AUNClient {
2188
2494
  uploadViaGatewayHost: true,
2189
2495
  ownerAidGetter: () => identity.aid,
2190
2496
  currentAidGetter: () => identity,
2191
- entityOwnerResolver: (aid, type) => this._resolveEntityAgentMdOwner(aid, type),
2497
+ entityOwnerResolver: (aid, type) => this._resolveEntityAgentMdOwner(aid, type, bootstrap?.completedTransfer || pending.condition?.if_none_match === '*' ? bootstrap : undefined),
2192
2498
  entityIdentityGetter: (aid, type) => this._freshEntityCertificate(aid, type),
2193
2499
  entitySignerResolver: (aid, fingerprint) => this._freshEntitySigner(aid, fingerprint),
2194
2500
  gatewayResolver: (aid) => identityStore._resolveGateway(aid),
@@ -2203,15 +2509,16 @@ export class AUNClient {
2203
2509
  });
2204
2510
  return await manager.resumePendingUpload(groupAid);
2205
2511
  }
2206
- async _uploadGroupAgentMd(store, groupAid, params, group) {
2512
+ async _uploadGroupAgentMd(store, groupAid, params, group, initialCertificate) {
2207
2513
  const uploaderAid = String(this._aid ?? '').trim();
2208
2514
  if (!uploaderAid) {
2209
2515
  throw new ValidationError('group agent.md upload requires current owner AID');
2210
2516
  }
2211
2517
  this._aidStore = store;
2212
- if (await this._resumeGroupAgentMdUpload(store, groupAid))
2518
+ const bootstrap = initialCertificate ? { aid: groupAid, ownerAid: String(group.owner_aid ?? '').trim(), certFingerprint: certFingerprint(initialCertificate), type: 'group' } : undefined;
2519
+ if (await this._resumeGroupAgentMdUpload(store, groupAid, bootstrap))
2213
2520
  return;
2214
- const uploaded = await store.uploadAgentMd(groupAid, this._groupAgentMdContent(groupAid, params, group), { uploaderAid, entityOwnerResolver: (aid, type) => this._resolveEntityAgentMdOwner(aid, type), entityIdentityGetter: (aid) => this._freshEntityCertificate(aid) });
2521
+ const uploaded = await store.uploadAgentMd(groupAid, this._groupAgentMdContent(groupAid, params, group), { uploaderAid, entityOwnerResolver: (aid, type) => this._resolveEntityAgentMdOwner(aid, type, bootstrap), entityIdentityGetter: (aid) => this._freshEntityCertificate(aid) });
2215
2522
  if (!uploaded.ok) {
2216
2523
  const code = ['invalid', 'unavailable', 'forbidden', 'conflict', 'persistence_unavailable'].includes(uploaded.error.code) ? uploaded.error.code : uploaded.error.code === 'NETWORK_ERROR' ? 'unavailable' : 'invalid';
2217
2524
  throw ownershipFailure(code, `group agent.md upload failed: ${uploaded.error.message}`, code === 'unavailable');
@@ -2232,8 +2539,20 @@ export class AUNClient {
2232
2539
  const material = await this._auth.resolveOwnershipCertificate(await this._resolveOwnershipCertificateGateway(aid), aid, fingerprint, 'signing');
2233
2540
  return AID._create({ aid, aunPath: this._configModel.aunPath, certPem: material.cert_pem, certValid: true, privateKeyPem: null, privateKeyValid: false });
2234
2541
  }
2235
- async _resolveEntityAgentMdOwner(aid, type) {
2236
- const authority = await this._agentMdManager.readEntityAuthorityFresh(aid);
2542
+ async _resolveEntityAgentMdOwner(aid, type, bootstrap) {
2543
+ let authority;
2544
+ try {
2545
+ authority = bootstrap?.completedTransfer && type === 'org' && bootstrap.aid === aid && bootstrap.ownerAid === this._currentAid?.aid
2546
+ ? { owner_aid: bootstrap.ownerAid, entity_cert_fingerprint: bootstrap.certFingerprint }
2547
+ : await this._agentMdManager.readEntityAuthorityFresh(aid);
2548
+ }
2549
+ catch (error) {
2550
+ if (!bootstrap || bootstrap.aid !== aid || type !== (bootstrap.type ?? 'org') || !(error instanceof AUNError) || !isJsonObject(error.data) || error.data.http_status !== 404)
2551
+ throw error;
2552
+ if (!bootstrap.ownerAid || bootstrap.ownerAid !== this._currentAid?.aid)
2553
+ throw ownershipFailure('forbidden', 'entity initial owner does not match uploader');
2554
+ authority = { owner_aid: bootstrap.ownerAid, entity_cert_fingerprint: bootstrap.certFingerprint };
2555
+ }
2237
2556
  const identity = await this._freshEntityCertificate(aid, type);
2238
2557
  if (authority.entity_cert_fingerprint !== identity.certFingerprint)
2239
2558
  throw ownershipFailure('conflict', 'entity authority changed during current-certificate verification');
@@ -2301,7 +2620,7 @@ export class AUNClient {
2301
2620
  if (!imported.ok) {
2302
2621
  throw new ValidationError(imported.error.message);
2303
2622
  }
2304
- await this._uploadGroupAgentMd(store, groupAid, payload, group);
2623
+ await this._uploadGroupAgentMd(store, groupAid, payload, group, certPem);
2305
2624
  // 落盘成功,清除 pending 槽位
2306
2625
  if (groupId && keystore && typeof keystore.clearPendingGroupBind === 'function') {
2307
2626
  keystore.clearPendingGroupBind(pendingKey);
@@ -2600,6 +2919,8 @@ export class AUNClient {
2600
2919
  * 发送轻量在线通知,不走离线存储、seq/pull 或 ack。
2601
2920
  */
2602
2921
  async notify(method, params, options = {}) {
2922
+ if (this._migrationPending)
2923
+ throw new ConnectionError('client is migrating');
2603
2924
  if (params !== undefined && params !== null && !isJsonObject(params)) {
2604
2925
  throw new ValidationError('notify params must be an object');
2605
2926
  }
@@ -2675,6 +2996,10 @@ export class AUNClient {
2675
2996
  const messageEvent = event === 'message.received' || event === 'message.recalled' || event === 'message.batch';
2676
2997
  const hadMessageConsumer = messageEvent && [...this._dispatcher._snapshotApplicationHandlers(['message.received', 'message.recalled', 'message.batch']).values()].some((handlers) => handlers.length > 0);
2677
2998
  const result = this._dispatcher.subscribe(event, handler);
2999
+ if (event === 'agent_md_changed') {
3000
+ this._getAgentMdChanges().schedule();
3001
+ void this._getAgentMdChanges().deliverEvents().catch((error) => this._clientLog.debug(String(error)));
3002
+ }
2678
3003
  if (messageEvent) {
2679
3004
  if (this._p2pPeerPullEnabled && !hadMessageConsumer)
2680
3005
  this._delivery.refreshPendingPeers();
@@ -2912,6 +3237,8 @@ export class AUNClient {
2912
3237
  // ── 事件自动解密管线 ──────────────────────────────────────
2913
3238
  /** 处理 transport 层推送的原始 P2P 消息 */
2914
3239
  async _onRawMessageReceived(data) {
3240
+ if (this._controlMessages().onPush(data))
3241
+ return;
2915
3242
  return this._delivery.onRawMessageReceived(data);
2916
3243
  }
2917
3244
  async _onRawMessageRecalled(data) {
@@ -2919,6 +3246,8 @@ export class AUNClient {
2919
3246
  }
2920
3247
  /** 处理群组消息推送:自动解密后 re-publish */
2921
3248
  async _onRawGroupMessageCreated(data) {
3249
+ if (this._controlMessages().onPush(data, true))
3250
+ return;
2922
3251
  return this._delivery.onRawGroupMessageCreated(data);
2923
3252
  }
2924
3253
  async _onRawGroupMessageRecalled(data) {
@@ -3006,6 +3335,8 @@ export class AUNClient {
3006
3335
  this._delivery.markPublishedSeq(ns, seq);
3007
3336
  }
3008
3337
  _publishAppEvent(event, payload, source = 'direct', deliveryContext) {
3338
+ if (messageClassOf(payload) === 'sdk_control')
3339
+ return this._controlMessages().consume(payload);
3009
3340
  if (event === 'message.received') {
3010
3341
  return this._handleOwnershipMessage(payload).then((handled) => {
3011
3342
  if (!handled)
@@ -3488,7 +3819,7 @@ export class AUNClient {
3488
3819
  throw new ValidationError('gateway url unavailable for e2ee cert fetch');
3489
3820
  }
3490
3821
  const peerGatewayUrl = AUNClient._resolvePeerGatewayUrl(gatewayUrl, aid);
3491
- const certPem = await _httpGetText(AUNClient._buildCertUrl(peerGatewayUrl, aid, certFingerprint), this._configModel.verifySsl, timeoutMs);
3822
+ const certPem = await this._auth._fetchRiskText(AUNClient._buildCertUrl(peerGatewayUrl, aid, certFingerprint));
3492
3823
  if (auth !== this._auth || pkiEpoch !== auth._strictPkiEpoch)
3493
3824
  throw new ValidationError('peer certificate fetch context changed');
3494
3825
  const validated = await this._validateAndCachePeerCert({
@@ -3929,6 +4260,9 @@ export class AUNClient {
3929
4260
  // ── 内部:连接 ────────────────────────────────────────────
3930
4261
  /** 执行一次连接流程 */
3931
4262
  async _connectOnce(params, allowReauth, reconnectOwner, connectionOwner) {
4263
+ await this._restoreRiskState();
4264
+ this._assertRiskReconnect();
4265
+ this._riskNegotiated = false;
3932
4266
  const tStart = Date.now();
3933
4267
  connectionOwner ??= this._connectAbort ?? undefined;
3934
4268
  this._lifecycle.assertConnectionAttemptOwner(connectionOwner);
@@ -3938,8 +4272,15 @@ export class AUNClient {
3938
4272
  this._captureCapabilitiesFromConnect(params);
3939
4273
  this._connectDeliveryMode = { ...(params.delivery_mode ?? this._connectDeliveryMode) };
3940
4274
  const extraInfo = (params.extra_info && typeof params.extra_info === 'object' && !Array.isArray(params.extra_info))
3941
- ? params.extra_info
3942
- : undefined;
4275
+ ? { ...params.extra_info }
4276
+ : {};
4277
+ if (typeof this._tokenStore.updateInstanceState === 'function') {
4278
+ extraInfo._capabilities = {
4279
+ ...(isJsonObject(extraInfo._capabilities) ? extraInfo._capabilities : {}),
4280
+ message_classes_v1: { p2p: ['normal', 'control', 'sdk_control'], group: ['normal', 'control', 'sdk_control'] },
4281
+ sdk_control_protocols: { 'aun.agent_md.changed': [1] },
4282
+ };
4283
+ }
3943
4284
  const prevState = this._state;
3944
4285
  this._auth.setInstanceContext({ deviceId: this._deviceId, slotId: this._slotId });
3945
4286
  this._state = 'connecting';
@@ -3953,10 +4294,16 @@ export class AUNClient {
3953
4294
  const challenge = await this._transport.connect(gatewayUrl);
3954
4295
  this._assertReconnectOwner(reconnectOwner);
3955
4296
  this._lifecycle.assertConnectionAttemptOwner(connectionOwner);
4297
+ const authTransport = reconnectOwner ? {
4298
+ call: (...args) => {
4299
+ this._assertReconnectOwner(reconnectOwner);
4300
+ return this._transport.call(...args);
4301
+ },
4302
+ } : this._transport;
3956
4303
  this._clientLog.debug(`WebSocket connection established: gateway=${gatewayUrl}`);
3957
4304
  this._state = 'authenticating';
3958
4305
  if (allowReauth) {
3959
- const authContext = await this._auth.connectSession(this._transport, challenge, gatewayUrl, {
4306
+ const authContext = await this._auth.connectSession(authTransport, challenge, gatewayUrl, {
3960
4307
  accessToken: String(params.access_token ?? ''),
3961
4308
  deviceId: this._deviceId,
3962
4309
  slotId: this._slotId,
@@ -3982,10 +4329,14 @@ export class AUNClient {
3982
4329
  if (isJsonObject(auth.hello) && 'heartbeat_interval' in auth.hello) {
3983
4330
  this._applyServerHeartbeatInterval(auth.hello.heartbeat_interval, 'auth');
3984
4331
  }
4332
+ this._gatewayConnectionId = isJsonObject(auth.hello) && isJsonObject(auth.hello.connection) ? String(auth.hello.connection.id ?? '') : '';
4333
+ this._negotiatedCapabilities = isJsonObject(auth.hello) && isJsonObject(auth.hello.negotiated_capabilities) ? auth.hello.negotiated_capabilities : {};
4334
+ if (isJsonObject(auth.hello))
4335
+ await this._applyRiskHello(auth.hello);
3985
4336
  }
3986
4337
  }
3987
4338
  else {
3988
- const hello = await this._auth.initializeWithToken(this._transport, challenge, String(params.access_token), {
4339
+ const hello = await this._auth.initializeWithToken(authTransport, challenge, String(params.access_token), {
3989
4340
  deviceId: this._deviceId,
3990
4341
  slotId: this._slotId,
3991
4342
  deliveryMode: this._connectDeliveryMode,
@@ -3999,6 +4350,10 @@ export class AUNClient {
3999
4350
  if (isJsonObject(hello) && 'heartbeat_interval' in hello) {
4000
4351
  this._applyServerHeartbeatInterval(hello.heartbeat_interval, 'auth');
4001
4352
  }
4353
+ this._gatewayConnectionId = isJsonObject(hello) && isJsonObject(hello.connection) ? String(hello.connection.id ?? '') : '';
4354
+ this._negotiatedCapabilities = isJsonObject(hello) && isJsonObject(hello.negotiated_capabilities) ? hello.negotiated_capabilities : {};
4355
+ if (isJsonObject(hello))
4356
+ await this._applyRiskHello(hello);
4002
4357
  }
4003
4358
  this._assertReconnectOwner(reconnectOwner);
4004
4359
  this._lifecycle.assertConnectionAttemptOwner(connectionOwner);
@@ -4006,6 +4361,7 @@ export class AUNClient {
4006
4361
  throw new ConnectionError('websocket disconnected during authentication');
4007
4362
  }
4008
4363
  this._serverKicked = false;
4364
+ this._migrationPending = false;
4009
4365
  this._lastDisconnectInfo = null;
4010
4366
  this._state = 'ready';
4011
4367
  this._connectedAt = Date.now();
@@ -4061,6 +4417,11 @@ export class AUNClient {
4061
4417
  else {
4062
4418
  this._clientLog.debug('V2 session init deferred for short connection');
4063
4419
  }
4420
+ this._controlMessages().close();
4421
+ this._safeAsync(this._controlMessages().recover());
4422
+ this._agentMdChanges?.close();
4423
+ this._agentMdChanges = new AgentMdChangeCoordinator(this);
4424
+ this._agentMdChanges.schedule();
4064
4425
  // connect/reconnect 成功后自动触发一次 P2P message.v2.pull,补齐离线期间积压
4065
4426
  // 群消息按惰性触发,不在此处主动 pull
4066
4427
  if (backgroundSyncEnabled) {
@@ -4298,6 +4659,8 @@ export class AUNClient {
4298
4659
  this._clientLog.warn(`V2 decrypt: invalid envelope_json for msg seq=${String(msg.seq)}`);
4299
4660
  return null;
4300
4661
  }
4662
+ if (messageClassOf({ ...msg, envelope }) === 'sdk_control' && source !== 'control_pull')
4663
+ return null;
4301
4664
  const e2eeMeta = this._v2E2eeMeta(envelope);
4302
4665
  if (observeAgentMd) {
4303
4666
  if (deferredEffects)
@@ -4435,8 +4798,10 @@ export class AUNClient {
4435
4798
  return null;
4436
4799
  }
4437
4800
  let plaintext;
4801
+ let payloadBytes;
4438
4802
  try {
4439
4803
  plaintext = decryptMessage(envelope, this._aid ?? '', this._deviceId, ikPriv, spkPriv, senderPubDer);
4804
+ payloadBytes = decryptedMessagePayloadBytes(plaintext);
4440
4805
  if (plaintext === null)
4441
4806
  throw new Error('recipient_not_found_or_envelope_verification_failed');
4442
4807
  }
@@ -4488,6 +4853,11 @@ export class AUNClient {
4488
4853
  encrypted: true,
4489
4854
  e2ee,
4490
4855
  };
4856
+ if (!this._v2E2EE.isEncryptedEnvelopePayload(msg.payload))
4857
+ result._semantic_evidence = {
4858
+ envelope_bytes_base64: Buffer.from(envelopeJson, 'utf8').toString('base64'), payload_bytes_base64: Buffer.from(payloadBytes).toString('base64'),
4859
+ received_by: this._aid ?? '', received_at: Math.trunc(Date.now() / 1000), source,
4860
+ };
4491
4861
  if (typeof plaintext.type === 'string' && plaintext.type.startsWith('aun.ownership.'))
4492
4862
  result.from_aid = fromAid;
4493
4863
  if (senderDeviceId)
@@ -4684,6 +5054,8 @@ export class AUNClient {
4684
5054
  return this._groupState.autoConfirmPendingProposals();
4685
5055
  }
4686
5056
  async _onV2PushNotification(data, expectedVersion, inlineGeneration) {
5057
+ if (this._controlMessages().onPush(data))
5058
+ return;
4687
5059
  return this._delivery.onV2PushNotification(data, expectedVersion, inlineGeneration);
4688
5060
  }
4689
5061
  async _onV2StateProposed(data) {
@@ -4696,6 +5068,8 @@ export class AUNClient {
4696
5068
  return this._groupState.onV2StateConfirmed(data);
4697
5069
  }
4698
5070
  async _onRawGroupV2MessageCreated(data, expectedVersion, inlineGeneration) {
5071
+ if (this._controlMessages().onPush(data, true))
5072
+ return;
4699
5073
  return this._delivery.onRawGroupV2MessageCreated(data, expectedVersion, inlineGeneration);
4700
5074
  }
4701
5075
  /** Push 通知带 payload 时的就地解密(复用 _decryptV2Message) */
@@ -4719,7 +5093,7 @@ export class AUNClient {
4719
5093
  }
4720
5094
  }
4721
5095
  /** 发现并缓存全部 Gateway 候选,供首连和自动重连故障转移使用。 */
4722
- async _resolveGatewayCandidatesForAid(aid) {
5096
+ async _resolveGatewayCandidatesForAid(aid, fresh = false, reconnectOwner) {
4723
5097
  const resolvedAid = String(aid ?? this._aid ?? '').trim();
4724
5098
  if (!resolvedAid) {
4725
5099
  throw new StateError('gateway discovery requires a loaded AID');
@@ -4732,14 +5106,15 @@ export class AUNClient {
4732
5106
  const tokenStore = this._tokenStore;
4733
5107
  const discoveryIsCurrent = () => (this._discovery === discovery
4734
5108
  && this._tokenStore === tokenStore
4735
- && !this._closing);
4736
- if (sameIssuer && this._gatewayCandidates.length > 0)
5109
+ && !this._closing
5110
+ && (!reconnectOwner || this._ownsReconnect(reconnectOwner)));
5111
+ if (!fresh && sameIssuer && this._gatewayCandidates.length > 0)
4737
5112
  return [...this._gatewayCandidates];
4738
- if (sameIssuer && this._gatewayUrl) {
5113
+ if (!fresh && sameIssuer && this._gatewayUrl) {
4739
5114
  this._gatewayCandidates = [this._gatewayUrl];
4740
5115
  return [...this._gatewayCandidates];
4741
5116
  }
4742
- if (!sameIssuer) {
5117
+ if (!fresh && !sameIssuer) {
4743
5118
  const cached = this._peerGatewayCache.get(issuerDomain);
4744
5119
  if (cached)
4745
5120
  return [...cached];
@@ -4754,7 +5129,7 @@ export class AUNClient {
4754
5129
  : [];
4755
5130
  const cachedGateway = cachedList[0]
4756
5131
  ?? (isJsonObject(metadata) ? String(metadata.gateway_url ?? '').trim() : '');
4757
- if (cachedGateway) {
5132
+ if (!fresh && cachedGateway) {
4758
5133
  const gatewayUrls = [...new Set([cachedGateway, ...cachedList])];
4759
5134
  if (sameIssuer) {
4760
5135
  this._gatewayCandidates = gatewayUrls;
@@ -5135,21 +5510,182 @@ export class AUNClient {
5135
5510
  // ── 内部:断线重连 ────────────────────────────────────────
5136
5511
  /** 不重连 close code 集合:认证失败/权限错误/被踢等,重连无意义 */
5137
5512
  static _NO_RECONNECT_CODES = new Set([4001, 4003, 4008, 4009, 4010, 4011, 4013, 4014, 4015]);
5513
+ _onGatewayMigrate(input) {
5514
+ const data = input;
5515
+ if (!isJsonObject(data) || this._closing || this._disconnecting || this._connectAbort || this._reconnectActive
5516
+ || this._state !== 'ready' || !this._sessionParams || !this._gatewayConnectionId)
5517
+ return;
5518
+ const aid = this._aid ?? '';
5519
+ const issuer = aid.slice(aid.indexOf('.') + 1).toLowerCase().replace(/\.$/, '');
5520
+ const now = Date.now() / 1000;
5521
+ if (typeof data.migration_id !== 'string' || !data.migration_id.trim()
5522
+ || typeof data.revision !== 'number' || !Number.isSafeInteger(data.revision) || data.revision <= 0
5523
+ || typeof data.issuer !== 'string' || data.issuer.trim().toLowerCase().replace(/\.$/, '') !== issuer
5524
+ || data.aid !== aid || data.device_id !== this._deviceId || data.slot_id !== this._slotId
5525
+ || data.connection_id !== this._gatewayConnectionId || typeof data.gateway_url !== 'string'
5526
+ || typeof data.expires_at !== 'number' || !Number.isFinite(data.expires_at)
5527
+ || data.expires_at <= now || data.expires_at > now + 300)
5528
+ return;
5529
+ let target;
5530
+ try {
5531
+ target = new URL(data.gateway_url);
5532
+ }
5533
+ catch {
5534
+ return;
5535
+ }
5536
+ const hostname = target.hostname.toLowerCase();
5537
+ if (target.protocol !== 'wss:' || target.username || target.password || target.hash
5538
+ || /^\w+:\/\/[^/]*@/.test(data.gateway_url) || target.pathname !== '/aun'
5539
+ || !(hostname === issuer || hostname.endsWith(`.${issuer}`))
5540
+ || !hostname.split('.').every((label) => /^[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?$/.test(label)))
5541
+ return;
5542
+ const identity = JSON.stringify([this._aid, this._deviceId, this._slotId]);
5543
+ const previous = this._migrationRevision.get(identity);
5544
+ if (previous && (data.revision <= previous.revision || data.migration_id === previous.migrationId))
5545
+ return;
5546
+ this._migrationRevision.set(identity, { revision: data.revision, migrationId: data.migration_id });
5547
+ this._migrationPending = true;
5548
+ this._reconnectActive = true;
5549
+ const owner = new AbortController();
5550
+ this._reconnectAbort = owner;
5551
+ this._state = 'standby';
5552
+ this._stopBackgroundTasks();
5553
+ this._dispatcher.enqueue('gateway.migrate', data);
5554
+ const task = Promise.resolve().then(() => this._runGatewayMigration(data, owner, identity));
5555
+ this._reconnectTask = task;
5556
+ this._safeAsync(task);
5557
+ }
5558
+ async _runGatewayMigration(data, owner, identity) {
5559
+ const expiresAt = Number(data.expires_at) * 1000;
5560
+ const current = () => this._ownsReconnect(owner) && !this._disconnecting && !this._connectAbort
5561
+ && identity === JSON.stringify([this._aid, this._deviceId, this._slotId]);
5562
+ const wait = (operation) => this._lifecycle.waitForAttempt(operation, undefined, owner);
5563
+ const retry = this._sessionOptions.retry;
5564
+ const maxAttempts = Number(retry.max_attempts ?? 0);
5565
+ let delay = clampReconnectDelayMs(Number(retry.initial_delay ?? 1) * 1000, RECONNECT_MIN_BASE_DELAY_MS);
5566
+ const maxDelay = clampReconnectDelayMs(Number(retry.max_delay ?? 64) * 1000, RECONNECT_MAX_BASE_DELAY_MS);
5567
+ const expiryTimer = setTimeout(() => {
5568
+ if (!current())
5569
+ return;
5570
+ owner.abort();
5571
+ this._discovery._cancelPending?.();
5572
+ this._auth._cancelPending?.();
5573
+ void this._transport.close().catch(() => { });
5574
+ }, Math.max(0, expiresAt - Date.now()));
5575
+ let lastError = new ConnectionError('gateway migration expired');
5576
+ try {
5577
+ if (!current())
5578
+ return;
5579
+ await this._transport.waitForPending(Math.min(5000, Math.max(0, expiresAt - Date.now())), owner.signal);
5580
+ if (!current())
5581
+ return;
5582
+ await this._saveSeqTrackerState();
5583
+ await this._transport.close();
5584
+ if (!current())
5585
+ return;
5586
+ this._delivery.resetInlineAckState();
5587
+ for (let attempt = 1; current() && Date.now() < expiresAt; attempt++) {
5588
+ if (maxAttempts > 0 && attempt > maxAttempts)
5589
+ throw lastError;
5590
+ let candidates = attempt === 1 ? [String(data.gateway_url)] : [];
5591
+ if (attempt > 1) {
5592
+ try {
5593
+ candidates = await wait(this._resolveGatewayCandidatesForAid(String(data.aid), true, owner));
5594
+ }
5595
+ catch (error) {
5596
+ lastError = error;
5597
+ }
5598
+ }
5599
+ for (let index = 0; index < candidates.length; index++) {
5600
+ const gateway = candidates[index];
5601
+ if (!current() || Date.now() >= expiresAt)
5602
+ return;
5603
+ this._state = 'reconnecting';
5604
+ this._migrationPending = true;
5605
+ try {
5606
+ await wait(this._connectOnce({ ...this._sessionParams, gateway, gateways: candidates }, true, owner));
5607
+ if (!current())
5608
+ return;
5609
+ if (!this._transport.isConnected())
5610
+ throw new ConnectionError('websocket disconnected while migrating');
5611
+ this._sessionParams.gateway = gateway;
5612
+ this._sessionParams.gateways = candidates;
5613
+ return;
5614
+ }
5615
+ catch (error) {
5616
+ lastError = error;
5617
+ if (!current())
5618
+ return;
5619
+ this._migrationPending = true;
5620
+ await this._transport.close().catch(() => { });
5621
+ if (!current())
5622
+ return;
5623
+ if (attempt === 1 && index === 0) {
5624
+ try {
5625
+ const discovered = await wait(this._resolveGatewayCandidatesForAid(String(data.aid), true, owner));
5626
+ candidates = [...candidates, ...discovered.filter((item) => !candidates.includes(item))];
5627
+ }
5628
+ catch (discoveryError) {
5629
+ lastError = discoveryError;
5630
+ }
5631
+ }
5632
+ }
5633
+ }
5634
+ if (!AUNClient._shouldRetryReconnect(lastError))
5635
+ throw lastError;
5636
+ if (maxAttempts > 0 && attempt >= maxAttempts)
5637
+ throw lastError;
5638
+ await this._sleep(Math.min(delay, Math.max(0, expiresAt - Date.now())), owner.signal);
5639
+ if (!current())
5640
+ return;
5641
+ delay = Math.min(delay * 2, maxDelay);
5642
+ }
5643
+ }
5644
+ catch (error) {
5645
+ lastError = error;
5646
+ }
5647
+ finally {
5648
+ clearTimeout(expiryTimer);
5649
+ if (this._reconnectAbort === owner) {
5650
+ this._migrationPending = false;
5651
+ this._reconnectActive = false;
5652
+ this._reconnectAbort = null;
5653
+ this._reconnectTask = null;
5654
+ this._nextRetryAt = null;
5655
+ if ((this._state !== 'ready' || owner.signal.aborted) && !this._closing) {
5656
+ this._stopBackgroundTasks();
5657
+ this._state = 'standby';
5658
+ if (!owner.signal.aborted)
5659
+ this._dispatcher.enqueue('connection.error', { error: formatCaughtError(lastError) });
5660
+ }
5661
+ }
5662
+ }
5663
+ }
5138
5664
  /** 处理服务端主动断开通知 event/gateway.disconnect。
5139
5665
  *
5140
5666
  * 服务端可能附带结构化 detail 字段(如配额超限时含 aid/device_id/slot_id/quota_kind/evicted_by)。
5141
5667
  * 透传到应用层可订阅事件 'gateway.disconnect',方便业务定位被踢原因。
5142
5668
  */
5143
5669
  async _onGatewayDisconnect(data) {
5144
- const payload = (data && typeof data === 'object') ? data : {};
5670
+ const payload = (data && typeof data === 'object') ? structuredClone(data) : {};
5145
5671
  const code = payload.code;
5146
5672
  const reason = payload.reason ?? '';
5147
5673
  const detail = (payload.detail && typeof payload.detail === 'object' && !Array.isArray(payload.detail))
5148
5674
  ? payload.detail
5149
5675
  : {};
5676
+ const wrapper = payload.risk_state ?? detail.risk_state;
5677
+ delete payload.risk_state;
5678
+ delete detail.risk_state;
5679
+ if (isJsonObject(wrapper) && Array.isArray(wrapper.states))
5680
+ for (const state of wrapper.states)
5681
+ if (isJsonObject(state))
5682
+ await this._onRiskControl(state);
5683
+ const retryMs = Number(detail.retry_after_ms ?? payload.retry_after_ms ?? 0);
5684
+ if (Number(code) === 4029 && Number.isSafeInteger(retryMs) && retryMs > 0)
5685
+ this._riskScalarReconnectAt = (typeof performance !== 'undefined' ? performance.now() : Date.now()) + retryMs;
5150
5686
  this._clientLog.warn(`server initiated disconnect: code=${code}, reason=${reason}, detail=${JSON.stringify(detail)}`);
5151
5687
  const reasonText = String(reason ?? '').trim().toLowerCase();
5152
- const retryable = Number(code) === 4012
5688
+ const retryable = Number(code) === 4012 || Number(code) === 4029
5153
5689
  || reasonText.includes('long_connection_already_exists')
5154
5690
  || reasonText.includes('long connection already exists');
5155
5691
  this._serverKicked = !retryable;
@@ -5362,7 +5898,7 @@ export class AUNClient {
5362
5898
  break;
5363
5899
  }
5364
5900
  this._retryAttempt = attempt;
5365
- this._nextRetryAt = Date.now() + reconnectSleepDelayMs(delay, maxBaseDelay);
5901
+ this._nextRetryAt = Date.now() + Math.max(reconnectSleepDelayMs(delay, maxBaseDelay), this._riskReconnectDelay());
5366
5902
  this._state = 'retry_backoff';
5367
5903
  if (!reconnectAbort || !await this._publishReconnectEvent(reconnectAbort, 'state_change', {
5368
5904
  state: this._publicState(this._state),