@openclaw/fs-safe 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +15 -0
- package/LICENSE +21 -0
- package/README.md +390 -0
- package/SECURITY.md +12 -0
- package/dist/absolute-path.d.ts +19 -0
- package/dist/absolute-path.d.ts.map +1 -0
- package/dist/absolute-path.js +98 -0
- package/dist/advanced.d.ts +22 -0
- package/dist/advanced.d.ts.map +1 -0
- package/dist/advanced.js +24 -0
- package/dist/archive-entry.d.ts +13 -0
- package/dist/archive-entry.d.ts.map +1 -0
- package/dist/archive-entry.js +47 -0
- package/dist/archive-kind.d.ts +8 -0
- package/dist/archive-kind.d.ts.map +1 -0
- package/dist/archive-kind.js +58 -0
- package/dist/archive-limits.d.ts +40 -0
- package/dist/archive-limits.d.ts.map +1 -0
- package/dist/archive-limits.js +92 -0
- package/dist/archive-staging.d.ts +26 -0
- package/dist/archive-staging.d.ts.map +1 -0
- package/dist/archive-staging.js +171 -0
- package/dist/archive-tar.d.ts +14 -0
- package/dist/archive-tar.d.ts.map +1 -0
- package/dist/archive-tar.js +52 -0
- package/dist/archive-utils.d.ts +3 -0
- package/dist/archive-utils.d.ts.map +1 -0
- package/dist/archive-utils.js +26 -0
- package/dist/archive-zip-preflight.d.ts +7 -0
- package/dist/archive-zip-preflight.d.ts.map +1 -0
- package/dist/archive-zip-preflight.js +170 -0
- package/dist/archive.d.ts +24 -0
- package/dist/archive.d.ts.map +1 -0
- package/dist/archive.js +251 -0
- package/dist/async-lock.d.ts +2 -0
- package/dist/async-lock.d.ts.map +1 -0
- package/dist/async-lock.js +17 -0
- package/dist/atomic.d.ts +5 -0
- package/dist/atomic.d.ts.map +1 -0
- package/dist/atomic.js +4 -0
- package/dist/boundary-file-read.d.ts +44 -0
- package/dist/boundary-file-read.d.ts.map +1 -0
- package/dist/boundary-file-read.js +129 -0
- package/dist/boundary-path.d.ts +39 -0
- package/dist/boundary-path.d.ts.map +1 -0
- package/dist/boundary-path.js +598 -0
- package/dist/config.d.ts +2 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/config.js +1 -0
- package/dist/errors.d.ts +11 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/errors.js +20 -0
- package/dist/file-identity.d.ts +6 -0
- package/dist/file-identity.d.ts.map +1 -0
- package/dist/file-identity.js +14 -0
- package/dist/file-lock.d.ts +20 -0
- package/dist/file-lock.d.ts.map +1 -0
- package/dist/file-lock.js +34 -0
- package/dist/file-store.d.ts +66 -0
- package/dist/file-store.d.ts.map +1 -0
- package/dist/file-store.js +436 -0
- package/dist/file-url.d.ts +2 -0
- package/dist/file-url.d.ts.map +1 -0
- package/dist/file-url.js +1 -0
- package/dist/filename.d.ts +2 -0
- package/dist/filename.d.ts.map +1 -0
- package/dist/filename.js +25 -0
- package/dist/fs-pinned-path-helper.d.ts +7 -0
- package/dist/fs-pinned-path-helper.d.ts.map +1 -0
- package/dist/fs-pinned-path-helper.js +182 -0
- package/dist/fs-pinned-write-helper.d.ts +21 -0
- package/dist/fs-pinned-write-helper.d.ts.map +1 -0
- package/dist/fs-pinned-write-helper.js +263 -0
- package/dist/fs.d.ts +13 -0
- package/dist/fs.d.ts.map +1 -0
- package/dist/fs.js +30 -0
- package/dist/hardlink-guards.d.ts +7 -0
- package/dist/hardlink-guards.d.ts.map +1 -0
- package/dist/hardlink-guards.js +30 -0
- package/dist/home-dir.d.ts +22 -0
- package/dist/home-dir.d.ts.map +1 -0
- package/dist/home-dir.js +107 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +3 -0
- package/dist/install-path.d.ts +20 -0
- package/dist/install-path.d.ts.map +1 -0
- package/dist/install-path.js +94 -0
- package/dist/install-safe-path.d.ts +20 -0
- package/dist/install-safe-path.d.ts.map +1 -0
- package/dist/install-safe-path.js +94 -0
- package/dist/json-document-store.d.ts +30 -0
- package/dist/json-document-store.d.ts.map +1 -0
- package/dist/json-document-store.js +68 -0
- package/dist/json-file.d.ts +3 -0
- package/dist/json-file.d.ts.map +1 -0
- package/dist/json-file.js +123 -0
- package/dist/json-files.d.ts +20 -0
- package/dist/json-files.d.ts.map +1 -0
- package/dist/json-files.js +153 -0
- package/dist/json-store.d.ts +9 -0
- package/dist/json-store.d.ts.map +1 -0
- package/dist/json-store.js +12 -0
- package/dist/json.d.ts +17 -0
- package/dist/json.d.ts.map +1 -0
- package/dist/json.js +198 -0
- package/dist/local-file-access.d.ts +8 -0
- package/dist/local-file-access.d.ts.map +1 -0
- package/dist/local-file-access.js +73 -0
- package/dist/local-roots.d.ts +26 -0
- package/dist/local-roots.d.ts.map +1 -0
- package/dist/local-roots.js +150 -0
- package/dist/mode.d.ts +2 -0
- package/dist/mode.d.ts.map +1 -0
- package/dist/mode.js +3 -0
- package/dist/move-path.d.ts +6 -0
- package/dist/move-path.d.ts.map +1 -0
- package/dist/move-path.js +18 -0
- package/dist/path-alias-guards.d.ts +19 -0
- package/dist/path-alias-guards.d.ts.map +1 -0
- package/dist/path-alias-guards.js +21 -0
- package/dist/path-guards.d.ts +7 -0
- package/dist/path-guards.d.ts.map +1 -0
- package/dist/path-guards.js +49 -0
- package/dist/path-policy.d.ts +25 -0
- package/dist/path-policy.d.ts.map +1 -0
- package/dist/path-policy.js +50 -0
- package/dist/path-safety.d.ts +12 -0
- package/dist/path-safety.d.ts.map +1 -0
- package/dist/path-safety.js +50 -0
- package/dist/path-scope.d.ts +2 -0
- package/dist/path-scope.d.ts.map +1 -0
- package/dist/path-scope.js +1 -0
- package/dist/path.d.ts +18 -0
- package/dist/path.d.ts.map +1 -0
- package/dist/path.js +123 -0
- package/dist/permissions-public.d.ts +2 -0
- package/dist/permissions-public.d.ts.map +1 -0
- package/dist/permissions-public.js +1 -0
- package/dist/permissions.d.ts +86 -0
- package/dist/permissions.d.ts.map +1 -0
- package/dist/permissions.js +435 -0
- package/dist/pinned-helper.d.ts +9 -0
- package/dist/pinned-helper.d.ts.map +1 -0
- package/dist/pinned-helper.js +19 -0
- package/dist/pinned-open.d.ts +24 -0
- package/dist/pinned-open.d.ts.map +1 -0
- package/dist/pinned-open.js +71 -0
- package/dist/pinned-path.d.ts +7 -0
- package/dist/pinned-path.d.ts.map +1 -0
- package/dist/pinned-path.js +21 -0
- package/dist/pinned-python-config.d.ts +9 -0
- package/dist/pinned-python-config.d.ts.map +1 -0
- package/dist/pinned-python-config.js +39 -0
- package/dist/pinned-python.d.ts +12 -0
- package/dist/pinned-python.d.ts.map +1 -0
- package/dist/pinned-python.js +599 -0
- package/dist/pinned-write.d.ts +33 -0
- package/dist/pinned-write.d.ts.map +1 -0
- package/dist/pinned-write.js +198 -0
- package/dist/private-file-store.d.ts +60 -0
- package/dist/private-file-store.d.ts.map +1 -0
- package/dist/private-file-store.js +233 -0
- package/dist/private-temp-workspace.d.ts +39 -0
- package/dist/private-temp-workspace.d.ts.map +1 -0
- package/dist/private-temp-workspace.js +185 -0
- package/dist/regular-file.d.ts +38 -0
- package/dist/regular-file.d.ts.map +1 -0
- package/dist/regular-file.js +239 -0
- package/dist/replace-directory.d.ts +7 -0
- package/dist/replace-directory.d.ts.map +1 -0
- package/dist/replace-directory.js +32 -0
- package/dist/replace-file.d.ts +41 -0
- package/dist/replace-file.d.ts.map +1 -0
- package/dist/replace-file.js +325 -0
- package/dist/root-file.d.ts +44 -0
- package/dist/root-file.d.ts.map +1 -0
- package/dist/root-file.js +129 -0
- package/dist/root-path.d.ts +39 -0
- package/dist/root-path.d.ts.map +1 -0
- package/dist/root-path.js +598 -0
- package/dist/root-paths.d.ts +92 -0
- package/dist/root-paths.d.ts.map +1 -0
- package/dist/root-paths.js +338 -0
- package/dist/root.d.ts +108 -0
- package/dist/root.d.ts.map +1 -0
- package/dist/root.js +1332 -0
- package/dist/safe-open-sync.d.ts +24 -0
- package/dist/safe-open-sync.d.ts.map +1 -0
- package/dist/safe-open-sync.js +71 -0
- package/dist/safe-root.d.ts +123 -0
- package/dist/safe-root.d.ts.map +1 -0
- package/dist/safe-root.js +1060 -0
- package/dist/secret-file.d.ts +17 -0
- package/dist/secret-file.d.ts.map +1 -0
- package/dist/secret-file.js +232 -0
- package/dist/secret.d.ts +2 -0
- package/dist/secret.d.ts.map +1 -0
- package/dist/secret.js +1 -0
- package/dist/secure-file.d.ts +32 -0
- package/dist/secure-file.d.ts.map +1 -0
- package/dist/secure-file.js +163 -0
- package/dist/secure-temp-dir.d.ts +27 -0
- package/dist/secure-temp-dir.d.ts.map +1 -0
- package/dist/secure-temp-dir.js +155 -0
- package/dist/secure-temp-workspace.d.ts +25 -0
- package/dist/secure-temp-workspace.d.ts.map +1 -0
- package/dist/secure-temp-workspace.js +136 -0
- package/dist/sibling-temp-file.d.ts +16 -0
- package/dist/sibling-temp-file.d.ts.map +1 -0
- package/dist/sibling-temp-file.js +73 -0
- package/dist/sibling-temp-write.d.ts +8 -0
- package/dist/sibling-temp-write.d.ts.map +1 -0
- package/dist/sibling-temp-write.js +40 -0
- package/dist/sibling-temp.d.ts +23 -0
- package/dist/sibling-temp.d.ts.map +1 -0
- package/dist/sibling-temp.js +119 -0
- package/dist/sidecar-lock.d.ts +50 -0
- package/dist/sidecar-lock.d.ts.map +1 -0
- package/dist/sidecar-lock.js +235 -0
- package/dist/store.d.ts +3 -0
- package/dist/store.d.ts.map +1 -0
- package/dist/store.js +2 -0
- package/dist/string-coerce.d.ts +14 -0
- package/dist/string-coerce.d.ts.map +1 -0
- package/dist/string-coerce.js +72 -0
- package/dist/symlink-parents.d.ts +12 -0
- package/dist/symlink-parents.d.ts.map +1 -0
- package/dist/symlink-parents.js +78 -0
- package/dist/temp-cleanup.d.ts +6 -0
- package/dist/temp-cleanup.d.ts.map +1 -0
- package/dist/temp-cleanup.js +42 -0
- package/dist/temp-target.d.ts +28 -0
- package/dist/temp-target.d.ts.map +1 -0
- package/dist/temp-target.js +84 -0
- package/dist/temp.d.ts +3 -0
- package/dist/temp.d.ts.map +1 -0
- package/dist/temp.js +2 -0
- package/dist/test-hooks.d.ts +9 -0
- package/dist/test-hooks.d.ts.map +1 -0
- package/dist/test-hooks.js +13 -0
- package/dist/text-atomic.d.ts +7 -0
- package/dist/text-atomic.d.ts.map +1 -0
- package/dist/text-atomic.js +13 -0
- package/dist/timing.d.ts +6 -0
- package/dist/timing.d.ts.map +1 -0
- package/dist/timing.js +24 -0
- package/dist/trash.d.ts +5 -0
- package/dist/trash.d.ts.map +1 -0
- package/dist/trash.js +127 -0
- package/dist/types.d.ts +23 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +1 -0
- package/dist/walk.d.ts +26 -0
- package/dist/walk.d.ts.map +1 -0
- package/dist/walk.js +169 -0
- package/dist/write-queue.d.ts +2 -0
- package/dist/write-queue.d.ts.map +1 -0
- package/dist/write-queue.js +18 -0
- package/package.json +117 -0
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
## 0.1.0 - 2026-05-06
|
|
4
|
+
|
|
5
|
+
### Features
|
|
6
|
+
|
|
7
|
+
- Added `root()` capability-style filesystem handles for root-bounded reads, writes, appends, moves, copies, directory listing, stat, mkdir, remove, JSON, streams, and existence checks.
|
|
8
|
+
- Added traversal, symlink, hardlink, alias, and post-open/post-write identity checks for untrusted relative paths.
|
|
9
|
+
- Added process-global Python helper configuration for stronger POSIX fd-relative mutation paths, with `auto`, `off`, and `require` modes.
|
|
10
|
+
- Added atomic file and directory replacement helpers with mode control, fsync options, retry handling, and copy-fallback behavior.
|
|
11
|
+
- Added JSON helpers, `fileStore()`, `jsonStore()`, private store mode, and file-backed temporary workspaces.
|
|
12
|
+
- Added secure absolute file reads, secret-file helpers, permissions inspection, Windows ACL helpers, and local-root readers.
|
|
13
|
+
- Added archive extraction and preflight helpers for ZIP/TAR with optional `jszip` and `tar` dependencies, size/count/path/link limits, and staged destination writes.
|
|
14
|
+
- Added file locks, async locks, bounded directory walking, install-path sanitizers, filename sanitization, regular-file helpers, trash moves, and advanced composition helpers.
|
|
15
|
+
- Added OpenClaw bypass-parity coverage, API coverage, a benchmark workflow, docs site generation, security docs, and coverage CI.
|
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 openclaw
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,390 @@
|
|
|
1
|
+
# @openclaw/fs-safe
|
|
2
|
+
|
|
3
|
+
Capability-style filesystem roots for Node.js apps that handle untrusted relative paths.
|
|
4
|
+
|
|
5
|
+
Think Go's `os.Root` / `OpenInRoot` or Rust's [`cap-std`](https://github.com/bytecodealliance/cap-std), but for Node. Hand `root()` a trusted directory and you get back a handle whose every method resolves relative paths against it and refuses to escape — through `..`, symlink swaps, hardlink aliases, or TOCTOU rename races between check and use.
|
|
6
|
+
|
|
7
|
+
```ts
|
|
8
|
+
import { root } from "@openclaw/fs-safe";
|
|
9
|
+
|
|
10
|
+
const fs = await root("/safe/workspace");
|
|
11
|
+
await fs.write("notes/today.txt", "hello\n"); // ok
|
|
12
|
+
await fs.write("../escape.txt", "x"); // throws FsSafeError("outside-workspace")
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
That's the whole pitch. `root()` is the product; the rest of the package — JSON stores, atomic writes, secret files, archive extraction, temp workspaces — is supporting cast for the same boundary.
|
|
16
|
+
|
|
17
|
+
## Why this exists
|
|
18
|
+
|
|
19
|
+
Most Node code that has to touch caller-controlled paths reaches for:
|
|
20
|
+
|
|
21
|
+
```ts
|
|
22
|
+
path.resolve(root, input).startsWith(root)
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
That validates a *string*. It does not pin the file you opened, defend against a symlink retarget between check and use, reject hardlinked aliases of out-of-tree inodes, or verify that a write landed where you intended after a rename. The pieces to do those things exist scattered across the ecosystem — [`write-file-atomic`](https://www.npmjs.com/package/write-file-atomic) for atomic writes, `tar` / `jszip` for archive extraction, various `safefs`-style convenience wrappers — but none of them give you one root handle with traversal-resistant semantics across every operation.
|
|
26
|
+
|
|
27
|
+
The same idea has landed in other languages. Go [added `os.Root` and `OpenInRoot`](https://go.dev/blog/osroot); Rust has had [`cap-std`](https://github.com/bytecodealliance/cap-std) for years. Node's `fs` is path-string-oriented and exposes flags like `O_NOFOLLOW` but not an ergonomic "operate inside this root" API. `fs-safe` fills that gap.
|
|
28
|
+
|
|
29
|
+
## Not a sandbox
|
|
30
|
+
|
|
31
|
+
This is a **library-level guardrail**, not OS-level isolation. It does not replace containers, seccomp, AppArmor, or filesystem permissions. It is for code that already runs with the privileges of its workspace and wants to stop trivial path tricks from escaping it. If your threat model is a hostile process, you need OS isolation; if your threat model is "an agent, plugin, upload handler, or CLI will eventually be tricked into writing somewhere it shouldn't," `fs-safe` catches that.
|
|
32
|
+
|
|
33
|
+
## Install
|
|
34
|
+
|
|
35
|
+
```sh
|
|
36
|
+
pnpm add @openclaw/fs-safe
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
Node 20.11 or newer. Core root/path/json/temp helpers avoid framework dependencies. Archive helpers use optional `jszip` and `tar` dependencies for ZIP/TAR support; installs that omit optional dependencies can still use every non-archive subpath.
|
|
40
|
+
|
|
41
|
+
On POSIX, `root()` uses one process-global persistent Python helper for the
|
|
42
|
+
fd-relative operations Node does not expose ergonomically (`renameat`,
|
|
43
|
+
`unlinkat`, recursive `mkdirat`-style walks, and parent-fd writes). Configure it
|
|
44
|
+
before first use when you need a strict environment policy:
|
|
45
|
+
|
|
46
|
+
```ts
|
|
47
|
+
import { configureFsSafePython } from "@openclaw/fs-safe";
|
|
48
|
+
|
|
49
|
+
configureFsSafePython({ mode: "auto" }); // default: use helper, fall back if unavailable
|
|
50
|
+
configureFsSafePython({ mode: "off" }); // never spawn Python; use Node fallbacks
|
|
51
|
+
configureFsSafePython({ mode: "require" }); // fail closed if helper cannot start
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
Equivalent env vars: `FS_SAFE_PYTHON_MODE=auto|off|require` and
|
|
55
|
+
`FS_SAFE_PYTHON=/path/to/python3`. Without Python, `fs-safe` keeps lexical and
|
|
56
|
+
canonical root checks, no-follow opens, atomic temp+rename writes, and
|
|
57
|
+
post-write identity verification. What you lose is the strongest POSIX
|
|
58
|
+
fd-relative protection against a same-process-user racer swapping parent
|
|
59
|
+
directories between validation and mutation. Windows already uses the Node
|
|
60
|
+
fallback path. See the [Python helper policy](docs/python-helper.md) for
|
|
61
|
+
deployment guidance.
|
|
62
|
+
|
|
63
|
+
## Quick start
|
|
64
|
+
|
|
65
|
+
```ts
|
|
66
|
+
import { root } from "@openclaw/fs-safe";
|
|
67
|
+
|
|
68
|
+
const fs = await root("/safe/workspace", {
|
|
69
|
+
hardlinks: "reject",
|
|
70
|
+
symlinks: "reject",
|
|
71
|
+
mkdir: true,
|
|
72
|
+
mode: 0o600,
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
await fs.write("notes/today.txt", "hello\n");
|
|
76
|
+
const text = await fs.readText("notes/today.txt");
|
|
77
|
+
const config = await fs.readJson("config.json");
|
|
78
|
+
await fs.copyIn("uploads/upload.png", "/tmp/upload.png");
|
|
79
|
+
await fs.move("notes/today.txt", "notes/archive/today.txt", { overwrite: true });
|
|
80
|
+
await fs.remove("notes/archive/today.txt");
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
`root()` takes the trusted directory; relative paths in subsequent calls are resolved against it. Defaults you pass to `root()` apply to every call below; per-call options override them.
|
|
84
|
+
|
|
85
|
+
When you need metadata or a `FileHandle`:
|
|
86
|
+
|
|
87
|
+
```ts
|
|
88
|
+
const { buffer, realPath, stat } = await fs.read("notes/today.txt");
|
|
89
|
+
const opened = await fs.open("notes/today.txt");
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
`create()` is the don't-clobber variant of `write()` and throws `already-exists` when the target already exists:
|
|
93
|
+
|
|
94
|
+
```ts
|
|
95
|
+
await fs.create("notes/README.md", "seed\n"); // throws if it already exists
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
`write()` replaces file contents by default; pass `{ overwrite: false }` or use `create()` when an existing file should be an error. `move()` defaults to no clobber because it can otherwise delete an unrelated target while also consuming the source. Pass `{ overwrite: true }` when replacing the target is intended.
|
|
99
|
+
|
|
100
|
+
Use `ensureRoot()` when a computed relative directory target resolves to the root itself (`""` or `"."`) and you want the operation to be accepted. `root()` still requires the trusted root directory to already exist.
|
|
101
|
+
|
|
102
|
+
## Reading
|
|
103
|
+
|
|
104
|
+
Pick the narrowest read shape that gives you what you need:
|
|
105
|
+
|
|
106
|
+
```ts
|
|
107
|
+
await fs.readJson("config.json"); // parsed value; validate it at your boundary
|
|
108
|
+
await fs.readText("notes/today.txt");
|
|
109
|
+
await fs.readBytes("image.png");
|
|
110
|
+
await fs.read("notes/today.txt"); // { buffer, realPath, stat }
|
|
111
|
+
const opened = await fs.open("large.log"); // FileHandle for streaming
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
For streams, use `open()` and the returned `FileHandle`:
|
|
115
|
+
|
|
116
|
+
```ts
|
|
117
|
+
await using opened = await fs.open("large.log");
|
|
118
|
+
{
|
|
119
|
+
const stream = opened.handle.createReadStream();
|
|
120
|
+
// consume stream
|
|
121
|
+
}
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
Root reads default to `DEFAULT_ROOT_MAX_BYTES` (16 MiB). Pass a larger `maxBytes`
|
|
125
|
+
for expected large reads, or `Number.POSITIVE_INFINITY` when the caller has a
|
|
126
|
+
separate size budget.
|
|
127
|
+
|
|
128
|
+
`reader()` returns a callback that reads absolute or relative paths through the same root boundary. It is useful for APIs that accept a `(path) => Promise<Buffer>` loader. Absolute paths outside the root are rejected with `outside-workspace`. `readAbsolute()` has the same absolute-path behavior directly.
|
|
129
|
+
|
|
130
|
+
When you need a writable `FileHandle`, use `openWritable()` and prefer `await using` for cleanup:
|
|
131
|
+
|
|
132
|
+
```ts
|
|
133
|
+
await using opened = await fs.openWritable("logs/current.log", { writeMode: "append" });
|
|
134
|
+
{
|
|
135
|
+
await opened.handle.appendFile("line\n");
|
|
136
|
+
}
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
`nonBlockingRead` is the only I/O scheduling knob in `RootDefaults`; it applies to read/open operations because it changes how file descriptors are opened. Filesystem safety policy remains explicit through `hardlinks` and `symlinks`.
|
|
140
|
+
|
|
141
|
+
`stat()`, `exists()`, and `list()` are boundary-checked, but they cannot pin a later operation to the same filesystem object. Use `read()`, `open()`, `write()`, `create()`, `copyIn()`, `move()`, or `remove()` for operations that must be race-resistant at the point of use.
|
|
142
|
+
|
|
143
|
+
## Subpaths
|
|
144
|
+
|
|
145
|
+
The main entry point is intentionally small: `root`, the root option/result
|
|
146
|
+
types, and `FsSafeError`. Use subpaths for everything else. Low-level helpers
|
|
147
|
+
that OpenClaw needs to compose higher-level APIs are grouped under
|
|
148
|
+
`@openclaw/fs-safe/advanced` instead of being separate public leaf contracts.
|
|
149
|
+
|
|
150
|
+
| Subpath | Contents |
|
|
151
|
+
|---|---|
|
|
152
|
+
| `@openclaw/fs-safe/root` | `root()`, `Root`, `RootDefaults`, related types |
|
|
153
|
+
| `@openclaw/fs-safe/config` | process-global Python helper configuration |
|
|
154
|
+
| `@openclaw/fs-safe/path` | canonical path checks: `isPathInside`, `safeRealpathSync`, `isNotFoundPathError`, `isSymlinkOpenError` |
|
|
155
|
+
| `@openclaw/fs-safe/json` | `tryReadJson`, `readJson`, `readJsonIfExists`, `writeJson`, sync variants |
|
|
156
|
+
| `@openclaw/fs-safe/store` | `fileStore`, `fileStoreSync`, and `jsonStore` |
|
|
157
|
+
| `@openclaw/fs-safe/secret` | strict and try-style secret file read/write helpers |
|
|
158
|
+
| `@openclaw/fs-safe/atomic` | `replaceFileAtomic`, `replaceFileAtomicSync`, `replaceDirectoryAtomic`, `movePathWithCopyFallback` |
|
|
159
|
+
| `@openclaw/fs-safe/temp` | `tempWorkspace`, `tempWorkspaceSync`, `withTempWorkspace`, `resolveSecureTempRoot` |
|
|
160
|
+
| `@openclaw/fs-safe/secure-file` | fd-pinned absolute file reads with owner, mode, ACL, trusted-dir, size, and timeout checks |
|
|
161
|
+
| `@openclaw/fs-safe/file-lock` | `acquireFileLock`, `withFileLock`, `createFileLockManager`, and related lock types |
|
|
162
|
+
| `@openclaw/fs-safe/permissions` | POSIX mode and Windows ACL inspection plus remediation formatting helpers |
|
|
163
|
+
| `@openclaw/fs-safe/walk` | budget-bounded directory walking with symlink policy, filters, and truncation accounting; not root-bounded |
|
|
164
|
+
| `@openclaw/fs-safe/archive` | `extractArchive`, `resolveArchiveKind`, `ArchiveLimitError`, preflight helpers |
|
|
165
|
+
| `@openclaw/fs-safe/advanced` | lower-level composition helpers such as path scopes, root-file open, install paths, filename sanitizing, temp-file targets, sibling-temp writes, local-root readers, regular-file helpers, `pathExists`, and `withTimeout`; less stable than focused public subpaths |
|
|
166
|
+
| `@openclaw/fs-safe/errors` | `FsSafeError`, `FsSafeErrorCode` |
|
|
167
|
+
| `@openclaw/fs-safe/types` | shared types: `DirEntry`, `PathStat`, … |
|
|
168
|
+
| `@openclaw/fs-safe/test-hooks` | hooks the test suite uses to inject races; only active under `NODE_ENV=test` |
|
|
169
|
+
|
|
170
|
+
## Failure semantics in the name
|
|
171
|
+
|
|
172
|
+
When two helpers behave differently on the same input, the difference is in the name, not the docs.
|
|
173
|
+
|
|
174
|
+
```ts
|
|
175
|
+
import { readJson, tryReadJson } from "@openclaw/fs-safe/json";
|
|
176
|
+
|
|
177
|
+
await tryReadJson("./config.json"); // returns null on missing or invalid
|
|
178
|
+
await readJson("./manifest.json"); // throws on missing or invalid
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
## Atomic writes
|
|
182
|
+
|
|
183
|
+
`replaceFileAtomic()` writes a sibling temp file, optionally fsyncs it, and renames it over the destination. Mode preservation, rename retry / copy fallback on `EPERM`, parent-directory fsync, and a `beforeRename` hook for backup or observer flows are all opt-in.
|
|
184
|
+
|
|
185
|
+
```ts
|
|
186
|
+
import { replaceFileAtomic } from "@openclaw/fs-safe/atomic";
|
|
187
|
+
|
|
188
|
+
await replaceFileAtomic({
|
|
189
|
+
filePath: "/safe/workspace/state.json",
|
|
190
|
+
content: JSON.stringify(state, null, 2),
|
|
191
|
+
mode: 0o600,
|
|
192
|
+
syncTempFile: true,
|
|
193
|
+
syncParentDir: true,
|
|
194
|
+
});
|
|
195
|
+
```
|
|
196
|
+
|
|
197
|
+
`replaceFileAtomicSync()` covers the synchronous case with the same options shape. Both accept an injectable `fileSystem` for tests.
|
|
198
|
+
|
|
199
|
+
## Stores
|
|
200
|
+
|
|
201
|
+
Use `fileStore().json()` for small state files that need explicit fallback
|
|
202
|
+
reads, atomic writes, and optional sidecar locking around read-modify-write
|
|
203
|
+
updates:
|
|
204
|
+
|
|
205
|
+
```ts
|
|
206
|
+
import { fileStore } from "@openclaw/fs-safe/store";
|
|
207
|
+
|
|
208
|
+
const files = fileStore({ rootDir: "/safe/workspace/state", private: true });
|
|
209
|
+
const store = files.json("settings.json", { lock: true });
|
|
210
|
+
|
|
211
|
+
await store.updateOr({ enabled: false }, (current) => ({ ...current, enabled: true }));
|
|
212
|
+
```
|
|
213
|
+
|
|
214
|
+
`jsonStore({ filePath })` is the absolute-path convenience wrapper for the same
|
|
215
|
+
primitive.
|
|
216
|
+
|
|
217
|
+
Use `update()` when missing state is part of your model; use `updateOr()` for
|
|
218
|
+
the common merge-into-defaults case. Standalone helpers use options bags
|
|
219
|
+
because they do not carry a bound root and often need multiple authority, path,
|
|
220
|
+
and policy knobs.
|
|
221
|
+
|
|
222
|
+
Use `fileStore()` for cache/blob/media-style directories where callers
|
|
223
|
+
need safe relative paths, size limits, atomic replacement, stream writes, and
|
|
224
|
+
TTL cleanup behind one root. Pass `private: true` for credentials, auth
|
|
225
|
+
profiles, tokens, and per-agent private state; private mode keeps the same
|
|
226
|
+
store shape while routing writes through the secret-file atomic path.
|
|
227
|
+
|
|
228
|
+
```ts
|
|
229
|
+
import { fileStore } from "@openclaw/fs-safe/store";
|
|
230
|
+
|
|
231
|
+
const media = fileStore({
|
|
232
|
+
rootDir: "/safe/workspace/media",
|
|
233
|
+
maxBytes: 5 * 1024 * 1024,
|
|
234
|
+
mode: 0o600,
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
await media.write("inbound/photo.jpg", bytes);
|
|
238
|
+
await media.writeJson("state/photo.json", { id: "photo" });
|
|
239
|
+
const cached = await media.readJsonIfExists("state/photo.json");
|
|
240
|
+
const opened = await media.open("inbound/photo.jpg");
|
|
241
|
+
await media.pruneExpired({ ttlMs: 10 * 60 * 1000, recursive: true });
|
|
242
|
+
```
|
|
243
|
+
|
|
244
|
+
`tempWorkspace()` also exposes `writeText()`, `writeJson()`, and `copyIn()` for
|
|
245
|
+
single-file scratch workflows without hand-rolled path joins.
|
|
246
|
+
|
|
247
|
+
`tempFile()` is the smaller one-file temp helper. It is intentionally an
|
|
248
|
+
advanced primitive: use `tempWorkspace()` for the stable temp surface and reach
|
|
249
|
+
for `tempFile()` only when you need a raw file target.
|
|
250
|
+
|
|
251
|
+
```ts
|
|
252
|
+
import { tempFile } from "@openclaw/fs-safe/advanced";
|
|
253
|
+
|
|
254
|
+
await using target = await tempFile({ prefix: "download", fileName: "payload.bin" });
|
|
255
|
+
await fs.promises.writeFile(target.path, bytes);
|
|
256
|
+
const checksumPath = target.file("payload.sha256");
|
|
257
|
+
```
|
|
258
|
+
|
|
259
|
+
## Secure absolute file reads
|
|
260
|
+
|
|
261
|
+
Use `readSecureFile()` when the caller gives you an absolute credential path
|
|
262
|
+
instead of a root-relative workspace path. It opens the file first, validates the
|
|
263
|
+
same handle it will read from, checks trusted directories, owner, POSIX mode or
|
|
264
|
+
Windows ACLs, hardlink count, size, and optional timeout, then reads through the
|
|
265
|
+
pinned handle.
|
|
266
|
+
|
|
267
|
+
```ts
|
|
268
|
+
import { readSecureFile } from "@openclaw/fs-safe/secure-file";
|
|
269
|
+
|
|
270
|
+
const { buffer } = await readSecureFile({
|
|
271
|
+
filePath: "/var/lib/app/token",
|
|
272
|
+
label: "auth token",
|
|
273
|
+
trust: { trustedDirs: ["/var/lib/app"] },
|
|
274
|
+
io: { maxBytes: 16 * 1024, timeoutMs: 5_000 },
|
|
275
|
+
});
|
|
276
|
+
```
|
|
277
|
+
|
|
278
|
+
Use `permissions: { allowInsecure: true }` only for migration or explicit local-development
|
|
279
|
+
flows where a warning is preferable to refusing the file.
|
|
280
|
+
|
|
281
|
+
## Directory walking
|
|
282
|
+
|
|
283
|
+
`walkDirectory()` and `walkDirectorySync()` replace ad-hoc recursive
|
|
284
|
+
`readdir()` loops with entry and depth budgets, a symlink policy, and stable
|
|
285
|
+
relative paths.
|
|
286
|
+
|
|
287
|
+
```ts
|
|
288
|
+
import { walkDirectory } from "@openclaw/fs-safe/walk";
|
|
289
|
+
|
|
290
|
+
const scan = await walkDirectory("/safe/workspace", {
|
|
291
|
+
maxDepth: 4,
|
|
292
|
+
maxEntries: 10_000,
|
|
293
|
+
symlinks: "skip",
|
|
294
|
+
include: (entry) => entry.kind === "file",
|
|
295
|
+
});
|
|
296
|
+
|
|
297
|
+
for (const file of scan.entries) {
|
|
298
|
+
console.log(file.relativePath);
|
|
299
|
+
}
|
|
300
|
+
```
|
|
301
|
+
|
|
302
|
+
Check `scan.truncated` before treating the result as complete.
|
|
303
|
+
|
|
304
|
+
## Archive extraction
|
|
305
|
+
|
|
306
|
+
`extractArchive()` handles ZIP and TAR behind one API, with traversal checks, blocked-link-type rejection, and entry-count and byte budgets.
|
|
307
|
+
|
|
308
|
+
```ts
|
|
309
|
+
import { extractArchive, resolveArchiveKind } from "@openclaw/fs-safe/archive";
|
|
310
|
+
|
|
311
|
+
const kind = resolveArchiveKind(uploadPath);
|
|
312
|
+
if (!kind) throw new Error(`unsupported archive: ${uploadPath}`);
|
|
313
|
+
|
|
314
|
+
await extractArchive({
|
|
315
|
+
archivePath: uploadPath,
|
|
316
|
+
destDir: "/safe/workspace/plugin",
|
|
317
|
+
kind,
|
|
318
|
+
timeoutMs: 15_000,
|
|
319
|
+
limits: {
|
|
320
|
+
maxArchiveBytes: 256 * 1024 * 1024,
|
|
321
|
+
maxEntries: 50_000,
|
|
322
|
+
maxExtractedBytes: 512 * 1024 * 1024,
|
|
323
|
+
maxEntryBytes: 256 * 1024 * 1024,
|
|
324
|
+
},
|
|
325
|
+
});
|
|
326
|
+
```
|
|
327
|
+
|
|
328
|
+
Extraction stages into a private directory and merges through the same safe-open boundary used by direct writes, so a symlinked entry can't trick the merge into following an out-of-tree path.
|
|
329
|
+
|
|
330
|
+
## Advanced path scopes
|
|
331
|
+
|
|
332
|
+
For code that already has a trusted absolute path and wants lower-level boundary
|
|
333
|
+
validation without going through `root()`:
|
|
334
|
+
|
|
335
|
+
```ts
|
|
336
|
+
import { pathScope } from "@openclaw/fs-safe/advanced";
|
|
337
|
+
|
|
338
|
+
const uploads = pathScope("/safe/uploads", { label: "uploads directory" });
|
|
339
|
+
const files = await uploads.files(["photo.jpg"]);
|
|
340
|
+
const target = await uploads.writable("report.pdf");
|
|
341
|
+
```
|
|
342
|
+
|
|
343
|
+
## Errors
|
|
344
|
+
|
|
345
|
+
Every failure surfaces as an `FsSafeError` with a closed `code` union you can branch on:
|
|
346
|
+
|
|
347
|
+
```ts
|
|
348
|
+
import { FsSafeError } from "@openclaw/fs-safe/errors";
|
|
349
|
+
|
|
350
|
+
try {
|
|
351
|
+
await fs.write("../escape.txt", "x");
|
|
352
|
+
} catch (err) {
|
|
353
|
+
if (err instanceof FsSafeError && err.code === "outside-workspace") {
|
|
354
|
+
// handle
|
|
355
|
+
}
|
|
356
|
+
throw err;
|
|
357
|
+
}
|
|
358
|
+
```
|
|
359
|
+
|
|
360
|
+
Codes are grouped by category:
|
|
361
|
+
|
|
362
|
+
```ts
|
|
363
|
+
if (err instanceof FsSafeError) {
|
|
364
|
+
if (err.category === "policy") {
|
|
365
|
+
// Unsafe caller input or filesystem state.
|
|
366
|
+
} else {
|
|
367
|
+
// Operational problem such as helper startup, timeout, or unverifiable permissions.
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
```
|
|
371
|
+
|
|
372
|
+
Current `FsSafeErrorCode` values are `already-exists`, `hardlink`, `helper-failed`, `helper-unavailable`, `invalid-path`, `insecure-permissions`, `not-empty`, `not-file`, `not-found`, `not-owned`, `not-removable`, `outside-workspace`, `path-alias`, `path-mismatch`, `permission-unverified`, `symlink`, `timeout`, `too-large`, and `unsupported-platform`.
|
|
373
|
+
|
|
374
|
+
## Safety model
|
|
375
|
+
|
|
376
|
+
- root-bounded APIs resolve paths against a configured root and reject canonical escapes
|
|
377
|
+
- reads open with `O_NOFOLLOW` where available, then verify fd identity matches the path identity before returning the buffer or handle
|
|
378
|
+
- writes use pinned parent-directory helpers and atomic replacement on POSIX, with verified post-write identity
|
|
379
|
+
- `remove`, `mkdir`, `move`, `stat`, `list`, and parent-fd writes use one persistent fd-relative Python helper on POSIX, with Node fallbacks when the helper is disabled or unavailable
|
|
380
|
+
- archive extraction stages into a private directory and merges through the same boundary checks used by direct writes
|
|
381
|
+
|
|
382
|
+
## Limitations
|
|
383
|
+
|
|
384
|
+
- Windows uses the safest Node-level behavior available; some fd-relative POSIX hardening is unavailable there.
|
|
385
|
+
- Hardlink rejection depends on platform metadata. Treat it as defense-in-depth, not authorization.
|
|
386
|
+
- `fs-safe` does not validate file contents or archive payload semantics beyond filesystem safety constraints. Schemas, signatures, and authorization belong in the layer above.
|
|
387
|
+
|
|
388
|
+
## License
|
|
389
|
+
|
|
390
|
+
MIT.
|
package/SECURITY.md
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
# Security Policy
|
|
2
|
+
|
|
3
|
+
`@openclaw/fs-safe` is a filesystem-safety library, so potential boundary bypasses should be reported privately first.
|
|
4
|
+
|
|
5
|
+
Email security reports to Peter Steinberger at `steipete@gmail.com` with:
|
|
6
|
+
|
|
7
|
+
- affected version or commit
|
|
8
|
+
- platform and filesystem details
|
|
9
|
+
- minimal reproduction steps
|
|
10
|
+
- expected impact
|
|
11
|
+
|
|
12
|
+
Please do not open a public issue for traversal, symlink, hardlink, archive extraction, or credential-file bugs until we have coordinated disclosure.
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
export type AbsolutePathSymlinkPolicy = "reject" | "follow";
|
|
2
|
+
export type ResolvedAbsolutePath = {
|
|
3
|
+
path: string;
|
|
4
|
+
canonicalPath: string;
|
|
5
|
+
};
|
|
6
|
+
export type ResolvedWritableAbsolutePath = ResolvedAbsolutePath & {
|
|
7
|
+
parentDir: string;
|
|
8
|
+
parentExists: boolean;
|
|
9
|
+
};
|
|
10
|
+
export declare function assertAbsolutePathInput(filePath: string): string;
|
|
11
|
+
export declare function findExistingAncestor(filePath: string): Promise<string | null>;
|
|
12
|
+
export declare function canonicalPathFromExistingAncestor(filePath: string): Promise<string>;
|
|
13
|
+
export declare function resolveAbsolutePathForRead(filePath: string, options?: {
|
|
14
|
+
symlinks?: AbsolutePathSymlinkPolicy;
|
|
15
|
+
}): Promise<ResolvedAbsolutePath>;
|
|
16
|
+
export declare function resolveAbsolutePathForWrite(filePath: string, options?: {
|
|
17
|
+
symlinks?: AbsolutePathSymlinkPolicy;
|
|
18
|
+
}): Promise<ResolvedWritableAbsolutePath>;
|
|
19
|
+
//# sourceMappingURL=absolute-path.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"absolute-path.d.ts","sourceRoot":"","sources":["../src/absolute-path.ts"],"names":[],"mappings":"AAIA,MAAM,MAAM,yBAAyB,GAAG,QAAQ,GAAG,QAAQ,CAAC;AAE5D,MAAM,MAAM,oBAAoB,GAAG;IACjC,IAAI,EAAE,MAAM,CAAC;IACb,aAAa,EAAE,MAAM,CAAC;CACvB,CAAC;AAEF,MAAM,MAAM,4BAA4B,GAAG,oBAAoB,GAAG;IAChE,SAAS,EAAE,MAAM,CAAC;IAClB,YAAY,EAAE,OAAO,CAAC;CACvB,CAAC;AAEF,wBAAgB,uBAAuB,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM,CAWhE;AAWD,wBAAsB,oBAAoB,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAiBnF;AAED,wBAAsB,iCAAiC,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAazF;AAED,wBAAsB,0BAA0B,CAC9C,QAAQ,EAAE,MAAM,EAChB,OAAO,GAAE;IAAE,QAAQ,CAAC,EAAE,yBAAyB,CAAA;CAAO,GACrD,OAAO,CAAC,oBAAoB,CAAC,CAe/B;AAED,wBAAsB,2BAA2B,CAC/C,QAAQ,EAAE,MAAM,EAChB,OAAO,GAAE;IAAE,QAAQ,CAAC,EAAE,yBAAyB,CAAA;CAAO,GACrD,OAAO,CAAC,4BAA4B,CAAC,CAsBvC"}
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import fs from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { FsSafeError } from "./errors.js";
|
|
4
|
+
export function assertAbsolutePathInput(filePath) {
|
|
5
|
+
if (!filePath) {
|
|
6
|
+
throw new FsSafeError("invalid-path", "path is required");
|
|
7
|
+
}
|
|
8
|
+
if (filePath.includes("\0")) {
|
|
9
|
+
throw new FsSafeError("invalid-path", "path must not contain NUL bytes");
|
|
10
|
+
}
|
|
11
|
+
if (!path.isAbsolute(filePath)) {
|
|
12
|
+
throw new FsSafeError("invalid-path", "path must be absolute");
|
|
13
|
+
}
|
|
14
|
+
return path.normalize(filePath);
|
|
15
|
+
}
|
|
16
|
+
async function pathExists(filePath) {
|
|
17
|
+
try {
|
|
18
|
+
await fs.access(filePath);
|
|
19
|
+
return true;
|
|
20
|
+
}
|
|
21
|
+
catch {
|
|
22
|
+
return false;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
export async function findExistingAncestor(filePath) {
|
|
26
|
+
let current = path.resolve(filePath);
|
|
27
|
+
while (true) {
|
|
28
|
+
try {
|
|
29
|
+
await fs.lstat(current);
|
|
30
|
+
return current;
|
|
31
|
+
}
|
|
32
|
+
catch (err) {
|
|
33
|
+
if (err.code !== "ENOENT") {
|
|
34
|
+
throw err;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
const parent = path.dirname(current);
|
|
38
|
+
if (parent === current) {
|
|
39
|
+
return null;
|
|
40
|
+
}
|
|
41
|
+
current = parent;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
export async function canonicalPathFromExistingAncestor(filePath) {
|
|
45
|
+
const ancestor = await findExistingAncestor(filePath);
|
|
46
|
+
if (!ancestor) {
|
|
47
|
+
return path.resolve(filePath);
|
|
48
|
+
}
|
|
49
|
+
let canonicalAncestor = ancestor;
|
|
50
|
+
try {
|
|
51
|
+
canonicalAncestor = await fs.realpath(ancestor);
|
|
52
|
+
}
|
|
53
|
+
catch {
|
|
54
|
+
// Keep lexical path when the existing ancestor cannot be canonicalized.
|
|
55
|
+
}
|
|
56
|
+
const relative = path.relative(ancestor, filePath);
|
|
57
|
+
return relative ? path.join(canonicalAncestor, relative) : canonicalAncestor;
|
|
58
|
+
}
|
|
59
|
+
export async function resolveAbsolutePathForRead(filePath, options = {}) {
|
|
60
|
+
const normalized = assertAbsolutePathInput(filePath);
|
|
61
|
+
let canonicalPath;
|
|
62
|
+
try {
|
|
63
|
+
canonicalPath = await fs.realpath(normalized);
|
|
64
|
+
}
|
|
65
|
+
catch (err) {
|
|
66
|
+
if (err.code === "ENOENT") {
|
|
67
|
+
throw new FsSafeError("not-found", "path not found", { cause: err });
|
|
68
|
+
}
|
|
69
|
+
throw err;
|
|
70
|
+
}
|
|
71
|
+
if ((options.symlinks ?? "reject") === "reject" && canonicalPath !== normalized) {
|
|
72
|
+
throw new FsSafeError("symlink", "path traverses a symlink", { cause: { canonicalPath } });
|
|
73
|
+
}
|
|
74
|
+
return { path: normalized, canonicalPath };
|
|
75
|
+
}
|
|
76
|
+
export async function resolveAbsolutePathForWrite(filePath, options = {}) {
|
|
77
|
+
const normalized = assertAbsolutePathInput(filePath);
|
|
78
|
+
const parentDir = path.dirname(normalized);
|
|
79
|
+
const parentExists = await pathExists(parentDir);
|
|
80
|
+
if ((options.symlinks ?? "reject") === "reject") {
|
|
81
|
+
const ancestor = await findExistingAncestor(parentDir);
|
|
82
|
+
if (ancestor) {
|
|
83
|
+
const canonicalAncestor = await fs.realpath(ancestor).catch(() => ancestor);
|
|
84
|
+
if (canonicalAncestor !== ancestor) {
|
|
85
|
+
const canonicalPath = path.join(canonicalAncestor, path.relative(ancestor, normalized));
|
|
86
|
+
throw new FsSafeError("symlink", "path traverses a symlink", {
|
|
87
|
+
cause: { canonicalPath },
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
return {
|
|
93
|
+
path: normalized,
|
|
94
|
+
canonicalPath: await canonicalPathFromExistingAncestor(normalized),
|
|
95
|
+
parentDir,
|
|
96
|
+
parentExists,
|
|
97
|
+
};
|
|
98
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
export { createAsyncLock } from "./async-lock.js";
|
|
2
|
+
export { assertAbsolutePathInput, canonicalPathFromExistingAncestor, findExistingAncestor, resolveAbsolutePathForRead, resolveAbsolutePathForWrite, type AbsolutePathSymlinkPolicy, type ResolvedAbsolutePath, type ResolvedWritableAbsolutePath, } from "./absolute-path.js";
|
|
3
|
+
export { sameFileIdentity, type FileIdentityStat } from "./file-identity.js";
|
|
4
|
+
export { sanitizeUntrustedFileName } from "./filename.js";
|
|
5
|
+
export { pathExists, pathExistsSync } from "./fs.js";
|
|
6
|
+
export { resolveLocalPathFromRootsSync, readLocalFileFromRoots, type LocalRootsInputOptions, type LocalRootsPathResult, type LocalRootsReadResult, type ReadLocalFileFromRootsOptions, type ResolveLocalPathFromRootsSyncOptions, } from "./local-roots.js";
|
|
7
|
+
export { assertNoWindowsNetworkPath, basenameFromMediaSource, hasEncodedFileUrlSeparator, isWindowsDriveLetterPath, isWindowsNetworkPath, safeFileURLToPath, trySafeFileURLToPath, } from "./local-file-access.js";
|
|
8
|
+
export { formatPosixMode } from "./mode.js";
|
|
9
|
+
export { assertNoHardlinkedFinalPath, assertNoPathAliasEscape, PATH_ALIAS_POLICIES, type PathAliasPolicy, } from "./path-policy.js";
|
|
10
|
+
export { openRootFile, openRootFileSync, canUseRootFileOpen, matchRootFileOpenFailure, type OpenRootFileParams, type OpenRootFileSyncParams, type RootFileOpenFailure, type RootFileOpenFailureReason, type RootFileOpenResult, } from "./root-file.js";
|
|
11
|
+
export { ROOT_PATH_ALIAS_POLICIES, resolvePathViaExistingAncestorSync, resolveRootPath, resolveRootPathSync, type ResolvedRootPath, type RootPathAliasPolicy, } from "./root-path.js";
|
|
12
|
+
export { ensureDirectoryWithinRoot, pathScope, resolveExistingPathsWithinRoot, resolvePathWithinRoot, resolvePathsWithinRoot, resolveStrictExistingPathsWithinRoot, resolveWritablePathWithinRoot, type PathScope, type PathScopeOptions, type PathScopeResolveOptions, } from "./root-paths.js";
|
|
13
|
+
export { safeDirName, safePathSegmentHashed, resolveSafeInstallDir, assertCanonicalPathWithinBase, } from "./install-path.js";
|
|
14
|
+
export { assertNoSymlinkParents, assertNoSymlinkParentsSync, type AssertNoSymlinkParentsOptions, } from "./symlink-parents.js";
|
|
15
|
+
export { movePathToTrash, type MovePathToTrashOptions } from "./trash.js";
|
|
16
|
+
export { withTimeout } from "./timing.js";
|
|
17
|
+
export { resolveHomeRelativePath } from "./home-dir.js";
|
|
18
|
+
export { appendRegularFile, appendRegularFileSync, readRegularFile, readRegularFileSync, resolveRegularFileAppendFlags, statRegularFile, statRegularFileSync, type AppendRegularFileOptions, type RegularFileStatResult, } from "./regular-file.js";
|
|
19
|
+
export { buildRandomTempFilePath, sanitizeTempFileName, type TempFile, tempFile, withTempFile, } from "./temp-target.js";
|
|
20
|
+
export { writeSiblingTempFile, writeViaSiblingTempPath, type WriteSiblingTempFileOptions, type WriteSiblingTempFileResult, } from "./sibling-temp.js";
|
|
21
|
+
export { createIcaclsResetCommand, formatIcaclsResetCommand, formatWindowsAclSummary, inspectWindowsAcl, parseIcaclsOutput, resolveWindowsUserPrincipal, summarizeWindowsAcl, type IcaclsResetCommandOptions, type PermissionExec, type WindowsAclEntry, type WindowsAclSummary, } from "./permissions.js";
|
|
22
|
+
//# sourceMappingURL=advanced.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"advanced.d.ts","sourceRoot":"","sources":["../src/advanced.ts"],"names":[],"mappings":"AAGA,OAAO,EAAE,eAAe,EAAE,MAAM,iBAAiB,CAAC;AAClD,OAAO,EACL,uBAAuB,EACvB,iCAAiC,EACjC,oBAAoB,EACpB,0BAA0B,EAC1B,2BAA2B,EAC3B,KAAK,yBAAyB,EAC9B,KAAK,oBAAoB,EACzB,KAAK,4BAA4B,GAClC,MAAM,oBAAoB,CAAC;AAC5B,OAAO,EAAE,gBAAgB,EAAE,KAAK,gBAAgB,EAAE,MAAM,oBAAoB,CAAC;AAC7E,OAAO,EAAE,yBAAyB,EAAE,MAAM,eAAe,CAAC;AAC1D,OAAO,EAAE,UAAU,EAAE,cAAc,EAAE,MAAM,SAAS,CAAC;AACrD,OAAO,EACL,6BAA6B,EAC7B,sBAAsB,EACtB,KAAK,sBAAsB,EAC3B,KAAK,oBAAoB,EACzB,KAAK,oBAAoB,EACzB,KAAK,6BAA6B,EAClC,KAAK,oCAAoC,GAC1C,MAAM,kBAAkB,CAAC;AAC1B,OAAO,EACL,0BAA0B,EAC1B,uBAAuB,EACvB,0BAA0B,EAC1B,wBAAwB,EACxB,oBAAoB,EACpB,iBAAiB,EACjB,oBAAoB,GACrB,MAAM,wBAAwB,CAAC;AAChC,OAAO,EAAE,eAAe,EAAE,MAAM,WAAW,CAAC;AAC5C,OAAO,EACL,2BAA2B,EAC3B,uBAAuB,EACvB,mBAAmB,EACnB,KAAK,eAAe,GACrB,MAAM,kBAAkB,CAAC;AAC1B,OAAO,EACL,YAAY,EACZ,gBAAgB,EAChB,kBAAkB,EAClB,wBAAwB,EACxB,KAAK,kBAAkB,EACvB,KAAK,sBAAsB,EAC3B,KAAK,mBAAmB,EACxB,KAAK,yBAAyB,EAC9B,KAAK,kBAAkB,GACxB,MAAM,gBAAgB,CAAC;AACxB,OAAO,EACL,wBAAwB,EACxB,kCAAkC,EAClC,eAAe,EACf,mBAAmB,EACnB,KAAK,gBAAgB,EACrB,KAAK,mBAAmB,GACzB,MAAM,gBAAgB,CAAC;AACxB,OAAO,EACL,yBAAyB,EACzB,SAAS,EACT,8BAA8B,EAC9B,qBAAqB,EACrB,sBAAsB,EACtB,oCAAoC,EACpC,6BAA6B,EAC7B,KAAK,SAAS,EACd,KAAK,gBAAgB,EACrB,KAAK,uBAAuB,GAC7B,MAAM,iBAAiB,CAAC;AACzB,OAAO,EACL,WAAW,EACX,qBAAqB,EACrB,qBAAqB,EACrB,6BAA6B,GAC9B,MAAM,mBAAmB,CAAC;AAC3B,OAAO,EACL,sBAAsB,EACtB,0BAA0B,EAC1B,KAAK,6BAA6B,GACnC,MAAM,sBAAsB,CAAC;AAC9B,OAAO,EAAE,eAAe,EAAE,KAAK,sBAAsB,EAAE,MAAM,YAAY,CAAC;AAC1E,OAAO,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AAC1C,OAAO,EAAE,uBAAuB,EAAE,MAAM,eAAe,CAAC;AACxD,OAAO,EACL,iBAAiB,EACjB,qBAAqB,EACrB,eAAe,EACf,mBAAmB,EACnB,6BAA6B,EAC7B,eAAe,EACf,mBAAmB,EACnB,KAAK,wBAAwB,EAC7B,KAAK,qBAAqB,GAC3B,MAAM,mBAAmB,CAAC;AAC3B,OAAO,EACL,uBAAuB,EACvB,oBAAoB,EACpB,KAAK,QAAQ,EACb,QAAQ,EACR,YAAY,GACb,MAAM,kBAAkB,CAAC;AAC1B,OAAO,EACL,oBAAoB,EACpB,uBAAuB,EACvB,KAAK,2BAA2B,EAChC,KAAK,0BAA0B,GAChC,MAAM,mBAAmB,CAAC;AAC3B,OAAO,EACL,wBAAwB,EACxB,wBAAwB,EACxB,uBAAuB,EACvB,iBAAiB,EACjB,iBAAiB,EACjB,2BAA2B,EAC3B,mBAAmB,EACnB,KAAK,yBAAyB,EAC9B,KAAK,cAAc,EACnB,KAAK,eAAe,EACpB,KAAK,iBAAiB,GACvB,MAAM,kBAAkB,CAAC"}
|
package/dist/advanced.js
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
// Advanced composition surface. These exports are less stable than the focused
|
|
2
|
+
// public subpaths; prefer root/json/store/temp/archive unless you are building a
|
|
3
|
+
// higher-level primitive.
|
|
4
|
+
export { createAsyncLock } from "./async-lock.js";
|
|
5
|
+
export { assertAbsolutePathInput, canonicalPathFromExistingAncestor, findExistingAncestor, resolveAbsolutePathForRead, resolveAbsolutePathForWrite, } from "./absolute-path.js";
|
|
6
|
+
export { sameFileIdentity } from "./file-identity.js";
|
|
7
|
+
export { sanitizeUntrustedFileName } from "./filename.js";
|
|
8
|
+
export { pathExists, pathExistsSync } from "./fs.js";
|
|
9
|
+
export { resolveLocalPathFromRootsSync, readLocalFileFromRoots, } from "./local-roots.js";
|
|
10
|
+
export { assertNoWindowsNetworkPath, basenameFromMediaSource, hasEncodedFileUrlSeparator, isWindowsDriveLetterPath, isWindowsNetworkPath, safeFileURLToPath, trySafeFileURLToPath, } from "./local-file-access.js";
|
|
11
|
+
export { formatPosixMode } from "./mode.js";
|
|
12
|
+
export { assertNoHardlinkedFinalPath, assertNoPathAliasEscape, PATH_ALIAS_POLICIES, } from "./path-policy.js";
|
|
13
|
+
export { openRootFile, openRootFileSync, canUseRootFileOpen, matchRootFileOpenFailure, } from "./root-file.js";
|
|
14
|
+
export { ROOT_PATH_ALIAS_POLICIES, resolvePathViaExistingAncestorSync, resolveRootPath, resolveRootPathSync, } from "./root-path.js";
|
|
15
|
+
export { ensureDirectoryWithinRoot, pathScope, resolveExistingPathsWithinRoot, resolvePathWithinRoot, resolvePathsWithinRoot, resolveStrictExistingPathsWithinRoot, resolveWritablePathWithinRoot, } from "./root-paths.js";
|
|
16
|
+
export { safeDirName, safePathSegmentHashed, resolveSafeInstallDir, assertCanonicalPathWithinBase, } from "./install-path.js";
|
|
17
|
+
export { assertNoSymlinkParents, assertNoSymlinkParentsSync, } from "./symlink-parents.js";
|
|
18
|
+
export { movePathToTrash } from "./trash.js";
|
|
19
|
+
export { withTimeout } from "./timing.js";
|
|
20
|
+
export { resolveHomeRelativePath } from "./home-dir.js";
|
|
21
|
+
export { appendRegularFile, appendRegularFileSync, readRegularFile, readRegularFileSync, resolveRegularFileAppendFlags, statRegularFile, statRegularFileSync, } from "./regular-file.js";
|
|
22
|
+
export { buildRandomTempFilePath, sanitizeTempFileName, tempFile, withTempFile, } from "./temp-target.js";
|
|
23
|
+
export { writeSiblingTempFile, writeViaSiblingTempPath, } from "./sibling-temp.js";
|
|
24
|
+
export { createIcaclsResetCommand, formatIcaclsResetCommand, formatWindowsAclSummary, inspectWindowsAcl, parseIcaclsOutput, resolveWindowsUserPrincipal, summarizeWindowsAcl, } from "./permissions.js";
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
export declare function isWindowsDrivePath(value: string): boolean;
|
|
2
|
+
export declare function normalizeArchiveEntryPath(raw: string): string;
|
|
3
|
+
export declare function validateArchiveEntryPath(entryPath: string, params?: {
|
|
4
|
+
escapeLabel?: string;
|
|
5
|
+
}): void;
|
|
6
|
+
export declare function stripArchivePath(entryPath: string, stripComponents: number): string | null;
|
|
7
|
+
export declare function resolveArchiveOutputPath(params: {
|
|
8
|
+
rootDir: string;
|
|
9
|
+
relPath: string;
|
|
10
|
+
originalPath: string;
|
|
11
|
+
escapeLabel?: string;
|
|
12
|
+
}): string;
|
|
13
|
+
//# sourceMappingURL=archive-entry.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"archive-entry.d.ts","sourceRoot":"","sources":["../src/archive-entry.ts"],"names":[],"mappings":"AAGA,wBAAgB,kBAAkB,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAEzD;AAED,wBAAgB,yBAAyB,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,CAE7D;AAED,wBAAgB,wBAAwB,CACtC,SAAS,EAAE,MAAM,EACjB,MAAM,CAAC,EAAE;IAAE,WAAW,CAAC,EAAE,MAAM,CAAA;CAAE,GAChC,IAAI,CAeN;AAED,wBAAgB,gBAAgB,CAAC,SAAS,EAAE,MAAM,EAAE,eAAe,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAc1F;AAED,wBAAgB,wBAAwB,CAAC,MAAM,EAAE;IAC/C,OAAO,EAAE,MAAM,CAAC;IAChB,OAAO,EAAE,MAAM,CAAC;IAChB,YAAY,EAAE,MAAM,CAAC;IACrB,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB,GAAG,MAAM,CAQT"}
|