@agentunion/fastaun 0.4.2 → 0.4.4
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 +198 -173
- package/_packed_docs/0.4.0_/345/267/256/345/274/202/346/240/270/345/256/236/345/206/263/347/255/226/350/256/260/345/275/225.md +302 -0
- package/_packed_docs/AUN_SDK_0.4.0_/350/256/276/350/256/241/345/257/271/346/257/224/345/210/206/346/236/220.md +194 -0
- package/_packed_docs/AUN_SDK_/351/207/215/346/236/204/345/256/236/346/226/275/350/256/241/345/210/222.md +596 -596
- package/_packed_docs/AUN_SDK_/351/207/215/346/236/204/350/256/276/350/256/241/346/226/271/346/241/210_v3.md +1698 -1697
- package/_packed_docs/CHANGELOG.md +198 -173
- package/_packed_docs/INDEX.md +17 -17
- package/_packed_docs/KITE_DOCS_GUIDE.md +11 -11
- package/_packed_docs/agent.md/SCHEMA.md +49 -49
- package/_packed_docs/agent.md/examples/signed-openclaw-lobster.md +22 -22
- package/_packed_docs/agent.md//350/277/234/347/250/213agent.md/347/274/223/345/255/230/344/270/216etag/351/200/217/344/274/240/346/226/271/346/241/210.md +327 -327
- package/_packed_docs/cli/AUN-CLI/350/256/276/350/256/241/346/226/207/346/241/243.md +686 -686
- package/_packed_docs/design/2026-05-22-aun-rpc-trace-enhancement.md +542 -542
- package/_packed_docs/design/E2EE_V2/347/256/200/345/214/226/344/270/2721DH/345/212/240Per-AID_Wrap/346/226/271/346/241/210.md +124 -124
- package/_packed_docs/design//350/267/250/350/257/255/350/250/200/345/256/271/345/231/250E2E/346/265/213/350/257/225/346/226/271/346/241/210.md +665 -665
- package/_packed_docs/protocol/01-/350/272/253/344/273/275/344/270/216/345/207/255/350/257/201/345/215/217/350/256/256-auth.md +2 -2
- package/_packed_docs/protocol/14-/344/272/244/344/272/222/346/234/272/345/210/266-/345/223/215/345/272/224/346/250/241/345/274/217/344/270/216/350/207/252/344/270/273/346/250/241/345/274/217.md +170 -170
- package/_packed_docs/protocol/15-/347/246/273/347/272/277/346/216/250/351/200/201/351/200/232/347/237/245/345/215/217/350/256/256.md +419 -419
- package/_packed_docs/protocol/README.md +1 -1
- package/_packed_docs/protocol/aun-docs-guide.md +1 -1
- package/_packed_docs/protocol//351/231/204/345/275/225A-/346/234/257/350/257/255/350/241/250.md +15 -15
- package/_packed_docs/protocol//351/231/204/345/275/225B-/346/211/251/345/261/225/346/200/247/346/214/207/345/215/227.md +4 -4
- package/_packed_docs/protocol//351/231/204/345/275/225J-/345/256/242/346/210/267/347/253/257/346/216/245/345/205/245/347/244/272/344/276/213.md +98 -98
- package/_packed_docs/protocol//351/231/204/345/275/225M-JWT/350/256/244/350/257/201/345/256/236/347/216/260/346/214/207/345/215/227.md +46 -46
- package/_packed_docs/protocol//351/231/204/345/275/225N-/345/210/206/345/270/203/345/274/217Trace/345/215/217/350/256/256.md +257 -257
- package/_packed_docs/python-sdk-v2-only-changelog.md +189 -189
- package/_packed_docs/sdk/01-/345/277/253/351/200/237/345/274/200/345/247/213.md +7 -3
- package/_packed_docs/sdk/03-/346/240/270/345/277/203/346/246/202/345/277/265.md +1 -1
- package/_packed_docs/sdk/04-/350/277/236/346/216/245/344/270/216/350/256/244/350/257/201.md +3 -1
- package/_packed_docs/sdk/05-E2EE/345/212/240/345/257/206/351/200/232/344/277/241.md +1 -1
- package/_packed_docs/sdk/06-API/346/211/213/345/206/214.md +63 -15
- package/_packed_docs/sdk/09-payload-reference.md +13 -13
- package/_packed_docs/sdk/E2EE_V2/346/266/210/346/201/257/351/200/232/344/277/241/346/227/266/345/272/217/345/233/276.md +171 -171
- package/_packed_docs/sdk/README.md +5 -5
- package/dist/aid-store.js +1 -5
- package/dist/aid-store.js.map +1 -1
- package/dist/aid.d.ts +2 -1
- package/dist/aid.js +7 -6
- package/dist/aid.js.map +1 -1
- package/dist/auth.js +4 -0
- package/dist/auth.js.map +1 -1
- package/dist/client.d.ts +13 -16
- package/dist/client.js +242 -91
- package/dist/client.js.map +1 -1
- package/dist/config.d.ts +3 -0
- package/dist/config.js +17 -2
- package/dist/config.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js.map +1 -1
- package/dist/keystore/aid-db.js +103 -90
- package/dist/keystore/aid-db.js.map +1 -1
- package/dist/keystore/file.d.ts +0 -2
- package/dist/keystore/file.js +6 -44
- package/dist/keystore/file.js.map +1 -1
- package/dist/tools/cross-sdk-agent.js +0 -9
- package/dist/tools/cross-sdk-agent.js.map +1 -1
- package/dist/transport.d.ts +1 -0
- package/dist/transport.js +7 -1
- package/dist/transport.js.map +1 -1
- package/dist/v2/session/keystore.js +2 -2
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +1 -1
package/dist/client.js
CHANGED
|
@@ -16,7 +16,7 @@ import * as http from 'node:http';
|
|
|
16
16
|
import * as https from 'node:https';
|
|
17
17
|
import * as path from 'node:path';
|
|
18
18
|
import { URL } from 'node:url';
|
|
19
|
-
import { configFromMap, getDeviceId, normalizeInstanceId } from './config.js';
|
|
19
|
+
import { configFromMap, getDeviceId, normalizeInstanceId, normalizeSlotId, slotIsolationKey } from './config.js';
|
|
20
20
|
import { CryptoProvider } from './crypto.js';
|
|
21
21
|
import { GatewayDiscovery } from './discovery.js';
|
|
22
22
|
import { DnsResilientNet } from './net.js';
|
|
@@ -37,6 +37,14 @@ import { AID } from './aid.js';
|
|
|
37
37
|
function isPromiseLike(value) {
|
|
38
38
|
return Boolean(value && typeof value.then === 'function');
|
|
39
39
|
}
|
|
40
|
+
function isAIDObject(value) {
|
|
41
|
+
const candidate = value;
|
|
42
|
+
return Boolean(candidate
|
|
43
|
+
&& typeof candidate === 'object'
|
|
44
|
+
&& typeof candidate.aid === 'string'
|
|
45
|
+
&& typeof candidate.aunPath === 'string'
|
|
46
|
+
&& typeof candidate.isPrivateKeyValid === 'function');
|
|
47
|
+
}
|
|
40
48
|
/**
|
|
41
49
|
* 递归排序键的 JSON 序列化(Canonical JSON for AUN)
|
|
42
50
|
* 等价于 Python json.dumps(sort_keys=True, separators=(",",":"), ensure_ascii=False)
|
|
@@ -128,6 +136,20 @@ const DEFAULT_SESSION_OPTIONS = {
|
|
|
128
136
|
http: 30.0,
|
|
129
137
|
},
|
|
130
138
|
};
|
|
139
|
+
const PUBLIC_CONNECTION_OPTION_KEYS = new Set([
|
|
140
|
+
'auto_reconnect',
|
|
141
|
+
'connect_timeout',
|
|
142
|
+
'retry_initial_delay',
|
|
143
|
+
'retry_max_delay',
|
|
144
|
+
'retry_max_attempts',
|
|
145
|
+
'heartbeat_interval',
|
|
146
|
+
'call_timeout',
|
|
147
|
+
'connection_kind',
|
|
148
|
+
'short_ttl_ms',
|
|
149
|
+
'delivery_mode',
|
|
150
|
+
'extra_info',
|
|
151
|
+
'background_sync',
|
|
152
|
+
]);
|
|
131
153
|
const PROTECTED_HEADERS_METHODS = new Set([
|
|
132
154
|
'message.send',
|
|
133
155
|
'group.send',
|
|
@@ -410,22 +432,6 @@ async function fetchWithTimeout(input, init, timeoutMs = AGENT_MD_HTTP_TIMEOUT_M
|
|
|
410
432
|
clearTimeout(timer);
|
|
411
433
|
}
|
|
412
434
|
}
|
|
413
|
-
function assertClientOptions(value, label) {
|
|
414
|
-
if (value == null)
|
|
415
|
-
return;
|
|
416
|
-
if (typeof value !== 'object' || Array.isArray(value) || value instanceof AID) {
|
|
417
|
-
throw new ValidationError(`${label} must be an options object`);
|
|
418
|
-
}
|
|
419
|
-
}
|
|
420
|
-
function clientOptionsConfig(options) {
|
|
421
|
-
const raw = { ...(options ?? {}) };
|
|
422
|
-
if (Object.prototype.hasOwnProperty.call(raw, 'aid')) {
|
|
423
|
-
throw new ValidationError('AUNClient options must not include aid; pass an AID object as the first argument');
|
|
424
|
-
}
|
|
425
|
-
delete raw.debug;
|
|
426
|
-
delete raw.protected_headers;
|
|
427
|
-
return raw;
|
|
428
|
-
}
|
|
429
435
|
export class AUNClient {
|
|
430
436
|
/** 原始配置 */
|
|
431
437
|
config;
|
|
@@ -514,6 +520,7 @@ export class AUNClient {
|
|
|
514
520
|
// ── V2 E2EE 状态 ──────────────────────────────────────────────
|
|
515
521
|
_v2Session;
|
|
516
522
|
_v2KeyStore;
|
|
523
|
+
_v2SessionInitInFlight = null;
|
|
517
524
|
/** V2 bootstrap 缓存:aid/group:id → 设备列表 + 时间戳 */
|
|
518
525
|
_v2BootstrapCache = new Map();
|
|
519
526
|
_connectCapabilities = null;
|
|
@@ -543,12 +550,11 @@ export class AUNClient {
|
|
|
543
550
|
_logger;
|
|
544
551
|
_clientLog;
|
|
545
552
|
constructor(aid) {
|
|
546
|
-
if (
|
|
547
|
-
throw new ValidationError('AUNClient
|
|
553
|
+
if (aid !== null && aid !== undefined && !isAIDObject(aid)) {
|
|
554
|
+
throw new ValidationError('AUNClient only accepts an AID object or no argument');
|
|
548
555
|
}
|
|
549
|
-
const inputAid = aid
|
|
550
|
-
const
|
|
551
|
-
const rawConfig = clientOptionsConfig(options);
|
|
556
|
+
const inputAid = aid ?? null;
|
|
557
|
+
const rawConfig = {};
|
|
552
558
|
if (inputAid) {
|
|
553
559
|
rawConfig.aun_path = inputAid.aunPath;
|
|
554
560
|
rawConfig.verify_ssl = inputAid.verifySsl;
|
|
@@ -557,7 +563,7 @@ export class AUNClient {
|
|
|
557
563
|
rawConfig.debug = inputAid.debug;
|
|
558
564
|
}
|
|
559
565
|
this._configModel = configFromMap(rawConfig);
|
|
560
|
-
const initAid = inputAid ? inputAid.aid : null;
|
|
566
|
+
const initAid = (inputAid && inputAid.isPrivateKeyValid()) ? inputAid.aid : null;
|
|
561
567
|
this._agentMdPath = path.join(this._configModel.aunPath, 'AIDs');
|
|
562
568
|
this.config = {
|
|
563
569
|
aun_path: this._configModel.aunPath,
|
|
@@ -583,7 +589,6 @@ export class AUNClient {
|
|
|
583
589
|
});
|
|
584
590
|
this._discovery = new GatewayDiscovery({ verifySsl: this._configModel.verifySsl, logger: this._clientLog, net: dnsNet });
|
|
585
591
|
const keystore = new FileKeyStore(this._configModel.aunPath, {
|
|
586
|
-
encryptionSeed: this._configModel.seedPassword ?? undefined,
|
|
587
592
|
logger: this._logger.for('aun_core.keystore'),
|
|
588
593
|
secretStoreLogger: this._logger.for('aun_core.secret-store'),
|
|
589
594
|
});
|
|
@@ -626,17 +631,17 @@ export class AUNClient {
|
|
|
626
631
|
});
|
|
627
632
|
this._transport.setMetaObserver((meta) => this._observeRpcMeta(meta));
|
|
628
633
|
if (inputAid) {
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
634
|
+
// 与 Python 对齐:私钥无效时只用 aunPath 配置,state 保持 no_identity
|
|
635
|
+
if (inputAid.isPrivateKeyValid()) {
|
|
636
|
+
this._currentAid = inputAid;
|
|
637
|
+
this._identity = {
|
|
638
|
+
aid: inputAid.aid,
|
|
639
|
+
private_key_pem: inputAid.privateKeyPem,
|
|
640
|
+
public_key_der_b64: inputAid.publicKey,
|
|
641
|
+
cert: inputAid.certPem,
|
|
642
|
+
};
|
|
643
|
+
this._state = 'standby';
|
|
644
|
+
}
|
|
640
645
|
}
|
|
641
646
|
// 内部订阅:推送消息自动解密后 re-publish 给用户
|
|
642
647
|
this._dispatcher.subscribe('_raw.message.received', (data) => this._onRawMessageReceived(data));
|
|
@@ -714,6 +719,71 @@ export class AUNClient {
|
|
|
714
719
|
get lastErrorCode() {
|
|
715
720
|
return this._lastErrorCode;
|
|
716
721
|
}
|
|
722
|
+
_applyAidRuntimeContext(aid) {
|
|
723
|
+
const rawConfig = {
|
|
724
|
+
aun_path: aid.aunPath,
|
|
725
|
+
verify_ssl: aid.verifySsl,
|
|
726
|
+
debug: aid.debug,
|
|
727
|
+
};
|
|
728
|
+
if (aid.rootCaPath)
|
|
729
|
+
rawConfig.root_ca_path = aid.rootCaPath;
|
|
730
|
+
const nextConfig = configFromMap(rawConfig);
|
|
731
|
+
try {
|
|
732
|
+
const close = this._keystore.close;
|
|
733
|
+
if (typeof close === 'function')
|
|
734
|
+
close.call(this._keystore);
|
|
735
|
+
}
|
|
736
|
+
catch {
|
|
737
|
+
// best-effort cleanup before switching keystore roots
|
|
738
|
+
}
|
|
739
|
+
this._configModel = nextConfig;
|
|
740
|
+
this.config.aun_path = nextConfig.aunPath;
|
|
741
|
+
this.config.root_ca_path = nextConfig.rootCaPath;
|
|
742
|
+
this.config.seed_password = nextConfig.seedPassword;
|
|
743
|
+
this._agentMdPath = path.join(nextConfig.aunPath, 'AIDs');
|
|
744
|
+
this._agentMdCache.clear();
|
|
745
|
+
this._agentMdFetchInflight.clear();
|
|
746
|
+
this._agentMdDownloadInflight.clear();
|
|
747
|
+
this._peerCache.clear();
|
|
748
|
+
this._certCache.clear();
|
|
749
|
+
this._gatewayUrl = null;
|
|
750
|
+
this._deviceId = aid.deviceId || getDeviceId(nextConfig.aunPath);
|
|
751
|
+
this._slotId = aid.slotId || 'default';
|
|
752
|
+
const debugFlag = nextConfig.debug;
|
|
753
|
+
this._logger = new AUNLogger({ debug: debugFlag, aunPath: nextConfig.aunPath });
|
|
754
|
+
this._logger.bindDeviceId(this._deviceId);
|
|
755
|
+
this._clientLog = this._logger.for('aun_core.client');
|
|
756
|
+
const dnsNet = new DnsResilientNet({
|
|
757
|
+
verifySsl: nextConfig.verifySsl,
|
|
758
|
+
logger: this._clientLog,
|
|
759
|
+
});
|
|
760
|
+
this._discovery = new GatewayDiscovery({ verifySsl: nextConfig.verifySsl, logger: this._clientLog, net: dnsNet });
|
|
761
|
+
const keystore = new FileKeyStore(nextConfig.aunPath, {
|
|
762
|
+
logger: this._logger.for('aun_core.keystore'),
|
|
763
|
+
secretStoreLogger: this._logger.for('aun_core.secret-store'),
|
|
764
|
+
});
|
|
765
|
+
this._keystore = keystore;
|
|
766
|
+
this._auth = new AuthFlow({
|
|
767
|
+
keystore,
|
|
768
|
+
crypto: new CryptoProvider(),
|
|
769
|
+
aid: aid.aid,
|
|
770
|
+
deviceId: this._deviceId,
|
|
771
|
+
slotId: this._slotId,
|
|
772
|
+
rootCaPath: nextConfig.rootCaPath ?? undefined,
|
|
773
|
+
verifySsl: nextConfig.verifySsl,
|
|
774
|
+
logger: this._logger.for('aun_core.auth'),
|
|
775
|
+
net: dnsNet,
|
|
776
|
+
});
|
|
777
|
+
this._transport = new RPCTransport({
|
|
778
|
+
eventDispatcher: this._dispatcher,
|
|
779
|
+
timeout: 10_000,
|
|
780
|
+
onDisconnect: (err, closeCode) => this._handleTransportDisconnect(err, closeCode),
|
|
781
|
+
verifySsl: nextConfig.verifySsl,
|
|
782
|
+
logger: this._logger.for('aun_core.transport'),
|
|
783
|
+
dnsNet,
|
|
784
|
+
});
|
|
785
|
+
this._transport.setMetaObserver((meta) => this._observeRpcMeta(meta));
|
|
786
|
+
}
|
|
717
787
|
loadIdentity(aid) {
|
|
718
788
|
if (!aid?.isPrivateKeyValid()) {
|
|
719
789
|
throw new StateError('loadIdentity requires an AID with a valid private key');
|
|
@@ -722,15 +792,15 @@ export class AUNClient {
|
|
|
722
792
|
if (publicState !== ConnectionState.NO_IDENTITY && publicState !== ConnectionState.CLOSED) {
|
|
723
793
|
throw new StateError(`loadIdentity not allowed in state ${publicState}`);
|
|
724
794
|
}
|
|
795
|
+
this._applyAidRuntimeContext(aid);
|
|
725
796
|
this._currentAid = aid;
|
|
726
797
|
this._aid = aid.aid;
|
|
727
798
|
this._identity = {
|
|
728
799
|
aid: aid.aid,
|
|
729
|
-
private_key_pem: aid.
|
|
800
|
+
private_key_pem: aid.privateKeyPem,
|
|
730
801
|
public_key_der_b64: aid.publicKey,
|
|
731
802
|
cert: aid.certPem,
|
|
732
803
|
};
|
|
733
|
-
this._auth._aid = aid.aid;
|
|
734
804
|
this._state = 'standby';
|
|
735
805
|
this._closing = false;
|
|
736
806
|
this._lastError = null;
|
|
@@ -1028,7 +1098,7 @@ export class AUNClient {
|
|
|
1028
1098
|
*/
|
|
1029
1099
|
async publishAgentMd() {
|
|
1030
1100
|
const target = this._agentMdOwnerAid();
|
|
1031
|
-
if (!target) {
|
|
1101
|
+
if (!target || !this._currentAid) {
|
|
1032
1102
|
throw new ValidationError('publishAgentMd requires local AID');
|
|
1033
1103
|
}
|
|
1034
1104
|
const content = this._readAgentMdContent(target);
|
|
@@ -1116,7 +1186,7 @@ export class AUNClient {
|
|
|
1116
1186
|
this._agentMdCache.clear();
|
|
1117
1187
|
return this._agentMdPath;
|
|
1118
1188
|
}
|
|
1119
|
-
/**
|
|
1189
|
+
/** 返回本地 agent.md 文件的 etag;未设置或读取失败时返回空串。 */
|
|
1120
1190
|
getLocalAgentMdEtag() {
|
|
1121
1191
|
return this._localAgentMdEtag;
|
|
1122
1192
|
}
|
|
@@ -1604,12 +1674,18 @@ export class AUNClient {
|
|
|
1604
1674
|
/** 连接到 Gateway;身份来自构造函数或 loadIdentity(aid),认证由 SDK 内部自动完成。 */
|
|
1605
1675
|
async connect(opts) {
|
|
1606
1676
|
const tStart = Date.now();
|
|
1607
|
-
|
|
1677
|
+
// 先校验非法参数(ValidationError),再检查身份(StateError)
|
|
1678
|
+
if (opts !== undefined && opts !== null && typeof opts === 'object') {
|
|
1608
1679
|
const raw = opts;
|
|
1609
|
-
|
|
1610
|
-
|
|
1680
|
+
const invalid = Object.keys(raw).filter((key) => !PUBLIC_CONNECTION_OPTION_KEYS.has(key)).sort();
|
|
1681
|
+
if (invalid.length > 0) {
|
|
1682
|
+
throw new ValidationError(`connect options contain unsupported field(s): ${invalid.join(', ')}`);
|
|
1611
1683
|
}
|
|
1612
1684
|
}
|
|
1685
|
+
const target = this._currentAid?.aid ?? this._aid ?? '';
|
|
1686
|
+
if (!target || !this._currentAid?.isPrivateKeyValid()) {
|
|
1687
|
+
throw new StateError('connect requires a loaded AID with a valid private key');
|
|
1688
|
+
}
|
|
1613
1689
|
const options = {};
|
|
1614
1690
|
if (opts?.auto_reconnect !== undefined)
|
|
1615
1691
|
options.auto_reconnect = opts.auto_reconnect;
|
|
@@ -1628,10 +1704,16 @@ export class AUNClient {
|
|
|
1628
1704
|
max_attempts: opts.retry_max_attempts ?? 0,
|
|
1629
1705
|
};
|
|
1630
1706
|
}
|
|
1631
|
-
|
|
1632
|
-
|
|
1633
|
-
|
|
1634
|
-
|
|
1707
|
+
if (opts?.connection_kind !== undefined)
|
|
1708
|
+
options.connection_kind = opts.connection_kind;
|
|
1709
|
+
if (opts?.short_ttl_ms !== undefined)
|
|
1710
|
+
options.short_ttl_ms = opts.short_ttl_ms;
|
|
1711
|
+
if (opts?.delivery_mode !== undefined)
|
|
1712
|
+
options.delivery_mode = opts.delivery_mode;
|
|
1713
|
+
if (opts?.extra_info !== undefined)
|
|
1714
|
+
options.extra_info = opts.extra_info;
|
|
1715
|
+
if (opts?.background_sync !== undefined)
|
|
1716
|
+
options.background_sync = opts.background_sync;
|
|
1635
1717
|
const publicState = this.state;
|
|
1636
1718
|
const allowed = new Set([
|
|
1637
1719
|
ConnectionState.STANDBY,
|
|
@@ -1793,6 +1875,14 @@ export class AUNClient {
|
|
|
1793
1875
|
}
|
|
1794
1876
|
this._validateOutboundCall(method, p);
|
|
1795
1877
|
this._injectMessageCursorContext(method, p);
|
|
1878
|
+
if (method.startsWith('group.')
|
|
1879
|
+
&& !('_group_cursor_params' in p)
|
|
1880
|
+
&& !Boolean(p._pull_gate_locked)) {
|
|
1881
|
+
const explicitCursorParams = this._groupCursorParams(p);
|
|
1882
|
+
if (Object.keys(explicitCursorParams).length > 0) {
|
|
1883
|
+
p._group_cursor_params = explicitCursorParams;
|
|
1884
|
+
}
|
|
1885
|
+
}
|
|
1796
1886
|
// group.* 方法的 group_id 归一化为 canonical 格式(兼容老/污染数据)
|
|
1797
1887
|
if (method.startsWith('group.') && p.group_id !== undefined && p.group_id !== null) {
|
|
1798
1888
|
const rawGroupId = String(p.group_id);
|
|
@@ -1894,7 +1984,17 @@ export class AUNClient {
|
|
|
1894
1984
|
throw new ValidationError('group.pull requires group_id');
|
|
1895
1985
|
}
|
|
1896
1986
|
await this._ensureV2SessionReady('group.pull');
|
|
1897
|
-
const
|
|
1987
|
+
const hasExplicitAfterSeq = 'after_seq' in p || 'after_message_seq' in p;
|
|
1988
|
+
const cursorParams = this._explicitGroupCursorParams(p);
|
|
1989
|
+
const ownsCursor = Object.keys(cursorParams).length === 0 || this._groupCursorTargetsCurrentInstance(cursorParams);
|
|
1990
|
+
const pullOpts = { gateLocked: true };
|
|
1991
|
+
if (hasExplicitAfterSeq)
|
|
1992
|
+
pullOpts.explicitAfterSeq = true;
|
|
1993
|
+
if (Object.keys(cursorParams).length > 0)
|
|
1994
|
+
pullOpts.cursorParams = cursorParams;
|
|
1995
|
+
if (!ownsCursor)
|
|
1996
|
+
pullOpts.ownsCursor = false;
|
|
1997
|
+
const messages = await runWithRpcPriority(() => this._pullGroupV2(String(p.group_id), Number(p.after_seq ?? p.after_message_seq ?? 0) || 0, Number(p.limit ?? 50) || 50, pullOpts));
|
|
1898
1998
|
return { messages };
|
|
1899
1999
|
}
|
|
1900
2000
|
if (method === 'group.ack_messages' || method === 'group.v2.ack') {
|
|
@@ -1902,12 +2002,18 @@ export class AUNClient {
|
|
|
1902
2002
|
throw new ValidationError('group.ack_messages requires group_id');
|
|
1903
2003
|
}
|
|
1904
2004
|
await this._ensureV2SessionReady('group.ack_messages');
|
|
2005
|
+
const cursorParams = this._explicitGroupCursorParams(p);
|
|
2006
|
+
const ownsCursor = Object.keys(cursorParams).length === 0 || this._groupCursorTargetsCurrentInstance(cursorParams);
|
|
2007
|
+
if (method === 'group.ack_messages' && !ownsCursor) {
|
|
2008
|
+
return await runWithRpcPriority(() => this._rawGroupAckMessages(p));
|
|
2009
|
+
}
|
|
1905
2010
|
return await runWithRpcPriority(() => this._ackGroupV2(String(p.group_id), Number(p.seq ?? p.msg_seq ?? p.up_to_seq ?? 0) || undefined));
|
|
1906
2011
|
}
|
|
1907
2012
|
if (method === 'message.pull') {
|
|
1908
2013
|
delete p._skip_auto_ack;
|
|
1909
2014
|
delete p.skip_auto_ack;
|
|
1910
2015
|
}
|
|
2016
|
+
delete p._group_cursor_params;
|
|
1911
2017
|
// 关键操作自动附加客户端签名
|
|
1912
2018
|
if (SIGNED_METHODS.has(method)) {
|
|
1913
2019
|
if (this._shouldSkipClientSignature(method, p)) {
|
|
@@ -1978,6 +2084,7 @@ export class AUNClient {
|
|
|
1978
2084
|
delete p._pull_gate_locked;
|
|
1979
2085
|
delete p._skip_auto_ack;
|
|
1980
2086
|
delete p.skip_auto_ack;
|
|
2087
|
+
delete p._group_cursor_params;
|
|
1981
2088
|
if (method.startsWith('group.') && p.group_id !== undefined && p.group_id !== null) {
|
|
1982
2089
|
p.group_id = normalizeGroupId(String(p.group_id)) || String(p.group_id);
|
|
1983
2090
|
}
|
|
@@ -2026,13 +2133,12 @@ export class AUNClient {
|
|
|
2026
2133
|
* 签名覆盖所有非 _ 前缀且非 client_signature 的业务字段。
|
|
2027
2134
|
*/
|
|
2028
2135
|
_signClientOperation(method, params) {
|
|
2029
|
-
const
|
|
2030
|
-
if (!
|
|
2136
|
+
const currentAid = this._currentAid;
|
|
2137
|
+
if (!currentAid?.privateKeyPem)
|
|
2031
2138
|
return;
|
|
2032
2139
|
try {
|
|
2033
|
-
const aid =
|
|
2140
|
+
const aid = currentAid.aid;
|
|
2034
2141
|
const ts = String(Math.floor(Date.now() / 1000));
|
|
2035
|
-
// 计算 params hash — 必须递归排序所有键(与 Python json.dumps(sort_keys=True, separators=(",",":")) 一致)
|
|
2036
2142
|
const paramsForHash = {};
|
|
2037
2143
|
for (const [k, v] of Object.entries(params)) {
|
|
2038
2144
|
if (k !== 'client_signature' && !k.startsWith('_')) {
|
|
@@ -2042,11 +2148,11 @@ export class AUNClient {
|
|
|
2042
2148
|
const paramsJson = stableStringify(paramsForHash);
|
|
2043
2149
|
const paramsHash = crypto.createHash('sha256').update(paramsJson, 'utf-8').digest('hex');
|
|
2044
2150
|
const signData = Buffer.from(`${method}|${aid}|${ts}|${paramsHash}`, 'utf-8');
|
|
2045
|
-
const privateKey = crypto.createPrivateKey(
|
|
2151
|
+
const privateKey = crypto.createPrivateKey(currentAid.privateKeyPem);
|
|
2046
2152
|
const signature = crypto.sign('SHA256', signData, privateKey);
|
|
2047
2153
|
// 证书指纹
|
|
2048
2154
|
let certFingerprint = '';
|
|
2049
|
-
const certPem =
|
|
2155
|
+
const certPem = currentAid.certPem;
|
|
2050
2156
|
if (certPem) {
|
|
2051
2157
|
const certObj = new crypto.X509Certificate(certPem);
|
|
2052
2158
|
certFingerprint = 'sha256:' + certObj.fingerprint256.replace(/:/g, '').toLowerCase();
|
|
@@ -2605,7 +2711,7 @@ export class AUNClient {
|
|
|
2605
2711
|
}
|
|
2606
2712
|
if ('slot_id' in message) {
|
|
2607
2713
|
const targetSlotId = String(message.slot_id ?? '').trim();
|
|
2608
|
-
if (targetSlotId !== this._slotId) {
|
|
2714
|
+
if (slotIsolationKey(targetSlotId) !== slotIsolationKey(this._slotId)) {
|
|
2609
2715
|
return false;
|
|
2610
2716
|
}
|
|
2611
2717
|
}
|
|
@@ -2752,6 +2858,27 @@ export class AUNClient {
|
|
|
2752
2858
|
}
|
|
2753
2859
|
return Math.max(0, ...values.filter((value) => Number.isFinite(value)));
|
|
2754
2860
|
}
|
|
2861
|
+
_groupCursorParams(params) {
|
|
2862
|
+
const cursorParams = {};
|
|
2863
|
+
for (const key of ['device_id', 'slot_id', 'device_name', 'device_type']) {
|
|
2864
|
+
const value = params[key];
|
|
2865
|
+
if (value !== undefined && value !== null)
|
|
2866
|
+
cursorParams[key] = value;
|
|
2867
|
+
}
|
|
2868
|
+
return cursorParams;
|
|
2869
|
+
}
|
|
2870
|
+
_explicitGroupCursorParams(params) {
|
|
2871
|
+
const value = params._group_cursor_params;
|
|
2872
|
+
if (!isJsonObject(value))
|
|
2873
|
+
return {};
|
|
2874
|
+
return { ...value };
|
|
2875
|
+
}
|
|
2876
|
+
_groupCursorTargetsCurrentInstance(params) {
|
|
2877
|
+
const deviceId = String(params.device_id ?? '').trim();
|
|
2878
|
+
const slotId = String(params.slot_id ?? '').trim();
|
|
2879
|
+
return (!deviceId || deviceId === (this._deviceId ?? ''))
|
|
2880
|
+
&& (!slotId || slotId === (this._slotId ?? ''));
|
|
2881
|
+
}
|
|
2755
2882
|
_schedulePullFollowup(method, params, result) {
|
|
2756
2883
|
if (method === 'message.pull')
|
|
2757
2884
|
method = 'message.v2.pull';
|
|
@@ -4031,18 +4158,30 @@ export class AUNClient {
|
|
|
4031
4158
|
this._restoreSeqTrackerState();
|
|
4032
4159
|
}
|
|
4033
4160
|
this._startBackgroundTasks();
|
|
4034
|
-
|
|
4035
|
-
|
|
4036
|
-
|
|
4161
|
+
const connectionKind = String(params.connection_kind ?? 'long');
|
|
4162
|
+
const isShortConnection = connectionKind === 'short';
|
|
4163
|
+
if (!isShortConnection) {
|
|
4164
|
+
// V2 E2EE:长连接上线时初始化 session 并注册本设备 SPK。
|
|
4165
|
+
try {
|
|
4166
|
+
await this._initV2Session();
|
|
4167
|
+
}
|
|
4168
|
+
catch (exc) {
|
|
4169
|
+
this._clientLog.warn(`V2 session init failed (non-fatal): ${formatCaughtError(exc)}`);
|
|
4170
|
+
}
|
|
4037
4171
|
}
|
|
4038
|
-
|
|
4039
|
-
this._clientLog.
|
|
4172
|
+
else {
|
|
4173
|
+
this._clientLog.debug('V2 session init deferred for short connection');
|
|
4040
4174
|
}
|
|
4041
4175
|
// connect/reconnect 成功后自动触发一次 P2P message.v2.pull,补齐离线期间积压
|
|
4042
4176
|
// 群消息按惰性触发,不在此处主动 pull
|
|
4043
|
-
|
|
4044
|
-
|
|
4045
|
-
|
|
4177
|
+
const hasExplicitBackgroundSync = Object.prototype.hasOwnProperty.call(params, 'background_sync');
|
|
4178
|
+
const backgroundSyncEnabled = this._sessionOptions.background_sync !== false
|
|
4179
|
+
&& (!isShortConnection || hasExplicitBackgroundSync);
|
|
4180
|
+
if (backgroundSyncEnabled) {
|
|
4181
|
+
void this._fillP2pGap().catch((exc) => {
|
|
4182
|
+
this._clientLog.warn(`schedule post-connect P2P gap fill failed: ${formatCaughtError(exc)}`);
|
|
4183
|
+
});
|
|
4184
|
+
}
|
|
4046
4185
|
this._clientLog.debug(`_connectOnce exit: elapsed=${Date.now() - tStart}ms gateway=${gatewayUrl}, aid=${this._aid ?? ''}`);
|
|
4047
4186
|
}
|
|
4048
4187
|
catch (err) {
|
|
@@ -4077,6 +4216,15 @@ export class AUNClient {
|
|
|
4077
4216
|
}
|
|
4078
4217
|
/** V2-only:所有加密入口都必须有 V2 session。 */
|
|
4079
4218
|
async _ensureV2SessionReady(method, errorMessage) {
|
|
4219
|
+
if (!this._v2Session) {
|
|
4220
|
+
if (!this._v2SessionInitInFlight) {
|
|
4221
|
+
this._v2SessionInitInFlight = this._initV2Session()
|
|
4222
|
+
.finally(() => {
|
|
4223
|
+
this._v2SessionInitInFlight = null;
|
|
4224
|
+
});
|
|
4225
|
+
}
|
|
4226
|
+
await this._v2SessionInitInFlight;
|
|
4227
|
+
}
|
|
4080
4228
|
if (!this._v2Session) {
|
|
4081
4229
|
throw new StateError(errorMessage ?? `V2 session not initialized; encrypted ${method} requires E2EE V2`);
|
|
4082
4230
|
}
|
|
@@ -4109,32 +4257,13 @@ export class AUNClient {
|
|
|
4109
4257
|
identity = null;
|
|
4110
4258
|
}
|
|
4111
4259
|
}
|
|
4112
|
-
|
|
4113
|
-
|
|
4114
|
-
|
|
4115
|
-
identity = this._keystore.loadIdentity(this._aid);
|
|
4116
|
-
if (identity?.private_key_pem) {
|
|
4117
|
-
this._identity = identity;
|
|
4118
|
-
this._clientLog.warn('V2 session init: identity cache was stale, reloaded from keystore');
|
|
4119
|
-
// 重新持久化 instance_state,清理脏数据
|
|
4120
|
-
const persistIdentity = this._auth._persistIdentity;
|
|
4121
|
-
if (typeof persistIdentity === 'function') {
|
|
4122
|
-
try {
|
|
4123
|
-
persistIdentity.call(this._auth, identity);
|
|
4124
|
-
}
|
|
4125
|
-
catch { /* best-effort */ }
|
|
4126
|
-
}
|
|
4127
|
-
}
|
|
4128
|
-
}
|
|
4129
|
-
catch {
|
|
4130
|
-
identity = null;
|
|
4131
|
-
}
|
|
4132
|
-
}
|
|
4133
|
-
if (!identity?.private_key_pem) {
|
|
4260
|
+
// 私钥由 AIDStore 管理,直接从 _currentAid 读取明文私钥
|
|
4261
|
+
const currentAid = this._currentAid;
|
|
4262
|
+
if (!currentAid?.privateKeyPem) {
|
|
4134
4263
|
this._clientLog.warn('V2 session init skipped: no AID private key');
|
|
4135
4264
|
return;
|
|
4136
4265
|
}
|
|
4137
|
-
const privateKey = crypto.createPrivateKey(
|
|
4266
|
+
const privateKey = crypto.createPrivateKey(currentAid.privateKeyPem);
|
|
4138
4267
|
const jwk = privateKey.export({ format: 'jwk' });
|
|
4139
4268
|
if (jwk.kty !== 'EC' || jwk.crv !== 'P-256' || !jwk.d) {
|
|
4140
4269
|
throw new StateError('AID private key must be EC P-256');
|
|
@@ -4680,6 +4809,7 @@ export class AUNClient {
|
|
|
4680
4809
|
decrypted.push(plaintext);
|
|
4681
4810
|
this._logMessageDebug('decrypt-ok', 'message.v2.pull', 'message.received', plaintext);
|
|
4682
4811
|
}
|
|
4812
|
+
const hasServerAckSeq = Object.prototype.hasOwnProperty.call(result, 'server_ack_seq');
|
|
4683
4813
|
const serverAckSeq = Number(result.server_ack_seq ?? 0);
|
|
4684
4814
|
if (ns && Number.isFinite(serverAckSeq) && serverAckSeq > 0) {
|
|
4685
4815
|
const contig = this._seqTracker.getContiguousSeq(ns);
|
|
@@ -4695,7 +4825,11 @@ export class AUNClient {
|
|
|
4695
4825
|
await this._drainOrderedMessages(ns, undefined, true);
|
|
4696
4826
|
this._saveSeqTrackerState();
|
|
4697
4827
|
}
|
|
4698
|
-
|
|
4828
|
+
const ackNeeded = messages.length > 0
|
|
4829
|
+
&& ackSeq > 0
|
|
4830
|
+
&& !opts?.skipAutoAck
|
|
4831
|
+
&& (contigAdvanced || (hasServerAckSeq && ackSeq > serverAckSeq));
|
|
4832
|
+
if (ackNeeded) {
|
|
4699
4833
|
this._clientLog.debug(`message.v2.pull scheduling auto-ack: ns=${ns}, ack_seq=${ackSeq}, raw_count=${messages.length}`);
|
|
4700
4834
|
this._safeAsync(this._ackV2(ackSeq).then(() => undefined));
|
|
4701
4835
|
}
|
|
@@ -4958,7 +5092,9 @@ export class AUNClient {
|
|
|
4958
5092
|
}
|
|
4959
5093
|
const decrypted = [];
|
|
4960
5094
|
let totalRawCount = 0;
|
|
4961
|
-
|
|
5095
|
+
const cursorParams = opts?.cursorParams ?? {};
|
|
5096
|
+
const ownsCursor = opts?.ownsCursor !== false;
|
|
5097
|
+
let nextAfterSeq = opts?.explicitAfterSeq ? afterSeq : (afterSeq || this._seqTracker.getContiguousSeq(ns));
|
|
4962
5098
|
let pageCount = 0;
|
|
4963
5099
|
const maxPages = 100;
|
|
4964
5100
|
while (pageCount < maxPages) {
|
|
@@ -4968,6 +5104,7 @@ export class AUNClient {
|
|
|
4968
5104
|
group_id: gid,
|
|
4969
5105
|
after_seq: nextAfterSeq,
|
|
4970
5106
|
limit,
|
|
5107
|
+
...cursorParams,
|
|
4971
5108
|
});
|
|
4972
5109
|
const messages = (Array.isArray(result.messages) ? result.messages : []);
|
|
4973
5110
|
totalRawCount += messages.length;
|
|
@@ -5046,7 +5183,9 @@ export class AUNClient {
|
|
|
5046
5183
|
decrypted.push(plaintext);
|
|
5047
5184
|
this._logMessageDebug('decrypt-ok', 'group.v2.pull', 'group.message_created', plaintext);
|
|
5048
5185
|
}
|
|
5049
|
-
const
|
|
5186
|
+
const cursorCurrentSeq = Number(cursor?.current_seq ?? 0);
|
|
5187
|
+
const hasServerCursor = cursor !== null && Object.prototype.hasOwnProperty.call(cursor, 'current_seq');
|
|
5188
|
+
const retentionFloor = Math.max(this._pullRetentionFloor(result, 'retention_floor_message_seq', 'retention_floor_message_seq'), Number.isFinite(cursorCurrentSeq) ? cursorCurrentSeq : 0);
|
|
5050
5189
|
if (retentionFloor > 0) {
|
|
5051
5190
|
const contig = this._seqTracker.getContiguousSeq(ns);
|
|
5052
5191
|
if (contig < retentionFloor) {
|
|
@@ -5060,11 +5199,17 @@ export class AUNClient {
|
|
|
5060
5199
|
await this._drainOrderedMessages(ns, undefined, true);
|
|
5061
5200
|
this._saveSeqTrackerState();
|
|
5062
5201
|
}
|
|
5063
|
-
|
|
5202
|
+
const ackNeeded = messages.length > 0
|
|
5203
|
+
&& ackSeq > 0
|
|
5204
|
+
&& ownsCursor
|
|
5205
|
+
&& (contigAdvanced || (hasServerCursor && ackSeq > cursorCurrentSeq));
|
|
5206
|
+
if (ackNeeded) {
|
|
5064
5207
|
this._clientLog.debug(`group.v2.pull scheduling auto-ack: group=${gid}, ns=${ns}, ack_seq=${ackSeq}, raw_count=${messages.length}`);
|
|
5065
5208
|
this._safeAsync(this._ackGroupV2(gid, ackSeq).then(() => undefined));
|
|
5066
5209
|
}
|
|
5067
5210
|
const nextAfter = Math.max(pageMaxSeq, nextAfterSeq);
|
|
5211
|
+
if (!ownsCursor)
|
|
5212
|
+
break;
|
|
5068
5213
|
if (messages.length === 0 || nextAfter <= nextAfterSeq || result.has_more === false)
|
|
5069
5214
|
break;
|
|
5070
5215
|
nextAfterSeq = nextAfter;
|
|
@@ -5075,6 +5220,10 @@ export class AUNClient {
|
|
|
5075
5220
|
this._clientLog.debug(`group.v2.pull done: group=${gid}, requested_after_seq=${afterSeq}, pages=${pageCount}, decrypted=${decrypted.length}, ns=${ns}`);
|
|
5076
5221
|
return decrypted;
|
|
5077
5222
|
}
|
|
5223
|
+
async _rawGroupAckMessages(params) {
|
|
5224
|
+
const p = { ...params };
|
|
5225
|
+
return await this._callRawV2Rpc('group.ack_messages', p);
|
|
5226
|
+
}
|
|
5078
5227
|
/** V2 Group ack。 */
|
|
5079
5228
|
async _ackGroupV2(groupId, upToSeq) {
|
|
5080
5229
|
const gid = normalizeGroupId(groupId) || String(groupId ?? '').trim();
|
|
@@ -6074,7 +6223,7 @@ export class AUNClient {
|
|
|
6074
6223
|
return;
|
|
6075
6224
|
}
|
|
6076
6225
|
let signature = '';
|
|
6077
|
-
const privateKeyPem =
|
|
6226
|
+
const privateKeyPem = this._currentAid?.privateKeyPem ?? '';
|
|
6078
6227
|
if (privateKeyPem) {
|
|
6079
6228
|
try {
|
|
6080
6229
|
const signPayload = stableStringify({
|
|
@@ -6512,7 +6661,7 @@ export class AUNClient {
|
|
|
6512
6661
|
delete request.access_token;
|
|
6513
6662
|
request.gateway = gateway;
|
|
6514
6663
|
request.device_id = this._deviceId;
|
|
6515
|
-
request.slot_id =
|
|
6664
|
+
request.slot_id = normalizeSlotId(request.slot_id ?? this._slotId);
|
|
6516
6665
|
let deliveryModeRaw = request.delivery_mode;
|
|
6517
6666
|
if (deliveryModeRaw == null) {
|
|
6518
6667
|
deliveryModeRaw = { ...this._defaultConnectDeliveryMode };
|
|
@@ -6575,6 +6724,8 @@ export class AUNClient {
|
|
|
6575
6724
|
if ('timeouts' in params && isJsonObject(params.timeouts)) {
|
|
6576
6725
|
Object.assign(options.timeouts, params.timeouts);
|
|
6577
6726
|
}
|
|
6727
|
+
if ('background_sync' in params)
|
|
6728
|
+
options.background_sync = Boolean(params.background_sync);
|
|
6578
6729
|
return options;
|
|
6579
6730
|
}
|
|
6580
6731
|
// ── 内部:后台任务 ────────────────────────────────────────
|