@jini-ai/platform 0.5.9 → 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.
- package/CHANGELOG.md +7 -0
- 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 +11 -3
package/dist/fs/file-lock.js
CHANGED
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
* multi-process races and are deliberately avoided. Atomic file replacement independently prevents
|
|
11
11
|
* torn content. Hosts whose operations can legitimately exceed the stale budget use staleMs: Infinity.
|
|
12
12
|
*/
|
|
13
|
-
import { readBootIdentity } from "./boot-identity.js";
|
|
13
|
+
import { fromEarlierBoot, readBootIdentity } from "./boot-identity.js";
|
|
14
14
|
import fs from "node:fs";
|
|
15
15
|
import * as asyncFs from "node:fs/promises";
|
|
16
16
|
import { randomUUID } from "node:crypto";
|
|
@@ -47,13 +47,25 @@ export function isContendedLockError({ code, platform }) {
|
|
|
47
47
|
function errorCode(error) {
|
|
48
48
|
return typeof error === "object" && error !== null && "code" in error && typeof error.code === "string" ? error.code : undefined;
|
|
49
49
|
}
|
|
50
|
+
function isAbsentOrContended(error) {
|
|
51
|
+
const code = errorCode(error);
|
|
52
|
+
return code === "ENOENT" || isContendedLockError({ code, platform: process.platform });
|
|
53
|
+
}
|
|
54
|
+
function rethrowUnlessMissing(error) { if (errorCode(error) !== "ENOENT")
|
|
55
|
+
throw error; }
|
|
50
56
|
function validPid(pid) { return typeof pid === "number" && Number.isSafeInteger(pid) && pid > 0; }
|
|
57
|
+
function isHolderRecord(value) {
|
|
58
|
+
if (typeof value !== "object" || value === null)
|
|
59
|
+
return false;
|
|
60
|
+
const row = value;
|
|
61
|
+
return validPid(row.pid) && typeof row.hostname === "string" && typeof row.token === "string" && typeof row.acquiredAt === "string";
|
|
62
|
+
}
|
|
51
63
|
function parseHolder(raw) {
|
|
52
64
|
try {
|
|
53
65
|
const value = JSON.parse(raw);
|
|
54
|
-
if (
|
|
66
|
+
if (!isHolderRecord(value))
|
|
55
67
|
return undefined;
|
|
56
|
-
return { pid: value.pid, hostname: value.hostname, token: value.token, acquiredAt: value.acquiredAt, ...(
|
|
68
|
+
return { pid: value.pid, hostname: value.hostname, token: value.token, acquiredAt: value.acquiredAt, ...(typeof value.bootId === "string" ? { bootId: value.bootId } : {}) };
|
|
57
69
|
}
|
|
58
70
|
catch {
|
|
59
71
|
return undefined;
|
|
@@ -64,60 +76,86 @@ function parseHolder(raw) {
|
|
|
64
76
|
export function isLockStale(required) {
|
|
65
77
|
return (required.ownerAlive === false || required.nowMs - required.mtimeMs > required.staleMs) && required.observedInode === required.currentInode && required.observedDevice === required.currentDevice;
|
|
66
78
|
}
|
|
67
|
-
function
|
|
79
|
+
function intervalsValid({ timeoutMs, staleMs, pollMs }) {
|
|
80
|
+
return Number.isSafeInteger(timeoutMs) && timeoutMs >= 0 && (Number.isSafeInteger(staleMs) || staleMs === Infinity) && staleMs > 0 && Number.isSafeInteger(pollMs) && pollMs > 0;
|
|
81
|
+
}
|
|
82
|
+
function intervals(options) {
|
|
68
83
|
const timeoutMs = options.timeoutMs ?? DEFAULT_LOCK_TIMEOUT_MS, staleMs = options.staleMs ?? DEFAULT_LOCK_STALE_AFTER_MS, pollMs = options.pollMs ?? 10;
|
|
69
|
-
if (!
|
|
84
|
+
if (!intervalsValid({ timeoutMs, staleMs, pollMs }))
|
|
70
85
|
throw new RangeError((options.messages ?? defaultPlatformMessages).fileLockIntervalsInvalid());
|
|
71
|
-
|
|
86
|
+
return { timeoutMs, staleMs, pollMs };
|
|
87
|
+
}
|
|
88
|
+
function defaultProcessPort() {
|
|
89
|
+
return { pid: process.pid, bootIdentity: () => readBootIdentity({}), isAlive: ({ pid }) => { try {
|
|
72
90
|
process.kill(pid, 0);
|
|
73
91
|
return true;
|
|
74
92
|
}
|
|
75
93
|
catch (error) {
|
|
76
94
|
return errorCode(error) !== "ESRCH";
|
|
77
95
|
} } };
|
|
96
|
+
}
|
|
97
|
+
function settings(options) {
|
|
98
|
+
const checked = intervals(options);
|
|
99
|
+
const processPort = options.process ?? defaultProcessPort();
|
|
78
100
|
const clock = options.clock ?? createSystemClock();
|
|
79
101
|
const monotonicMs = options.monotonicMs ?? (() => clock.nowMs());
|
|
80
|
-
return {
|
|
102
|
+
return { ...checked, processPort, clock, monotonicMs, boot: processPort.bootIdentity?.(), hostname: (options.hostname ?? nodeHostname)(), token: (options.token ?? randomUUID)() };
|
|
81
103
|
}
|
|
82
104
|
function ownership(s, options) {
|
|
83
105
|
return options.format === "token" ? `${s.processPort.pid}:${s.token}${s.boot ? ":" + s.boot.id : ""}` : JSON.stringify({ pid: s.processPort.pid, hostname: s.hostname, token: s.token, acquiredAt: new Date(s.clock.nowMs()).toISOString(), ...(s.boot ? { bootId: s.boot.id } : {}) });
|
|
84
106
|
}
|
|
85
|
-
function
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
107
|
+
function holderAlive(holder, s) {
|
|
108
|
+
if (holder.hostname !== s.hostname)
|
|
109
|
+
return undefined;
|
|
110
|
+
// A recorded boot id decides; without one, only an acquisition before this boot proves a restart.
|
|
111
|
+
if (fromEarlierBoot({ boot: s.boot, bootId: holder.bootId, atMs: Date.parse(holder.acquiredAt) }))
|
|
112
|
+
return false;
|
|
113
|
+
return s.processPort.isAlive({ pid: holder.pid });
|
|
114
|
+
}
|
|
115
|
+
// Legacy synchronous stores wrote pid:random rather than JSON. Malformed pids remain unknown.
|
|
116
|
+
function legacyOwnerAlive(snapshot, s) {
|
|
95
117
|
if (!/^\d+:/.test(snapshot.raw))
|
|
96
118
|
return undefined;
|
|
97
119
|
const parts = snapshot.raw.split(":");
|
|
98
120
|
const pid = Number(parts[0]);
|
|
99
|
-
if (validPid(pid)
|
|
121
|
+
if (!validPid(pid))
|
|
122
|
+
return undefined;
|
|
123
|
+
if (fromEarlierBoot({ boot: s.boot, bootId: parts[2] || undefined, atMs: snapshot.mtimeMs }))
|
|
100
124
|
return false;
|
|
101
|
-
return
|
|
125
|
+
return s.processPort.isAlive({ pid });
|
|
126
|
+
}
|
|
127
|
+
function ownerAlive(snapshot, s) {
|
|
128
|
+
return snapshot.holder ? holderAlive(snapshot.holder, s) : legacyOwnerAlive(snapshot, s);
|
|
102
129
|
}
|
|
103
|
-
function
|
|
130
|
+
function sameFile(a, b) { return a.ino === b.ino && a.dev === b.dev; }
|
|
131
|
+
function sameStat(a, b) { return sameFile(a, b) && a.mtimeMs === b.mtimeMs; }
|
|
132
|
+
function unchanged(a, b) { return sameStat(a, b) && a.raw === b.raw; }
|
|
104
133
|
function stale(observed, current, s) {
|
|
105
134
|
return unchanged(observed, current) && isLockStale({ nowMs: s.clock.nowMs(), mtimeMs: observed.mtimeMs, staleMs: s.staleMs, ownerAlive: ownerAlive(current, s), observedInode: observed.ino, currentInode: current.ino, observedDevice: observed.dev, currentDevice: current.dev });
|
|
106
135
|
}
|
|
136
|
+
function messagesOption(options) { return options.messages ? { messages: options.messages } : {}; }
|
|
107
137
|
function checkTimeout(lockPath, holder, start, s, options) {
|
|
108
138
|
const waitedMs = s.monotonicMs() - start;
|
|
109
139
|
if (waitedMs >= s.timeoutMs) {
|
|
110
140
|
const message = options.timeoutMessage?.({ lockPath, waitMs: s.timeoutMs });
|
|
111
|
-
throw new FileLockTimeoutError({ lockPath, holder, waitedMs }, { ...(options
|
|
141
|
+
throw new FileLockTimeoutError({ lockPath, holder, waitedMs }, { ...messagesOption(options), ...(message !== undefined ? { message } : {}) });
|
|
112
142
|
}
|
|
113
143
|
}
|
|
144
|
+
function ownsSnapshot(current, raw, identity) {
|
|
145
|
+
return current !== undefined && current.raw === raw && sameFile(current, identity);
|
|
146
|
+
}
|
|
147
|
+
// A failed critical section keeps its original error; durable hosts also suppress release-only failures.
|
|
148
|
+
function rethrowReleaseError(error, completed, options) {
|
|
149
|
+
if (completed && !options.suppressReleaseErrors)
|
|
150
|
+
rethrowUnlessMissing(error);
|
|
151
|
+
}
|
|
114
152
|
function inspectSync(lockPath, filesystem) {
|
|
115
153
|
let descriptor;
|
|
116
154
|
try {
|
|
117
155
|
descriptor = filesystem.openSync(lockPath, "r");
|
|
118
156
|
}
|
|
119
157
|
catch (error) {
|
|
120
|
-
if (
|
|
158
|
+
if (isAbsentOrContended(error))
|
|
121
159
|
return undefined;
|
|
122
160
|
throw error;
|
|
123
161
|
}
|
|
@@ -135,7 +173,7 @@ async function inspectAsync(lockPath, filesystem) {
|
|
|
135
173
|
handle = await filesystem.open(lockPath, "r");
|
|
136
174
|
}
|
|
137
175
|
catch (error) {
|
|
138
|
-
if (
|
|
176
|
+
if (isAbsentOrContended(error))
|
|
139
177
|
return undefined;
|
|
140
178
|
throw error;
|
|
141
179
|
}
|
|
@@ -147,77 +185,82 @@ async function inspectAsync(lockPath, filesystem) {
|
|
|
147
185
|
await handle.close();
|
|
148
186
|
}
|
|
149
187
|
}
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
188
|
+
function writeOwnershipSync({ raw, filesystem, options }, descriptor) {
|
|
189
|
+
const bytes = Buffer.from(raw);
|
|
190
|
+
let offset = 0;
|
|
191
|
+
while (offset < bytes.length) {
|
|
192
|
+
const written = filesystem.writeSync(descriptor, bytes, offset, bytes.length - offset);
|
|
193
|
+
if (written === 0)
|
|
194
|
+
throw new Error((options.messages ?? defaultPlatformMessages).fileLockWriteNoProgress());
|
|
195
|
+
offset += written;
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
/** Exclusive create; undefined means another owner holds the path. */
|
|
199
|
+
function tryCreateSync(ctx) {
|
|
200
|
+
const { lockPath, filesystem } = ctx;
|
|
201
|
+
let descriptor;
|
|
202
|
+
try {
|
|
203
|
+
descriptor = filesystem.openSync(lockPath, "wx", 0o600);
|
|
204
|
+
}
|
|
205
|
+
catch (error) {
|
|
206
|
+
if (!isContendedLockError({ code: errorCode(error), platform: process.platform }))
|
|
207
|
+
throw error;
|
|
208
|
+
return undefined;
|
|
209
|
+
}
|
|
157
210
|
let identity;
|
|
158
|
-
|
|
159
|
-
|
|
211
|
+
try {
|
|
212
|
+
identity = filesystem.fstatSync(descriptor);
|
|
213
|
+
// An unidentifiable lock would wedge writers until stale timeout; clean a failed creation
|
|
214
|
+
// only while its inode still matches, rather than deleting a replacement owner.
|
|
215
|
+
writeOwnershipSync(ctx, descriptor);
|
|
216
|
+
}
|
|
217
|
+
catch (error) {
|
|
160
218
|
try {
|
|
161
|
-
descriptor
|
|
162
|
-
|
|
163
|
-
catch (error) {
|
|
164
|
-
if (!isContendedLockError({ code: errorCode(error), platform: process.platform }))
|
|
165
|
-
throw error;
|
|
166
|
-
}
|
|
167
|
-
if (descriptor !== undefined) {
|
|
168
|
-
try {
|
|
169
|
-
identity = filesystem.fstatSync(descriptor);
|
|
170
|
-
// An unidentifiable lock would wedge writers until stale timeout; clean a failed creation
|
|
171
|
-
// only while its inode still matches, rather than deleting a replacement owner.
|
|
172
|
-
const bytes = Buffer.from(raw);
|
|
173
|
-
let offset = 0;
|
|
174
|
-
while (offset < bytes.length) {
|
|
175
|
-
const written = filesystem.writeSync(descriptor, bytes, offset, bytes.length - offset);
|
|
176
|
-
if (written === 0)
|
|
177
|
-
throw new Error((options.messages ?? defaultPlatformMessages).fileLockWriteNoProgress());
|
|
178
|
-
offset += written;
|
|
179
|
-
}
|
|
180
|
-
}
|
|
181
|
-
catch (error) {
|
|
182
|
-
try {
|
|
183
|
-
const created = filesystem.fstatSync(descriptor), current = filesystem.statSync(lockPath);
|
|
184
|
-
if (created.ino === current.ino && created.dev === current.dev)
|
|
185
|
-
filesystem.rmSync(lockPath, { force: true });
|
|
186
|
-
}
|
|
187
|
-
catch { /* preserve creation failure */ }
|
|
188
|
-
throw error;
|
|
189
|
-
}
|
|
190
|
-
finally {
|
|
191
|
-
filesystem.closeSync(descriptor);
|
|
192
|
-
}
|
|
193
|
-
break;
|
|
194
|
-
}
|
|
195
|
-
const observed = inspectSync(lockPath, filesystem);
|
|
196
|
-
if (observed) {
|
|
197
|
-
const current = inspectSync(lockPath, filesystem);
|
|
198
|
-
if (current && stale(observed, current, s)) {
|
|
199
|
-
const last = filesystem.statSync(lockPath, { throwIfNoEntry: false });
|
|
200
|
-
if (last && last.ino === current.ino && last.dev === current.dev && last.mtimeMs === current.mtimeMs) {
|
|
201
|
-
try {
|
|
202
|
-
filesystem.rmSync(lockPath, { force: true });
|
|
203
|
-
options.onStaleLockRemoved?.({ holder: current.holder });
|
|
204
|
-
}
|
|
205
|
-
catch (error) {
|
|
206
|
-
if (errorCode(error) !== "ENOENT")
|
|
207
|
-
throw error;
|
|
208
|
-
}
|
|
209
|
-
}
|
|
210
|
-
}
|
|
219
|
+
if (sameFile(filesystem.fstatSync(descriptor), filesystem.statSync(lockPath)))
|
|
220
|
+
filesystem.rmSync(lockPath, { force: true });
|
|
211
221
|
}
|
|
222
|
+
catch { /* preserve creation failure */ }
|
|
223
|
+
throw error;
|
|
224
|
+
}
|
|
225
|
+
finally {
|
|
226
|
+
filesystem.closeSync(descriptor);
|
|
227
|
+
}
|
|
228
|
+
return identity;
|
|
229
|
+
}
|
|
230
|
+
function removeIfStaleSync(observed, { lockPath, filesystem, s, options }) {
|
|
231
|
+
const current = inspectSync(lockPath, filesystem);
|
|
232
|
+
if (!current || !stale(observed, current, s))
|
|
233
|
+
return;
|
|
234
|
+
const last = filesystem.statSync(lockPath, { throwIfNoEntry: false });
|
|
235
|
+
if (!last || !sameStat(last, current))
|
|
236
|
+
return;
|
|
237
|
+
try {
|
|
238
|
+
filesystem.rmSync(lockPath, { force: true });
|
|
239
|
+
options.onStaleLockRemoved?.({ holder: current.holder });
|
|
240
|
+
}
|
|
241
|
+
catch (error) {
|
|
242
|
+
rethrowUnlessMissing(error);
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
function acquireSync(ctx, start, sleep) {
|
|
246
|
+
for (;;) {
|
|
247
|
+
const identity = tryCreateSync(ctx);
|
|
248
|
+
if (identity)
|
|
249
|
+
return identity;
|
|
250
|
+
const observed = inspectSync(ctx.lockPath, ctx.filesystem);
|
|
251
|
+
if (observed)
|
|
252
|
+
removeIfStaleSync(observed, ctx);
|
|
212
253
|
// Every contention path, including repeated stale breaks and release races, is bounded.
|
|
213
|
-
checkTimeout(lockPath, observed?.holder, start, s, options);
|
|
214
|
-
|
|
254
|
+
checkTimeout(ctx.lockPath, observed?.holder, start, ctx.s, ctx.options);
|
|
255
|
+
sleep({ durationMs: ctx.s.pollMs });
|
|
215
256
|
}
|
|
216
|
-
|
|
257
|
+
}
|
|
258
|
+
function runHeldSync({ lockPath, raw, filesystem, options }, identity, run) {
|
|
259
|
+
const held = () => ownsSnapshot(inspectSync(lockPath, filesystem), raw, identity);
|
|
217
260
|
let completed = false;
|
|
218
261
|
try {
|
|
219
262
|
const value = run({ lockPath, assertHeld: (_required) => { if (!held())
|
|
220
|
-
throw new FileLockLostError({ lockPath }, options
|
|
263
|
+
throw new FileLockLostError({ lockPath }, messagesOption(options)); } });
|
|
221
264
|
completed = true;
|
|
222
265
|
return value;
|
|
223
266
|
}
|
|
@@ -226,77 +269,92 @@ export function withFileLockSync({ lockPath, run }, options = {}) {
|
|
|
226
269
|
if (held())
|
|
227
270
|
filesystem.rmSync(lockPath, { force: true });
|
|
228
271
|
}
|
|
229
|
-
// A failed critical section keeps its original error; durable hosts also suppress release-only failures.
|
|
230
272
|
catch (error) {
|
|
231
|
-
|
|
232
|
-
throw error;
|
|
273
|
+
rethrowReleaseError(error, completed, options);
|
|
233
274
|
}
|
|
234
275
|
}
|
|
235
276
|
}
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
277
|
+
const waitCell = new Int32Array(new SharedArrayBuffer(4));
|
|
278
|
+
/** Synchronous read-modify-write guard. Durable stores pass their former 5s/30s/10ms values.
|
|
279
|
+
* Release never evicts a later owner, even when the old owner reaches finally after a steal. */
|
|
280
|
+
export function withFileLockSync({ lockPath, run }, options = {}) {
|
|
281
|
+
const filesystem = options.filesystem ?? fs, s = settings(options), start = s.monotonicMs(), raw = ownership(s, options);
|
|
239
282
|
if (options.createParent)
|
|
240
|
-
|
|
283
|
+
filesystem.mkdirSync(path.dirname(lockPath), { recursive: true, mode: 0o700 });
|
|
284
|
+
const ctx = { lockPath, raw, filesystem, s, options };
|
|
285
|
+
const identity = acquireSync(ctx, start, options.sleep ?? (({ durationMs }) => { Atomics.wait(waitCell, 0, 0, durationMs); }));
|
|
286
|
+
return runHeldSync(ctx, identity, run);
|
|
287
|
+
}
|
|
288
|
+
async function removeFailedCreationAsync({ lockPath, filesystem }, handle) {
|
|
289
|
+
try {
|
|
290
|
+
const created = await handle.stat(), current = await inspectAsync(lockPath, filesystem);
|
|
291
|
+
if (current && sameFile(created, current))
|
|
292
|
+
await filesystem.rm(lockPath, { force: true });
|
|
293
|
+
}
|
|
294
|
+
catch { /* preserve creation failure */ }
|
|
295
|
+
}
|
|
296
|
+
/** Exclusive create; undefined means another owner holds the path. */
|
|
297
|
+
async function tryCreateAsync(ctx) {
|
|
298
|
+
let handle;
|
|
299
|
+
try {
|
|
300
|
+
handle = await ctx.filesystem.open(ctx.lockPath, "wx", 0o600);
|
|
301
|
+
}
|
|
302
|
+
catch (error) {
|
|
303
|
+
if (!isContendedLockError({ code: errorCode(error), platform: process.platform }))
|
|
304
|
+
throw error;
|
|
305
|
+
return undefined;
|
|
306
|
+
}
|
|
241
307
|
let identity;
|
|
308
|
+
try {
|
|
309
|
+
identity = await handle.stat();
|
|
310
|
+
await handle.writeFile(ctx.raw, "utf8");
|
|
311
|
+
}
|
|
312
|
+
catch (error) {
|
|
313
|
+
await removeFailedCreationAsync(ctx, handle);
|
|
314
|
+
throw error;
|
|
315
|
+
}
|
|
316
|
+
finally {
|
|
317
|
+
await handle.close();
|
|
318
|
+
}
|
|
319
|
+
return identity;
|
|
320
|
+
}
|
|
321
|
+
async function removeIfStaleAsync(observed, { lockPath, filesystem, s, options }) {
|
|
322
|
+
const current = await inspectAsync(lockPath, filesystem);
|
|
323
|
+
if (!current || !stale(observed, current, s))
|
|
324
|
+
return;
|
|
325
|
+
const last = await inspectAsync(lockPath, filesystem);
|
|
326
|
+
if (!last || !unchanged(current, last))
|
|
327
|
+
return;
|
|
328
|
+
// Reading a descriptor pins an inode, not the pathname. Re-stat the pathname after the
|
|
329
|
+
// final descriptor closes, so replacement during awaited inspection cannot be stolen.
|
|
330
|
+
try {
|
|
331
|
+
if (!sameStat(await filesystem.stat(lockPath), last))
|
|
332
|
+
return;
|
|
333
|
+
await filesystem.rm(lockPath, { force: true });
|
|
334
|
+
options.onStaleLockRemoved?.({ holder: current.holder });
|
|
335
|
+
}
|
|
336
|
+
catch (error) {
|
|
337
|
+
rethrowUnlessMissing(error);
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
async function acquireAsync(ctx, start, sleep) {
|
|
242
341
|
for (;;) {
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
}
|
|
251
|
-
if (handle) {
|
|
252
|
-
try {
|
|
253
|
-
identity = await handle.stat();
|
|
254
|
-
await handle.writeFile(raw, "utf8");
|
|
255
|
-
}
|
|
256
|
-
catch (error) {
|
|
257
|
-
try {
|
|
258
|
-
const created = await handle.stat(), current = await inspectAsync(lockPath, filesystem);
|
|
259
|
-
if (current && created.ino === current.ino && created.dev === current.dev)
|
|
260
|
-
await filesystem.rm(lockPath, { force: true });
|
|
261
|
-
}
|
|
262
|
-
catch { /* preserve creation failure */ }
|
|
263
|
-
throw error;
|
|
264
|
-
}
|
|
265
|
-
finally {
|
|
266
|
-
await handle.close();
|
|
267
|
-
}
|
|
268
|
-
break;
|
|
269
|
-
}
|
|
270
|
-
const observed = await inspectAsync(lockPath, filesystem);
|
|
271
|
-
if (observed) {
|
|
272
|
-
const current = await inspectAsync(lockPath, filesystem);
|
|
273
|
-
if (current && stale(observed, current, s)) {
|
|
274
|
-
const last = await inspectAsync(lockPath, filesystem);
|
|
275
|
-
if (last && unchanged(current, last)) {
|
|
276
|
-
// Reading a descriptor pins an inode, not the pathname. Re-stat the pathname after the
|
|
277
|
-
// final descriptor closes, so replacement during awaited inspection cannot be stolen.
|
|
278
|
-
try {
|
|
279
|
-
const atPath = await filesystem.stat(lockPath);
|
|
280
|
-
if (atPath.ino === last.ino && atPath.dev === last.dev && atPath.mtimeMs === last.mtimeMs) {
|
|
281
|
-
await filesystem.rm(lockPath, { force: true });
|
|
282
|
-
options.onStaleLockRemoved?.({ holder: current.holder });
|
|
283
|
-
}
|
|
284
|
-
}
|
|
285
|
-
catch (error) {
|
|
286
|
-
if (errorCode(error) !== "ENOENT")
|
|
287
|
-
throw error;
|
|
288
|
-
}
|
|
289
|
-
}
|
|
290
|
-
}
|
|
291
|
-
}
|
|
292
|
-
checkTimeout(lockPath, observed?.holder, start, s, options);
|
|
293
|
-
await (options.sleep ?? (({ durationMs }) => new Promise(resolve => setTimeout(resolve, durationMs))))({ durationMs: s.pollMs });
|
|
342
|
+
const identity = await tryCreateAsync(ctx);
|
|
343
|
+
if (identity)
|
|
344
|
+
return identity;
|
|
345
|
+
const observed = await inspectAsync(ctx.lockPath, ctx.filesystem);
|
|
346
|
+
if (observed)
|
|
347
|
+
await removeIfStaleAsync(observed, ctx);
|
|
348
|
+
checkTimeout(ctx.lockPath, observed?.holder, start, ctx.s, ctx.options);
|
|
349
|
+
await sleep({ durationMs: ctx.s.pollMs });
|
|
294
350
|
}
|
|
295
|
-
|
|
351
|
+
}
|
|
352
|
+
async function runHeldAsync({ lockPath, raw, filesystem, options }, identity, run) {
|
|
353
|
+
const held = async () => ownsSnapshot(await inspectAsync(lockPath, filesystem), raw, identity);
|
|
296
354
|
let completed = false;
|
|
297
355
|
try {
|
|
298
356
|
const value = await run({ lockPath, assertHeld: async (_required) => { if (!await held())
|
|
299
|
-
throw new FileLockLostError({ lockPath }, options
|
|
357
|
+
throw new FileLockLostError({ lockPath }, messagesOption(options)); } });
|
|
300
358
|
completed = true;
|
|
301
359
|
return value;
|
|
302
360
|
}
|
|
@@ -305,11 +363,18 @@ export async function withFileLock({ lockPath, run }, options = {}) {
|
|
|
305
363
|
if (await held())
|
|
306
364
|
await filesystem.rm(lockPath, { force: true });
|
|
307
365
|
}
|
|
308
|
-
// A failed critical section keeps its original error; durable hosts also suppress release-only failures.
|
|
309
366
|
catch (error) {
|
|
310
|
-
|
|
311
|
-
throw error;
|
|
367
|
+
rethrowReleaseError(error, completed, options);
|
|
312
368
|
}
|
|
313
369
|
}
|
|
314
370
|
}
|
|
371
|
+
/** Async adapter of the same stale and ownership policy; polling yields to other writers. */
|
|
372
|
+
export async function withFileLock({ lockPath, run }, options = {}) {
|
|
373
|
+
const filesystem = options.filesystem ?? asyncFs, s = settings(options), start = s.monotonicMs(), raw = ownership(s, options);
|
|
374
|
+
if (options.createParent)
|
|
375
|
+
await filesystem.mkdir(path.dirname(lockPath), { recursive: true, mode: 0o700 });
|
|
376
|
+
const ctx = { lockPath, raw, filesystem, s, options };
|
|
377
|
+
const identity = await acquireAsync(ctx, start, options.sleep ?? (({ durationMs }) => new Promise(resolve => setTimeout(resolve, durationMs))));
|
|
378
|
+
return runHeldAsync(ctx, identity, run);
|
|
379
|
+
}
|
|
315
380
|
//# sourceMappingURL=file-lock.js.map
|
|
@@ -137,7 +137,8 @@ export function createGuardedFileReader({ rootPath, denyRules, limits, filesyste
|
|
|
137
137
|
}
|
|
138
138
|
}
|
|
139
139
|
// A target equal to base yields [""] after split, so check leaf length, not array length.
|
|
140
|
-
|
|
140
|
+
// split always yields at least one element, so the leaf is always defined.
|
|
141
|
+
const effectiveLeaf = effectiveSegments[effectiveSegments.length - 1];
|
|
141
142
|
if (effectiveLeaf.length > 0 && isDeniedFsFileName(effectiveLeaf)) {
|
|
142
143
|
throw new FsFilePathError({ message: `path '${relativePath}' resolves to a denied filename pattern and cannot be accessed` });
|
|
143
144
|
}
|
|
@@ -213,9 +214,18 @@ export function createGuardedFileReader({ rootPath, denyRules, limits, filesyste
|
|
|
213
214
|
return statSync(target, { throwIfNoEntry: false });
|
|
214
215
|
}
|
|
215
216
|
catch (err) {
|
|
216
|
-
throw new FsFilePathError({ message: `path '${relativePath}' could not be read: ${
|
|
217
|
+
throw new FsFilePathError({ message: `path '${relativePath}' could not be read: ${errorText(err)}` });
|
|
217
218
|
}
|
|
218
219
|
}
|
|
220
|
+
// Both read paths refuse missing and non-regular targets with identical wording.
|
|
221
|
+
function statRegularFile(target, relativePath) {
|
|
222
|
+
const stat = statOrFsPathError(target, relativePath);
|
|
223
|
+
if (!stat)
|
|
224
|
+
throw new FsFilePathError({ message: `file '${relativePath}' does not exist` });
|
|
225
|
+
if (!stat.isFile())
|
|
226
|
+
throw new FsFilePathError({ message: `path '${relativePath}' is not a regular file` });
|
|
227
|
+
return stat;
|
|
228
|
+
}
|
|
219
229
|
// A bounded leading-NUL heuristic keeps binary databases/images/archives out of inline UTF-8
|
|
220
230
|
// model input without making type sniffing proportional to the full file size.
|
|
221
231
|
function looksBinary(buffer) {
|
|
@@ -228,16 +238,19 @@ export function createGuardedFileReader({ rootPath, denyRules, limits, filesyste
|
|
|
228
238
|
}
|
|
229
239
|
function readFsFile(required) {
|
|
230
240
|
const target = resolveFsFilePath(required);
|
|
231
|
-
const stat =
|
|
232
|
-
if (!stat)
|
|
233
|
-
throw new FsFilePathError({ message: `file '${required.relativePath}' does not exist` });
|
|
234
|
-
if (!stat.isFile())
|
|
235
|
-
throw new FsFilePathError({ message: `path '${required.relativePath}' is not a regular file` });
|
|
241
|
+
const stat = statRegularFile(target, required.relativePath);
|
|
236
242
|
if (stat.size > MAX_FS_FILE_BYTES) {
|
|
237
243
|
throw new FsFilePathError({ message: `file '${required.relativePath}' exceeds the ${MAX_FS_FILE_BYTES}-byte readable limit` });
|
|
238
244
|
}
|
|
245
|
+
const buffer = readBoundedSync(target, required.relativePath);
|
|
246
|
+
if (looksBinary(buffer)) {
|
|
247
|
+
throw new FsFilePathError({ message: `file '${required.relativePath}' looks like a binary file and cannot be read as text` });
|
|
248
|
+
}
|
|
249
|
+
return { content: buffer.toString("utf8"), bytes: buffer.byteLength };
|
|
250
|
+
}
|
|
251
|
+
// Read one byte past the cap so a file that grew after stat is refused rather than clipped.
|
|
252
|
+
function readBoundedSync(target, relativePath) {
|
|
239
253
|
const fd = fs.openSync(target, "r");
|
|
240
|
-
let buffer;
|
|
241
254
|
try {
|
|
242
255
|
const bounded = Buffer.alloc(MAX_FS_FILE_BYTES + 1);
|
|
243
256
|
let count = 0;
|
|
@@ -248,16 +261,12 @@ export function createGuardedFileReader({ rootPath, denyRules, limits, filesyste
|
|
|
248
261
|
count += read;
|
|
249
262
|
}
|
|
250
263
|
if (count > MAX_FS_FILE_BYTES)
|
|
251
|
-
throw new FsFilePathError({ message: `file '${
|
|
252
|
-
|
|
264
|
+
throw new FsFilePathError({ message: `file '${relativePath}' exceeds the ${MAX_FS_FILE_BYTES}-byte readable limit` });
|
|
265
|
+
return bounded.subarray(0, count);
|
|
253
266
|
}
|
|
254
267
|
finally {
|
|
255
268
|
fs.closeSync(fd);
|
|
256
269
|
}
|
|
257
|
-
if (looksBinary(buffer)) {
|
|
258
|
-
throw new FsFilePathError({ message: `file '${required.relativePath}' looks like a binary file and cannot be read as text` });
|
|
259
|
-
}
|
|
260
|
-
return { content: buffer.toString("utf8"), bytes: buffer.byteLength };
|
|
261
270
|
}
|
|
262
271
|
// A stat-only size check can be bypassed by a growing file: bound the stream independently and
|
|
263
272
|
// reject rather than return a clipped image/video. Destroy the stream on success or failure.
|
|
@@ -267,32 +276,19 @@ export function createGuardedFileReader({ rootPath, denyRules, limits, filesyste
|
|
|
267
276
|
throw new FsFilePathError({ message: "maxBytes must be a positive safe integer" });
|
|
268
277
|
}
|
|
269
278
|
const target = resolveFsFilePath(required);
|
|
270
|
-
const stat =
|
|
271
|
-
if (!stat)
|
|
272
|
-
throw new FsFilePathError({ message: `file '${relativePath}' does not exist` });
|
|
273
|
-
if (!stat.isFile())
|
|
274
|
-
throw new FsFilePathError({ message: `path '${relativePath}' is not a regular file` });
|
|
279
|
+
const stat = statRegularFile(target, relativePath);
|
|
275
280
|
if (stat.size > maxBytes) {
|
|
276
281
|
throw new FsFilePathError({ message: `file '${relativePath}' is ${stat.size} bytes and exceeds the ${maxBytes}-byte import limit` });
|
|
277
282
|
}
|
|
283
|
+
return readBoundedStream(target, { relativePath, maxBytes });
|
|
284
|
+
}
|
|
285
|
+
async function readBoundedStream(target, { relativePath, maxBytes }) {
|
|
278
286
|
const stream = fs.createReadStream(target, { highWaterMark: Math.min(64 * 1024, maxBytes + 1) });
|
|
279
|
-
const chunks = [];
|
|
280
|
-
let totalBytes = 0;
|
|
281
287
|
try {
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
throw new FsFilePathError({ message: `file '${relativePath}' is at least ${totalBytes} bytes and exceeds the ${maxBytes}-byte import limit` });
|
|
287
|
-
}
|
|
288
|
-
chunks.push(bytes);
|
|
289
|
-
}
|
|
290
|
-
return Buffer.concat(chunks, totalBytes);
|
|
291
|
-
}
|
|
292
|
-
catch (error) {
|
|
293
|
-
if (error instanceof FsFilePathError)
|
|
294
|
-
throw error;
|
|
295
|
-
throw new FsFilePathError({ message: `path '${relativePath}' could not be read: ${error instanceof Error ? error.message : String(error)}` });
|
|
288
|
+
// Own refusals pass through; any other stream failure becomes this reader's refusal class.
|
|
289
|
+
return await collectBoundedChunks(stream, { relativePath, maxBytes }).catch((error) => Promise.reject(error instanceof FsFilePathError
|
|
290
|
+
? error
|
|
291
|
+
: new FsFilePathError({ message: `path '${relativePath}' could not be read: ${errorText(error)}` })));
|
|
296
292
|
}
|
|
297
293
|
finally {
|
|
298
294
|
stream.destroy();
|
|
@@ -307,4 +303,20 @@ export function createGuardedFileReader({ rootPath, denyRules, limits, filesyste
|
|
|
307
303
|
isDeniedPathSegment: ({ segmentName }) => isDeniedFsPathSegment(segmentName),
|
|
308
304
|
};
|
|
309
305
|
}
|
|
306
|
+
function errorText(error) {
|
|
307
|
+
return error instanceof Error ? error.message : String(error);
|
|
308
|
+
}
|
|
309
|
+
async function collectBoundedChunks(stream, { relativePath, maxBytes }) {
|
|
310
|
+
const chunks = [];
|
|
311
|
+
let totalBytes = 0;
|
|
312
|
+
for await (const chunk of stream) {
|
|
313
|
+
const bytes = chunk;
|
|
314
|
+
totalBytes += bytes.byteLength;
|
|
315
|
+
if (totalBytes > maxBytes) {
|
|
316
|
+
throw new FsFilePathError({ message: `file '${relativePath}' is at least ${totalBytes} bytes and exceeds the ${maxBytes}-byte import limit` });
|
|
317
|
+
}
|
|
318
|
+
chunks.push(bytes);
|
|
319
|
+
}
|
|
320
|
+
return Buffer.concat(chunks, totalBytes);
|
|
321
|
+
}
|
|
310
322
|
//# sourceMappingURL=guarded-reader.js.map
|
|
@@ -15,7 +15,7 @@ export interface UserDirectoryOptions {
|
|
|
15
15
|
export declare function resolveUserDataDirectories({ homeDir, appName }: {
|
|
16
16
|
homeDir: string;
|
|
17
17
|
appName: string;
|
|
18
|
-
},
|
|
18
|
+
}, options?: UserDirectoryOptions): {
|
|
19
19
|
appData: string;
|
|
20
20
|
documents: string;
|
|
21
21
|
};
|