import { createRequire } from 'node:module'; import { $OpenApiUtil } from '@alicloud/openapi-core'; import { RuntimeOptions } from '@darabonba/typescript'; import { config } from '../../utils/config.js'; import { AppError, ValidationError } from '../../utils/errors.js'; // 阿里云 SDK 是 Darabonba 生成的 CJS 模块,ESM import 在不同运行时下行为不一致 // (Bun / Bun node 兼容模式 / Node.js 对 __esModule 和 exports.default 解析不同) // 使用 createRequire 直接走 CJS require 路径,绕开所有 ESM/CJS 互操作问题 const require = createRequire(import.meta.url); const DypnsClient = require('@alicloud/dypnsapi20170525').default; const { SendSmsVerifyCodeRequest, CheckSmsVerifyCodeRequest } = require('@alicloud/dypnsapi20170525/dist/models/model.js'); type SmsOperation = 'send' | 'verify'; type AliyunSdkError = { code?: unknown; message?: unknown; data?: { Code?: unknown; Message?: unknown; }; }; function createClient() { const openApiConfig = new $OpenApiUtil.Config({ accessKeyId: config.ALIYUN_ACCESS_KEY_ID, accessKeySecret: config.ALIYUN_ACCESS_KEY_SECRET, endpoint: 'dypnsapi.aliyuncs.com', }); return new DypnsClient(openApiConfig); } function assertSmsConfigured(): void { if ( !config.ALIYUN_ACCESS_KEY_ID || !config.ALIYUN_ACCESS_KEY_SECRET || !config.ALIYUN_SMS_SIGN_NAME || !config.ALIYUN_SMS_TEMPLATE_CODE ) { throw new AppError('SMS service is not configured on the server', 503, 'SERVICE_UNAVAILABLE'); } } function getString(value: unknown): string | undefined { return typeof value === 'string' && value.length > 0 ? value : undefined; } function invalidVerifyCodeError(): AppError { return new AppError('验证码无效或已过期', 401, 'INVALID_VERIFY_CODE'); } function mapAliyunError(code: string, message: string, operation: SmsOperation): AppError { switch (code) { case 'MOBILE_NUMBER_ILLEGAL': return new ValidationError('Invalid phone number'); case 'isv.ValidateFail': return operation === 'verify' ? invalidVerifyCodeError() : new AppError('SMS service error: verification failed', 502, 'SMS_SERVICE_ERROR'); case 'BUSINESS_LIMIT_CONTROL': case 'FREQUENCY_FAIL': return new AppError('Too many SMS requests, please try again later', 429, 'RATE_LIMITED'); case 'isv.BUSINESS_LIMIT_CONTROL': return new AppError('SMS daily limit reached for this number', 429, 'RATE_LIMITED'); case 'biz.FREQUENCY': return new AppError('Too many SMS requests, please try again later', 429, 'RATE_LIMITED'); case 'InternalError': return new AppError('SMS provider temporarily unavailable, please try again', 503, 'SMS_PROVIDER_ERROR'); case 'INVALID_PARAMETERS': return new ValidationError('Invalid request parameters'); default: return new AppError( `SMS service error: ${message || code}`, 502, 'SMS_SERVICE_ERROR', ); } } export function mapAliyunException(error: unknown, operation: SmsOperation): AppError { if (error instanceof AppError) return error; if (typeof error === 'object' && error !== null) { const sdkError = error as AliyunSdkError; const code = getString(sdkError.data?.Code) ?? getString(sdkError.code); const message = getString(sdkError.data?.Message) ?? getString(sdkError.message); if (code) { return mapAliyunError(code, message ?? code, operation); } } return new AppError('SMS service error: request failed', 502, 'SMS_SERVICE_ERROR'); } const RUNTIME_OPTIONS = new RuntimeOptions({ readTimeout: 10000, connectTimeout: 5000, }); export async function sendCode(phoneNumber: string): Promise { assertSmsConfigured(); const request = new SendSmsVerifyCodeRequest({ phoneNumber, signName: config.ALIYUN_SMS_SIGN_NAME, templateCode: config.ALIYUN_SMS_TEMPLATE_CODE, templateParam: config.ALIYUN_SMS_TEMPLATE_PARAM, }); const response = await createClient().sendSmsVerifyCodeWithOptions(request, RUNTIME_OPTIONS).catch((error: unknown) => { throw mapAliyunException(error, 'send'); }); if (!response.body || response.body.code !== 'OK') { throw mapAliyunError( response.body?.code ?? 'UNKNOWN', response.body?.message ?? 'Unknown SMS error', 'send', ); } } export async function verifyCode(phoneNumber: string, code: string): Promise { assertSmsConfigured(); const request = new CheckSmsVerifyCodeRequest({ phoneNumber, verifyCode: code, caseAuthPolicy: 1, }); const response = await createClient().checkSmsVerifyCodeWithOptions(request, RUNTIME_OPTIONS).catch((error: unknown) => { throw mapAliyunException(error, 'verify'); }); if (!response.body || response.body.code !== 'OK') { throw mapAliyunError( response.body?.code ?? 'UNKNOWN', response.body?.message ?? 'Unknown SMS verification error', 'verify', ); } if (response.body.model?.verifyResult !== 'PASS') { throw invalidVerifyCodeError(); } }