@btx-tools/middleware-hono 0.1.0 → 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/README.md +81 -4
  2. package/package.json +2 -2
package/README.md CHANGED
@@ -38,6 +38,48 @@ app.post('/v1/generate',
38
38
  export default app;
39
39
  ```
40
40
 
41
+ ## ⚠️ Body consumption (read before async resolvers)
42
+
43
+ Hono's `c.req.json()` is **one-shot** — once consumed, the body stream is gone. If your `resource` / `subject` resolver does `await c.req.json()`, the route handler downstream **cannot read the body again** and will throw `BodyAlreadyUsedError`.
44
+
45
+ ❌ **This breaks**:
46
+ ```ts
47
+ btxAdmission({
48
+ // ...
49
+ resource: async (c) => `model:${(await c.req.json()).model}`,
50
+ }),
51
+ async (c) => {
52
+ const body = await c.req.json(); // ← throws — body already consumed!
53
+ return c.json({ ok: true });
54
+ }
55
+ ```
56
+
57
+ ✅ **Two safe patterns**:
58
+
59
+ ```ts
60
+ // Pattern 1: cache the body once at the top, pass through context
61
+ app.post('/v1/generate', async (c, next) => {
62
+ c.set('body', await c.req.json());
63
+ return next();
64
+ });
65
+ app.post('/v1/generate',
66
+ btxAdmission({
67
+ // ...
68
+ resource: (c) => `model:${(c.get('body') as { model: string }).model}`,
69
+ }),
70
+ async (c) => {
71
+ const body = c.get('body');
72
+ return c.json({ ok: true, body });
73
+ },
74
+ );
75
+
76
+ // Pattern 2: derive resolver inputs from headers, not body
77
+ btxAdmission({
78
+ // ...
79
+ resource: (c) => `model:${c.req.header('x-model') ?? 'default'}`,
80
+ }),
81
+ ```
82
+
41
83
  ## How it works
42
84
 
43
85
  Stateless **echo-the-challenge** flow:
@@ -103,10 +145,45 @@ app.onError((err, c) => {
103
145
 
104
146
  ## Edge-runtime notes
105
147
 
106
- - **Cloudflare Workers / Pages**: works out of the box. `BtxChallengeClient` uses `fetch()` which is the native Workers networking primitive.
107
- - **Deno Deploy**: same — `fetch()` is standard.
108
- - **Bun**: same.
109
- - **Vercel Edge**: works, but check max header size (Vercel Edge caps incoming headers around 16 KB). For high-difficulty challenges the JSON envelope might exceed this — consider switching to a stateful challenge-store middleware variant if you hit this.
148
+ ### Network reachability
149
+
150
+ `BtxChallengeClient` uses `fetch()` to reach btxd's JSON-RPC endpoint. **Edge runtimes cannot reach `127.0.0.1`** — they're sandboxed away from the host loopback. You need a **publicly reachable** btxd RPC URL:
151
+
152
+ - **Cloudflare Tunnel** (Argo Tunnel) — runs in front of your btxd, gives you a stable HTTPS URL the Worker can call
153
+ - **Public RPC proxy** — terminate TLS at Caddy/nginx in front of btxd, expose on a real DNS name
154
+ - **Self-hosted relay** with a public IP + Basic auth (verify `rpcallowip` in btx.conf permits the egress IP)
155
+
156
+ Do **not** put btxd's RPC port directly on the public internet without auth + TLS termination.
157
+
158
+ ### Runtime-specific
159
+
160
+ - **Cloudflare Workers / Pages**: works once reachability is solved. `fetch()` is native; no Node polyfills needed.
161
+ - **Deno Deploy**: same — Web `fetch()` is standard.
162
+ - **Bun**: works natively (also accepts a Node btxd via localhost when self-hosting Bun on the same box).
163
+ - **Vercel Edge**: works for typical challenge sizes. **Header-size limits vary across edge platforms** — Vercel, Cloudflare, and Fastly all have different caps for incoming headers. The `X-BTX-Challenge` header is ~3-5 KB for default difficulty; check your platform's documentation if you set high `target_solve_time_s` or run into preflight errors. For very large challenges, consider a stateful challenge-store middleware variant.
164
+
165
+ ## CORS
166
+
167
+ The `X-BTX-Challenge`, `X-BTX-Proof-Nonce`, and `X-BTX-Proof-Digest` headers are **custom**, which triggers a CORS preflight for any browser-originated fetch. Configure Hono's built-in `cors` middleware:
168
+
169
+ ```ts
170
+ import { cors } from 'hono/cors';
171
+ app.use('/v1/*', cors({
172
+ origin: 'https://your-frontend.example',
173
+ allowHeaders: [
174
+ 'content-type',
175
+ 'x-btx-challenge',
176
+ 'x-btx-challenge-id',
177
+ 'x-btx-proof-nonce',
178
+ 'x-btx-proof-digest',
179
+ ],
180
+ exposeHeaders: [
181
+ 'x-btx-challenge', // so the browser can READ the 402's challenge header
182
+ ],
183
+ }));
184
+ ```
185
+
186
+ Without `exposeHeaders` including `x-btx-challenge`, the browser sees the 402 status but **cannot** read the challenge JSON from the response header (Web Fetch hides non-CORS-safelisted response headers by default).
110
187
 
111
188
  ## Requirements
112
189
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@btx-tools/middleware-hono",
3
- "version": "0.1.0",
3
+ "version": "0.1.1",
4
4
  "description": "Hono middleware for @btx-tools/challenges-sdk — drop-in BTX service-challenge admission gate (Node + edge)",
5
5
  "author": "visitor-code <visitor@friction.market>",
6
6
  "license": "MIT",
@@ -37,7 +37,7 @@
37
37
  "tsup": "^8.3.0",
38
38
  "typescript": "^5.6.0",
39
39
  "vitest": "^2.1.0",
40
- "@btx-tools/challenges-sdk": "0.1.0"
40
+ "@btx-tools/challenges-sdk": "0.1.1"
41
41
  },
42
42
  "keywords": [
43
43
  "btx",