@openclaw/fs-safe 0.2.7 → 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 CHANGED
@@ -1,5 +1,15 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.3.0 - 2026-05-21
4
+
5
+ ### Features
6
+
7
+ - Add opt-in `denyMutations` policies with exact `paths` and subtree `prefixes` so callers can protect application-sensitive files from root write, copy, move, remove, mkdir, and writable-open operations. (#20; thanks @amknight)
8
+
9
+ ### Security and Correctness
10
+
11
+ - Retry async JSON reads (`readJson`, `readJsonIfExists`, `tryReadJson`) up to five attempts with 50ms exponential backoff when the file is rotated mid-read by an atomic rename, and tag the underlying race as `FsSafeError("path-mismatch")` so callers can distinguish transient swaps from corruption. (#19; thanks @yetval)
12
+
3
13
  ## 0.2.7 - 2026-05-20
4
14
 
5
15
  ### Security and Correctness
package/README.md CHANGED
@@ -155,7 +155,18 @@ await using opened = await fs.openWritable("logs/current.log", { writeMode: "app
155
155
  }
156
156
  ```
157
157
 
158
- `nonBlockingRead` is the only I/O scheduling knob in `RootDefaults`; it applies to read/open operations because it changes how file descriptors are opened. Filesystem safety policy remains explicit through `hardlinks` and `symlinks`.
158
+ `nonBlockingRead` is the only I/O scheduling knob in `RootDefaults`; it applies to read/open operations because it changes how file descriptors are opened. Filesystem safety policy remains explicit through `hardlinks`, `symlinks`, and `denyMutations`.
159
+
160
+ ```ts
161
+ const locked = await root("/srv/workspace", {
162
+ denyMutations: {
163
+ paths: ["/srv/workspace/.env"],
164
+ prefixes: ["/srv/workspace/.ssh"],
165
+ },
166
+ });
167
+
168
+ await locked.write(".env", "token"); // FsSafeError code "denied-path"
169
+ ```
159
170
 
160
171
  `stat()`, `exists()`, and `list()` are boundary-checked, but they cannot pin a later operation to the same filesystem object. Use `read()`, `open()`, `write()`, `create()`, `copyIn()`, `move()`, or `remove()` for operations that must be race-resistant at the point of use.
161
172
 
@@ -0,0 +1,11 @@
1
+ export type DenyMutationPolicy = {
2
+ paths?: readonly string[];
3
+ prefixes?: readonly string[];
4
+ };
5
+ type DenyMutationCheckOptions = {
6
+ protectAncestors?: boolean;
7
+ };
8
+ export declare function assertMutationNotDenied(filePath: string, policy: DenyMutationPolicy | undefined, options?: DenyMutationCheckOptions): Promise<void>;
9
+ export declare function mergeDenyMutationPolicies(defaultPolicy: DenyMutationPolicy | undefined, callPolicy: DenyMutationPolicy | undefined): DenyMutationPolicy | undefined;
10
+ export {};
11
+ //# sourceMappingURL=deny-mutations.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"deny-mutations.d.ts","sourceRoot":"","sources":["../src/deny-mutations.ts"],"names":[],"mappings":"AAmDA,MAAM,MAAM,kBAAkB,GAAG;IAC/B,KAAK,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;IAC1B,QAAQ,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;CAC9B,CAAC;AAEF,KAAK,wBAAwB,GAAG;IAC9B,gBAAgB,CAAC,EAAE,OAAO,CAAC;CAC5B,CAAC;AAqBF,wBAAsB,uBAAuB,CAC3C,QAAQ,EAAE,MAAM,EAChB,MAAM,EAAE,kBAAkB,GAAG,SAAS,EACtC,OAAO,GAAE,wBAA6B,GACrC,OAAO,CAAC,IAAI,CAAC,CAiCf;AAED,wBAAgB,yBAAyB,CACvC,aAAa,EAAE,kBAAkB,GAAG,SAAS,EAC7C,UAAU,EAAE,kBAAkB,GAAG,SAAS,GACzC,kBAAkB,GAAG,SAAS,CAWhC"}
@@ -0,0 +1,102 @@
1
+ import fs from "node:fs/promises";
2
+ import path from "node:path";
3
+ import { FsSafeError } from "./errors.js";
4
+ import { assertNoNulPathInput, isNotFoundPathError, isPathInside } from "./path.js";
5
+ async function pathExists(filePath) {
6
+ try {
7
+ await fs.lstat(filePath);
8
+ return true;
9
+ }
10
+ catch (err) {
11
+ if (!isNotFoundPathError(err)) {
12
+ throw err;
13
+ }
14
+ return false;
15
+ }
16
+ }
17
+ async function resolvePathViaExistingAncestor(targetPath) {
18
+ const normalized = path.resolve(targetPath);
19
+ let cursor = normalized;
20
+ const missingSuffix = [];
21
+ while (path.dirname(cursor) !== cursor && !(await pathExists(cursor))) {
22
+ missingSuffix.unshift(path.basename(cursor));
23
+ cursor = path.dirname(cursor);
24
+ }
25
+ if (!(await pathExists(cursor))) {
26
+ return normalized;
27
+ }
28
+ try {
29
+ const resolvedAncestor = path.resolve(await fs.realpath(cursor));
30
+ return missingSuffix.length === 0
31
+ ? resolvedAncestor
32
+ : path.resolve(resolvedAncestor, ...missingSuffix);
33
+ }
34
+ catch {
35
+ return normalized;
36
+ }
37
+ }
38
+ async function comparablePaths(rawPath) {
39
+ assertNoNulPathInput(rawPath, "path contains a NUL byte");
40
+ const resolved = path.resolve(rawPath);
41
+ return new Set([resolved, await resolvePathViaExistingAncestor(resolved)]);
42
+ }
43
+ function isSamePath(left, right) {
44
+ return isPathInside(left, right) && isPathInside(right, left);
45
+ }
46
+ function hasPolicyEntries(policy) {
47
+ return Boolean(policy?.paths?.length || policy?.prefixes?.length);
48
+ }
49
+ function policyPathEntries(entries) {
50
+ const paths = [];
51
+ for (const entry of entries ?? []) {
52
+ if (entry.length === 0) {
53
+ throw new FsSafeError("invalid-path", "deny mutation paths must be non-empty");
54
+ }
55
+ assertNoNulPathInput(entry, "deny mutation path contains a NUL byte");
56
+ if (!path.isAbsolute(entry)) {
57
+ throw new FsSafeError("invalid-path", "deny mutation paths must be absolute");
58
+ }
59
+ paths.push(entry);
60
+ }
61
+ return paths;
62
+ }
63
+ export async function assertMutationNotDenied(filePath, policy, options = {}) {
64
+ if (!hasPolicyEntries(policy)) {
65
+ return;
66
+ }
67
+ const targetPaths = await comparablePaths(filePath);
68
+ for (const deniedPath of policyPathEntries(policy.paths)) {
69
+ const deniedPaths = await comparablePaths(deniedPath);
70
+ for (const target of targetPaths) {
71
+ for (const denied of deniedPaths) {
72
+ if (isSamePath(denied, target) ||
73
+ (options.protectAncestors === true && isPathInside(target, denied))) {
74
+ throw new FsSafeError("denied-path", "path is denied by denyMutations policy");
75
+ }
76
+ }
77
+ }
78
+ }
79
+ for (const deniedPrefix of policyPathEntries(policy.prefixes)) {
80
+ const deniedPaths = await comparablePaths(deniedPrefix);
81
+ for (const target of targetPaths) {
82
+ for (const denied of deniedPaths) {
83
+ if (isPathInside(denied, target) ||
84
+ (options.protectAncestors === true && isPathInside(target, denied))) {
85
+ throw new FsSafeError("denied-path", "path is denied by denyMutations policy");
86
+ }
87
+ }
88
+ }
89
+ }
90
+ }
91
+ export function mergeDenyMutationPolicies(defaultPolicy, callPolicy) {
92
+ if (!defaultPolicy) {
93
+ return callPolicy;
94
+ }
95
+ if (!callPolicy) {
96
+ return defaultPolicy;
97
+ }
98
+ return {
99
+ paths: [...(defaultPolicy.paths ?? []), ...(callPolicy.paths ?? [])],
100
+ prefixes: [...(defaultPolicy.prefixes ?? []), ...(callPolicy.prefixes ?? [])],
101
+ };
102
+ }
package/dist/errors.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- export type FsSafeErrorCode = "already-exists" | "hardlink" | "helper-failed" | "helper-unavailable" | "invalid-path" | "insecure-permissions" | "not-empty" | "not-file" | "not-found" | "not-owned" | "not-removable" | "outside-workspace" | "path-alias" | "path-mismatch" | "permission-unverified" | "symlink" | "timeout" | "too-large" | "unsupported-platform";
1
+ export type FsSafeErrorCode = "already-exists" | "denied-path" | "hardlink" | "helper-failed" | "helper-unavailable" | "invalid-path" | "insecure-permissions" | "not-empty" | "not-file" | "not-found" | "not-owned" | "not-removable" | "outside-workspace" | "path-alias" | "path-mismatch" | "permission-unverified" | "symlink" | "timeout" | "too-large" | "unsupported-platform";
2
2
  export type FsSafeErrorCategory = "policy" | "operational";
3
3
  export declare function categorizeFsSafeError(code: FsSafeErrorCode): FsSafeErrorCategory;
4
4
  export declare class FsSafeError extends Error {
@@ -1 +1 @@
1
- {"version":3,"file":"errors.d.ts","sourceRoot":"","sources":["../src/errors.ts"],"names":[],"mappings":"AAAA,MAAM,MAAM,eAAe,GACvB,gBAAgB,GAChB,UAAU,GACV,eAAe,GACf,oBAAoB,GACpB,cAAc,GACd,sBAAsB,GACtB,WAAW,GACX,UAAU,GACV,WAAW,GACX,WAAW,GACX,eAAe,GACf,mBAAmB,GACnB,YAAY,GACZ,eAAe,GACf,uBAAuB,GACvB,SAAS,GACT,SAAS,GACT,WAAW,GACX,sBAAsB,CAAC;AAE3B,MAAM,MAAM,mBAAmB,GAAG,QAAQ,GAAG,aAAa,CAAC;AAU3D,wBAAgB,qBAAqB,CAAC,IAAI,EAAE,eAAe,GAAG,mBAAmB,CAEhF;AAED,qBAAa,WAAY,SAAQ,KAAK;IACpC,QAAQ,CAAC,IAAI,EAAE,eAAe,CAAC;IAC/B,QAAQ,CAAC,QAAQ,EAAE,mBAAmB,CAAC;gBAE3B,IAAI,EAAE,eAAe,EAAE,OAAO,EAAE,MAAM,EAAE,OAAO,GAAE;QAAE,KAAK,CAAC,EAAE,OAAO,CAAA;KAAO;CAMtF"}
1
+ {"version":3,"file":"errors.d.ts","sourceRoot":"","sources":["../src/errors.ts"],"names":[],"mappings":"AAAA,MAAM,MAAM,eAAe,GACvB,gBAAgB,GAChB,aAAa,GACb,UAAU,GACV,eAAe,GACf,oBAAoB,GACpB,cAAc,GACd,sBAAsB,GACtB,WAAW,GACX,UAAU,GACV,WAAW,GACX,WAAW,GACX,eAAe,GACf,mBAAmB,GACnB,YAAY,GACZ,eAAe,GACf,uBAAuB,GACvB,SAAS,GACT,SAAS,GACT,WAAW,GACX,sBAAsB,CAAC;AAE3B,MAAM,MAAM,mBAAmB,GAAG,QAAQ,GAAG,aAAa,CAAC;AAU3D,wBAAgB,qBAAqB,CAAC,IAAI,EAAE,eAAe,GAAG,mBAAmB,CAEhF;AAED,qBAAa,WAAY,SAAQ,KAAK;IACpC,QAAQ,CAAC,IAAI,EAAE,eAAe,CAAC;IAC/B,QAAQ,CAAC,QAAQ,EAAE,mBAAmB,CAAC;gBAE3B,IAAI,EAAE,eAAe,EAAE,OAAO,EAAE,MAAM,EAAE,OAAO,GAAE;QAAE,KAAK,CAAC,EAAE,OAAO,CAAA;KAAO;CAMtF"}
package/dist/index.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  export { FsSafeError, categorizeFsSafeError, type FsSafeErrorCategory, type FsSafeErrorCode, } from "./errors.js";
2
- export { DEFAULT_ROOT_MAX_BYTES, 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.js";
2
+ export { DEFAULT_ROOT_MAX_BYTES, 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.js";
3
3
  export { configureFsSafePython, getFsSafePythonConfig, type FsSafePythonConfig, type FsSafePythonMode, } from "./pinned-python-config.js";
4
4
  export { writeExternalFileWithinRoot, type ExternalFileWriteOptions, type ExternalFileWriteResult, } from "./output.js";
5
5
  export { configureFsSafeLocks, getFsSafeLockConfig, type FsSafeLockConfig, } from "./lock-config.js";
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,WAAW,EACX,qBAAqB,EACrB,KAAK,mBAAmB,EACxB,KAAK,eAAe,GACrB,MAAM,aAAa,CAAC;AACrB,OAAO,EACL,sBAAsB,EACtB,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,WAAW,CAAC;AACnB,OAAO,EACL,qBAAqB,EACrB,qBAAqB,EACrB,KAAK,kBAAkB,EACvB,KAAK,gBAAgB,GACtB,MAAM,2BAA2B,CAAC;AACnC,OAAO,EACL,2BAA2B,EAC3B,KAAK,wBAAwB,EAC7B,KAAK,uBAAuB,GAC7B,MAAM,aAAa,CAAC;AACrB,OAAO,EACL,oBAAoB,EACpB,mBAAmB,EACnB,KAAK,gBAAgB,GACtB,MAAM,kBAAkB,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,WAAW,EACX,qBAAqB,EACrB,KAAK,mBAAmB,EACxB,KAAK,eAAe,GACrB,MAAM,aAAa,CAAC;AACrB,OAAO,EACL,sBAAsB,EACtB,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,WAAW,CAAC;AACnB,OAAO,EACL,qBAAqB,EACrB,qBAAqB,EACrB,KAAK,kBAAkB,EACvB,KAAK,gBAAgB,GACtB,MAAM,2BAA2B,CAAC;AACnC,OAAO,EACL,2BAA2B,EAC3B,KAAK,wBAAwB,EAC7B,KAAK,uBAAuB,GAC7B,MAAM,aAAa,CAAC;AACrB,OAAO,EACL,oBAAoB,EACpB,mBAAmB,EACnB,KAAK,gBAAgB,GACtB,MAAM,kBAAkB,CAAC"}
@@ -1 +1 @@
1
- {"version":3,"file":"json.d.ts","sourceRoot":"","sources":["../src/json.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"json.d.ts","sourceRoot":"","sources":["../src/json.ts"],"names":[],"mappings":"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"}
package/dist/json.js CHANGED
@@ -1,10 +1,49 @@
1
1
  import { randomUUID } from "node:crypto";
2
2
  import fsSync from "node:fs";
3
3
  import path from "node:path";
4
+ import { FsSafeError } from "./errors.js";
4
5
  import { stringifyJsonDocument } from "./json-stringify.js";
5
- import { readRegularFile, readRegularFileSync } from "./regular-file.js";
6
+ import { readRegularFile, readRegularFileSync, statRegularFile } from "./regular-file.js";
6
7
  import { openRootFileSync } from "./root-file.js";
7
8
  import { writeTextAtomic } from "./text-atomic.js";
9
+ const READ_RETRY_MAX_ATTEMPTS = 5;
10
+ const READ_RETRY_BASE_DELAY_MS = 50;
11
+ function isRetryableReadError(err, options) {
12
+ if (err instanceof FsSafeError && err.code === "path-mismatch") {
13
+ return true;
14
+ }
15
+ if (options.retryOpenRaceErrors !== true) {
16
+ return false;
17
+ }
18
+ const code = getErrorCode(err);
19
+ return code === "ENOENT" || code === "EPERM";
20
+ }
21
+ function sleep(ms) {
22
+ return new Promise((resolve) => setTimeout(resolve, ms));
23
+ }
24
+ async function readRegularFileWithRetry(filePath, options = {}) {
25
+ let lastErr;
26
+ for (let attempt = 0; attempt < READ_RETRY_MAX_ATTEMPTS; attempt++) {
27
+ try {
28
+ return (await readRegularFile({ filePath })).buffer;
29
+ }
30
+ catch (err) {
31
+ lastErr = err;
32
+ if (!isRetryableReadError(err, options) || attempt === READ_RETRY_MAX_ATTEMPTS - 1) {
33
+ throw err;
34
+ }
35
+ await sleep(READ_RETRY_BASE_DELAY_MS * Math.pow(2, attempt));
36
+ }
37
+ }
38
+ throw lastErr;
39
+ }
40
+ async function readRegularFileIfExistsWithRetry(filePath) {
41
+ const initial = await statRegularFile(filePath);
42
+ if (initial.missing) {
43
+ return null;
44
+ }
45
+ return await readRegularFileWithRetry(filePath, { retryOpenRaceErrors: true });
46
+ }
8
47
  const JSON_FILE_MODE = 0o600;
9
48
  const JSON_DIR_MODE = 0o700;
10
49
  const SUPPORTS_SYNC_NOFOLLOW = process.platform !== "win32" && "O_NOFOLLOW" in fsSync.constants;
@@ -200,7 +239,11 @@ export function readRootJsonObjectSync(options) {
200
239
  }
201
240
  export async function tryReadJson(filePath) {
202
241
  try {
203
- const raw = (await readRegularFile({ filePath })).buffer.toString("utf8");
242
+ const buffer = await readRegularFileIfExistsWithRetry(filePath);
243
+ if (buffer === null) {
244
+ return null;
245
+ }
246
+ const raw = buffer.toString("utf8");
204
247
  return JSON.parse(raw);
205
248
  }
206
249
  catch {
@@ -210,7 +253,7 @@ export async function tryReadJson(filePath) {
210
253
  export async function readJson(filePath) {
211
254
  let raw;
212
255
  try {
213
- raw = (await readRegularFile({ filePath })).buffer.toString("utf8");
256
+ raw = (await readRegularFileWithRetry(filePath, { retryOpenRaceErrors: true })).toString("utf8");
214
257
  }
215
258
  catch (err) {
216
259
  throw new JsonFileReadError(filePath, "read", err);
@@ -225,7 +268,11 @@ export async function readJson(filePath) {
225
268
  export async function readJsonIfExists(filePath) {
226
269
  let raw;
227
270
  try {
228
- raw = (await readRegularFile({ filePath })).buffer.toString("utf8");
271
+ const buffer = await readRegularFileIfExistsWithRetry(filePath);
272
+ if (buffer === null) {
273
+ return null;
274
+ }
275
+ raw = buffer.toString("utf8");
229
276
  }
230
277
  catch (err) {
231
278
  if (getErrorCode(err) === "ENOENT") {
@@ -0,0 +1,3 @@
1
+ import type { FileHandle } from "node:fs/promises";
2
+ export declare function resolveOpenedFileRealPathForHandle(handle: FileHandle, ioPath: string): Promise<string>;
3
+ //# sourceMappingURL=opened-realpath.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"opened-realpath.d.ts","sourceRoot":"","sources":["../src/opened-realpath.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,kBAAkB,CAAC;AAMnD,wBAAsB,kCAAkC,CACtD,MAAM,EAAE,UAAU,EAClB,MAAM,EAAE,MAAM,GACb,OAAO,CAAC,MAAM,CAAC,CAoCjB"}
@@ -0,0 +1,79 @@
1
+ import fs from "node:fs/promises";
2
+ import path from "node:path";
3
+ import { FsSafeError } from "./errors.js";
4
+ import { sameFileIdentity } from "./file-identity.js";
5
+ import { isNotFoundPathError } from "./path.js";
6
+ export async function resolveOpenedFileRealPathForHandle(handle, ioPath) {
7
+ const handleStat = await handle.stat();
8
+ const fdCandidates = process.platform === "linux"
9
+ ? [`/proc/self/fd/${handle.fd}`, `/dev/fd/${handle.fd}`]
10
+ : process.platform === "win32"
11
+ ? []
12
+ : [`/dev/fd/${handle.fd}`];
13
+ for (const fdPath of fdCandidates) {
14
+ try {
15
+ const fdRealPath = await fs.realpath(fdPath);
16
+ const fdRealStat = await fs.stat(fdRealPath);
17
+ if (sameFileIdentity(handleStat, fdRealStat)) {
18
+ return fdRealPath;
19
+ }
20
+ }
21
+ catch {
22
+ // try next fd path
23
+ }
24
+ }
25
+ try {
26
+ const ioRealPath = await fs.realpath(ioPath);
27
+ const ioRealStat = await fs.stat(ioRealPath);
28
+ if (sameFileIdentity(handleStat, ioRealStat)) {
29
+ return ioRealPath;
30
+ }
31
+ }
32
+ catch (err) {
33
+ if (!isNotFoundPathError(err)) {
34
+ throw err;
35
+ }
36
+ }
37
+ const parentResolved = await resolveOpenedFileRealPathFromParent(handleStat, ioPath);
38
+ if (parentResolved) {
39
+ return parentResolved;
40
+ }
41
+ throw new FsSafeError("path-mismatch", "unable to resolve opened file path");
42
+ }
43
+ async function resolveOpenedFileRealPathFromParent(handleStat, ioPath) {
44
+ let parentReal;
45
+ try {
46
+ parentReal = await fs.realpath(path.dirname(ioPath));
47
+ }
48
+ catch (err) {
49
+ if (isNotFoundPathError(err)) {
50
+ return null;
51
+ }
52
+ throw err;
53
+ }
54
+ let entries;
55
+ try {
56
+ entries = await fs.readdir(parentReal);
57
+ }
58
+ catch (err) {
59
+ if (isNotFoundPathError(err)) {
60
+ return null;
61
+ }
62
+ throw err;
63
+ }
64
+ for (const entry of entries.toSorted()) {
65
+ const candidatePath = path.join(parentReal, entry);
66
+ try {
67
+ const candidateStat = await fs.lstat(candidatePath);
68
+ if (candidateStat.isFile() && sameFileIdentity(handleStat, candidateStat)) {
69
+ return await fs.realpath(candidatePath);
70
+ }
71
+ }
72
+ catch (err) {
73
+ if (!isNotFoundPathError(err)) {
74
+ throw err;
75
+ }
76
+ }
77
+ }
78
+ return null;
79
+ }
@@ -0,0 +1,18 @@
1
+ import type { Stats } from "node:fs";
2
+ import type { FileHandle } from "node:fs/promises";
3
+ export type ReadResult = {
4
+ buffer: Buffer;
5
+ realPath: string;
6
+ stat: Stats;
7
+ };
8
+ type OpenedFile = {
9
+ handle: FileHandle;
10
+ realPath: string;
11
+ stat: Stats;
12
+ };
13
+ export declare function readOpenedFileSafely(params: {
14
+ opened: OpenedFile;
15
+ maxBytes?: number;
16
+ }): Promise<ReadResult>;
17
+ export {};
18
+ //# sourceMappingURL=read-opened-file.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"read-opened-file.d.ts","sourceRoot":"","sources":["../src/read-opened-file.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,SAAS,CAAC;AACrC,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,kBAAkB,CAAC;AAGnD,MAAM,MAAM,UAAU,GAAG;IACvB,MAAM,EAAE,MAAM,CAAC;IACf,QAAQ,EAAE,MAAM,CAAC;IACjB,IAAI,EAAE,KAAK,CAAC;CACb,CAAC;AAEF,KAAK,UAAU,GAAG;IAChB,MAAM,EAAE,UAAU,CAAC;IACnB,QAAQ,EAAE,MAAM,CAAC;IACjB,IAAI,EAAE,KAAK,CAAC;CACb,CAAC;AAEF,wBAAsB,oBAAoB,CAAC,MAAM,EAAE;IACjD,MAAM,EAAE,UAAU,CAAC;IACnB,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB,GAAG,OAAO,CAAC,UAAU,CAAC,CAmBtB"}
@@ -0,0 +1,15 @@
1
+ import { FsSafeError } from "./errors.js";
2
+ export async function readOpenedFileSafely(params) {
3
+ if (params.maxBytes !== undefined && params.opened.stat.size > params.maxBytes) {
4
+ throw new FsSafeError("too-large", `file exceeds limit of ${params.maxBytes} bytes (got ${params.opened.stat.size})`);
5
+ }
6
+ const buffer = await params.opened.handle.readFile();
7
+ if (params.maxBytes !== undefined && buffer.byteLength > params.maxBytes) {
8
+ throw new FsSafeError("too-large", `file exceeds limit of ${params.maxBytes} bytes (got ${buffer.byteLength})`);
9
+ }
10
+ return {
11
+ buffer,
12
+ realPath: params.opened.realPath,
13
+ stat: params.opened.stat,
14
+ };
15
+ }
@@ -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;AAQ7B,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,CAgC3C;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,CAoBA;AAmBD,wBAAsB,iBAAiB,CAAC,OAAO,EAAE,wBAAwB,GAAG,OAAO,CAAC,IAAI,CAAC,CAuDxF;AAED,wBAAgB,qBAAqB,CAAC,OAAO,EAAE,wBAAwB,GAAG,IAAI,CA2D7E"}
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;AAS7B,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,CAiD3C;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,CAoBA;AAmBD,wBAAsB,iBAAiB,CAAC,OAAO,EAAE,wBAAwB,GAAG,OAAO,CAAC,IAAI,CAAC,CAuDxF;AAED,wBAAgB,qBAAqB,CAAC,OAAO,EAAE,wBAAwB,GAAG,IAAI,CA2D7E"}
@@ -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 { FsSafeError } from "./errors.js";
4
5
  import { sameFileIdentity } from "./file-identity.js";
5
6
  import { isNotFoundPathError } from "./path.js";
6
7
  import { assertNoSymlinkParents, assertNoSymlinkParentsSync } from "./symlink-parents.js";
@@ -95,12 +96,31 @@ export async function readRegularFile(params) {
95
96
  if (params.maxBytes !== undefined && result.stat.size > params.maxBytes) {
96
97
  throw new Error(`File exceeds ${params.maxBytes} bytes: ${params.filePath}`);
97
98
  }
98
- const handle = await fs.open(params.filePath, resolveRegularFileReadFlags());
99
+ let handle;
100
+ try {
101
+ handle = await fs.open(params.filePath, resolveRegularFileReadFlags());
102
+ }
103
+ catch (err) {
104
+ if (isNotFoundPathError(err)) {
105
+ throw new FsSafeError("path-mismatch", `File changed during read: ${params.filePath}`);
106
+ }
107
+ throw err;
108
+ }
99
109
  try {
100
110
  const stat = await handle.stat();
111
+ let pathStat;
112
+ try {
113
+ pathStat = await fs.lstat(params.filePath);
114
+ }
115
+ catch (err) {
116
+ if (isNotFoundPathError(err)) {
117
+ throw new FsSafeError("path-mismatch", `File changed during read: ${params.filePath}`);
118
+ }
119
+ throw err;
120
+ }
101
121
  verifyStableReadTarget({
102
122
  filePath: params.filePath,
103
- pathStat: await fs.lstat(params.filePath),
123
+ pathStat,
104
124
  postOpenStat: stat,
105
125
  preOpenStat: result.stat,
106
126
  });
@@ -126,7 +146,7 @@ function verifyStableReadTarget(params) {
126
146
  }
127
147
  if (!sameFileIdentity(params.preOpenStat, params.postOpenStat) ||
128
148
  !sameFileIdentity(params.pathStat, params.postOpenStat)) {
129
- throw new Error(`File changed during read: ${params.filePath}`);
149
+ throw new FsSafeError("path-mismatch", `File changed during read: ${params.filePath}`);
130
150
  }
131
151
  }
132
152
  function readOpenedRegularFileSync(params) {
@@ -1,17 +1,17 @@
1
1
  import type { Stats } from "node:fs";
2
2
  import type { FileHandle } from "node:fs/promises";
3
+ import { type DenyMutationPolicy } from "./deny-mutations.js";
4
+ import { type ReadResult } from "./read-opened-file.js";
3
5
  import type { DirEntry, PathStat } from "./types.js";
6
+ export type { DenyMutationPolicy } from "./deny-mutations.js";
7
+ export { resolveOpenedFileRealPathForHandle } from "./opened-realpath.js";
8
+ export type { ReadResult } from "./read-opened-file.js";
4
9
  export type OpenResult = {
5
10
  handle: FileHandle;
6
11
  realPath: string;
7
12
  stat: Stats;
8
13
  [Symbol.asyncDispose](): Promise<void>;
9
14
  };
10
- export type ReadResult = {
11
- buffer: Buffer;
12
- realPath: string;
13
- stat: Stats;
14
- };
15
15
  export type RootOptions = {
16
16
  rootDir: string;
17
17
  defaults?: RootDefaults;
@@ -24,19 +24,20 @@ export type RootDefaults = {
24
24
  maxBytes?: number;
25
25
  mkdir?: boolean;
26
26
  mode?: number;
27
+ denyMutations?: DenyMutationPolicy;
27
28
  nonBlockingRead?: boolean;
28
29
  symlinks?: SymlinkPolicy;
29
30
  };
30
31
  export type RootReadOptions = Pick<RootDefaults, "hardlinks" | "maxBytes" | "nonBlockingRead" | "symlinks">;
31
32
  export type RootOpenOptions = Omit<RootReadOptions, "maxBytes">;
32
- export type RootWriteOptions = Pick<RootDefaults, "mkdir" | "mode"> & {
33
+ export type RootWriteOptions = Pick<RootDefaults, "denyMutations" | "mkdir" | "mode"> & {
33
34
  encoding?: BufferEncoding;
34
35
  overwrite?: boolean;
35
36
  };
36
- export type RootOpenWritableOptions = Pick<RootDefaults, "mkdir" | "mode"> & {
37
+ export type RootOpenWritableOptions = Pick<RootDefaults, "denyMutations" | "mkdir" | "mode"> & {
37
38
  writeMode?: WritableOpenMode;
38
39
  };
39
- export type RootCopyOptions = Pick<RootDefaults, "maxBytes" | "mkdir" | "mode"> & {
40
+ export type RootCopyOptions = Pick<RootDefaults, "denyMutations" | "maxBytes" | "mkdir" | "mode"> & {
40
41
  sourceHardlinks?: HardlinkPolicy;
41
42
  };
42
43
  export type RootWriteJsonOptions = RootWriteOptions & {
@@ -49,6 +50,11 @@ export type RootCreateJsonOptions = Omit<RootWriteJsonOptions, "overwrite">;
49
50
  export type RootAppendOptions = RootWriteOptions & {
50
51
  prependNewlineIfNeeded?: boolean;
51
52
  };
53
+ export type RootMoveOptions = Pick<RootDefaults, "denyMutations"> & {
54
+ overwrite?: boolean;
55
+ };
56
+ export type RootRemoveOptions = Pick<RootDefaults, "denyMutations">;
57
+ export type RootMkdirOptions = Pick<RootDefaults, "denyMutations">;
52
58
  export declare const DEFAULT_ROOT_MAX_BYTES: number;
53
59
  export interface Root {
54
60
  readonly rootDir: string;
@@ -69,9 +75,9 @@ export interface Root {
69
75
  reader(options?: RootReadOptions): (filePath: string) => Promise<Buffer>;
70
76
  openWritable(relativePath: string, options?: RootOpenWritableOptions): Promise<WritableOpenResult>;
71
77
  append(relativePath: string, data: string | Buffer, options?: RootAppendOptions): Promise<void>;
72
- remove(relativePath: string): Promise<void>;
73
- mkdir(relativePath: string): Promise<void>;
74
- ensureRoot(): Promise<void>;
78
+ remove(relativePath: string, options?: RootRemoveOptions): Promise<void>;
79
+ mkdir(relativePath: string, options?: RootMkdirOptions): Promise<void>;
80
+ ensureRoot(options?: RootMkdirOptions): Promise<void>;
75
81
  write(relativePath: string, data: string | Buffer, options?: RootWriteOptions): Promise<void>;
76
82
  create(relativePath: string, data: string | Buffer, options?: RootCreateOptions): Promise<void>;
77
83
  writeJson(relativePath: string, data: unknown, options?: RootWriteJsonOptions): Promise<void>;
@@ -85,9 +91,7 @@ export interface Root {
85
91
  list(relativePath: string, options: {
86
92
  withFileTypes: true;
87
93
  }): Promise<DirEntry[]>;
88
- move(fromRelative: string, toRelative: string, options?: {
89
- overwrite?: boolean;
90
- }): Promise<void>;
94
+ move(fromRelative: string, toRelative: string, options?: RootMoveOptions): Promise<void>;
91
95
  }
92
96
  export declare function root(rootDir: string, defaults?: RootDefaults): Promise<Root>;
93
97
  export declare function readLocalFileSafely(params: {
@@ -104,5 +108,4 @@ export type WritableOpenResult = {
104
108
  stat: Stats;
105
109
  [Symbol.asyncDispose](): Promise<void>;
106
110
  };
107
- export declare function resolveOpenedFileRealPathForHandle(handle: FileHandle, ioPath: string): Promise<string>;
108
111
  //# sourceMappingURL=root-impl.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"root-impl.d.ts","sourceRoot":"","sources":["../src/root-impl.ts"],"names":[],"mappings":"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"}
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
@@ -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, relativePath);
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, { relativePath });
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, { relativePath: "", allowRoot: true });
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, relativePath) {
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.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
@@ -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/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/root.md CHANGED
@@ -19,17 +19,23 @@ function root(rootDir: string, defaults?: RootDefaults): Promise<Root>;
19
19
 
20
20
  type RootDefaults = {
21
21
  hardlinks?: "reject" | "allow"; // refuse files with nlink > 1 on read; defaults to "reject"
22
+ denyMutations?: DenyMutationPolicy; // absolute paths/prefixes mutation methods may not change
22
23
  maxBytes?: number; // refuse reads larger than this many bytes; defaults to 16 MiB
23
24
  mkdir?: boolean; // create missing parent dirs on write/openWritable/append
24
25
  mode?: number; // file mode applied to new writes; per-call override available
25
26
  nonBlockingRead?: boolean; // schedule reads on a worker; useful for large files
26
27
  symlinks?: "reject" | "follow-within-root"; // policy when a path component is a symlink
27
28
  };
29
+
30
+ type DenyMutationPolicy = {
31
+ paths?: readonly string[];
32
+ prefixes?: readonly string[];
33
+ };
28
34
  ```
29
35
 
30
36
  `root()` resolves the directory through the real filesystem. A symlinked input becomes the canonical path; a non-existent root throws `FsSafeError` with code `not-found`, and malformed or non-directory roots throw `invalid-path`.
31
37
 
32
- `defaults` apply to every method on the returned handle. Per-call options on individual methods override the defaults for that call only.
38
+ `defaults` apply to every method on the returned handle. Per-call options on individual methods override the defaults for that call only, except `denyMutations`: root and per-call deny entries are merged so a call cannot clear a root-level deny.
33
39
 
34
40
  ## The `Root` interface
35
41
 
@@ -69,9 +75,9 @@ fs.append(rel, data, options?) // append text/buffer; respects mkdir d
69
75
  fs.copyIn(rel, sourceAbsPath, options?) // copy from outside the root, atomically, with size cap
70
76
  fs.openWritable(rel, options?) // FileHandle for streaming writes; supports await using
71
77
  fs.move(from, to, options?) // rename within the root; defaults to no clobber
72
- fs.remove(rel) // unlink file or rmdir empty directory
73
- fs.mkdir(rel) // mkdir -p (creates missing parents)
74
- fs.ensureRoot() // accepts "" / "." as the root itself
78
+ fs.remove(rel, options?) // unlink file or rmdir empty directory
79
+ fs.mkdir(rel, options?) // mkdir -p (creates missing parents)
80
+ fs.ensureRoot(options?) // accepts "" / "." as the root itself
75
81
  ```
76
82
 
77
83
  `write`, `create`, `append`, `writeJson`, and `createJson` accept `mode?: number`; use `0o600` for credentials and other private state. `writeJson` also accepts the same options as `JSON.stringify` plus `trailingNewline?: boolean` (defaults `true` so the file ends in `\n`).
@@ -80,6 +86,8 @@ fs.ensureRoot() // accepts "" / "." as the root itself
80
86
 
81
87
  `openWritable` opens a writable file with options `mode?: number` and `writeMode?: "replace" | "append" | "update"`. `replace` truncates existing files and is the default; `update` keeps existing contents. Use it for streaming output. Prefer `await using` for cleanup.
82
88
 
89
+ All mutation methods accept `denyMutations?: { paths?: string[]; prefixes?: string[] }`. Entries must be absolute paths. `paths` blocks those exact paths; `prefixes` blocks those paths and their descendants. fs-safe preserves path strings exactly and canonicalizes through existing ancestors before comparing, so a symlinked ancestor to a denied location is still denied. Denied mutations throw `FsSafeError` with code `denied-path`. Use this for caller-specific sensitive paths, not as a replacement for the root boundary, symlink, or hardlink checks.
90
+
83
91
  ### Inspection (advisory)
84
92
 
85
93
  ```ts
@@ -132,6 +140,7 @@ Every method throws `FsSafeError` with a `code`. Branch on `err.code`, not messa
132
140
  | `not-found` | The target does not exist (or its parent does not, with `mkdir: false`). |
133
141
  | `not-file` | A read or copy targeted a non-regular file (directory, FIFO, socket, …). |
134
142
  | `already-exists` | `create()` or `move()` without `overwrite` hit an existing target. |
143
+ | `denied-path` | A mutation target matched `denyMutations.paths` or `denyMutations.prefixes`. |
135
144
  | `symlink` | A path component is a symlink, and the call's `symlinks` policy is `reject`. |
136
145
  | `hardlink` | The target's `nlink > 1` and `hardlinks` policy is `reject`. |
137
146
  | `path-mismatch` | Post-open identity check failed — the opened fd does not match the resolved path. |
@@ -50,6 +50,10 @@ When `hardlinks: "reject"` is set, reads stat the target and refuse if `nlink >
50
50
 
51
51
  `resolve()`, `exists()`, `stat()`, and `list()` are explicitly **not** race-resistant — they answer a question and return. To act on a path with race resistance, use `read()`, `open()`, `write()`, `create()`, `copyIn()`, `move()`, or `remove()`. They re-pin the path identity at the point of use.
52
52
 
53
+ ### Denied mutations
54
+
55
+ `denyMutations` is an opt-in application policy for `root()` mutation methods. It blocks exact absolute paths with `paths` and whole subtrees with `prefixes`, merging root defaults with per-call entries so a call cannot clear root-level denies. This is not an OS permission boundary: code with access to `node:fs`, a shell, or another process with the same filesystem privileges can bypass it.
56
+
53
57
  ### Atomic writes
54
58
 
55
59
  `replaceFileAtomic` writes to a sibling temp file in the destination directory, optionally `fsync`s it, optionally `fsync`s the parent directory after rename, and atomically renames over the destination. On failure mid-write, the destination is either the old contents (rename never happened) or the new contents (rename succeeded). There is no half-written intermediate state visible at the destination path.
package/docs/types.md CHANGED
@@ -82,6 +82,7 @@ type ReadResult = {
82
82
 
83
83
  ```ts
84
84
  type RootDefaults = {
85
+ denyMutations?: DenyMutationPolicy;
85
86
  hardlinks?: "reject" | "allow";
86
87
  maxBytes?: number;
87
88
  mkdir?: boolean;
@@ -90,26 +91,31 @@ type RootDefaults = {
90
91
  symlinks?: "reject" | "follow-within-root";
91
92
  };
92
93
 
94
+ type DenyMutationPolicy = {
95
+ paths?: readonly string[];
96
+ prefixes?: readonly string[];
97
+ };
98
+
93
99
  type RootOptions = {
94
100
  rootDir: string;
95
101
  defaults?: RootDefaults;
96
102
  };
97
103
  ```
98
104
 
99
- `RootDefaults` is what `root(rootDir, defaults)` accepts. See [`root()`](root.md) for the per-method options that override these.
105
+ `RootDefaults` is what `root(rootDir, defaults)` accepts. See [`root()`](root.md) for the per-method options that override these. `denyMutations` is the exception: root and per-call deny entries are merged.
100
106
 
101
107
  ## `RootReadOptions` / `RootWriteOptions` / `RootCopyOptions`
102
108
 
103
109
  ```ts
104
110
  type RootReadOptions = Pick<RootDefaults, "hardlinks" | "maxBytes" | "nonBlockingRead" | "symlinks">;
105
- type RootWriteOptions = Pick<RootDefaults, "mkdir" | "mode"> & {
111
+ type RootWriteOptions = Pick<RootDefaults, "denyMutations" | "mkdir" | "mode"> & {
106
112
  encoding?: BufferEncoding;
107
113
  overwrite?: boolean;
108
114
  };
109
- type RootCopyOptions = Pick<RootDefaults, "maxBytes" | "mkdir" | "mode"> & {
115
+ type RootCopyOptions = Pick<RootDefaults, "denyMutations" | "maxBytes" | "mkdir" | "mode"> & {
110
116
  sourceHardlinks?: "reject" | "allow";
111
117
  };
112
- type RootOpenWritableOptions = Pick<RootDefaults, "mkdir" | "mode"> & {
118
+ type RootOpenWritableOptions = Pick<RootDefaults, "denyMutations" | "mkdir" | "mode"> & {
113
119
  writeMode?: "replace" | "append" | "update";
114
120
  };
115
121
  type RootWriteJsonOptions = RootWriteOptions & {
@@ -120,6 +126,11 @@ type RootWriteJsonOptions = RootWriteOptions & {
120
126
  type RootAppendOptions = RootWriteOptions & {
121
127
  prependNewlineIfNeeded?: boolean;
122
128
  };
129
+ type RootMoveOptions = Pick<RootDefaults, "denyMutations"> & {
130
+ overwrite?: boolean;
131
+ };
132
+ type RootRemoveOptions = Pick<RootDefaults, "denyMutations">;
133
+ type RootMkdirOptions = Pick<RootDefaults, "denyMutations">;
123
134
  ```
124
135
 
125
136
  Per-method option shapes. Each picks the `RootDefaults` keys that apply, plus method-specific extras.
@@ -137,11 +148,12 @@ The two policy unions you'll see throughout. `"reject"` is conservative; `"follo
137
148
 
138
149
  ```ts
139
150
  type FsSafeErrorCode =
140
- | "already-exists" | "hardlink" | "helper-failed" | "helper-unavailable"
141
- | "insecure-permissions" | "invalid-path" | "not-empty" | "not-file"
142
- | "not-found" | "not-owned" | "not-removable" | "outside-workspace"
143
- | "path-alias" | "path-mismatch" | "permission-unverified"
144
- | "symlink" | "timeout" | "too-large" | "unsupported-platform";
151
+ | "already-exists" | "denied-path" | "hardlink" | "helper-failed"
152
+ | "helper-unavailable" | "insecure-permissions" | "invalid-path"
153
+ | "not-empty" | "not-file" | "not-found" | "not-owned"
154
+ | "not-removable" | "outside-workspace" | "path-alias"
155
+ | "path-mismatch" | "permission-unverified" | "symlink"
156
+ | "timeout" | "too-large" | "unsupported-platform";
145
157
  ```
146
158
 
147
159
  Closed union you switch on. See the [Errors](errors.md) reference for what each one means.
package/docs/writing.md CHANGED
@@ -24,6 +24,24 @@ await fs.mkdir("snapshots/2026/05");
24
24
 
25
25
  A failure at any point either leaves the destination at its previous contents or surfaces an `FsSafeError` — never a partially-written file at the destination path.
26
26
 
27
+ ## Denying mutations
28
+
29
+ All mutation verbs accept `denyMutations?: DenyMutationPolicy`, either as a root default or per-call option:
30
+
31
+ ```ts
32
+ const fs = await root("/srv/workspace", {
33
+ denyMutations: {
34
+ paths: ["/srv/workspace/.env"],
35
+ prefixes: ["/srv/workspace/.ssh"],
36
+ },
37
+ });
38
+
39
+ await fs.write(".env", "x"); // throws FsSafeError code "denied-path"
40
+ await fs.remove(".ssh/id_rsa"); // throws FsSafeError code "denied-path"
41
+ ```
42
+
43
+ `paths` blocks exact absolute paths. `prefixes` blocks absolute paths and everything below them. fs-safe preserves path strings exactly and canonicalizes through existing ancestors before comparing, so a mutation through a symlinked ancestor to a denied path is still denied. Root-level and per-call policies are additive; per-call policy can add denies, but cannot clear root defaults.
44
+
27
45
  ## Write verbs
28
46
 
29
47
  ### `fs.write(rel, data, options?)`
@@ -35,7 +53,7 @@ await fs.write("state/last-run.json", JSON.stringify(run));
35
53
  await fs.write("notes/today.txt", "hello\n", { encoding: "utf8" });
36
54
  ```
37
55
 
38
- `data` accepts `string | Buffer`. `options` are `{ encoding?: BufferEncoding; mkdir?: boolean; mode?: number; overwrite?: boolean }`. `mode` sets the file's POSIX mode; if omitted, falls back to the `mode` from `RootDefaults` and then to umask. `overwrite` defaults to `true`; set it to `false` for the same no-clobber behavior as `create()`.
56
+ `data` accepts `string | Buffer`. `options` are `{ denyMutations?: DenyMutationPolicy; encoding?: BufferEncoding; mkdir?: boolean; mode?: number; overwrite?: boolean }`. `mode` sets the file's POSIX mode; if omitted, falls back to the `mode` from `RootDefaults` and then to umask. `overwrite` defaults to `true`; set it to `false` for the same no-clobber behavior as `create()`.
39
57
 
40
58
  ### `fs.create(rel, data, options?)`
41
59
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@openclaw/fs-safe",
3
- "version": "0.2.7",
3
+ "version": "0.3.0",
4
4
  "description": "Capability-style filesystem roots for Node.js apps that handle untrusted relative paths.",
5
5
  "license": "MIT",
6
6
  "repository": {