@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
@@ -17,38 +17,109 @@ export function createNodeAtomicFilesystem(_required, { filesystem = fs } = {})
17
17
  remove: ({ path }) => filesystem.rmSync(path, { force: true }),
18
18
  };
19
19
  }
20
+ const DIRECTORY_SYNC_UNSUPPORTED = ["EPERM", "EISDIR", "EINVAL", "EACCES", "ENOTSUP"];
21
+ function hasErrorCode(error, codes) {
22
+ return typeof error === "object" && error !== null && "code" in error && typeof error.code === "string" && codes.includes(error.code);
23
+ }
20
24
  function isMissing(error) {
21
- return typeof error === "object" && error !== null && "code" in error && error.code === "ENOENT";
25
+ return hasErrorCode(error, ["ENOENT"]);
26
+ }
27
+ /** Runs a probe where only ENOENT means absent; every other failure propagates. */
28
+ function ignoreMissing(probe) {
29
+ try {
30
+ return probe();
31
+ }
32
+ catch (error) {
33
+ if (!isMissing(error))
34
+ throw error;
35
+ return undefined;
36
+ }
37
+ }
38
+ async function ignoreMissingAsync(probe) {
39
+ try {
40
+ return await probe();
41
+ }
42
+ catch (error) {
43
+ if (!isMissing(error))
44
+ throw error;
45
+ return undefined;
46
+ }
47
+ }
48
+ /** Cleanup must never mask the failure that triggered it. */
49
+ function attempt(action) {
50
+ try {
51
+ action();
52
+ }
53
+ catch { /* preserve the original failure */ }
54
+ }
55
+ async function attemptAsync(action) {
56
+ try {
57
+ await action();
58
+ }
59
+ catch { /* preserve the original failure */ }
60
+ }
61
+ function messagesFor(options) {
62
+ return options.messages ?? defaultPlatformMessages;
22
63
  }
23
64
  function validateMode(mode, options) {
24
65
  if (!Number.isInteger(mode) || mode < 0 || mode > 0o777)
25
- throw new RangeError((options.messages ?? defaultPlatformMessages).atomicModeInvalid());
66
+ throw new RangeError(messagesFor(options).atomicModeInvalid());
26
67
  return mode;
27
68
  }
28
- function temporaryPath({ filePath }, options) {
29
- const { tempName = () => `${process.pid}.${randomUUID()}` } = options;
30
- if (options.tempPath) {
31
- const temporary = options.tempPath({ filePath });
32
- if (path.resolve(path.dirname(temporary)) !== path.resolve(path.dirname(filePath)) || path.resolve(temporary) === path.resolve(filePath) || temporary.includes("\0"))
33
- throw new RangeError((options.messages ?? defaultPlatformMessages).atomicTempPathInvalid());
34
- return temporary;
35
- }
36
- const token = tempName({ filePath });
69
+ /** Explicit mode wins, then the preserved destination mode, then the store default, then 0600. */
70
+ function resolveMode({ existingMode }, options) {
71
+ return validateMode(options.mode ?? existingMode ?? options.defaultMode ?? 0o600, options);
72
+ }
73
+ function defaultTempName() {
74
+ return `${process.pid}.${randomUUID()}`;
75
+ }
76
+ function checkedTempPath({ filePath, temporary }, options) {
77
+ if (path.resolve(path.dirname(temporary)) !== path.resolve(path.dirname(filePath)) || path.resolve(temporary) === path.resolve(filePath) || temporary.includes("\0"))
78
+ throw new RangeError(messagesFor(options).atomicTempPathInvalid());
79
+ return temporary;
80
+ }
81
+ function tokenTempPath({ filePath, token }, options) {
37
82
  if (!token || token === "." || token === ".." || /[/\\\x00]/.test(token))
38
- throw new RangeError((options.messages ?? defaultPlatformMessages).atomicTempNameInvalid());
83
+ throw new RangeError(messagesFor(options).atomicTempNameInvalid());
39
84
  return path.join(path.dirname(filePath), `.${path.basename(filePath)}.${token}.tmp`);
40
85
  }
86
+ function temporaryPath({ filePath }, options) {
87
+ if (options.tempPath)
88
+ return checkedTempPath({ filePath, temporary: options.tempPath({ filePath }) }, options);
89
+ return tokenTempPath({ filePath, token: (options.tempName ?? defaultTempName)({ filePath }) }, options);
90
+ }
41
91
  function assertNotSymlink(info, options) {
42
92
  if (info.isSymbolicLink())
43
- throw new Error((options.messages ?? defaultPlatformMessages).atomicSymlinkRefused());
93
+ throw new Error(messagesFor(options).atomicSymlinkRefused());
44
94
  }
45
95
  function assertOwnerOnly(mode, options, platform) {
46
96
  // Windows has no POSIX group/other bits; secret stores skip this verification there.
47
97
  if (options.verifyOwnerOnly && platform !== "win32" && (mode & 0o077) !== 0)
48
- throw new Error((options.messages ?? defaultPlatformMessages).atomicPermissionsRefused());
98
+ throw new Error(messagesFor(options).atomicPermissionsRefused());
49
99
  }
50
100
  function isDirectorySyncUnsupported(error) {
51
- return typeof error === "object" && error !== null && "code" in error && typeof error.code === "string" && ["EPERM", "EISDIR", "EINVAL", "EACCES", "ENOTSUP"].includes(error.code);
101
+ return hasErrorCode(error, DIRECTORY_SYNC_UNSUPPORTED);
102
+ }
103
+ /** Destination mode with optional symlink refusal; ENOENT handling is left to the caller. */
104
+ function destinationModeSync({ filePath, filesystem }, options) {
105
+ if (!options.refuseSymlink)
106
+ return filesystem.stat({ path: filePath }).mode & 0o777;
107
+ const info = filesystem.lstat({ path: filePath });
108
+ assertNotSymlink(info, options);
109
+ return info.mode & 0o777;
110
+ }
111
+ async function destinationModeAsync({ filePath, filesystem }, options) {
112
+ if (!options.refuseSymlink)
113
+ return (await filesystem.stat(filePath)).mode & 0o777;
114
+ const info = await filesystem.lstat(filePath);
115
+ assertNotSymlink(info, options);
116
+ return info.mode & 0o777;
117
+ }
118
+ function serializeJson({ data }, options) {
119
+ const content = JSON.stringify(data, null, 2);
120
+ if (content === undefined)
121
+ throw new TypeError(messagesFor(options).atomicJsonUnserializable());
122
+ return content;
52
123
  }
53
124
  /** Flush directory metadata where supported; Windows cannot fsync directory handles.
54
125
  * Unsupported open/sync errors are ignored because replacement has already landed.
@@ -101,22 +172,8 @@ export async function syncDirectoryBestEffortAsync({ directory, open }, { platfo
101
172
  export function writeFileAtomic(required, options = {}) {
102
173
  const { filePath, content, fs: filesystem } = required;
103
174
  const platform = options.platform ?? process.platform;
104
- let existingMode;
105
- try {
106
- if (options.refuseSymlink) {
107
- const info = filesystem.lstat({ path: filePath });
108
- assertNotSymlink(info, options);
109
- existingMode = info.mode & 0o777;
110
- }
111
- else {
112
- existingMode = filesystem.stat({ path: filePath }).mode & 0o777;
113
- }
114
- }
115
- catch (error) {
116
- if (!isMissing(error))
117
- throw error;
118
- }
119
- const mode = validateMode(options.mode ?? existingMode ?? options.defaultMode ?? 0o600, options);
175
+ const existingMode = ignoreMissing(() => destinationModeSync({ filePath, filesystem }, options));
176
+ const mode = resolveMode({ existingMode }, options);
120
177
  const temporary = temporaryPath(required, options);
121
178
  // A failed exclusive create does not grant ownership of an existing temporary file.
122
179
  let descriptor = filesystem.open({ path: temporary, flags: "wx", mode });
@@ -129,15 +186,8 @@ export function writeFileAtomic(required, options = {}) {
129
186
  const fileDescriptor = descriptor;
130
187
  descriptor = undefined;
131
188
  filesystem.close({ descriptor: fileDescriptor });
132
- if (options.refuseSymlink) {
133
- try {
134
- assertNotSymlink(filesystem.lstat({ path: filePath }), options);
135
- }
136
- catch (error) {
137
- if (!isMissing(error))
138
- throw error;
139
- }
140
- }
189
+ if (options.refuseSymlink)
190
+ ignoreMissing(() => assertNotSymlink(filesystem.lstat({ path: filePath }), options));
141
191
  filesystem.rename({ from: temporary, to: filePath });
142
192
  renamed = true;
143
193
  syncDirectoryBestEffort({ directory: path.dirname(filePath),
@@ -146,28 +196,18 @@ export function writeFileAtomic(required, options = {}) {
146
196
  }, { platform });
147
197
  }
148
198
  catch (error) {
149
- if (descriptor !== undefined) {
150
- try {
151
- filesystem.close({ descriptor });
152
- }
153
- catch { /* preserve the original failure */ }
154
- }
155
- if (!renamed) {
156
- try {
157
- filesystem.remove({ path: temporary });
158
- }
159
- catch { /* preserve the original failure */ }
160
- }
199
+ const openDescriptor = descriptor;
200
+ if (openDescriptor !== undefined)
201
+ attempt(() => filesystem.close({ descriptor: openDescriptor }));
202
+ if (!renamed)
203
+ attempt(() => filesystem.remove({ path: temporary }));
161
204
  throw error;
162
205
  }
163
206
  }
164
207
  /** Pretty JSON delegates to the same mode-preserving atomic primitive. */
165
208
  export function writeJsonFileAtomic(required, options = {}) {
166
209
  const { data, ...writer } = required;
167
- const content = JSON.stringify(data, null, 2);
168
- if (content === undefined)
169
- throw new TypeError((options.messages ?? defaultPlatformMessages).atomicJsonUnserializable());
170
- writeFileAtomic({ ...writer, content }, options);
210
+ writeFileAtomic({ ...writer, content: serializeJson({ data }, options) }, options);
171
211
  }
172
212
  /** Async counterpart with the same ownership, permission and durability guarantees.
173
213
  * Parent creation supports runtime-state and secret stores without another writer implementation. */
@@ -177,18 +217,8 @@ export async function writeFileAtomicAsync(required, options = {}) {
177
217
  const filesystem = options.fs ?? fsPromises;
178
218
  if (options.createParent)
179
219
  await filesystem.mkdir(path.dirname(filePath), { recursive: true });
180
- let existingMode;
181
- try {
182
- const info = options.refuseSymlink ? await filesystem.lstat(filePath) : await filesystem.stat(filePath);
183
- if (options.refuseSymlink)
184
- assertNotSymlink(info, options);
185
- existingMode = info.mode & 0o777;
186
- }
187
- catch (error) {
188
- if (!isMissing(error))
189
- throw error;
190
- }
191
- const mode = validateMode(options.mode ?? existingMode ?? options.defaultMode ?? 0o600, options);
220
+ const existingMode = await ignoreMissingAsync(() => destinationModeAsync({ filePath, filesystem }, options));
221
+ const mode = resolveMode({ existingMode }, options);
192
222
  const temporary = temporaryPath(required, options);
193
223
  // Keep the exclusive create outside cleanup: EEXIST belongs to a different writer.
194
224
  const handle = await filesystem.open(temporary, "wx", mode);
@@ -200,41 +230,23 @@ export async function writeFileAtomicAsync(required, options = {}) {
200
230
  await handle.sync();
201
231
  await handle.close();
202
232
  closed = true;
203
- if (options.refuseSymlink) {
204
- try {
205
- assertNotSymlink(await filesystem.lstat(filePath), options);
206
- }
207
- catch (error) {
208
- if (!isMissing(error))
209
- throw error;
210
- }
211
- }
233
+ if (options.refuseSymlink)
234
+ await ignoreMissingAsync(async () => assertNotSymlink(await filesystem.lstat(filePath), options));
212
235
  await filesystem.rename(temporary, filePath);
213
236
  renamed = true;
214
237
  await syncDirectoryBestEffortAsync({ directory: path.dirname(filePath), open: (path, flags) => filesystem.open(path, flags) }, { platform });
215
238
  }
216
239
  catch (error) {
217
- if (!closed) {
218
- try {
219
- await handle.close();
220
- }
221
- catch { /* preserve the original failure */ }
222
- }
223
- if (!renamed) {
224
- try {
225
- await filesystem.rm(temporary, { force: true });
226
- }
227
- catch { /* preserve the original failure */ }
228
- }
240
+ if (!closed)
241
+ await attemptAsync(() => handle.close());
242
+ if (!renamed)
243
+ await attemptAsync(() => filesystem.rm(temporary, { force: true }));
229
244
  throw error;
230
245
  }
231
246
  }
232
247
  /** Pretty JSON can retain a state-store's trailing newline without changing its serialization. */
233
248
  export async function writeJsonFileAtomicAsync(required, options = {}) {
234
249
  const { data, ...writer } = required;
235
- const content = JSON.stringify(data, null, 2);
236
- if (content === undefined)
237
- throw new TypeError((options.messages ?? defaultPlatformMessages).atomicJsonUnserializable());
238
- await writeFileAtomicAsync({ ...writer, content: content + (options.trailingNewline ? "\n" : "") }, options);
250
+ await writeFileAtomicAsync({ ...writer, content: serializeJson({ data }, options) + (options.trailingNewline ? "\n" : "") }, options);
239
251
  }
240
252
  //# sourceMappingURL=atomic-write.js.map
@@ -7,4 +7,20 @@ export interface BootIdentity {
7
7
  * Native calls are bounded and cached once per process. @complexity O(1) bounded OS reads.
8
8
  */
9
9
  export declare function readBootIdentity(_required: Record<string, never>, _options?: {}): BootIdentity | undefined;
10
+ /** Darwin/Windows identities are wall-clock boot times the OS re-derives when it steps the clock, so
11
+ * processes that cached the same boot at different times can disagree by seconds (kern.boottime drops
12
+ * usec, so a sub-second step flips the id). Exact comparison let a long-lived observer delete a live
13
+ * owner's lease. Two real boots are separated by the earlier boot's uptime plus a restart, so only a gap
14
+ * beyond this tolerance proves an earlier boot; shorter gaps fall back to the PID probe.
15
+ */
16
+ export declare const BOOT_CLOCK_TOLERANCE_MS: number;
17
+ /** True only when a recorded holder provably predates this boot. Linux boot_id is a random UUID and is
18
+ * compared exactly; numeric (time-derived) ids and legacy timestamps get the tolerance above. A record
19
+ * from a "later" boot is clock skew, never proof of death. @complexity O(1).
20
+ */
21
+ export declare function fromEarlierBoot({ boot, bootId, atMs }: {
22
+ boot: BootIdentity | undefined;
23
+ bootId: string | undefined;
24
+ atMs: number;
25
+ }, _options?: {}): boolean;
10
26
  //# sourceMappingURL=boot-identity.d.ts.map
@@ -1,6 +1,26 @@
1
1
  import fs from "node:fs";
2
2
  import { execFileSync } from "node:child_process";
3
3
  let cached;
4
+ const NATIVE_TIMEOUT_MS = 3000;
5
+ function linuxBootIdentity() {
6
+ const id = fs.readFileSync("/proc/sys/kernel/random/boot_id", "utf8").trim();
7
+ const startedAtMs = Number(/^btime (\d+)$/m.exec(fs.readFileSync("/proc/stat", "utf8"))?.[1]) * 1000;
8
+ return id && Number.isFinite(startedAtMs) ? { id, startedAtMs } : null;
9
+ }
10
+ /** Platforms without a kernel boot id use the boot timestamp itself as the identity. */
11
+ function timestampIdentity(startedAtMs) {
12
+ return Number.isFinite(startedAtMs) ? { id: String(startedAtMs), startedAtMs } : null;
13
+ }
14
+ function darwinBootIdentity() {
15
+ const text = execFileSync("/usr/sbin/sysctl", ["-n", "kern.boottime"], { encoding: "utf8", timeout: NATIVE_TIMEOUT_MS });
16
+ return timestampIdentity(Number(/sec\s*=\s*(\d+)/.exec(text)?.[1]) * 1000);
17
+ }
18
+ function windowsBootIdentity() {
19
+ // eslint-disable-next-line sonarjs/no-os-command-from-path -- Fixed OS utility with fixed argv, resolved through the desktop operator's trusted PATH; no request controls the executable.
20
+ const text = execFileSync("powershell.exe", ["-NoProfile", "-NonInteractive", "-Command", "(Get-CimInstance Win32_OperatingSystem).LastBootUpTime.ToUniversalTime().ToString('o')"], { encoding: "utf8", timeout: NATIVE_TIMEOUT_MS, windowsHide: true });
21
+ return timestampIdentity(Date.parse(text.trim()));
22
+ }
23
+ const PLATFORM_READERS = { linux: linuxBootIdentity, darwin: darwinBootIdentity, win32: windowsBootIdentity };
4
24
  /** Kernel boot identity distinguishes a recycled PID across reboots. Unknown identity fails safe
5
25
  * to the existing liveness probe; no age timeout is used to evict a potentially live owner.
6
26
  * Native calls are bounded and cached once per process. @complexity O(1) bounded OS reads.
@@ -10,25 +30,26 @@ export function readBootIdentity(_required, _options = {}) {
10
30
  return cached ?? undefined;
11
31
  cached = null;
12
32
  try {
13
- let startedAtMs;
14
- if (process.platform === "linux") {
15
- const id = fs.readFileSync("/proc/sys/kernel/random/boot_id", "utf8").trim();
16
- startedAtMs = Number(/^btime (\d+)$/m.exec(fs.readFileSync("/proc/stat", "utf8"))?.[1]) * 1000;
17
- if (id && Number.isFinite(startedAtMs))
18
- cached = { id, startedAtMs };
19
- }
20
- else {
21
- const text = process.platform === "darwin"
22
- ? execFileSync("/usr/sbin/sysctl", ["-n", "kern.boottime"], { encoding: "utf8", timeout: 3000 })
23
- : process.platform === "win32"
24
- ? execFileSync("powershell.exe", ["-NoProfile", "-NonInteractive", "-Command", "(Get-CimInstance Win32_OperatingSystem).LastBootUpTime.ToUniversalTime().ToString('o')"], { encoding: "utf8", timeout: 3000, windowsHide: true })
25
- : "";
26
- startedAtMs = process.platform === "darwin" ? Number(/sec\s*=\s*(\d+)/.exec(text)?.[1]) * 1000 : Date.parse(text.trim());
27
- if (Number.isFinite(startedAtMs))
28
- cached = { id: String(startedAtMs), startedAtMs };
29
- }
33
+ cached = PLATFORM_READERS[process.platform]?.() ?? null;
30
34
  }
31
35
  catch { /* Permission/OS failure is unknown identity, never proof of death. */ }
32
36
  return cached ?? undefined;
33
37
  }
38
+ /** Darwin/Windows identities are wall-clock boot times the OS re-derives when it steps the clock, so
39
+ * processes that cached the same boot at different times can disagree by seconds (kern.boottime drops
40
+ * usec, so a sub-second step flips the id). Exact comparison let a long-lived observer delete a live
41
+ * owner's lease. Two real boots are separated by the earlier boot's uptime plus a restart, so only a gap
42
+ * beyond this tolerance proves an earlier boot; shorter gaps fall back to the PID probe.
43
+ */
44
+ export const BOOT_CLOCK_TOLERANCE_MS = 10 * 60_000;
45
+ /** True only when a recorded holder provably predates this boot. Linux boot_id is a random UUID and is
46
+ * compared exactly; numeric (time-derived) ids and legacy timestamps get the tolerance above. A record
47
+ * from a "later" boot is clock skew, never proof of death. @complexity O(1).
48
+ */
49
+ export function fromEarlierBoot({ boot, bootId, atMs }, _options = {}) {
50
+ if (!boot)
51
+ return false;
52
+ const recordedAtMs = bootId === undefined ? atMs : /^\d+$/.test(bootId) ? Number(bootId) : undefined;
53
+ return recordedAtMs === undefined ? bootId !== boot.id : recordedAtMs < boot.startedAtMs - BOOT_CLOCK_TOLERANCE_MS;
54
+ }
34
55
  //# sourceMappingURL=boot-identity.js.map
@@ -8,6 +8,12 @@ export function resolveDirectoryHome({ homeDir, defaultFolder, envName, env, set
8
8
  const selected = env[envName] ?? setting ?? paths.join(homeDir, defaultFolder);
9
9
  if (!selected.trim())
10
10
  throw new Error(`${envName} must name a directory`);
11
+ return resolveSelected({ selected, homeDir, cwd, paths });
12
+ }
13
+ /** Expand a leading `~`, `~/` or `~\` against the supplied home; anything else resolves from cwd.
14
+ * `~user` forms are deliberately not expanded (no shell, no user database lookup).
15
+ */
16
+ function resolveSelected({ selected, homeDir, cwd, paths }) {
11
17
  if (selected === "~")
12
18
  return paths.resolve(homeDir);
13
19
  if (selected.startsWith("~/") || selected.startsWith("~\\"))
@@ -35,7 +35,7 @@ export interface DirectoryRegistry {
35
35
  * @complexity O(n + m) records for merge, O(n) for other mutations, plus file I/O/lock wait.
36
36
  */
37
37
  export declare function createDirectoryRegistry({ file, canonicalPath }: {
38
- file: DurableJsonFile;
38
+ file: Pick<DurableJsonFile, "read" | "write" | "withLock">;
39
39
  canonicalPath: (required: {
40
40
  path: string;
41
41
  }) => string;
@@ -3,7 +3,10 @@
3
3
  * Optional host admission prunes rejected entries on list under that same lock, then reports once.
4
4
  * @complexity O(n + m) records for merge, O(n) for other mutations, plus file I/O/lock wait.
5
5
  */
6
- export function createDirectoryRegistry({ file, canonicalPath }, { discard, reportDiscarded = () => { } } = {}) {
6
+ export function createDirectoryRegistry(
7
+ // The catalog only reads, writes and locks; it never quarantines or stages temp files, so
8
+ // transaction-local adapters need not stub those members.
9
+ { file, canonicalPath }, { discard, reportDiscarded = () => { } } = {}) {
7
10
  function read() {
8
11
  const result = file.read({});
9
12
  if (result.state === "missing")
@@ -23,7 +26,7 @@ export function createDirectoryRegistry({ file, canonicalPath }, { discard, repo
23
26
  if (JSON.stringify(next) !== JSON.stringify(previous))
24
27
  file.write({ value: next });
25
28
  } });
26
- const registry = {
29
+ return {
27
30
  hasMigration: ({ migration }, _optional = {}) => read().migrations.includes(migration),
28
31
  migrations: (_required, _optional = {}) => read().migrations,
29
32
  list: (_required, _optional = {}) => {
@@ -52,19 +55,8 @@ export function createDirectoryRegistry({ file, canonicalPath }, { discard, repo
52
55
  }
53
56
  const byPath = new Map(state.entries.map((entry) => [canonical(entry.path), { ...entry, path: canonical(entry.path) }]));
54
57
  const byId = new Map([...byPath.values()].map((entry) => [entry.id, entry.path]));
55
- for (const entry of entries) {
56
- if (!validRecord(entry))
57
- throw new Error("Cannot register an invalid directory record.");
58
- const dir = canonical(entry.path);
59
- const previous = byPath.get(dir);
60
- if (byId.has(entry.id) && byId.get(entry.id) !== dir)
61
- throw new Error(`Directory identity ${entry.id} is already registered at another path.`);
62
- // Reopening a site cannot reset who originally created it or its stable identity.
63
- const lastOpened = [previous?.lastOpened, entry.lastOpened].filter((at) => typeof at === "string").sort((a, b) => Date.parse(a) - Date.parse(b)).at(-1) ?? null;
64
- const next = previous ? { ...previous, name: entry.name, lastOpened } : { ...entry, path: dir };
65
- byPath.set(dir, next);
66
- byId.set(next.id, dir);
67
- }
58
+ for (const entry of entries)
59
+ mergeEntry({ entry, byPath, byId }, { canonical });
68
60
  result = [...byPath.values()];
69
61
  return { ...state, entries: result, migrations: [...new Set([...state.migrations, ...migrations, ...(migration ? [migration] : [])])] };
70
62
  });
@@ -87,7 +79,22 @@ export function createDirectoryRegistry({ file, canonicalPath }, { discard, repo
87
79
  });
88
80
  },
89
81
  };
90
- return registry;
82
+ }
83
+ /** Fold one incoming record into the canonical path/identity maps of a merge in progress.
84
+ * Refusals throw before either map changes, so the caller's update writes nothing.
85
+ */
86
+ function mergeEntry({ entry, byPath, byId }, { canonical }) {
87
+ if (!validRecord(entry))
88
+ throw new Error("Cannot register an invalid directory record.");
89
+ const dir = canonical(entry.path);
90
+ const previous = byPath.get(dir);
91
+ if (byId.has(entry.id) && byId.get(entry.id) !== dir)
92
+ throw new Error(`Directory identity ${entry.id} is already registered at another path.`);
93
+ // Reopening a site cannot reset who originally created it or its stable identity.
94
+ const lastOpened = [previous?.lastOpened, entry.lastOpened].filter((at) => typeof at === "string").sort((a, b) => Date.parse(a) - Date.parse(b)).at(-1) ?? null;
95
+ const next = previous ? { ...previous, name: entry.name, lastOpened } : { ...entry, path: dir };
96
+ byPath.set(dir, next);
97
+ byId.set(next.id, dir);
91
98
  }
92
99
  function validRecord(entry) {
93
100
  if (!entry || typeof entry !== "object")
@@ -1,4 +1,4 @@
1
- import type { BootIdentity } from "./boot-identity.js";
1
+ import { type BootIdentity } from "./boot-identity.js";
2
2
  import type { DurableJsonFile } from "./durable-json.js";
3
3
  export interface DirectoryRunningRecord {
4
4
  pid: number;
@@ -1,10 +1,12 @@
1
+ import { fromEarlierBoot } from "./boot-identity.js";
1
2
  /** A process lease is distinct from the brief file lock protecting its read/modify/write.
2
3
  * Only proven process death permits stale cleanup. Release/update require the unique claim token.
3
4
  * A zero port means boot is in progress and must still prevent a second database owner.
4
5
  * @complexity O(1) bounded JSON I/O and one liveness probe, plus the file lock wait.
5
6
  */
6
7
  export function createDirectoryRunningLock({ file, isAlive, remove }, { bootIdentity } = {}) {
7
- function readLive(cleanup = true) {
8
+ /** Validate before any mutation; corrupt or unreadable records fail closed. O(1) JSON I/O. */
9
+ function readRecord() {
8
10
  const result = file.read({});
9
11
  if (result.state === "missing")
10
12
  return null;
@@ -13,9 +15,14 @@ export function createDirectoryRunningLock({ file, isAlive, remove }, { bootIden
13
15
  const record = result.value;
14
16
  if (!validRunningRecord(record))
15
17
  throw new Error("The running lock is invalid; refusing to open this directory's database.");
16
- const boot = bootIdentity?.();
17
- const sameBoot = !boot || (record.bootId !== undefined ? record.bootId === boot.id : Date.parse(record.started) >= boot.startedAtMs);
18
- if (sameBoot && isAlive({ pid: record.pid }))
18
+ return record;
19
+ }
20
+ function readLive(cleanup = true) {
21
+ const record = readRecord();
22
+ if (!record)
23
+ return null;
24
+ const earlierBoot = fromEarlierBoot({ boot: bootIdentity?.(), bootId: record.bootId, atMs: Date.parse(record.started) });
25
+ if (!earlierBoot && isAlive({ pid: record.pid }))
19
26
  return record;
20
27
  if (cleanup)
21
28
  remove();
@@ -50,7 +57,8 @@ export function createDirectoryRunningLock({ file, isAlive, remove }, { bootIden
50
57
  release({ token }, _optional = {}) {
51
58
  if (file.read({}).state === "missing")
52
59
  return;
53
- file.withLock({ critical: () => { if (readLive()?.token === token)
60
+ // Release removes only its own record; stale cleanup of a different owner belongs to read/claim.
61
+ file.withLock({ critical: () => { if (readRecord()?.token === token)
54
62
  remove(); } });
55
63
  },
56
64
  };
@@ -60,7 +68,10 @@ function validRunningRecord(value) {
60
68
  return false;
61
69
  const record = value;
62
70
  return Number.isSafeInteger(record.pid) && record.pid > 0 && Number.isInteger(record.port) && record.port >= 0 && record.port <= 65535 &&
63
- typeof record.app === "string" && record.app.length > 0 && typeof record.token === "string" && record.token.length > 0 &&
64
- (record.bootId === undefined || typeof record.bootId === "string") && typeof record.started === "string" && Number.isFinite(Date.parse(record.started)) && ["http", "https"].includes(record.scheme);
71
+ validRunningIdentity({ record }) && ["http", "https"].includes(record.scheme);
72
+ }
73
+ function validRunningIdentity({ record }, _options = {}) {
74
+ return typeof record.app === "string" && record.app.length > 0 && typeof record.token === "string" && record.token.length > 0 &&
75
+ (record.bootId === undefined || typeof record.bootId === "string") && typeof record.started === "string" && Number.isFinite(Date.parse(record.started));
65
76
  }
66
77
  //# sourceMappingURL=directory-running-lock.js.map
@@ -5,7 +5,7 @@ export type DirectorySyncProvider = "iCloud" | "OneDrive" | "Dropbox" | "Google
5
5
  * Dropbox publishes info.json paths; macOS File Provider mounts identify their provider.
6
6
  * @complexity O(a + p) small account/mount lists and ancestor probes, no recursive site scan.
7
7
  */
8
- export declare function detectDirectorySync({ dir, homeDir, documents }: {
8
+ export declare function detectDirectorySync(required: {
9
9
  dir: string;
10
10
  homeDir: string;
11
11
  documents: string;