@agentunion/fastaun 0.3.5 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +18 -0
- package/_packed_docs/AUN_SDK_/351/207/215/346/236/204/345/256/236/346/226/275/350/256/241/345/210/222.md +596 -0
- package/_packed_docs/AUN_SDK_/351/207/215/346/236/204/350/256/276/350/256/241/346/226/271/346/241/210_v3.md +1633 -0
- package/_packed_docs/CHANGELOG.md +18 -0
- package/_packed_docs/INDEX.md +17 -11
- package/_packed_docs/KITE_DOCS_GUIDE.md +11 -10
- package/_packed_docs/sdk/01-/345/277/253/351/200/237/345/274/200/345/247/213.md +134 -158
- package/_packed_docs/sdk/02-WebSocket/345/215/217/350/256/256.md +11 -7
- package/_packed_docs/sdk/03-/346/240/270/345/277/203/346/246/202/345/277/265.md +98 -119
- package/_packed_docs/sdk/04-/350/277/236/346/216/245/344/270/216/350/256/244/350/257/201.md +147 -374
- package/_packed_docs/sdk/05-E2EE/345/212/240/345/257/206/351/200/232/344/277/241.md +153 -153
- package/_packed_docs/sdk/06-API/346/211/213/345/206/214.md +163 -1364
- package/_packed_docs/sdk/07-/351/224/231/350/257/257/345/244/204/347/220/206.md +71 -91
- package/_packed_docs/sdk/08-/346/234/200/344/275/263/345/256/236/350/267/265.md +76 -63
- package/_packed_docs/sdk/09-custody-api-manual.md +7 -6
- package/_packed_docs/sdk/09-meta-rpc-manual.md +13 -14
- package/_packed_docs/sdk/09-storage-rpc-manual.md +89 -0
- package/_packed_docs/sdk/AUN_DOCS_GUIDE.md +37 -49
- package/_packed_docs/sdk/INDEX.md +72 -98
- package/_packed_docs/sdk/README.md +85 -266
- package/dist/aid-store.d.ts +66 -0
- package/dist/aid-store.js +539 -0
- package/dist/aid-store.js.map +1 -0
- package/dist/aid.d.ts +52 -0
- package/dist/aid.js +140 -0
- package/dist/aid.js.map +1 -0
- package/dist/auth.d.ts +18 -1
- package/dist/auth.js +28 -9
- package/dist/auth.js.map +1 -1
- package/dist/cert-utils.d.ts +29 -0
- package/dist/cert-utils.js +142 -0
- package/dist/cert-utils.js.map +1 -0
- package/dist/client.d.ts +102 -89
- package/dist/client.js +871 -253
- package/dist/client.js.map +1 -1
- package/dist/error-codes.d.ts +25 -0
- package/dist/error-codes.js +26 -0
- package/dist/error-codes.js.map +1 -0
- package/dist/errors.d.ts +4 -1
- package/dist/errors.js +4 -1
- package/dist/errors.js.map +1 -1
- package/dist/index.d.ts +6 -5
- package/dist/index.js +5 -4
- package/dist/index.js.map +1 -1
- package/dist/keystore/aid-db.js +33 -0
- package/dist/keystore/aid-db.js.map +1 -1
- package/dist/keystore/file.d.ts +17 -0
- package/dist/keystore/file.js +194 -0
- package/dist/keystore/file.js.map +1 -1
- package/dist/keystore/index.d.ts +2 -0
- package/dist/namespaces/auth.js +2 -0
- package/dist/namespaces/auth.js.map +1 -1
- package/dist/result.d.ts +17 -0
- package/dist/result.js +10 -0
- package/dist/result.js.map +1 -0
- package/dist/secret-store/file-store.d.ts +5 -0
- package/dist/secret-store/file-store.js +32 -13
- package/dist/secret-store/file-store.js.map +1 -1
- package/dist/tools/cross-sdk-agent.js +24 -12
- package/dist/tools/cross-sdk-agent.js.map +1 -1
- package/dist/transport.js +1 -1
- package/dist/transport.js.map +1 -1
- package/dist/types.d.ts +14 -0
- package/dist/types.js +30 -0
- package/dist/types.js.map +1 -1
- package/dist/v2/e2ee/encrypt-p2p.js +1 -1
- package/dist/v2/e2ee/encrypt-p2p.js.map +1 -1
- package/dist/version.d.ts +1 -0
- package/dist/version.js +5 -0
- package/dist/version.js.map +1 -0
- package/package.json +1 -1
package/dist/client.js
CHANGED
|
@@ -20,14 +20,11 @@ import { configFromMap, getDeviceId, normalizeInstanceId } from './config.js';
|
|
|
20
20
|
import { CryptoProvider } from './crypto.js';
|
|
21
21
|
import { GatewayDiscovery } from './discovery.js';
|
|
22
22
|
import { DnsResilientNet } from './net.js';
|
|
23
|
-
import { AUNError, AuthError, ConnectionError, E2EEError, PermissionError, StateError, TimeoutError, ValidationError, } from './errors.js';
|
|
23
|
+
import { AUNError, AuthError, ConnectionError, E2EEError, NotFoundError, PermissionError, StateError, TimeoutError, ValidationError, } from './errors.js';
|
|
24
24
|
import { EventDispatcher } from './events.js';
|
|
25
25
|
import { FileKeyStore } from './keystore/file.js';
|
|
26
26
|
import { AUNLogger } from './logger.js';
|
|
27
27
|
import { normalizeGroupId } from './group-id.js';
|
|
28
|
-
import { AuthNamespace } from './namespaces/auth.js';
|
|
29
|
-
import { CustodyNamespace } from './namespaces/custody.js';
|
|
30
|
-
import { MetaNamespace } from './namespaces/meta.js';
|
|
31
28
|
import { RPCTransport } from './transport.js';
|
|
32
29
|
import { AuthFlow } from './auth.js';
|
|
33
30
|
import { SeqTracker } from './seq-tracker.js';
|
|
@@ -35,7 +32,8 @@ import { V2Session } from './v2/session/index.js';
|
|
|
35
32
|
import { encryptP2PMessage, encryptGroupMessage, decryptMessage, } from './v2/e2ee/index.js';
|
|
36
33
|
import { ecdsaVerifyRaw } from './v2/crypto/ecdsa.js';
|
|
37
34
|
import { computeStateCommitment } from './v2/state/index.js';
|
|
38
|
-
import { isJsonObject, } from './types.js';
|
|
35
|
+
import { isJsonObject, ConnectionState, STATE_TO_PUBLIC, } from './types.js';
|
|
36
|
+
import { AID } from './aid.js';
|
|
39
37
|
function isPromiseLike(value) {
|
|
40
38
|
return Boolean(value && typeof value.then === 'function');
|
|
41
39
|
}
|
|
@@ -130,6 +128,12 @@ const DEFAULT_SESSION_OPTIONS = {
|
|
|
130
128
|
http: 30.0,
|
|
131
129
|
},
|
|
132
130
|
};
|
|
131
|
+
const PROTECTED_HEADERS_METHODS = new Set([
|
|
132
|
+
'message.send',
|
|
133
|
+
'group.send',
|
|
134
|
+
'message.thought.put',
|
|
135
|
+
'group.thought.put',
|
|
136
|
+
]);
|
|
133
137
|
const RECONNECT_MIN_BASE_DELAY_MS = 1_000;
|
|
134
138
|
const RECONNECT_MAX_BASE_DELAY_MS = 64_000;
|
|
135
139
|
const TOKEN_REFRESH_CHECK_INTERVAL_MS = 30_000;
|
|
@@ -200,6 +204,7 @@ const SIGNED_METHODS = new Set([
|
|
|
200
204
|
]);
|
|
201
205
|
/** peer 证书缓存 TTL(1 小时) */
|
|
202
206
|
const PEER_CERT_CACHE_TTL = 3600;
|
|
207
|
+
const AGENT_MD_HTTP_TIMEOUT_MS = 30_000;
|
|
203
208
|
function normalizeV2WrapPolicy(raw) {
|
|
204
209
|
if (!raw || typeof raw !== 'object' || Array.isArray(raw)) {
|
|
205
210
|
return { explicit: false, version: '', protocol: '', scope: 'device' };
|
|
@@ -377,6 +382,50 @@ function lengthPrefixedBytesKey(...parts) {
|
|
|
377
382
|
}
|
|
378
383
|
return Buffer.concat(chunks);
|
|
379
384
|
}
|
|
385
|
+
function agentMdHttpScheme(gatewayUrl) {
|
|
386
|
+
const raw = String(gatewayUrl ?? '').trim().toLowerCase();
|
|
387
|
+
return raw.startsWith('ws://') ? 'http' : 'https';
|
|
388
|
+
}
|
|
389
|
+
function agentMdAuthority(aid, discoveryPort) {
|
|
390
|
+
const host = String(aid ?? '').trim();
|
|
391
|
+
if (!host)
|
|
392
|
+
return '';
|
|
393
|
+
if (discoveryPort && !host.includes(':'))
|
|
394
|
+
return `${host}:${discoveryPort}`;
|
|
395
|
+
return host;
|
|
396
|
+
}
|
|
397
|
+
async function fetchWithTimeout(input, init, timeoutMs = AGENT_MD_HTTP_TIMEOUT_MS) {
|
|
398
|
+
const controller = new AbortController();
|
|
399
|
+
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
400
|
+
try {
|
|
401
|
+
return await fetch(input, { ...init, signal: controller.signal });
|
|
402
|
+
}
|
|
403
|
+
catch (error) {
|
|
404
|
+
if (controller.signal.aborted) {
|
|
405
|
+
throw new AUNError(`agent.md request timed out after ${timeoutMs}ms`);
|
|
406
|
+
}
|
|
407
|
+
throw error;
|
|
408
|
+
}
|
|
409
|
+
finally {
|
|
410
|
+
clearTimeout(timer);
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
function assertClientOptions(value, label) {
|
|
414
|
+
if (value == null)
|
|
415
|
+
return;
|
|
416
|
+
if (typeof value !== 'object' || Array.isArray(value) || value instanceof AID) {
|
|
417
|
+
throw new ValidationError(`${label} must be an options object`);
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
function clientOptionsConfig(options) {
|
|
421
|
+
const raw = { ...(options ?? {}) };
|
|
422
|
+
if (Object.prototype.hasOwnProperty.call(raw, 'aid')) {
|
|
423
|
+
throw new ValidationError('AUNClient options must not include aid; pass an AID object as the first argument');
|
|
424
|
+
}
|
|
425
|
+
delete raw.debug;
|
|
426
|
+
delete raw.protected_headers;
|
|
427
|
+
return raw;
|
|
428
|
+
}
|
|
380
429
|
export class AUNClient {
|
|
381
430
|
/** 原始配置 */
|
|
382
431
|
config;
|
|
@@ -387,7 +436,17 @@ export class AUNClient {
|
|
|
387
436
|
/** 当前身份信息(内存缓存) */
|
|
388
437
|
_identity = null;
|
|
389
438
|
/** 连接状态 */
|
|
390
|
-
_state = '
|
|
439
|
+
_state = 'no_identity';
|
|
440
|
+
/** 当前 AID 值对象(新 API) */
|
|
441
|
+
_currentAid = null;
|
|
442
|
+
/** 实例级 protected_headers */
|
|
443
|
+
_instanceProtectedHeaders = null;
|
|
444
|
+
/** 重连退避时间戳(ms) */
|
|
445
|
+
_nextRetryAt = null;
|
|
446
|
+
_retryAttempt = 0;
|
|
447
|
+
_retryMaxAttempts = 0;
|
|
448
|
+
_lastError = null;
|
|
449
|
+
_lastErrorCode = null;
|
|
391
450
|
/** Gateway URL */
|
|
392
451
|
_gatewayUrl = null;
|
|
393
452
|
/** 是否正在关闭 */
|
|
@@ -402,12 +461,6 @@ export class AUNClient {
|
|
|
402
461
|
_auth;
|
|
403
462
|
/** 密钥存储 */
|
|
404
463
|
_keystore;
|
|
405
|
-
/** Auth 命名空间 */
|
|
406
|
-
auth;
|
|
407
|
-
/** AID 托管命名空间 */
|
|
408
|
-
custody;
|
|
409
|
-
/** Meta 命名空间(心跳、状态、信任根管理) */
|
|
410
|
-
meta;
|
|
411
464
|
/** 会话参数(重连用) */
|
|
412
465
|
_sessionParams = null;
|
|
413
466
|
/** 会话选项 */
|
|
@@ -428,6 +481,9 @@ export class AUNClient {
|
|
|
428
481
|
_remoteAgentMdEtag = '';
|
|
429
482
|
_agentMdCache = new Map();
|
|
430
483
|
_agentMdFetchInflight = new Map();
|
|
484
|
+
_agentMdDownloadInflight = new Map();
|
|
485
|
+
_agentMdDownloadActive = 0;
|
|
486
|
+
_agentMdDownloadWaiters = [];
|
|
431
487
|
/** 消息序列号跟踪器(群消息 + P2P 空洞检测) */
|
|
432
488
|
_seqTracker = new SeqTracker();
|
|
433
489
|
_seqTrackerContext = null;
|
|
@@ -474,8 +530,11 @@ export class AUNClient {
|
|
|
474
530
|
static V2_BOOTSTRAP_TTL_MS = 60 * 60 * 1000;
|
|
475
531
|
static V2_RETRYABLE_CODES = new Set([-33011, -33012, -33050, -33052, -33054]);
|
|
476
532
|
static PULL_GATE_STALE_MS = 3000;
|
|
533
|
+
/** 对端 AID 缓存(aid string → AID 对象) */
|
|
534
|
+
_peerCache = new Map();
|
|
477
535
|
static V2_SIG_CACHE_TTL_MS = 60 * 60 * 1000;
|
|
478
536
|
static V2_SIG_CACHE_MAX = 16_384;
|
|
537
|
+
static AGENT_MD_DOWNLOAD_CONCURRENCY = 8;
|
|
479
538
|
_reconnectActive = false;
|
|
480
539
|
_reconnectAbort = null;
|
|
481
540
|
_serverKicked = false;
|
|
@@ -483,17 +542,32 @@ export class AUNClient {
|
|
|
483
542
|
_lastDisconnectInfo = null;
|
|
484
543
|
_logger;
|
|
485
544
|
_clientLog;
|
|
486
|
-
constructor(
|
|
487
|
-
|
|
545
|
+
constructor(first, second) {
|
|
546
|
+
if (typeof first === 'string') {
|
|
547
|
+
throw new ValidationError('AUNClient aid must be an AID object, not a string');
|
|
548
|
+
}
|
|
549
|
+
if (typeof second === 'boolean') {
|
|
550
|
+
throw new ValidationError('AUNClient debug must be passed as options.debug');
|
|
551
|
+
}
|
|
552
|
+
const inputAid = first instanceof AID ? first : null;
|
|
553
|
+
if (!inputAid && second !== undefined) {
|
|
554
|
+
throw new ValidationError('AUNClient options-only construction accepts a single options object');
|
|
555
|
+
}
|
|
556
|
+
const options = inputAid ? (second ?? {}) : (first ?? {});
|
|
557
|
+
assertClientOptions(options, 'AUNClient options');
|
|
558
|
+
const rawConfig = clientOptionsConfig(options);
|
|
559
|
+
if (inputAid)
|
|
560
|
+
rawConfig.aun_path = inputAid.aunPath;
|
|
561
|
+
const debug = !!options?.debug;
|
|
488
562
|
this._configModel = configFromMap(rawConfig);
|
|
489
|
-
const initAid =
|
|
563
|
+
const initAid = inputAid ? inputAid.aid : null;
|
|
490
564
|
this._agentMdPath = path.join(this._configModel.aunPath, 'AIDs');
|
|
491
565
|
this.config = {
|
|
492
566
|
aun_path: this._configModel.aunPath,
|
|
493
567
|
root_ca_path: this._configModel.rootCaPath,
|
|
494
568
|
seed_password: this._configModel.seedPassword,
|
|
495
569
|
};
|
|
496
|
-
this._deviceId = getDeviceId(this._configModel.aunPath);
|
|
570
|
+
this._deviceId = (inputAid?.deviceId) || getDeviceId(this._configModel.aunPath);
|
|
497
571
|
// 初始化 Logger(per-client 单例,必须最早创建)
|
|
498
572
|
const debugFlag = this._configModel.debug || debug;
|
|
499
573
|
this._logger = new AUNLogger({
|
|
@@ -530,7 +604,7 @@ export class AUNClient {
|
|
|
530
604
|
catch (err) {
|
|
531
605
|
this._clientLog.warn(`_pending cleanup failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
532
606
|
}
|
|
533
|
-
this._slotId = '';
|
|
607
|
+
this._slotId = inputAid?.slotId || 'default';
|
|
534
608
|
this._connectDeliveryMode = normalizeDeliveryModeConfig({ mode: 'fanout' });
|
|
535
609
|
this._defaultConnectDeliveryMode = { ...this._connectDeliveryMode };
|
|
536
610
|
this._auth = new AuthFlow({
|
|
@@ -554,9 +628,22 @@ export class AUNClient {
|
|
|
554
628
|
dnsNet,
|
|
555
629
|
});
|
|
556
630
|
this._transport.setMetaObserver((meta) => this._observeRpcMeta(meta));
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
631
|
+
if (inputAid) {
|
|
632
|
+
if (!inputAid.isPrivateKeyValid()) {
|
|
633
|
+
throw new StateError('AUNClient requires an AID with a valid private key');
|
|
634
|
+
}
|
|
635
|
+
this._currentAid = inputAid;
|
|
636
|
+
this._identity = {
|
|
637
|
+
aid: inputAid.aid,
|
|
638
|
+
private_key_pem: inputAid._privateKeyPem ?? '',
|
|
639
|
+
public_key_der_b64: inputAid.publicKey,
|
|
640
|
+
cert: inputAid.certPem,
|
|
641
|
+
};
|
|
642
|
+
this._state = 'standby';
|
|
643
|
+
}
|
|
644
|
+
if (options?.protected_headers !== undefined) {
|
|
645
|
+
this.setProtectedHeaders(options.protected_headers);
|
|
646
|
+
}
|
|
560
647
|
// 内部订阅:推送消息自动解密后 re-publish 给用户
|
|
561
648
|
this._dispatcher.subscribe('_raw.message.received', (data) => this._onRawMessageReceived(data));
|
|
562
649
|
// V2 P2P 推送通知:收到通知后自动走 message.v2.pull 拉取并解密
|
|
@@ -587,6 +674,360 @@ export class AUNClient {
|
|
|
587
674
|
get aid() {
|
|
588
675
|
return this._aid;
|
|
589
676
|
}
|
|
677
|
+
/** 当前 AID 值对象 */
|
|
678
|
+
get currentAid() {
|
|
679
|
+
return this._currentAid;
|
|
680
|
+
}
|
|
681
|
+
get hasIdentity() {
|
|
682
|
+
return this._currentAid !== null && this.state !== ConnectionState.CLOSED;
|
|
683
|
+
}
|
|
684
|
+
get canSign() {
|
|
685
|
+
return this.hasIdentity && !!this._currentAid?.isPrivateKeyValid();
|
|
686
|
+
}
|
|
687
|
+
get canConnect() {
|
|
688
|
+
return this.hasIdentity && this.state !== ConnectionState.CLOSED;
|
|
689
|
+
}
|
|
690
|
+
get canSend() {
|
|
691
|
+
return this.state === ConnectionState.READY;
|
|
692
|
+
}
|
|
693
|
+
get isReady() {
|
|
694
|
+
return this.canSend;
|
|
695
|
+
}
|
|
696
|
+
get isOnline() {
|
|
697
|
+
return this.state === ConnectionState.READY || this.state === ConnectionState.RETRY_BACKOFF || this.state === ConnectionState.RECONNECTING;
|
|
698
|
+
}
|
|
699
|
+
get isClosed() {
|
|
700
|
+
return this.state === ConnectionState.CLOSED;
|
|
701
|
+
}
|
|
702
|
+
get aunPath() {
|
|
703
|
+
return this.hasIdentity ? this._currentAid?.aunPath ?? this._configModel.aunPath : null;
|
|
704
|
+
}
|
|
705
|
+
get nextRetryAt() {
|
|
706
|
+
return this.state === ConnectionState.RETRY_BACKOFF && this._nextRetryAt ? new Date(this._nextRetryAt) : null;
|
|
707
|
+
}
|
|
708
|
+
get nextRetryInSeconds() {
|
|
709
|
+
return this.state === ConnectionState.RETRY_BACKOFF && this._nextRetryAt ? Math.max(0, Math.ceil((this._nextRetryAt - Date.now()) / 1000)) : null;
|
|
710
|
+
}
|
|
711
|
+
get retryAttempt() {
|
|
712
|
+
return this._retryAttempt;
|
|
713
|
+
}
|
|
714
|
+
get retryMaxAttempts() {
|
|
715
|
+
return this._retryMaxAttempts;
|
|
716
|
+
}
|
|
717
|
+
get lastError() {
|
|
718
|
+
return this._lastError;
|
|
719
|
+
}
|
|
720
|
+
get lastErrorCode() {
|
|
721
|
+
return this._lastErrorCode;
|
|
722
|
+
}
|
|
723
|
+
loadIdentity(aid) {
|
|
724
|
+
if (!aid?.isPrivateKeyValid()) {
|
|
725
|
+
throw new StateError('loadIdentity requires an AID with a valid private key');
|
|
726
|
+
}
|
|
727
|
+
const publicState = this.state;
|
|
728
|
+
if (publicState !== ConnectionState.NO_IDENTITY && publicState !== ConnectionState.CLOSED) {
|
|
729
|
+
throw new StateError(`loadIdentity not allowed in state ${publicState}`);
|
|
730
|
+
}
|
|
731
|
+
this._currentAid = aid;
|
|
732
|
+
this._aid = aid.aid;
|
|
733
|
+
this._identity = {
|
|
734
|
+
aid: aid.aid,
|
|
735
|
+
private_key_pem: aid._privateKeyPem ?? '',
|
|
736
|
+
public_key_der_b64: aid.publicKey,
|
|
737
|
+
cert: aid.certPem,
|
|
738
|
+
};
|
|
739
|
+
this._auth._aid = aid.aid;
|
|
740
|
+
this._state = 'standby';
|
|
741
|
+
this._closing = false;
|
|
742
|
+
this._lastError = null;
|
|
743
|
+
this._lastErrorCode = null;
|
|
744
|
+
this._retryAttempt = 0;
|
|
745
|
+
this._nextRetryAt = null;
|
|
746
|
+
}
|
|
747
|
+
setProtectedHeaders(headers) {
|
|
748
|
+
if (!headers) {
|
|
749
|
+
this._instanceProtectedHeaders = null;
|
|
750
|
+
return;
|
|
751
|
+
}
|
|
752
|
+
// 字段规范:key 限 [a-z0-9_-],_auth 为保留键不可设置。
|
|
753
|
+
// 非法 key 静默跳过(不报错),值强转 str。
|
|
754
|
+
const cleaned = {};
|
|
755
|
+
for (const [key, value] of Object.entries(headers)) {
|
|
756
|
+
const keyStr = String(key);
|
|
757
|
+
if (keyStr === '_auth')
|
|
758
|
+
continue;
|
|
759
|
+
if (!/^[a-z0-9_-]+$/.test(keyStr))
|
|
760
|
+
continue;
|
|
761
|
+
cleaned[keyStr] = value == null ? '' : String(value);
|
|
762
|
+
}
|
|
763
|
+
this._instanceProtectedHeaders = Object.keys(cleaned).length ? cleaned : null;
|
|
764
|
+
}
|
|
765
|
+
getProtectedHeaders() {
|
|
766
|
+
return this._instanceProtectedHeaders ? { ...this._instanceProtectedHeaders } : null;
|
|
767
|
+
}
|
|
768
|
+
cachePeer(aid) {
|
|
769
|
+
if (!this.hasIdentity)
|
|
770
|
+
throw new StateError('cachePeer requires a loaded identity');
|
|
771
|
+
if (!aid.isCertValid())
|
|
772
|
+
throw new ValidationError('cachePeer requires an AID with a valid certificate');
|
|
773
|
+
this._peerCache.set(aid.aid, aid);
|
|
774
|
+
return aid;
|
|
775
|
+
}
|
|
776
|
+
getPeer(aid) {
|
|
777
|
+
if (!this.hasIdentity)
|
|
778
|
+
throw new StateError('getPeer requires a loaded identity');
|
|
779
|
+
return this._peerCache.get(String(aid ?? '').trim()) ?? null;
|
|
780
|
+
}
|
|
781
|
+
async lookupPeer(aid) {
|
|
782
|
+
if (!this.hasIdentity)
|
|
783
|
+
throw new StateError('lookupPeer requires a loaded identity');
|
|
784
|
+
const target = String(aid ?? '').trim();
|
|
785
|
+
if (!target)
|
|
786
|
+
throw new ValidationError('lookupPeer requires non-empty aid');
|
|
787
|
+
const cached = this._peerCache.get(target);
|
|
788
|
+
if (cached)
|
|
789
|
+
return cached;
|
|
790
|
+
throw new NotFoundError(`peer not found in cache: ${target}`);
|
|
791
|
+
}
|
|
792
|
+
peers() {
|
|
793
|
+
if (!this.hasIdentity)
|
|
794
|
+
throw new StateError('peers requires a loaded identity');
|
|
795
|
+
return [...this._peerCache.entries()].sort(([a], [b]) => a.localeCompare(b)).map(([, v]) => v);
|
|
796
|
+
}
|
|
797
|
+
async _resolveAgentMdUrl(aid) {
|
|
798
|
+
const target = String(aid ?? '').trim();
|
|
799
|
+
if (!target)
|
|
800
|
+
throw new ValidationError('agent.md requires non-empty aid');
|
|
801
|
+
let gatewayUrl = String(this._gatewayUrl ?? '').trim();
|
|
802
|
+
if (!gatewayUrl) {
|
|
803
|
+
try {
|
|
804
|
+
gatewayUrl = await this._resolveGatewayForAid(target);
|
|
805
|
+
}
|
|
806
|
+
catch {
|
|
807
|
+
gatewayUrl = '';
|
|
808
|
+
}
|
|
809
|
+
}
|
|
810
|
+
return `${agentMdHttpScheme(gatewayUrl)}://${agentMdAuthority(target, this._configModel.discoveryPort)}/agent.md`;
|
|
811
|
+
}
|
|
812
|
+
async _ensureAgentMdUploadToken(aid, gatewayUrl) {
|
|
813
|
+
let identity = this._auth.loadIdentityOrNone(aid);
|
|
814
|
+
if (!identity && this._identity && String(this._identity.aid ?? '') === aid) {
|
|
815
|
+
identity = this._identity;
|
|
816
|
+
}
|
|
817
|
+
if (!identity) {
|
|
818
|
+
throw new StateError('no local identity found, register or load an AID first');
|
|
819
|
+
}
|
|
820
|
+
const cachedToken = String(identity.access_token ?? '');
|
|
821
|
+
const expiresAt = this._auth.getAccessTokenExpiry(identity);
|
|
822
|
+
if (cachedToken && (expiresAt === null || expiresAt > Date.now() / 1000 + 30)) {
|
|
823
|
+
return cachedToken;
|
|
824
|
+
}
|
|
825
|
+
if (identity.refresh_token) {
|
|
826
|
+
try {
|
|
827
|
+
const refreshed = await this._auth.refreshCachedTokens(gatewayUrl, identity);
|
|
828
|
+
const refreshedToken = String(refreshed.access_token ?? '');
|
|
829
|
+
const refreshedExpiry = this._auth.getAccessTokenExpiry(refreshed);
|
|
830
|
+
if (refreshedToken && (refreshedExpiry === null || refreshedExpiry > Date.now() / 1000 + 30)) {
|
|
831
|
+
this._identity = refreshed;
|
|
832
|
+
return refreshedToken;
|
|
833
|
+
}
|
|
834
|
+
}
|
|
835
|
+
catch {
|
|
836
|
+
// refresh 失败时回退到完整 authenticate。
|
|
837
|
+
}
|
|
838
|
+
}
|
|
839
|
+
const result = await this._auth.authenticate(gatewayUrl, { aid });
|
|
840
|
+
const token = String(result.access_token ?? '');
|
|
841
|
+
if (!token)
|
|
842
|
+
throw new StateError('authenticate did not return access_token');
|
|
843
|
+
this._identity = this._auth.loadIdentityOrNone(aid) ?? {
|
|
844
|
+
...identity,
|
|
845
|
+
access_token: token,
|
|
846
|
+
refresh_token: String(result.refresh_token ?? identity.refresh_token ?? ''),
|
|
847
|
+
access_token_expires_at: typeof result.expires_at === 'number' ? result.expires_at : identity.access_token_expires_at,
|
|
848
|
+
token_exp: typeof result.expires_at === 'number' ? result.expires_at : identity.token_exp,
|
|
849
|
+
expires_at: typeof result.expires_at === 'number' ? result.expires_at : identity.expires_at,
|
|
850
|
+
};
|
|
851
|
+
return token;
|
|
852
|
+
}
|
|
853
|
+
async _uploadAgentMd(content) {
|
|
854
|
+
const target = String(this._aid ?? this._currentAid?.aid ?? '').trim();
|
|
855
|
+
if (!target)
|
|
856
|
+
throw new StateError('uploadAgentMd requires local AID');
|
|
857
|
+
const gatewayUrl = await this._resolveGatewayForAid(target);
|
|
858
|
+
this._gatewayUrl = gatewayUrl;
|
|
859
|
+
const token = await this._ensureAgentMdUploadToken(target, gatewayUrl);
|
|
860
|
+
const response = await fetchWithTimeout(await this._resolveAgentMdUrl(target), {
|
|
861
|
+
method: 'PUT',
|
|
862
|
+
headers: {
|
|
863
|
+
Authorization: `Bearer ${token}`,
|
|
864
|
+
'Content-Type': 'text/markdown; charset=utf-8',
|
|
865
|
+
},
|
|
866
|
+
body: content,
|
|
867
|
+
});
|
|
868
|
+
if (response.status === 404) {
|
|
869
|
+
throw new NotFoundError(`agent.md endpoint not found for aid: ${target}`);
|
|
870
|
+
}
|
|
871
|
+
if (!response.ok) {
|
|
872
|
+
const message = (await response.text()).trim();
|
|
873
|
+
throw new AUNError(`upload agent.md failed: HTTP ${response.status}${message ? ` - ${message}` : ''}`);
|
|
874
|
+
}
|
|
875
|
+
const payload = await response.json();
|
|
876
|
+
if (!isJsonObject(payload))
|
|
877
|
+
throw new AUNError('upload agent.md returned invalid JSON payload');
|
|
878
|
+
return payload;
|
|
879
|
+
}
|
|
880
|
+
async _acquireAgentMdDownloadSlot() {
|
|
881
|
+
if (this._agentMdDownloadActive < AUNClient.AGENT_MD_DOWNLOAD_CONCURRENCY) {
|
|
882
|
+
this._agentMdDownloadActive += 1;
|
|
883
|
+
return () => this._releaseAgentMdDownloadSlot();
|
|
884
|
+
}
|
|
885
|
+
await new Promise((resolve) => {
|
|
886
|
+
this._agentMdDownloadWaiters.push(resolve);
|
|
887
|
+
});
|
|
888
|
+
return () => this._releaseAgentMdDownloadSlot();
|
|
889
|
+
}
|
|
890
|
+
_releaseAgentMdDownloadSlot() {
|
|
891
|
+
const next = this._agentMdDownloadWaiters.shift();
|
|
892
|
+
if (next) {
|
|
893
|
+
next();
|
|
894
|
+
return;
|
|
895
|
+
}
|
|
896
|
+
if (this._agentMdDownloadActive > 0)
|
|
897
|
+
this._agentMdDownloadActive -= 1;
|
|
898
|
+
}
|
|
899
|
+
async _downloadAgentMd(aid) {
|
|
900
|
+
const target = String(aid ?? '').trim();
|
|
901
|
+
if (!target)
|
|
902
|
+
throw new ValidationError('downloadAgentMd requires non-empty aid');
|
|
903
|
+
const existing = this._agentMdDownloadInflight.get(target);
|
|
904
|
+
if (existing)
|
|
905
|
+
return await existing;
|
|
906
|
+
const task = (async () => {
|
|
907
|
+
const release = await this._acquireAgentMdDownloadSlot();
|
|
908
|
+
try {
|
|
909
|
+
return await this._downloadAgentMdOnce(target);
|
|
910
|
+
}
|
|
911
|
+
finally {
|
|
912
|
+
release();
|
|
913
|
+
}
|
|
914
|
+
})();
|
|
915
|
+
this._agentMdDownloadInflight.set(target, task);
|
|
916
|
+
task.finally(() => {
|
|
917
|
+
if (this._agentMdDownloadInflight.get(target) === task) {
|
|
918
|
+
this._agentMdDownloadInflight.delete(target);
|
|
919
|
+
}
|
|
920
|
+
}).catch(() => undefined);
|
|
921
|
+
return await task;
|
|
922
|
+
}
|
|
923
|
+
async _downloadAgentMdOnce(target) {
|
|
924
|
+
const cached = this._agentMdCache.get(target);
|
|
925
|
+
const url = await this._resolveAgentMdUrl(target);
|
|
926
|
+
let response = await fetchWithTimeout(url, {
|
|
927
|
+
method: 'GET',
|
|
928
|
+
headers: { Accept: 'text/markdown' },
|
|
929
|
+
redirect: 'follow',
|
|
930
|
+
});
|
|
931
|
+
if (response.status === 304 && typeof cached?.text === 'string') {
|
|
932
|
+
return String(cached.text);
|
|
933
|
+
}
|
|
934
|
+
if (response.status === 304) {
|
|
935
|
+
response = await fetchWithTimeout(url, {
|
|
936
|
+
method: 'GET',
|
|
937
|
+
headers: { Accept: 'text/markdown' },
|
|
938
|
+
cache: 'reload',
|
|
939
|
+
redirect: 'follow',
|
|
940
|
+
});
|
|
941
|
+
}
|
|
942
|
+
if (response.status === 404) {
|
|
943
|
+
throw new NotFoundError(`agent.md not found for aid: ${target}`);
|
|
944
|
+
}
|
|
945
|
+
if (!response.ok) {
|
|
946
|
+
const message = (await response.text()).trim();
|
|
947
|
+
throw new AUNError(`download agent.md failed: HTTP ${response.status}${message ? ` - ${message}` : ''}`);
|
|
948
|
+
}
|
|
949
|
+
const text = await response.text();
|
|
950
|
+
const etag = String(response.headers?.get('ETag') ?? response.headers?.get('etag') ?? '').trim();
|
|
951
|
+
const lastModified = String(response.headers?.get('Last-Modified') ?? response.headers?.get('last-modified') ?? '').trim();
|
|
952
|
+
this._agentMdCache.set(target, {
|
|
953
|
+
...(cached ?? {}),
|
|
954
|
+
text,
|
|
955
|
+
etag,
|
|
956
|
+
lastModified,
|
|
957
|
+
remote_etag: etag,
|
|
958
|
+
last_modified: lastModified,
|
|
959
|
+
});
|
|
960
|
+
return text;
|
|
961
|
+
}
|
|
962
|
+
async _headAgentMd(aid) {
|
|
963
|
+
const target = String(aid ?? '').trim();
|
|
964
|
+
if (!target)
|
|
965
|
+
throw new ValidationError('headAgentMd requires non-empty aid');
|
|
966
|
+
const response = await fetchWithTimeout(await this._resolveAgentMdUrl(target), {
|
|
967
|
+
method: 'HEAD',
|
|
968
|
+
headers: { Accept: 'text/markdown' },
|
|
969
|
+
redirect: 'follow',
|
|
970
|
+
}, 15_000);
|
|
971
|
+
const cached = this._agentMdCache.get(target) ?? {};
|
|
972
|
+
const etag = String(response.headers?.get('ETag') ?? response.headers?.get('etag') ?? '').trim();
|
|
973
|
+
const lastModified = String(response.headers?.get('Last-Modified') ?? response.headers?.get('last-modified') ?? '').trim();
|
|
974
|
+
if (response.status === 404) {
|
|
975
|
+
return { aid: target, found: false, etag: '', last_modified: '', status: 404 };
|
|
976
|
+
}
|
|
977
|
+
const resultEtag = response.status === 304 ? (etag || String(cached.etag ?? cached.remote_etag ?? '')) : etag;
|
|
978
|
+
const resultLastModified = response.status === 304 ? (lastModified || String(cached.lastModified ?? cached.last_modified ?? '')) : lastModified;
|
|
979
|
+
if (response.status < 200 || (response.status >= 300 && response.status !== 304)) {
|
|
980
|
+
throw new AUNError(`head agent.md failed: HTTP ${response.status}`);
|
|
981
|
+
}
|
|
982
|
+
this._agentMdCache.set(target, {
|
|
983
|
+
...cached,
|
|
984
|
+
etag: resultEtag,
|
|
985
|
+
lastModified: resultLastModified,
|
|
986
|
+
remote_etag: resultEtag,
|
|
987
|
+
last_modified: resultLastModified,
|
|
988
|
+
});
|
|
989
|
+
return { aid: target, found: true, etag: resultEtag, last_modified: resultLastModified, status: response.status };
|
|
990
|
+
}
|
|
991
|
+
async _verifyAgentMd(content, aid, certPem) {
|
|
992
|
+
const target = String(aid ?? '').trim();
|
|
993
|
+
if (!target)
|
|
994
|
+
throw new ValidationError('verifyAgentMd requires non-empty aid');
|
|
995
|
+
let peer = target === this._currentAid?.aid ? this._currentAid : null;
|
|
996
|
+
if (!peer) {
|
|
997
|
+
let resolvedCert = String(certPem ?? '').trim();
|
|
998
|
+
if (!resolvedCert) {
|
|
999
|
+
try {
|
|
1000
|
+
resolvedCert = String(this._keystore.loadCert(target) ?? '').trim();
|
|
1001
|
+
}
|
|
1002
|
+
catch {
|
|
1003
|
+
resolvedCert = '';
|
|
1004
|
+
}
|
|
1005
|
+
}
|
|
1006
|
+
if (!resolvedCert) {
|
|
1007
|
+
if (!this._gatewayUrl) {
|
|
1008
|
+
try {
|
|
1009
|
+
this._gatewayUrl = await this._resolveGatewayForAid(target);
|
|
1010
|
+
}
|
|
1011
|
+
catch { /* best-effort before cert fetch */ }
|
|
1012
|
+
}
|
|
1013
|
+
resolvedCert = String(await this._fetchPeerCert(target) ?? '').trim();
|
|
1014
|
+
}
|
|
1015
|
+
if (!resolvedCert)
|
|
1016
|
+
throw new NotFoundError(`certificate not found for aid: ${target}`);
|
|
1017
|
+
peer = AID._create({
|
|
1018
|
+
aid: target,
|
|
1019
|
+
aunPath: this._configModel.aunPath,
|
|
1020
|
+
certPem: resolvedCert,
|
|
1021
|
+
privateKeyPem: null,
|
|
1022
|
+
certValid: true,
|
|
1023
|
+
privateKeyValid: false,
|
|
1024
|
+
});
|
|
1025
|
+
}
|
|
1026
|
+
const result = peer.verifyAgentMd(content);
|
|
1027
|
+
if (!result.ok)
|
|
1028
|
+
throw new AUNError(result.error.message);
|
|
1029
|
+
return { ...result.data, verified: result.data.status === 'verified' };
|
|
1030
|
+
}
|
|
590
1031
|
/**
|
|
591
1032
|
* 读取 {agentMdPath}/{self_aid}/agent.md,签名后上传,并把签名结果原子写回本地。
|
|
592
1033
|
*/
|
|
@@ -596,14 +1037,18 @@ export class AUNClient {
|
|
|
596
1037
|
throw new ValidationError('publishAgentMd requires local AID');
|
|
597
1038
|
}
|
|
598
1039
|
const content = this._readAgentMdContent(target);
|
|
599
|
-
const signed =
|
|
600
|
-
|
|
601
|
-
|
|
1040
|
+
const signed = this._currentAid?.signAgentMd(content);
|
|
1041
|
+
if (!signed?.ok) {
|
|
1042
|
+
throw new StateError(signed?.error.message ?? 'publishAgentMd requires a valid local AID private key');
|
|
1043
|
+
}
|
|
1044
|
+
const signedContent = signed.data.signed;
|
|
1045
|
+
const result = await this._uploadAgentMd(signedContent);
|
|
1046
|
+
this._localAgentMdEtag = this._agentMdContentEtag(signedContent);
|
|
602
1047
|
const remoteEtag = isJsonObject(result) ? String(result.etag ?? '').trim() : '';
|
|
603
1048
|
if (remoteEtag)
|
|
604
1049
|
this._remoteAgentMdEtag = remoteEtag;
|
|
605
1050
|
this._saveAgentMdRecord(target, {
|
|
606
|
-
content:
|
|
1051
|
+
content: signedContent,
|
|
607
1052
|
local_etag: this._localAgentMdEtag,
|
|
608
1053
|
remote_etag: remoteEtag || undefined,
|
|
609
1054
|
last_modified: isJsonObject(result) ? String(result.last_modified ?? '').trim() : '',
|
|
@@ -613,16 +1058,6 @@ export class AUNClient {
|
|
|
613
1058
|
});
|
|
614
1059
|
return result;
|
|
615
1060
|
}
|
|
616
|
-
/**
|
|
617
|
-
* 下载 agent.md 并自动验签;内容固定保存到 {agentMdPath}/{aid}/agent.md。
|
|
618
|
-
*/
|
|
619
|
-
async fetchAgentMd(aid) {
|
|
620
|
-
const target = String(aid ?? this._aid ?? '').trim();
|
|
621
|
-
if (!target) {
|
|
622
|
-
throw new ValidationError('fetchAgentMd requires aid (or local AID)');
|
|
623
|
-
}
|
|
624
|
-
return await this._startAgentMdFetchTask(target);
|
|
625
|
-
}
|
|
626
1061
|
async _startAgentMdFetchTask(target) {
|
|
627
1062
|
const existing = this._agentMdFetchInflight.get(target);
|
|
628
1063
|
if (existing) {
|
|
@@ -638,8 +1073,8 @@ export class AUNClient {
|
|
|
638
1073
|
return await task;
|
|
639
1074
|
}
|
|
640
1075
|
async _fetchAgentMdOnce(target) {
|
|
641
|
-
const content = await this.
|
|
642
|
-
const signature = await this.
|
|
1076
|
+
const content = await this._downloadAgentMd(target);
|
|
1077
|
+
const signature = await this._verifyAgentMd(content, target);
|
|
643
1078
|
const isSelf = target === (this._aid ?? '');
|
|
644
1079
|
const localEtag = this._agentMdContentEtag(content);
|
|
645
1080
|
const cacheMeta = this._agentMdAuthCacheMeta(target);
|
|
@@ -678,7 +1113,7 @@ export class AUNClient {
|
|
|
678
1113
|
/**
|
|
679
1114
|
* 设置 agent.md 本地存储根目录;为空时恢复默认 {aun_path}/AIDs。
|
|
680
1115
|
*/
|
|
681
|
-
|
|
1116
|
+
_setAgentMdRoot(root) {
|
|
682
1117
|
const raw = String(root ?? '').trim();
|
|
683
1118
|
const next = raw || path.join(this._configModel.aunPath, 'AIDs');
|
|
684
1119
|
fs.mkdirSync(next, { recursive: true });
|
|
@@ -686,9 +1121,6 @@ export class AUNClient {
|
|
|
686
1121
|
this._agentMdCache.clear();
|
|
687
1122
|
return this._agentMdPath;
|
|
688
1123
|
}
|
|
689
|
-
SetAgentMDPath(root) {
|
|
690
|
-
return this.setAgentMdPath(root);
|
|
691
|
-
}
|
|
692
1124
|
/**
|
|
693
1125
|
* 记录本地 agent.md 文件路径并一次性计算 etag(quoted sha256,与服务端一致)。
|
|
694
1126
|
*
|
|
@@ -886,8 +1318,7 @@ export class AUNClient {
|
|
|
886
1318
|
}
|
|
887
1319
|
_agentMdAuthCacheMeta(aid) {
|
|
888
1320
|
try {
|
|
889
|
-
const
|
|
890
|
-
const record = store?.get(String(aid ?? '').trim());
|
|
1321
|
+
const record = this._agentMdCache.get(String(aid ?? '').trim());
|
|
891
1322
|
return record && typeof record === 'object' ? { ...record } : {};
|
|
892
1323
|
}
|
|
893
1324
|
catch {
|
|
@@ -1007,7 +1438,7 @@ export class AUNClient {
|
|
|
1007
1438
|
return;
|
|
1008
1439
|
if (this._agentMdFetchInflight.has(target))
|
|
1009
1440
|
return;
|
|
1010
|
-
void this.
|
|
1441
|
+
void this._startAgentMdFetchTask(target).catch((err) => {
|
|
1011
1442
|
this._saveAgentMdRecord(target, {
|
|
1012
1443
|
last_error: err instanceof Error ? err.message : String(err),
|
|
1013
1444
|
remote_status: 'found',
|
|
@@ -1063,7 +1494,7 @@ export class AUNClient {
|
|
|
1063
1494
|
}
|
|
1064
1495
|
this._observeAgentMdMeta(senderAid, String(sender.etag ?? '').trim(), String(sender.last_modified ?? sender.lastModified ?? '').trim(), 'envelope');
|
|
1065
1496
|
}
|
|
1066
|
-
async
|
|
1497
|
+
async _checkAgentMdCache(aid, maxUnsyncedDays = 1) {
|
|
1067
1498
|
const target = String(aid ?? this._aid ?? '').trim();
|
|
1068
1499
|
if (!target)
|
|
1069
1500
|
throw new ValidationError('checkAgentMd requires aid (or local AID)');
|
|
@@ -1114,7 +1545,7 @@ export class AUNClient {
|
|
|
1114
1545
|
const now = Date.now();
|
|
1115
1546
|
let remote;
|
|
1116
1547
|
try {
|
|
1117
|
-
remote = await this.
|
|
1548
|
+
remote = await this._headAgentMd(target);
|
|
1118
1549
|
}
|
|
1119
1550
|
catch (err) {
|
|
1120
1551
|
this._saveAgentMdRecord(target, { checked_at: now, remote_status: 'error', last_error: err instanceof Error ? err.message : String(err) });
|
|
@@ -1169,42 +1600,79 @@ export class AUNClient {
|
|
|
1169
1600
|
}
|
|
1170
1601
|
/** 连接状态 */
|
|
1171
1602
|
get state() {
|
|
1172
|
-
return this._state;
|
|
1603
|
+
return this._publicState(this._state);
|
|
1604
|
+
}
|
|
1605
|
+
_publicState(state) {
|
|
1606
|
+
return STATE_TO_PUBLIC[state] ?? state;
|
|
1173
1607
|
}
|
|
1174
1608
|
/** 最近一次 gateway health check 结果,null 表示尚未检查 */
|
|
1175
1609
|
get gatewayHealth() {
|
|
1176
1610
|
return this._discovery.lastHealthy;
|
|
1177
1611
|
}
|
|
1178
|
-
|
|
1179
|
-
|
|
1612
|
+
// ── 生命周期 ──────────────────────────────────────────────
|
|
1613
|
+
/** 仅认证当前身份,获取/刷新 token,但不建立长连接。 */
|
|
1614
|
+
async authenticate(options = {}) {
|
|
1180
1615
|
const tStart = Date.now();
|
|
1181
|
-
this.
|
|
1616
|
+
const target = this._currentAid?.aid ?? this._aid ?? '';
|
|
1617
|
+
if (!target || !this._currentAid?.isPrivateKeyValid()) {
|
|
1618
|
+
throw new StateError('authenticate requires a loaded AID with a valid private key');
|
|
1619
|
+
}
|
|
1620
|
+
const publicState = this.state;
|
|
1621
|
+
if (publicState !== ConnectionState.STANDBY && publicState !== ConnectionState.AUTHENTICATED) {
|
|
1622
|
+
throw new StateError(`authenticate not allowed in state ${publicState}`);
|
|
1623
|
+
}
|
|
1624
|
+
if ('aid' in options || 'access_token' in options || 'token' in options || 'kite_token' in options) {
|
|
1625
|
+
throw new ValidationError('authenticate options must not include aid or token fields; load an AID object first');
|
|
1626
|
+
}
|
|
1627
|
+
this._state = 'connecting';
|
|
1182
1628
|
try {
|
|
1183
|
-
const
|
|
1184
|
-
this.
|
|
1629
|
+
const gateway = String(options.gateway ?? this._gatewayUrl ?? await this._resolveGatewayForAid(target)).trim();
|
|
1630
|
+
const result = await this._auth.authenticate(gateway, { aid: target });
|
|
1631
|
+
this._gatewayUrl = String(result.gateway ?? gateway);
|
|
1632
|
+
this._identity = this._auth.loadIdentityOrNone(target);
|
|
1633
|
+
this._state = 'authenticated';
|
|
1634
|
+
this._lastError = null;
|
|
1635
|
+
this._lastErrorCode = null;
|
|
1636
|
+
this._clientLog.debug(`authenticate exit: elapsed=${Date.now() - tStart}ms aid=${target}`);
|
|
1185
1637
|
return result;
|
|
1186
1638
|
}
|
|
1187
1639
|
catch (err) {
|
|
1188
|
-
this.
|
|
1640
|
+
this._state = 'standby';
|
|
1641
|
+
this._lastError = err instanceof Error ? err : new Error(String(err));
|
|
1642
|
+
this._lastErrorCode = 'AUTHENTICATE_FAILED';
|
|
1643
|
+
this._clientLog.debug(`authenticate exit (error): elapsed=${Date.now() - tStart}ms err=${err instanceof Error ? err.message : String(err)}`);
|
|
1189
1644
|
throw err;
|
|
1190
1645
|
}
|
|
1191
1646
|
}
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
* 连接到 Gateway。
|
|
1195
|
-
*
|
|
1196
|
-
* @param auth - 认证参数(必须包含 access_token 和 gateway)
|
|
1197
|
-
* @param options - 会话选项(auto_reconnect、heartbeat_interval 等)
|
|
1198
|
-
*/
|
|
1199
|
-
async connect(auth, options) {
|
|
1647
|
+
/** 连接到 Gateway;身份来自构造函数或 loadIdentity(aid),认证由 SDK 内部自动完成。 */
|
|
1648
|
+
async connect(options = {}) {
|
|
1200
1649
|
const tStart = Date.now();
|
|
1201
|
-
if (
|
|
1202
|
-
throw new
|
|
1650
|
+
if (arguments.length > 1) {
|
|
1651
|
+
throw new ValidationError('connect accepts a single options object');
|
|
1652
|
+
}
|
|
1653
|
+
if ('aid' in options || 'access_token' in options || 'token' in options || 'kite_token' in options) {
|
|
1654
|
+
throw new ValidationError('connect options must not include aid or token fields; load an AID object first');
|
|
1655
|
+
}
|
|
1656
|
+
const target = this._currentAid?.aid ?? this._aid ?? '';
|
|
1657
|
+
if (!target || !this._currentAid?.isPrivateKeyValid()) {
|
|
1658
|
+
throw new StateError('connect requires a loaded AID with a valid private key');
|
|
1659
|
+
}
|
|
1660
|
+
const publicState = this.state;
|
|
1661
|
+
const allowed = new Set([
|
|
1662
|
+
ConnectionState.STANDBY,
|
|
1663
|
+
ConnectionState.AUTHENTICATED,
|
|
1664
|
+
ConnectionState.RETRY_BACKOFF,
|
|
1665
|
+
ConnectionState.CONNECTION_FAILED,
|
|
1666
|
+
]);
|
|
1667
|
+
if (!allowed.has(publicState)) {
|
|
1668
|
+
throw new StateError(`connect not allowed in state ${publicState}`);
|
|
1669
|
+
}
|
|
1670
|
+
if (publicState === ConnectionState.RETRY_BACKOFF) {
|
|
1671
|
+
this._stopReconnect();
|
|
1203
1672
|
}
|
|
1204
1673
|
this._state = 'connecting';
|
|
1205
|
-
const
|
|
1206
|
-
|
|
1207
|
-
Object.assign(params, options);
|
|
1674
|
+
const gateway = String(options.gateway ?? this._gatewayUrl ?? await this._resolveGatewayForAid(target)).trim();
|
|
1675
|
+
const params = { ...options, gateway };
|
|
1208
1676
|
const normalized = this._normalizeConnectParams(params);
|
|
1209
1677
|
this._captureCapabilitiesFromConnect(normalized);
|
|
1210
1678
|
this._sessionParams = normalized;
|
|
@@ -1218,7 +1686,9 @@ export class AUNClient {
|
|
|
1218
1686
|
for (const gw of gateways) {
|
|
1219
1687
|
try {
|
|
1220
1688
|
const gwParams = { ...normalized, gateway: gw };
|
|
1221
|
-
await this._connectOnce(gwParams,
|
|
1689
|
+
await this._connectOnce(gwParams, true);
|
|
1690
|
+
this._lastError = null;
|
|
1691
|
+
this._lastErrorCode = null;
|
|
1222
1692
|
this._clientLog.debug(`connect exit: elapsed=${Date.now() - tStart}ms aid=${this._aid ?? ''}, state=${this._state}`);
|
|
1223
1693
|
return;
|
|
1224
1694
|
}
|
|
@@ -1227,14 +1697,15 @@ export class AUNClient {
|
|
|
1227
1697
|
if (gateways.length > 1) {
|
|
1228
1698
|
this._clientLog.warn(`connect: gateway ${gw} failed, trying next: ${formatCaughtError(err)}`);
|
|
1229
1699
|
}
|
|
1230
|
-
if (this._state
|
|
1700
|
+
if (this._state !== 'closed')
|
|
1231
1701
|
this._state = 'connecting';
|
|
1232
|
-
}
|
|
1233
1702
|
}
|
|
1234
1703
|
}
|
|
1235
1704
|
if (this._state === 'connecting' || this._state === 'authenticating') {
|
|
1236
|
-
this._state = '
|
|
1705
|
+
this._state = 'connection_failed';
|
|
1237
1706
|
}
|
|
1707
|
+
this._lastError = lastErr instanceof Error ? lastErr : new Error(String(lastErr));
|
|
1708
|
+
this._lastErrorCode = 'CONNECT_FAILED';
|
|
1238
1709
|
this._clientLog.error(`connect failed: ${formatCaughtError(lastErr)}`, lastErr instanceof Error ? lastErr : undefined);
|
|
1239
1710
|
this._clientLog.debug(`connect exit (error): elapsed=${Date.now() - tStart}ms err=${lastErr instanceof Error ? lastErr.message : String(lastErr)}`);
|
|
1240
1711
|
throw lastErr;
|
|
@@ -1248,13 +1719,13 @@ export class AUNClient {
|
|
|
1248
1719
|
this._saveSeqTrackerState();
|
|
1249
1720
|
this._stopBackgroundTasks();
|
|
1250
1721
|
this._stopReconnect();
|
|
1251
|
-
if (this.
|
|
1722
|
+
if (this.state === ConnectionState.NO_IDENTITY || this.state === ConnectionState.CLOSED) {
|
|
1252
1723
|
const closableKeyStore = this._keystore;
|
|
1253
1724
|
closableKeyStore.close?.();
|
|
1254
1725
|
this._state = 'closed';
|
|
1255
1726
|
this._logger.close();
|
|
1256
1727
|
this._resetSeqTrackingState();
|
|
1257
|
-
this._clientLog.debug(`close exit: elapsed=${Date.now() - tStart}ms (was
|
|
1728
|
+
this._clientLog.debug(`close exit: elapsed=${Date.now() - tStart}ms (was no_identity/closed)`);
|
|
1258
1729
|
return;
|
|
1259
1730
|
}
|
|
1260
1731
|
await this._transport.close();
|
|
@@ -1262,7 +1733,7 @@ export class AUNClient {
|
|
|
1262
1733
|
closableKeyStore.close?.();
|
|
1263
1734
|
this._state = 'closed';
|
|
1264
1735
|
this._logger.close();
|
|
1265
|
-
await this._dispatcher.publish('connection.state', { state: this._state });
|
|
1736
|
+
await this._dispatcher.publish('connection.state', { state: this._publicState(this._state) });
|
|
1266
1737
|
this._resetSeqTrackingState();
|
|
1267
1738
|
this._clientLog.debug(`close exit: elapsed=${Date.now() - tStart}ms`);
|
|
1268
1739
|
}
|
|
@@ -1284,7 +1755,14 @@ export class AUNClient {
|
|
|
1284
1755
|
this._clientLog.debug(`disconnect exit: elapsed=${Date.now() - tStart}ms (closing)`);
|
|
1285
1756
|
return;
|
|
1286
1757
|
}
|
|
1287
|
-
if (
|
|
1758
|
+
if (![
|
|
1759
|
+
ConnectionState.AUTHENTICATED,
|
|
1760
|
+
ConnectionState.CONNECTING,
|
|
1761
|
+
ConnectionState.READY,
|
|
1762
|
+
ConnectionState.RETRY_BACKOFF,
|
|
1763
|
+
ConnectionState.RECONNECTING,
|
|
1764
|
+
ConnectionState.CONNECTION_FAILED,
|
|
1765
|
+
].includes(this.state)) {
|
|
1288
1766
|
this._clientLog.debug(`disconnect exit: elapsed=${Date.now() - tStart}ms (state=${this._state})`);
|
|
1289
1767
|
return;
|
|
1290
1768
|
}
|
|
@@ -1292,8 +1770,8 @@ export class AUNClient {
|
|
|
1292
1770
|
this._stopBackgroundTasks();
|
|
1293
1771
|
this._stopReconnect();
|
|
1294
1772
|
await this._transport.close();
|
|
1295
|
-
this._state = '
|
|
1296
|
-
await this._dispatcher.publish('connection.state', { state: this._state });
|
|
1773
|
+
this._state = 'standby';
|
|
1774
|
+
await this._dispatcher.publish('connection.state', { state: this._publicState(this._state) });
|
|
1297
1775
|
this._clientLog.debug(`disconnect exit: elapsed=${Date.now() - tStart}ms`);
|
|
1298
1776
|
}
|
|
1299
1777
|
catch (err) {
|
|
@@ -1301,83 +1779,6 @@ export class AUNClient {
|
|
|
1301
1779
|
throw err;
|
|
1302
1780
|
}
|
|
1303
1781
|
}
|
|
1304
|
-
/**
|
|
1305
|
-
* 列出本地身份摘要。
|
|
1306
|
-
*
|
|
1307
|
-
* @param opts.all=false(默认):仅返回严格校验通过的可用身份——
|
|
1308
|
-
* keypair 完整 + cert 公钥 == keypair 公钥 + cert 时间窗口有效
|
|
1309
|
-
* @param opts.all=true:返回所有 AIDs/ 子目录(不含 _pending/);
|
|
1310
|
-
* 每项含 valid=bool 和 reason=string 字段
|
|
1311
|
-
*/
|
|
1312
|
-
listIdentities(opts) {
|
|
1313
|
-
const tStart = Date.now();
|
|
1314
|
-
const includeAll = !!opts?.all;
|
|
1315
|
-
this._clientLog.debug(`listIdentities enter all=${includeAll}`);
|
|
1316
|
-
try {
|
|
1317
|
-
const listFn = this._keystore.listIdentities;
|
|
1318
|
-
if (typeof listFn !== 'function') {
|
|
1319
|
-
this._clientLog.debug(`listIdentities exit: elapsed=${Date.now() - tStart}ms (no_list_fn)`);
|
|
1320
|
-
return [];
|
|
1321
|
-
}
|
|
1322
|
-
const aids = listFn.call(this._keystore);
|
|
1323
|
-
const summaries = [];
|
|
1324
|
-
for (const aid of [...aids].sort()) {
|
|
1325
|
-
const { valid, reason } = this._validateLocalIdentity(aid);
|
|
1326
|
-
if (!includeAll && !valid)
|
|
1327
|
-
continue;
|
|
1328
|
-
const summary = { aid, valid };
|
|
1329
|
-
if (reason)
|
|
1330
|
-
summary.reason = reason;
|
|
1331
|
-
const loadMetadata = this._keystore.loadMetadata;
|
|
1332
|
-
if (typeof loadMetadata === 'function') {
|
|
1333
|
-
const md = loadMetadata.call(this._keystore, aid);
|
|
1334
|
-
if (md)
|
|
1335
|
-
summary.metadata = md;
|
|
1336
|
-
}
|
|
1337
|
-
summaries.push(summary);
|
|
1338
|
-
}
|
|
1339
|
-
this._clientLog.debug(`listIdentities exit: elapsed=${Date.now() - tStart}ms all=${includeAll} count=${summaries.length}`);
|
|
1340
|
-
return summaries;
|
|
1341
|
-
}
|
|
1342
|
-
catch (err) {
|
|
1343
|
-
this._clientLog.debug(`listIdentities exit (error): elapsed=${Date.now() - tStart}ms err=${err instanceof Error ? err.message : String(err)}`);
|
|
1344
|
-
throw err;
|
|
1345
|
-
}
|
|
1346
|
-
}
|
|
1347
|
-
/**
|
|
1348
|
-
* 严格校验本地身份的可用性。返回 {valid, reason}。
|
|
1349
|
-
* 4 项校验:keypair 完整 + cert 存在 + cert 公钥 == keypair 公钥 + cert 时间窗口有效。
|
|
1350
|
-
*/
|
|
1351
|
-
_validateLocalIdentity(aid) {
|
|
1352
|
-
const identity = this._keystore.loadIdentity(aid);
|
|
1353
|
-
if (!identity)
|
|
1354
|
-
return { valid: false, reason: 'no identity record' };
|
|
1355
|
-
const priv = String(identity.private_key_pem ?? '');
|
|
1356
|
-
const pubB64 = String(identity.public_key_der_b64 ?? '');
|
|
1357
|
-
const certPem = String(identity.cert ?? '');
|
|
1358
|
-
if (!priv || !pubB64)
|
|
1359
|
-
return { valid: false, reason: 'missing keypair' };
|
|
1360
|
-
if (!certPem)
|
|
1361
|
-
return { valid: false, reason: 'missing certificate' };
|
|
1362
|
-
try {
|
|
1363
|
-
const crypto = require('node:crypto');
|
|
1364
|
-
const cert = new crypto.X509Certificate(certPem);
|
|
1365
|
-
const certPubDer = cert.publicKey.export({ type: 'spki', format: 'der' });
|
|
1366
|
-
const localPubDer = Buffer.from(pubB64, 'base64');
|
|
1367
|
-
if (!certPubDer.equals(localPubDer)) {
|
|
1368
|
-
return { valid: false, reason: 'cert public key does not match keypair' };
|
|
1369
|
-
}
|
|
1370
|
-
const now = Date.now();
|
|
1371
|
-
if (now < new Date(cert.validFrom).getTime())
|
|
1372
|
-
return { valid: false, reason: 'cert not yet valid' };
|
|
1373
|
-
if (now > new Date(cert.validTo).getTime())
|
|
1374
|
-
return { valid: false, reason: 'cert expired' };
|
|
1375
|
-
return { valid: true, reason: '' };
|
|
1376
|
-
}
|
|
1377
|
-
catch (e) {
|
|
1378
|
-
return { valid: false, reason: `cert parse error: ${e instanceof Error ? e.message : String(e)}` };
|
|
1379
|
-
}
|
|
1380
|
-
}
|
|
1381
1782
|
// ── RPC ───────────────────────────────────────────────────
|
|
1382
1783
|
/**
|
|
1383
1784
|
* 发送 JSON-RPC 调用。
|
|
@@ -1387,7 +1788,7 @@ export class AUNClient {
|
|
|
1387
1788
|
const tStart = Date.now();
|
|
1388
1789
|
this._clientLog.debug(`call enter: method=${method}`);
|
|
1389
1790
|
try {
|
|
1390
|
-
if (this.
|
|
1791
|
+
if (this.state !== ConnectionState.READY) {
|
|
1391
1792
|
throw new ConnectionError('client is not connected');
|
|
1392
1793
|
}
|
|
1393
1794
|
if (INTERNAL_ONLY_METHODS.has(method)) {
|
|
@@ -1397,6 +1798,10 @@ export class AUNClient {
|
|
|
1397
1798
|
throw new PermissionError(`legacy E2EE method is removed in this SDK: ${method}`);
|
|
1398
1799
|
}
|
|
1399
1800
|
const p = { ...(params ?? {}) };
|
|
1801
|
+
if (this._instanceProtectedHeaders && PROTECTED_HEADERS_METHODS.has(method)) {
|
|
1802
|
+
const existing = isJsonObject(p.protected_headers) ? p.protected_headers : {};
|
|
1803
|
+
p.protected_headers = { ...this._instanceProtectedHeaders, ...existing };
|
|
1804
|
+
}
|
|
1400
1805
|
const rpcBackground = Boolean(p._rpc_background) || this._backgroundRpcDepth > 0;
|
|
1401
1806
|
delete p._rpc_background;
|
|
1402
1807
|
const runWithRpcPriority = async (operation) => {
|
|
@@ -1446,7 +1851,7 @@ export class AUNClient {
|
|
|
1446
1851
|
const encrypt = p.encrypt ?? true;
|
|
1447
1852
|
delete p.encrypt;
|
|
1448
1853
|
if (encrypt) {
|
|
1449
|
-
return await runWithRpcPriority(() => this.
|
|
1854
|
+
return await runWithRpcPriority(() => this._sendV2(String(p.to ?? ''), p.payload, {
|
|
1450
1855
|
messageId: String(p.message_id ?? '') || undefined,
|
|
1451
1856
|
timestamp: p.timestamp,
|
|
1452
1857
|
protectedHeaders: this._protectedHeadersFromParams(p),
|
|
@@ -1461,7 +1866,7 @@ export class AUNClient {
|
|
|
1461
1866
|
const encrypt = p.encrypt ?? true;
|
|
1462
1867
|
delete p.encrypt;
|
|
1463
1868
|
if (encrypt) {
|
|
1464
|
-
return await runWithRpcPriority(() => this.
|
|
1869
|
+
return await runWithRpcPriority(() => this._sendGroupV2(String(p.group_id ?? ''), p.payload, {
|
|
1465
1870
|
messageId: String(p.message_id ?? '') || undefined,
|
|
1466
1871
|
timestamp: p.timestamp,
|
|
1467
1872
|
protectedHeaders: this._protectedHeadersFromParams(p),
|
|
@@ -1493,23 +1898,24 @@ export class AUNClient {
|
|
|
1493
1898
|
if (method === 'message.pull' || method === 'message.v2.pull') {
|
|
1494
1899
|
await this._ensureV2SessionReady('message.pull');
|
|
1495
1900
|
const skipAutoAck = p._skip_auto_ack === true || p.skip_auto_ack === true;
|
|
1901
|
+
const force = p.force === true;
|
|
1496
1902
|
const afterSeq = Number(p.after_seq ?? 0) || 0;
|
|
1497
1903
|
const limit = Number(p.limit ?? 50) || 50;
|
|
1498
1904
|
const messages = skipAutoAck
|
|
1499
|
-
? await runWithRpcPriority(() => this.
|
|
1500
|
-
: await runWithRpcPriority(() => this.
|
|
1905
|
+
? await runWithRpcPriority(() => this._pullV2(afterSeq, limit, { skipAutoAck: true, gateLocked: true, force }))
|
|
1906
|
+
: await runWithRpcPriority(() => this._pullV2(afterSeq, limit, { gateLocked: true, force }));
|
|
1501
1907
|
return { messages };
|
|
1502
1908
|
}
|
|
1503
1909
|
if (method === 'message.ack' || method === 'message.v2.ack') {
|
|
1504
1910
|
await this._ensureV2SessionReady('message.ack');
|
|
1505
|
-
return await runWithRpcPriority(() => this.
|
|
1911
|
+
return await runWithRpcPriority(() => this._ackV2(Number(p.seq ?? p.up_to_seq ?? 0) || undefined));
|
|
1506
1912
|
}
|
|
1507
1913
|
if (method === 'group.pull' || method === 'group.v2.pull') {
|
|
1508
1914
|
if (!String(p.group_id ?? '').trim()) {
|
|
1509
1915
|
throw new ValidationError('group.pull requires group_id');
|
|
1510
1916
|
}
|
|
1511
1917
|
await this._ensureV2SessionReady('group.pull');
|
|
1512
|
-
const messages = await runWithRpcPriority(() => this.
|
|
1918
|
+
const messages = await runWithRpcPriority(() => this._pullGroupV2(String(p.group_id), Number(p.after_seq ?? p.after_message_seq ?? 0) || 0, Number(p.limit ?? 50) || 50, { gateLocked: true }));
|
|
1513
1919
|
return { messages };
|
|
1514
1920
|
}
|
|
1515
1921
|
if (method === 'group.ack_messages' || method === 'group.v2.ack') {
|
|
@@ -1517,7 +1923,7 @@ export class AUNClient {
|
|
|
1517
1923
|
throw new ValidationError('group.ack_messages requires group_id');
|
|
1518
1924
|
}
|
|
1519
1925
|
await this._ensureV2SessionReady('group.ack_messages');
|
|
1520
|
-
return await runWithRpcPriority(() => this.
|
|
1926
|
+
return await runWithRpcPriority(() => this._ackGroupV2(String(p.group_id), Number(p.seq ?? p.msg_seq ?? p.up_to_seq ?? 0) || undefined));
|
|
1521
1927
|
}
|
|
1522
1928
|
if (method === 'message.pull') {
|
|
1523
1929
|
delete p._skip_auto_ack;
|
|
@@ -1577,49 +1983,6 @@ export class AUNClient {
|
|
|
1577
1983
|
throw err;
|
|
1578
1984
|
}
|
|
1579
1985
|
}
|
|
1580
|
-
// ── 便利方法 ──────────────────────────────────────────────
|
|
1581
|
-
/** 心跳检测 */
|
|
1582
|
-
async ping(params) {
|
|
1583
|
-
const tStart = Date.now();
|
|
1584
|
-
this._clientLog.debug(`ping enter`);
|
|
1585
|
-
try {
|
|
1586
|
-
const result = await this.call('meta.ping', params ?? {});
|
|
1587
|
-
this._clientLog.debug(`ping exit: elapsed=${Date.now() - tStart}ms`);
|
|
1588
|
-
return result;
|
|
1589
|
-
}
|
|
1590
|
-
catch (err) {
|
|
1591
|
-
this._clientLog.debug(`ping exit (error): elapsed=${Date.now() - tStart}ms err=${err instanceof Error ? err.message : String(err)}`);
|
|
1592
|
-
throw err;
|
|
1593
|
-
}
|
|
1594
|
-
}
|
|
1595
|
-
/** 获取服务端状态 */
|
|
1596
|
-
async status(params) {
|
|
1597
|
-
const tStart = Date.now();
|
|
1598
|
-
this._clientLog.debug(`status enter`);
|
|
1599
|
-
try {
|
|
1600
|
-
const result = await this.call('meta.status', params ?? {});
|
|
1601
|
-
this._clientLog.debug(`status exit: elapsed=${Date.now() - tStart}ms`);
|
|
1602
|
-
return result;
|
|
1603
|
-
}
|
|
1604
|
-
catch (err) {
|
|
1605
|
-
this._clientLog.debug(`status exit (error): elapsed=${Date.now() - tStart}ms err=${err instanceof Error ? err.message : String(err)}`);
|
|
1606
|
-
throw err;
|
|
1607
|
-
}
|
|
1608
|
-
}
|
|
1609
|
-
/** 获取信任根证书列表 */
|
|
1610
|
-
async trustRoots(params) {
|
|
1611
|
-
const tStart = Date.now();
|
|
1612
|
-
this._clientLog.debug(`trustRoots enter`);
|
|
1613
|
-
try {
|
|
1614
|
-
const result = await this.call('meta.trust_roots', params ?? {});
|
|
1615
|
-
this._clientLog.debug(`trustRoots exit: elapsed=${Date.now() - tStart}ms`);
|
|
1616
|
-
return result;
|
|
1617
|
-
}
|
|
1618
|
-
catch (err) {
|
|
1619
|
-
this._clientLog.debug(`trustRoots exit (error): elapsed=${Date.now() - tStart}ms err=${err instanceof Error ? err.message : String(err)}`);
|
|
1620
|
-
throw err;
|
|
1621
|
-
}
|
|
1622
|
-
}
|
|
1623
1986
|
// ── 事件 ──────────────────────────────────────────────────
|
|
1624
1987
|
/** 订阅事件 */
|
|
1625
1988
|
on(event, handler) {
|
|
@@ -1762,6 +2125,7 @@ export class AUNClient {
|
|
|
1762
2125
|
this._clientLog.debug(`P2P push filtered by instance: message_id=${String(msg.message_id ?? '')}, seq=${String(msg.seq ?? '')}, target_device=${String(msg.device_id ?? '')}, target_slot=${String(msg.slot_id ?? '')}, local_device=${this._deviceId}, local_slot=${this._slotId}`);
|
|
1763
2126
|
return;
|
|
1764
2127
|
}
|
|
2128
|
+
const encryptedPush = this._isEncryptedPushMessage(msg);
|
|
1765
2129
|
// P2P 空洞检测
|
|
1766
2130
|
const seq = msg.seq;
|
|
1767
2131
|
if (seq !== undefined && seq !== null && this._aid) {
|
|
@@ -1770,7 +2134,9 @@ export class AUNClient {
|
|
|
1770
2134
|
if (seq > 0)
|
|
1771
2135
|
this._seqTracker.updateMaxSeen(ns, seq);
|
|
1772
2136
|
const contigBefore = this._seqTracker.getContiguousSeq(ns);
|
|
1773
|
-
const published =
|
|
2137
|
+
const published = encryptedPush
|
|
2138
|
+
? await this._publishEncryptedPushMessage('message.received', 'message.undecryptable', ns, seq, msg, false)
|
|
2139
|
+
: await this._publishOrderedMessage('message.received', ns, seq, msg);
|
|
1774
2140
|
const contigAfter = this._seqTracker.getContiguousSeq(ns);
|
|
1775
2141
|
const needPull = Number(seq) > contigAfter && !published;
|
|
1776
2142
|
if (needPull) {
|
|
@@ -1783,15 +2149,21 @@ export class AUNClient {
|
|
|
1783
2149
|
const maxSeen = this._seqTracker.getMaxSeenSeq(ns);
|
|
1784
2150
|
const ackSeq = maxSeen > 0 ? Math.min(contig, maxSeen) : contig;
|
|
1785
2151
|
this._clientLog.debug(`P2P push auto-ack send: ns=${ns}, seq=${ackSeq}, contiguous=${contig}, max_seen=${maxSeen}`);
|
|
1786
|
-
this._withBackgroundRpc(() => this.
|
|
2152
|
+
this._withBackgroundRpc(() => this._ackV2(ackSeq))
|
|
1787
2153
|
.then(() => { this._clientLog.debug(`P2P push auto-ack ok: ns=${ns}, seq=${ackSeq}`); })
|
|
1788
2154
|
.catch((e) => { this._clientLog.debug(`P2P auto-ack failed: ${formatCaughtError(e)}`); });
|
|
1789
2155
|
}
|
|
1790
2156
|
// 即时持久化 cursor,异常断连后不回退
|
|
1791
2157
|
if (contigAfter !== contigBefore)
|
|
1792
2158
|
this._saveSeqTrackerState();
|
|
2159
|
+
if (encryptedPush)
|
|
2160
|
+
return;
|
|
1793
2161
|
}
|
|
1794
2162
|
else {
|
|
2163
|
+
if (encryptedPush) {
|
|
2164
|
+
await this._publishEncryptedPushMessage('message.received', 'message.undecryptable', '', seq ?? 0, msg, false);
|
|
2165
|
+
return;
|
|
2166
|
+
}
|
|
1795
2167
|
// V2-only:普通 _raw.message.received 只承载明文;V2 密文由 peer.v2.message_received 通知触发 pull。
|
|
1796
2168
|
await this._publishAppEvent('message.received', msg, 'push');
|
|
1797
2169
|
}
|
|
@@ -1850,13 +2222,16 @@ export class AUNClient {
|
|
|
1850
2222
|
});
|
|
1851
2223
|
return;
|
|
1852
2224
|
}
|
|
2225
|
+
const encryptedPush = this._isEncryptedPushMessage(msg);
|
|
1853
2226
|
if (groupId && seq !== undefined && seq !== null) {
|
|
1854
2227
|
const ns = `group:${groupId}`;
|
|
1855
2228
|
// Push 只先更新 maxSeenSeq;contiguous_seq 是已交付游标,必须等应用层发布返回后再推进。
|
|
1856
2229
|
if (seq > 0)
|
|
1857
2230
|
this._seqTracker.updateMaxSeen(ns, seq);
|
|
1858
2231
|
const contigBefore = this._seqTracker.getContiguousSeq(ns);
|
|
1859
|
-
const published =
|
|
2232
|
+
const published = encryptedPush
|
|
2233
|
+
? await this._publishEncryptedPushMessage('group.message_created', 'group.message_undecryptable', ns, seq, msg, true)
|
|
2234
|
+
: await this._publishOrderedMessage('group.message_created', ns, seq, msg);
|
|
1860
2235
|
const contigAfter = this._seqTracker.getContiguousSeq(ns);
|
|
1861
2236
|
const needPull = Number(seq) > contigAfter && !published;
|
|
1862
2237
|
if (needPull) {
|
|
@@ -1868,14 +2243,20 @@ export class AUNClient {
|
|
|
1868
2243
|
const maxSeen = this._seqTracker.getMaxSeenSeq(ns);
|
|
1869
2244
|
const ackSeq = maxSeen > 0 ? Math.min(contig, maxSeen) : contig;
|
|
1870
2245
|
this._clientLog.debug(`group push auto-ack send: group=${groupId}, ns=${ns}, seq=${ackSeq}, contiguous=${contig}, max_seen=${maxSeen}`);
|
|
1871
|
-
this._withBackgroundRpc(() => this.
|
|
2246
|
+
this._withBackgroundRpc(() => this._ackGroupV2(groupId, ackSeq))
|
|
1872
2247
|
.then(() => { this._clientLog.debug(`group push auto-ack ok: group=${groupId}, seq=${ackSeq}`); })
|
|
1873
2248
|
.catch((e) => { this._clientLog.debug(`group message auto-ack failed: group=${groupId} ${formatCaughtError(e)}`); });
|
|
1874
2249
|
}
|
|
1875
2250
|
if (contigAfter !== contigBefore)
|
|
1876
2251
|
this._saveSeqTrackerState();
|
|
2252
|
+
if (encryptedPush)
|
|
2253
|
+
return;
|
|
1877
2254
|
}
|
|
1878
2255
|
else {
|
|
2256
|
+
if (encryptedPush) {
|
|
2257
|
+
await this._publishEncryptedPushMessage('group.message_created', 'group.message_undecryptable', '', seq ?? 0, msg, true);
|
|
2258
|
+
return;
|
|
2259
|
+
}
|
|
1879
2260
|
// V2-only:普通 group.message_created 只承载明文;V2 密文由 group.v2.message_created 通知触发 pull。
|
|
1880
2261
|
await this._publishAppEvent('group.message_created', msg, 'group-push');
|
|
1881
2262
|
}
|
|
@@ -1893,7 +2274,7 @@ export class AUNClient {
|
|
|
1893
2274
|
this._clientLog.debug(`auto pull group messages start: group=${groupId}, after_seq=${afterSeq}, seq=${String(notification.seq ?? '')}`);
|
|
1894
2275
|
const started = await this._tryRunBackgroundPull(ns, async () => {
|
|
1895
2276
|
const pullAfterSeq = this._seqTracker.getContiguousSeq(ns);
|
|
1896
|
-
const messages = await this.
|
|
2277
|
+
const messages = await this._pullGroupV2(groupId, pullAfterSeq, 50, { gateLocked: true });
|
|
1897
2278
|
this._prunePushedSeqs(ns);
|
|
1898
2279
|
return messages.length;
|
|
1899
2280
|
}, true);
|
|
@@ -1921,7 +2302,7 @@ export class AUNClient {
|
|
|
1921
2302
|
this._clientLog.debug(`group message gap fill start: group=${groupId}, after_seq=${afterSeq}`);
|
|
1922
2303
|
let filled = 0;
|
|
1923
2304
|
try {
|
|
1924
|
-
const messages = await this._withBackgroundRpc(() => this.
|
|
2305
|
+
const messages = await this._withBackgroundRpc(() => this._pullGroupV2(groupId, afterSeq, 50, { gateLocked: true }));
|
|
1925
2306
|
filled = messages.length;
|
|
1926
2307
|
this._prunePushedSeqs(ns);
|
|
1927
2308
|
if (this._seqTracker.getContiguousSeq(ns) !== afterSeq) {
|
|
@@ -1960,7 +2341,7 @@ export class AUNClient {
|
|
|
1960
2341
|
this._clientLog.debug(`P2P message gap fill start: after_seq=${afterSeq}`);
|
|
1961
2342
|
let filled = 0;
|
|
1962
2343
|
try {
|
|
1963
|
-
const messages = await this._withBackgroundRpc(() => this.
|
|
2344
|
+
const messages = await this._withBackgroundRpc(() => this._pullV2(afterSeq, 50, { skipAutoAck: true, gateLocked: true }));
|
|
1964
2345
|
filled = messages.length;
|
|
1965
2346
|
this._prunePushedSeqs(ns);
|
|
1966
2347
|
if (this._seqTracker.getContiguousSeq(ns) !== afterSeq) {
|
|
@@ -1969,7 +2350,7 @@ export class AUNClient {
|
|
|
1969
2350
|
}
|
|
1970
2351
|
const contig = this._seqTracker.getContiguousSeq(ns);
|
|
1971
2352
|
if (contig > 0 && contig !== afterSeq) {
|
|
1972
|
-
await this._withBackgroundRpc(() => this.
|
|
2353
|
+
await this._withBackgroundRpc(() => this._ackV2(contig));
|
|
1973
2354
|
}
|
|
1974
2355
|
this._clientLog.debug(`P2P message gap fill done: after_seq=${afterSeq}, filled=${filled}`);
|
|
1975
2356
|
}
|
|
@@ -2017,7 +2398,7 @@ export class AUNClient {
|
|
|
2017
2398
|
this._clientLog.debug(`P2P pending pull upper already covered: ns=${ns}, upper_seq=${upperSeq}, contiguous=${contig}, reason=${reason}`);
|
|
2018
2399
|
return false;
|
|
2019
2400
|
}
|
|
2020
|
-
if (this.
|
|
2401
|
+
if (this.state !== ConnectionState.READY || this._closing) {
|
|
2021
2402
|
this._clientLog.debug(`P2P pending pull postponed: ns=${ns}, upper_seq=${upperSeq}, contiguous=${contig}, state=${this._state}, closing=${this._closing}, reason=${reason}`);
|
|
2022
2403
|
return false;
|
|
2023
2404
|
}
|
|
@@ -2408,14 +2789,14 @@ export class AUNClient {
|
|
|
2408
2789
|
try {
|
|
2409
2790
|
await this._withBackgroundRpc(async () => {
|
|
2410
2791
|
if (method === 'message.pull' || method === 'message.v2.pull') {
|
|
2411
|
-
await this.
|
|
2792
|
+
await this._pullV2(Number(next.after_seq ?? 0) || 0, Number(next.limit ?? 50) || 50);
|
|
2412
2793
|
return;
|
|
2413
2794
|
}
|
|
2414
2795
|
if (method === 'group.pull' || method === 'group.v2.pull') {
|
|
2415
2796
|
const groupId = String(next.group_id ?? '').trim();
|
|
2416
2797
|
if (!groupId)
|
|
2417
2798
|
return;
|
|
2418
|
-
await this.
|
|
2799
|
+
await this._pullGroupV2(groupId, Number(next.after_seq ?? next.after_message_seq ?? 0) || 0, Number(next.limit ?? 50) || 50);
|
|
2419
2800
|
return;
|
|
2420
2801
|
}
|
|
2421
2802
|
await this.call(method, next);
|
|
@@ -3660,10 +4041,10 @@ export class AUNClient {
|
|
|
3660
4041
|
this._applyServerHeartbeatInterval(hello.heartbeat_interval, 'auth');
|
|
3661
4042
|
}
|
|
3662
4043
|
}
|
|
3663
|
-
this._state = '
|
|
4044
|
+
this._state = 'ready';
|
|
3664
4045
|
this._connectedAt = Date.now();
|
|
3665
4046
|
this._clientLog.debug(`auth complete, connection ready: aid=${this._aid ?? ''}, gateway=${gatewayUrl}`);
|
|
3666
|
-
await this._dispatcher.publish('connection.state', { state: this._state, gateway: gatewayUrl });
|
|
4047
|
+
await this._dispatcher.publish('connection.state', { state: this._publicState(this._state), gateway: gatewayUrl });
|
|
3667
4048
|
// auth 阶段 aid 可能被 identity 覆盖(上方 this._aid = identity.aid);
|
|
3668
4049
|
// 若 context 发生变化,重新 refresh + restore,保持 tracker 与真实身份一致。
|
|
3669
4050
|
if (this._seqTrackerContext !== this._currentSeqTrackerContext()) {
|
|
@@ -3673,7 +4054,7 @@ export class AUNClient {
|
|
|
3673
4054
|
this._startBackgroundTasks();
|
|
3674
4055
|
// V2 E2EE:初始化 session 并注册本设备 SPK。
|
|
3675
4056
|
try {
|
|
3676
|
-
await this.
|
|
4057
|
+
await this._initV2Session();
|
|
3677
4058
|
}
|
|
3678
4059
|
catch (exc) {
|
|
3679
4060
|
this._clientLog.warn(`V2 session init failed (non-fatal): ${formatCaughtError(exc)}`);
|
|
@@ -3686,7 +4067,7 @@ export class AUNClient {
|
|
|
3686
4067
|
this._clientLog.debug(`_connectOnce exit: elapsed=${Date.now() - tStart}ms gateway=${gatewayUrl}, aid=${this._aid ?? ''}`);
|
|
3687
4068
|
}
|
|
3688
4069
|
catch (err) {
|
|
3689
|
-
this._state = prevState === 'connected' ? '
|
|
4070
|
+
this._state = (prevState === 'connected' || prevState === 'ready') ? 'standby' : (this._currentAid ? 'standby' : 'no_identity');
|
|
3690
4071
|
this._clientLog.debug(`_connectOnce exit (error): elapsed=${Date.now() - tStart}ms gateway=${gatewayUrl} err=${err instanceof Error ? err.message : String(err)}`);
|
|
3691
4072
|
throw err;
|
|
3692
4073
|
}
|
|
@@ -3728,7 +4109,7 @@ export class AUNClient {
|
|
|
3728
4109
|
* 初始化 V2 session:IK 使用 AID 长期私钥,SPK 存储在 per-AID SQLite 的 v2_device_keys 表。
|
|
3729
4110
|
* connect 成功后会自动调用;重复调用幂等。
|
|
3730
4111
|
*/
|
|
3731
|
-
async
|
|
4112
|
+
async _initV2Session() {
|
|
3732
4113
|
if (!this._aid)
|
|
3733
4114
|
return;
|
|
3734
4115
|
const existing = this._v2Session;
|
|
@@ -3749,6 +4130,27 @@ export class AUNClient {
|
|
|
3749
4130
|
identity = null;
|
|
3750
4131
|
}
|
|
3751
4132
|
}
|
|
4133
|
+
if (!identity?.private_key_pem) {
|
|
4134
|
+
// fallback:缓存的 identity 可能被 instanceState 污染,重新从 keystore 加载
|
|
4135
|
+
try {
|
|
4136
|
+
identity = this._keystore.loadIdentity(this._aid);
|
|
4137
|
+
if (identity?.private_key_pem) {
|
|
4138
|
+
this._identity = identity;
|
|
4139
|
+
this._clientLog.warn('V2 session init: identity cache was stale, reloaded from keystore');
|
|
4140
|
+
// 重新持久化 instance_state,清理脏数据
|
|
4141
|
+
const persistIdentity = this._auth._persistIdentity;
|
|
4142
|
+
if (typeof persistIdentity === 'function') {
|
|
4143
|
+
try {
|
|
4144
|
+
persistIdentity.call(this._auth, identity);
|
|
4145
|
+
}
|
|
4146
|
+
catch { /* best-effort */ }
|
|
4147
|
+
}
|
|
4148
|
+
}
|
|
4149
|
+
}
|
|
4150
|
+
catch {
|
|
4151
|
+
identity = null;
|
|
4152
|
+
}
|
|
4153
|
+
}
|
|
3752
4154
|
if (!identity?.private_key_pem) {
|
|
3753
4155
|
this._clientLog.warn('V2 session init skipped: no AID private key');
|
|
3754
4156
|
return;
|
|
@@ -4155,7 +4557,7 @@ export class AUNClient {
|
|
|
4155
4557
|
return envelope;
|
|
4156
4558
|
}
|
|
4157
4559
|
/** V2 P2P 加密发送,推测性缓存失败后刷新 bootstrap 重试一次。 */
|
|
4158
|
-
async
|
|
4560
|
+
async _sendV2(to, payload, opts) {
|
|
4159
4561
|
await this._ensureV2SessionReady('message.send', 'V2 session not initialized; encrypted message.send requires V2 (V1 E2EE removed)');
|
|
4160
4562
|
const toAid = String(to ?? '').trim();
|
|
4161
4563
|
if (!toAid)
|
|
@@ -4200,11 +4602,11 @@ export class AUNClient {
|
|
|
4200
4602
|
}
|
|
4201
4603
|
}
|
|
4202
4604
|
/** V2 P2P 拉取并解密;直接方法返回消息数组,call("message.pull") 会包装为 {messages}. */
|
|
4203
|
-
async
|
|
4605
|
+
async _pullV2(afterSeq = 0, limit = 50, opts) {
|
|
4204
4606
|
await this._ensureV2SessionReady('message.pull');
|
|
4205
4607
|
const ns = this._aid ? `p2p:${this._aid}` : '';
|
|
4206
4608
|
if (ns && !opts?.gateLocked) {
|
|
4207
|
-
return await this._runPullSerialized(ns, async () => this.
|
|
4609
|
+
return await this._runPullSerialized(ns, async () => this._pullV2(afterSeq, limit, {
|
|
4208
4610
|
...(opts ?? {}),
|
|
4209
4611
|
gateLocked: true,
|
|
4210
4612
|
scheduleFollowup: true,
|
|
@@ -4212,7 +4614,7 @@ export class AUNClient {
|
|
|
4212
4614
|
}
|
|
4213
4615
|
const decrypted = [];
|
|
4214
4616
|
let totalRawCount = 0;
|
|
4215
|
-
let nextAfterSeq = afterSeq || (ns ? this._seqTracker.getContiguousSeq(ns) : 0);
|
|
4617
|
+
let nextAfterSeq = opts?.force ? afterSeq : (afterSeq || (ns ? this._seqTracker.getContiguousSeq(ns) : 0));
|
|
4216
4618
|
let pageCount = 0;
|
|
4217
4619
|
const maxPages = 100;
|
|
4218
4620
|
while (pageCount < maxPages) {
|
|
@@ -4221,6 +4623,7 @@ export class AUNClient {
|
|
|
4221
4623
|
const result = await this._callRawV2Rpc('message.v2.pull', {
|
|
4222
4624
|
after_seq: nextAfterSeq,
|
|
4223
4625
|
limit,
|
|
4626
|
+
...(opts?.force ? { force: true } : {}),
|
|
4224
4627
|
});
|
|
4225
4628
|
const messages = (Array.isArray(result?.messages) ? result.messages : []);
|
|
4226
4629
|
totalRawCount += messages.length;
|
|
@@ -4315,7 +4718,7 @@ export class AUNClient {
|
|
|
4315
4718
|
}
|
|
4316
4719
|
if (messages.length > 0 && contigAdvanced && ackSeq > 0 && !opts?.skipAutoAck) {
|
|
4317
4720
|
this._clientLog.debug(`message.v2.pull scheduling auto-ack: ns=${ns}, ack_seq=${ackSeq}, raw_count=${messages.length}`);
|
|
4318
|
-
this._safeAsync(this.
|
|
4721
|
+
this._safeAsync(this._ackV2(ackSeq).then(() => undefined));
|
|
4319
4722
|
}
|
|
4320
4723
|
}
|
|
4321
4724
|
const nextAfter = Math.max(pageMaxSeq, nextAfterSeq);
|
|
@@ -4330,7 +4733,7 @@ export class AUNClient {
|
|
|
4330
4733
|
return decrypted;
|
|
4331
4734
|
}
|
|
4332
4735
|
/** V2 P2P ack,并触发旧 SPK 销毁自检。 */
|
|
4333
|
-
async
|
|
4736
|
+
async _ackV2(upToSeq) {
|
|
4334
4737
|
const ns = this._aid ? `p2p:${this._aid}` : '';
|
|
4335
4738
|
let seq = Number(upToSeq ?? (ns ? this._seqTracker.getContiguousSeq(ns) : 0));
|
|
4336
4739
|
if (!Number.isFinite(seq) || seq <= 0) {
|
|
@@ -4378,7 +4781,7 @@ export class AUNClient {
|
|
|
4378
4781
|
return result;
|
|
4379
4782
|
}
|
|
4380
4783
|
/** V2 Group 加密发送,推测性缓存失败后刷新 bootstrap 重试一次。 */
|
|
4381
|
-
async
|
|
4784
|
+
async _sendGroupV2(groupId, payload, opts) {
|
|
4382
4785
|
await this._ensureV2SessionReady('group.send', 'V2 session not initialized; encrypted group.send requires V2 (V1 E2EE removed)');
|
|
4383
4786
|
const gid = normalizeGroupId(groupId) || String(groupId ?? '').trim();
|
|
4384
4787
|
if (!gid)
|
|
@@ -4558,17 +4961,17 @@ export class AUNClient {
|
|
|
4558
4961
|
return envelope;
|
|
4559
4962
|
}
|
|
4560
4963
|
async _pullGroupV2Internal(params) {
|
|
4561
|
-
await this.
|
|
4964
|
+
await this._pullGroupV2(params.group_id, params.after_seq, params.limit, { gateLocked: true });
|
|
4562
4965
|
}
|
|
4563
4966
|
/** V2 Group 拉取并解密;直接方法返回消息数组,call("group.pull") 会包装为 {messages}. */
|
|
4564
|
-
async
|
|
4967
|
+
async _pullGroupV2(groupId, afterSeq = 0, limit = 50, opts) {
|
|
4565
4968
|
await this._ensureV2SessionReady('group.pull');
|
|
4566
4969
|
const gid = normalizeGroupId(groupId) || String(groupId ?? '').trim();
|
|
4567
4970
|
if (!gid)
|
|
4568
4971
|
throw new ValidationError('group.pull requires group_id');
|
|
4569
4972
|
const ns = `group:${gid}`;
|
|
4570
4973
|
if (!opts?.gateLocked) {
|
|
4571
|
-
return await this._runPullSerialized(ns, async () => this.
|
|
4974
|
+
return await this._runPullSerialized(ns, async () => this._pullGroupV2(gid, afterSeq, limit, {
|
|
4572
4975
|
...(opts ?? {}),
|
|
4573
4976
|
gateLocked: true,
|
|
4574
4977
|
scheduleFollowup: true,
|
|
@@ -4680,7 +5083,7 @@ export class AUNClient {
|
|
|
4680
5083
|
}
|
|
4681
5084
|
if (messages.length > 0 && contigAdvanced && ackSeq > 0) {
|
|
4682
5085
|
this._clientLog.debug(`group.v2.pull scheduling auto-ack: group=${gid}, ns=${ns}, ack_seq=${ackSeq}, raw_count=${messages.length}`);
|
|
4683
|
-
this._safeAsync(this.
|
|
5086
|
+
this._safeAsync(this._ackGroupV2(gid, ackSeq).then(() => undefined));
|
|
4684
5087
|
}
|
|
4685
5088
|
const nextAfter = Math.max(pageMaxSeq, nextAfterSeq);
|
|
4686
5089
|
if (messages.length === 0 || nextAfter <= nextAfterSeq || result.has_more === false)
|
|
@@ -4694,7 +5097,7 @@ export class AUNClient {
|
|
|
4694
5097
|
return decrypted;
|
|
4695
5098
|
}
|
|
4696
5099
|
/** V2 Group ack。 */
|
|
4697
|
-
async
|
|
5100
|
+
async _ackGroupV2(groupId, upToSeq) {
|
|
4698
5101
|
const gid = normalizeGroupId(groupId) || String(groupId ?? '').trim();
|
|
4699
5102
|
if (!gid)
|
|
4700
5103
|
throw new ValidationError('group.ack_messages requires group_id');
|
|
@@ -4887,6 +5290,12 @@ export class AUNClient {
|
|
|
4887
5290
|
encrypted: true,
|
|
4888
5291
|
e2ee,
|
|
4889
5292
|
};
|
|
5293
|
+
const explicitDirection = String(msg.direction ?? '').trim();
|
|
5294
|
+
result.direction = explicitDirection || (fromAid && fromAid === this._aid ? 'outbound_sync' : 'inbound');
|
|
5295
|
+
if (msg.device_id !== undefined)
|
|
5296
|
+
result.device_id = msg.device_id;
|
|
5297
|
+
if (msg.slot_id !== undefined)
|
|
5298
|
+
result.slot_id = msg.slot_id;
|
|
4890
5299
|
this._attachV2EnvelopeMetadata(result, e2ee);
|
|
4891
5300
|
this._logMessageDebug('decrypt-ok', 'v2.decrypt', groupIdForKeys ? 'group.message_created' : 'message.received', result);
|
|
4892
5301
|
return result;
|
|
@@ -4953,6 +5362,146 @@ export class AUNClient {
|
|
|
4953
5362
|
}
|
|
4954
5363
|
return null;
|
|
4955
5364
|
}
|
|
5365
|
+
_truthyBool(value) {
|
|
5366
|
+
if (value === true || value === 1)
|
|
5367
|
+
return true;
|
|
5368
|
+
if (typeof value === 'string') {
|
|
5369
|
+
const normalized = value.trim().toLowerCase();
|
|
5370
|
+
return normalized === 'true' || normalized === '1' || normalized === 'yes' || normalized === 'on';
|
|
5371
|
+
}
|
|
5372
|
+
return false;
|
|
5373
|
+
}
|
|
5374
|
+
_encryptedPushEnvelope(msg) {
|
|
5375
|
+
const payload = msg.payload;
|
|
5376
|
+
if (this._isEncryptedEnvelopePayload(payload))
|
|
5377
|
+
return payload;
|
|
5378
|
+
if (typeof msg.envelope_json === 'string' && msg.envelope_json.trim()) {
|
|
5379
|
+
try {
|
|
5380
|
+
const parsed = JSON.parse(msg.envelope_json);
|
|
5381
|
+
if (this._isEncryptedEnvelopePayload(parsed))
|
|
5382
|
+
return parsed;
|
|
5383
|
+
}
|
|
5384
|
+
catch {
|
|
5385
|
+
return null;
|
|
5386
|
+
}
|
|
5387
|
+
}
|
|
5388
|
+
return null;
|
|
5389
|
+
}
|
|
5390
|
+
_isEncryptedPushMessage(msg) {
|
|
5391
|
+
if (this._truthyBool(msg.encrypted))
|
|
5392
|
+
return true;
|
|
5393
|
+
return this._encryptedPushEnvelope(msg) !== null;
|
|
5394
|
+
}
|
|
5395
|
+
_isEncryptedEnvelopePayload(payload) {
|
|
5396
|
+
if (!isJsonObject(payload))
|
|
5397
|
+
return false;
|
|
5398
|
+
const envelope = payload;
|
|
5399
|
+
const payloadType = String(envelope.type ?? '').trim();
|
|
5400
|
+
if (payloadType.startsWith('e2ee.'))
|
|
5401
|
+
return true;
|
|
5402
|
+
if (!String(envelope.ciphertext ?? '').trim())
|
|
5403
|
+
return false;
|
|
5404
|
+
return envelope.nonce !== undefined
|
|
5405
|
+
|| envelope.tag !== undefined
|
|
5406
|
+
|| envelope.recipient !== undefined
|
|
5407
|
+
|| envelope.recipients !== undefined
|
|
5408
|
+
|| envelope.wrapped_key !== undefined
|
|
5409
|
+
|| envelope.recipients_digest !== undefined;
|
|
5410
|
+
}
|
|
5411
|
+
_isV2EncryptedEnvelopePayload(envelope) {
|
|
5412
|
+
if (!envelope)
|
|
5413
|
+
return false;
|
|
5414
|
+
const payloadType = String(envelope.type ?? '').trim();
|
|
5415
|
+
if (payloadType === 'e2ee.p2p_encrypted' || payloadType === 'e2ee.group_encrypted')
|
|
5416
|
+
return true;
|
|
5417
|
+
return String(envelope.version ?? '').trim().toLowerCase() === 'v2' && payloadType.startsWith('e2ee.');
|
|
5418
|
+
}
|
|
5419
|
+
_safeUndecryptablePushEvent(msg, group) {
|
|
5420
|
+
const event = {
|
|
5421
|
+
message_id: msg.message_id,
|
|
5422
|
+
from: msg.from,
|
|
5423
|
+
seq: msg.seq,
|
|
5424
|
+
timestamp: (msg.timestamp ?? msg.t_server),
|
|
5425
|
+
device_id: msg.device_id,
|
|
5426
|
+
slot_id: msg.slot_id,
|
|
5427
|
+
_decrypt_error: 'encrypted push payload is not decryptable on raw push path',
|
|
5428
|
+
_decrypt_stage: 'push_envelope',
|
|
5429
|
+
};
|
|
5430
|
+
if (group) {
|
|
5431
|
+
event.group_id = msg.group_id;
|
|
5432
|
+
}
|
|
5433
|
+
else {
|
|
5434
|
+
event.to = msg.to;
|
|
5435
|
+
}
|
|
5436
|
+
const envelope = this._encryptedPushEnvelope(msg);
|
|
5437
|
+
if (envelope) {
|
|
5438
|
+
event._envelope_type = String(envelope.type ?? '');
|
|
5439
|
+
event._suite = String(envelope.suite ?? '');
|
|
5440
|
+
if (this._isV2EncryptedEnvelopePayload(envelope)) {
|
|
5441
|
+
this._attachV2EnvelopeMetadata(event, this._v2E2eeMeta(envelope));
|
|
5442
|
+
}
|
|
5443
|
+
}
|
|
5444
|
+
return event;
|
|
5445
|
+
}
|
|
5446
|
+
async _decryptEncryptedPushPayload(msg, group) {
|
|
5447
|
+
const envelope = this._encryptedPushEnvelope(msg);
|
|
5448
|
+
if (!this._isV2EncryptedEnvelopePayload(envelope))
|
|
5449
|
+
return null;
|
|
5450
|
+
const aad = isJsonObject(envelope.aad) ? envelope.aad : {};
|
|
5451
|
+
const fromAid = String(msg.from_aid ?? msg.from ?? msg.sender_aid ?? aad.from ?? '').trim();
|
|
5452
|
+
const plaintext = await this._decryptV2EnvelopeForThought({ envelope, fromAid });
|
|
5453
|
+
if (!plaintext)
|
|
5454
|
+
return null;
|
|
5455
|
+
const e2ee = this._v2E2eeMeta(envelope);
|
|
5456
|
+
const result = {
|
|
5457
|
+
message_id: String(msg.message_id ?? ''),
|
|
5458
|
+
from: fromAid,
|
|
5459
|
+
seq: msg.seq,
|
|
5460
|
+
timestamp: (msg.t_server ?? msg.timestamp),
|
|
5461
|
+
payload: plaintext,
|
|
5462
|
+
encrypted: true,
|
|
5463
|
+
e2ee,
|
|
5464
|
+
};
|
|
5465
|
+
result.direction = fromAid && fromAid === this._aid ? 'outbound_sync' : 'inbound';
|
|
5466
|
+
if (msg.t_server !== undefined)
|
|
5467
|
+
result.t_server = msg.t_server;
|
|
5468
|
+
if (msg.device_id !== undefined)
|
|
5469
|
+
result.device_id = msg.device_id;
|
|
5470
|
+
if (msg.slot_id !== undefined)
|
|
5471
|
+
result.slot_id = msg.slot_id;
|
|
5472
|
+
if (group) {
|
|
5473
|
+
result.group_id = (msg.group_id ?? aad.group_id ?? envelope.group_id);
|
|
5474
|
+
}
|
|
5475
|
+
else {
|
|
5476
|
+
result.to = (msg.to ?? this._aid ?? '');
|
|
5477
|
+
}
|
|
5478
|
+
this._attachV2EnvelopeMetadata(result, e2ee);
|
|
5479
|
+
this._logMessageDebug('decrypt-ok', 'push.encrypted', group ? 'group.message_created' : 'message.received', result);
|
|
5480
|
+
return result;
|
|
5481
|
+
}
|
|
5482
|
+
async _publishEncryptedPushAsUndecryptable(event, ns, seq, msg, group) {
|
|
5483
|
+
const safeEvent = this._safeUndecryptablePushEvent(msg, group);
|
|
5484
|
+
this._logMessageDebug('decrypt-fail', 'push.encrypted', event, safeEvent);
|
|
5485
|
+
if (ns) {
|
|
5486
|
+
return await this._publishOrderedMessage(event, ns, seq, safeEvent);
|
|
5487
|
+
}
|
|
5488
|
+
const published = this._publishAppEvent(event, safeEvent, 'push');
|
|
5489
|
+
if (isPromiseLike(published))
|
|
5490
|
+
await published;
|
|
5491
|
+
return true;
|
|
5492
|
+
}
|
|
5493
|
+
async _publishEncryptedPushMessage(normalEvent, undecryptableEvent, ns, seq, msg, group) {
|
|
5494
|
+
const decrypted = await this._decryptEncryptedPushPayload(msg, group);
|
|
5495
|
+
if (decrypted) {
|
|
5496
|
+
if (ns)
|
|
5497
|
+
return await this._publishOrderedMessage(normalEvent, ns, seq, decrypted);
|
|
5498
|
+
const published = this._publishAppEvent(normalEvent, decrypted, 'push');
|
|
5499
|
+
if (isPromiseLike(published))
|
|
5500
|
+
await published;
|
|
5501
|
+
return true;
|
|
5502
|
+
}
|
|
5503
|
+
return await this._publishEncryptedPushAsUndecryptable(undecryptableEvent, ns, seq, msg, group);
|
|
5504
|
+
}
|
|
4956
5505
|
_metadataWithoutAuth(value) {
|
|
4957
5506
|
const candidate = value;
|
|
4958
5507
|
if (!isJsonObject(candidate))
|
|
@@ -5743,7 +6292,7 @@ export class AUNClient {
|
|
|
5743
6292
|
}
|
|
5744
6293
|
this._gapFillDone.set(dedupKey, Date.now());
|
|
5745
6294
|
try {
|
|
5746
|
-
const pulled = await this.
|
|
6295
|
+
const pulled = await this._pullV2(0, 50, { gateLocked: true });
|
|
5747
6296
|
const newContig = this._seqTracker.getContiguousSeq(ns);
|
|
5748
6297
|
this._clientLog.debug(`_onV2PushNotification pull done: contiguous_seq=${contigBefore}->${newContig} (push_seq=${pushSeq || 'null'})`);
|
|
5749
6298
|
if (newContig <= operationBefore)
|
|
@@ -5832,7 +6381,7 @@ export class AUNClient {
|
|
|
5832
6381
|
this._gapFillDone.set(dedupKey, Date.now());
|
|
5833
6382
|
try {
|
|
5834
6383
|
this._clientLog.debug(`_onRawGroupV2MessageCreated auto-pull start: group=${groupId}, after_seq=${pullAfterSeq}, push_seq=${seq}`);
|
|
5835
|
-
const pulled = await this.
|
|
6384
|
+
const pulled = await this._pullGroupV2(groupId, pullAfterSeq, 50, { gateLocked: true });
|
|
5836
6385
|
const newContig = this._seqTracker.getContiguousSeq(ns);
|
|
5837
6386
|
this._clientLog.debug(`_onRawGroupV2MessageCreated auto-pull done: group=${groupId}, after_seq=${pullAfterSeq}, push_seq=${seq}, contiguous=${newContig}`);
|
|
5838
6387
|
if (newContig <= pullAfterSeq)
|
|
@@ -5869,6 +6418,56 @@ export class AUNClient {
|
|
|
5869
6418
|
this._clientLog.debug(`SPK rotation after V2 epoch change failed (non-fatal): ${formatCaughtError(exc)}`);
|
|
5870
6419
|
}
|
|
5871
6420
|
}
|
|
6421
|
+
/** 按当前 AID 发现 Gateway;用于 authenticate()/connect() 的新入口。 */
|
|
6422
|
+
async _resolveGatewayForAid(aid) {
|
|
6423
|
+
const resolvedAid = String(aid ?? this._aid ?? '').trim();
|
|
6424
|
+
if (!resolvedAid) {
|
|
6425
|
+
throw new StateError('gateway discovery requires a loaded AID');
|
|
6426
|
+
}
|
|
6427
|
+
if (this._gatewayUrl)
|
|
6428
|
+
return this._gatewayUrl;
|
|
6429
|
+
try {
|
|
6430
|
+
const loadMetadata = this._keystore.loadMetadata;
|
|
6431
|
+
const cachedGateway = typeof loadMetadata === 'function'
|
|
6432
|
+
? String(loadMetadata.call(this._keystore, resolvedAid)?.gateway_url ?? '').trim()
|
|
6433
|
+
: '';
|
|
6434
|
+
if (cachedGateway) {
|
|
6435
|
+
this._gatewayUrl = cachedGateway;
|
|
6436
|
+
return cachedGateway;
|
|
6437
|
+
}
|
|
6438
|
+
}
|
|
6439
|
+
catch {
|
|
6440
|
+
// 缓存读取失败不影响发现流程。
|
|
6441
|
+
}
|
|
6442
|
+
const dotIdx = resolvedAid.indexOf('.');
|
|
6443
|
+
const issuerDomain = dotIdx >= 0 ? resolvedAid.slice(dotIdx + 1) : resolvedAid;
|
|
6444
|
+
const portSuffix = this._configModel.discoveryPort ? `:${this._configModel.discoveryPort}` : '';
|
|
6445
|
+
const aidUrl = `https://${resolvedAid}${portSuffix}/.well-known/aun-gateway`;
|
|
6446
|
+
const gatewayDomainUrl = `https://gateway.${issuerDomain}${portSuffix}/.well-known/aun-gateway`;
|
|
6447
|
+
const candidates = this._configModel.verifySsl ? [aidUrl, gatewayDomainUrl] : [gatewayDomainUrl, aidUrl];
|
|
6448
|
+
let lastErr = null;
|
|
6449
|
+
for (const url of candidates) {
|
|
6450
|
+
try {
|
|
6451
|
+
const gateway = await this._discovery.discover(url);
|
|
6452
|
+
this._gatewayUrl = gateway;
|
|
6453
|
+
try {
|
|
6454
|
+
const saveMetadata = this._keystore.saveMetadata;
|
|
6455
|
+
if (typeof saveMetadata === 'function') {
|
|
6456
|
+
saveMetadata.call(this._keystore, resolvedAid, { gateway_url: gateway, gateway_cached_at: Date.now() });
|
|
6457
|
+
}
|
|
6458
|
+
}
|
|
6459
|
+
catch {
|
|
6460
|
+
// 缓存写入失败不影响连接。
|
|
6461
|
+
}
|
|
6462
|
+
return gateway;
|
|
6463
|
+
}
|
|
6464
|
+
catch (err) {
|
|
6465
|
+
lastErr = err;
|
|
6466
|
+
this._clientLog.warn(`gateway discovery failed: aid=${resolvedAid} url=${url} err=${formatCaughtError(err)}`);
|
|
6467
|
+
}
|
|
6468
|
+
}
|
|
6469
|
+
throw lastErr instanceof Error ? lastErr : new ConnectionError(`gateway discovery failed for ${resolvedAid}`);
|
|
6470
|
+
}
|
|
5872
6471
|
/** 从参数中解析 Gateway URL */
|
|
5873
6472
|
_resolveGateway(params) {
|
|
5874
6473
|
const gateways = this._resolveGateways(params);
|
|
@@ -5919,15 +6518,19 @@ export class AUNClient {
|
|
|
5919
6518
|
}
|
|
5920
6519
|
// ── 内部:参数处理 ────────────────────────────────────────
|
|
5921
6520
|
/** 规范化连接参数 */
|
|
5922
|
-
_normalizeConnectParams(params) {
|
|
6521
|
+
_normalizeConnectParams(params, opts = {}) {
|
|
5923
6522
|
const request = { ...params };
|
|
5924
6523
|
const accessToken = String(request.access_token ?? '');
|
|
5925
|
-
if (!accessToken)
|
|
6524
|
+
if (!accessToken && opts.requireAccessToken === true) {
|
|
5926
6525
|
throw new StateError('connect requires non-empty access_token');
|
|
6526
|
+
}
|
|
5927
6527
|
const gateway = String(request.gateway ?? this._gatewayUrl ?? '');
|
|
5928
6528
|
if (!gateway)
|
|
5929
6529
|
throw new StateError('connect requires non-empty gateway');
|
|
5930
|
-
|
|
6530
|
+
if (accessToken)
|
|
6531
|
+
request.access_token = accessToken;
|
|
6532
|
+
else
|
|
6533
|
+
delete request.access_token;
|
|
5931
6534
|
request.gateway = gateway;
|
|
5932
6535
|
request.device_id = this._deviceId;
|
|
5933
6536
|
request.slot_id = normalizeInstanceId(request.slot_id ?? this._slotId, 'slot_id', { allowEmpty: true });
|
|
@@ -6035,7 +6638,7 @@ export class AUNClient {
|
|
|
6035
6638
|
let consecutiveFailures = 0;
|
|
6036
6639
|
const maxFailures = 2;
|
|
6037
6640
|
this._heartbeatTimer = setInterval(() => {
|
|
6038
|
-
if (this._closing || this.
|
|
6641
|
+
if (this._closing || this.state !== ConnectionState.READY)
|
|
6039
6642
|
return;
|
|
6040
6643
|
this._transport.call('meta.ping', {}).then((pong) => {
|
|
6041
6644
|
consecutiveFailures = 0;
|
|
@@ -6070,7 +6673,7 @@ export class AUNClient {
|
|
|
6070
6673
|
clearInterval(this._heartbeatTimer);
|
|
6071
6674
|
this._heartbeatTimer = null;
|
|
6072
6675
|
}
|
|
6073
|
-
if (newInterval > 0 && this.
|
|
6676
|
+
if (newInterval > 0 && this.state === ConnectionState.READY && !this._closing) {
|
|
6074
6677
|
this._startHeartbeatTask();
|
|
6075
6678
|
}
|
|
6076
6679
|
}
|
|
@@ -6089,7 +6692,7 @@ export class AUNClient {
|
|
|
6089
6692
|
if (this._closing)
|
|
6090
6693
|
return;
|
|
6091
6694
|
this._tokenRefreshTimer = null;
|
|
6092
|
-
if (this.
|
|
6695
|
+
if (this.state !== ConnectionState.READY || !this._gatewayUrl) {
|
|
6093
6696
|
scheduleNext();
|
|
6094
6697
|
return;
|
|
6095
6698
|
}
|
|
@@ -6108,12 +6711,17 @@ export class AUNClient {
|
|
|
6108
6711
|
scheduleNext();
|
|
6109
6712
|
return;
|
|
6110
6713
|
}
|
|
6111
|
-
if (this._closing || this.
|
|
6714
|
+
if (this._closing || this.state !== ConnectionState.READY || !this._gatewayUrl) {
|
|
6112
6715
|
scheduleNext();
|
|
6113
6716
|
return;
|
|
6114
6717
|
}
|
|
6115
6718
|
try {
|
|
6116
6719
|
identity = await this._auth.refreshCachedTokens(this._gatewayUrl, identity);
|
|
6720
|
+
// 刷新期间可能已断线,复检状态,避免写回 stale identity
|
|
6721
|
+
if (this.state !== ConnectionState.READY) {
|
|
6722
|
+
scheduleNext();
|
|
6723
|
+
return;
|
|
6724
|
+
}
|
|
6117
6725
|
this._identity = identity;
|
|
6118
6726
|
if (this._sessionParams !== null && identity.access_token) {
|
|
6119
6727
|
this._sessionParams.access_token = identity.access_token;
|
|
@@ -6278,7 +6886,7 @@ export class AUNClient {
|
|
|
6278
6886
|
: {};
|
|
6279
6887
|
this._clientLog.warn(`server initiated disconnect: code=${code}, reason=${reason}, detail=${JSON.stringify(detail)}`);
|
|
6280
6888
|
this._serverKicked = true;
|
|
6281
|
-
// 缓存最近一次 disconnect 信息,让后续 connection.state(
|
|
6889
|
+
// 缓存最近一次 disconnect 信息,让后续 connection.state(connection_failed) 也能带 detail
|
|
6282
6890
|
this._lastDisconnectInfo = { code, reason, detail };
|
|
6283
6891
|
// 透传给应用层订阅者
|
|
6284
6892
|
try {
|
|
@@ -6294,27 +6902,27 @@ export class AUNClient {
|
|
|
6294
6902
|
}
|
|
6295
6903
|
/** 传输层断线回调 */
|
|
6296
6904
|
async _handleTransportDisconnect(error, closeCode) {
|
|
6297
|
-
if (this._closing || this.
|
|
6905
|
+
if (this._closing || this.state === ConnectionState.CLOSED)
|
|
6298
6906
|
return;
|
|
6299
6907
|
// 已在重连中则跳过,避免心跳超时和 transport 断线回调重复触发
|
|
6300
6908
|
if (this._reconnectActive)
|
|
6301
6909
|
return;
|
|
6302
6910
|
this._clientLog.warn(`transport disconnected: closeCode=${closeCode ?? 'none'}, error=${error ? formatCaughtError(error) : 'none'}`);
|
|
6303
|
-
this._state = '
|
|
6911
|
+
this._state = 'standby';
|
|
6304
6912
|
this._stopBackgroundTasks();
|
|
6305
|
-
await this._dispatcher.publish('connection.state', { state: this._state, error });
|
|
6913
|
+
await this._dispatcher.publish('connection.state', { state: this._publicState(this._state), error });
|
|
6306
6914
|
if (!this._sessionOptions.auto_reconnect)
|
|
6307
6915
|
return;
|
|
6308
6916
|
if (this._reconnectActive)
|
|
6309
6917
|
return;
|
|
6310
6918
|
// 不重连 close code(认证失败/权限错误/被踢等)或服务端通知断开:抑制重连
|
|
6311
6919
|
if (this._serverKicked || (closeCode !== undefined && AUNClient._NO_RECONNECT_CODES.has(closeCode))) {
|
|
6312
|
-
this._state = '
|
|
6920
|
+
this._state = 'connection_failed';
|
|
6313
6921
|
const reason = this._serverKicked ? 'server kicked' : `close code ${closeCode}`;
|
|
6314
6922
|
this._clientLog.warn(`suppressing auto-reconnect: ${reason}`);
|
|
6315
6923
|
const disconnectInfo = this._lastDisconnectInfo ?? {};
|
|
6316
6924
|
const eventPayload = {
|
|
6317
|
-
state: this._state, error, reason,
|
|
6925
|
+
state: this._publicState(this._state), error, reason,
|
|
6318
6926
|
};
|
|
6319
6927
|
// 把服务端附带的结构化 detail(如配额超限信息)也带给应用层
|
|
6320
6928
|
if (disconnectInfo.detail && Object.keys(disconnectInfo.detail).length > 0) {
|
|
@@ -6347,30 +6955,39 @@ export class AUNClient {
|
|
|
6347
6955
|
const maxBaseDelay = clampReconnectDelayMs(Number(retry.max_delay ?? 64.0) * 1000, RECONNECT_MAX_BASE_DELAY_MS);
|
|
6348
6956
|
const maxAttemptsRaw = Number(retry.max_attempts ?? 0);
|
|
6349
6957
|
const maxAttempts = Number.isFinite(maxAttemptsRaw) && maxAttemptsRaw > 0 ? Math.floor(maxAttemptsRaw) : 0;
|
|
6958
|
+
this._retryMaxAttempts = maxAttempts;
|
|
6350
6959
|
let delay = clampReconnectDelayMs(serverInitiated ? 16_000 : Number(retry.initial_delay ?? 1.0) * 1000, serverInitiated ? 16_000 : RECONNECT_MIN_BASE_DELAY_MS, maxBaseDelay);
|
|
6351
6960
|
for (let attempt = 1; !this._reconnectAbort?.signal.aborted; attempt++) {
|
|
6352
6961
|
if (this._closing)
|
|
6353
6962
|
break;
|
|
6354
6963
|
// max_attempts 检查在循环顶部,覆盖所有路径(含 health-fail)
|
|
6355
6964
|
if (maxAttempts > 0 && attempt > maxAttempts) {
|
|
6356
|
-
this._state = '
|
|
6965
|
+
this._state = 'connection_failed';
|
|
6357
6966
|
await this._dispatcher.publish('connection.state', {
|
|
6358
|
-
state: this._state,
|
|
6967
|
+
state: this._publicState(this._state),
|
|
6359
6968
|
attempt: attempt - 1,
|
|
6360
6969
|
reason: 'max_attempts_exhausted',
|
|
6361
6970
|
});
|
|
6362
6971
|
break;
|
|
6363
6972
|
}
|
|
6364
|
-
this.
|
|
6973
|
+
this._retryAttempt = attempt;
|
|
6974
|
+
this._nextRetryAt = Date.now() + reconnectSleepDelayMs(delay, maxBaseDelay);
|
|
6975
|
+
this._state = 'retry_backoff';
|
|
6365
6976
|
await this._dispatcher.publish('connection.state', {
|
|
6366
|
-
state: this._state,
|
|
6977
|
+
state: this._publicState(this._state),
|
|
6367
6978
|
attempt,
|
|
6979
|
+
next_retry_at: this._nextRetryAt,
|
|
6368
6980
|
});
|
|
6369
6981
|
try {
|
|
6370
6982
|
// 固定上限抖动:base=[1s, max_base],delay=base+rand(0..max_base)。
|
|
6371
|
-
await this._sleep(
|
|
6983
|
+
await this._sleep(Math.max(0, this._nextRetryAt - Date.now()));
|
|
6372
6984
|
if (this._reconnectAbort?.signal.aborted || this._closing)
|
|
6373
6985
|
break;
|
|
6986
|
+
this._state = 'reconnecting';
|
|
6987
|
+
await this._dispatcher.publish('connection.state', {
|
|
6988
|
+
state: this._publicState(this._state),
|
|
6989
|
+
attempt,
|
|
6990
|
+
});
|
|
6374
6991
|
// 重连前先 GET /health 探测,不健康则跳过本轮
|
|
6375
6992
|
if (this._gatewayUrl) {
|
|
6376
6993
|
const healthy = await this._discovery.checkHealth(this._gatewayUrl, 5_000);
|
|
@@ -6386,6 +7003,7 @@ export class AUNClient {
|
|
|
6386
7003
|
await this._connectOnce(this._sessionParams, true);
|
|
6387
7004
|
// 重连成功,退出循环
|
|
6388
7005
|
this._clientLog.debug(`reconnect success: attempt=${attempt}, aid=${this._aid ?? ''}`);
|
|
7006
|
+
this._nextRetryAt = null;
|
|
6389
7007
|
this._reconnectActive = false;
|
|
6390
7008
|
this._reconnectAbort = null;
|
|
6391
7009
|
return;
|
|
@@ -6396,9 +7014,9 @@ export class AUNClient {
|
|
|
6396
7014
|
attempt,
|
|
6397
7015
|
});
|
|
6398
7016
|
if (!AUNClient._shouldRetryReconnect(exc)) {
|
|
6399
|
-
this._state = '
|
|
7017
|
+
this._state = 'connection_failed';
|
|
6400
7018
|
await this._dispatcher.publish('connection.state', {
|
|
6401
|
-
state: this._state,
|
|
7019
|
+
state: this._publicState(this._state),
|
|
6402
7020
|
error: formatCaughtError(exc),
|
|
6403
7021
|
attempt,
|
|
6404
7022
|
});
|