@agentunion/fastaun-browser 0.3.3 → 0.3.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +113 -85
- package/_packed_docs/CHANGELOG.md +113 -85
- package/_packed_docs/INDEX.md +81 -0
- package/_packed_docs/KITE_DOCS_GUIDE.md +55 -0
- package/_packed_docs/agent.md//350/277/234/347/250/213agent.md/347/274/223/345/255/230/344/270/216etag/351/200/217/344/274/240/346/226/271/346/241/210.md +328 -0
- package/_packed_docs/cli/AUN-CLI/350/256/276/350/256/241/346/226/207/346/241/243.md +686 -0
- package/_packed_docs/design/E2EE_V2/347/256/200/345/214/226/344/270/2721DH/345/212/240Per-AID_Wrap/346/226/271/346/241/210.md +124 -0
- package/_packed_docs/design//350/267/250/350/257/255/350/250/200/345/256/271/345/231/250E2E/346/265/213/350/257/225/346/226/271/346/241/210.md +665 -0
- package/_packed_docs/protocol//351/231/204/345/275/225N-/345/210/206/345/270/203/345/274/217Trace/345/215/217/350/256/256.md +257 -0
- package/_packed_docs/sdk/01-/345/277/253/351/200/237/345/274/200/345/247/213.md +5 -5
- package/_packed_docs/sdk/02-WebSocket/345/215/217/350/256/256.md +1 -1
- package/_packed_docs/sdk/03-/346/240/270/345/277/203/346/246/202/345/277/265.md +2 -2
- package/_packed_docs/sdk/04-/350/277/236/346/216/245/344/270/216/350/256/244/350/257/201.md +46 -6
- package/_packed_docs/sdk/06-API/346/211/213/345/206/214.md +89 -12
- package/_packed_docs/sdk/07-/351/224/231/350/257/257/345/244/204/347/220/206.md +19 -1
- package/_packed_docs/sdk/08-/346/234/200/344/275/263/345/256/236/350/267/265.md +20 -5
- package/_packed_docs/sdk/AUN_DOCS_GUIDE.md +8 -8
- package/_packed_docs/sdk/E2EE_V2/346/266/210/346/201/257/351/200/232/344/277/241/346/227/266/345/272/217/345/233/276.md +171 -0
- package/_packed_docs/sdk/INDEX.md +22 -22
- package/_packed_docs/sdk/README.md +3 -3
- package/dist/auth.d.ts +10 -11
- package/dist/auth.d.ts.map +1 -1
- package/dist/auth.js +127 -91
- package/dist/auth.js.map +1 -1
- package/dist/bundle.js +649 -274
- package/dist/client.d.ts +19 -10
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js +238 -111
- package/dist/client.js.map +1 -1
- package/dist/errors.d.ts +4 -0
- package/dist/errors.d.ts.map +1 -1
- package/dist/errors.js +7 -0
- package/dist/errors.js.map +1 -1
- package/dist/index.d.ts +3 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -3
- package/dist/index.js.map +1 -1
- package/dist/keystore/index.d.ts +5 -0
- package/dist/keystore/index.d.ts.map +1 -1
- package/dist/keystore/indexeddb.d.ts +12 -0
- package/dist/keystore/indexeddb.d.ts.map +1 -1
- package/dist/keystore/indexeddb.js +64 -6
- package/dist/keystore/indexeddb.js.map +1 -1
- package/dist/namespaces/auth.d.ts +9 -3
- package/dist/namespaces/auth.d.ts.map +1 -1
- package/dist/namespaces/auth.js +64 -20
- package/dist/namespaces/auth.js.map +1 -1
- package/dist/secret-store/indexeddb-store.js +1 -1
- package/dist/secret-store/indexeddb-store.js.map +1 -1
- package/dist/transport.d.ts +9 -1
- package/dist/transport.d.ts.map +1 -1
- package/dist/transport.js +158 -64
- package/dist/transport.js.map +1 -1
- package/dist/v2/e2ee/decrypt.js +1 -1
- package/dist/v2/e2ee/decrypt.js.map +1 -1
- package/dist/v2/e2ee/encrypt-p2p.d.ts.map +1 -1
- package/dist/v2/e2ee/encrypt-p2p.js +3 -2
- package/dist/v2/e2ee/encrypt-p2p.js.map +1 -1
- package/dist/v2/session/session.d.ts +1 -0
- package/dist/v2/session/session.d.ts.map +1 -1
- package/dist/v2/session/session.js +7 -1
- package/dist/v2/session/session.js.map +1 -1
- package/package.json +43 -43
- package/dist/e2ee-group.d.ts +0 -276
- package/dist/e2ee-group.d.ts.map +0 -1
- package/dist/e2ee-group.js +0 -1653
- package/dist/e2ee-group.js.map +0 -1
package/dist/client.js
CHANGED
|
@@ -270,6 +270,63 @@ function isGroupServiceAid(value) {
|
|
|
270
270
|
const [name, ...issuerParts] = text.split('.');
|
|
271
271
|
return name === 'group' && issuerParts.join('.').length > 0;
|
|
272
272
|
}
|
|
273
|
+
function normalizeV2WrapPolicy(raw) {
|
|
274
|
+
if (!raw || typeof raw !== 'object' || Array.isArray(raw))
|
|
275
|
+
return undefined;
|
|
276
|
+
const obj = raw;
|
|
277
|
+
let protocol = String(obj.protocol ?? '').trim().toUpperCase();
|
|
278
|
+
let scope = String(obj.scope ?? '').trim().toLowerCase();
|
|
279
|
+
if (scope !== 'aid' && scope !== 'device') {
|
|
280
|
+
if (obj.per_aid_wrap === true)
|
|
281
|
+
scope = 'aid';
|
|
282
|
+
else if (obj.per_device_wrap === true)
|
|
283
|
+
scope = 'device';
|
|
284
|
+
else
|
|
285
|
+
scope = '';
|
|
286
|
+
}
|
|
287
|
+
if (protocol !== '1DH' && protocol !== '3DH')
|
|
288
|
+
protocol = '';
|
|
289
|
+
if (scope === 'aid')
|
|
290
|
+
protocol = '1DH';
|
|
291
|
+
if (!protocol && !scope)
|
|
292
|
+
return undefined;
|
|
293
|
+
return {
|
|
294
|
+
protocol: protocol ? protocol : undefined,
|
|
295
|
+
scope: scope ? scope : undefined,
|
|
296
|
+
};
|
|
297
|
+
}
|
|
298
|
+
function v2WrapCapabilities() {
|
|
299
|
+
return {
|
|
300
|
+
version: 'v2.1',
|
|
301
|
+
protocols: ['1DH', '3DH'],
|
|
302
|
+
scopes: ['aid', 'device'],
|
|
303
|
+
per_aid_wrap: true,
|
|
304
|
+
per_device_wrap: true,
|
|
305
|
+
};
|
|
306
|
+
}
|
|
307
|
+
function applyV2WrapPolicyToTargets(targets, policy) {
|
|
308
|
+
if (!policy)
|
|
309
|
+
return targets;
|
|
310
|
+
const normalized = targets.map((target) => {
|
|
311
|
+
const row = { ...target };
|
|
312
|
+
if (policy.protocol === '1DH') {
|
|
313
|
+
row.keySource = 'aid_master';
|
|
314
|
+
row.spkPkDer = undefined;
|
|
315
|
+
row.spkId = '';
|
|
316
|
+
}
|
|
317
|
+
return row;
|
|
318
|
+
});
|
|
319
|
+
if (policy.scope !== 'aid')
|
|
320
|
+
return normalized;
|
|
321
|
+
const collapsed = new Map();
|
|
322
|
+
for (const target of normalized) {
|
|
323
|
+
const key = `${target.aid}\u0000${target.role}`;
|
|
324
|
+
if (!collapsed.has(key)) {
|
|
325
|
+
collapsed.set(key, { ...target, deviceId: '' });
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
return Array.from(collapsed.values());
|
|
329
|
+
}
|
|
273
330
|
/** 32 字节左侧零填充(用于 P-256 私钥 scalar 规范化) */
|
|
274
331
|
function _v2LeftPad32(b) {
|
|
275
332
|
if (b.length === 32)
|
|
@@ -529,7 +586,7 @@ export class AUNClient {
|
|
|
529
586
|
_agentMdPath = '';
|
|
530
587
|
_agentMdCache = new Map();
|
|
531
588
|
_agentMdFetchInflight = new Set();
|
|
532
|
-
|
|
589
|
+
_agentMdLock = Promise.resolve();
|
|
533
590
|
/** 消息序列号跟踪器(群消息 + P2P 空洞检测) */
|
|
534
591
|
_seqTracker = new SeqTracker();
|
|
535
592
|
_seqTrackerContext = null;
|
|
@@ -543,6 +600,9 @@ export class AUNClient {
|
|
|
543
600
|
_groupSynced = new Set();
|
|
544
601
|
/** gap fill 来源标记:true 表示当前正在补洞(pull 触发),false 表示非补洞 */
|
|
545
602
|
_gapFillActive = false;
|
|
603
|
+
// Pull Gate:序列化同一 key 的并发 pull 操作,防止重复拉取
|
|
604
|
+
_pullGates = new Map();
|
|
605
|
+
static _PULL_GATE_STALE_MS = 30000;
|
|
546
606
|
// 重连相关
|
|
547
607
|
_reconnectActive = false;
|
|
548
608
|
_reconnectAbort = null;
|
|
@@ -583,7 +643,7 @@ export class AUNClient {
|
|
|
583
643
|
this._clientLog.info(`AUNClient initialized: debug=${_debug} aunPath=${this.configModel.aunPath} aid=${initAid ?? '-'}`);
|
|
584
644
|
this._dispatcher = new EventDispatcher();
|
|
585
645
|
this._discovery = new GatewayDiscovery();
|
|
586
|
-
this._keystore = new IndexedDBKeyStore();
|
|
646
|
+
this._keystore = new IndexedDBKeyStore({ encryptionSeed: this.configModel.seedPassword ?? undefined });
|
|
587
647
|
this._slotId = '';
|
|
588
648
|
this._connectDeliveryMode = normalizeDeliveryModeConfig({ mode: 'fanout' });
|
|
589
649
|
this._defaultConnectDeliveryMode = { ...this._connectDeliveryMode };
|
|
@@ -693,7 +753,7 @@ export class AUNClient {
|
|
|
693
753
|
}
|
|
694
754
|
/**
|
|
695
755
|
* 浏览器版本 publishAgentMd。默认从 {agentMdPath}/{self_aid}/agent.md 的等价 IndexedDB 正文读取,
|
|
696
|
-
* 然后签名、上传,并刷新
|
|
756
|
+
* 然后签名、上传,并刷新 agentmd.json 元数据。
|
|
697
757
|
*
|
|
698
758
|
* 兼容旧浏览器调用:传入 content 时会先写入等价正文,再从该正文发布。
|
|
699
759
|
*/
|
|
@@ -736,7 +796,7 @@ export class AUNClient {
|
|
|
736
796
|
}
|
|
737
797
|
/**
|
|
738
798
|
* 浏览器版本 fetchAgentMd。aid 缺省时取自身;下载后的正文固定写入
|
|
739
|
-
* {agentMdPath}/{aid}/agent.md 的等价 IndexedDB 正文,
|
|
799
|
+
* {agentMdPath}/{aid}/agent.md 的等价 IndexedDB 正文,agentmd.json 只保存元数据。
|
|
740
800
|
*/
|
|
741
801
|
async fetchAgentMd(aid) {
|
|
742
802
|
const target = String(aid ?? this._aid ?? '').trim();
|
|
@@ -809,8 +869,8 @@ export class AUNClient {
|
|
|
809
869
|
}
|
|
810
870
|
return target;
|
|
811
871
|
}
|
|
812
|
-
|
|
813
|
-
return
|
|
872
|
+
_agentMdMetaKey(aid) {
|
|
873
|
+
return `${this._agentMdSafeAid(aid)}/agentmd.json`;
|
|
814
874
|
}
|
|
815
875
|
_agentMdContentKey(aid) {
|
|
816
876
|
return `${this._agentMdSafeAid(aid)}/agent.md`;
|
|
@@ -844,18 +904,11 @@ export class AUNClient {
|
|
|
844
904
|
fetched_at: Date.now(),
|
|
845
905
|
});
|
|
846
906
|
}
|
|
847
|
-
async
|
|
848
|
-
const
|
|
849
|
-
if (typeof list !== 'function') {
|
|
850
|
-
throw new Error('IndexedDB agent.md storage unavailable');
|
|
851
|
-
}
|
|
852
|
-
return await list.call(this._keystore, this._agentMdRoot());
|
|
853
|
-
}
|
|
854
|
-
async _withAgentMdListLock(fn) {
|
|
855
|
-
const previous = this._agentMdListLock.catch(() => undefined);
|
|
907
|
+
async _withAgentMdLock(fn) {
|
|
908
|
+
const previous = this._agentMdLock.catch(() => undefined);
|
|
856
909
|
let release;
|
|
857
910
|
const current = new Promise((resolve) => { release = resolve; });
|
|
858
|
-
this.
|
|
911
|
+
this._agentMdLock = previous.then(() => current);
|
|
859
912
|
await previous;
|
|
860
913
|
try {
|
|
861
914
|
return await fn();
|
|
@@ -864,79 +917,39 @@ export class AUNClient {
|
|
|
864
917
|
release();
|
|
865
918
|
}
|
|
866
919
|
}
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
if (
|
|
873
|
-
|
|
874
|
-
else if (isJsonObject(payload.records))
|
|
875
|
-
iterable = Object.values(payload.records);
|
|
876
|
-
}
|
|
877
|
-
else if (Array.isArray(data)) {
|
|
878
|
-
iterable = data;
|
|
920
|
+
_normalizeAgentMdRecord(aid, data) {
|
|
921
|
+
if (!isJsonObject(data))
|
|
922
|
+
return {};
|
|
923
|
+
const record = {};
|
|
924
|
+
for (const [key, value] of Object.entries(data)) {
|
|
925
|
+
if (key !== 'content')
|
|
926
|
+
record[key] = value;
|
|
879
927
|
}
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
const raw = item;
|
|
884
|
-
const aid = String(raw.aid ?? '').trim();
|
|
885
|
-
if (!aid)
|
|
886
|
-
continue;
|
|
887
|
-
const record = {};
|
|
888
|
-
for (const [key, value] of Object.entries(raw)) {
|
|
889
|
-
if (key !== 'content')
|
|
890
|
-
record[key] = value;
|
|
891
|
-
}
|
|
892
|
-
record.aid = aid;
|
|
893
|
-
for (const key of ['fetched_at', 'observed_at', 'checked_at', 'updated_at']) {
|
|
894
|
-
record[key] = Number(record[key] ?? 0) || 0;
|
|
895
|
-
}
|
|
896
|
-
records[aid] = record;
|
|
928
|
+
record.aid = this._agentMdSafeAid(String(record.aid ?? aid));
|
|
929
|
+
for (const key of ['fetched_at', 'observed_at', 'checked_at', 'updated_at']) {
|
|
930
|
+
record[key] = Number(record[key] ?? 0) || 0;
|
|
897
931
|
}
|
|
898
|
-
return
|
|
899
|
-
}
|
|
900
|
-
async _writeAgentMdListUnlocked(records) {
|
|
901
|
-
const sorted = {};
|
|
902
|
-
for (const aid of Object.keys(records).sort())
|
|
903
|
-
sorted[aid] = records[aid];
|
|
904
|
-
await this._writeAgentMdStorage(this._agentMdListKey(), `${JSON.stringify({ version: 1, updated_at: Date.now(), records: sorted }, null, 2)}\n`);
|
|
932
|
+
return record;
|
|
905
933
|
}
|
|
906
|
-
async
|
|
907
|
-
const
|
|
908
|
-
const
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
const content = await this._readAgentMdStorage(this._agentMdContentKey(aid));
|
|
912
|
-
if (content === null)
|
|
913
|
-
continue;
|
|
914
|
-
records[aid] = {
|
|
915
|
-
aid,
|
|
916
|
-
local_etag: await this._agentMdContentEtag(content),
|
|
917
|
-
fetched_at: now,
|
|
918
|
-
updated_at: now,
|
|
919
|
-
};
|
|
920
|
-
}
|
|
921
|
-
catch (err) {
|
|
922
|
-
this._clientLog.warn(`agent.md rebuild skipped unreadable file aid=${aid} err=${err instanceof Error ? err.message : String(err)}`);
|
|
923
|
-
}
|
|
934
|
+
async _writeAgentMdRecordUnlocked(aid, record) {
|
|
935
|
+
const payload = {};
|
|
936
|
+
for (const [key, value] of Object.entries(record)) {
|
|
937
|
+
if (key !== 'content' && value !== undefined && value !== null)
|
|
938
|
+
payload[key] = value;
|
|
924
939
|
}
|
|
925
|
-
|
|
926
|
-
this.
|
|
927
|
-
return records;
|
|
940
|
+
payload.aid = this._agentMdSafeAid(aid);
|
|
941
|
+
await this._writeAgentMdStorage(this._agentMdMetaKey(aid), `${JSON.stringify(payload, null, 2)}\n`);
|
|
928
942
|
}
|
|
929
|
-
async
|
|
930
|
-
const raw = await this._readAgentMdStorage(this.
|
|
931
|
-
if (raw === null)
|
|
932
|
-
return
|
|
933
|
-
}
|
|
943
|
+
async _readAgentMdRecordUnlocked(aid) {
|
|
944
|
+
const raw = await this._readAgentMdStorage(this._agentMdMetaKey(aid));
|
|
945
|
+
if (raw === null)
|
|
946
|
+
return {};
|
|
934
947
|
try {
|
|
935
|
-
return this.
|
|
948
|
+
return this._normalizeAgentMdRecord(aid, JSON.parse(raw));
|
|
936
949
|
}
|
|
937
950
|
catch (err) {
|
|
938
|
-
this._clientLog.warn(`agent.md
|
|
939
|
-
return
|
|
951
|
+
this._clientLog.warn(`agent.md metadata damaged, ignoring: aid=${aid} err=${err instanceof Error ? err.message : String(err)}`);
|
|
952
|
+
return {};
|
|
940
953
|
}
|
|
941
954
|
}
|
|
942
955
|
async _readAgentMdContent(aid) {
|
|
@@ -960,21 +973,32 @@ export class AUNClient {
|
|
|
960
973
|
if (!target)
|
|
961
974
|
return null;
|
|
962
975
|
try {
|
|
963
|
-
const
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
976
|
+
const loaded = await this._withAgentMdLock(async () => {
|
|
977
|
+
const record = await this._readAgentMdRecordUnlocked(target);
|
|
978
|
+
const next = Object.keys(record).length > 0 ? { ...record, aid: target } : { aid: target };
|
|
979
|
+
try {
|
|
980
|
+
const content = await this._readAgentMdContent(target);
|
|
981
|
+
if (content !== null) {
|
|
982
|
+
next.content = content;
|
|
983
|
+
next.local_etag = await this._agentMdContentEtag(content);
|
|
984
|
+
}
|
|
985
|
+
else {
|
|
986
|
+
// 元数据存在但正文缺失
|
|
987
|
+
const metaRaw = await this._readAgentMdStorage(this._agentMdMetaKey(target));
|
|
988
|
+
if (metaRaw !== null) {
|
|
989
|
+
this._clientLog.warn(`agent.md content read failed: aid=${target}`);
|
|
990
|
+
}
|
|
991
|
+
}
|
|
971
992
|
}
|
|
972
|
-
|
|
973
|
-
this._clientLog.warn(`agent.md content read failed: aid=${target}`);
|
|
993
|
+
catch (err) {
|
|
994
|
+
this._clientLog.warn(`agent.md content read failed: aid=${target} err=${err instanceof Error ? err.message : String(err)}`);
|
|
974
995
|
}
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
996
|
+
return next;
|
|
997
|
+
});
|
|
998
|
+
if (Object.keys(loaded).length <= 1)
|
|
999
|
+
return null;
|
|
1000
|
+
this._agentMdCache.set(target, { ...loaded });
|
|
1001
|
+
return { ...loaded };
|
|
978
1002
|
}
|
|
979
1003
|
catch (err) {
|
|
980
1004
|
this._clientLog.debug(`agent.md cache load skipped: aid=${target} err=${err instanceof Error ? err.message : String(err)}`);
|
|
@@ -997,16 +1021,14 @@ export class AUNClient {
|
|
|
997
1021
|
inputFields.fetched_at = Date.now();
|
|
998
1022
|
}
|
|
999
1023
|
delete inputFields.content;
|
|
1000
|
-
const record = await this.
|
|
1001
|
-
const
|
|
1002
|
-
const next = { ...(records[target] ?? {}), aid: target };
|
|
1024
|
+
const record = await this._withAgentMdLock(async () => {
|
|
1025
|
+
const next = { ...(await this._readAgentMdRecordUnlocked(target)), aid: target };
|
|
1003
1026
|
for (const [key, value] of Object.entries(inputFields)) {
|
|
1004
1027
|
if (value !== undefined && value !== null)
|
|
1005
1028
|
next[key] = value;
|
|
1006
1029
|
}
|
|
1007
1030
|
next.updated_at = Date.now();
|
|
1008
|
-
|
|
1009
|
-
await this._writeAgentMdListUnlocked(records);
|
|
1031
|
+
await this._writeAgentMdRecordUnlocked(target, next);
|
|
1010
1032
|
return next;
|
|
1011
1033
|
});
|
|
1012
1034
|
const loaded = { ...record };
|
|
@@ -1494,6 +1516,20 @@ export class AUNClient {
|
|
|
1494
1516
|
return this._putMessageThoughtEncryptedV2(p);
|
|
1495
1517
|
}
|
|
1496
1518
|
}
|
|
1519
|
+
// Pull Gate:序列化同一 key 的 pull 操作,防止并发重复拉取
|
|
1520
|
+
const pullGateKey = this._pullGateKeyForCall(method, p);
|
|
1521
|
+
if (pullGateKey) {
|
|
1522
|
+
return await this._runPullSerialized(pullGateKey, async () => {
|
|
1523
|
+
return await this._callImplInner(method, p);
|
|
1524
|
+
});
|
|
1525
|
+
}
|
|
1526
|
+
return await this._callImplInner(method, p);
|
|
1527
|
+
}
|
|
1528
|
+
/**
|
|
1529
|
+
* _callImpl 的内层:pull gate 之后的实际 RPC 分发逻辑。
|
|
1530
|
+
* 拆分出来以便 pull gate 包裹整个操作。
|
|
1531
|
+
*/
|
|
1532
|
+
async _callImplInner(method, p) {
|
|
1497
1533
|
// message.pull:V2 就绪时走 V2 pull
|
|
1498
1534
|
if (method === 'message.pull' && this._v2Session) {
|
|
1499
1535
|
this._clientLog.debug('call route: message.pull → V2 pull');
|
|
@@ -4265,7 +4301,10 @@ export class AUNClient {
|
|
|
4265
4301
|
const session = this._v2Session;
|
|
4266
4302
|
if (session && fromAid) {
|
|
4267
4303
|
try {
|
|
4268
|
-
const bs = await this.call('message.v2.bootstrap', {
|
|
4304
|
+
const bs = await this.call('message.v2.bootstrap', {
|
|
4305
|
+
peer_aid: fromAid,
|
|
4306
|
+
e2ee_wrap_capabilities: v2WrapCapabilities(),
|
|
4307
|
+
});
|
|
4269
4308
|
const peers = (Array.isArray(bs?.peer_devices) ? bs.peer_devices : []);
|
|
4270
4309
|
for (const dev of peers)
|
|
4271
4310
|
this._cacheV2PeerIKFromDevice(dev, fromAid);
|
|
@@ -4275,7 +4314,10 @@ export class AUNClient {
|
|
|
4275
4314
|
}
|
|
4276
4315
|
if (groupId) {
|
|
4277
4316
|
try {
|
|
4278
|
-
const gbs = await this.call('group.v2.bootstrap', {
|
|
4317
|
+
const gbs = await this.call('group.v2.bootstrap', {
|
|
4318
|
+
group_id: groupId,
|
|
4319
|
+
e2ee_wrap_capabilities: v2WrapCapabilities(),
|
|
4320
|
+
});
|
|
4279
4321
|
const devices = (Array.isArray(gbs?.devices) ? gbs.devices : []);
|
|
4280
4322
|
const audit = (Array.isArray(gbs?.audit_recipients) ? gbs.audit_recipients : []);
|
|
4281
4323
|
for (const dev of devices)
|
|
@@ -4559,7 +4601,7 @@ export class AUNClient {
|
|
|
4559
4601
|
// 从 recipients 数组中查找本设备的 row 以获取 key_source
|
|
4560
4602
|
if (!spkId) {
|
|
4561
4603
|
for (const row of envelope.recipients) {
|
|
4562
|
-
if (Array.isArray(row) && row.length >= 6 && row[0] === this._aid && row[1] === this._deviceId) {
|
|
4604
|
+
if (Array.isArray(row) && row.length >= 6 && row[0] === this._aid && (row[1] === this._deviceId || row[1] === '')) {
|
|
4563
4605
|
spkId = String(row[5] ?? '');
|
|
4564
4606
|
recipientKeySource = row.length > 3 ? String(row[3] ?? '') : '';
|
|
4565
4607
|
break;
|
|
@@ -4568,7 +4610,7 @@ export class AUNClient {
|
|
|
4568
4610
|
}
|
|
4569
4611
|
else {
|
|
4570
4612
|
for (const row of envelope.recipients) {
|
|
4571
|
-
if (Array.isArray(row) && row.length >= 6 && row[0] === this._aid && row[1] === this._deviceId) {
|
|
4613
|
+
if (Array.isArray(row) && row.length >= 6 && row[0] === this._aid && (row[1] === this._deviceId || row[1] === '')) {
|
|
4572
4614
|
recipientKeySource = row.length > 3 ? String(row[3] ?? '') : '';
|
|
4573
4615
|
break;
|
|
4574
4616
|
}
|
|
@@ -4932,20 +4974,27 @@ export class AUNClient {
|
|
|
4932
4974
|
const useCache = opts.useCache !== false;
|
|
4933
4975
|
let peerDevices = [];
|
|
4934
4976
|
let auditRaw = [];
|
|
4977
|
+
let wrapPolicy;
|
|
4935
4978
|
const cached = useCache ? this._v2BootstrapCache.get(to) : undefined;
|
|
4936
4979
|
if (cached && (Date.now() - cached.cachedAt) < AUNClient.V2_BOOTSTRAP_TTL_MS) {
|
|
4937
4980
|
peerDevices = cached.devices;
|
|
4938
4981
|
auditRaw = cached.auditRecipients;
|
|
4982
|
+
wrapPolicy = cached.wrapPolicy;
|
|
4939
4983
|
}
|
|
4940
4984
|
else {
|
|
4941
|
-
const bs = await this.call('message.v2.bootstrap', {
|
|
4985
|
+
const bs = await this.call('message.v2.bootstrap', {
|
|
4986
|
+
peer_aid: to,
|
|
4987
|
+
e2ee_wrap_capabilities: v2WrapCapabilities(),
|
|
4988
|
+
});
|
|
4942
4989
|
peerDevices = (Array.isArray(bs?.peer_devices) ? bs.peer_devices : []);
|
|
4943
4990
|
auditRaw = (Array.isArray(bs?.audit_recipients) ? bs.audit_recipients : []);
|
|
4991
|
+
wrapPolicy = normalizeV2WrapPolicy(bs?.e2ee_wrap_policy);
|
|
4944
4992
|
if (peerDevices.length > 0) {
|
|
4945
4993
|
this._v2BootstrapCache.set(to, {
|
|
4946
4994
|
devices: peerDevices,
|
|
4947
4995
|
auditRecipients: auditRaw,
|
|
4948
4996
|
cachedAt: Date.now(),
|
|
4997
|
+
wrapPolicy,
|
|
4949
4998
|
});
|
|
4950
4999
|
}
|
|
4951
5000
|
}
|
|
@@ -4985,7 +5034,10 @@ export class AUNClient {
|
|
|
4985
5034
|
selfDevices = selfCached.devices;
|
|
4986
5035
|
}
|
|
4987
5036
|
else {
|
|
4988
|
-
const selfBs = await this.call('message.v2.bootstrap', {
|
|
5037
|
+
const selfBs = await this.call('message.v2.bootstrap', {
|
|
5038
|
+
peer_aid: this._aid,
|
|
5039
|
+
e2ee_wrap_capabilities: v2WrapCapabilities(),
|
|
5040
|
+
});
|
|
4989
5041
|
selfDevices = (Array.isArray(selfBs?.peer_devices) ? selfBs.peer_devices : []);
|
|
4990
5042
|
if (selfDevices.length > 0) {
|
|
4991
5043
|
this._v2BootstrapCache.set(this._aid, {
|
|
@@ -5015,7 +5067,8 @@ export class AUNClient {
|
|
|
5015
5067
|
}
|
|
5016
5068
|
}
|
|
5017
5069
|
const sender = await session.getSenderIdentity();
|
|
5018
|
-
const
|
|
5070
|
+
const sendTargets = applyV2WrapPolicyToTargets(targets, wrapPolicy);
|
|
5071
|
+
const envelope = await encryptP2PMessage(sender, { targets: sendTargets, auditRecipients: [] }, opts.payload, { messageId: opts.messageId, timestamp: opts.timestamp, protectedHeaders: opts.protectedHeaders, context: opts.context });
|
|
5019
5072
|
return envelope;
|
|
5020
5073
|
}
|
|
5021
5074
|
/**
|
|
@@ -5089,18 +5142,24 @@ export class AUNClient {
|
|
|
5089
5142
|
let epoch = 0;
|
|
5090
5143
|
let stateCommitment = { state_version: 0, state_hash: '', state_chain: '' };
|
|
5091
5144
|
let auditRecipientsRaw = [];
|
|
5145
|
+
let wrapPolicy;
|
|
5092
5146
|
const cached = useCache ? this._v2BootstrapCache.get(cacheKey) : undefined;
|
|
5093
5147
|
if (cached && (Date.now() - cached.cachedAt) < AUNClient.V2_BOOTSTRAP_TTL_MS) {
|
|
5094
5148
|
allDevices = cached.devices;
|
|
5095
5149
|
epoch = cached.epoch ?? 0;
|
|
5096
5150
|
stateCommitment = cached.stateCommitment ?? { state_version: 0, state_hash: '', state_chain: '' };
|
|
5097
5151
|
auditRecipientsRaw = cached.auditRecipients;
|
|
5152
|
+
wrapPolicy = cached.wrapPolicy;
|
|
5098
5153
|
}
|
|
5099
5154
|
else {
|
|
5100
|
-
const bs = await this.call('group.v2.bootstrap', {
|
|
5155
|
+
const bs = await this.call('group.v2.bootstrap', {
|
|
5156
|
+
group_id: groupId,
|
|
5157
|
+
e2ee_wrap_capabilities: v2WrapCapabilities(),
|
|
5158
|
+
});
|
|
5101
5159
|
allDevices = (Array.isArray(bs?.devices) ? bs.devices : []);
|
|
5102
5160
|
epoch = Number(bs?.epoch ?? 0);
|
|
5103
5161
|
auditRecipientsRaw = (Array.isArray(bs?.audit_recipients) ? bs.audit_recipients : []);
|
|
5162
|
+
wrapPolicy = normalizeV2WrapPolicy(bs?.e2ee_wrap_policy);
|
|
5104
5163
|
await this._v2CheckFork(groupId, String(bs?.state_chain ?? ''));
|
|
5105
5164
|
await this._v2VerifyStateSignature(groupId, bs);
|
|
5106
5165
|
await this._publishV2GroupSecurityLevel(groupId, bs);
|
|
@@ -5116,6 +5175,7 @@ export class AUNClient {
|
|
|
5116
5175
|
cachedAt: Date.now(),
|
|
5117
5176
|
epoch,
|
|
5118
5177
|
stateCommitment: stateCommitment,
|
|
5178
|
+
wrapPolicy,
|
|
5119
5179
|
});
|
|
5120
5180
|
}
|
|
5121
5181
|
// lazy sync 触发:发现 pending members 时异步发起提案
|
|
@@ -5159,7 +5219,8 @@ export class AUNClient {
|
|
|
5159
5219
|
throw new E2EEError(`V2 group: no target devices for ${groupId}`);
|
|
5160
5220
|
}
|
|
5161
5221
|
const sender = await session.getSenderIdentity();
|
|
5162
|
-
const
|
|
5222
|
+
const sendTargets = applyV2WrapPolicyToTargets(targets, wrapPolicy);
|
|
5223
|
+
const envelope = await encryptGroupMessage(sender, groupId, epoch, sendTargets, opts.payload, { messageId: opts.messageId, timestamp: opts.timestamp, protectedHeaders: opts.protectedHeaders, context: opts.context }, stateCommitment);
|
|
5163
5224
|
return envelope;
|
|
5164
5225
|
}
|
|
5165
5226
|
async _publishV2GroupSecurityLevel(groupId, bootstrap) {
|
|
@@ -5242,7 +5303,7 @@ export class AUNClient {
|
|
|
5242
5303
|
if (Array.isArray(recipients)) {
|
|
5243
5304
|
for (const row of recipients) {
|
|
5244
5305
|
if (Array.isArray(row) && row.length >= 6) {
|
|
5245
|
-
if (row[0] === this._aid && row[1] === this._deviceId) {
|
|
5306
|
+
if (row[0] === this._aid && (row[1] === this._deviceId || row[1] === '')) {
|
|
5246
5307
|
spkId = String(row[5] ?? '');
|
|
5247
5308
|
recipientKeySource = row.length > 3 ? String(row[3] ?? '') : '';
|
|
5248
5309
|
break;
|
|
@@ -5554,7 +5615,10 @@ export class AUNClient {
|
|
|
5554
5615
|
}
|
|
5555
5616
|
if (myRole !== 'owner' && myRole !== 'admin')
|
|
5556
5617
|
return false;
|
|
5557
|
-
const bootstrapResp = await this.call('group.v2.bootstrap', {
|
|
5618
|
+
const bootstrapResp = await this.call('group.v2.bootstrap', {
|
|
5619
|
+
group_id: groupId,
|
|
5620
|
+
e2ee_wrap_capabilities: v2WrapCapabilities(),
|
|
5621
|
+
});
|
|
5558
5622
|
const devices = (Array.isArray(bootstrapResp?.devices) ? bootstrapResp.devices : []);
|
|
5559
5623
|
const candidates = [];
|
|
5560
5624
|
for (const dev of devices) {
|
|
@@ -5656,7 +5720,10 @@ export class AUNClient {
|
|
|
5656
5720
|
}
|
|
5657
5721
|
}
|
|
5658
5722
|
// 获取群所有成员的设备列表(V2 bootstrap)
|
|
5659
|
-
const bootstrapResp = await this.call('group.v2.bootstrap', {
|
|
5723
|
+
const bootstrapResp = await this.call('group.v2.bootstrap', {
|
|
5724
|
+
group_id: groupId,
|
|
5725
|
+
e2ee_wrap_capabilities: v2WrapCapabilities(),
|
|
5726
|
+
});
|
|
5660
5727
|
const allDevices = (Array.isArray(bootstrapResp?.devices) ? bootstrapResp.devices : []);
|
|
5661
5728
|
const auditRecipients = (Array.isArray(bootstrapResp?.audit_recipients) ? bootstrapResp.audit_recipients : []);
|
|
5662
5729
|
const auditAidsList = [...new Set(auditRecipients.map(r => String(r.aid ?? '').trim()).filter(Boolean))].sort();
|
|
@@ -6000,6 +6067,66 @@ export class AUNClient {
|
|
|
6000
6067
|
this._clientLog.warn(`background task exception:${String(exc)}`);
|
|
6001
6068
|
});
|
|
6002
6069
|
}
|
|
6070
|
+
// ── Pull Gate(序列化同一 key 的并发 pull)──────────────────
|
|
6071
|
+
_pullGateKeyForCall(method, params) {
|
|
6072
|
+
if (method === 'message.pull' || method === 'message.v2.pull') {
|
|
6073
|
+
return this._aid ? `p2p:${this._aid}` : '';
|
|
6074
|
+
}
|
|
6075
|
+
if (method === 'group.pull' || method === 'group.v2.pull') {
|
|
6076
|
+
const gid = String(params.group_id ?? '').trim();
|
|
6077
|
+
return gid ? `group:${gid}` : '';
|
|
6078
|
+
}
|
|
6079
|
+
if (method === 'group.pull_events') {
|
|
6080
|
+
const gid = String(params.group_id ?? '').trim();
|
|
6081
|
+
return gid ? `group_event:${gid}` : '';
|
|
6082
|
+
}
|
|
6083
|
+
return '';
|
|
6084
|
+
}
|
|
6085
|
+
_tryAcquirePullGate(key) {
|
|
6086
|
+
if (!key)
|
|
6087
|
+
return 0;
|
|
6088
|
+
const now = Date.now();
|
|
6089
|
+
const gate = this._pullGates.get(key) ?? { inflight: false, startedAt: 0, token: 0 };
|
|
6090
|
+
if (gate.inflight && now - gate.startedAt <= AUNClient._PULL_GATE_STALE_MS) {
|
|
6091
|
+
return null;
|
|
6092
|
+
}
|
|
6093
|
+
if (gate.inflight) {
|
|
6094
|
+
this._clientLog.warn(`pull in-flight stale reset: key=${key} age=${now - gate.startedAt}ms`);
|
|
6095
|
+
}
|
|
6096
|
+
gate.token += 1;
|
|
6097
|
+
gate.inflight = true;
|
|
6098
|
+
gate.startedAt = now;
|
|
6099
|
+
this._pullGates.set(key, gate);
|
|
6100
|
+
return gate.token;
|
|
6101
|
+
}
|
|
6102
|
+
_releasePullGate(key, token) {
|
|
6103
|
+
if (!key || token == null)
|
|
6104
|
+
return;
|
|
6105
|
+
const gate = this._pullGates.get(key);
|
|
6106
|
+
if (!gate || gate.token !== token)
|
|
6107
|
+
return;
|
|
6108
|
+
gate.inflight = false;
|
|
6109
|
+
gate.startedAt = 0;
|
|
6110
|
+
}
|
|
6111
|
+
async _runPullSerialized(key, operation) {
|
|
6112
|
+
let token = this._tryAcquirePullGate(key);
|
|
6113
|
+
if (token === null) {
|
|
6114
|
+
const deadline = Date.now() + AUNClient._PULL_GATE_STALE_MS + 100;
|
|
6115
|
+
while (token === null && Date.now() <= deadline) {
|
|
6116
|
+
await this._sleep(25);
|
|
6117
|
+
token = this._tryAcquirePullGate(key);
|
|
6118
|
+
}
|
|
6119
|
+
if (token === null) {
|
|
6120
|
+
throw new StateError(`pull already in-flight for ${key}`);
|
|
6121
|
+
}
|
|
6122
|
+
}
|
|
6123
|
+
try {
|
|
6124
|
+
return await operation();
|
|
6125
|
+
}
|
|
6126
|
+
finally {
|
|
6127
|
+
this._releasePullGate(key, token);
|
|
6128
|
+
}
|
|
6129
|
+
}
|
|
6003
6130
|
/** 可取消的 sleep */
|
|
6004
6131
|
_sleep(ms) {
|
|
6005
6132
|
return new Promise((resolve) => {
|