chore: harden production readiness gates

This commit is contained in:
Codex
2026-07-01 06:09:23 +08:00
parent f3f6028633
commit bb938cf6e2
13 changed files with 695 additions and 63 deletions

View File

@@ -53,6 +53,8 @@ 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', 'aliyun-sms', 'aliyun_sms', 'tencent', 'tencent-sms', 'tencent_sms']);
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);
@@ -71,12 +73,37 @@ function isUnsafeSecret(value: string, defaultValue: string) {
);
}
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 (nextConfig.authSmsProvider === 'mock') failures.push('AUTH_SMS_PROVIDER=mock is not allowed in production');
if (!PRODUCTION_SMS_PROVIDERS.has(nextConfig.authSmsProvider.trim().toLowerCase())) {
failures.push('AUTH_SMS_PROVIDER must be aliyun/aliyun-sms or tencent/tencent-sms in production');
}
if (isUnsafeSecret(nextConfig.authCodePepper, DEFAULT_AUTH_CODE_PEPPER)) {
failures.push('AUTH_CODE_PEPPER must be a strong production secret');
}
@@ -100,6 +127,8 @@ function validateProductionConfig(nextConfig: ApiConfig) {
}
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');
@@ -107,13 +136,22 @@ function validateProductionConfig(nextConfig: ApiConfig) {
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) {
@@ -124,6 +162,9 @@ function validateProductionConfig(nextConfig: ApiConfig) {
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) {