From cd0f465689c1e3f962ba6a949a4b41c795fc1e94 Mon Sep 17 00:00:00 2001 From: Codex Date: Fri, 3 Jul 2026 22:54:36 +0800 Subject: [PATCH] fix: default tenant PNVS secrets to sms scope --- apps/api/src/features/tenant-admin/routes.ts | 13 +++++++++- scripts/aliyun-pnvs-provider-contract-test.js | 2 ++ scripts/api-integration-test.js | 25 +++++++++++++++++++ 3 files changed, 39 insertions(+), 1 deletion(-) diff --git a/apps/api/src/features/tenant-admin/routes.ts b/apps/api/src/features/tenant-admin/routes.ts index 9836099c..c0f9202b 100644 --- a/apps/api/src/features/tenant-admin/routes.ts +++ b/apps/api/src/features/tenant-admin/routes.ts @@ -379,6 +379,17 @@ function secretRef(scope: SecretScope, secretKey: string) { return `app_private.tenant_secrets:${scope}:${secretKey}`; } +function defaultAuthSecretScope(provider: string): SecretScope { + const normalized = provider.toLowerCase().replace(/[_\s]/g, '-'); + if ( + normalized.includes('sms') || + normalized === 'aliyun' || + normalized === 'aliyun-pnvs' || + normalized === 'tencent' + ) return 'sms'; + return 'oauth'; +} + function parseSecretScope(value: unknown, fallback: SecretScope): SecretScope { const candidate = (nullableString(value) || fallback) as SecretScope; if (!SECRET_SCOPES.has(candidate)) { @@ -1386,7 +1397,7 @@ export async function upsertAuthProviderRoute(ctx: RequestContext) { requireTenantPermission(auth, 'tenant:auth:write'); const body = await readJsonBody(ctx); const provider = requiredString(body, 'provider'); - const fallbackScope = provider.toLowerCase().includes('sms') ? 'sms' : 'oauth'; + const fallbackScope = defaultAuthSecretScope(provider); const secretPayload = parseSecretPayload(body, fallbackScope, provider, provider); const configPublic = objectValue(body.configPublic); diff --git a/scripts/aliyun-pnvs-provider-contract-test.js b/scripts/aliyun-pnvs-provider-contract-test.js index d7ebc76b..0af6598f 100644 --- a/scripts/aliyun-pnvs-provider-contract-test.js +++ b/scripts/aliyun-pnvs-provider-contract-test.js @@ -4,6 +4,7 @@ import path from 'node:path'; const providerSource = fs.readFileSync(path.join(process.cwd(), 'apps/api/src/features/auth/providers.ts'), 'utf8'); const routeSource = fs.readFileSync(path.join(process.cwd(), 'apps/api/src/features/auth/routes.ts'), 'utf8'); +const tenantAdminRouteSource = fs.readFileSync(path.join(process.cwd(), 'apps/api/src/features/tenant-admin/routes.ts'), 'utf8'); const deployEnvExample = fs.readFileSync(path.join(process.cwd(), 'scripts/deploy/env/api.env.example'), 'utf8'); const providerDoc = fs.readFileSync(path.join(process.cwd(), 'docs/refactor/auth-payment-provider-plan.md'), 'utf8'); const launchChecklist = fs.readFileSync(path.join(process.cwd(), 'docs/refactor/web-launch-acceptance-checklist.md'), 'utf8'); @@ -19,6 +20,7 @@ assert.match(routeSource, /aliyun-pnvs/, 'auth routes should recognize aliyun-pn 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(tenantProviderConfigSource, /normalized === 'aliyun-pnvs'[\s\S]*return 'sms'/, 'PNVS secrets should default to sms scope'); +assert.match(tenantAdminRouteSource, /normalized === 'aliyun-pnvs'[\s\S]*return 'sms'/, 'tenant admin PNVS secrets should default to sms scope'); assert.match(deployEnvExample, /AUTH_SMS_PROVIDER=aliyun-pnvs/, 'deploy env example should prefer aliyun-pnvs'); assert.match(readinessSource, /aliyunPnvs/, 'production readiness should validate aliyun-pnvs public config'); assert.match(providerDoc, /SendSmsVerifyCode/, 'provider doc should document PNVS send action'); diff --git a/scripts/api-integration-test.js b/scripts/api-integration-test.js index 275e73a8..1c628fff 100644 --- a/scripts/api-integration-test.js +++ b/scripts/api-integration-test.js @@ -7996,6 +7996,31 @@ async function testTenantAdminOps() { assert.equal(authProvider.item?.secret?.hasSecretValue, true, 'auth provider should report masked secret status'); assert.ok(!JSON.stringify(authProvider).includes('wechat-app-secret-smoke'), 'auth provider response must not include secret plaintext'); + const pnvsAuthProvider = await request('/api/tenant-admin/auth-providers', { + userId: TENANT_ADMIN_USER_ID, + method: 'PUT', + body: { + provider: 'aliyun-pnvs', + displayName: '阿里云短信认证', + status: 'testing', + configPublic: { + signName: '短信签名', + templateCode: 'SMS_123456789', + endpoint: 'https://dypnsapi.aliyuncs.com', + }, + secret: { + secretJson: { + accessKeyId: 'pnvs-access-key-id', + accessKeySecret: 'pnvs-access-key-secret', + }, + }, + }, + }); + assert.equal(pnvsAuthProvider.item?.provider, 'aliyun-pnvs', 'tenant admin should upsert PNVS auth provider'); + assert.equal(pnvsAuthProvider.item?.configPublic?.secretRef, 'app_private.tenant_secrets:sms:aliyun-pnvs', 'PNVS auth provider should default secrets to sms scope'); + assert.equal(pnvsAuthProvider.item?.secret?.hasSecretJson, true, 'PNVS auth provider should report masked JSON secret status'); + assert.ok(!JSON.stringify(pnvsAuthProvider).includes('pnvs-access-key-secret'), 'PNVS auth provider response must not include secret plaintext'); + const miniappLogin = await request('/api/auth/oauth/wechat-miniapp', { userId: false, method: 'POST',