@jini-ai/platform 0.5.9 → 0.6.10

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (79) hide show
  1. package/CHANGELOG.md +22 -1
  2. package/dist/asset-cache.js +26 -25
  3. package/dist/blob-storage.d.ts +7 -2
  4. package/dist/blob-storage.js +99 -59
  5. package/dist/child-environment.d.ts +36 -0
  6. package/dist/child-environment.js +68 -0
  7. package/dist/download.js +192 -148
  8. package/dist/fs/atomic-write.js +105 -93
  9. package/dist/fs/boot-identity.d.ts +16 -0
  10. package/dist/fs/boot-identity.js +38 -17
  11. package/dist/fs/directory-home.js +6 -0
  12. package/dist/fs/directory-registry.d.ts +1 -1
  13. package/dist/fs/directory-registry.js +23 -16
  14. package/dist/fs/directory-running-lock.d.ts +1 -1
  15. package/dist/fs/directory-running-lock.js +18 -7
  16. package/dist/fs/directory-sync.d.ts +1 -1
  17. package/dist/fs/directory-sync.js +64 -51
  18. package/dist/fs/directory-user-config.js +108 -67
  19. package/dist/fs/directory-workspace.d.ts +11 -0
  20. package/dist/fs/directory-workspace.js +16 -11
  21. package/dist/fs/durable-json.js +49 -37
  22. package/dist/fs/file-lock.js +214 -149
  23. package/dist/fs/guarded-reader.js +47 -35
  24. package/dist/fs/user-data-directories.d.ts +1 -1
  25. package/dist/fs/user-data-directories.js +25 -9
  26. package/dist/fs/verified-directory-move.d.ts +13 -11
  27. package/dist/fs/verified-directory-move.js +105 -68
  28. package/dist/fs/verified-file-relocation.d.ts +1 -1
  29. package/dist/fs/verified-file-relocation.js +111 -94
  30. package/dist/http/guarded/client.d.ts +3 -4
  31. package/dist/http/guarded/client.js +50 -60
  32. package/dist/http/guarded/fetch-adapter.d.ts +2 -3
  33. package/dist/http/guarded/fetch-adapter.js +26 -15
  34. package/dist/mail/builtin-providers.d.ts +39 -0
  35. package/dist/mail/builtin-providers.js +60 -0
  36. package/dist/mail/feedback.d.ts +84 -0
  37. package/dist/mail/feedback.js +90 -0
  38. package/dist/mail/index.d.ts +11 -0
  39. package/dist/mail/index.js +6 -0
  40. package/dist/mail/policy.d.ts +77 -0
  41. package/dist/mail/policy.js +155 -0
  42. package/dist/mail/ports.d.ts +56 -7
  43. package/dist/mail/provider.d.ts +117 -0
  44. package/dist/mail/provider.js +74 -0
  45. package/dist/mail/rate-limit.d.ts +33 -0
  46. package/dist/mail/rate-limit.js +35 -0
  47. package/dist/mail/sender-identity.d.ts +68 -0
  48. package/dist/mail/sender-identity.js +50 -0
  49. package/dist/mail/types.d.ts +9 -0
  50. package/dist/net/address.d.ts +9 -23
  51. package/dist/net/address.js +28 -133
  52. package/dist/net/endpoint-policy.d.ts +26 -2
  53. package/dist/net/endpoint-policy.js +159 -56
  54. package/dist/object-store/index.d.ts +2 -0
  55. package/dist/object-store/index.js +2 -0
  56. package/dist/object-store/local-fs.d.ts +17 -0
  57. package/dist/object-store/local-fs.js +146 -0
  58. package/dist/process.js +2 -0
  59. package/dist/proxy-env.js +131 -135
  60. package/dist/secrets/credential-sets/contracts.d.ts +1 -0
  61. package/dist/secrets/credential-sets/contracts.js +1 -0
  62. package/dist/secrets/credential-sets/index.d.ts +1 -0
  63. package/dist/secrets/credential-sets/index.js +1 -0
  64. package/dist/secrets/credential-sets/provider-matches.d.ts +61 -0
  65. package/dist/secrets/credential-sets/provider-matches.js +61 -0
  66. package/dist/secrets/credential-sets/types.d.ts +7 -0
  67. package/dist/secrets/credential-sets/vault.meta.js +58 -20
  68. package/dist/secrets/credential-sets/vault.repo.memory.d.ts +7 -0
  69. package/dist/secrets/credential-sets/vault.repo.memory.js +43 -36
  70. package/dist/secrets/credential-sets/vault.service.d.ts +9 -0
  71. package/dist/secrets/credential-sets/vault.service.js +131 -61
  72. package/dist/secrets/credential-sets/vault.types.d.ts +11 -3
  73. package/dist/secrets/credential-sets/vault.types.js +22 -8
  74. package/dist/secrets/keyring.env.js +14 -10
  75. package/dist/toolchain.js +119 -91
  76. package/dist/web/html-document.js +73 -69
  77. package/dist/web/html-to-markdown.js +127 -102
  78. package/dist/web/web-page.js +160 -80
  79. package/package.json +12 -4
@@ -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 (typeof value !== "object" || value === null || !("pid" in value) || !validPid(value.pid) || !("hostname" in value) || typeof value.hostname !== "string" || !("token" in value) || typeof value.token !== "string" || !("acquiredAt" in value) || typeof value.acquiredAt !== "string")
66
+ if (!isHolderRecord(value))
55
67
  return undefined;
56
- return { pid: value.pid, hostname: value.hostname, token: value.token, acquiredAt: value.acquiredAt, ...("bootId" in value && typeof value.bootId === "string" ? { bootId: value.bootId } : {}) };
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 settings(options) {
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 (!Number.isSafeInteger(timeoutMs) || timeoutMs < 0 || (!Number.isSafeInteger(staleMs) && staleMs !== Infinity) || staleMs <= 0 || !Number.isSafeInteger(pollMs) || pollMs <= 0)
84
+ if (!intervalsValid({ timeoutMs, staleMs, pollMs }))
70
85
  throw new RangeError((options.messages ?? defaultPlatformMessages).fileLockIntervalsInvalid());
71
- const processPort = options.process ?? { pid: process.pid, bootIdentity: () => readBootIdentity({}), isAlive: ({ pid }) => { try {
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 { timeoutMs, staleMs, pollMs, processPort, clock, monotonicMs, boot: processPort.bootIdentity?.(), hostname: (options.hostname ?? nodeHostname)(), token: (options.token ?? randomUUID)() };
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 ownerAlive(snapshot, s) {
86
- const holder = snapshot.holder;
87
- if (holder) {
88
- if (holder.hostname !== s.hostname)
89
- return undefined;
90
- if (s.boot && (holder.bootId !== undefined ? holder.bootId !== s.boot.id : Date.parse(holder.acquiredAt) < s.boot.startedAtMs))
91
- return false;
92
- return s.processPort.isAlive({ pid: holder.pid });
93
- }
94
- // Legacy synchronous stores wrote pid:random rather than JSON. Malformed pids remain unknown.
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) && s.boot && (parts[2] ? parts[2] !== s.boot.id : snapshot.mtimeMs < s.boot.startedAtMs))
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 validPid(pid) ? s.processPort.isAlive({ pid }) : undefined;
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 unchanged(a, b) { return a.ino === b.ino && a.dev === b.dev && a.raw === b.raw && a.mtimeMs === b.mtimeMs; }
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.messages ? { messages: options.messages } : {}), ...(message !== undefined ? { message } : {}) });
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 (errorCode(error) === "ENOENT" || isContendedLockError({ code: errorCode(error), platform: process.platform }))
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 (errorCode(error) === "ENOENT" || isContendedLockError({ code: errorCode(error), platform: process.platform }))
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
- const waitCell = new Int32Array(new SharedArrayBuffer(4));
151
- /** Synchronous read-modify-write guard. Durable stores pass their former 5s/30s/10ms values.
152
- * Release never evicts a later owner, even when the old owner reaches finally after a steal. */
153
- export function withFileLockSync({ lockPath, run }, options = {}) {
154
- const filesystem = options.filesystem ?? fs, s = settings(options), start = s.monotonicMs(), raw = ownership(s, options);
155
- if (options.createParent)
156
- filesystem.mkdirSync(path.dirname(lockPath), { recursive: true, mode: 0o700 });
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
- for (;;) {
159
- let descriptor;
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 = filesystem.openSync(lockPath, "wx", 0o600);
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
- (options.sleep ?? (({ durationMs }) => { Atomics.wait(waitCell, 0, 0, durationMs); }))({ durationMs: s.pollMs });
254
+ checkTimeout(ctx.lockPath, observed?.holder, start, ctx.s, ctx.options);
255
+ sleep({ durationMs: ctx.s.pollMs });
215
256
  }
216
- const held = () => { const current = inspectSync(lockPath, filesystem); return current && current.raw === raw && current.ino === identity.ino && current.dev === identity.dev; };
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.messages ? { messages: options.messages } : {}); } });
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
- if (completed && !options.suppressReleaseErrors && errorCode(error) !== "ENOENT")
232
- throw error;
273
+ rethrowReleaseError(error, completed, options);
233
274
  }
234
275
  }
235
276
  }
236
- /** Async adapter of the same stale and ownership policy; polling yields to other writers. */
237
- export async function withFileLock({ lockPath, run }, options = {}) {
238
- const filesystem = options.filesystem ?? asyncFs, s = settings(options), start = s.monotonicMs(), raw = ownership(s, options);
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
- await filesystem.mkdir(path.dirname(lockPath), { recursive: true, mode: 0o700 });
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
- let handle;
244
- try {
245
- handle = await filesystem.open(lockPath, "wx", 0o600);
246
- }
247
- catch (error) {
248
- if (!isContendedLockError({ code: errorCode(error), platform: process.platform }))
249
- throw error;
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
- const held = async () => { const current = await inspectAsync(lockPath, filesystem); return current && current.raw === raw && current.ino === identity.ino && current.dev === identity.dev; };
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.messages ? { messages: options.messages } : {}); } });
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
- if (completed && !options.suppressReleaseErrors && errorCode(error) !== "ENOENT")
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
- const effectiveLeaf = effectiveSegments.at(-1) ?? "";
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: ${err instanceof Error ? err.message : String(err)}` });
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 = statOrFsPathError(target, required.relativePath);
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 '${required.relativePath}' exceeds the ${MAX_FS_FILE_BYTES}-byte readable limit` });
252
- buffer = bounded.subarray(0, count);
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 = statOrFsPathError(target, relativePath);
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
- for await (const chunk of stream) {
283
- const bytes = chunk;
284
- totalBytes += bytes.byteLength;
285
- if (totalBytes > maxBytes) {
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
- }, { platform, env, appDataDir, readText, windowsDocuments }?: UserDirectoryOptions): {
18
+ }, options?: UserDirectoryOptions): {
19
19
  appData: string;
20
20
  documents: string;
21
21
  };