@openclaw/feishu 2026.9.8 → 2026.10.1-beta.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/dist/.setup/{channel-DV4hM2Uf.mjs → channel-CX62T71S.mjs} +146 -245
- package/dist/.setup/{channel.runtime-CXQcH5zZ.mjs → channel.runtime-COiaq-Rv.mjs} +6 -12
- package/dist/.setup/{chat-D8MlFAZi.mjs → chat-BdyvImsY.mjs} +6 -9
- package/dist/.setup/{client-BYG-_IZl.mjs → client-BsBVWJzN.mjs} +1 -1
- package/dist/.setup/{doctor-contract-3XC0vbIw.mjs → doctor-contract-B1B9ogdd.mjs} +212 -27
- package/dist/.setup/{monitor-D_rsHBZ6.mjs → monitor-7DvwUs4Y.mjs} +5 -6
- package/dist/.setup/{monitor.account-DvvY7i6k.mjs → monitor.account-7xVu7XWY.mjs} +190 -393
- package/dist/.setup/{probe-_ViqSi0j.mjs → probe-B9o7iey6.mjs} +13 -39
- package/dist/.setup/{reply-delivery-result-COAjaukc.mjs → reply-delivery-result-Bt-BWBEA.mjs} +151 -297
- package/dist/.setup/{setup-api-C4S5I3ac.mjs → setup-api-DmzX9S01.mjs} +1 -1
- package/dist/.setup/{subagent-hooks-BaFoMu8W.mjs → subagent-hooks-CUXg0pC-.mjs} +5 -11
- package/dist/.setup/{thread-bindings-EdLXPXzw.mjs → thread-bindings-jW2x5VuN.mjs} +23 -41
- package/dist/api.js +234 -297
- package/dist/channel-plugin-api.js +1 -1
- package/dist/doctor-contract-api.js +2 -2
- package/dist/session-binding-contract-api.js +1 -1
- package/dist/setup-api.js +1 -1
- package/dist/setup-entry.js +1 -1
- package/dist/subagent-hooks-api.js +1 -1
- package/node_modules/@openclaw/fs-safe/CHANGELOG.md +95 -0
- package/node_modules/@openclaw/fs-safe/README.md +70 -379
- package/node_modules/@openclaw/fs-safe/dist/absolute-path.js +35 -51
- package/node_modules/@openclaw/fs-safe/dist/advanced.d.ts +2 -0
- package/node_modules/@openclaw/fs-safe/dist/advanced.js +1 -0
- package/node_modules/@openclaw/fs-safe/dist/archive-input.js +2 -0
- package/node_modules/@openclaw/fs-safe/dist/archive-parser.wasm +0 -0
- package/node_modules/@openclaw/fs-safe/dist/archive-staging.d.ts +1 -1
- package/node_modules/@openclaw/fs-safe/dist/archive-staging.js +9 -10
- package/node_modules/@openclaw/fs-safe/dist/archive.js +2 -5
- package/node_modules/@openclaw/fs-safe/dist/atomic-io.d.ts +51 -0
- package/node_modules/@openclaw/fs-safe/dist/atomic-io.js +242 -0
- package/node_modules/@openclaw/fs-safe/dist/config.d.ts +1 -1
- package/node_modules/@openclaw/fs-safe/dist/config.js +1 -1
- package/node_modules/@openclaw/fs-safe/dist/copy-file-input.d.ts +1 -1
- package/node_modules/@openclaw/fs-safe/dist/copy-file-input.js +2 -2
- package/node_modules/@openclaw/fs-safe/dist/copy-tree-portable.js +2 -0
- package/node_modules/@openclaw/fs-safe/dist/create.js +12 -5
- package/node_modules/@openclaw/fs-safe/dist/directory-guard.d.ts +11 -0
- package/node_modules/@openclaw/fs-safe/dist/directory-guard.js +4 -7
- package/node_modules/@openclaw/fs-safe/dist/entry-publication-types.d.ts +55 -0
- package/node_modules/@openclaw/fs-safe/dist/entry-publication-types.js +1 -0
- package/node_modules/@openclaw/fs-safe/dist/entry-publication.d.ts +8 -0
- package/node_modules/@openclaw/fs-safe/dist/entry-publication.js +262 -0
- package/node_modules/@openclaw/fs-safe/dist/exclusive-create.d.ts +3 -0
- package/node_modules/@openclaw/fs-safe/dist/exclusive-create.js +22 -0
- package/node_modules/@openclaw/fs-safe/dist/file-cleanup.d.ts +1 -6
- package/node_modules/@openclaw/fs-safe/dist/file-cleanup.js +3 -6
- package/node_modules/@openclaw/fs-safe/dist/file-identity.js +3 -6
- package/node_modules/@openclaw/fs-safe/dist/file-lock-sync-root-io.js +2 -2
- package/node_modules/@openclaw/fs-safe/dist/file-lock-sync-root-mutation.js +9 -0
- package/node_modules/@openclaw/fs-safe/dist/file-lock-sync-root.js +4 -6
- package/node_modules/@openclaw/fs-safe/dist/file-lock-sync-stale-admission.js +1 -1
- package/node_modules/@openclaw/fs-safe/dist/file-lock-sync.js +8 -0
- package/node_modules/@openclaw/fs-safe/dist/file-store-boundary.js +2 -0
- package/node_modules/@openclaw/fs-safe/dist/file-store-sync-write.js +20 -18
- package/node_modules/@openclaw/fs-safe/dist/file-store.js +4 -10
- package/node_modules/@openclaw/fs-safe/dist/fs.d.ts +2 -10
- package/node_modules/@openclaw/fs-safe/dist/fs.js +2 -10
- package/node_modules/@openclaw/fs-safe/dist/guarded-mkdir.d.ts +3 -4
- package/node_modules/@openclaw/fs-safe/dist/guarded-mkdir.js +6 -18
- package/node_modules/@openclaw/fs-safe/dist/guest-dispatch-python.js +1 -1
- package/node_modules/@openclaw/fs-safe/dist/guest-native-python.js +0 -2
- package/node_modules/@openclaw/fs-safe/dist/guest.js +3 -9
- package/node_modules/@openclaw/fs-safe/dist/index.d.ts +1 -1
- package/node_modules/@openclaw/fs-safe/dist/index.js +1 -1
- package/node_modules/@openclaw/fs-safe/dist/json-durable-queue-directory.js +1 -5
- package/node_modules/@openclaw/fs-safe/dist/json.js +19 -37
- package/node_modules/@openclaw/fs-safe/dist/move-path.js +2 -0
- package/node_modules/@openclaw/fs-safe/dist/mutation-authority.d.ts +1 -0
- package/node_modules/@openclaw/fs-safe/dist/mutation-authority.js +9 -1
- package/node_modules/@openclaw/fs-safe/dist/native-binding.d.ts +47 -2
- package/node_modules/@openclaw/fs-safe/dist/native-config.d.ts +1 -9
- package/node_modules/@openclaw/fs-safe/dist/native-config.js +6 -40
- package/node_modules/@openclaw/fs-safe/dist/native-parent-admission.d.ts +2 -0
- package/node_modules/@openclaw/fs-safe/dist/native-parent-admission.js +5 -2
- package/node_modules/@openclaw/fs-safe/dist/native-pinned-write.js +11 -316
- package/node_modules/@openclaw/fs-safe/dist/native-policy-parent-windows.d.ts +7 -7
- package/node_modules/@openclaw/fs-safe/dist/native-policy-parent-windows.js +28 -175
- package/node_modules/@openclaw/fs-safe/dist/native-policy-parent.d.ts +15 -0
- package/node_modules/@openclaw/fs-safe/dist/native-policy-parent.js +418 -0
- package/node_modules/@openclaw/fs-safe/dist/native-rename-outcome.js +9 -4
- package/node_modules/@openclaw/fs-safe/dist/native-staged-file.js +17 -13
- package/node_modules/@openclaw/fs-safe/dist/native-staged-symlink.js +6 -24
- package/node_modules/@openclaw/fs-safe/dist/native.js +5 -1
- package/node_modules/@openclaw/fs-safe/dist/path-case.js +10 -9
- package/node_modules/@openclaw/fs-safe/dist/path-scope-lexical.js +4 -1
- package/node_modules/@openclaw/fs-safe/dist/path-segment-route.d.ts +1 -0
- package/node_modules/@openclaw/fs-safe/dist/path-segment-route.js +3 -0
- package/node_modules/@openclaw/fs-safe/dist/path.js +4 -1
- package/node_modules/@openclaw/fs-safe/dist/permissions-windows.d.ts +0 -1
- package/node_modules/@openclaw/fs-safe/dist/pinned-mutation-admission.js +1 -3
- package/node_modules/@openclaw/fs-safe/dist/pinned-write-staged.js +2 -0
- package/node_modules/@openclaw/fs-safe/dist/pinned-write.js +9 -10
- package/node_modules/@openclaw/fs-safe/dist/private-producer-handoff.js +3 -6
- package/node_modules/@openclaw/fs-safe/dist/publish-copy-stage.js +2 -2
- package/node_modules/@openclaw/fs-safe/dist/publish-file.js +4 -3
- package/node_modules/@openclaw/fs-safe/dist/replace-file-buffer.d.ts +3 -4
- package/node_modules/@openclaw/fs-safe/dist/replace-file-buffer.js +18 -32
- package/node_modules/@openclaw/fs-safe/dist/replace-file-copy-fallback.d.ts +5 -17
- package/node_modules/@openclaw/fs-safe/dist/replace-file-copy-fallback.js +92 -230
- package/node_modules/@openclaw/fs-safe/dist/replace-file-copy-source.d.ts +4 -15
- package/node_modules/@openclaw/fs-safe/dist/replace-file-copy-source.js +22 -63
- package/node_modules/@openclaw/fs-safe/dist/replace-file-descriptor.d.ts +9 -34
- package/node_modules/@openclaw/fs-safe/dist/replace-file-descriptor.js +78 -83
- package/node_modules/@openclaw/fs-safe/dist/replace-file-destination.d.ts +18 -20
- package/node_modules/@openclaw/fs-safe/dist/replace-file-destination.js +64 -98
- package/node_modules/@openclaw/fs-safe/dist/replace-file-temp-owner.d.ts +16 -33
- package/node_modules/@openclaw/fs-safe/dist/replace-file-temp-owner.js +61 -200
- package/node_modules/@openclaw/fs-safe/dist/replace-file-types.d.ts +1 -6
- package/node_modules/@openclaw/fs-safe/dist/replace-file.js +76 -216
- package/node_modules/@openclaw/fs-safe/dist/root-boundary.js +14 -24
- package/node_modules/@openclaw/fs-safe/dist/root-context.d.ts +1 -4
- package/node_modules/@openclaw/fs-safe/dist/root-context.js +4 -19
- package/node_modules/@openclaw/fs-safe/dist/root-create-native.d.ts +52 -0
- package/node_modules/@openclaw/fs-safe/dist/root-create-native.js +350 -0
- package/node_modules/@openclaw/fs-safe/dist/root-directory-list-types.d.ts +35 -0
- package/node_modules/@openclaw/fs-safe/dist/root-directory-list-types.js +1 -0
- package/node_modules/@openclaw/fs-safe/dist/root-directory-list.d.ts +2 -27
- package/node_modules/@openclaw/fs-safe/dist/root-directory-list.js +31 -28
- package/node_modules/@openclaw/fs-safe/dist/root-directory.js +1 -4
- package/node_modules/@openclaw/fs-safe/dist/root-errors.js +34 -0
- package/node_modules/@openclaw/fs-safe/dist/root-file.js +9 -21
- package/node_modules/@openclaw/fs-safe/dist/root-impl.js +105 -37
- package/node_modules/@openclaw/fs-safe/dist/root-move-noreplace.d.ts +7 -2
- package/node_modules/@openclaw/fs-safe/dist/root-move-noreplace.js +67 -38
- package/node_modules/@openclaw/fs-safe/dist/root-observed-path.d.ts +1 -1
- package/node_modules/@openclaw/fs-safe/dist/root-observed-path.js +0 -2
- package/node_modules/@openclaw/fs-safe/dist/root-options.d.ts +2 -3
- package/node_modules/@openclaw/fs-safe/dist/root-path-existing.js +3 -1
- package/node_modules/@openclaw/fs-safe/dist/root-path-stat.js +68 -85
- package/node_modules/@openclaw/fs-safe/dist/root-path.js +6 -1
- package/node_modules/@openclaw/fs-safe/dist/root-paths-lexical.d.ts +4 -0
- package/node_modules/@openclaw/fs-safe/dist/root-paths-lexical.js +1 -1
- package/node_modules/@openclaw/fs-safe/dist/root-paths.js +4 -9
- package/node_modules/@openclaw/fs-safe/dist/root-remove-native.d.ts +4 -0
- package/node_modules/@openclaw/fs-safe/dist/root-remove-native.js +311 -0
- package/node_modules/@openclaw/fs-safe/dist/root-remove.d.ts +11 -0
- package/node_modules/@openclaw/fs-safe/dist/root-remove.js +3 -1
- package/node_modules/@openclaw/fs-safe/dist/root-walk.d.ts +2 -2
- package/node_modules/@openclaw/fs-safe/dist/root-walk.js +1 -3
- package/node_modules/@openclaw/fs-safe/dist/root-write-admission.d.ts +3 -3
- package/node_modules/@openclaw/fs-safe/dist/root-write-admission.js +8 -10
- package/node_modules/@openclaw/fs-safe/dist/safe-path-segment.d.ts +0 -1
- package/node_modules/@openclaw/fs-safe/dist/safe-path-segment.js +0 -4
- package/node_modules/@openclaw/fs-safe/dist/secret-file.js +12 -9
- package/node_modules/@openclaw/fs-safe/dist/secure-temp-dir.d.ts +0 -2
- package/node_modules/@openclaw/fs-safe/dist/sibling-staged-file.js +2 -0
- package/node_modules/@openclaw/fs-safe/dist/sibling-temp.js +7 -4
- package/node_modules/@openclaw/fs-safe/dist/sidecar-lock-acquire.js +13 -6
- package/node_modules/@openclaw/fs-safe/dist/sidecar-lock-reclaim.d.ts +0 -3
- package/node_modules/@openclaw/fs-safe/dist/sidecar-lock-reclaim.js +5 -23
- package/node_modules/@openclaw/fs-safe/dist/staged-file-settlement.d.ts +2 -1
- package/node_modules/@openclaw/fs-safe/dist/staged-file-settlement.js +11 -6
- package/node_modules/@openclaw/fs-safe/dist/temp-cleanup.js +1 -8
- package/node_modules/@openclaw/fs-safe/dist/temp-workspace-child-admission.d.ts +1 -1
- package/node_modules/@openclaw/fs-safe/dist/temp-workspace-child-admission.js +2 -9
- package/node_modules/@openclaw/fs-safe/dist/text-atomic.d.ts +1 -6
- package/node_modules/@openclaw/fs-safe/dist/trash.js +6 -2
- package/node_modules/@openclaw/fs-safe/dist/walk.js +100 -46
- package/node_modules/@openclaw/fs-safe/dist/watch-hints.d.ts +7 -0
- package/node_modules/@openclaw/fs-safe/dist/watch-hints.js +235 -19
- package/node_modules/@openclaw/fs-safe/dist/watch-native.d.ts +21 -2
- package/node_modules/@openclaw/fs-safe/dist/watch-native.js +21 -3
- package/node_modules/@openclaw/fs-safe/dist/watch-rescan.d.ts +6 -0
- package/node_modules/@openclaw/fs-safe/dist/watch-rescan.js +111 -0
- package/node_modules/@openclaw/fs-safe/dist/watch-scan.d.ts +13 -0
- package/node_modules/@openclaw/fs-safe/dist/watch-scan.js +27 -4
- package/node_modules/@openclaw/fs-safe/dist/watch-stream.js +2 -0
- package/node_modules/@openclaw/fs-safe/dist/watch.js +131 -40
- package/node_modules/@openclaw/fs-safe/dist/windows-path-alias.d.ts +8 -0
- package/node_modules/@openclaw/fs-safe/dist/windows-path-alias.js +24 -6
- package/node_modules/@openclaw/fs-safe/dist/windows-path-syntax.d.ts +13 -0
- package/node_modules/@openclaw/fs-safe/dist/windows-path-syntax.js +51 -0
- package/node_modules/@openclaw/fs-safe/docs/advanced.md +4 -0
- package/node_modules/@openclaw/fs-safe/docs/atomic.md +4 -1
- package/node_modules/@openclaw/fs-safe/docs/config.md +6 -14
- package/node_modules/@openclaw/fs-safe/docs/contributing.md +4 -14
- package/node_modules/@openclaw/fs-safe/docs/durability.md +6 -29
- package/node_modules/@openclaw/fs-safe/docs/entry-publication.md +146 -0
- package/node_modules/@openclaw/fs-safe/docs/errors.md +16 -0
- package/node_modules/@openclaw/fs-safe/docs/file-store.md +28 -5
- package/node_modules/@openclaw/fs-safe/docs/index.md +2 -31
- package/node_modules/@openclaw/fs-safe/docs/install.md +10 -31
- package/node_modules/@openclaw/fs-safe/docs/json.md +1 -1
- package/node_modules/@openclaw/fs-safe/docs/local-roots.md +0 -1
- package/node_modules/@openclaw/fs-safe/docs/migrating-to-0.5.md +12 -12
- package/node_modules/@openclaw/fs-safe/docs/native-helper.md +35 -96
- package/node_modules/@openclaw/fs-safe/docs/native.md +22 -20
- package/node_modules/@openclaw/fs-safe/docs/path.md +1 -1
- package/node_modules/@openclaw/fs-safe/docs/permissions.md +1 -2
- package/node_modules/@openclaw/fs-safe/docs/public-api.md +1 -2
- package/node_modules/@openclaw/fs-safe/docs/reading.md +1 -2
- package/node_modules/@openclaw/fs-safe/docs/root.md +26 -184
- package/node_modules/@openclaw/fs-safe/docs/secret-file.md +1 -1
- package/node_modules/@openclaw/fs-safe/docs/security-model.md +115 -3
- package/node_modules/@openclaw/fs-safe/docs/store.md +2 -2
- package/node_modules/@openclaw/fs-safe/docs/temp.md +29 -65
- package/node_modules/@openclaw/fs-safe/docs/testing.md +90 -58
- package/node_modules/@openclaw/fs-safe/docs/types.md +3 -16
- package/node_modules/@openclaw/fs-safe/docs/walk.md +33 -5
- package/node_modules/@openclaw/fs-safe/docs/watch.md +96 -14
- package/node_modules/@openclaw/fs-safe/docs/writing.md +172 -16
- package/node_modules/@openclaw/fs-safe/package.json +10 -10
- package/node_modules/@openclaw/fs-safe-darwin-arm64/fs-safe-native.node +0 -0
- package/node_modules/@openclaw/fs-safe-darwin-arm64/package.json +1 -1
- package/node_modules/@openclaw/fs-safe-darwin-x64/fs-safe-native.node +0 -0
- package/node_modules/@openclaw/fs-safe-darwin-x64/package.json +1 -1
- package/node_modules/@openclaw/fs-safe-linux-arm64-gnu/fs-safe-native.node +0 -0
- package/node_modules/@openclaw/fs-safe-linux-arm64-gnu/package.json +1 -1
- package/node_modules/@openclaw/fs-safe-linux-arm64-musl/fs-safe-native.node +0 -0
- package/node_modules/@openclaw/fs-safe-linux-arm64-musl/package.json +1 -1
- package/node_modules/@openclaw/fs-safe-linux-x64-gnu/fs-safe-native.node +0 -0
- package/node_modules/@openclaw/fs-safe-linux-x64-gnu/package.json +1 -1
- package/node_modules/@openclaw/fs-safe-linux-x64-musl/fs-safe-native.node +0 -0
- package/node_modules/@openclaw/fs-safe-linux-x64-musl/package.json +1 -1
- package/node_modules/@openclaw/fs-safe-win32-x64-msvc/fs-safe-native.node +0 -0
- package/node_modules/@openclaw/fs-safe-win32-x64-msvc/package.json +1 -1
- package/package.json +5 -5
- package/skills/feishu-wiki/SKILL.md +1 -1
- package/dist/.setup/accounts-wRqItHug.mjs +0 -206
- package/node_modules/@openclaw/fs-safe/dist/watch-alias.d.ts +0 -6
- package/node_modules/@openclaw/fs-safe/dist/watch-alias.js +0 -88
- package/node_modules/@openclaw/fs-safe/docs/mutation-policy-proof.md +0 -69
- package/node_modules/@openclaw/fs-safe/docs/private-file-store.md +0 -68
- package/node_modules/@openclaw/fs-safe/docs/test-hooks.md +0 -110
|
@@ -4,7 +4,7 @@ import { FsSafeError } from "./errors.js";
|
|
|
4
4
|
import { formatErrorDetail } from "./error-detail.js";
|
|
5
5
|
import { isNotFoundPathError, isPathInside, isSymlinkOpenError } from "./path.js";
|
|
6
6
|
import { realpathSync } from "./realpath.js";
|
|
7
|
-
import { assertNoWindowsPathAlias, pathForWindowsFilesystem, resolvePathFromBasePreservingWindowsRoot, resolvePathPreservingWindowsRoot, } from "./windows-path-alias.js";
|
|
7
|
+
import { assertNoWindowsPathAlias, isForeignWindowsShareOrDevicePath, pathForWindowsFilesystem, resolvePathFromBasePreservingWindowsRoot, resolvePathPreservingWindowsRoot, } from "./windows-path-alias.js";
|
|
8
8
|
import { admitPathInsideRoot } from "./root-boundary.js";
|
|
9
9
|
import { errorCauseOptions } from "./root-errors.js";
|
|
10
10
|
export function absolutePathWithRawSegments(candidate) {
|
|
@@ -29,6 +29,8 @@ export function rawPathRelativeToCanonicalRoot(candidate, rootCanonicalPath, opt
|
|
|
29
29
|
assertNoWindowsPathAlias(rootCanonicalPath);
|
|
30
30
|
const absolute = absolutePathWithRawSegments(candidate);
|
|
31
31
|
const raw = process.platform === "win32" ? absolute.replaceAll("/", path.sep) : absolute;
|
|
32
|
+
if (isForeignWindowsShareOrDevicePath(raw, [rootCanonicalPath]))
|
|
33
|
+
return undefined;
|
|
32
34
|
const filesystemRoot = path.parse(raw).root;
|
|
33
35
|
const segments = raw.slice(filesystemRoot.length).split(path.sep);
|
|
34
36
|
const finalComponentIndex = segments.findLastIndex(segment => segment !== "" && segment !== ".");
|
|
@@ -50,102 +50,85 @@ export async function statResolvedPathInRoot(root, resolvedPath, receipt) {
|
|
|
50
50
|
const proof = {};
|
|
51
51
|
try {
|
|
52
52
|
// Nested observations in a hook cannot grant this invocation discard authority.
|
|
53
|
-
return await fileObservation().run(() =>
|
|
54
|
-
|
|
55
|
-
catch (error) {
|
|
56
|
-
const failure = proof.failure;
|
|
57
|
-
if (failure && failure.error === error)
|
|
58
|
-
recordFileObservationFailure(error, failure.kind);
|
|
59
|
-
throw error;
|
|
60
|
-
}
|
|
61
|
-
}
|
|
62
|
-
async function statObservedPathInRoot(root, resolvedPath, proof, receipt) {
|
|
63
|
-
let admittedTarget = false;
|
|
64
|
-
try {
|
|
65
|
-
if (receipt) {
|
|
66
|
-
if (receipt.kind !== "stat" || receipt.targetPath !== resolvedPath) {
|
|
67
|
-
throw new FsSafeError("path-mismatch", "file observation receipt does not match target");
|
|
68
|
-
}
|
|
69
|
-
admittedTarget = true;
|
|
70
|
-
const beforeObservation = getFsSafeTestHooks()?.beforeRootStatObservation;
|
|
71
|
-
if (beforeObservation)
|
|
72
|
-
await beforeObservation(resolvedPath);
|
|
73
|
-
let observed;
|
|
74
|
-
let sample, samples = 0;
|
|
75
|
-
const comparison = fileObservation();
|
|
53
|
+
return await fileObservation().run(async () => {
|
|
54
|
+
let admittedTarget = false;
|
|
76
55
|
try {
|
|
77
|
-
|
|
56
|
+
let parentGuard;
|
|
57
|
+
let expected;
|
|
58
|
+
if (receipt) {
|
|
59
|
+
if (receipt.kind !== "stat" || receipt.targetPath !== resolvedPath) {
|
|
60
|
+
throw new FsSafeError("path-mismatch", "file observation receipt does not match target");
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
else {
|
|
64
|
+
const guardPath = resolvedPath === root.rootReal ? root.rootReal : path.dirname(resolvedPath);
|
|
65
|
+
parentGuard = guardPath === root.rootReal && root.rootGuard
|
|
66
|
+
? root.rootGuard
|
|
67
|
+
: await createRootDirectoryObservationGuard(root, guardPath);
|
|
68
|
+
try {
|
|
69
|
+
const beforeInitialObservation = getFsSafeTestHooks()?.beforeRootStatInitialObservation;
|
|
70
|
+
if (beforeInitialObservation)
|
|
71
|
+
await beforeInitialObservation(resolvedPath);
|
|
72
|
+
expected = inspectFileIdentitySync(() => fsSync.lstatSync(resolvedPath, { bigint: true }));
|
|
73
|
+
if (expected.isSymbolicLink()) {
|
|
74
|
+
throw new FsSafeError("path-mismatch", "file changed during operation");
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
catch (error) {
|
|
78
|
+
// No initial target error is safe to expose until the admitted parent is
|
|
79
|
+
// proven current; a redirect must win over an ordinary lookup failure.
|
|
80
|
+
await assertRootDirectoryObservationGuard(root, parentGuard);
|
|
81
|
+
throw error;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
admittedTarget = true;
|
|
85
|
+
const beforeObservation = getFsSafeTestHooks()?.beforeRootStatObservation;
|
|
86
|
+
if (beforeObservation)
|
|
87
|
+
await beforeObservation(resolvedPath);
|
|
88
|
+
let observed;
|
|
89
|
+
let sample, samples = 0;
|
|
90
|
+
const comparison = fileObservation();
|
|
91
|
+
const inspect = (bigint = true) => {
|
|
78
92
|
sample = bigint ? fsSync.lstatSync(resolvedPath, { bigint: true }) : fsSync.lstatSync(resolvedPath);
|
|
79
93
|
samples += 1;
|
|
80
94
|
return sample;
|
|
81
|
-
}
|
|
95
|
+
};
|
|
96
|
+
try {
|
|
97
|
+
observed = comparison.run(() => receipt
|
|
98
|
+
? assertStatObservationSync(inspect, receipt.target.identity)
|
|
99
|
+
: inspectFileIdentitySync(inspect, expected));
|
|
100
|
+
}
|
|
101
|
+
catch (error) {
|
|
102
|
+
if (isNotFoundPathError(error)) {
|
|
103
|
+
throw await missingObservedFileError(error, resolvedPath, receipt ? ("stat" in receipt.target ? receipt.target.stat : undefined) : expected, () => receipt ? assertRootPathObservationReceiptCurrent(root, receipt)
|
|
104
|
+
: assertRootDirectoryObservationGuard(root, parentGuard), proof);
|
|
105
|
+
}
|
|
106
|
+
if (comparison.has(error, "identity"))
|
|
107
|
+
await captureChangedObservedFile(error, resolvedPath, receipt ? ("stat" in receipt.target ? receipt.target.stat : undefined) : expected, samples === 1 ? sample : undefined, () => receipt ? assertRootPathObservationReceiptCurrent(root, receipt)
|
|
108
|
+
: assertRootDirectoryObservationGuard(root, parentGuard), proof);
|
|
109
|
+
throw error;
|
|
110
|
+
}
|
|
111
|
+
if (observed.isSymbolicLink()) {
|
|
112
|
+
throw new FsSafeError("path-mismatch", "file changed during operation");
|
|
113
|
+
}
|
|
114
|
+
if (receipt)
|
|
115
|
+
assertRootPathObservationReceiptCurrent(root, receipt, observed);
|
|
116
|
+
else
|
|
117
|
+
await assertRootDirectoryObservationGuard(root, parentGuard);
|
|
118
|
+
return pathStatFromStats(observed);
|
|
82
119
|
}
|
|
83
120
|
catch (error) {
|
|
84
|
-
if (isNotFoundPathError(error)) {
|
|
85
|
-
throw
|
|
121
|
+
if (!admittedTarget && isNotFoundPathError(error)) {
|
|
122
|
+
throw fileNotFoundError(error instanceof Error ? error : undefined);
|
|
86
123
|
}
|
|
87
|
-
if (comparison.has(error, "identity"))
|
|
88
|
-
await captureChangedObservedFile(error, resolvedPath, "stat" in receipt.target ? receipt.target.stat : undefined, samples === 1 ? sample : undefined, () => assertRootPathObservationReceiptCurrent(root, receipt), proof);
|
|
89
124
|
throw error;
|
|
90
125
|
}
|
|
91
|
-
|
|
92
|
-
throw new FsSafeError("path-mismatch", "file changed during operation");
|
|
93
|
-
}
|
|
94
|
-
assertRootPathObservationReceiptCurrent(root, receipt, observed);
|
|
95
|
-
return pathStatFromStats(observed);
|
|
96
|
-
}
|
|
97
|
-
const guardPath = resolvedPath === root.rootReal ? root.rootReal : path.dirname(resolvedPath);
|
|
98
|
-
const parentGuard = guardPath === root.rootReal && root.rootGuard
|
|
99
|
-
? root.rootGuard
|
|
100
|
-
: await createRootDirectoryObservationGuard(root, guardPath);
|
|
101
|
-
let expected;
|
|
102
|
-
try {
|
|
103
|
-
const beforeInitialObservation = getFsSafeTestHooks()?.beforeRootStatInitialObservation;
|
|
104
|
-
if (beforeInitialObservation)
|
|
105
|
-
await beforeInitialObservation(resolvedPath);
|
|
106
|
-
expected = inspectFileIdentitySync(() => fsSync.lstatSync(resolvedPath, { bigint: true }));
|
|
107
|
-
if (expected.isSymbolicLink()) {
|
|
108
|
-
throw new FsSafeError("path-mismatch", "file changed during operation");
|
|
109
|
-
}
|
|
110
|
-
}
|
|
111
|
-
catch (error) {
|
|
112
|
-
// No initial target error is safe to expose until the admitted parent is
|
|
113
|
-
// proven current; a redirect must win over an ordinary lookup failure.
|
|
114
|
-
await assertRootDirectoryObservationGuard(root, parentGuard);
|
|
115
|
-
throw error;
|
|
116
|
-
}
|
|
117
|
-
admittedTarget = true;
|
|
118
|
-
const beforeObservation = getFsSafeTestHooks()?.beforeRootStatObservation;
|
|
119
|
-
if (beforeObservation)
|
|
120
|
-
await beforeObservation(resolvedPath);
|
|
121
|
-
let observed;
|
|
122
|
-
let sample, samples = 0;
|
|
123
|
-
const comparison = fileObservation();
|
|
124
|
-
try {
|
|
125
|
-
observed = comparison.run(() => inspectFileIdentitySync(() => {
|
|
126
|
-
sample = fsSync.lstatSync(resolvedPath, { bigint: true });
|
|
127
|
-
samples += 1;
|
|
128
|
-
return sample;
|
|
129
|
-
}, expected));
|
|
130
|
-
}
|
|
131
|
-
catch (error) {
|
|
132
|
-
if (isNotFoundPathError(error)) {
|
|
133
|
-
throw await missingObservedFileError(error, resolvedPath, expected, () => assertRootDirectoryObservationGuard(root, parentGuard), proof);
|
|
134
|
-
}
|
|
135
|
-
if (comparison.has(error, "identity"))
|
|
136
|
-
await captureChangedObservedFile(error, resolvedPath, expected, samples === 1 ? sample : undefined, () => assertRootDirectoryObservationGuard(root, parentGuard), proof);
|
|
137
|
-
throw error;
|
|
138
|
-
}
|
|
139
|
-
if (observed.isSymbolicLink()) {
|
|
140
|
-
throw new FsSafeError("path-mismatch", "file changed during operation");
|
|
141
|
-
}
|
|
142
|
-
await assertRootDirectoryObservationGuard(root, parentGuard);
|
|
143
|
-
return pathStatFromStats(observed);
|
|
126
|
+
});
|
|
144
127
|
}
|
|
145
128
|
catch (error) {
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
129
|
+
const failure = proof.failure;
|
|
130
|
+
if (failure && failure.error === error)
|
|
131
|
+
recordFileObservationFailure(error, failure.kind);
|
|
149
132
|
throw error;
|
|
150
133
|
}
|
|
151
134
|
}
|
|
@@ -9,7 +9,7 @@ import { absolutePathWithRawSegments, rawPathRelativeToCanonicalRoot, resolveExi
|
|
|
9
9
|
import { admitPathInsideRoot, } from "./root-boundary.js";
|
|
10
10
|
import { RootPathObservationError, captureRootPathObservedDirectory, createRootPathTraversalObservation, inspectRootPathTraversalEntry, } from "./root-path-observation.js";
|
|
11
11
|
import { assertNoDriveRelativePathSegments } from "./safe-path-segment.js";
|
|
12
|
-
import { assertNoWindowsPathAlias, pathForWindowsFilesystem, resolvePathFromBasePreservingWindowsRoot, resolvePathPreservingWindowsRoot, } from "./windows-path-alias.js";
|
|
12
|
+
import { assertNoWindowsPathAlias, isForeignWindowsShareOrDevicePath, pathForWindowsFilesystem, resolvePathFromBasePreservingWindowsRoot, resolvePathPreservingWindowsRoot, } from "./windows-path-alias.js";
|
|
13
13
|
export { resolvePathViaExistingAncestorSync } from "./root-path-existing.js";
|
|
14
14
|
export { RootPathObservationError, } from "./root-path-observation.js";
|
|
15
15
|
export const ROOT_PATH_ALIAS_POLICIES = {
|
|
@@ -55,6 +55,11 @@ function resolveRootPathInternal(params, options = {}) {
|
|
|
55
55
|
function prepareRootTraversal(params, rootPath, rootCanonicalPath, absolutePath, rawAbsolutePath) {
|
|
56
56
|
let raw = rawAbsolutePath;
|
|
57
57
|
let trustedAbsolutePath = false;
|
|
58
|
+
// Even a metadata lookup on a foreign share contacts that host, including the
|
|
59
|
+
// identity check behind a case-folded root prefix.
|
|
60
|
+
if (isForeignWindowsShareOrDevicePath(raw, [rootPath, rootCanonicalPath])) {
|
|
61
|
+
throw pathEscapeError({ rootPath, absolutePath: raw, boundaryLabel: params.boundaryLabel });
|
|
62
|
+
}
|
|
58
63
|
const direct = admitRawPathInsideRoot(rootPath, raw, params.rootIdentity);
|
|
59
64
|
if (direct) {
|
|
60
65
|
raw = direct.path;
|
|
@@ -1,4 +1,8 @@
|
|
|
1
1
|
import { type ResolvePathWithinRootParams } from "./path-scope-lexical.js";
|
|
2
|
+
export declare function invalidPath(scopeLabel: string): {
|
|
3
|
+
ok: false;
|
|
4
|
+
error: string;
|
|
5
|
+
};
|
|
2
6
|
export declare function resolvePathWithinRoot(params: ResolvePathWithinRootParams): {
|
|
3
7
|
ok: true;
|
|
4
8
|
path: string;
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import path from "node:path";
|
|
2
2
|
import { resolvePathWithinNormalizedRoot } from "./path-scope-lexical.js";
|
|
3
3
|
import { hasWindowsPathAlias, resolvePathPreservingWindowsRoot, } from "./windows-path-alias.js";
|
|
4
|
-
function invalidPath(scopeLabel) {
|
|
4
|
+
export function invalidPath(scopeLabel) {
|
|
5
5
|
return { ok: false, error: `Invalid path: must stay within ${scopeLabel}` };
|
|
6
6
|
}
|
|
7
7
|
export function resolvePathWithinRoot(params) {
|
|
@@ -4,18 +4,12 @@ import { FsSafeError } from "./errors.js";
|
|
|
4
4
|
import { isNotFoundPathError, isPathInside, } from "./path.js";
|
|
5
5
|
import { realpathSync } from "./realpath.js";
|
|
6
6
|
import { root as openRoot } from "./root.js";
|
|
7
|
-
import { hasWindowsPathAlias, pathForWindowsFilesystem, resolvePathPreservingWindowsRoot, } from "./windows-path-alias.js";
|
|
8
|
-
import { resolvePathWithinRoot } from "./root-paths-lexical.js";
|
|
7
|
+
import { hasWindowsPathAlias, isForeignWindowsShareOrDevicePath, pathForWindowsFilesystem, resolvePathPreservingWindowsRoot, } from "./windows-path-alias.js";
|
|
8
|
+
import { invalidPath, resolvePathWithinRoot } from "./root-paths-lexical.js";
|
|
9
9
|
import { resolvePathWithinNormalizedRoot } from "./path-scope-lexical.js";
|
|
10
10
|
import { assertNoSymlinkSegments, ensureDirectoryWithinRoot, resolveNearestExistingPath, } from "./root-directory.js";
|
|
11
11
|
export { resolvePathWithinRoot } from "./root-paths-lexical.js";
|
|
12
12
|
export { ensureDirectoryWithinRoot } from "./root-directory.js";
|
|
13
|
-
function invalidPath(scopeLabel) {
|
|
14
|
-
return {
|
|
15
|
-
ok: false,
|
|
16
|
-
error: `Invalid path: must stay within ${scopeLabel}`,
|
|
17
|
-
};
|
|
18
|
-
}
|
|
19
13
|
const INVALID_REAL_PATH = Symbol("invalid-real-path");
|
|
20
14
|
async function resolveRealPathIfExists(targetPath) {
|
|
21
15
|
if (hasWindowsPathAlias(targetPath, "filesystem"))
|
|
@@ -216,7 +210,8 @@ async function resolveCheckedPathsWithinRoot(params, allowMissingFallback) {
|
|
|
216
210
|
if (!rootRealPath ||
|
|
217
211
|
!raw ||
|
|
218
212
|
!path.isAbsolute(raw) ||
|
|
219
|
-
hasWindowsPathAlias(raw, "filesystem")
|
|
213
|
+
hasWindowsPathAlias(raw, "filesystem") ||
|
|
214
|
+
isForeignWindowsShareOrDevicePath(raw, [rootDir, rootRealPath])) {
|
|
220
215
|
return lexicalPathResult;
|
|
221
216
|
}
|
|
222
217
|
try {
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import { type RootContext } from "./root-context.js";
|
|
2
|
+
import { type InternalRemoveOptions } from "./root-remove.js";
|
|
3
|
+
import type { RemovalPathReceipts } from "./root-remove-receipt.js";
|
|
4
|
+
export declare function removePathInRootNative(root: RootContext, target: string, options: InternalRemoveOptions, receipts?: RemovalPathReceipts): Promise<void>;
|
|
@@ -0,0 +1,311 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { scheduler } from "node:timers/promises";
|
|
4
|
+
import { assertSyncDirectoryGuard } from "./directory-guard.js";
|
|
5
|
+
import { assertMutationNotDenied } from "./deny-mutations.js";
|
|
6
|
+
import { FsSafeError } from "./errors.js";
|
|
7
|
+
import { MutationAuthorityError } from "./mutation-authority.js";
|
|
8
|
+
import { getNativeBinding } from "./native.js";
|
|
9
|
+
import { openNativeParentAdmission, openNativeRootAdmission } from "./native-parent-admission.js";
|
|
10
|
+
import { isNotFoundPathError, isPathInside } from "./path.js";
|
|
11
|
+
import { assertRootIdentityCurrentSync } from "./root-context.js";
|
|
12
|
+
import { normalizeRemoveGuardError, normalizeRemovePathError } from "./root-errors.js";
|
|
13
|
+
import { captureNonrecursiveRemovalAdmission, nonrecursiveRemovalKind } from "./root-remove.js";
|
|
14
|
+
import { createSuppressedError } from "./suppressed-error.js";
|
|
15
|
+
import { getFsSafeTestHooks } from "./test-hooks.js";
|
|
16
|
+
function unavailable() {
|
|
17
|
+
throw new FsSafeError("helper-unavailable", "native confined removal is unavailable on this platform");
|
|
18
|
+
}
|
|
19
|
+
function normalize(error, details) {
|
|
20
|
+
if (error instanceof FsSafeError || error instanceof MutationAuthorityError ||
|
|
21
|
+
(error instanceof Error && error.name === "SuppressedError"))
|
|
22
|
+
return error;
|
|
23
|
+
const code = error?.code;
|
|
24
|
+
if (code === "path-mismatch")
|
|
25
|
+
return new FsSafeError("path-mismatch", "removal identity changed", { cause: error, details });
|
|
26
|
+
if (["ENOTSUP", "ENOSYS", "EOPNOTSUPP"].includes(code ?? "")) {
|
|
27
|
+
return new FsSafeError("helper-unavailable", "native confined removal is unavailable", { cause: error, details });
|
|
28
|
+
}
|
|
29
|
+
return normalizeRemovePathError(error, details);
|
|
30
|
+
}
|
|
31
|
+
export async function removePathInRootNative(root, target, options, receipts) {
|
|
32
|
+
const binding = getNativeBinding();
|
|
33
|
+
if (!binding?.rootRemovalStat || !binding.rootRemovalUnlink) {
|
|
34
|
+
unavailable();
|
|
35
|
+
}
|
|
36
|
+
const inspect = binding.rootRemovalStat.bind(binding);
|
|
37
|
+
const unlink = binding.rootRemovalUnlink.bind(binding);
|
|
38
|
+
const openDirectory = binding.openRootRemovalDirectory?.bind(binding);
|
|
39
|
+
const boundary = await captureNonrecursiveRemovalAdmission(root, target, options, receipts);
|
|
40
|
+
if (!boundary)
|
|
41
|
+
return;
|
|
42
|
+
const rootAdmission = await openNativeRootAdmission(binding, {
|
|
43
|
+
rootPath: root.rootReal, rootIdentity: root.rootIdentity, operation: "remove", reportCloseErrors: true, searchOnly: true,
|
|
44
|
+
}).catch(error => { throw normalizeRemoveGuardError(error); });
|
|
45
|
+
let parent;
|
|
46
|
+
let initialEntry;
|
|
47
|
+
let initialDirectory;
|
|
48
|
+
let initialDirectoryAttempted = false;
|
|
49
|
+
let operationError;
|
|
50
|
+
let failed = false;
|
|
51
|
+
try {
|
|
52
|
+
const parentPath = path.dirname(target);
|
|
53
|
+
if (!isPathInside(root.rootReal, parentPath))
|
|
54
|
+
throw new FsSafeError("outside-workspace", "removal parent is outside root");
|
|
55
|
+
try {
|
|
56
|
+
parent = await openNativeParentAdmission(binding, rootAdmission, path.relative(root.rootReal, parentPath).split(path.sep).join("/"));
|
|
57
|
+
}
|
|
58
|
+
catch (error) {
|
|
59
|
+
assertRootIdentityCurrentSync(root);
|
|
60
|
+
boundary.assertCurrent();
|
|
61
|
+
if (!(options.force && isNotFoundPathError(error)))
|
|
62
|
+
throw error;
|
|
63
|
+
}
|
|
64
|
+
if (parent) {
|
|
65
|
+
// Bind the native descriptor to the parent admitted before opening it;
|
|
66
|
+
// an in-root redirect must not inherit another directory's approval.
|
|
67
|
+
if (parent.guard.stat.dev !== boundary.parentIdentity.dev || parent.guard.stat.ino !== boundary.parentIdentity.ino) {
|
|
68
|
+
throw new FsSafeError("path-mismatch", "removal parent identity changed");
|
|
69
|
+
}
|
|
70
|
+
if (options.recursive) {
|
|
71
|
+
boundary.assertCurrent();
|
|
72
|
+
try {
|
|
73
|
+
initialEntry = inspect(parent.fd, path.basename(target));
|
|
74
|
+
}
|
|
75
|
+
catch (error) {
|
|
76
|
+
boundary.assertCurrent();
|
|
77
|
+
if (!(options.force && isNotFoundPathError(error)))
|
|
78
|
+
throw error;
|
|
79
|
+
}
|
|
80
|
+
if (initialEntry?.directory) {
|
|
81
|
+
if (!openDirectory)
|
|
82
|
+
unavailable();
|
|
83
|
+
initialDirectoryAttempted = true;
|
|
84
|
+
try {
|
|
85
|
+
initialDirectory = openDirectory(parent.fd, path.basename(target), initialEntry.dev, initialEntry.ino);
|
|
86
|
+
}
|
|
87
|
+
catch (error) {
|
|
88
|
+
boundary.assertCurrent();
|
|
89
|
+
const code = error?.code;
|
|
90
|
+
if (["ENOTSUP", "ENOSYS", "EOPNOTSUPP"].includes(code ?? ""))
|
|
91
|
+
unavailable();
|
|
92
|
+
else if (!(options.force && isNotFoundPathError(error))) {
|
|
93
|
+
throw normalize(error, { operation: "remove", phase: "enumerate", relativePath: "" });
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
if (!options.recursive || initialEntry)
|
|
99
|
+
await removeEntries(parent);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
catch (error) {
|
|
103
|
+
failed = true;
|
|
104
|
+
operationError = error instanceof MutationAuthorityError ? error : normalize(error);
|
|
105
|
+
}
|
|
106
|
+
const closeErrors = [];
|
|
107
|
+
try {
|
|
108
|
+
initialDirectory?.close();
|
|
109
|
+
}
|
|
110
|
+
catch (error) {
|
|
111
|
+
closeErrors.push(error);
|
|
112
|
+
}
|
|
113
|
+
try {
|
|
114
|
+
parent?.close();
|
|
115
|
+
}
|
|
116
|
+
catch (error) {
|
|
117
|
+
closeErrors.push(error);
|
|
118
|
+
}
|
|
119
|
+
try {
|
|
120
|
+
await rootAdmission.root.close();
|
|
121
|
+
}
|
|
122
|
+
catch (error) {
|
|
123
|
+
closeErrors.push(error);
|
|
124
|
+
}
|
|
125
|
+
if (closeErrors.length) {
|
|
126
|
+
const error = closeErrors.length === 1 ? closeErrors[0] : new AggregateError(closeErrors, "removal descriptors could not close");
|
|
127
|
+
if (failed)
|
|
128
|
+
throw createSuppressedError(error, operationError instanceof MutationAuthorityError ? operationError.rejection : operationError, "removal and descriptor close failed");
|
|
129
|
+
throw error;
|
|
130
|
+
}
|
|
131
|
+
if (failed)
|
|
132
|
+
throw operationError;
|
|
133
|
+
async function removeEntries(admitted) {
|
|
134
|
+
// The admission already checks Root through the retained parent. This list
|
|
135
|
+
// adds only traversal descendants, avoiding duplicate ancestry fences.
|
|
136
|
+
const guards = [];
|
|
137
|
+
const maxEntries = options.maxEntries ?? 100_000;
|
|
138
|
+
const maxDepth = options.maxDepth ?? 64;
|
|
139
|
+
let examined = 0;
|
|
140
|
+
const details = (entryPath, phase) => ({
|
|
141
|
+
operation: "remove", relativePath: path.relative(target, entryPath), phase,
|
|
142
|
+
});
|
|
143
|
+
async function beforeMutation(entryPath) {
|
|
144
|
+
try {
|
|
145
|
+
await getFsSafeTestHooks()?.beforeRootFallbackMutation?.("remove", entryPath);
|
|
146
|
+
}
|
|
147
|
+
catch (error) {
|
|
148
|
+
throw normalizeRemoveGuardError(error);
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
function assertNotAborted() {
|
|
152
|
+
try {
|
|
153
|
+
options.signal?.throwIfAborted();
|
|
154
|
+
}
|
|
155
|
+
catch (error) {
|
|
156
|
+
throw new MutationAuthorityError(error);
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
function assertTraversalCurrent() {
|
|
160
|
+
for (const guard of guards) {
|
|
161
|
+
try {
|
|
162
|
+
assertSyncDirectoryGuard(guard);
|
|
163
|
+
}
|
|
164
|
+
catch (cause) {
|
|
165
|
+
throw new FsSafeError("path-mismatch", "removal ancestor changed", { cause });
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
function assertCurrent() {
|
|
170
|
+
assertNotAborted();
|
|
171
|
+
boundary.assertCurrent();
|
|
172
|
+
assertTraversalCurrent();
|
|
173
|
+
}
|
|
174
|
+
function observe(fd, name, entryPath, expected) {
|
|
175
|
+
assertCurrent();
|
|
176
|
+
try {
|
|
177
|
+
const entry = inspect(fd, name);
|
|
178
|
+
if (expected && (entry.dev !== expected.dev || entry.ino !== expected.ino || entry.directory !== expected.directory)) {
|
|
179
|
+
throw new FsSafeError("path-mismatch", "removal entry changed");
|
|
180
|
+
}
|
|
181
|
+
if (entry.symlink && options.mutationSymlinks !== undefined) {
|
|
182
|
+
throw new FsSafeError("symlink", "symlink not allowed", { details: details(entryPath, expected ? "remove" : "inspect") });
|
|
183
|
+
}
|
|
184
|
+
return entry;
|
|
185
|
+
}
|
|
186
|
+
catch (error) {
|
|
187
|
+
if (options.force && isNotFoundPathError(error))
|
|
188
|
+
return undefined;
|
|
189
|
+
throw normalize(error, details(entryPath, expected ? "remove" : "inspect"));
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
async function visit(fd, name, entryPath, depth, counted = false) {
|
|
193
|
+
assertNotAborted();
|
|
194
|
+
if (options.recursive && ((!counted && examined >= maxEntries) || depth > maxDepth)) {
|
|
195
|
+
throw new FsSafeError("too-large", "recursive removal budget exceeded", { details: details(entryPath, "inspect") });
|
|
196
|
+
}
|
|
197
|
+
if (!counted)
|
|
198
|
+
examined++;
|
|
199
|
+
if (!options.recursive)
|
|
200
|
+
await beforeMutation(entryPath);
|
|
201
|
+
await assertMutationNotDenied(entryPath, options.denyMutations, { protectAncestors: true });
|
|
202
|
+
const initial = observe(fd, name, entryPath, depth === 0 ? initialEntry : undefined);
|
|
203
|
+
if (!initial)
|
|
204
|
+
return;
|
|
205
|
+
if (!initial.directory && options[nonrecursiveRemovalKind] === "directory") {
|
|
206
|
+
throw new FsSafeError("path-mismatch", "store entry is no longer a directory");
|
|
207
|
+
}
|
|
208
|
+
if (initial.directory && options.recursive) {
|
|
209
|
+
let directory;
|
|
210
|
+
try {
|
|
211
|
+
if (depth === 0 && initialDirectoryAttempted) {
|
|
212
|
+
directory = initialDirectory;
|
|
213
|
+
initialDirectory = undefined;
|
|
214
|
+
}
|
|
215
|
+
else
|
|
216
|
+
directory = openDirectory(fd, name, initial.dev, initial.ino);
|
|
217
|
+
}
|
|
218
|
+
catch (error) {
|
|
219
|
+
assertCurrent();
|
|
220
|
+
if (!(options.force && isNotFoundPathError(error)))
|
|
221
|
+
throw normalize(error, details(entryPath, "enumerate"));
|
|
222
|
+
if (!observe(fd, name, entryPath, initial))
|
|
223
|
+
return;
|
|
224
|
+
}
|
|
225
|
+
if (directory) {
|
|
226
|
+
let failure;
|
|
227
|
+
try {
|
|
228
|
+
const stat = fs.fstatSync(directory.fd, { bigint: true });
|
|
229
|
+
const read = () => {
|
|
230
|
+
try {
|
|
231
|
+
return directory.read();
|
|
232
|
+
}
|
|
233
|
+
catch (error) {
|
|
234
|
+
throw normalize(error, details(entryPath, "enumerate"));
|
|
235
|
+
}
|
|
236
|
+
};
|
|
237
|
+
guards.push({ dir: entryPath, realPath: entryPath, stat });
|
|
238
|
+
try {
|
|
239
|
+
assertCurrent();
|
|
240
|
+
if (options.order === "sorted") {
|
|
241
|
+
const names = [];
|
|
242
|
+
for (let child = read(); child !== null; child = read()) {
|
|
243
|
+
await scheduler.yield();
|
|
244
|
+
assertCurrent();
|
|
245
|
+
if (examined >= maxEntries)
|
|
246
|
+
throw new FsSafeError("too-large", "recursive removal budget exceeded", {
|
|
247
|
+
details: details(path.join(entryPath, child), "enumerate"),
|
|
248
|
+
});
|
|
249
|
+
examined++;
|
|
250
|
+
names.push(child);
|
|
251
|
+
}
|
|
252
|
+
for (const child of names.sort())
|
|
253
|
+
await visit(directory.fd, child, path.join(entryPath, child), depth + 1, true);
|
|
254
|
+
}
|
|
255
|
+
else {
|
|
256
|
+
for (let child = read(); child !== null; child = read()) {
|
|
257
|
+
await scheduler.yield();
|
|
258
|
+
await visit(directory.fd, child, path.join(entryPath, child), depth + 1);
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
finally {
|
|
263
|
+
guards.pop();
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
catch (error) {
|
|
267
|
+
failure = { error: error instanceof MutationAuthorityError ? error.rejection : error };
|
|
268
|
+
throw error;
|
|
269
|
+
}
|
|
270
|
+
finally {
|
|
271
|
+
try {
|
|
272
|
+
directory.close();
|
|
273
|
+
}
|
|
274
|
+
catch (error) {
|
|
275
|
+
const closeError = normalize(error, details(entryPath, "enumerate"));
|
|
276
|
+
if (failure)
|
|
277
|
+
throw createSuppressedError(closeError, failure.error, "removal and directory close failed");
|
|
278
|
+
throw closeError;
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
if (options.recursive) {
|
|
284
|
+
await beforeMutation(entryPath);
|
|
285
|
+
await assertMutationNotDenied(entryPath, options.denyMutations, { protectAncestors: true });
|
|
286
|
+
if (!observe(fd, name, entryPath, initial))
|
|
287
|
+
return;
|
|
288
|
+
}
|
|
289
|
+
if (options.assertBeforeMutation) {
|
|
290
|
+
options.assertBeforeMutation();
|
|
291
|
+
if (!observe(fd, name, entryPath, initial))
|
|
292
|
+
return;
|
|
293
|
+
}
|
|
294
|
+
// No await separates the final admission from native identity-checked unlink.
|
|
295
|
+
try {
|
|
296
|
+
unlink(fd, name, initial.dev, initial.ino, initial.directory);
|
|
297
|
+
}
|
|
298
|
+
catch (error) {
|
|
299
|
+
if (!(options.force && isNotFoundPathError(error)))
|
|
300
|
+
throw normalize(error, details(entryPath, "remove"));
|
|
301
|
+
}
|
|
302
|
+
if (options.recursive)
|
|
303
|
+
boundary.assertCurrent();
|
|
304
|
+
else
|
|
305
|
+
boundary.assertAfterMutation();
|
|
306
|
+
assertTraversalCurrent();
|
|
307
|
+
assertNotAborted();
|
|
308
|
+
}
|
|
309
|
+
await visit(admitted.fd, path.basename(target), target, 0);
|
|
310
|
+
}
|
|
311
|
+
}
|
|
@@ -1,6 +1,17 @@
|
|
|
1
|
+
import type { BigIntStats } from "node:fs";
|
|
1
2
|
import { type RootContext } from "./root-context.js";
|
|
2
3
|
import type { RootRemoveOptions } from "./root-options.js";
|
|
3
4
|
import type { RemovalPathReceipts } from "./root-remove-receipt.js";
|
|
4
5
|
export declare const nonrecursiveRemovalKind: unique symbol;
|
|
6
|
+
export type InternalRemoveOptions = RootRemoveOptions & {
|
|
7
|
+
[nonrecursiveRemovalKind]?: "directory";
|
|
8
|
+
};
|
|
5
9
|
export declare function validateRemoveOptions(options: RootRemoveOptions): void;
|
|
10
|
+
type NonrecursiveRemovalAdmission = Readonly<{
|
|
11
|
+
parentIdentity: Readonly<Pick<BigIntStats, "dev" | "ino">>;
|
|
12
|
+
assertAfterMutation(): void;
|
|
13
|
+
assertCurrent(): void;
|
|
14
|
+
}>;
|
|
15
|
+
export declare function captureNonrecursiveRemovalAdmission(root: RootContext, targetPath: string, options: RootRemoveOptions, receipts?: RemovalPathReceipts): Promise<NonrecursiveRemovalAdmission | undefined>;
|
|
6
16
|
export declare function removePathInRootFallback(root: RootContext, targetPath: string, options: RootRemoveOptions, receipts?: RemovalPathReceipts): Promise<void>;
|
|
17
|
+
export {};
|
|
@@ -80,7 +80,7 @@ async function assertMissingRemovalPrefixCurrent(rootGuard, intermediates) {
|
|
|
80
80
|
throw removalAncestorChanged();
|
|
81
81
|
}
|
|
82
82
|
}
|
|
83
|
-
async function captureNonrecursiveRemovalAdmission(root, targetPath, options, receipts) {
|
|
83
|
+
export async function captureNonrecursiveRemovalAdmission(root, targetPath, options, receipts) {
|
|
84
84
|
const parentPath = path.dirname(targetPath);
|
|
85
85
|
if (!isPathInside(root.rootReal, parentPath)) {
|
|
86
86
|
throw new FsSafeError("path-mismatch", "removal parent is outside the retained root");
|
|
@@ -105,6 +105,7 @@ async function captureNonrecursiveRemovalAdmission(root, targetPath, options, re
|
|
|
105
105
|
if (parentPath === root.rootReal) {
|
|
106
106
|
const rootAssertion = createRemovalDirectoryAssertion(rootGuard.dir, rootGuard.stat, rootGuard.realPath);
|
|
107
107
|
return Object.freeze({
|
|
108
|
+
parentIdentity: rootAssertion,
|
|
108
109
|
assertAfterMutation() {
|
|
109
110
|
// Here Root is also the immediate parent, so retain its established
|
|
110
111
|
// post-dispatch error classification while checking it only once.
|
|
@@ -187,6 +188,7 @@ async function captureNonrecursiveRemovalAdmission(root, targetPath, options, re
|
|
|
187
188
|
}
|
|
188
189
|
};
|
|
189
190
|
return Object.freeze({
|
|
191
|
+
parentIdentity: parentAssertion,
|
|
190
192
|
assertAfterMutation() {
|
|
191
193
|
assertPrefixCurrent();
|
|
192
194
|
// Preserve the established nonrecursive post-dispatch mapping for the
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { DirectoryObservationGuard } from "./directory-guard.js";
|
|
2
2
|
import { type RootPathObservationReceipt } from "./root-path.js";
|
|
3
|
-
import type { RootDirectoryListing, RootDirectoryListingOptions } from "./root-directory-list.js";
|
|
3
|
+
import type { RootDirectoryListing, RootDirectoryListingOptions } from "./root-directory-list-types.js";
|
|
4
4
|
import type { PathStat } from "./types.js";
|
|
5
5
|
export type RootWalkSymlinkPolicy = "skip" | "follow-within-root" | "include";
|
|
6
6
|
type LegacyRootWalkSymlinkPolicy = Exclude<RootWalkSymlinkPolicy, "include">;
|
|
@@ -40,7 +40,7 @@ type AnyRootWalkOptions = RootWalkOptions | RootWalkOptions<"include"> | RootWal
|
|
|
40
40
|
type AnyRootWalkEntry = RootWalkEntry<RootWalkSymlinkPolicy>;
|
|
41
41
|
type RootWalkCapability = {
|
|
42
42
|
rootReal: string;
|
|
43
|
-
observeRoot(): Promise<DirectoryObservationGuard
|
|
43
|
+
observeRoot(): Promise<DirectoryObservationGuard>;
|
|
44
44
|
stat(relativePath: string): Promise<PathStat>;
|
|
45
45
|
list(relativePath: string, options: RootDirectoryListingOptions, receipt?: RootPathObservationReceipt): Promise<RootDirectoryListing>;
|
|
46
46
|
};
|
|
@@ -87,9 +87,6 @@ export async function* walkRoot(root, relativePath, options) {
|
|
|
87
87
|
let resolvedDirectory;
|
|
88
88
|
if (includeChild) {
|
|
89
89
|
const rootGuard = await root.observeRoot();
|
|
90
|
-
if (!rootGuard) {
|
|
91
|
-
throw new FsSafeError("helper-unavailable", "root walk requires an exact directory observation");
|
|
92
|
-
}
|
|
93
90
|
const observed = await resolveRootPathWithObservation({
|
|
94
91
|
...resolution,
|
|
95
92
|
rootIdentity: rootGuard.identity,
|
|
@@ -192,6 +189,7 @@ export async function* walkRoot(root, relativePath, options) {
|
|
|
192
189
|
return;
|
|
193
190
|
}
|
|
194
191
|
}
|
|
192
|
+
options.signal?.throwIfAborted();
|
|
195
193
|
if (!["include", "skip", "skip-subtree"].includes(filterResult)) {
|
|
196
194
|
throw new TypeError(`invalid root walk entryFilter result: ${String(filterResult)}`);
|
|
197
195
|
}
|