@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,425 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import { assertMutationNotDenied } from "./deny-mutations.js";
|
|
3
|
+
import { FsSafeError } from "./errors.js";
|
|
4
|
+
import { isPathInside } from "./path.js";
|
|
5
|
+
import { admitPathInsideRoot } from "./root-boundary.js";
|
|
6
|
+
import { ordinarySharedAbsoluteInsideRoot, simpleSharedRoute, } from "./pinned-mutation-shared-route.js";
|
|
7
|
+
import { resolvePathViaExistingAncestor } from "./root-path-existing.js";
|
|
8
|
+
import { outsideWorkspaceError } from "./root-errors.js";
|
|
9
|
+
import { getFsSafeNativeConfig } from "./native-config.js";
|
|
10
|
+
import { advanceMutationObservation, mutationObservationCurrent, observeMutationPath, mutationDirectoryObservationCurrent, mutationObservationUsesDirectory, missingMutationSegments, nextMissingMutationPath, } from "./pinned-mutation-observation.js";
|
|
11
|
+
export function snapshotPinnedMutationPolicy(denyMutations, mutationSymlinks) {
|
|
12
|
+
if (denyMutations === undefined && mutationSymlinks === undefined)
|
|
13
|
+
return undefined;
|
|
14
|
+
const denySnapshot = denyMutations === undefined ? undefined : Object.freeze({
|
|
15
|
+
paths: denyMutations.paths === undefined ? undefined : Object.freeze([...denyMutations.paths]),
|
|
16
|
+
prefixes: denyMutations.prefixes === undefined ? undefined : Object.freeze([...denyMutations.prefixes]),
|
|
17
|
+
});
|
|
18
|
+
return Object.freeze({ denyMutations: denySnapshot, mutationSymlinks });
|
|
19
|
+
}
|
|
20
|
+
async function canonicalMutationEntryPath(targetPath) {
|
|
21
|
+
const canonicalParent = await resolvePathViaExistingAncestor(path.dirname(targetPath));
|
|
22
|
+
return path.join(canonicalParent, path.basename(targetPath));
|
|
23
|
+
}
|
|
24
|
+
function sameAbsolutePath(left, right) {
|
|
25
|
+
return path.relative(path.resolve(left), path.resolve(right)) === "";
|
|
26
|
+
}
|
|
27
|
+
function simplePinnedRoute(rootReal, originalPath) {
|
|
28
|
+
// These routes can depend on raw traversal, platform aliases, home expansion,
|
|
29
|
+
// or live native canonicalization. They retain full admission at every step.
|
|
30
|
+
if (!originalPath || process.platform === "win32" || process.versions.bun ||
|
|
31
|
+
originalPath.startsWith("~") || originalPath.includes("\\") || originalPath.includes("\0"))
|
|
32
|
+
return undefined;
|
|
33
|
+
const segments = originalPath.split("/");
|
|
34
|
+
if (segments.some((segment, index) => segment === "." || segment === ".." ||
|
|
35
|
+
(segment === "" && index !== 0)))
|
|
36
|
+
return undefined;
|
|
37
|
+
const target = path.resolve(rootReal, originalPath);
|
|
38
|
+
return isPathInside(rootReal, target) && target !== rootReal ? target : undefined;
|
|
39
|
+
}
|
|
40
|
+
function nonzeroObservationIdentity(observation) {
|
|
41
|
+
return observation.entry.dev !== 0n && observation.entry.ino !== 0n &&
|
|
42
|
+
observation.identity.dev !== 0n && observation.identity.ino !== 0n;
|
|
43
|
+
}
|
|
44
|
+
function captureEpoch(rootReal, target, policy, session) {
|
|
45
|
+
const observations = new Map();
|
|
46
|
+
const observe = (pathname) => {
|
|
47
|
+
if (!observations.has(pathname)) {
|
|
48
|
+
const observation = observeMutationPath(pathname);
|
|
49
|
+
if (!observation)
|
|
50
|
+
throw new Error("incomplete mutation observation");
|
|
51
|
+
observations.set(pathname, observation);
|
|
52
|
+
}
|
|
53
|
+
return observations.get(pathname);
|
|
54
|
+
};
|
|
55
|
+
try {
|
|
56
|
+
const root = observe(rootReal);
|
|
57
|
+
const route = observe(target);
|
|
58
|
+
// A canonical spelling alone cannot certify a route containing aliases.
|
|
59
|
+
if (root.canonicalPath !== rootReal || route.canonicalPath !== target ||
|
|
60
|
+
route.ancestor !== route.canonicalAncestor || route.entry.dev !== route.identity.dev ||
|
|
61
|
+
route.entry.ino !== route.identity.ino)
|
|
62
|
+
return undefined;
|
|
63
|
+
const comparables = (entries) => Object.freeze((entries ?? []).map((entry) => {
|
|
64
|
+
if (!entry || !path.isAbsolute(entry) || entry.includes("\0"))
|
|
65
|
+
throw new Error("invalid policy observation");
|
|
66
|
+
const resolved = path.resolve(entry);
|
|
67
|
+
if (session.kind === "shared" && (!session.rootIdentity || entry !== resolved ||
|
|
68
|
+
!ordinarySharedAbsoluteInsideRoot(rootReal, resolved, session.rootIdentity))) {
|
|
69
|
+
throw new Error("ineligible shared policy observation");
|
|
70
|
+
}
|
|
71
|
+
const observation = observe(resolved);
|
|
72
|
+
if (session.kind === "shared" && (observation.canonicalPath !== resolved ||
|
|
73
|
+
observation.ancestor !== observation.canonicalAncestor ||
|
|
74
|
+
!nonzeroObservationIdentity(observation))) {
|
|
75
|
+
throw new Error("incomplete shared policy observation");
|
|
76
|
+
}
|
|
77
|
+
return Object.freeze([resolved, observation.canonicalPath]);
|
|
78
|
+
}));
|
|
79
|
+
const paths = comparables(policy.denyMutations?.paths);
|
|
80
|
+
const prefixes = comparables(policy.denyMutations?.prefixes);
|
|
81
|
+
if (session.kind === "shared" && (!session.rootIdentity ||
|
|
82
|
+
root.entry.dev !== session.rootIdentity.dev || root.entry.ino !== session.rootIdentity.ino ||
|
|
83
|
+
root.identity.dev !== session.rootIdentity.dev || root.identity.ino !== session.rootIdentity.ino ||
|
|
84
|
+
!nonzeroObservationIdentity(root) || !nonzeroObservationIdentity(route)))
|
|
85
|
+
return undefined;
|
|
86
|
+
return Object.freeze({
|
|
87
|
+
mode: getFsSafeNativeConfig().mode, target: route, paths, prefixes,
|
|
88
|
+
observations: Object.freeze([...observations.values()]), session,
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
catch {
|
|
92
|
+
return undefined;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
function epochCurrent(epoch) {
|
|
96
|
+
return !process.versions.bun && epoch.mode === epoch.session.nativeMode &&
|
|
97
|
+
getFsSafeNativeConfig().mode === epoch.mode &&
|
|
98
|
+
epoch.observations.every(mutationObservationCurrent);
|
|
99
|
+
}
|
|
100
|
+
function assertCachedNotDenied(target, epoch) {
|
|
101
|
+
if (epoch.paths.some((paths) => paths.some((denied) => isPathInside(denied, target) && isPathInside(target, denied))) ||
|
|
102
|
+
epoch.prefixes.some((paths) => paths.some((denied) => isPathInside(denied, target)))) {
|
|
103
|
+
throw new FsSafeError("denied-path", "path is denied by denyMutations policy");
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
function reusableRequest(request, epoch) {
|
|
107
|
+
return request.targetPath === epoch.target.path &&
|
|
108
|
+
((request.phase === "parent" && request.mutationPath === request.targetPath) ||
|
|
109
|
+
(request.phase === "parent-create" && missingMutationSegments(epoch.target) > 1 &&
|
|
110
|
+
request.mutationPath === nextMissingMutationPath(epoch.target)));
|
|
111
|
+
}
|
|
112
|
+
function reusableSharedCreateRequest(request, epoch) {
|
|
113
|
+
return request.phase === "parent-create" && request.targetPath === epoch.target.path &&
|
|
114
|
+
request.mutationPath === nextMissingMutationPath(epoch.target);
|
|
115
|
+
}
|
|
116
|
+
function reusableSharedRequest(request, epoch) {
|
|
117
|
+
return request.phase === "parent"
|
|
118
|
+
? request.targetPath === epoch.target.path && request.mutationPath === request.targetPath
|
|
119
|
+
: reusableSharedCreateRequest(request, epoch);
|
|
120
|
+
}
|
|
121
|
+
function nonzeroDirectoryObservation(observation) {
|
|
122
|
+
return observation.identity.dev !== 0n && observation.identity.ino !== 0n;
|
|
123
|
+
}
|
|
124
|
+
function advanceAuthorizedDirectory(admittedEpoch, admitted, receipt) {
|
|
125
|
+
if (!admittedEpoch || !admitted || receipt.admission !== admitted ||
|
|
126
|
+
admitted.epoch !== admittedEpoch || admitted.session !== admittedEpoch.session ||
|
|
127
|
+
admitted.childPath !== receipt.child.path ||
|
|
128
|
+
(admittedEpoch.session.kind === "shared" &&
|
|
129
|
+
(!nonzeroDirectoryObservation(receipt.parent) ||
|
|
130
|
+
!nonzeroDirectoryObservation(receipt.child))))
|
|
131
|
+
return undefined;
|
|
132
|
+
// The walker calls this only after its live parent fence, exact-parent deny,
|
|
133
|
+
// authority callback, successful mkdir, and exact child checks.
|
|
134
|
+
const observations = admittedEpoch.observations.map((observation) => advanceMutationObservation(observation, receipt.parent, receipt.child));
|
|
135
|
+
if (observations.some((observation) => !observation))
|
|
136
|
+
return undefined;
|
|
137
|
+
const complete = observations;
|
|
138
|
+
const target = complete.find((observation) => observation.path === admittedEpoch.target.path);
|
|
139
|
+
if (!target || target.missingOffset !== admittedEpoch.target.missingOffset + 1)
|
|
140
|
+
return undefined;
|
|
141
|
+
const next = Object.freeze({
|
|
142
|
+
...admittedEpoch,
|
|
143
|
+
target,
|
|
144
|
+
observations: Object.freeze(complete),
|
|
145
|
+
});
|
|
146
|
+
if (!mutationDirectoryObservationCurrent(receipt.parent) ||
|
|
147
|
+
!mutationDirectoryObservationCurrent(receipt.child) ||
|
|
148
|
+
!mutationObservationUsesDirectory(target, receipt.child) ||
|
|
149
|
+
!epochCurrent(next))
|
|
150
|
+
return undefined;
|
|
151
|
+
assertCachedNotDenied(target.path, next);
|
|
152
|
+
assertCachedNotDenied(receipt.child.path, next);
|
|
153
|
+
return next;
|
|
154
|
+
}
|
|
155
|
+
export async function preparePinnedWriteMutationAdmission(params) {
|
|
156
|
+
if (!params.policy) {
|
|
157
|
+
return { relativeParentPath: params.defaultRelativeParentPath };
|
|
158
|
+
}
|
|
159
|
+
const policy = params.policy;
|
|
160
|
+
const route = simplePinnedRoute(params.rootReal, params.originalPath);
|
|
161
|
+
const sharedRoute = simpleSharedRoute({
|
|
162
|
+
rootReal: params.rootReal,
|
|
163
|
+
rootIdentity: params.rootIdentity,
|
|
164
|
+
originalPath: params.originalPath,
|
|
165
|
+
selectedTarget: params.resolvedTargetPath,
|
|
166
|
+
policy,
|
|
167
|
+
});
|
|
168
|
+
let pinnedSession;
|
|
169
|
+
let epoch;
|
|
170
|
+
let pending;
|
|
171
|
+
const observedCanonicalParent = await resolvePathViaExistingAncestor(path.dirname(params.resolvedTargetPath));
|
|
172
|
+
const admittedCanonicalParent = admitPathInsideRoot({
|
|
173
|
+
rootPath: params.rootReal,
|
|
174
|
+
candidatePath: observedCanonicalParent,
|
|
175
|
+
rootIdentity: params.rootIdentity,
|
|
176
|
+
});
|
|
177
|
+
if (!admittedCanonicalParent)
|
|
178
|
+
throw outsideWorkspaceError();
|
|
179
|
+
const relativeCanonicalParent = admittedCanonicalParent.relativePath;
|
|
180
|
+
const authorizeFully = async (request) => {
|
|
181
|
+
// Preserve the original route's symlink/deny error ordering, then apply
|
|
182
|
+
// the same snapshot to the object-bound destination selected below.
|
|
183
|
+
const current = await params.resolveCurrent();
|
|
184
|
+
await assertMutationNotDenied(request.targetPath, policy.denyMutations);
|
|
185
|
+
if (!sameAbsolutePath(request.targetPath, request.mutationPath)) {
|
|
186
|
+
await assertMutationNotDenied(request.mutationPath, policy.denyMutations);
|
|
187
|
+
}
|
|
188
|
+
const [currentTarget, admittedTarget] = await Promise.all([
|
|
189
|
+
canonicalMutationEntryPath(current.resolved),
|
|
190
|
+
canonicalMutationEntryPath(request.targetPath),
|
|
191
|
+
]);
|
|
192
|
+
if (!sameAbsolutePath(currentTarget, admittedTarget)) {
|
|
193
|
+
throw new FsSafeError("path-mismatch", "write target changed during mutation policy admission");
|
|
194
|
+
}
|
|
195
|
+
return current;
|
|
196
|
+
};
|
|
197
|
+
const beginSharedParentWalk = sharedRoute ? () => {
|
|
198
|
+
const session = Object.freeze({
|
|
199
|
+
kind: "shared",
|
|
200
|
+
route: sharedRoute.route,
|
|
201
|
+
selectedTarget: params.resolvedTargetPath,
|
|
202
|
+
nativeMode: getFsSafeNativeConfig().mode,
|
|
203
|
+
rootIdentity: sharedRoute.rootIdentity,
|
|
204
|
+
authorizationToken: Object.freeze({}),
|
|
205
|
+
});
|
|
206
|
+
let disposed = false;
|
|
207
|
+
let disabled = false;
|
|
208
|
+
let sharedEpoch;
|
|
209
|
+
let sharedPending;
|
|
210
|
+
const disable = () => {
|
|
211
|
+
disabled = true;
|
|
212
|
+
sharedEpoch = undefined;
|
|
213
|
+
sharedPending = undefined;
|
|
214
|
+
};
|
|
215
|
+
return Object.freeze({
|
|
216
|
+
retainedTargetPath: session.route,
|
|
217
|
+
tryAuthorizeAtParent(request, parent) {
|
|
218
|
+
if (disposed || disabled)
|
|
219
|
+
return undefined;
|
|
220
|
+
// The walk's first missing component has no receipt epoch yet. Treat
|
|
221
|
+
// that as an ordinary cache miss so authorize() can bracket the full
|
|
222
|
+
// ordered admission and seed this session.
|
|
223
|
+
if (!sharedEpoch && !sharedPending)
|
|
224
|
+
return undefined;
|
|
225
|
+
// A receipt must be consumed by the exact mkdir/advance pair before
|
|
226
|
+
// another parent-bound authorization can be attempted.
|
|
227
|
+
if (sharedPending) {
|
|
228
|
+
disable();
|
|
229
|
+
return undefined;
|
|
230
|
+
}
|
|
231
|
+
const reusable = sharedEpoch;
|
|
232
|
+
if (!reusable || reusable.session !== session ||
|
|
233
|
+
!reusableSharedCreateRequest(request, reusable) ||
|
|
234
|
+
!mutationObservationUsesDirectory(reusable.target, parent) ||
|
|
235
|
+
!epochCurrent(reusable)) {
|
|
236
|
+
disable();
|
|
237
|
+
return undefined;
|
|
238
|
+
}
|
|
239
|
+
assertCachedNotDenied(request.targetPath, reusable);
|
|
240
|
+
assertCachedNotDenied(request.mutationPath, reusable);
|
|
241
|
+
// This is the required end fence. A returned receipt remains usable
|
|
242
|
+
// only synchronously, while the exact parent identity and spelling
|
|
243
|
+
// still match the observation captured by this walk.
|
|
244
|
+
if (!nonzeroDirectoryObservation(parent) ||
|
|
245
|
+
!mutationDirectoryObservationCurrent(parent)) {
|
|
246
|
+
disable();
|
|
247
|
+
return undefined;
|
|
248
|
+
}
|
|
249
|
+
sharedPending = Object.freeze({
|
|
250
|
+
epoch: reusable,
|
|
251
|
+
childPath: request.mutationPath,
|
|
252
|
+
session,
|
|
253
|
+
});
|
|
254
|
+
return sharedPending;
|
|
255
|
+
},
|
|
256
|
+
async authorize(request) {
|
|
257
|
+
// A create receipt reaching a full before-use admission means mkdir
|
|
258
|
+
// did not yield owned, complete post-create evidence (including
|
|
259
|
+
// EEXIST). The rest of this walk stays on ordered admission.
|
|
260
|
+
if (sharedPending)
|
|
261
|
+
disable();
|
|
262
|
+
const reusable = sharedEpoch;
|
|
263
|
+
if (!disposed && !disabled && reusable?.session === session &&
|
|
264
|
+
reusableSharedRequest(request, reusable) && epochCurrent(reusable)) {
|
|
265
|
+
assertCachedNotDenied(request.targetPath, reusable);
|
|
266
|
+
assertCachedNotDenied(request.mutationPath, reusable);
|
|
267
|
+
return undefined;
|
|
268
|
+
}
|
|
269
|
+
if (reusable)
|
|
270
|
+
disable();
|
|
271
|
+
const captureRequested = !disposed && !disabled &&
|
|
272
|
+
request.targetPath === session.route;
|
|
273
|
+
const candidate = captureRequested
|
|
274
|
+
? captureEpoch(params.rootReal, session.route, policy, session)
|
|
275
|
+
: undefined;
|
|
276
|
+
if (captureRequested &&
|
|
277
|
+
(!candidate || !reusableSharedRequest(request, candidate)))
|
|
278
|
+
disable();
|
|
279
|
+
const current = await authorizeFully(request);
|
|
280
|
+
if (disposed || disabled || !candidate) {
|
|
281
|
+
sharedEpoch = undefined;
|
|
282
|
+
return undefined;
|
|
283
|
+
}
|
|
284
|
+
if (current.resolved !== session.route ||
|
|
285
|
+
current.resolved !== session.selectedTarget || !epochCurrent(candidate)) {
|
|
286
|
+
disable();
|
|
287
|
+
return undefined;
|
|
288
|
+
}
|
|
289
|
+
// Observations bracket the ordered admission. Never attach a receipt
|
|
290
|
+
// to evidence collected only after the policy decisions it replaces.
|
|
291
|
+
sharedEpoch = candidate;
|
|
292
|
+
if (request.phase === "parent")
|
|
293
|
+
return undefined;
|
|
294
|
+
sharedPending = Object.freeze({
|
|
295
|
+
epoch: candidate,
|
|
296
|
+
childPath: request.mutationPath,
|
|
297
|
+
session,
|
|
298
|
+
});
|
|
299
|
+
return sharedPending;
|
|
300
|
+
},
|
|
301
|
+
advanceCreatedDirectory(receipt) {
|
|
302
|
+
const admitted = sharedPending;
|
|
303
|
+
const admittedEpoch = sharedEpoch;
|
|
304
|
+
sharedPending = undefined;
|
|
305
|
+
sharedEpoch = undefined;
|
|
306
|
+
if (disposed || disabled || admittedEpoch?.session !== session) {
|
|
307
|
+
disable();
|
|
308
|
+
return undefined;
|
|
309
|
+
}
|
|
310
|
+
const next = advanceAuthorizedDirectory(admittedEpoch, admitted, receipt);
|
|
311
|
+
if (!next) {
|
|
312
|
+
disable();
|
|
313
|
+
return undefined;
|
|
314
|
+
}
|
|
315
|
+
sharedEpoch = next;
|
|
316
|
+
return session.authorizationToken;
|
|
317
|
+
},
|
|
318
|
+
dispose() {
|
|
319
|
+
disposed = true;
|
|
320
|
+
disable();
|
|
321
|
+
},
|
|
322
|
+
});
|
|
323
|
+
} : undefined;
|
|
324
|
+
const mutationAdmission = Object.freeze({
|
|
325
|
+
rejectParentSymlinks: policy.mutationSymlinks === "reject",
|
|
326
|
+
beginParentWalk: route ? () => {
|
|
327
|
+
pinnedSession = Object.freeze({
|
|
328
|
+
kind: "pinned",
|
|
329
|
+
route,
|
|
330
|
+
selectedTarget: params.resolvedTargetPath,
|
|
331
|
+
nativeMode: getFsSafeNativeConfig().mode,
|
|
332
|
+
authorizationToken: Object.freeze({}),
|
|
333
|
+
});
|
|
334
|
+
epoch = undefined;
|
|
335
|
+
pending = undefined;
|
|
336
|
+
return route;
|
|
337
|
+
} : undefined,
|
|
338
|
+
beginSharedParentWalk,
|
|
339
|
+
tryAuthorizeAtParent(request, parent) {
|
|
340
|
+
pending = undefined;
|
|
341
|
+
const reusable = epoch;
|
|
342
|
+
if (!reusable || reusable.session !== pinnedSession ||
|
|
343
|
+
!reusableRequest(request, reusable) ||
|
|
344
|
+
!mutationObservationUsesDirectory(reusable.target, parent) ||
|
|
345
|
+
!epochCurrent(reusable)) {
|
|
346
|
+
epoch = undefined;
|
|
347
|
+
return undefined;
|
|
348
|
+
}
|
|
349
|
+
assertCachedNotDenied(request.targetPath, reusable);
|
|
350
|
+
assertCachedNotDenied(request.mutationPath, reusable);
|
|
351
|
+
// This is the required end fence. The caller can consume the returned
|
|
352
|
+
// token without awaiting only while the exact pathname, canonical
|
|
353
|
+
// spelling, descriptor identity, type, mode, and link count still match.
|
|
354
|
+
if (!mutationDirectoryObservationCurrent(parent)) {
|
|
355
|
+
epoch = undefined;
|
|
356
|
+
return undefined;
|
|
357
|
+
}
|
|
358
|
+
if (request.phase === "parent-create") {
|
|
359
|
+
pending = Object.freeze({
|
|
360
|
+
epoch: reusable,
|
|
361
|
+
childPath: request.mutationPath,
|
|
362
|
+
session: reusable.session,
|
|
363
|
+
});
|
|
364
|
+
return pending;
|
|
365
|
+
}
|
|
366
|
+
return reusable.session.authorizationToken;
|
|
367
|
+
},
|
|
368
|
+
async authorize(request) {
|
|
369
|
+
pending = undefined;
|
|
370
|
+
if (epoch && epoch.session === pinnedSession &&
|
|
371
|
+
reusableRequest(request, epoch) && epochCurrent(epoch)) {
|
|
372
|
+
assertCachedNotDenied(request.targetPath, epoch);
|
|
373
|
+
assertCachedNotDenied(request.mutationPath, epoch);
|
|
374
|
+
if (request.phase === "parent-create") {
|
|
375
|
+
pending = Object.freeze({
|
|
376
|
+
epoch,
|
|
377
|
+
childPath: request.mutationPath,
|
|
378
|
+
session: epoch.session,
|
|
379
|
+
});
|
|
380
|
+
return pending;
|
|
381
|
+
}
|
|
382
|
+
return undefined;
|
|
383
|
+
}
|
|
384
|
+
epoch = undefined;
|
|
385
|
+
const session = pinnedSession;
|
|
386
|
+
const candidate = session && route === request.targetPath
|
|
387
|
+
? captureEpoch(params.rootReal, route, policy, session) : undefined;
|
|
388
|
+
const current = await authorizeFully(request);
|
|
389
|
+
// Observations bracket the ordered admission. Never attach a cache to
|
|
390
|
+
// evidence collected only after the policy decisions it would replace.
|
|
391
|
+
if (candidate && candidate.session === pinnedSession &&
|
|
392
|
+
current.resolved === route && epochCurrent(candidate)) {
|
|
393
|
+
epoch = candidate;
|
|
394
|
+
if (request.phase === "parent-create" && reusableRequest(request, candidate)) {
|
|
395
|
+
pending = Object.freeze({
|
|
396
|
+
epoch: candidate,
|
|
397
|
+
childPath: request.mutationPath,
|
|
398
|
+
session: candidate.session,
|
|
399
|
+
});
|
|
400
|
+
return pending;
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
return undefined;
|
|
404
|
+
},
|
|
405
|
+
advanceCreatedDirectory: route ? (receipt) => {
|
|
406
|
+
const admitted = pending;
|
|
407
|
+
pending = undefined;
|
|
408
|
+
const admittedEpoch = epoch;
|
|
409
|
+
// Completion is fail-closed: no partially checked candidate remains
|
|
410
|
+
// reusable if any provenance, observation, or freshness check fails.
|
|
411
|
+
epoch = undefined;
|
|
412
|
+
const next = advanceAuthorizedDirectory(admittedEpoch, admitted, receipt);
|
|
413
|
+
if (!next)
|
|
414
|
+
return undefined;
|
|
415
|
+
epoch = next;
|
|
416
|
+
return next.session.authorizationToken;
|
|
417
|
+
} : undefined,
|
|
418
|
+
});
|
|
419
|
+
return {
|
|
420
|
+
relativeParentPath: relativeCanonicalParent
|
|
421
|
+
? relativeCanonicalParent.split(path.sep).join(path.posix.sep)
|
|
422
|
+
: "",
|
|
423
|
+
mutationAdmission,
|
|
424
|
+
};
|
|
425
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { type BigIntStats } from "node:fs";
|
|
2
|
+
export type MutationIdentity = Readonly<{
|
|
3
|
+
dev: bigint;
|
|
4
|
+
ino: bigint;
|
|
5
|
+
mode: bigint;
|
|
6
|
+
nlink: bigint;
|
|
7
|
+
}>;
|
|
8
|
+
declare const checkedDirectoryObservation: unique symbol;
|
|
9
|
+
export type MutationDirectoryObservation = Readonly<{
|
|
10
|
+
path: string;
|
|
11
|
+
canonicalPath: string;
|
|
12
|
+
identity: MutationIdentity;
|
|
13
|
+
[checkedDirectoryObservation]: true;
|
|
14
|
+
}>;
|
|
15
|
+
export type MutationPathObservation = Readonly<{
|
|
16
|
+
path: string;
|
|
17
|
+
ancestor: string;
|
|
18
|
+
canonicalAncestor: string;
|
|
19
|
+
canonicalPath: string;
|
|
20
|
+
entry: MutationIdentity;
|
|
21
|
+
identity: MutationIdentity;
|
|
22
|
+
missingSegments: readonly string[];
|
|
23
|
+
missingOffset: number;
|
|
24
|
+
}>;
|
|
25
|
+
export declare function missingMutationSegments(observation: MutationPathObservation): number;
|
|
26
|
+
export declare function nextMissingMutationPath(observation: MutationPathObservation): string | undefined;
|
|
27
|
+
export declare function checkedMutationDirectory(pathname: string, canonicalPath: string, stat: BigIntStats): MutationDirectoryObservation;
|
|
28
|
+
export declare function mutationDirectoryObservationCurrent(observation: MutationDirectoryObservation): boolean;
|
|
29
|
+
export declare function mutationObservationUsesDirectory(observation: MutationPathObservation, directory: MutationDirectoryObservation): boolean;
|
|
30
|
+
export declare function observeMutationPath(pathname: string): MutationPathObservation | undefined;
|
|
31
|
+
export declare function mutationObservationCurrent(observation: MutationPathObservation): boolean;
|
|
32
|
+
export declare function advanceMutationObservation(observation: MutationPathObservation, parent: MutationDirectoryObservation, child: MutationDirectoryObservation): MutationPathObservation | undefined;
|
|
33
|
+
export {};
|
|
34
|
+
//# sourceMappingURL=pinned-mutation-observation.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"pinned-mutation-observation.d.ts","sourceRoot":"","sources":["../src/pinned-mutation-observation.ts"],"names":[],"mappings":"AAAA,OAAW,EAAE,KAAK,WAAW,EAAE,MAAM,SAAS,CAAC;AAM/C,MAAM,MAAM,gBAAgB,GAAG,QAAQ,CAAC;IACtC,GAAG,EAAE,MAAM,CAAC;IACZ,GAAG,EAAE,MAAM,CAAC;IACZ,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,MAAM,CAAC;CACf,CAAC,CAAC;AAEH,QAAA,MAAM,2BAA2B,eAAmD,CAAC;AAErF,MAAM,MAAM,4BAA4B,GAAG,QAAQ,CAAC;IAClD,IAAI,EAAE,MAAM,CAAC;IACb,aAAa,EAAE,MAAM,CAAC;IACtB,QAAQ,EAAE,gBAAgB,CAAC;IAC3B,CAAC,2BAA2B,CAAC,EAAE,IAAI,CAAC;CACrC,CAAC,CAAC;AAEH,MAAM,MAAM,uBAAuB,GAAG,QAAQ,CAAC;IAC7C,IAAI,EAAE,MAAM,CAAC;IACb,QAAQ,EAAE,MAAM,CAAC;IACjB,iBAAiB,EAAE,MAAM,CAAC;IAC1B,aAAa,EAAE,MAAM,CAAC;IACtB,KAAK,EAAE,gBAAgB,CAAC;IACxB,QAAQ,EAAE,gBAAgB,CAAC;IAC3B,eAAe,EAAE,SAAS,MAAM,EAAE,CAAC;IACnC,aAAa,EAAE,MAAM,CAAC;CACvB,CAAC,CAAC;AA8BH,wBAAgB,uBAAuB,CAAC,WAAW,EAAE,uBAAuB,GAAG,MAAM,CAEpF;AAED,wBAAgB,uBAAuB,CAAC,WAAW,EAAE,uBAAuB,GAAG,MAAM,GAAG,SAAS,CAGhG;AAKD,wBAAgB,wBAAwB,CACtC,QAAQ,EAAE,MAAM,EAChB,aAAa,EAAE,MAAM,EACrB,IAAI,EAAE,WAAW,GAChB,4BAA4B,CAU9B;AAQD,wBAAgB,mCAAmC,CACjD,WAAW,EAAE,4BAA4B,GACxC,OAAO,CAUT;AAED,wBAAgB,gCAAgC,CAC9C,WAAW,EAAE,uBAAuB,EACpC,SAAS,EAAE,4BAA4B,GACtC,OAAO,CAMT;AAGD,wBAAgB,mBAAmB,CAAC,QAAQ,EAAE,MAAM,GAAG,uBAAuB,GAAG,SAAS,CA0BzF;AAED,wBAAgB,0BAA0B,CAAC,WAAW,EAAE,uBAAuB,GAAG,OAAO,CAaxF;AAID,wBAAgB,0BAA0B,CACxC,WAAW,EAAE,uBAAuB,EACpC,MAAM,EAAE,4BAA4B,EACpC,KAAK,EAAE,4BAA4B,GAClC,uBAAuB,GAAG,SAAS,CAiBrC"}
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
import fs, {} from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { isNotFoundPathError } from "./path.js";
|
|
4
|
+
import { realpathSync } from "./realpath.js";
|
|
5
|
+
import { inspectFileIdentitySync } from "./strict-file-identity.js";
|
|
6
|
+
const checkedDirectoryObservation = Symbol("checked mutation directory observation");
|
|
7
|
+
function identity(stat) {
|
|
8
|
+
return Object.freeze({ dev: stat.dev, ino: stat.ino, mode: stat.mode, nlink: stat.nlink });
|
|
9
|
+
}
|
|
10
|
+
function sameIdentity(left, right, directory = false) {
|
|
11
|
+
return left.dev === right.dev && left.ino === right.ino && left.mode === right.mode &&
|
|
12
|
+
// mkdir changes a parent's link count. File link counts remain live evidence.
|
|
13
|
+
(directory || left.nlink === right.nlink);
|
|
14
|
+
}
|
|
15
|
+
function directoryMode(mode) {
|
|
16
|
+
return (mode & 61440n) === 16384n;
|
|
17
|
+
}
|
|
18
|
+
function absent(pathname) {
|
|
19
|
+
try {
|
|
20
|
+
fs.lstatSync(pathname);
|
|
21
|
+
return false;
|
|
22
|
+
}
|
|
23
|
+
catch (error) {
|
|
24
|
+
// ENOTDIR is incomplete evidence, not a missing child of a directory.
|
|
25
|
+
return isNotFoundPathError(error) && error.code === "ENOENT";
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
export function missingMutationSegments(observation) {
|
|
29
|
+
return observation.missingSegments.length - observation.missingOffset;
|
|
30
|
+
}
|
|
31
|
+
export function nextMissingMutationPath(observation) {
|
|
32
|
+
const segment = observation.missingSegments[observation.missingOffset];
|
|
33
|
+
return segment === undefined ? undefined : path.join(observation.ancestor, segment);
|
|
34
|
+
}
|
|
35
|
+
// Callers may construct this only from their just-completed exact pathname and
|
|
36
|
+
// canonical fences. The private brand prevents unverified structural objects
|
|
37
|
+
// from advancing an admission epoch.
|
|
38
|
+
export function checkedMutationDirectory(pathname, canonicalPath, stat) {
|
|
39
|
+
if (!stat.isDirectory() || stat.isSymbolicLink()) {
|
|
40
|
+
throw new TypeError("mutation directory evidence must describe a real directory");
|
|
41
|
+
}
|
|
42
|
+
return Object.freeze({
|
|
43
|
+
path: pathname,
|
|
44
|
+
canonicalPath,
|
|
45
|
+
identity: identity(stat),
|
|
46
|
+
[checkedDirectoryObservation]: true,
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
function checkedDirectory(observation) {
|
|
50
|
+
return observation[checkedDirectoryObservation] === true &&
|
|
51
|
+
path.isAbsolute(observation.path) && path.isAbsolute(observation.canonicalPath) &&
|
|
52
|
+
directoryMode(observation.identity.mode);
|
|
53
|
+
}
|
|
54
|
+
export function mutationDirectoryObservationCurrent(observation) {
|
|
55
|
+
if (!checkedDirectory(observation))
|
|
56
|
+
return false;
|
|
57
|
+
try {
|
|
58
|
+
const stat = inspectFileIdentitySync(() => fs.lstatSync(observation.path, { bigint: true }));
|
|
59
|
+
return stat.isDirectory() && !stat.isSymbolicLink() &&
|
|
60
|
+
sameIdentity(observation.identity, stat) &&
|
|
61
|
+
realpathSync.native(observation.path) === observation.canonicalPath;
|
|
62
|
+
}
|
|
63
|
+
catch {
|
|
64
|
+
return false;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
export function mutationObservationUsesDirectory(observation, directory) {
|
|
68
|
+
return checkedDirectory(directory) &&
|
|
69
|
+
observation.ancestor === directory.path &&
|
|
70
|
+
observation.canonicalAncestor === directory.canonicalPath &&
|
|
71
|
+
sameIdentity(observation.entry, directory.identity) &&
|
|
72
|
+
sameIdentity(observation.identity, directory.identity);
|
|
73
|
+
}
|
|
74
|
+
// Optional evidence only: failures select full admission, never a new error order.
|
|
75
|
+
export function observeMutationPath(pathname) {
|
|
76
|
+
try {
|
|
77
|
+
let ancestor = pathname;
|
|
78
|
+
const reversedMissing = [];
|
|
79
|
+
while (absent(ancestor)) {
|
|
80
|
+
const parent = path.dirname(ancestor);
|
|
81
|
+
if (parent === ancestor)
|
|
82
|
+
return undefined;
|
|
83
|
+
reversedMissing.push(path.basename(ancestor));
|
|
84
|
+
ancestor = parent;
|
|
85
|
+
}
|
|
86
|
+
reversedMissing.reverse();
|
|
87
|
+
const missingSegments = Object.freeze(reversedMissing);
|
|
88
|
+
const entry = inspectFileIdentitySync(() => fs.lstatSync(ancestor, { bigint: true }));
|
|
89
|
+
const canonicalAncestor = realpathSync.native(ancestor);
|
|
90
|
+
const stat = canonicalAncestor === ancestor ? entry :
|
|
91
|
+
inspectFileIdentitySync(() => fs.lstatSync(canonicalAncestor, { bigint: true }));
|
|
92
|
+
if (stat.isSymbolicLink() || (missingSegments.length > 0 && !stat.isDirectory()))
|
|
93
|
+
return undefined;
|
|
94
|
+
const observation = Object.freeze({
|
|
95
|
+
path: pathname, ancestor, canonicalAncestor,
|
|
96
|
+
canonicalPath: path.join(canonicalAncestor, ...missingSegments),
|
|
97
|
+
entry: identity(entry), identity: identity(stat), missingSegments, missingOffset: 0,
|
|
98
|
+
});
|
|
99
|
+
return mutationObservationCurrent(observation) ? observation : undefined;
|
|
100
|
+
}
|
|
101
|
+
catch {
|
|
102
|
+
return undefined;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
export function mutationObservationCurrent(observation) {
|
|
106
|
+
try {
|
|
107
|
+
const entry = inspectFileIdentitySync(() => fs.lstatSync(observation.ancestor, { bigint: true }));
|
|
108
|
+
if (!sameIdentity(observation.entry, entry, entry.isDirectory()) ||
|
|
109
|
+
realpathSync.native(observation.ancestor) !== observation.canonicalAncestor)
|
|
110
|
+
return false;
|
|
111
|
+
const stat = observation.canonicalAncestor === observation.ancestor ? entry :
|
|
112
|
+
inspectFileIdentitySync(() => fs.lstatSync(observation.canonicalAncestor, { bigint: true }));
|
|
113
|
+
return sameIdentity(observation.identity, stat, stat.isDirectory()) &&
|
|
114
|
+
(missingMutationSegments(observation) === 0 ||
|
|
115
|
+
(stat.isDirectory() && absent(nextMissingMutationPath(observation))));
|
|
116
|
+
}
|
|
117
|
+
catch {
|
|
118
|
+
return false;
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
// This is deliberately a pure tentative update. The immediately following
|
|
122
|
+
// parent authorization validates the complete epoch once before reusing it.
|
|
123
|
+
export function advanceMutationObservation(observation, parent, child) {
|
|
124
|
+
const nextMissingPath = nextMissingMutationPath(observation);
|
|
125
|
+
if (nextMissingPath === undefined || nextMissingPath !== child.path)
|
|
126
|
+
return observation;
|
|
127
|
+
if (!checkedDirectory(parent) || !checkedDirectory(child) ||
|
|
128
|
+
observation.ancestor !== parent.path || observation.canonicalAncestor !== parent.canonicalPath ||
|
|
129
|
+
!sameIdentity(observation.entry, parent.identity, true) ||
|
|
130
|
+
!sameIdentity(observation.identity, parent.identity, true) ||
|
|
131
|
+
parent.path !== parent.canonicalPath || child.path !== child.canonicalPath ||
|
|
132
|
+
path.dirname(child.path) !== parent.path)
|
|
133
|
+
return undefined;
|
|
134
|
+
return Object.freeze({
|
|
135
|
+
...observation,
|
|
136
|
+
ancestor: child.path,
|
|
137
|
+
canonicalAncestor: child.canonicalPath,
|
|
138
|
+
entry: child.identity,
|
|
139
|
+
identity: child.identity,
|
|
140
|
+
missingOffset: observation.missingOffset + 1,
|
|
141
|
+
});
|
|
142
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import type { DenyMutationPolicy } from "./deny-mutations.js";
|
|
2
|
+
import type { RootBoundaryIdentity } from "./root-boundary.js";
|
|
3
|
+
import type { MutationSymlinkPolicy } from "./root-symlink-policy.js";
|
|
4
|
+
export type ExactRootIdentity = Readonly<{
|
|
5
|
+
dev: bigint;
|
|
6
|
+
ino: bigint;
|
|
7
|
+
}>;
|
|
8
|
+
type SharedMutationPolicy = Readonly<{
|
|
9
|
+
denyMutations?: DenyMutationPolicy;
|
|
10
|
+
mutationSymlinks?: MutationSymlinkPolicy;
|
|
11
|
+
}>;
|
|
12
|
+
export declare function ordinarySharedAbsoluteInsideRoot(rootReal: string, candidatePath: string, rootIdentity: ExactRootIdentity): boolean;
|
|
13
|
+
export declare function simpleSharedRoute(params: {
|
|
14
|
+
rootReal: string;
|
|
15
|
+
rootIdentity?: RootBoundaryIdentity;
|
|
16
|
+
originalPath?: string;
|
|
17
|
+
selectedTarget: string;
|
|
18
|
+
policy: SharedMutationPolicy;
|
|
19
|
+
}): {
|
|
20
|
+
route: string;
|
|
21
|
+
rootIdentity: ExactRootIdentity;
|
|
22
|
+
} | undefined;
|
|
23
|
+
export {};
|
|
24
|
+
//# sourceMappingURL=pinned-mutation-shared-route.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"pinned-mutation-shared-route.d.ts","sourceRoot":"","sources":["../src/pinned-mutation-shared-route.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,qBAAqB,CAAC;AAG9D,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,oBAAoB,CAAC;AAE/D,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,0BAA0B,CAAC;AAEtE,MAAM,MAAM,iBAAiB,GAAG,QAAQ,CAAC;IAAE,GAAG,EAAE,MAAM,CAAC;IAAC,GAAG,EAAE,MAAM,CAAA;CAAE,CAAC,CAAC;AAEvE,KAAK,oBAAoB,GAAG,QAAQ,CAAC;IACnC,aAAa,CAAC,EAAE,kBAAkB,CAAC;IACnC,gBAAgB,CAAC,EAAE,qBAAqB,CAAC;CAC1C,CAAC,CAAC;AA+BH,wBAAgB,gCAAgC,CAC9C,QAAQ,EAAE,MAAM,EAChB,aAAa,EAAE,MAAM,EACrB,YAAY,EAAE,iBAAiB,GAC9B,OAAO,CAeT;AAED,wBAAgB,iBAAiB,CAAC,MAAM,EAAE;IACxC,QAAQ,EAAE,MAAM,CAAC;IACjB,YAAY,CAAC,EAAE,oBAAoB,CAAC;IACpC,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,cAAc,EAAE,MAAM,CAAC;IACvB,MAAM,EAAE,oBAAoB,CAAC;CAC9B,GAAG;IAAE,KAAK,EAAE,MAAM,CAAC;IAAC,YAAY,EAAE,iBAAiB,CAAA;CAAE,GAAG,SAAS,CAqBjE"}
|