@agentunion/fastaun 0.5.27 → 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 +36 -0
- package/_packed_docs/CHANGELOG.md +36 -0
- package/_packed_docs/INDEX.md +17 -2
- package/_packed_docs/KITE_DOCS_GUIDE.md +12 -9
- package/_packed_docs/sdk/AUN_DOCS_GUIDE.md +2 -2
- package/_packed_docs/sdk/INDEX.md +5 -3
- 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 +30 -13
- package/dist/agent-md-schema.js.map +1 -1
- package/dist/agent-md.d.ts +11 -0
- package/dist/agent-md.js +86 -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/auth.d.ts +10 -0
- package/dist/auth.js +44 -4
- package/dist/auth.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.js +10 -0
- package/dist/client/lifecycle.js.map +1 -1
- package/dist/client/rpc-pipeline.js +76 -13
- package/dist/client/rpc-pipeline.js.map +1 -1
- package/dist/client/v2-e2ee.d.ts +3 -0
- package/dist/client/v2-e2ee.js +5 -0
- package/dist/client/v2-e2ee.js.map +1 -1
- package/dist/client.d.ts +26 -0
- package/dist/client.js +267 -7
- 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/risk-control.d.ts +68 -0
- package/dist/risk-control.js +344 -0
- package/dist/risk-control.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 +1 -1
- package/dist/transport.d.ts +21 -1
- package/dist/transport.js +199 -20
- package/dist/transport.js.map +1 -1
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +1 -1
- package/_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 +0 -869
- 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 +0 -408
- 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 +0 -410
package/dist/client.d.ts
CHANGED
|
@@ -12,6 +12,7 @@
|
|
|
12
12
|
*/
|
|
13
13
|
import { AIDStore } from './aid-store.js';
|
|
14
14
|
import { type Subscription, type EventHandler } from './events.js';
|
|
15
|
+
import { type RiskStateEvent } from './risk-control.js';
|
|
15
16
|
import { CollabClient } from './collab/client.js';
|
|
16
17
|
import { DebugFacade, CredentialFacade, OrgFacade, GroupFacade, MailFacade, MessageFacade, StreamFacade } from './facades.js';
|
|
17
18
|
import { StorageVFS } from './storage/vfs.js';
|
|
@@ -207,6 +208,13 @@ export declare class AUNClient {
|
|
|
207
208
|
private _v2KeyStore?;
|
|
208
209
|
private _v2SessionInitInFlight;
|
|
209
210
|
private _v2RuntimeGeneration;
|
|
211
|
+
private _controlMessageCoordinator?;
|
|
212
|
+
private _agentMdChanges?;
|
|
213
|
+
private _agentMdCommitUnsubscribe?;
|
|
214
|
+
private _installAgentMdChangeHooks;
|
|
215
|
+
private _getAgentMdChanges;
|
|
216
|
+
private _onSdkControl;
|
|
217
|
+
private _controlMessages;
|
|
210
218
|
/** V2 bootstrap 缓存:aid/group:id → 设备列表 + 时间戳 */
|
|
211
219
|
private _v2BootstrapCache;
|
|
212
220
|
private _v2SigCache;
|
|
@@ -227,6 +235,15 @@ export declare class AUNClient {
|
|
|
227
235
|
private _reconnectTask;
|
|
228
236
|
private _reconnectEventDispatchDepth;
|
|
229
237
|
private _gatewayConnectionId;
|
|
238
|
+
private _negotiatedCapabilities;
|
|
239
|
+
private _riskNegotiated;
|
|
240
|
+
private _riskRestoredAid;
|
|
241
|
+
private _riskEvents;
|
|
242
|
+
private _riskApplying;
|
|
243
|
+
private _riskApplyTail;
|
|
244
|
+
private _riskScalarReconnectAt;
|
|
245
|
+
private _riskHttpRetryAt;
|
|
246
|
+
private _risk;
|
|
230
247
|
private _migrationPending;
|
|
231
248
|
private _migrationRevision;
|
|
232
249
|
private _serverKicked;
|
|
@@ -366,6 +383,15 @@ export declare class AUNClient {
|
|
|
366
383
|
* disconnect 是可恢复的:停止心跳、关闭 WebSocket,但不清理 keystore 等状态。
|
|
367
384
|
*/
|
|
368
385
|
disconnect(): Promise<void>;
|
|
386
|
+
get riskState(): Readonly<RiskStateEvent>;
|
|
387
|
+
private _installRiskHooks;
|
|
388
|
+
private _restoreRiskState;
|
|
389
|
+
private _assertRiskReconnect;
|
|
390
|
+
private _riskReconnectDelay;
|
|
391
|
+
private _applyRiskHello;
|
|
392
|
+
private _onRiskControl;
|
|
393
|
+
private _applyRiskControl;
|
|
394
|
+
_riskFetch(input: string, init?: RequestInit, perform?: (url: string, init: RequestInit) => Promise<Response>, requestScope?: string): Promise<Response>;
|
|
369
395
|
/**
|
|
370
396
|
* 发送 JSON-RPC 调用。
|
|
371
397
|
* 自动处理内部方法限制、E2EE 加解密、客户端签名等。
|
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';
|
|
@@ -929,6 +932,27 @@ export class AUNClient {
|
|
|
929
932
|
_v2KeyStore;
|
|
930
933
|
_v2SessionInitInFlight = null;
|
|
931
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
|
+
}
|
|
932
956
|
/** V2 bootstrap 缓存:aid/group:id → 设备列表 + 时间戳 */
|
|
933
957
|
_v2BootstrapCache = new Map();
|
|
934
958
|
_v2SigCache = new Map();
|
|
@@ -949,6 +973,27 @@ export class AUNClient {
|
|
|
949
973
|
_reconnectTask = null;
|
|
950
974
|
_reconnectEventDispatchDepth = 0;
|
|
951
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
|
+
});
|
|
952
997
|
_migrationPending = false;
|
|
953
998
|
_migrationRevision = new Map();
|
|
954
999
|
_serverKicked = false;
|
|
@@ -1059,6 +1104,7 @@ export class AUNClient {
|
|
|
1059
1104
|
fetchEntityIdentity: (target, type) => this._freshEntityCertificate(target, type),
|
|
1060
1105
|
fetchEntitySigner: (target, fingerprint) => this._freshEntitySigner(target, fingerprint),
|
|
1061
1106
|
});
|
|
1107
|
+
this._installAgentMdChangeHooks();
|
|
1062
1108
|
this._transport = new RPCTransport({
|
|
1063
1109
|
eventDispatcher: this._dispatcher,
|
|
1064
1110
|
timeout: 35_000,
|
|
@@ -1066,6 +1112,8 @@ export class AUNClient {
|
|
|
1066
1112
|
logger: this._logger.for('aun_core.transport'),
|
|
1067
1113
|
dnsNet,
|
|
1068
1114
|
});
|
|
1115
|
+
this._installRiskHooks();
|
|
1116
|
+
this._transport.setClassMessageHandler((event, payload) => this._controlMessages().onPush(payload, event.includes('group.')));
|
|
1069
1117
|
this._transport.setProtocolMetaObserver((meta) => this._observeRpcMeta(meta));
|
|
1070
1118
|
this._transport.setServiceEventVerifier((event, payload) => this._verifyIncomingServiceEvent(event, payload), () => this._identity?.aid ?? '', () => this._serviceEventIdentityVersion);
|
|
1071
1119
|
this._transport.setProtocolDisconnectCallback((err, closeCode) => this._handleTransportDisconnect(err, closeCode));
|
|
@@ -1100,6 +1148,8 @@ export class AUNClient {
|
|
|
1100
1148
|
this._dispatcher.subscribeProtocol('_raw.message.read_changed', (data) => this._onRawReadChanged(data, 'p2p'));
|
|
1101
1149
|
// V2 P2P 推送通知:收到通知后自动走 message.v2.pull 拉取并解密
|
|
1102
1150
|
this._dispatcher.subscribeProtocol('_raw.peer.v2.message_received', (data) => {
|
|
1151
|
+
if (this._controlMessages().onPush(data))
|
|
1152
|
+
return;
|
|
1103
1153
|
const sender = isJsonObject(data) ? String(data.sender_aid ?? '') : '';
|
|
1104
1154
|
const ns = this._p2pPeerPullEnabled ? (sender ? this._delivery.peerNamespace(sender) : '') : this._aid ? `p2p:${this._aid}` : '';
|
|
1105
1155
|
const hasInline = isJsonObject(data)
|
|
@@ -1111,6 +1161,8 @@ export class AUNClient {
|
|
|
1111
1161
|
});
|
|
1112
1162
|
// V2 群组消息推送通知:收到通知后自动走 group.v2.pull 拉取并解密
|
|
1113
1163
|
this._dispatcher.subscribeProtocol('_raw.group.v2.message_created', (data) => {
|
|
1164
|
+
if (this._controlMessages().onPush(data, true))
|
|
1165
|
+
return;
|
|
1114
1166
|
const groupId = isJsonObject(data)
|
|
1115
1167
|
? String(data.group_id ?? '').trim()
|
|
1116
1168
|
: '';
|
|
@@ -1291,7 +1343,9 @@ export class AUNClient {
|
|
|
1291
1343
|
async releaseSubaid(subAid) { return this._getOwnershipWorkflow().releaseSubaid(subAid); }
|
|
1292
1344
|
async transferSubaid(subAid, newOwnerAid) { return this._getOwnershipWorkflow().transferSubaid(subAid, newOwnerAid); }
|
|
1293
1345
|
async checkOwnershipConsensus(subAid) {
|
|
1294
|
-
|
|
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);
|
|
1295
1349
|
}
|
|
1296
1350
|
async _checkOwnershipConsensusFresh(subAid) {
|
|
1297
1351
|
return this._getOwnershipWorkflow().checkOwnershipConsensus(subAid);
|
|
@@ -1391,6 +1445,12 @@ export class AUNClient {
|
|
|
1391
1445
|
return session.aid === this._aid && session.deviceId === this._deviceId;
|
|
1392
1446
|
}
|
|
1393
1447
|
_resetV2IdentityRuntime() {
|
|
1448
|
+
this._controlMessageCoordinator?.close();
|
|
1449
|
+
this._agentMdChanges?.close();
|
|
1450
|
+
this._agentMdChanges = undefined;
|
|
1451
|
+
this._negotiatedCapabilities = {};
|
|
1452
|
+
this._riskNegotiated = false;
|
|
1453
|
+
this._riskRestoredAid = '';
|
|
1394
1454
|
this._v2RuntimeGeneration += 1;
|
|
1395
1455
|
const keyStore = this._v2KeyStore;
|
|
1396
1456
|
try {
|
|
@@ -1506,6 +1566,7 @@ export class AUNClient {
|
|
|
1506
1566
|
fetchEntityIdentity: (target, type) => this._freshEntityCertificate(target, type),
|
|
1507
1567
|
fetchEntitySigner: (target, fingerprint) => this._freshEntitySigner(target, fingerprint),
|
|
1508
1568
|
});
|
|
1569
|
+
this._installAgentMdChangeHooks();
|
|
1509
1570
|
this._transport = new RPCTransport({
|
|
1510
1571
|
eventDispatcher: this._dispatcher,
|
|
1511
1572
|
timeout: 35_000,
|
|
@@ -1513,6 +1574,8 @@ export class AUNClient {
|
|
|
1513
1574
|
logger: this._logger.for('aun_core.transport'),
|
|
1514
1575
|
dnsNet,
|
|
1515
1576
|
});
|
|
1577
|
+
this._installRiskHooks();
|
|
1578
|
+
this._transport.setClassMessageHandler((event, payload) => this._controlMessages().onPush(payload, event.includes('group.')));
|
|
1516
1579
|
this._transport.setProtocolMetaObserver((meta) => this._observeRpcMeta(meta));
|
|
1517
1580
|
this._transport.setServiceEventVerifier((event, payload) => this._verifyIncomingServiceEvent(event, payload), () => this._identity?.aid ?? '', () => this._serviceEventIdentityVersion);
|
|
1518
1581
|
this._transport.setProtocolDisconnectCallback((err, closeCode) => this._handleTransportDisconnect(err, closeCode));
|
|
@@ -1616,6 +1679,7 @@ export class AUNClient {
|
|
|
1616
1679
|
}
|
|
1617
1680
|
/** 关闭连接 */
|
|
1618
1681
|
async close() {
|
|
1682
|
+
this._risk.close();
|
|
1619
1683
|
this._peerGatewayCache.clear();
|
|
1620
1684
|
return this._lifecycle.close();
|
|
1621
1685
|
}
|
|
@@ -1626,6 +1690,155 @@ export class AUNClient {
|
|
|
1626
1690
|
async disconnect() {
|
|
1627
1691
|
return this._lifecycle.disconnect();
|
|
1628
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
|
+
}
|
|
1629
1842
|
// ── RPC ───────────────────────────────────────────────────
|
|
1630
1843
|
/**
|
|
1631
1844
|
* 发送 JSON-RPC 调用。
|
|
@@ -2783,6 +2996,10 @@ export class AUNClient {
|
|
|
2783
2996
|
const messageEvent = event === 'message.received' || event === 'message.recalled' || event === 'message.batch';
|
|
2784
2997
|
const hadMessageConsumer = messageEvent && [...this._dispatcher._snapshotApplicationHandlers(['message.received', 'message.recalled', 'message.batch']).values()].some((handlers) => handlers.length > 0);
|
|
2785
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
|
+
}
|
|
2786
3003
|
if (messageEvent) {
|
|
2787
3004
|
if (this._p2pPeerPullEnabled && !hadMessageConsumer)
|
|
2788
3005
|
this._delivery.refreshPendingPeers();
|
|
@@ -3020,6 +3237,8 @@ export class AUNClient {
|
|
|
3020
3237
|
// ── 事件自动解密管线 ──────────────────────────────────────
|
|
3021
3238
|
/** 处理 transport 层推送的原始 P2P 消息 */
|
|
3022
3239
|
async _onRawMessageReceived(data) {
|
|
3240
|
+
if (this._controlMessages().onPush(data))
|
|
3241
|
+
return;
|
|
3023
3242
|
return this._delivery.onRawMessageReceived(data);
|
|
3024
3243
|
}
|
|
3025
3244
|
async _onRawMessageRecalled(data) {
|
|
@@ -3027,6 +3246,8 @@ export class AUNClient {
|
|
|
3027
3246
|
}
|
|
3028
3247
|
/** 处理群组消息推送:自动解密后 re-publish */
|
|
3029
3248
|
async _onRawGroupMessageCreated(data) {
|
|
3249
|
+
if (this._controlMessages().onPush(data, true))
|
|
3250
|
+
return;
|
|
3030
3251
|
return this._delivery.onRawGroupMessageCreated(data);
|
|
3031
3252
|
}
|
|
3032
3253
|
async _onRawGroupMessageRecalled(data) {
|
|
@@ -3114,6 +3335,8 @@ export class AUNClient {
|
|
|
3114
3335
|
this._delivery.markPublishedSeq(ns, seq);
|
|
3115
3336
|
}
|
|
3116
3337
|
_publishAppEvent(event, payload, source = 'direct', deliveryContext) {
|
|
3338
|
+
if (messageClassOf(payload) === 'sdk_control')
|
|
3339
|
+
return this._controlMessages().consume(payload);
|
|
3117
3340
|
if (event === 'message.received') {
|
|
3118
3341
|
return this._handleOwnershipMessage(payload).then((handled) => {
|
|
3119
3342
|
if (!handled)
|
|
@@ -3596,7 +3819,7 @@ export class AUNClient {
|
|
|
3596
3819
|
throw new ValidationError('gateway url unavailable for e2ee cert fetch');
|
|
3597
3820
|
}
|
|
3598
3821
|
const peerGatewayUrl = AUNClient._resolvePeerGatewayUrl(gatewayUrl, aid);
|
|
3599
|
-
const certPem = await
|
|
3822
|
+
const certPem = await this._auth._fetchRiskText(AUNClient._buildCertUrl(peerGatewayUrl, aid, certFingerprint));
|
|
3600
3823
|
if (auth !== this._auth || pkiEpoch !== auth._strictPkiEpoch)
|
|
3601
3824
|
throw new ValidationError('peer certificate fetch context changed');
|
|
3602
3825
|
const validated = await this._validateAndCachePeerCert({
|
|
@@ -4037,6 +4260,9 @@ export class AUNClient {
|
|
|
4037
4260
|
// ── 内部:连接 ────────────────────────────────────────────
|
|
4038
4261
|
/** 执行一次连接流程 */
|
|
4039
4262
|
async _connectOnce(params, allowReauth, reconnectOwner, connectionOwner) {
|
|
4263
|
+
await this._restoreRiskState();
|
|
4264
|
+
this._assertRiskReconnect();
|
|
4265
|
+
this._riskNegotiated = false;
|
|
4040
4266
|
const tStart = Date.now();
|
|
4041
4267
|
connectionOwner ??= this._connectAbort ?? undefined;
|
|
4042
4268
|
this._lifecycle.assertConnectionAttemptOwner(connectionOwner);
|
|
@@ -4046,8 +4272,15 @@ export class AUNClient {
|
|
|
4046
4272
|
this._captureCapabilitiesFromConnect(params);
|
|
4047
4273
|
this._connectDeliveryMode = { ...(params.delivery_mode ?? this._connectDeliveryMode) };
|
|
4048
4274
|
const extraInfo = (params.extra_info && typeof params.extra_info === 'object' && !Array.isArray(params.extra_info))
|
|
4049
|
-
? params.extra_info
|
|
4050
|
-
:
|
|
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
|
+
}
|
|
4051
4284
|
const prevState = this._state;
|
|
4052
4285
|
this._auth.setInstanceContext({ deviceId: this._deviceId, slotId: this._slotId });
|
|
4053
4286
|
this._state = 'connecting';
|
|
@@ -4097,6 +4330,9 @@ export class AUNClient {
|
|
|
4097
4330
|
this._applyServerHeartbeatInterval(auth.hello.heartbeat_interval, 'auth');
|
|
4098
4331
|
}
|
|
4099
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);
|
|
4100
4336
|
}
|
|
4101
4337
|
}
|
|
4102
4338
|
else {
|
|
@@ -4115,6 +4351,9 @@ export class AUNClient {
|
|
|
4115
4351
|
this._applyServerHeartbeatInterval(hello.heartbeat_interval, 'auth');
|
|
4116
4352
|
}
|
|
4117
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);
|
|
4118
4357
|
}
|
|
4119
4358
|
this._assertReconnectOwner(reconnectOwner);
|
|
4120
4359
|
this._lifecycle.assertConnectionAttemptOwner(connectionOwner);
|
|
@@ -4178,6 +4417,11 @@ export class AUNClient {
|
|
|
4178
4417
|
else {
|
|
4179
4418
|
this._clientLog.debug('V2 session init deferred for short connection');
|
|
4180
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();
|
|
4181
4425
|
// connect/reconnect 成功后自动触发一次 P2P message.v2.pull,补齐离线期间积压
|
|
4182
4426
|
// 群消息按惰性触发,不在此处主动 pull
|
|
4183
4427
|
if (backgroundSyncEnabled) {
|
|
@@ -4415,6 +4659,8 @@ export class AUNClient {
|
|
|
4415
4659
|
this._clientLog.warn(`V2 decrypt: invalid envelope_json for msg seq=${String(msg.seq)}`);
|
|
4416
4660
|
return null;
|
|
4417
4661
|
}
|
|
4662
|
+
if (messageClassOf({ ...msg, envelope }) === 'sdk_control' && source !== 'control_pull')
|
|
4663
|
+
return null;
|
|
4418
4664
|
const e2eeMeta = this._v2E2eeMeta(envelope);
|
|
4419
4665
|
if (observeAgentMd) {
|
|
4420
4666
|
if (deferredEffects)
|
|
@@ -4808,6 +5054,8 @@ export class AUNClient {
|
|
|
4808
5054
|
return this._groupState.autoConfirmPendingProposals();
|
|
4809
5055
|
}
|
|
4810
5056
|
async _onV2PushNotification(data, expectedVersion, inlineGeneration) {
|
|
5057
|
+
if (this._controlMessages().onPush(data))
|
|
5058
|
+
return;
|
|
4811
5059
|
return this._delivery.onV2PushNotification(data, expectedVersion, inlineGeneration);
|
|
4812
5060
|
}
|
|
4813
5061
|
async _onV2StateProposed(data) {
|
|
@@ -4820,6 +5068,8 @@ export class AUNClient {
|
|
|
4820
5068
|
return this._groupState.onV2StateConfirmed(data);
|
|
4821
5069
|
}
|
|
4822
5070
|
async _onRawGroupV2MessageCreated(data, expectedVersion, inlineGeneration) {
|
|
5071
|
+
if (this._controlMessages().onPush(data, true))
|
|
5072
|
+
return;
|
|
4823
5073
|
return this._delivery.onRawGroupV2MessageCreated(data, expectedVersion, inlineGeneration);
|
|
4824
5074
|
}
|
|
4825
5075
|
/** Push 通知带 payload 时的就地解密(复用 _decryptV2Message) */
|
|
@@ -5417,15 +5667,25 @@ export class AUNClient {
|
|
|
5417
5667
|
* 透传到应用层可订阅事件 'gateway.disconnect',方便业务定位被踢原因。
|
|
5418
5668
|
*/
|
|
5419
5669
|
async _onGatewayDisconnect(data) {
|
|
5420
|
-
const payload = (data && typeof data === 'object') ? data : {};
|
|
5670
|
+
const payload = (data && typeof data === 'object') ? structuredClone(data) : {};
|
|
5421
5671
|
const code = payload.code;
|
|
5422
5672
|
const reason = payload.reason ?? '';
|
|
5423
5673
|
const detail = (payload.detail && typeof payload.detail === 'object' && !Array.isArray(payload.detail))
|
|
5424
5674
|
? payload.detail
|
|
5425
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;
|
|
5426
5686
|
this._clientLog.warn(`server initiated disconnect: code=${code}, reason=${reason}, detail=${JSON.stringify(detail)}`);
|
|
5427
5687
|
const reasonText = String(reason ?? '').trim().toLowerCase();
|
|
5428
|
-
const retryable = Number(code) === 4012
|
|
5688
|
+
const retryable = Number(code) === 4012 || Number(code) === 4029
|
|
5429
5689
|
|| reasonText.includes('long_connection_already_exists')
|
|
5430
5690
|
|| reasonText.includes('long connection already exists');
|
|
5431
5691
|
this._serverKicked = !retryable;
|
|
@@ -5638,7 +5898,7 @@ export class AUNClient {
|
|
|
5638
5898
|
break;
|
|
5639
5899
|
}
|
|
5640
5900
|
this._retryAttempt = attempt;
|
|
5641
|
-
this._nextRetryAt = Date.now() + reconnectSleepDelayMs(delay, maxBaseDelay);
|
|
5901
|
+
this._nextRetryAt = Date.now() + Math.max(reconnectSleepDelayMs(delay, maxBaseDelay), this._riskReconnectDelay());
|
|
5642
5902
|
this._state = 'retry_backoff';
|
|
5643
5903
|
if (!reconnectAbort || !await this._publishReconnectEvent(reconnectAbort, 'state_change', {
|
|
5644
5904
|
state: this._publicState(this._state),
|