162 lines
6.5 KiB
JavaScript
162 lines
6.5 KiB
JavaScript
(function attachJiapuTacAdapter(global) {
|
||
"use strict";
|
||
|
||
var SUPPORTED_TYPES = ["SLIDER", "ROTATE", "CONCAT", "WORD_IMAGE_CLICK"];
|
||
|
||
function fail(message, code) {
|
||
var error = new Error(message);
|
||
error.code = code || "TAC_PROTOCOL_INVALID";
|
||
throw error;
|
||
}
|
||
|
||
function text(value, label) {
|
||
if (typeof value !== "string" || !value.trim()) fail(label + "不能为空");
|
||
return value.trim();
|
||
}
|
||
|
||
function positiveInteger(value, label) {
|
||
var number = Number(value);
|
||
if (!Number.isInteger(number) || number < 1) fail(label + "必须是正整数");
|
||
return number;
|
||
}
|
||
|
||
function assertContext(context) {
|
||
if (!context || typeof context !== "object") fail("TAC 请求上下文无效");
|
||
return {
|
||
tenantId: text(context.tenantId, "租户标识"),
|
||
operationCode: text(context.operationCode, "认证动作"),
|
||
subject: text(context.subject, "验证主体"),
|
||
providerCode: text(context.providerCode, "验证服务商").toUpperCase(),
|
||
captchaType: text(context.captchaType, "验证码类型").toUpperCase(),
|
||
};
|
||
}
|
||
|
||
function assertChallengePayload(type, payload) {
|
||
if (!payload || typeof payload !== "object" || Array.isArray(payload)) {
|
||
fail("行为验证挑战缺少渲染载荷");
|
||
}
|
||
if (type === "CONCAT") {
|
||
text(payload.backgroundImage, "拼图背景图片");
|
||
positiveInteger(payload.backgroundImageHeight, "拼图背景高度");
|
||
if (!payload.data || !Number.isFinite(Number(payload.data.randomY))) {
|
||
fail("拼图挑战缺少 randomY");
|
||
}
|
||
return;
|
||
}
|
||
text(payload.backgroundImage, "行为验证背景图片");
|
||
text(payload.templateImage, "行为验证模板图片");
|
||
}
|
||
|
||
function normalizeChallengeResponse(response, expectedContext) {
|
||
var context = assertContext(expectedContext);
|
||
if (!response || typeof response !== "object" || Number(response.code) !== 200) {
|
||
fail((response && response.msg) || "行为验证挑战请求失败", "TAC_CHALLENGE_FAILED");
|
||
}
|
||
var data = response.data;
|
||
if (!data || typeof data !== "object" || data.required !== true) {
|
||
fail("行为验证挑战未处于强制验证状态", "TAC_CHALLENGE_NOT_REQUIRED");
|
||
}
|
||
var providerCode = text(data.providerCode, "挑战服务商").toUpperCase();
|
||
var captchaType = text(data.captchaType, "挑战验证码类型").toUpperCase();
|
||
if (providerCode !== "TIANAI" || providerCode !== context.providerCode) {
|
||
fail("行为验证挑战服务商不匹配", "TAC_PROVIDER_MISMATCH");
|
||
}
|
||
if (SUPPORTED_TYPES.indexOf(captchaType) < 0 || captchaType !== context.captchaType) {
|
||
fail("行为验证挑战类型不匹配", "TAC_TYPE_MISMATCH");
|
||
}
|
||
var challengeId = text(data.challengeId, "行为验证挑战标识");
|
||
var expireSeconds = positiveInteger(data.expireSeconds, "行为验证挑战有效期");
|
||
assertChallengePayload(captchaType, data.payload);
|
||
return {
|
||
challenge: {
|
||
required: true,
|
||
providerCode: providerCode,
|
||
captchaType: captchaType,
|
||
challengeId: challengeId,
|
||
expireSeconds: expireSeconds,
|
||
},
|
||
sdkResponse: {
|
||
code: 200,
|
||
msg: typeof response.msg === "string" ? response.msg : "操作成功",
|
||
data: Object.assign({}, data.payload, {
|
||
type: captchaType,
|
||
id: challengeId,
|
||
}),
|
||
},
|
||
};
|
||
}
|
||
|
||
function assertTrack(track) {
|
||
if (!track || typeof track !== "object" || Array.isArray(track)) fail("行为验证轨迹无效");
|
||
positiveInteger(track.bgImageWidth, "轨迹背景宽度");
|
||
positiveInteger(track.bgImageHeight, "轨迹背景高度");
|
||
var startTime = Number(track.startTime);
|
||
var stopTime = Number(track.stopTime);
|
||
if (!Number.isFinite(startTime) || !Number.isFinite(stopTime) || stopTime < startTime) {
|
||
fail("行为验证轨迹时间无效");
|
||
}
|
||
if (!Array.isArray(track.trackList) || track.trackList.length < 1) {
|
||
fail("行为验证轨迹不能为空");
|
||
}
|
||
track.trackList.forEach(function validatePoint(point) {
|
||
if (!point || !Number.isFinite(Number(point.x)) || !Number.isFinite(Number(point.y)) ||
|
||
!Number.isFinite(Number(point.t)) || typeof point.type !== "string" || !point.type) {
|
||
fail("行为验证轨迹点无效");
|
||
}
|
||
});
|
||
return track;
|
||
}
|
||
|
||
function buildVerifyBody(libraryRequest, expectedContext, challenge) {
|
||
var context = assertContext(expectedContext);
|
||
if (!challenge || typeof challenge !== "object") fail("行为验证挑战上下文缺失");
|
||
var challengeId = text(challenge.challengeId, "行为验证挑战标识");
|
||
if (!libraryRequest || libraryRequest.id !== challengeId) {
|
||
fail("行为验证 challenge 标识与当前挑战不匹配", "TAC_CHALLENGE_MISMATCH");
|
||
}
|
||
if (challenge.providerCode !== context.providerCode || challenge.captchaType !== context.captchaType) {
|
||
fail("行为验证挑战上下文已变化", "TAC_CONTEXT_MISMATCH");
|
||
}
|
||
return {
|
||
tenantId: context.tenantId,
|
||
subject: context.subject,
|
||
challengeId: challengeId,
|
||
providerCode: challenge.providerCode,
|
||
captchaType: challenge.captchaType,
|
||
// 受保护 OpenAPI 的唯一合同是 payload.track;SDK 回调 id 已提升为 challengeId,
|
||
// 不得再次混入 payload,也不得退回“直接传 track”的兼容分支。
|
||
payload: { track: assertTrack(libraryRequest.data) },
|
||
};
|
||
}
|
||
|
||
function normalizeVerifyResponse(response) {
|
||
if (!response || typeof response !== "object" || Number(response.code) !== 200) {
|
||
fail((response && response.msg) || "行为验证请求失败", "TAC_VERIFY_REQUEST_FAILED");
|
||
}
|
||
var data = response.data;
|
||
if (!data || data.passed !== true) {
|
||
fail((data && data.message) || "行为验证未通过", "TAC_VERIFY_REJECTED");
|
||
}
|
||
var validToken = text(data.validToken, "行为验证票据");
|
||
var expireSeconds = positiveInteger(data.expireSeconds, "行为验证票据有效期");
|
||
return {
|
||
code: 200,
|
||
data: { passed: true, validToken: validToken, expireSeconds: expireSeconds },
|
||
};
|
||
}
|
||
|
||
function toSdkFailure(error) {
|
||
return {
|
||
code: 4001,
|
||
msg: error && error.message ? error.message : "行为验证失败,请重试",
|
||
};
|
||
}
|
||
|
||
global.JiapuTacAdapter = Object.freeze({
|
||
normalizeChallengeResponse: normalizeChallengeResponse,
|
||
buildVerifyBody: buildVerifyBody,
|
||
normalizeVerifyResponse: normalizeVerifyResponse,
|
||
toSdkFailure: toSdkFailure,
|
||
});
|
||
})(typeof window !== "undefined" ? window : globalThis);
|