oliweb / laravel-cap
Laravel wrapper for Cap (tiagozip/cap) — self-hosted CAPTCHA alternative
Requires
- php: ^8.2
- illuminate/http: ^11.0|^12.0|^13.0
- illuminate/support: ^11.0|^12.0|^13.0
Requires (Dev)
- orchestra/testbench: ^9.0|^10.0|^11.0
- phpunit/phpunit: ^11.0|^12.0
README
A Laravel wrapper for Cap — the self-hosted, privacy-friendly CAPTCHA alternative based on Proof-of-Work.
Cap works without tracking, cookies, or third-party services. This package integrates server-side token verification into Laravel through a service, a facade, a middleware, a validation rule, and Blade directives.
Requirements
- PHP ^8.2
- Laravel 11 or 12
- A running Cap instance (self-hosted via Docker)
Installation
composer require oliweb/laravel-cap
The service provider and facade are registered automatically via Laravel's package auto-discovery.
Publish the configuration file:
php artisan vendor:publish --tag=cap-config
Configuration
Add the following variables to your .env file:
CAP_ENDPOINT=https://cap.example.com/your-site-key/ CAP_SECRET=your-secret-key CAP_TOKEN_FIELD=cap-token CAP_TIMEOUT=5 CAP_FAIL_OPEN=false
| Variable | Description | Default |
|---|---|---|
CAP_ENDPOINT |
Full URL of your Cap instance including the site key (trailing slash required) | — |
CAP_SECRET |
Secret key from your Cap dashboard | — |
CAP_TOKEN_FIELD |
Name of the hidden field injected by the Cap widget | cap-token |
CAP_TIMEOUT |
HTTP timeout in seconds for the /siteverify request |
5 |
CAP_FAIL_OPEN |
When true, let requests through on network/server errors (see below) |
false |
Fail-open mode
By default, any communication error with the Cap instance (network failure, timeout, HTTP 5xx) blocks the request, just like an invalid token would.
Setting CAP_FAIL_OPEN=true inverts this: communication errors silently pass, so a Cap outage does not take your forms down with it.
An explicitly invalid token (success: false) is always rejected regardless of this setting. Fail-open only covers infrastructure failures, not verification failures.
Usage
Blade directives
Include the Cap widget and its script in any Blade form:
@capScripts <form method="POST" action="/contact"> @csrf @cap <button type="submit">Submit</button> </form>
@cap renders the <cap-widget> element with the configured endpoint.
@capScripts renders the <script> tag loading the widget from jsDelivr.
The widget automatically injects a hidden cap-token field into its parent form upon successful verification.
CSP nonce support
@capScripts accepts an optional nonce for strict Content Security Policies:
{{-- Laravel Vite --}} @capScripts(Vite::cspNonce()) {{-- Spatie CSP or custom nonce --}} @capScripts($nonce)
CSP headers
Cap's widget relies on Web Workers and WebAssembly for the Proof-of-Work computation. A strict CSP must account for this beyond the script nonce:
Content-Security-Policy:
script-src 'nonce-{nonce}' 'strict-dynamic';
worker-src blob:;
wasm-unsafe-eval;
worker-src blob: is required because the widget spawns workers via Blob URLs.
wasm-unsafe-eval is required for the WebAssembly hash computation.
Middleware
Protect any route by applying the cap.verify middleware:
Route::post('/contact', [ContactController::class, 'store']) ->middleware('cap.verify');
Returns HTTP 422 with the message Cap verification failed. if the token is missing or invalid.
Validation rule
Use CapRule inside a Form Request or an inline validator:
use LaravelCap\Rules\CapRule; public function rules(): array { return [ 'cap-token' => ['required', new CapRule], // other fields... ]; }
Facade
use LaravelCap\Facades\Cap; if (Cap::verify($request->input('cap-token'))) { // token is valid }
Service (dependency injection)
use LaravelCap\Cap; class ContactController extends Controller { public function __construct(private readonly Cap $cap) {} public function store(Request $request): RedirectResponse { $this->cap->verifyOrFail($request->input('cap-token')); // ... } }
verifyOrFail() throws a CapVerificationException if the token is invalid.
Testing
composer install ./vendor/bin/phpunit
License
MIT