| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255 |
- 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;
- const ALLOWED_SEARCH_PROVIDERS = new Set(["fs", "qsirch"]);
- const ALLOWED_QSIRCH_DATE_FIELDS = new Set(["modified", "created"]);
- const ALLOWED_QSIRCH_MODES = new Set(["sync", "async", "auto"]);
- 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);
- if (p.length > 1) p = p.replace(/\/+$/, "");
- return p;
- }
- function containsDotDotSegment(p) {
- 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;
- }
- function isValidHttpUrl(value) {
- try {
- const u = new URL(value);
- return u.protocol === "http:" || u.protocol === "https:";
- } catch {
- return false;
- }
- }
- 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"',
- });
- }
- const cookieSecureRaw = !isBlank(e.SESSION_COOKIE_SECURE)
- ? normalizeString(e.SESSION_COOKIE_SECURE).toLowerCase()
- : "";
- if (
- cookieSecureRaw &&
- cookieSecureRaw !== "true" &&
- cookieSecureRaw !== "false"
- ) {
- invalid.push({
- key: "SESSION_COOKIE_SECURE",
- message: 'must be "true" or "false" if provided',
- });
- }
- // -------------------------------------------------------------------------
- // Search provider configuration (RHL-016)
- // -------------------------------------------------------------------------
- const searchProvider = !isBlank(e.SEARCH_PROVIDER)
- ? normalizeString(e.SEARCH_PROVIDER).toLowerCase()
- : "fs";
- if (!ALLOWED_SEARCH_PROVIDERS.has(searchProvider)) {
- invalid.push({
- key: "SEARCH_PROVIDER",
- message: 'must be one of "fs" or "qsirch"',
- });
- }
- if (searchProvider === "qsirch") {
- if (isBlank(e.QSIRCH_BASE_URL)) {
- missing.push("QSIRCH_BASE_URL");
- } else if (!isValidHttpUrl(normalizeString(e.QSIRCH_BASE_URL))) {
- invalid.push({
- key: "QSIRCH_BASE_URL",
- message:
- 'must be a valid http(s) URL (e.g. "http://192.168.0.22:8080")',
- });
- }
- if (isBlank(e.QSIRCH_ACCOUNT)) missing.push("QSIRCH_ACCOUNT");
- if (isBlank(e.QSIRCH_PASSWORD)) missing.push("QSIRCH_PASSWORD");
- if (isBlank(e.QSIRCH_PATH_PREFIX)) missing.push("QSIRCH_PATH_PREFIX");
- const dateField = !isBlank(e.QSIRCH_DATE_FIELD)
- ? normalizeString(e.QSIRCH_DATE_FIELD).toLowerCase()
- : "modified";
- if (!ALLOWED_QSIRCH_DATE_FIELDS.has(dateField)) {
- invalid.push({
- key: "QSIRCH_DATE_FIELD",
- message: 'must be "modified" or "created"',
- });
- }
- const mode = !isBlank(e.QSIRCH_MODE)
- ? normalizeString(e.QSIRCH_MODE).toLowerCase()
- : "sync";
- if (!ALLOWED_QSIRCH_MODES.has(mode)) {
- invalid.push({
- key: "QSIRCH_MODE",
- message: 'must be "sync", "async", or "auto"',
- });
- }
- }
- 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);
- }
- const cfg = {
- mongodbUri,
- sessionSecret,
- nasRootPath,
- nodeEnv: nodeEnvRaw,
- searchProvider,
- };
- if (port !== undefined) cfg.port = port;
- return cfg;
- }
|