@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
package/dist/file-store.d.ts
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import type { Readable } from "node:stream";
|
|
2
|
+
import { type FileStorePruneOptions } from "./file-store-prune.js";
|
|
3
|
+
export type { FileStorePruneOptions } from "./file-store-prune.js";
|
|
2
4
|
import { type JsonFileStoreOptions, type JsonStore } from "./json-document-store.js";
|
|
3
5
|
import { type OpenResult, type ReadResult, type Root, type RootReadOptions } from "./root.js";
|
|
4
6
|
export type FileStoreOptions = {
|
|
@@ -17,12 +19,6 @@ export type FileStoreWriteOptions = {
|
|
|
17
19
|
export type FileStoreReadOptions = RootReadOptions & {
|
|
18
20
|
encoding?: BufferEncoding;
|
|
19
21
|
};
|
|
20
|
-
export type FileStorePruneOptions = {
|
|
21
|
-
ttlMs: number;
|
|
22
|
-
recursive?: boolean;
|
|
23
|
-
maxDepth?: number;
|
|
24
|
-
pruneEmptyDirs?: boolean;
|
|
25
|
-
};
|
|
26
22
|
export type FileStore = {
|
|
27
23
|
readonly rootDir: string;
|
|
28
24
|
path(relativePath: string): string;
|
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":"AAIA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,aAAa,CAAC;AAG5C,OAAO,EAAmB,KAAK,oBAAoB,EAAE,KAAK,SAAS,EAAE,MAAM,0BAA0B,CAAC;AAEtG,OAAO,EAAQ,KAAK,UAAU,EAAE,KAAK,UAAU,EAAE,KAAK,IAAI,EAAE,KAAK,eAAe,EAAE,MAAM,WAAW,CAAC;
|
|
1
|
+
{"version":3,"file":"file-store.d.ts","sourceRoot":"","sources":["../src/file-store.ts"],"names":[],"mappings":"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"}
|
package/dist/file-store.js
CHANGED
|
@@ -2,13 +2,18 @@ import { randomUUID } from "node:crypto";
|
|
|
2
2
|
import syncFs from "node:fs";
|
|
3
3
|
import fs from "node:fs/promises";
|
|
4
4
|
import path from "node:path";
|
|
5
|
-
import {
|
|
5
|
+
import { createSyncDirectoryGuard } from "./directory-guard.js";
|
|
6
6
|
import { FsSafeError } from "./errors.js";
|
|
7
|
+
import { pruneExpiredStoreEntries, } from "./file-store-prune.js";
|
|
8
|
+
import { assertSyncDirectoryGuard, ensureParentInRoot, ensureParentSync, openWritableStoreRoot, writeStreamToTempSource, } from "./file-store-boundary.js";
|
|
9
|
+
import { readFileStoreCopySource } from "./file-store-source.js";
|
|
7
10
|
import { createJsonStore } from "./json-document-store.js";
|
|
8
11
|
import { isPathInside, resolveSafeRelativePath } from "./path.js";
|
|
9
12
|
import { root } from "./root.js";
|
|
13
|
+
import { DEFAULT_ROOT_MAX_BYTES } from "./root-impl.js";
|
|
14
|
+
import { matchRootFileOpenFailure, openRootFileSync } from "./root-file.js";
|
|
10
15
|
import { writeSecretFileAtomic } from "./secret-file.js";
|
|
11
|
-
import {
|
|
16
|
+
import { getFsSafeTestHooks } from "./test-hooks.js";
|
|
12
17
|
function assertRelativePath(relativePath) {
|
|
13
18
|
const raw = relativePath.trim();
|
|
14
19
|
if (!raw) {
|
|
@@ -24,43 +29,65 @@ function assertStoreFilePath(rootDir, filePath) {
|
|
|
24
29
|
throw new FsSafeError("outside-workspace", "file path escapes store root");
|
|
25
30
|
}
|
|
26
31
|
}
|
|
27
|
-
async function ensureParent(filePath, mode) {
|
|
28
|
-
const dir = path.dirname(filePath);
|
|
29
|
-
await fs.mkdir(dir, { recursive: true, mode });
|
|
30
|
-
await fs.chmod(dir, mode).catch(() => undefined);
|
|
31
|
-
}
|
|
32
32
|
function assertMaxBytes(size, maxBytes) {
|
|
33
33
|
if (maxBytes !== undefined && size > maxBytes) {
|
|
34
34
|
throw new FsSafeError("too-large", `file exceeds maximum size of ${maxBytes} bytes`);
|
|
35
35
|
}
|
|
36
36
|
}
|
|
37
37
|
function isNotFound(error) {
|
|
38
|
+
if (!error) {
|
|
39
|
+
return false;
|
|
40
|
+
}
|
|
38
41
|
return error instanceof FsSafeError
|
|
39
42
|
? error.code === "not-found"
|
|
40
43
|
: error.code === "ENOENT" ||
|
|
41
44
|
error.code === "ENOTDIR";
|
|
42
45
|
}
|
|
46
|
+
function handleSyncStoreReadOpenFailure(opened) {
|
|
47
|
+
return matchRootFileOpenFailure(opened, {
|
|
48
|
+
path: (failure) => {
|
|
49
|
+
if (isNotFound(failure.error)) {
|
|
50
|
+
return null;
|
|
51
|
+
}
|
|
52
|
+
throw new FsSafeError("path-mismatch", "store target changed during read", {
|
|
53
|
+
cause: failure.error instanceof Error ? failure.error : undefined,
|
|
54
|
+
});
|
|
55
|
+
},
|
|
56
|
+
validation: (failure) => {
|
|
57
|
+
// Validation failures mean the path existed but violated store policy
|
|
58
|
+
// (directory, hardlink, symlink race). Do not report them as missing.
|
|
59
|
+
throw new FsSafeError("path-mismatch", "store target failed read validation", {
|
|
60
|
+
cause: failure.error instanceof Error ? failure.error : undefined,
|
|
61
|
+
});
|
|
62
|
+
},
|
|
63
|
+
fallback: (failure) => {
|
|
64
|
+
throw new FsSafeError("path-mismatch", "store target changed during read", {
|
|
65
|
+
cause: failure.error instanceof Error ? failure.error : undefined,
|
|
66
|
+
});
|
|
67
|
+
},
|
|
68
|
+
});
|
|
69
|
+
}
|
|
43
70
|
async function copyIntoRoot(params) {
|
|
44
|
-
const
|
|
71
|
+
const relativePath = assertRelativePath(params.relativePath);
|
|
72
|
+
const destination = resolveStorePath(params.rootDir, relativePath);
|
|
45
73
|
const sourceStat = await fs.lstat(params.sourcePath);
|
|
46
74
|
if (sourceStat.isSymbolicLink() || !sourceStat.isFile()) {
|
|
47
75
|
throw new FsSafeError("not-file", "source path is not a file");
|
|
48
76
|
}
|
|
49
77
|
assertMaxBytes(sourceStat.size, params.maxBytes);
|
|
50
|
-
|
|
51
|
-
const
|
|
52
|
-
|
|
53
|
-
dirMode
|
|
78
|
+
const dirMode = params.dirMode ?? 0o700;
|
|
79
|
+
const scopedRoot = await openWritableStoreRoot({
|
|
80
|
+
rootDir: params.rootDir,
|
|
81
|
+
dirMode,
|
|
82
|
+
maxBytes: params.maxBytes,
|
|
83
|
+
});
|
|
84
|
+
await ensureParentInRoot(scopedRoot, relativePath, dirMode);
|
|
85
|
+
await scopedRoot.copyIn(relativePath, params.sourcePath, {
|
|
86
|
+
maxBytes: params.maxBytes,
|
|
87
|
+
mkdir: false,
|
|
54
88
|
mode: params.mode ?? 0o600,
|
|
55
|
-
tempPrefix: params.tempPrefix ?? `.${path.basename(destination)}`,
|
|
56
|
-
writeTemp: async (tempPath) => {
|
|
57
|
-
await fs.copyFile(params.sourcePath, tempPath);
|
|
58
|
-
},
|
|
59
|
-
resolveFinalPath: () => destination,
|
|
60
|
-
syncTempFile: true,
|
|
61
|
-
syncParentDir: true,
|
|
62
89
|
});
|
|
63
|
-
return
|
|
90
|
+
return destination;
|
|
64
91
|
}
|
|
65
92
|
export function fileStore(options) {
|
|
66
93
|
const rootDir = path.resolve(options.rootDir);
|
|
@@ -72,7 +99,8 @@ export function fileStore(options) {
|
|
|
72
99
|
return await root(rootDir, { hardlinks: "reject", maxBytes });
|
|
73
100
|
}
|
|
74
101
|
async function write(relativePath, data, writeOptions) {
|
|
75
|
-
const
|
|
102
|
+
const safeRelativePath = assertRelativePath(relativePath);
|
|
103
|
+
const destination = resolveStorePath(rootDir, safeRelativePath);
|
|
76
104
|
const content = Buffer.isBuffer(data) ? data : Buffer.from(data);
|
|
77
105
|
assertMaxBytes(content.byteLength, writeOptions?.maxBytes ?? maxBytes);
|
|
78
106
|
if (privateMode) {
|
|
@@ -85,20 +113,18 @@ export function fileStore(options) {
|
|
|
85
113
|
});
|
|
86
114
|
return destination;
|
|
87
115
|
}
|
|
88
|
-
|
|
89
|
-
const
|
|
90
|
-
|
|
91
|
-
dirMode:
|
|
116
|
+
const writeDirMode = writeOptions?.dirMode ?? dirMode;
|
|
117
|
+
const scopedRoot = await openWritableStoreRoot({
|
|
118
|
+
rootDir,
|
|
119
|
+
dirMode: writeDirMode,
|
|
120
|
+
maxBytes: writeOptions?.maxBytes ?? maxBytes,
|
|
121
|
+
});
|
|
122
|
+
await ensureParentInRoot(scopedRoot, safeRelativePath, writeDirMode);
|
|
123
|
+
await scopedRoot.write(safeRelativePath, content, {
|
|
124
|
+
mkdir: false,
|
|
92
125
|
mode: writeOptions?.mode ?? mode,
|
|
93
|
-
tempPrefix: writeOptions?.tempPrefix ?? `.${path.basename(destination)}`,
|
|
94
|
-
writeTemp: async (tempPath) => {
|
|
95
|
-
await fs.writeFile(tempPath, content);
|
|
96
|
-
},
|
|
97
|
-
resolveFinalPath: () => destination,
|
|
98
|
-
syncTempFile: true,
|
|
99
|
-
syncParentDir: true,
|
|
100
126
|
});
|
|
101
|
-
return
|
|
127
|
+
return destination;
|
|
102
128
|
}
|
|
103
129
|
return {
|
|
104
130
|
rootDir,
|
|
@@ -106,8 +132,9 @@ export function fileStore(options) {
|
|
|
106
132
|
root: openRoot,
|
|
107
133
|
write,
|
|
108
134
|
writeStream: async (relativePath, stream, writeOptions) => {
|
|
109
|
-
const
|
|
110
|
-
const
|
|
135
|
+
const safeRelativePath = assertRelativePath(relativePath);
|
|
136
|
+
const destination = resolveStorePath(rootDir, safeRelativePath);
|
|
137
|
+
const limit = writeOptions?.maxBytes ?? maxBytes ?? (privateMode ? DEFAULT_ROOT_MAX_BYTES : undefined);
|
|
111
138
|
if (privateMode) {
|
|
112
139
|
const chunks = [];
|
|
113
140
|
let total = 0;
|
|
@@ -126,43 +153,34 @@ export function fileStore(options) {
|
|
|
126
153
|
});
|
|
127
154
|
return destination;
|
|
128
155
|
}
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
dir: path.dirname(destination),
|
|
133
|
-
dirMode: writeOptions?.dirMode ?? dirMode,
|
|
156
|
+
const staged = await writeStreamToTempSource({
|
|
157
|
+
stream,
|
|
158
|
+
maxBytes: limit,
|
|
134
159
|
mode: writeOptions?.mode ?? mode,
|
|
135
|
-
tempPrefix: writeOptions?.tempPrefix ?? `.${path.basename(destination)}`,
|
|
136
|
-
writeTemp: async (tempPath) => {
|
|
137
|
-
const writable = await fs.open(tempPath, "w", writeOptions?.mode ?? mode);
|
|
138
|
-
try {
|
|
139
|
-
const out = writable.createWriteStream();
|
|
140
|
-
stream.on("data", (chunk) => {
|
|
141
|
-
total += Buffer.byteLength(chunk);
|
|
142
|
-
if (limit !== undefined && total > limit) {
|
|
143
|
-
stream.destroy(new FsSafeError("too-large", `file exceeds maximum size of ${limit} bytes`));
|
|
144
|
-
}
|
|
145
|
-
});
|
|
146
|
-
await pipeline(stream, out);
|
|
147
|
-
}
|
|
148
|
-
finally {
|
|
149
|
-
await writable.close().catch(() => undefined);
|
|
150
|
-
}
|
|
151
|
-
},
|
|
152
|
-
resolveFinalPath: () => destination,
|
|
153
|
-
syncTempFile: true,
|
|
154
|
-
syncParentDir: true,
|
|
155
160
|
});
|
|
156
|
-
|
|
161
|
+
try {
|
|
162
|
+
await copyIntoRoot({
|
|
163
|
+
rootDir,
|
|
164
|
+
relativePath: safeRelativePath,
|
|
165
|
+
sourcePath: staged.path,
|
|
166
|
+
maxBytes: limit,
|
|
167
|
+
mode: writeOptions?.mode ?? mode,
|
|
168
|
+
tempPrefix: writeOptions?.tempPrefix,
|
|
169
|
+
dirMode: writeOptions?.dirMode ?? dirMode,
|
|
170
|
+
});
|
|
171
|
+
}
|
|
172
|
+
finally {
|
|
173
|
+
await staged.cleanup();
|
|
174
|
+
}
|
|
175
|
+
return destination;
|
|
157
176
|
},
|
|
158
177
|
copyIn: async (relativePath, sourcePath, writeOptions) => privateMode
|
|
159
178
|
? await (async () => {
|
|
160
|
-
const
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
}
|
|
164
|
-
|
|
165
|
-
return await write(relativePath, await fs.readFile(sourcePath), writeOptions);
|
|
179
|
+
const buffer = await readFileStoreCopySource({
|
|
180
|
+
sourcePath,
|
|
181
|
+
maxBytes: writeOptions?.maxBytes ?? maxBytes ?? DEFAULT_ROOT_MAX_BYTES,
|
|
182
|
+
});
|
|
183
|
+
return await write(relativePath, buffer, writeOptions);
|
|
166
184
|
})()
|
|
167
185
|
: await copyIntoRoot({
|
|
168
186
|
rootDir,
|
|
@@ -240,50 +258,10 @@ export function fileStore(options) {
|
|
|
240
258
|
}, jsonOptions);
|
|
241
259
|
},
|
|
242
260
|
pruneExpired: async (pruneOptions) => {
|
|
243
|
-
|
|
244
|
-
const recursive = pruneOptions.recursive ?? false;
|
|
245
|
-
const maxDepth = pruneOptions.maxDepth;
|
|
246
|
-
const pruneEmptyDirs = (recursive || maxDepth !== undefined) && (pruneOptions.pruneEmptyDirs ?? false);
|
|
247
|
-
async function pruneDir(dir, depth) {
|
|
248
|
-
const entries = await fs.readdir(dir, { withFileTypes: true }).catch(() => []);
|
|
249
|
-
for (const entry of entries) {
|
|
250
|
-
const fullPath = path.join(dir, entry.name);
|
|
251
|
-
const stat = await fs.lstat(fullPath).catch(() => null);
|
|
252
|
-
if (!stat || stat.isSymbolicLink()) {
|
|
253
|
-
continue;
|
|
254
|
-
}
|
|
255
|
-
if (stat.isDirectory()) {
|
|
256
|
-
const shouldDescend = maxDepth !== undefined ? depth < maxDepth : recursive;
|
|
257
|
-
if (shouldDescend && (await pruneDir(fullPath, depth + 1))) {
|
|
258
|
-
await fs.rmdir(fullPath).catch(() => undefined);
|
|
259
|
-
}
|
|
260
|
-
continue;
|
|
261
|
-
}
|
|
262
|
-
if (stat.isFile() && now - stat.mtimeMs > pruneOptions.ttlMs) {
|
|
263
|
-
await fs.rm(fullPath, { force: true }).catch(() => undefined);
|
|
264
|
-
}
|
|
265
|
-
}
|
|
266
|
-
if (!pruneEmptyDirs) {
|
|
267
|
-
return false;
|
|
268
|
-
}
|
|
269
|
-
const remaining = await fs.readdir(dir).catch(() => null);
|
|
270
|
-
return remaining !== null && remaining.length === 0;
|
|
271
|
-
}
|
|
272
|
-
await fs.mkdir(rootDir, { recursive: true, mode: dirMode });
|
|
273
|
-
await pruneDir(rootDir, 0);
|
|
261
|
+
await pruneExpiredStoreEntries({ rootDir, dirMode, options: pruneOptions });
|
|
274
262
|
},
|
|
275
263
|
};
|
|
276
264
|
}
|
|
277
|
-
function ensureParentSync(filePath, mode) {
|
|
278
|
-
const dir = path.dirname(filePath);
|
|
279
|
-
syncFs.mkdirSync(dir, { recursive: true, mode });
|
|
280
|
-
try {
|
|
281
|
-
syncFs.chmodSync(dir, mode);
|
|
282
|
-
}
|
|
283
|
-
catch {
|
|
284
|
-
// Best-effort on platforms that do not enforce POSIX modes.
|
|
285
|
-
}
|
|
286
|
-
}
|
|
287
265
|
function ensurePrivateDirectorySync(rootDir, targetDir, mode) {
|
|
288
266
|
const root = path.resolve(rootDir);
|
|
289
267
|
const target = path.resolve(targetDir);
|
|
@@ -326,12 +304,16 @@ function ensurePrivateDirectorySync(rootDir, targetDir, mode) {
|
|
|
326
304
|
// Best-effort on platforms that do not enforce POSIX modes.
|
|
327
305
|
}
|
|
328
306
|
}
|
|
307
|
+
const guard = createSyncDirectoryGuard(target);
|
|
308
|
+
assertSyncDirectoryGuard(guard);
|
|
309
|
+
return guard;
|
|
329
310
|
}
|
|
330
311
|
function writeFileSyncAtomic(params) {
|
|
331
312
|
const filePath = path.resolve(params.filePath);
|
|
332
313
|
assertStoreFilePath(params.rootDir, filePath);
|
|
314
|
+
let parentGuard;
|
|
333
315
|
if (params.privateMode) {
|
|
334
|
-
ensurePrivateDirectorySync(params.rootDir, path.dirname(filePath), params.dirMode);
|
|
316
|
+
parentGuard = ensurePrivateDirectorySync(params.rootDir, path.dirname(filePath), params.dirMode);
|
|
335
317
|
try {
|
|
336
318
|
const stat = syncFs.lstatSync(filePath);
|
|
337
319
|
if (stat.isSymbolicLink() || !stat.isFile()) {
|
|
@@ -345,11 +327,19 @@ function writeFileSyncAtomic(params) {
|
|
|
345
327
|
}
|
|
346
328
|
}
|
|
347
329
|
else {
|
|
348
|
-
ensureParentSync(
|
|
330
|
+
parentGuard = ensureParentSync({
|
|
331
|
+
rootDir: params.rootDir,
|
|
332
|
+
filePath,
|
|
333
|
+
mode: params.dirMode,
|
|
334
|
+
});
|
|
349
335
|
}
|
|
350
|
-
const tempPath = path.join(path.dirname(filePath), `.fs-safe-${process.pid}-${randomUUID()}.tmp`);
|
|
336
|
+
const tempPath = path.join(parentGuard?.dir ?? path.dirname(filePath), `.fs-safe-${process.pid}-${randomUUID()}.tmp`);
|
|
351
337
|
let tempExists = false;
|
|
352
338
|
try {
|
|
339
|
+
getFsSafeTestHooks()?.beforeFileStoreSyncPrivateWrite?.(filePath);
|
|
340
|
+
if (parentGuard) {
|
|
341
|
+
assertSyncDirectoryGuard(parentGuard);
|
|
342
|
+
}
|
|
353
343
|
syncFs.writeFileSync(tempPath, params.content, { flag: "wx", mode: params.mode });
|
|
354
344
|
tempExists = true;
|
|
355
345
|
try {
|
|
@@ -358,8 +348,14 @@ function writeFileSyncAtomic(params) {
|
|
|
358
348
|
catch {
|
|
359
349
|
// Best-effort on platforms that do not enforce POSIX modes.
|
|
360
350
|
}
|
|
351
|
+
if (parentGuard) {
|
|
352
|
+
assertSyncDirectoryGuard(parentGuard);
|
|
353
|
+
}
|
|
361
354
|
syncFs.renameSync(tempPath, filePath);
|
|
362
355
|
tempExists = false;
|
|
356
|
+
if (parentGuard) {
|
|
357
|
+
assertSyncDirectoryGuard(parentGuard);
|
|
358
|
+
}
|
|
363
359
|
try {
|
|
364
360
|
syncFs.chmodSync(filePath, params.mode);
|
|
365
361
|
}
|
|
@@ -403,22 +399,23 @@ export function fileStoreSync(options) {
|
|
|
403
399
|
path: (relativePath) => resolveStorePath(rootDir, relativePath),
|
|
404
400
|
readTextIfExists: (relativePath, readOptions) => {
|
|
405
401
|
const targetPath = resolveStorePath(rootDir, relativePath);
|
|
402
|
+
const opened = openRootFileSync({
|
|
403
|
+
absolutePath: targetPath,
|
|
404
|
+
rootPath: rootDir,
|
|
405
|
+
boundaryLabel: "store root",
|
|
406
|
+
rejectHardlinks: privateMode,
|
|
407
|
+
});
|
|
408
|
+
if (!opened.ok) {
|
|
409
|
+
return handleSyncStoreReadOpenFailure(opened);
|
|
410
|
+
}
|
|
406
411
|
try {
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
assertMaxBytes(stat.size, readOptions?.maxBytes ?? maxBytes);
|
|
412
|
-
if (privateMode && stat.nlink > 1) {
|
|
413
|
-
throw new FsSafeError("hardlink", "private store target must not be hardlinked");
|
|
414
|
-
}
|
|
415
|
-
return syncFs.readFileSync(targetPath, "utf8");
|
|
412
|
+
assertMaxBytes(opened.stat.size, readOptions?.maxBytes ?? maxBytes);
|
|
413
|
+
const raw = syncFs.readFileSync(opened.fd, "utf8");
|
|
414
|
+
assertMaxBytes(Buffer.byteLength(raw, "utf8"), readOptions?.maxBytes ?? maxBytes);
|
|
415
|
+
return raw;
|
|
416
416
|
}
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
return null;
|
|
420
|
-
}
|
|
421
|
-
throw error;
|
|
417
|
+
finally {
|
|
418
|
+
syncFs.closeSync(opened.fd);
|
|
422
419
|
}
|
|
423
420
|
},
|
|
424
421
|
readJsonIfExists: (relativePath, readOptions) => {
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"guarded-mkdir.d.ts","sourceRoot":"","sources":["../src/guarded-mkdir.ts"],"names":[],"mappings":"AAaA,wBAAsB,6BAA6B,CAAC,MAAM,EAAE;IAC1D,QAAQ,EAAE,MAAM,CAAC;IACjB,UAAU,EAAE,MAAM,CAAC;IACnB,eAAe,CAAC,EAAE,CAAC,aAAa,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,GAAG,IAAI,CAAC;CACnE,GAAG,OAAO,CAAC,IAAI,CAAC,CAiChB"}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import fs from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { assertAsyncDirectoryGuard, createAsyncDirectoryGuard } from "./directory-guard.js";
|
|
4
|
+
import { FsSafeError } from "./errors.js";
|
|
5
|
+
function isSameOrChildPath(candidate, parent) {
|
|
6
|
+
return candidate === parent || candidate.startsWith(`${parent}${path.sep}`);
|
|
7
|
+
}
|
|
8
|
+
function isPathEscape(relativePath) {
|
|
9
|
+
return relativePath === ".." || relativePath.startsWith(`..${path.sep}`) || path.isAbsolute(relativePath);
|
|
10
|
+
}
|
|
11
|
+
export async function mkdirPathComponentsWithGuards(params) {
|
|
12
|
+
const root = path.resolve(params.rootReal);
|
|
13
|
+
const target = path.resolve(params.targetPath);
|
|
14
|
+
const relative = path.relative(root, target);
|
|
15
|
+
if (isPathEscape(relative)) {
|
|
16
|
+
throw new FsSafeError("outside-workspace", "directory is outside workspace root");
|
|
17
|
+
}
|
|
18
|
+
let current = root;
|
|
19
|
+
for (const part of relative.split(path.sep).filter(Boolean)) {
|
|
20
|
+
const next = path.join(current, part);
|
|
21
|
+
const parentGuard = await createAsyncDirectoryGuard(current);
|
|
22
|
+
await params.beforeComponent?.(next);
|
|
23
|
+
await assertAsyncDirectoryGuard(parentGuard);
|
|
24
|
+
try {
|
|
25
|
+
await fs.mkdir(next);
|
|
26
|
+
}
|
|
27
|
+
catch (error) {
|
|
28
|
+
if (!error || typeof error !== "object" || !("code" in error) || error.code !== "EEXIST") {
|
|
29
|
+
throw error;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
const stat = await fs.lstat(next);
|
|
33
|
+
if (stat.isSymbolicLink() || !stat.isDirectory()) {
|
|
34
|
+
throw new FsSafeError("not-file", "directory component must be a directory");
|
|
35
|
+
}
|
|
36
|
+
// Node's recursive mkdir follows symlinks in missing components. Build one
|
|
37
|
+
// segment at a time and realpath-check each segment before descending.
|
|
38
|
+
if (!isSameOrChildPath(path.resolve(await fs.realpath(next)), root)) {
|
|
39
|
+
throw new FsSafeError("outside-workspace", "directory escaped workspace root");
|
|
40
|
+
}
|
|
41
|
+
await createAsyncDirectoryGuard(next);
|
|
42
|
+
await assertAsyncDirectoryGuard(parentGuard);
|
|
43
|
+
current = next;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { type AsyncDirectoryGuard, type SyncDirectoryGuard } from "./directory-guard.js";
|
|
2
|
+
export declare function withAsyncDirectoryGuards<T>(guards: readonly AsyncDirectoryGuard[], mutate: () => Promise<T>, options?: {
|
|
3
|
+
verifyAfter?: boolean;
|
|
4
|
+
onPostGuardFailure?: (result: T, error: unknown) => Promise<void> | void;
|
|
5
|
+
}): Promise<T>;
|
|
6
|
+
export declare function withSyncDirectoryGuards<T>(guards: readonly SyncDirectoryGuard[], mutate: () => T, options?: {
|
|
7
|
+
verifyAfter?: boolean;
|
|
8
|
+
}): T;
|
|
9
|
+
export declare function guardedRename(params: {
|
|
10
|
+
from: string;
|
|
11
|
+
to: string;
|
|
12
|
+
targetRoot?: string;
|
|
13
|
+
verifyAfter?: boolean;
|
|
14
|
+
}): Promise<void>;
|
|
15
|
+
export declare function guardedRenameSync(params: {
|
|
16
|
+
from: string;
|
|
17
|
+
to: string;
|
|
18
|
+
targetRoot?: string;
|
|
19
|
+
verifyAfter?: boolean;
|
|
20
|
+
}): void;
|
|
21
|
+
export declare function guardedRm(params: {
|
|
22
|
+
target: string;
|
|
23
|
+
recursive?: boolean;
|
|
24
|
+
force?: boolean;
|
|
25
|
+
verifyAfter?: boolean;
|
|
26
|
+
}): Promise<void>;
|
|
27
|
+
export declare function guardedRmSync(params: {
|
|
28
|
+
target: string;
|
|
29
|
+
recursive?: boolean;
|
|
30
|
+
force?: boolean;
|
|
31
|
+
verifyAfter?: boolean;
|
|
32
|
+
}): void;
|
|
33
|
+
//# sourceMappingURL=guarded-mutation.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"guarded-mutation.d.ts","sourceRoot":"","sources":["../src/guarded-mutation.ts"],"names":[],"mappings":"AAGA,OAAO,EAOL,KAAK,mBAAmB,EACxB,KAAK,kBAAkB,EACxB,MAAM,sBAAsB,CAAC;AAE9B,wBAAsB,wBAAwB,CAAC,CAAC,EAC9C,MAAM,EAAE,SAAS,mBAAmB,EAAE,EACtC,MAAM,EAAE,MAAM,OAAO,CAAC,CAAC,CAAC,EACxB,OAAO,GAAE;IACP,WAAW,CAAC,EAAE,OAAO,CAAC;IACtB,kBAAkB,CAAC,EAAE,CAAC,MAAM,EAAE,CAAC,EAAE,KAAK,EAAE,OAAO,KAAK,OAAO,CAAC,IAAI,CAAC,GAAG,IAAI,CAAC;CACrE,GACL,OAAO,CAAC,CAAC,CAAC,CAyBZ;AAED,wBAAgB,uBAAuB,CAAC,CAAC,EACvC,MAAM,EAAE,SAAS,kBAAkB,EAAE,EACrC,MAAM,EAAE,MAAM,CAAC,EACf,OAAO,GAAE;IAAE,WAAW,CAAC,EAAE,OAAO,CAAA;CAAO,GACtC,CAAC,CAWH;AAED,wBAAsB,aAAa,CAAC,MAAM,EAAE;IAC1C,IAAI,EAAE,MAAM,CAAC;IACb,EAAE,EAAE,MAAM,CAAC;IACX,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,WAAW,CAAC,EAAE,OAAO,CAAC;CACvB,GAAG,OAAO,CAAC,IAAI,CAAC,CAYhB;AAED,wBAAgB,iBAAiB,CAAC,MAAM,EAAE;IACxC,IAAI,EAAE,MAAM,CAAC;IACb,EAAE,EAAE,MAAM,CAAC;IACX,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,WAAW,CAAC,EAAE,OAAO,CAAC;CACvB,GAAG,IAAI,CAUP;AAED,wBAAsB,SAAS,CAAC,MAAM,EAAE;IACtC,MAAM,EAAE,MAAM,CAAC;IACf,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB,KAAK,CAAC,EAAE,OAAO,CAAC;IAChB,WAAW,CAAC,EAAE,OAAO,CAAC;CACvB,GAAG,OAAO,CAAC,IAAI,CAAC,CAYhB;AAED,wBAAgB,aAAa,CAAC,MAAM,EAAE;IACpC,MAAM,EAAE,MAAM,CAAC;IACf,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB,KAAK,CAAC,EAAE,OAAO,CAAC;IAChB,WAAW,CAAC,EAAE,OAAO,CAAC;CACvB,GAAG,IAAI,CAWP"}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import fsSync from "node:fs";
|
|
2
|
+
import fs from "node:fs/promises";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { assertAsyncDirectoryGuard, assertSyncDirectoryGuard, createAsyncDirectoryGuard, createNearestExistingDirectoryGuard, createNearestExistingSyncDirectoryGuard, createSyncDirectoryGuard, } from "./directory-guard.js";
|
|
5
|
+
export async function withAsyncDirectoryGuards(guards, mutate, options = {}) {
|
|
6
|
+
for (const guard of guards) {
|
|
7
|
+
await assertAsyncDirectoryGuard(guard);
|
|
8
|
+
}
|
|
9
|
+
const result = await mutate();
|
|
10
|
+
if (options.verifyAfter !== false) {
|
|
11
|
+
try {
|
|
12
|
+
for (const guard of guards) {
|
|
13
|
+
await assertAsyncDirectoryGuard(guard);
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
catch (error) {
|
|
17
|
+
if (options.onPostGuardFailure) {
|
|
18
|
+
try {
|
|
19
|
+
// The mutation may have returned an owned resource before the post-guard
|
|
20
|
+
// check detected a swapped directory. Give callers one chance to close
|
|
21
|
+
// handles without letting cleanup hide the boundary failure.
|
|
22
|
+
await options.onPostGuardFailure(result, error);
|
|
23
|
+
}
|
|
24
|
+
catch {
|
|
25
|
+
// Preserve the boundary failure. Cleanup is best-effort.
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
throw error;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
return result;
|
|
32
|
+
}
|
|
33
|
+
export function withSyncDirectoryGuards(guards, mutate, options = {}) {
|
|
34
|
+
for (const guard of guards) {
|
|
35
|
+
assertSyncDirectoryGuard(guard);
|
|
36
|
+
}
|
|
37
|
+
const result = mutate();
|
|
38
|
+
if (options.verifyAfter !== false) {
|
|
39
|
+
for (const guard of guards) {
|
|
40
|
+
assertSyncDirectoryGuard(guard);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
return result;
|
|
44
|
+
}
|
|
45
|
+
export async function guardedRename(params) {
|
|
46
|
+
const sourceGuard = await createAsyncDirectoryGuard(path.dirname(params.from));
|
|
47
|
+
const targetGuard = params.targetRoot
|
|
48
|
+
? await createNearestExistingDirectoryGuard(params.targetRoot, path.dirname(params.to))
|
|
49
|
+
: await createAsyncDirectoryGuard(path.dirname(params.to));
|
|
50
|
+
await withAsyncDirectoryGuards([sourceGuard, targetGuard], async () => {
|
|
51
|
+
await fs.rename(params.from, params.to);
|
|
52
|
+
}, { verifyAfter: params.verifyAfter });
|
|
53
|
+
}
|
|
54
|
+
export function guardedRenameSync(params) {
|
|
55
|
+
const sourceGuard = createSyncDirectoryGuard(path.dirname(params.from));
|
|
56
|
+
const targetGuard = params.targetRoot
|
|
57
|
+
? createNearestExistingSyncDirectoryGuard(params.targetRoot, path.dirname(params.to))
|
|
58
|
+
: createSyncDirectoryGuard(path.dirname(params.to));
|
|
59
|
+
withSyncDirectoryGuards([sourceGuard, targetGuard], () => fsSync.renameSync(params.from, params.to), { verifyAfter: params.verifyAfter });
|
|
60
|
+
}
|
|
61
|
+
export async function guardedRm(params) {
|
|
62
|
+
const guard = await createAsyncDirectoryGuard(path.dirname(params.target));
|
|
63
|
+
await withAsyncDirectoryGuards([guard], async () => {
|
|
64
|
+
await fs.rm(params.target, {
|
|
65
|
+
...(params.recursive !== undefined ? { recursive: params.recursive } : {}),
|
|
66
|
+
...(params.force !== undefined ? { force: params.force } : {}),
|
|
67
|
+
});
|
|
68
|
+
}, { verifyAfter: params.verifyAfter });
|
|
69
|
+
}
|
|
70
|
+
export function guardedRmSync(params) {
|
|
71
|
+
const guard = createSyncDirectoryGuard(path.dirname(params.target));
|
|
72
|
+
withSyncDirectoryGuards([guard], () => fsSync.rmSync(params.target, {
|
|
73
|
+
...(params.recursive !== undefined ? { recursive: params.recursive } : {}),
|
|
74
|
+
...(params.force !== undefined ? { force: params.force } : {}),
|
|
75
|
+
}), { verifyAfter: params.verifyAfter });
|
|
76
|
+
}
|
package/dist/index.d.ts
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
export { FsSafeError, categorizeFsSafeError, type FsSafeErrorCategory, type FsSafeErrorCode, } from "./errors.js";
|
|
2
2
|
export { DEFAULT_ROOT_MAX_BYTES, root, type HardlinkPolicy, type OpenResult, type ReadResult, type Root, type RootAppendOptions, type RootCopyOptions, type RootCreateJsonOptions, type RootCreateOptions, type RootDefaults, type RootOpenOptions, type RootOpenWritableOptions, type RootOptions, type RootReadOptions, type RootWriteJsonOptions, type RootWriteOptions, type SymlinkPolicy, type WritableOpenMode, type WritableOpenResult, } from "./root.js";
|
|
3
3
|
export { configureFsSafePython, getFsSafePythonConfig, type FsSafePythonConfig, type FsSafePythonMode, } from "./pinned-python-config.js";
|
|
4
|
+
export { writeExternalFileWithinRoot, type ExternalFileWriteOptions, type ExternalFileWriteResult, } from "./output.js";
|
|
5
|
+
export { configureFsSafeLocks, getFsSafeLockConfig, type FsSafeLockConfig, } from "./lock-config.js";
|
|
4
6
|
//# sourceMappingURL=index.d.ts.map
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,WAAW,EACX,qBAAqB,EACrB,KAAK,mBAAmB,EACxB,KAAK,eAAe,GACrB,MAAM,aAAa,CAAC;AACrB,OAAO,EACL,sBAAsB,EACtB,IAAI,EACJ,KAAK,cAAc,EACnB,KAAK,UAAU,EACf,KAAK,UAAU,EACf,KAAK,IAAI,EACT,KAAK,iBAAiB,EACtB,KAAK,eAAe,EACpB,KAAK,qBAAqB,EAC1B,KAAK,iBAAiB,EACtB,KAAK,YAAY,EACjB,KAAK,eAAe,EACpB,KAAK,uBAAuB,EAC5B,KAAK,WAAW,EAChB,KAAK,eAAe,EACpB,KAAK,oBAAoB,EACzB,KAAK,gBAAgB,EACrB,KAAK,aAAa,EAClB,KAAK,gBAAgB,EACrB,KAAK,kBAAkB,GACxB,MAAM,WAAW,CAAC;AACnB,OAAO,EACL,qBAAqB,EACrB,qBAAqB,EACrB,KAAK,kBAAkB,EACvB,KAAK,gBAAgB,GACtB,MAAM,2BAA2B,CAAC"}
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,WAAW,EACX,qBAAqB,EACrB,KAAK,mBAAmB,EACxB,KAAK,eAAe,GACrB,MAAM,aAAa,CAAC;AACrB,OAAO,EACL,sBAAsB,EACtB,IAAI,EACJ,KAAK,cAAc,EACnB,KAAK,UAAU,EACf,KAAK,UAAU,EACf,KAAK,IAAI,EACT,KAAK,iBAAiB,EACtB,KAAK,eAAe,EACpB,KAAK,qBAAqB,EAC1B,KAAK,iBAAiB,EACtB,KAAK,YAAY,EACjB,KAAK,eAAe,EACpB,KAAK,uBAAuB,EAC5B,KAAK,WAAW,EAChB,KAAK,eAAe,EACpB,KAAK,oBAAoB,EACzB,KAAK,gBAAgB,EACrB,KAAK,aAAa,EAClB,KAAK,gBAAgB,EACrB,KAAK,kBAAkB,GACxB,MAAM,WAAW,CAAC;AACnB,OAAO,EACL,qBAAqB,EACrB,qBAAqB,EACrB,KAAK,kBAAkB,EACvB,KAAK,gBAAgB,GACtB,MAAM,2BAA2B,CAAC;AACnC,OAAO,EACL,2BAA2B,EAC3B,KAAK,wBAAwB,EAC7B,KAAK,uBAAuB,GAC7B,MAAM,aAAa,CAAC;AACrB,OAAO,EACL,oBAAoB,EACpB,mBAAmB,EACnB,KAAK,gBAAgB,GACtB,MAAM,kBAAkB,CAAC"}
|
package/dist/index.js
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
1
|
export { FsSafeError, categorizeFsSafeError, } from "./errors.js";
|
|
2
2
|
export { DEFAULT_ROOT_MAX_BYTES, root, } from "./root.js";
|
|
3
3
|
export { configureFsSafePython, getFsSafePythonConfig, } from "./pinned-python-config.js";
|
|
4
|
+
export { writeExternalFileWithinRoot, } from "./output.js";
|
|
5
|
+
export { configureFsSafeLocks, getFsSafeLockConfig, } from "./lock-config.js";
|
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
import type { FileLockRetryOptions } from "./file-lock.js";
|
|
2
|
+
import type { SidecarLockStaleRecovery } from "./sidecar-lock.js";
|
|
2
3
|
export type JsonStoreLockOptions = {
|
|
3
4
|
staleMs?: number;
|
|
4
5
|
timeoutMs?: number;
|
|
5
6
|
retry?: FileLockRetryOptions;
|
|
7
|
+
staleRecovery?: SidecarLockStaleRecovery;
|
|
6
8
|
managerKey?: string;
|
|
7
9
|
};
|
|
8
10
|
export type JsonFileStoreOptions = {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"json-document-store.d.ts","sourceRoot":"","sources":["../src/json-document-store.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,gBAAgB,CAAC;AAG3D,MAAM,MAAM,oBAAoB,GAAG;IACjC,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,KAAK,CAAC,EAAE,oBAAoB,CAAC;IAC7B,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB,CAAC;AAEF,MAAM,MAAM,oBAAoB,GAAG;IACjC,eAAe,CAAC,EAAE,OAAO,CAAC;IAC1B,IAAI,CAAC,EAAE,OAAO,GAAG,oBAAoB,CAAC;CACvC,CAAC;AAEF,MAAM,MAAM,SAAS,CAAC,CAAC,IAAI;IACzB,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,IAAI,IAAI,OAAO,CAAC,CAAC,GAAG,SAAS,CAAC,CAAC;IAC/B,MAAM,CAAC,QAAQ,EAAE,CAAC,GAAG,OAAO,CAAC,CAAC,CAAC,CAAC;IAChC,YAAY,IAAI,OAAO,CAAC,CAAC,CAAC,CAAC;IAC3B,KAAK,CAAC,KAAK,EAAE,CAAC,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAC/B,MAAM,CAAC,GAAG,EAAE,CAAC,OAAO,EAAE,CAAC,GAAG,SAAS,KAAK,CAAC,GAAG,OAAO,CAAC,CAAC,CAAC,GAAG,OAAO,CAAC,CAAC,CAAC,CAAC;IACpE,QAAQ,CAAC,QAAQ,EAAE,CAAC,EAAE,GAAG,EAAE,CAAC,OAAO,EAAE,CAAC,KAAK,CAAC,GAAG,OAAO,CAAC,CAAC,CAAC,GAAG,OAAO,CAAC,CAAC,CAAC,CAAC;CACxE,CAAC;AAEF,MAAM,MAAM,gBAAgB,CAAC,CAAC,IAAI;IAChC,QAAQ,EAAE,MAAM,CAAC;IACjB,YAAY,IAAI,OAAO,CAAC,CAAC,GAAG,IAAI,CAAC,CAAC;IAClC,YAAY,IAAI,OAAO,CAAC,CAAC,CAAC,CAAC;IAC3B,KAAK,CAAC,KAAK,EAAE,CAAC,EAAE,OAAO,CAAC,EAAE;QAAE,eAAe,CAAC,EAAE,OAAO,CAAA;KAAE,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;CACzE,CAAC;
|
|
1
|
+
{"version":3,"file":"json-document-store.d.ts","sourceRoot":"","sources":["../src/json-document-store.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,gBAAgB,CAAC;AAG3D,OAAO,KAAK,EAAE,wBAAwB,EAAE,MAAM,mBAAmB,CAAC;AAElE,MAAM,MAAM,oBAAoB,GAAG;IACjC,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,KAAK,CAAC,EAAE,oBAAoB,CAAC;IAC7B,aAAa,CAAC,EAAE,wBAAwB,CAAC;IACzC,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB,CAAC;AAEF,MAAM,MAAM,oBAAoB,GAAG;IACjC,eAAe,CAAC,EAAE,OAAO,CAAC;IAC1B,IAAI,CAAC,EAAE,OAAO,GAAG,oBAAoB,CAAC;CACvC,CAAC;AAEF,MAAM,MAAM,SAAS,CAAC,CAAC,IAAI;IACzB,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,IAAI,IAAI,OAAO,CAAC,CAAC,GAAG,SAAS,CAAC,CAAC;IAC/B,MAAM,CAAC,QAAQ,EAAE,CAAC,GAAG,OAAO,CAAC,CAAC,CAAC,CAAC;IAChC,YAAY,IAAI,OAAO,CAAC,CAAC,CAAC,CAAC;IAC3B,KAAK,CAAC,KAAK,EAAE,CAAC,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAC/B,MAAM,CAAC,GAAG,EAAE,CAAC,OAAO,EAAE,CAAC,GAAG,SAAS,KAAK,CAAC,GAAG,OAAO,CAAC,CAAC,CAAC,GAAG,OAAO,CAAC,CAAC,CAAC,CAAC;IACpE,QAAQ,CAAC,QAAQ,EAAE,CAAC,EAAE,GAAG,EAAE,CAAC,OAAO,EAAE,CAAC,KAAK,CAAC,GAAG,OAAO,CAAC,CAAC,CAAC,GAAG,OAAO,CAAC,CAAC,CAAC,CAAC;CACxE,CAAC;AAEF,MAAM,MAAM,gBAAgB,CAAC,CAAC,IAAI;IAChC,QAAQ,EAAE,MAAM,CAAC;IACjB,YAAY,IAAI,OAAO,CAAC,CAAC,GAAG,IAAI,CAAC,CAAC;IAClC,YAAY,IAAI,OAAO,CAAC,CAAC,CAAC,CAAC;IAC3B,KAAK,CAAC,KAAK,EAAE,CAAC,EAAE,OAAO,CAAC,EAAE;QAAE,eAAe,CAAC,EAAE,OAAO,CAAA;KAAE,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;CACzE,CAAC;AA2BF,wBAAgB,eAAe,CAAC,CAAC,EAC/B,OAAO,EAAE,gBAAgB,CAAC,CAAC,CAAC,EAC5B,OAAO,GAAE,oBAAyB,GACjC,SAAS,CAAC,CAAC,CAAC,CA2Dd"}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { getFsSafeLockConfig } from "./lock-config.js";
|
|
1
2
|
import { createSidecarLockManager } from "./sidecar-lock.js";
|
|
2
3
|
function cloneFallback(value) {
|
|
3
4
|
if (value && typeof value === "object") {
|
|
@@ -10,11 +11,13 @@ function resolveLockOptions(filePath, options) {
|
|
|
10
11
|
return null;
|
|
11
12
|
}
|
|
12
13
|
const lockOptions = options.lock === true ? {} : options.lock;
|
|
14
|
+
const defaults = getFsSafeLockConfig();
|
|
13
15
|
return {
|
|
14
16
|
managerKey: lockOptions.managerKey ?? `fs-safe.json-store:${filePath}`,
|
|
15
|
-
retry: lockOptions.retry ?? {},
|
|
16
|
-
staleMs: lockOptions.staleMs ?? 30_000,
|
|
17
|
-
|
|
17
|
+
retry: lockOptions.retry ?? defaults.retry ?? {},
|
|
18
|
+
staleMs: lockOptions.staleMs ?? defaults.staleMs ?? 30_000,
|
|
19
|
+
staleRecovery: lockOptions.staleRecovery ?? defaults.staleRecovery,
|
|
20
|
+
timeoutMs: lockOptions.timeoutMs ?? defaults.timeoutMs ?? 30_000,
|
|
18
21
|
};
|
|
19
22
|
}
|
|
20
23
|
export function createJsonStore(adapter, options = {}) {
|
|
@@ -40,6 +43,7 @@ export function createJsonStore(adapter, options = {}) {
|
|
|
40
43
|
staleMs: lockOptions.staleMs,
|
|
41
44
|
timeoutMs: lockOptions.timeoutMs,
|
|
42
45
|
retry: lockOptions.retry,
|
|
46
|
+
staleRecovery: lockOptions.staleRecovery,
|
|
43
47
|
allowReentrant: true,
|
|
44
48
|
payload: () => ({ pid: process.pid, createdAt: new Date().toISOString() }),
|
|
45
49
|
}, run);
|