import { isWriteOutcomeUnknown } from "@/utils/request-outcome.js"; export const AUTH_VERIFICATION_OPERATION = Object.freeze({ PASSWORD_LOGIN: "password-login", SMS_LOGIN: "sms-login", REGISTER: "register", FORGOT_PASSWORD: "forgot-password", PHONE_CHANGE: "phone-change", ACCOUNT_DEACTIVATE: "account-deactivate", }); const SUPPORTED_TAC_TYPES = new Set([ "SLIDER", "ROTATE", "CONCAT", "WORD_IMAGE_CLICK", ]); const PHONE_PATTERN = /^1[3-9]\d{9}$/; export const isAuthPhone = (value) => typeof value === "string" && PHONE_PATTERN.test(value); const contractError = (message, code) => { const error = new Error(message); error.code = code; return error; }; const requireText = (value, label) => { if (typeof value !== "string" || !value.trim()) { throw contractError(`${label}不能为空`, "AUTH_TAC_CONTRACT_INVALID"); } return value.trim(); }; export const assertSmsCode = (value) => { if (typeof value !== "string" || !/^\d{4}$/.test(value)) { throw contractError("请输入 4 位短信验证码", "SMS_CODE_INVALID"); } return value; }; const assertAuthVerificationOperation = (operationCode) => { if (!Object.values(AUTH_VERIFICATION_OPERATION).includes(operationCode)) { throw contractError("认证动作不属于当前认证合同", "AUTH_OPERATION_INVALID"); } return operationCode; }; export const normalizeCaptchaRequirement = (value) => { if (!value || typeof value !== "object" || Array.isArray(value)) { throw contractError("行为验证策略响应无效", "AUTH_TAC_REQUIREMENT_INVALID"); } const sceneCode = requireText(value.sceneCode, "行为验证场景"); if (value.required === false) { return { required: false, sceneCode }; } if (value.required !== true) { throw contractError( "行为验证策略缺少 required 布尔值", "AUTH_TAC_POLICY_INCOMPLETE", ); } const providerCode = requireText(value.providerCode, "行为验证服务商").toUpperCase(); if (providerCode !== "TIANAI") { throw contractError("当前仅支持 TIANAI 行为验证服务", "AUTH_TAC_PROVIDER_UNSUPPORTED"); } const captchaType = requireText(value.captchaType, "行为验证码类型").toUpperCase(); if (!SUPPORTED_TAC_TYPES.has(captchaType)) { throw contractError("服务端返回了客户端不支持的验证码类型", "AUTH_TAC_TYPE_UNSUPPORTED"); } const ttlSeconds = Number(value.ttlSeconds); if (!Number.isInteger(ttlSeconds) || ttlSeconds < 1) { throw contractError("行为验证策略缺少有效期", "AUTH_TAC_TTL_INVALID"); } return { required: true, providerCode, captchaType, sceneCode, ttlSeconds }; }; export const createTacRenderContext = ({ requestId, baseUrl, clientId, tenantId, operationCode, subject, requirement, }) => { const normalizedRequirement = normalizeCaptchaRequirement(requirement); if (!normalizedRequirement.required) { throw contractError("当前认证动作无需行为验证", "AUTH_TAC_NOT_REQUIRED"); } const normalizedBaseUrl = requireText(baseUrl, "后端地址").replace(/\/+$/, ""); if (!/^https:\/\/[^/]+/i.test(normalizedBaseUrl)) { throw contractError("行为验证只允许使用 HTTPS 后端地址", "AUTH_TAC_HTTPS_REQUIRED"); } const normalizedSubject = requireText(subject, "手机号"); if (!isAuthPhone(normalizedSubject)) { throw contractError("请输入正确的手机号", "AUTH_PHONE_INVALID"); } return { requestId: requireText(requestId, "验证请求标识"), baseUrl: normalizedBaseUrl, challengeUrl: `${normalizedBaseUrl}/genealogy/app/auth/verification/${assertAuthVerificationOperation(operationCode)}/challenge`, verifyUrl: `${normalizedBaseUrl}/genealogy/app/auth/verification/${assertAuthVerificationOperation(operationCode)}/verify`, clientId: requireText(clientId, "客户端标识"), tenantId: requireText(tenantId, "租户标识"), operationCode, subject: normalizedSubject, providerCode: normalizedRequirement.providerCode, captchaType: normalizedRequirement.captchaType, }; }; export const normalizeTacSuccess = (value, expectedRequestId) => { if (!value || typeof value !== "object" || value.requestId !== expectedRequestId) { throw contractError("行为验证结果已过期或与当前请求不匹配", "AUTH_TAC_RESULT_STALE"); } const validToken = requireText(value.validToken, "行为验证票据"); const expireSeconds = Number(value.expireSeconds); if (!Number.isInteger(expireSeconds) || expireSeconds < 1) { throw contractError("行为验证票据缺少有效期", "AUTH_TAC_RESULT_INVALID"); } return { requestId: value.requestId, validToken, expireSeconds }; }; export const isSmsDeliveryOutcomeUnknown = isWriteOutcomeUnknown;