@openclaw/fs-safe 0.15.0 → 0.17.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 +86 -0
- package/README.md +36 -7
- package/dist/absolute-path.d.ts.map +1 -1
- package/dist/absolute-path.js +2 -8
- package/dist/advanced.d.ts +3 -0
- package/dist/advanced.d.ts.map +1 -1
- package/dist/advanced.js +3 -0
- package/dist/archive-deadline.d.ts.map +1 -1
- package/dist/archive-deadline.js +22 -23
- package/dist/archive-kind.d.ts +0 -1
- package/dist/archive-kind.d.ts.map +1 -1
- package/dist/archive-kind.js +5 -17
- package/dist/archive-merge.d.ts +1 -0
- package/dist/archive-merge.d.ts.map +1 -1
- package/dist/archive-merge.js +4 -4
- package/dist/archive-native.d.ts +1 -0
- package/dist/archive-native.d.ts.map +1 -1
- package/dist/archive-native.js +1 -0
- package/dist/archive-options.d.ts +2 -0
- package/dist/archive-options.d.ts.map +1 -1
- package/dist/archive-parser.wasm +0 -0
- package/dist/archive-read.d.ts.map +1 -1
- package/dist/archive-read.js +6 -7
- package/dist/archive-tar-stream.d.ts +3 -0
- package/dist/archive-tar-stream.d.ts.map +1 -1
- package/dist/archive-tar-stream.js +56 -37
- package/dist/archive-tar-wasm.d.ts +16 -4
- package/dist/archive-tar-wasm.d.ts.map +1 -1
- package/dist/archive-tar-wasm.js +134 -34
- package/dist/archive-zip-count.d.ts.map +1 -1
- package/dist/archive-zip-count.js +21 -1
- package/dist/archive-zip-directory.d.ts.map +1 -1
- package/dist/archive-zip-directory.js +23 -1
- package/dist/archive-zip-loader.d.ts +2 -0
- package/dist/archive-zip-loader.d.ts.map +1 -1
- package/dist/archive-zip-loader.js +7 -0
- package/dist/archive-zip-names.d.ts.map +1 -1
- package/dist/archive-zip-names.js +7 -2
- package/dist/archive.d.ts.map +1 -1
- package/dist/archive.js +14 -9
- package/dist/byte-view.d.ts +3 -0
- package/dist/byte-view.d.ts.map +1 -0
- package/dist/byte-view.js +13 -0
- package/dist/clone-metadata.d.ts +1 -0
- package/dist/clone-metadata.d.ts.map +1 -1
- package/dist/clone-metadata.js +6 -2
- package/dist/create-directory.d.ts +20 -0
- package/dist/create-directory.d.ts.map +1 -0
- package/dist/create-directory.js +130 -0
- package/dist/create-file-async.d.ts +7 -0
- package/dist/create-file-async.d.ts.map +1 -0
- package/dist/create-file-async.js +121 -0
- package/dist/create-file.d.ts +8 -0
- package/dist/create-file.d.ts.map +1 -0
- package/dist/create-file.js +190 -0
- package/dist/create-owned-file.d.ts +8 -0
- package/dist/create-owned-file.d.ts.map +1 -0
- package/dist/create-owned-file.js +16 -0
- package/dist/create.d.ts +4 -0
- package/dist/create.d.ts.map +1 -0
- package/dist/create.js +2 -0
- package/dist/creation-darwin.d.ts +6 -0
- package/dist/creation-darwin.d.ts.map +1 -0
- package/dist/creation-darwin.js +70 -0
- package/dist/creation-file-state.d.ts +19 -0
- package/dist/creation-file-state.d.ts.map +1 -0
- package/dist/creation-file-state.js +118 -0
- package/dist/creation-path.d.ts +21 -0
- package/dist/creation-path.d.ts.map +1 -0
- package/dist/creation-path.js +71 -0
- package/dist/creation-permissions.d.ts +19 -0
- package/dist/creation-permissions.d.ts.map +1 -0
- package/dist/creation-permissions.js +125 -0
- package/dist/directory-durability.d.ts +7 -7
- package/dist/directory-durability.d.ts.map +1 -1
- package/dist/directory-durability.js +22 -80
- package/dist/directory-guard.d.ts +3 -0
- package/dist/directory-guard.d.ts.map +1 -1
- package/dist/directory-mode-node.d.ts +2 -0
- package/dist/directory-mode-node.d.ts.map +1 -1
- package/dist/directory-mode-node.js +8 -0
- package/dist/directory-receipt.d.ts +24 -0
- package/dist/directory-receipt.d.ts.map +1 -0
- package/dist/directory-receipt.js +123 -0
- package/dist/file-cleanup.d.ts +20 -0
- package/dist/file-cleanup.d.ts.map +1 -0
- package/dist/file-cleanup.js +81 -0
- package/dist/file-contents.d.ts +6 -0
- package/dist/file-contents.d.ts.map +1 -0
- package/dist/file-contents.js +40 -0
- package/dist/file-hash.d.ts.map +1 -1
- package/dist/file-hash.js +16 -4
- package/dist/file-lock-sync-root-acquire.d.ts.map +1 -1
- package/dist/file-lock-sync-root-acquire.js +3 -0
- package/dist/file-lock-sync-root-held.d.ts +1 -2
- package/dist/file-lock-sync-root-held.d.ts.map +1 -1
- package/dist/file-lock-sync-root-held.js +7 -5
- package/dist/file-lock-sync-stale-admission.d.ts.map +1 -1
- package/dist/file-lock-sync-stale-admission.js +3 -0
- package/dist/file-lock-sync.d.ts.map +1 -1
- package/dist/file-lock-sync.js +8 -11
- package/dist/file-observation.d.ts +1 -1
- package/dist/file-observation.d.ts.map +1 -1
- package/dist/file-store-boundary.d.ts +2 -6
- package/dist/file-store-boundary.d.ts.map +1 -1
- package/dist/file-store-boundary.js +3 -9
- package/dist/file-store-sync-write.d.ts.map +1 -1
- package/dist/file-store-sync-write.js +2 -5
- package/dist/file-store.js +3 -3
- package/dist/guarded-mkdir.d.ts +1 -0
- package/dist/guarded-mkdir.d.ts.map +1 -1
- package/dist/guarded-mkdir.js +27 -19
- package/dist/install-path.d.ts.map +1 -1
- package/dist/install-path.js +2 -5
- package/dist/json-durable-queue-ownership.d.ts.map +1 -1
- package/dist/json-durable-queue-ownership.js +2 -6
- package/dist/json-durable-queue-paths.d.ts.map +1 -1
- package/dist/json-durable-queue-paths.js +2 -24
- package/dist/json-durable-queue.d.ts.map +1 -1
- package/dist/json-durable-queue.js +10 -9
- package/dist/json.d.ts.map +1 -1
- package/dist/json.js +32 -75
- package/dist/local-roots.d.ts.map +1 -1
- package/dist/local-roots.js +19 -21
- package/dist/move-path-cleanup.d.ts +5 -19
- package/dist/move-path-cleanup.d.ts.map +1 -1
- package/dist/move-path-cleanup.js +57 -21
- package/dist/move-path.d.ts.map +1 -1
- package/dist/move-path.js +63 -40
- package/dist/native-binding.d.ts +11 -1
- package/dist/native-binding.d.ts.map +1 -1
- package/dist/native-fallback-warning.d.ts +4 -0
- package/dist/native-fallback-warning.d.ts.map +1 -0
- package/dist/native-fallback-warning.js +11 -0
- package/dist/native-operations.d.ts +0 -2
- package/dist/native-operations.d.ts.map +1 -1
- package/dist/native-operations.js +0 -24
- package/dist/native-parent-admission.d.ts +2 -0
- package/dist/native-parent-admission.d.ts.map +1 -1
- package/dist/native-parent-admission.js +3 -2
- package/dist/native-pinned-write-windows.d.ts +1 -1
- package/dist/native-pinned-write-windows.d.ts.map +1 -1
- package/dist/native-pinned-write-windows.js +173 -28
- package/dist/native-pinned-write.d.ts.map +1 -1
- package/dist/native-pinned-write.js +19 -3
- package/dist/native-policy-parent-windows.d.ts.map +1 -1
- package/dist/native-policy-parent-windows.js +15 -6
- package/dist/native-staged-file.d.ts +5 -3
- package/dist/native-staged-file.d.ts.map +1 -1
- package/dist/native-staged-file.js +90 -40
- package/dist/native.js +2 -2
- package/dist/opened-realpath.d.ts.map +1 -1
- package/dist/opened-realpath.js +11 -2
- package/dist/owner-dacl.d.ts.map +1 -1
- package/dist/owner-dacl.js +10 -4
- package/dist/path.d.ts.map +1 -1
- package/dist/path.js +2 -1
- package/dist/permissions.d.ts.map +1 -1
- package/dist/permissions.js +3 -17
- package/dist/pinned-write-input.d.ts +4 -0
- package/dist/pinned-write-input.d.ts.map +1 -0
- package/dist/pinned-write-input.js +35 -0
- package/dist/pinned-write-mode.d.ts +5 -0
- package/dist/pinned-write-mode.d.ts.map +1 -0
- package/dist/pinned-write-mode.js +31 -0
- package/dist/pinned-write-staged.d.ts +6 -0
- package/dist/pinned-write-staged.d.ts.map +1 -0
- package/dist/pinned-write-staged.js +186 -0
- package/dist/pinned-write-types.d.ts +3 -0
- package/dist/pinned-write-types.d.ts.map +1 -1
- package/dist/pinned-write.d.ts.map +1 -1
- package/dist/pinned-write.js +41 -147
- package/dist/private-directory.d.ts.map +1 -1
- package/dist/private-directory.js +18 -4
- package/dist/private-producer-handoff-sync.d.ts +14 -0
- package/dist/private-producer-handoff-sync.d.ts.map +1 -0
- package/dist/private-producer-handoff-sync.js +114 -0
- package/dist/private-producer-handoff.d.ts +22 -4
- package/dist/private-producer-handoff.d.ts.map +1 -1
- package/dist/private-producer-handoff.js +140 -77
- package/dist/publish-copy-stage.d.ts +2 -1
- package/dist/publish-copy-stage.d.ts.map +1 -1
- package/dist/publish-copy-stage.js +16 -7
- package/dist/publish-file.d.ts +2 -2
- package/dist/publish-file.d.ts.map +1 -1
- package/dist/publish-file.js +58 -98
- package/dist/regular-file.d.ts.map +1 -1
- package/dist/regular-file.js +35 -44
- package/dist/replace-file-copy-fallback.d.ts.map +1 -1
- package/dist/replace-file-copy-fallback.js +28 -26
- package/dist/replace-file-copy-source.d.ts.map +1 -1
- package/dist/replace-file-copy-source.js +13 -22
- package/dist/replace-file-descriptor.d.ts.map +1 -1
- package/dist/replace-file-descriptor.js +10 -16
- package/dist/replace-file-temp-owner.d.ts +0 -7
- package/dist/replace-file-temp-owner.d.ts.map +1 -1
- package/dist/replace-file-temp-owner.js +9 -60
- package/dist/replace-file.d.ts.map +1 -1
- package/dist/replace-file.js +9 -13
- package/dist/root-create-input.d.ts +2 -1
- package/dist/root-create-input.d.ts.map +1 -1
- package/dist/root-create-input.js +13 -4
- package/dist/root-directory-creation.d.ts +3 -3
- package/dist/root-directory-creation.d.ts.map +1 -1
- package/dist/root-directory-creation.js +15 -3
- package/dist/root-directory-list.d.ts.map +1 -1
- package/dist/root-directory-list.js +20 -3
- package/dist/root-file-final-admission.d.ts +1 -1
- package/dist/root-file-final-admission.d.ts.map +1 -1
- package/dist/root-file-final-admission.js +5 -2
- package/dist/root-file.d.ts.map +1 -1
- package/dist/root-file.js +3 -2
- package/dist/root-impl.d.ts.map +1 -1
- package/dist/root-impl.js +78 -27
- package/dist/root-move-noreplace.d.ts +2 -0
- package/dist/root-move-noreplace.d.ts.map +1 -1
- package/dist/root-move-noreplace.js +22 -13
- package/dist/root-options.d.ts +12 -4
- package/dist/root-options.d.ts.map +1 -1
- package/dist/root-path-stat.d.ts.map +1 -1
- package/dist/root-path-stat.js +59 -7
- package/dist/root-read-admission.d.ts.map +1 -1
- package/dist/root-read-admission.js +7 -2
- package/dist/root-remove.d.ts.map +1 -1
- package/dist/root-remove.js +15 -1
- package/dist/root-write-publication.js +1 -1
- package/dist/secret-file.d.ts.map +1 -1
- package/dist/secret-file.js +1 -0
- package/dist/secure-file-windows.d.ts +6 -0
- package/dist/secure-file-windows.d.ts.map +1 -1
- package/dist/secure-file-windows.js +34 -117
- package/dist/secure-file.js +2 -2
- package/dist/sidecar-lock-acquire.d.ts.map +1 -1
- package/dist/sidecar-lock-acquire.js +4 -6
- package/dist/sidecar-lock-handle.d.ts +3 -0
- package/dist/sidecar-lock-handle.d.ts.map +1 -1
- package/dist/sidecar-lock-handle.js +6 -0
- package/dist/sidecar-lock-reclaim.d.ts +1 -1
- package/dist/sidecar-lock-reclaim.d.ts.map +1 -1
- package/dist/sidecar-lock-reclaim.js +11 -8
- package/dist/sidecar-lock-root.d.ts.map +1 -1
- package/dist/sidecar-lock-root.js +2 -1
- package/dist/sidecar-lock.d.ts.map +1 -1
- package/dist/sidecar-lock.js +3 -5
- package/dist/staged-directory.d.ts +2 -2
- package/dist/staged-directory.d.ts.map +1 -1
- package/dist/staged-directory.js +6 -6
- package/dist/staged-file-settlement.d.ts +17 -0
- package/dist/staged-file-settlement.d.ts.map +1 -0
- package/dist/staged-file-settlement.js +57 -0
- package/dist/strict-file-identity.d.ts +1 -1
- package/dist/strict-file-identity.d.ts.map +1 -1
- package/dist/strict-file-identity.js +9 -9
- package/dist/symlink-parents.d.ts.map +1 -1
- package/dist/symlink-parents.js +2 -27
- package/dist/temp-workspace-owner.js +4 -4
- package/dist/unicode-path.d.ts.map +1 -1
- package/dist/unicode-path.js +3 -0
- package/dist/walk.d.ts.map +1 -1
- package/dist/walk.js +4 -2
- package/dist/windows-owner.d.ts.map +1 -1
- package/dist/windows-owner.js +2 -1
- package/dist/windows-security-bridge.cs +336 -0
- package/dist/windows-security-bridge.ps1 +15 -0
- package/dist/windows-security-command.d.ts +26 -0
- package/dist/windows-security-command.d.ts.map +1 -0
- package/dist/windows-security-command.js +363 -0
- package/dist/windows-security-facts.d.ts +6 -0
- package/dist/windows-security-facts.d.ts.map +1 -0
- package/dist/windows-security-facts.js +108 -0
- package/dist/write-file-handle.d.ts +7 -0
- package/dist/write-file-handle.d.ts.map +1 -1
- package/dist/write-file-handle.js +23 -0
- package/dist/write-open-flags.d.ts.map +1 -1
- package/dist/write-open-flags.js +1 -8
- package/dist/write-queue.d.ts.map +1 -1
- package/dist/write-queue.js +1 -4
- package/docs/advanced.md +71 -2
- package/docs/archive.md +102 -39
- package/docs/atomic.md +29 -5
- package/docs/config.md +6 -2
- package/docs/contributing.md +48 -4
- package/docs/copy.md +2 -0
- package/docs/creation.md +132 -0
- package/docs/durability.md +59 -0
- package/docs/file-contents.md +68 -0
- package/docs/install.md +31 -7
- package/docs/json.md +5 -4
- package/docs/local-roots.md +2 -0
- package/docs/migrating-to-0.5.md +15 -6
- package/docs/migrating-to-0.6.md +9 -4
- package/docs/mutation-policy-proof.md +5 -3
- package/docs/native-helper.md +22 -9
- package/docs/native.md +47 -15
- package/docs/path.md +4 -4
- package/docs/permissions.md +37 -14
- package/docs/public-api.md +5 -0
- package/docs/quickstart.md +1 -1
- package/docs/reading.md +2 -2
- package/docs/regular-file.md +3 -0
- package/docs/root.md +43 -0
- package/docs/secret-file.md +11 -2
- package/docs/secure-file.md +9 -4
- package/docs/sidecar-lock.md +14 -5
- package/docs/staged-file.md +9 -3
- package/docs/store.md +3 -1
- package/docs/temp.md +4 -1
- package/docs/types.md +18 -2
- package/docs/walk.md +7 -0
- package/docs/writing.md +80 -7
- package/package.json +18 -15
package/docs/path.md
CHANGED
|
@@ -44,7 +44,7 @@ opened or mutated.
|
|
|
44
44
|
|
|
45
45
|
### `isPathInsideWithRealpath(rootDir, target, opts?)`
|
|
46
46
|
|
|
47
|
-
Synchronous.
|
|
47
|
+
Synchronous. First requires lexical containment with `isPathInside`, then resolves both inputs through `realpath` and checks containment again. A lexically outside path is rejected even if its resolved target is inside the root.
|
|
48
48
|
|
|
49
49
|
```ts
|
|
50
50
|
isPathInsideWithRealpath("/srv/uploads", "/srv/symlink-to-elsewhere"); // false
|
|
@@ -121,7 +121,7 @@ The check is intentionally not a normal consumer policy knob. Safe read APIs rej
|
|
|
121
121
|
|
|
122
122
|
### `isNotFoundPathError(err)`
|
|
123
123
|
|
|
124
|
-
`true` if the error
|
|
124
|
+
`true` if the error has code `ENOENT` (file or directory missing) or `ENOTDIR` (a path component is not a directory).
|
|
125
125
|
|
|
126
126
|
```ts
|
|
127
127
|
try {
|
|
@@ -207,8 +207,8 @@ import {
|
|
|
207
207
|
} from "@openclaw/fs-safe/advanced";
|
|
208
208
|
```
|
|
209
209
|
|
|
210
|
-
- `assertNoPathAliasEscape({
|
|
211
|
-
- `assertNoHardlinkedFinalPath({ filePath })` — async.
|
|
210
|
+
- `assertNoPathAliasEscape({ absolutePath, rootPath, boundaryLabel, policy? })` — async. Applies root path resolution and final hardlink checks. `policy` defaults to `PATH_ALIAS_POLICIES.strict`; `PATH_ALIAS_POLICIES.unlinkTarget` permits final symlink and hardlink aliases for unlink operations.
|
|
211
|
+
- `assertNoHardlinkedFinalPath({ filePath, root, boundaryLabel, allowFinalHardlinkForUnlink? })` — async. Rejects a regular file with `nlink > 1`; missing paths and nonregular resolved entries are ignored. Setting `allowFinalHardlinkForUnlink: true` skips this check for unlink operations.
|
|
212
212
|
|
|
213
213
|
Use these when writing a custom helper that wants the same guards `root()` uses but with different surrounding logic.
|
|
214
214
|
|
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. This remains a pathname reporting API with the fallbacks described below. `readSecureFile()`
|
|
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()` obtains descriptor-bound owner/DACL facts for the exact handle it reads, using native support or the packaged PowerShell/C# bridge in `auto` and `off` modes.
|
|
46
46
|
|
|
47
47
|
## Advanced Windows ACL helpers
|
|
48
48
|
|
|
@@ -69,9 +69,10 @@ resolveWindowsUserPrincipal(env);
|
|
|
69
69
|
```
|
|
70
70
|
|
|
71
71
|
The fallback Windows inspector reads the owner and DACL together through one
|
|
72
|
-
built-in Windows PowerShell/.NET query.
|
|
73
|
-
|
|
74
|
-
|
|
72
|
+
built-in Windows PowerShell/.NET query. The query addresses its JSON command by
|
|
73
|
+
module name and limits module discovery to PowerShell's bundled system modules.
|
|
74
|
+
It returns canonical SIDs and numeric access masks, so Unicode paths and account
|
|
75
|
+
names do not pass through lossy console display text. `inspectWindowsAcl()` uses native descriptor facts for
|
|
75
76
|
complete local ACLs with nonzero inherited ACEs (or empty/null DACLs) when the
|
|
76
77
|
optional Windows binding is available. It applies
|
|
77
78
|
the same classifier to native facts and the fallback query, returning canonical
|
|
@@ -171,10 +172,12 @@ Object-specific and other ACE layouts are not guessed: they are omitted,
|
|
|
171
172
|
`complete` becomes false, and their numeric types appear in
|
|
172
173
|
`unsupportedAceTypes`, allowing a security-sensitive caller to fail closed.
|
|
173
174
|
Non-Windows systems return `{ status: "unsupported-platform", platform }`.
|
|
174
|
-
Windows
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
175
|
+
Windows prefers the native binding. In native `auto` or `off` mode, a missing
|
|
176
|
+
binding or capability uses the packaged PowerShell/C# bridge with
|
|
177
|
+
the same raw ACE projection. Native `require` rejects either absence with
|
|
178
|
+
`FsSafeError("helper-unavailable")` and starts no command. An available native
|
|
179
|
+
query's failure is terminal. The existing coarse `inspectPathPermissions()` API
|
|
180
|
+
still owns its compatibility fallback and trust classification.
|
|
178
181
|
|
|
179
182
|
## Private directories
|
|
180
183
|
|
|
@@ -188,10 +191,11 @@ await createPrivateDirectory(sqliteDirectory);
|
|
|
188
191
|
await openSqlite(path.join(sqliteDirectory, "sessions.sqlite"));
|
|
189
192
|
```
|
|
190
193
|
|
|
191
|
-
On Windows
|
|
194
|
+
On Windows, this creates the directory and applies a
|
|
192
195
|
protected owner + LocalSystem + Administrators full-control DACL directly with
|
|
193
|
-
an atomic security descriptor
|
|
194
|
-
|
|
196
|
+
an atomic security descriptor. The native route launches no command. When its
|
|
197
|
+
binding or capability is unavailable, native `auto` and `off` modes use the
|
|
198
|
+
packaged PowerShell/C# bridge. Both routes retain the parent and exact created-directory handles
|
|
195
199
|
through ACL and final pathname validation. If validation fails, it attempts only
|
|
196
200
|
nonrecursive deletion through the created handle, preserving any pathname
|
|
197
201
|
replacement. If cleanup also fails, the error retains the original failure and
|
|
@@ -216,13 +220,32 @@ also rejects explicit `.` and `..` components, including spellings such as
|
|
|
216
220
|
`.\private` and `parent\..\private`, as a compatibility restriction. Simple
|
|
217
221
|
relative names without these components remain supported.
|
|
218
222
|
|
|
219
|
-
This API is Windows-only
|
|
220
|
-
|
|
221
|
-
|
|
223
|
+
This API is Windows-only; it fails closed with `FsSafeError("helper-unavailable")`
|
|
224
|
+
on other platforms. Native `require` also fails if the binding or capability is
|
|
225
|
+
missing and never starts a command. An available native operation's failure is
|
|
226
|
+
terminal. POSIX callers should create private
|
|
222
227
|
directories through their existing trusted-root creation policy rather than a
|
|
223
228
|
pathname-only compatibility shim. Existing Windows permission inspection still
|
|
224
229
|
retains its structured .NET compatibility fallback.
|
|
225
230
|
|
|
231
|
+
The raw owner/DACL and private-directory fallbacks each emit one path-free
|
|
232
|
+
`FS_SAFE_NATIVE_FALLBACK` warning per process. PowerShell startup and C#
|
|
233
|
+
compilation add overhead to each call; install the native package for frequent
|
|
234
|
+
operations. These routes run the package's readable, fixed scripts under normal
|
|
235
|
+
system PowerShell policy; see the [Windows security fallback prerequisites](install.md#windows-security-fallback).
|
|
236
|
+
If command support is unavailable, disallowed, or fails, the operation rejects.
|
|
237
|
+
Private-directory creation never falls back to inherited permissions.
|
|
238
|
+
The asynchronous creation command has a 30-second deadline. After a timeout or
|
|
239
|
+
transport failure, fs-safe requests termination and waits at most one further
|
|
240
|
+
second before rejecting and closing its output pipes. The error distinguishes
|
|
241
|
+
observed process exit from an unconfirmed termination attempt. If the OS refuses
|
|
242
|
+
termination, the command can still create the directory after rejection. An
|
|
243
|
+
already-created object retains its protected DACL, but pathname validation and
|
|
244
|
+
owned-handle cleanup may not finish. An error therefore does not prove the
|
|
245
|
+
pathname is absent; a retry can report `EEXIST`. Before retrying or using the
|
|
246
|
+
pathname, establish that the earlier operation stopped and verify any existing
|
|
247
|
+
directory's security. The library does not attempt pathname-based cleanup.
|
|
248
|
+
|
|
226
249
|
Use `createIcaclsResetCommand()` when you need a structured command and argv pair. Use `formatIcaclsResetCommand()` when you only need a remediation string for a user-facing message.
|
|
227
250
|
|
|
228
251
|
## Types
|
package/docs/public-api.md
CHANGED
|
@@ -20,6 +20,9 @@ The root subpath also exports `openLocalFileSafely`, `readLocalFileSafely`, and
|
|
|
20
20
|
`resolveOpenedFileRealPathForHandle` for trusted absolute-file composition.
|
|
21
21
|
They do not create a root boundary around arbitrary caller input; prefer
|
|
22
22
|
`root()` for untrusted paths.
|
|
23
|
+
The handle resolver verifies exact descriptor and pathname identities, with one
|
|
24
|
+
bounded retry for unknown Windows observations. It borrows the handle without
|
|
25
|
+
reading, reopening, closing it, or changing its cursor.
|
|
23
26
|
|
|
24
27
|
The error helpers are `categorizeFsSafeError` and `FsSafeErrorDetails`. The
|
|
25
28
|
deprecated native-configuration bridge retains the `FsSafePythonConfig` type.
|
|
@@ -135,6 +138,8 @@ The durability surface also exports the synchronous strict
|
|
|
135
138
|
`Sha256FileSyncInput`, `Sha256FileOptions`, and `Sha256FileResult`.
|
|
136
139
|
`sha256FileSync()` provides synchronous pathname or borrowed-descriptor hashing
|
|
137
140
|
with the same byte-budget and digest-result contracts as `sha256File()`.
|
|
141
|
+
`DirectoryReceipt<T>` accepts `Stats` or `BigIntStats` input metadata; its default
|
|
142
|
+
type argument and returned durability receipts remain numeric `Stats`.
|
|
138
143
|
|
|
139
144
|
## Archives
|
|
140
145
|
|
package/docs/quickstart.md
CHANGED
|
@@ -60,7 +60,7 @@ await fs.move("notes/today.txt", "notes/archive/today.txt", { overwrite: true })
|
|
|
60
60
|
await fs.remove("notes/archive/today.txt");
|
|
61
61
|
```
|
|
62
62
|
|
|
63
|
-
`move()` defaults to no clobber. That mode requires the native helper so a concurrent target cannot be replaced between an absence check and the rename; without it, the call fails with `helper-unavailable`. Pass `{ overwrite: true }` when replacing the target is intentional. `remove()`
|
|
63
|
+
`move()` defaults to no clobber. That mode requires the native helper so a concurrent target cannot be replaced between an absence check and the rename; without it, the call fails with `helper-unavailable`. Pass `{ overwrite: true }` when replacing the target is intentional. `remove()` removes files and empty directories by default. To remove a non-empty directory, pass `{ recursive: true }`; use `maxEntries`, `maxDepth`, and `signal` to bound the work. See [`root()`](root.md) for removal ordering, limits, and partial-removal semantics.
|
|
64
64
|
|
|
65
65
|
## 5. Inspect
|
|
66
66
|
|
package/docs/reading.md
CHANGED
|
@@ -28,7 +28,7 @@ Regardless of shape, every read goes through the same boundary checks:
|
|
|
28
28
|
4. Reject `..` traversal and absolute spellings when they resolve outside the root. In-root absolute spellings remain accepted; `readAbsolute` makes that intent explicit.
|
|
29
29
|
5. Open with `O_NOFOLLOW` where available. Any remaining symlink in the path triggers `symlink` unless the call's `symlinks` policy is `follow-within-root`.
|
|
30
30
|
6. Compare the pre-open bigint path identity with the open fd, then perform one best-effort final admission: check the captured root identity, compare the policy-aware pathname with the fd, freshly canonicalize and re-admit that target inside the captured root, compare its exact bigint identity without following a final symlink with the fd, and check the root again. Both final pathname observations run even when their spellings match. An observed swap triggers `path-mismatch` or `outside-workspace`; a missing final path triggers `not-found`.
|
|
31
|
-
7. If `hardlinks: "reject"`, refuse files with `nlink > 1` (`hardlink`).
|
|
31
|
+
7. If `hardlinks: "reject"`, refuse files with `nlink > 1` (`hardlink`), including links introduced before either fresh final pathname observation. Root-file helpers apply the same final check when `rejectHardlinks` is enabled; directory admission is unaffected.
|
|
32
32
|
8. If `maxBytes` is set, refuse reads larger than the cap (`too-large`).
|
|
33
33
|
|
|
34
34
|
The final fence closes a rejected descriptor before any Root read consumes bytes or
|
|
@@ -100,7 +100,7 @@ type RootReadOptions = {
|
|
|
100
100
|
hardlinks?: "reject" | "allow"; // override defaults.hardlinks
|
|
101
101
|
maxBytes?: number; // refuse reads larger than this many bytes
|
|
102
102
|
nonBlockingRead?: boolean; // compatibility hint; safe opens are already nonblocking where supported
|
|
103
|
-
symlinks?: "reject" | "follow-within-root"; // override defaults.symlinks
|
|
103
|
+
symlinks?: "reject" | "follow-within-root" | "follow-parents-within-root"; // override defaults.symlinks
|
|
104
104
|
};
|
|
105
105
|
```
|
|
106
106
|
|
package/docs/regular-file.md
CHANGED
|
@@ -110,6 +110,9 @@ descriptor, and current pathname identities remain exact bigints through the
|
|
|
110
110
|
append boundary; rounded-equal replacements and persistent unknown Windows
|
|
111
111
|
identities reject before chmod or writing bytes. With
|
|
112
112
|
`rejectSymlinkParents: true`, it also rejects symlinked ancestor directories.
|
|
113
|
+
Supported option values are captured once before filesystem work, so replacing
|
|
114
|
+
the content, encoding, mode or cap cannot change an in-flight append. Byte-array
|
|
115
|
+
contents remain caller-owned; leave them unchanged until the append completes.
|
|
113
116
|
|
|
114
117
|
On POSIX, `O_NONBLOCK` prevents a no-reader FIFO substituted before open from
|
|
115
118
|
stalling admission. A confirmed non-regular target is refused before chmod or
|
package/docs/root.md
CHANGED
|
@@ -130,8 +130,32 @@ fs.mkdir(rel, options?) // mkdir -p (creates missing parents)
|
|
|
130
130
|
fs.ensureRoot(options?) // accepts "" / "." as the root itself
|
|
131
131
|
```
|
|
132
132
|
|
|
133
|
+
`mkdir`, `ensureRoot`, `create`, and `createJson` accept `private: true`.
|
|
134
|
+
Missing directories are created with private permissions, and an existing
|
|
135
|
+
requested directory must already be private. Existing ancestors are not
|
|
136
|
+
chmodded or assigned new ACLs. Private files use owner-only POSIX permissions
|
|
137
|
+
or a protected Windows DACL granting access to the current user, System, and
|
|
138
|
+
Administrators. On macOS, private directories and files must also have no ACL;
|
|
139
|
+
creation rejects relevant inheritable parent ACLs, while noninheriting parent
|
|
140
|
+
ACLs remain allowed. A native helper with `inspectDarwinAcl` is required. Native
|
|
141
|
+
`off`, a missing helper, or an older helper without that capability rejects with
|
|
142
|
+
`helper-unavailable` before creating parents or stages. See [creation](creation.md)
|
|
143
|
+
for platform support, synchronous leaf creation, and failure handling.
|
|
144
|
+
|
|
145
|
+
```ts
|
|
146
|
+
await fs.mkdir("private-data", { private: true });
|
|
147
|
+
await fs.create("private-data/credential", "synthetic credential", { private: true });
|
|
148
|
+
```
|
|
149
|
+
|
|
133
150
|
`write`, `create`, `append`, `writeJson`, and `createJson` accept `mode?: number`; use `0o600` for credentials and other private state. `writeJson` also accepts the same options as `JSON.stringify` plus `trailingNewline?: boolean` (defaults `true` so the file ends in `\n`).
|
|
134
151
|
|
|
152
|
+
Buffered `create` and `createJson` also accept `atomic?: boolean`. With `true`,
|
|
153
|
+
complete content is staged before exclusive publication even in native-off mode;
|
|
154
|
+
the fallback requires hardlinks. Omitted or `false` keeps the existing buffered
|
|
155
|
+
publication behavior. Streamed creates always stage complete content. The flag
|
|
156
|
+
does not change `durable` or promise stronger containment or crash durability.
|
|
157
|
+
See [atomic creation and settlement](writing.md#atomic-buffered-creation).
|
|
158
|
+
|
|
135
159
|
`create` also accepts `AsyncIterable<Uint8Array>` with `RootCreateStreamOptions`:
|
|
136
160
|
the same path, authority, mode, and durability options, plus `maxBytes` and
|
|
137
161
|
`signal`, without `encoding` or `renameIdentity`. It consumes one chunk at a
|
|
@@ -143,6 +167,8 @@ cleanup, and filesystem requirements.
|
|
|
143
167
|
`append` accepts `prependNewlineIfNeeded: true` to separate text from existing
|
|
144
168
|
content when neither side supplies a newline. String data uses its `encoding`
|
|
145
169
|
for the newline check, including UTF-16LE; Buffer data uses a single LF byte.
|
|
170
|
+
Empty strings and Buffers add no separator; an empty append still creates a
|
|
171
|
+
missing file.
|
|
146
172
|
|
|
147
173
|
These five methods and `copyIn` also accept `durable?: boolean`: the per-call value overrides
|
|
148
174
|
`Root.defaults.durable`, which defaults to `true` when omitted. An explicitly
|
|
@@ -152,6 +178,11 @@ and parent-directory fsync calls. Use it only for reconstructible data: a crash
|
|
|
152
178
|
may lose the write or leave the previous file. See [Writing](writing.md#write-options)
|
|
153
179
|
for platform details.
|
|
154
180
|
|
|
181
|
+
`create` and `createJson` additionally accept `durable: "file"` to require file
|
|
182
|
+
synchronization, including propagating `EPERM`. Parent-directory synchronization
|
|
183
|
+
retains its existing best-effort behavior. This option applies to buffered and
|
|
184
|
+
streamed creation and does not select a publication strategy.
|
|
185
|
+
|
|
155
186
|
`copyIn` accepts a `RootCopySource`: a trusted absolute source path or a file
|
|
156
187
|
within another Root. The guarded form supplies `root` with only its `open` and
|
|
157
188
|
`stat` read capabilities, plus `relativePath`:
|
|
@@ -279,6 +310,13 @@ from that dispatch. A thrown value rejects the operation unchanged; an async
|
|
|
279
310
|
or thenable-returning callback rejects with `TypeError` before that mutation.
|
|
280
311
|
Synchronous return values are ignored. Callbacks can run multiple times and
|
|
281
312
|
must inspect current authority each time.
|
|
313
|
+
Directory creation rechecks the retained parent after the callback and before
|
|
314
|
+
submitting mkdir, so a replacement is rejected before creating that component.
|
|
315
|
+
Overwrite moves recheck the retained root, parents, source identity and both
|
|
316
|
+
routes after the callback, including destination parents that were missing
|
|
317
|
+
during preparation. Removals recheck cancellation, retained ancestry and exact
|
|
318
|
+
leaf identity before dispatch; `force` tolerates a missing leaf, not replaced
|
|
319
|
+
ancestry. Removing an admitted hardlink still leaves its other names intact.
|
|
282
320
|
|
|
283
321
|
Already dispatched I/O cannot be revoked. Identity-checked cleanup, final
|
|
284
322
|
permissions, and durability finish under the existing operation owner even
|
|
@@ -300,6 +338,11 @@ the caller, which must check authority before its own later writes.
|
|
|
300
338
|
|
|
301
339
|
All mutation methods accept `denyMutations?: { paths?: string[]; prefixes?: string[] }`. Entries must be absolute paths. `paths` blocks those exact paths; `prefixes` blocks those paths and their descendants. fs-safe preserves path strings exactly and canonicalizes through existing ancestors before comparing, so a symlinked ancestor to a denied location is still denied. Denied mutations throw `FsSafeError` with code `denied-path`. Use this for caller-specific sensitive paths, not as a replacement for the root boundary, symlink, or hardlink checks.
|
|
302
340
|
|
|
341
|
+
`move()` snapshots its merged default and per-call mutation policy before
|
|
342
|
+
asynchronous preparation. Later changes to the original policy objects or arrays
|
|
343
|
+
apply to subsequent calls. Use `assertBeforeMutation` for live revocation of an
|
|
344
|
+
in-flight move.
|
|
345
|
+
|
|
303
346
|
For writes, creates, streams, and copies, parent creation admits the prospective
|
|
304
347
|
file and each missing directory before creating that directory, including on the
|
|
305
348
|
Windows native route. An exact deny on an existing parent does not prevent using
|
package/docs/secret-file.md
CHANGED
|
@@ -133,6 +133,15 @@ startWebhookVerifier(signingKey);
|
|
|
133
133
|
|
|
134
134
|
Async. Creates the parent directory at `dirMode` (default `0o700`) if missing, writes content to a sibling temp file, finalizes `mode` (default `0o600`) through an owned descriptor after content writes, and atomically renames over the destination. Publication verification checks the final file identity and mode.
|
|
135
135
|
|
|
136
|
+
On POSIX, both native and JavaScript writers verify actual `0o600` permission
|
|
137
|
+
bits through the retained descriptor before writing content. A filesystem that
|
|
138
|
+
reports successful chmod without enforcing those bits fails with
|
|
139
|
+
`insecure-permissions` before any payload is written, including when an explicit
|
|
140
|
+
`dirMode` permits other users to traverse the parent. The requested final `mode`
|
|
141
|
+
is still applied after content writes, including restrictive and special-bit
|
|
142
|
+
overrides. This mode-bit check does not require native ACL inspection; JavaScript
|
|
143
|
+
secret writes remain available on macOS.
|
|
144
|
+
|
|
136
145
|
Concurrent writes to distinct leaves may share creation of a missing parent.
|
|
137
146
|
After a parent-creation race, the helper re-inspects the entry and requires a
|
|
138
147
|
non-symlink directory, then revalidates root/parent guards, containment, and
|
|
@@ -270,9 +279,9 @@ await withTimeout(
|
|
|
270
279
|
|
|
271
280
|
## Threat model notes
|
|
272
281
|
|
|
273
|
-
-
|
|
282
|
+
- On POSIX, the default `0600` file and `0700` directory modes restrict group and other access. They do not protect against processes with the same UID, root, attackers who can read process memory, or access granted by additional ACL entries.
|
|
274
283
|
- Validation failures are tripwires, not authorization. Investigate before clearing a rejected credential file.
|
|
275
|
-
-
|
|
284
|
+
- On POSIX, a file that still reports a mode other than `0600` after initialization is rejected with `insecure-permissions` before payload is written. Matching mode reports alone cannot prove that an arbitrary filesystem actually enforces those permissions.
|
|
276
285
|
|
|
277
286
|
## See also
|
|
278
287
|
|
package/docs/secure-file.md
CHANGED
|
@@ -27,9 +27,13 @@ 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 queries owner, DACL, and locality from the same open descriptor that supplies the bytes.
|
|
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. Both native and system-command queries return 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
|
-
Windows secure reads
|
|
32
|
+
Windows secure reads prefer the matching optional native package. In native `auto` or `off` mode, a missing binding or descriptor-inspection capability uses a packaged, readable PowerShell script and adjacent C# source to inspect the borrowed file handle. This route requires the [Windows security fallback prerequisites](install.md#windows-security-fallback), including permission to run the scripts under normal system policy. The command does not read file contents or reopen the pathname. Successful inspection waits for the child to exit and its output pipes to close. This emits a path-free `FS_SAFE_NATIVE_FALLBACK` warning once per process for secure reads and adds PowerShell startup and compilation overhead to each inspection.
|
|
33
|
+
|
|
34
|
+
Descriptor commands have a 30-second deadline. After a timeout or transport failure, fs-safe requests termination, waits at most one further second, and then rejects even if process exit or pipe closure remains unconfirmed. It closes its own output pipes and reports the observed exit separately from the termination attempt in the error cause. If the OS refuses termination, the child may retain its independently inherited Windows handle; closing the caller's descriptor cannot retarget that handle. No file bytes are returned from a failed inspection.
|
|
35
|
+
|
|
36
|
+
Native `require` still rejects a missing binding or capability with `permission-unverified`, without starting a command. An available native helper's failure is terminal. On either route, fd-to-handle conversion failure, denied `READ_CONTROL`, a remote handle, incomplete descriptor, unsupported ACE form, or unavailable command support rejects with `permission-unverified` before content is read. A malformed or different handle identity rejects with `path-mismatch`. The standalone reporting APIs in [`permissions`](permissions.md) retain their documented pathname fallbacks. `permissions.allowInsecure` remains the explicit escape hatch and bypasses the ACL query.
|
|
33
37
|
|
|
34
38
|
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.
|
|
35
39
|
|
|
@@ -92,13 +96,14 @@ On an actual Windows process with effective `platform: "win32"`, `inject.env` an
|
|
|
92
96
|
| `timeout` | `timeoutMs` elapsed while reading. |
|
|
93
97
|
|
|
94
98
|
Windows descriptor-inspection failures are operational `permission-unverified`
|
|
95
|
-
errors and refuse the read. The original native exception is retained as
|
|
99
|
+
errors and refuse the read. The original native or descriptor-command exception is retained as
|
|
96
100
|
`cause`; treat causes as restricted local diagnostic data. No pathname or ACL
|
|
97
101
|
content is copied into the display message. Test adapters that simulate Windows
|
|
98
102
|
on another operating system retain the standalone pathname inspector's
|
|
99
103
|
structured command diagnostics (`ownerError`, `command`, `durationMs`,
|
|
100
104
|
`timedOut`, `exitCode`, `signal`, and bounded escaped `stderr`). Actual Windows
|
|
101
|
-
secure reads
|
|
105
|
+
secure reads never invoke the injected pathname inspector: their optional
|
|
106
|
+
command route inspects the borrowed descriptor instead. No retries are performed.
|
|
102
107
|
|
|
103
108
|
## See also
|
|
104
109
|
|
package/docs/sidecar-lock.md
CHANGED
|
@@ -55,6 +55,7 @@ invocation is not mutation authority. A failing final parser keeps its error
|
|
|
55
55
|
even if guard ownership has also changed.
|
|
56
56
|
`manager.reset()` invalidates admission bookkeeping but preserves a pending
|
|
57
57
|
Root guard; let its original attempt settle before retrying that guarded path.
|
|
58
|
+
It stops compromise monitoring for forgotten holders, including callbacks from checks already in flight.
|
|
58
59
|
|
|
59
60
|
Always release locks in a `finally` block. Application-managed graceful shutdown can await `release()` or `manager.drain()` before terminating. Explicit `process.exit()`, uncaught failures, crashes, default signal handling, and fatal termination (including `SIGKILL`) do not reliably run asynchronous Root cleanup and may leave sidecars. Recover only after an application-owned liveness policy proves the holder cannot still be writing.
|
|
60
61
|
|
|
@@ -64,7 +65,7 @@ Each new sidecar also carries an internal random ownership token encoded as JSON
|
|
|
64
65
|
|
|
65
66
|
The raw sidecar bytes are not a canonical JSON representation: tools that trim or rewrite the trailing whitespace invalidate the ownership token, so release leaves the changed sidecar in place and fails closed. The token distinguishes cooperating acquisitions; it is not a secret and does not make pathname compare-and-remove atomic against a hostile process that can replace files outside the lock protocol.
|
|
66
67
|
|
|
67
|
-
`release()` propagates an I/O failure that prevents deletion of an unchanged, owned sidecar; it never reports successful cleanup while leaving that lock behind. The handle and manager retain the exact cleanup receipt after a failure, so the same handle can retry `release()` and `manager.drain()` can retry retained cleanup. A changed sidecar remains an ownership mismatch rather than a deletion failure and is left untouched. If both a `withFileLock()` callback and release fail, the release error is the primary `SuppressedError.error` and the callback failure remains available as `SuppressedError.suppressed`. Failed asynchronous acquisition cleanup uses the same shape, with the cleanup error primary and the acquisition failure suppressed. On Node runtimes without the global `SuppressedError` constructor, fs-safe returns the equivalent `Error` shape with the same name and properties.
|
|
68
|
+
`release()` propagates an I/O failure that prevents deletion of an unchanged, owned sidecar; it never reports successful cleanup while leaving that lock behind. The handle and manager retain the exact cleanup receipt after a failure, so the same handle can retry `release()` and `manager.drain()` can retry retained cleanup. A changed sidecar remains an ownership mismatch rather than a deletion failure and is left untouched. If both a `withFileLock()` or `withFileLockSync()` callback and release fail, the release error is the primary `SuppressedError.error` and the callback failure remains available as `SuppressedError.suppressed`. Failed asynchronous acquisition cleanup uses the same shape, with the cleanup error primary and the acquisition failure suppressed. On Node runtimes without the global `SuppressedError` constructor, fs-safe returns the equivalent `Error` shape with the same name and properties.
|
|
68
69
|
|
|
69
70
|
## API
|
|
70
71
|
|
|
@@ -295,10 +296,11 @@ Discarding an acquisition observation is not proof that the pathname is absent:
|
|
|
295
296
|
another owner may already have created the next record. Every discarded
|
|
296
297
|
observation consumes the normal retry/deadline budget and requires fresh
|
|
297
298
|
exclusive creation. It supplies no release, reclaim, or held-lock authority.
|
|
298
|
-
If that successor disappears during the recovery metadata probe,
|
|
299
|
-
may discard the probe only with an operation-local receipt for an
|
|
300
|
-
regular file with one link
|
|
301
|
-
|
|
299
|
+
If that successor disappears or is replaced during the recovery metadata probe,
|
|
300
|
+
the waiter may discard the probe only with an operation-local receipt for an
|
|
301
|
+
admitted regular file with one link. Replacement also requires a single exact
|
|
302
|
+
observation of a different regular file with one link. Current Root and canonical
|
|
303
|
+
ancestor checks must still pass. A generic metadata error does not permit this retry, and public
|
|
302
304
|
`Root.stat()` still rejects a file that changes during observation.
|
|
303
305
|
Generic `Root.open()` and held-owner/reclaim reads still reject failed opens.
|
|
304
306
|
Moving an already-matched pinned descriptor without unlinking it, unknown or
|
|
@@ -335,6 +337,12 @@ sidecar no longer matches or after a verification I/O failure. This is
|
|
|
335
337
|
detection, not revocation of work already in progress. Asynchronous checks are
|
|
336
338
|
serialized, so a slow verification never overlaps the next timer tick.
|
|
337
339
|
|
|
340
|
+
Ownership-only checks compare serialized bytes, tokens, and file identities
|
|
341
|
+
without decoding an unused default JSON payload. Stale-policy reads still
|
|
342
|
+
decode the payload. Explicit `parsePayload` callbacks keep their existing
|
|
343
|
+
verification and asynchronous-cleanup calls, receivers, and errors;
|
|
344
|
+
synchronous release continues without invoking a custom parser.
|
|
345
|
+
|
|
338
346
|
The compromise-check interval is validated before payload evaluation or
|
|
339
347
|
filesystem acquisition. Omit it or pass `0` to disable monitoring; enabled
|
|
340
348
|
intervals must be finite and between 1 and 2,147,483,647 milliseconds. Values
|
|
@@ -437,6 +445,7 @@ error.
|
|
|
437
445
|
The sync payload, reclaim, and parsing callbacks must also be synchronous. This
|
|
438
446
|
shape is appropriate for a short boot migration; it is a poor fit for a server
|
|
439
447
|
request because retry backoff uses a blocking wait.
|
|
448
|
+
Synchronous `shouldReclaim` and `shouldRemoveStaleLock` reject Promise or thenable results with `TypeError` before deleting the observed sidecar; an asynchronous result is never approval.
|
|
440
449
|
|
|
441
450
|
If termination skips the relevant cleanup handler or cleanup fails, the sidecar remains. In particular, `process.exit()` skips asynchronous Root cleanup; await explicit release or drain during application-managed graceful shutdown. Once `staleMs` elapses (or your `shouldReclaim` returns true), acquisition fails closed by default instead of deleting by path.
|
|
442
451
|
|
package/docs/staged-file.md
CHANGED
|
@@ -49,7 +49,7 @@ remain with the caller.
|
|
|
49
49
|
|
|
50
50
|
```ts
|
|
51
51
|
function stageFileInDirectory(options: {
|
|
52
|
-
directory: string | DirectoryReceipt
|
|
52
|
+
directory: string | DirectoryReceipt<Stats | BigIntStats>;
|
|
53
53
|
content: string | Uint8Array;
|
|
54
54
|
mode?: number;
|
|
55
55
|
}): Promise<StagedFile>;
|
|
@@ -65,6 +65,11 @@ interface StagedFile extends AsyncDisposable {
|
|
|
65
65
|
Strings are UTF-8. `mode` is the requested **published** mode and defaults to
|
|
66
66
|
`0600`; exact final modes, including `000`, are supported. The unpublished file
|
|
67
67
|
stays at `0600` throughout preparation and any awaited application checks.
|
|
68
|
+
The retained descriptor's actual mode is checked before writing payload bytes,
|
|
69
|
+
by `assertCurrent()`, and before publication; a successful but ineffective
|
|
70
|
+
`chmod` fails with `insecure-permissions`. Final mode verification after
|
|
71
|
+
publication can fail with a `published` receipt while preserving the completed
|
|
72
|
+
file. These are POSIX mode checks, not ACL or ownership admission.
|
|
68
73
|
After rename succeeds and the published entry passes identity validation, the
|
|
69
74
|
owner applies the requested mode through its retained file descriptor. Content
|
|
70
75
|
was synchronized during preparation; publication always synchronizes the parent.
|
|
@@ -76,8 +81,9 @@ Creation uses an exclusive, no-follow, close-on-exec open of a generated direct
|
|
|
76
81
|
child name. Writes use that descriptor. Inspection uses non-following metadata
|
|
77
82
|
operations, never a potentially blocking reopen of the leaf.
|
|
78
83
|
|
|
79
|
-
A supplied directory receipt must still match at admission.
|
|
80
|
-
|
|
84
|
+
A supplied directory receipt must still match at admission. Caller receipts can
|
|
85
|
+
carry numeric `Stats` or exact `BigIntStats`; untracked numeric identities must
|
|
86
|
+
be exactly representable. Ambiguous identity fails closed.
|
|
81
87
|
Returned receipts are frozen descriptive snapshots, not mutable authority.
|
|
82
88
|
Changing a supplied receipt after admission cannot retarget the lifecycle.
|
|
83
89
|
|
package/docs/store.md
CHANGED
|
@@ -89,7 +89,9 @@ claim before publication. If another consumer acknowledged, quarantined, or
|
|
|
89
89
|
replaced that claim, the migration rejects with `FsSafeError("path-mismatch")`
|
|
90
90
|
and leaves the newer generation or failed evidence intact. A stale migration
|
|
91
91
|
rejects both single and batch loads; ordinary callback failures retain their
|
|
92
|
-
existing single-load rejection and batch-skip behavior.
|
|
92
|
+
existing single-load rejection and batch-skip behavior. A caller or migration
|
|
93
|
+
error with code `ENOENT` is still a failure, not a missing queue entry; only a
|
|
94
|
+
claim that is absent or disappears before reading returns `null` from a single load.
|
|
93
95
|
|
|
94
96
|
On Windows, migration releases its read pin once at this publication boundary
|
|
95
97
|
because an open target can block replacement. It rechecks the exact pathname
|
package/docs/temp.md
CHANGED
|
@@ -155,12 +155,15 @@ uses guarded pathname-recursive removal. This fallback never recursively
|
|
|
155
155
|
removes the public workspace name, but it is not atomic conditional deletion: a
|
|
156
156
|
same-privilege peer that discovers and replaces the private quarantine after
|
|
157
157
|
verification can still redirect the final pathname removal.
|
|
158
|
+
If admitting a cleanup parent fails and closing its descriptor also fails,
|
|
159
|
+
creation rejects with both failures in an `AggregateError`. This does not select
|
|
160
|
+
compatible fallback or retry the indeterminate descriptor close.
|
|
158
161
|
|
|
159
162
|
Set `cleanupSafety: "require-bounded"` when that concurrent attacker is in scope.
|
|
160
163
|
Creation then requires native no-replace directory rename, native owned-tree
|
|
161
164
|
removal, and a readable retained parent descriptor **before** child creation.
|
|
162
165
|
On POSIX, the final requested `dirMode` must also include owner read
|
|
163
|
-
and search (`(dirMode & 0o500) === 0o500`).
|
|
166
|
+
and search (`(dirMode & 0o500) === 0o500`). An unavailable capability throws
|
|
164
167
|
`FsSafeError("helper-unavailable")` without creating a child or calling a scoped
|
|
165
168
|
callback. The child descriptor is opened
|
|
166
169
|
while the new directory still has its private creation mode, before an explicit
|
package/docs/types.md
CHANGED
|
@@ -128,6 +128,8 @@ type RootOptions = {
|
|
|
128
128
|
## `RootReadOptions` / `RootWriteOptions` / `RootCopyOptions`
|
|
129
129
|
|
|
130
130
|
```ts
|
|
131
|
+
import type { CopyCloneMode, RootCopyPublicationReceipt } from "@openclaw/fs-safe";
|
|
132
|
+
|
|
131
133
|
type RootReadOptions = Pick<RootDefaults, "hardlinks" | "maxBytes" | "nonBlockingRead" | "symlinks">;
|
|
132
134
|
type RootWriteOptions = Pick<RootDefaults, "assertBeforeMutation" | "denyMutations" | "durable" | "mkdir" | "mode" | "renameIdentity" | "mutationSymlinks"> & {
|
|
133
135
|
encoding?: BufferEncoding;
|
|
@@ -135,6 +137,11 @@ type RootWriteOptions = Pick<RootDefaults, "assertBeforeMutation" | "denyMutatio
|
|
|
135
137
|
};
|
|
136
138
|
type RootCopyOptions = Pick<RootDefaults, "assertBeforeMutation" | "denyMutations" | "durable" | "maxBytes" | "mkdir" | "mode" | "mutationSymlinks"> & {
|
|
137
139
|
sourceHardlinks?: "reject" | "allow";
|
|
140
|
+
overwrite?: boolean;
|
|
141
|
+
clone?: CopyCloneMode;
|
|
142
|
+
signal?: AbortSignal;
|
|
143
|
+
preserveSourceMode?: boolean;
|
|
144
|
+
onDestinationPublished?: (receipt: RootCopyPublicationReceipt) => void;
|
|
138
145
|
};
|
|
139
146
|
type RootOpenWritableOptions = Pick<RootDefaults, "assertBeforeMutation" | "denyMutations" | "mkdir" | "mode" | "mutationSymlinks"> & {
|
|
140
147
|
writeMode?: "replace" | "append" | "update";
|
|
@@ -150,8 +157,17 @@ type RootAppendOptions = RootWriteOptions & {
|
|
|
150
157
|
type RootMoveOptions = Pick<RootDefaults, "assertBeforeMutation" | "denyMutations" | "mutationSymlinks"> & {
|
|
151
158
|
overwrite?: boolean;
|
|
152
159
|
};
|
|
153
|
-
type RootRemoveOptions = Pick<RootDefaults, "assertBeforeMutation" | "denyMutations" | "mutationSymlinks"
|
|
154
|
-
|
|
160
|
+
type RootRemoveOptions = Pick<RootDefaults, "assertBeforeMutation" | "denyMutations" | "mutationSymlinks"> & {
|
|
161
|
+
recursive?: boolean;
|
|
162
|
+
force?: boolean;
|
|
163
|
+
order?: "filesystem" | "sorted";
|
|
164
|
+
maxEntries?: number;
|
|
165
|
+
maxDepth?: number;
|
|
166
|
+
signal?: AbortSignal;
|
|
167
|
+
};
|
|
168
|
+
type RootMkdirOptions = Pick<RootDefaults, "assertBeforeMutation" | "denyMutations" | "mutationSymlinks"> & {
|
|
169
|
+
private?: boolean;
|
|
170
|
+
};
|
|
155
171
|
```
|
|
156
172
|
|
|
157
173
|
Per-method option shapes. Each picks the `RootDefaults` keys that apply, plus method-specific extras.
|
package/docs/walk.md
CHANGED
|
@@ -47,6 +47,10 @@ type WalkDirectoryFailure = {
|
|
|
47
47
|
|
|
48
48
|
`depth` starts at `1` for direct children of `rootDir`. `relativePath` is always relative to the supplied root. `scannedEntryCount` counts directory entries examined, including entries filtered out by `include`.
|
|
49
49
|
|
|
50
|
+
Each entry's `path` is absolute and retains the normalized spelling of the
|
|
51
|
+
supplied root, including followed directory aliases. Paths do not switch to
|
|
52
|
+
the canonical symlink target during descent.
|
|
53
|
+
|
|
50
54
|
`walkDirectory()` and `walkDirectorySync()` always return `failedDirs`; the property remains optional on the exported `WalkDirectoryResult` type so existing callers that manually construct the legacy result shape remain source-compatible. It lists every directory whose `realpath`/`readdir` threw, so its contents are absent from `entries`. `error` is the thrown value (a `NodeJS.ErrnoException` at runtime), so callers can distinguish a benign missing-directory race (`ENOENT`) from a real read failure (`EACCES`, `EIO`, `ESTALE`, …). The walk-root failure has an empty `relativePath` and `depth: 0`. Failures resolving a symlink's target kind are not reported here.
|
|
51
55
|
|
|
52
56
|
## Options
|
|
@@ -227,6 +231,9 @@ its exact identity, and rechecks it and the Root identity around each metadata
|
|
|
227
231
|
batch or individual filesystem-order observation. Sorted batches contain no
|
|
228
232
|
await or caller code between their before/after checks. It tracks canonical
|
|
229
233
|
directories to stop symlink cycles.
|
|
234
|
+
Directory rechecks retain exact identities while using ordinary numeric metadata
|
|
235
|
+
when it represents those identities without rounding. Large identities and
|
|
236
|
+
Windows unknown-identity retries keep the bigint inspection path.
|
|
230
237
|
Neither mode holds a descriptor for every path component, so it is not a process sandbox against a hostile peer that
|
|
231
238
|
can continuously swap and restore directories. Each individual lookup retains
|
|
232
239
|
the documented Node `Root` boundary checks.
|