@jini-ai/platform 0.5.8 → 0.5.10
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 +12 -0
- package/dist/asset-cache.js +26 -25
- package/dist/blob-storage.d.ts +7 -2
- package/dist/blob-storage.js +99 -59
- package/dist/child-environment.d.ts +36 -0
- package/dist/child-environment.js +68 -0
- package/dist/download.js +192 -148
- package/dist/fs/atomic-write.d.ts +25 -2
- package/dist/fs/atomic-write.js +162 -111
- package/dist/fs/boot-identity.d.ts +16 -0
- package/dist/fs/boot-identity.js +38 -17
- package/dist/fs/directory-home.js +6 -0
- package/dist/fs/directory-registry.d.ts +1 -1
- package/dist/fs/directory-registry.js +23 -16
- package/dist/fs/directory-running-lock.d.ts +1 -1
- package/dist/fs/directory-running-lock.js +18 -7
- package/dist/fs/directory-sync.d.ts +1 -1
- package/dist/fs/directory-sync.js +64 -51
- package/dist/fs/directory-user-config.js +108 -67
- package/dist/fs/directory-workspace.d.ts +11 -0
- package/dist/fs/directory-workspace.js +16 -11
- package/dist/fs/durable-json.d.ts +3 -1
- package/dist/fs/durable-json.js +55 -40
- package/dist/fs/file-lock.js +214 -149
- package/dist/fs/guarded-reader.js +47 -35
- package/dist/fs/user-data-directories.d.ts +1 -1
- package/dist/fs/user-data-directories.js +25 -9
- package/dist/fs/verified-directory-move.d.ts +14 -11
- package/dist/fs/verified-directory-move.js +106 -80
- package/dist/fs/verified-file-relocation.d.ts +2 -1
- package/dist/fs/verified-file-relocation.js +112 -100
- package/dist/http/guarded/client.d.ts +3 -4
- package/dist/http/guarded/client.js +50 -60
- package/dist/http/guarded/fetch-adapter.d.ts +2 -3
- package/dist/http/guarded/fetch-adapter.js +26 -15
- package/dist/mail/builtin-providers.d.ts +39 -0
- package/dist/mail/builtin-providers.js +60 -0
- package/dist/mail/feedback.d.ts +84 -0
- package/dist/mail/feedback.js +90 -0
- package/dist/mail/index.d.ts +11 -0
- package/dist/mail/index.js +6 -0
- package/dist/mail/policy.d.ts +77 -0
- package/dist/mail/policy.js +155 -0
- package/dist/mail/ports.d.ts +56 -7
- package/dist/mail/provider.d.ts +117 -0
- package/dist/mail/provider.js +74 -0
- package/dist/mail/rate-limit.d.ts +33 -0
- package/dist/mail/rate-limit.js +35 -0
- package/dist/mail/sender-identity.d.ts +68 -0
- package/dist/mail/sender-identity.js +50 -0
- package/dist/mail/types.d.ts +9 -0
- package/dist/net/address.d.ts +9 -23
- package/dist/net/address.js +28 -133
- package/dist/net/endpoint-policy.d.ts +26 -2
- package/dist/net/endpoint-policy.js +159 -56
- package/dist/object-store/index.d.ts +2 -0
- package/dist/object-store/index.js +2 -0
- package/dist/object-store/local-fs.d.ts +17 -0
- package/dist/object-store/local-fs.js +146 -0
- package/dist/process.js +2 -0
- package/dist/proxy-env.js +131 -135
- package/dist/secrets/credential-sets/contracts.d.ts +1 -0
- package/dist/secrets/credential-sets/contracts.js +1 -0
- package/dist/secrets/credential-sets/index.d.ts +1 -0
- package/dist/secrets/credential-sets/index.js +1 -0
- package/dist/secrets/credential-sets/provider-matches.d.ts +61 -0
- package/dist/secrets/credential-sets/provider-matches.js +61 -0
- package/dist/secrets/credential-sets/types.d.ts +7 -0
- package/dist/secrets/credential-sets/vault.meta.js +58 -20
- package/dist/secrets/credential-sets/vault.repo.memory.d.ts +7 -0
- package/dist/secrets/credential-sets/vault.repo.memory.js +43 -36
- package/dist/secrets/credential-sets/vault.service.d.ts +9 -0
- package/dist/secrets/credential-sets/vault.service.js +131 -61
- package/dist/secrets/credential-sets/vault.types.d.ts +11 -3
- package/dist/secrets/credential-sets/vault.types.js +22 -8
- package/dist/secrets/keyring.env.js +14 -10
- package/dist/toolchain.js +119 -91
- package/dist/web/html-document.js +73 -69
- package/dist/web/html-to-markdown.js +127 -102
- package/dist/web/web-page.d.ts +1 -1
- package/dist/web/web-page.js +162 -81
- package/package.json +11 -3
|
@@ -1,70 +1,62 @@
|
|
|
1
1
|
import fs from "node:fs";
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
import { readOptionalText, readWindowsOneDriveRoots } from "./user-data-directories.js";
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
* Dropbox publishes info.json paths; macOS File Provider mounts identify their provider.
|
|
7
|
-
* @complexity O(a + p) small account/mount lists and ancestor probes, no recursive site scan.
|
|
8
|
-
*/
|
|
9
|
-
export function detectDirectorySync({ dir, homeDir, documents }, { env = process.env, platform = process.platform, readText = (file) => readOptionalText({ file }), canonicalPath = ({ path: value }) => { try {
|
|
4
|
+
const MOUNT_PROVIDERS = [["OneDrive-", "OneDrive"], ["GoogleDrive-", "Google Drive"], ["Dropbox", "Dropbox"]];
|
|
5
|
+
function canonicalOrResolved({ path: value }) { try {
|
|
10
6
|
return fs.realpathSync(value);
|
|
11
7
|
}
|
|
12
8
|
catch {
|
|
13
9
|
return path.resolve(value);
|
|
14
|
-
} }
|
|
10
|
+
} }
|
|
11
|
+
function listDirectoriesOrEmpty(root) { try {
|
|
15
12
|
return fs.readdirSync(root);
|
|
16
13
|
}
|
|
17
14
|
catch (error) {
|
|
18
15
|
if (error.code === "ENOENT")
|
|
19
16
|
return [];
|
|
20
17
|
throw error;
|
|
21
|
-
} }
|
|
22
|
-
|
|
23
|
-
const
|
|
24
|
-
const
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
for (const name of listDirectories(mounts)) {
|
|
35
|
-
if (name.startsWith("OneDrive-"))
|
|
36
|
-
add(paths.join(mounts, name), "OneDrive");
|
|
37
|
-
else if (name.startsWith("GoogleDrive-"))
|
|
38
|
-
add(paths.join(mounts, name), "Google Drive");
|
|
39
|
-
else if (name.startsWith("Dropbox"))
|
|
40
|
-
add(paths.join(mounts, name), "Dropbox");
|
|
41
|
-
}
|
|
18
|
+
} }
|
|
19
|
+
function macRoots({ homeDir, documents }, { paths, exists, listDirectories }) {
|
|
20
|
+
const cloud = paths.join(homeDir, "Library", "Mobile Documents", "com~apple~CloudDocs");
|
|
21
|
+
const roots = [{ root: cloud, provider: "iCloud" }];
|
|
22
|
+
// Documents need not be a symlink on newer macOS: the cloud Documents entry is evidence
|
|
23
|
+
// of Desktop & Documents being enabled, rather than merely iCloud Drive being installed.
|
|
24
|
+
if (exists(paths.join(cloud, "Documents")) && documents)
|
|
25
|
+
roots.push({ root: documents, provider: "iCloud" });
|
|
26
|
+
const mounts = paths.join(homeDir, "Library", "CloudStorage");
|
|
27
|
+
for (const name of listDirectories(mounts)) {
|
|
28
|
+
const provider = MOUNT_PROVIDERS.find(([prefix]) => name.startsWith(prefix))?.[1];
|
|
29
|
+
if (provider)
|
|
30
|
+
roots.push({ root: paths.join(mounts, name), provider });
|
|
42
31
|
}
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
32
|
+
return roots;
|
|
33
|
+
}
|
|
34
|
+
function oneDriveRoots({ env, platform, oneDriveRoots: extra, windowsOneDriveRoots }) {
|
|
35
|
+
const fromEnv = [env.OneDrive, env.OneDriveConsumer, env.OneDriveCommercial];
|
|
36
|
+
const fromAccounts = platform === "win32" && !fromEnv.some(Boolean) ? windowsOneDriveRoots() : [];
|
|
37
|
+
return [...fromEnv, ...extra, ...fromAccounts];
|
|
38
|
+
}
|
|
39
|
+
function dropboxInfoFiles({ homeDir }, { env, platform }, paths) {
|
|
40
|
+
if (platform !== "win32")
|
|
41
|
+
return [paths.join(homeDir, ".dropbox", "info.json")];
|
|
42
|
+
return [env.APPDATA, env.LOCALAPPDATA].filter((root) => Boolean(root)).map((root) => paths.join(root, "Dropbox", "info.json"));
|
|
43
|
+
}
|
|
44
|
+
function dropboxAccountPaths(text) {
|
|
45
|
+
if (!text)
|
|
46
|
+
return [];
|
|
47
|
+
// Provider-owned metadata can be torn/corrupt; it is optional evidence, never a boot gate.
|
|
48
|
+
let accounts;
|
|
49
|
+
try {
|
|
50
|
+
accounts = JSON.parse(text);
|
|
48
51
|
}
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
const text = readText(file);
|
|
52
|
-
if (!text)
|
|
53
|
-
continue;
|
|
54
|
-
// Provider-owned metadata can be torn/corrupt; it is optional evidence, never a boot gate.
|
|
55
|
-
let accounts;
|
|
56
|
-
try {
|
|
57
|
-
accounts = JSON.parse(text);
|
|
58
|
-
}
|
|
59
|
-
catch {
|
|
60
|
-
continue;
|
|
61
|
-
}
|
|
62
|
-
if (!accounts || typeof accounts !== "object" || Array.isArray(accounts))
|
|
63
|
-
continue;
|
|
64
|
-
for (const account of Object.values(accounts))
|
|
65
|
-
if (typeof account?.path === "string")
|
|
66
|
-
add(account.path, "Dropbox");
|
|
52
|
+
catch {
|
|
53
|
+
return [];
|
|
67
54
|
}
|
|
55
|
+
if (!accounts || typeof accounts !== "object" || Array.isArray(accounts))
|
|
56
|
+
return [];
|
|
57
|
+
return Object.values(accounts).map((account) => account?.path).filter((root) => typeof root === "string");
|
|
58
|
+
}
|
|
59
|
+
function containingProvider({ dir, roots }, { canonicalPath, platform }, paths) {
|
|
68
60
|
const canonical = (value) => { const resolved = canonicalPath({ path: value }); return platform === "win32" ? resolved.toLowerCase() : resolved; };
|
|
69
61
|
const target = canonical(dir);
|
|
70
62
|
for (const { root, provider } of roots) {
|
|
@@ -72,6 +64,9 @@ catch (error) {
|
|
|
72
64
|
if (target === resolved || target.startsWith(resolved + paths.sep))
|
|
73
65
|
return provider;
|
|
74
66
|
}
|
|
67
|
+
return null;
|
|
68
|
+
}
|
|
69
|
+
function ancestorMarkerProvider({ dir }, { exists }, paths) {
|
|
75
70
|
let ancestor = paths.resolve(dir);
|
|
76
71
|
while (true) {
|
|
77
72
|
if (exists(paths.join(ancestor, ".dropbox.cache")))
|
|
@@ -84,4 +79,22 @@ catch (error) {
|
|
|
84
79
|
ancestor = parent;
|
|
85
80
|
}
|
|
86
81
|
}
|
|
82
|
+
function providerRoots(provider, roots) {
|
|
83
|
+
return roots.filter((root) => Boolean(root)).map((root) => ({ root, provider }));
|
|
84
|
+
}
|
|
85
|
+
function detectWithPorts({ dir, homeDir, documents }, ports) {
|
|
86
|
+
const paths = ports.platform === "win32" ? path.win32 : path.posix;
|
|
87
|
+
const mac = ports.platform === "darwin" ? macRoots({ homeDir, documents }, { paths, exists: ports.exists, listDirectories: ports.listDirectories }) : [];
|
|
88
|
+
const oneDrive = providerRoots("OneDrive", oneDriveRoots(ports));
|
|
89
|
+
const dropbox = providerRoots("Dropbox", dropboxInfoFiles({ homeDir }, ports, paths).flatMap((file) => dropboxAccountPaths(ports.readText(file))));
|
|
90
|
+
return containingProvider({ dir, roots: [...mac, ...oneDrive, ...dropbox] }, ports, paths) ?? ancestorMarkerProvider({ dir }, ports, paths);
|
|
91
|
+
}
|
|
92
|
+
/** Detect provider roots, never infer syncing just because a provider is installed. Canonical
|
|
93
|
+
* containment catches redirected Documents, while marker files cover older client layouts.
|
|
94
|
+
* Dropbox publishes info.json paths; macOS File Provider mounts identify their provider.
|
|
95
|
+
* @complexity O(a + p) small account/mount lists and ancestor probes, no recursive site scan.
|
|
96
|
+
*/
|
|
97
|
+
export function detectDirectorySync(required, { env = process.env, platform = process.platform, readText = (file) => readOptionalText({ file }), canonicalPath = canonicalOrResolved, listDirectories = listDirectoriesOrEmpty, exists = fs.existsSync, oneDriveRoots = [], windowsOneDriveRoots = () => readWindowsOneDriveRoots({}), } = {}) {
|
|
98
|
+
return detectWithPorts(required, { env, platform, readText, canonicalPath, listDirectories, exists, oneDriveRoots, windowsOneDriveRoots });
|
|
99
|
+
}
|
|
87
100
|
//# sourceMappingURL=directory-sync.js.map
|
|
@@ -3,6 +3,23 @@ import { createHash } from "node:crypto";
|
|
|
3
3
|
import { resolveDirectoryHome } from "./directory-home.js";
|
|
4
4
|
import { resolveUserDataDirectories } from "./user-data-directories.js";
|
|
5
5
|
import { detectDirectorySync } from "./directory-sync.js";
|
|
6
|
+
const SAFE_SITE_ID = /^[a-zA-Z0-9_-]+$/;
|
|
7
|
+
const invalidMarker = (markerPath) => new Error(`Interrupted database move has an invalid recovery marker: ${markerPath}. Restore the pre-move backup before starting.`);
|
|
8
|
+
function assertDatabaseFilename(filename) {
|
|
9
|
+
if (path.basename(filename) !== filename || !filename.endsWith(".db"))
|
|
10
|
+
throw new Error("A database filename must be a .db basename.");
|
|
11
|
+
}
|
|
12
|
+
/** iCloud keeps data.nosync out of sync; every other provider syncs live databases. */
|
|
13
|
+
function syncsDatabases(sync) { return sync !== null && sync !== "iCloud"; }
|
|
14
|
+
/** Old markers use the fixed home2 name; new moves identify their unique backup. Admit
|
|
15
|
+
* only a sibling of this source so malformed metadata cannot supply unrelated evidence. */
|
|
16
|
+
function markerBackup(source, namedBackup) {
|
|
17
|
+
if (namedBackup === undefined)
|
|
18
|
+
return source + ".home2-backup";
|
|
19
|
+
if (typeof namedBackup !== "string" || path.dirname(namedBackup) !== path.dirname(source) || !namedBackup.startsWith(source + ".relocation-") || !namedBackup.endsWith("-backup"))
|
|
20
|
+
return undefined;
|
|
21
|
+
return namedBackup;
|
|
22
|
+
}
|
|
6
23
|
/** One atomic settings owner for multiple processes. Existing DB files always win over preferences:
|
|
7
24
|
* changing placement controls new databases, never silently moves or abandons existing ones.
|
|
8
25
|
* @complexity O(n) canonicalizations for n roots; DB lookup probes three fixed locations.
|
|
@@ -47,11 +64,37 @@ export function createDirectoryUserConfig({ homeDir, defaultFolder, envName, pol
|
|
|
47
64
|
const nestedHome = legacyHomes.includes(savedHome) ? paths.join(savedHome, policy.defaultSubfolder) : savedHome;
|
|
48
65
|
const sitesHome = expand(env[envName] ?? nestedHome);
|
|
49
66
|
const sync = syncProvider(sitesHome);
|
|
50
|
-
return {
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
67
|
+
return { sitesHome, siteRoots: distinct(configuredRoots(saved)), databases: saved.databases ?? (syncsDatabases(sync) ? "appData" : "inSite") };
|
|
68
|
+
}
|
|
69
|
+
function configuredRoots(saved) {
|
|
70
|
+
const fromEnv = env[policy.rootsEnvName];
|
|
71
|
+
return fromEnv !== undefined ? fromEnv.split(",").map((root) => root.trim()).filter(Boolean) : saved.siteRoots ?? policy.sourceRoots ?? [];
|
|
72
|
+
}
|
|
73
|
+
// Prefer explicit settings, never coerce an existing regular file into a directory.
|
|
74
|
+
function assertDirectories(checked) {
|
|
75
|
+
for (const directory of [checked.sitesHome, ...(checked.siteRoots ?? [])]) {
|
|
76
|
+
if (directory === undefined)
|
|
77
|
+
continue;
|
|
78
|
+
const expanded = expand(directory);
|
|
79
|
+
if (exists(expanded) && !isDirectory(expanded))
|
|
80
|
+
throw new Error(`Sites folder is not a directory: ${directory}`);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
function assertNotOverridden(checked) {
|
|
84
|
+
if (checked.sitesHome !== undefined && env[envName] !== undefined)
|
|
85
|
+
throw new Error(`${envName} overrides this setting. Unset it before changing the directory home.`);
|
|
86
|
+
if (checked.siteRoots !== undefined && env[policy.rootsEnvName] !== undefined)
|
|
87
|
+
throw new Error(`${policy.rootsEnvName} overrides this setting. Unset it before changing site roots.`);
|
|
88
|
+
}
|
|
89
|
+
function merged(previous, checked, incremental) {
|
|
90
|
+
const next = { ...previous, ...checked,
|
|
91
|
+
...(checked.sitesHome !== undefined ? { sitesHome: expand(checked.sitesHome) } : {}),
|
|
92
|
+
...(checked.siteRoots !== undefined ? { siteRoots: distinct(checked.siteRoots) } : {}),
|
|
54
93
|
};
|
|
94
|
+
// Empty incremental extras restore implicit defaults instead of persisting those defaults.
|
|
95
|
+
if (incremental && checked.siteRoots?.length === 0)
|
|
96
|
+
delete next.siteRoots;
|
|
97
|
+
return next;
|
|
55
98
|
}
|
|
56
99
|
function update({ patch }, { transform } = {}) {
|
|
57
100
|
// Compute incremental root changes inside the same lock as the write: a sibling process's
|
|
@@ -59,25 +102,9 @@ export function createDirectoryUserConfig({ homeDir, defaultFolder, envName, pol
|
|
|
59
102
|
file.withLock({ critical: () => {
|
|
60
103
|
const previous = stored(true);
|
|
61
104
|
const checked = validateDirectoryUserConfig({ value: transform ? transform(read({}), previous) : patch });
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
continue;
|
|
66
|
-
const expanded = expand(directory);
|
|
67
|
-
if (exists(expanded) && !isDirectory(expanded))
|
|
68
|
-
throw new Error(`Sites folder is not a directory: ${directory}`);
|
|
69
|
-
}
|
|
70
|
-
if (checked.sitesHome !== undefined && env[envName] !== undefined)
|
|
71
|
-
throw new Error(`${envName} overrides this setting. Unset it before changing the directory home.`);
|
|
72
|
-
if (checked.siteRoots !== undefined && env[policy.rootsEnvName] !== undefined)
|
|
73
|
-
throw new Error(`${policy.rootsEnvName} overrides this setting. Unset it before changing site roots.`);
|
|
74
|
-
const next = { ...previous, ...checked,
|
|
75
|
-
...(checked.sitesHome !== undefined ? { sitesHome: expand(checked.sitesHome) } : {}),
|
|
76
|
-
...(checked.siteRoots !== undefined ? { siteRoots: distinct(checked.siteRoots) } : {}),
|
|
77
|
-
};
|
|
78
|
-
// Empty incremental extras restore implicit defaults instead of persisting those defaults.
|
|
79
|
-
if (transform && checked.siteRoots?.length === 0)
|
|
80
|
-
delete next.siteRoots;
|
|
105
|
+
assertDirectories(checked);
|
|
106
|
+
assertNotOverridden(checked);
|
|
107
|
+
const next = merged(previous, checked, transform !== undefined);
|
|
81
108
|
// With no other saved settings, removing the final extra also restores an absent config.
|
|
82
109
|
// The Node assembly supplies removal; non-Node adapters may retain the equivalent {}.
|
|
83
110
|
if (transform && Object.keys(next).length === 0 && removeFile)
|
|
@@ -94,50 +121,62 @@ export function createDirectoryUserConfig({ homeDir, defaultFolder, envName, pol
|
|
|
94
121
|
return { siteRoots: distinct([...(saved.siteRoots ?? []), ...add]).filter((root) => !excluded.has(root) && !protectedRoots.has(root)) };
|
|
95
122
|
} });
|
|
96
123
|
}
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
const
|
|
101
|
-
if (
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
124
|
+
// A move journal is written before copying; the old name stays authoritative until verification
|
|
125
|
+
// retires it. Recovery never guesses from two DB sizes/timestamps or asks users to delete data.
|
|
126
|
+
function moveMarker(source, candidates) {
|
|
127
|
+
const markerPath = source + ".home2-relocation.json", marker = fileAt(markerPath).read({});
|
|
128
|
+
if (marker.state === "missing")
|
|
129
|
+
return undefined;
|
|
130
|
+
const value = marker.state === "ok" ? marker.value : undefined;
|
|
131
|
+
const target = value?.to, backup = markerBackup(source, value?.backup);
|
|
132
|
+
if (typeof target !== "string" || !candidates.includes(target) || backup === undefined)
|
|
133
|
+
throw invalidMarker(markerPath);
|
|
134
|
+
return { markerPath, target, backup };
|
|
135
|
+
}
|
|
136
|
+
function authoritativeCopy(source, { markerPath, target, backup }) {
|
|
137
|
+
if (exists(source))
|
|
138
|
+
return source;
|
|
139
|
+
if (exists(backup) && exists(target))
|
|
140
|
+
return target;
|
|
141
|
+
throw new Error(`Interrupted database move needs recovery: ${markerPath}. Restore the pre-move backup before starting.`);
|
|
142
|
+
}
|
|
143
|
+
function recoveredMove(candidates) {
|
|
112
144
|
for (const source of candidates) {
|
|
113
|
-
const
|
|
114
|
-
if (marker
|
|
145
|
+
const marker = moveMarker(source, candidates);
|
|
146
|
+
if (!marker)
|
|
115
147
|
continue;
|
|
116
|
-
const
|
|
117
|
-
|
|
118
|
-
throw new Error(`Interrupted database move has an invalid recovery marker: ${markerPath}. Restore the pre-move backup before starting.`);
|
|
119
|
-
const namedBackup = marker.state === "ok" ? marker.value?.backup : undefined;
|
|
120
|
-
// Old markers use the fixed home2 name; new moves identify their unique backup. Admit
|
|
121
|
-
// only a sibling of this source so malformed metadata cannot supply unrelated evidence.
|
|
122
|
-
if (namedBackup !== undefined && (typeof namedBackup !== "string" || path.dirname(namedBackup) !== path.dirname(source) || !namedBackup.startsWith(source + ".relocation-") || !namedBackup.endsWith("-backup")))
|
|
123
|
-
throw new Error(`Interrupted database move has an invalid recovery marker: ${markerPath}. Restore the pre-move backup before starting.`);
|
|
124
|
-
const backup = typeof namedBackup === "string" ? namedBackup : source + ".home2-backup";
|
|
125
|
-
const authoritative = exists(source) ? source : exists(backup) && exists(target) ? target : undefined;
|
|
126
|
-
if (!authoritative)
|
|
127
|
-
throw new Error(`Interrupted database move needs recovery: ${markerPath}. Restore the pre-move backup before starting.`);
|
|
128
|
-
report(`Recovered interrupted database move using ${authoritative}. Keep the backup files (${backup}); finish the move before changing database placement.`);
|
|
148
|
+
const authoritative = authoritativeCopy(source, marker);
|
|
149
|
+
report(`Recovered interrupted database move using ${authoritative}. Keep the backup files (${marker.backup}); finish the move before changing database placement.`);
|
|
129
150
|
return authoritative;
|
|
130
151
|
}
|
|
152
|
+
return undefined;
|
|
153
|
+
}
|
|
154
|
+
function existingCopy(candidates, filename) {
|
|
131
155
|
const present = candidates.filter(exists);
|
|
132
156
|
if (present.length > 1)
|
|
133
157
|
throw new Error(`Multiple copies of ${filename} exist for this site. Choose the authoritative database before starting it.`);
|
|
134
|
-
|
|
135
|
-
|
|
158
|
+
return present[0];
|
|
159
|
+
}
|
|
160
|
+
function unsafeIdentityPath(legacy, placement) {
|
|
161
|
+
if (!placement && exists(legacy))
|
|
162
|
+
return legacy;
|
|
163
|
+
throw new Error("A database directory requires a safe, stable site identity.");
|
|
164
|
+
}
|
|
165
|
+
function databasePath({ dir, id, filename, placement }, _options = {}) {
|
|
166
|
+
assertDatabaseFilename(filename);
|
|
167
|
+
const inSite = path.join(dir, "data.nosync", filename), legacy = path.join(dir, filename);
|
|
168
|
+
if (!SAFE_SITE_ID.test(id))
|
|
169
|
+
return unsafeIdentityPath(legacy, placement);
|
|
170
|
+
const appData = path.join(directories.appData, "databases", id, filename);
|
|
171
|
+
if (placement)
|
|
172
|
+
return placement === "inSite" ? inSite : appData;
|
|
173
|
+
const candidates = [legacy, inSite, appData];
|
|
174
|
+
const found = recoveredMove(candidates) ?? existingCopy(candidates, filename);
|
|
175
|
+
if (found)
|
|
176
|
+
return found;
|
|
136
177
|
// A missing chat DB follows an existing content DB even after the user changes preferences.
|
|
137
|
-
if (filename !== "content.db")
|
|
138
|
-
|
|
139
|
-
return path.join(path.dirname(content), filename);
|
|
140
|
-
}
|
|
178
|
+
if (filename !== "content.db")
|
|
179
|
+
return path.join(path.dirname(databasePath({ dir, id, filename: "content.db" })), filename);
|
|
141
180
|
return read({}).databases === "inSite" ? inSite : appData;
|
|
142
181
|
}
|
|
143
182
|
return { read, update, updateRoots, databasePath, filePath, ...directories,
|
|
@@ -157,12 +196,17 @@ export function createDirectoryUserConfig({ homeDir, defaultFolder, envName, pol
|
|
|
157
196
|
syncWarning: (_required, _options = {}) => {
|
|
158
197
|
const config = read({});
|
|
159
198
|
const sync = syncProvider(config.sitesHome);
|
|
160
|
-
if (!sync
|
|
199
|
+
if (!syncsDatabases(sync))
|
|
161
200
|
return null;
|
|
162
201
|
return { provider: sync, message: config.databases === "appData" ? `${sync} syncs the sites folder. New databases use OS app-data for safety. Site folders are not self-contained: include their app-data databases when transferring them.` : `${sync} syncs the sites folder. It does not honor data.nosync; syncing live SQLite databases can corrupt them. Use app-data databases or a local sites folder.`, localHome };
|
|
163
202
|
},
|
|
164
203
|
};
|
|
165
204
|
}
|
|
205
|
+
const SETTING_RULES = [
|
|
206
|
+
["sitesHome", (value) => typeof value === "string" && Boolean(value.trim()), "sitesHome must name a directory."],
|
|
207
|
+
["siteRoots", (value) => Array.isArray(value) && value.every((root) => typeof root === "string" && root.trim()), "siteRoots must be an array of directory paths."],
|
|
208
|
+
["databases", (value) => value === "inSite" || value === "appData", 'databases must be "inSite" or "appData".'],
|
|
209
|
+
];
|
|
166
210
|
/** Validate only the three public settings; reject arrays, unknown fields and invalid enums.
|
|
167
211
|
* @complexity O(n) root strings. No I/O or mutation.
|
|
168
212
|
*/
|
|
@@ -170,14 +214,11 @@ export function validateDirectoryUserConfig({ value }, _options = {}) {
|
|
|
170
214
|
if (!value || typeof value !== "object" || Array.isArray(value))
|
|
171
215
|
throw new Error("User config must be an object.");
|
|
172
216
|
const row = value;
|
|
173
|
-
if (Object.keys(row).some((key) => ![
|
|
217
|
+
if (Object.keys(row).some((key) => !SETTING_RULES.some(([known]) => known === key)))
|
|
174
218
|
throw new Error("Unknown user config setting.");
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
throw new Error("siteRoots must be an array of directory paths.");
|
|
179
|
-
if (row.databases !== undefined && row.databases !== "inSite" && row.databases !== "appData")
|
|
180
|
-
throw new Error('databases must be "inSite" or "appData".');
|
|
219
|
+
for (const [key, valid, message] of SETTING_RULES)
|
|
220
|
+
if (row[key] !== undefined && !valid(row[key]))
|
|
221
|
+
throw new Error(message);
|
|
181
222
|
return row;
|
|
182
223
|
}
|
|
183
224
|
//# sourceMappingURL=directory-user-config.js.map
|
|
@@ -129,4 +129,15 @@ export declare function createNodeDirectoryWorkspace({ homeDir, defaultFolder, e
|
|
|
129
129
|
record: import("./directory-running-lock.js").DirectoryRunningRecord;
|
|
130
130
|
};
|
|
131
131
|
};
|
|
132
|
+
/** Canonicalize existing ancestors without creating the selected directory.
|
|
133
|
+
* The default canonicalizer of {@link createNodeDirectoryWorkspace}. `realpath` is the native
|
|
134
|
+
* resolver by default; it is injectable so a missing filesystem root (an unmounted Windows drive)
|
|
135
|
+
* can be exercised on any host. A missing root resolves to its absolute form.
|
|
136
|
+
* @complexity O(d) realpath probes for d path segments.
|
|
137
|
+
*/
|
|
138
|
+
export declare function canonicalNodeDirectory({ path: dir }: {
|
|
139
|
+
path: string;
|
|
140
|
+
}, { realpath }?: {
|
|
141
|
+
realpath?: (dir: string) => string;
|
|
142
|
+
}): string;
|
|
132
143
|
//# sourceMappingURL=directory-workspace.d.ts.map
|
|
@@ -94,6 +94,9 @@ export function createNodeDirectoryWorkspace({ homeDir, defaultFolder, envName,
|
|
|
94
94
|
const previous = oldCatalogs();
|
|
95
95
|
current.merge({ entries: previous.flatMap((catalog) => catalog.list({})), migrations: previous.flatMap((catalog) => catalog.migrations({})), migration: "app-data-catalog-v1" });
|
|
96
96
|
};
|
|
97
|
+
// One lease adapter for both the app-data and the site-local (legacy) lease locations.
|
|
98
|
+
const runningLockAt = (filePath) => createDirectoryRunningLock({ file: fileAt(filePath), isAlive: ports.process.isAlive,
|
|
99
|
+
remove: () => ports.filesystem.rmSync(filePath, { force: true }) }, { ...(ports.process.bootIdentity ? { bootIdentity: ports.process.bootIdentity } : {}) });
|
|
97
100
|
const mutate = (operation) => settings.withLock({ critical: () => { importOldCatalog(); return operation(); } });
|
|
98
101
|
// Serialize pointer changes with catalog mutations. A caller that obtained a registry before
|
|
99
102
|
// settings changed must still write into the current home, never strand a late registration.
|
|
@@ -181,7 +184,6 @@ export function createNodeDirectoryWorkspace({ homeDir, defaultFolder, envName,
|
|
|
181
184
|
const staged = createDirectoryRegistry({ canonicalPath, file: {
|
|
182
185
|
read: () => state === undefined ? { state: "missing" } : { state: "ok", value: state },
|
|
183
186
|
write: ({ value: next }) => { state = next; }, withLock: ({ critical }) => critical(),
|
|
184
|
-
tempPath: () => "", quarantine: () => false,
|
|
185
187
|
} });
|
|
186
188
|
if (!staged.hasMigration({ migration: "app-data-catalog-v1" })) {
|
|
187
189
|
const previous = oldCatalogs();
|
|
@@ -216,14 +218,12 @@ export function createNodeDirectoryWorkspace({ homeDir, defaultFolder, envName,
|
|
|
216
218
|
},
|
|
217
219
|
registry: (_required, _options = {}) => registry,
|
|
218
220
|
running: ({ path: dir }, _options = {}) => {
|
|
219
|
-
const
|
|
220
|
-
const current =
|
|
221
|
-
remove: () => ports.filesystem.rmSync(filePath, { force: true }) }, { ...(ports.process.bootIdentity ? { bootIdentity: ports.process.bootIdentity } : {}) });
|
|
221
|
+
const legacyPath = path.join(canonicalPath({ path: dir }), runningName);
|
|
222
|
+
const current = runningLockAt(config?.runningPath({ dir }) ?? legacyPath);
|
|
222
223
|
if (!config)
|
|
223
224
|
return current;
|
|
224
|
-
|
|
225
|
-
const legacy =
|
|
226
|
-
remove: () => ports.filesystem.rmSync(legacyPath, { force: true }) }, { ...(ports.process.bootIdentity ? { bootIdentity: ports.process.bootIdentity } : {}) });
|
|
225
|
+
// The legacy lease is only ever peeked, so its removal callback is never invoked here.
|
|
226
|
+
const legacy = runningLockAt(legacyPath);
|
|
227
227
|
return { ...current,
|
|
228
228
|
// Older servers may still own a site-local lease. Observe it without moving/editing it;
|
|
229
229
|
// claim serializes against new clients and refuses the old live owner.
|
|
@@ -249,10 +249,15 @@ export function createNodeDirectoryWorkspace({ homeDir, defaultFolder, envName,
|
|
|
249
249
|
overrides: { sitesHome: env[envName] !== undefined, siteRoots: env[userConfig.rootsEnvName] !== undefined } };
|
|
250
250
|
}
|
|
251
251
|
}
|
|
252
|
-
/** Canonicalize existing ancestors without creating the selected directory.
|
|
253
|
-
|
|
252
|
+
/** Canonicalize existing ancestors without creating the selected directory.
|
|
253
|
+
* The default canonicalizer of {@link createNodeDirectoryWorkspace}. `realpath` is the native
|
|
254
|
+
* resolver by default; it is injectable so a missing filesystem root (an unmounted Windows drive)
|
|
255
|
+
* can be exercised on any host. A missing root resolves to its absolute form.
|
|
256
|
+
* @complexity O(d) realpath probes for d path segments.
|
|
257
|
+
*/
|
|
258
|
+
export function canonicalNodeDirectory({ path: dir }, { realpath = fs.realpathSync } = {}) {
|
|
254
259
|
try {
|
|
255
|
-
return
|
|
260
|
+
return realpath(dir);
|
|
256
261
|
}
|
|
257
262
|
catch (error) {
|
|
258
263
|
if (error.code !== "ENOENT")
|
|
@@ -260,7 +265,7 @@ function canonicalNodeDirectory({ path: dir }, _options = {}) {
|
|
|
260
265
|
// Resolve the nearest existing ancestor too: a not-yet-created home may sit below a
|
|
261
266
|
// symlink (notably /var on macOS) and must compare equal before and after creation.
|
|
262
267
|
const absolute = path.resolve(dir), parent = path.dirname(absolute);
|
|
263
|
-
return parent === absolute ? absolute : path.join(canonicalNodeDirectory({ path: parent }), path.basename(absolute));
|
|
268
|
+
return parent === absolute ? absolute : path.join(canonicalNodeDirectory({ path: parent }, { realpath }), path.basename(absolute));
|
|
264
269
|
}
|
|
265
270
|
}
|
|
266
271
|
//# sourceMappingURL=directory-workspace.js.map
|
|
@@ -74,7 +74,9 @@ export interface DurableJsonFile {
|
|
|
74
74
|
}
|
|
75
75
|
/** Atomic replacement, distinct corrupt reads, quarantine, and token-owned synchronous locks.
|
|
76
76
|
* Lock stealing rechecks inode, bytes and mtime; conditional unlink is not available on all hosts.
|
|
77
|
-
* File fsync precedes rename; directory
|
|
77
|
+
* File fsync precedes rename; directory flush afterwards is best-effort: skipped on Windows,
|
|
78
|
+
* unsupported-code failures ignored (EPERM/EISDIR/EINVAL/EACCES/ENOTSUP), other failures reported
|
|
79
|
+
* after replacement. Persistence remains filesystem-dependent.
|
|
78
80
|
*/
|
|
79
81
|
export declare function createDurableJsonFile({ filePath, filesystem: fs, clock, sleep, process, random, report, notices }: DurableJsonPorts & {
|
|
80
82
|
filePath: string;
|
package/dist/fs/durable-json.js
CHANGED
|
@@ -36,7 +36,9 @@ export function createNodeDurableJsonPorts(_required) {
|
|
|
36
36
|
}
|
|
37
37
|
/** Atomic replacement, distinct corrupt reads, quarantine, and token-owned synchronous locks.
|
|
38
38
|
* Lock stealing rechecks inode, bytes and mtime; conditional unlink is not available on all hosts.
|
|
39
|
-
* File fsync precedes rename; directory
|
|
39
|
+
* File fsync precedes rename; directory flush afterwards is best-effort: skipped on Windows,
|
|
40
|
+
* unsupported-code failures ignored (EPERM/EISDIR/EINVAL/EACCES/ENOTSUP), other failures reported
|
|
41
|
+
* after replacement. Persistence remains filesystem-dependent.
|
|
40
42
|
*/
|
|
41
43
|
export function createDurableJsonFile({ filePath, filesystem: fs, clock, sleep, process, random, report, notices }, { pollMs = 10, staleMs = 30000 } = {}) {
|
|
42
44
|
if (!filePath)
|
|
@@ -70,8 +72,9 @@ export function createDurableJsonFile({ filePath, filesystem: fs, clock, sleep,
|
|
|
70
72
|
return `${filePath}.${pid}.tmp`;
|
|
71
73
|
}
|
|
72
74
|
// Same-directory rename gives readers complete old/new versions rather than a torn intermediate.
|
|
73
|
-
// Flush bytes before rename so metadata cannot precede content. The canonical writer
|
|
74
|
-
//
|
|
75
|
+
// Flush bytes before rename so metadata cannot precede content. The canonical writer's parent
|
|
76
|
+
// directory flush is best-effort: skipped on Windows, unsupported-code failures ignored
|
|
77
|
+
// (EPERM/EISDIR/EINVAL/EACCES/ENOTSUP), other failures reported after replacement.
|
|
75
78
|
// Keep the legacy temp path, pretty JSON bytes and new-file create mode for these shared stores.
|
|
76
79
|
function writeJson(filePath, value) {
|
|
77
80
|
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
|
@@ -176,46 +179,58 @@ export function updateDurableJsonFiles({ changes, fileAt, remove }, _options = {
|
|
|
176
179
|
throw new Error("Duplicate JSON transaction path.");
|
|
177
180
|
const ordered = [...changes].sort((a, b) => a.filePath.localeCompare(b.filePath));
|
|
178
181
|
const files = ordered.map((change) => fileAt(change.filePath));
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
182
|
+
withLocksInOrder({ files, critical: () => applyJsonUpdates({ updates: planJsonUpdates({ ordered, files }), remove }) });
|
|
183
|
+
}
|
|
184
|
+
/** Nests each file's lock in path order, then runs the critical section with all of them held. */
|
|
185
|
+
function withLocksInOrder({ files, critical }, { index = 0 } = {}) {
|
|
186
|
+
if (index >= files.length) {
|
|
187
|
+
critical();
|
|
188
|
+
return;
|
|
189
|
+
}
|
|
190
|
+
files[index].withLock({ critical: () => withLocksInOrder({ files, critical }, { index: index + 1 }) });
|
|
191
|
+
}
|
|
192
|
+
/** Every transform runs before any write, so a refusal leaves all participants untouched. */
|
|
193
|
+
function planJsonUpdates({ ordered, files }) {
|
|
194
|
+
return ordered.map((change, i) => {
|
|
195
|
+
const before = files[i].read({});
|
|
196
|
+
if (before.state === "unreadable")
|
|
197
|
+
throw new Error(`JSON reference is unreadable: ${change.filePath}. Nothing was moved.`);
|
|
198
|
+
const previous = before.state === "ok" ? before.value : undefined;
|
|
199
|
+
return { file: files[i], filePath: change.filePath, previous, next: change.transform(previous) };
|
|
200
|
+
}).filter((item) => JSON.stringify(item.previous) !== JSON.stringify(item.next));
|
|
201
|
+
}
|
|
202
|
+
/** Undefined means the file is absent, so it is removed rather than written. */
|
|
203
|
+
function persistJsonValue({ item, value, remove }) {
|
|
204
|
+
if (value === undefined)
|
|
205
|
+
remove({ filePath: item.filePath });
|
|
206
|
+
else
|
|
207
|
+
item.file.write({ value });
|
|
208
|
+
}
|
|
209
|
+
function applyJsonUpdates({ updates, remove }) {
|
|
210
|
+
const attempted = [];
|
|
211
|
+
try {
|
|
212
|
+
for (const item of updates) {
|
|
213
|
+
attempted.push(item); // A write can fail after its rename; include it in rollback.
|
|
214
|
+
persistJsonValue({ item, value: item.next, remove });
|
|
183
215
|
}
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
216
|
+
}
|
|
217
|
+
catch (failure) {
|
|
218
|
+
rollbackJsonUpdates({ attempted, remove, failure });
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
/** Restores prior bytes newest-first; keeps going past individual rollback failures. */
|
|
222
|
+
function rollbackJsonUpdates({ attempted, remove, failure }) {
|
|
223
|
+
const errors = [failure];
|
|
224
|
+
for (const item of attempted.reverse()) {
|
|
192
225
|
try {
|
|
193
|
-
|
|
194
|
-
attempted.push(item); // A write can fail after its rename; include it in rollback.
|
|
195
|
-
if (item.next === undefined)
|
|
196
|
-
remove({ filePath: item.filePath });
|
|
197
|
-
else
|
|
198
|
-
item.file.write({ value: item.next });
|
|
199
|
-
}
|
|
226
|
+
persistJsonValue({ item, value: item.previous, remove });
|
|
200
227
|
}
|
|
201
|
-
catch (
|
|
202
|
-
|
|
203
|
-
for (const item of attempted.reverse()) {
|
|
204
|
-
try {
|
|
205
|
-
if (item.previous === undefined)
|
|
206
|
-
remove({ filePath: item.filePath });
|
|
207
|
-
else
|
|
208
|
-
item.file.write({ value: item.previous });
|
|
209
|
-
}
|
|
210
|
-
catch (error) {
|
|
211
|
-
errors.push(error);
|
|
212
|
-
}
|
|
213
|
-
}
|
|
214
|
-
if (errors.length > 1)
|
|
215
|
-
throw new AggregateError(errors, "JSON reference rollback failed.");
|
|
216
|
-
throw failure;
|
|
228
|
+
catch (error) {
|
|
229
|
+
errors.push(error);
|
|
217
230
|
}
|
|
218
|
-
}
|
|
219
|
-
|
|
231
|
+
}
|
|
232
|
+
if (errors.length > 1)
|
|
233
|
+
throw new AggregateError(errors, "JSON reference rollback failed.");
|
|
234
|
+
throw failure;
|
|
220
235
|
}
|
|
221
236
|
//# sourceMappingURL=durable-json.js.map
|