@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.
Files changed (58) hide show
  1. package/dist/auth.d.ts +12 -0
  2. package/dist/auth.d.ts.map +1 -1
  3. package/dist/auth.js +370 -215
  4. package/dist/auth.js.map +1 -1
  5. package/dist/client.d.ts +24 -1
  6. package/dist/client.d.ts.map +1 -1
  7. package/dist/client.js +1307 -849
  8. package/dist/client.js.map +1 -1
  9. package/dist/config.d.ts.map +1 -1
  10. package/dist/config.js +3 -1
  11. package/dist/config.js.map +1 -1
  12. package/dist/discovery.d.ts +3 -0
  13. package/dist/discovery.d.ts.map +1 -1
  14. package/dist/discovery.js +15 -1
  15. package/dist/discovery.js.map +1 -1
  16. package/dist/e2ee-group.d.ts +4 -0
  17. package/dist/e2ee-group.d.ts.map +1 -1
  18. package/dist/e2ee-group.js +327 -201
  19. package/dist/e2ee-group.js.map +1 -1
  20. package/dist/e2ee.d.ts +4 -0
  21. package/dist/e2ee.d.ts.map +1 -1
  22. package/dist/e2ee.js +196 -117
  23. package/dist/e2ee.js.map +1 -1
  24. package/dist/events.d.ts +3 -0
  25. package/dist/events.d.ts.map +1 -1
  26. package/dist/events.js +4 -1
  27. package/dist/events.js.map +1 -1
  28. package/dist/keystore/index.d.ts +11 -0
  29. package/dist/keystore/index.d.ts.map +1 -1
  30. package/dist/keystore/indexeddb.d.ts +38 -0
  31. package/dist/keystore/indexeddb.d.ts.map +1 -1
  32. package/dist/keystore/indexeddb.js +245 -98
  33. package/dist/keystore/indexeddb.js.map +1 -1
  34. package/dist/logger.d.ts +37 -0
  35. package/dist/logger.d.ts.map +1 -0
  36. package/dist/logger.js +112 -0
  37. package/dist/logger.js.map +1 -0
  38. package/dist/namespaces/auth.d.ts +13 -3
  39. package/dist/namespaces/auth.d.ts.map +1 -1
  40. package/dist/namespaces/auth.js +284 -106
  41. package/dist/namespaces/auth.js.map +1 -1
  42. package/dist/namespaces/custody.d.ts +3 -0
  43. package/dist/namespaces/custody.d.ts.map +1 -1
  44. package/dist/namespaces/custody.js +147 -75
  45. package/dist/namespaces/custody.js.map +1 -1
  46. package/dist/namespaces/meta.d.ts +3 -0
  47. package/dist/namespaces/meta.d.ts.map +1 -1
  48. package/dist/namespaces/meta.js +94 -43
  49. package/dist/namespaces/meta.js.map +1 -1
  50. package/dist/secret-store/indexeddb-store.d.ts +3 -0
  51. package/dist/secret-store/indexeddb-store.d.ts.map +1 -1
  52. package/dist/secret-store/indexeddb-store.js +57 -29
  53. package/dist/secret-store/indexeddb-store.js.map +1 -1
  54. package/dist/transport.d.ts +3 -0
  55. package/dist/transport.d.ts.map +1 -1
  56. package/dist/transport.js +74 -4
  57. package/dist/transport.js.map +1 -1
  58. package/package.json +37 -37
package/dist/e2ee.js CHANGED
@@ -2,6 +2,10 @@
2
2
  // 所有密码学操作均为异步(SubtleCrypto API 要求)
3
3
  import { E2EEDecryptFailedError, E2EEError } from './errors.js';
4
4
  import { uint8ToBase64, base64ToUint8, pemToArrayBuffer, p1363ToDer, toArrayBuffer, toBufferSource } from './crypto.js';
5
+ const _noopLog = { error: () => { }, warn: () => { }, info: () => { }, debug: () => { } };
6
+ // 顶层函数共享的模块 logger(client 构造时通过 setModuleLogger 注入)
7
+ let _moduleLog = _noopLog;
8
+ export function setModuleLogger(log) { _moduleLog = log; }
5
9
  /** 加密套件标识 */
6
10
  export const SUITE = 'P256_HKDF_SHA256_AES_256_GCM';
7
11
  /** 加密模式 */
@@ -558,6 +562,8 @@ async function importUncompressedPointEcdh(pointBytes) {
558
562
  * 所有密码学操作均为异步(SubtleCrypto 要求)。
559
563
  */
560
564
  export class E2EEManager {
565
+ _log = _noopLog;
566
+ setLogger(log) { this._log = log; }
561
567
  _identityFn;
562
568
  _deviceIdFn;
563
569
  _keystoreRef;
@@ -607,16 +613,26 @@ export class E2EEManager {
607
613
  * 调用方负责提前获取 peerCertPem 和 prekey(可选)。
608
614
  */
609
615
  async encryptMessage(toAid, payload, opts) {
610
- const messageId = opts.messageId ?? uuidV4();
611
- const timestamp = opts.timestamp ?? Date.now();
612
- return this.encryptOutbound(toAid, payload, {
613
- peerCertPem: opts.peerCertPem,
614
- prekey: opts.prekey ?? null,
615
- messageId,
616
- timestamp,
617
- protectedHeaders: opts.protectedHeaders ?? opts.protected_headers ?? opts.headers,
618
- context: opts.context ?? null,
619
- });
616
+ const tStart = Date.now();
617
+ this._log.debug(`encryptMessage enter: to_aid=${toAid} has_prekey=${!!opts.prekey}`);
618
+ try {
619
+ const messageId = opts.messageId ?? uuidV4();
620
+ const timestamp = opts.timestamp ?? Date.now();
621
+ const result = await this.encryptOutbound(toAid, payload, {
622
+ peerCertPem: opts.peerCertPem,
623
+ prekey: opts.prekey ?? null,
624
+ messageId,
625
+ timestamp,
626
+ protectedHeaders: opts.protectedHeaders ?? opts.protected_headers ?? opts.headers,
627
+ context: opts.context ?? null,
628
+ });
629
+ this._log.debug(`encryptMessage exit: elapsed=${Date.now() - tStart}ms to_aid=${toAid} mode=${result[1].mode}`);
630
+ return result;
631
+ }
632
+ catch (err) {
633
+ this._log.debug(`encryptMessage exit (error): elapsed=${Date.now() - tStart}ms err=${err instanceof Error ? err.message : String(err)}`);
634
+ throw err;
635
+ }
620
636
  }
621
637
  // ── 加密 ──────────────────────────────────────────
622
638
  /**
@@ -626,37 +642,47 @@ export class E2EEManager {
626
642
  * prekey 传入时自动缓存;传入 null 时自动查缓存。
627
643
  */
628
644
  async encryptOutbound(peerAid, payload, opts) {
629
- let prekey = opts.prekey ?? null;
630
- // 传入 prekey 缓存;传入 null → 查缓存
631
- if (prekey !== null) {
632
- this.cachePrekey(peerAid, prekey);
633
- }
634
- else {
635
- prekey = this.getCachedPrekey(peerAid);
636
- }
637
- if (prekey) {
638
- try {
639
- const envelope = await this._encryptWithPrekey(peerAid, payload, prekey, opts.peerCertPem, opts.messageId, opts.timestamp, opts.protectedHeaders ?? opts.protected_headers ?? opts.headers, opts.context ?? null);
640
- return [envelope, {
641
- encrypted: true,
642
- forward_secrecy: true,
643
- mode: MODE_PREKEY_ECDH_V2,
644
- degraded: false,
645
- }];
645
+ const tStart = Date.now();
646
+ this._log.debug(`encryptOutbound enter: peer_aid=${peerAid} mid=${opts.messageId} has_prekey=${!!opts.prekey}`);
647
+ try {
648
+ let prekey = opts.prekey ?? null;
649
+ // 传入 prekey → 缓存;传入 null → 查缓存
650
+ if (prekey !== null) {
651
+ this.cachePrekey(peerAid, prekey);
646
652
  }
647
- catch (exc) {
648
- console.warn('prekey 加密失败,降级到 long_term_key(无前向保密):', exc);
653
+ else {
654
+ prekey = this.getCachedPrekey(peerAid);
649
655
  }
656
+ if (prekey) {
657
+ try {
658
+ const envelope = await this._encryptWithPrekey(peerAid, payload, prekey, opts.peerCertPem, opts.messageId, opts.timestamp, opts.protectedHeaders ?? opts.protected_headers ?? opts.headers, opts.context ?? null);
659
+ this._log.debug(`encryptOutbound exit: elapsed=${Date.now() - tStart}ms peer_aid=${peerAid} mode=prekey_ecdh_v2`);
660
+ return [envelope, {
661
+ encrypted: true,
662
+ forward_secrecy: true,
663
+ mode: MODE_PREKEY_ECDH_V2,
664
+ degraded: false,
665
+ }];
666
+ }
667
+ catch (exc) {
668
+ this._log.warn('prekey encrypt failed, degrade to long_term_key (no forward secrecy):', exc);
669
+ }
670
+ }
671
+ const envelope = await this._encryptWithLongTermKey(peerAid, payload, opts.peerCertPem, opts.messageId, opts.timestamp, opts.protectedHeaders ?? opts.protected_headers ?? opts.headers, opts.context ?? null);
672
+ const degraded = prekey !== null; // 有 prekey 但失败了才算降级
673
+ this._log.debug(`encryptOutbound exit: elapsed=${Date.now() - tStart}ms peer_aid=${peerAid} mode=long_term_key degraded=${degraded}`);
674
+ return [envelope, {
675
+ encrypted: true,
676
+ forward_secrecy: false,
677
+ mode: MODE_LONG_TERM_KEY,
678
+ degraded,
679
+ degradation_reason: degraded ? 'prekey_encrypt_failed' : 'no_prekey_available',
680
+ }];
681
+ }
682
+ catch (err) {
683
+ this._log.debug(`encryptOutbound exit (error): elapsed=${Date.now() - tStart}ms err=${err instanceof Error ? err.message : String(err)}`);
684
+ throw err;
650
685
  }
651
- const envelope = await this._encryptWithLongTermKey(peerAid, payload, opts.peerCertPem, opts.messageId, opts.timestamp, opts.protectedHeaders ?? opts.protected_headers ?? opts.headers, opts.context ?? null);
652
- const degraded = prekey !== null; // 有 prekey 但失败了才算降级
653
- return [envelope, {
654
- encrypted: true,
655
- forward_secrecy: false,
656
- mode: MODE_LONG_TERM_KEY,
657
- degraded,
658
- degradation_reason: degraded ? 'prekey_encrypt_failed' : 'no_prekey_available',
659
- }];
660
686
  }
661
687
  /**
662
688
  * 使用对方 prekey 加密(prekey_ecdh_v2 模式,四路 ECDH + 发送方签名)
@@ -828,44 +854,68 @@ export class E2EEManager {
828
854
  * opts.skipReplay: 跳过防重放和 timestamp 窗口检查(用于 message.pull 场景)。
829
855
  */
830
856
  async decryptMessage(message, opts) {
831
- const payload = message.payload;
832
- if (!payload || typeof payload !== 'object')
833
- return message;
834
- if (payload.type !== 'e2ee.encrypted')
835
- return message;
836
- if (message.encrypted === false)
837
- return message;
838
- if (!this._shouldDecryptForCurrentAid(message, payload))
839
- return message;
840
- const skipReplay = opts?.skipReplay ?? false;
841
- if (!skipReplay) {
842
- // timestamp 窗口检查
843
- const ts = (message.timestamp ?? payload.aad?.timestamp);
844
- if (typeof ts === 'number' && this._replayWindowSeconds > 0) {
845
- const nowMs = Date.now();
846
- const diffS = Math.abs(nowMs - ts) / 1000;
847
- if (diffS > this._replayWindowSeconds) {
848
- console.warn(`消息 timestamp 超出窗口 (${Math.round(diffS)}s > ${this._replayWindowSeconds}s),拒绝: from=${message.from} mid=${message.message_id}`);
849
- return null;
850
- }
857
+ const tStart = Date.now();
858
+ const mid = String(message.message_id ?? '');
859
+ const fromAid = String(message.from ?? '');
860
+ this._log.debug(`decryptMessage enter: from=${fromAid} mid=${mid} skip_replay=${!!opts?.skipReplay}`);
861
+ try {
862
+ const payload = message.payload;
863
+ if (!payload || typeof payload !== 'object') {
864
+ this._log.debug(`decryptMessage exit: elapsed=${Date.now() - tStart}ms result=passthrough_no_payload`);
865
+ return message;
851
866
  }
852
- // 本地防重放:先检查,解密成功后再记录。
853
- const messageIdVal = message.message_id;
854
- const fromAid = message.from;
855
- let seenKey = '';
856
- if (messageIdVal && fromAid) {
857
- seenKey = `${fromAid}:${messageIdVal}`;
858
- if (this._seenMessages.has(seenKey))
859
- return null;
867
+ if (payload.type !== 'e2ee.encrypted') {
868
+ this._log.debug(`decryptMessage exit: elapsed=${Date.now() - tStart}ms result=passthrough_not_encrypted`);
869
+ return message;
860
870
  }
861
- const result = await this._decryptMessageInternal(message);
862
- if (result !== null && seenKey) {
863
- this._seenMessages.set(seenKey, true);
864
- this._trimSeenSet();
871
+ if (message.encrypted === false) {
872
+ this._log.debug(`decryptMessage exit: elapsed=${Date.now() - tStart}ms result=passthrough_flag_false`);
873
+ return message;
874
+ }
875
+ if (!this._shouldDecryptForCurrentAid(message, payload)) {
876
+ this._log.debug(`decryptMessage exit: elapsed=${Date.now() - tStart}ms result=passthrough_not_for_current_aid`);
877
+ return message;
878
+ }
879
+ const skipReplay = opts?.skipReplay ?? false;
880
+ if (!skipReplay) {
881
+ // timestamp 窗口检查
882
+ const ts = (message.timestamp ?? payload.aad?.timestamp);
883
+ if (typeof ts === 'number' && this._replayWindowSeconds > 0) {
884
+ const nowMs = Date.now();
885
+ const diffS = Math.abs(nowMs - ts) / 1000;
886
+ if (diffS > this._replayWindowSeconds) {
887
+ this._log.warn(`消息 timestamp 超出窗口 (${Math.round(diffS)}s > ${this._replayWindowSeconds}s),拒绝: from=${message.from} mid=${message.message_id}`);
888
+ this._log.debug(`decryptMessage exit: elapsed=${Date.now() - tStart}ms result=rejected_timestamp_window`);
889
+ return null;
890
+ }
891
+ }
892
+ // 本地防重放:先检查,解密成功后再记录。
893
+ const messageIdVal = message.message_id;
894
+ const fromAidLocal = message.from;
895
+ let seenKey = '';
896
+ if (messageIdVal && fromAidLocal) {
897
+ seenKey = `${fromAidLocal}:${messageIdVal}`;
898
+ if (this._seenMessages.has(seenKey)) {
899
+ this._log.debug(`decryptMessage exit: elapsed=${Date.now() - tStart}ms result=rejected_replay`);
900
+ return null;
901
+ }
902
+ }
903
+ const result = await this._decryptMessageInternal(message);
904
+ if (result !== null && seenKey) {
905
+ this._seenMessages.set(seenKey, true);
906
+ this._trimSeenSet();
907
+ }
908
+ this._log.debug(`decryptMessage exit: elapsed=${Date.now() - tStart}ms result=${result !== null ? 'ok' : 'failed'}`);
909
+ return result;
865
910
  }
911
+ const result = await this._decryptMessageInternal(message);
912
+ this._log.debug(`decryptMessage exit: elapsed=${Date.now() - tStart}ms result=${result !== null ? 'ok' : 'failed'} skip_replay=true`);
866
913
  return result;
867
914
  }
868
- return this._decryptMessageInternal(message);
915
+ catch (err) {
916
+ this._log.debug(`decryptMessage exit (error): elapsed=${Date.now() - tStart}ms err=${err instanceof Error ? err.message : String(err)}`);
917
+ throw err;
918
+ }
869
919
  }
870
920
  /** 判断是否应该为当前 AID 解密(避免发送端回显消息误走解密) */
871
921
  _shouldDecryptForCurrentAid(message, payload) {
@@ -890,7 +940,7 @@ export class E2EEManager {
890
940
  await this._verifySenderSignature(payload, message);
891
941
  }
892
942
  catch (exc) {
893
- console.warn('发送方签名验证失败:', exc);
943
+ this._log.warn('sendersignature verifyfailed:', exc);
894
944
  return null;
895
945
  }
896
946
  const encryptionMode = payload.encryption_mode;
@@ -901,7 +951,7 @@ export class E2EEManager {
901
951
  return this._decryptMessageLongTerm(message);
902
952
  }
903
953
  else {
904
- console.warn('不支持的加密模式:', encryptionMode);
954
+ this._log.warn('unsupported encryption mode:', encryptionMode);
905
955
  return null;
906
956
  }
907
957
  }
@@ -1022,10 +1072,10 @@ export class E2EEManager {
1022
1072
  }
1023
1073
  catch (exc) {
1024
1074
  if (exc instanceof E2EEError) {
1025
- console.warn('prekey_ecdh_v2 解密失败 (E2EE):', exc);
1075
+ this._log.warn('prekey_ecdh_v2 decryptfailed (E2EE):', exc);
1026
1076
  }
1027
1077
  else {
1028
- console.warn('prekey_ecdh_v2 解密失败:', exc);
1078
+ this._log.warn('prekey_ecdh_v2 decryptfailed:', exc);
1029
1079
  }
1030
1080
  return null;
1031
1081
  }
@@ -1100,10 +1150,10 @@ export class E2EEManager {
1100
1150
  }
1101
1151
  catch (exc) {
1102
1152
  if (exc instanceof E2EEError) {
1103
- console.warn('long_term_key 解密失败 (E2EE):', exc);
1153
+ this._log.warn('long_term_key decryptfailed (E2EE):', exc);
1104
1154
  }
1105
1155
  else {
1106
- console.warn('long_term_key 解密失败:', exc);
1156
+ this._log.warn('long_term_key decryptfailed:', exc);
1107
1157
  }
1108
1158
  return null;
1109
1159
  }
@@ -1132,48 +1182,57 @@ export class E2EEManager {
1132
1182
  * 返回 { prekey_id, public_key, signature, created_at },可直接用于 RPC 上传。
1133
1183
  */
1134
1184
  async generatePrekey() {
1135
- const aid = this._currentAid();
1136
- if (!aid)
1137
- throw new E2EEError('AID unavailable for prekey generation');
1138
- const deviceId = this._currentDeviceId();
1139
- // 生成新 ECDH 密钥对(标记为 ECDH 用途)
1140
- const keyPair = await crypto.subtle.generateKey({ name: 'ECDH', namedCurve: 'P-256' }, true, ['deriveBits']);
1141
- // 导出 SPKI 公钥(DER 格式)
1142
- const spki = await crypto.subtle.exportKey('spki', keyPair.publicKey);
1143
- const publicKeyB64 = uint8ToBase64(new Uint8Array(spki));
1144
- // 导出 PKCS8 私钥(PEM 格式存储)
1145
- const pkcs8 = await crypto.subtle.exportKey('pkcs8', keyPair.privateKey);
1146
- const privateKeyPem = arrayBufferToPemLocal(pkcs8, 'PRIVATE KEY');
1147
- const prekeyId = uuidV4();
1148
- const nowMs = Date.now();
1149
- // 签名:prekey_id|public_key|created_at(绑定时间戳,防止旧 prekey 重放)
1150
- const signData = _encoder.encode(`${prekeyId}|${publicKeyB64}|${nowMs}`);
1151
- const senderSignKey = await this._loadSenderIdentityPrivateEcdsa();
1152
- const sig = await ecdsaSignDer(senderSignKey, signData);
1153
- const signatureB64 = uint8ToBase64(sig);
1154
- await saveKeyStorePrekey(this._keystoreRef, aid, deviceId, prekeyId, {
1155
- private_key_pem: privateKeyPem,
1156
- created_at: nowMs,
1157
- updated_at: nowMs,
1158
- });
1159
- // 内存缓存私钥 PEM
1160
- this._localPrekeyCache.set(prekeyId, privateKeyPem);
1161
- // 清理过期的旧 prekey
1162
- await this._cleanupExpiredPrekeys(aid, deviceId);
1163
- const result = {
1164
- prekey_id: prekeyId,
1165
- public_key: publicKeyB64,
1166
- signature: signatureB64,
1167
- created_at: nowMs,
1168
- };
1169
- const certFingerprint = await this._localCertSha256Fingerprint();
1170
- if (certFingerprint) {
1171
- result.cert_fingerprint = certFingerprint;
1185
+ const tStart = Date.now();
1186
+ this._log.debug('generatePrekey enter');
1187
+ try {
1188
+ const aid = this._currentAid();
1189
+ if (!aid)
1190
+ throw new E2EEError('AID unavailable for prekey generation');
1191
+ const deviceId = this._currentDeviceId();
1192
+ // 生成新 ECDH 密钥对(标记为 ECDH 用途)
1193
+ const keyPair = await crypto.subtle.generateKey({ name: 'ECDH', namedCurve: 'P-256' }, true, ['deriveBits']);
1194
+ // 导出 SPKI 公钥(DER 格式)
1195
+ const spki = await crypto.subtle.exportKey('spki', keyPair.publicKey);
1196
+ const publicKeyB64 = uint8ToBase64(new Uint8Array(spki));
1197
+ // 导出 PKCS8 私钥(PEM 格式存储)
1198
+ const pkcs8 = await crypto.subtle.exportKey('pkcs8', keyPair.privateKey);
1199
+ const privateKeyPem = arrayBufferToPemLocal(pkcs8, 'PRIVATE KEY');
1200
+ const prekeyId = uuidV4();
1201
+ const nowMs = Date.now();
1202
+ // 签名:prekey_id|public_key|created_at(绑定时间戳,防止旧 prekey 重放)
1203
+ const signData = _encoder.encode(`${prekeyId}|${publicKeyB64}|${nowMs}`);
1204
+ const senderSignKey = await this._loadSenderIdentityPrivateEcdsa();
1205
+ const sig = await ecdsaSignDer(senderSignKey, signData);
1206
+ const signatureB64 = uint8ToBase64(sig);
1207
+ await saveKeyStorePrekey(this._keystoreRef, aid, deviceId, prekeyId, {
1208
+ private_key_pem: privateKeyPem,
1209
+ created_at: nowMs,
1210
+ updated_at: nowMs,
1211
+ });
1212
+ // 内存缓存私钥 PEM
1213
+ this._localPrekeyCache.set(prekeyId, privateKeyPem);
1214
+ // 清理过期的旧 prekey
1215
+ await this._cleanupExpiredPrekeys(aid, deviceId);
1216
+ const result = {
1217
+ prekey_id: prekeyId,
1218
+ public_key: publicKeyB64,
1219
+ signature: signatureB64,
1220
+ created_at: nowMs,
1221
+ };
1222
+ const certFingerprint = await this._localCertSha256Fingerprint();
1223
+ if (certFingerprint) {
1224
+ result.cert_fingerprint = certFingerprint;
1225
+ }
1226
+ if (deviceId) {
1227
+ result.device_id = deviceId;
1228
+ }
1229
+ this._log.debug(`generatePrekey exit: elapsed=${Date.now() - tStart}ms aid=${aid} prekey_id=${prekeyId}`);
1230
+ return result;
1172
1231
  }
1173
- if (deviceId) {
1174
- result.device_id = deviceId;
1232
+ catch (err) {
1233
+ this._log.debug(`generatePrekey exit (error): elapsed=${Date.now() - tStart}ms err=${err instanceof Error ? err.message : String(err)}`);
1234
+ throw err;
1175
1235
  }
1176
- return result;
1177
1236
  }
1178
1237
  /** 清理过期的本地 prekey 私钥 */
1179
1238
  async _cleanupExpiredPrekeys(aid, deviceId) {
@@ -1195,6 +1254,26 @@ export class E2EEManager {
1195
1254
  const aid = this._currentAid();
1196
1255
  if (!aid)
1197
1256
  return null;
1257
+ // 优先按 prekey_id 单点查询(IndexedDB 主键直查 O(log N))。
1258
+ // 旧路径需要全量加载 prekey 池(prekey 数量大时是性能瓶颈),单查能直接命中信封里的目标 prekey。
1259
+ const byIdLoader = this._keystoreRef.loadE2EEPrekeyById;
1260
+ if (typeof byIdLoader === 'function') {
1261
+ try {
1262
+ const byIdData = await byIdLoader.call(this._keystoreRef, aid, prekeyId);
1263
+ if (byIdData) {
1264
+ const pem = byIdData.private_key_pem;
1265
+ if (typeof pem === 'string' && pem) {
1266
+ this._log.debug(`prekey ${prekeyId} by_id lookup hit`);
1267
+ this._localPrekeyCache.set(prekeyId, pem);
1268
+ return pem;
1269
+ }
1270
+ }
1271
+ }
1272
+ catch (err) {
1273
+ this._log.warn(`prekey ${prekeyId} by_id loader failed, falling back to full load: ${err instanceof Error ? err.message : String(err)}`);
1274
+ }
1275
+ }
1276
+ // 回退:旧版 keystore 没有 by_id 方法,或单查未命中 → 全量扫描(保持向后兼容)。
1198
1277
  const prekeys = await loadKeyStorePrekeys(this._keystoreRef, aid, this._currentDeviceId());
1199
1278
  const prekeyData = prekeys[prekeyId];
1200
1279
  if (!prekeyData)