Files
Cibello-app/apps/api/src/config.ts
T

126 lines
5.2 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { config as loadDotenv } from "dotenv";
import { existsSync } from "node:fs";
import path from "node:path";
// Ladda .env från paketet ELLER monorepo-roten (pnpm --filter sätter cwd till paketet).
for (const candidate of [".env", "../.env", "../../.env"]) {
const p = path.resolve(process.cwd(), candidate);
if (existsSync(p)) {
loadDotenv({ path: p });
break;
}
}
import { z } from "zod";
import { BRAND } from "@app/shared-types";
/** All konfiguration valideras vid start tydliga fel i stället för mystiska krascher. */
const configSchema = z.object({
NODE_ENV: z.enum(["development", "test", "production"]).default("development"),
API_PORT: z.coerce.number().int().default(4000),
API_BASE_URL: z.string().default("http://localhost:4000"),
CORS_ORIGINS: z.string().default("http://localhost:5173"),
LOG_LEVEL: z.enum(["fatal", "error", "warn", "info", "debug", "trace"]).default("info"),
DATABASE_URL: z.string().default("postgres://app_user:app_dev_password@localhost:5432/app"),
REDIS_URL: z.string().default("redis://localhost:6379"),
JWT_ACCESS_SECRET: z.string().min(10).default("dev-only-change-me"),
JWT_REFRESH_SECRET: z.string().min(10).default("dev-only-change-me-too"),
JWT_ACCESS_TTL_SECONDS: z.coerce.number().int().default(900),
JWT_REFRESH_TTL_SECONDS: z.coerce.number().int().default(2_592_000),
ENTITLEMENT_SIGNING_SECRET: z.string().min(10).default("dev-only-change-me-three"),
ENTITLEMENT_TTL_HOURS: z.coerce.number().int().default(24),
ENTITLEMENT_GRACE_DAYS: z.coerce.number().int().default(7),
S3_MODE: z.enum(["mock", "aws"]).default("mock"),
S3_BUCKET: z.string().default(""),
S3_REGION: z.string().default("eu-north-1"),
/** Tom = AWS S3; sätt för S3-kompatibel lagring (MinIO/R2/Hetzner). */
S3_ENDPOINT: z.string().default(""),
/** Tomma = IAM-roll/instansprofil används (rekommenderat i AWS). */
S3_ACCESS_KEY_ID: z.string().default(""),
S3_SECRET_ACCESS_KEY: z.string().default(""),
AAMOS_MODE: z.enum(["http", "mock", "gemini"]).default("http"),
AAMOS_API_URL: z.string().optional(),
AAMOS_API_KEY: z.string().optional(),
AAMOS_TIMEOUT_MS: z.coerce.number().int().default(60_000),
GEMINI_API_KEY: z.string().optional(),
GEMINI_MODEL: z.string().default("gemini-2.5-flash"),
GEMINI_TIMEOUT_MS: z.coerce.number().int().default(60_000),
GEMINI_DAILY_BUDGET_USD: z.coerce.number().default(0),
/** Strong, rotated ops token for /ops/v1/summary. Set via SSM in production. */
OPS_TOKEN: z.string().min(1).default("dev-ops-token-change-me"),
/** EOC base URL for outbound ops-summary push. Empty = push disabled. */
EOC_URL: z.string().url().optional(),
/** EOC ingest bearer token for this app. Set via SSM in production. */
EOC_PUSH_TOKEN: z.string().min(1).optional(),
APP_STORE_MODE: z.enum(["production", "sandbox"]).default("sandbox"),
EMAIL_MODE: z.enum(["log", "smtp"]).default("log"),
SMTP_HOST: z.string().default(""),
SMTP_PORT: z.coerce.number().int().default(587),
SMTP_SECURE: z
.enum(["true", "false"])
.default("false")
.transform((v) => v === "true"),
SMTP_USER: z.string().default(""),
SMTP_PASS: z.string().default(""),
MAIL_FROM: z.string().default(""),
/** Firebase Admin service-account (hela JSON:en base64-kodad). Tom = Firebase-session av. Lokalt via .env, prod via SSM. */
FIREBASE_SERVICE_ACCOUNT_B64: z.string().optional(),
APPLE_BUNDLE_ID: z.string().default(""),
GOOGLE_PACKAGE_NAME: z.string().default(""),
});
export type AppConfig = z.infer<typeof configSchema>;
export function loadConfig(env: NodeJS.ProcessEnv = process.env): AppConfig {
const parsed = configSchema.safeParse(env);
if (!parsed.success) {
console.error("Ogiltig konfiguration:", parsed.error.flatten().fieldErrors);
process.exit(1);
}
const cfg = parsed.data;
// Varumärkesberoende defaults ur brand.config.json (i18n-spec §28)
if (!cfg.S3_BUCKET) cfg.S3_BUCKET = `${BRAND.slug}-production`;
if (!cfg.APPLE_BUNDLE_ID) cfg.APPLE_BUNDLE_ID = BRAND.iosBundleId;
if (!cfg.GOOGLE_PACKAGE_NAME) cfg.GOOGLE_PACKAGE_NAME = BRAND.androidPackage;
if (cfg.NODE_ENV === "production") {
const insecure = [
cfg.JWT_ACCESS_SECRET,
cfg.JWT_REFRESH_SECRET,
cfg.ENTITLEMENT_SIGNING_SECRET,
].some((s) => s.startsWith("dev-only"));
if (insecure) {
console.error("SÄKERHETSSTOPP: dev-hemligheter i produktion (spec §56).");
process.exit(1);
}
if (cfg.AAMOS_MODE === "mock") {
console.error("SÄKERHETSSTOPP: AAMOS_MODE=mock är inte tillåtet i produktion.");
process.exit(1);
}
if (cfg.AAMOS_MODE === "gemini" && !cfg.GEMINI_API_KEY) {
console.error("SÄKERHETSSTOPP: AAMOS_MODE=gemini kräver GEMINI_API_KEY i produktion.");
process.exit(1);
}
if (cfg.AAMOS_MODE === "http" && (!cfg.AAMOS_API_URL || !cfg.AAMOS_API_KEY)) {
console.error(
"SÄKERHETSSTOPP: AAMOS_MODE=http kräver AAMOS_API_URL och AAMOS_API_KEY i produktion.",
);
process.exit(1);
}
if (!cfg.OPS_TOKEN || cfg.OPS_TOKEN.length < 32 || cfg.OPS_TOKEN.startsWith("dev-ops-token")) {
console.error(
"SÄKERHETSSTOPP: OPS_TOKEN måste vara en stark, icke-default hemlighet i produktion.",
);
process.exit(1);
}
}
return cfg;
}