| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190 |
- export const REQUIRED_ENV_VARS = [
- "MONGODB_URI",
- "SESSION_SECRET",
- "NAS_ROOT_PATH",
- ];
- export const ALLOWED_NODE_ENVS = new Set(["development", "test", "production"]);
- export const MIN_SESSION_SECRET_LENGTH = 32;
- function isBlank(value) {
- return value === undefined || value === null || String(value).trim() === "";
- }
- function normalizeString(value) {
- return String(value).trim();
- }
- function normalizeUnixPath(value) {
- let p = normalizeString(value);
- // Remove trailing slashes (but keep "/" as-is)
- if (p.length > 1) p = p.replace(/\/+$/, "");
- return p;
- }
- function containsDotDotSegment(p) {
- // Reject "/../", "/..", "../", etc.
- return /(^|\/)\.\.(\/|$)/.test(p);
- }
- function looksLikePlaceholderSecret(value) {
- const s = normalizeString(value).toLowerCase();
- return (
- s.includes("change-me") ||
- s.includes("changeme") ||
- s.includes("replace-me") ||
- s.includes("replace_this") ||
- s === "secret" ||
- s === "password"
- );
- }
- function validateMongoUri(uri) {
- return uri.startsWith("mongodb://") || uri.startsWith("mongodb+srv://");
- }
- function parsePort(value) {
- const raw = normalizeString(value);
- if (!/^\d+$/.test(raw)) return { ok: false, value: null };
- const n = Number(raw);
- if (!Number.isInteger(n) || n < 1 || n > 65535)
- return { ok: false, value: null };
- return { ok: true, value: n };
- }
- function buildEnvError(missing, invalid) {
- const lines = [];
- lines.push("Invalid environment configuration.");
- if (missing.length > 0) {
- lines.push("");
- lines.push("Missing required environment variables:");
- for (const key of missing) lines.push(`- ${key}`);
- }
- if (invalid.length > 0) {
- lines.push("");
- lines.push("Invalid environment variables:");
- for (const item of invalid) lines.push(`- ${item.key}: ${item.message}`);
- }
- lines.push("");
- lines.push(
- "Tip: Copy and adjust the example env files (.env.local.example / .env.docker.example)."
- );
- const err = new Error(lines.join("\n"));
- err.code = "ENV_INVALID";
- err.missing = missing;
- err.invalid = invalid;
- return err;
- }
- /**
- * Validates and normalizes environment variables.
- * This function is pure: pass in an env object (e.g. process.env) and it returns config or throws.
- *
- * @param {Record<string, any>} env
- * @returns {{
- * mongodbUri: string,
- * sessionSecret: string,
- * nasRootPath: string,
- * nodeEnv: "development" | "test" | "production",
- * port?: number
- * }}
- */
- export function validateEnv(env) {
- const e = env ?? {};
- const missing = [];
- const invalid = [];
- for (const key of REQUIRED_ENV_VARS) {
- if (isBlank(e[key])) missing.push(key);
- }
- const mongodbUri = !isBlank(e.MONGODB_URI)
- ? normalizeString(e.MONGODB_URI)
- : "";
- if (mongodbUri && !validateMongoUri(mongodbUri)) {
- invalid.push({
- key: "MONGODB_URI",
- message: 'must start with "mongodb://" or "mongodb+srv://"',
- });
- }
- const sessionSecret = !isBlank(e.SESSION_SECRET)
- ? normalizeString(e.SESSION_SECRET)
- : "";
- if (sessionSecret) {
- if (sessionSecret.length < MIN_SESSION_SECRET_LENGTH) {
- invalid.push({
- key: "SESSION_SECRET",
- message: `must be at least ${MIN_SESSION_SECRET_LENGTH} characters long`,
- });
- }
- if (looksLikePlaceholderSecret(sessionSecret)) {
- invalid.push({
- key: "SESSION_SECRET",
- message:
- "looks like a placeholder (replace it with a strong random secret)",
- });
- }
- }
- const nasRootPath = !isBlank(e.NAS_ROOT_PATH)
- ? normalizeUnixPath(e.NAS_ROOT_PATH)
- : "";
- if (nasRootPath) {
- if (!nasRootPath.startsWith("/")) {
- invalid.push({
- key: "NAS_ROOT_PATH",
- message: 'must be an absolute Unix path (starts with "/")',
- });
- }
- if (containsDotDotSegment(nasRootPath)) {
- invalid.push({
- key: "NAS_ROOT_PATH",
- message: 'must not contain ".." path segments',
- });
- }
- }
- const nodeEnvRaw = !isBlank(e.NODE_ENV)
- ? normalizeString(e.NODE_ENV)
- : "development";
- if (nodeEnvRaw && !ALLOWED_NODE_ENVS.has(nodeEnvRaw)) {
- invalid.push({
- key: "NODE_ENV",
- message: 'must be one of "development", "test", "production"',
- });
- }
- let port;
- if (!isBlank(e.PORT)) {
- const parsed = parsePort(e.PORT);
- if (!parsed.ok) {
- invalid.push({
- key: "PORT",
- message: "must be an integer between 1 and 65535",
- });
- } else {
- port = parsed.value;
- }
- }
- if (missing.length > 0 || invalid.length > 0) {
- throw buildEnvError(missing, invalid);
- }
- /** @type {any} */
- const cfg = {
- mongodbUri,
- sessionSecret,
- nasRootPath,
- nodeEnv: nodeEnvRaw,
- };
- if (port !== undefined) cfg.port = port;
- return cfg;
- }
|