Files
gongxue-base/apps/worker/src/config.ts
T

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;