@openclaw/fs-safe 0.12.0 → 0.13.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +73 -0
- package/README.md +12 -7
- package/dist/absolute-path.d.ts.map +1 -1
- package/dist/absolute-path.js +32 -11
- package/dist/advanced.d.ts +2 -0
- package/dist/advanced.d.ts.map +1 -1
- package/dist/advanced.js +2 -0
- package/dist/archive-input.d.ts.map +1 -1
- package/dist/archive-input.js +6 -2
- package/dist/archive-merge.d.ts.map +1 -1
- package/dist/archive-merge.js +15 -2
- package/dist/archive-native.d.ts.map +1 -1
- package/dist/archive-native.js +7 -19
- package/dist/archive-plan.js +1 -1
- package/dist/archive-read.d.ts.map +1 -1
- package/dist/archive-read.js +18 -13
- package/dist/archive-staging.d.ts.map +1 -1
- package/dist/archive-staging.js +43 -10
- package/dist/archive-tar-inspect.d.ts.map +1 -1
- package/dist/archive-tar-inspect.js +4 -1
- package/dist/archive-zip-admission.d.ts +1 -1
- package/dist/archive-zip-admission.d.ts.map +1 -1
- package/dist/archive-zip-admission.js +2 -2
- package/dist/archive-zip-directory.d.ts +3 -0
- package/dist/archive-zip-directory.d.ts.map +1 -1
- package/dist/archive-zip-directory.js +13 -2
- package/dist/archive-zip-loader.d.ts +3 -2
- package/dist/archive-zip-loader.d.ts.map +1 -1
- package/dist/archive-zip-loader.js +30 -4
- package/dist/archive-zip-manifest.d.ts +5 -0
- package/dist/archive-zip-manifest.d.ts.map +1 -0
- package/dist/archive-zip-manifest.js +22 -0
- package/dist/archive-zip-names.d.ts +6 -1
- package/dist/archive-zip-names.d.ts.map +1 -1
- package/dist/archive-zip-names.js +35 -14
- package/dist/archive-zip-preflight.d.ts.map +1 -1
- package/dist/archive-zip-preflight.js +3 -2
- package/dist/archive.d.ts.map +1 -1
- package/dist/archive.js +9 -4
- package/dist/copy-file-input.d.ts +1 -1
- package/dist/copy-file-input.d.ts.map +1 -1
- package/dist/copy-file-input.js +2 -0
- package/dist/darwin-acl.d.ts +4 -0
- package/dist/darwin-acl.d.ts.map +1 -0
- package/dist/darwin-acl.js +24 -0
- package/dist/deny-mutations.d.ts.map +1 -1
- package/dist/deny-mutations.js +8 -2
- package/dist/directory-durability.d.ts.map +1 -1
- package/dist/directory-durability.js +67 -23
- package/dist/directory-entry-path.d.ts +3 -0
- package/dist/directory-entry-path.d.ts.map +1 -0
- package/dist/directory-entry-path.js +21 -0
- package/dist/directory-guard.d.ts +17 -1
- package/dist/directory-guard.d.ts.map +1 -1
- package/dist/directory-guard.js +134 -48
- package/dist/directory-mode-node.d.ts +12 -0
- package/dist/directory-mode-node.d.ts.map +1 -1
- package/dist/directory-mode-node.js +102 -4
- package/dist/effective-uid.d.ts +2 -0
- package/dist/effective-uid.d.ts.map +1 -0
- package/dist/effective-uid.js +25 -0
- package/dist/file-handle-transfer.d.ts.map +1 -1
- package/dist/file-handle-transfer.js +98 -27
- package/dist/file-hash.d.ts.map +1 -1
- package/dist/file-hash.js +3 -0
- package/dist/file-lock-sync.d.ts.map +1 -1
- package/dist/file-lock-sync.js +36 -6
- package/dist/file-lock.d.ts.map +1 -1
- package/dist/file-lock.js +29 -9
- package/dist/file-observation.d.ts +1 -1
- package/dist/file-observation.d.ts.map +1 -1
- package/dist/file-store-boundary.d.ts +6 -2
- package/dist/file-store-boundary.d.ts.map +1 -1
- package/dist/file-store-boundary.js +20 -65
- package/dist/file-store-copy-source.d.ts +5 -0
- package/dist/file-store-copy-source.d.ts.map +1 -0
- package/dist/file-store-copy-source.js +31 -0
- package/dist/file-store-path.d.ts.map +1 -1
- package/dist/file-store-path.js +4 -1
- package/dist/file-store-sync-directory.d.ts +16 -0
- package/dist/file-store-sync-directory.d.ts.map +1 -0
- package/dist/file-store-sync-directory.js +349 -0
- package/dist/file-store.d.ts.map +1 -1
- package/dist/file-store.js +11 -28
- package/dist/filename.d.ts.map +1 -1
- package/dist/filename.js +65 -20
- package/dist/fs.d.ts.map +1 -1
- package/dist/fs.js +3 -2
- package/dist/guarded-mkdir.d.ts +8 -0
- package/dist/guarded-mkdir.d.ts.map +1 -1
- package/dist/guarded-mkdir.js +176 -27
- package/dist/guest.d.ts.map +1 -1
- package/dist/guest.js +4 -1
- package/dist/home-dir.d.ts.map +1 -1
- package/dist/home-dir.js +73 -10
- package/dist/install-path.d.ts.map +1 -1
- package/dist/install-path.js +54 -19
- package/dist/json-durable-queue-ownership.d.ts +6 -0
- package/dist/json-durable-queue-ownership.d.ts.map +1 -1
- package/dist/json-durable-queue-ownership.js +89 -42
- package/dist/json-durable-queue-paths.d.ts +11 -0
- package/dist/json-durable-queue-paths.d.ts.map +1 -0
- package/dist/json-durable-queue-paths.js +42 -0
- package/dist/json-durable-queue-read.d.ts.map +1 -1
- package/dist/json-durable-queue-read.js +2 -0
- package/dist/json-durable-queue.d.ts.map +1 -1
- package/dist/json-durable-queue.js +50 -58
- package/dist/json-store.d.ts.map +1 -1
- package/dist/json-store.js +5 -1
- package/dist/json.d.ts.map +1 -1
- package/dist/json.js +54 -22
- package/dist/local-file-access.d.ts.map +1 -1
- package/dist/local-file-access.js +4 -0
- package/dist/local-file-descriptor.d.ts +17 -0
- package/dist/local-file-descriptor.d.ts.map +1 -0
- package/dist/local-file-descriptor.js +84 -0
- package/dist/local-roots.d.ts.map +1 -1
- package/dist/local-roots.js +35 -7
- package/dist/move-path-cleanup.d.ts +2 -0
- package/dist/move-path-cleanup.d.ts.map +1 -1
- package/dist/move-path-cleanup.js +44 -18
- package/dist/move-path.d.ts.map +1 -1
- package/dist/move-path.js +31 -6
- package/dist/native-binding.d.ts +17 -0
- package/dist/native-binding.d.ts.map +1 -1
- package/dist/native-binding.js +8 -1
- package/dist/native-directory-observation.d.ts +17 -0
- package/dist/native-directory-observation.d.ts.map +1 -0
- package/dist/native-directory-observation.js +37 -0
- package/dist/native-operations.d.ts.map +1 -1
- package/dist/native-operations.js +6 -4
- package/dist/native-parent-admission.d.ts +32 -0
- package/dist/native-parent-admission.d.ts.map +1 -0
- package/dist/native-parent-admission.js +127 -0
- package/dist/native-pinned-write-windows.d.ts +0 -1
- package/dist/native-pinned-write-windows.d.ts.map +1 -1
- package/dist/native-pinned-write-windows.js +8 -9
- package/dist/native-pinned-write.d.ts.map +1 -1
- package/dist/native-pinned-write.js +314 -42
- package/dist/native-staged-file.d.ts +4 -4
- package/dist/native-staged-file.d.ts.map +1 -1
- package/dist/native-staged-file.js +20 -10
- package/dist/native.d.ts +1 -1
- package/dist/native.d.ts.map +1 -1
- package/dist/native.js +7 -2
- package/dist/output.d.ts.map +1 -1
- package/dist/output.js +15 -5
- package/dist/overwrite-file-handle.d.ts.map +1 -1
- package/dist/overwrite-file-handle.js +5 -1
- package/dist/owner-dacl.d.ts.map +1 -1
- package/dist/owner-dacl.js +2 -0
- package/dist/path-policy.d.ts.map +1 -1
- package/dist/path-policy.js +7 -2
- package/dist/path-prefix.d.ts +7 -0
- package/dist/path-prefix.d.ts.map +1 -0
- package/dist/path-prefix.js +82 -0
- package/dist/path-scope-lexical.d.ts.map +1 -1
- package/dist/path-scope-lexical.js +18 -8
- package/dist/path-segment-route.d.ts +7 -0
- package/dist/path-segment-route.d.ts.map +1 -0
- package/dist/path-segment-route.js +24 -0
- package/dist/path-suffix-aliases.d.ts +10 -0
- package/dist/path-suffix-aliases.d.ts.map +1 -0
- package/dist/path-suffix-aliases.js +386 -0
- package/dist/path.d.ts.map +1 -1
- package/dist/path.js +16 -5
- package/dist/permissions-windows.d.ts.map +1 -1
- package/dist/permissions-windows.js +14 -3
- package/dist/permissions.d.ts.map +1 -1
- package/dist/permissions.js +37 -8
- package/dist/pinned-mutation-admission.d.ts +25 -0
- package/dist/pinned-mutation-admission.d.ts.map +1 -0
- package/dist/pinned-mutation-admission.js +425 -0
- package/dist/pinned-mutation-observation.d.ts +34 -0
- package/dist/pinned-mutation-observation.d.ts.map +1 -0
- package/dist/pinned-mutation-observation.js +142 -0
- package/dist/pinned-mutation-shared-route.d.ts +24 -0
- package/dist/pinned-mutation-shared-route.d.ts.map +1 -0
- package/dist/pinned-mutation-shared-route.js +70 -0
- package/dist/pinned-open.d.ts +6 -0
- package/dist/pinned-open.d.ts.map +1 -1
- package/dist/pinned-open.js +28 -10
- package/dist/pinned-write-types.d.ts +75 -0
- package/dist/pinned-write-types.d.ts.map +1 -0
- package/dist/pinned-write-types.js +1 -0
- package/dist/pinned-write.d.ts +7 -33
- package/dist/pinned-write.d.ts.map +1 -1
- package/dist/pinned-write.js +151 -17
- package/dist/private-directory.d.ts.map +1 -1
- package/dist/private-directory.js +2 -0
- package/dist/private-producer-handoff.d.ts +16 -0
- package/dist/private-producer-handoff.d.ts.map +1 -0
- package/dist/private-producer-handoff.js +272 -0
- package/dist/private-temp-workspace.d.ts +2 -39
- package/dist/private-temp-workspace.d.ts.map +1 -1
- package/dist/private-temp-workspace.js +183 -77
- package/dist/publish-file.d.ts.map +1 -1
- package/dist/publish-file.js +38 -32
- package/dist/regular-file.d.ts.map +1 -1
- package/dist/regular-file.js +56 -45
- package/dist/replace-directory.d.ts.map +1 -1
- package/dist/replace-directory.js +12 -5
- package/dist/replace-file-temp-owner.d.ts +1 -0
- package/dist/replace-file-temp-owner.d.ts.map +1 -1
- package/dist/replace-file-temp-owner.js +12 -1
- package/dist/replace-file.d.ts.map +1 -1
- package/dist/replace-file.js +24 -24
- package/dist/root-boundary.d.ts +4 -0
- package/dist/root-boundary.d.ts.map +1 -1
- package/dist/root-boundary.js +6 -1
- package/dist/root-context.d.ts +12 -1
- package/dist/root-context.d.ts.map +1 -1
- package/dist/root-context.js +57 -11
- package/dist/root-directory-creation.d.ts +13 -0
- package/dist/root-directory-creation.d.ts.map +1 -0
- package/dist/root-directory-creation.js +212 -0
- package/dist/root-directory-list.d.ts +16 -4
- package/dist/root-directory-list.d.ts.map +1 -1
- package/dist/root-directory-list.js +180 -39
- package/dist/root-directory.d.ts +23 -0
- package/dist/root-directory.d.ts.map +1 -0
- package/dist/root-directory.js +141 -0
- package/dist/root-errors.d.ts.map +1 -1
- package/dist/root-errors.js +12 -1
- package/dist/root-file-final-admission.d.ts +21 -0
- package/dist/root-file-final-admission.d.ts.map +1 -0
- package/dist/root-file-final-admission.js +83 -0
- package/dist/root-file.d.ts.map +1 -1
- package/dist/root-file.js +103 -24
- package/dist/root-impl.d.ts +1 -0
- package/dist/root-impl.d.ts.map +1 -1
- package/dist/root-impl.js +289 -317
- package/dist/root-move-noreplace.d.ts +14 -0
- package/dist/root-move-noreplace.d.ts.map +1 -0
- package/dist/root-move-noreplace.js +202 -0
- package/dist/root-observed-path.d.ts +9 -0
- package/dist/root-observed-path.d.ts.map +1 -0
- package/dist/root-observed-path.js +95 -0
- package/dist/root-path-errors.d.ts +12 -0
- package/dist/root-path-errors.d.ts.map +1 -0
- package/dist/root-path-errors.js +13 -0
- package/dist/root-path-existing.d.ts.map +1 -1
- package/dist/root-path-existing.js +53 -20
- package/dist/root-path-observation.d.ts +63 -0
- package/dist/root-path-observation.d.ts.map +1 -0
- package/dist/root-path-observation.js +180 -0
- package/dist/root-path-stat.d.ts +5 -0
- package/dist/root-path-stat.d.ts.map +1 -0
- package/dist/root-path-stat.js +101 -0
- package/dist/root-path-symlink.d.ts.map +1 -1
- package/dist/root-path-symlink.js +23 -4
- package/dist/root-path.d.ts +11 -0
- package/dist/root-path.d.ts.map +1 -1
- package/dist/root-path.js +215 -53
- package/dist/root-paths-lexical.d.ts +9 -0
- package/dist/root-paths-lexical.d.ts.map +1 -0
- package/dist/root-paths-lexical.js +22 -0
- package/dist/root-paths.d.ts +3 -25
- package/dist/root-paths.d.ts.map +1 -1
- package/dist/root-paths.js +77 -154
- package/dist/root-read-admission.d.ts +26 -0
- package/dist/root-read-admission.d.ts.map +1 -0
- package/dist/root-read-admission.js +94 -0
- package/dist/root-remove-identity.d.ts +15 -0
- package/dist/root-remove-identity.d.ts.map +1 -0
- package/dist/root-remove-identity.js +89 -0
- package/dist/root-remove-receipt.d.ts +17 -0
- package/dist/root-remove-receipt.d.ts.map +1 -0
- package/dist/root-remove-receipt.js +37 -0
- package/dist/root-remove.d.ts +2 -1
- package/dist/root-remove.d.ts.map +1 -1
- package/dist/root-remove.js +172 -10
- package/dist/root-write-admission.d.ts +68 -0
- package/dist/root-write-admission.d.ts.map +1 -0
- package/dist/root-write-admission.js +322 -0
- package/dist/root-write-compatibility.d.ts +13 -0
- package/dist/root-write-compatibility.d.ts.map +1 -0
- package/dist/root-write-compatibility.js +74 -0
- package/dist/root-write-complete-parent.d.ts +42 -0
- package/dist/root-write-complete-parent.d.ts.map +1 -0
- package/dist/root-write-complete-parent.js +195 -0
- package/dist/root-write-publication.d.ts +24 -0
- package/dist/root-write-publication.d.ts.map +1 -0
- package/dist/root-write-publication.js +85 -0
- package/dist/root-write-verification.d.ts.map +1 -1
- package/dist/root-write-verification.js +6 -4
- package/dist/secret-file.d.ts.map +1 -1
- package/dist/secret-file.js +62 -24
- package/dist/secret-read-async.d.ts.map +1 -1
- package/dist/secret-read-async.js +18 -13
- package/dist/secret-read-policy.d.ts.map +1 -1
- package/dist/secret-read-policy.js +5 -1
- package/dist/secure-file.d.ts.map +1 -1
- package/dist/secure-file.js +92 -7
- package/dist/secure-temp-dir.d.ts +6 -3
- package/dist/secure-temp-dir.d.ts.map +1 -1
- package/dist/secure-temp-dir.js +121 -101
- package/dist/secure-temp-repair.d.ts +35 -0
- package/dist/secure-temp-repair.d.ts.map +1 -0
- package/dist/secure-temp-repair.js +106 -0
- package/dist/sibling-staged-file.d.ts +3 -2
- package/dist/sibling-staged-file.d.ts.map +1 -1
- package/dist/sibling-staged-file.js +179 -55
- package/dist/sibling-temp.d.ts.map +1 -1
- package/dist/sibling-temp.js +27 -13
- package/dist/sidecar-lock-acquire.d.ts.map +1 -1
- package/dist/sidecar-lock-acquire.js +44 -14
- package/dist/sidecar-lock-root.d.ts.map +1 -1
- package/dist/sidecar-lock-root.js +4 -2
- package/dist/staged-directory.d.ts +14 -5
- package/dist/staged-directory.d.ts.map +1 -1
- package/dist/staged-directory.js +54 -3
- package/dist/standalone-publication-path.d.ts +2 -0
- package/dist/standalone-publication-path.d.ts.map +1 -0
- package/dist/standalone-publication-path.js +6 -0
- package/dist/stat-observation.d.ts +11 -0
- package/dist/stat-observation.d.ts.map +1 -0
- package/dist/stat-observation.js +64 -0
- package/dist/strict-file-identity.d.ts.map +1 -1
- package/dist/strict-file-identity.js +39 -2
- package/dist/symlink-parents.d.ts.map +1 -1
- package/dist/symlink-parents.js +7 -2
- package/dist/temp-target.d.ts +2 -0
- package/dist/temp-target.d.ts.map +1 -1
- package/dist/temp-target.js +130 -3
- package/dist/temp-workspace-admission.d.ts +22 -0
- package/dist/temp-workspace-admission.d.ts.map +1 -0
- package/dist/temp-workspace-admission.js +374 -0
- package/dist/temp-workspace-child-admission.d.ts +13 -0
- package/dist/temp-workspace-child-admission.d.ts.map +1 -0
- package/dist/temp-workspace-child-admission.js +95 -0
- package/dist/temp-workspace-descriptor.d.ts +48 -0
- package/dist/temp-workspace-descriptor.d.ts.map +1 -0
- package/dist/temp-workspace-descriptor.js +363 -0
- package/dist/temp-workspace-identity.d.ts +15 -0
- package/dist/temp-workspace-identity.d.ts.map +1 -0
- package/dist/temp-workspace-identity.js +41 -0
- package/dist/temp-workspace-owner.d.ts +7 -6
- package/dist/temp-workspace-owner.d.ts.map +1 -1
- package/dist/temp-workspace-owner.js +121 -61
- package/dist/temp-workspace-permissions.d.ts +4 -0
- package/dist/temp-workspace-permissions.d.ts.map +1 -0
- package/dist/temp-workspace-permissions.js +32 -0
- package/dist/temp-workspace-types.d.ts +40 -0
- package/dist/temp-workspace-types.d.ts.map +1 -0
- package/dist/temp-workspace-types.js +1 -0
- package/dist/temp.d.ts +1 -1
- package/dist/temp.d.ts.map +1 -1
- package/dist/temp.js +1 -1
- package/dist/test-hooks.d.ts +7 -0
- package/dist/test-hooks.d.ts.map +1 -1
- package/dist/text-atomic.d.ts.map +1 -1
- package/dist/text-atomic.js +3 -1
- package/dist/trash.d.ts.map +1 -1
- package/dist/trash.js +54 -8
- package/dist/walk.d.ts.map +1 -1
- package/dist/walk.js +9 -6
- package/dist/windows-owner.d.ts.map +1 -1
- package/dist/windows-owner.js +5 -0
- package/dist/windows-path-alias.d.ts +39 -0
- package/dist/windows-path-alias.d.ts.map +1 -0
- package/dist/windows-path-alias.js +153 -0
- package/docs/advanced.md +25 -0
- package/docs/archive.md +23 -2
- package/docs/atomic.md +27 -3
- package/docs/copy.md +3 -0
- package/docs/durability.md +14 -0
- package/docs/errors.md +14 -6
- package/docs/file-store.md +24 -3
- package/docs/filename.md +14 -7
- package/docs/install-path.md +2 -2
- package/docs/install.md +8 -7
- package/docs/json-store.md +5 -1
- package/docs/json.md +11 -0
- package/docs/mutation-policy-proof.md +65 -0
- package/docs/native-helper.md +41 -11
- package/docs/native.md +32 -8
- package/docs/output.md +33 -11
- package/docs/path-prefix.md +64 -0
- package/docs/path-suffix-aliases.md +159 -0
- package/docs/path.md +11 -0
- package/docs/private-file-store.md +14 -0
- package/docs/public-api.md +9 -0
- package/docs/quickstart.md +1 -1
- package/docs/reading.md +8 -1
- package/docs/root.md +18 -3
- package/docs/secret-file.md +3 -0
- package/docs/secure-file.md +6 -2
- package/docs/security-model.md +75 -8
- package/docs/sidecar-lock.md +28 -1
- package/docs/store.md +5 -1
- package/docs/temp.md +260 -36
- package/docs/test-hooks.md +14 -0
- package/docs/writing.md +38 -8
- package/package.json +10 -10
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import { WINDOWS_RESERVED_DEVICE_NAMES } from "./device-path.js";
|
|
3
|
+
import { admitPathInsideRoot } from "./root-boundary.js";
|
|
4
|
+
import { isSafePathSegment } from "./safe-path-segment.js";
|
|
5
|
+
function windowsReservedDeviceSegment(segment) {
|
|
6
|
+
const extension = segment.indexOf(".");
|
|
7
|
+
const stem = segment.slice(0, extension < 0 ? segment.length : extension).toUpperCase();
|
|
8
|
+
return WINDOWS_RESERVED_DEVICE_NAMES.has(stem);
|
|
9
|
+
}
|
|
10
|
+
function ordinaryWindowsSegments(relativePath) {
|
|
11
|
+
if (relativePath === "" || relativePath.includes("/") || relativePath.includes("\0"))
|
|
12
|
+
return false;
|
|
13
|
+
const segments = relativePath.split("\\");
|
|
14
|
+
return segments.every((segment) => isSafePathSegment(segment, { allowDotPrefix: true }) &&
|
|
15
|
+
!segment.endsWith(".") && !segment.endsWith(" ") &&
|
|
16
|
+
!windowsReservedDeviceSegment(segment));
|
|
17
|
+
}
|
|
18
|
+
function ordinaryPosixSegments(relativePath) {
|
|
19
|
+
return relativePath !== "" && !relativePath.includes("\\") &&
|
|
20
|
+
relativePath.split("/").every((segment) => isSafePathSegment(segment, { allowDotPrefix: true }));
|
|
21
|
+
}
|
|
22
|
+
function exactNonzeroRootIdentity(identity) {
|
|
23
|
+
return typeof identity?.dev === "bigint" && typeof identity.ino === "bigint" &&
|
|
24
|
+
identity.dev !== 0n && identity.ino !== 0n
|
|
25
|
+
? Object.freeze({ dev: identity.dev, ino: identity.ino })
|
|
26
|
+
: undefined;
|
|
27
|
+
}
|
|
28
|
+
export function ordinarySharedAbsoluteInsideRoot(rootReal, candidatePath, rootIdentity) {
|
|
29
|
+
if (!path.isAbsolute(candidatePath) || candidatePath.includes("\0") ||
|
|
30
|
+
path.resolve(candidatePath) !== candidatePath ||
|
|
31
|
+
(process.platform === "win32"
|
|
32
|
+
? candidatePath.startsWith("\\\\") || candidatePath.includes("/")
|
|
33
|
+
: candidatePath.includes("\\")))
|
|
34
|
+
return false;
|
|
35
|
+
const admitted = admitPathInsideRoot({
|
|
36
|
+
rootPath: rootReal,
|
|
37
|
+
candidatePath,
|
|
38
|
+
rootIdentity,
|
|
39
|
+
});
|
|
40
|
+
return admitted?.admission === "exact" && admitted.path === candidatePath &&
|
|
41
|
+
(admitted.relativePath === "" || (process.platform === "win32"
|
|
42
|
+
? ordinaryWindowsSegments(admitted.relativePath)
|
|
43
|
+
: ordinaryPosixSegments(admitted.relativePath)));
|
|
44
|
+
}
|
|
45
|
+
export function simpleSharedRoute(params) {
|
|
46
|
+
if (process.versions.bun ||
|
|
47
|
+
params.policy.mutationSymlinks !== "reject" || !params.originalPath ||
|
|
48
|
+
params.originalPath.startsWith("~") || path.isAbsolute(params.originalPath) ||
|
|
49
|
+
!(process.platform === "win32"
|
|
50
|
+
? ordinaryWindowsSegments(params.originalPath) &&
|
|
51
|
+
!params.rootReal.startsWith("\\\\") && !params.rootReal.includes("/")
|
|
52
|
+
: ordinaryPosixSegments(params.originalPath) && !params.rootReal.includes("\\")) ||
|
|
53
|
+
path.resolve(params.rootReal) !== params.rootReal)
|
|
54
|
+
return undefined;
|
|
55
|
+
const rootIdentity = exactNonzeroRootIdentity(params.rootIdentity);
|
|
56
|
+
if (!rootIdentity)
|
|
57
|
+
return undefined;
|
|
58
|
+
const route = path.resolve(params.rootReal, params.originalPath);
|
|
59
|
+
if (route !== params.selectedTarget ||
|
|
60
|
+
!ordinarySharedAbsoluteInsideRoot(params.rootReal, route, rootIdentity))
|
|
61
|
+
return undefined;
|
|
62
|
+
const policyEntries = [
|
|
63
|
+
...(params.policy.denyMutations?.paths ?? []),
|
|
64
|
+
...(params.policy.denyMutations?.prefixes ?? []),
|
|
65
|
+
];
|
|
66
|
+
if (policyEntries.some((entry) => entry !== path.resolve(entry) ||
|
|
67
|
+
!ordinarySharedAbsoluteInsideRoot(params.rootReal, entry, rootIdentity)))
|
|
68
|
+
return undefined;
|
|
69
|
+
return { route, rootIdentity };
|
|
70
|
+
}
|
package/dist/pinned-open.d.ts
CHANGED
|
@@ -11,6 +11,11 @@ export type PinnedOpenSyncResult = {
|
|
|
11
11
|
error?: unknown;
|
|
12
12
|
};
|
|
13
13
|
export type PinnedOpenSyncAllowedType = "file" | "directory";
|
|
14
|
+
export type PinnedOpenSyncFinalAdmission = (params: {
|
|
15
|
+
path: string;
|
|
16
|
+
preRealpathIdentity: fs.BigIntStats;
|
|
17
|
+
descriptorIdentity: fs.BigIntStats;
|
|
18
|
+
}) => string;
|
|
14
19
|
export type PinnedOpenSyncFs = Pick<typeof fs, "constants" | "lstatSync" | "realpathSync" | "openSync" | "fstatSync" | "closeSync">;
|
|
15
20
|
export declare function openPinnedFileSync(params: {
|
|
16
21
|
filePath: string;
|
|
@@ -20,5 +25,6 @@ export declare function openPinnedFileSync(params: {
|
|
|
20
25
|
maxBytes?: number;
|
|
21
26
|
allowedType?: PinnedOpenSyncAllowedType;
|
|
22
27
|
ioFs?: PinnedOpenSyncFs;
|
|
28
|
+
finalAdmission?: PinnedOpenSyncFinalAdmission;
|
|
23
29
|
}): PinnedOpenSyncResult;
|
|
24
30
|
//# sourceMappingURL=pinned-open.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"pinned-open.d.ts","sourceRoot":"","sources":["../src/pinned-open.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,SAAS,CAAC;
|
|
1
|
+
{"version":3,"file":"pinned-open.d.ts","sourceRoot":"","sources":["../src/pinned-open.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,SAAS,CAAC;AAQzB,MAAM,MAAM,2BAA2B,GAAG,MAAM,GAAG,YAAY,GAAG,IAAI,CAAC;AAEvE,MAAM,MAAM,oBAAoB,GAC5B;IAAE,EAAE,EAAE,IAAI,CAAC;IAAC,IAAI,EAAE,MAAM,CAAC;IAAC,EAAE,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,EAAE,CAAC,KAAK,CAAA;CAAE,GACtD;IAAE,EAAE,EAAE,KAAK,CAAC;IAAC,MAAM,EAAE,2BAA2B,CAAC;IAAC,KAAK,CAAC,EAAE,OAAO,CAAA;CAAE,CAAC;AAExE,MAAM,MAAM,yBAAyB,GAAG,MAAM,GAAG,WAAW,CAAC;AAE7D,MAAM,MAAM,4BAA4B,GAAG,CAAC,MAAM,EAAE;IAClD,IAAI,EAAE,MAAM,CAAC;IACb,mBAAmB,EAAE,EAAE,CAAC,WAAW,CAAC;IACpC,kBAAkB,EAAE,EAAE,CAAC,WAAW,CAAC;CACpC,KAAK,MAAM,CAAC;AAEb,MAAM,MAAM,gBAAgB,GAAG,IAAI,CACjC,OAAO,EAAE,EACT,WAAW,GAAG,WAAW,GAAG,cAAc,GAAG,UAAU,GAAG,WAAW,GAAG,WAAW,CACpF,CAAC;AAQF,wBAAgB,kBAAkB,CAAC,MAAM,EAAE;IACzC,QAAQ,EAAE,MAAM,CAAC;IACjB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,iBAAiB,CAAC,EAAE,OAAO,CAAC;IAC5B,eAAe,CAAC,EAAE,OAAO,CAAC;IAC1B,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,WAAW,CAAC,EAAE,yBAAyB,CAAC;IACxC,IAAI,CAAC,EAAE,gBAAgB,CAAC;IACxB,cAAc,CAAC,EAAE,4BAA4B,CAAC;CAC/C,GAAG,oBAAoB,CAuEvB"}
|
package/dist/pinned-open.js
CHANGED
|
@@ -3,49 +3,67 @@ import { isUnsafeDeviceReadPath } from "./device-path.js";
|
|
|
3
3
|
import { FsSafeError } from "./errors.js";
|
|
4
4
|
import { inspectFileIdentitySync } from "./strict-file-identity.js";
|
|
5
5
|
import { resolveReadOpenFlags } from "./read-open-flags.js";
|
|
6
|
+
import { assertNoWindowsPathAlias } from "./windows-path-alias.js";
|
|
6
7
|
import { realpathSync } from "./realpath.js";
|
|
7
8
|
function isExpectedPathError(error) {
|
|
8
9
|
const code = typeof error === "object" && error !== null && "code" in error ? String(error.code) : "";
|
|
9
10
|
return code === "ENOENT" || code === "ENOTDIR" || code === "ELOOP";
|
|
10
11
|
}
|
|
11
12
|
export function openPinnedFileSync(params) {
|
|
13
|
+
const filePath = params.filePath;
|
|
14
|
+
const resolvedPath = params.resolvedPath;
|
|
12
15
|
const ioFs = params.ioFs ?? fs;
|
|
13
16
|
const allowedType = params.allowedType ?? "file";
|
|
17
|
+
const rejectPathSymlink = params.rejectPathSymlink === true;
|
|
18
|
+
const statPolicy = {
|
|
19
|
+
rejectHardlinks: params.rejectHardlinks,
|
|
20
|
+
maxBytes: params.maxBytes,
|
|
21
|
+
};
|
|
14
22
|
const openReadFlags = resolveReadOpenFlags({ constants: ioFs.constants });
|
|
15
23
|
let fd = null;
|
|
16
24
|
try {
|
|
17
|
-
|
|
25
|
+
assertNoWindowsPathAlias(filePath, "filesystem", "file path uses a Windows filesystem namespace alias");
|
|
26
|
+
if (resolvedPath !== undefined) {
|
|
27
|
+
assertNoWindowsPathAlias(resolvedPath, "filesystem", "resolved path uses a Windows filesystem namespace alias");
|
|
28
|
+
}
|
|
29
|
+
if (isUnsafeDeviceReadPath(filePath)) {
|
|
18
30
|
return { ok: false, reason: "validation" };
|
|
19
31
|
}
|
|
20
|
-
if (
|
|
21
|
-
const candidateStat = ioFs.lstatSync(
|
|
32
|
+
if (rejectPathSymlink) {
|
|
33
|
+
const candidateStat = ioFs.lstatSync(filePath);
|
|
22
34
|
if (candidateStat.isSymbolicLink()) {
|
|
23
35
|
return { ok: false, reason: "validation" };
|
|
24
36
|
}
|
|
25
37
|
}
|
|
26
|
-
const realPath =
|
|
27
|
-
(ioFs === fs ? realpathSync(
|
|
38
|
+
const realPath = resolvedPath ??
|
|
39
|
+
(ioFs === fs ? realpathSync(filePath) : ioFs.realpathSync(filePath));
|
|
40
|
+
assertNoWindowsPathAlias(realPath, "filesystem", "resolved path uses a Windows filesystem namespace alias");
|
|
28
41
|
if (isUnsafeDeviceReadPath(realPath)) {
|
|
29
42
|
return { ok: false, reason: "validation" };
|
|
30
43
|
}
|
|
31
44
|
const preOpenStat = inspectFileIdentitySync(() => {
|
|
32
45
|
const stat = ioFs.lstatSync(realPath, { bigint: true });
|
|
33
|
-
assertAllowedStat(stat, allowedType,
|
|
46
|
+
assertAllowedStat(stat, allowedType, statPolicy);
|
|
34
47
|
return stat;
|
|
35
48
|
});
|
|
36
49
|
fd = ioFs.openSync(realPath, openReadFlags);
|
|
37
50
|
const openedStat = ioFs.fstatSync(fd);
|
|
38
51
|
const identity = inspectFileIdentitySync(() => {
|
|
39
52
|
const stat = ioFs.fstatSync(fd, { bigint: true });
|
|
40
|
-
assertAllowedStat(stat, allowedType,
|
|
53
|
+
assertAllowedStat(stat, allowedType, statPolicy);
|
|
41
54
|
return stat;
|
|
42
55
|
}, preOpenStat);
|
|
43
|
-
inspectFileIdentitySync(() => {
|
|
56
|
+
const pathIdentity = inspectFileIdentitySync(() => {
|
|
44
57
|
const stat = ioFs.lstatSync(realPath, { bigint: true });
|
|
45
|
-
assertAllowedStat(stat, allowedType,
|
|
58
|
+
assertAllowedStat(stat, allowedType, statPolicy);
|
|
46
59
|
return stat;
|
|
47
60
|
}, identity);
|
|
48
|
-
const
|
|
61
|
+
const admittedPath = params.finalAdmission?.({
|
|
62
|
+
path: realPath,
|
|
63
|
+
preRealpathIdentity: pathIdentity,
|
|
64
|
+
descriptorIdentity: identity,
|
|
65
|
+
}) ?? realPath;
|
|
66
|
+
const opened = { ok: true, path: admittedPath, fd, stat: openedStat };
|
|
49
67
|
fd = null;
|
|
50
68
|
return opened;
|
|
51
69
|
}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import type { CopyFileInput } from "./copy-file-input.js";
|
|
2
|
+
import type { AnyAsyncDirectoryGuard } from "./directory-guard.js";
|
|
3
|
+
import type { FileIdentityStat } from "./file-identity.js";
|
|
4
|
+
import type { MutationDirectoryObservation } from "./pinned-mutation-observation.js";
|
|
5
|
+
export type PinnedWriteInput = {
|
|
6
|
+
kind: "buffer";
|
|
7
|
+
data: string | Buffer;
|
|
8
|
+
encoding?: BufferEncoding;
|
|
9
|
+
} | {
|
|
10
|
+
kind: "stream";
|
|
11
|
+
stream: AsyncIterable<Uint8Array | string>;
|
|
12
|
+
stageBeforePublish?: boolean;
|
|
13
|
+
} | CopyFileInput;
|
|
14
|
+
export type RenameIdentityPolicy = "strict" | "verify-content-with-lock";
|
|
15
|
+
export type PublishedWriteIdentity = Readonly<{
|
|
16
|
+
dev: bigint;
|
|
17
|
+
ino: bigint;
|
|
18
|
+
}>;
|
|
19
|
+
export type PinnedMutationAdmissionReceipt = Readonly<object>;
|
|
20
|
+
export type PinnedMutationAuthorizationToken = Readonly<object>;
|
|
21
|
+
export type PinnedCreatedDirectoryReceipt = Readonly<{
|
|
22
|
+
admission: PinnedMutationAdmissionReceipt;
|
|
23
|
+
parent: MutationDirectoryObservation;
|
|
24
|
+
child: MutationDirectoryObservation;
|
|
25
|
+
}>;
|
|
26
|
+
export type PinnedMutationParentWalkSession = Readonly<{
|
|
27
|
+
retainedTargetPath: string;
|
|
28
|
+
tryAuthorizeAtParent(request: Readonly<{
|
|
29
|
+
targetPath: string;
|
|
30
|
+
mutationPath: string;
|
|
31
|
+
phase: "parent" | "parent-create";
|
|
32
|
+
}>, parent: MutationDirectoryObservation): PinnedMutationAuthorizationToken | undefined;
|
|
33
|
+
authorize(request: Readonly<{
|
|
34
|
+
targetPath: string;
|
|
35
|
+
mutationPath: string;
|
|
36
|
+
phase: "parent" | "parent-create";
|
|
37
|
+
}>): Promise<PinnedMutationAdmissionReceipt | undefined>;
|
|
38
|
+
advanceCreatedDirectory(receipt: PinnedCreatedDirectoryReceipt): PinnedMutationAuthorizationToken | undefined;
|
|
39
|
+
dispose(): void;
|
|
40
|
+
}>;
|
|
41
|
+
export type PinnedWriteMutationAdmission = Readonly<{
|
|
42
|
+
rejectParentSymlinks: boolean;
|
|
43
|
+
beginParentWalk?(): string | undefined;
|
|
44
|
+
beginSharedParentWalk?(): PinnedMutationParentWalkSession | undefined;
|
|
45
|
+
tryAuthorizeAtParent?(request: Readonly<{
|
|
46
|
+
targetPath: string;
|
|
47
|
+
mutationPath: string;
|
|
48
|
+
phase: "parent" | "parent-create";
|
|
49
|
+
}>, parent: MutationDirectoryObservation): PinnedMutationAuthorizationToken | undefined;
|
|
50
|
+
authorize(request: Readonly<{
|
|
51
|
+
targetPath: string;
|
|
52
|
+
mutationPath: string;
|
|
53
|
+
phase: "parent" | "parent-create";
|
|
54
|
+
}>): Promise<PinnedMutationAdmissionReceipt | undefined>;
|
|
55
|
+
advanceCreatedDirectory?(receipt: PinnedCreatedDirectoryReceipt): PinnedMutationAuthorizationToken | undefined;
|
|
56
|
+
}>;
|
|
57
|
+
export type PinnedWriteParams = {
|
|
58
|
+
rootPath: string;
|
|
59
|
+
relativeParentPath: string;
|
|
60
|
+
basename: string;
|
|
61
|
+
mkdir: boolean;
|
|
62
|
+
mode: number;
|
|
63
|
+
sync?: boolean;
|
|
64
|
+
overwrite?: boolean;
|
|
65
|
+
assertBeforeMutation?: () => void;
|
|
66
|
+
rejectFinalSymlink?: boolean;
|
|
67
|
+
maxBytes?: number;
|
|
68
|
+
input: PinnedWriteInput;
|
|
69
|
+
rootIdentity?: FileIdentityStat;
|
|
70
|
+
mutationAdmission?: PinnedWriteMutationAdmission;
|
|
71
|
+
onRenameIdentityMismatch?: "verify-content";
|
|
72
|
+
onPublished?: (identity: PublishedWriteIdentity) => void;
|
|
73
|
+
verifyPublished?: (fd: number, identity: PublishedWriteIdentity, parentGuard: AnyAsyncDirectoryGuard) => Promise<void>;
|
|
74
|
+
};
|
|
75
|
+
//# sourceMappingURL=pinned-write-types.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"pinned-write-types.d.ts","sourceRoot":"","sources":["../src/pinned-write-types.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/dist/pinned-write.d.ts
CHANGED
|
@@ -1,40 +1,14 @@
|
|
|
1
|
-
import { type AnyAsyncDirectoryGuard } from "./directory-guard.js";
|
|
2
1
|
import type { FileIdentityStat } from "./file-identity.js";
|
|
3
|
-
import {
|
|
4
|
-
export type PinnedWriteInput
|
|
5
|
-
kind: "buffer";
|
|
6
|
-
data: string | Buffer;
|
|
7
|
-
encoding?: BufferEncoding;
|
|
8
|
-
} | {
|
|
9
|
-
kind: "stream";
|
|
10
|
-
stream: AsyncIterable<Uint8Array | string>;
|
|
11
|
-
stageBeforePublish?: boolean;
|
|
12
|
-
} | CopyFileInput;
|
|
13
|
-
export type RenameIdentityPolicy = "strict" | "verify-content-with-lock";
|
|
14
|
-
export type PublishedWriteIdentity = Readonly<{
|
|
15
|
-
dev: bigint;
|
|
16
|
-
ino: bigint;
|
|
17
|
-
}>;
|
|
18
|
-
export type PinnedWriteParams = {
|
|
19
|
-
rootPath: string;
|
|
20
|
-
relativeParentPath: string;
|
|
21
|
-
basename: string;
|
|
22
|
-
mkdir: boolean;
|
|
23
|
-
mode: number;
|
|
24
|
-
sync?: boolean;
|
|
25
|
-
overwrite?: boolean;
|
|
26
|
-
assertBeforeMutation?: () => void;
|
|
27
|
-
rejectFinalSymlink?: boolean;
|
|
28
|
-
maxBytes?: number;
|
|
29
|
-
input: PinnedWriteInput;
|
|
30
|
-
rootIdentity?: FileIdentityStat;
|
|
31
|
-
onRenameIdentityMismatch?: "verify-content";
|
|
32
|
-
onPublished?: (identity: PublishedWriteIdentity) => void;
|
|
33
|
-
verifyPublished?: (fd: number, identity: PublishedWriteIdentity, parentGuard: AnyAsyncDirectoryGuard) => Promise<void>;
|
|
34
|
-
};
|
|
2
|
+
import type { PinnedWriteParams, RenameIdentityPolicy } from "./pinned-write-types.js";
|
|
3
|
+
export type { PinnedWriteInput, RenameIdentityPolicy, PublishedWriteIdentity, PinnedMutationAdmissionReceipt, PinnedMutationAuthorizationToken, PinnedCreatedDirectoryReceipt, PinnedMutationParentWalkSession, PinnedWriteMutationAdmission, PinnedWriteParams, } from "./pinned-write-types.js";
|
|
35
4
|
export declare function runPinnedWriteHelper(params: PinnedWriteParams): Promise<FileIdentityStat>;
|
|
36
5
|
export declare function runPinnedWriteWithRenamePolicy(params: PinnedWriteParams & {
|
|
37
6
|
targetPath: string;
|
|
38
7
|
renameIdentity?: RenameIdentityPolicy;
|
|
39
8
|
}): Promise<FileIdentityStat>;
|
|
9
|
+
export declare function withPinnedWriteRenameIdentityLock<T>(params: {
|
|
10
|
+
rootPath: string;
|
|
11
|
+
targetPath: string;
|
|
12
|
+
relativeTargetPath: string;
|
|
13
|
+
}, run: () => Promise<T>): Promise<T>;
|
|
40
14
|
//# sourceMappingURL=pinned-write.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"pinned-write.d.ts","sourceRoot":"","sources":["../src/pinned-write.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"pinned-write.d.ts","sourceRoot":"","sources":["../src/pinned-write.ts"],"names":[],"mappings":"AAUA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,oBAAoB,CAAC;AAiB3D,OAAO,KAAK,EAAE,iBAAiB,EAAE,oBAAoB,EAAE,MAAM,yBAAyB,CAAC;AACvF,YAAY,EACV,gBAAgB,EAAE,oBAAoB,EAAE,sBAAsB,EAC9D,8BAA8B,EAAE,gCAAgC,EAChE,6BAA6B,EAAE,+BAA+B,EAC9D,4BAA4B,EAAE,iBAAiB,GAChD,MAAM,yBAAyB,CAAC;AAgFjC,wBAAsB,oBAAoB,CAAC,MAAM,EAAE,iBAAiB,GAAG,OAAO,CAAC,gBAAgB,CAAC,CAsD/F;AAED,wBAAsB,8BAA8B,CAClD,MAAM,EAAE,iBAAiB,GAAG;IAC1B,UAAU,EAAE,MAAM,CAAC;IACnB,cAAc,CAAC,EAAE,oBAAoB,CAAC;CACvC,GACA,OAAO,CAAC,gBAAgB,CAAC,CAkB3B;AAED,wBAAsB,iCAAiC,CAAC,CAAC,EACvD,MAAM,EAAE;IAAE,QAAQ,EAAE,MAAM,CAAC;IAAC,UAAU,EAAE,MAAM,CAAC;IAAC,kBAAkB,EAAE,MAAM,CAAA;CAAE,EAC5E,GAAG,EAAE,MAAM,OAAO,CAAC,CAAC,CAAC,GACpB,OAAO,CAAC,CAAC,CAAC,CAiBZ"}
|
package/dist/pinned-write.js
CHANGED
|
@@ -4,7 +4,7 @@ import fsSync, {} from "node:fs";
|
|
|
4
4
|
import fs from "node:fs/promises";
|
|
5
5
|
import path from "node:path";
|
|
6
6
|
import { normalizeMaxBytes } from "./byte-budget.js";
|
|
7
|
-
import { createAsyncDirectoryGuard, createNearestExistingDirectoryGuard, inspectDirectoryIdentity } from "./directory-guard.js";
|
|
7
|
+
import { assertAsyncDirectoryGuard, createAsyncDirectoryGuard, createNearestExistingDirectoryGuard, inspectDirectoryIdentity } from "./directory-guard.js";
|
|
8
8
|
import { FsSafeError } from "./errors.js";
|
|
9
9
|
import { syncDirectoryBestEffort } from "./directory-durability.js";
|
|
10
10
|
import { sameFileIdentity, sha256Hex } from "./file-identity.js";
|
|
@@ -21,6 +21,7 @@ import { writeAllToFile } from "./write-file-handle.js";
|
|
|
21
21
|
import { assertFinalSymlinkRejected } from "./root-symlink-policy.js";
|
|
22
22
|
import { writeCopyFileToFd } from "./copy-file-input.js";
|
|
23
23
|
import { publishCopyStage } from "./publish-copy-stage.js";
|
|
24
|
+
import { assertNoWindowsPathAlias } from "./windows-path-alias.js";
|
|
24
25
|
function byteLength(input, encoding) {
|
|
25
26
|
return typeof input === "string"
|
|
26
27
|
? Buffer.byteLength(input, encoding ?? "utf8")
|
|
@@ -31,6 +32,7 @@ function assertSafeBasename(basename) {
|
|
|
31
32
|
basename === "." ||
|
|
32
33
|
basename === ".." ||
|
|
33
34
|
basename.includes("/") ||
|
|
35
|
+
(process.platform === "win32" && basename.includes("\\")) ||
|
|
34
36
|
basename.includes("\0")) {
|
|
35
37
|
throw new FsSafeError("invalid-path", "invalid target path");
|
|
36
38
|
}
|
|
@@ -40,6 +42,10 @@ function assertWithinMaxBytes(bytes, maxBytes) {
|
|
|
40
42
|
throw new FsSafeError("too-large", `file exceeds limit of ${maxBytes} bytes (got at least ${bytes})`);
|
|
41
43
|
}
|
|
42
44
|
}
|
|
45
|
+
function fastParentGuardDeopt(error) {
|
|
46
|
+
return error instanceof FsSafeError &&
|
|
47
|
+
(error.code === "path-mismatch" || error.code === "not-file");
|
|
48
|
+
}
|
|
43
49
|
async function writeStreamToHandle(stream, handle, maxBytes, assertBeforeMutation) {
|
|
44
50
|
let bytes = 0;
|
|
45
51
|
for await (const chunk of stream) {
|
|
@@ -49,12 +55,54 @@ async function writeStreamToHandle(stream, handle, maxBytes, assertBeforeMutatio
|
|
|
49
55
|
await writeAllToFile(handle, buffer, { assertBeforeMutation });
|
|
50
56
|
}
|
|
51
57
|
}
|
|
58
|
+
const PINNED_WRITE_SNAPSHOT_KEYS = [
|
|
59
|
+
"rootPath", "relativeParentPath", "basename", "maxBytes", "rootIdentity",
|
|
60
|
+
];
|
|
61
|
+
const RENAME_POLICY_SNAPSHOT_KEYS = ["targetPath", "renameIdentity"];
|
|
62
|
+
function copyOwnEnumerableExcept(source, excluded) {
|
|
63
|
+
const owned = {};
|
|
64
|
+
for (const key of Reflect.ownKeys(source)) {
|
|
65
|
+
// Node 22's object-rest fast path can read excluded accessors eagerly.
|
|
66
|
+
// Exclude before even inspecting the descriptor so named authority and
|
|
67
|
+
// pathname fields remain single-read snapshots on every supported Node.
|
|
68
|
+
if (excluded.includes(key))
|
|
69
|
+
continue;
|
|
70
|
+
if (!Object.getOwnPropertyDescriptor(source, key)?.enumerable)
|
|
71
|
+
continue;
|
|
72
|
+
Object.defineProperty(owned, key, {
|
|
73
|
+
value: Reflect.get(source, key),
|
|
74
|
+
enumerable: true,
|
|
75
|
+
writable: true,
|
|
76
|
+
configurable: true,
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
return owned;
|
|
80
|
+
}
|
|
52
81
|
export async function runPinnedWriteHelper(params) {
|
|
53
|
-
const
|
|
54
|
-
|
|
82
|
+
const { rootPath, relativeParentPath, basename, maxBytes, rootIdentity } = params;
|
|
83
|
+
const ownedParams = copyOwnEnumerableExcept(params, PINNED_WRITE_SNAPSHOT_KEYS);
|
|
84
|
+
const normalizedParams = {
|
|
85
|
+
...ownedParams,
|
|
86
|
+
rootPath,
|
|
87
|
+
relativeParentPath,
|
|
88
|
+
basename,
|
|
89
|
+
maxBytes: normalizeMaxBytes(maxBytes),
|
|
90
|
+
rootIdentity,
|
|
91
|
+
};
|
|
92
|
+
assertSafeBasename(normalizedParams.basename);
|
|
55
93
|
validatePinnedOperationPayload({
|
|
56
|
-
relativeParentPath:
|
|
94
|
+
relativeParentPath: normalizedParams.relativeParentPath,
|
|
57
95
|
});
|
|
96
|
+
assertNoWindowsPathAlias(normalizedParams.rootPath, "filesystem", "pinned write root uses a Windows filesystem namespace alias");
|
|
97
|
+
assertNoWindowsPathAlias(normalizedParams.relativeParentPath, "relative", "pinned write parent uses a Windows filesystem namespace alias");
|
|
98
|
+
assertNoWindowsPathAlias(normalizedParams.basename, "relative", "pinned write basename uses a Windows filesystem namespace alias");
|
|
99
|
+
if (rootIdentity !== undefined) {
|
|
100
|
+
normalizedParams.rootIdentity = { dev: rootIdentity.dev, ino: rootIdentity.ino };
|
|
101
|
+
}
|
|
102
|
+
if (normalizedParams.mutationAdmission) {
|
|
103
|
+
const targetPath = path.join(normalizedParams.rootPath, ...normalizedParams.relativeParentPath.split("/").filter(Boolean), normalizedParams.basename);
|
|
104
|
+
await getFsSafeTestHooks()?.beforePinnedWriteParentAdmission?.(targetPath);
|
|
105
|
+
}
|
|
58
106
|
// The explicit compatibility policy uses the guarded Node fallback, where
|
|
59
107
|
// content verification can replace the strict post-rename inode check.
|
|
60
108
|
if (normalizedParams.onRenameIdentityMismatch === "verify-content") {
|
|
@@ -67,51 +115,137 @@ export async function runPinnedWriteHelper(params) {
|
|
|
67
115
|
return await runPinnedWriteFallback(normalizedParams);
|
|
68
116
|
}
|
|
69
117
|
export async function runPinnedWriteWithRenamePolicy(params) {
|
|
70
|
-
const { targetPath, renameIdentity
|
|
118
|
+
const { targetPath, renameIdentity } = params;
|
|
119
|
+
const writeParams = copyOwnEnumerableExcept(params, RENAME_POLICY_SNAPSHOT_KEYS);
|
|
71
120
|
if (renameIdentity !== "verify-content-with-lock") {
|
|
72
121
|
return await runPinnedWriteHelper(writeParams);
|
|
73
122
|
}
|
|
74
123
|
const relativeTargetPath = writeParams.relativeParentPath
|
|
75
124
|
? `${writeParams.relativeParentPath}/${writeParams.basename}`
|
|
76
125
|
: writeParams.basename;
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
126
|
+
return await withPinnedWriteRenameIdentityLock({
|
|
127
|
+
rootPath: writeParams.rootPath, targetPath, relativeTargetPath,
|
|
128
|
+
}, async () => await runPinnedWriteHelper({
|
|
129
|
+
...writeParams,
|
|
130
|
+
onRenameIdentityMismatch: "verify-content",
|
|
131
|
+
}));
|
|
132
|
+
}
|
|
133
|
+
export async function withPinnedWriteRenameIdentityLock(params, run) {
|
|
134
|
+
const lockPath = path.join(params.rootPath, `.fs-safe-write-${sha256Hex(params.relativeTargetPath)}.lock`);
|
|
135
|
+
return await withSidecarLock(params.rootPath, {
|
|
136
|
+
managerKey: `fs-safe.write:${params.targetPath}`,
|
|
80
137
|
lockPath,
|
|
81
138
|
staleMs: 30_000,
|
|
82
139
|
timeoutMs: 5_000,
|
|
83
140
|
payload: () => ({ pid: process.pid, createdAt: new Date().toISOString() }),
|
|
84
141
|
retry: { retries: 5, minTimeout: 100, maxTimeout: 2_000, factor: 2 },
|
|
85
|
-
},
|
|
86
|
-
...writeParams,
|
|
87
|
-
onRenameIdentityMismatch: "verify-content",
|
|
88
|
-
}));
|
|
142
|
+
}, run);
|
|
89
143
|
}
|
|
90
144
|
async function runPinnedWriteFallback(params) {
|
|
91
145
|
const exactRoot = typeof params.rootIdentity?.dev === "bigint" && typeof params.rootIdentity.ino === "bigint"
|
|
92
146
|
? { dev: params.rootIdentity.dev, ino: params.rootIdentity.ino } : undefined;
|
|
147
|
+
const mutationAdmission = params.mutationAdmission;
|
|
93
148
|
if (exactRoot)
|
|
94
149
|
await inspectDirectoryIdentity(params.rootPath, exactRoot);
|
|
95
150
|
let parentPath = params.relativeParentPath
|
|
96
151
|
? path.join(params.rootPath, ...params.relativeParentPath.split("/"))
|
|
97
152
|
: params.rootPath;
|
|
98
|
-
|
|
153
|
+
let parentGuard;
|
|
154
|
+
let parentAdmitted = false;
|
|
155
|
+
const initialTargetPath = path.join(parentPath, params.basename);
|
|
156
|
+
const retainedTargetPath = mutationAdmission?.beginParentWalk?.();
|
|
157
|
+
const ordinaryRetainedTarget = retainedTargetPath !== undefined &&
|
|
158
|
+
path.relative(path.resolve(retainedTargetPath), path.resolve(initialTargetPath)) === "";
|
|
159
|
+
let fastGuardClassifierFailure;
|
|
160
|
+
if (ordinaryRetainedTarget) {
|
|
161
|
+
try {
|
|
162
|
+
parentGuard = await createAsyncDirectoryGuard(parentPath, { bigint: true });
|
|
163
|
+
}
|
|
164
|
+
catch (error) {
|
|
165
|
+
// Only a genuinely missing complete parent may enter the component
|
|
166
|
+
// creator. ENOTDIR, permission, identity, and canonicalization failures
|
|
167
|
+
// retain their original fail-closed result.
|
|
168
|
+
if (error?.code !== "ENOENT") {
|
|
169
|
+
if (!fastParentGuardDeopt(error))
|
|
170
|
+
throw error;
|
|
171
|
+
fastGuardClassifierFailure = error;
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
if (parentGuard) {
|
|
175
|
+
await mutationAdmission.authorize(Object.freeze({
|
|
176
|
+
targetPath: initialTargetPath,
|
|
177
|
+
mutationPath: initialTargetPath,
|
|
178
|
+
phase: "parent",
|
|
179
|
+
}));
|
|
180
|
+
await assertAsyncDirectoryGuard(parentGuard);
|
|
181
|
+
parentAdmitted = true;
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
if (fastGuardClassifierFailure) {
|
|
185
|
+
// The optimized exact-guard classifier runs before policy admission. Let
|
|
186
|
+
// the established resolver restore deny/symlink precedence before using
|
|
187
|
+
// the guarded walker or reporting the classifier failure. Because this
|
|
188
|
+
// was not an ENOENT probe, it may never authorize a newly missing parent.
|
|
189
|
+
await mutationAdmission.authorize(Object.freeze({
|
|
190
|
+
targetPath: initialTargetPath,
|
|
191
|
+
mutationPath: initialTargetPath,
|
|
192
|
+
phase: "parent",
|
|
193
|
+
}));
|
|
194
|
+
}
|
|
195
|
+
if (params.mkdir && !parentGuard) {
|
|
99
196
|
// mkdirPathComponentsWithGuards may resolve the final component through
|
|
100
197
|
// an in-root symlink (e.g. a skill-bank layout). Use its returned real
|
|
101
198
|
// path for the subsequent guard and target path so we don't re-check the
|
|
102
199
|
// original, possibly-symlinked, lexical path and reject it outright.
|
|
103
|
-
|
|
200
|
+
const mkdirParams = {
|
|
104
201
|
rootReal: params.rootPath,
|
|
105
202
|
targetPath: parentPath,
|
|
106
203
|
rootIdentity: params.rootIdentity,
|
|
107
204
|
assertBeforeMutation: params.assertBeforeMutation,
|
|
108
|
-
beforeComponent: async (componentPath) =>
|
|
109
|
-
|
|
205
|
+
beforeComponent: async (componentPath) => {
|
|
206
|
+
await getFsSafeTestHooks()?.beforeRootFallbackMutation?.("mkdir", componentPath);
|
|
207
|
+
},
|
|
208
|
+
};
|
|
209
|
+
parentPath = await mkdirPathComponentsWithGuards(mutationAdmission ? {
|
|
210
|
+
...mkdirParams,
|
|
211
|
+
rejectSymlinks: mutationAdmission.rejectParentSymlinks,
|
|
212
|
+
revalidateParentAfterBeforeComponent: true,
|
|
213
|
+
synchronousAuthorizationIncludesFence: true,
|
|
214
|
+
retainedTargetPath,
|
|
215
|
+
afterCreateComponent: mutationAdmission.advanceCreatedDirectory,
|
|
216
|
+
beforeCreateComponent: (componentPath, prospectiveParentPath, retainedTarget, parent) => {
|
|
217
|
+
if (fastGuardClassifierFailure)
|
|
218
|
+
throw fastGuardClassifierFailure;
|
|
219
|
+
const request = Object.freeze({
|
|
220
|
+
targetPath: retainedTarget ?? path.join(prospectiveParentPath, params.basename),
|
|
221
|
+
mutationPath: componentPath,
|
|
222
|
+
phase: "parent-create",
|
|
223
|
+
});
|
|
224
|
+
return mutationAdmission.tryAuthorizeAtParent?.(request, parent) ??
|
|
225
|
+
mutationAdmission.authorize(request);
|
|
226
|
+
},
|
|
227
|
+
beforeUseComponent: async (_componentPath, prospectiveParentPath, retainedTarget) => {
|
|
228
|
+
const targetPath = retainedTarget ?? path.join(prospectiveParentPath, params.basename);
|
|
229
|
+
await mutationAdmission.authorize(Object.freeze({
|
|
230
|
+
targetPath,
|
|
231
|
+
mutationPath: targetPath,
|
|
232
|
+
phase: "parent",
|
|
233
|
+
}));
|
|
234
|
+
},
|
|
235
|
+
} : mkdirParams);
|
|
110
236
|
}
|
|
111
|
-
|
|
237
|
+
parentGuard ??= params.mkdir
|
|
112
238
|
? await createAsyncDirectoryGuard(parentPath, { bigint: true })
|
|
113
239
|
: await createNearestExistingDirectoryGuard(params.rootPath, parentPath, { bigint: true });
|
|
114
240
|
const targetPath = path.join(parentPath, params.basename);
|
|
241
|
+
if (mutationAdmission && !parentAdmitted) {
|
|
242
|
+
await mutationAdmission.authorize(Object.freeze({
|
|
243
|
+
targetPath,
|
|
244
|
+
mutationPath: targetPath,
|
|
245
|
+
phase: "parent",
|
|
246
|
+
}));
|
|
247
|
+
await assertAsyncDirectoryGuard(parentGuard);
|
|
248
|
+
}
|
|
115
249
|
if (params.overwrite === false && (params.input.kind === "buffer" ||
|
|
116
250
|
(params.input.kind === "stream" && !params.input.stageBeforePublish))) {
|
|
117
251
|
const assertBeforeMutation = () => {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"private-directory.d.ts","sourceRoot":"","sources":["../src/private-directory.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"private-directory.d.ts","sourceRoot":"","sources":["../src/private-directory.ts"],"names":[],"mappings":"AAIA,MAAM,MAAM,6BAA6B,GAAG;IAC1C,QAAQ,CAAC,EAAE,MAAM,CAAC,QAAQ,CAAC;CAC5B,CAAC;AAEF,wBAAsB,sBAAsB,CAC1C,UAAU,EAAE,MAAM,EAClB,OAAO,CAAC,EAAE,6BAA6B,GACtC,OAAO,CAAC,IAAI,CAAC,CAyBf"}
|
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
import { FsSafeError } from "./errors.js";
|
|
2
2
|
import { getNativeBinding } from "./native.js";
|
|
3
|
+
import { assertNoWindowsPathAlias } from "./windows-path-alias.js";
|
|
3
4
|
export async function createPrivateDirectory(targetPath, options) {
|
|
4
5
|
const platform = options?.platform ?? process.platform;
|
|
5
6
|
if (platform !== "win32") {
|
|
6
7
|
throw new FsSafeError("helper-unavailable", "private-directory creation is supported only on Windows");
|
|
7
8
|
}
|
|
9
|
+
assertNoWindowsPathAlias(targetPath, "filesystem", "private directory path uses a Windows filesystem namespace alias", platform);
|
|
8
10
|
const native = getNativeBinding();
|
|
9
11
|
if (!native) {
|
|
10
12
|
throw new FsSafeError("helper-unavailable", "private Windows directory creation requires the matching optional native platform package; " +
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { type BigIntStats } from "node:fs";
|
|
2
|
+
import { type FileHandle } from "node:fs/promises";
|
|
3
|
+
import { type TempPathRegistration } from "./temp-cleanup.js";
|
|
4
|
+
export type PrivateProducerHandoff = {
|
|
5
|
+
handle: FileHandle;
|
|
6
|
+
identity: BigIntStats;
|
|
7
|
+
unregister: TempPathRegistration;
|
|
8
|
+
};
|
|
9
|
+
export declare function handoffPrivateProducerFile(params: {
|
|
10
|
+
sourcePath: string;
|
|
11
|
+
targetPath: string;
|
|
12
|
+
assertSourceParent: () => void;
|
|
13
|
+
assertTargetParent: () => void;
|
|
14
|
+
readWrite: boolean;
|
|
15
|
+
}): Promise<PrivateProducerHandoff>;
|
|
16
|
+
//# sourceMappingURL=private-producer-handoff.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"private-producer-handoff.d.ts","sourceRoot":"","sources":["../src/private-producer-handoff.ts"],"names":[],"mappings":"AAAA,OAAe,EAAE,KAAK,WAAW,EAAE,MAAM,SAAS,CAAC;AACnD,OAAW,EAAE,KAAK,UAAU,EAAE,MAAM,kBAAkB,CAAC;AAKvD,OAAO,EAEL,KAAK,oBAAoB,EAC1B,MAAM,mBAAmB,CAAC;AAE3B,MAAM,MAAM,sBAAsB,GAAG;IACnC,MAAM,EAAE,UAAU,CAAC;IACnB,QAAQ,EAAE,WAAW,CAAC;IACtB,UAAU,EAAE,oBAAoB,CAAC;CAClC,CAAC;AA2OF,wBAAsB,0BAA0B,CAAC,MAAM,EAAE;IACvD,UAAU,EAAE,MAAM,CAAC;IACnB,UAAU,EAAE,MAAM,CAAC;IACnB,kBAAkB,EAAE,MAAM,IAAI,CAAC;IAC/B,kBAAkB,EAAE,MAAM,IAAI,CAAC;IAC/B,SAAS,EAAE,OAAO,CAAC;CACpB,GAAG,OAAO,CAAC,sBAAsB,CAAC,CA6HlC"}
|