@jini-ai/platform 0.5.8 → 0.5.10

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (82) hide show
  1. package/CHANGELOG.md +12 -0
  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.d.ts +25 -2
  9. package/dist/fs/atomic-write.js +162 -111
  10. package/dist/fs/boot-identity.d.ts +16 -0
  11. package/dist/fs/boot-identity.js +38 -17
  12. package/dist/fs/directory-home.js +6 -0
  13. package/dist/fs/directory-registry.d.ts +1 -1
  14. package/dist/fs/directory-registry.js +23 -16
  15. package/dist/fs/directory-running-lock.d.ts +1 -1
  16. package/dist/fs/directory-running-lock.js +18 -7
  17. package/dist/fs/directory-sync.d.ts +1 -1
  18. package/dist/fs/directory-sync.js +64 -51
  19. package/dist/fs/directory-user-config.js +108 -67
  20. package/dist/fs/directory-workspace.d.ts +11 -0
  21. package/dist/fs/directory-workspace.js +16 -11
  22. package/dist/fs/durable-json.d.ts +3 -1
  23. package/dist/fs/durable-json.js +55 -40
  24. package/dist/fs/file-lock.js +214 -149
  25. package/dist/fs/guarded-reader.js +47 -35
  26. package/dist/fs/user-data-directories.d.ts +1 -1
  27. package/dist/fs/user-data-directories.js +25 -9
  28. package/dist/fs/verified-directory-move.d.ts +14 -11
  29. package/dist/fs/verified-directory-move.js +106 -80
  30. package/dist/fs/verified-file-relocation.d.ts +2 -1
  31. package/dist/fs/verified-file-relocation.js +112 -100
  32. package/dist/http/guarded/client.d.ts +3 -4
  33. package/dist/http/guarded/client.js +50 -60
  34. package/dist/http/guarded/fetch-adapter.d.ts +2 -3
  35. package/dist/http/guarded/fetch-adapter.js +26 -15
  36. package/dist/mail/builtin-providers.d.ts +39 -0
  37. package/dist/mail/builtin-providers.js +60 -0
  38. package/dist/mail/feedback.d.ts +84 -0
  39. package/dist/mail/feedback.js +90 -0
  40. package/dist/mail/index.d.ts +11 -0
  41. package/dist/mail/index.js +6 -0
  42. package/dist/mail/policy.d.ts +77 -0
  43. package/dist/mail/policy.js +155 -0
  44. package/dist/mail/ports.d.ts +56 -7
  45. package/dist/mail/provider.d.ts +117 -0
  46. package/dist/mail/provider.js +74 -0
  47. package/dist/mail/rate-limit.d.ts +33 -0
  48. package/dist/mail/rate-limit.js +35 -0
  49. package/dist/mail/sender-identity.d.ts +68 -0
  50. package/dist/mail/sender-identity.js +50 -0
  51. package/dist/mail/types.d.ts +9 -0
  52. package/dist/net/address.d.ts +9 -23
  53. package/dist/net/address.js +28 -133
  54. package/dist/net/endpoint-policy.d.ts +26 -2
  55. package/dist/net/endpoint-policy.js +159 -56
  56. package/dist/object-store/index.d.ts +2 -0
  57. package/dist/object-store/index.js +2 -0
  58. package/dist/object-store/local-fs.d.ts +17 -0
  59. package/dist/object-store/local-fs.js +146 -0
  60. package/dist/process.js +2 -0
  61. package/dist/proxy-env.js +131 -135
  62. package/dist/secrets/credential-sets/contracts.d.ts +1 -0
  63. package/dist/secrets/credential-sets/contracts.js +1 -0
  64. package/dist/secrets/credential-sets/index.d.ts +1 -0
  65. package/dist/secrets/credential-sets/index.js +1 -0
  66. package/dist/secrets/credential-sets/provider-matches.d.ts +61 -0
  67. package/dist/secrets/credential-sets/provider-matches.js +61 -0
  68. package/dist/secrets/credential-sets/types.d.ts +7 -0
  69. package/dist/secrets/credential-sets/vault.meta.js +58 -20
  70. package/dist/secrets/credential-sets/vault.repo.memory.d.ts +7 -0
  71. package/dist/secrets/credential-sets/vault.repo.memory.js +43 -36
  72. package/dist/secrets/credential-sets/vault.service.d.ts +9 -0
  73. package/dist/secrets/credential-sets/vault.service.js +131 -61
  74. package/dist/secrets/credential-sets/vault.types.d.ts +11 -3
  75. package/dist/secrets/credential-sets/vault.types.js +22 -8
  76. package/dist/secrets/keyring.env.js +14 -10
  77. package/dist/toolchain.js +119 -91
  78. package/dist/web/html-document.js +73 -69
  79. package/dist/web/html-to-markdown.js +127 -102
  80. package/dist/web/web-page.d.ts +1 -1
  81. package/dist/web/web-page.js +162 -81
  82. package/package.json +11 -3
@@ -17,59 +17,163 @@ 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
- function assertOwnerOnly(mode, options) {
95
+ function assertOwnerOnly(mode, options, platform) {
46
96
  // Windows has no POSIX group/other bits; secret stores skip this verification there.
47
- if (options.verifyOwnerOnly && (options.platform ?? process.platform) !== "win32" && (mode & 0o077) !== 0)
48
- throw new Error((options.messages ?? defaultPlatformMessages).atomicPermissionsRefused());
97
+ if (options.verifyOwnerOnly && platform !== "win32" && (mode & 0o077) !== 0)
98
+ throw new Error(messagesFor(options).atomicPermissionsRefused());
49
99
  }
50
- /** Same-directory rename; preserve existing mode exactly, default new files to 0600.
51
- * Only ENOENT means absent. Stat/write/chmod failures propagate before any rename.
52
- * Flush file bytes before rename so metadata cannot precede content; then flush the parent dir.
53
- * A directory-sync failure is reported after replacement, because rename cannot be rolled back safely.
100
+ function isDirectorySyncUnsupported(error) {
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;
123
+ }
124
+ /** Flush directory metadata where supported; Windows cannot fsync directory handles.
125
+ * Unsupported open/sync errors are ignored because replacement has already landed.
126
+ * Other failures propagate, and an opened descriptor is always closed.
127
+ * @complexity O(1) filesystem calls and space.
54
128
  */
55
- export function writeFileAtomic(required, options = {}) {
56
- const { filePath, content, fs: filesystem } = required;
57
- let existingMode;
129
+ export function syncDirectoryBestEffort({ directory, open, sync, close }, { platform }) {
130
+ if (platform === "win32")
131
+ return;
132
+ let descriptor;
58
133
  try {
59
- if (options.refuseSymlink) {
60
- const info = filesystem.lstat({ path: filePath });
61
- assertNotSymlink(info, options);
62
- existingMode = info.mode & 0o777;
63
- }
64
- else {
65
- existingMode = filesystem.stat({ path: filePath }).mode & 0o777;
66
- }
134
+ descriptor = open(directory, "r");
135
+ sync(descriptor);
67
136
  }
68
137
  catch (error) {
69
- if (!isMissing(error))
138
+ if (!isDirectorySyncUnsupported(error))
70
139
  throw error;
71
140
  }
72
- const mode = validateMode(options.mode ?? existingMode ?? options.defaultMode ?? 0o600, options);
141
+ finally {
142
+ if (descriptor !== undefined)
143
+ close(descriptor);
144
+ }
145
+ }
146
+ /** Async directory flush with the same unsupported-error and handle-ownership contract.
147
+ * @complexity O(1) filesystem calls and space.
148
+ */
149
+ export async function syncDirectoryBestEffortAsync({ directory, open }, { platform }) {
150
+ if (platform === "win32")
151
+ return;
152
+ let handle;
153
+ try {
154
+ handle = await open(directory, "r");
155
+ await handle.sync();
156
+ }
157
+ catch (error) {
158
+ if (!isDirectorySyncUnsupported(error))
159
+ throw error;
160
+ }
161
+ finally {
162
+ if (handle !== undefined)
163
+ await handle.close();
164
+ }
165
+ }
166
+ /** Same-directory rename; preserve existing mode exactly, default new files to 0600.
167
+ * Only ENOENT means absent. Stat/write/chmod failures propagate before any rename.
168
+ * Flush file bytes before rename so metadata cannot precede content; directory flush is best-effort:
169
+ * skip Windows and ignore unsupported-code failures (EPERM/EISDIR/EINVAL/EACCES/ENOTSUP).
170
+ * Other directory-flush failures are reported after replacement, because rename cannot be rolled back safely.
171
+ */
172
+ export function writeFileAtomic(required, options = {}) {
173
+ const { filePath, content, fs: filesystem } = required;
174
+ const platform = options.platform ?? process.platform;
175
+ const existingMode = ignoreMissing(() => destinationModeSync({ filePath, filesystem }, options));
176
+ const mode = resolveMode({ existingMode }, options);
73
177
  const temporary = temporaryPath(required, options);
74
178
  // A failed exclusive create does not grant ownership of an existing temporary file.
75
179
  let descriptor = filesystem.open({ path: temporary, flags: "wx", mode });
@@ -77,73 +181,44 @@ export function writeFileAtomic(required, options = {}) {
77
181
  try {
78
182
  filesystem.write({ descriptor, content });
79
183
  filesystem.chmod({ path: temporary, mode });
80
- assertOwnerOnly(filesystem.stat({ path: temporary }).mode, options);
184
+ assertOwnerOnly(filesystem.stat({ path: temporary }).mode, options, platform);
81
185
  filesystem.sync({ descriptor });
82
186
  const fileDescriptor = descriptor;
83
187
  descriptor = undefined;
84
188
  filesystem.close({ descriptor: fileDescriptor });
85
- if (options.refuseSymlink) {
86
- try {
87
- assertNotSymlink(filesystem.lstat({ path: filePath }), options);
88
- }
89
- catch (error) {
90
- if (!isMissing(error))
91
- throw error;
92
- }
93
- }
189
+ if (options.refuseSymlink)
190
+ ignoreMissing(() => assertNotSymlink(filesystem.lstat({ path: filePath }), options));
94
191
  filesystem.rename({ from: temporary, to: filePath });
95
192
  renamed = true;
96
- const directory = filesystem.open({ path: path.dirname(filePath), flags: "r" });
97
- try {
98
- filesystem.sync({ descriptor: directory });
99
- }
100
- finally {
101
- filesystem.close({ descriptor: directory });
102
- }
193
+ syncDirectoryBestEffort({ directory: path.dirname(filePath),
194
+ open: (path, flags) => filesystem.open({ path, flags }),
195
+ sync: descriptor => filesystem.sync({ descriptor }), close: descriptor => filesystem.close({ descriptor }),
196
+ }, { platform });
103
197
  }
104
198
  catch (error) {
105
- if (descriptor !== undefined) {
106
- try {
107
- filesystem.close({ descriptor });
108
- }
109
- catch { /* preserve the original failure */ }
110
- }
111
- if (!renamed) {
112
- try {
113
- filesystem.remove({ path: temporary });
114
- }
115
- catch { /* preserve the original failure */ }
116
- }
199
+ const openDescriptor = descriptor;
200
+ if (openDescriptor !== undefined)
201
+ attempt(() => filesystem.close({ descriptor: openDescriptor }));
202
+ if (!renamed)
203
+ attempt(() => filesystem.remove({ path: temporary }));
117
204
  throw error;
118
205
  }
119
206
  }
120
207
  /** Pretty JSON delegates to the same mode-preserving atomic primitive. */
121
208
  export function writeJsonFileAtomic(required, options = {}) {
122
209
  const { data, ...writer } = required;
123
- const content = JSON.stringify(data, null, 2);
124
- if (content === undefined)
125
- throw new TypeError((options.messages ?? defaultPlatformMessages).atomicJsonUnserializable());
126
- writeFileAtomic({ ...writer, content }, options);
210
+ writeFileAtomic({ ...writer, content: serializeJson({ data }, options) }, options);
127
211
  }
128
212
  /** Async counterpart with the same ownership, permission and durability guarantees.
129
213
  * Parent creation supports runtime-state and secret stores without another writer implementation. */
130
214
  export async function writeFileAtomicAsync(required, options = {}) {
131
215
  const { filePath, content } = required;
216
+ const platform = options.platform ?? process.platform;
132
217
  const filesystem = options.fs ?? fsPromises;
133
218
  if (options.createParent)
134
219
  await filesystem.mkdir(path.dirname(filePath), { recursive: true });
135
- let existingMode;
136
- try {
137
- const info = options.refuseSymlink ? await filesystem.lstat(filePath) : await filesystem.stat(filePath);
138
- if (options.refuseSymlink)
139
- assertNotSymlink(info, options);
140
- existingMode = info.mode & 0o777;
141
- }
142
- catch (error) {
143
- if (!isMissing(error))
144
- throw error;
145
- }
146
- 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);
147
222
  const temporary = temporaryPath(required, options);
148
223
  // Keep the exclusive create outside cleanup: EEXIST belongs to a different writer.
149
224
  const handle = await filesystem.open(temporary, "wx", mode);
@@ -151,51 +226,27 @@ export async function writeFileAtomicAsync(required, options = {}) {
151
226
  try {
152
227
  await handle.writeFile(content, "utf8");
153
228
  await handle.chmod(mode);
154
- assertOwnerOnly((await handle.stat()).mode, options);
229
+ assertOwnerOnly((await handle.stat()).mode, options, platform);
155
230
  await handle.sync();
156
231
  await handle.close();
157
232
  closed = true;
158
- if (options.refuseSymlink) {
159
- try {
160
- assertNotSymlink(await filesystem.lstat(filePath), options);
161
- }
162
- catch (error) {
163
- if (!isMissing(error))
164
- throw error;
165
- }
166
- }
233
+ if (options.refuseSymlink)
234
+ await ignoreMissingAsync(async () => assertNotSymlink(await filesystem.lstat(filePath), options));
167
235
  await filesystem.rename(temporary, filePath);
168
236
  renamed = true;
169
- const directory = await filesystem.open(path.dirname(filePath), "r");
170
- try {
171
- await directory.sync();
172
- }
173
- finally {
174
- await directory.close();
175
- }
237
+ await syncDirectoryBestEffortAsync({ directory: path.dirname(filePath), open: (path, flags) => filesystem.open(path, flags) }, { platform });
176
238
  }
177
239
  catch (error) {
178
- if (!closed) {
179
- try {
180
- await handle.close();
181
- }
182
- catch { /* preserve the original failure */ }
183
- }
184
- if (!renamed) {
185
- try {
186
- await filesystem.rm(temporary, { force: true });
187
- }
188
- catch { /* preserve the original failure */ }
189
- }
240
+ if (!closed)
241
+ await attemptAsync(() => handle.close());
242
+ if (!renamed)
243
+ await attemptAsync(() => filesystem.rm(temporary, { force: true }));
190
244
  throw error;
191
245
  }
192
246
  }
193
247
  /** Pretty JSON can retain a state-store's trailing newline without changing its serialization. */
194
248
  export async function writeJsonFileAtomicAsync(required, options = {}) {
195
249
  const { data, ...writer } = required;
196
- const content = JSON.stringify(data, null, 2);
197
- if (content === undefined)
198
- throw new TypeError((options.messages ?? defaultPlatformMessages).atomicJsonUnserializable());
199
- await writeFileAtomicAsync({ ...writer, content: content + (options.trailingNewline ? "\n" : "") }, options);
250
+ await writeFileAtomicAsync({ ...writer, content: serializeJson({ data }, options) + (options.trailingNewline ? "\n" : "") }, options);
200
251
  }
201
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;