@openclaw/fs-safe 0.2.3 → 0.2.5
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 +21 -1
- package/README.md +2 -0
- package/dist/archive-staging.d.ts.map +1 -1
- package/dist/archive-staging.js +12 -1
- package/dist/file-store.d.ts.map +1 -1
- package/dist/file-store.js +6 -5
- package/dist/json-document-store.d.ts +1 -1
- package/dist/json-document-store.d.ts.map +1 -1
- package/dist/json-document-store.js +5 -3
- package/dist/json-durable-queue.d.ts.map +1 -1
- package/dist/json-durable-queue.js +157 -4
- package/dist/json-stringify.d.ts +2 -0
- package/dist/json-stringify.d.ts.map +1 -0
- package/dist/json-stringify.js +7 -0
- package/dist/json.d.ts.map +1 -1
- package/dist/json.js +3 -2
- package/dist/local-roots.d.ts.map +1 -1
- package/dist/local-roots.js +4 -1
- package/dist/path.js +1 -1
- package/dist/permissions.d.ts.map +1 -1
- package/dist/permissions.js +7 -10
- package/dist/pinned-python.d.ts.map +1 -1
- package/dist/pinned-python.js +125 -43
- package/dist/root-impl.d.ts.map +1 -1
- package/dist/root-impl.js +6 -2
- package/dist/root-paths.js +25 -3
- package/dist/secure-file.d.ts.map +1 -1
- package/dist/secure-file.js +3 -1
- package/dist/sidecar-lock.d.ts +8 -1
- package/dist/sidecar-lock.d.ts.map +1 -1
- package/dist/sidecar-lock.js +40 -7
- package/dist/timing.d.ts.map +1 -1
- package/dist/timing.js +8 -1
- package/docs/config.md +2 -2
- package/docs/index.md +1 -1
- package/docs/json-store.md +3 -1
- package/docs/sidecar-lock.md +33 -5
- package/package.json +4 -4
- package/dist/path-scope.d.ts +0 -2
- package/dist/path-scope.d.ts.map +0 -1
- package/dist/path-scope.js +0 -1
- package/dist/private-file-store.d.ts +0 -58
- package/dist/private-file-store.d.ts.map +0 -1
- package/dist/private-file-store.js +0 -220
package/docs/sidecar-lock.md
CHANGED
|
@@ -51,7 +51,7 @@ type FileLockAcquireOptions<TPayload extends Record<string, unknown>> = {
|
|
|
51
51
|
staleMs?: number; // default 30_000
|
|
52
52
|
timeoutMs?: number; // overall acquire deadline; default unbounded
|
|
53
53
|
retry?: FileLockRetryOptions;
|
|
54
|
-
staleRecovery?: "fail-closed";
|
|
54
|
+
staleRecovery?: "fail-closed" | "remove-if-unchanged"; // default "fail-closed"
|
|
55
55
|
allowReentrant?: boolean; // if this process already holds it, increment a count instead of failing
|
|
56
56
|
payload: () => TPayload | Promise<TPayload>;
|
|
57
57
|
shouldReclaim?: (params: {
|
|
@@ -62,6 +62,12 @@ type FileLockAcquireOptions<TPayload extends Record<string, unknown>> = {
|
|
|
62
62
|
nowMs: number;
|
|
63
63
|
heldByThisProcess: boolean;
|
|
64
64
|
}) => boolean | Promise<boolean>;
|
|
65
|
+
shouldRemoveStaleLock?: (snapshot: {
|
|
66
|
+
lockPath: string;
|
|
67
|
+
normalizedTargetPath: string;
|
|
68
|
+
raw: string;
|
|
69
|
+
payload: Record<string, unknown> | null;
|
|
70
|
+
}) => boolean | Promise<boolean>;
|
|
65
71
|
metadata?: Record<string, unknown>; // attached to heldEntries() output for diagnostics
|
|
66
72
|
};
|
|
67
73
|
|
|
@@ -101,7 +107,7 @@ try {
|
|
|
101
107
|
}
|
|
102
108
|
```
|
|
103
109
|
|
|
104
|
-
If your process dies before `release()` runs and skips the exit handler, the sidecar remains. Once `staleMs` elapses (or your `shouldReclaim` returns true), acquisition fails closed instead of deleting by path
|
|
110
|
+
If your process dies before `release()` runs and skips the exit handler, the sidecar remains. Once `staleMs` elapses (or your `shouldReclaim` returns true), acquisition fails closed by default instead of deleting by path.
|
|
105
111
|
|
|
106
112
|
## `withFileLock` — common shape made one-liner
|
|
107
113
|
|
|
@@ -164,18 +170,40 @@ const handle = await acquireFileLock(targetPath, {
|
|
|
164
170
|
});
|
|
165
171
|
```
|
|
166
172
|
|
|
167
|
-
`heldByThisProcess` is true when this manager already holds the lock (relevant for the reentrant case). A `true` result
|
|
173
|
+
`heldByThisProcess` is true when this manager already holds the lock (relevant for the reentrant case). A `true` result marks the observed sidecar as stale; `staleRecovery` then decides whether acquisition fails closed or tries caller-approved removal.
|
|
174
|
+
|
|
175
|
+
## Stale recovery: `remove-if-unchanged`
|
|
176
|
+
|
|
177
|
+
The default `staleRecovery: "fail-closed"` never removes third-party sidecars. Use `staleRecovery: "remove-if-unchanged"` only when your app has a reliable owner-liveness policy and can prove a stale owner cannot still be writing.
|
|
178
|
+
|
|
179
|
+
```ts
|
|
180
|
+
const handle = await acquireFileLock(targetPath, {
|
|
181
|
+
staleMs: 60_000,
|
|
182
|
+
staleRecovery: "remove-if-unchanged",
|
|
183
|
+
payload: () => ({ pid: process.pid, createdAt: new Date().toISOString() }),
|
|
184
|
+
shouldReclaim: ({ payload }) => {
|
|
185
|
+
const pid = Number(payload?.pid);
|
|
186
|
+
return Number.isInteger(pid) && pid > 0 && ownerIsDefinitelyDead(pid);
|
|
187
|
+
},
|
|
188
|
+
shouldRemoveStaleLock: ({ payload }) => {
|
|
189
|
+
const pid = Number(payload?.pid);
|
|
190
|
+
return Number.isInteger(pid) && pid > 0 && ownerIsDefinitelyDead(pid);
|
|
191
|
+
},
|
|
192
|
+
});
|
|
193
|
+
```
|
|
194
|
+
|
|
195
|
+
`shouldRemoveStaleLock` receives the exact lock snapshot that `fs-safe` inspected. `fs-safe` re-reads the sidecar and removes it only if the raw content and file identity are unchanged. If the callback is missing, returns false, or the file changed, acquisition fails closed or keeps retrying according to the normal retry policy.
|
|
168
196
|
|
|
169
197
|
## What sidecar locks defend against
|
|
170
198
|
|
|
171
199
|
- **Two processes writing the same file at once.** `acquire` serializes the critical section.
|
|
172
|
-
- **Accidentally deleting a fresh lock during stale recovery.** Stale third-party locks fail closed
|
|
200
|
+
- **Accidentally deleting a fresh lock during stale recovery.** Stale third-party locks fail closed by default. Opt-in removal rechecks the observed snapshot before unlinking.
|
|
173
201
|
- **Race between simultaneous acquire attempts.** `O_CREAT | O_EXCL` ensures one wins.
|
|
174
202
|
|
|
175
203
|
## What they do **not** defend against
|
|
176
204
|
|
|
177
205
|
- **Misbehaving holders that ignore the lock.** Locks are advisory — only callers that go through `acquire` are bound.
|
|
178
|
-
- **Automatic stale lock deletion.** If a process crashes, use the payload and your own supervisor/process table to decide when
|
|
206
|
+
- **Automatic stale lock deletion.** If a process crashes, use the payload and your own supervisor/process table to decide when removal is safe, then opt into `remove-if-unchanged`.
|
|
179
207
|
- **Multi-host coordination over network filesystems.** Behavior depends on the underlying filesystem's `O_EXCL` semantics; treat as best-effort.
|
|
180
208
|
|
|
181
209
|
## Common patterns
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@openclaw/fs-safe",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.5",
|
|
4
4
|
"description": "Capability-style filesystem roots for Node.js apps that handle untrusted relative paths.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -99,7 +99,7 @@
|
|
|
99
99
|
},
|
|
100
100
|
"scripts": {
|
|
101
101
|
"benchmark": "node scripts/benchmark.mjs",
|
|
102
|
-
"build": "tsc -p tsconfig.json",
|
|
102
|
+
"build": "node -e \"require('fs').rmSync('dist',{recursive:true,force:true})\" && tsc -p tsconfig.json",
|
|
103
103
|
"lint:file-size": "node scripts/check-file-size.mjs",
|
|
104
104
|
"lint:fs-boundary": "node scripts/check-fs-boundary-primitives.mjs",
|
|
105
105
|
"prepack": "node scripts/prepack-build.mjs",
|
|
@@ -115,9 +115,9 @@
|
|
|
115
115
|
},
|
|
116
116
|
"devDependencies": {
|
|
117
117
|
"@types/node": "^22.15.19",
|
|
118
|
-
"@vitest/coverage-v8": "
|
|
118
|
+
"@vitest/coverage-v8": "4.1.6",
|
|
119
119
|
"typescript": "^5.8.3",
|
|
120
|
-
"vitest": "^
|
|
120
|
+
"vitest": "^4.1.6"
|
|
121
121
|
},
|
|
122
122
|
"engines": {
|
|
123
123
|
"node": ">=20.11"
|
package/dist/path-scope.d.ts
DELETED
package/dist/path-scope.d.ts.map
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"path-scope.d.ts","sourceRoot":"","sources":["../src/path-scope.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,SAAS,EACT,KAAK,SAAS,EACd,KAAK,gBAAgB,EACrB,KAAK,uBAAuB,GAC7B,MAAM,iBAAiB,CAAC"}
|
package/dist/path-scope.js
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export { pathScope, } from "./root-paths.js";
|
|
@@ -1,58 +0,0 @@
|
|
|
1
|
-
export type PrivateFileStore = {
|
|
2
|
-
rootDir: string;
|
|
3
|
-
path(relativePath: string): string;
|
|
4
|
-
readText(relativePath: string, options?: {
|
|
5
|
-
maxBytes?: number;
|
|
6
|
-
}): Promise<string | null>;
|
|
7
|
-
readJson<T = unknown>(relativePath: string, options?: {
|
|
8
|
-
maxBytes?: number;
|
|
9
|
-
}): Promise<T | null>;
|
|
10
|
-
writeText(relativePath: string, content: string | Uint8Array): Promise<void>;
|
|
11
|
-
writeJson(relativePath: string, value: unknown, options?: {
|
|
12
|
-
trailingNewline?: boolean;
|
|
13
|
-
}): Promise<void>;
|
|
14
|
-
};
|
|
15
|
-
export declare function writePrivateTextAtomic(params: {
|
|
16
|
-
rootDir: string;
|
|
17
|
-
filePath: string;
|
|
18
|
-
content: string | Uint8Array;
|
|
19
|
-
}): Promise<void>;
|
|
20
|
-
export declare function readPrivateText(params: {
|
|
21
|
-
rootDir: string;
|
|
22
|
-
filePath: string;
|
|
23
|
-
maxBytes?: number;
|
|
24
|
-
}): Promise<string | null>;
|
|
25
|
-
export declare function readPrivateTextSync(params: {
|
|
26
|
-
rootDir: string;
|
|
27
|
-
filePath: string;
|
|
28
|
-
maxBytes?: number;
|
|
29
|
-
}): string | null;
|
|
30
|
-
export declare function readPrivateJson<T = unknown>(params: {
|
|
31
|
-
rootDir: string;
|
|
32
|
-
filePath: string;
|
|
33
|
-
maxBytes?: number;
|
|
34
|
-
}): Promise<T | null>;
|
|
35
|
-
export declare function readPrivateJsonSync<T = unknown>(params: {
|
|
36
|
-
rootDir: string;
|
|
37
|
-
filePath: string;
|
|
38
|
-
maxBytes?: number;
|
|
39
|
-
}): T | null;
|
|
40
|
-
export declare function writePrivateTextAtomicSync(params: {
|
|
41
|
-
rootDir: string;
|
|
42
|
-
filePath: string;
|
|
43
|
-
content: string | Uint8Array;
|
|
44
|
-
}): void;
|
|
45
|
-
export declare function writePrivateJsonAtomic(params: {
|
|
46
|
-
rootDir: string;
|
|
47
|
-
filePath: string;
|
|
48
|
-
value: unknown;
|
|
49
|
-
trailingNewline?: boolean;
|
|
50
|
-
}): Promise<void>;
|
|
51
|
-
export declare function writePrivateJsonAtomicSync(params: {
|
|
52
|
-
rootDir: string;
|
|
53
|
-
filePath: string;
|
|
54
|
-
value: unknown;
|
|
55
|
-
trailingNewline?: boolean;
|
|
56
|
-
}): void;
|
|
57
|
-
export declare function privateFileStore(rootDir: string): PrivateFileStore;
|
|
58
|
-
//# sourceMappingURL=private-file-store.d.ts.map
|
|
@@ -1 +0,0 @@
|
|
|
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"}
|
|
@@ -1,220 +0,0 @@
|
|
|
1
|
-
import { randomUUID } from "node:crypto";
|
|
2
|
-
import path from "node:path";
|
|
3
|
-
import fs from "node:fs";
|
|
4
|
-
import { FsSafeError } from "./errors.js";
|
|
5
|
-
import { isPathInside } from "./path.js";
|
|
6
|
-
import { readRegularFileSync } from "./regular-file.js";
|
|
7
|
-
import { root } from "./root.js";
|
|
8
|
-
import { writePrivateSecretFileAtomic } from "./secret-file.js";
|
|
9
|
-
function resolvePrivateStorePath(rootDir, relativePath) {
|
|
10
|
-
const root = path.resolve(rootDir);
|
|
11
|
-
const raw = relativePath.trim();
|
|
12
|
-
if (!raw || path.isAbsolute(raw) || raw.includes("\0")) {
|
|
13
|
-
throw new Error("Private file path must be a relative path.");
|
|
14
|
-
}
|
|
15
|
-
const resolved = path.resolve(root, raw);
|
|
16
|
-
const rel = path.relative(root, resolved);
|
|
17
|
-
if (!rel || rel.startsWith("..") || path.isAbsolute(rel)) {
|
|
18
|
-
throw new Error("Private file path must stay under the private store root.");
|
|
19
|
-
}
|
|
20
|
-
return resolved;
|
|
21
|
-
}
|
|
22
|
-
export async function writePrivateTextAtomic(params) {
|
|
23
|
-
await writePrivateSecretFileAtomic(params);
|
|
24
|
-
}
|
|
25
|
-
export async function readPrivateText(params) {
|
|
26
|
-
const rootDir = path.resolve(params.rootDir);
|
|
27
|
-
const filePath = path.resolve(params.filePath);
|
|
28
|
-
const relative = path.relative(rootDir, filePath);
|
|
29
|
-
if (!relative || relative.startsWith("..") || path.isAbsolute(relative)) {
|
|
30
|
-
throw new Error("Private file path must stay under the private store root.");
|
|
31
|
-
}
|
|
32
|
-
try {
|
|
33
|
-
const storeRoot = await root(rootDir, { hardlinks: "reject", maxBytes: params.maxBytes });
|
|
34
|
-
const result = await storeRoot.read(relative);
|
|
35
|
-
return result.buffer.toString("utf8");
|
|
36
|
-
}
|
|
37
|
-
catch (err) {
|
|
38
|
-
if (err instanceof FsSafeError && err.code === "not-found") {
|
|
39
|
-
return null;
|
|
40
|
-
}
|
|
41
|
-
throw err;
|
|
42
|
-
}
|
|
43
|
-
}
|
|
44
|
-
function assertPrivateReadPathSync(params) {
|
|
45
|
-
const rootDir = path.resolve(params.rootDir);
|
|
46
|
-
const filePath = path.resolve(params.filePath);
|
|
47
|
-
const relative = path.relative(rootDir, filePath);
|
|
48
|
-
if (!relative || relative.startsWith("..") || path.isAbsolute(relative)) {
|
|
49
|
-
throw new Error("Private file path must stay under the private store root.");
|
|
50
|
-
}
|
|
51
|
-
const rootStat = fs.lstatSync(rootDir);
|
|
52
|
-
if (rootStat.isSymbolicLink() || !rootStat.isDirectory()) {
|
|
53
|
-
throw new Error(`Private file root must be a directory: ${rootDir}`);
|
|
54
|
-
}
|
|
55
|
-
const rootReal = fs.realpathSync(rootDir);
|
|
56
|
-
const fileReal = fs.realpathSync(filePath);
|
|
57
|
-
if (!isPathInside(rootReal, fileReal)) {
|
|
58
|
-
throw new Error("Private file path must stay under the private store root.");
|
|
59
|
-
}
|
|
60
|
-
}
|
|
61
|
-
export function readPrivateTextSync(params) {
|
|
62
|
-
try {
|
|
63
|
-
assertPrivateReadPathSync(params);
|
|
64
|
-
const result = readRegularFileSync({
|
|
65
|
-
filePath: path.resolve(params.filePath),
|
|
66
|
-
maxBytes: params.maxBytes,
|
|
67
|
-
});
|
|
68
|
-
if (result.stat.nlink > 1) {
|
|
69
|
-
throw new Error(`Private file target must not be hardlinked: ${params.filePath}`);
|
|
70
|
-
}
|
|
71
|
-
return result.buffer.toString("utf8");
|
|
72
|
-
}
|
|
73
|
-
catch (err) {
|
|
74
|
-
if (err.code === "ENOENT") {
|
|
75
|
-
return null;
|
|
76
|
-
}
|
|
77
|
-
throw err;
|
|
78
|
-
}
|
|
79
|
-
}
|
|
80
|
-
export async function readPrivateJson(params) {
|
|
81
|
-
const raw = await readPrivateText(params);
|
|
82
|
-
return raw === null ? null : JSON.parse(raw);
|
|
83
|
-
}
|
|
84
|
-
export function readPrivateJsonSync(params) {
|
|
85
|
-
const raw = readPrivateTextSync(params);
|
|
86
|
-
return raw === null ? null : JSON.parse(raw);
|
|
87
|
-
}
|
|
88
|
-
function ensurePrivateDirectorySync(rootDir, targetDir) {
|
|
89
|
-
const root = path.resolve(rootDir);
|
|
90
|
-
const target = path.resolve(targetDir);
|
|
91
|
-
const relative = path.relative(root, target);
|
|
92
|
-
if (relative.startsWith("..") || path.isAbsolute(relative)) {
|
|
93
|
-
throw new Error("Private file directory must stay under the private store root.");
|
|
94
|
-
}
|
|
95
|
-
let current = root;
|
|
96
|
-
fs.mkdirSync(current, { recursive: true, mode: 0o700 });
|
|
97
|
-
const rootStat = fs.lstatSync(current);
|
|
98
|
-
if (rootStat.isSymbolicLink() || !rootStat.isDirectory()) {
|
|
99
|
-
throw new Error(`Private file root must be a directory: ${current}`);
|
|
100
|
-
}
|
|
101
|
-
for (const segment of relative.split(path.sep).filter(Boolean)) {
|
|
102
|
-
current = path.join(current, segment);
|
|
103
|
-
try {
|
|
104
|
-
const stat = fs.lstatSync(current);
|
|
105
|
-
if (stat.isSymbolicLink() || !stat.isDirectory()) {
|
|
106
|
-
throw new Error(`Private file directory component must be a directory: ${current}`);
|
|
107
|
-
}
|
|
108
|
-
}
|
|
109
|
-
catch (err) {
|
|
110
|
-
if (err.code !== "ENOENT") {
|
|
111
|
-
throw err;
|
|
112
|
-
}
|
|
113
|
-
fs.mkdirSync(current, { mode: 0o700 });
|
|
114
|
-
}
|
|
115
|
-
try {
|
|
116
|
-
fs.chmodSync(current, 0o700);
|
|
117
|
-
}
|
|
118
|
-
catch {
|
|
119
|
-
// Best-effort on platforms that do not enforce POSIX modes.
|
|
120
|
-
}
|
|
121
|
-
}
|
|
122
|
-
}
|
|
123
|
-
export function writePrivateTextAtomicSync(params) {
|
|
124
|
-
const root = path.resolve(params.rootDir);
|
|
125
|
-
const filePath = path.resolve(params.filePath);
|
|
126
|
-
const relative = path.relative(root, filePath);
|
|
127
|
-
if (!relative || relative.startsWith("..") || path.isAbsolute(relative)) {
|
|
128
|
-
throw new Error("Private file path must stay under the private store root.");
|
|
129
|
-
}
|
|
130
|
-
ensurePrivateDirectorySync(root, path.dirname(filePath));
|
|
131
|
-
try {
|
|
132
|
-
const stat = fs.lstatSync(filePath);
|
|
133
|
-
if (stat.isSymbolicLink() || !stat.isFile()) {
|
|
134
|
-
throw new Error(`Private file target must be a regular file: ${filePath}`);
|
|
135
|
-
}
|
|
136
|
-
}
|
|
137
|
-
catch (err) {
|
|
138
|
-
if (err.code !== "ENOENT") {
|
|
139
|
-
throw err;
|
|
140
|
-
}
|
|
141
|
-
}
|
|
142
|
-
const tempPath = path.join(path.dirname(filePath), `.tmp-${process.pid}-${randomUUID()}`);
|
|
143
|
-
let created = false;
|
|
144
|
-
try {
|
|
145
|
-
fs.writeFileSync(tempPath, params.content, { mode: 0o600, flag: "wx" });
|
|
146
|
-
created = true;
|
|
147
|
-
try {
|
|
148
|
-
fs.chmodSync(tempPath, 0o600);
|
|
149
|
-
}
|
|
150
|
-
catch {
|
|
151
|
-
// Best-effort on platforms that do not enforce POSIX modes.
|
|
152
|
-
}
|
|
153
|
-
fs.renameSync(tempPath, filePath);
|
|
154
|
-
created = false;
|
|
155
|
-
try {
|
|
156
|
-
fs.chmodSync(filePath, 0o600);
|
|
157
|
-
}
|
|
158
|
-
catch {
|
|
159
|
-
// Best-effort on platforms that do not enforce POSIX modes.
|
|
160
|
-
}
|
|
161
|
-
}
|
|
162
|
-
finally {
|
|
163
|
-
if (created) {
|
|
164
|
-
try {
|
|
165
|
-
fs.unlinkSync(tempPath);
|
|
166
|
-
}
|
|
167
|
-
catch {
|
|
168
|
-
// The temp file is best-effort cleanup after write failure.
|
|
169
|
-
}
|
|
170
|
-
}
|
|
171
|
-
}
|
|
172
|
-
}
|
|
173
|
-
export async function writePrivateJsonAtomic(params) {
|
|
174
|
-
const json = JSON.stringify(params.value, null, 2);
|
|
175
|
-
await writePrivateSecretFileAtomic({
|
|
176
|
-
rootDir: params.rootDir,
|
|
177
|
-
filePath: params.filePath,
|
|
178
|
-
content: params.trailingNewline && !json.endsWith("\n") ? `${json}\n` : json,
|
|
179
|
-
});
|
|
180
|
-
}
|
|
181
|
-
export function writePrivateJsonAtomicSync(params) {
|
|
182
|
-
const json = JSON.stringify(params.value, null, 2);
|
|
183
|
-
writePrivateTextAtomicSync({
|
|
184
|
-
rootDir: params.rootDir,
|
|
185
|
-
filePath: params.filePath,
|
|
186
|
-
content: params.trailingNewline && !json.endsWith("\n") ? `${json}\n` : json,
|
|
187
|
-
});
|
|
188
|
-
}
|
|
189
|
-
export function privateFileStore(rootDir) {
|
|
190
|
-
const root = path.resolve(rootDir);
|
|
191
|
-
return {
|
|
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
|
-
}),
|
|
204
|
-
writeText: async (relativePath, content) => {
|
|
205
|
-
await writePrivateTextAtomic({
|
|
206
|
-
rootDir: root,
|
|
207
|
-
filePath: resolvePrivateStorePath(root, relativePath),
|
|
208
|
-
content,
|
|
209
|
-
});
|
|
210
|
-
},
|
|
211
|
-
writeJson: async (relativePath, value, options) => {
|
|
212
|
-
await writePrivateJsonAtomic({
|
|
213
|
-
rootDir: root,
|
|
214
|
-
filePath: resolvePrivateStorePath(root, relativePath),
|
|
215
|
-
value,
|
|
216
|
-
trailingNewline: options?.trailingNewline,
|
|
217
|
-
});
|
|
218
|
-
},
|
|
219
|
-
};
|
|
220
|
-
}
|