@openclaw/fs-safe 0.2.7 → 0.4.0
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 +26 -0
- package/README.md +18 -5
- package/dist/advanced.d.ts +1 -0
- package/dist/advanced.d.ts.map +1 -1
- package/dist/advanced.js +1 -0
- package/dist/deny-mutations.d.ts +11 -0
- package/dist/deny-mutations.d.ts.map +1 -0
- package/dist/deny-mutations.js +102 -0
- package/dist/device-path.d.ts +13 -0
- package/dist/device-path.d.ts.map +1 -0
- package/dist/device-path.js +112 -0
- package/dist/errors.d.ts +1 -1
- package/dist/errors.d.ts.map +1 -1
- package/dist/fsync.d.ts +2 -0
- package/dist/fsync.d.ts.map +1 -0
- package/dist/fsync.js +21 -0
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/json.d.ts.map +1 -1
- package/dist/json.js +51 -4
- package/dist/move-path.d.ts +3 -0
- package/dist/move-path.d.ts.map +1 -1
- package/dist/move-path.js +13 -1
- package/dist/opened-realpath.d.ts +3 -0
- package/dist/opened-realpath.d.ts.map +1 -0
- package/dist/opened-realpath.js +79 -0
- package/dist/path.d.ts +1 -0
- package/dist/path.d.ts.map +1 -1
- package/dist/path.js +1 -0
- package/dist/pinned-open.d.ts.map +1 -1
- package/dist/pinned-open.js +7 -0
- package/dist/pinned-write.d.ts.map +1 -1
- package/dist/pinned-write.js +3 -0
- package/dist/read-opened-file.d.ts +18 -0
- package/dist/read-opened-file.d.ts.map +1 -0
- package/dist/read-opened-file.js +15 -0
- package/dist/regular-file.d.ts.map +1 -1
- package/dist/regular-file.js +26 -3
- package/dist/root-impl.d.ts +18 -15
- package/dist/root-impl.d.ts.map +1 -1
- package/dist/root-impl.js +110 -147
- package/dist/root.d.ts +1 -1
- package/dist/root.d.ts.map +1 -1
- package/dist/secure-file.d.ts.map +1 -1
- package/dist/secure-file.js +2 -0
- package/dist/walk.d.ts +13 -2
- package/dist/walk.d.ts.map +1 -1
- package/dist/walk.js +29 -6
- package/docs/contributing.md +1 -1
- package/docs/errors.md +5 -0
- package/docs/install.md +3 -3
- package/docs/path.md +13 -0
- package/docs/reading.md +6 -4
- package/docs/root.md +15 -5
- package/docs/security-model.md +7 -2
- package/docs/types.md +22 -9
- package/docs/walk.md +11 -1
- package/docs/writing.md +21 -3
- package/package.json +22 -17
package/dist/root-impl.js
CHANGED
|
@@ -6,14 +6,18 @@ import { pipeline } from "node:stream/promises";
|
|
|
6
6
|
import { createBoundedReadStream } from "./bounded-read-stream.js";
|
|
7
7
|
import { assertAsyncDirectoryGuard, createAsyncDirectoryGuard, createNearestExistingDirectoryGuard } from "./directory-guard.js";
|
|
8
8
|
import { FsSafeError } from "./errors.js";
|
|
9
|
+
import { syncDirectoryBestEffort } from "./fsync.js";
|
|
9
10
|
import { sameFileIdentity } from "./file-identity.js";
|
|
10
11
|
import { mkdirPathComponentsWithGuards } from "./guarded-mkdir.js";
|
|
11
12
|
import { withAsyncDirectoryGuards } from "./guarded-mutation.js";
|
|
13
|
+
import { assertMutationNotDenied, mergeDenyMutationPolicies, } from "./deny-mutations.js";
|
|
14
|
+
import { resolveOpenedFileRealPathForHandle } from "./opened-realpath.js";
|
|
12
15
|
import { isPinnedPathHelperSpawnError, runPinnedPathHelper } from "./pinned-path.js";
|
|
13
16
|
import { runPinnedCopyHelper, runPinnedWriteHelper } from "./pinned-write.js";
|
|
14
17
|
import { canFallbackFromPythonError, getFsSafePythonConfig } from "./pinned-python-config.js";
|
|
15
18
|
import { assertNoPathAliasEscape, PATH_ALIAS_POLICIES } from "./path-policy.js";
|
|
16
|
-
import { assertNoNulPathInput, hasNodeErrorCode, isNotFoundPathError, isPathInside, isSymlinkOpenError, } from "./path.js";
|
|
19
|
+
import { assertNoNulPathInput, assertNoUnsafeDeviceReadPath, hasNodeErrorCode, isNotFoundPathError, isPathInside, isSymlinkOpenError, } from "./path.js";
|
|
20
|
+
import { readOpenedFileSafely } from "./read-opened-file.js";
|
|
17
21
|
import { helperReaddir, helperStat, runPinnedHelper, } from "./pinned-helper.js";
|
|
18
22
|
import { pathStatFromStats } from "./path-stat.js";
|
|
19
23
|
import { resolveRootPath } from "./root-path.js";
|
|
@@ -23,6 +27,7 @@ import { getFsSafeTestHooks } from "./test-hooks.js";
|
|
|
23
27
|
import { stringifyJsonDocument } from "./json-stringify.js";
|
|
24
28
|
import { registerTempPathForExit } from "./temp-cleanup.js";
|
|
25
29
|
import { serializePathWrite } from "./write-queue.js";
|
|
30
|
+
export { resolveOpenedFileRealPathForHandle } from "./opened-realpath.js";
|
|
26
31
|
function logWarn(message) {
|
|
27
32
|
if (process.env.FS_SAFE_DEBUG_WARNINGS === "1") {
|
|
28
33
|
console.warn(message);
|
|
@@ -46,20 +51,16 @@ const OPEN_APPEND_CREATE_FLAGS = fsConstants.O_RDWR |
|
|
|
46
51
|
fsConstants.O_EXCL |
|
|
47
52
|
(SUPPORTS_NOFOLLOW ? fsConstants.O_NOFOLLOW : 0);
|
|
48
53
|
export const DEFAULT_ROOT_MAX_BYTES = 16 * 1024 * 1024;
|
|
49
|
-
function closeHandleForDispose(handle) {
|
|
50
|
-
return handle.close().catch(() => undefined);
|
|
51
|
-
}
|
|
52
54
|
function openResult(params) {
|
|
53
55
|
return {
|
|
54
56
|
handle: params.handle,
|
|
55
57
|
realPath: params.realPath,
|
|
56
58
|
stat: params.stat,
|
|
57
|
-
[Symbol.asyncDispose]:
|
|
58
|
-
await closeHandleForDispose(params.handle);
|
|
59
|
-
},
|
|
59
|
+
[Symbol.asyncDispose]: () => params.handle.close().catch(() => undefined),
|
|
60
60
|
};
|
|
61
61
|
}
|
|
62
62
|
async function openVerifiedLocalFile(filePath, options) {
|
|
63
|
+
assertNoUnsafeDeviceReadPath(filePath);
|
|
63
64
|
const fsSafeTestHooks = getFsSafeTestHooks();
|
|
64
65
|
// Reject directories before opening so we never surface EISDIR to callers (e.g. tool
|
|
65
66
|
// results that get sent to messaging channels). See openclaw/openclaw#31186.
|
|
@@ -216,6 +217,7 @@ class RootHandle {
|
|
|
216
217
|
mkdir: this.defaults.mkdir,
|
|
217
218
|
mode: this.defaults.mode,
|
|
218
219
|
...options,
|
|
220
|
+
denyMutations: mergeDenyMutationPolicies(this.defaults.denyMutations, options.denyMutations),
|
|
219
221
|
append: writeMode === "append",
|
|
220
222
|
truncateExisting: writeMode === "replace",
|
|
221
223
|
});
|
|
@@ -227,18 +229,29 @@ class RootHandle {
|
|
|
227
229
|
mkdir: this.defaults.mkdir,
|
|
228
230
|
mode: this.defaults.mode,
|
|
229
231
|
...options,
|
|
232
|
+
denyMutations: mergeDenyMutationPolicies(this.defaults.denyMutations, options.denyMutations),
|
|
230
233
|
});
|
|
231
234
|
}
|
|
232
|
-
async remove(relativePath) {
|
|
235
|
+
async remove(relativePath, options = {}) {
|
|
233
236
|
assertValidRootRelativePath(relativePath);
|
|
234
|
-
await removePathInRoot(this.context,
|
|
237
|
+
await removePathInRoot(this.context, {
|
|
238
|
+
relativePath,
|
|
239
|
+
denyMutations: mergeDenyMutationPolicies(this.defaults.denyMutations, options.denyMutations),
|
|
240
|
+
});
|
|
235
241
|
}
|
|
236
|
-
async mkdir(relativePath) {
|
|
242
|
+
async mkdir(relativePath, options = {}) {
|
|
237
243
|
assertValidRootRelativePath(relativePath);
|
|
238
|
-
await mkdirPathInRoot(this.context, {
|
|
244
|
+
await mkdirPathInRoot(this.context, {
|
|
245
|
+
relativePath,
|
|
246
|
+
denyMutations: mergeDenyMutationPolicies(this.defaults.denyMutations, options.denyMutations),
|
|
247
|
+
});
|
|
239
248
|
}
|
|
240
|
-
async ensureRoot() {
|
|
241
|
-
await mkdirPathInRoot(this.context, {
|
|
249
|
+
async ensureRoot(options = {}) {
|
|
250
|
+
await mkdirPathInRoot(this.context, {
|
|
251
|
+
relativePath: "",
|
|
252
|
+
allowRoot: true,
|
|
253
|
+
denyMutations: mergeDenyMutationPolicies(this.defaults.denyMutations, options.denyMutations),
|
|
254
|
+
});
|
|
242
255
|
}
|
|
243
256
|
async write(relativePath, data, options = {}) {
|
|
244
257
|
await writeFileInRoot(this.context, {
|
|
@@ -247,6 +260,7 @@ class RootHandle {
|
|
|
247
260
|
mkdir: this.defaults.mkdir,
|
|
248
261
|
mode: this.defaults.mode,
|
|
249
262
|
...options,
|
|
263
|
+
denyMutations: mergeDenyMutationPolicies(this.defaults.denyMutations, options.denyMutations),
|
|
250
264
|
});
|
|
251
265
|
}
|
|
252
266
|
async create(relativePath, data, options = {}) {
|
|
@@ -256,6 +270,7 @@ class RootHandle {
|
|
|
256
270
|
mkdir: this.defaults.mkdir,
|
|
257
271
|
mode: this.defaults.mode,
|
|
258
272
|
...options,
|
|
273
|
+
denyMutations: mergeDenyMutationPolicies(this.defaults.denyMutations, options.denyMutations),
|
|
259
274
|
overwrite: false,
|
|
260
275
|
});
|
|
261
276
|
}
|
|
@@ -278,6 +293,7 @@ class RootHandle {
|
|
|
278
293
|
mkdir: this.defaults.mkdir,
|
|
279
294
|
mode: this.defaults.mode,
|
|
280
295
|
...options,
|
|
296
|
+
denyMutations: mergeDenyMutationPolicies(this.defaults.denyMutations, options.denyMutations),
|
|
281
297
|
});
|
|
282
298
|
}
|
|
283
299
|
async exists(relativePath) {
|
|
@@ -321,6 +337,12 @@ class RootHandle {
|
|
|
321
337
|
async move(fromRelative, toRelative, options = {}) {
|
|
322
338
|
assertValidRootRelativePath(fromRelative);
|
|
323
339
|
assertValidRootRelativePath(toRelative);
|
|
340
|
+
const denyMutations = mergeDenyMutationPolicies(this.defaults.denyMutations, options.denyMutations);
|
|
341
|
+
await assertMoveMutationAllowed(this.context, {
|
|
342
|
+
fromRelative,
|
|
343
|
+
toRelative,
|
|
344
|
+
denyMutations,
|
|
345
|
+
});
|
|
324
346
|
try {
|
|
325
347
|
await runPinnedHelper("rename", this.rootReal, {
|
|
326
348
|
from: fromRelative,
|
|
@@ -332,6 +354,7 @@ class RootHandle {
|
|
|
332
354
|
if (canFallbackFromPythonError(error)) {
|
|
333
355
|
await movePathFallback(this.context, {
|
|
334
356
|
fromRelative,
|
|
357
|
+
denyMutations,
|
|
335
358
|
overwrite: options.overwrite ?? false,
|
|
336
359
|
toRelative,
|
|
337
360
|
});
|
|
@@ -413,20 +436,6 @@ export async function openLocalFileSafely(params) {
|
|
|
413
436
|
assertNoNulPathInput(params.filePath, "file path contains a NUL byte");
|
|
414
437
|
return await openVerifiedLocalFile(params.filePath);
|
|
415
438
|
}
|
|
416
|
-
async function readOpenedFileSafely(params) {
|
|
417
|
-
if (params.maxBytes !== undefined && params.opened.stat.size > params.maxBytes) {
|
|
418
|
-
throw new FsSafeError("too-large", `file exceeds limit of ${params.maxBytes} bytes (got ${params.opened.stat.size})`);
|
|
419
|
-
}
|
|
420
|
-
const buffer = await params.opened.handle.readFile();
|
|
421
|
-
if (params.maxBytes !== undefined && buffer.byteLength > params.maxBytes) {
|
|
422
|
-
throw new FsSafeError("too-large", `file exceeds limit of ${params.maxBytes} bytes (got ${buffer.byteLength})`);
|
|
423
|
-
}
|
|
424
|
-
return {
|
|
425
|
-
buffer,
|
|
426
|
-
realPath: params.opened.realPath,
|
|
427
|
-
stat: params.opened.stat,
|
|
428
|
-
};
|
|
429
|
-
}
|
|
430
439
|
function emitWriteBoundaryWarning(reason) {
|
|
431
440
|
logWarn(`security: fs-safe write boundary warning (${reason})`);
|
|
432
441
|
}
|
|
@@ -467,82 +476,9 @@ async function verifyAtomicWriteResult(params) {
|
|
|
467
476
|
await opened.handle.close().catch(() => { });
|
|
468
477
|
}
|
|
469
478
|
}
|
|
470
|
-
export async function resolveOpenedFileRealPathForHandle(handle, ioPath) {
|
|
471
|
-
const handleStat = await handle.stat();
|
|
472
|
-
const fdCandidates = process.platform === "linux"
|
|
473
|
-
? [`/proc/self/fd/${handle.fd}`, `/dev/fd/${handle.fd}`]
|
|
474
|
-
: process.platform === "win32"
|
|
475
|
-
? []
|
|
476
|
-
: [`/dev/fd/${handle.fd}`];
|
|
477
|
-
for (const fdPath of fdCandidates) {
|
|
478
|
-
try {
|
|
479
|
-
const fdRealPath = await fs.realpath(fdPath);
|
|
480
|
-
const fdRealStat = await fs.stat(fdRealPath);
|
|
481
|
-
if (sameFileIdentity(handleStat, fdRealStat)) {
|
|
482
|
-
return fdRealPath;
|
|
483
|
-
}
|
|
484
|
-
}
|
|
485
|
-
catch {
|
|
486
|
-
// try next fd path
|
|
487
|
-
}
|
|
488
|
-
}
|
|
489
|
-
try {
|
|
490
|
-
const ioRealPath = await fs.realpath(ioPath);
|
|
491
|
-
const ioRealStat = await fs.stat(ioRealPath);
|
|
492
|
-
if (sameFileIdentity(handleStat, ioRealStat)) {
|
|
493
|
-
return ioRealPath;
|
|
494
|
-
}
|
|
495
|
-
}
|
|
496
|
-
catch (err) {
|
|
497
|
-
if (!isNotFoundPathError(err)) {
|
|
498
|
-
throw err;
|
|
499
|
-
}
|
|
500
|
-
}
|
|
501
|
-
const parentResolved = await resolveOpenedFileRealPathFromParent(handleStat, ioPath);
|
|
502
|
-
if (parentResolved) {
|
|
503
|
-
return parentResolved;
|
|
504
|
-
}
|
|
505
|
-
throw new FsSafeError("path-mismatch", "unable to resolve opened file path");
|
|
506
|
-
}
|
|
507
|
-
async function resolveOpenedFileRealPathFromParent(handleStat, ioPath) {
|
|
508
|
-
let parentReal;
|
|
509
|
-
try {
|
|
510
|
-
parentReal = await fs.realpath(path.dirname(ioPath));
|
|
511
|
-
}
|
|
512
|
-
catch (err) {
|
|
513
|
-
if (isNotFoundPathError(err)) {
|
|
514
|
-
return null;
|
|
515
|
-
}
|
|
516
|
-
throw err;
|
|
517
|
-
}
|
|
518
|
-
let entries;
|
|
519
|
-
try {
|
|
520
|
-
entries = await fs.readdir(parentReal);
|
|
521
|
-
}
|
|
522
|
-
catch (err) {
|
|
523
|
-
if (isNotFoundPathError(err)) {
|
|
524
|
-
return null;
|
|
525
|
-
}
|
|
526
|
-
throw err;
|
|
527
|
-
}
|
|
528
|
-
for (const entry of entries.toSorted()) {
|
|
529
|
-
const candidatePath = path.join(parentReal, entry);
|
|
530
|
-
try {
|
|
531
|
-
const candidateStat = await fs.lstat(candidatePath);
|
|
532
|
-
if (candidateStat.isFile() && sameFileIdentity(handleStat, candidateStat)) {
|
|
533
|
-
return await fs.realpath(candidatePath);
|
|
534
|
-
}
|
|
535
|
-
}
|
|
536
|
-
catch (err) {
|
|
537
|
-
if (!isNotFoundPathError(err)) {
|
|
538
|
-
throw err;
|
|
539
|
-
}
|
|
540
|
-
}
|
|
541
|
-
}
|
|
542
|
-
return null;
|
|
543
|
-
}
|
|
544
479
|
async function openWritableFileInRoot(root, params) {
|
|
545
480
|
const { rootReal, rootWithSep, resolved } = await resolvePathInRoot(root, params.relativePath);
|
|
481
|
+
await assertMutationNotDenied(resolved, params.denyMutations);
|
|
546
482
|
try {
|
|
547
483
|
await assertNoPathAliasEscape({
|
|
548
484
|
absolutePath: resolved,
|
|
@@ -649,9 +585,7 @@ async function openWritableFileInRoot(root, params) {
|
|
|
649
585
|
createdForWrite,
|
|
650
586
|
realPath,
|
|
651
587
|
stat,
|
|
652
|
-
[Symbol.asyncDispose]:
|
|
653
|
-
await closeHandleForDispose(handle);
|
|
654
|
-
},
|
|
588
|
+
[Symbol.asyncDispose]: () => handle.close().catch(() => undefined),
|
|
655
589
|
};
|
|
656
590
|
}
|
|
657
591
|
catch (err) {
|
|
@@ -669,6 +603,7 @@ async function appendFileInRoot(root, params) {
|
|
|
669
603
|
relativePath: params.relativePath,
|
|
670
604
|
mkdir: params.mkdir,
|
|
671
605
|
mode: params.mode,
|
|
606
|
+
denyMutations: params.denyMutations,
|
|
672
607
|
truncateExisting: false,
|
|
673
608
|
append: true,
|
|
674
609
|
});
|
|
@@ -687,17 +622,22 @@ async function appendFileInRoot(root, params) {
|
|
|
687
622
|
}
|
|
688
623
|
if (typeof params.data === "string") {
|
|
689
624
|
await target.handle.appendFile(`${prefix}${params.data}`, params.encoding ?? "utf8");
|
|
690
|
-
return;
|
|
691
625
|
}
|
|
692
|
-
|
|
693
|
-
|
|
626
|
+
else {
|
|
627
|
+
const payload = prefix.length > 0 ? Buffer.concat([Buffer.from(prefix, "utf8"), params.data]) : params.data;
|
|
628
|
+
await target.handle.appendFile(payload);
|
|
629
|
+
}
|
|
630
|
+
await target.handle.sync();
|
|
631
|
+
if (target.createdForWrite) {
|
|
632
|
+
await syncDirectoryBestEffort(path.dirname(target.realPath));
|
|
633
|
+
}
|
|
694
634
|
}
|
|
695
635
|
finally {
|
|
696
636
|
await target.handle.close().catch(() => { });
|
|
697
637
|
}
|
|
698
638
|
}
|
|
699
|
-
async function removePathInRoot(root,
|
|
700
|
-
const resolved = await resolvePinnedRemovePathInRoot(root, relativePath);
|
|
639
|
+
async function removePathInRoot(root, params) {
|
|
640
|
+
const resolved = await resolvePinnedRemovePathInRoot(root, params.relativePath, params.denyMutations);
|
|
701
641
|
if (process.platform === "win32") {
|
|
702
642
|
await removePathFallback(resolved);
|
|
703
643
|
return;
|
|
@@ -745,44 +685,47 @@ async function writeFileInRoot(root, params) {
|
|
|
745
685
|
});
|
|
746
686
|
return;
|
|
747
687
|
}
|
|
748
|
-
const pinned = await resolvePinnedWriteTargetInRoot(root, params.relativePath, params.mode);
|
|
688
|
+
const pinned = await resolvePinnedWriteTargetInRoot(root, params.relativePath, params.mode, params.denyMutations);
|
|
749
689
|
await serializePathWrite(pinned.targetPath, async () => {
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
throw
|
|
773
|
-
|
|
774
|
-
try {
|
|
775
|
-
await verifyAtomicWriteResult({
|
|
776
|
-
root,
|
|
777
|
-
targetPath: pinned.targetPath,
|
|
778
|
-
expectedIdentity: identity,
|
|
690
|
+
await commitPinnedWriteInRoot(root, pinned, params);
|
|
691
|
+
});
|
|
692
|
+
}
|
|
693
|
+
async function commitPinnedWriteInRoot(root, pinned, params) {
|
|
694
|
+
let identity;
|
|
695
|
+
try {
|
|
696
|
+
identity = await runPinnedWriteHelper({
|
|
697
|
+
rootPath: pinned.rootReal,
|
|
698
|
+
relativeParentPath: pinned.relativeParentPath,
|
|
699
|
+
basename: pinned.basename,
|
|
700
|
+
mkdir: params.mkdir !== false,
|
|
701
|
+
mode: params.mode ?? pinned.mode,
|
|
702
|
+
overwrite: params.overwrite,
|
|
703
|
+
input: {
|
|
704
|
+
kind: "buffer",
|
|
705
|
+
data: params.data,
|
|
706
|
+
encoding: params.encoding,
|
|
707
|
+
},
|
|
708
|
+
});
|
|
709
|
+
}
|
|
710
|
+
catch (error) {
|
|
711
|
+
if (params.overwrite === false && isAlreadyExistsError(error)) {
|
|
712
|
+
throw new FsSafeError("already-exists", "file already exists", {
|
|
713
|
+
cause: error instanceof Error ? error : undefined,
|
|
779
714
|
});
|
|
780
715
|
}
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
716
|
+
throw normalizePinnedWriteError(error);
|
|
717
|
+
}
|
|
718
|
+
try {
|
|
719
|
+
await verifyAtomicWriteResult({
|
|
720
|
+
root,
|
|
721
|
+
targetPath: pinned.targetPath,
|
|
722
|
+
expectedIdentity: identity,
|
|
723
|
+
});
|
|
724
|
+
}
|
|
725
|
+
catch (err) {
|
|
726
|
+
emitWriteBoundaryWarning(`post-write verification failed: ${String(err)}`);
|
|
727
|
+
throw err;
|
|
728
|
+
}
|
|
786
729
|
}
|
|
787
730
|
async function copyFileInRoot(root, params) {
|
|
788
731
|
assertValidRootRelativePath(params.relativePath);
|
|
@@ -801,7 +744,7 @@ async function copyFileInRoot(root, params) {
|
|
|
801
744
|
});
|
|
802
745
|
return;
|
|
803
746
|
}
|
|
804
|
-
const pinned = await resolvePinnedWriteTargetInRoot(root, params.relativePath, params.mode);
|
|
747
|
+
const pinned = await resolvePinnedWriteTargetInRoot(root, params.relativePath, params.mode, params.denyMutations);
|
|
805
748
|
await serializePathWrite(pinned.targetPath, async () => {
|
|
806
749
|
let identity;
|
|
807
750
|
try {
|
|
@@ -845,8 +788,9 @@ async function copyFileInRoot(root, params) {
|
|
|
845
788
|
await source.handle.close().catch(() => { });
|
|
846
789
|
}
|
|
847
790
|
}
|
|
848
|
-
async function resolvePinnedWriteTargetInRoot(root, relativePath, requestedMode) {
|
|
791
|
+
async function resolvePinnedWriteTargetInRoot(root, relativePath, requestedMode, denyMutations) {
|
|
849
792
|
const { rootReal, rootWithSep, resolved } = await resolvePathInRoot(root, relativePath);
|
|
793
|
+
await assertMutationNotDenied(resolved, denyMutations);
|
|
850
794
|
try {
|
|
851
795
|
await assertNoPathAliasEscape({
|
|
852
796
|
absolutePath: resolved,
|
|
@@ -903,12 +847,16 @@ async function resolvePinnedWriteTargetInRoot(root, relativePath, requestedMode)
|
|
|
903
847
|
async function resolvePinnedPathInRoot(root, params) {
|
|
904
848
|
return await resolvePinnedOperationPathInRoot(root, {
|
|
905
849
|
allowRoot: params.allowRoot,
|
|
850
|
+
denyMutations: params.denyMutations,
|
|
851
|
+
protectDenyMutationAncestors: false,
|
|
906
852
|
relativePath: params.relativePath,
|
|
907
853
|
policy: PATH_ALIAS_POLICIES.strict,
|
|
908
854
|
});
|
|
909
855
|
}
|
|
910
|
-
async function resolvePinnedRemovePathInRoot(root, relativePath) {
|
|
856
|
+
async function resolvePinnedRemovePathInRoot(root, relativePath, denyMutations) {
|
|
911
857
|
return await resolvePinnedOperationPathInRoot(root, {
|
|
858
|
+
denyMutations,
|
|
859
|
+
protectDenyMutationAncestors: true,
|
|
912
860
|
relativePath,
|
|
913
861
|
policy: PATH_ALIAS_POLICIES.unlinkTarget,
|
|
914
862
|
});
|
|
@@ -920,6 +868,7 @@ async function resolvePinnedOperationPathInRoot(root, params) {
|
|
|
920
868
|
});
|
|
921
869
|
const relativeResolved = path.relative(resolved.rootReal, resolved.canonicalPath);
|
|
922
870
|
if ((relativeResolved === "" || relativeResolved === ".") && params.allowRoot === true) {
|
|
871
|
+
await assertMutationNotDenied(resolved.canonicalPath, params.denyMutations);
|
|
923
872
|
return { rootReal: resolved.rootReal, resolved: resolved.canonicalPath, relativePosix: "" };
|
|
924
873
|
}
|
|
925
874
|
const firstSegment = relativeResolved.split(path.sep)[0];
|
|
@@ -933,6 +882,9 @@ async function resolvePinnedOperationPathInRoot(root, params) {
|
|
|
933
882
|
if (!isPathInside(resolved.rootWithSep, resolved.canonicalPath)) {
|
|
934
883
|
throw new FsSafeError("outside-workspace", "file is outside workspace root");
|
|
935
884
|
}
|
|
885
|
+
await assertMutationNotDenied(resolved.canonicalPath, params.denyMutations, {
|
|
886
|
+
protectAncestors: params.protectDenyMutationAncestors,
|
|
887
|
+
});
|
|
936
888
|
return { rootReal: resolved.rootReal, resolved: resolved.canonicalPath, relativePosix };
|
|
937
889
|
}
|
|
938
890
|
async function resolvePinnedRootPathInRoot(root, params) {
|
|
@@ -1010,13 +962,21 @@ async function listPathFallback(root, relativePath, withFileTypes) {
|
|
|
1010
962
|
throw error;
|
|
1011
963
|
}
|
|
1012
964
|
}
|
|
965
|
+
async function assertMoveMutationAllowed(root, params) {
|
|
966
|
+
const source = await resolvePathInRoot(root, params.fromRelative);
|
|
967
|
+
await assertMutationNotDenied(source.resolved, params.denyMutations, { protectAncestors: true });
|
|
968
|
+
const target = await resolvePathInRoot(root, params.toRelative);
|
|
969
|
+
await assertMutationNotDenied(target.resolved, params.denyMutations, { protectAncestors: true });
|
|
970
|
+
}
|
|
1013
971
|
async function movePathFallback(root, params) {
|
|
1014
972
|
const source = await resolvePathInRoot(root, params.fromRelative);
|
|
973
|
+
await assertMutationNotDenied(source.resolved, params.denyMutations, { protectAncestors: true });
|
|
1015
974
|
await resolvePinnedRootPathInRoot(root, {
|
|
1016
975
|
relativePath: params.fromRelative,
|
|
1017
976
|
policy: PATH_ALIAS_POLICIES.strict,
|
|
1018
977
|
});
|
|
1019
978
|
const target = await resolvePathInRoot(root, params.toRelative);
|
|
979
|
+
await assertMutationNotDenied(target.resolved, params.denyMutations, { protectAncestors: true });
|
|
1020
980
|
await resolvePinnedRootPathInRoot(root, {
|
|
1021
981
|
relativePath: params.toRelative,
|
|
1022
982
|
policy: PATH_ALIAS_POLICIES.unlinkTarget,
|
|
@@ -1100,6 +1060,7 @@ async function writeFileFallback(root, params) {
|
|
|
1100
1060
|
relativePath: params.relativePath,
|
|
1101
1061
|
mkdir: params.mkdir,
|
|
1102
1062
|
mode: params.mode,
|
|
1063
|
+
denyMutations: params.denyMutations,
|
|
1103
1064
|
truncateExisting: false,
|
|
1104
1065
|
});
|
|
1105
1066
|
const destinationPath = target.realPath;
|
|
@@ -1145,6 +1106,7 @@ async function writeFileFallback(root, params) {
|
|
|
1145
1106
|
}
|
|
1146
1107
|
async function writeMissingFileFallback(root, params) {
|
|
1147
1108
|
const { rootReal, resolved } = await resolvePathInRoot(root, params.relativePath);
|
|
1109
|
+
await assertMutationNotDenied(resolved, params.denyMutations);
|
|
1148
1110
|
try {
|
|
1149
1111
|
await assertNoPathAliasEscape({
|
|
1150
1112
|
absolutePath: resolved,
|
|
@@ -1218,6 +1180,7 @@ async function copyFileFallback(root, params, source) {
|
|
|
1218
1180
|
relativePath: params.relativePath,
|
|
1219
1181
|
mkdir: params.mkdir,
|
|
1220
1182
|
mode: params.mode,
|
|
1183
|
+
denyMutations: params.denyMutations,
|
|
1221
1184
|
truncateExisting: false,
|
|
1222
1185
|
});
|
|
1223
1186
|
const destinationPath = target.realPath;
|
package/dist/root.d.ts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
export { DEFAULT_ROOT_MAX_BYTES, openLocalFileSafely, readLocalFileSafely, resolveOpenedFileRealPathForHandle, root, type HardlinkPolicy, type OpenResult, type ReadResult, type Root, type RootAppendOptions, type RootCopyOptions, type RootCreateJsonOptions, type RootCreateOptions, type RootDefaults, type RootOpenOptions, type RootOpenWritableOptions, type RootOptions, type RootReadOptions, type RootWriteJsonOptions, type RootWriteOptions, type SymlinkPolicy, type WritableOpenMode, type WritableOpenResult, } from "./root-impl.js";
|
|
1
|
+
export { DEFAULT_ROOT_MAX_BYTES, openLocalFileSafely, readLocalFileSafely, resolveOpenedFileRealPathForHandle, root, type DenyMutationPolicy, type HardlinkPolicy, type OpenResult, type ReadResult, type Root, type RootAppendOptions, type RootCopyOptions, type RootCreateJsonOptions, type RootCreateOptions, type RootDefaults, type RootMkdirOptions, type RootMoveOptions, type RootOpenOptions, type RootOpenWritableOptions, type RootOptions, type RootReadOptions, type RootRemoveOptions, type RootWriteJsonOptions, type RootWriteOptions, type SymlinkPolicy, type WritableOpenMode, type WritableOpenResult, } from "./root-impl.js";
|
|
2
2
|
//# sourceMappingURL=root.d.ts.map
|
package/dist/root.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"root.d.ts","sourceRoot":"","sources":["../src/root.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,sBAAsB,EACtB,mBAAmB,EACnB,mBAAmB,EACnB,kCAAkC,EAClC,IAAI,EACJ,KAAK,cAAc,EACnB,KAAK,UAAU,EACf,KAAK,UAAU,EACf,KAAK,IAAI,EACT,KAAK,iBAAiB,EACtB,KAAK,eAAe,EACpB,KAAK,qBAAqB,EAC1B,KAAK,iBAAiB,EACtB,KAAK,YAAY,EACjB,KAAK,eAAe,EACpB,KAAK,uBAAuB,EAC5B,KAAK,WAAW,EAChB,KAAK,eAAe,EACpB,KAAK,oBAAoB,EACzB,KAAK,gBAAgB,EACrB,KAAK,aAAa,EAClB,KAAK,gBAAgB,EACrB,KAAK,kBAAkB,GACxB,MAAM,gBAAgB,CAAC"}
|
|
1
|
+
{"version":3,"file":"root.d.ts","sourceRoot":"","sources":["../src/root.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,sBAAsB,EACtB,mBAAmB,EACnB,mBAAmB,EACnB,kCAAkC,EAClC,IAAI,EACJ,KAAK,kBAAkB,EACvB,KAAK,cAAc,EACnB,KAAK,UAAU,EACf,KAAK,UAAU,EACf,KAAK,IAAI,EACT,KAAK,iBAAiB,EACtB,KAAK,eAAe,EACpB,KAAK,qBAAqB,EAC1B,KAAK,iBAAiB,EACtB,KAAK,YAAY,EACjB,KAAK,gBAAgB,EACrB,KAAK,eAAe,EACpB,KAAK,eAAe,EACpB,KAAK,uBAAuB,EAC5B,KAAK,WAAW,EAChB,KAAK,eAAe,EACpB,KAAK,iBAAiB,EACtB,KAAK,oBAAoB,EACzB,KAAK,gBAAgB,EACrB,KAAK,aAAa,EAClB,KAAK,gBAAgB,EACrB,KAAK,kBAAkB,GACxB,MAAM,gBAAgB,CAAC"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"secure-file.d.ts","sourceRoot":"","sources":["../src/secure-file.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,SAAS,CAAC;
|
|
1
|
+
{"version":3,"file":"secure-file.d.ts","sourceRoot":"","sources":["../src/secure-file.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,SAAS,CAAC;AAWrC,OAAO,EAOL,KAAK,eAAe,EACpB,KAAK,sBAAsB,EAC5B,MAAM,kBAAkB,CAAC;AAK1B,MAAM,MAAM,qBAAqB,GAAG;IAClC,QAAQ,EAAE,MAAM,CAAC;IACjB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,KAAK,CAAC,EAAE,sBAAsB,CAAC;IAC/B,WAAW,CAAC,EAAE,2BAA2B,CAAC;IAC1C,MAAM,CAAC,EAAE,uBAAuB,CAAC;IACjC,EAAE,CAAC,EAAE,mBAAmB,CAAC;CAC1B,CAAC;AAEF,MAAM,MAAM,sBAAsB,GAAG;IACnC,WAAW,CAAC,EAAE,MAAM,EAAE,CAAC;IACvB,YAAY,CAAC,EAAE,OAAO,CAAC;IACvB,gBAAgB,CAAC,EAAE,OAAO,CAAC;CAC5B,CAAC;AAEF,MAAM,MAAM,2BAA2B,GAAG;IACxC,aAAa,CAAC,EAAE,OAAO,CAAC;IACxB,qBAAqB,CAAC,EAAE,OAAO,CAAC;CACjC,CAAC;AAEF,MAAM,MAAM,uBAAuB,GAAG,sBAAsB,CAAC;AAE7D,MAAM,MAAM,mBAAmB,GAAG;IAChC,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB,CAAC;AAEF,MAAM,MAAM,oBAAoB,GAAG;IACjC,MAAM,EAAE,MAAM,CAAC;IACf,QAAQ,EAAE,MAAM,CAAC;IACjB,IAAI,EAAE,KAAK,CAAC;IACZ,WAAW,CAAC,EAAE,eAAe,CAAC;CAC/B,CAAC;AAuKF,wBAAsB,cAAc,CAClC,OAAO,EAAE,qBAAqB,GAC7B,OAAO,CAAC,oBAAoB,CAAC,CAa/B"}
|
package/dist/secure-file.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { constants as fsConstants } from "node:fs";
|
|
2
2
|
import fs from "node:fs/promises";
|
|
3
3
|
import path from "node:path";
|
|
4
|
+
import { assertNoUnsafeDeviceReadPath } from "./device-path.js";
|
|
4
5
|
import { FsSafeError } from "./errors.js";
|
|
5
6
|
import { sameFileIdentity } from "./file-identity.js";
|
|
6
7
|
import { isWindowsDriveLetterPath, isWindowsNetworkPath } from "./local-file-access.js";
|
|
@@ -17,6 +18,7 @@ function label(options) {
|
|
|
17
18
|
return options.label ?? "Secure file";
|
|
18
19
|
}
|
|
19
20
|
async function openSecureHandle(options) {
|
|
21
|
+
assertNoUnsafeDeviceReadPath(options.filePath);
|
|
20
22
|
if (isWindowsNetworkPath(options.filePath, "win32") && !options.trust?.allowNetworkPath) {
|
|
21
23
|
throw new FsSafeError("invalid-path", `${label(options)} must be a local absolute path.`);
|
|
22
24
|
}
|
package/dist/walk.d.ts
CHANGED
|
@@ -16,11 +16,22 @@ export type WalkDirectoryOptions = {
|
|
|
16
16
|
include?: (entry: WalkDirectoryEntry) => boolean;
|
|
17
17
|
descend?: (entry: WalkDirectoryEntry) => boolean;
|
|
18
18
|
};
|
|
19
|
+
export type WalkDirectoryFailure = {
|
|
20
|
+
path: string;
|
|
21
|
+
relativePath: string;
|
|
22
|
+
depth: number;
|
|
23
|
+
error: unknown;
|
|
24
|
+
};
|
|
19
25
|
export type WalkDirectoryResult = {
|
|
20
26
|
entries: WalkDirectoryEntry[];
|
|
21
27
|
scannedEntryCount: number;
|
|
22
28
|
truncated: boolean;
|
|
29
|
+
failedDirs?: WalkDirectoryFailure[];
|
|
30
|
+
};
|
|
31
|
+
type WalkDirectoryResultWithFailures = WalkDirectoryResult & {
|
|
32
|
+
failedDirs: WalkDirectoryFailure[];
|
|
23
33
|
};
|
|
24
|
-
export declare function walkDirectorySync(rootDir: string, options?: WalkDirectoryOptions):
|
|
25
|
-
export declare function walkDirectory(rootDir: string, options?: WalkDirectoryOptions): Promise<
|
|
34
|
+
export declare function walkDirectorySync(rootDir: string, options?: WalkDirectoryOptions): WalkDirectoryResultWithFailures;
|
|
35
|
+
export declare function walkDirectory(rootDir: string, options?: WalkDirectoryOptions): Promise<WalkDirectoryResultWithFailures>;
|
|
36
|
+
export {};
|
|
26
37
|
//# sourceMappingURL=walk.d.ts.map
|
package/dist/walk.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"walk.d.ts","sourceRoot":"","sources":["../src/walk.ts"],"names":[],"mappings":"AAAA,OAAO,MAAM,MAAM,SAAS,CAAC;AAI7B,MAAM,MAAM,aAAa,GAAG,MAAM,GAAG,WAAW,GAAG,SAAS,GAAG,OAAO,CAAC;AACvE,MAAM,MAAM,iBAAiB,GAAG,MAAM,GAAG,QAAQ,GAAG,SAAS,CAAC;AAE9D,MAAM,MAAM,kBAAkB,GAAG;IAC/B,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,YAAY,EAAE,MAAM,CAAC;IACrB,KAAK,EAAE,MAAM,CAAC;IACd,IAAI,EAAE,aAAa,CAAC;IACpB,MAAM,EAAE,MAAM,CAAC,MAAM,CAAC;CACvB,CAAC;AAEF,MAAM,MAAM,oBAAoB,GAAG;IACjC,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,QAAQ,CAAC,EAAE,iBAAiB,CAAC;IAC7B,OAAO,CAAC,EAAE,CAAC,KAAK,EAAE,kBAAkB,KAAK,OAAO,CAAC;IACjD,OAAO,CAAC,EAAE,CAAC,KAAK,EAAE,kBAAkB,KAAK,OAAO,CAAC;CAClD,CAAC;AAEF,MAAM,MAAM,mBAAmB,GAAG;IAChC,OAAO,EAAE,kBAAkB,EAAE,CAAC;IAC9B,iBAAiB,EAAE,MAAM,CAAC;IAC1B,SAAS,EAAE,OAAO,CAAC;
|
|
1
|
+
{"version":3,"file":"walk.d.ts","sourceRoot":"","sources":["../src/walk.ts"],"names":[],"mappings":"AAAA,OAAO,MAAM,MAAM,SAAS,CAAC;AAI7B,MAAM,MAAM,aAAa,GAAG,MAAM,GAAG,WAAW,GAAG,SAAS,GAAG,OAAO,CAAC;AACvE,MAAM,MAAM,iBAAiB,GAAG,MAAM,GAAG,QAAQ,GAAG,SAAS,CAAC;AAE9D,MAAM,MAAM,kBAAkB,GAAG;IAC/B,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,YAAY,EAAE,MAAM,CAAC;IACrB,KAAK,EAAE,MAAM,CAAC;IACd,IAAI,EAAE,aAAa,CAAC;IACpB,MAAM,EAAE,MAAM,CAAC,MAAM,CAAC;CACvB,CAAC;AAEF,MAAM,MAAM,oBAAoB,GAAG;IACjC,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,QAAQ,CAAC,EAAE,iBAAiB,CAAC;IAC7B,OAAO,CAAC,EAAE,CAAC,KAAK,EAAE,kBAAkB,KAAK,OAAO,CAAC;IACjD,OAAO,CAAC,EAAE,CAAC,KAAK,EAAE,kBAAkB,KAAK,OAAO,CAAC;CAClD,CAAC;AAEF,MAAM,MAAM,oBAAoB,GAAG;IACjC,IAAI,EAAE,MAAM,CAAC;IACb,YAAY,EAAE,MAAM,CAAC;IACrB,KAAK,EAAE,MAAM,CAAC;IACd,KAAK,EAAE,OAAO,CAAC;CAChB,CAAC;AAEF,MAAM,MAAM,mBAAmB,GAAG;IAChC,OAAO,EAAE,kBAAkB,EAAE,CAAC;IAC9B,iBAAiB,EAAE,MAAM,CAAC;IAC1B,SAAS,EAAE,OAAO,CAAC;IAGnB,UAAU,CAAC,EAAE,oBAAoB,EAAE,CAAC;CACrC,CAAC;AAEF,KAAK,+BAA+B,GAAG,mBAAmB,GAAG;IAC3D,UAAU,EAAE,oBAAoB,EAAE,CAAC;CACpC,CAAC;AA8EF,wBAAgB,iBAAiB,CAC/B,OAAO,EAAE,MAAM,EACf,OAAO,GAAE,oBAAyB,GACjC,+BAA+B,CAwDjC;AAED,wBAAsB,aAAa,CACjC,OAAO,EAAE,MAAM,EACf,OAAO,GAAE,oBAAyB,GACjC,OAAO,CAAC,+BAA+B,CAAC,CAwD1C"}
|
package/dist/walk.js
CHANGED
|
@@ -25,6 +25,15 @@ function buildEntry(params) {
|
|
|
25
25
|
dirent: params.dirent,
|
|
26
26
|
};
|
|
27
27
|
}
|
|
28
|
+
function recordFailedDir(result, root, dir, depth, error) {
|
|
29
|
+
const relativePath = path.relative(root, dir);
|
|
30
|
+
result.failedDirs.push({
|
|
31
|
+
path: dir,
|
|
32
|
+
relativePath,
|
|
33
|
+
depth: relativePath === "" ? 0 : depth - 1,
|
|
34
|
+
error,
|
|
35
|
+
});
|
|
36
|
+
}
|
|
28
37
|
function resolveSyncKind(fullPath, dirent, symlinks) {
|
|
29
38
|
const kind = kindForDirent(dirent);
|
|
30
39
|
if (kind !== "symlink")
|
|
@@ -68,7 +77,12 @@ async function resolveAsyncKind(fullPath, dirent, symlinks) {
|
|
|
68
77
|
export function walkDirectorySync(rootDir, options = {}) {
|
|
69
78
|
const root = path.resolve(rootDir);
|
|
70
79
|
const symlinks = options.symlinks ?? "skip";
|
|
71
|
-
const result = {
|
|
80
|
+
const result = {
|
|
81
|
+
entries: [],
|
|
82
|
+
scannedEntryCount: 0,
|
|
83
|
+
truncated: false,
|
|
84
|
+
failedDirs: [],
|
|
85
|
+
};
|
|
72
86
|
const visitedDirs = new Set();
|
|
73
87
|
function visit(dir, depth) {
|
|
74
88
|
if (options.maxDepth !== undefined && depth > options.maxDepth)
|
|
@@ -77,7 +91,8 @@ export function walkDirectorySync(rootDir, options = {}) {
|
|
|
77
91
|
try {
|
|
78
92
|
realDir = fsSync.realpathSync(dir);
|
|
79
93
|
}
|
|
80
|
-
catch {
|
|
94
|
+
catch (error) {
|
|
95
|
+
recordFailedDir(result, root, dir, depth, error);
|
|
81
96
|
return;
|
|
82
97
|
}
|
|
83
98
|
if (visitedDirs.has(realDir))
|
|
@@ -87,7 +102,8 @@ export function walkDirectorySync(rootDir, options = {}) {
|
|
|
87
102
|
try {
|
|
88
103
|
entries = fsSync.readdirSync(dir, { withFileTypes: true });
|
|
89
104
|
}
|
|
90
|
-
catch {
|
|
105
|
+
catch (error) {
|
|
106
|
+
recordFailedDir(result, root, dir, depth, error);
|
|
91
107
|
return;
|
|
92
108
|
}
|
|
93
109
|
for (const dirent of entries) {
|
|
@@ -119,7 +135,12 @@ export function walkDirectorySync(rootDir, options = {}) {
|
|
|
119
135
|
export async function walkDirectory(rootDir, options = {}) {
|
|
120
136
|
const root = path.resolve(rootDir);
|
|
121
137
|
const symlinks = options.symlinks ?? "skip";
|
|
122
|
-
const result = {
|
|
138
|
+
const result = {
|
|
139
|
+
entries: [],
|
|
140
|
+
scannedEntryCount: 0,
|
|
141
|
+
truncated: false,
|
|
142
|
+
failedDirs: [],
|
|
143
|
+
};
|
|
123
144
|
const visitedDirs = new Set();
|
|
124
145
|
async function visit(dir, depth) {
|
|
125
146
|
if (options.maxDepth !== undefined && depth > options.maxDepth)
|
|
@@ -128,7 +149,8 @@ export async function walkDirectory(rootDir, options = {}) {
|
|
|
128
149
|
try {
|
|
129
150
|
realDir = await fs.realpath(dir);
|
|
130
151
|
}
|
|
131
|
-
catch {
|
|
152
|
+
catch (error) {
|
|
153
|
+
recordFailedDir(result, root, dir, depth, error);
|
|
132
154
|
return;
|
|
133
155
|
}
|
|
134
156
|
if (visitedDirs.has(realDir))
|
|
@@ -138,7 +160,8 @@ export async function walkDirectory(rootDir, options = {}) {
|
|
|
138
160
|
try {
|
|
139
161
|
entries = await fs.readdir(dir, { withFileTypes: true });
|
|
140
162
|
}
|
|
141
|
-
catch {
|
|
163
|
+
catch (error) {
|
|
164
|
+
recordFailedDir(result, root, dir, depth, error);
|
|
142
165
|
return;
|
|
143
166
|
}
|
|
144
167
|
for (const dirent of entries) {
|
package/docs/contributing.md
CHANGED
|
@@ -10,7 +10,7 @@ cd fs-safe
|
|
|
10
10
|
pnpm install
|
|
11
11
|
```
|
|
12
12
|
|
|
13
|
-
Node
|
|
13
|
+
Node 22 or newer. The dev toolchain uses pnpm; `npm install` works too but pnpm is what the lockfile is keyed against.
|
|
14
14
|
|
|
15
15
|
## Build
|
|
16
16
|
|
package/docs/errors.md
CHANGED
|
@@ -30,6 +30,8 @@ class FsSafeError extends Error {
|
|
|
30
30
|
```ts
|
|
31
31
|
type FsSafeErrorCode =
|
|
32
32
|
| "already-exists"
|
|
33
|
+
| "denied-path"
|
|
34
|
+
| "device-path"
|
|
33
35
|
| "hardlink"
|
|
34
36
|
| "helper-failed"
|
|
35
37
|
| "helper-unavailable"
|
|
@@ -55,6 +57,8 @@ type FsSafeErrorCode =
|
|
|
55
57
|
| Code | When it fires | Common causes |
|
|
56
58
|
|---|---|---|
|
|
57
59
|
| `already-exists` | `create()`, `createJson()`, `move({ overwrite: false })`. | Target file or directory already at the destination. |
|
|
60
|
+
| `denied-path` | A root mutation matched `denyMutations.paths` or `denyMutations.prefixes`. | Caller configured application-sensitive paths that must not be written, removed, moved, or created. |
|
|
61
|
+
| `device-path` | A read/open target is a known unsafe device or process-fd path. | `/dev/zero`, `/dev/random`, `/dev/stdin`, `/dev/fd/*`, `/proc/*/fd/*`, or a Windows reserved device name. |
|
|
58
62
|
| `hardlink` | Read or copy with `hardlinks: "reject"` saw `nlink > 1`. | File is hardlinked — possibly an alias of an out-of-tree inode. |
|
|
59
63
|
| `helper-failed` | Internal POSIX helper failed after startup. | Inspect `cause`; retrying may be unsafe if the operation may have partially completed. |
|
|
60
64
|
| `helper-unavailable` | Persistent Python helper was disabled or could not be spawned. | `FS_SAFE_PYTHON_MODE=off`, Python missing in PATH, restricted sandbox. `auto` falls back where possible; `require` fails closed. |
|
|
@@ -93,6 +97,7 @@ try {
|
|
|
93
97
|
case "not-found":
|
|
94
98
|
return reply(404, "missing");
|
|
95
99
|
case "symlink":
|
|
100
|
+
case "device-path":
|
|
96
101
|
case "hardlink":
|
|
97
102
|
case "path-mismatch":
|
|
98
103
|
case "path-alias":
|