@powforge/captcha
v0.1.1
Published
Self-hosted proof-of-work CAPTCHA. Privacy-first reCAPTCHA alternative with zero tracking.
Downloads
267
Maintainers
Readme
@powforge/captcha
Self-hosted reCAPTCHA alternative using proof-of-work. No tracking, no cookies, no external dependencies.
Your users prove they're human by computing SHA-256 hashes in their browser. No data leaves your site. No third-party scripts. Works offline once loaded.
Quick Start
1. Install
npm install @powforge/captchaOr use a script tag (no build step needed):
<script src="https://unpkg.com/@powforge/captcha/dist/powforge-captcha.min.js"></script>2. Add the widget
<form action="/submit" method="POST">
<input type="text" name="email" placeholder="Email">
<!-- CAPTCHA mounts here -->
<div id="pow-captcha"></div>
<input type="hidden" name="pf_token">
<button type="submit">Submit</button>
</form>
<script src="powforge-captcha.min.js"
data-target="#pow-captcha"
data-server="https://captcha.powforge.dev"
data-theme="dark"></script>3. Verify server-side
const { verifyToken } = require('@powforge/captcha/verify');
app.post('/submit', async (req, res) => {
const result = await verifyToken(req.body.pf_token, {
server: 'https://captcha.powforge.dev',
});
if (!result.valid) {
return res.status(403).json({ error: 'CAPTCHA failed' });
}
// User verified - process form...
});How It Works
- Your page loads the widget (5KB gzipped)
- Widget fetches a challenge from the verification server
- A Web Worker mines SHA-256 hashes until it finds one with enough leading zero bits
- The solution is sent to the server for verification
- Server returns a signed token your backend can validate
Default difficulty: 16 bits (~65,536 hashes, ~4 seconds on average hardware). Adjustable per-challenge.
The proof-of-work is real energy expenditure. Bots pay a real cost. Humans wait a few seconds. No behavioral tracking, no fingerprinting, no cookies.
API
Script Tag (zero config)
<script src="powforge-captcha.min.js"
data-target="#my-div"
data-server="https://captcha.powforge.dev"
data-theme="dark"
data-difficulty="16"
data-callback="onVerified">
</script>| Attribute | Default | Description |
|-----------|---------|-------------|
| data-target | (required) | CSS selector for mount element |
| data-server | https://captcha.powforge.dev | Verification server URL |
| data-theme | dark | dark or light |
| data-difficulty | 16 | Leading zero bits required |
| data-callback | none | Global function called with (token, method) |
ES Module / npm
import { PowCaptcha } from '@powforge/captcha';
const captcha = new PowCaptcha({
target: '#pow-captcha',
server: 'https://captcha.powforge.dev',
theme: 'dark',
difficulty: 16,
});
captcha.on('verified', ({ token, method }) => {
console.log('Token:', token);
});
captcha.on('progress', ({ nonce, elapsed, percent }) => {
console.log(`${percent.toFixed(0)}% complete`);
});
captcha.on('error', (err) => {
console.error('CAPTCHA error:', err);
});Methods
| Method | Description |
|--------|-------------|
| start() | Fetch challenge and begin solving |
| reset() | Destroy current work, fetch new challenge |
| getToken() | Returns token string or null |
| destroy() | Terminate worker, free resources |
| on(event, fn) | Listen for events |
Events
| Event | Payload | Description |
|-------|---------|-------------|
| verified | { token, method } | Challenge solved and verified |
| progress | { nonce, elapsed, percent } | Mining progress update |
| error | Error | Challenge fetch or solve failure |
DOM Events
The widget also fires a powforge:token CustomEvent on window:
window.addEventListener('powforge:token', (e) => {
console.log(e.detail.token, e.detail.method);
});Hidden Input
If your form contains <input type="hidden" name="pf_token">, it is auto-filled when the CAPTCHA is solved.
Server Verification
const { verifyToken } = require('@powforge/captcha/verify');
// or: import { verifyToken } from '@powforge/captcha/verify';
const result = await verifyToken(token, {
server: 'https://captcha.powforge.dev', // your self-hosted URL
timeout: 5000, // ms
});
// result: { valid: true, method: 'pow', issued_at: '...', expires_at: '...' }
// or: { valid: false, reason: 'expired token' }Comparison
| Feature | PowForge | reCAPTCHA | hCaptcha | ALTCHA | Turnstile | |---------|----------|-----------|----------|--------|-----------| | Privacy | No tracking | Google tracking | Fingerprinting | No tracking | Cloudflare tracking | | Self-hosted | Yes | No | No | Yes | No | | Dependencies | 0 | Google JS | hCaptcha JS | 0 | Cloudflare JS | | Method | SHA-256 PoW | ML behavior | ML + labeling | PoW | ML behavior | | Bundle size | ~8KB gz | ~150KB | ~120KB | ~30KB | ~80KB | | Open source | MIT | No | No | MIT | No | | Cookies | None | Yes | Yes | None | Yes | | Works offline | Once loaded | No | No | Once loaded | No | | Lightning skip | Yes | No | No | No | No | | Cost | Free / self-host | Free tier + paid | Free tier + paid | Free / self-host | Free tier |
Self-Hosted Server
Run your own verification server:
# Clone the server
git clone https://gitlab.com/powforge/captcha.git
cd captcha
# The server is a single Node.js file, zero npm dependencies for core
node server.js
# Listening on port 3077Then point the widget at your server:
<script src="powforge-captcha.min.js"
data-target="#captcha"
data-server="https://your-server.com:3077">
</script>Server endpoints:
| Endpoint | Method | Description |
|----------|--------|-------------|
| /api/challenge | GET | Get a new PoW challenge |
| /api/verify | POST | Submit solution, get token |
| /api/token/verify | POST | Server-side token validation |
Difficulty Guide
| Bits | Expected Hashes | Avg Time (desktop) | Use Case | |------|----------------|---------------------|----------| | 10 | ~1,024 | <1s | Low friction, comment forms | | 14 | ~16,384 | ~1s | Standard forms | | 16 | ~65,536 | ~4s | Default. Good balance | | 18 | ~262,144 | ~15s | High-value actions | | 20 | ~1,048,576 | ~60s | Rate limiting, account creation |
License
MIT
