@vrtmrz/livesync-commonlib 0.1.42 → 0.1.44
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/README.md +40 -0
- package/dist/common/pathIdentity.d.ts +10 -1
- package/dist/common/pathIdentity.js +1 -0
- package/dist/diagnostics/couchdb.d.ts +15 -0
- package/dist/diagnostics/couchdb.js +202 -0
- package/dist/diagnostics/index.d.ts +10 -0
- package/dist/diagnostics/index.js +28 -0
- package/dist/diagnostics/integrity.d.ts +5 -0
- package/dist/diagnostics/integrity.js +901 -0
- package/dist/diagnostics/local.d.ts +4 -0
- package/dist/diagnostics/local.js +25 -0
- package/dist/diagnostics/names.d.ts +16 -0
- package/dist/diagnostics/names.js +50 -0
- package/dist/diagnostics/reports.d.ts +111 -0
- package/dist/diagnostics/reports.js +494 -0
- package/dist/diagnostics/types.d.ts +190 -0
- package/dist/diagnostics/types.js +13 -0
- package/dist/diagnostics/usage.d.ts +55 -0
- package/dist/diagnostics/usage.js +345 -0
- package/dist/pouchdb/encryption.d.ts +3 -1
- package/dist/pouchdb/encryption.js +38 -30
- package/dist/serviceFeatures/offlineScanner.d.ts +11 -2
- package/dist/serviceModules/Rebuilder.js +1 -1
- package/docs/database-diagnostics.md +69 -0
- package/docs/development.md +1 -0
- package/docs/proven-in-use.md +11 -1
- package/docs/unicode-path-identity.md +98 -0
- package/package.json +10 -1
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
class DiagnosticReadError extends Error {
|
|
2
|
+
constructor(reason, missing = false) {
|
|
3
|
+
super(reason);
|
|
4
|
+
this.reason = reason;
|
|
5
|
+
this.missing = missing;
|
|
6
|
+
this.name = "DiagnosticReadError";
|
|
7
|
+
}
|
|
8
|
+
reason;
|
|
9
|
+
missing;
|
|
10
|
+
}
|
|
11
|
+
export {
|
|
12
|
+
DiagnosticReadError
|
|
13
|
+
};
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
export interface DatabaseUsageGetOptions {
|
|
2
|
+
revs?: boolean;
|
|
3
|
+
revs_info?: boolean;
|
|
4
|
+
conflicts?: boolean;
|
|
5
|
+
rev?: string;
|
|
6
|
+
}
|
|
7
|
+
export interface DatabaseUsageSource {
|
|
8
|
+
info(): Promise<{
|
|
9
|
+
update_seq?: string | number;
|
|
10
|
+
}>;
|
|
11
|
+
enumerateIds(): AsyncIterable<string>;
|
|
12
|
+
get(id: string, options?: DatabaseUsageGetOptions): Promise<object | null | undefined>;
|
|
13
|
+
}
|
|
14
|
+
export interface DatabaseUsageProgress {
|
|
15
|
+
phase: "reading" | "accounting";
|
|
16
|
+
idsProcessed: number;
|
|
17
|
+
revisionsProcessed: number;
|
|
18
|
+
processed: number;
|
|
19
|
+
total: number;
|
|
20
|
+
totalIsFinal: boolean;
|
|
21
|
+
}
|
|
22
|
+
export type DatabaseUsageStatus = "complete" | "partial" | "failed" | "cancelled";
|
|
23
|
+
export type DatabaseUsageErrorCode = "start_info_read_failed" | "start_update_seq_unavailable" | "end_info_read_failed" | "end_update_seq_unavailable" | "configuration_changed" | "id_enumeration_failed" | "revision_list_read_failed" | "revision_not_found" | "revision_read_failed" | "revision_document_invalid";
|
|
24
|
+
export interface DatabaseUsageError {
|
|
25
|
+
code: DatabaseUsageErrorCode;
|
|
26
|
+
id?: string;
|
|
27
|
+
revision?: string;
|
|
28
|
+
}
|
|
29
|
+
export interface DatabaseUsageRow {
|
|
30
|
+
title: string;
|
|
31
|
+
id: string;
|
|
32
|
+
path: string;
|
|
33
|
+
revision: string;
|
|
34
|
+
rev: string;
|
|
35
|
+
revHash: string;
|
|
36
|
+
uniqueChunkCount: number;
|
|
37
|
+
sharedChunkCount: number;
|
|
38
|
+
uniqueChunkSize: number;
|
|
39
|
+
sharedChunkSize: number;
|
|
40
|
+
}
|
|
41
|
+
export interface DatabaseUsageResult {
|
|
42
|
+
version: 1;
|
|
43
|
+
kind: "usage";
|
|
44
|
+
status: DatabaseUsageStatus;
|
|
45
|
+
startUpdateSeq?: string | number;
|
|
46
|
+
endUpdateSeq?: string | number;
|
|
47
|
+
changedDuringRun?: boolean;
|
|
48
|
+
rows: DatabaseUsageRow[];
|
|
49
|
+
errors: DatabaseUsageError[];
|
|
50
|
+
}
|
|
51
|
+
export declare function analyseDatabaseUsage(options: {
|
|
52
|
+
source: DatabaseUsageSource;
|
|
53
|
+
signal?: AbortSignal;
|
|
54
|
+
onProgress?: (progress: DatabaseUsageProgress) => void;
|
|
55
|
+
}): Promise<DatabaseUsageResult>;
|
|
@@ -0,0 +1,345 @@
|
|
|
1
|
+
const USAGE_ACCOUNTING_YIELD_EVERY = 64;
|
|
2
|
+
function asRecord(value) {
|
|
3
|
+
return value;
|
|
4
|
+
}
|
|
5
|
+
function getRevisionNumber(revision) {
|
|
6
|
+
const generation = revision.split("-")[0];
|
|
7
|
+
return `${generation ? Number.parseInt(generation) : 0}`;
|
|
8
|
+
}
|
|
9
|
+
function getRevisionHash(revision) {
|
|
10
|
+
const hash = revision.split("-")[1];
|
|
11
|
+
if (hash === void 0) throw new Error("invalid revision");
|
|
12
|
+
return hash.substring(0, 6);
|
|
13
|
+
}
|
|
14
|
+
function getRevisionInfos(value) {
|
|
15
|
+
if (!Array.isArray(value)) return [];
|
|
16
|
+
return value;
|
|
17
|
+
}
|
|
18
|
+
function getChildren(value) {
|
|
19
|
+
const children = value ?? [];
|
|
20
|
+
if (!Array.isArray(children) || !children.every((child) => typeof child === "string")) {
|
|
21
|
+
throw new Error("invalid chunk references");
|
|
22
|
+
}
|
|
23
|
+
return new Set(children);
|
|
24
|
+
}
|
|
25
|
+
function getStringField(value) {
|
|
26
|
+
if (typeof value !== "string") throw new Error("invalid document field");
|
|
27
|
+
return value;
|
|
28
|
+
}
|
|
29
|
+
async function analyseDatabaseUsage(options) {
|
|
30
|
+
const { source, signal, onProgress } = options;
|
|
31
|
+
const errors = [];
|
|
32
|
+
const documentMap = /* @__PURE__ */ new Map();
|
|
33
|
+
const chunkMap = /* @__PURE__ */ new Map();
|
|
34
|
+
const progress = {
|
|
35
|
+
phase: "reading",
|
|
36
|
+
idsProcessed: 0,
|
|
37
|
+
revisionsProcessed: 0,
|
|
38
|
+
processed: 0,
|
|
39
|
+
total: 0,
|
|
40
|
+
totalIsFinal: false
|
|
41
|
+
};
|
|
42
|
+
let startUpdateSeq;
|
|
43
|
+
let endUpdateSeq;
|
|
44
|
+
let enumerationFailed = false;
|
|
45
|
+
const reportProgress = () => {
|
|
46
|
+
try {
|
|
47
|
+
onProgress?.({ ...progress });
|
|
48
|
+
} catch {
|
|
49
|
+
}
|
|
50
|
+
};
|
|
51
|
+
const isCancelled = () => signal?.aborted === true;
|
|
52
|
+
let cancelled = isCancelled();
|
|
53
|
+
let documentRevisionCount = 0;
|
|
54
|
+
let referenceCount = 0;
|
|
55
|
+
reportProgress();
|
|
56
|
+
if (isCancelled()) cancelled = true;
|
|
57
|
+
if (!cancelled) {
|
|
58
|
+
try {
|
|
59
|
+
const info = await source.info();
|
|
60
|
+
startUpdateSeq = info.update_seq;
|
|
61
|
+
if (startUpdateSeq === void 0) errors.push({ code: "start_update_seq_unavailable" });
|
|
62
|
+
} catch {
|
|
63
|
+
errors.push({ code: "start_info_read_failed" });
|
|
64
|
+
}
|
|
65
|
+
if (isCancelled()) cancelled = true;
|
|
66
|
+
}
|
|
67
|
+
let enumerationFinished = false;
|
|
68
|
+
try {
|
|
69
|
+
if (!cancelled && !isCancelled()) {
|
|
70
|
+
for await (const id of source.enumerateIds()) {
|
|
71
|
+
if (isCancelled()) {
|
|
72
|
+
cancelled = true;
|
|
73
|
+
break;
|
|
74
|
+
}
|
|
75
|
+
let revisionInfos = [];
|
|
76
|
+
let revisionList;
|
|
77
|
+
let revisionListReadFailed = false;
|
|
78
|
+
try {
|
|
79
|
+
revisionList = await source.get(id, { revs: true, revs_info: true, conflicts: true });
|
|
80
|
+
} catch {
|
|
81
|
+
if (!isCancelled()) {
|
|
82
|
+
errors.push({ code: "revision_list_read_failed", id });
|
|
83
|
+
revisionListReadFailed = true;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
if (isCancelled()) {
|
|
87
|
+
cancelled = true;
|
|
88
|
+
break;
|
|
89
|
+
}
|
|
90
|
+
if (revisionList) revisionInfos = getRevisionInfos(asRecord(revisionList)._revs_info);
|
|
91
|
+
else if (!revisionListReadFailed) {
|
|
92
|
+
errors.push({ code: "revision_list_read_failed", id });
|
|
93
|
+
}
|
|
94
|
+
progress.idsProcessed++;
|
|
95
|
+
for (const revisionInfo of revisionInfos) {
|
|
96
|
+
if (revisionInfo.status === "available" && typeof revisionInfo.rev === "string") {
|
|
97
|
+
progress.total++;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
reportProgress();
|
|
101
|
+
if (isCancelled()) {
|
|
102
|
+
cancelled = true;
|
|
103
|
+
break;
|
|
104
|
+
}
|
|
105
|
+
for (const revisionInfo of revisionInfos) {
|
|
106
|
+
if (isCancelled()) {
|
|
107
|
+
cancelled = true;
|
|
108
|
+
break;
|
|
109
|
+
}
|
|
110
|
+
if (revisionInfo.status !== "available") continue;
|
|
111
|
+
const revision = typeof revisionInfo.rev === "string" ? revisionInfo.rev : void 0;
|
|
112
|
+
if (revision === void 0) {
|
|
113
|
+
errors.push({ code: "revision_document_invalid", id });
|
|
114
|
+
continue;
|
|
115
|
+
}
|
|
116
|
+
let rawDocument;
|
|
117
|
+
try {
|
|
118
|
+
rawDocument = await source.get(id, { rev: revision });
|
|
119
|
+
} catch {
|
|
120
|
+
if (isCancelled()) {
|
|
121
|
+
cancelled = true;
|
|
122
|
+
break;
|
|
123
|
+
}
|
|
124
|
+
errors.push({ code: "revision_read_failed", id, revision });
|
|
125
|
+
progress.revisionsProcessed++;
|
|
126
|
+
progress.processed++;
|
|
127
|
+
reportProgress();
|
|
128
|
+
continue;
|
|
129
|
+
}
|
|
130
|
+
if (isCancelled()) {
|
|
131
|
+
cancelled = true;
|
|
132
|
+
break;
|
|
133
|
+
}
|
|
134
|
+
if (!rawDocument) {
|
|
135
|
+
errors.push({ code: "revision_not_found", id, revision });
|
|
136
|
+
progress.revisionsProcessed++;
|
|
137
|
+
progress.processed++;
|
|
138
|
+
reportProgress();
|
|
139
|
+
continue;
|
|
140
|
+
}
|
|
141
|
+
try {
|
|
142
|
+
const document = asRecord(rawDocument);
|
|
143
|
+
if ("children" in document) {
|
|
144
|
+
const documentId = getStringField(document._id);
|
|
145
|
+
const documentRevision = getStringField(document._rev);
|
|
146
|
+
const path = getStringField(document.path);
|
|
147
|
+
const chunks = getChildren(document.children);
|
|
148
|
+
const revisions = documentMap.get(documentId) ?? [];
|
|
149
|
+
revisions.push({
|
|
150
|
+
id: documentId,
|
|
151
|
+
revision: documentRevision,
|
|
152
|
+
path,
|
|
153
|
+
chunks,
|
|
154
|
+
uniqueChunks: /* @__PURE__ */ new Set(),
|
|
155
|
+
sharedChunks: /* @__PURE__ */ new Set(),
|
|
156
|
+
uniqueChunkSize: 0,
|
|
157
|
+
sharedChunkSize: 0
|
|
158
|
+
});
|
|
159
|
+
documentMap.set(documentId, revisions);
|
|
160
|
+
documentRevisionCount++;
|
|
161
|
+
referenceCount += chunks.size;
|
|
162
|
+
} else if (document.type === "leaf") {
|
|
163
|
+
const chunkId = getStringField(document._id);
|
|
164
|
+
if (!chunkMap.has(chunkId) && !document._deleted) {
|
|
165
|
+
if (typeof document.data !== "string") throw new Error("invalid chunk data");
|
|
166
|
+
chunkMap.set(chunkId, { length: document.data.length, refCount: 0 });
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
} catch {
|
|
170
|
+
errors.push({ code: "revision_document_invalid", id, revision });
|
|
171
|
+
}
|
|
172
|
+
progress.revisionsProcessed++;
|
|
173
|
+
progress.processed++;
|
|
174
|
+
reportProgress();
|
|
175
|
+
}
|
|
176
|
+
if (isCancelled()) cancelled = true;
|
|
177
|
+
if (cancelled) break;
|
|
178
|
+
}
|
|
179
|
+
if (!cancelled && !isCancelled()) enumerationFinished = true;
|
|
180
|
+
} else if (isCancelled()) {
|
|
181
|
+
cancelled = true;
|
|
182
|
+
}
|
|
183
|
+
} catch {
|
|
184
|
+
if (isCancelled()) cancelled = true;
|
|
185
|
+
else {
|
|
186
|
+
enumerationFailed = true;
|
|
187
|
+
errors.push({ code: "id_enumeration_failed" });
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
if (isCancelled()) cancelled = true;
|
|
191
|
+
if (enumerationFinished) {
|
|
192
|
+
progress.totalIsFinal = true;
|
|
193
|
+
reportProgress();
|
|
194
|
+
if (isCancelled()) cancelled = true;
|
|
195
|
+
}
|
|
196
|
+
const readingWasCancelled = cancelled;
|
|
197
|
+
progress.phase = "accounting";
|
|
198
|
+
progress.processed = 0;
|
|
199
|
+
progress.total = referenceCount + documentRevisionCount + chunkMap.size;
|
|
200
|
+
progress.totalIsFinal = true;
|
|
201
|
+
reportProgress();
|
|
202
|
+
let accountingCancelled = !readingWasCancelled && isCancelled();
|
|
203
|
+
const rows = [];
|
|
204
|
+
const accountingErrors = [];
|
|
205
|
+
const recordAccountingWork = () => {
|
|
206
|
+
progress.processed++;
|
|
207
|
+
return progress.processed % USAGE_ACCOUNTING_YIELD_EVERY === 0;
|
|
208
|
+
};
|
|
209
|
+
const yieldAfterAccountingBatch = async () => {
|
|
210
|
+
reportProgress();
|
|
211
|
+
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
212
|
+
return readingWasCancelled || !isCancelled();
|
|
213
|
+
};
|
|
214
|
+
if (!readingWasCancelled && isCancelled()) accountingCancelled = true;
|
|
215
|
+
if (!accountingCancelled) {
|
|
216
|
+
for (const revisions of documentMap.values()) {
|
|
217
|
+
for (const document of revisions) {
|
|
218
|
+
for (const chunkId of document.chunks) {
|
|
219
|
+
const chunk = chunkMap.get(chunkId);
|
|
220
|
+
if (chunk) {
|
|
221
|
+
if (chunk.refCount === 0) {
|
|
222
|
+
document.uniqueChunks.add(chunkId);
|
|
223
|
+
document.uniqueChunkSize += chunk.length;
|
|
224
|
+
} else {
|
|
225
|
+
document.sharedChunks.add(chunkId);
|
|
226
|
+
document.sharedChunkSize += chunk.length;
|
|
227
|
+
}
|
|
228
|
+
chunk.refCount++;
|
|
229
|
+
}
|
|
230
|
+
if (recordAccountingWork() && !await yieldAfterAccountingBatch()) {
|
|
231
|
+
accountingCancelled = true;
|
|
232
|
+
break;
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
if (accountingCancelled) break;
|
|
236
|
+
}
|
|
237
|
+
if (accountingCancelled) break;
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
if (!accountingCancelled) {
|
|
241
|
+
for (const revisions of documentMap.values()) {
|
|
242
|
+
for (const document of revisions) {
|
|
243
|
+
const revisionNumber = getRevisionNumber(document.revision);
|
|
244
|
+
let revisionHash;
|
|
245
|
+
try {
|
|
246
|
+
revisionHash = getRevisionHash(document.revision);
|
|
247
|
+
} catch {
|
|
248
|
+
accountingErrors.push({
|
|
249
|
+
code: "revision_document_invalid",
|
|
250
|
+
id: document.id,
|
|
251
|
+
revision: document.revision
|
|
252
|
+
});
|
|
253
|
+
if (recordAccountingWork() && !await yieldAfterAccountingBatch()) {
|
|
254
|
+
accountingCancelled = true;
|
|
255
|
+
break;
|
|
256
|
+
}
|
|
257
|
+
continue;
|
|
258
|
+
}
|
|
259
|
+
rows.push({
|
|
260
|
+
title: `${document.path} (${document.revision})`,
|
|
261
|
+
id: document.id,
|
|
262
|
+
path: document.path,
|
|
263
|
+
revision: document.revision,
|
|
264
|
+
rev: revisionNumber,
|
|
265
|
+
revHash: revisionHash,
|
|
266
|
+
uniqueChunkCount: document.uniqueChunks.size,
|
|
267
|
+
sharedChunkCount: document.sharedChunks.size,
|
|
268
|
+
uniqueChunkSize: document.uniqueChunkSize,
|
|
269
|
+
sharedChunkSize: document.sharedChunkSize
|
|
270
|
+
});
|
|
271
|
+
if (recordAccountingWork() && !await yieldAfterAccountingBatch()) {
|
|
272
|
+
accountingCancelled = true;
|
|
273
|
+
break;
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
if (accountingCancelled) break;
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
let orphanChunkCount = 0;
|
|
280
|
+
let orphanChunkSize = 0;
|
|
281
|
+
if (!accountingCancelled) {
|
|
282
|
+
for (const chunk of chunkMap.values()) {
|
|
283
|
+
if (chunk.refCount === 0) {
|
|
284
|
+
orphanChunkCount++;
|
|
285
|
+
orphanChunkSize += chunk.length;
|
|
286
|
+
}
|
|
287
|
+
if (recordAccountingWork() && !await yieldAfterAccountingBatch()) {
|
|
288
|
+
accountingCancelled = true;
|
|
289
|
+
break;
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
if (accountingCancelled) {
|
|
294
|
+
cancelled = true;
|
|
295
|
+
rows.length = 0;
|
|
296
|
+
} else {
|
|
297
|
+
errors.push(...accountingErrors);
|
|
298
|
+
rows.push({
|
|
299
|
+
title: "__orphan",
|
|
300
|
+
id: "__orphan",
|
|
301
|
+
path: "__orphan",
|
|
302
|
+
revision: "1-xxxxx",
|
|
303
|
+
rev: "1",
|
|
304
|
+
revHash: "xxxxx",
|
|
305
|
+
uniqueChunkCount: orphanChunkCount,
|
|
306
|
+
sharedChunkCount: 0,
|
|
307
|
+
uniqueChunkSize: orphanChunkSize,
|
|
308
|
+
sharedChunkSize: 0
|
|
309
|
+
});
|
|
310
|
+
}
|
|
311
|
+
if (!cancelled && !isCancelled()) {
|
|
312
|
+
try {
|
|
313
|
+
const info = await source.info();
|
|
314
|
+
endUpdateSeq = info.update_seq;
|
|
315
|
+
if (endUpdateSeq === void 0) errors.push({ code: "end_update_seq_unavailable" });
|
|
316
|
+
} catch {
|
|
317
|
+
errors.push({ code: "end_info_read_failed" });
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
if (isCancelled()) cancelled = true;
|
|
321
|
+
reportProgress();
|
|
322
|
+
if (isCancelled()) cancelled = true;
|
|
323
|
+
const changedDuringRun = startUpdateSeq === void 0 || endUpdateSeq === void 0 ? void 0 : startUpdateSeq !== endUpdateSeq;
|
|
324
|
+
const snapshotUncertain = startUpdateSeq === void 0 || endUpdateSeq === void 0;
|
|
325
|
+
const hasDocumentRows = rows.length > 1;
|
|
326
|
+
let status;
|
|
327
|
+
if (cancelled) status = "cancelled";
|
|
328
|
+
else if (changedDuringRun === true || snapshotUncertain) status = "partial";
|
|
329
|
+
else if (enumerationFailed && !hasDocumentRows) status = "failed";
|
|
330
|
+
else if (errors.length > 0) status = "partial";
|
|
331
|
+
else status = "complete";
|
|
332
|
+
return {
|
|
333
|
+
version: 1,
|
|
334
|
+
kind: "usage",
|
|
335
|
+
status,
|
|
336
|
+
...startUpdateSeq === void 0 ? {} : { startUpdateSeq },
|
|
337
|
+
...endUpdateSeq === void 0 ? {} : { endUpdateSeq },
|
|
338
|
+
...changedDuringRun === void 0 ? {} : { changedDuringRun },
|
|
339
|
+
rows,
|
|
340
|
+
errors
|
|
341
|
+
};
|
|
342
|
+
}
|
|
343
|
+
export {
|
|
344
|
+
analyseDatabaseUsage
|
|
345
|
+
};
|
|
@@ -1,12 +1,14 @@
|
|
|
1
|
+
import { Logger } from "../common/logger.js";
|
|
1
2
|
import { type EntryDoc, type AnyEntry, type EntryLeaf, type DocumentID, type E2EEAlgorithm } from "../common/types.js";
|
|
2
3
|
import { encryptWorker, decryptWorker, encryptHKDFWorker, decryptHKDFWorker } from "#worker";
|
|
3
4
|
export declare const encrypt: typeof encryptWorker;
|
|
4
5
|
export declare const decrypt: typeof decryptWorker;
|
|
5
6
|
export declare const encryptHKDF: typeof encryptHKDFWorker;
|
|
6
7
|
export declare const decryptHKDF: typeof decryptHKDFWorker;
|
|
8
|
+
export declare function isEncryptedMeta<T extends AnyEntry>(doc: T): boolean;
|
|
7
9
|
export declare let preprocessOutgoing: (doc: AnyEntry | EntryLeaf) => Promise<AnyEntry | EntryLeaf>;
|
|
8
10
|
export declare let preprocessIncoming: (doc: EntryDoc) => Promise<EntryDoc>;
|
|
9
|
-
export declare function getConfiguredFunctionsForEncryption(passphrase: string, useDynamicIterationCount: boolean, migrationDecrypt: boolean, getPBKDF2Salt: () => Promise<Uint8Array<ArrayBuffer>>, algorithm: E2EEAlgorithm, encryptInternalMetadata?: boolean): {
|
|
11
|
+
export declare function getConfiguredFunctionsForEncryption(passphrase: string, useDynamicIterationCount: boolean, migrationDecrypt: boolean, getPBKDF2Salt: () => Promise<Uint8Array<ArrayBuffer>>, algorithm: E2EEAlgorithm, encryptInternalMetadata?: boolean, log?: typeof Logger): {
|
|
10
12
|
incoming: (doc: AnyEntry | EntryLeaf) => Promise<AnyEntry | EntryLeaf>;
|
|
11
13
|
outgoing: (doc: EntryDoc) => Promise<AnyEntry | EntryLeaf>;
|
|
12
14
|
};
|
|
@@ -32,20 +32,20 @@ function getEncryptionVersion(data) {
|
|
|
32
32
|
}
|
|
33
33
|
return EncryptionVersions.UNENCRYPTED;
|
|
34
34
|
}
|
|
35
|
-
async function tryDecryptV1AsFallback(encryptedData, passphrase, useDynamicIterationCount) {
|
|
35
|
+
async function tryDecryptV1AsFallback(encryptedData, passphrase, useDynamicIterationCount, log = Logger) {
|
|
36
36
|
try {
|
|
37
37
|
return await decrypt(encryptedData, passphrase, useDynamicIterationCount);
|
|
38
38
|
} catch (ex) {
|
|
39
39
|
try {
|
|
40
|
-
|
|
40
|
+
log(
|
|
41
41
|
"Failed to decrypt with V1 method. Fallback to disable useDynamicIterationCount.",
|
|
42
42
|
LOG_LEVEL_VERBOSE
|
|
43
43
|
);
|
|
44
|
-
|
|
44
|
+
log(ex, LOG_LEVEL_VERBOSE);
|
|
45
45
|
return await decrypt(encryptedData, passphrase, false);
|
|
46
46
|
} catch (ex2) {
|
|
47
|
-
|
|
48
|
-
|
|
47
|
+
log("Completely failed to decrypt with V1 method.", LOG_LEVEL_VERBOSE);
|
|
48
|
+
log(ex2, LOG_LEVEL_VERBOSE);
|
|
49
49
|
return false;
|
|
50
50
|
}
|
|
51
51
|
}
|
|
@@ -153,7 +153,7 @@ async function incomingEncryptHKDF(doc, passphrase, useDynamicIterationCount, ge
|
|
|
153
153
|
}
|
|
154
154
|
return saveDoc;
|
|
155
155
|
}
|
|
156
|
-
async function outgoingDecryptHKDF(doc, migrationDecrypt, decrypted, passphrase, useDynamicIterationCount, getPBKDF2Salt) {
|
|
156
|
+
async function outgoingDecryptHKDF(doc, migrationDecrypt, decrypted, passphrase, useDynamicIterationCount, getPBKDF2Salt, log) {
|
|
157
157
|
const loadDoc = {
|
|
158
158
|
...doc
|
|
159
159
|
};
|
|
@@ -165,21 +165,26 @@ async function outgoingDecryptHKDF(doc, migrationDecrypt, decrypted, passphrase,
|
|
|
165
165
|
loadDoc.data = await decryptHKDF(loadDoc.data, passphrase, pbkdf2salt);
|
|
166
166
|
delete loadDoc.e_;
|
|
167
167
|
} else if (encryptionVersion === EncryptionVersions.ENCRYPTED) {
|
|
168
|
-
const decryptedData = await tryDecryptV1AsFallback(
|
|
168
|
+
const decryptedData = await tryDecryptV1AsFallback(
|
|
169
|
+
loadDoc.data,
|
|
170
|
+
passphrase,
|
|
171
|
+
useDynamicIterationCount,
|
|
172
|
+
log
|
|
173
|
+
);
|
|
169
174
|
if (decryptedData === false) {
|
|
170
|
-
|
|
175
|
+
log(MESSAGE_FALLBACK_DECRYPT_FAILED, LOG_LEVEL_NOTICE);
|
|
171
176
|
throw new Error(MESSAGE_FALLBACK_DECRYPT_FAILED);
|
|
172
177
|
}
|
|
173
178
|
loadDoc.data = decryptedData;
|
|
174
179
|
delete loadDoc.e_;
|
|
175
180
|
} else if (encryptionVersion === EncryptionVersions.UNENCRYPTED) {
|
|
176
181
|
} else {
|
|
177
|
-
|
|
182
|
+
log("Unknown encryption version. Cannot decrypt.", LOG_LEVEL_NOTICE);
|
|
178
183
|
throw new Error("Unknown encryption version. Cannot decrypt.");
|
|
179
184
|
}
|
|
180
185
|
} catch (ex) {
|
|
181
|
-
|
|
182
|
-
|
|
186
|
+
log(DECRYPTION_HKDF_FAILED, LOG_LEVEL_NOTICE);
|
|
187
|
+
log(ex);
|
|
183
188
|
throw ex;
|
|
184
189
|
}
|
|
185
190
|
}
|
|
@@ -192,16 +197,16 @@ async function outgoingDecryptHKDF(doc, migrationDecrypt, decrypted, passphrase,
|
|
|
192
197
|
loadDoc[key] = metadata[key];
|
|
193
198
|
}
|
|
194
199
|
} catch (ex) {
|
|
195
|
-
|
|
196
|
-
|
|
200
|
+
log(`${DECRYPTION_HKDF_FAILED} on Path`, LOG_LEVEL_NOTICE);
|
|
201
|
+
log(ex);
|
|
197
202
|
throw ex;
|
|
198
203
|
}
|
|
199
204
|
} else {
|
|
200
205
|
const path = getPath(loadDoc);
|
|
201
206
|
if (isPathProbablyObfuscated(path)) {
|
|
202
|
-
const decryptedPath = await tryDecryptV1AsFallback(path, passphrase, useDynamicIterationCount);
|
|
207
|
+
const decryptedPath = await tryDecryptV1AsFallback(path, passphrase, useDynamicIterationCount, log);
|
|
203
208
|
if (decryptedPath === false) {
|
|
204
|
-
|
|
209
|
+
log(`${MESSAGE_FALLBACK_DECRYPT_FAILED} on Path`, LOG_LEVEL_NOTICE);
|
|
205
210
|
throw new Error(MESSAGE_FALLBACK_DECRYPT_FAILED);
|
|
206
211
|
}
|
|
207
212
|
loadDoc.path = decryptedPath;
|
|
@@ -215,7 +220,8 @@ async function outgoingDecryptHKDF(doc, migrationDecrypt, decrypted, passphrase,
|
|
|
215
220
|
const decryptedEden = await tryDecryptV1AsFallback(
|
|
216
221
|
loadDoc.eden[EDEN_ENCRYPTED_KEY].data,
|
|
217
222
|
passphrase,
|
|
218
|
-
useDynamicIterationCount
|
|
223
|
+
useDynamicIterationCount,
|
|
224
|
+
log
|
|
219
225
|
);
|
|
220
226
|
if (decryptedEden === false) throw new Error(MESSAGE_FALLBACK_DECRYPT_FAILED);
|
|
221
227
|
readEden = {
|
|
@@ -224,8 +230,8 @@ async function outgoingDecryptHKDF(doc, migrationDecrypt, decrypted, passphrase,
|
|
|
224
230
|
};
|
|
225
231
|
edenDecrypted = true;
|
|
226
232
|
} catch (ex) {
|
|
227
|
-
|
|
228
|
-
|
|
233
|
+
log(`${DECRYPTION_FALLBACK_FAILED} on Eden`, LOG_LEVEL_NOTICE);
|
|
234
|
+
log(ex);
|
|
229
235
|
throw ex;
|
|
230
236
|
}
|
|
231
237
|
}
|
|
@@ -243,8 +249,8 @@ async function outgoingDecryptHKDF(doc, migrationDecrypt, decrypted, passphrase,
|
|
|
243
249
|
};
|
|
244
250
|
edenDecrypted = true;
|
|
245
251
|
} catch (ex) {
|
|
246
|
-
|
|
247
|
-
|
|
252
|
+
log(`${DECRYPTION_HKDF_FAILED} on Eden`, LOG_LEVEL_NOTICE);
|
|
253
|
+
log(ex);
|
|
248
254
|
throw ex;
|
|
249
255
|
}
|
|
250
256
|
}
|
|
@@ -296,7 +302,7 @@ async function incomingEncryptV1(doc, passphrase, useDynamicIterationCount) {
|
|
|
296
302
|
}
|
|
297
303
|
return saveDoc;
|
|
298
304
|
}
|
|
299
|
-
async function outgoingDecryptV1(doc, migrationDecrypt, decrypted, passphrase, useDynamicIterationCount) {
|
|
305
|
+
async function outgoingDecryptV1(doc, migrationDecrypt, decrypted, passphrase, useDynamicIterationCount, log) {
|
|
300
306
|
const loadDoc = {
|
|
301
307
|
...doc
|
|
302
308
|
};
|
|
@@ -361,15 +367,15 @@ async function outgoingDecryptV1(doc, migrationDecrypt, decrypted, passphrase, u
|
|
|
361
367
|
if (migrationDecrypt && ex2 instanceof SyntaxError) {
|
|
362
368
|
return loadDoc;
|
|
363
369
|
}
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
370
|
+
log("Decryption failed.", LOG_LEVEL_NOTICE);
|
|
371
|
+
log(ex2, LOG_LEVEL_VERBOSE);
|
|
372
|
+
log(`id:${loadDoc._id}-${loadDoc._rev?.substring(0, 10)}`, LOG_LEVEL_VERBOSE);
|
|
367
373
|
throw ex2;
|
|
368
374
|
}
|
|
369
375
|
} else {
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
376
|
+
log("Decryption failed.", LOG_LEVEL_NOTICE);
|
|
377
|
+
log(ex, LOG_LEVEL_VERBOSE);
|
|
378
|
+
log(`id:${loadDoc._id}-${loadDoc._rev?.substring(0, 10)}`, LOG_LEVEL_VERBOSE);
|
|
373
379
|
throw ex;
|
|
374
380
|
}
|
|
375
381
|
}
|
|
@@ -382,7 +388,7 @@ let preprocessOutgoing = async (doc) => {
|
|
|
382
388
|
let preprocessIncoming = async (doc) => {
|
|
383
389
|
return await Promise.resolve(doc);
|
|
384
390
|
};
|
|
385
|
-
function getConfiguredFunctionsForEncryption(passphrase, useDynamicIterationCount, migrationDecrypt, getPBKDF2Salt, algorithm, encryptInternalMetadata = false) {
|
|
391
|
+
function getConfiguredFunctionsForEncryption(passphrase, useDynamicIterationCount, migrationDecrypt, getPBKDF2Salt, algorithm, encryptInternalMetadata = false, log = Logger) {
|
|
386
392
|
const decryptedCache = /* @__PURE__ */ new Map();
|
|
387
393
|
const incoming = (doc) => algorithm === E2EEAlgorithms.V2 ? incomingEncryptHKDF(doc, passphrase, useDynamicIterationCount, getPBKDF2Salt, encryptInternalMetadata) : incomingEncryptV1(doc, passphrase, useDynamicIterationCount);
|
|
388
394
|
const outgoing = (doc) => algorithm !== E2EEAlgorithms.ForceV1 ? outgoingDecryptHKDF(
|
|
@@ -391,8 +397,9 @@ function getConfiguredFunctionsForEncryption(passphrase, useDynamicIterationCoun
|
|
|
391
397
|
decryptedCache,
|
|
392
398
|
passphrase,
|
|
393
399
|
useDynamicIterationCount,
|
|
394
|
-
getPBKDF2Salt
|
|
395
|
-
|
|
400
|
+
getPBKDF2Salt,
|
|
401
|
+
log
|
|
402
|
+
) : outgoingDecryptV1(doc, migrationDecrypt, decryptedCache, passphrase, useDynamicIterationCount, log);
|
|
396
403
|
return {
|
|
397
404
|
incoming,
|
|
398
405
|
outgoing
|
|
@@ -459,6 +466,7 @@ export {
|
|
|
459
466
|
encrypt,
|
|
460
467
|
encryptHKDF,
|
|
461
468
|
getConfiguredFunctionsForEncryption,
|
|
469
|
+
isEncryptedMeta,
|
|
462
470
|
preprocessIncoming,
|
|
463
471
|
preprocessOutgoing,
|
|
464
472
|
shouldDecryptEden,
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { type DocumentID, type FilePathWithPrefix, type FilePathWithPrefixLC, type MetaEntry, type UXFileInfoStub, type ObsidianLiveSyncSettings, type LOG_LEVEL } from "../common/types.js";
|
|
2
2
|
import { type LogFunction } from "../services/lib/logUtils.js";
|
|
3
3
|
import type { NecessaryServices } from "../interfaces/ServiceModule.js";
|
|
4
|
+
import type { IPathService } from "../services/base/IService.js";
|
|
4
5
|
import { UnresolvedErrorManager } from "../services/base/UnresolvedErrorManager.js";
|
|
5
6
|
import { VaultScanResults, type VaultScanResult } from "../services/base/VaultScanResult.js";
|
|
6
7
|
export { VaultScanResults };
|
|
@@ -105,7 +106,11 @@ export declare function getMetadataDocumentNamespace(value: string): MetadataDoc
|
|
|
105
106
|
* separate consistent document for the same logical path may still proceed
|
|
106
107
|
* through ordinary path-based handling.
|
|
107
108
|
*/
|
|
108
|
-
export declare function inspectMetadataDocumentIdentity(host:
|
|
109
|
+
export declare function inspectMetadataDocumentIdentity(host: {
|
|
110
|
+
services: {
|
|
111
|
+
path: Pick<IPathService, "getPath" | "path2id">;
|
|
112
|
+
};
|
|
113
|
+
}, doc: MetaEntry): Promise<MetadataDocumentIdentityInspection>;
|
|
109
114
|
/**
|
|
110
115
|
* Enumerate Metadata from its actual local document IDs and report only the
|
|
111
116
|
* entries which ordinary path-based inspection cannot address.
|
|
@@ -139,7 +144,11 @@ export declare function collectDeletedFiles(host: NecessaryServices<"setting" |
|
|
|
139
144
|
* @param doc Meta entry document
|
|
140
145
|
* @returns Path string
|
|
141
146
|
*/
|
|
142
|
-
export declare function getPathFromEntry(host:
|
|
147
|
+
export declare function getPathFromEntry(host: {
|
|
148
|
+
services: {
|
|
149
|
+
path: Pick<IPathService, "getPath">;
|
|
150
|
+
};
|
|
151
|
+
}, doc: MetaEntry): FilePathWithPrefix;
|
|
143
152
|
/**
|
|
144
153
|
* Synchronise a single file between database and storage based on freshness comparison.
|
|
145
154
|
* @param host Services container
|
|
@@ -183,7 +183,7 @@ Please enable them from the settings screen after setup is complete.`,
|
|
|
183
183
|
await assertUniqueStoragePathIdentities(selected, this.path);
|
|
184
184
|
if (databaseReady) {
|
|
185
185
|
await assertMetadataPathIdentities(
|
|
186
|
-
this.database.localDatabase.findAllDocs(),
|
|
186
|
+
this.database.localDatabase.findAllDocs({ conflicts: true }),
|
|
187
187
|
this.path,
|
|
188
188
|
requireMatchingIds
|
|
189
189
|
);
|