pineward
v0.1.0
Published
Framework-agnostic anti-bot form shield. VDF + behavior + honeypots. No third-party calls.
Maintainers
Readme
Pineward
Stateless anti-bot form shield. No third-party calls. No CAPTCHAs. One import.
Pineward combines a Verifiable Delay Function (VDF), behavioral entropy analysis, rotated honeypots, and browser instrumentation into a single library that protects HTML forms from automated abuse — with zero external dependencies.
Table of Contents
- Features
- Quick Start
- How It Works
- Defense Layers
- Framework Support
- Configuration
- Architecture
- Algorithm Details
- Project Structure
- Scripts
- Testing
- License
Features
- Verifiable Delay Function — Wesolowski VDF forces sequential computation (500ms–2s), blocks mass automation and GPU farms
- Behavioral Entropy — Passive timing analysis distinguishes humans from robotic input patterns
- Rotated Honeypots — Per-session field names that look real to bots, verified empty server-side
- Browser Instrumentation — 20 fingerprint operations detect headless browsers and automation tools
- Adaptive Difficulty — VDF difficulty scales with request rate to counter sustained DDoS
- Stateless Tokens — HMAC-SHA256 signed tokens with nonce replay protection, no session store required
- Framework Agnostic — Pure TypeScript core + thin adapters for React, Next.js, Vite, Express, Hono
- Privacy First — Behavior collector captures timestamps only, never coordinates or key codes
- Universal Runtime — Works in Node 20+, Bun, Deno, Cloudflare Workers, Vercel Edge, browsers
Quick Start
1. Install
pnpm add pineward2. Server — Issue Challenges
// app/api/pineward/route.ts (Next.js App Router)
import { createChallengeRoute } from 'pineward/next';
export const GET = createChallengeRoute({
secret: process.env.PINEWARD_SECRET!,
});3. Client — Protect a Form
// app/contact/page.tsx
import { Pineward } from 'pineward/react';
export default function ContactPage() {
return (
<Pineward challengeUrl="/api/pineward">
<input name="email" type="email" required />
<textarea name="message" required />
<button type="submit">Send</button>
</Pineward>
);
}4. Server — Validate Submissions
// app/contact/actions.ts
'use server';
import { withPineward } from 'pineward/next';
export const submitContact = withPineward(async (formData: FormData) => {
const email = formData.get('email') as string;
const message = formData.get('message') as string;
// Save to DB, send email, etc.
}, {
secret: process.env.PINEWARD_SECRET!,
origin: 'https://yoursite.com',
});Three files and your form is protected.
How It Works
When a user loads a page with a Pineward-protected form:
- Challenge issued — the server generates a signed token with an embedded VDF challenge, honeypot field names, and an instrumentation seed
- VDF solved — the browser solves
y = x^(2^t) mod Nvia sequential squaring in a Web Worker (500ms–2s depending on load) - Behavior collected — passive DOM listeners capture timing entropy from mouse, keyboard, and scroll events
- Instrumentation run — browser fingerprint operations detect headless/automated environments
- Form submitted — all proof data is attached as hidden fields and validated server-side in a fail-fast pipeline
Defense Layers
| Layer | What it stops | Client cost | Server cost | |---|---|---|---| | HMAC token | Forgery, tamper | µs | µs | | Nonce | Replay attacks | — | µs | | Honeypot | Naive crawlers | 0 | µs | | VDF (Wesolowski) | Mass automation, GPU farms | 500ms–2s | 3–5ms | | Instrumentation | Headless browsers | <50ms | µs | | Behavior entropy | Robotic input patterns | 0 (passive) | µs | | Adaptive difficulty | Sustained DDoS | scales | µs |
Framework Support
React
import { Pineward } from 'pineward/react';
<Pineward
challengeUrl="/api/pineward"
onReady={() => console.log('Ready!')}
onProgress={(done, total) => console.log(`${done}/${total}`)}
>
{/* form fields */}
</Pineward>React Hook
import { usePineward } from 'pineward/react';
function MyForm() {
const { ready, solving } = usePineward({
challengeUrl: '/api/pineward',
formSelector: '#my-form',
});
return (
<form id="my-form">
<input name="email" />
<button disabled={!ready}>
{solving ? 'Verifying...' : 'Submit'}
</button>
</form>
);
}Next.js
// API Route — issue challenges
import { createChallengeRoute } from 'pineward/next';
export const GET = createChallengeRoute({ secret: process.env.PINEWARD_SECRET! });
// Server Action — validate submissions
import { withPineward } from 'pineward/next';
export const submit = withPineward(async (formData) => {
// your logic
}, { secret: process.env.PINEWARD_SECRET!, origin: 'https://yoursite.com' });Vite
// vite.config.ts
import { pineward } from 'pineward/vite';
export default defineConfig({
plugins: [pineward()],
});The Vite plugin serves /__pineward/challenge in dev mode with fast t values for instant iteration.
Vanilla JS
import { Pineward } from 'pineward';
const pw = new Pineward({
challengeUrl: '/api/pineward',
formSelector: '#contact-form',
onReady: () => document.querySelector('button')!.disabled = false,
});
await pw.mount();Express
import express from 'express';
import { createNodeChallengeHandler, createNodeValidateMiddleware } from 'pineward/server';
const app = express();
app.use(express.json());
const opts = { secret: process.env.PINEWARD_SECRET!, origin: 'https://yoursite.com' };
app.get('/api/pineward', createNodeChallengeHandler(opts));
app.post('/api/contact', createNodeValidateMiddleware(opts), (req, res) => {
res.json({ ok: true });
});Hono / Cloudflare Workers
import { Hono } from 'hono';
import { createWebChallengeHandler, createWebValidateHandler } from 'pineward/server';
const app = new Hono();
const opts = { secret: 'your-secret', origin: 'https://yoursite.com' };
app.get('/api/pineward', (c) => createWebChallengeHandler(opts)(c.req.raw));
app.post('/api/contact', async (c) => {
const result = await createWebValidateHandler(opts)(c.req.raw);
if (!result.ok) return c.json({ error: result.reason }, 403);
return c.json({ ok: true });
});Configuration
Environment Variables
| Variable | Required | Description |
|---|---|---|
| PINEWARD_SECRET | Yes | HMAC secret for signing tokens. Minimum 32 characters. |
Difficulty Tuning
| Parameter | Default | Description |
|---|---|---|
| baseT | 2^20 | ~500ms on a 2024 phone (normal traffic) |
| maxT | 2^23 | ~4s (under sustained attack) |
| windowMs | 60000 | Sliding window for rate tracking |
| thresholdLow | 30 | Requests/min before scaling starts |
| thresholdHigh | 300 | Requests/min for maximum difficulty |
Behavior Scoring
| Parameter | Default | Description |
|---|---|---|
| minDurationMs | 500 | Reject submissions faster than 500ms |
| minTimingEntropy | 2.0 | Reject perfectly regular timing (bits) |
| minEventCount | 3 | Minimum total interaction events |
Architecture
pineward (single package, multiple subpath exports)
├── pineward → client widget + core types
├── pineward/server → validate(), issueChallenge(), framework handlers
├── pineward/react → <Pineward />, usePineward()
├── pineward/next → createChallengeRoute(), withPineward(), middleware
└── pineward/vite → Vite plugin with dev endpointCore is pure TypeScript — no framework imports. Adapters are thin wrappers (<500 LOC each).
Algorithm Details
VDF (Wesolowski)
The VDF forces clients to perform sequential computation that cannot be parallelized:
- Solve:
y = x^(2^t) mod N— sequential squaring, O(t) - Prove:
π = x^(⌊2^t/l⌋) mod N— Wesolowski proof, O(t) - Verify:
π^l · x^r ≡ y (mod N)— two modular exponentiations, O(log N)
Uses the RSA-2048 modulus from the RSA Factoring Challenge (no known factorization).
Token Format
base64url(json_payload).base64url(hmac_sha256_signature)
All crypto via Web Crypto API — works in Node 20+, Bun, Deno, browsers, Cloudflare Workers, Vercel Edge.
Project Structure
pineward/
├── src/
│ ├── index.ts # main entry: client widget + core types
│ ├── core/ # framework-agnostic, pure TS
│ │ ├── types.ts # shared types
│ │ ├── constants.ts # RSA-2048 modulus, defaults
│ │ ├── vdf.ts # Wesolowski VDF (solve, verify, createChallenge)
│ │ ├── hash-to-prime.ts # Miller-Rabin primality, hashToPrime
│ │ ├── token.ts # HMAC-signed stateless tokens
│ │ ├── honeypot.ts # rotated honeypot field names
│ │ ├── behavior-score.ts # entropy scoring (server-side)
│ │ ├── instrumentation.ts # browser fingerprint challenges
│ │ └── adaptive.ts # difficulty engine (rate-aware)
│ ├── server/ # server validator
│ │ ├── validate.ts # main validate() + issueChallenge()
│ │ ├── handlers.ts # Express, Hono, Web Standard handlers
│ │ └── storage.ts # NonceStore abstraction
│ ├── client/ # vanilla browser widget
│ │ ├── widget.ts # Pineward class (mount, solve, submit)
│ │ ├── worker.ts # Web Worker for VDF
│ │ ├── behavior.ts # DOM event listeners
│ │ └── instrumentation.ts # runs instrumentation ops
│ ├── react/ # React adapter
│ │ ├── component.tsx # <Pineward />
│ │ └── hook.ts # usePineward()
│ ├── next/ # Next.js adapter
│ │ ├── route-handler.ts # createChallengeRoute()
│ │ ├── server-action.ts # withPineward() HOF
│ │ └── middleware.ts # Edge middleware
│ └── vite/ # Vite plugin
│ └── plugin.ts # auto-inject + dev endpoint
├── test/ # vitest tests
├── examples/ # Express, Next.js, Vite+React, Hono+CF
├── spec/ # wire format spec + test vectors
└── package.jsonScripts
| Command | Description |
|---------|-------------|
| pnpm build | Build all entry points (ESM + CJS + DTS) |
| pnpm test | Run test suite (vitest) |
| pnpm test:watch | Run tests in watch mode |
| pnpm lint | Lint with Biome |
| pnpm lint:fix | Lint and auto-fix |
| pnpm typecheck | TypeScript type checking |
Testing
pnpm test| Test Suite | Tests | What |
|-----------|-------|------|
| vdf.test.ts | 8 | VDF round-trip, tamper rejection, determinism, progress |
| token.test.ts | 15 | Sign/verify, wrong secret, tamper, expiry, base64url |
| honeypot.test.ts | 12 | Deterministic generation, uniqueness, checker |
| behavior.test.ts | 16 | Scoring, entropy, quantization, thresholds |
| adaptive.test.ts | 11 | computeT bounds, log-space interpolation, RateTracker |
| validate.test.ts | 14 | Full pipeline round-trip, every failure mode, replay |
| client.test.ts | 10 | BehaviorCollector, widget mount, event counting |
87 tests, all passing in under 1 second.
License
MIT
