Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 23 additions & 2 deletions src/client/live-score.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,16 @@ interface TurnstileApi {
element: HTMLElement | string,
options: {
sitekey: string;
size?: 'invisible' | 'normal' | 'compact';
// `size` is widget dimensions (compact|flexible|normal). `invisible`
// is NOT a valid size value — Turnstile throws on render. Invisible
// behavior is controlled by the sitekey's mode (set in the CF
// dashboard), not by render-time params.
size?: 'compact' | 'flexible' | 'normal';
// `execution: 'execute'` defers the challenge until execute() is
// called explicitly (matches our acquire-on-submit flow). The
// default 'render' starts the challenge as soon as the widget
// mounts, which races our acquireTurnstileToken caller.
execution?: 'render' | 'execute';
callback?: (token: string) => void;
'error-callback'?: () => void;
'expired-callback'?: () => void;
Expand Down Expand Up @@ -112,6 +121,18 @@ function initLiveScore(els: {
// curated-reward or phase-progression text from the previous submit.
// Reset to a clean state so the form is immediately usable again.
// Standard a11y pattern, no copy change needed.
//
// Also tear down the Turnstile widget on pagehide so a bfcache-restored
// page doesn't reuse a half-dead widget instance. On the next
// acquireTurnstileToken the module-scope state is empty and a fresh
// widget is rendered.
window.addEventListener('pagehide', () => {
if (turnstileWidget && window.turnstile) {
window.turnstile.remove(turnstileWidget.id);
}
turnstileWidget = null;
pendingTurnstile = null;
});
window.addEventListener('pageshow', (event) => {
if (!event.persisted) return;
setSubmitting(els, false);
Expand Down Expand Up @@ -275,7 +296,7 @@ function acquireTurnstileToken(
}
const id = api.render(container, {
sitekey,
size: 'invisible',
execution: 'execute',
callback: (token: string) => settleTurnstile({ token }),
'error-callback': () => settleTurnstile({ error: new Error('turnstile_error') }),
'expired-callback': () => settleTurnstile({ error: new Error('turnstile_expired') }),
Expand Down
10 changes: 8 additions & 2 deletions src/worker/headers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -65,14 +65,20 @@ const IMMUTABLE_CACHE = 'public, max-age=31536000, immutable';
//
// Applied to every HTML response (not just /), so a CSP regression test
// hitting any page surfaces drift on every directive.
// style-src + font-src include the Google Fonts origins because the
// Turnstile widget bootstrap injects `<link rel=stylesheet
// href="https://fonts.googleapis.com/css?family=Lato...">` into the
// host document even when the sitekey is configured as Invisible mode
// in the CF dashboard (defensive UI prep in case the challenge elevates).
// The CSS file in turn loads font files from fonts.gstatic.com.
const CSP_HTML =
"default-src 'self'; " +
"script-src 'self' 'unsafe-inline' https://challenges.cloudflare.com; " +
'frame-src https://challenges.cloudflare.com; ' +
"connect-src 'self' https://challenges.cloudflare.com; " +
"img-src 'self' data:; " +
"style-src 'self' 'unsafe-inline'; " +
"font-src 'self'; " +
"style-src 'self' 'unsafe-inline' https://fonts.googleapis.com; " +
"font-src 'self' https://fonts.gstatic.com; " +
"base-uri 'self'; " +
"form-action 'self'; " +
"object-src 'none'; " +
Expand Down
4 changes: 2 additions & 2 deletions src/worker/score/summary-render.ts
Original file line number Diff line number Diff line change
Expand Up @@ -235,8 +235,8 @@ const LIVE_SCORE_CSP =
'frame-src https://challenges.cloudflare.com; ' +
"connect-src 'self' https://challenges.cloudflare.com; " +
"img-src 'self' data:; " +
"style-src 'self' 'unsafe-inline'; " +
"font-src 'self'; " +
"style-src 'self' 'unsafe-inline' https://fonts.googleapis.com; " +
"font-src 'self' https://fonts.gstatic.com; " +
"base-uri 'self'; " +
"form-action 'self'; " +
"object-src 'none'; " +
Expand Down
Loading