forked from wangziqi/gongxue-base
feat: harden production storage readiness
This commit is contained in:
@@ -3,6 +3,8 @@ import { DEFAULT_DATABASE_URL, envBoolean, envList, envNumber, envString, loadDo
|
||||
loadDotenv();
|
||||
|
||||
export interface WorkerConfig {
|
||||
nodeEnv: string;
|
||||
isProduction: boolean;
|
||||
databaseUrl: string;
|
||||
crmBatchSize: number;
|
||||
crmPollIntervalMs: number;
|
||||
@@ -59,7 +61,96 @@ export interface WorkerConfig {
|
||||
supabaseStorageServiceKey: string;
|
||||
}
|
||||
|
||||
export const config: WorkerConfig = {
|
||||
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 isUnsafeSecret(value: string) {
|
||||
const normalized = value.trim().toLowerCase();
|
||||
return (
|
||||
!normalized ||
|
||||
normalized.length < 32 ||
|
||||
normalized.includes('replace_with') ||
|
||||
normalized.includes('change-me') ||
|
||||
normalized.includes('changeme') ||
|
||||
normalized.includes('your_') ||
|
||||
normalized.includes('example')
|
||||
);
|
||||
}
|
||||
|
||||
function validateProductionConfig(nextConfig: WorkerConfig) {
|
||||
if (!nextConfig.isProduction) return;
|
||||
|
||||
const failures: string[] = [];
|
||||
if (nextConfig.storageDefaultProvider === 'local_dev') {
|
||||
failures.push('STORAGE_DEFAULT_PROVIDER=local_dev is not allowed in production workers');
|
||||
}
|
||||
if (!nextConfig.storageDefaultBucket.trim()) {
|
||||
failures.push('STORAGE_DEFAULT_BUCKET is required in production workers');
|
||||
}
|
||||
if (!nextConfig.storageRequireTenantPrefix) {
|
||||
failures.push('STORAGE_REQUIRE_TENANT_PREFIX=false is not allowed in production workers');
|
||||
}
|
||||
if (nextConfig.assetSecurityScanFailOpen) {
|
||||
failures.push('WORKER_ASSET_SECURITY_SCAN_FAIL_OPEN=true is not allowed in production workers');
|
||||
}
|
||||
const scannerModes = nextConfig.assetSecurityScanner
|
||||
.split(',')
|
||||
.map(item => item.trim().toLowerCase())
|
||||
.filter(Boolean);
|
||||
const unsupportedScannerModes = scannerModes.filter(mode => mode !== 'metadata_rules' && mode !== 'http');
|
||||
if (scannerModes.length === 0 || unsupportedScannerModes.length > 0) {
|
||||
failures.push('WORKER_ASSET_SECURITY_SCANNER must include supported modes: metadata_rules,http');
|
||||
}
|
||||
if (!scannerModes.includes('http')) {
|
||||
failures.push('WORKER_ASSET_SECURITY_SCANNER must include http in production workers');
|
||||
}
|
||||
if (scannerModes.includes('http')) {
|
||||
const scannerHost = hostFromUrl(nextConfig.assetSecurityScanHttpEndpoint);
|
||||
if (!nextConfig.assetSecurityScanHttpEndpoint.startsWith('https://') || isLocalHost(scannerHost)) {
|
||||
failures.push('WORKER_ASSET_SECURITY_SCAN_HTTP_ENDPOINT must be a production HTTPS URL');
|
||||
}
|
||||
if (isUnsafeSecret(nextConfig.assetSecurityScanHttpToken)) {
|
||||
failures.push('WORKER_ASSET_SECURITY_SCAN_HTTP_TOKEN must be a strong production secret');
|
||||
}
|
||||
}
|
||||
if (nextConfig.storageDefaultProvider === 'aliyun_oss') {
|
||||
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.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 (failures.length > 0) {
|
||||
throw new Error(`Invalid production worker configuration: ${failures.join('; ')}`);
|
||||
}
|
||||
}
|
||||
|
||||
const nodeEnv = envString('NODE_ENV', 'development');
|
||||
|
||||
const loadedConfig: WorkerConfig = {
|
||||
nodeEnv,
|
||||
isProduction: nodeEnv === 'production',
|
||||
databaseUrl: envString('DATABASE_URL', DEFAULT_DATABASE_URL),
|
||||
crmBatchSize: envNumber('WORKER_CRM_BATCH_SIZE', 20),
|
||||
crmPollIntervalMs: envNumber('WORKER_CRM_POLL_INTERVAL_MS', 10_000),
|
||||
@@ -139,3 +230,7 @@ export const config: WorkerConfig = {
|
||||
supabaseStorageUrl: envString('SUPABASE_STORAGE_URL', ''),
|
||||
supabaseStorageServiceKey: envString('SUPABASE_STORAGE_SERVICE_KEY', ''),
|
||||
};
|
||||
|
||||
validateProductionConfig(loadedConfig);
|
||||
|
||||
export const config = loadedConfig;
|
||||
|
||||
Reference in New Issue
Block a user