@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.
- package/CHANGELOG.md +44 -0
- package/_packed_docs/CHANGELOG.md +44 -0
- package/_packed_docs/INDEX.md +11 -2
- package/_packed_docs/KITE_DOCS_GUIDE.md +10 -8
- 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
- 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
- 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
- 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
- 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
- package/_packed_docs/sdk/AUN_DOCS_GUIDE.md +5 -3
- package/_packed_docs/sdk/INDEX.md +9 -1
- 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
- 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
- package/dist/agent-md-schema.js +7 -6
- package/dist/agent-md-schema.js.map +1 -1
- package/dist/agent-md.d.ts +2 -0
- package/dist/agent-md.js +33 -10
- package/dist/agent-md.js.map +1 -1
- package/dist/aid-store.d.ts +1 -0
- package/dist/aid-store.js +6 -3
- package/dist/aid-store.js.map +1 -1
- package/dist/aid.js +1 -1
- package/dist/aid.js.map +1 -1
- package/dist/auth.js +1 -1
- package/dist/auth.js.map +1 -1
- package/dist/client/control-messages.d.ts +0 -2
- package/dist/client/control-messages.js +79 -108
- package/dist/client/control-messages.js.map +1 -1
- package/dist/client/lifecycle.js +11 -3
- package/dist/client/lifecycle.js.map +1 -1
- package/dist/client/rpc-pipeline.d.ts +1 -1
- package/dist/client/rpc-pipeline.js +17 -5
- package/dist/client/rpc-pipeline.js.map +1 -1
- package/dist/client/v2-e2ee.js +7 -7
- package/dist/client/v2-e2ee.js.map +1 -1
- package/dist/client.d.ts +12 -0
- package/dist/client.js +346 -126
- package/dist/client.js.map +1 -1
- package/dist/config.js +23 -18
- package/dist/config.js.map +1 -1
- package/dist/discovery.d.ts +1 -0
- package/dist/discovery.js +3 -1
- package/dist/discovery.js.map +1 -1
- package/dist/facades.d.ts +20 -0
- package/dist/facades.js +9 -0
- package/dist/facades.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js.map +1 -1
- package/dist/keystore/aid-db.js +14 -2
- package/dist/keystore/aid-db.js.map +1 -1
- package/dist/keystore/index.d.ts +3 -0
- package/dist/keystore/index.js +61 -1
- package/dist/keystore/index.js.map +1 -1
- package/dist/keystore/local-identity-store.js +112 -76
- package/dist/keystore/local-identity-store.js.map +1 -1
- package/dist/keystore/local-token-store.js +15 -26
- package/dist/keystore/local-token-store.js.map +1 -1
- package/dist/ownership-v21.d.ts +2 -2
- package/dist/ownership-v21.js +18 -21
- package/dist/ownership-v21.js.map +1 -1
- package/dist/ownership-workflow.js +7 -4
- package/dist/ownership-workflow.js.map +1 -1
- package/dist/register-flow.js +4 -3
- package/dist/register-flow.js.map +1 -1
- package/dist/risk-control.js +2 -2
- package/dist/risk-control.js.map +1 -1
- package/dist/secret-store/file-store.js +68 -56
- package/dist/secret-store/file-store.js.map +1 -1
- package/dist/semantic-protocol.d.ts +2 -12
- package/dist/semantic-protocol.js +31 -48
- package/dist/semantic-protocol.js.map +1 -1
- package/dist/storage/lowlevel.d.ts +4 -0
- package/dist/storage/lowlevel.js +62 -1
- package/dist/storage/lowlevel.js.map +1 -1
- package/dist/storage/vfs.d.ts +4 -0
- package/dist/storage/vfs.js +55 -12
- package/dist/storage/vfs.js.map +1 -1
- package/dist/tools/cross-sdk-agent.d.ts +1 -1
- package/dist/tools/cross-sdk-agent.js +14 -3
- package/dist/tools/cross-sdk-agent.js.map +1 -1
- package/dist/transport.d.ts +1 -1
- package/dist/transport.js +8 -8
- package/dist/transport.js.map +1 -1
- package/dist/validators.js +9 -8
- package/dist/validators.js.map +1 -1
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- 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.
|
|
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
|
|
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
|
|
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
|
|
1930
|
-
|
|
1931
|
-
|
|
1932
|
-
|
|
1933
|
-
|
|
1934
|
-
|
|
1935
|
-
|
|
1936
|
-
|
|
1937
|
-
|
|
1938
|
-
|
|
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
|
-
|
|
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
|
-
|
|
2346
|
-
const content = String(params.org_agent_md ?? buildDefaultAgentMd(orgAid, { type: 'org', owner: owner.aid,
|
|
2347
|
-
|
|
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
|
-
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
4330
|
-
|
|
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(
|
|
5209
|
-
: await discovery.discoverAll(
|
|
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
|
|
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.
|
|
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) {
|