@openclaw/fs-safe 0.5.5 → 0.6.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 +49 -0
- package/README.md +16 -10
- package/dist/advanced.d.ts +2 -1
- package/dist/advanced.d.ts.map +1 -1
- package/dist/advanced.js +1 -0
- package/dist/archive-crc32.d.ts +2 -0
- package/dist/archive-crc32.d.ts.map +1 -0
- package/dist/archive-crc32.js +14 -0
- package/dist/archive-kind.js +2 -2
- package/dist/archive-native.d.ts.map +1 -1
- package/dist/archive-native.js +7 -0
- package/dist/archive-read.d.ts.map +1 -1
- package/dist/archive-read.js +24 -5
- package/dist/archive-tar-meta.d.ts.map +1 -1
- package/dist/archive-tar-meta.js +53 -6
- package/dist/archive-tar-pax.d.ts +8 -0
- package/dist/archive-tar-pax.d.ts.map +1 -0
- package/dist/archive-tar-pax.js +100 -0
- package/dist/archive-tar-runtime.d.ts +1 -0
- package/dist/archive-tar-runtime.d.ts.map +1 -1
- package/dist/archive-zip-admission.d.ts +7 -0
- package/dist/archive-zip-admission.d.ts.map +1 -0
- package/dist/archive-zip-admission.js +60 -0
- package/dist/archive-zip-count.d.ts +2 -0
- package/dist/archive-zip-count.d.ts.map +1 -0
- package/dist/archive-zip-count.js +140 -0
- package/dist/archive-zip-directory.d.ts +8 -0
- package/dist/archive-zip-directory.d.ts.map +1 -0
- package/dist/archive-zip-directory.js +224 -0
- package/dist/archive-zip-integrity.d.ts.map +1 -1
- package/dist/archive-zip-integrity.js +1 -14
- package/dist/archive-zip-names.d.ts +12 -0
- package/dist/archive-zip-names.d.ts.map +1 -0
- package/dist/archive-zip-names.js +99 -0
- package/dist/archive-zip-preflight.d.ts +1 -1
- package/dist/archive-zip-preflight.d.ts.map +1 -1
- package/dist/archive-zip-preflight.js +8 -145
- package/dist/archive.d.ts.map +1 -1
- package/dist/archive.js +32 -23
- package/dist/file-hash.d.ts.map +1 -1
- package/dist/file-hash.js +25 -19
- package/dist/file-identity.d.ts +1 -0
- package/dist/file-identity.d.ts.map +1 -1
- package/dist/file-identity.js +10 -0
- package/dist/file-lock-sync.d.ts.map +1 -1
- package/dist/file-lock-sync.js +19 -7
- package/dist/move-path.js +2 -2
- package/dist/native-binding.d.ts +3 -0
- package/dist/native-binding.d.ts.map +1 -1
- package/dist/native-operations.d.ts +4 -1
- package/dist/native-operations.d.ts.map +1 -1
- package/dist/native-operations.js +24 -8
- package/dist/native-pinned-write-windows.d.ts +8 -0
- package/dist/native-pinned-write-windows.d.ts.map +1 -0
- package/dist/native-pinned-write-windows.js +92 -0
- package/dist/native-pinned-write.d.ts.map +1 -1
- package/dist/native-pinned-write.js +136 -127
- package/dist/native-staged-file.d.ts +24 -0
- package/dist/native-staged-file.d.ts.map +1 -0
- package/dist/native-staged-file.js +337 -0
- package/dist/native.d.ts.map +1 -1
- package/dist/native.js +4 -4
- package/dist/opened-realpath.d.ts +2 -0
- package/dist/opened-realpath.d.ts.map +1 -1
- package/dist/opened-realpath.js +12 -7
- package/dist/output-sibling.js +3 -3
- package/dist/owner-dacl.d.ts.map +1 -1
- package/dist/owner-dacl.js +2 -1
- package/dist/permission-exec.d.ts +19 -0
- package/dist/permission-exec.d.ts.map +1 -1
- package/dist/permission-exec.js +57 -11
- package/dist/permissions-public.d.ts +1 -1
- package/dist/permissions-public.d.ts.map +1 -1
- package/dist/permissions-windows.d.ts +3 -0
- package/dist/permissions-windows.d.ts.map +1 -1
- package/dist/permissions-windows.js +16 -5
- package/dist/permissions.d.ts +5 -0
- package/dist/permissions.d.ts.map +1 -1
- package/dist/pinned-write.d.ts +6 -0
- package/dist/pinned-write.d.ts.map +1 -1
- package/dist/pinned-write.js +19 -29
- package/dist/private-directory.d.ts.map +1 -1
- package/dist/private-directory.js +3 -2
- package/dist/private-temp-workspace.d.ts.map +1 -1
- package/dist/private-temp-workspace.js +23 -15
- package/dist/publish-file.d.ts.map +1 -1
- package/dist/publish-file.js +2 -2
- package/dist/replace-file-descriptor.d.ts +3 -3
- package/dist/replace-file-descriptor.d.ts.map +1 -1
- package/dist/replace-file-descriptor.js +2 -2
- package/dist/root-impl.d.ts.map +1 -1
- package/dist/root-impl.js +84 -75
- package/dist/root-paths.d.ts +11 -14
- package/dist/root-paths.d.ts.map +1 -1
- package/dist/root-paths.js +36 -27
- package/dist/root-write-verification.d.ts +11 -0
- package/dist/root-write-verification.d.ts.map +1 -0
- package/dist/root-write-verification.js +91 -0
- package/dist/secret-file.d.ts +1 -6
- package/dist/secret-file.d.ts.map +1 -1
- package/dist/secret-file.js +43 -117
- package/dist/secret-read-async.d.ts +1 -1
- package/dist/secret-read-async.d.ts.map +1 -1
- package/dist/secret-read-async.js +45 -67
- package/dist/secret-read-policy.d.ts +13 -0
- package/dist/secret-read-policy.d.ts.map +1 -0
- package/dist/secret-read-policy.js +28 -0
- package/dist/secret.d.ts +2 -1
- package/dist/secret.d.ts.map +1 -1
- package/dist/secret.js +2 -1
- package/dist/secure-file.d.ts.map +1 -1
- package/dist/secure-file.js +25 -16
- package/dist/sibling-temp.js +1 -1
- package/dist/sidecar-lock-acquire.d.ts.map +1 -1
- package/dist/sidecar-lock-acquire.js +4 -1
- package/dist/staged-directory.d.ts +16 -0
- package/dist/staged-directory.d.ts.map +1 -0
- package/dist/staged-directory.js +60 -0
- package/dist/staged-file-types.d.ts +56 -0
- package/dist/staged-file-types.d.ts.map +1 -0
- package/dist/staged-file-types.js +1 -0
- package/dist/staged-file.d.ts +10 -0
- package/dist/staged-file.d.ts.map +1 -0
- package/dist/staged-file.js +15 -0
- package/dist/strict-file-identity.d.ts +6 -0
- package/dist/strict-file-identity.d.ts.map +1 -0
- package/dist/strict-file-identity.js +48 -0
- package/dist/temp-cleanup.d.ts.map +1 -1
- package/dist/temp-cleanup.js +3 -3
- package/dist/temp-target.d.ts.map +1 -1
- package/dist/temp-target.js +9 -7
- package/dist/windows-owner.d.ts +3 -0
- package/dist/windows-owner.d.ts.map +1 -1
- package/dist/windows-owner.js +10 -2
- package/docs/advanced.md +11 -1
- package/docs/archive.md +62 -15
- package/docs/config.md +7 -0
- package/docs/contributing.md +45 -7
- package/docs/durability.md +17 -4
- package/docs/errors.md +16 -1
- package/docs/index.md +3 -1
- package/docs/install.md +15 -7
- package/docs/migrating-to-0.5.md +7 -7
- package/docs/migrating-to-0.6.md +43 -0
- package/docs/native-helper.md +15 -5
- package/docs/native.md +40 -11
- package/docs/path-scope.md +28 -2
- package/docs/permissions.md +13 -2
- package/docs/root.md +1 -1
- package/docs/secret-file.md +8 -0
- package/docs/secure-file.md +17 -1
- package/docs/security-model.md +15 -1
- package/docs/sidecar-lock.md +13 -3
- package/docs/staged-file.md +178 -0
- package/docs/testing.md +1 -1
- package/docs/writing.md +19 -0
- package/package.json +16 -9
- package/dist/native/darwin-arm64/fs-safe-native.node +0 -0
- package/dist/native/darwin-x64/fs-safe-native.node +0 -0
- package/dist/native/linux-arm64-gnu/fs-safe-native.node +0 -0
- package/dist/native/linux-arm64-musl/fs-safe-native.node +0 -0
- package/dist/native/linux-x64-gnu/fs-safe-native.node +0 -0
- package/dist/native/linux-x64-musl/fs-safe-native.node +0 -0
- package/dist/native/win32-x64-msvc/fs-safe-native.node +0 -0
package/dist/secret-file.js
CHANGED
|
@@ -9,155 +9,81 @@ import { sameFileIdentity } from "./file-identity.js";
|
|
|
9
9
|
import { resolveHomeRelativePath } from "./home-dir.js";
|
|
10
10
|
import { openPinnedFileSync } from "./pinned-open.js";
|
|
11
11
|
import { runPinnedWriteHelper } from "./pinned-write.js";
|
|
12
|
+
import { assertSecretFilePreview, DEFAULT_SECRET_FILE_MAX_BYTES, secretPathErrorCode, secretReadError, trimSecretFileContent, } from "./secret-read-policy.js";
|
|
13
|
+
import { inspectFileIdentitySync } from "./strict-file-identity.js";
|
|
12
14
|
import { serializePathWrite } from "./write-queue.js";
|
|
13
|
-
export const DEFAULT_SECRET_FILE_MAX_BYTES = 16 * 1024;
|
|
14
15
|
export const PRIVATE_SECRET_DIR_MODE = 0o700;
|
|
15
16
|
export const PRIVATE_SECRET_FILE_MODE = 0o600;
|
|
16
|
-
function
|
|
17
|
-
|
|
18
|
-
}
|
|
19
|
-
function secretPathErrorCode(error) {
|
|
20
|
-
const code = error.code;
|
|
21
|
-
return code === "ENOENT" || code === "ENOTDIR" ? "not-found" : "invalid-path";
|
|
22
|
-
}
|
|
23
|
-
function resolveUserPath(input) {
|
|
24
|
-
return resolveHomeRelativePath(input);
|
|
25
|
-
}
|
|
26
|
-
function readSecretFileOutcomeSync(filePath, label, options = {}) {
|
|
27
|
-
const trimmedPath = filePath.trim();
|
|
28
|
-
const resolvedPath = resolveUserPath(trimmedPath);
|
|
17
|
+
export function readSecretFileSync(filePath, label, options = {}) {
|
|
18
|
+
const resolvedPath = resolveHomeRelativePath(filePath.trim());
|
|
29
19
|
if (!resolvedPath) {
|
|
30
|
-
|
|
20
|
+
throw new FsSafeError("invalid-path", `${label} file path is empty.`, { cause: undefined });
|
|
31
21
|
}
|
|
32
22
|
const maxBytes = options.maxBytes ?? DEFAULT_SECRET_FILE_MAX_BYTES;
|
|
23
|
+
function inspectInput(symlinkMessage) {
|
|
24
|
+
const stat = options.rejectSymlink
|
|
25
|
+
? fs.lstatSync(resolvedPath, { bigint: true })
|
|
26
|
+
: fs.statSync(resolvedPath, { bigint: true });
|
|
27
|
+
if (options.rejectSymlink && stat.isSymbolicLink()) {
|
|
28
|
+
throw new FsSafeError("symlink", symlinkMessage);
|
|
29
|
+
}
|
|
30
|
+
return stat;
|
|
31
|
+
}
|
|
33
32
|
let previewStat;
|
|
34
33
|
try {
|
|
35
|
-
previewStat =
|
|
34
|
+
previewStat = inspectFileIdentitySync(() => inspectInput(`${label} file at ${resolvedPath} must not be a symlink.`));
|
|
36
35
|
}
|
|
37
36
|
catch (error) {
|
|
38
|
-
|
|
39
|
-
return {
|
|
40
|
-
ok: false,
|
|
41
|
-
code: secretPathErrorCode(error),
|
|
42
|
-
error: normalized,
|
|
43
|
-
message: `Failed to inspect ${label} file at ${resolvedPath}: ${String(normalized)}`,
|
|
44
|
-
};
|
|
45
|
-
}
|
|
46
|
-
if (previewStat.isSymbolicLink()) {
|
|
47
|
-
if (!options.rejectSymlink) {
|
|
48
|
-
try {
|
|
49
|
-
previewStat = fs.statSync(resolvedPath);
|
|
50
|
-
}
|
|
51
|
-
catch (error) {
|
|
52
|
-
const normalized = normalizeSecretReadError(error);
|
|
53
|
-
return {
|
|
54
|
-
ok: false,
|
|
55
|
-
code: secretPathErrorCode(error),
|
|
56
|
-
error: normalized,
|
|
57
|
-
message: `Failed to inspect ${label} file at ${resolvedPath}: ${String(normalized)}`,
|
|
58
|
-
};
|
|
59
|
-
}
|
|
60
|
-
}
|
|
61
|
-
else {
|
|
62
|
-
return {
|
|
63
|
-
ok: false,
|
|
64
|
-
code: "symlink",
|
|
65
|
-
message: `${label} file at ${resolvedPath} must not be a symlink.`,
|
|
66
|
-
};
|
|
67
|
-
}
|
|
68
|
-
}
|
|
69
|
-
if (!previewStat.isFile()) {
|
|
70
|
-
return {
|
|
71
|
-
ok: false,
|
|
72
|
-
code: "not-file",
|
|
73
|
-
message: `${label} file at ${resolvedPath} must be a regular file.`,
|
|
74
|
-
};
|
|
75
|
-
}
|
|
76
|
-
if (options.rejectHardlinks !== false && previewStat.nlink > 1) {
|
|
77
|
-
return {
|
|
78
|
-
ok: false,
|
|
79
|
-
code: "hardlink",
|
|
80
|
-
message: `${label} file at ${resolvedPath} must not be hardlinked.`,
|
|
81
|
-
};
|
|
82
|
-
}
|
|
83
|
-
if (previewStat.size > maxBytes) {
|
|
84
|
-
return {
|
|
85
|
-
ok: false,
|
|
86
|
-
code: "too-large",
|
|
87
|
-
message: `${label} file at ${resolvedPath} exceeds ${maxBytes} bytes.`,
|
|
88
|
-
};
|
|
37
|
+
throw secretReadError(error instanceof FsSafeError ? error.code : secretPathErrorCode(error), "inspect", label, resolvedPath, error);
|
|
89
38
|
}
|
|
39
|
+
assertSecretFilePreview(previewStat, label, resolvedPath, maxBytes, options.rejectHardlinks !== false);
|
|
90
40
|
const opened = openPinnedFileSync({
|
|
91
41
|
filePath: resolvedPath,
|
|
92
42
|
rejectPathSymlink: options.rejectSymlink,
|
|
93
43
|
rejectHardlinks: options.rejectHardlinks !== false,
|
|
94
44
|
});
|
|
95
45
|
if (!opened.ok) {
|
|
96
|
-
|
|
97
|
-
return {
|
|
98
|
-
ok: false,
|
|
99
|
-
code: opened.reason === "path" ? "not-found" : "path-mismatch",
|
|
100
|
-
error,
|
|
101
|
-
message: `Failed to read ${label} file at ${resolvedPath}: ${String(error)}`,
|
|
102
|
-
};
|
|
46
|
+
throw secretReadError(opened.reason === "path" ? "not-found" : "path-mismatch", "read", label, resolvedPath, opened.reason === "validation" ? new Error("security validation failed") : opened.error);
|
|
103
47
|
}
|
|
48
|
+
let raw;
|
|
104
49
|
try {
|
|
105
|
-
|
|
106
|
-
const
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
return
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
};
|
|
122
|
-
}
|
|
123
|
-
return { ok: true, secret };
|
|
50
|
+
const openedIdentity = inspectFileIdentitySync(() => {
|
|
51
|
+
const stat = fs.fstatSync(opened.fd, { bigint: true });
|
|
52
|
+
if (!stat.isFile() || (options.rejectHardlinks !== false && stat.nlink > 1n)) {
|
|
53
|
+
throw new FsSafeError("path-mismatch", "security validation failed");
|
|
54
|
+
}
|
|
55
|
+
return stat;
|
|
56
|
+
}, previewStat);
|
|
57
|
+
inspectFileIdentitySync(() => {
|
|
58
|
+
const stat = fs.lstatSync(opened.path, { bigint: true });
|
|
59
|
+
if (!stat.isFile() || (options.rejectHardlinks !== false && stat.nlink > 1n)) {
|
|
60
|
+
throw new FsSafeError("path-mismatch", "security validation failed");
|
|
61
|
+
}
|
|
62
|
+
return stat;
|
|
63
|
+
}, openedIdentity);
|
|
64
|
+
inspectFileIdentitySync(() => inspectInput("secret path became a symlink"), openedIdentity);
|
|
65
|
+
raw = readFileDescriptorBoundedSync(opened.fd, maxBytes).toString("utf8");
|
|
124
66
|
}
|
|
125
67
|
catch (error) {
|
|
126
|
-
|
|
127
|
-
return {
|
|
128
|
-
ok: false,
|
|
129
|
-
code: error instanceof FsSafeError ? error.code : "read-failed",
|
|
130
|
-
error: normalized,
|
|
131
|
-
message: `Failed to read ${label} file at ${resolvedPath}: ${String(normalized)}`,
|
|
132
|
-
};
|
|
68
|
+
throw secretReadError(error instanceof FsSafeError ? error.code : "read-failed", "read", label, resolvedPath, error);
|
|
133
69
|
}
|
|
134
70
|
finally {
|
|
135
71
|
fs.closeSync(opened.fd);
|
|
136
72
|
}
|
|
137
|
-
|
|
138
|
-
export function readSecretFileSync(filePath, label, options = {}) {
|
|
139
|
-
const result = readSecretFileOutcomeSync(filePath, label, options);
|
|
140
|
-
if (result.ok) {
|
|
141
|
-
return result.secret;
|
|
142
|
-
}
|
|
143
|
-
throw new FsSafeError(result.code, result.message, {
|
|
144
|
-
cause: result.error,
|
|
145
|
-
});
|
|
73
|
+
return trimSecretFileContent(raw, label, resolvedPath);
|
|
146
74
|
}
|
|
147
75
|
export function tryReadSecretFileSync(filePath, label, options = {}) {
|
|
148
76
|
if (!filePath?.trim()) {
|
|
149
77
|
return undefined;
|
|
150
78
|
}
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
return result.secret;
|
|
79
|
+
try {
|
|
80
|
+
return readSecretFileSync(filePath, label, options);
|
|
154
81
|
}
|
|
155
|
-
|
|
156
|
-
|
|
82
|
+
catch (error) {
|
|
83
|
+
if (error instanceof FsSafeError && error.code === "not-found")
|
|
84
|
+
return undefined;
|
|
85
|
+
throw error;
|
|
157
86
|
}
|
|
158
|
-
throw new FsSafeError(result.code, result.message, {
|
|
159
|
-
cause: result.error,
|
|
160
|
-
});
|
|
161
87
|
}
|
|
162
88
|
function isRelativeEscape(relativePath) {
|
|
163
89
|
return relativePath === ".." || relativePath.startsWith(`..${path.sep}`) || path.isAbsolute(relativePath);
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { type SecretFileReadOptions } from "./secret-
|
|
1
|
+
import { type SecretFileReadOptions } from "./secret-read-policy.js";
|
|
2
2
|
export declare function readSecretFile(filePath: string, label: string, options?: SecretFileReadOptions): Promise<string>;
|
|
3
3
|
export declare function tryReadSecretFile(filePath: string | undefined, label: string, options?: SecretFileReadOptions): Promise<string | undefined>;
|
|
4
4
|
//# sourceMappingURL=secret-read-async.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"secret-read-async.d.ts","sourceRoot":"","sources":["../src/secret-read-async.ts"],"names":[],"mappings":"AAMA,OAAO,
|
|
1
|
+
{"version":3,"file":"secret-read-async.d.ts","sourceRoot":"","sources":["../src/secret-read-async.ts"],"names":[],"mappings":"AAMA,OAAO,EAML,KAAK,qBAAqB,EAC3B,MAAM,yBAAyB,CAAC;AAEjC,wBAAsB,cAAc,CAClC,QAAQ,EAAE,MAAM,EAChB,KAAK,EAAE,MAAM,EACb,OAAO,GAAE,qBAA0B,GAClC,OAAO,CAAC,MAAM,CAAC,CAgEjB;AAED,wBAAsB,iBAAiB,CACrC,QAAQ,EAAE,MAAM,GAAG,SAAS,EAC5B,KAAK,EAAE,MAAM,EACb,OAAO,GAAE,qBAA0B,GAClC,OAAO,CAAC,MAAM,GAAG,SAAS,CAAC,CAQ7B"}
|
|
@@ -2,98 +2,76 @@ import fsSync from "node:fs";
|
|
|
2
2
|
import fs from "node:fs/promises";
|
|
3
3
|
import { readFileHandleBounded } from "./bounded-read.js";
|
|
4
4
|
import { FsSafeError } from "./errors.js";
|
|
5
|
-
import { sameFileIdentity } from "./file-identity.js";
|
|
6
5
|
import { resolveHomeRelativePath } from "./home-dir.js";
|
|
7
|
-
import {
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
return code === "ENOENT" || code === "ENOTDIR" ? "not-found" : "invalid-path";
|
|
11
|
-
}
|
|
12
|
-
async function readSecretFileOutcome(filePath, label, options) {
|
|
6
|
+
import { inspectFileIdentity } from "./strict-file-identity.js";
|
|
7
|
+
import { assertSecretFilePreview, DEFAULT_SECRET_FILE_MAX_BYTES, secretPathErrorCode, secretReadError, trimSecretFileContent, } from "./secret-read-policy.js";
|
|
8
|
+
export async function readSecretFile(filePath, label, options = {}) {
|
|
13
9
|
const resolvedPath = resolveHomeRelativePath(filePath.trim());
|
|
14
10
|
if (!resolvedPath) {
|
|
15
|
-
|
|
11
|
+
throw new FsSafeError("invalid-path", `${label} file path is empty.`, { cause: undefined });
|
|
16
12
|
}
|
|
17
13
|
const maxBytes = options.maxBytes ?? DEFAULT_SECRET_FILE_MAX_BYTES;
|
|
14
|
+
async function inspectInput(symlinkMessage) {
|
|
15
|
+
const stat = options.rejectSymlink
|
|
16
|
+
? await fs.lstat(resolvedPath, { bigint: true })
|
|
17
|
+
: await fs.stat(resolvedPath, { bigint: true });
|
|
18
|
+
if (options.rejectSymlink && stat.isSymbolicLink()) {
|
|
19
|
+
throw new FsSafeError("symlink", symlinkMessage);
|
|
20
|
+
}
|
|
21
|
+
return stat;
|
|
22
|
+
}
|
|
18
23
|
let previewStat;
|
|
19
24
|
try {
|
|
20
|
-
previewStat = await
|
|
21
|
-
if (previewStat.isSymbolicLink()) {
|
|
22
|
-
if (options.rejectSymlink) {
|
|
23
|
-
return { ok: false, code: "symlink", message: `${label} file at ${resolvedPath} must not be a symlink.` };
|
|
24
|
-
}
|
|
25
|
-
previewStat = await fs.stat(resolvedPath);
|
|
26
|
-
}
|
|
25
|
+
previewStat = await inspectFileIdentity(() => inspectInput(`${label} file at ${resolvedPath} must not be a symlink.`));
|
|
27
26
|
}
|
|
28
27
|
catch (error) {
|
|
29
|
-
|
|
30
|
-
return {
|
|
31
|
-
ok: false,
|
|
32
|
-
code: pathErrorCode(error),
|
|
33
|
-
error: normalized,
|
|
34
|
-
message: `Failed to inspect ${label} file at ${resolvedPath}: ${String(normalized)}`,
|
|
35
|
-
};
|
|
36
|
-
}
|
|
37
|
-
if (!previewStat.isFile()) {
|
|
38
|
-
return { ok: false, code: "not-file", message: `${label} file at ${resolvedPath} must be a regular file.` };
|
|
39
|
-
}
|
|
40
|
-
if (options.rejectHardlinks !== false && previewStat.nlink > 1) {
|
|
41
|
-
return { ok: false, code: "hardlink", message: `${label} file at ${resolvedPath} must not be hardlinked.` };
|
|
42
|
-
}
|
|
43
|
-
if (previewStat.size > maxBytes) {
|
|
44
|
-
return { ok: false, code: "too-large", message: `${label} file at ${resolvedPath} exceeds ${maxBytes} bytes.` };
|
|
28
|
+
throw secretReadError(error instanceof FsSafeError ? error.code : secretPathErrorCode(error), "inspect", label, resolvedPath, error);
|
|
45
29
|
}
|
|
30
|
+
assertSecretFilePreview(previewStat, label, resolvedPath, maxBytes, options.rejectHardlinks !== false);
|
|
46
31
|
let handle;
|
|
32
|
+
let raw;
|
|
47
33
|
try {
|
|
48
34
|
const realPath = await fs.realpath(resolvedPath);
|
|
49
35
|
const noFollow = process.platform !== "win32" && "O_NOFOLLOW" in fsSync.constants
|
|
50
36
|
? fsSync.constants.O_NOFOLLOW
|
|
51
37
|
: 0;
|
|
52
38
|
handle = await fs.open(realPath, fsSync.constants.O_RDONLY | noFollow);
|
|
53
|
-
const
|
|
54
|
-
const
|
|
55
|
-
|
|
56
|
-
!
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
39
|
+
const openedHandle = handle;
|
|
40
|
+
const openedStat = await inspectFileIdentity(async () => {
|
|
41
|
+
const stat = await openedHandle.stat({ bigint: true });
|
|
42
|
+
if (!stat.isFile() || (options.rejectHardlinks !== false && stat.nlink > 1n)) {
|
|
43
|
+
throw new FsSafeError("path-mismatch", "security validation failed");
|
|
44
|
+
}
|
|
45
|
+
return stat;
|
|
46
|
+
}, previewStat);
|
|
47
|
+
await inspectFileIdentity(async () => {
|
|
48
|
+
const stat = await fs.lstat(realPath, { bigint: true });
|
|
49
|
+
if (!stat.isFile())
|
|
50
|
+
throw new FsSafeError("path-mismatch", "security validation failed");
|
|
51
|
+
return stat;
|
|
52
|
+
}, openedStat);
|
|
53
|
+
await inspectFileIdentity(() => inspectInput("secret path became a symlink"), openedStat);
|
|
54
|
+
raw = (await readFileHandleBounded(handle, maxBytes)).toString("utf8");
|
|
66
55
|
}
|
|
67
56
|
catch (error) {
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
code: error instanceof FsSafeError
|
|
72
|
-
? error.code
|
|
73
|
-
: pathErrorCode(error) === "not-found"
|
|
74
|
-
? "not-found"
|
|
75
|
-
: "read-failed",
|
|
76
|
-
error: normalized,
|
|
77
|
-
message: `Failed to read ${label} file at ${resolvedPath}: ${String(normalized)}`,
|
|
78
|
-
};
|
|
57
|
+
throw secretReadError(error instanceof FsSafeError
|
|
58
|
+
? error.code
|
|
59
|
+
: secretPathErrorCode(error) === "not-found" ? "not-found" : "read-failed", "read", label, resolvedPath, error);
|
|
79
60
|
}
|
|
80
61
|
finally {
|
|
81
62
|
await handle?.close().catch(() => undefined);
|
|
82
63
|
}
|
|
83
|
-
|
|
84
|
-
export async function readSecretFile(filePath, label, options = {}) {
|
|
85
|
-
const result = await readSecretFileOutcome(filePath, label, options);
|
|
86
|
-
if (result.ok)
|
|
87
|
-
return result.secret;
|
|
88
|
-
throw new FsSafeError(result.code, result.message, { cause: result.error });
|
|
64
|
+
return trimSecretFileContent(raw, label, resolvedPath);
|
|
89
65
|
}
|
|
90
66
|
export async function tryReadSecretFile(filePath, label, options = {}) {
|
|
91
67
|
if (!filePath?.trim())
|
|
92
68
|
return undefined;
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
69
|
+
try {
|
|
70
|
+
return await readSecretFile(filePath, label, options);
|
|
71
|
+
}
|
|
72
|
+
catch (error) {
|
|
73
|
+
if (error instanceof FsSafeError && error.code === "not-found")
|
|
74
|
+
return undefined;
|
|
75
|
+
throw error;
|
|
76
|
+
}
|
|
99
77
|
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { BigIntStats } from "node:fs";
|
|
2
|
+
import { FsSafeError, type FsSafeErrorCode } from "./errors.js";
|
|
3
|
+
export declare const DEFAULT_SECRET_FILE_MAX_BYTES: number;
|
|
4
|
+
export type SecretFileReadOptions = {
|
|
5
|
+
maxBytes?: number;
|
|
6
|
+
rejectSymlink?: boolean;
|
|
7
|
+
rejectHardlinks?: boolean;
|
|
8
|
+
};
|
|
9
|
+
export declare function secretPathErrorCode(error: unknown): FsSafeErrorCode;
|
|
10
|
+
export declare function secretReadError(code: FsSafeErrorCode, action: "inspect" | "read", label: string, resolvedPath: string, error: unknown): FsSafeError;
|
|
11
|
+
export declare function assertSecretFilePreview(stat: BigIntStats, label: string, resolvedPath: string, maxBytes: number, rejectHardlinks: boolean): void;
|
|
12
|
+
export declare function trimSecretFileContent(raw: string, label: string, resolvedPath: string): string;
|
|
13
|
+
//# sourceMappingURL=secret-read-policy.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"secret-read-policy.d.ts","sourceRoot":"","sources":["../src/secret-read-policy.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,SAAS,CAAC;AAC3C,OAAO,EAAE,WAAW,EAAE,KAAK,eAAe,EAAE,MAAM,aAAa,CAAC;AAEhE,eAAO,MAAM,6BAA6B,QAAY,CAAC;AAEvD,MAAM,MAAM,qBAAqB,GAAG;IAClC,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,aAAa,CAAC,EAAE,OAAO,CAAC;IACxB,eAAe,CAAC,EAAE,OAAO,CAAC;CAC3B,CAAC;AAEF,wBAAgB,mBAAmB,CAAC,KAAK,EAAE,OAAO,GAAG,eAAe,CAGnE;AAED,wBAAgB,eAAe,CAC7B,IAAI,EAAE,eAAe,EACrB,MAAM,EAAE,SAAS,GAAG,MAAM,EAC1B,KAAK,EAAE,MAAM,EACb,YAAY,EAAE,MAAM,EACpB,KAAK,EAAE,OAAO,GACb,WAAW,CAGb;AAED,wBAAgB,uBAAuB,CACrC,IAAI,EAAE,WAAW,EACjB,KAAK,EAAE,MAAM,EACb,YAAY,EAAE,MAAM,EACpB,QAAQ,EAAE,MAAM,EAChB,eAAe,EAAE,OAAO,GACvB,IAAI,CAUN;AAED,wBAAgB,qBAAqB,CAAC,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,YAAY,EAAE,MAAM,GAAG,MAAM,CAM9F"}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { FsSafeError } from "./errors.js";
|
|
2
|
+
export const DEFAULT_SECRET_FILE_MAX_BYTES = 16 * 1024;
|
|
3
|
+
export function secretPathErrorCode(error) {
|
|
4
|
+
const code = error.code;
|
|
5
|
+
return code === "ENOENT" || code === "ENOTDIR" ? "not-found" : "invalid-path";
|
|
6
|
+
}
|
|
7
|
+
export function secretReadError(code, action, label, resolvedPath, error) {
|
|
8
|
+
const cause = error instanceof Error ? error : new Error(String(error));
|
|
9
|
+
return new FsSafeError(code, `Failed to ${action} ${label} file at ${resolvedPath}: ${String(cause)}`, { cause });
|
|
10
|
+
}
|
|
11
|
+
export function assertSecretFilePreview(stat, label, resolvedPath, maxBytes, rejectHardlinks) {
|
|
12
|
+
if (!stat.isFile()) {
|
|
13
|
+
throw new FsSafeError("not-file", `${label} file at ${resolvedPath} must be a regular file.`, { cause: undefined });
|
|
14
|
+
}
|
|
15
|
+
if (rejectHardlinks && stat.nlink > 1n) {
|
|
16
|
+
throw new FsSafeError("hardlink", `${label} file at ${resolvedPath} must not be hardlinked.`, { cause: undefined });
|
|
17
|
+
}
|
|
18
|
+
if (stat.size > maxBytes) {
|
|
19
|
+
throw new FsSafeError("too-large", `${label} file at ${resolvedPath} exceeds ${maxBytes} bytes.`, { cause: undefined });
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
export function trimSecretFileContent(raw, label, resolvedPath) {
|
|
23
|
+
const secret = raw.trim();
|
|
24
|
+
if (!secret) {
|
|
25
|
+
throw new FsSafeError("invalid-path", `${label} file at ${resolvedPath} is empty.`, { cause: undefined });
|
|
26
|
+
}
|
|
27
|
+
return secret;
|
|
28
|
+
}
|
package/dist/secret.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
-
export { createSecretFileAtomic,
|
|
1
|
+
export { createSecretFileAtomic, PRIVATE_SECRET_DIR_MODE, PRIVATE_SECRET_FILE_MODE, readSecretFileSync, tryReadSecretFileSync, writeSecretFileAtomic, } from "./secret-file.js";
|
|
2
|
+
export { DEFAULT_SECRET_FILE_MAX_BYTES, type SecretFileReadOptions } from "./secret-read-policy.js";
|
|
2
3
|
export { readSecretFile, tryReadSecretFile } from "./secret-read-async.js";
|
|
3
4
|
//# sourceMappingURL=secret.d.ts.map
|
package/dist/secret.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"secret.d.ts","sourceRoot":"","sources":["../src/secret.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,sBAAsB,EACtB,
|
|
1
|
+
{"version":3,"file":"secret.d.ts","sourceRoot":"","sources":["../src/secret.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,sBAAsB,EACtB,uBAAuB,EACvB,wBAAwB,EACxB,kBAAkB,EAClB,qBAAqB,EACrB,qBAAqB,GACtB,MAAM,kBAAkB,CAAC;AAC1B,OAAO,EAAE,6BAA6B,EAAE,KAAK,qBAAqB,EAAE,MAAM,yBAAyB,CAAC;AACpG,OAAO,EAAE,cAAc,EAAE,iBAAiB,EAAE,MAAM,wBAAwB,CAAC"}
|
package/dist/secret.js
CHANGED
|
@@ -1,2 +1,3 @@
|
|
|
1
|
-
export { createSecretFileAtomic,
|
|
1
|
+
export { createSecretFileAtomic, PRIVATE_SECRET_DIR_MODE, PRIVATE_SECRET_FILE_MODE, readSecretFileSync, tryReadSecretFileSync, writeSecretFileAtomic, } from "./secret-file.js";
|
|
2
|
+
export { DEFAULT_SECRET_FILE_MAX_BYTES } from "./secret-read-policy.js";
|
|
2
3
|
export { readSecretFile, tryReadSecretFile } from "./secret-read-async.js";
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"secure-file.d.ts","sourceRoot":"","sources":["../src/secure-file.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,SAAS,CAAC;AAYrC,OAAO,EAOL,KAAK,eAAe,EACpB,KAAK,sBAAsB,EAC5B,MAAM,kBAAkB,CAAC;
|
|
1
|
+
{"version":3,"file":"secure-file.d.ts","sourceRoot":"","sources":["../src/secure-file.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,SAAS,CAAC;AAYrC,OAAO,EAOL,KAAK,eAAe,EACpB,KAAK,sBAAsB,EAC5B,MAAM,kBAAkB,CAAC;AAM1B,MAAM,MAAM,qBAAqB,GAAG;IAClC,QAAQ,EAAE,MAAM,CAAC;IACjB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,KAAK,CAAC,EAAE,sBAAsB,CAAC;IAC/B,WAAW,CAAC,EAAE,2BAA2B,CAAC;IAC1C,MAAM,CAAC,EAAE,uBAAuB,CAAC;IACjC,EAAE,CAAC,EAAE,mBAAmB,CAAC;CAC1B,CAAC;AAEF,MAAM,MAAM,sBAAsB,GAAG;IACnC,WAAW,CAAC,EAAE,MAAM,EAAE,CAAC;IACvB,YAAY,CAAC,EAAE,OAAO,CAAC;IACvB,gBAAgB,CAAC,EAAE,OAAO,CAAC;CAC5B,CAAC;AAEF,MAAM,MAAM,2BAA2B,GAAG;IACxC,aAAa,CAAC,EAAE,OAAO,CAAC;IACxB,qBAAqB,CAAC,EAAE,OAAO,CAAC;CACjC,CAAC;AAEF,MAAM,MAAM,uBAAuB,GAAG,sBAAsB,CAAC;AAE7D,MAAM,MAAM,mBAAmB,GAAG;IAChC,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB,CAAC;AAEF,MAAM,MAAM,oBAAoB,GAAG;IACjC,MAAM,EAAE,MAAM,CAAC;IACf,QAAQ,EAAE,MAAM,CAAC;IACjB,IAAI,EAAE,KAAK,CAAC;IACZ,WAAW,CAAC,EAAE,eAAe,CAAC;CAC/B,CAAC;AAyLF,wBAAsB,cAAc,CAClC,OAAO,EAAE,qBAAqB,GAC7B,OAAO,CAAC,oBAAoB,CAAC,CAc/B"}
|
package/dist/secure-file.js
CHANGED
|
@@ -4,10 +4,11 @@ import path from "node:path";
|
|
|
4
4
|
import { readFileHandleBounded } from "./bounded-read.js";
|
|
5
5
|
import { assertNoUnsafeDeviceReadPath } from "./device-path.js";
|
|
6
6
|
import { FsSafeError } from "./errors.js";
|
|
7
|
-
import { sameFileIdentity } from "./file-identity.js";
|
|
8
7
|
import { isWindowsDriveLetterPath, isWindowsNetworkPath } from "./local-file-access.js";
|
|
9
8
|
import { isPathInside, isSymlinkOpenError } from "./path.js";
|
|
9
|
+
import { formatPermissionErrorDetail } from "./permission-exec.js";
|
|
10
10
|
import { inspectPathPermissions, isGroupReadable, isGroupWritable, isWorldReadable, isWorldWritable, modeBits, } from "./permissions.js";
|
|
11
|
+
import { inspectFileIdentity } from "./strict-file-identity.js";
|
|
11
12
|
const SUPPORTS_NOFOLLOW = process.platform !== "win32" && "O_NOFOLLOW" in fsConstants;
|
|
12
13
|
const OPEN_READ_FLAGS = fsConstants.O_RDONLY | (SUPPORTS_NOFOLLOW ? fsConstants.O_NOFOLLOW : 0);
|
|
13
14
|
function isAbsolutePathname(value) {
|
|
@@ -52,20 +53,18 @@ async function openSecureHandle(options) {
|
|
|
52
53
|
if (!openedStat.isFile()) {
|
|
53
54
|
throw new FsSafeError("not-file", `${label(options)} must be a file: ${options.filePath}`);
|
|
54
55
|
}
|
|
55
|
-
const
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
56
|
+
const openedIdentity = await inspectFileIdentity(() => handle.stat({ bigint: true }));
|
|
57
|
+
await inspectFileIdentity(async () => {
|
|
58
|
+
const pathStat = options.trust?.allowSymlink
|
|
59
|
+
? await fs.stat(options.filePath, { bigint: true })
|
|
60
|
+
: await fs.lstat(options.filePath, { bigint: true });
|
|
61
|
+
if (!options.trust?.allowSymlink && pathStat.isSymbolicLink()) {
|
|
62
|
+
throw new FsSafeError("symlink", `${label(options)} must not be a symlink: ${options.filePath}`);
|
|
63
|
+
}
|
|
64
|
+
return pathStat;
|
|
65
|
+
}, openedIdentity);
|
|
64
66
|
const realPath = await fs.realpath(options.filePath);
|
|
65
|
-
|
|
66
|
-
if (!sameFileIdentity(realStat, openedStat)) {
|
|
67
|
-
throw new FsSafeError("path-mismatch", `${label(options)} real path changed during open.`);
|
|
68
|
-
}
|
|
67
|
+
await inspectFileIdentity(() => fs.stat(realPath, { bigint: true }), openedIdentity);
|
|
69
68
|
if (options.io?.maxBytes !== undefined && openedStat.size > options.io.maxBytes) {
|
|
70
69
|
throw new FsSafeError("too-large", `${label(options)} exceeded maxBytes (${options.io.maxBytes}).`);
|
|
71
70
|
}
|
|
@@ -111,11 +110,21 @@ async function assertSecurePermissions(options, stat, realPath) {
|
|
|
111
110
|
const permissions = platform === "win32"
|
|
112
111
|
? await inspectPathPermissions(realPath, options.inject)
|
|
113
112
|
: inspectOpenedPermissions(stat, platform);
|
|
113
|
+
const reason = permissions.error ? `: ${formatPermissionErrorDetail(permissions.error)}` : "";
|
|
114
|
+
const diagnostics = {
|
|
115
|
+
...(permissions.errorCause !== undefined ? { cause: permissions.errorCause } : {}),
|
|
116
|
+
...(permissions.ownerError || permissions.errorDetail ? {
|
|
117
|
+
details: {
|
|
118
|
+
...(permissions.ownerError ? { ownerError: formatPermissionErrorDetail(permissions.ownerError) } : {}),
|
|
119
|
+
...permissions.errorDetail,
|
|
120
|
+
},
|
|
121
|
+
} : {}),
|
|
122
|
+
};
|
|
114
123
|
if (!permissions.ok) {
|
|
115
|
-
throw new FsSafeError("permission-unverified", `${label(options)} permissions could not be verified: ${realPath}
|
|
124
|
+
throw new FsSafeError("permission-unverified", `${label(options)} permissions could not be verified: ${realPath}${reason}`, diagnostics);
|
|
116
125
|
}
|
|
117
126
|
if (platform === "win32" && permissions.source === "unknown") {
|
|
118
|
-
throw new FsSafeError("permission-unverified", `${label(options)} ACL verification unavailable on Windows for ${realPath}
|
|
127
|
+
throw new FsSafeError("permission-unverified", `${label(options)} ACL verification unavailable on Windows for ${realPath}${reason || "."}`, diagnostics);
|
|
119
128
|
}
|
|
120
129
|
if (platform === "win32" && permissions.ownerTrusted !== true) {
|
|
121
130
|
throw new FsSafeError(permissions.ownerTrusted === false ? "not-owned" : "permission-unverified", `${label(options)} owner could not be trusted on Windows: ${realPath}`);
|
package/dist/sibling-temp.js
CHANGED
|
@@ -51,7 +51,7 @@ export async function writeSiblingTempFile(options) {
|
|
|
51
51
|
try {
|
|
52
52
|
tempExists = true;
|
|
53
53
|
const result = await options.writeTemp(tempPath);
|
|
54
|
-
unregisterTempPath.setIdentity(await fs.lstat(tempPath));
|
|
54
|
+
unregisterTempPath.setIdentity(await fs.lstat(tempPath, { bigint: true }));
|
|
55
55
|
if (options.mode !== undefined) {
|
|
56
56
|
await fs.chmod(tempPath, options.mode).catch(() => undefined);
|
|
57
57
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"sidecar-lock-acquire.d.ts","sourceRoot":"","sources":["../src/sidecar-lock-acquire.ts"],"names":[],"mappings":"AAGA,OAAO,EAA6B,KAAK,gBAAgB,EAAE,MAAM,wBAAwB,CAAC;AAC1F,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,gBAAgB,CAAC;AAO3C,OAAO,EAUL,KAAK,mBAAmB,EACzB,MAAM,2BAA2B,CAAC;AACnC,OAAO,KAAK,EAAE,yBAAyB,EAAE,iBAAiB,EAAE,MAAM,yBAAyB,CAAC;AAE5F,KAAK,iBAAiB,GAAG,IAAI,CAAC,gBAAgB,EAAE,OAAO,GAAG,MAAM,GAAG,WAAW,CAAC,CAAC;AAEhF,MAAM,MAAM,eAAe,GAAG;IAC5B,QAAQ,EAAE,MAAM,CAAC;IACjB,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,MAAM,EAAE,iBAAiB,CAAC;IAC1B,QAAQ,EAAE,MAAM,CAAC;IACjB,QAAQ,EAAE,mBAAmB,CAAC;IAC9B,UAAU,EAAE,MAAM,CAAC;IACnB,QAAQ,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IAClC,cAAc,CAAC,EAAE,OAAO,CAAC,IAAI,CAAC,CAAC;IAC/B,QAAQ,CAAC,EAAE,IAAI,CAAC;IAChB,YAAY,CAAC,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,OAAO,CAAC;IACxC,eAAe,CAAC,EAAE,MAAM,CAAC,OAAO,CAAC;CAClC,CAAC;AAEF,KAAK,6BAA6B,GAAG;IACnC,IAAI,EAAE,GAAG,CAAC,MAAM,EAAE,eAAe,CAAC,CAAC;IACnC,aAAa,EAAE,GAAG,CAAC,MAAM,CAAC,CAAC;IAC3B,2BAA2B,IAAI,IAAI,CAAC;IACpC,iBAAiB,CAAC,oBAAoB,EAAE,MAAM,EAAE,IAAI,EAAE,eAAe,GAAG,iBAAiB,CAAC;IAC1F,eAAe,CACb,oBAAoB,EAAE,MAAM,EAC5B,IAAI,EAAE,eAAe,EACrB,OAAO,CAAC,EAAE;QAAE,KAAK,CAAC,EAAE,OAAO,CAAA;KAAE,GAC5B,OAAO,CAAC,OAAO,CAAC,CAAC;CACrB,CAAC;AAaF,wBAAsB,kBAAkB,CAAC,QAAQ,SAAS,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAC/E,OAAO,EAAE,yBAAyB,CAAC,QAAQ,CAAC,EAC5C,OAAO,EAAE,6BAA6B,GACrC,OAAO,CAAC,iBAAiB,CAAC,
|
|
1
|
+
{"version":3,"file":"sidecar-lock-acquire.d.ts","sourceRoot":"","sources":["../src/sidecar-lock-acquire.ts"],"names":[],"mappings":"AAGA,OAAO,EAA6B,KAAK,gBAAgB,EAAE,MAAM,wBAAwB,CAAC;AAC1F,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,gBAAgB,CAAC;AAO3C,OAAO,EAUL,KAAK,mBAAmB,EACzB,MAAM,2BAA2B,CAAC;AACnC,OAAO,KAAK,EAAE,yBAAyB,EAAE,iBAAiB,EAAE,MAAM,yBAAyB,CAAC;AAE5F,KAAK,iBAAiB,GAAG,IAAI,CAAC,gBAAgB,EAAE,OAAO,GAAG,MAAM,GAAG,WAAW,CAAC,CAAC;AAEhF,MAAM,MAAM,eAAe,GAAG;IAC5B,QAAQ,EAAE,MAAM,CAAC;IACjB,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,MAAM,EAAE,iBAAiB,CAAC;IAC1B,QAAQ,EAAE,MAAM,CAAC;IACjB,QAAQ,EAAE,mBAAmB,CAAC;IAC9B,UAAU,EAAE,MAAM,CAAC;IACnB,QAAQ,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IAClC,cAAc,CAAC,EAAE,OAAO,CAAC,IAAI,CAAC,CAAC;IAC/B,QAAQ,CAAC,EAAE,IAAI,CAAC;IAChB,YAAY,CAAC,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,OAAO,CAAC;IACxC,eAAe,CAAC,EAAE,MAAM,CAAC,OAAO,CAAC;CAClC,CAAC;AAEF,KAAK,6BAA6B,GAAG;IACnC,IAAI,EAAE,GAAG,CAAC,MAAM,EAAE,eAAe,CAAC,CAAC;IACnC,aAAa,EAAE,GAAG,CAAC,MAAM,CAAC,CAAC;IAC3B,2BAA2B,IAAI,IAAI,CAAC;IACpC,iBAAiB,CAAC,oBAAoB,EAAE,MAAM,EAAE,IAAI,EAAE,eAAe,GAAG,iBAAiB,CAAC;IAC1F,eAAe,CACb,oBAAoB,EAAE,MAAM,EAC5B,IAAI,EAAE,eAAe,EACrB,OAAO,CAAC,EAAE;QAAE,KAAK,CAAC,EAAE,OAAO,CAAA;KAAE,GAC5B,OAAO,CAAC,OAAO,CAAC,CAAC;CACrB,CAAC;AAaF,wBAAsB,kBAAkB,CAAC,QAAQ,SAAS,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAC/E,OAAO,EAAE,yBAAyB,CAAC,QAAQ,CAAC,EAC5C,OAAO,EAAE,6BAA6B,GACrC,OAAO,CAAC,iBAAiB,CAAC,CAsP5B"}
|
|
@@ -133,7 +133,10 @@ export async function acquireSidecarLock(options, context) {
|
|
|
133
133
|
const interval = options.compromiseCheckIntervalMs;
|
|
134
134
|
if (options.onCompromised && interval !== undefined && interval > 0) {
|
|
135
135
|
createdHeld.compromiseTimer = setInterval(() => {
|
|
136
|
-
void returnedHandle
|
|
136
|
+
void returnedHandle
|
|
137
|
+
.verifyStillHeld()
|
|
138
|
+
.catch(() => false)
|
|
139
|
+
.then((stillHeld) => {
|
|
137
140
|
if (!stillHeld && createdHeld.compromiseTimer) {
|
|
138
141
|
clearInterval(createdHeld.compromiseTimer);
|
|
139
142
|
createdHeld.compromiseTimer = undefined;
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { DirectoryReceipt } from "./directory-durability.js";
|
|
2
|
+
import type { FileIdentityStat } from "./file-identity.js";
|
|
3
|
+
import type { StagedFileReceipt } from "./staged-file-types.js";
|
|
4
|
+
type DirectorySnapshot = StagedFileReceipt["directory"];
|
|
5
|
+
export declare function exactIdentityMatches(expected: FileIdentityStat, actual: Readonly<{
|
|
6
|
+
dev: bigint;
|
|
7
|
+
ino: bigint;
|
|
8
|
+
}>): boolean;
|
|
9
|
+
export declare function describeStagedDirectory(fd: number, pathname: string): DirectorySnapshot;
|
|
10
|
+
export declare function assertStagedDirectoryCurrent(receipt: DirectorySnapshot): void;
|
|
11
|
+
export declare function openStagedDirectory(directory: string | DirectoryReceipt): {
|
|
12
|
+
fd: number;
|
|
13
|
+
receipt: DirectorySnapshot;
|
|
14
|
+
};
|
|
15
|
+
export {};
|
|
16
|
+
//# sourceMappingURL=staged-directory.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"staged-directory.d.ts","sourceRoot":"","sources":["../src/staged-directory.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,2BAA2B,CAAC;AAElE,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,oBAAoB,CAAC;AAC3D,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,wBAAwB,CAAC;AAEhE,KAAK,iBAAiB,GAAG,iBAAiB,CAAC,WAAW,CAAC,CAAC;AAExD,wBAAgB,oBAAoB,CAClC,QAAQ,EAAE,gBAAgB,EAC1B,MAAM,EAAE,QAAQ,CAAC;IAAE,GAAG,EAAE,MAAM,CAAC;IAAC,GAAG,EAAE,MAAM,CAAA;CAAE,CAAC,GAC7C,OAAO,CAKT;AAED,wBAAgB,uBAAuB,CAAC,EAAE,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,iBAAiB,CAYvF;AAED,wBAAgB,4BAA4B,CAAC,OAAO,EAAE,iBAAiB,GAAG,IAAI,CAQ7E;AAED,wBAAgB,mBAAmB,CAAC,SAAS,EAAE,MAAM,GAAG,gBAAgB,GAAG;IACzE,EAAE,EAAE,MAAM,CAAC;IACX,OAAO,EAAE,iBAAiB,CAAC;CAC5B,CA+BA"}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { FsSafeError } from "./errors.js";
|
|
4
|
+
export function exactIdentityMatches(expected, actual) {
|
|
5
|
+
return ["dev", "ino"].every((key) => {
|
|
6
|
+
const value = expected[key];
|
|
7
|
+
return (typeof value === "bigint" || Number.isSafeInteger(value)) && BigInt(value) === actual[key];
|
|
8
|
+
});
|
|
9
|
+
}
|
|
10
|
+
export function describeStagedDirectory(fd, pathname) {
|
|
11
|
+
const identity = fs.fstatSync(fd, { bigint: true });
|
|
12
|
+
if (!identity.isDirectory()) {
|
|
13
|
+
throw new FsSafeError("not-file", "staging parent must be a directory");
|
|
14
|
+
}
|
|
15
|
+
const receipt = Object.freeze({
|
|
16
|
+
path: path.resolve(pathname),
|
|
17
|
+
realPath: fs.realpathSync(pathname),
|
|
18
|
+
identity: Object.freeze({ dev: identity.dev, ino: identity.ino }),
|
|
19
|
+
});
|
|
20
|
+
assertStagedDirectoryCurrent(receipt);
|
|
21
|
+
return receipt;
|
|
22
|
+
}
|
|
23
|
+
export function assertStagedDirectoryCurrent(receipt) {
|
|
24
|
+
const current = fs.lstatSync(receipt.path, { bigint: true });
|
|
25
|
+
if (!current.isDirectory() || !exactIdentityMatches(receipt.identity, current) ||
|
|
26
|
+
fs.realpathSync(receipt.path) !== receipt.realPath) {
|
|
27
|
+
throw new FsSafeError("path-mismatch", "staging directory pathname changed");
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
export function openStagedDirectory(directory) {
|
|
31
|
+
// Copy supplied facts before any asynchronous work; receipts are not authority.
|
|
32
|
+
const pathname = path.resolve(typeof directory === "string" ? directory : directory.path);
|
|
33
|
+
const expected = typeof directory === "string" ? undefined : {
|
|
34
|
+
realPath: directory.realPath, dev: directory.identity.dev, ino: directory.identity.ino,
|
|
35
|
+
};
|
|
36
|
+
const before = fs.lstatSync(pathname, { bigint: true });
|
|
37
|
+
if (!before.isDirectory()) {
|
|
38
|
+
throw new FsSafeError("not-file", "staging parent must be a real directory");
|
|
39
|
+
}
|
|
40
|
+
if (expected && (!exactIdentityMatches(expected, before) || fs.realpathSync(pathname) !== expected.realPath)) {
|
|
41
|
+
throw new FsSafeError("path-mismatch", "stale staging directory receipt");
|
|
42
|
+
}
|
|
43
|
+
const fd = fs.openSync(pathname, fs.constants.O_RDONLY | fs.constants.O_DIRECTORY | fs.constants.O_NOFOLLOW | fs.constants.O_NONBLOCK);
|
|
44
|
+
try {
|
|
45
|
+
const receipt = describeStagedDirectory(fd, pathname);
|
|
46
|
+
if (!exactIdentityMatches(before, receipt.identity)) {
|
|
47
|
+
throw new FsSafeError("path-mismatch", "staging directory changed while opening");
|
|
48
|
+
}
|
|
49
|
+
return { fd, receipt };
|
|
50
|
+
}
|
|
51
|
+
catch (error) {
|
|
52
|
+
try {
|
|
53
|
+
fs.closeSync(fd);
|
|
54
|
+
}
|
|
55
|
+
catch (closeError) {
|
|
56
|
+
throw new AggregateError([error, closeError], "staging directory admission and close failed");
|
|
57
|
+
}
|
|
58
|
+
throw error;
|
|
59
|
+
}
|
|
60
|
+
}
|