@agentunion/fastaun-browser 0.2.17 → 0.2.19
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 +12 -0
- package/dist/auth.d.ts.map +1 -1
- package/dist/auth.js +370 -215
- package/dist/auth.js.map +1 -1
- package/dist/client.d.ts +24 -1
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js +1307 -849
- package/dist/client.js.map +1 -1
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +3 -1
- package/dist/config.js.map +1 -1
- package/dist/discovery.d.ts +3 -0
- package/dist/discovery.d.ts.map +1 -1
- package/dist/discovery.js +15 -1
- package/dist/discovery.js.map +1 -1
- package/dist/e2ee-group.d.ts +4 -0
- package/dist/e2ee-group.d.ts.map +1 -1
- package/dist/e2ee-group.js +327 -201
- package/dist/e2ee-group.js.map +1 -1
- package/dist/e2ee.d.ts +4 -0
- package/dist/e2ee.d.ts.map +1 -1
- package/dist/e2ee.js +196 -117
- package/dist/e2ee.js.map +1 -1
- package/dist/events.d.ts +3 -0
- package/dist/events.d.ts.map +1 -1
- package/dist/events.js +4 -1
- package/dist/events.js.map +1 -1
- package/dist/keystore/index.d.ts +11 -0
- package/dist/keystore/index.d.ts.map +1 -1
- package/dist/keystore/indexeddb.d.ts +38 -0
- package/dist/keystore/indexeddb.d.ts.map +1 -1
- package/dist/keystore/indexeddb.js +245 -98
- package/dist/keystore/indexeddb.js.map +1 -1
- package/dist/logger.d.ts +37 -0
- package/dist/logger.d.ts.map +1 -0
- package/dist/logger.js +112 -0
- package/dist/logger.js.map +1 -0
- package/dist/namespaces/auth.d.ts +13 -3
- package/dist/namespaces/auth.d.ts.map +1 -1
- package/dist/namespaces/auth.js +284 -106
- package/dist/namespaces/auth.js.map +1 -1
- package/dist/namespaces/custody.d.ts +3 -0
- package/dist/namespaces/custody.d.ts.map +1 -1
- package/dist/namespaces/custody.js +147 -75
- package/dist/namespaces/custody.js.map +1 -1
- package/dist/namespaces/meta.d.ts +3 -0
- package/dist/namespaces/meta.d.ts.map +1 -1
- package/dist/namespaces/meta.js +94 -43
- package/dist/namespaces/meta.js.map +1 -1
- package/dist/secret-store/indexeddb-store.d.ts +3 -0
- package/dist/secret-store/indexeddb-store.d.ts.map +1 -1
- package/dist/secret-store/indexeddb-store.js +57 -29
- package/dist/secret-store/indexeddb-store.js.map +1 -1
- package/dist/transport.d.ts +3 -0
- package/dist/transport.d.ts.map +1 -1
- package/dist/transport.js +74 -4
- package/dist/transport.js.map +1 -1
- package/package.json +37 -37
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
// ── IndexedDB KeyStore 实现 ──────────────────────────────
|
|
2
2
|
import { pemToArrayBuffer } from '../crypto.js';
|
|
3
3
|
import { normalizeInstanceId } from '../config.js';
|
|
4
|
+
const _noopLog = { error: () => { }, warn: () => { }, info: () => { }, debug: () => { } };
|
|
4
5
|
import { isJsonObject, } from '../types.js';
|
|
5
6
|
/** AID 安全化(替换路径分隔符) */
|
|
6
7
|
function safeAid(aid) {
|
|
@@ -393,6 +394,8 @@ async function idbDelete(storeName, key) {
|
|
|
393
394
|
* - 若检测到旧版本把结构化数据写进了 metadata,会自动迁移到结构化 store。
|
|
394
395
|
*/
|
|
395
396
|
export class IndexedDBKeyStore {
|
|
397
|
+
_log = _noopLog;
|
|
398
|
+
setLogger(log) { this._log = log; }
|
|
396
399
|
static _aidTails = new Map();
|
|
397
400
|
/** 私钥加密种子;为空时降级为明文存储(向后兼容) */
|
|
398
401
|
_encryptionSeed;
|
|
@@ -420,29 +423,39 @@ export class IndexedDBKeyStore {
|
|
|
420
423
|
}
|
|
421
424
|
}
|
|
422
425
|
async listIdentities() {
|
|
423
|
-
const
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
const
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
const
|
|
442
|
-
|
|
443
|
-
|
|
426
|
+
const tStart = Date.now();
|
|
427
|
+
this._log.debug('listIdentities enter');
|
|
428
|
+
try {
|
|
429
|
+
const records = await idbGetAll(STORE_METADATA);
|
|
430
|
+
const aids = new Set();
|
|
431
|
+
for (const item of records) {
|
|
432
|
+
if (item.key.startsWith('_seq_|'))
|
|
433
|
+
continue;
|
|
434
|
+
const value = item.value;
|
|
435
|
+
const aid = typeof value === 'object' && value !== null && isRecord(value) && typeof value.aid === 'string' && value.aid
|
|
436
|
+
? value.aid
|
|
437
|
+
: item.key;
|
|
438
|
+
aids.add(String(aid));
|
|
439
|
+
}
|
|
440
|
+
for (const item of await idbGetAll(STORE_KEY_PAIRS)) {
|
|
441
|
+
if (!item.key.startsWith('_seq_|'))
|
|
442
|
+
aids.add(item.key);
|
|
443
|
+
}
|
|
444
|
+
for (const item of await idbGetAll(STORE_CERTS)) {
|
|
445
|
+
if (item.key.startsWith('_seq_|'))
|
|
446
|
+
continue;
|
|
447
|
+
const [safe] = item.key.split('|', 1);
|
|
448
|
+
if (safe)
|
|
449
|
+
aids.add(safe);
|
|
450
|
+
}
|
|
451
|
+
const result = [...aids].sort();
|
|
452
|
+
this._log.debug(`listIdentities exit: elapsed=${Date.now() - tStart}ms count=${result.length}`);
|
|
453
|
+
return result;
|
|
454
|
+
}
|
|
455
|
+
catch (err) {
|
|
456
|
+
this._log.debug(`listIdentities exit (error): elapsed=${Date.now() - tStart}ms err=${err instanceof Error ? err.message : String(err)}`);
|
|
457
|
+
throw err;
|
|
444
458
|
}
|
|
445
|
-
return [...aids].sort();
|
|
446
459
|
}
|
|
447
460
|
// ── 密钥对 ──────────────────────────────────────────
|
|
448
461
|
async loadKeyPair(aid) {
|
|
@@ -459,7 +472,7 @@ export class IndexedDBKeyStore {
|
|
|
459
472
|
delete result._encrypted_pk;
|
|
460
473
|
}
|
|
461
474
|
catch {
|
|
462
|
-
|
|
475
|
+
this._log.error(`[keystore] decrypt ${aid} private keyfailed, maybe encryptionSeed mismatch`);
|
|
463
476
|
}
|
|
464
477
|
}
|
|
465
478
|
else if (
|
|
@@ -485,30 +498,54 @@ export class IndexedDBKeyStore {
|
|
|
485
498
|
}
|
|
486
499
|
// ── 证书 ──────────────────────────────────────────
|
|
487
500
|
async loadCert(aid, certFingerprint) {
|
|
488
|
-
const
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
501
|
+
const tStart = Date.now();
|
|
502
|
+
this._log.debug(`loadCert enter: aid=${aid} fingerprint=${certFingerprint ?? '<none>'}`);
|
|
503
|
+
try {
|
|
504
|
+
const normalized = normalizeCertFingerprint(certFingerprint);
|
|
505
|
+
if (normalized) {
|
|
506
|
+
const versioned = await idbGet(STORE_CERTS, certStoreKey(aid, normalized));
|
|
507
|
+
if (typeof versioned === 'string') {
|
|
508
|
+
this._log.debug(`loadCert exit: elapsed=${Date.now() - tStart}ms aid=${aid} found=true source=versioned`);
|
|
509
|
+
return versioned;
|
|
510
|
+
}
|
|
511
|
+
const active = await idbGet(STORE_CERTS, certStoreKey(aid));
|
|
512
|
+
if (typeof active === 'string' && await fingerprintFromCertPem(active) === normalized) {
|
|
513
|
+
this._log.debug(`loadCert exit: elapsed=${Date.now() - tStart}ms aid=${aid} found=true source=active`);
|
|
514
|
+
return active;
|
|
515
|
+
}
|
|
516
|
+
this._log.debug(`loadCert exit: elapsed=${Date.now() - tStart}ms aid=${aid} found=false`);
|
|
517
|
+
return null;
|
|
496
518
|
}
|
|
497
|
-
|
|
519
|
+
const data = await idbGet(STORE_CERTS, certStoreKey(aid));
|
|
520
|
+
const found = typeof data === 'string';
|
|
521
|
+
this._log.debug(`loadCert exit: elapsed=${Date.now() - tStart}ms aid=${aid} found=${found}`);
|
|
522
|
+
return found ? data : null;
|
|
523
|
+
}
|
|
524
|
+
catch (err) {
|
|
525
|
+
this._log.debug(`loadCert exit (error): elapsed=${Date.now() - tStart}ms aid=${aid} err=${err instanceof Error ? err.message : String(err)}`);
|
|
526
|
+
throw err;
|
|
498
527
|
}
|
|
499
|
-
const data = await idbGet(STORE_CERTS, certStoreKey(aid));
|
|
500
|
-
return typeof data === 'string' ? data : null;
|
|
501
528
|
}
|
|
502
529
|
async saveCert(aid, certPem, certFingerprint, opts) {
|
|
503
|
-
const
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
530
|
+
const tStart = Date.now();
|
|
531
|
+
this._log.debug(`saveCert enter: aid=${aid} fingerprint=${certFingerprint ?? '<none>'} make_active=${opts?.makeActive ?? false}`);
|
|
532
|
+
try {
|
|
533
|
+
const normalized = normalizeCertFingerprint(certFingerprint);
|
|
534
|
+
if (normalized) {
|
|
535
|
+
await idbPut(STORE_CERTS, certStoreKey(aid, normalized), certPem);
|
|
536
|
+
if (opts?.makeActive) {
|
|
537
|
+
await idbPut(STORE_CERTS, certStoreKey(aid), certPem);
|
|
538
|
+
}
|
|
539
|
+
this._log.debug(`saveCert exit: elapsed=${Date.now() - tStart}ms aid=${aid}`);
|
|
540
|
+
return;
|
|
508
541
|
}
|
|
509
|
-
|
|
542
|
+
await idbPut(STORE_CERTS, certStoreKey(aid), certPem);
|
|
543
|
+
this._log.debug(`saveCert exit: elapsed=${Date.now() - tStart}ms aid=${aid}`);
|
|
544
|
+
}
|
|
545
|
+
catch (err) {
|
|
546
|
+
this._log.debug(`saveCert exit (error): elapsed=${Date.now() - tStart}ms aid=${aid} err=${err instanceof Error ? err.message : String(err)}`);
|
|
547
|
+
throw err;
|
|
510
548
|
}
|
|
511
|
-
await idbPut(STORE_CERTS, certStoreKey(aid), certPem);
|
|
512
549
|
}
|
|
513
550
|
// ── 实例态 ──────────────────────────────────────────
|
|
514
551
|
async loadInstanceState(aid, deviceId, slotId = '') {
|
|
@@ -534,71 +571,90 @@ export class IndexedDBKeyStore {
|
|
|
534
571
|
}
|
|
535
572
|
// ── 身份信息(组合操作) ──────────────────────────
|
|
536
573
|
async loadIdentity(aid) {
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
if (
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
574
|
+
const tStart = Date.now();
|
|
575
|
+
this._log.debug(`loadIdentity enter: aid=${aid}`);
|
|
576
|
+
try {
|
|
577
|
+
const result = await this._withAidLock(aid, async () => {
|
|
578
|
+
const [keyPair, cert] = await Promise.all([
|
|
579
|
+
this._loadKeyPairUnlocked(aid),
|
|
580
|
+
this._loadCertUnlocked(aid),
|
|
581
|
+
]);
|
|
582
|
+
// 直接读取 metadata KV(含旧数据迁移)
|
|
583
|
+
const metadataOnly = await this._migrateLegacyStructuredStateUnlocked(aid);
|
|
584
|
+
const hasMeta = Object.keys(metadataOnly).length > 0;
|
|
585
|
+
if (!keyPair && !cert && !hasMeta)
|
|
586
|
+
return null;
|
|
587
|
+
const identity = {};
|
|
588
|
+
if (hasMeta)
|
|
589
|
+
Object.assign(identity, metadataOnly);
|
|
590
|
+
if (keyPair)
|
|
591
|
+
Object.assign(identity, keyPair);
|
|
592
|
+
if (cert) {
|
|
593
|
+
// key/cert 公钥一致性校验:防止 cert 被意外覆盖
|
|
594
|
+
const localPubB64 = keyPair?.public_key_der_b64;
|
|
595
|
+
if (typeof localPubB64 === 'string' && localPubB64) {
|
|
596
|
+
const certSpkiB64 = extractSpkiB64FromCertPem(cert);
|
|
597
|
+
if (certSpkiB64 && certSpkiB64 !== localPubB64) {
|
|
598
|
+
this._log.error(`[keystore] identity ${aid} key public key mismatches cert public key, discard cert`);
|
|
599
|
+
}
|
|
600
|
+
else {
|
|
601
|
+
identity.cert = cert;
|
|
602
|
+
}
|
|
559
603
|
}
|
|
560
604
|
else {
|
|
561
605
|
identity.cert = cert;
|
|
562
606
|
}
|
|
563
607
|
}
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
608
|
+
return identity;
|
|
609
|
+
});
|
|
610
|
+
this._log.debug(`loadIdentity exit: elapsed=${Date.now() - tStart}ms aid=${aid} found=${result !== null}`);
|
|
611
|
+
return result;
|
|
612
|
+
}
|
|
613
|
+
catch (err) {
|
|
614
|
+
this._log.debug(`loadIdentity exit (error): elapsed=${Date.now() - tStart}ms aid=${aid} err=${err instanceof Error ? err.message : String(err)}`);
|
|
615
|
+
throw err;
|
|
616
|
+
}
|
|
570
617
|
}
|
|
571
618
|
async saveIdentity(aid, identity) {
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
}
|
|
581
|
-
const cert = identity.cert;
|
|
582
|
-
if (typeof cert === 'string' && cert) {
|
|
583
|
-
await this._saveCertUnlocked(aid, cert);
|
|
584
|
-
}
|
|
585
|
-
const metadataFields = {};
|
|
586
|
-
for (const [key, value] of Object.entries(identity)) {
|
|
587
|
-
if (!['private_key_pem', 'public_key_der_b64', 'curve', 'cert'].includes(key)) {
|
|
588
|
-
metadataFields[key] = value;
|
|
619
|
+
const tStart = Date.now();
|
|
620
|
+
this._log.debug(`saveIdentity enter: aid=${aid} has_cert=${!!identity.cert}`);
|
|
621
|
+
try {
|
|
622
|
+
await this._withAidLock(aid, async () => {
|
|
623
|
+
const keyPairFields = {};
|
|
624
|
+
for (const key of ['private_key_pem', 'public_key_der_b64', 'curve']) {
|
|
625
|
+
if (key in identity)
|
|
626
|
+
keyPairFields[key] = identity[key];
|
|
589
627
|
}
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
if (Object.keys(metadataFields).length > 0) {
|
|
593
|
-
await this._replaceStructuredStateUnlocked(aid, metadataFields);
|
|
594
|
-
const plain = stripStructuredFields(metadataFields);
|
|
595
|
-
if (Object.keys(plain).length > 0) {
|
|
596
|
-
const current = await this._migrateLegacyStructuredStateUnlocked(aid);
|
|
597
|
-
Object.assign(current, plain);
|
|
598
|
-
await this._saveMetadataOnlyUnlocked(aid, current);
|
|
628
|
+
if (Object.keys(keyPairFields).length > 0) {
|
|
629
|
+
await this._saveKeyPairUnlocked(aid, keyPairFields);
|
|
599
630
|
}
|
|
600
|
-
|
|
601
|
-
|
|
631
|
+
const cert = identity.cert;
|
|
632
|
+
if (typeof cert === 'string' && cert) {
|
|
633
|
+
await this._saveCertUnlocked(aid, cert);
|
|
634
|
+
}
|
|
635
|
+
const metadataFields = {};
|
|
636
|
+
for (const [key, value] of Object.entries(identity)) {
|
|
637
|
+
if (!['private_key_pem', 'public_key_der_b64', 'curve', 'cert'].includes(key)) {
|
|
638
|
+
metadataFields[key] = value;
|
|
639
|
+
}
|
|
640
|
+
}
|
|
641
|
+
// 增量保存 metadata 字段
|
|
642
|
+
if (Object.keys(metadataFields).length > 0) {
|
|
643
|
+
await this._replaceStructuredStateUnlocked(aid, metadataFields);
|
|
644
|
+
const plain = stripStructuredFields(metadataFields);
|
|
645
|
+
if (Object.keys(plain).length > 0) {
|
|
646
|
+
const current = await this._migrateLegacyStructuredStateUnlocked(aid);
|
|
647
|
+
Object.assign(current, plain);
|
|
648
|
+
await this._saveMetadataOnlyUnlocked(aid, current);
|
|
649
|
+
}
|
|
650
|
+
}
|
|
651
|
+
});
|
|
652
|
+
this._log.debug(`saveIdentity exit: elapsed=${Date.now() - tStart}ms aid=${aid}`);
|
|
653
|
+
}
|
|
654
|
+
catch (err) {
|
|
655
|
+
this._log.debug(`saveIdentity exit (error): elapsed=${Date.now() - tStart}ms aid=${aid} err=${err instanceof Error ? err.message : String(err)}`);
|
|
656
|
+
throw err;
|
|
657
|
+
}
|
|
602
658
|
}
|
|
603
659
|
// ── 结构化 prekeys ───────────────────────────────────
|
|
604
660
|
async loadE2EEPrekeys(aid, deviceId) {
|
|
@@ -607,6 +663,22 @@ export class IndexedDBKeyStore {
|
|
|
607
663
|
return this._loadPrekeysUnlocked(aid, String(deviceId ?? '').trim());
|
|
608
664
|
});
|
|
609
665
|
}
|
|
666
|
+
/**
|
|
667
|
+
* 按 prekey_id 单点查询本地 prekey(O(log N) IndexedDB primary-key 查找)。
|
|
668
|
+
*
|
|
669
|
+
* 解密入站消息时,envelope 里只带 prekey_id,没有 device_id。原先要走
|
|
670
|
+
* `loadE2EEPrekeys` 全量加载(prekey 池大时一次扫描数百毫秒)。
|
|
671
|
+
* 本方法优先用 IndexedDB 主键直查;命中则 O(log N),未命中再回退到前缀扫描,
|
|
672
|
+
* 保证跨 device_id 也能找到(兼容设备级 prekey 场景)。
|
|
673
|
+
*
|
|
674
|
+
* 返回结构与 `_loadPrekeysUnlocked` 单条值一致(已剥离 prekey_id / device_id 字段)。
|
|
675
|
+
*/
|
|
676
|
+
async loadE2EEPrekeyById(aid, prekeyId) {
|
|
677
|
+
return this._withAidLock(aid, async () => {
|
|
678
|
+
await this._migrateLegacyStructuredStateUnlocked(aid);
|
|
679
|
+
return this._loadPrekeyByIdUnlocked(aid, prekeyId);
|
|
680
|
+
});
|
|
681
|
+
}
|
|
610
682
|
async saveE2EEPrekey(aid, prekeyId, prekeyData, deviceId) {
|
|
611
683
|
await this._withAidLock(aid, async () => {
|
|
612
684
|
await this._migrateLegacyStructuredStateUnlocked(aid);
|
|
@@ -788,6 +860,44 @@ export class IndexedDBKeyStore {
|
|
|
788
860
|
await idbPut(STORE_SESSIONS, sessionStoreKey(aid, sessionId), record);
|
|
789
861
|
});
|
|
790
862
|
}
|
|
863
|
+
/**
|
|
864
|
+
* 读取单个 metadata KV(如 gateway_url 等跨进程缓存项)。
|
|
865
|
+
*
|
|
866
|
+
* 不存在时返回空字符串,与 Python keystore 的 get_metadata 语义保持一致。
|
|
867
|
+
*/
|
|
868
|
+
async getMetadata(aid, key) {
|
|
869
|
+
if (!key)
|
|
870
|
+
return '';
|
|
871
|
+
return this._withAidLock(aid, async () => {
|
|
872
|
+
const md = (await this._loadMetadataOnlyUnlocked(aid)) ?? {};
|
|
873
|
+
const value = md[key];
|
|
874
|
+
if (typeof value === 'string')
|
|
875
|
+
return value;
|
|
876
|
+
if (value === null || value === undefined)
|
|
877
|
+
return '';
|
|
878
|
+
return String(value);
|
|
879
|
+
});
|
|
880
|
+
}
|
|
881
|
+
/**
|
|
882
|
+
* 写入单个 metadata KV。空字符串视为删除该字段。
|
|
883
|
+
*
|
|
884
|
+
* 与 _saveMetadataOnlyUnlocked 不同,本方法只更新指定 key,不影响其他字段。
|
|
885
|
+
*/
|
|
886
|
+
async setMetadata(aid, key, value) {
|
|
887
|
+
if (!key)
|
|
888
|
+
return;
|
|
889
|
+
await this._withAidLock(aid, async () => {
|
|
890
|
+
const current = (await this._loadMetadataOnlyUnlocked(aid)) ?? {};
|
|
891
|
+
const next = deepClone(current);
|
|
892
|
+
if (value === '' || value === undefined || value === null) {
|
|
893
|
+
delete next[key];
|
|
894
|
+
}
|
|
895
|
+
else {
|
|
896
|
+
next[key] = value;
|
|
897
|
+
}
|
|
898
|
+
await this._saveMetadataOnlyUnlocked(aid, next);
|
|
899
|
+
});
|
|
900
|
+
}
|
|
791
901
|
// ── 内部辅助 ─────────────────────────────────────────
|
|
792
902
|
async _loadKeyPairUnlocked(aid) {
|
|
793
903
|
const data = await idbGet(STORE_KEY_PAIRS, metadataStoreKey(aid));
|
|
@@ -802,7 +912,7 @@ export class IndexedDBKeyStore {
|
|
|
802
912
|
delete result._encrypted_pk;
|
|
803
913
|
}
|
|
804
914
|
catch {
|
|
805
|
-
|
|
915
|
+
this._log.error(`[keystore] decrypt ${aid} private keyfailed, maybe encryptionSeed mismatch`);
|
|
806
916
|
}
|
|
807
917
|
}
|
|
808
918
|
else if (
|
|
@@ -942,6 +1052,43 @@ export class IndexedDBKeyStore {
|
|
|
942
1052
|
}
|
|
943
1053
|
return result;
|
|
944
1054
|
}
|
|
1055
|
+
/**
|
|
1056
|
+
* 按 prekey_id 单点查找(不锁外部,调用方负责加锁)。
|
|
1057
|
+
*
|
|
1058
|
+
* 实现策略:
|
|
1059
|
+
* 1) 先按"无 device_id"的 canonical key 直查(命中即 O(log N))。
|
|
1060
|
+
* 2) 未命中(设备级 prekey 走的是含 device_id 的复合 key)→ 回退前缀扫描,
|
|
1061
|
+
* 在 prekey_id 字段匹配的第一条记录上返回;这种情况下扫描代价仍存在,但
|
|
1062
|
+
* 只在边缘 case 触发。
|
|
1063
|
+
*
|
|
1064
|
+
* 返回结构与 `_loadPrekeysUnlocked` 单条值保持一致:剥离 prekey_id / device_id。
|
|
1065
|
+
*/
|
|
1066
|
+
async _loadPrekeyByIdUnlocked(aid, prekeyId) {
|
|
1067
|
+
if (!prekeyId)
|
|
1068
|
+
return null;
|
|
1069
|
+
// 优先:deviceId='' 形态的 canonical key(generatePrekey 默认走这条路径)。
|
|
1070
|
+
const directKey = prekeyStoreKey(aid, prekeyId, '');
|
|
1071
|
+
const direct = await idbGet(STORE_PREKEYS, directKey);
|
|
1072
|
+
if (isRecord(direct)) {
|
|
1073
|
+
const record = deepClone(direct);
|
|
1074
|
+
delete record.prekey_id;
|
|
1075
|
+
delete record.device_id;
|
|
1076
|
+
return record;
|
|
1077
|
+
}
|
|
1078
|
+
// 回退:设备级 prekey(key 形如 aid|device|prekey_id)→ 前缀扫描匹配 prekey_id 字段。
|
|
1079
|
+
for (const item of await idbGetAllByPrefix(STORE_PREKEYS, prekeyPrefix(aid))) {
|
|
1080
|
+
if (!isRecord(item.value))
|
|
1081
|
+
continue;
|
|
1082
|
+
const itemPrekeyId = typeof item.value.prekey_id === 'string' ? item.value.prekey_id : '';
|
|
1083
|
+
if (itemPrekeyId !== prekeyId)
|
|
1084
|
+
continue;
|
|
1085
|
+
const record = deepClone(item.value);
|
|
1086
|
+
delete record.prekey_id;
|
|
1087
|
+
delete record.device_id;
|
|
1088
|
+
return record;
|
|
1089
|
+
}
|
|
1090
|
+
return null;
|
|
1091
|
+
}
|
|
945
1092
|
async _replacePrekeysUnlocked(aid, prekeys, deviceId = '') {
|
|
946
1093
|
const desired = new Set(Object.keys(prekeys));
|
|
947
1094
|
const normalizedDeviceId = String(deviceId ?? '').trim();
|