@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/path.md
CHANGED
|
@@ -37,6 +37,10 @@ isPathInside("/srv/uploads", "/srv/uploads"); // true (root itsel
|
|
|
37
37
|
```
|
|
38
38
|
|
|
39
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");
|
|
@@ -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
|
@@ -61,6 +61,15 @@ prefix preparation. See [in-place writes](in-place-write.md).
|
|
|
61
61
|
for local ASCII-case observations. An unavailable answer remains `undefined`;
|
|
62
62
|
the caller selects any fallback. See [path case probing](path-case.md).
|
|
63
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
|
+
|
|
64
73
|
## Guest source
|
|
65
74
|
|
|
66
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
|
@@ -114,7 +114,7 @@ fs.createJson(rel, value, options?) // create() variant of writeJson
|
|
|
114
114
|
fs.append(rel, data, options?) // append text/buffer; syncs before close by default
|
|
115
115
|
fs.copyIn(rel, sourceAbsPath, options?) // copy from outside the root, atomically, with size cap
|
|
116
116
|
fs.openWritable(rel, options?) // FileHandle for streaming writes; supports await using
|
|
117
|
-
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
|
|
118
118
|
fs.remove(rel, options?) // unlink file, rmdir, or bounded recursive removal
|
|
119
119
|
fs.mkdir(rel, options?) // mkdir -p (creates missing parents)
|
|
120
120
|
fs.ensureRoot(options?) // accepts "" / "." as the root itself
|
|
@@ -221,7 +221,17 @@ source)` can reject a legal POSIX basename such as `c:photo.png`; callers that
|
|
|
221
221
|
derive portable destination names from host files must sanitize or map that
|
|
222
222
|
basename first.
|
|
223
223
|
|
|
224
|
-
|
|
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.
|
|
225
235
|
|
|
226
236
|
`remove` leaves non-empty directories unchanged unless `recursive: true` is
|
|
227
237
|
provided. Recursive removal defaults to streaming entries in filesystem order;
|
|
@@ -299,7 +309,12 @@ fs.entries(rel, options?) // nonrecursive AsyncIterable<DirEntry>, includ
|
|
|
299
309
|
fs.resolve(rel) // absolute path inside the root, after canonicalization
|
|
300
310
|
```
|
|
301
311
|
|
|
302
|
-
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.
|
|
303
318
|
|
|
304
319
|
`entries()` streams immediate children in filesystem order by default. It
|
|
305
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
|
package/docs/secure-file.md
CHANGED
|
@@ -62,6 +62,10 @@ type SecureFileReadOptions = {
|
|
|
62
62
|
|
|
63
63
|
`io.maxBytes` must be a non-negative safe integer or positive `Infinity`; zero is an active cap and `Infinity` disables the cap. Invalid limits reject before filesystem admission.
|
|
64
64
|
|
|
65
|
+
The helper synchronously snapshots the supplied options, including nested permission and I/O settings, the injection callback, and supplied injection environment values, before opening the file or reaching its first `await`. Mutating those objects after this snapshot does not change that read's policy. This is not an atomic snapshot at invocation entry: caller getters run during snapshot construction and can affect values or working directories that have not yet been captured. `trust.trustedDirs` must be an array with a valid length and an own string entry without null bytes at every index; malformed lengths or entries, including sparse entries filled by inherited properties, reject with `invalid-path` before filesystem admission. An omitted or empty array leaves the read unrestricted by directory.
|
|
66
|
+
|
|
67
|
+
Relative trusted directories (including an empty string) are resolved to absolute lexical paths during this synchronous snapshot using Node's `path.resolve()` semantics. Windows drive-relative entries retain their per-drive current-directory semantics, and extended-length drive roots retain their root separator. Raw alternate-stream and filesystem-namespace aliases reject before normalization. Working-directory changes after the snapshot cannot redirect this allowlist. The existing realpath check still follows trusted-directory symlinks when it runs and falls back to the captured lexical path if realpath lookup fails; the allowlist does not pin directory identities.
|
|
68
|
+
|
|
65
69
|
`permissions.allowInsecure` is a migration escape hatch. Prefer fixing permissions and using [`formatPermissionRemediation`](permissions.md) to show the user what to run. `trust.allowNetworkPath` is off by default because UNC paths are remote authority, not local filesystem input. `inject` is for tests and platform adapters; production callers usually leave it unset.
|
|
66
70
|
|
|
67
71
|
On an actual Windows process with effective `platform: "win32"`, `inject.env` and `inject.exec` do not replace descriptor inspection. They remain available to simulated Windows checks on non-Windows hosts.
|
|
@@ -74,7 +78,7 @@ On an actual Windows process with effective `platform: "win32"`, `inject.env` an
|
|
|
74
78
|
|
|
75
79
|
| Code | Meaning |
|
|
76
80
|
|---|---|
|
|
77
|
-
| `invalid-path` | `filePath` was not a local absolute path. |
|
|
81
|
+
| `invalid-path` | `filePath` was not a local absolute path, `trust.trustedDirs` contained malformed paths, or a Windows file path/trusted directory used an alternate-stream or filesystem-namespace alias. |
|
|
78
82
|
| `not-found` | The path could not be stat'd before open. |
|
|
79
83
|
| `not-file` | The opened target is not a regular file. |
|
|
80
84
|
| `symlink` | The path is a symlink and `trust.allowSymlink` is false. |
|
|
@@ -83,7 +87,7 @@ On an actual Windows process with effective `platform: "win32"`, `inject.env` an
|
|
|
83
87
|
| `outside-workspace` | `realPath` is outside `trust.trustedDirs`. |
|
|
84
88
|
| `permission-unverified` | Required mode/ACL checks could not be completed, including when descriptor-bound Windows inspection is unavailable. |
|
|
85
89
|
| `insecure-permissions` | Mode bits or ACLs grant broader access than allowed. |
|
|
86
|
-
| `not-owned` | POSIX owner uid is not the
|
|
90
|
+
| `not-owned` | POSIX owner uid is not the process's effective uid. |
|
|
87
91
|
| `too-large` | File size or bytes read exceeded `maxBytes`. |
|
|
88
92
|
| `timeout` | `timeoutMs` elapsed while reading. |
|
|
89
93
|
|
package/docs/security-model.md
CHANGED
|
@@ -28,6 +28,8 @@ You hand a `root()` boundary to a piece of code that takes caller-controlled rel
|
|
|
28
28
|
- replaces the destination directory with a symlink right before a write
|
|
29
29
|
- creates a hardlink that aliases an out-of-tree inode and asks you to read or replace it
|
|
30
30
|
- asks a read/open primitive to target a known unsafe device or process-fd path
|
|
31
|
+
- uses an NTFS alternate-stream or directory-index pathname to alias a different
|
|
32
|
+
Windows filesystem object than the visible path suggests
|
|
31
33
|
- triggers a partial write that leaves a half-written file at the destination
|
|
32
34
|
- ships an archive with `..` paths, absolute paths, or symlinks pointing outside the destination
|
|
33
35
|
|
|
@@ -47,6 +49,30 @@ If you need full sandboxing, run the worker under reduced privileges (uid, conta
|
|
|
47
49
|
|
|
48
50
|
Every path is resolved against the canonicalized real path of the root, then checked at that boundary. On Windows, an exact-case structural Root prefix stays on the lexical fast path; a prefix accepted only by case folding must have the Root's exact directory identity and is rebased onto the trusted Root spelling before use. Alias resolution walks components before applying a later `..`, so a symlink cannot change what that parent segment means after validation. Parent traversal, an absolute spelling, or any alias whose canonical result is outside the root throws `outside-workspace`; absolute spellings that remain inside the root are accepted.
|
|
49
51
|
|
|
52
|
+
Guarded pathname APIs reject Windows `:` namespace aliases before normalization
|
|
53
|
+
or filesystem access. The only colon admitted in a Windows filesystem path is
|
|
54
|
+
the rooted ASCII drive designator (including extended-drive syntax); relative
|
|
55
|
+
paths admit none. This rule does not authorize device or network paths, which
|
|
56
|
+
retain their independent restrictions. Pure formatters, descriptor-only calls,
|
|
57
|
+
and `walkDirectory` (documented as a non-boundary traversal helper) are outside
|
|
58
|
+
this pathname-admission guarantee. Output helpers continue to sanitize an
|
|
59
|
+
untrusted basename, then validate the resulting path.
|
|
60
|
+
|
|
61
|
+
The low-level existing-object readers `openRootFile()` and
|
|
62
|
+
`openRootFileSync()` preserve their historical Windows drive-relative input:
|
|
63
|
+
they anchor its leading drive designator before this admission check. The raw
|
|
64
|
+
suffix remains unnormalized, and any additional colon is still rejected before
|
|
65
|
+
filesystem access.
|
|
66
|
+
|
|
67
|
+
Trusted-path standalone publication APIs retain the same drive-relative
|
|
68
|
+
compatibility. This includes the atomic file, text, JSON, JSON store/direct
|
|
69
|
+
queue writer, directory-replacement, move, and exclusive-publication helpers.
|
|
70
|
+
They capture the drive's current directory at publication entry and carry the
|
|
71
|
+
anchored spelling through their remaining checks, locks, callbacks, receipts,
|
|
72
|
+
publication, and cleanup. File writers preserve the raw suffix. Root-relative
|
|
73
|
+
APIs and caller-constructed relative directory receipts continue to reject
|
|
74
|
+
drive designators.
|
|
75
|
+
|
|
50
76
|
### Symlinks (read side)
|
|
51
77
|
|
|
52
78
|
`open()` and `read()` use `fs.open` with `O_NOFOLLOW` on POSIX where available. The library then `fstat`s the open fd and `realpath`s the original input, asserting the two refer to the same inode. A symlink swap that happens between resolve and open will fail at the identity check rather than silently following the new target.
|
|
@@ -60,12 +86,33 @@ checks, so callers do not need their own parent canonicalization.
|
|
|
60
86
|
|
|
61
87
|
Guarded root reads compare lossless bigint identities from before open, the opened
|
|
62
88
|
descriptor, the input path, and the canonical target; numeric public `Stats`
|
|
63
|
-
receipts are not used as identity evidence.
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
89
|
+
receipts are not used as identity evidence. Before returning a handle or reading
|
|
90
|
+
bytes, one best-effort final admission fence checks the originally captured root
|
|
91
|
+
identity, freshly compares the policy-aware pathname with the opened descriptor,
|
|
92
|
+
canonicalizes and re-admits that current target inside the captured root, compares
|
|
93
|
+
the canonical target's exact bigint identity without following a final symlink
|
|
94
|
+
with the descriptor, and checks the root identity again. The two final pathname
|
|
95
|
+
observations remain independent even when the spellings match. Unknown Windows
|
|
96
|
+
device/inode values receive one re-inspection without reopening the file.
|
|
97
|
+
A definite mismatch or persistent unknown identity
|
|
98
|
+
rejects with `path-mismatch`; escaped fresh containment rejects with
|
|
99
|
+
`outside-workspace`. This is not an atomic kernel pathname/open primitive, so the
|
|
100
|
+
namespace can still change after the final observation.
|
|
101
|
+
|
|
102
|
+
Other regular-file readers, root-file adapters, and archive input staging retain
|
|
103
|
+
their documented descriptor/path admission. The exported unrooted
|
|
104
|
+
`openLocalFileSafely()` and `readLocalFileSafely()` helpers have no captured `Root`
|
|
105
|
+
identity and therefore do not provide the replacement-root fence. `copyIn()`
|
|
106
|
+
retains the admitted source identity for its checks before and after copying,
|
|
107
|
+
independently of its numeric metadata receipt.
|
|
108
|
+
|
|
109
|
+
The low-level `openRootFile()` adapters additionally retain the canonical root's
|
|
110
|
+
exact identity from before component traversal. After their existing pathname and
|
|
111
|
+
descriptor checks, they verify the root, freshly resolve and re-admit the consumed
|
|
112
|
+
pathname, compare that canonical leaf with the descriptor, and verify the root
|
|
113
|
+
again before returning ownership. A failed final fence closes the descriptor
|
|
114
|
+
without reading. The checks detect substitutions at each observation boundary;
|
|
115
|
+
they do not make pathname confinement atomic against a continuously racing peer.
|
|
69
116
|
|
|
70
117
|
### Symlinks (write side)
|
|
71
118
|
|
|
@@ -75,6 +122,26 @@ directory descriptors. Replacement uses descriptor-relative rename just like
|
|
|
75
122
|
no-replace publication, so replacing the parent pathname does not divert the
|
|
76
123
|
mutation.
|
|
77
124
|
|
|
125
|
+
When either `denyMutations` or an explicit `mutationSymlinks` policy applies,
|
|
126
|
+
the POSIX writer binds that exact policy snapshot to parent admission. An existing
|
|
127
|
+
parent is canonicalized and identity-matched to its retained descriptor before
|
|
128
|
+
the actual destination is authorized. A missing-parent walk authorizes each
|
|
129
|
+
prospective directory before `mkdirat`, opens it without following a newly
|
|
130
|
+
introduced link, and authorizes the opened object before continuing. This
|
|
131
|
+
prevents a contained Linux `openat2(RESOLVE_BENEATH | RESOLVE_NO_MAGICLINKS)`
|
|
132
|
+
redirect from reusing policy approval for a different in-root subtree.
|
|
133
|
+
For an ordinary unchanged route, operation-local observations may carry that
|
|
134
|
+
admission across a direct-child creation only after exact parent and child
|
|
135
|
+
fences and a synchronous full-epoch validation. The resulting operation-local
|
|
136
|
+
token authorizes the opened child without an intervening await; stale,
|
|
137
|
+
redirected, incomplete, or foreign evidence returns to the full ordered
|
|
138
|
+
admission. An already-complete fallback parent is likewise retained only after
|
|
139
|
+
full target admission and a fresh exact guard fence. Native acceleration additionally requires an exclusive
|
|
140
|
+
direct-child mkdir result proving that this syscall created the name; a
|
|
141
|
+
collision, legacy helper, or malformed result performs the guarded walk but
|
|
142
|
+
cannot advance the receipt. That boolean is admission provenance only and does
|
|
143
|
+
not grant ownership for cleanup by pathname.
|
|
144
|
+
|
|
78
145
|
The opt-in `mutationSymlinks` policy applies independently of read policy.
|
|
79
146
|
`"reject"` rejects symlink components; `"follow-parents-within-root"` resolves
|
|
80
147
|
contained directory aliases and rejects final symlinks. Publication checks the
|
|
@@ -107,13 +174,13 @@ When `hardlinks: "reject"` is set, reads stat the target and refuse if `nlink >
|
|
|
107
174
|
|
|
108
175
|
### TOCTOU between resolve and use
|
|
109
176
|
|
|
110
|
-
`resolve()`, `exists()`, `stat()`, and `list()` are explicitly advisory — they answer a question and return. To act on a path with operation-local identity checks, use `read()`, `open()`, `write()`, `create()`, `copyIn()`, `move()`, or `remove()`. The containment table below states which opens are kernel-atomic and which remain best-effort.
|
|
177
|
+
`resolve()`, `exists()`, `stat()`, and `list()` are explicitly advisory — they answer a question and return. `stat()` checks the exact selected target and parent while collecting metadata, and `list()` checks the exact selected directory around its complete batch, so detectable descendant redirection rejects before results are returned. Those checks do not preserve identity after the call. To act on a path with operation-local identity checks, use `read()`, `open()`, `write()`, `create()`, `copyIn()`, `move()`, or `remove()`. The containment table below states which opens are kernel-atomic and which remain best-effort.
|
|
111
178
|
|
|
112
179
|
A `root()` handle also remembers the canonical root directory identity. Calls fail with `path-mismatch` if that canonical pathname is replaced, including advisory inspection and walking calls, rather than following a replacement root into another tree.
|
|
113
180
|
|
|
114
181
|
### Denied mutations
|
|
115
182
|
|
|
116
|
-
`denyMutations` is an opt-in application policy for `root()` mutation methods. It blocks exact absolute paths with `paths` and whole subtrees with `prefixes`, merging root defaults with per-call entries so a call cannot clear root-level denies. This is not an OS permission boundary: code with access to `node:fs`, a shell, or another process with the same filesystem privileges can bypass it.
|
|
183
|
+
`denyMutations` is an opt-in application policy for `root()` mutation methods. It blocks exact absolute paths with `paths` and whole subtrees with `prefixes`, merging root defaults with per-call entries so a call cannot clear root-level denies. POSIX pinned `write`, `create`, and `copyIn` copy the merged entries before awaiting preflight and reapply them to their admitted canonical parent, including before missing parent creation. This is not an OS permission boundary: code with access to `node:fs`, a shell, or another process with the same filesystem privileges can bypass it.
|
|
117
184
|
|
|
118
185
|
### Atomic writes
|
|
119
186
|
|
package/docs/sidecar-lock.md
CHANGED
|
@@ -2,6 +2,11 @@
|
|
|
2
2
|
|
|
3
3
|
`acquireFileLock()` and `withFileLock()` provide a cross-process file lock with retry and process-exit cleanup. The lock is implemented as a sidecar file (e.g. `state.json` ↔ `state.json.lock`) — only one acquirer can create the sidecar with `O_CREAT | O_EXCL` at a time.
|
|
4
4
|
|
|
5
|
+
On Windows, both the target and an explicitly supplied `lockPath` reject NTFS
|
|
6
|
+
alternate-stream and directory-index namespace spellings before parent creation,
|
|
7
|
+
in-process reentrant lookup, or sidecar access. Rooted drive paths retain their
|
|
8
|
+
normal meaning, and ordinary colon-bearing POSIX paths remain valid.
|
|
9
|
+
|
|
5
10
|
JavaScript raw-sidecar creation passes mode `0o600` to that exclusive open. On POSIX, the process umask may further restrict the new file but cannot add group or other access. No pathname `chmod` fallback is used.
|
|
6
11
|
|
|
7
12
|
```ts
|
|
@@ -33,7 +38,7 @@ Each new sidecar also carries an internal random ownership token encoded as JSON
|
|
|
33
38
|
|
|
34
39
|
The raw sidecar bytes are not a canonical JSON representation: tools that trim or rewrite the trailing whitespace invalidate the ownership token, so release leaves the changed sidecar in place and fails closed. The token distinguishes cooperating acquisitions; it is not a secret and does not make pathname compare-and-remove atomic against a hostile process that can replace files outside the lock protocol.
|
|
35
40
|
|
|
36
|
-
`release()` propagates an I/O failure that prevents deletion of an unchanged, owned sidecar; it never reports successful cleanup while leaving that lock behind. The handle and manager retain the exact cleanup receipt after a failure, so the same handle can retry `release()` and `manager.drain()` can retry retained cleanup. A changed sidecar remains an ownership mismatch rather than a deletion failure and is left untouched. If both a `withFileLock()` callback and release fail, the release error is the primary `SuppressedError.error` and the callback failure remains available as `SuppressedError.suppressed`. Failed acquisition cleanup uses the same shape, with the cleanup error primary and the acquisition failure suppressed. On Node runtimes without the global `SuppressedError` constructor, fs-safe returns the equivalent `Error` shape with the same name and properties.
|
|
41
|
+
`release()` propagates an I/O failure that prevents deletion of an unchanged, owned sidecar; it never reports successful cleanup while leaving that lock behind. The handle and manager retain the exact cleanup receipt after a failure, so the same handle can retry `release()` and `manager.drain()` can retry retained cleanup. A changed sidecar remains an ownership mismatch rather than a deletion failure and is left untouched. If both a `withFileLock()` callback and release fail, the release error is the primary `SuppressedError.error` and the callback failure remains available as `SuppressedError.suppressed`. Failed asynchronous acquisition cleanup uses the same shape, with the cleanup error primary and the acquisition failure suppressed. On Node runtimes without the global `SuppressedError` constructor, fs-safe returns the equivalent `Error` shape with the same name and properties.
|
|
37
42
|
|
|
38
43
|
## API
|
|
39
44
|
|
|
@@ -102,6 +107,16 @@ type FileLockRetryOptions = {
|
|
|
102
107
|
|
|
103
108
|
`payload` is a function so you can re-evaluate it on each retry (e.g. timestamp, PID).
|
|
104
109
|
|
|
110
|
+
Asynchronous acquisition snapshots `targetPath`, an explicit `lockPath`, and
|
|
111
|
+
`lockRoot` before its first asynchronous operation. Cwd-dependent path spellings
|
|
112
|
+
are resolved using Node's platform path-resolution rules at that point, and the
|
|
113
|
+
resulting absolute paths remain fixed through retries, stale recovery,
|
|
114
|
+
verification, and release even if the process later changes its working
|
|
115
|
+
directory. An explicit, fully qualified `lockPath` retains its caller-supplied
|
|
116
|
+
spelling; current-drive-rooted and drive-relative Windows paths are resolved at
|
|
117
|
+
the snapshot boundary. The snapshot adds no normalization beyond what is
|
|
118
|
+
required to remove that cwd or current-drive dependency.
|
|
119
|
+
|
|
105
120
|
The complete serialized sidecar must fit within 1 MiB (1,048,576 UTF-8 bytes),
|
|
106
121
|
including pretty-printed JSON, newlines, and the internal ownership token's
|
|
107
122
|
trailing whitespace. The limit counts bytes, not string characters. Oversized
|
|
@@ -215,6 +230,11 @@ Discarding an acquisition observation is not proof that the pathname is absent:
|
|
|
215
230
|
another owner may already have created the next record. Every discarded
|
|
216
231
|
observation consumes the normal retry/deadline budget and requires fresh
|
|
217
232
|
exclusive creation. It supplies no release, reclaim, or held-lock authority.
|
|
233
|
+
If that successor disappears during the recovery metadata probe, the waiter
|
|
234
|
+
may discard the probe only with an operation-local receipt for an admitted
|
|
235
|
+
regular file with one link, followed by current Root and canonical ancestor
|
|
236
|
+
checks. A generic metadata error does not permit this retry, and public
|
|
237
|
+
`Root.stat()` still rejects a file that changes during observation.
|
|
218
238
|
Generic `Root.open()` and held-owner/reclaim reads still reject failed opens.
|
|
219
239
|
Moving an already-matched pinned descriptor without unlinking it, unknown or
|
|
220
240
|
inexact identities, retargeted ancestors, and unrelated filesystem or caller
|
|
@@ -302,6 +322,13 @@ try {
|
|
|
302
322
|
}
|
|
303
323
|
```
|
|
304
324
|
|
|
325
|
+
Failed synchronous acquisition attempts close the created descriptor once even
|
|
326
|
+
if its metadata cannot be read. Cleanup leaves the sidecar in place without an
|
|
327
|
+
exact descriptor identity. A metadata-capture failure does not replace the
|
|
328
|
+
acquisition error; if close or identity-checked removal also fails, the
|
|
329
|
+
`SuppressedError.error` is the acquisition error and `suppressed` is the cleanup
|
|
330
|
+
error.
|
|
331
|
+
|
|
305
332
|
The sync payload, reclaim, and parsing callbacks must also be synchronous. This
|
|
306
333
|
shape is appropriate for a short boot migration; it is a poor fit for a server
|
|
307
334
|
request because retry backoff uses a blocking wait.
|
package/docs/store.md
CHANGED
|
@@ -32,7 +32,7 @@ import {
|
|
|
32
32
|
| Durable JSON queue helpers | Append/load/ack JSON entry files using atomic writes and delivered markers. |
|
|
33
33
|
| [Private file-store mode](private-file-store.md) | `fileStore({ private: true })` for credentials, tokens, and per-agent state at `0600` files under `0700` directories. |
|
|
34
34
|
|
|
35
|
-
`fileStore().json("rel.json")` and `jsonStore({ filePath })` are intentionally separate primitives. Use `fileStore().json(...)` when JSON state lives alongside other files in the same managed directory; use `jsonStore({ filePath })` when you have
|
|
35
|
+
`fileStore().json("rel.json")` and `jsonStore({ filePath })` are intentionally separate primitives. Use `fileStore().json(...)` when JSON state lives alongside other files in the same managed directory; use `jsonStore({ filePath })` when you have one trusted path, resolved to an absolute path at construction, and want the keyed JSON shape directly.
|
|
36
36
|
|
|
37
37
|
## Picking a shape
|
|
38
38
|
|
|
@@ -74,6 +74,10 @@ Queue and failed directory creation fsyncs every newly-created parent edge from
|
|
|
74
74
|
|
|
75
75
|
`writeJsonDurableQueueEntry()` and migrations share strict parent synchronization inside the atomic writer's retained descriptor and per-path serialization lifetime, followed by published-file identity verification. If sync fails after publication, the write rejects without rolling back the published JSON; retrying `writeJsonDurableQueueEntry()` writes the entry again and must complete its own sync. Loader retries resync an existing processing claim's parent under the transfer lock before calling `read`, even when a version-dependent callback would no longer request migration. Fresh claims and same-directory source retirement already complete that sync. This is not a rollback, deduplication, or exactly-once guarantee. The generic `replaceFileAtomic({ syncParentDir: true })` option remains best-effort.
|
|
76
76
|
|
|
77
|
+
The direct queue writer accepts trusted relative paths. On Windows it anchors
|
|
78
|
+
an ordinary drive-relative `filePath` before publication and strict parent
|
|
79
|
+
sync. Other queue lifecycle APIs retain their own root/path admission contracts.
|
|
80
|
+
|
|
77
81
|
Batch loading skips invalid entry names, malformed, oversized, or unreadable entry content, and caller `read` callback failures. Initially unowned pending entries (hardlinks or unverifiable identities), symlinks, non-files, and absent pending entries are also skipped. Claim, transfer-lock, retirement, and migration write/publication/durability failures reject the batch with the original error, even if earlier entries succeeded. Migration in both loaders strictly syncs the parent directory after successful publication. Visible transitions and earlier processing claims remain for retry; a rejected batch does not acknowledge or roll them back.
|
|
78
82
|
|
|
79
83
|
Failed destinations are create-only. Quarantine publishes the claimed file by hardlink, so the queue and failed directories must share a filesystem with hardlink support. If `failed/<id>.json` already exists, quarantine rejects while preserving both that earlier evidence and the current claimed entry instead of overwriting either file. The `read` callback continues to receive the logical `.json` path even though bytes are read and migrations are written through the claimed path.
|