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