xtablo-source/apps/api/src/config.ts
2026-05-06 23:44:02 +02:00

204 lines
7.5 KiB
TypeScript

import dotenv from "dotenv";
import type { Secrets } from "./secrets.js";
export interface AppConfig {
NODE_ENV: "development" | "production" | "test" | "staging";
PORT: number;
SUPABASE_URL: string;
SUPABASE_SERVICE_ROLE_KEY: string;
SUPABASE_CONNECTION_STRING: string;
SUPABASE_CA_CERT: string;
STRIPE_SECRET_KEY: string;
STRIPE_WEBHOOK_SECRET: string;
STRIPE_SOLO_PRICE_ID: string;
STRIPE_TEAM_PRICE_ID: string;
STRIPE_FOUNDER_PRICE_ID: string;
REVENUECAT_WEBHOOK_AUTH_HEADER: string;
REVENUECAT_SOLO_PRODUCT_ID: string;
REVENUECAT_ANNUAL_PRODUCT_ID: string;
EMAIL_USER: string;
EMAIL_CLIENT_ID: string;
EMAIL_CLIENT_SECRET: string;
EMAIL_REFRESH_TOKEN: string;
API_BASE_URL: string;
XTABLO_URL: string;
R2_ACCOUNT_ID: string;
R2_ACCESS_KEY_ID: string;
R2_SECRET_ACCESS_KEY: string;
LOG_LEVEL: "debug" | "info" | "warn" | "error";
TASKS_SECRET: string;
ADMIN_TOKEN_SIGNING_SECRET: string;
ADMIN_TOKEN_AUDIENCE: string;
ADMIN_APP_URL: string;
CLIENT_AUTH_JWT_SECRET: string;
CLIENT_AUTH_COOKIE_NAME: string;
CLIENT_AUTH_COOKIE_DOMAIN: string;
CLIENT_MAGIC_LINK_TTL_MINUTES: number;
CLIENT_SESSION_TTL_DAYS: number;
CLIENTS_URL: string;
/**
* Test user
*/
TEST_USER_DATA: {
id: string;
email: string;
user_metadata: Record<string, unknown>;
app_metadata: Record<string, unknown>;
aud: string;
created_at: string;
};
}
function validateEnvVar(name: string, value: string | undefined): string {
if (!value) {
throw new Error(`Missing required environment variable: ${name}`);
}
return value;
}
function trimTrailingSlash(value: string) {
return value.replace(/\/+$/, "");
}
function resolveApiBaseUrl(input: {
apiBaseUrl?: string;
nodeEnv: AppConfig["NODE_ENV"];
port: number;
xtabloUrl: string;
}) {
if (input.apiBaseUrl) {
return input.apiBaseUrl;
}
if (input.nodeEnv === "development" || input.nodeEnv === "test") {
return `http://localhost:${input.port}/api/v1`;
}
const xtabloUrl = trimTrailingSlash(input.xtabloUrl);
if (xtabloUrl === "https://app.xtablo.com") {
return "https://api.xtablo.com/api/v1";
}
if (xtabloUrl === "https://app-staging.xtablo.com") {
return "https://api-staging.xtablo.com/api/v1";
}
return `${xtabloUrl}/api/v1`;
}
export function createConfig(secrets?: Secrets): AppConfig {
const NODE_ENV = (process.env.NODE_ENV || "development") as
| "development"
| "production"
| "staging"
| "test";
dotenv.config({ path: `.env.${NODE_ENV}` });
// In test mode, use environment variables directly instead of secrets
const isTestMode = NODE_ENV === "test";
const isStagingMode = NODE_ENV === "staging";
const getStripeSecretKey = (isStagingMode: boolean) =>
isStagingMode ? secrets!.stripeSecretKeyStaging : secrets!.stripeSecretKey;
const getStripeWebhookSecret = (isStagingMode: boolean) =>
isStagingMode ? secrets!.stripeWebhookSecretStaging : secrets!.stripeWebhookSecret;
const getStripeSecretKeyFromEnv = () => process.env.STRIPE_SECRET_KEY;
const getStripeWebhookSecretFromEnv = () => process.env.STRIPE_WEBHOOK_SECRET;
const XTABLO_URL = process.env.XTABLO_URL || "https://app.xtablo.com";
const PORT = parseInt(process.env.PORT || "8080", 10);
// Base configuration
const baseConfig: AppConfig = {
NODE_ENV,
PORT,
SUPABASE_URL: validateEnvVar("SUPABASE_URL", process.env.SUPABASE_URL),
SUPABASE_SERVICE_ROLE_KEY: isTestMode
? validateEnvVar("SUPABASE_SERVICE_ROLE_KEY", process.env.SUPABASE_SERVICE_ROLE_KEY)
: secrets!.supabaseServiceRoleKey,
SUPABASE_CONNECTION_STRING: isTestMode
? validateEnvVar("SUPABASE_CONNECTION_STRING", process.env.SUPABASE_CONNECTION_STRING)
: secrets!.supabaseConnectionString,
SUPABASE_CA_CERT: isTestMode
? validateEnvVar("SUPABASE_CA_CERT", process.env.SUPABASE_CA_CERT)
: secrets!.supabaseCaCert,
STRIPE_SECRET_KEY: isTestMode
? validateEnvVar("STRIPE_SECRET_KEY", process.env.STRIPE_SECRET_KEY)
: getStripeSecretKeyFromEnv() || getStripeSecretKey(isStagingMode),
STRIPE_WEBHOOK_SECRET: isTestMode
? validateEnvVar("STRIPE_WEBHOOK_SECRET", process.env.STRIPE_WEBHOOK_SECRET)
: getStripeWebhookSecretFromEnv() || getStripeWebhookSecret(isStagingMode),
STRIPE_SOLO_PRICE_ID: validateEnvVar("STRIPE_SOLO_PRICE_ID", process.env.STRIPE_SOLO_PRICE_ID),
STRIPE_TEAM_PRICE_ID: validateEnvVar("STRIPE_TEAM_PRICE_ID", process.env.STRIPE_TEAM_PRICE_ID),
STRIPE_FOUNDER_PRICE_ID: validateEnvVar(
"STRIPE_FOUNDER_PRICE_ID",
process.env.STRIPE_FOUNDER_PRICE_ID
),
REVENUECAT_WEBHOOK_AUTH_HEADER: process.env.REVENUECAT_WEBHOOK_AUTH_HEADER || "",
REVENUECAT_SOLO_PRODUCT_ID: process.env.REVENUECAT_SOLO_PRODUCT_ID || "",
REVENUECAT_ANNUAL_PRODUCT_ID: process.env.REVENUECAT_ANNUAL_PRODUCT_ID || "",
EMAIL_USER: validateEnvVar("EMAIL_USER", process.env.EMAIL_USER),
EMAIL_CLIENT_ID: validateEnvVar("EMAIL_CLIENT_ID", process.env.EMAIL_CLIENT_ID),
EMAIL_CLIENT_SECRET: isTestMode
? validateEnvVar("EMAIL_CLIENT_SECRET", process.env.EMAIL_CLIENT_SECRET)
: secrets!.emailClientSecret,
EMAIL_REFRESH_TOKEN: isTestMode
? validateEnvVar("EMAIL_REFRESH_TOKEN", process.env.EMAIL_REFRESH_TOKEN)
: secrets!.emailRefreshToken,
API_BASE_URL: resolveApiBaseUrl({
apiBaseUrl: process.env.API_BASE_URL,
nodeEnv: NODE_ENV,
port: PORT,
xtabloUrl: XTABLO_URL,
}),
XTABLO_URL,
R2_ACCOUNT_ID: validateEnvVar("R2_ACCOUNT_ID", process.env.R2_ACCOUNT_ID),
R2_ACCESS_KEY_ID: isTestMode
? validateEnvVar("R2_ACCESS_KEY_ID", process.env.R2_ACCESS_KEY_ID)
: secrets!.r2AccessKeyId,
R2_SECRET_ACCESS_KEY: isTestMode
? validateEnvVar("R2_SECRET_ACCESS_KEY", process.env.R2_SECRET_ACCESS_KEY)
: secrets!.r2SecretAccessKey,
TASKS_SECRET: process.env.TASKS_SECRET || "",
ADMIN_TOKEN_SIGNING_SECRET: isTestMode
? validateEnvVar("ADMIN_TOKEN_SIGNING_SECRET", process.env.ADMIN_TOKEN_SIGNING_SECRET)
: secrets!.adminTokenSigningSecret,
ADMIN_TOKEN_AUDIENCE: process.env.ADMIN_TOKEN_AUDIENCE || "xtablo-admin",
ADMIN_APP_URL: process.env.ADMIN_APP_URL || "http://localhost:5176",
CLIENT_AUTH_JWT_SECRET: isTestMode
? process.env.CLIENT_AUTH_JWT_SECRET || "client-auth-local-secret"
: process.env.CLIENT_AUTH_JWT_SECRET ||
secrets!.clientAuthJwtSecret,
CLIENT_AUTH_COOKIE_NAME: process.env.CLIENT_AUTH_COOKIE_NAME || "xtablo_client_session",
CLIENT_AUTH_COOKIE_DOMAIN: process.env.CLIENT_AUTH_COOKIE_DOMAIN || "clients.xtablo.com",
CLIENT_MAGIC_LINK_TTL_MINUTES: parseInt(process.env.CLIENT_MAGIC_LINK_TTL_MINUTES || "30", 10),
CLIENT_SESSION_TTL_DAYS: parseInt(process.env.CLIENT_SESSION_TTL_DAYS || "7", 10),
CLIENTS_URL: process.env.CLIENTS_URL || "https://clients.xtablo.com",
LOG_LEVEL: "info",
TEST_USER_DATA: {
id: "test",
email: "test@test.com",
user_metadata: {},
app_metadata: {},
aud: "test",
created_at: new Date("2025-01-01").toISOString(),
},
};
// Environment-specific configurations
if (NODE_ENV === "development") {
baseConfig.LOG_LEVEL = "debug";
} else if (NODE_ENV === "production") {
baseConfig.LOG_LEVEL = "info";
}
console.log("✓ Configuration loaded successfully");
return baseConfig;
}
// Helper functions for common environment checks
// export const isDevelopment = () => config.NODE_ENV === "development";
// export const isProduction = () => config.NODE_ENV === "production";