{"_id":"@0x30-ch/recaptcha","name":"@0x30-ch/recaptcha","dist-tags":{"latest":"1.0.0"},"versions":{"1.0.0":{"version":"1.0.0","keywords":["strapi","plugin","recaptcha","security","google","captcha","policy"],"type":"commonjs","exports":{"./package.json":"./package.json","./strapi-admin":{"types":"./dist/admin/src/index.d.ts","source":"./admin/src/index.ts","import":"./dist/admin/index.mjs","require":"./dist/admin/index.js","default":"./dist/admin/index.js"},"./strapi-server":{"types":"./dist/server/src/index.d.ts","source":"./server/src/index.ts","import":"./dist/server/index.mjs","require":"./dist/server/index.js","default":"./dist/server/index.js"}},"scripts":{"build":"strapi-plugin build","watch":"strapi-plugin watch","dev":"strapi-plugin watch","watch:link":"strapi-plugin watch:link","verify":"strapi-plugin verify","test:ts:front":"tsc -p admin/tsconfig.json","test:ts:back":"tsc -p server/tsconfig.json"},"dependencies":{},"devDependencies":{"@strapi/strapi":"^5.31.3","@strapi/sdk-plugin":"^5.3.2","@strapi/typescript-utils":"^5.31.3","prettier":"^3.6.2","typescript":"^5.8.3"},"peerDependencies":{"@strapi/strapi":"^5.18.1","@strapi/sdk-plugin":"^5.3.2","@strapi/design-system":"^2.0.0","@strapi/icons":"^2.0.0","react":"^18.3.1","react-dom":"^18.3.1","react-router-dom":"^6.30.1","styled-components":"^6.1.19"},"strapi":{"kind":"plugin","name":"recaptcha","displayName":"reCAPTCHA","description":"reCAPTCHA v3 policy for protecting Strapi routes"},"name":"@0x30-ch/recaptcha","description":"Strapi plugin providing reCAPTCHA v3 route protection via policy","license":"MIT","repository":{"type":"git","url":"git+ssh://git@github.com/0x30-ch/strapi-plugins.git"},"bugs":{"url":"https://github.com/0x30-ch/strapi-plugins/issues"},"homepage":"https://github.com/0x30-ch/strapi-plugins#readme","author":{"name":"jeanvier","email":"hello@0x30.ch"},"_id":"@0x30-ch/recaptcha@1.0.0","gitHead":"8ebe7573eb70d8e5daa6f1b4ebdf7178690b31f4","_nodeVersion":"22.18.0","_npmVersion":"10.9.3","dist":{"integrity":"sha512-2644bZMmYPePYWx9qL4gHeG8M0hyI6ha6xk06tMYKhj9/gVH4Dp0+doKjru3WD8K9qjHScS7jvMlAWp5fRkuBA==","shasum":"8d8eb0755ec2f455847869d462347df9e6df32bd","tarball":"https://registry.npmjs.org/@0x30-ch/recaptcha/-/recaptcha-1.0.0.tgz","fileCount":26,"unpackedSize":51980,"signatures":[{"keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U","sig":"MEUCIQCOB/wLQvAcUtM6yX0N885cp/tBSUAMAcRf0ord3JQ1NQIgKktd9zsoKJ/Ykj7kIKtreBOV6MGs410sCTkAYxDodHc="}]},"_npmUser":{"name":"monsieurjanvier","email":"damien.grossfeld@gmail.com"},"directories":{},"maintainers":[{"name":"monsieurjanvier","email":"damien.grossfeld@gmail.com"}],"_npmOperationalInternal":{"host":"s3://npm-registry-packages-npm-production","tmp":"tmp/recaptcha_1.0.0_1770389743130_0.3001710436053868"},"_hasShrinkwrap":false}},"time":{"created":"2026-02-06T14:55:43.028Z","1.0.0":"2026-02-06T14:55:43.279Z","modified":"2026-02-06T14:55:43.640Z"},"maintainers":[{"name":"monsieurjanvier","email":"damien.grossfeld@gmail.com"}],"description":"Strapi plugin providing reCAPTCHA v3 route protection via policy","homepage":"https://github.com/0x30-ch/strapi-plugins#readme","keywords":["strapi","plugin","recaptcha","security","google","captcha","policy"],"repository":{"type":"git","url":"git+ssh://git@github.com/0x30-ch/strapi-plugins.git"},"author":{"name":"jeanvier","email":"hello@0x30.ch"},"bugs":{"url":"https://github.com/0x30-ch/strapi-plugins/issues"},"license":"MIT","readme":"# @0x30-ch/recaptcha\n\nStrapi 5 plugin providing **reCAPTCHA v3** route protection via a reusable policy. No admin UI — configured entirely via environment variables.\n\n## Features\n\n- **Policy-based protection** — add `'plugin::recaptcha.recaptcha'` to any route\n- **Score threshold** — configurable globally and per-route\n- **Action validation** — optional `expectedAction` check per-route\n- **Graceful degradation** — missing secret key logs a warning at boot, returns 403 at request time\n- **Zero runtime dependencies** — uses native `fetch` (Node 18+)\n- **Health-check endpoint** — `GET /api/recaptcha` returns `{ configured, siteKey }`\n\n## Installation\n\nThe plugin is part of the `strapi-plugins` monorepo. It is already registered in `apps/cms/config/plugins.ts`.\n\n```bash\n# From the monorepo root\npnpm install\npnpm run build --filter=@0x30-ch/recaptcha\n```\n\n## Configuration\n\nAdd the following environment variables to your `.env` file:\n\n```env\nRECAPTCHA_SITE_KEY=your-site-key\nRECAPTCHA_SECRET_KEY=your-secret-key\nRECAPTCHA_SCORE_THRESHOLD=0.5\n```\n\nThe plugin is registered in `apps/cms/config/plugins.ts`:\n\n```typescript\nrecaptcha: {\n  enabled: true,\n  resolve: '../../packages/recaptcha',\n  config: {\n    secretKey: process.env.RECAPTCHA_SECRET_KEY ?? '',\n    siteKey: process.env.RECAPTCHA_SITE_KEY ?? '',\n    scoreThreshold: Number(process.env.RECAPTCHA_SCORE_THRESHOLD) || 0.5,\n  },\n},\n```\n\n### Config Options\n\n| Option | Type | Default | Description |\n|--------|------|---------|-------------|\n| `secretKey` | `string` | `''` | Google reCAPTCHA v3 secret key |\n| `siteKey` | `string` | `''` | Google reCAPTCHA v3 site key (exposed via health-check) |\n| `scoreThreshold` | `number` | `0.5` | Minimum score to pass (0.0 – 1.0) |\n| `tokenHeader` | `string` | `x-recaptcha-token` | HTTP header name for the token |\n\n## Usage\n\n### Protecting a Route\n\nAdd the policy to any route configuration:\n\n```typescript\n// Basic usage\n{\n  method: 'POST',\n  path: '/contact',\n  handler: 'contact.send',\n  config: {\n    policies: ['plugin::recaptcha.recaptcha'],\n  },\n}\n```\n\n### Per-Route Score Threshold\n\nOverride the global threshold for specific routes:\n\n```typescript\n{\n  method: 'POST',\n  path: '/payment',\n  handler: 'payment.create',\n  config: {\n    policies: [\n      {\n        name: 'plugin::recaptcha.recaptcha',\n        config: { scoreThreshold: 0.7 },\n      },\n    ],\n  },\n}\n```\n\n### Action Validation\n\nVerify that the token was generated for the expected action:\n\n```typescript\n{\n  method: 'POST',\n  path: '/contact',\n  handler: 'contact.send',\n  config: {\n    policies: [\n      {\n        name: 'plugin::recaptcha.recaptcha',\n        config: { expectedAction: 'contact_form' },\n      },\n    ],\n  },\n}\n```\n\n### Client-Side Integration\n\nSend the reCAPTCHA token via the `x-recaptcha-token` HTTP header:\n\n```typescript\nconst token = await grecaptcha.execute('your-site-key', { action: 'contact_form' });\n\nconst response = await fetch('/api/contact', {\n  method: 'POST',\n  headers: {\n    'Content-Type': 'application/json',\n    'x-recaptcha-token': token,\n  },\n  body: JSON.stringify({ name: 'John', message: 'Hello' }),\n});\n```\n\n## API\n\n### Health-Check Endpoint\n\n`GET /api/recaptcha`\n\nReturns the plugin status and public site key:\n\n```json\n{\n  \"configured\": true,\n  \"siteKey\": \"your-site-key\"\n}\n```\n\n### Error Responses\n\nAll errors return HTTP `403` with a JSON body:\n\n| Scenario | Response |\n|----------|----------|\n| Missing token | `{ \"error\": \"reCAPTCHA token is required\" }` |\n| No secret key configured | `{ \"error\": \"reCAPTCHA is not configured on the server\" }` |\n| Verification failed | `{ \"error\": \"reCAPTCHA verification failed\" }` |\n| Score too low | `{ \"error\": \"reCAPTCHA score too low\" }` |\n| Action mismatch | `{ \"error\": \"reCAPTCHA action mismatch\" }` |\n\n## Development\n\n```bash\n# Build\nnpm run build\n\n# Watch mode\nnpm run dev\n\n# Verify plugin structure\nnpm run verify\n\n# Type check\nnpm run test:ts:front\nnpm run test:ts:back\n```\n\n## Architecture\n\n```\npackages/recaptcha/\n├── admin/src/\n│   └── index.ts              # Empty register (no admin UI)\n├── server/src/\n│   ├── config/index.ts        # Defaults + validator\n│   ├── controllers/\n│   │   └── controller.ts      # Health-check endpoint\n│   ├── policies/\n│   │   └── recaptcha.ts       # Core policy — token extraction, verification, score check\n│   ├── services/\n│   │   └── recaptcha.ts       # Google siteverify API client\n│   ├── bootstrap.ts           # Validates config, logs warnings\n│   ├── register.ts            # Registration log\n│   └── index.ts               # Plugin entry point\n└── package.json\n```\n\n## License\n\nMIT\n","readmeFilename":"README.md","_rev":"1-d3b3c60f5ba8b735aa46873bd72e14b3"}