@agentunion/fastaun-browser 0.5.0 → 0.5.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +56 -0
- package/_packed_docs/CHANGELOG-validators.md +144 -0
- package/_packed_docs/CHANGELOG.md +56 -0
- package/_packed_docs/INDEX.md +198 -181
- package/_packed_docs/KITE_DOCS_GUIDE.md +26 -23
- package/_packed_docs/cli/AUN-CLI/350/256/276/350/256/241/346/226/207/346/241/243.md +4 -3
- package/_packed_docs/cli/CLI/346/211/213/345/206/214.md +331 -0
- package/_packed_docs/protocol/08-AUN-E2EE-Group.md +296 -902
- package/_packed_docs/protocol/10-Group-/345/255/220/345/215/217/350/256/256.md +64 -114
- package/_packed_docs/protocol/11-Storage-/345/255/220/345/215/217/350/256/256.md +7 -1
- package/_packed_docs/protocol/16-/347/263/273/347/273/237/347/233/256/345/275/225/344/277/235/346/212/244/346/226/271/346/241/210.md +177 -0
- package/_packed_docs/protocol/README.md +2 -1
- package/_packed_docs/protocol/index.md +8 -3
- package/_packed_docs/sdk/05-E2EE/345/212/240/345/257/206/351/200/232/344/277/241.md +4 -252
- package/_packed_docs/sdk/06-API/346/211/213/345/206/214.md +483 -457
- package/_packed_docs/sdk/09-collab-rpc-manual.md +581 -550
- package/_packed_docs/sdk/09-group-rpc-manual.md +248 -335
- package/_packed_docs/sdk/09-storage-rpc-manual.md +56 -19
- package/_packed_docs/sdk/AUN_DOCS_GUIDE.md +13 -13
- package/_packed_docs/sdk/INDEX.md +14 -14
- package/dist/bundle.js +1560 -1159
- package/dist/client/delivery.d.ts +13 -2
- package/dist/client/delivery.d.ts.map +1 -1
- package/dist/client/delivery.js +251 -46
- package/dist/client/delivery.js.map +1 -1
- package/dist/client/group-state.d.ts.map +1 -1
- package/dist/client/group-state.js +36 -14
- package/dist/client/group-state.js.map +1 -1
- package/dist/client/lifecycle.js +2 -2
- package/dist/client/lifecycle.js.map +1 -1
- package/dist/client/rpc-pipeline.d.ts +1 -0
- package/dist/client/rpc-pipeline.d.ts.map +1 -1
- package/dist/client/rpc-pipeline.js +166 -50
- package/dist/client/rpc-pipeline.js.map +1 -1
- package/dist/client/v2-e2ee.d.ts +14 -1
- package/dist/client/v2-e2ee.d.ts.map +1 -1
- package/dist/client/v2-e2ee.js +300 -121
- package/dist/client/v2-e2ee.js.map +1 -1
- package/dist/client.d.ts +5 -4
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js +187 -46
- package/dist/client.js.map +1 -1
- package/dist/collab/client.d.ts +8 -0
- package/dist/collab/client.d.ts.map +1 -1
- package/dist/collab/client.js +12 -0
- package/dist/collab/client.js.map +1 -1
- package/dist/errors.d.ts +0 -20
- package/dist/errors.d.ts.map +1 -1
- package/dist/errors.js +8 -51
- package/dist/errors.js.map +1 -1
- package/dist/facades.d.ts +9 -4
- package/dist/facades.d.ts.map +1 -1
- package/dist/facades.js +192 -31
- package/dist/facades.js.map +1 -1
- package/dist/group-fs.d.ts +17 -0
- package/dist/group-fs.d.ts.map +1 -1
- package/dist/group-fs.js +54 -11
- package/dist/group-fs.js.map +1 -1
- package/dist/group-id.d.ts +9 -12
- package/dist/group-id.d.ts.map +1 -1
- package/dist/group-id.js +41 -63
- package/dist/group-id.js.map +1 -1
- package/dist/index.d.ts +4 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4 -1
- package/dist/index.js.map +1 -1
- package/dist/keystore/index.d.ts +2 -54
- package/dist/keystore/index.d.ts.map +1 -1
- package/dist/keystore/indexeddb-identity-store.d.ts +3 -0
- package/dist/keystore/indexeddb-identity-store.d.ts.map +1 -1
- package/dist/keystore/indexeddb-identity-store.js +65 -0
- package/dist/keystore/indexeddb-identity-store.js.map +1 -1
- package/dist/keystore/indexeddb-shared.d.ts +3 -17
- package/dist/keystore/indexeddb-shared.d.ts.map +1 -1
- package/dist/keystore/indexeddb-shared.js +4 -47
- package/dist/keystore/indexeddb-shared.js.map +1 -1
- package/dist/keystore/indexeddb-token-store.d.ts +1 -64
- package/dist/keystore/indexeddb-token-store.d.ts.map +1 -1
- package/dist/keystore/indexeddb-token-store.js +45 -774
- package/dist/keystore/indexeddb-token-store.js.map +1 -1
- package/dist/logger.d.ts +2 -0
- package/dist/logger.d.ts.map +1 -1
- package/dist/logger.js +4 -0
- package/dist/logger.js.map +1 -1
- package/dist/storage/lowlevel.d.ts +9 -1
- package/dist/storage/lowlevel.d.ts.map +1 -1
- package/dist/storage/lowlevel.js +12 -1
- package/dist/storage/lowlevel.js.map +1 -1
- package/dist/storage/vfs.d.ts +22 -0
- package/dist/storage/vfs.d.ts.map +1 -1
- package/dist/storage/vfs.js +54 -0
- package/dist/storage/vfs.js.map +1 -1
- package/dist/tools/cross-sdk-agent.js +336 -49
- package/dist/tools/cross-sdk-agent.js.map +1 -1
- package/dist/transport.d.ts +2 -0
- package/dist/transport.d.ts.map +1 -1
- package/dist/transport.js +96 -3
- package/dist/transport.js.map +1 -1
- package/dist/types.d.ts +39 -56
- package/dist/types.d.ts.map +1 -1
- package/dist/v2/session/session.d.ts +2 -0
- package/dist/v2/session/session.d.ts.map +1 -1
- package/dist/v2/session/session.js +58 -22
- package/dist/v2/session/session.js.map +1 -1
- package/dist/v2/state/commitment.d.ts +1 -1
- package/dist/v2/state/commitment.d.ts.map +1 -1
- package/dist/v2/state/commitment.js +5 -3
- package/dist/v2/state/commitment.js.map +1 -1
- package/dist/validators.d.ts +35 -0
- package/dist/validators.d.ts.map +1 -0
- package/dist/validators.js +127 -0
- package/dist/validators.js.map +1 -0
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +1 -1
- package/_packed_docs/collab-gateway-boundary-test-report.md +0 -164
|
@@ -1,18 +1,38 @@
|
|
|
1
1
|
// ── IndexedDB TokenStore 实现 ──────────────────────────────
|
|
2
|
-
// 不含私钥操作:证书、实例态、seq
|
|
2
|
+
// 不含私钥操作:证书、实例态、seq、信任根、metadata、agent.md 缓存和群组状态。
|
|
3
3
|
// AUNClient / AuthFlow 持有此类型。
|
|
4
4
|
//
|
|
5
5
|
// 与 IndexedDBIdentityStore 共用同一 IndexedDB 数据库(aun-keystore),
|
|
6
6
|
// 但二者之间无任何代码依赖——共享基础设施集中在 indexeddb-shared.ts。
|
|
7
7
|
import { pemToArrayBuffer } from '../crypto.js';
|
|
8
|
-
import {
|
|
8
|
+
import { convertToGroupAid } from '../group-id.js';
|
|
9
|
+
import { _noopLog, STORE_CERTS, STORE_METADATA, STORE_INSTANCE_STATE, STORE_GROUP_STATE, STORE_AGENT_MD_CACHE, safeAid, encodePart, deepClone, isRecord, metadataStoreKey, normalizeCertFingerprint, fingerprintFromCertPem, certStoreKey, instanceStateStoreKey, seqTrackerPrefix, seqTrackerStoreKey, agentMdCachePrefix, agentMdCacheStoreKey, stripStructuredFields, normalizeAgentMdCacheRecord, mergeAgentMdCacheRecord, idbGet, idbGetAllByPrefix, idbPut, idbDelete, } from './indexeddb-shared.js';
|
|
10
|
+
function groupLookupCandidates(groupId, localIssuer = '') {
|
|
11
|
+
const raw = String(groupId ?? '').trim();
|
|
12
|
+
if (!raw)
|
|
13
|
+
return [];
|
|
14
|
+
const normalized = convertToGroupAid(raw, { localIssuer });
|
|
15
|
+
const candidates = [];
|
|
16
|
+
const add = (value) => {
|
|
17
|
+
const trimmed = String(value ?? '').trim();
|
|
18
|
+
if (trimmed && !candidates.includes(trimmed))
|
|
19
|
+
candidates.push(trimmed);
|
|
20
|
+
};
|
|
21
|
+
add(raw);
|
|
22
|
+
add(normalized);
|
|
23
|
+
const dotIdx = normalized.indexOf('.');
|
|
24
|
+
if (dotIdx > 0 && dotIdx < normalized.length - 1) {
|
|
25
|
+
const base = normalized.slice(0, dotIdx);
|
|
26
|
+
const issuer = normalized.slice(dotIdx + 1);
|
|
27
|
+
add(`group.${issuer}/${base}`);
|
|
28
|
+
add(`${base}@${issuer}`);
|
|
29
|
+
if (localIssuer && issuer === localIssuer.replace(/^\.+|\.+$/g, '').toLowerCase())
|
|
30
|
+
add(base);
|
|
31
|
+
}
|
|
32
|
+
return candidates;
|
|
33
|
+
}
|
|
9
34
|
/**
|
|
10
35
|
* 基于 IndexedDB 的 TokenStore 实现(不含私钥操作)。
|
|
11
|
-
*
|
|
12
|
-
* 设计语义:
|
|
13
|
-
* - metadata 只保存普通 metadata 字段;
|
|
14
|
-
* - e2ee_prekeys / group_secrets 只保存到结构化 store;
|
|
15
|
-
* - 若检测到旧版本把结构化数据写进了 metadata,会自动迁移到结构化 store。
|
|
16
36
|
*/
|
|
17
37
|
export class IndexedDBTokenStore {
|
|
18
38
|
_log = _noopLog;
|
|
@@ -111,204 +131,6 @@ export class IndexedDBTokenStore {
|
|
|
111
131
|
return deepClone(updated);
|
|
112
132
|
});
|
|
113
133
|
}
|
|
114
|
-
// ── 结构化 prekeys ───────────────────────────────────
|
|
115
|
-
async loadE2EEPrekeys(aid, deviceId) {
|
|
116
|
-
return this._withAidLock(aid, async () => {
|
|
117
|
-
await this._migrateLegacyStructuredStateUnlocked(aid);
|
|
118
|
-
return this._loadPrekeysUnlocked(aid, String(deviceId ?? '').trim());
|
|
119
|
-
});
|
|
120
|
-
}
|
|
121
|
-
/**
|
|
122
|
-
* 按 prekey_id 单点查询本地 prekey(O(log N) IndexedDB primary-key 查找)。
|
|
123
|
-
*
|
|
124
|
-
* 解密入站消息时,envelope 里只带 prekey_id,没有 device_id。优先用 IndexedDB
|
|
125
|
-
* 主键直查;命中则 O(log N),未命中再回退到前缀扫描,保证跨 device_id 也能找到。
|
|
126
|
-
*/
|
|
127
|
-
async loadE2EEPrekeyById(aid, prekeyId) {
|
|
128
|
-
return this._withAidLock(aid, async () => {
|
|
129
|
-
await this._migrateLegacyStructuredStateUnlocked(aid);
|
|
130
|
-
return this._loadPrekeyByIdUnlocked(aid, prekeyId);
|
|
131
|
-
});
|
|
132
|
-
}
|
|
133
|
-
async saveE2EEPrekey(aid, prekeyId, prekeyData, deviceId) {
|
|
134
|
-
await this._withAidLock(aid, async () => {
|
|
135
|
-
await this._migrateLegacyStructuredStateUnlocked(aid);
|
|
136
|
-
const record = deepClone(prekeyData);
|
|
137
|
-
record.prekey_id = prekeyId;
|
|
138
|
-
const normalizedDeviceId = String(deviceId ?? '').trim();
|
|
139
|
-
if (normalizedDeviceId) {
|
|
140
|
-
record.device_id = normalizedDeviceId;
|
|
141
|
-
}
|
|
142
|
-
else {
|
|
143
|
-
delete record.device_id;
|
|
144
|
-
}
|
|
145
|
-
const targetKey = prekeyStoreKey(aid, prekeyId, normalizedDeviceId);
|
|
146
|
-
for (const item of await idbGetAllByPrefix(STORE_PREKEYS, prekeyPrefix(aid))) {
|
|
147
|
-
if (!isRecord(item.value))
|
|
148
|
-
continue;
|
|
149
|
-
const existingPrekeyId = typeof item.value.prekey_id === 'string'
|
|
150
|
-
? item.value.prekey_id
|
|
151
|
-
: '';
|
|
152
|
-
const existingDeviceId = String(item.value.device_id ?? '').trim();
|
|
153
|
-
if (existingPrekeyId === prekeyId && existingDeviceId === normalizedDeviceId && item.key !== targetKey) {
|
|
154
|
-
await idbDelete(STORE_PREKEYS, item.key);
|
|
155
|
-
}
|
|
156
|
-
}
|
|
157
|
-
await idbPut(STORE_PREKEYS, targetKey, record);
|
|
158
|
-
});
|
|
159
|
-
}
|
|
160
|
-
async cleanupE2EEPrekeys(aid, cutoffMs, keepLatest = 7, deviceId) {
|
|
161
|
-
return this._withAidLock(aid, async () => {
|
|
162
|
-
await this._migrateLegacyStructuredStateUnlocked(aid);
|
|
163
|
-
const normalizedDeviceId = String(deviceId ?? '').trim();
|
|
164
|
-
const prekeys = await this._loadPrekeysUnlocked(aid, normalizedDeviceId);
|
|
165
|
-
const retainedIds = new Set(Object.entries(prekeys)
|
|
166
|
-
.sort((left, right) => {
|
|
167
|
-
const leftMarker = Number(left[1].created_at ?? left[1].updated_at ?? left[1].expires_at ?? 0);
|
|
168
|
-
const rightMarker = Number(right[1].created_at ?? right[1].updated_at ?? right[1].expires_at ?? 0);
|
|
169
|
-
if (rightMarker !== leftMarker)
|
|
170
|
-
return rightMarker - leftMarker;
|
|
171
|
-
return right[0].localeCompare(left[0]);
|
|
172
|
-
})
|
|
173
|
-
.slice(0, keepLatest)
|
|
174
|
-
.map(([prekeyId]) => prekeyId));
|
|
175
|
-
const removed = Object.entries(prekeys)
|
|
176
|
-
.filter(([prekeyId, data]) => Number(data.created_at ?? data.updated_at ?? data.expires_at ?? 0) < cutoffMs
|
|
177
|
-
&& !retainedIds.has(prekeyId))
|
|
178
|
-
.map(([prekeyId]) => prekeyId);
|
|
179
|
-
const removedIds = new Set(removed);
|
|
180
|
-
for (const item of await idbGetAllByPrefix(STORE_PREKEYS, prekeyPrefix(aid))) {
|
|
181
|
-
if (!isRecord(item.value))
|
|
182
|
-
continue;
|
|
183
|
-
const recordDeviceId = String(item.value.device_id ?? '').trim();
|
|
184
|
-
if (recordDeviceId !== normalizedDeviceId)
|
|
185
|
-
continue;
|
|
186
|
-
const prekeyId = typeof item.value.prekey_id === 'string' ? item.value.prekey_id : '';
|
|
187
|
-
if (removedIds.has(prekeyId))
|
|
188
|
-
await idbDelete(STORE_PREKEYS, item.key);
|
|
189
|
-
}
|
|
190
|
-
return removed;
|
|
191
|
-
});
|
|
192
|
-
}
|
|
193
|
-
// ── 结构化群组密钥 ─────────────────────────────────
|
|
194
|
-
async listGroupSecretIds(aid) {
|
|
195
|
-
return this._withAidLock(aid, async () => {
|
|
196
|
-
await this._migrateLegacyStructuredStateUnlocked(aid);
|
|
197
|
-
const ids = new Set();
|
|
198
|
-
for (const item of await idbGetAllByPrefix(STORE_GROUP_CURRENT, groupCurrentPrefix(aid))) {
|
|
199
|
-
const [, encodedGroupId] = item.key.split('|');
|
|
200
|
-
if (encodedGroupId)
|
|
201
|
-
ids.add(decodeURIComponent(encodedGroupId));
|
|
202
|
-
}
|
|
203
|
-
for (const item of await idbGetAllByPrefix(STORE_GROUP_OLD_EPOCHS, groupOldPrefix(aid))) {
|
|
204
|
-
const [, encodedGroupId] = item.key.split('|');
|
|
205
|
-
if (encodedGroupId)
|
|
206
|
-
ids.add(decodeURIComponent(encodedGroupId));
|
|
207
|
-
}
|
|
208
|
-
return [...ids].sort();
|
|
209
|
-
});
|
|
210
|
-
}
|
|
211
|
-
async cleanupGroupOldEpochsState(aid, groupId, cutoffMs) {
|
|
212
|
-
return this._withAidLock(aid, async () => {
|
|
213
|
-
await this._migrateLegacyStructuredStateUnlocked(aid);
|
|
214
|
-
let removed = 0;
|
|
215
|
-
for (const item of await idbGetAllByPrefix(STORE_GROUP_OLD_EPOCHS, groupOldPrefix(aid, groupId))) {
|
|
216
|
-
if (!isRecord(item.value))
|
|
217
|
-
continue;
|
|
218
|
-
if (Number(item.value.updated_at ?? 0) <= cutoffMs) {
|
|
219
|
-
await idbDelete(STORE_GROUP_OLD_EPOCHS, item.key);
|
|
220
|
-
removed += 1;
|
|
221
|
-
}
|
|
222
|
-
}
|
|
223
|
-
return removed;
|
|
224
|
-
});
|
|
225
|
-
}
|
|
226
|
-
async loadGroupSecretEpoch(aid, groupId, epoch) {
|
|
227
|
-
return this._withAidLock(aid, async () => {
|
|
228
|
-
await this._migrateLegacyStructuredStateUnlocked(aid);
|
|
229
|
-
const current = await idbGet(STORE_GROUP_CURRENT, groupCurrentStoreKey(aid, groupId));
|
|
230
|
-
if (isRecord(current)) {
|
|
231
|
-
const record = deepClone(current);
|
|
232
|
-
delete record.group_id;
|
|
233
|
-
if (epoch === undefined || epoch === null || Number(record.epoch ?? 0) === Number(epoch)) {
|
|
234
|
-
return record;
|
|
235
|
-
}
|
|
236
|
-
}
|
|
237
|
-
else if (epoch === undefined || epoch === null) {
|
|
238
|
-
return null;
|
|
239
|
-
}
|
|
240
|
-
const old = await idbGet(STORE_GROUP_OLD_EPOCHS, groupOldStoreKey(aid, groupId, Number(epoch)));
|
|
241
|
-
if (!isRecord(old))
|
|
242
|
-
return null;
|
|
243
|
-
const record = deepClone(old);
|
|
244
|
-
delete record.group_id;
|
|
245
|
-
return record;
|
|
246
|
-
});
|
|
247
|
-
}
|
|
248
|
-
async loadGroupSecretEpochs(aid, groupId) {
|
|
249
|
-
return this._withAidLock(aid, async () => {
|
|
250
|
-
await this._migrateLegacyStructuredStateUnlocked(aid);
|
|
251
|
-
const result = [];
|
|
252
|
-
const current = await idbGet(STORE_GROUP_CURRENT, groupCurrentStoreKey(aid, groupId));
|
|
253
|
-
if (isRecord(current)) {
|
|
254
|
-
const record = deepClone(current);
|
|
255
|
-
delete record.group_id;
|
|
256
|
-
result.push(record);
|
|
257
|
-
}
|
|
258
|
-
for (const item of await idbGetAllByPrefix(STORE_GROUP_OLD_EPOCHS, groupOldPrefix(aid, groupId))) {
|
|
259
|
-
if (!isRecord(item.value))
|
|
260
|
-
continue;
|
|
261
|
-
const record = deepClone(item.value);
|
|
262
|
-
delete record.group_id;
|
|
263
|
-
result.push(record);
|
|
264
|
-
}
|
|
265
|
-
return result;
|
|
266
|
-
});
|
|
267
|
-
}
|
|
268
|
-
async storeGroupSecretTransition(aid, groupId, opts) {
|
|
269
|
-
return this._withAidLock(aid, async () => {
|
|
270
|
-
await this._migrateLegacyStructuredStateUnlocked(aid);
|
|
271
|
-
return this._storeGroupSecretTransitionUnlocked(aid, groupId, opts);
|
|
272
|
-
});
|
|
273
|
-
}
|
|
274
|
-
async storeGroupSecretEpoch(aid, groupId, opts) {
|
|
275
|
-
return this._withAidLock(aid, async () => {
|
|
276
|
-
await this._migrateLegacyStructuredStateUnlocked(aid);
|
|
277
|
-
return this._storeGroupSecretEpochUnlocked(aid, groupId, opts);
|
|
278
|
-
});
|
|
279
|
-
}
|
|
280
|
-
async discardPendingGroupSecretState(aid, groupId, epoch, rotationId) {
|
|
281
|
-
return this._withAidLock(aid, async () => {
|
|
282
|
-
const rid = String(rotationId ?? '').trim();
|
|
283
|
-
if (!rid)
|
|
284
|
-
return false;
|
|
285
|
-
await this._migrateLegacyStructuredStateUnlocked(aid);
|
|
286
|
-
return this._discardPendingGroupSecretUnlocked(aid, groupId, epoch, rid);
|
|
287
|
-
});
|
|
288
|
-
}
|
|
289
|
-
async deleteGroupSecretState(aid, groupId) {
|
|
290
|
-
await this._withAidLock(aid, async () => {
|
|
291
|
-
await idbDelete(STORE_GROUP_CURRENT, groupCurrentStoreKey(aid, groupId));
|
|
292
|
-
for (const item of await idbGetAllByPrefix(STORE_GROUP_OLD_EPOCHS, groupOldPrefix(aid, groupId))) {
|
|
293
|
-
await idbDelete(STORE_GROUP_OLD_EPOCHS, item.key);
|
|
294
|
-
}
|
|
295
|
-
});
|
|
296
|
-
}
|
|
297
|
-
// ── E2EE Sessions ─────────────────────────────────────
|
|
298
|
-
async loadE2EESessions(aid) {
|
|
299
|
-
return this._withAidLock(aid, async () => {
|
|
300
|
-
await this._migrateLegacySessionsUnlocked(aid);
|
|
301
|
-
return this._loadSessionsUnlocked(aid);
|
|
302
|
-
});
|
|
303
|
-
}
|
|
304
|
-
async saveE2EESession(aid, sessionId, data) {
|
|
305
|
-
await this._withAidLock(aid, async () => {
|
|
306
|
-
await this._migrateLegacySessionsUnlocked(aid);
|
|
307
|
-
const record = deepClone(data);
|
|
308
|
-
record.session_id = sessionId;
|
|
309
|
-
await idbPut(STORE_SESSIONS, sessionStoreKey(aid, sessionId), record);
|
|
310
|
-
});
|
|
311
|
-
}
|
|
312
134
|
/**
|
|
313
135
|
* 读取单个 metadata KV(如 gateway_url 等跨进程缓存项)。
|
|
314
136
|
* 不存在时返回空字符串,与 Python keystore 的 get_metadata 语义保持一致。
|
|
@@ -432,19 +254,24 @@ export class IndexedDBTokenStore {
|
|
|
432
254
|
});
|
|
433
255
|
}
|
|
434
256
|
async loadGroupState(groupId) {
|
|
435
|
-
const
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
group_id:
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
257
|
+
for (const candidate of groupLookupCandidates(groupId)) {
|
|
258
|
+
const key = encodePart(candidate);
|
|
259
|
+
const data = await idbGet(STORE_GROUP_STATE, key);
|
|
260
|
+
if (!data || typeof data.state_version !== 'number')
|
|
261
|
+
continue;
|
|
262
|
+
const storedGroupId = typeof data.group_id === 'string' ? data.group_id : candidate;
|
|
263
|
+
return {
|
|
264
|
+
group_id: storedGroupId,
|
|
265
|
+
group_aid: convertToGroupAid(storedGroupId),
|
|
266
|
+
state_version: data.state_version,
|
|
267
|
+
state_hash: data.state_hash ?? '',
|
|
268
|
+
key_epoch: data.key_epoch ?? 0,
|
|
269
|
+
membership_json: data.membership_json ?? '[]',
|
|
270
|
+
policy_json: data.policy_json ?? '{}',
|
|
271
|
+
updated_at: data.updated_at ?? 0,
|
|
272
|
+
};
|
|
273
|
+
}
|
|
274
|
+
return null;
|
|
448
275
|
}
|
|
449
276
|
// ── Trust Root Storage(信任根证书存储) ─────────────────
|
|
450
277
|
// 浏览器环境无文件系统,统一存入 STORE_INSTANCE_STATE 并以特定前缀的 key 区分。
|
|
@@ -526,561 +353,5 @@ export class IndexedDBTokenStore {
|
|
|
526
353
|
const plain = stripStructuredFields(metadata);
|
|
527
354
|
await idbPut(STORE_METADATA, metadataStoreKey(aid), plain);
|
|
528
355
|
}
|
|
529
|
-
async _migrateLegacyStructuredStateUnlocked(aid) {
|
|
530
|
-
const metadataOnly = (await this._loadMetadataOnlyUnlocked(aid)) ?? {};
|
|
531
|
-
const hasLegacyPrekeys = isRecord(metadataOnly.e2ee_prekeys);
|
|
532
|
-
const hasLegacyGroups = isRecord(metadataOnly.group_secrets);
|
|
533
|
-
const hasLegacySessions = Array.isArray(metadataOnly.e2ee_sessions) && metadataOnly.e2ee_sessions.length > 0;
|
|
534
|
-
if (!hasLegacyPrekeys && !hasLegacyGroups && !hasLegacySessions) {
|
|
535
|
-
return metadataOnly;
|
|
536
|
-
}
|
|
537
|
-
const cleaned = deepClone(metadataOnly);
|
|
538
|
-
if (hasLegacyPrekeys) {
|
|
539
|
-
const legacyPrekeys = metadataOnly.e2ee_prekeys;
|
|
540
|
-
const structuredPrekeys = await this._loadPrekeysUnlocked(aid);
|
|
541
|
-
let changed = false;
|
|
542
|
-
if (isRecord(legacyPrekeys)) {
|
|
543
|
-
for (const [prekeyId, rawData] of Object.entries(legacyPrekeys)) {
|
|
544
|
-
if (!isRecord(rawData) || structuredPrekeys[prekeyId])
|
|
545
|
-
continue;
|
|
546
|
-
if (!this._isPrekeyRecoverable(rawData))
|
|
547
|
-
continue;
|
|
548
|
-
structuredPrekeys[prekeyId] = deepClone(rawData);
|
|
549
|
-
changed = true;
|
|
550
|
-
}
|
|
551
|
-
}
|
|
552
|
-
if (changed) {
|
|
553
|
-
await this._replacePrekeysUnlocked(aid, structuredPrekeys);
|
|
554
|
-
}
|
|
555
|
-
delete cleaned.e2ee_prekeys;
|
|
556
|
-
}
|
|
557
|
-
if (hasLegacyGroups) {
|
|
558
|
-
const legacyGroups = metadataOnly.group_secrets;
|
|
559
|
-
const structuredGroups = await this._loadGroupEntriesUnlocked(aid);
|
|
560
|
-
let changed = false;
|
|
561
|
-
if (isRecord(legacyGroups)) {
|
|
562
|
-
for (const [groupId, rawEntry] of Object.entries(legacyGroups)) {
|
|
563
|
-
if (!isRecord(rawEntry))
|
|
564
|
-
continue;
|
|
565
|
-
const merged = this._mergeGroupEntryFromLegacy(structuredGroups[groupId], rawEntry);
|
|
566
|
-
if (!sameJson(structuredGroups[groupId] ?? null, merged)) {
|
|
567
|
-
if (Object.keys(merged).length > 0) {
|
|
568
|
-
structuredGroups[groupId] = merged;
|
|
569
|
-
changed = true;
|
|
570
|
-
}
|
|
571
|
-
}
|
|
572
|
-
}
|
|
573
|
-
}
|
|
574
|
-
if (changed) {
|
|
575
|
-
await this._replaceGroupEntriesUnlocked(aid, structuredGroups);
|
|
576
|
-
}
|
|
577
|
-
delete cleaned.group_secrets;
|
|
578
|
-
}
|
|
579
|
-
if (hasLegacySessions) {
|
|
580
|
-
const legacySessions = metadataOnly.e2ee_sessions;
|
|
581
|
-
for (const session of legacySessions) {
|
|
582
|
-
const sid = typeof session.session_id === 'string' ? session.session_id : '';
|
|
583
|
-
if (!sid)
|
|
584
|
-
continue;
|
|
585
|
-
const record = deepClone(session);
|
|
586
|
-
record.session_id = sid;
|
|
587
|
-
await idbPut(STORE_SESSIONS, sessionStoreKey(aid, sid), record);
|
|
588
|
-
}
|
|
589
|
-
delete cleaned.e2ee_sessions;
|
|
590
|
-
}
|
|
591
|
-
await this._saveMetadataOnlyUnlocked(aid, cleaned);
|
|
592
|
-
return cleaned;
|
|
593
|
-
}
|
|
594
|
-
async _loadPrekeysUnlocked(aid, deviceId = '') {
|
|
595
|
-
const items = await idbGetAllByPrefix(STORE_PREKEYS, prekeyPrefix(aid));
|
|
596
|
-
const prefix = prekeyPrefix(aid);
|
|
597
|
-
const result = {};
|
|
598
|
-
const normalizedDeviceId = String(deviceId ?? '').trim();
|
|
599
|
-
for (const item of items) {
|
|
600
|
-
if (!item.key.startsWith(prefix) || !isRecord(item.value))
|
|
601
|
-
continue;
|
|
602
|
-
const recordDeviceId = String(item.value.device_id ?? '').trim();
|
|
603
|
-
if (recordDeviceId !== normalizedDeviceId)
|
|
604
|
-
continue;
|
|
605
|
-
const prekeyId = typeof item.value.prekey_id === 'string'
|
|
606
|
-
? item.value.prekey_id
|
|
607
|
-
: item.key.slice(prefix.length);
|
|
608
|
-
const record = deepClone(item.value);
|
|
609
|
-
delete record.prekey_id;
|
|
610
|
-
delete record.device_id;
|
|
611
|
-
const canonicalKey = prekeyStoreKey(aid, prekeyId, normalizedDeviceId);
|
|
612
|
-
if (!(prekeyId in result) || item.key === canonicalKey) {
|
|
613
|
-
result[prekeyId] = record;
|
|
614
|
-
}
|
|
615
|
-
}
|
|
616
|
-
return result;
|
|
617
|
-
}
|
|
618
|
-
async _loadPrekeyByIdUnlocked(aid, prekeyId) {
|
|
619
|
-
if (!prekeyId)
|
|
620
|
-
return null;
|
|
621
|
-
const directKey = prekeyStoreKey(aid, prekeyId, '');
|
|
622
|
-
const direct = await idbGet(STORE_PREKEYS, directKey);
|
|
623
|
-
if (isRecord(direct)) {
|
|
624
|
-
const record = deepClone(direct);
|
|
625
|
-
delete record.prekey_id;
|
|
626
|
-
delete record.device_id;
|
|
627
|
-
return record;
|
|
628
|
-
}
|
|
629
|
-
for (const item of await idbGetAllByPrefix(STORE_PREKEYS, prekeyPrefix(aid))) {
|
|
630
|
-
if (!isRecord(item.value))
|
|
631
|
-
continue;
|
|
632
|
-
const itemPrekeyId = typeof item.value.prekey_id === 'string' ? item.value.prekey_id : '';
|
|
633
|
-
if (itemPrekeyId !== prekeyId)
|
|
634
|
-
continue;
|
|
635
|
-
const record = deepClone(item.value);
|
|
636
|
-
delete record.prekey_id;
|
|
637
|
-
delete record.device_id;
|
|
638
|
-
return record;
|
|
639
|
-
}
|
|
640
|
-
return null;
|
|
641
|
-
}
|
|
642
|
-
async _replacePrekeysUnlocked(aid, prekeys, deviceId = '') {
|
|
643
|
-
const desired = new Set(Object.keys(prekeys));
|
|
644
|
-
const normalizedDeviceId = String(deviceId ?? '').trim();
|
|
645
|
-
for (const item of await idbGetAllByPrefix(STORE_PREKEYS, prekeyPrefix(aid))) {
|
|
646
|
-
if (!isRecord(item.value))
|
|
647
|
-
continue;
|
|
648
|
-
const recordDeviceId = String(item.value.device_id ?? '').trim();
|
|
649
|
-
if (recordDeviceId !== normalizedDeviceId)
|
|
650
|
-
continue;
|
|
651
|
-
const prekeyId = typeof item.value.prekey_id === 'string' ? item.value.prekey_id : '';
|
|
652
|
-
const canonicalKey = prekeyStoreKey(aid, prekeyId, normalizedDeviceId);
|
|
653
|
-
if (!desired.has(prekeyId) || item.key !== canonicalKey) {
|
|
654
|
-
await idbDelete(STORE_PREKEYS, item.key);
|
|
655
|
-
}
|
|
656
|
-
}
|
|
657
|
-
for (const [prekeyId, prekeyData] of Object.entries(prekeys)) {
|
|
658
|
-
const record = deepClone(prekeyData);
|
|
659
|
-
if (normalizedDeviceId) {
|
|
660
|
-
record.device_id = normalizedDeviceId;
|
|
661
|
-
}
|
|
662
|
-
else {
|
|
663
|
-
delete record.device_id;
|
|
664
|
-
}
|
|
665
|
-
await idbPut(STORE_PREKEYS, prekeyStoreKey(aid, prekeyId, normalizedDeviceId), { ...record, prekey_id: prekeyId });
|
|
666
|
-
}
|
|
667
|
-
}
|
|
668
|
-
async _loadGroupEntriesUnlocked(aid) {
|
|
669
|
-
const result = {};
|
|
670
|
-
for (const item of await idbGetAllByPrefix(STORE_GROUP_CURRENT, groupCurrentPrefix(aid))) {
|
|
671
|
-
if (!isRecord(item.value))
|
|
672
|
-
continue;
|
|
673
|
-
const groupId = typeof item.value.group_id === 'string'
|
|
674
|
-
? item.value.group_id
|
|
675
|
-
: item.key.slice(groupCurrentPrefix(aid).length);
|
|
676
|
-
const record = deepClone(item.value);
|
|
677
|
-
delete record.group_id;
|
|
678
|
-
result[groupId] = record;
|
|
679
|
-
}
|
|
680
|
-
for (const item of await idbGetAllByPrefix(STORE_GROUP_OLD_EPOCHS, groupOldPrefix(aid))) {
|
|
681
|
-
if (!isRecord(item.value))
|
|
682
|
-
continue;
|
|
683
|
-
const groupId = typeof item.value.group_id === 'string' ? item.value.group_id : '';
|
|
684
|
-
if (!groupId)
|
|
685
|
-
continue;
|
|
686
|
-
const record = deepClone(item.value);
|
|
687
|
-
delete record.group_id;
|
|
688
|
-
const entry = result[groupId] ?? {};
|
|
689
|
-
const oldEpochs = Array.isArray(entry.old_epochs) ? entry.old_epochs : [];
|
|
690
|
-
oldEpochs.push(record);
|
|
691
|
-
oldEpochs.sort((a, b) => Number(a.epoch ?? 0) - Number(b.epoch ?? 0));
|
|
692
|
-
entry.old_epochs = oldEpochs;
|
|
693
|
-
result[groupId] = entry;
|
|
694
|
-
}
|
|
695
|
-
return result;
|
|
696
|
-
}
|
|
697
|
-
async _replaceGroupEntriesUnlocked(aid, groups) {
|
|
698
|
-
const desiredGroups = new Set(Object.keys(groups));
|
|
699
|
-
for (const item of await idbGetAllByPrefix(STORE_GROUP_CURRENT, groupCurrentPrefix(aid))) {
|
|
700
|
-
const groupId = isRecord(item.value) && typeof item.value.group_id === 'string'
|
|
701
|
-
? item.value.group_id
|
|
702
|
-
: item.key.slice(groupCurrentPrefix(aid).length);
|
|
703
|
-
if (!desiredGroups.has(groupId))
|
|
704
|
-
await idbDelete(STORE_GROUP_CURRENT, item.key);
|
|
705
|
-
}
|
|
706
|
-
for (const item of await idbGetAllByPrefix(STORE_GROUP_OLD_EPOCHS, groupOldPrefix(aid))) {
|
|
707
|
-
if (!isRecord(item.value))
|
|
708
|
-
continue;
|
|
709
|
-
const groupId = typeof item.value.group_id === 'string' ? item.value.group_id : '';
|
|
710
|
-
if (!groupId || desiredGroups.has(groupId))
|
|
711
|
-
continue;
|
|
712
|
-
await idbDelete(STORE_GROUP_OLD_EPOCHS, item.key);
|
|
713
|
-
}
|
|
714
|
-
for (const [groupId, rawEntry] of Object.entries(groups)) {
|
|
715
|
-
if (!isRecord(rawEntry))
|
|
716
|
-
continue;
|
|
717
|
-
await this._saveSingleGroupEntryUnlocked(aid, groupId, rawEntry);
|
|
718
|
-
}
|
|
719
|
-
}
|
|
720
|
-
async _saveSingleGroupEntryUnlocked(aid, groupId, entry) {
|
|
721
|
-
for (const item of await idbGetAllByPrefix(STORE_GROUP_OLD_EPOCHS, groupOldPrefix(aid, groupId))) {
|
|
722
|
-
if (!isRecord(item.value))
|
|
723
|
-
continue;
|
|
724
|
-
if (typeof item.value.group_id === 'string' && item.value.group_id === groupId) {
|
|
725
|
-
await idbDelete(STORE_GROUP_OLD_EPOCHS, item.key);
|
|
726
|
-
}
|
|
727
|
-
}
|
|
728
|
-
const current = deepClone(entry);
|
|
729
|
-
const oldEpochs = Array.isArray(current.old_epochs) ? current.old_epochs : [];
|
|
730
|
-
delete current.old_epochs;
|
|
731
|
-
if (typeof current.epoch === 'number') {
|
|
732
|
-
await idbPut(STORE_GROUP_CURRENT, groupCurrentStoreKey(aid, groupId), { ...current, group_id: groupId });
|
|
733
|
-
}
|
|
734
|
-
else {
|
|
735
|
-
await idbDelete(STORE_GROUP_CURRENT, groupCurrentStoreKey(aid, groupId));
|
|
736
|
-
}
|
|
737
|
-
for (const oldEpoch of oldEpochs) {
|
|
738
|
-
if (!isRecord(oldEpoch) || typeof oldEpoch.epoch !== 'number')
|
|
739
|
-
continue;
|
|
740
|
-
await idbPut(STORE_GROUP_OLD_EPOCHS, groupOldStoreKey(aid, groupId, oldEpoch.epoch), {
|
|
741
|
-
...deepClone(oldEpoch), group_id: groupId,
|
|
742
|
-
});
|
|
743
|
-
}
|
|
744
|
-
}
|
|
745
|
-
async _storeGroupSecretTransitionUnlocked(aid, groupId, opts) {
|
|
746
|
-
const currentKey = groupCurrentStoreKey(aid, groupId);
|
|
747
|
-
const currentRaw = await idbGet(STORE_GROUP_CURRENT, currentKey);
|
|
748
|
-
const now = Date.now();
|
|
749
|
-
const epoch = Number(opts.epoch);
|
|
750
|
-
const members = [...(opts.memberAids ?? [])].map((item) => String(item)).sort();
|
|
751
|
-
const pendingRotationId = String(opts.pendingRotationId ?? '').trim();
|
|
752
|
-
if (isRecord(currentRaw)) {
|
|
753
|
-
const current = deepClone(currentRaw);
|
|
754
|
-
delete current.group_id;
|
|
755
|
-
const localEpoch = Number(current.epoch ?? 0);
|
|
756
|
-
if (epoch < localEpoch)
|
|
757
|
-
return false;
|
|
758
|
-
if (epoch === localEpoch && typeof current.secret === 'string') {
|
|
759
|
-
if (current.secret !== opts.secret) {
|
|
760
|
-
if (String(current.pending_rotation_id ?? '').trim()) {
|
|
761
|
-
await idbPut(STORE_GROUP_CURRENT, currentKey, {
|
|
762
|
-
...this._buildGroupCurrentRecord(groupId, opts, members, now),
|
|
763
|
-
});
|
|
764
|
-
return true;
|
|
765
|
-
}
|
|
766
|
-
return false;
|
|
767
|
-
}
|
|
768
|
-
const updated = { ...current };
|
|
769
|
-
let changed = false;
|
|
770
|
-
const oldMembers = Array.isArray(updated.member_aids) ? updated.member_aids.map(String).sort() : [];
|
|
771
|
-
if (members.length > 0 && JSON.stringify(oldMembers) !== JSON.stringify(members)) {
|
|
772
|
-
updated.member_aids = members;
|
|
773
|
-
updated.commitment = opts.commitment;
|
|
774
|
-
updated.updated_at = now;
|
|
775
|
-
changed = true;
|
|
776
|
-
}
|
|
777
|
-
if (opts.epochChain !== undefined && updated.epoch_chain !== opts.epochChain) {
|
|
778
|
-
updated.epoch_chain = opts.epochChain;
|
|
779
|
-
updated.updated_at = now;
|
|
780
|
-
changed = true;
|
|
781
|
-
}
|
|
782
|
-
if (opts.epochChainUnverified === true) {
|
|
783
|
-
if (updated.epoch_chain_unverified !== true) {
|
|
784
|
-
updated.epoch_chain_unverified = true;
|
|
785
|
-
changed = true;
|
|
786
|
-
}
|
|
787
|
-
if (opts.epochChainUnverifiedReason && updated.epoch_chain_unverified_reason !== opts.epochChainUnverifiedReason) {
|
|
788
|
-
updated.epoch_chain_unverified_reason = opts.epochChainUnverifiedReason;
|
|
789
|
-
changed = true;
|
|
790
|
-
}
|
|
791
|
-
}
|
|
792
|
-
else if (opts.epochChainUnverified === false) {
|
|
793
|
-
if ('epoch_chain_unverified' in updated || 'epoch_chain_unverified_reason' in updated) {
|
|
794
|
-
delete updated.epoch_chain_unverified;
|
|
795
|
-
delete updated.epoch_chain_unverified_reason;
|
|
796
|
-
changed = true;
|
|
797
|
-
}
|
|
798
|
-
}
|
|
799
|
-
if (pendingRotationId && updated.pending_rotation_id !== pendingRotationId) {
|
|
800
|
-
updated.pending_rotation_id = pendingRotationId;
|
|
801
|
-
updated.pending_created_at = now;
|
|
802
|
-
changed = true;
|
|
803
|
-
}
|
|
804
|
-
if (!pendingRotationId && updated.pending_rotation_id) {
|
|
805
|
-
delete updated.pending_rotation_id;
|
|
806
|
-
delete updated.pending_created_at;
|
|
807
|
-
changed = true;
|
|
808
|
-
}
|
|
809
|
-
if (changed) {
|
|
810
|
-
await idbPut(STORE_GROUP_CURRENT, currentKey, { ...updated, group_id: groupId });
|
|
811
|
-
}
|
|
812
|
-
return true;
|
|
813
|
-
}
|
|
814
|
-
if (localEpoch !== epoch) {
|
|
815
|
-
const oldEntry = { ...current };
|
|
816
|
-
oldEntry.expires_at = Number(current.updated_at ?? now) + opts.oldEpochRetentionMs;
|
|
817
|
-
await idbPut(STORE_GROUP_OLD_EPOCHS, groupOldStoreKey(aid, groupId, localEpoch), {
|
|
818
|
-
...oldEntry,
|
|
819
|
-
group_id: groupId,
|
|
820
|
-
});
|
|
821
|
-
}
|
|
822
|
-
else {
|
|
823
|
-
const merged = { ...current, ...this._buildGroupCurrentRecord(groupId, opts, members, now) };
|
|
824
|
-
if (!opts.epochChain && current.epoch_chain) {
|
|
825
|
-
merged.epoch_chain = current.epoch_chain;
|
|
826
|
-
}
|
|
827
|
-
if (current.pending_rotation_id && !opts.pendingRotationId) {
|
|
828
|
-
merged.pending_rotation_id = current.pending_rotation_id;
|
|
829
|
-
if (current.pending_created_at)
|
|
830
|
-
merged.pending_created_at = current.pending_created_at;
|
|
831
|
-
}
|
|
832
|
-
await idbPut(STORE_GROUP_CURRENT, currentKey, merged);
|
|
833
|
-
return true;
|
|
834
|
-
}
|
|
835
|
-
}
|
|
836
|
-
await idbPut(STORE_GROUP_CURRENT, currentKey, {
|
|
837
|
-
...this._buildGroupCurrentRecord(groupId, opts, members, now),
|
|
838
|
-
});
|
|
839
|
-
return true;
|
|
840
|
-
}
|
|
841
|
-
async _storeGroupSecretEpochUnlocked(aid, groupId, opts) {
|
|
842
|
-
const now = Date.now();
|
|
843
|
-
const epoch = Number(opts.epoch);
|
|
844
|
-
const members = [...(opts.memberAids ?? [])].map((item) => String(item)).sort();
|
|
845
|
-
const pendingRotationId = String(opts.pendingRotationId ?? '').trim();
|
|
846
|
-
const currentKey = groupCurrentStoreKey(aid, groupId);
|
|
847
|
-
const currentRaw = await idbGet(STORE_GROUP_CURRENT, currentKey);
|
|
848
|
-
const newRecord = this._buildGroupCurrentRecord(groupId, opts, members, now);
|
|
849
|
-
if (!isRecord(currentRaw)) {
|
|
850
|
-
await idbPut(STORE_GROUP_CURRENT, currentKey, newRecord);
|
|
851
|
-
return true;
|
|
852
|
-
}
|
|
853
|
-
const current = deepClone(currentRaw);
|
|
854
|
-
delete current.group_id;
|
|
855
|
-
const localEpoch = Number(current.epoch ?? 0);
|
|
856
|
-
if (epoch > localEpoch) {
|
|
857
|
-
const expiresAt = Date.now() + (opts.oldEpochRetentionMs ?? 604800_000);
|
|
858
|
-
const oldRecord = { ...current, group_id: groupId, expires_at: expiresAt };
|
|
859
|
-
const oldKey = groupOldStoreKey(aid, groupId, localEpoch);
|
|
860
|
-
await idbPut(STORE_GROUP_OLD_EPOCHS, oldKey, oldRecord);
|
|
861
|
-
await idbPut(STORE_GROUP_CURRENT, currentKey, newRecord);
|
|
862
|
-
return true;
|
|
863
|
-
}
|
|
864
|
-
if (epoch === localEpoch) {
|
|
865
|
-
if (typeof current.secret === 'string' && current.secret !== opts.secret) {
|
|
866
|
-
if (!String(current.pending_rotation_id ?? '').trim())
|
|
867
|
-
return false;
|
|
868
|
-
await idbPut(STORE_GROUP_CURRENT, currentKey, newRecord);
|
|
869
|
-
return true;
|
|
870
|
-
}
|
|
871
|
-
const updated = { ...current };
|
|
872
|
-
let changed = false;
|
|
873
|
-
const oldMembers = Array.isArray(updated.member_aids) ? updated.member_aids.map(String).sort() : [];
|
|
874
|
-
if (members.length > 0 && JSON.stringify(oldMembers) !== JSON.stringify(members)) {
|
|
875
|
-
updated.member_aids = members;
|
|
876
|
-
updated.commitment = opts.commitment;
|
|
877
|
-
updated.updated_at = now;
|
|
878
|
-
changed = true;
|
|
879
|
-
}
|
|
880
|
-
if (opts.epochChain !== undefined && updated.epoch_chain !== opts.epochChain) {
|
|
881
|
-
updated.epoch_chain = opts.epochChain;
|
|
882
|
-
updated.updated_at = now;
|
|
883
|
-
changed = true;
|
|
884
|
-
}
|
|
885
|
-
if (opts.epochChainUnverified === true) {
|
|
886
|
-
if (updated.epoch_chain_unverified !== true) {
|
|
887
|
-
updated.epoch_chain_unverified = true;
|
|
888
|
-
changed = true;
|
|
889
|
-
}
|
|
890
|
-
if (opts.epochChainUnverifiedReason && updated.epoch_chain_unverified_reason !== opts.epochChainUnverifiedReason) {
|
|
891
|
-
updated.epoch_chain_unverified_reason = opts.epochChainUnverifiedReason;
|
|
892
|
-
changed = true;
|
|
893
|
-
}
|
|
894
|
-
}
|
|
895
|
-
else if (opts.epochChainUnverified === false) {
|
|
896
|
-
if ('epoch_chain_unverified' in updated || 'epoch_chain_unverified_reason' in updated) {
|
|
897
|
-
delete updated.epoch_chain_unverified;
|
|
898
|
-
delete updated.epoch_chain_unverified_reason;
|
|
899
|
-
changed = true;
|
|
900
|
-
}
|
|
901
|
-
}
|
|
902
|
-
if (pendingRotationId && updated.pending_rotation_id !== pendingRotationId) {
|
|
903
|
-
updated.pending_rotation_id = pendingRotationId;
|
|
904
|
-
updated.pending_created_at = now;
|
|
905
|
-
changed = true;
|
|
906
|
-
}
|
|
907
|
-
if (!pendingRotationId && updated.pending_rotation_id) {
|
|
908
|
-
delete updated.pending_rotation_id;
|
|
909
|
-
delete updated.pending_created_at;
|
|
910
|
-
changed = true;
|
|
911
|
-
}
|
|
912
|
-
if (changed)
|
|
913
|
-
await idbPut(STORE_GROUP_CURRENT, currentKey, { ...updated, group_id: groupId });
|
|
914
|
-
return true;
|
|
915
|
-
}
|
|
916
|
-
const oldKey = groupOldStoreKey(aid, groupId, epoch);
|
|
917
|
-
const oldRaw = await idbGet(STORE_GROUP_OLD_EPOCHS, oldKey);
|
|
918
|
-
if (isRecord(oldRaw)) {
|
|
919
|
-
const old = deepClone(oldRaw);
|
|
920
|
-
if (typeof old.secret === 'string' && old.secret !== opts.secret)
|
|
921
|
-
return false;
|
|
922
|
-
}
|
|
923
|
-
await idbPut(STORE_GROUP_OLD_EPOCHS, oldKey, {
|
|
924
|
-
...newRecord,
|
|
925
|
-
epoch,
|
|
926
|
-
expires_at: now + opts.oldEpochRetentionMs,
|
|
927
|
-
});
|
|
928
|
-
return true;
|
|
929
|
-
}
|
|
930
|
-
async _discardPendingGroupSecretUnlocked(aid, groupId, epoch, rotationId) {
|
|
931
|
-
const currentKey = groupCurrentStoreKey(aid, groupId);
|
|
932
|
-
const currentRaw = await idbGet(STORE_GROUP_CURRENT, currentKey);
|
|
933
|
-
if (!isRecord(currentRaw))
|
|
934
|
-
return false;
|
|
935
|
-
const current = deepClone(currentRaw);
|
|
936
|
-
if (Number(current.epoch ?? 0) !== Number(epoch))
|
|
937
|
-
return false;
|
|
938
|
-
if (String(current.pending_rotation_id ?? '').trim() !== rotationId)
|
|
939
|
-
return false;
|
|
940
|
-
let restored = null;
|
|
941
|
-
let restoredKey = '';
|
|
942
|
-
for (const item of await idbGetAllByPrefix(STORE_GROUP_OLD_EPOCHS, groupOldPrefix(aid, groupId))) {
|
|
943
|
-
if (!isRecord(item.value))
|
|
944
|
-
continue;
|
|
945
|
-
const old = deepClone(item.value);
|
|
946
|
-
const oldEpoch = Number(old.epoch ?? 0);
|
|
947
|
-
if (oldEpoch >= Number(epoch) || !old.secret)
|
|
948
|
-
continue;
|
|
949
|
-
if (!restored || oldEpoch > Number(restored.epoch ?? 0)) {
|
|
950
|
-
restored = old;
|
|
951
|
-
restoredKey = item.key;
|
|
952
|
-
}
|
|
953
|
-
}
|
|
954
|
-
if (!restored) {
|
|
955
|
-
await idbDelete(STORE_GROUP_CURRENT, currentKey);
|
|
956
|
-
for (const item of await idbGetAllByPrefix(STORE_GROUP_OLD_EPOCHS, groupOldPrefix(aid, groupId))) {
|
|
957
|
-
await idbDelete(STORE_GROUP_OLD_EPOCHS, item.key);
|
|
958
|
-
}
|
|
959
|
-
return true;
|
|
960
|
-
}
|
|
961
|
-
delete restored.group_id;
|
|
962
|
-
delete restored.expires_at;
|
|
963
|
-
await idbPut(STORE_GROUP_CURRENT, currentKey, { ...restored, group_id: groupId, updated_at: Date.now() });
|
|
964
|
-
if (restoredKey)
|
|
965
|
-
await idbDelete(STORE_GROUP_OLD_EPOCHS, restoredKey);
|
|
966
|
-
return true;
|
|
967
|
-
}
|
|
968
|
-
_buildGroupCurrentRecord(groupId, opts, memberAids, now) {
|
|
969
|
-
const record = {
|
|
970
|
-
group_id: groupId,
|
|
971
|
-
epoch: opts.epoch,
|
|
972
|
-
secret: opts.secret,
|
|
973
|
-
commitment: opts.commitment,
|
|
974
|
-
member_aids: memberAids,
|
|
975
|
-
updated_at: now,
|
|
976
|
-
};
|
|
977
|
-
if (opts.epochChain !== undefined)
|
|
978
|
-
record.epoch_chain = opts.epochChain;
|
|
979
|
-
const pendingRotationId = String(opts.pendingRotationId ?? '').trim();
|
|
980
|
-
if (pendingRotationId) {
|
|
981
|
-
record.pending_rotation_id = pendingRotationId;
|
|
982
|
-
record.pending_created_at = now;
|
|
983
|
-
}
|
|
984
|
-
if (opts.epochChainUnverified === true) {
|
|
985
|
-
record.epoch_chain_unverified = true;
|
|
986
|
-
if (opts.epochChainUnverifiedReason)
|
|
987
|
-
record.epoch_chain_unverified_reason = opts.epochChainUnverifiedReason;
|
|
988
|
-
}
|
|
989
|
-
return record;
|
|
990
|
-
}
|
|
991
|
-
_mergeGroupEntryFromLegacy(existing, incoming) {
|
|
992
|
-
const oldByEpoch = new Map();
|
|
993
|
-
let current = existing && typeof existing.epoch === 'number'
|
|
994
|
-
? deepClone(Object.fromEntries(Object.entries(existing).filter(([key]) => key !== 'old_epochs')))
|
|
995
|
-
: null;
|
|
996
|
-
if (existing && Array.isArray(existing.old_epochs)) {
|
|
997
|
-
for (const rawOld of existing.old_epochs) {
|
|
998
|
-
if (typeof rawOld.epoch !== 'number')
|
|
999
|
-
continue;
|
|
1000
|
-
oldByEpoch.set(rawOld.epoch, deepClone(rawOld));
|
|
1001
|
-
}
|
|
1002
|
-
}
|
|
1003
|
-
if (typeof incoming.epoch === 'number' && this._isGroupEpochRecoverable(incoming)) {
|
|
1004
|
-
const incomingCurrent = deepClone(Object.fromEntries(Object.entries(incoming).filter(([key]) => key !== 'old_epochs')));
|
|
1005
|
-
if (!current) {
|
|
1006
|
-
current = incomingCurrent;
|
|
1007
|
-
}
|
|
1008
|
-
else if (incomingCurrent.epoch > current.epoch) {
|
|
1009
|
-
oldByEpoch.set(current.epoch, this._preferNewerGroupEpochRecord(oldByEpoch.get(current.epoch), current));
|
|
1010
|
-
current = incomingCurrent;
|
|
1011
|
-
}
|
|
1012
|
-
else if (incomingCurrent.epoch === current.epoch) {
|
|
1013
|
-
current = this._preferNewerGroupEpochRecord(current, incomingCurrent);
|
|
1014
|
-
}
|
|
1015
|
-
else {
|
|
1016
|
-
oldByEpoch.set(incomingCurrent.epoch, this._preferNewerGroupEpochRecord(oldByEpoch.get(incomingCurrent.epoch), incomingCurrent));
|
|
1017
|
-
}
|
|
1018
|
-
}
|
|
1019
|
-
const incomingOldEpochs = Array.isArray(incoming.old_epochs) ? incoming.old_epochs : [];
|
|
1020
|
-
for (const rawOld of incomingOldEpochs) {
|
|
1021
|
-
if (typeof rawOld.epoch !== 'number' || !this._isGroupEpochRecoverable(rawOld))
|
|
1022
|
-
continue;
|
|
1023
|
-
oldByEpoch.set(rawOld.epoch, this._preferNewerGroupEpochRecord(oldByEpoch.get(rawOld.epoch), rawOld));
|
|
1024
|
-
}
|
|
1025
|
-
const merged = current ? deepClone(current) : {};
|
|
1026
|
-
if (current && typeof current.epoch === 'number') {
|
|
1027
|
-
oldByEpoch.delete(current.epoch);
|
|
1028
|
-
}
|
|
1029
|
-
if (oldByEpoch.size > 0) {
|
|
1030
|
-
merged.old_epochs = [...oldByEpoch.entries()].sort(([a], [b]) => a - b).map(([, value]) => deepClone(value));
|
|
1031
|
-
}
|
|
1032
|
-
return merged;
|
|
1033
|
-
}
|
|
1034
|
-
_preferNewerGroupEpochRecord(existing, incoming) {
|
|
1035
|
-
if (!existing)
|
|
1036
|
-
return deepClone(incoming);
|
|
1037
|
-
return Number(incoming.updated_at ?? 0) > Number(existing.updated_at ?? 0)
|
|
1038
|
-
? deepClone(incoming)
|
|
1039
|
-
: deepClone(existing);
|
|
1040
|
-
}
|
|
1041
|
-
_isUnexpiredRecord(record, fallbackKey) {
|
|
1042
|
-
const expiresAt = typeof record.expires_at === 'number' ? record.expires_at : null;
|
|
1043
|
-
if (expiresAt !== null)
|
|
1044
|
-
return expiresAt >= Date.now();
|
|
1045
|
-
const marker = typeof record[fallbackKey] === 'number' ? record[fallbackKey] : null;
|
|
1046
|
-
return marker !== null && marker + STRUCTURED_RECOVERY_RETENTION_MS >= Date.now();
|
|
1047
|
-
}
|
|
1048
|
-
_isPrekeyRecoverable(record) {
|
|
1049
|
-
return this._isUnexpiredRecord(record, 'created_at');
|
|
1050
|
-
}
|
|
1051
|
-
_isGroupEpochRecoverable(record) {
|
|
1052
|
-
const secret = record.secret;
|
|
1053
|
-
if (!secret || (typeof secret === 'string' && secret.length === 0))
|
|
1054
|
-
return false;
|
|
1055
|
-
return this._isUnexpiredRecord(record, 'updated_at');
|
|
1056
|
-
}
|
|
1057
|
-
// ── Sessions 内部辅助 ─────────────────────────────────
|
|
1058
|
-
async _loadSessionsUnlocked(aid) {
|
|
1059
|
-
const items = await idbGetAllByPrefix(STORE_SESSIONS, sessionPrefix(aid));
|
|
1060
|
-
const result = [];
|
|
1061
|
-
for (const item of items) {
|
|
1062
|
-
if (!isRecord(item.value))
|
|
1063
|
-
continue;
|
|
1064
|
-
result.push(deepClone(item.value));
|
|
1065
|
-
}
|
|
1066
|
-
return result;
|
|
1067
|
-
}
|
|
1068
|
-
async _migrateLegacySessionsUnlocked(aid) {
|
|
1069
|
-
const metadataOnly = (await this._loadMetadataOnlyUnlocked(aid)) ?? {};
|
|
1070
|
-
if (!Array.isArray(metadataOnly.e2ee_sessions) || metadataOnly.e2ee_sessions.length === 0) {
|
|
1071
|
-
return;
|
|
1072
|
-
}
|
|
1073
|
-
for (const session of metadataOnly.e2ee_sessions) {
|
|
1074
|
-
const sid = typeof session.session_id === 'string' ? session.session_id : '';
|
|
1075
|
-
if (!sid)
|
|
1076
|
-
continue;
|
|
1077
|
-
const record = deepClone(session);
|
|
1078
|
-
record.session_id = sid;
|
|
1079
|
-
await idbPut(STORE_SESSIONS, sessionStoreKey(aid, sid), record);
|
|
1080
|
-
}
|
|
1081
|
-
const cleaned = deepClone(metadataOnly);
|
|
1082
|
-
delete cleaned.e2ee_sessions;
|
|
1083
|
-
await this._saveMetadataOnlyUnlocked(aid, cleaned);
|
|
1084
|
-
}
|
|
1085
356
|
}
|
|
1086
357
|
//# sourceMappingURL=indexeddb-token-store.js.map
|