openclaw/src/infra/secret-file.ts
2026-03-10 23:40:10 +00:00

134 lines
3.2 KiB
TypeScript

import fs from "node:fs";
import { resolveUserPath } from "../utils.js";
import { openVerifiedFileSync } from "./safe-open-sync.js";
export const DEFAULT_SECRET_FILE_MAX_BYTES = 16 * 1024;
export type SecretFileReadOptions = {
maxBytes?: number;
rejectSymlink?: boolean;
};
export type SecretFileReadResult =
| {
ok: true;
secret: string;
resolvedPath: string;
}
| {
ok: false;
message: string;
resolvedPath?: string;
error?: unknown;
};
export function loadSecretFileSync(
filePath: string,
label: string,
options: SecretFileReadOptions = {},
): SecretFileReadResult {
const trimmedPath = filePath.trim();
const resolvedPath = resolveUserPath(trimmedPath);
if (!resolvedPath) {
return { ok: false, message: `${label} file path is empty.` };
}
const maxBytes = options.maxBytes ?? DEFAULT_SECRET_FILE_MAX_BYTES;
let previewStat: fs.Stats;
try {
previewStat = fs.lstatSync(resolvedPath);
} catch (error) {
return {
ok: false,
resolvedPath,
error,
message: `Failed to inspect ${label} file at ${resolvedPath}: ${String(error)}`,
};
}
if (options.rejectSymlink && previewStat.isSymbolicLink()) {
return {
ok: false,
resolvedPath,
message: `${label} file at ${resolvedPath} must not be a symlink.`,
};
}
if (!previewStat.isFile()) {
return {
ok: false,
resolvedPath,
message: `${label} file at ${resolvedPath} must be a regular file.`,
};
}
if (previewStat.size > maxBytes) {
return {
ok: false,
resolvedPath,
message: `${label} file at ${resolvedPath} exceeds ${maxBytes} bytes.`,
};
}
const opened = openVerifiedFileSync({
filePath: resolvedPath,
rejectPathSymlink: options.rejectSymlink,
maxBytes,
});
if (!opened.ok) {
const error =
opened.reason === "validation" ? new Error("security validation failed") : opened.error;
return {
ok: false,
resolvedPath,
error,
message: `Failed to read ${label} file at ${resolvedPath}: ${String(error)}`,
};
}
try {
const raw = fs.readFileSync(opened.fd, "utf8");
const secret = raw.trim();
if (!secret) {
return {
ok: false,
resolvedPath,
message: `${label} file at ${resolvedPath} is empty.`,
};
}
return { ok: true, secret, resolvedPath };
} catch (error) {
return {
ok: false,
resolvedPath,
error,
message: `Failed to read ${label} file at ${resolvedPath}: ${String(error)}`,
};
} finally {
fs.closeSync(opened.fd);
}
}
export function readSecretFileSync(
filePath: string,
label: string,
options: SecretFileReadOptions = {},
): string {
const result = loadSecretFileSync(filePath, label, options);
if (result.ok) {
return result.secret;
}
throw new Error(result.message, result.error ? { cause: result.error } : undefined);
}
export function tryReadSecretFileSync(
filePath: string | undefined,
label: string,
options: SecretFileReadOptions = {},
): string | undefined {
if (!filePath?.trim()) {
return undefined;
}
const result = loadSecretFileSync(filePath, label, options);
return result.ok ? result.secret : undefined;
}