@openclaw/fs-safe 0.3.0 → 0.4.1
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 +6 -4
- package/dist/advanced.d.ts +1 -0
- package/dist/advanced.d.ts.map +1 -1
- package/dist/advanced.js +1 -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/file-identity.d.ts +1 -0
- package/dist/file-identity.d.ts.map +1 -1
- package/dist/file-identity.js +10 -2
- 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/move-path.d.ts +3 -0
- package/dist/move-path.d.ts.map +1 -1
- package/dist/move-path.js +13 -1
- 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 +9 -1
- package/dist/pinned-write.d.ts.map +1 -1
- package/dist/pinned-write.js +80 -4
- package/dist/regular-file.d.ts.map +1 -1
- package/dist/regular-file.js +3 -0
- package/dist/root-impl.d.ts +4 -1
- package/dist/root-impl.d.ts.map +1 -1
- package/dist/root-impl.js +53 -47
- 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/test-hooks.d.ts +1 -0
- package/dist/test-hooks.d.ts.map +1 -1
- 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 +3 -0
- package/docs/install.md +3 -3
- package/docs/path.md +13 -0
- package/docs/reading.md +6 -4
- package/docs/root.md +2 -1
- package/docs/security-model.md +4 -2
- package/docs/types.md +2 -1
- package/docs/walk.md +11 -1
- package/docs/writing.md +23 -3
- package/package.json +23 -17
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"regular-file.d.ts","sourceRoot":"","sources":["../src/regular-file.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,SAAS,CAAC;AACrC,OAAO,MAAM,MAAM,SAAS,CAAC;
|
|
1
|
+
{"version":3,"file":"regular-file.d.ts","sourceRoot":"","sources":["../src/regular-file.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,SAAS,CAAC;AACrC,OAAO,MAAM,MAAM,SAAS,CAAC;AAU7B,MAAM,MAAM,qBAAqB,GAAG;IAAE,OAAO,EAAE,IAAI,CAAA;CAAE,GAAG;IAAE,OAAO,EAAE,KAAK,CAAC;IAAC,IAAI,EAAE,KAAK,CAAA;CAAE,CAAC;AAExF,KAAK,8BAA8B,GAAG,IAAI,CACxC,OAAO,MAAM,CAAC,SAAS,EACvB,UAAU,GAAG,SAAS,GAAG,UAAU,CACpC,GACC,OAAO,CAAC,IAAI,CAAC,OAAO,MAAM,CAAC,SAAS,EAAE,YAAY,CAAC,CAAC,CAAC;AAEvD,MAAM,MAAM,wBAAwB,GAAG;IACrC,QAAQ,EAAE,MAAM,CAAC;IACjB,OAAO,EAAE,MAAM,GAAG,UAAU,CAAC;IAC7B,QAAQ,CAAC,EAAE,cAAc,CAAC;IAC1B,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,oBAAoB,CAAC,EAAE,OAAO,CAAC;CAChC,CAAC;AAEF,wBAAgB,6BAA6B,CAC3C,SAAS,GAAE,8BAAiD,GAC3D,MAAM,CAQR;AA2DD,wBAAsB,eAAe,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,qBAAqB,CAAC,CActF;AAED,wBAAgB,mBAAmB,CAAC,QAAQ,EAAE,MAAM,GAAG,qBAAqB,CAc3E;AAED,wBAAsB,eAAe,CAAC,MAAM,EAAE;IAC5C,QAAQ,EAAE,MAAM,CAAC;IACjB,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB,GAAG,OAAO,CAAC;IAAE,MAAM,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,KAAK,CAAA;CAAE,CAAC,CAkD3C;AA6CD,wBAAgB,mBAAmB,CAAC,MAAM,EAAE;IAAE,QAAQ,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,EAAE,MAAM,CAAA;CAAE,GAAG;IACpF,MAAM,EAAE,MAAM,CAAC;IACf,IAAI,EAAE,KAAK,CAAC;CACb,CAqBA;AAmBD,wBAAsB,iBAAiB,CAAC,OAAO,EAAE,wBAAwB,GAAG,OAAO,CAAC,IAAI,CAAC,CAuDxF;AAED,wBAAgB,qBAAqB,CAAC,OAAO,EAAE,wBAAwB,GAAG,IAAI,CA2D7E"}
|
package/dist/regular-file.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import fsSync 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 { isNotFoundPathError } from "./path.js";
|
|
@@ -89,6 +90,7 @@ export function statRegularFileSync(filePath) {
|
|
|
89
90
|
return { missing: false, stat };
|
|
90
91
|
}
|
|
91
92
|
export async function readRegularFile(params) {
|
|
93
|
+
assertNoUnsafeDeviceReadPath(params.filePath);
|
|
92
94
|
const result = await statRegularFile(params.filePath);
|
|
93
95
|
if (result.missing) {
|
|
94
96
|
throw Object.assign(new Error(`File not found: ${params.filePath}`), { code: "ENOENT" });
|
|
@@ -170,6 +172,7 @@ function readOpenedRegularFileSync(params) {
|
|
|
170
172
|
return { buffer, stat };
|
|
171
173
|
}
|
|
172
174
|
export function readRegularFileSync(params) {
|
|
175
|
+
assertNoUnsafeDeviceReadPath(params.filePath);
|
|
173
176
|
const result = statRegularFileSync(params.filePath);
|
|
174
177
|
if (result.missing) {
|
|
175
178
|
throw Object.assign(new Error(`File not found: ${params.filePath}`), { code: "ENOENT" });
|
package/dist/root-impl.d.ts
CHANGED
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
import type { Stats } from "node:fs";
|
|
2
2
|
import type { FileHandle } from "node:fs/promises";
|
|
3
3
|
import { type DenyMutationPolicy } from "./deny-mutations.js";
|
|
4
|
+
import { type RenameIdentityPolicy } from "./pinned-write.js";
|
|
4
5
|
import { type ReadResult } from "./read-opened-file.js";
|
|
5
6
|
import type { DirEntry, PathStat } from "./types.js";
|
|
6
7
|
export type { DenyMutationPolicy } from "./deny-mutations.js";
|
|
8
|
+
export type { RenameIdentityPolicy } from "./pinned-write.js";
|
|
7
9
|
export { resolveOpenedFileRealPathForHandle } from "./opened-realpath.js";
|
|
8
10
|
export type { ReadResult } from "./read-opened-file.js";
|
|
9
11
|
export type OpenResult = {
|
|
@@ -26,11 +28,12 @@ export type RootDefaults = {
|
|
|
26
28
|
mode?: number;
|
|
27
29
|
denyMutations?: DenyMutationPolicy;
|
|
28
30
|
nonBlockingRead?: boolean;
|
|
31
|
+
renameIdentity?: RenameIdentityPolicy;
|
|
29
32
|
symlinks?: SymlinkPolicy;
|
|
30
33
|
};
|
|
31
34
|
export type RootReadOptions = Pick<RootDefaults, "hardlinks" | "maxBytes" | "nonBlockingRead" | "symlinks">;
|
|
32
35
|
export type RootOpenOptions = Omit<RootReadOptions, "maxBytes">;
|
|
33
|
-
export type RootWriteOptions = Pick<RootDefaults, "denyMutations" | "mkdir" | "mode"> & {
|
|
36
|
+
export type RootWriteOptions = Pick<RootDefaults, "denyMutations" | "mkdir" | "mode" | "renameIdentity"> & {
|
|
34
37
|
encoding?: BufferEncoding;
|
|
35
38
|
overwrite?: boolean;
|
|
36
39
|
};
|
package/dist/root-impl.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"root-impl.d.ts","sourceRoot":"","sources":["../src/root-impl.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,SAAS,CAAC;AAErC,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,kBAAkB,CAAC;
|
|
1
|
+
{"version":3,"file":"root-impl.d.ts","sourceRoot":"","sources":["../src/root-impl.ts"],"names":[],"mappings":"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"}
|
package/dist/root-impl.js
CHANGED
|
@@ -6,16 +6,17 @@ 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";
|
|
12
13
|
import { assertMutationNotDenied, mergeDenyMutationPolicies, } from "./deny-mutations.js";
|
|
13
14
|
import { resolveOpenedFileRealPathForHandle } from "./opened-realpath.js";
|
|
14
15
|
import { isPinnedPathHelperSpawnError, runPinnedPathHelper } from "./pinned-path.js";
|
|
15
|
-
import { runPinnedCopyHelper,
|
|
16
|
+
import { runPinnedCopyHelper, runPinnedWriteWithRenamePolicy, } from "./pinned-write.js";
|
|
16
17
|
import { canFallbackFromPythonError, getFsSafePythonConfig } from "./pinned-python-config.js";
|
|
17
18
|
import { assertNoPathAliasEscape, PATH_ALIAS_POLICIES } from "./path-policy.js";
|
|
18
|
-
import { assertNoNulPathInput, hasNodeErrorCode, isNotFoundPathError, isPathInside, isSymlinkOpenError, } from "./path.js";
|
|
19
|
+
import { assertNoNulPathInput, assertNoUnsafeDeviceReadPath, hasNodeErrorCode, isNotFoundPathError, isPathInside, isSymlinkOpenError, } from "./path.js";
|
|
19
20
|
import { readOpenedFileSafely } from "./read-opened-file.js";
|
|
20
21
|
import { helperReaddir, helperStat, runPinnedHelper, } from "./pinned-helper.js";
|
|
21
22
|
import { pathStatFromStats } from "./path-stat.js";
|
|
@@ -50,20 +51,16 @@ const OPEN_APPEND_CREATE_FLAGS = fsConstants.O_RDWR |
|
|
|
50
51
|
fsConstants.O_EXCL |
|
|
51
52
|
(SUPPORTS_NOFOLLOW ? fsConstants.O_NOFOLLOW : 0);
|
|
52
53
|
export const DEFAULT_ROOT_MAX_BYTES = 16 * 1024 * 1024;
|
|
53
|
-
function closeHandleForDispose(handle) {
|
|
54
|
-
return handle.close().catch(() => undefined);
|
|
55
|
-
}
|
|
56
54
|
function openResult(params) {
|
|
57
55
|
return {
|
|
58
56
|
handle: params.handle,
|
|
59
57
|
realPath: params.realPath,
|
|
60
58
|
stat: params.stat,
|
|
61
|
-
[Symbol.asyncDispose]:
|
|
62
|
-
await closeHandleForDispose(params.handle);
|
|
63
|
-
},
|
|
59
|
+
[Symbol.asyncDispose]: () => params.handle.close().catch(() => undefined),
|
|
64
60
|
};
|
|
65
61
|
}
|
|
66
62
|
async function openVerifiedLocalFile(filePath, options) {
|
|
63
|
+
assertNoUnsafeDeviceReadPath(filePath);
|
|
67
64
|
const fsSafeTestHooks = getFsSafeTestHooks();
|
|
68
65
|
// Reject directories before opening so we never surface EISDIR to callers (e.g. tool
|
|
69
66
|
// results that get sent to messaging channels). See openclaw/openclaw#31186.
|
|
@@ -262,6 +259,7 @@ class RootHandle {
|
|
|
262
259
|
data,
|
|
263
260
|
mkdir: this.defaults.mkdir,
|
|
264
261
|
mode: this.defaults.mode,
|
|
262
|
+
renameIdentity: this.defaults.renameIdentity,
|
|
265
263
|
...options,
|
|
266
264
|
denyMutations: mergeDenyMutationPolicies(this.defaults.denyMutations, options.denyMutations),
|
|
267
265
|
});
|
|
@@ -588,9 +586,7 @@ async function openWritableFileInRoot(root, params) {
|
|
|
588
586
|
createdForWrite,
|
|
589
587
|
realPath,
|
|
590
588
|
stat,
|
|
591
|
-
[Symbol.asyncDispose]:
|
|
592
|
-
await closeHandleForDispose(handle);
|
|
593
|
-
},
|
|
589
|
+
[Symbol.asyncDispose]: () => handle.close().catch(() => undefined),
|
|
594
590
|
};
|
|
595
591
|
}
|
|
596
592
|
catch (err) {
|
|
@@ -627,10 +623,15 @@ async function appendFileInRoot(root, params) {
|
|
|
627
623
|
}
|
|
628
624
|
if (typeof params.data === "string") {
|
|
629
625
|
await target.handle.appendFile(`${prefix}${params.data}`, params.encoding ?? "utf8");
|
|
630
|
-
return;
|
|
631
626
|
}
|
|
632
|
-
|
|
633
|
-
|
|
627
|
+
else {
|
|
628
|
+
const payload = prefix.length > 0 ? Buffer.concat([Buffer.from(prefix, "utf8"), params.data]) : params.data;
|
|
629
|
+
await target.handle.appendFile(payload);
|
|
630
|
+
}
|
|
631
|
+
await target.handle.sync();
|
|
632
|
+
if (target.createdForWrite) {
|
|
633
|
+
await syncDirectoryBestEffort(path.dirname(target.realPath));
|
|
634
|
+
}
|
|
634
635
|
}
|
|
635
636
|
finally {
|
|
636
637
|
await target.handle.close().catch(() => { });
|
|
@@ -687,42 +688,47 @@ async function writeFileInRoot(root, params) {
|
|
|
687
688
|
}
|
|
688
689
|
const pinned = await resolvePinnedWriteTargetInRoot(root, params.relativePath, params.mode, params.denyMutations);
|
|
689
690
|
await serializePathWrite(pinned.targetPath, async () => {
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
throw normalizePinnedWriteError(error);
|
|
691
|
+
await commitPinnedWriteInRoot(root, pinned, params);
|
|
692
|
+
});
|
|
693
|
+
}
|
|
694
|
+
async function commitPinnedWriteInRoot(root, pinned, params) {
|
|
695
|
+
let identity;
|
|
696
|
+
try {
|
|
697
|
+
identity = await runPinnedWriteWithRenamePolicy({
|
|
698
|
+
rootPath: pinned.rootReal,
|
|
699
|
+
relativeParentPath: pinned.relativeParentPath,
|
|
700
|
+
basename: pinned.basename,
|
|
701
|
+
targetPath: pinned.targetPath,
|
|
702
|
+
renameIdentity: params.renameIdentity,
|
|
703
|
+
mkdir: params.mkdir !== false,
|
|
704
|
+
mode: params.mode ?? pinned.mode,
|
|
705
|
+
overwrite: params.overwrite,
|
|
706
|
+
input: { kind: "buffer", data: params.data, encoding: params.encoding },
|
|
707
|
+
});
|
|
708
|
+
}
|
|
709
|
+
catch (error) {
|
|
710
|
+
const errorCode = error?.code;
|
|
711
|
+
if (errorCode === "file_lock_stale" || errorCode === "file_lock_timeout") {
|
|
712
|
+
throw error;
|
|
713
713
|
}
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
targetPath: pinned.targetPath,
|
|
718
|
-
expectedIdentity: identity,
|
|
714
|
+
if (params.overwrite === false && isAlreadyExistsError(error)) {
|
|
715
|
+
throw new FsSafeError("already-exists", "file already exists", {
|
|
716
|
+
cause: error instanceof Error ? error : undefined,
|
|
719
717
|
});
|
|
720
718
|
}
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
719
|
+
throw normalizePinnedWriteError(error);
|
|
720
|
+
}
|
|
721
|
+
try {
|
|
722
|
+
await verifyAtomicWriteResult({
|
|
723
|
+
root,
|
|
724
|
+
targetPath: pinned.targetPath,
|
|
725
|
+
expectedIdentity: identity,
|
|
726
|
+
});
|
|
727
|
+
}
|
|
728
|
+
catch (err) {
|
|
729
|
+
emitWriteBoundaryWarning(`post-write verification failed: ${String(err)}`);
|
|
730
|
+
throw err;
|
|
731
|
+
}
|
|
726
732
|
}
|
|
727
733
|
async function copyFileInRoot(root, params) {
|
|
728
734
|
assertValidRootRelativePath(params.relativePath);
|
package/dist/root.d.ts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
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";
|
|
1
|
+
export { DEFAULT_ROOT_MAX_BYTES, openLocalFileSafely, readLocalFileSafely, resolveOpenedFileRealPathForHandle, root, type DenyMutationPolicy, type HardlinkPolicy, type RenameIdentityPolicy, 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,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
|
+
{"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,oBAAoB,EACzB,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/test-hooks.d.ts
CHANGED
|
@@ -7,6 +7,7 @@ export type FsSafeTestHooks = {
|
|
|
7
7
|
beforeFileStorePruneDescend?: (dirPath: string) => Promise<void> | void;
|
|
8
8
|
beforeFileStoreSyncPrivateWrite?: (filePath: string) => void;
|
|
9
9
|
beforeRootFallbackMutation?: (operation: "mkdir" | "move" | "remove", targetPath: string) => Promise<void> | void;
|
|
10
|
+
afterPinnedWriteFallbackRename?: (targetPath: string) => Promise<void> | void;
|
|
10
11
|
beforeSiblingTempWrite?: (tempPath: string) => Promise<void> | void;
|
|
11
12
|
beforeTrashMove?: (targetPath: string, destPath: string) => void;
|
|
12
13
|
};
|
package/dist/test-hooks.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"test-hooks.d.ts","sourceRoot":"","sources":["../src/test-hooks.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,kBAAkB,CAAC;AAEnD,MAAM,MAAM,eAAe,GAAG;IAC5B,iBAAiB,CAAC,EAAE,CAAC,QAAQ,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,GAAG,IAAI,CAAC;IAC/D,UAAU,CAAC,EAAE,CAAC,QAAQ,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,GAAG,IAAI,CAAC;IACvE,SAAS,CAAC,EAAE,CAAC,QAAQ,EAAE,MAAM,EAAE,MAAM,EAAE,UAAU,KAAK,OAAO,CAAC,IAAI,CAAC,GAAG,IAAI,CAAC;IAC3E,2BAA2B,CAAC,EAAE,CAC5B,SAAS,EAAE,OAAO,GAAG,OAAO,EAC5B,UAAU,EAAE,MAAM,KACf,OAAO,CAAC,IAAI,CAAC,GAAG,IAAI,CAAC;IAC1B,2BAA2B,CAAC,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,GAAG,IAAI,CAAC;IACxE,+BAA+B,CAAC,EAAE,CAAC,QAAQ,EAAE,MAAM,KAAK,IAAI,CAAC;IAC7D,0BAA0B,CAAC,EAAE,CAC3B,SAAS,EAAE,OAAO,GAAG,MAAM,GAAG,QAAQ,EACtC,UAAU,EAAE,MAAM,KACf,OAAO,CAAC,IAAI,CAAC,GAAG,IAAI,CAAC;IAC1B,sBAAsB,CAAC,EAAE,CAAC,QAAQ,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,GAAG,IAAI,CAAC;IACpE,eAAe,CAAC,EAAE,CAAC,UAAU,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,KAAK,IAAI,CAAC;CAClE,CAAC;AAQF,wBAAgB,kBAAkB,IAAI,eAAe,GAAG,SAAS,CAEhE;AAED,wBAAgB,2BAA2B,CAAC,KAAK,CAAC,EAAE,eAAe,GAAG,IAAI,CAKzE"}
|
|
1
|
+
{"version":3,"file":"test-hooks.d.ts","sourceRoot":"","sources":["../src/test-hooks.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,kBAAkB,CAAC;AAEnD,MAAM,MAAM,eAAe,GAAG;IAC5B,iBAAiB,CAAC,EAAE,CAAC,QAAQ,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,GAAG,IAAI,CAAC;IAC/D,UAAU,CAAC,EAAE,CAAC,QAAQ,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,GAAG,IAAI,CAAC;IACvE,SAAS,CAAC,EAAE,CAAC,QAAQ,EAAE,MAAM,EAAE,MAAM,EAAE,UAAU,KAAK,OAAO,CAAC,IAAI,CAAC,GAAG,IAAI,CAAC;IAC3E,2BAA2B,CAAC,EAAE,CAC5B,SAAS,EAAE,OAAO,GAAG,OAAO,EAC5B,UAAU,EAAE,MAAM,KACf,OAAO,CAAC,IAAI,CAAC,GAAG,IAAI,CAAC;IAC1B,2BAA2B,CAAC,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,GAAG,IAAI,CAAC;IACxE,+BAA+B,CAAC,EAAE,CAAC,QAAQ,EAAE,MAAM,KAAK,IAAI,CAAC;IAC7D,0BAA0B,CAAC,EAAE,CAC3B,SAAS,EAAE,OAAO,GAAG,MAAM,GAAG,QAAQ,EACtC,UAAU,EAAE,MAAM,KACf,OAAO,CAAC,IAAI,CAAC,GAAG,IAAI,CAAC;IAC1B,8BAA8B,CAAC,EAAE,CAAC,UAAU,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,GAAG,IAAI,CAAC;IAC9E,sBAAsB,CAAC,EAAE,CAAC,QAAQ,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,GAAG,IAAI,CAAC;IACpE,eAAe,CAAC,EAAE,CAAC,UAAU,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,KAAK,IAAI,CAAC;CAClE,CAAC;AAQF,wBAAgB,kBAAkB,IAAI,eAAe,GAAG,SAAS,CAEhE;AAED,wBAAgB,2BAA2B,CAAC,KAAK,CAAC,EAAE,eAAe,GAAG,IAAI,CAKzE"}
|
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
|
@@ -31,6 +31,7 @@ class FsSafeError extends Error {
|
|
|
31
31
|
type FsSafeErrorCode =
|
|
32
32
|
| "already-exists"
|
|
33
33
|
| "denied-path"
|
|
34
|
+
| "device-path"
|
|
34
35
|
| "hardlink"
|
|
35
36
|
| "helper-failed"
|
|
36
37
|
| "helper-unavailable"
|
|
@@ -57,6 +58,7 @@ type FsSafeErrorCode =
|
|
|
57
58
|
|---|---|---|
|
|
58
59
|
| `already-exists` | `create()`, `createJson()`, `move({ overwrite: false })`. | Target file or directory already at the destination. |
|
|
59
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. |
|
|
60
62
|
| `hardlink` | Read or copy with `hardlinks: "reject"` saw `nlink > 1`. | File is hardlinked — possibly an alias of an out-of-tree inode. |
|
|
61
63
|
| `helper-failed` | Internal POSIX helper failed after startup. | Inspect `cause`; retrying may be unsafe if the operation may have partially completed. |
|
|
62
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. |
|
|
@@ -95,6 +97,7 @@ try {
|
|
|
95
97
|
case "not-found":
|
|
96
98
|
return reply(404, "missing");
|
|
97
99
|
case "symlink":
|
|
100
|
+
case "device-path":
|
|
98
101
|
case "hardlink":
|
|
99
102
|
case "path-mismatch":
|
|
100
103
|
case "path-alias":
|
package/docs/install.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Install
|
|
2
2
|
|
|
3
|
-
`fs-safe` is published to npm as `@openclaw/fs-safe`. It targets Node
|
|
3
|
+
`fs-safe` is published to npm as `@openclaw/fs-safe`. It targets Node 22 or newer, ships ESM only, and works on macOS, Linux, and Windows.
|
|
4
4
|
|
|
5
5
|
## Package managers
|
|
6
6
|
|
|
@@ -22,13 +22,13 @@ bun add @openclaw/fs-safe
|
|
|
22
22
|
|
|
23
23
|
## Node version
|
|
24
24
|
|
|
25
|
-
Minimum **Node
|
|
25
|
+
Minimum **Node 22**. The package uses `fs.promises`, `fs.constants.O_NOFOLLOW` where available, and `node:stream/promises`. Earlier Node releases will fail at import time.
|
|
26
26
|
|
|
27
27
|
Verify the runtime:
|
|
28
28
|
|
|
29
29
|
```bash
|
|
30
30
|
node --version
|
|
31
|
-
#
|
|
31
|
+
# v22.0.0 or newer
|
|
32
32
|
```
|
|
33
33
|
|
|
34
34
|
## TypeScript
|
package/docs/path.md
CHANGED
|
@@ -11,6 +11,8 @@ import {
|
|
|
11
11
|
safeRealpathSync,
|
|
12
12
|
safeStatSync,
|
|
13
13
|
assertNoNulPathInput,
|
|
14
|
+
assertNoUnsafeDeviceReadPath,
|
|
15
|
+
isUnsafeDeviceReadPath,
|
|
14
16
|
isNotFoundPathError,
|
|
15
17
|
isSymlinkOpenError,
|
|
16
18
|
hasNodeErrorCode,
|
|
@@ -89,6 +91,17 @@ if (!stat?.isFile()) return notFound();
|
|
|
89
91
|
|
|
90
92
|
Throws `FsSafeError` with code `invalid-path` when a path string contains an embedded NUL byte. Use it before calling Node `fs` APIs directly; Node's native error can include raw path text in the message.
|
|
91
93
|
|
|
94
|
+
### `assertNoUnsafeDeviceReadPath(filePath, options?)`
|
|
95
|
+
|
|
96
|
+
Throws `FsSafeError` with code `device-path` when a read target is a known unsafe device or process-fd path. The built-in read/open helpers call this automatically before opening files; use it only when you are building your own read primitive.
|
|
97
|
+
|
|
98
|
+
```ts
|
|
99
|
+
assertNoUnsafeDeviceReadPath("/dev/zero"); // throws on POSIX
|
|
100
|
+
isUnsafeDeviceReadPath("/dev/fd/0"); // true on POSIX
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
The check is intentionally not a normal consumer policy knob. Safe read APIs reject these targets by default because they can block forever, stream indefinitely, or alias process file descriptors.
|
|
104
|
+
|
|
92
105
|
## Error inspection
|
|
93
106
|
|
|
94
107
|
### `isNotFoundPathError(err)`
|
package/docs/reading.md
CHANGED
|
@@ -17,10 +17,11 @@ Regardless of shape, every read goes through the same boundary checks:
|
|
|
17
17
|
1. Resolve the relative path against the canonical real root.
|
|
18
18
|
2. Reject anything that escapes the root (`outside-workspace`).
|
|
19
19
|
3. Reject `..` segments and absolute inputs (unless via `readAbsolute` with an in-root absolute path).
|
|
20
|
-
4.
|
|
21
|
-
5.
|
|
22
|
-
6.
|
|
23
|
-
7. If `
|
|
20
|
+
4. Reject known unsafe device and process-fd paths before opening (`device-path`).
|
|
21
|
+
5. Open with `O_NOFOLLOW` where available. A symlink in the path triggers `symlink` unless the call's `symlinks` policy is `follow-within-root`.
|
|
22
|
+
6. Stat the open fd and compare to the resolved path's identity (`sameFileIdentity`). A swap mid-call triggers `path-mismatch`.
|
|
23
|
+
7. If `hardlinks: "reject"`, refuse files with `nlink > 1` (`hardlink`).
|
|
24
|
+
8. If `maxBytes` is set, refuse reads larger than the cap (`too-large`).
|
|
24
25
|
|
|
25
26
|
## Read shapes
|
|
26
27
|
|
|
@@ -160,6 +161,7 @@ try {
|
|
|
160
161
|
- **`outside-workspace`** — relative path escaped the root, or `readAbsolute` got an absolute path outside.
|
|
161
162
|
- **`not-found`** — the file is gone.
|
|
162
163
|
- **`not-file`** — you read a directory or a non-regular file (FIFO, socket, …).
|
|
164
|
+
- **`device-path`** — the path targets a known unsafe device or process fd path.
|
|
163
165
|
- **`symlink`** — a path component is a symlink and the policy is `reject`.
|
|
164
166
|
- **`path-mismatch`** — opened fd identity did not match the resolved path. Almost always a TOCTOU swap by something else.
|
|
165
167
|
- **`hardlink`** — `hardlinks: "reject"` saw `nlink > 1`.
|
package/docs/root.md
CHANGED
|
@@ -71,7 +71,7 @@ fs.write(rel, data, options?) // overwrite-ok atomic write
|
|
|
71
71
|
fs.create(rel, data, options?) // throws "already-exists" if target exists
|
|
72
72
|
fs.writeJson(rel, value, options?) // JSON.stringify + atomic write
|
|
73
73
|
fs.createJson(rel, value, options?) // create() variant of writeJson
|
|
74
|
-
fs.append(rel, data, options?) // append text/buffer;
|
|
74
|
+
fs.append(rel, data, options?) // append text/buffer; syncs before close
|
|
75
75
|
fs.copyIn(rel, sourceAbsPath, options?) // copy from outside the root, atomically, with size cap
|
|
76
76
|
fs.openWritable(rel, options?) // FileHandle for streaming writes; supports await using
|
|
77
77
|
fs.move(from, to, options?) // rename within the root; defaults to no clobber
|
|
@@ -139,6 +139,7 @@ Every method throws `FsSafeError` with a `code`. Branch on `err.code`, not messa
|
|
|
139
139
|
| `outside-workspace` | The input resolves outside the root, or contains a `..` segment that would escape it. |
|
|
140
140
|
| `not-found` | The target does not exist (or its parent does not, with `mkdir: false`). |
|
|
141
141
|
| `not-file` | A read or copy targeted a non-regular file (directory, FIFO, socket, …). |
|
|
142
|
+
| `device-path` | A read/open target is a known unsafe device or process-fd path. |
|
|
142
143
|
| `already-exists` | `create()` or `move()` without `overwrite` hit an existing target. |
|
|
143
144
|
| `denied-path` | A mutation target matched `denyMutations.paths` or `denyMutations.prefixes`. |
|
|
144
145
|
| `symlink` | A path component is a symlink, and the call's `symlinks` policy is `reject`. |
|
package/docs/security-model.md
CHANGED
|
@@ -13,6 +13,7 @@ You hand a `root()` boundary to a piece of code that takes caller-controlled rel
|
|
|
13
13
|
- replaces a path component with a symlink between check and use (TOCTOU)
|
|
14
14
|
- replaces the destination directory with a symlink right before a write
|
|
15
15
|
- creates a hardlink that aliases an out-of-tree inode and asks you to read or replace it
|
|
16
|
+
- asks a read/open primitive to target a known unsafe device or process-fd path
|
|
16
17
|
- triggers a partial write that leaves a half-written file at the destination
|
|
17
18
|
- ships an archive with `..` paths, absolute paths, or symlinks pointing outside the destination
|
|
18
19
|
|
|
@@ -20,7 +21,7 @@ It does **not** defend against:
|
|
|
20
21
|
|
|
21
22
|
- a process running with permissions to write anywhere on the filesystem and choosing to ignore the library
|
|
22
23
|
- another process with the same UID racing to mutate the same directory between two separate `fs-safe` calls — the boundary is per-call, not per-session
|
|
23
|
-
- traversal across filesystem boundaries, bind mounts,
|
|
24
|
+
- arbitrary traversal across filesystem boundaries, bind mounts, or virtual filesystems beyond the known unsafe read device paths
|
|
24
25
|
- container escape, TOCTOU between fork and exec of helpers, or kernel-level vulnerabilities
|
|
25
26
|
- semantic content checks: file types, archive payload schemas, signature verification
|
|
26
27
|
|
|
@@ -83,12 +84,13 @@ The library does not advertise different security guarantees per platform — it
|
|
|
83
84
|
|---|---|
|
|
84
85
|
| Not ambient authority removal | Code that can import `node:fs` can still bypass the handle. Keep caller-controlled path operations behind `root()` by convention, review, and tests. |
|
|
85
86
|
| Absolute paths are escape hatches | APIs that accept or return absolute paths exist for audit, ingest, and advanced composition. Prefer root-relative names in normal application flow. |
|
|
86
|
-
| Not a mount
|
|
87
|
+
| Not a mount boundary | `root()` keeps path traversal inside the directory tree and blocks known unsafe read device paths, but it does not make bind mounts or virtual filesystems safe to expose wholesale. |
|
|
87
88
|
| Per-call, not per-session | Another process with the same privileges can still mutate the tree between two separate calls. Use one verb method for the operation you need to make race-resistant. |
|
|
88
89
|
| Hardlink rejection is best-effort | Link-count checks depend on platform metadata. Treat `hardlinks: "reject"` as a tripwire, not an authorization primitive. |
|
|
89
90
|
| Mode bits are not a full policy engine | `replaceFileAtomic` and secret-file helpers set requested modes, but you should still set umask and inspect modes when policy requires it. |
|
|
90
91
|
| Archive extraction is path safety, not content safety | Unsafe entry paths and links are rejected; malicious payload contents remain your application layer's problem. |
|
|
91
92
|
| Helper failures degrade fd-relative hardening | `helper-unavailable` falls back in `auto` mode and fails closed in `require` mode. Atomicity and identity checks remain, but parent-directory swaps between validation and mutation are less tightly pinned without the helper. |
|
|
93
|
+
| FUSE mounts with rename-unstable inode numbers | Some FUSE mounts (rclone is a confirmed example) do not preserve source inode identity at the rename destination. The explicit `renameIdentity: "verify-content-with-lock"` compatibility mode verifies content under a cooperative lock for that boundary only; subsequent path identity checks and the default remain strict. See [Writing](writing.md) for the weaker opt-in contract. |
|
|
92
94
|
|
|
93
95
|
## Recommended deployment shape
|
|
94
96
|
|
package/docs/types.md
CHANGED
|
@@ -148,7 +148,8 @@ The two policy unions you'll see throughout. `"reject"` is conservative; `"follo
|
|
|
148
148
|
|
|
149
149
|
```ts
|
|
150
150
|
type FsSafeErrorCode =
|
|
151
|
-
| "already-exists" | "denied-path" | "
|
|
151
|
+
| "already-exists" | "denied-path" | "device-path" | "hardlink"
|
|
152
|
+
| "helper-failed"
|
|
152
153
|
| "helper-unavailable" | "insecure-permissions" | "invalid-path"
|
|
153
154
|
| "not-empty" | "not-file" | "not-found" | "not-owned"
|
|
154
155
|
| "not-removable" | "outside-workspace" | "path-alias"
|