refactor: move hashcash stamp from X-Hashcash header to JSON request body

Move the hashcash proof-of-work stamp from the X-Hashcash HTTP header
into the JSON request body as a 'hashcash' field on POST /api/v1/session.

Updated server handler, CLI client, SPA client, and documentation.
This commit is contained in:
clawbot
2026-03-10 10:00:20 -07:00
committed by user
parent db4cd9f055
commit 09a1dddbd0
5 changed files with 52 additions and 59 deletions

View File

@@ -989,23 +989,18 @@ Create a new user session. This is the entry point for all clients.
If the server requires hashcash proof-of-work (see
[Hashcash Proof-of-Work](#hashcash-proof-of-work)), the client must include a
valid stamp in the `X-Hashcash` request header. The required difficulty is
advertised via `GET /api/v1/server` in the `hashcash_bits` field.
**Request Headers:**
| Header | Required | Description |
|--------------|----------|-------------|
| `X-Hashcash` | Conditional | Hashcash stamp (required when server has `hashcash_bits` > 0) |
valid stamp in the `hashcash` field of the JSON request body. The required
difficulty is advertised via `GET /api/v1/server` in the `hashcash_bits` field.
**Request Body:**
```json
{"nick": "alice"}
{"nick": "alice", "hashcash": "1:20:260310:neoirc::3a2f1"}
```
| Field | Type | Required | Constraints |
|--------|--------|----------|-------------|
|------------|--------|-------------|-------------|
| `nick` | string | Yes | 132 characters, must be unique on the server |
| `hashcash` | string | Conditional | Hashcash stamp (required when server has `hashcash_bits` > 0) |
**Response:** `201 Created`
```json
@@ -1027,7 +1022,7 @@ advertised via `GET /api/v1/server` in the `hashcash_bits` field.
| Status | Error | When |
|--------|-------|------|
| 400 | `nick must be 1-32 characters` | Empty or too-long nick |
| 402 | `hashcash proof-of-work required` | Missing `X-Hashcash` header when hashcash is enabled |
| 402 | `hashcash proof-of-work required` | Missing `hashcash` field in request body when hashcash is enabled |
| 402 | `invalid hashcash stamp: ...` | Stamp fails validation (wrong bits, expired, reused, etc.) |
| 409 | `nick already taken` | Another active session holds this nick |
@@ -1035,8 +1030,7 @@ advertised via `GET /api/v1/server` in the `hashcash_bits` field.
```bash
TOKEN=$(curl -s -X POST http://localhost:8080/api/v1/session \
-H 'Content-Type: application/json' \
-H 'X-Hashcash: 1:20:260310:neoirc::3a2f1' \
-d '{"nick":"alice"}' | jq -r .token)
-d '{"nick":"alice","hashcash":"1:20:260310:neoirc::3a2f1"}' | jq -r .token)
echo $TOKEN
```
@@ -2138,7 +2132,7 @@ account registration, no IP-based rate limits that punish shared networks.
2. Client computes a hashcash stamp: find a counter value such that the
SHA-256 hash of the stamp string has the required number of leading zero
bits.
3. Client includes the stamp in the `X-Hashcash` request header when creating
3. Client includes the stamp in the `hashcash` field of the JSON request body when creating
a session: `POST /api/v1/session`.
4. Server validates the stamp:
- Version is `1`
@@ -2195,7 +2189,7 @@ Both the embedded web SPA and the CLI client automatically handle hashcash:
1. Fetch `GET /api/v1/server` to read `hashcash_bits`
2. If `hashcash_bits > 0`, compute a valid stamp
3. Include the stamp in the `X-Hashcash` header on `POST /api/v1/session`
3. Include the stamp in the `hashcash` field of the JSON body on `POST /api/v1/session`
The web SPA uses the Web Crypto API (`crypto.subtle.digest`) for SHA-256
computation with batched parallelism. The CLI client uses Go's `crypto/sha256`.

View File

@@ -52,7 +52,7 @@ func (client *Client) CreateSession(
// Fetch server info to check for hashcash requirement.
info, err := client.GetServerInfo()
var headers map[string]string
var hashcashStamp string
if err == nil && info.HashcashBits > 0 {
resource := info.Name
@@ -60,17 +60,13 @@ func (client *Client) CreateSession(
resource = "neoirc"
}
stamp := MintHashcash(info.HashcashBits, resource)
headers = map[string]string{
"X-Hashcash": stamp,
}
hashcashStamp = MintHashcash(info.HashcashBits, resource)
}
data, err := client.doWithHeaders(
data, err := client.do(
http.MethodPost,
"/api/v1/session",
&SessionRequest{Nick: nick},
headers,
&SessionRequest{Nick: nick, Hashcash: hashcashStamp},
)
if err != nil {
return nil, err

View File

@@ -5,6 +5,7 @@ import "time"
// SessionRequest is the body for POST /api/v1/session.
type SessionRequest struct {
Nick string `json:"nick"`
Hashcash string `json:"hashcash,omitempty"`
}
// SessionResponse is the response from session creation.

View File

@@ -145,35 +145,9 @@ func (hdlr *Handlers) handleCreateSession(
writer http.ResponseWriter,
request *http.Request,
) {
// Validate hashcash proof-of-work if configured.
if hdlr.params.Config.HashcashBits > 0 {
stamp := request.Header.Get("X-Hashcash")
if stamp == "" {
hdlr.respondError(
writer, request,
"hashcash proof-of-work required",
http.StatusPaymentRequired,
)
return
}
err := hdlr.hashcashVal.Validate(
stamp, hdlr.params.Config.HashcashBits,
)
if err != nil {
hdlr.respondError(
writer, request,
"invalid hashcash stamp: "+err.Error(),
http.StatusPaymentRequired,
)
return
}
}
type createRequest struct {
Nick string `json:"nick"`
Hashcash string `json:"hashcash,omitempty"`
}
var payload createRequest
@@ -189,6 +163,32 @@ func (hdlr *Handlers) handleCreateSession(
return
}
// Validate hashcash proof-of-work if configured.
if hdlr.params.Config.HashcashBits > 0 {
if payload.Hashcash == "" {
hdlr.respondError(
writer, request,
"hashcash proof-of-work required",
http.StatusPaymentRequired,
)
return
}
err = hdlr.hashcashVal.Validate(
payload.Hashcash, hdlr.params.Config.HashcashBits,
)
if err != nil {
hdlr.respondError(
writer, request,
"invalid hashcash stamp: "+err.Error(),
http.StatusPaymentRequired,
)
return
}
}
payload.Nick = strings.TrimSpace(payload.Nick)
if !validNickRe.MatchString(payload.Nick) {

View File

@@ -135,20 +135,22 @@ function LoginScreen({ onLogin }) {
e.preventDefault();
setError("");
try {
const extraHeaders = {};
let hashcashStamp = "";
if (hashcashBitsRef.current > 0) {
setError("Computing proof-of-work...");
const stamp = await mintHashcash(
hashcashStamp = await mintHashcash(
hashcashBitsRef.current,
hashcashResourceRef.current,
);
extraHeaders["X-Hashcash"] = stamp;
setError("");
}
const reqBody = { nick: nick.trim() };
if (hashcashStamp) {
reqBody.hashcash = hashcashStamp;
}
const res = await api("/session", {
method: "POST",
body: JSON.stringify({ nick: nick.trim() }),
headers: extraHeaders,
body: JSON.stringify(reqBody),
});
localStorage.setItem("neoirc_token", res.token);
onLogin(res.nick);