From 3a10daf8472672c49ea616dd0f4690be3386fcb8 Mon Sep 17 00:00:00 2001 From: Codex Date: Sat, 4 Jul 2026 00:27:56 +0800 Subject: [PATCH] fix: normalize PNVS SMS provider aliases --- apps/api/src/core/config.ts | 4 +-- apps/api/src/features/auth/routes.ts | 2 +- scripts/aliyun-pnvs-provider-contract-test.js | 1 + scripts/production-config-failfast-test.js | 6 ++++ scripts/production-readiness-check-test.js | 28 +++++++++++++++++++ scripts/production-readiness-check.js | 4 +-- 6 files changed, 38 insertions(+), 7 deletions(-) diff --git a/apps/api/src/core/config.ts b/apps/api/src/core/config.ts index 39ead072..51e73955 100644 --- a/apps/api/src/core/config.ts +++ b/apps/api/src/core/config.ts @@ -55,10 +55,8 @@ const DEFAULT_MAX_IMPORT_JSON_BODY_BYTES = 10 * 1024 * 1024; const HARD_MAX_JSON_BODY_BYTES = 50 * 1024 * 1024; const PRODUCTION_SMS_PROVIDERS = new Set([ 'aliyun-pnvs', - 'aliyun_pnvs', 'aliyun-pnvs-sms', 'aliyun-sms-auth', - 'aliyun_sms_auth', ]); const PRODUCTION_STORAGE_PROVIDERS = new Set(['aliyun_oss', 'tencent_cos', 'supabase_storage']); @@ -107,7 +105,7 @@ function validateProductionConfig(nextConfig: ApiConfig) { const failures: string[] = []; if (nextConfig.corsOrigins.includes('*')) failures.push('CORS_ORIGIN must not include * in production'); - if (!PRODUCTION_SMS_PROVIDERS.has(nextConfig.authSmsProvider.trim().toLowerCase())) { + if (!PRODUCTION_SMS_PROVIDERS.has(nextConfig.authSmsProvider.trim().toLowerCase().replace(/[_\s]/g, '-'))) { failures.push('AUTH_SMS_PROVIDER must be aliyun-pnvs in production'); } if (isUnsafeSecret(nextConfig.authCodePepper, DEFAULT_AUTH_CODE_PEPPER)) { diff --git a/apps/api/src/features/auth/routes.ts b/apps/api/src/features/auth/routes.ts index 268e3d69..29647d5f 100644 --- a/apps/api/src/features/auth/routes.ts +++ b/apps/api/src/features/auth/routes.ts @@ -208,7 +208,7 @@ async function consumeSmsCode( } function normalizeSmsProviderName(value: string) { - const normalized = value.toLowerCase().replace(/_/g, '-'); + const normalized = value.toLowerCase().replace(/[_\s]/g, '-'); if (normalized === 'aliyun-pnvs' || normalized === 'aliyun-pnvs-sms' || normalized === 'aliyun-sms-auth') return 'aliyun-pnvs'; if (normalized === 'aliyun' || normalized === 'aliyun-sms') return 'aliyun'; if (normalized === 'tencent' || normalized === 'tencent-sms') return 'tencent'; diff --git a/scripts/aliyun-pnvs-provider-contract-test.js b/scripts/aliyun-pnvs-provider-contract-test.js index f4774d17..b543705d 100644 --- a/scripts/aliyun-pnvs-provider-contract-test.js +++ b/scripts/aliyun-pnvs-provider-contract-test.js @@ -19,6 +19,7 @@ assert.match(providerSource, /CheckSmsVerifyCode/, 'PNVS provider should call Ch assert.match(providerSource, /VerifyResult.*PASS/s, 'PNVS verify should require provider PASS result'); assert.match(providerSource, /##code##/, 'PNVS provider should preserve Aliyun-generated code placeholder'); assert.match(routeSource, /aliyun-pnvs/, 'auth routes should recognize aliyun-pnvs aliases'); +assert.match(routeSource, /replace\(\s*\/\[_\\s\]\/g,\s*'-'\s*\)/, 'auth routes should normalize underscore PNVS provider aliases before runtime selection'); assert.match(routeSource, /verification.*provider/s, 'auth routes should store provider verification mode'); assert.match(routeSource, /provider\.verify/, 'auth routes should delegate PNVS verification to provider'); assert.match(routeSource, /export async function bindPhoneRoute[\s\S]*activeSmsProvider[\s\S]*consumeSmsCode/, 'phone binding should reuse active SMS provider verification'); diff --git a/scripts/production-config-failfast-test.js b/scripts/production-config-failfast-test.js index 585ebcd9..f17adf75 100644 --- a/scripts/production-config-failfast-test.js +++ b/scripts/production-config-failfast-test.js @@ -120,6 +120,12 @@ assert.match( const safeApi = runImport(apiConfigUrl, safeApiEnv); assert.equal(safeApi.status, 0, `safe production API config should load: ${safeApi.output}`); +const safeApiUnderscorePnvsAlias = runImport(apiConfigUrl, { + ...safeApiEnv, + AUTH_SMS_PROVIDER: 'aliyun_pnvs', +}); +assert.equal(safeApiUnderscorePnvsAlias.status, 0, `underscore PNVS alias should load in production API config: ${safeApiUnderscorePnvsAlias.output}`); + const unsafeWorker = runImport(workerConfigUrl, { ...safeBaseEnv, STORAGE_DEFAULT_PROVIDER: 'local_dev', diff --git a/scripts/production-readiness-check-test.js b/scripts/production-readiness-check-test.js index a82c8b2f..1b331679 100644 --- a/scripts/production-readiness-check-test.js +++ b/scripts/production-readiness-check-test.js @@ -196,6 +196,34 @@ WORKER_PUBLIC_BANK_SYNC_BATCH_SIZE=5 assert.equal(safeAliyunPnvs.status, 0, `aliyun-pnvs readiness should pass without blockers: ${safeAliyunPnvs.stdout} ${safeAliyunPnvs.stderr}`); assert.equal(safeAliyunPnvs.payload.summary?.blocker, 0, 'aliyun-pnvs readiness should have no blockers'); +const safeAliyunPnvsUnderscoreAlias = runReadiness(` +NODE_ENV=production +DATABASE_URL=postgresql://prod_user:prod_password@db.prod.internal:5432/tiku +CORS_ORIGIN=https://student.gongxue100.com,https://tenant-admin.gongxue100.com,https://platform-admin.gongxue100.com +AUTH_SMS_PROVIDER=aliyun_pnvs +AUTH_CODE_PEPPER=${strongSecretA} +AUTH_SESSION_SECRET=${strongSecretB} +AUTH_JWT_JWKS_URL=https://auth.gongxue100.com/auth/v1/.well-known/jwks.json +AUTH_JWT_ISSUER=https://auth.gongxue100.com/auth/v1 +ALLOW_LEGACY_AUTH_HEADERS=false +ALLOW_PLATFORM_ADMIN_KEY=false +PLATFORM_ADMIN_API_KEY=${strongSecretC} +STORAGE_DEFAULT_PROVIDER=aliyun_oss +STORAGE_DEFAULT_BUCKET=tiku-assets +STORAGE_REQUIRE_TENANT_PREFIX=true +ALIYUN_OSS_REGION=cn-hangzhou +ALIYUN_OSS_ENDPOINT=https://oss-cn-hangzhou.aliyuncs.com +ALIYUN_OSS_ACCESS_KEY_ID=LTAI_READINESS_TEST_ONLY +ALIYUN_OSS_ACCESS_KEY_SECRET=aliyun-readiness-secret-placeholder +WORKER_ASSET_SECURITY_SCANNER=metadata_rules,http +WORKER_ASSET_SECURITY_SCAN_HTTP_ENDPOINT=https://scanner.gongxue100.com/api/scan +WORKER_ASSET_SECURITY_SCAN_HTTP_TOKEN=s3cure-asset-scanner-token-2026-06-29-stuvwx +WORKER_ASSET_SECURITY_SCAN_FAIL_OPEN=false +`); + +assert.equal(safeAliyunPnvsUnderscoreAlias.status, 0, `aliyun_pnvs readiness should pass without blockers: ${safeAliyunPnvsUnderscoreAlias.stdout} ${safeAliyunPnvsUnderscoreAlias.stderr}`); +assert.equal(safeAliyunPnvsUnderscoreAlias.payload.summary?.blocker, 0, 'aliyun_pnvs readiness should have no blockers'); + const pnvsTemplateParamWarning = runReadiness( ` NODE_ENV=production diff --git a/scripts/production-readiness-check.js b/scripts/production-readiness-check.js index 6ccb5edf..ff216224 100644 --- a/scripts/production-readiness-check.js +++ b/scripts/production-readiness-check.js @@ -12,10 +12,8 @@ const DEFAULT_PLATFORM_ADMIN_API_KEY = 'local-platform-admin-key'; const HARD_MAX_JSON_BODY_BYTES = 50 * 1024 * 1024; const PRODUCTION_SMS_PROVIDERS = new Set([ 'aliyun-pnvs', - 'aliyun_pnvs', 'aliyun-pnvs-sms', 'aliyun-sms-auth', - 'aliyun_sms_auth', ]); const PRODUCTION_STORAGE_PROVIDERS = new Set(['aliyun_oss', 'tencent_cos', 'supabase_storage']); const AUTH_PROVIDER_ALIASES = { @@ -567,7 +565,7 @@ function validateEnv() { } } - const authSmsProvider = env('AUTH_SMS_PROVIDER', 'mock').trim().toLowerCase(); + const authSmsProvider = normalizeProvider(env('AUTH_SMS_PROVIDER', 'mock')).replace(/[_\s]/g, '-'); if (!PRODUCTION_SMS_PROVIDERS.has(authSmsProvider)) { block('env.auth_sms_provider', 'AUTH_SMS_PROVIDER must be aliyun-pnvs in production', { provider: authSmsProvider || '(empty)',