2026-04-26 02:42:42 -04:00
import path from 'node:path' ;
import { fileURLToPath } from 'node:url' ;
import dotenv from 'dotenv' ;
import { z } from 'zod' ;
const __dirname = path . dirname ( fileURLToPath ( import . meta . url ));
2026-07-17 13:34:33 -04:00
// Load .env from the monorepo root regardless of cwd — but NEVER under test. The test suites
// inject their own env explicitly; loading a developer's/CI's real .env here makes tests
// non-hermetic (e.g. a real TURNSTILE_SECRET_KEY would switch on CAPTCHA and break auth tests).
if ( process . env . NODE_ENV !== 'test' ) {
dotenv . config ({ path : path.resolve ( __dirname , '../../../.env' ) });
}
2026-04-26 02:42:42 -04:00
const envSchema = z . object ({
NODE_ENV : z.enum ([ 'development' , 'production' , 'test' ]). default ( 'development' ),
PORT : z.coerce.number (). int (). positive (). default ( 8080 ),
PUBLIC_URL : z.string (). url (). default ( 'http://localhost:8080' ),
COOKIE_DOMAIN : z.string (). optional (),
SESSION_SECRET : z.string (). min ( 32 ),
CSRF_SECRET : z.string (). min ( 32 ),
DATABASE_URL : z.string (). min ( 1 ),
DATABASE_CA_CERT_PATH : z.string (). optional (),
WEB_DIST_PATH : z.string (). optional (),
SUPERADMIN_EMAILS : z.string (). optional (). default ( '' ),
SENTRY_DSN_API : z.string (). optional (). default ( '' ),
2026-07-16 13:18:10 -04:00
// Object storage — DigitalOcean Spaces (S3-compatible), the platform's sole storage backend.
// Required: the API must not boot into a state where uploads silently have nowhere to go.
SPACES_ENDPOINT : z.string (). url (),
SPACES_REGION : z.string (). min ( 1 ),
SPACES_BUCKET : z.string (). min ( 1 ),
SPACES_KEY : z.string (). min ( 1 ),
SPACES_SECRET : z.string (). min ( 1 ),
SPACES_PUBLIC_BASE : z.string (). optional (). default ( '' ),
// Legacy local path — read only by the one-time migration script, not the running app.
STORAGE_PATH : z.string (). optional (). default ( './storage' ),
SMTP2GO_API_KEY : z.string (). optional (). default ( '' ),
2026-07-17 13:04:18 -04:00
ANTHROPIC_API_KEY : z.string (). optional (). default ( '' ),
TURNSTILE_SECRET_KEY : z.string (). optional (). default ( '' ),
2026-04-26 02:42:42 -04:00
EMAIL_FROM : z.string (). optional (). default ( 'eLegal Software <noreply@elegalsoftware.com>' ),
STRIPE_SECRET_KEY : z.string (). optional (). default ( '' ),
STRIPE_WEBHOOK_SECRET : z.string (). optional (). default ( '' ),
STRIPE_PRICE_PRO : z.string (). optional (). default ( '' ),
STRIPE_PRICE_LIFETIME : z.string (). optional (). default ( '' ),
});
const parsed = envSchema . parse ( process . env );
2026-07-17 13:34:33 -04:00
// Production guard: Turnstile bot protection fails open when TURNSTILE_SECRET_KEY is unset
// (correct for dev/test, but in production a forgotten key silently disables all CAPTCHA/bot
// protection on signup/login/password-reset/contact). Fail fast at boot rather than run exposed.
if ( parsed . NODE_ENV === 'production' && ! parsed . TURNSTILE_SECRET_KEY ) {
throw new Error (
'TURNSTILE_SECRET_KEY is required in production: without it, bot protection fails open and CAPTCHA verification is skipped entirely. Set TURNSTILE_SECRET_KEY in the environment.' ,
);
}
2026-04-26 02:42:42 -04:00
export const env = {
... parsed ,
superadminEmails : parsed.SUPERADMIN_EMAILS.split ( ',' )
. map (( s ) => s . trim (). toLowerCase ())
. filter ( Boolean ),
};
export const isProd = env . NODE_ENV === 'production' ;