import { DEFAULT_DATABASE_URL, DEFAULT_TENANT_SLUG, envBoolean, envList, envNumber, envString, loadDotenv } from '../../../../packages/config/src/index.js'; export interface ApiConfig { nodeEnv: string; port: number; databaseUrl: string; defaultTenantSlug: string; corsOrigins: string[]; maxJsonBodyBytes: number; maxImportJsonBodyBytes: number; authCodePepper: string; authSessionSecret: string; authSmsProvider: string; authJwtIssuer: string; authJwtAudience: string; authJwtSecret: string; authJwtJwksUrl: string; authCodeTtlSeconds: number; authSmsCooldownSeconds: number; authSessionTtlSeconds: number; allowLegacyAuthHeaders: boolean; allowPlatformAdminKey: boolean; platformAdminApiKey: string; storageDefaultProvider: string; storageDefaultBucket: string; storagePublicBaseUrl: string; storageMaxUploadBytes: number; storageAllowedMimePrefixes: string[]; storageAllowedMimeTypes: string[]; storageRequireTenantPrefix: boolean; aliyunOssRegion: string; aliyunOssEndpoint: string; aliyunOssAccessKeyId: string; aliyunOssAccessKeySecret: string; aliyunOssStsToken: string; aliyunOssInternal: boolean; tencentCosRegion: string; tencentCosAppId: string; tencentCosSecretId: string; tencentCosSecretKey: string; tencentCosSecurityToken: string; supabaseStorageUrl: string; supabaseStorageServiceKey: string; isProduction: boolean; } loadDotenv(); const DEFAULT_AUTH_CODE_PEPPER = 'development-code-pepper-change-me'; const DEFAULT_AUTH_SESSION_SECRET = 'development-session-secret-change-me'; const DEFAULT_AUTH_JWT_SECRET = 'development-jwt-secret-change-me'; const DEFAULT_PLATFORM_ADMIN_API_KEY = 'local-platform-admin-key'; const DEFAULT_MAX_JSON_BODY_BYTES = 1024 * 1024; const DEFAULT_MAX_IMPORT_JSON_BODY_BYTES = 10 * 1024 * 1024; const HARD_MAX_JSON_BODY_BYTES = 50 * 1024 * 1024; const PRODUCTION_SMS_PROVIDERS = new Set([ 'aliyun-pnvs', 'aliyun_pnvs', 'aliyun-pnvs-sms', 'aliyun-sms-auth', 'aliyun_sms_auth', ]); const PRODUCTION_STORAGE_PROVIDERS = new Set(['aliyun_oss', 'tencent_cos', 'supabase_storage']); function boundedBytes(key: string, fallback: number, hardMax = HARD_MAX_JSON_BODY_BYTES) { const value = envNumber(key, fallback); if (!Number.isFinite(value) || value <= 0) return fallback; return Math.min(Math.trunc(value), hardMax); } function isUnsafeSecret(value: string, defaultValue: string) { const normalized = value.trim().toLowerCase(); return ( value === defaultValue || normalized.length < 32 || normalized.includes('replace_with') || normalized.includes('change-me') || normalized.includes('changeme') ); } function hostFromUrl(value: string) { try { return new URL(value).hostname.toLowerCase(); } catch { return ''; } } function isLocalHost(hostname: string) { return ['localhost', '127.0.0.1', '::1', '0.0.0.0'].includes(hostname); } function isHttpsProductionUrl(value: string) { if (!value.trim()) return false; const host = hostFromUrl(value); return value.startsWith('https://') && Boolean(host) && !isLocalHost(host); } function isAllowedHost(value: string, allowedHosts: string[]) { const host = hostFromUrl(value); return allowedHosts.some(allowed => host === allowed || host.endsWith(`.${allowed}`)); } function validateProductionConfig(nextConfig: ApiConfig) { if (!nextConfig.isProduction) return; const failures: string[] = []; if (nextConfig.corsOrigins.includes('*')) failures.push('CORS_ORIGIN must not include * in production'); if (!PRODUCTION_SMS_PROVIDERS.has(nextConfig.authSmsProvider.trim().toLowerCase())) { failures.push('AUTH_SMS_PROVIDER must be aliyun-pnvs in production'); } if (isUnsafeSecret(nextConfig.authCodePepper, DEFAULT_AUTH_CODE_PEPPER)) { failures.push('AUTH_CODE_PEPPER must be a strong production secret'); } if (isUnsafeSecret(nextConfig.authSessionSecret, DEFAULT_AUTH_SESSION_SECRET)) { failures.push('AUTH_SESSION_SECRET must be a strong production secret'); } if (!nextConfig.authJwtJwksUrl && isUnsafeSecret(nextConfig.authJwtSecret, DEFAULT_AUTH_JWT_SECRET)) { failures.push('AUTH_JWT_SECRET or AUTH_JWT_JWKS_URL must be configured for production JWT verification'); } if (nextConfig.authJwtJwksUrl && !nextConfig.authJwtIssuer.trim()) { failures.push('AUTH_JWT_ISSUER is required when AUTH_JWT_JWKS_URL is configured'); } if (isUnsafeSecret(nextConfig.platformAdminApiKey, DEFAULT_PLATFORM_ADMIN_API_KEY)) { failures.push('PLATFORM_ADMIN_API_KEY must be a strong production secret until platform JWT is implemented'); } if (nextConfig.allowLegacyAuthHeaders) { failures.push('ALLOW_LEGACY_AUTH_HEADERS=true is not allowed in production'); } if (nextConfig.allowPlatformAdminKey) { failures.push('ALLOW_PLATFORM_ADMIN_KEY=true is not allowed in production'); } if (nextConfig.storageDefaultProvider === 'local_dev') { failures.push('STORAGE_DEFAULT_PROVIDER=local_dev is not allowed in production'); } else if (!PRODUCTION_STORAGE_PROVIDERS.has(nextConfig.storageDefaultProvider)) { failures.push('STORAGE_DEFAULT_PROVIDER must be aliyun_oss, tencent_cos or supabase_storage in production'); } if (!nextConfig.storageDefaultBucket.trim()) { failures.push('STORAGE_DEFAULT_BUCKET is required in production'); } if (!nextConfig.storageRequireTenantPrefix) { failures.push('STORAGE_REQUIRE_TENANT_PREFIX=false is not allowed in production'); } if (nextConfig.storagePublicBaseUrl && !isHttpsProductionUrl(nextConfig.storagePublicBaseUrl)) { failures.push('STORAGE_PUBLIC_BASE_URL must be a production HTTPS URL when configured'); } if (nextConfig.storageDefaultProvider === 'aliyun_oss') { if (nextConfig.aliyunOssInternal) { failures.push('ALIYUN_OSS_INTERNAL=true is not allowed for user-facing production signed URLs'); } if (!nextConfig.aliyunOssAccessKeyId || !nextConfig.aliyunOssAccessKeySecret) { failures.push('ALIYUN_OSS_ACCESS_KEY_ID and ALIYUN_OSS_ACCESS_KEY_SECRET are required for aliyun_oss'); } if (!nextConfig.aliyunOssRegion && !nextConfig.aliyunOssEndpoint) { failures.push('ALIYUN_OSS_REGION or ALIYUN_OSS_ENDPOINT is required for aliyun_oss'); } if (nextConfig.aliyunOssEndpoint && (!isHttpsProductionUrl(nextConfig.aliyunOssEndpoint) || !isAllowedHost(nextConfig.aliyunOssEndpoint, ['aliyuncs.com']))) { failures.push('ALIYUN_OSS_ENDPOINT must be a production HTTPS aliyuncs.com endpoint'); } } if (nextConfig.storageDefaultProvider === 'tencent_cos') { if (!nextConfig.tencentCosRegion || !nextConfig.tencentCosAppId || !nextConfig.tencentCosSecretId || !nextConfig.tencentCosSecretKey) { failures.push('TENCENT_COS_REGION, TENCENT_COS_APP_ID, TENCENT_COS_SECRET_ID and TENCENT_COS_SECRET_KEY are required for tencent_cos'); } } if (nextConfig.storageDefaultProvider === 'supabase_storage') { if (!nextConfig.supabaseStorageUrl || !nextConfig.supabaseStorageServiceKey) { failures.push('SUPABASE_STORAGE_URL and SUPABASE_STORAGE_SERVICE_KEY are required for supabase_storage'); } if (nextConfig.supabaseStorageUrl && !isHttpsProductionUrl(nextConfig.supabaseStorageUrl)) { failures.push('SUPABASE_STORAGE_URL must be a production HTTPS URL'); } } if (failures.length > 0) { throw new Error(`Invalid production API configuration: ${failures.join('; ')}`); } } const nodeEnv = envString('NODE_ENV', 'development'); const isProduction = nodeEnv === 'production'; const loadedConfig: ApiConfig = { nodeEnv, port: envNumber('PORT', 8787), databaseUrl: envString('DATABASE_URL', DEFAULT_DATABASE_URL), defaultTenantSlug: envString('DEFAULT_TENANT_SLUG', DEFAULT_TENANT_SLUG), corsOrigins: envList('CORS_ORIGIN', '*'), maxJsonBodyBytes: boundedBytes('MAX_JSON_BODY_BYTES', DEFAULT_MAX_JSON_BODY_BYTES), maxImportJsonBodyBytes: boundedBytes('MAX_IMPORT_JSON_BODY_BYTES', DEFAULT_MAX_IMPORT_JSON_BODY_BYTES), authCodePepper: envString('AUTH_CODE_PEPPER', DEFAULT_AUTH_CODE_PEPPER), authSessionSecret: envString('AUTH_SESSION_SECRET', DEFAULT_AUTH_SESSION_SECRET), authSmsProvider: envString('AUTH_SMS_PROVIDER', 'mock'), authJwtIssuer: envString('AUTH_JWT_ISSUER', ''), authJwtAudience: envString('AUTH_JWT_AUDIENCE', 'authenticated'), authJwtSecret: envString('AUTH_JWT_SECRET', DEFAULT_AUTH_JWT_SECRET), authJwtJwksUrl: envString('AUTH_JWT_JWKS_URL', ''), authCodeTtlSeconds: envNumber('AUTH_CODE_TTL_SECONDS', 300), authSmsCooldownSeconds: envNumber('AUTH_SMS_COOLDOWN_SECONDS', 60), authSessionTtlSeconds: envNumber('AUTH_SESSION_TTL_SECONDS', 60 * 60 * 24 * 7), allowLegacyAuthHeaders: envBoolean('ALLOW_LEGACY_AUTH_HEADERS', !isProduction), allowPlatformAdminKey: envBoolean('ALLOW_PLATFORM_ADMIN_KEY', !isProduction), platformAdminApiKey: envString('PLATFORM_ADMIN_API_KEY', DEFAULT_PLATFORM_ADMIN_API_KEY), storageDefaultProvider: envString('STORAGE_DEFAULT_PROVIDER', 'local_dev'), storageDefaultBucket: envString('STORAGE_DEFAULT_BUCKET', 'tenant-assets'), storagePublicBaseUrl: envString('STORAGE_PUBLIC_BASE_URL', ''), storageMaxUploadBytes: envNumber('STORAGE_MAX_UPLOAD_BYTES', 1024 * 1024 * 500), storageAllowedMimePrefixes: envList('STORAGE_ALLOWED_MIME_PREFIXES', 'image/,video/,audio/'), storageAllowedMimeTypes: envList( 'STORAGE_ALLOWED_MIME_TYPES', [ 'application/pdf', 'application/json', 'application/zip', 'application/x-zip-compressed', 'application/msword', 'application/vnd.openxmlformats-officedocument.wordprocessingml.document', 'application/vnd.ms-excel', 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', 'application/vnd.ms-powerpoint', 'application/vnd.openxmlformats-officedocument.presentationml.presentation', 'application/octet-stream', 'text/plain', 'text/markdown', 'text/csv', ].join(','), ), storageRequireTenantPrefix: envBoolean('STORAGE_REQUIRE_TENANT_PREFIX', true), aliyunOssRegion: envString('ALIYUN_OSS_REGION', ''), aliyunOssEndpoint: envString('ALIYUN_OSS_ENDPOINT', ''), aliyunOssAccessKeyId: envString('ALIYUN_OSS_ACCESS_KEY_ID', ''), aliyunOssAccessKeySecret: envString('ALIYUN_OSS_ACCESS_KEY_SECRET', ''), aliyunOssStsToken: envString('ALIYUN_OSS_STS_TOKEN', ''), aliyunOssInternal: envBoolean('ALIYUN_OSS_INTERNAL', false), tencentCosRegion: envString('TENCENT_COS_REGION', ''), tencentCosAppId: envString('TENCENT_COS_APP_ID', ''), tencentCosSecretId: envString('TENCENT_COS_SECRET_ID', ''), tencentCosSecretKey: envString('TENCENT_COS_SECRET_KEY', ''), tencentCosSecurityToken: envString('TENCENT_COS_SECURITY_TOKEN', ''), supabaseStorageUrl: envString('SUPABASE_STORAGE_URL', ''), supabaseStorageServiceKey: envString('SUPABASE_STORAGE_SERVICE_KEY', ''), isProduction, }; validateProductionConfig(loadedConfig); export const config = loadedConfig;