[Turnstile] Spin docs: refresh hosted prompt, helper scripts, and spin.mdx#32147
Conversation
The dashboard 'Set up with Spin' button orchestrates widget + Worker creation server-side; this commit teaches the docs about it. - spin.mdx restructured to lead with the dashboard path (DashButton + 5 steps), demote the command-line agent flow to an alternative for users without dashboard access or who prefer to drive setup from their editor. - New 'Wire up the frontend' section with the HTML + JS pattern for dashboard-deployed widgets (sitekey + Worker URL handed off, agent prompt or hand-wired). - Telemetry marker section documents both data-action values: turnstile-spin-v1 (command-line) and turnstile-spin-v2 (dashboard). - Recovery section mentions the in-dashboard 'Fix all with Spin' banner alongside the agent-led recovery prompt. - Agent-step table converted from em-dash separators to parentheses; agent skill index.md style-only tweaks (root-relative URLs, no contractions, imperative heading).
- Refer to (not 'See') for the in-page link to Telemetry marker. - Convert the two-item 'edge cases to flag' bullet list to prose. - Replace passive 'Both markers are applied automatically by the respective setup path' with active 'Each setup path applies its own marker automatically'.
Match the rename in cloudflare#31532. The agent skill route is moving from /turnstile/spin/index.md to /turnstile/spin/prompt.md; align the V2 docs PR with that so the diff stays clean when cloudflare#31532 merges first.
…nt recovery is the fallback
Rewrite developers.cloudflare.com/turnstile/spin to match the new Spin Lite architecture: dashboard creates the widget, agent wires canonical server-side siteverify in the customer's existing backend. No managed Worker. Drops the 'Managed Worker endpoints' and 'Worker configuration' reference tables. Updates wire-up examples to canonical challenges.cloudflare.com/turnstile/v0/siteverify. Updates recovery and migration sections to match the no-Worker shape. Pairs with cloudflare/skills and cloudflare/fe/stratus !39261.
- Rename public/turnstile/spin/{index.md → prompt.md} so the URL matches
the four references from spin.mdx (previously 404'd in production).
- Sync prompt.md from the canonical V2 SKILL in cloudflare/skills: drops
the Worker deploy step, wires canonical siteverify from the customer's
existing backend, uses turnstile-spin-v2 marker.
- Add 'Set up from the Wrangler CLI' section documenting
'wrangler turnstile widget create/list/get/update/delete' (Wrangler
4.109+, alpha).
- Rename 'Set up from the command line' → 'Set up from an AI coding
agent' now that there are three setup paths.
- prompt.md now also documents the wrangler-first fallback in Step 8.
Version-gate notes are useful for the narrow window right after a new Wrangler version ships; once the release stabilizes they read as noise and the number ages. Docs stay accurate without pinning to 4.109.
…, marker wording)
This reverts commit 2985122.
…r V2 Combines two changes in the docs mirror of the Spin skill: 1. Sync prompt.md to the current cloudflare/skills SKILL.md (em-dash cleanup, 'Do not propose features' bullet added to Things you must NOT do, recovery flow line 4 wording updated to match SKILL.md, redundant backend snippet dropped since it now lives only in Step 9). 2. Token-reset hotfix. Turnstile tokens are single-use; when siteverify returns success:false and the user retries, the browser resubmits the same cf-turnstile-response and Cloudflare's edge rejects it as timeout-or-duplicate. V2 telemetry since the 2026-07-14 launch shows 4.86% sv_fail rate, 99% of which are double redemptions (5x V1). The canonical HTML snippet in 'The frontend-edit contract' now gets id=cf-form + a setTimeout reset-on-submit listener, plus a new 'Token lifecycle' paragraph explaining the single-use contract. Companion PRs: - cloudflare/skills#83 (canonical SKILL.md + 6 framework references) - stratus (spinSkillContent.ts template-literal mirror)
ReviewReviewing new changes (commit |
…arnings Resolves the prompt.md merge conflict between this branch's V2 token-reset hotfix and production's Jul 10 canonical siteverify pivot. All conflict blocks resolve to HEAD; the branch's tidy-ups (no em-dashes, dashboard Deployment column removed, redundant '## Do not' section consolidated into 'Things you must NOT do') already superseded the production text. Also addresses three warnings from cloudflare-docs-bot on this PR: * CR-b07f3bb2e801 (missing helper scripts): commit auth-probe.sh, fetch-secret.sh, persist-skill.sh, validate.sh, widget-create.sh to public/turnstile/spin/scripts/ so the docs page is self-contained. Same content as the cloudflare/skills bundle. * CR-79e4bf260a6b (contradictory pre-clearance handling): recovery flow step 3 no longer offers 'ask user or exit' for non-no_clearance widgets, which contradicted the scope boundary. Now always exits per the boundary. * CR-223fc933d2c8 (missing HTTP error handling in siteverify snippet): wrap fetch+json in try/catch, check r.ok, replace the non-existent reject(403, ...) helper with res.status(403).send() and a note about adapting to the target framework.
Mirrors the fixes applied in cloudflare/cloudflare-docs#32147: * Siteverify snippet in Step 9 now wraps fetch+json in try/catch, checks r.ok, and replaces the non-existent reject(403, ...) helper with res.status(403).send('forbidden') plus a comment about adapting to the target framework. * Recovery flow step 3 no longer offers 'ask user or exit' for non-no_clearance widgets, which contradicted the scope boundary. Always exits per the boundary now. Keeps this file byte-for-byte in sync with the docs prompt.md and stratus spinSkillContent.ts (the three-surface parity that the Spin machinery depends on).
|
Triaged all 3 findings from the code review; all fixed in merge commit 7995c2d.
Also propagated the CR-79e4bf260a6b and CR-223fc933d2c8 fixes to the two companion PRs to keep the three-surface skill content in sync:
|
Marina flagged on cloudflare/cloudflare-docs#32147 that the skill's scope reads too form-centric, when Turnstile actually applies to any user-triggered request the customer wants to gate: form submissions, SPA button-triggered API calls, download links, comment or vote submissions, and any other explicit user action that hits a backend the customer controls. Broadened six spots to match how Turnstile is actually used in the wild (per the product docs at developers.cloudflare.com/turnstile), while keeping the canonical form example in the frontend-edit contract section as the primary illustration: * Description: 'embed it on the right forms' -> 'embed it where user requests need bot verification (form submissions, SPA actions, API endpoints, download links, comment or vote submissions, etc.)'. * When-to-load triggers: added 'protect this endpoint', 'protect this button', 'block bots on <target>'; the 'specific request' example bullet now lists downloads / comments / API endpoints alongside signup / login / contact form. * Brief-acknowledge line the agent says on Step 1. * Wire-the-integration statement the agent says on Step 9. * Hard-scope-boundary sentence: dropped 'form' qualifier on handler. * Frontend-edit-contract opener: covers form or user-triggered endpoint; dropped 'submit' from handler. Kept in sync across all three surfaces (this canonical SKILL.md, prompt.md docs, stratus spinSkillContent.ts).
Marina flagged on this PR that the skill's scope reads too form-centric, when Turnstile actually applies to any user-triggered request the customer wants to gate: form submissions, SPA button-triggered API calls, download links, comment or vote submissions, and any other explicit user action that hits a backend the customer controls. Broadened six spots to match how Turnstile is actually used in the wild (per the product docs at developers.cloudflare.com/turnstile), while keeping the canonical form example in the frontend-edit contract section as the primary illustration: * Description: 'embed it on the right forms' -> 'embed it where user requests need bot verification (form submissions, SPA actions, API endpoints, download links, comment or vote submissions, etc.)'. * When-to-load triggers: added 'protect this endpoint', 'protect this button', 'block bots on <target>'; the 'specific request' example bullet now lists downloads / comments / API endpoints alongside signup / login / contact form. * Brief-acknowledge line the agent says on Step 1. * Wire-the-integration statement the agent says on Step 9. * Hard-scope-boundary sentence: dropped 'form' qualifier on handler. * Frontend-edit-contract opener: covers form or user-triggered endpoint; dropped 'submit' from handler. Kept in sync across all three surfaces (this prompt.md, cloudflare/skills SKILL.md, stratus spinSkillContent.ts). Companion commits: - cloudflare/skills#83: 8192e1d - stratus MR !40817: 399fbbbd1e0
Address all 19 findings from cloudflare-docs-bot on this PR: 1
critical + 15 warnings + 1 suggestion + 1 convention (PR description
scope, updated separately).
widget-create.sh:
- CR-22ef8f6771b1 (critical): build request body via python3
json.dumps instead of hand-escaping quotes into a raw JSON string
- CR-f735353f7ad5: argument value validation before shift
- CR-e48c7d0bb1e9: drop `2>/dev/null` on the API curl so network
errors reach stderr
- CR-a9681e1b8608, CR-5fd7af1e876a: build success and error output
via python3 json.dumps
validate.sh:
- CR-e781359b7c28: argument value validation
- CR-afe9e2b7b7c0: mktemp + %{http_code} pattern with explicit
status check for the widget-domains lookup
- CR-391c70408be0: python3 parse with structured error handling
auth-probe.sh:
- CR-1f8cc5f1475c: `command -v wrangler` guard before invocation,
fall back to $CLOUDFLARE_ACCOUNT_ID when wrangler is unavailable
- CR-c5afb0714a2f: replace GET-based scope probe with
POST-invalid-payload probe to test Turnstile:Edit specifically
fetch-secret.sh:
- CR-e82849253dd7: argument value validation
- CR-30730b7afe35: mktemp with explicit template for macOS/BSD
- CR-1bd6e05a63d9 (suggestion): trap-based temp cleanup
- CR-2a35782321aa: Python .get() fallbacks with structured errors
- CR-76a6232130e9: URL-encode account_id and sitekey
persist-skill.sh:
- CR-03ee17a69501: argument value validation
- CR-bb0ae4241b8d: build scripts JSON list via python3
os.listdir + json.dumps
All scripts pass 'bash -n' and end-to-end smoke tests against a
non-existent account (structured error output, no tracebacks). Same
5 scripts committed to cloudflare/skills at
cloudflare/skills#83 and inlined into
stratus MR !40817.
|
Triaged all 19 findings on the helper scripts; agreed with all, fixing in the pending commit.
All 5 scripts now pass
|
Replaces hand-escaped JSON with python3 json.dumps for both request
bodies and response outputs, adds argument-value validation before
each shift, structures HTTP error handling, and switches auth-probe
from a GET-based read probe to a POST-invalid-payload probe so it
tests Turnstile:Edit specifically.
widget-create.sh:
- python3 json.dumps for request body + success/error output
(replaces `echo "{\"name\":\"$NAME\",...}"` which broke
on names containing quotes, backslashes, or control characters)
- Argument value validation before shift
- Remove `2>/dev/null` on the API curl so network errors reach stderr
- URL-encode account_id
validate.sh:
- Argument value validation
- mktemp + -o + %{http_code} pattern for the widget-domains lookup
- python3 with structured error handling for JSON parse failures
- URL-encode account_id and sitekey
auth-probe.sh:
- `command -v wrangler` guard before invocation (avoids npx
install-prompt hang in non-interactive envs); fall back to
$CLOUDFLARE_ACCOUNT_ID when wrangler is not on PATH
- Replace GET-based scope probe with POST-invalid-payload probe:
a GET would authorize a Read-only token and let the agent proceed
to widget-create where it would fail. POST-invalid tests Edit
specifically: 401/403/err-10000 -> missing_scope, 400/422 -> scope OK.
- mktemp with explicit template for macOS/BSD portability
- Argument value validation
fetch-secret.sh:
- Argument value validation
- mktemp with explicit template + trap-based cleanup on early exit
- Python fallbacks use .get() with structured error output on
missing keys or non-JSON responses
- URL-encode account_id and sitekey
persist-skill.sh:
- Argument value validation
- Build scripts JSON list via python3 os.listdir + json.dumps
(replaces ls | sed | paste which mishandled filenames with quotes,
backslashes, or newlines)
Flagged by cloudflare-docs-bot on
cloudflare/cloudflare-docs#32147: 1 critical
(CR-22ef8f6771b1) + 15 warnings + 1 suggestion. All 19 addressed.
Companion: cloudflare-docs #32147, stratus MR !40817.
|
Triaged the second-review findings. Skipping 2 with reasoning, fixing the other 9 in the pending commit. SkippingCR-04913c72363f (critical) — EXIT trap does NOT fire in
|
| ID | File:Line | Change |
|---|---|---|
| CR-8564af26b1eb | validate.sh domain parsing |
[d.strip() for d in expected_csv.split(",") if d.strip()] — trims whitespace from each expected domain token. |
| CR-244ecc0a2bae | fetch-secret.sh:97 |
(errors[0] or {}).get("message") — matches the defensive pattern already used for first_code. Applied consistently across all 5 scripts. |
| CR-170e734f63cc | fetch-secret.sh:74 |
Explicit isinstance(data, dict) check after json.loads; emits structured error output when the response isn't a JSON object. Applied consistently across widget-create, validate, fetch-secret, and auth-probe. |
| CR-9cdbd7ec5a36 | all scripts | command -v python3 guard at the top of each script; emits the script's canonical error status (missing_token / error+reason:python3_not_available etc.) and exits 1 when python3 is absent. |
| CR-460039bb0738 (suggestion) | all script headers | Header comments now document all three exit codes: 0 on success, 1 on failure or missing prerequisite, 2 on invalid usage. |
| CR-3b31dbc5640a (suggestion) | persist-skill.sh:63 |
Catch OSError (not just FileNotFoundError) so NotADirectoryError, PermissionError, and other listdir failures emit scripts: [] instead of a traceback. Verified locally with a scripts-is-a-file-not-a-directory case. |
| CR-4a512fac7815 | auth-probe.sh:131 |
Bearer token now written to a mode-600 tempfile; curl reads it via -H "@$auth_headers". Token never appears in argv or ps output. |
| CR-333053976387 | validate.sh:51 |
Turnstile secret now written to a mode-600 tempfile; curl reads it via --data-urlencode "secret@$secret_file". Secret never appears in argv. printf '%s' (no trailing newline) so the url-encoded value is exactly the secret. |
| CR-f14858b663da | validate.sh:120 |
Same tempfile-header pattern as CR-4a512fac7815. |
The same 9 fixes are applied byte-for-byte to cloudflare/skills#83 (canonical) and stratus MR !40817 (inlined into spinSkillContent.ts).
Second round of docs-bot findings on this PR: 6 real issues + 3
secrets-in-argv warnings addressed. The bot's critical CR-04913c72363f
(EXIT trap fires in $() subshell) is a false positive; see triage
comment on this PR for the bash-test disproof. CR-14b4e5e6a596
(unvalidated --path) is skipped with reasoning in the same comment.
- CR-8564af26b1eb: strip whitespace from expected-domains tokens
- CR-244ecc0a2bae: (errors[0] or {}).get() defensive guard
- CR-170e734f63cc: isinstance(data, dict) after json.loads
- CR-9cdbd7ec5a36: 'command -v python3' guard at top of each script
- CR-460039bb0738: document exit 2 for invalid usage in headers
- CR-3b31dbc5640a: catch OSError instead of just FileNotFoundError
- CR-4a512fac7815, CR-333053976387, CR-f14858b663da: Bearer token and
Turnstile secret now go through mode-600 tempfiles (curl -H @file /
--data-urlencode name@file); never in argv.
Same 9 fixes propagated to cloudflare/skills#83 and stratus MR !40817.
Docs-bot third-round review flagged four items: - Unchecked mktemp in widget-create.sh (two calls). Applied inline mktemp guards across all mktemp calls in widget-create.sh, validate.sh, fetch-secret.sh, and auth-probe.sh. On failure the scripts emit structured error JSON matching the existing contract and clean up any earlier tempfiles before exit. - fetch-secret.sh accessed errors[0] without first checking that errors is a list. Added isinstance(errors, list) guard and applied the same fix to widget-create.sh, validate.sh, and auth-probe.sh which have the same pattern. - persist-skill.sh had a python3 command -v guard but not one for npx (needed for the degit call). Added the npx guard next to it, returning a structured error JSON on absence.
|
Third-round docs-bot review addressed — all 4 warnings fixed, no skips this round. Findings addressed
Verification
Same set of changes synced to the stratus dashboard inline copy (regenerated on the token-reset branch, extracted matches canonical byte-for-byte). Note: this PR has already used both of the auto-review passes docs-bot allocates per PR. If a codeowner wants a fresh full review over these changes, comment |
Docs-bot and gsa_claude flagged that the Edit-scope probe POSTs a widget-create request with intentionally-invalid empty name/domains to distinguish Read scope (401/403) from Edit scope (400/422). The API rejects the payload today, so no widget is created, but there was no safety net if validation ever loosened. Parse result.sitekey from the probe response and, if a widget was unexpectedly created (success:true with a sitekey), fire a DELETE against it so the probe stays side-effect-free regardless of future API validation changes. Verdict logic and existing 400/422/200/401/ 403 branches are unchanged.
Mirror the same fix from the cloudflare-skills canonical: Step 2 (CLI check) and Troubleshooting row for `wrangler whoami` now describe the actual auth-probe.sh behavior (PATH-resident wrangler, fallback to $CLOUDFLARE_ACCOUNT_ID, no `/accounts` curl because the token is scoped to Account.Turnstile:Edit only).
The dashboard's inlined SPIN_SKILL_CONTENT in stratus had accumulated edits (32.5KB) that were never mirrored back to this docs copy (19.5KB before this commit). Copying the stratus template-literal body into prompt.md so both surfaces ship the same skill. Follow-up: stratus will switch to fetching this URL at runtime and drop the inline mirror, at which point prompt.md becomes the single source of truth.
|
/rebase |
Mirrors the same independent-semantic-review fixes applied to the
canonical skill in cloudflare/skills:
- Run every embedded python3 with -I (both inline -c and the bootstrap
loop) to prevent cwd-relative module shadowing of urllib, json,
hashlib, os, re, sys.
- auth-probe.sh emits distinct network_failure and upstream_failure
statuses; hosted prompt Step 3 documents branches for both.
- Post-auth recovery resumes at Step 4 instead of jumping over the
numbered wizard.
- Secret-write prose (Step 9) requires git check-ignore before writing
to any .env-style file, with fallback to the platform's secret
manager.
- spin.mdx splits the canonical siteverify block into a Node.js form
(req.body, req.ip, res.status(403)) and a Cloudflare Worker form
(request.formData(), CF-Connecting-IP, env.TURNSTILE_SECRET,
export default { async fetch(request, env) { ... } }). The old
"Node / Workers" hybrid used Express APIs that do not exist on the
Worker Request.
- Refresh fetch_spin_script SHA-256 pins in prompt.md to match the
updated scripts.
- Delete fetch-secret.sh (superseded by the in-flow existing-widget
recovery documented in prompt.md and spin.mdx).
…ning (#83) * turnstile-spin: guide token reset after submit to fix V2 double-redemption Turnstile tokens are single-use. When siteverify returns success:false and the user retries, the browser resubmits the same cf-turnstile-response and Cloudflare's edge rejects it with timeout-or-duplicate. V2 telemetry since the 2026-07-14 launch shows 4.86% sv_fail rate, of which 99% (123/124) are double redemptions — 5x the V1 baseline of 0.9%. Add reset-on-submit guidance to the canonical frontend snippet in SKILL.md and to each framework reference so the widget re-solves before the retry. Also add a 'Token lifecycle' paragraph explaining the single-use contract. - SKILL.md: id=cf-form + reset script; Token lifecycle paragraph - vanilla-html: main HTML + AJAX variant get reset calls - nextjs-app: handleSubmit resets on !data.ok; Server Action variant covers useActionState error branch - nextjs-pages: reset script wrapped in <Script> for hydration ordering - sveltekit: use:enhance callback resets on non-redirect + client-fetch variant - astro: main form script + Astro Actions variant reset - hugo: partial gets reset script * [Turnstile] Spin skill: address code review warnings from docs PR Mirrors the fixes applied in cloudflare/cloudflare-docs#32147: * Siteverify snippet in Step 9 now wraps fetch+json in try/catch, checks r.ok, and replaces the non-existent reject(403, ...) helper with res.status(403).send('forbidden') plus a comment about adapting to the target framework. * Recovery flow step 3 no longer offers 'ask user or exit' for non-no_clearance widgets, which contradicted the scope boundary. Always exits per the boundary now. Keeps this file byte-for-byte in sync with the docs prompt.md and stratus spinSkillContent.ts (the three-surface parity that the Spin machinery depends on). * [Turnstile] Spin skill: broaden scope beyond forms per review Marina flagged on cloudflare/cloudflare-docs#32147 that the skill's scope reads too form-centric, when Turnstile actually applies to any user-triggered request the customer wants to gate: form submissions, SPA button-triggered API calls, download links, comment or vote submissions, and any other explicit user action that hits a backend the customer controls. Broadened six spots to match how Turnstile is actually used in the wild (per the product docs at developers.cloudflare.com/turnstile), while keeping the canonical form example in the frontend-edit contract section as the primary illustration: * Description: 'embed it on the right forms' -> 'embed it where user requests need bot verification (form submissions, SPA actions, API endpoints, download links, comment or vote submissions, etc.)'. * When-to-load triggers: added 'protect this endpoint', 'protect this button', 'block bots on <target>'; the 'specific request' example bullet now lists downloads / comments / API endpoints alongside signup / login / contact form. * Brief-acknowledge line the agent says on Step 1. * Wire-the-integration statement the agent says on Step 9. * Hard-scope-boundary sentence: dropped 'form' qualifier on handler. * Frontend-edit-contract opener: covers form or user-triggered endpoint; dropped 'submit' from handler. Kept in sync across all three surfaces (this canonical SKILL.md, prompt.md docs, stratus spinSkillContent.ts). * Harden Turnstile Spin helper scripts (safety + portability) Replaces hand-escaped JSON with python3 json.dumps for both request bodies and response outputs, adds argument-value validation before each shift, structures HTTP error handling, and switches auth-probe from a GET-based read probe to a POST-invalid-payload probe so it tests Turnstile:Edit specifically. widget-create.sh: - python3 json.dumps for request body + success/error output (replaces `echo "{\"name\":\"$NAME\",...}"` which broke on names containing quotes, backslashes, or control characters) - Argument value validation before shift - Remove `2>/dev/null` on the API curl so network errors reach stderr - URL-encode account_id validate.sh: - Argument value validation - mktemp + -o + %{http_code} pattern for the widget-domains lookup - python3 with structured error handling for JSON parse failures - URL-encode account_id and sitekey auth-probe.sh: - `command -v wrangler` guard before invocation (avoids npx install-prompt hang in non-interactive envs); fall back to $CLOUDFLARE_ACCOUNT_ID when wrangler is not on PATH - Replace GET-based scope probe with POST-invalid-payload probe: a GET would authorize a Read-only token and let the agent proceed to widget-create where it would fail. POST-invalid tests Edit specifically: 401/403/err-10000 -> missing_scope, 400/422 -> scope OK. - mktemp with explicit template for macOS/BSD portability - Argument value validation fetch-secret.sh: - Argument value validation - mktemp with explicit template + trap-based cleanup on early exit - Python fallbacks use .get() with structured error output on missing keys or non-JSON responses - URL-encode account_id and sitekey persist-skill.sh: - Argument value validation - Build scripts JSON list via python3 os.listdir + json.dumps (replaces ls | sed | paste which mishandled filenames with quotes, backslashes, or newlines) Flagged by cloudflare-docs-bot on cloudflare/cloudflare-docs#32147: 1 critical (CR-22ef8f6771b1) + 15 warnings + 1 suggestion. All 19 addressed. Companion: cloudflare-docs #32147, stratus MR !40817. * Harden Turnstile Spin helper scripts (second pass on docs-bot findings) Second round of docs-bot findings: 6 real issues + 3 secrets-in-argv warnings addressed. The bot's critical (EXIT trap fires in $() subshell) is a false positive verified with a bash test: subshells reset their parent's traps per the bash manual, so $tmp survives curl $() calls and the trap only fires on outer-script exit. - All scripts: 'command -v python3' guard at top; emit structured JSON error on missing prerequisite instead of raw shell error. - All scripts: header comments now document exit 2 (invalid usage) in addition to exit 0 (success) and exit 1 (failure). - widget-create.sh, validate.sh, fetch-secret.sh, auth-probe.sh: Authorization header written to mode-600 tempfile, curl reads it via '-H @file'. Bearer token never appears in argv. - validate.sh: Turnstile secret written to mode-600 tempfile, curl reads it via --data-urlencode 'secret@file'. Secret never appears in argv. - validate.sh: strip whitespace from each expected-domains token so '--expected-domains "a.com, b.com"' matches correctly. - fetch-secret.sh, auth-probe.sh: guard 'errors[0]' with '(errors[0] or {})' before .get() to survive non-dict error entries. - All parse blocks: 'isinstance(data, dict)' check after json.loads; emit structured error output when response is not a JSON object. - persist-skill.sh: catch OSError (not just FileNotFoundError) so NotADirectoryError/PermissionError on the scripts dir emit 'scripts: []' instead of a traceback. Skipped: - CR-04913c72363f (critical, false positive; bash test in the docs PR triage comment) - CR-14b4e5e6a596 (suggestion; --path is user-supplied by design so agents can persist to .claude/skills, .opencode/skills, etc.) Companion: cloudflare-docs #32147, stratus MR !40817. * Guard mktemp, malformed errors list, and npx prerequisite Docs-bot third-round review flagged four items: - Unchecked mktemp in widget-create.sh (two calls). Applied inline mktemp guards across all mktemp calls in widget-create.sh, validate.sh, fetch-secret.sh, and auth-probe.sh. On failure the scripts emit structured error JSON matching the existing contract and clean up any earlier tempfiles before exit. - fetch-secret.sh accessed errors[0] without first checking that errors is a list. Added isinstance(errors, list) guard and applied the same fix to widget-create.sh, validate.sh, and auth-probe.sh which have the same pattern. - persist-skill.sh had a python3 command -v guard but not one for npx (needed for the degit call). Added the npx guard next to it, returning a structured error JSON on absence. * Add cleanup safety net to auth-probe Edit-scope probe Docs-bot and gsa_claude flagged that the Edit-scope probe POSTs a widget-create request with intentionally-invalid empty name/domains to distinguish Read scope (401/403) from Edit scope (400/422). The API rejects the payload today, so no widget is created, but there was no safety net if validation ever loosened. Parse result.sitekey from the probe response and, if a widget was unexpectedly created (success:true with a sitekey), fire a DELETE against it so the probe stays side-effect-free regardless of future API validation changes. Verdict logic and existing 400/422/200/401/ 403 branches are unchanged. * Correct Step 2 + Troubleshooting prose to match auth-probe behavior gsa_claude flagged that Step 2's CLI-check paragraph still said Spin's helper scripts use `npx wrangler whoami` for account enumeration and that no persistent CLI install is required. Neither is accurate after the auth-probe.sh rewrite: the probe uses PATH-resident `wrangler` (not npx, so a non-interactive shell won't hang on auto-install) and falls back to requiring `$CLOUDFLARE_ACCOUNT_ID` when wrangler is missing. Rewrite line 38 (Step 2) and line 195 (`wrangler whoami` troubleshooting row) to reflect the actual behavior: PATH check first, otherwise require the account ID be exported. Also drop the misleading suggestion to curl `/accounts` as a fallback \x2D the Account.Turnstile:Edit token can't list accounts (`/accounts` is Account.Read-scoped), so that fallback would 403. * Harden Turnstile Spin skill after independent semantic review Independent review of the Turnstile Spin skill surfaced a critical Python module-shadowing risk, several correctness gaps in the recovery flow and reference examples, and drift in the README install path. Fixes applied to SKILL.md, all six framework references, the four helper scripts, the test doc, and the README: - Run every embedded python3 invocation with -I to prevent cwd-relative module shadowing of stdlib (urllib, json, hashlib, os, re, sys). Covers -c inline, - stdin heredoc, and the bootstrap loop. - auth-probe.sh: distinguish network_failure and upstream_failure from missing_scope; SKILL.md Step 3 documents branches for both. - SKILL.md Step 3: post-auth recovery resumes at Step 4 (the numbered wizard) instead of jumping over Steps 4-7. - SKILL.md Step 9: require git check-ignore before writing to any .env-style file, with fallback to the platform's secret manager. - Framework references (vanilla-html, nextjs-app, nextjs-pages, astro, sveltekit, hugo): inline a TURNSTILE_HOSTNAMES allowlist and compare result.hostname against it in every server-side snippet. Includes Node, Ruby, and Python variants in vanilla-html. - README: recommend the git-clone/symlink install path; keep the single-file curl bootstrap only as a read-only fallback with a note that persist-skill.sh needs the cloned bundle. - Remove fetch-secret.sh (superseded by the in-flow existing-widget recovery in SKILL.md, no callers remain). Scripts still pass shellcheck and safe smoke tests (missing_token, file_target_not_supported, empty-array rejection, unsupported-mode rejection). Isolation is confirmed by poisoning urllib.py/json.py/ hashlib.py in the working directory and re-running each script. --------- Co-authored-by: Jules Lemee <jules@cloudflare.com>
Summary
Turnstile Spin docs: refresh the hosted agent prompt, the four deterministic helper scripts it invokes, and the
spin.mdxreference page. This mirrors the canonical skill in cloudflare/skills#83.What's in the PR
public/turnstile/spin/prompt.md— hosted mirror of the canonical Spin skill. Bootstraps into any agent (Claude Code, Cursor, Codex, OpenCode, Copilot Chat) and drives the twelve-step wizard end-to-end. Recovery flow for an already-created widget is included in this file, not shipped separately.public/turnstile/spin/scripts/{auth-probe,widget-create,validate,persist-skill}.sh— four deterministic helpers the agent branches on. Each emits structured JSON on stdout, human diagnostics on stderr, and pinned exit codes. All bootstrap fetches inprompt.mdverify SHA-256 pins before executing.src/content/docs/turnstile/spin.mdx— reference page for developers who want to set Spin up manually (dashboard, Wrangler, or AI agent). Includes a canonical siteverify snippet in two variants: Node.js (req.body[...],req.ip,res.status(403)) and Cloudflare Worker (request.formData(),CF-Connecting-IP,env.TURNSTILE_SECRET,export default { fetch(request, env) }).Hardening (this revision)
An independent semantic review of the skill surfaced several correctness and security issues; fixes are applied here and in the companion skills PR:
python3invocation runs with-I(both inline-cand- <<'PY'heredocs, plus the bootstrap loop) to prevent cwd-relative module shadowing ofurllib,json,hashlib,os,re,sys.auth-probe.shemits distinctnetwork_failureandupstream_failurestatuses;prompt.mdStep 3 documents branches for both. Reachability problems no longer surface as scope problems.git check-ignore -q <path>before writing to any.env-style file, with an explicit fallback to the platform's secret manager.spin.mdxcanonical block split into Node.js and Cloudflare Worker forms. The previous "Node / Workers" hybrid used Express APIs (request.body[...],request.ip) that do not exist on the WorkerRequest.fetch-secret.sh(superseded by the in-flow existing-widget recovery inprompt.md; no callers remain).fetch_spin_scriptSHA-256 pins inprompt.mdto match the updated scripts.Local validation
Ran locally against the merged tree:
pnpm run check— 0 errorspnpm run lint— cleanpnpm run format:core:check— all matched files use Prettier code stylepnpm run test -- --run— 93/93 passingRUN_LINK_CHECK=true pnpm run build— 8701 pages built, link check cleanbash -n+ exec bit on all four, safe smoke tests (missing_token, file_target_not_supported, empty-array rejection, unsupported-mode rejection), isolation verified by poisoningurllib.py/json.py/hashlib.pyin CWDCompanion
SKILL.md, six framework references, the same four helper scripts, and the test doc. This docs PR mirrors it byte-for-byte on scripts.