import fs from 'node:fs'; import os from 'node:os'; import path from 'node:path'; import process from 'node:process'; import pg from 'pg'; const DEFAULT_DATABASE_URL = 'postgresql://postgres:postgres@127.0.0.1:54322/postgres'; const DEFAULT_AUTH_CODE_PEPPER = 'development-code-pepper-change-me'; const DEFAULT_AUTH_SESSION_SECRET = 'development-session-secret-change-me'; const DEFAULT_AUTH_JWT_SECRET = 'development-jwt-secret-change-me'; const DEFAULT_PLATFORM_ADMIN_API_KEY = 'local-platform-admin-key'; const HARD_MAX_JSON_BODY_BYTES = 50 * 1024 * 1024; const args = new Set(process.argv.slice(2)); const argValues = new Map(); for (let index = 2; index < process.argv.length; index += 1) { const current = process.argv[index]; if (current.startsWith('--') && process.argv[index + 1] && !process.argv[index + 1].startsWith('--')) { argValues.set(current, process.argv[index + 1]); index += 1; } } const jsonOutput = args.has('--json'); const checkDb = args.has('--check-db'); const skipDb = args.has('--skip-db') || !checkDb; const envFile = argValues.get('--env-file') || path.resolve(process.cwd(), '.env'); const checks = []; function loadEnvFile(filePath) { if (!filePath || !fs.existsSync(filePath)) return; const content = fs.readFileSync(filePath, 'utf8'); for (const line of content.split(/\r?\n/)) { const trimmed = line.trim(); if (!trimmed || trimmed.startsWith('#')) continue; const index = trimmed.indexOf('='); if (index === -1) continue; const key = trimmed.slice(0, index).trim(); const value = trimmed.slice(index + 1).trim().replace(/^"|"$/g, ''); if (key && process.env[key] === undefined) process.env[key] = value; } } function env(key, fallback = '') { return process.env[key] ?? fallback; } function envBool(key, fallback = false) { const value = process.env[key]; if (value === undefined) return fallback; return ['true', '1', 'yes', 'y', 'on'].includes(value.toLowerCase()); } function envNumber(key, fallback) { const parsed = Number(process.env[key]); return Number.isFinite(parsed) ? parsed : fallback; } function envList(key, fallback = '') { return env(key, fallback) .split(',') .map(item => item.trim()) .filter(Boolean); } function add(status, id, message, details = {}) { checks.push({ status, id, message, details }); } function pass(id, message, details = {}) { add('pass', id, message, details); } function warn(id, message, details = {}) { add('warn', id, message, details); } function block(id, message, details = {}) { add('blocker', id, message, details); } function isUnsafeSecret(value, defaultValue) { const normalized = String(value || '').trim().toLowerCase(); return ( !normalized || value === defaultValue || normalized.length < 32 || normalized.includes('replace_with') || normalized.includes('change-me') || normalized.includes('changeme') || normalized.includes('your_') || normalized.includes('example') ); } function hostFromUrl(value) { try { return new URL(value).hostname.toLowerCase(); } catch { return ''; } } function isLocalHost(hostname) { return ['localhost', '127.0.0.1', '::1', '0.0.0.0'].includes(hostname); } function isSecretLikeKey(key) { const normalized = key.toLowerCase().replace(/[-_\s]/g, ''); const allowedSecretRef = normalized === 'secretref' || normalized.endsWith('secretref'); return ( !allowedSecretRef && (normalized.includes('secret') || normalized.includes('password') || normalized.includes('token') || normalized.includes('privatekey') || normalized.includes('apikey') || normalized.includes('apiv3key') || normalized.includes('mchkey') || normalized.includes('signkey') || normalized.includes('aeskey') || normalized.includes('partnerkey')) ); } function findSecretLikePaths(value, prefix = 'configPublic') { if (!value || typeof value !== 'object') return []; if (Array.isArray(value)) { return value.flatMap((item, index) => findSecretLikePaths(item, `${prefix}[${index}]`)); } const paths = []; for (const [key, child] of Object.entries(value)) { const nextPath = `${prefix}.${key}`; if (isSecretLikeKey(key)) paths.push(nextPath); paths.push(...findSecretLikePaths(child, nextPath)); } return paths; } function safeProviderName(provider) { return String(provider || '').replace(/[^a-zA-Z0-9_.:-]/g, '_'); } function formatTableName(row) { return `${row.table_schema}.${row.table_name}`; } function validateEnv() { const nodeEnv = env('NODE_ENV', 'development'); if (nodeEnv !== 'production') block('env.node_env', 'NODE_ENV must be production for production readiness checks'); else pass('env.node_env', 'NODE_ENV is production'); const databaseUrl = env('DATABASE_URL', DEFAULT_DATABASE_URL); const databaseHost = hostFromUrl(databaseUrl); if (!databaseUrl || databaseUrl === DEFAULT_DATABASE_URL) { block('env.database_url', 'DATABASE_URL must point to the production Supabase/PostgreSQL database'); } else if (isLocalHost(databaseHost)) { warn('env.database_url.localhost', 'DATABASE_URL points to a local host; confirm this is intentional for self-hosted deployment'); } else { pass('env.database_url', 'DATABASE_URL is not the local development default'); } const corsOrigins = envList('CORS_ORIGIN', '*'); if (corsOrigins.includes('*')) { block('env.cors_origin', 'CORS_ORIGIN must not include * in production'); } else if (corsOrigins.length === 0) { block('env.cors_origin.empty', 'CORS_ORIGIN must include the deployed H5/admin domains'); } else { const unsafeOrigins = corsOrigins.filter(origin => { const host = hostFromUrl(origin); return !origin.startsWith('https://') || isLocalHost(host); }); if (unsafeOrigins.length > 0) { block('env.cors_origin.unsafe', 'CORS_ORIGIN must use production HTTPS origins only', { count: unsafeOrigins.length }); } else { pass('env.cors_origin', 'CORS_ORIGIN is restricted to HTTPS origins', { count: corsOrigins.length }); } } const authSmsProvider = env('AUTH_SMS_PROVIDER', 'mock'); if (authSmsProvider === 'mock') block('env.auth_sms_provider', 'AUTH_SMS_PROVIDER=mock is not allowed in production'); else pass('env.auth_sms_provider', 'AUTH_SMS_PROVIDER is not mock'); if (isUnsafeSecret(env('AUTH_CODE_PEPPER', DEFAULT_AUTH_CODE_PEPPER), DEFAULT_AUTH_CODE_PEPPER)) { block('env.auth_code_pepper', 'AUTH_CODE_PEPPER must be a strong production secret'); } else { pass('env.auth_code_pepper', 'AUTH_CODE_PEPPER looks production-grade'); } if (isUnsafeSecret(env('AUTH_SESSION_SECRET', DEFAULT_AUTH_SESSION_SECRET), DEFAULT_AUTH_SESSION_SECRET)) { block('env.auth_session_secret', 'AUTH_SESSION_SECRET must be a strong production secret'); } else { pass('env.auth_session_secret', 'AUTH_SESSION_SECRET looks production-grade'); } const jwksUrl = env('AUTH_JWT_JWKS_URL', ''); const jwtSecret = env('AUTH_JWT_SECRET', DEFAULT_AUTH_JWT_SECRET); if (jwksUrl) { const host = hostFromUrl(jwksUrl); if (!jwksUrl.startsWith('https://') || isLocalHost(host)) { block('env.auth_jwt_jwks_url', 'AUTH_JWT_JWKS_URL must be an HTTPS production URL'); } else { pass('env.auth_jwt_jwks_url', 'AUTH_JWT_JWKS_URL is configured'); } if (!env('AUTH_JWT_ISSUER', '').trim()) { block('env.auth_jwt_issuer', 'AUTH_JWT_ISSUER is required when AUTH_JWT_JWKS_URL is configured'); } else { pass('env.auth_jwt_issuer', 'AUTH_JWT_ISSUER is configured for JWKS verification'); } } else if (isUnsafeSecret(jwtSecret, DEFAULT_AUTH_JWT_SECRET)) { block('env.auth_jwt_secret', 'AUTH_JWT_SECRET or AUTH_JWT_JWKS_URL must be configured for production JWT verification'); } else { pass('env.auth_jwt_secret', 'AUTH_JWT_SECRET looks production-grade'); } if (envBool('ALLOW_LEGACY_AUTH_HEADERS', false)) { block('env.allow_legacy_auth_headers', 'ALLOW_LEGACY_AUTH_HEADERS must be false in production'); } else { pass('env.allow_legacy_auth_headers', 'legacy x-user-id auth headers are disabled'); } if (envBool('ALLOW_PLATFORM_ADMIN_KEY', false)) { block('env.allow_platform_admin_key', 'ALLOW_PLATFORM_ADMIN_KEY must be false in production'); } else { pass('env.allow_platform_admin_key', 'platform admin API key compatibility is disabled'); } if (isUnsafeSecret(env('PLATFORM_ADMIN_API_KEY', DEFAULT_PLATFORM_ADMIN_API_KEY), DEFAULT_PLATFORM_ADMIN_API_KEY)) { warn('env.platform_admin_api_key', 'PLATFORM_ADMIN_API_KEY is weak/default; keep ALLOW_PLATFORM_ADMIN_KEY=false and rotate before any temporary use'); } else { pass('env.platform_admin_api_key', 'PLATFORM_ADMIN_API_KEY is not default'); } const maxJson = envNumber('MAX_JSON_BODY_BYTES', 1024 * 1024); const maxImportJson = envNumber('MAX_IMPORT_JSON_BODY_BYTES', 10 * 1024 * 1024); if (maxJson > HARD_MAX_JSON_BODY_BYTES || maxImportJson > HARD_MAX_JSON_BODY_BYTES) { block('env.body_size', 'JSON body limits must stay at or below 50MB'); } else { pass('env.body_size', 'JSON body limits are bounded'); } const storageProvider = env('STORAGE_DEFAULT_PROVIDER', 'local_dev'); if (storageProvider === 'local_dev') { block('env.storage_provider', 'STORAGE_DEFAULT_PROVIDER=local_dev is not allowed for production assets'); } else { pass('env.storage_provider', 'STORAGE_DEFAULT_PROVIDER is production-capable', { provider: storageProvider }); } if (!env('STORAGE_DEFAULT_BUCKET', '')) { block('env.storage_bucket', 'STORAGE_DEFAULT_BUCKET is required'); } else { pass('env.storage_bucket', 'STORAGE_DEFAULT_BUCKET is configured'); } if (!envBool('STORAGE_REQUIRE_TENANT_PREFIX', true)) { block('env.storage_tenant_prefix', 'STORAGE_REQUIRE_TENANT_PREFIX must remain true to protect tenant assets'); } else { pass('env.storage_tenant_prefix', 'tenant-prefixed object keys are required'); } if (storageProvider === 'aliyun_oss') { for (const key of ['ALIYUN_OSS_REGION', 'ALIYUN_OSS_ENDPOINT', 'ALIYUN_OSS_ACCESS_KEY_ID', 'ALIYUN_OSS_ACCESS_KEY_SECRET']) { if (!env(key, '')) block(`env.${key.toLowerCase()}`, `${key} is required for aliyun_oss`); } } if (storageProvider === 'tencent_cos') { for (const key of ['TENCENT_COS_REGION', 'TENCENT_COS_APP_ID', 'TENCENT_COS_SECRET_ID', 'TENCENT_COS_SECRET_KEY']) { if (!env(key, '')) block(`env.${key.toLowerCase()}`, `${key} is required for tencent_cos`); } } if (storageProvider === 'supabase_storage') { for (const key of ['SUPABASE_STORAGE_URL', 'SUPABASE_STORAGE_SERVICE_KEY']) { if (!env(key, '')) block(`env.${key.toLowerCase()}`, `${key} is required for supabase_storage`); } } if (envList('STORAGE_ALLOWED_MIME_TYPES').includes('application/octet-stream')) { warn('env.storage_octet_stream', 'application/octet-stream is allowed; consider removing it after import migration is stable'); } const scannerModes = envList('WORKER_ASSET_SECURITY_SCANNER', 'metadata_rules').map(item => item.toLowerCase()); const unsupportedScannerModes = scannerModes.filter(mode => mode !== 'metadata_rules' && mode !== 'http'); if (scannerModes.length === 0) { block('env.asset_security_scanner.empty', 'WORKER_ASSET_SECURITY_SCANNER must include metadata_rules and a production scanner'); } else if (unsupportedScannerModes.length > 0) { block('env.asset_security_scanner.unsupported', 'WORKER_ASSET_SECURITY_SCANNER contains unsupported modes', { modes: unsupportedScannerModes, }); } else if (!scannerModes.includes('http')) { block('env.asset_security_scanner.external', 'Production assets require WORKER_ASSET_SECURITY_SCANNER=http or metadata_rules,http'); } else { pass('env.asset_security_scanner', 'external asset security scanner is enabled', { modes: scannerModes }); } const scannerEndpoint = env('WORKER_ASSET_SECURITY_SCAN_HTTP_ENDPOINT', ''); if (scannerModes.includes('http')) { const scannerHost = hostFromUrl(scannerEndpoint); if (!scannerEndpoint) { block('env.asset_security_scan_http_endpoint', 'WORKER_ASSET_SECURITY_SCAN_HTTP_ENDPOINT is required when http scanner is enabled'); } else if (!scannerEndpoint.startsWith('https://') || isLocalHost(scannerHost)) { block('env.asset_security_scan_http_endpoint.unsafe', 'WORKER_ASSET_SECURITY_SCAN_HTTP_ENDPOINT must be a production HTTPS URL'); } else { pass('env.asset_security_scan_http_endpoint', 'asset security scanner endpoint is HTTPS'); } if (isUnsafeSecret(env('WORKER_ASSET_SECURITY_SCAN_HTTP_TOKEN', ''), '')) { block('env.asset_security_scan_http_token', 'WORKER_ASSET_SECURITY_SCAN_HTTP_TOKEN must be a strong shared secret or service token'); } else { pass('env.asset_security_scan_http_token', 'asset security scanner token looks production-grade'); } } const scannerTimeout = envNumber('WORKER_ASSET_SECURITY_SCAN_HTTP_TIMEOUT_MS', 10_000); if (scannerTimeout <= 0 || scannerTimeout > 60_000) { block('env.asset_security_scan_http_timeout', 'WORKER_ASSET_SECURITY_SCAN_HTTP_TIMEOUT_MS must be between 1 and 60000'); } else { pass('env.asset_security_scan_http_timeout', 'asset security scanner timeout is bounded'); } if (envBool('WORKER_ASSET_SECURITY_SCAN_FAIL_OPEN', false)) { block('env.asset_security_scan_fail_open', 'WORKER_ASSET_SECURITY_SCAN_FAIL_OPEN must be false in production'); } else { pass('env.asset_security_scan_fail_open', 'asset security scanning fails closed'); } if (envBool('WORKER_CRM_ALLOW_INSECURE_LOCALHOST', false)) { block('env.worker_crm_insecure_localhost', 'WORKER_CRM_ALLOW_INSECURE_LOCALHOST must be false in production'); } else { pass('env.worker_crm_insecure_localhost', 'CRM worker insecure localhost webhook mode is disabled'); } if (envBool('WORKER_PLATFORM_AUDIT_NOTIFICATION_ALLOW_INSECURE_LOCALHOST', false)) { block('env.worker_platform_audit_notification_insecure_localhost', 'WORKER_PLATFORM_AUDIT_NOTIFICATION_ALLOW_INSECURE_LOCALHOST must be false in production'); } else { pass('env.worker_platform_audit_notification_insecure_localhost', 'Platform audit notification worker insecure localhost webhook mode is disabled'); } if (envBool('WORKER_PLATFORM_DUNNING_NOTIFICATION_ALLOW_INSECURE_LOCALHOST', false)) { block('env.worker_platform_dunning_notification_insecure_localhost', 'WORKER_PLATFORM_DUNNING_NOTIFICATION_ALLOW_INSECURE_LOCALHOST must be false in production'); } else { pass('env.worker_platform_dunning_notification_insecure_localhost', 'Platform dunning notification worker insecure localhost webhook mode is disabled'); } const requiredPositiveNumbers = [ 'WORKER_CRM_BATCH_SIZE', 'WORKER_COMMERCE_BATCH_SIZE', 'WORKER_ASSET_BATCH_SIZE', 'WORKER_IMPORT_BATCH_SIZE', 'WORKER_PUBLIC_BANK_SYNC_BATCH_SIZE', ]; for (const key of requiredPositiveNumbers) { if (envNumber(key, 1) <= 0) block(`env.${key.toLowerCase()}`, `${key} must be greater than 0`); } } async function validateDatabase() { if (skipDb) { warn('db.skipped', 'Database readiness checks skipped; run with --check-db after production DATABASE_URL is configured'); return; } const pool = new pg.Pool({ connectionString: env('DATABASE_URL', DEFAULT_DATABASE_URL), max: 2 }); try { const tenantRows = await pool.query(` select id, name, status from public.tenants where status = 'active' order by created_at asc `); if (tenantRows.rowCount === 0) block('db.tenants', 'No active tenant exists in the production database'); else pass('db.tenants', 'Active tenants found', { count: tenantRows.rowCount }); const publicSecretRows = await pool.query(` select source, tenant_id, provider, config_public from ( select 'auth' as source, tenant_id, provider, config_public from public.tenant_auth_providers where status in ('active', 'testing') union all select 'payment' as source, tenant_id, provider, config_public from public.tenant_payment_accounts where status = 'active' ) providers `); for (const row of publicSecretRows.rows) { const secretPaths = findSecretLikePaths(row.config_public || {}); if (secretPaths.length > 0) { block(`db.${row.source}.${safeProviderName(row.provider)}.public_secret`, 'Provider public config contains secret-like keys', { tenantId: row.tenant_id, provider: row.provider, secretPathCount: secretPaths.length, }); } } if (publicSecretRows.rows.every(row => findSecretLikePaths(row.config_public || {}).length === 0)) { pass('db.provider_public_config', 'Active provider public configs do not contain secret-like keys'); } const missingAuthSecretRows = await pool.query(` select p.tenant_id, p.provider from public.tenant_auth_providers p left join app_private.tenant_secrets s on s.tenant_id = p.tenant_id and s.secret_scope = case when lower(replace(p.provider, '_', '-')) in ('aliyun', 'aliyun-sms', 'tencent', 'tencent-sms') then 'sms' else 'oauth' end and s.secret_key = coalesce(nullif(split_part(p.config_public->>'secretRef', ':', 3), ''), p.provider) where p.status in ('active', 'testing') and p.provider not in ('mock') and s.id is null `); if (missingAuthSecretRows.rowCount > 0) { block('db.auth_provider_secrets', 'Some active auth providers are missing tenant secret rows', { count: missingAuthSecretRows.rowCount }); } else { pass('db.auth_provider_secrets', 'Active auth providers have private secret rows'); } const missingPaymentSecretRows = await pool.query(` select p.tenant_id, p.provider from public.tenant_payment_accounts p left join app_private.tenant_secrets s on s.tenant_id = p.tenant_id and s.secret_scope = 'payment' and s.secret_key = coalesce(nullif(split_part(p.config_public->>'secretRef', ':', 3), ''), p.provider) where p.status = 'active' and p.provider not in ('manual') and s.id is null `); if (missingPaymentSecretRows.rowCount > 0) { block('db.payment_provider_secrets', 'Some active payment accounts are missing tenant secret rows', { count: missingPaymentSecretRows.rowCount }); } else { pass('db.payment_provider_secrets', 'Active payment accounts have private secret rows'); } const unsafePlatformAuditNotificationRows = await pool.query(` select id, channel_code, provider, webhook_url from public.platform_audit_notification_channels where enabled = true and ( webhook_url !~* '^https://' or webhook_url ~* '^https?://(localhost|127\\.0\\.0\\.1|\\[?::1\\]?)' ) `); if (unsafePlatformAuditNotificationRows.rowCount > 0) { block('db.platform_audit_notification_webhooks', 'Enabled platform audit notification webhooks must use production HTTPS URLs', { count: unsafePlatformAuditNotificationRows.rowCount, samples: unsafePlatformAuditNotificationRows.rows.slice(0, 5).map(row => ({ id: row.id, channelCode: row.channel_code, provider: row.provider, })), }); } else { pass('db.platform_audit_notification_webhooks', 'Enabled platform audit notification webhooks use production HTTPS URLs'); } const missingPlatformAuditNotificationSecretRows = await pool.query(` select c.id, c.channel_code, c.provider, c.secret_ref from public.platform_audit_notification_channels c left join app_private.platform_secrets s on s.secret_scope = split_part(c.secret_ref, ':', 2) and s.secret_key = split_part(c.secret_ref, ':', 3) where c.enabled = true and c.provider in ('dingtalk', 'feishu') and (c.secret_ref is null or c.secret_ref !~ '^app_private\\.platform_secrets:' or s.id is null) `); if (missingPlatformAuditNotificationSecretRows.rowCount > 0) { block('db.platform_audit_notification_secrets', 'Signed platform audit notification channels require app_private.platform_secrets rows', { count: missingPlatformAuditNotificationSecretRows.rowCount, samples: missingPlatformAuditNotificationSecretRows.rows.slice(0, 5).map(row => ({ id: row.id, channelCode: row.channel_code, provider: row.provider, })), }); } else { pass('db.platform_audit_notification_secrets', 'Signed platform audit notification channels have private secret rows'); } const unsafePlatformDunningNotificationRows = await pool.query(` select id, channel_code, provider, webhook_url from public.platform_dunning_notification_channels where enabled = true and ( webhook_url !~* '^https://' or webhook_url ~* '^https?://(localhost|127\\.0\\.0\\.1|\\[?::1\\]?)' ) `); if (unsafePlatformDunningNotificationRows.rowCount > 0) { block('db.platform_dunning_notification_webhooks', 'Enabled platform dunning notification webhooks must use production HTTPS URLs', { count: unsafePlatformDunningNotificationRows.rowCount, samples: unsafePlatformDunningNotificationRows.rows.slice(0, 5).map(row => ({ id: row.id, channelCode: row.channel_code, provider: row.provider, })), }); } else { pass('db.platform_dunning_notification_webhooks', 'Enabled platform dunning notification webhooks use production HTTPS URLs'); } const missingPlatformDunningNotificationSecretRows = await pool.query(` select c.id, c.channel_code, c.provider, c.secret_ref from public.platform_dunning_notification_channels c left join app_private.platform_secrets s on s.secret_scope = split_part(c.secret_ref, ':', 2) and s.secret_key = split_part(c.secret_ref, ':', 3) where c.enabled = true and c.provider in ('dingtalk', 'feishu') and (c.secret_ref is null or c.secret_ref !~ '^app_private\\.platform_secrets:' or s.id is null) `); if (missingPlatformDunningNotificationSecretRows.rowCount > 0) { block('db.platform_dunning_notification_secrets', 'Signed platform dunning notification channels require app_private.platform_secrets rows', { count: missingPlatformDunningNotificationSecretRows.rowCount, samples: missingPlatformDunningNotificationSecretRows.rows.slice(0, 5).map(row => ({ id: row.id, channelCode: row.channel_code, provider: row.provider, })), }); } else { pass('db.platform_dunning_notification_secrets', 'Signed platform dunning notification channels have private secret rows'); } const unverifiedDomainRows = await pool.query(` select count(*)::int as count from public.tenant_domains where status not in ('active', 'verified') `); const unverifiedDomains = Number(unverifiedDomainRows.rows[0]?.count || 0); if (unverifiedDomains > 0) warn('db.tenant_domains', 'Some tenant domains are not active/verified', { count: unverifiedDomains }); else pass('db.tenant_domains', 'Tenant domains are active/verified or not configured'); const tenantTablesWithoutRls = await pool.query(` select c.table_schema, c.table_name from information_schema.columns c join pg_class cls on cls.relname = c.table_name join pg_namespace ns on ns.oid = cls.relnamespace and ns.nspname = c.table_schema where c.column_name = 'tenant_id' and c.table_schema in ('public', 'app_private') and cls.relkind in ('r', 'p') and not cls.relrowsecurity order by c.table_schema, c.table_name `); if (tenantTablesWithoutRls.rowCount > 0) { block('db.rls.tenant_tables_enabled', 'Every tenant-scoped table must have row level security enabled', { count: tenantTablesWithoutRls.rowCount, samples: tenantTablesWithoutRls.rows.slice(0, 10).map(formatTableName), }); } else { pass('db.rls.tenant_tables_enabled', 'All tenant-scoped tables have RLS enabled'); } const tenantTablesWithoutPolicies = await pool.query(` select c.table_schema, c.table_name from information_schema.columns c join pg_class cls on cls.relname = c.table_name join pg_namespace ns on ns.oid = cls.relnamespace and ns.nspname = c.table_schema where c.column_name = 'tenant_id' and c.table_schema in ('public', 'app_private') and cls.relkind in ('r', 'p') and not exists ( select 1 from pg_policy p where p.polrelid = cls.oid ) order by c.table_schema, c.table_name `); if (tenantTablesWithoutPolicies.rowCount > 0) { block('db.rls.tenant_tables_policy', 'Every tenant-scoped table must have at least one RLS policy', { count: tenantTablesWithoutPolicies.rowCount, samples: tenantTablesWithoutPolicies.rows.slice(0, 10).map(formatTableName), }); } else { pass('db.rls.tenant_tables_policy', 'All tenant-scoped tables have at least one RLS policy'); } const publicTenantTablesWithoutTenantContextPolicy = await pool.query(` select c.table_schema, c.table_name from information_schema.columns c join pg_class cls on cls.relname = c.table_name join pg_namespace ns on ns.oid = cls.relnamespace and ns.nspname = c.table_schema where c.column_name = 'tenant_id' and c.table_schema = 'public' and cls.relkind in ('r', 'p') and not exists ( select 1 from pg_policy p where p.polrelid = cls.oid and ( lower(pg_get_expr(p.polqual, p.polrelid)) like '%app.current_tenant_id()%' or lower(pg_get_expr(p.polwithcheck, p.polrelid)) like '%app.current_tenant_id()%' ) ) order by c.table_schema, c.table_name `); if (publicTenantTablesWithoutTenantContextPolicy.rowCount > 0) { block( 'db.rls.public_tenant_context', 'Every public tenant-scoped table must include app.current_tenant_id() in an RLS policy', { count: publicTenantTablesWithoutTenantContextPolicy.rowCount, samples: publicTenantTablesWithoutTenantContextPolicy.rows.slice(0, 10).map(formatTableName), }, ); } else { pass('db.rls.public_tenant_context', 'Public tenant-scoped tables include tenant context in RLS policies'); } } finally { await pool.end(); } } function printSummary() { const summary = checks.reduce( (acc, item) => { acc[item.status] += 1; return acc; }, { blocker: 0, warn: 0, pass: 0 }, ); if (jsonOutput) { console.log(JSON.stringify({ summary, checks }, null, 2)); return; } console.log('Production readiness check'); console.log(`Env file: ${fs.existsSync(envFile) ? envFile : '(not found, using process env)'}`); console.log(`Host: ${os.hostname()}`); console.log(`Summary: ${summary.blocker} blocker(s), ${summary.warn} warning(s), ${summary.pass} pass(es)`); for (const item of checks) { const marker = item.status === 'blocker' ? 'BLOCK' : item.status === 'warn' ? 'WARN ' : 'PASS '; console.log(`[${marker}] ${item.id}: ${item.message}`); } } async function main() { loadEnvFile(envFile); validateEnv(); await validateDatabase(); printSummary(); if (checks.some(item => item.status === 'blocker')) { process.exitCode = 1; } } main().catch(error => { console.error(error instanceof Error ? error.message : error); process.exitCode = 1; });