@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.
Files changed (79) hide show
  1. package/CHANGELOG.md +22 -1
  2. package/dist/asset-cache.js +26 -25
  3. package/dist/blob-storage.d.ts +7 -2
  4. package/dist/blob-storage.js +99 -59
  5. package/dist/child-environment.d.ts +36 -0
  6. package/dist/child-environment.js +68 -0
  7. package/dist/download.js +192 -148
  8. package/dist/fs/atomic-write.js +105 -93
  9. package/dist/fs/boot-identity.d.ts +16 -0
  10. package/dist/fs/boot-identity.js +38 -17
  11. package/dist/fs/directory-home.js +6 -0
  12. package/dist/fs/directory-registry.d.ts +1 -1
  13. package/dist/fs/directory-registry.js +23 -16
  14. package/dist/fs/directory-running-lock.d.ts +1 -1
  15. package/dist/fs/directory-running-lock.js +18 -7
  16. package/dist/fs/directory-sync.d.ts +1 -1
  17. package/dist/fs/directory-sync.js +64 -51
  18. package/dist/fs/directory-user-config.js +108 -67
  19. package/dist/fs/directory-workspace.d.ts +11 -0
  20. package/dist/fs/directory-workspace.js +16 -11
  21. package/dist/fs/durable-json.js +49 -37
  22. package/dist/fs/file-lock.js +214 -149
  23. package/dist/fs/guarded-reader.js +47 -35
  24. package/dist/fs/user-data-directories.d.ts +1 -1
  25. package/dist/fs/user-data-directories.js +25 -9
  26. package/dist/fs/verified-directory-move.d.ts +13 -11
  27. package/dist/fs/verified-directory-move.js +105 -68
  28. package/dist/fs/verified-file-relocation.d.ts +1 -1
  29. package/dist/fs/verified-file-relocation.js +111 -94
  30. package/dist/http/guarded/client.d.ts +3 -4
  31. package/dist/http/guarded/client.js +50 -60
  32. package/dist/http/guarded/fetch-adapter.d.ts +2 -3
  33. package/dist/http/guarded/fetch-adapter.js +26 -15
  34. package/dist/mail/builtin-providers.d.ts +39 -0
  35. package/dist/mail/builtin-providers.js +60 -0
  36. package/dist/mail/feedback.d.ts +84 -0
  37. package/dist/mail/feedback.js +90 -0
  38. package/dist/mail/index.d.ts +11 -0
  39. package/dist/mail/index.js +6 -0
  40. package/dist/mail/policy.d.ts +77 -0
  41. package/dist/mail/policy.js +155 -0
  42. package/dist/mail/ports.d.ts +56 -7
  43. package/dist/mail/provider.d.ts +117 -0
  44. package/dist/mail/provider.js +74 -0
  45. package/dist/mail/rate-limit.d.ts +33 -0
  46. package/dist/mail/rate-limit.js +35 -0
  47. package/dist/mail/sender-identity.d.ts +68 -0
  48. package/dist/mail/sender-identity.js +50 -0
  49. package/dist/mail/types.d.ts +9 -0
  50. package/dist/net/address.d.ts +9 -23
  51. package/dist/net/address.js +28 -133
  52. package/dist/net/endpoint-policy.d.ts +26 -2
  53. package/dist/net/endpoint-policy.js +159 -56
  54. package/dist/object-store/index.d.ts +2 -0
  55. package/dist/object-store/index.js +2 -0
  56. package/dist/object-store/local-fs.d.ts +17 -0
  57. package/dist/object-store/local-fs.js +146 -0
  58. package/dist/process.js +2 -0
  59. package/dist/proxy-env.js +131 -135
  60. package/dist/secrets/credential-sets/contracts.d.ts +1 -0
  61. package/dist/secrets/credential-sets/contracts.js +1 -0
  62. package/dist/secrets/credential-sets/index.d.ts +1 -0
  63. package/dist/secrets/credential-sets/index.js +1 -0
  64. package/dist/secrets/credential-sets/provider-matches.d.ts +61 -0
  65. package/dist/secrets/credential-sets/provider-matches.js +61 -0
  66. package/dist/secrets/credential-sets/types.d.ts +7 -0
  67. package/dist/secrets/credential-sets/vault.meta.js +58 -20
  68. package/dist/secrets/credential-sets/vault.repo.memory.d.ts +7 -0
  69. package/dist/secrets/credential-sets/vault.repo.memory.js +43 -36
  70. package/dist/secrets/credential-sets/vault.service.d.ts +9 -0
  71. package/dist/secrets/credential-sets/vault.service.js +131 -61
  72. package/dist/secrets/credential-sets/vault.types.d.ts +11 -3
  73. package/dist/secrets/credential-sets/vault.types.js +22 -8
  74. package/dist/secrets/keyring.env.js +14 -10
  75. package/dist/toolchain.js +119 -91
  76. package/dist/web/html-document.js +73 -69
  77. package/dist/web/html-to-markdown.js +127 -102
  78. package/dist/web/web-page.js +160 -80
  79. 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 }, { platform = process.platform, env = process.env, appDataDir, readText = (file) => readOptionalText({ file }), windowsDocuments = readWindowsDocuments } = {}) {
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
- if (appDataDir !== undefined && (!appDataDir.trim() || !paths.isAbsolute(appDataDir)))
12
- throw new Error("appDataDir must be an absolute profile directory.");
13
- if (platform === "win32") {
14
- const personal = windowsDocuments();
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
- /** Move only into an absent target. Verify before registry commit or source deletion.
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
- }): void;
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 = stat.isSymbolicLink() ? "link" : stat.isDirectory() ? "directory" : stat.isFile() ? "file" : "unsupported";
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
- const fileDigest = createHash("sha256"), buffer = Buffer.allocUnsafe(64 * 1024);
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 }, { excluded = [], filesystem = fs, prepareDestination, relocateFiles, verifyFile, backupId = randomUUID, platform = process.platform } = {}) {
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
- const before = directoryFingerprint({ dir: source }, { excluded, filesystem });
63
- let copied = false;
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
- let retiredSource;
85
- let staged;
111
+ filesystem.mkdirSync(target);
86
112
  try {
87
- if (directoryFingerprint({ dir: target }, { excluded, filesystem }) !== before)
88
- throw new Error("Moved directory verification failed.");
89
- if (relocateFiles) {
90
- if (!verifyFile)
91
- throw new Error("File relocation requires a verification port.");
92
- staged = stageVerifiedFileRelocation({ groups: relocateFiles({ source, target }), verify: verifyFile }, { filesystem, backupId, platform, backupCovered: ({ from }) => copied && path.resolve(from).startsWith(target + path.sep) });
93
- }
94
- if (copied) {
95
- const backupRoot = path.join(path.dirname(source), `.relocation-${backupId()}-backup`);
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 (error) {
113
- if (retiredSource)
114
- filesystem.renameSync(retiredSource, source);
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
- // If deletion fails after commit, the verified target and registry remain usable; old DB live
123
- // names were already retired before commit. Never roll back a registry whose commit succeeded.
124
- staged?.finalize();
125
- // Keep the retired EXDEV recovery folder; it is never a registered/live site path.
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
- }, { filesystem, backupCovered, backupId, platform }?: {
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 }, { filesystem = fs, backupCovered = () => false, backupId = randomUUID, platform = process.platform } = {}) {
11
- const created = [], retired = [], markers = [], redundantBackups = [];
12
- const backupSuffix = `.relocation-${backupId()}-backup`;
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
- const pairs = ["", ...group.sidecars].filter((suffix) => filesystem.existsSync(group.from + suffix))
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
- rollback();
18
+ rollbackStage(stage);
97
19
  throw error;
98
20
  }
99
- return { rollback, finalize: () => {
100
- for (const backup of redundantBackups) {
101
- filesystem.rmSync(backup);
102
- syncParent(backup);
103
- }
104
- for (const marker of markers) {
105
- filesystem.rmSync(marker);
106
- syncParent(marker);
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 = "public" | "private" | "loopback" | "link-local" | "reserved";
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 function classifyAddress({ ip }: {
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.