6b0c633a75
Add visitor roles, row-level scope, owner ops APIs, MySQL policy index, Turnstile captcha, browser client SDK, publish-panel dataset binding, acceptance tests, and usage documentation. Co-authored-by: Cursor <cursoragent@cursor.com>
59 lines
1.8 KiB
JavaScript
59 lines
1.8 KiB
JavaScript
const TURNSTILE_VERIFY_URL = 'https://challenges.cloudflare.com/turnstile/v0/siteverify';
|
|
|
|
export function isPageDataCaptchaEnabled() {
|
|
return Boolean(String(process.env.PAGE_DATA_TURNSTILE_SECRET ?? '').trim());
|
|
}
|
|
|
|
export async function verifyPageDataCaptcha(token, remoteIp, { fetchImpl = globalThis.fetch } = {}) {
|
|
const secret = String(process.env.PAGE_DATA_TURNSTILE_SECRET ?? '').trim();
|
|
if (!secret) {
|
|
return { ok: true, skipped: true };
|
|
}
|
|
const responseToken = String(token ?? '').trim();
|
|
if (!responseToken) {
|
|
throw Object.assign(new Error('提交表单需要验证码'), { code: 'captcha_required', status: 400 });
|
|
}
|
|
const body = new URLSearchParams({
|
|
secret,
|
|
response: responseToken,
|
|
});
|
|
if (remoteIp) body.set('remoteip', String(remoteIp));
|
|
const response = await fetchImpl(TURNSTILE_VERIFY_URL, {
|
|
method: 'POST',
|
|
headers: { 'content-type': 'application/x-www-form-urlencoded' },
|
|
body: body.toString(),
|
|
});
|
|
const payload = await response.json().catch(() => ({}));
|
|
if (!payload?.success) {
|
|
throw Object.assign(new Error('验证码校验失败'), {
|
|
code: 'captcha_invalid',
|
|
status: 403,
|
|
});
|
|
}
|
|
return { ok: true, skipped: false };
|
|
}
|
|
|
|
export function extractPageDataCaptchaToken(req, payload) {
|
|
return (
|
|
String(req.headers?.['x-page-data-captcha'] ?? '').trim() ||
|
|
String(payload?.turnstile_token ?? payload?.turnstileToken ?? payload?.captcha_token ?? '').trim() ||
|
|
null
|
|
);
|
|
}
|
|
|
|
export const PAGE_DATA_META_FIELDS = new Set([
|
|
'turnstile_token',
|
|
'turnstileToken',
|
|
'captcha_token',
|
|
'updated_by_label',
|
|
]);
|
|
|
|
export function stripPageDataMetaFields(payload) {
|
|
if (!payload || typeof payload !== 'object' || Array.isArray(payload)) return payload;
|
|
const cleaned = { ...payload };
|
|
for (const key of PAGE_DATA_META_FIELDS) {
|
|
delete cleaned[key];
|
|
}
|
|
return cleaned;
|
|
}
|