forked from wangziqi/gongxue-base
237 lines
10 KiB
TypeScript
237 lines
10 KiB
TypeScript
import { DEFAULT_DATABASE_URL, envBoolean, envList, envNumber, envString, loadDotenv } from '../../../packages/config/src/index.js';
|
|
|
|
loadDotenv();
|
|
|
|
export interface WorkerConfig {
|
|
nodeEnv: string;
|
|
isProduction: boolean;
|
|
databaseUrl: string;
|
|
crmBatchSize: number;
|
|
crmPollIntervalMs: number;
|
|
crmMaxAttempts: number;
|
|
crmBackoffSeconds: number[];
|
|
crmRequestTimeoutMs: number;
|
|
crmAllowInsecureLocalhost: boolean;
|
|
commerceBatchSize: number;
|
|
commerceMinAgeSeconds: number;
|
|
commerceRequestTimeoutMs: number;
|
|
providerBillBatchSize: number;
|
|
providerBillWorkerId: string;
|
|
providerBillClaimStaleSeconds: number;
|
|
assetBatchSize: number;
|
|
assetMinAgeSeconds: number;
|
|
assetRecheckIntervalSeconds: number;
|
|
assetRequestTimeoutMs: number;
|
|
assetSecurityScanner: string;
|
|
assetSecurityScanHttpEndpoint: string;
|
|
assetSecurityScanHttpToken: string;
|
|
assetSecurityScanHttpTimeoutMs: number;
|
|
assetSecurityScanFailOpen: boolean;
|
|
importBatchSize: number;
|
|
importWorkerId: string;
|
|
importBackoffSeconds: number[];
|
|
publicBankSyncBatchSize: number;
|
|
publicBankSyncCopyLimit: number;
|
|
publicBankSyncWorkerId: string;
|
|
publicBankSyncClaimStaleSeconds: number;
|
|
exportBatchSize: number;
|
|
exportWorkerId: string;
|
|
exportBackoffSeconds: number[];
|
|
exportLocalStorageRoot: string;
|
|
exportPdfFontPath: 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;
|
|
}
|
|
|
|
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),
|
|
crmMaxAttempts: envNumber('WORKER_CRM_MAX_ATTEMPTS', 5),
|
|
crmBackoffSeconds: envList('WORKER_CRM_BACKOFF_SECONDS', '5,30,120,600,1800')
|
|
.map((value: string) => Number(value))
|
|
.filter((value: number) => Number.isFinite(value) && value > 0),
|
|
crmRequestTimeoutMs: envNumber('WORKER_CRM_REQUEST_TIMEOUT_MS', 10_000),
|
|
crmAllowInsecureLocalhost: envBoolean('WORKER_CRM_ALLOW_INSECURE_LOCALHOST', false),
|
|
commerceBatchSize: envNumber('WORKER_COMMERCE_BATCH_SIZE', 20),
|
|
commerceMinAgeSeconds: envNumber('WORKER_COMMERCE_MIN_AGE_SECONDS', 300),
|
|
commerceRequestTimeoutMs: envNumber('WORKER_COMMERCE_REQUEST_TIMEOUT_MS', 10_000),
|
|
providerBillBatchSize: envNumber('WORKER_PROVIDER_BILL_BATCH_SIZE', 5),
|
|
providerBillWorkerId: envString('WORKER_PROVIDER_BILL_ID', `provider-bills-${process.pid}`),
|
|
providerBillClaimStaleSeconds: envNumber('WORKER_PROVIDER_BILL_CLAIM_STALE_SECONDS', 15 * 60),
|
|
assetBatchSize: envNumber('WORKER_ASSET_BATCH_SIZE', 50),
|
|
assetMinAgeSeconds: envNumber('WORKER_ASSET_MIN_AGE_SECONDS', 300),
|
|
assetRecheckIntervalSeconds: envNumber('WORKER_ASSET_RECHECK_INTERVAL_SECONDS', 60 * 60 * 24),
|
|
assetRequestTimeoutMs: envNumber('WORKER_ASSET_REQUEST_TIMEOUT_MS', 10_000),
|
|
assetSecurityScanner: envString('WORKER_ASSET_SECURITY_SCANNER', 'metadata_rules'),
|
|
assetSecurityScanHttpEndpoint: envString('WORKER_ASSET_SECURITY_SCAN_HTTP_ENDPOINT', ''),
|
|
assetSecurityScanHttpToken: envString('WORKER_ASSET_SECURITY_SCAN_HTTP_TOKEN', ''),
|
|
assetSecurityScanHttpTimeoutMs: envNumber('WORKER_ASSET_SECURITY_SCAN_HTTP_TIMEOUT_MS', 10_000),
|
|
assetSecurityScanFailOpen: envBoolean('WORKER_ASSET_SECURITY_SCAN_FAIL_OPEN', false),
|
|
importBatchSize: envNumber('WORKER_IMPORT_BATCH_SIZE', 5),
|
|
importWorkerId: envString('WORKER_IMPORT_ID', `imports-${process.pid}`),
|
|
importBackoffSeconds: envList('WORKER_IMPORT_BACKOFF_SECONDS', '30,120,600,1800')
|
|
.map((value: string) => Number(value))
|
|
.filter((value: number) => Number.isFinite(value) && value > 0),
|
|
publicBankSyncBatchSize: envNumber('WORKER_PUBLIC_BANK_SYNC_BATCH_SIZE', 5),
|
|
publicBankSyncCopyLimit: envNumber('WORKER_PUBLIC_BANK_SYNC_COPY_LIMIT', 1000),
|
|
publicBankSyncWorkerId: envString('WORKER_PUBLIC_BANK_SYNC_ID', `public-banks-${process.pid}`),
|
|
publicBankSyncClaimStaleSeconds: envNumber('WORKER_PUBLIC_BANK_SYNC_CLAIM_STALE_SECONDS', 15 * 60),
|
|
exportBatchSize: envNumber('WORKER_EXPORT_BATCH_SIZE', 5),
|
|
exportWorkerId: envString('WORKER_EXPORT_ID', `exports-${process.pid}`),
|
|
exportBackoffSeconds: envList('WORKER_EXPORT_BACKOFF_SECONDS', '30,120,600,1800')
|
|
.map((value: string) => Number(value))
|
|
.filter((value: number) => Number.isFinite(value) && value > 0),
|
|
exportLocalStorageRoot: envString('EXPORT_LOCAL_STORAGE_ROOT', '.local-storage'),
|
|
exportPdfFontPath: envString('EXPORT_PDF_FONT_PATH', ''),
|
|
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', ''),
|
|
};
|
|
|
|
validateProductionConfig(loadedConfig);
|
|
|
|
export const config = loadedConfig;
|