Metadata-Version: 2.4
Name: pineward
Version: 0.1.0
Summary: Stateless anti-bot form shield — server-side validation (Wesolowski VDF + HMAC tokens + behavior + honeypots) plus a last-resort captcha. Standard library + Pillow.
Author-email: DeutschePolska LLC <hello@deutschepolska.com>
License-Expression: MIT
Project-URL: Homepage, https://github.com/pinecall/pineward-py
Project-URL: Repository, https://github.com/pinecall/pineward-py
Keywords: anti-bot,captcha,form-protection,vdf,honeypot,bot-detection,proof-of-work,security
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Operating System :: OS Independent
Classifier: Intended Audience :: Developers
Classifier: Topic :: Internet :: WWW/HTTP
Classifier: Topic :: Security
Requires-Python: >=3.10
Description-Content-Type: text/markdown
License-File: LICENSE
Requires-Dist: Pillow>=9
Provides-Extra: dev
Requires-Dist: pytest>=7; extra == "dev"
Dynamic: license-file

# pineward (Python)

Stateless anti-bot form shield — **server-side validation in pure Python** (standard library + Pillow for the captcha renderer).

This is the Python reference implementation of [Pineward](https://github.com/pinecall/pineward)'s
server validation: it verifies the HMAC tokens, Wesolowski VDF proofs, honeypots, behavior
metrics, and instrumentation fingerprints produced by the JavaScript client. It is
**byte-compatible** with `@pineward/server` — verified against the shared golden vectors in
[`tests/vectors/v0-vectors.json`](tests/vectors/v0-vectors.json) (generated by the JS core).

## Install

```bash
pip install pineward
```

Requires Python 3.10+. One runtime dependency — **Pillow** (renders the last-resort captcha); everything else is standard library. So `pip install pineward` ships the captcha out of the box.

## Usage

```python
from pineward import validate, ValidateOptions, MemoryNonceStore

opts = ValidateOptions(
    secret=b"my-secret-minimum-32-characters-long!!",
    expected_origin="https://yoursite.com",
    nonce_store=MemoryNonceStore(),   # use Redis/DB in serverless/multi-instance
)

# `submission` is the parsed form body: the __pw_* fields + formData.
submission = {
    "token": form["__pw_token"],
    "vdfProof": json.loads(form["__pw_vdf"]),
    "behavior": json.loads(form["__pw_behavior"]),
    "instrFingerprint": form["__pw_instr"],
    "formData": form,
}

result = validate(submission, opts)
if not result.ok:
    abort(403, f"pineward: {result.reason}")
# result.payload is the verified token payload
```

`result.reason` is one of: `bad-token`, `expired`, `wrong-origin`, `replay`,
`honeypot-tripped`, `bad-vdf`, `bad-instrumentation`, `bad-behavior`.

For lightweight integrations, set `skip_vdf=True` and/or `skip_instrumentation=True`.

## Last-resort captcha

Every layer above is invisible. The one **visible** layer is a server-rendered,
adversarial-text captcha — the escalation target when a request looks automated.
It verifies an *answer* (not forgeable client-reported motion): the challenge is
rendered server-side with secret randomness, so the answer lives in the pixels and
can't be precomputed from this open-source code — an agent has to actually read an
image tuned to break general OCR/VLMs while staying human-legible. The signed token
carries only an HMAC of the answer, never the text.

```python
from pineward import issue_captcha, verify_captcha, CaptchaVerifyOptions, MemoryNonceStore

# escalate (e.g. when validate() soft-fails a forgeable signal) instead of hard-blocking:
ch = issue_captcha(secret=SECRET, form_id="contact", origin="https://yoursite.com")
#   -> ch.token (opaque, signed) + ch.png (bytes) / ch.data_url (for <img src>)

res = verify_captcha(
    {"captchaToken": token, "answer": user_typed},
    CaptchaVerifyOptions(secret=SECRET, expected_origin="https://yoursite.com",
                         nonce_store=MemoryNonceStore()),
)
# res.reason: bad-token | expired | wrong-origin | wrong-kind | replay | wrong-answer
```

Optionally also require the answer to have been *typed* (a forgeable, naive-bot filter
— not a guarantee): pass `keystrokes=KeystrokeConfig()` to `CaptchaVerifyOptions` and
include `typing` (`{"t": [per-key ms], "paste": bool}`) in the submission; a direct POST
or pasted answer then fails with `bad-keystrokes`.

Rendering uses Pillow, which ships with pineward — nothing extra to install.

Bound brute-force with PineGuard (per-IP rate-limit) — see
[`examples/captcha`](examples/captcha) for the full escalation flow.

## Public API

- `validate(submission, opts)` · `ValidateOptions` · `ValidateResult`
- `verify_token` / `sign_token` · `verify_vdf` / `hash_to_prime` / `miller_rabin`
- `score_behavior` · `check_honeypots` / `generate_honeypots` · `is_valid_instr_fingerprint`
- `RateTracker` / `compute_t` · `MemoryNonceStore` / `NonceStore`
- `PineGuard` / `GuardRule` / `GuardDecision`
- `issue_captcha` / `verify_captcha` / `render_challenge` · `CaptchaVerifyOptions` · `check_keystrokes` / `KeystrokeConfig`
- `ScoringConfig` · `DifficultyConfig` · `RSA_2048_MODULUS` · `FIELD_*`

## Develop

```bash
pip install -e '.[dev]'
pytest
```

The wire format is specified in the JS repo's `spec/v0.md`; the vectors here are the
cross-language contract. If the token/VDF encoding changes, regenerate vectors on the JS
side and copy them here.

## License

MIT
