import { format } from "node:util";
const REDACT_KEYS = ["password", "pass", "token", "secret", "authorization", "cookie", "key"];
const MAX_JSON_CHARS = 2000;
/**
* Return true when input contains CR or LF characters.
*/
export function containsCarriageReturnOrLineFeed(value: string): boolean {
return value.includes("\r") || value.includes("\n");
}
/**
* Truncate a string to a maximum length with an ellipsis suffix.
*/
export function truncateString(value: string, maxLength: number): string {
if (value.length <= maxLength) {
return value;
}
return `${value.slice(0, Math.max(0, maxLength - 1))}…`;
}
/**
* Normalize a CSV environment variable into a lowercase string array.
*/
export function parseCsvList(value: string | undefined): string[] {
if (!value) {
return [];
}
return value
.split(",")
.map((item) => item.trim().toLowerCase())
.filter((item) => item.length > 0);
}
/**
* Scrub secret-like keys in objects and arrays for logging.
*/
export function redactSecrets(value: unknown): unknown {
if (Array.isArray(value)) {
return value.map((item) => redactSecrets(item));
}
if (value && typeof value === "object") {
const entries = Object.entries(value as Record<string, unknown>);
return Object.fromEntries(
entries.map(([key, entryValue]) => {
const lowerKey = key.toLowerCase();
const shouldRedact = REDACT_KEYS.some((redactKey) => lowerKey.includes(redactKey));
if (shouldRedact) {
return [key, "[REDACTED]"];
}
return [key, redactSecrets(entryValue)];
}),
);
}
return value;
}
/**
* Safely format strings for logs using util.format semantics.
*/
export function formatMessage(message: string, args: unknown[]): string {
return format(message, ...args);
}
/**
* Convert arbitrary data to a compact JSON string.
*/
export function safeJsonStringify(value: unknown): string {
try {
const json = JSON.stringify(value);
return truncateString(json, MAX_JSON_CHARS);
} catch {
return "[unserializable]";
}
}
/**
* Validate filename inputs to avoid path traversal.
*/
export function isSafeFilename(filename: string): boolean {
if (filename.length === 0 || filename.length > 256) {
return false;
}
if (filename.includes("/") || filename.includes("\\") || filename.includes("..")) {
return false;
}
return true;
}