Makes Cloudflare Turnstile the bot-protection provider of a Laioutr storefront. Every action the project protects must carry a Turnstile token, and the server checks it with Cloudflare before the action runs. Most visitors never see Turnstile. When Cloudflare asks a visitor to confirm they are human, the app shows the widget in a small dialog.
Warning
A visitor whose browser cannot reach Cloudflare cannot run a protected action, even with
whenUnavailable: "open". This includes a blocked or failed load of Turnstile's script and a browser
Turnstile does not support. open covers only an outage that the server sees. A browser cannot prove
an outage, because every bot could make the same claim.
-
Create a widget in your own Cloudflare account: Turnstile → Add widget, mode Managed. Add every hostname the storefront answers on, including preview hostnames. On the Free plan a widget takes up to 10 hostnames, and an account up to 20 widgets. Verifications have no limit.
-
Install the app and configure it with the widget's two keys. List the actions to protect in the project's bot-protection configuration:
{ "apps": [ { "name": "@laioutr/app-turnstile", "config": { "siteKey": "0x…", "secretKey": "0x…" } } ], "config": { "botProtection": { "actions": ["<action token>"] } } }The site key reaches the browser. The secret stays on the server. If either key is missing, the app logs a warning at build time and registers nothing, so every protected action is rejected.
Nothing loads before the first protected action. On that action, the browser loads
https://challenges.cloudflare.com/turnstile/v0/api.js and Cloudflare runs its challenge. Whether that
needs the visitor's consent is for the storefront operator to decide.
The dialog is a native <dialog data-laioutr-turnstile> with data-state="open" | "closed". Its
default styles use :where(…), so any rule in the storefront's stylesheet overrides them without
!important. Escape and the close control both cancel the action, and the action then sends no
request.
With a Content Security Policy, allow Cloudflare in two directives, or the browser blocks Turnstile and every protected action fails:
script-src https://challenges.cloudflare.com;
frame-src https://challenges.cloudflare.com;
A nonce-based policy needs 'strict-dynamic'. The app inserts Turnstile's script from its own code and
does not set a nonce on it, so the script loads only when your trusted scripts may load further ones.
The dialog's default styles are an inline <style>. A policy without style-src 'unsafe-inline' blocks
them. The dialog still works, but unstyled, so style it in your own stylesheet.
Cloudflare's test keys work on any hostname, including localhost:
| Site key | Behaviour |
|---|---|
1x00000000000000000000BB |
Always passes, invisible |
2x00000000000000000000BB |
Always fails, invisible |
3x00000000000000000000FF |
Forces an interactive challenge |
| Secret | Behaviour |
|---|---|
1x0000000000000000000000000000000AA |
Always passes |
2x0000000000000000000000000000000AA |
Always fails |
3x0000000000000000000000000000000AA |
Returns "token already spent" |
A test secret accepts every token, so the server logs a warning at startup when one is configured. Never ship a test secret.
Requires Node.js 22.12 or newer and pnpm 10 or newer. The committed .npmrc maps the Laioutr scopes
to npm.laioutr.cloud.
npm login --registry https://npm.laioutr.cloud— once per machine. See the Laioutr NPM Guide.pnpm installpnpm dev— starts the playground on http://localhost:3000. Its page runs one protected action against Cloudflare. Choose the keys withTURNSTILE_SITE_KEYandTURNSTILE_SECRET_KEY. Without them the playground uses the always-passing test keys.
We use ESLint and Prettier to lint and format the code. This repository contains opinionated configurations for both tools. You can, of course, replace them with your own configurations.
Releases run through changesets and publish to npmjs.org with npm trusted publishing, so CI needs no npm token and every release carries provenance.
Day to day: run pnpm changeset to describe your change and merge it. The release workflow opens a
"chore: release" PR collecting the pending changesets; merging that builds and publishes.
-
Repository secrets
NPM_LAIOUTR_TOKEN— read access to npm.laioutr.cloud, so CI can install@laioutr-core/*.RELEASE_TOKEN— a fine-grained PAT owned by the org, scoped to this repo, with Contents: read and write and Pull requests: read and write. A PR opened with the defaultGITHUB_TOKENcannot trigger workflows, so release PRs would arrive with no CI and could never satisfy a required-status rule.
-
Bootstrap the package on npm. Trusted publishing is configured on a package that already exists, so the very first version has to be published by hand.
publishConfig.provenancefails outside CI — there is no OIDC provider — so disable it for that one publish:pnpm prepack npm publish --access public --no-provenance
A brand-new package can 404 on the registry for a few minutes afterwards. That is replication lag, not a failed publish; check again before re-running anything.
-
Configure the trusted publisher on the package's npm settings page: GitHub Actions, this repository, workflow
release.yml. Every release after that is tokenless.
If you want to publish a private package to npm.laioutr.cloud, you need to:
- Log in with publish access:
npm login --registry https://npm.laioutr.cloud, then enable Publish on the authorize screen. Keep the token in your user~/.npmrc— the project's.npmrcis committed. - Add this line to the root of the
package.jsonfile:"publishConfig": { "registry": "https://npm.laioutr.cloud/" } - Make sure your package-name follows the
@laioutr-org/<organization-slug>__<package-name>format.
After that you can run pnpm release to publish the package to npm.laioutr.cloud.
More information for publishing can be found in the NPM Guide.
Follow the setup guide to get started.