@agentunion/fastaun 0.4.3 → 0.4.5
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 +213 -185
- package/_packed_docs/CHANGELOG.md +213 -185
- 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/sdk/01-/345/277/253/351/200/237/345/274/200/345/247/213.md +1 -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 +1 -0
- 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/dist/aid-store.d.ts +1 -0
- package/dist/aid-store.js +26 -3
- 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.d.ts +17 -32
- package/dist/auth.js +42 -291
- package/dist/auth.js.map +1 -1
- package/dist/client.d.ts +10 -3
- package/dist/client.js +275 -138
- package/dist/client.js.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/dist/keystore/aid-db.d.ts +0 -4
- package/dist/keystore/aid-db.js +4 -95
- package/dist/keystore/aid-db.js.map +1 -1
- package/dist/keystore/file.d.ts +8 -5
- package/dist/keystore/file.js +109 -68
- package/dist/keystore/file.js.map +1 -1
- package/dist/keystore/index.d.ts +39 -36
- package/dist/keystore/index.js +3 -2
- package/dist/keystore/index.js.map +1 -1
- package/dist/register-flow.d.ts +49 -0
- package/dist/register-flow.js +366 -0
- package/dist/register-flow.js.map +1 -0
- package/dist/secret-store/file-store.js +6 -1
- package/dist/secret-store/file-store.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.d.ts +5 -0
- package/dist/v2/session/keystore.js +21 -3
- package/dist/v2/session/keystore.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/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 +0 -302
- 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 +0 -194
- package/_packed_docs/AUN_SDK_/351/207/215/346/236/204/345/256/236/346/226/275/350/256/241/345/210/222.md +0 -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 +0 -1697
- package/_packed_docs/python-sdk-v2-only-changelog.md +0 -189
package/dist/client.js
CHANGED
|
@@ -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',
|
|
@@ -444,7 +466,7 @@ export class AUNClient {
|
|
|
444
466
|
/** 认证流程 */
|
|
445
467
|
_auth;
|
|
446
468
|
/** 密钥存储 */
|
|
447
|
-
|
|
469
|
+
_tokenStore;
|
|
448
470
|
/** 会话参数(重连用) */
|
|
449
471
|
_sessionParams = null;
|
|
450
472
|
/** 会话选项 */
|
|
@@ -498,6 +520,7 @@ export class AUNClient {
|
|
|
498
520
|
// ── V2 E2EE 状态 ──────────────────────────────────────────────
|
|
499
521
|
_v2Session;
|
|
500
522
|
_v2KeyStore;
|
|
523
|
+
_v2SessionInitInFlight = null;
|
|
501
524
|
/** V2 bootstrap 缓存:aid/group:id → 设备列表 + 时间戳 */
|
|
502
525
|
_v2BootstrapCache = new Map();
|
|
503
526
|
_connectCapabilities = null;
|
|
@@ -527,10 +550,10 @@ export class AUNClient {
|
|
|
527
550
|
_logger;
|
|
528
551
|
_clientLog;
|
|
529
552
|
constructor(aid) {
|
|
530
|
-
if (
|
|
531
|
-
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');
|
|
532
555
|
}
|
|
533
|
-
const inputAid =
|
|
556
|
+
const inputAid = aid ?? null;
|
|
534
557
|
const rawConfig = {};
|
|
535
558
|
if (inputAid) {
|
|
536
559
|
rawConfig.aun_path = inputAid.aunPath;
|
|
@@ -565,30 +588,16 @@ export class AUNClient {
|
|
|
565
588
|
logger: this._clientLog,
|
|
566
589
|
});
|
|
567
590
|
this._discovery = new GatewayDiscovery({ verifySsl: this._configModel.verifySsl, logger: this._clientLog, net: dnsNet });
|
|
568
|
-
const
|
|
569
|
-
encryptionSeed: this._configModel.seedPassword ?? undefined,
|
|
591
|
+
const tokenStore = new FileKeyStore(this._configModel.aunPath, {
|
|
570
592
|
logger: this._logger.for('aun_core.keystore'),
|
|
571
593
|
secretStoreLogger: this._logger.for('aun_core.secret-store'),
|
|
572
594
|
});
|
|
573
|
-
this.
|
|
574
|
-
// 启动时被动清理 registerAid 留下的孤儿临时目录(>10 分钟)
|
|
575
|
-
try {
|
|
576
|
-
const cleanup = keystore.cleanupPendingDirs;
|
|
577
|
-
if (typeof cleanup === 'function') {
|
|
578
|
-
const removed = cleanup.call(keystore, 600_000);
|
|
579
|
-
if (removed > 0) {
|
|
580
|
-
this._clientLog.info(`_pending cleanup removed=${removed}`);
|
|
581
|
-
}
|
|
582
|
-
}
|
|
583
|
-
}
|
|
584
|
-
catch (err) {
|
|
585
|
-
this._clientLog.warn(`_pending cleanup failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
586
|
-
}
|
|
595
|
+
this._tokenStore = tokenStore;
|
|
587
596
|
this._slotId = inputAid?.slotId || 'default';
|
|
588
597
|
this._connectDeliveryMode = normalizeDeliveryModeConfig({ mode: 'fanout' });
|
|
589
598
|
this._defaultConnectDeliveryMode = { ...this._connectDeliveryMode };
|
|
590
599
|
this._auth = new AuthFlow({
|
|
591
|
-
|
|
600
|
+
tokenStore,
|
|
592
601
|
crypto: new CryptoProvider(),
|
|
593
602
|
aid: initAid,
|
|
594
603
|
deviceId: this._deviceId,
|
|
@@ -614,10 +623,11 @@ export class AUNClient {
|
|
|
614
623
|
this._currentAid = inputAid;
|
|
615
624
|
this._identity = {
|
|
616
625
|
aid: inputAid.aid,
|
|
617
|
-
private_key_pem: inputAid.
|
|
626
|
+
private_key_pem: inputAid.privateKeyPem,
|
|
618
627
|
public_key_der_b64: inputAid.publicKey,
|
|
619
628
|
cert: inputAid.certPem,
|
|
620
629
|
};
|
|
630
|
+
this._auth.setIdentity(this._identity);
|
|
621
631
|
this._state = 'standby';
|
|
622
632
|
}
|
|
623
633
|
}
|
|
@@ -697,6 +707,71 @@ export class AUNClient {
|
|
|
697
707
|
get lastErrorCode() {
|
|
698
708
|
return this._lastErrorCode;
|
|
699
709
|
}
|
|
710
|
+
_applyAidRuntimeContext(aid) {
|
|
711
|
+
const rawConfig = {
|
|
712
|
+
aun_path: aid.aunPath,
|
|
713
|
+
verify_ssl: aid.verifySsl,
|
|
714
|
+
debug: aid.debug,
|
|
715
|
+
};
|
|
716
|
+
if (aid.rootCaPath)
|
|
717
|
+
rawConfig.root_ca_path = aid.rootCaPath;
|
|
718
|
+
const nextConfig = configFromMap(rawConfig);
|
|
719
|
+
try {
|
|
720
|
+
const close = this._tokenStore.close;
|
|
721
|
+
if (typeof close === 'function')
|
|
722
|
+
close.call(this._tokenStore);
|
|
723
|
+
}
|
|
724
|
+
catch {
|
|
725
|
+
// best-effort cleanup before switching tokenStore roots
|
|
726
|
+
}
|
|
727
|
+
this._configModel = nextConfig;
|
|
728
|
+
this.config.aun_path = nextConfig.aunPath;
|
|
729
|
+
this.config.root_ca_path = nextConfig.rootCaPath;
|
|
730
|
+
this.config.seed_password = nextConfig.seedPassword;
|
|
731
|
+
this._agentMdPath = path.join(nextConfig.aunPath, 'AIDs');
|
|
732
|
+
this._agentMdCache.clear();
|
|
733
|
+
this._agentMdFetchInflight.clear();
|
|
734
|
+
this._agentMdDownloadInflight.clear();
|
|
735
|
+
this._peerCache.clear();
|
|
736
|
+
this._certCache.clear();
|
|
737
|
+
this._gatewayUrl = null;
|
|
738
|
+
this._deviceId = aid.deviceId || getDeviceId(nextConfig.aunPath);
|
|
739
|
+
this._slotId = aid.slotId || 'default';
|
|
740
|
+
const debugFlag = nextConfig.debug;
|
|
741
|
+
this._logger = new AUNLogger({ debug: debugFlag, aunPath: nextConfig.aunPath });
|
|
742
|
+
this._logger.bindDeviceId(this._deviceId);
|
|
743
|
+
this._clientLog = this._logger.for('aun_core.client');
|
|
744
|
+
const dnsNet = new DnsResilientNet({
|
|
745
|
+
verifySsl: nextConfig.verifySsl,
|
|
746
|
+
logger: this._clientLog,
|
|
747
|
+
});
|
|
748
|
+
this._discovery = new GatewayDiscovery({ verifySsl: nextConfig.verifySsl, logger: this._clientLog, net: dnsNet });
|
|
749
|
+
const tokenStore = new FileKeyStore(nextConfig.aunPath, {
|
|
750
|
+
logger: this._logger.for('aun_core.keystore'),
|
|
751
|
+
secretStoreLogger: this._logger.for('aun_core.secret-store'),
|
|
752
|
+
});
|
|
753
|
+
this._tokenStore = tokenStore;
|
|
754
|
+
this._auth = new AuthFlow({
|
|
755
|
+
tokenStore,
|
|
756
|
+
crypto: new CryptoProvider(),
|
|
757
|
+
aid: aid.aid,
|
|
758
|
+
deviceId: this._deviceId,
|
|
759
|
+
slotId: this._slotId,
|
|
760
|
+
rootCaPath: nextConfig.rootCaPath ?? undefined,
|
|
761
|
+
verifySsl: nextConfig.verifySsl,
|
|
762
|
+
logger: this._logger.for('aun_core.auth'),
|
|
763
|
+
net: dnsNet,
|
|
764
|
+
});
|
|
765
|
+
this._transport = new RPCTransport({
|
|
766
|
+
eventDispatcher: this._dispatcher,
|
|
767
|
+
timeout: 10_000,
|
|
768
|
+
onDisconnect: (err, closeCode) => this._handleTransportDisconnect(err, closeCode),
|
|
769
|
+
verifySsl: nextConfig.verifySsl,
|
|
770
|
+
logger: this._logger.for('aun_core.transport'),
|
|
771
|
+
dnsNet,
|
|
772
|
+
});
|
|
773
|
+
this._transport.setMetaObserver((meta) => this._observeRpcMeta(meta));
|
|
774
|
+
}
|
|
700
775
|
loadIdentity(aid) {
|
|
701
776
|
if (!aid?.isPrivateKeyValid()) {
|
|
702
777
|
throw new StateError('loadIdentity requires an AID with a valid private key');
|
|
@@ -705,16 +780,17 @@ export class AUNClient {
|
|
|
705
780
|
if (publicState !== ConnectionState.NO_IDENTITY && publicState !== ConnectionState.CLOSED) {
|
|
706
781
|
throw new StateError(`loadIdentity not allowed in state ${publicState}`);
|
|
707
782
|
}
|
|
783
|
+
this._applyAidRuntimeContext(aid);
|
|
708
784
|
this._currentAid = aid;
|
|
709
785
|
this._aid = aid.aid;
|
|
710
786
|
this._identity = {
|
|
711
787
|
aid: aid.aid,
|
|
712
|
-
private_key_pem: aid.
|
|
788
|
+
private_key_pem: aid.privateKeyPem,
|
|
713
789
|
public_key_der_b64: aid.publicKey,
|
|
714
790
|
cert: aid.certPem,
|
|
715
791
|
};
|
|
716
|
-
|
|
717
|
-
this.
|
|
792
|
+
// 注入内存私钥到 AuthFlow,禁止 AuthFlow 内部再走 keystore 解密
|
|
793
|
+
this._auth.setIdentity(this._identity);
|
|
718
794
|
this._state = 'standby';
|
|
719
795
|
this._closing = false;
|
|
720
796
|
this._lastError = null;
|
|
@@ -788,10 +864,7 @@ export class AUNClient {
|
|
|
788
864
|
return `${agentMdHttpScheme(gatewayUrl)}://${agentMdAuthority(target, this._configModel.discoveryPort)}/agent.md`;
|
|
789
865
|
}
|
|
790
866
|
async _ensureAgentMdUploadToken(aid, gatewayUrl) {
|
|
791
|
-
let identity = this.
|
|
792
|
-
if (!identity && this._identity && String(this._identity.aid ?? '') === aid) {
|
|
793
|
-
identity = this._identity;
|
|
794
|
-
}
|
|
867
|
+
let identity = this._identity && String(this._identity.aid ?? '') === aid ? this._identity : null;
|
|
795
868
|
if (!identity) {
|
|
796
869
|
throw new StateError('no local identity found, register or load an AID first');
|
|
797
870
|
}
|
|
@@ -975,7 +1048,7 @@ export class AUNClient {
|
|
|
975
1048
|
let resolvedCert = String(certPem ?? '').trim();
|
|
976
1049
|
if (!resolvedCert) {
|
|
977
1050
|
try {
|
|
978
|
-
resolvedCert = String(this.
|
|
1051
|
+
resolvedCert = String(this._tokenStore.loadCert(target) ?? '').trim();
|
|
979
1052
|
}
|
|
980
1053
|
catch {
|
|
981
1054
|
resolvedCert = '';
|
|
@@ -1589,10 +1662,11 @@ export class AUNClient {
|
|
|
1589
1662
|
async connect(opts) {
|
|
1590
1663
|
const tStart = Date.now();
|
|
1591
1664
|
// 先校验非法参数(ValidationError),再检查身份(StateError)
|
|
1592
|
-
if (opts !== undefined && typeof opts === 'object') {
|
|
1665
|
+
if (opts !== undefined && opts !== null && typeof opts === 'object') {
|
|
1593
1666
|
const raw = opts;
|
|
1594
|
-
|
|
1595
|
-
|
|
1667
|
+
const invalid = Object.keys(raw).filter((key) => !PUBLIC_CONNECTION_OPTION_KEYS.has(key)).sort();
|
|
1668
|
+
if (invalid.length > 0) {
|
|
1669
|
+
throw new ValidationError(`connect options contain unsupported field(s): ${invalid.join(', ')}`);
|
|
1596
1670
|
}
|
|
1597
1671
|
}
|
|
1598
1672
|
const target = this._currentAid?.aid ?? this._aid ?? '';
|
|
@@ -1694,8 +1768,8 @@ export class AUNClient {
|
|
|
1694
1768
|
this._stopBackgroundTasks();
|
|
1695
1769
|
this._stopReconnect();
|
|
1696
1770
|
if (this.state === ConnectionState.NO_IDENTITY || this.state === ConnectionState.CLOSED) {
|
|
1697
|
-
const
|
|
1698
|
-
|
|
1771
|
+
const closableStore = this._tokenStore;
|
|
1772
|
+
closableStore.close?.();
|
|
1699
1773
|
this._state = 'closed';
|
|
1700
1774
|
this._logger.close();
|
|
1701
1775
|
this._resetSeqTrackingState();
|
|
@@ -1703,8 +1777,8 @@ export class AUNClient {
|
|
|
1703
1777
|
return;
|
|
1704
1778
|
}
|
|
1705
1779
|
await this._transport.close();
|
|
1706
|
-
const
|
|
1707
|
-
|
|
1780
|
+
const closableStore = this._tokenStore;
|
|
1781
|
+
closableStore.close?.();
|
|
1708
1782
|
this._state = 'closed';
|
|
1709
1783
|
this._logger.close();
|
|
1710
1784
|
await this._dispatcher.publish('state_change', { state: this._publicState(this._state) });
|
|
@@ -1788,6 +1862,14 @@ export class AUNClient {
|
|
|
1788
1862
|
}
|
|
1789
1863
|
this._validateOutboundCall(method, p);
|
|
1790
1864
|
this._injectMessageCursorContext(method, p);
|
|
1865
|
+
if (method.startsWith('group.')
|
|
1866
|
+
&& !('_group_cursor_params' in p)
|
|
1867
|
+
&& !Boolean(p._pull_gate_locked)) {
|
|
1868
|
+
const explicitCursorParams = this._groupCursorParams(p);
|
|
1869
|
+
if (Object.keys(explicitCursorParams).length > 0) {
|
|
1870
|
+
p._group_cursor_params = explicitCursorParams;
|
|
1871
|
+
}
|
|
1872
|
+
}
|
|
1791
1873
|
// group.* 方法的 group_id 归一化为 canonical 格式(兼容老/污染数据)
|
|
1792
1874
|
if (method.startsWith('group.') && p.group_id !== undefined && p.group_id !== null) {
|
|
1793
1875
|
const rawGroupId = String(p.group_id);
|
|
@@ -1889,7 +1971,17 @@ export class AUNClient {
|
|
|
1889
1971
|
throw new ValidationError('group.pull requires group_id');
|
|
1890
1972
|
}
|
|
1891
1973
|
await this._ensureV2SessionReady('group.pull');
|
|
1892
|
-
const
|
|
1974
|
+
const hasExplicitAfterSeq = 'after_seq' in p || 'after_message_seq' in p;
|
|
1975
|
+
const cursorParams = this._explicitGroupCursorParams(p);
|
|
1976
|
+
const ownsCursor = Object.keys(cursorParams).length === 0 || this._groupCursorTargetsCurrentInstance(cursorParams);
|
|
1977
|
+
const pullOpts = { gateLocked: true };
|
|
1978
|
+
if (hasExplicitAfterSeq)
|
|
1979
|
+
pullOpts.explicitAfterSeq = true;
|
|
1980
|
+
if (Object.keys(cursorParams).length > 0)
|
|
1981
|
+
pullOpts.cursorParams = cursorParams;
|
|
1982
|
+
if (!ownsCursor)
|
|
1983
|
+
pullOpts.ownsCursor = false;
|
|
1984
|
+
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));
|
|
1893
1985
|
return { messages };
|
|
1894
1986
|
}
|
|
1895
1987
|
if (method === 'group.ack_messages' || method === 'group.v2.ack') {
|
|
@@ -1897,12 +1989,18 @@ export class AUNClient {
|
|
|
1897
1989
|
throw new ValidationError('group.ack_messages requires group_id');
|
|
1898
1990
|
}
|
|
1899
1991
|
await this._ensureV2SessionReady('group.ack_messages');
|
|
1992
|
+
const cursorParams = this._explicitGroupCursorParams(p);
|
|
1993
|
+
const ownsCursor = Object.keys(cursorParams).length === 0 || this._groupCursorTargetsCurrentInstance(cursorParams);
|
|
1994
|
+
if (method === 'group.ack_messages' && !ownsCursor) {
|
|
1995
|
+
return await runWithRpcPriority(() => this._rawGroupAckMessages(p));
|
|
1996
|
+
}
|
|
1900
1997
|
return await runWithRpcPriority(() => this._ackGroupV2(String(p.group_id), Number(p.seq ?? p.msg_seq ?? p.up_to_seq ?? 0) || undefined));
|
|
1901
1998
|
}
|
|
1902
1999
|
if (method === 'message.pull') {
|
|
1903
2000
|
delete p._skip_auto_ack;
|
|
1904
2001
|
delete p.skip_auto_ack;
|
|
1905
2002
|
}
|
|
2003
|
+
delete p._group_cursor_params;
|
|
1906
2004
|
// 关键操作自动附加客户端签名
|
|
1907
2005
|
if (SIGNED_METHODS.has(method)) {
|
|
1908
2006
|
if (this._shouldSkipClientSignature(method, p)) {
|
|
@@ -1973,6 +2071,7 @@ export class AUNClient {
|
|
|
1973
2071
|
delete p._pull_gate_locked;
|
|
1974
2072
|
delete p._skip_auto_ack;
|
|
1975
2073
|
delete p.skip_auto_ack;
|
|
2074
|
+
delete p._group_cursor_params;
|
|
1976
2075
|
if (method.startsWith('group.') && p.group_id !== undefined && p.group_id !== null) {
|
|
1977
2076
|
p.group_id = normalizeGroupId(String(p.group_id)) || String(p.group_id);
|
|
1978
2077
|
}
|
|
@@ -2021,13 +2120,12 @@ export class AUNClient {
|
|
|
2021
2120
|
* 签名覆盖所有非 _ 前缀且非 client_signature 的业务字段。
|
|
2022
2121
|
*/
|
|
2023
2122
|
_signClientOperation(method, params) {
|
|
2024
|
-
const
|
|
2025
|
-
if (!
|
|
2123
|
+
const currentAid = this._currentAid;
|
|
2124
|
+
if (!currentAid?.privateKeyPem)
|
|
2026
2125
|
return;
|
|
2027
2126
|
try {
|
|
2028
|
-
const aid =
|
|
2127
|
+
const aid = currentAid.aid;
|
|
2029
2128
|
const ts = String(Math.floor(Date.now() / 1000));
|
|
2030
|
-
// 计算 params hash — 必须递归排序所有键(与 Python json.dumps(sort_keys=True, separators=(",",":")) 一致)
|
|
2031
2129
|
const paramsForHash = {};
|
|
2032
2130
|
for (const [k, v] of Object.entries(params)) {
|
|
2033
2131
|
if (k !== 'client_signature' && !k.startsWith('_')) {
|
|
@@ -2037,11 +2135,11 @@ export class AUNClient {
|
|
|
2037
2135
|
const paramsJson = stableStringify(paramsForHash);
|
|
2038
2136
|
const paramsHash = crypto.createHash('sha256').update(paramsJson, 'utf-8').digest('hex');
|
|
2039
2137
|
const signData = Buffer.from(`${method}|${aid}|${ts}|${paramsHash}`, 'utf-8');
|
|
2040
|
-
const privateKey = crypto.createPrivateKey(
|
|
2138
|
+
const privateKey = crypto.createPrivateKey(currentAid.privateKeyPem);
|
|
2041
2139
|
const signature = crypto.sign('SHA256', signData, privateKey);
|
|
2042
2140
|
// 证书指纹
|
|
2043
2141
|
let certFingerprint = '';
|
|
2044
|
-
const certPem =
|
|
2142
|
+
const certPem = currentAid.certPem;
|
|
2045
2143
|
if (certPem) {
|
|
2046
2144
|
const certObj = new crypto.X509Certificate(certPem);
|
|
2047
2145
|
certFingerprint = 'sha256:' + certObj.fingerprint256.replace(/:/g, '').toLowerCase();
|
|
@@ -2747,6 +2845,27 @@ export class AUNClient {
|
|
|
2747
2845
|
}
|
|
2748
2846
|
return Math.max(0, ...values.filter((value) => Number.isFinite(value)));
|
|
2749
2847
|
}
|
|
2848
|
+
_groupCursorParams(params) {
|
|
2849
|
+
const cursorParams = {};
|
|
2850
|
+
for (const key of ['device_id', 'slot_id', 'device_name', 'device_type']) {
|
|
2851
|
+
const value = params[key];
|
|
2852
|
+
if (value !== undefined && value !== null)
|
|
2853
|
+
cursorParams[key] = value;
|
|
2854
|
+
}
|
|
2855
|
+
return cursorParams;
|
|
2856
|
+
}
|
|
2857
|
+
_explicitGroupCursorParams(params) {
|
|
2858
|
+
const value = params._group_cursor_params;
|
|
2859
|
+
if (!isJsonObject(value))
|
|
2860
|
+
return {};
|
|
2861
|
+
return { ...value };
|
|
2862
|
+
}
|
|
2863
|
+
_groupCursorTargetsCurrentInstance(params) {
|
|
2864
|
+
const deviceId = String(params.device_id ?? '').trim();
|
|
2865
|
+
const slotId = String(params.slot_id ?? '').trim();
|
|
2866
|
+
return (!deviceId || deviceId === (this._deviceId ?? ''))
|
|
2867
|
+
&& (!slotId || slotId === (this._slotId ?? ''));
|
|
2868
|
+
}
|
|
2750
2869
|
_schedulePullFollowup(method, params, result) {
|
|
2751
2870
|
if (method === 'message.pull')
|
|
2752
2871
|
method = 'message.v2.pull';
|
|
@@ -3237,8 +3356,8 @@ export class AUNClient {
|
|
|
3237
3356
|
const membershipSnapshot = String(d.membership_snapshot ?? '').trim();
|
|
3238
3357
|
const policySnapshot = String(d.policy_snapshot ?? '').trim();
|
|
3239
3358
|
// 1. 验证 prev_state_hash 连续性
|
|
3240
|
-
const loadFn = this.
|
|
3241
|
-
const localState = loadFn ? loadFn.call(this.
|
|
3359
|
+
const loadFn = this._tokenStore.loadGroupState;
|
|
3360
|
+
const localState = loadFn ? loadFn.call(this._tokenStore, groupId) : null;
|
|
3242
3361
|
if (localState && localState.state_hash && localState.state_hash !== prevStateHash) {
|
|
3243
3362
|
this._clientLog.warn(`state_hash chain discontinuous group=${groupId} local_sv=${localState.state_version} event_sv=${stateVersion}`);
|
|
3244
3363
|
// 回源同步
|
|
@@ -3264,9 +3383,9 @@ export class AUNClient {
|
|
|
3264
3383
|
return;
|
|
3265
3384
|
}
|
|
3266
3385
|
}
|
|
3267
|
-
const saveFn = this.
|
|
3386
|
+
const saveFn = this._tokenStore.saveGroupState;
|
|
3268
3387
|
if (saveFn) {
|
|
3269
|
-
saveFn.call(this.
|
|
3388
|
+
saveFn.call(this._tokenStore, groupId, sv, sHash, sEpoch, sMembersJson || membershipSnapshot, sPolicyJson || policySnapshot);
|
|
3270
3389
|
}
|
|
3271
3390
|
}
|
|
3272
3391
|
}
|
|
@@ -3287,9 +3406,9 @@ export class AUNClient {
|
|
|
3287
3406
|
return;
|
|
3288
3407
|
}
|
|
3289
3408
|
// 3. 更新本地存储
|
|
3290
|
-
const saveFn = this.
|
|
3409
|
+
const saveFn = this._tokenStore.saveGroupState;
|
|
3291
3410
|
if (saveFn) {
|
|
3292
|
-
saveFn.call(this.
|
|
3411
|
+
saveFn.call(this._tokenStore, groupId, stateVersion, stateHash, keyEpoch, membershipSnapshot, policySnapshot);
|
|
3293
3412
|
}
|
|
3294
3413
|
this._clientLog.debug(`_onGroupStateCommitted exit: elapsed=${Date.now() - tStart}ms group=${groupId}`);
|
|
3295
3414
|
}
|
|
@@ -3449,7 +3568,7 @@ export class AUNClient {
|
|
|
3449
3568
|
}
|
|
3450
3569
|
try {
|
|
3451
3570
|
// peer 证书只存版本目录,不覆盖 cert.pem
|
|
3452
|
-
this.
|
|
3571
|
+
this._tokenStore.saveCert(aid, certPem, certFingerprint, { makeActive: false });
|
|
3453
3572
|
}
|
|
3454
3573
|
catch (exc) {
|
|
3455
3574
|
this._clientLog.error(`failed to write cert to keystore (aid=${aid}, fp=${certFingerprint ?? ''}): ${formatCaughtError(exc)}`, exc instanceof Error ? exc : undefined);
|
|
@@ -3720,9 +3839,9 @@ export class AUNClient {
|
|
|
3720
3839
|
return;
|
|
3721
3840
|
try {
|
|
3722
3841
|
// 优先从 seq_tracker 表按行读取
|
|
3723
|
-
const loadAll = this.
|
|
3842
|
+
const loadAll = this._tokenStore.loadAllSeqs;
|
|
3724
3843
|
if (typeof loadAll === 'function') {
|
|
3725
|
-
let state = loadAll.call(this.
|
|
3844
|
+
let state = loadAll.call(this._tokenStore, this._aid, this._deviceId, this._slotId);
|
|
3726
3845
|
if (state && Object.keys(state).length > 0) {
|
|
3727
3846
|
state = this._migrateSeqStateGroupIds(state);
|
|
3728
3847
|
this._seqTracker.restoreState(state);
|
|
@@ -3730,9 +3849,9 @@ export class AUNClient {
|
|
|
3730
3849
|
}
|
|
3731
3850
|
}
|
|
3732
3851
|
// fallback: 从旧 instance_state JSON blob 恢复
|
|
3733
|
-
const loader = this.
|
|
3852
|
+
const loader = this._tokenStore.loadInstanceState;
|
|
3734
3853
|
if (typeof loader === 'function') {
|
|
3735
|
-
const instanceState = loader.call(this.
|
|
3854
|
+
const instanceState = loader.call(this._tokenStore, this._aid, this._deviceId, this._slotId);
|
|
3736
3855
|
if (instanceState && typeof instanceState.seq_tracker_state === 'object') {
|
|
3737
3856
|
let state = instanceState.seq_tracker_state;
|
|
3738
3857
|
state = this._migrateSeqStateGroupIds(state);
|
|
@@ -3783,20 +3902,20 @@ export class AUNClient {
|
|
|
3783
3902
|
}
|
|
3784
3903
|
this._clientLog.info(`SeqTracker group_id migration: ${Object.keys(renameMap).length} namespaces rewritten`);
|
|
3785
3904
|
// 落盘
|
|
3786
|
-
const saver = this.
|
|
3787
|
-
const deleter = this.
|
|
3905
|
+
const saver = this._tokenStore.saveSeq;
|
|
3906
|
+
const deleter = this._tokenStore.deleteSeq;
|
|
3788
3907
|
if (typeof saver === 'function' && this._aid) {
|
|
3789
3908
|
for (const [oldNs, newNs] of Object.entries(renameMap)) {
|
|
3790
3909
|
if (typeof deleter === 'function') {
|
|
3791
3910
|
try {
|
|
3792
|
-
deleter.call(this.
|
|
3911
|
+
deleter.call(this._tokenStore, this._aid, this._deviceId, this._slotId, oldNs);
|
|
3793
3912
|
}
|
|
3794
3913
|
catch (e) {
|
|
3795
3914
|
this._clientLog.debug(`delete old seq ns failed: ns=${oldNs} err=${formatCaughtError(e)}`);
|
|
3796
3915
|
}
|
|
3797
3916
|
}
|
|
3798
3917
|
try {
|
|
3799
|
-
saver.call(this.
|
|
3918
|
+
saver.call(this._tokenStore, this._aid, this._deviceId, this._slotId, newNs, newState[newNs]);
|
|
3800
3919
|
}
|
|
3801
3920
|
catch (e) {
|
|
3802
3921
|
this._clientLog.debug(`write new seq ns failed: ns=${newNs} err=${formatCaughtError(e)}`);
|
|
@@ -3844,17 +3963,17 @@ export class AUNClient {
|
|
|
3844
3963
|
return;
|
|
3845
3964
|
try {
|
|
3846
3965
|
// 优先按行写入 seq_tracker 表
|
|
3847
|
-
const saveFn = this.
|
|
3966
|
+
const saveFn = this._tokenStore.saveSeq;
|
|
3848
3967
|
if (typeof saveFn === 'function') {
|
|
3849
3968
|
for (const [ns, seq] of Object.entries(state)) {
|
|
3850
|
-
saveFn.call(this.
|
|
3969
|
+
saveFn.call(this._tokenStore, this._aid, this._deviceId, this._slotId, ns, seq);
|
|
3851
3970
|
}
|
|
3852
3971
|
return;
|
|
3853
3972
|
}
|
|
3854
3973
|
// fallback: 旧版 updateInstanceState JSON blob
|
|
3855
|
-
const updater = this.
|
|
3974
|
+
const updater = this._tokenStore.updateInstanceState;
|
|
3856
3975
|
if (typeof updater === 'function') {
|
|
3857
|
-
updater.call(this.
|
|
3976
|
+
updater.call(this._tokenStore, this._aid, this._deviceId, this._slotId, (metadata) => {
|
|
3858
3977
|
metadata.seq_tracker_state = state;
|
|
3859
3978
|
return metadata;
|
|
3860
3979
|
});
|
|
@@ -3877,13 +3996,13 @@ export class AUNClient {
|
|
|
3877
3996
|
return;
|
|
3878
3997
|
const seq = this._seqTracker.getContiguousSeq(ns);
|
|
3879
3998
|
try {
|
|
3880
|
-
if (seq > 0 && typeof this.
|
|
3881
|
-
this.
|
|
3999
|
+
if (seq > 0 && typeof this._tokenStore.saveSeq === 'function') {
|
|
4000
|
+
this._tokenStore.saveSeq(this._aid, this._deviceId, this._slotId, ns, seq);
|
|
3882
4001
|
return;
|
|
3883
4002
|
}
|
|
3884
|
-
const deleteSeq = this.
|
|
4003
|
+
const deleteSeq = this._tokenStore.deleteSeq;
|
|
3885
4004
|
if (seq <= 0 && typeof deleteSeq === 'function') {
|
|
3886
|
-
deleteSeq.call(this.
|
|
4005
|
+
deleteSeq.call(this._tokenStore, this._aid, this._deviceId, this._slotId, ns);
|
|
3887
4006
|
return;
|
|
3888
4007
|
}
|
|
3889
4008
|
if (seq > 0) {
|
|
@@ -4026,16 +4145,26 @@ export class AUNClient {
|
|
|
4026
4145
|
this._restoreSeqTrackerState();
|
|
4027
4146
|
}
|
|
4028
4147
|
this._startBackgroundTasks();
|
|
4029
|
-
|
|
4030
|
-
|
|
4031
|
-
|
|
4148
|
+
const connectionKind = String(params.connection_kind ?? 'long');
|
|
4149
|
+
const isShortConnection = connectionKind === 'short';
|
|
4150
|
+
if (!isShortConnection) {
|
|
4151
|
+
// V2 E2EE:长连接上线时初始化 session 并注册本设备 SPK。
|
|
4152
|
+
try {
|
|
4153
|
+
await this._initV2Session();
|
|
4154
|
+
}
|
|
4155
|
+
catch (exc) {
|
|
4156
|
+
this._clientLog.warn(`V2 session init failed (non-fatal): ${formatCaughtError(exc)}`);
|
|
4157
|
+
}
|
|
4032
4158
|
}
|
|
4033
|
-
|
|
4034
|
-
this._clientLog.
|
|
4159
|
+
else {
|
|
4160
|
+
this._clientLog.debug('V2 session init deferred for short connection');
|
|
4035
4161
|
}
|
|
4036
4162
|
// connect/reconnect 成功后自动触发一次 P2P message.v2.pull,补齐离线期间积压
|
|
4037
4163
|
// 群消息按惰性触发,不在此处主动 pull
|
|
4038
|
-
|
|
4164
|
+
const hasExplicitBackgroundSync = Object.prototype.hasOwnProperty.call(params, 'background_sync');
|
|
4165
|
+
const backgroundSyncEnabled = this._sessionOptions.background_sync !== false
|
|
4166
|
+
&& (!isShortConnection || hasExplicitBackgroundSync);
|
|
4167
|
+
if (backgroundSyncEnabled) {
|
|
4039
4168
|
void this._fillP2pGap().catch((exc) => {
|
|
4040
4169
|
this._clientLog.warn(`schedule post-connect P2P gap fill failed: ${formatCaughtError(exc)}`);
|
|
4041
4170
|
});
|
|
@@ -4074,6 +4203,15 @@ export class AUNClient {
|
|
|
4074
4203
|
}
|
|
4075
4204
|
/** V2-only:所有加密入口都必须有 V2 session。 */
|
|
4076
4205
|
async _ensureV2SessionReady(method, errorMessage) {
|
|
4206
|
+
if (!this._v2Session) {
|
|
4207
|
+
if (!this._v2SessionInitInFlight) {
|
|
4208
|
+
this._v2SessionInitInFlight = this._initV2Session()
|
|
4209
|
+
.finally(() => {
|
|
4210
|
+
this._v2SessionInitInFlight = null;
|
|
4211
|
+
});
|
|
4212
|
+
}
|
|
4213
|
+
await this._v2SessionInitInFlight;
|
|
4214
|
+
}
|
|
4077
4215
|
if (!this._v2Session) {
|
|
4078
4216
|
throw new StateError(errorMessage ?? `V2 session not initialized; encrypted ${method} requires E2EE V2`);
|
|
4079
4217
|
}
|
|
@@ -4096,42 +4234,13 @@ export class AUNClient {
|
|
|
4096
4234
|
this._v2BootstrapCache.clear();
|
|
4097
4235
|
}
|
|
4098
4236
|
let identity = this._identity;
|
|
4099
|
-
|
|
4100
|
-
|
|
4101
|
-
|
|
4102
|
-
if (identity)
|
|
4103
|
-
this._identity = identity;
|
|
4104
|
-
}
|
|
4105
|
-
catch {
|
|
4106
|
-
identity = null;
|
|
4107
|
-
}
|
|
4108
|
-
}
|
|
4109
|
-
if (!identity?.private_key_pem) {
|
|
4110
|
-
// fallback:缓存的 identity 可能被 instanceState 污染,重新从 keystore 加载
|
|
4111
|
-
try {
|
|
4112
|
-
identity = this._keystore.loadIdentity(this._aid);
|
|
4113
|
-
if (identity?.private_key_pem) {
|
|
4114
|
-
this._identity = identity;
|
|
4115
|
-
this._clientLog.warn('V2 session init: identity cache was stale, reloaded from keystore');
|
|
4116
|
-
// 重新持久化 instance_state,清理脏数据
|
|
4117
|
-
const persistIdentity = this._auth._persistIdentity;
|
|
4118
|
-
if (typeof persistIdentity === 'function') {
|
|
4119
|
-
try {
|
|
4120
|
-
persistIdentity.call(this._auth, identity);
|
|
4121
|
-
}
|
|
4122
|
-
catch { /* best-effort */ }
|
|
4123
|
-
}
|
|
4124
|
-
}
|
|
4125
|
-
}
|
|
4126
|
-
catch {
|
|
4127
|
-
identity = null;
|
|
4128
|
-
}
|
|
4129
|
-
}
|
|
4130
|
-
if (!identity?.private_key_pem) {
|
|
4237
|
+
// 私钥由 AIDStore 管理,直接从 _currentAid 读取明文私钥
|
|
4238
|
+
const currentAid = this._currentAid;
|
|
4239
|
+
if (!currentAid?.privateKeyPem) {
|
|
4131
4240
|
this._clientLog.warn('V2 session init skipped: no AID private key');
|
|
4132
4241
|
return;
|
|
4133
4242
|
}
|
|
4134
|
-
const privateKey = crypto.createPrivateKey(
|
|
4243
|
+
const privateKey = crypto.createPrivateKey(currentAid.privateKeyPem);
|
|
4135
4244
|
const jwk = privateKey.export({ format: 'jwk' });
|
|
4136
4245
|
if (jwk.kty !== 'EC' || jwk.crv !== 'P-256' || !jwk.d) {
|
|
4137
4246
|
throw new StateError('AID private key must be EC P-256');
|
|
@@ -4139,8 +4248,8 @@ export class AUNClient {
|
|
|
4139
4248
|
const aidPriv = _v2LeftPad32(_v2B64uToBytes(jwk.d));
|
|
4140
4249
|
const pubDer = crypto.createPublicKey(privateKey).export({ format: 'der', type: 'spki' });
|
|
4141
4250
|
const aidPubDer = new Uint8Array(pubDer);
|
|
4142
|
-
const storeProvider = this.
|
|
4143
|
-
const v2Store = storeProvider.getV2KeyStore?.call(this.
|
|
4251
|
+
const storeProvider = this._tokenStore;
|
|
4252
|
+
const v2Store = storeProvider.getV2KeyStore?.call(this._tokenStore, this._aid);
|
|
4144
4253
|
if (!v2Store) {
|
|
4145
4254
|
throw new StateError('V2 key store is unavailable for current keystore');
|
|
4146
4255
|
}
|
|
@@ -4150,6 +4259,28 @@ export class AUNClient {
|
|
|
4150
4259
|
this._clientLog.debug(`V2 session initialized aid=${this._aid} device=${this._deviceId}`);
|
|
4151
4260
|
// 群 state proposal 由服务端在 client.online 时定向通知。
|
|
4152
4261
|
}
|
|
4262
|
+
_currentV2KeyStore() {
|
|
4263
|
+
if (this._v2KeyStore)
|
|
4264
|
+
return this._v2KeyStore;
|
|
4265
|
+
if (!this._aid)
|
|
4266
|
+
throw new StateError('V2 key store requires a loaded AID');
|
|
4267
|
+
const storeProvider = this._tokenStore;
|
|
4268
|
+
const v2Store = storeProvider.getV2KeyStore?.call(this._tokenStore, this._aid);
|
|
4269
|
+
if (!v2Store) {
|
|
4270
|
+
throw new StateError('V2 key store is unavailable for current identity');
|
|
4271
|
+
}
|
|
4272
|
+
this._v2KeyStore = v2Store;
|
|
4273
|
+
return v2Store;
|
|
4274
|
+
}
|
|
4275
|
+
_saveGroupIdentityToV2(groupAid, identity) {
|
|
4276
|
+
const privateKeyPem = String(identity.private_key_pem ?? '').trim();
|
|
4277
|
+
const publicKeyDerB64 = String(identity.public_key_der_b64 ?? '').trim();
|
|
4278
|
+
if (!groupAid || !privateKeyPem || !publicKeyDerB64) {
|
|
4279
|
+
throw new StateError('group identity is incomplete');
|
|
4280
|
+
}
|
|
4281
|
+
const pubDer = new Uint8Array(Buffer.from(publicKeyDerB64, 'base64'));
|
|
4282
|
+
this._currentV2KeyStore().saveGroupIdentity(this._deviceId, groupAid, privateKeyPem, pubDer);
|
|
4283
|
+
}
|
|
4153
4284
|
async _v2TrustedIKPubDer(aid) {
|
|
4154
4285
|
const normalizedAid = String(aid ?? '').trim();
|
|
4155
4286
|
if (!normalizedAid)
|
|
@@ -4677,6 +4808,7 @@ export class AUNClient {
|
|
|
4677
4808
|
decrypted.push(plaintext);
|
|
4678
4809
|
this._logMessageDebug('decrypt-ok', 'message.v2.pull', 'message.received', plaintext);
|
|
4679
4810
|
}
|
|
4811
|
+
const hasServerAckSeq = Object.prototype.hasOwnProperty.call(result, 'server_ack_seq');
|
|
4680
4812
|
const serverAckSeq = Number(result.server_ack_seq ?? 0);
|
|
4681
4813
|
if (ns && Number.isFinite(serverAckSeq) && serverAckSeq > 0) {
|
|
4682
4814
|
const contig = this._seqTracker.getContiguousSeq(ns);
|
|
@@ -4692,7 +4824,11 @@ export class AUNClient {
|
|
|
4692
4824
|
await this._drainOrderedMessages(ns, undefined, true);
|
|
4693
4825
|
this._saveSeqTrackerState();
|
|
4694
4826
|
}
|
|
4695
|
-
|
|
4827
|
+
const ackNeeded = messages.length > 0
|
|
4828
|
+
&& ackSeq > 0
|
|
4829
|
+
&& !opts?.skipAutoAck
|
|
4830
|
+
&& (contigAdvanced || (hasServerAckSeq && ackSeq > serverAckSeq));
|
|
4831
|
+
if (ackNeeded) {
|
|
4696
4832
|
this._clientLog.debug(`message.v2.pull scheduling auto-ack: ns=${ns}, ack_seq=${ackSeq}, raw_count=${messages.length}`);
|
|
4697
4833
|
this._safeAsync(this._ackV2(ackSeq).then(() => undefined));
|
|
4698
4834
|
}
|
|
@@ -4955,7 +5091,9 @@ export class AUNClient {
|
|
|
4955
5091
|
}
|
|
4956
5092
|
const decrypted = [];
|
|
4957
5093
|
let totalRawCount = 0;
|
|
4958
|
-
|
|
5094
|
+
const cursorParams = opts?.cursorParams ?? {};
|
|
5095
|
+
const ownsCursor = opts?.ownsCursor !== false;
|
|
5096
|
+
let nextAfterSeq = opts?.explicitAfterSeq ? afterSeq : (afterSeq || this._seqTracker.getContiguousSeq(ns));
|
|
4959
5097
|
let pageCount = 0;
|
|
4960
5098
|
const maxPages = 100;
|
|
4961
5099
|
while (pageCount < maxPages) {
|
|
@@ -4965,6 +5103,7 @@ export class AUNClient {
|
|
|
4965
5103
|
group_id: gid,
|
|
4966
5104
|
after_seq: nextAfterSeq,
|
|
4967
5105
|
limit,
|
|
5106
|
+
...cursorParams,
|
|
4968
5107
|
});
|
|
4969
5108
|
const messages = (Array.isArray(result.messages) ? result.messages : []);
|
|
4970
5109
|
totalRawCount += messages.length;
|
|
@@ -5043,7 +5182,9 @@ export class AUNClient {
|
|
|
5043
5182
|
decrypted.push(plaintext);
|
|
5044
5183
|
this._logMessageDebug('decrypt-ok', 'group.v2.pull', 'group.message_created', plaintext);
|
|
5045
5184
|
}
|
|
5046
|
-
const
|
|
5185
|
+
const cursorCurrentSeq = Number(cursor?.current_seq ?? 0);
|
|
5186
|
+
const hasServerCursor = cursor !== null && Object.prototype.hasOwnProperty.call(cursor, 'current_seq');
|
|
5187
|
+
const retentionFloor = Math.max(this._pullRetentionFloor(result, 'retention_floor_message_seq', 'retention_floor_message_seq'), Number.isFinite(cursorCurrentSeq) ? cursorCurrentSeq : 0);
|
|
5047
5188
|
if (retentionFloor > 0) {
|
|
5048
5189
|
const contig = this._seqTracker.getContiguousSeq(ns);
|
|
5049
5190
|
if (contig < retentionFloor) {
|
|
@@ -5057,11 +5198,17 @@ export class AUNClient {
|
|
|
5057
5198
|
await this._drainOrderedMessages(ns, undefined, true);
|
|
5058
5199
|
this._saveSeqTrackerState();
|
|
5059
5200
|
}
|
|
5060
|
-
|
|
5201
|
+
const ackNeeded = messages.length > 0
|
|
5202
|
+
&& ackSeq > 0
|
|
5203
|
+
&& ownsCursor
|
|
5204
|
+
&& (contigAdvanced || (hasServerCursor && ackSeq > cursorCurrentSeq));
|
|
5205
|
+
if (ackNeeded) {
|
|
5061
5206
|
this._clientLog.debug(`group.v2.pull scheduling auto-ack: group=${gid}, ns=${ns}, ack_seq=${ackSeq}, raw_count=${messages.length}`);
|
|
5062
5207
|
this._safeAsync(this._ackGroupV2(gid, ackSeq).then(() => undefined));
|
|
5063
5208
|
}
|
|
5064
5209
|
const nextAfter = Math.max(pageMaxSeq, nextAfterSeq);
|
|
5210
|
+
if (!ownsCursor)
|
|
5211
|
+
break;
|
|
5065
5212
|
if (messages.length === 0 || nextAfter <= nextAfterSeq || result.has_more === false)
|
|
5066
5213
|
break;
|
|
5067
5214
|
nextAfterSeq = nextAfter;
|
|
@@ -5072,6 +5219,10 @@ export class AUNClient {
|
|
|
5072
5219
|
this._clientLog.debug(`group.v2.pull done: group=${gid}, requested_after_seq=${afterSeq}, pages=${pageCount}, decrypted=${decrypted.length}, ns=${ns}`);
|
|
5073
5220
|
return decrypted;
|
|
5074
5221
|
}
|
|
5222
|
+
async _rawGroupAckMessages(params) {
|
|
5223
|
+
const p = { ...params };
|
|
5224
|
+
return await this._callRawV2Rpc('group.ack_messages', p);
|
|
5225
|
+
}
|
|
5075
5226
|
/** V2 Group ack。 */
|
|
5076
5227
|
async _ackGroupV2(groupId, upToSeq) {
|
|
5077
5228
|
const gid = normalizeGroupId(groupId) || String(groupId ?? '').trim();
|
|
@@ -6071,7 +6222,7 @@ export class AUNClient {
|
|
|
6071
6222
|
return;
|
|
6072
6223
|
}
|
|
6073
6224
|
let signature = '';
|
|
6074
|
-
const privateKeyPem =
|
|
6225
|
+
const privateKeyPem = this._currentAid?.privateKeyPem ?? '';
|
|
6075
6226
|
if (privateKeyPem) {
|
|
6076
6227
|
try {
|
|
6077
6228
|
const signPayload = stableStringify({
|
|
@@ -6403,9 +6554,9 @@ export class AUNClient {
|
|
|
6403
6554
|
if (this._gatewayUrl)
|
|
6404
6555
|
return this._gatewayUrl;
|
|
6405
6556
|
try {
|
|
6406
|
-
const loadMetadata = this.
|
|
6557
|
+
const loadMetadata = this._tokenStore.loadMetadata;
|
|
6407
6558
|
const cachedGateway = typeof loadMetadata === 'function'
|
|
6408
|
-
? String(loadMetadata.call(this.
|
|
6559
|
+
? String(loadMetadata.call(this._tokenStore, resolvedAid)?.gateway_url ?? '').trim()
|
|
6409
6560
|
: '';
|
|
6410
6561
|
if (cachedGateway) {
|
|
6411
6562
|
this._gatewayUrl = cachedGateway;
|
|
@@ -6427,9 +6578,9 @@ export class AUNClient {
|
|
|
6427
6578
|
const gateway = await this._discovery.discover(url);
|
|
6428
6579
|
this._gatewayUrl = gateway;
|
|
6429
6580
|
try {
|
|
6430
|
-
const saveMetadata = this.
|
|
6581
|
+
const saveMetadata = this._tokenStore.saveMetadata;
|
|
6431
6582
|
if (typeof saveMetadata === 'function') {
|
|
6432
|
-
saveMetadata.call(this.
|
|
6583
|
+
saveMetadata.call(this._tokenStore, resolvedAid, { gateway_url: gateway, gateway_cached_at: Date.now() });
|
|
6433
6584
|
}
|
|
6434
6585
|
}
|
|
6435
6586
|
catch {
|
|
@@ -6475,9 +6626,8 @@ export class AUNClient {
|
|
|
6475
6626
|
}
|
|
6476
6627
|
/** 连接后同步身份信息 */
|
|
6477
6628
|
_syncIdentityAfterConnect(accessToken) {
|
|
6478
|
-
const identity = this.
|
|
6629
|
+
const identity = this._identity;
|
|
6479
6630
|
if (identity === null) {
|
|
6480
|
-
this._identity = null;
|
|
6481
6631
|
return;
|
|
6482
6632
|
}
|
|
6483
6633
|
identity.access_token = accessToken;
|
|
@@ -6488,9 +6638,7 @@ export class AUNClient {
|
|
|
6488
6638
|
const persistIdentity = this._auth._persistIdentity;
|
|
6489
6639
|
if (typeof persistIdentity === 'function') {
|
|
6490
6640
|
persistIdentity.call(this._auth, identity);
|
|
6491
|
-
return;
|
|
6492
6641
|
}
|
|
6493
|
-
this._keystore.saveIdentity(String(identity.aid), identity);
|
|
6494
6642
|
}
|
|
6495
6643
|
// ── 内部:参数处理 ────────────────────────────────────────
|
|
6496
6644
|
/** 规范化连接参数 */
|
|
@@ -6674,7 +6822,7 @@ export class AUNClient {
|
|
|
6674
6822
|
scheduleNext();
|
|
6675
6823
|
return;
|
|
6676
6824
|
}
|
|
6677
|
-
let identity = this._identity
|
|
6825
|
+
let identity = this._identity;
|
|
6678
6826
|
if (identity === null) {
|
|
6679
6827
|
scheduleNext();
|
|
6680
6828
|
return;
|
|
@@ -7023,8 +7171,7 @@ export class AUNClient {
|
|
|
7023
7171
|
}
|
|
7024
7172
|
// ── Named Group(命名群)高层 API ────────────────────────────
|
|
7025
7173
|
/**
|
|
7026
|
-
*
|
|
7027
|
-
* 服务端签发群 AID 证书,返回后将证书和私钥存入 keystore。
|
|
7174
|
+
* 创建命名群:群/P2P 私钥由 V2 数据库存储,不写入 AID 身份私钥存储。
|
|
7028
7175
|
*/
|
|
7029
7176
|
async createNamedGroup(groupName, opts = {}) {
|
|
7030
7177
|
const tStart = Date.now();
|
|
@@ -7044,15 +7191,10 @@ export class AUNClient {
|
|
|
7044
7191
|
const aidCert = result?.aid_cert;
|
|
7045
7192
|
const groupAid = String(groupInfo?.group_aid ?? '');
|
|
7046
7193
|
if (groupAid && aidCert) {
|
|
7047
|
-
this.
|
|
7048
|
-
private_key_pem: identity.private_key_pem,
|
|
7049
|
-
public_key: identity.public_key_der_b64,
|
|
7050
|
-
curve: 'P-256',
|
|
7051
|
-
type: 'group_identity',
|
|
7052
|
-
});
|
|
7194
|
+
this._saveGroupIdentityToV2(groupAid, identity);
|
|
7053
7195
|
const certPem = String(aidCert.cert ?? '');
|
|
7054
7196
|
if (certPem) {
|
|
7055
|
-
this.
|
|
7197
|
+
this._tokenStore.saveCert(groupAid, certPem);
|
|
7056
7198
|
}
|
|
7057
7199
|
}
|
|
7058
7200
|
this._clientLog.debug(`createNamedGroup exit: elapsed=${Date.now() - tStart}ms groupAid=${groupAid}`);
|
|
@@ -7083,15 +7225,10 @@ export class AUNClient {
|
|
|
7083
7225
|
const aidCert = result?.aid_cert;
|
|
7084
7226
|
const groupAid = String(groupInfo?.group_aid ?? '');
|
|
7085
7227
|
if (groupAid && aidCert) {
|
|
7086
|
-
this.
|
|
7087
|
-
private_key_pem: identity.private_key_pem,
|
|
7088
|
-
public_key: identity.public_key_der_b64,
|
|
7089
|
-
curve: 'P-256',
|
|
7090
|
-
type: 'group_identity',
|
|
7091
|
-
});
|
|
7228
|
+
this._saveGroupIdentityToV2(groupAid, identity);
|
|
7092
7229
|
const certPem = String(aidCert.cert ?? '');
|
|
7093
7230
|
if (certPem) {
|
|
7094
|
-
this.
|
|
7231
|
+
this._tokenStore.saveCert(groupAid, certPem);
|
|
7095
7232
|
}
|
|
7096
7233
|
}
|
|
7097
7234
|
this._clientLog.debug(`bindGroupAid exit: elapsed=${Date.now() - tStart}ms groupAid=${groupAid}`);
|