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