@agentunion/fastaun-browser 0.2.13

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 (82) hide show
  1. package/README.md +604 -0
  2. package/dist/auth.d.ts +150 -0
  3. package/dist/auth.d.ts.map +1 -0
  4. package/dist/auth.js +1388 -0
  5. package/dist/auth.js.map +1 -0
  6. package/dist/certs/root.d.ts +2 -0
  7. package/dist/certs/root.d.ts.map +1 -0
  8. package/dist/certs/root.js +16 -0
  9. package/dist/certs/root.js.map +1 -0
  10. package/dist/client.d.ts +341 -0
  11. package/dist/client.d.ts.map +1 -0
  12. package/dist/client.js +4061 -0
  13. package/dist/client.js.map +1 -0
  14. package/dist/config.d.ts +37 -0
  15. package/dist/config.d.ts.map +1 -0
  16. package/dist/config.js +85 -0
  17. package/dist/config.js.map +1 -0
  18. package/dist/crypto.d.ts +41 -0
  19. package/dist/crypto.d.ts.map +1 -0
  20. package/dist/crypto.js +132 -0
  21. package/dist/crypto.js.map +1 -0
  22. package/dist/discovery.d.ts +20 -0
  23. package/dist/discovery.d.ts.map +1 -0
  24. package/dist/discovery.js +75 -0
  25. package/dist/discovery.js.map +1 -0
  26. package/dist/e2ee-group.d.ts +221 -0
  27. package/dist/e2ee-group.d.ts.map +1 -0
  28. package/dist/e2ee-group.js +1174 -0
  29. package/dist/e2ee-group.js.map +1 -0
  30. package/dist/e2ee.d.ts +187 -0
  31. package/dist/e2ee.d.ts.map +1 -0
  32. package/dist/e2ee.js +1067 -0
  33. package/dist/e2ee.js.map +1 -0
  34. package/dist/errors.d.ts +118 -0
  35. package/dist/errors.d.ts.map +1 -0
  36. package/dist/errors.js +250 -0
  37. package/dist/errors.js.map +1 -0
  38. package/dist/events.d.ts +33 -0
  39. package/dist/events.d.ts.map +1 -0
  40. package/dist/events.js +68 -0
  41. package/dist/events.js.map +1 -0
  42. package/dist/index.d.ts +22 -0
  43. package/dist/index.d.ts.map +1 -0
  44. package/dist/index.js +32 -0
  45. package/dist/index.js.map +1 -0
  46. package/dist/keystore/index.d.ts +88 -0
  47. package/dist/keystore/index.d.ts.map +1 -0
  48. package/dist/keystore/index.js +3 -0
  49. package/dist/keystore/index.js.map +1 -0
  50. package/dist/keystore/indexeddb.d.ts +94 -0
  51. package/dist/keystore/indexeddb.d.ts.map +1 -0
  52. package/dist/keystore/indexeddb.js +1434 -0
  53. package/dist/keystore/indexeddb.js.map +1 -0
  54. package/dist/namespaces/auth.d.ts +52 -0
  55. package/dist/namespaces/auth.d.ts.map +1 -0
  56. package/dist/namespaces/auth.js +237 -0
  57. package/dist/namespaces/auth.js.map +1 -0
  58. package/dist/namespaces/custody.d.ts +48 -0
  59. package/dist/namespaces/custody.d.ts.map +1 -0
  60. package/dist/namespaces/custody.js +230 -0
  61. package/dist/namespaces/custody.js.map +1 -0
  62. package/dist/secret-store/index.d.ts +20 -0
  63. package/dist/secret-store/index.d.ts.map +1 -0
  64. package/dist/secret-store/index.js +12 -0
  65. package/dist/secret-store/index.js.map +1 -0
  66. package/dist/secret-store/indexeddb-store.d.ts +22 -0
  67. package/dist/secret-store/indexeddb-store.d.ts.map +1 -0
  68. package/dist/secret-store/indexeddb-store.js +133 -0
  69. package/dist/secret-store/indexeddb-store.js.map +1 -0
  70. package/dist/seq-tracker.d.ts +30 -0
  71. package/dist/seq-tracker.d.ts.map +1 -0
  72. package/dist/seq-tracker.js +219 -0
  73. package/dist/seq-tracker.js.map +1 -0
  74. package/dist/transport.d.ts +45 -0
  75. package/dist/transport.d.ts.map +1 -0
  76. package/dist/transport.js +251 -0
  77. package/dist/transport.js.map +1 -0
  78. package/dist/types.d.ts +171 -0
  79. package/dist/types.d.ts.map +1 -0
  80. package/dist/types.js +10 -0
  81. package/dist/types.js.map +1 -0
  82. package/package.json +37 -0
@@ -0,0 +1,1434 @@
1
+ // ── IndexedDB KeyStore 实现 ──────────────────────────────
2
+ import { pemToArrayBuffer } from '../crypto.js';
3
+ import { normalizeInstanceId } from '../config.js';
4
+ import { isJsonObject, } from '../types.js';
5
+ /** AID 安全化(替换路径分隔符) */
6
+ function safeAid(aid) {
7
+ return aid.replace(/[/\\:]/g, '_');
8
+ }
9
+ /** 从 cert PEM 中提取 SPKI 公钥的 base64(纯 ASN.1 DER 解析,浏览器兼容) */
10
+ function extractSpkiB64FromCertPem(certPem) {
11
+ try {
12
+ const der = new Uint8Array(pemToArrayBuffer(certPem));
13
+ // 最小 ASN.1 TLV 读取
14
+ const tlv = (d, o) => {
15
+ let lo = o + 1, len;
16
+ if (d[lo] & 0x80) {
17
+ const n = d[lo] & 0x7f;
18
+ len = 0;
19
+ for (let i = 0; i < n; i++)
20
+ len = (len << 8) | d[lo + 1 + i];
21
+ lo += 1 + n;
22
+ }
23
+ else {
24
+ len = d[lo];
25
+ lo += 1;
26
+ }
27
+ return [lo, len]; // [valueStart, valueLen]
28
+ };
29
+ const skip = (d, o) => { const [vs, vl] = tlv(d, o); return vs + vl; };
30
+ // Certificate → SEQUENCE
31
+ let [vs] = tlv(der, 0);
32
+ // TBSCertificate → SEQUENCE
33
+ let pos = vs;
34
+ const [tbsVs] = tlv(der, pos);
35
+ pos = tbsVs;
36
+ // version [0] EXPLICIT (optional)
37
+ if (der[pos] === 0xa0)
38
+ pos = skip(der, pos);
39
+ // serialNumber, signatureAlgorithm, issuer, validity, subject — 跳过 5 个字段
40
+ for (let i = 0; i < 5; i++)
41
+ pos = skip(der, pos);
42
+ // SubjectPublicKeyInfo — 就是当前位置
43
+ const spkiEnd = skip(der, pos);
44
+ const spkiBytes = der.slice(pos, spkiEnd);
45
+ // base64 编码
46
+ let b = '';
47
+ for (let i = 0; i < spkiBytes.length; i++)
48
+ b += String.fromCharCode(spkiBytes[i]);
49
+ return btoa(b);
50
+ }
51
+ catch {
52
+ return '';
53
+ }
54
+ }
55
+ function encodePart(value) {
56
+ return encodeURIComponent(value);
57
+ }
58
+ function deepClone(value) {
59
+ return JSON.parse(JSON.stringify(value));
60
+ }
61
+ function isRecord(value) {
62
+ return isJsonObject(value);
63
+ }
64
+ function toBufferSource(bytes) {
65
+ return bytes.slice().buffer;
66
+ }
67
+ function sameJson(a, b) {
68
+ return JSON.stringify(a) === JSON.stringify(b);
69
+ }
70
+ // ── 私钥字段级加密 (AES-256-GCM + PBKDF2) ──────────────
71
+ const _ENC_ALGO = 'AES-GCM';
72
+ const _PBKDF2_ITERATIONS = 100_000;
73
+ /** 将 Uint8Array 编码为 base64 字符串 */
74
+ function _uint8ToBase64(bytes) {
75
+ let b = '';
76
+ for (let i = 0; i < bytes.length; i++)
77
+ b += String.fromCharCode(bytes[i]);
78
+ return btoa(b);
79
+ }
80
+ /** 将 base64 字符串解码为 Uint8Array */
81
+ function _base64ToUint8(b64) {
82
+ return Uint8Array.from(atob(b64), c => c.charCodeAt(0));
83
+ }
84
+ /** 从 seed 派生 AES-256 加密密钥 */
85
+ async function _deriveEncKey(seed, salt) {
86
+ const raw = new TextEncoder().encode(seed);
87
+ const base = await crypto.subtle.importKey('raw', raw, 'PBKDF2', false, ['deriveKey']);
88
+ return crypto.subtle.deriveKey({ name: 'PBKDF2', salt: salt, iterations: _PBKDF2_ITERATIONS, hash: 'SHA-256' }, base, { name: _ENC_ALGO, length: 256 }, false, ['encrypt', 'decrypt']);
89
+ }
90
+ /** 加密 PEM 字符串,返回加密信封 */
91
+ async function _encryptPEM(pem, seed) {
92
+ const salt = crypto.getRandomValues(new Uint8Array(16));
93
+ const iv = crypto.getRandomValues(new Uint8Array(12));
94
+ const key = await _deriveEncKey(seed, salt);
95
+ const ct = await crypto.subtle.encrypt({ name: _ENC_ALGO, iv: iv }, key, new TextEncoder().encode(pem));
96
+ return {
97
+ ct: _uint8ToBase64(new Uint8Array(ct)),
98
+ iv: _uint8ToBase64(iv),
99
+ salt: _uint8ToBase64(salt),
100
+ };
101
+ }
102
+ /** 解密加密信封,还原 PEM 字符串 */
103
+ async function _decryptPEM(enc, seed) {
104
+ const salt = _base64ToUint8(enc.salt);
105
+ const iv = _base64ToUint8(enc.iv);
106
+ const ct = _base64ToUint8(enc.ct);
107
+ const key = await _deriveEncKey(seed, salt);
108
+ const pt = await crypto.subtle.decrypt({ name: _ENC_ALGO, iv: toBufferSource(iv) }, key, toBufferSource(ct));
109
+ return new TextDecoder().decode(pt);
110
+ }
111
+ // ── IndexedDB 工具 ──────────────────────────────────────
112
+ const DB_NAME = 'aun-keystore';
113
+ const DB_VERSION = 4;
114
+ /** 对象仓库名称 */
115
+ const STORE_KEY_PAIRS = 'key_pairs';
116
+ const STORE_CERTS = 'certs';
117
+ const STORE_METADATA = 'metadata';
118
+ const STORE_INSTANCE_STATE = 'instance_state';
119
+ const STORE_PREKEYS = 'prekeys';
120
+ const STORE_GROUP_CURRENT = 'group_current';
121
+ const STORE_GROUP_OLD_EPOCHS = 'group_old_epochs';
122
+ const STORE_SESSIONS = 'e2ee_sessions';
123
+ const STRUCTURED_RECOVERY_RETENTION_MS = 7 * 24 * 3600 * 1000;
124
+ const CRITICAL_METADATA_KEYS = [];
125
+ function metadataStoreKey(aid) {
126
+ return safeAid(aid);
127
+ }
128
+ function normalizeCertFingerprint(certFingerprint) {
129
+ const normalized = String(certFingerprint ?? '').trim().toLowerCase();
130
+ if (!normalized)
131
+ return '';
132
+ if (!normalized.startsWith('sha256:'))
133
+ return '';
134
+ const hexPart = normalized.slice(7);
135
+ if (hexPart.length !== 64 || /[^0-9a-f]/.test(hexPart))
136
+ return '';
137
+ return normalized;
138
+ }
139
+ async function fingerprintFromCertPem(certPem) {
140
+ try {
141
+ const der = pemToArrayBuffer(certPem);
142
+ const hash = await crypto.subtle.digest('SHA-256', der);
143
+ const hex = Array.from(new Uint8Array(hash)).map((b) => b.toString(16).padStart(2, '0')).join('');
144
+ return `sha256:${hex}`;
145
+ }
146
+ catch {
147
+ return '';
148
+ }
149
+ }
150
+ function certStoreKey(aid, certFingerprint) {
151
+ const normalized = normalizeCertFingerprint(certFingerprint);
152
+ if (!normalized)
153
+ return safeAid(aid);
154
+ return `${safeAid(aid)}|${encodePart(normalized)}`;
155
+ }
156
+ function instanceStateStoreKey(aid, deviceId, slotId = '') {
157
+ const normalizedDevice = normalizeInstanceId(deviceId, 'device_id');
158
+ const normalizedSlot = normalizeInstanceId(slotId, 'slot_id', { allowEmpty: true }) || '_singleton';
159
+ return `${safeAid(aid)}|${encodePart(normalizedDevice)}|${encodePart(normalizedSlot)}`;
160
+ }
161
+ function prekeyPrefix(aid) {
162
+ return `${safeAid(aid)}|`;
163
+ }
164
+ function prekeyStoreKey(aid, prekeyId, deviceId = '') {
165
+ const normalizedDeviceId = String(deviceId ?? '').trim();
166
+ if (!normalizedDeviceId) {
167
+ return `${safeAid(aid)}|${encodePart(prekeyId)}`;
168
+ }
169
+ return `${safeAid(aid)}|${encodePart(normalizedDeviceId)}|${encodePart(prekeyId)}`;
170
+ }
171
+ function groupCurrentPrefix(aid) {
172
+ return `${safeAid(aid)}|`;
173
+ }
174
+ function groupCurrentStoreKey(aid, groupId) {
175
+ return `${safeAid(aid)}|${encodePart(groupId)}`;
176
+ }
177
+ function groupOldPrefix(aid, groupId) {
178
+ const base = `${safeAid(aid)}|`;
179
+ if (groupId === undefined)
180
+ return base;
181
+ return `${base}${encodePart(groupId)}|`;
182
+ }
183
+ function groupOldStoreKey(aid, groupId, epoch) {
184
+ return `${safeAid(aid)}|${encodePart(groupId)}|${epoch}`;
185
+ }
186
+ function sessionPrefix(aid) {
187
+ return `${safeAid(aid)}|`;
188
+ }
189
+ function sessionStoreKey(aid, sessionId) {
190
+ return `${safeAid(aid)}|${encodePart(sessionId)}`;
191
+ }
192
+ function seqTrackerPrefix(aid, deviceId, slotId) {
193
+ const normalizedDevice = normalizeInstanceId(deviceId, 'device_id');
194
+ const normalizedSlot = normalizeInstanceId(slotId, 'slot_id', { allowEmpty: true }) || '_singleton';
195
+ return `_seq_|${safeAid(aid)}|${encodePart(normalizedDevice)}|${encodePart(normalizedSlot)}|`;
196
+ }
197
+ function seqTrackerStoreKey(aid, deviceId, slotId, namespace) {
198
+ return `${seqTrackerPrefix(aid, deviceId, slotId)}${encodePart(namespace)}`;
199
+ }
200
+ function stripStructuredFields(metadata) {
201
+ const plain = deepClone(metadata);
202
+ delete plain.e2ee_prekeys;
203
+ delete plain.group_secrets;
204
+ delete plain.e2ee_sessions;
205
+ return plain;
206
+ }
207
+ /** 缓存的数据库连接 */
208
+ let _cachedDB = null;
209
+ /** 并发竞态保护:缓存正在进行的 openDB Promise,避免多个并发调用同时创建连接 */
210
+ let _openDBPromise = null;
211
+ /** 打开或升级数据库(复用已有连接) */
212
+ function openDB() {
213
+ if (_cachedDB)
214
+ return Promise.resolve(_cachedDB);
215
+ // 如果已有正在进行的打开请求,直接复用同一个 Promise
216
+ if (_openDBPromise)
217
+ return _openDBPromise;
218
+ _openDBPromise = new Promise((resolve, reject) => {
219
+ const request = indexedDB.open(DB_NAME, DB_VERSION);
220
+ request.onupgradeneeded = () => {
221
+ const db = request.result;
222
+ if (!db.objectStoreNames.contains(STORE_KEY_PAIRS)) {
223
+ db.createObjectStore(STORE_KEY_PAIRS);
224
+ }
225
+ if (!db.objectStoreNames.contains(STORE_CERTS)) {
226
+ db.createObjectStore(STORE_CERTS);
227
+ }
228
+ if (!db.objectStoreNames.contains(STORE_METADATA)) {
229
+ db.createObjectStore(STORE_METADATA);
230
+ }
231
+ if (!db.objectStoreNames.contains(STORE_INSTANCE_STATE)) {
232
+ db.createObjectStore(STORE_INSTANCE_STATE);
233
+ }
234
+ if (!db.objectStoreNames.contains(STORE_PREKEYS)) {
235
+ db.createObjectStore(STORE_PREKEYS);
236
+ }
237
+ if (!db.objectStoreNames.contains(STORE_GROUP_CURRENT)) {
238
+ db.createObjectStore(STORE_GROUP_CURRENT);
239
+ }
240
+ if (!db.objectStoreNames.contains(STORE_GROUP_OLD_EPOCHS)) {
241
+ db.createObjectStore(STORE_GROUP_OLD_EPOCHS);
242
+ }
243
+ if (!db.objectStoreNames.contains(STORE_SESSIONS)) {
244
+ db.createObjectStore(STORE_SESSIONS);
245
+ }
246
+ };
247
+ request.onsuccess = () => {
248
+ const db = request.result;
249
+ // H22: 其它 tab 持有旧版本连接时,触发 versionchange 通知它们关闭,避免 onupgradeneeded 挂起
250
+ db.onversionchange = () => {
251
+ _cachedDB = null;
252
+ _openDBPromise = null;
253
+ try {
254
+ db.close();
255
+ }
256
+ catch { /* ignore */ }
257
+ };
258
+ db.onclose = () => { _cachedDB = null; };
259
+ _cachedDB = db;
260
+ _openDBPromise = null; // 成功后清除 Promise 缓存
261
+ resolve(db);
262
+ };
263
+ request.onerror = () => {
264
+ _openDBPromise = null; // 失败后清除,允许后续重试
265
+ reject(new Error('IndexedDB 不可用(可能处于隐私模式或 IndexedDB 被禁用)。' +
266
+ '请使用 FileKeyStore (Node.js) 或在浏览器中允许 IndexedDB。'));
267
+ };
268
+ // H22: onblocked = 其它 tab 持有旧版本阻塞升级;必须显式 reject 让上层感知而非永久挂起
269
+ request.onblocked = () => {
270
+ _openDBPromise = null; // 阻塞时清除,允许后续重试
271
+ reject(new Error('IndexedDB 升级被其它 tab 阻塞,请关闭其它页面后重试'));
272
+ };
273
+ });
274
+ return _openDBPromise;
275
+ }
276
+ /** 从指定仓库读取一条记录 */
277
+ async function idbGet(storeName, key) {
278
+ const db = await openDB();
279
+ return new Promise((resolve, reject) => {
280
+ const tx = db.transaction(storeName, 'readonly');
281
+ const store = tx.objectStore(storeName);
282
+ const req = store.get(key);
283
+ tx.oncomplete = () => {
284
+ resolve(req.result ?? null);
285
+ };
286
+ tx.onerror = () => {
287
+ reject(tx.error ?? new Error(`读取 ${storeName} 失败`));
288
+ };
289
+ tx.onabort = () => {
290
+ reject(tx.error ?? new Error(`读取 ${storeName} 被中止`));
291
+ };
292
+ });
293
+ }
294
+ /** 读取仓库内全部记录 */
295
+ async function idbGetAll(storeName) {
296
+ const db = await openDB();
297
+ return new Promise((resolve, reject) => {
298
+ const tx = db.transaction(storeName, 'readonly');
299
+ const store = tx.objectStore(storeName);
300
+ const valuesReq = store.getAll();
301
+ const keysReq = store.getAllKeys();
302
+ tx.oncomplete = () => {
303
+ const values = valuesReq.result ?? [];
304
+ const keys = keysReq.result ?? [];
305
+ resolve(keys.map((key, index) => ({
306
+ key: typeof key === 'string' ? key : String(key),
307
+ value: values[index],
308
+ })));
309
+ };
310
+ tx.onerror = () => {
311
+ reject(tx.error ?? new Error(`读取 ${storeName} 全量数据失败`));
312
+ };
313
+ tx.onabort = () => {
314
+ reject(tx.error ?? new Error(`读取 ${storeName} 全量数据被中止`));
315
+ };
316
+ });
317
+ }
318
+ async function idbGetAllByPrefix(storeName, prefix) {
319
+ const db = await openDB();
320
+ return new Promise((resolve, reject) => {
321
+ const tx = db.transaction(storeName, 'readonly');
322
+ const store = tx.objectStore(storeName);
323
+ const range = IDBKeyRange.bound(prefix, `${prefix}\uffff`, false, false);
324
+ const result = [];
325
+ const req = store.openCursor(range);
326
+ req.onsuccess = () => {
327
+ const cursor = req.result;
328
+ if (!cursor)
329
+ return;
330
+ result.push({
331
+ key: typeof cursor.key === 'string' ? cursor.key : String(cursor.key),
332
+ value: cursor.value,
333
+ });
334
+ cursor.continue();
335
+ };
336
+ tx.oncomplete = () => {
337
+ resolve(result);
338
+ };
339
+ tx.onerror = () => {
340
+ reject(tx.error ?? new Error(`按前缀读取 ${storeName} 失败`));
341
+ };
342
+ tx.onabort = () => {
343
+ reject(tx.error ?? new Error(`按前缀读取 ${storeName} 被中止`));
344
+ };
345
+ });
346
+ }
347
+ /** 向指定仓库写入一条记录 */
348
+ async function idbPut(storeName, key, value) {
349
+ const db = await openDB();
350
+ return new Promise((resolve, reject) => {
351
+ const tx = db.transaction(storeName, 'readwrite');
352
+ tx.objectStore(storeName).put(value, key);
353
+ tx.oncomplete = () => {
354
+ resolve();
355
+ };
356
+ tx.onerror = () => {
357
+ reject(tx.error ?? new Error(`写入 ${storeName} 失败`));
358
+ };
359
+ tx.onabort = () => {
360
+ reject(tx.error ?? new Error(`写入 ${storeName} 被中止`));
361
+ };
362
+ });
363
+ }
364
+ /** 从指定仓库删除一条记录 */
365
+ async function idbDelete(storeName, key) {
366
+ const db = await openDB();
367
+ return new Promise((resolve, reject) => {
368
+ const tx = db.transaction(storeName, 'readwrite');
369
+ tx.objectStore(storeName).delete(key);
370
+ tx.oncomplete = () => {
371
+ resolve();
372
+ };
373
+ tx.onerror = () => {
374
+ reject(tx.error ?? new Error(`删除 ${storeName} 失败`));
375
+ };
376
+ tx.onabort = () => {
377
+ reject(tx.error ?? new Error(`删除 ${storeName} 被中止`));
378
+ };
379
+ });
380
+ }
381
+ // ── KeyStore 实现 ───────────────────────────────────────
382
+ /**
383
+ * 基于 IndexedDB 的密钥存储实现。
384
+ *
385
+ * 设计语义:
386
+ * - metadata 只保存普通 metadata 字段;
387
+ * - e2ee_prekeys / group_secrets 只保存到结构化 store;
388
+ * - loadMetadata() 返回的是运行时拼出来的 merged view;
389
+ * - 若检测到旧版本把结构化数据写进了 metadata,会自动迁移到结构化 store。
390
+ */
391
+ export class IndexedDBKeyStore {
392
+ static _aidTails = new Map();
393
+ /** 私钥加密种子;为空时降级为明文存储(向后兼容) */
394
+ _encryptionSeed;
395
+ constructor(opts) {
396
+ this._encryptionSeed = opts?.encryptionSeed;
397
+ }
398
+ async _withAidLock(aid, fn) {
399
+ const key = safeAid(aid);
400
+ const previous = IndexedDBKeyStore._aidTails.get(key) ?? Promise.resolve();
401
+ let release;
402
+ const current = new Promise((resolve) => {
403
+ release = resolve;
404
+ });
405
+ const tail = previous.catch(() => undefined).then(() => current);
406
+ IndexedDBKeyStore._aidTails.set(key, tail);
407
+ await previous.catch(() => undefined);
408
+ try {
409
+ return await fn();
410
+ }
411
+ finally {
412
+ release();
413
+ if (IndexedDBKeyStore._aidTails.get(key) === tail) {
414
+ IndexedDBKeyStore._aidTails.delete(key);
415
+ }
416
+ }
417
+ }
418
+ async listIdentities() {
419
+ const records = await idbGetAll(STORE_METADATA);
420
+ const aids = new Set();
421
+ for (const item of records) {
422
+ if (item.key.startsWith('_seq_|'))
423
+ continue;
424
+ const value = item.value;
425
+ const aid = typeof value === 'object' && value !== null && isRecord(value) && typeof value.aid === 'string' && value.aid
426
+ ? value.aid
427
+ : item.key;
428
+ aids.add(String(aid));
429
+ }
430
+ for (const item of await idbGetAll(STORE_KEY_PAIRS)) {
431
+ if (!item.key.startsWith('_seq_|'))
432
+ aids.add(item.key);
433
+ }
434
+ for (const item of await idbGetAll(STORE_CERTS)) {
435
+ if (item.key.startsWith('_seq_|'))
436
+ continue;
437
+ const [safe] = item.key.split('|', 1);
438
+ if (safe)
439
+ aids.add(safe);
440
+ }
441
+ return [...aids].sort();
442
+ }
443
+ // ── 密钥对 ──────────────────────────────────────────
444
+ async loadKeyPair(aid) {
445
+ const data = await idbGet(STORE_KEY_PAIRS, metadataStoreKey(aid));
446
+ if (!isRecord(data))
447
+ return null;
448
+ const result = deepClone(data);
449
+ // 如果存在加密私钥信封,尝试解密还原
450
+ const epk = result._encrypted_pk;
451
+ if (epk && typeof epk === 'object' && !Array.isArray(epk) && this._encryptionSeed) {
452
+ try {
453
+ const envelope = epk;
454
+ result.private_key_pem = await _decryptPEM(envelope, this._encryptionSeed);
455
+ delete result._encrypted_pk;
456
+ }
457
+ catch {
458
+ console.error(`[keystore] 解密 ${aid} 私钥失败,可能 encryptionSeed 不匹配`);
459
+ }
460
+ }
461
+ else if (
462
+ // 透明迁移:旧版明文数据自动加密回写
463
+ !epk && typeof result.private_key_pem === 'string' && this._encryptionSeed) {
464
+ try {
465
+ await this.saveKeyPair(aid, result);
466
+ }
467
+ catch {
468
+ // 迁移失败不影响读取
469
+ }
470
+ }
471
+ return result;
472
+ }
473
+ async saveKeyPair(aid, keyPair) {
474
+ const record = deepClone(keyPair);
475
+ // 如果配置了加密种子且包含明文私钥,加密后再存储
476
+ if (this._encryptionSeed && typeof record.private_key_pem === 'string') {
477
+ record._encrypted_pk = await _encryptPEM(record.private_key_pem, this._encryptionSeed);
478
+ delete record.private_key_pem;
479
+ }
480
+ await idbPut(STORE_KEY_PAIRS, metadataStoreKey(aid), record);
481
+ }
482
+ // ── 证书 ──────────────────────────────────────────
483
+ async loadCert(aid, certFingerprint) {
484
+ const normalized = normalizeCertFingerprint(certFingerprint);
485
+ if (normalized) {
486
+ const versioned = await idbGet(STORE_CERTS, certStoreKey(aid, normalized));
487
+ if (typeof versioned === 'string')
488
+ return versioned;
489
+ const active = await idbGet(STORE_CERTS, certStoreKey(aid));
490
+ if (typeof active === 'string' && await fingerprintFromCertPem(active) === normalized) {
491
+ return active;
492
+ }
493
+ return null;
494
+ }
495
+ const data = await idbGet(STORE_CERTS, certStoreKey(aid));
496
+ return typeof data === 'string' ? data : null;
497
+ }
498
+ async saveCert(aid, certPem, certFingerprint, opts) {
499
+ const normalized = normalizeCertFingerprint(certFingerprint);
500
+ if (normalized) {
501
+ await idbPut(STORE_CERTS, certStoreKey(aid, normalized), certPem);
502
+ if (opts?.makeActive) {
503
+ await idbPut(STORE_CERTS, certStoreKey(aid), certPem);
504
+ }
505
+ return;
506
+ }
507
+ await idbPut(STORE_CERTS, certStoreKey(aid), certPem);
508
+ }
509
+ // ── 实例态 ──────────────────────────────────────────
510
+ async loadInstanceState(aid, deviceId, slotId = '') {
511
+ return this._withAidLock(aid, async () => {
512
+ const data = await idbGet(STORE_INSTANCE_STATE, instanceStateStoreKey(aid, deviceId, slotId));
513
+ return isRecord(data) ? deepClone(data) : null;
514
+ });
515
+ }
516
+ async saveInstanceState(aid, deviceId, slotId, state) {
517
+ await this._withAidLock(aid, async () => {
518
+ await idbPut(STORE_INSTANCE_STATE, instanceStateStoreKey(aid, deviceId, slotId), deepClone(state));
519
+ });
520
+ }
521
+ async updateInstanceState(aid, deviceId, slotId, updater) {
522
+ return this._withAidLock(aid, async () => {
523
+ const currentRaw = await idbGet(STORE_INSTANCE_STATE, instanceStateStoreKey(aid, deviceId, slotId));
524
+ const current = isRecord(currentRaw) ? deepClone(currentRaw) : {};
525
+ const working = deepClone(current);
526
+ const updated = updater(working) ?? working;
527
+ await idbPut(STORE_INSTANCE_STATE, instanceStateStoreKey(aid, deviceId, slotId), deepClone(updated));
528
+ return deepClone(updated);
529
+ });
530
+ }
531
+ // ── 身份信息(组合操作) ──────────────────────────
532
+ async loadIdentity(aid) {
533
+ return this._withAidLock(aid, async () => {
534
+ const [keyPair, cert] = await Promise.all([
535
+ this._loadKeyPairUnlocked(aid),
536
+ this._loadCertUnlocked(aid),
537
+ ]);
538
+ // 直接读取 metadata KV(含旧数据迁移)
539
+ const metadataOnly = await this._migrateLegacyStructuredStateUnlocked(aid);
540
+ const hasMeta = Object.keys(metadataOnly).length > 0;
541
+ if (!keyPair && !cert && !hasMeta)
542
+ return null;
543
+ const identity = {};
544
+ if (hasMeta)
545
+ Object.assign(identity, metadataOnly);
546
+ if (keyPair)
547
+ Object.assign(identity, keyPair);
548
+ if (cert) {
549
+ // key/cert 公钥一致性校验:防止 cert 被意外覆盖
550
+ const localPubB64 = keyPair?.public_key_der_b64;
551
+ if (typeof localPubB64 === 'string' && localPubB64) {
552
+ const certSpkiB64 = extractSpkiB64FromCertPem(cert);
553
+ if (certSpkiB64 && certSpkiB64 !== localPubB64) {
554
+ console.error(`[keystore] 身份 ${aid} 的 key 公钥与 cert 公钥不匹配,丢弃 cert`);
555
+ }
556
+ else {
557
+ identity.cert = cert;
558
+ }
559
+ }
560
+ else {
561
+ identity.cert = cert;
562
+ }
563
+ }
564
+ return identity;
565
+ });
566
+ }
567
+ async saveIdentity(aid, identity) {
568
+ await this._withAidLock(aid, async () => {
569
+ const keyPairFields = {};
570
+ for (const key of ['private_key_pem', 'public_key_der_b64', 'curve']) {
571
+ if (key in identity)
572
+ keyPairFields[key] = identity[key];
573
+ }
574
+ if (Object.keys(keyPairFields).length > 0) {
575
+ await this._saveKeyPairUnlocked(aid, keyPairFields);
576
+ }
577
+ const cert = identity.cert;
578
+ if (typeof cert === 'string' && cert) {
579
+ await this._saveCertUnlocked(aid, cert);
580
+ }
581
+ const metadataFields = {};
582
+ for (const [key, value] of Object.entries(identity)) {
583
+ if (!['private_key_pem', 'public_key_der_b64', 'curve', 'cert'].includes(key)) {
584
+ metadataFields[key] = value;
585
+ }
586
+ }
587
+ // 增量保存 metadata 字段
588
+ if (Object.keys(metadataFields).length > 0) {
589
+ await this._replaceStructuredStateUnlocked(aid, metadataFields);
590
+ const plain = stripStructuredFields(metadataFields);
591
+ if (Object.keys(plain).length > 0) {
592
+ const current = await this._migrateLegacyStructuredStateUnlocked(aid);
593
+ Object.assign(current, plain);
594
+ await this._saveMetadataOnlyUnlocked(aid, current);
595
+ }
596
+ }
597
+ });
598
+ }
599
+ // ── 结构化 prekeys ───────────────────────────────────
600
+ async loadE2EEPrekeys(aid, deviceId) {
601
+ return this._withAidLock(aid, async () => {
602
+ await this._migrateLegacyStructuredStateUnlocked(aid);
603
+ return this._loadPrekeysUnlocked(aid, String(deviceId ?? '').trim());
604
+ });
605
+ }
606
+ async saveE2EEPrekey(aid, prekeyId, prekeyData, deviceId) {
607
+ await this._withAidLock(aid, async () => {
608
+ await this._migrateLegacyStructuredStateUnlocked(aid);
609
+ const record = deepClone(prekeyData);
610
+ record.prekey_id = prekeyId;
611
+ const normalizedDeviceId = String(deviceId ?? '').trim();
612
+ if (normalizedDeviceId) {
613
+ record.device_id = normalizedDeviceId;
614
+ }
615
+ else {
616
+ delete record.device_id;
617
+ }
618
+ const targetKey = prekeyStoreKey(aid, prekeyId, normalizedDeviceId);
619
+ for (const item of await idbGetAllByPrefix(STORE_PREKEYS, prekeyPrefix(aid))) {
620
+ if (!isRecord(item.value))
621
+ continue;
622
+ const existingPrekeyId = typeof item.value.prekey_id === 'string'
623
+ ? item.value.prekey_id
624
+ : '';
625
+ const existingDeviceId = String(item.value.device_id ?? '').trim();
626
+ if (existingPrekeyId === prekeyId && existingDeviceId === normalizedDeviceId && item.key !== targetKey) {
627
+ await idbDelete(STORE_PREKEYS, item.key);
628
+ }
629
+ }
630
+ await idbPut(STORE_PREKEYS, targetKey, record);
631
+ });
632
+ }
633
+ async cleanupE2EEPrekeys(aid, cutoffMs, keepLatest = 7, deviceId) {
634
+ return this._withAidLock(aid, async () => {
635
+ await this._migrateLegacyStructuredStateUnlocked(aid);
636
+ const normalizedDeviceId = String(deviceId ?? '').trim();
637
+ const prekeys = await this._loadPrekeysUnlocked(aid, normalizedDeviceId);
638
+ const retainedIds = new Set(Object.entries(prekeys)
639
+ .sort((left, right) => {
640
+ const leftMarker = Number(left[1].created_at ?? left[1].updated_at ?? left[1].expires_at ?? 0);
641
+ const rightMarker = Number(right[1].created_at ?? right[1].updated_at ?? right[1].expires_at ?? 0);
642
+ if (rightMarker !== leftMarker)
643
+ return rightMarker - leftMarker;
644
+ return right[0].localeCompare(left[0]);
645
+ })
646
+ .slice(0, keepLatest)
647
+ .map(([prekeyId]) => prekeyId));
648
+ const removed = Object.entries(prekeys)
649
+ .filter(([prekeyId, data]) => Number(data.created_at ?? data.updated_at ?? data.expires_at ?? 0) < cutoffMs
650
+ && !retainedIds.has(prekeyId))
651
+ .map(([prekeyId]) => prekeyId);
652
+ const removedIds = new Set(removed);
653
+ for (const item of await idbGetAllByPrefix(STORE_PREKEYS, prekeyPrefix(aid))) {
654
+ if (!isRecord(item.value))
655
+ continue;
656
+ const recordDeviceId = String(item.value.device_id ?? '').trim();
657
+ if (recordDeviceId !== normalizedDeviceId)
658
+ continue;
659
+ const prekeyId = typeof item.value.prekey_id === 'string' ? item.value.prekey_id : '';
660
+ if (removedIds.has(prekeyId))
661
+ await idbDelete(STORE_PREKEYS, item.key);
662
+ }
663
+ return removed;
664
+ });
665
+ }
666
+ // ── 结构化群组密钥 ─────────────────────────────────
667
+ async listGroupSecretIds(aid) {
668
+ return this._withAidLock(aid, async () => {
669
+ await this._migrateLegacyStructuredStateUnlocked(aid);
670
+ const ids = new Set();
671
+ for (const item of await idbGetAllByPrefix(STORE_GROUP_CURRENT, groupCurrentPrefix(aid))) {
672
+ const [, encodedGroupId] = item.key.split('|');
673
+ if (encodedGroupId)
674
+ ids.add(decodeURIComponent(encodedGroupId));
675
+ }
676
+ for (const item of await idbGetAllByPrefix(STORE_GROUP_OLD_EPOCHS, groupOldPrefix(aid))) {
677
+ const [, encodedGroupId] = item.key.split('|');
678
+ if (encodedGroupId)
679
+ ids.add(decodeURIComponent(encodedGroupId));
680
+ }
681
+ return [...ids].sort();
682
+ });
683
+ }
684
+ async cleanupGroupOldEpochsState(aid, groupId, cutoffMs) {
685
+ return this._withAidLock(aid, async () => {
686
+ await this._migrateLegacyStructuredStateUnlocked(aid);
687
+ let removed = 0;
688
+ for (const item of await idbGetAllByPrefix(STORE_GROUP_OLD_EPOCHS, groupOldPrefix(aid, groupId))) {
689
+ if (!isRecord(item.value))
690
+ continue;
691
+ if (Number(item.value.updated_at ?? 0) <= cutoffMs) {
692
+ await idbDelete(STORE_GROUP_OLD_EPOCHS, item.key);
693
+ removed += 1;
694
+ }
695
+ }
696
+ return removed;
697
+ });
698
+ }
699
+ async loadGroupSecretEpoch(aid, groupId, epoch) {
700
+ return this._withAidLock(aid, async () => {
701
+ await this._migrateLegacyStructuredStateUnlocked(aid);
702
+ const current = await idbGet(STORE_GROUP_CURRENT, groupCurrentStoreKey(aid, groupId));
703
+ if (isRecord(current)) {
704
+ const record = deepClone(current);
705
+ delete record.group_id;
706
+ if (epoch === undefined || epoch === null || Number(record.epoch ?? 0) === Number(epoch)) {
707
+ return record;
708
+ }
709
+ }
710
+ else if (epoch === undefined || epoch === null) {
711
+ return null;
712
+ }
713
+ const old = await idbGet(STORE_GROUP_OLD_EPOCHS, groupOldStoreKey(aid, groupId, Number(epoch)));
714
+ if (!isRecord(old))
715
+ return null;
716
+ const record = deepClone(old);
717
+ delete record.group_id;
718
+ return record;
719
+ });
720
+ }
721
+ async loadGroupSecretEpochs(aid, groupId) {
722
+ return this._withAidLock(aid, async () => {
723
+ await this._migrateLegacyStructuredStateUnlocked(aid);
724
+ const result = [];
725
+ const current = await idbGet(STORE_GROUP_CURRENT, groupCurrentStoreKey(aid, groupId));
726
+ if (isRecord(current)) {
727
+ const record = deepClone(current);
728
+ delete record.group_id;
729
+ result.push(record);
730
+ }
731
+ for (const item of await idbGetAllByPrefix(STORE_GROUP_OLD_EPOCHS, groupOldPrefix(aid, groupId))) {
732
+ if (!isRecord(item.value))
733
+ continue;
734
+ const record = deepClone(item.value);
735
+ delete record.group_id;
736
+ result.push(record);
737
+ }
738
+ return result;
739
+ });
740
+ }
741
+ async storeGroupSecretTransition(aid, groupId, opts) {
742
+ return this._withAidLock(aid, async () => {
743
+ await this._migrateLegacyStructuredStateUnlocked(aid);
744
+ return this._storeGroupSecretTransitionUnlocked(aid, groupId, opts);
745
+ });
746
+ }
747
+ async storeGroupSecretEpoch(aid, groupId, opts) {
748
+ return this._withAidLock(aid, async () => {
749
+ await this._migrateLegacyStructuredStateUnlocked(aid);
750
+ return this._storeGroupSecretEpochUnlocked(aid, groupId, opts);
751
+ });
752
+ }
753
+ async discardPendingGroupSecretState(aid, groupId, epoch, rotationId) {
754
+ return this._withAidLock(aid, async () => {
755
+ const rid = String(rotationId ?? '').trim();
756
+ if (!rid)
757
+ return false;
758
+ await this._migrateLegacyStructuredStateUnlocked(aid);
759
+ return this._discardPendingGroupSecretUnlocked(aid, groupId, epoch, rid);
760
+ });
761
+ }
762
+ async deleteGroupSecretState(aid, groupId) {
763
+ await this._withAidLock(aid, async () => {
764
+ // 删除 current epoch
765
+ await idbDelete(STORE_GROUP_CURRENT, groupCurrentStoreKey(aid, groupId));
766
+ // 删除所有 old epochs
767
+ for (const item of await idbGetAllByPrefix(STORE_GROUP_OLD_EPOCHS, groupOldPrefix(aid, groupId))) {
768
+ await idbDelete(STORE_GROUP_OLD_EPOCHS, item.key);
769
+ }
770
+ });
771
+ }
772
+ // ── E2EE Sessions ─────────────────────────────────────
773
+ async loadE2EESessions(aid) {
774
+ return this._withAidLock(aid, async () => {
775
+ await this._migrateLegacySessionsUnlocked(aid);
776
+ return this._loadSessionsUnlocked(aid);
777
+ });
778
+ }
779
+ async saveE2EESession(aid, sessionId, data) {
780
+ await this._withAidLock(aid, async () => {
781
+ await this._migrateLegacySessionsUnlocked(aid);
782
+ const record = deepClone(data);
783
+ record.session_id = sessionId;
784
+ await idbPut(STORE_SESSIONS, sessionStoreKey(aid, sessionId), record);
785
+ });
786
+ }
787
+ // ── 内部辅助 ─────────────────────────────────────────
788
+ async _loadKeyPairUnlocked(aid) {
789
+ const data = await idbGet(STORE_KEY_PAIRS, metadataStoreKey(aid));
790
+ if (!isRecord(data))
791
+ return null;
792
+ const result = deepClone(data);
793
+ // 如果存在加密私钥信封,尝试解密还原
794
+ if (isRecord(result._encrypted_pk) && this._encryptionSeed) {
795
+ try {
796
+ const envelope = result._encrypted_pk;
797
+ result.private_key_pem = await _decryptPEM(envelope, this._encryptionSeed);
798
+ delete result._encrypted_pk;
799
+ }
800
+ catch {
801
+ console.error(`[keystore] 解密 ${aid} 私钥失败,可能 encryptionSeed 不匹配`);
802
+ }
803
+ }
804
+ else if (
805
+ // 透明迁移:旧版明文数据自动加密回写
806
+ !isRecord(result._encrypted_pk) && typeof result.private_key_pem === 'string' && this._encryptionSeed) {
807
+ try {
808
+ await this._saveKeyPairUnlocked(aid, result);
809
+ }
810
+ catch {
811
+ // 迁移失败不影响读取
812
+ }
813
+ }
814
+ return result;
815
+ }
816
+ async _saveKeyPairUnlocked(aid, keyPair) {
817
+ const record = deepClone(keyPair);
818
+ // 如果配置了加密种子且包含明文私钥,加密后再存储
819
+ if (this._encryptionSeed && typeof record.private_key_pem === 'string') {
820
+ record._encrypted_pk = await _encryptPEM(record.private_key_pem, this._encryptionSeed);
821
+ delete record.private_key_pem;
822
+ }
823
+ await idbPut(STORE_KEY_PAIRS, metadataStoreKey(aid), record);
824
+ }
825
+ async _loadCertUnlocked(aid) {
826
+ const data = await idbGet(STORE_CERTS, certStoreKey(aid));
827
+ return typeof data === 'string' ? data : null;
828
+ }
829
+ async _saveCertUnlocked(aid, certPem) {
830
+ await idbPut(STORE_CERTS, certStoreKey(aid), certPem);
831
+ }
832
+ async _loadMetadataOnlyUnlocked(aid) {
833
+ const data = await idbGet(STORE_METADATA, metadataStoreKey(aid));
834
+ return isRecord(data) ? deepClone(data) : null;
835
+ }
836
+ async _replaceStructuredStateUnlocked(aid, metadata) {
837
+ // 增量保存:只处理传入的字段,不删除未传入的
838
+ if ('e2ee_prekeys' in metadata && isRecord(metadata.e2ee_prekeys)) {
839
+ await this._replacePrekeysUnlocked(aid, metadata.e2ee_prekeys, '');
840
+ }
841
+ if ('group_secrets' in metadata && isRecord(metadata.group_secrets)) {
842
+ await this._replaceGroupEntriesUnlocked(aid, metadata.group_secrets);
843
+ }
844
+ if ('e2ee_sessions' in metadata && Array.isArray(metadata.e2ee_sessions)) {
845
+ await this._replaceSessionsUnlocked(aid, metadata.e2ee_sessions);
846
+ }
847
+ }
848
+ async _saveMetadataOnlyUnlocked(aid, metadata) {
849
+ const plain = stripStructuredFields(metadata);
850
+ await idbPut(STORE_METADATA, metadataStoreKey(aid), plain);
851
+ }
852
+ async _migrateLegacyStructuredStateUnlocked(aid) {
853
+ const metadataOnly = (await this._loadMetadataOnlyUnlocked(aid)) ?? {};
854
+ const hasLegacyPrekeys = isRecord(metadataOnly.e2ee_prekeys);
855
+ const hasLegacyGroups = isRecord(metadataOnly.group_secrets);
856
+ const hasLegacySessions = Array.isArray(metadataOnly.e2ee_sessions) && metadataOnly.e2ee_sessions.length > 0;
857
+ if (!hasLegacyPrekeys && !hasLegacyGroups && !hasLegacySessions) {
858
+ return metadataOnly;
859
+ }
860
+ const cleaned = deepClone(metadataOnly);
861
+ if (hasLegacyPrekeys) {
862
+ const legacyPrekeys = metadataOnly.e2ee_prekeys;
863
+ const structuredPrekeys = await this._loadPrekeysUnlocked(aid);
864
+ let changed = false;
865
+ if (isRecord(legacyPrekeys)) {
866
+ for (const [prekeyId, rawData] of Object.entries(legacyPrekeys)) {
867
+ if (!isRecord(rawData) || structuredPrekeys[prekeyId])
868
+ continue;
869
+ if (!this._isPrekeyRecoverable(rawData))
870
+ continue;
871
+ structuredPrekeys[prekeyId] = deepClone(rawData);
872
+ changed = true;
873
+ }
874
+ }
875
+ if (changed) {
876
+ await this._replacePrekeysUnlocked(aid, structuredPrekeys);
877
+ }
878
+ delete cleaned.e2ee_prekeys;
879
+ }
880
+ if (hasLegacyGroups) {
881
+ const legacyGroups = metadataOnly.group_secrets;
882
+ const structuredGroups = await this._loadGroupEntriesUnlocked(aid);
883
+ let changed = false;
884
+ if (isRecord(legacyGroups)) {
885
+ for (const [groupId, rawEntry] of Object.entries(legacyGroups)) {
886
+ if (!isRecord(rawEntry))
887
+ continue;
888
+ const merged = this._mergeGroupEntryFromLegacy(structuredGroups[groupId], rawEntry);
889
+ if (!sameJson(structuredGroups[groupId] ?? null, merged)) {
890
+ if (Object.keys(merged).length > 0) {
891
+ structuredGroups[groupId] = merged;
892
+ changed = true;
893
+ }
894
+ }
895
+ }
896
+ }
897
+ if (changed) {
898
+ await this._replaceGroupEntriesUnlocked(aid, structuredGroups);
899
+ }
900
+ delete cleaned.group_secrets;
901
+ }
902
+ if (hasLegacySessions) {
903
+ const legacySessions = metadataOnly.e2ee_sessions;
904
+ for (const session of legacySessions) {
905
+ const sid = typeof session.session_id === 'string' ? session.session_id : '';
906
+ if (!sid)
907
+ continue;
908
+ const record = deepClone(session);
909
+ record.session_id = sid;
910
+ await idbPut(STORE_SESSIONS, sessionStoreKey(aid, sid), record);
911
+ }
912
+ delete cleaned.e2ee_sessions;
913
+ }
914
+ await this._saveMetadataOnlyUnlocked(aid, cleaned);
915
+ return cleaned;
916
+ }
917
+ async _loadPrekeysUnlocked(aid, deviceId = '') {
918
+ const items = await idbGetAllByPrefix(STORE_PREKEYS, prekeyPrefix(aid));
919
+ const prefix = prekeyPrefix(aid);
920
+ const result = {};
921
+ const normalizedDeviceId = String(deviceId ?? '').trim();
922
+ for (const item of items) {
923
+ if (!item.key.startsWith(prefix) || !isRecord(item.value))
924
+ continue;
925
+ const recordDeviceId = String(item.value.device_id ?? '').trim();
926
+ if (recordDeviceId !== normalizedDeviceId)
927
+ continue;
928
+ const prekeyId = typeof item.value.prekey_id === 'string'
929
+ ? item.value.prekey_id
930
+ : item.key.slice(prefix.length);
931
+ const record = deepClone(item.value);
932
+ delete record.prekey_id;
933
+ delete record.device_id;
934
+ const canonicalKey = prekeyStoreKey(aid, prekeyId, normalizedDeviceId);
935
+ if (!(prekeyId in result) || item.key === canonicalKey) {
936
+ result[prekeyId] = record;
937
+ }
938
+ }
939
+ return result;
940
+ }
941
+ async _replacePrekeysUnlocked(aid, prekeys, deviceId = '') {
942
+ const desired = new Set(Object.keys(prekeys));
943
+ const normalizedDeviceId = String(deviceId ?? '').trim();
944
+ for (const item of await idbGetAllByPrefix(STORE_PREKEYS, prekeyPrefix(aid))) {
945
+ if (!isRecord(item.value))
946
+ continue;
947
+ const recordDeviceId = String(item.value.device_id ?? '').trim();
948
+ if (recordDeviceId !== normalizedDeviceId)
949
+ continue;
950
+ const prekeyId = typeof item.value.prekey_id === 'string'
951
+ ? item.value.prekey_id
952
+ : '';
953
+ const canonicalKey = prekeyStoreKey(aid, prekeyId, normalizedDeviceId);
954
+ if (!desired.has(prekeyId) || item.key !== canonicalKey) {
955
+ await idbDelete(STORE_PREKEYS, item.key);
956
+ }
957
+ }
958
+ for (const [prekeyId, prekeyData] of Object.entries(prekeys)) {
959
+ const record = deepClone(prekeyData);
960
+ if (normalizedDeviceId) {
961
+ record.device_id = normalizedDeviceId;
962
+ }
963
+ else {
964
+ delete record.device_id;
965
+ }
966
+ await idbPut(STORE_PREKEYS, prekeyStoreKey(aid, prekeyId, normalizedDeviceId), {
967
+ ...record,
968
+ prekey_id: prekeyId,
969
+ });
970
+ }
971
+ }
972
+ async _loadGroupEntriesUnlocked(aid) {
973
+ const result = {};
974
+ for (const item of await idbGetAllByPrefix(STORE_GROUP_CURRENT, groupCurrentPrefix(aid))) {
975
+ if (!isRecord(item.value))
976
+ continue;
977
+ const groupId = typeof item.value.group_id === 'string'
978
+ ? item.value.group_id
979
+ : item.key.slice(groupCurrentPrefix(aid).length);
980
+ const record = deepClone(item.value);
981
+ delete record.group_id;
982
+ result[groupId] = record;
983
+ }
984
+ for (const item of await idbGetAllByPrefix(STORE_GROUP_OLD_EPOCHS, groupOldPrefix(aid))) {
985
+ if (!isRecord(item.value))
986
+ continue;
987
+ const groupId = typeof item.value.group_id === 'string'
988
+ ? item.value.group_id
989
+ : '';
990
+ if (!groupId)
991
+ continue;
992
+ const record = deepClone(item.value);
993
+ delete record.group_id;
994
+ const entry = result[groupId] ?? {};
995
+ const oldEpochs = Array.isArray(entry.old_epochs)
996
+ ? entry.old_epochs
997
+ : [];
998
+ oldEpochs.push(record);
999
+ oldEpochs.sort((a, b) => Number(a.epoch ?? 0) - Number(b.epoch ?? 0));
1000
+ entry.old_epochs = oldEpochs;
1001
+ result[groupId] = entry;
1002
+ }
1003
+ return result;
1004
+ }
1005
+ async _replaceGroupEntriesUnlocked(aid, groups) {
1006
+ const desiredGroups = new Set(Object.keys(groups));
1007
+ for (const item of await idbGetAllByPrefix(STORE_GROUP_CURRENT, groupCurrentPrefix(aid))) {
1008
+ const groupId = isRecord(item.value) && typeof item.value.group_id === 'string'
1009
+ ? item.value.group_id
1010
+ : item.key.slice(groupCurrentPrefix(aid).length);
1011
+ if (!desiredGroups.has(groupId)) {
1012
+ await idbDelete(STORE_GROUP_CURRENT, item.key);
1013
+ }
1014
+ }
1015
+ for (const item of await idbGetAllByPrefix(STORE_GROUP_OLD_EPOCHS, groupOldPrefix(aid))) {
1016
+ if (!isRecord(item.value))
1017
+ continue;
1018
+ const groupId = typeof item.value.group_id === 'string' ? item.value.group_id : '';
1019
+ if (!groupId || desiredGroups.has(groupId))
1020
+ continue;
1021
+ await idbDelete(STORE_GROUP_OLD_EPOCHS, item.key);
1022
+ }
1023
+ for (const [groupId, rawEntry] of Object.entries(groups)) {
1024
+ if (!isRecord(rawEntry))
1025
+ continue;
1026
+ await this._saveSingleGroupEntryUnlocked(aid, groupId, rawEntry);
1027
+ }
1028
+ }
1029
+ async _saveSingleGroupEntryUnlocked(aid, groupId, entry) {
1030
+ for (const item of await idbGetAllByPrefix(STORE_GROUP_OLD_EPOCHS, groupOldPrefix(aid, groupId))) {
1031
+ if (!isRecord(item.value))
1032
+ continue;
1033
+ const existingGroupId = typeof item.value.group_id === 'string' ? item.value.group_id : '';
1034
+ if (existingGroupId === groupId) {
1035
+ await idbDelete(STORE_GROUP_OLD_EPOCHS, item.key);
1036
+ }
1037
+ }
1038
+ const current = deepClone(entry);
1039
+ const oldEpochs = Array.isArray(current.old_epochs)
1040
+ ? current.old_epochs
1041
+ : [];
1042
+ delete current.old_epochs;
1043
+ if (typeof current.epoch === 'number') {
1044
+ await idbPut(STORE_GROUP_CURRENT, groupCurrentStoreKey(aid, groupId), {
1045
+ ...current,
1046
+ group_id: groupId,
1047
+ });
1048
+ }
1049
+ else {
1050
+ await idbDelete(STORE_GROUP_CURRENT, groupCurrentStoreKey(aid, groupId));
1051
+ }
1052
+ for (const oldEpoch of oldEpochs) {
1053
+ if (!isRecord(oldEpoch) || typeof oldEpoch.epoch !== 'number')
1054
+ continue;
1055
+ await idbPut(STORE_GROUP_OLD_EPOCHS, groupOldStoreKey(aid, groupId, oldEpoch.epoch), {
1056
+ ...deepClone(oldEpoch),
1057
+ group_id: groupId,
1058
+ });
1059
+ }
1060
+ }
1061
+ async _storeGroupSecretTransitionUnlocked(aid, groupId, opts) {
1062
+ const currentKey = groupCurrentStoreKey(aid, groupId);
1063
+ const currentRaw = await idbGet(STORE_GROUP_CURRENT, currentKey);
1064
+ const now = Date.now();
1065
+ const epoch = Number(opts.epoch);
1066
+ const members = [...(opts.memberAids ?? [])].map((item) => String(item)).sort();
1067
+ const pendingRotationId = String(opts.pendingRotationId ?? '').trim();
1068
+ if (isRecord(currentRaw)) {
1069
+ const current = deepClone(currentRaw);
1070
+ delete current.group_id;
1071
+ const localEpoch = Number(current.epoch ?? 0);
1072
+ if (epoch < localEpoch)
1073
+ return false;
1074
+ if (epoch === localEpoch && typeof current.secret === 'string') {
1075
+ if (current.secret !== opts.secret) {
1076
+ if (String(current.pending_rotation_id ?? '').trim()) {
1077
+ await idbPut(STORE_GROUP_CURRENT, currentKey, {
1078
+ ...this._buildGroupCurrentRecord(groupId, opts, members, now),
1079
+ });
1080
+ return true;
1081
+ }
1082
+ return false;
1083
+ }
1084
+ const updated = { ...current };
1085
+ let changed = false;
1086
+ const oldMembers = Array.isArray(updated.member_aids) ? updated.member_aids.map(String).sort() : [];
1087
+ if (members.length > 0 && JSON.stringify(oldMembers) !== JSON.stringify(members)) {
1088
+ updated.member_aids = members;
1089
+ updated.commitment = opts.commitment;
1090
+ updated.updated_at = now;
1091
+ changed = true;
1092
+ }
1093
+ if (opts.epochChain !== undefined && updated.epoch_chain !== opts.epochChain) {
1094
+ updated.epoch_chain = opts.epochChain;
1095
+ updated.updated_at = now;
1096
+ changed = true;
1097
+ }
1098
+ if (opts.epochChainUnverified === true) {
1099
+ if (updated.epoch_chain_unverified !== true) {
1100
+ updated.epoch_chain_unverified = true;
1101
+ changed = true;
1102
+ }
1103
+ if (opts.epochChainUnverifiedReason && updated.epoch_chain_unverified_reason !== opts.epochChainUnverifiedReason) {
1104
+ updated.epoch_chain_unverified_reason = opts.epochChainUnverifiedReason;
1105
+ changed = true;
1106
+ }
1107
+ }
1108
+ else if (opts.epochChainUnverified === false) {
1109
+ if ('epoch_chain_unverified' in updated || 'epoch_chain_unverified_reason' in updated) {
1110
+ delete updated.epoch_chain_unverified;
1111
+ delete updated.epoch_chain_unverified_reason;
1112
+ changed = true;
1113
+ }
1114
+ }
1115
+ if (pendingRotationId && updated.pending_rotation_id !== pendingRotationId) {
1116
+ updated.pending_rotation_id = pendingRotationId;
1117
+ updated.pending_created_at = now;
1118
+ changed = true;
1119
+ }
1120
+ if (!pendingRotationId && updated.pending_rotation_id) {
1121
+ delete updated.pending_rotation_id;
1122
+ delete updated.pending_created_at;
1123
+ changed = true;
1124
+ }
1125
+ if (changed) {
1126
+ await idbPut(STORE_GROUP_CURRENT, currentKey, { ...updated, group_id: groupId });
1127
+ }
1128
+ return true;
1129
+ }
1130
+ if (localEpoch !== epoch) {
1131
+ const oldEntry = { ...current };
1132
+ oldEntry.expires_at = Number(current.updated_at ?? now) + opts.oldEpochRetentionMs;
1133
+ await idbPut(STORE_GROUP_OLD_EPOCHS, groupOldStoreKey(aid, groupId, localEpoch), {
1134
+ ...oldEntry,
1135
+ group_id: groupId,
1136
+ });
1137
+ }
1138
+ }
1139
+ await idbPut(STORE_GROUP_CURRENT, currentKey, {
1140
+ ...this._buildGroupCurrentRecord(groupId, opts, members, now),
1141
+ });
1142
+ return true;
1143
+ }
1144
+ async _storeGroupSecretEpochUnlocked(aid, groupId, opts) {
1145
+ const now = Date.now();
1146
+ const epoch = Number(opts.epoch);
1147
+ const members = [...(opts.memberAids ?? [])].map((item) => String(item)).sort();
1148
+ const pendingRotationId = String(opts.pendingRotationId ?? '').trim();
1149
+ const currentKey = groupCurrentStoreKey(aid, groupId);
1150
+ const currentRaw = await idbGet(STORE_GROUP_CURRENT, currentKey);
1151
+ const newRecord = this._buildGroupCurrentRecord(groupId, opts, members, now);
1152
+ if (!isRecord(currentRaw)) {
1153
+ await idbPut(STORE_GROUP_CURRENT, currentKey, newRecord);
1154
+ return true;
1155
+ }
1156
+ const current = deepClone(currentRaw);
1157
+ delete current.group_id;
1158
+ const localEpoch = Number(current.epoch ?? 0);
1159
+ if (epoch > localEpoch)
1160
+ return false;
1161
+ if (epoch === localEpoch) {
1162
+ if (typeof current.secret === 'string' && current.secret !== opts.secret) {
1163
+ if (!String(current.pending_rotation_id ?? '').trim())
1164
+ return false;
1165
+ await idbPut(STORE_GROUP_CURRENT, currentKey, newRecord);
1166
+ return true;
1167
+ }
1168
+ const updated = { ...current };
1169
+ let changed = false;
1170
+ const oldMembers = Array.isArray(updated.member_aids) ? updated.member_aids.map(String).sort() : [];
1171
+ if (members.length > 0 && JSON.stringify(oldMembers) !== JSON.stringify(members)) {
1172
+ updated.member_aids = members;
1173
+ updated.commitment = opts.commitment;
1174
+ updated.updated_at = now;
1175
+ changed = true;
1176
+ }
1177
+ if (opts.epochChain !== undefined && updated.epoch_chain !== opts.epochChain) {
1178
+ updated.epoch_chain = opts.epochChain;
1179
+ updated.updated_at = now;
1180
+ changed = true;
1181
+ }
1182
+ if (opts.epochChainUnverified === true) {
1183
+ if (updated.epoch_chain_unverified !== true) {
1184
+ updated.epoch_chain_unverified = true;
1185
+ changed = true;
1186
+ }
1187
+ if (opts.epochChainUnverifiedReason && updated.epoch_chain_unverified_reason !== opts.epochChainUnverifiedReason) {
1188
+ updated.epoch_chain_unverified_reason = opts.epochChainUnverifiedReason;
1189
+ changed = true;
1190
+ }
1191
+ }
1192
+ else if (opts.epochChainUnverified === false) {
1193
+ if ('epoch_chain_unverified' in updated || 'epoch_chain_unverified_reason' in updated) {
1194
+ delete updated.epoch_chain_unverified;
1195
+ delete updated.epoch_chain_unverified_reason;
1196
+ changed = true;
1197
+ }
1198
+ }
1199
+ if (pendingRotationId && updated.pending_rotation_id !== pendingRotationId) {
1200
+ updated.pending_rotation_id = pendingRotationId;
1201
+ updated.pending_created_at = now;
1202
+ changed = true;
1203
+ }
1204
+ if (!pendingRotationId && updated.pending_rotation_id) {
1205
+ delete updated.pending_rotation_id;
1206
+ delete updated.pending_created_at;
1207
+ changed = true;
1208
+ }
1209
+ if (changed)
1210
+ await idbPut(STORE_GROUP_CURRENT, currentKey, { ...updated, group_id: groupId });
1211
+ return true;
1212
+ }
1213
+ const oldKey = groupOldStoreKey(aid, groupId, epoch);
1214
+ const oldRaw = await idbGet(STORE_GROUP_OLD_EPOCHS, oldKey);
1215
+ if (isRecord(oldRaw)) {
1216
+ const old = deepClone(oldRaw);
1217
+ if (typeof old.secret === 'string' && old.secret !== opts.secret)
1218
+ return false;
1219
+ }
1220
+ await idbPut(STORE_GROUP_OLD_EPOCHS, oldKey, {
1221
+ ...newRecord,
1222
+ epoch,
1223
+ expires_at: now + opts.oldEpochRetentionMs,
1224
+ });
1225
+ return true;
1226
+ }
1227
+ async _discardPendingGroupSecretUnlocked(aid, groupId, epoch, rotationId) {
1228
+ const currentKey = groupCurrentStoreKey(aid, groupId);
1229
+ const currentRaw = await idbGet(STORE_GROUP_CURRENT, currentKey);
1230
+ if (!isRecord(currentRaw))
1231
+ return false;
1232
+ const current = deepClone(currentRaw);
1233
+ if (Number(current.epoch ?? 0) !== Number(epoch))
1234
+ return false;
1235
+ if (String(current.pending_rotation_id ?? '').trim() !== rotationId)
1236
+ return false;
1237
+ let restored = null;
1238
+ let restoredKey = '';
1239
+ for (const item of await idbGetAllByPrefix(STORE_GROUP_OLD_EPOCHS, groupOldPrefix(aid, groupId))) {
1240
+ if (!isRecord(item.value))
1241
+ continue;
1242
+ const old = deepClone(item.value);
1243
+ const oldEpoch = Number(old.epoch ?? 0);
1244
+ if (oldEpoch >= Number(epoch) || !old.secret)
1245
+ continue;
1246
+ if (!restored || oldEpoch > Number(restored.epoch ?? 0)) {
1247
+ restored = old;
1248
+ restoredKey = item.key;
1249
+ }
1250
+ }
1251
+ if (!restored) {
1252
+ await idbDelete(STORE_GROUP_CURRENT, currentKey);
1253
+ for (const item of await idbGetAllByPrefix(STORE_GROUP_OLD_EPOCHS, groupOldPrefix(aid, groupId))) {
1254
+ await idbDelete(STORE_GROUP_OLD_EPOCHS, item.key);
1255
+ }
1256
+ return true;
1257
+ }
1258
+ delete restored.group_id;
1259
+ delete restored.expires_at;
1260
+ await idbPut(STORE_GROUP_CURRENT, currentKey, { ...restored, group_id: groupId, updated_at: Date.now() });
1261
+ if (restoredKey)
1262
+ await idbDelete(STORE_GROUP_OLD_EPOCHS, restoredKey);
1263
+ return true;
1264
+ }
1265
+ _buildGroupCurrentRecord(groupId, opts, memberAids, now) {
1266
+ const record = {
1267
+ group_id: groupId,
1268
+ epoch: opts.epoch,
1269
+ secret: opts.secret,
1270
+ commitment: opts.commitment,
1271
+ member_aids: memberAids,
1272
+ updated_at: now,
1273
+ };
1274
+ if (opts.epochChain !== undefined)
1275
+ record.epoch_chain = opts.epochChain;
1276
+ const pendingRotationId = String(opts.pendingRotationId ?? '').trim();
1277
+ if (pendingRotationId) {
1278
+ record.pending_rotation_id = pendingRotationId;
1279
+ record.pending_created_at = now;
1280
+ }
1281
+ if (opts.epochChainUnverified === true) {
1282
+ record.epoch_chain_unverified = true;
1283
+ if (opts.epochChainUnverifiedReason)
1284
+ record.epoch_chain_unverified_reason = opts.epochChainUnverifiedReason;
1285
+ }
1286
+ return record;
1287
+ }
1288
+ _mergeGroupEntryFromLegacy(existing, incoming) {
1289
+ const oldByEpoch = new Map();
1290
+ let current = existing && typeof existing.epoch === 'number'
1291
+ ? deepClone(Object.fromEntries(Object.entries(existing).filter(([key]) => key !== 'old_epochs')))
1292
+ : null;
1293
+ if (existing && Array.isArray(existing.old_epochs)) {
1294
+ for (const rawOld of existing.old_epochs) {
1295
+ if (typeof rawOld.epoch !== 'number')
1296
+ continue;
1297
+ oldByEpoch.set(rawOld.epoch, deepClone(rawOld));
1298
+ }
1299
+ }
1300
+ if (typeof incoming.epoch === 'number' && this._isGroupEpochRecoverable(incoming)) {
1301
+ const incomingCurrent = deepClone(Object.fromEntries(Object.entries(incoming).filter(([key]) => key !== 'old_epochs')));
1302
+ if (!current) {
1303
+ current = incomingCurrent;
1304
+ }
1305
+ else if (incomingCurrent.epoch > current.epoch) {
1306
+ oldByEpoch.set(current.epoch, this._preferNewerGroupEpochRecord(oldByEpoch.get(current.epoch), current));
1307
+ current = incomingCurrent;
1308
+ }
1309
+ else if (incomingCurrent.epoch === current.epoch) {
1310
+ current = this._preferNewerGroupEpochRecord(current, incomingCurrent);
1311
+ }
1312
+ else {
1313
+ oldByEpoch.set(incomingCurrent.epoch, this._preferNewerGroupEpochRecord(oldByEpoch.get(incomingCurrent.epoch), incomingCurrent));
1314
+ }
1315
+ }
1316
+ const incomingOldEpochs = Array.isArray(incoming.old_epochs)
1317
+ ? incoming.old_epochs
1318
+ : [];
1319
+ for (const rawOld of incomingOldEpochs) {
1320
+ if (typeof rawOld.epoch !== 'number' || !this._isGroupEpochRecoverable(rawOld))
1321
+ continue;
1322
+ oldByEpoch.set(rawOld.epoch, this._preferNewerGroupEpochRecord(oldByEpoch.get(rawOld.epoch), rawOld));
1323
+ }
1324
+ const merged = current ? deepClone(current) : {};
1325
+ if (current && typeof current.epoch === 'number') {
1326
+ oldByEpoch.delete(current.epoch);
1327
+ }
1328
+ if (oldByEpoch.size > 0) {
1329
+ merged.old_epochs = [...oldByEpoch.entries()]
1330
+ .sort(([a], [b]) => a - b)
1331
+ .map(([, value]) => deepClone(value));
1332
+ }
1333
+ return merged;
1334
+ }
1335
+ _preferNewerGroupEpochRecord(existing, incoming) {
1336
+ if (!existing)
1337
+ return deepClone(incoming);
1338
+ return Number(incoming.updated_at ?? 0) > Number(existing.updated_at ?? 0)
1339
+ ? deepClone(incoming)
1340
+ : deepClone(existing);
1341
+ }
1342
+ _isUnexpiredRecord(record, fallbackKey) {
1343
+ const expiresAt = typeof record.expires_at === 'number' ? record.expires_at : null;
1344
+ if (expiresAt !== null)
1345
+ return expiresAt >= Date.now();
1346
+ const marker = typeof record[fallbackKey] === 'number' ? record[fallbackKey] : null;
1347
+ return marker !== null && marker + STRUCTURED_RECOVERY_RETENTION_MS >= Date.now();
1348
+ }
1349
+ _isPrekeyRecoverable(record) {
1350
+ return this._isUnexpiredRecord(record, 'created_at');
1351
+ }
1352
+ _isGroupEpochRecoverable(record) {
1353
+ // 空字符串 secret 不视为可恢复的有效记录
1354
+ const secret = record.secret;
1355
+ if (!secret || (typeof secret === 'string' && secret.length === 0))
1356
+ return false;
1357
+ return this._isUnexpiredRecord(record, 'updated_at');
1358
+ }
1359
+ // ── Sessions 内部辅助 ─────────────────────────────────
1360
+ async _loadSessionsUnlocked(aid) {
1361
+ const items = await idbGetAllByPrefix(STORE_SESSIONS, sessionPrefix(aid));
1362
+ const result = [];
1363
+ for (const item of items) {
1364
+ if (!isRecord(item.value))
1365
+ continue;
1366
+ result.push(deepClone(item.value));
1367
+ }
1368
+ return result;
1369
+ }
1370
+ async _replaceSessionsUnlocked(aid, sessions) {
1371
+ const desired = new Set();
1372
+ for (const session of sessions) {
1373
+ const sid = typeof session.session_id === 'string' ? session.session_id : '';
1374
+ if (!sid)
1375
+ continue;
1376
+ desired.add(sid);
1377
+ const record = deepClone(session);
1378
+ record.session_id = sid;
1379
+ await idbPut(STORE_SESSIONS, sessionStoreKey(aid, sid), record);
1380
+ }
1381
+ for (const item of await idbGetAllByPrefix(STORE_SESSIONS, sessionPrefix(aid))) {
1382
+ if (!isRecord(item.value))
1383
+ continue;
1384
+ const sid = typeof item.value.session_id === 'string'
1385
+ ? item.value.session_id
1386
+ : item.key.slice(sessionPrefix(aid).length);
1387
+ if (!desired.has(sid)) {
1388
+ await idbDelete(STORE_SESSIONS, item.key);
1389
+ }
1390
+ }
1391
+ }
1392
+ async _migrateLegacySessionsUnlocked(aid) {
1393
+ const metadataOnly = (await this._loadMetadataOnlyUnlocked(aid)) ?? {};
1394
+ if (!Array.isArray(metadataOnly.e2ee_sessions) || metadataOnly.e2ee_sessions.length === 0) {
1395
+ return;
1396
+ }
1397
+ for (const session of metadataOnly.e2ee_sessions) {
1398
+ const sid = typeof session.session_id === 'string' ? session.session_id : '';
1399
+ if (!sid)
1400
+ continue;
1401
+ const record = deepClone(session);
1402
+ record.session_id = sid;
1403
+ await idbPut(STORE_SESSIONS, sessionStoreKey(aid, sid), record);
1404
+ }
1405
+ const cleaned = deepClone(metadataOnly);
1406
+ delete cleaned.e2ee_sessions;
1407
+ await this._saveMetadataOnlyUnlocked(aid, cleaned);
1408
+ }
1409
+ // ── Seq Tracker ─────────────────────────────────────────
1410
+ async saveSeq(aid, deviceId, slotId, namespace, contiguousSeq) {
1411
+ const key = seqTrackerStoreKey(aid, deviceId, slotId, namespace);
1412
+ await idbPut(STORE_INSTANCE_STATE, key, { contiguous_seq: contiguousSeq, updated_at: Date.now() });
1413
+ }
1414
+ async loadSeq(aid, deviceId, slotId, namespace) {
1415
+ const key = seqTrackerStoreKey(aid, deviceId, slotId, namespace);
1416
+ const data = await idbGet(STORE_INSTANCE_STATE, key);
1417
+ return data?.contiguous_seq ?? 0;
1418
+ }
1419
+ async loadAllSeqs(aid, deviceId, slotId) {
1420
+ const prefix = seqTrackerPrefix(aid, deviceId, slotId);
1421
+ const items = await idbGetAllByPrefix(STORE_INSTANCE_STATE, prefix);
1422
+ const result = {};
1423
+ for (const item of items) {
1424
+ if (!item.key.startsWith(prefix))
1425
+ continue;
1426
+ const ns = decodeURIComponent(item.key.slice(prefix.length));
1427
+ if (ns && typeof item.value?.contiguous_seq === 'number') {
1428
+ result[ns] = item.value.contiguous_seq;
1429
+ }
1430
+ }
1431
+ return result;
1432
+ }
1433
+ }
1434
+ //# sourceMappingURL=indexeddb.js.map