@jini-ai/platform 0.5.9 → 0.6.10
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +22 -1
- package/dist/asset-cache.js +26 -25
- package/dist/blob-storage.d.ts +7 -2
- package/dist/blob-storage.js +99 -59
- package/dist/child-environment.d.ts +36 -0
- package/dist/child-environment.js +68 -0
- package/dist/download.js +192 -148
- package/dist/fs/atomic-write.js +105 -93
- package/dist/fs/boot-identity.d.ts +16 -0
- package/dist/fs/boot-identity.js +38 -17
- package/dist/fs/directory-home.js +6 -0
- package/dist/fs/directory-registry.d.ts +1 -1
- package/dist/fs/directory-registry.js +23 -16
- package/dist/fs/directory-running-lock.d.ts +1 -1
- package/dist/fs/directory-running-lock.js +18 -7
- package/dist/fs/directory-sync.d.ts +1 -1
- package/dist/fs/directory-sync.js +64 -51
- package/dist/fs/directory-user-config.js +108 -67
- package/dist/fs/directory-workspace.d.ts +11 -0
- package/dist/fs/directory-workspace.js +16 -11
- package/dist/fs/durable-json.js +49 -37
- package/dist/fs/file-lock.js +214 -149
- package/dist/fs/guarded-reader.js +47 -35
- package/dist/fs/user-data-directories.d.ts +1 -1
- package/dist/fs/user-data-directories.js +25 -9
- package/dist/fs/verified-directory-move.d.ts +13 -11
- package/dist/fs/verified-directory-move.js +105 -68
- package/dist/fs/verified-file-relocation.d.ts +1 -1
- package/dist/fs/verified-file-relocation.js +111 -94
- package/dist/http/guarded/client.d.ts +3 -4
- package/dist/http/guarded/client.js +50 -60
- package/dist/http/guarded/fetch-adapter.d.ts +2 -3
- package/dist/http/guarded/fetch-adapter.js +26 -15
- package/dist/mail/builtin-providers.d.ts +39 -0
- package/dist/mail/builtin-providers.js +60 -0
- package/dist/mail/feedback.d.ts +84 -0
- package/dist/mail/feedback.js +90 -0
- package/dist/mail/index.d.ts +11 -0
- package/dist/mail/index.js +6 -0
- package/dist/mail/policy.d.ts +77 -0
- package/dist/mail/policy.js +155 -0
- package/dist/mail/ports.d.ts +56 -7
- package/dist/mail/provider.d.ts +117 -0
- package/dist/mail/provider.js +74 -0
- package/dist/mail/rate-limit.d.ts +33 -0
- package/dist/mail/rate-limit.js +35 -0
- package/dist/mail/sender-identity.d.ts +68 -0
- package/dist/mail/sender-identity.js +50 -0
- package/dist/mail/types.d.ts +9 -0
- package/dist/net/address.d.ts +9 -23
- package/dist/net/address.js +28 -133
- package/dist/net/endpoint-policy.d.ts +26 -2
- package/dist/net/endpoint-policy.js +159 -56
- package/dist/object-store/index.d.ts +2 -0
- package/dist/object-store/index.js +2 -0
- package/dist/object-store/local-fs.d.ts +17 -0
- package/dist/object-store/local-fs.js +146 -0
- package/dist/process.js +2 -0
- package/dist/proxy-env.js +131 -135
- package/dist/secrets/credential-sets/contracts.d.ts +1 -0
- package/dist/secrets/credential-sets/contracts.js +1 -0
- package/dist/secrets/credential-sets/index.d.ts +1 -0
- package/dist/secrets/credential-sets/index.js +1 -0
- package/dist/secrets/credential-sets/provider-matches.d.ts +61 -0
- package/dist/secrets/credential-sets/provider-matches.js +61 -0
- package/dist/secrets/credential-sets/types.d.ts +7 -0
- package/dist/secrets/credential-sets/vault.meta.js +58 -20
- package/dist/secrets/credential-sets/vault.repo.memory.d.ts +7 -0
- package/dist/secrets/credential-sets/vault.repo.memory.js +43 -36
- package/dist/secrets/credential-sets/vault.service.d.ts +9 -0
- package/dist/secrets/credential-sets/vault.service.js +131 -61
- package/dist/secrets/credential-sets/vault.types.d.ts +11 -3
- package/dist/secrets/credential-sets/vault.types.js +22 -8
- package/dist/secrets/keyring.env.js +14 -10
- package/dist/toolchain.js +119 -91
- package/dist/web/html-document.js +73 -69
- package/dist/web/html-to-markdown.js +127 -102
- package/dist/web/web-page.js +160 -80
- package/package.json +12 -4
package/dist/fs/atomic-write.js
CHANGED
|
@@ -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
|
|
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
|
|
66
|
+
throw new RangeError(messagesFor(options).atomicModeInvalid());
|
|
26
67
|
return mode;
|
|
27
68
|
}
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
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
|
|
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
|
|
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
|
|
98
|
+
throw new Error(messagesFor(options).atomicPermissionsRefused());
|
|
49
99
|
}
|
|
50
100
|
function isDirectorySyncUnsupported(error) {
|
|
51
|
-
return
|
|
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
|
-
|
|
105
|
-
|
|
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
|
-
|
|
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
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
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
|
-
|
|
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
|
-
|
|
181
|
-
|
|
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
|
-
|
|
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
|
-
|
|
219
|
-
|
|
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
|
-
|
|
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
|
package/dist/fs/boot-identity.js
CHANGED
|
@@ -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
|
-
|
|
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(
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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,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
|
-
|
|
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
|
-
|
|
17
|
-
|
|
18
|
-
|
|
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
|
-
|
|
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
|
-
|
|
64
|
-
|
|
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(
|
|
8
|
+
export declare function detectDirectorySync(required: {
|
|
9
9
|
dir: string;
|
|
10
10
|
homeDir: string;
|
|
11
11
|
documents: string;
|