@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/secure-file.md
CHANGED
|
@@ -27,9 +27,11 @@ The helper:
|
|
|
27
27
|
- enforces `maxBytes` before and after reading
|
|
28
28
|
- closes the handle on success, error, and timeout
|
|
29
29
|
|
|
30
|
-
On POSIX, unsafe permissions mean group/world writable, and group/world readable unless `permissions.allowReadableByOthers` is true. On Windows, the helper
|
|
30
|
+
On POSIX, unsafe permissions mean group/world writable, and group/world readable unless `permissions.allowReadableByOthers` is true. On Windows, the helper queries owner, DACL, and locality from the same open descriptor that supplies the bytes. The native query returns the 32-bit volume serial and 64-bit file-index projection used by Node, which must equal Node's bigint descriptor receipt before its ACL facts are trusted. This avoids JavaScript number rounding but does not represent the full 128-bit file identity available on ReFS. Only the current user, LocalSystem, and built-in Administrators are trusted owner classes.
|
|
31
31
|
|
|
32
|
-
|
|
32
|
+
Windows secure reads require the matching current optional native package. A missing or stale helper, fd-to-handle conversion failure, denied `READ_CONTROL`, remote handle, incomplete descriptor, or unsupported ACE form rejects with `permission-unverified` before content is read. A malformed or different handle identity rejects with `path-mismatch`. There is no pathname-command fallback for `readSecureFile()`; the standalone reporting APIs in [`permissions`](permissions.md) retain their documented fallbacks. `permissions.allowInsecure` remains the explicit escape hatch and bypasses the ACL query.
|
|
33
|
+
|
|
34
|
+
Descriptor, pathname, and realpath identity checks use bigint stats internally to avoid JavaScript number rounding. The returned `stat` remains a normal Node `Stats` object with numeric fields. A zero Windows device or inode is unverified, never a match: the helper re-inspects that identity once using the same descriptor or pathname, then rejects persistent ambiguity with `path-mismatch`. A definite mismatch rejects immediately; retries retain known identity components and still enforce symlink policy.
|
|
33
35
|
|
|
34
36
|
## Options
|
|
35
37
|
|
|
@@ -60,8 +62,14 @@ type SecureFileReadOptions = {
|
|
|
60
62
|
|
|
61
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.
|
|
62
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
|
+
|
|
63
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.
|
|
64
70
|
|
|
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.
|
|
72
|
+
|
|
65
73
|
`permissions.allowInsecure` bypasses only permission checks. Neither it nor `inject.platform` changes filesystem identity verification, which always uses the actual process platform. `trust.allowSymlink` permits an alias but still requires its target and realpath to match the opened descriptor.
|
|
66
74
|
|
|
67
75
|
## Errors
|
|
@@ -70,30 +78,27 @@ type SecureFileReadOptions = {
|
|
|
70
78
|
|
|
71
79
|
| Code | Meaning |
|
|
72
80
|
|---|---|
|
|
73
|
-
| `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. |
|
|
74
82
|
| `not-found` | The path could not be stat'd before open. |
|
|
75
83
|
| `not-file` | The opened target is not a regular file. |
|
|
76
84
|
| `symlink` | The path is a symlink and `trust.allowSymlink` is false. |
|
|
77
85
|
| `hardlink` | The descriptor, pathname, or realpath has more than one link. |
|
|
78
86
|
| `path-mismatch` | The path or realpath changed between open and verification, or filesystem identity could not be verified after bounded re-inspection. |
|
|
79
87
|
| `outside-workspace` | `realPath` is outside `trust.trustedDirs`. |
|
|
80
|
-
| `permission-unverified` | Required mode/ACL checks could not be completed. |
|
|
88
|
+
| `permission-unverified` | Required mode/ACL checks could not be completed, including when descriptor-bound Windows inspection is unavailable. |
|
|
81
89
|
| `insecure-permissions` | Mode bits or ACLs grant broader access than allowed. |
|
|
82
|
-
| `not-owned` | POSIX owner uid is not the
|
|
90
|
+
| `not-owned` | POSIX owner uid is not the process's effective uid. |
|
|
83
91
|
| `too-large` | File size or bytes read exceeded `maxBytes`. |
|
|
84
92
|
| `timeout` | `timeoutMs` elapsed while reading. |
|
|
85
93
|
|
|
86
|
-
Windows inspection failures
|
|
87
|
-
and
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
errors also retain their original execFile exception in the cause chain.
|
|
95
|
-
Treat causes as restricted local diagnostic data. No retries are performed,
|
|
96
|
-
and verification order and rejection conditions are unchanged.
|
|
94
|
+
Windows descriptor-inspection failures are operational `permission-unverified`
|
|
95
|
+
errors and refuse the read. The original native exception is retained as
|
|
96
|
+
`cause`; treat causes as restricted local diagnostic data. No pathname or ACL
|
|
97
|
+
content is copied into the display message. Test adapters that simulate Windows
|
|
98
|
+
on another operating system retain the standalone pathname inspector's
|
|
99
|
+
structured command diagnostics (`ownerError`, `command`, `durationMs`,
|
|
100
|
+
`timedOut`, `exitCode`, `signal`, and bounded escaped `stderr`). Actual Windows
|
|
101
|
+
secure reads do not start those commands. No retries are performed.
|
|
97
102
|
|
|
98
103
|
## See also
|
|
99
104
|
|
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
|
|
|
@@ -45,7 +47,31 @@ If you need full sandboxing, run the worker under reduced privileges (uid, conta
|
|
|
45
47
|
|
|
46
48
|
### Path traversal and absolute paths
|
|
47
49
|
|
|
48
|
-
Every path is resolved against the canonicalized real path of the root, then checked
|
|
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.
|
|
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.
|
|
49
75
|
|
|
50
76
|
### Symlinks (read side)
|
|
51
77
|
|
|
@@ -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
|
|
|
@@ -72,12 +72,37 @@ Loading serializes consumers for one ID through a sidecar lock, then creates `pr
|
|
|
72
72
|
|
|
73
73
|
Queue and failed directory creation fsyncs every newly-created parent edge from the leaf toward the trusted root. Enqueue and migration writes fsync the temp file and parent; claim, acknowledgement, quarantine, delivered-marker cleanup, and retirement transitions fsync every affected directory and propagate real sync failures. A transition may already be visible when a post-mutation sync fails, so retry the same operation to complete its crash-recovery state. Acknowledgement retries resync the queue directory even when both `.processing` and `.delivered` marker names are already absent, before reporting completion or rejecting a newer pending generation; quarantine retries with only failed evidence resync that destination before repairing the vanished queue source.
|
|
74
74
|
|
|
75
|
-
`writeJsonDurableQueueEntry()` and migrations share strict parent synchronization inside the atomic writer's retained descriptor and per-path serialization lifetime, followed by published-file identity verification. If sync fails after publication, the write rejects without rolling back the published JSON; retrying writes the entry again and must complete its own sync. This is not a rollback, deduplication, or exactly-once guarantee. The generic `replaceFileAtomic({ syncParentDir: true })` option remains best-effort.
|
|
75
|
+
`writeJsonDurableQueueEntry()` and migrations share strict parent synchronization inside the atomic writer's retained descriptor and per-path serialization lifetime, followed by published-file identity verification. If sync fails after publication, the write rejects without rolling back the published JSON; retrying `writeJsonDurableQueueEntry()` writes the entry again and must complete its own sync. Loader retries resync an existing processing claim's parent under the transfer lock before calling `read`, even when a version-dependent callback would no longer request migration. Fresh claims and same-directory source retirement already complete that sync. This is not a rollback, deduplication, or exactly-once guarantee. The generic `replaceFileAtomic({ syncParentDir: true })` option remains best-effort.
|
|
76
|
+
|
|
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.
|
|
76
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.
|
|
80
84
|
|
|
85
|
+
Migrations stay bound to the exact processing file opened for that load. The
|
|
86
|
+
read descriptor remains pinned while the callback runs outside the transfer
|
|
87
|
+
lock; after the callback returns, migration reacquires the lock and rechecks the
|
|
88
|
+
claim before publication. If another consumer acknowledged, quarantined, or
|
|
89
|
+
replaced that claim, the migration rejects with `FsSafeError("path-mismatch")`
|
|
90
|
+
and leaves the newer generation or failed evidence intact. A stale migration
|
|
91
|
+
rejects both single and batch loads; ordinary callback failures retain their
|
|
92
|
+
existing single-load rejection and batch-skip behavior.
|
|
93
|
+
|
|
94
|
+
On Windows, migration releases its read pin once at this publication boundary
|
|
95
|
+
because an open target can block replacement. It rechecks the exact pathname
|
|
96
|
+
identity after the asynchronous close while still holding the transfer lock.
|
|
97
|
+
POSIX retains the read pin through publication. Other readers keep ownership
|
|
98
|
+
of their handles; Windows sharing denials still reject and can be retried after
|
|
99
|
+
those readers close.
|
|
100
|
+
|
|
101
|
+
Generation arbitration requires consumers to use the transfer lock. As with
|
|
102
|
+
[atomic writes](atomic.md#beforerename), identity checks and pathname replacement
|
|
103
|
+
are separate operations; use a trusted writable parent or OS isolation against
|
|
104
|
+
processes that ignore the lock and mutate queue paths concurrently.
|
|
105
|
+
|
|
81
106
|
Queue entry reads verify lossless file identities before opening, on the opened
|
|
82
107
|
descriptor, and at the current pathname before reading bytes. POSIX opens are
|
|
83
108
|
nonblocking, so a raced FIFO is rejected rather than stalling a consumer. On Windows, an
|