@openclaw/fs-safe 0.11.0 → 0.12.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 +58 -0
- package/README.md +4 -2
- package/dist/advanced.d.ts +1 -1
- package/dist/advanced.d.ts.map +1 -1
- package/dist/advanced.js +1 -1
- package/dist/archive-entry.d.ts.map +1 -1
- package/dist/archive-entry.js +4 -3
- package/dist/archive-gzip-tail.d.ts.map +1 -1
- package/dist/archive-gzip-tail.js +13 -9
- package/dist/archive-read.d.ts.map +1 -1
- package/dist/archive-read.js +37 -33
- package/dist/archive-zip-names.d.ts.map +1 -1
- package/dist/archive-zip-names.js +17 -5
- package/dist/bounded-read.js +2 -2
- package/dist/device-path.d.ts.map +1 -1
- package/dist/device-path.js +5 -3
- package/dist/file-hash.d.ts.map +1 -1
- package/dist/file-hash.js +9 -2
- package/dist/file-store-prune.d.ts.map +1 -1
- package/dist/file-store-prune.js +9 -1
- package/dist/filename.d.ts +1 -0
- package/dist/filename.d.ts.map +1 -1
- package/dist/filename.js +30 -13
- package/dist/guarded-mkdir.d.ts +2 -0
- package/dist/guarded-mkdir.d.ts.map +1 -1
- package/dist/guarded-mkdir.js +50 -15
- package/dist/guest.d.ts.map +1 -1
- package/dist/guest.js +12 -4
- package/dist/install-path.d.ts +6 -0
- package/dist/install-path.d.ts.map +1 -1
- package/dist/install-path.js +13 -0
- package/dist/json-durable-queue-ownership.d.ts +2 -0
- package/dist/json-durable-queue-ownership.d.ts.map +1 -1
- package/dist/json-durable-queue-ownership.js +47 -3
- package/dist/json-durable-queue-read.d.ts +6 -0
- package/dist/json-durable-queue-read.d.ts.map +1 -0
- package/dist/json-durable-queue-read.js +59 -0
- package/dist/json-durable-queue.d.ts +1 -1
- package/dist/json-durable-queue.d.ts.map +1 -1
- package/dist/json-durable-queue.js +37 -80
- package/dist/native-binding.d.ts +7 -0
- package/dist/native-binding.d.ts.map +1 -1
- package/dist/path-scope-lexical.d.ts +14 -0
- package/dist/path-scope-lexical.d.ts.map +1 -0
- package/dist/path-scope-lexical.js +27 -0
- package/dist/path.d.ts.map +1 -1
- package/dist/path.js +20 -2
- package/dist/pinned-write.js +1 -0
- package/dist/root-boundary.d.ts +25 -0
- package/dist/root-boundary.d.ts.map +1 -0
- package/dist/root-boundary.js +177 -0
- package/dist/root-context.d.ts.map +1 -1
- package/dist/root-context.js +32 -5
- package/dist/root-directory-list.d.ts.map +1 -1
- package/dist/root-directory-list.js +10 -2
- package/dist/root-impl.d.ts.map +1 -1
- package/dist/root-impl.js +53 -34
- package/dist/root-move-preflight.d.ts +8 -0
- package/dist/root-move-preflight.d.ts.map +1 -0
- package/dist/root-move-preflight.js +16 -0
- package/dist/root-path-existing.d.ts +2 -0
- package/dist/root-path-existing.d.ts.map +1 -1
- package/dist/root-path-existing.js +10 -2
- package/dist/root-path.d.ts +2 -0
- package/dist/root-path.d.ts.map +1 -1
- package/dist/root-path.js +54 -26
- package/dist/root-paths.d.ts +2 -6
- package/dist/root-paths.d.ts.map +1 -1
- package/dist/root-paths.js +14 -26
- package/dist/root-walk.d.ts.map +1 -1
- package/dist/root-walk.js +2 -1
- package/dist/root-write-mode.d.ts +2 -0
- package/dist/root-write-mode.d.ts.map +1 -1
- package/dist/root-write-mode.js +20 -7
- package/dist/root-write-verification.d.ts.map +1 -1
- package/dist/root-write-verification.js +12 -3
- package/dist/safe-path-segment.d.ts.map +1 -1
- package/dist/safe-path-segment.js +3 -1
- package/dist/secure-file-windows.d.ts +10 -0
- package/dist/secure-file-windows.d.ts.map +1 -0
- package/dist/secure-file-windows.js +186 -0
- package/dist/secure-file.d.ts.map +1 -1
- package/dist/secure-file.js +6 -3
- package/dist/sibling-staged-file.d.ts +1 -0
- package/dist/sibling-staged-file.d.ts.map +1 -1
- package/dist/sibling-staged-file.js +7 -0
- package/dist/sibling-temp.d.ts.map +1 -1
- package/dist/sibling-temp.js +2 -1
- package/dist/sidecar-lock-acquire.js +1 -1
- package/dist/sidecar-lock.d.ts.map +1 -1
- package/dist/sidecar-lock.js +2 -3
- package/dist/temp-target.d.ts.map +1 -1
- package/dist/temp-target.js +3 -1
- package/dist/walk.d.ts.map +1 -1
- package/dist/walk.js +11 -10
- package/docs/advanced.md +3 -1
- package/docs/archive.md +6 -0
- package/docs/contributing.md +4 -1
- package/docs/copy.md +1 -1
- package/docs/durability.md +4 -2
- package/docs/file-store.md +6 -0
- package/docs/filename.md +9 -2
- package/docs/guest.md +5 -0
- package/docs/install-path.md +59 -13
- package/docs/install.md +4 -1
- package/docs/native-helper.md +4 -2
- package/docs/native.md +14 -2
- package/docs/path.md +1 -1
- package/docs/permissions.md +26 -1
- package/docs/public-api.md +5 -0
- package/docs/root.md +3 -1
- package/docs/secure-file.md +15 -14
- package/docs/security-model.md +1 -1
- package/docs/store.md +22 -1
- package/docs/temp.md +4 -0
- package/package.json +8 -8
package/docs/contributing.md
CHANGED
|
@@ -76,7 +76,10 @@ setup use `useSuiteFixture` from `test/helpers/suite-fixture.ts`: setup has a se
|
|
|
76
76
|
removing directories. Run shared-state corpora sequentially with a deadline per
|
|
77
77
|
payload. Keep child-process liveness limits separate from fixture preparation.
|
|
78
78
|
The Windows CI slow-copy proof runs the real package-copy process-exit test with a
|
|
79
|
-
|
|
79
|
+
16-second copy delay, a 10-second child deadline, a 15-second test deadline, and
|
|
80
|
+
60-second setup and teardown hook budgets. Other hosts retain the ordinary
|
|
81
|
+
six-second delay, four-second child deadline, five-second test deadline, and
|
|
82
|
+
30-second hook budgets:
|
|
80
83
|
|
|
81
84
|
```bash
|
|
82
85
|
pnpm build
|
package/docs/copy.md
CHANGED
|
@@ -64,7 +64,7 @@ Automatic copying does not recover from permission errors, I/O errors, cancellat
|
|
|
64
64
|
|
|
65
65
|
On Windows, automatic byte copying uses the native binding when available to transfer data between the already-checked file handles in chunks of at most 1 MiB, with smaller buffers for small files. This accelerates NTFS and cross-volume copies without reopening source or destination pathnames. The native worker finishes before its descriptors are closed or cancellation is reported. `clone: "never"` and native-disabled copies use JavaScript read/write loops with reusable buffers: at most 1 MiB per active file on Windows, or 128 KiB elsewhere. Both paths share the file-worker budget across sibling directories, wait for all admitted writes after cancellation or failure, and restore directory timestamps after their descendants finish. Completed directory traversals awaiting writes or metadata are bounded by concurrency; ancestors remain pinned while traversing their children.
|
|
66
66
|
|
|
67
|
-
On Linux, automatic byte copying also uses the native binding when available. It reads in 1 MiB
|
|
67
|
+
On Linux, automatic byte copying also uses the native binding when available. It reads in chunks up to 1 MiB, using smaller buffers for smaller source-size hints, and leaves leading and trailing zero-filled portions of each chunk unwritten in the new file, avoiding their allocation on filesystems that support sparse files. A final size update preserves trailing holes and all-zero files. This path uses reads and writes, without cloning or copy offload; it still reads the full logical contents and does not promise identical sparse extent layout. `clone: "never"` retains the JavaScript byte-copy path.
|
|
68
68
|
|
|
69
69
|
Clones preserve file contents, empty directories, timestamps, executable modes where supported, and literal symbolic links. Editing a clone does not modify its source. Unsupported filesystem operations fail; callers may choose their own copy or checkout fallback after the failed operation has settled.
|
|
70
70
|
|
package/docs/durability.md
CHANGED
|
@@ -242,8 +242,10 @@ When the optional binding is active, hashing runs as an async native task and
|
|
|
242
242
|
does not occupy the JavaScript event loop with digest updates. With native mode
|
|
243
243
|
`off`, or in `auto` when no binding loads, the fallback performs asynchronous
|
|
244
244
|
positioned reads in chunks of up to 256 KiB but updates Node's `Hash` on the JavaScript
|
|
245
|
-
thread. Both paths stream
|
|
246
|
-
|
|
245
|
+
thread. Both paths stream bounded buffers rather than loading the file into memory.
|
|
246
|
+
The fallback sizes its scratch buffer to small files and grows it if a stale
|
|
247
|
+
size hint is exceeded, while still probing for actual EOF and byte-limit overflow.
|
|
248
|
+
Native mode `require` keeps its usual fail-closed loader semantics.
|
|
247
249
|
|
|
248
250
|
### Synchronous hashing
|
|
249
251
|
|
package/docs/file-store.md
CHANGED
|
@@ -256,6 +256,12 @@ type FileStorePruneOptions = {
|
|
|
256
256
|
|
|
257
257
|
Symlinks are skipped. The walk is best-effort — failures on individual entries don't abort the whole prune. Compares against `mtimeMs`.
|
|
258
258
|
|
|
259
|
+
Pruning rechecks that a selected entry is still a regular file and still expired
|
|
260
|
+
immediately before guarded removal. Fresh replacements and in-place timestamp
|
|
261
|
+
refreshes are preserved; replacements that are themselves expired remain
|
|
262
|
+
eligible. This does not require read permission. The existing best-effort
|
|
263
|
+
external-process race window after dispatch still applies.
|
|
264
|
+
|
|
259
265
|
## Difference from `Root`
|
|
260
266
|
|
|
261
267
|
| `FileStore` | `Root` |
|
package/docs/filename.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Filenames
|
|
2
2
|
|
|
3
|
-
`sanitizeUntrustedFileName(name, fallback)` reduces a filename string from an untrusted source to one traversal-free path segment. Use it as a thin first pass before storing user-supplied names;
|
|
3
|
+
`sanitizeUntrustedFileName(name, fallback)` reduces a filename string from an untrusted source to one traversal-free path segment. Use it as a thin first pass before storing user-supplied names; use [`safePathSegmentHashedV2`](install-path.md#safepathsegmenthashedv2) when mapping untrusted install IDs to separate directory names.
|
|
4
4
|
|
|
5
5
|
```ts
|
|
6
6
|
import { sanitizeUntrustedFileName } from "@openclaw/fs-safe/advanced";
|
|
@@ -27,6 +27,13 @@ In order:
|
|
|
27
27
|
6. **Suffix Windows reserved basenames.** Compare the part before the first `.` case-insensitively with the Windows device-name set, including `CON`, `PRN`, `AUX`, `NUL`, `CLOCK$`, `CONIN$`, `CONOUT$`, `COM1..9`, `LPT1..9`, and their superscript `¹`, `²`, and `³` variants. Windows-ignored spaces and dots at the end of that basename do not disguise a device name. A match gains `_` before its extension, preserving the original case and extension on every platform.
|
|
28
28
|
7. **Truncate.** If the cleaned segment is longer than 200 UTF-16 code units, take up to the first 200 without splitting a valid Unicode surrogate pair.
|
|
29
29
|
|
|
30
|
+
If truncation itself exposes a reserved-device basename after Windows ignores
|
|
31
|
+
trailing spaces or dots, the result is shortened once more and receives the
|
|
32
|
+
same underscore suffix. A name that reaches the sanitization branch therefore
|
|
33
|
+
remains at most 200 UTF-16 code units and is never a Windows reserved-device
|
|
34
|
+
alias. `fallbackName` is returned verbatim for empty or path-alias input, so
|
|
35
|
+
callers must supply a fallback that already satisfies their filename policy.
|
|
36
|
+
|
|
30
37
|
That's it. The function stays intentionally small: it removes traversal and
|
|
31
38
|
the most obvious cross-platform device and character hazards, but it is not a
|
|
32
39
|
complete portable-filename or uniqueness policy.
|
|
@@ -92,5 +99,5 @@ await fs.write(`uploads/${safe}`, body); // fs is a Root() handle; rejects trave
|
|
|
92
99
|
|
|
93
100
|
## See also
|
|
94
101
|
|
|
95
|
-
- [Install path helpers](install-path.md) —
|
|
102
|
+
- [Install path helpers](install-path.md) — legacy directory-segment sanitizers and `safePathSegmentHashedV2` for untrusted install IDs.
|
|
96
103
|
- [`root()`](root.md) — the boundary you'll write into after sanitizing.
|
package/docs/guest.md
CHANGED
|
@@ -113,6 +113,11 @@ prefixes and use short random suffixes independent of the destination basename,
|
|
|
113
113
|
so legal names near the filesystem's component limit also work for writes and
|
|
114
114
|
cross-device moves.
|
|
115
115
|
|
|
116
|
+
Cross-device symlink moves create the new link in a private destination-side
|
|
117
|
+
staging directory before atomically replacing the destination. Link creation or
|
|
118
|
+
publication failure preserves the existing destination and source link; ordinary
|
|
119
|
+
failure cleanup removes the staging directory.
|
|
120
|
+
|
|
116
121
|
Cross-device directory moves build a copy manifest and check it during source
|
|
117
122
|
cleanup. Source changes can leave the published destination and some or all
|
|
118
123
|
of the source. Regular-file and symlink move fallbacks unlink the source
|
package/docs/install-path.md
CHANGED
|
@@ -8,6 +8,7 @@ import {
|
|
|
8
8
|
resolveSafeInstallDir,
|
|
9
9
|
safeDirName,
|
|
10
10
|
safePathSegmentHashed,
|
|
11
|
+
safePathSegmentHashedV2,
|
|
11
12
|
} from "@openclaw/fs-safe/advanced";
|
|
12
13
|
```
|
|
13
14
|
|
|
@@ -35,14 +36,17 @@ if (!r.ok) return reply(400, r.error);
|
|
|
35
36
|
await fs.mkdir(r.path, { recursive: true });
|
|
36
37
|
```
|
|
37
38
|
|
|
38
|
-
For
|
|
39
|
+
For untrusted IDs that must occupy separate install directories, pass
|
|
40
|
+
`nameEncoder: safePathSegmentHashedV2`. The default `safeDirName` and legacy
|
|
41
|
+
`safePathSegmentHashed` can map distinct IDs to the same directory; the boundary
|
|
42
|
+
check does not establish which ID owns an existing directory.
|
|
39
43
|
|
|
40
44
|
```ts
|
|
41
45
|
const r = resolveSafeInstallDir({
|
|
42
|
-
baseDir: "/srv/plugins",
|
|
46
|
+
baseDir: "/srv/plugins-v2",
|
|
43
47
|
id: untrustedId,
|
|
44
48
|
invalidNameMessage: "invalid plugin name",
|
|
45
|
-
nameEncoder:
|
|
49
|
+
nameEncoder: safePathSegmentHashedV2,
|
|
46
50
|
});
|
|
47
51
|
```
|
|
48
52
|
|
|
@@ -87,11 +91,48 @@ safeDirName(""); // ""
|
|
|
87
91
|
|
|
88
92
|
`safeDirName` does *not* try to be exhaustive about Windows-reserved names or special characters. It is purely a separator-stripping pass — `resolveSafeInstallDir` adds the boundary check on top so an `"../../etc"` input cannot escape `baseDir`.
|
|
89
93
|
|
|
90
|
-
|
|
94
|
+
Use `safePathSegmentHashedV2` when distinct untrusted IDs need separate names.
|
|
95
|
+
|
|
96
|
+
### `safePathSegmentHashedV2`
|
|
97
|
+
|
|
98
|
+
```ts
|
|
99
|
+
function safePathSegmentHashedV2(input: string): string;
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
Hashes every trimmed ID, including ordinary short names, into `id-v2-` followed
|
|
103
|
+
by 64 lowercase hexadecimal SHA-256 digits. The result is always 70 ASCII bytes,
|
|
104
|
+
contains no separators, and avoids Windows device names and ignored suffixes.
|
|
105
|
+
There is no readable prefix to truncate and no unchanged-name branch. Distinct
|
|
106
|
+
trimmed IDs, including an ID that looks like an encoded output, remain distinct
|
|
107
|
+
unless their full SHA-256 digests collide. The lowercase ASCII output also
|
|
108
|
+
preserves that distinction on case-insensitive and Unicode-normalizing volumes.
|
|
109
|
+
|
|
110
|
+
The stable V2 digest recipe is SHA-256 of the UTF-8 bytes of
|
|
111
|
+
`"@openclaw/fs-safe:install-path:v2\0"`, followed by the UTF-16LE bytes of
|
|
112
|
+
`input.trim()`, without a byte-order mark. The NUL-terminated prefix separates
|
|
113
|
+
this use of SHA-256 from other hash domains. UTF-16LE preserves exact JavaScript
|
|
114
|
+
code units, including lone surrogates. Inputs are not case-folded or Unicode
|
|
115
|
+
normalized. Surrounding whitespace, as removed by JavaScript `String.trim()`,
|
|
116
|
+
is the only intentional equivalence; internal whitespace remains significant.
|
|
117
|
+
|
|
118
|
+
```ts
|
|
119
|
+
const segment = safePathSegmentHashedV2("plugin/v1"); // id-v2-<64 hex digits>
|
|
120
|
+
safePathSegmentHashedV2(" plugin/v1 ") === segment; // true
|
|
121
|
+
safePathSegmentHashedV2("Plugin/v1") === segment; // false
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
This encoder computes a name; it does not authorize access or prove ownership
|
|
125
|
+
of a directory. Store the original trimmed ID in application-owned metadata and
|
|
126
|
+
verify it before reusing an existing install directory. Keep each install tree
|
|
127
|
+
on one encoding version. Switching to V2 changes existing paths: use a new base
|
|
128
|
+
directory or explicitly migrate directories after verifying their recorded IDs.
|
|
129
|
+
Do not silently fall back to a legacy path when the V2 path is missing.
|
|
91
130
|
|
|
92
131
|
### `safePathSegmentHashed`
|
|
93
132
|
|
|
94
|
-
|
|
133
|
+
Legacy readable encoding retained for path compatibility. It appends a short
|
|
134
|
+
content hash when sanitization changed the input or when the safe form is too
|
|
135
|
+
long; ordinary short names remain unchanged. Use V2 for new untrusted-ID mappings.
|
|
95
136
|
|
|
96
137
|
```ts
|
|
97
138
|
safePathSegmentHashed("plugin-v1"); // "plugin-v1" (unchanged short input)
|
|
@@ -104,31 +145,35 @@ safePathSegmentHashed("."); // "skill-cdb4ee2aea"
|
|
|
104
145
|
|
|
105
146
|
The sanitization is more aggressive than `safeDirName`: any character not in `[A-Za-z0-9._-]` becomes `-`, runs of `-` collapse, leading and trailing `-` are stripped, the empty/`.`/`..` fallback is `"skill"`. Long results are truncated to 50 chars before the hash is appended.
|
|
106
147
|
|
|
107
|
-
The suffix is the first 10 hex characters of `sha256(trimmedInput)`.
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
148
|
+
The suffix is the first 10 hex characters of `sha256(trimmedInput)`. This is a
|
|
149
|
+
40-bit identifier, and the hash is not applied to every ID: short generated
|
|
150
|
+
outputs overlap with accepted literal inputs. Case variants can also share a
|
|
151
|
+
directory on case-insensitive filesystems. Distinct IDs can therefore alias
|
|
152
|
+
without a hash collision. Do not use this legacy encoder as an identity or
|
|
153
|
+
authorization boundary for untrusted IDs. Inputs that differ only by surrounding
|
|
154
|
+
whitespace intentionally map to the same output. Its output and the default
|
|
155
|
+
encoder selected by `resolveSafeInstallDir` remain unchanged for compatibility.
|
|
111
156
|
|
|
112
157
|
## Common patterns
|
|
113
158
|
|
|
114
159
|
### Install a plugin
|
|
115
160
|
|
|
116
161
|
```ts
|
|
117
|
-
import { resolveSafeInstallDir, assertCanonicalPathWithinBase,
|
|
162
|
+
import { resolveSafeInstallDir, assertCanonicalPathWithinBase, safePathSegmentHashedV2 } from "@openclaw/fs-safe/advanced";
|
|
118
163
|
import { extractArchive } from "@openclaw/fs-safe/archive";
|
|
119
164
|
import fs from "node:fs/promises";
|
|
120
165
|
|
|
121
166
|
const r = resolveSafeInstallDir({
|
|
122
|
-
baseDir: "/srv/plugins",
|
|
167
|
+
baseDir: "/srv/plugins-v2",
|
|
123
168
|
id: untrustedName,
|
|
124
169
|
invalidNameMessage: "invalid plugin name",
|
|
125
|
-
nameEncoder:
|
|
170
|
+
nameEncoder: safePathSegmentHashedV2,
|
|
126
171
|
});
|
|
127
172
|
if (!r.ok) return reply(400, r.error);
|
|
128
173
|
|
|
129
174
|
await fs.mkdir(r.path, { recursive: true, mode: 0o755 });
|
|
130
175
|
await assertCanonicalPathWithinBase({
|
|
131
|
-
baseDir: "/srv/plugins",
|
|
176
|
+
baseDir: "/srv/plugins-v2",
|
|
132
177
|
candidatePath: r.path,
|
|
133
178
|
boundaryLabel: "plugin install dir",
|
|
134
179
|
});
|
|
@@ -148,6 +193,7 @@ const snap = resolveSafeInstallDir({
|
|
|
148
193
|
baseDir: "/srv/snapshots",
|
|
149
194
|
id: `${runId}-${version}`,
|
|
150
195
|
invalidNameMessage: "invalid snapshot id",
|
|
196
|
+
nameEncoder: safePathSegmentHashedV2,
|
|
151
197
|
});
|
|
152
198
|
if (!snap.ok) throw new Error(snap.error);
|
|
153
199
|
await fs.mkdir(snap.path, { recursive: true });
|
package/docs/install.md
CHANGED
|
@@ -54,7 +54,10 @@ also rejects canonicalization when the addon or its canonicalizer is unavailable
|
|
|
54
54
|
Node and Windows use their existing runtime canonicalizers. On Windows, Bun's
|
|
55
55
|
recursive directory creation receives an absolute spelling that preserves raw
|
|
56
56
|
path components, working around its rejection of existing relative `.` and `..`
|
|
57
|
-
directories.
|
|
57
|
+
directories. Windows native descriptor-relative operations require Bun to expose
|
|
58
|
+
the paired libuv descriptor bridge from its host executable; a missing or partial
|
|
59
|
+
bridge fails explicitly with `ENOTSUP`. Public paths and caller-supplied filesystem
|
|
60
|
+
adapters remain unchanged.
|
|
58
61
|
|
|
59
62
|
The upstream fix is tracked in [Bun #42374](https://github.com/oven-sh/bun/pull/42374).
|
|
60
63
|
The adapter can be removed when the supported Bun baseline includes that fix.
|
package/docs/native-helper.md
CHANGED
|
@@ -70,10 +70,12 @@ normalization, and the decision to fall back.
|
|
|
70
70
|
|
|
71
71
|
- Linux uses `openat2` with `RESOLVE_BENEATH | RESOLVE_NO_MAGICLINKS`, `renameat`, and `renameat2(RENAME_NOREPLACE)`. Owned-tree cleanup enumerates and unlinks through retained directory descriptors and rejects device crossings.
|
|
72
72
|
- 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; owned-tree cleanup uses descriptor-relative `openat`/`unlinkat`.
|
|
73
|
-
- Windows uses handle-relative `NtCreateFile`, rejects reparse points during root-bounded traversal, uses `FileRenameInfoEx` with replacement selected explicitly by the TypeScript policy layer, and deletes owned trees through exact opened handles with `FileDispositionInfoEx`; symlink/reparse entries in owned trees are removed as leaves and never traversed.
|
|
73
|
+
- Windows uses handle-relative `NtCreateFile`, rejects reparse points during root-bounded traversal, uses `FileRenameInfoEx` with replacement selected explicitly by the TypeScript policy layer, and deletes owned trees through exact opened handles with `FileDispositionInfoEx`; symlink/reparse entries in owned trees are removed as leaves and never traversed. Descriptors crossing N-API are converted only by the host executable's paired `uv_get_osfhandle` and `uv_open_osfhandle` exports. A runtime without both exports is unsupported for these native operations; the binding never guesses a raw HANDLE or uses a foreign CRT descriptor table.
|
|
74
74
|
|
|
75
75
|
Native primitives back create-only and replacing pinned writes, async sidecar creation,
|
|
76
|
-
guarded publication, archive acceleration, and direct Windows ACL operations.
|
|
76
|
+
guarded publication, archive acceleration, and direct Windows ACL operations. Windows
|
|
77
|
+
secure-file reads require descriptor-bound owner/DACL facts from the current helper;
|
|
78
|
+
they do not use the standalone pathname inspector's command fallback.
|
|
77
79
|
Equivalent JavaScript paths remain available for documented fallback-capable
|
|
78
80
|
features. See [Native architecture](native.md#javascript-fallback-guarantees-and-delta)
|
|
79
81
|
for the exact difference.
|
package/docs/native.md
CHANGED
|
@@ -55,7 +55,12 @@ whether a path, archive entry, mode, owner, or cleanup policy is acceptable.
|
|
|
55
55
|
`FILE_OPEN_REPARSE_POINT`, then explicitly rejects reparse points. Rename and
|
|
56
56
|
hardlink operations stay rooted in already-open handles. Owner/DACL reads
|
|
57
57
|
use `GetSecurityInfo`; private directories receive their protected DACL in
|
|
58
|
-
|
|
58
|
+
an exclusive, handle-relative `NtCreateFile` call. Their created handles remain
|
|
59
|
+
open through ACL and pathname-association checks and own any failure cleanup.
|
|
60
|
+
N-API descriptors cross into and out of
|
|
61
|
+
this layer only through the host executable's paired libuv descriptor bridge;
|
|
62
|
+
missing or partial exports fail with `ENOTSUP` instead of trying a raw HANDLE
|
|
63
|
+
or add-on CRT descriptor namespace.
|
|
59
64
|
|
|
60
65
|
## Archives
|
|
61
66
|
|
|
@@ -125,6 +130,13 @@ All routes preserve `wx` semantics and the same source/target identity and
|
|
|
125
130
|
SHA-256 fencing. Native hashing and Linux whole-file copying run on N-API async
|
|
126
131
|
workers rather than the JavaScript event loop.
|
|
127
132
|
|
|
133
|
+
Linux range copying confirms every zero-byte result with a positioned source
|
|
134
|
+
read at the current transfer offset, including after earlier calls copied data.
|
|
135
|
+
If readable bytes remain, automatic Root copying resumes its byte loop from
|
|
136
|
+
that offset; exclusive publication removes its partial target before retrying
|
|
137
|
+
the guarded byte-copy fallback. EOF checks preserve descriptor cursors and do
|
|
138
|
+
not bypass the byte limit.
|
|
139
|
+
|
|
128
140
|
## Mode semantics
|
|
129
141
|
|
|
130
142
|
| Mode | Native loading | Fallback |
|
|
@@ -182,7 +194,7 @@ remain TypeScript-owned. What changes is the syscall strength or availability:
|
|
|
182
194
|
| Zstd/bzip2 TAR | Supported. | Unsupported; typed `helper-unavailable`. |
|
|
183
195
|
| 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. |
|
|
184
196
|
| `rename-noreplace` | Atomic platform no-replace rename. | Unsupported; no emulation by check-then-rename. |
|
|
185
|
-
| Windows DACL read | Direct `GetSecurityInfo`; the public facts API exposes ordered basic allow/deny ACE SIDs, masks, and decoded flags without trust policy. | Structured .NET owner/DACL inspection
|
|
197
|
+
| Windows DACL read | Direct `GetSecurityInfo`; the public facts API exposes ordered basic allow/deny ACE SIDs, masks, and decoded flags without trust policy. Secure-file reads query the borrowed open descriptor and compare its 32-bit volume serial and 64-bit file-index projection with Node's bigint receipt. | Structured .NET owner/DACL inspection remains available to standalone pathname reporting. Secure-file reads fail closed without the descriptor capability. |
|
|
186
198
|
| Windows private directory | Creation-time protected DACL. | Unsupported; no weaker pathname-only substitute. |
|
|
187
199
|
|
|
188
200
|
Use `off` in CI to keep the fallback contract exercised. Use `require` when a
|
package/docs/path.md
CHANGED
|
@@ -36,7 +36,7 @@ isPathInside("/srv/uploads", "/srv/uploads-other/x"); // false
|
|
|
36
36
|
isPathInside("/srv/uploads", "/srv/uploads"); // true (root itself counts)
|
|
37
37
|
```
|
|
38
38
|
|
|
39
|
-
The check is platform-aware: on Windows, paths are normalized for case and separator before comparison.
|
|
39
|
+
The check is platform-aware: on Windows, paths are normalized for case and separator before comparison. This is deliberately a string-only, lexical answer; case folding does not prove that two differently cased prefixes name the same directory on a case-sensitive Windows directory. Root operations perform their own filesystem-identity admission when containment depends on case folding.
|
|
40
40
|
|
|
41
41
|
### `isPathInsideWithRealpath(rootDir, target, opts?)`
|
|
42
42
|
|
package/docs/permissions.md
CHANGED
|
@@ -42,7 +42,7 @@ POSIX remediation strings shell-quote paths with whitespace or metacharacters
|
|
|
42
42
|
and protect option-like paths with `--`, so they can be presented as commands
|
|
43
43
|
without letting the inspected pathname add shell syntax.
|
|
44
44
|
|
|
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.
|
|
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. This remains a pathname reporting API with the fallbacks described below. `readSecureFile()` does not use those pathname fallbacks on Windows: it requires descriptor-bound native owner/DACL facts for the exact handle it reads.
|
|
46
46
|
|
|
47
47
|
## Advanced Windows ACL helpers
|
|
48
48
|
|
|
@@ -178,6 +178,31 @@ await openSqlite(path.join(sqliteDirectory, "sessions.sqlite"));
|
|
|
178
178
|
On Windows with native support, this creates the directory and applies a
|
|
179
179
|
protected owner + LocalSystem + Administrators full-control DACL directly with
|
|
180
180
|
an atomic security descriptor; no PowerShell or `icacls` process is launched.
|
|
181
|
+
The native operation retains the parent and exact created-directory handles
|
|
182
|
+
through ACL and final pathname validation. If validation fails, it attempts only
|
|
183
|
+
nonrecursive deletion through the created handle, preserving any pathname
|
|
184
|
+
replacement. If cleanup also fails, the error retains the original failure and
|
|
185
|
+
includes the cleanup failure.
|
|
186
|
+
|
|
187
|
+
Directory association checks compare the complete 64-bit volume serial and
|
|
188
|
+
128-bit `FILE_ID_INFO` identity, including on ReFS. If that identity class is
|
|
189
|
+
unavailable, the operation fails closed without a narrower file-index fallback.
|
|
190
|
+
Validation confirms that the created directory is local, its DACL is protected
|
|
191
|
+
from inheritance, and its final public pathname opens the same local directory.
|
|
192
|
+
|
|
193
|
+
This is a point-in-time pathname association check. The function closes its
|
|
194
|
+
handles before returning; callers must keep the pathname's ancestry trusted
|
|
195
|
+
during subsequent use, including opening SQLite databases in the example above.
|
|
196
|
+
The immediate parent and final directory must not be reparse points. Earlier
|
|
197
|
+
ancestor reparse points can be followed; this API does not reject every reparse
|
|
198
|
+
point in the full ancestry.
|
|
199
|
+
|
|
200
|
+
Path components ending in a space or period are rejected before filesystem
|
|
201
|
+
operations to avoid differing Win32 and native pathname interpretations. This
|
|
202
|
+
also rejects explicit `.` and `..` components, including spellings such as
|
|
203
|
+
`.\private` and `parent\..\private`, as a compatibility restriction. Simple
|
|
204
|
+
relative names without these components remain supported.
|
|
205
|
+
|
|
181
206
|
This API is Windows-only and native-only; it fails closed with
|
|
182
207
|
`FsSafeError("helper-unavailable")` on other platforms, when native mode is off,
|
|
183
208
|
or when the binding is unavailable. POSIX callers should create private
|
package/docs/public-api.md
CHANGED
|
@@ -26,6 +26,11 @@ deprecated native-configuration bridge retains the `FsSafePythonConfig` type.
|
|
|
26
26
|
|
|
27
27
|
## `path` and `advanced`
|
|
28
28
|
|
|
29
|
+
`safePathSegmentHashedV2` encodes every trimmed install ID with domain-separated
|
|
30
|
+
SHA-256 into a fixed lowercase segment. The legacy `safePathSegmentHashed` keeps
|
|
31
|
+
its existing output but can alias distinct IDs. See [install paths](install-path.md)
|
|
32
|
+
for the exact encoding and migration contract.
|
|
33
|
+
|
|
29
34
|
The lexical path surface additionally exports `isNodeError`,
|
|
30
35
|
`isPathRelativeEscape`, `normalizeWindowsPathForComparison`,
|
|
31
36
|
`resolveSafeRelativePath`, `splitSafeRelativePath`, and
|
package/docs/root.md
CHANGED
|
@@ -100,7 +100,9 @@ The read methods also accept an absolute spelling that already resolves inside
|
|
|
100
100
|
the root. `readAbsolute()` and `reader()` make that intent explicit and accept
|
|
101
101
|
both the configured root spelling and its canonical real path when the Root was
|
|
102
102
|
created through a directory symlink or Windows junction. An absolute path
|
|
103
|
-
outside the root is still rejected.
|
|
103
|
+
outside the root is still rejected. On Windows, alternate casing is accepted
|
|
104
|
+
only when the differently cased Root prefix has the Root's exact directory
|
|
105
|
+
identity; the operation then continues under the trusted Root spelling.
|
|
104
106
|
|
|
105
107
|
### Writes
|
|
106
108
|
|
package/docs/secure-file.md
CHANGED
|
@@ -27,9 +27,11 @@ The helper:
|
|
|
27
27
|
- enforces `maxBytes` before and after reading
|
|
28
28
|
- closes the handle on success, error, and timeout
|
|
29
29
|
|
|
30
|
-
On POSIX, unsafe permissions mean group/world writable, and group/world readable unless `permissions.allowReadableByOthers` is true. On Windows, the helper
|
|
30
|
+
On POSIX, unsafe permissions mean group/world writable, and group/world readable unless `permissions.allowReadableByOthers` is true. On Windows, the helper queries owner, DACL, and locality from the same open descriptor that supplies the bytes. The native query returns the 32-bit volume serial and 64-bit file-index projection used by Node, which must equal Node's bigint descriptor receipt before its ACL facts are trusted. This avoids JavaScript number rounding but does not represent the full 128-bit file identity available on ReFS. Only the current user, LocalSystem, and built-in Administrators are trusted owner classes.
|
|
31
31
|
|
|
32
|
-
|
|
32
|
+
Windows secure reads require the matching current optional native package. A missing or stale helper, fd-to-handle conversion failure, denied `READ_CONTROL`, remote handle, incomplete descriptor, or unsupported ACE form rejects with `permission-unverified` before content is read. A malformed or different handle identity rejects with `path-mismatch`. There is no pathname-command fallback for `readSecureFile()`; the standalone reporting APIs in [`permissions`](permissions.md) retain their documented fallbacks. `permissions.allowInsecure` remains the explicit escape hatch and bypasses the ACL query.
|
|
33
|
+
|
|
34
|
+
Descriptor, pathname, and realpath identity checks use bigint stats internally to avoid JavaScript number rounding. The returned `stat` remains a normal Node `Stats` object with numeric fields. A zero Windows device or inode is unverified, never a match: the helper re-inspects that identity once using the same descriptor or pathname, then rejects persistent ambiguity with `path-mismatch`. A definite mismatch rejects immediately; retries retain known identity components and still enforce symlink policy.
|
|
33
35
|
|
|
34
36
|
## Options
|
|
35
37
|
|
|
@@ -62,6 +64,8 @@ type SecureFileReadOptions = {
|
|
|
62
64
|
|
|
63
65
|
`permissions.allowInsecure` is a migration escape hatch. Prefer fixing permissions and using [`formatPermissionRemediation`](permissions.md) to show the user what to run. `trust.allowNetworkPath` is off by default because UNC paths are remote authority, not local filesystem input. `inject` is for tests and platform adapters; production callers usually leave it unset.
|
|
64
66
|
|
|
67
|
+
On an actual Windows process with effective `platform: "win32"`, `inject.env` and `inject.exec` do not replace descriptor inspection. They remain available to simulated Windows checks on non-Windows hosts.
|
|
68
|
+
|
|
65
69
|
`permissions.allowInsecure` bypasses only permission checks. Neither it nor `inject.platform` changes filesystem identity verification, which always uses the actual process platform. `trust.allowSymlink` permits an alias but still requires its target and realpath to match the opened descriptor.
|
|
66
70
|
|
|
67
71
|
## Errors
|
|
@@ -77,23 +81,20 @@ type SecureFileReadOptions = {
|
|
|
77
81
|
| `hardlink` | The descriptor, pathname, or realpath has more than one link. |
|
|
78
82
|
| `path-mismatch` | The path or realpath changed between open and verification, or filesystem identity could not be verified after bounded re-inspection. |
|
|
79
83
|
| `outside-workspace` | `realPath` is outside `trust.trustedDirs`. |
|
|
80
|
-
| `permission-unverified` | Required mode/ACL checks could not be completed. |
|
|
84
|
+
| `permission-unverified` | Required mode/ACL checks could not be completed, including when descriptor-bound Windows inspection is unavailable. |
|
|
81
85
|
| `insecure-permissions` | Mode bits or ACLs grant broader access than allowed. |
|
|
82
86
|
| `not-owned` | POSIX owner uid is not the current process uid. |
|
|
83
87
|
| `too-large` | File size or bytes read exceeded `maxBytes`. |
|
|
84
88
|
| `timeout` | `timeoutMs` elapsed while reading. |
|
|
85
89
|
|
|
86
|
-
Windows inspection failures
|
|
87
|
-
and
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
errors also retain their original execFile exception in the cause chain.
|
|
95
|
-
Treat causes as restricted local diagnostic data. No retries are performed,
|
|
96
|
-
and verification order and rejection conditions are unchanged.
|
|
90
|
+
Windows descriptor-inspection failures are operational `permission-unverified`
|
|
91
|
+
errors and refuse the read. The original native exception is retained as
|
|
92
|
+
`cause`; treat causes as restricted local diagnostic data. No pathname or ACL
|
|
93
|
+
content is copied into the display message. Test adapters that simulate Windows
|
|
94
|
+
on another operating system retain the standalone pathname inspector's
|
|
95
|
+
structured command diagnostics (`ownerError`, `command`, `durationMs`,
|
|
96
|
+
`timedOut`, `exitCode`, `signal`, and bounded escaped `stderr`). Actual Windows
|
|
97
|
+
secure reads do not start those commands. No retries are performed.
|
|
97
98
|
|
|
98
99
|
## See also
|
|
99
100
|
|
package/docs/security-model.md
CHANGED
|
@@ -45,7 +45,7 @@ If you need full sandboxing, run the worker under reduced privileges (uid, conta
|
|
|
45
45
|
|
|
46
46
|
### Path traversal and absolute paths
|
|
47
47
|
|
|
48
|
-
Every path is resolved against the canonicalized real path of the root, then checked
|
|
48
|
+
Every path is resolved against the canonicalized real path of the root, then checked at that boundary. On Windows, an exact-case structural Root prefix stays on the lexical fast path; a prefix accepted only by case folding must have the Root's exact directory identity and is rebased onto the trusted Root spelling before use. 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.
|
|
49
49
|
|
|
50
50
|
### Symlinks (read side)
|
|
51
51
|
|
package/docs/store.md
CHANGED
|
@@ -72,12 +72,33 @@ Loading serializes consumers for one ID through a sidecar lock, then creates `pr
|
|
|
72
72
|
|
|
73
73
|
Queue and failed directory creation fsyncs every newly-created parent edge from the leaf toward the trusted root. Enqueue and migration writes fsync the temp file and parent; claim, acknowledgement, quarantine, delivered-marker cleanup, and retirement transitions fsync every affected directory and propagate real sync failures. A transition may already be visible when a post-mutation sync fails, so retry the same operation to complete its crash-recovery state. Acknowledgement retries resync the queue directory even when both `.processing` and `.delivered` marker names are already absent, before reporting completion or rejecting a newer pending generation; quarantine retries with only failed evidence resync that destination before repairing the vanished queue source.
|
|
74
74
|
|
|
75
|
-
`writeJsonDurableQueueEntry()` and migrations share strict parent synchronization inside the atomic writer's retained descriptor and per-path serialization lifetime, followed by published-file identity verification. If sync fails after publication, the write rejects without rolling back the published JSON; retrying writes the entry again and must complete its own sync. This is not a rollback, deduplication, or exactly-once guarantee. The generic `replaceFileAtomic({ syncParentDir: true })` option remains best-effort.
|
|
75
|
+
`writeJsonDurableQueueEntry()` and migrations share strict parent synchronization inside the atomic writer's retained descriptor and per-path serialization lifetime, followed by published-file identity verification. If sync fails after publication, the write rejects without rolling back the published JSON; retrying `writeJsonDurableQueueEntry()` writes the entry again and must complete its own sync. Loader retries resync an existing processing claim's parent under the transfer lock before calling `read`, even when a version-dependent callback would no longer request migration. Fresh claims and same-directory source retirement already complete that sync. This is not a rollback, deduplication, or exactly-once guarantee. The generic `replaceFileAtomic({ syncParentDir: true })` option remains best-effort.
|
|
76
76
|
|
|
77
77
|
Batch loading skips invalid entry names, malformed, oversized, or unreadable entry content, and caller `read` callback failures. Initially unowned pending entries (hardlinks or unverifiable identities), symlinks, non-files, and absent pending entries are also skipped. Claim, transfer-lock, retirement, and migration write/publication/durability failures reject the batch with the original error, even if earlier entries succeeded. Migration in both loaders strictly syncs the parent directory after successful publication. Visible transitions and earlier processing claims remain for retry; a rejected batch does not acknowledge or roll them back.
|
|
78
78
|
|
|
79
79
|
Failed destinations are create-only. Quarantine publishes the claimed file by hardlink, so the queue and failed directories must share a filesystem with hardlink support. If `failed/<id>.json` already exists, quarantine rejects while preserving both that earlier evidence and the current claimed entry instead of overwriting either file. The `read` callback continues to receive the logical `.json` path even though bytes are read and migrations are written through the claimed path.
|
|
80
80
|
|
|
81
|
+
Migrations stay bound to the exact processing file opened for that load. The
|
|
82
|
+
read descriptor remains pinned while the callback runs outside the transfer
|
|
83
|
+
lock; after the callback returns, migration reacquires the lock and rechecks the
|
|
84
|
+
claim before publication. If another consumer acknowledged, quarantined, or
|
|
85
|
+
replaced that claim, the migration rejects with `FsSafeError("path-mismatch")`
|
|
86
|
+
and leaves the newer generation or failed evidence intact. A stale migration
|
|
87
|
+
rejects both single and batch loads; ordinary callback failures retain their
|
|
88
|
+
existing single-load rejection and batch-skip behavior.
|
|
89
|
+
|
|
90
|
+
On Windows, migration releases its read pin once at this publication boundary
|
|
91
|
+
because an open target can block replacement. It rechecks the exact pathname
|
|
92
|
+
identity after the asynchronous close while still holding the transfer lock.
|
|
93
|
+
POSIX retains the read pin through publication. Other readers keep ownership
|
|
94
|
+
of their handles; Windows sharing denials still reject and can be retried after
|
|
95
|
+
those readers close.
|
|
96
|
+
|
|
97
|
+
Generation arbitration requires consumers to use the transfer lock. As with
|
|
98
|
+
[atomic writes](atomic.md#beforerename), identity checks and pathname replacement
|
|
99
|
+
are separate operations; use a trusted writable parent or OS isolation against
|
|
100
|
+
processes that ignore the lock and mutate queue paths concurrently.
|
|
101
|
+
|
|
81
102
|
Queue entry reads verify lossless file identities before opening, on the opened
|
|
82
103
|
descriptor, and at the current pathname before reading bytes. POSIX opens are
|
|
83
104
|
nonblocking, so a raced FIFO is rejected rather than stalling a consumer. On Windows, an
|
package/docs/temp.md
CHANGED
|
@@ -269,6 +269,10 @@ files, hardlinks, and changes between the pre-open pathname, opened descriptor,
|
|
|
269
269
|
and current pathname are rejected. The callback must finish and close its
|
|
270
270
|
writer before returning. Its return value is preserved as `result`.
|
|
271
271
|
|
|
272
|
+
Generated temp filenames suffix Windows reserved-device basenames on every
|
|
273
|
+
platform. A completed sibling staging component that still resolves as a
|
|
274
|
+
Windows device alias rejects with `invalid-path` before hooks or producers run.
|
|
275
|
+
|
|
272
276
|
The helper retains one descriptor through requested mode application, opt-in
|
|
273
277
|
file synchronization, rename, and publication verification. It opens read-only
|
|
274
278
|
unless file synchronization is requested, so closed read-only producer output
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@openclaw/fs-safe",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.12.0",
|
|
4
4
|
"description": "Capability-style filesystem roots for Node.js apps that handle untrusted relative paths.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"filesystem",
|
|
@@ -167,13 +167,13 @@
|
|
|
167
167
|
"archive:producer-smoke": "node scripts/archive-producer-smoke.mjs"
|
|
168
168
|
},
|
|
169
169
|
"optionalDependencies": {
|
|
170
|
-
"@openclaw/fs-safe-darwin-arm64": "0.
|
|
171
|
-
"@openclaw/fs-safe-darwin-x64": "0.
|
|
172
|
-
"@openclaw/fs-safe-linux-arm64-gnu": "0.
|
|
173
|
-
"@openclaw/fs-safe-linux-arm64-musl": "0.
|
|
174
|
-
"@openclaw/fs-safe-linux-x64-gnu": "0.
|
|
175
|
-
"@openclaw/fs-safe-linux-x64-musl": "0.
|
|
176
|
-
"@openclaw/fs-safe-win32-x64-msvc": "0.
|
|
170
|
+
"@openclaw/fs-safe-darwin-arm64": "0.12.0",
|
|
171
|
+
"@openclaw/fs-safe-darwin-x64": "0.12.0",
|
|
172
|
+
"@openclaw/fs-safe-linux-arm64-gnu": "0.12.0",
|
|
173
|
+
"@openclaw/fs-safe-linux-arm64-musl": "0.12.0",
|
|
174
|
+
"@openclaw/fs-safe-linux-x64-gnu": "0.12.0",
|
|
175
|
+
"@openclaw/fs-safe-linux-x64-musl": "0.12.0",
|
|
176
|
+
"@openclaw/fs-safe-win32-x64-msvc": "0.12.0",
|
|
177
177
|
"jszip": "^3.10.2"
|
|
178
178
|
},
|
|
179
179
|
"devDependencies": {
|