@openclaw/fs-safe 0.5.1 → 0.5.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +86 -0
- package/README.md +19 -6
- package/dist/absolute-path.d.ts.map +1 -1
- package/dist/absolute-path.js +20 -12
- package/dist/archive-entry.d.ts +1 -0
- package/dist/archive-entry.d.ts.map +1 -1
- package/dist/archive-entry.js +37 -6
- package/dist/archive-errors.d.ts +1 -0
- package/dist/archive-errors.d.ts.map +1 -1
- package/dist/archive-errors.js +4 -0
- package/dist/archive-input.d.ts +7 -0
- package/dist/archive-input.d.ts.map +1 -1
- package/dist/archive-input.js +11 -1
- package/dist/archive-limits.js +1 -1
- package/dist/archive-native.d.ts.map +1 -1
- package/dist/archive-native.js +12 -6
- package/dist/archive-policy.d.ts.map +1 -1
- package/dist/archive-policy.js +2 -1
- package/dist/archive-read.d.ts.map +1 -1
- package/dist/archive-read.js +62 -37
- package/dist/archive-staging.d.ts.map +1 -1
- package/dist/archive-staging.js +3 -2
- package/dist/archive-tar-meta.d.ts.map +1 -1
- package/dist/archive-tar-meta.js +15 -1
- package/dist/archive-tar-runtime.d.ts +1 -0
- package/dist/archive-tar-runtime.d.ts.map +1 -1
- package/dist/archive-tar-runtime.js +11 -0
- package/dist/archive-tar.d.ts.map +1 -1
- package/dist/archive-tar.js +8 -4
- package/dist/archive-zip-entry.d.ts +7 -1
- package/dist/archive-zip-entry.d.ts.map +1 -1
- package/dist/archive-zip-entry.js +11 -1
- package/dist/archive-zip-integrity.d.ts +5 -0
- package/dist/archive-zip-integrity.d.ts.map +1 -0
- package/dist/archive-zip-integrity.js +54 -0
- package/dist/archive-zip-preflight.d.ts.map +1 -1
- package/dist/archive-zip-preflight.js +12 -1
- package/dist/archive.d.ts.map +1 -1
- package/dist/archive.js +9 -5
- package/dist/bounded-read-stream.d.ts.map +1 -1
- package/dist/bounded-read-stream.js +13 -2
- package/dist/deny-mutations.d.ts +1 -0
- package/dist/deny-mutations.d.ts.map +1 -1
- package/dist/deny-mutations.js +6 -39
- package/dist/device-path.d.ts +1 -0
- package/dist/device-path.d.ts.map +1 -1
- package/dist/device-path.js +6 -6
- package/dist/directory-guard.d.ts.map +1 -1
- package/dist/directory-guard.js +5 -4
- package/dist/error-detail.d.ts +2 -0
- package/dist/error-detail.d.ts.map +1 -0
- package/dist/error-detail.js +4 -0
- package/dist/errors.d.ts +1 -1
- package/dist/errors.d.ts.map +1 -1
- package/dist/errors.js +4 -0
- package/dist/file-hash.d.ts.map +1 -1
- package/dist/file-hash.js +4 -1
- package/dist/file-lock-sync.d.ts.map +1 -1
- package/dist/file-lock-sync.js +88 -35
- package/dist/file-store-boundary.d.ts +6 -0
- package/dist/file-store-boundary.d.ts.map +1 -1
- package/dist/file-store-boundary.js +20 -5
- package/dist/file-store-sync-write.d.ts +9 -0
- package/dist/file-store-sync-write.d.ts.map +1 -0
- package/dist/file-store-sync-write.js +97 -0
- package/dist/file-store.d.ts.map +1 -1
- package/dist/file-store.js +62 -139
- package/dist/filename.d.ts.map +1 -1
- package/dist/filename.js +27 -0
- package/dist/guarded-mkdir.d.ts.map +1 -1
- package/dist/guarded-mkdir.js +5 -6
- package/dist/install-path.js +1 -1
- package/dist/json-document-store.d.ts.map +1 -1
- package/dist/json-document-store.js +19 -13
- package/dist/json.d.ts.map +1 -1
- package/dist/json.js +1 -3
- package/dist/local-file-access.d.ts +3 -2
- package/dist/local-file-access.d.ts.map +1 -1
- package/dist/local-file-access.js +12 -6
- package/dist/local-roots.d.ts.map +1 -1
- package/dist/local-roots.js +35 -30
- package/dist/move-path.d.ts.map +1 -1
- package/dist/move-path.js +51 -12
- package/dist/native/darwin-arm64/fs-safe-native.node +0 -0
- package/dist/native/darwin-x64/fs-safe-native.node +0 -0
- package/dist/native/linux-arm64-gnu/fs-safe-native.node +0 -0
- package/dist/native/linux-arm64-musl/fs-safe-native.node +0 -0
- package/dist/native/linux-x64-gnu/fs-safe-native.node +0 -0
- package/dist/native/linux-x64-musl/fs-safe-native.node +0 -0
- package/dist/native/win32-x64-msvc/fs-safe-native.node +0 -0
- package/dist/native-binding.d.ts +1 -0
- package/dist/native-binding.d.ts.map +1 -1
- package/dist/native-operations.d.ts.map +1 -1
- package/dist/native-operations.js +14 -1
- package/dist/native-pinned-write.d.ts.map +1 -1
- package/dist/native-pinned-write.js +44 -17
- package/dist/output-sibling.d.ts.map +1 -1
- package/dist/output-sibling.js +6 -5
- package/dist/path-policy.d.ts.map +1 -1
- package/dist/path-policy.js +1 -7
- package/dist/path.d.ts.map +1 -1
- package/dist/path.js +4 -0
- package/dist/permission-exec.d.ts +6 -0
- package/dist/permission-exec.d.ts.map +1 -0
- package/dist/permission-exec.js +28 -0
- package/dist/permissions-windows.d.ts +56 -0
- package/dist/permissions-windows.d.ts.map +1 -0
- package/dist/permissions-windows.js +285 -0
- package/dist/permissions.d.ts +3 -46
- package/dist/permissions.d.ts.map +1 -1
- package/dist/permissions.js +17 -328
- package/dist/pinned-open.d.ts.map +1 -1
- package/dist/pinned-open.js +21 -4
- package/dist/pinned-write.js +2 -1
- package/dist/private-temp-workspace.d.ts.map +1 -1
- package/dist/private-temp-workspace.js +45 -4
- package/dist/read-error.d.ts +2 -0
- package/dist/read-error.d.ts.map +1 -0
- package/dist/read-error.js +11 -0
- package/dist/regular-file.d.ts.map +1 -1
- package/dist/regular-file.js +21 -2
- package/dist/replace-directory.d.ts.map +1 -1
- package/dist/replace-directory.js +10 -1
- package/dist/replace-file-copy-fallback.d.ts +3 -0
- package/dist/replace-file-copy-fallback.d.ts.map +1 -1
- package/dist/replace-file-copy-fallback.js +18 -11
- package/dist/replace-file-descriptor.d.ts +33 -0
- package/dist/replace-file-descriptor.d.ts.map +1 -0
- package/dist/replace-file-descriptor.js +96 -0
- package/dist/replace-file.d.ts +9 -2
- package/dist/replace-file.d.ts.map +1 -1
- package/dist/replace-file.js +64 -72
- package/dist/root-context.d.ts +8 -0
- package/dist/root-context.d.ts.map +1 -1
- package/dist/root-context.js +43 -2
- package/dist/root-errors.d.ts +7 -0
- package/dist/root-errors.d.ts.map +1 -1
- package/dist/root-errors.js +46 -1
- package/dist/root-file.d.ts +1 -0
- package/dist/root-file.d.ts.map +1 -1
- package/dist/root-file.js +2 -0
- package/dist/root-impl.d.ts.map +1 -1
- package/dist/root-impl.js +216 -169
- package/dist/root-path-symlink.d.ts +3 -0
- package/dist/root-path-symlink.d.ts.map +1 -0
- package/dist/root-path-symlink.js +35 -0
- package/dist/root-path.d.ts +1 -0
- package/dist/root-path.d.ts.map +1 -1
- package/dist/root-path.js +129 -190
- package/dist/root-walk.d.ts.map +1 -1
- package/dist/root-walk.js +15 -0
- package/dist/safe-path-segment.d.ts +2 -0
- package/dist/safe-path-segment.d.ts.map +1 -1
- package/dist/safe-path-segment.js +12 -0
- package/dist/secret-file.d.ts.map +1 -1
- package/dist/secret-file.js +29 -6
- package/dist/secret-read-async.d.ts.map +1 -1
- package/dist/secret-read-async.js +5 -1
- package/dist/secure-temp-dir.d.ts.map +1 -1
- package/dist/secure-temp-dir.js +6 -1
- package/dist/short-path.d.ts +2 -0
- package/dist/short-path.d.ts.map +1 -0
- package/dist/short-path.js +7 -0
- package/dist/sidecar-lock-acquire.d.ts +30 -0
- package/dist/sidecar-lock-acquire.d.ts.map +1 -0
- package/dist/sidecar-lock-acquire.js +260 -0
- package/dist/sidecar-lock-policy.d.ts +3 -0
- package/dist/sidecar-lock-policy.d.ts.map +1 -1
- package/dist/sidecar-lock-policy.js +20 -3
- package/dist/sidecar-lock-reclaim.d.ts +5 -1
- package/dist/sidecar-lock-reclaim.d.ts.map +1 -1
- package/dist/sidecar-lock-reclaim.js +59 -9
- package/dist/sidecar-lock.d.ts.map +1 -1
- package/dist/sidecar-lock.js +27 -226
- package/dist/symlink-parents.d.ts.map +1 -1
- package/dist/symlink-parents.js +9 -8
- package/dist/temp-target.d.ts.map +1 -1
- package/dist/temp-target.js +15 -4
- package/dist/test-hooks.d.ts +1 -0
- package/dist/test-hooks.d.ts.map +1 -1
- package/dist/timing.d.ts +2 -0
- package/dist/timing.d.ts.map +1 -1
- package/dist/timing.js +9 -0
- package/dist/trash.d.ts.map +1 -1
- package/dist/trash.js +10 -1
- package/dist/walk.d.ts.map +1 -1
- package/dist/walk.js +15 -0
- package/docs/advanced.md +7 -1
- package/docs/archive.md +27 -20
- package/docs/assets/readme-banner.jpg +0 -0
- package/docs/atomic.md +48 -19
- package/docs/errors.md +34 -8
- package/docs/file-store.md +14 -1
- package/docs/filename.md +17 -16
- package/docs/index.md +1 -0
- package/docs/install-path.md +9 -6
- package/docs/json.md +8 -2
- package/docs/local-roots.md +79 -103
- package/docs/native-helper.md +11 -5
- package/docs/native.md +2 -2
- package/docs/output.md +8 -3
- package/docs/path-scope.md +4 -1
- package/docs/path.md +7 -3
- package/docs/permissions.md +8 -0
- package/docs/public-api.md +109 -0
- package/docs/quickstart.md +6 -2
- package/docs/reading.md +13 -10
- package/docs/root.md +26 -4
- package/docs/secret-file.md +21 -4
- package/docs/security-model.md +21 -4
- package/docs/sidecar-lock.md +15 -5
- package/docs/temp.md +42 -5
- package/docs/test-hooks.md +2 -0
- package/docs/types.md +32 -15
- package/docs/writing.md +25 -12
- package/package.json +15 -4
- package/dist/archive-file-io.d.ts +0 -9
- package/dist/archive-file-io.d.ts.map +0 -1
- package/dist/archive-file-io.js +0 -11
- package/dist/file-store-source.d.ts +0 -5
- package/dist/file-store-source.d.ts.map +0 -1
- package/dist/file-store-source.js +0 -30
- package/dist/file-url.d.ts +0 -2
- package/dist/file-url.d.ts.map +0 -1
- package/dist/file-url.js +0 -1
- package/dist/path-stat.d.ts +0 -4
- package/dist/path-stat.d.ts.map +0 -1
- package/dist/path-stat.js +0 -15
- package/dist/windows-permissions-native.d.ts +0 -9
- package/dist/windows-permissions-native.d.ts.map +0 -1
- package/dist/windows-permissions-native.js +0 -31
package/dist/deny-mutations.js
CHANGED
|
@@ -1,41 +1,8 @@
|
|
|
1
|
-
import fs from "node:fs/promises";
|
|
2
1
|
import path from "node:path";
|
|
3
2
|
import { FsSafeError } from "./errors.js";
|
|
4
|
-
import { assertNoNulPathInput,
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
await fs.lstat(filePath);
|
|
8
|
-
return true;
|
|
9
|
-
}
|
|
10
|
-
catch (err) {
|
|
11
|
-
if (!isNotFoundPathError(err)) {
|
|
12
|
-
throw err;
|
|
13
|
-
}
|
|
14
|
-
return false;
|
|
15
|
-
}
|
|
16
|
-
}
|
|
17
|
-
async function resolvePathViaExistingAncestor(targetPath) {
|
|
18
|
-
const normalized = path.resolve(targetPath);
|
|
19
|
-
let cursor = normalized;
|
|
20
|
-
const missingSuffix = [];
|
|
21
|
-
while (path.dirname(cursor) !== cursor && !(await pathExists(cursor))) {
|
|
22
|
-
missingSuffix.unshift(path.basename(cursor));
|
|
23
|
-
cursor = path.dirname(cursor);
|
|
24
|
-
}
|
|
25
|
-
if (!(await pathExists(cursor))) {
|
|
26
|
-
return normalized;
|
|
27
|
-
}
|
|
28
|
-
try {
|
|
29
|
-
const resolvedAncestor = path.resolve(await fs.realpath(cursor));
|
|
30
|
-
return missingSuffix.length === 0
|
|
31
|
-
? resolvedAncestor
|
|
32
|
-
: path.resolve(resolvedAncestor, ...missingSuffix);
|
|
33
|
-
}
|
|
34
|
-
catch {
|
|
35
|
-
return normalized;
|
|
36
|
-
}
|
|
37
|
-
}
|
|
38
|
-
async function comparablePaths(rawPath) {
|
|
3
|
+
import { assertNoNulPathInput, isPathInside } from "./path.js";
|
|
4
|
+
import { resolvePathViaExistingAncestor } from "./root-path-existing.js";
|
|
5
|
+
export async function resolveMutationComparablePaths(rawPath) {
|
|
39
6
|
assertNoNulPathInput(rawPath, "path contains a NUL byte");
|
|
40
7
|
const resolved = path.resolve(rawPath);
|
|
41
8
|
return new Set([resolved, await resolvePathViaExistingAncestor(resolved)]);
|
|
@@ -64,9 +31,9 @@ export async function assertMutationNotDenied(filePath, policy, options = {}) {
|
|
|
64
31
|
if (!hasPolicyEntries(policy)) {
|
|
65
32
|
return;
|
|
66
33
|
}
|
|
67
|
-
const targetPaths = await
|
|
34
|
+
const targetPaths = await resolveMutationComparablePaths(filePath);
|
|
68
35
|
for (const deniedPath of policyPathEntries(policy.paths)) {
|
|
69
|
-
const deniedPaths = await
|
|
36
|
+
const deniedPaths = await resolveMutationComparablePaths(deniedPath);
|
|
70
37
|
for (const target of targetPaths) {
|
|
71
38
|
for (const denied of deniedPaths) {
|
|
72
39
|
if (isSamePath(denied, target) ||
|
|
@@ -77,7 +44,7 @@ export async function assertMutationNotDenied(filePath, policy, options = {}) {
|
|
|
77
44
|
}
|
|
78
45
|
}
|
|
79
46
|
for (const deniedPrefix of policyPathEntries(policy.prefixes)) {
|
|
80
|
-
const deniedPaths = await
|
|
47
|
+
const deniedPaths = await resolveMutationComparablePaths(deniedPrefix);
|
|
81
48
|
for (const target of targetPaths) {
|
|
82
49
|
for (const denied of deniedPaths) {
|
|
83
50
|
if (isPathInside(denied, target) ||
|
package/dist/device-path.d.ts
CHANGED
|
@@ -7,6 +7,7 @@ export type UnsafeDeviceReadPathOptions = {
|
|
|
7
7
|
cwd?: string;
|
|
8
8
|
platform?: NodeJS.Platform;
|
|
9
9
|
};
|
|
10
|
+
export declare const WINDOWS_RESERVED_DEVICE_NAMES: ReadonlySet<string>;
|
|
10
11
|
export declare function matchUnsafeDeviceReadPath(filePath: string, options?: UnsafeDeviceReadPathOptions): UnsafeDeviceReadPathMatch | undefined;
|
|
11
12
|
export declare function isUnsafeDeviceReadPath(filePath: string, options?: UnsafeDeviceReadPathOptions): boolean;
|
|
12
13
|
export declare function assertNoUnsafeDeviceReadPath(filePath: string, options?: UnsafeDeviceReadPathOptions): void;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"device-path.d.ts","sourceRoot":"","sources":["../src/device-path.ts"],"names":[],"mappings":"AAIA,MAAM,MAAM,0BAA0B,GAClC,cAAc,GACd,UAAU,GACV,gBAAgB,CAAC;AAErB,MAAM,MAAM,yBAAyB,GAAG;IACtC,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,0BAA0B,CAAC;CACpC,CAAC;AAEF,MAAM,MAAM,2BAA2B,GAAG;IACxC,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,QAAQ,CAAC,EAAE,MAAM,CAAC,QAAQ,CAAC;CAC5B,CAAC;
|
|
1
|
+
{"version":3,"file":"device-path.d.ts","sourceRoot":"","sources":["../src/device-path.ts"],"names":[],"mappings":"AAIA,MAAM,MAAM,0BAA0B,GAClC,cAAc,GACd,UAAU,GACV,gBAAgB,CAAC;AAErB,MAAM,MAAM,yBAAyB,GAAG;IACtC,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,0BAA0B,CAAC;CACpC,CAAC;AAEF,MAAM,MAAM,2BAA2B,GAAG;IACxC,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,QAAQ,CAAC,EAAE,MAAM,CAAC,QAAQ,CAAC;CAC5B,CAAC;AAcF,eAAO,MAAM,6BAA6B,EAAE,WAAW,CAAC,MAAM,CAgC5D,CAAC;AAkFH,wBAAgB,yBAAyB,CACvC,QAAQ,EAAE,MAAM,EAChB,OAAO,GAAE,2BAAgC,GACxC,yBAAyB,GAAG,SAAS,CAWvC;AAED,wBAAgB,sBAAsB,CACpC,QAAQ,EAAE,MAAM,EAChB,OAAO,CAAC,EAAE,2BAA2B,GACpC,OAAO,CAET;AAED,wBAAgB,4BAA4B,CAC1C,QAAQ,EAAE,MAAM,EAChB,OAAO,CAAC,EAAE,2BAA2B,GACpC,IAAI,CAON"}
|
package/dist/device-path.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import path from "node:path";
|
|
2
2
|
import { FsSafeError } from "./errors.js";
|
|
3
|
-
import { trySafeFileURLToPath } from "./local-file-access.js";
|
|
3
|
+
import { isFileUrl, trySafeFileURLToPath } from "./local-file-access.js";
|
|
4
4
|
const POSIX_BLOCKED_DEVICE_PATHS = new Set([
|
|
5
5
|
"/dev/zero",
|
|
6
6
|
"/dev/random",
|
|
@@ -12,7 +12,7 @@ const POSIX_BLOCKED_DEVICE_PATHS = new Set([
|
|
|
12
12
|
"/dev/tty",
|
|
13
13
|
"/dev/console",
|
|
14
14
|
]);
|
|
15
|
-
const WINDOWS_RESERVED_DEVICE_NAMES = new Set([
|
|
15
|
+
export const WINDOWS_RESERVED_DEVICE_NAMES = new Set([
|
|
16
16
|
"CON",
|
|
17
17
|
"PRN",
|
|
18
18
|
"AUX",
|
|
@@ -67,11 +67,11 @@ function trimTrailingWindowsIgnoredChars(value) {
|
|
|
67
67
|
}
|
|
68
68
|
return end === value.length ? value : value.slice(0, end);
|
|
69
69
|
}
|
|
70
|
-
function candidateReadPaths(filePath) {
|
|
71
|
-
if (!filePath
|
|
70
|
+
function candidateReadPaths(filePath, platform) {
|
|
71
|
+
if (!isFileUrl(filePath)) {
|
|
72
72
|
return [filePath];
|
|
73
73
|
}
|
|
74
|
-
const parsed = trySafeFileURLToPath(filePath);
|
|
74
|
+
const parsed = trySafeFileURLToPath(filePath, platform);
|
|
75
75
|
return parsed === undefined ? [filePath] : [filePath, parsed];
|
|
76
76
|
}
|
|
77
77
|
function normalizePosixPath(filePath, cwd) {
|
|
@@ -114,7 +114,7 @@ function matchWindowsDeviceReadPath(filePath) {
|
|
|
114
114
|
}
|
|
115
115
|
export function matchUnsafeDeviceReadPath(filePath, options = {}) {
|
|
116
116
|
const platform = options.platform ?? process.platform;
|
|
117
|
-
for (const candidate of candidateReadPaths(filePath)) {
|
|
117
|
+
for (const candidate of candidateReadPaths(filePath, platform)) {
|
|
118
118
|
const match = platform === "win32"
|
|
119
119
|
? matchWindowsDeviceReadPath(candidate)
|
|
120
120
|
: matchPosixDeviceReadPath(candidate, options.cwd);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"directory-guard.d.ts","sourceRoot":"","sources":["../src/directory-guard.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,SAAS,CAAC;
|
|
1
|
+
{"version":3,"file":"directory-guard.d.ts","sourceRoot":"","sources":["../src/directory-guard.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,SAAS,CAAC;AASrC,MAAM,MAAM,mBAAmB,GAAG;IAChC,GAAG,EAAE,MAAM,CAAC;IACZ,QAAQ,EAAE,MAAM,CAAC;IACjB,IAAI,EAAE,KAAK,CAAC;CACb,CAAC;AAEF,MAAM,MAAM,kBAAkB,GAAG;IAC/B,GAAG,EAAE,MAAM,CAAC;IACZ,QAAQ,EAAE,MAAM,CAAC;IACjB,IAAI,EAAE,KAAK,CAAC;CACb,CAAC;AAEF,wBAAsB,yBAAyB,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,mBAAmB,CAAC,CAMzF;AAED,wBAAsB,yBAAyB,CAAC,KAAK,EAAE,mBAAmB,GAAG,OAAO,CAAC,IAAI,CAAC,CAQzF;AAED,wBAAgB,wBAAwB,CAAC,GAAG,EAAE,MAAM,GAAG,kBAAkB,CAMxE;AAED,wBAAgB,wBAAwB,CAAC,KAAK,EAAE,kBAAkB,GAAG,IAAI,CAQxE;AAED,wBAAsB,mCAAmC,CACvD,QAAQ,EAAE,MAAM,EAChB,UAAU,EAAE,MAAM,GACjB,OAAO,CAAC,mBAAmB,CAAC,CAc9B;AAED,wBAAgB,uCAAuC,CACrD,QAAQ,EAAE,MAAM,EAChB,UAAU,EAAE,MAAM,GACjB,kBAAkB,CAcpB"}
|
package/dist/directory-guard.js
CHANGED
|
@@ -4,17 +4,18 @@ import path from "node:path";
|
|
|
4
4
|
import { FsSafeError } from "./errors.js";
|
|
5
5
|
import { sameFileIdentity } from "./file-identity.js";
|
|
6
6
|
import { isNotFoundPathError } from "./path.js";
|
|
7
|
+
import { directoryComponentNotDirectoryError } from "./root-errors.js";
|
|
7
8
|
export async function createAsyncDirectoryGuard(dir) {
|
|
8
9
|
const stat = await fs.lstat(dir);
|
|
9
10
|
if (stat.isSymbolicLink() || !stat.isDirectory()) {
|
|
10
|
-
throw
|
|
11
|
+
throw directoryComponentNotDirectoryError();
|
|
11
12
|
}
|
|
12
13
|
return { dir, realPath: await fs.realpath(dir), stat };
|
|
13
14
|
}
|
|
14
15
|
export async function assertAsyncDirectoryGuard(guard) {
|
|
15
16
|
const stat = await fs.lstat(guard.dir);
|
|
16
17
|
if (stat.isSymbolicLink() || !stat.isDirectory()) {
|
|
17
|
-
throw
|
|
18
|
+
throw directoryComponentNotDirectoryError();
|
|
18
19
|
}
|
|
19
20
|
if (!sameFileIdentity(stat, guard.stat) || (await fs.realpath(guard.dir)) !== guard.realPath) {
|
|
20
21
|
throw new FsSafeError("path-mismatch", "directory changed during operation");
|
|
@@ -23,14 +24,14 @@ export async function assertAsyncDirectoryGuard(guard) {
|
|
|
23
24
|
export function createSyncDirectoryGuard(dir) {
|
|
24
25
|
const stat = fsSync.lstatSync(dir);
|
|
25
26
|
if (stat.isSymbolicLink() || !stat.isDirectory()) {
|
|
26
|
-
throw
|
|
27
|
+
throw directoryComponentNotDirectoryError();
|
|
27
28
|
}
|
|
28
29
|
return { dir, realPath: fsSync.realpathSync(dir), stat };
|
|
29
30
|
}
|
|
30
31
|
export function assertSyncDirectoryGuard(guard) {
|
|
31
32
|
const stat = fsSync.lstatSync(guard.dir);
|
|
32
33
|
if (stat.isSymbolicLink() || !stat.isDirectory()) {
|
|
33
|
-
throw
|
|
34
|
+
throw directoryComponentNotDirectoryError();
|
|
34
35
|
}
|
|
35
36
|
if (!sameFileIdentity(stat, guard.stat) || fsSync.realpathSync(guard.dir) !== guard.realPath) {
|
|
36
37
|
throw new FsSafeError("path-mismatch", "directory changed during operation");
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"error-detail.d.ts","sourceRoot":"","sources":["../src/error-detail.ts"],"names":[],"mappings":"AAGA,wBAAgB,iBAAiB,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAIvD"}
|
package/dist/errors.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export type FsSafeErrorCode = "already-exists" | "denied-path" | "device-path" | "hardlink" | "helper-failed" | "helper-unavailable" | "invalid-path" | "insecure-permissions" | "not-empty" | "not-file" | "not-found" | "not-owned" | "not-removable" | "outside-workspace" | "path-alias" | "path-mismatch" | "permission-unverified" | "secret-exists" | "store-reentrant-update" | "symlink" | "timeout" | "too-large" | "unsupported-platform";
|
|
1
|
+
export type FsSafeErrorCode = "already-exists" | "denied-path" | "device-path" | "hardlink" | "helper-failed" | "helper-unavailable" | "invalid-path" | "insecure-permissions" | "not-empty" | "not-file" | "not-found" | "not-owned" | "not-removable" | "outside-workspace" | "path-alias" | "path-mismatch" | "permission-unverified" | "read-failed" | "secret-exists" | "store-reentrant-update" | "symlink" | "timeout" | "too-large" | "unsupported-platform";
|
|
2
2
|
export type FsSafeErrorCategory = "policy" | "operational";
|
|
3
3
|
export type FsSafeErrorDetails = Readonly<Record<string, unknown>>;
|
|
4
4
|
export declare function categorizeFsSafeError(code: FsSafeErrorCode): FsSafeErrorCategory;
|
package/dist/errors.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"errors.d.ts","sourceRoot":"","sources":["../src/errors.ts"],"names":[],"mappings":"AAAA,MAAM,MAAM,eAAe,GACvB,gBAAgB,GAChB,aAAa,GACb,aAAa,GACb,UAAU,GACV,eAAe,GACf,oBAAoB,GACpB,cAAc,GACd,sBAAsB,GACtB,WAAW,GACX,UAAU,GACV,WAAW,GACX,WAAW,GACX,eAAe,GACf,mBAAmB,GACnB,YAAY,GACZ,eAAe,GACf,uBAAuB,GACvB,eAAe,GACf,wBAAwB,GACxB,SAAS,GACT,SAAS,GACT,WAAW,GACX,sBAAsB,CAAC;AAE3B,MAAM,MAAM,mBAAmB,GAAG,QAAQ,GAAG,aAAa,CAAC;AAC3D,MAAM,MAAM,kBAAkB,GAAG,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,CAAC;
|
|
1
|
+
{"version":3,"file":"errors.d.ts","sourceRoot":"","sources":["../src/errors.ts"],"names":[],"mappings":"AAAA,MAAM,MAAM,eAAe,GACvB,gBAAgB,GAChB,aAAa,GACb,aAAa,GACb,UAAU,GACV,eAAe,GACf,oBAAoB,GACpB,cAAc,GACd,sBAAsB,GACtB,WAAW,GACX,UAAU,GACV,WAAW,GACX,WAAW,GACX,eAAe,GACf,mBAAmB,GACnB,YAAY,GACZ,eAAe,GACf,uBAAuB,GACvB,aAAa,GACb,eAAe,GACf,wBAAwB,GACxB,SAAS,GACT,SAAS,GACT,WAAW,GACX,sBAAsB,CAAC;AAE3B,MAAM,MAAM,mBAAmB,GAAG,QAAQ,GAAG,aAAa,CAAC;AAC3D,MAAM,MAAM,kBAAkB,GAAG,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,CAAC;AAcnE,wBAAgB,qBAAqB,CAAC,IAAI,EAAE,eAAe,GAAG,mBAAmB,CAEhF;AAED,qBAAa,WAAY,SAAQ,KAAK;IACpC,QAAQ,CAAC,IAAI,EAAE,eAAe,CAAC;IAC/B,QAAQ,CAAC,QAAQ,EAAE,mBAAmB,CAAC;IACvC,QAAQ,CAAC,OAAO,CAAC,EAAE,kBAAkB,CAAC;IAEtC,YACE,IAAI,EAAE,eAAe,EACrB,OAAO,EAAE,MAAM,EACf,OAAO,GAAE;QAAE,KAAK,CAAC,EAAE,OAAO,CAAC;QAAC,OAAO,CAAC,EAAE,kBAAkB,CAAA;KAAO,EAOhE;CACF"}
|
package/dist/errors.js
CHANGED
package/dist/file-hash.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"file-hash.d.ts","sourceRoot":"","sources":["../src/file-hash.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,kBAAkB,CAAC;AAInD,OAAO,EAAoB,KAAK,aAAa,EAAE,MAAM,aAAa,CAAC;AAEnE,MAAM,MAAM,eAAe,GAAG,MAAM,GAAG,UAAU,CAAC;AAElD,MAAM,MAAM,gBAAgB,GAAG;IAC7B,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,MAAM,CAAC;CAChB,CAAC;AAcF,wBAAsB,cAAc,CAClC,MAAM,EAAE,UAAU,EAClB,MAAM,GAAE,aAAa,GAAG,SAA8B,GACrD,OAAO,CAAC,gBAAgB,CAAC,CAoB3B;
|
|
1
|
+
{"version":3,"file":"file-hash.d.ts","sourceRoot":"","sources":["../src/file-hash.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,kBAAkB,CAAC;AAInD,OAAO,EAAoB,KAAK,aAAa,EAAE,MAAM,aAAa,CAAC;AAEnE,MAAM,MAAM,eAAe,GAAG,MAAM,GAAG,UAAU,CAAC;AAElD,MAAM,MAAM,gBAAgB,GAAG;IAC7B,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,MAAM,CAAC;CAChB,CAAC;AAcF,wBAAsB,cAAc,CAClC,MAAM,EAAE,UAAU,EAClB,MAAM,GAAE,aAAa,GAAG,SAA8B,GACrD,OAAO,CAAC,gBAAgB,CAAC,CAoB3B;AA2CD,wBAAsB,UAAU,CAAC,KAAK,EAAE,eAAe,GAAG,OAAO,CAAC,gBAAgB,CAAC,CAElF"}
|
package/dist/file-hash.js
CHANGED
|
@@ -59,7 +59,10 @@ async function hashPath(filePath) {
|
|
|
59
59
|
if (!opened.isFile()) {
|
|
60
60
|
throw new FsSafeError("not-file", "SHA-256 path is not a regular file");
|
|
61
61
|
}
|
|
62
|
-
if (current.isSymbolicLink() ||
|
|
62
|
+
if (current.isSymbolicLink() ||
|
|
63
|
+
!current.isFile() ||
|
|
64
|
+
!sameFileIdentity(before, opened) ||
|
|
65
|
+
!sameFileIdentity(opened, current)) {
|
|
63
66
|
throw new FsSafeError("path-mismatch", "SHA-256 path changed while opening");
|
|
64
67
|
}
|
|
65
68
|
return await hashFileHandle(handle);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"file-lock-sync.d.ts","sourceRoot":"","sources":["../src/file-lock-sync.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,gBAAgB,CAAC;AAC3C,OAAO,EAOL,KAAK,wBAAwB,EAC9B,MAAM,2BAA2B,CAAC;AAKnC,OAAO,KAAK,EACV,0BAA0B,EAC1B,uBAAuB,EACvB,wBAAwB,EACzB,MAAM,yBAAyB,CAAC;
|
|
1
|
+
{"version":3,"file":"file-lock-sync.d.ts","sourceRoot":"","sources":["../src/file-lock-sync.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,gBAAgB,CAAC;AAC3C,OAAO,EAOL,KAAK,wBAAwB,EAC9B,MAAM,2BAA2B,CAAC;AAKnC,OAAO,KAAK,EACV,0BAA0B,EAC1B,uBAAuB,EACvB,wBAAwB,EACzB,MAAM,yBAAyB,CAAC;AAGjC,MAAM,MAAM,0BAA0B,CAAC,QAAQ,SAAS,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,IAAI;IACjF,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,KAAK,CAAC,EAAE,uBAAuB,CAAC;IAChC,aAAa,CAAC,EAAE,wBAAwB,CAAC;IACzC,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,OAAO,EAAE,MAAM,QAAQ,CAAC;IACxB,aAAa,CAAC,EAAE,CAAC,MAAM,EAAE;QACvB,QAAQ,EAAE,MAAM,CAAC;QACjB,oBAAoB,EAAE,MAAM,CAAC;QAC7B,OAAO,EAAE,OAAO,CAAC;QACjB,OAAO,EAAE,MAAM,CAAC;QAChB,KAAK,EAAE,MAAM,CAAC;QACd,iBAAiB,EAAE,KAAK,CAAC;KAC1B,KAAK,OAAO,CAAC;IACd,qBAAqB,CAAC,EAAE,CAAC,QAAQ,EAAE,wBAAwB,KAAK,OAAO,CAAC;IACxE,YAAY,CAAC,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,OAAO,CAAC;IACxC,QAAQ,CAAC,EAAE,IAAI,CAAC;IAChB,aAAa,CAAC,EAAE,CAAC,IAAI,EAAE,0BAA0B,KAAK,IAAI,CAAC;IAC3D,yBAAyB,CAAC,EAAE,MAAM,CAAC;CACpC,CAAC;AAEF,MAAM,MAAM,kBAAkB,GAAG;IAC/B,QAAQ,EAAE,MAAM,CAAC;IACjB,oBAAoB,EAAE,MAAM,CAAC;IAC7B,eAAe,IAAI,OAAO,CAAC;IAC3B,OAAO,IAAI,IAAI,CAAC;IAChB,CAAC,MAAM,CAAC,OAAO,CAAC,IAAI,IAAI,CAAC;CAC1B,CAAC;AA0IF,wBAAgB,mBAAmB,CAAC,QAAQ,SAAS,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAC1E,UAAU,EAAE,MAAM,EAClB,OAAO,EAAE,0BAA0B,CAAC,QAAQ,CAAC,GAC5C,kBAAkB,CAuJpB;AAED,wBAAgB,gBAAgB,CAAC,CAAC,EAAE,QAAQ,SAAS,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAC1E,UAAU,EAAE,MAAM,EAClB,OAAO,EAAE,0BAA0B,CAAC,QAAQ,CAAC,EAC7C,EAAE,EAAE,MAAM,CAAC,GACV,CAAC,CAOH"}
|
package/dist/file-lock-sync.js
CHANGED
|
@@ -2,8 +2,11 @@ import fs from "node:fs";
|
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
import { FsSafeError } from "./errors.js";
|
|
4
4
|
import { readSidecarLockSnapshotSync, relativeSidecarLockPath, removeSidecarLockIfUnchangedSync, serializeSidecarLockPayload, sidecarLockSnapshotMatches, } from "./sidecar-lock-reclaim.js";
|
|
5
|
-
import { computeSidecarLockDelayMs,
|
|
5
|
+
import { computeSidecarLockDelayMs, sidecarLockPayloadCreatedAtMs, } from "./sidecar-lock-policy.js";
|
|
6
|
+
import { sleepSync } from "./timing.js";
|
|
6
7
|
const SYNC_HELD_LOCKS_KEY = Symbol.for("fsSafe.syncSidecarLocks");
|
|
8
|
+
const SYNC_CLEANUP_REGISTERED_KEY = Symbol.for("fsSafe.syncSidecarLockCleanupRegistered");
|
|
9
|
+
const SYNC_CLEANUP_HANDLER_KEY = Symbol.for("fsSafe.syncSidecarLockCleanupHandler");
|
|
7
10
|
function getSyncHeldLocks() {
|
|
8
11
|
const globalWithState = globalThis;
|
|
9
12
|
if (!globalWithState[SYNC_HELD_LOCKS_KEY]) {
|
|
@@ -11,6 +14,36 @@ function getSyncHeldLocks() {
|
|
|
11
14
|
}
|
|
12
15
|
return globalWithState[SYNC_HELD_LOCKS_KEY];
|
|
13
16
|
}
|
|
17
|
+
function releaseAllSyncHeldLocks() {
|
|
18
|
+
const heldLocks = getSyncHeldLocks();
|
|
19
|
+
for (const [normalizedTargetPath, held] of heldLocks) {
|
|
20
|
+
if (held.timer) {
|
|
21
|
+
clearInterval(held.timer);
|
|
22
|
+
held.timer = undefined;
|
|
23
|
+
}
|
|
24
|
+
try {
|
|
25
|
+
fs.closeSync(held.fd);
|
|
26
|
+
}
|
|
27
|
+
catch {
|
|
28
|
+
// Best-effort process-exit cleanup.
|
|
29
|
+
}
|
|
30
|
+
try {
|
|
31
|
+
removeSidecarLockIfUnchangedSync(held.lockPath, held.snapshot);
|
|
32
|
+
}
|
|
33
|
+
catch {
|
|
34
|
+
// A surviving sidecar fails closed and can be reclaimed by policy.
|
|
35
|
+
}
|
|
36
|
+
heldLocks.delete(normalizedTargetPath);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
function ensureSyncExitCleanupRegistered() {
|
|
40
|
+
const globalWithCleanup = globalThis;
|
|
41
|
+
if (globalWithCleanup[SYNC_CLEANUP_REGISTERED_KEY])
|
|
42
|
+
return;
|
|
43
|
+
globalWithCleanup[SYNC_CLEANUP_REGISTERED_KEY] = true;
|
|
44
|
+
globalWithCleanup[SYNC_CLEANUP_HANDLER_KEY] = releaseAllSyncHeldLocks;
|
|
45
|
+
process.on("exit", releaseAllSyncHeldLocks);
|
|
46
|
+
}
|
|
14
47
|
function verifySyncHeldLock(held) {
|
|
15
48
|
const current = readSidecarLockSnapshotSync(held.lockPath, held.parsePayload);
|
|
16
49
|
return !!current && sidecarLockSnapshotMatches(current, held.snapshot);
|
|
@@ -71,8 +104,9 @@ function boundedLockPath(lockPath, lockRoot) {
|
|
|
71
104
|
return path.join(parentReal, path.basename(resolved));
|
|
72
105
|
}
|
|
73
106
|
function defaultShouldReclaim(payload, lockPath, staleMs, nowMs) {
|
|
74
|
-
|
|
75
|
-
|
|
107
|
+
const createdAtMs = sidecarLockPayloadCreatedAtMs(payload);
|
|
108
|
+
if (createdAtMs !== null)
|
|
109
|
+
return nowMs - createdAtMs > staleMs;
|
|
76
110
|
try {
|
|
77
111
|
return nowMs - fs.statSync(lockPath).mtimeMs > staleMs;
|
|
78
112
|
}
|
|
@@ -80,8 +114,16 @@ function defaultShouldReclaim(payload, lockPath, staleMs, nowMs) {
|
|
|
80
114
|
return true;
|
|
81
115
|
}
|
|
82
116
|
}
|
|
83
|
-
function
|
|
84
|
-
|
|
117
|
+
function reclaimGuardExists(reclaimGuardPath) {
|
|
118
|
+
try {
|
|
119
|
+
fs.lstatSync(reclaimGuardPath);
|
|
120
|
+
return true;
|
|
121
|
+
}
|
|
122
|
+
catch (error) {
|
|
123
|
+
if (error.code === "ENOENT")
|
|
124
|
+
return false;
|
|
125
|
+
throw error;
|
|
126
|
+
}
|
|
85
127
|
}
|
|
86
128
|
export function acquireFileLockSync(targetPath, options) {
|
|
87
129
|
const normalizedTargetPath = normalizeTargetPath(targetPath);
|
|
@@ -99,7 +141,25 @@ export function acquireFileLockSync(targetPath, options) {
|
|
|
99
141
|
const retry = options.retry ?? {};
|
|
100
142
|
const startedAt = Date.now();
|
|
101
143
|
let attempt = 0;
|
|
144
|
+
const reclaimGuardPath = `${lockPath}.reclaim`;
|
|
145
|
+
const waitForRetry = () => {
|
|
146
|
+
const elapsed = Date.now() - startedAt;
|
|
147
|
+
const timedOut = options.timeoutMs !== undefined && elapsed >= options.timeoutMs;
|
|
148
|
+
if (timedOut || (retry.retries !== undefined && attempt >= retry.retries)) {
|
|
149
|
+
throw Object.assign(new Error(`file lock timeout for ${normalizedTargetPath}`), {
|
|
150
|
+
code: "file_lock_timeout",
|
|
151
|
+
lockPath,
|
|
152
|
+
normalizedTargetPath,
|
|
153
|
+
});
|
|
154
|
+
}
|
|
155
|
+
sleepSync(computeSidecarLockDelayMs(retry, attempt));
|
|
156
|
+
attempt += 1;
|
|
157
|
+
};
|
|
102
158
|
while (true) {
|
|
159
|
+
if (reclaimGuardExists(reclaimGuardPath)) {
|
|
160
|
+
waitForRetry();
|
|
161
|
+
continue;
|
|
162
|
+
}
|
|
103
163
|
let fd;
|
|
104
164
|
try {
|
|
105
165
|
const payload = options.payload();
|
|
@@ -126,6 +186,7 @@ export function acquireFileLockSync(targetPath, options) {
|
|
|
126
186
|
snapshot,
|
|
127
187
|
};
|
|
128
188
|
heldLocks.set(normalizedTargetPath, createdHeld);
|
|
189
|
+
ensureSyncExitCleanupRegistered();
|
|
129
190
|
const returnedHandle = createSyncHeldLockHandle(createdHeld);
|
|
130
191
|
if (options.onCompromised && (options.compromiseCheckIntervalMs ?? 0) > 0) {
|
|
131
192
|
createdHeld.timer = setInterval(() => {
|
|
@@ -151,20 +212,12 @@ export function acquireFileLockSync(targetPath, options) {
|
|
|
151
212
|
if (error.code !== "EEXIST")
|
|
152
213
|
throw error;
|
|
153
214
|
if (heldLocks.has(normalizedTargetPath)) {
|
|
154
|
-
|
|
155
|
-
const timedOut = options.timeoutMs !== undefined && elapsed >= options.timeoutMs;
|
|
156
|
-
if (timedOut || (retry.retries !== undefined && attempt >= retry.retries)) {
|
|
157
|
-
throw Object.assign(new Error(`file lock timeout for ${normalizedTargetPath}`), {
|
|
158
|
-
code: "file_lock_timeout",
|
|
159
|
-
lockPath,
|
|
160
|
-
normalizedTargetPath,
|
|
161
|
-
});
|
|
162
|
-
}
|
|
163
|
-
sleep(computeSidecarLockDelayMs(retry, attempt));
|
|
164
|
-
attempt += 1;
|
|
215
|
+
waitForRetry();
|
|
165
216
|
continue;
|
|
166
217
|
}
|
|
167
|
-
const snapshot = readSidecarLockSnapshotSync(lockPath, options.parsePayload
|
|
218
|
+
const snapshot = readSidecarLockSnapshotSync(lockPath, options.parsePayload, {
|
|
219
|
+
rejectNonFile: true,
|
|
220
|
+
});
|
|
168
221
|
if (!snapshot)
|
|
169
222
|
continue;
|
|
170
223
|
const nowMs = Date.now();
|
|
@@ -187,18 +240,28 @@ export function acquireFileLockSync(targetPath, options) {
|
|
|
187
240
|
raw: snapshot.raw,
|
|
188
241
|
payload: snapshot.payload,
|
|
189
242
|
})) {
|
|
190
|
-
|
|
243
|
+
let ownsReclaimGuard = false;
|
|
191
244
|
try {
|
|
192
|
-
fs.mkdirSync(
|
|
245
|
+
fs.mkdirSync(reclaimGuardPath);
|
|
246
|
+
ownsReclaimGuard = true;
|
|
193
247
|
if (removeSidecarLockIfUnchangedSync(lockPath, snapshot))
|
|
194
248
|
continue;
|
|
195
249
|
}
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
250
|
+
catch (reclaimError) {
|
|
251
|
+
if (reclaimError.code !== "EEXIST") {
|
|
252
|
+
throw reclaimError;
|
|
199
253
|
}
|
|
200
|
-
|
|
201
|
-
|
|
254
|
+
waitForRetry();
|
|
255
|
+
continue;
|
|
256
|
+
}
|
|
257
|
+
finally {
|
|
258
|
+
if (ownsReclaimGuard) {
|
|
259
|
+
try {
|
|
260
|
+
fs.rmdirSync(reclaimGuardPath);
|
|
261
|
+
}
|
|
262
|
+
catch {
|
|
263
|
+
// A surviving reclaim guard fails closed.
|
|
264
|
+
}
|
|
202
265
|
}
|
|
203
266
|
}
|
|
204
267
|
}
|
|
@@ -208,17 +271,7 @@ export function acquireFileLockSync(targetPath, options) {
|
|
|
208
271
|
normalizedTargetPath,
|
|
209
272
|
});
|
|
210
273
|
}
|
|
211
|
-
|
|
212
|
-
const timedOut = options.timeoutMs !== undefined && elapsed >= options.timeoutMs;
|
|
213
|
-
if (timedOut || (retry.retries !== undefined && attempt >= retry.retries)) {
|
|
214
|
-
throw Object.assign(new Error(`file lock timeout for ${normalizedTargetPath}`), {
|
|
215
|
-
code: "file_lock_timeout",
|
|
216
|
-
lockPath,
|
|
217
|
-
normalizedTargetPath,
|
|
218
|
-
});
|
|
219
|
-
}
|
|
220
|
-
sleep(computeSidecarLockDelayMs(retry, attempt));
|
|
221
|
-
attempt += 1;
|
|
274
|
+
waitForRetry();
|
|
222
275
|
}
|
|
223
276
|
}
|
|
224
277
|
}
|
|
@@ -22,4 +22,10 @@ export declare function ensureParentSync(params: {
|
|
|
22
22
|
filePath: string;
|
|
23
23
|
mode: number;
|
|
24
24
|
}): SyncParentGuard;
|
|
25
|
+
export declare function ensureStoreDirectorySync(params: {
|
|
26
|
+
rootDir: string;
|
|
27
|
+
targetDir: string;
|
|
28
|
+
mode: number;
|
|
29
|
+
messagePrefix: "private store" | "store";
|
|
30
|
+
}): SyncParentGuard;
|
|
25
31
|
//# sourceMappingURL=file-store-boundary.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"file-store-boundary.d.ts","sourceRoot":"","sources":["../src/file-store-boundary.ts"],"names":[],"mappings":"AAGA,OAAO,EAAa,KAAK,QAAQ,EAAE,MAAM,aAAa,CAAC;AAEvD,OAAO,EAGL,KAAK,kBAAkB,EACxB,MAAM,sBAAsB,CAAC;
|
|
1
|
+
{"version":3,"file":"file-store-boundary.d.ts","sourceRoot":"","sources":["../src/file-store-boundary.ts"],"names":[],"mappings":"AAGA,OAAO,EAAa,KAAK,QAAQ,EAAE,MAAM,aAAa,CAAC;AAEvD,OAAO,EAGL,KAAK,kBAAkB,EACxB,MAAM,sBAAsB,CAAC;AAI9B,OAAO,EAA4C,KAAK,IAAI,EAAE,MAAM,WAAW,CAAC;AAGhF,MAAM,MAAM,eAAe,GAAG,kBAAkB,CAAC;AAOjD,wBAAsB,kBAAkB,CACtC,UAAU,EAAE,IAAI,EAChB,YAAY,EAAE,MAAM,EACpB,IAAI,EAAE,MAAM,GACX,OAAO,CAAC,IAAI,CAAC,CAOf;AAED,wBAAsB,qBAAqB,CAAC,MAAM,EAAE;IAClD,OAAO,EAAE,MAAM,CAAC;IAChB,OAAO,EAAE,MAAM,CAAC;IAChB,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB,GAAG,OAAO,CAAC,IAAI,CAAC,CAIhB;AA+CD,wBAAsB,uBAAuB,CAAC,MAAM,EAAE;IACpD,MAAM,EAAE,QAAQ,CAAC;IACjB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,IAAI,EAAE,MAAM,CAAC;CACd,GAAG,OAAO,CAAC;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAA;CAAE,CAAC,CAuC1D;AAED,wBAAgB,wBAAwB,CAAC,KAAK,EAAE,eAAe,GAAG,IAAI,CAWrE;AAUD,wBAAgB,gBAAgB,CAAC,MAAM,EAAE;IACvC,OAAO,EAAE,MAAM,CAAC;IAChB,QAAQ,EAAE,MAAM,CAAC;IACjB,IAAI,EAAE,MAAM,CAAC;CACd,GAAG,eAAe,CAOlB;AAED,wBAAgB,wBAAwB,CAAC,MAAM,EAAE;IAC/C,OAAO,EAAE,MAAM,CAAC;IAChB,SAAS,EAAE,MAAM,CAAC;IAClB,IAAI,EAAE,MAAM,CAAC;IACb,aAAa,EAAE,eAAe,GAAG,OAAO,CAAC;CAC1C,GAAG,eAAe,CAyDlB"}
|
|
@@ -5,6 +5,7 @@ import { Transform } from "node:stream";
|
|
|
5
5
|
import { pipeline } from "node:stream/promises";
|
|
6
6
|
import { assertSyncDirectoryGuard as assertDirectoryGuardSync, createSyncDirectoryGuard, } from "./directory-guard.js";
|
|
7
7
|
import { FsSafeError } from "./errors.js";
|
|
8
|
+
import { sameFileIdentity } from "./file-identity.js";
|
|
8
9
|
import { isPathInside, isPathRelativeEscape } from "./path.js";
|
|
9
10
|
import { resolveOpenedFileRealPathForHandle, root } from "./root.js";
|
|
10
11
|
import { resolveSecureTempRoot } from "./secure-temp-dir.js";
|
|
@@ -128,8 +129,16 @@ function chmodDirectorySyncBestEffort(dir, mode) {
|
|
|
128
129
|
}
|
|
129
130
|
}
|
|
130
131
|
export function ensureParentSync(params) {
|
|
132
|
+
return ensureStoreDirectorySync({
|
|
133
|
+
rootDir: params.rootDir,
|
|
134
|
+
targetDir: path.dirname(path.resolve(params.filePath)),
|
|
135
|
+
mode: params.mode,
|
|
136
|
+
messagePrefix: "store",
|
|
137
|
+
});
|
|
138
|
+
}
|
|
139
|
+
export function ensureStoreDirectorySync(params) {
|
|
131
140
|
const rootDir = path.resolve(params.rootDir);
|
|
132
|
-
const dir = path.
|
|
141
|
+
const dir = path.resolve(params.targetDir);
|
|
133
142
|
const relative = path.relative(rootDir, dir);
|
|
134
143
|
if (isPathRelativeEscape(relative)) {
|
|
135
144
|
throw new FsSafeError("outside-workspace", "file path escapes store root");
|
|
@@ -137,7 +146,7 @@ export function ensureParentSync(params) {
|
|
|
137
146
|
syncFs.mkdirSync(rootDir, { recursive: true, mode: params.mode });
|
|
138
147
|
const rootStat = syncFs.lstatSync(rootDir);
|
|
139
148
|
if (rootStat.isSymbolicLink() || !rootStat.isDirectory()) {
|
|
140
|
-
throw new FsSafeError("not-file",
|
|
149
|
+
throw new FsSafeError("not-file", `${params.messagePrefix} root must be a directory: ${rootDir}`);
|
|
141
150
|
}
|
|
142
151
|
const rootReal = syncFs.realpathSync(rootDir);
|
|
143
152
|
chmodDirectorySyncBestEffort(rootDir, params.mode);
|
|
@@ -147,7 +156,7 @@ export function ensureParentSync(params) {
|
|
|
147
156
|
try {
|
|
148
157
|
const stat = syncFs.lstatSync(current);
|
|
149
158
|
if (stat.isSymbolicLink() || !stat.isDirectory()) {
|
|
150
|
-
throw new FsSafeError("not-file",
|
|
159
|
+
throw new FsSafeError("not-file", `${params.messagePrefix} directory component must be a directory: ${current}`);
|
|
151
160
|
}
|
|
152
161
|
}
|
|
153
162
|
catch (error) {
|
|
@@ -156,9 +165,15 @@ export function ensureParentSync(params) {
|
|
|
156
165
|
}
|
|
157
166
|
syncFs.mkdirSync(current, { mode: params.mode });
|
|
158
167
|
}
|
|
168
|
+
const currentRootStat = syncFs.lstatSync(rootDir);
|
|
169
|
+
const currentRootReal = syncFs.realpathSync(rootDir);
|
|
159
170
|
const currentReal = syncFs.realpathSync(current);
|
|
160
|
-
if (
|
|
161
|
-
|
|
171
|
+
if (currentRootStat.isSymbolicLink() ||
|
|
172
|
+
!currentRootStat.isDirectory() ||
|
|
173
|
+
!sameFileIdentity(rootStat, currentRootStat) ||
|
|
174
|
+
currentRootReal !== rootReal ||
|
|
175
|
+
!isPathInside(rootReal, currentReal)) {
|
|
176
|
+
throw new FsSafeError("outside-workspace", `${params.messagePrefix} directory escapes root`);
|
|
162
177
|
}
|
|
163
178
|
chmodDirectorySyncBestEffort(current, params.mode);
|
|
164
179
|
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"file-store-sync-write.d.ts","sourceRoot":"","sources":["../src/file-store-sync-write.ts"],"names":[],"mappings":"AAuBA,wBAAgB,mBAAmB,CAAC,MAAM,EAAE;IAC1C,OAAO,EAAE,MAAM,CAAC;IAChB,QAAQ,EAAE,MAAM,CAAC;IACjB,OAAO,EAAE,MAAM,GAAG,UAAU,CAAC;IAC7B,WAAW,EAAE,OAAO,CAAC;IACrB,OAAO,EAAE,MAAM,CAAC;IAChB,IAAI,EAAE,MAAM,CAAC;CACd,GAAG,MAAM,CA+ET"}
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
import fs from "node:fs";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { FsSafeError } from "./errors.js";
|
|
5
|
+
import { sameFileIdentity } from "./file-identity.js";
|
|
6
|
+
import { assertSyncDirectoryGuard, ensureParentSync, ensureStoreDirectorySync, } from "./file-store-boundary.js";
|
|
7
|
+
import { isPathInside } from "./path.js";
|
|
8
|
+
import { getFsSafeTestHooks } from "./test-hooks.js";
|
|
9
|
+
function ensurePrivateDirectorySync(rootDir, targetDir, mode) {
|
|
10
|
+
return ensureStoreDirectorySync({
|
|
11
|
+
rootDir,
|
|
12
|
+
targetDir,
|
|
13
|
+
mode,
|
|
14
|
+
messagePrefix: "private store",
|
|
15
|
+
});
|
|
16
|
+
}
|
|
17
|
+
export function writeFileSyncAtomic(params) {
|
|
18
|
+
const filePath = path.resolve(params.filePath);
|
|
19
|
+
if (!isPathInside(params.rootDir, filePath)) {
|
|
20
|
+
throw new FsSafeError("outside-workspace", "file path escapes store root");
|
|
21
|
+
}
|
|
22
|
+
let parentGuard;
|
|
23
|
+
if (params.privateMode) {
|
|
24
|
+
parentGuard = ensurePrivateDirectorySync(params.rootDir, path.dirname(filePath), params.dirMode);
|
|
25
|
+
try {
|
|
26
|
+
const stat = fs.lstatSync(filePath);
|
|
27
|
+
if (stat.isSymbolicLink() || !stat.isFile()) {
|
|
28
|
+
throw new FsSafeError("not-file", `private store target must be a regular file: ${filePath}`);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
catch (error) {
|
|
32
|
+
if (error.code !== "ENOENT") {
|
|
33
|
+
throw error;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
else {
|
|
38
|
+
parentGuard = ensureParentSync({
|
|
39
|
+
rootDir: params.rootDir,
|
|
40
|
+
filePath,
|
|
41
|
+
mode: params.dirMode,
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
const tempPath = path.join(parentGuard?.dir ?? path.dirname(filePath), `.fs-safe-${process.pid}-${randomUUID()}.tmp`);
|
|
45
|
+
let tempExists = false;
|
|
46
|
+
try {
|
|
47
|
+
getFsSafeTestHooks()?.beforeFileStoreSyncPrivateWrite?.(filePath);
|
|
48
|
+
if (parentGuard) {
|
|
49
|
+
assertSyncDirectoryGuard(parentGuard);
|
|
50
|
+
}
|
|
51
|
+
fs.writeFileSync(tempPath, params.content, { flag: "wx", mode: params.mode });
|
|
52
|
+
tempExists = true;
|
|
53
|
+
try {
|
|
54
|
+
fs.chmodSync(tempPath, params.mode);
|
|
55
|
+
}
|
|
56
|
+
catch {
|
|
57
|
+
// Best-effort on platforms that do not enforce POSIX modes.
|
|
58
|
+
}
|
|
59
|
+
const tempStat = fs.lstatSync(tempPath);
|
|
60
|
+
if (parentGuard) {
|
|
61
|
+
assertSyncDirectoryGuard(parentGuard);
|
|
62
|
+
}
|
|
63
|
+
fs.renameSync(tempPath, filePath);
|
|
64
|
+
tempExists = false;
|
|
65
|
+
if (parentGuard) {
|
|
66
|
+
assertSyncDirectoryGuard(parentGuard);
|
|
67
|
+
}
|
|
68
|
+
try {
|
|
69
|
+
const publishedStat = fs.lstatSync(filePath);
|
|
70
|
+
if (publishedStat.isSymbolicLink() ||
|
|
71
|
+
!publishedStat.isFile() ||
|
|
72
|
+
publishedStat.nlink > 1 ||
|
|
73
|
+
!sameFileIdentity(tempStat, publishedStat)) {
|
|
74
|
+
throw new FsSafeError("path-mismatch", "store target changed after write");
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
catch (error) {
|
|
78
|
+
if (error instanceof FsSafeError) {
|
|
79
|
+
throw error;
|
|
80
|
+
}
|
|
81
|
+
throw new FsSafeError("path-mismatch", "store target changed after write", {
|
|
82
|
+
cause: error instanceof Error ? error : undefined,
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
return filePath;
|
|
86
|
+
}
|
|
87
|
+
finally {
|
|
88
|
+
if (tempExists) {
|
|
89
|
+
try {
|
|
90
|
+
fs.unlinkSync(tempPath);
|
|
91
|
+
}
|
|
92
|
+
catch {
|
|
93
|
+
// Best-effort cleanup after write failure.
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
}
|
package/dist/file-store.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"file-store.d.ts","sourceRoot":"","sources":["../src/file-store.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"file-store.d.ts","sourceRoot":"","sources":["../src/file-store.ts"],"names":[],"mappings":"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"}
|