@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/move-path.js
CHANGED
|
@@ -1,7 +1,186 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
import { constants as fsConstants } from "node:fs";
|
|
1
3
|
import fs from "node:fs/promises";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
import { guardedRename } from "./guarded-mutation.js";
|
|
6
|
+
function entryIdentity(stat) {
|
|
7
|
+
return {
|
|
8
|
+
ctimeMs: stat.ctimeMs,
|
|
9
|
+
dev: stat.dev,
|
|
10
|
+
ino: stat.ino,
|
|
11
|
+
mode: stat.mode,
|
|
12
|
+
mtimeMs: stat.mtimeMs,
|
|
13
|
+
size: stat.size,
|
|
14
|
+
};
|
|
15
|
+
}
|
|
16
|
+
function sameIdentity(a, b) {
|
|
17
|
+
return (a.dev === b.dev &&
|
|
18
|
+
a.ino === b.ino &&
|
|
19
|
+
a.mode === b.mode &&
|
|
20
|
+
a.size === b.size &&
|
|
21
|
+
a.mtimeMs === b.mtimeMs &&
|
|
22
|
+
a.ctimeMs === b.ctimeMs);
|
|
23
|
+
}
|
|
24
|
+
function sameDirectoryNode(a, b) {
|
|
25
|
+
return a.dev === b.dev && a.ino === b.ino;
|
|
26
|
+
}
|
|
27
|
+
function modeBits(mode) {
|
|
28
|
+
return mode & 0o777;
|
|
29
|
+
}
|
|
30
|
+
function sourceChangedError(sourcePath) {
|
|
31
|
+
return Object.assign(new Error(`Source changed during move fallback: ${sourcePath}`), {
|
|
32
|
+
code: "ESTALE",
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
async function assertSourceStillMatches(sourcePath, identity) {
|
|
36
|
+
if (!sameIdentity(identity, entryIdentity(await fs.lstat(sourcePath)))) {
|
|
37
|
+
throw sourceChangedError(sourcePath);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
function regularReadFlags() {
|
|
41
|
+
return (fsConstants.O_RDONLY |
|
|
42
|
+
(typeof fsConstants.O_NOFOLLOW === "number" && process.platform !== "win32"
|
|
43
|
+
? fsConstants.O_NOFOLLOW
|
|
44
|
+
: 0));
|
|
45
|
+
}
|
|
46
|
+
async function writeAll(handle, buffer, bytesRead) {
|
|
47
|
+
let offset = 0;
|
|
48
|
+
while (offset < bytesRead) {
|
|
49
|
+
const { bytesWritten } = await handle.write(buffer, offset, bytesRead - offset);
|
|
50
|
+
offset += bytesWritten;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
async function copyRegularFilePinned(params) {
|
|
54
|
+
let destinationCreated = false;
|
|
55
|
+
let sourceHandle;
|
|
56
|
+
try {
|
|
57
|
+
sourceHandle = await fs.open(params.from, regularReadFlags());
|
|
58
|
+
}
|
|
59
|
+
catch (error) {
|
|
60
|
+
const code = error?.code;
|
|
61
|
+
if (code === "ELOOP" || code === "ENOENT" || code === "ENOTDIR") {
|
|
62
|
+
throw sourceChangedError(params.from);
|
|
63
|
+
}
|
|
64
|
+
throw error;
|
|
65
|
+
}
|
|
66
|
+
try {
|
|
67
|
+
const openedStat = await sourceHandle.stat();
|
|
68
|
+
if (!openedStat.isFile() || !sameIdentity(params.identity, entryIdentity(openedStat))) {
|
|
69
|
+
throw sourceChangedError(params.from);
|
|
70
|
+
}
|
|
71
|
+
const destinationHandle = await fs.open(params.to, fsConstants.O_WRONLY | fsConstants.O_CREAT | fsConstants.O_EXCL, modeBits(params.mode) || 0o666);
|
|
72
|
+
destinationCreated = true;
|
|
73
|
+
try {
|
|
74
|
+
const scratch = Buffer.allocUnsafe(64 * 1024);
|
|
75
|
+
while (true) {
|
|
76
|
+
const { bytesRead } = await sourceHandle.read(scratch, 0, scratch.length, null);
|
|
77
|
+
if (bytesRead === 0) {
|
|
78
|
+
break;
|
|
79
|
+
}
|
|
80
|
+
await writeAll(destinationHandle, scratch, bytesRead);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
finally {
|
|
84
|
+
await destinationHandle.close();
|
|
85
|
+
}
|
|
86
|
+
// Re-check the opened handle before the staged tree can be committed. If
|
|
87
|
+
// the source changed while we copied, the caller should retry the move.
|
|
88
|
+
if (!sameIdentity(params.identity, entryIdentity(await sourceHandle.stat()))) {
|
|
89
|
+
throw sourceChangedError(params.from);
|
|
90
|
+
}
|
|
91
|
+
await fs.chmod(params.to, modeBits(params.mode)).catch(() => undefined);
|
|
92
|
+
}
|
|
93
|
+
catch (error) {
|
|
94
|
+
if (destinationCreated) {
|
|
95
|
+
await fs.rm(params.to, { force: true }).catch(() => undefined);
|
|
96
|
+
}
|
|
97
|
+
throw error;
|
|
98
|
+
}
|
|
99
|
+
finally {
|
|
100
|
+
await sourceHandle.close();
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
async function copyEntryWithManifest(from, to, options) {
|
|
104
|
+
const sourceStat = await fs.lstat(from);
|
|
105
|
+
const identity = entryIdentity(sourceStat);
|
|
106
|
+
if (sourceStat.isSymbolicLink()) {
|
|
107
|
+
await fs.symlink(await fs.readlink(from), to);
|
|
108
|
+
// readlink() is path-based; verify the symlink we copied is still the one
|
|
109
|
+
// we inspected before letting the staged destination become visible.
|
|
110
|
+
await assertSourceStillMatches(from, identity);
|
|
111
|
+
return { ...identity, kind: "leaf" };
|
|
112
|
+
}
|
|
113
|
+
if (sourceStat.isDirectory()) {
|
|
114
|
+
await fs.mkdir(to, { mode: modeBits(sourceStat.mode) || 0o755 });
|
|
115
|
+
const children = [];
|
|
116
|
+
for (const child of await fs.readdir(from)) {
|
|
117
|
+
children.push({
|
|
118
|
+
name: child,
|
|
119
|
+
manifest: await copyEntryWithManifest(path.join(from, child), path.join(to, child), options),
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
// Directory traversal is path-based in Node. Treat a changed parent as a
|
|
123
|
+
// stale move before committing so swapped-in outside trees are not imported.
|
|
124
|
+
await assertSourceStillMatches(from, identity);
|
|
125
|
+
// mkdir() honors process umask. Restore the source mode before commit so
|
|
126
|
+
// EXDEV fallback preserves directory permissions like fs.cp did.
|
|
127
|
+
await fs.chmod(to, modeBits(sourceStat.mode));
|
|
128
|
+
return { ...identity, children, kind: "directory" };
|
|
129
|
+
}
|
|
130
|
+
if (!sourceStat.isFile()) {
|
|
131
|
+
throw new Error(`Refusing to move non-file path with copy fallback: ${from}`);
|
|
132
|
+
}
|
|
133
|
+
if (options.sourceHardlinks === "reject" && sourceStat.nlink > 1) {
|
|
134
|
+
throw new Error(`Refusing to move hardlinked file with copy fallback: ${from}`);
|
|
135
|
+
}
|
|
136
|
+
await copyRegularFilePinned({ from, identity, mode: sourceStat.mode, to });
|
|
137
|
+
return { ...identity, kind: "leaf" };
|
|
138
|
+
}
|
|
139
|
+
function mergeCleanupResults(a, b) {
|
|
140
|
+
return a === "stale" || b === "stale" ? "stale" : "removed";
|
|
141
|
+
}
|
|
142
|
+
async function cleanupCopiedEntry(sourcePath, manifest) {
|
|
143
|
+
let currentStat;
|
|
144
|
+
try {
|
|
145
|
+
currentStat = await fs.lstat(sourcePath);
|
|
146
|
+
}
|
|
147
|
+
catch (error) {
|
|
148
|
+
if (error?.code === "ENOENT") {
|
|
149
|
+
return "removed";
|
|
150
|
+
}
|
|
151
|
+
throw error;
|
|
152
|
+
}
|
|
153
|
+
if (manifest.kind === "directory") {
|
|
154
|
+
if (!currentStat.isDirectory() || !sameDirectoryNode(manifest, entryIdentity(currentStat))) {
|
|
155
|
+
return "stale";
|
|
156
|
+
}
|
|
157
|
+
// A same-inode directory can gain unrelated children after commit. Still
|
|
158
|
+
// clean manifest children so the fallback does not duplicate copied files.
|
|
159
|
+
let result = "removed";
|
|
160
|
+
for (const child of manifest.children) {
|
|
161
|
+
result = mergeCleanupResults(result, await cleanupCopiedEntry(path.join(sourcePath, child.name), child.manifest));
|
|
162
|
+
}
|
|
163
|
+
try {
|
|
164
|
+
await fs.rmdir(sourcePath);
|
|
165
|
+
}
|
|
166
|
+
catch (error) {
|
|
167
|
+
const code = error?.code;
|
|
168
|
+
if (code === "ENOTEMPTY" || code === "EEXIST") {
|
|
169
|
+
return "stale";
|
|
170
|
+
}
|
|
171
|
+
throw error;
|
|
172
|
+
}
|
|
173
|
+
return result;
|
|
174
|
+
}
|
|
175
|
+
if (!sameIdentity(manifest, entryIdentity(currentStat))) {
|
|
176
|
+
return "stale";
|
|
177
|
+
}
|
|
178
|
+
await fs.unlink(sourcePath);
|
|
179
|
+
return "removed";
|
|
180
|
+
}
|
|
2
181
|
export async function movePathWithCopyFallback(options) {
|
|
3
182
|
try {
|
|
4
|
-
await
|
|
183
|
+
await guardedRename({ from: options.from, to: options.to });
|
|
5
184
|
return;
|
|
6
185
|
}
|
|
7
186
|
catch (error) {
|
|
@@ -9,10 +188,19 @@ export async function movePathWithCopyFallback(options) {
|
|
|
9
188
|
throw error;
|
|
10
189
|
}
|
|
11
190
|
}
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
191
|
+
const targetDir = path.dirname(path.resolve(options.to));
|
|
192
|
+
const staged = path.join(targetDir, `.fs-safe-move-${process.pid}-${randomUUID()}.tmp`);
|
|
193
|
+
try {
|
|
194
|
+
const manifest = await copyEntryWithManifest(options.from, staged, {
|
|
195
|
+
sourceHardlinks: options.sourceHardlinks ?? "allow",
|
|
196
|
+
});
|
|
197
|
+
await guardedRename({ from: staged, to: options.to });
|
|
198
|
+
const cleanupResult = await cleanupCopiedEntry(options.from, manifest);
|
|
199
|
+
if (cleanupResult === "stale") {
|
|
200
|
+
throw sourceChangedError(options.from);
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
finally {
|
|
204
|
+
await fs.rm(staged, { recursive: true, force: true }).catch(() => undefined);
|
|
205
|
+
}
|
|
18
206
|
}
|
package/dist/output.d.ts
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
export type ExternalFileWriteOptions<T = void> = {
|
|
2
|
+
rootDir: string;
|
|
3
|
+
path: string;
|
|
4
|
+
write: (filePath: string) => Promise<T>;
|
|
5
|
+
maxBytes?: number;
|
|
6
|
+
mode?: number;
|
|
7
|
+
};
|
|
8
|
+
export type ExternalFileWriteResult<T = void> = {
|
|
9
|
+
path: string;
|
|
10
|
+
result: T;
|
|
11
|
+
};
|
|
12
|
+
export declare function writeExternalFileWithinRoot<T = void>(options: ExternalFileWriteOptions<T>): Promise<ExternalFileWriteResult<T>>;
|
|
13
|
+
//# sourceMappingURL=output.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"output.d.ts","sourceRoot":"","sources":["../src/output.ts"],"names":[],"mappings":"AAOA,MAAM,MAAM,wBAAwB,CAAC,CAAC,GAAG,IAAI,IAAI;IAC/C,OAAO,EAAE,MAAM,CAAC;IAChB,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,CAAC,QAAQ,EAAE,MAAM,KAAK,OAAO,CAAC,CAAC,CAAC,CAAC;IACxC,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,IAAI,CAAC,EAAE,MAAM,CAAC;CACf,CAAC;AAEF,MAAM,MAAM,uBAAuB,CAAC,CAAC,GAAG,IAAI,IAAI;IAC9C,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,CAAC,CAAC;CACX,CAAC;AA6CF,wBAAsB,2BAA2B,CAAC,CAAC,GAAG,IAAI,EACxD,OAAO,EAAE,wBAAwB,CAAC,CAAC,CAAC,GACnC,OAAO,CAAC,uBAAuB,CAAC,CAAC,CAAC,CAAC,CA8BrC"}
|
package/dist/output.js
ADDED
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import { FsSafeError } from "./errors.js";
|
|
3
|
+
import { sanitizeUntrustedFileName } from "./filename.js";
|
|
4
|
+
import { isPathInside } from "./path.js";
|
|
5
|
+
import { root } from "./root.js";
|
|
6
|
+
import { tempFile } from "./temp-target.js";
|
|
7
|
+
function tempFileNameForTarget(targetPath) {
|
|
8
|
+
return sanitizeUntrustedFileName(path.basename(targetPath), "output.bin");
|
|
9
|
+
}
|
|
10
|
+
function ensureTrailingSep(value) {
|
|
11
|
+
return value.endsWith(path.sep) ? value : `${value}${path.sep}`;
|
|
12
|
+
}
|
|
13
|
+
function toRootPathInput(params) {
|
|
14
|
+
if (!path.isAbsolute(params.targetPath)) {
|
|
15
|
+
return params.targetPath;
|
|
16
|
+
}
|
|
17
|
+
const absoluteTarget = path.resolve(params.targetPath);
|
|
18
|
+
const rootDir = path.resolve(params.rootDir);
|
|
19
|
+
if (isPathInside(ensureTrailingSep(rootDir), absoluteTarget)) {
|
|
20
|
+
return path.relative(rootDir, absoluteTarget);
|
|
21
|
+
}
|
|
22
|
+
if (isPathInside(ensureTrailingSep(params.rootReal), absoluteTarget)) {
|
|
23
|
+
return path.relative(params.rootReal, absoluteTarget);
|
|
24
|
+
}
|
|
25
|
+
return params.targetPath;
|
|
26
|
+
}
|
|
27
|
+
function assertFileTargetPath(targetPath) {
|
|
28
|
+
const basename = path.basename(targetPath);
|
|
29
|
+
if (!targetPath ||
|
|
30
|
+
targetPath === "." ||
|
|
31
|
+
targetPath.endsWith("/") ||
|
|
32
|
+
targetPath.endsWith("\\") ||
|
|
33
|
+
!basename ||
|
|
34
|
+
basename === "." ||
|
|
35
|
+
basename === "..") {
|
|
36
|
+
throw new FsSafeError("invalid-path", "target path must name a file");
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
export async function writeExternalFileWithinRoot(options) {
|
|
40
|
+
const targetRoot = await root(options.rootDir);
|
|
41
|
+
const requestedTargetPath = options.path;
|
|
42
|
+
if (requestedTargetPath.length === 0) {
|
|
43
|
+
throw new FsSafeError("invalid-path", "target path is required");
|
|
44
|
+
}
|
|
45
|
+
const targetPath = toRootPathInput({
|
|
46
|
+
rootDir: targetRoot.rootDir,
|
|
47
|
+
rootReal: targetRoot.rootReal,
|
|
48
|
+
targetPath: requestedTargetPath,
|
|
49
|
+
});
|
|
50
|
+
assertFileTargetPath(targetPath);
|
|
51
|
+
const finalPath = await targetRoot.resolve(targetPath);
|
|
52
|
+
const staged = await tempFile({
|
|
53
|
+
prefix: "fs-safe-output",
|
|
54
|
+
fileName: tempFileNameForTarget(targetPath),
|
|
55
|
+
});
|
|
56
|
+
try {
|
|
57
|
+
const result = await options.write(staged.path);
|
|
58
|
+
await targetRoot.copyIn(targetPath, staged.path, {
|
|
59
|
+
maxBytes: options.maxBytes,
|
|
60
|
+
mode: options.mode,
|
|
61
|
+
mkdir: true,
|
|
62
|
+
sourceHardlinks: "reject",
|
|
63
|
+
});
|
|
64
|
+
return { path: finalPath, result };
|
|
65
|
+
}
|
|
66
|
+
finally {
|
|
67
|
+
await staged.cleanup();
|
|
68
|
+
}
|
|
69
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"path-stat.d.ts","sourceRoot":"","sources":["../src/path-stat.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,SAAS,CAAC;AACrC,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,YAAY,CAAC;AAE3C,wBAAgB,iBAAiB,CAAC,IAAI,EAAE,KAAK,GAAG,QAAQ,CAcvD"}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
export function pathStatFromStats(stat) {
|
|
2
|
+
return {
|
|
3
|
+
dev: Number(stat.dev),
|
|
4
|
+
gid: Number(stat.gid),
|
|
5
|
+
ino: Number(stat.ino),
|
|
6
|
+
isDirectory: stat.isDirectory(),
|
|
7
|
+
isFile: stat.isFile(),
|
|
8
|
+
isSymbolicLink: stat.isSymbolicLink(),
|
|
9
|
+
mode: stat.mode,
|
|
10
|
+
mtimeMs: stat.mtimeMs,
|
|
11
|
+
nlink: stat.nlink,
|
|
12
|
+
size: stat.size,
|
|
13
|
+
uid: stat.uid,
|
|
14
|
+
};
|
|
15
|
+
}
|
package/dist/path.d.ts
CHANGED
|
@@ -2,6 +2,7 @@ import fs from "node:fs";
|
|
|
2
2
|
export declare function normalizeWindowsPathForComparison(input: string): string;
|
|
3
3
|
export declare function isNodeError(value: unknown): value is NodeJS.ErrnoException;
|
|
4
4
|
export declare function hasNodeErrorCode(value: unknown, code: string): boolean;
|
|
5
|
+
export declare function assertNoNulPathInput(filePath: string, message?: string): void;
|
|
5
6
|
export declare function isNotFoundPathError(value: unknown): boolean;
|
|
6
7
|
export declare function isSymlinkOpenError(value: unknown): boolean;
|
|
7
8
|
export declare function isPathInside(root: string, target: string): boolean;
|
package/dist/path.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"path.d.ts","sourceRoot":"","sources":["../src/path.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,SAAS,CAAC;AAWzB,wBAAgB,iCAAiC,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CASvE;AAED,wBAAgB,WAAW,CAAC,KAAK,EAAE,OAAO,GAAG,KAAK,IAAI,MAAM,CAAC,cAAc,CAI1E;AAED,wBAAgB,gBAAgB,CAAC,KAAK,EAAE,OAAO,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO,CAEtE;AAED,wBAAgB,mBAAmB,CAAC,KAAK,EAAE,OAAO,GAAG,OAAO,CAE3D;AAED,wBAAgB,kBAAkB,CAAC,KAAK,EAAE,OAAO,GAAG,OAAO,CAE1D;AAED,wBAAgB,YAAY,CAAC,IAAI,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,OAAO,CA0BlE;AAED,wBAAgB,kBAAkB,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,CAG1D;AAED,wBAAgB,WAAW,CAAC,OAAO,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,GAAG,OAAO,CAExE;AAED,wBAAgB,gBAAgB,CAAC,UAAU,EAAE,MAAM,EAAE,KAAK,CAAC,EAAE,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,GAAG,MAAM,GAAG,IAAI,CAa/F;AAED,wBAAgB,wBAAwB,CACtC,QAAQ,EAAE,MAAM,EAChB,aAAa,EAAE,MAAM,EACrB,IAAI,CAAC,EAAE;IAAE,eAAe,CAAC,EAAE,OAAO,CAAC;IAAC,KAAK,CAAC,EAAE,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;CAAE,GAChE,OAAO,CAUT;AAED,wBAAgB,YAAY,CAAC,UAAU,EAAE,MAAM,GAAG,EAAE,CAAC,KAAK,GAAG,IAAI,CAMhE;AAED,wBAAgB,qBAAqB,CAAC,YAAY,EAAE,MAAM,GAAG,MAAM,EAAE,
|
|
1
|
+
{"version":3,"file":"path.d.ts","sourceRoot":"","sources":["../src/path.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,SAAS,CAAC;AAWzB,wBAAgB,iCAAiC,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CASvE;AAED,wBAAgB,WAAW,CAAC,KAAK,EAAE,OAAO,GAAG,KAAK,IAAI,MAAM,CAAC,cAAc,CAI1E;AAED,wBAAgB,gBAAgB,CAAC,KAAK,EAAE,OAAO,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO,CAEtE;AAED,wBAAgB,oBAAoB,CAAC,QAAQ,EAAE,MAAM,EAAE,OAAO,SAA6B,GAAG,IAAI,CAIjG;AAED,wBAAgB,mBAAmB,CAAC,KAAK,EAAE,OAAO,GAAG,OAAO,CAE3D;AAED,wBAAgB,kBAAkB,CAAC,KAAK,EAAE,OAAO,GAAG,OAAO,CAE1D;AAED,wBAAgB,YAAY,CAAC,IAAI,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,OAAO,CA0BlE;AAED,wBAAgB,kBAAkB,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,CAG1D;AAED,wBAAgB,WAAW,CAAC,OAAO,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,GAAG,OAAO,CAExE;AAED,wBAAgB,gBAAgB,CAAC,UAAU,EAAE,MAAM,EAAE,KAAK,CAAC,EAAE,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,GAAG,MAAM,GAAG,IAAI,CAa/F;AAED,wBAAgB,wBAAwB,CACtC,QAAQ,EAAE,MAAM,EAChB,aAAa,EAAE,MAAM,EACrB,IAAI,CAAC,EAAE;IAAE,eAAe,CAAC,EAAE,OAAO,CAAC;IAAC,KAAK,CAAC,EAAE,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;CAAE,GAChE,OAAO,CAUT;AAED,wBAAgB,YAAY,CAAC,UAAU,EAAE,MAAM,GAAG,EAAE,CAAC,KAAK,GAAG,IAAI,CAMhE;AAED,wBAAgB,qBAAqB,CAAC,YAAY,EAAE,MAAM,GAAG,MAAM,EAAE,CAuBpE;AAED,wBAAgB,uBAAuB,CAAC,OAAO,EAAE,MAAM,EAAE,YAAY,EAAE,MAAM,GAAG,MAAM,CAOrF"}
|
package/dist/path.js
CHANGED
|
@@ -22,6 +22,11 @@ export function isNodeError(value) {
|
|
|
22
22
|
export function hasNodeErrorCode(value, code) {
|
|
23
23
|
return isNodeError(value) && value.code === code;
|
|
24
24
|
}
|
|
25
|
+
export function assertNoNulPathInput(filePath, message = "path contains a NUL byte") {
|
|
26
|
+
if (filePath.includes("\0")) {
|
|
27
|
+
throw new FsSafeError("invalid-path", message);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
25
30
|
export function isNotFoundPathError(value) {
|
|
26
31
|
return isNodeError(value) && typeof value.code === "string" && NOT_FOUND_CODES.has(value.code);
|
|
27
32
|
}
|
|
@@ -94,9 +99,7 @@ export function splitSafeRelativePath(relativePath) {
|
|
|
94
99
|
if (relativePath.length === 0 || relativePath === ".") {
|
|
95
100
|
return [];
|
|
96
101
|
}
|
|
97
|
-
|
|
98
|
-
throw new FsSafeError("invalid-path", "relative path contains a NUL byte");
|
|
99
|
-
}
|
|
102
|
+
assertNoNulPathInput(relativePath, "relative path contains a NUL byte");
|
|
100
103
|
if (relativePath.includes("\\")) {
|
|
101
104
|
throw new FsSafeError("invalid-path", "relative path must use forward slashes");
|
|
102
105
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"pinned-write.d.ts","sourceRoot":"","sources":["../src/pinned-write.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"pinned-write.d.ts","sourceRoot":"","sources":["../src/pinned-write.ts"],"names":[],"mappings":"AAIA,OAAO,EAAa,KAAK,QAAQ,EAAE,MAAM,aAAa,CAAC;AAIvD,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,oBAAoB,CAAC;AAS3D,KAAK,gBAAgB,GACjB;IAAE,IAAI,EAAE,QAAQ,CAAC;IAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAAC;IAAC,QAAQ,CAAC,EAAE,cAAc,CAAA;CAAE,GACpE;IAAE,IAAI,EAAE,QAAQ,CAAC;IAAC,MAAM,EAAE,QAAQ,CAAA;CAAE,CAAC;AAuFzC,wBAAsB,oBAAoB,CAAC,MAAM,EAAE;IACjD,QAAQ,EAAE,MAAM,CAAC;IACjB,kBAAkB,EAAE,MAAM,CAAC;IAC3B,QAAQ,EAAE,MAAM,CAAC;IACjB,KAAK,EAAE,OAAO,CAAC;IACf,IAAI,EAAE,MAAM,CAAC;IACb,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,KAAK,EAAE,gBAAgB,CAAC;CACzB,GAAG,OAAO,CAAC,gBAAgB,CAAC,CAuC5B;AAED,wBAAsB,mBAAmB,CAAC,MAAM,EAAE;IAChD,QAAQ,EAAE,MAAM,CAAC;IACjB,kBAAkB,EAAE,MAAM,CAAC;IAC3B,QAAQ,EAAE,MAAM,CAAC;IACjB,KAAK,EAAE,OAAO,CAAC;IACf,IAAI,EAAE,MAAM,CAAC;IACb,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,UAAU,EAAE,MAAM,CAAC;IACnB,cAAc,EAAE,gBAAgB,CAAC;CAClC,GAAG,OAAO,CAAC,gBAAgB,CAAC,CAoB5B"}
|
package/dist/pinned-write.js
CHANGED
|
@@ -1,9 +1,12 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
1
2
|
import fsSync from "node:fs";
|
|
2
3
|
import fs from "node:fs/promises";
|
|
3
4
|
import path from "node:path";
|
|
4
5
|
import { Transform } from "node:stream";
|
|
5
6
|
import { pipeline } from "node:stream/promises";
|
|
7
|
+
import { createNearestExistingDirectoryGuard } from "./directory-guard.js";
|
|
6
8
|
import { FsSafeError } from "./errors.js";
|
|
9
|
+
import { withAsyncDirectoryGuards } from "./guarded-mutation.js";
|
|
7
10
|
import { canFallbackFromPythonError, getFsSafePythonConfig } from "./pinned-python-config.js";
|
|
8
11
|
import { assertPinnedPythonOperationAvailable, runPinnedPythonOperation, validatePinnedOperationPayload, } from "./pinned-python.js";
|
|
9
12
|
function byteLength(input, encoding) {
|
|
@@ -67,13 +70,13 @@ async function inputToBase64(input, maxBytes) {
|
|
|
67
70
|
return Buffer.concat(chunks, bytes).toString("base64");
|
|
68
71
|
}
|
|
69
72
|
export async function runPinnedWriteHelper(params) {
|
|
70
|
-
if (getFsSafePythonConfig().mode === "off") {
|
|
71
|
-
return await runPinnedWriteFallback(params);
|
|
72
|
-
}
|
|
73
73
|
assertSafeBasename(params.basename);
|
|
74
74
|
validatePinnedOperationPayload({
|
|
75
75
|
relativeParentPath: params.relativeParentPath,
|
|
76
76
|
});
|
|
77
|
+
if (getFsSafePythonConfig().mode === "off") {
|
|
78
|
+
return await runPinnedWriteFallback(params);
|
|
79
|
+
}
|
|
77
80
|
if (params.input.kind === "stream") {
|
|
78
81
|
try {
|
|
79
82
|
assertPinnedPythonOperationAvailable();
|
|
@@ -133,12 +136,21 @@ async function runPinnedWriteFallback(params) {
|
|
|
133
136
|
const parentPath = params.relativeParentPath
|
|
134
137
|
? path.join(params.rootPath, ...params.relativeParentPath.split("/"))
|
|
135
138
|
: params.rootPath;
|
|
139
|
+
const parentGuard = await createNearestExistingDirectoryGuard(params.rootPath, parentPath);
|
|
136
140
|
if (params.mkdir) {
|
|
137
|
-
await
|
|
141
|
+
await withAsyncDirectoryGuards([parentGuard], async () => {
|
|
142
|
+
await fs.mkdir(parentPath, { recursive: true });
|
|
143
|
+
});
|
|
138
144
|
}
|
|
139
145
|
const targetPath = path.join(parentPath, params.basename);
|
|
140
146
|
if (params.overwrite === false) {
|
|
141
|
-
|
|
147
|
+
let handle = await withAsyncDirectoryGuards([parentGuard], async () => await fs.open(targetPath, fsSync.constants.O_WRONLY | fsSync.constants.O_CREAT | fsSync.constants.O_EXCL, params.mode), {
|
|
148
|
+
onPostGuardFailure: async (openedHandle) => {
|
|
149
|
+
// The parent failed verification, so targetPath may now resolve
|
|
150
|
+
// somewhere else. Close the fd, but do not clean up by path.
|
|
151
|
+
await openedHandle.close().catch(() => undefined);
|
|
152
|
+
},
|
|
153
|
+
});
|
|
142
154
|
let created = true;
|
|
143
155
|
try {
|
|
144
156
|
if (params.input.kind === "buffer") {
|
|
@@ -164,32 +176,45 @@ async function runPinnedWriteFallback(params) {
|
|
|
164
176
|
}
|
|
165
177
|
}
|
|
166
178
|
}
|
|
167
|
-
const tempPath = path.join(parentPath, `.${params.basename}.fallback.tmp`);
|
|
179
|
+
const tempPath = path.join(parentPath, `.${params.basename}.${randomUUID()}.fallback.tmp`);
|
|
180
|
+
const tempFlags = fsSync.constants.O_WRONLY |
|
|
181
|
+
fsSync.constants.O_CREAT |
|
|
182
|
+
fsSync.constants.O_EXCL |
|
|
183
|
+
(process.platform !== "win32" && "O_NOFOLLOW" in fsSync.constants
|
|
184
|
+
? fsSync.constants.O_NOFOLLOW
|
|
185
|
+
: 0);
|
|
186
|
+
let handle;
|
|
187
|
+
let handleClosedByStream = false;
|
|
168
188
|
try {
|
|
189
|
+
handle = await fs.open(tempPath, tempFlags, params.mode);
|
|
169
190
|
if (params.input.kind === "buffer") {
|
|
170
191
|
assertWithinMaxBytes(byteLength(params.input.data, params.input.encoding), params.maxBytes);
|
|
171
192
|
if (typeof params.input.data === "string") {
|
|
172
|
-
await
|
|
173
|
-
encoding: params.input.encoding ?? "utf8",
|
|
174
|
-
mode: params.mode,
|
|
175
|
-
});
|
|
193
|
+
await handle.writeFile(params.input.data, params.input.encoding ?? "utf8");
|
|
176
194
|
}
|
|
177
195
|
else {
|
|
178
|
-
await
|
|
196
|
+
await handle.writeFile(params.input.data);
|
|
179
197
|
}
|
|
180
198
|
}
|
|
181
199
|
else {
|
|
182
|
-
const
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
}
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
200
|
+
const writable = handle.createWriteStream();
|
|
201
|
+
writable.once("close", () => {
|
|
202
|
+
handleClosedByStream = true;
|
|
203
|
+
});
|
|
204
|
+
await pipelineWithMaxBytes(params.input.stream, writable, params.maxBytes);
|
|
205
|
+
}
|
|
206
|
+
if (!handleClosedByStream) {
|
|
207
|
+
await handle.close().catch(() => undefined);
|
|
208
|
+
handle = undefined;
|
|
189
209
|
}
|
|
190
|
-
await
|
|
210
|
+
await withAsyncDirectoryGuards([parentGuard], async () => {
|
|
211
|
+
await fs.rename(tempPath, targetPath);
|
|
212
|
+
});
|
|
191
213
|
}
|
|
192
214
|
catch (error) {
|
|
215
|
+
if (handle && !handleClosedByStream) {
|
|
216
|
+
await handle.close().catch(() => undefined);
|
|
217
|
+
}
|
|
193
218
|
await fs.rm(tempPath, { force: true }).catch(() => undefined);
|
|
194
219
|
throw error;
|
|
195
220
|
}
|
|
@@ -1,18 +1,16 @@
|
|
|
1
|
-
|
|
2
|
-
export type PrivateStateStoreOptions = {
|
|
1
|
+
export type PrivateFileStore = {
|
|
3
2
|
rootDir: string;
|
|
4
|
-
|
|
5
|
-
export type PrivateStateStore = Omit<FileStore, "readText" | "readJson" | "writeText" | "writeJson"> & {
|
|
3
|
+
path(relativePath: string): string;
|
|
6
4
|
readText(relativePath: string, options?: {
|
|
7
5
|
maxBytes?: number;
|
|
8
6
|
}): Promise<string | null>;
|
|
9
7
|
readJson<T = unknown>(relativePath: string, options?: {
|
|
10
8
|
maxBytes?: number;
|
|
11
9
|
}): Promise<T | null>;
|
|
12
|
-
writeText(relativePath: string, content: string | Uint8Array): Promise<
|
|
10
|
+
writeText(relativePath: string, content: string | Uint8Array): Promise<void>;
|
|
13
11
|
writeJson(relativePath: string, value: unknown, options?: {
|
|
14
12
|
trailingNewline?: boolean;
|
|
15
|
-
}): Promise<
|
|
13
|
+
}): Promise<void>;
|
|
16
14
|
};
|
|
17
15
|
export declare function writePrivateTextAtomic(params: {
|
|
18
16
|
rootDir: string;
|
|
@@ -56,5 +54,5 @@ export declare function writePrivateJsonAtomicSync(params: {
|
|
|
56
54
|
value: unknown;
|
|
57
55
|
trailingNewline?: boolean;
|
|
58
56
|
}): void;
|
|
59
|
-
export declare function
|
|
57
|
+
export declare function privateFileStore(rootDir: string): PrivateFileStore;
|
|
60
58
|
//# sourceMappingURL=private-file-store.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"private-file-store.d.ts","sourceRoot":"","sources":["../src/private-file-store.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"private-file-store.d.ts","sourceRoot":"","sources":["../src/private-file-store.ts"],"names":[],"mappings":"AASA,MAAM,MAAM,gBAAgB,GAAG;IAC7B,OAAO,EAAE,MAAM,CAAC;IAChB,IAAI,CAAC,YAAY,EAAE,MAAM,GAAG,MAAM,CAAC;IACnC,QAAQ,CAAC,YAAY,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE;QAAE,QAAQ,CAAC,EAAE,MAAM,CAAA;KAAE,GAAG,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAAC;IACxF,QAAQ,CAAC,CAAC,GAAG,OAAO,EAAE,YAAY,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE;QAAE,QAAQ,CAAC,EAAE,MAAM,CAAA;KAAE,GAAG,OAAO,CAAC,CAAC,GAAG,IAAI,CAAC,CAAC;IAChG,SAAS,CAAC,YAAY,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,UAAU,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAC7E,SAAS,CAAC,YAAY,EAAE,MAAM,EAAE,KAAK,EAAE,OAAO,EAAE,OAAO,CAAC,EAAE;QAAE,eAAe,CAAC,EAAE,OAAO,CAAA;KAAE,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;CACzG,CAAC;AAgBF,wBAAsB,sBAAsB,CAAC,MAAM,EAAE;IACnD,OAAO,EAAE,MAAM,CAAC;IAChB,QAAQ,EAAE,MAAM,CAAC;IACjB,OAAO,EAAE,MAAM,GAAG,UAAU,CAAC;CAC9B,GAAG,OAAO,CAAC,IAAI,CAAC,CAEhB;AAED,wBAAsB,eAAe,CAAC,MAAM,EAAE;IAC5C,OAAO,EAAE,MAAM,CAAC;IAChB,QAAQ,EAAE,MAAM,CAAC;IACjB,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB,GAAG,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAiBzB;AAoBD,wBAAgB,mBAAmB,CAAC,MAAM,EAAE;IAC1C,OAAO,EAAE,MAAM,CAAC;IAChB,QAAQ,EAAE,MAAM,CAAC;IACjB,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB,GAAG,MAAM,GAAG,IAAI,CAiBhB;AAED,wBAAsB,eAAe,CAAC,CAAC,GAAG,OAAO,EAAE,MAAM,EAAE;IACzD,OAAO,EAAE,MAAM,CAAC;IAChB,QAAQ,EAAE,MAAM,CAAC;IACjB,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB,GAAG,OAAO,CAAC,CAAC,GAAG,IAAI,CAAC,CAGpB;AAED,wBAAgB,mBAAmB,CAAC,CAAC,GAAG,OAAO,EAAE,MAAM,EAAE;IACvD,OAAO,EAAE,MAAM,CAAC;IAChB,QAAQ,EAAE,MAAM,CAAC;IACjB,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB,GAAG,CAAC,GAAG,IAAI,CAGX;AAoCD,wBAAgB,0BAA0B,CAAC,MAAM,EAAE;IACjD,OAAO,EAAE,MAAM,CAAC;IAChB,QAAQ,EAAE,MAAM,CAAC;IACjB,OAAO,EAAE,MAAM,GAAG,UAAU,CAAC;CAC9B,GAAG,IAAI,CA4CP;AAED,wBAAsB,sBAAsB,CAAC,MAAM,EAAE;IACnD,OAAO,EAAE,MAAM,CAAC;IAChB,QAAQ,EAAE,MAAM,CAAC;IACjB,KAAK,EAAE,OAAO,CAAC;IACf,eAAe,CAAC,EAAE,OAAO,CAAC;CAC3B,GAAG,OAAO,CAAC,IAAI,CAAC,CAOhB;AAED,wBAAgB,0BAA0B,CAAC,MAAM,EAAE;IACjD,OAAO,EAAE,MAAM,CAAC;IAChB,QAAQ,EAAE,MAAM,CAAC;IACjB,KAAK,EAAE,OAAO,CAAC;IACf,eAAe,CAAC,EAAE,OAAO,CAAC;CAC3B,GAAG,IAAI,CAOP;AAED,wBAAgB,gBAAgB,CAAC,OAAO,EAAE,MAAM,GAAG,gBAAgB,CAiClE"}
|
|
@@ -2,11 +2,10 @@ import { randomUUID } from "node:crypto";
|
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
import fs from "node:fs";
|
|
4
4
|
import { FsSafeError } from "./errors.js";
|
|
5
|
-
import { fileStore } from "./file-store.js";
|
|
6
5
|
import { isPathInside } from "./path.js";
|
|
7
6
|
import { readRegularFileSync } from "./regular-file.js";
|
|
8
7
|
import { root } from "./root.js";
|
|
9
|
-
import {
|
|
8
|
+
import { writePrivateSecretFileAtomic } from "./secret-file.js";
|
|
10
9
|
function resolvePrivateStorePath(rootDir, relativePath) {
|
|
11
10
|
const root = path.resolve(rootDir);
|
|
12
11
|
const raw = relativePath.trim();
|
|
@@ -21,7 +20,7 @@ function resolvePrivateStorePath(rootDir, relativePath) {
|
|
|
21
20
|
return resolved;
|
|
22
21
|
}
|
|
23
22
|
export async function writePrivateTextAtomic(params) {
|
|
24
|
-
await
|
|
23
|
+
await writePrivateSecretFileAtomic(params);
|
|
25
24
|
}
|
|
26
25
|
export async function readPrivateText(params) {
|
|
27
26
|
const rootDir = path.resolve(params.rootDir);
|
|
@@ -173,7 +172,7 @@ export function writePrivateTextAtomicSync(params) {
|
|
|
173
172
|
}
|
|
174
173
|
export async function writePrivateJsonAtomic(params) {
|
|
175
174
|
const json = JSON.stringify(params.value, null, 2);
|
|
176
|
-
await
|
|
175
|
+
await writePrivateSecretFileAtomic({
|
|
177
176
|
rootDir: params.rootDir,
|
|
178
177
|
filePath: params.filePath,
|
|
179
178
|
content: params.trailingNewline && !json.endsWith("\n") ? `${json}\n` : json,
|
|
@@ -187,47 +186,35 @@ export function writePrivateJsonAtomicSync(params) {
|
|
|
187
186
|
content: params.trailingNewline && !json.endsWith("\n") ? `${json}\n` : json,
|
|
188
187
|
});
|
|
189
188
|
}
|
|
190
|
-
export function
|
|
191
|
-
const
|
|
192
|
-
const store = fileStore({ rootDir, private: true });
|
|
189
|
+
export function privateFileStore(rootDir) {
|
|
190
|
+
const root = path.resolve(rootDir);
|
|
193
191
|
return {
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
const safePath = resolvePrivateStorePath(rootDir, relativePath);
|
|
207
|
-
return await readPrivateJson({
|
|
208
|
-
rootDir,
|
|
209
|
-
filePath: safePath,
|
|
210
|
-
maxBytes: options?.maxBytes,
|
|
211
|
-
});
|
|
212
|
-
},
|
|
192
|
+
rootDir: root,
|
|
193
|
+
path: (relativePath) => resolvePrivateStorePath(root, relativePath),
|
|
194
|
+
readText: async (relativePath, options) => await readPrivateText({
|
|
195
|
+
rootDir: root,
|
|
196
|
+
filePath: resolvePrivateStorePath(root, relativePath),
|
|
197
|
+
maxBytes: options?.maxBytes,
|
|
198
|
+
}),
|
|
199
|
+
readJson: async (relativePath, options) => await readPrivateJson({
|
|
200
|
+
rootDir: root,
|
|
201
|
+
filePath: resolvePrivateStorePath(root, relativePath),
|
|
202
|
+
maxBytes: options?.maxBytes,
|
|
203
|
+
}),
|
|
213
204
|
writeText: async (relativePath, content) => {
|
|
214
|
-
const safePath = resolvePrivateStorePath(rootDir, relativePath);
|
|
215
205
|
await writePrivateTextAtomic({
|
|
216
|
-
rootDir,
|
|
217
|
-
filePath:
|
|
206
|
+
rootDir: root,
|
|
207
|
+
filePath: resolvePrivateStorePath(root, relativePath),
|
|
218
208
|
content,
|
|
219
209
|
});
|
|
220
|
-
return safePath;
|
|
221
210
|
},
|
|
222
211
|
writeJson: async (relativePath, value, options) => {
|
|
223
|
-
const safePath = resolvePrivateStorePath(rootDir, relativePath);
|
|
224
212
|
await writePrivateJsonAtomic({
|
|
225
|
-
rootDir,
|
|
226
|
-
filePath:
|
|
213
|
+
rootDir: root,
|
|
214
|
+
filePath: resolvePrivateStorePath(root, relativePath),
|
|
227
215
|
value,
|
|
228
216
|
trailingNewline: options?.trailingNewline,
|
|
229
217
|
});
|
|
230
|
-
return safePath;
|
|
231
218
|
},
|
|
232
219
|
};
|
|
233
220
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"private-temp-workspace.d.ts","sourceRoot":"","sources":["../src/private-temp-workspace.ts"],"names":[],"mappings":"AAIA,OAAO,EAGL,KAAK,SAAS,EACd,KAAK,aAAa,EACnB,MAAM,iBAAiB,CAAC;
|
|
1
|
+
{"version":3,"file":"private-temp-workspace.d.ts","sourceRoot":"","sources":["../src/private-temp-workspace.ts"],"names":[],"mappings":"AAIA,OAAO,EAGL,KAAK,SAAS,EACd,KAAK,aAAa,EACnB,MAAM,iBAAiB,CAAC;AAIzB,MAAM,MAAM,oBAAoB,GAAG;IACjC,OAAO,EAAE,MAAM,CAAC;IAChB,MAAM,EAAE,MAAM,CAAC;IACf,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,IAAI,CAAC,EAAE,MAAM,CAAC;CACf,CAAC;AAEF,MAAM,MAAM,aAAa,GAAG;IAC1B,GAAG,EAAE,MAAM,CAAC;IACZ,KAAK,EAAE,SAAS,CAAC;IACjB,IAAI,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM,CAAC;IAC/B,KAAK,CAAC,QAAQ,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,UAAU,GAAG,OAAO,CAAC,MAAM,CAAC,CAAC;IACpE,SAAS,CAAC,QAAQ,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAAC;IAC3D,SAAS,CACP,QAAQ,EAAE,MAAM,EAChB,IAAI,EAAE,OAAO,EACb,OAAO,CAAC,EAAE;QAAE,eAAe,CAAC,EAAE,OAAO,CAAA;KAAE,GACtC,OAAO,CAAC,MAAM,CAAC,CAAC;IACnB,MAAM,CAAC,QAAQ,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAAC;IAC9D,IAAI,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAAC;IACxC,OAAO,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;IACzB,CAAC,MAAM,CAAC,YAAY,CAAC,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;CACxC,CAAC;AAEF,MAAM,MAAM,iBAAiB,GAAG;IAC9B,GAAG,EAAE,MAAM,CAAC;IACZ,KAAK,EAAE,aAAa,CAAC;IACrB,IAAI,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM,CAAC;IAC/B,KAAK,CAAC,QAAQ,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,UAAU,GAAG,MAAM,CAAC;IAC3D,SAAS,CAAC,QAAQ,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,MAAM,CAAC;IAClD,SAAS,CAAC,QAAQ,EAAE,MAAM,EAAE,IAAI,EAAE,OAAO,EAAE,OAAO,CAAC,EAAE;QAAE,eAAe,CAAC,EAAE,OAAO,CAAA;KAAE,GAAG,MAAM,CAAC;IAC5F,IAAI,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM,CAAC;IAC/B,OAAO,IAAI,IAAI,CAAC;IAChB,CAAC,MAAM,CAAC,OAAO,CAAC,IAAI,IAAI,CAAC;CAC1B,CAAC;AAsGF,wBAAsB,aAAa,CACjC,OAAO,EAAE,oBAAoB,GAC5B,OAAO,CAAC,aAAa,CAAC,CAExB;AAED,wBAAsB,iBAAiB,CAAC,CAAC,EACvC,OAAO,EAAE,oBAAoB,EAC7B,GAAG,EAAE,CAAC,SAAS,EAAE,aAAa,KAAK,OAAO,CAAC,CAAC,CAAC,GAC5C,OAAO,CAAC,CAAC,CAAC,CAUZ;AAED,wBAAgB,iBAAiB,CAC/B,OAAO,EAAE,oBAAoB,GAC5B,iBAAiB,CAwEnB;AAED,wBAAgB,qBAAqB,CAAC,CAAC,EACrC,OAAO,EAAE,oBAAoB,EAC7B,GAAG,EAAE,CAAC,SAAS,EAAE,iBAAiB,KAAK,CAAC,GACvC,CAAC,CAUH"}
|