{"_id":"@divami-labs/nestjs-captcha","_rev":"3-c7d9eadea68f0387388b6e1b52d0e0d3","name":"@divami-labs/nestjs-captcha","dist-tags":{"latest":"0.0.3"},"versions":{"0.0.1":{"name":"@divami-labs/nestjs-captcha","version":"0.0.1","author":{"name":"Anantha Gatta"},"license":"MIT","_id":"@divami-labs/nestjs-captcha@0.0.1","maintainers":[{"name":"divami-artefacts","email":"devops@divami.com"}],"dist":{"shasum":"b0c71ee28b4b88d00f4a7f652a821c3eeff3031b","tarball":"https://registry.npmjs.org/@divami-labs/nestjs-captcha/-/nestjs-captcha-0.0.1.tgz","fileCount":30,"integrity":"sha512-BzdBGVu4PT6NQ1/QM5eReOHxDBfKc4B57pMIb97C4ycHpBFAuOBmsZXjQDszy4W74gNzj+TMfB3BYt1rsLB/nA==","signatures":[{"sig":"MEUCIQCOG+0dOBhc2u8NexkEgbegN/5jIRcynlCimiKklss97QIgAkAWwHzpVPhrX2XYmlLaeq/8mdwVJ93Pvgkbew0bMCA=","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":207607},"jest":{"rootDir":"src","testRegex":".*\\.spec\\.ts$","transform":{"^.+\\.(t|j)s$":"ts-jest"},"testEnvironment":"node","coverageDirectory":"../coverage","collectCoverageFrom":["**/*.(t|j)s"],"moduleFileExtensions":["js","json","ts"]},"main":"dist/index.js","types":"dist/index.d.ts","gitHead":"6e7463eefa596732cd257084734707a8859248d1","private":false,"scripts":{"lint":"eslint \"{src,apps,libs,test}/**/*.ts\" --fix","test":"jest","build":"nest build","format":"prettier --write \"src/**/*.ts\" \"test/**/*.ts\"","prepublishOnly":"npm run build"},"_npmUser":{"name":"divami-artefacts","email":"devops@divami.com"},"_npmVersion":"10.8.2","description":"NestJS CAPTCHA module supporting Google reCAPTCHA and Cloudflare Turnstile","directories":{},"_nodeVersion":"18.20.8","dependencies":{"axios":"^1.13.3","class-validator":"^0.14.3","reflect-metadata":"^0.2.2","class-transformer":"^0.5.1"},"publishConfig":{"access":"public"},"_hasShrinkwrap":false,"devDependencies":{"jest":"^30.0.0","eslint":"^9.18.0","globals":"^16.0.0","ts-jest":"^29.2.5","ts-node":"^10.9.2","prettier":"^3.4.2","supertest":"^7.0.0","ts-loader":"^9.5.2","@eslint/js":"^9.18.0","typescript":"^5.7.3","@nestjs/cli":"^11.0.0","@types/jest":"^30.0.0","@types/node":"^22.10.7","@types/express":"^5.0.0","tsconfig-paths":"^4.2.0","@nestjs/testing":"^11.0.1","@eslint/eslintrc":"^3.2.0","@types/supertest":"^6.0.2","typescript-eslint":"^8.20.0","@nestjs/schematics":"^11.0.0","source-map-support":"^0.5.21","eslint-config-prettier":"^10.0.1","eslint-plugin-prettier":"^5.2.2"},"peerDependencies":{"rxjs":"^7","@nestjs/core":">=10 <12","@nestjs/axios":">=3 <5","@nestjs/common":">=10 <12"},"_npmOperationalInternal":{"tmp":"tmp/nestjs-captcha_0.0.1_1770369159752_0.8642001772105208","host":"s3://npm-registry-packages-npm-production"}},"0.0.2":{"name":"@divami-labs/nestjs-captcha","version":"0.0.2","author":{"name":"Anantha Gatta"},"license":"MIT","_id":"@divami-labs/nestjs-captcha@0.0.2","maintainers":[{"name":"divami-artefacts","email":"devops@divami.com"}],"dist":{"shasum":"9bd63bab696baa789389554f421f3ad999884133","tarball":"https://registry.npmjs.org/@divami-labs/nestjs-captcha/-/nestjs-captcha-0.0.2.tgz","fileCount":30,"integrity":"sha512-kHembumIizM+4MqS9tpUNnFekRqCXTooQyKQwF5PtZ4UoSNpQEvRk7upWze7ZEdqW2qjWT2jhvd1zh/mOcGBvg==","signatures":[{"sig":"MEUCIDjz8JVbUGNXdCnb6xyF1yyPdxLko8ySNnTi43seKHe3AiEA5y41A6vT2G6ZI8Kkp6RXSyi1bOnJreS6ym0aEUA8e4M=","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":188879},"jest":{"rootDir":"src","testRegex":".*\\.spec\\.ts$","transform":{"^.+\\.(t|j)s$":"ts-jest"},"testEnvironment":"node","coverageDirectory":"../coverage","collectCoverageFrom":["**/*.(t|j)s"],"moduleFileExtensions":["js","json","ts"]},"main":"dist/index.js","types":"dist/index.d.ts","gitHead":"272ba5ec7b7b84e90fcc7a6906d90d096d92b6ba","private":false,"scripts":{"lint":"eslint \"{src,apps,libs,test}/**/*.ts\" --fix","test":"jest","build":"nest build","format":"prettier --write \"src/**/*.ts\" \"test/**/*.ts\"","prepublishOnly":"npm run build"},"_npmUser":{"name":"divami-artefacts","email":"devops@divami.com"},"_npmVersion":"10.8.2","description":"NestJS CAPTCHA module supporting Google reCAPTCHA and Cloudflare Turnstile","directories":{},"_nodeVersion":"18.20.8","dependencies":{"axios":"^1.13.3","class-validator":"^0.14.3","reflect-metadata":"^0.2.2","class-transformer":"^0.5.1"},"publishConfig":{"access":"public"},"_hasShrinkwrap":false,"devDependencies":{"jest":"^30.0.0","eslint":"^9.18.0","globals":"^16.0.0","ts-jest":"^29.2.5","ts-node":"^10.9.2","prettier":"^3.4.2","supertest":"^7.0.0","ts-loader":"^9.5.2","@eslint/js":"^9.18.0","typescript":"^5.7.3","@nestjs/cli":"^11.0.0","@types/jest":"^30.0.0","@types/node":"^22.10.7","@types/express":"^5.0.0","tsconfig-paths":"^4.2.0","@nestjs/testing":"^11.0.1","@eslint/eslintrc":"^3.2.0","@types/supertest":"^6.0.2","typescript-eslint":"^8.20.0","@nestjs/schematics":"^11.0.0","source-map-support":"^0.5.21","eslint-config-prettier":"^10.0.1","eslint-plugin-prettier":"^5.2.2"},"peerDependencies":{"rxjs":"^7","@nestjs/core":">=10 <12","@nestjs/axios":">=3 <5","@nestjs/common":">=10 <12"},"_npmOperationalInternal":{"tmp":"tmp/nestjs-captcha_0.0.2_1770635351651_0.5569116258110887","host":"s3://npm-registry-packages-npm-production"}},"0.0.3":{"name":"@divami-labs/nestjs-captcha","version":"0.0.3","description":"NestJS CAPTCHA module supporting Google reCAPTCHA and Cloudflare Turnstile","author":{"name":"Anantha Gatta"},"license":"MIT","private":false,"main":"dist/index.js","types":"dist/index.d.ts","publishConfig":{"access":"public"},"scripts":{"build":"nest build","prepublishOnly":"npm run build","format":"prettier --write \"src/**/*.ts\" \"test/**/*.ts\"","lint":"eslint \"{src,apps,libs,test}/**/*.ts\" --fix","test":"jest"},"peerDependencies":{"@nestjs/common":">=10 <12","@nestjs/core":">=10 <12","@nestjs/axios":">=3 <5","rxjs":"^7"},"dependencies":{"axios":"^1.13.3","class-transformer":"^0.5.1","class-validator":"^0.14.3","reflect-metadata":"^0.2.2"},"devDependencies":{"@eslint/eslintrc":"^3.2.0","@eslint/js":"^9.18.0","@nestjs/cli":"^11.0.0","@nestjs/schematics":"^11.0.0","@nestjs/testing":"^11.0.1","@types/express":"^5.0.0","@types/jest":"^30.0.0","@types/node":"^22.10.7","@types/supertest":"^6.0.2","eslint":"^9.18.0","eslint-config-prettier":"^10.0.1","eslint-plugin-prettier":"^5.2.2","globals":"^16.0.0","jest":"^30.0.0","prettier":"^3.4.2","source-map-support":"^0.5.21","supertest":"^7.0.0","ts-jest":"^29.2.5","ts-loader":"^9.5.2","ts-node":"^10.9.2","tsconfig-paths":"^4.2.0","typescript":"^5.7.3","typescript-eslint":"^8.20.0"},"jest":{"moduleFileExtensions":["js","json","ts"],"rootDir":"src","testRegex":".*\\.spec\\.ts$","transform":{"^.+\\.(t|j)s$":"ts-jest"},"collectCoverageFrom":["**/*.(t|j)s"],"coverageDirectory":"../coverage","testEnvironment":"node"},"_id":"@divami-labs/nestjs-captcha@0.0.3","gitHead":"46c42b64a85b3f934bd9ba8ab96e45aa0ffc25f4","_nodeVersion":"18.20.8","_npmVersion":"10.8.2","dist":{"integrity":"sha512-QZirqbrycQQm8tEqvrBFShnCLOXpoIeAjYk2C9Bo0gF+LgXHPqaXORjuaOlbkoE5/+CSLjeu29RHqsZBn1WBjg==","shasum":"36bce8e7b97080d1172d121520039ca5fcb32b19","tarball":"https://registry.npmjs.org/@divami-labs/nestjs-captcha/-/nestjs-captcha-0.0.3.tgz","fileCount":33,"unpackedSize":191306,"signatures":[{"keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U","sig":"MEQCICYGI2H4kbJeJk/h4rFScXLS0iWgS0ndsR98BKGHTCSKAiBB50/EGeOSxVci1TeG1vtXH2YCVLCntHKr9Ac+cpctqA=="}]},"_npmUser":{"name":"divami-artefacts","email":"devops@divami.com"},"directories":{},"maintainers":[{"name":"divami-artefacts","email":"devops@divami.com"}],"_npmOperationalInternal":{"host":"s3://npm-registry-packages-npm-production","tmp":"tmp/nestjs-captcha_0.0.3_1770973655355_0.08196913238771386"},"_hasShrinkwrap":false}},"time":{"created":"2026-02-06T09:12:39.693Z","modified":"2026-02-13T09:07:35.655Z","0.0.1":"2026-02-06T09:12:39.889Z","0.0.2":"2026-02-09T11:09:12.295Z","0.0.3":"2026-02-13T09:07:35.520Z"},"author":{"name":"Anantha Gatta"},"license":"MIT","description":"NestJS CAPTCHA module supporting Google reCAPTCHA and Cloudflare Turnstile","maintainers":[{"name":"divami-artefacts","email":"devops@divami.com"}],"readme":"# NestJS Captcha Module\n\nA production-ready NestJS module for validating **Google reCAPTCHA v3** and **Cloudflare Turnstile** captcha tokens.\n\n## Features\n\n **Multiple Providers**: Support for Google reCAPTCHA v3 and Cloudflare Turnstile  \n **Guard-Based Protection**: Easy-to-use `@UseGuards(CaptchaGuard)` decorator  \n **Flexible Token Extraction**: Accepts tokens from headers or request body  \n **Skip Routes**: Use `@SkipCaptcha()` decorator to bypass validation on specific routes  \n **Async Configuration**: Load config from environment variables or ConfigService  \n **Score Validation**: Configure minimum score threshold for Google reCAPTCHA v3  \n **TypeScript**: Full type safety with TypeScript support\n\n---\n\n## Installation\n\n```bash\nnpm install @divami-labs/captcha-nestjs\n```\n\n---\n\n## Quick Start\n\n### 1. Import Module\n\n```typescript\nimport { Module } from '@nestjs/common';\nimport { CaptchaModule, CaptchaProvider } from '@divami/captcha-nestjs';\n\n@Module({\n  imports: [\n    CaptchaModule.register({\n      provider: CaptchaProvider.GOOGLE_RECAPTCHA_V3,\n      secretKey: process.env.RECAPTCHA_SECRET_KEY,\n      minimumScore: 0.5, \n    }),\n  ],\n})\nexport class AppModule {}\n```\n\n### 2. Apply Guard to Routes\n\n```typescript\nimport { Controller, Post, Body, UseGuards } from '@nestjs/common';\nimport { CaptchaGuard } from '@divami-labs/captcha-nestjs';\n\n@Controller('auth')\nexport class AuthController {\n  @Post('login')\n  @UseGuards(CaptchaGuard)\n  async login(@Body() body: LoginDto) {\n    // Captcha is already validated by the guard\n    return this.authService.login(body);\n  }\n}\n```\n\n---\n\n## Configuration\n\n### Synchronous Configuration\n\n```typescript\nCaptchaModule.register({\n  provider: CaptchaProvider.CLOUDFLARE_TURNSTILE,\n  secretKey: 'your-secret-key',\n  minimumScore: 0.7, // Only for Google reCAPTCHA v3\n  verifyUrl: 'https://custom-verify-url.com', // Optional\n})\n```\n\n### Async Configuration (Recommended)\n\n```typescript\nimport { ConfigModule, ConfigService } from '@nestjs/config';\n\n@Module({\n  imports: [\n    ConfigModule.forRoot(),\n    CaptchaModule.registerAsync({\n      imports: [ConfigModule],\n      inject: [ConfigService],\n      useFactory: (config: ConfigService) => ({\n        provider: config.get('CAPTCHA_PROVIDER') as CaptchaProvider,\n        secretKey: config.get('CAPTCHA_SECRET_KEY'),\n        minimumScore: parseFloat(config.get('CAPTCHA_MINIMUM_SCORE') || '0.5'),\n      }),\n    }),\n  ],\n})\nexport class AppModule {}\n```\n\n### Environment Variables\n\n```env\n# Google reCAPTCHA v3\nCAPTCHA_PROVIDER=google-v3\nRECAPTCHA_SECRET_KEY=your-google-secret-key\nCAPTCHA_MINIMUM_SCORE=0.5\n\n# OR Cloudflare Turnstile\nCAPTCHA_PROVIDER=cloudflare-turnstile\nTURNSTILE_SECRET_KEY=your-cloudflare-secret-key\n```\n\n---\n\n## Sending Captcha Tokens\n\nThe guard accepts tokens from **headers** or **request body** (NOT query params).\n\n### Method 1: Custom Header (Recommended)\n\n```typescript\nfetch('/api/auth/login', {\n  method: 'POST',\n  headers: {\n    'Content-Type': 'application/json',\n    'X-Captcha-Token': 'your-captcha-token',\n  },\n  body: JSON.stringify({ email, password }),\n});\n```\n\n\n### Method 2: Request Body\n\n```typescript\nfetch('/api/auth/login', {\n  method: 'POST',\n  headers: {\n    'Content-Type': 'application/json',\n  },\n  body: JSON.stringify({\n    email,\n    password,\n    captchaToken: 'your-captcha-token', \n  }),\n});\n```\n\n---\n\n## Decorators\n\n### `@SkipCaptcha()`\n\nSkip captcha validation for specific routes:\n\n```typescript\nimport { Controller, Get } from '@nestjs/common';\nimport { SkipCaptcha } from '@divami-labs/captcha-nestjs';\n\n@Controller('public')\n@UseGuards(CaptchaGuard) // Applied globally\nexport class PublicController {\n  @Get('health')\n  @SkipCaptcha() // Skip captcha for this route\n  health() {\n    return { status: 'ok' };\n  }\n\n  @Get('data')\n  // Captcha required for this route\n  getData() {\n    return { data: [] };\n  }\n}\n```\n\n### `@CaptchaToken()`\n\nAccess the validated token in your controller:\n\n```typescript\nimport { Controller, Post, UseGuards } from '@nestjs/common';\nimport { CaptchaGuard, CaptchaToken } from '@divami-labs/captcha-nestjs';\n\n@Controller('auth')\nexport class AuthController {\n  @Post('register')\n  @UseGuards(CaptchaGuard)\n  async register(@CaptchaToken() token: string) {\n    console.log('Validated captcha token:', token);\n    // Your registration logic\n  }\n}\n```\n\n---\n\n## Global Guard Setup\n\nApply captcha validation globally to all routes:\n\n```typescript\nimport { Module } from '@nestjs/common';\nimport { APP_GUARD } from '@nestjs/core';\nimport { CaptchaGuard, CaptchaModule } from '@divami-labs/captcha-nestjs';\n\n@Module({\n  imports: [CaptchaModule.register({ /* config */ })],\n  providers: [\n    {\n      provide: APP_GUARD,\n      useClass: CaptchaGuard,\n    },\n  ],\n})\nexport class AppModule {}\n```\n\nThen use `@SkipCaptcha()` on public routes.\n\n---\n\n## Providers\n\n### Google reCAPTCHA v3\n\n```typescript\nCaptchaModule.register({\n  provider: CaptchaProvider.GOOGLE_RECAPTCHA_V3,\n  secretKey: 'your-google-secret-key',\n  minimumScore: 0.5, // 0.0 (bot) to 1.0 (human)\n})\n```\n\n- **Score Range**: 0.0 (likely bot) to 1.0 (likely human)\n- **Recommended Minimum**: 0.5 (configurable)\n- **Invisible**: No user interaction required\n\n### Cloudflare Turnstile\n\n```typescript\nCaptchaModule.register({\n  provider: CaptchaProvider.CLOUDFLARE_TURNSTILE,\n  secretKey: 'your-cloudflare-secret-key',\n})\n```\n\n- **Modes**: Invisible or checkbox (configured on frontend)\n- **No Score**: Binary pass/fail validation\n\n---\n\n## API Reference\n\n### `CaptchaService`\n\nInjectable service for manual captcha validation:\n\n```typescript\nimport { Injectable } from '@nestjs/common';\nimport { CaptchaService } from '@divami-labs/captcha-nestjs';\n\n@Injectable()\nexport class MyService {\n  constructor(private captchaService: CaptchaService) {}\n\n  async validateCaptcha(token: string) {\n    const result = await this.captchaService.validateToken(token);\n    \n    if (result.success) {\n      console.log('Score:', result.score); // Google reCAPTCHA v3 only\n      console.log('Action:', result.action);\n    } else {\n      console.error('Validation failed:', result.message);\n    }\n    \n    return result;\n  }\n}\n```\n\n#### Methods\n\n- `validateToken(token: string): Promise<CaptchaValidationResponse>`\n- `getProvider(): CaptchaProvider`\n- `getMinimumScore(): number | undefined`\n\n---\n\n## Error Handling\n\nThe guard throws `UnauthorizedException` when:\n\n- Token is missing\n- Token validation fails\n- Score is below minimum threshold (Google reCAPTCHA v3)\n\n```typescript\n{\n  \"statusCode\": 401,\n  \"message\": \"Captcha token is required. Provide it in header (X-Captcha-Token or Authorization: Captcha <token>) or body (captchaToken)\",\n  \"error\": \"Unauthorized\"\n}\n```\n\n---\n\n## Testing\n\n### Skip Captcha in Tests\n\n```typescript\nimport { Test } from '@nestjs/testing';\nimport { CaptchaModule, CaptchaProvider } from '@divami-labs/captcha-nestjs';\n\nconst moduleRef = await Test.createTestingModule({\n  imports: [\n    CaptchaModule.register({\n      provider: CaptchaProvider.GOOGLE_RECAPTCHA_V3,\n      secretKey: 'test-key',\n    }),\n  ],\n  controllers: [AuthController],\n})\n  .overrideGuard(CaptchaGuard)\n  .useValue({ canActivate: () => true }) // Mock guard\n  .compile();\n```\n\n---\n\n## TypeScript Types\n\n```typescript\nimport type { CaptchaConfig, CaptchaValidationResponse } from '@divami-labs/captcha-nestjs';\n\nconst config: CaptchaConfig = {\n  provider: CaptchaProvider.GOOGLE_RECAPTCHA_V3,\n  secretKey: 'key',\n  minimumScore: 0.5,\n};\n\nconst response: CaptchaValidationResponse = {\n  success: true,\n  score: 0.9,\n  action: 'login',\n  challenge_ts: '2026-02-05T12:00:00Z',\n  hostname: 'example.com',\n};\n```\n\n---\n\n## License\n\nMIT\n\n---\n\n## Support\n\n- **Google reCAPTCHA v3**: [Documentation](https://developers.google.com/recaptcha/docs/v3)\n- **Cloudflare Turnstile**: [Documentation](https://developers.cloudflare.com/turnstile/)\n","readmeFilename":"README.md"}