@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.
- package/README.md +81 -4
- 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
|
-
|
|
107
|
-
|
|
108
|
-
- **
|
|
109
|
-
|
|
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.
|
|
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.
|
|
40
|
+
"@btx-tools/challenges-sdk": "0.1.1"
|
|
41
41
|
},
|
|
42
42
|
"keywords": [
|
|
43
43
|
"btx",
|