050c958285
- Gemini-adapter bakom AamosClient-interface (AAMOS_MODE=gemini) - Serversida/worker: hämtar bild, anropar Gemini, mappar mot canonical_ingredients - Kostnad/tokens bokförs i ai_usage_counters; global dagsbudget via BudgetStore - Redis-backed budget i worker, in-memory i tester - Migration 0019: ai_cost_usd_microcents - Hermetiska tester med inspelad fixture; separat pnpm eval:scan - docs/09 uppdaterad ärligt: AAMOS-status, Gemini-flöde, säkerhet/kostnad - REQUIRE_REAL=1 stödjer AAMOS_MODE=gemini; deploy-grind uppdaterad
104 lines
4.2 KiB
TypeScript
104 lines
4.2 KiB
TypeScript
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),
|
||
|
||
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(""),
|
||
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);
|
||
}
|
||
}
|
||
return cfg;
|
||
}
|