# Cloudflare Turnstile Bot Protection

This document covers the Turnstile bot protection integration for public forms (contact forms, support requests, etc.).

## Overview

Cloudflare Turnstile provides invisible bot protection that replaces traditional CAPTCHAs. It's integrated into:

- Contact forms on dealer pages
- Support request forms

## Environment Variables

```env
# Required for Turnstile to function
NEXT_PUBLIC_TURNSTILE_SITE_KEY=""  # Public site key (visible to clients)
TURNSTILE_SECRET_KEY=""             # Secret key (server-side verification)

# Optional: For automatic hostname management with custom domains
TURNSTILE_WIDGET_ID=""              # Widget ID for API calls
```

### Getting Turnstile Keys

1. Go to [Cloudflare Dashboard](https://dash.cloudflare.com/) → Turnstile
2. Click "Add widget"
3. Configure the widget:
   - **Name**: "AMSOIL DLP" (or similar)
   - **Domains**: Add your base domains (see Hostname Configuration below)
   - **Widget Mode**: "Managed" (recommended)
4. Copy the Site Key and Secret Key to your `.env.local`

### Test Keys (Development)

Cloudflare provides test keys that always pass:

- Site Key: `1x00000000000000000000AA`
- Secret Key: `1x0000000000000000000000000000000AA`

## Hostname Configuration

Turnstile requires hostnames to be explicitly allowed for the widget to function.

### Base Domains (Manual Setup)

Add these to your Turnstile widget in the Cloudflare dashboard:

- `myamsoil.com` (covers all `*.myamsoil.com` subdomains)
- `myamsoil.ca`
- `shopamsoil.com`
- `shopamsoil.ca`
- `localhost` (for development)

### Custom Domains (Automatic)

When a dealer activates a custom domain (e.g., `bobsoil.com`), the system automatically adds it to the Turnstile widget's allowed hostnames via the Cloudflare API.

**Requirements for automatic hostname management:**

1. Set `TURNSTILE_WIDGET_ID` in your environment
2. The Cloudflare API token must have Turnstile widget edit permissions

**API Token Permissions:**

- Account → Turnstile → Edit

To get the Widget ID:

1. Go to Cloudflare Dashboard → Turnstile
2. Click on your widget
3. Copy the Widget ID from the URL or widget details

### Hostname Limits

- **Free tier**: 10 hostnames per widget (Cloudflare docs say 15, but API enforces 10)
- **Enterprise**: 200 hostnames per widget

The system enforces these limits to prevent API errors. Set `TURNSTILE_HOSTNAME_LIMIT` in your environment to match your plan:

```env
# For free tier (Cloudflare API enforces 10):
TURNSTILE_HOSTNAME_LIMIT="10"

# For enterprise (default if not set):
TURNSTILE_HOSTNAME_LIMIT="200"
```

If you exceed the limit, custom domain activation will fail with an error. You may need to upgrade your plan or create multiple widgets.

### Hostname Validation

Hostnames are validated before being added to the widget:

- Must be a valid RFC 1123 hostname (e.g., `bobsoil.com`)
- Special exception for `localhost` (development)
- Maximum 253 characters
- Invalid hostnames are rejected with an error

## Implementation Details

### Client-Side (`hooks/useTurnstile.ts`)

The `useTurnstile` hook handles:

- Loading the Turnstile script
- Rendering the widget
- Managing token state
- Handling expiration and errors

```tsx
function ContactForm() {
  const { containerRef, token, isVerified, reset } = useTurnstile();

  return (
    <form onSubmit={handleSubmit}>
      {/* Turnstile widget renders here */}
      <div ref={containerRef} />
      <button disabled={!isVerified}>Submit</button>
    </form>
  );
}
```

### Server-Side (`lib/turnstile.ts`)

The `verifyTurnstile` function validates tokens:

```ts
const result = await verifyTurnstile(token, clientIP);
if (!result.success) {
  return { error: result.error };
}
// Token is valid, process the form
```

### Hostname Management (`lib/turnstile.ts`)

Functions for managing widget hostnames:

- `addTurnstileHostname(hostname)` - Adds a hostname when a custom domain is activated
- `removeTurnstileHostname(hostname)` - Removes a hostname when a custom domain is removed
- `getTurnstileWidget()` - Gets current widget configuration
- `updateTurnstileHostnames(domains)` - Replaces all hostnames

## Graceful Degradation

If Turnstile is not configured:

- Forms work normally without bot protection
- Existing spam prevention (honeypot, timing checks) still applies
- Log messages indicate Turnstile is skipped

If hostname management is not configured:

- Custom domain activation/removal continues normally
- Log warnings indicate Turnstile hostnames weren't updated
- Manual hostname management in Cloudflare dashboard is required

## Troubleshooting

### "Widget doesn't appear"

1. Check `NEXT_PUBLIC_TURNSTILE_SITE_KEY` is set
2. Verify the hostname is in the widget's allowed list
3. Check browser console for errors

### "Verification fails on custom domain"

1. Verify `TURNSTILE_WIDGET_ID` is set
2. Check logs for "Failed to add custom domain to Turnstile hostnames"
3. Manually add the domain in Cloudflare dashboard if needed

### "Widget shows error"

Common causes:

- Domain not in allowed list
- Invalid site key
- Network issues reaching Cloudflare

## Syncing Existing Custom Domains

When setting up Turnstile for the first time, or if hostnames get out of sync, use the sync script:

```bash
# Preview what would change (no actual changes)
npm run sync-turnstile -- --dry-run

# Add missing custom domains to Turnstile
npm run sync-turnstile

# Also remove stale domains no longer in use
npm run sync-turnstile -- --cleanup
```

The script:

1. Fetches all active custom domains from the database
2. Compares with current Turnstile widget hostnames
3. Adds any missing domains
4. With `--cleanup`, removes domains that are no longer active

## Security Notes

- Never expose `TURNSTILE_SECRET_KEY` to clients
- Always verify tokens server-side before processing forms
- Turnstile tokens are single-use and expire after a few minutes
- Consider rate limiting in addition to Turnstile for defense in depth

## Multi-Widget Pool (200+ Custom Domains)

When you have more than 15 custom domains, you need multiple Turnstile widgets due to Cloudflare's free tier hostname limit.

### Architecture

- **Primary widget**: Serves base domains (myamsoil.com, myamsoil.ca, etc.)
- **Pool widgets**: Assigned to custom domains (10 domains per widget max)
- **Capacity**: 20 widgets x 10 hostnames = 200 custom domains

### Quick Setup (Automated)

The fastest way to set up the widget pool is using the automated provisioning script:

1. **Configure environment variables:**

   ```bash
   # Add to .env.local
   CLOUDFLARE_API_TOKEN="your-token"       # Must have Turnstile write permissions
   CLOUDFLARE_ACCOUNT_ID="your-account-id"
   TURNSTILE_ENCRYPTION_KEY="$(openssl rand -hex 32)"
   ```

2. **Create primary widget (for base domains):**

   ```bash
   npm run provision-turnstile-pool -- --primary
   ```

3. **Create pool widgets (20 by default):**

   ```bash
   npm run provision-turnstile-pool -- --count=20
   ```

4. **Migrate existing custom domains:**
   ```bash
   npm run migrate-turnstile-domains -- --dry-run  # Preview
   npm run migrate-turnstile-domains               # Execute
   ```

The provisioning script:

- Creates widgets in Cloudflare via API
- Encrypts secret keys with AES-256-GCM
- Registers widgets in the database
- Handles rate limiting automatically
- Skips widgets that already exist

**Preview mode (no changes):**

```bash
npm run provision-turnstile-pool -- --count=20 --dry-run
```

### Cross-Environment Setup (Dev → Staging → Prod)

When multiple environments (dev, staging, prod) share the same Cloudflare account, widgets only need to be created once in Cloudflare, but each environment needs them registered in its database.

**First environment (e.g., dev) - Create widgets and export bootstrap file:**

```bash
npm run provision-turnstile-pool -- --primary --export-bootstrap=turnstile-bootstrap.json
npm run provision-turnstile-pool -- --count=20 --export-bootstrap=turnstile-bootstrap.json
```

**Subsequent environments (staging, prod) - Import from bootstrap file:**

```bash
# Copy turnstile-bootstrap.json to the server, then:
npm run provision-turnstile-pool -- --import-bootstrap=turnstile-bootstrap.json
```

The bootstrap file contains:

- Widget names and sitekeys
- Encrypted secret keys (safe to transfer, requires `TURNSTILE_ENCRYPTION_KEY` to use)
- Widget configuration (isPrimary, maxHostnames, etc.)

**Important:** The bootstrap file is gitignored by default. Transfer it securely between environments (e.g., via secure copy, secrets manager, or encrypted channel).

### Manual Setup (Alternative)

If you prefer to create widgets manually in the Cloudflare dashboard:

1. **Generate encryption key:**

   ```bash
   openssl rand -hex 32
   ```

   Add to `.env.local` as `TURNSTILE_ENCRYPTION_KEY`.

2. **Create primary widget in Cloudflare:**
   - Go to Cloudflare Dashboard -> Turnstile
   - Create widget with base domains: myamsoil.com, myamsoil.ca, shopamsoil.com, shopamsoil.ca, localhost
   - Note the sitekey, secret, and widget ID

3. **Register primary widget:**

   ```bash
   npm run setup-turnstile-widget -- \
     --name="primary" \
     --sitekey="YOUR_SITEKEY" \
     --secret="YOUR_SECRET" \
     --cloudflare-id="WIDGET_ID" \
     --primary
   ```

4. **Create pool widgets (repeat as needed):**

   ```bash
   npm run setup-turnstile-widget -- \
     --name="pool-1" \
     --sitekey="POOL_SITEKEY" \
     --secret="POOL_SECRET" \
     --cloudflare-id="POOL_WIDGET_ID"
   ```

5. **Migrate existing domains:**
   ```bash
   npm run migrate-turnstile-domains -- --dry-run  # Preview
   npm run migrate-turnstile-domains               # Execute
   ```

### Monitoring

Check pool utilization programmatically:

```typescript
import { getPoolStatus } from '@/lib/turnstile-pool';

const status = await getPoolStatus();
console.log(
  `Using ${status.totalUsed}/${status.totalCapacity} slots (${status.utilizationPercent}%)`
);
```

### Capacity Planning

- Free tier: 20 widgets x 10 hostnames = 200 custom domains
- Add widgets before reaching 80% utilization
- Monitor via admin dashboard or scheduled job
