@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/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
|
|
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,12 +68,22 @@ 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
|
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
|
@@ -157,11 +157,15 @@ type TempFile = {
|
|
|
157
157
|
path: string; // absolute path; safe to write to
|
|
158
158
|
dir: string; // the enclosing private workspace dir
|
|
159
159
|
file(fileName?: string): string; // resolve another file in the same dir
|
|
160
|
-
cleanup(): Promise<void>; // removes the private workspace dir
|
|
160
|
+
cleanup(): Promise<void>; // removes the original private workspace dir
|
|
161
161
|
[Symbol.asyncDispose](): Promise<void>; // alias of cleanup()
|
|
162
162
|
};
|
|
163
163
|
```
|
|
164
164
|
|
|
165
|
+
Cleanup captures the directory identity at creation time. If that path is
|
|
166
|
+
renamed away and replaced, cleanup preserves the replacement rather than
|
|
167
|
+
recursively deleting a directory it did not create.
|
|
168
|
+
|
|
165
169
|
### `withTempFile`
|
|
166
170
|
|
|
167
171
|
Same shape with auto-cleanup:
|
|
@@ -239,13 +243,41 @@ const tempRoot = resolveSecureTempRoot({ fallbackPrefix: "my-app" });
|
|
|
239
243
|
|
|
240
244
|
```ts
|
|
241
245
|
type ResolveSecureTempRootOptions = {
|
|
242
|
-
fallbackPrefix: string;
|
|
243
|
-
preferredDir?: string;
|
|
244
|
-
|
|
246
|
+
fallbackPrefix: string; // one portable path segment; invalid values throw
|
|
247
|
+
preferredDir?: string; // preferred secure temp root
|
|
248
|
+
skipPreferredOnWindows?: boolean;
|
|
249
|
+
unsafeFallbackLabel?: string; // text used in thrown errors
|
|
250
|
+
warningPrefix?: string; // default "[fs-safe]"
|
|
251
|
+
warn?: (message: string) => void; // default console.warn
|
|
252
|
+
|
|
253
|
+
// Platform/test adapters; production callers normally omit these.
|
|
254
|
+
platform?: NodeJS.Platform;
|
|
255
|
+
getuid?: () => number | undefined;
|
|
256
|
+
tmpdir?: () => string;
|
|
257
|
+
accessSync?: typeof import("node:fs").accessSync;
|
|
258
|
+
chmodSync?: typeof import("node:fs").chmodSync;
|
|
259
|
+
lstatSync?: (path: string) => {
|
|
260
|
+
isDirectory(): boolean;
|
|
261
|
+
isSymbolicLink(): boolean;
|
|
262
|
+
mode?: number;
|
|
263
|
+
uid?: number;
|
|
264
|
+
};
|
|
265
|
+
mkdirSync?: (
|
|
266
|
+
path: string,
|
|
267
|
+
options: { recursive: boolean; mode?: number },
|
|
268
|
+
) => void;
|
|
245
269
|
};
|
|
246
270
|
```
|
|
247
271
|
|
|
248
|
-
|
|
272
|
+
When `process.getuid()` is available, the fallback is
|
|
273
|
+
`<tmpdir>/<fallbackPrefix>-<uid>`. Without a UID (including Windows), it is
|
|
274
|
+
`<tmpdir>/<fallbackPrefix>`; no username is appended. The helper never returns
|
|
275
|
+
the shared `os.tmpdir()` directory itself. It requires the selected path to be
|
|
276
|
+
a writable, non-symlink directory and, when UID/mode facts are available,
|
|
277
|
+
owned by the current user without group/world write bits. It creates or repairs
|
|
278
|
+
the fallback to mode `0o700` where mode bits apply. If it cannot establish that
|
|
279
|
+
state, it throws an ordinary `Error`; there is no native mode or
|
|
280
|
+
`helper-unavailable` branch on this API.
|
|
249
281
|
|
|
250
282
|
## Common patterns
|
|
251
283
|
|