@openclaw/fs-safe 0.5.6 → 0.7.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 +89 -0
- package/README.md +23 -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-deadline.d.ts +3 -0
- package/dist/archive-deadline.d.ts.map +1 -1
- package/dist/archive-deadline.js +44 -8
- package/dist/archive-entry.d.ts.map +1 -1
- package/dist/archive-entry.js +1 -0
- package/dist/archive-errors.d.ts +1 -0
- package/dist/archive-errors.d.ts.map +1 -1
- package/dist/archive-errors.js +3 -0
- package/dist/archive-input.d.ts.map +1 -1
- package/dist/archive-input.js +26 -16
- package/dist/archive-kind.js +2 -2
- package/dist/archive-limits.d.ts +11 -3
- package/dist/archive-limits.d.ts.map +1 -1
- package/dist/archive-limits.js +24 -0
- package/dist/archive-native.d.ts +3 -2
- package/dist/archive-native.d.ts.map +1 -1
- package/dist/archive-native.js +31 -9
- package/dist/archive-policy.d.ts +2 -0
- package/dist/archive-policy.d.ts.map +1 -1
- package/dist/archive-policy.js +9 -1
- package/dist/archive-read.d.ts.map +1 -1
- package/dist/archive-read.js +88 -41
- package/dist/archive-staging.d.ts +3 -0
- package/dist/archive-staging.d.ts.map +1 -1
- package/dist/archive-staging.js +91 -43
- package/dist/archive-tar-admission.d.ts +7 -0
- package/dist/archive-tar-admission.d.ts.map +1 -0
- package/dist/archive-tar-admission.js +43 -0
- package/dist/archive-tar-gnu.d.ts +2 -0
- package/dist/archive-tar-gnu.d.ts.map +1 -0
- package/dist/archive-tar-gnu.js +20 -0
- package/dist/archive-tar-header.d.ts +8 -0
- package/dist/archive-tar-header.d.ts.map +1 -0
- package/dist/archive-tar-header.js +47 -0
- package/dist/archive-tar-meta.d.ts +29 -1
- package/dist/archive-tar-meta.d.ts.map +1 -1
- package/dist/archive-tar-meta.js +135 -19
- 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 +4 -0
- package/dist/archive-tar-runtime.d.ts.map +1 -1
- package/dist/archive-tar-runtime.js +3 -0
- package/dist/archive-tar.d.ts.map +1 -1
- package/dist/archive-tar.js +8 -2
- 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 +63 -42
- package/dist/atomic.d.ts +1 -1
- package/dist/atomic.d.ts.map +1 -1
- package/dist/bounded-read-stream.d.ts.map +1 -1
- package/dist/bounded-read-stream.js +2 -4
- package/dist/bounded-read.d.ts.map +1 -1
- package/dist/bounded-read.js +3 -10
- package/dist/byte-budget.d.ts +5 -0
- package/dist/byte-budget.d.ts.map +1 -0
- package/dist/byte-budget.js +9 -0
- package/dist/file-hash.d.ts.map +1 -1
- package/dist/file-hash.js +25 -19
- package/dist/file-lock-sync.d.ts.map +1 -1
- package/dist/file-lock-sync.js +26 -9
- package/dist/file-store-boundary.d.ts.map +1 -1
- package/dist/file-store-boundary.js +9 -5
- package/dist/file-store-limit.d.ts +2 -0
- package/dist/file-store-limit.d.ts.map +1 -0
- package/dist/file-store-limit.js +8 -0
- package/dist/file-store-sync-write.d.ts.map +1 -1
- package/dist/file-store-sync-write.js +38 -8
- package/dist/file-store.d.ts.map +1 -1
- package/dist/file-store.js +26 -26
- package/dist/json-durable-queue-directory.d.ts +2 -0
- package/dist/json-durable-queue-directory.d.ts.map +1 -0
- package/dist/json-durable-queue-directory.js +20 -0
- package/dist/json-durable-queue-ownership.d.ts +14 -0
- package/dist/json-durable-queue-ownership.d.ts.map +1 -0
- package/dist/json-durable-queue-ownership.js +168 -0
- package/dist/json-durable-queue-retirement.d.ts +9 -0
- package/dist/json-durable-queue-retirement.d.ts.map +1 -0
- package/dist/json-durable-queue-retirement.js +126 -0
- package/dist/json-durable-queue-transfer-lock.d.ts +2 -0
- package/dist/json-durable-queue-transfer-lock.d.ts.map +1 -0
- package/dist/json-durable-queue-transfer-lock.js +19 -0
- package/dist/json-durable-queue.d.ts +1 -0
- package/dist/json-durable-queue.d.ts.map +1 -1
- package/dist/json-durable-queue.js +90 -57
- package/dist/json.d.ts.map +1 -1
- package/dist/json.js +27 -8
- package/dist/local-roots.d.ts.map +1 -1
- package/dist/local-roots.js +4 -2
- package/dist/native-binding.d.ts +15 -3
- 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 +22 -6
- 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.d.ts.map +1 -1
- package/dist/output-sibling.js +11 -110
- package/dist/output.d.ts.map +1 -1
- package/dist/output.js +4 -2
- 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-open.d.ts.map +1 -1
- package/dist/pinned-open.js +27 -48
- package/dist/pinned-write.d.ts +6 -0
- package/dist/pinned-write.d.ts.map +1 -1
- package/dist/pinned-write.js +26 -37
- package/dist/private-directory.d.ts.map +1 -1
- package/dist/private-directory.js +3 -2
- package/dist/private-temp-workspace.d.ts +3 -1
- package/dist/private-temp-workspace.d.ts.map +1 -1
- package/dist/private-temp-workspace.js +81 -56
- package/dist/publish-file.d.ts.map +1 -1
- package/dist/publish-file.js +2 -4
- package/dist/read-opened-file.d.ts.map +1 -1
- package/dist/read-opened-file.js +6 -4
- package/dist/regular-file.d.ts.map +1 -1
- package/dist/regular-file.js +136 -82
- package/dist/replace-file-copy-fallback.d.ts +3 -1
- package/dist/replace-file-copy-fallback.d.ts.map +1 -1
- package/dist/replace-file-copy-fallback.js +21 -36
- package/dist/replace-file-copy-source.d.ts +21 -0
- package/dist/replace-file-copy-source.d.ts.map +1 -0
- package/dist/replace-file-copy-source.js +112 -0
- package/dist/replace-file-descriptor.d.ts +13 -3
- package/dist/replace-file-descriptor.d.ts.map +1 -1
- package/dist/replace-file-descriptor.js +32 -7
- package/dist/replace-file-rename-policy.d.ts +7 -0
- package/dist/replace-file-rename-policy.d.ts.map +1 -0
- package/dist/replace-file-rename-policy.js +30 -0
- package/dist/replace-file-temp-owner.d.ts +46 -0
- package/dist/replace-file-temp-owner.d.ts.map +1 -0
- package/dist/replace-file-temp-owner.js +346 -0
- package/dist/replace-file.d.ts +6 -1
- package/dist/replace-file.d.ts.map +1 -1
- package/dist/replace-file.js +72 -58
- package/dist/root-impl.d.ts.map +1 -1
- package/dist/root-impl.js +118 -98
- 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 +49 -120
- 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 +51 -72
- 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 +42 -29
- package/dist/sibling-staged-file.d.ts +15 -0
- package/dist/sibling-staged-file.d.ts.map +1 -0
- package/dist/sibling-staged-file.js +148 -0
- package/dist/sibling-temp.d.ts +3 -0
- package/dist/sibling-temp.d.ts.map +1 -1
- package/dist/sibling-temp.js +30 -74
- package/dist/sidecar-lock-acquire.d.ts.map +1 -1
- package/dist/sidecar-lock-acquire.js +48 -27
- package/dist/sidecar-lock-handle.d.ts +6 -2
- package/dist/sidecar-lock-handle.d.ts.map +1 -1
- package/dist/sidecar-lock-handle.js +17 -3
- package/dist/sidecar-lock-policy.d.ts +2 -0
- package/dist/sidecar-lock-policy.d.ts.map +1 -1
- package/dist/sidecar-lock-policy.js +29 -0
- package/dist/sidecar-lock-reclaim.d.ts.map +1 -1
- package/dist/sidecar-lock-reclaim.js +29 -7
- package/dist/sidecar-lock.d.ts.map +1 -1
- package/dist/sidecar-lock.js +28 -16
- 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/suppressed-error.d.ts +6 -0
- package/dist/suppressed-error.d.ts.map +1 -0
- package/dist/suppressed-error.js +15 -0
- package/dist/temp-cleanup.d.ts +2 -0
- package/dist/temp-cleanup.d.ts.map +1 -1
- package/dist/temp-cleanup.js +25 -10
- package/dist/temp-workspace-owner.d.ts +23 -0
- package/dist/temp-workspace-owner.d.ts.map +1 -0
- package/dist/temp-workspace-owner.js +320 -0
- package/dist/temp.d.ts +1 -1
- package/dist/temp.d.ts.map +1 -1
- package/dist/test-hooks.d.ts +5 -0
- package/dist/test-hooks.d.ts.map +1 -1
- 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 +19 -2
- package/docs/archive.md +250 -35
- package/docs/atomic.md +11 -2
- package/docs/config.md +7 -0
- package/docs/contributing.md +45 -7
- package/docs/durability.md +18 -5
- package/docs/errors.md +16 -1
- package/docs/file-store.md +2 -0
- package/docs/index.md +3 -1
- package/docs/install.md +20 -8
- package/docs/json.md +8 -4
- package/docs/migrating-to-0.5.md +7 -7
- package/docs/migrating-to-0.6.md +43 -0
- package/docs/native-helper.md +30 -8
- package/docs/native.md +73 -17
- package/docs/output.md +10 -0
- package/docs/path-scope.md +28 -2
- package/docs/permissions.md +13 -2
- package/docs/public-api.md +6 -3
- package/docs/quickstart.md +1 -1
- package/docs/reading.md +1 -1
- package/docs/regular-file.md +9 -2
- package/docs/root.md +3 -1
- package/docs/secret-file.md +12 -0
- package/docs/secure-file.md +21 -3
- package/docs/security-model.md +33 -1
- package/docs/sidecar-lock.md +15 -1
- package/docs/staged-file.md +178 -0
- package/docs/store.md +13 -0
- package/docs/temp.md +128 -14
- 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
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import fsSync, {} from "node:fs";
|
|
2
|
+
import fs from "node:fs/promises";
|
|
3
|
+
import { assertAsyncDirectoryGuard } from "./directory-guard.js";
|
|
4
|
+
import { FsSafeError } from "./errors.js";
|
|
5
|
+
import { sameFileIdentity } from "./file-identity.js";
|
|
6
|
+
import { resolveOpenedFileRealPathForFd } from "./opened-realpath.js";
|
|
7
|
+
import { assertNoUnsafeDeviceReadPath, hasNodeErrorCode, isNotFoundPathError, isPathInside, isSymlinkOpenError } from "./path.js";
|
|
8
|
+
import { resolveReadOpenFlags } from "./read-open-flags.js";
|
|
9
|
+
import { assertRootIdentityCurrent } from "./root-context.js";
|
|
10
|
+
import { fileNotFoundError, hardlinkedPathNotAllowedError, outsideWorkspaceError } from "./root-errors.js";
|
|
11
|
+
export async function verifyAtomicWriteResult(params) {
|
|
12
|
+
let needsPathOpen = false;
|
|
13
|
+
const assertFile = (stat) => {
|
|
14
|
+
if (stat.isSymbolicLink()) {
|
|
15
|
+
throw new FsSafeError("symlink", "symlink not allowed");
|
|
16
|
+
}
|
|
17
|
+
if (!stat.isFile()) {
|
|
18
|
+
throw new FsSafeError("not-file", "not a file");
|
|
19
|
+
}
|
|
20
|
+
if (!sameFileIdentity(stat, params.expectedIdentity)) {
|
|
21
|
+
throw new FsSafeError("path-mismatch", "path changed during write");
|
|
22
|
+
}
|
|
23
|
+
if (stat.nlink > 1) {
|
|
24
|
+
throw hardlinkedPathNotAllowedError();
|
|
25
|
+
}
|
|
26
|
+
};
|
|
27
|
+
const assertPath = (stat) => {
|
|
28
|
+
assertFile(stat);
|
|
29
|
+
needsPathOpen ||= process.platform === "win32" && (stat.dev === 0n || stat.ino === 0n);
|
|
30
|
+
};
|
|
31
|
+
const assertDescriptor = (fd = params.fd) => {
|
|
32
|
+
const stat = fsSync.fstatSync(fd, { bigint: true });
|
|
33
|
+
// Only Windows pathname stats may have unknown identity; fd stats must match exactly.
|
|
34
|
+
if (params.expectedIdentity.dev !== stat.dev || params.expectedIdentity.ino !== stat.ino) {
|
|
35
|
+
throw new FsSafeError("path-mismatch", "descriptor changed during write");
|
|
36
|
+
}
|
|
37
|
+
assertFile(stat);
|
|
38
|
+
return stat;
|
|
39
|
+
};
|
|
40
|
+
try {
|
|
41
|
+
// This descriptor remains owned by the writer, even when final mode forbids opens.
|
|
42
|
+
const stat = assertDescriptor();
|
|
43
|
+
assertPath(await fs.lstat(params.targetPath, { bigint: true }));
|
|
44
|
+
const realPath = await resolveOpenedFileRealPathForFd(params.fd, stat, params.targetPath);
|
|
45
|
+
assertPath(await fs.stat(realPath, { bigint: true }));
|
|
46
|
+
if (!isPathInside(params.root.rootWithSep, realPath)) {
|
|
47
|
+
throw outsideWorkspaceError();
|
|
48
|
+
}
|
|
49
|
+
await assertAsyncDirectoryGuard(params.parentGuard);
|
|
50
|
+
await assertRootIdentityCurrent(params.root);
|
|
51
|
+
// Recheck after canonical resolution and directory checks, including late links.
|
|
52
|
+
assertPath(await fs.lstat(params.targetPath, { bigint: true }));
|
|
53
|
+
assertDescriptor();
|
|
54
|
+
if (needsPathOpen) {
|
|
55
|
+
// A retained fd cannot prove that an opaque Windows pathname still names it.
|
|
56
|
+
// Reopen only for publication verification: the writer's exact identity is
|
|
57
|
+
// independent proof unavailable to ordinary readers. Never read any bytes.
|
|
58
|
+
assertNoUnsafeDeviceReadPath(params.targetPath);
|
|
59
|
+
const opened = await fs.open(params.targetPath, resolveReadOpenFlags()).catch((error) => {
|
|
60
|
+
if (isSymlinkOpenError(error)) {
|
|
61
|
+
throw new FsSafeError("symlink", "symlink open blocked", { cause: error });
|
|
62
|
+
}
|
|
63
|
+
if (hasNodeErrorCode(error, "EISDIR")) {
|
|
64
|
+
throw new FsSafeError("not-file", "not a file");
|
|
65
|
+
}
|
|
66
|
+
throw error;
|
|
67
|
+
});
|
|
68
|
+
try {
|
|
69
|
+
const reopenedStat = assertDescriptor(opened.fd);
|
|
70
|
+
assertPath(await fs.lstat(params.targetPath, { bigint: true }));
|
|
71
|
+
const reopenedPath = await resolveOpenedFileRealPathForFd(opened.fd, reopenedStat, params.targetPath);
|
|
72
|
+
assertPath(await fs.stat(reopenedPath, { bigint: true }));
|
|
73
|
+
if (!isPathInside(params.root.rootWithSep, reopenedPath)) {
|
|
74
|
+
throw outsideWorkspaceError();
|
|
75
|
+
}
|
|
76
|
+
await assertAsyncDirectoryGuard(params.parentGuard);
|
|
77
|
+
await assertRootIdentityCurrent(params.root);
|
|
78
|
+
assertPath(await fs.lstat(params.targetPath, { bigint: true }));
|
|
79
|
+
assertDescriptor(opened.fd);
|
|
80
|
+
}
|
|
81
|
+
finally {
|
|
82
|
+
await opened.close().catch(() => undefined);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
catch (error) {
|
|
87
|
+
if (isNotFoundPathError(error))
|
|
88
|
+
throw fileNotFoundError();
|
|
89
|
+
throw error;
|
|
90
|
+
}
|
|
91
|
+
}
|
package/dist/secret-file.d.ts
CHANGED
|
@@ -1,11 +1,6 @@
|
|
|
1
|
-
|
|
1
|
+
import { type SecretFileReadOptions } from "./secret-read-policy.js";
|
|
2
2
|
export declare const PRIVATE_SECRET_DIR_MODE = 448;
|
|
3
3
|
export declare const PRIVATE_SECRET_FILE_MODE = 384;
|
|
4
|
-
export type SecretFileReadOptions = {
|
|
5
|
-
maxBytes?: number;
|
|
6
|
-
rejectSymlink?: boolean;
|
|
7
|
-
rejectHardlinks?: boolean;
|
|
8
|
-
};
|
|
9
4
|
export declare function readSecretFileSync(filePath: string, label: string, options?: SecretFileReadOptions): string;
|
|
10
5
|
export declare function tryReadSecretFileSync(filePath: string | undefined, label: string, options?: SecretFileReadOptions): string | undefined;
|
|
11
6
|
type SecretFileWriteParams = {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"secret-file.d.ts","sourceRoot":"","sources":["../src/secret-file.ts"],"names":[],"mappings":"AAaA,
|
|
1
|
+
{"version":3,"file":"secret-file.d.ts","sourceRoot":"","sources":["../src/secret-file.ts"],"names":[],"mappings":"AAaA,OAAO,EAML,KAAK,qBAAqB,EAC3B,MAAM,yBAAyB,CAAC;AAIjC,eAAO,MAAM,uBAAuB,MAAQ,CAAC;AAC7C,eAAO,MAAM,wBAAwB,MAAQ,CAAC;AAE9C,wBAAgB,kBAAkB,CAChC,QAAQ,EAAE,MAAM,EAChB,KAAK,EAAE,MAAM,EACb,OAAO,GAAE,qBAA0B,GAClC,MAAM,CAwER;AAED,wBAAgB,qBAAqB,CACnC,QAAQ,EAAE,MAAM,GAAG,SAAS,EAC5B,KAAK,EAAE,MAAM,EACb,OAAO,GAAE,qBAA0B,GAClC,MAAM,GAAG,SAAS,CAUpB;AAkID,KAAK,qBAAqB,GAAG;IAC3B,OAAO,EAAE,MAAM,CAAC;IAChB,QAAQ,EAAE,MAAM,CAAC;IACjB,OAAO,EAAE,MAAM,GAAG,UAAU,CAAC;IAC7B,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB,CAAC;AAkEF,wBAAsB,qBAAqB,CAAC,MAAM,EAAE,qBAAqB,GAAG,OAAO,CAAC,IAAI,CAAC,CAKxF;AAED,wBAAsB,sBAAsB,CAAC,MAAM,EAAE,qBAAqB,GAAG,OAAO,CAAC,IAAI,CAAC,CAezF"}
|
package/dist/secret-file.js
CHANGED
|
@@ -3,161 +3,91 @@ import fsp from "node:fs/promises";
|
|
|
3
3
|
import path from "node:path";
|
|
4
4
|
import { canonicalPathFromExistingAncestor } from "./absolute-path.js";
|
|
5
5
|
import { readFileDescriptorBoundedSync } from "./bounded-read.js";
|
|
6
|
+
import { normalizeMaxBytes } from "./byte-budget.js";
|
|
6
7
|
import { assertAsyncDirectoryGuard, createAsyncDirectoryGuard } from "./directory-guard.js";
|
|
7
8
|
import { FsSafeError } from "./errors.js";
|
|
8
9
|
import { sameFileIdentity } from "./file-identity.js";
|
|
9
10
|
import { resolveHomeRelativePath } from "./home-dir.js";
|
|
10
11
|
import { openPinnedFileSync } from "./pinned-open.js";
|
|
12
|
+
import { resolveReadOpenFlags } from "./read-open-flags.js";
|
|
11
13
|
import { runPinnedWriteHelper } from "./pinned-write.js";
|
|
14
|
+
import { assertSecretFilePreview, DEFAULT_SECRET_FILE_MAX_BYTES, secretPathErrorCode, secretReadError, trimSecretFileContent, } from "./secret-read-policy.js";
|
|
15
|
+
import { inspectFileIdentitySync } from "./strict-file-identity.js";
|
|
12
16
|
import { serializePathWrite } from "./write-queue.js";
|
|
13
|
-
export const DEFAULT_SECRET_FILE_MAX_BYTES = 16 * 1024;
|
|
14
17
|
export const PRIVATE_SECRET_DIR_MODE = 0o700;
|
|
15
18
|
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);
|
|
19
|
+
export function readSecretFileSync(filePath, label, options = {}) {
|
|
20
|
+
const resolvedPath = resolveHomeRelativePath(filePath.trim());
|
|
29
21
|
if (!resolvedPath) {
|
|
30
|
-
|
|
22
|
+
throw new FsSafeError("invalid-path", `${label} file path is empty.`, { cause: undefined });
|
|
23
|
+
}
|
|
24
|
+
const maxBytes = normalizeMaxBytes(options.maxBytes, {
|
|
25
|
+
defaultValue: DEFAULT_SECRET_FILE_MAX_BYTES,
|
|
26
|
+
});
|
|
27
|
+
function inspectInput(symlinkMessage) {
|
|
28
|
+
const stat = options.rejectSymlink
|
|
29
|
+
? fs.lstatSync(resolvedPath, { bigint: true })
|
|
30
|
+
: fs.statSync(resolvedPath, { bigint: true });
|
|
31
|
+
if (options.rejectSymlink && stat.isSymbolicLink()) {
|
|
32
|
+
throw new FsSafeError("symlink", symlinkMessage);
|
|
33
|
+
}
|
|
34
|
+
return stat;
|
|
31
35
|
}
|
|
32
|
-
const maxBytes = options.maxBytes ?? DEFAULT_SECRET_FILE_MAX_BYTES;
|
|
33
36
|
let previewStat;
|
|
34
37
|
try {
|
|
35
|
-
previewStat =
|
|
38
|
+
previewStat = inspectFileIdentitySync(() => inspectInput(`${label} file at ${resolvedPath} must not be a symlink.`));
|
|
36
39
|
}
|
|
37
40
|
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
|
-
};
|
|
41
|
+
throw secretReadError(error instanceof FsSafeError ? error.code : secretPathErrorCode(error), "inspect", label, resolvedPath, error);
|
|
89
42
|
}
|
|
43
|
+
assertSecretFilePreview(previewStat, label, resolvedPath, maxBytes, options.rejectHardlinks !== false);
|
|
90
44
|
const opened = openPinnedFileSync({
|
|
91
45
|
filePath: resolvedPath,
|
|
92
46
|
rejectPathSymlink: options.rejectSymlink,
|
|
93
47
|
rejectHardlinks: options.rejectHardlinks !== false,
|
|
94
48
|
});
|
|
95
49
|
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
|
-
};
|
|
50
|
+
throw secretReadError(opened.reason === "path" ? "not-found" : "path-mismatch", "read", label, resolvedPath, opened.reason === "validation" ? new Error("security validation failed") : opened.error);
|
|
103
51
|
}
|
|
52
|
+
let raw;
|
|
104
53
|
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 };
|
|
54
|
+
const openedIdentity = inspectFileIdentitySync(() => {
|
|
55
|
+
const stat = fs.fstatSync(opened.fd, { bigint: true });
|
|
56
|
+
if (!stat.isFile() || (options.rejectHardlinks !== false && stat.nlink > 1n)) {
|
|
57
|
+
throw new FsSafeError("path-mismatch", "security validation failed");
|
|
58
|
+
}
|
|
59
|
+
return stat;
|
|
60
|
+
}, previewStat);
|
|
61
|
+
inspectFileIdentitySync(() => {
|
|
62
|
+
const stat = fs.lstatSync(opened.path, { bigint: true });
|
|
63
|
+
if (!stat.isFile() || (options.rejectHardlinks !== false && stat.nlink > 1n)) {
|
|
64
|
+
throw new FsSafeError("path-mismatch", "security validation failed");
|
|
65
|
+
}
|
|
66
|
+
return stat;
|
|
67
|
+
}, openedIdentity);
|
|
68
|
+
inspectFileIdentitySync(() => inspectInput("secret path became a symlink"), openedIdentity);
|
|
69
|
+
raw = readFileDescriptorBoundedSync(opened.fd, maxBytes).toString("utf8");
|
|
124
70
|
}
|
|
125
71
|
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
|
-
};
|
|
72
|
+
throw secretReadError(error instanceof FsSafeError ? error.code : "read-failed", "read", label, resolvedPath, error);
|
|
133
73
|
}
|
|
134
74
|
finally {
|
|
135
75
|
fs.closeSync(opened.fd);
|
|
136
76
|
}
|
|
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
|
-
});
|
|
77
|
+
return trimSecretFileContent(raw, label, resolvedPath);
|
|
146
78
|
}
|
|
147
79
|
export function tryReadSecretFileSync(filePath, label, options = {}) {
|
|
148
80
|
if (!filePath?.trim()) {
|
|
149
81
|
return undefined;
|
|
150
82
|
}
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
return result.secret;
|
|
83
|
+
try {
|
|
84
|
+
return readSecretFileSync(filePath, label, options);
|
|
154
85
|
}
|
|
155
|
-
|
|
156
|
-
|
|
86
|
+
catch (error) {
|
|
87
|
+
if (error instanceof FsSafeError && error.code === "not-found")
|
|
88
|
+
return undefined;
|
|
89
|
+
throw error;
|
|
157
90
|
}
|
|
158
|
-
throw new FsSafeError(result.code, result.message, {
|
|
159
|
-
cause: result.error,
|
|
160
|
-
});
|
|
161
91
|
}
|
|
162
92
|
function isRelativeEscape(relativePath) {
|
|
163
93
|
return relativePath === ".." || relativePath.startsWith(`..${path.sep}`) || path.isAbsolute(relativePath);
|
|
@@ -186,8 +116,7 @@ async function enforcePrivatePathMode(resolvedPath, expectedMode, kind) {
|
|
|
186
116
|
}
|
|
187
117
|
}
|
|
188
118
|
async function enforcePrivateFileIdentityAndMode(resolvedPath, expectedIdentity, expectedMode) {
|
|
189
|
-
const
|
|
190
|
-
const handle = await fsp.open(resolvedPath, fs.constants.O_RDONLY | noFollowFlag);
|
|
119
|
+
const handle = await fsp.open(resolvedPath, resolveReadOpenFlags());
|
|
191
120
|
try {
|
|
192
121
|
const openedStat = await handle.stat();
|
|
193
122
|
if (!openedStat.isFile() || !sameFileIdentity(openedStat, expectedIdentity)) {
|
|
@@ -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":"
|
|
1
|
+
{"version":3,"file":"secret-read-async.d.ts","sourceRoot":"","sources":["../src/secret-read-async.ts"],"names":[],"mappings":"AAQA,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,CA8DjB;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"}
|
|
@@ -1,99 +1,78 @@
|
|
|
1
1
|
import fsSync from "node:fs";
|
|
2
2
|
import fs from "node:fs/promises";
|
|
3
3
|
import { readFileHandleBounded } from "./bounded-read.js";
|
|
4
|
+
import { normalizeMaxBytes } from "./byte-budget.js";
|
|
4
5
|
import { FsSafeError } from "./errors.js";
|
|
5
|
-
import { sameFileIdentity } from "./file-identity.js";
|
|
6
6
|
import { resolveHomeRelativePath } from "./home-dir.js";
|
|
7
|
-
import {
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
}
|
|
12
|
-
async function readSecretFileOutcome(filePath, label, options) {
|
|
7
|
+
import { resolveReadOpenFlags } from "./read-open-flags.js";
|
|
8
|
+
import { inspectFileIdentity } from "./strict-file-identity.js";
|
|
9
|
+
import { assertSecretFilePreview, DEFAULT_SECRET_FILE_MAX_BYTES, secretPathErrorCode, secretReadError, trimSecretFileContent, } from "./secret-read-policy.js";
|
|
10
|
+
export async function readSecretFile(filePath, label, options = {}) {
|
|
13
11
|
const resolvedPath = resolveHomeRelativePath(filePath.trim());
|
|
14
12
|
if (!resolvedPath) {
|
|
15
|
-
|
|
13
|
+
throw new FsSafeError("invalid-path", `${label} file path is empty.`, { cause: undefined });
|
|
14
|
+
}
|
|
15
|
+
const maxBytes = normalizeMaxBytes(options.maxBytes, {
|
|
16
|
+
defaultValue: DEFAULT_SECRET_FILE_MAX_BYTES,
|
|
17
|
+
});
|
|
18
|
+
async function inspectInput(symlinkMessage) {
|
|
19
|
+
const stat = options.rejectSymlink
|
|
20
|
+
? await fs.lstat(resolvedPath, { bigint: true })
|
|
21
|
+
: await fs.stat(resolvedPath, { bigint: true });
|
|
22
|
+
if (options.rejectSymlink && stat.isSymbolicLink()) {
|
|
23
|
+
throw new FsSafeError("symlink", symlinkMessage);
|
|
24
|
+
}
|
|
25
|
+
return stat;
|
|
16
26
|
}
|
|
17
|
-
const maxBytes = options.maxBytes ?? DEFAULT_SECRET_FILE_MAX_BYTES;
|
|
18
27
|
let previewStat;
|
|
19
28
|
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
|
-
}
|
|
29
|
+
previewStat = await inspectFileIdentity(() => inspectInput(`${label} file at ${resolvedPath} must not be a symlink.`));
|
|
27
30
|
}
|
|
28
31
|
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.` };
|
|
32
|
+
throw secretReadError(error instanceof FsSafeError ? error.code : secretPathErrorCode(error), "inspect", label, resolvedPath, error);
|
|
45
33
|
}
|
|
34
|
+
assertSecretFilePreview(previewStat, label, resolvedPath, maxBytes, options.rejectHardlinks !== false);
|
|
46
35
|
let handle;
|
|
36
|
+
let raw;
|
|
47
37
|
try {
|
|
48
38
|
const realPath = await fs.realpath(resolvedPath);
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
39
|
+
handle = await fs.open(realPath, resolveReadOpenFlags());
|
|
40
|
+
const openedHandle = handle;
|
|
41
|
+
const openedStat = await inspectFileIdentity(async () => {
|
|
42
|
+
const stat = await openedHandle.stat({ bigint: true });
|
|
43
|
+
if (!stat.isFile() || (options.rejectHardlinks !== false && stat.nlink > 1n)) {
|
|
44
|
+
throw new FsSafeError("path-mismatch", "security validation failed");
|
|
45
|
+
}
|
|
46
|
+
return stat;
|
|
47
|
+
}, previewStat);
|
|
48
|
+
await inspectFileIdentity(async () => {
|
|
49
|
+
const stat = await fs.lstat(realPath, { bigint: true });
|
|
50
|
+
if (!stat.isFile())
|
|
51
|
+
throw new FsSafeError("path-mismatch", "security validation failed");
|
|
52
|
+
return stat;
|
|
53
|
+
}, openedStat);
|
|
54
|
+
await inspectFileIdentity(() => inspectInput("secret path became a symlink"), openedStat);
|
|
55
|
+
raw = (await readFileHandleBounded(handle, maxBytes)).toString("utf8");
|
|
66
56
|
}
|
|
67
57
|
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
|
-
};
|
|
58
|
+
throw secretReadError(error instanceof FsSafeError
|
|
59
|
+
? error.code
|
|
60
|
+
: secretPathErrorCode(error) === "not-found" ? "not-found" : "read-failed", "read", label, resolvedPath, error);
|
|
79
61
|
}
|
|
80
62
|
finally {
|
|
81
63
|
await handle?.close().catch(() => undefined);
|
|
82
64
|
}
|
|
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 });
|
|
65
|
+
return trimSecretFileContent(raw, label, resolvedPath);
|
|
89
66
|
}
|
|
90
67
|
export async function tryReadSecretFile(filePath, label, options = {}) {
|
|
91
68
|
if (!filePath?.trim())
|
|
92
69
|
return undefined;
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
70
|
+
try {
|
|
71
|
+
return await readSecretFile(filePath, label, options);
|
|
72
|
+
}
|
|
73
|
+
catch (error) {
|
|
74
|
+
if (error instanceof FsSafeError && error.code === "not-found")
|
|
75
|
+
return undefined;
|
|
76
|
+
throw error;
|
|
77
|
+
}
|
|
99
78
|
}
|
|
@@ -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;
|
|
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;AAarC,OAAO,EAOL,KAAK,eAAe,EACpB,KAAK,sBAAsB,EAC5B,MAAM,kBAAkB,CAAC;AAG1B,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;AA4LF,wBAAsB,cAAc,CAClC,OAAO,EAAE,qBAAqB,GAC7B,OAAO,CAAC,oBAAoB,CAAC,CAe/B"}
|