@agentunion/fastaun 0.5.30 → 0.5.32

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 (105) hide show
  1. package/CHANGELOG.md +60 -0
  2. package/_packed_docs/CHANGELOG.md +60 -0
  3. package/_packed_docs/sdk/0.5.31-SDK/345/272/224/347/224/250/351/233/206/346/210/220/346/214/207/345/215/227.md +162 -0
  4. package/_packed_docs/sdk/0.5.31-SDK/345/274/200/345/217/221/344/270/216/347/247/273/346/244/215/346/214/207/345/215/227.md +167 -0
  5. package/_packed_docs/sdk/0.5.32-SDK/345/272/224/347/224/250/351/233/206/346/210/220/346/214/207/345/215/227.md +350 -0
  6. package/_packed_docs/sdk/0.5.32-SDK/345/274/200/345/217/221/344/270/216/347/247/273/346/244/215/346/214/207/345/215/227.md +319 -0
  7. package/_packed_docs/sdk/AUN_DOCS_GUIDE.md +12 -5
  8. package/_packed_docs/sdk/AUN/344/272/213/344/273/266/345/217/221/345/270/203/350/256/242/351/230/205/345/272/224/347/224/250/351/233/206/346/210/220/346/214/207/345/215/227.md +459 -0
  9. package/_packed_docs/sdk/AUN/344/272/213/344/273/266/345/217/221/345/270/203/350/256/242/351/230/205/345/274/200/345/217/221/347/247/273/346/244/215/346/214/207/345/215/227.md +554 -0
  10. package/_packed_docs/sdk/INDEX.md +27 -5
  11. package/_packed_docs/sdk//345/255/220AID/344/277/256/346/224/271agent.md/351/200/232/347/237/245Owner/345/234/272/346/231/257/346/214/207/345/215/227.md +234 -0
  12. package/dist/agent-md-schema.js +7 -6
  13. package/dist/agent-md-schema.js.map +1 -1
  14. package/dist/agent-md.d.ts +2 -0
  15. package/dist/agent-md.js +20 -6
  16. package/dist/agent-md.js.map +1 -1
  17. package/dist/aid-store.d.ts +1 -0
  18. package/dist/aid-store.js +1 -0
  19. package/dist/aid-store.js.map +1 -1
  20. package/dist/aid.js +1 -1
  21. package/dist/aid.js.map +1 -1
  22. package/dist/auth.js +1 -1
  23. package/dist/auth.js.map +1 -1
  24. package/dist/client/control-messages.d.ts +22 -7
  25. package/dist/client/control-messages.js +220 -185
  26. package/dist/client/control-messages.js.map +1 -1
  27. package/dist/client/delivery.d.ts +3 -0
  28. package/dist/client/delivery.js +67 -3
  29. package/dist/client/delivery.js.map +1 -1
  30. package/dist/client/group-state.d.ts +3 -0
  31. package/dist/client/group-state.js +48 -14
  32. package/dist/client/group-state.js.map +1 -1
  33. package/dist/client/lifecycle.js +12 -3
  34. package/dist/client/lifecycle.js.map +1 -1
  35. package/dist/client/rpc-pipeline.d.ts +9 -3
  36. package/dist/client/rpc-pipeline.js +61 -26
  37. package/dist/client/rpc-pipeline.js.map +1 -1
  38. package/dist/client/v2-e2ee.d.ts +13 -1
  39. package/dist/client/v2-e2ee.js +32 -19
  40. package/dist/client/v2-e2ee.js.map +1 -1
  41. package/dist/client.d.ts +18 -1
  42. package/dist/client.js +396 -140
  43. package/dist/client.js.map +1 -1
  44. package/dist/discovery.d.ts +1 -0
  45. package/dist/discovery.js +3 -1
  46. package/dist/discovery.js.map +1 -1
  47. package/dist/group-fs.js +1 -1
  48. package/dist/group-fs.js.map +1 -1
  49. package/dist/index.d.ts +2 -0
  50. package/dist/index.js +1 -0
  51. package/dist/index.js.map +1 -1
  52. package/dist/ownership-v21.d.ts +2 -2
  53. package/dist/ownership-v21.js +18 -21
  54. package/dist/ownership-v21.js.map +1 -1
  55. package/dist/ownership-workflow.js +6 -4
  56. package/dist/ownership-workflow.js.map +1 -1
  57. package/dist/pubsub/binding.d.ts +54 -0
  58. package/dist/pubsub/binding.js +343 -0
  59. package/dist/pubsub/binding.js.map +1 -0
  60. package/dist/pubsub/channel.d.ts +45 -0
  61. package/dist/pubsub/channel.js +137 -0
  62. package/dist/pubsub/channel.js.map +1 -0
  63. package/dist/pubsub/client.d.ts +93 -0
  64. package/dist/pubsub/client.js +318 -0
  65. package/dist/pubsub/client.js.map +1 -0
  66. package/dist/pubsub/credentials.d.ts +51 -0
  67. package/dist/pubsub/credentials.js +360 -0
  68. package/dist/pubsub/credentials.js.map +1 -0
  69. package/dist/pubsub/errors.d.ts +5 -0
  70. package/dist/pubsub/errors.js +6 -0
  71. package/dist/pubsub/errors.js.map +1 -0
  72. package/dist/pubsub/index.d.ts +48 -0
  73. package/dist/pubsub/index.js +152 -0
  74. package/dist/pubsub/index.js.map +1 -0
  75. package/dist/pubsub/scheduler.d.ts +58 -0
  76. package/dist/pubsub/scheduler.js +249 -0
  77. package/dist/pubsub/scheduler.js.map +1 -0
  78. package/dist/pubsub/service.d.ts +100 -0
  79. package/dist/pubsub/service.js +650 -0
  80. package/dist/pubsub/service.js.map +1 -0
  81. package/dist/pubsub/types.d.ts +119 -0
  82. package/dist/pubsub/types.js +2 -0
  83. package/dist/pubsub/types.js.map +1 -0
  84. package/dist/register-flow.js +1 -1
  85. package/dist/register-flow.js.map +1 -1
  86. package/dist/storage/lowlevel.js +49 -8
  87. package/dist/storage/lowlevel.js.map +1 -1
  88. package/dist/storage/read-cache.d.ts +7 -3
  89. package/dist/storage/read-cache.js +137 -18
  90. package/dist/storage/read-cache.js.map +1 -1
  91. package/dist/storage/vfs.d.ts +1 -0
  92. package/dist/storage/vfs.js +12 -4
  93. package/dist/storage/vfs.js.map +1 -1
  94. package/dist/tools/cross-sdk-agent.d.ts +1 -1
  95. package/dist/tools/cross-sdk-agent.js +4 -2
  96. package/dist/tools/cross-sdk-agent.js.map +1 -1
  97. package/dist/tools/pubsub-actor.d.ts +27 -0
  98. package/dist/tools/pubsub-actor.js +260 -0
  99. package/dist/tools/pubsub-actor.js.map +1 -0
  100. package/dist/transport.d.ts +26 -2
  101. package/dist/transport.js +182 -17
  102. package/dist/transport.js.map +1 -1
  103. package/dist/version.d.ts +1 -1
  104. package/dist/version.js +1 -1
  105. package/package.json +1 -1
package/dist/client.js CHANGED
@@ -17,6 +17,7 @@ import { URL } from 'node:url';
17
17
  import { configFromMap, getDeviceId, normalizeSlotId } from './config.js';
18
18
  import { CryptoProvider } from './crypto.js';
19
19
  import { GatewayDiscovery } from './discovery.js';
20
+ import { VERSION } from './version.js';
20
21
  import { DnsResilientNet } from './net.js';
21
22
  import { AUNError, AuthError, ConnectionError, E2EEError, NotFoundError, PermissionError, StateError, TimeoutError, ValidationError, } from './errors.js';
22
23
  import { EventDispatcher } from './events.js';
@@ -26,7 +27,7 @@ import { normalizeGroupId } from './group-id.js';
26
27
  import { validateAIDFormat, validateGroupIDFormat } from './validators.js';
27
28
  import { RPCTransport } from './transport.js';
28
29
  import { AuthFlow } from './auth.js';
29
- import { AgentMdManager, buildDefaultAgentMd } from './agent-md.js';
30
+ import { AgentMdManager, buildDefaultAgentMd, updateOwnershipAgentMd } from './agent-md.js';
30
31
  import { AgentMdChangeCoordinator } from './agent-md-changes.js';
31
32
  import { GroupIndexMetaCache } from './group-index.js';
32
33
  import { SeqTracker } from './seq-tracker.js';
@@ -41,6 +42,7 @@ import { RiskController } from './risk-control.js';
41
42
  import { ControlMessageCoordinator, messageClassOf } from './client/control-messages.js';
42
43
  import { CollabClient } from './collab/client.js';
43
44
  import { DebugFacade, CredentialFacade, OrgFacade, GroupFacade, MailFacade, MessageFacade, StreamFacade } from './facades.js';
45
+ import { EventService } from './pubsub/index.js';
44
46
  import { StorageVFS } from './storage/vfs.js';
45
47
  import { V2E2EECoordinator } from './client/v2-e2ee.js';
46
48
  import { GroupStateCoordinator } from './client/group-state.js';
@@ -50,8 +52,8 @@ import { decryptedOwnershipJsonText, decryptedMessagePayloadBytes } from './v2/e
50
52
  import { isJsonObject, ConnectionState, STATE_TO_PUBLIC, } from './types.js';
51
53
  import { AID } from './aid.js';
52
54
  import { certFingerprint, certMatchesFingerprint, normalizeFingerprintHex, publicKeyMatchesFingerprint } from './cert-utils.js';
53
- import { validateAgentMdCertificate, validateAgentMdDocument } from './agent-md-schema.js';
54
- import { OwnershipFiles, OwnershipStorageAdapter, ownershipAid, ownershipFailure, ownershipSha256, parseOwnershipFile } from './ownership-v21.js';
55
+ import { validateAgentMdCertificate, validateAgentMdDocument, validateAgentMdReadDocument } from './agent-md-schema.js';
56
+ import { OwnershipFiles, OwnershipStorageAdapter, ownershipAid, ownershipFailure, ownershipSha256, parseOwnershipFile, signOwnershipFile } from './ownership-v21.js';
55
57
  import { OwnershipWorkflow, OWNERSHIP_SDK_MESSAGE_TYPE, parseOwnershipMessage } from './ownership-workflow.js';
56
58
  import { canonicalJson, createCredential, createMemberCredential, signMembershipBinding, digest, validateMembershipBinding, verifyMembershipBinding, verifyCredentialSignature, createRepresentationProof, verifyServiceEvent, buildRequestBinding, validateRequestBinding, CredentialRenewal } from './org-protocol.js';
57
59
  import { SemanticProtocolError } from './semantic-protocol.js';
@@ -491,7 +493,8 @@ export class AUNClient {
491
493
  _ownershipWorkflow = null;
492
494
  _ownershipConsensusCache = new Map();
493
495
  _getOwnershipStorage() {
494
- return this._ownershipStorage ??= new OwnershipStorageAdapter({ currentAid: () => this._aid, call: (method, params) => this.call(method, params), httpPut: (url, data, headers) => this.storage.lowlevel.httpPut(url, data, headers) });
496
+ return this._ownershipStorage ??= new OwnershipStorageAdapter({ currentAid: () => this._aid, call: (method, params, guard) => guard ? this._rpcPipeline.call(method, params, guard) : this.call(method, params), httpPut: async (url, data, headers, guard) => { const response = await this._riskFetch(url, { method: 'PUT', body: new Uint8Array(data).buffer, headers }, (target, init) => { guard?.(); return fetch(target, init); }); if (!response.ok)
497
+ throw ownershipFailure('unavailable', `Storage owner.md HTTP PUT failed: ${response.status}`, true); } });
495
498
  }
496
499
  _getOwnershipFiles() {
497
500
  return this._ownershipFiles ??= this._createOwnershipFiles((subAid, stage) => this._agentMdManager.readOwnershipFresh(subAid, stage));
@@ -589,6 +592,7 @@ export class AUNClient {
589
592
  _credentialRenewalTimer = null;
590
593
  _credentialRenewalAbort = null;
591
594
  _credentialRenewalScan = Promise.resolve();
595
+ _credentialRenewalBackground = false;
592
596
  _nextCredentialRevocationSyncAt = 0;
593
597
  _getCredentialRenewal() {
594
598
  return this._credentialRenewal ??= new CredentialRenewal({
@@ -623,6 +627,8 @@ export class AUNClient {
623
627
  certificate: (aid, fingerprint, signedAt) => signedAt === undefined
624
628
  ? this._fetchServiceEventCertificate(aid, fingerprint) : this._fetchPeerCert(aid, fingerprint, undefined, signedAt),
625
629
  call: (method, params, onSent) => {
630
+ if (this._credentialRenewalBackground && params._rpc_background === undefined && params._rpc_background_kind === undefined)
631
+ params = { ...params, _rpc_background: true };
626
632
  if (!onSent)
627
633
  return this._rpcPipeline.call(method, params);
628
634
  const version = this._serviceEventIdentityVersion, aid = this._aid;
@@ -632,7 +638,7 @@ export class AUNClient {
632
638
  await onSent();
633
639
  });
634
640
  },
635
- send: (to, payload) => this.call('message.send', { to, encrypt: true, payload }),
641
+ send: (to, payload) => this.call('message.send', { to, encrypt: true, payload, ...(this._credentialRenewalBackground ? { _rpc_background: true } : {}) }),
636
642
  });
637
643
  }
638
644
  _assertCredentialNotRevoked(credential, proofChain) {
@@ -708,10 +714,18 @@ export class AUNClient {
708
714
  const task = this._credentialRenewalScan.then(async () => {
709
715
  if (!current())
710
716
  return;
717
+ if (this._transport._rateLimitRemainingMs() > 0)
718
+ return;
711
719
  const syncRevocations = this._sessionOptions.connection_kind !== 'short' && Date.now() >= this._nextCredentialRevocationSyncAt;
712
720
  if (syncRevocations)
713
721
  this._nextCredentialRevocationSyncAt = Date.now() + 3_600_000;
714
- await this._resumeCredentialRenewalAfterConnect(guard, syncRevocations);
722
+ this._credentialRenewalBackground = true;
723
+ try {
724
+ await this._resumeCredentialRenewalAfterConnect(guard, syncRevocations);
725
+ }
726
+ finally {
727
+ this._credentialRenewalBackground = false;
728
+ }
715
729
  });
716
730
  this._credentialRenewalScan = task.catch(() => { });
717
731
  await this._safeAsync(task);
@@ -906,6 +920,8 @@ export class AUNClient {
906
920
  _agentMdManager;
907
921
  _aidStore;
908
922
  _groupIdentityOperations = new Map();
923
+ _entityOwnershipConnection = 0;
924
+ _entityOwnershipChecks = new Map();
909
925
  _groupIndexMetaCache = new GroupIndexMetaCache();
910
926
  /** 消息序列号跟踪器(群消息 + P2P 空洞检测) */
911
927
  _p2pPeerPullEnabled = true;
@@ -990,6 +1006,8 @@ export class AUNClient {
990
1006
  _reconnectTask = null;
991
1007
  _reconnectEventDispatchDepth = 0;
992
1008
  _gatewayConnectionId = '';
1009
+ _gatewayRuntimeIdentity = null;
1010
+ _gatewayReleaseRevision = 0;
993
1011
  _negotiatedCapabilities = {};
994
1012
  _riskNegotiated = false;
995
1013
  _riskRestoredAid = '';
@@ -1029,6 +1047,7 @@ export class AUNClient {
1029
1047
  _storage;
1030
1048
  _collab;
1031
1049
  _message;
1050
+ _eventServiceFacade;
1032
1051
  _debug;
1033
1052
  _mail;
1034
1053
  _group;
@@ -1269,6 +1288,11 @@ export class AUNClient {
1269
1288
  });
1270
1289
  return this._debug;
1271
1290
  }
1291
+ get EventService() {
1292
+ if (!this._eventServiceFacade)
1293
+ this._eventServiceFacade = new EventService(this);
1294
+ return this._eventServiceFacade;
1295
+ }
1272
1296
  /** Message facade 入口 */
1273
1297
  get message() {
1274
1298
  if (!this._message)
@@ -1324,8 +1348,8 @@ export class AUNClient {
1324
1348
  }
1325
1349
  async _localOwnershipOperation(sub, next) {
1326
1350
  const sources = [this, sub, ...(next ? [next] : [])];
1327
- if (sources.some((source, index) => !(source instanceof AUNClient) || !source._currentAid?.isPrivateKeyValid() || !source._currentAid.isCertValid() || !source._currentAid.privateKeyPem || source._currentAid.aid !== source._aid || (index === 1 ? source.getType() !== 'agent' : !['human', 'agent'].includes(source.getType()))) || new Set(sources.map((source) => source._aid)).size !== sources.length)
1328
- throw ownershipFailure('forbidden', 'local ownership requires a distinct agent subaid and human or agent owners with private identities');
1351
+ if (sources.some((source, index) => !(source instanceof AUNClient) || !source._currentAid?.isPrivateKeyValid() || !source._currentAid.isCertValid() || !source._currentAid.privateKeyPem || source._currentAid.aid !== source._aid || (index === 1 ? ['human', 'group', 'org'].includes(source.getType()) : !['human', 'agent'].includes(source.getType()))) || new Set(sources.map((source) => source._aid)).size !== sources.length)
1352
+ throw ownershipFailure('forbidden', 'local ownership requires a distinct non-human subaid outside group/org lifecycles and human or agent owners with private identities');
1329
1353
  const identities = sources.map((source) => [source._aid, source._currentAid, source._serviceEventIdentityVersion]);
1330
1354
  const guard = () => { if (sources.some((source, index) => source._aid !== identities[index][0] || source._currentAid !== identities[index][1] || source._serviceEventIdentityVersion !== identities[index][2]))
1331
1355
  throw ownershipFailure('conflict', 'local ownership source identity changed'); };
@@ -1337,8 +1361,8 @@ export class AUNClient {
1337
1361
  throw ownershipFailure('forbidden', 'local ownership requires a published agent document');
1338
1362
  const verified = await source._getOwnershipFiles().verifyFile(document.content, 'agent', source._aid);
1339
1363
  guard();
1340
- if ((index === 1 ? verified.frontmatter.type !== 'agent' : typeof verified.frontmatter.type !== 'string' || !['human', 'agent'].includes(verified.frontmatter.type)))
1341
- throw ownershipFailure('forbidden', 'local ownership requires an agent subaid and human or agent owners');
1364
+ if (typeof verified.frontmatter.type !== 'string' || !verified.frontmatter.type.trim() || (index === 1 ? ['human', 'group', 'org'].includes(verified.frontmatter.type) : !['human', 'agent'].includes(verified.frontmatter.type)))
1365
+ throw ownershipFailure('forbidden', 'local ownership requires a non-human subaid outside group/org lifecycles and human or agent owners');
1342
1366
  }
1343
1367
  const slot = `ownership-${crypto.randomUUID()}`, clients = [];
1344
1368
  try {
@@ -1898,21 +1922,232 @@ export class AUNClient {
1898
1922
  * 发送 JSON-RPC 调用。
1899
1923
  * 自动处理内部方法限制、E2EE 加解密、客户端签名等。
1900
1924
  */
1901
- async call(method, params) {
1925
+ async call(method, params, options) {
1926
+ const assertCurrent = ['group.transfer_owner', 'org.lifecycle.transfer', 'group.dissolve', 'org.lifecycle.dissolve'].includes(method) ? this._entityOwnershipGuard() : undefined;
1927
+ const recoverOwnership = ['group.transfer_owner', 'org.lifecycle.transfer', 'group.dissolve', 'org.lifecycle.dissolve'].includes(method) ? await this._prepareEntityOwnershipLifecycle(method, params ?? {}) : undefined;
1928
+ await this._checkEntityOwnershipOnAccess(method, params ?? {});
1902
1929
  const readIdentity = ['message.read', 'message.read_state', 'group.read', 'group.read_state'].includes(method)
1903
1930
  ? this._ensureReadStateIdentity() : null;
1904
1931
  try {
1905
- const result = await this._rpcPipeline.call(method, params);
1932
+ const result = await (options === undefined ? this._rpcPipeline.call(method, params, assertCurrent) : this._rpcPipeline.call(method, params, assertCurrent, options));
1906
1933
  await this._safeAsync(this._getCredentialRenewal().observe(method, params ?? {}, result));
1907
1934
  if (readIdentity !== null && readIdentity === this._ensureReadStateIdentity())
1908
1935
  this._observeReadStateResult(method, result, params ?? {});
1909
1936
  return result;
1910
1937
  }
1911
1938
  catch (err) {
1939
+ if (recoverOwnership) {
1940
+ try {
1941
+ await recoverOwnership(err);
1942
+ }
1943
+ catch { }
1944
+ }
1912
1945
  await this._maybeHandleHalfOpenRpcFailure(err);
1913
1946
  throw err;
1914
1947
  }
1915
1948
  }
1949
+ _entityOwnershipGuard() {
1950
+ const owner = this._currentAid, identity = `${this._aid}:${this._serviceEventIdentityVersion}:${this._entityOwnershipConnection}`;
1951
+ return () => { if (`${this._aid}:${this._serviceEventIdentityVersion}:${this._entityOwnershipConnection}` !== identity || this._currentAid !== owner || this._closing || this._disconnecting)
1952
+ throw ownershipFailure('conflict', 'entity ownership connection changed'); };
1953
+ }
1954
+ async _prepareEntityOwnershipLifecycle(method, params) {
1955
+ const type = method.startsWith('group.') ? 'group' : 'org';
1956
+ const aid = String(params.group_aid ?? params.group_id ?? params.org_id ?? '').trim().toLowerCase();
1957
+ if (!aid)
1958
+ return;
1959
+ const owner = this._currentAid;
1960
+ const identity = `${this._aid}:${this._serviceEventIdentityVersion}:${this._entityOwnershipConnection}`;
1961
+ const assertCurrent = () => { if (`${this._aid}:${this._serviceEventIdentityVersion}:${this._entityOwnershipConnection}` !== identity || this._currentAid !== owner)
1962
+ throw ownershipFailure('conflict', 'entity lifecycle identity changed'); };
1963
+ const response = await this._entityLifecycle(aid, type);
1964
+ assertCurrent();
1965
+ const result = response;
1966
+ const entity = (isJsonObject(result.group) ? result.group : isJsonObject(result.organization) ? result.organization : result);
1967
+ if (entity.owner_aid !== this._aid || entity.status !== 'active')
1968
+ return;
1969
+ const target = String(entity.group_aid ?? entity.org_id ?? aid);
1970
+ if (!owner?.isPrivateKeyValid())
1971
+ throw ownershipFailure('unavailable', 'entity lifecycle requires its owner signing identity', true);
1972
+ const observed = await this._agentMdManager.readAgentMdFresh(target);
1973
+ const meta = observed.exists ? validateAgentMdReadDocument(observed.content, { expectedAid: target }).frontmatter : {};
1974
+ if (!meta.ownership)
1975
+ await this._completeEntityOwnership(target, type, undefined, assertCurrent);
1976
+ const storage = this._getOwnershipStorage(), original = await storage.readOwnerMd(owner.aid, target);
1977
+ if (!original.exists)
1978
+ throw ownershipFailure('conflict', 'entity lifecycle owner declaration is missing');
1979
+ const declaration = await this._getOwnershipFiles().verifyFile(original.content, 'owner', target, owner.aid);
1980
+ const transfer = method.endsWith('transfer') || method.endsWith('transfer_owner');
1981
+ const newOwner = String(params.target_member_aid ?? params.new_owner_aid ?? params.new_owner ?? params.aid ?? '').trim().toLowerCase();
1982
+ const action = transfer ? 'transfer_owner' : 'release';
1983
+ if (declaration.action === action && (!transfer || declaration.transfer?.new_owner_aid === newOwner && declaration.transfer.expires_at >= Date.now() / 1000))
1984
+ return;
1985
+ if (declaration.action !== 'declare' || !declaration.statement)
1986
+ throw ownershipFailure('conflict', 'entity lifecycle ownership action changed');
1987
+ const ownership = { statement: declaration.statement, action, ...(transfer ? { transfer: { new_owner_aid: ownershipAid(newOwner, 'new_owner_aid'), target_relation_version: declaration.statement.relation_version + 1, expires_at: Number(params.expires_at ?? Math.floor(Date.now() / 1000) + 3600) } } : {}) };
1988
+ const raw = signOwnershipFile(owner, `---\nowner_aid: ${owner.aid}\nsub_aid: ${target}\nownership: ${JSON.stringify(ownership)}\n---\n`, 'owner');
1989
+ await storage.writeOwnerMd(owner.aid, target, raw, original, assertCurrent);
1990
+ return async (error) => {
1991
+ if (!(error instanceof AUNError) || error instanceof TimeoutError || error instanceof ConnectionError || error.retryable || isJsonObject(error.data) && error.data.committed === true || ![-32602, 4000, 4030, 403].includes(error.code) && !['INVALID_ARGUMENT', 'ROLE_ACCESS_DENIED', 'MEMBER_NOT_FOUND', 'permission_denied', 'member_banned', 'member_blocked'].includes(error.stringCode))
1992
+ return;
1993
+ assertCurrent();
1994
+ const response = await this._entityLifecycle(target, type);
1995
+ const data = response, current = (isJsonObject(data.group) ? data.group : isJsonObject(data.organization) ? data.organization : data);
1996
+ if (current.owner_aid !== owner.aid || current.status !== 'active')
1997
+ return;
1998
+ const observed = await storage.readOwnerMd(owner.aid, target);
1999
+ if (!observed.exists || observed.content !== raw)
2000
+ return;
2001
+ assertCurrent();
2002
+ await storage.writeOwnerMd(owner.aid, target, original.content, observed, assertCurrent);
2003
+ };
2004
+ }
2005
+ async _checkEntityOwnershipOnAccess(method, params) {
2006
+ const type = method.startsWith('group.') ? 'group' : method.startsWith('org.') ? 'org' : '';
2007
+ const rawTarget = String(params.group_aid ?? params.group_id ?? params.org_id ?? '').trim().toLowerCase();
2008
+ const target = type === 'group' ? normalizeGroupId(rawTarget, { localIssuer: this._aid?.split('.').slice(1).join('.') }) : rawTarget;
2009
+ if (!type || !target || ['group.create', 'group.bind_group_aid', 'group.renew_group_aid', 'group.complete_transfer', 'group.transfer_owner', 'group.dissolve', 'org.lifecycle.create', 'org.lifecycle.transfer', 'org.lifecycle.transferAccept', 'org.lifecycle.dissolve'].includes(method))
2010
+ return;
2011
+ const identity = `${this._aid}:${this._serviceEventIdentityVersion}:${this._entityOwnershipConnection}`;
2012
+ const key = `${identity}:${target}`;
2013
+ let task = this._entityOwnershipChecks.get(key);
2014
+ if (!task) {
2015
+ task = (async () => {
2016
+ const store = this._aidStore;
2017
+ if (!store)
2018
+ return false;
2019
+ if (!store.load(target).ok) {
2020
+ let pending = null;
2021
+ const keystore = store._keystore;
2022
+ if (type === 'group')
2023
+ for (const id of new Set([target, String(params.group_id ?? target)])) {
2024
+ pending = keystore?.loadPendingGroupBind?.(`bind:${id}`) ?? keystore?.loadPendingGroupBind?.(id);
2025
+ if (pending?.private_key_pem && pending.public_key_der_b64)
2026
+ break;
2027
+ }
2028
+ if (!pending?.private_key_pem || !pending.public_key_der_b64)
2029
+ return false;
2030
+ }
2031
+ const result = await this._entityLifecycle(target, type);
2032
+ const data = result;
2033
+ const entity = (isJsonObject(data.group) ? data.group : isJsonObject(data.organization) ? data.organization : data);
2034
+ if (`${this._aid}:${this._serviceEventIdentityVersion}:${this._entityOwnershipConnection}` !== identity || entity.owner_aid !== this._aid || entity.status !== 'active')
2035
+ return false;
2036
+ const aid = String(entity.group_aid ?? entity.org_id ?? target).toLowerCase();
2037
+ if (aid !== target) {
2038
+ const existing = this._entityOwnershipChecks.get(`${identity}:${aid}`);
2039
+ if (existing && existing !== task)
2040
+ return await existing;
2041
+ this._entityOwnershipChecks.set(`${identity}:${aid}`, task);
2042
+ }
2043
+ const consensus = await this.checkOwnershipConsensus(aid, true);
2044
+ if (consensus.status === 'consensus' && consensus.owner_aid === this._aid)
2045
+ return true;
2046
+ await this._completeEntityOwnership(aid, type, undefined, () => {
2047
+ if (`${this._aid}:${this._serviceEventIdentityVersion}:${this._entityOwnershipConnection}` !== identity || this._closing || this._disconnecting)
2048
+ throw ownershipFailure('conflict', 'entity ownership connection changed');
2049
+ });
2050
+ return true;
2051
+ })().catch(() => false);
2052
+ this._entityOwnershipChecks.set(key, task);
2053
+ }
2054
+ await task;
2055
+ }
2056
+ async _completeEntityOwnership(aid, type, content, assertCurrent = () => { }) {
2057
+ const store = this._aidStore;
2058
+ const owner = this._currentAid;
2059
+ const identity = `${this._aid}:${this._serviceEventIdentityVersion}:${this._entityOwnershipConnection}`, guard = assertCurrent;
2060
+ assertCurrent = () => { guard(); if (`${this._aid}:${this._serviceEventIdentityVersion}:${this._entityOwnershipConnection}` !== identity || this._currentAid !== owner || this._aidStore !== store)
2061
+ throw ownershipFailure('conflict', 'entity ownership connection changed'); };
2062
+ if (!store || !owner?.isPrivateKeyValid())
2063
+ throw ownershipFailure('unavailable', 'entity ownership requires the current owner private key', true);
2064
+ const key = `${identity}:${aid}`, pending = this._entityOwnershipChecks.get(key);
2065
+ if (content !== undefined && pending) {
2066
+ await pending;
2067
+ assertCurrent();
2068
+ }
2069
+ if (!this._entityOwnershipChecks.has(key))
2070
+ this._entityOwnershipChecks.set(key, Promise.resolve(false));
2071
+ const authority = await this._entityLifecycle(aid, type);
2072
+ const result = authority;
2073
+ const entity = (isJsonObject(result.group) ? result.group : isJsonObject(result.organization) ? result.organization : result);
2074
+ if (entity.owner_aid !== owner.aid || entity.status !== 'active')
2075
+ throw ownershipFailure('forbidden', 'entity ownership requires its current active lifecycle owner');
2076
+ assertCurrent();
2077
+ let local = store.load(aid);
2078
+ if ((!local.ok || !local.data.aid.isPrivateKeyValid()) && type === 'group') {
2079
+ const keystore = store._keystore;
2080
+ let pending = null;
2081
+ for (const id of new Set([String(entity.group_id ?? aid), aid])) {
2082
+ pending = keystore?.loadPendingGroupBind?.(`bind:${id}`) ?? keystore?.loadPendingGroupBind?.(id);
2083
+ if (pending?.private_key_pem && pending.public_key_der_b64)
2084
+ break;
2085
+ }
2086
+ if (pending?.private_key_pem && pending.public_key_der_b64) {
2087
+ const certificate = await this._freshEntityCertificate(aid, type);
2088
+ await this.verifyCertPublicKeyMatch(certificate.certPem, pending.public_key_der_b64, 'entity ownership recovery');
2089
+ assertCurrent();
2090
+ const imported = store.importGroupIdentity(aid, { ...pending, cert_pem: certificate.certPem });
2091
+ if (!imported.ok)
2092
+ throw ownershipFailure('unavailable', imported.error.message, true);
2093
+ local = store.load(aid);
2094
+ }
2095
+ }
2096
+ if (!local.ok || !local.data.aid.isPrivateKeyValid())
2097
+ throw ownershipFailure('unavailable', 'entity ownership requires its local private key', true);
2098
+ const observed = await this._agentMdManager.readAgentMdFresh(aid);
2099
+ const current = observed.exists ? validateAgentMdReadDocument(observed.content, { expectedAid: aid }).frontmatter : {};
2100
+ const previous = isJsonObject(current.ownership) && isJsonObject(current.ownership.statement) ? current.ownership.statement : undefined;
2101
+ if (!previous || previous.owner_aid === owner.aid) {
2102
+ const consensus = await this.checkOwnershipConsensus(aid, true);
2103
+ assertCurrent();
2104
+ if (consensus.status === 'consensus' && consensus.owner_aid === owner.aid && content === undefined)
2105
+ return observed.content;
2106
+ if (!['consensus', 'unbound', 'single_sided'].includes(consensus.status))
2107
+ throw ownershipFailure(consensus.code ?? 'conflict', consensus.reason ?? 'entity ownership cannot be repaired from current materials');
2108
+ }
2109
+ let version = Number(previous?.relation_version ?? 1);
2110
+ if (previous && previous.owner_aid !== owner.aid) {
2111
+ const authorization = await this._getOwnershipFiles().readOwner(String(previous.owner_aid), aid);
2112
+ if (!authorization.exists)
2113
+ throw ownershipFailure('conflict', 'entity transfer authorization is missing');
2114
+ const verified = await this._getOwnershipFiles().verifyFile(authorization.content, 'owner', aid, String(previous.owner_aid));
2115
+ if (verified.action !== 'transfer_owner' || verified.transfer?.new_owner_aid !== owner.aid || verified.transfer.target_relation_version !== version + 1 || verified.transfer.expires_at < Date.now() / 1000)
2116
+ throw ownershipFailure('conflict', 'entity transfer authorization does not match');
2117
+ version++;
2118
+ }
2119
+ const storage = this._getOwnershipStorage();
2120
+ const original = await storage.readOwnerMd(owner.aid, aid);
2121
+ let statement = previous?.owner_aid === owner.aid ? previous : { version: 2, declaration_id: crypto.randomUUID(), owner_aid: owner.aid, sub_aid: aid, relation_version: version, declared_at: Math.floor(Date.now() / 1000) };
2122
+ if (original.exists) {
2123
+ const verified = await this._getOwnershipFiles().verifyFile(original.content, 'owner', aid, owner.aid);
2124
+ if (verified.action !== 'declare' || verified.statement?.relation_version !== version || verified.statement?.owner_aid !== owner.aid || verified.statement?.sub_aid !== aid)
2125
+ throw ownershipFailure('conflict', 'entity owner declaration changed');
2126
+ statement = verified.statement;
2127
+ }
2128
+ else {
2129
+ const payload = `---\nowner_aid: ${owner.aid}\nsub_aid: ${aid}\nownership: ${JSON.stringify({ statement, action: 'declare' })}\n---\n`;
2130
+ const raw = signOwnershipFile(owner, payload, 'owner');
2131
+ assertCurrent();
2132
+ await storage.writeOwnerMd(owner.aid, aid, raw, original, assertCurrent);
2133
+ }
2134
+ const source = content ?? (observed.exists ? observed.content : buildDefaultAgentMd(aid, { type, name: String(entity.name ?? aid) }));
2135
+ const updated = updateOwnershipAgentMd(source, { owner: owner.aid, ownership: { statement } }).replace(/^entity_cert_fingerprint:.*(?:\r?\n|$)/m, '');
2136
+ const signed = signOwnershipFile(local.data.aid, updated, 'agent');
2137
+ if (content === undefined) {
2138
+ if (!previous || previous.owner_aid !== owner.aid || previous.relation_version !== version) {
2139
+ assertCurrent();
2140
+ const uploaded = await store._uploadAgentMd(aid, signed, { uploaderAid: owner.aid, sourceGuard: assertCurrent, entityOwnerResolver: () => this._entityLifecycleOwner(aid, type), entityIdentityGetter: () => this._freshEntityCertificate(aid, type) });
2141
+ if (!uploaded.ok)
2142
+ throw ownershipFailure('unavailable', uploaded.error.message, true);
2143
+ }
2144
+ const consensus = await this.checkOwnershipConsensus(aid, true);
2145
+ assertCurrent();
2146
+ if (consensus.status !== 'consensus' || consensus.owner_aid !== owner.aid || consensus.relation_version !== version)
2147
+ throw ownershipFailure('unavailable', 'entity ownership publication could not be confirmed', true);
2148
+ }
2149
+ return signed;
2150
+ }
1916
2151
  async _maybeHandleHalfOpenRpcFailure(err) {
1917
2152
  if (this._closing || this.state !== ConnectionState.READY)
1918
2153
  return;
@@ -1926,44 +2161,17 @@ export class AUNClient {
1926
2161
  if (!(err instanceof AUNError) || err.code !== -32003)
1927
2162
  return false;
1928
2163
  const message = String(err.message ?? '').trim();
1929
- const groupCertPrefix = 'valid group identity certificate is required for ';
1930
- if (message.toLowerCase().startsWith(groupCertPrefix)) {
1931
- const fallback = message.slice(groupCertPrefix.length).trim();
1932
- const store = this._aidStore;
1933
- const groupId = String(params.group_id ?? params.groupId ?? params.group_aid ?? params.groupAid ?? fallback).trim();
1934
- if (!groupId || !store)
1935
- return false;
1936
- const groupManager = store._agentMdManager ?? this._agentMdManager;
1937
- const keystore = store._keystore;
1938
- let pendingBind = keystore?.loadPendingGroupBind?.(`bind:${groupId}`);
1939
- if (!pendingBind?.public_key_der_b64 || !pendingBind?.private_key_pem)
1940
- pendingBind = keystore?.loadPendingGroupBind?.(groupId);
1941
- if (groupManager.pendingUpload(fallback) && (!pendingBind?.public_key_der_b64 || !pendingBind?.private_key_pem))
1942
- return false;
1943
- try {
1944
- await this._runGroupIdentityOperation(groupId, () => this.bindGroupAid({ group_id: groupId }, { aidStore: store }));
1945
- }
1946
- catch {
1947
- return false;
2164
+ for (const prefix of ['valid group identity certificate is required for ', 'valid group agent.md is required for ']) {
2165
+ if (message.toLowerCase().startsWith(prefix)) {
2166
+ if (_method === 'group.get_info')
2167
+ return false;
2168
+ const groupId = String(params.group_aid ?? params.groupAid ?? params.group_id ?? params.groupId ?? message.slice(prefix.length)).trim();
2169
+ const target = normalizeGroupId(groupId.toLowerCase(), { localIssuer: this._aid?.split('.').slice(1).join('.') });
2170
+ const key = `${this._aid}:${this._serviceEventIdentityVersion}:${this._entityOwnershipConnection}:${target}`;
2171
+ if (!this._entityOwnershipChecks.has(key))
2172
+ await this._checkEntityOwnershipOnAccess('group.get_info', { group_id: groupId });
2173
+ return await this._entityOwnershipChecks.get(key) ?? false;
1948
2174
  }
1949
- return true;
1950
- }
1951
- const groupMDPrefix = 'valid group agent.md is required for ';
1952
- if (message.toLowerCase().startsWith(groupMDPrefix)) {
1953
- const groupAid = message.slice(groupMDPrefix.length).trim();
1954
- const store = this._aidStore;
1955
- if (!groupAid || !store)
1956
- return false;
1957
- const loaded = store.load(groupAid);
1958
- if (!loaded.ok || !loaded.data?.aid?.isPrivateKeyValid())
1959
- return false;
1960
- const groupManager = store._agentMdManager ?? this._agentMdManager;
1961
- if (groupManager.pendingUpload(groupAid))
1962
- return false;
1963
- if (String((groupManager._loadRecord?.(groupAid, true) ?? groupManager.loadRecord(groupAid))?.local_draft_content ?? ''))
1964
- return false;
1965
- await this._runGroupIdentityOperation(groupAid, () => this._uploadGroupAgentMd(store, groupAid, {}, {}));
1966
- return true;
1967
2175
  }
1968
2176
  const agentPrefix = 'valid agent.md is required for ';
1969
2177
  if (!message.toLowerCase().startsWith(agentPrefix))
@@ -2008,79 +2216,26 @@ export class AUNClient {
2008
2216
  if (!store)
2009
2217
  return;
2010
2218
  await this._resumeOrgIdentityAfterConnect(store);
2011
- let listed;
2012
- try {
2013
- listed = await this.call('group.list_my', {});
2014
- }
2015
- catch (exc) {
2016
- this._clientLog.warn(`post-connect group identity check failed: ${formatCaughtError(exc)}`);
2017
- return;
2018
- }
2019
- const result = isJsonObject(listed) ? listed : {};
2020
- const groups = Array.isArray(result.groups) ? result.groups : (Array.isArray(result.items) ? result.items : []);
2021
- for (const raw of groups) {
2022
- if (!isJsonObject(raw))
2023
- continue;
2024
- const group = raw;
2025
- const status = String(group.status ?? '').trim().toLowerCase();
2026
- if (status && status !== 'active')
2027
- continue;
2028
- const role = String(group.role ?? group.my_role ?? '').trim().toLowerCase();
2029
- const groupId = String(group.group_id ?? group.groupId ?? group.group_aid ?? group.groupAid ?? '').trim();
2030
- const groupAid = String(group.group_aid ?? group.groupAid ?? groupId).trim();
2031
- if (!groupId || !groupAid)
2032
- continue;
2033
- const keystore = store._keystore;
2034
- const pendingTransfer = keystore && typeof keystore.loadPendingGroupBind === 'function'
2035
- ? keystore.loadPendingGroupBind(`complete:${groupId}`)
2036
- : null;
2037
- if (role !== 'owner' && !pendingTransfer)
2038
- continue;
2039
- try {
2040
- await this._runGroupIdentityOperation(groupId, async () => {
2041
- if (pendingTransfer) {
2042
- await this.completeGroupTransfer({ group_id: groupId, group_aid: groupAid }, { aidStore: store });
2043
- return;
2044
- }
2045
- const groupManager = store._agentMdManager ?? this._agentMdManager;
2046
- if (groupManager.pendingUpload(groupAid))
2047
- return;
2048
- if (String((groupManager._loadRecord?.(groupAid, true) ?? groupManager.loadRecord(groupAid))?.local_draft_content ?? '')) {
2049
- await this._agentMdManager.check(groupAid);
2050
- return;
2051
- }
2052
- const loaded = store.load(groupAid);
2053
- if (!loaded.ok || !loaded.data?.aid?.isPrivateKeyValid()) {
2054
- await this.bindGroupAid({ group_id: groupId, group_aid: groupAid }, { aidStore: store });
2055
- return;
2056
- }
2057
- const checked = await this._agentMdManager.check(groupAid);
2058
- if (!checked.remote_found) {
2059
- await this._uploadGroupAgentMd(store, groupAid, {}, {});
2060
- }
2061
- });
2062
- }
2063
- catch (exc) {
2064
- this._clientLog.warn(`post-connect group identity check failed: group=${groupId} err=${formatCaughtError(exc)}`);
2065
- }
2066
- }
2067
2219
  }
2068
- async _runGroupIdentityOperation(groupId, operation) {
2220
+ async _runGroupIdentityOperation(groupId, operation, context = '') {
2069
2221
  const aid = String(this._aid ?? '').trim();
2070
2222
  const dot = aid.indexOf('.');
2071
2223
  const key = normalizeGroupId(groupId, { localIssuer: dot >= 0 ? aid.slice(dot + 1) : '' }).toLowerCase();
2072
2224
  if (!key)
2073
2225
  return operation();
2074
- let task = this._groupIdentityOperations.get(key);
2226
+ const pending = this._groupIdentityOperations.get(key);
2227
+ if (pending && pending.context !== context)
2228
+ throw ownershipFailure('conflict', 'pending Org creation context changed');
2229
+ let task = pending?.task;
2075
2230
  if (!task) {
2076
2231
  task = Promise.resolve().then(operation);
2077
- this._groupIdentityOperations.set(key, task);
2232
+ this._groupIdentityOperations.set(key, { task, context });
2078
2233
  }
2079
2234
  try {
2080
2235
  return await task;
2081
2236
  }
2082
2237
  finally {
2083
- if (this._groupIdentityOperations.get(key) === task)
2238
+ if (this._groupIdentityOperations.get(key)?.task === task)
2084
2239
  this._groupIdentityOperations.delete(key);
2085
2240
  }
2086
2241
  }
@@ -2111,6 +2266,9 @@ export class AUNClient {
2111
2266
  return refreshed;
2112
2267
  }
2113
2268
  async createOrg(params = {}, options = {}) {
2269
+ return this._runGroupIdentityOperation(`org-create:${this._aid}:${this._serviceEventIdentityVersion}:${String(params.org_id ?? '')}`, () => this._createOrg(params, options), new TextDecoder().decode(canonicalJson({ params, serviceAid: options.serviceAid ?? '' })));
2270
+ }
2271
+ async _createOrg(params, options) {
2114
2272
  const store = options.aidStore;
2115
2273
  const owner = this._currentAid;
2116
2274
  const orgAid = String(params.org_id ?? '').trim();
@@ -2334,21 +2492,28 @@ export class AUNClient {
2334
2492
  return result;
2335
2493
  }
2336
2494
  async _uploadOrgAgentMd(store, orgAid, params, initialResult) {
2495
+ const assertCurrent = this._entityOwnershipGuard();
2337
2496
  const owner = this._currentAid;
2338
2497
  if (!owner)
2339
2498
  throw ownershipFailure('forbidden', 'Org agent.md requires current owner');
2340
2499
  const organization = isJsonObject(initialResult) && isJsonObject(initialResult.organization) ? initialResult.organization : undefined;
2500
+ if (params.transfer_id && (organization?.org_id !== orgAid || organization.owner_aid !== owner.aid || organization.status !== 'active' || !isJsonObject(initialResult) || initialResult.transfer_id !== params.transfer_id))
2501
+ throw ownershipFailure('conflict', 'Org transfer response does not match the completed transfer');
2341
2502
  const bootstrap = organization?.org_id === orgAid && organization.owner_aid === owner.aid && organization.status === 'active'
2342
2503
  ? { aid: orgAid, ownerAid: owner.aid, certFingerprint: this._localEntityCertificate(orgAid).certFingerprint, completedTransfer: Boolean(params.transfer_id && isJsonObject(initialResult) && initialResult.transfer_id === params.transfer_id) } : undefined;
2343
2504
  if (await this._resumeGroupAgentMdUpload(store, orgAid, bootstrap))
2344
2505
  return;
2345
- const identity = this._localEntityCertificate(orgAid);
2346
- const content = String(params.org_agent_md ?? buildDefaultAgentMd(orgAid, { type: 'org', owner: owner.aid, entityCertFingerprint: identity.certFingerprint, name: String(params.name ?? orgAid) }));
2347
- const uploaded = await store._uploadAgentMd(orgAid, content, { uploaderAid: owner.aid, entityOwnerResolver: (aid, type) => this._resolveEntityAgentMdOwner(aid, type, bootstrap), entityIdentityGetter: (aid) => this._freshEntityCertificate(aid, 'org') }, (aid, notice, manager) => this._getAgentMdChanges().committed(aid, notice, manager));
2506
+ this._aidStore = store;
2507
+ const content = await this._completeEntityOwnership(orgAid, 'org', String(params.org_agent_md ?? buildDefaultAgentMd(orgAid, { type: 'org', owner: owner.aid, name: String(params.name ?? orgAid) })), assertCurrent);
2508
+ assertCurrent();
2509
+ const uploaded = await store._uploadAgentMd(orgAid, content, { uploaderAid: owner.aid, sourceGuard: assertCurrent, entityOwnerResolver: (aid, type) => this._resolveEntityAgentMdOwner(aid, type, bootstrap), entityIdentityGetter: (aid) => this._freshEntityCertificate(aid, 'org') }, (aid, notice, manager) => this._getAgentMdChanges().committed(aid, notice, manager));
2348
2510
  if (!uploaded.ok) {
2349
2511
  const code = ['invalid', 'unavailable', 'forbidden', 'conflict', 'persistence_unavailable'].includes(uploaded.error.code) ? uploaded.error.code : uploaded.error.code === 'NETWORK_ERROR' ? 'unavailable' : 'invalid';
2350
2512
  throw ownershipFailure(code, uploaded.error.message, code === 'unavailable');
2351
2513
  }
2514
+ const consensus = await this.checkOwnershipConsensus(orgAid, true);
2515
+ if (consensus.status !== 'consensus' || consensus.owner_aid !== owner.aid)
2516
+ throw ownershipFailure('unavailable', 'Org ownership publication could not be confirmed', true);
2352
2517
  }
2353
2518
  async createGroup(params = {}, options = {}) {
2354
2519
  if (!isJsonObject(params)) {
@@ -2529,13 +2694,14 @@ export class AUNClient {
2529
2694
  ].join('\n');
2530
2695
  }
2531
2696
  async _resumeGroupAgentMdUpload(store, groupAid, bootstrap) {
2697
+ const assertCurrent = this._entityOwnershipGuard();
2532
2698
  const identityStore = store;
2533
2699
  const pendingManager = identityStore._agentMdManager ?? this._agentMdManager;
2534
2700
  const pending = pendingManager.pendingUpload(groupAid);
2535
2701
  if (!pending)
2536
2702
  return false;
2537
2703
  const fields = validateAgentMdDocument(pending.content, { expectedAid: groupAid, validateOwnership: false }).frontmatter;
2538
- if (['ownership', 'ownership_proof', 'ownership_release'].some((field) => Object.hasOwn(fields, field))) {
2704
+ if (!['group', 'org'].includes(String(fields.type)) && ['ownership', 'ownership_proof', 'ownership_release'].some((field) => Object.hasOwn(fields, field))) {
2539
2705
  throw new StateError('ownership pending agent.md requires its persisted 2.1 task', { stringCode: 'unsupported_protocol' });
2540
2706
  }
2541
2707
  const identity = this._currentAid;
@@ -2549,6 +2715,7 @@ export class AUNClient {
2549
2715
  uploadViaGatewayHost: true,
2550
2716
  ownerAidGetter: () => identity.aid,
2551
2717
  currentAidGetter: () => identity,
2718
+ sourceGuard: assertCurrent,
2552
2719
  entityOwnerResolver: (aid, type) => this._resolveEntityAgentMdOwner(aid, type, bootstrap?.completedTransfer || pending.condition?.if_none_match === '*' ? bootstrap : undefined),
2553
2720
  entityIdentityGetter: (aid, type) => this._freshEntityCertificate(aid, type),
2554
2721
  entitySignerResolver: (aid, fingerprint) => this._freshEntitySigner(aid, fingerprint),
@@ -2562,10 +2729,18 @@ export class AUNClient {
2562
2729
  },
2563
2730
  aidValidator: (aid) => identityStore._registerFlow.validateAidName(aid),
2564
2731
  });
2565
- return await manager.resumePendingUpload(groupAid);
2732
+ const resumed = await manager.resumePendingUpload(groupAid);
2733
+ assertCurrent();
2734
+ if (resumed) {
2735
+ const consensus = await this.checkOwnershipConsensus(groupAid, true);
2736
+ if (consensus.status !== 'consensus' || consensus.owner_aid !== identity.aid)
2737
+ throw ownershipFailure('unavailable', 'entity ownership recovery could not be confirmed', true);
2738
+ }
2739
+ return resumed;
2566
2740
  }
2567
2741
  async _uploadGroupAgentMd(store, groupAid, params, group, initialCertificate) {
2568
2742
  const uploaderAid = String(this._aid ?? '').trim();
2743
+ const assertCurrent = this._entityOwnershipGuard();
2569
2744
  if (!uploaderAid) {
2570
2745
  throw new ValidationError('group agent.md upload requires current owner AID');
2571
2746
  }
@@ -2573,11 +2748,14 @@ export class AUNClient {
2573
2748
  const bootstrap = initialCertificate ? { aid: groupAid, ownerAid: String(group.owner_aid ?? '').trim(), certFingerprint: certFingerprint(initialCertificate), type: 'group' } : undefined;
2574
2749
  if (await this._resumeGroupAgentMdUpload(store, groupAid, bootstrap))
2575
2750
  return;
2576
- 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) }, (aid, notice, manager) => this._getAgentMdChanges().committed(aid, notice, manager));
2751
+ const uploaded = await store._uploadAgentMd(groupAid, await this._completeEntityOwnership(groupAid, 'group', this._groupAgentMdContent(groupAid, params, group), assertCurrent), { uploaderAid, sourceGuard: assertCurrent, entityOwnerResolver: (aid, type) => this._resolveEntityAgentMdOwner(aid, type, bootstrap), entityIdentityGetter: (aid) => this._freshEntityCertificate(aid) }, (aid, notice, manager) => this._getAgentMdChanges().committed(aid, notice, manager));
2577
2752
  if (!uploaded.ok) {
2578
2753
  const code = ['invalid', 'unavailable', 'forbidden', 'conflict', 'persistence_unavailable'].includes(uploaded.error.code) ? uploaded.error.code : uploaded.error.code === 'NETWORK_ERROR' ? 'unavailable' : 'invalid';
2579
2754
  throw ownershipFailure(code, `group agent.md upload failed: ${uploaded.error.message}`, code === 'unavailable');
2580
2755
  }
2756
+ const consensus = await this.checkOwnershipConsensus(groupAid, true);
2757
+ if (consensus.status !== 'consensus' || consensus.owner_aid !== uploaderAid)
2758
+ throw ownershipFailure('unavailable', 'group ownership publication could not be confirmed', true);
2581
2759
  }
2582
2760
  _localEntityCertificate(aid) {
2583
2761
  const loaded = this._aidStore?.load(aid);
@@ -2599,7 +2777,7 @@ export class AUNClient {
2599
2777
  try {
2600
2778
  authority = bootstrap?.completedTransfer && type === 'org' && bootstrap.aid === aid && bootstrap.ownerAid === this._currentAid?.aid
2601
2779
  ? { owner_aid: bootstrap.ownerAid, entity_cert_fingerprint: bootstrap.certFingerprint }
2602
- : await this._agentMdManager.readEntityAuthorityFresh(aid);
2780
+ : { owner_aid: await this._entityLifecycleOwner(aid, type), entity_cert_fingerprint: (await this._freshEntityCertificate(aid, type)).certFingerprint };
2603
2781
  }
2604
2782
  catch (error) {
2605
2783
  if (!bootstrap || bootstrap.aid !== aid || type !== (bootstrap.type ?? 'org') || !(error instanceof AUNError) || !isJsonObject(error.data) || error.data.http_status !== 404)
@@ -2613,6 +2791,26 @@ export class AUNClient {
2613
2791
  throw ownershipFailure('conflict', 'entity authority changed during current-certificate verification');
2614
2792
  return authority.owner_aid;
2615
2793
  }
2794
+ async _entityLifecycle(aid, type) {
2795
+ const method = type === 'group' ? 'group.get_info' : 'org.lifecycle.get';
2796
+ let params = type === 'group' ? { group_id: aid, required: ['member'] } : { org_id: aid };
2797
+ const signer = this._currentAid;
2798
+ if (type === 'org' && signer) {
2799
+ const records = this._tokenStore.loadMetadata?.(signer.aid)?.credential_renewals_v1 ?? {};
2800
+ const record = Object.values(records).sort((left, right) => (right.membership_binding?.binding_version ?? 0) - (left.membership_binding?.binding_version ?? 0)).find(record => !record.successor_cid && record.credential?.iss === aid && record.credential?.sub === signer.aid && record.membership_binding?.org_id === aid && record.membership_binding?.member_aid === signer.aid);
2801
+ if (record)
2802
+ params = this.org.prepareRequest(method, { org_id: aid, fields: ['org_id', 'status', 'owner_aid'], credentials: { [record.membership_binding.role === 'admin' ? 'approval_credential' : 'member_credential']: record.credential, membership_binding: record.membership_binding } }, 'org.' + aid.slice(aid.indexOf('.') + 1), signer);
2803
+ }
2804
+ return this._rpcPipeline.call(method, params);
2805
+ }
2806
+ async _entityLifecycleOwner(aid, type) {
2807
+ const response = await this._entityLifecycle(aid, type);
2808
+ const data = response;
2809
+ const entity = (isJsonObject(data.group) ? data.group : isJsonObject(data.organization) ? data.organization : data);
2810
+ if (entity.status !== 'active' || typeof entity.owner_aid !== 'string')
2811
+ throw ownershipFailure('unavailable', 'entity lifecycle owner is unavailable', true);
2812
+ return entity.owner_aid;
2813
+ }
2616
2814
  async bindGroupAid(params = {}, options = {}) {
2617
2815
  if (!isJsonObject(params)) {
2618
2816
  throw new ValidationError('bindGroupAid params must be an object');
@@ -3051,8 +3249,8 @@ export class AUNClient {
3051
3249
  const messageEvent = event === 'message.received' || event === 'message.recalled' || event === 'message.batch';
3052
3250
  const hadMessageConsumer = messageEvent && [...this._dispatcher._snapshotApplicationHandlers(['message.received', 'message.recalled', 'message.batch']).values()].some((handlers) => handlers.length > 0);
3053
3251
  const result = this._dispatcher.subscribe(event, handler);
3054
- if (messageEvent) {
3055
- if (this._p2pPeerPullEnabled && !hadMessageConsumer)
3252
+ if (messageEvent || event === 'group.message_created' || event === 'group.message_batch') {
3253
+ if (messageEvent && this._p2pPeerPullEnabled && !hadMessageConsumer)
3056
3254
  this._delivery.refreshPendingPeers();
3057
3255
  this._delivery.requestBackgroundPullRecovery();
3058
3256
  }
@@ -3196,7 +3394,7 @@ export class AUNClient {
3196
3394
  const errors = [];
3197
3395
  for (const entry of [...this._readStateQueries.values()]) {
3198
3396
  try {
3199
- const result = await this.call(entry.method, { ...entry.params });
3397
+ const result = await this.call(entry.method, { ...entry.params, _rpc_background: true });
3200
3398
  if (identity !== this._ensureReadStateIdentity())
3201
3399
  return;
3202
3400
  this._observeReadStateResult(entry.method, result, entry.params);
@@ -3386,8 +3584,11 @@ export class AUNClient {
3386
3584
  this._delivery.markPublishedSeq(ns, seq);
3387
3585
  }
3388
3586
  _publishAppEvent(event, payload, source = 'direct', deliveryContext) {
3389
- if (messageClassOf(payload) === 'sdk_control')
3390
- return this._controlMessages().consume(payload);
3587
+ if (messageClassOf(payload) === 'sdk_control') {
3588
+ if (event === 'message.received')
3589
+ return this._controlMessages().consume(payload);
3590
+ return;
3591
+ }
3391
3592
  if (event === 'message.received') {
3392
3593
  return this._handleOwnershipMessage(payload).then((handled) => {
3393
3594
  if (!handled)
@@ -4203,7 +4404,7 @@ export class AUNClient {
4203
4404
  const seen = new Set();
4204
4405
  do {
4205
4406
  if (response === undefined) {
4206
- response = await this.call('group.list_my_invites', cursor ? { cursor } : {});
4407
+ response = await this.call('group.list_my_invites', { ...(cursor ? { cursor } : {}), _rpc_background: true });
4207
4408
  }
4208
4409
  let items = [];
4209
4410
  let nextCursor = '';
@@ -4317,6 +4518,7 @@ export class AUNClient {
4317
4518
  const tStart = Date.now();
4318
4519
  connectionOwner ??= this._connectAbort ?? undefined;
4319
4520
  this._lifecycle.assertConnectionAttemptOwner(connectionOwner);
4521
+ this._controlMessageCoordinator?.close();
4320
4522
  const gatewayUrl = this._resolveGateway(params);
4321
4523
  this._gatewayUrl = gatewayUrl;
4322
4524
  this._slotId = String(params.slot_id ?? '');
@@ -4325,13 +4527,17 @@ export class AUNClient {
4325
4527
  const extraInfo = (params.extra_info && typeof params.extra_info === 'object' && !Array.isArray(params.extra_info))
4326
4528
  ? { ...params.extra_info }
4327
4529
  : {};
4530
+ const capabilities = isJsonObject(extraInfo._capabilities) ? { ...extraInfo._capabilities } : {};
4531
+ delete capabilities.message_classes_v1;
4328
4532
  if (typeof this._tokenStore.updateInstanceState === 'function') {
4329
- extraInfo._capabilities = {
4330
- ...(isJsonObject(extraInfo._capabilities) ? extraInfo._capabilities : {}),
4331
- message_classes_v1: { p2p: ['normal', 'control', 'sdk_control'], group: ['normal', 'control', 'sdk_control'] },
4332
- sdk_control_protocols: { 'aun.agent_md.changed': [1] },
4333
- };
4533
+ capabilities.message_classes_v2 = { p2p: ['normal', 'control', 'sdk_control'], group: ['normal', 'control'] };
4534
+ capabilities.sdk_control_protocols = { 'aun.agent_md.changed': [1], 'aun.pubsub': [1] };
4535
+ }
4536
+ else {
4537
+ delete capabilities.message_classes_v2;
4538
+ delete capabilities.sdk_control_protocols;
4334
4539
  }
4540
+ extraInfo._capabilities = capabilities;
4335
4541
  const prevState = this._state;
4336
4542
  this._auth.setInstanceContext({ deviceId: this._deviceId, slotId: this._slotId });
4337
4543
  this._state = 'connecting';
@@ -4380,6 +4586,7 @@ export class AUNClient {
4380
4586
  if (isJsonObject(auth.hello) && 'heartbeat_interval' in auth.hello) {
4381
4587
  this._applyServerHeartbeatInterval(auth.hello.heartbeat_interval, 'auth');
4382
4588
  }
4589
+ this._bindGatewayRuntimeIdentity(auth.hello, params);
4383
4590
  this._gatewayConnectionId = isJsonObject(auth.hello) && isJsonObject(auth.hello.connection) ? String(auth.hello.connection.id ?? '') : '';
4384
4591
  this._negotiatedCapabilities = isJsonObject(auth.hello) && isJsonObject(auth.hello.negotiated_capabilities) ? auth.hello.negotiated_capabilities : {};
4385
4592
  if (isJsonObject(auth.hello))
@@ -4401,6 +4608,7 @@ export class AUNClient {
4401
4608
  if (isJsonObject(hello) && 'heartbeat_interval' in hello) {
4402
4609
  this._applyServerHeartbeatInterval(hello.heartbeat_interval, 'auth');
4403
4610
  }
4611
+ this._bindGatewayRuntimeIdentity(hello, params);
4404
4612
  this._gatewayConnectionId = isJsonObject(hello) && isJsonObject(hello.connection) ? String(hello.connection.id ?? '') : '';
4405
4613
  this._negotiatedCapabilities = isJsonObject(hello) && isJsonObject(hello.negotiated_capabilities) ? hello.negotiated_capabilities : {};
4406
4614
  if (isJsonObject(hello))
@@ -4468,8 +4676,6 @@ export class AUNClient {
4468
4676
  else {
4469
4677
  this._clientLog.debug('V2 session init deferred for short connection');
4470
4678
  }
4471
- this._controlMessages().close();
4472
- this._safeAsync(this._controlMessages().recover());
4473
4679
  this._agentMdChanges?.close();
4474
4680
  this._agentMdChanges = new AgentMdChangeCoordinator(this);
4475
4681
  // connect/reconnect 成功后自动触发一次 P2P message.v2.pull,补齐离线期间积压
@@ -4479,8 +4685,14 @@ export class AUNClient {
4479
4685
  this._clientLog.warn(`schedule post-connect P2P gap fill failed: ${formatCaughtError(exc)}`);
4480
4686
  });
4481
4687
  }
4688
+ this._entityOwnershipConnection++;
4689
+ this._entityOwnershipChecks.clear();
4482
4690
  if (!this._ownershipOperation)
4483
4691
  this._safeAsync(this._checkIdentityAdmissionAfterConnect());
4692
+ this._safeAsync(this._delivery.restoreMessageClassPulls());
4693
+ if (!isShortConnection && backgroundSyncEnabled && this._v2Session) {
4694
+ this._safeAsync(this._v2AutoConfirmPendingProposals());
4695
+ }
4484
4696
  this._clientLog.debug(`_connectOnce exit: elapsed=${Date.now() - tStart}ms gateway=${gatewayUrl}, aid=${this._aid ?? ''}`);
4485
4697
  }
4486
4698
  catch (err) {
@@ -4632,8 +4844,8 @@ export class AUNClient {
4632
4844
  async _getV2SenderPubDer(fromAid, senderDeviceId, certFingerprint) {
4633
4845
  return await this._v2E2EE.getV2SenderPubDer(fromAid, senderDeviceId, certFingerprint);
4634
4846
  }
4635
- async _fetchV2SenderIKOnce(fromAid, senderDeviceId, groupId, certFingerprint) {
4636
- return await this._v2E2EE.fetchSenderIKOnce(fromAid, senderDeviceId, groupId, certFingerprint);
4847
+ async _fetchV2SenderIKOnce(fromAid, senderDeviceId, groupId, certFingerprint, rpcOptions) {
4848
+ return await this._v2E2EE.fetchSenderIKOnce(fromAid, senderDeviceId, groupId, certFingerprint, ...(rpcOptions ? [rpcOptions] : []));
4637
4849
  }
4638
4850
  _cacheV2PeerIKFromDevice(dev, fallbackAid = '') {
4639
4851
  const session = this._v2Session;
@@ -4694,7 +4906,9 @@ export class AUNClient {
4694
4906
  return await this._v2E2EE.ackGroupV2(groupId, upToSeq, groupAid);
4695
4907
  }
4696
4908
  /** 解密单条 V2 pull 消息。缺 sender IK 时先入 pending,后台补齐后重试。 */
4697
- async _decryptV2Message(msg, allowPending = true, emitUndecryptable = true, rotateKeys = true, observeAgentMd = true, deferStatus, deferKeyFetch = false, source = 'pull', deferredEffects) {
4909
+ async _decryptV2Message(msg, allowPending = true, emitUndecryptable = true, rotateKeys = true, observeAgentMd = true, deferStatus, deferKeyFetch = false, source = 'pull', deferredEffects, rpcOptions) {
4910
+ if (rpcOptions?.deadline !== undefined && rpcOptions.deadline <= Date.now())
4911
+ return null;
4698
4912
  const session = this._v2Session;
4699
4913
  if (!session)
4700
4914
  return null;
@@ -4809,9 +5023,11 @@ export class AUNClient {
4809
5023
  const senderDeviceId = String(aad.from_device ?? '');
4810
5024
  const senderCertFingerprint = String(envelope.sender_cert_fingerprint ?? '').trim().toLowerCase();
4811
5025
  let senderPubDer = await this._getV2SenderPubDer(fromAid, senderDeviceId, senderCertFingerprint);
5026
+ if (rpcOptions?.deadline !== undefined && rpcOptions.deadline <= Date.now())
5027
+ return null;
4812
5028
  if (!senderPubDer && !deferKeyFetch) {
4813
5029
  this._clientLog.warn(`V2 decrypt: no sender IK for ${fromAid} device=${senderDeviceId}`);
4814
- senderPubDer = await this._fetchV2SenderIKOnce(fromAid, senderDeviceId, groupIdForKeys, senderCertFingerprint);
5030
+ senderPubDer = await this._fetchV2SenderIKOnce(fromAid, senderDeviceId, groupIdForKeys, senderCertFingerprint, ...(rpcOptions ? [rpcOptions] : []));
4815
5031
  }
4816
5032
  if (!senderPubDer) {
4817
5033
  if (allowPending || deferStatus) {
@@ -5143,7 +5359,7 @@ export class AUNClient {
5143
5359
  }
5144
5360
  }
5145
5361
  /** 发现并缓存全部 Gateway 候选,供首连和自动重连故障转移使用。 */
5146
- async _resolveGatewayCandidatesForAid(aid, fresh = false, reconnectOwner) {
5362
+ async _resolveGatewayCandidatesForAid(aid, fresh = false, reconnectOwner, connectionOwner) {
5147
5363
  const resolvedAid = String(aid ?? this._aid ?? '').trim();
5148
5364
  if (!resolvedAid) {
5149
5365
  throw new StateError('gateway discovery requires a loaded AID');
@@ -5157,7 +5373,8 @@ export class AUNClient {
5157
5373
  const discoveryIsCurrent = () => (this._discovery === discovery
5158
5374
  && this._tokenStore === tokenStore
5159
5375
  && !this._closing
5160
- && (!reconnectOwner || this._ownsReconnect(reconnectOwner)));
5376
+ && (!reconnectOwner || this._ownsReconnect(reconnectOwner))
5377
+ && (!connectionOwner || this._lifecycle.ownsConnectionAttempt(connectionOwner)));
5161
5378
  if (!fresh && sameIssuer && this._gatewayCandidates.length > 0)
5162
5379
  return [...this._gatewayCandidates];
5163
5380
  if (!fresh && sameIssuer && this._gatewayUrl) {
@@ -5200,13 +5417,17 @@ export class AUNClient {
5200
5417
  let lastErr = null;
5201
5418
  for (const url of candidates) {
5202
5419
  try {
5420
+ const requestUrl = new URL(url);
5421
+ if (resolvedAid === this._aid) {
5422
+ requestUrl.search = new URLSearchParams({ aid: resolvedAid, sdk_lang: 'typescript', sdk_version: VERSION, capabilities: 'gateway_migrate_v1' }).toString();
5423
+ }
5203
5424
  // discoverAll 是生产路径;若调用方注入了旧版/测试 discovery
5204
5425
  // 替身而只提供 discover,则保留单候选兼容,不影响多候选发现。
5205
5426
  const hasInjectedDiscover = Object.prototype.hasOwnProperty.call(discovery, 'discover')
5206
5427
  && typeof discovery.discover === 'function';
5207
5428
  const discovered = hasInjectedDiscover
5208
- ? [await discovery.discover(url)]
5209
- : await discovery.discoverAll(url);
5429
+ ? [await discovery.discover(requestUrl.toString())]
5430
+ : await discovery.discoverAll(requestUrl.toString());
5210
5431
  if (!discoveryIsCurrent())
5211
5432
  throw new StateError('gateway discovery superseded by identity change');
5212
5433
  const gatewayUrls = [...new Set(discovered.map((item) => String(item ?? '').trim()).filter(Boolean))];
@@ -5446,6 +5667,11 @@ export class AUNClient {
5446
5667
  scheduleNext();
5447
5668
  return;
5448
5669
  }
5670
+ const rateLimitRemaining = this._transport._rateLimitRemainingMs();
5671
+ if (rateLimitRemaining > 0) {
5672
+ scheduleNext(rateLimitRemaining);
5673
+ return;
5674
+ }
5449
5675
  let identity = this._identity;
5450
5676
  if (identity === null) {
5451
5677
  scheduleNext();
@@ -5483,6 +5709,11 @@ export class AUNClient {
5483
5709
  this._tokenRefreshFailures = 0;
5484
5710
  }
5485
5711
  catch (exc) {
5712
+ if (exc instanceof AUNError && (exc.code === -32429 || exc.code === -32029 || exc.code === 429 || exc.code === 4290)) {
5713
+ this._transport._observeRateLimit(exc);
5714
+ scheduleNext(this._transport._rateLimitRemainingMs());
5715
+ return;
5716
+ }
5486
5717
  if (exc instanceof AuthError) {
5487
5718
  if (authErrorRequiresRelogin(exc)) {
5488
5719
  this._clientLog.warn(`token refresh requires relogin, stopping refresh loop and triggering reconnect: ${exc.message}`);
@@ -5560,6 +5791,18 @@ export class AUNClient {
5560
5791
  // ── 内部:断线重连 ────────────────────────────────────────
5561
5792
  /** 不重连 close code 集合:认证失败/权限错误/被踢等,重连无意义 */
5562
5793
  static _NO_RECONNECT_CODES = new Set([4001, 4003, 4008, 4009, 4010, 4011, 4013, 4014, 4015]);
5794
+ _bindGatewayRuntimeIdentity(hello, params) {
5795
+ const actual = isJsonObject(hello) && isJsonObject(hello.runtime_identity) ? hello.runtime_identity : null;
5796
+ const metadata = this._discovery._gatewayMetadata?.get(this._gatewayUrl ?? '');
5797
+ const expected = params._expected_gateway_runtime_identity ?? Object.fromEntries(['cluster_id', 'node_id', 'node_incarnation', 'service_instance_id'].filter((key) => metadata?.[key]).map((key) => [key, metadata[key]]));
5798
+ if (isJsonObject(expected) && Object.entries(expected).some(([key, value]) => value !== actual?.[key])) {
5799
+ throw new ConnectionError('gateway runtime identity changed');
5800
+ }
5801
+ const previousRevision = this._gatewayRuntimeIdentity?.cluster_id === actual?.cluster_id ? this._gatewayReleaseRevision : 0;
5802
+ const revision = params._expected_gateway_release_revision ?? metadata?.release_revision;
5803
+ this._gatewayReleaseRevision = Number.isSafeInteger(revision) && Number(revision) > 0 ? Math.max(previousRevision, Number(revision)) : previousRevision;
5804
+ this._gatewayRuntimeIdentity = actual && Object.keys(actual).length > 0 ? actual : null;
5805
+ }
5563
5806
  _onGatewayMigrate(input) {
5564
5807
  const data = input;
5565
5808
  if (!isJsonObject(data) || this._closing || this._disconnecting || this._connectAbort || this._reconnectActive
@@ -5570,6 +5813,7 @@ export class AUNClient {
5570
5813
  const now = Date.now() / 1000;
5571
5814
  if (typeof data.migration_id !== 'string' || !data.migration_id.trim()
5572
5815
  || typeof data.revision !== 'number' || !Number.isSafeInteger(data.revision) || data.revision <= 0
5816
+ || data.revision < this._gatewayReleaseRevision
5573
5817
  || typeof data.issuer !== 'string' || data.issuer.trim().toLowerCase().replace(/\.$/, '') !== issuer
5574
5818
  || data.aid !== aid || data.device_id !== this._deviceId || data.slot_id !== this._slotId
5575
5819
  || data.connection_id !== this._gatewayConnectionId || typeof data.gateway_url !== 'string'
@@ -5589,6 +5833,14 @@ export class AUNClient {
5589
5833
  || !(hostname === issuer || hostname.endsWith(`.${issuer}`))
5590
5834
  || !hostname.split('.').every((label) => /^[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?$/.test(label)))
5591
5835
  return;
5836
+ if (this._gatewayReleaseRevision > 0 || ['cluster_id', 'source', 'target', 'release_revision'].some((key) => key in data)) {
5837
+ const source = data.source, runtime = this._gatewayRuntimeIdentity, destination = data.target;
5838
+ if (!runtime || !isJsonObject(source) || !isJsonObject(destination) || !data.cluster_id
5839
+ || data.cluster_id !== runtime.cluster_id || destination.cluster_id !== data.cluster_id || data.release_revision !== data.revision
5840
+ || ['cluster_id', 'node_id', 'node_incarnation', 'service_instance_id'].some((key) => typeof source[key] !== 'string' || !String(source[key]).trim() || source[key] !== runtime[key]
5841
+ || typeof destination[key] !== 'string' || !String(destination[key]).trim()))
5842
+ return;
5843
+ }
5592
5844
  const identity = JSON.stringify([this._aid, this._deviceId, this._slotId]);
5593
5845
  const previous = this._migrationRevision.get(identity);
5594
5846
  if (previous && (data.revision <= previous.revision || data.migration_id === previous.migrationId))
@@ -5653,7 +5905,8 @@ export class AUNClient {
5653
5905
  this._state = 'reconnecting';
5654
5906
  this._migrationPending = true;
5655
5907
  try {
5656
- await wait(this._connectOnce({ ...this._sessionParams, gateway, gateways: candidates }, true, owner));
5908
+ await wait(this._connectOnce({ ...this._sessionParams, gateway, gateways: candidates,
5909
+ ...(attempt === 1 && index === 0 && isJsonObject(data.target) ? { _expected_gateway_runtime_identity: data.target, _expected_gateway_release_revision: data.release_revision } : {}), }, true, owner));
5657
5910
  if (!current())
5658
5911
  return;
5659
5912
  if (!this._transport.isConnected())
@@ -5673,7 +5926,7 @@ export class AUNClient {
5673
5926
  if (attempt === 1 && index === 0) {
5674
5927
  try {
5675
5928
  const discovered = await wait(this._resolveGatewayCandidatesForAid(String(data.aid), true, owner));
5676
- candidates = [...candidates, ...discovered.filter((item) => !candidates.includes(item))];
5929
+ candidates = [...candidates, ...discovered.filter((item) => item === gateway || !candidates.includes(item))];
5677
5930
  }
5678
5931
  catch (discoveryError) {
5679
5932
  lastError = discoveryError;
@@ -6014,7 +6267,10 @@ export class AUNClient {
6014
6267
  this._sessionParams.access_token = '';
6015
6268
  }
6016
6269
  }
6017
- const candidates = this._orderedReconnectGateways(this._sessionParams);
6270
+ const candidates = await this._lifecycle.waitForAttempt(this._resolveGatewayCandidatesForAid(this._aid, true, reconnectAbort ?? undefined), undefined, reconnectAbort ?? undefined);
6271
+ if (!this._ownsReconnect(reconnectAbort))
6272
+ return;
6273
+ this._sessionParams.gateways = [...candidates];
6018
6274
  let lastGatewayError = null;
6019
6275
  let connected = false;
6020
6276
  for (const gateway of candidates) {