@agentunion/fastaun-browser 0.2.17 → 0.2.18
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/dist/auth.d.ts +3 -0
- package/dist/auth.d.ts.map +1 -1
- package/dist/auth.js +293 -211
- package/dist/auth.js.map +1 -1
- package/dist/client.d.ts +11 -0
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js +1080 -812
- package/dist/client.js.map +1 -1
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +3 -1
- package/dist/config.js.map +1 -1
- package/dist/discovery.d.ts +3 -0
- package/dist/discovery.d.ts.map +1 -1
- package/dist/discovery.js +15 -1
- package/dist/discovery.js.map +1 -1
- package/dist/e2ee-group.d.ts +4 -0
- package/dist/e2ee-group.d.ts.map +1 -1
- package/dist/e2ee-group.js +327 -201
- package/dist/e2ee-group.js.map +1 -1
- package/dist/e2ee.d.ts +4 -0
- package/dist/e2ee.d.ts.map +1 -1
- package/dist/e2ee.js +176 -117
- package/dist/e2ee.js.map +1 -1
- package/dist/events.d.ts +3 -0
- package/dist/events.d.ts.map +1 -1
- package/dist/events.js +4 -1
- package/dist/events.js.map +1 -1
- package/dist/keystore/indexeddb.d.ts +3 -0
- package/dist/keystore/indexeddb.d.ts.map +1 -1
- package/dist/keystore/indexeddb.js +153 -97
- package/dist/keystore/indexeddb.js.map +1 -1
- package/dist/logger.d.ts +37 -0
- package/dist/logger.d.ts.map +1 -0
- package/dist/logger.js +112 -0
- package/dist/logger.js.map +1 -0
- package/dist/namespaces/auth.d.ts +4 -0
- package/dist/namespaces/auth.d.ts.map +1 -1
- package/dist/namespaces/auth.js +214 -101
- package/dist/namespaces/auth.js.map +1 -1
- package/dist/namespaces/custody.d.ts +3 -0
- package/dist/namespaces/custody.d.ts.map +1 -1
- package/dist/namespaces/custody.js +147 -75
- package/dist/namespaces/custody.js.map +1 -1
- package/dist/namespaces/meta.d.ts +3 -0
- package/dist/namespaces/meta.d.ts.map +1 -1
- package/dist/namespaces/meta.js +94 -43
- package/dist/namespaces/meta.js.map +1 -1
- package/dist/secret-store/indexeddb-store.d.ts +3 -0
- package/dist/secret-store/indexeddb-store.d.ts.map +1 -1
- package/dist/secret-store/indexeddb-store.js +57 -29
- package/dist/secret-store/indexeddb-store.js.map +1 -1
- package/dist/transport.d.ts +3 -0
- package/dist/transport.d.ts.map +1 -1
- package/dist/transport.js +74 -4
- package/dist/transport.js.map +1 -1
- package/package.json +1 -1
package/dist/client.js
CHANGED
|
@@ -15,9 +15,10 @@ import { AuthNamespace } from './namespaces/auth.js';
|
|
|
15
15
|
import { CustodyNamespace } from './namespaces/custody.js';
|
|
16
16
|
import { MetaNamespace } from './namespaces/meta.js';
|
|
17
17
|
import { CryptoProvider, uint8ToBase64, base64ToUint8, pemToArrayBuffer, p1363ToDer, toBufferSource } from './crypto.js';
|
|
18
|
-
import { E2EEManager, _certificateSha256Fingerprint as certificateSha256Fingerprint, _ecdsaVerifyDer as ecdsaVerifyDer, _importCertPublicKeyEcdsa as importCertPublicKeyEcdsa, } from './e2ee.js';
|
|
19
|
-
import { GroupE2EEManager, computeMembershipCommitment, computeStateHash, storeGroupSecret, storeGroupSecretEpoch, buildKeyDistribution, buildKeyRequest, buildMembershipManifest, signMembershipManifest, verifyEpochChain, } from './e2ee-group.js';
|
|
18
|
+
import { E2EEManager, _certificateSha256Fingerprint as certificateSha256Fingerprint, _ecdsaVerifyDer as ecdsaVerifyDer, _importCertPublicKeyEcdsa as importCertPublicKeyEcdsa, setModuleLogger as setE2eeModuleLogger, } from './e2ee.js';
|
|
19
|
+
import { GroupE2EEManager, computeMembershipCommitment, computeStateHash, storeGroupSecret, storeGroupSecretEpoch, buildKeyDistribution, buildKeyRequest, buildMembershipManifest, signMembershipManifest, verifyEpochChain, setModuleLogger as setE2eeGroupModuleLogger, } from './e2ee-group.js';
|
|
20
20
|
import { IndexedDBKeyStore } from './keystore/indexeddb.js';
|
|
21
|
+
import { AUNLogger } from './logger.js';
|
|
21
22
|
import { AUNError, AuthError, ConnectionError, E2EEError, PermissionError, StateError, ValidationError, } from './errors.js';
|
|
22
23
|
import { isJsonObject, } from './types.js';
|
|
23
24
|
/**
|
|
@@ -377,14 +378,36 @@ export class AUNClient {
|
|
|
377
378
|
_reconnectActive = false;
|
|
378
379
|
_reconnectAbort = null;
|
|
379
380
|
_serverKicked = false;
|
|
381
|
+
// Logger(per-client 单例 + 各模块子 logger)
|
|
382
|
+
_logger;
|
|
383
|
+
_clientLog;
|
|
384
|
+
_logE2;
|
|
385
|
+
_logEG;
|
|
386
|
+
_logAuth;
|
|
387
|
+
_logTransport;
|
|
388
|
+
_logKeystore;
|
|
389
|
+
_logDiscovery;
|
|
390
|
+
_logEvents;
|
|
380
391
|
constructor(config, _debug = false) {
|
|
381
392
|
const rawConfig = config ?? {};
|
|
382
393
|
this.configModel = createConfig(rawConfig);
|
|
394
|
+
const initAid = String(rawConfig.aid ?? '').trim() || null;
|
|
383
395
|
this.config = {
|
|
384
396
|
aun_path: this.configModel.aunPath,
|
|
385
397
|
root_ca_path: this.configModel.rootCaPem,
|
|
386
398
|
seed_password: this.configModel.seedPassword,
|
|
387
399
|
};
|
|
400
|
+
// Logger 必须最早初始化(其他子模块构造时通过 logger 输出)
|
|
401
|
+
this._logger = new AUNLogger({ debug: _debug });
|
|
402
|
+
this._clientLog = this._logger.for('aun_core.client');
|
|
403
|
+
this._logE2 = this._logger.for('aun_core.e2ee');
|
|
404
|
+
this._logEG = this._logger.for('aun_core.e2ee-group');
|
|
405
|
+
this._logAuth = this._logger.for('aun_core.auth');
|
|
406
|
+
this._logTransport = this._logger.for('aun_core.transport');
|
|
407
|
+
this._logKeystore = this._logger.for('aun_core.keystore');
|
|
408
|
+
this._logDiscovery = this._logger.for('aun_core.discovery');
|
|
409
|
+
this._logEvents = this._logger.for('aun_core.events');
|
|
410
|
+
this._clientLog.info(`AUNClient initialized: debug=${_debug} aunPath=${this.configModel.aunPath} aid=${initAid ?? '-'}`);
|
|
388
411
|
this._dispatcher = new EventDispatcher();
|
|
389
412
|
this._discovery = new GatewayDiscovery();
|
|
390
413
|
this._keystore = new IndexedDBKeyStore();
|
|
@@ -395,12 +418,13 @@ export class AUNClient {
|
|
|
395
418
|
this._auth = new AuthFlow({
|
|
396
419
|
keystore: this._keystore,
|
|
397
420
|
crypto: new CryptoProvider(),
|
|
398
|
-
aid:
|
|
421
|
+
aid: initAid,
|
|
399
422
|
deviceId: this._deviceId,
|
|
400
423
|
slotId: this._slotId,
|
|
401
424
|
rootCaPem: this.configModel.rootCaPem,
|
|
402
425
|
verifySsl: this.configModel.verifySsl,
|
|
403
426
|
});
|
|
427
|
+
this._aid = initAid;
|
|
404
428
|
this._transport = new RPCTransport({
|
|
405
429
|
eventDispatcher: this._dispatcher,
|
|
406
430
|
timeout: DEFAULT_SESSION_OPTIONS.timeouts.call,
|
|
@@ -422,6 +446,29 @@ export class AUNClient {
|
|
|
422
446
|
this.auth = new AuthNamespace(this);
|
|
423
447
|
this.custody = new CustodyNamespace(this);
|
|
424
448
|
this.meta = new MetaNamespace(this);
|
|
449
|
+
// 注入 logger 到各子模块(构造时未传 logger,构造后通过 setLogger 注入)
|
|
450
|
+
this._auth.setLogger(this._logAuth);
|
|
451
|
+
this._transport.setLogger(this._logTransport);
|
|
452
|
+
this._dispatcher.setLogger(this._logEvents);
|
|
453
|
+
this._e2ee.setLogger(this._logE2);
|
|
454
|
+
this._groupE2ee.setLogger(this._logEG);
|
|
455
|
+
setE2eeModuleLogger(this._logE2);
|
|
456
|
+
setE2eeGroupModuleLogger(this._logEG);
|
|
457
|
+
if (typeof this._discovery.setLogger === 'function') {
|
|
458
|
+
this._discovery.setLogger(this._logger.for('aun_core.discovery'));
|
|
459
|
+
}
|
|
460
|
+
if (typeof this.auth.setLogger === 'function') {
|
|
461
|
+
this.auth.setLogger(this._logger.for('aun_core.namespace.auth'));
|
|
462
|
+
}
|
|
463
|
+
if (typeof this.custody.setLogger === 'function') {
|
|
464
|
+
this.custody.setLogger(this._logger.for('aun_core.namespace.custody'));
|
|
465
|
+
}
|
|
466
|
+
if (typeof this._keystore.setLogger === 'function') {
|
|
467
|
+
this._keystore.setLogger(this._logKeystore);
|
|
468
|
+
}
|
|
469
|
+
if (typeof this.meta.setLogger === 'function') {
|
|
470
|
+
this.meta.setLogger(this._logger.for('aun_core.namespace.meta'));
|
|
471
|
+
}
|
|
425
472
|
// 内部订阅:推送消息自动解密后 re-publish 给用户
|
|
426
473
|
this._dispatcher.subscribe('_raw.message.received', (data) => {
|
|
427
474
|
this._onRawMessageReceived(data);
|
|
@@ -471,7 +518,17 @@ export class AUNClient {
|
|
|
471
518
|
}
|
|
472
519
|
/** 主动检查 gateway 可用性(GET /health) */
|
|
473
520
|
async checkGatewayHealth(gatewayUrl, timeout = 5000) {
|
|
474
|
-
|
|
521
|
+
const tStart = Date.now();
|
|
522
|
+
this._clientLog.debug(`checkGatewayHealth enter: gateway=${gatewayUrl} timeout=${timeout}`);
|
|
523
|
+
try {
|
|
524
|
+
const result = await this._discovery.checkHealth(gatewayUrl, timeout);
|
|
525
|
+
this._clientLog.debug(`checkGatewayHealth exit: elapsed=${Date.now() - tStart}ms healthy=${result}`);
|
|
526
|
+
return result;
|
|
527
|
+
}
|
|
528
|
+
catch (err) {
|
|
529
|
+
this._clientLog.debug(`checkGatewayHealth exit (error): elapsed=${Date.now() - tStart}ms err=${err instanceof Error ? err.message : String(err)}`);
|
|
530
|
+
throw err;
|
|
531
|
+
}
|
|
475
532
|
}
|
|
476
533
|
get e2ee() {
|
|
477
534
|
return this._e2ee;
|
|
@@ -487,7 +544,10 @@ export class AUNClient {
|
|
|
487
544
|
* @param options - 可选的会话选项(auto_reconnect, heartbeat_interval 等)
|
|
488
545
|
*/
|
|
489
546
|
async connect(auth, options) {
|
|
547
|
+
const tStart = Date.now();
|
|
548
|
+
this._clientLog.debug(`connect enter: state=${this._state} aid=${this._aid ?? '-'}`);
|
|
490
549
|
if (this._state !== 'idle' && this._state !== 'closed' && this._state !== 'disconnected') {
|
|
550
|
+
this._clientLog.debug(`connect exit (error): elapsed=${Date.now() - tStart}ms err=invalid_state state=${this._state}`);
|
|
491
551
|
throw new StateError(`connect not allowed in state ${this._state}`);
|
|
492
552
|
}
|
|
493
553
|
this._state = 'connecting';
|
|
@@ -499,18 +559,23 @@ export class AUNClient {
|
|
|
499
559
|
this._closing = false;
|
|
500
560
|
try {
|
|
501
561
|
await this._connectOnce(normalized, false);
|
|
562
|
+
this._clientLog.debug(`connect exit: elapsed=${Date.now() - tStart}ms state=${this._state}`);
|
|
502
563
|
}
|
|
503
564
|
catch (err) {
|
|
504
565
|
// 连接失败时回退状态,允许重试
|
|
505
566
|
if (this._state === 'connecting' || this._state === 'authenticating') {
|
|
506
567
|
this._state = 'disconnected';
|
|
507
568
|
}
|
|
569
|
+
this._clientLog.debug(`connect exit (error): elapsed=${Date.now() - tStart}ms err=${err instanceof Error ? err.message : String(err)}`);
|
|
508
570
|
throw err;
|
|
509
571
|
}
|
|
510
572
|
}
|
|
511
573
|
/** 断开连接但保留本地状态,可再次 connect */
|
|
512
574
|
async disconnect() {
|
|
575
|
+
const tStart = Date.now();
|
|
576
|
+
this._clientLog.debug(`disconnect enter: state=${this._state}`);
|
|
513
577
|
if (this._state !== 'connected' && this._state !== 'reconnecting') {
|
|
578
|
+
this._clientLog.debug(`disconnect exit: elapsed=${Date.now() - tStart}ms reason=not_connected`);
|
|
514
579
|
return;
|
|
515
580
|
}
|
|
516
581
|
this._saveSeqTrackerState();
|
|
@@ -523,45 +588,59 @@ export class AUNClient {
|
|
|
523
588
|
await this._transport.close();
|
|
524
589
|
this._state = 'disconnected';
|
|
525
590
|
await this._dispatcher.publish('connection.state', { state: this._state });
|
|
591
|
+
this._clientLog.debug(`disconnect exit: elapsed=${Date.now() - tStart}ms`);
|
|
526
592
|
}
|
|
527
593
|
/** 列出本地所有已存储的身份摘要(仅返回有有效私钥的 AID) */
|
|
528
594
|
async listIdentities() {
|
|
529
|
-
const
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
const
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
for (const [key, value] of Object.entries(identity)) {
|
|
551
|
-
if (!['aid', 'private_key_pem', 'public_key_der_b64', 'curve', 'cert'].includes(key)) {
|
|
552
|
-
metadata[key] = value;
|
|
595
|
+
const tStart = Date.now();
|
|
596
|
+
this._clientLog.debug('listIdentities enter');
|
|
597
|
+
try {
|
|
598
|
+
const listFn = this._keystore.listIdentities;
|
|
599
|
+
if (typeof listFn !== 'function') {
|
|
600
|
+
this._clientLog.debug(`listIdentities exit: elapsed=${Date.now() - tStart}ms count=0 reason=keystore_no_list`);
|
|
601
|
+
return [];
|
|
602
|
+
}
|
|
603
|
+
const aids = await listFn.call(this._keystore);
|
|
604
|
+
const summaries = [];
|
|
605
|
+
for (const aid of [...aids].sort()) {
|
|
606
|
+
const identity = await this._keystore.loadIdentity(aid);
|
|
607
|
+
if (!identity || !identity.private_key_pem)
|
|
608
|
+
continue;
|
|
609
|
+
const summary = { aid };
|
|
610
|
+
// 优先从 loadMetadata 获取
|
|
611
|
+
const loadMeta = this._keystore.loadMetadata;
|
|
612
|
+
if (typeof loadMeta === 'function') {
|
|
613
|
+
const md = await loadMeta.call(this._keystore, aid);
|
|
614
|
+
if (md && Object.keys(md).length > 0) {
|
|
615
|
+
summary.metadata = md;
|
|
553
616
|
}
|
|
554
617
|
}
|
|
555
|
-
|
|
556
|
-
|
|
618
|
+
// 回退:从 identity 中提取非核心字段
|
|
619
|
+
if (!summary.metadata) {
|
|
620
|
+
const metadata = {};
|
|
621
|
+
for (const [key, value] of Object.entries(identity)) {
|
|
622
|
+
if (!['aid', 'private_key_pem', 'public_key_der_b64', 'curve', 'cert'].includes(key)) {
|
|
623
|
+
metadata[key] = value;
|
|
624
|
+
}
|
|
625
|
+
}
|
|
626
|
+
if (Object.keys(metadata).length > 0) {
|
|
627
|
+
summary.metadata = metadata;
|
|
628
|
+
}
|
|
557
629
|
}
|
|
630
|
+
summaries.push(summary);
|
|
558
631
|
}
|
|
559
|
-
|
|
632
|
+
this._clientLog.debug(`listIdentities exit: elapsed=${Date.now() - tStart}ms count=${summaries.length}`);
|
|
633
|
+
return summaries;
|
|
634
|
+
}
|
|
635
|
+
catch (err) {
|
|
636
|
+
this._clientLog.debug(`listIdentities exit (error): elapsed=${Date.now() - tStart}ms err=${err instanceof Error ? err.message : String(err)}`);
|
|
637
|
+
throw err;
|
|
560
638
|
}
|
|
561
|
-
return summaries;
|
|
562
639
|
}
|
|
563
640
|
/** 关闭连接 */
|
|
564
641
|
async close() {
|
|
642
|
+
const tStart = Date.now();
|
|
643
|
+
this._clientLog.debug(`close enter: state=${this._state}`);
|
|
565
644
|
this._closing = true;
|
|
566
645
|
this._saveSeqTrackerState();
|
|
567
646
|
this._stopBackgroundTasks();
|
|
@@ -574,6 +653,7 @@ export class AUNClient {
|
|
|
574
653
|
if (this._state === 'idle' || this._state === 'closed') {
|
|
575
654
|
this._state = 'closed';
|
|
576
655
|
this._resetSeqTrackingState();
|
|
656
|
+
this._clientLog.debug(`close exit: elapsed=${Date.now() - tStart}ms reason=already_idle`);
|
|
577
657
|
return;
|
|
578
658
|
}
|
|
579
659
|
// 关闭前通知服务端主动退出(best-effort,失败不阻塞)
|
|
@@ -587,6 +667,7 @@ export class AUNClient {
|
|
|
587
667
|
this._state = 'closed';
|
|
588
668
|
await this._dispatcher.publish('connection.state', { state: this._state });
|
|
589
669
|
this._resetSeqTrackingState();
|
|
670
|
+
this._clientLog.debug(`close exit: elapsed=${Date.now() - tStart}ms`);
|
|
590
671
|
}
|
|
591
672
|
// ── RPC ───────────────────────────────────────────
|
|
592
673
|
/**
|
|
@@ -596,6 +677,19 @@ export class AUNClient {
|
|
|
596
677
|
* 自动解密 message.pull/group.pull、Group E2EE 生命周期编排。
|
|
597
678
|
*/
|
|
598
679
|
async call(method, params) {
|
|
680
|
+
const tStart = Date.now();
|
|
681
|
+
this._clientLog.debug(`call enter: method=${method}`);
|
|
682
|
+
try {
|
|
683
|
+
const result = await this._callImpl(method, params);
|
|
684
|
+
this._clientLog.debug(`call exit: elapsed=${Date.now() - tStart}ms method=${method}`);
|
|
685
|
+
return result;
|
|
686
|
+
}
|
|
687
|
+
catch (err) {
|
|
688
|
+
this._clientLog.debug(`call exit (error): elapsed=${Date.now() - tStart}ms method=${method} err=${err instanceof Error ? err.message : String(err)}`);
|
|
689
|
+
throw err;
|
|
690
|
+
}
|
|
691
|
+
}
|
|
692
|
+
async _callImpl(method, params) {
|
|
599
693
|
if (this._state !== 'connected') {
|
|
600
694
|
throw new ConnectionError('client is not connected');
|
|
601
695
|
}
|
|
@@ -605,10 +699,6 @@ export class AUNClient {
|
|
|
605
699
|
const p = { ...(params ?? {}) };
|
|
606
700
|
this._validateOutboundCall(method, p);
|
|
607
701
|
this._injectMessageCursorContext(method, p);
|
|
608
|
-
// group.* 方法的 group_id 归一化为 canonical 格式(兼容老/污染数据)
|
|
609
|
-
if (method.startsWith('group.') && p.group_id !== undefined && p.group_id !== null && p.group_id !== '') {
|
|
610
|
-
p.group_id = normalizeGroupId(p.group_id);
|
|
611
|
-
}
|
|
612
702
|
// group.* 方法注入 device_id(服务端用于多设备消息路由)
|
|
613
703
|
if (method.startsWith('group.') && this._deviceId && p.device_id === undefined) {
|
|
614
704
|
p.device_id = this._deviceId;
|
|
@@ -678,7 +768,7 @@ export class AUNClient {
|
|
|
678
768
|
if (serverAck > 0) {
|
|
679
769
|
const contig = this._seqTracker.getContiguousSeq(ns);
|
|
680
770
|
if (contig < serverAck) {
|
|
681
|
-
|
|
771
|
+
this._clientLog.info('message.pull retention-floor advance: ns=' + ns + ' contiguous=' + contig + ' -> server_ack_seq=' + serverAck);
|
|
682
772
|
this._seqTracker.forceContiguousSeq(ns, serverAck);
|
|
683
773
|
}
|
|
684
774
|
}
|
|
@@ -690,7 +780,7 @@ export class AUNClient {
|
|
|
690
780
|
seq: contig,
|
|
691
781
|
device_id: this._deviceId,
|
|
692
782
|
slot_id: this._slotId,
|
|
693
|
-
}).catch((e) => {
|
|
783
|
+
}).catch((e) => { this._clientLog.warn(`message.pull auto-ack failed:${String(e)}`); });
|
|
694
784
|
}
|
|
695
785
|
}
|
|
696
786
|
}
|
|
@@ -719,7 +809,7 @@ export class AUNClient {
|
|
|
719
809
|
if (serverAck > 0) {
|
|
720
810
|
const contig = this._seqTracker.getContiguousSeq(ns);
|
|
721
811
|
if (contig < serverAck) {
|
|
722
|
-
|
|
812
|
+
this._clientLog.info('group.pull retention-floor advance: ns=' + ns + ' contiguous=' + contig + ' -> cursor.current_seq=' + serverAck);
|
|
723
813
|
this._seqTracker.forceContiguousSeq(ns, serverAck);
|
|
724
814
|
}
|
|
725
815
|
}
|
|
@@ -734,7 +824,7 @@ export class AUNClient {
|
|
|
734
824
|
msg_seq: contig,
|
|
735
825
|
device_id: this._deviceId,
|
|
736
826
|
slot_id: this._slotId,
|
|
737
|
-
}).catch((e) => {
|
|
827
|
+
}).catch((e) => { this._clientLog.warn('group.pull auto-ack failed: group=' + gid, e); });
|
|
738
828
|
}
|
|
739
829
|
}
|
|
740
830
|
}
|
|
@@ -781,7 +871,7 @@ export class AUNClient {
|
|
|
781
871
|
// P0-12: await rotation 完成(带超时兜底),确保后续 group.send 使用新 epoch
|
|
782
872
|
const rotationPromise = this._maybeLeadRotateGroupEpoch(groupId, this._membershipRotationTriggerId(groupId, result), expectedEpoch, allowMember);
|
|
783
873
|
const timeoutPromise = new Promise((resolve) => globalThis.setTimeout(resolve, 5000));
|
|
784
|
-
await Promise.race([rotationPromise, timeoutPromise]).catch((exc) =>
|
|
874
|
+
await Promise.race([rotationPromise, timeoutPromise]).catch((exc) => this._clientLog.warn(`membership RPC epoch rotation fallback failed:${String(exc)}`));
|
|
785
875
|
}
|
|
786
876
|
}
|
|
787
877
|
return result;
|
|
@@ -813,7 +903,9 @@ export class AUNClient {
|
|
|
813
903
|
// ── 事件管道:消息解密 ────────────────────────────
|
|
814
904
|
/** 处理 transport 层推送的原始消息:解密后 re-publish 给用户 */
|
|
815
905
|
_onRawMessageReceived(data) {
|
|
906
|
+
this._clientLog.debug(`_onRawMessageReceived enter: from=${data?.from ?? '-'} mid=${data?.message_id ?? '-'} seq=${data?.seq ?? '-'}`);
|
|
816
907
|
this._safeAsync(this._processAndPublishMessage(data));
|
|
908
|
+
this._clientLog.debug(`_onRawMessageReceived exit: elapsed=0ms (dispatched async)`);
|
|
817
909
|
}
|
|
818
910
|
/** 实际处理推送消息的异步任务 */
|
|
819
911
|
async _processAndPublishMessage(data) {
|
|
@@ -847,7 +939,7 @@ export class AUNClient {
|
|
|
847
939
|
seq: contig,
|
|
848
940
|
device_id: this._deviceId,
|
|
849
941
|
slot_id: this._slotId,
|
|
850
|
-
}).catch((e) => {
|
|
942
|
+
}).catch((e) => { this._clientLog.warn(`P2P auto-ack failed:${String(e)}`); });
|
|
851
943
|
}
|
|
852
944
|
// 即时持久化 cursor,异常断连后不回退
|
|
853
945
|
this._saveSeqTrackerState();
|
|
@@ -862,7 +954,7 @@ export class AUNClient {
|
|
|
862
954
|
}
|
|
863
955
|
}
|
|
864
956
|
catch (exc) {
|
|
865
|
-
|
|
957
|
+
this._clientLog.warn(`messagedecryptfailed:${String(exc)}`);
|
|
866
958
|
// H26: 解密失败不再投递原始密文 payload(避免元数据泄漏 + 语义混淆),
|
|
867
959
|
// 改为发布 message.undecryptable 事件,仅携带安全的 header 信息。
|
|
868
960
|
if (isJsonObject(data)) {
|
|
@@ -881,7 +973,9 @@ export class AUNClient {
|
|
|
881
973
|
}
|
|
882
974
|
/** 处理群组消息推送:自动解密后 re-publish */
|
|
883
975
|
_onRawGroupMessageCreated(data) {
|
|
976
|
+
this._clientLog.debug(`_onRawGroupMessageCreated enter: group_id=${data?.group_id ?? '-'} from=${data?.from ?? '-'} seq=${data?.seq ?? '-'}`);
|
|
884
977
|
this._safeAsync(this._processAndPublishGroupMessage(data));
|
|
978
|
+
this._clientLog.debug(`_onRawGroupMessageCreated exit: elapsed=0ms (dispatched async)`);
|
|
885
979
|
}
|
|
886
980
|
/**
|
|
887
981
|
* 处理群组推送消息的异步任务。
|
|
@@ -924,7 +1018,7 @@ export class AUNClient {
|
|
|
924
1018
|
msg_seq: contig,
|
|
925
1019
|
device_id: this._deviceId,
|
|
926
1020
|
slot_id: this._slotId,
|
|
927
|
-
}).catch((e) => {
|
|
1021
|
+
}).catch((e) => { this._clientLog.warn('group message auto-ack failed: group=' + groupId, e); });
|
|
928
1022
|
}
|
|
929
1023
|
this._saveSeqTrackerState();
|
|
930
1024
|
}
|
|
@@ -952,7 +1046,7 @@ export class AUNClient {
|
|
|
952
1046
|
}
|
|
953
1047
|
}
|
|
954
1048
|
catch (exc) {
|
|
955
|
-
|
|
1049
|
+
this._clientLog.warn(`group message decrypt failed:${String(exc)}`);
|
|
956
1050
|
// H26: 解密失败改发 group.message_undecryptable 事件,不投递原始密文 payload。
|
|
957
1051
|
if (isJsonObject(data)) {
|
|
958
1052
|
const src = data;
|
|
@@ -1009,7 +1103,7 @@ export class AUNClient {
|
|
|
1009
1103
|
}
|
|
1010
1104
|
}
|
|
1011
1105
|
catch (exc) {
|
|
1012
|
-
|
|
1106
|
+
this._clientLog.warn(`auto pull group message failed:${String(exc)}`);
|
|
1013
1107
|
}
|
|
1014
1108
|
// pull 失败时仍透传原始通知
|
|
1015
1109
|
await this._publishAppEvent('group.message_created', notification);
|
|
@@ -1059,7 +1153,7 @@ export class AUNClient {
|
|
|
1059
1153
|
}
|
|
1060
1154
|
}
|
|
1061
1155
|
catch (exc) {
|
|
1062
|
-
|
|
1156
|
+
this._clientLog.warn(`group message gap-fill failed:${String(exc)}`);
|
|
1063
1157
|
}
|
|
1064
1158
|
finally {
|
|
1065
1159
|
// S1: 成功 / 失败路径都必须清理飞行标记
|
|
@@ -1097,7 +1191,7 @@ export class AUNClient {
|
|
|
1097
1191
|
if (serverAck > 0) {
|
|
1098
1192
|
const contigBefore = this._seqTracker.getContiguousSeq(ns);
|
|
1099
1193
|
if (contigBefore < serverAck) {
|
|
1100
|
-
|
|
1194
|
+
this._clientLog.info('group.pull_events retention-floor advance: ns=' + ns + ' contiguous=' + contigBefore + ' -> cursor.current_seq=' + serverAck);
|
|
1101
1195
|
this._seqTracker.forceContiguousSeq(ns, serverAck);
|
|
1102
1196
|
}
|
|
1103
1197
|
}
|
|
@@ -1110,7 +1204,7 @@ export class AUNClient {
|
|
|
1110
1204
|
event_seq: contig,
|
|
1111
1205
|
device_id: this._deviceId,
|
|
1112
1206
|
slot_id: this._slotId,
|
|
1113
|
-
}).catch((e) => {
|
|
1207
|
+
}).catch((e) => { this._clientLog.warn('group event auto-ack failed: group=' + groupId, e); });
|
|
1114
1208
|
}
|
|
1115
1209
|
for (const evt of events) {
|
|
1116
1210
|
if (isJsonObject(evt)) {
|
|
@@ -1132,7 +1226,7 @@ export class AUNClient {
|
|
|
1132
1226
|
}
|
|
1133
1227
|
}
|
|
1134
1228
|
catch (exc) {
|
|
1135
|
-
|
|
1229
|
+
this._clientLog.warn(`group event gap-fill failed:${String(exc)}`);
|
|
1136
1230
|
}
|
|
1137
1231
|
finally {
|
|
1138
1232
|
// S1: 成功 / 失败路径都必须清理飞行标记
|
|
@@ -1185,7 +1279,7 @@ export class AUNClient {
|
|
|
1185
1279
|
}
|
|
1186
1280
|
}
|
|
1187
1281
|
catch (exc) {
|
|
1188
|
-
|
|
1282
|
+
this._clientLog.warn(`P2P message gap-fill failed:${String(exc)}`);
|
|
1189
1283
|
}
|
|
1190
1284
|
finally {
|
|
1191
1285
|
// S1: 成功 / 失败路径都必须清理飞行标记
|
|
@@ -1330,10 +1424,10 @@ export class AUNClient {
|
|
|
1330
1424
|
encrypt: true,
|
|
1331
1425
|
persist_required: true,
|
|
1332
1426
|
});
|
|
1333
|
-
|
|
1427
|
+
this._clientLog.info(`to ${targetAid} request group ${groupId} key`);
|
|
1334
1428
|
}
|
|
1335
1429
|
catch (exc) {
|
|
1336
|
-
|
|
1430
|
+
this._clientLog.warn(`to ${targetAid} request group ${groupId} key failed: ${String(exc)}`);
|
|
1337
1431
|
}
|
|
1338
1432
|
}
|
|
1339
1433
|
/**
|
|
@@ -1460,82 +1554,93 @@ export class AUNClient {
|
|
|
1460
1554
|
return member ? String(member.group_id ?? '') : '';
|
|
1461
1555
|
}
|
|
1462
1556
|
async _onRawGroupChanged(data) {
|
|
1463
|
-
|
|
1464
|
-
|
|
1465
|
-
|
|
1466
|
-
|
|
1467
|
-
|
|
1468
|
-
|
|
1469
|
-
|
|
1470
|
-
|
|
1471
|
-
|
|
1472
|
-
|
|
1473
|
-
|
|
1474
|
-
|
|
1475
|
-
|
|
1476
|
-
|
|
1477
|
-
|
|
1478
|
-
|
|
1479
|
-
|
|
1480
|
-
|
|
1481
|
-
|
|
1482
|
-
|
|
1483
|
-
|
|
1484
|
-
|
|
1485
|
-
}
|
|
1486
|
-
if (d.action === 'member_left' || d.action === 'member_removed') {
|
|
1487
|
-
if (groupId) {
|
|
1488
|
-
const expectedEpoch = this._membershipRotationExpectedEpoch(d);
|
|
1489
|
-
if (expectedEpoch === null) {
|
|
1490
|
-
console.debug('membership event without old_epoch skipped for epoch rotation: aid=%s group=%s action=%s event_seq=%s', this._aid ?? '', groupId, String(d.action ?? ''), String(d.event_seq ?? ''));
|
|
1491
|
-
}
|
|
1492
|
-
else {
|
|
1493
|
-
this._safeAsync(this._maybeLeadRotateGroupEpoch(groupId, this._membershipRotationTriggerId(groupId, d), expectedEpoch));
|
|
1557
|
+
const tStart = Date.now();
|
|
1558
|
+
const action = String(data?.action ?? '');
|
|
1559
|
+
const groupIdInit = String(data?.group_id ?? '');
|
|
1560
|
+
this._clientLog.debug(`_onRawGroupChanged enter: group_id=${groupIdInit} action=${action}`);
|
|
1561
|
+
try {
|
|
1562
|
+
if (isJsonObject(data)) {
|
|
1563
|
+
const d = data;
|
|
1564
|
+
// 验签:有 client_signature 就验,没有默认安全
|
|
1565
|
+
const cs = d.client_signature;
|
|
1566
|
+
if (cs && isJsonObject(cs)) {
|
|
1567
|
+
d._verified = await this._verifyEventSignature(d, cs);
|
|
1568
|
+
}
|
|
1569
|
+
await this._dispatcher.publish('group.changed', d);
|
|
1570
|
+
const groupId = (d.group_id ?? '');
|
|
1571
|
+
// event_seq 空洞检测:持久化后的 group.changed 会携带 event_seq。
|
|
1572
|
+
// 用 onMessageSeq 返回值决定是否补拉,与 P2P / group.message 路径对齐。
|
|
1573
|
+
let needPull = false;
|
|
1574
|
+
const rawEventSeq = d.event_seq;
|
|
1575
|
+
if (rawEventSeq != null && groupId) {
|
|
1576
|
+
const es = Number(rawEventSeq);
|
|
1577
|
+
if (Number.isFinite(es) && es > 0) {
|
|
1578
|
+
needPull = this._seqTracker.onMessageSeq(`group_event:${groupId}`, es);
|
|
1494
1579
|
}
|
|
1495
1580
|
}
|
|
1496
|
-
|
|
1497
|
-
|
|
1498
|
-
|
|
1499
|
-
|
|
1500
|
-
|
|
1501
|
-
|
|
1502
|
-
|
|
1503
|
-
|
|
1504
|
-
|
|
1505
|
-
const isSelfJoining = joinedAids.includes(this._aid ?? '') && (action === 'joined' || action === 'invite_code_used');
|
|
1506
|
-
if (isSelfJoining || (action === 'joined' || action === 'invite_code_used')) {
|
|
1507
|
-
// open/invite_code 群:所有在线成员都参与延迟轮换
|
|
1508
|
-
// 新成员自己延迟更长,优先让其他在线成员先轮换
|
|
1509
|
-
const triggerId = this._membershipRotationTriggerId(groupId, d);
|
|
1510
|
-
if (!isSelfJoining) {
|
|
1511
|
-
this._safeAsync(this._maybeBackfillKeyToJoinedMember(groupId, d, triggerId));
|
|
1581
|
+
// 仅在真实 event gap 时才触发补拉(补洞回来的事件不再触发新补洞)
|
|
1582
|
+
if (needPull && groupId && !d._from_gap_fill) {
|
|
1583
|
+
this._safeAsync(this._fillGroupEventGap(groupId));
|
|
1584
|
+
}
|
|
1585
|
+
if (d.action === 'member_left' || d.action === 'member_removed') {
|
|
1586
|
+
if (groupId) {
|
|
1587
|
+
const expectedEpoch = this._membershipRotationExpectedEpoch(d);
|
|
1588
|
+
if (expectedEpoch === null) {
|
|
1589
|
+
this._clientLog.debug(`membership event without old_epoch skipped for epoch rotation: aid=${this._aid ?? ''} group=${groupId} action=${String(d.action ?? '')} event_seq=${String(d.event_seq ?? '')}`);
|
|
1512
1590
|
}
|
|
1513
|
-
|
|
1514
|
-
|
|
1515
|
-
this._safeAsync(this._delayedRotateAfterJoin(groupId, triggerId, expectedEpoch, true, delay));
|
|
1591
|
+
else {
|
|
1592
|
+
this._safeAsync(this._maybeLeadRotateGroupEpoch(groupId, this._membershipRotationTriggerId(groupId, d), expectedEpoch));
|
|
1516
1593
|
}
|
|
1517
1594
|
}
|
|
1518
|
-
|
|
1519
|
-
|
|
1595
|
+
}
|
|
1596
|
+
// 成员加入:按 action 区分策略
|
|
1597
|
+
// - member_added / join_approved(私密群/审批群):admin 必然在线,立即轮换
|
|
1598
|
+
// - joined / invite_code_used(开放群/邀请码群):所有在线成员延迟轮换,新成员自己延迟更长
|
|
1599
|
+
if (['member_added', 'joined', 'join_approved', 'invite_code_used'].includes(String(d.action ?? ''))) {
|
|
1600
|
+
if (groupId) {
|
|
1601
|
+
const action = String(d.action ?? '');
|
|
1602
|
+
const expectedEpoch = this._membershipRotationExpectedEpoch(d);
|
|
1603
|
+
const joinedAids = this._joinedMemberAidsFromPayload(d);
|
|
1604
|
+
const isSelfJoining = joinedAids.includes(this._aid ?? '') && (action === 'joined' || action === 'invite_code_used');
|
|
1605
|
+
if (isSelfJoining || (action === 'joined' || action === 'invite_code_used')) {
|
|
1606
|
+
// open/invite_code 群:所有在线成员都参与延迟轮换
|
|
1607
|
+
// 新成员自己延迟更长,优先让其他在线成员先轮换
|
|
1520
1608
|
const triggerId = this._membershipRotationTriggerId(groupId, d);
|
|
1521
|
-
|
|
1609
|
+
if (!isSelfJoining) {
|
|
1610
|
+
this._safeAsync(this._maybeBackfillKeyToJoinedMember(groupId, d, triggerId));
|
|
1611
|
+
}
|
|
1612
|
+
if (expectedEpoch !== null) {
|
|
1613
|
+
const delay = isSelfJoining ? AUNClient._SELF_JOIN_ROTATION_DELAY_MS : undefined;
|
|
1614
|
+
this._safeAsync(this._delayedRotateAfterJoin(groupId, triggerId, expectedEpoch, true, delay));
|
|
1615
|
+
}
|
|
1522
1616
|
}
|
|
1523
1617
|
else {
|
|
1524
|
-
|
|
1618
|
+
if (expectedEpoch === null) {
|
|
1619
|
+
const triggerId = this._membershipRotationTriggerId(groupId, d);
|
|
1620
|
+
this._safeAsync(this._maybeBackfillKeyToJoinedMember(groupId, d, triggerId));
|
|
1621
|
+
}
|
|
1622
|
+
else {
|
|
1623
|
+
this._safeAsync(this._maybeLeadRotateGroupEpoch(groupId, this._membershipRotationTriggerId(groupId, d), expectedEpoch));
|
|
1624
|
+
}
|
|
1525
1625
|
}
|
|
1526
1626
|
}
|
|
1527
1627
|
}
|
|
1528
|
-
|
|
1529
|
-
|
|
1530
|
-
|
|
1531
|
-
|
|
1532
|
-
|
|
1628
|
+
// 群组解散 → 清理本地 epoch key、seq_tracker、补洞去重缓存
|
|
1629
|
+
if (d.action === 'dissolved') {
|
|
1630
|
+
if (groupId) {
|
|
1631
|
+
this._cleanupDissolvedGroup(groupId);
|
|
1632
|
+
}
|
|
1533
1633
|
}
|
|
1534
1634
|
}
|
|
1635
|
+
else {
|
|
1636
|
+
// data 非对象也透传给用户(兼容旧版)
|
|
1637
|
+
await this._dispatcher.publish('group.changed', data);
|
|
1638
|
+
}
|
|
1639
|
+
this._clientLog.debug(`_onRawGroupChanged exit: elapsed=${Date.now() - tStart}ms group_id=${groupIdInit}`);
|
|
1535
1640
|
}
|
|
1536
|
-
|
|
1537
|
-
|
|
1538
|
-
|
|
1641
|
+
catch (err) {
|
|
1642
|
+
this._clientLog.debug(`_onRawGroupChanged exit (error): elapsed=${Date.now() - tStart}ms err=${err instanceof Error ? err.message : String(err)}`);
|
|
1643
|
+
throw err;
|
|
1539
1644
|
}
|
|
1540
1645
|
}
|
|
1541
1646
|
/**
|
|
@@ -1543,18 +1648,35 @@ export class AUNClient {
|
|
|
1543
1648
|
* 当 prev_state_hash 与本地不连续时回源 group.get_state,并对回源数据做 hash 验证。
|
|
1544
1649
|
*/
|
|
1545
1650
|
async _onGroupStateCommitted(data) {
|
|
1546
|
-
|
|
1547
|
-
|
|
1548
|
-
|
|
1549
|
-
|
|
1550
|
-
|
|
1551
|
-
|
|
1651
|
+
const tStart = Date.now();
|
|
1652
|
+
const groupIdInit = String(data?.group_id ?? '');
|
|
1653
|
+
this._clientLog.debug(`_onGroupStateCommitted enter: group_id=${groupIdInit} state_version=${String(data?.state_version ?? '-')}`);
|
|
1654
|
+
try {
|
|
1655
|
+
if (!isJsonObject(data)) {
|
|
1656
|
+
this._clientLog.debug(`_onGroupStateCommitted exit: elapsed=${Date.now() - tStart}ms reason=non_object`);
|
|
1657
|
+
return;
|
|
1658
|
+
}
|
|
1659
|
+
const d = data;
|
|
1660
|
+
const groupId = String(d.group_id ?? '').trim();
|
|
1661
|
+
if (!groupId) {
|
|
1662
|
+
this._clientLog.debug(`_onGroupStateCommitted exit: elapsed=${Date.now() - tStart}ms reason=no_group_id`);
|
|
1663
|
+
return;
|
|
1664
|
+
}
|
|
1665
|
+
await this._onGroupStateCommittedImpl(d, groupId);
|
|
1666
|
+
this._clientLog.debug(`_onGroupStateCommitted exit: elapsed=${Date.now() - tStart}ms group_id=${groupId}`);
|
|
1667
|
+
}
|
|
1668
|
+
catch (err) {
|
|
1669
|
+
this._clientLog.debug(`_onGroupStateCommitted exit (error): elapsed=${Date.now() - tStart}ms err=${err instanceof Error ? err.message : String(err)}`);
|
|
1670
|
+
throw err;
|
|
1671
|
+
}
|
|
1672
|
+
}
|
|
1673
|
+
async _onGroupStateCommittedImpl(d, groupId) {
|
|
1552
1674
|
// 提交者签名验证
|
|
1553
1675
|
const cs = d.client_signature;
|
|
1554
1676
|
if (cs && isJsonObject(cs)) {
|
|
1555
1677
|
const verified = await this._verifyEventSignature(d, cs);
|
|
1556
1678
|
if (verified === false) {
|
|
1557
|
-
|
|
1679
|
+
this._clientLog.warn(`state_committed committer signature verify failed group=%s${String(groupId)}`);
|
|
1558
1680
|
return;
|
|
1559
1681
|
}
|
|
1560
1682
|
d._verified = verified;
|
|
@@ -1571,7 +1693,7 @@ export class AUNClient {
|
|
|
1571
1693
|
? await loadFn.call(this._keystore, groupId)
|
|
1572
1694
|
: null;
|
|
1573
1695
|
if (localState && localState.state_hash && localState.state_hash !== prevStateHash) {
|
|
1574
|
-
|
|
1696
|
+
this._clientLog.warn('[aun_core] state_hash 链不连续 group=%s local_sv=%d event_sv=%d', groupId, localState.state_version, stateVersion);
|
|
1575
1697
|
// 回源同步
|
|
1576
1698
|
try {
|
|
1577
1699
|
const serverState = await this._transport.call('group.get_state', { group_id: groupId });
|
|
@@ -1591,7 +1713,7 @@ export class AUNClient {
|
|
|
1591
1713
|
members: sMembers, policy: sPolicy, prevStateHash: sPrev,
|
|
1592
1714
|
});
|
|
1593
1715
|
if (computed !== sHash) {
|
|
1594
|
-
|
|
1716
|
+
this._clientLog.warn('[aun_core] 回源 state_hash 验证失败 group=%s sv=%d expected=%s got=%s', groupId, sv, sHash, computed);
|
|
1595
1717
|
return;
|
|
1596
1718
|
}
|
|
1597
1719
|
}
|
|
@@ -1610,7 +1732,7 @@ export class AUNClient {
|
|
|
1610
1732
|
}
|
|
1611
1733
|
}
|
|
1612
1734
|
catch (exc) {
|
|
1613
|
-
|
|
1735
|
+
this._clientLog.warn(`state pull-back failed group=%s:${groupId} ${exc}`);
|
|
1614
1736
|
}
|
|
1615
1737
|
return;
|
|
1616
1738
|
}
|
|
@@ -1622,7 +1744,7 @@ export class AUNClient {
|
|
|
1622
1744
|
members, policy, prevStateHash,
|
|
1623
1745
|
});
|
|
1624
1746
|
if (computed !== stateHash) {
|
|
1625
|
-
|
|
1747
|
+
this._clientLog.warn('[aun_core] state_hash 重算不匹配 group=%s sv=%d expected=%s got=%s', groupId, stateVersion, stateHash, computed);
|
|
1626
1748
|
return;
|
|
1627
1749
|
}
|
|
1628
1750
|
// 3. 更新本地存储
|
|
@@ -1649,7 +1771,7 @@ export class AUNClient {
|
|
|
1649
1771
|
_cleanupDissolvedGroup(groupId) {
|
|
1650
1772
|
// 1. 清理 GroupE2EEManager / keystore 中的 epoch 密钥
|
|
1651
1773
|
this._safeAsync(this._groupE2ee.removeGroup(groupId).catch((exc) => {
|
|
1652
|
-
|
|
1774
|
+
this._clientLog.warn(`cleanup dissolved group ${groupId} epoch key failed: ${String(exc)}`);
|
|
1653
1775
|
}));
|
|
1654
1776
|
// 2. 清理 seq_tracker 中的群消息和群事件命名空间
|
|
1655
1777
|
this._seqTracker.removeNamespace(`group:${groupId}`);
|
|
@@ -1666,7 +1788,7 @@ export class AUNClient {
|
|
|
1666
1788
|
this._pushedSeqs.delete(`group_event:${groupId}`);
|
|
1667
1789
|
this._pendingOrderedMsgs.delete(`group:${groupId}`);
|
|
1668
1790
|
this._pendingDecryptMsgs.delete(`group:${groupId}`);
|
|
1669
|
-
|
|
1791
|
+
this._clientLog.info(`cleanup dissolved group ${groupId} local state`);
|
|
1670
1792
|
}
|
|
1671
1793
|
async _verifyEventSignature(_event, cs) {
|
|
1672
1794
|
const sigAid = String(cs.aid ?? '');
|
|
@@ -1683,7 +1805,7 @@ export class AUNClient {
|
|
|
1683
1805
|
if (expectedFP) {
|
|
1684
1806
|
const actualFP = await certificateSha256Fingerprint(cached.certPem);
|
|
1685
1807
|
if (actualFP !== expectedFP) {
|
|
1686
|
-
|
|
1808
|
+
this._clientLog.warn(`group event sig verify failed: cert fingerprint mismatch aid=%s${String(sigAid)}`);
|
|
1687
1809
|
return false;
|
|
1688
1810
|
}
|
|
1689
1811
|
}
|
|
@@ -1698,7 +1820,7 @@ export class AUNClient {
|
|
|
1698
1820
|
const sigBytes = base64ToUint8(sigB64);
|
|
1699
1821
|
const ok = await ecdsaVerifyDer(pubKey, sigBytes, signData);
|
|
1700
1822
|
if (!ok) {
|
|
1701
|
-
|
|
1823
|
+
this._clientLog.warn(`group event sig verify failed aid=%s method=%s${sigAid} ${method}`);
|
|
1702
1824
|
// P1-16: 签名失败统一发布事件
|
|
1703
1825
|
this._dispatcher.publish('signature.verification_failed', {
|
|
1704
1826
|
aid: sigAid, method, error: 'ECDSA verification failed',
|
|
@@ -1707,7 +1829,7 @@ export class AUNClient {
|
|
|
1707
1829
|
return ok;
|
|
1708
1830
|
}
|
|
1709
1831
|
catch (exc) {
|
|
1710
|
-
|
|
1832
|
+
this._clientLog.warn(`group event sig verify exception:${String(exc)}`);
|
|
1711
1833
|
// P1-16: 签名失败统一发布事件
|
|
1712
1834
|
this._dispatcher.publish('signature.verification_failed', {
|
|
1713
1835
|
aid: sigAid, method, error: String(exc),
|
|
@@ -1729,74 +1851,86 @@ export class AUNClient {
|
|
|
1729
1851
|
}
|
|
1730
1852
|
/** 自动加密并发送 P2P 消息 */
|
|
1731
1853
|
async _sendEncrypted(params) {
|
|
1854
|
+
const tStart = Date.now();
|
|
1732
1855
|
const toAid = String(params.to ?? '');
|
|
1733
|
-
this.
|
|
1734
|
-
|
|
1735
|
-
|
|
1736
|
-
|
|
1737
|
-
|
|
1738
|
-
|
|
1739
|
-
|
|
1740
|
-
|
|
1741
|
-
|
|
1742
|
-
|
|
1743
|
-
|
|
1744
|
-
|
|
1745
|
-
|
|
1746
|
-
|
|
1747
|
-
|
|
1748
|
-
|
|
1749
|
-
|
|
1750
|
-
|
|
1751
|
-
|
|
1752
|
-
|
|
1753
|
-
|
|
1754
|
-
|
|
1755
|
-
|
|
1756
|
-
|
|
1757
|
-
|
|
1758
|
-
|
|
1759
|
-
|
|
1760
|
-
|
|
1761
|
-
|
|
1762
|
-
|
|
1763
|
-
|
|
1856
|
+
this._clientLog.debug(`_sendEncrypted enter: to=${toAid} mid=${String(params.message_id ?? '<auto>')}`);
|
|
1857
|
+
try {
|
|
1858
|
+
this._validateMessageRecipient(toAid);
|
|
1859
|
+
const payload = isJsonObject(params.payload) ? params.payload : null;
|
|
1860
|
+
const messageId = String(params.message_id ?? '') || _uuidV4();
|
|
1861
|
+
const timestamp = params.timestamp ?? Date.now();
|
|
1862
|
+
if (payload === null) {
|
|
1863
|
+
throw new ValidationError('message.send payload must be an object when encrypt=true');
|
|
1864
|
+
}
|
|
1865
|
+
const persistRequired = Boolean(params.persist_required || params.durable);
|
|
1866
|
+
const protectedHeaders = this._protectedHeadersFromParams(params);
|
|
1867
|
+
// Lazy P2P sync:首次发送前自动拉取历史,避免重连后 seq 空洞
|
|
1868
|
+
if (!this._p2pSynced) {
|
|
1869
|
+
await this._lazySyncP2p();
|
|
1870
|
+
}
|
|
1871
|
+
// 内部发送逻辑,refreshPeerMaterial=true 时强制清缓存重新拉取对端材料
|
|
1872
|
+
const sendAttempt = async (refreshPeerMaterial = false) => {
|
|
1873
|
+
const recipientPrekeys = refreshPeerMaterial
|
|
1874
|
+
? await this._refreshPeerPrekeys(toAid)
|
|
1875
|
+
: await this._fetchPeerPrekeys(toAid);
|
|
1876
|
+
const selfSyncCopies = await this._buildSelfSyncCopies({
|
|
1877
|
+
logicalToAid: toAid, payload, messageId, timestamp, protectedHeaders,
|
|
1878
|
+
});
|
|
1879
|
+
// 多设备过滤:只保留有有效 device_id 的可路由 prekey,
|
|
1880
|
+
// 占位符 PREKEY_FALLBACK_DEVICE_ID 表示服务端未分配真实设备 ID,不可用于多设备路由
|
|
1881
|
+
const routablePrekeys = recipientPrekeys.filter(pk => {
|
|
1882
|
+
const did = String(pk.device_id ?? '').trim();
|
|
1883
|
+
return did && did !== PREKEY_FALLBACK_DEVICE_ID;
|
|
1884
|
+
});
|
|
1885
|
+
const canUseMultiDevice = routablePrekeys.length > 0
|
|
1886
|
+
&& (routablePrekeys.length > 1 || selfSyncCopies.length > 0);
|
|
1887
|
+
if (!canUseMultiDevice) {
|
|
1888
|
+
return await this._sendEncryptedSingle({
|
|
1889
|
+
toAid, payload, messageId, timestamp,
|
|
1890
|
+
prekey: routablePrekeys[0] ?? recipientPrekeys[0],
|
|
1891
|
+
persistRequired, protectedHeaders,
|
|
1892
|
+
});
|
|
1893
|
+
}
|
|
1894
|
+
const recipientCopies = await this._buildRecipientDeviceCopies({
|
|
1764
1895
|
toAid, payload, messageId, timestamp,
|
|
1765
|
-
|
|
1766
|
-
persistRequired, protectedHeaders,
|
|
1896
|
+
prekeys: routablePrekeys, protectedHeaders,
|
|
1767
1897
|
});
|
|
1768
|
-
|
|
1769
|
-
|
|
1770
|
-
|
|
1771
|
-
|
|
1772
|
-
|
|
1773
|
-
|
|
1774
|
-
|
|
1775
|
-
|
|
1898
|
+
const sendParams = {
|
|
1899
|
+
to: toAid,
|
|
1900
|
+
payload: {
|
|
1901
|
+
type: 'e2ee.multi_device',
|
|
1902
|
+
logical_message_id: messageId,
|
|
1903
|
+
recipient_copies: recipientCopies,
|
|
1904
|
+
self_copies: selfSyncCopies,
|
|
1905
|
+
},
|
|
1776
1906
|
type: 'e2ee.multi_device',
|
|
1777
|
-
|
|
1778
|
-
|
|
1779
|
-
|
|
1780
|
-
}
|
|
1781
|
-
|
|
1782
|
-
|
|
1783
|
-
|
|
1784
|
-
timestamp,
|
|
1907
|
+
encrypted: true,
|
|
1908
|
+
message_id: messageId,
|
|
1909
|
+
timestamp,
|
|
1910
|
+
};
|
|
1911
|
+
if (persistRequired)
|
|
1912
|
+
sendParams.persist_required = true;
|
|
1913
|
+
return this._transport.call('message.send', sendParams);
|
|
1785
1914
|
};
|
|
1786
|
-
|
|
1787
|
-
|
|
1788
|
-
|
|
1789
|
-
|
|
1790
|
-
|
|
1791
|
-
|
|
1792
|
-
|
|
1915
|
+
// 首次尝试(使用缓存);若对端证书/prekey 过期导致指纹不匹配,清缓存后重试一次
|
|
1916
|
+
try {
|
|
1917
|
+
const result = await sendAttempt(false);
|
|
1918
|
+
this._clientLog.debug(`_sendEncrypted exit: elapsed=${Date.now() - tStart}ms to=${toAid} retry=false`);
|
|
1919
|
+
return result;
|
|
1920
|
+
}
|
|
1921
|
+
catch (exc) {
|
|
1922
|
+
if (!isRetryablePeerMaterialError(exc))
|
|
1923
|
+
throw exc;
|
|
1924
|
+
this._clientLog.warn(`peer cert/prekey mismatch for ${toAid}, refreshing and retrying once`);
|
|
1925
|
+
}
|
|
1926
|
+
const result = await sendAttempt(true);
|
|
1927
|
+
this._clientLog.debug(`_sendEncrypted exit: elapsed=${Date.now() - tStart}ms to=${toAid} retry=true`);
|
|
1928
|
+
return result;
|
|
1793
1929
|
}
|
|
1794
|
-
catch (
|
|
1795
|
-
|
|
1796
|
-
|
|
1797
|
-
console.warn(`[aun_core] peer cert/prekey mismatch for ${toAid}, refreshing and retrying once`);
|
|
1930
|
+
catch (err) {
|
|
1931
|
+
this._clientLog.debug(`_sendEncrypted exit (error): elapsed=${Date.now() - tStart}ms to=${toAid} err=${err instanceof Error ? err.message : String(err)}`);
|
|
1932
|
+
throw err;
|
|
1798
1933
|
}
|
|
1799
|
-
return await sendAttempt(true);
|
|
1800
1934
|
}
|
|
1801
1935
|
/**
|
|
1802
1936
|
* 首次发送 P2P 消息前懒拉取历史消息,同步 seqTracker 避免空洞。
|
|
@@ -1822,7 +1956,7 @@ export class AUNClient {
|
|
|
1822
1956
|
}
|
|
1823
1957
|
}
|
|
1824
1958
|
catch (exc) {
|
|
1825
|
-
|
|
1959
|
+
this._clientLog.warn(`lazySyncP2p failed:${String(exc)}`);
|
|
1826
1960
|
}
|
|
1827
1961
|
}
|
|
1828
1962
|
async _sendEncryptedSingle(opts) {
|
|
@@ -1933,7 +2067,7 @@ export class AUNClient {
|
|
|
1933
2067
|
}
|
|
1934
2068
|
catch (e) {
|
|
1935
2069
|
// 旧设备的 prekey 可能携带已轮换的证书指纹,跳过该设备的自同步副本
|
|
1936
|
-
|
|
2070
|
+
this._clientLog.warn(`self-sync skip device ${deviceId}: cert parse failed (${e}), may be old prekey`);
|
|
1937
2071
|
continue;
|
|
1938
2072
|
}
|
|
1939
2073
|
const [envelope, encryptResult] = await this._encryptCopyPayload({
|
|
@@ -1980,7 +2114,7 @@ export class AUNClient {
|
|
|
1980
2114
|
});
|
|
1981
2115
|
}
|
|
1982
2116
|
catch (exc) {
|
|
1983
|
-
|
|
2117
|
+
this._clientLog.warn(`publish e2ee.degraded eventfailed:${String(exc)}`);
|
|
1984
2118
|
}
|
|
1985
2119
|
}
|
|
1986
2120
|
}
|
|
@@ -2067,10 +2201,21 @@ export class AUNClient {
|
|
|
2067
2201
|
}
|
|
2068
2202
|
/** 自动加密并发送群组消息 */
|
|
2069
2203
|
async _sendGroupEncrypted(params) {
|
|
2070
|
-
|
|
2071
|
-
|
|
2072
|
-
|
|
2073
|
-
|
|
2204
|
+
const tStart = Date.now();
|
|
2205
|
+
const groupId = String(params.group_id ?? '');
|
|
2206
|
+
this._clientLog.debug(`_sendGroupEncrypted enter: group_id=${groupId}`);
|
|
2207
|
+
try {
|
|
2208
|
+
const result = await this._callGroupEncryptedRpc('group.send', params, {
|
|
2209
|
+
idField: 'message_id',
|
|
2210
|
+
idPrefix: 'gm',
|
|
2211
|
+
});
|
|
2212
|
+
this._clientLog.debug(`_sendGroupEncrypted exit: elapsed=${Date.now() - tStart}ms group_id=${groupId}`);
|
|
2213
|
+
return result;
|
|
2214
|
+
}
|
|
2215
|
+
catch (err) {
|
|
2216
|
+
this._clientLog.debug(`_sendGroupEncrypted exit (error): elapsed=${Date.now() - tStart}ms group_id=${groupId} err=${err instanceof Error ? err.message : String(err)}`);
|
|
2217
|
+
throw err;
|
|
2218
|
+
}
|
|
2074
2219
|
}
|
|
2075
2220
|
async _putGroupThoughtEncrypted(params) {
|
|
2076
2221
|
return this._callGroupEncryptedRpc('group.thought.put', params, {
|
|
@@ -2126,7 +2271,7 @@ export class AUNClient {
|
|
|
2126
2271
|
}
|
|
2127
2272
|
catch (exc) {
|
|
2128
2273
|
if (attempt === 0 && this._isRecoverableGroupEpochError(exc)) {
|
|
2129
|
-
|
|
2274
|
+
this._clientLog.warn(`group ${prepared.groupId} call ${method} epoch too old, retry encrypt after key recovery : ${formatCaughtError(exc)}`);
|
|
2130
2275
|
prepared = await this._prepareGroupEncryptedRpcParams(method, params, options, true);
|
|
2131
2276
|
continue;
|
|
2132
2277
|
}
|
|
@@ -2217,7 +2362,7 @@ export class AUNClient {
|
|
|
2217
2362
|
}
|
|
2218
2363
|
}
|
|
2219
2364
|
catch (exc) {
|
|
2220
|
-
|
|
2365
|
+
this._clientLog.warn(`lazySyncGroup(${groupId}) failed: ${String(exc)}`);
|
|
2221
2366
|
}
|
|
2222
2367
|
}
|
|
2223
2368
|
_isGroupEpochTooOldError(exc) {
|
|
@@ -2275,7 +2420,7 @@ export class AUNClient {
|
|
|
2275
2420
|
const secretData = await this._groupE2ee.loadSecret(groupId, 1);
|
|
2276
2421
|
if (!secretData || secretData.pending_rotation_id)
|
|
2277
2422
|
return epochResult;
|
|
2278
|
-
|
|
2423
|
+
this._clientLog.warn(`group ${groupId} local epoch=1 but server epoch=0, try sync initial epoch`);
|
|
2279
2424
|
await this._syncEpochToServer(groupId);
|
|
2280
2425
|
try {
|
|
2281
2426
|
const refreshed = await this.call('group.e2ee.get_epoch', { group_id: groupId });
|
|
@@ -2283,7 +2428,7 @@ export class AUNClient {
|
|
|
2283
2428
|
return refreshed;
|
|
2284
2429
|
}
|
|
2285
2430
|
catch (exc) {
|
|
2286
|
-
|
|
2431
|
+
this._clientLog.warn(`group ${groupId} initial epoch sync then refresh server epoch failed: ${formatCaughtError(exc)}`);
|
|
2287
2432
|
}
|
|
2288
2433
|
return epochResult;
|
|
2289
2434
|
}
|
|
@@ -2300,7 +2445,7 @@ export class AUNClient {
|
|
|
2300
2445
|
catch (exc) {
|
|
2301
2446
|
if (strict)
|
|
2302
2447
|
throw new StateError(`group ${groupId} failed to query server epoch before retry: ${formatCaughtError(exc)}`);
|
|
2303
|
-
|
|
2448
|
+
this._clientLog.warn(`group ${groupId} epoch precheck failed: ${formatCaughtError(exc)}`);
|
|
2304
2449
|
return;
|
|
2305
2450
|
}
|
|
2306
2451
|
let serverEpoch = Number(epochResult.committed_epoch ?? epochResult.epoch ?? 0);
|
|
@@ -2348,7 +2493,7 @@ export class AUNClient {
|
|
|
2348
2493
|
throw new StateError(`group ${groupId} epoch rotation has not completed`);
|
|
2349
2494
|
}
|
|
2350
2495
|
}
|
|
2351
|
-
|
|
2496
|
+
this._clientLog.warn(`group ${groupId} local epoch=${effectiveLocalEpoch} < server epoch=${serverEpoch}; requesting key recovery`);
|
|
2352
2497
|
await this._recoverGroupEpochKey(groupId, serverEpoch, '', 5000);
|
|
2353
2498
|
const deadline = Date.now() + 5000;
|
|
2354
2499
|
while (Date.now() < deadline) {
|
|
@@ -2372,7 +2517,7 @@ export class AUNClient {
|
|
|
2372
2517
|
members = isJsonObject(membersResult) ? membersResult.members : null;
|
|
2373
2518
|
}
|
|
2374
2519
|
catch (exc) {
|
|
2375
|
-
|
|
2520
|
+
this._clientLog.warn(`group ${groupId} member epoch floor pre-check skip: ${formatCaughtError(exc)}`);
|
|
2376
2521
|
return;
|
|
2377
2522
|
}
|
|
2378
2523
|
let maxMinReadEpoch = 0;
|
|
@@ -2387,7 +2532,7 @@ export class AUNClient {
|
|
|
2387
2532
|
}
|
|
2388
2533
|
if (maxMinReadEpoch <= committedEpoch)
|
|
2389
2534
|
return;
|
|
2390
|
-
|
|
2535
|
+
this._clientLog.warn(`group ${groupId} min_read_epoch above committed epoch, send with committed epoch: committed=${committedEpoch} floor=${maxMinReadEpoch}`);
|
|
2391
2536
|
return;
|
|
2392
2537
|
}
|
|
2393
2538
|
}
|
|
@@ -2398,7 +2543,7 @@ export class AUNClient {
|
|
|
2398
2543
|
return epochResult;
|
|
2399
2544
|
}
|
|
2400
2545
|
catch (exc) {
|
|
2401
|
-
|
|
2546
|
+
this._clientLog.warn(`group ${groupId} query committed epoch state failed, rollback local epoch: ${formatCaughtError(exc)}`);
|
|
2402
2547
|
}
|
|
2403
2548
|
const localEpoch = await this._groupE2ee.currentEpoch(groupId);
|
|
2404
2549
|
return { epoch: localEpoch ?? 0, committed_epoch: localEpoch ?? 0 };
|
|
@@ -2426,7 +2571,7 @@ export class AUNClient {
|
|
|
2426
2571
|
let committedRotation = isJsonObject(epochResult.committed_rotation) ? epochResult.committed_rotation : null;
|
|
2427
2572
|
if (await this._committedRotationMembershipGap(groupId, committedEpoch, committedRotation)) {
|
|
2428
2573
|
const allowMember = await this._groupAllowsMemberEpochRotation(groupId);
|
|
2429
|
-
|
|
2574
|
+
this._clientLog.warn(`group ${groupId} committed epoch ${committedEpoch} member snapshot mismatches current members, trigger rotation fix`);
|
|
2430
2575
|
await this._maybeLeadRotateGroupEpoch(groupId, `${groupId}:committed_membership_gap:aid:${this._aid}:epoch:${committedEpoch}`, committedEpoch, allowMember);
|
|
2431
2576
|
const refreshed = await this._committedGroupEpochState(groupId);
|
|
2432
2577
|
const refreshedCommittedEpoch = Number(refreshed.committed_epoch ?? refreshed.epoch ?? committedEpoch);
|
|
@@ -2443,7 +2588,7 @@ export class AUNClient {
|
|
|
2443
2588
|
return committedEpoch;
|
|
2444
2589
|
}
|
|
2445
2590
|
const pendingRotationId = secretData ? String(secretData.pending_rotation_id ?? '') : '';
|
|
2446
|
-
|
|
2591
|
+
this._clientLog.warn(`group ${groupId} epoch ${committedEpoch} local pending key mismatches server committed rotation, recover key first: local_rotation=${pendingRotationId || '-'}`);
|
|
2447
2592
|
await this._recoverGroupEpochKey(groupId, committedEpoch, '', 5000);
|
|
2448
2593
|
let refreshed = await this._committedGroupEpochState(groupId);
|
|
2449
2594
|
const refreshedCommittedEpoch = Number(refreshed.committed_epoch ?? refreshed.epoch ?? committedEpoch);
|
|
@@ -2488,13 +2633,13 @@ export class AUNClient {
|
|
|
2488
2633
|
if (activeMembers.join('\n') !== expectedMembers.join('\n')) {
|
|
2489
2634
|
const missing = activeMembers.filter((aid) => !expectedMembers.includes(aid));
|
|
2490
2635
|
const extra = expectedMembers.filter((aid) => !activeMembers.includes(aid));
|
|
2491
|
-
|
|
2636
|
+
this._clientLog.info(`group ${groupId} committed membership gap: epoch=${committedEpoch} missing=${JSON.stringify(missing)} extra=${JSON.stringify(extra)}`);
|
|
2492
2637
|
return true;
|
|
2493
2638
|
}
|
|
2494
2639
|
return false;
|
|
2495
2640
|
}
|
|
2496
2641
|
catch (exc) {
|
|
2497
|
-
|
|
2642
|
+
this._clientLog.debug(`query current members failed, cannot determine committed membership gap: group=${groupId} err=${formatCaughtError(exc)}`);
|
|
2498
2643
|
return false;
|
|
2499
2644
|
}
|
|
2500
2645
|
}
|
|
@@ -2514,7 +2659,7 @@ export class AUNClient {
|
|
|
2514
2659
|
if (fromAid) {
|
|
2515
2660
|
const certReady = await this._ensureSenderCertCached(fromAid, senderCertFingerprint || undefined);
|
|
2516
2661
|
if (!certReady) {
|
|
2517
|
-
|
|
2662
|
+
this._clientLog.warn(`cannot fetch sender ${fromAid} cert, skip decrypt`);
|
|
2518
2663
|
throw new Error(`发送方证书不可用: from=${fromAid}, mid=${message.message_id}`);
|
|
2519
2664
|
}
|
|
2520
2665
|
}
|
|
@@ -2528,47 +2673,56 @@ export class AUNClient {
|
|
|
2528
2673
|
}
|
|
2529
2674
|
/** 批量解密 P2P 消息(用于 message.pull) */
|
|
2530
2675
|
async _decryptMessages(messages) {
|
|
2531
|
-
const
|
|
2532
|
-
|
|
2533
|
-
|
|
2534
|
-
const
|
|
2535
|
-
|
|
2536
|
-
|
|
2537
|
-
|
|
2538
|
-
seenInBatch.
|
|
2539
|
-
|
|
2540
|
-
|
|
2541
|
-
|
|
2542
|
-
|
|
2543
|
-
|
|
2544
|
-
|
|
2545
|
-
|
|
2546
|
-
|
|
2547
|
-
|
|
2548
|
-
|
|
2549
|
-
|
|
2550
|
-
const
|
|
2551
|
-
|
|
2552
|
-
|
|
2553
|
-
|
|
2676
|
+
const tStart = Date.now();
|
|
2677
|
+
this._clientLog.debug(`_decryptMessages enter: count=${messages.length}`);
|
|
2678
|
+
try {
|
|
2679
|
+
const seenInBatch = new Set();
|
|
2680
|
+
const result = [];
|
|
2681
|
+
for (const msg of messages) {
|
|
2682
|
+
const mid = (msg.message_id ?? '');
|
|
2683
|
+
if (mid && seenInBatch.has(mid))
|
|
2684
|
+
continue;
|
|
2685
|
+
if (mid)
|
|
2686
|
+
seenInBatch.add(mid);
|
|
2687
|
+
const payload = isJsonObject(msg.payload) ? msg.payload : null;
|
|
2688
|
+
if (payload !== null && await this._tryHandleGroupKeyMessage(msg)) {
|
|
2689
|
+
continue;
|
|
2690
|
+
}
|
|
2691
|
+
if (payload !== null
|
|
2692
|
+
&& payload.type === 'e2ee.encrypted'
|
|
2693
|
+
&& (msg.encrypted === true || !('encrypted' in msg))) {
|
|
2694
|
+
try {
|
|
2695
|
+
const fromAid = (msg.from ?? '');
|
|
2696
|
+
const senderCertFingerprint = String(payload.sender_cert_fingerprint ?? payload.aad?.sender_cert_fingerprint ?? '').trim().toLowerCase();
|
|
2697
|
+
if (fromAid) {
|
|
2698
|
+
const certReady = await this._ensureSenderCertCached(fromAid, senderCertFingerprint || undefined);
|
|
2699
|
+
if (!certReady) {
|
|
2700
|
+
this._clientLog.warn(`cannot fetch sender %s cert, skip decrypt${String(fromAid)}`);
|
|
2701
|
+
continue;
|
|
2702
|
+
}
|
|
2703
|
+
}
|
|
2704
|
+
// Pull 场景:跳过防重放和 timestamp 窗口检查(push 已处理过的消息仍需要能解密)
|
|
2705
|
+
const decrypted = await this._e2ee.decryptMessage(msg, { skipReplay: true });
|
|
2706
|
+
if (decrypted !== null) {
|
|
2707
|
+
result.push(decrypted);
|
|
2554
2708
|
}
|
|
2555
2709
|
}
|
|
2556
|
-
|
|
2557
|
-
|
|
2558
|
-
|
|
2559
|
-
result.push(decrypted);
|
|
2710
|
+
catch (decryptExc) {
|
|
2711
|
+
this._clientLog.warn(`pull messagedecryptfailed, skip: from=${String(msg.from ?? '')} mid=${mid} err=${decryptExc instanceof Error ? decryptExc.message : String(decryptExc)}`);
|
|
2712
|
+
continue;
|
|
2560
2713
|
}
|
|
2561
2714
|
}
|
|
2562
|
-
|
|
2563
|
-
|
|
2564
|
-
continue;
|
|
2715
|
+
else {
|
|
2716
|
+
result.push(msg);
|
|
2565
2717
|
}
|
|
2566
2718
|
}
|
|
2567
|
-
|
|
2568
|
-
|
|
2569
|
-
|
|
2719
|
+
this._clientLog.debug(`_decryptMessages exit: elapsed=${Date.now() - tStart}ms in=${messages.length} out=${result.length}`);
|
|
2720
|
+
return result;
|
|
2721
|
+
}
|
|
2722
|
+
catch (err) {
|
|
2723
|
+
this._clientLog.debug(`_decryptMessages exit (error): elapsed=${Date.now() - tStart}ms err=${err instanceof Error ? err.message : String(err)}`);
|
|
2724
|
+
throw err;
|
|
2570
2725
|
}
|
|
2571
|
-
return result;
|
|
2572
2726
|
}
|
|
2573
2727
|
/** 解密单条群组消息。opts.skipReplay 用于 pull 场景跳过防重放。 */
|
|
2574
2728
|
_enqueuePendingDecrypt(groupId, msg) {
|
|
@@ -2617,20 +2771,34 @@ export class AUNClient {
|
|
|
2617
2771
|
this._safeAsync(this._retryPendingDecryptMsgs(groupId));
|
|
2618
2772
|
}
|
|
2619
2773
|
async _recoverGroupEpochKey(groupId, epoch, senderAid = '', timeoutMs = 5000) {
|
|
2620
|
-
const
|
|
2621
|
-
|
|
2622
|
-
|
|
2623
|
-
|
|
2774
|
+
const tStart = Date.now();
|
|
2775
|
+
this._clientLog.debug(`_recoverGroupEpochKey enter: group_id=${groupId} epoch=${epoch} sender=${senderAid} timeout=${timeoutMs}`);
|
|
2776
|
+
try {
|
|
2777
|
+
const existing = await this._groupE2ee.loadSecret(groupId, epoch);
|
|
2778
|
+
if (await this._groupEpochSecretReadyForRecovery(groupId, epoch, existing)) {
|
|
2779
|
+
this._scheduleRetryPendingDecryptMsgs(groupId);
|
|
2780
|
+
this._clientLog.debug(`_recoverGroupEpochKey exit: elapsed=${Date.now() - tStart}ms group_id=${groupId} epoch=${epoch} result=already_ready`);
|
|
2781
|
+
return true;
|
|
2782
|
+
}
|
|
2783
|
+
// inflight 去重:同 groupId:epoch 的并发恢复共享同一个 Promise
|
|
2784
|
+
const key = `${groupId}:${epoch}`;
|
|
2785
|
+
const inflight = this._groupEpochRecoveryInflight.get(key);
|
|
2786
|
+
if (inflight) {
|
|
2787
|
+
const r = await inflight;
|
|
2788
|
+
this._clientLog.debug(`_recoverGroupEpochKey exit: elapsed=${Date.now() - tStart}ms group_id=${groupId} epoch=${epoch} result=${r ? 'ok' : 'failed'} source=inflight`);
|
|
2789
|
+
return r;
|
|
2790
|
+
}
|
|
2791
|
+
const promise = this._doRecoverGroupEpochKey(groupId, epoch, senderAid, timeoutMs)
|
|
2792
|
+
.finally(() => this._groupEpochRecoveryInflight.delete(key));
|
|
2793
|
+
this._groupEpochRecoveryInflight.set(key, promise);
|
|
2794
|
+
const r = await promise;
|
|
2795
|
+
this._clientLog.debug(`_recoverGroupEpochKey exit: elapsed=${Date.now() - tStart}ms group_id=${groupId} epoch=${epoch} result=${r ? 'ok' : 'failed'}`);
|
|
2796
|
+
return r;
|
|
2797
|
+
}
|
|
2798
|
+
catch (err) {
|
|
2799
|
+
this._clientLog.debug(`_recoverGroupEpochKey exit (error): elapsed=${Date.now() - tStart}ms group_id=${groupId} epoch=${epoch} err=${err instanceof Error ? err.message : String(err)}`);
|
|
2800
|
+
throw err;
|
|
2624
2801
|
}
|
|
2625
|
-
// inflight 去重:同 groupId:epoch 的并发恢复共享同一个 Promise
|
|
2626
|
-
const key = `${groupId}:${epoch}`;
|
|
2627
|
-
const inflight = this._groupEpochRecoveryInflight.get(key);
|
|
2628
|
-
if (inflight)
|
|
2629
|
-
return inflight;
|
|
2630
|
-
const promise = this._doRecoverGroupEpochKey(groupId, epoch, senderAid, timeoutMs)
|
|
2631
|
-
.finally(() => this._groupEpochRecoveryInflight.delete(key));
|
|
2632
|
-
this._groupEpochRecoveryInflight.set(key, promise);
|
|
2633
|
-
return promise;
|
|
2634
2802
|
}
|
|
2635
2803
|
static _extractGroupJoinMode(payload) {
|
|
2636
2804
|
if (!isJsonObject(payload))
|
|
@@ -2976,46 +3144,66 @@ export class AUNClient {
|
|
|
2976
3144
|
}
|
|
2977
3145
|
}
|
|
2978
3146
|
async _decryptGroupMessage(message, opts) {
|
|
2979
|
-
const
|
|
2980
|
-
|
|
2981
|
-
|
|
2982
|
-
}
|
|
2983
|
-
|
|
2984
|
-
|
|
2985
|
-
|
|
2986
|
-
|
|
2987
|
-
|
|
2988
|
-
|
|
2989
|
-
|
|
2990
|
-
|
|
2991
|
-
|
|
2992
|
-
|
|
2993
|
-
|
|
2994
|
-
|
|
2995
|
-
|
|
2996
|
-
|
|
2997
|
-
|
|
2998
|
-
// 不是解密失败,不应触发 recover
|
|
2999
|
-
if (result !== null) {
|
|
3000
|
-
return result;
|
|
3001
|
-
}
|
|
3002
|
-
// 真正的解密失败(result === null),尝试密钥恢复后重试
|
|
3003
|
-
const groupId = String(message.group_id ?? '');
|
|
3004
|
-
const sender = String(message.from ?? message.sender_aid ?? '');
|
|
3005
|
-
const epoch = Number(payload.epoch ?? 0);
|
|
3006
|
-
if (epoch > 0 && groupId) {
|
|
3007
|
-
try {
|
|
3008
|
-
if (await this._recoverGroupEpochKey(groupId, epoch, sender, 5000)) {
|
|
3009
|
-
const retry = await this._groupE2ee.decrypt(message, opts);
|
|
3010
|
-
if (retry !== null && retry.e2ee)
|
|
3011
|
-
return this._attachGroupDispatchModeToPayload(retry);
|
|
3147
|
+
const tStart = Date.now();
|
|
3148
|
+
const groupIdInit = String(message.group_id ?? '');
|
|
3149
|
+
const midInit = String(message.message_id ?? '');
|
|
3150
|
+
this._clientLog.debug(`_decryptGroupMessage enter: group_id=${groupIdInit} mid=${midInit} skip_replay=${!!opts?.skipReplay}`);
|
|
3151
|
+
try {
|
|
3152
|
+
const payload = isJsonObject(message.payload) ? message.payload : null;
|
|
3153
|
+
if (payload === null || payload.type !== 'e2ee.group_encrypted') {
|
|
3154
|
+
const r = this._attachGroupDispatchModeToPayload(message);
|
|
3155
|
+
this._clientLog.debug(`_decryptGroupMessage exit: elapsed=${Date.now() - tStart}ms result=passthrough_not_encrypted`);
|
|
3156
|
+
return r;
|
|
3157
|
+
}
|
|
3158
|
+
// 确保发送方证书已缓存(签名验证需要)
|
|
3159
|
+
const senderAid = String(message.from ?? message.sender_aid ?? '');
|
|
3160
|
+
if (senderAid) {
|
|
3161
|
+
const certOk = await this._ensureSenderCertCached(senderAid);
|
|
3162
|
+
if (!certOk) {
|
|
3163
|
+
this._clientLog.warn(`group message decrypt skip: sender ${senderAid} cert unavailable`);
|
|
3164
|
+
this._clientLog.debug(`_decryptGroupMessage exit: elapsed=${Date.now() - tStart}ms result=skip_no_sender_cert`);
|
|
3165
|
+
return message;
|
|
3012
3166
|
}
|
|
3013
3167
|
}
|
|
3014
|
-
|
|
3015
|
-
|
|
3168
|
+
// 先尝试直接解密
|
|
3169
|
+
const result = await this._groupE2ee.decrypt(message, opts);
|
|
3170
|
+
if (result !== null && isJsonObject(result.e2ee)) {
|
|
3171
|
+
const r = this._attachGroupDispatchModeToPayload(result);
|
|
3172
|
+
this._clientLog.debug(`_decryptGroupMessage exit: elapsed=${Date.now() - tStart}ms result=ok group_id=${groupIdInit}`);
|
|
3173
|
+
return r;
|
|
3174
|
+
}
|
|
3175
|
+
// replay guard 命中:decrypt 返回了原消息(非 null)但无 e2ee 字段
|
|
3176
|
+
// 不是解密失败,不应触发 recover
|
|
3177
|
+
if (result !== null) {
|
|
3178
|
+
this._clientLog.debug(`_decryptGroupMessage exit: elapsed=${Date.now() - tStart}ms result=replay_skipped`);
|
|
3179
|
+
return result;
|
|
3180
|
+
}
|
|
3181
|
+
// 真正的解密失败(result === null),尝试密钥恢复后重试
|
|
3182
|
+
const groupId = String(message.group_id ?? '');
|
|
3183
|
+
const sender = String(message.from ?? message.sender_aid ?? '');
|
|
3184
|
+
const epoch = Number(payload.epoch ?? 0);
|
|
3185
|
+
if (epoch > 0 && groupId) {
|
|
3186
|
+
try {
|
|
3187
|
+
if (await this._recoverGroupEpochKey(groupId, epoch, sender, 5000)) {
|
|
3188
|
+
const retry = await this._groupE2ee.decrypt(message, opts);
|
|
3189
|
+
if (retry !== null && retry.e2ee) {
|
|
3190
|
+
const r = this._attachGroupDispatchModeToPayload(retry);
|
|
3191
|
+
this._clientLog.debug(`_decryptGroupMessage exit: elapsed=${Date.now() - tStart}ms result=ok_after_recover`);
|
|
3192
|
+
return r;
|
|
3193
|
+
}
|
|
3194
|
+
}
|
|
3195
|
+
}
|
|
3196
|
+
catch (exc) {
|
|
3197
|
+
this._clientLog.debug(`group ${groupId} epoch ${epoch} sync recover failed: ${formatCaughtError(exc)}`);
|
|
3198
|
+
}
|
|
3016
3199
|
}
|
|
3200
|
+
this._clientLog.debug(`_decryptGroupMessage exit: elapsed=${Date.now() - tStart}ms result=undecryptable group_id=${groupIdInit}`);
|
|
3201
|
+
return message;
|
|
3202
|
+
}
|
|
3203
|
+
catch (err) {
|
|
3204
|
+
this._clientLog.debug(`_decryptGroupMessage exit (error): elapsed=${Date.now() - tStart}ms err=${err instanceof Error ? err.message : String(err)}`);
|
|
3205
|
+
throw err;
|
|
3017
3206
|
}
|
|
3018
|
-
return message;
|
|
3019
3207
|
}
|
|
3020
3208
|
_attachGroupDispatchModeToPayload(message) {
|
|
3021
3209
|
const payload = message.payload;
|
|
@@ -3132,14 +3320,14 @@ export class AUNClient {
|
|
|
3132
3320
|
if (fromAid) {
|
|
3133
3321
|
const certReady = await this._ensureSenderCertCached(fromAid, senderCertFingerprint || undefined);
|
|
3134
3322
|
if (!certReady) {
|
|
3135
|
-
|
|
3323
|
+
this._clientLog.warn('p2p.thought.decrypt failed: cannot fetch sendercert thought_id=' + thoughtId + ' from=' + fromAid);
|
|
3136
3324
|
decryptFailed = true;
|
|
3137
3325
|
}
|
|
3138
3326
|
}
|
|
3139
3327
|
if (!decryptFailed) {
|
|
3140
3328
|
decrypted = await this._e2ee.decryptMessage(message, { skipReplay: true });
|
|
3141
3329
|
if (decrypted === null || (isJsonObject(decrypted.payload) && decrypted.payload.type === 'e2ee.encrypted')) {
|
|
3142
|
-
|
|
3330
|
+
this._clientLog.warn('p2p.thought.decrypt failed thought_id=' + thoughtId);
|
|
3143
3331
|
decryptFailed = true;
|
|
3144
3332
|
decrypted = message;
|
|
3145
3333
|
}
|
|
@@ -3188,7 +3376,7 @@ export class AUNClient {
|
|
|
3188
3376
|
decrypted = await this._e2ee.decryptMessage(message, { skipReplay: true });
|
|
3189
3377
|
}
|
|
3190
3378
|
catch (exc) {
|
|
3191
|
-
|
|
3379
|
+
this._clientLog.warn(`e2ee.encrypted outer decrypt exception, fallback to normal path:${String(exc)}`);
|
|
3192
3380
|
return false;
|
|
3193
3381
|
}
|
|
3194
3382
|
if (!decrypted) {
|
|
@@ -3227,7 +3415,7 @@ export class AUNClient {
|
|
|
3227
3415
|
}
|
|
3228
3416
|
}
|
|
3229
3417
|
catch (exc) {
|
|
3230
|
-
|
|
3418
|
+
this._clientLog.warn(`group key message handle exception:${String(exc)}`);
|
|
3231
3419
|
// S14: 控制面消息处理异常也要抑制业务分发
|
|
3232
3420
|
if (isGroupKeyCtrl)
|
|
3233
3421
|
return true;
|
|
@@ -3255,7 +3443,7 @@ export class AUNClient {
|
|
|
3255
3443
|
members = memberList.map(m => String(m.aid ?? ''));
|
|
3256
3444
|
}
|
|
3257
3445
|
catch (exc) {
|
|
3258
|
-
|
|
3446
|
+
this._clientLog.warn(`group ${groupId} member list pull-back failed: ${String(exc)}`);
|
|
3259
3447
|
}
|
|
3260
3448
|
}
|
|
3261
3449
|
const response = await this._groupE2ee.handleKeyRequestMsg(actualPayload, members);
|
|
@@ -3269,7 +3457,7 @@ export class AUNClient {
|
|
|
3269
3457
|
});
|
|
3270
3458
|
}
|
|
3271
3459
|
catch (exc) {
|
|
3272
|
-
|
|
3460
|
+
this._clientLog.warn(`reply group key to ${requester} failed: ${String(exc)}`);
|
|
3273
3461
|
}
|
|
3274
3462
|
}
|
|
3275
3463
|
}
|
|
@@ -3291,144 +3479,170 @@ export class AUNClient {
|
|
|
3291
3479
|
* 跨域时自动将请求路由到 peer 所在域的 Gateway。
|
|
3292
3480
|
*/
|
|
3293
3481
|
async _fetchPeerCert(aid, certFingerprint) {
|
|
3294
|
-
const
|
|
3295
|
-
|
|
3296
|
-
const now = Date.now() / 1000;
|
|
3297
|
-
if (cached && now < cached.refreshAfter) {
|
|
3298
|
-
return cached.certPem;
|
|
3299
|
-
}
|
|
3300
|
-
const gatewayUrl = this._gatewayUrl;
|
|
3301
|
-
if (!gatewayUrl) {
|
|
3302
|
-
throw new ValidationError('gateway url unavailable for e2ee cert fetch');
|
|
3303
|
-
}
|
|
3304
|
-
// 跨域时用 peer 所在域的 Gateway URL
|
|
3305
|
-
const peerGatewayUrl = resolvePeerGatewayUrl(gatewayUrl, aid);
|
|
3306
|
-
let certPem;
|
|
3482
|
+
const tStart = Date.now();
|
|
3483
|
+
this._clientLog.debug(`_fetchPeerCert enter: aid=${aid} fingerprint=${certFingerprint ?? '<none>'}`);
|
|
3307
3484
|
try {
|
|
3308
|
-
const
|
|
3309
|
-
|
|
3310
|
-
const
|
|
3311
|
-
|
|
3485
|
+
const cacheKey = certCacheKey(aid, certFingerprint);
|
|
3486
|
+
const cached = this._certCache.get(cacheKey);
|
|
3487
|
+
const now = Date.now() / 1000;
|
|
3488
|
+
if (cached && now < cached.refreshAfter) {
|
|
3489
|
+
this._clientLog.debug(`_fetchPeerCert exit: elapsed=${Date.now() - tStart}ms aid=${aid} source=cache`);
|
|
3490
|
+
return cached.certPem;
|
|
3491
|
+
}
|
|
3492
|
+
const gatewayUrl = this._gatewayUrl;
|
|
3493
|
+
if (!gatewayUrl) {
|
|
3494
|
+
throw new ValidationError('gateway url unavailable for e2ee cert fetch');
|
|
3495
|
+
}
|
|
3496
|
+
// 跨域时用 peer 所在域的 Gateway URL
|
|
3497
|
+
const peerGatewayUrl = resolvePeerGatewayUrl(gatewayUrl, aid);
|
|
3498
|
+
let certPem;
|
|
3312
3499
|
try {
|
|
3313
|
-
const
|
|
3314
|
-
|
|
3315
|
-
|
|
3316
|
-
|
|
3500
|
+
const certUrl = buildCertUrl(peerGatewayUrl, aid, certFingerprint);
|
|
3501
|
+
// 兼容旧浏览器,不使用 AbortSignal.timeout(Chrome 103+ 才支持)
|
|
3502
|
+
const controller = new AbortController();
|
|
3503
|
+
const timeoutId = setTimeout(() => controller.abort(), 5000);
|
|
3504
|
+
try {
|
|
3505
|
+
const resp = await fetch(certUrl, { signal: controller.signal });
|
|
3506
|
+
if (!resp.ok)
|
|
3507
|
+
throw new ValidationError(`failed to fetch peer cert for ${aid}: HTTP ${resp.status}`);
|
|
3508
|
+
certPem = await resp.text();
|
|
3509
|
+
}
|
|
3510
|
+
finally {
|
|
3511
|
+
clearTimeout(timeoutId);
|
|
3512
|
+
}
|
|
3317
3513
|
}
|
|
3318
|
-
|
|
3319
|
-
|
|
3514
|
+
catch (exc) {
|
|
3515
|
+
if (!certFingerprint) {
|
|
3516
|
+
throw exc;
|
|
3517
|
+
}
|
|
3518
|
+
// 兼容旧浏览器,不使用 AbortSignal.timeout(Chrome 103+ 才支持)
|
|
3519
|
+
const fallbackController = new AbortController();
|
|
3520
|
+
const fallbackTimeoutId = setTimeout(() => fallbackController.abort(), 5000);
|
|
3521
|
+
try {
|
|
3522
|
+
const fallbackResp = await fetch(buildCertUrl(peerGatewayUrl, aid), { signal: fallbackController.signal });
|
|
3523
|
+
if (!fallbackResp.ok) {
|
|
3524
|
+
throw exc;
|
|
3525
|
+
}
|
|
3526
|
+
certPem = await fallbackResp.text();
|
|
3527
|
+
}
|
|
3528
|
+
finally {
|
|
3529
|
+
clearTimeout(fallbackTimeoutId);
|
|
3530
|
+
}
|
|
3320
3531
|
}
|
|
3321
|
-
|
|
3322
|
-
|
|
3323
|
-
|
|
3324
|
-
|
|
3532
|
+
// H7: 严格校验指纹(DER SHA-256 或 SPKI SHA-256 任一匹配即可)
|
|
3533
|
+
if (certFingerprint) {
|
|
3534
|
+
const expectedFP = String(certFingerprint).trim().toLowerCase();
|
|
3535
|
+
if (!expectedFP.startsWith('sha256:')) {
|
|
3536
|
+
throw new ValidationError(`unsupported cert_fingerprint format for ${aid}: ${expectedFP.slice(0, 24)}`);
|
|
3537
|
+
}
|
|
3538
|
+
const derFP = await this._certFingerprint(certPem);
|
|
3539
|
+
if (derFP !== expectedFP) {
|
|
3540
|
+
const spkiFP = await this._spkiFingerprint(certPem);
|
|
3541
|
+
if (!spkiFP || spkiFP !== expectedFP) {
|
|
3542
|
+
throw new ValidationError(`peer cert fingerprint mismatch for ${aid}: expected=${expectedFP.slice(0, 24)}...`);
|
|
3543
|
+
}
|
|
3544
|
+
}
|
|
3325
3545
|
}
|
|
3326
|
-
//
|
|
3327
|
-
const fallbackController = new AbortController();
|
|
3328
|
-
const fallbackTimeoutId = setTimeout(() => fallbackController.abort(), 5000);
|
|
3546
|
+
// 完整 PKI 验证:链 + CRL + OCSP + AID 绑定
|
|
3329
3547
|
try {
|
|
3330
|
-
|
|
3331
|
-
if (!fallbackResp.ok) {
|
|
3332
|
-
throw exc;
|
|
3333
|
-
}
|
|
3334
|
-
certPem = await fallbackResp.text();
|
|
3548
|
+
await this._auth.verifyPeerCertificate(peerGatewayUrl, certPem, aid);
|
|
3335
3549
|
}
|
|
3336
|
-
|
|
3337
|
-
|
|
3550
|
+
catch (exc) {
|
|
3551
|
+
throw new ValidationError(`peer cert verification failed for ${aid}: ${exc}`);
|
|
3338
3552
|
}
|
|
3339
|
-
|
|
3340
|
-
|
|
3341
|
-
|
|
3342
|
-
|
|
3343
|
-
|
|
3344
|
-
|
|
3553
|
+
this._certCache.set(cacheKey, {
|
|
3554
|
+
certPem,
|
|
3555
|
+
validatedAt: now,
|
|
3556
|
+
refreshAfter: now + PEER_CERT_CACHE_TTL,
|
|
3557
|
+
});
|
|
3558
|
+
try {
|
|
3559
|
+
// peer 证书只存版本目录,不覆盖 cert.pem
|
|
3560
|
+
await this._keystore.saveCert(aid, certPem, certFingerprint, { makeActive: false });
|
|
3345
3561
|
}
|
|
3346
|
-
|
|
3347
|
-
|
|
3348
|
-
const spkiFP = await this._spkiFingerprint(certPem);
|
|
3349
|
-
if (!spkiFP || spkiFP !== expectedFP) {
|
|
3350
|
-
throw new ValidationError(`peer cert fingerprint mismatch for ${aid}: expected=${expectedFP.slice(0, 24)}...`);
|
|
3351
|
-
}
|
|
3562
|
+
catch (exc) {
|
|
3563
|
+
this._clientLog.error(`write cert to keystore failed (aid=${aid}): ${String(exc)}`, exc instanceof Error ? exc : undefined);
|
|
3352
3564
|
}
|
|
3565
|
+
this._clientLog.debug(`_fetchPeerCert exit: elapsed=${Date.now() - tStart}ms aid=${aid} source=fetched`);
|
|
3566
|
+
return certPem;
|
|
3353
3567
|
}
|
|
3354
|
-
|
|
3355
|
-
|
|
3356
|
-
|
|
3357
|
-
}
|
|
3358
|
-
catch (exc) {
|
|
3359
|
-
throw new ValidationError(`peer cert verification failed for ${aid}: ${exc}`);
|
|
3360
|
-
}
|
|
3361
|
-
this._certCache.set(cacheKey, {
|
|
3362
|
-
certPem,
|
|
3363
|
-
validatedAt: now,
|
|
3364
|
-
refreshAfter: now + PEER_CERT_CACHE_TTL,
|
|
3365
|
-
});
|
|
3366
|
-
try {
|
|
3367
|
-
// peer 证书只存版本目录,不覆盖 cert.pem
|
|
3368
|
-
await this._keystore.saveCert(aid, certPem, certFingerprint, { makeActive: false });
|
|
3369
|
-
}
|
|
3370
|
-
catch (exc) {
|
|
3371
|
-
console.error(`写入证书到 keystore 失败 (aid=${aid}):`, exc);
|
|
3568
|
+
catch (err) {
|
|
3569
|
+
this._clientLog.debug(`_fetchPeerCert exit (error): elapsed=${Date.now() - tStart}ms aid=${aid} err=${err instanceof Error ? err.message : String(err)}`);
|
|
3570
|
+
throw err;
|
|
3372
3571
|
}
|
|
3373
|
-
return certPem;
|
|
3374
3572
|
}
|
|
3375
3573
|
/** 获取对方所有设备的 prekey(带缓存)。 */
|
|
3376
3574
|
async _fetchPeerPrekeys(peerAid) {
|
|
3377
|
-
const
|
|
3378
|
-
|
|
3379
|
-
const normalized = normalizePeerPrekeys(cachedList.items);
|
|
3380
|
-
if (normalized.length > 0)
|
|
3381
|
-
return normalized.map((item) => ({ ...item }));
|
|
3382
|
-
}
|
|
3383
|
-
const cached = this._e2ee.getCachedPrekey(peerAid);
|
|
3384
|
-
if (cached !== null) {
|
|
3385
|
-
const normalized = normalizePeerPrekeys([cached]);
|
|
3386
|
-
if (normalized.length > 0)
|
|
3387
|
-
return normalized.map((item) => ({ ...item }));
|
|
3388
|
-
}
|
|
3389
|
-
let result;
|
|
3575
|
+
const tStart = Date.now();
|
|
3576
|
+
this._clientLog.debug(`_fetchPeerPrekeys enter: peer_aid=${peerAid}`);
|
|
3390
3577
|
try {
|
|
3391
|
-
|
|
3392
|
-
|
|
3393
|
-
|
|
3394
|
-
|
|
3395
|
-
|
|
3396
|
-
|
|
3397
|
-
|
|
3398
|
-
}
|
|
3399
|
-
if (result.found === false) {
|
|
3400
|
-
return [];
|
|
3401
|
-
}
|
|
3402
|
-
const devicePrekeys = Array.isArray(result.device_prekeys) ? result.device_prekeys : null;
|
|
3403
|
-
if (devicePrekeys) {
|
|
3404
|
-
const normalized = normalizePeerPrekeys(devicePrekeys);
|
|
3405
|
-
if (normalized.length > 0) {
|
|
3406
|
-
this._peerPrekeysCache.set(peerAid, {
|
|
3407
|
-
items: normalized.map((item) => ({ ...item })),
|
|
3408
|
-
expireAt: Date.now() / 1000 + PEER_PREKEYS_CACHE_TTL,
|
|
3409
|
-
});
|
|
3410
|
-
this._e2ee.cachePrekey(peerAid, normalized[0]);
|
|
3411
|
-
return normalized;
|
|
3578
|
+
const cachedList = this._peerPrekeysCache.get(peerAid);
|
|
3579
|
+
if (cachedList && Date.now() / 1000 < cachedList.expireAt) {
|
|
3580
|
+
const normalized = normalizePeerPrekeys(cachedList.items);
|
|
3581
|
+
if (normalized.length > 0) {
|
|
3582
|
+
this._clientLog.debug(`_fetchPeerPrekeys exit: elapsed=${Date.now() - tStart}ms peer_aid=${peerAid} count=${normalized.length} source=list_cache`);
|
|
3583
|
+
return normalized.map((item) => ({ ...item }));
|
|
3584
|
+
}
|
|
3412
3585
|
}
|
|
3413
|
-
|
|
3414
|
-
|
|
3415
|
-
|
|
3416
|
-
|
|
3417
|
-
|
|
3418
|
-
|
|
3419
|
-
|
|
3420
|
-
|
|
3421
|
-
|
|
3422
|
-
|
|
3423
|
-
});
|
|
3424
|
-
|
|
3425
|
-
|
|
3586
|
+
const cached = this._e2ee.getCachedPrekey(peerAid);
|
|
3587
|
+
if (cached !== null) {
|
|
3588
|
+
const normalized = normalizePeerPrekeys([cached]);
|
|
3589
|
+
if (normalized.length > 0) {
|
|
3590
|
+
this._clientLog.debug(`_fetchPeerPrekeys exit: elapsed=${Date.now() - tStart}ms peer_aid=${peerAid} count=${normalized.length} source=e2ee_cache`);
|
|
3591
|
+
return normalized.map((item) => ({ ...item }));
|
|
3592
|
+
}
|
|
3593
|
+
}
|
|
3594
|
+
let result;
|
|
3595
|
+
try {
|
|
3596
|
+
result = await this._transport.call('message.e2ee.get_prekey', { aid: peerAid });
|
|
3597
|
+
}
|
|
3598
|
+
catch (exc) {
|
|
3599
|
+
throw new ValidationError(`failed to fetch peer prekey for ${peerAid}: ${String(exc)}`);
|
|
3600
|
+
}
|
|
3601
|
+
if (!isJsonObject(result)) {
|
|
3602
|
+
throw new ValidationError(`invalid prekey response for ${peerAid}`);
|
|
3603
|
+
}
|
|
3604
|
+
if (result.found === false) {
|
|
3605
|
+
this._clientLog.debug(`_fetchPeerPrekeys exit: elapsed=${Date.now() - tStart}ms peer_aid=${peerAid} count=0 reason=not_found`);
|
|
3606
|
+
return [];
|
|
3607
|
+
}
|
|
3608
|
+
const devicePrekeys = Array.isArray(result.device_prekeys) ? result.device_prekeys : null;
|
|
3609
|
+
if (devicePrekeys) {
|
|
3610
|
+
const normalized = normalizePeerPrekeys(devicePrekeys);
|
|
3611
|
+
if (normalized.length > 0) {
|
|
3612
|
+
this._peerPrekeysCache.set(peerAid, {
|
|
3613
|
+
items: normalized.map((item) => ({ ...item })),
|
|
3614
|
+
expireAt: Date.now() / 1000 + PEER_PREKEYS_CACHE_TTL,
|
|
3615
|
+
});
|
|
3616
|
+
this._e2ee.cachePrekey(peerAid, normalized[0]);
|
|
3617
|
+
this._clientLog.debug(`_fetchPeerPrekeys exit: elapsed=${Date.now() - tStart}ms peer_aid=${peerAid} count=${normalized.length} source=device_prekeys`);
|
|
3618
|
+
return normalized;
|
|
3619
|
+
}
|
|
3620
|
+
}
|
|
3621
|
+
if (!isPeerPrekeyResponse(result)) {
|
|
3622
|
+
throw new ValidationError(`invalid prekey response for ${peerAid}`);
|
|
3426
3623
|
}
|
|
3624
|
+
if (result.prekey) {
|
|
3625
|
+
const normalized = normalizePeerPrekeys([result.prekey]);
|
|
3626
|
+
if (normalized.length > 0) {
|
|
3627
|
+
this._peerPrekeysCache.set(peerAid, {
|
|
3628
|
+
items: normalized.map((item) => ({ ...item })),
|
|
3629
|
+
expireAt: Date.now() / 1000 + PEER_PREKEYS_CACHE_TTL,
|
|
3630
|
+
});
|
|
3631
|
+
this._e2ee.cachePrekey(peerAid, normalized[0]);
|
|
3632
|
+
this._clientLog.debug(`_fetchPeerPrekeys exit: elapsed=${Date.now() - tStart}ms peer_aid=${peerAid} count=${normalized.length} source=single_prekey`);
|
|
3633
|
+
return normalized.map((item) => ({ ...item }));
|
|
3634
|
+
}
|
|
3635
|
+
}
|
|
3636
|
+
if (result.found) {
|
|
3637
|
+
throw new ValidationError(`invalid prekey response for ${peerAid}`);
|
|
3638
|
+
}
|
|
3639
|
+
this._clientLog.debug(`_fetchPeerPrekeys exit: elapsed=${Date.now() - tStart}ms peer_aid=${peerAid} count=0`);
|
|
3640
|
+
return [];
|
|
3427
3641
|
}
|
|
3428
|
-
|
|
3429
|
-
|
|
3642
|
+
catch (err) {
|
|
3643
|
+
this._clientLog.debug(`_fetchPeerPrekeys exit (error): elapsed=${Date.now() - tStart}ms peer_aid=${peerAid} err=${err instanceof Error ? err.message : String(err)}`);
|
|
3644
|
+
throw err;
|
|
3430
3645
|
}
|
|
3431
|
-
return [];
|
|
3432
3646
|
}
|
|
3433
3647
|
/** 清除对端 prekey 的双层缓存(_peerPrekeysCache + e2ee 内部缓存) */
|
|
3434
3648
|
_invalidatePeerPrekeyCache(peerAid) {
|
|
@@ -3507,10 +3721,10 @@ export class AUNClient {
|
|
|
3507
3721
|
catch (exc) {
|
|
3508
3722
|
// 刷新失败时:若缓存有 PKI 验证过的证书(2 倍 TTL 内)则继续用
|
|
3509
3723
|
if (cached && now < cached.validatedAt + PEER_CERT_CACHE_TTL * 2) {
|
|
3510
|
-
|
|
3724
|
+
this._clientLog.warn(`refresh sender ${aid} cert failed, continue using verified memory cache: ${String(exc)}`);
|
|
3511
3725
|
return true;
|
|
3512
3726
|
}
|
|
3513
|
-
|
|
3727
|
+
this._clientLog.warn(`fetch sender ${aid} cert failed and no verify cache, reject trust: ${String(exc)}`);
|
|
3514
3728
|
return false;
|
|
3515
3729
|
}
|
|
3516
3730
|
}
|
|
@@ -3638,7 +3852,7 @@ export class AUNClient {
|
|
|
3638
3852
|
}
|
|
3639
3853
|
else {
|
|
3640
3854
|
failed.push(String(dist.to));
|
|
3641
|
-
|
|
3855
|
+
this._clientLog.warn(`epoch keydistributefailed (to=%s):${dist.to} ${exc}`);
|
|
3642
3856
|
}
|
|
3643
3857
|
}
|
|
3644
3858
|
}
|
|
@@ -3656,7 +3870,7 @@ export class AUNClient {
|
|
|
3656
3870
|
return isJsonObject(result) && result.success === true;
|
|
3657
3871
|
}
|
|
3658
3872
|
catch (exc) {
|
|
3659
|
-
|
|
3873
|
+
this._clientLog.warn(`refresh epoch rotation lease failed: rotation=${rotationId} err=${formatCaughtError(exc)}`);
|
|
3660
3874
|
return false;
|
|
3661
3875
|
}
|
|
3662
3876
|
}
|
|
@@ -3672,7 +3886,7 @@ export class AUNClient {
|
|
|
3672
3886
|
return isJsonObject(result) && result.success === true;
|
|
3673
3887
|
}
|
|
3674
3888
|
catch (exc) {
|
|
3675
|
-
|
|
3889
|
+
this._clientLog.warn(`commit epoch key ack failed: rotation=${rotationId} err=${formatCaughtError(exc)}`);
|
|
3676
3890
|
return false;
|
|
3677
3891
|
}
|
|
3678
3892
|
}
|
|
@@ -3700,7 +3914,7 @@ export class AUNClient {
|
|
|
3700
3914
|
? committedRotation.expected_members.map((item) => String(item ?? '').trim()).filter(Boolean)
|
|
3701
3915
|
: [];
|
|
3702
3916
|
if (this._aid && !expectedMembers.includes(this._aid)) {
|
|
3703
|
-
|
|
3917
|
+
this._clientLog.debug(`allow group key distribute: new member recover commitment mismatch is normal group=${groupId} epoch=${epoch}`);
|
|
3704
3918
|
}
|
|
3705
3919
|
else {
|
|
3706
3920
|
return false;
|
|
@@ -3709,7 +3923,7 @@ export class AUNClient {
|
|
|
3709
3923
|
}
|
|
3710
3924
|
return true;
|
|
3711
3925
|
}
|
|
3712
|
-
|
|
3926
|
+
this._clientLog.info(`reject missing rotation_id future epoch key distribute: group=${groupId} epoch=${epoch} committed=${committedEpoch}`);
|
|
3713
3927
|
return false;
|
|
3714
3928
|
}
|
|
3715
3929
|
const pending = isJsonObject(epochResult.pending_rotation) ? epochResult.pending_rotation : null;
|
|
@@ -3730,10 +3944,10 @@ export class AUNClient {
|
|
|
3730
3944
|
}
|
|
3731
3945
|
}
|
|
3732
3946
|
catch (exc) {
|
|
3733
|
-
|
|
3947
|
+
this._clientLog.warn(`reject cannot check active rotation epoch key distribute: group=${groupId} rotation=${rotationId} err=${formatCaughtError(exc)}`);
|
|
3734
3948
|
return false;
|
|
3735
3949
|
}
|
|
3736
|
-
|
|
3950
|
+
this._clientLog.info(`reject non-pending/committed state epoch key distribute: group=${groupId} rotation=${rotationId} epoch=${epoch}`);
|
|
3737
3951
|
return false;
|
|
3738
3952
|
}
|
|
3739
3953
|
async _discardGroupDistributionIfStale(payload) {
|
|
@@ -3748,10 +3962,10 @@ export class AUNClient {
|
|
|
3748
3962
|
return;
|
|
3749
3963
|
try {
|
|
3750
3964
|
await this._groupE2ee.discardPendingSecret(groupId, epoch, rotationId);
|
|
3751
|
-
|
|
3965
|
+
this._clientLog.info('discard stale group epoch key after verify: group=%s epoch=%s rotation=%s', groupId, epoch, rotationId);
|
|
3752
3966
|
}
|
|
3753
3967
|
catch (exc) {
|
|
3754
|
-
|
|
3968
|
+
this._clientLog.debug('cleanup stale group epoch key failed: group=%s epoch=%s rotation=%s err=%s', groupId, epoch, rotationId, formatCaughtError(exc));
|
|
3755
3969
|
}
|
|
3756
3970
|
}
|
|
3757
3971
|
async _verifyGroupKeyResponseEpoch(payload) {
|
|
@@ -3768,7 +3982,7 @@ export class AUNClient {
|
|
|
3768
3982
|
return false;
|
|
3769
3983
|
const committedEpoch = Number(epochResult.committed_epoch ?? epochResult.epoch ?? 0);
|
|
3770
3984
|
if (epoch > committedEpoch) {
|
|
3771
|
-
|
|
3985
|
+
this._clientLog.info(`reject uncommitted epoch group key response: group=${groupId} epoch=${epoch} committed=${committedEpoch}`);
|
|
3772
3986
|
return false;
|
|
3773
3987
|
}
|
|
3774
3988
|
const committedRotation = isJsonObject(epochResult.committed_rotation) ? epochResult.committed_rotation : null;
|
|
@@ -3779,7 +3993,7 @@ export class AUNClient {
|
|
|
3779
3993
|
? committedRotation.expected_members.map((item) => String(item ?? '').trim()).filter(Boolean)
|
|
3780
3994
|
: [];
|
|
3781
3995
|
if (this._aid && !expectedMembers.includes(this._aid)) {
|
|
3782
|
-
|
|
3996
|
+
this._clientLog.debug(`allow group key response: new member recover commitment mismatch is normal group=${groupId} epoch=${epoch}`);
|
|
3783
3997
|
}
|
|
3784
3998
|
else {
|
|
3785
3999
|
return false;
|
|
@@ -3789,7 +4003,7 @@ export class AUNClient {
|
|
|
3789
4003
|
return true;
|
|
3790
4004
|
}
|
|
3791
4005
|
catch (exc) {
|
|
3792
|
-
|
|
4006
|
+
this._clientLog.warn(`reject cannot check committed epoch group key response: group=${groupId} epoch=${epoch} err=${formatCaughtError(exc)}`);
|
|
3793
4007
|
return false;
|
|
3794
4008
|
}
|
|
3795
4009
|
}
|
|
@@ -3804,7 +4018,7 @@ export class AUNClient {
|
|
|
3804
4018
|
return isJsonObject(result) && result.success === true;
|
|
3805
4019
|
}
|
|
3806
4020
|
catch (exc) {
|
|
3807
|
-
|
|
4021
|
+
this._clientLog.warn(`abort epoch rotation failed: rotation=${rotationId} err=${formatCaughtError(exc)}`);
|
|
3808
4022
|
return false;
|
|
3809
4023
|
}
|
|
3810
4024
|
}
|
|
@@ -3848,7 +4062,7 @@ export class AUNClient {
|
|
|
3848
4062
|
if (this._closing || this._state !== 'connected')
|
|
3849
4063
|
return;
|
|
3850
4064
|
if (Date.now() - started > 20000) {
|
|
3851
|
-
|
|
4065
|
+
this._clientLog.warn(`group epoch create sync still in-flight; skip duplicate sync (group=%s)${String(groupId)}`);
|
|
3852
4066
|
return;
|
|
3853
4067
|
}
|
|
3854
4068
|
await new Promise((resolve) => setTimeout(resolve, 200));
|
|
@@ -3882,12 +4096,12 @@ export class AUNClient {
|
|
|
3882
4096
|
const beginResult = await this.call('group.e2ee.begin_rotation', rotateParams);
|
|
3883
4097
|
const rotation = isJsonObject(beginResult) && isJsonObject(beginResult.rotation) ? beginResult.rotation : null;
|
|
3884
4098
|
if (!isJsonObject(beginResult) || beginResult.success !== true || !rotation) {
|
|
3885
|
-
|
|
4099
|
+
this._clientLog.warn('group epoch begin failed; stop key distribution (group=%s, returned=%s)', groupId, JSON.stringify(beginResult));
|
|
3886
4100
|
return;
|
|
3887
4101
|
}
|
|
3888
4102
|
const activeRotationId = String(rotation.rotation_id ?? rotationId);
|
|
3889
4103
|
if (!await this._ackGroupRotationKey(activeRotationId, secretData.commitment)) {
|
|
3890
|
-
|
|
4104
|
+
this._clientLog.warn(`group epoch self ack failed (group=%s, rotation=%s)${groupId} ${activeRotationId}`);
|
|
3891
4105
|
await this._abortGroupRotation(activeRotationId, 'self_ack_failed');
|
|
3892
4106
|
return;
|
|
3893
4107
|
}
|
|
@@ -3904,17 +4118,17 @@ export class AUNClient {
|
|
|
3904
4118
|
await storeGroupSecret(this._keystore, this._aid, groupId, 1, secretData.secret, secretData.commitment, secretData.member_aids, secretData.epoch_chain);
|
|
3905
4119
|
return;
|
|
3906
4120
|
}
|
|
3907
|
-
|
|
4121
|
+
this._clientLog.warn('group epoch commit failed (group=%s, returned=%s)', groupId, JSON.stringify(commitResult));
|
|
3908
4122
|
return;
|
|
3909
4123
|
}
|
|
3910
4124
|
catch (exc) {
|
|
3911
4125
|
if (attempt < maxRetries) {
|
|
3912
4126
|
const delay = 500 * Math.pow(2, attempt - 1);
|
|
3913
|
-
|
|
4127
|
+
this._clientLog.warn(`sync epoch to server failed (group=${groupId}, #${attempt}/${maxRetries} ), ${delay}ms then retry: ${String(exc)}`);
|
|
3914
4128
|
await new Promise(r => setTimeout(r, delay));
|
|
3915
4129
|
}
|
|
3916
4130
|
else {
|
|
3917
|
-
|
|
4131
|
+
this._clientLog.error(`sync epoch to server final failed (group=${groupId}, retry${maxRetries} ): ${String(exc)}`, exc instanceof Error ? exc : undefined);
|
|
3918
4132
|
}
|
|
3919
4133
|
}
|
|
3920
4134
|
}
|
|
@@ -3928,118 +4142,136 @@ export class AUNClient {
|
|
|
3928
4142
|
* 避免所有剩余 admin 同时触发 `_rotateGroupEpoch` 造成 CAS 风暴。
|
|
3929
4143
|
*/
|
|
3930
4144
|
async _maybeLeadRotateGroupEpoch(groupId, triggerId = '', expectedEpoch = null, allowMember = false) {
|
|
3931
|
-
const
|
|
3932
|
-
|
|
3933
|
-
return;
|
|
3934
|
-
const started = Date.now();
|
|
3935
|
-
while (this._groupEpochRotationInflight.has(groupId)) {
|
|
3936
|
-
if (triggerId && this._groupMembershipRotationDone.has(triggerId))
|
|
3937
|
-
return;
|
|
3938
|
-
if (this._closing || this._state !== 'connected')
|
|
3939
|
-
return;
|
|
3940
|
-
if (Date.now() - started > 20000) {
|
|
3941
|
-
console.warn('group epoch rotation still in-flight; skip pending trigger (group=%s trigger=%s)', groupId, triggerId || '-');
|
|
3942
|
-
return;
|
|
3943
|
-
}
|
|
3944
|
-
await new Promise((resolve) => setTimeout(resolve, 200));
|
|
3945
|
-
}
|
|
3946
|
-
if (this._closing || this._state !== 'connected')
|
|
3947
|
-
return;
|
|
3948
|
-
this._groupEpochRotationInflight.add(groupId);
|
|
4145
|
+
const tStart = Date.now();
|
|
4146
|
+
this._clientLog.debug(`_maybeLeadRotateGroupEpoch enter: group_id=${groupId} trigger=${triggerId || '-'} expected_epoch=${expectedEpoch ?? '-'} allow_member=${allowMember}`);
|
|
3949
4147
|
try {
|
|
3950
|
-
|
|
3951
|
-
|
|
3952
|
-
|
|
3953
|
-
if (!isJsonObject(membersResp))
|
|
4148
|
+
const myAid = this._aid;
|
|
4149
|
+
if (!myAid || this._closing || this._state !== 'connected') {
|
|
4150
|
+
this._clientLog.debug(`_maybeLeadRotateGroupEpoch exit: elapsed=${Date.now() - tStart}ms reason=not_ready`);
|
|
3954
4151
|
return;
|
|
3955
|
-
|
|
3956
|
-
|
|
3957
|
-
|
|
3958
|
-
|
|
3959
|
-
|
|
3960
|
-
|
|
3961
|
-
if (!isJsonObject(m))
|
|
3962
|
-
continue;
|
|
3963
|
-
const role = String(m.role ?? '');
|
|
3964
|
-
const aid = String(m.aid ?? '');
|
|
3965
|
-
if (!aid)
|
|
3966
|
-
continue;
|
|
3967
|
-
if (role === 'admin' || role === 'owner') {
|
|
3968
|
-
admins.push(aid);
|
|
4152
|
+
}
|
|
4153
|
+
const started = Date.now();
|
|
4154
|
+
while (this._groupEpochRotationInflight.has(groupId)) {
|
|
4155
|
+
if (triggerId && this._groupMembershipRotationDone.has(triggerId)) {
|
|
4156
|
+
this._clientLog.debug(`_maybeLeadRotateGroupEpoch exit: elapsed=${Date.now() - tStart}ms reason=trigger_done`);
|
|
4157
|
+
return;
|
|
3969
4158
|
}
|
|
3970
|
-
|
|
3971
|
-
|
|
4159
|
+
if (this._closing || this._state !== 'connected') {
|
|
4160
|
+
this._clientLog.debug(`_maybeLeadRotateGroupEpoch exit: elapsed=${Date.now() - tStart}ms reason=disconnected_during_wait`);
|
|
4161
|
+
return;
|
|
3972
4162
|
}
|
|
4163
|
+
if (Date.now() - started > 20000) {
|
|
4164
|
+
this._clientLog.warn(`group epoch rotation still in-flight; skip pending trigger (group=%s trigger=%s)${groupId} ${triggerId || '-'}`);
|
|
4165
|
+
this._clientLog.debug(`_maybeLeadRotateGroupEpoch exit: elapsed=${Date.now() - tStart}ms reason=inflight_timeout`);
|
|
4166
|
+
return;
|
|
4167
|
+
}
|
|
4168
|
+
await new Promise((resolve) => setTimeout(resolve, 200));
|
|
3973
4169
|
}
|
|
3974
|
-
|
|
3975
|
-
|
|
3976
|
-
if (candidates.length === 0)
|
|
4170
|
+
if (this._closing || this._state !== 'connected') {
|
|
4171
|
+
this._clientLog.debug(`_maybeLeadRotateGroupEpoch exit: elapsed=${Date.now() - tStart}ms reason=disconnected`);
|
|
3977
4172
|
return;
|
|
3978
|
-
|
|
3979
|
-
|
|
3980
|
-
|
|
3981
|
-
if (
|
|
3982
|
-
|
|
3983
|
-
|
|
3984
|
-
|
|
4173
|
+
}
|
|
4174
|
+
this._groupEpochRotationInflight.add(groupId);
|
|
4175
|
+
try {
|
|
4176
|
+
if (this._closing || this._state !== 'connected')
|
|
4177
|
+
return;
|
|
4178
|
+
const membersResp = await this.call('group.get_members', { group_id: groupId });
|
|
4179
|
+
if (!isJsonObject(membersResp))
|
|
4180
|
+
return;
|
|
4181
|
+
const rawList = membersResp.members ?? membersResp.items;
|
|
4182
|
+
if (!Array.isArray(rawList))
|
|
4183
|
+
return;
|
|
4184
|
+
const admins = [];
|
|
4185
|
+
const members = [];
|
|
4186
|
+
for (const m of rawList) {
|
|
4187
|
+
if (!isJsonObject(m))
|
|
4188
|
+
continue;
|
|
4189
|
+
const role = String(m.role ?? '');
|
|
4190
|
+
const aid = String(m.aid ?? '');
|
|
4191
|
+
if (!aid)
|
|
4192
|
+
continue;
|
|
4193
|
+
if (role === 'admin' || role === 'owner') {
|
|
4194
|
+
admins.push(aid);
|
|
3985
4195
|
}
|
|
3986
|
-
else if (
|
|
3987
|
-
|
|
4196
|
+
else if (allowMember && role === 'member') {
|
|
4197
|
+
members.push(aid);
|
|
3988
4198
|
}
|
|
3989
4199
|
}
|
|
3990
|
-
|
|
3991
|
-
|
|
3992
|
-
|
|
3993
|
-
|
|
4200
|
+
// 候选列表:admin/owner 排序在前,member 排序在后
|
|
4201
|
+
let candidates = [...admins.sort(), ...members.sort()];
|
|
4202
|
+
if (candidates.length === 0)
|
|
4203
|
+
return;
|
|
4204
|
+
// 没有当前 epoch key 的成员不参与 leader 选举
|
|
4205
|
+
if (expectedEpoch !== null && expectedEpoch > 0) {
|
|
4206
|
+
const localSecret = await this._groupE2ee.loadSecret(groupId, expectedEpoch);
|
|
4207
|
+
if (!localSecret) {
|
|
4208
|
+
const filtered = candidates.filter(c => c !== myAid);
|
|
4209
|
+
if (filtered.length > 0) {
|
|
4210
|
+
candidates = filtered;
|
|
4211
|
+
}
|
|
4212
|
+
else if (!allowMember) {
|
|
4213
|
+
return;
|
|
4214
|
+
}
|
|
4215
|
+
}
|
|
4216
|
+
}
|
|
4217
|
+
const leader = candidates[0];
|
|
4218
|
+
if (leader === myAid) {
|
|
4219
|
+
// 我是 leader,直接发起
|
|
4220
|
+
await this._rotateGroupEpoch(groupId, triggerId, expectedEpoch);
|
|
4221
|
+
return;
|
|
4222
|
+
}
|
|
4223
|
+
if (!candidates.includes(myAid))
|
|
4224
|
+
return;
|
|
4225
|
+
// 非 leader:随机 jitter(2~6s)后查询服务端 epoch 是否已被 leader 推进
|
|
4226
|
+
const jitterMs = 2000 + Math.floor(Math.random() * 4000);
|
|
4227
|
+
let beforeEpoch = 0;
|
|
4228
|
+
try {
|
|
4229
|
+
const resp = await this.call('group.e2ee.get_epoch', { group_id: groupId });
|
|
4230
|
+
if (isJsonObject(resp))
|
|
4231
|
+
beforeEpoch = Number(resp.epoch ?? 0);
|
|
4232
|
+
}
|
|
4233
|
+
catch {
|
|
4234
|
+
beforeEpoch = (await this._groupE2ee.currentEpoch(groupId)) ?? 0;
|
|
4235
|
+
}
|
|
4236
|
+
await new Promise((r) => setTimeout(r, jitterMs));
|
|
4237
|
+
if (this._closing || this._state !== 'connected')
|
|
4238
|
+
return;
|
|
4239
|
+
let afterEpoch = 0;
|
|
4240
|
+
let afterResp = {};
|
|
4241
|
+
try {
|
|
4242
|
+
afterResp = await this.call('group.e2ee.get_epoch', { group_id: groupId });
|
|
4243
|
+
if (isJsonObject(afterResp))
|
|
4244
|
+
afterEpoch = Number(afterResp.epoch ?? 0);
|
|
4245
|
+
}
|
|
4246
|
+
catch {
|
|
4247
|
+
afterEpoch = (await this._groupE2ee.currentEpoch(groupId)) ?? 0;
|
|
4248
|
+
}
|
|
4249
|
+
if (afterEpoch > beforeEpoch)
|
|
4250
|
+
return; // leader 已完成
|
|
4251
|
+
const pending = isJsonObject(afterResp) && isJsonObject(afterResp.pending_rotation) ? afterResp.pending_rotation : null;
|
|
4252
|
+
if (pending && !pending.expired) {
|
|
4253
|
+
this._scheduleGroupRotationRetry(groupId, {
|
|
4254
|
+
reason: 'membership_changed',
|
|
4255
|
+
triggerId,
|
|
4256
|
+
expectedEpoch,
|
|
4257
|
+
pending,
|
|
4258
|
+
});
|
|
4259
|
+
return;
|
|
4260
|
+
}
|
|
4261
|
+
this._clientLog.info(`[H21] leader did not complete epoch rotation, non-leader fallback: group=%s myAid=%s${groupId} ${myAid}`);
|
|
3994
4262
|
await this._rotateGroupEpoch(groupId, triggerId, expectedEpoch);
|
|
3995
|
-
return;
|
|
3996
|
-
}
|
|
3997
|
-
if (!candidates.includes(myAid))
|
|
3998
|
-
return;
|
|
3999
|
-
// 非 leader:随机 jitter(2~6s)后查询服务端 epoch 是否已被 leader 推进
|
|
4000
|
-
const jitterMs = 2000 + Math.floor(Math.random() * 4000);
|
|
4001
|
-
let beforeEpoch = 0;
|
|
4002
|
-
try {
|
|
4003
|
-
const resp = await this.call('group.e2ee.get_epoch', { group_id: groupId });
|
|
4004
|
-
if (isJsonObject(resp))
|
|
4005
|
-
beforeEpoch = Number(resp.epoch ?? 0);
|
|
4006
|
-
}
|
|
4007
|
-
catch {
|
|
4008
|
-
beforeEpoch = (await this._groupE2ee.currentEpoch(groupId)) ?? 0;
|
|
4009
|
-
}
|
|
4010
|
-
await new Promise((r) => setTimeout(r, jitterMs));
|
|
4011
|
-
if (this._closing || this._state !== 'connected')
|
|
4012
|
-
return;
|
|
4013
|
-
let afterEpoch = 0;
|
|
4014
|
-
let afterResp = {};
|
|
4015
|
-
try {
|
|
4016
|
-
afterResp = await this.call('group.e2ee.get_epoch', { group_id: groupId });
|
|
4017
|
-
if (isJsonObject(afterResp))
|
|
4018
|
-
afterEpoch = Number(afterResp.epoch ?? 0);
|
|
4019
4263
|
}
|
|
4020
|
-
catch {
|
|
4021
|
-
|
|
4264
|
+
catch (exc) {
|
|
4265
|
+
this._clientLog.warn(`_maybeLeadRotateGroupEpoch failed: %s${String(exc)}`);
|
|
4022
4266
|
}
|
|
4023
|
-
|
|
4024
|
-
|
|
4025
|
-
const pending = isJsonObject(afterResp) && isJsonObject(afterResp.pending_rotation) ? afterResp.pending_rotation : null;
|
|
4026
|
-
if (pending && !pending.expired) {
|
|
4027
|
-
this._scheduleGroupRotationRetry(groupId, {
|
|
4028
|
-
reason: 'membership_changed',
|
|
4029
|
-
triggerId,
|
|
4030
|
-
expectedEpoch,
|
|
4031
|
-
pending,
|
|
4032
|
-
});
|
|
4033
|
-
return;
|
|
4267
|
+
finally {
|
|
4268
|
+
this._groupEpochRotationInflight.delete(groupId);
|
|
4034
4269
|
}
|
|
4035
|
-
|
|
4036
|
-
await this._rotateGroupEpoch(groupId, triggerId, expectedEpoch);
|
|
4037
|
-
}
|
|
4038
|
-
catch (exc) {
|
|
4039
|
-
console.warn('_maybeLeadRotateGroupEpoch 失败: %s', exc);
|
|
4270
|
+
this._clientLog.debug(`_maybeLeadRotateGroupEpoch exit: elapsed=${Date.now() - tStart}ms group_id=${groupId}`);
|
|
4040
4271
|
}
|
|
4041
|
-
|
|
4042
|
-
this.
|
|
4272
|
+
catch (err) {
|
|
4273
|
+
this._clientLog.debug(`_maybeLeadRotateGroupEpoch exit (error): elapsed=${Date.now() - tStart}ms err=${err instanceof Error ? err.message : String(err)}`);
|
|
4274
|
+
throw err;
|
|
4043
4275
|
}
|
|
4044
4276
|
}
|
|
4045
4277
|
/**
|
|
@@ -4047,196 +4279,205 @@ export class AUNClient {
|
|
|
4047
4279
|
* 使用服务端 CAS 保证只有一方成功。
|
|
4048
4280
|
*/
|
|
4049
4281
|
async _rotateGroupEpoch(groupId, triggerId = '', expectedEpoch = null) {
|
|
4282
|
+
const tStart = Date.now();
|
|
4283
|
+
this._clientLog.debug(`_rotateGroupEpoch enter: group_id=${groupId} trigger=${triggerId || '-'} expected_epoch=${expectedEpoch ?? '-'}`);
|
|
4050
4284
|
try {
|
|
4051
|
-
|
|
4052
|
-
|
|
4053
|
-
|
|
4054
|
-
|
|
4055
|
-
|
|
4056
|
-
|
|
4057
|
-
|
|
4058
|
-
|
|
4059
|
-
|
|
4060
|
-
|
|
4061
|
-
|
|
4062
|
-
|
|
4063
|
-
|
|
4064
|
-
|
|
4065
|
-
|
|
4066
|
-
|
|
4067
|
-
|
|
4285
|
+
try {
|
|
4286
|
+
if (!this._aid)
|
|
4287
|
+
return;
|
|
4288
|
+
const memberAids = await this._getGroupMemberAids(groupId);
|
|
4289
|
+
if (triggerId && this._groupMembershipRotationDone.has(triggerId))
|
|
4290
|
+
return;
|
|
4291
|
+
const epochResult = await this.call('group.e2ee.get_epoch', { group_id: groupId });
|
|
4292
|
+
const serverEpoch = isJsonObject(epochResult) ? Number(epochResult.epoch ?? 0) : 0;
|
|
4293
|
+
const pendingRotation = isJsonObject(epochResult) && isJsonObject(epochResult.pending_rotation)
|
|
4294
|
+
? epochResult.pending_rotation
|
|
4295
|
+
: null;
|
|
4296
|
+
if (pendingRotation && !pendingRotation.expired) {
|
|
4297
|
+
const pendingRotationId = String(pendingRotation.rotation_id ?? '');
|
|
4298
|
+
const stalePending = (expectedEpoch !== null
|
|
4299
|
+
&& serverEpoch === expectedEpoch
|
|
4300
|
+
&& this._rotationExpectedMembersStale(pendingRotation, memberAids));
|
|
4301
|
+
if (stalePending && await this._abortGroupRotation(pendingRotationId, 'membership_changed_during_rotation')) {
|
|
4302
|
+
this._clientLog.info(`aborted stale pending group epoch rotation: group=%s rotation=%s${groupId} ${pendingRotationId || '-'}`);
|
|
4303
|
+
}
|
|
4304
|
+
else {
|
|
4305
|
+
this._scheduleGroupRotationRetry(groupId, {
|
|
4306
|
+
reason: 'membership_changed',
|
|
4307
|
+
triggerId,
|
|
4308
|
+
expectedEpoch,
|
|
4309
|
+
pending: pendingRotation,
|
|
4310
|
+
});
|
|
4311
|
+
return;
|
|
4312
|
+
}
|
|
4068
4313
|
}
|
|
4069
|
-
|
|
4070
|
-
|
|
4071
|
-
|
|
4072
|
-
|
|
4073
|
-
expectedEpoch,
|
|
4074
|
-
pending: pendingRotation,
|
|
4075
|
-
});
|
|
4314
|
+
if (expectedEpoch !== null && serverEpoch !== expectedEpoch) {
|
|
4315
|
+
if (triggerId)
|
|
4316
|
+
this._groupMembershipRotationDone.add(triggerId);
|
|
4317
|
+
this._clientLog.info(`skip membership epoch rotation: group=%s expected_epoch=%d server_epoch=%d trigger=%s${groupId} ${expectedEpoch} ${serverEpoch} ${triggerId || '-'}`);
|
|
4076
4318
|
return;
|
|
4077
4319
|
}
|
|
4078
|
-
|
|
4079
|
-
|
|
4080
|
-
|
|
4081
|
-
|
|
4082
|
-
|
|
4083
|
-
|
|
4084
|
-
|
|
4085
|
-
|
|
4086
|
-
|
|
4087
|
-
|
|
4088
|
-
|
|
4089
|
-
|
|
4090
|
-
|
|
4091
|
-
const cr = epochResult.committed_rotation;
|
|
4092
|
-
if (isJsonObject(cr)) {
|
|
4093
|
-
const rawChain = String(cr.epoch_chain ?? '').trim();
|
|
4094
|
-
if (rawChain) {
|
|
4095
|
-
prevChainHint = rawChain;
|
|
4096
|
-
console.info(`[aun_core] 轮换补充 prev epoch chain from server: group=${groupId} epoch=${currentEpoch}`);
|
|
4320
|
+
const currentEpoch = expectedEpoch ?? serverEpoch;
|
|
4321
|
+
const targetEpoch = currentEpoch + 1;
|
|
4322
|
+
let prevChainHint = null;
|
|
4323
|
+
const localPrev = await this._groupE2ee.loadSecret(groupId, currentEpoch);
|
|
4324
|
+
const localPrevChain = String(localPrev?.epoch_chain ?? '').trim();
|
|
4325
|
+
if (!localPrevChain && isJsonObject(epochResult)) {
|
|
4326
|
+
const cr = epochResult.committed_rotation;
|
|
4327
|
+
if (isJsonObject(cr)) {
|
|
4328
|
+
const rawChain = String(cr.epoch_chain ?? '').trim();
|
|
4329
|
+
if (rawChain) {
|
|
4330
|
+
prevChainHint = rawChain;
|
|
4331
|
+
this._clientLog.info(`rotation supplement prev epoch chain from server: group=${groupId} epoch=${currentEpoch}`);
|
|
4332
|
+
}
|
|
4097
4333
|
}
|
|
4098
4334
|
}
|
|
4099
|
-
|
|
4100
|
-
|
|
4101
|
-
|
|
4102
|
-
|
|
4103
|
-
|
|
4335
|
+
const rotationId = `rot-${_uuidV4().replace(/-/g, '')}`;
|
|
4336
|
+
const info = await this._groupE2ee.rotateEpochTo(groupId, targetEpoch, memberAids, { rotationId, prevChainHint });
|
|
4337
|
+
this._attachRotationId(info, rotationId);
|
|
4338
|
+
const discardGeneratedPending = async () => {
|
|
4339
|
+
try {
|
|
4340
|
+
await this._groupE2ee.discardPendingSecret(groupId, targetEpoch, rotationId);
|
|
4341
|
+
}
|
|
4342
|
+
catch (cleanupExc) {
|
|
4343
|
+
this._clientLog.debug('cleanup local pending group key failed: group=%s epoch=%d rotation=%s err=%s', groupId, targetEpoch, rotationId, formatCaughtError(cleanupExc));
|
|
4344
|
+
}
|
|
4345
|
+
};
|
|
4346
|
+
const rotateParams = {
|
|
4347
|
+
group_id: groupId,
|
|
4348
|
+
base_epoch: currentEpoch,
|
|
4349
|
+
target_epoch: targetEpoch,
|
|
4350
|
+
rotation_id: rotationId,
|
|
4351
|
+
reason: triggerId || expectedEpoch !== null ? 'membership_changed' : 'manual',
|
|
4352
|
+
key_commitment: String(info.commitment ?? ''),
|
|
4353
|
+
expected_members: memberAids,
|
|
4354
|
+
required_acks: [this._aid],
|
|
4355
|
+
lease_ms: GROUP_ROTATION_LEASE_MS,
|
|
4356
|
+
};
|
|
4357
|
+
const sigParams = await this._buildRotationSignature(groupId, currentEpoch, targetEpoch, rotateParams);
|
|
4358
|
+
Object.assign(rotateParams, sigParams);
|
|
4359
|
+
let rawBeginResult;
|
|
4104
4360
|
try {
|
|
4105
|
-
await this.
|
|
4361
|
+
rawBeginResult = await this.call('group.e2ee.begin_rotation', rotateParams);
|
|
4106
4362
|
}
|
|
4107
|
-
catch (
|
|
4108
|
-
|
|
4363
|
+
catch (exc) {
|
|
4364
|
+
await discardGeneratedPending();
|
|
4365
|
+
throw exc;
|
|
4109
4366
|
}
|
|
4110
|
-
|
|
4111
|
-
|
|
4112
|
-
|
|
4113
|
-
|
|
4114
|
-
|
|
4115
|
-
|
|
4116
|
-
|
|
4117
|
-
|
|
4118
|
-
|
|
4119
|
-
|
|
4120
|
-
|
|
4121
|
-
|
|
4122
|
-
|
|
4123
|
-
|
|
4124
|
-
|
|
4125
|
-
|
|
4126
|
-
|
|
4127
|
-
|
|
4128
|
-
|
|
4129
|
-
|
|
4130
|
-
|
|
4131
|
-
|
|
4132
|
-
const beginResult = isJsonObject(rawBeginResult) ? rawBeginResult : null;
|
|
4133
|
-
const beginRotationRaw = beginResult ? beginResult.rotation : null;
|
|
4134
|
-
const rotation = isJsonObject(beginRotationRaw) ? beginRotationRaw : null;
|
|
4135
|
-
if (!beginResult || beginResult.success !== true || !rotation) {
|
|
4136
|
-
if (rotation && !rotation.expired) {
|
|
4137
|
-
if (this._rotationExpectedMembersStale(rotation, memberAids)
|
|
4138
|
-
&& await this._abortGroupRotation(String(rotation.rotation_id ?? ''), 'membership_changed_during_rotation')) {
|
|
4139
|
-
this._scheduleGroupRotationRetry(groupId, {
|
|
4140
|
-
reason: 'membership_changed',
|
|
4141
|
-
triggerId,
|
|
4142
|
-
expectedEpoch,
|
|
4143
|
-
pending: null,
|
|
4144
|
-
});
|
|
4367
|
+
const beginResult = isJsonObject(rawBeginResult) ? rawBeginResult : null;
|
|
4368
|
+
const beginRotationRaw = beginResult ? beginResult.rotation : null;
|
|
4369
|
+
const rotation = isJsonObject(beginRotationRaw) ? beginRotationRaw : null;
|
|
4370
|
+
if (!beginResult || beginResult.success !== true || !rotation) {
|
|
4371
|
+
if (rotation && !rotation.expired) {
|
|
4372
|
+
if (this._rotationExpectedMembersStale(rotation, memberAids)
|
|
4373
|
+
&& await this._abortGroupRotation(String(rotation.rotation_id ?? ''), 'membership_changed_during_rotation')) {
|
|
4374
|
+
this._scheduleGroupRotationRetry(groupId, {
|
|
4375
|
+
reason: 'membership_changed',
|
|
4376
|
+
triggerId,
|
|
4377
|
+
expectedEpoch,
|
|
4378
|
+
pending: null,
|
|
4379
|
+
});
|
|
4380
|
+
}
|
|
4381
|
+
else {
|
|
4382
|
+
this._scheduleGroupRotationRetry(groupId, {
|
|
4383
|
+
reason: 'membership_changed',
|
|
4384
|
+
triggerId,
|
|
4385
|
+
expectedEpoch,
|
|
4386
|
+
pending: rotation,
|
|
4387
|
+
});
|
|
4388
|
+
}
|
|
4145
4389
|
}
|
|
4146
|
-
else {
|
|
4390
|
+
else if (beginResult && beginResult.reason === 'expected_members_mismatch') {
|
|
4147
4391
|
this._scheduleGroupRotationRetry(groupId, {
|
|
4148
4392
|
reason: 'membership_changed',
|
|
4149
4393
|
triggerId,
|
|
4150
4394
|
expectedEpoch,
|
|
4151
|
-
pending:
|
|
4395
|
+
pending: null,
|
|
4152
4396
|
});
|
|
4153
4397
|
}
|
|
4398
|
+
this._clientLog.warn('group epoch begin failed; stop key distribution (group=%s, current_epoch=%d, returned=%s)', groupId, currentEpoch, JSON.stringify(beginResult));
|
|
4399
|
+
await discardGeneratedPending();
|
|
4400
|
+
return;
|
|
4154
4401
|
}
|
|
4155
|
-
|
|
4402
|
+
const activeRotationId = String(rotation.rotation_id ?? rotationId);
|
|
4403
|
+
const distributeResult = await this._distributeGroupEpochKey(info, activeRotationId);
|
|
4404
|
+
if (distributeResult.failed.length > 0) {
|
|
4405
|
+
this._clientLog.warn('group epoch key distribution incomplete; abort rotation before retry (group=%s rotation=%s failed=%s)', groupId, activeRotationId, distributeResult.failed.join(','));
|
|
4406
|
+
await this._abortGroupRotation(activeRotationId, 'distribution_failed');
|
|
4156
4407
|
this._scheduleGroupRotationRetry(groupId, {
|
|
4157
4408
|
reason: 'membership_changed',
|
|
4158
4409
|
triggerId,
|
|
4159
4410
|
expectedEpoch,
|
|
4160
4411
|
pending: null,
|
|
4161
4412
|
});
|
|
4413
|
+
await discardGeneratedPending();
|
|
4414
|
+
return;
|
|
4162
4415
|
}
|
|
4163
|
-
|
|
4164
|
-
await
|
|
4165
|
-
|
|
4166
|
-
|
|
4167
|
-
|
|
4168
|
-
|
|
4169
|
-
|
|
4170
|
-
|
|
4171
|
-
|
|
4172
|
-
|
|
4173
|
-
reason: 'membership_changed',
|
|
4174
|
-
triggerId,
|
|
4175
|
-
expectedEpoch,
|
|
4176
|
-
pending: null,
|
|
4177
|
-
});
|
|
4178
|
-
await discardGeneratedPending();
|
|
4179
|
-
return;
|
|
4180
|
-
}
|
|
4181
|
-
await this._heartbeatGroupRotation(activeRotationId);
|
|
4182
|
-
if (!await this._ackGroupRotationKey(activeRotationId, String(info.commitment ?? ''))) {
|
|
4183
|
-
console.warn('group epoch self ack failed; abort rotation before retry (group=%s rotation=%s)', groupId, activeRotationId);
|
|
4184
|
-
await this._abortGroupRotation(activeRotationId, 'self_ack_failed');
|
|
4185
|
-
this._scheduleGroupRotationRetry(groupId, {
|
|
4186
|
-
reason: 'membership_changed',
|
|
4187
|
-
triggerId,
|
|
4188
|
-
expectedEpoch,
|
|
4189
|
-
pending: null,
|
|
4190
|
-
});
|
|
4191
|
-
await discardGeneratedPending();
|
|
4192
|
-
return;
|
|
4193
|
-
}
|
|
4194
|
-
const commitParams = { rotation_id: activeRotationId };
|
|
4195
|
-
// 构建 per-member ECIES 加密的 epoch key 上传到服务端
|
|
4196
|
-
if (await this._groupAllowsMemberEpochRotation(groupId)) {
|
|
4197
|
-
const encryptedKeys = await this._buildEpochEncryptedKeys(info, memberAids, targetEpoch, groupId);
|
|
4198
|
-
if (encryptedKeys && Object.keys(encryptedKeys).length > 0) {
|
|
4199
|
-
commitParams.encrypted_keys = encryptedKeys;
|
|
4200
|
-
}
|
|
4201
|
-
}
|
|
4202
|
-
const commitResult = await this.call('group.e2ee.commit_rotation', commitParams);
|
|
4203
|
-
if (!isJsonObject(commitResult) || commitResult.success !== true) {
|
|
4204
|
-
console.warn('group epoch commit failed (group=%s, rotation=%s, returned=%s)', groupId, activeRotationId, JSON.stringify(commitResult));
|
|
4205
|
-
this._scheduleGroupRotationRetry(groupId, {
|
|
4206
|
-
reason: 'membership_changed',
|
|
4207
|
-
triggerId,
|
|
4208
|
-
expectedEpoch,
|
|
4209
|
-
pending: isJsonObject(commitResult) && isJsonObject(commitResult.rotation) ? commitResult.rotation : rotation,
|
|
4210
|
-
});
|
|
4211
|
-
const returnedRotation = isJsonObject(commitResult) && isJsonObject(commitResult.rotation) ? commitResult.rotation : null;
|
|
4212
|
-
if (!(returnedRotation
|
|
4213
|
-
&& String(returnedRotation.rotation_id ?? '') === activeRotationId
|
|
4214
|
-
&& String(returnedRotation.status ?? '') === 'distributing')) {
|
|
4416
|
+
await this._heartbeatGroupRotation(activeRotationId);
|
|
4417
|
+
if (!await this._ackGroupRotationKey(activeRotationId, String(info.commitment ?? ''))) {
|
|
4418
|
+
this._clientLog.warn(`group epoch self ack failed; abort rotation before retry (group=%s rotation=%s)${groupId} ${activeRotationId}`);
|
|
4419
|
+
await this._abortGroupRotation(activeRotationId, 'self_ack_failed');
|
|
4420
|
+
this._scheduleGroupRotationRetry(groupId, {
|
|
4421
|
+
reason: 'membership_changed',
|
|
4422
|
+
triggerId,
|
|
4423
|
+
expectedEpoch,
|
|
4424
|
+
pending: null,
|
|
4425
|
+
});
|
|
4215
4426
|
await discardGeneratedPending();
|
|
4427
|
+
return;
|
|
4216
4428
|
}
|
|
4217
|
-
|
|
4218
|
-
|
|
4219
|
-
|
|
4220
|
-
|
|
4221
|
-
|
|
4222
|
-
|
|
4223
|
-
|
|
4224
|
-
if (this._groupSecretMatchesCommittedRotation(committedSecret, committedRotation)) {
|
|
4225
|
-
await storeGroupSecret(this._keystore, this._aid, groupId, targetEpoch, committedSecret.secret, committedSecret.commitment, committedSecret.member_aids.length > 0 ? committedSecret.member_aids : memberAids, committedSecret.epoch_chain);
|
|
4429
|
+
const commitParams = { rotation_id: activeRotationId };
|
|
4430
|
+
// 构建 per-member ECIES 加密的 epoch key 上传到服务端
|
|
4431
|
+
if (await this._groupAllowsMemberEpochRotation(groupId)) {
|
|
4432
|
+
const encryptedKeys = await this._buildEpochEncryptedKeys(info, memberAids, targetEpoch, groupId);
|
|
4433
|
+
if (encryptedKeys && Object.keys(encryptedKeys).length > 0) {
|
|
4434
|
+
commitParams.encrypted_keys = encryptedKeys;
|
|
4435
|
+
}
|
|
4226
4436
|
}
|
|
4227
|
-
|
|
4228
|
-
|
|
4437
|
+
const commitResult = await this.call('group.e2ee.commit_rotation', commitParams);
|
|
4438
|
+
if (!isJsonObject(commitResult) || commitResult.success !== true) {
|
|
4439
|
+
this._clientLog.warn('group epoch commit failed (group=%s, rotation=%s, returned=%s)', groupId, activeRotationId, JSON.stringify(commitResult));
|
|
4440
|
+
this._scheduleGroupRotationRetry(groupId, {
|
|
4441
|
+
reason: 'membership_changed',
|
|
4442
|
+
triggerId,
|
|
4443
|
+
expectedEpoch,
|
|
4444
|
+
pending: isJsonObject(commitResult) && isJsonObject(commitResult.rotation) ? commitResult.rotation : rotation,
|
|
4445
|
+
});
|
|
4446
|
+
const returnedRotation = isJsonObject(commitResult) && isJsonObject(commitResult.rotation) ? commitResult.rotation : null;
|
|
4447
|
+
if (!(returnedRotation
|
|
4448
|
+
&& String(returnedRotation.rotation_id ?? '') === activeRotationId
|
|
4449
|
+
&& String(returnedRotation.status ?? '') === 'distributing')) {
|
|
4450
|
+
await discardGeneratedPending();
|
|
4451
|
+
}
|
|
4452
|
+
return;
|
|
4229
4453
|
}
|
|
4230
|
-
|
|
4231
|
-
|
|
4232
|
-
|
|
4233
|
-
|
|
4234
|
-
|
|
4454
|
+
const committedSecret = await this._groupE2ee.loadSecret(groupId, targetEpoch);
|
|
4455
|
+
if (committedSecret && this._aid) {
|
|
4456
|
+
const committedRotation = isJsonObject(commitResult.rotation)
|
|
4457
|
+
? commitResult.rotation
|
|
4458
|
+
: { rotation_id: activeRotationId, key_commitment: String(info.commitment ?? '') };
|
|
4459
|
+
if (this._groupSecretMatchesCommittedRotation(committedSecret, committedRotation)) {
|
|
4460
|
+
await storeGroupSecret(this._keystore, this._aid, groupId, targetEpoch, committedSecret.secret, committedSecret.commitment, committedSecret.member_aids.length > 0 ? committedSecret.member_aids : memberAids, committedSecret.epoch_chain);
|
|
4461
|
+
}
|
|
4462
|
+
else {
|
|
4463
|
+
this._clientLog.warn(`group epoch commit succeeded but local target key does not match committed rotation; keep pending blocked (group=%s rotation=%s epoch=%d)${groupId} ${activeRotationId} ${targetEpoch}`);
|
|
4464
|
+
}
|
|
4465
|
+
}
|
|
4466
|
+
if (triggerId) {
|
|
4467
|
+
this._groupMembershipRotationDone.add(triggerId);
|
|
4468
|
+
if (this._groupMembershipRotationDone.size > 2000) {
|
|
4469
|
+
this._groupMembershipRotationDone = new Set(Array.from(this._groupMembershipRotationDone).slice(-1000));
|
|
4470
|
+
}
|
|
4235
4471
|
}
|
|
4236
4472
|
}
|
|
4473
|
+
catch (exc) {
|
|
4474
|
+
this._logE2eeError('rotate_epoch', groupId, '', exc);
|
|
4475
|
+
}
|
|
4476
|
+
this._clientLog.debug(`_rotateGroupEpoch exit: elapsed=${Date.now() - tStart}ms group_id=${groupId}`);
|
|
4237
4477
|
}
|
|
4238
|
-
catch (
|
|
4239
|
-
this.
|
|
4478
|
+
catch (err) {
|
|
4479
|
+
this._clientLog.debug(`_rotateGroupEpoch exit (error): elapsed=${Date.now() - tStart}ms err=${err instanceof Error ? err.message : String(err)}`);
|
|
4480
|
+
throw err;
|
|
4240
4481
|
}
|
|
4241
4482
|
}
|
|
4242
4483
|
/** 从成员加入事件 payload 中提取新加入的成员 AID 列表。 */
|
|
@@ -4416,73 +4657,82 @@ export class AUNClient {
|
|
|
4416
4657
|
}
|
|
4417
4658
|
// ── 内部:连接 ────────────────────────────────────
|
|
4418
4659
|
async _connectOnce(params, allowReauth) {
|
|
4419
|
-
const
|
|
4420
|
-
this.
|
|
4421
|
-
this._slotId = String(params.slot_id ?? '');
|
|
4422
|
-
this._connectDeliveryMode = { ...(params.delivery_mode ?? this._connectDeliveryMode) };
|
|
4423
|
-
this._auth.setInstanceContext({ deviceId: this._deviceId, slotId: this._slotId });
|
|
4424
|
-
this._state = 'connecting';
|
|
4425
|
-
// 前置 restore:在 _transport.connect 启动 reader 之前完成,
|
|
4426
|
-
// 避免 reader 把积压 push 交给空 tracker 的 handler,触发 S2 历史 gap 误补拉。
|
|
4427
|
-
this._refreshSeqTrackerContext();
|
|
4428
|
-
await this._restoreSeqTrackerState();
|
|
4660
|
+
const tStart = Date.now();
|
|
4661
|
+
this._clientLog.debug(`_connectOnce enter: allow_reauth=${allowReauth}`);
|
|
4429
4662
|
try {
|
|
4430
|
-
const
|
|
4431
|
-
this.
|
|
4432
|
-
|
|
4433
|
-
|
|
4434
|
-
|
|
4435
|
-
|
|
4436
|
-
|
|
4437
|
-
|
|
4438
|
-
|
|
4439
|
-
|
|
4440
|
-
|
|
4441
|
-
|
|
4442
|
-
|
|
4443
|
-
|
|
4444
|
-
|
|
4445
|
-
|
|
4446
|
-
|
|
4663
|
+
const gatewayUrl = this._resolveGateway(params);
|
|
4664
|
+
this._gatewayUrl = gatewayUrl;
|
|
4665
|
+
this._slotId = String(params.slot_id ?? '');
|
|
4666
|
+
this._connectDeliveryMode = { ...(params.delivery_mode ?? this._connectDeliveryMode) };
|
|
4667
|
+
this._auth.setInstanceContext({ deviceId: this._deviceId, slotId: this._slotId });
|
|
4668
|
+
this._state = 'connecting';
|
|
4669
|
+
// 前置 restore:在 _transport.connect 启动 reader 之前完成,
|
|
4670
|
+
// 避免 reader 把积压 push 交给空 tracker 的 handler,触发 S2 历史 gap 误补拉。
|
|
4671
|
+
this._refreshSeqTrackerContext();
|
|
4672
|
+
await this._restoreSeqTrackerState();
|
|
4673
|
+
try {
|
|
4674
|
+
const challenge = await this._transport.connect(gatewayUrl);
|
|
4675
|
+
this._state = 'authenticating';
|
|
4676
|
+
if (allowReauth) {
|
|
4677
|
+
const authContext = await this._auth.connectSession(this._transport, challenge, gatewayUrl, {
|
|
4678
|
+
accessToken: params.access_token,
|
|
4679
|
+
deviceId: this._deviceId,
|
|
4680
|
+
slotId: this._slotId,
|
|
4681
|
+
deliveryMode: this._connectDeliveryMode,
|
|
4682
|
+
});
|
|
4683
|
+
if (isJsonObject(authContext)) {
|
|
4684
|
+
const auth = authContext;
|
|
4685
|
+
const identity = auth.identity;
|
|
4686
|
+
if (identity && isJsonObject(identity)) {
|
|
4687
|
+
this._identity = identity;
|
|
4688
|
+
this._aid = String(identity.aid ?? this._aid ?? '');
|
|
4689
|
+
if (this._sessionParams) {
|
|
4690
|
+
this._sessionParams.access_token = String(auth.token ?? params.access_token ?? '');
|
|
4691
|
+
}
|
|
4447
4692
|
}
|
|
4448
4693
|
}
|
|
4449
4694
|
}
|
|
4695
|
+
else {
|
|
4696
|
+
await this._auth.initializeWithToken(this._transport, challenge, String(params.access_token), {
|
|
4697
|
+
deviceId: this._deviceId,
|
|
4698
|
+
slotId: this._slotId,
|
|
4699
|
+
deliveryMode: this._connectDeliveryMode,
|
|
4700
|
+
});
|
|
4701
|
+
await this._syncIdentityAfterConnect(String(params.access_token));
|
|
4702
|
+
}
|
|
4450
4703
|
}
|
|
4451
|
-
|
|
4452
|
-
|
|
4453
|
-
|
|
4454
|
-
|
|
4455
|
-
|
|
4456
|
-
}
|
|
4457
|
-
|
|
4704
|
+
catch (err) {
|
|
4705
|
+
// P1-19: 首连失败时重置状态,避免半连接残留
|
|
4706
|
+
this._state = 'disconnected';
|
|
4707
|
+
try {
|
|
4708
|
+
await this._transport.close();
|
|
4709
|
+
}
|
|
4710
|
+
catch { /* 忽略关闭错误 */ }
|
|
4711
|
+
throw err;
|
|
4458
4712
|
}
|
|
4459
|
-
|
|
4460
|
-
|
|
4461
|
-
|
|
4462
|
-
|
|
4713
|
+
this._state = 'connected';
|
|
4714
|
+
await this._dispatcher.publish('connection.state', {
|
|
4715
|
+
state: this._state,
|
|
4716
|
+
gateway: gatewayUrl,
|
|
4717
|
+
});
|
|
4718
|
+
// auth 阶段 aid 可能被 identity 覆盖;若 context 发生变化,重新 refresh + restore。
|
|
4719
|
+
if (this._seqTrackerContext !== this._currentSeqTrackerContext()) {
|
|
4720
|
+
this._refreshSeqTrackerContext();
|
|
4721
|
+
await this._restoreSeqTrackerState();
|
|
4722
|
+
}
|
|
4723
|
+
this._startBackgroundTasks();
|
|
4724
|
+
// 上线后自动上传 prekey
|
|
4463
4725
|
try {
|
|
4464
|
-
await this.
|
|
4726
|
+
await this._uploadPrekey();
|
|
4465
4727
|
}
|
|
4466
|
-
catch {
|
|
4467
|
-
|
|
4468
|
-
|
|
4469
|
-
|
|
4470
|
-
await this._dispatcher.publish('connection.state', {
|
|
4471
|
-
state: this._state,
|
|
4472
|
-
gateway: gatewayUrl,
|
|
4473
|
-
});
|
|
4474
|
-
// auth 阶段 aid 可能被 identity 覆盖;若 context 发生变化,重新 refresh + restore。
|
|
4475
|
-
if (this._seqTrackerContext !== this._currentSeqTrackerContext()) {
|
|
4476
|
-
this._refreshSeqTrackerContext();
|
|
4477
|
-
await this._restoreSeqTrackerState();
|
|
4478
|
-
}
|
|
4479
|
-
this._startBackgroundTasks();
|
|
4480
|
-
// 上线后自动上传 prekey
|
|
4481
|
-
try {
|
|
4482
|
-
await this._uploadPrekey();
|
|
4728
|
+
catch (exc) {
|
|
4729
|
+
this._clientLog.warn(`prekey upload failed:${String(exc)}`);
|
|
4730
|
+
}
|
|
4731
|
+
this._clientLog.debug(`_connectOnce exit: elapsed=${Date.now() - tStart}ms aid=${this._aid ?? '-'}`);
|
|
4483
4732
|
}
|
|
4484
|
-
catch (
|
|
4485
|
-
|
|
4733
|
+
catch (err) {
|
|
4734
|
+
this._clientLog.debug(`_connectOnce exit (error): elapsed=${Date.now() - tStart}ms err=${err instanceof Error ? err.message : String(err)}`);
|
|
4735
|
+
throw err;
|
|
4486
4736
|
}
|
|
4487
4737
|
}
|
|
4488
4738
|
_resolveGateway(params) {
|
|
@@ -4650,10 +4900,10 @@ export class AUNClient {
|
|
|
4650
4900
|
}
|
|
4651
4901
|
catch (exc) {
|
|
4652
4902
|
consecutiveFailures++;
|
|
4653
|
-
|
|
4903
|
+
this._clientLog.warn(`heartbeat failed (${consecutiveFailures}/${maxFailures}): ${String(exc)}`);
|
|
4654
4904
|
this._dispatcher.publish('connection.error', { error: formatCaughtError(exc) });
|
|
4655
4905
|
if (consecutiveFailures >= maxFailures) {
|
|
4656
|
-
|
|
4906
|
+
this._clientLog.warn(`consecutive ${maxFailures} heartbeat failed, trigger disconnect reconnect`);
|
|
4657
4907
|
this._handleTransportDisconnect(exc instanceof Error ? exc : new Error(String(exc)));
|
|
4658
4908
|
}
|
|
4659
4909
|
}
|
|
@@ -4712,7 +4962,7 @@ export class AUNClient {
|
|
|
4712
4962
|
if (exc instanceof AuthError) {
|
|
4713
4963
|
this._tokenRefreshFailures++;
|
|
4714
4964
|
if (this._tokenRefreshFailures >= 3) {
|
|
4715
|
-
|
|
4965
|
+
this._clientLog.warn(`token refreshconsecutivefailed ${this._tokenRefreshFailures} , stop refresh loop and trigger reconnect`);
|
|
4716
4966
|
this._dispatcher.publish('token.refresh_exhausted', {
|
|
4717
4967
|
aid: this._identity?.aid ?? null,
|
|
4718
4968
|
consecutive_failures: this._tokenRefreshFailures,
|
|
@@ -4722,7 +4972,7 @@ export class AUNClient {
|
|
|
4722
4972
|
this._handleTransportDisconnect(new Error('token refresh exhausted, triggering reconnect'));
|
|
4723
4973
|
return;
|
|
4724
4974
|
}
|
|
4725
|
-
|
|
4975
|
+
this._clientLog.warn(`token refresh failed (${this._tokenRefreshFailures}/3), next retry: ${String(exc)}`);
|
|
4726
4976
|
}
|
|
4727
4977
|
else {
|
|
4728
4978
|
this._dispatcher.publish('connection.error', { error: formatCaughtError(exc) });
|
|
@@ -4770,7 +5020,7 @@ export class AUNClient {
|
|
|
4770
5020
|
}
|
|
4771
5021
|
}
|
|
4772
5022
|
catch (exc) {
|
|
4773
|
-
|
|
5023
|
+
this._clientLog.warn(`prekey scheduled refresh failed:${String(exc)}`);
|
|
4774
5024
|
}
|
|
4775
5025
|
// 仍处于连接状态时安排下一次检查
|
|
4776
5026
|
if (this._state === 'connected') {
|
|
@@ -4869,7 +5119,7 @@ export class AUNClient {
|
|
|
4869
5119
|
this._prekeyReplenished.add(prekeyId);
|
|
4870
5120
|
}
|
|
4871
5121
|
catch (exc) {
|
|
4872
|
-
|
|
5122
|
+
this._clientLog.warn(`consume prekey ${prekeyId} then replenish current prekey failed: ${String(exc)}`);
|
|
4873
5123
|
}
|
|
4874
5124
|
finally {
|
|
4875
5125
|
this._prekeyReplenishInflight.delete(prekeyId);
|
|
@@ -4894,7 +5144,7 @@ export class AUNClient {
|
|
|
4894
5144
|
}
|
|
4895
5145
|
}
|
|
4896
5146
|
catch (exc) {
|
|
4897
|
-
|
|
5147
|
+
this._clientLog.warn(`epoch cleanup failed:${String(exc)}`);
|
|
4898
5148
|
}
|
|
4899
5149
|
}, 3600 * 1000);
|
|
4900
5150
|
}
|
|
@@ -4913,7 +5163,7 @@ export class AUNClient {
|
|
|
4913
5163
|
}
|
|
4914
5164
|
}
|
|
4915
5165
|
catch (exc) {
|
|
4916
|
-
|
|
5166
|
+
this._clientLog.warn(`epoch scheduled rotation failed:${String(exc)}`);
|
|
4917
5167
|
}
|
|
4918
5168
|
}, rotateInterval * 1000);
|
|
4919
5169
|
}
|
|
@@ -4946,7 +5196,7 @@ export class AUNClient {
|
|
|
4946
5196
|
_onGatewayDisconnect(data) {
|
|
4947
5197
|
const code = data?.code;
|
|
4948
5198
|
const reason = data?.reason ?? '';
|
|
4949
|
-
|
|
5199
|
+
this._clientLog.warn(`server initiated disconnect: code=${code}, reason=${reason}`);
|
|
4950
5200
|
this._serverKicked = true;
|
|
4951
5201
|
}
|
|
4952
5202
|
async _handleTransportDisconnect(error, closeCode) {
|
|
@@ -4967,7 +5217,7 @@ export class AUNClient {
|
|
|
4967
5217
|
if (this._serverKicked || (closeCode !== undefined && AUNClient._NO_RECONNECT_CODES.has(closeCode))) {
|
|
4968
5218
|
this._state = 'terminal_failed';
|
|
4969
5219
|
const reason = this._serverKicked ? 'server kicked' : `close code ${closeCode}`;
|
|
4970
|
-
|
|
5220
|
+
this._clientLog.warn(`suppress auto-reconnect: ${reason}`);
|
|
4971
5221
|
await this._dispatcher.publish('connection.state', {
|
|
4972
5222
|
state: this._state, error, reason,
|
|
4973
5223
|
});
|
|
@@ -5057,62 +5307,80 @@ export class AUNClient {
|
|
|
5057
5307
|
* 服务端签发群 AID 证书,返回后将证书和私钥存入 keystore。
|
|
5058
5308
|
*/
|
|
5059
5309
|
async createNamedGroup(groupName, opts = {}) {
|
|
5060
|
-
const
|
|
5061
|
-
|
|
5062
|
-
|
|
5063
|
-
|
|
5064
|
-
|
|
5065
|
-
|
|
5066
|
-
|
|
5067
|
-
|
|
5068
|
-
|
|
5069
|
-
|
|
5070
|
-
|
|
5071
|
-
|
|
5072
|
-
|
|
5073
|
-
|
|
5074
|
-
|
|
5075
|
-
|
|
5076
|
-
|
|
5077
|
-
|
|
5078
|
-
|
|
5079
|
-
|
|
5080
|
-
|
|
5081
|
-
|
|
5082
|
-
|
|
5310
|
+
const tStart = Date.now();
|
|
5311
|
+
this._clientLog.debug(`createNamedGroup enter: name=${groupName}`);
|
|
5312
|
+
try {
|
|
5313
|
+
const cp = new CryptoProvider();
|
|
5314
|
+
const identity = await cp.generateIdentity();
|
|
5315
|
+
const params = {};
|
|
5316
|
+
for (const [k, v] of Object.entries(opts)) {
|
|
5317
|
+
params[k] = v;
|
|
5318
|
+
}
|
|
5319
|
+
params.group_name = groupName;
|
|
5320
|
+
params.public_key = identity.public_key_der_b64;
|
|
5321
|
+
params.curve = 'P-256';
|
|
5322
|
+
const result = await this.call('group.create', params);
|
|
5323
|
+
const groupInfo = result?.group;
|
|
5324
|
+
const aidCert = result?.aid_cert;
|
|
5325
|
+
const groupAid = String(groupInfo?.group_aid ?? '');
|
|
5326
|
+
if (groupAid && aidCert) {
|
|
5327
|
+
await this._keystore.saveIdentity(groupAid, {
|
|
5328
|
+
private_key_pem: identity.private_key_pem,
|
|
5329
|
+
public_key: identity.public_key_der_b64,
|
|
5330
|
+
curve: 'P-256',
|
|
5331
|
+
type: 'group_identity',
|
|
5332
|
+
});
|
|
5333
|
+
const certPem = String(aidCert.cert ?? '');
|
|
5334
|
+
if (certPem) {
|
|
5335
|
+
await this._keystore.saveCert(groupAid, certPem);
|
|
5336
|
+
}
|
|
5083
5337
|
}
|
|
5338
|
+
this._clientLog.debug(`createNamedGroup exit: elapsed=${Date.now() - tStart}ms group_aid=${groupAid}`);
|
|
5339
|
+
return result;
|
|
5340
|
+
}
|
|
5341
|
+
catch (err) {
|
|
5342
|
+
this._clientLog.debug(`createNamedGroup exit (error): elapsed=${Date.now() - tStart}ms err=${err instanceof Error ? err.message : String(err)}`);
|
|
5343
|
+
throw err;
|
|
5084
5344
|
}
|
|
5085
|
-
return result;
|
|
5086
5345
|
}
|
|
5087
5346
|
/**
|
|
5088
5347
|
* 为已有普通群绑定命名 AID(升级为命名群)。
|
|
5089
5348
|
*/
|
|
5090
5349
|
async bindGroupAid(groupId, groupName) {
|
|
5091
|
-
const
|
|
5092
|
-
|
|
5093
|
-
|
|
5094
|
-
|
|
5095
|
-
|
|
5096
|
-
|
|
5097
|
-
|
|
5098
|
-
|
|
5099
|
-
const result = await this.call('group.bind_aid', params);
|
|
5100
|
-
const groupInfo = result?.group;
|
|
5101
|
-
const aidCert = result?.aid_cert;
|
|
5102
|
-
const groupAid = String(groupInfo?.group_aid ?? '');
|
|
5103
|
-
if (groupAid && aidCert) {
|
|
5104
|
-
await this._keystore.saveIdentity(groupAid, {
|
|
5105
|
-
private_key_pem: identity.private_key_pem,
|
|
5350
|
+
const tStart = Date.now();
|
|
5351
|
+
this._clientLog.debug(`bindGroupAid enter: group_id=${groupId} name=${groupName}`);
|
|
5352
|
+
try {
|
|
5353
|
+
const cp = new CryptoProvider();
|
|
5354
|
+
const identity = await cp.generateIdentity();
|
|
5355
|
+
const params = {
|
|
5356
|
+
group_id: groupId,
|
|
5357
|
+
group_name: groupName,
|
|
5106
5358
|
public_key: identity.public_key_der_b64,
|
|
5107
5359
|
curve: 'P-256',
|
|
5108
|
-
|
|
5109
|
-
|
|
5110
|
-
const
|
|
5111
|
-
|
|
5112
|
-
|
|
5360
|
+
};
|
|
5361
|
+
const result = await this.call('group.bind_aid', params);
|
|
5362
|
+
const groupInfo = result?.group;
|
|
5363
|
+
const aidCert = result?.aid_cert;
|
|
5364
|
+
const groupAid = String(groupInfo?.group_aid ?? '');
|
|
5365
|
+
if (groupAid && aidCert) {
|
|
5366
|
+
await this._keystore.saveIdentity(groupAid, {
|
|
5367
|
+
private_key_pem: identity.private_key_pem,
|
|
5368
|
+
public_key: identity.public_key_der_b64,
|
|
5369
|
+
curve: 'P-256',
|
|
5370
|
+
type: 'group_identity',
|
|
5371
|
+
});
|
|
5372
|
+
const certPem = String(aidCert.cert ?? '');
|
|
5373
|
+
if (certPem) {
|
|
5374
|
+
await this._keystore.saveCert(groupAid, certPem);
|
|
5375
|
+
}
|
|
5113
5376
|
}
|
|
5377
|
+
this._clientLog.debug(`bindGroupAid exit: elapsed=${Date.now() - tStart}ms group_aid=${groupAid}`);
|
|
5378
|
+
return result;
|
|
5379
|
+
}
|
|
5380
|
+
catch (err) {
|
|
5381
|
+
this._clientLog.debug(`bindGroupAid exit (error): elapsed=${Date.now() - tStart}ms err=${err instanceof Error ? err.message : String(err)}`);
|
|
5382
|
+
throw err;
|
|
5114
5383
|
}
|
|
5115
|
-
return result;
|
|
5116
5384
|
}
|
|
5117
5385
|
/** 判断是否应重试重连 */
|
|
5118
5386
|
_shouldRetryReconnect(error) {
|
|
@@ -5338,7 +5606,7 @@ export class AUNClient {
|
|
|
5338
5606
|
/** 安全执行异步操作(不阻塞调用方,错误打 warning 便于排障) */
|
|
5339
5607
|
_safeAsync(promise) {
|
|
5340
5608
|
promise.catch((exc) => {
|
|
5341
|
-
|
|
5609
|
+
this._clientLog.warn(`background task exception:${String(exc)}`);
|
|
5342
5610
|
});
|
|
5343
5611
|
}
|
|
5344
5612
|
/** 可取消的 sleep */
|