@openclaw/fs-safe 0.12.0 → 0.13.1
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 +73 -0
- package/README.md +12 -7
- package/dist/absolute-path.d.ts.map +1 -1
- package/dist/absolute-path.js +32 -11
- package/dist/advanced.d.ts +2 -0
- package/dist/advanced.d.ts.map +1 -1
- package/dist/advanced.js +2 -0
- package/dist/archive-input.d.ts.map +1 -1
- package/dist/archive-input.js +6 -2
- package/dist/archive-merge.d.ts.map +1 -1
- package/dist/archive-merge.js +15 -2
- package/dist/archive-native.d.ts.map +1 -1
- package/dist/archive-native.js +7 -19
- package/dist/archive-plan.js +1 -1
- package/dist/archive-read.d.ts.map +1 -1
- package/dist/archive-read.js +18 -13
- package/dist/archive-staging.d.ts.map +1 -1
- package/dist/archive-staging.js +43 -10
- package/dist/archive-tar-inspect.d.ts.map +1 -1
- package/dist/archive-tar-inspect.js +4 -1
- package/dist/archive-zip-admission.d.ts +1 -1
- package/dist/archive-zip-admission.d.ts.map +1 -1
- package/dist/archive-zip-admission.js +2 -2
- package/dist/archive-zip-directory.d.ts +3 -0
- package/dist/archive-zip-directory.d.ts.map +1 -1
- package/dist/archive-zip-directory.js +13 -2
- package/dist/archive-zip-loader.d.ts +3 -2
- package/dist/archive-zip-loader.d.ts.map +1 -1
- package/dist/archive-zip-loader.js +30 -4
- package/dist/archive-zip-manifest.d.ts +5 -0
- package/dist/archive-zip-manifest.d.ts.map +1 -0
- package/dist/archive-zip-manifest.js +22 -0
- package/dist/archive-zip-names.d.ts +6 -1
- package/dist/archive-zip-names.d.ts.map +1 -1
- package/dist/archive-zip-names.js +35 -14
- package/dist/archive-zip-preflight.d.ts.map +1 -1
- package/dist/archive-zip-preflight.js +3 -2
- package/dist/archive.d.ts.map +1 -1
- package/dist/archive.js +9 -4
- package/dist/copy-file-input.d.ts +1 -1
- package/dist/copy-file-input.d.ts.map +1 -1
- package/dist/copy-file-input.js +2 -0
- package/dist/darwin-acl.d.ts +4 -0
- package/dist/darwin-acl.d.ts.map +1 -0
- package/dist/darwin-acl.js +24 -0
- package/dist/deny-mutations.d.ts.map +1 -1
- package/dist/deny-mutations.js +8 -2
- package/dist/directory-durability.d.ts.map +1 -1
- package/dist/directory-durability.js +67 -23
- package/dist/directory-entry-path.d.ts +3 -0
- package/dist/directory-entry-path.d.ts.map +1 -0
- package/dist/directory-entry-path.js +21 -0
- package/dist/directory-guard.d.ts +17 -1
- package/dist/directory-guard.d.ts.map +1 -1
- package/dist/directory-guard.js +134 -48
- package/dist/directory-mode-node.d.ts +12 -0
- package/dist/directory-mode-node.d.ts.map +1 -1
- package/dist/directory-mode-node.js +102 -4
- package/dist/effective-uid.d.ts +2 -0
- package/dist/effective-uid.d.ts.map +1 -0
- package/dist/effective-uid.js +25 -0
- package/dist/file-handle-transfer.d.ts.map +1 -1
- package/dist/file-handle-transfer.js +98 -27
- package/dist/file-hash.d.ts.map +1 -1
- package/dist/file-hash.js +3 -0
- package/dist/file-lock-sync.d.ts.map +1 -1
- package/dist/file-lock-sync.js +36 -6
- package/dist/file-lock.d.ts.map +1 -1
- package/dist/file-lock.js +29 -9
- package/dist/file-observation.d.ts +1 -1
- package/dist/file-observation.d.ts.map +1 -1
- package/dist/file-store-boundary.d.ts +6 -2
- package/dist/file-store-boundary.d.ts.map +1 -1
- package/dist/file-store-boundary.js +20 -65
- package/dist/file-store-copy-source.d.ts +5 -0
- package/dist/file-store-copy-source.d.ts.map +1 -0
- package/dist/file-store-copy-source.js +31 -0
- package/dist/file-store-path.d.ts.map +1 -1
- package/dist/file-store-path.js +4 -1
- package/dist/file-store-sync-directory.d.ts +16 -0
- package/dist/file-store-sync-directory.d.ts.map +1 -0
- package/dist/file-store-sync-directory.js +349 -0
- package/dist/file-store.d.ts.map +1 -1
- package/dist/file-store.js +11 -28
- package/dist/filename.d.ts.map +1 -1
- package/dist/filename.js +65 -20
- package/dist/fs.d.ts.map +1 -1
- package/dist/fs.js +3 -2
- package/dist/guarded-mkdir.d.ts +8 -0
- package/dist/guarded-mkdir.d.ts.map +1 -1
- package/dist/guarded-mkdir.js +176 -27
- package/dist/guest.d.ts.map +1 -1
- package/dist/guest.js +4 -1
- package/dist/home-dir.d.ts.map +1 -1
- package/dist/home-dir.js +73 -10
- package/dist/install-path.d.ts.map +1 -1
- package/dist/install-path.js +54 -19
- package/dist/json-durable-queue-ownership.d.ts +6 -0
- package/dist/json-durable-queue-ownership.d.ts.map +1 -1
- package/dist/json-durable-queue-ownership.js +89 -42
- package/dist/json-durable-queue-paths.d.ts +11 -0
- package/dist/json-durable-queue-paths.d.ts.map +1 -0
- package/dist/json-durable-queue-paths.js +42 -0
- package/dist/json-durable-queue-read.d.ts.map +1 -1
- package/dist/json-durable-queue-read.js +2 -0
- package/dist/json-durable-queue.d.ts.map +1 -1
- package/dist/json-durable-queue.js +50 -58
- package/dist/json-store.d.ts.map +1 -1
- package/dist/json-store.js +5 -1
- package/dist/json.d.ts.map +1 -1
- package/dist/json.js +54 -22
- package/dist/local-file-access.d.ts.map +1 -1
- package/dist/local-file-access.js +4 -0
- package/dist/local-file-descriptor.d.ts +17 -0
- package/dist/local-file-descriptor.d.ts.map +1 -0
- package/dist/local-file-descriptor.js +84 -0
- package/dist/local-roots.d.ts.map +1 -1
- package/dist/local-roots.js +35 -7
- package/dist/move-path-cleanup.d.ts +2 -0
- package/dist/move-path-cleanup.d.ts.map +1 -1
- package/dist/move-path-cleanup.js +44 -18
- package/dist/move-path.d.ts.map +1 -1
- package/dist/move-path.js +31 -6
- package/dist/native-binding.d.ts +17 -0
- package/dist/native-binding.d.ts.map +1 -1
- package/dist/native-binding.js +8 -1
- package/dist/native-directory-observation.d.ts +17 -0
- package/dist/native-directory-observation.d.ts.map +1 -0
- package/dist/native-directory-observation.js +37 -0
- package/dist/native-operations.d.ts.map +1 -1
- package/dist/native-operations.js +6 -4
- package/dist/native-parent-admission.d.ts +32 -0
- package/dist/native-parent-admission.d.ts.map +1 -0
- package/dist/native-parent-admission.js +127 -0
- package/dist/native-pinned-write-windows.d.ts +0 -1
- package/dist/native-pinned-write-windows.d.ts.map +1 -1
- package/dist/native-pinned-write-windows.js +8 -9
- package/dist/native-pinned-write.d.ts.map +1 -1
- package/dist/native-pinned-write.js +314 -42
- package/dist/native-staged-file.d.ts +4 -4
- package/dist/native-staged-file.d.ts.map +1 -1
- package/dist/native-staged-file.js +20 -10
- package/dist/native.d.ts +1 -1
- package/dist/native.d.ts.map +1 -1
- package/dist/native.js +7 -2
- package/dist/output.d.ts.map +1 -1
- package/dist/output.js +15 -5
- package/dist/overwrite-file-handle.d.ts.map +1 -1
- package/dist/overwrite-file-handle.js +5 -1
- package/dist/owner-dacl.d.ts.map +1 -1
- package/dist/owner-dacl.js +2 -0
- package/dist/path-policy.d.ts.map +1 -1
- package/dist/path-policy.js +7 -2
- package/dist/path-prefix.d.ts +7 -0
- package/dist/path-prefix.d.ts.map +1 -0
- package/dist/path-prefix.js +82 -0
- package/dist/path-scope-lexical.d.ts.map +1 -1
- package/dist/path-scope-lexical.js +18 -8
- package/dist/path-segment-route.d.ts +7 -0
- package/dist/path-segment-route.d.ts.map +1 -0
- package/dist/path-segment-route.js +24 -0
- package/dist/path-suffix-aliases.d.ts +10 -0
- package/dist/path-suffix-aliases.d.ts.map +1 -0
- package/dist/path-suffix-aliases.js +386 -0
- package/dist/path.d.ts.map +1 -1
- package/dist/path.js +16 -5
- package/dist/permissions-windows.d.ts.map +1 -1
- package/dist/permissions-windows.js +14 -3
- package/dist/permissions.d.ts.map +1 -1
- package/dist/permissions.js +37 -8
- package/dist/pinned-mutation-admission.d.ts +25 -0
- package/dist/pinned-mutation-admission.d.ts.map +1 -0
- package/dist/pinned-mutation-admission.js +425 -0
- package/dist/pinned-mutation-observation.d.ts +34 -0
- package/dist/pinned-mutation-observation.d.ts.map +1 -0
- package/dist/pinned-mutation-observation.js +142 -0
- package/dist/pinned-mutation-shared-route.d.ts +24 -0
- package/dist/pinned-mutation-shared-route.d.ts.map +1 -0
- package/dist/pinned-mutation-shared-route.js +70 -0
- package/dist/pinned-open.d.ts +6 -0
- package/dist/pinned-open.d.ts.map +1 -1
- package/dist/pinned-open.js +28 -10
- package/dist/pinned-write-types.d.ts +75 -0
- package/dist/pinned-write-types.d.ts.map +1 -0
- package/dist/pinned-write-types.js +1 -0
- package/dist/pinned-write.d.ts +7 -33
- package/dist/pinned-write.d.ts.map +1 -1
- package/dist/pinned-write.js +151 -17
- package/dist/private-directory.d.ts.map +1 -1
- package/dist/private-directory.js +2 -0
- package/dist/private-producer-handoff.d.ts +16 -0
- package/dist/private-producer-handoff.d.ts.map +1 -0
- package/dist/private-producer-handoff.js +272 -0
- package/dist/private-temp-workspace.d.ts +2 -39
- package/dist/private-temp-workspace.d.ts.map +1 -1
- package/dist/private-temp-workspace.js +183 -77
- package/dist/publish-file.d.ts.map +1 -1
- package/dist/publish-file.js +38 -32
- package/dist/regular-file.d.ts.map +1 -1
- package/dist/regular-file.js +56 -45
- package/dist/replace-directory.d.ts.map +1 -1
- package/dist/replace-directory.js +12 -5
- package/dist/replace-file-temp-owner.d.ts +1 -0
- package/dist/replace-file-temp-owner.d.ts.map +1 -1
- package/dist/replace-file-temp-owner.js +12 -1
- package/dist/replace-file.d.ts.map +1 -1
- package/dist/replace-file.js +24 -24
- package/dist/root-boundary.d.ts +4 -0
- package/dist/root-boundary.d.ts.map +1 -1
- package/dist/root-boundary.js +6 -1
- package/dist/root-context.d.ts +12 -1
- package/dist/root-context.d.ts.map +1 -1
- package/dist/root-context.js +57 -11
- package/dist/root-directory-creation.d.ts +13 -0
- package/dist/root-directory-creation.d.ts.map +1 -0
- package/dist/root-directory-creation.js +212 -0
- package/dist/root-directory-list.d.ts +16 -4
- package/dist/root-directory-list.d.ts.map +1 -1
- package/dist/root-directory-list.js +180 -39
- package/dist/root-directory.d.ts +23 -0
- package/dist/root-directory.d.ts.map +1 -0
- package/dist/root-directory.js +141 -0
- package/dist/root-errors.d.ts.map +1 -1
- package/dist/root-errors.js +12 -1
- package/dist/root-file-final-admission.d.ts +21 -0
- package/dist/root-file-final-admission.d.ts.map +1 -0
- package/dist/root-file-final-admission.js +83 -0
- package/dist/root-file.d.ts.map +1 -1
- package/dist/root-file.js +103 -24
- package/dist/root-impl.d.ts +1 -0
- package/dist/root-impl.d.ts.map +1 -1
- package/dist/root-impl.js +289 -317
- package/dist/root-move-noreplace.d.ts +14 -0
- package/dist/root-move-noreplace.d.ts.map +1 -0
- package/dist/root-move-noreplace.js +202 -0
- package/dist/root-observed-path.d.ts +9 -0
- package/dist/root-observed-path.d.ts.map +1 -0
- package/dist/root-observed-path.js +95 -0
- package/dist/root-path-errors.d.ts +12 -0
- package/dist/root-path-errors.d.ts.map +1 -0
- package/dist/root-path-errors.js +13 -0
- package/dist/root-path-existing.d.ts.map +1 -1
- package/dist/root-path-existing.js +53 -20
- package/dist/root-path-observation.d.ts +63 -0
- package/dist/root-path-observation.d.ts.map +1 -0
- package/dist/root-path-observation.js +180 -0
- package/dist/root-path-stat.d.ts +5 -0
- package/dist/root-path-stat.d.ts.map +1 -0
- package/dist/root-path-stat.js +101 -0
- package/dist/root-path-symlink.d.ts.map +1 -1
- package/dist/root-path-symlink.js +23 -4
- package/dist/root-path.d.ts +11 -0
- package/dist/root-path.d.ts.map +1 -1
- package/dist/root-path.js +215 -53
- package/dist/root-paths-lexical.d.ts +9 -0
- package/dist/root-paths-lexical.d.ts.map +1 -0
- package/dist/root-paths-lexical.js +22 -0
- package/dist/root-paths.d.ts +3 -25
- package/dist/root-paths.d.ts.map +1 -1
- package/dist/root-paths.js +77 -154
- package/dist/root-read-admission.d.ts +26 -0
- package/dist/root-read-admission.d.ts.map +1 -0
- package/dist/root-read-admission.js +94 -0
- package/dist/root-remove-identity.d.ts +15 -0
- package/dist/root-remove-identity.d.ts.map +1 -0
- package/dist/root-remove-identity.js +89 -0
- package/dist/root-remove-receipt.d.ts +17 -0
- package/dist/root-remove-receipt.d.ts.map +1 -0
- package/dist/root-remove-receipt.js +37 -0
- package/dist/root-remove.d.ts +2 -1
- package/dist/root-remove.d.ts.map +1 -1
- package/dist/root-remove.js +172 -10
- package/dist/root-write-admission.d.ts +68 -0
- package/dist/root-write-admission.d.ts.map +1 -0
- package/dist/root-write-admission.js +322 -0
- package/dist/root-write-compatibility.d.ts +13 -0
- package/dist/root-write-compatibility.d.ts.map +1 -0
- package/dist/root-write-compatibility.js +74 -0
- package/dist/root-write-complete-parent.d.ts +42 -0
- package/dist/root-write-complete-parent.d.ts.map +1 -0
- package/dist/root-write-complete-parent.js +195 -0
- package/dist/root-write-publication.d.ts +24 -0
- package/dist/root-write-publication.d.ts.map +1 -0
- package/dist/root-write-publication.js +85 -0
- package/dist/root-write-verification.d.ts.map +1 -1
- package/dist/root-write-verification.js +6 -4
- package/dist/secret-file.d.ts.map +1 -1
- package/dist/secret-file.js +62 -24
- package/dist/secret-read-async.d.ts.map +1 -1
- package/dist/secret-read-async.js +18 -13
- package/dist/secret-read-policy.d.ts.map +1 -1
- package/dist/secret-read-policy.js +5 -1
- package/dist/secure-file.d.ts.map +1 -1
- package/dist/secure-file.js +92 -7
- package/dist/secure-temp-dir.d.ts +6 -3
- package/dist/secure-temp-dir.d.ts.map +1 -1
- package/dist/secure-temp-dir.js +121 -101
- package/dist/secure-temp-repair.d.ts +35 -0
- package/dist/secure-temp-repair.d.ts.map +1 -0
- package/dist/secure-temp-repair.js +106 -0
- package/dist/sibling-staged-file.d.ts +3 -2
- package/dist/sibling-staged-file.d.ts.map +1 -1
- package/dist/sibling-staged-file.js +179 -55
- package/dist/sibling-temp.d.ts.map +1 -1
- package/dist/sibling-temp.js +27 -13
- package/dist/sidecar-lock-acquire.d.ts.map +1 -1
- package/dist/sidecar-lock-acquire.js +44 -14
- package/dist/sidecar-lock-root.d.ts.map +1 -1
- package/dist/sidecar-lock-root.js +4 -2
- package/dist/staged-directory.d.ts +14 -5
- package/dist/staged-directory.d.ts.map +1 -1
- package/dist/staged-directory.js +54 -3
- package/dist/standalone-publication-path.d.ts +2 -0
- package/dist/standalone-publication-path.d.ts.map +1 -0
- package/dist/standalone-publication-path.js +6 -0
- package/dist/stat-observation.d.ts +11 -0
- package/dist/stat-observation.d.ts.map +1 -0
- package/dist/stat-observation.js +64 -0
- package/dist/strict-file-identity.d.ts.map +1 -1
- package/dist/strict-file-identity.js +39 -2
- package/dist/symlink-parents.d.ts.map +1 -1
- package/dist/symlink-parents.js +7 -2
- package/dist/temp-target.d.ts +2 -0
- package/dist/temp-target.d.ts.map +1 -1
- package/dist/temp-target.js +130 -3
- package/dist/temp-workspace-admission.d.ts +22 -0
- package/dist/temp-workspace-admission.d.ts.map +1 -0
- package/dist/temp-workspace-admission.js +374 -0
- package/dist/temp-workspace-child-admission.d.ts +13 -0
- package/dist/temp-workspace-child-admission.d.ts.map +1 -0
- package/dist/temp-workspace-child-admission.js +95 -0
- package/dist/temp-workspace-descriptor.d.ts +48 -0
- package/dist/temp-workspace-descriptor.d.ts.map +1 -0
- package/dist/temp-workspace-descriptor.js +363 -0
- package/dist/temp-workspace-identity.d.ts +15 -0
- package/dist/temp-workspace-identity.d.ts.map +1 -0
- package/dist/temp-workspace-identity.js +41 -0
- package/dist/temp-workspace-owner.d.ts +7 -6
- package/dist/temp-workspace-owner.d.ts.map +1 -1
- package/dist/temp-workspace-owner.js +121 -61
- package/dist/temp-workspace-permissions.d.ts +4 -0
- package/dist/temp-workspace-permissions.d.ts.map +1 -0
- package/dist/temp-workspace-permissions.js +32 -0
- package/dist/temp-workspace-types.d.ts +40 -0
- package/dist/temp-workspace-types.d.ts.map +1 -0
- package/dist/temp-workspace-types.js +1 -0
- package/dist/temp.d.ts +1 -1
- package/dist/temp.d.ts.map +1 -1
- package/dist/temp.js +1 -1
- package/dist/test-hooks.d.ts +7 -0
- package/dist/test-hooks.d.ts.map +1 -1
- package/dist/text-atomic.d.ts.map +1 -1
- package/dist/text-atomic.js +3 -1
- package/dist/trash.d.ts.map +1 -1
- package/dist/trash.js +54 -8
- package/dist/walk.d.ts.map +1 -1
- package/dist/walk.js +9 -6
- package/dist/windows-owner.d.ts.map +1 -1
- package/dist/windows-owner.js +5 -0
- package/dist/windows-path-alias.d.ts +39 -0
- package/dist/windows-path-alias.d.ts.map +1 -0
- package/dist/windows-path-alias.js +153 -0
- package/docs/advanced.md +25 -0
- package/docs/archive.md +23 -2
- package/docs/atomic.md +27 -3
- package/docs/copy.md +3 -0
- package/docs/durability.md +14 -0
- package/docs/errors.md +14 -6
- package/docs/file-store.md +24 -3
- package/docs/filename.md +14 -7
- package/docs/install-path.md +2 -2
- package/docs/install.md +8 -7
- package/docs/json-store.md +5 -1
- package/docs/json.md +11 -0
- package/docs/mutation-policy-proof.md +65 -0
- package/docs/native-helper.md +41 -11
- package/docs/native.md +32 -8
- package/docs/output.md +33 -11
- package/docs/path-prefix.md +64 -0
- package/docs/path-suffix-aliases.md +159 -0
- package/docs/path.md +11 -0
- package/docs/private-file-store.md +14 -0
- package/docs/public-api.md +9 -0
- package/docs/quickstart.md +1 -1
- package/docs/reading.md +8 -1
- package/docs/root.md +18 -3
- package/docs/secret-file.md +3 -0
- package/docs/secure-file.md +6 -2
- package/docs/security-model.md +75 -8
- package/docs/sidecar-lock.md +28 -1
- package/docs/store.md +5 -1
- package/docs/temp.md +260 -36
- package/docs/test-hooks.md +14 -0
- package/docs/writing.md +38 -8
- package/package.json +10 -10
package/docs/temp.md
CHANGED
|
@@ -14,7 +14,89 @@ import {
|
|
|
14
14
|
|
|
15
15
|
## Private temp workspaces
|
|
16
16
|
|
|
17
|
-
A private workspace is a
|
|
17
|
+
A private workspace is a uniquely named directory under a caller-provided temp
|
|
18
|
+
root. The default requested mode is `0o700`. Calling `cleanup()` or leaving an
|
|
19
|
+
`await using` scope moves an unchanged workspace through a private quarantine
|
|
20
|
+
before removal. Descriptor-bounded cleanup prevents recursive traversal of
|
|
21
|
+
substitutions; the compatible JavaScript fallback has the narrower race
|
|
22
|
+
contract documented below.
|
|
23
|
+
|
|
24
|
+
On POSIX, workspace creation verifies the supplied root and its canonical
|
|
25
|
+
ancestors before creating a child. Each existing directory must be owned by the
|
|
26
|
+
effective user or root. Group/world-writable directories must also have the
|
|
27
|
+
sticky bit, so ordinary system temp directories remain usable without changing
|
|
28
|
+
their modes. Foreign-owned directories and non-sticky writable ancestors reject
|
|
29
|
+
with `not-owned` or `insecure-permissions`; unavailable effective-user identity
|
|
30
|
+
rejects with `permission-unverified`. Existing supplied directories keep their
|
|
31
|
+
permissions. Missing root components are created at `0o700` and initialized
|
|
32
|
+
from their first exact security snapshot; if a restrictive umask changes that
|
|
33
|
+
mode, correction uses a verified directory descriptor.
|
|
34
|
+
|
|
35
|
+
For an already existing canonical root, discovery retains only its immutable
|
|
36
|
+
exact identity. Cleanup-parent retention is provisional: after any native
|
|
37
|
+
capability probe, creation captures and validates the complete ancestry,
|
|
38
|
+
re-observes the root against discovery, and associates the retained parent
|
|
39
|
+
descriptor. Async creation and sync creation outside the Linux/macOS
|
|
40
|
+
direct-mode case dispatch `mkdtemp` immediately after that synchronous boundary
|
|
41
|
+
without another yield or native probe. Existing aliases and missing-component
|
|
42
|
+
roots keep the guarded admission route.
|
|
43
|
+
|
|
44
|
+
On Linux and macOS, synchronous creation can instead use an exclusive six-character
|
|
45
|
+
random child name when an explicit requested mode other than `0o700` has owner
|
|
46
|
+
`rwx`, no special bits, and no group/world write bits. The requested mode is
|
|
47
|
+
passed directly to `mkdir` and the observed complete permission bits, rather
|
|
48
|
+
than the requested bits, are authoritative. Umask, inherited ACL state, or
|
|
49
|
+
inherited special bits can make that observation differ, in which case creation
|
|
50
|
+
corrects the mode through the retained descriptor. This mode-based optimization
|
|
51
|
+
does not claim that Linux and macOS have identical syscall or ACL behavior, and
|
|
52
|
+
POSIX mode bits do not establish ACL privacy. Creation makes at most 64 attempts;
|
|
53
|
+
after a name collision, each retry generates its candidate first, replays the
|
|
54
|
+
already admitted immutable ancestry and descriptor receipts, and then
|
|
55
|
+
immediately attempts exclusive creation. A colliding entry is never inspected,
|
|
56
|
+
adopted, corrected, registered, or deleted. The default `0o700`, async creation, and
|
|
57
|
+
other sync modes retain the `mkdtemp` path. That path requests initial mode
|
|
58
|
+
`0o700`; a result different from `dirMode` is initialized through the same
|
|
59
|
+
descriptor-bound correction.
|
|
60
|
+
|
|
61
|
+
The direct sync path opens the new child without following its final component
|
|
62
|
+
and captures one exact descriptor observation after the parent replay. The new
|
|
63
|
+
child's exact identity, type, owner, private bits, and complete `0o7777` mode are
|
|
64
|
+
checked before mode initialization. When its creation mode already
|
|
65
|
+
matches `dirMode` (including the default `0o700`), creation avoids an extra mode
|
|
66
|
+
descriptor and chmod. If the observed creation mode differs from `dirMode`, the
|
|
67
|
+
immediate synchronous correction consumes that one-shot observation, checks the
|
|
68
|
+
fresh child name, replays the parent, and applies correction through the retained
|
|
69
|
+
descriptor. Later admission always performs fresh descriptor and name checks.
|
|
70
|
+
Permission failures propagate. POSIX `dirMode`
|
|
71
|
+
must not grant group/world write access; it only controls the new workspace,
|
|
72
|
+
not existing supplied directories. After the
|
|
73
|
+
first exact child observation, final adoption retains a no-follow child
|
|
74
|
+
descriptor, rechecks complete ancestry and retained cleanup-parent authority,
|
|
75
|
+
and then validates the original child's descriptor and current name for exact
|
|
76
|
+
identity, owner, private bits, and requested mode before cleanup is registered.
|
|
77
|
+
Linux and macOS may replay exact identities through round-trip-safe nonnegative
|
|
78
|
+
numeric `dev`/`ino` projections. Initial receipts that cannot be represented
|
|
79
|
+
exactly stay on the BigInt path; a malformed or mismatched numeric replay fails
|
|
80
|
+
closed without an exact retry.
|
|
81
|
+
Parent or child replacements observed during creation reject before cleanup
|
|
82
|
+
ownership is registered. Unverified artifacts are left in place for
|
|
83
|
+
caller-directed recovery.
|
|
84
|
+
|
|
85
|
+
On Windows, POSIX mode/UID metadata does not establish ACL privacy, and these
|
|
86
|
+
factories neither claim nor initialize a POSIX `dirMode`; every requested value
|
|
87
|
+
uses the identity-only path without opening a mode descriptor or applying chmod.
|
|
88
|
+
Callers must supply a root with trusted ACLs that protect its children and
|
|
89
|
+
ancestors; exact pathname identity checks still apply. `cleanupSafety` controls
|
|
90
|
+
removal capability, not Windows ACL admission.
|
|
91
|
+
|
|
92
|
+
Root aliases already present at entry retain their historical support and are
|
|
93
|
+
canonicalized. Callers remain responsible for choosing trusted root paths and
|
|
94
|
+
excluding hostile peers with the same filesystem authority. Node's pathname
|
|
95
|
+
`mkdir`/`mkdtemp` calls do not atomically return a creation descriptor: identity
|
|
96
|
+
checks detect observed substitutions but cannot prove provenance against every
|
|
97
|
+
same-privilege replacement before the first observation. Descriptor chmod
|
|
98
|
+
cannot be redirected to a subsequently substituted pathname. Native bounded
|
|
99
|
+
cleanup does not upgrade the creation operation to an atomic namespace boundary.
|
|
18
100
|
|
|
19
101
|
### `tempWorkspace`
|
|
20
102
|
|
|
@@ -76,18 +158,36 @@ verification can still redirect the final pathname removal.
|
|
|
76
158
|
|
|
77
159
|
Set `cleanupSafety: "require-bounded"` when that concurrent attacker is in scope.
|
|
78
160
|
Creation then requires native no-replace directory rename, native owned-tree
|
|
79
|
-
removal, and a retained parent descriptor **before**
|
|
80
|
-
|
|
81
|
-
`
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
161
|
+
removal, and a readable retained parent descriptor **before** child creation.
|
|
162
|
+
On POSIX, the final requested `dirMode` must also include owner read
|
|
163
|
+
and search (`(dirMode & 0o500) === 0o500`). Preflight failure throws
|
|
164
|
+
`FsSafeError("helper-unavailable")` without creating a child or calling a scoped
|
|
165
|
+
callback. The child descriptor is opened
|
|
166
|
+
while the new directory still has its private creation mode, before an explicit
|
|
167
|
+
`dirMode` can lower access. Retaining a read descriptor does not bypass the
|
|
168
|
+
POSIX final-mode requirement: enumeration reopens the directory relative to
|
|
169
|
+
that descriptor. Compatible mode accepts these restrictive modes but selects
|
|
170
|
+
the JavaScript fallback and does not retain native traversal authority for the
|
|
171
|
+
child, even if the caller later restores its permissions. Windows cleanup
|
|
172
|
+
does not use this POSIX mode gate. A search-only descriptor remains valid identity
|
|
173
|
+
evidence but is never native traversal authority: compatible cleanup selects
|
|
174
|
+
the JavaScript fallback, while `require-bounded` rejects and leaves the
|
|
175
|
+
unregistered child in place for caller-directed recovery. The compatible
|
|
176
|
+
default retains its fallback even if process-global native mode is `require`;
|
|
177
|
+
select `require-bounded` to make cleanup capability mandatory for this API.
|
|
85
178
|
|
|
86
179
|
On Linux, bounded cleanup requires a successful runtime probe of the exact
|
|
87
180
|
`openat2` child-directory flags, including `RESOLVE_NO_XDEV`, against the retained
|
|
88
181
|
parent descriptor. If the kernel or seccomp policy denies that capability,
|
|
89
182
|
compatible mode uses the guarded JavaScript fallback; `require-bounded` rejects
|
|
90
|
-
before child creation.
|
|
183
|
+
before child creation. For eligible modes, the probe runs once at creation,
|
|
184
|
+
without filesystem mutation.
|
|
185
|
+
|
|
186
|
+
Cleanup does not repair POSIX workspace or descendant permissions. In compatible
|
|
187
|
+
mode, a restrictive workspace mode or caller-created unreadable descendants
|
|
188
|
+
can prevent recursive removal; native bounded cleanup can also encounter later
|
|
189
|
+
permission changes or inaccessible descendants. These remain operational
|
|
190
|
+
cleanup failures, with the propagation and recovery behavior described below.
|
|
91
191
|
|
|
92
192
|
Bounded cleanup checks the parent and public workspace identity, quarantines
|
|
93
193
|
the direct child without replacement, and verifies the quarantine against the
|
|
@@ -183,12 +283,20 @@ try {
|
|
|
183
283
|
type TempWorkspaceOptions = {
|
|
184
284
|
rootDir: string; // parent directory for workspaces
|
|
185
285
|
prefix: string; // dir prefix (sanitized)
|
|
186
|
-
dirMode?: number; //
|
|
286
|
+
dirMode?: number; // new workspace mode; default 0o700; no POSIX group/world write
|
|
187
287
|
mode?: number; // file write mode; default 0o600
|
|
188
288
|
cleanupSafety?: "compatible" | "require-bounded"; // default compatible
|
|
189
289
|
};
|
|
190
290
|
```
|
|
191
291
|
|
|
292
|
+
On Windows, caller-provided workspace roots and workspace leaf names reject
|
|
293
|
+
NTFS alternate-stream and directory-index namespace spellings before directory
|
|
294
|
+
creation or file access. The lower-level temp and sibling helpers apply the
|
|
295
|
+
same admission to supplied roots, sibling directories, and callback-selected
|
|
296
|
+
final paths. Prefixes and `tempFile()` filenames keep their documented
|
|
297
|
+
sanitization behavior; ordinary colon-bearing POSIX roots and leaf names remain
|
|
298
|
+
valid.
|
|
299
|
+
|
|
192
300
|
## Advanced temp primitives
|
|
193
301
|
|
|
194
302
|
When you don't need the stable workspace abstraction, the lower-level temp-file
|
|
@@ -212,6 +320,18 @@ try {
|
|
|
212
320
|
}
|
|
213
321
|
```
|
|
214
322
|
|
|
323
|
+
Options:
|
|
324
|
+
|
|
325
|
+
```ts
|
|
326
|
+
type TempFileOptions = {
|
|
327
|
+
rootDir?: string;
|
|
328
|
+
prefix: string;
|
|
329
|
+
fileName?: string;
|
|
330
|
+
onCleanupError?: (error: unknown) => void;
|
|
331
|
+
cleanupSafety?: "compatible" | "require-bounded"; // default compatible
|
|
332
|
+
};
|
|
333
|
+
```
|
|
334
|
+
|
|
215
335
|
Returns:
|
|
216
336
|
|
|
217
337
|
```ts
|
|
@@ -224,9 +344,30 @@ type TempFile = {
|
|
|
224
344
|
};
|
|
225
345
|
```
|
|
226
346
|
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
347
|
+
The default `cleanupSafety: "compatible"` retains the historical temp-file
|
|
348
|
+
behavior without loading or probing the native helper. Cleanup captures the
|
|
349
|
+
directory identity at creation time and preserves a replacement observed by
|
|
350
|
+
its pre-removal identity check. That check and pathname-recursive removal are
|
|
351
|
+
separate operations, however: a same-privilege peer can substitute a directory
|
|
352
|
+
in the final gap and redirect recursive traversal. Process-exit cleanup has the
|
|
353
|
+
same compatible contract.
|
|
354
|
+
|
|
355
|
+
The bounded mode requires an existing supplied root and applies the same
|
|
356
|
+
trusted-ancestry admission as private temp workspaces. On POSIX it finalizes
|
|
357
|
+
the new directory to `0o700` through its retained descriptor; Windows keeps
|
|
358
|
+
identity checks without treating POSIX modes as ACL privacy. Unverified
|
|
359
|
+
creation artifacts are preserved for caller-directed recovery.
|
|
360
|
+
|
|
361
|
+
Set `cleanupSafety: "require-bounded"` to require the retained-parent,
|
|
362
|
+
no-replace quarantine, and descriptor-bounded owned-tree removal described for
|
|
363
|
+
[private temp workspaces](#private-temp-workspaces). Admission, including the
|
|
364
|
+
runtime probe, completes before `mkdtemp`; unavailable support throws
|
|
365
|
+
`FsSafeError("helper-unavailable")` before a child is created. Manual, disposal,
|
|
366
|
+
scoped, and process-exit cleanup then share one owner, and no pathname-recursive
|
|
367
|
+
fallback is used. `cleanup()` still resolves `Promise<void>`: operational
|
|
368
|
+
cleanup errors are passed to `onCleanupError` when supplied and otherwise
|
|
369
|
+
suppressed for compatibility. The bounded POSIX final-entry unlink limit still
|
|
370
|
+
applies.
|
|
230
371
|
|
|
231
372
|
### `withTempFile`
|
|
232
373
|
|
|
@@ -270,8 +411,13 @@ and current pathname are rejected. The callback must finish and close its
|
|
|
270
411
|
writer before returning. Its return value is preserved as `result`.
|
|
271
412
|
|
|
272
413
|
Generated temp filenames suffix Windows reserved-device basenames on every
|
|
273
|
-
platform.
|
|
274
|
-
|
|
414
|
+
platform. Before either an ordinary or isolated producer runs, the completed staging
|
|
415
|
+
name must be a nonempty, non-dot path component with no POSIX or Windows
|
|
416
|
+
separator, C0/C1 control, Windows-invalid punctuation or stream colon. Windows
|
|
417
|
+
reserved devices and trailing-dot/space aliases are rejected on every host.
|
|
418
|
+
The helper joins that validated component to the guarded or owned directory and
|
|
419
|
+
verifies that the result is a direct child. An invalid completion rejects with
|
|
420
|
+
`invalid-path` without calling the producer or its pre-write hook.
|
|
275
421
|
|
|
276
422
|
The helper retains one descriptor through requested mode application, opt-in
|
|
277
423
|
file synchronization, rename, and publication verification. It opens read-only
|
|
@@ -313,23 +459,35 @@ owned workspace cleanup, including partial output, subject to directory
|
|
|
313
459
|
identity checks and I/O failures. The callback must still finish and close its
|
|
314
460
|
writer before returning.
|
|
315
461
|
|
|
316
|
-
After the callback succeeds,
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
462
|
+
After the callback succeeds, an available native helper uses guarded
|
|
463
|
+
no-replace `Root.move`. Native-off operation admits the completed regular file
|
|
464
|
+
with a retained descriptor. On Windows, that branch first requests write-only
|
|
465
|
+
access so admission does not request completed-file data reads; access or provider
|
|
466
|
+
rejections fall back to the historical read-only or read/write open before admission.
|
|
467
|
+
It then creates the randomized sibling with an atomic
|
|
468
|
+
no-clobber hard link and verifies the expected two-link transition. On Windows,
|
|
469
|
+
it opens and verifies a sibling descriptor before closing the source descriptor,
|
|
470
|
+
keeping the file pinned while allowing the private name to disappear on runtimes
|
|
471
|
+
with legacy deletion behavior. It removes the private name before continuing.
|
|
472
|
+
If legacy deletion clears the completed file's read-only attribute, the helper
|
|
473
|
+
restores it through the retained sibling descriptor and verifies its mode and
|
|
474
|
+
identity before publication. Restoration failures reject the operation.
|
|
475
|
+
An escaping symlink fails with `path-alias`, and
|
|
476
|
+
a filesystem without either handoff reports `helper-unavailable`. The retained
|
|
477
|
+
descriptor carries file admission, requested modes, sync options, and final
|
|
478
|
+
rename with their existing contracts; `resolveFinalPath(result)` still names a
|
|
479
|
+
direct child of `dir`.
|
|
323
480
|
|
|
324
481
|
The isolated path retains exact bigint identities for both the parent and the
|
|
325
482
|
workspace and rechecks them before moving output to the sibling path. An
|
|
326
|
-
observed replacement is rejected.
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
retained directory after a rename,
|
|
331
|
-
|
|
332
|
-
|
|
483
|
+
observed replacement is rejected. This internal use of [`withTempFile`](#withtempfile)
|
|
484
|
+
does not expose `cleanupSafety` and uses compatible cleanup: moving or replacing
|
|
485
|
+
the parent or workspace can leave artifacts, and a workspace substituted in
|
|
486
|
+
the final check-to-recursive-removal gap can redirect traversal. It does not
|
|
487
|
+
promise cleanup through a retained directory after a rename, bounded cleanup,
|
|
488
|
+
stronger permissions, or additional crash durability. Omitting producer
|
|
489
|
+
isolation preserves the direct sibling callback path and unadmitted
|
|
490
|
+
partial-file retention.
|
|
333
491
|
|
|
334
492
|
On POSIX, admission uses no-follow and nonblocking open flags, so a FIFO swap
|
|
335
493
|
does not block the helper. Windows retains Node's guarded pathname-open behavior
|
|
@@ -343,8 +501,11 @@ Identity checks and pathname rename/unlink are separate syscalls, not atomic
|
|
|
343
501
|
conditional mutations. A hostile process can still replace a leaf or parent in
|
|
344
502
|
the final syscall gap or mutate an open file's contents. Use an approved writable
|
|
345
503
|
directory and cooperative locking or OS isolation; a moved parent can leave an
|
|
346
|
-
unpublished original temp behind.
|
|
347
|
-
|
|
504
|
+
unpublished original temp behind. Replacements observed before the final
|
|
505
|
+
namespace mutation are preserved, but that observation is not an atomic
|
|
506
|
+
condition on the later unlink or rename. Private producer isolation also has
|
|
507
|
+
the compatible recursive-cleanup gap described above. Arbitrary concurrent
|
|
508
|
+
namespace changes cannot be prevented by these helpers.
|
|
348
509
|
|
|
349
510
|
By default the helper attempts to set `dir` to `dirMode` (default `0o700`)
|
|
350
511
|
through the shared verified POSIX directory-descriptor helper. Only the actual
|
|
@@ -375,12 +536,18 @@ await writeViaSiblingTempPath({
|
|
|
375
536
|
If `replaceFileAtomic` does what you need, prefer that. Use
|
|
376
537
|
`writeViaSiblingTempPath` when the producer needs a concrete temp pathname but
|
|
377
538
|
the final destination still needs root-boundary checks.
|
|
378
|
-
Its private workspace uses
|
|
379
|
-
|
|
539
|
+
Its private workspace uses `tempFile()`'s compatible identity-aware cleanup.
|
|
540
|
+
It preserves replacements observed before removal, but retains the final
|
|
541
|
+
pathname-recursive-removal gap described above; this helper does not expose
|
|
542
|
+
`cleanupSafety: "require-bounded"`.
|
|
380
543
|
The callback staging component is capped at 255 bytes as written and under NFC and NFD by
|
|
381
544
|
shortening only an overlong embedded destination tail, while preserving an
|
|
382
545
|
extension when possible. Short callback paths and the final target stay
|
|
383
|
-
unchanged.
|
|
546
|
+
unchanged. An unusable target basename causes `fallbackFileName` to pass through
|
|
547
|
+
the same basename, character, reserved-device, and length sanitization before it
|
|
548
|
+
is embedded; if neither candidate is usable, the fixed tail `file` is used. The
|
|
549
|
+
completed component is then checked as a direct child before test hooks or the
|
|
550
|
+
producer run. This workspace owns its contents, unlike the unadmitted sibling
|
|
384
551
|
pathname above.
|
|
385
552
|
|
|
386
553
|
## Secure temp root
|
|
@@ -400,6 +567,7 @@ Consumers that only need this resolver can use the narrow package subpath:
|
|
|
400
567
|
import {
|
|
401
568
|
resolveSecureTempRoot,
|
|
402
569
|
type ResolveSecureTempRootOptions,
|
|
570
|
+
type SecureTempRootDescriptorAdapter,
|
|
403
571
|
} from "@openclaw/fs-safe/secure-temp-root";
|
|
404
572
|
```
|
|
405
573
|
|
|
@@ -424,12 +592,13 @@ type ResolveSecureTempRootOptions = {
|
|
|
424
592
|
getuid?: () => number | undefined;
|
|
425
593
|
tmpdir?: () => string;
|
|
426
594
|
accessSync?: typeof import("node:fs").accessSync;
|
|
427
|
-
chmodSync?: typeof import("node:fs").chmodSync;
|
|
595
|
+
chmodSync?: typeof import("node:fs").chmodSync; // deprecated, never read or called
|
|
596
|
+
descriptor?: SecureTempRootDescriptorAdapter; // complete bundle; see below
|
|
428
597
|
lstatSync?: (path: string) => {
|
|
429
598
|
isDirectory(): boolean;
|
|
430
599
|
isSymbolicLink(): boolean;
|
|
431
|
-
mode?: number;
|
|
432
|
-
uid?: number;
|
|
600
|
+
mode?: number | bigint;
|
|
601
|
+
uid?: number | bigint;
|
|
433
602
|
};
|
|
434
603
|
mkdirSync?: (
|
|
435
604
|
path: string,
|
|
@@ -448,6 +617,61 @@ the fallback to mode `0o700` where mode bits apply. If it cannot establish that
|
|
|
448
617
|
state, it throws an ordinary `Error`; there is no native mode or
|
|
449
618
|
`helper-unavailable` branch on this API.
|
|
450
619
|
|
|
620
|
+
Existing secure directories retain the one-`lstat`/access fast path, with no
|
|
621
|
+
descriptor open or chmod. On POSIX, a directory created by this call is instead
|
|
622
|
+
finalized through a pinned descriptor and must finish at exactly `0o700`, even
|
|
623
|
+
when a privileged caller can access its initial restrictive mode. A concurrent
|
|
624
|
+
recursive-`mkdir` winner is inspected as an untrusted existing directory.
|
|
625
|
+
Broad-mode repair also uses a pinned descriptor; there is no pathname chmod.
|
|
626
|
+
|
|
627
|
+
Repair and finalization require a known nonnegative safe-integer UID and exact
|
|
628
|
+
bigint device, inode, owner, mode, and directory-type facts. They open with
|
|
629
|
+
`O_RDONLY | O_DIRECTORY | O_NOFOLLOW | O_NONBLOCK`, verify the descriptor and
|
|
630
|
+
current directory entry against the initial receipt before `fchmod`, then
|
|
631
|
+
verify identity, permissions, and write/search access again before closing.
|
|
632
|
+
Trailing separators are stripped only for entry inspection, preserving filesystem
|
|
633
|
+
roots and symlink-sensitive `..` components. Unknown, numeric, or malformed
|
|
634
|
+
identity facts cannot authorize a mutation. A failed chmod is tolerated only for
|
|
635
|
+
the existing `EPERM`/`EACCES`/`ENOENT` cases when the same exact pinned directory
|
|
636
|
+
has concurrently become safe and accessible; it emits no repair warning.
|
|
637
|
+
|
|
638
|
+
There is no search-only descriptor or `/proc` fallback. A newly created `000`
|
|
639
|
+
directory that cannot be opened read-only is left in place; the resolver tries
|
|
640
|
+
the secure fallback or throws. Actual Windows uses directory-type and access
|
|
641
|
+
checks and performs no POSIX chmod, regardless of an injected `platform` value.
|
|
642
|
+
Failures keep the ordinary `Error` contract; available underlying repair and
|
|
643
|
+
close failures are retained in `cause`, including paired errors.
|
|
644
|
+
|
|
645
|
+
The optional descriptor adapter is a complete authority bundle:
|
|
646
|
+
|
|
647
|
+
```ts
|
|
648
|
+
type SecureTempRootDescriptorAdapter = {
|
|
649
|
+
lstatSync(path: string, options: { bigint: true }): Pick<import("node:fs").BigIntStats,
|
|
650
|
+
"dev" | "ino" | "uid" | "mode" | "isDirectory" | "isSymbolicLink">;
|
|
651
|
+
fstatSync(fd: number, options: { bigint: true }): ReturnType<SecureTempRootDescriptorAdapter["lstatSync"]>;
|
|
652
|
+
openSync(path: string, flags: number): number;
|
|
653
|
+
fchmodSync(fd: number, mode: number): void;
|
|
654
|
+
closeSync(fd: number): void;
|
|
655
|
+
constants: { O_RDONLY: number; O_DIRECTORY: number; O_NOFOLLOW: number; O_NONBLOCK: number };
|
|
656
|
+
};
|
|
657
|
+
```
|
|
658
|
+
|
|
659
|
+
Options, adapter functions, and flags are captured synchronously before callbacks.
|
|
660
|
+
On POSIX a complete bundle supplies exact admission and repair observations,
|
|
661
|
+
taking precedence over the legacy `lstatSync` hook. Partial bundles or unavailable
|
|
662
|
+
flags make repair/finalization unavailable. Injecting `lstatSync`, `accessSync`,
|
|
663
|
+
or `mkdirSync` disables the default host descriptor bundle. Injected observations
|
|
664
|
+
also require an explicit `mkdirSync` for creation; supplying a descriptor bundle
|
|
665
|
+
likewise never implicitly authorizes host mkdir. A custom mkdir requires the
|
|
666
|
+
complete descriptor bundle for POSIX finalization. The deprecated `chmodSync`
|
|
667
|
+
option is inert and alone does not disable normal host behavior.
|
|
668
|
+
|
|
669
|
+
These checks bind chmod to the admitted object and reject observed replacements;
|
|
670
|
+
the returned path is not a retained capability. Pathname access and identity
|
|
671
|
+
checks remain separate syscalls, and another process can replace the path after
|
|
672
|
+
the final check. Applications still need a trusted namespace or OS isolation
|
|
673
|
+
when other processes can mutate it.
|
|
674
|
+
|
|
451
675
|
## Common patterns
|
|
452
676
|
|
|
453
677
|
### Build something, atomically place it
|
package/docs/test-hooks.md
CHANGED
|
@@ -31,10 +31,17 @@ type FsSafeTestHooks = {
|
|
|
31
31
|
afterPreOpenLstat?: (filePath: string) => Promise<void> | void;
|
|
32
32
|
beforeOpen?: (filePath: string, flags: number) => Promise<void> | void;
|
|
33
33
|
afterOpen?: (filePath: string, handle: FileHandle) => Promise<void> | void;
|
|
34
|
+
afterOpenedPathIdentityCheck?: (filePath: string, handle: FileHandle) => Promise<void> | void;
|
|
35
|
+
afterRootReadPathResolution?: (filePath: string) => Promise<void> | void;
|
|
36
|
+
beforeRootReadFinalFence?: (filePath: string, handle: FileHandle) => Promise<void> | void;
|
|
37
|
+
afterRootReadFinalPathIdentityCheck?: (filePath: string, handle: FileHandle) => void;
|
|
34
38
|
beforeArchiveOutputMutation?: (operation: "mkdir" | "chmod", targetPath: string) => Promise<void> | void;
|
|
35
39
|
beforeFileStorePruneDescend?: (dirPath: string) => Promise<void> | void;
|
|
36
40
|
beforeFileStoreSyncPrivateWrite?: (filePath: string) => void;
|
|
37
41
|
beforeRootFallbackMutation?: (operation: "mkdir" | "move" | "remove", targetPath: string) => Promise<void> | void;
|
|
42
|
+
beforeRootStatObservation?: (targetPath: string) => Promise<void> | void;
|
|
43
|
+
beforeRootStatInitialObservation?: (targetPath: string) => Promise<void> | void;
|
|
44
|
+
beforeRootListObservation?: (directoryPath: string, withFileTypes: boolean) => Promise<void> | void;
|
|
38
45
|
afterPinnedWriteFallbackRename?: (targetPath: string) => Promise<void> | void;
|
|
39
46
|
beforeSiblingTempWrite?: (tempPath: string) => Promise<void> | void;
|
|
40
47
|
beforeSidecarLockSnapshotOpen?: (lockPath: string) => Promise<void> | void;
|
|
@@ -49,10 +56,17 @@ type FsSafeTestHooks = {
|
|
|
49
56
|
| `afterPreOpenLstat` | A pre-open `lstat` has just resolved. Use this to swap a path between validation and open. |
|
|
50
57
|
| `beforeOpen` | The library is about to call `open(path, flags)`. Use this to inject a TOCTOU window. |
|
|
51
58
|
| `afterOpen` | An open just succeeded. Use this to mutate state before the post-open identity check runs. |
|
|
59
|
+
| `afterOpenedPathIdentityCheck` | A standalone local-file or absolute copy-source descriptor matches its pathname and the generic opened-path resolver is about to run. Root reads use their final admission hooks instead. |
|
|
60
|
+
| `afterRootReadPathResolution` | A Root read path was resolved and has not yet entered local-file open admission. |
|
|
61
|
+
| `beforeRootReadFinalFence` | A Root read descriptor passed pre-open/descriptor identity and hardlink checks and is about to enter its single final root/path/canonical-path/root admission fence. |
|
|
62
|
+
| `afterRootReadFinalPathIdentityCheck` | The final pathname-to-descriptor comparison passed and the second root check has not run. This hook is synchronous-only. |
|
|
52
63
|
| `beforeArchiveOutputMutation` | Archive staging is about to create a directory or apply a mode. |
|
|
53
64
|
| `beforeFileStorePruneDescend` | File-store pruning is about to descend into a directory. |
|
|
54
65
|
| `beforeFileStoreSyncPrivateWrite` | A synchronous private-store write is about to mutate its target. |
|
|
55
66
|
| `beforeRootFallbackMutation` | A guarded JS root fallback is about to mkdir, move, or remove. |
|
|
67
|
+
| `beforeRootStatObservation` | `Root.stat()` admitted its exact target and parent and is about to collect returned metadata. |
|
|
68
|
+
| `beforeRootStatInitialObservation` | `Root.stat()` admitted its exact parent and is about to inspect the target for the first time. |
|
|
69
|
+
| `beforeRootListObservation` | `Root.list()` admitted its exact selected directory and is about to collect names and optional metadata. |
|
|
56
70
|
| `afterPinnedWriteFallbackRename` | A fallback rename committed and post-commit identity checks have not run yet. |
|
|
57
71
|
| `beforeSiblingTempWrite` | A sibling temp file exists and its writer is about to run. |
|
|
58
72
|
| `beforeSidecarLockSnapshotOpen` | A sidecar lock was inspected and is about to be opened for a bounded snapshot read. |
|
package/docs/writing.md
CHANGED
|
@@ -29,8 +29,8 @@ await fs.mkdir("snapshots/2026/05");
|
|
|
29
29
|
## What replacement writes do
|
|
30
30
|
|
|
31
31
|
1. Resolve the relative target against the canonical root and reject anything that escapes (`outside-workspace`).
|
|
32
|
-
2. If `mkdir: true`, create missing parent directories relative to a pinned parent fd in the native path, or with per-component identity guards in the JavaScript fallback.
|
|
33
|
-
3. Pin or guard the parent directory for the selected mechanism. Native operations use a parent fd;
|
|
32
|
+
2. If `mkdir: true`, create missing parent directories relative to a pinned parent fd in the native path, or with per-component identity guards in the JavaScript fallback. When `denyMutations` or an explicit `mutationSymlinks` policy is present, each missing POSIX-native component is authorized before creation and its opened descriptor is authorized again before descent.
|
|
33
|
+
3. Pin or guard the parent directory for the selected mechanism. Native operations use a parent fd and reapply configured mutation policy to the actual canonical destination selected by that descriptor; the pinned JavaScript fallback verifies directory identity before and after mutation and performs the same policy revalidation before its pathname dispatch. The JavaScript check cannot make the intervening pathname syscall atomic, so a same-privilege peer that can replace the parent may cause an out-of-root side effect before detection. Use native `require` mode for that threat model; see the [security model](security-model.md#symlinks-write-side).
|
|
34
34
|
4. Write data to a sibling temp file in the same directory.
|
|
35
35
|
5. Atomically rename the temp file over the destination.
|
|
36
36
|
6. Stat the resulting fd and verify identity.
|
|
@@ -74,7 +74,7 @@ await fs.write(".env", "x"); // throws FsSafeError code "denied-path"
|
|
|
74
74
|
await fs.remove(".ssh/id_rsa"); // throws FsSafeError code "denied-path"
|
|
75
75
|
```
|
|
76
76
|
|
|
77
|
-
`paths` blocks exact absolute paths. `prefixes` blocks absolute paths and everything below them. fs-safe preserves path strings exactly and canonicalizes through existing ancestors before comparing, so a mutation through a symlinked ancestor to a denied path is still denied. Root-level and per-call policies are additive; per-call policy can add denies, but cannot clear root defaults.
|
|
77
|
+
`paths` blocks exact absolute paths. `prefixes` blocks absolute paths and everything below them. For POSIX pinned `write`, `create`, and `copyIn`, an existing exact-path directory does not implicitly deny a mutation to its descendants, but creating a missing directory at that exact path is itself denied. fs-safe preserves path strings exactly and canonicalizes through existing ancestors before comparing, so a mutation through a symlinked ancestor to a denied path is still denied. Root-level and per-call policies are additive; per-call policy can add denies, but cannot clear root defaults. Those POSIX pinned operations snapshot the merged policy before their first path observation, then authorize the actual descriptor-selected parent before staging or publication. A caller mutation of the original arrays, or a contained parent redirect after preflight, cannot clear that admission check.
|
|
78
78
|
|
|
79
79
|
## Write verbs
|
|
80
80
|
|
|
@@ -268,16 +268,35 @@ await fs.move("incoming/foo.txt", "archive/foo.txt", { overwrite: true });
|
|
|
268
268
|
|
|
269
269
|
Both `from` and `to` are bounded; `..` in either is rejected.
|
|
270
270
|
|
|
271
|
-
The
|
|
272
|
-
|
|
273
|
-
|
|
271
|
+
The default no-clobber mode requires the native helper. It admits both parent
|
|
272
|
+
directory descriptors and performs a descriptor-relative no-replace rename, so
|
|
273
|
+
a competitor that creates the target first is preserved and the source remains
|
|
274
|
+
in place. If the helper or safe parent admission is unavailable, the call fails
|
|
275
|
+
with `helper-unavailable`; it never falls back to a check followed by a
|
|
276
|
+
replacing rename. After dispatch it rechecks both parent identities, so a
|
|
277
|
+
post-operation rejection can mean the no-replace rename completed. Directory
|
|
278
|
+
moves continue to require `overwrite: true`.
|
|
279
|
+
|
|
280
|
+
Both selected canonical endpoints are admitted inside the retained Root after
|
|
281
|
+
native parent admission. With `mutationSymlinks: "reject"`, both full operation
|
|
282
|
+
paths are rechecked after the live mutation-authority callback and before
|
|
283
|
+
dispatch. The Root and retained parents are fenced again after any such callback.
|
|
284
|
+
These checks retain the documented final check-to-syscall race.
|
|
285
|
+
|
|
286
|
+
For `{ overwrite: true }`, the JavaScript path checks both parent directories
|
|
287
|
+
before and after the rename. A failed post-operation check rejects even though
|
|
288
|
+
the rename may already have completed; rejection does not imply rollback.
|
|
274
289
|
|
|
275
290
|
### `fs.remove(rel)`
|
|
276
291
|
|
|
277
292
|
Unlink a file or `rmdir` an empty directory. Non-empty directories throw `not-empty`. For atomic directory replacement, use [`replaceDirectoryAtomic`](atomic.md#replacedirectoryatomic).
|
|
278
293
|
|
|
279
|
-
|
|
280
|
-
|
|
294
|
+
Before a nonrecursive JavaScript fallback removal, fs-safe retains exact
|
|
295
|
+
Root-to-parent directory identities, with canonical endpoint checks at Root and
|
|
296
|
+
the immediate parent. It rejects a parent redirected through a symlink or
|
|
297
|
+
junction before `unlink` or `rmdir`, even with `force: true`, and rechecks the
|
|
298
|
+
retained ancestry after the operation settles. The entry may already have been
|
|
299
|
+
removed when that final verification rejects.
|
|
281
300
|
|
|
282
301
|
```ts
|
|
283
302
|
await fs.remove("logs/yesterday.log");
|
|
@@ -426,6 +445,13 @@ opens, including for mode `0o200` files; replacement truncation happens only
|
|
|
426
445
|
after type, identity, and boundary checks pass. Rejected existing paths are
|
|
427
446
|
never cleanup-owned or unlinked.
|
|
428
447
|
|
|
448
|
+
Identity admission uses bigint descriptor and pathname receipts even though the
|
|
449
|
+
public `stat` field remains a numeric Node `Stats` object. Windows retries an
|
|
450
|
+
unknown device or file index once while retaining known components, then rejects
|
|
451
|
+
persistent ambiguity. If admission of a newly created file fails, cleanup only
|
|
452
|
+
unlinks a pathname that still has the exact created identity; rounded aliases,
|
|
453
|
+
symlinks, and unknown identities are preserved.
|
|
454
|
+
|
|
429
455
|
## Write defaults vs per-call options
|
|
430
456
|
|
|
431
457
|
Set `mkdir: true` once on `root()`; pass text encodings per call when needed:
|
|
@@ -507,6 +533,10 @@ await fs.write("state.json", body); // succeeds on rclone FUSE
|
|
|
507
533
|
|
|
508
534
|
**Security note.** `verify-content-with-lock` proves that the bytes observed after rename match the requested write and prevents *cooperating* writers from interleaving. It does **not** prove that the destination still names the temp-file object, retain fd-relative parent pinning, or stop a same-UID process that ignores the advisory lock. Do not use this option on directories writable by untrusted same-UID processes. Strict identity verification remains the default.
|
|
509
535
|
|
|
536
|
+
Windows `Root.write()` and `Root.writeJson()` honor this policy both as a Root default and as a per-call option. The Windows buffered writer acquires the same Root compatibility lock before creating parents, placeholders, or content. It keeps staging writable until publication, then applies the final mode through the retained destination descriptor. When content verification accepts a changed rename identity, that destination remains pinned through file sync, parent sync, and final strict identity checks.
|
|
537
|
+
|
|
538
|
+
The Windows buffered compatibility path resolves permitted in-root aliases before choosing its lock and binds publication to that effective destination. With the existing lock protocol, effective path components beneath the Root must contain only lower-case ASCII letters, digits, `.`, `_`, or `-`, with no trailing `.`. Unsupported spellings, including missing upper-case or non-ASCII names, fail with `path-alias` before mutation; no filesystem case-sensitivity or Unicode-folding behavior is guessed. This restriction does not apply to strict writes. Opaque Windows pathname identities still use strict verification against the retained original descriptor: they never, by themselves, authorize content-based acceptance of a replacement.
|
|
539
|
+
|
|
510
540
|
Lock recovery is fail-closed. If a process crashes and leaves the root-level `.fs-safe-write-<sha256>.lock`, a later write reports the stale lock instead of deleting it based on a host-local PID. Recover only under external authority that excludes every competing writer; see [File lock](sidecar-lock.md#stale-recovery-guarded-remove-if-unchanged).
|
|
511
541
|
|
|
512
542
|
## See also
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@openclaw/fs-safe",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.13.1",
|
|
4
4
|
"description": "Capability-style filesystem roots for Node.js apps that handle untrusted relative paths.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"filesystem",
|
|
@@ -167,18 +167,18 @@
|
|
|
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.13.1",
|
|
171
|
+
"@openclaw/fs-safe-darwin-x64": "0.13.1",
|
|
172
|
+
"@openclaw/fs-safe-linux-arm64-gnu": "0.13.1",
|
|
173
|
+
"@openclaw/fs-safe-linux-arm64-musl": "0.13.1",
|
|
174
|
+
"@openclaw/fs-safe-linux-x64-gnu": "0.13.1",
|
|
175
|
+
"@openclaw/fs-safe-linux-x64-musl": "0.13.1",
|
|
176
|
+
"@openclaw/fs-safe-win32-x64-msvc": "0.13.1",
|
|
177
177
|
"jszip": "^3.10.2"
|
|
178
178
|
},
|
|
179
179
|
"devDependencies": {
|
|
180
180
|
"@emnapi/runtime": "2.0.0-alpha.5",
|
|
181
|
-
"@napi-rs/cli": "3.9.
|
|
181
|
+
"@napi-rs/cli": "3.9.1",
|
|
182
182
|
"@types/node": "^26.5.1",
|
|
183
183
|
"@vitest/coverage-v8": "5.0.0",
|
|
184
184
|
"fast-check": "^4.9.0",
|
|
@@ -188,7 +188,7 @@
|
|
|
188
188
|
"sigstore": "5.0.0",
|
|
189
189
|
"tar": "7.5.22",
|
|
190
190
|
"typescript": "^7.0.2",
|
|
191
|
-
"vite": "8.
|
|
191
|
+
"vite": "8.3.0",
|
|
192
192
|
"vitest": "^5.0.0"
|
|
193
193
|
},
|
|
194
194
|
"engines": {
|