@0x30-ch/recaptcha
v1.0.0
Published
Strapi plugin providing reCAPTCHA v3 route protection via policy
Downloads
18
Maintainers
Readme
@0x30-ch/recaptcha
Strapi 5 plugin providing reCAPTCHA v3 route protection via a reusable policy. No admin UI — configured entirely via environment variables.
Features
- Policy-based protection — add
'plugin::recaptcha.recaptcha'to any route - Score threshold — configurable globally and per-route
- Action validation — optional
expectedActioncheck per-route - Graceful degradation — missing secret key logs a warning at boot, returns 403 at request time
- Zero runtime dependencies — uses native
fetch(Node 18+) - Health-check endpoint —
GET /api/recaptchareturns{ configured, siteKey }
Installation
The plugin is part of the strapi-plugins monorepo. It is already registered in apps/cms/config/plugins.ts.
# From the monorepo root
pnpm install
pnpm run build --filter=@0x30-ch/recaptchaConfiguration
Add the following environment variables to your .env file:
RECAPTCHA_SITE_KEY=your-site-key
RECAPTCHA_SECRET_KEY=your-secret-key
RECAPTCHA_SCORE_THRESHOLD=0.5The plugin is registered in apps/cms/config/plugins.ts:
recaptcha: {
enabled: true,
resolve: '../../packages/recaptcha',
config: {
secretKey: process.env.RECAPTCHA_SECRET_KEY ?? '',
siteKey: process.env.RECAPTCHA_SITE_KEY ?? '',
scoreThreshold: Number(process.env.RECAPTCHA_SCORE_THRESHOLD) || 0.5,
},
},Config Options
| Option | Type | Default | Description |
|--------|------|---------|-------------|
| secretKey | string | '' | Google reCAPTCHA v3 secret key |
| siteKey | string | '' | Google reCAPTCHA v3 site key (exposed via health-check) |
| scoreThreshold | number | 0.5 | Minimum score to pass (0.0 – 1.0) |
| tokenHeader | string | x-recaptcha-token | HTTP header name for the token |
Usage
Protecting a Route
Add the policy to any route configuration:
// Basic usage
{
method: 'POST',
path: '/contact',
handler: 'contact.send',
config: {
policies: ['plugin::recaptcha.recaptcha'],
},
}Per-Route Score Threshold
Override the global threshold for specific routes:
{
method: 'POST',
path: '/payment',
handler: 'payment.create',
config: {
policies: [
{
name: 'plugin::recaptcha.recaptcha',
config: { scoreThreshold: 0.7 },
},
],
},
}Action Validation
Verify that the token was generated for the expected action:
{
method: 'POST',
path: '/contact',
handler: 'contact.send',
config: {
policies: [
{
name: 'plugin::recaptcha.recaptcha',
config: { expectedAction: 'contact_form' },
},
],
},
}Client-Side Integration
Send the reCAPTCHA token via the x-recaptcha-token HTTP header:
const token = await grecaptcha.execute('your-site-key', { action: 'contact_form' });
const response = await fetch('/api/contact', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'x-recaptcha-token': token,
},
body: JSON.stringify({ name: 'John', message: 'Hello' }),
});API
Health-Check Endpoint
GET /api/recaptcha
Returns the plugin status and public site key:
{
"configured": true,
"siteKey": "your-site-key"
}Error Responses
All errors return HTTP 403 with a JSON body:
| Scenario | Response |
|----------|----------|
| Missing token | { "error": "reCAPTCHA token is required" } |
| No secret key configured | { "error": "reCAPTCHA is not configured on the server" } |
| Verification failed | { "error": "reCAPTCHA verification failed" } |
| Score too low | { "error": "reCAPTCHA score too low" } |
| Action mismatch | { "error": "reCAPTCHA action mismatch" } |
Development
# Build
npm run build
# Watch mode
npm run dev
# Verify plugin structure
npm run verify
# Type check
npm run test:ts:front
npm run test:ts:backArchitecture
packages/recaptcha/
├── admin/src/
│ └── index.ts # Empty register (no admin UI)
├── server/src/
│ ├── config/index.ts # Defaults + validator
│ ├── controllers/
│ │ └── controller.ts # Health-check endpoint
│ ├── policies/
│ │ └── recaptcha.ts # Core policy — token extraction, verification, score check
│ ├── services/
│ │ └── recaptcha.ts # Google siteverify API client
│ ├── bootstrap.ts # Validates config, logs warnings
│ ├── register.ts # Registration log
│ └── index.ts # Plugin entry point
└── package.jsonLicense
MIT
