@openclaw/fs-safe 0.1.1 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +58 -0
- package/README.md +58 -2
- package/dist/absolute-path.d.ts +14 -0
- package/dist/absolute-path.d.ts.map +1 -1
- package/dist/absolute-path.js +203 -2
- package/dist/advanced.d.ts +2 -1
- package/dist/advanced.d.ts.map +1 -1
- package/dist/advanced.js +2 -1
- package/dist/archive-staging.d.ts.map +1 -1
- package/dist/archive-staging.js +81 -7
- package/dist/archive.d.ts.map +1 -1
- package/dist/archive.js +25 -42
- package/dist/bounded-read-stream.d.ts +8 -0
- package/dist/bounded-read-stream.d.ts.map +1 -0
- package/dist/bounded-read-stream.js +20 -0
- package/dist/config.d.ts +1 -0
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +1 -0
- package/dist/directory-guard.d.ts +18 -0
- package/dist/directory-guard.d.ts.map +1 -0
- package/dist/directory-guard.js +70 -0
- package/dist/file-lock.d.ts +4 -2
- package/dist/file-lock.d.ts.map +1 -1
- package/dist/file-lock.js +13 -2
- package/dist/file-store-boundary.d.ts +25 -0
- package/dist/file-store-boundary.d.ts.map +1 -0
- package/dist/file-store-boundary.js +168 -0
- package/dist/file-store-prune.d.ts +12 -0
- package/dist/file-store-prune.d.ts.map +1 -0
- package/dist/file-store-prune.js +86 -0
- package/dist/file-store-source.d.ts +5 -0
- package/dist/file-store-source.d.ts.map +1 -0
- package/dist/file-store-source.js +30 -0
- package/dist/file-store.d.ts +2 -6
- package/dist/file-store.d.ts.map +1 -1
- package/dist/file-store.js +122 -125
- package/dist/guarded-mkdir.d.ts +6 -0
- package/dist/guarded-mkdir.d.ts.map +1 -0
- package/dist/guarded-mkdir.js +45 -0
- package/dist/guarded-mutation.d.ts +33 -0
- package/dist/guarded-mutation.d.ts.map +1 -0
- package/dist/guarded-mutation.js +76 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -0
- package/dist/json-document-store.d.ts +2 -0
- package/dist/json-document-store.d.ts.map +1 -1
- package/dist/json-document-store.js +7 -3
- package/dist/json-durable-queue.d.ts +45 -0
- package/dist/json-durable-queue.d.ts.map +1 -0
- package/dist/json-durable-queue.js +200 -0
- package/dist/json.d.ts +35 -5
- package/dist/json.d.ts.map +1 -1
- package/dist/json.js +69 -2
- package/dist/local-roots.d.ts.map +1 -1
- package/dist/local-roots.js +17 -3
- package/dist/lock-config.d.ts +10 -0
- package/dist/lock-config.d.ts.map +1 -0
- package/dist/lock-config.js +12 -0
- package/dist/move-path.d.ts +1 -0
- package/dist/move-path.d.ts.map +1 -1
- package/dist/move-path.js +195 -7
- package/dist/output.d.ts +13 -0
- package/dist/output.d.ts.map +1 -0
- package/dist/output.js +69 -0
- package/dist/path-stat.d.ts +4 -0
- package/dist/path-stat.d.ts.map +1 -0
- package/dist/path-stat.js +15 -0
- package/dist/path.d.ts +1 -0
- package/dist/path.d.ts.map +1 -1
- package/dist/path.js +6 -3
- package/dist/pinned-write.d.ts.map +1 -1
- package/dist/pinned-write.js +44 -19
- package/dist/private-file-store.d.ts +5 -7
- package/dist/private-file-store.d.ts.map +1 -1
- package/dist/private-file-store.js +21 -34
- package/dist/private-temp-workspace.d.ts.map +1 -1
- package/dist/private-temp-workspace.js +28 -15
- package/dist/regular-file.d.ts.map +1 -1
- package/dist/regular-file.js +52 -8
- package/dist/replace-directory.d.ts.map +1 -1
- package/dist/replace-directory.js +5 -4
- package/dist/replace-file.d.ts +1 -1
- package/dist/replace-file.d.ts.map +1 -1
- package/dist/replace-file.js +93 -31
- package/dist/root-context.d.ts +23 -0
- package/dist/root-context.d.ts.map +1 -0
- package/dist/root-context.js +62 -0
- package/dist/root-errors.d.ts +4 -0
- package/dist/root-errors.d.ts.map +1 -0
- package/dist/root-errors.js +21 -0
- package/dist/root-impl.d.ts +108 -0
- package/dist/root-impl.d.ts.map +1 -0
- package/dist/{safe-root.js → root-impl.js} +484 -265
- package/dist/root.d.ts +1 -107
- package/dist/root.d.ts.map +1 -1
- package/dist/root.js +1 -1332
- package/dist/safe-path-segment.d.ts +9 -0
- package/dist/safe-path-segment.d.ts.map +1 -0
- package/dist/safe-path-segment.js +51 -0
- package/dist/secret-file.d.ts.map +1 -1
- package/dist/secret-file.js +6 -1
- package/dist/sibling-temp.d.ts +1 -0
- package/dist/sibling-temp.d.ts.map +1 -1
- package/dist/sibling-temp.js +32 -7
- package/dist/sidecar-lock.d.ts +2 -0
- package/dist/sidecar-lock.d.ts.map +1 -1
- package/dist/sidecar-lock.js +93 -13
- package/dist/store.d.ts +1 -0
- package/dist/store.d.ts.map +1 -1
- package/dist/store.js +1 -0
- package/dist/temp-target.d.ts.map +1 -1
- package/dist/temp-target.js +7 -4
- package/dist/test-hooks.d.ts +6 -0
- package/dist/test-hooks.d.ts.map +1 -1
- package/dist/text-atomic.d.ts +7 -0
- package/dist/text-atomic.d.ts.map +1 -1
- package/dist/text-atomic.js +3 -2
- package/dist/trash.d.ts.map +1 -1
- package/dist/trash.js +48 -13
- package/docs/advanced.md +110 -0
- package/docs/archive.md +199 -0
- package/docs/atomic.md +191 -0
- package/docs/config.md +94 -0
- package/docs/contributing.md +80 -0
- package/docs/errors.md +154 -0
- package/docs/file-store.md +213 -0
- package/docs/filename.md +0 -0
- package/docs/index.md +77 -0
- package/docs/install-path.md +167 -0
- package/docs/install.md +150 -0
- package/docs/json-store.md +204 -0
- package/docs/json.md +192 -0
- package/docs/local-roots.md +164 -0
- package/docs/output.md +92 -0
- package/docs/path-scope.md +130 -0
- package/docs/path.md +191 -0
- package/docs/permissions.md +95 -0
- package/docs/private-file-store.md +45 -0
- package/docs/python-helper.md +103 -0
- package/docs/quickstart.md +147 -0
- package/docs/reading.md +174 -0
- package/docs/regular-file.md +175 -0
- package/docs/root.md +205 -0
- package/docs/secret-file.md +154 -0
- package/docs/secure-file.md +82 -0
- package/docs/security-model.md +99 -0
- package/docs/sidecar-lock.md +230 -0
- package/docs/store.md +77 -0
- package/docs/temp.md +284 -0
- package/docs/test-hooks.md +74 -0
- package/docs/testing.md +178 -0
- package/docs/timing.md +121 -0
- package/docs/types.md +155 -0
- package/docs/walk.md +63 -0
- package/docs/writing.md +222 -0
- package/package.json +10 -2
- package/dist/archive-utils.d.ts +0 -3
- package/dist/archive-utils.d.ts.map +0 -1
- package/dist/archive-utils.js +0 -26
- package/dist/boundary-file-read.d.ts +0 -44
- package/dist/boundary-file-read.d.ts.map +0 -1
- package/dist/boundary-file-read.js +0 -129
- package/dist/boundary-path.d.ts +0 -39
- package/dist/boundary-path.d.ts.map +0 -1
- package/dist/boundary-path.js +0 -598
- package/dist/fs-pinned-path-helper.d.ts +0 -7
- package/dist/fs-pinned-path-helper.d.ts.map +0 -1
- package/dist/fs-pinned-path-helper.js +0 -182
- package/dist/fs-pinned-write-helper.d.ts +0 -21
- package/dist/fs-pinned-write-helper.d.ts.map +0 -1
- package/dist/fs-pinned-write-helper.js +0 -263
- package/dist/hardlink-guards.d.ts +0 -7
- package/dist/hardlink-guards.d.ts.map +0 -1
- package/dist/hardlink-guards.js +0 -30
- package/dist/install-safe-path.d.ts +0 -20
- package/dist/install-safe-path.d.ts.map +0 -1
- package/dist/install-safe-path.js +0 -94
- package/dist/json-file.d.ts +0 -3
- package/dist/json-file.d.ts.map +0 -1
- package/dist/json-file.js +0 -123
- package/dist/json-files.d.ts +0 -20
- package/dist/json-files.d.ts.map +0 -1
- package/dist/json-files.js +0 -153
- package/dist/path-alias-guards.d.ts +0 -19
- package/dist/path-alias-guards.d.ts.map +0 -1
- package/dist/path-alias-guards.js +0 -21
- package/dist/path-guards.d.ts +0 -7
- package/dist/path-guards.d.ts.map +0 -1
- package/dist/path-guards.js +0 -49
- package/dist/path-safety.d.ts +0 -12
- package/dist/path-safety.d.ts.map +0 -1
- package/dist/path-safety.js +0 -50
- package/dist/safe-open-sync.d.ts +0 -24
- package/dist/safe-open-sync.d.ts.map +0 -1
- package/dist/safe-open-sync.js +0 -71
- package/dist/safe-root.d.ts +0 -123
- package/dist/safe-root.d.ts.map +0 -1
- package/dist/secure-temp-workspace.d.ts +0 -25
- package/dist/secure-temp-workspace.d.ts.map +0 -1
- package/dist/secure-temp-workspace.js +0 -136
- package/dist/sibling-temp-file.d.ts +0 -16
- package/dist/sibling-temp-file.d.ts.map +0 -1
- package/dist/sibling-temp-file.js +0 -73
- package/dist/sibling-temp-write.d.ts +0 -8
- package/dist/sibling-temp-write.d.ts.map +0 -1
- package/dist/sibling-temp-write.js +0 -40
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export type SafePathSegmentOptions = {
|
|
2
|
+
allowDotPrefix?: boolean;
|
|
3
|
+
label?: string;
|
|
4
|
+
};
|
|
5
|
+
export declare function isSafePathSegment(segment: string, options?: SafePathSegmentOptions): boolean;
|
|
6
|
+
export declare function assertSafePathSegment(segment: string, options?: SafePathSegmentOptions): string;
|
|
7
|
+
export declare function sanitizeSafePathSegment(value: string, fallback: string, options?: SafePathSegmentOptions): string;
|
|
8
|
+
export declare function assertSafePathPrefix(prefix: string, options?: SafePathSegmentOptions): string;
|
|
9
|
+
//# sourceMappingURL=safe-path-segment.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"safe-path-segment.d.ts","sourceRoot":"","sources":["../src/safe-path-segment.ts"],"names":[],"mappings":"AAKA,MAAM,MAAM,sBAAsB,GAAG;IACnC,cAAc,CAAC,EAAE,OAAO,CAAC;IACzB,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB,CAAC;AAEF,wBAAgB,iBAAiB,CAC/B,OAAO,EAAE,MAAM,EACf,OAAO,GAAE,sBAA2B,GACnC,OAAO,CAaT;AAED,wBAAgB,qBAAqB,CACnC,OAAO,EAAE,MAAM,EACf,OAAO,GAAE,sBAA2B,GACnC,MAAM,CAUR;AAED,wBAAgB,uBAAuB,CACrC,KAAK,EAAE,MAAM,EACb,QAAQ,EAAE,MAAM,EAChB,OAAO,GAAE,sBAA2B,GACnC,MAAM,CAWR;AAED,wBAAgB,oBAAoB,CAClC,MAAM,EAAE,MAAM,EACd,OAAO,GAAE,sBAA2B,GACnC,MAAM,CAeR"}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { FsSafeError } from "./errors.js";
|
|
2
|
+
const SAFE_PATH_SEGMENT_PATTERN = /^[A-Za-z0-9_-][A-Za-z0-9._-]*$/;
|
|
3
|
+
const SAFE_DOT_PREFIX_PATH_SEGMENT_PATTERN = /^[A-Za-z0-9._-]+$/;
|
|
4
|
+
export function isSafePathSegment(segment, options = {}) {
|
|
5
|
+
return (segment !== "" &&
|
|
6
|
+
segment !== "." &&
|
|
7
|
+
segment !== ".." &&
|
|
8
|
+
!segment.includes("/") &&
|
|
9
|
+
!segment.includes("\\") &&
|
|
10
|
+
!segment.includes("\0") &&
|
|
11
|
+
(options.allowDotPrefix === true || !segment.startsWith(".")) &&
|
|
12
|
+
(options.allowDotPrefix === true
|
|
13
|
+
? SAFE_DOT_PREFIX_PATH_SEGMENT_PATTERN.test(segment)
|
|
14
|
+
: SAFE_PATH_SEGMENT_PATTERN.test(segment)));
|
|
15
|
+
}
|
|
16
|
+
export function assertSafePathSegment(segment, options = {}) {
|
|
17
|
+
// Validate the exact value callers will later join into paths; trimming here
|
|
18
|
+
// would let whitespace-padded ids pass and then be used verbatim.
|
|
19
|
+
if (!isSafePathSegment(segment, options)) {
|
|
20
|
+
throw new FsSafeError("invalid-path", `${options.label ?? "path segment"} must be a safe path segment`);
|
|
21
|
+
}
|
|
22
|
+
return segment;
|
|
23
|
+
}
|
|
24
|
+
export function sanitizeSafePathSegment(value, fallback, options = {}) {
|
|
25
|
+
const sanitized = value
|
|
26
|
+
.trim()
|
|
27
|
+
.replace(/[\\/]+/g, "-")
|
|
28
|
+
.replace(/\0/g, "")
|
|
29
|
+
.replace(/[^A-Za-z0-9._-]+/g, "-")
|
|
30
|
+
.replace(/^-+|-+$/g, "");
|
|
31
|
+
if (isSafePathSegment(sanitized, options)) {
|
|
32
|
+
return sanitized;
|
|
33
|
+
}
|
|
34
|
+
return assertSafePathSegment(fallback, { ...options, label: "fallback path segment" });
|
|
35
|
+
}
|
|
36
|
+
export function assertSafePathPrefix(prefix, options = {}) {
|
|
37
|
+
// Prefixes are often derived from safe filenames. Normalize harmless
|
|
38
|
+
// filename characters first, but still reject real path-control bytes.
|
|
39
|
+
if (prefix.includes("/") || prefix.includes("\\") || prefix.includes("\0")) {
|
|
40
|
+
return assertSafePathSegment(prefix, {
|
|
41
|
+
allowDotPrefix: true,
|
|
42
|
+
...options,
|
|
43
|
+
label: options.label ?? "path prefix",
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
return assertSafePathSegment(prefix.replace(/[^A-Za-z0-9._-]+/g, "-"), {
|
|
47
|
+
allowDotPrefix: true,
|
|
48
|
+
...options,
|
|
49
|
+
label: options.label ?? "path prefix",
|
|
50
|
+
});
|
|
51
|
+
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"secret-file.d.ts","sourceRoot":"","sources":["../src/secret-file.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"secret-file.d.ts","sourceRoot":"","sources":["../src/secret-file.ts"],"names":[],"mappings":"AAUA,eAAO,MAAM,6BAA6B,QAAY,CAAC;AACvD,eAAO,MAAM,uBAAuB,MAAQ,CAAC;AAC7C,eAAO,MAAM,wBAAwB,MAAQ,CAAC;AAE9C,MAAM,MAAM,qBAAqB,GAAG;IAClC,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,aAAa,CAAC,EAAE,OAAO,CAAC;CACzB,CAAC;AAuGF,wBAAgB,kBAAkB,CAChC,QAAQ,EAAE,MAAM,EAChB,KAAK,EAAE,MAAM,EACb,OAAO,GAAE,qBAA0B,GAClC,MAAM,CAQR;AAED,wBAAgB,qBAAqB,CACnC,QAAQ,EAAE,MAAM,GAAG,SAAS,EAC5B,KAAK,EAAE,MAAM,EACb,OAAO,GAAE,qBAA0B,GAClC,MAAM,GAAG,SAAS,CAMpB;AAoFD,wBAAsB,qBAAqB,CAAC,MAAM,EAAE;IAClD,OAAO,EAAE,MAAM,CAAC;IAChB,QAAQ,EAAE,MAAM,CAAC;IACjB,OAAO,EAAE,MAAM,GAAG,UAAU,CAAC;IAC7B,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB,GAAG,OAAO,CAAC,IAAI,CAAC,CAyDhB"}
|
package/dist/secret-file.js
CHANGED
|
@@ -2,7 +2,9 @@ import { randomBytes } from "node:crypto";
|
|
|
2
2
|
import fs from "node:fs";
|
|
3
3
|
import fsp from "node:fs/promises";
|
|
4
4
|
import path from "node:path";
|
|
5
|
+
import { createAsyncDirectoryGuard } from "./directory-guard.js";
|
|
5
6
|
import { FsSafeError } from "./errors.js";
|
|
7
|
+
import { withAsyncDirectoryGuards } from "./guarded-mutation.js";
|
|
6
8
|
import { resolveHomeRelativePath } from "./home-dir.js";
|
|
7
9
|
import { openPinnedFileSync } from "./pinned-open.js";
|
|
8
10
|
export const DEFAULT_SECRET_FILE_MAX_BYTES = 16 * 1024;
|
|
@@ -188,6 +190,7 @@ export async function writeSecretFileAtomic(params) {
|
|
|
188
190
|
const resolvedRootReal = await fsp.realpath(resolvedRoot);
|
|
189
191
|
const parentDir = await fsp.realpath(intendedParentDir);
|
|
190
192
|
assertRealPathWithinRoot(resolvedRootReal, parentDir);
|
|
193
|
+
const parentGuard = await createAsyncDirectoryGuard(parentDir);
|
|
191
194
|
const fileName = path.basename(resolvedFile);
|
|
192
195
|
const finalFilePath = path.join(parentDir, fileName);
|
|
193
196
|
try {
|
|
@@ -220,7 +223,9 @@ export async function writeSecretFileAtomic(params) {
|
|
|
220
223
|
if (refreshedParentReal !== parentDir) {
|
|
221
224
|
throw new Error(`Private secret parent directory changed during write for ${finalFilePath}.`);
|
|
222
225
|
}
|
|
223
|
-
await
|
|
226
|
+
await withAsyncDirectoryGuards([parentGuard], async () => {
|
|
227
|
+
await fsp.rename(tempPath, finalFilePath);
|
|
228
|
+
});
|
|
224
229
|
createdTemp = false;
|
|
225
230
|
await enforcePrivatePathMode(finalFilePath, mode, "file");
|
|
226
231
|
}
|
package/dist/sibling-temp.d.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"sibling-temp.d.ts","sourceRoot":"","sources":["../src/sibling-temp.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"sibling-temp.d.ts","sourceRoot":"","sources":["../src/sibling-temp.ts"],"names":[],"mappings":"AAaA,MAAM,MAAM,2BAA2B,CAAC,CAAC,IAAI;IAC3C,GAAG,EAAE,MAAM,CAAC;IACZ,SAAS,EAAE,CAAC,QAAQ,EAAE,MAAM,KAAK,OAAO,CAAC,CAAC,CAAC,CAAC;IAC5C,gBAAgB,EAAE,CAAC,MAAM,EAAE,CAAC,KAAK,MAAM,CAAC;IACxC,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,YAAY,CAAC,EAAE,OAAO,CAAC;IACvB,aAAa,CAAC,EAAE,OAAO,CAAC;CACzB,CAAC;AAEF,MAAM,MAAM,0BAA0B,CAAC,CAAC,IAAI;IAC1C,QAAQ,EAAE,MAAM,CAAC;IACjB,MAAM,EAAE,CAAC,CAAC;CACX,CAAC;AA0CF,wBAAsB,oBAAoB,CAAC,CAAC,EAC1C,OAAO,EAAE,2BAA2B,CAAC,CAAC,CAAC,GACtC,OAAO,CAAC,0BAA0B,CAAC,CAAC,CAAC,CAAC,CAyCxC;AAkBD,wBAAsB,uBAAuB,CAAC,MAAM,EAAE;IACpD,OAAO,EAAE,MAAM,CAAC;IAChB,UAAU,EAAE,MAAM,CAAC;IACnB,SAAS,EAAE,CAAC,QAAQ,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;IAC/C,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB,GAAG,OAAO,CAAC,IAAI,CAAC,CA8ChB"}
|
package/dist/sibling-temp.js
CHANGED
|
@@ -1,12 +1,20 @@
|
|
|
1
1
|
import crypto, { randomUUID } from "node:crypto";
|
|
2
2
|
import fs from "node:fs/promises";
|
|
3
3
|
import path from "node:path";
|
|
4
|
+
import { assertAsyncDirectoryGuard, createAsyncDirectoryGuard } from "./directory-guard.js";
|
|
5
|
+
import { withAsyncDirectoryGuards } from "./guarded-mutation.js";
|
|
4
6
|
import { sanitizeUntrustedFileName } from "./filename.js";
|
|
5
7
|
import { root } from "./root.js";
|
|
8
|
+
import { assertSafePathPrefix } from "./safe-path-segment.js";
|
|
9
|
+
import { resolveSecureTempRoot } from "./secure-temp-dir.js";
|
|
6
10
|
import { registerTempPathForExit } from "./temp-cleanup.js";
|
|
11
|
+
import { getFsSafeTestHooks } from "./test-hooks.js";
|
|
7
12
|
import { serializePathWrite } from "./write-queue.js";
|
|
8
13
|
function buildTempPath(dir, tempPrefix) {
|
|
9
|
-
|
|
14
|
+
const safePrefix = assertSafePathPrefix(tempPrefix ?? ".fs-safe-stream", {
|
|
15
|
+
label: "sibling temp prefix",
|
|
16
|
+
});
|
|
17
|
+
return path.join(dir, `${safePrefix}.${process.pid}.${randomUUID()}.tmp`);
|
|
10
18
|
}
|
|
11
19
|
async function syncFileBestEffort(filePath) {
|
|
12
20
|
const handle = await fs.open(filePath, "r+");
|
|
@@ -45,7 +53,10 @@ function assertFinalPathIsSibling(dir, filePath) {
|
|
|
45
53
|
export async function writeSiblingTempFile(options) {
|
|
46
54
|
const dir = path.resolve(options.dir);
|
|
47
55
|
await fs.mkdir(dir, { recursive: true, mode: options.dirMode ?? 0o700 });
|
|
48
|
-
|
|
56
|
+
if (options.chmodDir !== false) {
|
|
57
|
+
await fs.chmod(dir, options.dirMode ?? 0o700).catch(() => undefined);
|
|
58
|
+
}
|
|
59
|
+
const dirGuard = await createAsyncDirectoryGuard(dir);
|
|
49
60
|
const tempPath = buildTempPath(dir, options.tempPrefix);
|
|
50
61
|
const unregisterTempPath = registerTempPathForExit(tempPath);
|
|
51
62
|
let tempExists = false;
|
|
@@ -61,7 +72,9 @@ export async function writeSiblingTempFile(options) {
|
|
|
61
72
|
const filePath = path.resolve(options.resolveFinalPath(result));
|
|
62
73
|
assertFinalPathIsSibling(dir, filePath);
|
|
63
74
|
await serializePathWrite(filePath, async () => {
|
|
64
|
-
await
|
|
75
|
+
await withAsyncDirectoryGuards([dirGuard], async () => {
|
|
76
|
+
await fs.rename(tempPath, filePath);
|
|
77
|
+
});
|
|
65
78
|
tempExists = false;
|
|
66
79
|
unregisterTempPath();
|
|
67
80
|
if (options.mode !== undefined) {
|
|
@@ -82,8 +95,11 @@ export async function writeSiblingTempFile(options) {
|
|
|
82
95
|
}
|
|
83
96
|
function buildSiblingTempPath(params) {
|
|
84
97
|
const id = crypto.randomUUID();
|
|
98
|
+
const safePrefix = assertSafePathPrefix(params.tempPrefix, {
|
|
99
|
+
label: "sibling temp prefix",
|
|
100
|
+
});
|
|
85
101
|
const safeTail = sanitizeUntrustedFileName(path.basename(params.targetPath), params.fallbackFileName);
|
|
86
|
-
return path.join(path.dirname(params.targetPath), `${
|
|
102
|
+
return path.join(path.dirname(params.targetPath), `${safePrefix}${id}-${safeTail}.part`);
|
|
87
103
|
}
|
|
88
104
|
export async function writeViaSiblingTempPath(params) {
|
|
89
105
|
const rootDir = await fs
|
|
@@ -101,19 +117,28 @@ export async function writeViaSiblingTempPath(params) {
|
|
|
101
117
|
path.isAbsolute(relativeTargetPath)) {
|
|
102
118
|
throw new Error("Target path is outside the allowed root");
|
|
103
119
|
}
|
|
120
|
+
const rootGuard = await createAsyncDirectoryGuard(rootDir);
|
|
121
|
+
const tempDir = await fs.mkdtemp(path.join(resolveSecureTempRoot({
|
|
122
|
+
fallbackPrefix: "fs-safe-output",
|
|
123
|
+
unsafeFallbackLabel: "sibling temp output dir",
|
|
124
|
+
warn: () => undefined,
|
|
125
|
+
}), "fs-safe-output-"));
|
|
104
126
|
const tempPath = buildSiblingTempPath({
|
|
105
|
-
targetPath,
|
|
127
|
+
targetPath: path.join(tempDir, path.basename(targetPath)),
|
|
106
128
|
fallbackFileName: params.fallbackFileName ?? "output.bin",
|
|
107
129
|
tempPrefix: params.tempPrefix ?? ".fs-safe-output-",
|
|
108
130
|
});
|
|
109
|
-
const unregisterTempPath = registerTempPathForExit(
|
|
131
|
+
const unregisterTempPath = registerTempPathForExit(tempDir, { recursive: true });
|
|
110
132
|
try {
|
|
133
|
+
await getFsSafeTestHooks()?.beforeSiblingTempWrite?.(tempPath);
|
|
111
134
|
await params.writeTemp(tempPath);
|
|
135
|
+
await assertAsyncDirectoryGuard(rootGuard);
|
|
112
136
|
const targetRoot = await root(rootDir);
|
|
113
137
|
await targetRoot.copyIn(relativeTargetPath, tempPath, { mkdir: false });
|
|
138
|
+
await assertAsyncDirectoryGuard(rootGuard);
|
|
114
139
|
}
|
|
115
140
|
finally {
|
|
116
|
-
await fs.rm(
|
|
141
|
+
await fs.rm(tempDir, { recursive: true, force: true }).catch(() => { });
|
|
117
142
|
unregisterTempPath();
|
|
118
143
|
}
|
|
119
144
|
}
|
package/dist/sidecar-lock.d.ts
CHANGED
|
@@ -5,12 +5,14 @@ export type SidecarLockRetryOptions = {
|
|
|
5
5
|
maxTimeout?: number;
|
|
6
6
|
randomize?: boolean;
|
|
7
7
|
};
|
|
8
|
+
export type SidecarLockStaleRecovery = "fail-closed";
|
|
8
9
|
export type SidecarLockAcquireOptions<TPayload extends Record<string, unknown>> = {
|
|
9
10
|
targetPath: string;
|
|
10
11
|
lockPath?: string;
|
|
11
12
|
staleMs: number;
|
|
12
13
|
timeoutMs?: number;
|
|
13
14
|
retry?: SidecarLockRetryOptions;
|
|
15
|
+
staleRecovery?: SidecarLockStaleRecovery;
|
|
14
16
|
allowReentrant?: boolean;
|
|
15
17
|
payload: () => TPayload | Promise<TPayload>;
|
|
16
18
|
shouldReclaim?: (params: {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"sidecar-lock.d.ts","sourceRoot":"","sources":["../src/sidecar-lock.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"sidecar-lock.d.ts","sourceRoot":"","sources":["../src/sidecar-lock.ts"],"names":[],"mappings":"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"}
|
package/dist/sidecar-lock.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import fsSync from "node:fs";
|
|
2
2
|
import fs from "node:fs/promises";
|
|
3
3
|
import path from "node:path";
|
|
4
|
+
import { sameFileIdentity } from "./file-identity.js";
|
|
4
5
|
const GLOBAL_STATE_KEY = Symbol.for("fsSafe.sidecarLockManagers");
|
|
5
6
|
function getGlobalManagers() {
|
|
6
7
|
const globalWithState = globalThis;
|
|
@@ -18,17 +19,63 @@ function resolveManagerState(key) {
|
|
|
18
19
|
}
|
|
19
20
|
return state;
|
|
20
21
|
}
|
|
21
|
-
async function
|
|
22
|
+
async function readLockSnapshot(lockPath) {
|
|
22
23
|
try {
|
|
23
|
-
const
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
24
|
+
const stat = await fs.lstat(lockPath);
|
|
25
|
+
const raw = await fs.readFile(lockPath, "utf8");
|
|
26
|
+
try {
|
|
27
|
+
const parsed = JSON.parse(raw);
|
|
28
|
+
const payload = parsed && typeof parsed === "object" && !Array.isArray(parsed)
|
|
29
|
+
? parsed
|
|
30
|
+
: null;
|
|
31
|
+
return { raw, payload, stat };
|
|
32
|
+
}
|
|
33
|
+
catch {
|
|
34
|
+
return { raw, payload: null, stat };
|
|
35
|
+
}
|
|
27
36
|
}
|
|
28
37
|
catch {
|
|
29
38
|
return null;
|
|
30
39
|
}
|
|
31
40
|
}
|
|
41
|
+
function snapshotMatches(current, observed) {
|
|
42
|
+
if (observed.stat && current.stat && !sameFileIdentity(observed.stat, current.stat)) {
|
|
43
|
+
return false;
|
|
44
|
+
}
|
|
45
|
+
if (observed.raw !== undefined) {
|
|
46
|
+
return current.raw === observed.raw;
|
|
47
|
+
}
|
|
48
|
+
return observed.stat !== undefined && current.stat !== undefined;
|
|
49
|
+
}
|
|
50
|
+
async function removeLockIfUnchanged(lockPath, observed) {
|
|
51
|
+
const current = await readLockSnapshot(lockPath);
|
|
52
|
+
if (!current || !observed) {
|
|
53
|
+
return false;
|
|
54
|
+
}
|
|
55
|
+
if (!snapshotMatches(current, observed)) {
|
|
56
|
+
// The lock changed after we decided it was stale. Leave the fresh holder's
|
|
57
|
+
// file alone; deleting by path here would break mutual exclusion.
|
|
58
|
+
return false;
|
|
59
|
+
}
|
|
60
|
+
await fs.rm(lockPath, { force: true }).catch(() => undefined);
|
|
61
|
+
return true;
|
|
62
|
+
}
|
|
63
|
+
async function lockSnapshotStillPresent(lockPath, observed) {
|
|
64
|
+
const current = await readLockSnapshot(lockPath);
|
|
65
|
+
return !!current && !!observed && snapshotMatches(current, observed);
|
|
66
|
+
}
|
|
67
|
+
function snapshotMatchesSync(lockPath, observed) {
|
|
68
|
+
try {
|
|
69
|
+
const stat = fsSync.lstatSync(lockPath);
|
|
70
|
+
if (observed.stat && !sameFileIdentity(observed.stat, stat)) {
|
|
71
|
+
return false;
|
|
72
|
+
}
|
|
73
|
+
return observed.raw === undefined || fsSync.readFileSync(lockPath, "utf8") === observed.raw;
|
|
74
|
+
}
|
|
75
|
+
catch {
|
|
76
|
+
return false;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
32
79
|
async function resolveNormalizedTargetPath(targetPath) {
|
|
33
80
|
const resolved = path.resolve(targetPath);
|
|
34
81
|
const dir = path.dirname(resolved);
|
|
@@ -66,7 +113,9 @@ function releaseAllLocksSync(state) {
|
|
|
66
113
|
for (const [normalizedTargetPath, held] of state.held) {
|
|
67
114
|
void held.handle.close().catch(() => undefined);
|
|
68
115
|
try {
|
|
69
|
-
|
|
116
|
+
if (snapshotMatchesSync(held.lockPath, held.snapshot)) {
|
|
117
|
+
fsSync.rmSync(held.lockPath, { force: true });
|
|
118
|
+
}
|
|
70
119
|
}
|
|
71
120
|
catch {
|
|
72
121
|
// Best-effort process-exit cleanup.
|
|
@@ -95,7 +144,7 @@ async function releaseHeldLock(state, normalizedTargetPath, held, opts = {}) {
|
|
|
95
144
|
state.held.delete(normalizedTargetPath);
|
|
96
145
|
held.releasePromise = (async () => {
|
|
97
146
|
await held.handle.close().catch(() => undefined);
|
|
98
|
-
await
|
|
147
|
+
await removeLockIfUnchanged(held.lockPath, held.snapshot);
|
|
99
148
|
})();
|
|
100
149
|
try {
|
|
101
150
|
await held.releasePromise;
|
|
@@ -137,15 +186,19 @@ export function createSidecarLockManager(key) {
|
|
|
137
186
|
let handle = null;
|
|
138
187
|
try {
|
|
139
188
|
handle = await fs.open(lockPath, "wx");
|
|
189
|
+
const payload = await options.payload();
|
|
190
|
+
const raw = `${JSON.stringify(payload, null, 2)}\n`;
|
|
191
|
+
await handle.writeFile(raw, "utf8");
|
|
192
|
+
const snapshot = { raw, payload, stat: await handle.stat() };
|
|
140
193
|
const createdHeld = {
|
|
141
194
|
count: 1,
|
|
142
195
|
handle,
|
|
143
196
|
lockPath,
|
|
197
|
+
snapshot,
|
|
144
198
|
acquiredAt: Date.now(),
|
|
145
199
|
metadata: options.metadata ?? {},
|
|
146
200
|
};
|
|
147
201
|
state.held.set(normalizedTargetPath, createdHeld);
|
|
148
|
-
await handle.writeFile(`${JSON.stringify(await options.payload(), null, 2)}\n`, "utf8");
|
|
149
202
|
const release = () => releaseHeldLock(state, normalizedTargetPath, createdHeld).then(() => undefined);
|
|
150
203
|
return {
|
|
151
204
|
lockPath,
|
|
@@ -156,29 +209,56 @@ export function createSidecarLockManager(key) {
|
|
|
156
209
|
}
|
|
157
210
|
catch (err) {
|
|
158
211
|
if (handle) {
|
|
212
|
+
const failedSnapshot = { payload: null };
|
|
213
|
+
try {
|
|
214
|
+
failedSnapshot.stat = await handle.stat();
|
|
215
|
+
}
|
|
216
|
+
catch {
|
|
217
|
+
// Best-effort cleanup of a failed exclusive create.
|
|
218
|
+
}
|
|
159
219
|
const current = state.held.get(normalizedTargetPath);
|
|
160
220
|
if (current?.handle === handle) {
|
|
161
221
|
state.held.delete(normalizedTargetPath);
|
|
162
222
|
}
|
|
163
|
-
|
|
223
|
+
// If payload serialization/write fails, the file may be empty or
|
|
224
|
+
// partial JSON, so remove while our exclusive handle is still open.
|
|
164
225
|
await fs.rm(lockPath, { force: true }).catch(() => undefined);
|
|
226
|
+
await handle.close().catch(() => undefined);
|
|
227
|
+
// Windows can refuse removing an open file; retry after close but
|
|
228
|
+
// only if the path still points at the file identity we created.
|
|
229
|
+
await removeLockIfUnchanged(lockPath, failedSnapshot);
|
|
165
230
|
}
|
|
166
231
|
if (err.code !== "EEXIST") {
|
|
167
232
|
throw err;
|
|
168
233
|
}
|
|
169
234
|
const nowMs = Date.now();
|
|
170
|
-
const
|
|
235
|
+
const snapshot = await readLockSnapshot(lockPath);
|
|
236
|
+
if (!snapshot) {
|
|
237
|
+
continue;
|
|
238
|
+
}
|
|
171
239
|
const shouldReclaim = options.shouldReclaim ?? defaultShouldReclaim;
|
|
172
240
|
if (await shouldReclaim({
|
|
173
241
|
lockPath,
|
|
174
242
|
normalizedTargetPath,
|
|
175
|
-
payload,
|
|
243
|
+
payload: snapshot?.payload ?? null,
|
|
176
244
|
staleMs: options.staleMs,
|
|
177
245
|
nowMs,
|
|
178
246
|
heldByThisProcess: state.held.has(normalizedTargetPath),
|
|
179
247
|
})) {
|
|
180
|
-
await
|
|
181
|
-
|
|
248
|
+
if (!(await lockSnapshotStillPresent(lockPath, snapshot))) {
|
|
249
|
+
continue;
|
|
250
|
+
}
|
|
251
|
+
// Node exposes only path-based unlink/rename here. A stale-lock
|
|
252
|
+
// reclaimer cannot bind the delete to the file it inspected, so a
|
|
253
|
+
// concurrent release+fresh-acquire could otherwise lose its lock.
|
|
254
|
+
// Fail closed and let callers choose a higher-level recovery path.
|
|
255
|
+
if ((options.staleRecovery ?? "fail-closed") === "fail-closed") {
|
|
256
|
+
throw Object.assign(new Error(`file lock stale for ${normalizedTargetPath}`), {
|
|
257
|
+
code: "file_lock_stale",
|
|
258
|
+
lockPath,
|
|
259
|
+
normalizedTargetPath,
|
|
260
|
+
});
|
|
261
|
+
}
|
|
182
262
|
}
|
|
183
263
|
const elapsed = Date.now() - startedAt;
|
|
184
264
|
if ((options.timeoutMs !== undefined &&
|
package/dist/store.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
1
|
export { fileStore, fileStoreSync, type FileStore, type FileStoreOptions, type FileStorePruneOptions, type FileStoreReadOptions, type FileStoreSync, type FileStoreWriteOptions, } from "./file-store.js";
|
|
2
2
|
export { jsonStore, type JsonStore, type JsonFileStoreOptions, type JsonStoreLockOptions, type JsonStoreOptions, } from "./json-store.js";
|
|
3
|
+
export { ackJsonDurableQueueEntry, ensureJsonDurableQueueDirs, jsonDurableQueueEntryExists, loadJsonDurableQueueEntry, loadPendingJsonDurableQueueEntries, moveJsonDurableQueueEntryToFailed, readJsonDurableQueueEntry, resolveJsonDurableQueueEntryPaths, unlinkBestEffort, writeJsonDurableQueueEntry, type JsonDurableQueueEntryPaths, type JsonDurableQueueLoadOptions, type JsonDurableQueueReadResult, } from "./json-durable-queue.js";
|
|
3
4
|
//# sourceMappingURL=store.d.ts.map
|
package/dist/store.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"store.d.ts","sourceRoot":"","sources":["../src/store.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,SAAS,EACT,aAAa,EACb,KAAK,SAAS,EACd,KAAK,gBAAgB,EACrB,KAAK,qBAAqB,EAC1B,KAAK,oBAAoB,EACzB,KAAK,aAAa,EAClB,KAAK,qBAAqB,GAC3B,MAAM,iBAAiB,CAAC;AACzB,OAAO,EACL,SAAS,EACT,KAAK,SAAS,EACd,KAAK,oBAAoB,EACzB,KAAK,oBAAoB,EACzB,KAAK,gBAAgB,GACtB,MAAM,iBAAiB,CAAC"}
|
|
1
|
+
{"version":3,"file":"store.d.ts","sourceRoot":"","sources":["../src/store.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,SAAS,EACT,aAAa,EACb,KAAK,SAAS,EACd,KAAK,gBAAgB,EACrB,KAAK,qBAAqB,EAC1B,KAAK,oBAAoB,EACzB,KAAK,aAAa,EAClB,KAAK,qBAAqB,GAC3B,MAAM,iBAAiB,CAAC;AACzB,OAAO,EACL,SAAS,EACT,KAAK,SAAS,EACd,KAAK,oBAAoB,EACzB,KAAK,oBAAoB,EACzB,KAAK,gBAAgB,GACtB,MAAM,iBAAiB,CAAC;AACzB,OAAO,EACL,wBAAwB,EACxB,0BAA0B,EAC1B,2BAA2B,EAC3B,yBAAyB,EACzB,kCAAkC,EAClC,iCAAiC,EACjC,yBAAyB,EACzB,iCAAiC,EACjC,gBAAgB,EAChB,0BAA0B,EAC1B,KAAK,0BAA0B,EAC/B,KAAK,2BAA2B,EAChC,KAAK,0BAA0B,GAChC,MAAM,yBAAyB,CAAC"}
|
package/dist/store.js
CHANGED
|
@@ -1,2 +1,3 @@
|
|
|
1
1
|
export { fileStore, fileStoreSync, } from "./file-store.js";
|
|
2
2
|
export { jsonStore, } from "./json-store.js";
|
|
3
|
+
export { ackJsonDurableQueueEntry, ensureJsonDurableQueueDirs, jsonDurableQueueEntryExists, loadJsonDurableQueueEntry, loadPendingJsonDurableQueueEntries, moveJsonDurableQueueEntryToFailed, readJsonDurableQueueEntry, resolveJsonDurableQueueEntryPaths, unlinkBestEffort, writeJsonDurableQueueEntry, } from "./json-durable-queue.js";
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"temp-target.d.ts","sourceRoot":"","sources":["../src/temp-target.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"temp-target.d.ts","sourceRoot":"","sources":["../src/temp-target.ts"],"names":[],"mappings":"AAOA,MAAM,MAAM,QAAQ,GAAG;IACrB,GAAG,EAAE,MAAM,CAAC;IACZ,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,CAAC,QAAQ,CAAC,EAAE,MAAM,GAAG,MAAM,CAAC;IAChC,OAAO,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;IAC7B,CAAC,MAAM,CAAC,YAAY,CAAC,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;CACxC,CAAC;AAiBF,wBAAgB,oBAAoB,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM,CAI7D;AAED,wBAAgB,uBAAuB,CAAC,MAAM,EAAE;IAC9C,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,MAAM,EAAE,MAAM,CAAC;IACf,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,IAAI,CAAC,EAAE,MAAM,CAAC;CACf,GAAG,MAAM,CAaT;AAyBD,wBAAsB,QAAQ,CAAC,MAAM,EAAE;IACrC,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,MAAM,EAAE,MAAM,CAAC;IACf,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,cAAc,CAAC,EAAE,CAAC,KAAK,EAAE,OAAO,KAAK,IAAI,CAAC;CAC3C,GAAG,OAAO,CAAC,QAAQ,CAAC,CAqBpB;AAED,wBAAsB,YAAY,CAAC,CAAC,EAClC,MAAM,EAAE;IACN,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,MAAM,EAAE,MAAM,CAAC;IACf,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,cAAc,CAAC,EAAE,CAAC,KAAK,EAAE,OAAO,KAAK,IAAI,CAAC;CAC3C,EACD,EAAE,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,OAAO,CAAC,CAAC,CAAC,GAClC,OAAO,CAAC,CAAC,CAAC,CAOZ"}
|
package/dist/temp-target.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import crypto from "node:crypto";
|
|
2
2
|
import { mkdtemp, rm } from "node:fs/promises";
|
|
3
3
|
import path from "node:path";
|
|
4
|
+
import { assertSafePathSegment, sanitizeSafePathSegment } from "./safe-path-segment.js";
|
|
4
5
|
import { resolveSecureTempRoot } from "./secure-temp-dir.js";
|
|
5
6
|
import { registerTempPathForExit } from "./temp-cleanup.js";
|
|
6
7
|
function sanitizePrefix(prefix) {
|
|
@@ -17,9 +18,9 @@ function sanitizeExtension(extension) {
|
|
|
17
18
|
return token ? `.${token}` : "";
|
|
18
19
|
}
|
|
19
20
|
export function sanitizeTempFileName(fileName) {
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
21
|
+
return sanitizeSafePathSegment(path.basename(fileName), "download.bin", {
|
|
22
|
+
allowDotPrefix: true,
|
|
23
|
+
});
|
|
23
24
|
}
|
|
24
25
|
export function buildRandomTempFilePath(params) {
|
|
25
26
|
const rootDir = resolveTempRoot(params.rootDir);
|
|
@@ -29,7 +30,9 @@ export function buildRandomTempFilePath(params) {
|
|
|
29
30
|
const now = typeof nowCandidate === "number" && Number.isFinite(nowCandidate)
|
|
30
31
|
? Math.trunc(nowCandidate)
|
|
31
32
|
: Date.now();
|
|
32
|
-
const uuid = params.uuid
|
|
33
|
+
const uuid = params.uuid
|
|
34
|
+
? assertSafePathSegment(params.uuid.trim(), { label: "temp uuid" })
|
|
35
|
+
: crypto.randomUUID();
|
|
33
36
|
return path.join(rootDir, `${prefix}-${now}-${uuid}${extension}`);
|
|
34
37
|
}
|
|
35
38
|
function isNodeErrorWithCode(err, code) {
|
package/dist/test-hooks.d.ts
CHANGED
|
@@ -3,6 +3,12 @@ export type FsSafeTestHooks = {
|
|
|
3
3
|
afterPreOpenLstat?: (filePath: string) => Promise<void> | void;
|
|
4
4
|
beforeOpen?: (filePath: string, flags: number) => Promise<void> | void;
|
|
5
5
|
afterOpen?: (filePath: string, handle: FileHandle) => Promise<void> | void;
|
|
6
|
+
beforeArchiveOutputMutation?: (operation: "mkdir" | "chmod", targetPath: string) => Promise<void> | void;
|
|
7
|
+
beforeFileStorePruneDescend?: (dirPath: string) => Promise<void> | void;
|
|
8
|
+
beforeFileStoreSyncPrivateWrite?: (filePath: string) => void;
|
|
9
|
+
beforeRootFallbackMutation?: (operation: "mkdir" | "move" | "remove", targetPath: string) => Promise<void> | void;
|
|
10
|
+
beforeSiblingTempWrite?: (tempPath: string) => Promise<void> | void;
|
|
11
|
+
beforeTrashMove?: (targetPath: string, destPath: string) => void;
|
|
6
12
|
};
|
|
7
13
|
export declare function getFsSafeTestHooks(): FsSafeTestHooks | undefined;
|
|
8
14
|
export declare function __setFsSafeTestHooksForTest(hooks?: FsSafeTestHooks): void;
|
package/dist/test-hooks.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"test-hooks.d.ts","sourceRoot":"","sources":["../src/test-hooks.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,kBAAkB,CAAC;AAEnD,MAAM,MAAM,eAAe,GAAG;IAC5B,iBAAiB,CAAC,EAAE,CAAC,QAAQ,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,GAAG,IAAI,CAAC;IAC/D,UAAU,CAAC,EAAE,CAAC,QAAQ,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,GAAG,IAAI,CAAC;IACvE,SAAS,CAAC,EAAE,CAAC,QAAQ,EAAE,MAAM,EAAE,MAAM,EAAE,UAAU,KAAK,OAAO,CAAC,IAAI,CAAC,GAAG,IAAI,CAAC;
|
|
1
|
+
{"version":3,"file":"test-hooks.d.ts","sourceRoot":"","sources":["../src/test-hooks.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,kBAAkB,CAAC;AAEnD,MAAM,MAAM,eAAe,GAAG;IAC5B,iBAAiB,CAAC,EAAE,CAAC,QAAQ,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,GAAG,IAAI,CAAC;IAC/D,UAAU,CAAC,EAAE,CAAC,QAAQ,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,GAAG,IAAI,CAAC;IACvE,SAAS,CAAC,EAAE,CAAC,QAAQ,EAAE,MAAM,EAAE,MAAM,EAAE,UAAU,KAAK,OAAO,CAAC,IAAI,CAAC,GAAG,IAAI,CAAC;IAC3E,2BAA2B,CAAC,EAAE,CAC5B,SAAS,EAAE,OAAO,GAAG,OAAO,EAC5B,UAAU,EAAE,MAAM,KACf,OAAO,CAAC,IAAI,CAAC,GAAG,IAAI,CAAC;IAC1B,2BAA2B,CAAC,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,GAAG,IAAI,CAAC;IACxE,+BAA+B,CAAC,EAAE,CAAC,QAAQ,EAAE,MAAM,KAAK,IAAI,CAAC;IAC7D,0BAA0B,CAAC,EAAE,CAC3B,SAAS,EAAE,OAAO,GAAG,MAAM,GAAG,QAAQ,EACtC,UAAU,EAAE,MAAM,KACf,OAAO,CAAC,IAAI,CAAC,GAAG,IAAI,CAAC;IAC1B,sBAAsB,CAAC,EAAE,CAAC,QAAQ,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,GAAG,IAAI,CAAC;IACpE,eAAe,CAAC,EAAE,CAAC,UAAU,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,KAAK,IAAI,CAAC;CAClE,CAAC;AAQF,wBAAgB,kBAAkB,IAAI,eAAe,GAAG,SAAS,CAEhE;AAED,wBAAgB,2BAA2B,CAAC,KAAK,CAAC,EAAE,eAAe,GAAG,IAAI,CAKzE"}
|
package/dist/text-atomic.d.ts
CHANGED
|
@@ -2,6 +2,13 @@ export type WriteTextAtomicOptions = {
|
|
|
2
2
|
mode?: number;
|
|
3
3
|
dirMode?: number;
|
|
4
4
|
trailingNewline?: boolean;
|
|
5
|
+
/**
|
|
6
|
+
* When false, skip the temp-file and parent-directory fsync calls while
|
|
7
|
+
* preserving the temp-file replace/rename behavior.
|
|
8
|
+
*
|
|
9
|
+
* Defaults to true.
|
|
10
|
+
*/
|
|
11
|
+
durable?: boolean;
|
|
5
12
|
};
|
|
6
13
|
export declare function writeTextAtomic(filePath: string, content: string, options?: WriteTextAtomicOptions): Promise<void>;
|
|
7
14
|
//# sourceMappingURL=text-atomic.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"text-atomic.d.ts","sourceRoot":"","sources":["../src/text-atomic.ts"],"names":[],"mappings":"AAEA,MAAM,MAAM,sBAAsB,GAAG;IACnC,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,eAAe,CAAC,EAAE,OAAO,CAAC;
|
|
1
|
+
{"version":3,"file":"text-atomic.d.ts","sourceRoot":"","sources":["../src/text-atomic.ts"],"names":[],"mappings":"AAEA,MAAM,MAAM,sBAAsB,GAAG;IACnC,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,eAAe,CAAC,EAAE,OAAO,CAAC;IAC1B;;;;;OAKG;IACH,OAAO,CAAC,EAAE,OAAO,CAAC;CACnB,CAAC;AAEF,wBAAsB,eAAe,CACnC,QAAQ,EAAE,MAAM,EAChB,OAAO,EAAE,MAAM,EACf,OAAO,CAAC,EAAE,sBAAsB,GAC/B,OAAO,CAAC,IAAI,CAAC,CAYf"}
|
package/dist/text-atomic.js
CHANGED
|
@@ -1,13 +1,14 @@
|
|
|
1
1
|
import { replaceFileAtomic } from "./replace-file.js";
|
|
2
2
|
export async function writeTextAtomic(filePath, content, options) {
|
|
3
3
|
const payload = options?.trailingNewline && !content.endsWith("\n") ? `${content}\n` : content;
|
|
4
|
+
const durable = options?.durable ?? true;
|
|
4
5
|
await replaceFileAtomic({
|
|
5
6
|
filePath,
|
|
6
7
|
content: payload,
|
|
7
8
|
mode: options?.mode ?? 0o600,
|
|
8
9
|
dirMode: options?.dirMode ?? (0o777 & ~process.umask()),
|
|
9
10
|
copyFallbackOnPermissionError: true,
|
|
10
|
-
syncTempFile:
|
|
11
|
-
syncParentDir:
|
|
11
|
+
syncTempFile: durable,
|
|
12
|
+
syncParentDir: durable,
|
|
12
13
|
});
|
|
13
14
|
}
|
package/dist/trash.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"trash.d.ts","sourceRoot":"","sources":["../src/trash.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"trash.d.ts","sourceRoot":"","sources":["../src/trash.ts"],"names":[],"mappings":"AAOA,MAAM,MAAM,sBAAsB,GAAG;IACnC,YAAY,CAAC,EAAE,QAAQ,CAAC,MAAM,CAAC,CAAC;CACjC,CAAC;AAwKF,wBAAsB,eAAe,CACnC,UAAU,EAAE,MAAM,EAClB,OAAO,GAAE,sBAA2B,GACnC,OAAO,CAAC,MAAM,CAAC,CAcjB"}
|