@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/dist/root-write-mode.js
CHANGED
|
@@ -1,26 +1,33 @@
|
|
|
1
1
|
import fsSync from "node:fs";
|
|
2
2
|
import fs from "node:fs/promises";
|
|
3
3
|
import { FsSafeError } from "./errors.js";
|
|
4
|
-
import { isNotFoundPathError
|
|
4
|
+
import { isNotFoundPathError } from "./path.js";
|
|
5
5
|
import { resolveReadOpenFlags } from "./read-open-flags.js";
|
|
6
6
|
import { realpathSync } from "./realpath.js";
|
|
7
7
|
import { hardlinkedPathNotAllowedError, outsideWorkspaceError } from "./root-errors.js";
|
|
8
8
|
import { inspectFileIdentity } from "./strict-file-identity.js";
|
|
9
|
+
import { admitPathInsideRoot } from "./root-boundary.js";
|
|
9
10
|
// The caller has already resolved and guarded this write target.
|
|
10
11
|
export async function inheritWriteTargetMode(params) {
|
|
11
12
|
try {
|
|
12
|
-
const
|
|
13
|
+
const admittedTarget = admitPathInsideRoot({
|
|
14
|
+
rootPath: params.rootWithSep,
|
|
15
|
+
candidatePath: params.targetPath,
|
|
16
|
+
rootIdentity: params.rootIdentity,
|
|
17
|
+
});
|
|
18
|
+
if (!admittedTarget)
|
|
19
|
+
throw outsideWorkspaceError();
|
|
20
|
+
const targetPath = admittedTarget.path;
|
|
21
|
+
const existing = await inspectFileIdentity(() => fsSync.lstatSync(targetPath, { bigint: true }));
|
|
13
22
|
if (existing.isSymbolicLink())
|
|
14
23
|
throw new FsSafeError("path-alias", "path alias escape blocked");
|
|
15
24
|
if (!existing.isFile())
|
|
16
25
|
throw new FsSafeError("not-file", "not a file");
|
|
17
26
|
if (existing.nlink > 1n)
|
|
18
27
|
throw hardlinkedPathNotAllowedError();
|
|
19
|
-
if (!isPathInside(params.rootWithSep, params.targetPath))
|
|
20
|
-
throw outsideWorkspaceError();
|
|
21
28
|
// Preserve read-open admission of the pre-existing destination. access(2)
|
|
22
29
|
// is not equivalent: it ignores ACLs on Windows and capabilities on Linux.
|
|
23
|
-
const handle = await fs.open(
|
|
30
|
+
const handle = await fs.open(targetPath, resolveReadOpenFlags());
|
|
24
31
|
try {
|
|
25
32
|
// Bind admission to the inode whose metadata is inherited.
|
|
26
33
|
await inspectFileIdentity(() => fsSync.fstatSync(handle.fd, { bigint: true }), existing);
|
|
@@ -31,9 +38,15 @@ export async function inheritWriteTargetMode(params) {
|
|
|
31
38
|
try {
|
|
32
39
|
// A parent can change after guarded resolution. Do not inherit metadata
|
|
33
40
|
// from an outside inode, even if the parent is restored before publication.
|
|
34
|
-
const
|
|
35
|
-
|
|
41
|
+
const observedRealPath = realpathSync.native(targetPath);
|
|
42
|
+
const admittedRealPath = admitPathInsideRoot({
|
|
43
|
+
rootPath: params.rootWithSep,
|
|
44
|
+
candidatePath: observedRealPath,
|
|
45
|
+
rootIdentity: params.rootIdentity,
|
|
46
|
+
});
|
|
47
|
+
if (!admittedRealPath)
|
|
36
48
|
throw outsideWorkspaceError();
|
|
49
|
+
const realPath = admittedRealPath.path;
|
|
37
50
|
await inspectFileIdentity(async () => {
|
|
38
51
|
const current = fsSync.statSync(realPath, { bigint: true });
|
|
39
52
|
if (!current.isFile())
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { type BigIntStats } from "node:fs";
|
|
2
|
+
import type { FileHandle } from "node:fs/promises";
|
|
3
|
+
import type { AnyAsyncDirectoryGuard } from "./directory-guard.js";
|
|
4
|
+
import type { RootContext } from "./root-context.js";
|
|
5
|
+
import type { RootWriteOptions } from "./root-options.js";
|
|
6
|
+
export declare function finishRootFallbackWrite(params: {
|
|
7
|
+
root: RootContext;
|
|
8
|
+
targetPath: string;
|
|
9
|
+
handle: FileHandle;
|
|
10
|
+
identity: BigIntStats;
|
|
11
|
+
parentGuard: AnyAsyncDirectoryGuard;
|
|
12
|
+
mode: number;
|
|
13
|
+
options: RootWriteOptions & {
|
|
14
|
+
data: string | Buffer;
|
|
15
|
+
};
|
|
16
|
+
openForCompatibility: () => Promise<{
|
|
17
|
+
opened: {
|
|
18
|
+
handle: FileHandle;
|
|
19
|
+
};
|
|
20
|
+
identity: BigIntStats;
|
|
21
|
+
}>;
|
|
22
|
+
onVerificationFailure: (error: unknown) => void;
|
|
23
|
+
}): Promise<void>;
|
|
24
|
+
//# sourceMappingURL=root-write-publication.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"root-write-publication.d.ts","sourceRoot":"","sources":["../src/root-write-publication.ts"],"names":[],"mappings":"AAAA,OAAe,EAAE,KAAK,WAAW,EAAE,MAAM,SAAS,CAAC;AACnD,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,kBAAkB,CAAC;AAGnD,OAAO,KAAK,EAAE,sBAAsB,EAAE,MAAM,sBAAsB,CAAC;AAKnE,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAC;AACrD,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,mBAAmB,CAAC;AAK1D,wBAAsB,uBAAuB,CAAC,MAAM,EAAE;IACpD,IAAI,EAAE,WAAW,CAAC;IAClB,UAAU,EAAE,MAAM,CAAC;IACnB,MAAM,EAAE,UAAU,CAAC;IACnB,QAAQ,EAAE,WAAW,CAAC;IACtB,WAAW,EAAE,sBAAsB,CAAC;IACpC,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,gBAAgB,GAAG;QAAE,IAAI,EAAE,MAAM,GAAG,MAAM,CAAA;KAAE,CAAC;IACtD,oBAAoB,EAAE,MAAM,OAAO,CAAC;QAAE,MAAM,EAAE;YAAE,MAAM,EAAE,UAAU,CAAA;SAAE,CAAC;QAAC,QAAQ,EAAE,WAAW,CAAA;KAAE,CAAC,CAAC;IAC/F,qBAAqB,EAAE,CAAC,KAAK,EAAE,OAAO,KAAK,IAAI,CAAC;CACjD,GAAG,OAAO,CAAC,IAAI,CAAC,CAoEhB"}
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import fsSync, {} from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { readFileHandleBounded } from "./bounded-read.js";
|
|
4
|
+
import { syncDirectoryBestEffort } from "./directory-durability.js";
|
|
5
|
+
import { FsSafeError } from "./errors.js";
|
|
6
|
+
import { sameFileIdentityForCleanup, sha256Hex } from "./file-identity.js";
|
|
7
|
+
import { cleanupPinnedFilePath } from "./replace-file-temp-owner.js";
|
|
8
|
+
import { verifyAtomicWriteResult } from "./root-write-verification.js";
|
|
9
|
+
// Compatibility callers hold the lock throughout this operation. The caller
|
|
10
|
+
// owns the original write handle; this helper owns a content-verified reopen.
|
|
11
|
+
export async function finishRootFallbackWrite(params) {
|
|
12
|
+
let handle = params.handle;
|
|
13
|
+
let identity = params.identity;
|
|
14
|
+
let accepted;
|
|
15
|
+
const compatibility = params.options.renameIdentity === "verify-content-with-lock";
|
|
16
|
+
const verify = async () => {
|
|
17
|
+
try {
|
|
18
|
+
await verifyAtomicWriteResult({
|
|
19
|
+
root: params.root, targetPath: params.targetPath, fd: handle.fd,
|
|
20
|
+
expectedIdentity: identity, parentGuard: params.parentGuard,
|
|
21
|
+
});
|
|
22
|
+
}
|
|
23
|
+
catch (error) {
|
|
24
|
+
params.onVerificationFailure(error);
|
|
25
|
+
throw error;
|
|
26
|
+
}
|
|
27
|
+
};
|
|
28
|
+
try {
|
|
29
|
+
if (compatibility) {
|
|
30
|
+
const current = fsSync.lstatSync(params.targetPath, { bigint: true });
|
|
31
|
+
if (current.isSymbolicLink() || !current.isFile()) {
|
|
32
|
+
throw new FsSafeError("path-mismatch", "fallback target changed during write");
|
|
33
|
+
}
|
|
34
|
+
if (process.platform === "win32" && (current.dev === 0n || current.ino === 0n)) {
|
|
35
|
+
// Opaque pathname stats are not evidence of rename identity drift. The
|
|
36
|
+
// strict verifier can reopen against the retained descriptor's exact
|
|
37
|
+
// identity without reading bytes; a different object must still fail.
|
|
38
|
+
await verify();
|
|
39
|
+
}
|
|
40
|
+
else if (!sameFileIdentityForCleanup(current, identity)) {
|
|
41
|
+
const opened = await params.openForCompatibility();
|
|
42
|
+
accepted = handle = opened.opened.handle;
|
|
43
|
+
identity = opened.identity;
|
|
44
|
+
// Admit the destination under the retained Root and parent before any
|
|
45
|
+
// read, chmod, or sync; the original temp no longer proves its identity.
|
|
46
|
+
await verify();
|
|
47
|
+
const data = params.options.data;
|
|
48
|
+
const expectedBytes = typeof data === "string"
|
|
49
|
+
? Buffer.byteLength(data, params.options.encoding ?? "utf8") : data.byteLength;
|
|
50
|
+
const content = await readFileHandleBounded(handle, expectedBytes).catch((error) => {
|
|
51
|
+
if (error instanceof FsSafeError && error.code === "too-large") {
|
|
52
|
+
throw new FsSafeError("path-mismatch", "fallback target changed during write", { cause: error });
|
|
53
|
+
}
|
|
54
|
+
throw error;
|
|
55
|
+
});
|
|
56
|
+
if (sha256Hex(content) !== sha256Hex(data, params.options.encoding)) {
|
|
57
|
+
throw new FsSafeError("path-mismatch", "fallback target changed during write");
|
|
58
|
+
}
|
|
59
|
+
await verify();
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
// Final mode belongs to the accepted object, including on rename-unstable
|
|
63
|
+
// filesystems; never chmod the old temp and assume its mode was transferred.
|
|
64
|
+
try {
|
|
65
|
+
await handle.chmod(params.mode);
|
|
66
|
+
}
|
|
67
|
+
catch (error) {
|
|
68
|
+
await cleanupPinnedFilePath({
|
|
69
|
+
pathname: params.targetPath, handle, identity, parentGuard: params.parentGuard,
|
|
70
|
+
});
|
|
71
|
+
throw error;
|
|
72
|
+
}
|
|
73
|
+
if (params.options.durable !== false)
|
|
74
|
+
await handle.sync();
|
|
75
|
+
await verify();
|
|
76
|
+
if (params.options.durable !== false) {
|
|
77
|
+
await syncDirectoryBestEffort(path.dirname(params.targetPath));
|
|
78
|
+
if (compatibility)
|
|
79
|
+
await verify();
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
finally {
|
|
83
|
+
await accepted?.close().catch(() => undefined);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"root-write-verification.d.ts","sourceRoot":"","sources":["../src/root-write-verification.ts"],"names":[],"mappings":"AAEA,OAAO,EAA6B,KAAK,sBAAsB,EAAE,MAAM,sBAAsB,CAAC;AAK9F,OAAO,KAAK,EAAE,sBAAsB,EAAE,MAAM,mBAAmB,CAAC;AAEhE,OAAO,EAA6B,KAAK,WAAW,EAAE,MAAM,mBAAmB,CAAC;
|
|
1
|
+
{"version":3,"file":"root-write-verification.d.ts","sourceRoot":"","sources":["../src/root-write-verification.ts"],"names":[],"mappings":"AAEA,OAAO,EAA6B,KAAK,sBAAsB,EAAE,MAAM,sBAAsB,CAAC;AAK9F,OAAO,KAAK,EAAE,sBAAsB,EAAE,MAAM,mBAAmB,CAAC;AAEhE,OAAO,EAA6B,KAAK,WAAW,EAAE,MAAM,mBAAmB,CAAC;AAIhF,wBAAsB,uBAAuB,CAAC,MAAM,EAAE;IACpD,IAAI,EAAE,WAAW,CAAC;IAClB,UAAU,EAAE,MAAM,CAAC;IACnB,EAAE,EAAE,MAAM,CAAC;IACX,gBAAgB,EAAE,sBAAsB,CAAC;IACzC,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,WAAW,EAAE,sBAAsB,CAAC;CACrC,GAAG,OAAO,CAAC,IAAI,CAAC,CAgGhB"}
|
|
@@ -4,10 +4,11 @@ import { assertAsyncDirectoryGuard } from "./directory-guard.js";
|
|
|
4
4
|
import { FsSafeError } from "./errors.js";
|
|
5
5
|
import { sameFileIdentity } from "./file-identity.js";
|
|
6
6
|
import { resolveOpenedFileRealPathForFd } from "./opened-realpath.js";
|
|
7
|
-
import { assertNoUnsafeDeviceReadPath, hasNodeErrorCode, isNotFoundPathError,
|
|
7
|
+
import { assertNoUnsafeDeviceReadPath, hasNodeErrorCode, isNotFoundPathError, isSymlinkOpenError } from "./path.js";
|
|
8
8
|
import { resolveReadOpenFlags } from "./read-open-flags.js";
|
|
9
9
|
import { assertRootIdentityCurrent } from "./root-context.js";
|
|
10
10
|
import { fileNotFoundError, hardlinkedPathNotAllowedError, outsideWorkspaceError } from "./root-errors.js";
|
|
11
|
+
import { admitPathInsideRoot } from "./root-boundary.js";
|
|
11
12
|
export async function verifyAtomicWriteResult(params) {
|
|
12
13
|
let needsPathOpen = false;
|
|
13
14
|
const assertFile = (stat) => {
|
|
@@ -47,9 +48,15 @@ export async function verifyAtomicWriteResult(params) {
|
|
|
47
48
|
// This descriptor remains owned by the writer, even when final mode forbids opens.
|
|
48
49
|
const stat = assertDescriptor();
|
|
49
50
|
assertPath(fsSync.lstatSync(params.targetPath, { bigint: true }));
|
|
50
|
-
const { realPath } = await resolveOpenedFileRealPathForFd(params.fd, stat, params.targetPath);
|
|
51
|
-
|
|
52
|
-
|
|
51
|
+
const { realPath, stat: resolvedStat } = await resolveOpenedFileRealPathForFd(params.fd, stat, params.targetPath);
|
|
52
|
+
// Consume the resolver's observation only in this pass; the checks after
|
|
53
|
+
// the directory guards must still sample the current path and descriptor.
|
|
54
|
+
assertPath(resolvedStat);
|
|
55
|
+
if (!admitPathInsideRoot({
|
|
56
|
+
rootPath: params.root.rootReal,
|
|
57
|
+
candidatePath: realPath,
|
|
58
|
+
rootIdentity: params.root.rootIdentity,
|
|
59
|
+
})) {
|
|
53
60
|
throw outsideWorkspaceError();
|
|
54
61
|
}
|
|
55
62
|
await assertAsyncDirectoryGuard(params.parentGuard);
|
|
@@ -74,9 +81,13 @@ export async function verifyAtomicWriteResult(params) {
|
|
|
74
81
|
try {
|
|
75
82
|
const reopenedStat = assertDescriptor(opened.fd);
|
|
76
83
|
assertPath(fsSync.lstatSync(params.targetPath, { bigint: true }));
|
|
77
|
-
const { realPath: reopenedPath } = await resolveOpenedFileRealPathForFd(opened.fd, reopenedStat, params.targetPath);
|
|
78
|
-
assertPath(
|
|
79
|
-
if (!
|
|
84
|
+
const { realPath: reopenedPath, stat: reopenedPathStat } = await resolveOpenedFileRealPathForFd(opened.fd, reopenedStat, params.targetPath);
|
|
85
|
+
assertPath(reopenedPathStat);
|
|
86
|
+
if (!admitPathInsideRoot({
|
|
87
|
+
rootPath: params.root.rootReal,
|
|
88
|
+
candidatePath: reopenedPath,
|
|
89
|
+
rootIdentity: params.root.rootIdentity,
|
|
90
|
+
})) {
|
|
80
91
|
throw outsideWorkspaceError();
|
|
81
92
|
}
|
|
82
93
|
await assertAsyncDirectoryGuard(params.parentGuard);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"safe-path-segment.d.ts","sourceRoot":"","sources":["../src/safe-path-segment.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"safe-path-segment.d.ts","sourceRoot":"","sources":["../src/safe-path-segment.ts"],"names":[],"mappings":"AAWA,MAAM,MAAM,sBAAsB,GAAG;IACnC,cAAc,CAAC,EAAE,OAAO,CAAC;IACzB,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB,CAAC;AAEF,wBAAgB,mBAAmB,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAE1D;AAED,wBAAgB,iCAAiC,CAAC,KAAK,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,MAAM,CAKtF;AAED,wBAAgB,eAAe,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAUrD;AAED,wBAAgB,iBAAiB,CAC/B,OAAO,EAAE,MAAM,EACf,OAAO,GAAE,sBAA2B,GACnC,OAAO,CAaT;AAED,wBAAgB,qBAAqB,CACnC,OAAO,EAAE,MAAM,EACf,OAAO,GAAE,sBAA2B,GACnC,MAAM,CAUR;AAED,wBAAgB,uBAAuB,CACrC,KAAK,EAAE,MAAM,EACb,QAAQ,EAAE,MAAM,EAChB,OAAO,GAAE,sBAA2B,GACnC,MAAM,CAWR;AAED,wBAAgB,oBAAoB,CAClC,MAAM,EAAE,MAAM,EACd,OAAO,GAAE,sBAA2B,GACnC,MAAM,CAeR"}
|
|
@@ -4,12 +4,14 @@ const SAFE_DOT_PREFIX_PATH_SEGMENT_PATTERN = /^[A-Za-z0-9._-]+$/;
|
|
|
4
4
|
// Windows treats "C:name" as relative to the drive's current directory even
|
|
5
5
|
// though path.win32.isAbsolute() reports false.
|
|
6
6
|
const DRIVE_RELATIVE_PREFIX = /^[A-Za-z]:(?![\\/])/;
|
|
7
|
+
// A forward slash ends the segment, so "C:/file" still contains the "C:" segment.
|
|
8
|
+
const DRIVE_RELATIVE_SEGMENT = /(?:^|\/)[A-Za-z]:(?!\\)/;
|
|
7
9
|
const HYPHEN_CHAR_CODE = 0x2d;
|
|
8
10
|
export function isDriveRelativePath(value) {
|
|
9
11
|
return DRIVE_RELATIVE_PREFIX.test(value);
|
|
10
12
|
}
|
|
11
13
|
export function assertNoDriveRelativePathSegments(value, label) {
|
|
12
|
-
if (
|
|
14
|
+
if (DRIVE_RELATIVE_SEGMENT.test(value)) {
|
|
13
15
|
throw new FsSafeError("invalid-path", `${label} must not contain a drive letter`);
|
|
14
16
|
}
|
|
15
17
|
return value;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"secret-file.d.ts","sourceRoot":"","sources":["../src/secret-file.ts"],"names":[],"mappings":"AAAA,OAAW,EAAE,KAAK,WAAW,EAAE,MAAM,SAAS,CAAC;AAM/C,OAAO,EAAkF,KAAK,mBAAmB,EAAE,MAAM,sBAAsB,CAAC;AAUhJ,OAAO,EAML,KAAK,qBAAqB,EAC3B,MAAM,yBAAyB,CAAC;
|
|
1
|
+
{"version":3,"file":"secret-file.d.ts","sourceRoot":"","sources":["../src/secret-file.ts"],"names":[],"mappings":"AAAA,OAAW,EAAE,KAAK,WAAW,EAAE,MAAM,SAAS,CAAC;AAM/C,OAAO,EAAkF,KAAK,mBAAmB,EAAE,MAAM,sBAAsB,CAAC;AAUhJ,OAAO,EAML,KAAK,qBAAqB,EAC3B,MAAM,yBAAyB,CAAC;AAQjC,eAAO,MAAM,uBAAuB,MAAQ,CAAC;AAC7C,eAAO,MAAM,wBAAwB,MAAQ,CAAC;AAE9C,wBAAgB,kBAAkB,CAChC,QAAQ,EAAE,MAAM,EAChB,KAAK,EAAE,MAAM,EACb,OAAO,GAAE,qBAA0B,GAClC,MAAM,CAmER;AAED,wBAAgB,qBAAqB,CACnC,QAAQ,EAAE,MAAM,GAAG,SAAS,EAC5B,KAAK,EAAE,MAAM,EACb,OAAO,GAAE,qBAA0B,GAClC,MAAM,GAAG,SAAS,CAUpB;AA8JD,KAAK,qBAAqB,GAAG;IAC3B,OAAO,EAAE,MAAM,CAAC;IAChB,QAAQ,EAAE,MAAM,CAAC;IACjB,OAAO,EAAE,MAAM,GAAG,UAAU,CAAC;IAC7B,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,OAAO,CAAC,EAAE,OAAO,CAAC;CACnB,CAAC;AA+BF,wBAAsB,sBAAsB,CAC1C,MAAM,EAAE,IAAI,CAAC,qBAAqB,EAAE,SAAS,CAAC,GAC7C,OAAO,CAAC;IACT,IAAI,EAAE,MAAM,CAAC;IACb,SAAS,EAAE,mBAAmB,CAAC,WAAW,CAAC,CAAC;IAC5C,WAAW,EAAE,mBAAmB,CAAC,WAAW,CAAC,CAAC;IAC9C,QAAQ,EAAE,MAAM,CAAC;IACjB,aAAa,EAAE,MAAM,CAAC;CACvB,CAAC,CAgCD;AAwDD,wBAAsB,qBAAqB,CAAC,MAAM,EAAE,qBAAqB,GAAG,OAAO,CAAC,IAAI,CAAC,CAUxF;AAED,wBAAsB,sBAAsB,CAAC,MAAM,EAAE,qBAAqB,GAAG,OAAO,CAAC,IAAI,CAAC,CAoBzF"}
|
package/dist/secret-file.js
CHANGED
|
@@ -17,32 +17,36 @@ import { verifyAtomicWriteResult } from "./root-write-verification.js";
|
|
|
17
17
|
import { assertSecretFilePreview, resolveSecretReadPolicy, secretPathErrorCode, secretReadError, trimSecretFileContent, } from "./secret-read-policy.js";
|
|
18
18
|
import { inspectFileIdentity, inspectFileIdentitySync } from "./strict-file-identity.js";
|
|
19
19
|
import { serializePathWrite } from "./write-queue.js";
|
|
20
|
+
import { assertNoWindowsPathAlias, resolvePathPreservingWindowsRoot, } from "./windows-path-alias.js";
|
|
20
21
|
export const PRIVATE_SECRET_DIR_MODE = 0o700;
|
|
21
22
|
export const PRIVATE_SECRET_FILE_MODE = 0o600;
|
|
22
23
|
export function readSecretFileSync(filePath, label, options = {}) {
|
|
23
24
|
const { resolvedPath, maxBytes } = resolveSecretReadPolicy(filePath, label, options);
|
|
24
|
-
|
|
25
|
-
const stat = options.rejectSymlink
|
|
26
|
-
? fs.lstatSync(resolvedPath, { bigint: true })
|
|
27
|
-
: fs.statSync(resolvedPath, { bigint: true });
|
|
28
|
-
if (options.rejectSymlink && stat.isSymbolicLink()) {
|
|
29
|
-
throw new FsSafeError("symlink", symlinkMessage);
|
|
30
|
-
}
|
|
31
|
-
return stat;
|
|
32
|
-
}
|
|
25
|
+
let rejectSymlink;
|
|
33
26
|
let previewStat;
|
|
34
27
|
try {
|
|
35
28
|
assertNoUnsafeDeviceReadPath(resolvedPath);
|
|
29
|
+
rejectSymlink = Boolean(options.rejectSymlink);
|
|
36
30
|
previewStat = inspectFileIdentitySync(() => inspectInput(`${label} file at ${resolvedPath} must not be a symlink.`));
|
|
37
31
|
}
|
|
38
32
|
catch (error) {
|
|
39
33
|
throw secretReadError(error instanceof FsSafeError ? error.code : secretPathErrorCode(error), "inspect", label, resolvedPath, error);
|
|
40
34
|
}
|
|
41
|
-
|
|
35
|
+
function inspectInput(symlinkMessage) {
|
|
36
|
+
const stat = rejectSymlink
|
|
37
|
+
? fs.lstatSync(resolvedPath, { bigint: true })
|
|
38
|
+
: fs.statSync(resolvedPath, { bigint: true });
|
|
39
|
+
if (rejectSymlink && stat.isSymbolicLink()) {
|
|
40
|
+
throw new FsSafeError("symlink", symlinkMessage);
|
|
41
|
+
}
|
|
42
|
+
return stat;
|
|
43
|
+
}
|
|
44
|
+
const rejectHardlinks = options.rejectHardlinks !== false;
|
|
45
|
+
assertSecretFilePreview(previewStat, label, resolvedPath, maxBytes, rejectHardlinks);
|
|
42
46
|
const opened = openPinnedFileSync({
|
|
43
47
|
filePath: resolvedPath,
|
|
44
|
-
rejectPathSymlink:
|
|
45
|
-
rejectHardlinks
|
|
48
|
+
rejectPathSymlink: rejectSymlink,
|
|
49
|
+
rejectHardlinks,
|
|
46
50
|
});
|
|
47
51
|
if (!opened.ok) {
|
|
48
52
|
throw secretReadError(opened.reason === "path" ? "not-found" : "path-mismatch", "read", label, resolvedPath, opened.reason === "validation" ? new Error("security validation failed") : opened.error);
|
|
@@ -51,14 +55,14 @@ export function readSecretFileSync(filePath, label, options = {}) {
|
|
|
51
55
|
try {
|
|
52
56
|
const openedIdentity = inspectFileIdentitySync(() => {
|
|
53
57
|
const stat = fs.fstatSync(opened.fd, { bigint: true });
|
|
54
|
-
if (!stat.isFile() || (
|
|
58
|
+
if (!stat.isFile() || (rejectHardlinks && stat.nlink > 1n)) {
|
|
55
59
|
throw new FsSafeError("path-mismatch", "security validation failed");
|
|
56
60
|
}
|
|
57
61
|
return stat;
|
|
58
62
|
}, previewStat);
|
|
59
63
|
inspectFileIdentitySync(() => {
|
|
60
64
|
const stat = fs.lstatSync(opened.path, { bigint: true });
|
|
61
|
-
if (!stat.isFile() || (
|
|
65
|
+
if (!stat.isFile() || (rejectHardlinks && stat.nlink > 1n)) {
|
|
62
66
|
throw new FsSafeError("path-mismatch", "security validation failed");
|
|
63
67
|
}
|
|
64
68
|
return stat;
|
|
@@ -152,8 +156,8 @@ async function inspectPrivateDirectory(directory, kind) {
|
|
|
152
156
|
});
|
|
153
157
|
}
|
|
154
158
|
async function ensurePrivateDirectory(rootDir, targetDir, mode) {
|
|
155
|
-
const resolvedRoot =
|
|
156
|
-
const resolvedTarget =
|
|
159
|
+
const resolvedRoot = resolvePathPreservingWindowsRoot(rootDir);
|
|
160
|
+
const resolvedTarget = resolvePathPreservingWindowsRoot(targetDir);
|
|
157
161
|
let rootStat = await inspectPrivateDirectory(resolvedRoot, "root").catch((error) => {
|
|
158
162
|
if (error.code !== "ENOENT")
|
|
159
163
|
throw error;
|
|
@@ -228,21 +232,42 @@ async function ensurePrivateDirectory(rootDir, targetDir, mode) {
|
|
|
228
232
|
}
|
|
229
233
|
return { rootGuard, parentGuard: targetGuard };
|
|
230
234
|
}
|
|
235
|
+
function snapshotSecretFileWriteParams(params, rootDir, filePath) {
|
|
236
|
+
return {
|
|
237
|
+
rootDir,
|
|
238
|
+
filePath,
|
|
239
|
+
content: params.content,
|
|
240
|
+
mode: params.mode,
|
|
241
|
+
dirMode: params.dirMode,
|
|
242
|
+
durable: params.durable,
|
|
243
|
+
};
|
|
244
|
+
}
|
|
231
245
|
async function secretFileWriteQueueKey(filePath) {
|
|
246
|
+
assertNoWindowsPathAlias(filePath, "filesystem", "private secret path uses a Windows filesystem namespace alias");
|
|
232
247
|
try {
|
|
233
248
|
return await canonicalPathFromExistingAncestor(filePath);
|
|
234
249
|
}
|
|
235
|
-
catch {
|
|
250
|
+
catch (error) {
|
|
251
|
+
if (error instanceof FsSafeError && error.code === "invalid-path")
|
|
252
|
+
throw error;
|
|
236
253
|
// Keep validation and its public error shape owned by the write path below.
|
|
237
|
-
|
|
254
|
+
const resolved = path.resolve(filePath);
|
|
255
|
+
assertNoWindowsPathAlias(resolved, "filesystem", "private secret path uses a Windows filesystem namespace alias");
|
|
256
|
+
return resolved;
|
|
238
257
|
}
|
|
239
258
|
}
|
|
240
259
|
// Internal preparation for private writers and their pre-write locks; not lasting authorization.
|
|
241
260
|
export async function prepareSecretFileWrite(params) {
|
|
261
|
+
const rootDir = params.rootDir;
|
|
262
|
+
const filePath = params.filePath;
|
|
263
|
+
assertNoWindowsPathAlias(rootDir, "filesystem", "private secret root uses a Windows filesystem namespace alias");
|
|
264
|
+
assertNoWindowsPathAlias(filePath, "filesystem", "private secret path uses a Windows filesystem namespace alias");
|
|
242
265
|
const mode = params.mode ?? PRIVATE_SECRET_FILE_MODE;
|
|
243
266
|
const dirMode = params.dirMode ?? PRIVATE_SECRET_DIR_MODE;
|
|
244
|
-
const resolvedRoot =
|
|
245
|
-
const resolvedFile = path.resolve(
|
|
267
|
+
const resolvedRoot = resolvePathPreservingWindowsRoot(rootDir);
|
|
268
|
+
const resolvedFile = path.resolve(filePath);
|
|
269
|
+
assertNoWindowsPathAlias(resolvedRoot, "filesystem", "private secret root uses a Windows filesystem namespace alias");
|
|
270
|
+
assertNoWindowsPathAlias(resolvedFile, "filesystem", "private secret path uses a Windows filesystem namespace alias");
|
|
246
271
|
assertPathWithinRoot(resolvedRoot, resolvedFile);
|
|
247
272
|
for (const [kind, value] of [["file", mode], ["directory", dirMode]]) {
|
|
248
273
|
if (!Number.isInteger(value) || value < 0 || value > 0o7777) {
|
|
@@ -254,8 +279,11 @@ export async function prepareSecretFileWrite(params) {
|
|
|
254
279
|
await assertAsyncDirectoryGuard(rootGuard);
|
|
255
280
|
await assertAsyncDirectoryGuard(parentGuard);
|
|
256
281
|
assertRealPathWithinRoot(rootGuard.realPath, parentGuard.realPath);
|
|
282
|
+
assertNoWindowsPathAlias(rootGuard.realPath, "filesystem", "private secret root uses a Windows filesystem namespace alias");
|
|
283
|
+
assertNoWindowsPathAlias(parentGuard.realPath, "filesystem", "private secret parent uses a Windows filesystem namespace alias");
|
|
257
284
|
const fileName = path.basename(resolvedFile);
|
|
258
285
|
const finalFilePath = path.join(parentGuard.realPath, fileName);
|
|
286
|
+
assertNoWindowsPathAlias(finalFilePath, "filesystem", "private secret path uses a Windows filesystem namespace alias");
|
|
259
287
|
return { mode, rootGuard, parentGuard, fileName, finalFilePath };
|
|
260
288
|
}
|
|
261
289
|
async function materializeSecretFileAtomic(params, createOnly) {
|
|
@@ -309,16 +337,26 @@ async function materializeSecretFileAtomic(params, createOnly) {
|
|
|
309
337
|
});
|
|
310
338
|
}
|
|
311
339
|
export async function writeSecretFileAtomic(params) {
|
|
312
|
-
const
|
|
340
|
+
const rootDir = params.rootDir;
|
|
341
|
+
const filePath = params.filePath;
|
|
342
|
+
assertNoWindowsPathAlias(rootDir, "filesystem", "private secret root uses a Windows filesystem namespace alias");
|
|
343
|
+
assertNoWindowsPathAlias(filePath, "filesystem", "private secret path uses a Windows filesystem namespace alias");
|
|
344
|
+
const ownedParams = snapshotSecretFileWriteParams(params, rootDir, filePath);
|
|
345
|
+
const canonicalPath = await secretFileWriteQueueKey(filePath);
|
|
313
346
|
await serializePathWrite(canonicalPath, async () => {
|
|
314
|
-
await materializeSecretFileAtomic(
|
|
347
|
+
await materializeSecretFileAtomic(ownedParams, false);
|
|
315
348
|
});
|
|
316
349
|
}
|
|
317
350
|
export async function createSecretFileAtomic(params) {
|
|
318
351
|
try {
|
|
319
|
-
const
|
|
352
|
+
const rootDir = params.rootDir;
|
|
353
|
+
const filePath = params.filePath;
|
|
354
|
+
assertNoWindowsPathAlias(rootDir, "filesystem", "private secret root uses a Windows filesystem namespace alias");
|
|
355
|
+
assertNoWindowsPathAlias(filePath, "filesystem", "private secret path uses a Windows filesystem namespace alias");
|
|
356
|
+
const ownedParams = snapshotSecretFileWriteParams(params, rootDir, filePath);
|
|
357
|
+
const canonicalPath = await secretFileWriteQueueKey(filePath);
|
|
320
358
|
await serializePathWrite(canonicalPath, async () => {
|
|
321
|
-
await materializeSecretFileAtomic(
|
|
359
|
+
await materializeSecretFileAtomic(ownedParams, true);
|
|
322
360
|
});
|
|
323
361
|
}
|
|
324
362
|
catch (error) {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"secret-read-async.d.ts","sourceRoot":"","sources":["../src/secret-read-async.ts"],"names":[],"mappings":"AAQA,OAAO,EAML,KAAK,qBAAqB,EAC3B,MAAM,yBAAyB,CAAC;
|
|
1
|
+
{"version":3,"file":"secret-read-async.d.ts","sourceRoot":"","sources":["../src/secret-read-async.ts"],"names":[],"mappings":"AAQA,OAAO,EAML,KAAK,qBAAqB,EAC3B,MAAM,yBAAyB,CAAC;AAGjC,wBAAsB,cAAc,CAClC,QAAQ,EAAE,MAAM,EAChB,KAAK,EAAE,MAAM,EACb,OAAO,GAAE,qBAA0B,GAClC,OAAO,CAAC,MAAM,CAAC,CA6DjB;AAED,wBAAsB,iBAAiB,CACrC,QAAQ,EAAE,MAAM,GAAG,SAAS,EAC5B,KAAK,EAAE,MAAM,EACb,OAAO,GAAE,qBAA0B,GAClC,OAAO,CAAC,MAAM,GAAG,SAAS,CAAC,CAQ7B"}
|
|
@@ -5,38 +5,43 @@ import { assertNoUnsafeDeviceReadPath } from "./device-path.js";
|
|
|
5
5
|
import { FsSafeError } from "./errors.js";
|
|
6
6
|
import { resolveReadOpenFlags } from "./read-open-flags.js";
|
|
7
7
|
import { realpathSync } from "./realpath.js";
|
|
8
|
-
import { inspectFileIdentity } from "./strict-file-identity.js";
|
|
8
|
+
import { inspectFileIdentity, inspectFileIdentitySync } from "./strict-file-identity.js";
|
|
9
9
|
import { assertSecretFilePreview, resolveSecretReadPolicy, secretPathErrorCode, secretReadError, trimSecretFileContent, } from "./secret-read-policy.js";
|
|
10
|
+
import { assertNoWindowsPathAlias } from "./windows-path-alias.js";
|
|
10
11
|
export async function readSecretFile(filePath, label, options = {}) {
|
|
11
12
|
const { resolvedPath, maxBytes } = resolveSecretReadPolicy(filePath, label, options);
|
|
12
|
-
|
|
13
|
-
const stat = options.rejectSymlink
|
|
14
|
-
? fsSync.lstatSync(resolvedPath, { bigint: true })
|
|
15
|
-
: fsSync.statSync(resolvedPath, { bigint: true });
|
|
16
|
-
if (options.rejectSymlink && stat.isSymbolicLink()) {
|
|
17
|
-
throw new FsSafeError("symlink", symlinkMessage);
|
|
18
|
-
}
|
|
19
|
-
return stat;
|
|
20
|
-
}
|
|
13
|
+
let rejectSymlink;
|
|
21
14
|
let previewStat;
|
|
22
15
|
try {
|
|
23
16
|
assertNoUnsafeDeviceReadPath(resolvedPath);
|
|
24
|
-
|
|
17
|
+
rejectSymlink = Boolean(options.rejectSymlink);
|
|
18
|
+
previewStat = inspectFileIdentitySync(() => inspectInput(`${label} file at ${resolvedPath} must not be a symlink.`));
|
|
25
19
|
}
|
|
26
20
|
catch (error) {
|
|
27
21
|
throw secretReadError(error instanceof FsSafeError ? error.code : secretPathErrorCode(error), "inspect", label, resolvedPath, error);
|
|
28
22
|
}
|
|
29
|
-
|
|
23
|
+
function inspectInput(symlinkMessage) {
|
|
24
|
+
const stat = rejectSymlink
|
|
25
|
+
? fsSync.lstatSync(resolvedPath, { bigint: true })
|
|
26
|
+
: fsSync.statSync(resolvedPath, { bigint: true });
|
|
27
|
+
if (rejectSymlink && stat.isSymbolicLink()) {
|
|
28
|
+
throw new FsSafeError("symlink", symlinkMessage);
|
|
29
|
+
}
|
|
30
|
+
return stat;
|
|
31
|
+
}
|
|
32
|
+
const rejectHardlinks = options.rejectHardlinks !== false;
|
|
33
|
+
assertSecretFilePreview(previewStat, label, resolvedPath, maxBytes, rejectHardlinks);
|
|
30
34
|
let handle;
|
|
31
35
|
let raw;
|
|
32
36
|
try {
|
|
33
37
|
const realPath = realpathSync.native(resolvedPath);
|
|
38
|
+
assertNoWindowsPathAlias(realPath, "filesystem", `${label} file path uses a Windows filesystem namespace alias`);
|
|
34
39
|
assertNoUnsafeDeviceReadPath(realPath);
|
|
35
40
|
handle = await fs.open(realPath, resolveReadOpenFlags());
|
|
36
41
|
const openedHandle = handle;
|
|
37
42
|
const openedStat = await inspectFileIdentity(async () => {
|
|
38
43
|
const stat = fsSync.fstatSync(openedHandle.fd, { bigint: true });
|
|
39
|
-
if (!stat.isFile() || (
|
|
44
|
+
if (!stat.isFile() || (rejectHardlinks && stat.nlink > 1n)) {
|
|
40
45
|
throw new FsSafeError("path-mismatch", "security validation failed");
|
|
41
46
|
}
|
|
42
47
|
return stat;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"secret-read-policy.d.ts","sourceRoot":"","sources":["../src/secret-read-policy.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,SAAS,CAAC;AAC3C,OAAO,EAAE,WAAW,EAAE,KAAK,eAAe,EAAE,MAAM,aAAa,CAAC;
|
|
1
|
+
{"version":3,"file":"secret-read-policy.d.ts","sourceRoot":"","sources":["../src/secret-read-policy.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,SAAS,CAAC;AAC3C,OAAO,EAAE,WAAW,EAAE,KAAK,eAAe,EAAE,MAAM,aAAa,CAAC;AAGhE,eAAO,MAAM,6BAA6B,QAAY,CAAC;AAEvD,MAAM,MAAM,qBAAqB,GAAG;IAClC,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,aAAa,CAAC,EAAE,OAAO,CAAC;IACxB,eAAe,CAAC,EAAE,OAAO,CAAC;CAC3B,CAAC;AAEF,wBAAgB,mBAAmB,CAAC,KAAK,EAAE,OAAO,GAAG,eAAe,CAGnE;AAED,wBAAgB,eAAe,CAC7B,IAAI,EAAE,eAAe,EACrB,MAAM,EAAE,SAAS,GAAG,MAAM,EAC1B,KAAK,EAAE,MAAM,EACb,YAAY,EAAE,MAAM,EACpB,KAAK,EAAE,OAAO,GACb,WAAW,CAGb;AAED,wBAAgB,uBAAuB,CACrC,IAAI,EAAE,WAAW,EACjB,KAAK,EAAE,MAAM,EACb,YAAY,EAAE,MAAM,EACpB,QAAQ,EAAE,MAAM,EAChB,eAAe,EAAE,OAAO,GACvB,IAAI,CAUN;AAED,wBAAgB,qBAAqB,CAAC,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,YAAY,EAAE,MAAM,GAAG,MAAM,CAM9F;AAED,wBAAgB,uBAAuB,CAAC,QAAQ,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,OAAO,EAAE,qBAAqB,GAAG;IACxG,YAAY,EAAE,MAAM,CAAC;IACrB,QAAQ,EAAE,MAAM,CAAC;CAClB,CAYA"}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { normalizeMaxBytes } from "./byte-budget.js";
|
|
2
2
|
import { resolveHomeRelativePath } from "./home-dir.js";
|
|
3
3
|
import { FsSafeError } from "./errors.js";
|
|
4
|
+
import { assertNoWindowsPathAlias } from "./windows-path-alias.js";
|
|
4
5
|
export const DEFAULT_SECRET_FILE_MAX_BYTES = 16 * 1024;
|
|
5
6
|
export function secretPathErrorCode(error) {
|
|
6
7
|
const code = error.code;
|
|
@@ -29,10 +30,13 @@ export function trimSecretFileContent(raw, label, resolvedPath) {
|
|
|
29
30
|
return secret;
|
|
30
31
|
}
|
|
31
32
|
export function resolveSecretReadPolicy(filePath, label, options) {
|
|
32
|
-
const
|
|
33
|
+
const trimmedPath = filePath.trim();
|
|
34
|
+
assertNoWindowsPathAlias(trimmedPath, "filesystem", `${label} file path uses a Windows filesystem namespace alias`);
|
|
35
|
+
const resolvedPath = resolveHomeRelativePath(trimmedPath);
|
|
33
36
|
if (!resolvedPath) {
|
|
34
37
|
throw new FsSafeError("invalid-path", `${label} file path is empty.`, { cause: undefined });
|
|
35
38
|
}
|
|
39
|
+
assertNoWindowsPathAlias(resolvedPath, "filesystem", `${label} file path uses a Windows filesystem namespace alias`);
|
|
36
40
|
const maxBytes = normalizeMaxBytes(options.maxBytes, {
|
|
37
41
|
defaultValue: DEFAULT_SECRET_FILE_MAX_BYTES,
|
|
38
42
|
});
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { BigIntStats, Stats } from "node:fs";
|
|
2
|
+
import type { PermissionCheck } from "./permissions.js";
|
|
3
|
+
type ExactIdentity = Pick<BigIntStats, "dev" | "ino">;
|
|
4
|
+
export declare function inspectSecureWindowsDescriptor(params: {
|
|
5
|
+
fd: number;
|
|
6
|
+
identity: ExactIdentity;
|
|
7
|
+
stat: Stats;
|
|
8
|
+
}): PermissionCheck;
|
|
9
|
+
export {};
|
|
10
|
+
//# sourceMappingURL=secure-file-windows.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"secure-file-windows.d.ts","sourceRoot":"","sources":["../src/secure-file-windows.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,KAAK,EAAE,MAAM,SAAS,CAAC;AASlD,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,kBAAkB,CAAC;AA4BxD,KAAK,aAAa,GAAG,IAAI,CAAC,WAAW,EAAE,KAAK,GAAG,KAAK,CAAC,CAAC;AAuHtD,wBAAgB,8BAA8B,CAAC,MAAM,EAAE;IACrD,EAAE,EAAE,MAAM,CAAC;IACX,QAAQ,EAAE,aAAa,CAAC;IACxB,IAAI,EAAE,KAAK,CAAC;CACb,GAAG,eAAe,CA0ClB"}
|