@vrtmrz/livesync-commonlib 0.1.42 → 0.1.43
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 +22 -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 +129 -0
- package/dist/diagnostics/index.d.ts +10 -0
- package/dist/diagnostics/index.js +26 -0
- package/dist/diagnostics/integrity.d.ts +5 -0
- package/dist/diagnostics/integrity.js +674 -0
- package/dist/diagnostics/local.d.ts +4 -0
- package/dist/diagnostics/local.js +25 -0
- package/dist/diagnostics/reports.d.ts +108 -0
- package/dist/diagnostics/reports.js +353 -0
- package/dist/diagnostics/types.d.ts +130 -0
- package/dist/diagnostics/types.js +13 -0
- package/dist/diagnostics/usage.d.ts +51 -0
- package/dist/diagnostics/usage.js +241 -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 +24 -0
- package/docs/development.md +1 -0
- package/docs/proven-in-use.md +5 -1
- package/docs/unicode-path-identity.md +98 -0
- package/package.json +9 -1
package/README.md
CHANGED
|
@@ -26,6 +26,7 @@ The package is ESM-only and declares Node 20 or later. Browser entry points are
|
|
|
26
26
|
| -------------------------------------------------- | --------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------- |
|
|
27
27
|
| `@vrtmrz/livesync-commonlib` | `DirectFileManipulator` for integrations which access CouchDB directly | Deliberately small, but its high-level lifecycle and file-operation semantics are not final |
|
|
28
28
|
| `@vrtmrz/livesync-commonlib/context` | Instance-owned events, translation injection, and host-neutral standard-I/O types | Focused, package-tested pre-1.0 contract |
|
|
29
|
+
| `@vrtmrz/livesync-commonlib/diagnostics` | Read-only Usage and integrity inspection, name projection, and reports | Focused pre-1.0 diagnostic contract |
|
|
29
30
|
| `@vrtmrz/livesync-commonlib/hashing` | Chunk ID calculation and exact file-content hashing | Focused, package-tested pre-1.0 contract |
|
|
30
31
|
| `@vrtmrz/livesync-commonlib/browser` | Rooted File System Access API storage | Focused, package-tested pre-1.0 contract |
|
|
31
32
|
| `@vrtmrz/livesync-commonlib/node` | Rooted Node storage, Node standard I/O, and selected Node capabilities | Supported platform façade; keeps Node-only dependencies behind an explicit boundary |
|
|
@@ -119,6 +120,27 @@ See [the settings lifecycle guide](docs/settings-lifecycle.md) before initialisi
|
|
|
119
120
|
|
|
120
121
|
The focused Setup URI entry offers an Ephemeral mode bound to the current fixed seven-day window and a Persistent mode compatible with existing readers. Both use the same URI structure. See [the Setup URI sharing guide](docs/setup-uri.md) for the availability boundary, import behaviour, and compatibility limits.
|
|
121
122
|
|
|
123
|
+
## Database diagnostics
|
|
124
|
+
|
|
125
|
+
The focused `/diagnostics` entry provides read-only Usage and integrity reports, with original names or temporary pseudonyms selected by the host for each run. Integrity inspection covers normal-file Metadata, current conflict branches, and content comparisons for suspect groups in the local database and CouchDB. See [Database diagnostics](docs/database-diagnostics.md) for coverage, cancellation, and privacy boundaries.
|
|
126
|
+
|
|
127
|
+
```typescript
|
|
128
|
+
import {
|
|
129
|
+
analyseDatabaseUsage,
|
|
130
|
+
createDiagnosticNameMapper,
|
|
131
|
+
createLocalDiagnosticSource,
|
|
132
|
+
formatUsageTSV,
|
|
133
|
+
projectUsageReport,
|
|
134
|
+
} from '@vrtmrz/livesync-commonlib/diagnostics';
|
|
135
|
+
|
|
136
|
+
const result = await analyseDatabaseUsage({ source: createLocalDiagnosticSource(localPouchDB) });
|
|
137
|
+
const report = projectUsageReport(result, createDiagnosticNameMapper('pseudonym'));
|
|
138
|
+
if (report.status === 'complete') {
|
|
139
|
+
const tsv = formatUsageTSV(report);
|
|
140
|
+
// Present the report through the host's own UI or output stream.
|
|
141
|
+
}
|
|
142
|
+
```
|
|
143
|
+
|
|
122
144
|
## Chunk ID calculation
|
|
123
145
|
|
|
124
146
|
The `/hashing` entry exports `HashManager` and its `HashManagerCoreOptions` type. It uses the same implementation as database writes, including legacy algorithms and independent ID keys. Hosts supply their settings service and await `initialise()` before calling `computeHash(piece)`.
|
|
@@ -2,9 +2,18 @@ import type { EntryHasPath, FilePathWithPrefix } from "./types.js";
|
|
|
2
2
|
import type { IPathService } from "../services/base/IService.js";
|
|
3
3
|
/** Check the complete source listing before an operation can discard either database. */
|
|
4
4
|
export declare function assertUniqueStoragePathIdentities(paths: Iterable<FilePathWithPrefix>, pathService: Pick<IPathService, "path2id">): Promise<void>;
|
|
5
|
-
/**
|
|
5
|
+
/**
|
|
6
|
+
* Distinct Metadata documents must not be merged by a change of path identity.
|
|
7
|
+
* Callers must enumerate entries with `conflicts: true` so unresolved revisions remain visible.
|
|
8
|
+
* `requireMatchingIds: false` is reserved for preflight checks before replacing the local database;
|
|
9
|
+
* only that mode excludes conflict-free logical deletions from collision checks.
|
|
10
|
+
*/
|
|
6
11
|
export declare function assertMetadataPathIdentities(entries: AsyncIterable<EntryHasPath & {
|
|
7
12
|
_id: string;
|
|
13
|
+
deleted?: boolean;
|
|
14
|
+
_conflicts?: string[];
|
|
8
15
|
}> | Iterable<EntryHasPath & {
|
|
9
16
|
_id: string;
|
|
17
|
+
deleted?: boolean;
|
|
18
|
+
_conflicts?: string[];
|
|
10
19
|
}>, pathService: Pick<IPathService, "path2id">, requireMatchingIds: boolean): Promise<void>;
|
|
@@ -16,6 +16,7 @@ async function assertUniqueStoragePathIdentities(paths, pathService) {
|
|
|
16
16
|
async function assertMetadataPathIdentities(entries, pathService, requireMatchingIds) {
|
|
17
17
|
const identities = /* @__PURE__ */ new Map();
|
|
18
18
|
for await (const entry of entries) {
|
|
19
|
+
if (!requireMatchingIds && entry.deleted === true && !entry._conflicts?.length) continue;
|
|
19
20
|
const path = id2path_base(entry._id, entry);
|
|
20
21
|
const id = await pathService.path2id(path);
|
|
21
22
|
const previous = identities.get(id);
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { type RemoteDBSettings } from "../common/types.js";
|
|
2
|
+
import { type DiagnosticDatabaseSource } from "./types.js";
|
|
3
|
+
export interface CouchDBDiagnosticSourceOptions {
|
|
4
|
+
settings: RemoteDBSettings;
|
|
5
|
+
fetch(request: string, options: RequestInit): Promise<Response>;
|
|
6
|
+
signal?: AbortSignal;
|
|
7
|
+
}
|
|
8
|
+
/**
|
|
9
|
+
* Open an operation-owned HTTP reader. All requests are GETs, including the
|
|
10
|
+
* Security Seed lookup; the replication connection and its setup paths are
|
|
11
|
+
* deliberately absent from this boundary.
|
|
12
|
+
*/
|
|
13
|
+
export declare function createCouchDBDiagnosticSource({ settings: originalSettings, fetch, signal, }: CouchDBDiagnosticSourceOptions): DiagnosticDatabaseSource & {
|
|
14
|
+
close(): Promise<void>;
|
|
15
|
+
};
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
import { DOCID_SYNC_PARAMETERS } from "../common/types.js";
|
|
2
|
+
import { isObfuscatedEntry, parseHeaderValues } from "../common/utils.js";
|
|
3
|
+
import { getConfiguredFunctionsForEncryption, isEncryptedMeta } from "../pouchdb/encryption.js";
|
|
4
|
+
import { isPathProbablyObfuscated } from "octagonal-wheels/encryption/encryption";
|
|
5
|
+
import { decompressDoc } from "../pouchdb/compress.js";
|
|
6
|
+
import { AuthorizationHeaderGenerator, generateCredentialObject } from "../replication/httplib.js";
|
|
7
|
+
import { base64ToArrayBuffer } from "../string_and_binary/convert.js";
|
|
8
|
+
import { DiagnosticReadError } from "./types.js";
|
|
9
|
+
function createCouchDBDiagnosticSource({
|
|
10
|
+
settings: originalSettings,
|
|
11
|
+
fetch,
|
|
12
|
+
signal
|
|
13
|
+
}) {
|
|
14
|
+
const settings = { ...originalSettings };
|
|
15
|
+
const root = `${settings.couchDB_URI.replace(/\/+$/, "")}/${encodeURIComponent(settings.couchDB_DBNAME)}`;
|
|
16
|
+
const url = new URL(root);
|
|
17
|
+
if (!settings.couchDB_DBNAME || !["https:", "http:"].includes(url.protocol) || url.search || url.hash) {
|
|
18
|
+
throw new DiagnosticReadError("source-unavailable");
|
|
19
|
+
}
|
|
20
|
+
if (settings.encrypt && !settings.passphrase && !settings.permitEmptyPassphrase) {
|
|
21
|
+
throw new DiagnosticReadError("source-unavailable");
|
|
22
|
+
}
|
|
23
|
+
const controller = new AbortController();
|
|
24
|
+
const abort = () => controller.abort();
|
|
25
|
+
if (signal?.aborted) abort();
|
|
26
|
+
else signal?.addEventListener("abort", abort, { once: true });
|
|
27
|
+
const auth = new AuthorizationHeaderGenerator();
|
|
28
|
+
const credentials = generateCredentialObject(settings);
|
|
29
|
+
const headers = new Headers(parseHeaderValues(settings.couchDB_CustomHeaders));
|
|
30
|
+
const checkCancelled = () => {
|
|
31
|
+
if (controller.signal.aborted) throw new DOMException("Diagnostic cancelled", "AbortError");
|
|
32
|
+
};
|
|
33
|
+
const readJSON = async (path, parameters = {}) => {
|
|
34
|
+
checkCancelled();
|
|
35
|
+
const requestURL = new URL(`${root}${path}`);
|
|
36
|
+
for (const [key, value] of Object.entries(parameters)) requestURL.searchParams.set(key, value);
|
|
37
|
+
headers.set("authorization", await auth.getAuthorizationHeader(credentials));
|
|
38
|
+
checkCancelled();
|
|
39
|
+
const response = await fetch(requestURL.toString(), {
|
|
40
|
+
method: "GET",
|
|
41
|
+
headers,
|
|
42
|
+
cache: "no-store",
|
|
43
|
+
signal: controller.signal
|
|
44
|
+
});
|
|
45
|
+
checkCancelled();
|
|
46
|
+
if (!response.ok) throw new DiagnosticReadError("document-unreadable", response.status === 404);
|
|
47
|
+
const result = await response.json();
|
|
48
|
+
checkCancelled();
|
|
49
|
+
return result;
|
|
50
|
+
};
|
|
51
|
+
let salt;
|
|
52
|
+
const readSalt = () => salt ??= (async () => {
|
|
53
|
+
try {
|
|
54
|
+
const parameters = await readJSON(`/${DOCID_SYNC_PARAMETERS}`);
|
|
55
|
+
if (typeof parameters.pbkdf2salt !== "string" || !parameters.pbkdf2salt) {
|
|
56
|
+
throw new DiagnosticReadError("security-seed-unavailable");
|
|
57
|
+
}
|
|
58
|
+
const bytes = new Uint8Array(base64ToArrayBuffer(parameters.pbkdf2salt));
|
|
59
|
+
if (!bytes.length) throw new DiagnosticReadError("security-seed-unavailable");
|
|
60
|
+
return bytes;
|
|
61
|
+
} catch {
|
|
62
|
+
checkCancelled();
|
|
63
|
+
throw new DiagnosticReadError("security-seed-unavailable");
|
|
64
|
+
}
|
|
65
|
+
})();
|
|
66
|
+
const decrypt = settings.encrypt ? getConfiguredFunctionsForEncryption(
|
|
67
|
+
settings.passphrase,
|
|
68
|
+
settings.useDynamicIterationCount,
|
|
69
|
+
false,
|
|
70
|
+
readSalt,
|
|
71
|
+
settings.E2EEAlgorithm,
|
|
72
|
+
false,
|
|
73
|
+
() => {
|
|
74
|
+
}
|
|
75
|
+
).outgoing : async (doc) => doc;
|
|
76
|
+
return {
|
|
77
|
+
async info() {
|
|
78
|
+
const info = await readJSON("");
|
|
79
|
+
return { update_seq: info.update_seq };
|
|
80
|
+
},
|
|
81
|
+
async *enumerateIds() {
|
|
82
|
+
let previous;
|
|
83
|
+
while (true) {
|
|
84
|
+
const parameters = { limit: "256" };
|
|
85
|
+
if (previous !== void 0) {
|
|
86
|
+
parameters.startkey = JSON.stringify(previous);
|
|
87
|
+
parameters.skip = "1";
|
|
88
|
+
}
|
|
89
|
+
const page = await readJSON("/_all_docs", parameters);
|
|
90
|
+
if (!Array.isArray(page.rows)) throw new DiagnosticReadError("source-unavailable");
|
|
91
|
+
if (page.rows.length === 0) return;
|
|
92
|
+
for (const row of page.rows) {
|
|
93
|
+
checkCancelled();
|
|
94
|
+
if (typeof row.id !== "string") throw new DiagnosticReadError("source-unavailable");
|
|
95
|
+
if (!row.value?.deleted) yield row.id;
|
|
96
|
+
}
|
|
97
|
+
const last = page.rows.at(-1).id;
|
|
98
|
+
if (last === previous) throw new DiagnosticReadError("source-unavailable");
|
|
99
|
+
previous = last;
|
|
100
|
+
if (page.rows.length < 256) return;
|
|
101
|
+
}
|
|
102
|
+
},
|
|
103
|
+
async get(id, options = {}) {
|
|
104
|
+
const parameters = {};
|
|
105
|
+
if (options.rev !== void 0) parameters.rev = options.rev;
|
|
106
|
+
if (options.conflicts) parameters.conflicts = "true";
|
|
107
|
+
if (options.revs) parameters.revs = "true";
|
|
108
|
+
if (options.revs_info) parameters.revs_info = "true";
|
|
109
|
+
const raw = await readJSON(`/${encodeURIComponent(id)}`, parameters);
|
|
110
|
+
if (raw._id !== id || typeof raw._rev !== "string") {
|
|
111
|
+
throw new DiagnosticReadError("document-unreadable");
|
|
112
|
+
}
|
|
113
|
+
const decoded = await decompressDoc(await decrypt(raw));
|
|
114
|
+
if ("e_" in decoded && decoded.e_ === true || "path" in decoded && typeof decoded.path === "string" && (isEncryptedMeta(decoded) || isObfuscatedEntry(decoded) && isPathProbablyObfuscated(decoded.path))) {
|
|
115
|
+
throw new DiagnosticReadError("document-unreadable");
|
|
116
|
+
}
|
|
117
|
+
checkCancelled();
|
|
118
|
+
return decoded;
|
|
119
|
+
},
|
|
120
|
+
close() {
|
|
121
|
+
signal?.removeEventListener("abort", abort);
|
|
122
|
+
controller.abort();
|
|
123
|
+
return Promise.resolve();
|
|
124
|
+
}
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
export {
|
|
128
|
+
createCouchDBDiagnosticSource
|
|
129
|
+
};
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export { analyseDatabaseUsage } from "./usage.js";
|
|
2
|
+
export type { DatabaseUsageSource, DatabaseUsageResult, DatabaseUsageRow, DatabaseUsageProgress, DatabaseUsageError, } from "./usage.js";
|
|
3
|
+
export { analyseDatabaseIntegrity } from "./integrity.js";
|
|
4
|
+
export { createLocalDiagnosticSource } from "./local.js";
|
|
5
|
+
export { createCouchDBDiagnosticSource } from "./couchdb.js";
|
|
6
|
+
export type { CouchDBDiagnosticSourceOptions } from "./couchdb.js";
|
|
7
|
+
export { createDiagnosticNameMapper, projectIntegrityReport, projectUsageReport, formatDiagnosticJSON, formatIntegrityMarkdown, formatUsageTSV, } from "./reports.js";
|
|
8
|
+
export type { DiagnosticNameMapper, DiagnosticReportVersions, ProjectedDiagnosticReport, ProjectedDatabaseIntegrityReport, ProjectedDatabaseUsageReport, } from "./reports.js";
|
|
9
|
+
export { DiagnosticReadError } from "./types.js";
|
|
10
|
+
export type { DiagnosticStatus, DiagnosticNameMode, DiagnosticSourceName, DiagnosticReason, DiagnosticReadOptions, DiagnosticDatabaseSource, DiagnosticError, DiagnosticContent, DiagnosticCoverage, DiagnosticObservation, DiagnosticIdentitySettings, DatabaseIntegrityResult, DatabaseIntegrityOptions, } from "./types.js";
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { analyseDatabaseUsage } from "./usage.js";
|
|
2
|
+
import { analyseDatabaseIntegrity } from "./integrity.js";
|
|
3
|
+
import { createLocalDiagnosticSource } from "./local.js";
|
|
4
|
+
import { createCouchDBDiagnosticSource } from "./couchdb.js";
|
|
5
|
+
import {
|
|
6
|
+
createDiagnosticNameMapper,
|
|
7
|
+
projectIntegrityReport,
|
|
8
|
+
projectUsageReport,
|
|
9
|
+
formatDiagnosticJSON,
|
|
10
|
+
formatIntegrityMarkdown,
|
|
11
|
+
formatUsageTSV
|
|
12
|
+
} from "./reports.js";
|
|
13
|
+
import { DiagnosticReadError } from "./types.js";
|
|
14
|
+
export {
|
|
15
|
+
DiagnosticReadError,
|
|
16
|
+
analyseDatabaseIntegrity,
|
|
17
|
+
analyseDatabaseUsage,
|
|
18
|
+
createCouchDBDiagnosticSource,
|
|
19
|
+
createDiagnosticNameMapper,
|
|
20
|
+
createLocalDiagnosticSource,
|
|
21
|
+
formatDiagnosticJSON,
|
|
22
|
+
formatIntegrityMarkdown,
|
|
23
|
+
formatUsageTSV,
|
|
24
|
+
projectIntegrityReport,
|
|
25
|
+
projectUsageReport
|
|
26
|
+
};
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import type { DatabaseIntegrityOptions, DatabaseIntegrityResult } from "./types.js";
|
|
2
|
+
/**
|
|
3
|
+
* Inspect current normal-file Metadata and content evidence without changing a source.
|
|
4
|
+
*/
|
|
5
|
+
export declare function analyseDatabaseIntegrity(options: DatabaseIntegrityOptions): Promise<DatabaseIntegrityResult>;
|