@openclaw/fs-safe 0.10.0 → 0.12.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +114 -0
- package/LICENSE +1 -0
- package/README.md +39 -6
- package/dist/absolute-path.d.ts.map +1 -1
- package/dist/absolute-path.js +3 -2
- package/dist/advanced.d.ts +5 -1
- package/dist/advanced.d.ts.map +1 -1
- package/dist/advanced.js +5 -1
- package/dist/archive-durability.d.ts +6 -6
- package/dist/archive-durability.d.ts.map +1 -1
- package/dist/archive-durability.js +1 -1
- package/dist/archive-entry.d.ts.map +1 -1
- package/dist/archive-entry.js +8 -8
- package/dist/archive-gzip-tail.d.ts +1 -0
- package/dist/archive-gzip-tail.d.ts.map +1 -1
- package/dist/archive-gzip-tail.js +16 -9
- package/dist/archive-input.d.ts.map +1 -1
- package/dist/archive-input.js +4 -2
- package/dist/archive-merge.d.ts +5 -1
- package/dist/archive-merge.d.ts.map +1 -1
- package/dist/archive-merge.js +15 -12
- package/dist/archive-native.js +4 -4
- package/dist/archive-parser.wasm +0 -0
- package/dist/archive-read.d.ts.map +1 -1
- package/dist/archive-read.js +54 -42
- package/dist/archive-staging.d.ts +6 -3
- package/dist/archive-staging.d.ts.map +1 -1
- package/dist/archive-staging.js +42 -22
- package/dist/archive-tar-stream.d.ts.map +1 -1
- package/dist/archive-tar-stream.js +7 -6
- package/dist/archive-tar-wasm.d.ts.map +1 -1
- package/dist/archive-tar-wasm.js +16 -13
- 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 +48 -12
- package/dist/archive-zip-loader.d.ts +6 -0
- package/dist/archive-zip-loader.d.ts.map +1 -0
- package/dist/archive-zip-loader.js +38 -0
- package/dist/archive-zip-names.d.ts.map +1 -1
- package/dist/archive-zip-names.js +26 -9
- package/dist/archive-zip-preflight.d.ts +2 -3
- package/dist/archive-zip-preflight.d.ts.map +1 -1
- package/dist/archive-zip-preflight.js +2 -34
- package/dist/archive.d.ts.map +1 -1
- package/dist/archive.js +12 -10
- package/dist/bounded-read.d.ts +5 -0
- package/dist/bounded-read.d.ts.map +1 -1
- package/dist/bounded-read.js +18 -11
- package/dist/copy-file-input.d.ts +0 -1
- package/dist/copy-file-input.d.ts.map +1 -1
- package/dist/copy-file-input.js +4 -26
- package/dist/copy-tree-portable.d.ts.map +1 -1
- package/dist/copy-tree-portable.js +57 -26
- package/dist/copy.d.ts +1 -1
- package/dist/copy.d.ts.map +1 -1
- package/dist/copy.js +3 -1
- package/dist/device-path.d.ts.map +1 -1
- package/dist/device-path.js +5 -3
- package/dist/directory-durability.d.ts.map +1 -1
- package/dist/directory-durability.js +5 -4
- package/dist/directory-guard.d.ts +11 -1
- package/dist/directory-guard.d.ts.map +1 -1
- package/dist/directory-guard.js +53 -11
- package/dist/durability.d.ts +1 -1
- package/dist/durability.d.ts.map +1 -1
- package/dist/durability.js +1 -1
- package/dist/file-handle-transfer.d.ts +14 -0
- package/dist/file-handle-transfer.d.ts.map +1 -0
- package/dist/file-handle-transfer.js +64 -0
- package/dist/file-hash.d.ts +3 -0
- package/dist/file-hash.d.ts.map +1 -1
- package/dist/file-hash.js +99 -32
- package/dist/file-lock-sync.d.ts.map +1 -1
- package/dist/file-lock-sync.js +8 -4
- package/dist/file-store-boundary.d.ts.map +1 -1
- package/dist/file-store-boundary.js +7 -5
- package/dist/file-store-path.d.ts +3 -0
- package/dist/file-store-path.d.ts.map +1 -0
- package/dist/file-store-path.js +27 -0
- package/dist/file-store-prune.d.ts.map +1 -1
- package/dist/file-store-prune.js +15 -5
- package/dist/file-store-sync-write.d.ts.map +1 -1
- package/dist/file-store-sync-write.js +56 -44
- package/dist/file-store.d.ts.map +1 -1
- package/dist/file-store.js +2 -18
- package/dist/filename.d.ts +1 -0
- package/dist/filename.d.ts.map +1 -1
- package/dist/filename.js +32 -14
- package/dist/guarded-mkdir.d.ts +2 -0
- package/dist/guarded-mkdir.d.ts.map +1 -1
- package/dist/guarded-mkdir.js +52 -16
- package/dist/guest-dispatch-python.d.ts +2 -0
- package/dist/guest-dispatch-python.d.ts.map +1 -0
- package/dist/guest-dispatch-python.js +117 -0
- package/dist/guest-native-python.d.ts +4 -0
- package/dist/guest-native-python.d.ts.map +1 -0
- package/dist/guest-native-python.js +135 -0
- package/dist/guest.d.ts +9 -0
- package/dist/guest.d.ts.map +1 -0
- package/dist/guest.js +421 -0
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/install-path.d.ts +6 -0
- package/dist/install-path.d.ts.map +1 -1
- package/dist/install-path.js +16 -2
- package/dist/json-durable-queue-directory.js +3 -3
- package/dist/json-durable-queue-ownership.d.ts +2 -0
- package/dist/json-durable-queue-ownership.d.ts.map +1 -1
- package/dist/json-durable-queue-ownership.js +47 -3
- package/dist/json-durable-queue-read.d.ts +6 -0
- package/dist/json-durable-queue-read.d.ts.map +1 -0
- package/dist/json-durable-queue-read.js +59 -0
- package/dist/json-durable-queue.d.ts +1 -1
- package/dist/json-durable-queue.d.ts.map +1 -1
- package/dist/json-durable-queue.js +43 -84
- package/dist/json.d.ts.map +1 -1
- package/dist/json.js +2 -1
- package/dist/local-roots.d.ts.map +1 -1
- package/dist/local-roots.js +2 -1
- package/dist/move-path-stage.d.ts.map +1 -1
- package/dist/move-path-stage.js +2 -1
- package/dist/move-path.d.ts.map +1 -1
- package/dist/move-path.js +4 -3
- package/dist/mutation-authority.d.ts +1 -0
- package/dist/mutation-authority.d.ts.map +1 -1
- package/dist/mutation-authority.js +4 -4
- package/dist/native-binding.d.ts +14 -2
- package/dist/native-binding.d.ts.map +1 -1
- package/dist/native-pinned-write-windows.d.ts.map +1 -1
- package/dist/native-pinned-write-windows.js +3 -2
- package/dist/native-pinned-write.d.ts.map +1 -1
- package/dist/native-pinned-write.js +2 -1
- package/dist/opened-realpath.d.ts.map +1 -1
- package/dist/opened-realpath.js +5 -4
- package/dist/output.d.ts +2 -0
- package/dist/output.d.ts.map +1 -1
- package/dist/output.js +2 -0
- package/dist/overwrite-file-handle.d.ts +8 -0
- package/dist/overwrite-file-handle.d.ts.map +1 -0
- package/dist/overwrite-file-handle.js +42 -0
- package/dist/path-case.d.ts +7 -0
- package/dist/path-case.d.ts.map +1 -0
- package/dist/path-case.js +136 -0
- package/dist/path-scope-lexical.d.ts +14 -0
- package/dist/path-scope-lexical.d.ts.map +1 -0
- package/dist/path-scope-lexical.js +27 -0
- package/dist/path.d.ts.map +1 -1
- package/dist/path.js +22 -3
- package/dist/permissions-windows.d.ts +1 -1
- package/dist/permissions-windows.d.ts.map +1 -1
- package/dist/permissions-windows.js +48 -6
- package/dist/pinned-open.d.ts.map +1 -1
- package/dist/pinned-open.js +3 -1
- package/dist/pinned-write.d.ts +2 -2
- package/dist/pinned-write.d.ts.map +1 -1
- package/dist/pinned-write.js +3 -1
- package/dist/private-temp-workspace.d.ts.map +1 -1
- package/dist/private-temp-workspace.js +6 -4
- package/dist/realpath.d.ts +4 -0
- package/dist/realpath.d.ts.map +1 -0
- package/dist/realpath.js +43 -0
- package/dist/recursive-mkdir-path.d.ts +3 -0
- package/dist/recursive-mkdir-path.d.ts.map +1 -0
- package/dist/recursive-mkdir-path.js +8 -0
- package/dist/replace-directory.d.ts.map +1 -1
- package/dist/replace-directory.js +2 -1
- package/dist/replace-file-copy-fallback.d.ts.map +1 -1
- package/dist/replace-file-copy-fallback.js +23 -31
- package/dist/replace-file-copy-source.d.ts.map +1 -1
- package/dist/replace-file-copy-source.js +7 -12
- package/dist/replace-file-mode.d.ts +3 -0
- package/dist/replace-file-mode.d.ts.map +1 -0
- package/dist/replace-file-mode.js +10 -0
- package/dist/replace-file.d.ts +1 -0
- package/dist/replace-file.d.ts.map +1 -1
- package/dist/replace-file.js +14 -8
- package/dist/root-boundary.d.ts +25 -0
- package/dist/root-boundary.d.ts.map +1 -0
- package/dist/root-boundary.js +177 -0
- package/dist/root-context.d.ts.map +1 -1
- package/dist/root-context.js +40 -13
- package/dist/root-create-input.d.ts +10 -0
- package/dist/root-create-input.d.ts.map +1 -0
- package/dist/root-create-input.js +80 -0
- package/dist/root-directory-list.d.ts +3 -1
- package/dist/root-directory-list.d.ts.map +1 -1
- package/dist/root-directory-list.js +31 -5
- package/dist/root-entries.d.ts +11 -0
- package/dist/root-entries.d.ts.map +1 -0
- package/dist/root-entries.js +61 -0
- package/dist/root-errors.d.ts +5 -5
- package/dist/root-errors.d.ts.map +1 -1
- package/dist/root-errors.js +13 -12
- package/dist/root-impl.d.ts +13 -3
- package/dist/root-impl.d.ts.map +1 -1
- package/dist/root-impl.js +93 -70
- package/dist/root-move-preflight.d.ts +8 -0
- package/dist/root-move-preflight.d.ts.map +1 -0
- package/dist/root-move-preflight.js +16 -0
- package/dist/root-options.d.ts +12 -1
- package/dist/root-options.d.ts.map +1 -1
- package/dist/root-path-existing.d.ts +2 -0
- package/dist/root-path-existing.d.ts.map +1 -1
- package/dist/root-path-existing.js +14 -5
- package/dist/root-path-symlink.d.ts.map +1 -1
- package/dist/root-path-symlink.js +3 -2
- package/dist/root-path.d.ts +2 -0
- package/dist/root-path.d.ts.map +1 -1
- package/dist/root-path.js +54 -26
- package/dist/root-paths.d.ts +2 -6
- package/dist/root-paths.d.ts.map +1 -1
- package/dist/root-paths.js +24 -32
- package/dist/root-remove.d.ts +5 -0
- package/dist/root-remove.d.ts.map +1 -0
- package/dist/root-remove.js +286 -0
- package/dist/root-symlink-policy.d.ts +2 -1
- package/dist/root-symlink-policy.d.ts.map +1 -1
- package/dist/root-symlink-policy.js +2 -2
- package/dist/root-walk.d.ts.map +1 -1
- package/dist/root-walk.js +2 -1
- package/dist/root-write-mode.d.ts +2 -0
- package/dist/root-write-mode.d.ts.map +1 -1
- package/dist/root-write-mode.js +21 -7
- package/dist/root-write-verification.d.ts.map +1 -1
- package/dist/root-write-verification.js +12 -3
- package/dist/root.d.ts +2 -1
- package/dist/root.d.ts.map +1 -1
- package/dist/safe-path-segment.d.ts.map +1 -1
- package/dist/safe-path-segment.js +3 -1
- package/dist/secret-file.d.ts.map +1 -1
- package/dist/secret-file.js +2 -1
- package/dist/secret-read-async.d.ts.map +1 -1
- package/dist/secret-read-async.js +2 -1
- package/dist/secure-file-windows.d.ts +10 -0
- package/dist/secure-file-windows.d.ts.map +1 -0
- package/dist/secure-file-windows.js +186 -0
- package/dist/secure-file.d.ts.map +1 -1
- package/dist/secure-file.js +27 -7
- package/dist/secure-temp-dir.d.ts.map +1 -1
- package/dist/secure-temp-dir.js +2 -1
- package/dist/sibling-staged-file.d.ts +2 -0
- package/dist/sibling-staged-file.d.ts.map +1 -1
- package/dist/sibling-staged-file.js +49 -8
- package/dist/sibling-temp.d.ts +2 -0
- package/dist/sibling-temp.d.ts.map +1 -1
- package/dist/sibling-temp.js +8 -5
- package/dist/sidecar-lock-acquire.d.ts.map +1 -1
- package/dist/sidecar-lock-acquire.js +16 -5
- package/dist/sidecar-lock-policy.d.ts +2 -0
- package/dist/sidecar-lock-policy.d.ts.map +1 -1
- package/dist/sidecar-lock-policy.js +17 -0
- package/dist/sidecar-lock.d.ts.map +1 -1
- package/dist/sidecar-lock.js +2 -3
- package/dist/staged-directory.d.ts.map +1 -1
- package/dist/staged-directory.js +4 -3
- package/dist/temp-target.d.ts +14 -12
- package/dist/temp-target.d.ts.map +1 -1
- package/dist/temp-target.js +15 -7
- package/dist/trash.d.ts.map +1 -1
- package/dist/trash.js +7 -5
- package/dist/unicode-path.d.ts +3 -0
- package/dist/unicode-path.d.ts.map +1 -0
- package/dist/unicode-path.js +13 -0
- package/dist/walk.d.ts.map +1 -1
- package/dist/walk.js +14 -12
- package/dist/write-file-handle.d.ts +1 -0
- package/dist/write-file-handle.d.ts.map +1 -1
- package/dist/write-file-handle.js +3 -2
- package/docs/advanced.md +7 -1
- package/docs/archive.md +31 -5
- package/docs/atomic.md +17 -1
- package/docs/config.md +1 -0
- package/docs/contributing.md +33 -2
- package/docs/copy.md +75 -6
- package/docs/directory-identity.md +85 -0
- package/docs/durability.md +40 -3
- package/docs/entries.md +109 -0
- package/docs/errors.md +3 -3
- package/docs/file-store.md +21 -0
- package/docs/filename.md +9 -2
- package/docs/guest.md +146 -0
- package/docs/in-place-write.md +81 -0
- package/docs/index.md +2 -0
- package/docs/install-path.md +59 -13
- package/docs/install.md +34 -0
- package/docs/native-helper.md +14 -5
- package/docs/native.md +18 -2
- package/docs/output.md +32 -6
- package/docs/path-case.md +64 -0
- package/docs/path-scope.md +1 -1
- package/docs/path.md +1 -1
- package/docs/permissions.md +37 -3
- package/docs/public-api.md +36 -2
- package/docs/root.md +33 -3
- package/docs/secure-file.md +17 -14
- package/docs/security-model.md +1 -1
- package/docs/sidecar-lock.md +12 -3
- package/docs/store.md +22 -1
- package/docs/temp.md +39 -6
- package/docs/types.md +1 -1
- package/docs/writing.md +153 -3
- package/package.json +14 -8
|
@@ -0,0 +1,286 @@
|
|
|
1
|
+
import fsSync from "node:fs";
|
|
2
|
+
import fs from "node:fs/promises";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { assertAsyncDirectoryGuard, assertSyncDirectoryGuard, createAsyncDirectoryGuard, } from "./directory-guard.js";
|
|
5
|
+
import { assertMutationNotDenied } from "./deny-mutations.js";
|
|
6
|
+
import { FsSafeError } from "./errors.js";
|
|
7
|
+
import { sameFileIdentityForCleanup } from "./file-identity.js";
|
|
8
|
+
import { MutationAuthorityError } from "./mutation-authority.js";
|
|
9
|
+
import { isNotFoundPathError, isPathInside } from "./path.js";
|
|
10
|
+
import { assertRootIdentityCurrent } from "./root-context.js";
|
|
11
|
+
import { normalizeRemoveGuardError, normalizeRemovePathError, rootPathChangedError } from "./root-errors.js";
|
|
12
|
+
import { assertFinalSymlinkRejected } from "./root-symlink-policy.js";
|
|
13
|
+
import { inspectFileIdentitySync } from "./strict-file-identity.js";
|
|
14
|
+
import { createSuppressedError } from "./suppressed-error.js";
|
|
15
|
+
import { getFsSafeTestHooks } from "./test-hooks.js";
|
|
16
|
+
const DEFAULT_MAX_ENTRIES = 100_000;
|
|
17
|
+
const DEFAULT_MAX_DEPTH = 64;
|
|
18
|
+
export function validateRemoveOptions(options) {
|
|
19
|
+
if (options.order !== undefined && options.order !== "filesystem" && options.order !== "sorted") {
|
|
20
|
+
throw new TypeError("remove order must be filesystem or sorted");
|
|
21
|
+
}
|
|
22
|
+
for (const key of ["maxEntries", "maxDepth"]) {
|
|
23
|
+
const value = options[key];
|
|
24
|
+
if (value !== undefined && value !== Number.POSITIVE_INFINITY && (!Number.isSafeInteger(value) || value < 0)) {
|
|
25
|
+
throw new RangeError(`${key} must be a non-negative safe integer`);
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
if (!options.recursive && (options.maxEntries !== undefined || options.maxDepth !== undefined)) {
|
|
29
|
+
throw new TypeError("remove budgets require recursive: true");
|
|
30
|
+
}
|
|
31
|
+
if (!options.recursive && options.order !== undefined) {
|
|
32
|
+
throw new TypeError("remove order requires recursive: true");
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
function assertNotAborted(signal) {
|
|
36
|
+
try {
|
|
37
|
+
signal?.throwIfAborted();
|
|
38
|
+
}
|
|
39
|
+
catch (error) {
|
|
40
|
+
// Preserve arbitrary abort reasons through Root's filesystem error mapping.
|
|
41
|
+
throw new MutationAuthorityError(error);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
async function removeOne(root, targetPath, options) {
|
|
45
|
+
let guard;
|
|
46
|
+
try {
|
|
47
|
+
guard = await createAsyncDirectoryGuard(path.dirname(targetPath));
|
|
48
|
+
}
|
|
49
|
+
catch (error) {
|
|
50
|
+
if (options.force && isNotFoundPathError(error)) {
|
|
51
|
+
await assertRootIdentityCurrent(root);
|
|
52
|
+
assertNotAborted(options.signal);
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
throw normalizeRemoveGuardError(error);
|
|
56
|
+
}
|
|
57
|
+
try {
|
|
58
|
+
await getFsSafeTestHooks()?.beforeRootFallbackMutation?.("remove", targetPath);
|
|
59
|
+
await assertAsyncDirectoryGuard(guard);
|
|
60
|
+
}
|
|
61
|
+
catch (error) {
|
|
62
|
+
throw normalizeRemoveGuardError(error);
|
|
63
|
+
}
|
|
64
|
+
try {
|
|
65
|
+
const isDirectory = fsSync.lstatSync(targetPath).isDirectory();
|
|
66
|
+
assertFinalSymlinkRejected(targetPath, options.mutationSymlinks !== undefined);
|
|
67
|
+
assertNotAborted(options.signal);
|
|
68
|
+
options.assertBeforeMutation?.();
|
|
69
|
+
await (isDirectory ? fs.rmdir(targetPath) : fs.unlink(targetPath));
|
|
70
|
+
}
|
|
71
|
+
catch (error) {
|
|
72
|
+
if (!(options.force && isNotFoundPathError(error)))
|
|
73
|
+
throw normalizeRemovePathError(error);
|
|
74
|
+
}
|
|
75
|
+
await assertAsyncDirectoryGuard(guard).catch(error => { throw normalizeRemoveGuardError(error); });
|
|
76
|
+
assertNotAborted(options.signal);
|
|
77
|
+
}
|
|
78
|
+
export async function removePathInRootFallback(root, targetPath, options) {
|
|
79
|
+
assertNotAborted(options.signal);
|
|
80
|
+
if (!options.recursive) {
|
|
81
|
+
await removeOne(root, targetPath, options);
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
84
|
+
const maxEntries = options.maxEntries ?? DEFAULT_MAX_ENTRIES;
|
|
85
|
+
const maxDepth = options.maxDepth ?? DEFAULT_MAX_DEPTH;
|
|
86
|
+
const details = (target, phase) => ({
|
|
87
|
+
operation: "remove",
|
|
88
|
+
phase,
|
|
89
|
+
relativePath: path.relative(targetPath, target),
|
|
90
|
+
});
|
|
91
|
+
let examined = 0;
|
|
92
|
+
const rootGuard = await createAsyncDirectoryGuard(root.rootReal, { bigint: true });
|
|
93
|
+
if (!sameFileIdentityForCleanup(rootGuard.stat, root.rootIdentity))
|
|
94
|
+
throw rootPathChangedError();
|
|
95
|
+
const guards = [rootGuard];
|
|
96
|
+
let ancestor = root.rootReal;
|
|
97
|
+
const parentRelative = path.relative(root.rootReal, path.dirname(targetPath));
|
|
98
|
+
for (const segment of parentRelative.split(path.sep).filter(Boolean)) {
|
|
99
|
+
ancestor = path.join(ancestor, segment);
|
|
100
|
+
let guard;
|
|
101
|
+
try {
|
|
102
|
+
guard = await createAsyncDirectoryGuard(ancestor, { bigint: true });
|
|
103
|
+
}
|
|
104
|
+
catch (error) {
|
|
105
|
+
if (isNotFoundPathError(error)) {
|
|
106
|
+
assertCurrent();
|
|
107
|
+
assertNotAborted(options.signal);
|
|
108
|
+
if (options.force)
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
111
|
+
throw normalizeRemoveGuardError(error);
|
|
112
|
+
}
|
|
113
|
+
if (!isPathInside(guard.realPath, ancestor) || !isPathInside(ancestor, guard.realPath)) {
|
|
114
|
+
throw new FsSafeError("path-mismatch", "removal ancestor changed during operation");
|
|
115
|
+
}
|
|
116
|
+
guards.push(guard);
|
|
117
|
+
}
|
|
118
|
+
function assertCurrent() {
|
|
119
|
+
for (const guard of guards) {
|
|
120
|
+
try {
|
|
121
|
+
assertSyncDirectoryGuard(guard);
|
|
122
|
+
}
|
|
123
|
+
catch (error) {
|
|
124
|
+
// force tolerates a vanished leaf, never a lost traversal boundary.
|
|
125
|
+
throw new FsSafeError("path-mismatch", "removal ancestor changed during operation", { cause: error });
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
const inspect = (target, expected) => {
|
|
130
|
+
assertCurrent();
|
|
131
|
+
try {
|
|
132
|
+
return inspectFileIdentitySync(() => fsSync.lstatSync(target, { bigint: true }), expected);
|
|
133
|
+
}
|
|
134
|
+
catch (error) {
|
|
135
|
+
if (options.force && isNotFoundPathError(error))
|
|
136
|
+
return;
|
|
137
|
+
throw normalizeRemovePathError(error, details(target, expected ? "remove" : "inspect"));
|
|
138
|
+
}
|
|
139
|
+
};
|
|
140
|
+
async function visit(target, depth, entryCounted = false) {
|
|
141
|
+
assertNotAborted(options.signal);
|
|
142
|
+
if ((!entryCounted && examined >= maxEntries) || depth > maxDepth) {
|
|
143
|
+
throw new FsSafeError("too-large", "recursive removal budget exceeded", {
|
|
144
|
+
details: details(target, "inspect"),
|
|
145
|
+
});
|
|
146
|
+
}
|
|
147
|
+
if (!entryCounted)
|
|
148
|
+
examined += 1;
|
|
149
|
+
await assertMutationNotDenied(target, options.denyMutations, { protectAncestors: true });
|
|
150
|
+
assertNotAborted(options.signal);
|
|
151
|
+
const initial = inspect(target);
|
|
152
|
+
if (!initial)
|
|
153
|
+
return;
|
|
154
|
+
assertFinalSymlinkRejected(target, options.mutationSymlinks !== undefined, details(target, "inspect"));
|
|
155
|
+
if (initial.isDirectory()) {
|
|
156
|
+
let directoryGuard;
|
|
157
|
+
try {
|
|
158
|
+
directoryGuard = await createAsyncDirectoryGuard(target, { bigint: true });
|
|
159
|
+
}
|
|
160
|
+
catch (error) {
|
|
161
|
+
assertCurrent();
|
|
162
|
+
assertNotAborted(options.signal);
|
|
163
|
+
if (options.force && isNotFoundPathError(error))
|
|
164
|
+
return;
|
|
165
|
+
throw normalizeRemovePathError(error, details(target, "inspect"));
|
|
166
|
+
}
|
|
167
|
+
if (!isPathInside(directoryGuard.realPath, target) || !isPathInside(target, directoryGuard.realPath) ||
|
|
168
|
+
!sameFileIdentityForCleanup(directoryGuard.stat, initial)) {
|
|
169
|
+
throw new FsSafeError("path-mismatch", "removal directory changed during operation");
|
|
170
|
+
}
|
|
171
|
+
guards.push(directoryGuard);
|
|
172
|
+
try {
|
|
173
|
+
assertCurrent();
|
|
174
|
+
assertNotAborted(options.signal);
|
|
175
|
+
const handle = await fs.opendir(target, { bufferSize: 1 }).catch(error => {
|
|
176
|
+
if (options.force && isNotFoundPathError(error)) {
|
|
177
|
+
assertNotAborted(options.signal);
|
|
178
|
+
return undefined;
|
|
179
|
+
}
|
|
180
|
+
throw normalizeRemovePathError(error, details(target, "enumerate"));
|
|
181
|
+
});
|
|
182
|
+
if (handle) {
|
|
183
|
+
let failed = false;
|
|
184
|
+
let operationError;
|
|
185
|
+
try {
|
|
186
|
+
let names = options.order === "sorted" ? [] : undefined;
|
|
187
|
+
if (names && maxEntries === Infinity) {
|
|
188
|
+
assertCurrent();
|
|
189
|
+
assertNotAborted(options.signal);
|
|
190
|
+
const snapshot = await fs.readdir(target).catch(error => {
|
|
191
|
+
if (options.force && isNotFoundPathError(error))
|
|
192
|
+
return undefined;
|
|
193
|
+
throw normalizeRemovePathError(error, details(target, "enumerate"));
|
|
194
|
+
});
|
|
195
|
+
assertNotAborted(options.signal);
|
|
196
|
+
if (snapshot) {
|
|
197
|
+
assertCurrent();
|
|
198
|
+
names = snapshot;
|
|
199
|
+
examined += names.length;
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
else {
|
|
203
|
+
while (true) {
|
|
204
|
+
assertCurrent();
|
|
205
|
+
assertNotAborted(options.signal);
|
|
206
|
+
const entry = await handle.read().catch(error => {
|
|
207
|
+
if (options.force && isNotFoundPathError(error))
|
|
208
|
+
return undefined;
|
|
209
|
+
throw normalizeRemovePathError(error, details(target, "enumerate"));
|
|
210
|
+
});
|
|
211
|
+
if (entry === undefined) {
|
|
212
|
+
assertNotAborted(options.signal);
|
|
213
|
+
if (names)
|
|
214
|
+
names.length = 0;
|
|
215
|
+
break;
|
|
216
|
+
}
|
|
217
|
+
assertCurrent();
|
|
218
|
+
assertNotAborted(options.signal);
|
|
219
|
+
if (!entry)
|
|
220
|
+
break;
|
|
221
|
+
if (names) {
|
|
222
|
+
if (examined >= maxEntries) {
|
|
223
|
+
throw new FsSafeError("too-large", "recursive removal budget exceeded", {
|
|
224
|
+
details: details(path.join(target, entry.name), "enumerate"),
|
|
225
|
+
});
|
|
226
|
+
}
|
|
227
|
+
examined += 1;
|
|
228
|
+
names.push(entry.name);
|
|
229
|
+
}
|
|
230
|
+
else {
|
|
231
|
+
await visit(path.join(target, entry.name), depth + 1);
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
if (names) {
|
|
236
|
+
for (const name of names.sort()) {
|
|
237
|
+
await visit(path.join(target, name), depth + 1, true);
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
catch (error) {
|
|
242
|
+
failed = true;
|
|
243
|
+
operationError = error instanceof MutationAuthorityError ? error.rejection : error;
|
|
244
|
+
throw error;
|
|
245
|
+
}
|
|
246
|
+
finally {
|
|
247
|
+
try {
|
|
248
|
+
// Close before rmdir, including on Windows and after revocation.
|
|
249
|
+
await handle.close();
|
|
250
|
+
}
|
|
251
|
+
catch (error) {
|
|
252
|
+
const closeError = normalizeRemovePathError(error, details(target, "enumerate"));
|
|
253
|
+
if (failed)
|
|
254
|
+
throw createSuppressedError(closeError, operationError, "recursive removal and close both failed");
|
|
255
|
+
throw closeError;
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
finally {
|
|
261
|
+
guards.pop();
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
// A tolerated missing-directory observation still rechecks its target and ancestors.
|
|
265
|
+
await getFsSafeTestHooks()?.beforeRootFallbackMutation?.("remove", target);
|
|
266
|
+
await assertMutationNotDenied(target, options.denyMutations, { protectAncestors: true });
|
|
267
|
+
assertNotAborted(options.signal);
|
|
268
|
+
if (!inspect(target, initial))
|
|
269
|
+
return;
|
|
270
|
+
assertFinalSymlinkRejected(target, options.mutationSymlinks !== undefined, details(target, "remove"));
|
|
271
|
+
assertNotAborted(options.signal);
|
|
272
|
+
options.assertBeforeMutation?.();
|
|
273
|
+
try {
|
|
274
|
+
await (initial.isDirectory() ? fs.rmdir(target) : fs.unlink(target));
|
|
275
|
+
}
|
|
276
|
+
catch (error) {
|
|
277
|
+
if (!(options.force && isNotFoundPathError(error))) {
|
|
278
|
+
throw normalizeRemovePathError(error, details(target, "remove"));
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
assertCurrent();
|
|
282
|
+
assertNotAborted(options.signal);
|
|
283
|
+
}
|
|
284
|
+
await assertRootIdentityCurrent(root);
|
|
285
|
+
await visit(targetPath, 0);
|
|
286
|
+
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { type FsSafeErrorDetails } from "./errors.js";
|
|
1
2
|
export type SymlinkPolicy = "reject" | "follow-within-root" | "follow-parents-within-root";
|
|
2
3
|
export type MutationSymlinkPolicy = "reject" | "follow-parents-within-root";
|
|
3
4
|
export declare function readSymlinkResolution(symlinks: SymlinkPolicy | undefined): {
|
|
@@ -9,5 +10,5 @@ export declare function mutationSymlinkResolution(symlinks: MutationSymlinkPolic
|
|
|
9
10
|
rejectFinalSymlink: boolean;
|
|
10
11
|
resolveCanonical: boolean;
|
|
11
12
|
};
|
|
12
|
-
export declare function assertFinalSymlinkRejected(targetPath: string, reject: boolean | undefined): void;
|
|
13
|
+
export declare function assertFinalSymlinkRejected(targetPath: string, reject: boolean | undefined, details?: FsSafeErrorDetails): void;
|
|
13
14
|
//# sourceMappingURL=root-symlink-policy.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"root-symlink-policy.d.ts","sourceRoot":"","sources":["../src/root-symlink-policy.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"root-symlink-policy.d.ts","sourceRoot":"","sources":["../src/root-symlink-policy.ts"],"names":[],"mappings":"AACA,OAAO,EAAe,KAAK,kBAAkB,EAAE,MAAM,aAAa,CAAC;AAGnE,MAAM,MAAM,aAAa,GAAG,QAAQ,GAAG,oBAAoB,GAAG,4BAA4B,CAAC;AAC3F,MAAM,MAAM,qBAAqB,GAAG,QAAQ,GAAG,4BAA4B,CAAC;AAE5E,wBAAgB,qBAAqB,CAAC,QAAQ,EAAE,aAAa,GAAG,SAAS;IAErE,cAAc;IACd,kBAAkB;EAErB;AAED,wBAAgB,yBAAyB,CAAC,QAAQ,EAAE,qBAAqB,GAAG,SAAS;IAKjF,cAAc;IACd,kBAAkB;IAClB,gBAAgB;EAEnB;AAID,wBAAgB,0BAA0B,CACxC,UAAU,EAAE,MAAM,EAClB,MAAM,EAAE,OAAO,GAAG,SAAS,EAC3B,OAAO,CAAC,EAAE,kBAAkB,GAC3B,IAAI,CASN"}
|
|
@@ -19,12 +19,12 @@ export function mutationSymlinkResolution(symlinks) {
|
|
|
19
19
|
}
|
|
20
20
|
// Call after parent fences and immediately before the mutation. This is a
|
|
21
21
|
// final-component check, not an atomic conditional rename against another process.
|
|
22
|
-
export function assertFinalSymlinkRejected(targetPath, reject) {
|
|
22
|
+
export function assertFinalSymlinkRejected(targetPath, reject, details) {
|
|
23
23
|
if (!reject)
|
|
24
24
|
return;
|
|
25
25
|
try {
|
|
26
26
|
if (fs.lstatSync(targetPath).isSymbolicLink()) {
|
|
27
|
-
throw new FsSafeError("symlink", "final symlink not allowed");
|
|
27
|
+
throw new FsSafeError("symlink", "final symlink not allowed", { details });
|
|
28
28
|
}
|
|
29
29
|
}
|
|
30
30
|
catch (error) {
|
package/dist/root-walk.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"root-walk.d.ts","sourceRoot":"","sources":["../src/root-walk.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,oBAAoB,EAAE,2BAA2B,EAAE,MAAM,0BAA0B,CAAC;AAClG,OAAO,KAAK,EAAY,QAAQ,EAAE,MAAM,YAAY,CAAC;AAGrD,MAAM,MAAM,qBAAqB,GAAG,MAAM,GAAG,oBAAoB,CAAC;AAClE,MAAM,MAAM,qBAAqB,GAAG,UAAU,GAAG,OAAO,CAAC;AACzD,MAAM,MAAM,8BAA8B,GAAG,OAAO,GAAG,iBAAiB,CAAC;AACzE,MAAM,MAAM,yBAAyB,GAAG,SAAS,GAAG,MAAM,GAAG,cAAc,CAAC;AAC5E,MAAM,MAAM,qBAAqB,GAAG,MAAM,GAAG,WAAW,GAAG,OAAO,CAAC;AACnE,MAAM,MAAM,iBAAiB,GAAG,qBAAqB,GAAG,iBAAiB,GAAG,WAAW,CAAC;AAExF,MAAM,MAAM,iBAAiB,GAAG;IAC9B,YAAY,EAAE,MAAM,CAAC;IACrB,IAAI,EAAE,qBAAqB,CAAC;IAC5B,IAAI,EAAE,MAAM,CAAC;CACd,CAAC;AAEF,MAAM,MAAM,aAAa,GACrB,iBAAiB,GACjB;IAAE,YAAY,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,WAAW,CAAC;IAAC,IAAI,EAAE,CAAC,CAAA;CAAE,GACpD;IAAE,YAAY,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,iBAAiB,CAAC;IAAC,IAAI,EAAE,CAAC,CAAC;IAAC,KAAK,EAAE,OAAO,CAAA;CAAE,CAAC;AAE/E,MAAM,MAAM,mBAAmB,GAAG,CAAC,KAAK,EAAE,iBAAiB,KAAK,yBAAyB,CAAC;AAE1F,MAAM,MAAM,eAAe,GAAG;IAC5B,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,KAAK,CAAC,EAAE,QAAQ,GAAG,YAAY,CAAC;IAChC,aAAa,EAAE,qBAAqB,CAAC;IACrC,MAAM,CAAC,EAAE,WAAW,CAAC;IACrB,aAAa,CAAC,EAAE,qBAAqB,CAAC;IACtC,WAAW,CAAC,EAAE,mBAAmB,CAAC;IAClC,gBAAgB,CAAC,EAAE,8BAA8B,CAAC;CACnD,CAAC;AAEF,KAAK,kBAAkB,GAAG;IACxB,QAAQ,EAAE,MAAM,CAAC;IACjB,IAAI,CAAC,YAAY,EAAE,MAAM,GAAG,OAAO,CAAC,QAAQ,CAAC,CAAC;IAC9C,IAAI,CACF,YAAY,EAAE,MAAM,EACpB,OAAO,EAAE,2BAA2B,GACnC,OAAO,CAAC,oBAAoB,CAAC,CAAC;CAClC,CAAC;AAqBF,wBAAuB,QAAQ,CAC7B,IAAI,EAAE,kBAAkB,EACxB,YAAY,EAAE,MAAM,EACpB,OAAO,EAAE,eAAe,GACvB,cAAc,CAAC,aAAa,CAAC,
|
|
1
|
+
{"version":3,"file":"root-walk.d.ts","sourceRoot":"","sources":["../src/root-walk.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,oBAAoB,EAAE,2BAA2B,EAAE,MAAM,0BAA0B,CAAC;AAClG,OAAO,KAAK,EAAY,QAAQ,EAAE,MAAM,YAAY,CAAC;AAGrD,MAAM,MAAM,qBAAqB,GAAG,MAAM,GAAG,oBAAoB,CAAC;AAClE,MAAM,MAAM,qBAAqB,GAAG,UAAU,GAAG,OAAO,CAAC;AACzD,MAAM,MAAM,8BAA8B,GAAG,OAAO,GAAG,iBAAiB,CAAC;AACzE,MAAM,MAAM,yBAAyB,GAAG,SAAS,GAAG,MAAM,GAAG,cAAc,CAAC;AAC5E,MAAM,MAAM,qBAAqB,GAAG,MAAM,GAAG,WAAW,GAAG,OAAO,CAAC;AACnE,MAAM,MAAM,iBAAiB,GAAG,qBAAqB,GAAG,iBAAiB,GAAG,WAAW,CAAC;AAExF,MAAM,MAAM,iBAAiB,GAAG;IAC9B,YAAY,EAAE,MAAM,CAAC;IACrB,IAAI,EAAE,qBAAqB,CAAC;IAC5B,IAAI,EAAE,MAAM,CAAC;CACd,CAAC;AAEF,MAAM,MAAM,aAAa,GACrB,iBAAiB,GACjB;IAAE,YAAY,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,WAAW,CAAC;IAAC,IAAI,EAAE,CAAC,CAAA;CAAE,GACpD;IAAE,YAAY,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,iBAAiB,CAAC;IAAC,IAAI,EAAE,CAAC,CAAC;IAAC,KAAK,EAAE,OAAO,CAAA;CAAE,CAAC;AAE/E,MAAM,MAAM,mBAAmB,GAAG,CAAC,KAAK,EAAE,iBAAiB,KAAK,yBAAyB,CAAC;AAE1F,MAAM,MAAM,eAAe,GAAG;IAC5B,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,KAAK,CAAC,EAAE,QAAQ,GAAG,YAAY,CAAC;IAChC,aAAa,EAAE,qBAAqB,CAAC;IACrC,MAAM,CAAC,EAAE,WAAW,CAAC;IACrB,aAAa,CAAC,EAAE,qBAAqB,CAAC;IACtC,WAAW,CAAC,EAAE,mBAAmB,CAAC;IAClC,gBAAgB,CAAC,EAAE,8BAA8B,CAAC;CACnD,CAAC;AAEF,KAAK,kBAAkB,GAAG;IACxB,QAAQ,EAAE,MAAM,CAAC;IACjB,IAAI,CAAC,YAAY,EAAE,MAAM,GAAG,OAAO,CAAC,QAAQ,CAAC,CAAC;IAC9C,IAAI,CACF,YAAY,EAAE,MAAM,EACpB,OAAO,EAAE,2BAA2B,GACnC,OAAO,CAAC,oBAAoB,CAAC,CAAC;CAClC,CAAC;AAqBF,wBAAuB,QAAQ,CAC7B,IAAI,EAAE,kBAAkB,EACxB,YAAY,EAAE,MAAM,EACpB,OAAO,EAAE,eAAe,GACvB,cAAc,CAAC,aAAa,CAAC,CAsK/B"}
|
package/dist/root-walk.js
CHANGED
|
@@ -98,6 +98,7 @@ export async function* walkRoot(root, relativePath, options) {
|
|
|
98
98
|
let failed = false;
|
|
99
99
|
let operationError;
|
|
100
100
|
try {
|
|
101
|
+
const relativeDirectory = directory.split(path.sep).join(path.posix.sep);
|
|
101
102
|
while (true) {
|
|
102
103
|
let next;
|
|
103
104
|
try {
|
|
@@ -112,7 +113,7 @@ export async function* walkRoot(root, relativePath, options) {
|
|
|
112
113
|
return;
|
|
113
114
|
const name = next.kind === "entry" ? next.entry.name : next.name;
|
|
114
115
|
const child = directory
|
|
115
|
-
? path.posix.join(
|
|
116
|
+
? path.posix.join(relativeDirectory, name)
|
|
116
117
|
: name;
|
|
117
118
|
if (next.kind === "limit") {
|
|
118
119
|
yield onLimit(child);
|
|
@@ -1,6 +1,8 @@
|
|
|
1
|
+
import { type RootBoundaryIdentity } from "./root-boundary.js";
|
|
1
2
|
export declare function inheritWriteTargetMode(params: {
|
|
2
3
|
targetPath: string;
|
|
3
4
|
rootWithSep: string;
|
|
5
|
+
rootIdentity?: RootBoundaryIdentity;
|
|
4
6
|
requestedMode?: number;
|
|
5
7
|
}): Promise<number>;
|
|
6
8
|
//# sourceMappingURL=root-write-mode.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"root-write-mode.d.ts","sourceRoot":"","sources":["../src/root-write-mode.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"root-write-mode.d.ts","sourceRoot":"","sources":["../src/root-write-mode.ts"],"names":[],"mappings":"AAQA,OAAO,EAAuB,KAAK,oBAAoB,EAAE,MAAM,oBAAoB,CAAC;AAGpF,wBAAsB,sBAAsB,CAAC,MAAM,EAAE;IACnD,UAAU,EAAE,MAAM,CAAC;IACnB,WAAW,EAAE,MAAM,CAAC;IACpB,YAAY,CAAC,EAAE,oBAAoB,CAAC;IACpC,aAAa,CAAC,EAAE,MAAM,CAAC;CACxB,GAAG,OAAO,CAAC,MAAM,CAAC,CAkDlB"}
|
package/dist/root-write-mode.js
CHANGED
|
@@ -1,25 +1,33 @@
|
|
|
1
1
|
import fsSync from "node:fs";
|
|
2
2
|
import fs from "node:fs/promises";
|
|
3
3
|
import { FsSafeError } from "./errors.js";
|
|
4
|
-
import { isNotFoundPathError
|
|
4
|
+
import { isNotFoundPathError } from "./path.js";
|
|
5
5
|
import { resolveReadOpenFlags } from "./read-open-flags.js";
|
|
6
|
+
import { realpathSync } from "./realpath.js";
|
|
6
7
|
import { hardlinkedPathNotAllowedError, outsideWorkspaceError } from "./root-errors.js";
|
|
7
8
|
import { inspectFileIdentity } from "./strict-file-identity.js";
|
|
9
|
+
import { admitPathInsideRoot } from "./root-boundary.js";
|
|
8
10
|
// The caller has already resolved and guarded this write target.
|
|
9
11
|
export async function inheritWriteTargetMode(params) {
|
|
10
12
|
try {
|
|
11
|
-
const
|
|
13
|
+
const admittedTarget = admitPathInsideRoot({
|
|
14
|
+
rootPath: params.rootWithSep,
|
|
15
|
+
candidatePath: params.targetPath,
|
|
16
|
+
rootIdentity: params.rootIdentity,
|
|
17
|
+
});
|
|
18
|
+
if (!admittedTarget)
|
|
19
|
+
throw outsideWorkspaceError();
|
|
20
|
+
const targetPath = admittedTarget.path;
|
|
21
|
+
const existing = await inspectFileIdentity(() => fsSync.lstatSync(targetPath, { bigint: true }));
|
|
12
22
|
if (existing.isSymbolicLink())
|
|
13
23
|
throw new FsSafeError("path-alias", "path alias escape blocked");
|
|
14
24
|
if (!existing.isFile())
|
|
15
25
|
throw new FsSafeError("not-file", "not a file");
|
|
16
26
|
if (existing.nlink > 1n)
|
|
17
27
|
throw hardlinkedPathNotAllowedError();
|
|
18
|
-
if (!isPathInside(params.rootWithSep, params.targetPath))
|
|
19
|
-
throw outsideWorkspaceError();
|
|
20
28
|
// Preserve read-open admission of the pre-existing destination. access(2)
|
|
21
29
|
// is not equivalent: it ignores ACLs on Windows and capabilities on Linux.
|
|
22
|
-
const handle = await fs.open(
|
|
30
|
+
const handle = await fs.open(targetPath, resolveReadOpenFlags());
|
|
23
31
|
try {
|
|
24
32
|
// Bind admission to the inode whose metadata is inherited.
|
|
25
33
|
await inspectFileIdentity(() => fsSync.fstatSync(handle.fd, { bigint: true }), existing);
|
|
@@ -30,9 +38,15 @@ export async function inheritWriteTargetMode(params) {
|
|
|
30
38
|
try {
|
|
31
39
|
// A parent can change after guarded resolution. Do not inherit metadata
|
|
32
40
|
// from an outside inode, even if the parent is restored before publication.
|
|
33
|
-
const
|
|
34
|
-
|
|
41
|
+
const observedRealPath = realpathSync.native(targetPath);
|
|
42
|
+
const admittedRealPath = admitPathInsideRoot({
|
|
43
|
+
rootPath: params.rootWithSep,
|
|
44
|
+
candidatePath: observedRealPath,
|
|
45
|
+
rootIdentity: params.rootIdentity,
|
|
46
|
+
});
|
|
47
|
+
if (!admittedRealPath)
|
|
35
48
|
throw outsideWorkspaceError();
|
|
49
|
+
const realPath = admittedRealPath.path;
|
|
36
50
|
await inspectFileIdentity(async () => {
|
|
37
51
|
const current = fsSync.statSync(realPath, { bigint: true });
|
|
38
52
|
if (!current.isFile())
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"root-write-verification.d.ts","sourceRoot":"","sources":["../src/root-write-verification.ts"],"names":[],"mappings":"AAEA,OAAO,EAA6B,KAAK,sBAAsB,EAAE,MAAM,sBAAsB,CAAC;AAK9F,OAAO,KAAK,EAAE,sBAAsB,EAAE,MAAM,mBAAmB,CAAC;AAEhE,OAAO,EAA6B,KAAK,WAAW,EAAE,MAAM,mBAAmB,CAAC;
|
|
1
|
+
{"version":3,"file":"root-write-verification.d.ts","sourceRoot":"","sources":["../src/root-write-verification.ts"],"names":[],"mappings":"AAEA,OAAO,EAA6B,KAAK,sBAAsB,EAAE,MAAM,sBAAsB,CAAC;AAK9F,OAAO,KAAK,EAAE,sBAAsB,EAAE,MAAM,mBAAmB,CAAC;AAEhE,OAAO,EAA6B,KAAK,WAAW,EAAE,MAAM,mBAAmB,CAAC;AAIhF,wBAAsB,uBAAuB,CAAC,MAAM,EAAE;IACpD,IAAI,EAAE,WAAW,CAAC;IAClB,UAAU,EAAE,MAAM,CAAC;IACnB,EAAE,EAAE,MAAM,CAAC;IACX,gBAAgB,EAAE,sBAAsB,CAAC;IACzC,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,WAAW,EAAE,sBAAsB,CAAC;CACrC,GAAG,OAAO,CAAC,IAAI,CAAC,CA6FhB"}
|
|
@@ -4,10 +4,11 @@ import { assertAsyncDirectoryGuard } from "./directory-guard.js";
|
|
|
4
4
|
import { FsSafeError } from "./errors.js";
|
|
5
5
|
import { sameFileIdentity } from "./file-identity.js";
|
|
6
6
|
import { resolveOpenedFileRealPathForFd } from "./opened-realpath.js";
|
|
7
|
-
import { assertNoUnsafeDeviceReadPath, hasNodeErrorCode, isNotFoundPathError,
|
|
7
|
+
import { assertNoUnsafeDeviceReadPath, hasNodeErrorCode, isNotFoundPathError, isSymlinkOpenError } from "./path.js";
|
|
8
8
|
import { resolveReadOpenFlags } from "./read-open-flags.js";
|
|
9
9
|
import { assertRootIdentityCurrent } from "./root-context.js";
|
|
10
10
|
import { fileNotFoundError, hardlinkedPathNotAllowedError, outsideWorkspaceError } from "./root-errors.js";
|
|
11
|
+
import { admitPathInsideRoot } from "./root-boundary.js";
|
|
11
12
|
export async function verifyAtomicWriteResult(params) {
|
|
12
13
|
let needsPathOpen = false;
|
|
13
14
|
const assertFile = (stat) => {
|
|
@@ -49,7 +50,11 @@ export async function verifyAtomicWriteResult(params) {
|
|
|
49
50
|
assertPath(fsSync.lstatSync(params.targetPath, { bigint: true }));
|
|
50
51
|
const { realPath } = await resolveOpenedFileRealPathForFd(params.fd, stat, params.targetPath);
|
|
51
52
|
assertPath(fsSync.statSync(realPath, { bigint: true }));
|
|
52
|
-
if (!
|
|
53
|
+
if (!admitPathInsideRoot({
|
|
54
|
+
rootPath: params.root.rootReal,
|
|
55
|
+
candidatePath: realPath,
|
|
56
|
+
rootIdentity: params.root.rootIdentity,
|
|
57
|
+
})) {
|
|
53
58
|
throw outsideWorkspaceError();
|
|
54
59
|
}
|
|
55
60
|
await assertAsyncDirectoryGuard(params.parentGuard);
|
|
@@ -76,7 +81,11 @@ export async function verifyAtomicWriteResult(params) {
|
|
|
76
81
|
assertPath(fsSync.lstatSync(params.targetPath, { bigint: true }));
|
|
77
82
|
const { realPath: reopenedPath } = await resolveOpenedFileRealPathForFd(opened.fd, reopenedStat, params.targetPath);
|
|
78
83
|
assertPath(fsSync.statSync(reopenedPath, { bigint: true }));
|
|
79
|
-
if (!
|
|
84
|
+
if (!admitPathInsideRoot({
|
|
85
|
+
rootPath: params.root.rootReal,
|
|
86
|
+
candidatePath: reopenedPath,
|
|
87
|
+
rootIdentity: params.root.rootIdentity,
|
|
88
|
+
})) {
|
|
80
89
|
throw outsideWorkspaceError();
|
|
81
90
|
}
|
|
82
91
|
await assertAsyncDirectoryGuard(params.parentGuard);
|
package/dist/root.d.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
|
-
export { DEFAULT_ROOT_MAX_BYTES, openLocalFileSafely, readLocalFileSafely, resolveOpenedFileRealPathForHandle, root, type DenyMutationPolicy, type HardlinkPolicy, type MutationSymlinkPolicy, type RenameIdentityPolicy, type OpenResult, type ReadResult, type Root, type RootAppendOptions, type RootCopyOptions, type RootCopySource, type RootCreateJsonOptions, type RootCreateOptions, type RootDefaults, type RootMkdirOptions, type RootMoveOptions, type RootOpenOptions, type RootOpenWritableOptions, type RootOptions, type RootReadOptions, type RootRemoveOptions, type RootWriteJsonOptions, type RootWriteOptions, type SymlinkPolicy, type WritableOpenMode, type WritableOpenResult, } from "./root-impl.js";
|
|
1
|
+
export { DEFAULT_ROOT_MAX_BYTES, openLocalFileSafely, readLocalFileSafely, resolveOpenedFileRealPathForHandle, root, type DenyMutationPolicy, type HardlinkPolicy, type MutationSymlinkPolicy, type RenameIdentityPolicy, type OpenResult, type ReadResult, type Root, type RootAppendOptions, type RootCopyOptions, type RootCopySource, type RootCreateJsonOptions, type RootCreateOptions, type RootCreateStreamOptions, type RootDefaults, type RootMkdirOptions, type RootMoveOptions, type RootOpenOptions, type RootOpenWritableOptions, type RootOptions, type RootReadOptions, type RootRemoveOptions, type RootWriteJsonOptions, type RootWriteOptions, type SymlinkPolicy, type WritableOpenMode, type WritableOpenResult, } from "./root-impl.js";
|
|
2
2
|
export type { RootCopyPublicationReceipt } from "./copy-publication.js";
|
|
3
3
|
export type { CopyCloneMode } from "./copy-policy.js";
|
|
4
|
+
export type { RootEntriesOptions } from "./root-entries.js";
|
|
4
5
|
export type { ContainmentGuarantee } from "./containment.js";
|
|
5
6
|
export type { RootWalkDataEntry, RootWalkDataEntryKind, RootWalkDirectoryErrorBehavior, RootWalkEntry, RootWalkEntryFilter, RootWalkEntryFilterResult, RootWalkEntryKind, RootWalkLimitBehavior, RootWalkOptions, RootWalkSymlinkPolicy, } from "./root-walk.js";
|
|
6
7
|
//# sourceMappingURL=root.d.ts.map
|
package/dist/root.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"root.d.ts","sourceRoot":"","sources":["../src/root.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,sBAAsB,EACtB,mBAAmB,EACnB,mBAAmB,EACnB,kCAAkC,EAClC,IAAI,EACJ,KAAK,kBAAkB,EACvB,KAAK,cAAc,EACnB,KAAK,qBAAqB,EAC1B,KAAK,oBAAoB,EACzB,KAAK,UAAU,EACf,KAAK,UAAU,EACf,KAAK,IAAI,EACT,KAAK,iBAAiB,EACtB,KAAK,eAAe,EACpB,KAAK,cAAc,EACnB,KAAK,qBAAqB,EAC1B,KAAK,iBAAiB,EACtB,KAAK,YAAY,EACjB,KAAK,gBAAgB,EACrB,KAAK,eAAe,EACpB,KAAK,eAAe,EACpB,KAAK,uBAAuB,EAC5B,KAAK,WAAW,EAChB,KAAK,eAAe,EACpB,KAAK,iBAAiB,EACtB,KAAK,oBAAoB,EACzB,KAAK,gBAAgB,EACrB,KAAK,aAAa,EAClB,KAAK,gBAAgB,EACrB,KAAK,kBAAkB,GACxB,MAAM,gBAAgB,CAAC;AACxB,YAAY,EAAE,0BAA0B,EAAE,MAAM,uBAAuB,CAAC;AACxE,YAAY,EAAE,aAAa,EAAE,MAAM,kBAAkB,CAAC;AACtD,YAAY,EAAE,oBAAoB,EAAE,MAAM,kBAAkB,CAAC;AAC7D,YAAY,EACV,iBAAiB,EACjB,qBAAqB,EACrB,8BAA8B,EAC9B,aAAa,EACb,mBAAmB,EACnB,yBAAyB,EACzB,iBAAiB,EACjB,qBAAqB,EACrB,eAAe,EACf,qBAAqB,GACtB,MAAM,gBAAgB,CAAC"}
|
|
1
|
+
{"version":3,"file":"root.d.ts","sourceRoot":"","sources":["../src/root.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,sBAAsB,EACtB,mBAAmB,EACnB,mBAAmB,EACnB,kCAAkC,EAClC,IAAI,EACJ,KAAK,kBAAkB,EACvB,KAAK,cAAc,EACnB,KAAK,qBAAqB,EAC1B,KAAK,oBAAoB,EACzB,KAAK,UAAU,EACf,KAAK,UAAU,EACf,KAAK,IAAI,EACT,KAAK,iBAAiB,EACtB,KAAK,eAAe,EACpB,KAAK,cAAc,EACnB,KAAK,qBAAqB,EAC1B,KAAK,iBAAiB,EACtB,KAAK,uBAAuB,EAC5B,KAAK,YAAY,EACjB,KAAK,gBAAgB,EACrB,KAAK,eAAe,EACpB,KAAK,eAAe,EACpB,KAAK,uBAAuB,EAC5B,KAAK,WAAW,EAChB,KAAK,eAAe,EACpB,KAAK,iBAAiB,EACtB,KAAK,oBAAoB,EACzB,KAAK,gBAAgB,EACrB,KAAK,aAAa,EAClB,KAAK,gBAAgB,EACrB,KAAK,kBAAkB,GACxB,MAAM,gBAAgB,CAAC;AACxB,YAAY,EAAE,0BAA0B,EAAE,MAAM,uBAAuB,CAAC;AACxE,YAAY,EAAE,aAAa,EAAE,MAAM,kBAAkB,CAAC;AACtD,YAAY,EAAE,kBAAkB,EAAE,MAAM,mBAAmB,CAAC;AAC5D,YAAY,EAAE,oBAAoB,EAAE,MAAM,kBAAkB,CAAC;AAC7D,YAAY,EACV,iBAAiB,EACjB,qBAAqB,EACrB,8BAA8B,EAC9B,aAAa,EACb,mBAAmB,EACnB,yBAAyB,EACzB,iBAAiB,EACjB,qBAAqB,EACrB,eAAe,EACf,qBAAqB,GACtB,MAAM,gBAAgB,CAAC"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"safe-path-segment.d.ts","sourceRoot":"","sources":["../src/safe-path-segment.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"safe-path-segment.d.ts","sourceRoot":"","sources":["../src/safe-path-segment.ts"],"names":[],"mappings":"AAWA,MAAM,MAAM,sBAAsB,GAAG;IACnC,cAAc,CAAC,EAAE,OAAO,CAAC;IACzB,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB,CAAC;AAEF,wBAAgB,mBAAmB,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAE1D;AAED,wBAAgB,iCAAiC,CAAC,KAAK,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,MAAM,CAKtF;AAED,wBAAgB,eAAe,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAUrD;AAED,wBAAgB,iBAAiB,CAC/B,OAAO,EAAE,MAAM,EACf,OAAO,GAAE,sBAA2B,GACnC,OAAO,CAaT;AAED,wBAAgB,qBAAqB,CACnC,OAAO,EAAE,MAAM,EACf,OAAO,GAAE,sBAA2B,GACnC,MAAM,CAUR;AAED,wBAAgB,uBAAuB,CACrC,KAAK,EAAE,MAAM,EACb,QAAQ,EAAE,MAAM,EAChB,OAAO,GAAE,sBAA2B,GACnC,MAAM,CAWR;AAED,wBAAgB,oBAAoB,CAClC,MAAM,EAAE,MAAM,EACd,OAAO,GAAE,sBAA2B,GACnC,MAAM,CAeR"}
|
|
@@ -4,12 +4,14 @@ const SAFE_DOT_PREFIX_PATH_SEGMENT_PATTERN = /^[A-Za-z0-9._-]+$/;
|
|
|
4
4
|
// Windows treats "C:name" as relative to the drive's current directory even
|
|
5
5
|
// though path.win32.isAbsolute() reports false.
|
|
6
6
|
const DRIVE_RELATIVE_PREFIX = /^[A-Za-z]:(?![\\/])/;
|
|
7
|
+
// A forward slash ends the segment, so "C:/file" still contains the "C:" segment.
|
|
8
|
+
const DRIVE_RELATIVE_SEGMENT = /(?:^|\/)[A-Za-z]:(?!\\)/;
|
|
7
9
|
const HYPHEN_CHAR_CODE = 0x2d;
|
|
8
10
|
export function isDriveRelativePath(value) {
|
|
9
11
|
return DRIVE_RELATIVE_PREFIX.test(value);
|
|
10
12
|
}
|
|
11
13
|
export function assertNoDriveRelativePathSegments(value, label) {
|
|
12
|
-
if (
|
|
14
|
+
if (DRIVE_RELATIVE_SEGMENT.test(value)) {
|
|
13
15
|
throw new FsSafeError("invalid-path", `${label} must not contain a drive letter`);
|
|
14
16
|
}
|
|
15
17
|
return value;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"secret-file.d.ts","sourceRoot":"","sources":["../src/secret-file.ts"],"names":[],"mappings":"AAAA,OAAW,EAAE,KAAK,WAAW,EAAE,MAAM,SAAS,CAAC;
|
|
1
|
+
{"version":3,"file":"secret-file.d.ts","sourceRoot":"","sources":["../src/secret-file.ts"],"names":[],"mappings":"AAAA,OAAW,EAAE,KAAK,WAAW,EAAE,MAAM,SAAS,CAAC;AAM/C,OAAO,EAAkF,KAAK,mBAAmB,EAAE,MAAM,sBAAsB,CAAC;AAUhJ,OAAO,EAML,KAAK,qBAAqB,EAC3B,MAAM,yBAAyB,CAAC;AAIjC,eAAO,MAAM,uBAAuB,MAAQ,CAAC;AAC7C,eAAO,MAAM,wBAAwB,MAAQ,CAAC;AAE9C,wBAAgB,kBAAkB,CAChC,QAAQ,EAAE,MAAM,EAChB,KAAK,EAAE,MAAM,EACb,OAAO,GAAE,qBAA0B,GAClC,MAAM,CAkER;AAED,wBAAgB,qBAAqB,CACnC,QAAQ,EAAE,MAAM,GAAG,SAAS,EAC5B,KAAK,EAAE,MAAM,EACb,OAAO,GAAE,qBAA0B,GAClC,MAAM,GAAG,SAAS,CAUpB;AA8JD,KAAK,qBAAqB,GAAG;IAC3B,OAAO,EAAE,MAAM,CAAC;IAChB,QAAQ,EAAE,MAAM,CAAC;IACjB,OAAO,EAAE,MAAM,GAAG,UAAU,CAAC;IAC7B,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,OAAO,CAAC,EAAE,OAAO,CAAC;CACnB,CAAC;AAYF,wBAAsB,sBAAsB,CAC1C,MAAM,EAAE,IAAI,CAAC,qBAAqB,EAAE,SAAS,CAAC,GAC7C,OAAO,CAAC;IACT,IAAI,EAAE,MAAM,CAAC;IACb,SAAS,EAAE,mBAAmB,CAAC,WAAW,CAAC,CAAC;IAC5C,WAAW,EAAE,mBAAmB,CAAC,WAAW,CAAC,CAAC;IAC9C,QAAQ,EAAE,MAAM,CAAC;IACjB,aAAa,EAAE,MAAM,CAAC;CACvB,CAAC,CAuBD;AAwDD,wBAAsB,qBAAqB,CAAC,MAAM,EAAE,qBAAqB,GAAG,OAAO,CAAC,IAAI,CAAC,CAKxF;AAED,wBAAsB,sBAAsB,CAAC,MAAM,EAAE,qBAAqB,GAAG,OAAO,CAAC,IAAI,CAAC,CAezF"}
|
package/dist/secret-file.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import fs, {} from "node:fs";
|
|
2
2
|
import fsp from "node:fs/promises";
|
|
3
3
|
import path from "node:path";
|
|
4
|
+
import { recursiveMkdirPath } from "./recursive-mkdir-path.js";
|
|
4
5
|
import { canonicalPathFromExistingAncestor } from "./absolute-path.js";
|
|
5
6
|
import { readFileDescriptorBoundedSync } from "./bounded-read.js";
|
|
6
7
|
import { assertAsyncDirectoryGuard, createAsyncDirectoryGuard, inspectDirectoryIdentity } from "./directory-guard.js";
|
|
@@ -166,7 +167,7 @@ async function ensurePrivateDirectory(rootDir, targetDir, mode) {
|
|
|
166
167
|
catch (error) {
|
|
167
168
|
if (error.code !== "ENOENT")
|
|
168
169
|
throw error;
|
|
169
|
-
await fsp.mkdir(path.dirname(resolvedRoot), { recursive: true, mode });
|
|
170
|
+
await fsp.mkdir(recursiveMkdirPath(path.dirname(resolvedRoot)), { recursive: true, mode });
|
|
170
171
|
createdRoot = await createPrivateDirectory(resolvedRoot, mode);
|
|
171
172
|
}
|
|
172
173
|
rootStat = await inspectPrivateDirectory(resolvedRoot, "root");
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"secret-read-async.d.ts","sourceRoot":"","sources":["../src/secret-read-async.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"secret-read-async.d.ts","sourceRoot":"","sources":["../src/secret-read-async.ts"],"names":[],"mappings":"AAQA,OAAO,EAML,KAAK,qBAAqB,EAC3B,MAAM,yBAAyB,CAAC;AAEjC,wBAAsB,cAAc,CAClC,QAAQ,EAAE,MAAM,EAChB,KAAK,EAAE,MAAM,EACb,OAAO,GAAE,qBAA0B,GAClC,OAAO,CAAC,MAAM,CAAC,CA0DjB;AAED,wBAAsB,iBAAiB,CACrC,QAAQ,EAAE,MAAM,GAAG,SAAS,EAC5B,KAAK,EAAE,MAAM,EACb,OAAO,GAAE,qBAA0B,GAClC,OAAO,CAAC,MAAM,GAAG,SAAS,CAAC,CAQ7B"}
|
|
@@ -4,6 +4,7 @@ import { readFileHandleBounded } from "./bounded-read.js";
|
|
|
4
4
|
import { assertNoUnsafeDeviceReadPath } from "./device-path.js";
|
|
5
5
|
import { FsSafeError } from "./errors.js";
|
|
6
6
|
import { resolveReadOpenFlags } from "./read-open-flags.js";
|
|
7
|
+
import { realpathSync } from "./realpath.js";
|
|
7
8
|
import { inspectFileIdentity } from "./strict-file-identity.js";
|
|
8
9
|
import { assertSecretFilePreview, resolveSecretReadPolicy, secretPathErrorCode, secretReadError, trimSecretFileContent, } from "./secret-read-policy.js";
|
|
9
10
|
export async function readSecretFile(filePath, label, options = {}) {
|
|
@@ -29,7 +30,7 @@ export async function readSecretFile(filePath, label, options = {}) {
|
|
|
29
30
|
let handle;
|
|
30
31
|
let raw;
|
|
31
32
|
try {
|
|
32
|
-
const realPath =
|
|
33
|
+
const realPath = realpathSync.native(resolvedPath);
|
|
33
34
|
assertNoUnsafeDeviceReadPath(realPath);
|
|
34
35
|
handle = await fs.open(realPath, resolveReadOpenFlags());
|
|
35
36
|
const openedHandle = handle;
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { BigIntStats, Stats } from "node:fs";
|
|
2
|
+
import type { PermissionCheck } from "./permissions.js";
|
|
3
|
+
type ExactIdentity = Pick<BigIntStats, "dev" | "ino">;
|
|
4
|
+
export declare function inspectSecureWindowsDescriptor(params: {
|
|
5
|
+
fd: number;
|
|
6
|
+
identity: ExactIdentity;
|
|
7
|
+
stat: Stats;
|
|
8
|
+
}): PermissionCheck;
|
|
9
|
+
export {};
|
|
10
|
+
//# sourceMappingURL=secure-file-windows.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"secure-file-windows.d.ts","sourceRoot":"","sources":["../src/secure-file-windows.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,KAAK,EAAE,MAAM,SAAS,CAAC;AASlD,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,kBAAkB,CAAC;AA4BxD,KAAK,aAAa,GAAG,IAAI,CAAC,WAAW,EAAE,KAAK,GAAG,KAAK,CAAC,CAAC;AAuHtD,wBAAgB,8BAA8B,CAAC,MAAM,EAAE;IACrD,EAAE,EAAE,MAAM,CAAC;IACX,QAAQ,EAAE,aAAa,CAAC;IACxB,IAAI,EAAE,KAAK,CAAC;CACb,GAAG,eAAe,CA0ClB"}
|