Skip to content

turnstile-spin: canonical skill refresh with independent-review hardening#83

Merged
dmmulroy merged 9 commits into
cloudflare:mainfrom
juleslemee:turnstile-spin-v2-compat
Jul 24, 2026
Merged

turnstile-spin: canonical skill refresh with independent-review hardening#83
dmmulroy merged 9 commits into
cloudflare:mainfrom
juleslemee:turnstile-spin-v2-compat

Conversation

@juleslemee

@juleslemee juleslemee commented Jul 17, 2026

Copy link
Copy Markdown
Contributor

Summary

Canonical Turnstile Spin skill: refresh SKILL.md, six framework references, four helper scripts, the test doc, and the README. This is the source of truth that the hosted prompt.md mirrors and the dashboard-inlined spinSkillContent.ts inlines.

What the skill does

The agent runs a twelve-step wizard end-to-end: acknowledge, CLI check, auth + scope probe, account selection, domain confirmation, codebase scan, insertion plan, widget creation, wiring (widget + canonical server-side siteverify inside the customer's existing backend handler), validation, skill persistence, and final report. It confirms before every irreversible step. It never asks the user to paste the Turnstile secret into chat.

Recovery for an already-created widget lives in the same SKILL.md under Existing-widget flow. It requires a user-approved absolute WRANGLER_BIN outside the project and confirms the exact sitekey-to-destination mapping (Worker + env + config + binding, or gitignored .env, or platform secret manager) before any secret-bearing getter.

Hardening (this revision)

Independent semantic review of the skill surfaced a critical Python module-shadowing risk plus correctness gaps in the recovery flow and reference examples. Fixes:

  • Every embedded python3 invocation runs with -I (both inline -c and - <<'PY' heredocs) to prevent cwd-relative module shadowing of urllib, json, hashlib, os, re, sys. Verified by poisoning urllib.py / json.py / hashlib.py in CWD and confirming scripts still succeed.
  • auth-probe.sh emits distinct network_failure and upstream_failure statuses; SKILL.md Step 3 documents branches for both. Reachability problems no longer surface as scope problems.
  • Post-auth recovery resumes at Step 4 (the numbered wizard) rather than jumping over Steps 4-7.
  • Secret-write prose (Step 9) requires git check-ignore -q <path> before writing to any .env-style file, with an explicit fallback to the platform's secret manager.
  • Every framework reference (vanilla-html, nextjs-app route + server action, nextjs-pages, astro route + action, sveltekit form action + +server.ts, hugo) inlines a TURNSTILE_HOSTNAMES allowlist and compares result.hostname against it. Includes Node, Ruby, and Python variants in vanilla-html. Previously hostname validation was described in prose but not enforced in the code snippets.
  • README.md recommends the git-clone/symlink install path; the single-file curl bootstrap is a read-only fallback with a note that persist-skill.sh requires the cloned bundle.
  • Delete fetch-secret.sh (superseded by the in-flow existing-widget recovery in SKILL.md, no callers remain).

Layout

File Purpose
skills/turnstile-spin/SKILL.md Canonical machine-readable wizard for the agent
skills/turnstile-spin/scripts/auth-probe.sh Cloudflare token account + Turnstile:Edit scope probe
skills/turnstile-spin/scripts/widget-create.sh Create widget via POST /accounts/:id/challenges/widgets
skills/turnstile-spin/scripts/validate.sh Dummy-token siteverify + hostname sanity check
skills/turnstile-spin/scripts/persist-skill.sh Install canonical bundle into .claude/skills/turnstile-spin/ (or equivalent)
skills/turnstile-spin/references/*.md Per-framework snippets (vanilla-html, nextjs-app, nextjs-pages, astro, sveltekit, hugo)
skills/turnstile-spin/tests/validation.md Validation cases
skills/turnstile-spin/README.md Install path + skill-authoring notes

Each script emits structured JSON on stdout, human diagnostics on stderr, and pinned exit codes.

Companion

Verification

  • bash -n and exec bit on all four scripts
  • Safe smoke tests: missing_token, file_target_not_supported, empty-array rejection, unsupported-mode rejection
  • python3 -I isolation confirmed by poisoning urllib.py / json.py / hashlib.py in a temp CWD
  • Byte-for-byte sync with the hosted prompt.md mirror in cloudflare-docs

…ption

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
juleslemee pushed a commit to juleslemee/cloudflare-docs that referenced this pull request Jul 17, 2026
…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)
@juleslemee
juleslemee marked this pull request as ready for review July 17, 2026 21:33
@juleslemee

Copy link
Copy Markdown
Contributor Author

cc @marinaelmore — this is the canonical-skill half of the Spin V2 token-reset hotfix. Companion PRs where you're already auto-added as reviewer:

Ready for review; hoping to merge Monday AM.

Jules Lemee added 2 commits July 21, 2026 14:58
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).
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).
juleslemee pushed a commit to juleslemee/cloudflare-docs that referenced this pull request Jul 21, 2026
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
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.
juleslemee pushed a commit to juleslemee/cloudflare-docs that referenced this pull request Jul 21, 2026
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.
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.
juleslemee pushed a commit to juleslemee/cloudflare-docs that referenced this pull request Jul 21, 2026
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.
@juleslemee

Copy link
Copy Markdown
Contributor Author

@marinaelmore GitHub won't let me add you as a formal reviewer here (fork PR, non-codeowner restriction), but you can submit an approve directly from the Files tab without being requested. This PR is the canonical skills half of the same three-repo Turnstile Spin V2 change you're already reviewing in cloudflare/cloudflare-docs#32147.

Jules Lemee added 3 commits July 22, 2026 13:30
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.
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.
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.
@juleslemee juleslemee changed the title turnstile-spin: guide token reset after submit to fix V2 double-redemption turnstile-spin: canonical skill refresh with independent-review hardening Jul 23, 2026
marinaelmore pushed a commit to cloudflare/cloudflare-docs that referenced this pull request Jul 23, 2026
…n.mdx (#32147)

* [Turnstile] Set autocomplete hints on client-side rendering examples (VULN-140143)

* [Turnstile] Spin: document dashboard setup path alongside command-line

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).

* [Turnstile] Spin docs: address style-bot suggestions

- 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'.

* [Turnstile] Spin V2 docs: point references at prompt.md

Match the rename in #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 #31532 merges first.

* [Turnstile] Spin V2 docs: dashboard recovery is the primary path, agent recovery is the fallback

* [Turnstile] Spin docs: pivot to canonical siteverify

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.

* [Turnstile] Spin V2 sync: prompt.md + Wrangler CLI setup path

- 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.

* [Turnstile] Drop Wrangler version note from Spin CLI section

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.

* [Turnstile] Fix Spin docs accuracy (removed column, local dev domains, marker wording)

* [Turnstile] Drop Spin beta tag; bump reviewed date

* [Turnstile] Redirect old Spin index.md to prompt.md

* Revert "[Turnstile] Redirect old Spin index.md to prompt.md"

This reverts commit 2985122.

* [Turnstile] Spin docs: spell out insertion-preference examples per review feedback

* [Turnstile] Spin: sync prompt.md to SKILL.md and guide token reset for 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)

* [Turnstile] Spin docs: broaden scope beyond forms per review

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

* Harden Turnstile Spin helper scripts (docs-bot findings)

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.

* Harden Turnstile Spin helper scripts (second pass on docs-bot findings)

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.

* 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

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).

* [Turnstile] Spin: sync prompt.md to stratus SPIN_SKILL_CONTENT (latest)

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.

* [Turnstile] Spin: harden hosted prompt, scripts, and spin.mdx

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).

---------

Co-authored-by: jules lemee <jules@cloudflare.com>
@dmmulroy
dmmulroy merged commit 30553f8 into cloudflare:main Jul 24, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants