@jini-ai/platform 0.5.9 → 0.6.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 +22 -1
- 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.js +105 -93
- 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.js +49 -37
- 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 +13 -11
- package/dist/fs/verified-directory-move.js +105 -68
- package/dist/fs/verified-file-relocation.d.ts +1 -1
- package/dist/fs/verified-file-relocation.js +111 -94
- 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.js +160 -80
- package/package.json +12 -4
|
@@ -6,18 +6,32 @@ import { execFileSync } from "node:child_process";
|
|
|
6
6
|
* env-paths is intentionally not used: its Windows data directory is LocalAppData/<app>/Data.
|
|
7
7
|
* @complexity O(1) bounded configuration reads.
|
|
8
8
|
*/
|
|
9
|
-
export function resolveUserDataDirectories({ homeDir, appName },
|
|
9
|
+
export function resolveUserDataDirectories({ homeDir, appName }, options = {}) {
|
|
10
|
+
const { platform, env, appDataDir, readText, windowsDocuments } = withNativeDefaults(options);
|
|
10
11
|
const paths = platform === "win32" ? path.win32 : path.posix;
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
if (platform === "win32")
|
|
14
|
-
|
|
15
|
-
const documents = personal?.replace(/%([^%]+)%/g, (match, key) => env[key] ?? match);
|
|
16
|
-
return { appData: appDataDir ?? paths.join(env.APPDATA ?? paths.join(homeDir, "AppData", "Roaming"), appName),
|
|
17
|
-
documents: documents ?? paths.join(homeDir, "Documents") };
|
|
18
|
-
}
|
|
12
|
+
assertProfileDirectory({ appDataDir, paths });
|
|
13
|
+
const context = { homeDir, appName, env, appDataDir, paths };
|
|
14
|
+
if (platform === "win32")
|
|
15
|
+
return windowsDirectories(context, { windowsDocuments });
|
|
19
16
|
if (platform === "darwin")
|
|
20
17
|
return { appData: appDataDir ?? paths.join(homeDir, "Library", "Application Support", appName), documents: paths.join(homeDir, "Documents") };
|
|
18
|
+
return xdgDirectories(context, { readText });
|
|
19
|
+
}
|
|
20
|
+
function withNativeDefaults({ platform = process.platform, env = process.env, appDataDir, readText = (file) => readOptionalText({ file }), windowsDocuments = readWindowsDocuments }) {
|
|
21
|
+
return { platform, env, appDataDir, readText, windowsDocuments };
|
|
22
|
+
}
|
|
23
|
+
function assertProfileDirectory({ appDataDir, paths }) {
|
|
24
|
+
// A blank or whitespace-only value is never absolute, so one check refuses both.
|
|
25
|
+
if (appDataDir !== undefined && !paths.isAbsolute(appDataDir))
|
|
26
|
+
throw new Error("appDataDir must be an absolute profile directory.");
|
|
27
|
+
}
|
|
28
|
+
/** Unknown %VARS% stay literal rather than collapsing to an unintended path. */
|
|
29
|
+
function windowsDirectories({ homeDir, appName, env, appDataDir, paths }, { windowsDocuments }) {
|
|
30
|
+
const documents = windowsDocuments()?.replace(/%([^%]+)%/g, (match, key) => env[key] ?? match);
|
|
31
|
+
return { appData: appDataDir ?? paths.join(env.APPDATA ?? paths.join(homeDir, "AppData", "Roaming"), appName),
|
|
32
|
+
documents: documents ?? paths.join(homeDir, "Documents") };
|
|
33
|
+
}
|
|
34
|
+
function xdgDirectories({ homeDir, appName, env, appDataDir, paths }, { readText }) {
|
|
21
35
|
const userDirs = readText(paths.join(env.XDG_CONFIG_HOME ?? paths.join(homeDir, ".config"), "user-dirs.dirs"));
|
|
22
36
|
const configured = /^XDG_DOCUMENTS_DIR="([^"\n]*)"/m.exec(userDirs ?? "")?.[1]?.replace(/\$HOME/g, homeDir);
|
|
23
37
|
return { appData: appDataDir ?? paths.join(env.XDG_DATA_HOME ?? paths.join(homeDir, ".local", "share"), appName),
|
|
@@ -35,12 +49,14 @@ export function readOptionalText({ file }, _options = {}) {
|
|
|
35
49
|
}
|
|
36
50
|
}
|
|
37
51
|
function readWindowsDocuments() {
|
|
52
|
+
// eslint-disable-next-line sonarjs/no-os-command-from-path -- Fixed Windows registry utility/argv intentionally uses the desktop operator's trusted PATH, not request input.
|
|
38
53
|
const output = execFileSync("reg.exe", ["query", "HKCU\\Software\\Microsoft\\Windows\\CurrentVersion\\Explorer\\User Shell Folders", "/v", "Personal"], { encoding: "utf8", timeout: 3000, windowsHide: true });
|
|
39
54
|
return /Personal\s+REG_(?:EXPAND_)?SZ\s+(.+)/.exec(output)?.[1]?.trim();
|
|
40
55
|
}
|
|
41
56
|
/** OneDrive publishes account mount paths in HKCU; absent accounts are a normal empty result. */
|
|
42
57
|
export function readWindowsOneDriveRoots(_required, _options = {}) {
|
|
43
58
|
try {
|
|
59
|
+
// eslint-disable-next-line sonarjs/no-os-command-from-path -- Fixed Windows registry utility/argv intentionally uses the desktop operator's trusted PATH, not request input.
|
|
44
60
|
const output = execFileSync("reg.exe", ["query", "HKCU\\Software\\Microsoft\\OneDrive\\Accounts", "/s", "/v", "UserFolder"], { encoding: "utf8", timeout: 3000, windowsHide: true });
|
|
45
61
|
return [...output.matchAll(/UserFolder\s+REG_(?:EXPAND_)?SZ\s+([^\r\n]+)/g)].map((match) => match[1].trim());
|
|
46
62
|
}
|
|
@@ -10,16 +10,7 @@ export declare function directoryFingerprint({ dir }: {
|
|
|
10
10
|
excluded?: readonly string[];
|
|
11
11
|
filesystem?: typeof fs;
|
|
12
12
|
}): string;
|
|
13
|
-
|
|
14
|
-
* EXDEV uses copy/verify/retire/commit; failure restores the source and removes only our new copy.
|
|
15
|
-
* Same-filesystem failures roll the rename back. Commit must atomically update the host registry.
|
|
16
|
-
* @complexity O(bytes + entries log entries), with two verification passes.
|
|
17
|
-
*/
|
|
18
|
-
export declare function moveDirectoryVerified({ from, to, commit }: {
|
|
19
|
-
from: string;
|
|
20
|
-
to: string;
|
|
21
|
-
commit: () => void;
|
|
22
|
-
}, { excluded, filesystem, prepareDestination, relocateFiles, verifyFile, backupId, platform }?: {
|
|
13
|
+
interface MoveDirectoryOptions {
|
|
23
14
|
excluded?: readonly string[];
|
|
24
15
|
filesystem?: typeof fs;
|
|
25
16
|
prepareDestination?: (required: {
|
|
@@ -34,5 +25,16 @@ export declare function moveDirectoryVerified({ from, to, commit }: {
|
|
|
34
25
|
}) => string;
|
|
35
26
|
backupId?: () => string;
|
|
36
27
|
platform?: NodeJS.Platform;
|
|
37
|
-
}
|
|
28
|
+
}
|
|
29
|
+
/** Move only into an absent target. Verify before registry commit or source deletion.
|
|
30
|
+
* EXDEV uses copy/verify/retire/commit; failure restores the source and removes only our new copy.
|
|
31
|
+
* Same-filesystem failures roll the rename back. Commit must atomically update the host registry.
|
|
32
|
+
* @complexity O(bytes + entries log entries), with two verification passes.
|
|
33
|
+
*/
|
|
34
|
+
export declare function moveDirectoryVerified({ from, to, commit }: {
|
|
35
|
+
from: string;
|
|
36
|
+
to: string;
|
|
37
|
+
commit: () => void;
|
|
38
|
+
}, options?: MoveDirectoryOptions): void;
|
|
39
|
+
export {};
|
|
38
40
|
//# sourceMappingURL=verified-directory-move.d.ts.map
|
|
@@ -15,7 +15,7 @@ export function directoryFingerprint({ dir }, { excluded = [], filesystem = fs }
|
|
|
15
15
|
continue;
|
|
16
16
|
const name = path.join(relative, entry.name), absolute = path.join(dir, name);
|
|
17
17
|
const stat = filesystem.lstatSync(absolute);
|
|
18
|
-
const kind =
|
|
18
|
+
const kind = entryKind(stat);
|
|
19
19
|
if (kind === "unsupported")
|
|
20
20
|
throw new Error(`Cannot move special filesystem entry: ${name}`);
|
|
21
21
|
digest.update(JSON.stringify([name, kind, stat.mode & 0o777]));
|
|
@@ -23,32 +23,70 @@ export function directoryFingerprint({ dir }, { excluded = [], filesystem = fs }
|
|
|
23
23
|
visit(name);
|
|
24
24
|
else if (kind === "link")
|
|
25
25
|
digest.update(JSON.stringify(filesystem.readlinkSync(absolute)));
|
|
26
|
-
else
|
|
27
|
-
|
|
28
|
-
const descriptor = filesystem.openSync(absolute, "r");
|
|
29
|
-
try {
|
|
30
|
-
let bytes;
|
|
31
|
-
while ((bytes = filesystem.readSync(descriptor, buffer, 0, buffer.length, null)) > 0)
|
|
32
|
-
fileDigest.update(buffer.subarray(0, bytes));
|
|
33
|
-
}
|
|
34
|
-
finally {
|
|
35
|
-
filesystem.closeSync(descriptor);
|
|
36
|
-
}
|
|
37
|
-
digest.update(fileDigest.digest());
|
|
38
|
-
}
|
|
26
|
+
else
|
|
27
|
+
digest.update(fileDigest({ absolute, filesystem }));
|
|
39
28
|
}
|
|
40
29
|
}
|
|
41
30
|
visit("");
|
|
42
31
|
return digest.digest("hex");
|
|
43
32
|
}
|
|
33
|
+
function entryKind(stat) {
|
|
34
|
+
if (stat.isSymbolicLink())
|
|
35
|
+
return "link";
|
|
36
|
+
if (stat.isDirectory())
|
|
37
|
+
return "directory";
|
|
38
|
+
return stat.isFile() ? "file" : "unsupported";
|
|
39
|
+
}
|
|
40
|
+
function fileDigest({ absolute, filesystem }) {
|
|
41
|
+
const digest = createHash("sha256"), buffer = Buffer.allocUnsafe(64 * 1024);
|
|
42
|
+
const descriptor = filesystem.openSync(absolute, "r");
|
|
43
|
+
try {
|
|
44
|
+
let bytes;
|
|
45
|
+
while ((bytes = filesystem.readSync(descriptor, buffer, 0, buffer.length, null)) > 0)
|
|
46
|
+
digest.update(buffer.subarray(0, bytes));
|
|
47
|
+
}
|
|
48
|
+
finally {
|
|
49
|
+
filesystem.closeSync(descriptor);
|
|
50
|
+
}
|
|
51
|
+
return digest.digest();
|
|
52
|
+
}
|
|
53
|
+
function withMoveDefaults({ excluded = [], filesystem = fs, prepareDestination, relocateFiles, verifyFile, backupId = randomUUID, platform = process.platform }) {
|
|
54
|
+
return { excluded, filesystem, prepareDestination, relocateFiles, verifyFile, backupId, platform };
|
|
55
|
+
}
|
|
44
56
|
/** Move only into an absent target. Verify before registry commit or source deletion.
|
|
45
57
|
* EXDEV uses copy/verify/retire/commit; failure restores the source and removes only our new copy.
|
|
46
58
|
* Same-filesystem failures roll the rename back. Commit must atomically update the host registry.
|
|
47
59
|
* @complexity O(bytes + entries log entries), with two verification passes.
|
|
48
60
|
*/
|
|
49
|
-
export function moveDirectoryVerified({ from, to, commit },
|
|
61
|
+
export function moveDirectoryVerified({ from, to, commit }, options = {}) {
|
|
62
|
+
const resolved = withMoveDefaults(options), { excluded, filesystem } = resolved;
|
|
50
63
|
const source = filesystem.realpathSync(from);
|
|
51
64
|
const target = path.resolve(to);
|
|
65
|
+
assertAbsentTarget({ source, target }, { filesystem });
|
|
66
|
+
const before = directoryFingerprint({ dir: source }, { excluded, filesystem });
|
|
67
|
+
const copied = placeAtTarget({ source, target }, resolved);
|
|
68
|
+
const progress = {};
|
|
69
|
+
try {
|
|
70
|
+
if (directoryFingerprint({ dir: target }, { excluded, filesystem }) !== before)
|
|
71
|
+
throw new Error("Moved directory verification failed.");
|
|
72
|
+
progress.staged = stageRelocation({ source, target, copied }, resolved);
|
|
73
|
+
if (copied)
|
|
74
|
+
retireCopiedSource({ source, progress }, resolved);
|
|
75
|
+
// Where supported, persist directory-name changes for same-filesystem renames as well as EXDEV retirement.
|
|
76
|
+
for (const directory of new Set([path.dirname(source), path.dirname(target)]))
|
|
77
|
+
syncDirectory({ directory }, resolved);
|
|
78
|
+
commit();
|
|
79
|
+
}
|
|
80
|
+
catch (error) {
|
|
81
|
+
rollbackMove({ source, target, copied, ...progress }, resolved);
|
|
82
|
+
throw error;
|
|
83
|
+
}
|
|
84
|
+
// If deletion fails after commit, the verified target and registry remain usable; old DB live
|
|
85
|
+
// names were already retired before commit. Never roll back a registry whose commit succeeded.
|
|
86
|
+
progress.staged?.finalize();
|
|
87
|
+
// Keep the retired EXDEV recovery folder; it is never a registered/live site path.
|
|
88
|
+
}
|
|
89
|
+
function assertAbsentTarget({ source, target }, { filesystem }) {
|
|
52
90
|
if (target === source || target.startsWith(source + path.sep))
|
|
53
91
|
throw new Error("A directory cannot be moved into itself.");
|
|
54
92
|
try {
|
|
@@ -59,69 +97,68 @@ export function moveDirectoryVerified({ from, to, commit }, { excluded = [], fil
|
|
|
59
97
|
if (error.code !== "ENOENT")
|
|
60
98
|
throw error;
|
|
61
99
|
}
|
|
62
|
-
|
|
63
|
-
|
|
100
|
+
}
|
|
101
|
+
/** Same-filesystem rename, or an EXDEV copy into a freshly created target. Returns whether it copied. */
|
|
102
|
+
function placeAtTarget({ source, target }, { filesystem, excluded, prepareDestination }) {
|
|
64
103
|
try {
|
|
65
104
|
filesystem.renameSync(source, target);
|
|
105
|
+
return false;
|
|
66
106
|
}
|
|
67
107
|
catch (error) {
|
|
68
108
|
if (error.code !== "EXDEV")
|
|
69
109
|
throw error;
|
|
70
|
-
filesystem.mkdirSync(target);
|
|
71
|
-
copied = true;
|
|
72
|
-
try {
|
|
73
|
-
// The host may reserve the empty destination before any commit marker or database is copied.
|
|
74
|
-
prepareDestination?.({ path: target });
|
|
75
|
-
filesystem.cpSync(source, target, { recursive: true, force: false, errorOnExist: true, preserveTimestamps: true, verbatimSymlinks: true,
|
|
76
|
-
filter: (candidate) => candidate === source || !excluded.includes(path.relative(source, candidate).split(path.sep)[0]),
|
|
77
|
-
});
|
|
78
|
-
}
|
|
79
|
-
catch (failure) {
|
|
80
|
-
filesystem.rmSync(target, { recursive: true });
|
|
81
|
-
throw failure;
|
|
82
|
-
}
|
|
83
110
|
}
|
|
84
|
-
|
|
85
|
-
let staged;
|
|
111
|
+
filesystem.mkdirSync(target);
|
|
86
112
|
try {
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
const backup = path.join(backupRoot, path.basename(source));
|
|
97
|
-
if (filesystem.existsSync(backup))
|
|
98
|
-
throw new Error(`Move backup target already exists: ${backup}`);
|
|
99
|
-
// The EXDEV original becomes a recovery folder before catalog commit, never a second live
|
|
100
|
-
// site at the old path if cleanup fails. An interrupted apply leaves explicit backups.
|
|
101
|
-
filesystem.mkdirSync(backupRoot);
|
|
102
|
-
filesystem.renameSync(source, backup);
|
|
103
|
-
retiredSource = backup;
|
|
104
|
-
syncDirectoryBestEffort({ directory: backupRoot, open: filesystem.openSync, sync: filesystem.fsyncSync, close: filesystem.closeSync }, { platform });
|
|
105
|
-
}
|
|
106
|
-
// Where supported, persist directory-name changes for same-filesystem renames as well as EXDEV retirement.
|
|
107
|
-
for (const directory of new Set([path.dirname(source), path.dirname(target)])) {
|
|
108
|
-
syncDirectoryBestEffort({ directory, open: filesystem.openSync, sync: filesystem.fsyncSync, close: filesystem.closeSync }, { platform });
|
|
113
|
+
// The host may reserve the empty destination before any commit marker or database is copied.
|
|
114
|
+
prepareDestination?.({ path: target });
|
|
115
|
+
// MOVE: Copy entries into the reserved root so exclusive copy never collides with that root.
|
|
116
|
+
for (const entry of filesystem.readdirSync(source)) {
|
|
117
|
+
if (excluded.includes(entry))
|
|
118
|
+
continue;
|
|
119
|
+
filesystem.cpSync(path.join(source, entry), path.join(target, entry), {
|
|
120
|
+
recursive: true, force: false, errorOnExist: true, preserveTimestamps: true, verbatimSymlinks: true,
|
|
121
|
+
});
|
|
109
122
|
}
|
|
110
|
-
commit();
|
|
111
123
|
}
|
|
112
|
-
catch (
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
staged?.rollback();
|
|
116
|
-
if (copied)
|
|
117
|
-
filesystem.rmSync(target, { recursive: true });
|
|
118
|
-
else
|
|
119
|
-
filesystem.renameSync(target, source);
|
|
120
|
-
throw error;
|
|
124
|
+
catch (failure) {
|
|
125
|
+
filesystem.rmSync(target, { recursive: true });
|
|
126
|
+
throw failure;
|
|
121
127
|
}
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
128
|
+
return true;
|
|
129
|
+
}
|
|
130
|
+
function stageRelocation({ source, target, copied }, { relocateFiles, verifyFile, filesystem, backupId, platform }) {
|
|
131
|
+
if (!relocateFiles)
|
|
132
|
+
return undefined;
|
|
133
|
+
if (!verifyFile)
|
|
134
|
+
throw new Error("File relocation requires a verification port.");
|
|
135
|
+
return stageVerifiedFileRelocation({ groups: relocateFiles({ source, target }), verify: verifyFile }, { filesystem, backupId, platform, backupCovered: ({ from }) => copied && path.resolve(from).startsWith(target + path.sep) });
|
|
136
|
+
}
|
|
137
|
+
/** Records the retired path in `progress` right after the rename, before the directory sync, so a
|
|
138
|
+
* failed fsync still lets rollback restore the source before it removes the copied target. */
|
|
139
|
+
function retireCopiedSource({ source, progress }, resolved) {
|
|
140
|
+
const { filesystem, backupId } = resolved;
|
|
141
|
+
const backupRoot = path.join(path.dirname(source), `.relocation-${backupId()}-backup`);
|
|
142
|
+
const backup = path.join(backupRoot, path.basename(source));
|
|
143
|
+
if (filesystem.existsSync(backup))
|
|
144
|
+
throw new Error(`Move backup target already exists: ${backup}`);
|
|
145
|
+
// The EXDEV original becomes a recovery folder before catalog commit, never a second live
|
|
146
|
+
// site at the old path if cleanup fails. An interrupted apply leaves explicit backups.
|
|
147
|
+
filesystem.mkdirSync(backupRoot);
|
|
148
|
+
filesystem.renameSync(source, backup);
|
|
149
|
+
progress.retiredSource = backup;
|
|
150
|
+
syncDirectory({ directory: backupRoot }, resolved);
|
|
151
|
+
}
|
|
152
|
+
function syncDirectory({ directory }, { filesystem, platform }) {
|
|
153
|
+
syncDirectoryBestEffort({ directory, open: filesystem.openSync, sync: filesystem.fsyncSync, close: filesystem.closeSync }, { platform });
|
|
154
|
+
}
|
|
155
|
+
function rollbackMove({ source, target, copied, retiredSource, staged }, { filesystem }) {
|
|
156
|
+
if (retiredSource)
|
|
157
|
+
filesystem.renameSync(retiredSource, source);
|
|
158
|
+
staged?.rollback();
|
|
159
|
+
if (copied)
|
|
160
|
+
filesystem.rmSync(target, { recursive: true });
|
|
161
|
+
else
|
|
162
|
+
filesystem.renameSync(target, source);
|
|
126
163
|
}
|
|
127
164
|
//# sourceMappingURL=verified-directory-move.js.map
|
|
@@ -14,7 +14,7 @@ export declare function stageVerifiedFileRelocation({ groups, verify }: {
|
|
|
14
14
|
verify: (required: {
|
|
15
15
|
path: string;
|
|
16
16
|
}) => string;
|
|
17
|
-
},
|
|
17
|
+
}, options?: {
|
|
18
18
|
filesystem?: typeof fs;
|
|
19
19
|
backupCovered?: (required: {
|
|
20
20
|
from: string;
|
|
@@ -7,104 +7,121 @@ import { syncDirectoryBestEffort } from "./atomic-write.js";
|
|
|
7
7
|
* stopped databases through an injected read-only inspector; the filesystem owner never uses SQL.
|
|
8
8
|
* @complexity O(total bytes + verification cost), bounded memory via native file copies.
|
|
9
9
|
*/
|
|
10
|
-
export function stageVerifiedFileRelocation({ groups, verify },
|
|
11
|
-
const
|
|
12
|
-
const
|
|
13
|
-
const copyExclusive = (from, to) => {
|
|
14
|
-
// Reserve before copying so a disk-full failure is still ours to clean up. COPYFILE_EXCL
|
|
15
|
-
// alone may create a partial file before throwing, which cannot be tracked after the fact.
|
|
16
|
-
const fd = filesystem.openSync(to, "wx");
|
|
17
|
-
created.push(to);
|
|
18
|
-
filesystem.closeSync(fd);
|
|
19
|
-
filesystem.copyFileSync(from, to);
|
|
20
|
-
const copied = filesystem.openSync(to, "r+");
|
|
21
|
-
try {
|
|
22
|
-
filesystem.fsyncSync(copied);
|
|
23
|
-
}
|
|
24
|
-
finally {
|
|
25
|
-
filesystem.closeSync(copied);
|
|
26
|
-
}
|
|
27
|
-
syncParent(to);
|
|
28
|
-
};
|
|
29
|
-
const syncParent = (file) => {
|
|
30
|
-
// Where supported, directory fsync persists retirement/marker ordering before the host commits its catalog.
|
|
31
|
-
syncDirectoryBestEffort({ directory: path.dirname(file), open: filesystem.openSync, sync: filesystem.fsyncSync, close: filesystem.closeSync }, { platform });
|
|
32
|
-
};
|
|
33
|
-
const rollback = () => {
|
|
34
|
-
for (const item of [...retired].reverse()) {
|
|
35
|
-
filesystem.renameSync(item.backup, item.from);
|
|
36
|
-
syncParent(item.from);
|
|
37
|
-
}
|
|
38
|
-
for (const file of [...created].reverse()) {
|
|
39
|
-
filesystem.rmSync(file, { force: true });
|
|
40
|
-
syncParent(file);
|
|
41
|
-
}
|
|
42
|
-
};
|
|
10
|
+
export function stageVerifiedFileRelocation({ groups, verify }, options = {}) {
|
|
11
|
+
const { filesystem, backupCovered, backupId, platform } = withStageDefaults(options);
|
|
12
|
+
const stage = { filesystem, platform, backupSuffix: `.relocation-${backupId()}-backup`, created: [], retired: [], markers: [], redundantBackups: [] };
|
|
43
13
|
try {
|
|
44
|
-
for (const group of groups)
|
|
45
|
-
|
|
46
|
-
.map((suffix) => ({ from: group.from + suffix, to: group.to + suffix }));
|
|
47
|
-
for (const pair of pairs)
|
|
48
|
-
if (!filesystem.lstatSync(pair.from).isFile())
|
|
49
|
-
throw new Error(`Database move requires a regular file: ${pair.from}`);
|
|
50
|
-
filesystem.mkdirSync(path.dirname(group.to), { recursive: true });
|
|
51
|
-
const moving = path.resolve(group.from) !== path.resolve(group.to);
|
|
52
|
-
// A directory rename already carries unchanged DB paths atomically; verify those bytes
|
|
53
|
-
// without copying them. For a file move the original inode becomes the sole backup after
|
|
54
|
-
// verification: another full destination backup doubles storage without rollback benefit.
|
|
55
|
-
// Cross-volume directory moves already retain the untouched original directory. A copied
|
|
56
|
-
// DB inside that destination needs no second full backup of the same bytes.
|
|
57
|
-
if (moving) {
|
|
58
|
-
const marker = group.from + ".home2-relocation.json";
|
|
59
|
-
const fd = filesystem.openSync(marker, "wx");
|
|
60
|
-
created.push(marker);
|
|
61
|
-
markers.push(marker);
|
|
62
|
-
try {
|
|
63
|
-
filesystem.writeFileSync(fd, JSON.stringify({ to: group.to, backup: group.from + backupSuffix }));
|
|
64
|
-
filesystem.fsyncSync(fd);
|
|
65
|
-
}
|
|
66
|
-
finally {
|
|
67
|
-
filesystem.closeSync(fd);
|
|
68
|
-
}
|
|
69
|
-
syncParent(marker);
|
|
70
|
-
}
|
|
71
|
-
const before = verify({ path: group.from });
|
|
72
|
-
for (const pair of pairs) {
|
|
73
|
-
if (path.resolve(pair.from) === path.resolve(pair.to))
|
|
74
|
-
continue;
|
|
75
|
-
// A destination collision aborts; never overwrite another site's data or an old move.
|
|
76
|
-
copyExclusive(pair.from, pair.to);
|
|
77
|
-
}
|
|
78
|
-
if (verify({ path: group.to }) !== before)
|
|
79
|
-
throw new Error(`Database row counts changed while moving ${path.basename(group.to)}.`);
|
|
80
|
-
// Retire old live names BEFORE commit. Finalization removes recovery markers and redundant copies covered by the retained original: a crash
|
|
81
|
-
// or cleanup failure cannot leave two independently writable database names after commit.
|
|
82
|
-
if (moving)
|
|
83
|
-
for (const pair of pairs) {
|
|
84
|
-
const backup = pair.from + backupSuffix;
|
|
85
|
-
if (filesystem.existsSync(backup))
|
|
86
|
-
throw new Error(`Database backup target already exists: ${backup}`);
|
|
87
|
-
filesystem.renameSync(pair.from, backup);
|
|
88
|
-
retired.push({ from: pair.from, backup });
|
|
89
|
-
if (backupCovered({ from: pair.from }))
|
|
90
|
-
redundantBackups.push(backup);
|
|
91
|
-
syncParent(pair.from);
|
|
92
|
-
}
|
|
93
|
-
}
|
|
14
|
+
for (const group of groups)
|
|
15
|
+
stageGroup({ stage, group, verify, backupCovered });
|
|
94
16
|
}
|
|
95
17
|
catch (error) {
|
|
96
|
-
|
|
18
|
+
rollbackStage(stage);
|
|
97
19
|
throw error;
|
|
98
20
|
}
|
|
99
|
-
return { rollback, finalize: () =>
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
21
|
+
return { rollback: () => rollbackStage(stage), finalize: () => finalizeStage(stage) };
|
|
22
|
+
}
|
|
23
|
+
function withStageDefaults({ filesystem = fs, backupCovered = () => false, backupId = randomUUID, platform = process.platform }) {
|
|
24
|
+
return { filesystem, backupCovered, backupId, platform };
|
|
25
|
+
}
|
|
26
|
+
function stageGroup({ stage, group, verify, backupCovered }) {
|
|
27
|
+
const { filesystem } = stage;
|
|
28
|
+
const pairs = existingRegularPairs({ stage, group });
|
|
29
|
+
filesystem.mkdirSync(path.dirname(group.to), { recursive: true });
|
|
30
|
+
const moving = path.resolve(group.from) !== path.resolve(group.to);
|
|
31
|
+
// A directory rename already carries unchanged DB paths atomically; verify those bytes
|
|
32
|
+
// without copying them. For a file move the original inode becomes the sole backup after
|
|
33
|
+
// verification: another full destination backup doubles storage without rollback benefit.
|
|
34
|
+
// Cross-volume directory moves already retain the untouched original directory. A copied
|
|
35
|
+
// DB inside that destination needs no second full backup of the same bytes.
|
|
36
|
+
if (moving)
|
|
37
|
+
writeRelocationMarker({ stage, group });
|
|
38
|
+
const before = verify({ path: group.from });
|
|
39
|
+
for (const pair of pairs) {
|
|
40
|
+
if (path.resolve(pair.from) === path.resolve(pair.to))
|
|
41
|
+
continue;
|
|
42
|
+
// A destination collision aborts; never overwrite another site's data or an old move.
|
|
43
|
+
copyExclusive({ stage, ...pair });
|
|
44
|
+
}
|
|
45
|
+
if (verify({ path: group.to }) !== before)
|
|
46
|
+
throw new Error(`Database row counts changed while moving ${path.basename(group.to)}.`);
|
|
47
|
+
// Retire old live names BEFORE commit. Finalization removes recovery markers and redundant copies covered by the retained original: a crash
|
|
48
|
+
// or cleanup failure cannot leave two independently writable database names after commit.
|
|
49
|
+
if (moving)
|
|
50
|
+
for (const pair of pairs)
|
|
51
|
+
retireLiveName({ stage, pair, backupCovered });
|
|
52
|
+
}
|
|
53
|
+
function existingRegularPairs({ stage: { filesystem }, group }) {
|
|
54
|
+
const pairs = ["", ...group.sidecars].filter((suffix) => filesystem.existsSync(group.from + suffix))
|
|
55
|
+
.map((suffix) => ({ from: group.from + suffix, to: group.to + suffix }));
|
|
56
|
+
for (const pair of pairs)
|
|
57
|
+
if (!filesystem.lstatSync(pair.from).isFile())
|
|
58
|
+
throw new Error(`Database move requires a regular file: ${pair.from}`);
|
|
59
|
+
return pairs;
|
|
60
|
+
}
|
|
61
|
+
function writeRelocationMarker({ stage, group }) {
|
|
62
|
+
const { filesystem } = stage;
|
|
63
|
+
const marker = group.from + ".home2-relocation.json";
|
|
64
|
+
const fd = filesystem.openSync(marker, "wx");
|
|
65
|
+
stage.created.push(marker);
|
|
66
|
+
stage.markers.push(marker);
|
|
67
|
+
try {
|
|
68
|
+
filesystem.writeFileSync(fd, JSON.stringify({ to: group.to, backup: group.from + stage.backupSuffix }));
|
|
69
|
+
filesystem.fsyncSync(fd);
|
|
70
|
+
}
|
|
71
|
+
finally {
|
|
72
|
+
filesystem.closeSync(fd);
|
|
73
|
+
}
|
|
74
|
+
syncParent({ stage, file: marker });
|
|
75
|
+
}
|
|
76
|
+
function copyExclusive({ stage, from, to }) {
|
|
77
|
+
const { filesystem } = stage;
|
|
78
|
+
// Reserve before copying so a disk-full failure is still ours to clean up. COPYFILE_EXCL
|
|
79
|
+
// alone may create a partial file before throwing, which cannot be tracked after the fact.
|
|
80
|
+
const fd = filesystem.openSync(to, "wx");
|
|
81
|
+
stage.created.push(to);
|
|
82
|
+
filesystem.closeSync(fd);
|
|
83
|
+
filesystem.copyFileSync(from, to);
|
|
84
|
+
const copied = filesystem.openSync(to, "r+");
|
|
85
|
+
try {
|
|
86
|
+
filesystem.fsyncSync(copied);
|
|
87
|
+
}
|
|
88
|
+
finally {
|
|
89
|
+
filesystem.closeSync(copied);
|
|
90
|
+
}
|
|
91
|
+
syncParent({ stage, file: to });
|
|
92
|
+
}
|
|
93
|
+
function retireLiveName({ stage, pair, backupCovered }) {
|
|
94
|
+
const backup = pair.from + stage.backupSuffix;
|
|
95
|
+
if (stage.filesystem.existsSync(backup))
|
|
96
|
+
throw new Error(`Database backup target already exists: ${backup}`);
|
|
97
|
+
stage.filesystem.renameSync(pair.from, backup);
|
|
98
|
+
stage.retired.push({ from: pair.from, backup });
|
|
99
|
+
if (backupCovered({ from: pair.from }))
|
|
100
|
+
stage.redundantBackups.push(backup);
|
|
101
|
+
syncParent({ stage, file: pair.from });
|
|
102
|
+
}
|
|
103
|
+
function syncParent({ stage: { filesystem, platform }, file }) {
|
|
104
|
+
// Where supported, directory fsync persists retirement/marker ordering before the host commits its catalog.
|
|
105
|
+
syncDirectoryBestEffort({ directory: path.dirname(file), open: filesystem.openSync, sync: filesystem.fsyncSync, close: filesystem.closeSync }, { platform });
|
|
106
|
+
}
|
|
107
|
+
function rollbackStage(stage) {
|
|
108
|
+
for (const item of [...stage.retired].reverse()) {
|
|
109
|
+
stage.filesystem.renameSync(item.backup, item.from);
|
|
110
|
+
syncParent({ stage, file: item.from });
|
|
111
|
+
}
|
|
112
|
+
for (const file of [...stage.created].reverse()) {
|
|
113
|
+
stage.filesystem.rmSync(file, { force: true });
|
|
114
|
+
syncParent({ stage, file });
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
function finalizeStage(stage) {
|
|
118
|
+
for (const backup of stage.redundantBackups) {
|
|
119
|
+
stage.filesystem.rmSync(backup);
|
|
120
|
+
syncParent({ stage, file: backup });
|
|
121
|
+
}
|
|
122
|
+
for (const marker of stage.markers) {
|
|
123
|
+
stage.filesystem.rmSync(marker);
|
|
124
|
+
syncParent({ stage, file: marker });
|
|
125
|
+
}
|
|
109
126
|
}
|
|
110
127
|
//# sourceMappingURL=verified-file-relocation.js.map
|
|
@@ -1,10 +1,9 @@
|
|
|
1
|
+
import { classifyIpAddress } from "../../net/address.js";
|
|
1
2
|
import type { HttpClientPort } from "@jini-ai/core/primitives";
|
|
2
3
|
import type { GuardedHttpRequired } from "./ports.js";
|
|
3
|
-
export type AddressClass
|
|
4
|
+
export type { AddressClass } from "../../net/address.js";
|
|
4
5
|
/** Uses the canonical net address validator; labels preserve the guarded refusal contract. */
|
|
5
|
-
export declare
|
|
6
|
-
ip: string;
|
|
7
|
-
}): AddressClass;
|
|
6
|
+
export declare const classifyAddress: typeof classifyIpAddress;
|
|
8
7
|
/** Builds an adapter-friendly outbound port. Every hop is resolved, checked, and pinned before I/O.
|
|
9
8
|
* Socket idle time is bounded on every hop. An optional totalDeadlineMs includes DNS,
|
|
10
9
|
* redirect hops and decoded body reads; it is never inferred from the idle budget.
|