Provision Turnstile

What is created, and where

pithy add turnstile wrote the wiring; pithy turnstile is the command; Using Turnstile is the gate itself; the reference has the keys.

EnvironmentWhat it gets
devCloudflare’s documented test keys. No widget created
stagingThe same test keys. No widget created
prodA real widget, its sitekey written to config and its secret to the store

Only production gets a real widget, and that is the design rather than a shortcut. The test keys need no Cloudflare round-trip and make the pass, block and forced-challenge paths trivially testable.

The test keys

Cloudflare publishes them, and the kit wires the always-pass pair:

Visible, always passes1x00000000000000000000AA
Invisible, always passes1x00000000000000000000BB
Visible, always blocks2x00000000000000000000AB
Always forces an interaction3x00000000000000000000FF

The sitekeys are public — the front end renders with them. The secrets feed siteverify.

The test key answers with no action, and the gate knows

This is the one exception to the action binding, and without it dev and staging sign-in was impossible.

Cloudflare’s always-pass secret answers success: true with no action field at all. The auth capability stacks its gate with an expected action, so the binding compared that action against nothing and denied every sign-in in exactly the two environments provisioning wires the test key into — a token that was valid, refused by a field the key never populates.

The exception needs all three of:

  1. Cloudflare says it is a test key — its own flag on its own answer, not a comparison against a list of key strings the kit keeps. A real widget’s answer never carries it, so no real deployment can reach this branch however its secret is spelled.
  2. No action came back at all. An action that came back and differs is a token minted for another action — exactly what the binding exists to refuse, and it is refused here too.
  3. The Worker says it is dev or staging — read off the stamped environment var, which nothing in a request can influence. prod is not in that list, and neither is an unstamped Worker.

The alternative was to relax the binding itself, which would have traded a real protection — a token solved for one action must not be replayable against another — for a developer’s convenience.

The widget’s name carries the project

<project>-prod-turnstile-<mode>.

Provisioning is reuse-or-create by name, and Turnstile widgets are account-scoped with a flat list. An unscoped name means a second Pithy project in one account adopts the first’s widget — and turnstile deprovision deletes it out from under them.

prod sits in the environment slot because that is the environment this widget serves, and it is the only one.

Three writes that must agree

The environment appears in the widget’s own name, in the secret write, and in the sitekey write.

A widget provisioned under one environment and a secret written under another is a production login page verifying against a key nobody holds. So the environment is named once in the code rather than spelled at each call site.

What the front end reads

The public sitekey for each mode is surfaced as a Worker var — TURNSTILE_SITEKEY_VISIBLE, TURNSTILE_SITEKEY_INVISIBLE — and reaches the browser through the client projection, indexed by environment name.

ESC