@openclaw/openshell-sandbox 2026.9.6 → 2026.9.8
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/assets/icon.png +0 -0
- package/dist/index.js +75 -229
- package/node_modules/@openclaw/fs-safe/CHANGELOG.md +102 -0
- package/node_modules/@openclaw/fs-safe/README.md +24 -6
- package/node_modules/@openclaw/fs-safe/dist/advanced.d.ts +6 -1
- package/node_modules/@openclaw/fs-safe/dist/advanced.js +3 -1
- package/node_modules/@openclaw/fs-safe/dist/archive-durability.js +1 -1
- package/node_modules/@openclaw/fs-safe/dist/archive-merge.js +1 -2
- package/node_modules/@openclaw/fs-safe/dist/archive-plan.d.ts +2 -7
- package/node_modules/@openclaw/fs-safe/dist/archive-read.js +9 -18
- package/node_modules/@openclaw/fs-safe/dist/archive-staging.d.ts +1 -2
- package/node_modules/@openclaw/fs-safe/dist/archive-staging.js +3 -3
- package/node_modules/@openclaw/fs-safe/dist/archive-zip-directory.js +4 -0
- package/node_modules/@openclaw/fs-safe/dist/archive-zip-entry.d.ts +11 -11
- package/node_modules/@openclaw/fs-safe/dist/archive-zip-entry.js +3 -35
- package/node_modules/@openclaw/fs-safe/dist/archive-zip-integrity.d.ts +2 -2
- package/node_modules/@openclaw/fs-safe/dist/archive-zip-integrity.js +2 -12
- package/node_modules/@openclaw/fs-safe/dist/archive-zip-loader.d.ts +7 -3
- package/node_modules/@openclaw/fs-safe/dist/archive-zip-loader.js +13 -10
- package/node_modules/@openclaw/fs-safe/dist/archive-zip-manifest.js +3 -1
- package/node_modules/@openclaw/fs-safe/dist/archive-zip-preflight.d.ts +2 -3
- package/node_modules/@openclaw/fs-safe/dist/archive-zip-preflight.js +16 -8
- package/node_modules/@openclaw/fs-safe/dist/archive.d.ts +6 -3
- package/node_modules/@openclaw/fs-safe/dist/archive.js +22 -19
- package/node_modules/@openclaw/fs-safe/dist/atomic.d.ts +1 -1
- package/node_modules/@openclaw/fs-safe/dist/copy-publication.d.ts +1 -3
- package/node_modules/@openclaw/fs-safe/dist/copy-publication.js +2 -6
- package/node_modules/@openclaw/fs-safe/dist/deny-mutation-match.d.ts +2 -0
- package/node_modules/@openclaw/fs-safe/dist/deny-mutation-match.js +71 -0
- package/node_modules/@openclaw/fs-safe/dist/deny-mutations.js +4 -3
- package/node_modules/@openclaw/fs-safe/dist/directory-guard.d.ts +0 -1
- package/node_modules/@openclaw/fs-safe/dist/directory-guard.js +0 -3
- package/node_modules/@openclaw/fs-safe/dist/directory-mode-node.d.ts +20 -2
- package/node_modules/@openclaw/fs-safe/dist/directory-mode-node.js +77 -1
- package/node_modules/@openclaw/fs-safe/dist/directory-receipt.js +5 -7
- package/node_modules/@openclaw/fs-safe/dist/effective-uid.js +1 -4
- package/node_modules/@openclaw/fs-safe/dist/errors.d.ts +3 -1
- package/node_modules/@openclaw/fs-safe/dist/errors.js +3 -2
- package/node_modules/@openclaw/fs-safe/dist/file-identity.js +10 -2
- package/node_modules/@openclaw/fs-safe/dist/file-lock-sync-acquisition.js +1 -1
- package/node_modules/@openclaw/fs-safe/dist/file-lock-sync-admission.js +5 -1
- package/node_modules/@openclaw/fs-safe/dist/file-lock-sync-root-held.d.ts +4 -11
- package/node_modules/@openclaw/fs-safe/dist/file-lock-sync-root-held.js +1 -4
- package/node_modules/@openclaw/fs-safe/dist/file-lock-sync-root-io.d.ts +3 -6
- package/node_modules/@openclaw/fs-safe/dist/file-lock-sync-root-io.js +1 -1
- package/node_modules/@openclaw/fs-safe/dist/file-lock-sync-root-options.d.ts +2 -10
- package/node_modules/@openclaw/fs-safe/dist/file-lock-sync-root.d.ts +2 -4
- package/node_modules/@openclaw/fs-safe/dist/file-lock-sync.js +2 -2
- package/node_modules/@openclaw/fs-safe/dist/file-store-boundary.d.ts +3 -7
- package/node_modules/@openclaw/fs-safe/dist/file-store-boundary.js +7 -10
- package/node_modules/@openclaw/fs-safe/dist/file-store-prune.js +7 -6
- package/node_modules/@openclaw/fs-safe/dist/file-store.d.ts +4 -7
- package/node_modules/@openclaw/fs-safe/dist/file-store.js +19 -21
- package/node_modules/@openclaw/fs-safe/dist/guest-native-python.js +23 -31
- package/node_modules/@openclaw/fs-safe/dist/guest.js +21 -12
- package/node_modules/@openclaw/fs-safe/dist/json-document-store.d.ts +4 -9
- package/node_modules/@openclaw/fs-safe/dist/json-durable-queue-ownership.d.ts +1 -5
- package/node_modules/@openclaw/fs-safe/dist/json-durable-queue.d.ts +0 -1
- package/node_modules/@openclaw/fs-safe/dist/json-durable-queue.js +2 -7
- package/node_modules/@openclaw/fs-safe/dist/local-file-access.js +2 -5
- package/node_modules/@openclaw/fs-safe/dist/local-file-descriptor.d.ts +2 -5
- package/node_modules/@openclaw/fs-safe/dist/local-roots.d.ts +2 -7
- package/node_modules/@openclaw/fs-safe/dist/move-path-cleanup.js +4 -4
- package/node_modules/@openclaw/fs-safe/dist/mutation-authority.js +5 -0
- package/node_modules/@openclaw/fs-safe/dist/native-binding.d.ts +51 -14
- package/node_modules/@openclaw/fs-safe/dist/native-pinned-write.js +8 -2
- package/node_modules/@openclaw/fs-safe/dist/native-staged-symlink.d.ts +13 -0
- package/node_modules/@openclaw/fs-safe/dist/native-staged-symlink.js +303 -0
- package/node_modules/@openclaw/fs-safe/dist/owner-dacl-batch-worker.d.ts +1 -0
- package/node_modules/@openclaw/fs-safe/dist/owner-dacl-batch-worker.js +54 -0
- package/node_modules/@openclaw/fs-safe/dist/owner-dacl-batch.d.ts +5 -0
- package/node_modules/@openclaw/fs-safe/dist/owner-dacl-batch.js +64 -0
- package/node_modules/@openclaw/fs-safe/dist/owner-dacl.d.ts +2 -0
- package/node_modules/@openclaw/fs-safe/dist/owner-dacl.js +3 -0
- package/node_modules/@openclaw/fs-safe/dist/path.js +17 -1
- package/node_modules/@openclaw/fs-safe/dist/permission-exec.d.ts +6 -0
- package/node_modules/@openclaw/fs-safe/dist/permission-exec.js +3 -6
- package/node_modules/@openclaw/fs-safe/dist/permissions-public.d.ts +3 -1
- package/node_modules/@openclaw/fs-safe/dist/permissions-public.js +1 -0
- package/node_modules/@openclaw/fs-safe/dist/permissions-windows.d.ts +3 -6
- package/node_modules/@openclaw/fs-safe/dist/permissions.d.ts +3 -10
- package/node_modules/@openclaw/fs-safe/dist/permissions.js +0 -1
- package/node_modules/@openclaw/fs-safe/dist/pinned-mutation-admission.d.ts +0 -1
- package/node_modules/@openclaw/fs-safe/dist/pinned-mutation-admission.js +4 -2
- package/node_modules/@openclaw/fs-safe/dist/pinned-mutation-shared-route.d.ts +2 -8
- package/node_modules/@openclaw/fs-safe/dist/pinned-open.d.ts +1 -1
- package/node_modules/@openclaw/fs-safe/dist/pinned-open.js +2 -3
- package/node_modules/@openclaw/fs-safe/dist/publish-copy-stage.js +4 -0
- package/node_modules/@openclaw/fs-safe/dist/read-opened-file.d.ts +2 -5
- package/node_modules/@openclaw/fs-safe/dist/regular-file.js +3 -3
- package/node_modules/@openclaw/fs-safe/dist/replace-directory.js +5 -17
- package/node_modules/@openclaw/fs-safe/dist/replace-file-buffer.d.ts +4 -0
- package/node_modules/@openclaw/fs-safe/dist/replace-file-buffer.js +36 -0
- package/node_modules/@openclaw/fs-safe/dist/replace-file-copy-fallback.d.ts +4 -10
- package/node_modules/@openclaw/fs-safe/dist/replace-file-copy-fallback.js +66 -38
- package/node_modules/@openclaw/fs-safe/dist/replace-file-descriptor.d.ts +7 -11
- package/node_modules/@openclaw/fs-safe/dist/replace-file-descriptor.js +10 -2
- package/node_modules/@openclaw/fs-safe/dist/replace-file-destination.d.ts +21 -0
- package/node_modules/@openclaw/fs-safe/dist/replace-file-destination.js +123 -0
- package/node_modules/@openclaw/fs-safe/dist/replace-file-mutation.d.ts +26 -0
- package/node_modules/@openclaw/fs-safe/dist/replace-file-mutation.js +47 -0
- package/node_modules/@openclaw/fs-safe/dist/replace-file-temp-owner.d.ts +2 -2
- package/node_modules/@openclaw/fs-safe/dist/replace-file-temp-owner.js +16 -4
- package/node_modules/@openclaw/fs-safe/dist/replace-file-types.d.ts +55 -0
- package/node_modules/@openclaw/fs-safe/dist/replace-file-types.js +1 -0
- package/node_modules/@openclaw/fs-safe/dist/replace-file.d.ts +3 -55
- package/node_modules/@openclaw/fs-safe/dist/replace-file.js +31 -11
- package/node_modules/@openclaw/fs-safe/dist/retained-directory-replacement.d.ts +1 -0
- package/node_modules/@openclaw/fs-safe/dist/retained-directory-replacement.js +1 -1
- package/node_modules/@openclaw/fs-safe/dist/retained-file-types.d.ts +50 -0
- package/node_modules/@openclaw/fs-safe/dist/retained-file-types.js +1 -0
- package/node_modules/@openclaw/fs-safe/dist/retained-file.d.ts +3 -0
- package/node_modules/@openclaw/fs-safe/dist/retained-file.js +121 -0
- package/node_modules/@openclaw/fs-safe/dist/root-context.js +3 -2
- package/node_modules/@openclaw/fs-safe/dist/root-directory-entry.d.ts +9 -0
- package/node_modules/@openclaw/fs-safe/dist/root-directory-entry.js +28 -0
- package/node_modules/@openclaw/fs-safe/dist/root-directory-list.d.ts +9 -1
- package/node_modules/@openclaw/fs-safe/dist/root-directory-list.js +88 -37
- package/node_modules/@openclaw/fs-safe/dist/root-file-final-admission.d.ts +1 -1
- package/node_modules/@openclaw/fs-safe/dist/root-file-final-admission.js +1 -6
- package/node_modules/@openclaw/fs-safe/dist/root-file.js +2 -5
- package/node_modules/@openclaw/fs-safe/dist/root-handle-context.d.ts +4 -0
- package/node_modules/@openclaw/fs-safe/dist/root-handle-context.js +12 -0
- package/node_modules/@openclaw/fs-safe/dist/root-impl.d.ts +3 -3
- package/node_modules/@openclaw/fs-safe/dist/root-impl.js +8 -5
- package/node_modules/@openclaw/fs-safe/dist/root-move-noreplace.d.ts +2 -7
- package/node_modules/@openclaw/fs-safe/dist/root-move-noreplace.js +3 -3
- package/node_modules/@openclaw/fs-safe/dist/root-observed-path.d.ts +0 -1
- package/node_modules/@openclaw/fs-safe/dist/root-observed-path.js +0 -3
- package/node_modules/@openclaw/fs-safe/dist/root-path-observation.d.ts +4 -11
- package/node_modules/@openclaw/fs-safe/dist/root-path.js +7 -10
- package/node_modules/@openclaw/fs-safe/dist/root-paths.d.ts +2 -6
- package/node_modules/@openclaw/fs-safe/dist/root-remove-identity.d.ts +1 -3
- package/node_modules/@openclaw/fs-safe/dist/root-walk.d.ts +19 -12
- package/node_modules/@openclaw/fs-safe/dist/root-walk.js +56 -20
- package/node_modules/@openclaw/fs-safe/dist/root-write-admission.js +1 -6
- package/node_modules/@openclaw/fs-safe/dist/root-write-complete-parent.d.ts +2 -0
- package/node_modules/@openclaw/fs-safe/dist/root-write-complete-parent.js +1 -1
- package/node_modules/@openclaw/fs-safe/dist/safe-path-segment.d.ts +1 -0
- package/node_modules/@openclaw/fs-safe/dist/safe-path-segment.js +8 -2
- package/node_modules/@openclaw/fs-safe/dist/secret-file.d.ts +6 -2
- package/node_modules/@openclaw/fs-safe/dist/secret-file.js +12 -16
- package/node_modules/@openclaw/fs-safe/dist/secret-read-policy.js +1 -1
- package/node_modules/@openclaw/fs-safe/dist/secure-file-windows.js +5 -14
- package/node_modules/@openclaw/fs-safe/dist/secure-file.js +3 -2
- package/node_modules/@openclaw/fs-safe/dist/secure-temp-dir.js +2 -7
- package/node_modules/@openclaw/fs-safe/dist/sidecar-lock-acquire.js +4 -4
- package/node_modules/@openclaw/fs-safe/dist/sidecar-lock-admission-parser.d.ts +1 -2
- package/node_modules/@openclaw/fs-safe/dist/sidecar-lock-handle.d.ts +2 -8
- package/node_modules/@openclaw/fs-safe/dist/sidecar-lock-policy.d.ts +2 -7
- package/node_modules/@openclaw/fs-safe/dist/sidecar-lock-reclaim.d.ts +2 -2
- package/node_modules/@openclaw/fs-safe/dist/sidecar-lock-reclaim.js +11 -12
- package/node_modules/@openclaw/fs-safe/dist/sidecar-lock-stale-admission.d.ts +1 -5
- package/node_modules/@openclaw/fs-safe/dist/sidecar-lock.js +9 -7
- package/node_modules/@openclaw/fs-safe/dist/staged-symlink-types.d.ts +39 -0
- package/node_modules/@openclaw/fs-safe/dist/staged-symlink-types.js +1 -0
- package/node_modules/@openclaw/fs-safe/dist/stat-observation.js +3 -10
- package/node_modules/@openclaw/fs-safe/dist/strict-file-identity.d.ts +2 -0
- package/node_modules/@openclaw/fs-safe/dist/strict-file-identity.js +6 -6
- package/node_modules/@openclaw/fs-safe/dist/symlink-parents.js +58 -7
- package/node_modules/@openclaw/fs-safe/dist/temp-target.js +8 -10
- package/node_modules/@openclaw/fs-safe/dist/temp-workspace-admission.js +28 -33
- 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 +16 -17
- package/node_modules/@openclaw/fs-safe/dist/temp-workspace-descriptor.js +1 -2
- package/node_modules/@openclaw/fs-safe/dist/temp-workspace-owner.js +4 -9
- package/node_modules/@openclaw/fs-safe/dist/temp-workspace-ownership.d.ts +8 -0
- package/node_modules/@openclaw/fs-safe/dist/temp-workspace-ownership.js +52 -0
- package/node_modules/@openclaw/fs-safe/dist/test-hooks.d.ts +5 -1
- package/node_modules/@openclaw/fs-safe/dist/text-atomic.d.ts +2 -1
- package/node_modules/@openclaw/fs-safe/dist/text-atomic.js +2 -0
- package/node_modules/@openclaw/fs-safe/dist/trash.js +27 -1
- package/node_modules/@openclaw/fs-safe/dist/walk.d.ts +2 -5
- package/node_modules/@openclaw/fs-safe/dist/watch-alias.d.ts +6 -0
- package/node_modules/@openclaw/fs-safe/dist/watch-alias.js +88 -0
- package/node_modules/@openclaw/fs-safe/dist/watch-hints.d.ts +9 -0
- package/node_modules/@openclaw/fs-safe/dist/watch-hints.js +93 -0
- package/node_modules/@openclaw/fs-safe/dist/watch-native.d.ts +36 -0
- package/node_modules/@openclaw/fs-safe/dist/watch-native.js +73 -0
- package/node_modules/@openclaw/fs-safe/dist/watch-scan.d.ts +28 -0
- package/node_modules/@openclaw/fs-safe/dist/watch-scan.js +300 -0
- package/node_modules/@openclaw/fs-safe/dist/watch-stream.d.ts +8 -0
- package/node_modules/@openclaw/fs-safe/dist/watch-stream.js +32 -0
- package/node_modules/@openclaw/fs-safe/dist/watch-types.d.ts +60 -0
- package/node_modules/@openclaw/fs-safe/dist/watch-types.js +1 -0
- package/node_modules/@openclaw/fs-safe/dist/watch.d.ts +5 -0
- package/node_modules/@openclaw/fs-safe/dist/watch.js +530 -0
- package/node_modules/@openclaw/fs-safe/dist/windows-owner.d.ts +3 -7
- package/node_modules/@openclaw/fs-safe/dist/windows-owner.js +0 -1
- package/node_modules/@openclaw/fs-safe/dist/windows-security-bridge.cs +6 -6
- package/node_modules/@openclaw/fs-safe/dist/windows-security-bridge.ps1 +78 -3
- package/node_modules/@openclaw/fs-safe/dist/windows-security-command.d.ts +8 -0
- package/node_modules/@openclaw/fs-safe/dist/windows-security-command.js +66 -15
- package/node_modules/@openclaw/fs-safe/dist/windows-security-facts.d.ts +4 -0
- package/node_modules/@openclaw/fs-safe/dist/windows-security-facts.js +6 -2
- package/node_modules/@openclaw/fs-safe/docs/advanced.md +4 -2
- package/node_modules/@openclaw/fs-safe/docs/archive.md +16 -0
- package/node_modules/@openclaw/fs-safe/docs/atomic.md +93 -3
- package/node_modules/@openclaw/fs-safe/docs/contributing.md +102 -4
- package/node_modules/@openclaw/fs-safe/docs/copy.md +2 -0
- package/node_modules/@openclaw/fs-safe/docs/durability.md +7 -0
- package/node_modules/@openclaw/fs-safe/docs/file-store.md +5 -0
- package/node_modules/@openclaw/fs-safe/docs/guest.md +7 -1
- package/node_modules/@openclaw/fs-safe/docs/index.md +1 -0
- package/node_modules/@openclaw/fs-safe/docs/install.md +30 -0
- package/node_modules/@openclaw/fs-safe/docs/native-helper.md +20 -4
- package/node_modules/@openclaw/fs-safe/docs/native.md +63 -1
- package/node_modules/@openclaw/fs-safe/docs/permissions.md +66 -0
- package/node_modules/@openclaw/fs-safe/docs/public-api.md +30 -3
- package/node_modules/@openclaw/fs-safe/docs/retained-file.md +115 -0
- package/node_modules/@openclaw/fs-safe/docs/root.md +35 -9
- package/node_modules/@openclaw/fs-safe/docs/secret-file.md +19 -0
- package/node_modules/@openclaw/fs-safe/docs/security-model.md +4 -1
- package/node_modules/@openclaw/fs-safe/docs/sidecar-lock.md +5 -1
- package/node_modules/@openclaw/fs-safe/docs/staged-symlink.md +124 -0
- package/node_modules/@openclaw/fs-safe/docs/store.md +3 -1
- package/node_modules/@openclaw/fs-safe/docs/temp.md +24 -4
- package/node_modules/@openclaw/fs-safe/docs/testing.md +210 -0
- package/node_modules/@openclaw/fs-safe/docs/types.md +6 -0
- package/node_modules/@openclaw/fs-safe/docs/walk.md +34 -1
- package/node_modules/@openclaw/fs-safe/docs/watch.md +251 -0
- package/node_modules/@openclaw/fs-safe/docs/writing.md +21 -0
- package/node_modules/@openclaw/fs-safe/package.json +13 -9
- 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/node_modules/p-limit/index.d.ts +2 -0
- package/node_modules/p-limit/index.js +17 -1
- package/node_modules/p-limit/package.json +1 -1
- package/node_modules/p-limit/readme.md +2 -0
- package/package.json +6 -6
- package/node_modules/@openclaw/fs-safe/dist/directory-mode-owner.d.ts +0 -20
- package/node_modules/@openclaw/fs-safe/dist/directory-mode-owner.js +0 -78
|
@@ -202,7 +202,8 @@ function inspectOpenedPermissions(stat, platform) {
|
|
|
202
202
|
groupReadable: isGroupReadable(bits),
|
|
203
203
|
};
|
|
204
204
|
}
|
|
205
|
-
async function assertSecurePermissions(options,
|
|
205
|
+
async function assertSecurePermissions(options, opened, fd) {
|
|
206
|
+
const { pathStat: stat, realPath, identity } = opened;
|
|
206
207
|
if (options.permissions?.allowInsecure) {
|
|
207
208
|
return undefined;
|
|
208
209
|
}
|
|
@@ -282,7 +283,7 @@ export async function readSecureFile(options) {
|
|
|
282
283
|
const opened = await openSecureHandle(options, maxBytes);
|
|
283
284
|
try {
|
|
284
285
|
assertTrustedDirs(options, opened.realPath);
|
|
285
|
-
const permissions = await assertSecurePermissions(options, opened
|
|
286
|
+
const permissions = await assertSecurePermissions(options, opened, opened.handle.fd);
|
|
286
287
|
const buffer = await readHandleWithTimeout(opened.handle, options.io?.timeoutMs, maxBytes);
|
|
287
288
|
const finalIdentity = inspectFileIdentitySync(() => fsSync.fstatSync(opened.handle.fd, { bigint: true }), opened.identity);
|
|
288
289
|
if (!finalIdentity.isFile()) {
|
|
@@ -3,15 +3,10 @@ import { tmpdir as getOsTmpDir } from "node:os";
|
|
|
3
3
|
import path from "node:path";
|
|
4
4
|
import { directoryEntryPath } from "./directory-entry-path.js";
|
|
5
5
|
import { recursiveMkdirPath } from "./recursive-mkdir-path.js";
|
|
6
|
+
import { hasNodeErrorCode } from "./path.js";
|
|
6
7
|
import { assertSafePathSegment } from "./safe-path-segment.js";
|
|
7
8
|
import { assertNoWindowsPathAlias, pathForWindowsFilesystem } from "./windows-path-alias.js";
|
|
8
9
|
import { captureSecureTempRepairAdapter, repairSecureTempDirectory, secureTempDirectoryReceipt, } from "./secure-temp-repair.js";
|
|
9
|
-
function isNodeErrorWithCode(err, code) {
|
|
10
|
-
return (typeof err === "object" &&
|
|
11
|
-
err !== null &&
|
|
12
|
-
"code" in err &&
|
|
13
|
-
err.code === code);
|
|
14
|
-
}
|
|
15
10
|
export function resolveSecureTempRoot(options) {
|
|
16
11
|
const { fallbackPrefix: prefix, accessSync: suppliedAccess, lstatSync: suppliedLstat, mkdirSync: suppliedMkdir, descriptor: suppliedDescriptor, getuid: suppliedGetuid, tmpdir: suppliedTmpdir, platform: suppliedPlatform, preferredDir, skipPreferredOnWindows, warn: suppliedWarn, warningPrefix: suppliedWarningPrefix, unsafeFallbackLabel: suppliedFallbackLabel, } = options;
|
|
17
12
|
const platform = suppliedPlatform ?? process.platform;
|
|
@@ -87,7 +82,7 @@ export function resolveSecureTempRoot(options) {
|
|
|
87
82
|
candidate = lstatSync(candidatePath);
|
|
88
83
|
}
|
|
89
84
|
catch (error) {
|
|
90
|
-
return { kind:
|
|
85
|
+
return { kind: hasNodeErrorCode(error, "ENOENT") ? "missing" : "invalid", error };
|
|
91
86
|
}
|
|
92
87
|
let receipt;
|
|
93
88
|
try {
|
|
@@ -102,7 +102,7 @@ export async function acquireSidecarLock(options, context) {
|
|
|
102
102
|
await waitForRetry();
|
|
103
103
|
}
|
|
104
104
|
catch (waitError) {
|
|
105
|
-
if (waitError
|
|
105
|
+
if (waitError?.code === "file_lock_timeout")
|
|
106
106
|
throw denial;
|
|
107
107
|
throw waitError;
|
|
108
108
|
}
|
|
@@ -290,9 +290,9 @@ export async function acquireSidecarLock(options, context) {
|
|
|
290
290
|
}
|
|
291
291
|
await handle.writeFile(raw, "utf8");
|
|
292
292
|
}
|
|
293
|
-
const snapshot = { raw, payload, stat: fsSync.fstatSync(handle.fd), ownershipToken };
|
|
293
|
+
const snapshot = { raw, payload, stat: fsSync.fstatSync(handle.fd, { bigint: true }), ownershipToken };
|
|
294
294
|
createdSnapshot = snapshot;
|
|
295
|
-
if (snapshot.stat.nlink ===
|
|
295
|
+
if (snapshot.stat.nlink === 0n) {
|
|
296
296
|
await handle.close();
|
|
297
297
|
handle = null;
|
|
298
298
|
await waitForRetry();
|
|
@@ -361,7 +361,7 @@ export async function acquireSidecarLock(options, context) {
|
|
|
361
361
|
if (handle) {
|
|
362
362
|
failedSnapshot ??= { payload: null };
|
|
363
363
|
try {
|
|
364
|
-
failedSnapshot.stat = fsSync.fstatSync(handle.fd);
|
|
364
|
+
failedSnapshot.stat = fsSync.fstatSync(handle.fd, { bigint: true });
|
|
365
365
|
}
|
|
366
366
|
catch {
|
|
367
367
|
// Best-effort cleanup of a failed exclusive create.
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { Root } from "./root-impl.js";
|
|
2
2
|
import type { HeldSidecarLock } from "./sidecar-lock-admission.js";
|
|
3
3
|
import { readSidecarLockRawSnapshot, type SidecarLockSnapshot } from "./sidecar-lock-reclaim.js";
|
|
4
|
-
type SidecarAdmissionRunner = {
|
|
4
|
+
export type SidecarAdmissionRunner = {
|
|
5
5
|
hasToken(): boolean;
|
|
6
6
|
run<T>(callback: () => T): T;
|
|
7
7
|
};
|
|
@@ -39,4 +39,3 @@ export declare function observeHeldSidecarParser(params: {
|
|
|
39
39
|
parserAccessor(): ((raw: string) => unknown) | undefined;
|
|
40
40
|
isTransientDenial(error: unknown): boolean;
|
|
41
41
|
}): Promise<HeldSidecarParserObservation>;
|
|
42
|
-
export {};
|
|
@@ -1,5 +1,4 @@
|
|
|
1
|
-
import type {
|
|
2
|
-
import { type SidecarLockSnapshot } from "./sidecar-lock-reclaim.js";
|
|
1
|
+
import type { HeldSidecarLock } from "./sidecar-lock-admission.js";
|
|
3
2
|
import type { SidecarLockHandle } from "./sidecar-lock-types.js";
|
|
4
3
|
export declare function stopSidecarLockMonitoring(held: {
|
|
5
4
|
compromiseTimer?: NodeJS.Timeout;
|
|
@@ -14,12 +13,7 @@ export declare function createSidecarLockHandle(params: {
|
|
|
14
13
|
}): SidecarLockHandle;
|
|
15
14
|
export declare function createHeldSidecarLockHandle(params: {
|
|
16
15
|
normalizedTargetPath: string;
|
|
17
|
-
held:
|
|
18
|
-
lockPath: string;
|
|
19
|
-
snapshot: SidecarLockSnapshot;
|
|
20
|
-
lockRoot?: Root;
|
|
21
|
-
parsePayload?: (raw: string) => unknown;
|
|
22
|
-
};
|
|
16
|
+
held: Pick<HeldSidecarLock, "lockPath" | "snapshot" | "lockRoot" | "parsePayload">;
|
|
23
17
|
release: (options?: {
|
|
24
18
|
retry?: boolean;
|
|
25
19
|
}) => Promise<unknown>;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { SidecarLockRetryOptions } from "./sidecar-lock-types.js";
|
|
1
|
+
import type { SidecarLockReclaimParams, SidecarLockRetryOptions } from "./sidecar-lock-types.js";
|
|
2
2
|
export declare function validateSidecarLockRetryOptions(retry: SidecarLockRetryOptions): void;
|
|
3
3
|
export declare function validateSidecarLockTimeoutMs(timeoutMs: number | undefined): void;
|
|
4
4
|
export declare function validateSidecarLockStaleMs(staleMs: number | undefined): void;
|
|
@@ -11,9 +11,4 @@ export declare function sidecarLockRetryDelay(retry: SidecarLockRetryOptions, ti
|
|
|
11
11
|
export declare const maxTransientLockDenials = 8;
|
|
12
12
|
export declare function isTransientLockFileDenial(error: unknown, lockPath: string): boolean;
|
|
13
13
|
export declare function sidecarLockPayloadCreatedAtMs(payload: unknown): number | null;
|
|
14
|
-
export declare function defaultSidecarLockShouldReclaim(params:
|
|
15
|
-
lockPath: string;
|
|
16
|
-
payload: unknown;
|
|
17
|
-
staleMs: number;
|
|
18
|
-
nowMs: number;
|
|
19
|
-
}): Promise<boolean>;
|
|
14
|
+
export declare function defaultSidecarLockShouldReclaim(params: Pick<SidecarLockReclaimParams, "lockPath" | "payload" | "staleMs" | "nowMs">): Promise<boolean>;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { type Stats } from "node:fs";
|
|
1
|
+
import { type BigIntStats, type Stats } from "node:fs";
|
|
2
2
|
import type { Root } from "./root-impl.js";
|
|
3
3
|
export type SidecarLockStaleSnapshot = {
|
|
4
4
|
lockPath: string;
|
|
@@ -9,7 +9,7 @@ export type SidecarLockStaleSnapshot = {
|
|
|
9
9
|
export type SidecarLockSnapshot = {
|
|
10
10
|
raw?: string;
|
|
11
11
|
payload: unknown;
|
|
12
|
-
stat?: Stats;
|
|
12
|
+
stat?: Stats | BigIntStats;
|
|
13
13
|
ownershipToken?: string;
|
|
14
14
|
};
|
|
15
15
|
type SidecarLockRawSnapshot = Omit<SidecarLockSnapshot, "payload"> & {
|
|
@@ -90,7 +90,7 @@ export async function readSidecarLockRawSnapshot(lockPath, options = {}) {
|
|
|
90
90
|
const raw = (await readFileHandleBounded(opened.handle, MAX_LOCK_PAYLOAD_BYTES)).toString("utf8");
|
|
91
91
|
return {
|
|
92
92
|
raw,
|
|
93
|
-
stat: opened.
|
|
93
|
+
stat: fsSync.fstatSync(opened.handle.fd, { bigint: true }),
|
|
94
94
|
};
|
|
95
95
|
}
|
|
96
96
|
finally {
|
|
@@ -99,7 +99,7 @@ export async function readSidecarLockRawSnapshot(lockPath, options = {}) {
|
|
|
99
99
|
}
|
|
100
100
|
let before;
|
|
101
101
|
try {
|
|
102
|
-
before = fsSync.lstatSync(lockPath);
|
|
102
|
+
before = fsSync.lstatSync(lockPath, { bigint: true });
|
|
103
103
|
}
|
|
104
104
|
catch (error) {
|
|
105
105
|
before = missingSnapshotPath(error);
|
|
@@ -132,7 +132,7 @@ export async function readSidecarLockRawSnapshot(lockPath, options = {}) {
|
|
|
132
132
|
options.onOpenFailure?.(error);
|
|
133
133
|
throw error;
|
|
134
134
|
}
|
|
135
|
-
const opened = fsSync.fstatSync(handle.fd);
|
|
135
|
+
const opened = fsSync.fstatSync(handle.fd, { bigint: true });
|
|
136
136
|
if (!opened.isFile()) {
|
|
137
137
|
if (options.rejectNonFile) {
|
|
138
138
|
throw new FsSafeError("not-file", `sidecar lock is not a regular file: ${lockPath}`);
|
|
@@ -144,7 +144,7 @@ export async function readSidecarLockRawSnapshot(lockPath, options = {}) {
|
|
|
144
144
|
const raw = (await readFileHandleBounded(handle, MAX_LOCK_PAYLOAD_BYTES)).toString("utf8");
|
|
145
145
|
let after;
|
|
146
146
|
try {
|
|
147
|
-
after = fsSync.lstatSync(lockPath);
|
|
147
|
+
after = fsSync.lstatSync(lockPath, { bigint: true });
|
|
148
148
|
}
|
|
149
149
|
catch (error) {
|
|
150
150
|
after = missingSnapshotPath(error);
|
|
@@ -159,7 +159,7 @@ export async function readSidecarLockRawSnapshot(lockPath, options = {}) {
|
|
|
159
159
|
}
|
|
160
160
|
function lstatSidecarLockSync(lockPath) {
|
|
161
161
|
try {
|
|
162
|
-
return fsSync.lstatSync(lockPath);
|
|
162
|
+
return fsSync.lstatSync(lockPath, { bigint: true });
|
|
163
163
|
}
|
|
164
164
|
catch (error) {
|
|
165
165
|
return missingSnapshotPath(error);
|
|
@@ -189,7 +189,7 @@ export function readSidecarLockRawSnapshotSync(lockPath, options = {}) {
|
|
|
189
189
|
options.onOpenFailure?.(error);
|
|
190
190
|
throw error;
|
|
191
191
|
}
|
|
192
|
-
const opened = fsSync.fstatSync(fd);
|
|
192
|
+
const opened = fsSync.fstatSync(fd, { bigint: true });
|
|
193
193
|
if (!opened.isFile()) {
|
|
194
194
|
if (options.rejectNonFile) {
|
|
195
195
|
throw new FsSafeError("not-file", `sidecar lock is not a regular file: ${lockPath}`);
|
|
@@ -357,13 +357,12 @@ export async function removeStaleSidecarLockIfAllowed(params) {
|
|
|
357
357
|
if (params.assertGuardHeld)
|
|
358
358
|
await params.assertGuardHeld();
|
|
359
359
|
params.assertAuthorized?.();
|
|
360
|
+
if (params.lockRoot) {
|
|
361
|
+
await params.lockRoot.remove(relativeSidecarLockPath(params.lockRoot, params.lockPath), { assertBeforeMutation: params.assertAuthorized });
|
|
362
|
+
return "removed";
|
|
363
|
+
}
|
|
360
364
|
try {
|
|
361
|
-
|
|
362
|
-
await params.lockRoot.remove(relativeSidecarLockPath(params.lockRoot, params.lockPath), { assertBeforeMutation: params.assertAuthorized });
|
|
363
|
-
}
|
|
364
|
-
else {
|
|
365
|
-
await fs.rm(params.lockPath);
|
|
366
|
-
}
|
|
365
|
+
await fs.rm(params.lockPath);
|
|
367
366
|
return "removed";
|
|
368
367
|
}
|
|
369
368
|
catch (err) {
|
|
@@ -1,11 +1,7 @@
|
|
|
1
1
|
import { type SidecarReclaimGuard } from "./sidecar-lock-reclaim.js";
|
|
2
|
-
import { type SidecarLockParserState } from "./sidecar-lock-admission-parser.js";
|
|
2
|
+
import { type SidecarLockParserState, type SidecarAdmissionRunner } from "./sidecar-lock-admission-parser.js";
|
|
3
3
|
import type { Root } from "./root-impl.js";
|
|
4
4
|
import type { SidecarLockAcquireOptions } from "./sidecar-lock-types.js";
|
|
5
|
-
type SidecarAdmissionRunner = {
|
|
6
|
-
hasToken(): boolean;
|
|
7
|
-
run<T>(callback: () => T): T;
|
|
8
|
-
};
|
|
9
5
|
type StaleOptions = Pick<SidecarLockAcquireOptions<Record<string, unknown>>, "shouldReclaim" | "shouldRemoveStaleLock" | "staleRecovery">;
|
|
10
6
|
export type SidecarLockStaleOptionsState = {
|
|
11
7
|
shouldReclaimObserved?: boolean;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import fsSync from "node:fs";
|
|
2
2
|
import { FsSafeError } from "./errors.js";
|
|
3
|
-
import {
|
|
3
|
+
import { sameFileIdentityForCleanup } from "./file-identity.js";
|
|
4
4
|
import { removeSidecarLockIfUnchanged, sidecarLockSnapshotMatches, } from "./sidecar-lock-reclaim.js";
|
|
5
5
|
import { acquireSidecarLock } from "./sidecar-lock-acquire.js";
|
|
6
6
|
import { createHeldSidecarLockHandle, stopSidecarLockMonitoring } from "./sidecar-lock-handle.js";
|
|
@@ -50,7 +50,7 @@ function resolveManagerState(key) {
|
|
|
50
50
|
function snapshotMatchesSync(lockPath, observed) {
|
|
51
51
|
let fd;
|
|
52
52
|
try {
|
|
53
|
-
const beforeStat = fsSync.lstatSync(lockPath);
|
|
53
|
+
const beforeStat = fsSync.lstatSync(lockPath, { bigint: true });
|
|
54
54
|
if (!beforeStat.isFile()) {
|
|
55
55
|
return false;
|
|
56
56
|
}
|
|
@@ -60,16 +60,18 @@ function snapshotMatchesSync(lockPath, observed) {
|
|
|
60
60
|
: 0) |
|
|
61
61
|
(typeof fsSync.constants.O_NONBLOCK === "number" ? fsSync.constants.O_NONBLOCK : 0);
|
|
62
62
|
fd = fsSync.openSync(lockPath, openFlags);
|
|
63
|
-
const openedStat = fsSync.fstatSync(fd);
|
|
64
|
-
|
|
63
|
+
const openedStat = fsSync.fstatSync(fd, { bigint: true });
|
|
64
|
+
// Token-owned files can have different descriptor/path identities on VirtioFS.
|
|
65
|
+
// Require a known descriptor identity without rejecting that supported drift.
|
|
66
|
+
if (!openedStat.isFile() || !sameFileIdentityForCleanup(openedStat, openedStat)) {
|
|
65
67
|
return false;
|
|
66
68
|
}
|
|
67
|
-
if (observed.raw !== undefined && openedStat.size !== Buffer.byteLength(observed.raw)) {
|
|
69
|
+
if (observed.raw !== undefined && openedStat.size !== BigInt(Buffer.byteLength(observed.raw))) {
|
|
68
70
|
return false;
|
|
69
71
|
}
|
|
70
72
|
const raw = fsSync.readFileSync(fd, "utf8");
|
|
71
|
-
const afterStat = fsSync.lstatSync(lockPath);
|
|
72
|
-
if (!afterStat.isFile() || !
|
|
73
|
+
const afterStat = fsSync.lstatSync(lockPath, { bigint: true });
|
|
74
|
+
if (!afterStat.isFile() || !sameFileIdentityForCleanup(beforeStat, afterStat)) {
|
|
73
75
|
return false;
|
|
74
76
|
}
|
|
75
77
|
return sidecarLockSnapshotMatches({ raw, payload: null, stat: afterStat }, observed);
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import type { PublishedFileReceipt, StagedFileCleanupReceipt, StagedFileReceipt } from "./staged-file-types.js";
|
|
2
|
+
/** Caller-captured admission evidence, never a same-target ownership heuristic. */
|
|
3
|
+
export type StagedSymlinkExpected = Readonly<Pick<StagedFileReceipt["identity"], "dev" | "ino" | "uid" | "gid" | "ctimeNs"> & {
|
|
4
|
+
target: string;
|
|
5
|
+
}>;
|
|
6
|
+
export type StagedSymlinkReceipt = StagedFileReceipt & Readonly<{
|
|
7
|
+
target: string;
|
|
8
|
+
}>;
|
|
9
|
+
export type PublishedSymlinkReceipt = Readonly<Pick<PublishedFileReceipt, "status" | "basename"> & {
|
|
10
|
+
staged: StagedSymlinkReceipt;
|
|
11
|
+
overwrite: false;
|
|
12
|
+
}>;
|
|
13
|
+
export type StagedSymlinkPublication = Readonly<{
|
|
14
|
+
status: "not-published";
|
|
15
|
+
}> | PublishedSymlinkReceipt | Readonly<{
|
|
16
|
+
status: "indeterminate";
|
|
17
|
+
basename: string;
|
|
18
|
+
overwrite: false;
|
|
19
|
+
}>;
|
|
20
|
+
export type StagedSymlinkRemoval = "removed" | "name-absent" | "preserved";
|
|
21
|
+
export type StagedSymlinkCleanupReceipt = Readonly<Pick<StagedFileCleanupReceipt, "temporaryBasename" | "status" | "resources"> & {
|
|
22
|
+
publication: StagedSymlinkPublication;
|
|
23
|
+
}>;
|
|
24
|
+
export type StagedSymlinkFailureDetails = Readonly<{
|
|
25
|
+
phase: "prepare" | "publish" | "remove-published" | "cleanup";
|
|
26
|
+
publication: StagedSymlinkPublication;
|
|
27
|
+
cleanup?: StagedSymlinkCleanupReceipt;
|
|
28
|
+
}>;
|
|
29
|
+
export interface StagedSymlink extends AsyncDisposable {
|
|
30
|
+
readonly receipt: StagedSymlinkReceipt;
|
|
31
|
+
assertCurrent(): Promise<void>;
|
|
32
|
+
publish(basename: string): Promise<PublishedSymlinkReceipt>;
|
|
33
|
+
/** Check the published name against the still-retained original symlink. */
|
|
34
|
+
assertPublished(): Promise<void>;
|
|
35
|
+
/** Explicit recovery only. Never removes an observed foreign replacement. */
|
|
36
|
+
removePublished(): Promise<StagedSymlinkRemoval>;
|
|
37
|
+
/** Closes the owner; only an unattempted, still-owned stage is removed. */
|
|
38
|
+
cleanup(): Promise<StagedSymlinkCleanupReceipt>;
|
|
39
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -1,11 +1,4 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { recordFileObservationFailure } from "./file-observation.js";
|
|
3
|
-
import { inspectFileIdentitySync } from "./strict-file-identity.js";
|
|
4
|
-
function identityMismatch() {
|
|
5
|
-
const error = new FsSafeError("path-mismatch", "file identity changed or could not be verified");
|
|
6
|
-
recordFileObservationFailure(error, "identity");
|
|
7
|
-
throw error;
|
|
8
|
-
}
|
|
1
|
+
import { fileIdentityMismatchError, inspectFileIdentitySync } from "./strict-file-identity.js";
|
|
9
2
|
function safeNumber(value) {
|
|
10
3
|
return typeof value === "number" && Number.isSafeInteger(value) && value >= 0;
|
|
11
4
|
}
|
|
@@ -35,7 +28,7 @@ function observeStatSync(inspect, expected, initial, platform = process.platform
|
|
|
35
28
|
const ino = numericIno ? BigInt(rawIno) : typeof rawIno === "bigint" ? rawIno : undefined;
|
|
36
29
|
if (expected && ((dev !== undefined && dev !== expected.dev) ||
|
|
37
30
|
(ino !== undefined && ino !== expected.ino)))
|
|
38
|
-
|
|
31
|
+
throw fileIdentityMismatchError();
|
|
39
32
|
if (numericDev && numericIno) {
|
|
40
33
|
return (captureIdentity
|
|
41
34
|
? { stat, identity: expected ?? { dev: dev, ino: ino } }
|
|
@@ -47,7 +40,7 @@ function observeStatSync(inspect, expected, initial, platform = process.platform
|
|
|
47
40
|
const current = inspect(true);
|
|
48
41
|
if ((dev !== undefined && current.dev !== dev) ||
|
|
49
42
|
(ino !== undefined && current.ino !== ino))
|
|
50
|
-
|
|
43
|
+
throw fileIdentityMismatchError();
|
|
51
44
|
return current;
|
|
52
45
|
}, expected, platform);
|
|
53
46
|
return (captureIdentity
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import type { BigIntStats } from "node:fs";
|
|
2
|
+
import { FsSafeError } from "./errors.js";
|
|
2
3
|
type ExactFileIdentity = Pick<BigIntStats, "dev" | "ino">;
|
|
4
|
+
export declare function fileIdentityMismatchError(): FsSafeError;
|
|
3
5
|
export declare function inspectFileIdentity<T extends ExactFileIdentity>(inspect: () => T | Promise<T>, expected?: ExactFileIdentity, platform?: NodeJS.Platform): Promise<T>;
|
|
4
6
|
export declare function inspectFileIdentitySync<T extends ExactFileIdentity>(inspect: () => T, expected?: ExactFileIdentity, platform?: NodeJS.Platform, mismatch?: () => Error): T;
|
|
5
7
|
export {};
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { FsSafeError } from "./errors.js";
|
|
2
2
|
import { recordFileObservationFailure } from "./file-observation.js";
|
|
3
|
-
function
|
|
3
|
+
export function fileIdentityMismatchError() {
|
|
4
4
|
const error = new FsSafeError("path-mismatch", "file identity changed or could not be verified");
|
|
5
5
|
recordFileObservationFailure(error, "identity");
|
|
6
6
|
return error;
|
|
@@ -13,20 +13,20 @@ function identityCheck(expected, platform) {
|
|
|
13
13
|
const value = stat[field];
|
|
14
14
|
// Numeric receipts cannot recover identity bits already lost to rounding.
|
|
15
15
|
if (typeof value !== "bigint")
|
|
16
|
-
throw
|
|
16
|
+
throw fileIdentityMismatchError();
|
|
17
17
|
if (platform === "win32" && value === 0n) {
|
|
18
18
|
complete = false;
|
|
19
19
|
}
|
|
20
20
|
else {
|
|
21
21
|
if (known[field] !== undefined && known[field] !== value)
|
|
22
|
-
throw
|
|
22
|
+
throw fileIdentityMismatchError();
|
|
23
23
|
known[field] = value;
|
|
24
24
|
}
|
|
25
25
|
}
|
|
26
26
|
return complete;
|
|
27
27
|
};
|
|
28
28
|
if (expected && !check(expected))
|
|
29
|
-
throw
|
|
29
|
+
throw fileIdentityMismatchError();
|
|
30
30
|
return check;
|
|
31
31
|
}
|
|
32
32
|
// Retry only unknown Windows identities, retaining every known component so a
|
|
@@ -38,9 +38,9 @@ export async function inspectFileIdentity(inspect, expected, platform = process.
|
|
|
38
38
|
if (check(stat))
|
|
39
39
|
return stat;
|
|
40
40
|
}
|
|
41
|
-
throw
|
|
41
|
+
throw fileIdentityMismatchError();
|
|
42
42
|
}
|
|
43
|
-
export function inspectFileIdentitySync(inspect, expected, platform = process.platform, mismatch =
|
|
43
|
+
export function inspectFileIdentitySync(inspect, expected, platform = process.platform, mismatch = fileIdentityMismatchError) {
|
|
44
44
|
let knownDev;
|
|
45
45
|
let knownIno;
|
|
46
46
|
if (expected) {
|
|
@@ -3,24 +3,46 @@ import path from "node:path";
|
|
|
3
3
|
import { FsSafeError } from "./errors.js";
|
|
4
4
|
import { hasNodeErrorCode, isPathRelativeEscape } from "./path.js";
|
|
5
5
|
import { assertNoWindowsPathAlias, resolvePathPreservingWindowsRoot, } from "./windows-path-alias.js";
|
|
6
|
+
function outsideRootError(params, root) {
|
|
7
|
+
return new Error(`${params.messagePrefix ?? "Path"} must stay under ${root}.`);
|
|
8
|
+
}
|
|
9
|
+
function pathSegments(value) {
|
|
10
|
+
return value.split(process.platform === "win32" ? /[/\\]+/ : /\/+/)
|
|
11
|
+
.filter((segment) => segment.length > 0 && segment !== ".");
|
|
12
|
+
}
|
|
13
|
+
function rawTargetSegments(root, targetPath) {
|
|
14
|
+
// Resolve only the drive/root or cwd, never the caller's dotdot segments.
|
|
15
|
+
const targetRoot = path.parse(targetPath).root;
|
|
16
|
+
const base = resolvePathPreservingWindowsRoot(targetRoot || ".");
|
|
17
|
+
const absoluteTarget = `${base}${path.sep}${targetPath.slice(targetRoot.length)}`;
|
|
18
|
+
const rootSegments = pathSegments(root);
|
|
19
|
+
const targetSegments = pathSegments(absoluteTarget);
|
|
20
|
+
const fold = (segment) => process.platform === "win32" ? segment.toLowerCase() : segment;
|
|
21
|
+
if (rootSegments.some((segment, index) => fold(segment) !== fold(targetSegments[index] ?? ""))) {
|
|
22
|
+
return undefined;
|
|
23
|
+
}
|
|
24
|
+
return targetSegments.slice(rootSegments.length);
|
|
25
|
+
}
|
|
6
26
|
function resolvePathWalk(params) {
|
|
7
27
|
const rawRootDir = params.rootDir;
|
|
8
28
|
assertNoWindowsPathAlias(rawRootDir, "filesystem", "root dir uses a Windows filesystem namespace alias");
|
|
9
29
|
const rawTargetPath = params.targetPath;
|
|
10
30
|
assertNoWindowsPathAlias(rawTargetPath, "filesystem", "target path uses a Windows filesystem namespace alias");
|
|
11
31
|
const root = resolvePathPreservingWindowsRoot(rawRootDir);
|
|
12
|
-
const
|
|
13
|
-
const relative = path.relative(root,
|
|
32
|
+
const lexicalTarget = resolvePathPreservingWindowsRoot(rawTargetPath);
|
|
33
|
+
const relative = path.relative(root, lexicalTarget);
|
|
14
34
|
if (isPathRelativeEscape(relative)) {
|
|
15
35
|
if (params.allowOutsideRoot) {
|
|
16
36
|
return null;
|
|
17
37
|
}
|
|
18
|
-
throw
|
|
38
|
+
throw outsideRootError(params, root);
|
|
19
39
|
}
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
40
|
+
const segments = rawTargetSegments(root, rawTargetPath);
|
|
41
|
+
// A spelling outside the root must not fall back to a normalized walk that
|
|
42
|
+
// could erase a symlink before the filesystem receives the original path.
|
|
43
|
+
if (!segments)
|
|
44
|
+
throw outsideRootError(params, root);
|
|
45
|
+
return { root, segments };
|
|
24
46
|
}
|
|
25
47
|
function formatUnsafePath(params, current) {
|
|
26
48
|
return `${params.messagePrefix ?? "Path"} must not traverse symlinked directory: ${current}`;
|
|
@@ -28,18 +50,39 @@ function formatUnsafePath(params, current) {
|
|
|
28
50
|
export async function assertNoSymlinkParents(params) {
|
|
29
51
|
assertNoSymlinkParentsSync(params);
|
|
30
52
|
}
|
|
53
|
+
function isFilesystemRoot(root) {
|
|
54
|
+
return root === path.parse(root).root;
|
|
55
|
+
}
|
|
31
56
|
export function assertNoSymlinkParentsSync(params) {
|
|
32
57
|
const walk = resolvePathWalk(params);
|
|
33
58
|
if (!walk) {
|
|
34
59
|
return;
|
|
35
60
|
}
|
|
36
61
|
let current = walk.root;
|
|
62
|
+
// `..` may only undo a real directory this walk already lstat'd.
|
|
63
|
+
const walked = [];
|
|
37
64
|
for (const [index, segment] of walk.segments.entries()) {
|
|
65
|
+
if (segment === "..") {
|
|
66
|
+
const top = walked[walked.length - 1];
|
|
67
|
+
if (top?.kind === "dir") {
|
|
68
|
+
walked.pop();
|
|
69
|
+
current = walked[walked.length - 1]?.path ?? walk.root;
|
|
70
|
+
continue;
|
|
71
|
+
}
|
|
72
|
+
if (top?.kind === "symlink") {
|
|
73
|
+
throw new Error(formatUnsafePath(params, top.path));
|
|
74
|
+
}
|
|
75
|
+
if (!isFilesystemRoot(walk.root)) {
|
|
76
|
+
throw outsideRootError(params, walk.root);
|
|
77
|
+
}
|
|
78
|
+
continue;
|
|
79
|
+
}
|
|
38
80
|
current = path.join(current, segment);
|
|
39
81
|
try {
|
|
40
82
|
const stat = fsSync.lstatSync(current);
|
|
41
83
|
if (stat.isSymbolicLink()) {
|
|
42
84
|
if (params.allowRootChildSymlink && path.dirname(current) === walk.root) {
|
|
85
|
+
walked.push({ path: current, kind: "symlink" });
|
|
43
86
|
continue;
|
|
44
87
|
}
|
|
45
88
|
throw new Error(formatUnsafePath(params, current));
|
|
@@ -47,9 +90,17 @@ export function assertNoSymlinkParentsSync(params) {
|
|
|
47
90
|
if ((params.requireDirectories || index < walk.segments.length - 1) && !stat.isDirectory()) {
|
|
48
91
|
throw new FsSafeError("not-file", `${params.messagePrefix ?? "Path"} must traverse directories: ${current}`);
|
|
49
92
|
}
|
|
93
|
+
if (stat.isDirectory()) {
|
|
94
|
+
walked.push({ path: current, kind: "dir" });
|
|
95
|
+
}
|
|
50
96
|
}
|
|
51
97
|
catch (err) {
|
|
52
98
|
if (hasNodeErrorCode(err, "ENOENT") && params.allowMissing !== false) {
|
|
99
|
+
// Win32 can cancel a nonexistent component before filesystem lookup.
|
|
100
|
+
// Returning early would leave later, reachable symlinks unchecked.
|
|
101
|
+
if (process.platform === "win32" && walk.segments.slice(index + 1).includes("..")) {
|
|
102
|
+
throw new FsSafeError("invalid-path", `${params.messagePrefix ?? "Path"} must not cancel a missing directory: ${current}`);
|
|
103
|
+
}
|
|
53
104
|
return;
|
|
54
105
|
}
|
|
55
106
|
throw err;
|
|
@@ -4,8 +4,9 @@ import fs from "node:fs/promises";
|
|
|
4
4
|
import path from "node:path";
|
|
5
5
|
import { suffixWindowsReservedDeviceName } from "./filename.js";
|
|
6
6
|
import { sameFileIdentityForCleanup } from "./file-identity.js";
|
|
7
|
-
import { assertSafePathSegment,
|
|
7
|
+
import { assertSafePathSegment, isSafePathSegment, normalizeSafePathSegment, trimHyphenEdges, } from "./safe-path-segment.js";
|
|
8
8
|
import { resolveSecureTempRoot } from "./secure-temp-dir.js";
|
|
9
|
+
import { hasNodeErrorCode } from "./path.js";
|
|
9
10
|
import { registerTempPathForExit } from "./temp-cleanup.js";
|
|
10
11
|
import { assertNoWindowsPathAlias, resolvePathPreservingWindowsRoot, } from "./windows-path-alias.js";
|
|
11
12
|
const HYPHEN_CHAR_CODE = 0x2d;
|
|
@@ -59,7 +60,10 @@ function sanitizeExtension(extension) {
|
|
|
59
60
|
return token ? `.${token}` : "";
|
|
60
61
|
}
|
|
61
62
|
export function sanitizeTempFileName(fileName) {
|
|
62
|
-
|
|
63
|
+
// Suffix reserved stems before admission so CON.txt stays CON_.txt.
|
|
64
|
+
const suffixed = suffixWindowsReservedDeviceName(normalizeSafePathSegment(path.basename(fileName)));
|
|
65
|
+
// The suffix only inserts "_", so this value is already normalized.
|
|
66
|
+
return isSafePathSegment(suffixed, { allowDotPrefix: true }) ? suffixed : "download.bin";
|
|
63
67
|
}
|
|
64
68
|
export function buildRandomTempFilePath(params) {
|
|
65
69
|
const rootDir = resolveTempRoot(params.rootDir);
|
|
@@ -76,12 +80,6 @@ export function buildRandomTempFilePath(params) {
|
|
|
76
80
|
assertNoWindowsPathAlias(filePath, "filesystem", "temp file path uses a Windows filesystem namespace alias");
|
|
77
81
|
return filePath;
|
|
78
82
|
}
|
|
79
|
-
function isNodeErrorWithCode(err, code) {
|
|
80
|
-
return (typeof err === "object" &&
|
|
81
|
-
err !== null &&
|
|
82
|
-
"code" in err &&
|
|
83
|
-
err.code === code);
|
|
84
|
-
}
|
|
85
83
|
async function cleanupTempDir(dir, identity, onCleanupError) {
|
|
86
84
|
try {
|
|
87
85
|
const current = fsSync.lstatSync(dir, { bigint: true });
|
|
@@ -91,7 +89,7 @@ async function cleanupTempDir(dir, identity, onCleanupError) {
|
|
|
91
89
|
await fs.rm(dir, { recursive: true, force: true });
|
|
92
90
|
}
|
|
93
91
|
catch (err) {
|
|
94
|
-
if (!
|
|
92
|
+
if (!hasNodeErrorCode(err, "ENOENT")) {
|
|
95
93
|
onCleanupError?.(err);
|
|
96
94
|
}
|
|
97
95
|
}
|
|
@@ -174,7 +172,7 @@ export async function createOwnedTempFile(params) {
|
|
|
174
172
|
await owner.cleanup();
|
|
175
173
|
}
|
|
176
174
|
catch (err) {
|
|
177
|
-
if (!
|
|
175
|
+
if (!hasNodeErrorCode(err, "ENOENT")) {
|
|
178
176
|
params.onCleanupError?.(err);
|
|
179
177
|
}
|
|
180
178
|
}
|