[wrangler] Add turnstile widget CLI commands#14511
Conversation
🦋 Changeset detectedLatest commit: d9e466a The changes in this PR will be included in the next version bump. This PR includes changesets to release 3 packages
Not sure what this means? Click here to learn what changesets are. Click here if you're a maintainer who wants to add another changeset to this PR |
575b9c8 to
79b9217
Compare
|
Codeowners approval required for this PR:
Show detailed file reviewers
|
79b9217 to
cc8356e
Compare
cc8356e to
157dd73
Compare
|
There's some merge conflicts that need resolving @juleslemee |
@cloudflare/autoconfig
create-cloudflare
@cloudflare/deploy-helpers
@cloudflare/kv-asset-handler
miniflare
@cloudflare/pages-shared
@cloudflare/unenv-preset
@cloudflare/vite-plugin
@cloudflare/vitest-pool-workers
@cloudflare/workers-auth
@cloudflare/workers-editor-shared
@cloudflare/workers-utils
wrangler
commit: |
Adds `wrangler turnstile widget create | list | get | update | delete` so developers and AI coding agents can provision Cloudflare Turnstile widgets from the CLI instead of dashboard-only. Modelled on the hyperdrive command structure (createNamespace + createCommand, single-purpose files per subcommand, cfetch client layer). Requests the existing `challenge-widgets.write` OAuth scope during `wrangler login`; the scope was added to wrangler's prod OAuth client allowlist via cloudflare/iam/terraform-internal-oauth-clients !137 (already applied). Behaviour notes: - `create` prints sitekey + secret + a backend-agnostic siteverify pointer (does not prescribe Workers as the backend). - `update` does GET-then-merge-then-PUT since Turnstile's PUT requires the full body; strips `secret` from output to avoid an incidental leak when the user only intended to rename. - `delete` prompts for confirmation (`--skip-confirmation`/`-y` bypasses); `fallbackValue: false` in non-interactive contexts. - `create`/`list`/`update` accept `--json`; `get` is always JSON. Banner is suppressed with `--json` and in CI so output is pipeable. - `--domain` accepts comma-separated values in addition to repeated flags. - No new Cloudflare TypeScript SDK dependency (matches the hyperdrive pattern in the same repo); the surface can be migrated later. 19 unit tests via msw. Live-verified end to end against the API using both an API token and the OAuth flow with the newly-allowlisted scope. Companion changes (out of scope here): - cloudflare/skills#73 - canonical SKILL.md rewrite for Turnstile Spin - cloudflare/cloudflare-docs#31517 - public docs page - stratus !39261 - dashboard Spin Lite dialog Bach and OAuth-client sides: - `challenge-widgets.write` OAuth scope was already registered in bach (derived from the `challenge_widgets_write` api_tokens role); no bach change was needed. - cloudflare/iam/terraform-internal-oauth-clients !137 added `challenge-widgets.write` to the wrangler prod client's scopes list (applied + merged).
157dd73 to
27a783b
Compare
|
Thanks for the review! All five comments addressed in 27a783b:
Also rebased onto latest One follow-up worth flagging: I added a Windows CI failure is in |
The backend now derives attribution from the request's User-Agent header (`wrangler/<version>`) and stores it on the widget row as `deployed_via = 'wrangler'`. The name-suffix workaround is no longer needed and disappears from the customer-visible widget name.
|
Codeowners approval required for this PR:
Show detailed file reviewers |
Let fetchResult errors propagate from deleteWidget rather than rewrapping them as UserError. Matches queues/flagship convention and lets server-side API failures reach Sentry as regular Errors while truly user-driven failures (like a missing sitekey) still surface via fetchResult's own error shaping. The sitekey stays visible in the CLI context so users know which widget failed.
The changeset still said the secret was 'shown only at creation time', which contradicted the create output rewrite that now points users at `wrangler turnstile widget get` for later retrieval. Bring the changelog wording in line with what the command actually prints.
workers-devprod
left a comment
There was a problem hiding this comment.
Codeowners reviews satisfied
Match the Flagship delete pattern: --json requires --skip-confirmation
so the interactive prompt cannot corrupt the JSON output. Emits
{"sitekey": "...", "success": true} on success; throws a
JsonFriendlyFatalError when -y is missing.
This PR adds a new
wrangler turnstile widgetcommand surface (create, list, get, update, delete) so developers and AI agents can set up Cloudflare Turnstile widgets from the CLI without going through the dashboard.What's in the PR
The new code is in
packages/wrangler/src/turnstile/, roughly 470 LOC across 8 files, modeled on the existing hyperdrive command structure.index.tsdeclares the two namespaces and a shared option builder,client.tsholds the types and five API calls (usingfetchResultfrom../cfetch, same pattern as hyperdrive, r2, and d1), and each subcommand gets its own file. There's also a smallshared.tsfor formatters.A few things worth knowing about how the commands behave:
createprints the sitekey, the secret, and a backend-agnostic siteverify pointer (POST tochallenges.cloudflare.com/turnstile/v0/siteverifywith{ secret, response }). It intentionally doesn't prescribe Workers as the backend, since customers verify from Node, Python, Go, Pages, or wherever their existing app runs.updatedoes GET-then-merge-then-PUT because Turnstile's PUT endpoint requires the full widget body. It also strips the secret from the response before printing, so someone who just wanted to rename a widget doesn't get the secret dumped into their scrollback.getstill returns the secret if you actually need it.deleteprompts for confirmation and takes--skip-confirmation(-y) to bypass. The non-interactive fallback is "no" instead of "yes", so a CI script without-yaborts safely rather than quietly deleting.create,list, andupdateaccept--json;getis always JSON. The banner is suppressed when--jsonis set or when running in CI, so the output pipes cleanly throughjq. I ran into this during dogfooding and fixed it early.--domainaccepts comma-separated values in addition to repeated flags. Yargs'sarray: trueon its own preserves commas as literal characters, so there's a smallcoercesplitter that trims and drops empties.Two smaller quality-of-life touches:
updatefast-fails with a helpful message when no fields are specified (instead of doing a wasteful no-op GET+PUT), andlistprints "Found N widget(s):" before the table or "No Turnstile widgets found." for the empty state.Also touched:
packages/wrangler/src/user/user.tsadds"challenge-widgets.write": "See and change Turnstile widgets"toDefaultScopes. The scope was already registered in bach (derived from the existingchallenge_widgets_writeapi_tokens role), and I added it to the wrangler prod OAuth client's allowlist via cloudflare/iam/terraform-internal-oauth-clients !137 (already applied and merged).packages/wrangler/src/core/teams.d.tsadds"Product: Turnstile".packages/wrangler/src/index.tsgets the imports and registry block for the new namespace and five subcommands.Testing
There are 21 unit tests in
src/__tests__/turnstile.test.ts, using msw for mocking. They cover the happy paths, missing-arg errors,--jsonoutput, the empty-list message, comma-split domains, the GET-then-PUT merge behavior in update, the delete confirmation flow with and without-y, and the no-op update fast-fail. The mock handlers also assert that requests carryContent-Type: application/json, because Turnstile's API rejectstext/plainbodies (which is what undici's fetch defaults to when the body is a JSON string), and the client sets the header explicitly.I verified the whole flow live against the real API on my personal account. First round used a personal API token with
Account.Turnstile:Editto validate the commands themselves. Second round used the new OAuth flow, after thechallenge-widgets.writescope was allowlisted in prod hydra: I ranwrangler login, saw the consent screen show "Turnstile Sites Write" under App Security, completed the token exchange, then ran create, get, update, and delete against my personal account. All succeeded.Snapshot updates in
user.test.ts,whoami.test.ts,profiles.test.ts,deploy/core.test.ts,index.test.ts, andexperimental-commands-api.test.tsare mechanical: the new scope shows up in the URL-encoded scope strings and in the whoami permission bullet list, and the five new subcommand names show up in the top-level help snapshots.Context
Dane Knecht, Peter Bacon Darwin, Maxime Guerreiro, and Skye Roberts all endorsed this direction last week in the Workers leadership chat. A few companion changes are shipping in parallel:
wrangler turnstile widget createas the preferred provisioning path.!39261(internal GitLab) is the dashboard Spin Lite dialog with a matching agent prompt.Two things I'm intentionally not doing in this PR:
An
env.TURNSTILE.verifyWorkers binding for a typed siteverify path from Workers-hosted backends. Dane also greenlit this in the same thread, but it's a multi-repo epic that spans workerd, wrangler config, cf-api metadata, and workers-types. Good follow-up quarter, not a bolt-on to this PR.The staging wrangler OAuth client scope allowlist. The staging config in the terraform repo is intentionally minimal (5 scopes versus prod's 80+); adding
challenge-widgets.writeto staging is a small follow-up MR if internal staging testing of these commands becomes a workstream.