/** * Environment variable validation and defaults for ATProto OAuth. * Allows missing values in dev mode (defaults kick in). */ const DEV_COOKIE_SECRET = "development-secret-at-least-32-chars!!"; let _cookieSecretWarned = false; export const env = { /** iron-session encryption key (32+ chars) */ get COOKIE_SECRET(): string { if (process.env.COOKIE_SECRET) return process.env.COOKIE_SECRET; // Warn loudly in production — the default secret is published in source code if (process.env.NODE_ENV === "production" && !_cookieSecretWarned) { _cookieSecretWarned = true; console.warn( "\n⚠️ WARNING: COOKIE_SECRET is not set. Using an insecure default.\n" + " ATProto session cookies can be forged by anyone who reads the source code.\n" + " Set COOKIE_SECRET to a random 32+ character string for production use.\n" ); } return DEV_COOKIE_SECRET; }, /** App's public URL (empty = localhost dev mode with public OAuth client) */ get PUBLIC_URL(): string { return process.env.PUBLIC_URL || ""; }, /** Dev server port */ get PORT(): number { return parseInt(process.env.PORT || "3000", 10); }, /** ES256 JWK private key JSON (empty = public client mode) */ get ATPROTO_JWK_PRIVATE(): string { return process.env.ATPROTO_JWK_PRIVATE || ""; }, /** Dashboard password (empty = no auth, open access) */ get HEARTBEADS_PASSWORD(): string { return process.env.HEARTBEADS_PASSWORD || ""; }, /** Hypergoat GraphQL indexer URL for ATProto comments/likes */ get INDEXER_URL(): string { return ( process.env.INDEXER_URL || "https://hypergoat-app-production.up.railway.app/graphql" ); }, };