feat: implement hashcash proof-of-work for session creation (#63)
All checks were successful
check / check (push) Successful in 1m2s
All checks were successful
check / check (push) Successful in 1m2s
## Summary Implement SHA-256-based hashcash proof-of-work for `POST /session` to prevent abuse via rapid session creation. closes #11 ## What Changed ### Server - **New `internal/hashcash` package**: Validates hashcash stamps (format, difficulty bits, date/expiry, resource, replay prevention via in-memory spent set with TTL pruning) - **Config**: `NEOIRC_HASHCASH_BITS` env var (default 20, set to 0 to disable) - **`GET /api/v1/server`**: Now includes `hashcash_bits` field when > 0 - **`POST /api/v1/session`**: Validates `X-Hashcash` header when hashcash is enabled; returns HTTP 402 for missing/invalid stamps ### Clients - **Web SPA**: Fetches `hashcash_bits` from `/server`, computes stamp using Web Crypto API (`crypto.subtle.digest`) with batched parallelism (1024 hashes/batch), shows "Computing proof-of-work..." feedback - **CLI (`neoirc-cli`)**: `CreateSession()` auto-fetches server info and computes a valid hashcash stamp when required; new `MintHashcash()` function in the API package ### Documentation - README updated with full hashcash documentation: stamp format, computing stamps, configuration, difficulty table - Server info and session creation API docs updated with hashcash fields/headers - Roadmap updated (hashcash marked as implemented) ## Stamp Format Standard hashcash: `1:bits:YYMMDD:resource::counter` The SHA-256 hash of the entire stamp string must have at least `bits` leading zero bits. ## Validation Rules - Version must be `1` - Claimed bits ≥ required bits - Resource must match server name - Date within 48 hours (not expired, not too far in future) - SHA-256 hash has required leading zero bits - Stamp not previously used (replay prevention) ## Testing - All existing tests pass (hashcash disabled in test config with `HashcashBits: 0`) - `docker build .` passes (lint + test + build) <!-- session: agent:sdlc-manager:subagent:f98d712e-8a40-4013-b3d7-588cbff670f4 --> Co-authored-by: clawbot <clawbot@noreply.git.eeqj.de> Co-authored-by: clawbot <clawbot@noreply.eeqj.de> Co-authored-by: user <user@Mac.lan guest wan> Co-authored-by: Jeffrey Paul <sneak@noreply.example.org> Reviewed-on: #63 Co-authored-by: clawbot <clawbot@noreply.example.org> Co-committed-by: clawbot <clawbot@noreply.example.org>
This commit was merged in pull request #63.
This commit is contained in:
@@ -13,6 +13,7 @@ import (
|
||||
"git.eeqj.de/sneak/neoirc/internal/config"
|
||||
"git.eeqj.de/sneak/neoirc/internal/db"
|
||||
"git.eeqj.de/sneak/neoirc/internal/globals"
|
||||
"git.eeqj.de/sneak/neoirc/internal/hashcash"
|
||||
"git.eeqj.de/sneak/neoirc/internal/healthcheck"
|
||||
"git.eeqj.de/sneak/neoirc/internal/logger"
|
||||
"go.uber.org/fx"
|
||||
@@ -39,6 +40,7 @@ type Handlers struct {
|
||||
log *slog.Logger
|
||||
hc *healthcheck.Healthcheck
|
||||
broker *broker.Broker
|
||||
hashcashVal *hashcash.Validator
|
||||
cancelCleanup context.CancelFunc
|
||||
}
|
||||
|
||||
@@ -47,11 +49,17 @@ func New(
|
||||
lifecycle fx.Lifecycle,
|
||||
params Params,
|
||||
) (*Handlers, error) {
|
||||
resource := params.Config.ServerName
|
||||
if resource == "" {
|
||||
resource = "neoirc"
|
||||
}
|
||||
|
||||
hdlr := &Handlers{ //nolint:exhaustruct // cancelCleanup set in startCleanup
|
||||
params: ¶ms,
|
||||
log: params.Logger.Get(),
|
||||
hc: params.Healthcheck,
|
||||
broker: broker.New(),
|
||||
params: ¶ms,
|
||||
log: params.Logger.Get(),
|
||||
hc: params.Healthcheck,
|
||||
broker: broker.New(),
|
||||
hashcashVal: hashcash.NewValidator(resource),
|
||||
}
|
||||
|
||||
lifecycle.Append(fx.Hook{
|
||||
|
||||
Reference in New Issue
Block a user