Files
memind/page-data-captcha.mjs
T
john 6b0c633a75 feat(page-data): complete Phase 4-5, ops UI, and publish integration
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>
2026-07-08 14:52:49 +08:00

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;
}