@agentunion/fastaun-browser 0.2.13 → 0.2.15
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/auth.d.ts +4 -0
- package/dist/auth.d.ts.map +1 -1
- package/dist/auth.js +16 -0
- package/dist/auth.js.map +1 -1
- package/dist/client.d.ts +23 -1
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js +626 -117
- package/dist/client.js.map +1 -1
- package/dist/e2ee-group.d.ts +13 -0
- package/dist/e2ee-group.d.ts.map +1 -1
- package/dist/e2ee-group.js +200 -17
- package/dist/e2ee-group.js.map +1 -1
- package/dist/e2ee.d.ts +23 -0
- package/dist/e2ee.d.ts.map +1 -1
- package/dist/e2ee.js +270 -33
- package/dist/e2ee.js.map +1 -1
- package/dist/index.d.ts +3 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -1
- package/dist/index.js.map +1 -1
- package/dist/keystore/index.d.ts +10 -0
- package/dist/keystore/index.d.ts.map +1 -1
- package/dist/keystore/indexeddb.d.ts +19 -0
- package/dist/keystore/indexeddb.d.ts.map +1 -1
- package/dist/keystore/indexeddb.js +83 -0
- package/dist/keystore/indexeddb.js.map +1 -1
- package/dist/namespaces/auth.d.ts +19 -0
- package/dist/namespaces/auth.d.ts.map +1 -1
- package/dist/namespaces/auth.js +237 -0
- package/dist/namespaces/auth.js.map +1 -1
- package/dist/namespaces/meta.d.ts +106 -0
- package/dist/namespaces/meta.d.ts.map +1 -0
- package/dist/namespaces/meta.js +498 -0
- package/dist/namespaces/meta.js.map +1 -0
- package/dist/seq-tracker.d.ts +3 -0
- package/dist/seq-tracker.d.ts.map +1 -1
- package/dist/seq-tracker.js +38 -2
- package/dist/seq-tracker.js.map +1 -1
- package/package.json +1 -1
package/dist/client.js
CHANGED
|
@@ -12,6 +12,7 @@ import { AuthFlow } from './auth.js';
|
|
|
12
12
|
import { SeqTracker } from './seq-tracker.js';
|
|
13
13
|
import { AuthNamespace } from './namespaces/auth.js';
|
|
14
14
|
import { CustodyNamespace } from './namespaces/custody.js';
|
|
15
|
+
import { MetaNamespace } from './namespaces/meta.js';
|
|
15
16
|
import { CryptoProvider, uint8ToBase64, base64ToUint8, pemToArrayBuffer, p1363ToDer, toBufferSource } from './crypto.js';
|
|
16
17
|
import { E2EEManager, _certificateSha256Fingerprint as certificateSha256Fingerprint, _ecdsaVerifyDer as ecdsaVerifyDer, _importCertPublicKeyEcdsa as importCertPublicKeyEcdsa, } from './e2ee.js';
|
|
17
18
|
import { GroupE2EEManager, computeMembershipCommitment, storeGroupSecret, buildKeyDistribution, buildKeyRequest, buildMembershipManifest, signMembershipManifest, } from './e2ee-group.js';
|
|
@@ -59,6 +60,9 @@ const SIGNED_METHODS = new Set([
|
|
|
59
60
|
'group.transfer_owner', 'group.review_join_request',
|
|
60
61
|
'group.batch_review_join_request',
|
|
61
62
|
'group.request_join', 'group.use_invite_code',
|
|
63
|
+
'group.thought.put',
|
|
64
|
+
'message.thought.put',
|
|
65
|
+
'group.set_settings',
|
|
62
66
|
'group.resources.put', 'group.resources.update',
|
|
63
67
|
'group.resources.delete', 'group.resources.request_add',
|
|
64
68
|
'group.resources.direct_add', 'group.resources.approve_request',
|
|
@@ -84,6 +88,21 @@ const RECONNECT_MIN_BASE_DELAY_SECONDS = 1.0;
|
|
|
84
88
|
const RECONNECT_MAX_BASE_DELAY_SECONDS = 64.0;
|
|
85
89
|
const GROUP_ROTATION_LEASE_MS = 120_000;
|
|
86
90
|
const GROUP_ROTATION_RETRY_MAX_DELAY_MS = 300_000;
|
|
91
|
+
const PENDING_DECRYPT_LIMIT = 100;
|
|
92
|
+
const PUSHED_SEQS_LIMIT = 50_000;
|
|
93
|
+
const PENDING_ORDERED_LIMIT = 50_000;
|
|
94
|
+
// P1-23: 非幂等方法使用更长超时(35s),避免 SDK 10s 超时 < gateway 30s 处理时间
|
|
95
|
+
const NON_IDEMPOTENT_TIMEOUT = 35;
|
|
96
|
+
const NON_IDEMPOTENT_METHODS = new Set([
|
|
97
|
+
'message.send', 'group.send', 'group.create', 'group.invite',
|
|
98
|
+
'group.kick', 'group.remove_member', 'group.leave', 'group.dissolve',
|
|
99
|
+
'group.update_name', 'group.update_avatar', 'group.update_announcement',
|
|
100
|
+
'group.update_settings', 'group.rotate_epoch',
|
|
101
|
+
'storage.upload', 'storage.complete_upload', 'storage.delete',
|
|
102
|
+
'auth.create_aid', 'auth.renew_cert', 'auth.rekey',
|
|
103
|
+
'message.thought.put', 'group.thought.put',
|
|
104
|
+
'group.add_member',
|
|
105
|
+
]);
|
|
87
106
|
function clampReconnectDelaySeconds(value, fallback, upper = RECONNECT_MAX_BASE_DELAY_SECONDS) {
|
|
88
107
|
const parsed = Number(value);
|
|
89
108
|
const seconds = Number.isFinite(parsed) ? parsed : fallback;
|
|
@@ -151,6 +170,7 @@ function isGroupServiceAid(value) {
|
|
|
151
170
|
const [name, ...issuerParts] = text.split('.');
|
|
152
171
|
return name === 'group' && issuerParts.join('.').length > 0;
|
|
153
172
|
}
|
|
173
|
+
const PREKEY_FALLBACK_DEVICE_ID = 'aun_device_id';
|
|
154
174
|
function isPeerPrekeyMaterial(value) {
|
|
155
175
|
if (!isJsonObject(value))
|
|
156
176
|
return false;
|
|
@@ -168,6 +188,52 @@ function isPeerPrekeyResponse(value) {
|
|
|
168
188
|
return false;
|
|
169
189
|
return candidate.prekey === undefined || isPeerPrekeyMaterial(candidate.prekey);
|
|
170
190
|
}
|
|
191
|
+
function normalizePeerPrekeys(prekeys) {
|
|
192
|
+
const normalized = [];
|
|
193
|
+
for (const item of prekeys) {
|
|
194
|
+
if (!isPeerPrekeyMaterial(item))
|
|
195
|
+
continue;
|
|
196
|
+
const prekeyId = item.prekey_id.trim();
|
|
197
|
+
const publicKey = item.public_key.trim();
|
|
198
|
+
const signature = item.signature.trim();
|
|
199
|
+
if (!prekeyId || !publicKey || !signature)
|
|
200
|
+
continue;
|
|
201
|
+
const deviceId = String(item.device_id ?? '').trim();
|
|
202
|
+
const certFingerprint = String(item.cert_fingerprint ?? '').trim().toLowerCase();
|
|
203
|
+
const candidate = {
|
|
204
|
+
...item,
|
|
205
|
+
prekey_id: prekeyId,
|
|
206
|
+
public_key: publicKey,
|
|
207
|
+
signature,
|
|
208
|
+
device_id: deviceId,
|
|
209
|
+
};
|
|
210
|
+
if (certFingerprint) {
|
|
211
|
+
candidate.cert_fingerprint = certFingerprint;
|
|
212
|
+
}
|
|
213
|
+
else {
|
|
214
|
+
delete candidate.cert_fingerprint;
|
|
215
|
+
}
|
|
216
|
+
normalized.push(candidate);
|
|
217
|
+
}
|
|
218
|
+
if (normalized.length === 0)
|
|
219
|
+
return [];
|
|
220
|
+
if (normalized.length === 1) {
|
|
221
|
+
if (!String(normalized[0].device_id ?? '').trim()) {
|
|
222
|
+
normalized[0].device_id = PREKEY_FALLBACK_DEVICE_ID;
|
|
223
|
+
}
|
|
224
|
+
return normalized;
|
|
225
|
+
}
|
|
226
|
+
const seen = new Set();
|
|
227
|
+
const filtered = [];
|
|
228
|
+
for (const item of normalized) {
|
|
229
|
+
const deviceId = String(item.device_id ?? '').trim();
|
|
230
|
+
if (!deviceId || deviceId === PREKEY_FALLBACK_DEVICE_ID || seen.has(deviceId))
|
|
231
|
+
continue;
|
|
232
|
+
seen.add(deviceId);
|
|
233
|
+
filtered.push(item);
|
|
234
|
+
}
|
|
235
|
+
return filtered;
|
|
236
|
+
}
|
|
171
237
|
function formatCaughtError(error) {
|
|
172
238
|
return error instanceof Error ? error : String(error);
|
|
173
239
|
}
|
|
@@ -249,6 +315,8 @@ export class AUNClient {
|
|
|
249
315
|
auth;
|
|
250
316
|
/** AID 托管命名空间 */
|
|
251
317
|
custody;
|
|
318
|
+
/** 元数据命名空间(心跳、状态、信任根管理) */
|
|
319
|
+
meta;
|
|
252
320
|
// E2EE 编排状态(内存缓存)
|
|
253
321
|
_certCache = new Map();
|
|
254
322
|
_prekeyReplenishInflight = new Set();
|
|
@@ -269,8 +337,10 @@ export class AUNClient {
|
|
|
269
337
|
_seqTrackerContext = null;
|
|
270
338
|
/** 补洞去重:已完成/进行中的 key 集合,防止重复 pull 同一区间 */
|
|
271
339
|
_gapFillDone = new Set();
|
|
272
|
-
/**
|
|
340
|
+
/** 已发布到应用层的 seq 集合(按命名空间),补洞路径 publish 前检查以避免重复分发 */
|
|
273
341
|
_pushedSeqs = new Map();
|
|
342
|
+
/** 已解密但因 seq 空洞暂缓发布的应用层消息(按 namespace -> seq) */
|
|
343
|
+
_pendingOrderedMsgs = new Map();
|
|
274
344
|
_pendingDecryptMsgs = new Map();
|
|
275
345
|
_groupEpochRotationInflight = new Set();
|
|
276
346
|
_groupEpochRecoveryInflight = new Map();
|
|
@@ -330,6 +400,7 @@ export class AUNClient {
|
|
|
330
400
|
});
|
|
331
401
|
this.auth = new AuthNamespace(this);
|
|
332
402
|
this.custody = new CustodyNamespace(this);
|
|
403
|
+
this.meta = new MetaNamespace(this);
|
|
333
404
|
// 内部订阅:推送消息自动解密后 re-publish 给用户
|
|
334
405
|
this._dispatcher.subscribe('_raw.message.received', (data) => {
|
|
335
406
|
this._onRawMessageReceived(data);
|
|
@@ -343,7 +414,7 @@ export class AUNClient {
|
|
|
343
414
|
this._onRawGroupChanged(data);
|
|
344
415
|
});
|
|
345
416
|
// 其他事件直接透传
|
|
346
|
-
for (const evt of ['message.recalled', 'message.ack']) {
|
|
417
|
+
for (const evt of ['message.recalled', 'message.ack', 'storage.object_changed']) {
|
|
347
418
|
this._dispatcher.subscribe(`_raw.${evt}`, (data) => {
|
|
348
419
|
this._dispatcher.publish(evt, data);
|
|
349
420
|
});
|
|
@@ -513,6 +584,8 @@ export class AUNClient {
|
|
|
513
584
|
if (encrypt) {
|
|
514
585
|
return this._sendEncrypted(p);
|
|
515
586
|
}
|
|
587
|
+
delete p.protected_headers;
|
|
588
|
+
delete p.headers;
|
|
516
589
|
}
|
|
517
590
|
// 自动加密:group.send 默认加密(encrypt 默认 true)
|
|
518
591
|
if (method === 'group.send') {
|
|
@@ -521,12 +594,34 @@ export class AUNClient {
|
|
|
521
594
|
if (encrypt) {
|
|
522
595
|
return this._sendGroupEncrypted(p);
|
|
523
596
|
}
|
|
597
|
+
delete p.protected_headers;
|
|
598
|
+
delete p.headers;
|
|
599
|
+
}
|
|
600
|
+
if (method === 'group.thought.put') {
|
|
601
|
+
const encrypt = p.encrypt !== undefined ? p.encrypt : true;
|
|
602
|
+
delete p.encrypt;
|
|
603
|
+
if (!encrypt) {
|
|
604
|
+
throw new ValidationError('group.thought.put requires encrypt=true');
|
|
605
|
+
}
|
|
606
|
+
return this._putGroupThoughtEncrypted(p);
|
|
607
|
+
}
|
|
608
|
+
if (method === 'message.thought.put') {
|
|
609
|
+
const encrypt = p.encrypt !== undefined ? p.encrypt : true;
|
|
610
|
+
delete p.encrypt;
|
|
611
|
+
if (!encrypt) {
|
|
612
|
+
throw new ValidationError('message.thought.put requires encrypt=true');
|
|
613
|
+
}
|
|
614
|
+
return this._putMessageThoughtEncrypted(p);
|
|
524
615
|
}
|
|
525
616
|
// 关键操作自动附加客户端签名
|
|
526
617
|
if (SIGNED_METHODS.has(method)) {
|
|
527
618
|
await this._signClientOperation(method, p);
|
|
528
619
|
}
|
|
529
|
-
|
|
620
|
+
// P1-23: 非幂等方法使用更长超时
|
|
621
|
+
const callTimeout = NON_IDEMPOTENT_METHODS.has(method) ? NON_IDEMPOTENT_TIMEOUT : undefined;
|
|
622
|
+
const result = callTimeout
|
|
623
|
+
? await this._transport.call(method, p, callTimeout)
|
|
624
|
+
: await this._transport.call(method, p);
|
|
530
625
|
// 自动解密:message.pull 返回的消息
|
|
531
626
|
if (method === 'message.pull' && isJsonObject(result)) {
|
|
532
627
|
const r = result;
|
|
@@ -560,6 +655,7 @@ export class AUNClient {
|
|
|
560
655
|
this._transport.call('message.ack', {
|
|
561
656
|
seq: contig,
|
|
562
657
|
device_id: this._deviceId,
|
|
658
|
+
slot_id: this._slotId,
|
|
563
659
|
}).catch((e) => { console.warn('message.pull auto-ack 失败:', e); });
|
|
564
660
|
}
|
|
565
661
|
}
|
|
@@ -608,6 +704,12 @@ export class AUNClient {
|
|
|
608
704
|
}
|
|
609
705
|
}
|
|
610
706
|
}
|
|
707
|
+
if (method === 'group.thought.get' && isJsonObject(result)) {
|
|
708
|
+
return this._decryptGroupThoughts(result);
|
|
709
|
+
}
|
|
710
|
+
if (method === 'message.thought.get' && isJsonObject(result)) {
|
|
711
|
+
return this._decryptMessageThoughts(result);
|
|
712
|
+
}
|
|
611
713
|
// ── Group E2EE 自动编排 ────────────────────────────
|
|
612
714
|
// ── Group E2EE 自动编排(必备能力,始终启用)────────
|
|
613
715
|
{
|
|
@@ -639,20 +741,23 @@ export class AUNClient {
|
|
|
639
741
|
const groupId = this._extractGroupIdFromResult(result) || String(p.group_id ?? '');
|
|
640
742
|
if (groupId && this._membershipRotationChanged(method, result)) {
|
|
641
743
|
const expectedEpoch = this._membershipRotationExpectedEpoch(result);
|
|
642
|
-
|
|
744
|
+
// P0-12: await rotation 完成(带超时兜底),确保后续 group.send 使用新 epoch
|
|
745
|
+
const rotationPromise = this._maybeLeadRotateGroupEpoch(groupId, this._membershipRotationTriggerId(groupId, result), expectedEpoch);
|
|
746
|
+
const timeoutPromise = new Promise((resolve) => globalThis.setTimeout(resolve, 5000));
|
|
747
|
+
await Promise.race([rotationPromise, timeoutPromise]).catch((exc) => console.warn('membership RPC epoch rotation fallback failed:', exc));
|
|
643
748
|
}
|
|
644
749
|
}
|
|
645
750
|
return result;
|
|
646
751
|
}
|
|
647
752
|
// ── 便利方法 ──────────────────────────────────────
|
|
648
753
|
async ping(params) {
|
|
649
|
-
return this.
|
|
754
|
+
return this.meta.ping(params);
|
|
650
755
|
}
|
|
651
756
|
async status(params) {
|
|
652
|
-
return this.
|
|
757
|
+
return this.meta.status(params);
|
|
653
758
|
}
|
|
654
759
|
async trustRoots(params) {
|
|
655
|
-
return this.
|
|
760
|
+
return this.meta.trustRoots(params);
|
|
656
761
|
}
|
|
657
762
|
// ── 事件 ──────────────────────────────────────────
|
|
658
763
|
/**
|
|
@@ -677,10 +782,13 @@ export class AUNClient {
|
|
|
677
782
|
async _processAndPublishMessage(data) {
|
|
678
783
|
try {
|
|
679
784
|
if (!isJsonObject(data)) {
|
|
680
|
-
await this.
|
|
785
|
+
await this._publishAppEvent('message.received', data);
|
|
681
786
|
return;
|
|
682
787
|
}
|
|
683
788
|
const msg = { ...data };
|
|
789
|
+
if (!this._messageTargetsCurrentInstance(msg)) {
|
|
790
|
+
return;
|
|
791
|
+
}
|
|
684
792
|
// 拦截 P2P 传输的群组密钥分发/请求/响应消息
|
|
685
793
|
if (await this._tryHandleGroupKeyMessage(msg)) {
|
|
686
794
|
return;
|
|
@@ -701,20 +809,20 @@ export class AUNClient {
|
|
|
701
809
|
this._transport.call('message.ack', {
|
|
702
810
|
seq: contig,
|
|
703
811
|
device_id: this._deviceId,
|
|
812
|
+
slot_id: this._slotId,
|
|
704
813
|
}).catch((e) => { console.warn('P2P auto-ack 失败:', e); });
|
|
705
814
|
}
|
|
706
815
|
// 即时持久化 cursor,异常断连后不回退
|
|
707
816
|
this._saveSeqTrackerState();
|
|
708
817
|
}
|
|
709
818
|
const decrypted = await this._decryptSingleMessage(msg);
|
|
710
|
-
// 记录已推送的 seq,补洞路径据此去重
|
|
711
819
|
if (seq !== undefined && seq !== null && this._aid) {
|
|
712
820
|
const ns = `p2p:${this._aid}`;
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
821
|
+
await this._publishOrderedMessage('message.received', ns, seq, decrypted);
|
|
822
|
+
}
|
|
823
|
+
else {
|
|
824
|
+
await this._publishAppEvent('message.received', decrypted);
|
|
716
825
|
}
|
|
717
|
-
await this._dispatcher.publish('message.received', decrypted);
|
|
718
826
|
}
|
|
719
827
|
catch (exc) {
|
|
720
828
|
console.warn('消息解密失败:', exc);
|
|
@@ -730,7 +838,7 @@ export class AUNClient {
|
|
|
730
838
|
timestamp: (src.timestamp ?? null),
|
|
731
839
|
_decrypt_error: String(exc),
|
|
732
840
|
};
|
|
733
|
-
await this.
|
|
841
|
+
await this._publishAppEvent('message.undecryptable', safeEvent);
|
|
734
842
|
}
|
|
735
843
|
}
|
|
736
844
|
}
|
|
@@ -747,7 +855,7 @@ export class AUNClient {
|
|
|
747
855
|
async _processAndPublishGroupMessage(data) {
|
|
748
856
|
try {
|
|
749
857
|
if (!isJsonObject(data)) {
|
|
750
|
-
await this.
|
|
858
|
+
await this._publishAppEvent('group.message_created', data);
|
|
751
859
|
return;
|
|
752
860
|
}
|
|
753
861
|
const msg = { ...data };
|
|
@@ -783,19 +891,12 @@ export class AUNClient {
|
|
|
783
891
|
}
|
|
784
892
|
this._saveSeqTrackerState();
|
|
785
893
|
}
|
|
786
|
-
// 记录已推送的 seq,补洞路径据此去重
|
|
787
|
-
if (groupId && seq !== undefined && seq !== null) {
|
|
788
|
-
const nsKey = `group:${groupId}`;
|
|
789
|
-
if (!this._pushedSeqs.has(nsKey))
|
|
790
|
-
this._pushedSeqs.set(nsKey, new Set());
|
|
791
|
-
this._pushedSeqs.get(nsKey).add(seq);
|
|
792
|
-
}
|
|
793
894
|
// R3: 解密失败 → 不 publish 密文给应用层,入 pending 队列 + 发 undecryptable 事件
|
|
794
895
|
const payloadForCheck = isJsonObject(msg.payload) ? msg.payload : null;
|
|
795
896
|
if (payloadForCheck?.type === 'e2ee.group_encrypted' && !decrypted.e2ee) {
|
|
796
897
|
if (groupId)
|
|
797
898
|
this._enqueuePendingDecrypt(groupId, msg);
|
|
798
|
-
await this.
|
|
899
|
+
await this._publishAppEvent('group.message_undecryptable', {
|
|
799
900
|
message_id: msg.message_id ?? null,
|
|
800
901
|
group_id: groupId,
|
|
801
902
|
from: msg.from ?? null,
|
|
@@ -805,7 +906,13 @@ export class AUNClient {
|
|
|
805
906
|
});
|
|
806
907
|
return;
|
|
807
908
|
}
|
|
808
|
-
|
|
909
|
+
if (groupId && seq !== undefined && seq !== null) {
|
|
910
|
+
const nsKey = `group:${groupId}`;
|
|
911
|
+
await this._publishOrderedMessage('group.message_created', nsKey, seq, decrypted);
|
|
912
|
+
}
|
|
913
|
+
else {
|
|
914
|
+
await this._publishAppEvent('group.message_created', decrypted);
|
|
915
|
+
}
|
|
809
916
|
}
|
|
810
917
|
catch (exc) {
|
|
811
918
|
console.warn('群消息解密失败:', exc);
|
|
@@ -820,7 +927,7 @@ export class AUNClient {
|
|
|
820
927
|
timestamp: (src.timestamp ?? null),
|
|
821
928
|
_decrypt_error: String(exc),
|
|
822
929
|
};
|
|
823
|
-
await this.
|
|
930
|
+
await this._publishAppEvent('group.message_undecryptable', safeEvent);
|
|
824
931
|
}
|
|
825
932
|
}
|
|
826
933
|
}
|
|
@@ -828,7 +935,7 @@ export class AUNClient {
|
|
|
828
935
|
async _autoPullGroupMessages(notification) {
|
|
829
936
|
const groupId = (notification.group_id ?? '');
|
|
830
937
|
if (!groupId) {
|
|
831
|
-
await this.
|
|
938
|
+
await this._publishAppEvent('group.message_created', notification);
|
|
832
939
|
return;
|
|
833
940
|
}
|
|
834
941
|
const ns = `group:${groupId}`;
|
|
@@ -844,15 +951,19 @@ export class AUNClient {
|
|
|
844
951
|
const messages = result.messages;
|
|
845
952
|
if (Array.isArray(messages)) {
|
|
846
953
|
// ⚠️ 不再重复调用 onPullResult:call('group.pull') 拦截器已在内部调用过一次
|
|
847
|
-
// pushedSeqs 去重:跳过已通过推送路径分发的消息
|
|
848
954
|
const pushed = this._pushedSeqs.get(ns);
|
|
849
955
|
for (const msg of messages) {
|
|
850
956
|
if (isJsonObject(msg)) {
|
|
851
957
|
const s = msg.seq;
|
|
852
958
|
if (pushed && s !== undefined && s !== null && pushed.has(s)) {
|
|
853
|
-
continue; //
|
|
959
|
+
continue; // 已发布到应用层,跳过
|
|
960
|
+
}
|
|
961
|
+
if (s !== undefined && s !== null) {
|
|
962
|
+
await this._publishOrderedMessage('group.message_created', ns, s, msg);
|
|
963
|
+
}
|
|
964
|
+
else {
|
|
965
|
+
await this._publishAppEvent('group.message_created', msg);
|
|
854
966
|
}
|
|
855
|
-
await this._dispatcher.publish('group.message_created', msg);
|
|
856
967
|
}
|
|
857
968
|
}
|
|
858
969
|
this._prunePushedSeqs(ns);
|
|
@@ -864,7 +975,7 @@ export class AUNClient {
|
|
|
864
975
|
console.warn('自动 pull 群消息失败:', exc);
|
|
865
976
|
}
|
|
866
977
|
// pull 失败时仍透传原始通知
|
|
867
|
-
await this.
|
|
978
|
+
await this._publishAppEvent('group.message_created', notification);
|
|
868
979
|
}
|
|
869
980
|
/** 后台补齐群消息空洞 */
|
|
870
981
|
async _fillGroupGap(groupId) {
|
|
@@ -873,10 +984,6 @@ export class AUNClient {
|
|
|
873
984
|
return;
|
|
874
985
|
const ns = `group:${groupId}`;
|
|
875
986
|
const afterSeq = this._seqTracker.getContiguousSeq(ns);
|
|
876
|
-
// 冷启动(seq=0):服务端推送会带全量消息,SDK 不主动补洞避免重复拉取
|
|
877
|
-
if (afterSeq === 0) {
|
|
878
|
-
return;
|
|
879
|
-
}
|
|
880
987
|
// 去重:同一 (group:id:after_seq) 在飞行中只补一次
|
|
881
988
|
// S1: 使用 try/finally 保证无论成功失败都清理 dedupKey,避免旧实现只在异常路径清理
|
|
882
989
|
// 导致成功后相同 afterSeq 再次出现空洞时被永久抑制。
|
|
@@ -902,7 +1009,12 @@ export class AUNClient {
|
|
|
902
1009
|
const s = msg.seq;
|
|
903
1010
|
if (pushed && s !== undefined && s !== null && pushed.has(s))
|
|
904
1011
|
continue;
|
|
905
|
-
|
|
1012
|
+
if (s !== undefined && s !== null) {
|
|
1013
|
+
await this._publishOrderedMessage('group.message_created', ns, s, msg);
|
|
1014
|
+
}
|
|
1015
|
+
else {
|
|
1016
|
+
await this._publishAppEvent('group.message_created', msg);
|
|
1017
|
+
}
|
|
906
1018
|
}
|
|
907
1019
|
}
|
|
908
1020
|
this._prunePushedSeqs(ns);
|
|
@@ -925,10 +1037,6 @@ export class AUNClient {
|
|
|
925
1037
|
return;
|
|
926
1038
|
const ns = `group_event:${groupId}`;
|
|
927
1039
|
const afterSeq = this._seqTracker.getContiguousSeq(ns);
|
|
928
|
-
// 冷启动(seq=0):服务端推送会带全量事件,SDK 不主动补洞避免重复拉取
|
|
929
|
-
if (afterSeq === 0) {
|
|
930
|
-
return;
|
|
931
|
-
}
|
|
932
1040
|
// 去重:同一 (group_evt:id:after_seq) 在飞行中只补一次
|
|
933
1041
|
// S1: 使用 try/finally 保证无论成功失败都清理 dedupKey
|
|
934
1042
|
const dedupKey = `group_evt:${groupId}:${afterSeq}`;
|
|
@@ -947,14 +1055,24 @@ export class AUNClient {
|
|
|
947
1055
|
const events = result.events;
|
|
948
1056
|
if (Array.isArray(events)) {
|
|
949
1057
|
this._seqTracker.onPullResult(ns, events.filter(isJsonObject));
|
|
1058
|
+
const cursor = isJsonObject(result.cursor) ? result.cursor : null;
|
|
1059
|
+
const serverAck = cursor ? Number(cursor.current_seq ?? 0) : 0;
|
|
1060
|
+
if (serverAck > 0) {
|
|
1061
|
+
const contigBefore = this._seqTracker.getContiguousSeq(ns);
|
|
1062
|
+
if (contigBefore < serverAck) {
|
|
1063
|
+
console.info('[aun_core] group.pull_events retention-floor 推进: ns=' + ns + ' contiguous=' + contigBefore + ' -> cursor.current_seq=' + serverAck);
|
|
1064
|
+
this._seqTracker.forceContiguousSeq(ns, serverAck);
|
|
1065
|
+
}
|
|
1066
|
+
}
|
|
950
1067
|
// 持久化 cursor + ack_events(与 Python 对齐)
|
|
951
1068
|
this._saveSeqTrackerState();
|
|
952
1069
|
const contig = this._seqTracker.getContiguousSeq(ns);
|
|
953
|
-
if (contig > 0) {
|
|
1070
|
+
if (contig > 0 && (events.length > 0 || serverAck > 0)) {
|
|
954
1071
|
this._transport.call('group.ack_events', {
|
|
955
1072
|
group_id: groupId,
|
|
956
1073
|
event_seq: contig,
|
|
957
1074
|
device_id: this._deviceId,
|
|
1075
|
+
slot_id: this._slotId,
|
|
958
1076
|
}).catch((e) => { console.warn('群事件 auto-ack 失败: group=' + groupId, e); });
|
|
959
1077
|
}
|
|
960
1078
|
for (const evt of events) {
|
|
@@ -989,10 +1107,6 @@ export class AUNClient {
|
|
|
989
1107
|
return;
|
|
990
1108
|
const ns = `p2p:${this._aid}`;
|
|
991
1109
|
const afterSeq = this._seqTracker.getContiguousSeq(ns);
|
|
992
|
-
// 新设备(seq=0)没有历史 prekey,拉旧消息也解不了
|
|
993
|
-
if (afterSeq === 0) {
|
|
994
|
-
return;
|
|
995
|
-
}
|
|
996
1110
|
// 去重:同一 (type:after_seq) 在飞行中只补一次
|
|
997
1111
|
// S1: 使用 try/finally 保证无论成功失败都清理 dedupKey
|
|
998
1112
|
const dedupKey = `p2p:${afterSeq}`;
|
|
@@ -1016,7 +1130,12 @@ export class AUNClient {
|
|
|
1016
1130
|
const s = msg.seq;
|
|
1017
1131
|
if (pushed && s !== undefined && s !== null && pushed.has(s))
|
|
1018
1132
|
continue;
|
|
1019
|
-
|
|
1133
|
+
if (s !== undefined && s !== null) {
|
|
1134
|
+
await this._publishOrderedMessage('message.received', ns, s, msg);
|
|
1135
|
+
}
|
|
1136
|
+
else {
|
|
1137
|
+
await this._publishAppEvent('message.received', msg);
|
|
1138
|
+
}
|
|
1020
1139
|
}
|
|
1021
1140
|
}
|
|
1022
1141
|
this._prunePushedSeqs(ns);
|
|
@@ -1045,6 +1164,119 @@ export class AUNClient {
|
|
|
1045
1164
|
if (pushed.size === 0)
|
|
1046
1165
|
this._pushedSeqs.delete(ns);
|
|
1047
1166
|
}
|
|
1167
|
+
_markPublishedSeq(ns, seq) {
|
|
1168
|
+
let pushed = this._pushedSeqs.get(ns);
|
|
1169
|
+
if (!pushed) {
|
|
1170
|
+
pushed = new Set();
|
|
1171
|
+
this._pushedSeqs.set(ns, pushed);
|
|
1172
|
+
}
|
|
1173
|
+
pushed.add(seq);
|
|
1174
|
+
if (pushed.size > PUSHED_SEQS_LIMIT) {
|
|
1175
|
+
const keep = [...pushed].sort((a, b) => a - b).slice(-PUSHED_SEQS_LIMIT);
|
|
1176
|
+
this._pushedSeqs.set(ns, new Set(keep));
|
|
1177
|
+
}
|
|
1178
|
+
}
|
|
1179
|
+
_enqueueOrderedMessage(ns, event, seq, payload) {
|
|
1180
|
+
let queue = this._pendingOrderedMsgs.get(ns);
|
|
1181
|
+
if (!queue) {
|
|
1182
|
+
queue = new Map();
|
|
1183
|
+
this._pendingOrderedMsgs.set(ns, queue);
|
|
1184
|
+
}
|
|
1185
|
+
queue.set(seq, { event, payload });
|
|
1186
|
+
if (queue.size > PENDING_ORDERED_LIMIT) {
|
|
1187
|
+
const drop = [...queue.keys()].sort((a, b) => a - b).slice(0, queue.size - PENDING_ORDERED_LIMIT);
|
|
1188
|
+
for (const oldSeq of drop)
|
|
1189
|
+
queue.delete(oldSeq);
|
|
1190
|
+
}
|
|
1191
|
+
}
|
|
1192
|
+
_isInstanceScopedMessageEvent(event) {
|
|
1193
|
+
return event === 'message.received'
|
|
1194
|
+
|| event === 'message.undecryptable'
|
|
1195
|
+
|| event === 'group.message_created'
|
|
1196
|
+
|| event === 'group.message_undecryptable';
|
|
1197
|
+
}
|
|
1198
|
+
_attachCurrentInstanceContext(payload) {
|
|
1199
|
+
if (!isJsonObject(payload))
|
|
1200
|
+
return payload;
|
|
1201
|
+
const result = { ...payload };
|
|
1202
|
+
if (this._deviceId && !String(result.device_id ?? '').trim()) {
|
|
1203
|
+
result.device_id = this._deviceId;
|
|
1204
|
+
}
|
|
1205
|
+
if (this._slotId && !String(result.slot_id ?? '').trim()) {
|
|
1206
|
+
result.slot_id = this._slotId;
|
|
1207
|
+
}
|
|
1208
|
+
return result;
|
|
1209
|
+
}
|
|
1210
|
+
_normalizePublishedMessagePayload(event, payload) {
|
|
1211
|
+
if (!this._isInstanceScopedMessageEvent(event))
|
|
1212
|
+
return payload;
|
|
1213
|
+
return this._attachCurrentInstanceContext(payload);
|
|
1214
|
+
}
|
|
1215
|
+
async _publishAppEvent(event, payload) {
|
|
1216
|
+
await this._dispatcher.publish(event, this._normalizePublishedMessagePayload(event, payload));
|
|
1217
|
+
}
|
|
1218
|
+
_messageTargetsCurrentInstance(message) {
|
|
1219
|
+
if (!isJsonObject(message))
|
|
1220
|
+
return true;
|
|
1221
|
+
const targetDeviceId = String(message.device_id ?? '').trim();
|
|
1222
|
+
if (targetDeviceId && this._deviceId && targetDeviceId !== this._deviceId) {
|
|
1223
|
+
return false;
|
|
1224
|
+
}
|
|
1225
|
+
const targetSlotId = String(message.slot_id ?? '').trim();
|
|
1226
|
+
if (targetSlotId && this._slotId && targetSlotId !== this._slotId) {
|
|
1227
|
+
return false;
|
|
1228
|
+
}
|
|
1229
|
+
return true;
|
|
1230
|
+
}
|
|
1231
|
+
async _drainOrderedMessages(ns, beforeSeq) {
|
|
1232
|
+
const queue = this._pendingOrderedMsgs.get(ns);
|
|
1233
|
+
if (!queue || queue.size === 0)
|
|
1234
|
+
return;
|
|
1235
|
+
const contig = this._seqTracker.getContiguousSeq(ns);
|
|
1236
|
+
const ready = [...queue.keys()]
|
|
1237
|
+
.filter((seq) => seq <= contig && (beforeSeq === undefined || seq < beforeSeq))
|
|
1238
|
+
.sort((a, b) => a - b);
|
|
1239
|
+
for (const seq of ready) {
|
|
1240
|
+
const item = queue.get(seq);
|
|
1241
|
+
queue.delete(seq);
|
|
1242
|
+
if (!item || this._pushedSeqs.get(ns)?.has(seq))
|
|
1243
|
+
continue;
|
|
1244
|
+
await this._publishAppEvent(item.event, item.payload);
|
|
1245
|
+
this._markPublishedSeq(ns, seq);
|
|
1246
|
+
}
|
|
1247
|
+
if (queue.size === 0)
|
|
1248
|
+
this._pendingOrderedMsgs.delete(ns);
|
|
1249
|
+
}
|
|
1250
|
+
async _publishOrderedMessage(event, ns, seq, payload) {
|
|
1251
|
+
const seqNum = Number(seq);
|
|
1252
|
+
if (!Number.isFinite(seqNum) || !Number.isInteger(seqNum) || seqNum <= 0) {
|
|
1253
|
+
await this._publishAppEvent(event, payload);
|
|
1254
|
+
return true;
|
|
1255
|
+
}
|
|
1256
|
+
if (this._pushedSeqs.get(ns)?.has(seqNum)) {
|
|
1257
|
+
const queue = this._pendingOrderedMsgs.get(ns);
|
|
1258
|
+
queue?.delete(seqNum);
|
|
1259
|
+
if (queue && queue.size === 0)
|
|
1260
|
+
this._pendingOrderedMsgs.delete(ns);
|
|
1261
|
+
return false;
|
|
1262
|
+
}
|
|
1263
|
+
const contig = this._seqTracker.getContiguousSeq(ns);
|
|
1264
|
+
if (seqNum > contig) {
|
|
1265
|
+
this._enqueueOrderedMessage(ns, event, seqNum, payload);
|
|
1266
|
+
return false;
|
|
1267
|
+
}
|
|
1268
|
+
await this._drainOrderedMessages(ns, seqNum);
|
|
1269
|
+
if (this._pushedSeqs.get(ns)?.has(seqNum))
|
|
1270
|
+
return false;
|
|
1271
|
+
const queue = this._pendingOrderedMsgs.get(ns);
|
|
1272
|
+
queue?.delete(seqNum);
|
|
1273
|
+
if (queue && queue.size === 0)
|
|
1274
|
+
this._pendingOrderedMsgs.delete(ns);
|
|
1275
|
+
await this._publishAppEvent(event, payload);
|
|
1276
|
+
this._markPublishedSeq(ns, seqNum);
|
|
1277
|
+
await this._drainOrderedMessages(ns);
|
|
1278
|
+
return true;
|
|
1279
|
+
}
|
|
1048
1280
|
/**
|
|
1049
1281
|
* 上线/重连后一次性同步所有已加入群:
|
|
1050
1282
|
* 1. 有 epoch key 的群 → 补消息 + 补事件
|
|
@@ -1313,6 +1545,8 @@ export class AUNClient {
|
|
|
1313
1545
|
// 4. 清理推送 seq 去重缓存
|
|
1314
1546
|
this._pushedSeqs.delete(`group:${groupId}`);
|
|
1315
1547
|
this._pushedSeqs.delete(`group_event:${groupId}`);
|
|
1548
|
+
this._pendingOrderedMsgs.delete(`group:${groupId}`);
|
|
1549
|
+
this._pendingDecryptMsgs.delete(`group:${groupId}`);
|
|
1316
1550
|
console.info(`[aun_core] 已清理解散群组 ${groupId} 的本地状态`);
|
|
1317
1551
|
}
|
|
1318
1552
|
async _verifyEventSignature(_event, cs) {
|
|
@@ -1346,15 +1580,34 @@ export class AUNClient {
|
|
|
1346
1580
|
const ok = await ecdsaVerifyDer(pubKey, sigBytes, signData);
|
|
1347
1581
|
if (!ok) {
|
|
1348
1582
|
console.warn('[aun_core] 群事件验签失败 aid=%s method=%s', sigAid, method);
|
|
1583
|
+
// P1-16: 签名失败统一发布事件
|
|
1584
|
+
this._dispatcher.publish('signature.verification_failed', {
|
|
1585
|
+
aid: sigAid, method, error: 'ECDSA verification failed',
|
|
1586
|
+
});
|
|
1349
1587
|
}
|
|
1350
1588
|
return ok;
|
|
1351
1589
|
}
|
|
1352
1590
|
catch (exc) {
|
|
1353
1591
|
console.warn('[aun_core] 群事件验签异常:', exc);
|
|
1592
|
+
// P1-16: 签名失败统一发布事件
|
|
1593
|
+
this._dispatcher.publish('signature.verification_failed', {
|
|
1594
|
+
aid: sigAid, method, error: String(exc),
|
|
1595
|
+
});
|
|
1354
1596
|
return false;
|
|
1355
1597
|
}
|
|
1356
1598
|
}
|
|
1357
1599
|
// ── E2EE 自动加密 ────────────────────────────────
|
|
1600
|
+
_protectedHeadersFromParams(params) {
|
|
1601
|
+
const value = params.protected_headers ?? params.headers;
|
|
1602
|
+
if (value == null)
|
|
1603
|
+
return null;
|
|
1604
|
+
if (isJsonObject(value))
|
|
1605
|
+
return value;
|
|
1606
|
+
if (typeof value === 'object' && typeof value.toObject === 'function') {
|
|
1607
|
+
return value;
|
|
1608
|
+
}
|
|
1609
|
+
return null;
|
|
1610
|
+
}
|
|
1358
1611
|
/** 自动加密并发送 P2P 消息 */
|
|
1359
1612
|
async _sendEncrypted(params) {
|
|
1360
1613
|
const toAid = String(params.to ?? '');
|
|
@@ -1366,6 +1619,7 @@ export class AUNClient {
|
|
|
1366
1619
|
throw new ValidationError('message.send payload must be an object when encrypt=true');
|
|
1367
1620
|
}
|
|
1368
1621
|
const persistRequired = Boolean(params.persist_required || params.durable);
|
|
1622
|
+
const protectedHeaders = this._protectedHeadersFromParams(params);
|
|
1369
1623
|
// Lazy P2P sync:首次发送前自动拉取历史,避免重连后 seq 空洞
|
|
1370
1624
|
if (!this._p2pSynced) {
|
|
1371
1625
|
await this._lazySyncP2p();
|
|
@@ -1376,6 +1630,7 @@ export class AUNClient {
|
|
|
1376
1630
|
payload,
|
|
1377
1631
|
messageId,
|
|
1378
1632
|
timestamp,
|
|
1633
|
+
protectedHeaders,
|
|
1379
1634
|
});
|
|
1380
1635
|
if (recipientPrekeys.length <= 1 && selfSyncCopies.length === 0) {
|
|
1381
1636
|
return await this._sendEncryptedSingle({
|
|
@@ -1385,6 +1640,7 @@ export class AUNClient {
|
|
|
1385
1640
|
timestamp,
|
|
1386
1641
|
prekey: recipientPrekeys[0],
|
|
1387
1642
|
persistRequired,
|
|
1643
|
+
protectedHeaders,
|
|
1388
1644
|
});
|
|
1389
1645
|
}
|
|
1390
1646
|
const recipientCopies = await this._buildRecipientDeviceCopies({
|
|
@@ -1393,6 +1649,7 @@ export class AUNClient {
|
|
|
1393
1649
|
messageId,
|
|
1394
1650
|
timestamp,
|
|
1395
1651
|
prekeys: recipientPrekeys,
|
|
1652
|
+
protectedHeaders,
|
|
1396
1653
|
});
|
|
1397
1654
|
const sendParams = {
|
|
1398
1655
|
to: toAid,
|
|
@@ -1453,6 +1710,7 @@ export class AUNClient {
|
|
|
1453
1710
|
prekey,
|
|
1454
1711
|
messageId: opts.messageId,
|
|
1455
1712
|
timestamp: opts.timestamp,
|
|
1713
|
+
protectedHeaders: opts.protectedHeaders,
|
|
1456
1714
|
});
|
|
1457
1715
|
await this._ensureEncryptResult(opts.toAid, encryptResult);
|
|
1458
1716
|
const sendParams = {
|
|
@@ -1471,7 +1729,7 @@ export class AUNClient {
|
|
|
1471
1729
|
async _buildRecipientDeviceCopies(opts) {
|
|
1472
1730
|
const recipientCopies = [];
|
|
1473
1731
|
const certCache = new Map();
|
|
1474
|
-
for (const prekey of opts.prekeys) {
|
|
1732
|
+
for (const prekey of normalizePeerPrekeys(opts.prekeys)) {
|
|
1475
1733
|
const deviceId = String(prekey.device_id ?? '').trim();
|
|
1476
1734
|
const peerCertFingerprint = String(prekey.cert_fingerprint ?? '').trim().toLowerCase();
|
|
1477
1735
|
const cacheKey = peerCertFingerprint || '__default__';
|
|
@@ -1487,6 +1745,7 @@ export class AUNClient {
|
|
|
1487
1745
|
prekey,
|
|
1488
1746
|
messageId: opts.messageId,
|
|
1489
1747
|
timestamp: opts.timestamp,
|
|
1748
|
+
protectedHeaders: opts.protectedHeaders,
|
|
1490
1749
|
});
|
|
1491
1750
|
await this._ensureEncryptResult(opts.toAid, encryptResult);
|
|
1492
1751
|
recipientCopies.push({
|
|
@@ -1528,7 +1787,7 @@ export class AUNClient {
|
|
|
1528
1787
|
const myAid = this._aid;
|
|
1529
1788
|
if (!myAid)
|
|
1530
1789
|
return [];
|
|
1531
|
-
const prekeys = await this._fetchPeerPrekeys(myAid);
|
|
1790
|
+
const prekeys = normalizePeerPrekeys(await this._fetchPeerPrekeys(myAid));
|
|
1532
1791
|
if (prekeys.length === 0)
|
|
1533
1792
|
return [];
|
|
1534
1793
|
const copies = [];
|
|
@@ -1545,6 +1804,7 @@ export class AUNClient {
|
|
|
1545
1804
|
prekey,
|
|
1546
1805
|
messageId: opts.messageId,
|
|
1547
1806
|
timestamp: opts.timestamp,
|
|
1807
|
+
protectedHeaders: opts.protectedHeaders,
|
|
1548
1808
|
});
|
|
1549
1809
|
await this._ensureEncryptResult(myAid, encryptResult);
|
|
1550
1810
|
copies.push({
|
|
@@ -1560,6 +1820,8 @@ export class AUNClient {
|
|
|
1560
1820
|
prekey: opts.prekey ?? null,
|
|
1561
1821
|
messageId: opts.messageId,
|
|
1562
1822
|
timestamp: opts.timestamp,
|
|
1823
|
+
protectedHeaders: opts.protectedHeaders,
|
|
1824
|
+
context: opts.context ?? null,
|
|
1563
1825
|
});
|
|
1564
1826
|
return [envelope, encryptResult];
|
|
1565
1827
|
}
|
|
@@ -1666,49 +1928,131 @@ export class AUNClient {
|
|
|
1666
1928
|
}
|
|
1667
1929
|
/** 自动加密并发送群组消息 */
|
|
1668
1930
|
async _sendGroupEncrypted(params) {
|
|
1669
|
-
|
|
1931
|
+
return this._callGroupEncryptedRpc('group.send', params, {
|
|
1932
|
+
idField: 'message_id',
|
|
1933
|
+
idPrefix: 'gm',
|
|
1934
|
+
});
|
|
1935
|
+
}
|
|
1936
|
+
async _putGroupThoughtEncrypted(params) {
|
|
1937
|
+
return this._callGroupEncryptedRpc('group.thought.put', params, {
|
|
1938
|
+
idField: 'thought_id',
|
|
1939
|
+
idPrefix: 'gt',
|
|
1940
|
+
extraFields: ['context'],
|
|
1941
|
+
});
|
|
1942
|
+
}
|
|
1943
|
+
async _putMessageThoughtEncrypted(params) {
|
|
1944
|
+
const toAid = String(params.to ?? '').trim();
|
|
1945
|
+
this._validateMessageRecipient(toAid);
|
|
1670
1946
|
const payload = isJsonObject(params.payload) ? params.payload : null;
|
|
1671
|
-
if (!
|
|
1672
|
-
throw new ValidationError('
|
|
1947
|
+
if (!toAid) {
|
|
1948
|
+
throw new ValidationError('message.thought.put requires to');
|
|
1673
1949
|
}
|
|
1674
1950
|
if (payload === null) {
|
|
1675
|
-
throw new ValidationError('
|
|
1676
|
-
}
|
|
1677
|
-
// Lazy group sync:首次发送群消息前自动拉取历史,避免重连后 seq 空洞
|
|
1678
|
-
if (!this._groupSynced.has(groupId)) {
|
|
1679
|
-
await this._lazySyncGroup(groupId);
|
|
1951
|
+
throw new ValidationError('message.thought.put payload must be an object when encrypt=true');
|
|
1680
1952
|
}
|
|
1681
|
-
|
|
1682
|
-
|
|
1953
|
+
const thoughtId = String(params.thought_id ?? '') || `mt-${_uuidV4()}`;
|
|
1954
|
+
const timestamp = Number(params.timestamp ?? Date.now());
|
|
1955
|
+
const prekey = await this._fetchPeerPrekey(toAid);
|
|
1956
|
+
const peerCertFingerprint = typeof prekey?.cert_fingerprint === 'string' ? prekey.cert_fingerprint : undefined;
|
|
1957
|
+
const peerCertPem = await this._fetchPeerCert(toAid, peerCertFingerprint);
|
|
1958
|
+
const [envelope, encryptResult] = await this._encryptCopyPayload({
|
|
1959
|
+
logicalToAid: toAid,
|
|
1960
|
+
payload,
|
|
1961
|
+
peerCertPem,
|
|
1962
|
+
prekey,
|
|
1963
|
+
messageId: thoughtId,
|
|
1964
|
+
timestamp,
|
|
1965
|
+
protectedHeaders: this._protectedHeadersFromParams(params),
|
|
1966
|
+
context: isJsonObject(params.context) ? params.context : null,
|
|
1967
|
+
});
|
|
1968
|
+
await this._ensureEncryptResult(toAid, encryptResult);
|
|
1969
|
+
const sendParams = {
|
|
1970
|
+
to: toAid,
|
|
1971
|
+
payload: envelope,
|
|
1972
|
+
type: 'e2ee.encrypted',
|
|
1973
|
+
encrypted: true,
|
|
1974
|
+
thought_id: thoughtId,
|
|
1975
|
+
timestamp,
|
|
1976
|
+
};
|
|
1977
|
+
if ('context' in params)
|
|
1978
|
+
sendParams.context = params.context;
|
|
1979
|
+
await this._signClientOperation('message.thought.put', sendParams);
|
|
1980
|
+
return this._transport.call('message.thought.put', sendParams);
|
|
1981
|
+
}
|
|
1982
|
+
async _callGroupEncryptedRpc(method, params, options) {
|
|
1983
|
+
let prepared = await this._prepareGroupEncryptedRpcParams(method, params, options);
|
|
1683
1984
|
for (let attempt = 0; attempt < 2; attempt += 1) {
|
|
1684
|
-
const epochResult = await this._committedGroupEpochState(groupId);
|
|
1685
|
-
const committedEpoch = Number(epochResult.committed_epoch ?? epochResult.epoch ?? 0);
|
|
1686
|
-
const envelope = committedEpoch > 0
|
|
1687
|
-
? await this._groupE2ee.encryptWithEpoch(groupId, await this._ensureCommittedGroupSecretForSend(groupId, committedEpoch, epochResult), payload)
|
|
1688
|
-
: await this._groupE2ee.encrypt(groupId, payload);
|
|
1689
|
-
const sendParams = {
|
|
1690
|
-
group_id: groupId,
|
|
1691
|
-
payload: envelope,
|
|
1692
|
-
type: 'e2ee.group_encrypted',
|
|
1693
|
-
encrypted: true,
|
|
1694
|
-
};
|
|
1695
|
-
if (this._deviceId && sendParams.device_id === undefined) {
|
|
1696
|
-
sendParams.device_id = this._deviceId;
|
|
1697
|
-
}
|
|
1698
|
-
await this._signClientOperation('group.send', sendParams);
|
|
1699
1985
|
try {
|
|
1700
|
-
return await this._transport.call(
|
|
1986
|
+
return await this._transport.call(method, prepared.sendParams);
|
|
1701
1987
|
}
|
|
1702
1988
|
catch (exc) {
|
|
1703
1989
|
if (attempt === 0 && this._isRecoverableGroupEpochError(exc)) {
|
|
1704
|
-
console.warn(`[aun_core] 群 ${groupId}
|
|
1705
|
-
await this.
|
|
1990
|
+
console.warn(`[aun_core] 群 ${prepared.groupId} 调用 ${method} 时 epoch 已过旧,恢复密钥后重加密重试一次: ${formatCaughtError(exc)}`);
|
|
1991
|
+
prepared = await this._prepareGroupEncryptedRpcParams(method, params, options, true);
|
|
1706
1992
|
continue;
|
|
1707
1993
|
}
|
|
1708
1994
|
throw exc;
|
|
1709
1995
|
}
|
|
1710
1996
|
}
|
|
1711
|
-
throw new StateError(
|
|
1997
|
+
throw new StateError(`${method} failed after epoch recovery retry: group=${prepared.groupId}`);
|
|
1998
|
+
}
|
|
1999
|
+
async _prepareGroupEncryptedRpcParams(method, params, options, strictEpochReady = false) {
|
|
2000
|
+
const groupId = String(params.group_id ?? '');
|
|
2001
|
+
const payload = isJsonObject(params.payload) ? params.payload : null;
|
|
2002
|
+
if (!groupId) {
|
|
2003
|
+
throw new ValidationError(`${method} requires group_id`);
|
|
2004
|
+
}
|
|
2005
|
+
if (payload === null) {
|
|
2006
|
+
throw new ValidationError(`${method} payload must be an object when encrypt=true`);
|
|
2007
|
+
}
|
|
2008
|
+
if (!this._groupSynced.has(groupId)) {
|
|
2009
|
+
await this._lazySyncGroup(groupId);
|
|
2010
|
+
}
|
|
2011
|
+
await this._ensureGroupEpochReady(groupId, strictEpochReady);
|
|
2012
|
+
await this._waitForGroupMembershipEpochFloor(groupId, 2000);
|
|
2013
|
+
const epochResult = await this._committedGroupEpochState(groupId);
|
|
2014
|
+
const committedEpoch = Number(epochResult.committed_epoch ?? epochResult.epoch ?? 0);
|
|
2015
|
+
const operationId = String(params[options.idField] ?? '').trim()
|
|
2016
|
+
|| `${options.idPrefix}-${crypto.randomUUID()}`;
|
|
2017
|
+
const timestamp = Number(params.timestamp ?? Date.now());
|
|
2018
|
+
const protectedHeaders = this._protectedHeadersFromParams(params);
|
|
2019
|
+
const context = method === 'group.thought.put' && isJsonObject(params.context)
|
|
2020
|
+
? params.context
|
|
2021
|
+
: null;
|
|
2022
|
+
const envelope = committedEpoch > 0
|
|
2023
|
+
? await this._groupE2ee.encryptWithEpoch(groupId, await this._ensureCommittedGroupSecretForSend(groupId, committedEpoch, epochResult), payload, {
|
|
2024
|
+
messageId: operationId,
|
|
2025
|
+
timestamp,
|
|
2026
|
+
protectedHeaders,
|
|
2027
|
+
context,
|
|
2028
|
+
})
|
|
2029
|
+
: await this._groupE2ee.encrypt(groupId, payload, {
|
|
2030
|
+
messageId: operationId,
|
|
2031
|
+
timestamp,
|
|
2032
|
+
protectedHeaders,
|
|
2033
|
+
context,
|
|
2034
|
+
});
|
|
2035
|
+
const sendParams = {
|
|
2036
|
+
group_id: groupId,
|
|
2037
|
+
payload: envelope,
|
|
2038
|
+
type: 'e2ee.group_encrypted',
|
|
2039
|
+
encrypted: true,
|
|
2040
|
+
[options.idField]: operationId,
|
|
2041
|
+
timestamp,
|
|
2042
|
+
};
|
|
2043
|
+
if (this._deviceId && sendParams.device_id === undefined) {
|
|
2044
|
+
sendParams.device_id = this._deviceId;
|
|
2045
|
+
}
|
|
2046
|
+
if (sendParams.slot_id === undefined) {
|
|
2047
|
+
sendParams.slot_id = this._slotId;
|
|
2048
|
+
}
|
|
2049
|
+
for (const field of options.extraFields ?? []) {
|
|
2050
|
+
if (params[field] !== undefined) {
|
|
2051
|
+
sendParams[field] = params[field];
|
|
2052
|
+
}
|
|
2053
|
+
}
|
|
2054
|
+
await this._signClientOperation(method, sendParams);
|
|
2055
|
+
return { sendParams, groupId };
|
|
1712
2056
|
}
|
|
1713
2057
|
/**
|
|
1714
2058
|
* 首次发送群消息前懒拉取历史消息,同步 seqTracker 避免空洞。
|
|
@@ -2030,7 +2374,7 @@ export class AUNClient {
|
|
|
2030
2374
|
const ns = `group:${groupId}`;
|
|
2031
2375
|
const queue = this._pendingDecryptMsgs.get(ns) ?? [];
|
|
2032
2376
|
queue.push(msg);
|
|
2033
|
-
this._pendingDecryptMsgs.set(ns, queue.slice(-
|
|
2377
|
+
this._pendingDecryptMsgs.set(ns, queue.slice(-PENDING_DECRYPT_LIMIT));
|
|
2034
2378
|
}
|
|
2035
2379
|
async _retryPendingDecryptMsgs(groupId) {
|
|
2036
2380
|
const ns = `group:${groupId}`;
|
|
@@ -2047,14 +2391,20 @@ export class AUNClient {
|
|
|
2047
2391
|
stillPending.push(msg);
|
|
2048
2392
|
continue;
|
|
2049
2393
|
}
|
|
2050
|
-
|
|
2394
|
+
const seq = msg.seq;
|
|
2395
|
+
if (seq !== undefined && seq !== null) {
|
|
2396
|
+
await this._publishOrderedMessage('group.message_created', ns, seq, decrypted);
|
|
2397
|
+
}
|
|
2398
|
+
else {
|
|
2399
|
+
await this._publishAppEvent('group.message_created', decrypted);
|
|
2400
|
+
}
|
|
2051
2401
|
}
|
|
2052
2402
|
catch {
|
|
2053
2403
|
stillPending.push(msg);
|
|
2054
2404
|
}
|
|
2055
2405
|
}
|
|
2056
2406
|
const queuedDuringRetry = this._pendingDecryptMsgs.get(ns) ?? [];
|
|
2057
|
-
const mergedPending = [...stillPending, ...queuedDuringRetry].slice(-
|
|
2407
|
+
const mergedPending = [...stillPending, ...queuedDuringRetry].slice(-PENDING_DECRYPT_LIMIT);
|
|
2058
2408
|
if (mergedPending.length)
|
|
2059
2409
|
this._pendingDecryptMsgs.set(ns, mergedPending);
|
|
2060
2410
|
else
|
|
@@ -2144,7 +2494,7 @@ export class AUNClient {
|
|
|
2144
2494
|
async _decryptGroupMessage(message, opts) {
|
|
2145
2495
|
const payload = isJsonObject(message.payload) ? message.payload : null;
|
|
2146
2496
|
if (payload === null || payload.type !== 'e2ee.group_encrypted') {
|
|
2147
|
-
return message;
|
|
2497
|
+
return this._attachGroupDispatchModeToPayload(message);
|
|
2148
2498
|
}
|
|
2149
2499
|
// 确保发送方证书已缓存(签名验证需要)
|
|
2150
2500
|
const senderAid = String(message.from ?? message.sender_aid ?? '');
|
|
@@ -2158,7 +2508,7 @@ export class AUNClient {
|
|
|
2158
2508
|
// 先尝试直接解密
|
|
2159
2509
|
const result = await this._groupE2ee.decrypt(message, opts);
|
|
2160
2510
|
if (result !== null && isJsonObject(result.e2ee)) {
|
|
2161
|
-
return result;
|
|
2511
|
+
return this._attachGroupDispatchModeToPayload(result);
|
|
2162
2512
|
}
|
|
2163
2513
|
// replay guard 命中:decrypt 返回了原消息(非 null)但无 e2ee 字段
|
|
2164
2514
|
// 不是解密失败,不应触发 recover
|
|
@@ -2174,7 +2524,7 @@ export class AUNClient {
|
|
|
2174
2524
|
if (await this._recoverGroupEpochKey(groupId, epoch, sender, 5000)) {
|
|
2175
2525
|
const retry = await this._groupE2ee.decrypt(message, opts);
|
|
2176
2526
|
if (retry !== null && retry.e2ee)
|
|
2177
|
-
return retry;
|
|
2527
|
+
return this._attachGroupDispatchModeToPayload(retry);
|
|
2178
2528
|
}
|
|
2179
2529
|
}
|
|
2180
2530
|
catch (exc) {
|
|
@@ -2183,6 +2533,21 @@ export class AUNClient {
|
|
|
2183
2533
|
}
|
|
2184
2534
|
return message;
|
|
2185
2535
|
}
|
|
2536
|
+
_attachGroupDispatchModeToPayload(message) {
|
|
2537
|
+
const payload = message.payload;
|
|
2538
|
+
if (!isJsonObject(payload))
|
|
2539
|
+
return message;
|
|
2540
|
+
const rawMode = String(message.dispatch_mode ?? 'broadcast').trim().toLowerCase();
|
|
2541
|
+
const mode = rawMode === 'mention' || rawMode === 'broadcast' ? rawMode : 'broadcast';
|
|
2542
|
+
return {
|
|
2543
|
+
...message,
|
|
2544
|
+
dispatch_mode: mode,
|
|
2545
|
+
payload: {
|
|
2546
|
+
...payload,
|
|
2547
|
+
dispatch_mode: mode,
|
|
2548
|
+
},
|
|
2549
|
+
};
|
|
2550
|
+
}
|
|
2186
2551
|
/** 批量解密群组消息(用于 group.pull)。跳过防重放检查。 */
|
|
2187
2552
|
async _decryptGroupMessages(messages) {
|
|
2188
2553
|
const result = [];
|
|
@@ -2194,10 +2559,108 @@ export class AUNClient {
|
|
|
2194
2559
|
this._enqueuePendingDecrypt(groupId, msg);
|
|
2195
2560
|
continue; // R3: 解密失败不入 result,不 publish 密文给应用层
|
|
2196
2561
|
}
|
|
2562
|
+
if (payload?.type !== 'e2ee.group_encrypted') {
|
|
2563
|
+
result.push(this._attachGroupDispatchModeToPayload(decrypted));
|
|
2564
|
+
continue;
|
|
2565
|
+
}
|
|
2197
2566
|
result.push(decrypted);
|
|
2198
2567
|
}
|
|
2199
2568
|
return result;
|
|
2200
2569
|
}
|
|
2570
|
+
async _decryptGroupThoughts(result) {
|
|
2571
|
+
if (!result.found) {
|
|
2572
|
+
return { ...result, thoughts: [] };
|
|
2573
|
+
}
|
|
2574
|
+
const items = (Array.isArray(result.thoughts) ? result.thoughts : []).filter(isJsonObject);
|
|
2575
|
+
if (!items.length) {
|
|
2576
|
+
return { ...result, thoughts: [] };
|
|
2577
|
+
}
|
|
2578
|
+
const groupId = String(result.group_id ?? '');
|
|
2579
|
+
const senderAid = String(result.sender_aid ?? '');
|
|
2580
|
+
const thoughts = [];
|
|
2581
|
+
for (const item of items) {
|
|
2582
|
+
const payload = isJsonObject(item.payload) ? item.payload : null;
|
|
2583
|
+
const thoughtId = String(item.thought_id ?? item.message_id ?? '');
|
|
2584
|
+
const message = {
|
|
2585
|
+
group_id: groupId,
|
|
2586
|
+
sender_aid: senderAid,
|
|
2587
|
+
from: senderAid,
|
|
2588
|
+
message_id: thoughtId,
|
|
2589
|
+
payload: payload ?? {},
|
|
2590
|
+
created_at: Number(item.created_at ?? 0),
|
|
2591
|
+
};
|
|
2592
|
+
const decrypted = await this._decryptGroupMessage(message, { skipReplay: true });
|
|
2593
|
+
if (payload?.type === 'e2ee.group_encrypted' && groupId && !decrypted.e2ee) {
|
|
2594
|
+
this._enqueuePendingDecrypt(groupId, message);
|
|
2595
|
+
continue;
|
|
2596
|
+
}
|
|
2597
|
+
const thought = {
|
|
2598
|
+
thought_id: thoughtId,
|
|
2599
|
+
message_id: thoughtId,
|
|
2600
|
+
payload: decrypted.payload,
|
|
2601
|
+
created_at: item.created_at,
|
|
2602
|
+
e2ee: decrypted.e2ee,
|
|
2603
|
+
};
|
|
2604
|
+
if ('context' in item)
|
|
2605
|
+
thought.context = item.context;
|
|
2606
|
+
thoughts.push(thought);
|
|
2607
|
+
}
|
|
2608
|
+
return { ...result, thoughts };
|
|
2609
|
+
}
|
|
2610
|
+
async _decryptMessageThoughts(result) {
|
|
2611
|
+
if (!result.found) {
|
|
2612
|
+
return { ...result, thoughts: [] };
|
|
2613
|
+
}
|
|
2614
|
+
const items = (Array.isArray(result.thoughts) ? result.thoughts : []).filter(isJsonObject);
|
|
2615
|
+
if (!items.length) {
|
|
2616
|
+
return { ...result, thoughts: [] };
|
|
2617
|
+
}
|
|
2618
|
+
const senderAid = String(result.sender_aid ?? '');
|
|
2619
|
+
const peerAid = String(result.peer_aid ?? '');
|
|
2620
|
+
const thoughts = [];
|
|
2621
|
+
for (const item of items) {
|
|
2622
|
+
const payload = isJsonObject(item.payload) ? item.payload : null;
|
|
2623
|
+
const thoughtId = String(item.thought_id ?? item.message_id ?? '');
|
|
2624
|
+
const fromAid = String(item.from ?? senderAid);
|
|
2625
|
+
const toAid = String(item.to ?? peerAid);
|
|
2626
|
+
const message = {
|
|
2627
|
+
from: fromAid,
|
|
2628
|
+
to: toAid,
|
|
2629
|
+
message_id: thoughtId,
|
|
2630
|
+
payload: payload ?? {},
|
|
2631
|
+
encrypted: item.encrypted !== false,
|
|
2632
|
+
timestamp: Number(item.created_at ?? 0),
|
|
2633
|
+
};
|
|
2634
|
+
let decrypted = message;
|
|
2635
|
+
if (payload?.type === 'e2ee.encrypted') {
|
|
2636
|
+
const senderCertFingerprint = String(payload.sender_cert_fingerprint ?? payload.aad?.sender_cert_fingerprint ?? '').trim().toLowerCase();
|
|
2637
|
+
if (fromAid) {
|
|
2638
|
+
const certReady = await this._ensureSenderCertCached(fromAid, senderCertFingerprint || undefined);
|
|
2639
|
+
if (!certReady) {
|
|
2640
|
+
console.warn('[aun_core] 无法获取发送方证书,跳过 message.thought.get 解密:', fromAid);
|
|
2641
|
+
continue;
|
|
2642
|
+
}
|
|
2643
|
+
}
|
|
2644
|
+
decrypted = await this._e2ee.decryptMessage(message, { skipReplay: true });
|
|
2645
|
+
if (decrypted === null || (isJsonObject(decrypted.payload) && decrypted.payload.type === 'e2ee.encrypted')) {
|
|
2646
|
+
continue;
|
|
2647
|
+
}
|
|
2648
|
+
}
|
|
2649
|
+
const thought = {
|
|
2650
|
+
thought_id: thoughtId,
|
|
2651
|
+
message_id: thoughtId,
|
|
2652
|
+
from: fromAid,
|
|
2653
|
+
to: toAid,
|
|
2654
|
+
payload: decrypted.payload,
|
|
2655
|
+
created_at: item.created_at,
|
|
2656
|
+
e2ee: decrypted.e2ee,
|
|
2657
|
+
};
|
|
2658
|
+
if ('context' in item)
|
|
2659
|
+
thought.context = item.context;
|
|
2660
|
+
thoughts.push(thought);
|
|
2661
|
+
}
|
|
2662
|
+
return { ...result, thoughts };
|
|
2663
|
+
}
|
|
2201
2664
|
/**
|
|
2202
2665
|
* 尝试处理 P2P 传输的群组密钥消息。
|
|
2203
2666
|
*
|
|
@@ -2414,11 +2877,15 @@ export class AUNClient {
|
|
|
2414
2877
|
async _fetchPeerPrekeys(peerAid) {
|
|
2415
2878
|
const cachedList = this._peerPrekeysCache.get(peerAid);
|
|
2416
2879
|
if (cachedList && Date.now() / 1000 < cachedList.expireAt) {
|
|
2417
|
-
|
|
2880
|
+
const normalized = normalizePeerPrekeys(cachedList.items);
|
|
2881
|
+
if (normalized.length > 0)
|
|
2882
|
+
return normalized.map((item) => ({ ...item }));
|
|
2418
2883
|
}
|
|
2419
2884
|
const cached = this._e2ee.getCachedPrekey(peerAid);
|
|
2420
|
-
if (cached !== null
|
|
2421
|
-
|
|
2885
|
+
if (cached !== null) {
|
|
2886
|
+
const normalized = normalizePeerPrekeys([cached]);
|
|
2887
|
+
if (normalized.length > 0)
|
|
2888
|
+
return normalized.map((item) => ({ ...item }));
|
|
2422
2889
|
}
|
|
2423
2890
|
let result;
|
|
2424
2891
|
try {
|
|
@@ -2435,7 +2902,7 @@ export class AUNClient {
|
|
|
2435
2902
|
}
|
|
2436
2903
|
const devicePrekeys = Array.isArray(result.device_prekeys) ? result.device_prekeys : null;
|
|
2437
2904
|
if (devicePrekeys) {
|
|
2438
|
-
const normalized = devicePrekeys
|
|
2905
|
+
const normalized = normalizePeerPrekeys(devicePrekeys);
|
|
2439
2906
|
if (normalized.length > 0) {
|
|
2440
2907
|
this._peerPrekeysCache.set(peerAid, {
|
|
2441
2908
|
items: normalized.map((item) => ({ ...item })),
|
|
@@ -2449,12 +2916,15 @@ export class AUNClient {
|
|
|
2449
2916
|
throw new ValidationError(`invalid prekey response for ${peerAid}`);
|
|
2450
2917
|
}
|
|
2451
2918
|
if (result.prekey) {
|
|
2452
|
-
|
|
2453
|
-
|
|
2454
|
-
|
|
2455
|
-
|
|
2456
|
-
|
|
2457
|
-
|
|
2919
|
+
const normalized = normalizePeerPrekeys([result.prekey]);
|
|
2920
|
+
if (normalized.length > 0) {
|
|
2921
|
+
this._peerPrekeysCache.set(peerAid, {
|
|
2922
|
+
items: normalized.map((item) => ({ ...item })),
|
|
2923
|
+
expireAt: Date.now() / 1000 + 300,
|
|
2924
|
+
});
|
|
2925
|
+
this._e2ee.cachePrekey(peerAid, normalized[0]);
|
|
2926
|
+
return normalized.map((item) => ({ ...item }));
|
|
2927
|
+
}
|
|
2458
2928
|
}
|
|
2459
2929
|
if (result.found) {
|
|
2460
2930
|
throw new ValidationError(`invalid prekey response for ${peerAid}`);
|
|
@@ -2465,7 +2935,9 @@ export class AUNClient {
|
|
|
2465
2935
|
async _fetchPeerPrekey(peerAid) {
|
|
2466
2936
|
const cachedList = this._peerPrekeysCache.get(peerAid);
|
|
2467
2937
|
if (cachedList && Date.now() / 1000 < cachedList.expireAt && cachedList.items.length > 0) {
|
|
2468
|
-
|
|
2938
|
+
const normalized = normalizePeerPrekeys(cachedList.items);
|
|
2939
|
+
if (normalized.length > 0)
|
|
2940
|
+
return { ...normalized[0] };
|
|
2469
2941
|
}
|
|
2470
2942
|
const prekeys = await this._fetchPeerPrekeys(peerAid);
|
|
2471
2943
|
if (prekeys.length === 0) {
|
|
@@ -3292,34 +3764,45 @@ export class AUNClient {
|
|
|
3292
3764
|
// 避免 reader 把积压 push 交给空 tracker 的 handler,触发 S2 历史 gap 误补拉。
|
|
3293
3765
|
this._refreshSeqTrackerContext();
|
|
3294
3766
|
await this._restoreSeqTrackerState();
|
|
3295
|
-
|
|
3296
|
-
|
|
3297
|
-
|
|
3298
|
-
|
|
3299
|
-
|
|
3300
|
-
|
|
3301
|
-
|
|
3302
|
-
|
|
3303
|
-
|
|
3304
|
-
|
|
3305
|
-
|
|
3306
|
-
|
|
3307
|
-
|
|
3308
|
-
|
|
3309
|
-
|
|
3310
|
-
|
|
3311
|
-
this._sessionParams
|
|
3767
|
+
try {
|
|
3768
|
+
const challenge = await this._transport.connect(gatewayUrl);
|
|
3769
|
+
this._state = 'authenticating';
|
|
3770
|
+
if (allowReauth) {
|
|
3771
|
+
const authContext = await this._auth.connectSession(this._transport, challenge, gatewayUrl, {
|
|
3772
|
+
accessToken: params.access_token,
|
|
3773
|
+
deviceId: this._deviceId,
|
|
3774
|
+
slotId: this._slotId,
|
|
3775
|
+
deliveryMode: this._connectDeliveryMode,
|
|
3776
|
+
});
|
|
3777
|
+
if (isJsonObject(authContext)) {
|
|
3778
|
+
const auth = authContext;
|
|
3779
|
+
const identity = auth.identity;
|
|
3780
|
+
if (identity && isJsonObject(identity)) {
|
|
3781
|
+
this._identity = identity;
|
|
3782
|
+
this._aid = String(identity.aid ?? this._aid ?? '');
|
|
3783
|
+
if (this._sessionParams) {
|
|
3784
|
+
this._sessionParams.access_token = String(auth.token ?? params.access_token ?? '');
|
|
3785
|
+
}
|
|
3312
3786
|
}
|
|
3313
3787
|
}
|
|
3314
3788
|
}
|
|
3789
|
+
else {
|
|
3790
|
+
await this._auth.initializeWithToken(this._transport, challenge, String(params.access_token), {
|
|
3791
|
+
deviceId: this._deviceId,
|
|
3792
|
+
slotId: this._slotId,
|
|
3793
|
+
deliveryMode: this._connectDeliveryMode,
|
|
3794
|
+
});
|
|
3795
|
+
await this._syncIdentityAfterConnect(String(params.access_token));
|
|
3796
|
+
}
|
|
3315
3797
|
}
|
|
3316
|
-
|
|
3317
|
-
|
|
3318
|
-
|
|
3319
|
-
|
|
3320
|
-
|
|
3321
|
-
}
|
|
3322
|
-
|
|
3798
|
+
catch (err) {
|
|
3799
|
+
// P1-19: 首连失败时重置状态,避免半连接残留
|
|
3800
|
+
this._state = 'disconnected';
|
|
3801
|
+
try {
|
|
3802
|
+
await this._transport.close();
|
|
3803
|
+
}
|
|
3804
|
+
catch { /* 忽略关闭错误 */ }
|
|
3805
|
+
throw err;
|
|
3323
3806
|
}
|
|
3324
3807
|
this._state = 'connected';
|
|
3325
3808
|
await this._dispatcher.publish('connection.state', {
|
|
@@ -3666,6 +4149,28 @@ export class AUNClient {
|
|
|
3666
4149
|
throw new ValidationError('group.send does not accept delivery_mode; group messages are always fanout');
|
|
3667
4150
|
}
|
|
3668
4151
|
}
|
|
4152
|
+
if (method === 'group.thought.put' || method === 'group.thought.get'
|
|
4153
|
+
|| method === 'message.thought.put' || method === 'message.thought.get') {
|
|
4154
|
+
const context = isJsonObject(params.context) ? params.context : null;
|
|
4155
|
+
const contextType = String(context?.type ?? '').trim();
|
|
4156
|
+
const contextId = String(context?.id ?? '').trim();
|
|
4157
|
+
const hasContext = contextType.length > 0 && contextId.length > 0;
|
|
4158
|
+
if (!hasContext) {
|
|
4159
|
+
throw new ValidationError(`${method} requires context.type + context.id`);
|
|
4160
|
+
}
|
|
4161
|
+
}
|
|
4162
|
+
if (method === 'group.thought.get' && !String(params.sender_aid ?? '').trim()) {
|
|
4163
|
+
throw new ValidationError('group.thought.get requires sender_aid');
|
|
4164
|
+
}
|
|
4165
|
+
if (method === 'message.thought.put') {
|
|
4166
|
+
this._validateMessageRecipient(params.to);
|
|
4167
|
+
if (!String(params.to ?? '').trim()) {
|
|
4168
|
+
throw new ValidationError('message.thought.put requires to');
|
|
4169
|
+
}
|
|
4170
|
+
}
|
|
4171
|
+
if (method === 'message.thought.get' && !String(params.sender_aid ?? '').trim()) {
|
|
4172
|
+
throw new ValidationError('message.thought.get requires sender_aid');
|
|
4173
|
+
}
|
|
3669
4174
|
}
|
|
3670
4175
|
_currentMessageDeliveryMode() {
|
|
3671
4176
|
return { ...this._connectDeliveryMode };
|
|
@@ -3959,6 +4464,8 @@ export class AUNClient {
|
|
|
3959
4464
|
this._seqTrackerContext = null;
|
|
3960
4465
|
this._gapFillDone.clear();
|
|
3961
4466
|
this._pushedSeqs.clear();
|
|
4467
|
+
this._pendingOrderedMsgs.clear();
|
|
4468
|
+
this._pendingDecryptMsgs.clear();
|
|
3962
4469
|
this._groupSynced.clear();
|
|
3963
4470
|
this._p2pSynced = false;
|
|
3964
4471
|
}
|
|
@@ -3969,6 +4476,8 @@ export class AUNClient {
|
|
|
3969
4476
|
this._seqTracker = new SeqTracker();
|
|
3970
4477
|
this._gapFillDone.clear();
|
|
3971
4478
|
this._pushedSeqs.clear();
|
|
4479
|
+
this._pendingOrderedMsgs.clear();
|
|
4480
|
+
this._pendingDecryptMsgs.clear();
|
|
3972
4481
|
this._groupSynced.clear();
|
|
3973
4482
|
this._p2pSynced = false;
|
|
3974
4483
|
this._seqTrackerContext = nextContext;
|