@openclaw/fs-safe 0.5.1 → 0.5.3
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 +86 -0
- package/README.md +19 -6
- package/dist/absolute-path.d.ts.map +1 -1
- package/dist/absolute-path.js +20 -12
- package/dist/archive-entry.d.ts +1 -0
- package/dist/archive-entry.d.ts.map +1 -1
- package/dist/archive-entry.js +37 -6
- package/dist/archive-errors.d.ts +1 -0
- package/dist/archive-errors.d.ts.map +1 -1
- package/dist/archive-errors.js +4 -0
- package/dist/archive-input.d.ts +7 -0
- package/dist/archive-input.d.ts.map +1 -1
- package/dist/archive-input.js +11 -1
- package/dist/archive-limits.js +1 -1
- package/dist/archive-native.d.ts.map +1 -1
- package/dist/archive-native.js +12 -6
- package/dist/archive-policy.d.ts.map +1 -1
- package/dist/archive-policy.js +2 -1
- package/dist/archive-read.d.ts.map +1 -1
- package/dist/archive-read.js +62 -37
- package/dist/archive-staging.d.ts.map +1 -1
- package/dist/archive-staging.js +3 -2
- package/dist/archive-tar-meta.d.ts.map +1 -1
- package/dist/archive-tar-meta.js +15 -1
- package/dist/archive-tar-runtime.d.ts +1 -0
- package/dist/archive-tar-runtime.d.ts.map +1 -1
- package/dist/archive-tar-runtime.js +11 -0
- package/dist/archive-tar.d.ts.map +1 -1
- package/dist/archive-tar.js +8 -4
- package/dist/archive-zip-entry.d.ts +7 -1
- package/dist/archive-zip-entry.d.ts.map +1 -1
- package/dist/archive-zip-entry.js +11 -1
- package/dist/archive-zip-integrity.d.ts +5 -0
- package/dist/archive-zip-integrity.d.ts.map +1 -0
- package/dist/archive-zip-integrity.js +54 -0
- package/dist/archive-zip-preflight.d.ts.map +1 -1
- package/dist/archive-zip-preflight.js +12 -1
- package/dist/archive.d.ts.map +1 -1
- package/dist/archive.js +9 -5
- package/dist/bounded-read-stream.d.ts.map +1 -1
- package/dist/bounded-read-stream.js +13 -2
- package/dist/deny-mutations.d.ts +1 -0
- package/dist/deny-mutations.d.ts.map +1 -1
- package/dist/deny-mutations.js +6 -39
- package/dist/device-path.d.ts +1 -0
- package/dist/device-path.d.ts.map +1 -1
- package/dist/device-path.js +6 -6
- package/dist/directory-guard.d.ts.map +1 -1
- package/dist/directory-guard.js +5 -4
- package/dist/error-detail.d.ts +2 -0
- package/dist/error-detail.d.ts.map +1 -0
- package/dist/error-detail.js +4 -0
- package/dist/errors.d.ts +1 -1
- package/dist/errors.d.ts.map +1 -1
- package/dist/errors.js +4 -0
- package/dist/file-hash.d.ts.map +1 -1
- package/dist/file-hash.js +4 -1
- package/dist/file-lock-sync.d.ts.map +1 -1
- package/dist/file-lock-sync.js +88 -35
- package/dist/file-store-boundary.d.ts +6 -0
- package/dist/file-store-boundary.d.ts.map +1 -1
- package/dist/file-store-boundary.js +20 -5
- package/dist/file-store-sync-write.d.ts +9 -0
- package/dist/file-store-sync-write.d.ts.map +1 -0
- package/dist/file-store-sync-write.js +97 -0
- package/dist/file-store.d.ts.map +1 -1
- package/dist/file-store.js +62 -139
- package/dist/filename.d.ts.map +1 -1
- package/dist/filename.js +27 -0
- package/dist/guarded-mkdir.d.ts.map +1 -1
- package/dist/guarded-mkdir.js +5 -6
- package/dist/install-path.js +1 -1
- package/dist/json-document-store.d.ts.map +1 -1
- package/dist/json-document-store.js +19 -13
- package/dist/json.d.ts.map +1 -1
- package/dist/json.js +1 -3
- package/dist/local-file-access.d.ts +3 -2
- package/dist/local-file-access.d.ts.map +1 -1
- package/dist/local-file-access.js +12 -6
- package/dist/local-roots.d.ts.map +1 -1
- package/dist/local-roots.js +35 -30
- package/dist/move-path.d.ts.map +1 -1
- package/dist/move-path.js +51 -12
- package/dist/native/darwin-arm64/fs-safe-native.node +0 -0
- package/dist/native/darwin-x64/fs-safe-native.node +0 -0
- package/dist/native/linux-arm64-gnu/fs-safe-native.node +0 -0
- package/dist/native/linux-arm64-musl/fs-safe-native.node +0 -0
- package/dist/native/linux-x64-gnu/fs-safe-native.node +0 -0
- package/dist/native/linux-x64-musl/fs-safe-native.node +0 -0
- package/dist/native/win32-x64-msvc/fs-safe-native.node +0 -0
- package/dist/native-binding.d.ts +1 -0
- package/dist/native-binding.d.ts.map +1 -1
- package/dist/native-operations.d.ts.map +1 -1
- package/dist/native-operations.js +14 -1
- package/dist/native-pinned-write.d.ts.map +1 -1
- package/dist/native-pinned-write.js +44 -17
- package/dist/output-sibling.d.ts.map +1 -1
- package/dist/output-sibling.js +6 -5
- package/dist/path-policy.d.ts.map +1 -1
- package/dist/path-policy.js +1 -7
- package/dist/path.d.ts.map +1 -1
- package/dist/path.js +4 -0
- package/dist/permission-exec.d.ts +6 -0
- package/dist/permission-exec.d.ts.map +1 -0
- package/dist/permission-exec.js +28 -0
- package/dist/permissions-windows.d.ts +56 -0
- package/dist/permissions-windows.d.ts.map +1 -0
- package/dist/permissions-windows.js +285 -0
- package/dist/permissions.d.ts +3 -46
- package/dist/permissions.d.ts.map +1 -1
- package/dist/permissions.js +17 -328
- package/dist/pinned-open.d.ts.map +1 -1
- package/dist/pinned-open.js +21 -4
- package/dist/pinned-write.js +2 -1
- package/dist/private-temp-workspace.d.ts.map +1 -1
- package/dist/private-temp-workspace.js +45 -4
- package/dist/read-error.d.ts +2 -0
- package/dist/read-error.d.ts.map +1 -0
- package/dist/read-error.js +11 -0
- package/dist/regular-file.d.ts.map +1 -1
- package/dist/regular-file.js +21 -2
- package/dist/replace-directory.d.ts.map +1 -1
- package/dist/replace-directory.js +10 -1
- package/dist/replace-file-copy-fallback.d.ts +3 -0
- package/dist/replace-file-copy-fallback.d.ts.map +1 -1
- package/dist/replace-file-copy-fallback.js +18 -11
- package/dist/replace-file-descriptor.d.ts +33 -0
- package/dist/replace-file-descriptor.d.ts.map +1 -0
- package/dist/replace-file-descriptor.js +96 -0
- package/dist/replace-file.d.ts +9 -2
- package/dist/replace-file.d.ts.map +1 -1
- package/dist/replace-file.js +64 -72
- package/dist/root-context.d.ts +8 -0
- package/dist/root-context.d.ts.map +1 -1
- package/dist/root-context.js +43 -2
- package/dist/root-errors.d.ts +7 -0
- package/dist/root-errors.d.ts.map +1 -1
- package/dist/root-errors.js +46 -1
- package/dist/root-file.d.ts +1 -0
- package/dist/root-file.d.ts.map +1 -1
- package/dist/root-file.js +2 -0
- package/dist/root-impl.d.ts.map +1 -1
- package/dist/root-impl.js +216 -169
- package/dist/root-path-symlink.d.ts +3 -0
- package/dist/root-path-symlink.d.ts.map +1 -0
- package/dist/root-path-symlink.js +35 -0
- package/dist/root-path.d.ts +1 -0
- package/dist/root-path.d.ts.map +1 -1
- package/dist/root-path.js +129 -190
- package/dist/root-walk.d.ts.map +1 -1
- package/dist/root-walk.js +15 -0
- package/dist/safe-path-segment.d.ts +2 -0
- package/dist/safe-path-segment.d.ts.map +1 -1
- package/dist/safe-path-segment.js +12 -0
- package/dist/secret-file.d.ts.map +1 -1
- package/dist/secret-file.js +29 -6
- package/dist/secret-read-async.d.ts.map +1 -1
- package/dist/secret-read-async.js +5 -1
- package/dist/secure-temp-dir.d.ts.map +1 -1
- package/dist/secure-temp-dir.js +6 -1
- package/dist/short-path.d.ts +2 -0
- package/dist/short-path.d.ts.map +1 -0
- package/dist/short-path.js +7 -0
- package/dist/sidecar-lock-acquire.d.ts +30 -0
- package/dist/sidecar-lock-acquire.d.ts.map +1 -0
- package/dist/sidecar-lock-acquire.js +260 -0
- package/dist/sidecar-lock-policy.d.ts +3 -0
- package/dist/sidecar-lock-policy.d.ts.map +1 -1
- package/dist/sidecar-lock-policy.js +20 -3
- package/dist/sidecar-lock-reclaim.d.ts +5 -1
- package/dist/sidecar-lock-reclaim.d.ts.map +1 -1
- package/dist/sidecar-lock-reclaim.js +59 -9
- package/dist/sidecar-lock.d.ts.map +1 -1
- package/dist/sidecar-lock.js +27 -226
- package/dist/symlink-parents.d.ts.map +1 -1
- package/dist/symlink-parents.js +9 -8
- package/dist/temp-target.d.ts.map +1 -1
- package/dist/temp-target.js +15 -4
- package/dist/test-hooks.d.ts +1 -0
- package/dist/test-hooks.d.ts.map +1 -1
- package/dist/timing.d.ts +2 -0
- package/dist/timing.d.ts.map +1 -1
- package/dist/timing.js +9 -0
- package/dist/trash.d.ts.map +1 -1
- package/dist/trash.js +10 -1
- package/dist/walk.d.ts.map +1 -1
- package/dist/walk.js +15 -0
- package/docs/advanced.md +7 -1
- package/docs/archive.md +27 -20
- package/docs/assets/readme-banner.jpg +0 -0
- package/docs/atomic.md +48 -19
- package/docs/errors.md +34 -8
- package/docs/file-store.md +14 -1
- package/docs/filename.md +17 -16
- package/docs/index.md +1 -0
- package/docs/install-path.md +9 -6
- package/docs/json.md +8 -2
- package/docs/local-roots.md +79 -103
- package/docs/native-helper.md +11 -5
- package/docs/native.md +2 -2
- package/docs/output.md +8 -3
- package/docs/path-scope.md +4 -1
- package/docs/path.md +7 -3
- package/docs/permissions.md +8 -0
- package/docs/public-api.md +109 -0
- package/docs/quickstart.md +6 -2
- package/docs/reading.md +13 -10
- package/docs/root.md +26 -4
- package/docs/secret-file.md +21 -4
- package/docs/security-model.md +21 -4
- package/docs/sidecar-lock.md +15 -5
- package/docs/temp.md +42 -5
- package/docs/test-hooks.md +2 -0
- package/docs/types.md +32 -15
- package/docs/writing.md +25 -12
- package/package.json +15 -4
- package/dist/archive-file-io.d.ts +0 -9
- package/dist/archive-file-io.d.ts.map +0 -1
- package/dist/archive-file-io.js +0 -11
- package/dist/file-store-source.d.ts +0 -5
- package/dist/file-store-source.d.ts.map +0 -1
- package/dist/file-store-source.js +0 -30
- package/dist/file-url.d.ts +0 -2
- package/dist/file-url.d.ts.map +0 -1
- package/dist/file-url.js +0 -1
- package/dist/path-stat.d.ts +0 -4
- package/dist/path-stat.d.ts.map +0 -1
- package/dist/path-stat.js +0 -15
- package/dist/windows-permissions-native.d.ts +0 -9
- package/dist/windows-permissions-native.d.ts.map +0 -1
- package/dist/windows-permissions-native.js +0 -31
package/docs/local-roots.md
CHANGED
|
@@ -1,11 +1,9 @@
|
|
|
1
1
|
# Local roots
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
- "Resolve this path string to an absolute path that lives inside one of the configured roots, or refuse it."
|
|
8
|
-
- "Read this path, where it could be `/abs/path`, `~/relative`, `file://…`, or simply the basename of a file in one of my roots."
|
|
3
|
+
The local-roots helpers accept a file path plus a list of trusted absolute
|
|
4
|
+
directories and return the first canonical root that contains the file. Use
|
|
5
|
+
them when configuration may name one of several approved media, cache, or
|
|
6
|
+
workspace roots.
|
|
9
7
|
|
|
10
8
|
```ts
|
|
11
9
|
import {
|
|
@@ -14,92 +12,102 @@ import {
|
|
|
14
12
|
} from "@openclaw/fs-safe/advanced";
|
|
15
13
|
```
|
|
16
14
|
|
|
17
|
-
##
|
|
15
|
+
## Input shape
|
|
18
16
|
|
|
19
|
-
Both helpers take
|
|
17
|
+
Both helpers take one options object. `filePath` may be absolute, home-relative,
|
|
18
|
+
relative to the current working directory, or a local `file://` URL. Relative
|
|
19
|
+
inputs are resolved exactly as Node resolves them; they are not searched as a
|
|
20
|
+
basename under each root.
|
|
20
21
|
|
|
21
22
|
```ts
|
|
22
23
|
type LocalRootsInputOptions = {
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
expandHome?: boolean; // expand ~ in inputs (default true)
|
|
24
|
+
filePath: string;
|
|
25
|
+
roots: readonly string[]; // trusted absolute paths, checked in order
|
|
26
|
+
label?: string; // used in validation errors
|
|
27
27
|
};
|
|
28
28
|
```
|
|
29
29
|
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
30
|
+
Roots may use `~` or local `file://` spellings, but each resolved root must be
|
|
31
|
+
absolute. Existing root symlinks are canonicalized before containment is
|
|
32
|
+
checked. Invalid root entries throw `FsSafeError("invalid-path")`; an invalid
|
|
33
|
+
`file://` input throws `Error`. A path that is valid but does not fall inside
|
|
34
|
+
any usable root returns `null`.
|
|
33
35
|
|
|
34
|
-
|
|
36
|
+
## `resolveLocalPathFromRootsSync(options)`
|
|
35
37
|
|
|
36
38
|
```ts
|
|
37
|
-
type
|
|
38
|
-
|
|
39
|
-
|
|
39
|
+
type ResolveLocalPathFromRootsSyncOptions = LocalRootsInputOptions & {
|
|
40
|
+
allowMissing?: boolean; // default false
|
|
41
|
+
requireFile?: boolean; // default false
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
type LocalRootsPathResult = {
|
|
45
|
+
path: string; // canonical candidate path
|
|
46
|
+
root: string; // canonical containing root
|
|
47
|
+
};
|
|
40
48
|
```
|
|
41
49
|
|
|
50
|
+
For an existing upload:
|
|
51
|
+
|
|
42
52
|
```ts
|
|
43
53
|
import { resolveLocalPathFromRootsSync } from "@openclaw/fs-safe/advanced";
|
|
44
54
|
|
|
45
|
-
const r = resolveLocalPathFromRootsSync(
|
|
55
|
+
const r = resolveLocalPathFromRootsSync({
|
|
56
|
+
filePath: "/srv/uploads/photo.jpg",
|
|
46
57
|
roots: ["/srv/uploads", "/srv/cache"],
|
|
58
|
+
requireFile: true,
|
|
47
59
|
});
|
|
48
60
|
|
|
49
|
-
if (!r
|
|
50
|
-
console.log(r.
|
|
51
|
-
console.log(r.
|
|
52
|
-
console.log(r.relativePath); // photo.jpg
|
|
61
|
+
if (!r) throw new Error("photo is outside the configured roots");
|
|
62
|
+
console.log(r.path); // canonical path to photo.jpg
|
|
63
|
+
console.log(r.root); // canonical /srv/uploads
|
|
53
64
|
```
|
|
54
65
|
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
3. If the input is absolute (`/...` or Windows drive) and `allowAbsolute` is true, accept it as-is and check it falls under one of the roots.
|
|
62
|
-
4. Otherwise, treat the input as relative and resolve it against each root in order until one contains the resulting path.
|
|
63
|
-
|
|
64
|
-
If no root contains the result, returns `{ ok: false, reason: "outside-roots" }`.
|
|
65
|
-
|
|
66
|
-
`"invalid-input"` covers empty strings, embedded NULs, encoded `..` traversal, Windows network paths (`\\server\share`), and other constructs that should not be resolved at all.
|
|
66
|
+
By default the candidate must exist. `allowMissing: true` instead canonicalizes
|
|
67
|
+
the nearest existing ancestor and validates the missing tail, which is useful
|
|
68
|
+
when selecting a future output location. `requireFile: true` rejects existing
|
|
69
|
+
directories and other non-file leaves. Dangling symlinks and candidates whose
|
|
70
|
+
ancestors cannot be canonicalized are rejected rather than treated as safe
|
|
71
|
+
missing paths.
|
|
67
72
|
|
|
68
|
-
## `readLocalFileFromRoots(
|
|
73
|
+
## `readLocalFileFromRoots(options)`
|
|
69
74
|
|
|
70
|
-
|
|
75
|
+
The asynchronous helper opens the candidate through the matched [`Root`](root.md),
|
|
76
|
+
so no-follow, identity, hardlink, device-path, and byte-limit checks happen at
|
|
77
|
+
the read itself.
|
|
71
78
|
|
|
72
79
|
```ts
|
|
80
|
+
type ReadLocalFileFromRootsOptions = LocalRootsInputOptions & {
|
|
81
|
+
hardlinks?: "reject" | "allow";
|
|
82
|
+
maxBytes?: number;
|
|
83
|
+
nonBlockingRead?: boolean;
|
|
84
|
+
symlinks?: "reject" | "follow-within-root";
|
|
85
|
+
};
|
|
86
|
+
|
|
73
87
|
type LocalRootsReadResult = ReadResult & {
|
|
74
|
-
|
|
75
|
-
relativePath: string;
|
|
88
|
+
root: string; // canonical containing root
|
|
76
89
|
};
|
|
90
|
+
```
|
|
77
91
|
|
|
78
|
-
|
|
92
|
+
```ts
|
|
93
|
+
const r = await readLocalFileFromRoots({
|
|
94
|
+
filePath: "/srv/uploads/photo.jpg",
|
|
79
95
|
roots: ["/srv/uploads", "/srv/cache"],
|
|
80
96
|
maxBytes: 8 * 1024 * 1024,
|
|
81
97
|
});
|
|
82
|
-
if (!r)
|
|
98
|
+
if (!r) throw new Error("photo is missing, unreadable, or outside the roots");
|
|
83
99
|
process.stdout.write(r.buffer);
|
|
84
100
|
```
|
|
85
101
|
|
|
86
|
-
The
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
```ts
|
|
91
|
-
type ReadLocalFileFromRootsOptions = LocalRootsInputOptions & {
|
|
92
|
-
hardlinks?: "reject" | "allow";
|
|
93
|
-
maxBytes?: number;
|
|
94
|
-
symlinks?: "reject" | "follow-within-root";
|
|
95
|
-
};
|
|
96
|
-
```
|
|
97
|
-
|
|
98
|
-
The read-side options are forwarded to `Root` for the actual read.
|
|
102
|
+
The helper returns `null` when no configured root can be opened or no safe read
|
|
103
|
+
succeeds. This intentionally collapses missing, outside-root, and per-root read
|
|
104
|
+
failures; use a single `Root` directly when the caller must distinguish those
|
|
105
|
+
outcomes. Omitting `maxBytes` preserves `Root`'s 16 MiB default.
|
|
99
106
|
|
|
100
|
-
##
|
|
107
|
+
## File URL and Windows-path companions
|
|
101
108
|
|
|
102
|
-
The
|
|
109
|
+
The advanced surface also exports the normalization helpers used around this
|
|
110
|
+
API:
|
|
103
111
|
|
|
104
112
|
```ts
|
|
105
113
|
import {
|
|
@@ -113,52 +121,20 @@ import {
|
|
|
113
121
|
} from "@openclaw/fs-safe/advanced";
|
|
114
122
|
```
|
|
115
123
|
|
|
116
|
-
- `safeFileURLToPath(fileUrl)`
|
|
117
|
-
|
|
118
|
-
- `
|
|
119
|
-
- `
|
|
120
|
-
|
|
121
|
-
- `
|
|
122
|
-
- `
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
### Multi-root config: search project, then user, then system
|
|
127
|
-
|
|
128
|
-
```ts
|
|
129
|
-
const text = await readLocalFileFromRoots(name, {
|
|
130
|
-
roots: [path.join(projectDir, "templates"), path.join(homedir(), ".app/templates"), "/etc/app/templates"],
|
|
131
|
-
allowAbsolute: false, // only resolve names, never absolute paths
|
|
132
|
-
maxBytes: 256 * 1024,
|
|
133
|
-
});
|
|
134
|
-
```
|
|
135
|
-
|
|
136
|
-
### Validate a file:// URL at the API boundary
|
|
137
|
-
|
|
138
|
-
```ts
|
|
139
|
-
import { safeFileURLToPath, isWindowsNetworkPath } from "@openclaw/fs-safe/advanced";
|
|
140
|
-
|
|
141
|
-
let abs: string;
|
|
142
|
-
try {
|
|
143
|
-
abs = safeFileURLToPath(req.body.fileUrl);
|
|
144
|
-
} catch {
|
|
145
|
-
return reply(400, "invalid file URL");
|
|
146
|
-
}
|
|
147
|
-
if (isWindowsNetworkPath(abs)) return reply(400, "network paths not allowed");
|
|
148
|
-
```
|
|
149
|
-
|
|
150
|
-
### Deny absolute, allow relative-only
|
|
151
|
-
|
|
152
|
-
```ts
|
|
153
|
-
const r = resolveLocalPathFromRootsSync(input, {
|
|
154
|
-
roots: ["/srv/workspace"],
|
|
155
|
-
allowAbsolute: false,
|
|
156
|
-
allowFileUrls: false,
|
|
157
|
-
});
|
|
158
|
-
```
|
|
124
|
+
- `safeFileURLToPath(fileUrl)` parses a local file URL and refuses remote hosts
|
|
125
|
+
or paths that decode to Windows network paths.
|
|
126
|
+
- `trySafeFileURLToPath(fileUrl)` returns `undefined` instead of throwing.
|
|
127
|
+
- `isWindowsDriveLetterPath()` and `isWindowsNetworkPath()` classify Windows
|
|
128
|
+
absolute and network spellings.
|
|
129
|
+
- `assertNoWindowsNetworkPath()` throws for a network path on Windows.
|
|
130
|
+
- `basenameFromMediaSource()` extracts a best-effort filename from a URL, data
|
|
131
|
+
URI, or path.
|
|
132
|
+
- `hasEncodedFileUrlSeparator()` detects percent-encoded slash or backslash
|
|
133
|
+
spellings.
|
|
159
134
|
|
|
160
135
|
## See also
|
|
161
136
|
|
|
162
|
-
- [`root()`](root.md) —
|
|
163
|
-
|
|
164
|
-
- [
|
|
137
|
+
- [`root()`](root.md) — use when one trusted root should preserve individual
|
|
138
|
+
failure codes.
|
|
139
|
+
- [Path helpers](path.md) — lexical and canonical containment primitives.
|
|
140
|
+
- [`pathScope()`](path-scope.md) — result-shaped single-root validation.
|
package/docs/native-helper.md
CHANGED
|
@@ -30,21 +30,27 @@ Configure the mode once during startup. Loading is lazy and cached; changing fro
|
|
|
30
30
|
## Native boundary
|
|
31
31
|
|
|
32
32
|
The bundled native layer exposes policy-free filesystem mechanisms: beneath-root
|
|
33
|
-
open/mkdir/link, no-replace rename, identity reads, archive decode/execution,
|
|
33
|
+
open/mkdir/link, replace and no-replace rename, identity reads, archive decode/execution,
|
|
34
34
|
clone/copy/hash workers, and Windows security descriptor calls. The TypeScript
|
|
35
35
|
layer owns policy, retries, filters, budgets, modes, cleanup, error
|
|
36
36
|
normalization, and the decision to fall back.
|
|
37
37
|
|
|
38
|
-
- Linux uses `openat2` with `RESOLVE_BENEATH | RESOLVE_NO_MAGICLINKS` and `renameat2(RENAME_NOREPLACE)`.
|
|
39
|
-
- macOS 15.4 and newer prefer `O_RESOLVE_BENEATH`; older kernels resolve components with `O_NOFOLLOW` and restart in-root symlinks from the pinned root descriptor. Both routes use an `F_GETPATH` post-open escape detector and report `best-effort` because directory rename races are not atomic with that check.
|
|
40
|
-
- Windows uses handle-relative `NtCreateFile`, rejects reparse points, and uses `FileRenameInfoEx` with replacement
|
|
38
|
+
- Linux uses `openat2` with `RESOLVE_BENEATH | RESOLVE_NO_MAGICLINKS`, `renameat`, and `renameat2(RENAME_NOREPLACE)`.
|
|
39
|
+
- macOS 15.4 and newer prefer `O_RESOLVE_BENEATH`; older kernels resolve components with `O_NOFOLLOW` and restart in-root symlinks from the pinned root descriptor. Both routes use an `F_GETPATH` post-open escape detector and report `best-effort` because directory rename races are not atomic with that check. Publication uses `renameat` for replacement and `renameatx_np(RENAME_EXCL)` for no-replace.
|
|
40
|
+
- Windows uses handle-relative `NtCreateFile`, rejects reparse points, and uses `FileRenameInfoEx` with replacement selected explicitly by the TypeScript policy layer.
|
|
41
41
|
|
|
42
|
-
Native primitives back create-only pinned writes, async sidecar creation,
|
|
42
|
+
Native primitives back create-only and replacing pinned writes, async sidecar creation,
|
|
43
43
|
guarded publication, archive acceleration, and direct Windows ACL operations.
|
|
44
44
|
Equivalent JavaScript paths remain available for documented fallback-capable
|
|
45
45
|
features. See [Native architecture](native.md#javascript-fallback-guarantees-and-delta)
|
|
46
46
|
for the exact difference.
|
|
47
47
|
|
|
48
|
+
The guarded JavaScript mutation path is detection-based, not containment-atomic.
|
|
49
|
+
If a same-privilege peer can replace a writable parent after its identity guard
|
|
50
|
+
but before Node resolves a pathname mutation, the mutation can land outside the
|
|
51
|
+
intended root before the post-operation guard throws. Select `require` rather
|
|
52
|
+
than `auto` or `off` when that concurrent attacker is part of the threat model.
|
|
53
|
+
|
|
48
54
|
`openBeneath()` returns `{ fd, containment }`. `containment` is
|
|
49
55
|
`"kernel-atomic"` for Linux `openat2` and `"best-effort"` for macOS and
|
|
50
56
|
Windows. Public JavaScript root open/read/writable results also expose the
|
package/docs/native.md
CHANGED
|
@@ -41,7 +41,7 @@ The TypeScript layer validates and decides. The native layer never decides
|
|
|
41
41
|
whether a path, archive entry, mode, owner, or cleanup policy is acceptable.
|
|
42
42
|
|
|
43
43
|
- Linux uses `openat2(RESOLVE_BENEATH | RESOLVE_NO_MAGICLINKS)`, fd-relative
|
|
44
|
-
`mkdirat`/`linkat`/`renameat2`, `FICLONE`, and `copy_file_range`.
|
|
44
|
+
`mkdirat`/`linkat`/`renameat`/`renameat2`, `FICLONE`, and `copy_file_range`.
|
|
45
45
|
- macOS 15.4 and newer first use `openat(O_RESOLVE_BENEATH)`; older kernels walk
|
|
46
46
|
components with `openat(O_NOFOLLOW)` and restart in-root symlinks from the
|
|
47
47
|
pinned root. Both routes apply an `F_GETPATH` post-open containment detector,
|
|
@@ -99,7 +99,7 @@ remain TypeScript-owned. What changes is the syscall strength or availability:
|
|
|
99
99
|
|
|
100
100
|
| Capability | Native path | Guarded JavaScript path |
|
|
101
101
|
|---|---|---|
|
|
102
|
-
| Root-relative opens/mutations | Descriptor-relative beneath operations. Linux reports `kernel-atomic`; macOS and Windows report `best-effort`. macOS uses `O_RESOLVE_BENEATH` when available plus an `F_GETPATH` detector, while Windows rejects reparse traversal in the object-manager call. | Reports `best-effort`: component-wise alias checks, no-follow opens where Node exposes them, private temp/rename, and post-operation identity verification. A
|
|
102
|
+
| Root-relative opens/mutations | Descriptor-relative beneath operations. Pinned writes create parents and publish both replacement and no-replace targets relative to open directory descriptors. Linux reports `kernel-atomic`; macOS and Windows report `best-effort`. macOS uses `O_RESOLVE_BENEATH` when available plus an `F_GETPATH` detector, while Windows rejects reparse traversal in the object-manager call. | Reports `best-effort`: component-wise alias checks, no-follow opens where Node exposes them, private temp/rename, and post-operation identity verification. A same-privilege peer can replace a writable parent after a guard assertion but before Node resolves the pathname mutation; the mutation may land outside the intended root before the post-check detects it. |
|
|
103
103
|
| ZIP/TAR/gzip | Rust streaming decode and fd-relative output creation. | JSZip/node-tar into a private stage, then the same guarded merge policy. |
|
|
104
104
|
| Zstd/bzip2 TAR | Supported. | Unsupported; typed `helper-unavailable`. |
|
|
105
105
|
| Publication copy | Clone, Linux `copy_file_range`, async native SHA-256. | Exclusive `wx` byte loop and Node SHA-256 with the same content/identity fences. |
|
package/docs/output.md
CHANGED
|
@@ -51,8 +51,11 @@ root"; callers should choose the filename before calling this API.
|
|
|
51
51
|
Use `maxBytes` when the external producer can create arbitrarily large files,
|
|
52
52
|
and `mode` when the finalized file needs a specific POSIX mode. Both staging
|
|
53
53
|
modes enforce them after the producer returns and before committing the target.
|
|
54
|
-
Requested basenames containing C0/C1 controls or Windows-invalid characters
|
|
55
|
-
|
|
54
|
+
Requested basenames containing C0/C1 controls or Windows-invalid characters go
|
|
55
|
+
through the package's filename sanitizer; `fallbackFileName` supplies the name
|
|
56
|
+
when nothing remains. This removes traversal, device-name, and invalid-character
|
|
57
|
+
hazards but does not trim Windows-normalized trailing dots or spaces; reject or
|
|
58
|
+
rewrite those when cross-platform filename uniqueness matters.
|
|
56
59
|
The same sanitized basename is used for producer staging, guarded internal
|
|
57
60
|
temps, the final rename target, and the returned `path`; raw and staged names
|
|
58
61
|
never diverge.
|
|
@@ -73,7 +76,9 @@ and size-limit violations, applies `mode`, fsyncs it, and atomically renames it
|
|
|
73
76
|
over the target. Existing files and symlink entries are replaced without
|
|
74
77
|
following their contents or referents. The parent identity is guarded across
|
|
75
78
|
the operation and the parent directory is synchronized best-effort after
|
|
76
|
-
rename.
|
|
79
|
+
rename. If an error leaves the sibling temp in place and immediate cleanup
|
|
80
|
+
fails, its verified identity remains registered for a best-effort process-exit
|
|
81
|
+
cleanup retry.
|
|
77
82
|
|
|
78
83
|
## Why not pass the final path to the library?
|
|
79
84
|
|
package/docs/path-scope.md
CHANGED
|
@@ -77,7 +77,10 @@ Async. Strict variant: every input must already exist as a regular file inside t
|
|
|
77
77
|
|
|
78
78
|
### `writable(rel, options?)`
|
|
79
79
|
|
|
80
|
-
Async. Resolves a writable target:
|
|
80
|
+
Async. Resolves a writable target: requires the parent directory to already
|
|
81
|
+
exist canonically inside the scope, refuses targets whose existing inode is a
|
|
82
|
+
symlink or hardlinked, and returns the absolute path to use for the write. Use
|
|
83
|
+
`ensureDir()` first when the parent needs to be created.
|
|
81
84
|
|
|
82
85
|
```ts
|
|
83
86
|
const t = await uploads.writable("reports/2026/05/report.pdf");
|
package/docs/path.md
CHANGED
|
@@ -19,7 +19,9 @@ import {
|
|
|
19
19
|
} from "@openclaw/fs-safe/path";
|
|
20
20
|
```
|
|
21
21
|
|
|
22
|
-
|
|
22
|
+
Path helpers are deliberately a subpath import so the main entry stays focused
|
|
23
|
+
on the root capability, its shared configuration, errors, and external-output
|
|
24
|
+
helper.
|
|
23
25
|
|
|
24
26
|
## Boundary checks
|
|
25
27
|
|
|
@@ -62,7 +64,7 @@ Convenience wrapper around `isPathInside`. Same semantics, different name kept f
|
|
|
62
64
|
Resolve a base directory to an absolute, normalized form ready for prefix comparison. Pre-normalized directories make subsequent `isPathInside` checks unambiguous.
|
|
63
65
|
|
|
64
66
|
```ts
|
|
65
|
-
const base = resolveSafeBaseDir("/srv/uploads/."); // "/srv/uploads"
|
|
67
|
+
const base = resolveSafeBaseDir("/srv/uploads/."); // "/srv/uploads/"
|
|
66
68
|
```
|
|
67
69
|
|
|
68
70
|
## Realpath and stat
|
|
@@ -80,7 +82,9 @@ All `realpath` failures collapse to `null` — there is no distinction between `
|
|
|
80
82
|
|
|
81
83
|
### `safeStatSync(targetPath)`
|
|
82
84
|
|
|
83
|
-
Synchronous `stat` that returns `
|
|
85
|
+
Synchronous `stat` that returns `Stats` on success and `null` on any failure,
|
|
86
|
+
including missing paths and permission errors. Use `fs.statSync` directly when
|
|
87
|
+
the distinction matters.
|
|
84
88
|
|
|
85
89
|
```ts
|
|
86
90
|
const stat = safeStatSync("/srv/uploads/photo.jpg");
|
package/docs/permissions.md
CHANGED
|
@@ -38,6 +38,10 @@ isWorldReadable(bits);
|
|
|
38
38
|
isGroupReadable(bits);
|
|
39
39
|
```
|
|
40
40
|
|
|
41
|
+
POSIX remediation strings shell-quote paths with whitespace or metacharacters
|
|
42
|
+
and protect option-like paths with `--`, so they can be presented as commands
|
|
43
|
+
without letting the inspected pathname add shell syntax.
|
|
44
|
+
|
|
41
45
|
`inspectPathPermissions()` follows symlink targets for the effective mode but tells you whether the original path was a symlink. On POSIX it reports owner/group/world bits. On Windows it delegates to the ACL helpers below and also reports `ownerSid` plus `ownerTrusted` when ownership can be verified. `ownerTrusted` is true only for a local volume owned by the current user, LocalSystem, or built-in Administrators; remote filesystems fail closed. Secure reads and callers that protect credential-bearing execution require `ownerTrusted === true`.
|
|
42
46
|
|
|
43
47
|
## Advanced Windows ACL helpers
|
|
@@ -67,6 +71,10 @@ resolveWindowsUserPrincipal(env);
|
|
|
67
71
|
The fallback Windows inspector calls `icacls.exe <path>` using its supported
|
|
68
72
|
path-only inspection syntax and classifies principals as trusted, world, or
|
|
69
73
|
group. Trusted defaults include the current user, SYSTEM, and Administrators.
|
|
74
|
+
Built-in PowerShell, `icacls.exe`, and `whoami.exe` invocations have a fixed
|
|
75
|
+
30-second per-process deadline. A command failure or timeout returns an
|
|
76
|
+
unverified result (`source: "unknown"`) so callers fail closed. Advanced callers
|
|
77
|
+
that inject a custom `exec` implementation own that executor's deadline.
|
|
70
78
|
The parser is on the advanced surface so tests and CLIs can process captured
|
|
71
79
|
`icacls` output without spawning a process.
|
|
72
80
|
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
# Public API inventory
|
|
2
|
+
|
|
3
|
+
This page closes the gap between the narrative guides and the complete package
|
|
4
|
+
surface. The generated declarations define the exact signatures; the lists
|
|
5
|
+
below name the lower-frequency exports that are easy to miss when reading only
|
|
6
|
+
the focused guides. `test/public-api.json` guards the same inventory during
|
|
7
|
+
pack checks.
|
|
8
|
+
|
|
9
|
+
## Main entry and `root`
|
|
10
|
+
|
|
11
|
+
The main entry and `@openclaw/fs-safe/root` expose the root capability types,
|
|
12
|
+
including `ContainmentGuarantee`, `RootOpenOptions`, `RootCreateOptions`,
|
|
13
|
+
`RootCreateJsonOptions`, and `WritableOpenMode`. The root-bounded iterator uses
|
|
14
|
+
`RootWalkOptions`, `RootWalkEntry`, `RootWalkDataEntry`, `RootWalkEntryKind`,
|
|
15
|
+
`RootWalkDataEntryKind`, `RootWalkSymlinkPolicy`, `RootWalkLimitBehavior`,
|
|
16
|
+
`RootWalkDirectoryErrorBehavior`, `RootWalkEntryFilter`, and
|
|
17
|
+
`RootWalkEntryFilterResult`.
|
|
18
|
+
|
|
19
|
+
The root subpath also exports `openLocalFileSafely`, `readLocalFileSafely`, and
|
|
20
|
+
`resolveOpenedFileRealPathForHandle` for trusted absolute-file composition.
|
|
21
|
+
They do not create a root boundary around arbitrary caller input; prefer
|
|
22
|
+
`root()` for untrusted paths.
|
|
23
|
+
|
|
24
|
+
The error helpers are `categorizeFsSafeError` and `FsSafeErrorDetails`. The
|
|
25
|
+
deprecated native-configuration bridge retains the `FsSafePythonConfig` type.
|
|
26
|
+
|
|
27
|
+
## `path` and `advanced`
|
|
28
|
+
|
|
29
|
+
The lexical path surface additionally exports `isNodeError`,
|
|
30
|
+
`isPathRelativeEscape`, `normalizeWindowsPathForComparison`,
|
|
31
|
+
`resolveSafeRelativePath`, `splitSafeRelativePath`, and
|
|
32
|
+
`matchUnsafeDeviceReadPath`. The device matcher is described by
|
|
33
|
+
`UnsafeDeviceReadPathMatch`, `UnsafeDeviceReadPathOptions`, and
|
|
34
|
+
`UnsafeDeviceReadPathReason`.
|
|
35
|
+
|
|
36
|
+
The advanced root-file primitive exports `OpenRootFileParams`,
|
|
37
|
+
`OpenRootFileSyncParams`, `RootFileOpenResult`, and
|
|
38
|
+
`RootFileOpenFailureReason`. These are composition types for callers building
|
|
39
|
+
their own pinned-open flow, not substitutes for the higher-level `Root` verbs.
|
|
40
|
+
|
|
41
|
+
## `json` and `store`
|
|
42
|
+
|
|
43
|
+
Standalone structured reads use `ReadJsonOptions`, `ReadRootJsonSyncOptions`,
|
|
44
|
+
`ReadRootStructuredFileSyncOptions`, and `RootStructuredFileReadResult`.
|
|
45
|
+
|
|
46
|
+
The store surface additionally exports `FileStoreReadOptions` and
|
|
47
|
+
`JsonFileStoreOptions`. Durable-queue inspection and recovery use
|
|
48
|
+
`jsonDurableQueueEntryExists`, `loadJsonDurableQueueEntry`,
|
|
49
|
+
`readJsonDurableQueueEntry`, and the `JsonDurableQueueLoadOptions` and
|
|
50
|
+
`JsonDurableQueueReadResult` types. `unlinkBestEffort` is the explicitly
|
|
51
|
+
best-effort cleanup helper used by those queue flows.
|
|
52
|
+
|
|
53
|
+
## Permissions and secure files
|
|
54
|
+
|
|
55
|
+
Permission inspection exposes `PermissionCheckOptions` and `SafeStatResult`.
|
|
56
|
+
Private-directory creation uses `CreatePrivateDirectoryOptions`. Raw Windows
|
|
57
|
+
descriptor facts use `OwnerAndDaclResult`, `WindowsAccessControlEntry`, and
|
|
58
|
+
`WindowsAceFlags`.
|
|
59
|
+
|
|
60
|
+
Secure reads split their option and result shapes into
|
|
61
|
+
`SecureFileTrustOptions`, `SecureFilePermissionOptions`,
|
|
62
|
+
`SecureFileInjectOptions`, `SecureFileIoOptions`, and `SecureFileReadResult`.
|
|
63
|
+
|
|
64
|
+
## Locks, walking, and temp workspaces
|
|
65
|
+
|
|
66
|
+
The file-lock diagnostics surface includes `FileLockHeldEntry`,
|
|
67
|
+
`FileLockStaleRecovery`, and `SidecarLockCompromisedInfo`.
|
|
68
|
+
`drainFileLockManagerForTest` and `resetFileLockManagerForTest` are test-only
|
|
69
|
+
manager controls; production code should not use them as lock recovery.
|
|
70
|
+
|
|
71
|
+
Standalone walkers use the `WalkEntryKind` and `WalkSymlinkPolicy` unions.
|
|
72
|
+
Private workspaces expose `TempPathIdentityReceipt` and the
|
|
73
|
+
`TempWorkspaceCleanupResult` union so callers can distinguish removal,
|
|
74
|
+
absence, and identity mismatch.
|
|
75
|
+
|
|
76
|
+
## Atomic replacement and durability
|
|
77
|
+
|
|
78
|
+
Atomic helper option and receipt types include
|
|
79
|
+
`MovePathWithCopyFallbackOptions`, `ReplaceDirectoryAtomicOptions`,
|
|
80
|
+
`ReplaceFileAtomicSyncOptions`, `ReplaceFileAtomicResult`,
|
|
81
|
+
`ReplaceFileAtomicRestoreCleanup`, `ReplaceFileCopyFallbackRestorePolicy`, and
|
|
82
|
+
`ReplaceFileDestinationHardlinkPolicy`.
|
|
83
|
+
|
|
84
|
+
The durability surface also exports the synchronous strict
|
|
85
|
+
`syncDirectorySync`, plus `DirectoryReceipt`, `DurableDirectoryReceipt`,
|
|
86
|
+
`EnsureDurableDirectoryOptions`, `PublishFileExclusiveResult`,
|
|
87
|
+
`PublishFileExclusiveStrategy`, `PublishFileExclusiveCleanup`,
|
|
88
|
+
`PublishFileExclusiveFailurePhase`,
|
|
89
|
+
`PublishFileExclusiveDirectorySyncFailure`, `Sha256FileInput`, and
|
|
90
|
+
`Sha256FileResult`.
|
|
91
|
+
|
|
92
|
+
## Archives
|
|
93
|
+
|
|
94
|
+
Archive option and policy types are `ExtractArchiveOptions`,
|
|
95
|
+
`ArchiveEntryFilter`, `ArchiveEntryModePolicy`, and
|
|
96
|
+
`ArchiveFilteredEntryPolicy`. Typed error-code unions are
|
|
97
|
+
`ArchiveFormatErrorCode`, `ArchiveLimitErrorCode`, and
|
|
98
|
+
`ArchiveSecurityErrorCode`. TAR and ZIP preflight composition uses
|
|
99
|
+
`TarEntryInfo` and `ZipArchiveWithFiles`.
|
|
100
|
+
|
|
101
|
+
`createArchiveSymlinkTraversalError` constructs the typed traversal failure
|
|
102
|
+
used by extractors. `resolvePackedRootDir` finds the single packed root when an
|
|
103
|
+
archive layout permits it; neither helper weakens entry validation.
|
|
104
|
+
|
|
105
|
+
## Keeping this list honest
|
|
106
|
+
|
|
107
|
+
Every runtime and type name in `test/public-api.json` must appear somewhere in
|
|
108
|
+
`README.md` or `docs/`. Documentation examples are also checked so a named
|
|
109
|
+
import cannot silently move to another package subpath.
|
package/docs/quickstart.md
CHANGED
|
@@ -18,7 +18,11 @@ const fs = await root("/srv/jobs/incoming", {
|
|
|
18
18
|
|
|
19
19
|
`root()` resolves the directory through the real filesystem (so symlinked roots become canonical) and verifies it exists. The defaults you pass apply to every call below; per-call options override them.
|
|
20
20
|
|
|
21
|
-
If the root directory itself does not exist yet, `root()` throws `FsSafeError`
|
|
21
|
+
If the root directory itself does not exist yet, `root()` throws `FsSafeError`
|
|
22
|
+
with code `not-found`; create it before constructing the capability.
|
|
23
|
+
`fs.ensureRoot()` is for generic code that needs to assert or recreate the root
|
|
24
|
+
itself after a handle was successfully constructed. It does not accept a
|
|
25
|
+
subpath and cannot bootstrap a root that prevented `root()` from returning.
|
|
22
26
|
|
|
23
27
|
## 2. Read and write text
|
|
24
28
|
|
|
@@ -62,7 +66,7 @@ await fs.remove("notes/archive/today.txt");
|
|
|
62
66
|
|
|
63
67
|
```ts
|
|
64
68
|
const here = await fs.exists("state/config.json"); // boolean
|
|
65
|
-
const stat = await fs.stat("state/config.json"); // {
|
|
69
|
+
const stat = await fs.stat("state/config.json"); // { isFile, isDirectory, size, mtimeMs, ... }
|
|
66
70
|
const names = await fs.list("state"); // string[]
|
|
67
71
|
const entries = await fs.list("state", { withFileTypes: true }); // DirEntry[]
|
|
68
72
|
```
|
package/docs/reading.md
CHANGED
|
@@ -14,12 +14,12 @@ const opened = await fs.open("large.log"); // FileHandle for strea
|
|
|
14
14
|
|
|
15
15
|
Regardless of shape, every read goes through the same boundary checks:
|
|
16
16
|
|
|
17
|
-
1. Resolve the
|
|
18
|
-
2. Reject
|
|
19
|
-
3.
|
|
20
|
-
4. Reject
|
|
21
|
-
5. Open with `O_NOFOLLOW` where available.
|
|
22
|
-
6.
|
|
17
|
+
1. Resolve the input lexically against the canonical real root.
|
|
18
|
+
2. Reject a lexically explicit unsafe device or process-fd namespace (`device-path`). This check precedes component alias policy because paths such as `/dev/fd` are themselves symlinks on common Linux hosts.
|
|
19
|
+
3. Resolve path components and reject anything that escapes the root (`outside-workspace`).
|
|
20
|
+
4. Reject `..` traversal and absolute spellings when they resolve outside the root. In-root absolute spellings remain accepted; `readAbsolute` makes that intent explicit.
|
|
21
|
+
5. Open with `O_NOFOLLOW` where available. Any remaining symlink in the path triggers `symlink` unless the call's `symlinks` policy is `follow-within-root`.
|
|
22
|
+
6. Compare the pre-open path identity, the open fd, and the post-open resolved path (`sameFileIdentity`). A swap mid-call triggers `path-mismatch`.
|
|
23
23
|
7. If `hardlinks: "reject"`, refuse files with `nlink > 1` (`hardlink`).
|
|
24
24
|
8. If `maxBytes` is set, refuse reads larger than the cap (`too-large`).
|
|
25
25
|
|
|
@@ -36,7 +36,8 @@ console.log(`${stat.size} bytes at ${realPath}`);
|
|
|
36
36
|
|
|
37
37
|
### `fs.readText(rel, options?)`
|
|
38
38
|
|
|
39
|
-
`buffer.toString(encoding)`. Defaults to `
|
|
39
|
+
`buffer.toString(encoding)`. Defaults to `"utf8"`; encoding is a per-call text
|
|
40
|
+
option, not a `RootDefaults` field:
|
|
40
41
|
|
|
41
42
|
```ts
|
|
42
43
|
const utf16 = await fs.readText("doc.txt", { encoding: "utf16le" });
|
|
@@ -83,14 +84,14 @@ try {
|
|
|
83
84
|
type RootReadOptions = {
|
|
84
85
|
hardlinks?: "reject" | "allow"; // override defaults.hardlinks
|
|
85
86
|
maxBytes?: number; // refuse reads larger than this many bytes
|
|
86
|
-
nonBlockingRead?: boolean; //
|
|
87
|
+
nonBlockingRead?: boolean; // compatibility hint; safe opens are already nonblocking where supported
|
|
87
88
|
symlinks?: "reject" | "follow-within-root"; // override defaults.symlinks
|
|
88
89
|
};
|
|
89
90
|
```
|
|
90
91
|
|
|
91
92
|
`maxBytes` is enforced eagerly: the library reads up to `maxBytes + 1` and throws `too-large` if there is more, so a hostile target cannot silently exhaust memory.
|
|
92
93
|
|
|
93
|
-
`nonBlockingRead`
|
|
94
|
+
`nonBlockingRead` remains as a compatibility hint. Safe reads always add the platform's nonblocking open flag where available so a raced FIFO cannot pin a worker indefinitely; regular-file descriptor reads retain normal Node behavior.
|
|
94
95
|
|
|
95
96
|
## `readAbsolute()` and `reader()`
|
|
96
97
|
|
|
@@ -101,7 +102,9 @@ fs.readAbsolute(absPath, options?) // ReadResult, abs path must be inside the
|
|
|
101
102
|
fs.reader(options?) // (path) => Promise<Buffer>
|
|
102
103
|
```
|
|
103
104
|
|
|
104
|
-
`readAbsolute` accepts absolute paths. Anything outside the root throws
|
|
105
|
+
`readAbsolute` accepts absolute paths. Anything outside the root throws
|
|
106
|
+
`outside-workspace`. It also accepts relative paths for compatibility, but use
|
|
107
|
+
`read()`/`readBytes()` when the input contract is explicitly relative.
|
|
105
108
|
|
|
106
109
|
`reader()` returns a closure that takes either a relative or an absolute path and returns a Buffer. Useful for plugging `fs-safe` into framework loader hooks:
|
|
107
110
|
|