sveltekit-turnstile
Version:
SvelteKit components for rendering forms and validating form submissions that include cloudflare turnstile tokens for enhanced security
103 lines (102 loc) • 4.16 kB
JavaScript
import { turnstileLoaded } from './stores.js';
const TURNSTILE_SRC = 'https://challenges.cloudflare.com/turnstile/v0/api.js';
const TURNSTILE_SCRIPT = `${TURNSTILE_SRC}?render=explicit`;
/**
* Programmatically obtain Cloudflare pre-clearance on the client.
*
* Renders a Turnstile widget (hidden by default), runs the challenge, and resolves the
* Turnstile token on success. When the widget's site key has pre-clearance enabled in the
* Cloudflare dashboard and is served on the protected zone, Cloudflare sets the httpOnly
* `cf_clearance` cookie automatically as a side effect. Validate the returned token
* server-side with `validateToken`.
*/
export default async function getClearance(siteKey, options = {}) {
if (typeof window === 'undefined' || typeof document === 'undefined') {
throw new Error('getClearance can only be called in the browser');
}
await loadTurnstileScript();
// Use the caller's container, or create an off-screen one we own and clean up.
const ownContainer = !options.container;
const container = options.container ?? createHiddenContainer();
let widgetId;
let timer;
try {
return await new Promise((resolve, reject) => {
if (options.timeoutMs) {
timer = setTimeout(() => reject(new Error('getClearance timed out')), options.timeoutMs);
}
widgetId = window.turnstile.render(container, {
sitekey: siteKey,
action: options.action,
cData: options.cData,
appearance: options.appearance ?? 'interaction-only',
execution: options.execution ?? 'render',
theme: options.theme,
language: options.language,
size: options.size,
callback: (token) => resolve(token),
'error-callback': (code) => reject(new Error(`Turnstile error: ${code}`)),
'timeout-callback': () => reject(new Error('Turnstile challenge timed out')),
'unsupported-callback': () => reject(new Error('Turnstile is not supported in this browser'))
});
if ((options.execution ?? 'render') === 'execute') {
window.turnstile.execute(container);
}
});
}
finally {
if (timer)
clearTimeout(timer);
if (widgetId) {
try {
window.turnstile.remove(widgetId);
}
catch {
/* widget already gone */
}
}
if (ownContainer)
container.remove();
}
}
function createHiddenContainer() {
const el = document.createElement('div');
// Off-screen rather than `display: none`, which can prevent the widget rendering.
el.style.position = 'absolute';
el.style.width = '1px';
el.style.height = '1px';
el.style.overflow = 'hidden';
el.style.left = '-9999px';
el.style.top = '-9999px';
document.body.appendChild(el);
return el;
}
function loadTurnstileScript() {
return new Promise((resolve, reject) => {
if (window.turnstile) {
window.turnstile.ready(() => {
turnstileLoaded.set(true);
resolve();
});
return;
}
const existing = document.querySelector(`script[src^="${TURNSTILE_SRC}"]`);
if (existing) {
existing.addEventListener('load', () => window.turnstile.ready(() => {
turnstileLoaded.set(true);
resolve();
}));
existing.addEventListener('error', () => reject(new Error('Failed to load the Turnstile script')));
return;
}
const script = document.createElement('script');
script.src = TURNSTILE_SCRIPT;
script.async = true;
script.addEventListener('load', () => window.turnstile.ready(() => {
turnstileLoaded.set(true);
resolve();
}));
script.addEventListener('error', () => reject(new Error('Failed to load the Turnstile script')));
document.head.appendChild(script);
});
}