@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/root.md
CHANGED
|
@@ -21,9 +21,10 @@ type RootDefaults = {
|
|
|
21
21
|
hardlinks?: "reject" | "allow"; // refuse files with nlink > 1 on read; defaults to "reject"
|
|
22
22
|
denyMutations?: DenyMutationPolicy; // absolute paths/prefixes mutation methods may not change
|
|
23
23
|
maxBytes?: number; // refuse reads larger than this many bytes; defaults to 16 MiB
|
|
24
|
-
mkdir?: boolean; // create missing parent dirs on write/openWritable/append
|
|
24
|
+
mkdir?: boolean; // create missing parent dirs on write/openWritable/append; default true
|
|
25
25
|
mode?: number; // file mode applied to new writes; per-call override available
|
|
26
|
-
nonBlockingRead?: boolean; //
|
|
26
|
+
nonBlockingRead?: boolean; // compatibility hint; safe opens are already nonblocking where supported
|
|
27
|
+
renameIdentity?: "strict" | "verify-content-with-lock"; // default "strict"
|
|
27
28
|
symlinks?: "reject" | "follow-within-root"; // policy when a path component is a symlink
|
|
28
29
|
};
|
|
29
30
|
|
|
@@ -83,6 +84,10 @@ await using opened = await fs.open("large.log");
|
|
|
83
84
|
`containment: "best-effort"`. The field reports the mechanism used; see the
|
|
84
85
|
[security model](security-model.md#containment-guarantees-by-platform).
|
|
85
86
|
|
|
87
|
+
The read methods also accept an absolute spelling that already resolves inside
|
|
88
|
+
the root. `readAbsolute()` and `reader()` make that intent explicit; an absolute
|
|
89
|
+
path outside the root is still rejected.
|
|
90
|
+
|
|
86
91
|
### Writes
|
|
87
92
|
|
|
88
93
|
```ts
|
|
@@ -103,6 +108,14 @@ fs.ensureRoot(options?) // accepts "" / "." as the root itself
|
|
|
103
108
|
|
|
104
109
|
`copyIn` is a one-shot ingest from a trusted absolute source path: it streams the source through the boundary, atomically renames into the root, and respects `maxBytes`.
|
|
105
110
|
|
|
111
|
+
Root operations that choose a new destination reject a leading Windows
|
|
112
|
+
drive-relative spelling such as `C:name` on every platform. This applies to
|
|
113
|
+
`write`, `create`, `append`, `openWritable`, `mkdir`, `copyIn`, and the
|
|
114
|
+
destination argument of `move`. In particular, `copyIn(path.basename(source),
|
|
115
|
+
source)` can reject a legal POSIX basename such as `c:photo.png`; callers that
|
|
116
|
+
derive portable destination names from host files must sanitize or map that
|
|
117
|
+
basename first.
|
|
118
|
+
|
|
106
119
|
`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.
|
|
107
120
|
|
|
108
121
|
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.
|
|
@@ -119,6 +132,12 @@ fs.resolve(rel) // absolute path inside the root, after canonic
|
|
|
119
132
|
|
|
120
133
|
These do not pin a later operation. They are safe to expose to UIs and decision points; for the actual read or write, use the verb methods so the operation pins identity at the point of use.
|
|
121
134
|
|
|
135
|
+
`resolve()` is the exception to the existing-object rule: because it selects a
|
|
136
|
+
location for later use, it rejects a leading drive-relative spelling. Reads,
|
|
137
|
+
`stat`, `exists`, `list`, `walk`, `remove`, and the source argument of `move`
|
|
138
|
+
accept an existing POSIX filename such as `c:notes.txt`. For `move`, only the
|
|
139
|
+
new destination name is subject to the portable guard.
|
|
140
|
+
|
|
122
141
|
## Native helper mode
|
|
123
142
|
|
|
124
143
|
Create-only writes prefer the bundled native helper for fd-relative opens and
|
|
@@ -147,11 +166,14 @@ fs.defaults // the RootDefaults you passed
|
|
|
147
166
|
|
|
148
167
|
## Failure semantics
|
|
149
168
|
|
|
150
|
-
|
|
169
|
+
Boundary and policy failures throw `FsSafeError` with a `code`. Parsing callbacks
|
|
170
|
+
and underlying filesystem operations can also surface `SyntaxError` or native
|
|
171
|
+
`NodeJS.ErrnoException` values. Branch on `err.code`, not message text, after
|
|
172
|
+
checking `err instanceof FsSafeError`. Common fs-safe codes:
|
|
151
173
|
|
|
152
174
|
| Code | When it fires |
|
|
153
175
|
|---|---|
|
|
154
|
-
| `invalid-path` | The input path is malformed, including embedded NUL bytes. |
|
|
176
|
+
| `invalid-path` | The input path is malformed, including embedded NUL bytes. Portable relative-path helpers and `FileStore` keys reject drive-relative segments; Root destination and resolution operations reject a leading drive-relative spelling such as `C:name`. |
|
|
155
177
|
| `outside-workspace` | The input resolves outside the root, or contains a `..` segment that would escape it. |
|
|
156
178
|
| `not-found` | The target does not exist (or its parent does not, with `mkdir: false`). |
|
|
157
179
|
| `not-file` | A read or copy targeted a non-regular file (directory, FIFO, socket, …). |
|
package/docs/secret-file.md
CHANGED
|
@@ -23,7 +23,7 @@ import {
|
|
|
23
23
|
| The file is a credential (token, key, password). | The file is application state. |
|
|
24
24
|
| You want the parent directory created at `0o700` if missing. | You don't care about the parent directory mode. |
|
|
25
25
|
| You want a hard size cap on reads (to defend against bogus input). | You're reading bounded JSON state. |
|
|
26
|
-
| Mode `0o600`
|
|
26
|
+
| Mode `0o600` and the private parent policy are mandatory. | Standalone `writeJson` also defaults to `0o600`, but it does not enforce the secret-directory ownership and permission policy. |
|
|
27
27
|
|
|
28
28
|
## Constants
|
|
29
29
|
|
|
@@ -39,7 +39,10 @@ The 16 KiB cap is intentionally aggressive — credentials should be small. If y
|
|
|
39
39
|
|
|
40
40
|
### `tryReadSecretFileSync(filePath, label, options?)`
|
|
41
41
|
|
|
42
|
-
The
|
|
42
|
+
The missing-is-optional reader. It returns the trimmed secret string, or
|
|
43
|
+
`undefined` when the `filePath` argument is absent/blank or the target does not
|
|
44
|
+
exist. An existing empty file is invalid and throws, as do unreadable,
|
|
45
|
+
oversized, symlink, hardlink, and other validation failures.
|
|
43
46
|
|
|
44
47
|
```ts
|
|
45
48
|
import { tryReadSecretFileSync } from "@openclaw/fs-safe/secret";
|
|
@@ -65,17 +68,31 @@ const token = readSecretFileSync("/var/lib/app/auth.token", "auth token");
|
|
|
65
68
|
```ts
|
|
66
69
|
type SecretFileReadOptions = {
|
|
67
70
|
maxBytes?: number; // default DEFAULT_SECRET_FILE_MAX_BYTES (16 KiB)
|
|
68
|
-
rejectSymlink?: boolean;
|
|
71
|
+
rejectSymlink?: boolean; // default false
|
|
69
72
|
rejectHardlinks?: boolean; // default true
|
|
70
73
|
};
|
|
71
74
|
```
|
|
72
75
|
|
|
73
|
-
The reader trims the file content and rejects empty results.
|
|
76
|
+
The reader trims the file content and rejects empty results. Symlink paths are
|
|
77
|
+
followed and pinned by default; set `rejectSymlink: true` when the pathname
|
|
78
|
+
itself must not be an alias. Hardlinks are rejected by default so another
|
|
79
|
+
in-tree name cannot alias the credential; pass `rejectHardlinks: false` only
|
|
80
|
+
when you explicitly trust that layout.
|
|
81
|
+
|
|
82
|
+
These readers do not enforce ownership or mode bits on an existing file. Their
|
|
83
|
+
read contract covers pinned identity, file type, link policy, and byte bounds;
|
|
84
|
+
the `0o600` guarantee belongs to the write helpers below. Use
|
|
85
|
+
[`readSecureFile`](secure-file.md) when reading an externally managed
|
|
86
|
+
credential must also fail on broad permissions or unexpected ownership.
|
|
74
87
|
|
|
75
88
|
`readSecretFile()` and `tryReadSecretFile()` are asynchronous counterparts with
|
|
76
89
|
the same pinned-handle validation, byte cap, trimming, error codes, and strict
|
|
77
90
|
versus missing-is-undefined naming semantics.
|
|
78
91
|
|
|
92
|
+
If an already validated descriptor fails while reading, both readers throw an
|
|
93
|
+
operational `FsSafeError` with `code: "read-failed"`; inspect `cause` for the
|
|
94
|
+
underlying Node filesystem code such as `EIO`.
|
|
95
|
+
|
|
79
96
|
Use the async strict reader when a service cannot start safely without the
|
|
80
97
|
credential:
|
|
81
98
|
|
package/docs/security-model.md
CHANGED
|
@@ -22,7 +22,8 @@ case since `5ddca80`. Exposure is limited to consumers that call
|
|
|
22
22
|
You hand a `root()` boundary to a piece of code that takes caller-controlled relative paths. The library defends against a caller that:
|
|
23
23
|
|
|
24
24
|
- supplies `..` traversal segments to escape the boundary
|
|
25
|
-
- supplies an absolute path
|
|
25
|
+
- supplies an absolute path outside a configured root, or to an API whose input
|
|
26
|
+
contract is strictly relative/portable
|
|
26
27
|
- replaces a path component with a symlink between check and use (TOCTOU)
|
|
27
28
|
- replaces the destination directory with a symlink right before a write
|
|
28
29
|
- creates a hardlink that aliases an out-of-tree inode and asks you to read or replace it
|
|
@@ -44,7 +45,7 @@ If you need full sandboxing, run the worker under reduced privileges (uid, conta
|
|
|
44
45
|
|
|
45
46
|
### Path traversal and absolute paths
|
|
46
47
|
|
|
47
|
-
Every
|
|
48
|
+
Every path is resolved against the canonicalized real path of the root, then checked with `isPathInside`. Alias resolution walks components before applying a later `..`, so a symlink cannot change what that parent segment means after validation. Parent traversal, an absolute spelling, or any alias whose canonical result is outside the root throws `outside-workspace`; absolute spellings that remain inside the root are accepted.
|
|
48
49
|
|
|
49
50
|
### Symlinks (read side)
|
|
50
51
|
|
|
@@ -54,7 +55,20 @@ Per-call `symlinks: "follow-within-root"` allows symlinks whose final target is
|
|
|
54
55
|
|
|
55
56
|
### Symlinks (write side)
|
|
56
57
|
|
|
57
|
-
|
|
58
|
+
With the native binding loaded, `write()`, `create()`, and `copyIn()` use a
|
|
59
|
+
sibling temp file and create parents and publish the target relative to pinned
|
|
60
|
+
directory descriptors. Replacement uses descriptor-relative rename just like
|
|
61
|
+
no-replace publication, so replacing the parent pathname does not divert the
|
|
62
|
+
mutation.
|
|
63
|
+
|
|
64
|
+
The JavaScript fallback used by `off`, by `auto` when no binding loads, and by
|
|
65
|
+
the explicit `renameIdentity: "verify-content-with-lock"` compatibility policy
|
|
66
|
+
cannot provide that guarantee because Node exposes no `mkdirat` or `renameat`.
|
|
67
|
+
It asserts directory identity around a pathname mutation and detects many
|
|
68
|
+
swaps, but detection occurs after the kernel may already have followed a new
|
|
69
|
+
parent symlink. A same-privilege peer with write access to the parent can
|
|
70
|
+
therefore cause an out-of-root side effect before the operation throws. Use
|
|
71
|
+
native `require` mode when concurrent hostile mutation is in scope.
|
|
58
72
|
|
|
59
73
|
### Hardlink aliasing
|
|
60
74
|
|
|
@@ -64,13 +78,15 @@ When `hardlinks: "reject"` is set, reads stat the target and refuse if `nlink >
|
|
|
64
78
|
|
|
65
79
|
`resolve()`, `exists()`, `stat()`, and `list()` are explicitly advisory — they answer a question and return. To act on a path with operation-local identity checks, use `read()`, `open()`, `write()`, `create()`, `copyIn()`, `move()`, or `remove()`. The containment table below states which opens are kernel-atomic and which remain best-effort.
|
|
66
80
|
|
|
81
|
+
A `root()` handle also remembers the canonical root directory identity. Calls fail with `path-mismatch` if that canonical pathname is replaced, including advisory inspection and walking calls, rather than following a replacement root into another tree.
|
|
82
|
+
|
|
67
83
|
### Denied mutations
|
|
68
84
|
|
|
69
85
|
`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.
|
|
70
86
|
|
|
71
87
|
### Atomic writes
|
|
72
88
|
|
|
73
|
-
`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.
|
|
89
|
+
`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 unless the caller explicitly enables `copyFallbackOnPermissionError`, whose default `copyFallbackRestore: "none"` contract may leave a partial destination after a failed in-place fallback.
|
|
74
90
|
|
|
75
91
|
Within one process, async writes to the same target are queued so their temp-write/rename phases do not overlap. Cross-process writers still need an external protocol such as the sidecar lock helpers.
|
|
76
92
|
|
|
@@ -120,6 +136,7 @@ The public `OpenResult`, `ReadResult`, and `WritableOpenResult` expose `containm
|
|
|
120
136
|
| Absolute paths are escape hatches | APIs that accept or return absolute paths exist for audit, ingest, and advanced composition. Prefer root-relative names in normal application flow. |
|
|
121
137
|
| Not a mount boundary | `root()` keeps path traversal inside the directory tree and blocks known unsafe read device paths, but it does not make bind mounts or virtual filesystems safe to expose wholesale. |
|
|
122
138
|
| Per-call, not per-session | Another process with the same privileges can still mutate the tree between calls, and best-effort mechanisms retain documented same-call race windows. Use one verb method to minimize the window and inspect its reported containment class. |
|
|
139
|
+
| JavaScript mutations are detection-based | Without the native binding, Node pathname mutations retain a check-to-syscall race. A writable parent can be swapped so a create, rename, or removal affects an out-of-root path before the fallback detects identity drift. |
|
|
123
140
|
| Hardlink rejection is best-effort | Link-count checks depend on platform metadata. Treat `hardlinks: "reject"` as a tripwire, not an authorization primitive. |
|
|
124
141
|
| Mode bits are not a full policy engine | `replaceFileAtomic` and secret-file helpers set requested modes, but you should still set umask and inspect modes when policy requires it. |
|
|
125
142
|
| Archive extraction is path safety, not content safety | Unsafe entry paths and links are rejected; malicious payload contents remain your application layer's problem. |
|
package/docs/sidecar-lock.md
CHANGED
|
@@ -96,6 +96,12 @@ type FileLockRetryOptions = {
|
|
|
96
96
|
result is passed to `shouldReclaim` and `shouldRemoveStaleLock`, allowing PID,
|
|
97
97
|
process-start, argv, or role schemas to remain application-owned.
|
|
98
98
|
|
|
99
|
+
On Windows, a pathed `EPERM` from creating or opening the lock file can be a
|
|
100
|
+
short teardown race after another holder unlinks it. The async lock retries that
|
|
101
|
+
specific denial at most eight times. A parent-directory denial, a denial from a
|
|
102
|
+
callback, or a ninth consecutive lock-file denial surfaces as the original
|
|
103
|
+
`EPERM`; it is not converted to `file_lock_timeout`.
|
|
104
|
+
|
|
99
105
|
## Owner-scoped reentrancy
|
|
100
106
|
|
|
101
107
|
Version 0.5 removes the unsound process-scoped `allowReentrant` boolean and
|
|
@@ -167,10 +173,10 @@ progress.
|
|
|
167
173
|
## Synchronous locks
|
|
168
174
|
|
|
169
175
|
`acquireFileLockSync()` and `withFileLockSync()` mirror filesystem arbitration,
|
|
170
|
-
retry, payload parsing, stale policy,
|
|
171
|
-
verification, and compromise monitoring. They do
|
|
172
|
-
|
|
173
|
-
|
|
176
|
+
retry, payload parsing, stale policy, owner-scoped reentrancy, guarded
|
|
177
|
+
identity-conditioned reclaim, verification, and compromise monitoring. They do
|
|
178
|
+
not use the async manager queue or support async callbacks. Retry waits block
|
|
179
|
+
the calling thread; use the async API in request-serving code.
|
|
174
180
|
|
|
175
181
|
Always release in a `finally`:
|
|
176
182
|
|
|
@@ -236,7 +242,11 @@ await locks.drain();
|
|
|
236
242
|
|
|
237
243
|
## Stale policy: `shouldReclaim`
|
|
238
244
|
|
|
239
|
-
The default policy treats locks whose `createdAt` is older than `staleMs`
|
|
245
|
+
The default policy treats locks whose valid `createdAt` is older than `staleMs`
|
|
246
|
+
as stale. A valid current or future timestamp remains authoritative under
|
|
247
|
+
filesystem clock skew; only absent or malformed timestamps fall back to the
|
|
248
|
+
sidecar `mtime`. Pass a custom callback when you want a richer notion of "is the
|
|
249
|
+
holder still alive":
|
|
240
250
|
|
|
241
251
|
```ts
|
|
242
252
|
import { kill } from "node:process";
|
package/docs/temp.md
CHANGED
|
@@ -45,6 +45,11 @@ await runBuild(workspace.dir, inputPath);
|
|
|
45
45
|
```
|
|
46
46
|
|
|
47
47
|
`write` writes at `mode` (default `0o600`); `writeText` and `writeJson` are convenience wrappers for the common scratch-file shapes; `copyIn` ingests an absolute source path through the same atomic-rename machinery as `Root.copyIn`. `read` is a small accessor that reads back any file you wrote into the workspace.
|
|
48
|
+
Both the async and sync `read` methods throw `FsSafeError("not-file")` when the
|
|
49
|
+
named leaf is a directory or another non-regular target, and preserve the
|
|
50
|
+
`not-found`, `hardlink`, or `symlink` code for those stable target states.
|
|
51
|
+
Operational filesystem read failures use `read-failed` with the Node error in
|
|
52
|
+
`cause`.
|
|
48
53
|
|
|
49
54
|
`store` is a `fileStore({ rootDir: workspace.dir, private: true })` handle. Use
|
|
50
55
|
it when you want the richer store surface, including `writeStream`, `exists`,
|
|
@@ -157,11 +162,15 @@ type TempFile = {
|
|
|
157
162
|
path: string; // absolute path; safe to write to
|
|
158
163
|
dir: string; // the enclosing private workspace dir
|
|
159
164
|
file(fileName?: string): string; // resolve another file in the same dir
|
|
160
|
-
cleanup(): Promise<void>; // removes the private workspace dir
|
|
165
|
+
cleanup(): Promise<void>; // removes the original private workspace dir
|
|
161
166
|
[Symbol.asyncDispose](): Promise<void>; // alias of cleanup()
|
|
162
167
|
};
|
|
163
168
|
```
|
|
164
169
|
|
|
170
|
+
Cleanup captures the directory identity at creation time. If that path is
|
|
171
|
+
renamed away and replaced, cleanup preserves the replacement rather than
|
|
172
|
+
recursively deleting a directory it did not create.
|
|
173
|
+
|
|
165
174
|
### `withTempFile`
|
|
166
175
|
|
|
167
176
|
Same shape with auto-cleanup:
|
|
@@ -239,13 +248,41 @@ const tempRoot = resolveSecureTempRoot({ fallbackPrefix: "my-app" });
|
|
|
239
248
|
|
|
240
249
|
```ts
|
|
241
250
|
type ResolveSecureTempRootOptions = {
|
|
242
|
-
fallbackPrefix: string;
|
|
243
|
-
preferredDir?: string;
|
|
244
|
-
|
|
251
|
+
fallbackPrefix: string; // one portable path segment; invalid values throw
|
|
252
|
+
preferredDir?: string; // preferred secure temp root
|
|
253
|
+
skipPreferredOnWindows?: boolean;
|
|
254
|
+
unsafeFallbackLabel?: string; // text used in thrown errors
|
|
255
|
+
warningPrefix?: string; // default "[fs-safe]"
|
|
256
|
+
warn?: (message: string) => void; // default console.warn
|
|
257
|
+
|
|
258
|
+
// Platform/test adapters; production callers normally omit these.
|
|
259
|
+
platform?: NodeJS.Platform;
|
|
260
|
+
getuid?: () => number | undefined;
|
|
261
|
+
tmpdir?: () => string;
|
|
262
|
+
accessSync?: typeof import("node:fs").accessSync;
|
|
263
|
+
chmodSync?: typeof import("node:fs").chmodSync;
|
|
264
|
+
lstatSync?: (path: string) => {
|
|
265
|
+
isDirectory(): boolean;
|
|
266
|
+
isSymbolicLink(): boolean;
|
|
267
|
+
mode?: number;
|
|
268
|
+
uid?: number;
|
|
269
|
+
};
|
|
270
|
+
mkdirSync?: (
|
|
271
|
+
path: string,
|
|
272
|
+
options: { recursive: boolean; mode?: number },
|
|
273
|
+
) => void;
|
|
245
274
|
};
|
|
246
275
|
```
|
|
247
276
|
|
|
248
|
-
|
|
277
|
+
When `process.getuid()` is available, the fallback is
|
|
278
|
+
`<tmpdir>/<fallbackPrefix>-<uid>`. Without a UID (including Windows), it is
|
|
279
|
+
`<tmpdir>/<fallbackPrefix>`; no username is appended. The helper never returns
|
|
280
|
+
the shared `os.tmpdir()` directory itself. It requires the selected path to be
|
|
281
|
+
a writable, non-symlink directory and, when UID/mode facts are available,
|
|
282
|
+
owned by the current user without group/world write bits. It creates or repairs
|
|
283
|
+
the fallback to mode `0o700` where mode bits apply. If it cannot establish that
|
|
284
|
+
state, it throws an ordinary `Error`; there is no native mode or
|
|
285
|
+
`helper-unavailable` branch on this API.
|
|
249
286
|
|
|
250
287
|
## Common patterns
|
|
251
288
|
|
package/docs/test-hooks.md
CHANGED
|
@@ -37,6 +37,7 @@ type FsSafeTestHooks = {
|
|
|
37
37
|
beforeRootFallbackMutation?: (operation: "mkdir" | "move" | "remove", targetPath: string) => Promise<void> | void;
|
|
38
38
|
afterPinnedWriteFallbackRename?: (targetPath: string) => Promise<void> | void;
|
|
39
39
|
beforeSiblingTempWrite?: (tempPath: string) => Promise<void> | void;
|
|
40
|
+
beforeSidecarLockSnapshotOpen?: (lockPath: string) => Promise<void> | void;
|
|
40
41
|
beforeTrashMove?: (targetPath: string, destPath: string) => void;
|
|
41
42
|
afterPublishTargetCreated?: (method, targetPath, identity) => Promise<void> | void;
|
|
42
43
|
beforePublishDirectorySync?: (method, targetPath, identity) => Promise<void> | void;
|
|
@@ -54,6 +55,7 @@ type FsSafeTestHooks = {
|
|
|
54
55
|
| `beforeRootFallbackMutation` | A guarded JS root fallback is about to mkdir, move, or remove. |
|
|
55
56
|
| `afterPinnedWriteFallbackRename` | A fallback rename committed and post-commit identity checks have not run yet. |
|
|
56
57
|
| `beforeSiblingTempWrite` | A sibling temp file exists and its writer is about to run. |
|
|
58
|
+
| `beforeSidecarLockSnapshotOpen` | A sidecar lock was inspected and is about to be opened for a bounded snapshot read. |
|
|
57
59
|
| `beforeTrashMove` | Trash handling is about to move the target. |
|
|
58
60
|
| `afterPublishTargetCreated` | Exclusive publication created its target and final fences have not run yet. |
|
|
59
61
|
| `beforePublishDirectorySync` | Publication verified the target and is about to sync its parent directory. |
|
package/docs/types.md
CHANGED
|
@@ -2,6 +2,8 @@
|
|
|
2
2
|
|
|
3
3
|
The types most callers reach for. Shared data shapes are exported from `@openclaw/fs-safe/types`; method-specific option/result types live next to their subpath.
|
|
4
4
|
|
|
5
|
+
For atomic replacement, `ReplaceFileAtomicFileSystem` and `ReplaceFileAtomicSyncFileSystem` are exported from `@openclaw/fs-safe/atomic`. Async adapters use `chmod()` on the `FileHandle` returned by their required `open()` operation. The synchronous type adds optional `fchmodSync(fd, mode)`; custom sync adapters that explicitly request `mode` or `preserveExistingMode` must implement it. See [Atomic writes](atomic.md#test-injection).
|
|
6
|
+
|
|
5
7
|
```ts
|
|
6
8
|
import type {
|
|
7
9
|
BasePathOptions,
|
|
@@ -16,15 +18,24 @@ import type {
|
|
|
16
18
|
|
|
17
19
|
```ts
|
|
18
20
|
type PathStat = {
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
21
|
+
dev: number;
|
|
22
|
+
gid: number;
|
|
23
|
+
ino: number;
|
|
24
|
+
isDirectory: boolean;
|
|
25
|
+
isFile: boolean;
|
|
26
|
+
isSymbolicLink: boolean;
|
|
27
|
+
mode: number;
|
|
28
|
+
mtimeMs: number;
|
|
29
|
+
nlink: number;
|
|
30
|
+
size: number;
|
|
31
|
+
uid: number;
|
|
24
32
|
};
|
|
25
33
|
```
|
|
26
34
|
|
|
27
|
-
The shape returned by `Root.stat()`.
|
|
35
|
+
The shape returned by `Root.stat()`. It is a serializable view of the identity,
|
|
36
|
+
ownership, mode, size, timestamp, link count, and three file-kind facts the
|
|
37
|
+
boundary uses. Unlike Node's `Stats`, `isFile`, `isDirectory`, and
|
|
38
|
+
`isSymbolicLink` are boolean fields rather than methods.
|
|
28
39
|
|
|
29
40
|
## `DirEntry`
|
|
30
41
|
|
|
@@ -34,21 +45,23 @@ type DirEntry = PathStat & {
|
|
|
34
45
|
};
|
|
35
46
|
```
|
|
36
47
|
|
|
37
|
-
Returned by `Root.list(rel, { withFileTypes: true })`. Includes
|
|
48
|
+
Returned by `Root.list(rel, { withFileTypes: true })`. Includes every
|
|
49
|
+
`PathStat` field plus the entry's `name`.
|
|
38
50
|
|
|
39
51
|
## `BasePathOptions`
|
|
40
52
|
|
|
41
53
|
```ts
|
|
42
54
|
type BasePathOptions = {
|
|
43
|
-
|
|
55
|
+
rootDir: string;
|
|
56
|
+
relativePath: string;
|
|
44
57
|
};
|
|
45
58
|
|
|
46
59
|
type FastPathMode = "auto" | "never" | "require";
|
|
47
60
|
```
|
|
48
61
|
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
62
|
+
`BasePathOptions` is the shared root-plus-relative-path record. `FastPathMode`
|
|
63
|
+
is retained as a public compatibility union; no current exported options record
|
|
64
|
+
consumes it, so setting a fast-path policy is not part of the current API.
|
|
52
65
|
|
|
53
66
|
## `SafeEncoding`
|
|
54
67
|
|
|
@@ -83,13 +96,16 @@ type ReadResult = {
|
|
|
83
96
|
## `RootDefaults` / `RootOptions`
|
|
84
97
|
|
|
85
98
|
```ts
|
|
99
|
+
type RenameIdentityPolicy = "strict" | "verify-content-with-lock";
|
|
100
|
+
|
|
86
101
|
type RootDefaults = {
|
|
87
102
|
denyMutations?: DenyMutationPolicy;
|
|
88
103
|
hardlinks?: "reject" | "allow";
|
|
89
104
|
maxBytes?: number;
|
|
90
|
-
mkdir?: boolean;
|
|
105
|
+
mkdir?: boolean; // default true for mutation methods
|
|
91
106
|
mode?: number;
|
|
92
107
|
nonBlockingRead?: boolean;
|
|
108
|
+
renameIdentity?: RenameIdentityPolicy;
|
|
93
109
|
symlinks?: "reject" | "follow-within-root";
|
|
94
110
|
};
|
|
95
111
|
|
|
@@ -110,7 +126,7 @@ type RootOptions = {
|
|
|
110
126
|
|
|
111
127
|
```ts
|
|
112
128
|
type RootReadOptions = Pick<RootDefaults, "hardlinks" | "maxBytes" | "nonBlockingRead" | "symlinks">;
|
|
113
|
-
type RootWriteOptions = Pick<RootDefaults, "denyMutations" | "mkdir" | "mode"> & {
|
|
129
|
+
type RootWriteOptions = Pick<RootDefaults, "denyMutations" | "mkdir" | "mode" | "renameIdentity"> & {
|
|
114
130
|
encoding?: BufferEncoding;
|
|
115
131
|
overwrite?: boolean;
|
|
116
132
|
};
|
|
@@ -155,13 +171,14 @@ type FsSafeErrorCode =
|
|
|
155
171
|
| "helper-unavailable" | "insecure-permissions" | "invalid-path"
|
|
156
172
|
| "not-empty" | "not-file" | "not-found" | "not-owned"
|
|
157
173
|
| "not-removable" | "outside-workspace" | "path-alias"
|
|
158
|
-
| "path-mismatch" | "permission-unverified" | "
|
|
174
|
+
| "path-mismatch" | "permission-unverified" | "read-failed" | "secret-exists"
|
|
175
|
+
| "store-reentrant-update" | "symlink"
|
|
159
176
|
| "timeout" | "too-large" | "unsupported-platform";
|
|
160
177
|
```
|
|
161
178
|
|
|
162
179
|
Closed union you switch on. See the [Errors](errors.md) reference for what each one means.
|
|
163
180
|
|
|
164
|
-
`FsSafeError.category` is `"policy"` for unsafe input
|
|
181
|
+
`FsSafeError.category` is `"policy"` for unsafe input or target state rejected by a safety policy and `"operational"` for routine filesystem outcomes or environment/runtime failures. `not-found`, `not-empty`, `not-removable`, and `read-failed` are operational.
|
|
165
182
|
|
|
166
183
|
## See also
|
|
167
184
|
|
package/docs/writing.md
CHANGED
|
@@ -1,6 +1,11 @@
|
|
|
1
1
|
# Writing
|
|
2
2
|
|
|
3
|
-
The `Root` handle exposes a tight set of
|
|
3
|
+
The `Root` handle exposes a tight set of mutation verbs. Replacement writes
|
|
4
|
+
(`write`, `create`, `writeJson`, `createJson`, and `copyIn`) publish with a
|
|
5
|
+
sibling-temp commit so no half-written replacement appears at the destination.
|
|
6
|
+
`append` and `openWritable` intentionally modify an opened file in place;
|
|
7
|
+
`move`, `remove`, and `mkdir` mutate directory entries rather than file bytes.
|
|
8
|
+
Each verb applies the boundary checks appropriate to its operation.
|
|
4
9
|
|
|
5
10
|
```ts
|
|
6
11
|
await fs.write("state.json", body);
|
|
@@ -13,16 +18,20 @@ await fs.remove("logs/yesterday.log");
|
|
|
13
18
|
await fs.mkdir("snapshots/2026/05");
|
|
14
19
|
```
|
|
15
20
|
|
|
16
|
-
## What
|
|
21
|
+
## What replacement writes do
|
|
17
22
|
|
|
18
23
|
1. Resolve the relative target against the canonical root and reject anything that escapes (`outside-workspace`).
|
|
19
|
-
2. If `mkdir: true`, create missing parent directories
|
|
20
|
-
3. Pin or guard the parent directory for the selected mechanism. Native operations use a parent fd; guarded JavaScript verifies directory identity before and after mutation.
|
|
24
|
+
2. If `mkdir: true`, create missing parent directories relative to a pinned parent fd in the native path, or with per-component identity guards in the JavaScript fallback.
|
|
25
|
+
3. Pin or guard the parent directory for the selected mechanism. Native operations use a parent fd; guarded JavaScript verifies directory identity before and after mutation. The JavaScript check cannot make the intervening pathname syscall atomic, so a same-privilege peer that can replace the parent may cause an out-of-root side effect before detection. Use native `require` mode for that threat model; see the [security model](security-model.md#symlinks-write-side).
|
|
21
26
|
4. Write data to a sibling temp file in the same directory.
|
|
22
27
|
5. Atomically rename the temp file over the destination.
|
|
23
28
|
6. Stat the resulting fd and verify identity.
|
|
24
29
|
|
|
25
|
-
A failure
|
|
30
|
+
A failure before the final rename leaves the destination at its previous
|
|
31
|
+
contents. A successful rename publishes the complete replacement. This
|
|
32
|
+
old-or-new guarantee does not apply to `append()` or `openWritable()`, which
|
|
33
|
+
write in place, or to lower-level atomic helpers when their explicitly
|
|
34
|
+
non-atomic permission-error copy fallback is enabled.
|
|
26
35
|
|
|
27
36
|
## Denying mutations
|
|
28
37
|
|
|
@@ -53,7 +62,7 @@ await fs.write("state/last-run.json", JSON.stringify(run));
|
|
|
53
62
|
await fs.write("notes/today.txt", "hello\n", { encoding: "utf8" });
|
|
54
63
|
```
|
|
55
64
|
|
|
56
|
-
`data` accepts `string | Buffer`. `options` are `{ denyMutations?: DenyMutationPolicy; encoding?: BufferEncoding; mkdir?: boolean; mode?: number; overwrite?: boolean }`. `mode` sets the file's POSIX mode
|
|
65
|
+
`data` accepts `string | Buffer`. `options` are `{ denyMutations?: DenyMutationPolicy; encoding?: BufferEncoding; mkdir?: boolean; mode?: number; overwrite?: boolean; renameIdentity?: RenameIdentityPolicy }`. `mode` sets the file's POSIX mode. If neither the call nor `RootDefaults` supplies it, a replacement preserves the existing file mode and a new file uses `0o600`. `mkdir` and `overwrite` both default to `true`; set `overwrite: false` for the same no-clobber behavior as `create()`.
|
|
57
66
|
|
|
58
67
|
### `fs.create(rel, data, options?)`
|
|
59
68
|
|
|
@@ -79,10 +88,7 @@ await fs.writeJson("compact.json", state, { trailingNewline: false });
|
|
|
79
88
|
Options:
|
|
80
89
|
|
|
81
90
|
```ts
|
|
82
|
-
type RootWriteJsonOptions = {
|
|
83
|
-
encoding?: BufferEncoding;
|
|
84
|
-
mkdir?: boolean;
|
|
85
|
-
mode?: number;
|
|
91
|
+
type RootWriteJsonOptions = RootWriteOptions & {
|
|
86
92
|
replacer?: (this: any, key: string, value: any) => any | (number | string)[];
|
|
87
93
|
space?: number | string;
|
|
88
94
|
trailingNewline?: boolean; // default true
|
|
@@ -112,7 +118,9 @@ await fs.copyIn("inbox/upload.bin", "/tmp/incoming.bin", {
|
|
|
112
118
|
});
|
|
113
119
|
```
|
|
114
120
|
|
|
115
|
-
Options
|
|
121
|
+
Options are `{ denyMutations?, maxBytes?, mkdir?, mode?, sourceHardlinks? }`.
|
|
122
|
+
Use `sourceHardlinks: "reject"` to refuse if the source itself is a hardlinked
|
|
123
|
+
alias. There is no encoding option: copying preserves source bytes.
|
|
116
124
|
|
|
117
125
|
### `fs.move(from, to, options?)`
|
|
118
126
|
|
|
@@ -167,7 +175,12 @@ try {
|
|
|
167
175
|
}
|
|
168
176
|
```
|
|
169
177
|
|
|
170
|
-
Options
|
|
178
|
+
Options are `{ denyMutations?, mkdir?, mode?, writeMode? }`, where `writeMode`
|
|
179
|
+
is `"replace"` (default), `"append"`, or `"update"`. `replace` truncates existing
|
|
180
|
+
files; `update` keeps existing contents. Streaming writes go directly to the
|
|
181
|
+
destination — there is no atomic-rename step. If you need both streaming and
|
|
182
|
+
atomicity, write to a sibling temp yourself and rename when done; the
|
|
183
|
+
[`atomic`](atomic.md) helpers can do this for you.
|
|
171
184
|
|
|
172
185
|
## Write defaults vs per-call options
|
|
173
186
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@openclaw/fs-safe",
|
|
3
|
-
"version": "0.5.
|
|
3
|
+
"version": "0.5.3",
|
|
4
4
|
"description": "Capability-style filesystem roots for Node.js apps that handle untrusted relative paths.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"filesystem",
|
|
@@ -29,6 +29,7 @@
|
|
|
29
29
|
"dist/**/*.d.ts.map",
|
|
30
30
|
"dist/native/**/*.node",
|
|
31
31
|
"docs/**/*.md",
|
|
32
|
+
"docs/assets/readme-banner.jpg",
|
|
32
33
|
"README.md",
|
|
33
34
|
"CHANGELOG.md",
|
|
34
35
|
"SECURITY.md",
|
|
@@ -128,12 +129,16 @@
|
|
|
128
129
|
"prepack": "node scripts/prepack-build.mjs",
|
|
129
130
|
"test": "vitest run",
|
|
130
131
|
"test:coverage": "vitest run --coverage",
|
|
132
|
+
"test:coverage:collect": "vitest run --coverage --coverage.reporter=json --coverage.thresholds.lines=0 --coverage.thresholds.functions=0 --coverage.thresholds.statements=0 --coverage.thresholds.branches=0",
|
|
133
|
+
"test:coverage:merge": "node scripts/merge-coverage.mjs",
|
|
131
134
|
"test:security": "vitest run test/fs-safe.test.ts test/read-boundary-bypass.test.ts test/write-boundary-bypass.test.ts test/additional-boundary-bypass.test.ts test/adversarial-boundary-payloads.test.ts",
|
|
132
|
-
"check": "pnpm lint:file-size && pnpm lint:fs-boundary && pnpm build && pnpm test && node scripts/check-pack.mjs",
|
|
135
|
+
"check": "pnpm lint:file-size && pnpm lint:fs-boundary && pnpm build && pnpm docs:check && pnpm test && node scripts/check-pack.mjs",
|
|
136
|
+
"docs:check": "node scripts/check-doc-examples.mjs",
|
|
133
137
|
"docs:site": "node scripts/build-docs-site.mjs",
|
|
134
138
|
"native:build": "pnpm --filter @openclaw/fs-safe-native-build build",
|
|
135
139
|
"native:test": "cargo test --manifest-path native/Cargo.toml",
|
|
136
140
|
"pack:check": "pnpm build && node scripts/check-pack.mjs",
|
|
141
|
+
"public-api:update": "pnpm build && node scripts/check-pack.mjs --update-public-api",
|
|
137
142
|
"package:smoke": "node scripts/check-release-packages.mjs --allow-host-only --output release-artifacts",
|
|
138
143
|
"check:changed": "pnpm run check",
|
|
139
144
|
"release:notes": "node scripts/release-notes.mjs",
|
|
@@ -148,11 +153,17 @@
|
|
|
148
153
|
"tar": "7.5.22"
|
|
149
154
|
},
|
|
150
155
|
"devDependencies": {
|
|
151
|
-
"@
|
|
156
|
+
"@emnapi/runtime": "2.0.0-alpha.3",
|
|
157
|
+
"@napi-rs/cli": "3.8.2",
|
|
152
158
|
"@types/node": "^26.1.2",
|
|
153
159
|
"@vitest/coverage-v8": "4.1.10",
|
|
160
|
+
"fast-check": "^4.9.0",
|
|
161
|
+
"istanbul-lib-coverage": "3.2.2",
|
|
162
|
+
"istanbul-lib-report": "3.0.1",
|
|
163
|
+
"istanbul-reports": "3.2.0",
|
|
164
|
+
"sigstore": "5.0.0",
|
|
154
165
|
"typescript": "^7.0.2",
|
|
155
|
-
"vite": "8.2.
|
|
166
|
+
"vite": "8.2.1",
|
|
156
167
|
"vitest": "^4.1.10"
|
|
157
168
|
},
|
|
158
169
|
"engines": {
|
|
@@ -1,9 +0,0 @@
|
|
|
1
|
-
import type { FileHandle } from "node:fs/promises";
|
|
2
|
-
import type { ExtractionDeadline } from "./archive-deadline.js";
|
|
3
|
-
export declare function writeFileHandleFully(params: {
|
|
4
|
-
handle: FileHandle;
|
|
5
|
-
buffer: Buffer;
|
|
6
|
-
bytes: number;
|
|
7
|
-
deadline: ExtractionDeadline;
|
|
8
|
-
}): Promise<void>;
|
|
9
|
-
//# sourceMappingURL=archive-file-io.d.ts.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"archive-file-io.d.ts","sourceRoot":"","sources":["../src/archive-file-io.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,kBAAkB,CAAC;AACnD,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,uBAAuB,CAAC;AAEhE,wBAAsB,oBAAoB,CAAC,MAAM,EAAE;IACjD,MAAM,EAAE,UAAU,CAAC;IACnB,MAAM,EAAE,MAAM,CAAC;IACf,KAAK,EAAE,MAAM,CAAC;IACd,QAAQ,EAAE,kBAAkB,CAAC;CAC9B,GAAG,OAAO,CAAC,IAAI,CAAC,CAchB"}
|
package/dist/archive-file-io.js
DELETED
|
@@ -1,11 +0,0 @@
|
|
|
1
|
-
export async function writeFileHandleFully(params) {
|
|
2
|
-
let offset = 0;
|
|
3
|
-
while (offset < params.bytes) {
|
|
4
|
-
params.deadline.check();
|
|
5
|
-
const { bytesWritten } = await params.handle.write(params.buffer, offset, params.bytes - offset);
|
|
6
|
-
if (bytesWritten <= 0) {
|
|
7
|
-
throw new Error("archive staging write made no progress");
|
|
8
|
-
}
|
|
9
|
-
offset += bytesWritten;
|
|
10
|
-
}
|
|
11
|
-
}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"file-store-source.d.ts","sourceRoot":"","sources":["../src/file-store-source.ts"],"names":[],"mappings":"AAIA,wBAAsB,uBAAuB,CAAC,MAAM,EAAE;IACpD,UAAU,EAAE,MAAM,CAAC;IACnB,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB,GAAG,OAAO,CAAC,MAAM,CAAC,CAyBlB"}
|