@agentunion/fastaun-browser 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 +14 -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 +14 -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 +64 -0
- package/dist/aid-store.d.ts.map +1 -0
- package/dist/aid-store.js +855 -0
- package/dist/aid-store.js.map +1 -0
- package/dist/aid.d.ts +50 -0
- package/dist/aid.d.ts.map +1 -0
- package/dist/aid.js +106 -0
- package/dist/aid.js.map +1 -0
- package/dist/auth.d.ts +17 -1
- package/dist/auth.d.ts.map +1 -1
- package/dist/auth.js +27 -4
- package/dist/auth.js.map +1 -1
- package/dist/bundle.js +1981 -2048
- package/dist/cert-utils.d.ts +26 -0
- package/dist/cert-utils.d.ts.map +1 -0
- package/dist/cert-utils.js +221 -0
- package/dist/cert-utils.js.map +1 -0
- package/dist/client.d.ts +93 -58
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js +775 -170
- package/dist/client.js.map +1 -1
- package/dist/error-codes.d.ts +25 -0
- package/dist/error-codes.d.ts.map +1 -0
- package/dist/error-codes.js +31 -0
- package/dist/error-codes.js.map +1 -0
- package/dist/errors.d.ts +4 -0
- package/dist/errors.d.ts.map +1 -1
- package/dist/errors.js +4 -0
- package/dist/errors.js.map +1 -1
- package/dist/index.d.ts +6 -6
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +5 -5
- package/dist/index.js.map +1 -1
- package/dist/keystore/index.d.ts +1 -1
- package/dist/keystore/index.d.ts.map +1 -1
- package/dist/result.d.ts +19 -0
- package/dist/result.d.ts.map +1 -0
- package/dist/result.js +10 -0
- package/dist/result.js.map +1 -0
- package/dist/transport.d.ts +3 -0
- package/dist/transport.d.ts.map +1 -1
- package/dist/transport.js +17 -2
- package/dist/transport.js.map +1 -1
- package/dist/types.d.ts +13 -2
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js +22 -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 +2 -0
- package/dist/version.d.ts.map +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
|
@@ -11,9 +11,6 @@ import { GatewayDiscovery } from './discovery.js';
|
|
|
11
11
|
import { RPCTransport } from './transport.js';
|
|
12
12
|
import { AuthFlow } from './auth.js';
|
|
13
13
|
import { SeqTracker } from './seq-tracker.js';
|
|
14
|
-
import { AuthNamespace } from './namespaces/auth.js';
|
|
15
|
-
import { CustodyNamespace } from './namespaces/custody.js';
|
|
16
|
-
import { MetaNamespace } from './namespaces/meta.js';
|
|
17
14
|
import { CryptoProvider, uint8ToBase64, base64ToUint8, pemToArrayBuffer, p1363ToDer, certificateSha256Fingerprint, ecdsaSignDer, ecdsaVerifyDer, importCertPublicKeyEcdsa, importPrivateKeyEcdsa, } from './crypto.js';
|
|
18
15
|
import { IndexedDBKeyStore } from './keystore/indexeddb.js';
|
|
19
16
|
import { V2Session, V2KeyStore } from './v2/session/index.js';
|
|
@@ -21,8 +18,9 @@ import { encryptP2PMessage, encryptGroupMessage, decryptMessage, } from './v2/e2
|
|
|
21
18
|
import { ecdsaVerifyRaw } from './v2/crypto/ecdsa.js';
|
|
22
19
|
import { computeStateCommitment } from './v2/state/index.js';
|
|
23
20
|
import { AUNLogger } from './logger.js';
|
|
24
|
-
import { AUNError, AuthError, ConnectionError, E2EEError, PermissionError, StateError, ValidationError, } from './errors.js';
|
|
25
|
-
import { isJsonObject, } from './types.js';
|
|
21
|
+
import { AUNError, AuthError, ConnectionError, E2EEError, NotFoundError, PermissionError, StateError, ValidationError, } from './errors.js';
|
|
22
|
+
import { isJsonObject, ConnectionState, STATE_TO_PUBLIC, } from './types.js';
|
|
23
|
+
import { AID } from './aid.js';
|
|
26
24
|
/**
|
|
27
25
|
* 递归排序键的 JSON 序列化(Canonical JSON for AUN)
|
|
28
26
|
* 等价于 Python json.dumps(sort_keys=True, separators=(",",":"), ensure_ascii=False)
|
|
@@ -171,6 +169,12 @@ const DEFAULT_SESSION_OPTIONS = {
|
|
|
171
169
|
http: 30.0,
|
|
172
170
|
},
|
|
173
171
|
};
|
|
172
|
+
const PROTECTED_HEADERS_METHODS = new Set([
|
|
173
|
+
'message.send',
|
|
174
|
+
'group.send',
|
|
175
|
+
'message.thought.put',
|
|
176
|
+
'group.thought.put',
|
|
177
|
+
]);
|
|
174
178
|
const RECONNECT_MIN_BASE_DELAY_SECONDS = 1.0;
|
|
175
179
|
const RECONNECT_MAX_BASE_DELAY_SECONDS = 64.0;
|
|
176
180
|
const TOKEN_REFRESH_CHECK_INTERVAL_MS = 30_000;
|
|
@@ -213,6 +217,7 @@ function reconnectSleepDelaySeconds(baseDelay, maxBaseDelay) {
|
|
|
213
217
|
/** 对端证书缓存 TTL(秒) */
|
|
214
218
|
const PEER_CERT_CACHE_TTL = 3600;
|
|
215
219
|
const PEER_PREKEYS_CACHE_TTL = 3600;
|
|
220
|
+
const AGENT_MD_HTTP_TIMEOUT_MS = 30_000;
|
|
216
221
|
/**
|
|
217
222
|
* 将 WebSocket URL 转为对应的 HTTP URL
|
|
218
223
|
*/
|
|
@@ -245,6 +250,34 @@ function buildCertUrl(gatewayUrl, aid, certFingerprint) {
|
|
|
245
250
|
}
|
|
246
251
|
return url.toString();
|
|
247
252
|
}
|
|
253
|
+
function agentMdHttpScheme(gatewayUrl) {
|
|
254
|
+
const raw = String(gatewayUrl ?? '').trim().toLowerCase();
|
|
255
|
+
return raw.startsWith('ws://') ? 'http' : 'https';
|
|
256
|
+
}
|
|
257
|
+
function agentMdAuthority(aid, discoveryPort) {
|
|
258
|
+
const host = String(aid ?? '').trim();
|
|
259
|
+
if (!host)
|
|
260
|
+
return '';
|
|
261
|
+
if (discoveryPort && !host.includes(':'))
|
|
262
|
+
return `${host}:${discoveryPort}`;
|
|
263
|
+
return host;
|
|
264
|
+
}
|
|
265
|
+
async function fetchWithTimeout(input, init, timeoutMs = AGENT_MD_HTTP_TIMEOUT_MS) {
|
|
266
|
+
const controller = new AbortController();
|
|
267
|
+
const timer = globalThis.setTimeout(() => controller.abort(), timeoutMs);
|
|
268
|
+
try {
|
|
269
|
+
return await fetch(input, { ...init, signal: controller.signal });
|
|
270
|
+
}
|
|
271
|
+
catch (error) {
|
|
272
|
+
if (controller.signal.aborted) {
|
|
273
|
+
throw new AUNError(`agent.md request timed out after ${timeoutMs}ms`);
|
|
274
|
+
}
|
|
275
|
+
throw error;
|
|
276
|
+
}
|
|
277
|
+
finally {
|
|
278
|
+
globalThis.clearTimeout(timer);
|
|
279
|
+
}
|
|
280
|
+
}
|
|
248
281
|
/**
|
|
249
282
|
* 跨域时将 Gateway URL 替换为 peer 所在域的 Gateway URL。
|
|
250
283
|
*
|
|
@@ -453,6 +486,85 @@ function extractV2EnvelopeFromSource(source) {
|
|
|
453
486
|
}
|
|
454
487
|
return null;
|
|
455
488
|
}
|
|
489
|
+
function truthyBool(value) {
|
|
490
|
+
if (value === true || value === 1)
|
|
491
|
+
return true;
|
|
492
|
+
if (typeof value === 'string') {
|
|
493
|
+
const normalized = value.trim().toLowerCase();
|
|
494
|
+
return normalized === 'true' || normalized === '1' || normalized === 'yes' || normalized === 'on';
|
|
495
|
+
}
|
|
496
|
+
return false;
|
|
497
|
+
}
|
|
498
|
+
function isEncryptedEnvelopePayload(payload) {
|
|
499
|
+
if (!isJsonObject(payload))
|
|
500
|
+
return false;
|
|
501
|
+
const payloadType = String(payload.type ?? '').trim();
|
|
502
|
+
if (payloadType.startsWith('e2ee.'))
|
|
503
|
+
return true;
|
|
504
|
+
if (!String(payload.ciphertext ?? '').trim())
|
|
505
|
+
return false;
|
|
506
|
+
return payload.nonce !== undefined
|
|
507
|
+
|| payload.tag !== undefined
|
|
508
|
+
|| payload.recipient !== undefined
|
|
509
|
+
|| payload.recipients !== undefined
|
|
510
|
+
|| payload.wrapped_key !== undefined
|
|
511
|
+
|| payload.recipients_digest !== undefined;
|
|
512
|
+
}
|
|
513
|
+
function encryptedPushEnvelope(msg) {
|
|
514
|
+
if (isEncryptedEnvelopePayload(msg.payload))
|
|
515
|
+
return msg.payload;
|
|
516
|
+
if (typeof msg.envelope_json === 'string' && msg.envelope_json.trim()) {
|
|
517
|
+
try {
|
|
518
|
+
const parsed = JSON.parse(msg.envelope_json);
|
|
519
|
+
if (isEncryptedEnvelopePayload(parsed))
|
|
520
|
+
return parsed;
|
|
521
|
+
}
|
|
522
|
+
catch {
|
|
523
|
+
return null;
|
|
524
|
+
}
|
|
525
|
+
}
|
|
526
|
+
return null;
|
|
527
|
+
}
|
|
528
|
+
function isEncryptedPushMessage(msg) {
|
|
529
|
+
if (truthyBool(msg.encrypted))
|
|
530
|
+
return true;
|
|
531
|
+
return encryptedPushEnvelope(msg) !== null;
|
|
532
|
+
}
|
|
533
|
+
function isV2EncryptedEnvelopePayload(envelope) {
|
|
534
|
+
if (!envelope)
|
|
535
|
+
return false;
|
|
536
|
+
const payloadType = String(envelope.type ?? '').trim();
|
|
537
|
+
if (payloadType === 'e2ee.p2p_encrypted' || payloadType === 'e2ee.group_encrypted')
|
|
538
|
+
return true;
|
|
539
|
+
return String(envelope.version ?? '').trim().toLowerCase() === 'v2' && payloadType.startsWith('e2ee.');
|
|
540
|
+
}
|
|
541
|
+
function safeUndecryptablePushEvent(msg, group) {
|
|
542
|
+
const event = {
|
|
543
|
+
message_id: msg.message_id ?? null,
|
|
544
|
+
from: msg.from ?? null,
|
|
545
|
+
seq: msg.seq ?? null,
|
|
546
|
+
timestamp: msg.timestamp ?? msg.t_server ?? null,
|
|
547
|
+
device_id: msg.device_id ?? null,
|
|
548
|
+
slot_id: msg.slot_id ?? null,
|
|
549
|
+
_decrypt_error: 'encrypted push payload is not decryptable on raw push path',
|
|
550
|
+
_decrypt_stage: 'push_envelope',
|
|
551
|
+
};
|
|
552
|
+
if (group) {
|
|
553
|
+
event.group_id = msg.group_id ?? null;
|
|
554
|
+
}
|
|
555
|
+
else {
|
|
556
|
+
event.to = msg.to ?? null;
|
|
557
|
+
}
|
|
558
|
+
const envelope = encryptedPushEnvelope(msg);
|
|
559
|
+
if (envelope) {
|
|
560
|
+
event._envelope_type = String(envelope.type ?? '');
|
|
561
|
+
event._suite = String(envelope.suite ?? '');
|
|
562
|
+
if (isV2EncryptedEnvelopePayload(envelope)) {
|
|
563
|
+
attachV2EnvelopeMetadata(event, v2E2eeMeta(envelope));
|
|
564
|
+
}
|
|
565
|
+
}
|
|
566
|
+
return event;
|
|
567
|
+
}
|
|
456
568
|
function metadataWithoutAuth(value) {
|
|
457
569
|
if (!isJsonObject(value))
|
|
458
570
|
return null;
|
|
@@ -502,6 +614,22 @@ function normalizeDeliveryModeConfig(raw, opts = {}) {
|
|
|
502
614
|
affinity_ttl_ms: affinityTtlMs,
|
|
503
615
|
};
|
|
504
616
|
}
|
|
617
|
+
function assertClientOptions(value, label) {
|
|
618
|
+
if (value == null)
|
|
619
|
+
return;
|
|
620
|
+
if (typeof value !== 'object' || Array.isArray(value) || value instanceof AID) {
|
|
621
|
+
throw new ValidationError(`${label} must be an options object`);
|
|
622
|
+
}
|
|
623
|
+
}
|
|
624
|
+
function clientOptionsConfig(options) {
|
|
625
|
+
const raw = { ...(options ?? {}) };
|
|
626
|
+
if (Object.prototype.hasOwnProperty.call(raw, 'aid')) {
|
|
627
|
+
throw new ValidationError('AUNClient options must not include aid; pass an AID object as the first argument');
|
|
628
|
+
}
|
|
629
|
+
delete raw.debug;
|
|
630
|
+
delete raw.protected_headers;
|
|
631
|
+
return raw;
|
|
632
|
+
}
|
|
505
633
|
/**
|
|
506
634
|
* AUN Core SDK 客户端 — 浏览器版本。
|
|
507
635
|
*
|
|
@@ -522,6 +650,8 @@ export class AUNClient {
|
|
|
522
650
|
_aid = null;
|
|
523
651
|
_identity = null;
|
|
524
652
|
_state = 'idle';
|
|
653
|
+
_currentAid = null;
|
|
654
|
+
_instanceProtectedHeaders = null;
|
|
525
655
|
_gatewayUrl = null;
|
|
526
656
|
_deviceId;
|
|
527
657
|
_slotId;
|
|
@@ -536,12 +666,6 @@ export class AUNClient {
|
|
|
536
666
|
_keystore;
|
|
537
667
|
_auth;
|
|
538
668
|
_transport;
|
|
539
|
-
/** 认证命名空间 */
|
|
540
|
-
auth;
|
|
541
|
-
/** AID 托管命名空间 */
|
|
542
|
-
custody;
|
|
543
|
-
/** 元数据命名空间(心跳、状态、信任根管理) */
|
|
544
|
-
meta;
|
|
545
669
|
// E2EE 编排状态(内存缓存)
|
|
546
670
|
_certCache = new Map();
|
|
547
671
|
// 后台任务 handle(浏览器 setInterval/setTimeout)
|
|
@@ -582,7 +706,7 @@ export class AUNClient {
|
|
|
582
706
|
_localAgentMdEtag = '';
|
|
583
707
|
/** gateway 在 RPC envelope._meta.agent_md_etag 注入的服务端 etag;纯观察,无下游依赖。 */
|
|
584
708
|
_remoteAgentMdEtag = '';
|
|
585
|
-
/** 浏览器侧
|
|
709
|
+
/** 浏览器侧 AIDs 逻辑根目录,正文映射到 IndexedDB 里的 {aid}/agent.md。 */
|
|
586
710
|
_agentMdPath = '';
|
|
587
711
|
_agentMdCache = new Map();
|
|
588
712
|
_agentMdFetchInflight = new Set();
|
|
@@ -607,6 +731,14 @@ export class AUNClient {
|
|
|
607
731
|
_reconnectActive = false;
|
|
608
732
|
_reconnectAbort = null;
|
|
609
733
|
_serverKicked = false;
|
|
734
|
+
// 重连状态追踪(对齐 Python client.py)
|
|
735
|
+
_nextRetryAt = null;
|
|
736
|
+
_retryAttempt = 0;
|
|
737
|
+
_retryMaxAttempts = 0;
|
|
738
|
+
_lastError = null;
|
|
739
|
+
_lastErrorCode = null;
|
|
740
|
+
/** 对端 AID 缓存(aid string → AID 对象) */
|
|
741
|
+
_peerCache = new Map();
|
|
610
742
|
/**
|
|
611
743
|
* 缓存最近一次服务端 gateway.disconnect 信息(含 code/reason/detail),
|
|
612
744
|
* 让后续 connection.state(terminal_failed) 也能携带 detail(如配额超限信息)。
|
|
@@ -620,17 +752,32 @@ export class AUNClient {
|
|
|
620
752
|
_logKeystore;
|
|
621
753
|
_logDiscovery;
|
|
622
754
|
_logEvents;
|
|
623
|
-
constructor(
|
|
624
|
-
|
|
755
|
+
constructor(first, second) {
|
|
756
|
+
if (typeof first === 'string') {
|
|
757
|
+
throw new ValidationError('AUNClient aid must be an AID object, not a string');
|
|
758
|
+
}
|
|
759
|
+
if (typeof second === 'boolean') {
|
|
760
|
+
throw new ValidationError('AUNClient debug must be passed as options.debug');
|
|
761
|
+
}
|
|
762
|
+
const inputAid = first instanceof AID ? first : null;
|
|
763
|
+
if (!inputAid && second !== undefined) {
|
|
764
|
+
throw new ValidationError('AUNClient options-only construction accepts a single options object');
|
|
765
|
+
}
|
|
766
|
+
const options = inputAid ? (second ?? {}) : (first ?? {});
|
|
767
|
+
assertClientOptions(options, 'AUNClient options');
|
|
768
|
+
const rawConfig = clientOptionsConfig(options);
|
|
769
|
+
if (inputAid)
|
|
770
|
+
rawConfig.aun_path = inputAid.aunPath;
|
|
771
|
+
const _debug = !!options?.debug;
|
|
625
772
|
this.configModel = createConfig(rawConfig);
|
|
626
|
-
const initAid =
|
|
773
|
+
const initAid = inputAid ? inputAid.aid : null;
|
|
627
774
|
this.config = {
|
|
628
775
|
aun_path: this.configModel.aunPath,
|
|
629
776
|
root_ca_path: this.configModel.rootCaPem,
|
|
630
777
|
seed_password: this.configModel.seedPassword,
|
|
631
778
|
};
|
|
632
779
|
this._agentMdPath = this._agentMdDefaultRoot();
|
|
633
|
-
this._deviceId = getDeviceId();
|
|
780
|
+
this._deviceId = (inputAid?.deviceId) || getDeviceId();
|
|
634
781
|
// Logger 必须最早初始化(其他子模块构造时通过 logger 输出)
|
|
635
782
|
this._logger = new AUNLogger({ debug: _debug, aunPath: this.configModel.aunPath });
|
|
636
783
|
this._logger.bindDeviceId(this._deviceId);
|
|
@@ -644,7 +791,7 @@ export class AUNClient {
|
|
|
644
791
|
this._dispatcher = new EventDispatcher();
|
|
645
792
|
this._discovery = new GatewayDiscovery();
|
|
646
793
|
this._keystore = new IndexedDBKeyStore({ encryptionSeed: this.configModel.seedPassword ?? undefined });
|
|
647
|
-
this._slotId = '';
|
|
794
|
+
this._slotId = inputAid?.slotId || 'default';
|
|
648
795
|
this._connectDeliveryMode = normalizeDeliveryModeConfig({ mode: 'fanout' });
|
|
649
796
|
this._defaultConnectDeliveryMode = { ...this._connectDeliveryMode };
|
|
650
797
|
this._auth = new AuthFlow({
|
|
@@ -667,9 +814,21 @@ export class AUNClient {
|
|
|
667
814
|
this._clientLog.debug(`agent.md meta observer skipped: ${String(exc)}`);
|
|
668
815
|
});
|
|
669
816
|
});
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
817
|
+
if (inputAid) {
|
|
818
|
+
if (!inputAid.isPrivateKeyValid())
|
|
819
|
+
throw new StateError('AUNClient requires an AID with a valid private key');
|
|
820
|
+
this._currentAid = inputAid;
|
|
821
|
+
this._identity = {
|
|
822
|
+
aid: inputAid.aid,
|
|
823
|
+
private_key_pem: inputAid._privateKeyPem ?? '',
|
|
824
|
+
public_key_der_b64: inputAid.publicKey,
|
|
825
|
+
cert: inputAid.certPem,
|
|
826
|
+
};
|
|
827
|
+
this._state = 'disconnected';
|
|
828
|
+
}
|
|
829
|
+
if (options?.protected_headers !== undefined) {
|
|
830
|
+
this.setProtectedHeaders(options.protected_headers);
|
|
831
|
+
}
|
|
673
832
|
// 注入 logger 到各子模块(构造时未传 logger,构造后通过 setLogger 注入)
|
|
674
833
|
this._auth.setLogger(this._logAuth);
|
|
675
834
|
this._transport.setLogger(this._logTransport);
|
|
@@ -677,18 +836,9 @@ export class AUNClient {
|
|
|
677
836
|
if (typeof this._discovery.setLogger === 'function') {
|
|
678
837
|
this._discovery.setLogger(this._logger.for('aun_core.discovery'));
|
|
679
838
|
}
|
|
680
|
-
if (typeof this.auth.setLogger === 'function') {
|
|
681
|
-
this.auth.setLogger(this._logger.for('aun_core.namespace.auth'));
|
|
682
|
-
}
|
|
683
|
-
if (typeof this.custody.setLogger === 'function') {
|
|
684
|
-
this.custody.setLogger(this._logger.for('aun_core.namespace.custody'));
|
|
685
|
-
}
|
|
686
839
|
if (typeof this._keystore.setLogger === 'function') {
|
|
687
840
|
this._keystore.setLogger(this._logKeystore);
|
|
688
841
|
}
|
|
689
|
-
if (typeof this.meta.setLogger === 'function') {
|
|
690
|
-
this.meta.setLogger(this._logger.for('aun_core.namespace.meta'));
|
|
691
|
-
}
|
|
692
842
|
// 内部订阅:推送消息 re-publish 给用户(V2 加密消息走 _raw.peer.v2.message_received)
|
|
693
843
|
this._dispatcher.subscribe('_raw.message.received', (data) => {
|
|
694
844
|
this._onRawMessageReceived(data);
|
|
@@ -739,17 +889,182 @@ export class AUNClient {
|
|
|
739
889
|
get aid() {
|
|
740
890
|
return this._aid;
|
|
741
891
|
}
|
|
742
|
-
|
|
892
|
+
_setAgentMdRoot(root) {
|
|
743
893
|
const next = String(root ?? '').trim() || this._agentMdDefaultRoot();
|
|
744
894
|
this._agentMdPath = next;
|
|
745
895
|
this._agentMdCache.clear();
|
|
746
896
|
return next;
|
|
747
897
|
}
|
|
748
|
-
|
|
749
|
-
|
|
898
|
+
async _resolveAgentMdUrl(aid) {
|
|
899
|
+
const target = String(aid ?? '').trim();
|
|
900
|
+
if (!target)
|
|
901
|
+
throw new ValidationError('agent.md requires non-empty aid');
|
|
902
|
+
let gatewayUrl = String(this._gatewayUrl ?? '').trim();
|
|
903
|
+
if (!gatewayUrl) {
|
|
904
|
+
try {
|
|
905
|
+
gatewayUrl = await this._resolveGatewayForAid(target);
|
|
906
|
+
}
|
|
907
|
+
catch {
|
|
908
|
+
gatewayUrl = '';
|
|
909
|
+
}
|
|
910
|
+
}
|
|
911
|
+
const authority = agentMdAuthority(target, this.configModel.discoveryPort);
|
|
912
|
+
return `${agentMdHttpScheme(gatewayUrl)}://${authority}/agent.md`;
|
|
750
913
|
}
|
|
751
|
-
|
|
752
|
-
|
|
914
|
+
async _ensureAgentMdUploadToken(aid, gatewayUrl) {
|
|
915
|
+
let identity = await this._auth.loadIdentityOrNone(aid);
|
|
916
|
+
if (!identity && this._identity && String(this._identity.aid ?? '') === aid) {
|
|
917
|
+
identity = this._identity;
|
|
918
|
+
}
|
|
919
|
+
if (!identity) {
|
|
920
|
+
throw new StateError('no local identity found, register or load an AID first');
|
|
921
|
+
}
|
|
922
|
+
const cachedToken = String(identity.access_token ?? '');
|
|
923
|
+
const expiresAt = this._auth.getAccessTokenExpiry(identity);
|
|
924
|
+
if (cachedToken && (expiresAt === null || expiresAt > Date.now() / 1000 + 30)) {
|
|
925
|
+
return cachedToken;
|
|
926
|
+
}
|
|
927
|
+
if (identity.refresh_token) {
|
|
928
|
+
try {
|
|
929
|
+
const refreshed = await this._auth.refreshCachedTokens(gatewayUrl, identity);
|
|
930
|
+
const refreshedToken = String(refreshed.access_token ?? '');
|
|
931
|
+
const refreshedExpiry = this._auth.getAccessTokenExpiry(refreshed);
|
|
932
|
+
if (refreshedToken && (refreshedExpiry === null || refreshedExpiry > Date.now() / 1000 + 30)) {
|
|
933
|
+
this._identity = refreshed;
|
|
934
|
+
return refreshedToken;
|
|
935
|
+
}
|
|
936
|
+
}
|
|
937
|
+
catch {
|
|
938
|
+
// refresh 失败时回退到完整 authenticate。
|
|
939
|
+
}
|
|
940
|
+
}
|
|
941
|
+
const result = await this._auth.authenticate(gatewayUrl, aid);
|
|
942
|
+
const token = String(result.access_token ?? '');
|
|
943
|
+
if (!token)
|
|
944
|
+
throw new StateError('authenticate did not return access_token');
|
|
945
|
+
const fallbackIdentity = {
|
|
946
|
+
...identity,
|
|
947
|
+
access_token: token,
|
|
948
|
+
refresh_token: String(result.refresh_token ?? identity.refresh_token ?? ''),
|
|
949
|
+
};
|
|
950
|
+
const fallbackExpiresAt = Number(result.expires_at ?? identity.expires_at ?? NaN);
|
|
951
|
+
if (Number.isFinite(fallbackExpiresAt))
|
|
952
|
+
fallbackIdentity.expires_at = fallbackExpiresAt;
|
|
953
|
+
this._identity = await this._auth.loadIdentityOrNone(aid) ?? fallbackIdentity;
|
|
954
|
+
return token;
|
|
955
|
+
}
|
|
956
|
+
async _uploadAgentMd(content) {
|
|
957
|
+
const target = String(this._aid ?? this._currentAid?.aid ?? '').trim();
|
|
958
|
+
if (!target)
|
|
959
|
+
throw new StateError('uploadAgentMd requires local AID');
|
|
960
|
+
const gatewayUrl = await this._resolveGatewayForAid(target);
|
|
961
|
+
this._gatewayUrl = gatewayUrl;
|
|
962
|
+
const token = await this._ensureAgentMdUploadToken(target, gatewayUrl);
|
|
963
|
+
const response = await fetchWithTimeout(await this._resolveAgentMdUrl(target), {
|
|
964
|
+
method: 'PUT',
|
|
965
|
+
headers: {
|
|
966
|
+
Authorization: `Bearer ${token}`,
|
|
967
|
+
'Content-Type': 'text/markdown; charset=utf-8',
|
|
968
|
+
},
|
|
969
|
+
body: content,
|
|
970
|
+
});
|
|
971
|
+
if (response.status === 404) {
|
|
972
|
+
throw new NotFoundError(`agent.md endpoint not found for aid: ${target}`);
|
|
973
|
+
}
|
|
974
|
+
if (!response.ok) {
|
|
975
|
+
const message = (await response.text()).trim();
|
|
976
|
+
throw new AUNError(`upload agent.md failed: HTTP ${response.status}${message ? ` - ${message}` : ''}`);
|
|
977
|
+
}
|
|
978
|
+
const payload = await response.json();
|
|
979
|
+
if (!isJsonObject(payload))
|
|
980
|
+
throw new AUNError('upload agent.md returned invalid JSON payload');
|
|
981
|
+
return payload;
|
|
982
|
+
}
|
|
983
|
+
async _downloadAgentMd(aid) {
|
|
984
|
+
const target = String(aid ?? '').trim();
|
|
985
|
+
if (!target)
|
|
986
|
+
throw new ValidationError('downloadAgentMd requires non-empty aid');
|
|
987
|
+
const cached = this._agentMdCache.get(target);
|
|
988
|
+
const url = await this._resolveAgentMdUrl(target);
|
|
989
|
+
const response = await fetchWithTimeout(url, {
|
|
990
|
+
method: 'GET',
|
|
991
|
+
headers: { Accept: 'text/markdown' },
|
|
992
|
+
redirect: 'follow',
|
|
993
|
+
});
|
|
994
|
+
if (response.status === 304 && typeof cached?.text === 'string') {
|
|
995
|
+
return String(cached.text);
|
|
996
|
+
}
|
|
997
|
+
if (response.status === 404) {
|
|
998
|
+
throw new NotFoundError(`agent.md not found for aid: ${target}`);
|
|
999
|
+
}
|
|
1000
|
+
if (!response.ok) {
|
|
1001
|
+
const message = (await response.text()).trim();
|
|
1002
|
+
throw new AUNError(`download agent.md failed: HTTP ${response.status}${message ? ` - ${message}` : ''}`);
|
|
1003
|
+
}
|
|
1004
|
+
const text = await response.text();
|
|
1005
|
+
const etag = String(response.headers?.get('ETag') ?? response.headers?.get('etag') ?? '').trim();
|
|
1006
|
+
const lastModified = String(response.headers?.get('Last-Modified') ?? response.headers?.get('last-modified') ?? '').trim();
|
|
1007
|
+
this._agentMdCache.set(target, {
|
|
1008
|
+
...(cached ?? {}),
|
|
1009
|
+
text,
|
|
1010
|
+
etag,
|
|
1011
|
+
lastModified,
|
|
1012
|
+
remote_etag: etag,
|
|
1013
|
+
last_modified: lastModified,
|
|
1014
|
+
});
|
|
1015
|
+
return text;
|
|
1016
|
+
}
|
|
1017
|
+
async _headAgentMd(aid) {
|
|
1018
|
+
const target = String(aid ?? '').trim();
|
|
1019
|
+
if (!target)
|
|
1020
|
+
throw new ValidationError('headAgentMd requires non-empty aid');
|
|
1021
|
+
const response = await fetchWithTimeout(await this._resolveAgentMdUrl(target), {
|
|
1022
|
+
method: 'HEAD',
|
|
1023
|
+
headers: { Accept: 'text/markdown' },
|
|
1024
|
+
});
|
|
1025
|
+
const etag = String(response.headers?.get('ETag') ?? response.headers?.get('etag') ?? '').trim();
|
|
1026
|
+
const lastModified = String(response.headers?.get('Last-Modified') ?? response.headers?.get('last-modified') ?? '').trim();
|
|
1027
|
+
if (response.status === 404) {
|
|
1028
|
+
return { aid: target, found: false, etag: '', last_modified: '', status: 404 };
|
|
1029
|
+
}
|
|
1030
|
+
if (!response.ok) {
|
|
1031
|
+
throw new AUNError(`head agent.md failed: HTTP ${response.status}`);
|
|
1032
|
+
}
|
|
1033
|
+
const cached = this._agentMdCache.get(target) ?? {};
|
|
1034
|
+
this._agentMdCache.set(target, {
|
|
1035
|
+
...cached,
|
|
1036
|
+
etag,
|
|
1037
|
+
lastModified,
|
|
1038
|
+
remote_etag: etag,
|
|
1039
|
+
last_modified: lastModified,
|
|
1040
|
+
});
|
|
1041
|
+
return { aid: target, found: true, etag, last_modified: lastModified, status: response.status };
|
|
1042
|
+
}
|
|
1043
|
+
async _verifyAgentMd(content, aid) {
|
|
1044
|
+
const target = String(aid ?? '').trim();
|
|
1045
|
+
if (!target)
|
|
1046
|
+
throw new ValidationError('verifyAgentMd requires non-empty aid');
|
|
1047
|
+
let peer = target === this._currentAid?.aid ? this._currentAid : null;
|
|
1048
|
+
if (!peer) {
|
|
1049
|
+
let certPem = String(await this._keystore.loadCert(target) ?? '').trim();
|
|
1050
|
+
if (!certPem) {
|
|
1051
|
+
certPem = String(await this._fetchPeerCert(target) ?? '').trim();
|
|
1052
|
+
}
|
|
1053
|
+
if (!certPem)
|
|
1054
|
+
throw new NotFoundError(`certificate not found for aid: ${target}`);
|
|
1055
|
+
peer = await AID.create({
|
|
1056
|
+
aid: target,
|
|
1057
|
+
aunPath: this.configModel.aunPath,
|
|
1058
|
+
certPem,
|
|
1059
|
+
privateKeyPem: null,
|
|
1060
|
+
certValid: true,
|
|
1061
|
+
privateKeyValid: false,
|
|
1062
|
+
});
|
|
1063
|
+
}
|
|
1064
|
+
const result = await peer.verifyAgentMd(content);
|
|
1065
|
+
if (!result.ok)
|
|
1066
|
+
throw new AUNError(result.error.message);
|
|
1067
|
+
return { ...result.data, verified: result.data.status === 'verified' };
|
|
753
1068
|
}
|
|
754
1069
|
/**
|
|
755
1070
|
* 浏览器版本 publishAgentMd。默认从 {agentMdPath}/{self_aid}/agent.md 的等价 IndexedDB 正文读取,
|
|
@@ -777,8 +1092,12 @@ export class AUNClient {
|
|
|
777
1092
|
if (localContent === null || localContent.length === 0) {
|
|
778
1093
|
throw new ValidationError('publishAgentMd requires local agent.md content');
|
|
779
1094
|
}
|
|
780
|
-
const
|
|
781
|
-
|
|
1095
|
+
const signedResult = await this._currentAid?.signAgentMd(localContent);
|
|
1096
|
+
if (!signedResult?.ok) {
|
|
1097
|
+
throw new StateError(signedResult?.error.message ?? 'publishAgentMd requires a valid local AID private key');
|
|
1098
|
+
}
|
|
1099
|
+
const signed = signedResult.data.signed;
|
|
1100
|
+
const result = await this._uploadAgentMd(signed);
|
|
782
1101
|
this._localAgentMdEtag = await this._agentMdContentEtag(signed);
|
|
783
1102
|
const remoteEtag = isJsonObject(result) ? String(result.etag ?? '').trim() : '';
|
|
784
1103
|
if (remoteEtag)
|
|
@@ -798,13 +1117,13 @@ export class AUNClient {
|
|
|
798
1117
|
* 浏览器版本 fetchAgentMd。aid 缺省时取自身;下载后的正文固定写入
|
|
799
1118
|
* {agentMdPath}/{aid}/agent.md 的等价 IndexedDB 正文,agentmd.json 只保存元数据。
|
|
800
1119
|
*/
|
|
801
|
-
async
|
|
1120
|
+
async _fetchAgentMdCache(aid) {
|
|
802
1121
|
const target = String(aid ?? this._aid ?? '').trim();
|
|
803
1122
|
if (!target) {
|
|
804
1123
|
throw new ValidationError('fetchAgentMd requires aid (or local AID)');
|
|
805
1124
|
}
|
|
806
|
-
const content = await this.
|
|
807
|
-
const signature = await this.
|
|
1125
|
+
const content = await this._downloadAgentMd(target);
|
|
1126
|
+
const signature = await this._verifyAgentMd(content, target);
|
|
808
1127
|
const isSelf = target === (this._aid ?? '');
|
|
809
1128
|
const localEtag = await this._agentMdContentEtag(content);
|
|
810
1129
|
const cacheMeta = this._agentMdAuthCacheMeta(target);
|
|
@@ -853,7 +1172,7 @@ export class AUNClient {
|
|
|
853
1172
|
return String(this._aid ?? '').trim();
|
|
854
1173
|
}
|
|
855
1174
|
_agentMdDefaultRoot() {
|
|
856
|
-
return this._joinAgentMdPath(this.configModel.aunPath || '.', '
|
|
1175
|
+
return this._joinAgentMdPath(this.configModel.aunPath || '.', 'AIDs');
|
|
857
1176
|
}
|
|
858
1177
|
_joinAgentMdPath(base, name) {
|
|
859
1178
|
const left = String(base ?? '').trim().replace(/[\\/]+$/g, '');
|
|
@@ -960,8 +1279,7 @@ export class AUNClient {
|
|
|
960
1279
|
}
|
|
961
1280
|
_agentMdAuthCacheMeta(aid) {
|
|
962
1281
|
try {
|
|
963
|
-
const
|
|
964
|
-
const record = store?.get(String(aid ?? '').trim());
|
|
1282
|
+
const record = this._agentMdCache.get(String(aid ?? '').trim());
|
|
965
1283
|
return record && typeof record === 'object' ? { ...record } : {};
|
|
966
1284
|
}
|
|
967
1285
|
catch {
|
|
@@ -1086,7 +1404,7 @@ export class AUNClient {
|
|
|
1086
1404
|
return;
|
|
1087
1405
|
this._agentMdFetchInflight.add(target);
|
|
1088
1406
|
try {
|
|
1089
|
-
await this.
|
|
1407
|
+
await this._fetchAgentMdCache(target);
|
|
1090
1408
|
}
|
|
1091
1409
|
catch (err) {
|
|
1092
1410
|
await this._saveAgentMdRecord(target, {
|
|
@@ -1147,7 +1465,7 @@ export class AUNClient {
|
|
|
1147
1465
|
}
|
|
1148
1466
|
await this._observeAgentMdMeta(senderAid, String(sender.etag ?? '').trim(), String(sender.last_modified ?? sender.lastModified ?? '').trim(), 'envelope');
|
|
1149
1467
|
}
|
|
1150
|
-
async
|
|
1468
|
+
async _checkAgentMdCache(aid, maxUnsyncedDays = 0) {
|
|
1151
1469
|
const target = String(aid ?? this._aid ?? '').trim();
|
|
1152
1470
|
if (!target)
|
|
1153
1471
|
throw new ValidationError('checkAgentMd requires aid (or local AID)');
|
|
@@ -1177,7 +1495,7 @@ export class AUNClient {
|
|
|
1177
1495
|
const now = Date.now();
|
|
1178
1496
|
let remote;
|
|
1179
1497
|
try {
|
|
1180
|
-
remote = await this.
|
|
1498
|
+
remote = await this._headAgentMd(target);
|
|
1181
1499
|
}
|
|
1182
1500
|
catch (err) {
|
|
1183
1501
|
await this._saveAgentMdRecord(target, { checked_at: now, remote_status: 'error', last_error: err instanceof Error ? err.message : String(err) });
|
|
@@ -1231,7 +1549,116 @@ export class AUNClient {
|
|
|
1231
1549
|
}
|
|
1232
1550
|
}
|
|
1233
1551
|
get state() {
|
|
1234
|
-
return this._state;
|
|
1552
|
+
return this._publicState(this._state);
|
|
1553
|
+
}
|
|
1554
|
+
_publicState(state) {
|
|
1555
|
+
return STATE_TO_PUBLIC[state] ?? state;
|
|
1556
|
+
}
|
|
1557
|
+
get currentAid() {
|
|
1558
|
+
return this._currentAid;
|
|
1559
|
+
}
|
|
1560
|
+
get hasIdentity() {
|
|
1561
|
+
return this._currentAid !== null && this.state !== ConnectionState.CLOSED;
|
|
1562
|
+
}
|
|
1563
|
+
get canSign() {
|
|
1564
|
+
return this.hasIdentity && !!this._currentAid?.isPrivateKeyValid();
|
|
1565
|
+
}
|
|
1566
|
+
get canConnect() {
|
|
1567
|
+
return this.hasIdentity && this.state !== ConnectionState.CLOSED;
|
|
1568
|
+
}
|
|
1569
|
+
get canSend() {
|
|
1570
|
+
return this.state === ConnectionState.READY;
|
|
1571
|
+
}
|
|
1572
|
+
get isReady() { return this.canSend; }
|
|
1573
|
+
get isOnline() {
|
|
1574
|
+
return this.state === ConnectionState.READY
|
|
1575
|
+
|| this.state === ConnectionState.RECONNECTING
|
|
1576
|
+
|| this.state === ConnectionState.RETRY_BACKOFF;
|
|
1577
|
+
}
|
|
1578
|
+
get isClosed() { return this.state === ConnectionState.CLOSED; }
|
|
1579
|
+
get aunPath() { return this.hasIdentity ? this._currentAid?.aunPath ?? this.configModel.aunPath : null; }
|
|
1580
|
+
/** 下次重连时间(仅在 retry_backoff 状态时非 null,对齐 Python next_retry_at) */
|
|
1581
|
+
get nextRetryAt() {
|
|
1582
|
+
return this.state === ConnectionState.RETRY_BACKOFF ? this._nextRetryAt : null;
|
|
1583
|
+
}
|
|
1584
|
+
/** 距下次重连的剩余秒数(仅在 retry_backoff 状态时非 null,对齐 Python next_retry_in_seconds) */
|
|
1585
|
+
get nextRetryInSeconds() {
|
|
1586
|
+
const t = this.nextRetryAt;
|
|
1587
|
+
if (t === null)
|
|
1588
|
+
return null;
|
|
1589
|
+
return Math.max(0, (t.getTime() - Date.now()) / 1000);
|
|
1590
|
+
}
|
|
1591
|
+
/** 当前重连尝试次数(对齐 Python retry_attempt) */
|
|
1592
|
+
get retryAttempt() { return this._retryAttempt; }
|
|
1593
|
+
/** 最大重连次数(0 = 无限,对齐 Python retry_max_attempts) */
|
|
1594
|
+
get retryMaxAttempts() { return this._retryMaxAttempts; }
|
|
1595
|
+
/** 最近一次错误(对齐 Python last_error) */
|
|
1596
|
+
get lastError() { return this._lastError; }
|
|
1597
|
+
/** 最近一次错误码(对齐 Python last_error_code) */
|
|
1598
|
+
get lastErrorCode() { return this._lastErrorCode; }
|
|
1599
|
+
loadIdentity(aid) {
|
|
1600
|
+
if (!aid?.isPrivateKeyValid())
|
|
1601
|
+
throw new StateError('loadIdentity requires an AID with a valid private key');
|
|
1602
|
+
const publicState = this.state;
|
|
1603
|
+
if (publicState !== ConnectionState.NO_IDENTITY && publicState !== ConnectionState.CLOSED) {
|
|
1604
|
+
throw new StateError(`loadIdentity not allowed in state ${publicState}`);
|
|
1605
|
+
}
|
|
1606
|
+
this._currentAid = aid;
|
|
1607
|
+
this._aid = aid.aid;
|
|
1608
|
+
this._identity = {
|
|
1609
|
+
aid: aid.aid,
|
|
1610
|
+
private_key_pem: aid._privateKeyPem ?? '',
|
|
1611
|
+
public_key_der_b64: aid.publicKey,
|
|
1612
|
+
cert: aid.certPem,
|
|
1613
|
+
};
|
|
1614
|
+
this._auth._aid = aid.aid;
|
|
1615
|
+
this._state = 'disconnected';
|
|
1616
|
+
this._closing = false;
|
|
1617
|
+
}
|
|
1618
|
+
setProtectedHeaders(headers) {
|
|
1619
|
+
if (!headers) {
|
|
1620
|
+
this._instanceProtectedHeaders = null;
|
|
1621
|
+
return;
|
|
1622
|
+
}
|
|
1623
|
+
const cleaned = {};
|
|
1624
|
+
for (const [key, value] of Object.entries(headers)) {
|
|
1625
|
+
if (key === '_auth')
|
|
1626
|
+
continue;
|
|
1627
|
+
cleaned[String(key)] = String(value);
|
|
1628
|
+
}
|
|
1629
|
+
this._instanceProtectedHeaders = Object.keys(cleaned).length ? cleaned : null;
|
|
1630
|
+
}
|
|
1631
|
+
getProtectedHeaders() {
|
|
1632
|
+
return this._instanceProtectedHeaders ? { ...this._instanceProtectedHeaders } : null;
|
|
1633
|
+
}
|
|
1634
|
+
cachePeer(aid) {
|
|
1635
|
+
if (!this.hasIdentity)
|
|
1636
|
+
throw new StateError('cachePeer requires a loaded identity');
|
|
1637
|
+
if (!aid.isCertValid())
|
|
1638
|
+
throw new ValidationError('cachePeer requires an AID with a valid certificate');
|
|
1639
|
+
this._peerCache.set(aid.aid, aid);
|
|
1640
|
+
return aid;
|
|
1641
|
+
}
|
|
1642
|
+
getPeer(aid) {
|
|
1643
|
+
if (!this.hasIdentity)
|
|
1644
|
+
throw new StateError('getPeer requires a loaded identity');
|
|
1645
|
+
return this._peerCache.get(String(aid ?? '').trim()) ?? null;
|
|
1646
|
+
}
|
|
1647
|
+
async lookupPeer(aid) {
|
|
1648
|
+
if (!this.hasIdentity)
|
|
1649
|
+
throw new StateError('lookupPeer requires a loaded identity');
|
|
1650
|
+
const target = String(aid ?? '').trim();
|
|
1651
|
+
if (!target)
|
|
1652
|
+
throw new ValidationError('lookupPeer requires non-empty aid');
|
|
1653
|
+
const cached = this._peerCache.get(target);
|
|
1654
|
+
if (cached)
|
|
1655
|
+
return cached;
|
|
1656
|
+
throw new NotFoundError(`peer not found in cache: ${target}`);
|
|
1657
|
+
}
|
|
1658
|
+
peers() {
|
|
1659
|
+
if (!this.hasIdentity)
|
|
1660
|
+
throw new StateError('peers requires a loaded identity');
|
|
1661
|
+
return [...this._peerCache.entries()].sort(([a], [b]) => a.localeCompare(b)).map(([, v]) => v);
|
|
1235
1662
|
}
|
|
1236
1663
|
get gatewayUrl() {
|
|
1237
1664
|
return this._gatewayUrl;
|
|
@@ -1246,36 +1673,65 @@ export class AUNClient {
|
|
|
1246
1673
|
get gatewayHealth() {
|
|
1247
1674
|
return this._discovery.lastHealthy;
|
|
1248
1675
|
}
|
|
1249
|
-
|
|
1250
|
-
|
|
1676
|
+
// ── 生命周期 ──────────────────────────────────────
|
|
1677
|
+
/** 仅认证当前身份,获取/刷新 token,但不建立长连接。 */
|
|
1678
|
+
async authenticate(options = {}) {
|
|
1251
1679
|
const tStart = Date.now();
|
|
1252
|
-
this.
|
|
1680
|
+
const target = this._currentAid?.aid ?? this._aid ?? '';
|
|
1681
|
+
if (!target || !this._currentAid?.isPrivateKeyValid()) {
|
|
1682
|
+
throw new StateError('authenticate requires a loaded AID with a valid private key');
|
|
1683
|
+
}
|
|
1684
|
+
const publicState = this.state;
|
|
1685
|
+
if (publicState !== ConnectionState.STANDBY && publicState !== ConnectionState.AUTHENTICATED) {
|
|
1686
|
+
throw new StateError(`authenticate not allowed in state ${publicState}`);
|
|
1687
|
+
}
|
|
1688
|
+
if ('aid' in options || 'access_token' in options || 'token' in options || 'kite_token' in options) {
|
|
1689
|
+
throw new ValidationError('authenticate options must not include aid or token fields; load an AID object first');
|
|
1690
|
+
}
|
|
1691
|
+
this._state = 'connecting';
|
|
1253
1692
|
try {
|
|
1254
|
-
const
|
|
1255
|
-
this.
|
|
1693
|
+
const gateway = String(options.gateway ?? this._gatewayUrl ?? await this._resolveGatewayForAid(target)).trim();
|
|
1694
|
+
const result = await this._auth.authenticate(gateway, target);
|
|
1695
|
+
this._gatewayUrl = String(result.gateway ?? gateway);
|
|
1696
|
+
this._identity = await this._auth.loadIdentityOrNone(target);
|
|
1697
|
+
this._state = 'authenticated';
|
|
1698
|
+
this._clientLog.debug(`authenticate exit: elapsed=${Date.now() - tStart}ms aid=${target}`);
|
|
1256
1699
|
return result;
|
|
1257
1700
|
}
|
|
1258
1701
|
catch (err) {
|
|
1259
|
-
this.
|
|
1702
|
+
this._state = 'disconnected';
|
|
1703
|
+
this._clientLog.debug(`authenticate exit (error): elapsed=${Date.now() - tStart}ms err=${err instanceof Error ? err.message : String(err)}`);
|
|
1260
1704
|
throw err;
|
|
1261
1705
|
}
|
|
1262
1706
|
}
|
|
1263
|
-
|
|
1264
|
-
|
|
1265
|
-
* 连接到 Gateway。
|
|
1266
|
-
*
|
|
1267
|
-
* @param auth - 认证参数,必须包含 access_token 和 gateway
|
|
1268
|
-
* @param options - 可选的会话选项(auto_reconnect, heartbeat_interval 等)
|
|
1269
|
-
*/
|
|
1270
|
-
async connect(auth, options) {
|
|
1707
|
+
/** 连接到 Gateway;身份来自构造函数或 loadIdentity(aid),认证由 SDK 内部自动完成。 */
|
|
1708
|
+
async connect(options = {}) {
|
|
1271
1709
|
const tStart = Date.now();
|
|
1272
1710
|
this._clientLog.debug(`connect enter: state=${this._state} aid=${this._aid ?? '-'}`);
|
|
1273
|
-
if (
|
|
1711
|
+
if (arguments.length > 1) {
|
|
1712
|
+
throw new ValidationError('connect accepts a single options object');
|
|
1713
|
+
}
|
|
1714
|
+
if ('aid' in options || 'access_token' in options || 'token' in options || 'kite_token' in options) {
|
|
1715
|
+
throw new ValidationError('connect options must not include aid or token fields; load an AID object first');
|
|
1716
|
+
}
|
|
1717
|
+
const target = this._currentAid?.aid ?? this._aid ?? '';
|
|
1718
|
+
if (!target || !this._currentAid?.isPrivateKeyValid()) {
|
|
1719
|
+
throw new StateError('connect requires a loaded AID with a valid private key');
|
|
1720
|
+
}
|
|
1721
|
+
const publicState = this.state;
|
|
1722
|
+
const allowed = new Set([
|
|
1723
|
+
ConnectionState.STANDBY,
|
|
1724
|
+
ConnectionState.AUTHENTICATED,
|
|
1725
|
+
ConnectionState.RETRY_BACKOFF,
|
|
1726
|
+
ConnectionState.CONNECTION_FAILED,
|
|
1727
|
+
]);
|
|
1728
|
+
if (!allowed.has(publicState)) {
|
|
1274
1729
|
this._clientLog.debug(`connect exit (error): elapsed=${Date.now() - tStart}ms err=invalid_state state=${this._state}`);
|
|
1275
|
-
throw new StateError(`connect not allowed in state ${
|
|
1730
|
+
throw new StateError(`connect not allowed in state ${publicState}`);
|
|
1276
1731
|
}
|
|
1277
1732
|
this._state = 'connecting';
|
|
1278
|
-
const
|
|
1733
|
+
const gateway = String(options.gateway ?? this._gatewayUrl ?? await this._resolveGatewayForAid(target)).trim();
|
|
1734
|
+
const params = { ...options, gateway };
|
|
1279
1735
|
const normalized = this._normalizeConnectParams(params);
|
|
1280
1736
|
this._sessionParams = normalized;
|
|
1281
1737
|
this._sessionOptions = this._buildSessionOptions(normalized);
|
|
@@ -1286,7 +1742,7 @@ export class AUNClient {
|
|
|
1286
1742
|
for (const gw of gateways) {
|
|
1287
1743
|
try {
|
|
1288
1744
|
const gwParams = { ...normalized, gateway: gw };
|
|
1289
|
-
await this._connectOnce(gwParams,
|
|
1745
|
+
await this._connectOnce(gwParams, true);
|
|
1290
1746
|
this._clientLog.debug(`connect exit: elapsed=${Date.now() - tStart}ms state=${this._state}`);
|
|
1291
1747
|
return;
|
|
1292
1748
|
}
|
|
@@ -1301,7 +1757,7 @@ export class AUNClient {
|
|
|
1301
1757
|
}
|
|
1302
1758
|
}
|
|
1303
1759
|
if (this._state === 'connecting' || this._state === 'authenticating') {
|
|
1304
|
-
this._state = '
|
|
1760
|
+
this._state = 'terminal_failed';
|
|
1305
1761
|
}
|
|
1306
1762
|
this._clientLog.debug(`connect exit (error): elapsed=${Date.now() - tStart}ms err=${lastErr instanceof Error ? lastErr.message : String(lastErr)}`);
|
|
1307
1763
|
throw lastErr;
|
|
@@ -1323,56 +1779,9 @@ export class AUNClient {
|
|
|
1323
1779
|
}
|
|
1324
1780
|
await this._transport.close();
|
|
1325
1781
|
this._state = 'disconnected';
|
|
1326
|
-
await this._dispatcher.publish('connection.state', { state: this._state });
|
|
1782
|
+
await this._dispatcher.publish('connection.state', { state: this._publicState(this._state) });
|
|
1327
1783
|
this._clientLog.debug(`disconnect exit: elapsed=${Date.now() - tStart}ms`);
|
|
1328
1784
|
}
|
|
1329
|
-
/** 列出本地所有已存储的身份摘要(仅返回有有效私钥的 AID) */
|
|
1330
|
-
async listIdentities() {
|
|
1331
|
-
const tStart = Date.now();
|
|
1332
|
-
this._clientLog.debug('listIdentities enter');
|
|
1333
|
-
try {
|
|
1334
|
-
const listFn = this._keystore.listIdentities;
|
|
1335
|
-
if (typeof listFn !== 'function') {
|
|
1336
|
-
this._clientLog.debug(`listIdentities exit: elapsed=${Date.now() - tStart}ms count=0 reason=keystore_no_list`);
|
|
1337
|
-
return [];
|
|
1338
|
-
}
|
|
1339
|
-
const aids = await listFn.call(this._keystore);
|
|
1340
|
-
const summaries = [];
|
|
1341
|
-
for (const aid of [...aids].sort()) {
|
|
1342
|
-
const identity = await this._keystore.loadIdentity(aid);
|
|
1343
|
-
if (!identity || !identity.private_key_pem)
|
|
1344
|
-
continue;
|
|
1345
|
-
const summary = { aid };
|
|
1346
|
-
// 优先从 loadMetadata 获取
|
|
1347
|
-
const loadMeta = this._keystore.loadMetadata;
|
|
1348
|
-
if (typeof loadMeta === 'function') {
|
|
1349
|
-
const md = await loadMeta.call(this._keystore, aid);
|
|
1350
|
-
if (md && Object.keys(md).length > 0) {
|
|
1351
|
-
summary.metadata = md;
|
|
1352
|
-
}
|
|
1353
|
-
}
|
|
1354
|
-
// 回退:从 identity 中提取非核心字段
|
|
1355
|
-
if (!summary.metadata) {
|
|
1356
|
-
const metadata = {};
|
|
1357
|
-
for (const [key, value] of Object.entries(identity)) {
|
|
1358
|
-
if (!['aid', 'private_key_pem', 'public_key_der_b64', 'curve', 'cert'].includes(key)) {
|
|
1359
|
-
metadata[key] = value;
|
|
1360
|
-
}
|
|
1361
|
-
}
|
|
1362
|
-
if (Object.keys(metadata).length > 0) {
|
|
1363
|
-
summary.metadata = metadata;
|
|
1364
|
-
}
|
|
1365
|
-
}
|
|
1366
|
-
summaries.push(summary);
|
|
1367
|
-
}
|
|
1368
|
-
this._clientLog.debug(`listIdentities exit: elapsed=${Date.now() - tStart}ms count=${summaries.length}`);
|
|
1369
|
-
return summaries;
|
|
1370
|
-
}
|
|
1371
|
-
catch (err) {
|
|
1372
|
-
this._clientLog.debug(`listIdentities exit (error): elapsed=${Date.now() - tStart}ms err=${err instanceof Error ? err.message : String(err)}`);
|
|
1373
|
-
throw err;
|
|
1374
|
-
}
|
|
1375
|
-
}
|
|
1376
1785
|
/** 关闭连接 */
|
|
1377
1786
|
async close() {
|
|
1378
1787
|
const tStart = Date.now();
|
|
@@ -1401,7 +1810,7 @@ export class AUNClient {
|
|
|
1401
1810
|
}
|
|
1402
1811
|
await this._transport.close();
|
|
1403
1812
|
this._state = 'closed';
|
|
1404
|
-
await this._dispatcher.publish('connection.state', { state: this._state });
|
|
1813
|
+
await this._dispatcher.publish('connection.state', { state: this._publicState(this._state) });
|
|
1405
1814
|
this._resetSeqTrackingState();
|
|
1406
1815
|
this._clientLog.debug(`close exit: elapsed=${Date.now() - tStart}ms`);
|
|
1407
1816
|
}
|
|
@@ -1436,6 +1845,10 @@ export class AUNClient {
|
|
|
1436
1845
|
throw new PermissionError(`legacy E2EE method is removed in this SDK: ${method}`);
|
|
1437
1846
|
}
|
|
1438
1847
|
const p = { ...(params ?? {}) };
|
|
1848
|
+
if (this._instanceProtectedHeaders && PROTECTED_HEADERS_METHODS.has(method)) {
|
|
1849
|
+
const existing = isJsonObject(p.protected_headers) ? p.protected_headers : {};
|
|
1850
|
+
p.protected_headers = { ...this._instanceProtectedHeaders, ...existing };
|
|
1851
|
+
}
|
|
1439
1852
|
if (method === 'message.send' || method === 'group.send') {
|
|
1440
1853
|
this._normalizeOutboundMessagePayload(p, method);
|
|
1441
1854
|
}
|
|
@@ -1466,7 +1879,7 @@ export class AUNClient {
|
|
|
1466
1879
|
throw new StateError('V2 session not initialized; encrypted message.send requires V2 (V1 E2EE removed)');
|
|
1467
1880
|
}
|
|
1468
1881
|
this._clientLog.debug('call route: message.send → V2 encrypted send');
|
|
1469
|
-
return await this.
|
|
1882
|
+
return await this._sendV2(String(p.to ?? ''), p.payload ?? {}, {
|
|
1470
1883
|
messageId: String(p.message_id ?? '') || undefined,
|
|
1471
1884
|
timestamp: p.timestamp,
|
|
1472
1885
|
protectedHeaders: this._protectedHeadersFromParams(p),
|
|
@@ -1485,7 +1898,7 @@ export class AUNClient {
|
|
|
1485
1898
|
throw new StateError('V2 session not initialized; encrypted group.send requires V2 (V1 E2EE removed)');
|
|
1486
1899
|
}
|
|
1487
1900
|
this._clientLog.debug('call route: group.send → V2 encrypted send');
|
|
1488
|
-
return await this.
|
|
1901
|
+
return await this._sendGroupV2(String(p.group_id ?? ''), p.payload ?? {}, {
|
|
1489
1902
|
messageId: String(p.message_id ?? '') || undefined,
|
|
1490
1903
|
timestamp: p.timestamp,
|
|
1491
1904
|
protectedHeaders: this._protectedHeadersFromParams(p),
|
|
@@ -1533,24 +1946,24 @@ export class AUNClient {
|
|
|
1533
1946
|
// message.pull:V2 就绪时走 V2 pull
|
|
1534
1947
|
if (method === 'message.pull' && this._v2Session) {
|
|
1535
1948
|
this._clientLog.debug('call route: message.pull → V2 pull');
|
|
1536
|
-
const messages = await this.
|
|
1949
|
+
const messages = await this._pullV2(Number(p.after_seq ?? 0) || 0, Number(p.limit ?? 50) || 50, { force: p.force === true });
|
|
1537
1950
|
return { messages };
|
|
1538
1951
|
}
|
|
1539
1952
|
// message.ack:V2 就绪时走 V2 ack
|
|
1540
1953
|
if (method === 'message.ack' && this._v2Session) {
|
|
1541
1954
|
this._clientLog.debug('call route: message.ack → V2 ack');
|
|
1542
|
-
return await this.
|
|
1955
|
+
return await this._ackV2(Number(p.seq ?? p.up_to_seq ?? 0) || undefined);
|
|
1543
1956
|
}
|
|
1544
1957
|
// group.pull:V2 就绪时走 V2 pull
|
|
1545
1958
|
if (method === 'group.pull' && this._v2Session && p.group_id) {
|
|
1546
1959
|
this._clientLog.debug('call route: group.pull → V2 pull');
|
|
1547
|
-
const messages = await this.
|
|
1960
|
+
const messages = await this._pullGroupV2(String(p.group_id), Number(p.after_seq ?? p.after_message_seq ?? 0) || 0, Number(p.limit ?? 50) || 50);
|
|
1548
1961
|
return { messages };
|
|
1549
1962
|
}
|
|
1550
1963
|
// group.ack_messages:V2 就绪时走 V2 ack
|
|
1551
1964
|
if (method === 'group.ack_messages' && this._v2Session && p.group_id) {
|
|
1552
1965
|
this._clientLog.debug('call route: group.ack_messages → V2 ack');
|
|
1553
|
-
return await this.
|
|
1966
|
+
return await this._ackGroupV2(String(p.group_id), Number(p.seq ?? p.msg_seq ?? p.up_to_seq ?? 0) || undefined);
|
|
1554
1967
|
}
|
|
1555
1968
|
// 关键操作自动附加客户端签名
|
|
1556
1969
|
if (SIGNED_METHODS.has(method)) {
|
|
@@ -1651,15 +2064,29 @@ export class AUNClient {
|
|
|
1651
2064
|
// ── Group E2EE 自动编排已移除(V2-only:由 group.v2.bootstrap 驱动)────────
|
|
1652
2065
|
return result;
|
|
1653
2066
|
}
|
|
1654
|
-
|
|
1655
|
-
|
|
1656
|
-
|
|
1657
|
-
|
|
1658
|
-
|
|
1659
|
-
|
|
1660
|
-
|
|
1661
|
-
|
|
1662
|
-
|
|
2067
|
+
async _callRawV2Rpc(method, params) {
|
|
2068
|
+
const p = { ...(params ?? {}) };
|
|
2069
|
+
delete p._pull_gate_locked;
|
|
2070
|
+
delete p._skip_auto_ack;
|
|
2071
|
+
delete p.skip_auto_ack;
|
|
2072
|
+
if (method.startsWith('group.') && p.group_id !== undefined && p.group_id !== null) {
|
|
2073
|
+
p.group_id = normalizeGroupId(String(p.group_id)) || String(p.group_id);
|
|
2074
|
+
}
|
|
2075
|
+
if (method.startsWith('group.') && p.device_id === undefined) {
|
|
2076
|
+
p.device_id = this._deviceId;
|
|
2077
|
+
}
|
|
2078
|
+
if (method.startsWith('group.') && p.slot_id === undefined) {
|
|
2079
|
+
p.slot_id = this._slotId;
|
|
2080
|
+
}
|
|
2081
|
+
if (SIGNED_METHODS.has(method)) {
|
|
2082
|
+
if (this._shouldSkipClientSignature(method, p)) {
|
|
2083
|
+
delete p.client_signature;
|
|
2084
|
+
}
|
|
2085
|
+
else {
|
|
2086
|
+
await this._signClientOperation(method, p);
|
|
2087
|
+
}
|
|
2088
|
+
}
|
|
2089
|
+
return await this._transport.call(method, p);
|
|
1663
2090
|
}
|
|
1664
2091
|
// ── 事件 ──────────────────────────────────────────
|
|
1665
2092
|
/**
|
|
@@ -1695,12 +2122,19 @@ export class AUNClient {
|
|
|
1695
2122
|
}
|
|
1696
2123
|
// P2P 空洞检测
|
|
1697
2124
|
const seq = msg.seq;
|
|
2125
|
+
const encryptedPush = isEncryptedPushMessage(msg);
|
|
1698
2126
|
if (seq !== undefined && seq !== null && this._aid) {
|
|
1699
2127
|
const ns = `p2p:${this._aid}`;
|
|
1700
2128
|
// Push 修上界:先更新 maxSeenSeq
|
|
1701
2129
|
if (seq > 0)
|
|
1702
2130
|
this._seqTracker.updateMaxSeen(ns, seq);
|
|
1703
|
-
const
|
|
2131
|
+
const contigBefore = this._seqTracker.getContiguousSeq(ns);
|
|
2132
|
+
const seqNeedsPull = this._seqTracker.onMessageSeq(ns, seq);
|
|
2133
|
+
const published = encryptedPush
|
|
2134
|
+
? await this._publishEncryptedPushMessage('message.received', 'message.undecryptable', ns, seq, msg, false)
|
|
2135
|
+
: await this._publishOrderedMessage('message.received', ns, seq, msg);
|
|
2136
|
+
const contigAfter = this._seqTracker.getContiguousSeq(ns);
|
|
2137
|
+
const needPull = seqNeedsPull && !published;
|
|
1704
2138
|
if (needPull) {
|
|
1705
2139
|
this._safeAsync(this._fillP2pGap());
|
|
1706
2140
|
}
|
|
@@ -1716,14 +2150,16 @@ export class AUNClient {
|
|
|
1716
2150
|
}).catch((e) => { this._clientLog.warn(`P2P auto-ack failed:${String(e)}`); });
|
|
1717
2151
|
}
|
|
1718
2152
|
// 即时持久化 cursor,异常断连后不回退
|
|
1719
|
-
|
|
1720
|
-
|
|
1721
|
-
|
|
1722
|
-
|
|
1723
|
-
const ns = `p2p:${this._aid}`;
|
|
1724
|
-
await this._publishOrderedMessage('message.received', ns, seq, msg);
|
|
2153
|
+
if (contigAfter !== contigBefore)
|
|
2154
|
+
this._saveSeqTrackerState();
|
|
2155
|
+
if (encryptedPush)
|
|
2156
|
+
return;
|
|
1725
2157
|
}
|
|
1726
2158
|
else {
|
|
2159
|
+
if (encryptedPush) {
|
|
2160
|
+
await this._publishEncryptedPushMessage('message.received', 'message.undecryptable', '', seq ?? 0, msg, false);
|
|
2161
|
+
return;
|
|
2162
|
+
}
|
|
1727
2163
|
await this._publishAppEvent('message.received', msg);
|
|
1728
2164
|
}
|
|
1729
2165
|
}
|
|
@@ -1789,7 +2225,7 @@ export class AUNClient {
|
|
|
1789
2225
|
this._gapFillDone.add(dedupKey);
|
|
1790
2226
|
try {
|
|
1791
2227
|
this._clientLog.debug(`_onRawGroupV2MessageCreated -> group.v2.pull group=${groupId} after_seq=${afterSeq}`);
|
|
1792
|
-
const messages = await this.
|
|
2228
|
+
const messages = await this._pullGroupV2(groupId, afterSeq, 50);
|
|
1793
2229
|
this._clientLog.debug(`_onRawGroupV2MessageCreated pulled ${messages.length} msgs for group=${groupId}`);
|
|
1794
2230
|
}
|
|
1795
2231
|
finally {
|
|
@@ -1830,12 +2266,19 @@ export class AUNClient {
|
|
|
1830
2266
|
return;
|
|
1831
2267
|
}
|
|
1832
2268
|
// seq 跟踪 + auto-ack
|
|
2269
|
+
const encryptedPush = isEncryptedPushMessage(msg);
|
|
1833
2270
|
if (groupId && seq !== undefined && seq !== null) {
|
|
1834
2271
|
const ns = `group:${groupId}`;
|
|
1835
2272
|
// Push 修上界:先更新 maxSeenSeq
|
|
1836
2273
|
if (seq > 0)
|
|
1837
2274
|
this._seqTracker.updateMaxSeen(ns, seq);
|
|
1838
|
-
const
|
|
2275
|
+
const contigBefore = this._seqTracker.getContiguousSeq(ns);
|
|
2276
|
+
const seqNeedsPull = this._seqTracker.onMessageSeq(ns, seq);
|
|
2277
|
+
const published = encryptedPush
|
|
2278
|
+
? await this._publishEncryptedPushMessage('group.message_created', 'group.message_undecryptable', ns, seq, msg, true)
|
|
2279
|
+
: await this._publishOrderedMessage('group.message_created', ns, seq, msg);
|
|
2280
|
+
const contigAfter = this._seqTracker.getContiguousSeq(ns);
|
|
2281
|
+
const needPull = seqNeedsPull && !published;
|
|
1839
2282
|
if (needPull) {
|
|
1840
2283
|
this._safeAsync(this._fillGroupGap(groupId));
|
|
1841
2284
|
}
|
|
@@ -1850,14 +2293,16 @@ export class AUNClient {
|
|
|
1850
2293
|
slot_id: this._slotId,
|
|
1851
2294
|
}).catch((e) => { this._clientLog.warn('group message auto-ack failed: group=' + groupId, e); });
|
|
1852
2295
|
}
|
|
1853
|
-
|
|
1854
|
-
|
|
1855
|
-
|
|
1856
|
-
|
|
1857
|
-
const nsKey = `group:${groupId}`;
|
|
1858
|
-
await this._publishOrderedMessage('group.message_created', nsKey, seq, msg);
|
|
2296
|
+
if (contigAfter !== contigBefore)
|
|
2297
|
+
this._saveSeqTrackerState();
|
|
2298
|
+
if (encryptedPush)
|
|
2299
|
+
return;
|
|
1859
2300
|
}
|
|
1860
2301
|
else {
|
|
2302
|
+
if (encryptedPush) {
|
|
2303
|
+
await this._publishEncryptedPushMessage('group.message_created', 'group.message_undecryptable', '', seq ?? 0, msg, true);
|
|
2304
|
+
return;
|
|
2305
|
+
}
|
|
1861
2306
|
await this._publishAppEvent('group.message_created', msg);
|
|
1862
2307
|
}
|
|
1863
2308
|
}
|
|
@@ -1878,6 +2323,59 @@ export class AUNClient {
|
|
|
1878
2323
|
}
|
|
1879
2324
|
}
|
|
1880
2325
|
}
|
|
2326
|
+
async _decryptEncryptedPushPayload(msg, group) {
|
|
2327
|
+
const envelope = encryptedPushEnvelope(msg);
|
|
2328
|
+
if (!isV2EncryptedEnvelopePayload(envelope))
|
|
2329
|
+
return null;
|
|
2330
|
+
const aad = isJsonObject(envelope.aad) ? envelope.aad : {};
|
|
2331
|
+
const fromAid = String(msg.from_aid ?? msg.from ?? msg.sender_aid ?? aad.from ?? '').trim();
|
|
2332
|
+
const plaintext = await this._decryptV2EnvelopeForThought({ envelope, fromAid });
|
|
2333
|
+
if (!plaintext)
|
|
2334
|
+
return null;
|
|
2335
|
+
const e2eeMeta = v2E2eeMeta(envelope);
|
|
2336
|
+
const result = {
|
|
2337
|
+
message_id: String(msg.message_id ?? ''),
|
|
2338
|
+
from: fromAid,
|
|
2339
|
+
seq: msg.seq ?? null,
|
|
2340
|
+
timestamp: msg.t_server ?? msg.timestamp ?? null,
|
|
2341
|
+
payload: plaintext,
|
|
2342
|
+
encrypted: true,
|
|
2343
|
+
e2ee: e2eeMeta,
|
|
2344
|
+
};
|
|
2345
|
+
result.direction = fromAid && fromAid === this._aid ? 'outbound_sync' : 'inbound';
|
|
2346
|
+
if (msg.t_server !== undefined)
|
|
2347
|
+
result.t_server = msg.t_server;
|
|
2348
|
+
if (msg.device_id !== undefined)
|
|
2349
|
+
result.device_id = msg.device_id;
|
|
2350
|
+
if (msg.slot_id !== undefined)
|
|
2351
|
+
result.slot_id = msg.slot_id;
|
|
2352
|
+
if (group) {
|
|
2353
|
+
result.group_id = msg.group_id ?? aad.group_id ?? envelope.group_id ?? null;
|
|
2354
|
+
}
|
|
2355
|
+
else {
|
|
2356
|
+
result.to = msg.to ?? this._aid ?? '';
|
|
2357
|
+
}
|
|
2358
|
+
attachV2EnvelopeMetadata(result, e2eeMeta);
|
|
2359
|
+
return result;
|
|
2360
|
+
}
|
|
2361
|
+
async _publishEncryptedPushAsUndecryptable(event, ns, seq, msg, group) {
|
|
2362
|
+
const safeEvent = safeUndecryptablePushEvent(msg, group);
|
|
2363
|
+
if (ns) {
|
|
2364
|
+
return this._publishOrderedMessage(event, ns, seq, safeEvent);
|
|
2365
|
+
}
|
|
2366
|
+
await this._publishAppEvent(event, safeEvent);
|
|
2367
|
+
return true;
|
|
2368
|
+
}
|
|
2369
|
+
async _publishEncryptedPushMessage(normalEvent, undecryptableEvent, ns, seq, msg, group) {
|
|
2370
|
+
const decrypted = await this._decryptEncryptedPushPayload(msg, group);
|
|
2371
|
+
if (decrypted) {
|
|
2372
|
+
if (ns)
|
|
2373
|
+
return this._publishOrderedMessage(normalEvent, ns, seq, decrypted);
|
|
2374
|
+
await this._publishAppEvent(normalEvent, decrypted);
|
|
2375
|
+
return true;
|
|
2376
|
+
}
|
|
2377
|
+
return this._publishEncryptedPushAsUndecryptable(undecryptableEvent, ns, seq, msg, group);
|
|
2378
|
+
}
|
|
1881
2379
|
/** 收到不带 payload 的 group.message_created 通知后,自动 pull 最新消息 */
|
|
1882
2380
|
async _autoPullGroupMessages(notification) {
|
|
1883
2381
|
const groupId = (notification.group_id ?? '');
|
|
@@ -3199,7 +3697,7 @@ export class AUNClient {
|
|
|
3199
3697
|
this._state = 'connected';
|
|
3200
3698
|
this._connectedAt = Date.now();
|
|
3201
3699
|
await this._dispatcher.publish('connection.state', {
|
|
3202
|
-
state: this._state,
|
|
3700
|
+
state: this._publicState(this._state),
|
|
3203
3701
|
gateway: gatewayUrl,
|
|
3204
3702
|
});
|
|
3205
3703
|
if (this._seqTrackerContext !== this._currentSeqTrackerContext()) {
|
|
@@ -3209,7 +3707,7 @@ export class AUNClient {
|
|
|
3209
3707
|
this._startBackgroundTasks();
|
|
3210
3708
|
// V2 E2EE: 初始化 session 并注册设备 SPK(与 Python `_init_v2_session` 对齐)
|
|
3211
3709
|
try {
|
|
3212
|
-
await this.
|
|
3710
|
+
await this._initV2Session();
|
|
3213
3711
|
}
|
|
3214
3712
|
catch (exc) {
|
|
3215
3713
|
this._clientLog.warn(`V2 session init failed (non-fatal): ${String(exc)}`);
|
|
@@ -3228,6 +3726,57 @@ export class AUNClient {
|
|
|
3228
3726
|
const gateways = this._resolveGateways(params);
|
|
3229
3727
|
return gateways[0];
|
|
3230
3728
|
}
|
|
3729
|
+
async _resolveGatewayForAid(aid) {
|
|
3730
|
+
const target = String(aid ?? this._aid ?? '').trim();
|
|
3731
|
+
if (!target)
|
|
3732
|
+
throw new StateError('gateway discovery requires a loaded AID');
|
|
3733
|
+
if (this._gatewayUrl)
|
|
3734
|
+
return this._gatewayUrl;
|
|
3735
|
+
try {
|
|
3736
|
+
const getMetadata = this._keystore.getMetadata;
|
|
3737
|
+
const raw = typeof getMetadata === 'function'
|
|
3738
|
+
? String(await getMetadata.call(this._keystore, target, 'gateway_url') ?? '').trim()
|
|
3739
|
+
: '';
|
|
3740
|
+
if (raw) {
|
|
3741
|
+
const gateway = raw.startsWith('"') && raw.endsWith('"') ? String(JSON.parse(raw)).trim() : raw;
|
|
3742
|
+
if (gateway) {
|
|
3743
|
+
this._gatewayUrl = gateway;
|
|
3744
|
+
return gateway;
|
|
3745
|
+
}
|
|
3746
|
+
}
|
|
3747
|
+
}
|
|
3748
|
+
catch {
|
|
3749
|
+
// 缓存读取失败不影响发现流程。
|
|
3750
|
+
}
|
|
3751
|
+
const dotIdx = target.indexOf('.');
|
|
3752
|
+
const issuerDomain = dotIdx >= 0 ? target.slice(dotIdx + 1) : target;
|
|
3753
|
+
const portSuffix = this.configModel.discoveryPort ? `:${this.configModel.discoveryPort}` : '';
|
|
3754
|
+
const candidates = [
|
|
3755
|
+
`https://${target}${portSuffix}/.well-known/aun-gateway`,
|
|
3756
|
+
`https://gateway.${issuerDomain}${portSuffix}/.well-known/aun-gateway`,
|
|
3757
|
+
];
|
|
3758
|
+
let lastError = null;
|
|
3759
|
+
for (const url of candidates) {
|
|
3760
|
+
try {
|
|
3761
|
+
const gateway = await this._discovery.discover(url);
|
|
3762
|
+
this._gatewayUrl = gateway;
|
|
3763
|
+
try {
|
|
3764
|
+
const setMetadata = this._keystore.setMetadata;
|
|
3765
|
+
if (typeof setMetadata === 'function') {
|
|
3766
|
+
await setMetadata.call(this._keystore, target, 'gateway_url', gateway);
|
|
3767
|
+
}
|
|
3768
|
+
}
|
|
3769
|
+
catch {
|
|
3770
|
+
// 缓存写入失败不影响连接。
|
|
3771
|
+
}
|
|
3772
|
+
return gateway;
|
|
3773
|
+
}
|
|
3774
|
+
catch (err) {
|
|
3775
|
+
lastError = err;
|
|
3776
|
+
}
|
|
3777
|
+
}
|
|
3778
|
+
throw lastError instanceof Error ? lastError : new ConnectionError(`gateway discovery failed for ${target}`);
|
|
3779
|
+
}
|
|
3231
3780
|
_resolveGateways(params) {
|
|
3232
3781
|
const topology = isJsonObject(params.topology) ? params.topology : null;
|
|
3233
3782
|
if (topology) {
|
|
@@ -3277,12 +3826,13 @@ export class AUNClient {
|
|
|
3277
3826
|
_normalizeConnectParams(params) {
|
|
3278
3827
|
const request = { ...params };
|
|
3279
3828
|
const accessToken = String(request.access_token ?? '');
|
|
3280
|
-
if (!accessToken)
|
|
3281
|
-
throw new StateError('connect requires non-empty access_token');
|
|
3282
3829
|
const gateway = String(request.gateway ?? this._gatewayUrl ?? '');
|
|
3283
3830
|
if (!gateway)
|
|
3284
3831
|
throw new StateError('connect requires non-empty gateway');
|
|
3285
|
-
|
|
3832
|
+
if (accessToken)
|
|
3833
|
+
request.access_token = accessToken;
|
|
3834
|
+
else
|
|
3835
|
+
delete request.access_token;
|
|
3286
3836
|
request.gateway = gateway;
|
|
3287
3837
|
request.device_id = this._deviceId;
|
|
3288
3838
|
request.slot_id = normalizeInstanceId(request.slot_id ?? this._slotId, 'slot_id', { allowEmpty: true });
|
|
@@ -3502,6 +4052,11 @@ export class AUNClient {
|
|
|
3502
4052
|
}
|
|
3503
4053
|
try {
|
|
3504
4054
|
identity = await this._auth.refreshCachedTokens(this._gatewayUrl, identity);
|
|
4055
|
+
// 刷新期间可能已断线,复检状态,避免写回 stale identity
|
|
4056
|
+
if (this._state !== 'connected') {
|
|
4057
|
+
scheduleRefresh();
|
|
4058
|
+
return;
|
|
4059
|
+
}
|
|
3505
4060
|
this._identity = identity;
|
|
3506
4061
|
if (this._sessionParams && identity.access_token) {
|
|
3507
4062
|
this._sessionParams.access_token = identity.access_token;
|
|
@@ -3642,7 +4197,7 @@ export class AUNClient {
|
|
|
3642
4197
|
// 先停止后台任务,避免心跳/token刷新在重连期间继续触发
|
|
3643
4198
|
this._stopBackgroundTasks();
|
|
3644
4199
|
await this._dispatcher.publish('connection.state', {
|
|
3645
|
-
state: this._state,
|
|
4200
|
+
state: this._publicState(this._state),
|
|
3646
4201
|
error,
|
|
3647
4202
|
});
|
|
3648
4203
|
if (!this._sessionOptions.auto_reconnect)
|
|
@@ -3656,7 +4211,7 @@ export class AUNClient {
|
|
|
3656
4211
|
this._clientLog.warn(`suppress auto-reconnect: ${reason}`);
|
|
3657
4212
|
const disconnectInfo = this._lastDisconnectInfo ?? {};
|
|
3658
4213
|
const eventPayload = {
|
|
3659
|
-
state: this._state, error, reason,
|
|
4214
|
+
state: this._publicState(this._state), error, reason,
|
|
3660
4215
|
};
|
|
3661
4216
|
// 把服务端附带的结构化 detail(如配额超限信息)也带给应用层
|
|
3662
4217
|
const detail = disconnectInfo.detail;
|
|
@@ -3684,34 +4239,51 @@ export class AUNClient {
|
|
|
3684
4239
|
const maxAttempts = Number.isFinite(maxAttemptsRaw) && maxAttemptsRaw > 0 ? Math.floor(maxAttemptsRaw) : 0;
|
|
3685
4240
|
// 服务端主动关闭时从 16s 起跳,避免重连风暴;网络断开从 initial_delay 起跳
|
|
3686
4241
|
let delay = clampReconnectDelaySeconds(serverInitiated ? 16.0 : retry.initial_delay, serverInitiated ? 16.0 : 1.0, maxBaseDelay);
|
|
4242
|
+
this._retryAttempt = 0;
|
|
4243
|
+
this._retryMaxAttempts = maxAttempts;
|
|
3687
4244
|
for (let attempt = 1; !this._reconnectAbort?.signal.aborted; attempt++) {
|
|
3688
4245
|
// R1 fix: max_attempts 检查在循环顶部,覆盖所有路径(含 health-fail)
|
|
3689
4246
|
if (maxAttempts > 0 && attempt > maxAttempts) {
|
|
3690
4247
|
this._state = 'terminal_failed';
|
|
4248
|
+
this._nextRetryAt = null;
|
|
3691
4249
|
this._reconnectActive = false;
|
|
3692
4250
|
this._reconnectAbort = null;
|
|
3693
4251
|
await this._dispatcher.publish('connection.state', {
|
|
3694
|
-
state: this._state,
|
|
4252
|
+
state: this._publicState(this._state),
|
|
3695
4253
|
attempt: attempt - 1,
|
|
3696
4254
|
reason: 'max_attempts_exhausted',
|
|
3697
4255
|
});
|
|
3698
4256
|
return;
|
|
3699
4257
|
}
|
|
3700
|
-
|
|
4258
|
+
// 先进入 retry_backoff 状态(对齐 Python:先退避再重连)
|
|
4259
|
+
this._retryAttempt = attempt;
|
|
4260
|
+
const sleepMs = reconnectSleepDelaySeconds(delay, maxBaseDelay) * 1000;
|
|
4261
|
+
this._nextRetryAt = new Date(Date.now() + sleepMs);
|
|
4262
|
+
this._state = 'retry_backoff';
|
|
3701
4263
|
await this._dispatcher.publish('connection.state', {
|
|
3702
|
-
state: this._state,
|
|
4264
|
+
state: this._publicState(this._state),
|
|
3703
4265
|
attempt,
|
|
4266
|
+
next_retry_at: this._nextRetryAt.getTime() / 1000,
|
|
3704
4267
|
});
|
|
3705
4268
|
try {
|
|
3706
|
-
await this._sleep(
|
|
4269
|
+
await this._sleep(sleepMs);
|
|
4270
|
+
this._nextRetryAt = null;
|
|
3707
4271
|
if (this._reconnectAbort?.signal.aborted) {
|
|
3708
4272
|
this._reconnectActive = false;
|
|
3709
4273
|
return;
|
|
3710
4274
|
}
|
|
4275
|
+
// 退避结束,进入 reconnecting 状态
|
|
4276
|
+
this._state = 'reconnecting';
|
|
4277
|
+
await this._dispatcher.publish('connection.state', {
|
|
4278
|
+
state: this._publicState(this._state),
|
|
4279
|
+
attempt,
|
|
4280
|
+
});
|
|
3711
4281
|
// 重连前先 GET /health 探测,不健康则跳过本轮
|
|
3712
4282
|
if (this._gatewayUrl) {
|
|
3713
4283
|
const healthy = await this._discovery.checkHealth(this._gatewayUrl, 5000);
|
|
3714
4284
|
if (!healthy) {
|
|
4285
|
+
this._lastError = new Error('gateway health check failed');
|
|
4286
|
+
this._lastErrorCode = 'gateway_unhealthy';
|
|
3715
4287
|
delay = Math.min(delay * 2, maxBaseDelay);
|
|
3716
4288
|
continue;
|
|
3717
4289
|
}
|
|
@@ -3721,21 +4293,27 @@ export class AUNClient {
|
|
|
3721
4293
|
throw new StateError('missing connect params for reconnect');
|
|
3722
4294
|
}
|
|
3723
4295
|
await this._connectOnce(this._sessionParams, true);
|
|
4296
|
+
this._lastError = null;
|
|
4297
|
+
this._lastErrorCode = null;
|
|
4298
|
+
this._nextRetryAt = null;
|
|
3724
4299
|
this._reconnectActive = false;
|
|
3725
4300
|
this._reconnectAbort = null;
|
|
3726
4301
|
return;
|
|
3727
4302
|
}
|
|
3728
4303
|
catch (exc) {
|
|
4304
|
+
this._lastError = exc instanceof Error ? exc : new Error(String(exc));
|
|
4305
|
+
this._lastErrorCode = 'reconnect_failed';
|
|
3729
4306
|
await this._dispatcher.publish('connection.error', {
|
|
3730
4307
|
error: formatCaughtError(exc),
|
|
3731
4308
|
attempt,
|
|
3732
4309
|
});
|
|
3733
4310
|
if (!this._shouldRetryReconnect(exc)) {
|
|
3734
4311
|
this._state = 'terminal_failed';
|
|
4312
|
+
this._nextRetryAt = null;
|
|
3735
4313
|
this._reconnectActive = false;
|
|
3736
4314
|
this._reconnectAbort = null;
|
|
3737
4315
|
await this._dispatcher.publish('connection.state', {
|
|
3738
|
-
state: this._state,
|
|
4316
|
+
state: this._publicState(this._state),
|
|
3739
4317
|
error: formatCaughtError(exc),
|
|
3740
4318
|
attempt,
|
|
3741
4319
|
});
|
|
@@ -4085,10 +4663,30 @@ export class AUNClient {
|
|
|
4085
4663
|
*
|
|
4086
4664
|
* connect 成功后自动调用,可幂等手动调用。
|
|
4087
4665
|
*/
|
|
4088
|
-
async
|
|
4666
|
+
async _initV2Session() {
|
|
4089
4667
|
if (!this._aid)
|
|
4090
4668
|
return;
|
|
4091
|
-
|
|
4669
|
+
let identity = this._identity;
|
|
4670
|
+
if (!identity?.private_key_pem) {
|
|
4671
|
+
// fallback:缓存的 identity 可能被 instance_state 污染,重新从 keystore 加载
|
|
4672
|
+
try {
|
|
4673
|
+
const reloaded = await this._keystore.loadIdentity(this._aid);
|
|
4674
|
+
if (reloaded?.private_key_pem) {
|
|
4675
|
+
this._identity = reloaded;
|
|
4676
|
+
identity = reloaded;
|
|
4677
|
+
this._clientLog.warn('V2 session init: identity cache was stale, reloaded from keystore');
|
|
4678
|
+
// 自愈:重新持久化,清理 instance_state 中的脏数据
|
|
4679
|
+
try {
|
|
4680
|
+
const persistIdentity = this._auth._persistIdentity;
|
|
4681
|
+
if (typeof persistIdentity === 'function') {
|
|
4682
|
+
await persistIdentity.call(this._auth, reloaded);
|
|
4683
|
+
}
|
|
4684
|
+
}
|
|
4685
|
+
catch { /* best-effort */ }
|
|
4686
|
+
}
|
|
4687
|
+
}
|
|
4688
|
+
catch { /* ignore */ }
|
|
4689
|
+
}
|
|
4092
4690
|
if (!identity?.private_key_pem) {
|
|
4093
4691
|
this._clientLog.warn('V2 session init skipped: no AID private key');
|
|
4094
4692
|
return;
|
|
@@ -4370,7 +4968,7 @@ export class AUNClient {
|
|
|
4370
4968
|
* @param opts 可选 messageId / timestamp(与 Python 行为一致)
|
|
4371
4969
|
* @returns 服务端响应
|
|
4372
4970
|
*/
|
|
4373
|
-
async
|
|
4971
|
+
async _sendV2(to, payload, opts) {
|
|
4374
4972
|
if (!this._v2Session) {
|
|
4375
4973
|
throw new StateError('V2 session not initialized (not connected?)');
|
|
4376
4974
|
}
|
|
@@ -4414,20 +5012,21 @@ export class AUNClient {
|
|
|
4414
5012
|
* @param afterSeq 从此 seq 之后开始拉取(0/省略 = 从当前 contiguous 开始)
|
|
4415
5013
|
* @param limit 最多拉取条数
|
|
4416
5014
|
*/
|
|
4417
|
-
async
|
|
5015
|
+
async _pullV2(afterSeq = 0, limit = 50, opts) {
|
|
4418
5016
|
if (!this._v2Session) {
|
|
4419
5017
|
throw new StateError('V2 session not initialized (not connected?)');
|
|
4420
5018
|
}
|
|
4421
5019
|
const ns = this._aid ? `p2p:${this._aid}` : '';
|
|
4422
5020
|
const decrypted = [];
|
|
4423
|
-
let nextAfterSeq = afterSeq || (ns ? this._seqTracker.getContiguousSeq(ns) : 0);
|
|
5021
|
+
let nextAfterSeq = opts?.force ? afterSeq : (afterSeq || (ns ? this._seqTracker.getContiguousSeq(ns) : 0));
|
|
4424
5022
|
let pageCount = 0;
|
|
4425
5023
|
const maxPages = 100;
|
|
4426
5024
|
while (pageCount < maxPages) {
|
|
4427
5025
|
pageCount += 1;
|
|
4428
|
-
const result = await this.
|
|
5026
|
+
const result = await this._callRawV2Rpc('message.v2.pull', {
|
|
4429
5027
|
after_seq: nextAfterSeq,
|
|
4430
5028
|
limit,
|
|
5029
|
+
...(opts?.force ? { force: true } : {}),
|
|
4431
5030
|
});
|
|
4432
5031
|
const messages = (Array.isArray(result?.messages) ? result.messages : []);
|
|
4433
5032
|
const seqs = messages
|
|
@@ -4509,7 +5108,7 @@ export class AUNClient {
|
|
|
4509
5108
|
this._saveSeqTrackerState();
|
|
4510
5109
|
}
|
|
4511
5110
|
if (messages.length > 0 && contigAdvanced && ackSeq > 0) {
|
|
4512
|
-
this._safeAsync(this.
|
|
5111
|
+
this._safeAsync(this._ackV2(ackSeq).then(() => undefined));
|
|
4513
5112
|
}
|
|
4514
5113
|
}
|
|
4515
5114
|
const nextAfter = Math.max(pageMaxSeq, nextAfterSeq);
|
|
@@ -4527,7 +5126,7 @@ export class AUNClient {
|
|
|
4527
5126
|
*
|
|
4528
5127
|
* @param upToSeq 确认到此 seq;省略则用当前 contiguous
|
|
4529
5128
|
*/
|
|
4530
|
-
async
|
|
5129
|
+
async _ackV2(upToSeq) {
|
|
4531
5130
|
const ns = this._aid ? `p2p:${this._aid}` : '';
|
|
4532
5131
|
let seq = upToSeq ?? (ns ? this._seqTracker.getContiguousSeq(ns) : 0);
|
|
4533
5132
|
if (seq <= 0)
|
|
@@ -4747,6 +5346,12 @@ export class AUNClient {
|
|
|
4747
5346
|
encrypted: true,
|
|
4748
5347
|
e2ee: e2ee,
|
|
4749
5348
|
};
|
|
5349
|
+
const explicitDirection = String(msg.direction ?? '').trim();
|
|
5350
|
+
result.direction = explicitDirection || (fromAid && fromAid === this._aid ? 'outbound_sync' : 'inbound');
|
|
5351
|
+
if (msg.device_id !== undefined)
|
|
5352
|
+
result.device_id = msg.device_id;
|
|
5353
|
+
if (msg.slot_id !== undefined)
|
|
5354
|
+
result.slot_id = msg.slot_id;
|
|
4750
5355
|
attachV2EnvelopeMetadata(result, e2ee);
|
|
4751
5356
|
return result;
|
|
4752
5357
|
}
|
|
@@ -4758,7 +5363,7 @@ export class AUNClient {
|
|
|
4758
5363
|
* @param opts 可选 messageId / timestamp
|
|
4759
5364
|
* @returns 服务端响应
|
|
4760
5365
|
*/
|
|
4761
|
-
async
|
|
5366
|
+
async _sendGroupV2(groupId, payload, opts) {
|
|
4762
5367
|
if (!this._v2Session) {
|
|
4763
5368
|
throw new StateError('V2 session not initialized (not connected?)');
|
|
4764
5369
|
}
|
|
@@ -4817,7 +5422,7 @@ export class AUNClient {
|
|
|
4817
5422
|
}
|
|
4818
5423
|
}
|
|
4819
5424
|
async _pullGroupV2Internal(params) {
|
|
4820
|
-
await this.
|
|
5425
|
+
await this._pullGroupV2(params.group_id, params.after_seq, params.limit);
|
|
4821
5426
|
}
|
|
4822
5427
|
/**
|
|
4823
5428
|
* 拉取并解密 V2 Group 消息。
|
|
@@ -4826,7 +5431,7 @@ export class AUNClient {
|
|
|
4826
5431
|
* @param afterSeq 从此 seq 之后开始拉取(0/省略 = 从当前 contiguous 开始)
|
|
4827
5432
|
* @param limit 最多拉取条数
|
|
4828
5433
|
*/
|
|
4829
|
-
async
|
|
5434
|
+
async _pullGroupV2(groupId, afterSeq = 0, limit = 50) {
|
|
4830
5435
|
if (!this._v2Session) {
|
|
4831
5436
|
throw new StateError('V2 session not initialized (not connected?)');
|
|
4832
5437
|
}
|
|
@@ -4925,7 +5530,7 @@ export class AUNClient {
|
|
|
4925
5530
|
this._saveSeqTrackerState();
|
|
4926
5531
|
}
|
|
4927
5532
|
if (messages.length > 0 && contigAdvanced && ackSeq > 0) {
|
|
4928
|
-
this._safeAsync(this.
|
|
5533
|
+
this._safeAsync(this._ackGroupV2(gid, ackSeq).then(() => undefined));
|
|
4929
5534
|
}
|
|
4930
5535
|
const nextAfter = Math.max(pageMaxSeq, nextAfterSeq);
|
|
4931
5536
|
if (messages.length === 0 || nextAfter <= nextAfterSeq || result.has_more === false)
|
|
@@ -4943,7 +5548,7 @@ export class AUNClient {
|
|
|
4943
5548
|
* @param groupId 群 ID
|
|
4944
5549
|
* @param upToSeq 确认到此 seq;省略则用当前 contiguous
|
|
4945
5550
|
*/
|
|
4946
|
-
async
|
|
5551
|
+
async _ackGroupV2(groupId, upToSeq) {
|
|
4947
5552
|
const gid = normalizeGroupId(groupId) || String(groupId ?? '').trim();
|
|
4948
5553
|
if (!gid)
|
|
4949
5554
|
throw new ValidationError('group.ack_messages requires group_id');
|
|
@@ -6020,7 +6625,7 @@ export class AUNClient {
|
|
|
6020
6625
|
try {
|
|
6021
6626
|
do {
|
|
6022
6627
|
this._v2PullPending = false;
|
|
6023
|
-
await this.
|
|
6628
|
+
await this._pullV2();
|
|
6024
6629
|
const newContig = ns ? this._seqTracker.getContiguousSeq(ns) : -1;
|
|
6025
6630
|
this._clientLog.debug(`_onV2PushNotification pull done: contiguous_seq=${contigBefore}->${newContig} (push_seq=${pushSeq || 'null'})`);
|
|
6026
6631
|
} while (this._v2PullPending);
|