@openclaw/fs-safe 0.1.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 +15 -0
- package/LICENSE +21 -0
- package/README.md +390 -0
- package/SECURITY.md +12 -0
- package/dist/absolute-path.d.ts +19 -0
- package/dist/absolute-path.d.ts.map +1 -0
- package/dist/absolute-path.js +98 -0
- package/dist/advanced.d.ts +22 -0
- package/dist/advanced.d.ts.map +1 -0
- package/dist/advanced.js +24 -0
- package/dist/archive-entry.d.ts +13 -0
- package/dist/archive-entry.d.ts.map +1 -0
- package/dist/archive-entry.js +47 -0
- package/dist/archive-kind.d.ts +8 -0
- package/dist/archive-kind.d.ts.map +1 -0
- package/dist/archive-kind.js +58 -0
- package/dist/archive-limits.d.ts +40 -0
- package/dist/archive-limits.d.ts.map +1 -0
- package/dist/archive-limits.js +92 -0
- package/dist/archive-staging.d.ts +26 -0
- package/dist/archive-staging.d.ts.map +1 -0
- package/dist/archive-staging.js +171 -0
- package/dist/archive-tar.d.ts +14 -0
- package/dist/archive-tar.d.ts.map +1 -0
- package/dist/archive-tar.js +52 -0
- package/dist/archive-utils.d.ts +3 -0
- package/dist/archive-utils.d.ts.map +1 -0
- package/dist/archive-utils.js +26 -0
- package/dist/archive-zip-preflight.d.ts +7 -0
- package/dist/archive-zip-preflight.d.ts.map +1 -0
- package/dist/archive-zip-preflight.js +170 -0
- package/dist/archive.d.ts +24 -0
- package/dist/archive.d.ts.map +1 -0
- package/dist/archive.js +251 -0
- package/dist/async-lock.d.ts +2 -0
- package/dist/async-lock.d.ts.map +1 -0
- package/dist/async-lock.js +17 -0
- package/dist/atomic.d.ts +5 -0
- package/dist/atomic.d.ts.map +1 -0
- package/dist/atomic.js +4 -0
- package/dist/boundary-file-read.d.ts +44 -0
- package/dist/boundary-file-read.d.ts.map +1 -0
- package/dist/boundary-file-read.js +129 -0
- package/dist/boundary-path.d.ts +39 -0
- package/dist/boundary-path.d.ts.map +1 -0
- package/dist/boundary-path.js +598 -0
- package/dist/config.d.ts +2 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/config.js +1 -0
- package/dist/errors.d.ts +11 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/errors.js +20 -0
- package/dist/file-identity.d.ts +6 -0
- package/dist/file-identity.d.ts.map +1 -0
- package/dist/file-identity.js +14 -0
- package/dist/file-lock.d.ts +20 -0
- package/dist/file-lock.d.ts.map +1 -0
- package/dist/file-lock.js +34 -0
- package/dist/file-store.d.ts +66 -0
- package/dist/file-store.d.ts.map +1 -0
- package/dist/file-store.js +436 -0
- package/dist/file-url.d.ts +2 -0
- package/dist/file-url.d.ts.map +1 -0
- package/dist/file-url.js +1 -0
- package/dist/filename.d.ts +2 -0
- package/dist/filename.d.ts.map +1 -0
- package/dist/filename.js +25 -0
- package/dist/fs-pinned-path-helper.d.ts +7 -0
- package/dist/fs-pinned-path-helper.d.ts.map +1 -0
- package/dist/fs-pinned-path-helper.js +182 -0
- package/dist/fs-pinned-write-helper.d.ts +21 -0
- package/dist/fs-pinned-write-helper.d.ts.map +1 -0
- package/dist/fs-pinned-write-helper.js +263 -0
- package/dist/fs.d.ts +13 -0
- package/dist/fs.d.ts.map +1 -0
- package/dist/fs.js +30 -0
- package/dist/hardlink-guards.d.ts +7 -0
- package/dist/hardlink-guards.d.ts.map +1 -0
- package/dist/hardlink-guards.js +30 -0
- package/dist/home-dir.d.ts +22 -0
- package/dist/home-dir.d.ts.map +1 -0
- package/dist/home-dir.js +107 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +3 -0
- package/dist/install-path.d.ts +20 -0
- package/dist/install-path.d.ts.map +1 -0
- package/dist/install-path.js +94 -0
- package/dist/install-safe-path.d.ts +20 -0
- package/dist/install-safe-path.d.ts.map +1 -0
- package/dist/install-safe-path.js +94 -0
- package/dist/json-document-store.d.ts +30 -0
- package/dist/json-document-store.d.ts.map +1 -0
- package/dist/json-document-store.js +68 -0
- package/dist/json-file.d.ts +3 -0
- package/dist/json-file.d.ts.map +1 -0
- package/dist/json-file.js +123 -0
- package/dist/json-files.d.ts +20 -0
- package/dist/json-files.d.ts.map +1 -0
- package/dist/json-files.js +153 -0
- package/dist/json-store.d.ts +9 -0
- package/dist/json-store.d.ts.map +1 -0
- package/dist/json-store.js +12 -0
- package/dist/json.d.ts +17 -0
- package/dist/json.d.ts.map +1 -0
- package/dist/json.js +198 -0
- package/dist/local-file-access.d.ts +8 -0
- package/dist/local-file-access.d.ts.map +1 -0
- package/dist/local-file-access.js +73 -0
- package/dist/local-roots.d.ts +26 -0
- package/dist/local-roots.d.ts.map +1 -0
- package/dist/local-roots.js +150 -0
- package/dist/mode.d.ts +2 -0
- package/dist/mode.d.ts.map +1 -0
- package/dist/mode.js +3 -0
- package/dist/move-path.d.ts +6 -0
- package/dist/move-path.d.ts.map +1 -0
- package/dist/move-path.js +18 -0
- package/dist/path-alias-guards.d.ts +19 -0
- package/dist/path-alias-guards.d.ts.map +1 -0
- package/dist/path-alias-guards.js +21 -0
- package/dist/path-guards.d.ts +7 -0
- package/dist/path-guards.d.ts.map +1 -0
- package/dist/path-guards.js +49 -0
- package/dist/path-policy.d.ts +25 -0
- package/dist/path-policy.d.ts.map +1 -0
- package/dist/path-policy.js +50 -0
- package/dist/path-safety.d.ts +12 -0
- package/dist/path-safety.d.ts.map +1 -0
- package/dist/path-safety.js +50 -0
- package/dist/path-scope.d.ts +2 -0
- package/dist/path-scope.d.ts.map +1 -0
- package/dist/path-scope.js +1 -0
- package/dist/path.d.ts +18 -0
- package/dist/path.d.ts.map +1 -0
- package/dist/path.js +123 -0
- package/dist/permissions-public.d.ts +2 -0
- package/dist/permissions-public.d.ts.map +1 -0
- package/dist/permissions-public.js +1 -0
- package/dist/permissions.d.ts +86 -0
- package/dist/permissions.d.ts.map +1 -0
- package/dist/permissions.js +435 -0
- package/dist/pinned-helper.d.ts +9 -0
- package/dist/pinned-helper.d.ts.map +1 -0
- package/dist/pinned-helper.js +19 -0
- package/dist/pinned-open.d.ts +24 -0
- package/dist/pinned-open.d.ts.map +1 -0
- package/dist/pinned-open.js +71 -0
- package/dist/pinned-path.d.ts +7 -0
- package/dist/pinned-path.d.ts.map +1 -0
- package/dist/pinned-path.js +21 -0
- package/dist/pinned-python-config.d.ts +9 -0
- package/dist/pinned-python-config.d.ts.map +1 -0
- package/dist/pinned-python-config.js +39 -0
- package/dist/pinned-python.d.ts +12 -0
- package/dist/pinned-python.d.ts.map +1 -0
- package/dist/pinned-python.js +599 -0
- package/dist/pinned-write.d.ts +33 -0
- package/dist/pinned-write.d.ts.map +1 -0
- package/dist/pinned-write.js +198 -0
- package/dist/private-file-store.d.ts +60 -0
- package/dist/private-file-store.d.ts.map +1 -0
- package/dist/private-file-store.js +233 -0
- package/dist/private-temp-workspace.d.ts +39 -0
- package/dist/private-temp-workspace.d.ts.map +1 -0
- package/dist/private-temp-workspace.js +185 -0
- package/dist/regular-file.d.ts +38 -0
- package/dist/regular-file.d.ts.map +1 -0
- package/dist/regular-file.js +239 -0
- package/dist/replace-directory.d.ts +7 -0
- package/dist/replace-directory.d.ts.map +1 -0
- package/dist/replace-directory.js +32 -0
- package/dist/replace-file.d.ts +41 -0
- package/dist/replace-file.d.ts.map +1 -0
- package/dist/replace-file.js +325 -0
- package/dist/root-file.d.ts +44 -0
- package/dist/root-file.d.ts.map +1 -0
- package/dist/root-file.js +129 -0
- package/dist/root-path.d.ts +39 -0
- package/dist/root-path.d.ts.map +1 -0
- package/dist/root-path.js +598 -0
- package/dist/root-paths.d.ts +92 -0
- package/dist/root-paths.d.ts.map +1 -0
- package/dist/root-paths.js +338 -0
- package/dist/root.d.ts +108 -0
- package/dist/root.d.ts.map +1 -0
- package/dist/root.js +1332 -0
- package/dist/safe-open-sync.d.ts +24 -0
- package/dist/safe-open-sync.d.ts.map +1 -0
- package/dist/safe-open-sync.js +71 -0
- package/dist/safe-root.d.ts +123 -0
- package/dist/safe-root.d.ts.map +1 -0
- package/dist/safe-root.js +1060 -0
- package/dist/secret-file.d.ts +17 -0
- package/dist/secret-file.d.ts.map +1 -0
- package/dist/secret-file.js +232 -0
- package/dist/secret.d.ts +2 -0
- package/dist/secret.d.ts.map +1 -0
- package/dist/secret.js +1 -0
- package/dist/secure-file.d.ts +32 -0
- package/dist/secure-file.d.ts.map +1 -0
- package/dist/secure-file.js +163 -0
- package/dist/secure-temp-dir.d.ts +27 -0
- package/dist/secure-temp-dir.d.ts.map +1 -0
- package/dist/secure-temp-dir.js +155 -0
- package/dist/secure-temp-workspace.d.ts +25 -0
- package/dist/secure-temp-workspace.d.ts.map +1 -0
- package/dist/secure-temp-workspace.js +136 -0
- package/dist/sibling-temp-file.d.ts +16 -0
- package/dist/sibling-temp-file.d.ts.map +1 -0
- package/dist/sibling-temp-file.js +73 -0
- package/dist/sibling-temp-write.d.ts +8 -0
- package/dist/sibling-temp-write.d.ts.map +1 -0
- package/dist/sibling-temp-write.js +40 -0
- package/dist/sibling-temp.d.ts +23 -0
- package/dist/sibling-temp.d.ts.map +1 -0
- package/dist/sibling-temp.js +119 -0
- package/dist/sidecar-lock.d.ts +50 -0
- package/dist/sidecar-lock.d.ts.map +1 -0
- package/dist/sidecar-lock.js +235 -0
- package/dist/store.d.ts +3 -0
- package/dist/store.d.ts.map +1 -0
- package/dist/store.js +2 -0
- package/dist/string-coerce.d.ts +14 -0
- package/dist/string-coerce.d.ts.map +1 -0
- package/dist/string-coerce.js +72 -0
- package/dist/symlink-parents.d.ts +12 -0
- package/dist/symlink-parents.d.ts.map +1 -0
- package/dist/symlink-parents.js +78 -0
- package/dist/temp-cleanup.d.ts +6 -0
- package/dist/temp-cleanup.d.ts.map +1 -0
- package/dist/temp-cleanup.js +42 -0
- package/dist/temp-target.d.ts +28 -0
- package/dist/temp-target.d.ts.map +1 -0
- package/dist/temp-target.js +84 -0
- package/dist/temp.d.ts +3 -0
- package/dist/temp.d.ts.map +1 -0
- package/dist/temp.js +2 -0
- package/dist/test-hooks.d.ts +9 -0
- package/dist/test-hooks.d.ts.map +1 -0
- package/dist/test-hooks.js +13 -0
- package/dist/text-atomic.d.ts +7 -0
- package/dist/text-atomic.d.ts.map +1 -0
- package/dist/text-atomic.js +13 -0
- package/dist/timing.d.ts +6 -0
- package/dist/timing.d.ts.map +1 -0
- package/dist/timing.js +24 -0
- package/dist/trash.d.ts +5 -0
- package/dist/trash.d.ts.map +1 -0
- package/dist/trash.js +127 -0
- package/dist/types.d.ts +23 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +1 -0
- package/dist/walk.d.ts +26 -0
- package/dist/walk.d.ts.map +1 -0
- package/dist/walk.js +169 -0
- package/dist/write-queue.d.ts +2 -0
- package/dist/write-queue.d.ts.map +1 -0
- package/dist/write-queue.js +18 -0
- package/package.json +117 -0
package/dist/home-dir.js
ADDED
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import os from "node:os";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { normalizeOptionalString } from "./string-coerce.js";
|
|
4
|
+
function normalize(value) {
|
|
5
|
+
const trimmed = normalizeOptionalString(value);
|
|
6
|
+
if (!trimmed) {
|
|
7
|
+
return undefined;
|
|
8
|
+
}
|
|
9
|
+
if (trimmed === "undefined" || trimmed === "null") {
|
|
10
|
+
return undefined;
|
|
11
|
+
}
|
|
12
|
+
return trimmed;
|
|
13
|
+
}
|
|
14
|
+
export function resolveEffectiveHomeDir(env = process.env, homedir = os.homedir) {
|
|
15
|
+
const raw = resolveRawHomeDir(env, homedir);
|
|
16
|
+
return raw ? path.resolve(raw) : undefined;
|
|
17
|
+
}
|
|
18
|
+
export function resolveOsHomeDir(env = process.env, homedir = os.homedir) {
|
|
19
|
+
const raw = resolveRawOsHomeDir(env, homedir);
|
|
20
|
+
return raw ? path.resolve(raw) : undefined;
|
|
21
|
+
}
|
|
22
|
+
function resolveRawHomeDir(env, homedir) {
|
|
23
|
+
const explicitHome = normalize(env.OPENCLAW_HOME);
|
|
24
|
+
if (explicitHome) {
|
|
25
|
+
if (explicitHome === "~" || explicitHome.startsWith("~/") || explicitHome.startsWith("~\\")) {
|
|
26
|
+
const fallbackHome = resolveRawOsHomeDir(env, homedir);
|
|
27
|
+
if (fallbackHome) {
|
|
28
|
+
return explicitHome.replace(/^~(?=$|[\\/])/, fallbackHome);
|
|
29
|
+
}
|
|
30
|
+
return undefined;
|
|
31
|
+
}
|
|
32
|
+
return explicitHome;
|
|
33
|
+
}
|
|
34
|
+
return resolveRawOsHomeDir(env, homedir);
|
|
35
|
+
}
|
|
36
|
+
function resolveRawOsHomeDir(env, homedir) {
|
|
37
|
+
const envHome = normalize(env.HOME);
|
|
38
|
+
if (envHome) {
|
|
39
|
+
return envHome;
|
|
40
|
+
}
|
|
41
|
+
const userProfile = normalize(env.USERPROFILE);
|
|
42
|
+
if (userProfile) {
|
|
43
|
+
return userProfile;
|
|
44
|
+
}
|
|
45
|
+
return normalizeSafe(homedir);
|
|
46
|
+
}
|
|
47
|
+
function normalizeSafe(homedir) {
|
|
48
|
+
try {
|
|
49
|
+
return normalize(homedir());
|
|
50
|
+
}
|
|
51
|
+
catch {
|
|
52
|
+
return undefined;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
export function resolveRequiredHomeDir(env = process.env, homedir = os.homedir) {
|
|
56
|
+
return resolveEffectiveHomeDir(env, homedir) ?? path.resolve(process.cwd());
|
|
57
|
+
}
|
|
58
|
+
export function resolveRequiredOsHomeDir(env = process.env, homedir = os.homedir) {
|
|
59
|
+
return resolveOsHomeDir(env, homedir) ?? path.resolve(process.cwd());
|
|
60
|
+
}
|
|
61
|
+
export function expandHomePrefix(input, opts) {
|
|
62
|
+
if (!input.startsWith("~")) {
|
|
63
|
+
return input;
|
|
64
|
+
}
|
|
65
|
+
const home = normalize(opts?.home) ??
|
|
66
|
+
resolveEffectiveHomeDir(opts?.env ?? process.env, opts?.homedir ?? os.homedir);
|
|
67
|
+
if (!home) {
|
|
68
|
+
return input;
|
|
69
|
+
}
|
|
70
|
+
return input.replace(/^~(?=$|[\\/])/, home);
|
|
71
|
+
}
|
|
72
|
+
export function resolveHomeRelativePath(input, opts) {
|
|
73
|
+
const trimmed = input.trim();
|
|
74
|
+
if (!trimmed) {
|
|
75
|
+
return trimmed;
|
|
76
|
+
}
|
|
77
|
+
if (trimmed.startsWith("~")) {
|
|
78
|
+
const expanded = expandHomePrefix(trimmed, {
|
|
79
|
+
home: resolveRequiredHomeDir(opts?.env ?? process.env, opts?.homedir ?? os.homedir),
|
|
80
|
+
env: opts?.env,
|
|
81
|
+
homedir: opts?.homedir,
|
|
82
|
+
});
|
|
83
|
+
return path.resolve(expanded);
|
|
84
|
+
}
|
|
85
|
+
return path.resolve(trimmed);
|
|
86
|
+
}
|
|
87
|
+
export function resolveUserPath(input, optsOrEnv, homedir) {
|
|
88
|
+
const opts = optsOrEnv && ("env" in optsOrEnv || "homedir" in optsOrEnv)
|
|
89
|
+
? optsOrEnv
|
|
90
|
+
: { env: optsOrEnv, homedir };
|
|
91
|
+
return resolveHomeRelativePath(input, opts);
|
|
92
|
+
}
|
|
93
|
+
export function resolveOsHomeRelativePath(input, opts) {
|
|
94
|
+
const trimmed = input.trim();
|
|
95
|
+
if (!trimmed) {
|
|
96
|
+
return trimmed;
|
|
97
|
+
}
|
|
98
|
+
if (trimmed.startsWith("~")) {
|
|
99
|
+
const expanded = expandHomePrefix(trimmed, {
|
|
100
|
+
home: resolveRequiredOsHomeDir(opts?.env ?? process.env, opts?.homedir ?? os.homedir),
|
|
101
|
+
env: opts?.env,
|
|
102
|
+
homedir: opts?.homedir,
|
|
103
|
+
});
|
|
104
|
+
return path.resolve(expanded);
|
|
105
|
+
}
|
|
106
|
+
return path.resolve(trimmed);
|
|
107
|
+
}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
export { FsSafeError, categorizeFsSafeError, type FsSafeErrorCategory, type FsSafeErrorCode, } from "./errors.js";
|
|
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
|
+
export { configureFsSafePython, getFsSafePythonConfig, type FsSafePythonConfig, type FsSafePythonMode, } from "./pinned-python-config.js";
|
|
4
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +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"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
export declare function safeDirName(input: string): string;
|
|
2
|
+
export declare function safePathSegmentHashed(input: string): string;
|
|
3
|
+
export declare function resolveSafeInstallDir(params: {
|
|
4
|
+
baseDir: string;
|
|
5
|
+
id: string;
|
|
6
|
+
invalidNameMessage: string;
|
|
7
|
+
nameEncoder?: (id: string) => string;
|
|
8
|
+
}): {
|
|
9
|
+
ok: true;
|
|
10
|
+
path: string;
|
|
11
|
+
} | {
|
|
12
|
+
ok: false;
|
|
13
|
+
error: string;
|
|
14
|
+
};
|
|
15
|
+
export declare function assertCanonicalPathWithinBase(params: {
|
|
16
|
+
baseDir: string;
|
|
17
|
+
candidatePath: string;
|
|
18
|
+
boundaryLabel: string;
|
|
19
|
+
}): Promise<void>;
|
|
20
|
+
//# sourceMappingURL=install-path.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"install-path.d.ts","sourceRoot":"","sources":["../src/install-path.ts"],"names":[],"mappings":"AAKA,wBAAgB,WAAW,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAMjD;AAED,wBAAgB,qBAAqB,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAsB3D;AAED,wBAAgB,qBAAqB,CAAC,MAAM,EAAE;IAC5C,OAAO,EAAE,MAAM,CAAC;IAChB,EAAE,EAAE,MAAM,CAAC;IACX,kBAAkB,EAAE,MAAM,CAAC;IAC3B,WAAW,CAAC,EAAE,CAAC,EAAE,EAAE,MAAM,KAAK,MAAM,CAAC;CACtC,GAAG;IAAE,EAAE,EAAE,IAAI,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE,GAAG;IAAE,EAAE,EAAE,KAAK,CAAC;IAAC,KAAK,EAAE,MAAM,CAAA;CAAE,CAe5D;AAED,wBAAsB,6BAA6B,CAAC,MAAM,EAAE;IAC1D,OAAO,EAAE,MAAM,CAAC;IAChB,aAAa,EAAE,MAAM,CAAC;IACtB,aAAa,EAAE,MAAM,CAAC;CACvB,GAAG,OAAO,CAAC,IAAI,CAAC,CAkDhB"}
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import fs from "node:fs/promises";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { isPathInside } from "./path.js";
|
|
5
|
+
export function safeDirName(input) {
|
|
6
|
+
const trimmed = input.trim();
|
|
7
|
+
if (!trimmed) {
|
|
8
|
+
return trimmed;
|
|
9
|
+
}
|
|
10
|
+
return trimmed.replaceAll("/", "__").replaceAll("\\", "__");
|
|
11
|
+
}
|
|
12
|
+
export function safePathSegmentHashed(input) {
|
|
13
|
+
const trimmed = input.trim();
|
|
14
|
+
const base = trimmed
|
|
15
|
+
.replaceAll(/[\\/]/g, "-")
|
|
16
|
+
.replaceAll(/[^a-zA-Z0-9._-]/g, "-")
|
|
17
|
+
.replaceAll(/-+/g, "-")
|
|
18
|
+
.replaceAll(/^-+/g, "")
|
|
19
|
+
.replaceAll(/-+$/g, "");
|
|
20
|
+
const normalized = base.length > 0 ? base : "skill";
|
|
21
|
+
const safe = normalized === "." || normalized === ".." ? "skill" : normalized;
|
|
22
|
+
const hash = createHash("sha256").update(trimmed).digest("hex").slice(0, 10);
|
|
23
|
+
if (safe !== trimmed) {
|
|
24
|
+
const prefix = safe.length > 50 ? safe.slice(0, 50) : safe;
|
|
25
|
+
return `${prefix}-${hash}`;
|
|
26
|
+
}
|
|
27
|
+
if (safe.length > 60) {
|
|
28
|
+
return `${safe.slice(0, 50)}-${hash}`;
|
|
29
|
+
}
|
|
30
|
+
return safe;
|
|
31
|
+
}
|
|
32
|
+
export function resolveSafeInstallDir(params) {
|
|
33
|
+
const encodedName = (params.nameEncoder ?? safeDirName)(params.id);
|
|
34
|
+
const targetDir = path.join(params.baseDir, encodedName);
|
|
35
|
+
const resolvedBase = path.resolve(params.baseDir);
|
|
36
|
+
const resolvedTarget = path.resolve(targetDir);
|
|
37
|
+
const relative = path.relative(resolvedBase, resolvedTarget);
|
|
38
|
+
if (!relative ||
|
|
39
|
+
relative === ".." ||
|
|
40
|
+
relative.startsWith(`..${path.sep}`) ||
|
|
41
|
+
path.isAbsolute(relative)) {
|
|
42
|
+
return { ok: false, error: params.invalidNameMessage };
|
|
43
|
+
}
|
|
44
|
+
return { ok: true, path: targetDir };
|
|
45
|
+
}
|
|
46
|
+
export async function assertCanonicalPathWithinBase(params) {
|
|
47
|
+
const baseDir = path.resolve(params.baseDir);
|
|
48
|
+
const candidatePath = path.resolve(params.candidatePath);
|
|
49
|
+
if (!isPathInside(baseDir, candidatePath)) {
|
|
50
|
+
throw new Error(`Invalid path: must stay within ${params.boundaryLabel}`);
|
|
51
|
+
}
|
|
52
|
+
const baseLstat = await fs.lstat(baseDir);
|
|
53
|
+
if (baseLstat.isSymbolicLink()) {
|
|
54
|
+
const baseStat = await fs.stat(baseDir);
|
|
55
|
+
if (!baseStat.isDirectory()) {
|
|
56
|
+
throw new Error(`Invalid ${params.boundaryLabel}: base directory must resolve to a directory`);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
else if (!baseLstat.isDirectory()) {
|
|
60
|
+
throw new Error(`Invalid ${params.boundaryLabel}: base directory must be a directory`);
|
|
61
|
+
}
|
|
62
|
+
const baseRealPath = await fs.realpath(baseDir);
|
|
63
|
+
const validateDirectory = async (dirPath) => {
|
|
64
|
+
const resolvedDirPath = path.resolve(dirPath);
|
|
65
|
+
const dirLstat = await fs.lstat(dirPath);
|
|
66
|
+
if (dirLstat.isSymbolicLink()) {
|
|
67
|
+
if (resolvedDirPath !== baseDir) {
|
|
68
|
+
throw new Error(`Invalid path: must stay within ${params.boundaryLabel}`);
|
|
69
|
+
}
|
|
70
|
+
const dirStat = await fs.stat(dirPath);
|
|
71
|
+
if (!dirStat.isDirectory()) {
|
|
72
|
+
throw new Error(`Invalid path: must stay within ${params.boundaryLabel}`);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
else if (!dirLstat.isDirectory()) {
|
|
76
|
+
throw new Error(`Invalid path: must stay within ${params.boundaryLabel}`);
|
|
77
|
+
}
|
|
78
|
+
const dirRealPath = await fs.realpath(dirPath);
|
|
79
|
+
if (!isPathInside(baseRealPath, dirRealPath)) {
|
|
80
|
+
throw new Error(`Invalid path: must stay within ${params.boundaryLabel}`);
|
|
81
|
+
}
|
|
82
|
+
};
|
|
83
|
+
try {
|
|
84
|
+
await validateDirectory(candidatePath);
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
catch (err) {
|
|
88
|
+
const code = err.code;
|
|
89
|
+
if (code !== "ENOENT") {
|
|
90
|
+
throw err;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
await validateDirectory(path.dirname(candidatePath));
|
|
94
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
export declare function safeDirName(input: string): string;
|
|
2
|
+
export declare function safePathSegmentHashed(input: string): string;
|
|
3
|
+
export declare function resolveSafeInstallDir(params: {
|
|
4
|
+
baseDir: string;
|
|
5
|
+
id: string;
|
|
6
|
+
invalidNameMessage: string;
|
|
7
|
+
nameEncoder?: (id: string) => string;
|
|
8
|
+
}): {
|
|
9
|
+
ok: true;
|
|
10
|
+
path: string;
|
|
11
|
+
} | {
|
|
12
|
+
ok: false;
|
|
13
|
+
error: string;
|
|
14
|
+
};
|
|
15
|
+
export declare function assertCanonicalPathWithinBase(params: {
|
|
16
|
+
baseDir: string;
|
|
17
|
+
candidatePath: string;
|
|
18
|
+
boundaryLabel: string;
|
|
19
|
+
}): Promise<void>;
|
|
20
|
+
//# sourceMappingURL=install-safe-path.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"install-safe-path.d.ts","sourceRoot":"","sources":["../src/install-safe-path.ts"],"names":[],"mappings":"AAKA,wBAAgB,WAAW,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAMjD;AAED,wBAAgB,qBAAqB,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAsB3D;AAED,wBAAgB,qBAAqB,CAAC,MAAM,EAAE;IAC5C,OAAO,EAAE,MAAM,CAAC;IAChB,EAAE,EAAE,MAAM,CAAC;IACX,kBAAkB,EAAE,MAAM,CAAC;IAC3B,WAAW,CAAC,EAAE,CAAC,EAAE,EAAE,MAAM,KAAK,MAAM,CAAC;CACtC,GAAG;IAAE,EAAE,EAAE,IAAI,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE,GAAG;IAAE,EAAE,EAAE,KAAK,CAAC;IAAC,KAAK,EAAE,MAAM,CAAA;CAAE,CAe5D;AAED,wBAAsB,6BAA6B,CAAC,MAAM,EAAE;IAC1D,OAAO,EAAE,MAAM,CAAC;IAChB,aAAa,EAAE,MAAM,CAAC;IACtB,aAAa,EAAE,MAAM,CAAC;CACvB,GAAG,OAAO,CAAC,IAAI,CAAC,CAkDhB"}
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import fs from "node:fs/promises";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { isPathInside } from "./path-guards.js";
|
|
5
|
+
export function safeDirName(input) {
|
|
6
|
+
const trimmed = input.trim();
|
|
7
|
+
if (!trimmed) {
|
|
8
|
+
return trimmed;
|
|
9
|
+
}
|
|
10
|
+
return trimmed.replaceAll("/", "__").replaceAll("\\", "__");
|
|
11
|
+
}
|
|
12
|
+
export function safePathSegmentHashed(input) {
|
|
13
|
+
const trimmed = input.trim();
|
|
14
|
+
const base = trimmed
|
|
15
|
+
.replaceAll(/[\\/]/g, "-")
|
|
16
|
+
.replaceAll(/[^a-zA-Z0-9._-]/g, "-")
|
|
17
|
+
.replaceAll(/-+/g, "-")
|
|
18
|
+
.replaceAll(/^-+/g, "")
|
|
19
|
+
.replaceAll(/-+$/g, "");
|
|
20
|
+
const normalized = base.length > 0 ? base : "skill";
|
|
21
|
+
const safe = normalized === "." || normalized === ".." ? "skill" : normalized;
|
|
22
|
+
const hash = createHash("sha256").update(trimmed).digest("hex").slice(0, 10);
|
|
23
|
+
if (safe !== trimmed) {
|
|
24
|
+
const prefix = safe.length > 50 ? safe.slice(0, 50) : safe;
|
|
25
|
+
return `${prefix}-${hash}`;
|
|
26
|
+
}
|
|
27
|
+
if (safe.length > 60) {
|
|
28
|
+
return `${safe.slice(0, 50)}-${hash}`;
|
|
29
|
+
}
|
|
30
|
+
return safe;
|
|
31
|
+
}
|
|
32
|
+
export function resolveSafeInstallDir(params) {
|
|
33
|
+
const encodedName = (params.nameEncoder ?? safeDirName)(params.id);
|
|
34
|
+
const targetDir = path.join(params.baseDir, encodedName);
|
|
35
|
+
const resolvedBase = path.resolve(params.baseDir);
|
|
36
|
+
const resolvedTarget = path.resolve(targetDir);
|
|
37
|
+
const relative = path.relative(resolvedBase, resolvedTarget);
|
|
38
|
+
if (!relative ||
|
|
39
|
+
relative === ".." ||
|
|
40
|
+
relative.startsWith(`..${path.sep}`) ||
|
|
41
|
+
path.isAbsolute(relative)) {
|
|
42
|
+
return { ok: false, error: params.invalidNameMessage };
|
|
43
|
+
}
|
|
44
|
+
return { ok: true, path: targetDir };
|
|
45
|
+
}
|
|
46
|
+
export async function assertCanonicalPathWithinBase(params) {
|
|
47
|
+
const baseDir = path.resolve(params.baseDir);
|
|
48
|
+
const candidatePath = path.resolve(params.candidatePath);
|
|
49
|
+
if (!isPathInside(baseDir, candidatePath)) {
|
|
50
|
+
throw new Error(`Invalid path: must stay within ${params.boundaryLabel}`);
|
|
51
|
+
}
|
|
52
|
+
const baseLstat = await fs.lstat(baseDir);
|
|
53
|
+
if (baseLstat.isSymbolicLink()) {
|
|
54
|
+
const baseStat = await fs.stat(baseDir);
|
|
55
|
+
if (!baseStat.isDirectory()) {
|
|
56
|
+
throw new Error(`Invalid ${params.boundaryLabel}: base directory must resolve to a directory`);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
else if (!baseLstat.isDirectory()) {
|
|
60
|
+
throw new Error(`Invalid ${params.boundaryLabel}: base directory must be a directory`);
|
|
61
|
+
}
|
|
62
|
+
const baseRealPath = await fs.realpath(baseDir);
|
|
63
|
+
const validateDirectory = async (dirPath) => {
|
|
64
|
+
const resolvedDirPath = path.resolve(dirPath);
|
|
65
|
+
const dirLstat = await fs.lstat(dirPath);
|
|
66
|
+
if (dirLstat.isSymbolicLink()) {
|
|
67
|
+
if (resolvedDirPath !== baseDir) {
|
|
68
|
+
throw new Error(`Invalid path: must stay within ${params.boundaryLabel}`);
|
|
69
|
+
}
|
|
70
|
+
const dirStat = await fs.stat(dirPath);
|
|
71
|
+
if (!dirStat.isDirectory()) {
|
|
72
|
+
throw new Error(`Invalid path: must stay within ${params.boundaryLabel}`);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
else if (!dirLstat.isDirectory()) {
|
|
76
|
+
throw new Error(`Invalid path: must stay within ${params.boundaryLabel}`);
|
|
77
|
+
}
|
|
78
|
+
const dirRealPath = await fs.realpath(dirPath);
|
|
79
|
+
if (!isPathInside(baseRealPath, dirRealPath)) {
|
|
80
|
+
throw new Error(`Invalid path: must stay within ${params.boundaryLabel}`);
|
|
81
|
+
}
|
|
82
|
+
};
|
|
83
|
+
try {
|
|
84
|
+
await validateDirectory(candidatePath);
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
catch (err) {
|
|
88
|
+
const code = err.code;
|
|
89
|
+
if (code !== "ENOENT") {
|
|
90
|
+
throw err;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
await validateDirectory(path.dirname(candidatePath));
|
|
94
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import type { FileLockRetryOptions } from "./file-lock.js";
|
|
2
|
+
export type JsonStoreLockOptions = {
|
|
3
|
+
staleMs?: number;
|
|
4
|
+
timeoutMs?: number;
|
|
5
|
+
retry?: FileLockRetryOptions;
|
|
6
|
+
managerKey?: string;
|
|
7
|
+
};
|
|
8
|
+
export type JsonFileStoreOptions = {
|
|
9
|
+
trailingNewline?: boolean;
|
|
10
|
+
lock?: boolean | JsonStoreLockOptions;
|
|
11
|
+
};
|
|
12
|
+
export type JsonStore<T> = {
|
|
13
|
+
readonly filePath: string;
|
|
14
|
+
read(): Promise<T | undefined>;
|
|
15
|
+
readOr(fallback: T): Promise<T>;
|
|
16
|
+
readRequired(): Promise<T>;
|
|
17
|
+
write(value: T): Promise<void>;
|
|
18
|
+
update(run: (current: T | undefined) => T | Promise<T>): Promise<T>;
|
|
19
|
+
updateOr(fallback: T, run: (current: T) => T | Promise<T>): Promise<T>;
|
|
20
|
+
};
|
|
21
|
+
export type JsonStoreAdapter<T> = {
|
|
22
|
+
filePath: string;
|
|
23
|
+
readIfExists(): Promise<T | null>;
|
|
24
|
+
readRequired(): Promise<T>;
|
|
25
|
+
write(value: T, options?: {
|
|
26
|
+
trailingNewline?: boolean;
|
|
27
|
+
}): Promise<void>;
|
|
28
|
+
};
|
|
29
|
+
export declare function createJsonStore<T>(adapter: JsonStoreAdapter<T>, options?: JsonFileStoreOptions): JsonStore<T>;
|
|
30
|
+
//# sourceMappingURL=json-document-store.d.ts.map
|
|
@@ -0,0 +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;AAyBF,wBAAgB,eAAe,CAAC,CAAC,EAC/B,OAAO,EAAE,gBAAgB,CAAC,CAAC,CAAC,EAC5B,OAAO,GAAE,oBAAyB,GACjC,SAAS,CAAC,CAAC,CAAC,CA0Dd"}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { createSidecarLockManager } from "./sidecar-lock.js";
|
|
2
|
+
function cloneFallback(value) {
|
|
3
|
+
if (value && typeof value === "object") {
|
|
4
|
+
return structuredClone(value);
|
|
5
|
+
}
|
|
6
|
+
return value;
|
|
7
|
+
}
|
|
8
|
+
function resolveLockOptions(filePath, options) {
|
|
9
|
+
if (!options.lock) {
|
|
10
|
+
return null;
|
|
11
|
+
}
|
|
12
|
+
const lockOptions = options.lock === true ? {} : options.lock;
|
|
13
|
+
return {
|
|
14
|
+
managerKey: lockOptions.managerKey ?? `fs-safe.json-store:${filePath}`,
|
|
15
|
+
retry: lockOptions.retry ?? {},
|
|
16
|
+
staleMs: lockOptions.staleMs ?? 30_000,
|
|
17
|
+
timeoutMs: lockOptions.timeoutMs ?? 30_000,
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
export function createJsonStore(adapter, options = {}) {
|
|
21
|
+
const lockOptions = resolveLockOptions(adapter.filePath, options);
|
|
22
|
+
const locks = lockOptions ? createSidecarLockManager(lockOptions.managerKey) : null;
|
|
23
|
+
async function read() {
|
|
24
|
+
return (await adapter.readIfExists()) ?? undefined;
|
|
25
|
+
}
|
|
26
|
+
async function readOr(fallback) {
|
|
27
|
+
return (await read()) ?? cloneFallback(fallback);
|
|
28
|
+
}
|
|
29
|
+
async function write(value) {
|
|
30
|
+
await adapter.write(value, {
|
|
31
|
+
trailingNewline: options.trailingNewline ?? true,
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
async function withOptionalLock(run) {
|
|
35
|
+
if (!locks || !lockOptions) {
|
|
36
|
+
return await run();
|
|
37
|
+
}
|
|
38
|
+
return await locks.withLock({
|
|
39
|
+
targetPath: adapter.filePath,
|
|
40
|
+
staleMs: lockOptions.staleMs,
|
|
41
|
+
timeoutMs: lockOptions.timeoutMs,
|
|
42
|
+
retry: lockOptions.retry,
|
|
43
|
+
allowReentrant: true,
|
|
44
|
+
payload: () => ({ pid: process.pid, createdAt: new Date().toISOString() }),
|
|
45
|
+
}, run);
|
|
46
|
+
}
|
|
47
|
+
return {
|
|
48
|
+
filePath: adapter.filePath,
|
|
49
|
+
read,
|
|
50
|
+
readOr,
|
|
51
|
+
readRequired: adapter.readRequired,
|
|
52
|
+
write: async (value) => {
|
|
53
|
+
await withOptionalLock(async () => {
|
|
54
|
+
await write(value);
|
|
55
|
+
});
|
|
56
|
+
},
|
|
57
|
+
update: async (run) => await withOptionalLock(async () => {
|
|
58
|
+
const next = await run(await read());
|
|
59
|
+
await write(next);
|
|
60
|
+
return next;
|
|
61
|
+
}),
|
|
62
|
+
updateOr: async (fallback, run) => await withOptionalLock(async () => {
|
|
63
|
+
const next = await run((await read()) ?? cloneFallback(fallback));
|
|
64
|
+
await write(next);
|
|
65
|
+
return next;
|
|
66
|
+
}),
|
|
67
|
+
};
|
|
68
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"json-file.d.ts","sourceRoot":"","sources":["../src/json-file.ts"],"names":[],"mappings":"AAkGA,wBAAgB,YAAY,CAAC,CAAC,GAAG,OAAO,EAAE,QAAQ,EAAE,MAAM,GAAG,CAAC,GAAG,SAAS,CAOzE;AAED,wBAAgB,YAAY,CAAC,QAAQ,EAAE,MAAM,EAAE,IAAI,EAAE,OAAO,QAqB3D"}
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
import fs from "node:fs";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
const JSON_FILE_MODE = 0o600;
|
|
5
|
+
const JSON_DIR_MODE = 0o700;
|
|
6
|
+
function trySetSecureMode(pathname) {
|
|
7
|
+
try {
|
|
8
|
+
fs.chmodSync(pathname, JSON_FILE_MODE);
|
|
9
|
+
}
|
|
10
|
+
catch {
|
|
11
|
+
// best-effort on platforms without chmod support
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
function trySyncDirectory(pathname) {
|
|
15
|
+
let fd;
|
|
16
|
+
try {
|
|
17
|
+
fd = fs.openSync(path.dirname(pathname), "r");
|
|
18
|
+
fs.fsyncSync(fd);
|
|
19
|
+
}
|
|
20
|
+
catch {
|
|
21
|
+
// best-effort; some platforms/filesystems do not support syncing directories.
|
|
22
|
+
}
|
|
23
|
+
finally {
|
|
24
|
+
if (fd !== undefined) {
|
|
25
|
+
try {
|
|
26
|
+
fs.closeSync(fd);
|
|
27
|
+
}
|
|
28
|
+
catch {
|
|
29
|
+
// best-effort cleanup
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
function readSymlinkTargetPath(linkPath) {
|
|
35
|
+
const target = fs.readlinkSync(linkPath);
|
|
36
|
+
return path.resolve(path.dirname(linkPath), target);
|
|
37
|
+
}
|
|
38
|
+
function resolveJsonWriteTarget(pathname) {
|
|
39
|
+
let currentPath = pathname;
|
|
40
|
+
const visited = new Set();
|
|
41
|
+
let followsSymlink = false;
|
|
42
|
+
for (;;) {
|
|
43
|
+
let stat;
|
|
44
|
+
try {
|
|
45
|
+
stat = fs.lstatSync(currentPath);
|
|
46
|
+
}
|
|
47
|
+
catch (error) {
|
|
48
|
+
if (error.code !== "ENOENT") {
|
|
49
|
+
throw error;
|
|
50
|
+
}
|
|
51
|
+
return { targetPath: currentPath, followsSymlink };
|
|
52
|
+
}
|
|
53
|
+
if (!stat.isSymbolicLink()) {
|
|
54
|
+
return { targetPath: currentPath, followsSymlink };
|
|
55
|
+
}
|
|
56
|
+
if (visited.has(currentPath)) {
|
|
57
|
+
const err = new Error(`Too many symlink levels while resolving ${pathname}`);
|
|
58
|
+
err.code = "ELOOP";
|
|
59
|
+
throw err;
|
|
60
|
+
}
|
|
61
|
+
visited.add(currentPath);
|
|
62
|
+
followsSymlink = true;
|
|
63
|
+
currentPath = readSymlinkTargetPath(currentPath);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
function renameJsonFileWithFallback(tmpPath, pathname) {
|
|
67
|
+
try {
|
|
68
|
+
fs.renameSync(tmpPath, pathname);
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
catch (error) {
|
|
72
|
+
const code = error.code;
|
|
73
|
+
// Windows does not reliably support rename-based overwrite for existing files.
|
|
74
|
+
if (code === "EPERM" || code === "EEXIST") {
|
|
75
|
+
fs.copyFileSync(tmpPath, pathname);
|
|
76
|
+
fs.rmSync(tmpPath, { force: true });
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
throw error;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
function writeTempJsonFile(pathname, payload) {
|
|
83
|
+
const fd = fs.openSync(pathname, "w", JSON_FILE_MODE);
|
|
84
|
+
try {
|
|
85
|
+
fs.writeFileSync(fd, payload, "utf8");
|
|
86
|
+
fs.fsyncSync(fd);
|
|
87
|
+
}
|
|
88
|
+
finally {
|
|
89
|
+
fs.closeSync(fd);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
export function loadJsonFile(pathname) {
|
|
93
|
+
try {
|
|
94
|
+
const raw = fs.readFileSync(pathname, "utf8");
|
|
95
|
+
return JSON.parse(raw);
|
|
96
|
+
}
|
|
97
|
+
catch {
|
|
98
|
+
return undefined;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
export function saveJsonFile(pathname, data) {
|
|
102
|
+
const { targetPath, followsSymlink } = resolveJsonWriteTarget(pathname);
|
|
103
|
+
const tmpPath = `${targetPath}.${randomUUID()}.tmp`;
|
|
104
|
+
const payload = `${JSON.stringify(data, null, 2)}\n`;
|
|
105
|
+
if (!followsSymlink) {
|
|
106
|
+
fs.mkdirSync(path.dirname(targetPath), { recursive: true, mode: JSON_DIR_MODE });
|
|
107
|
+
}
|
|
108
|
+
try {
|
|
109
|
+
writeTempJsonFile(tmpPath, payload);
|
|
110
|
+
trySetSecureMode(tmpPath);
|
|
111
|
+
renameJsonFileWithFallback(tmpPath, targetPath);
|
|
112
|
+
trySetSecureMode(targetPath);
|
|
113
|
+
trySyncDirectory(targetPath);
|
|
114
|
+
}
|
|
115
|
+
finally {
|
|
116
|
+
try {
|
|
117
|
+
fs.rmSync(tmpPath, { force: true });
|
|
118
|
+
}
|
|
119
|
+
catch {
|
|
120
|
+
// best-effort cleanup when rename does not happen
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
export declare class JsonFileReadError extends Error {
|
|
2
|
+
readonly filePath: string;
|
|
3
|
+
readonly reason: "read" | "parse";
|
|
4
|
+
constructor(filePath: string, reason: "read" | "parse", cause: unknown);
|
|
5
|
+
}
|
|
6
|
+
export declare function readJsonFile<T>(filePath: string): Promise<T | null>;
|
|
7
|
+
export declare function readDurableJsonFile<T>(filePath: string): Promise<T | null>;
|
|
8
|
+
export declare function readJsonFileSync(filePath: string): unknown;
|
|
9
|
+
export declare function writeJsonAtomic(filePath: string, value: unknown, options?: {
|
|
10
|
+
mode?: number;
|
|
11
|
+
trailingNewline?: boolean;
|
|
12
|
+
ensureDirMode?: number;
|
|
13
|
+
}): Promise<void>;
|
|
14
|
+
export declare function writeTextAtomic(filePath: string, content: string, options?: {
|
|
15
|
+
mode?: number;
|
|
16
|
+
ensureDirMode?: number;
|
|
17
|
+
appendTrailingNewline?: boolean;
|
|
18
|
+
}): Promise<void>;
|
|
19
|
+
export declare function createAsyncLock(): <T>(fn: () => Promise<T>) => Promise<T>;
|
|
20
|
+
//# sourceMappingURL=json-files.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"json-files.d.ts","sourceRoot":"","sources":["../src/json-files.ts"],"names":[],"mappings":"AASA,qBAAa,iBAAkB,SAAQ,KAAK;IAC1C,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,QAAQ,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC;gBAEtB,QAAQ,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,OAAO,EAAE,KAAK,EAAE,OAAO;CAMvE;AA6BD,wBAAsB,YAAY,CAAC,CAAC,EAAE,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,CAAC,GAAG,IAAI,CAAC,CAOzE;AAED,wBAAsB,mBAAmB,CAAC,CAAC,EAAE,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,CAAC,GAAG,IAAI,CAAC,CAehF;AAED,wBAAgB,gBAAgB,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAO1D;AAED,wBAAsB,eAAe,CACnC,QAAQ,EAAE,MAAM,EAChB,KAAK,EAAE,OAAO,EACd,OAAO,CAAC,EAAE;IAAE,IAAI,CAAC,EAAE,MAAM,CAAC;IAAC,eAAe,CAAC,EAAE,OAAO,CAAC;IAAC,aAAa,CAAC,EAAE,MAAM,CAAA;CAAE,iBAQ/E;AAED,wBAAsB,eAAe,CACnC,QAAQ,EAAE,MAAM,EAChB,OAAO,EAAE,MAAM,EACf,OAAO,CAAC,EAAE;IAAE,IAAI,CAAC,EAAE,MAAM,CAAC;IAAC,aAAa,CAAC,EAAE,MAAM,CAAC;IAAC,qBAAqB,CAAC,EAAE,OAAO,CAAA;CAAE,iBA4CrF;AAED,wBAAgB,eAAe,KAEE,CAAC,EAAE,IAAI,MAAM,OAAO,CAAC,CAAC,CAAC,KAAG,OAAO,CAAC,CAAC,CAAC,CAapE"}
|