@openclaw/fs-safe 0.11.0 → 0.13.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 +119 -0
- package/README.md +15 -8
- package/dist/absolute-path.d.ts.map +1 -1
- package/dist/absolute-path.js +32 -11
- package/dist/advanced.d.ts +3 -1
- package/dist/advanced.d.ts.map +1 -1
- package/dist/advanced.js +3 -1
- package/dist/archive-entry.d.ts.map +1 -1
- package/dist/archive-entry.js +4 -3
- package/dist/archive-gzip-tail.d.ts.map +1 -1
- package/dist/archive-gzip-tail.js +13 -9
- package/dist/archive-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 +55 -46
- 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 +50 -17
- 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/bounded-read.js +2 -2
- 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/device-path.d.ts.map +1 -1
- package/dist/device-path.js +5 -3
- 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 +12 -2
- 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-prune.d.ts.map +1 -1
- package/dist/file-store-prune.js +9 -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 +1 -0
- package/dist/filename.d.ts.map +1 -1
- package/dist/filename.js +85 -23
- package/dist/fs.d.ts.map +1 -1
- package/dist/fs.js +3 -2
- package/dist/guarded-mkdir.d.ts +10 -0
- package/dist/guarded-mkdir.d.ts.map +1 -1
- package/dist/guarded-mkdir.js +217 -33
- package/dist/guest.d.ts.map +1 -1
- package/dist/guest.js +16 -5
- package/dist/home-dir.d.ts.map +1 -1
- package/dist/home-dir.js +73 -10
- package/dist/install-path.d.ts +6 -0
- package/dist/install-path.d.ts.map +1 -1
- package/dist/install-path.js +67 -19
- package/dist/json-durable-queue-ownership.d.ts +8 -0
- package/dist/json-durable-queue-ownership.d.ts.map +1 -1
- package/dist/json-durable-queue-ownership.js +134 -43
- 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 +6 -0
- package/dist/json-durable-queue-read.d.ts.map +1 -0
- package/dist/json-durable-queue-read.js +61 -0
- package/dist/json-durable-queue.d.ts +1 -1
- package/dist/json-durable-queue.d.ts.map +1 -1
- package/dist/json-durable-queue.js +83 -134
- 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 +21 -0
- package/dist/native-binding.d.ts.map +1 -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-parent-admission.d.ts +31 -0
- package/dist/native-parent-admission.d.ts.map +1 -0
- package/dist/native-parent-admission.js +124 -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 +0 -3
- package/dist/native-pinned-write.d.ts.map +1 -1
- package/dist/native-pinned-write.js +309 -40
- 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 +14 -0
- package/dist/path-scope-lexical.d.ts.map +1 -0
- package/dist/path-scope-lexical.js +37 -0
- 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 +35 -6
- 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 +152 -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 +33 -29
- 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 +29 -0
- package/dist/root-boundary.d.ts.map +1 -0
- package/dist/root-boundary.js +182 -0
- package/dist/root-context.d.ts +12 -1
- package/dist/root-context.d.ts.map +1 -1
- package/dist/root-context.js +89 -16
- 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 +182 -33
- 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 +316 -325
- 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-move-preflight.d.ts +8 -0
- package/dist/root-move-preflight.d.ts.map +1 -0
- package/dist/root-move-preflight.js +16 -0
- package/dist/root-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 +2 -0
- package/dist/root-path-existing.d.ts.map +1 -1
- package/dist/root-path-existing.js +63 -22
- 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 +13 -0
- package/dist/root-path.d.ts.map +1 -1
- package/dist/root-path.js +263 -73
- 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 -29
- package/dist/root-paths.d.ts.map +1 -1
- package/dist/root-paths.js +89 -178
- 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-walk.d.ts.map +1 -1
- package/dist/root-walk.js +2 -1
- 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-mode.d.ts +2 -0
- package/dist/root-write-mode.d.ts.map +1 -1
- package/dist/root-write-mode.js +20 -7
- package/dist/root-write-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 +18 -7
- package/dist/safe-path-segment.d.ts.map +1 -1
- package/dist/safe-path-segment.js +3 -1
- 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-windows.d.ts +10 -0
- package/dist/secure-file-windows.d.ts.map +1 -0
- package/dist/secure-file-windows.js +186 -0
- package/dist/secure-file.d.ts.map +1 -1
- package/dist/secure-file.js +98 -10
- 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 -1
- package/dist/sibling-staged-file.d.ts.map +1 -1
- package/dist/sibling-staged-file.js +181 -50
- package/dist/sibling-temp.d.ts.map +1 -1
- package/dist/sibling-temp.js +27 -12
- package/dist/sidecar-lock-acquire.d.ts.map +1 -1
- package/dist/sidecar-lock-acquire.js +45 -15
- package/dist/sidecar-lock-root.d.ts.map +1 -1
- package/dist/sidecar-lock-root.js +4 -2
- package/dist/sidecar-lock.d.ts.map +1 -1
- package/dist/sidecar-lock.js +2 -3
- 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 +133 -4
- 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 +20 -16
- 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 +28 -1
- package/docs/archive.md +29 -2
- package/docs/atomic.md +27 -3
- package/docs/contributing.md +4 -1
- package/docs/copy.md +4 -1
- package/docs/durability.md +18 -2
- package/docs/errors.md +14 -6
- package/docs/file-store.md +30 -3
- package/docs/filename.md +21 -7
- package/docs/guest.md +5 -0
- package/docs/install-path.md +61 -15
- package/docs/install.md +12 -8
- 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 +28 -9
- package/docs/native.md +46 -10
- 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 +12 -1
- package/docs/permissions.md +26 -1
- package/docs/private-file-store.md +14 -0
- package/docs/public-api.md +14 -0
- package/docs/quickstart.md +1 -1
- package/docs/reading.md +8 -1
- package/docs/root.md +21 -4
- package/docs/secret-file.md +3 -0
- package/docs/secure-file.md +21 -16
- package/docs/security-model.md +76 -9
- package/docs/sidecar-lock.md +28 -1
- package/docs/store.md +27 -2
- package/docs/temp.md +262 -34
- package/docs/test-hooks.md +14 -0
- package/docs/writing.md +38 -8
- package/package.json +10 -10
package/docs/native.md
CHANGED
|
@@ -50,12 +50,40 @@ whether a path, archive entry, mode, owner, or cleanup policy is acceptable.
|
|
|
50
50
|
race-atomic. macOS uses `renameatx_np(RENAME_EXCL)` and permits
|
|
51
51
|
`fclonefileat` in an owned, non-shared parent. The clone is normalized inside
|
|
52
52
|
a private staging directory: flags, ACLs, extended attributes, and broad mode
|
|
53
|
-
bits are cleared before no-replace publication.
|
|
53
|
+
bits are cleared before no-replace publication. Clone admission obtains mode,
|
|
54
|
+
owner, exact identity, flags, and ACL state together from the retained descriptor;
|
|
55
|
+
immutable receipts are compared across the private staging operation and against
|
|
56
|
+
fresh no-follow pathname identity reads. Any extended entry on the target parent
|
|
57
|
+
or private staging directory is rejected before cloning bytes. The payload's
|
|
58
|
+
cleared ACL and normalized descriptor facts are verified before publication and
|
|
59
|
+
again, with a fresh published-name identity fence, before its descriptor is
|
|
60
|
+
returned. Unsupported admission before payload
|
|
61
|
+
creation or an unsupported clone syscall may still select the documented
|
|
62
|
+
ordinary-copy path. After the clone creates bytes, normalization and security
|
|
63
|
+
verification failures report terminal `EIO`, retaining the original error
|
|
64
|
+
detail; successful cleanup does not make them eligible for ordinary-copy retry.
|
|
65
|
+
Cleanup failures remain secondary diagnostics, and already-terminal publication
|
|
66
|
+
errors such as `EEXIST` retain their status.
|
|
54
67
|
- Windows uses handle-relative `NtCreateFile` with `OBJ_DONT_REPARSE` and
|
|
55
68
|
`FILE_OPEN_REPARSE_POINT`, then explicitly rejects reparse points. Rename and
|
|
56
69
|
hardlink operations stay rooted in already-open handles. Owner/DACL reads
|
|
57
70
|
use `GetSecurityInfo`; private directories receive their protected DACL in
|
|
58
|
-
|
|
71
|
+
an exclusive, handle-relative `NtCreateFile` call. Their created handles remain
|
|
72
|
+
open through ACL and pathname-association checks and own any failure cleanup.
|
|
73
|
+
N-API descriptors cross into and out of
|
|
74
|
+
this layer only through the host executable's paired libuv descriptor bridge;
|
|
75
|
+
missing or partial exports fail with `ENOTSUP` instead of trying a raw HANDLE
|
|
76
|
+
or add-on CRT descriptor namespace.
|
|
77
|
+
|
|
78
|
+
The internal macOS `inspectDarwinAcl(fd)` capability reports `absent`, `empty`,
|
|
79
|
+
or `present` for the opened object's extended ACL. It synchronously owns a
|
|
80
|
+
close-on-exec duplicate for inspection, leaves the caller's descriptor and file
|
|
81
|
+
position alone, and never reopens a pathname. Darwin's `acl_get_entry` returns
|
|
82
|
+
zero for an entry; end-of-list is accepted only for the first entry of a valid,
|
|
83
|
+
privately owned empty ACL. Unsupported, malformed, and failed inspection is not
|
|
84
|
+
reported as absence. These facts do not classify individual ACE permissions,
|
|
85
|
+
prove volume ownership enforcement, or add ACL enforcement to private writers
|
|
86
|
+
and secure readers outside the clone path.
|
|
59
87
|
|
|
60
88
|
## Archives
|
|
61
89
|
|
|
@@ -125,22 +153,30 @@ All routes preserve `wx` semantics and the same source/target identity and
|
|
|
125
153
|
SHA-256 fencing. Native hashing and Linux whole-file copying run on N-API async
|
|
126
154
|
workers rather than the JavaScript event loop.
|
|
127
155
|
|
|
156
|
+
Linux range copying confirms every zero-byte result with a positioned source
|
|
157
|
+
read at the current transfer offset, including after earlier calls copied data.
|
|
158
|
+
If readable bytes remain, automatic Root copying resumes its byte loop from
|
|
159
|
+
that offset; exclusive publication removes its partial target before retrying
|
|
160
|
+
the guarded byte-copy fallback. EOF checks preserve descriptor cursors and do
|
|
161
|
+
not bypass the byte limit.
|
|
162
|
+
|
|
128
163
|
## Mode semantics
|
|
129
164
|
|
|
130
165
|
| Mode | Native loading | Fallback |
|
|
131
166
|
|---|---|---|
|
|
132
|
-
| `auto` | Try once, cache the result | Use guarded JavaScript when
|
|
167
|
+
| `auto` | Try once, cache the result | Use guarded JavaScript when safe; reject native-only operations |
|
|
133
168
|
| `require` | Try once, cache the result | Throw `FsSafeError("helper-unavailable")` |
|
|
134
|
-
| `off` | Never attempt a binding load |
|
|
169
|
+
| `off` | Never attempt a binding load | Use guarded JavaScript when safe; reject native-only operations |
|
|
135
170
|
|
|
136
171
|
`sha256FileSync()` is a synchronous Node implementation in all three modes and
|
|
137
172
|
does not load the binding. Use asynchronous `sha256File()` for native hashing
|
|
138
173
|
and cancellation that can respond while JavaScript callbacks run.
|
|
139
174
|
|
|
140
|
-
Features without a safe JavaScript implementation, including
|
|
141
|
-
Windows private-directory creation, and
|
|
142
|
-
fail with `helper-unavailable`
|
|
143
|
-
is currently Linux/macOS only and
|
|
175
|
+
Features without a safe JavaScript implementation, including no-clobber
|
|
176
|
+
`Root.move()`, zstd/bzip2 TAR, Windows private-directory creation, and
|
|
177
|
+
[retained-directory staging](staged-file.md), fail with `helper-unavailable`
|
|
178
|
+
when native support is absent or off. Staging is currently Linux/macOS only and
|
|
179
|
+
rejects Windows with `unsupported-platform`.
|
|
144
180
|
|
|
145
181
|
The staged-file owner also serves POSIX native pinned writes, including streaming.
|
|
146
182
|
Unpublished files remain at `0600`; requested modes are applied through the
|
|
@@ -177,12 +213,12 @@ remain TypeScript-owned. What changes is the syscall strength or availability:
|
|
|
177
213
|
|
|
178
214
|
| Capability | Native path | Guarded JavaScript path |
|
|
179
215
|
|---|---|---|
|
|
180
|
-
| Root-relative opens/mutations | Descriptor-relative beneath operations. Pinned writes create parents and publish both replacement and no-replace targets relative to open directory descriptors. Linux reports `kernel-atomic`; macOS and Windows report `best-effort`. macOS uses `O_RESOLVE_BENEATH` when available plus an `F_GETPATH` detector, while Windows rejects reparse traversal in the object-manager call. | Reports `best-effort`: component-wise alias checks, no-follow opens where Node exposes them, private temp/rename, and post-operation identity verification. A same-privilege peer can replace a writable parent after a guard assertion but before Node resolves
|
|
216
|
+
| Root-relative opens/mutations | Descriptor-relative beneath operations. Pinned writes create parents and publish both replacement and no-replace targets relative to open directory descriptors. No-clobber `Root.move()` admits both parents and uses the native no-replace rename. Linux reports `kernel-atomic`; macOS and Windows report `best-effort`. macOS uses `O_RESOLVE_BENEATH` when available plus an `F_GETPATH` detector, while Windows rejects reparse traversal in the object-manager call. | Reports `best-effort`: component-wise alias checks, no-follow opens where Node exposes them, private temp/rename, and post-operation identity verification. No-clobber `Root.move()` is unsupported because a check followed by a replacing rename is unsafe. A same-privilege peer can replace a writable parent after a guard assertion but before Node resolves another pathname mutation; the mutation may land outside the intended root before the post-check detects it. |
|
|
181
217
|
| ZIP/TAR/gzip | Rust streaming decode and fd-relative output creation. | Optional JSZip or bundled WASM TAR into guarded private staging, then the same guarded merge policy. |
|
|
182
218
|
| Zstd/bzip2 TAR | Supported. | Unsupported; typed `helper-unavailable`. |
|
|
183
219
|
| Publication copy | Clone, Linux `copy_file_range`, async native SHA-256. | Exclusive `wx` byte loop and Node SHA-256 with the same content/identity fences. |
|
|
184
220
|
| `rename-noreplace` | Atomic platform no-replace rename. | Unsupported; no emulation by check-then-rename. |
|
|
185
|
-
| Windows DACL read | Direct `GetSecurityInfo`; the public facts API exposes ordered basic allow/deny ACE SIDs, masks, and decoded flags without trust policy. | Structured .NET owner/DACL inspection
|
|
221
|
+
| Windows DACL read | Direct `GetSecurityInfo`; the public facts API exposes ordered basic allow/deny ACE SIDs, masks, and decoded flags without trust policy. Secure-file reads query the borrowed open descriptor and compare its 32-bit volume serial and 64-bit file-index projection with Node's bigint receipt. | Structured .NET owner/DACL inspection remains available to standalone pathname reporting. Secure-file reads fail closed without the descriptor capability. |
|
|
186
222
|
| Windows private directory | Creation-time protected DACL. | Unsupported; no weaker pathname-only substitute. |
|
|
187
223
|
|
|
188
224
|
Use `off` in CI to keep the fallback contract exercised. Use `require` when a
|
package/docs/output.md
CHANGED
|
@@ -69,13 +69,23 @@ when needed as described above. Guarded temporary files used only inside
|
|
|
69
69
|
fs-safe have independent names so their length does not grow with the
|
|
70
70
|
destination basename.
|
|
71
71
|
|
|
72
|
+
On Windows, `rootDir` and every target parent reject NTFS alternate-stream and
|
|
73
|
+
directory-index namespace spellings such as `file:stream` and
|
|
74
|
+
`dir::$INDEX_ALLOCATION`. A colon in only the requested basename still follows
|
|
75
|
+
the documented portable filename sanitization above instead of being treated
|
|
76
|
+
as a raw stream path. Ordinary colon-bearing POSIX roots and parents remain
|
|
77
|
+
valid.
|
|
78
|
+
|
|
72
79
|
## Choosing a staging mode
|
|
73
80
|
|
|
74
81
|
`staging: "workspace"` is the default. The producer writes in private temp
|
|
75
82
|
storage, then fs-safe copies through the guarded root boundary. Choose it when
|
|
76
83
|
the temp and destination filesystems may differ, or when an externally produced
|
|
77
84
|
partial file must never appear in the destination directory. The final target
|
|
78
|
-
still appears only after guarded finalization.
|
|
85
|
+
still appears only after guarded finalization. Its internal `tempFile()` does
|
|
86
|
+
not expose `cleanupSafety` through this API and uses compatible cleanup, with
|
|
87
|
+
the final check-to-pathname-recursive-removal gap documented in
|
|
88
|
+
[`tempFile`](temp.md#tempfile).
|
|
79
89
|
|
|
80
90
|
By default, `staging: "sibling"` gives the producer a randomized temp path in
|
|
81
91
|
the target directory. Choose it only when that directory itself is the approved writable
|
|
@@ -104,19 +114,31 @@ absent file path inside a private child workspace under the target parent, on
|
|
|
104
114
|
the target filesystem. Directory cleanup ownership is captured before the
|
|
105
115
|
callback. A callback exception triggers owned workspace cleanup, including
|
|
106
116
|
partial output, subject to directory identity checks and I/O failures.
|
|
107
|
-
After success,
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
117
|
+
After success, an available native helper uses guarded no-replace `Root.move`.
|
|
118
|
+
With native mode off, the helper opens and identity-fences the completed regular
|
|
119
|
+
file, creates the randomized sibling through an atomic no-clobber hard link,
|
|
120
|
+
verifies its temporary two-link state, then removes the private name. Windows
|
|
121
|
+
first transfers the pin to an independently verified sibling descriptor so the
|
|
122
|
+
source name can disappear before workspace cleanup, including on runtimes with
|
|
123
|
+
legacy deletion behavior. If that unlink clears the producer's read-only
|
|
124
|
+
attribute, the retained descriptor restores it and the helper verifies mode and
|
|
125
|
+
identity before continuing. This keeps the handoff zero-copy while preserving the
|
|
126
|
+
ordinary retained-descriptor, mode,
|
|
127
|
+
file-sync, and final-rename lifecycle. Escaping symlinks still fail with
|
|
128
|
+
`path-alias`; filesystems without hard-link support fail with
|
|
129
|
+
`helper-unavailable`.
|
|
112
130
|
|
|
113
131
|
Exact bigint parent and workspace identities are rechecked before moving
|
|
114
132
|
output to the sibling path to reject observed replacements. Cleanup uses the
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
133
|
+
compatible [`withTempFile` ownership contract](temp.md#withtempfile); this
|
|
134
|
+
output option does not expose `cleanupSafety: "require-bounded"`. A moved or
|
|
135
|
+
replaced parent or workspace can leave artifacts, and a workspace substituted
|
|
136
|
+
in the final check-to-pathname-recursive-removal gap can redirect traversal.
|
|
137
|
+
The option does not promise cleanup through a retained directory after a
|
|
138
|
+
rename. The existing Windows and JavaScript pathname-guard limitations remain,
|
|
139
|
+
with no additional permissions or durability guarantee. Native-off publication
|
|
140
|
+
is supported only where hard links are available. See the
|
|
141
|
+
[producer-isolation contract](temp.md#sibling-temp-writes)
|
|
120
142
|
for cleanup and pathname-race details. The option affects only `staging: "sibling"`;
|
|
121
143
|
with `staging: "workspace"`, it is redundant and harmless because the producer
|
|
122
144
|
already uses a private workspace. Omitting it leaves both staging defaults
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
# Resolving an existing path prefix
|
|
2
|
+
|
|
3
|
+
`resolvePathPrefixSync()` follows filesystem path components until the first
|
|
4
|
+
missing entry. It returns a canonical existing prefix and the unprocessed
|
|
5
|
+
suffix separately, including dangling symlink targets.
|
|
6
|
+
|
|
7
|
+
```ts
|
|
8
|
+
import { resolvePathPrefixSync, type ResolvedPathPrefix } from "@openclaw/fs-safe/advanced";
|
|
9
|
+
|
|
10
|
+
const observed: ResolvedPathPrefix = resolvePathPrefixSync("/srv/data/future/file.json");
|
|
11
|
+
// When /srv/data exists but future does not:
|
|
12
|
+
// observed.existingPath is the canonical spelling of /srv/data.
|
|
13
|
+
// observed.unresolvedSegments is ["future", "file.json"].
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
The result has three readonly fields:
|
|
17
|
+
|
|
18
|
+
| Field | Meaning |
|
|
19
|
+
| --- | --- |
|
|
20
|
+
| `absolutePath` | Input anchored to the current directory or Windows drive, with raw path components retained and Windows separators converted to backslashes. |
|
|
21
|
+
| `existingPath` | Existing prefix canonicalized by fs-safe's native-realpath owner. This may be a file when the entire path exists. |
|
|
22
|
+
| `unresolvedSegments` | Components from the first missing entry onward, after expanding any earlier symlinks. Empty when the entire path exists. |
|
|
23
|
+
|
|
24
|
+
## Physical traversal and missing paths
|
|
25
|
+
|
|
26
|
+
The helper resolves `link/..` from the link's physical target. It does not use
|
|
27
|
+
`path.resolve()` on the full input or a symlink target, because lexical
|
|
28
|
+
normalization would erase that traversal. Relative inputs use the current
|
|
29
|
+
directory; Windows drive-relative inputs use Node's current directory for that
|
|
30
|
+
drive. A root-relative Windows symlink target retains its containing link's
|
|
31
|
+
drive or share root. Windows junctions and full UNC share roots are supported.
|
|
32
|
+
|
|
33
|
+
At the first `ENOENT`, traversal stops. A suffix such as
|
|
34
|
+
`missing/../live.sqlite` remains `["missing", "..", "live.sqlite"]`, even if
|
|
35
|
+
`live.sqlite` exists beside the missing component. Dot components, repeated
|
|
36
|
+
separators, and a trailing separator in the unresolved suffix are retained.
|
|
37
|
+
Normalizing that suffix would invent an alias to a file the filesystem cannot
|
|
38
|
+
reach through the missing directory. The caller owns any application-specific
|
|
39
|
+
comparison or prospective-path policy.
|
|
40
|
+
|
|
41
|
+
## Failures and limits
|
|
42
|
+
|
|
43
|
+
Only `ENOENT` from component inspection produces a missing suffix. Permission,
|
|
44
|
+
I/O, non-directory traversal, symlink-reading, and final canonicalization
|
|
45
|
+
failures propagate. A vanished symlink that was already observed is an error,
|
|
46
|
+
not an unprocessed missing component. NUL bytes reject with `invalid-path`.
|
|
47
|
+
|
|
48
|
+
Resolution rejects with `ELOOP` after 64 symlink expansions or a repeated
|
|
49
|
+
resolution state. The state retains exact bigint device/inode identity and
|
|
50
|
+
the remaining suffix, so revisiting a link with a shorter suffix is permitted.
|
|
51
|
+
Traversing through a non-directory, including `file/..`, `file/.`, or `file/`,
|
|
52
|
+
rejects with `ENOTDIR`.
|
|
53
|
+
|
|
54
|
+
Dot and parent components require the directory's search permission before
|
|
55
|
+
they are collapsed. Native realpath alone does not establish this permission
|
|
56
|
+
on every platform. Empty components from repeated or trailing separators do
|
|
57
|
+
not introduce a `.` lookup.
|
|
58
|
+
|
|
59
|
+
This is a read-only path observation. It neither pins files nor creates a root
|
|
60
|
+
boundary, authorizes access, or guarantees a consistent snapshot during
|
|
61
|
+
concurrent changes. Results can become stale immediately. Use a guarded Root
|
|
62
|
+
operation for subsequent access to untrusted paths; keep any database ownership,
|
|
63
|
+
cache invalidation, and mutation policy with the caller. Native configuration
|
|
64
|
+
and Bun realpath limitations follow the existing [runtime contract](install.md#bun-runtime).
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Path suffix alias probing
|
|
3
|
+
description: "Bounded local observations for selected missing relative path suffixes."
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Path suffix alias probing
|
|
7
|
+
|
|
8
|
+
`probePathSuffixAliasesSync()` observes whether selected missing relative suffixes
|
|
9
|
+
would alias beneath an existing directory. It returns `boolean | undefined`; it
|
|
10
|
+
does not infer filesystem behavior from the operating system or normalize a path
|
|
11
|
+
into an authorization decision.
|
|
12
|
+
|
|
13
|
+
```ts
|
|
14
|
+
import { probePathSuffixAliasesSync } from "@openclaw/fs-safe/advanced";
|
|
15
|
+
|
|
16
|
+
const aliases = probePathSuffixAliasesSync({
|
|
17
|
+
directory: "/trusted/existing-directory",
|
|
18
|
+
left: "Reports/Caf\u00e9",
|
|
19
|
+
right: "reports/Cafe\u0301",
|
|
20
|
+
});
|
|
21
|
+
if (aliases === undefined) {
|
|
22
|
+
// The caller must choose an explicit ambiguity policy.
|
|
23
|
+
}
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
Use this for suffixes that do not yet exist. For local ASCII-case observations,
|
|
27
|
+
including an explicit read-only mode, use
|
|
28
|
+
[`probePathCaseInsensitiveSync()`](path-case.md). Suffix probing has no read-only
|
|
29
|
+
mode: a nontrivial observation can create and remove directories and requires an
|
|
30
|
+
approved writable parent.
|
|
31
|
+
|
|
32
|
+
## API and validation
|
|
33
|
+
|
|
34
|
+
```ts
|
|
35
|
+
type ProbePathSuffixAliasesOptions = {
|
|
36
|
+
directory: string;
|
|
37
|
+
left: string;
|
|
38
|
+
right: string;
|
|
39
|
+
shouldProbeCaseVariants?: (leftNfc: string, rightNfc: string) => boolean;
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
function probePathSuffixAliasesSync(
|
|
43
|
+
options: ProbePathSuffixAliasesOptions,
|
|
44
|
+
): boolean | undefined;
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
The helper reads and validates `directory`, then resolves it to an absolute path
|
|
48
|
+
before reading the suffixes or predicate. Later option getters or the predicate
|
|
49
|
+
cannot retarget a relative directory by changing the working directory. When
|
|
50
|
+
filesystem observations are needed, the directory is canonicalized and its
|
|
51
|
+
identity is checked; an initial directory alias can be followed.
|
|
52
|
+
|
|
53
|
+
Suffixes must have the same number of ordinary relative path components. Empty
|
|
54
|
+
components, `.` and `..`, absolute suffixes, NUL characters, and non-string path
|
|
55
|
+
inputs are rejected with `TypeError`. Windows additionally rejects drive-relative
|
|
56
|
+
components, colons, and reserved device names, including device aliases with
|
|
57
|
+
extensions or trailing ignored characters. On POSIX, backslashes and colons are
|
|
58
|
+
ordinary filename characters. The optional predicate must be a function.
|
|
59
|
+
|
|
60
|
+
Both suffixes and the predicate are validated before the identical-suffix fast
|
|
61
|
+
path. Identical, valid, within-budget suffixes return `true` without filesystem
|
|
62
|
+
access or a predicate call. This does not prove that the directory exists or that
|
|
63
|
+
the suffix can be created.
|
|
64
|
+
|
|
65
|
+
## Fixed resource limits
|
|
66
|
+
|
|
67
|
+
These limits apply to one call and cannot be raised through options:
|
|
68
|
+
|
|
69
|
+
| Resource | Limit | On exceeding the limit |
|
|
70
|
+
|---|---|---|
|
|
71
|
+
| Each supplied suffix | 8,192 UTF-16 code units | `RangeError` before mutation |
|
|
72
|
+
| Supplied and resolved directory paths | 32,768 UTF-16 code units each | `RangeError` before mutation |
|
|
73
|
+
| Each suffix's component count | 32 | `RangeError` before mutation |
|
|
74
|
+
| Directory-creation attempts | 128 | `undefined` after cleanup |
|
|
75
|
+
| Successfully created probe directories | 64 | `undefined` after cleanup |
|
|
76
|
+
| Forward filesystem observations | 4,096 | `undefined` after cleanup |
|
|
77
|
+
| Each generated actual path | 32,768 UTF-16 code units | `undefined` after cleanup |
|
|
78
|
+
|
|
79
|
+
Input limits are checked even for identical suffixes. Dynamic budgets count work
|
|
80
|
+
across the whole call, including collision retries; removing a probe does not
|
|
81
|
+
restore its creation budget. Cleanup is still attempted when a forward budget is
|
|
82
|
+
exhausted and is not disabled by that exhausted budget.
|
|
83
|
+
|
|
84
|
+
Candidates are generated lazily, only as needed. The helper does not eagerly
|
|
85
|
+
allocate every possible probe name or consume randomness for unused retries.
|
|
86
|
+
These are count and string-size limits, not a wall-clock guarantee. Synchronous
|
|
87
|
+
filesystem calls, randomness, and caller code can block; the helper cannot
|
|
88
|
+
interrupt them or promise a maximum elapsed time.
|
|
89
|
+
|
|
90
|
+
## Results and caller policy
|
|
91
|
+
|
|
92
|
+
- `true`: the suffixes are identical, or the requested observations found aliases.
|
|
93
|
+
- `false`: an observation found distinct entries, or the caller's predicate
|
|
94
|
+
excluded a pair.
|
|
95
|
+
- `undefined`: no reliable answer was obtained, including filesystem failures,
|
|
96
|
+
identity changes, exhausted collision candidates or dynamic budgets, generated
|
|
97
|
+
path limits, or incomplete cleanup.
|
|
98
|
+
|
|
99
|
+
`undefined` is not evidence of either case sensitivity or aliasing. The helper
|
|
100
|
+
does not cache observations, choose a fallback, reserve the future destination,
|
|
101
|
+
or establish a root-confinement boundary. A result applies only to the local
|
|
102
|
+
observations made during that call, not to every Unicode pair, mount, or later
|
|
103
|
+
filesystem state.
|
|
104
|
+
|
|
105
|
+
The optional trusted, synchronous `shouldProbeCaseVariants` predicate receives
|
|
106
|
+
NFC-normalized component pairs when they differ and are not equivalent under
|
|
107
|
+
ASCII case folding. Without a predicate, those pairs are eligible for probing.
|
|
108
|
+
It is called in component order. Returning `false` excludes
|
|
109
|
+
that pair and produces `false`; it is caller policy, not a filesystem finding.
|
|
110
|
+
A first-component exclusion needs no probes or randomness. A later exclusion
|
|
111
|
+
can follow earlier mutations, so it does not make the call read-only.
|
|
112
|
+
|
|
113
|
+
A non-boolean predicate result throws `TypeError`; a predicate exception is
|
|
114
|
+
re-thrown unchanged after cleanup attempts. Cleanup problems do not replace the
|
|
115
|
+
original predicate failure. Do not use an asynchronous predicate or rely on the
|
|
116
|
+
resource limits to bound arbitrary callback work.
|
|
117
|
+
|
|
118
|
+
## Probe design
|
|
119
|
+
|
|
120
|
+
The helper works through corresponding components, creating nested directories
|
|
121
|
+
to observe inherited lookup behavior. ASCII-case probes use generated names.
|
|
122
|
+
Normalization probes retain the original non-ASCII spellings while substituting
|
|
123
|
+
suitable ASCII letters. When a generated pair is unavailable, the exact raw pair
|
|
124
|
+
can be tried inside an owned neutral directory. Requested names are never
|
|
125
|
+
materialized directly in the caller's unowned parent.
|
|
126
|
+
|
|
127
|
+
Generated names are conservatively excluded when their NFC lowercase or uppercase
|
|
128
|
+
forms could collide with either requested component. These folds are only a name
|
|
129
|
+
exclusion rule; they never classify two requested paths as aliases. Short neutral
|
|
130
|
+
and ASCII probe names use at most six characters and are no longer than the
|
|
131
|
+
shorter requested component. Generated normalization pairs are also limited by
|
|
132
|
+
the longer original joined path length. A raw-pair fallback adds a directory
|
|
133
|
+
level; filesystem-specific name or path limits can still make it unavailable.
|
|
134
|
+
|
|
135
|
+
Probes use normal directory creation and the process umask, preserving inherited
|
|
136
|
+
directory behavior. They do not force mode `0700`, change parent permissions, or
|
|
137
|
+
promise private probe names. Creation and removal can affect directory timestamps
|
|
138
|
+
and filesystem watchers.
|
|
139
|
+
|
|
140
|
+
## Identity and cleanup limitations
|
|
141
|
+
|
|
142
|
+
The requested directory, canonical parent, and owned probe chain are rechecked
|
|
143
|
+
with exact bigint filesystem identities. An alternate spelling must identify the
|
|
144
|
+
same ordinary directory, not a symlink or unrelated collision. Existing files,
|
|
145
|
+
directories, and symlinks at a candidate name are not removed to make room.
|
|
146
|
+
|
|
147
|
+
Cleanup attempts owned directories in reverse order with identity checks and
|
|
148
|
+
non-recursive removal. Nonempty directories and observed replacements are
|
|
149
|
+
preserved. A cleanup failure changes an otherwise boolean result to `undefined`.
|
|
150
|
+
If the first identity observation after a successful creation fails, a directory
|
|
151
|
+
can remain: the helper does not guess ownership to delete it.
|
|
152
|
+
|
|
153
|
+
This is a pathname-based observation helper, not an atomic filesystem
|
|
154
|
+
transaction. There are unavoidable gaps between creation and the first identity
|
|
155
|
+
observation, and between an identity check and a subsequent syscall. No pinned
|
|
156
|
+
directory handle or atomic conditional deletion closes those gaps. Use a trusted,
|
|
157
|
+
approved writable directory and application-level concurrency control where
|
|
158
|
+
needed; do not use this helper as authorization to access attacker-controlled
|
|
159
|
+
paths.
|
package/docs/path.md
CHANGED
|
@@ -36,7 +36,11 @@ isPathInside("/srv/uploads", "/srv/uploads-other/x"); // false
|
|
|
36
36
|
isPathInside("/srv/uploads", "/srv/uploads"); // true (root itself counts)
|
|
37
37
|
```
|
|
38
38
|
|
|
39
|
-
The check is platform-aware: on Windows, paths are normalized for case and separator before comparison.
|
|
39
|
+
The check is platform-aware: on Windows, paths are normalized for case and separator before comparison. This is deliberately a string-only, lexical answer; case folding does not prove that two differently cased prefixes name the same directory on a case-sensitive Windows directory. Root operations perform their own filesystem-identity admission when containment depends on case folding.
|
|
40
|
+
It is a lexical comparison, not a filesystem-admission boundary: it does not
|
|
41
|
+
reject Windows alternate data streams or index-allocation aliases. Use a
|
|
42
|
+
filesystem operation such as `root()` when a caller-controlled path will be
|
|
43
|
+
opened or mutated.
|
|
40
44
|
|
|
41
45
|
### `isPathInsideWithRealpath(rootDir, target, opts?)`
|
|
42
46
|
|
|
@@ -54,6 +58,9 @@ type Options = {
|
|
|
54
58
|
```
|
|
55
59
|
|
|
56
60
|
Does not throw on missing inputs — `realpath` failures are absorbed by the underlying `safeRealpathSync`. By default (`requireRealpath: true`) the function returns `false` when either input cannot be resolved. Pass `{ requireRealpath: false }` to fall back to the lexical answer from `isPathInside` instead.
|
|
61
|
+
On Windows it returns `false` for namespace aliases in either raw input or in
|
|
62
|
+
a canonical value returned by `realpath` or the supplied cache. The
|
|
63
|
+
`requireRealpath: false` fallback does not admit those aliases.
|
|
57
64
|
|
|
58
65
|
### `isWithinDir(rootDir, targetPath)`
|
|
59
66
|
|
|
@@ -79,12 +86,16 @@ if (real === null) return notFound();
|
|
|
79
86
|
```
|
|
80
87
|
|
|
81
88
|
All `realpath` failures collapse to `null` — there is no distinction between `ENOENT`, `EACCES`, and other I/O errors. Use `fs.realpathSync` directly if you need to branch on the error code.
|
|
89
|
+
This convenience wrapper preserves ordinary Node `realpath` semantics; it is
|
|
90
|
+
not a caller-path admission boundary on its own.
|
|
82
91
|
|
|
83
92
|
### `safeStatSync(targetPath)`
|
|
84
93
|
|
|
85
94
|
Synchronous `stat` that returns `Stats` on success and `null` on any failure,
|
|
86
95
|
including missing paths and permission errors. Use `fs.statSync` directly when
|
|
87
96
|
the distinction matters.
|
|
97
|
+
Like `safeRealpathSync`, it does not apply the pathname-admission policy used
|
|
98
|
+
by the higher-level filesystem boundaries.
|
|
88
99
|
|
|
89
100
|
```ts
|
|
90
101
|
const stat = safeStatSync("/srv/uploads/photo.jpg");
|
package/docs/permissions.md
CHANGED
|
@@ -42,7 +42,7 @@ POSIX remediation strings shell-quote paths with whitespace or metacharacters
|
|
|
42
42
|
and protect option-like paths with `--`, so they can be presented as commands
|
|
43
43
|
without letting the inspected pathname add shell syntax.
|
|
44
44
|
|
|
45
|
-
`inspectPathPermissions()` follows symlink targets for the effective mode but tells you whether the original path was a symlink. On POSIX it reports owner/group/world bits. On Windows it delegates to the ACL helpers below and also reports `ownerSid` plus `ownerTrusted` when ownership can be verified. `ownerTrusted` is true only for a local volume owned by the current user, LocalSystem, or built-in Administrators; remote filesystems fail closed.
|
|
45
|
+
`inspectPathPermissions()` follows symlink targets for the effective mode but tells you whether the original path was a symlink. On POSIX it reports owner/group/world bits. On Windows it delegates to the ACL helpers below and also reports `ownerSid` plus `ownerTrusted` when ownership can be verified. `ownerTrusted` is true only for a local volume owned by the current user, LocalSystem, or built-in Administrators; remote filesystems fail closed. This remains a pathname reporting API with the fallbacks described below. `readSecureFile()` does not use those pathname fallbacks on Windows: it requires descriptor-bound native owner/DACL facts for the exact handle it reads.
|
|
46
46
|
|
|
47
47
|
## Advanced Windows ACL helpers
|
|
48
48
|
|
|
@@ -178,6 +178,31 @@ await openSqlite(path.join(sqliteDirectory, "sessions.sqlite"));
|
|
|
178
178
|
On Windows with native support, this creates the directory and applies a
|
|
179
179
|
protected owner + LocalSystem + Administrators full-control DACL directly with
|
|
180
180
|
an atomic security descriptor; no PowerShell or `icacls` process is launched.
|
|
181
|
+
The native operation retains the parent and exact created-directory handles
|
|
182
|
+
through ACL and final pathname validation. If validation fails, it attempts only
|
|
183
|
+
nonrecursive deletion through the created handle, preserving any pathname
|
|
184
|
+
replacement. If cleanup also fails, the error retains the original failure and
|
|
185
|
+
includes the cleanup failure.
|
|
186
|
+
|
|
187
|
+
Directory association checks compare the complete 64-bit volume serial and
|
|
188
|
+
128-bit `FILE_ID_INFO` identity, including on ReFS. If that identity class is
|
|
189
|
+
unavailable, the operation fails closed without a narrower file-index fallback.
|
|
190
|
+
Validation confirms that the created directory is local, its DACL is protected
|
|
191
|
+
from inheritance, and its final public pathname opens the same local directory.
|
|
192
|
+
|
|
193
|
+
This is a point-in-time pathname association check. The function closes its
|
|
194
|
+
handles before returning; callers must keep the pathname's ancestry trusted
|
|
195
|
+
during subsequent use, including opening SQLite databases in the example above.
|
|
196
|
+
The immediate parent and final directory must not be reparse points. Earlier
|
|
197
|
+
ancestor reparse points can be followed; this API does not reject every reparse
|
|
198
|
+
point in the full ancestry.
|
|
199
|
+
|
|
200
|
+
Path components ending in a space or period are rejected before filesystem
|
|
201
|
+
operations to avoid differing Win32 and native pathname interpretations. This
|
|
202
|
+
also rejects explicit `.` and `..` components, including spellings such as
|
|
203
|
+
`.\private` and `parent\..\private`, as a compatibility restriction. Simple
|
|
204
|
+
relative names without these components remain supported.
|
|
205
|
+
|
|
181
206
|
This API is Windows-only and native-only; it fails closed with
|
|
182
207
|
`FsSafeError("helper-unavailable")` on other platforms, when native mode is off,
|
|
183
208
|
or when the binding is unavailable. POSIX callers should create private
|
|
@@ -47,6 +47,20 @@ fileStoreSync({ rootDir: "/var/lib/app", private: true }).writeJson("config.json
|
|
|
47
47
|
The sync store intentionally exposes a smaller surface: path resolution,
|
|
48
48
|
lenient reads, and atomic text/JSON writes.
|
|
49
49
|
|
|
50
|
+
Sync directory modes remain repair-compatible on POSIX, but repairs are applied
|
|
51
|
+
only through an exact-identity, no-follow directory descriptor after the store
|
|
52
|
+
root and admitted parent name are revalidated. Root or component swaps fail
|
|
53
|
+
without chmodding the substituted directory. Matching modes take the no-open
|
|
54
|
+
fast path. Windows uses its existing `mkdir` mode request plus exact directory
|
|
55
|
+
identity checks and never falls back to pathname chmod.
|
|
56
|
+
|
|
57
|
+
On Linux, Node offers no portable search-only descriptor that can also be
|
|
58
|
+
`fchmod`ed. A mismatched directory without effective read access—including one
|
|
59
|
+
created under an owner-read-removing umask—fails closed with
|
|
60
|
+
`permission-unverified`. Supported macOS x64/arm64 hosts additionally try
|
|
61
|
+
`O_SEARCH` when the directory remains searchable; an inaccessible directory
|
|
62
|
+
still fails rather than restoring the pathname race.
|
|
63
|
+
|
|
50
64
|
## See also
|
|
51
65
|
|
|
52
66
|
- [`fileStore`](file-store.md) — full store API.
|
package/docs/public-api.md
CHANGED
|
@@ -26,6 +26,11 @@ deprecated native-configuration bridge retains the `FsSafePythonConfig` type.
|
|
|
26
26
|
|
|
27
27
|
## `path` and `advanced`
|
|
28
28
|
|
|
29
|
+
`safePathSegmentHashedV2` encodes every trimmed install ID with domain-separated
|
|
30
|
+
SHA-256 into a fixed lowercase segment. The legacy `safePathSegmentHashed` keeps
|
|
31
|
+
its existing output but can alias distinct IDs. See [install paths](install-path.md)
|
|
32
|
+
for the exact encoding and migration contract.
|
|
33
|
+
|
|
29
34
|
The lexical path surface additionally exports `isNodeError`,
|
|
30
35
|
`isPathRelativeEscape`, `normalizeWindowsPathForComparison`,
|
|
31
36
|
`resolveSafeRelativePath`, `splitSafeRelativePath`, and
|
|
@@ -56,6 +61,15 @@ prefix preparation. See [in-place writes](in-place-write.md).
|
|
|
56
61
|
for local ASCII-case observations. An unavailable answer remains `undefined`;
|
|
57
62
|
the caller selects any fallback. See [path case probing](path-case.md).
|
|
58
63
|
|
|
64
|
+
`resolvePathPrefixSync` and `ResolvedPathPrefix` separate a canonical existing
|
|
65
|
+
path prefix from its raw unresolved suffix after physical symlink traversal.
|
|
66
|
+
See [resolving path prefixes](path-prefix.md).
|
|
67
|
+
|
|
68
|
+
`probePathSuffixAliasesSync` and `ProbePathSuffixAliasesOptions` compare selected
|
|
69
|
+
missing relative suffixes beneath an existing directory using bounded temporary
|
|
70
|
+
directory probes. The caller owns Unicode-pair policy, caching, and the fallback
|
|
71
|
+
for `undefined`. See [path suffix alias probing](path-suffix-aliases.md).
|
|
72
|
+
|
|
59
73
|
## Guest source
|
|
60
74
|
|
|
61
75
|
`@openclaw/fs-safe/guest` exports `GUEST_FILESYSTEM_PYTHON`,
|
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. Pass `{ overwrite: true }` when replacing the target is intentional. `remove()` works on files and empty directories. For non-empty directories, list and remove children first or use [`replaceDirectoryAtomic`](atomic.md#replacedirectoryatomic).
|
|
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()` works on files and empty directories. For non-empty directories, list and remove children first or use [`replaceDirectoryAtomic`](atomic.md#replacedirectoryatomic).
|
|
64
64
|
|
|
65
65
|
## 5. Inspect
|
|
66
66
|
|
package/docs/reading.md
CHANGED
|
@@ -27,10 +27,17 @@ Regardless of shape, every read goes through the same boundary checks:
|
|
|
27
27
|
3. Resolve path components and reject anything that escapes the root (`outside-workspace`).
|
|
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
|
-
6. Compare the pre-open path identity
|
|
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
31
|
7. If `hardlinks: "reject"`, refuse files with `nlink > 1` (`hardlink`).
|
|
32
32
|
8. If `maxBytes` is set, refuse reads larger than the cap (`too-large`).
|
|
33
33
|
|
|
34
|
+
The final fence closes a rejected descriptor before any Root read consumes bytes or
|
|
35
|
+
`open()` hands the descriptor to its caller. It is a sequence of filesystem
|
|
36
|
+
observations, not an atomic kernel pathname/open primitive, so a hostile peer can
|
|
37
|
+
still race the namespace after the last observation. Standalone absolute-file
|
|
38
|
+
helpers have no captured `Root` identity and do not claim this replacement-root
|
|
39
|
+
fence; use a `Root` for untrusted paths.
|
|
40
|
+
|
|
34
41
|
## Read shapes
|
|
35
42
|
|
|
36
43
|
### `fs.read(rel, options?)`
|
package/docs/root.md
CHANGED
|
@@ -100,7 +100,9 @@ The read methods also accept an absolute spelling that already resolves inside
|
|
|
100
100
|
the root. `readAbsolute()` and `reader()` make that intent explicit and accept
|
|
101
101
|
both the configured root spelling and its canonical real path when the Root was
|
|
102
102
|
created through a directory symlink or Windows junction. An absolute path
|
|
103
|
-
outside the root is still rejected.
|
|
103
|
+
outside the root is still rejected. On Windows, alternate casing is accepted
|
|
104
|
+
only when the differently cased Root prefix has the Root's exact directory
|
|
105
|
+
identity; the operation then continues under the trusted Root spelling.
|
|
104
106
|
|
|
105
107
|
### Writes
|
|
106
108
|
|
|
@@ -112,7 +114,7 @@ fs.createJson(rel, value, options?) // create() variant of writeJson
|
|
|
112
114
|
fs.append(rel, data, options?) // append text/buffer; syncs before close by default
|
|
113
115
|
fs.copyIn(rel, sourceAbsPath, options?) // copy from outside the root, atomically, with size cap
|
|
114
116
|
fs.openWritable(rel, options?) // FileHandle for streaming writes; supports await using
|
|
115
|
-
fs.move(from, to, options?) // rename within the root;
|
|
117
|
+
fs.move(from, to, options?) // rename within the root; native-backed no clobber by default
|
|
116
118
|
fs.remove(rel, options?) // unlink file, rmdir, or bounded recursive removal
|
|
117
119
|
fs.mkdir(rel, options?) // mkdir -p (creates missing parents)
|
|
118
120
|
fs.ensureRoot(options?) // accepts "" / "." as the root itself
|
|
@@ -219,7 +221,17 @@ source)` can reject a legal POSIX basename such as `c:photo.png`; callers that
|
|
|
219
221
|
derive portable destination names from host files must sanitize or map that
|
|
220
222
|
basename first.
|
|
221
223
|
|
|
222
|
-
|
|
224
|
+
On Windows, every Root pathname admission also rejects NTFS alternate-data-stream
|
|
225
|
+
and directory-index aliases: relative names containing `:`, or absolute names
|
|
226
|
+
with a colon beyond the single rooted drive designator, fail with
|
|
227
|
+
`invalid-path` before filesystem access. This includes spellings such as
|
|
228
|
+
`file:stream`, `dir::$INDEX_ALLOCATION`, and `dir:$I30:$INDEX_ALLOCATION`.
|
|
229
|
+
Rooted drive, UNC, and extended-drive paths keep their existing handling, and
|
|
230
|
+
the separate device/network policies remain in force. Ordinary colon-bearing
|
|
231
|
+
names remain valid on POSIX where the operation's existing drive-relative rule
|
|
232
|
+
does not otherwise reject them.
|
|
233
|
+
|
|
234
|
+
`openWritable` opens a writable file with options `mode?: number` and `writeMode?: "replace" | "append" | "update"`. `replace` truncates existing files and is the default; `update` keeps existing contents. Before truncation or handle return, descriptor and pathname identities are compared with lossless bigint metadata; persistently unknown Windows identities fail closed. The returned `stat` remains an ordinary numeric Node `Stats` object. Use it for streaming output. Prefer `await using` for cleanup.
|
|
223
235
|
|
|
224
236
|
`remove` leaves non-empty directories unchanged unless `recursive: true` is
|
|
225
237
|
provided. Recursive removal defaults to streaming entries in filesystem order;
|
|
@@ -297,7 +309,12 @@ fs.entries(rel, options?) // nonrecursive AsyncIterable<DirEntry>, includ
|
|
|
297
309
|
fs.resolve(rel) // absolute path inside the root, after canonicalization
|
|
298
310
|
```
|
|
299
311
|
|
|
300
|
-
These do not pin a later operation.
|
|
312
|
+
These do not pin a later operation. During `stat()`, the exact selected target and
|
|
313
|
+
parent are checked around metadata collection; `list()` checks one exact selected
|
|
314
|
+
directory around the complete name/metadata batch instead of repeating containment
|
|
315
|
+
work for every child. A detectable redirection rejects with `path-mismatch` rather
|
|
316
|
+
than returning names or metadata from the replacement. Results remain advisory
|
|
317
|
+
after the call returns, so use the verb methods for the actual read or write.
|
|
301
318
|
|
|
302
319
|
`entries()` streams immediate children in filesystem order by default. It
|
|
303
320
|
supports cancellation, a physical-entry limit that throws on overflow, and
|
package/docs/secret-file.md
CHANGED
|
@@ -81,6 +81,9 @@ itself must not be an alias. Hardlinks are rejected by default so another
|
|
|
81
81
|
in-tree name cannot alias the credential; pass `rejectHardlinks: false` only
|
|
82
82
|
when you explicitly trust that layout.
|
|
83
83
|
|
|
84
|
+
Read options are captured when the call starts. Mutating a shared options
|
|
85
|
+
object while an asynchronous read is in flight cannot relax its link policy.
|
|
86
|
+
|
|
84
87
|
These readers do not enforce ownership or mode bits on an existing file. Their
|
|
85
88
|
read contract covers pinned identity, file type, link policy, and byte bounds;
|
|
86
89
|
the `0o600` guarantee belongs to the write helpers below. Use
|