mirror of
https://github.com/prdlk/dotfiles.git
synced 2026-11-06 20:47:36 +00:00
refactor(turnstile-spin): tighten auth handling, expand documentation, and add tldraw-offline matcher
This commit is contained in:
1 parent
20dc5f4489
commit
03ef5c94ea
14 files changed
+1559
-499
No files matched your search
@@ -1,6 +1,6 @@
|
||||
# Vanilla HTML
|
||||
|
||||
For static sites or any project without a JS framework. The form posts directly to the Worker, no client-side JavaScript required for the integration itself.
|
||||
For static sites or any project without a JS framework. The widget renders client-side; the form submits to whatever backend handles your form (a Node/PHP/Ruby/Go server, a Cloudflare Worker, a Pages Function, a third-party form host that supports server-side hooks, etc.).
|
||||
|
||||
```html
|
||||
<!doctype html>
|
||||
@@ -13,12 +13,12 @@ For static sites or any project without a JS framework. The form posts directly
|
||||
></script>
|
||||
</head>
|
||||
<body>
|
||||
<form action="https://YOUR_WORKER_URL/" method="POST">
|
||||
<form action="/api/subscribe" method="POST">
|
||||
<input name="email" type="email" required />
|
||||
<div
|
||||
class="cf-turnstile"
|
||||
data-sitekey="YOUR_SITEKEY"
|
||||
data-action="turnstile-spin-v1"
|
||||
data-action="subscribe"
|
||||
></div>
|
||||
<button type="submit">Subscribe</button>
|
||||
</form>
|
||||
@@ -26,37 +26,132 @@ For static sites or any project without a JS framework. The form posts directly
|
||||
</html>
|
||||
```
|
||||
|
||||
When the form is submitted, the browser includes `cf-turnstile-response` automatically. The Worker reads it (alias for `token`) and responds with siteverify JSON.
|
||||
When the form submits, the browser includes `cf-turnstile-response` automatically. Your backend reads it and calls canonical siteverify.
|
||||
|
||||
## Where to put the snippet
|
||||
## Backend (any language)
|
||||
|
||||
- Single-page site: in the `<body>` of `index.html`, wherever the form lives.
|
||||
- Multi-page site: in each page that has a form. The script tag in `<head>` can live in a shared layout.
|
||||
Add this to your existing `/api/subscribe` handler before the rest of its logic:
|
||||
|
||||
## Substitutions
|
||||
```js
|
||||
// Node / fetch idiom
|
||||
const expectedHostnames = new Set(
|
||||
(process.env.TURNSTILE_HOSTNAMES ?? '')
|
||||
.split(',')
|
||||
.map((h) => h.trim())
|
||||
.filter(Boolean),
|
||||
);
|
||||
if (expectedHostnames.size === 0) return res.status(403).end();
|
||||
|
||||
| Placeholder | Replace with |
|
||||
| ------------------ | ------------------------------------------------------- |
|
||||
| `YOUR_WORKER_URL` | The deployed Worker URL from Step 5 |
|
||||
| `YOUR_SITEKEY` | The widget site key from Step 4 |
|
||||
const token = req.body['cf-turnstile-response'];
|
||||
const r = await fetch('https://challenges.cloudflare.com/turnstile/v0/siteverify', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
||||
body: new URLSearchParams({
|
||||
secret: process.env.TURNSTILE_SECRET,
|
||||
response: token,
|
||||
remoteip: req.ip,
|
||||
}),
|
||||
});
|
||||
const result = await r.json();
|
||||
if (
|
||||
r.ok !== true ||
|
||||
result.success !== true ||
|
||||
result.action !== 'subscribe' ||
|
||||
!expectedHostnames.has(result.hostname)
|
||||
) {
|
||||
return res.status(403).end();
|
||||
}
|
||||
// existing handler logic runs here
|
||||
```
|
||||
|
||||
Equivalent calls in other backend languages (each also compares `result.hostname` to a `TURNSTILE_HOSTNAMES` allowlist):
|
||||
|
||||
```ruby
|
||||
# Ruby
|
||||
require 'net/http'; require 'uri'; require 'json'; require 'set'
|
||||
expected_hostnames = (ENV['TURNSTILE_HOSTNAMES'] || '').split(',').map(&:strip).reject(&:empty?).to_set
|
||||
halt 403 if expected_hostnames.empty?
|
||||
res = Net::HTTP.post_form(URI('https://challenges.cloudflare.com/turnstile/v0/siteverify'),
|
||||
secret: ENV['TURNSTILE_SECRET'], response: params['cf-turnstile-response'], remoteip: request.ip)
|
||||
result = JSON.parse(res.body)
|
||||
halt 403 unless res.is_a?(Net::HTTPSuccess) && result['success'] == true && result['action'] == 'subscribe' && expected_hostnames.include?(result['hostname'])
|
||||
```
|
||||
|
||||
```python
|
||||
# Python (requests)
|
||||
expected_hostnames = {h.strip() for h in os.environ.get('TURNSTILE_HOSTNAMES', '').split(',') if h.strip()}
|
||||
if not expected_hostnames:
|
||||
return '', 403
|
||||
r = requests.post('https://challenges.cloudflare.com/turnstile/v0/siteverify',
|
||||
data={'secret': os.environ['TURNSTILE_SECRET'],
|
||||
'response': form['cf-turnstile-response'],
|
||||
'remoteip': request.remote_addr})
|
||||
result = r.json()
|
||||
if (not r.ok or result.get('success') is not True or result.get('action') != 'subscribe'
|
||||
or result.get('hostname') not in expected_hostnames):
|
||||
return '', 403
|
||||
```
|
||||
|
||||
`subscribe` is the stable action for this surface. Preserve an existing custom migration action and compare the returned action to the same value. Siteverify is mandatory for every widget mode, including pre-clearance. Set `TURNSTILE_HOSTNAMES` to the deployment-specific frontend hostnames; a production value must not include `localhost` or `127.0.0.1`.
|
||||
|
||||
## Variant: AJAX submit instead of form action
|
||||
|
||||
If the form is submitted via `fetch` instead of a native form post, the snippet still works. The widget div populates a hidden `cf-turnstile-response` input that you can read from `new FormData(form)`.
|
||||
For an AJAX flow, replace the native form and API script with explicit rendering. Keep this surface's widget ID and reset it in `finally`, which covers network, JSON, validation, and server failures as well as successful same-page completion.
|
||||
|
||||
```html
|
||||
<form id="subscribe-form">
|
||||
<input name="email" type="email" required />
|
||||
<div id="subscribe-turnstile"></div>
|
||||
<button type="submit">Subscribe</button>
|
||||
</form>
|
||||
<script>
|
||||
document.querySelector("form").addEventListener("submit", async (e) => {
|
||||
e.preventDefault();
|
||||
const data = new FormData(e.target);
|
||||
const res = await fetch("https://YOUR_WORKER_URL/", {
|
||||
method: "POST",
|
||||
body: data,
|
||||
let subscribeWidgetId;
|
||||
|
||||
window.onSubscribeTurnstileLoad = () => {
|
||||
subscribeWidgetId = window.turnstile.render("#subscribe-turnstile", {
|
||||
sitekey: "YOUR_SITEKEY",
|
||||
action: "subscribe",
|
||||
});
|
||||
const json = await res.json();
|
||||
if (json.success) {
|
||||
};
|
||||
|
||||
document.getElementById("subscribe-form").addEventListener("submit", async (event) => {
|
||||
event.preventDefault();
|
||||
try {
|
||||
const res = await fetch("/api/subscribe", {
|
||||
method: "POST",
|
||||
body: new FormData(event.currentTarget),
|
||||
});
|
||||
const json = await res.json();
|
||||
if (!res.ok || json.ok !== true) throw new Error("Submission failed");
|
||||
// proceed
|
||||
} catch {
|
||||
// surface the error
|
||||
} finally {
|
||||
if (subscribeWidgetId !== undefined) {
|
||||
window.turnstile.reset(subscribeWidgetId);
|
||||
}
|
||||
}
|
||||
});
|
||||
</script>
|
||||
<script
|
||||
src="https://challenges.cloudflare.com/turnstile/v0/api.js?onload=onSubscribeTurnstileLoad&render=explicit"
|
||||
async
|
||||
defer
|
||||
></script>
|
||||
```
|
||||
|
||||
## No backend?
|
||||
|
||||
If your project is pure-static (no server-side handler — just HTML served from a CDN), Spin doesn't apply. Siteverify is server-side by design. Options:
|
||||
|
||||
- Add a Cloudflare Pages Function (`functions/api/subscribe.js`) to host the siteverify call.
|
||||
- Deploy a tiny Cloudflare Worker that does siteverify against your existing form host.
|
||||
- Use a third-party form host that exposes a server-side webhook where you can wire siteverify.
|
||||
|
||||
## Substitutions
|
||||
|
||||
| Placeholder | Replace with |
|
||||
| ------------------- | -------------------------------------------------------------------- |
|
||||
| `YOUR_SITEKEY` | The widget site key from Step 8 |
|
||||
| `/api/subscribe` | The path to your existing form-handling endpoint |
|
||||
| `TURNSTILE_SECRET` | Env-var name. Value is the secret captured in Step 8, kept off disk. |
|
||||
Reference in new issue
Block a user