@agentunion/fastaun 0.5.29 → 0.5.31

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 (88) hide show
  1. package/CHANGELOG.md +44 -0
  2. package/_packed_docs/CHANGELOG.md +44 -0
  3. package/_packed_docs/INDEX.md +11 -2
  4. package/_packed_docs/KITE_DOCS_GUIDE.md +10 -8
  5. package/_packed_docs/aun/346/265/213/350/257/225/350/277/220/350/241/214/346/214/207/345/215/227.md +18 -1
  6. package/_packed_docs/sdk/0.5.30-SDK/345/272/224/347/224/250/351/233/206/346/210/220/346/214/207/345/215/227.md +61 -0
  7. package/_packed_docs/sdk/0.5.30-SDK/345/274/200/345/217/221/344/270/216/347/247/273/346/244/215/346/214/207/345/215/227.md +68 -0
  8. 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
  9. 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
  10. package/_packed_docs/sdk/AUN_DOCS_GUIDE.md +5 -3
  11. package/_packed_docs/sdk/INDEX.md +9 -1
  12. package/_packed_docs//351/231/214/347/224/237/344/272/272/346/213/222/347/273/235/345/215/217/350/256/256TDD/345/210/206/351/230/266/346/256/265/345/256/236/346/226/275/350/256/241/345/210/222.md +138 -0
  13. package/_packed_docs//351/231/214/347/224/237/344/272/272/346/213/222/347/273/235/345/215/217/350/256/256/350/257/246/347/273/206/350/256/276/350/256/241.md +231 -0
  14. package/dist/agent-md-schema.js +7 -6
  15. package/dist/agent-md-schema.js.map +1 -1
  16. package/dist/agent-md.d.ts +2 -0
  17. package/dist/agent-md.js +33 -10
  18. package/dist/agent-md.js.map +1 -1
  19. package/dist/aid-store.d.ts +1 -0
  20. package/dist/aid-store.js +6 -3
  21. package/dist/aid-store.js.map +1 -1
  22. package/dist/aid.js +1 -1
  23. package/dist/aid.js.map +1 -1
  24. package/dist/auth.js +1 -1
  25. package/dist/auth.js.map +1 -1
  26. package/dist/client/control-messages.d.ts +0 -2
  27. package/dist/client/control-messages.js +79 -108
  28. package/dist/client/control-messages.js.map +1 -1
  29. package/dist/client/lifecycle.js +11 -3
  30. package/dist/client/lifecycle.js.map +1 -1
  31. package/dist/client/rpc-pipeline.d.ts +1 -1
  32. package/dist/client/rpc-pipeline.js +17 -5
  33. package/dist/client/rpc-pipeline.js.map +1 -1
  34. package/dist/client/v2-e2ee.js +7 -7
  35. package/dist/client/v2-e2ee.js.map +1 -1
  36. package/dist/client.d.ts +12 -0
  37. package/dist/client.js +346 -126
  38. package/dist/client.js.map +1 -1
  39. package/dist/config.js +23 -18
  40. package/dist/config.js.map +1 -1
  41. package/dist/discovery.d.ts +1 -0
  42. package/dist/discovery.js +3 -1
  43. package/dist/discovery.js.map +1 -1
  44. package/dist/facades.d.ts +20 -0
  45. package/dist/facades.js +9 -0
  46. package/dist/facades.js.map +1 -1
  47. package/dist/index.d.ts +1 -1
  48. package/dist/index.js.map +1 -1
  49. package/dist/keystore/aid-db.js +14 -2
  50. package/dist/keystore/aid-db.js.map +1 -1
  51. package/dist/keystore/index.d.ts +3 -0
  52. package/dist/keystore/index.js +61 -1
  53. package/dist/keystore/index.js.map +1 -1
  54. package/dist/keystore/local-identity-store.js +112 -76
  55. package/dist/keystore/local-identity-store.js.map +1 -1
  56. package/dist/keystore/local-token-store.js +15 -26
  57. package/dist/keystore/local-token-store.js.map +1 -1
  58. package/dist/ownership-v21.d.ts +2 -2
  59. package/dist/ownership-v21.js +18 -21
  60. package/dist/ownership-v21.js.map +1 -1
  61. package/dist/ownership-workflow.js +7 -4
  62. package/dist/ownership-workflow.js.map +1 -1
  63. package/dist/register-flow.js +4 -3
  64. package/dist/register-flow.js.map +1 -1
  65. package/dist/risk-control.js +2 -2
  66. package/dist/risk-control.js.map +1 -1
  67. package/dist/secret-store/file-store.js +68 -56
  68. package/dist/secret-store/file-store.js.map +1 -1
  69. package/dist/semantic-protocol.d.ts +2 -12
  70. package/dist/semantic-protocol.js +31 -48
  71. package/dist/semantic-protocol.js.map +1 -1
  72. package/dist/storage/lowlevel.d.ts +4 -0
  73. package/dist/storage/lowlevel.js +62 -1
  74. package/dist/storage/lowlevel.js.map +1 -1
  75. package/dist/storage/vfs.d.ts +4 -0
  76. package/dist/storage/vfs.js +55 -12
  77. package/dist/storage/vfs.js.map +1 -1
  78. package/dist/tools/cross-sdk-agent.d.ts +1 -1
  79. package/dist/tools/cross-sdk-agent.js +14 -3
  80. package/dist/tools/cross-sdk-agent.js.map +1 -1
  81. package/dist/transport.d.ts +1 -1
  82. package/dist/transport.js +8 -8
  83. package/dist/transport.js.map +1 -1
  84. package/dist/validators.js +9 -8
  85. package/dist/validators.js.map +1 -1
  86. package/dist/version.d.ts +1 -1
  87. package/dist/version.js +1 -1
  88. 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';
@@ -50,8 +51,8 @@ import { decryptedOwnershipJsonText, decryptedMessagePayloadBytes } from './v2/e
50
51
  import { isJsonObject, ConnectionState, STATE_TO_PUBLIC, } from './types.js';
51
52
  import { AID } from './aid.js';
52
53
  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';
54
+ import { validateAgentMdCertificate, validateAgentMdDocument, validateAgentMdReadDocument } from './agent-md-schema.js';
55
+ import { OwnershipFiles, OwnershipStorageAdapter, ownershipAid, ownershipFailure, ownershipSha256, parseOwnershipFile, signOwnershipFile } from './ownership-v21.js';
55
56
  import { OwnershipWorkflow, OWNERSHIP_SDK_MESSAGE_TYPE, parseOwnershipMessage } from './ownership-workflow.js';
56
57
  import { canonicalJson, createCredential, createMemberCredential, signMembershipBinding, digest, validateMembershipBinding, verifyMembershipBinding, verifyCredentialSignature, createRepresentationProof, verifyServiceEvent, buildRequestBinding, validateRequestBinding, CredentialRenewal } from './org-protocol.js';
57
58
  import { SemanticProtocolError } from './semantic-protocol.js';
@@ -491,7 +492,8 @@ export class AUNClient {
491
492
  _ownershipWorkflow = null;
492
493
  _ownershipConsensusCache = new Map();
493
494
  _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) });
495
+ 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)
496
+ throw ownershipFailure('unavailable', `Storage owner.md HTTP PUT failed: ${response.status}`, true); } });
495
497
  }
496
498
  _getOwnershipFiles() {
497
499
  return this._ownershipFiles ??= this._createOwnershipFiles((subAid, stage) => this._agentMdManager.readOwnershipFresh(subAid, stage));
@@ -906,6 +908,8 @@ export class AUNClient {
906
908
  _agentMdManager;
907
909
  _aidStore;
908
910
  _groupIdentityOperations = new Map();
911
+ _entityOwnershipConnection = 0;
912
+ _entityOwnershipChecks = new Map();
909
913
  _groupIndexMetaCache = new GroupIndexMetaCache();
910
914
  /** 消息序列号跟踪器(群消息 + P2P 空洞检测) */
911
915
  _p2pPeerPullEnabled = true;
@@ -990,6 +994,8 @@ export class AUNClient {
990
994
  _reconnectTask = null;
991
995
  _reconnectEventDispatchDepth = 0;
992
996
  _gatewayConnectionId = '';
997
+ _gatewayRuntimeIdentity = null;
998
+ _gatewayReleaseRevision = 0;
993
999
  _negotiatedCapabilities = {};
994
1000
  _riskNegotiated = false;
995
1001
  _riskRestoredAid = '';
@@ -1324,12 +1330,12 @@ export class AUNClient {
1324
1330
  }
1325
1331
  async _localOwnershipOperation(sub, next) {
1326
1332
  const sources = [this, sub, ...(next ? [next] : [])];
1327
- if (sources.some((source) => !(source instanceof AUNClient) || !source._currentAid?.isPrivateKeyValid() || !source._currentAid.isCertValid() || !source._currentAid.privateKeyPem || source._currentAid.aid !== source._aid || source.getType() !== 'agent') || new Set(sources.map((source) => source._aid)).size !== sources.length)
1328
- throw ownershipFailure('forbidden', 'local ownership requires distinct agent clients with private identities');
1333
+ 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)
1334
+ throw ownershipFailure('forbidden', 'local ownership requires a distinct agent subaid and human or agent owners with private identities');
1329
1335
  const identities = sources.map((source) => [source._aid, source._currentAid, source._serviceEventIdentityVersion]);
1330
1336
  const guard = () => { if (sources.some((source, index) => source._aid !== identities[index][0] || source._currentAid !== identities[index][1] || source._serviceEventIdentityVersion !== identities[index][2]))
1331
1337
  throw ownershipFailure('conflict', 'local ownership source identity changed'); };
1332
- for (const source of sources) {
1338
+ for (const [index, source] of sources.entries()) {
1333
1339
  guard();
1334
1340
  const document = await source._agentMdManager.readAgentMdFresh(source._aid);
1335
1341
  guard();
@@ -1337,8 +1343,8 @@ export class AUNClient {
1337
1343
  throw ownershipFailure('forbidden', 'local ownership requires a published agent document');
1338
1344
  const verified = await source._getOwnershipFiles().verifyFile(document.content, 'agent', source._aid);
1339
1345
  guard();
1340
- if (verified.frontmatter.type !== 'agent')
1341
- throw ownershipFailure('forbidden', 'local ownership requires verified agent identities');
1346
+ if ((index === 1 ? verified.frontmatter.type !== 'agent' : typeof verified.frontmatter.type !== 'string' || !['human', 'agent'].includes(verified.frontmatter.type)))
1347
+ throw ownershipFailure('forbidden', 'local ownership requires an agent subaid and human or agent owners');
1342
1348
  }
1343
1349
  const slot = `ownership-${crypto.randomUUID()}`, clients = [];
1344
1350
  try {
@@ -1899,20 +1905,231 @@ export class AUNClient {
1899
1905
  * 自动处理内部方法限制、E2EE 加解密、客户端签名等。
1900
1906
  */
1901
1907
  async call(method, params) {
1908
+ const assertCurrent = ['group.transfer_owner', 'org.lifecycle.transfer', 'group.dissolve', 'org.lifecycle.dissolve'].includes(method) ? this._entityOwnershipGuard() : undefined;
1909
+ const recoverOwnership = ['group.transfer_owner', 'org.lifecycle.transfer', 'group.dissolve', 'org.lifecycle.dissolve'].includes(method) ? await this._prepareEntityOwnershipLifecycle(method, params ?? {}) : undefined;
1910
+ await this._checkEntityOwnershipOnAccess(method, params ?? {});
1902
1911
  const readIdentity = ['message.read', 'message.read_state', 'group.read', 'group.read_state'].includes(method)
1903
1912
  ? this._ensureReadStateIdentity() : null;
1904
1913
  try {
1905
- const result = await this._rpcPipeline.call(method, params);
1914
+ const result = await this._rpcPipeline.call(method, params, assertCurrent);
1906
1915
  await this._safeAsync(this._getCredentialRenewal().observe(method, params ?? {}, result));
1907
1916
  if (readIdentity !== null && readIdentity === this._ensureReadStateIdentity())
1908
1917
  this._observeReadStateResult(method, result, params ?? {});
1909
1918
  return result;
1910
1919
  }
1911
1920
  catch (err) {
1921
+ if (recoverOwnership) {
1922
+ try {
1923
+ await recoverOwnership(err);
1924
+ }
1925
+ catch { }
1926
+ }
1912
1927
  await this._maybeHandleHalfOpenRpcFailure(err);
1913
1928
  throw err;
1914
1929
  }
1915
1930
  }
1931
+ _entityOwnershipGuard() {
1932
+ const owner = this._currentAid, identity = `${this._aid}:${this._serviceEventIdentityVersion}:${this._entityOwnershipConnection}`;
1933
+ return () => { if (`${this._aid}:${this._serviceEventIdentityVersion}:${this._entityOwnershipConnection}` !== identity || this._currentAid !== owner || this._closing || this._disconnecting)
1934
+ throw ownershipFailure('conflict', 'entity ownership connection changed'); };
1935
+ }
1936
+ async _prepareEntityOwnershipLifecycle(method, params) {
1937
+ const type = method.startsWith('group.') ? 'group' : 'org';
1938
+ const aid = String(params.group_aid ?? params.group_id ?? params.org_id ?? '').trim().toLowerCase();
1939
+ if (!aid)
1940
+ return;
1941
+ const owner = this._currentAid;
1942
+ const identity = `${this._aid}:${this._serviceEventIdentityVersion}:${this._entityOwnershipConnection}`;
1943
+ const assertCurrent = () => { if (`${this._aid}:${this._serviceEventIdentityVersion}:${this._entityOwnershipConnection}` !== identity || this._currentAid !== owner)
1944
+ throw ownershipFailure('conflict', 'entity lifecycle identity changed'); };
1945
+ const response = await this._entityLifecycle(aid, type);
1946
+ assertCurrent();
1947
+ const result = response;
1948
+ const entity = (isJsonObject(result.group) ? result.group : isJsonObject(result.organization) ? result.organization : result);
1949
+ if (entity.owner_aid !== this._aid || entity.status !== 'active')
1950
+ return;
1951
+ const target = String(entity.group_aid ?? entity.org_id ?? aid);
1952
+ if (!owner?.isPrivateKeyValid())
1953
+ throw ownershipFailure('unavailable', 'entity lifecycle requires its owner signing identity', true);
1954
+ const observed = await this._agentMdManager.readAgentMdFresh(target);
1955
+ const meta = observed.exists ? validateAgentMdReadDocument(observed.content, { expectedAid: target }).frontmatter : {};
1956
+ if (!meta.ownership)
1957
+ await this._completeEntityOwnership(target, type, undefined, assertCurrent);
1958
+ const storage = this._getOwnershipStorage(), original = await storage.readOwnerMd(owner.aid, target);
1959
+ if (!original.exists)
1960
+ throw ownershipFailure('conflict', 'entity lifecycle owner declaration is missing');
1961
+ const declaration = await this._getOwnershipFiles().verifyFile(original.content, 'owner', target, owner.aid);
1962
+ const transfer = method.endsWith('transfer') || method.endsWith('transfer_owner');
1963
+ const newOwner = String(params.target_member_aid ?? params.new_owner_aid ?? params.new_owner ?? params.aid ?? '').trim().toLowerCase();
1964
+ const action = transfer ? 'transfer_owner' : 'release';
1965
+ if (declaration.action === action && (!transfer || declaration.transfer?.new_owner_aid === newOwner && declaration.transfer.expires_at >= Date.now() / 1000))
1966
+ return;
1967
+ if (declaration.action !== 'declare' || !declaration.statement)
1968
+ throw ownershipFailure('conflict', 'entity lifecycle ownership action changed');
1969
+ 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) } } : {}) };
1970
+ const raw = signOwnershipFile(owner, `---\nowner_aid: ${owner.aid}\nsub_aid: ${target}\nownership: ${JSON.stringify(ownership)}\n---\n`, 'owner');
1971
+ await storage.writeOwnerMd(owner.aid, target, raw, original, assertCurrent);
1972
+ return async (error) => {
1973
+ 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))
1974
+ return;
1975
+ assertCurrent();
1976
+ const response = await this._entityLifecycle(target, type);
1977
+ const data = response, current = (isJsonObject(data.group) ? data.group : isJsonObject(data.organization) ? data.organization : data);
1978
+ if (current.owner_aid !== owner.aid || current.status !== 'active')
1979
+ return;
1980
+ const observed = await storage.readOwnerMd(owner.aid, target);
1981
+ if (!observed.exists || observed.content !== raw)
1982
+ return;
1983
+ assertCurrent();
1984
+ await storage.writeOwnerMd(owner.aid, target, original.content, observed, assertCurrent);
1985
+ };
1986
+ }
1987
+ async _checkEntityOwnershipOnAccess(method, params) {
1988
+ const type = method.startsWith('group.') ? 'group' : method.startsWith('org.') ? 'org' : '';
1989
+ const rawTarget = String(params.group_aid ?? params.group_id ?? params.org_id ?? '').trim().toLowerCase();
1990
+ const target = type === 'group' ? normalizeGroupId(rawTarget, { localIssuer: this._aid?.split('.').slice(1).join('.') }) : rawTarget;
1991
+ 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))
1992
+ return;
1993
+ const identity = `${this._aid}:${this._serviceEventIdentityVersion}:${this._entityOwnershipConnection}`;
1994
+ const key = `${identity}:${target}`;
1995
+ let task = this._entityOwnershipChecks.get(key);
1996
+ if (!task) {
1997
+ task = (async () => {
1998
+ const store = this._aidStore;
1999
+ if (!store)
2000
+ return false;
2001
+ if (!store.load(target).ok) {
2002
+ let pending = null;
2003
+ const keystore = store._keystore;
2004
+ if (type === 'group')
2005
+ for (const id of new Set([target, String(params.group_id ?? target)])) {
2006
+ pending = keystore?.loadPendingGroupBind?.(`bind:${id}`) ?? keystore?.loadPendingGroupBind?.(id);
2007
+ if (pending?.private_key_pem && pending.public_key_der_b64)
2008
+ break;
2009
+ }
2010
+ if (!pending?.private_key_pem || !pending.public_key_der_b64)
2011
+ return false;
2012
+ }
2013
+ const result = await this._entityLifecycle(target, type);
2014
+ const data = result;
2015
+ const entity = (isJsonObject(data.group) ? data.group : isJsonObject(data.organization) ? data.organization : data);
2016
+ if (`${this._aid}:${this._serviceEventIdentityVersion}:${this._entityOwnershipConnection}` !== identity || entity.owner_aid !== this._aid || entity.status !== 'active')
2017
+ return false;
2018
+ const aid = String(entity.group_aid ?? entity.org_id ?? target).toLowerCase();
2019
+ if (aid !== target) {
2020
+ const existing = this._entityOwnershipChecks.get(`${identity}:${aid}`);
2021
+ if (existing && existing !== task)
2022
+ return await existing;
2023
+ this._entityOwnershipChecks.set(`${identity}:${aid}`, task);
2024
+ }
2025
+ const consensus = await this.checkOwnershipConsensus(aid, true);
2026
+ if (consensus.status === 'consensus' && consensus.owner_aid === this._aid)
2027
+ return true;
2028
+ await this._completeEntityOwnership(aid, type, undefined, () => {
2029
+ if (`${this._aid}:${this._serviceEventIdentityVersion}:${this._entityOwnershipConnection}` !== identity || this._closing || this._disconnecting)
2030
+ throw ownershipFailure('conflict', 'entity ownership connection changed');
2031
+ });
2032
+ return true;
2033
+ })().catch(() => false);
2034
+ this._entityOwnershipChecks.set(key, task);
2035
+ }
2036
+ await task;
2037
+ }
2038
+ async _completeEntityOwnership(aid, type, content, assertCurrent = () => { }) {
2039
+ const store = this._aidStore;
2040
+ const owner = this._currentAid;
2041
+ const identity = `${this._aid}:${this._serviceEventIdentityVersion}:${this._entityOwnershipConnection}`, guard = assertCurrent;
2042
+ assertCurrent = () => { guard(); if (`${this._aid}:${this._serviceEventIdentityVersion}:${this._entityOwnershipConnection}` !== identity || this._currentAid !== owner || this._aidStore !== store)
2043
+ throw ownershipFailure('conflict', 'entity ownership connection changed'); };
2044
+ if (!store || !owner?.isPrivateKeyValid())
2045
+ throw ownershipFailure('unavailable', 'entity ownership requires the current owner private key', true);
2046
+ const key = `${identity}:${aid}`, pending = this._entityOwnershipChecks.get(key);
2047
+ if (content !== undefined && pending) {
2048
+ await pending;
2049
+ assertCurrent();
2050
+ }
2051
+ if (!this._entityOwnershipChecks.has(key))
2052
+ this._entityOwnershipChecks.set(key, Promise.resolve(false));
2053
+ const authority = await this._entityLifecycle(aid, type);
2054
+ const result = authority;
2055
+ const entity = (isJsonObject(result.group) ? result.group : isJsonObject(result.organization) ? result.organization : result);
2056
+ if (entity.owner_aid !== owner.aid || entity.status !== 'active')
2057
+ throw ownershipFailure('forbidden', 'entity ownership requires its current active lifecycle owner');
2058
+ assertCurrent();
2059
+ let local = store.load(aid);
2060
+ if ((!local.ok || !local.data.aid.isPrivateKeyValid()) && type === 'group') {
2061
+ const keystore = store._keystore;
2062
+ let pending = null;
2063
+ for (const id of new Set([String(entity.group_id ?? aid), aid])) {
2064
+ pending = keystore?.loadPendingGroupBind?.(`bind:${id}`) ?? keystore?.loadPendingGroupBind?.(id);
2065
+ if (pending?.private_key_pem && pending.public_key_der_b64)
2066
+ break;
2067
+ }
2068
+ if (pending?.private_key_pem && pending.public_key_der_b64) {
2069
+ const certificate = await this._freshEntityCertificate(aid, type);
2070
+ await this.verifyCertPublicKeyMatch(certificate.certPem, pending.public_key_der_b64, 'entity ownership recovery');
2071
+ assertCurrent();
2072
+ const imported = store.importGroupIdentity(aid, { ...pending, cert_pem: certificate.certPem });
2073
+ if (!imported.ok)
2074
+ throw ownershipFailure('unavailable', imported.error.message, true);
2075
+ local = store.load(aid);
2076
+ }
2077
+ }
2078
+ if (!local.ok || !local.data.aid.isPrivateKeyValid())
2079
+ throw ownershipFailure('unavailable', 'entity ownership requires its local private key', true);
2080
+ const observed = await this._agentMdManager.readAgentMdFresh(aid);
2081
+ const current = observed.exists ? validateAgentMdReadDocument(observed.content, { expectedAid: aid }).frontmatter : {};
2082
+ const previous = isJsonObject(current.ownership) && isJsonObject(current.ownership.statement) ? current.ownership.statement : undefined;
2083
+ if (!previous || previous.owner_aid === owner.aid) {
2084
+ const consensus = await this.checkOwnershipConsensus(aid, true);
2085
+ assertCurrent();
2086
+ if (consensus.status === 'consensus' && consensus.owner_aid === owner.aid && content === undefined)
2087
+ return observed.content;
2088
+ if (!['consensus', 'unbound', 'single_sided'].includes(consensus.status))
2089
+ throw ownershipFailure(consensus.code ?? 'conflict', consensus.reason ?? 'entity ownership cannot be repaired from current materials');
2090
+ }
2091
+ let version = Number(previous?.relation_version ?? 1);
2092
+ if (previous && previous.owner_aid !== owner.aid) {
2093
+ const authorization = await this._getOwnershipFiles().readOwner(String(previous.owner_aid), aid);
2094
+ if (!authorization.exists)
2095
+ throw ownershipFailure('conflict', 'entity transfer authorization is missing');
2096
+ const verified = await this._getOwnershipFiles().verifyFile(authorization.content, 'owner', aid, String(previous.owner_aid));
2097
+ 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)
2098
+ throw ownershipFailure('conflict', 'entity transfer authorization does not match');
2099
+ version++;
2100
+ }
2101
+ const storage = this._getOwnershipStorage();
2102
+ const original = await storage.readOwnerMd(owner.aid, aid);
2103
+ 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) };
2104
+ if (original.exists) {
2105
+ const verified = await this._getOwnershipFiles().verifyFile(original.content, 'owner', aid, owner.aid);
2106
+ if (verified.action !== 'declare' || verified.statement?.relation_version !== version || verified.statement?.owner_aid !== owner.aid || verified.statement?.sub_aid !== aid)
2107
+ throw ownershipFailure('conflict', 'entity owner declaration changed');
2108
+ statement = verified.statement;
2109
+ }
2110
+ else {
2111
+ const payload = `---\nowner_aid: ${owner.aid}\nsub_aid: ${aid}\nownership: ${JSON.stringify({ statement, action: 'declare' })}\n---\n`;
2112
+ const raw = signOwnershipFile(owner, payload, 'owner');
2113
+ assertCurrent();
2114
+ await storage.writeOwnerMd(owner.aid, aid, raw, original, assertCurrent);
2115
+ }
2116
+ const source = content ?? (observed.exists ? observed.content : buildDefaultAgentMd(aid, { type, name: String(entity.name ?? aid) }));
2117
+ const updated = updateOwnershipAgentMd(source, { owner: owner.aid, ownership: { statement } }).replace(/^entity_cert_fingerprint:.*(?:\r?\n|$)/m, '');
2118
+ const signed = signOwnershipFile(local.data.aid, updated, 'agent');
2119
+ if (content === undefined) {
2120
+ if (!previous || previous.owner_aid !== owner.aid || previous.relation_version !== version) {
2121
+ assertCurrent();
2122
+ const uploaded = await store._uploadAgentMd(aid, signed, { uploaderAid: owner.aid, sourceGuard: assertCurrent, entityOwnerResolver: () => this._entityLifecycleOwner(aid, type), entityIdentityGetter: () => this._freshEntityCertificate(aid, type) });
2123
+ if (!uploaded.ok)
2124
+ throw ownershipFailure('unavailable', uploaded.error.message, true);
2125
+ }
2126
+ const consensus = await this.checkOwnershipConsensus(aid, true);
2127
+ assertCurrent();
2128
+ if (consensus.status !== 'consensus' || consensus.owner_aid !== owner.aid || consensus.relation_version !== version)
2129
+ throw ownershipFailure('unavailable', 'entity ownership publication could not be confirmed', true);
2130
+ }
2131
+ return signed;
2132
+ }
1916
2133
  async _maybeHandleHalfOpenRpcFailure(err) {
1917
2134
  if (this._closing || this.state !== ConnectionState.READY)
1918
2135
  return;
@@ -1926,44 +2143,17 @@ export class AUNClient {
1926
2143
  if (!(err instanceof AUNError) || err.code !== -32003)
1927
2144
  return false;
1928
2145
  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;
2146
+ for (const prefix of ['valid group identity certificate is required for ', 'valid group agent.md is required for ']) {
2147
+ if (message.toLowerCase().startsWith(prefix)) {
2148
+ if (_method === 'group.get_info')
2149
+ return false;
2150
+ const groupId = String(params.group_aid ?? params.groupAid ?? params.group_id ?? params.groupId ?? message.slice(prefix.length)).trim();
2151
+ const target = normalizeGroupId(groupId.toLowerCase(), { localIssuer: this._aid?.split('.').slice(1).join('.') });
2152
+ const key = `${this._aid}:${this._serviceEventIdentityVersion}:${this._entityOwnershipConnection}:${target}`;
2153
+ if (!this._entityOwnershipChecks.has(key))
2154
+ await this._checkEntityOwnershipOnAccess('group.get_info', { group_id: groupId });
2155
+ return await this._entityOwnershipChecks.get(key) ?? false;
1948
2156
  }
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
2157
  }
1968
2158
  const agentPrefix = 'valid agent.md is required for ';
1969
2159
  if (!message.toLowerCase().startsWith(agentPrefix))
@@ -2008,79 +2198,26 @@ export class AUNClient {
2008
2198
  if (!store)
2009
2199
  return;
2010
2200
  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
2201
  }
2068
- async _runGroupIdentityOperation(groupId, operation) {
2202
+ async _runGroupIdentityOperation(groupId, operation, context = '') {
2069
2203
  const aid = String(this._aid ?? '').trim();
2070
2204
  const dot = aid.indexOf('.');
2071
2205
  const key = normalizeGroupId(groupId, { localIssuer: dot >= 0 ? aid.slice(dot + 1) : '' }).toLowerCase();
2072
2206
  if (!key)
2073
2207
  return operation();
2074
- let task = this._groupIdentityOperations.get(key);
2208
+ const pending = this._groupIdentityOperations.get(key);
2209
+ if (pending && pending.context !== context)
2210
+ throw ownershipFailure('conflict', 'pending Org creation context changed');
2211
+ let task = pending?.task;
2075
2212
  if (!task) {
2076
2213
  task = Promise.resolve().then(operation);
2077
- this._groupIdentityOperations.set(key, task);
2214
+ this._groupIdentityOperations.set(key, { task, context });
2078
2215
  }
2079
2216
  try {
2080
2217
  return await task;
2081
2218
  }
2082
2219
  finally {
2083
- if (this._groupIdentityOperations.get(key) === task)
2220
+ if (this._groupIdentityOperations.get(key)?.task === task)
2084
2221
  this._groupIdentityOperations.delete(key);
2085
2222
  }
2086
2223
  }
@@ -2111,6 +2248,9 @@ export class AUNClient {
2111
2248
  return refreshed;
2112
2249
  }
2113
2250
  async createOrg(params = {}, options = {}) {
2251
+ 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 ?? '' })));
2252
+ }
2253
+ async _createOrg(params, options) {
2114
2254
  const store = options.aidStore;
2115
2255
  const owner = this._currentAid;
2116
2256
  const orgAid = String(params.org_id ?? '').trim();
@@ -2334,21 +2474,28 @@ export class AUNClient {
2334
2474
  return result;
2335
2475
  }
2336
2476
  async _uploadOrgAgentMd(store, orgAid, params, initialResult) {
2477
+ const assertCurrent = this._entityOwnershipGuard();
2337
2478
  const owner = this._currentAid;
2338
2479
  if (!owner)
2339
2480
  throw ownershipFailure('forbidden', 'Org agent.md requires current owner');
2340
2481
  const organization = isJsonObject(initialResult) && isJsonObject(initialResult.organization) ? initialResult.organization : undefined;
2482
+ if (params.transfer_id && (organization?.org_id !== orgAid || organization.owner_aid !== owner.aid || organization.status !== 'active' || !isJsonObject(initialResult) || initialResult.transfer_id !== params.transfer_id))
2483
+ throw ownershipFailure('conflict', 'Org transfer response does not match the completed transfer');
2341
2484
  const bootstrap = organization?.org_id === orgAid && organization.owner_aid === owner.aid && organization.status === 'active'
2342
2485
  ? { 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
2486
  if (await this._resumeGroupAgentMdUpload(store, orgAid, bootstrap))
2344
2487
  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));
2488
+ this._aidStore = store;
2489
+ 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);
2490
+ assertCurrent();
2491
+ 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
2492
  if (!uploaded.ok) {
2349
2493
  const code = ['invalid', 'unavailable', 'forbidden', 'conflict', 'persistence_unavailable'].includes(uploaded.error.code) ? uploaded.error.code : uploaded.error.code === 'NETWORK_ERROR' ? 'unavailable' : 'invalid';
2350
2494
  throw ownershipFailure(code, uploaded.error.message, code === 'unavailable');
2351
2495
  }
2496
+ const consensus = await this.checkOwnershipConsensus(orgAid, true);
2497
+ if (consensus.status !== 'consensus' || consensus.owner_aid !== owner.aid)
2498
+ throw ownershipFailure('unavailable', 'Org ownership publication could not be confirmed', true);
2352
2499
  }
2353
2500
  async createGroup(params = {}, options = {}) {
2354
2501
  if (!isJsonObject(params)) {
@@ -2529,13 +2676,14 @@ export class AUNClient {
2529
2676
  ].join('\n');
2530
2677
  }
2531
2678
  async _resumeGroupAgentMdUpload(store, groupAid, bootstrap) {
2679
+ const assertCurrent = this._entityOwnershipGuard();
2532
2680
  const identityStore = store;
2533
2681
  const pendingManager = identityStore._agentMdManager ?? this._agentMdManager;
2534
2682
  const pending = pendingManager.pendingUpload(groupAid);
2535
2683
  if (!pending)
2536
2684
  return false;
2537
2685
  const fields = validateAgentMdDocument(pending.content, { expectedAid: groupAid, validateOwnership: false }).frontmatter;
2538
- if (['ownership', 'ownership_proof', 'ownership_release'].some((field) => Object.hasOwn(fields, field))) {
2686
+ if (!['group', 'org'].includes(String(fields.type)) && ['ownership', 'ownership_proof', 'ownership_release'].some((field) => Object.hasOwn(fields, field))) {
2539
2687
  throw new StateError('ownership pending agent.md requires its persisted 2.1 task', { stringCode: 'unsupported_protocol' });
2540
2688
  }
2541
2689
  const identity = this._currentAid;
@@ -2549,6 +2697,7 @@ export class AUNClient {
2549
2697
  uploadViaGatewayHost: true,
2550
2698
  ownerAidGetter: () => identity.aid,
2551
2699
  currentAidGetter: () => identity,
2700
+ sourceGuard: assertCurrent,
2552
2701
  entityOwnerResolver: (aid, type) => this._resolveEntityAgentMdOwner(aid, type, bootstrap?.completedTransfer || pending.condition?.if_none_match === '*' ? bootstrap : undefined),
2553
2702
  entityIdentityGetter: (aid, type) => this._freshEntityCertificate(aid, type),
2554
2703
  entitySignerResolver: (aid, fingerprint) => this._freshEntitySigner(aid, fingerprint),
@@ -2562,10 +2711,18 @@ export class AUNClient {
2562
2711
  },
2563
2712
  aidValidator: (aid) => identityStore._registerFlow.validateAidName(aid),
2564
2713
  });
2565
- return await manager.resumePendingUpload(groupAid);
2714
+ const resumed = await manager.resumePendingUpload(groupAid);
2715
+ assertCurrent();
2716
+ if (resumed) {
2717
+ const consensus = await this.checkOwnershipConsensus(groupAid, true);
2718
+ if (consensus.status !== 'consensus' || consensus.owner_aid !== identity.aid)
2719
+ throw ownershipFailure('unavailable', 'entity ownership recovery could not be confirmed', true);
2720
+ }
2721
+ return resumed;
2566
2722
  }
2567
2723
  async _uploadGroupAgentMd(store, groupAid, params, group, initialCertificate) {
2568
2724
  const uploaderAid = String(this._aid ?? '').trim();
2725
+ const assertCurrent = this._entityOwnershipGuard();
2569
2726
  if (!uploaderAid) {
2570
2727
  throw new ValidationError('group agent.md upload requires current owner AID');
2571
2728
  }
@@ -2573,11 +2730,14 @@ export class AUNClient {
2573
2730
  const bootstrap = initialCertificate ? { aid: groupAid, ownerAid: String(group.owner_aid ?? '').trim(), certFingerprint: certFingerprint(initialCertificate), type: 'group' } : undefined;
2574
2731
  if (await this._resumeGroupAgentMdUpload(store, groupAid, bootstrap))
2575
2732
  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));
2733
+ 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
2734
  if (!uploaded.ok) {
2578
2735
  const code = ['invalid', 'unavailable', 'forbidden', 'conflict', 'persistence_unavailable'].includes(uploaded.error.code) ? uploaded.error.code : uploaded.error.code === 'NETWORK_ERROR' ? 'unavailable' : 'invalid';
2579
2736
  throw ownershipFailure(code, `group agent.md upload failed: ${uploaded.error.message}`, code === 'unavailable');
2580
2737
  }
2738
+ const consensus = await this.checkOwnershipConsensus(groupAid, true);
2739
+ if (consensus.status !== 'consensus' || consensus.owner_aid !== uploaderAid)
2740
+ throw ownershipFailure('unavailable', 'group ownership publication could not be confirmed', true);
2581
2741
  }
2582
2742
  _localEntityCertificate(aid) {
2583
2743
  const loaded = this._aidStore?.load(aid);
@@ -2599,7 +2759,7 @@ export class AUNClient {
2599
2759
  try {
2600
2760
  authority = bootstrap?.completedTransfer && type === 'org' && bootstrap.aid === aid && bootstrap.ownerAid === this._currentAid?.aid
2601
2761
  ? { owner_aid: bootstrap.ownerAid, entity_cert_fingerprint: bootstrap.certFingerprint }
2602
- : await this._agentMdManager.readEntityAuthorityFresh(aid);
2762
+ : { owner_aid: await this._entityLifecycleOwner(aid, type), entity_cert_fingerprint: (await this._freshEntityCertificate(aid, type)).certFingerprint };
2603
2763
  }
2604
2764
  catch (error) {
2605
2765
  if (!bootstrap || bootstrap.aid !== aid || type !== (bootstrap.type ?? 'org') || !(error instanceof AUNError) || !isJsonObject(error.data) || error.data.http_status !== 404)
@@ -2613,6 +2773,26 @@ export class AUNClient {
2613
2773
  throw ownershipFailure('conflict', 'entity authority changed during current-certificate verification');
2614
2774
  return authority.owner_aid;
2615
2775
  }
2776
+ async _entityLifecycle(aid, type) {
2777
+ const method = type === 'group' ? 'group.get_info' : 'org.lifecycle.get';
2778
+ let params = type === 'group' ? { group_id: aid, required: ['member'] } : { org_id: aid };
2779
+ const signer = this._currentAid;
2780
+ if (type === 'org' && signer) {
2781
+ const records = this._tokenStore.loadMetadata?.(signer.aid)?.credential_renewals_v1 ?? {};
2782
+ 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);
2783
+ if (record)
2784
+ 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);
2785
+ }
2786
+ return this._rpcPipeline.call(method, params);
2787
+ }
2788
+ async _entityLifecycleOwner(aid, type) {
2789
+ const response = await this._entityLifecycle(aid, type);
2790
+ const data = response;
2791
+ const entity = (isJsonObject(data.group) ? data.group : isJsonObject(data.organization) ? data.organization : data);
2792
+ if (entity.status !== 'active' || typeof entity.owner_aid !== 'string')
2793
+ throw ownershipFailure('unavailable', 'entity lifecycle owner is unavailable', true);
2794
+ return entity.owner_aid;
2795
+ }
2616
2796
  async bindGroupAid(params = {}, options = {}) {
2617
2797
  if (!isJsonObject(params)) {
2618
2798
  throw new ValidationError('bindGroupAid params must be an object');
@@ -3386,8 +3566,11 @@ export class AUNClient {
3386
3566
  this._delivery.markPublishedSeq(ns, seq);
3387
3567
  }
3388
3568
  _publishAppEvent(event, payload, source = 'direct', deliveryContext) {
3389
- if (messageClassOf(payload) === 'sdk_control')
3390
- return this._controlMessages().consume(payload);
3569
+ if (messageClassOf(payload) === 'sdk_control') {
3570
+ if (event === 'message.received')
3571
+ return this._controlMessages().consume(payload);
3572
+ return;
3573
+ }
3391
3574
  if (event === 'message.received') {
3392
3575
  return this._handleOwnershipMessage(payload).then((handled) => {
3393
3576
  if (!handled)
@@ -4325,13 +4508,17 @@ export class AUNClient {
4325
4508
  const extraInfo = (params.extra_info && typeof params.extra_info === 'object' && !Array.isArray(params.extra_info))
4326
4509
  ? { ...params.extra_info }
4327
4510
  : {};
4511
+ const capabilities = isJsonObject(extraInfo._capabilities) ? { ...extraInfo._capabilities } : {};
4512
+ delete capabilities.message_classes_v1;
4328
4513
  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
- };
4514
+ capabilities.message_classes_v2 = { p2p: ['normal', 'control', 'sdk_control'], group: ['normal', 'control'] };
4515
+ capabilities.sdk_control_protocols = { 'aun.agent_md.changed': [1] };
4334
4516
  }
4517
+ else {
4518
+ delete capabilities.message_classes_v2;
4519
+ delete capabilities.sdk_control_protocols;
4520
+ }
4521
+ extraInfo._capabilities = capabilities;
4335
4522
  const prevState = this._state;
4336
4523
  this._auth.setInstanceContext({ deviceId: this._deviceId, slotId: this._slotId });
4337
4524
  this._state = 'connecting';
@@ -4380,6 +4567,7 @@ export class AUNClient {
4380
4567
  if (isJsonObject(auth.hello) && 'heartbeat_interval' in auth.hello) {
4381
4568
  this._applyServerHeartbeatInterval(auth.hello.heartbeat_interval, 'auth');
4382
4569
  }
4570
+ this._bindGatewayRuntimeIdentity(auth.hello, params);
4383
4571
  this._gatewayConnectionId = isJsonObject(auth.hello) && isJsonObject(auth.hello.connection) ? String(auth.hello.connection.id ?? '') : '';
4384
4572
  this._negotiatedCapabilities = isJsonObject(auth.hello) && isJsonObject(auth.hello.negotiated_capabilities) ? auth.hello.negotiated_capabilities : {};
4385
4573
  if (isJsonObject(auth.hello))
@@ -4401,6 +4589,7 @@ export class AUNClient {
4401
4589
  if (isJsonObject(hello) && 'heartbeat_interval' in hello) {
4402
4590
  this._applyServerHeartbeatInterval(hello.heartbeat_interval, 'auth');
4403
4591
  }
4592
+ this._bindGatewayRuntimeIdentity(hello, params);
4404
4593
  this._gatewayConnectionId = isJsonObject(hello) && isJsonObject(hello.connection) ? String(hello.connection.id ?? '') : '';
4405
4594
  this._negotiatedCapabilities = isJsonObject(hello) && isJsonObject(hello.negotiated_capabilities) ? hello.negotiated_capabilities : {};
4406
4595
  if (isJsonObject(hello))
@@ -4479,6 +4668,8 @@ export class AUNClient {
4479
4668
  this._clientLog.warn(`schedule post-connect P2P gap fill failed: ${formatCaughtError(exc)}`);
4480
4669
  });
4481
4670
  }
4671
+ this._entityOwnershipConnection++;
4672
+ this._entityOwnershipChecks.clear();
4482
4673
  if (!this._ownershipOperation)
4483
4674
  this._safeAsync(this._checkIdentityAdmissionAfterConnect());
4484
4675
  this._clientLog.debug(`_connectOnce exit: elapsed=${Date.now() - tStart}ms gateway=${gatewayUrl}, aid=${this._aid ?? ''}`);
@@ -5200,13 +5391,17 @@ export class AUNClient {
5200
5391
  let lastErr = null;
5201
5392
  for (const url of candidates) {
5202
5393
  try {
5394
+ const requestUrl = new URL(url);
5395
+ if (resolvedAid === this._aid) {
5396
+ requestUrl.search = new URLSearchParams({ aid: resolvedAid, sdk_lang: 'typescript', sdk_version: VERSION, capabilities: 'gateway_migrate_v1' }).toString();
5397
+ }
5203
5398
  // discoverAll 是生产路径;若调用方注入了旧版/测试 discovery
5204
5399
  // 替身而只提供 discover,则保留单候选兼容,不影响多候选发现。
5205
5400
  const hasInjectedDiscover = Object.prototype.hasOwnProperty.call(discovery, 'discover')
5206
5401
  && typeof discovery.discover === 'function';
5207
5402
  const discovered = hasInjectedDiscover
5208
- ? [await discovery.discover(url)]
5209
- : await discovery.discoverAll(url);
5403
+ ? [await discovery.discover(requestUrl.toString())]
5404
+ : await discovery.discoverAll(requestUrl.toString());
5210
5405
  if (!discoveryIsCurrent())
5211
5406
  throw new StateError('gateway discovery superseded by identity change');
5212
5407
  const gatewayUrls = [...new Set(discovered.map((item) => String(item ?? '').trim()).filter(Boolean))];
@@ -5560,6 +5755,18 @@ export class AUNClient {
5560
5755
  // ── 内部:断线重连 ────────────────────────────────────────
5561
5756
  /** 不重连 close code 集合:认证失败/权限错误/被踢等,重连无意义 */
5562
5757
  static _NO_RECONNECT_CODES = new Set([4001, 4003, 4008, 4009, 4010, 4011, 4013, 4014, 4015]);
5758
+ _bindGatewayRuntimeIdentity(hello, params) {
5759
+ const actual = isJsonObject(hello) && isJsonObject(hello.runtime_identity) ? hello.runtime_identity : null;
5760
+ const metadata = this._discovery._gatewayMetadata?.get(this._gatewayUrl ?? '');
5761
+ 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]]));
5762
+ if (isJsonObject(expected) && Object.entries(expected).some(([key, value]) => value !== actual?.[key])) {
5763
+ throw new ConnectionError('gateway runtime identity changed');
5764
+ }
5765
+ const previousRevision = this._gatewayRuntimeIdentity?.cluster_id === actual?.cluster_id ? this._gatewayReleaseRevision : 0;
5766
+ const revision = params._expected_gateway_release_revision ?? metadata?.release_revision;
5767
+ this._gatewayReleaseRevision = Number.isSafeInteger(revision) && Number(revision) > 0 ? Math.max(previousRevision, Number(revision)) : previousRevision;
5768
+ this._gatewayRuntimeIdentity = actual && Object.keys(actual).length > 0 ? actual : null;
5769
+ }
5563
5770
  _onGatewayMigrate(input) {
5564
5771
  const data = input;
5565
5772
  if (!isJsonObject(data) || this._closing || this._disconnecting || this._connectAbort || this._reconnectActive
@@ -5570,6 +5777,7 @@ export class AUNClient {
5570
5777
  const now = Date.now() / 1000;
5571
5778
  if (typeof data.migration_id !== 'string' || !data.migration_id.trim()
5572
5779
  || typeof data.revision !== 'number' || !Number.isSafeInteger(data.revision) || data.revision <= 0
5780
+ || data.revision < this._gatewayReleaseRevision
5573
5781
  || typeof data.issuer !== 'string' || data.issuer.trim().toLowerCase().replace(/\.$/, '') !== issuer
5574
5782
  || data.aid !== aid || data.device_id !== this._deviceId || data.slot_id !== this._slotId
5575
5783
  || data.connection_id !== this._gatewayConnectionId || typeof data.gateway_url !== 'string'
@@ -5589,6 +5797,14 @@ export class AUNClient {
5589
5797
  || !(hostname === issuer || hostname.endsWith(`.${issuer}`))
5590
5798
  || !hostname.split('.').every((label) => /^[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?$/.test(label)))
5591
5799
  return;
5800
+ if (this._gatewayReleaseRevision > 0 || ['cluster_id', 'source', 'target', 'release_revision'].some((key) => key in data)) {
5801
+ const source = data.source, runtime = this._gatewayRuntimeIdentity, destination = data.target;
5802
+ if (!runtime || !isJsonObject(source) || !isJsonObject(destination) || !data.cluster_id
5803
+ || data.cluster_id !== runtime.cluster_id || destination.cluster_id !== data.cluster_id || data.release_revision !== data.revision
5804
+ || ['cluster_id', 'node_id', 'node_incarnation', 'service_instance_id'].some((key) => typeof source[key] !== 'string' || !String(source[key]).trim() || source[key] !== runtime[key]
5805
+ || typeof destination[key] !== 'string' || !String(destination[key]).trim()))
5806
+ return;
5807
+ }
5592
5808
  const identity = JSON.stringify([this._aid, this._deviceId, this._slotId]);
5593
5809
  const previous = this._migrationRevision.get(identity);
5594
5810
  if (previous && (data.revision <= previous.revision || data.migration_id === previous.migrationId))
@@ -5653,7 +5869,8 @@ export class AUNClient {
5653
5869
  this._state = 'reconnecting';
5654
5870
  this._migrationPending = true;
5655
5871
  try {
5656
- await wait(this._connectOnce({ ...this._sessionParams, gateway, gateways: candidates }, true, owner));
5872
+ await wait(this._connectOnce({ ...this._sessionParams, gateway, gateways: candidates,
5873
+ ...(attempt === 1 && index === 0 && isJsonObject(data.target) ? { _expected_gateway_runtime_identity: data.target, _expected_gateway_release_revision: data.release_revision } : {}), }, true, owner));
5657
5874
  if (!current())
5658
5875
  return;
5659
5876
  if (!this._transport.isConnected())
@@ -5673,7 +5890,7 @@ export class AUNClient {
5673
5890
  if (attempt === 1 && index === 0) {
5674
5891
  try {
5675
5892
  const discovered = await wait(this._resolveGatewayCandidatesForAid(String(data.aid), true, owner));
5676
- candidates = [...candidates, ...discovered.filter((item) => !candidates.includes(item))];
5893
+ candidates = [...candidates, ...discovered.filter((item) => item === gateway || !candidates.includes(item))];
5677
5894
  }
5678
5895
  catch (discoveryError) {
5679
5896
  lastError = discoveryError;
@@ -6014,7 +6231,10 @@ export class AUNClient {
6014
6231
  this._sessionParams.access_token = '';
6015
6232
  }
6016
6233
  }
6017
- const candidates = this._orderedReconnectGateways(this._sessionParams);
6234
+ const candidates = await this._lifecycle.waitForAttempt(this._resolveGatewayCandidatesForAid(this._aid, true, reconnectAbort ?? undefined), undefined, reconnectAbort ?? undefined);
6235
+ if (!this._ownsReconnect(reconnectAbort))
6236
+ return;
6237
+ this._sessionParams.gateways = [...candidates];
6018
6238
  let lastGatewayError = null;
6019
6239
  let connected = false;
6020
6240
  for (const gateway of candidates) {