mirror of
https://github.com/ksyasuda/dotfiles.git
synced 2026-03-22 06:11:27 -07:00
update skills
This commit is contained in:
218
.agents/skills/cloudflare-deploy/references/turnstile/gotchas.md
Normal file
218
.agents/skills/cloudflare-deploy/references/turnstile/gotchas.md
Normal file
@@ -0,0 +1,218 @@
|
||||
# Troubleshooting & Gotchas
|
||||
|
||||
## Critical Rules
|
||||
|
||||
### ❌ Skipping Server-Side Validation
|
||||
**Problem:** Client-only validation is easily bypassed.
|
||||
|
||||
**Solution:** Always validate on server.
|
||||
```javascript
|
||||
// CORRECT - Server validates token
|
||||
app.post('/submit', async (req, res) => {
|
||||
const token = req.body['cf-turnstile-response'];
|
||||
const validation = await fetch('https://challenges.cloudflare.com/turnstile/v0/siteverify', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({ secret: SECRET, response: token })
|
||||
}).then(r => r.json());
|
||||
|
||||
if (!validation.success) return res.status(403).json({ error: 'CAPTCHA failed' });
|
||||
});
|
||||
```
|
||||
|
||||
### ❌ Exposing Secret Key
|
||||
**Problem:** Secret key leaked in client-side code.
|
||||
|
||||
**Solution:** Server-side validation only. Never send secret to client.
|
||||
|
||||
### ❌ Reusing Tokens (Single-Use Rule)
|
||||
**Problem:** Tokens are single-use. Revalidation fails with `timeout-or-duplicate`.
|
||||
|
||||
**Solution:** Generate new token for each submission. Reset widget on error.
|
||||
```javascript
|
||||
if (!response.ok) window.turnstile.reset(widgetId);
|
||||
```
|
||||
|
||||
### ❌ Not Handling Token Expiry
|
||||
**Problem:** Tokens expire after 5 minutes.
|
||||
|
||||
**Solution:** Handle expiry callback or use auto-refresh.
|
||||
```javascript
|
||||
window.turnstile.render('#container', {
|
||||
sitekey: 'YOUR_SITE_KEY',
|
||||
'refresh-expired': 'auto', // or 'manual' with expired-callback
|
||||
'expired-callback': () => window.turnstile.reset(widgetId)
|
||||
});
|
||||
```
|
||||
|
||||
## Common Errors
|
||||
|
||||
| Error | Cause | Solution |
|
||||
|-------|-------|----------|
|
||||
| **Widget not rendering** | Incorrect sitekey, CSP blocking, file:// protocol | Check sitekey, add CSP for challenges.cloudflare.com, use http:// |
|
||||
| **timeout-or-duplicate** | Token expired (>5min) or reused | Generate fresh token, don't cache >5min |
|
||||
| **invalid-input-secret** | Wrong secret key | Verify secret from dashboard, check env vars |
|
||||
| **missing-input-response** | Token not sent | Check form field name is 'cf-turnstile-response' |
|
||||
|
||||
## Framework Gotchas
|
||||
|
||||
### React: Widget Re-mounting
|
||||
**Problem:** Widget re-renders on state change, losing token.
|
||||
|
||||
**Solution:** Control lifecycle with useRef.
|
||||
```tsx
|
||||
function TurnstileWidget({ onToken }) {
|
||||
const containerRef = useRef(null);
|
||||
const widgetIdRef = useRef(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (containerRef.current && !widgetIdRef.current) {
|
||||
widgetIdRef.current = window.turnstile.render(containerRef.current, {
|
||||
sitekey: 'YOUR_SITE_KEY',
|
||||
callback: onToken
|
||||
});
|
||||
}
|
||||
return () => {
|
||||
if (widgetIdRef.current) {
|
||||
window.turnstile.remove(widgetIdRef.current);
|
||||
widgetIdRef.current = null;
|
||||
}
|
||||
};
|
||||
}, []);
|
||||
|
||||
return <div ref={containerRef} />;
|
||||
}
|
||||
```
|
||||
|
||||
### React StrictMode: Double Render
|
||||
**Problem:** Widget renders twice in dev due to StrictMode.
|
||||
|
||||
**Solution:** Use cleanup function.
|
||||
```tsx
|
||||
useEffect(() => {
|
||||
const widgetId = window.turnstile.render('#container', { sitekey });
|
||||
return () => window.turnstile.remove(widgetId);
|
||||
}, []);
|
||||
```
|
||||
|
||||
### Next.js: SSR Hydration
|
||||
**Problem:** `window.turnstile` undefined during SSR.
|
||||
|
||||
**Solution:** Use `'use client'` or dynamic import with `ssr: false`.
|
||||
```tsx
|
||||
'use client';
|
||||
export default function Turnstile() { /* component */ }
|
||||
```
|
||||
|
||||
### SPA: Navigation Without Cleanup
|
||||
**Problem:** Navigating leaves orphaned widgets.
|
||||
|
||||
**Solution:** Remove widget in cleanup.
|
||||
```javascript
|
||||
// Vue
|
||||
onBeforeUnmount(() => window.turnstile.remove(widgetId));
|
||||
|
||||
// React
|
||||
useEffect(() => () => window.turnstile.remove(widgetId), []);
|
||||
```
|
||||
|
||||
## Network & Security
|
||||
|
||||
### CSP Blocking
|
||||
**Problem:** Content Security Policy blocks script/iframe.
|
||||
|
||||
**Solution:** Add CSP directives.
|
||||
```html
|
||||
<meta http-equiv="Content-Security-Policy"
|
||||
content="script-src 'self' https://challenges.cloudflare.com;
|
||||
frame-src https://challenges.cloudflare.com;">
|
||||
```
|
||||
|
||||
### IP Address Forwarding
|
||||
**Problem:** Server receives proxy IP instead of client IP.
|
||||
|
||||
**Solution:** Use correct header.
|
||||
```javascript
|
||||
// Cloudflare Workers
|
||||
const ip = request.headers.get('CF-Connecting-IP');
|
||||
|
||||
// Behind proxy
|
||||
const ip = request.headers.get('X-Forwarded-For')?.split(',')[0];
|
||||
```
|
||||
|
||||
### CORS (Siteverify)
|
||||
**Problem:** CORS error calling siteverify from browser.
|
||||
|
||||
**Solution:** Never call siteverify client-side. Call your backend, backend calls siteverify.
|
||||
|
||||
## Limits & Constraints
|
||||
|
||||
| Limit | Value | Impact |
|
||||
|-------|-------|--------|
|
||||
| Token validity | 5 minutes | Must regenerate after expiry |
|
||||
| Token use | Single-use | Cannot revalidate same token |
|
||||
| Widget size | 300x65px (normal), 130x120px (compact) | Plan layout |
|
||||
|
||||
## Debugging
|
||||
|
||||
### Console Logging
|
||||
```javascript
|
||||
window.turnstile.render('#container', {
|
||||
sitekey: 'YOUR_SITE_KEY',
|
||||
callback: (token) => console.log('✓ Token:', token),
|
||||
'error-callback': (code) => console.error('✗ Error:', code),
|
||||
'expired-callback': () => console.warn('⏱ Expired'),
|
||||
'timeout-callback': () => console.warn('⏱ Timeout')
|
||||
});
|
||||
```
|
||||
|
||||
### Check Token State
|
||||
```javascript
|
||||
const token = window.turnstile.getResponse(widgetId);
|
||||
console.log('Token:', token || 'NOT READY');
|
||||
console.log('Expired:', window.turnstile.isExpired(widgetId));
|
||||
```
|
||||
|
||||
### Test Keys (Use First)
|
||||
Always develop with test keys before production:
|
||||
- Site: `1x00000000000000000000AA`
|
||||
- Secret: `1x0000000000000000000000000000000AA`
|
||||
|
||||
### Network Tab
|
||||
- Verify `api.js` loads (200 OK)
|
||||
- Check siteverify request/response
|
||||
- Look for 4xx/5xx errors
|
||||
|
||||
## Misconfigurations
|
||||
|
||||
### Wrong Key Pairing
|
||||
**Problem:** Site key from one widget, secret from another.
|
||||
|
||||
**Solution:** Verify site key and secret are from same widget in dashboard.
|
||||
|
||||
### Test Keys in Production
|
||||
**Problem:** Using test keys in production.
|
||||
|
||||
**Solution:** Environment-based keys.
|
||||
```javascript
|
||||
const SITE_KEY = process.env.NODE_ENV === 'production'
|
||||
? process.env.TURNSTILE_SITE_KEY
|
||||
: '1x00000000000000000000AA';
|
||||
```
|
||||
|
||||
### Missing Environment Variables
|
||||
**Problem:** Secret undefined on server.
|
||||
|
||||
**Solution:** Check .env and verify loading.
|
||||
```bash
|
||||
# .env
|
||||
TURNSTILE_SECRET=your_secret_here
|
||||
|
||||
# Verify
|
||||
console.log('Secret loaded:', !!process.env.TURNSTILE_SECRET);
|
||||
```
|
||||
|
||||
## Reference
|
||||
|
||||
- [Turnstile Docs](https://developers.cloudflare.com/turnstile/)
|
||||
- [Dashboard](https://dash.cloudflare.com/?to=/:account/turnstile)
|
||||
- [Error Codes](https://developers.cloudflare.com/turnstile/troubleshooting/)
|
||||
Reference in New Issue
Block a user