@openclaw/fs-safe 0.5.1 → 0.5.2
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 +66 -0
- package/README.md +18 -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 +25 -5
- 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 +6 -3
- 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 +30 -12
- 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 +7 -1
- package/dist/archive-tar.d.ts.map +1 -1
- package/dist/archive-tar.js +7 -4
- package/dist/archive-zip-entry.d.ts +1 -0
- package/dist/archive-zip-entry.d.ts.map +1 -1
- package/dist/archive-zip-integrity.d.ts +4 -0
- package/dist/archive-zip-integrity.d.ts.map +1 -0
- package/dist/archive-zip-integrity.js +44 -0
- package/dist/archive-zip-preflight.d.ts.map +1 -1
- package/dist/archive-zip-preflight.js +6 -1
- package/dist/archive.d.ts.map +1 -1
- package/dist/archive.js +5 -2
- 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.map +1 -1
- package/dist/errors.js +3 -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 +36 -128
- 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-write.js +2 -1
- 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 +213 -168
- 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 +19 -3
- 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/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/walk.d.ts.map +1 -1
- package/dist/walk.js +15 -0
- package/docs/advanced.md +1 -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 +31 -7
- package/docs/file-store.md +10 -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 +17 -4
- package/docs/security-model.md +21 -4
- package/docs/sidecar-lock.md +15 -5
- package/docs/temp.md +37 -5
- package/docs/test-hooks.md +2 -0
- package/docs/types.md +32 -15
- package/docs/writing.md +25 -12
- package/package.json +7 -3
- 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/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" | "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`, and `not-removable` 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.2",
|
|
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",
|
|
@@ -129,11 +130,13 @@
|
|
|
129
130
|
"test": "vitest run",
|
|
130
131
|
"test:coverage": "vitest run --coverage",
|
|
131
132
|
"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",
|
|
133
|
+
"check": "pnpm lint:file-size && pnpm lint:fs-boundary && pnpm build && pnpm docs:check && pnpm test && node scripts/check-pack.mjs",
|
|
134
|
+
"docs:check": "node scripts/check-doc-examples.mjs",
|
|
133
135
|
"docs:site": "node scripts/build-docs-site.mjs",
|
|
134
136
|
"native:build": "pnpm --filter @openclaw/fs-safe-native-build build",
|
|
135
137
|
"native:test": "cargo test --manifest-path native/Cargo.toml",
|
|
136
138
|
"pack:check": "pnpm build && node scripts/check-pack.mjs",
|
|
139
|
+
"public-api:update": "pnpm build && node scripts/check-pack.mjs --update-public-api",
|
|
137
140
|
"package:smoke": "node scripts/check-release-packages.mjs --allow-host-only --output release-artifacts",
|
|
138
141
|
"check:changed": "pnpm run check",
|
|
139
142
|
"release:notes": "node scripts/release-notes.mjs",
|
|
@@ -148,9 +151,10 @@
|
|
|
148
151
|
"tar": "7.5.22"
|
|
149
152
|
},
|
|
150
153
|
"devDependencies": {
|
|
151
|
-
"@napi-rs/cli": "3.8.
|
|
154
|
+
"@napi-rs/cli": "3.8.2",
|
|
152
155
|
"@types/node": "^26.1.2",
|
|
153
156
|
"@vitest/coverage-v8": "4.1.10",
|
|
157
|
+
"sigstore": "5.0.0",
|
|
154
158
|
"typescript": "^7.0.2",
|
|
155
159
|
"vite": "8.2.0",
|
|
156
160
|
"vitest": "^4.1.10"
|
|
@@ -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"}
|
|
@@ -1,30 +0,0 @@
|
|
|
1
|
-
import fs from "node:fs/promises";
|
|
2
|
-
import { FsSafeError } from "./errors.js";
|
|
3
|
-
import { readRegularFile } from "./regular-file.js";
|
|
4
|
-
export async function readFileStoreCopySource(params) {
|
|
5
|
-
const sourceStat = await fs.lstat(params.sourcePath);
|
|
6
|
-
if (sourceStat.isSymbolicLink() || !sourceStat.isFile()) {
|
|
7
|
-
throw new FsSafeError("not-file", "source path is not a file");
|
|
8
|
-
}
|
|
9
|
-
if (params.maxBytes !== undefined && sourceStat.size > params.maxBytes) {
|
|
10
|
-
throw new FsSafeError("too-large", `file exceeds maximum size of ${params.maxBytes} bytes`);
|
|
11
|
-
}
|
|
12
|
-
try {
|
|
13
|
-
return (await readRegularFile({ filePath: params.sourcePath, maxBytes: params.maxBytes }))
|
|
14
|
-
.buffer;
|
|
15
|
-
}
|
|
16
|
-
catch (error) {
|
|
17
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
18
|
-
if (message.includes("regular file") || message.includes("not a regular file")) {
|
|
19
|
-
throw new FsSafeError("not-file", "source path is not a file", {
|
|
20
|
-
cause: error instanceof Error ? error : undefined,
|
|
21
|
-
});
|
|
22
|
-
}
|
|
23
|
-
if (params.maxBytes !== undefined && message.includes(`exceeds ${params.maxBytes} bytes`)) {
|
|
24
|
-
throw new FsSafeError("too-large", `file exceeds maximum size of ${params.maxBytes} bytes`, {
|
|
25
|
-
cause: error instanceof Error ? error : undefined,
|
|
26
|
-
});
|
|
27
|
-
}
|
|
28
|
-
throw error;
|
|
29
|
-
}
|
|
30
|
-
}
|
package/dist/file-url.d.ts
DELETED
package/dist/file-url.d.ts.map
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"file-url.d.ts","sourceRoot":"","sources":["../src/file-url.ts"],"names":[],"mappings":"AAAA,cAAc,wBAAwB,CAAC"}
|
package/dist/file-url.js
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export * from "./local-file-access.js";
|
package/dist/path-stat.d.ts
DELETED
package/dist/path-stat.d.ts.map
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"path-stat.d.ts","sourceRoot":"","sources":["../src/path-stat.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,SAAS,CAAC;AACrC,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,YAAY,CAAC;AAE3C,wBAAgB,iBAAiB,CAAC,IAAI,EAAE,KAAK,GAAG,QAAQ,CAcvD"}
|
package/dist/path-stat.js
DELETED
|
@@ -1,15 +0,0 @@
|
|
|
1
|
-
export function pathStatFromStats(stat) {
|
|
2
|
-
return {
|
|
3
|
-
dev: Number(stat.dev),
|
|
4
|
-
gid: Number(stat.gid),
|
|
5
|
-
ino: Number(stat.ino),
|
|
6
|
-
isDirectory: stat.isDirectory(),
|
|
7
|
-
isFile: stat.isFile(),
|
|
8
|
-
isSymbolicLink: stat.isSymbolicLink(),
|
|
9
|
-
mode: stat.mode,
|
|
10
|
-
mtimeMs: stat.mtimeMs,
|
|
11
|
-
nlink: stat.nlink,
|
|
12
|
-
size: stat.size,
|
|
13
|
-
uid: stat.uid,
|
|
14
|
-
};
|
|
15
|
-
}
|
|
@@ -1,9 +0,0 @@
|
|
|
1
|
-
import type { PermissionCheck, SafeStatResult } from "./permissions.js";
|
|
2
|
-
export declare function inspectWindowsPermissionsNative(params: {
|
|
3
|
-
targetPath: string;
|
|
4
|
-
stat: SafeStatResult;
|
|
5
|
-
effectiveIsDir: boolean;
|
|
6
|
-
effectiveMode: number | null;
|
|
7
|
-
bits: number | null;
|
|
8
|
-
}): PermissionCheck | undefined;
|
|
9
|
-
//# sourceMappingURL=windows-permissions-native.d.ts.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"windows-permissions-native.d.ts","sourceRoot":"","sources":["../src/windows-permissions-native.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,eAAe,EAAE,cAAc,EAAE,MAAM,kBAAkB,CAAC;AAExE,wBAAgB,+BAA+B,CAAC,MAAM,EAAE;IACtD,UAAU,EAAE,MAAM,CAAC;IACnB,IAAI,EAAE,cAAc,CAAC;IACrB,cAAc,EAAE,OAAO,CAAC;IACxB,aAAa,EAAE,MAAM,GAAG,IAAI,CAAC;IAC7B,IAAI,EAAE,MAAM,GAAG,IAAI,CAAC;CACrB,GAAG,eAAe,GAAG,SAAS,CA2B9B"}
|
|
@@ -1,31 +0,0 @@
|
|
|
1
|
-
import { getNativeBinding } from "./native.js";
|
|
2
|
-
export function inspectWindowsPermissionsNative(params) {
|
|
3
|
-
const native = getNativeBinding();
|
|
4
|
-
if (!native)
|
|
5
|
-
return undefined;
|
|
6
|
-
try {
|
|
7
|
-
const facts = native.readOwnerAndDacl(params.targetPath);
|
|
8
|
-
if (facts.fallbackRequired)
|
|
9
|
-
return undefined;
|
|
10
|
-
return {
|
|
11
|
-
ok: true,
|
|
12
|
-
isSymlink: params.stat.isSymlink,
|
|
13
|
-
isDir: params.effectiveIsDir,
|
|
14
|
-
mode: params.effectiveMode,
|
|
15
|
-
bits: params.bits,
|
|
16
|
-
source: "windows-acl",
|
|
17
|
-
worldWritable: facts.worldWritable,
|
|
18
|
-
groupWritable: facts.groupWritable,
|
|
19
|
-
worldReadable: facts.worldReadable,
|
|
20
|
-
groupReadable: facts.groupReadable,
|
|
21
|
-
ownerSid: facts.ownerSid,
|
|
22
|
-
ownerTrusted: facts.ownerClass !== "foreign",
|
|
23
|
-
aclSummary: `native owner=${facts.ownerClass} world=` +
|
|
24
|
-
`${facts.worldReadable ? "r" : "-"}${facts.worldWritable ? "w" : "-"} ` +
|
|
25
|
-
`group=${facts.groupReadable ? "r" : "-"}${facts.groupWritable ? "w" : "-"}`,
|
|
26
|
-
};
|
|
27
|
-
}
|
|
28
|
-
catch {
|
|
29
|
-
return undefined;
|
|
30
|
-
}
|
|
31
|
-
}
|