@agentunion/fastaun 0.5.25 → 0.5.27
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 +41 -0
- package/_packed_docs/CHANGELOG.md +41 -0
- package/_packed_docs/INDEX.md +22 -14
- package/_packed_docs/KITE_DOCS_GUIDE.md +11 -6
- 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/cli//345/207/255/350/257/201/344/270/216/347/273/204/347/273/207/346/234/215/345/212/241CLI/345/274/200/345/217/221/344/270/216/344/275/277/347/224/250/346/214/207/345/215/227.md +869 -0
- 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/0.5.27-SDK/345/272/224/347/224/250/351/233/206/346/210/220/346/214/207/345/215/227.md +408 -0
- package/_packed_docs/sdk/0.5.27-SDK/345/274/200/345/217/221/344/270/216/347/247/273/346/244/215/346/214/207/345/215/227.md +410 -0
- 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 +21 -13
- package/_packed_docs/sdk/agent.md/344/270/216group.index/347/232/204ETag/350/247/202/345/257/237/344/270/216/345/220/214/346/255/245/346/234/272/345/210/266.md +240 -0
- 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-schema.js +1 -1
- package/dist/agent-md-schema.js.map +1 -1
- package/dist/agent-md.js +5 -0
- package/dist/agent-md.js.map +1 -1
- package/dist/aid.js +3 -3
- package/dist/aid.js.map +1 -1
- package/dist/auth.js +21 -19
- 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/lifecycle.d.ts +1 -1
- package/dist/client/lifecycle.js.map +1 -1
- package/dist/client/rpc-pipeline.js +8 -1
- package/dist/client/rpc-pipeline.js.map +1 -1
- package/dist/client/v2-e2ee.js +7 -0
- package/dist/client/v2-e2ee.js.map +1 -1
- package/dist/client.d.ts +12 -0
- package/dist/client.js +305 -29
- package/dist/client.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/keystore/local-identity-store.js +16 -1
- package/dist/keystore/local-identity-store.js.map +1 -1
- package/dist/keystore/local-token-store.js +16 -1
- package/dist/keystore/local-token-store.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/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/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 +1 -0
- package/dist/transport.js +28 -2
- 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 +2 -1
- package/src/certs/root.crt +13 -0
package/dist/client.d.ts
CHANGED
|
@@ -20,6 +20,8 @@ import { AID } from './aid.js';
|
|
|
20
20
|
import { type OwnershipConsensusResult } from './ownership-v21.js';
|
|
21
21
|
import { type OwnershipTaskResult } from './ownership-workflow.js';
|
|
22
22
|
import { type CertificateMaterial } from './org-protocol.js';
|
|
23
|
+
import { type RecordVerificationContext as SemanticRecordVerificationContext, type SemanticMessageVerificationContext } from './semantic-protocol.js';
|
|
24
|
+
import type { SemanticPersistentState } from './semantic-facade.js';
|
|
23
25
|
/**
|
|
24
26
|
* 递归排序键的 JSON 序列化(Canonical JSON for AUN)
|
|
25
27
|
* 等价于 Python json.dumps(sort_keys=True, separators=(",",":"), ensure_ascii=False)
|
|
@@ -108,6 +110,11 @@ export declare class AUNClient {
|
|
|
108
110
|
private _startCredentialRenewal;
|
|
109
111
|
private _stopCredentialRenewal;
|
|
110
112
|
_rememberCredential(credential: JsonObject, material: Record<string, unknown>, certificate?: CertificateMaterial): void;
|
|
113
|
+
_semanticContextKey(): string;
|
|
114
|
+
_loadSemanticState(): SemanticPersistentState;
|
|
115
|
+
_saveSemanticState(state: SemanticPersistentState): void;
|
|
116
|
+
_semanticVerificationContext(): SemanticRecordVerificationContext;
|
|
117
|
+
_semanticMessageVerificationContext(): SemanticMessageVerificationContext;
|
|
111
118
|
private _verifyIncomingServiceEvent;
|
|
112
119
|
private _handleCredentialRenewal;
|
|
113
120
|
/** 实例级 protected_headers */
|
|
@@ -219,6 +226,9 @@ export declare class AUNClient {
|
|
|
219
226
|
private _reconnectAbort;
|
|
220
227
|
private _reconnectTask;
|
|
221
228
|
private _reconnectEventDispatchDepth;
|
|
229
|
+
private _gatewayConnectionId;
|
|
230
|
+
private _migrationPending;
|
|
231
|
+
private _migrationRevision;
|
|
222
232
|
private _serverKicked;
|
|
223
233
|
/** 缓存最近一次 gateway.disconnect 信息(含服务端附带的 detail),用于后续 connection.state 透传 */
|
|
224
234
|
private _lastDisconnectInfo;
|
|
@@ -612,6 +622,8 @@ export declare class AUNClient {
|
|
|
612
622
|
private _unrefTimer;
|
|
613
623
|
/** 不重连 close code 集合:认证失败/权限错误/被踢等,重连无意义 */
|
|
614
624
|
private static readonly _NO_RECONNECT_CODES;
|
|
625
|
+
private _onGatewayMigrate;
|
|
626
|
+
private _runGatewayMigration;
|
|
615
627
|
/** 处理服务端主动断开通知 event/gateway.disconnect。
|
|
616
628
|
*
|
|
617
629
|
* 服务端可能附带结构化 detail 字段(如配额超限时含 aid/device_id/slot_id/quota_kind/evicted_by)。
|
package/dist/client.js
CHANGED
|
@@ -43,7 +43,7 @@ import { V2E2EECoordinator } from './client/v2-e2ee.js';
|
|
|
43
43
|
import { GroupStateCoordinator } from './client/group-state.js';
|
|
44
44
|
import { decryptMessage, } from './v2/e2ee/index.js';
|
|
45
45
|
import { ecdsaVerifyRaw } from './v2/crypto/ecdsa.js';
|
|
46
|
-
import { decryptedOwnershipJsonText } from './v2/e2ee/decrypt.js';
|
|
46
|
+
import { decryptedOwnershipJsonText, decryptedMessagePayloadBytes } from './v2/e2ee/decrypt.js';
|
|
47
47
|
import { isJsonObject, ConnectionState, STATE_TO_PUBLIC, } from './types.js';
|
|
48
48
|
import { AID } from './aid.js';
|
|
49
49
|
import { certFingerprint, certMatchesFingerprint, normalizeFingerprintHex, publicKeyMatchesFingerprint } from './cert-utils.js';
|
|
@@ -51,6 +51,7 @@ import { validateAgentMdCertificate, validateAgentMdDocument } from './agent-md-
|
|
|
51
51
|
import { OwnershipFiles, OwnershipStorageAdapter, ownershipAid, ownershipFailure } from './ownership-v21.js';
|
|
52
52
|
import { OwnershipWorkflow, OWNERSHIP_SDK_MESSAGE_TYPE, parseOwnershipMessage } from './ownership-workflow.js';
|
|
53
53
|
import { canonicalJson, createCredential, createMemberCredential, signMembershipBinding, digest, validateMembershipBinding, verifyMembershipBinding, verifyCredentialSignature, createRepresentationProof, verifyServiceEvent, buildRequestBinding, validateRequestBinding, CredentialRenewal } from './org-protocol.js';
|
|
54
|
+
import { SemanticProtocolError } from './semantic-protocol.js';
|
|
54
55
|
function isPromiseLike(value) {
|
|
55
56
|
return Boolean(value && typeof value.then === 'function');
|
|
56
57
|
}
|
|
@@ -675,12 +676,12 @@ export class AUNClient {
|
|
|
675
676
|
this._credentialRenewalAbort = abort;
|
|
676
677
|
const current = () => this._credentialRenewalAbort === abort && !abort.signal.aborted && aid === this._aid && version === this._serviceEventIdentityVersion && ['ready', 'connected'].includes(this._state) && !this._closing && !this._disconnecting;
|
|
677
678
|
const guard = { current, signal: abort.signal };
|
|
678
|
-
this._nextCredentialRevocationSyncAt
|
|
679
|
+
this._nextCredentialRevocationSyncAt = Date.now();
|
|
679
680
|
const scan = async () => {
|
|
680
681
|
const task = this._credentialRenewalScan.then(async () => {
|
|
681
682
|
if (!current())
|
|
682
683
|
return;
|
|
683
|
-
const syncRevocations = Date.now() >= this._nextCredentialRevocationSyncAt;
|
|
684
|
+
const syncRevocations = this._sessionOptions.connection_kind !== 'short' && Date.now() >= this._nextCredentialRevocationSyncAt;
|
|
684
685
|
if (syncRevocations)
|
|
685
686
|
this._nextCredentialRevocationSyncAt = Date.now() + 3_600_000;
|
|
686
687
|
await this._resumeCredentialRenewalAfterConnect(guard, syncRevocations);
|
|
@@ -710,13 +711,95 @@ export class AUNClient {
|
|
|
710
711
|
}
|
|
711
712
|
this._safeAsync(this._getCredentialRenewal().remember(credential, material));
|
|
712
713
|
}
|
|
714
|
+
_semanticContextKey() {
|
|
715
|
+
return JSON.stringify([this._aid, this._currentAid?.certFingerprint, this._serviceEventIdentityVersion]);
|
|
716
|
+
}
|
|
717
|
+
_loadSemanticState() {
|
|
718
|
+
const state = (this._tokenStore._loadMetadataStrict?.(this._aid ?? '') ?? this._tokenStore.loadMetadata?.(this._aid ?? ''))?.semantic_credentials_v1;
|
|
719
|
+
return structuredClone(state ?? {});
|
|
720
|
+
}
|
|
721
|
+
_saveSemanticState(state) {
|
|
722
|
+
if (!this._aid)
|
|
723
|
+
throw new ValidationError('semantic credential persistence requires a loaded identity');
|
|
724
|
+
if (!this._tokenStore.saveMetadata)
|
|
725
|
+
throw new ValidationError('semantic credential metadata storage is unavailable');
|
|
726
|
+
this._tokenStore.saveMetadata(this._aid, { semantic_credentials_v1: state });
|
|
727
|
+
}
|
|
728
|
+
_semanticVerificationContext() {
|
|
729
|
+
const scope = this._semanticContextKey();
|
|
730
|
+
const current = () => { if (this._semanticContextKey() !== scope)
|
|
731
|
+
throw new ValidationError('semantic credential identity changed'); };
|
|
732
|
+
return {
|
|
733
|
+
resolveCertificate: async (fingerprint, aid, signedAt) => {
|
|
734
|
+
current();
|
|
735
|
+
const certificate = await this._fetchPeerCert(aid, fingerprint, undefined, signedAt);
|
|
736
|
+
current();
|
|
737
|
+
return certificate;
|
|
738
|
+
},
|
|
739
|
+
verifyCertificateTrust: async (material, context) => {
|
|
740
|
+
current();
|
|
741
|
+
const certificate = typeof material === 'string' && material.startsWith('-----BEGIN CERTIFICATE-----')
|
|
742
|
+
? material : new crypto.X509Certificate(typeof material === 'string' ? Buffer.from(material, 'base64') : material).toString();
|
|
743
|
+
const gateway = await this._resolveOwnershipCertificateGateway(context.expectedAID);
|
|
744
|
+
await this._auth.verifyPeerCertificate(gateway, certificate, context.expectedAID, context.signedAt);
|
|
745
|
+
await this._auth.verifyPeerCertificate(gateway, certificate, context.expectedAID, undefined, true);
|
|
746
|
+
current();
|
|
747
|
+
},
|
|
748
|
+
};
|
|
749
|
+
}
|
|
750
|
+
_semanticMessageVerificationContext() {
|
|
751
|
+
const actor = this._aid, device = this._deviceId, scope = this._semanticContextKey(), session = this._v2Session;
|
|
752
|
+
const trust = this._semanticVerificationContext();
|
|
753
|
+
const current = () => {
|
|
754
|
+
if (!actor || this._semanticContextKey() !== scope || device !== this._deviceId || session !== this._v2Session)
|
|
755
|
+
throw new SemanticProtocolError('SEMANTIC_IDENTITY_INVALID', 'semantic message verification identity changed');
|
|
756
|
+
};
|
|
757
|
+
const senderCertificate = async (sender, fingerprint, signedAt) => {
|
|
758
|
+
current();
|
|
759
|
+
const certificate = await this._fetchPeerCert(sender, fingerprint, undefined, signedAt);
|
|
760
|
+
if (!publicKeyMatchesFingerprint(certificate, fingerprint))
|
|
761
|
+
throw new SemanticProtocolError('SEMANTIC_IDENTITY_INVALID', 'historical sender key fingerprint differs');
|
|
762
|
+
current();
|
|
763
|
+
return certificate;
|
|
764
|
+
};
|
|
765
|
+
return {
|
|
766
|
+
selfAid: actor ?? '', selfDeviceId: device,
|
|
767
|
+
resolveDecryptionKeys: async (envelope) => {
|
|
768
|
+
current();
|
|
769
|
+
if (!session || session.aid !== actor || session.deviceId !== device)
|
|
770
|
+
throw new SemanticProtocolError('SEMANTIC_STATE_UNAVAILABLE', 'authenticated V2 receiver session is unavailable');
|
|
771
|
+
const recipient = envelope.recipient;
|
|
772
|
+
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;
|
|
773
|
+
if (!row && (!recipient || recipient.aid !== actor || recipient.device_id !== device && recipient.device_id !== ''))
|
|
774
|
+
throw new SemanticProtocolError('SEMANTIC_IDENTITY_INVALID', 'original receiver row is unavailable');
|
|
775
|
+
const spkId = String(row ? row[5] ?? '' : recipient.spk_id ?? ''), aad = envelope.aad;
|
|
776
|
+
if (envelope.type === 'e2ee.group_encrypted') {
|
|
777
|
+
const keys = session.getGroupDecryptKeys(String(aad.group_id ?? ''), spkId);
|
|
778
|
+
return { ikPriv: keys.ikPriv, spkPriv: keys.spkPriv ?? undefined };
|
|
779
|
+
}
|
|
780
|
+
return session.getDecryptKeys(spkId);
|
|
781
|
+
},
|
|
782
|
+
resolveSenderKey: async (sender, _device, fingerprint, signedAt) => {
|
|
783
|
+
const certificate = await senderCertificate(sender, fingerprint, signedAt);
|
|
784
|
+
return new Uint8Array(new crypto.X509Certificate(certificate).publicKey.export({ type: 'spki', format: 'der' }));
|
|
785
|
+
},
|
|
786
|
+
verifySenderIdentity: async (sender, _device, fingerprint, signedAt, publicKey) => {
|
|
787
|
+
const certificate = await senderCertificate(sender, fingerprint, signedAt);
|
|
788
|
+
await trust.verifyCertificateTrust(certificate, { expectedAID: sender, signedAt, now: Math.trunc(Date.now() / 1000) });
|
|
789
|
+
const expected = new crypto.X509Certificate(certificate).publicKey.export({ type: 'spki', format: 'der' });
|
|
790
|
+
if (!expected.equals(Buffer.from(publicKey)))
|
|
791
|
+
throw new SemanticProtocolError('SEMANTIC_IDENTITY_INVALID', 'historical sender identity differs from message key');
|
|
792
|
+
current();
|
|
793
|
+
},
|
|
794
|
+
};
|
|
795
|
+
}
|
|
713
796
|
async _verifyIncomingServiceEvent(event, payload) {
|
|
714
797
|
const targetAid = this._identity?.aid ?? '';
|
|
715
798
|
const version = this._serviceEventIdentityVersion;
|
|
716
799
|
const signal = this._serviceEventIdentityAbort.signal;
|
|
717
800
|
const expiresAt = Number(payload?.expires_at);
|
|
718
801
|
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
|
|
802
|
+
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
803
|
const verified = await verifyServiceEvent(event, payload, { targetAid, resolveEventCertificate: (aid, fingerprint) => this._fetchServiceEventCertificate(aid, fingerprint), resolveCredentialService: this.resolveCredentialService });
|
|
721
804
|
if (!current())
|
|
722
805
|
throw new ValidationError('service event receiver identity changed');
|
|
@@ -865,6 +948,9 @@ export class AUNClient {
|
|
|
865
948
|
_reconnectAbort = null;
|
|
866
949
|
_reconnectTask = null;
|
|
867
950
|
_reconnectEventDispatchDepth = 0;
|
|
951
|
+
_gatewayConnectionId = '';
|
|
952
|
+
_migrationPending = false;
|
|
953
|
+
_migrationRevision = new Map();
|
|
868
954
|
_serverKicked = false;
|
|
869
955
|
/** 缓存最近一次 gateway.disconnect 信息(含服务端附带的 detail),用于后续 connection.state 透传 */
|
|
870
956
|
_lastDisconnectInfo = null;
|
|
@@ -1058,6 +1144,10 @@ export class AUNClient {
|
|
|
1058
1144
|
}
|
|
1059
1145
|
// 服务端主动断开通知:记录日志并标记不重连
|
|
1060
1146
|
this._dispatcher.subscribeProtocol('_raw.gateway.disconnect', (data) => this._onGatewayDisconnect(data));
|
|
1147
|
+
this._dispatcher.subscribeProtocol('notification', (data) => {
|
|
1148
|
+
if (isJsonObject(data) && data.method === 'gateway.migrate' && !('id' in data))
|
|
1149
|
+
this._onGatewayMigrate(data.params);
|
|
1150
|
+
});
|
|
1061
1151
|
}
|
|
1062
1152
|
// ── 属性 ──────────────────────────────────────────────────
|
|
1063
1153
|
/** 当前 AID */
|
|
@@ -1730,7 +1820,7 @@ export class AUNClient {
|
|
|
1730
1820
|
async _resumeOrgRequest(method, params, target) {
|
|
1731
1821
|
const binding = params.request_binding;
|
|
1732
1822
|
const now = Math.trunc(Date.now() / 1000);
|
|
1733
|
-
if (!isJsonObject(binding) || now
|
|
1823
|
+
if (!isJsonObject(binding) || now <= Number(binding.exp) + 300)
|
|
1734
1824
|
return params;
|
|
1735
1825
|
const credentials = isJsonObject(params.credentials) ? params.credentials : {};
|
|
1736
1826
|
const credential = ['member_credential', 'approval_credential', 'audit_credential'].map((field) => credentials[field]).find(isJsonObject) ?? params.credential_template;
|
|
@@ -1787,7 +1877,7 @@ export class AUNClient {
|
|
|
1787
1877
|
checkpoint = { ...checkpoint, result, phase: 'upload' };
|
|
1788
1878
|
tokens.saveMetadata(orgAid, { org_identity_creation: checkpoint });
|
|
1789
1879
|
}
|
|
1790
|
-
await this._uploadOrgAgentMd(store, orgAid, params);
|
|
1880
|
+
await this._uploadOrgAgentMd(store, orgAid, params, checkpoint.result);
|
|
1791
1881
|
tokens.saveMetadata(orgAid, { org_identity_creation: null });
|
|
1792
1882
|
return checkpoint.result;
|
|
1793
1883
|
}
|
|
@@ -1829,7 +1919,7 @@ export class AUNClient {
|
|
|
1829
1919
|
const now = Math.trunc(Date.now() / 1000);
|
|
1830
1920
|
if (binding.org_id !== orgAid || nextBinding.org_id !== orgAid || binding.member_aid === ownerAid || nextBinding.member_aid !== ownerAid
|
|
1831
1921
|
|| 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)
|
|
1922
|
+
|| !Number.isSafeInteger(nextBinding.iat) || !Number.isSafeInteger(nextBinding.exp) || Number(nextBinding.iat) > now + 300 || now > Number(nextBinding.exp) + 300
|
|
1833
1923
|
|| credential.iss !== orgAid || credential.sub !== binding.member_aid || credential.parent != null || credential.delegate !== false
|
|
1834
1924
|
|| ext.credential_type !== 'member' || ext.bound_to_membership !== true || ext.org_id !== orgAid || ext.membership_id !== binding.membership_id
|
|
1835
1925
|
|| ext.binding_version !== binding.binding_version || ext.membership_terms_hash !== binding.terms_hash
|
|
@@ -1848,7 +1938,7 @@ export class AUNClient {
|
|
|
1848
1938
|
const checkedAt = Math.trunc(Date.now() / 1000);
|
|
1849
1939
|
verifyMembershipBinding(binding, resolve, checkedAt, orgAid);
|
|
1850
1940
|
verifyCredentialSignature(credential, resolve, checkedAt, orgAid);
|
|
1851
|
-
if (Number(nextBinding.exp)
|
|
1941
|
+
if (checkedAt > Number(nextBinding.exp) + 300)
|
|
1852
1942
|
throw ownershipFailure('invalid', 'Org transfer membership materials do not match');
|
|
1853
1943
|
}
|
|
1854
1944
|
async completeOrgTransfer(params = {}, options = {}) {
|
|
@@ -1938,7 +2028,7 @@ export class AUNClient {
|
|
|
1938
2028
|
tokenStore.saveMetadata(orgAid, { org_identity_transfer: pending });
|
|
1939
2029
|
}
|
|
1940
2030
|
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);
|
|
2031
|
+
await this._uploadOrgAgentMd(store, orgAid, pending.source_params, result);
|
|
1942
2032
|
tokenStore.saveMetadata(orgAid, { org_identity_transfer: { transfer_id: transferId, owner_aid: owner.aid, input_hash: inputHash, phase: 'completed', result } });
|
|
1943
2033
|
localStore.clearPendingGroupBind(pendingKey);
|
|
1944
2034
|
return result;
|
|
@@ -1976,15 +2066,18 @@ export class AUNClient {
|
|
|
1976
2066
|
}
|
|
1977
2067
|
return result;
|
|
1978
2068
|
}
|
|
1979
|
-
async _uploadOrgAgentMd(store, orgAid, params) {
|
|
2069
|
+
async _uploadOrgAgentMd(store, orgAid, params, initialResult) {
|
|
1980
2070
|
const owner = this._currentAid;
|
|
1981
2071
|
if (!owner)
|
|
1982
2072
|
throw ownershipFailure('forbidden', 'Org agent.md requires current owner');
|
|
1983
|
-
|
|
2073
|
+
const organization = isJsonObject(initialResult) && isJsonObject(initialResult.organization) ? initialResult.organization : undefined;
|
|
2074
|
+
const bootstrap = organization?.org_id === orgAid && organization.owner_aid === owner.aid && organization.status === 'active'
|
|
2075
|
+
? { aid: orgAid, ownerAid: owner.aid, certFingerprint: this._localEntityCertificate(orgAid).certFingerprint, completedTransfer: Boolean(params.transfer_id && isJsonObject(initialResult) && initialResult.transfer_id === params.transfer_id) } : undefined;
|
|
2076
|
+
if (await this._resumeGroupAgentMdUpload(store, orgAid, bootstrap))
|
|
1984
2077
|
return;
|
|
1985
2078
|
const identity = this._localEntityCertificate(orgAid);
|
|
1986
2079
|
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') });
|
|
2080
|
+
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
2081
|
if (!uploaded.ok) {
|
|
1989
2082
|
const code = ['invalid', 'unavailable', 'forbidden', 'conflict', 'persistence_unavailable'].includes(uploaded.error.code) ? uploaded.error.code : uploaded.error.code === 'NETWORK_ERROR' ? 'unavailable' : 'invalid';
|
|
1990
2083
|
throw ownershipFailure(code, uploaded.error.message, code === 'unavailable');
|
|
@@ -2100,7 +2193,7 @@ export class AUNClient {
|
|
|
2100
2193
|
if (!imported.ok) {
|
|
2101
2194
|
throw new ValidationError(imported.error.message);
|
|
2102
2195
|
}
|
|
2103
|
-
await this._uploadGroupAgentMd(store, groupAid, payload, group);
|
|
2196
|
+
await this._uploadGroupAgentMd(store, groupAid, payload, group, certPem);
|
|
2104
2197
|
if (keystore && typeof keystore.clearPendingGroupBind === 'function') {
|
|
2105
2198
|
keystore.clearPendingGroupBind(pendingKey);
|
|
2106
2199
|
}
|
|
@@ -2168,7 +2261,7 @@ export class AUNClient {
|
|
|
2168
2261
|
'',
|
|
2169
2262
|
].join('\n');
|
|
2170
2263
|
}
|
|
2171
|
-
async _resumeGroupAgentMdUpload(store, groupAid) {
|
|
2264
|
+
async _resumeGroupAgentMdUpload(store, groupAid, bootstrap) {
|
|
2172
2265
|
const identityStore = store;
|
|
2173
2266
|
const pendingManager = identityStore._agentMdManager ?? this._agentMdManager;
|
|
2174
2267
|
const pending = pendingManager.pendingUpload(groupAid);
|
|
@@ -2188,7 +2281,7 @@ export class AUNClient {
|
|
|
2188
2281
|
uploadViaGatewayHost: true,
|
|
2189
2282
|
ownerAidGetter: () => identity.aid,
|
|
2190
2283
|
currentAidGetter: () => identity,
|
|
2191
|
-
entityOwnerResolver: (aid, type) => this._resolveEntityAgentMdOwner(aid, type),
|
|
2284
|
+
entityOwnerResolver: (aid, type) => this._resolveEntityAgentMdOwner(aid, type, bootstrap?.completedTransfer || pending.condition?.if_none_match === '*' ? bootstrap : undefined),
|
|
2192
2285
|
entityIdentityGetter: (aid, type) => this._freshEntityCertificate(aid, type),
|
|
2193
2286
|
entitySignerResolver: (aid, fingerprint) => this._freshEntitySigner(aid, fingerprint),
|
|
2194
2287
|
gatewayResolver: (aid) => identityStore._resolveGateway(aid),
|
|
@@ -2203,15 +2296,16 @@ export class AUNClient {
|
|
|
2203
2296
|
});
|
|
2204
2297
|
return await manager.resumePendingUpload(groupAid);
|
|
2205
2298
|
}
|
|
2206
|
-
async _uploadGroupAgentMd(store, groupAid, params, group) {
|
|
2299
|
+
async _uploadGroupAgentMd(store, groupAid, params, group, initialCertificate) {
|
|
2207
2300
|
const uploaderAid = String(this._aid ?? '').trim();
|
|
2208
2301
|
if (!uploaderAid) {
|
|
2209
2302
|
throw new ValidationError('group agent.md upload requires current owner AID');
|
|
2210
2303
|
}
|
|
2211
2304
|
this._aidStore = store;
|
|
2212
|
-
|
|
2305
|
+
const bootstrap = initialCertificate ? { aid: groupAid, ownerAid: String(group.owner_aid ?? '').trim(), certFingerprint: certFingerprint(initialCertificate), type: 'group' } : undefined;
|
|
2306
|
+
if (await this._resumeGroupAgentMdUpload(store, groupAid, bootstrap))
|
|
2213
2307
|
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) });
|
|
2308
|
+
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
2309
|
if (!uploaded.ok) {
|
|
2216
2310
|
const code = ['invalid', 'unavailable', 'forbidden', 'conflict', 'persistence_unavailable'].includes(uploaded.error.code) ? uploaded.error.code : uploaded.error.code === 'NETWORK_ERROR' ? 'unavailable' : 'invalid';
|
|
2217
2311
|
throw ownershipFailure(code, `group agent.md upload failed: ${uploaded.error.message}`, code === 'unavailable');
|
|
@@ -2232,8 +2326,20 @@ export class AUNClient {
|
|
|
2232
2326
|
const material = await this._auth.resolveOwnershipCertificate(await this._resolveOwnershipCertificateGateway(aid), aid, fingerprint, 'signing');
|
|
2233
2327
|
return AID._create({ aid, aunPath: this._configModel.aunPath, certPem: material.cert_pem, certValid: true, privateKeyPem: null, privateKeyValid: false });
|
|
2234
2328
|
}
|
|
2235
|
-
async _resolveEntityAgentMdOwner(aid, type) {
|
|
2236
|
-
|
|
2329
|
+
async _resolveEntityAgentMdOwner(aid, type, bootstrap) {
|
|
2330
|
+
let authority;
|
|
2331
|
+
try {
|
|
2332
|
+
authority = bootstrap?.completedTransfer && type === 'org' && bootstrap.aid === aid && bootstrap.ownerAid === this._currentAid?.aid
|
|
2333
|
+
? { owner_aid: bootstrap.ownerAid, entity_cert_fingerprint: bootstrap.certFingerprint }
|
|
2334
|
+
: await this._agentMdManager.readEntityAuthorityFresh(aid);
|
|
2335
|
+
}
|
|
2336
|
+
catch (error) {
|
|
2337
|
+
if (!bootstrap || bootstrap.aid !== aid || type !== (bootstrap.type ?? 'org') || !(error instanceof AUNError) || !isJsonObject(error.data) || error.data.http_status !== 404)
|
|
2338
|
+
throw error;
|
|
2339
|
+
if (!bootstrap.ownerAid || bootstrap.ownerAid !== this._currentAid?.aid)
|
|
2340
|
+
throw ownershipFailure('forbidden', 'entity initial owner does not match uploader');
|
|
2341
|
+
authority = { owner_aid: bootstrap.ownerAid, entity_cert_fingerprint: bootstrap.certFingerprint };
|
|
2342
|
+
}
|
|
2237
2343
|
const identity = await this._freshEntityCertificate(aid, type);
|
|
2238
2344
|
if (authority.entity_cert_fingerprint !== identity.certFingerprint)
|
|
2239
2345
|
throw ownershipFailure('conflict', 'entity authority changed during current-certificate verification');
|
|
@@ -2301,7 +2407,7 @@ export class AUNClient {
|
|
|
2301
2407
|
if (!imported.ok) {
|
|
2302
2408
|
throw new ValidationError(imported.error.message);
|
|
2303
2409
|
}
|
|
2304
|
-
await this._uploadGroupAgentMd(store, groupAid, payload, group);
|
|
2410
|
+
await this._uploadGroupAgentMd(store, groupAid, payload, group, certPem);
|
|
2305
2411
|
// 落盘成功,清除 pending 槽位
|
|
2306
2412
|
if (groupId && keystore && typeof keystore.clearPendingGroupBind === 'function') {
|
|
2307
2413
|
keystore.clearPendingGroupBind(pendingKey);
|
|
@@ -2600,6 +2706,8 @@ export class AUNClient {
|
|
|
2600
2706
|
* 发送轻量在线通知,不走离线存储、seq/pull 或 ack。
|
|
2601
2707
|
*/
|
|
2602
2708
|
async notify(method, params, options = {}) {
|
|
2709
|
+
if (this._migrationPending)
|
|
2710
|
+
throw new ConnectionError('client is migrating');
|
|
2603
2711
|
if (params !== undefined && params !== null && !isJsonObject(params)) {
|
|
2604
2712
|
throw new ValidationError('notify params must be an object');
|
|
2605
2713
|
}
|
|
@@ -3953,10 +4061,16 @@ export class AUNClient {
|
|
|
3953
4061
|
const challenge = await this._transport.connect(gatewayUrl);
|
|
3954
4062
|
this._assertReconnectOwner(reconnectOwner);
|
|
3955
4063
|
this._lifecycle.assertConnectionAttemptOwner(connectionOwner);
|
|
4064
|
+
const authTransport = reconnectOwner ? {
|
|
4065
|
+
call: (...args) => {
|
|
4066
|
+
this._assertReconnectOwner(reconnectOwner);
|
|
4067
|
+
return this._transport.call(...args);
|
|
4068
|
+
},
|
|
4069
|
+
} : this._transport;
|
|
3956
4070
|
this._clientLog.debug(`WebSocket connection established: gateway=${gatewayUrl}`);
|
|
3957
4071
|
this._state = 'authenticating';
|
|
3958
4072
|
if (allowReauth) {
|
|
3959
|
-
const authContext = await this._auth.connectSession(
|
|
4073
|
+
const authContext = await this._auth.connectSession(authTransport, challenge, gatewayUrl, {
|
|
3960
4074
|
accessToken: String(params.access_token ?? ''),
|
|
3961
4075
|
deviceId: this._deviceId,
|
|
3962
4076
|
slotId: this._slotId,
|
|
@@ -3982,10 +4096,11 @@ export class AUNClient {
|
|
|
3982
4096
|
if (isJsonObject(auth.hello) && 'heartbeat_interval' in auth.hello) {
|
|
3983
4097
|
this._applyServerHeartbeatInterval(auth.hello.heartbeat_interval, 'auth');
|
|
3984
4098
|
}
|
|
4099
|
+
this._gatewayConnectionId = isJsonObject(auth.hello) && isJsonObject(auth.hello.connection) ? String(auth.hello.connection.id ?? '') : '';
|
|
3985
4100
|
}
|
|
3986
4101
|
}
|
|
3987
4102
|
else {
|
|
3988
|
-
const hello = await this._auth.initializeWithToken(
|
|
4103
|
+
const hello = await this._auth.initializeWithToken(authTransport, challenge, String(params.access_token), {
|
|
3989
4104
|
deviceId: this._deviceId,
|
|
3990
4105
|
slotId: this._slotId,
|
|
3991
4106
|
deliveryMode: this._connectDeliveryMode,
|
|
@@ -3999,6 +4114,7 @@ export class AUNClient {
|
|
|
3999
4114
|
if (isJsonObject(hello) && 'heartbeat_interval' in hello) {
|
|
4000
4115
|
this._applyServerHeartbeatInterval(hello.heartbeat_interval, 'auth');
|
|
4001
4116
|
}
|
|
4117
|
+
this._gatewayConnectionId = isJsonObject(hello) && isJsonObject(hello.connection) ? String(hello.connection.id ?? '') : '';
|
|
4002
4118
|
}
|
|
4003
4119
|
this._assertReconnectOwner(reconnectOwner);
|
|
4004
4120
|
this._lifecycle.assertConnectionAttemptOwner(connectionOwner);
|
|
@@ -4006,6 +4122,7 @@ export class AUNClient {
|
|
|
4006
4122
|
throw new ConnectionError('websocket disconnected during authentication');
|
|
4007
4123
|
}
|
|
4008
4124
|
this._serverKicked = false;
|
|
4125
|
+
this._migrationPending = false;
|
|
4009
4126
|
this._lastDisconnectInfo = null;
|
|
4010
4127
|
this._state = 'ready';
|
|
4011
4128
|
this._connectedAt = Date.now();
|
|
@@ -4435,8 +4552,10 @@ export class AUNClient {
|
|
|
4435
4552
|
return null;
|
|
4436
4553
|
}
|
|
4437
4554
|
let plaintext;
|
|
4555
|
+
let payloadBytes;
|
|
4438
4556
|
try {
|
|
4439
4557
|
plaintext = decryptMessage(envelope, this._aid ?? '', this._deviceId, ikPriv, spkPriv, senderPubDer);
|
|
4558
|
+
payloadBytes = decryptedMessagePayloadBytes(plaintext);
|
|
4440
4559
|
if (plaintext === null)
|
|
4441
4560
|
throw new Error('recipient_not_found_or_envelope_verification_failed');
|
|
4442
4561
|
}
|
|
@@ -4488,6 +4607,11 @@ export class AUNClient {
|
|
|
4488
4607
|
encrypted: true,
|
|
4489
4608
|
e2ee,
|
|
4490
4609
|
};
|
|
4610
|
+
if (!this._v2E2EE.isEncryptedEnvelopePayload(msg.payload))
|
|
4611
|
+
result._semantic_evidence = {
|
|
4612
|
+
envelope_bytes_base64: Buffer.from(envelopeJson, 'utf8').toString('base64'), payload_bytes_base64: Buffer.from(payloadBytes).toString('base64'),
|
|
4613
|
+
received_by: this._aid ?? '', received_at: Math.trunc(Date.now() / 1000), source,
|
|
4614
|
+
};
|
|
4491
4615
|
if (typeof plaintext.type === 'string' && plaintext.type.startsWith('aun.ownership.'))
|
|
4492
4616
|
result.from_aid = fromAid;
|
|
4493
4617
|
if (senderDeviceId)
|
|
@@ -4719,7 +4843,7 @@ export class AUNClient {
|
|
|
4719
4843
|
}
|
|
4720
4844
|
}
|
|
4721
4845
|
/** 发现并缓存全部 Gateway 候选,供首连和自动重连故障转移使用。 */
|
|
4722
|
-
async _resolveGatewayCandidatesForAid(aid) {
|
|
4846
|
+
async _resolveGatewayCandidatesForAid(aid, fresh = false, reconnectOwner) {
|
|
4723
4847
|
const resolvedAid = String(aid ?? this._aid ?? '').trim();
|
|
4724
4848
|
if (!resolvedAid) {
|
|
4725
4849
|
throw new StateError('gateway discovery requires a loaded AID');
|
|
@@ -4732,14 +4856,15 @@ export class AUNClient {
|
|
|
4732
4856
|
const tokenStore = this._tokenStore;
|
|
4733
4857
|
const discoveryIsCurrent = () => (this._discovery === discovery
|
|
4734
4858
|
&& this._tokenStore === tokenStore
|
|
4735
|
-
&& !this._closing
|
|
4736
|
-
|
|
4859
|
+
&& !this._closing
|
|
4860
|
+
&& (!reconnectOwner || this._ownsReconnect(reconnectOwner)));
|
|
4861
|
+
if (!fresh && sameIssuer && this._gatewayCandidates.length > 0)
|
|
4737
4862
|
return [...this._gatewayCandidates];
|
|
4738
|
-
if (sameIssuer && this._gatewayUrl) {
|
|
4863
|
+
if (!fresh && sameIssuer && this._gatewayUrl) {
|
|
4739
4864
|
this._gatewayCandidates = [this._gatewayUrl];
|
|
4740
4865
|
return [...this._gatewayCandidates];
|
|
4741
4866
|
}
|
|
4742
|
-
if (!sameIssuer) {
|
|
4867
|
+
if (!fresh && !sameIssuer) {
|
|
4743
4868
|
const cached = this._peerGatewayCache.get(issuerDomain);
|
|
4744
4869
|
if (cached)
|
|
4745
4870
|
return [...cached];
|
|
@@ -4754,7 +4879,7 @@ export class AUNClient {
|
|
|
4754
4879
|
: [];
|
|
4755
4880
|
const cachedGateway = cachedList[0]
|
|
4756
4881
|
?? (isJsonObject(metadata) ? String(metadata.gateway_url ?? '').trim() : '');
|
|
4757
|
-
if (cachedGateway) {
|
|
4882
|
+
if (!fresh && cachedGateway) {
|
|
4758
4883
|
const gatewayUrls = [...new Set([cachedGateway, ...cachedList])];
|
|
4759
4884
|
if (sameIssuer) {
|
|
4760
4885
|
this._gatewayCandidates = gatewayUrls;
|
|
@@ -5135,6 +5260,157 @@ export class AUNClient {
|
|
|
5135
5260
|
// ── 内部:断线重连 ────────────────────────────────────────
|
|
5136
5261
|
/** 不重连 close code 集合:认证失败/权限错误/被踢等,重连无意义 */
|
|
5137
5262
|
static _NO_RECONNECT_CODES = new Set([4001, 4003, 4008, 4009, 4010, 4011, 4013, 4014, 4015]);
|
|
5263
|
+
_onGatewayMigrate(input) {
|
|
5264
|
+
const data = input;
|
|
5265
|
+
if (!isJsonObject(data) || this._closing || this._disconnecting || this._connectAbort || this._reconnectActive
|
|
5266
|
+
|| this._state !== 'ready' || !this._sessionParams || !this._gatewayConnectionId)
|
|
5267
|
+
return;
|
|
5268
|
+
const aid = this._aid ?? '';
|
|
5269
|
+
const issuer = aid.slice(aid.indexOf('.') + 1).toLowerCase().replace(/\.$/, '');
|
|
5270
|
+
const now = Date.now() / 1000;
|
|
5271
|
+
if (typeof data.migration_id !== 'string' || !data.migration_id.trim()
|
|
5272
|
+
|| typeof data.revision !== 'number' || !Number.isSafeInteger(data.revision) || data.revision <= 0
|
|
5273
|
+
|| typeof data.issuer !== 'string' || data.issuer.trim().toLowerCase().replace(/\.$/, '') !== issuer
|
|
5274
|
+
|| data.aid !== aid || data.device_id !== this._deviceId || data.slot_id !== this._slotId
|
|
5275
|
+
|| data.connection_id !== this._gatewayConnectionId || typeof data.gateway_url !== 'string'
|
|
5276
|
+
|| typeof data.expires_at !== 'number' || !Number.isFinite(data.expires_at)
|
|
5277
|
+
|| data.expires_at <= now || data.expires_at > now + 300)
|
|
5278
|
+
return;
|
|
5279
|
+
let target;
|
|
5280
|
+
try {
|
|
5281
|
+
target = new URL(data.gateway_url);
|
|
5282
|
+
}
|
|
5283
|
+
catch {
|
|
5284
|
+
return;
|
|
5285
|
+
}
|
|
5286
|
+
const hostname = target.hostname.toLowerCase();
|
|
5287
|
+
if (target.protocol !== 'wss:' || target.username || target.password || target.hash
|
|
5288
|
+
|| /^\w+:\/\/[^/]*@/.test(data.gateway_url) || target.pathname !== '/aun'
|
|
5289
|
+
|| !(hostname === issuer || hostname.endsWith(`.${issuer}`))
|
|
5290
|
+
|| !hostname.split('.').every((label) => /^[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?$/.test(label)))
|
|
5291
|
+
return;
|
|
5292
|
+
const identity = JSON.stringify([this._aid, this._deviceId, this._slotId]);
|
|
5293
|
+
const previous = this._migrationRevision.get(identity);
|
|
5294
|
+
if (previous && (data.revision <= previous.revision || data.migration_id === previous.migrationId))
|
|
5295
|
+
return;
|
|
5296
|
+
this._migrationRevision.set(identity, { revision: data.revision, migrationId: data.migration_id });
|
|
5297
|
+
this._migrationPending = true;
|
|
5298
|
+
this._reconnectActive = true;
|
|
5299
|
+
const owner = new AbortController();
|
|
5300
|
+
this._reconnectAbort = owner;
|
|
5301
|
+
this._state = 'standby';
|
|
5302
|
+
this._stopBackgroundTasks();
|
|
5303
|
+
this._dispatcher.enqueue('gateway.migrate', data);
|
|
5304
|
+
const task = Promise.resolve().then(() => this._runGatewayMigration(data, owner, identity));
|
|
5305
|
+
this._reconnectTask = task;
|
|
5306
|
+
this._safeAsync(task);
|
|
5307
|
+
}
|
|
5308
|
+
async _runGatewayMigration(data, owner, identity) {
|
|
5309
|
+
const expiresAt = Number(data.expires_at) * 1000;
|
|
5310
|
+
const current = () => this._ownsReconnect(owner) && !this._disconnecting && !this._connectAbort
|
|
5311
|
+
&& identity === JSON.stringify([this._aid, this._deviceId, this._slotId]);
|
|
5312
|
+
const wait = (operation) => this._lifecycle.waitForAttempt(operation, undefined, owner);
|
|
5313
|
+
const retry = this._sessionOptions.retry;
|
|
5314
|
+
const maxAttempts = Number(retry.max_attempts ?? 0);
|
|
5315
|
+
let delay = clampReconnectDelayMs(Number(retry.initial_delay ?? 1) * 1000, RECONNECT_MIN_BASE_DELAY_MS);
|
|
5316
|
+
const maxDelay = clampReconnectDelayMs(Number(retry.max_delay ?? 64) * 1000, RECONNECT_MAX_BASE_DELAY_MS);
|
|
5317
|
+
const expiryTimer = setTimeout(() => {
|
|
5318
|
+
if (!current())
|
|
5319
|
+
return;
|
|
5320
|
+
owner.abort();
|
|
5321
|
+
this._discovery._cancelPending?.();
|
|
5322
|
+
this._auth._cancelPending?.();
|
|
5323
|
+
void this._transport.close().catch(() => { });
|
|
5324
|
+
}, Math.max(0, expiresAt - Date.now()));
|
|
5325
|
+
let lastError = new ConnectionError('gateway migration expired');
|
|
5326
|
+
try {
|
|
5327
|
+
if (!current())
|
|
5328
|
+
return;
|
|
5329
|
+
await this._transport.waitForPending(Math.min(5000, Math.max(0, expiresAt - Date.now())), owner.signal);
|
|
5330
|
+
if (!current())
|
|
5331
|
+
return;
|
|
5332
|
+
await this._saveSeqTrackerState();
|
|
5333
|
+
await this._transport.close();
|
|
5334
|
+
if (!current())
|
|
5335
|
+
return;
|
|
5336
|
+
this._delivery.resetInlineAckState();
|
|
5337
|
+
for (let attempt = 1; current() && Date.now() < expiresAt; attempt++) {
|
|
5338
|
+
if (maxAttempts > 0 && attempt > maxAttempts)
|
|
5339
|
+
throw lastError;
|
|
5340
|
+
let candidates = attempt === 1 ? [String(data.gateway_url)] : [];
|
|
5341
|
+
if (attempt > 1) {
|
|
5342
|
+
try {
|
|
5343
|
+
candidates = await wait(this._resolveGatewayCandidatesForAid(String(data.aid), true, owner));
|
|
5344
|
+
}
|
|
5345
|
+
catch (error) {
|
|
5346
|
+
lastError = error;
|
|
5347
|
+
}
|
|
5348
|
+
}
|
|
5349
|
+
for (let index = 0; index < candidates.length; index++) {
|
|
5350
|
+
const gateway = candidates[index];
|
|
5351
|
+
if (!current() || Date.now() >= expiresAt)
|
|
5352
|
+
return;
|
|
5353
|
+
this._state = 'reconnecting';
|
|
5354
|
+
this._migrationPending = true;
|
|
5355
|
+
try {
|
|
5356
|
+
await wait(this._connectOnce({ ...this._sessionParams, gateway, gateways: candidates }, true, owner));
|
|
5357
|
+
if (!current())
|
|
5358
|
+
return;
|
|
5359
|
+
if (!this._transport.isConnected())
|
|
5360
|
+
throw new ConnectionError('websocket disconnected while migrating');
|
|
5361
|
+
this._sessionParams.gateway = gateway;
|
|
5362
|
+
this._sessionParams.gateways = candidates;
|
|
5363
|
+
return;
|
|
5364
|
+
}
|
|
5365
|
+
catch (error) {
|
|
5366
|
+
lastError = error;
|
|
5367
|
+
if (!current())
|
|
5368
|
+
return;
|
|
5369
|
+
this._migrationPending = true;
|
|
5370
|
+
await this._transport.close().catch(() => { });
|
|
5371
|
+
if (!current())
|
|
5372
|
+
return;
|
|
5373
|
+
if (attempt === 1 && index === 0) {
|
|
5374
|
+
try {
|
|
5375
|
+
const discovered = await wait(this._resolveGatewayCandidatesForAid(String(data.aid), true, owner));
|
|
5376
|
+
candidates = [...candidates, ...discovered.filter((item) => !candidates.includes(item))];
|
|
5377
|
+
}
|
|
5378
|
+
catch (discoveryError) {
|
|
5379
|
+
lastError = discoveryError;
|
|
5380
|
+
}
|
|
5381
|
+
}
|
|
5382
|
+
}
|
|
5383
|
+
}
|
|
5384
|
+
if (!AUNClient._shouldRetryReconnect(lastError))
|
|
5385
|
+
throw lastError;
|
|
5386
|
+
if (maxAttempts > 0 && attempt >= maxAttempts)
|
|
5387
|
+
throw lastError;
|
|
5388
|
+
await this._sleep(Math.min(delay, Math.max(0, expiresAt - Date.now())), owner.signal);
|
|
5389
|
+
if (!current())
|
|
5390
|
+
return;
|
|
5391
|
+
delay = Math.min(delay * 2, maxDelay);
|
|
5392
|
+
}
|
|
5393
|
+
}
|
|
5394
|
+
catch (error) {
|
|
5395
|
+
lastError = error;
|
|
5396
|
+
}
|
|
5397
|
+
finally {
|
|
5398
|
+
clearTimeout(expiryTimer);
|
|
5399
|
+
if (this._reconnectAbort === owner) {
|
|
5400
|
+
this._migrationPending = false;
|
|
5401
|
+
this._reconnectActive = false;
|
|
5402
|
+
this._reconnectAbort = null;
|
|
5403
|
+
this._reconnectTask = null;
|
|
5404
|
+
this._nextRetryAt = null;
|
|
5405
|
+
if ((this._state !== 'ready' || owner.signal.aborted) && !this._closing) {
|
|
5406
|
+
this._stopBackgroundTasks();
|
|
5407
|
+
this._state = 'standby';
|
|
5408
|
+
if (!owner.signal.aborted)
|
|
5409
|
+
this._dispatcher.enqueue('connection.error', { error: formatCaughtError(lastError) });
|
|
5410
|
+
}
|
|
5411
|
+
}
|
|
5412
|
+
}
|
|
5413
|
+
}
|
|
5138
5414
|
/** 处理服务端主动断开通知 event/gateway.disconnect。
|
|
5139
5415
|
*
|
|
5140
5416
|
* 服务端可能附带结构化 detail 字段(如配额超限时含 aid/device_id/slot_id/quota_kind/evicted_by)。
|