Skip to content

[wrangler] Add turnstile widget CLI commands#14511

Merged
penalosa merged 11 commits into
cloudflare:mainfrom
juleslemee:wrangler-turnstile
Jul 8, 2026
Merged

[wrangler] Add turnstile widget CLI commands#14511
penalosa merged 11 commits into
cloudflare:mainfrom
juleslemee:wrangler-turnstile

Conversation

@juleslemee

@juleslemee juleslemee commented Jul 1, 2026

Copy link
Copy Markdown
Contributor

This PR adds a new wrangler turnstile widget command 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.ts declares the two namespaces and a shared option builder, client.ts holds the types and five API calls (using fetchResult from ../cfetch, same pattern as hyperdrive, r2, and d1), and each subcommand gets its own file. There's also a small shared.ts for formatters.

A few things worth knowing about how the commands behave:

create prints the sitekey, the secret, and a backend-agnostic siteverify pointer (POST to challenges.cloudflare.com/turnstile/v0/siteverify with { 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.

update does 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. get still returns the secret if you actually need it.

delete prompts for confirmation and takes --skip-confirmation (-y) to bypass. The non-interactive fallback is "no" instead of "yes", so a CI script without -y aborts safely rather than quietly deleting.

create, list, and update accept --json; get is always JSON. The banner is suppressed when --json is set or when running in CI, so the output pipes cleanly through jq. I ran into this during dogfooding and fixed it early.

--domain accepts comma-separated values in addition to repeated flags. Yargs's array: true on its own preserves commas as literal characters, so there's a small coerce splitter that trims and drops empties.

Two smaller quality-of-life touches: update fast-fails with a helpful message when no fields are specified (instead of doing a wasteful no-op GET+PUT), and list prints "Found N widget(s):" before the table or "No Turnstile widgets found." for the empty state.

Also touched:

packages/wrangler/src/user/user.ts adds "challenge-widgets.write": "See and change Turnstile widgets" to DefaultScopes. The scope was already registered in bach (derived from the existing challenge_widgets_write api_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.ts adds "Product: Turnstile". packages/wrangler/src/index.ts gets 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, --json output, 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 carry Content-Type: application/json, because Turnstile's API rejects text/plain bodies (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:Edit to validate the commands themselves. Second round used the new OAuth flow, after the challenge-widgets.write scope was allowlisted in prod hydra: I ran wrangler 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, and experimental-commands-api.test.ts are 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:

  • cloudflare/skills#73 rewrites the canonical Turnstile Spin SKILL.md so that once this ships, AI agents can recommend wrangler turnstile widget create as the preferred provisioning path.
  • cloudflare/cloudflare-docs#31517 is the public docs page for the Spin flow.
  • Stratus !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.verify Workers 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.write to staging is a small follow-up MR if internal staging testing of these commands becomes a workstream.


@changeset-bot

changeset-bot Bot commented Jul 1, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: d9e466a

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 3 packages
Name Type
wrangler Minor
@cloudflare/vite-plugin Patch
@cloudflare/vitest-pool-workers Patch

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

@juleslemee juleslemee force-pushed the wrangler-turnstile branch from 575b9c8 to 79b9217 Compare July 1, 2026 16:06
@juleslemee juleslemee marked this pull request as ready for review July 1, 2026 16:10
@workers-devprod workers-devprod requested review from a team and NuroDev and removed request for a team July 1, 2026 16:11
@workers-devprod

workers-devprod commented Jul 1, 2026

Copy link
Copy Markdown
Contributor

Codeowners approval required for this PR:

  • @cloudflare/wrangler
Show detailed file reviewers
  • .changeset/wrangler-turnstile.md: [@cloudflare/wrangler]
  • packages/wrangler/src/tests/deploy/core.test.ts: [@cloudflare/wrangler]
  • packages/wrangler/src/tests/experimental-commands-api.test.ts: [@cloudflare/wrangler]
  • packages/wrangler/src/tests/index.test.ts: [@cloudflare/wrangler]
  • packages/wrangler/src/tests/profiles.test.ts: [@cloudflare/wrangler]
  • packages/wrangler/src/tests/turnstile.test.ts: [@cloudflare/wrangler]
  • packages/wrangler/src/tests/user.test.ts: [@cloudflare/wrangler]
  • packages/wrangler/src/tests/whoami.test.ts: [@cloudflare/wrangler]
  • packages/wrangler/src/core/teams.d.ts: [@cloudflare/wrangler]
  • packages/wrangler/src/index.ts: [@cloudflare/wrangler]
  • packages/wrangler/src/turnstile/client.ts: [@cloudflare/wrangler]
  • packages/wrangler/src/turnstile/create.ts: [@cloudflare/wrangler]
  • packages/wrangler/src/turnstile/delete.ts: [@cloudflare/wrangler]
  • packages/wrangler/src/turnstile/get.ts: [@cloudflare/wrangler]
  • packages/wrangler/src/turnstile/index.ts: [@cloudflare/wrangler]
  • packages/wrangler/src/turnstile/list.ts: [@cloudflare/wrangler]
  • packages/wrangler/src/turnstile/update.ts: [@cloudflare/wrangler]
  • packages/wrangler/src/user/user.ts: [@cloudflare/wrangler]

devin-ai-integration[bot]

This comment was marked as resolved.

@juleslemee juleslemee force-pushed the wrangler-turnstile branch from 79b9217 to cc8356e Compare July 1, 2026 16:21
devin-ai-integration[bot]

This comment was marked as resolved.

@juleslemee juleslemee force-pushed the wrangler-turnstile branch from cc8356e to 157dd73 Compare July 1, 2026 16:47
Comment thread packages/wrangler/src/turnstile/index.ts Outdated
Comment thread packages/wrangler/src/turnstile/shared.ts Outdated
Comment thread packages/wrangler/src/turnstile/index.ts Outdated
Comment thread packages/wrangler/src/turnstile/delete.ts
Comment thread packages/wrangler/src/turnstile/get.ts Outdated
@NuroDev

NuroDev commented Jul 2, 2026

Copy link
Copy Markdown
Member

There's some merge conflicts that need resolving @juleslemee

@pkg-pr-new

pkg-pr-new Bot commented Jul 2, 2026

Copy link
Copy Markdown
@cloudflare/autoconfig

npm i https://pkg.pr.new/@cloudflare/autoconfig@14511

create-cloudflare

npm i https://pkg.pr.new/create-cloudflare@14511

@cloudflare/deploy-helpers

npm i https://pkg.pr.new/@cloudflare/deploy-helpers@14511

@cloudflare/kv-asset-handler

npm i https://pkg.pr.new/@cloudflare/kv-asset-handler@14511

miniflare

npm i https://pkg.pr.new/miniflare@14511

@cloudflare/pages-shared

npm i https://pkg.pr.new/@cloudflare/pages-shared@14511

@cloudflare/unenv-preset

npm i https://pkg.pr.new/@cloudflare/unenv-preset@14511

@cloudflare/vite-plugin

npm i https://pkg.pr.new/@cloudflare/vite-plugin@14511

@cloudflare/vitest-pool-workers

npm i https://pkg.pr.new/@cloudflare/vitest-pool-workers@14511

@cloudflare/workers-auth

npm i https://pkg.pr.new/@cloudflare/workers-auth@14511

@cloudflare/workers-editor-shared

npm i https://pkg.pr.new/@cloudflare/workers-editor-shared@14511

@cloudflare/workers-utils

npm i https://pkg.pr.new/@cloudflare/workers-utils@14511

wrangler

npm i https://pkg.pr.new/wrangler@14511

commit: d9e466a

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).
@juleslemee juleslemee force-pushed the wrangler-turnstile branch from 157dd73 to 27a783b Compare July 2, 2026 15:01
@juleslemee

juleslemee commented Jul 2, 2026

Copy link
Copy Markdown
Contributor Author

Thanks for the review! All five comments addressed in 27a783b:

  • Description spacing fixed (single space after the emoji).
  • shared.ts deleted, widgetRowForTable inlined into list.ts.
  • sharedWidgetOptions now uses the options object pattern with generic-preserved narrowing.
  • delete wraps the API call in try/catch and rethrows as a UserError naming the sitekey and cause.
  • get default is now a padded key-value view; --json opts into raw JSON.

Also rebased onto latest main (the flagship PR added imports next to mine in index.ts and teams.d.ts). No functional change from the rebase.

One follow-up worth flagging: I added a (Wrangler) name suffix on widget creation, matching the Spin dashboard's (Spin) convention (dashboard detection lives in useGetSpinDeployedSitekeys.tsx and matches on /\s\(Spin\)\s*$/). This gives the dashboard a durable, queryable marker for wrangler-created widgets. The analogous stratus-side detection for (Wrangler) will be a separate follow-up MR.

Windows CI failure is in miniflare/test/plugins/browser/index.spec.ts (browser rendering plugin, unrelated to this PR).

devin-ai-integration[bot]

This comment was marked as resolved.

juleslemee and others added 3 commits July 2, 2026 10:40
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.
@workers-devprod

workers-devprod commented Jul 7, 2026

Copy link
Copy Markdown
Contributor

Codeowners approval required for this PR:

  • ✅ @cloudflare/wrangler
Show detailed file reviewers

devin-ai-integration[bot]

This comment was marked as resolved.

Jules Lemee and others added 3 commits July 7, 2026 09:57
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.
Comment thread packages/wrangler/src/turnstile/delete.ts

@workers-devprod workers-devprod left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Codeowners reviews satisfied

@github-project-automation github-project-automation Bot moved this from Untriaged to Approved in workers-sdk Jul 8, 2026
devin-ai-integration[bot]

This comment was marked as resolved.

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.
devin-ai-integration[bot]

This comment was marked as resolved.

devin-ai-integration[bot]

This comment was marked as resolved.

@penalosa penalosa enabled auto-merge (squash) July 8, 2026 23:34
@penalosa penalosa merged commit 17d2fc1 into cloudflare:main Jul 8, 2026
55 checks passed
@github-project-automation github-project-automation Bot moved this from Approved to Done in workers-sdk Jul 8, 2026
@juleslemee juleslemee deleted the wrangler-turnstile branch July 9, 2026 01:45
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: Done

Development

Successfully merging this pull request may close these issues.

4 participants