From 1c2ce38cea7f08f96bbbcacb32f1554b98ba952a Mon Sep 17 00:00:00 2001 From: Codex Date: Sat, 4 Jul 2026 00:47:36 +0800 Subject: [PATCH] fix: align SMS expiry with PNVS valid time --- apps/api/src/features/auth/providers.ts | 10 +++++++++- apps/api/src/features/auth/routes.ts | 7 +++++-- scripts/aliyun-pnvs-provider-contract-test.js | 2 ++ 3 files changed, 16 insertions(+), 3 deletions(-) diff --git a/apps/api/src/features/auth/providers.ts b/apps/api/src/features/auth/providers.ts index 91366cf0..ad8625a8 100644 --- a/apps/api/src/features/auth/providers.ts +++ b/apps/api/src/features/auth/providers.ts @@ -29,6 +29,7 @@ export interface SmsSendResult { provider: SmsProviderName; status: 'sent' | 'mocked'; verification?: 'local' | 'provider'; + ttlSeconds?: number; providerMessageId?: string; raw?: Record; } @@ -242,6 +243,11 @@ function optionalIntegerString(config: TenantAuthProviderConfig, keys: string[], return fallback; } +function positiveInteger(value: string, fallback: number) { + const parsed = Number(value); + return Number.isFinite(parsed) && parsed > 0 ? Math.trunc(parsed) : fallback; +} + class AliyunPnvsSmsProvider implements SmsProvider { readonly name = 'aliyun-pnvs' as const; @@ -250,6 +256,7 @@ class AliyunPnvsSmsProvider implements SmsProvider { async send(input: SmsSendInput): Promise { const signName = requirePublicString(this.providerConfig, ['signName'], 'SMS_PUBLIC_CONFIG_REQUIRED'); const templateCode = requirePublicString(this.providerConfig, ['templateCode'], 'SMS_PUBLIC_CONFIG_REQUIRED'); + const validTime = optionalIntegerString(this.providerConfig, ['validTime'], String(input.ttlSeconds)) || String(input.ttlSeconds); const params: Record = { CountryCode: optionalPublicString(this.providerConfig, ['countryCode']) || '86', PhoneNumber: input.phone, @@ -259,7 +266,7 @@ class AliyunPnvsSmsProvider implements SmsProvider { OutId: input.outId, CodeType: optionalIntegerString(this.providerConfig, ['codeType'], '1') || '1', CodeLength: optionalIntegerString(this.providerConfig, ['codeLength'], '6') || '6', - ValidTime: optionalIntegerString(this.providerConfig, ['validTime'], String(input.ttlSeconds)) || String(input.ttlSeconds), + ValidTime: validTime, DuplicatePolicy: optionalIntegerString(this.providerConfig, ['duplicatePolicy'], '1') || '1', Interval: optionalIntegerString(this.providerConfig, ['interval'], String(input.cooldownSeconds)) || String(input.cooldownSeconds), }; @@ -286,6 +293,7 @@ class AliyunPnvsSmsProvider implements SmsProvider { provider: this.name, status: 'sent', verification: 'provider', + ttlSeconds: positiveInteger(validTime, input.ttlSeconds), providerMessageId: typeof model.BizId === 'string' ? model.BizId : undefined, raw: { requestId: raw.RequestId, diff --git a/apps/api/src/features/auth/routes.ts b/apps/api/src/features/auth/routes.ts index 29647d5f..61ae3c1e 100644 --- a/apps/api/src/features/auth/routes.ts +++ b/apps/api/src/features/auth/routes.ts @@ -307,8 +307,11 @@ export async function sendSmsCodeRoute(ctx: RequestContext) { cooldownSeconds: config.authSmsCooldownSeconds, metadata, }); + const ttlSeconds = Number.isFinite(providerResult.ttlSeconds) && Number(providerResult.ttlSeconds) > 0 + ? Math.trunc(Number(providerResult.ttlSeconds)) + : config.authCodeTtlSeconds; const codeHash = hashSmsCode(tenantId, phone, purpose, code); - const expiresAt = new Date(Date.now() + config.authCodeTtlSeconds * 1000).toISOString(); + const expiresAt = new Date(Date.now() + ttlSeconds * 1000).toISOString(); const item = await transaction(async client => { const insertResult = await client.query( @@ -354,7 +357,7 @@ export async function sendSmsCodeRoute(ctx: RequestContext) { return { item, - expireIn: config.authCodeTtlSeconds, + expireIn: ttlSeconds, cooldown: config.authSmsCooldownSeconds, debugCode: provider.name === 'mock' && !config.isProduction ? code : undefined, }; diff --git a/scripts/aliyun-pnvs-provider-contract-test.js b/scripts/aliyun-pnvs-provider-contract-test.js index ce4cf4e3..f859b201 100644 --- a/scripts/aliyun-pnvs-provider-contract-test.js +++ b/scripts/aliyun-pnvs-provider-contract-test.js @@ -18,7 +18,9 @@ assert.match(providerSource, /SendSmsVerifyCode/, 'PNVS provider should call Sen assert.match(providerSource, /CheckSmsVerifyCode/, 'PNVS provider should call CheckSmsVerifyCode'); 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(providerSource, /ttlSeconds:\s*positiveInteger\(validTime,\s*input\.ttlSeconds\)/, 'PNVS provider should expose provider ValidTime as send result ttlSeconds'); assert.match(routeSource, /aliyun-pnvs/, 'auth routes should recognize aliyun-pnvs aliases'); +assert.match(routeSource, /const ttlSeconds =[\s\S]*providerResult\.ttlSeconds[\s\S]*expireIn: ttlSeconds/, 'auth routes should persist and return provider-specific SMS TTL'); 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');