forked from wangziqi/gongxue-base
chore: harden production readiness gates
This commit is contained in:
@@ -102,6 +102,25 @@ 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;
|
||||
try {
|
||||
const url = new URL(value);
|
||||
return url.protocol === 'https:' && !isLocalHost(url.hostname.toLowerCase());
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
function isAllowedHost(value: string, allowedHosts: string[]) {
|
||||
try {
|
||||
const hostname = new URL(value).hostname.toLowerCase();
|
||||
return allowedHosts.some(host => hostname === host || hostname.endsWith(`.${host}`));
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
function isUnsafeSecret(value: string) {
|
||||
const normalized = value.trim().toLowerCase();
|
||||
return (
|
||||
@@ -121,6 +140,8 @@ function validateProductionConfig(nextConfig: WorkerConfig) {
|
||||
const failures: string[] = [];
|
||||
if (nextConfig.storageDefaultProvider === 'local_dev') {
|
||||
failures.push('STORAGE_DEFAULT_PROVIDER=local_dev is not allowed in production workers');
|
||||
} else if (!['aliyun_oss', 'tencent_cos', 'supabase_storage'].includes(nextConfig.storageDefaultProvider)) {
|
||||
failures.push('STORAGE_DEFAULT_PROVIDER must be aliyun_oss, tencent_cos or supabase_storage in production workers');
|
||||
}
|
||||
if (!nextConfig.storageDefaultBucket.trim()) {
|
||||
failures.push('STORAGE_DEFAULT_BUCKET is required in production workers');
|
||||
@@ -128,6 +149,9 @@ function validateProductionConfig(nextConfig: WorkerConfig) {
|
||||
if (!nextConfig.storageRequireTenantPrefix) {
|
||||
failures.push('STORAGE_REQUIRE_TENANT_PREFIX=false is not allowed in production workers');
|
||||
}
|
||||
if (nextConfig.storagePublicBaseUrl && !isHttpsProductionUrl(nextConfig.storagePublicBaseUrl)) {
|
||||
failures.push('STORAGE_PUBLIC_BASE_URL must be a production HTTPS URL when configured');
|
||||
}
|
||||
if (nextConfig.assetSecurityScanFailOpen) {
|
||||
failures.push('WORKER_ASSET_SECURITY_SCAN_FAIL_OPEN=true is not allowed in production workers');
|
||||
}
|
||||
@@ -158,12 +182,18 @@ function validateProductionConfig(nextConfig: WorkerConfig) {
|
||||
}
|
||||
}
|
||||
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) {
|
||||
@@ -174,6 +204,9 @@ function validateProductionConfig(nextConfig: WorkerConfig) {
|
||||
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) {
|
||||
|
||||
Reference in New Issue
Block a user