@openclaw/fs-safe 0.2.6 → 0.3.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 +17 -0
- package/README.md +21 -7
- package/dist/archive-deadline.d.ts +9 -0
- package/dist/archive-deadline.d.ts.map +1 -0
- package/dist/archive-deadline.js +67 -0
- package/dist/archive-file-io.d.ts +9 -0
- package/dist/archive-file-io.d.ts.map +1 -0
- package/dist/archive-file-io.js +11 -0
- package/dist/archive-staging.d.ts.map +1 -1
- package/dist/archive-staging.js +43 -3
- package/dist/archive.d.ts.map +1 -1
- package/dist/archive.js +213 -105
- 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/errors.d.ts +1 -1
- package/dist/errors.d.ts.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/json-durable-queue.d.ts.map +1 -1
- package/dist/json-durable-queue.js +5 -0
- package/dist/json.d.ts.map +1 -1
- package/dist/json.js +51 -4
- 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/output.d.ts.map +1 -1
- package/dist/output.js +1 -0
- package/dist/pinned-write.js +3 -9
- 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 +23 -3
- package/dist/root-impl.d.ts +18 -15
- package/dist/root-impl.d.ts.map +1 -1
- package/dist/root-impl.js +60 -100
- package/dist/root-paths.d.ts.map +1 -1
- package/dist/root-paths.js +16 -5
- package/dist/root.d.ts +1 -1
- package/dist/root.d.ts.map +1 -1
- package/dist/secret-file.d.ts +1 -0
- package/dist/secret-file.d.ts.map +1 -1
- package/dist/secret-file.js +23 -3
- package/dist/sidecar-lock.d.ts.map +1 -1
- package/dist/sidecar-lock.js +9 -3
- package/dist/symlink-parents.js +2 -2
- package/docs/errors.md +2 -0
- package/docs/python-helper.md +3 -3
- package/docs/root.md +19 -9
- package/docs/secret-file.md +3 -2
- package/docs/security-model.md +4 -0
- package/docs/timing.md +1 -1
- package/docs/types.md +21 -9
- package/docs/writing.md +19 -1
- package/package.json +1 -1
package/dist/root-impl.js
CHANGED
|
@@ -9,11 +9,14 @@ import { FsSafeError } from "./errors.js";
|
|
|
9
9
|
import { sameFileIdentity } from "./file-identity.js";
|
|
10
10
|
import { mkdirPathComponentsWithGuards } from "./guarded-mkdir.js";
|
|
11
11
|
import { withAsyncDirectoryGuards } from "./guarded-mutation.js";
|
|
12
|
+
import { assertMutationNotDenied, mergeDenyMutationPolicies, } from "./deny-mutations.js";
|
|
13
|
+
import { resolveOpenedFileRealPathForHandle } from "./opened-realpath.js";
|
|
12
14
|
import { isPinnedPathHelperSpawnError, runPinnedPathHelper } from "./pinned-path.js";
|
|
13
15
|
import { runPinnedCopyHelper, runPinnedWriteHelper } from "./pinned-write.js";
|
|
14
16
|
import { canFallbackFromPythonError, getFsSafePythonConfig } from "./pinned-python-config.js";
|
|
15
17
|
import { assertNoPathAliasEscape, PATH_ALIAS_POLICIES } from "./path-policy.js";
|
|
16
18
|
import { assertNoNulPathInput, hasNodeErrorCode, isNotFoundPathError, isPathInside, isSymlinkOpenError, } from "./path.js";
|
|
19
|
+
import { readOpenedFileSafely } from "./read-opened-file.js";
|
|
17
20
|
import { helperReaddir, helperStat, runPinnedHelper, } from "./pinned-helper.js";
|
|
18
21
|
import { pathStatFromStats } from "./path-stat.js";
|
|
19
22
|
import { resolveRootPath } from "./root-path.js";
|
|
@@ -23,6 +26,7 @@ import { getFsSafeTestHooks } from "./test-hooks.js";
|
|
|
23
26
|
import { stringifyJsonDocument } from "./json-stringify.js";
|
|
24
27
|
import { registerTempPathForExit } from "./temp-cleanup.js";
|
|
25
28
|
import { serializePathWrite } from "./write-queue.js";
|
|
29
|
+
export { resolveOpenedFileRealPathForHandle } from "./opened-realpath.js";
|
|
26
30
|
function logWarn(message) {
|
|
27
31
|
if (process.env.FS_SAFE_DEBUG_WARNINGS === "1") {
|
|
28
32
|
console.warn(message);
|
|
@@ -216,6 +220,7 @@ class RootHandle {
|
|
|
216
220
|
mkdir: this.defaults.mkdir,
|
|
217
221
|
mode: this.defaults.mode,
|
|
218
222
|
...options,
|
|
223
|
+
denyMutations: mergeDenyMutationPolicies(this.defaults.denyMutations, options.denyMutations),
|
|
219
224
|
append: writeMode === "append",
|
|
220
225
|
truncateExisting: writeMode === "replace",
|
|
221
226
|
});
|
|
@@ -227,18 +232,29 @@ class RootHandle {
|
|
|
227
232
|
mkdir: this.defaults.mkdir,
|
|
228
233
|
mode: this.defaults.mode,
|
|
229
234
|
...options,
|
|
235
|
+
denyMutations: mergeDenyMutationPolicies(this.defaults.denyMutations, options.denyMutations),
|
|
230
236
|
});
|
|
231
237
|
}
|
|
232
|
-
async remove(relativePath) {
|
|
238
|
+
async remove(relativePath, options = {}) {
|
|
233
239
|
assertValidRootRelativePath(relativePath);
|
|
234
|
-
await removePathInRoot(this.context,
|
|
240
|
+
await removePathInRoot(this.context, {
|
|
241
|
+
relativePath,
|
|
242
|
+
denyMutations: mergeDenyMutationPolicies(this.defaults.denyMutations, options.denyMutations),
|
|
243
|
+
});
|
|
235
244
|
}
|
|
236
|
-
async mkdir(relativePath) {
|
|
245
|
+
async mkdir(relativePath, options = {}) {
|
|
237
246
|
assertValidRootRelativePath(relativePath);
|
|
238
|
-
await mkdirPathInRoot(this.context, {
|
|
247
|
+
await mkdirPathInRoot(this.context, {
|
|
248
|
+
relativePath,
|
|
249
|
+
denyMutations: mergeDenyMutationPolicies(this.defaults.denyMutations, options.denyMutations),
|
|
250
|
+
});
|
|
239
251
|
}
|
|
240
|
-
async ensureRoot() {
|
|
241
|
-
await mkdirPathInRoot(this.context, {
|
|
252
|
+
async ensureRoot(options = {}) {
|
|
253
|
+
await mkdirPathInRoot(this.context, {
|
|
254
|
+
relativePath: "",
|
|
255
|
+
allowRoot: true,
|
|
256
|
+
denyMutations: mergeDenyMutationPolicies(this.defaults.denyMutations, options.denyMutations),
|
|
257
|
+
});
|
|
242
258
|
}
|
|
243
259
|
async write(relativePath, data, options = {}) {
|
|
244
260
|
await writeFileInRoot(this.context, {
|
|
@@ -247,6 +263,7 @@ class RootHandle {
|
|
|
247
263
|
mkdir: this.defaults.mkdir,
|
|
248
264
|
mode: this.defaults.mode,
|
|
249
265
|
...options,
|
|
266
|
+
denyMutations: mergeDenyMutationPolicies(this.defaults.denyMutations, options.denyMutations),
|
|
250
267
|
});
|
|
251
268
|
}
|
|
252
269
|
async create(relativePath, data, options = {}) {
|
|
@@ -256,6 +273,7 @@ class RootHandle {
|
|
|
256
273
|
mkdir: this.defaults.mkdir,
|
|
257
274
|
mode: this.defaults.mode,
|
|
258
275
|
...options,
|
|
276
|
+
denyMutations: mergeDenyMutationPolicies(this.defaults.denyMutations, options.denyMutations),
|
|
259
277
|
overwrite: false,
|
|
260
278
|
});
|
|
261
279
|
}
|
|
@@ -278,6 +296,7 @@ class RootHandle {
|
|
|
278
296
|
mkdir: this.defaults.mkdir,
|
|
279
297
|
mode: this.defaults.mode,
|
|
280
298
|
...options,
|
|
299
|
+
denyMutations: mergeDenyMutationPolicies(this.defaults.denyMutations, options.denyMutations),
|
|
281
300
|
});
|
|
282
301
|
}
|
|
283
302
|
async exists(relativePath) {
|
|
@@ -321,6 +340,12 @@ class RootHandle {
|
|
|
321
340
|
async move(fromRelative, toRelative, options = {}) {
|
|
322
341
|
assertValidRootRelativePath(fromRelative);
|
|
323
342
|
assertValidRootRelativePath(toRelative);
|
|
343
|
+
const denyMutations = mergeDenyMutationPolicies(this.defaults.denyMutations, options.denyMutations);
|
|
344
|
+
await assertMoveMutationAllowed(this.context, {
|
|
345
|
+
fromRelative,
|
|
346
|
+
toRelative,
|
|
347
|
+
denyMutations,
|
|
348
|
+
});
|
|
324
349
|
try {
|
|
325
350
|
await runPinnedHelper("rename", this.rootReal, {
|
|
326
351
|
from: fromRelative,
|
|
@@ -332,6 +357,7 @@ class RootHandle {
|
|
|
332
357
|
if (canFallbackFromPythonError(error)) {
|
|
333
358
|
await movePathFallback(this.context, {
|
|
334
359
|
fromRelative,
|
|
360
|
+
denyMutations,
|
|
335
361
|
overwrite: options.overwrite ?? false,
|
|
336
362
|
toRelative,
|
|
337
363
|
});
|
|
@@ -413,20 +439,6 @@ export async function openLocalFileSafely(params) {
|
|
|
413
439
|
assertNoNulPathInput(params.filePath, "file path contains a NUL byte");
|
|
414
440
|
return await openVerifiedLocalFile(params.filePath);
|
|
415
441
|
}
|
|
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
442
|
function emitWriteBoundaryWarning(reason) {
|
|
431
443
|
logWarn(`security: fs-safe write boundary warning (${reason})`);
|
|
432
444
|
}
|
|
@@ -467,82 +479,9 @@ async function verifyAtomicWriteResult(params) {
|
|
|
467
479
|
await opened.handle.close().catch(() => { });
|
|
468
480
|
}
|
|
469
481
|
}
|
|
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
482
|
async function openWritableFileInRoot(root, params) {
|
|
545
483
|
const { rootReal, rootWithSep, resolved } = await resolvePathInRoot(root, params.relativePath);
|
|
484
|
+
await assertMutationNotDenied(resolved, params.denyMutations);
|
|
546
485
|
try {
|
|
547
486
|
await assertNoPathAliasEscape({
|
|
548
487
|
absolutePath: resolved,
|
|
@@ -669,6 +608,7 @@ async function appendFileInRoot(root, params) {
|
|
|
669
608
|
relativePath: params.relativePath,
|
|
670
609
|
mkdir: params.mkdir,
|
|
671
610
|
mode: params.mode,
|
|
611
|
+
denyMutations: params.denyMutations,
|
|
672
612
|
truncateExisting: false,
|
|
673
613
|
append: true,
|
|
674
614
|
});
|
|
@@ -696,8 +636,8 @@ async function appendFileInRoot(root, params) {
|
|
|
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,7 +685,7 @@ 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
690
|
let identity;
|
|
751
691
|
try {
|
|
@@ -801,7 +741,7 @@ async function copyFileInRoot(root, params) {
|
|
|
801
741
|
});
|
|
802
742
|
return;
|
|
803
743
|
}
|
|
804
|
-
const pinned = await resolvePinnedWriteTargetInRoot(root, params.relativePath, params.mode);
|
|
744
|
+
const pinned = await resolvePinnedWriteTargetInRoot(root, params.relativePath, params.mode, params.denyMutations);
|
|
805
745
|
await serializePathWrite(pinned.targetPath, async () => {
|
|
806
746
|
let identity;
|
|
807
747
|
try {
|
|
@@ -845,8 +785,9 @@ async function copyFileInRoot(root, params) {
|
|
|
845
785
|
await source.handle.close().catch(() => { });
|
|
846
786
|
}
|
|
847
787
|
}
|
|
848
|
-
async function resolvePinnedWriteTargetInRoot(root, relativePath, requestedMode) {
|
|
788
|
+
async function resolvePinnedWriteTargetInRoot(root, relativePath, requestedMode, denyMutations) {
|
|
849
789
|
const { rootReal, rootWithSep, resolved } = await resolvePathInRoot(root, relativePath);
|
|
790
|
+
await assertMutationNotDenied(resolved, denyMutations);
|
|
850
791
|
try {
|
|
851
792
|
await assertNoPathAliasEscape({
|
|
852
793
|
absolutePath: resolved,
|
|
@@ -903,12 +844,16 @@ async function resolvePinnedWriteTargetInRoot(root, relativePath, requestedMode)
|
|
|
903
844
|
async function resolvePinnedPathInRoot(root, params) {
|
|
904
845
|
return await resolvePinnedOperationPathInRoot(root, {
|
|
905
846
|
allowRoot: params.allowRoot,
|
|
847
|
+
denyMutations: params.denyMutations,
|
|
848
|
+
protectDenyMutationAncestors: false,
|
|
906
849
|
relativePath: params.relativePath,
|
|
907
850
|
policy: PATH_ALIAS_POLICIES.strict,
|
|
908
851
|
});
|
|
909
852
|
}
|
|
910
|
-
async function resolvePinnedRemovePathInRoot(root, relativePath) {
|
|
853
|
+
async function resolvePinnedRemovePathInRoot(root, relativePath, denyMutations) {
|
|
911
854
|
return await resolvePinnedOperationPathInRoot(root, {
|
|
855
|
+
denyMutations,
|
|
856
|
+
protectDenyMutationAncestors: true,
|
|
912
857
|
relativePath,
|
|
913
858
|
policy: PATH_ALIAS_POLICIES.unlinkTarget,
|
|
914
859
|
});
|
|
@@ -920,6 +865,7 @@ async function resolvePinnedOperationPathInRoot(root, params) {
|
|
|
920
865
|
});
|
|
921
866
|
const relativeResolved = path.relative(resolved.rootReal, resolved.canonicalPath);
|
|
922
867
|
if ((relativeResolved === "" || relativeResolved === ".") && params.allowRoot === true) {
|
|
868
|
+
await assertMutationNotDenied(resolved.canonicalPath, params.denyMutations);
|
|
923
869
|
return { rootReal: resolved.rootReal, resolved: resolved.canonicalPath, relativePosix: "" };
|
|
924
870
|
}
|
|
925
871
|
const firstSegment = relativeResolved.split(path.sep)[0];
|
|
@@ -933,6 +879,9 @@ async function resolvePinnedOperationPathInRoot(root, params) {
|
|
|
933
879
|
if (!isPathInside(resolved.rootWithSep, resolved.canonicalPath)) {
|
|
934
880
|
throw new FsSafeError("outside-workspace", "file is outside workspace root");
|
|
935
881
|
}
|
|
882
|
+
await assertMutationNotDenied(resolved.canonicalPath, params.denyMutations, {
|
|
883
|
+
protectAncestors: params.protectDenyMutationAncestors,
|
|
884
|
+
});
|
|
936
885
|
return { rootReal: resolved.rootReal, resolved: resolved.canonicalPath, relativePosix };
|
|
937
886
|
}
|
|
938
887
|
async function resolvePinnedRootPathInRoot(root, params) {
|
|
@@ -1010,13 +959,21 @@ async function listPathFallback(root, relativePath, withFileTypes) {
|
|
|
1010
959
|
throw error;
|
|
1011
960
|
}
|
|
1012
961
|
}
|
|
962
|
+
async function assertMoveMutationAllowed(root, params) {
|
|
963
|
+
const source = await resolvePathInRoot(root, params.fromRelative);
|
|
964
|
+
await assertMutationNotDenied(source.resolved, params.denyMutations, { protectAncestors: true });
|
|
965
|
+
const target = await resolvePathInRoot(root, params.toRelative);
|
|
966
|
+
await assertMutationNotDenied(target.resolved, params.denyMutations, { protectAncestors: true });
|
|
967
|
+
}
|
|
1013
968
|
async function movePathFallback(root, params) {
|
|
1014
969
|
const source = await resolvePathInRoot(root, params.fromRelative);
|
|
970
|
+
await assertMutationNotDenied(source.resolved, params.denyMutations, { protectAncestors: true });
|
|
1015
971
|
await resolvePinnedRootPathInRoot(root, {
|
|
1016
972
|
relativePath: params.fromRelative,
|
|
1017
973
|
policy: PATH_ALIAS_POLICIES.strict,
|
|
1018
974
|
});
|
|
1019
975
|
const target = await resolvePathInRoot(root, params.toRelative);
|
|
976
|
+
await assertMutationNotDenied(target.resolved, params.denyMutations, { protectAncestors: true });
|
|
1020
977
|
await resolvePinnedRootPathInRoot(root, {
|
|
1021
978
|
relativePath: params.toRelative,
|
|
1022
979
|
policy: PATH_ALIAS_POLICIES.unlinkTarget,
|
|
@@ -1100,6 +1057,7 @@ async function writeFileFallback(root, params) {
|
|
|
1100
1057
|
relativePath: params.relativePath,
|
|
1101
1058
|
mkdir: params.mkdir,
|
|
1102
1059
|
mode: params.mode,
|
|
1060
|
+
denyMutations: params.denyMutations,
|
|
1103
1061
|
truncateExisting: false,
|
|
1104
1062
|
});
|
|
1105
1063
|
const destinationPath = target.realPath;
|
|
@@ -1145,6 +1103,7 @@ async function writeFileFallback(root, params) {
|
|
|
1145
1103
|
}
|
|
1146
1104
|
async function writeMissingFileFallback(root, params) {
|
|
1147
1105
|
const { rootReal, resolved } = await resolvePathInRoot(root, params.relativePath);
|
|
1106
|
+
await assertMutationNotDenied(resolved, params.denyMutations);
|
|
1148
1107
|
try {
|
|
1149
1108
|
await assertNoPathAliasEscape({
|
|
1150
1109
|
absolutePath: resolved,
|
|
@@ -1218,6 +1177,7 @@ async function copyFileFallback(root, params, source) {
|
|
|
1218
1177
|
relativePath: params.relativePath,
|
|
1219
1178
|
mkdir: params.mkdir,
|
|
1220
1179
|
mode: params.mode,
|
|
1180
|
+
denyMutations: params.denyMutations,
|
|
1221
1181
|
truncateExisting: false,
|
|
1222
1182
|
});
|
|
1223
1183
|
const destinationPath = target.realPath;
|
package/dist/root-paths.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"root-paths.d.ts","sourceRoot":"","sources":["../src/root-paths.ts"],"names":[],"mappings":"AAMA,KAAK,iBAAiB,GAAG;IAAE,EAAE,EAAE,KAAK,CAAC;IAAC,KAAK,EAAE,MAAM,CAAA;CAAE,CAAC;AACtD,KAAK,4BAA4B,GAAG;IAClC,OAAO,EAAE,MAAM,CAAC;IAChB,cAAc,EAAE,MAAM,EAAE,CAAC;IACzB,UAAU,EAAE,MAAM,CAAC;CACpB,CAAC;AACF,KAAK,4BAA4B,GAAG;IAAE,EAAE,EAAE,IAAI,CAAC;IAAC,KAAK,EAAE,MAAM,EAAE,CAAA;CAAE,GAAG,iBAAiB,CAAC;AACtF,MAAM,MAAM,uBAAuB,GAAG;IACpC,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB,CAAC;AACF,MAAM,MAAM,gBAAgB,GAAG;IAC7B,KAAK,EAAE,MAAM,CAAC;CACf,CAAC;AACF,MAAM,MAAM,SAAS,GAAG;IACtB,OAAO,EAAE,MAAM,CAAC;IAChB,KAAK,EAAE,MAAM,CAAC;IACd,OAAO,CACL,aAAa,EAAE,MAAM,EACrB,OAAO,CAAC,EAAE,uBAAuB,GAChC;QAAE,EAAE,EAAE,IAAI,CAAC;QAAC,IAAI,EAAE,MAAM,CAAA;KAAE,GAAG;QAAE,EAAE,EAAE,KAAK,CAAC;QAAC,KAAK,EAAE,MAAM,CAAA;KAAE,CAAC;IAC7D,UAAU,CAAC,cAAc,EAAE,MAAM,EAAE,GAAG,4BAA4B,CAAC;IACnE,QAAQ,CAAC,cAAc,EAAE,MAAM,EAAE,GAAG,OAAO,CAAC,4BAA4B,CAAC,CAAC;IAC1E,KAAK,CAAC,cAAc,EAAE,MAAM,EAAE,GAAG,OAAO,CAAC,4BAA4B,CAAC,CAAC;IACvE,QAAQ,CACN,aAAa,EAAE,MAAM,EACrB,OAAO,CAAC,EAAE,uBAAuB,GAChC,OAAO,CAAC;QAAE,EAAE,EAAE,IAAI,CAAC;QAAC,IAAI,EAAE,MAAM,CAAA;KAAE,GAAG;QAAE,EAAE,EAAE,KAAK,CAAC;QAAC,KAAK,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;IACtE,SAAS,CACP,aAAa,EAAE,MAAM,EACrB,OAAO,CAAC,EAAE,uBAAuB,GAAG;QAAE,IAAI,CAAC,EAAE,MAAM,CAAA;KAAE,GACpD,OAAO,CAAC;QAAE,EAAE,EAAE,IAAI,CAAC;QAAC,IAAI,EAAE,MAAM,CAAA;KAAE,GAAG;QAAE,EAAE,EAAE,KAAK,CAAC;QAAC,KAAK,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;CACvE,CAAC;
|
|
1
|
+
{"version":3,"file":"root-paths.d.ts","sourceRoot":"","sources":["../src/root-paths.ts"],"names":[],"mappings":"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"}
|
package/dist/root-paths.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import fs from "node:fs/promises";
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
import { FsSafeError } from "./errors.js";
|
|
4
|
-
import { isNotFoundPathError, isPathInside } from "./path.js";
|
|
4
|
+
import { isNotFoundPathError, isPathInside, isPathRelativeEscape } from "./path.js";
|
|
5
5
|
import { root as openRoot } from "./root.js";
|
|
6
6
|
function invalidPath(scopeLabel) {
|
|
7
7
|
return {
|
|
@@ -9,6 +9,10 @@ function invalidPath(scopeLabel) {
|
|
|
9
9
|
error: `Invalid path: must stay within ${scopeLabel}`,
|
|
10
10
|
};
|
|
11
11
|
}
|
|
12
|
+
function pathStaysWithinRoot(rootDir, candidatePath) {
|
|
13
|
+
const relative = path.relative(rootDir, candidatePath);
|
|
14
|
+
return Boolean(relative) && !isPathRelativeEscape(relative);
|
|
15
|
+
}
|
|
12
16
|
async function resolveRealPathIfExists(targetPath) {
|
|
13
17
|
try {
|
|
14
18
|
return await fs.realpath(targetPath);
|
|
@@ -58,11 +62,14 @@ export function resolvePathWithinRoot(params) {
|
|
|
58
62
|
if (!params.defaultFileName) {
|
|
59
63
|
return { ok: false, error: "path is required" };
|
|
60
64
|
}
|
|
61
|
-
|
|
65
|
+
const defaultPath = path.resolve(root, params.defaultFileName);
|
|
66
|
+
if (!pathStaysWithinRoot(root, defaultPath)) {
|
|
67
|
+
return { ok: false, error: `Invalid path: must stay within ${params.scopeLabel}` };
|
|
68
|
+
}
|
|
69
|
+
return { ok: true, path: defaultPath };
|
|
62
70
|
}
|
|
63
71
|
const resolved = path.resolve(root, raw);
|
|
64
|
-
|
|
65
|
-
if (!rel || rel === ".." || rel.startsWith(`..${path.sep}`) || path.isAbsolute(rel)) {
|
|
72
|
+
if (!pathStaysWithinRoot(root, resolved)) {
|
|
66
73
|
return { ok: false, error: `Invalid path: must stay within ${params.scopeLabel}` };
|
|
67
74
|
}
|
|
68
75
|
return { ok: true, path: resolved };
|
|
@@ -118,7 +125,7 @@ async function resolveNearestExistingPath(targetPath) {
|
|
|
118
125
|
}
|
|
119
126
|
async function assertNoSymlinkSegments(params) {
|
|
120
127
|
const relative = path.relative(params.rootDir, params.targetPath);
|
|
121
|
-
if (
|
|
128
|
+
if (isPathRelativeEscape(relative)) {
|
|
122
129
|
throw new Error(`Invalid path: must stay within ${params.scopeLabel}`);
|
|
123
130
|
}
|
|
124
131
|
let current = params.rootDir;
|
|
@@ -195,6 +202,10 @@ export async function ensureDirectoryWithinRoot(params) {
|
|
|
195
202
|
}
|
|
196
203
|
}
|
|
197
204
|
}
|
|
205
|
+
const currentReal = await fs.realpath(current);
|
|
206
|
+
if (!isPathInside(rootReal, currentReal)) {
|
|
207
|
+
return invalidPath(params.scopeLabel);
|
|
208
|
+
}
|
|
198
209
|
}
|
|
199
210
|
const targetReal = await fs.realpath(targetPath);
|
|
200
211
|
if (!isPathInside(rootReal, targetReal)) {
|
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"}
|
package/dist/secret-file.d.ts
CHANGED
|
@@ -4,6 +4,7 @@ export declare const PRIVATE_SECRET_FILE_MODE = 384;
|
|
|
4
4
|
export type SecretFileReadOptions = {
|
|
5
5
|
maxBytes?: number;
|
|
6
6
|
rejectSymlink?: boolean;
|
|
7
|
+
rejectHardlinks?: boolean;
|
|
7
8
|
};
|
|
8
9
|
export declare function readSecretFileSync(filePath: string, label: string, options?: SecretFileReadOptions): string;
|
|
9
10
|
export declare function tryReadSecretFileSync(filePath: string | undefined, label: string, options?: SecretFileReadOptions): string | undefined;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"secret-file.d.ts","sourceRoot":"","sources":["../src/secret-file.ts"],"names":[],"mappings":"AAUA,eAAO,MAAM,6BAA6B,QAAY,CAAC;AACvD,eAAO,MAAM,uBAAuB,MAAQ,CAAC;AAC7C,eAAO,MAAM,wBAAwB,MAAQ,CAAC;AAE9C,MAAM,MAAM,qBAAqB,GAAG;IAClC,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,aAAa,CAAC,EAAE,OAAO,CAAC;
|
|
1
|
+
{"version":3,"file":"secret-file.d.ts","sourceRoot":"","sources":["../src/secret-file.ts"],"names":[],"mappings":"AAUA,eAAO,MAAM,6BAA6B,QAAY,CAAC;AACvD,eAAO,MAAM,uBAAuB,MAAQ,CAAC;AAC7C,eAAO,MAAM,wBAAwB,MAAQ,CAAC;AAE9C,MAAM,MAAM,qBAAqB,GAAG;IAClC,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,aAAa,CAAC,EAAE,OAAO,CAAC;IACxB,eAAe,CAAC,EAAE,OAAO,CAAC;CAC3B,CAAC;AAkIF,wBAAgB,kBAAkB,CAChC,QAAQ,EAAE,MAAM,EAChB,KAAK,EAAE,MAAM,EACb,OAAO,GAAE,qBAA0B,GAClC,MAAM,CAQR;AAED,wBAAgB,qBAAqB,CACnC,QAAQ,EAAE,MAAM,GAAG,SAAS,EAC5B,KAAK,EAAE,MAAM,EACb,OAAO,GAAE,qBAA0B,GAClC,MAAM,GAAG,SAAS,CAcpB;AAoID,wBAAsB,qBAAqB,CAAC,MAAM,EAAE;IAClD,OAAO,EAAE,MAAM,CAAC;IAChB,QAAQ,EAAE,MAAM,CAAC;IACjB,OAAO,EAAE,MAAM,GAAG,UAAU,CAAC;IAC7B,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB,GAAG,OAAO,CAAC,IAAI,CAAC,CA+ChB"}
|
package/dist/secret-file.js
CHANGED
|
@@ -13,6 +13,10 @@ export const PRIVATE_SECRET_FILE_MODE = 0o600;
|
|
|
13
13
|
function normalizeSecretReadError(error) {
|
|
14
14
|
return error instanceof Error ? error : new Error(String(error));
|
|
15
15
|
}
|
|
16
|
+
function secretPathErrorCode(error) {
|
|
17
|
+
const code = error.code;
|
|
18
|
+
return code === "ENOENT" || code === "ENOTDIR" ? "not-found" : "invalid-path";
|
|
19
|
+
}
|
|
16
20
|
function resolveUserPath(input) {
|
|
17
21
|
return resolveHomeRelativePath(input);
|
|
18
22
|
}
|
|
@@ -31,7 +35,7 @@ function readSecretFileOutcomeSync(filePath, label, options = {}) {
|
|
|
31
35
|
const normalized = normalizeSecretReadError(error);
|
|
32
36
|
return {
|
|
33
37
|
ok: false,
|
|
34
|
-
code: error
|
|
38
|
+
code: secretPathErrorCode(error),
|
|
35
39
|
error: normalized,
|
|
36
40
|
message: `Failed to inspect ${label} file at ${resolvedPath}: ${String(normalized)}`,
|
|
37
41
|
};
|
|
@@ -45,7 +49,7 @@ function readSecretFileOutcomeSync(filePath, label, options = {}) {
|
|
|
45
49
|
const normalized = normalizeSecretReadError(error);
|
|
46
50
|
return {
|
|
47
51
|
ok: false,
|
|
48
|
-
code: error
|
|
52
|
+
code: secretPathErrorCode(error),
|
|
49
53
|
error: normalized,
|
|
50
54
|
message: `Failed to inspect ${label} file at ${resolvedPath}: ${String(normalized)}`,
|
|
51
55
|
};
|
|
@@ -66,6 +70,13 @@ function readSecretFileOutcomeSync(filePath, label, options = {}) {
|
|
|
66
70
|
message: `${label} file at ${resolvedPath} must be a regular file.`,
|
|
67
71
|
};
|
|
68
72
|
}
|
|
73
|
+
if (options.rejectHardlinks !== false && previewStat.nlink > 1) {
|
|
74
|
+
return {
|
|
75
|
+
ok: false,
|
|
76
|
+
code: "hardlink",
|
|
77
|
+
message: `${label} file at ${resolvedPath} must not be hardlinked.`,
|
|
78
|
+
};
|
|
79
|
+
}
|
|
69
80
|
if (previewStat.size > maxBytes) {
|
|
70
81
|
return {
|
|
71
82
|
ok: false,
|
|
@@ -76,6 +87,7 @@ function readSecretFileOutcomeSync(filePath, label, options = {}) {
|
|
|
76
87
|
const opened = openPinnedFileSync({
|
|
77
88
|
filePath: resolvedPath,
|
|
78
89
|
rejectPathSymlink: options.rejectSymlink,
|
|
90
|
+
rejectHardlinks: options.rejectHardlinks !== false,
|
|
79
91
|
maxBytes,
|
|
80
92
|
});
|
|
81
93
|
if (!opened.ok) {
|
|
@@ -126,7 +138,15 @@ export function tryReadSecretFileSync(filePath, label, options = {}) {
|
|
|
126
138
|
return undefined;
|
|
127
139
|
}
|
|
128
140
|
const result = readSecretFileOutcomeSync(filePath, label, options);
|
|
129
|
-
|
|
141
|
+
if (result.ok) {
|
|
142
|
+
return result.secret;
|
|
143
|
+
}
|
|
144
|
+
if (result.code === "not-found") {
|
|
145
|
+
return undefined;
|
|
146
|
+
}
|
|
147
|
+
throw new FsSafeError(result.code, result.message, {
|
|
148
|
+
cause: result.error,
|
|
149
|
+
});
|
|
130
150
|
}
|
|
131
151
|
function isRelativeEscape(relativePath) {
|
|
132
152
|
return relativePath === ".." || relativePath.startsWith(`..${path.sep}`) || path.isAbsolute(relativePath);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"sidecar-lock.d.ts","sourceRoot":"","sources":["../src/sidecar-lock.ts"],"names":[],"mappings":"AAOA,MAAM,MAAM,uBAAuB,GAAG;IACpC,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,SAAS,CAAC,EAAE,OAAO,CAAC;CACrB,CAAC;AAEF,MAAM,MAAM,wBAAwB,GAAG,aAAa,GAAG,qBAAqB,CAAC;AAE7E,MAAM,MAAM,wBAAwB,GAAG;IACrC,QAAQ,EAAE,MAAM,CAAC;IACjB,oBAAoB,EAAE,MAAM,CAAC;IAC7B,GAAG,EAAE,MAAM,CAAC;IACZ,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI,CAAC;CACzC,CAAC;AAEF,MAAM,MAAM,yBAAyB,CAAC,QAAQ,SAAS,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,IAAI;IAChF,UAAU,EAAE,MAAM,CAAC;IACnB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,OAAO,EAAE,MAAM,CAAC;IAChB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,KAAK,CAAC,EAAE,uBAAuB,CAAC;IAChC,aAAa,CAAC,EAAE,wBAAwB,CAAC;IACzC,cAAc,CAAC,EAAE,OAAO,CAAC;IACzB,OAAO,EAAE,MAAM,QAAQ,GAAG,OAAO,CAAC,QAAQ,CAAC,CAAC;IAC5C,aAAa,CAAC,EAAE,CAAC,MAAM,EAAE;QACvB,QAAQ,EAAE,MAAM,CAAC;QACjB,oBAAoB,EAAE,MAAM,CAAC;QAC7B,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI,CAAC;QACxC,OAAO,EAAE,MAAM,CAAC;QAChB,KAAK,EAAE,MAAM,CAAC;QACd,iBAAiB,EAAE,OAAO,CAAC;KAC5B,KAAK,OAAO,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC;IACjC,qBAAqB,CAAC,EAAE,CACtB,QAAQ,EAAE,wBAAwB,KAC/B,OAAO,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC;IAChC,QAAQ,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CACpC,CAAC;AAEF,MAAM,MAAM,iBAAiB,GAAG;IAC9B,QAAQ,EAAE,MAAM,CAAC;IACjB,oBAAoB,EAAE,MAAM,CAAC;IAC7B,OAAO,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;IAC7B,CAAC,MAAM,CAAC,YAAY,CAAC,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;CACxC,CAAC;AAEF,MAAM,MAAM,oBAAoB,GAAG;IACjC,oBAAoB,EAAE,MAAM,CAAC;IAC7B,QAAQ,EAAE,MAAM,CAAC;IACjB,UAAU,EAAE,MAAM,CAAC;IACnB,QAAQ,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IAClC,YAAY,EAAE,MAAM,OAAO,CAAC,OAAO,CAAC,CAAC;CACtC,CAAC;AAEF,MAAM,MAAM,sBAAsB,CAAC,QAAQ,SAAS,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,IAAI,IAAI,CACjF,yBAAyB,CAAC,QAAQ,CAAC,EACnC,YAAY,CACb,GAAG;IACF,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB,CAAC;
|
|
1
|
+
{"version":3,"file":"sidecar-lock.d.ts","sourceRoot":"","sources":["../src/sidecar-lock.ts"],"names":[],"mappings":"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"}
|
package/dist/sidecar-lock.js
CHANGED
|
@@ -34,8 +34,11 @@ async function readLockSnapshot(lockPath) {
|
|
|
34
34
|
return { raw, payload: null, stat };
|
|
35
35
|
}
|
|
36
36
|
}
|
|
37
|
-
catch {
|
|
38
|
-
|
|
37
|
+
catch (err) {
|
|
38
|
+
if (err.code === "ENOENT") {
|
|
39
|
+
return null;
|
|
40
|
+
}
|
|
41
|
+
throw err;
|
|
39
42
|
}
|
|
40
43
|
}
|
|
41
44
|
function snapshotMatches(current, observed) {
|
|
@@ -86,7 +89,10 @@ async function removeStaleLockIfAllowed(params) {
|
|
|
86
89
|
try {
|
|
87
90
|
await fs.rm(params.lockPath, { force: true });
|
|
88
91
|
}
|
|
89
|
-
catch {
|
|
92
|
+
catch (err) {
|
|
93
|
+
if (err.code === "ENOENT") {
|
|
94
|
+
return "changed";
|
|
95
|
+
}
|
|
90
96
|
return "not-approved";
|
|
91
97
|
}
|
|
92
98
|
return "removed";
|
package/dist/symlink-parents.js
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
import fsSync from "node:fs";
|
|
2
2
|
import fs from "node:fs/promises";
|
|
3
3
|
import path from "node:path";
|
|
4
|
-
import { isNotFoundPathError } from "./path.js";
|
|
4
|
+
import { isNotFoundPathError, isPathRelativeEscape } from "./path.js";
|
|
5
5
|
function resolvePathWalk(params) {
|
|
6
6
|
const root = path.resolve(params.rootDir);
|
|
7
7
|
const target = path.resolve(params.targetPath);
|
|
8
8
|
const relative = path.relative(root, target);
|
|
9
|
-
if (
|
|
9
|
+
if (isPathRelativeEscape(relative)) {
|
|
10
10
|
if (params.allowOutsideRoot) {
|
|
11
11
|
return null;
|
|
12
12
|
}
|
package/docs/errors.md
CHANGED
|
@@ -30,6 +30,7 @@ class FsSafeError extends Error {
|
|
|
30
30
|
```ts
|
|
31
31
|
type FsSafeErrorCode =
|
|
32
32
|
| "already-exists"
|
|
33
|
+
| "denied-path"
|
|
33
34
|
| "hardlink"
|
|
34
35
|
| "helper-failed"
|
|
35
36
|
| "helper-unavailable"
|
|
@@ -55,6 +56,7 @@ type FsSafeErrorCode =
|
|
|
55
56
|
| Code | When it fires | Common causes |
|
|
56
57
|
|---|---|---|
|
|
57
58
|
| `already-exists` | `create()`, `createJson()`, `move({ overwrite: false })`. | Target file or directory already at the destination. |
|
|
59
|
+
| `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. |
|
|
58
60
|
| `hardlink` | Read or copy with `hardlinks: "reject"` saw `nlink > 1`. | File is hardlinked — possibly an alias of an out-of-tree inode. |
|
|
59
61
|
| `helper-failed` | Internal POSIX helper failed after startup. | Inspect `cause`; retrying may be unsafe if the operation may have partially completed. |
|
|
60
62
|
| `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. |
|
package/docs/python-helper.md
CHANGED
|
@@ -62,12 +62,12 @@ Node-only mode still keeps the important application-level guardrails:
|
|
|
62
62
|
- root-relative path validation;
|
|
63
63
|
- canonical root checks;
|
|
64
64
|
- no-follow opens where Node/platform support exists;
|
|
65
|
-
- file identity checks around reads and writes
|
|
66
|
-
- atomic sibling-temp replacement
|
|
65
|
+
- file identity checks around reads and writes;
|
|
66
|
+
- atomic sibling-temp replacement;
|
|
67
67
|
- hardlink/symlink policy checks where the API requests them;
|
|
68
68
|
- byte limits and structured `FsSafeError` failures.
|
|
69
69
|
|
|
70
|
-
What gets weaker is the POSIX defense against another same-UID process swapping a parent directory between validation and mutation.
|
|
70
|
+
What gets weaker is the POSIX defense against another same-UID process swapping a parent directory between validation and mutation. Without fd-relative mutation, `root().move()`, `root().remove()`, `root().mkdir()`, and some write paths rely on Node path operations plus pre/post checks instead of parent-fd syscalls.
|
|
71
71
|
|
|
72
72
|
That is usually acceptable when the root directory is only writable by the trusted application user. It is not the right posture if untrusted local processes can race writes in the same tree and you are relying on `fs-safe` as part of the security boundary.
|
|
73
73
|
|