@basaltkit/hono 1.4.0 → 1.5.0
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 +76 -3
- package/dist/index.d.ts +8 -2
- package/dist/index.js +12 -5
- package/package.json +7 -3
package/README.md
CHANGED
|
@@ -14,7 +14,7 @@ The Basalt adapter for [Hono](https://hono.dev): the same typed routes, enricher
|
|
|
14
14
|
|
|
15
15
|
This module connects Hono to Basalt. **Routes** (a path + HTTP method, e.g. `POST /echo`) are defined with the `route()` function from `@basaltkit/http`, with [Zod](https://zod.dev) schemas that validate the body, query, and URL parameters — and give you TypeScript types for free. The adapter converts each Hono request into Basalt's neutral format, runs the shared pipeline (validation, *enrichers* — functions that enrich the request context — and *guards* — functions that can reject it, e.g. authentication), and returns responses with errors in a stable format.
|
|
16
16
|
|
|
17
|
-
The main benefit is **portability**: a route written here runs unchanged on `@basaltkit/fastify` and `@basaltkit/express
|
|
17
|
+
The main benefit is **portability**: a route written here runs unchanged on `@basaltkit/fastify` and `@basaltkit/express` — **the three adapters are equals**. Routes, enrichers, guards, the per-request context, standardized errors, the neutral 404, ETags (`meta.etag`), SSE, per-route rate limits and the boot-time guarded-meta check all behave identically. The neutral edge plugins (security, health, metrics, tracing, OpenAPI) from `@basaltkit/http` work on Hono exactly the same way.
|
|
18
18
|
|
|
19
19
|
## Installation
|
|
20
20
|
|
|
@@ -111,6 +111,57 @@ Unexpected errors respond with `500` and `{ error: { code: 'INTERNAL_ERROR', ...
|
|
|
111
111
|
|
|
112
112
|
The adapter reads the body based on `Content-Type`: `application/json` → JSON object; forms (`form`) → Hono's `parseBody()`; other text → string; empty or invalid body → `undefined` (Zod validation handles the rest). `GET`/`HEAD` requests never have a body.
|
|
113
113
|
|
|
114
|
+
### Body-size limit — `bodyLimit`
|
|
115
|
+
|
|
116
|
+
Hono and edge runtimes impose **no** default cap on a request body, so an upload is
|
|
117
|
+
unbounded unless something stops it. The plugin installs a guard that rejects any request
|
|
118
|
+
whose `Content-Length` exceeds the limit with `413` and
|
|
119
|
+
`{ code: 'PAYLOAD_TOO_LARGE', message: … }` — **before the body is read**.
|
|
120
|
+
|
|
121
|
+
```ts
|
|
122
|
+
import { DEFAULT_BODY_LIMIT, honoPlugin } from '@basaltkit/hono'
|
|
123
|
+
|
|
124
|
+
honoPlugin({ routes, bodyLimit: 5 * 1024 * 1024 }) // 5 MiB
|
|
125
|
+
DEFAULT_BODY_LIMIT // 1_048_576 — 1 MiB, the default
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
> Note this checks the declared `Content-Length`; it is a cheap first line of defence, not
|
|
129
|
+
> a streaming byte counter.
|
|
130
|
+
|
|
131
|
+
### Guarded route meta — the boot check
|
|
132
|
+
|
|
133
|
+
If a route declares `meta.auth`, `meta.can` or `meta.teamRole` and **no registered plugin
|
|
134
|
+
enforces that key**, the route would serve unprotected. `honoPlugin` refuses to boot: it
|
|
135
|
+
calls `assertRoutesGuarded()` in its boot phase and throws `UnguardedRouteMetaError`
|
|
136
|
+
(code `HTTP_UNGUARDED_ROUTE_META`), naming every offending route and key, before a single
|
|
137
|
+
request is served.
|
|
138
|
+
|
|
139
|
+
Fix it by registering the enforcing plugin (`auth` → `authPlugin`, `can` →
|
|
140
|
+
`permissionsPlugin`, `teamRole` → `teamsPlugin`). If protection really does happen at an
|
|
141
|
+
outer edge (a Worker in front, a gateway), waive it explicitly:
|
|
142
|
+
|
|
143
|
+
```ts
|
|
144
|
+
honoPlugin({ routes, allowUnguardedMeta: true }) // waive every key
|
|
145
|
+
honoPlugin({ routes, allowUnguardedMeta: ['auth'] }) // waive one key
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
Fastify and Express run the identical check with the identical option.
|
|
149
|
+
|
|
150
|
+
### The neutral 404
|
|
151
|
+
|
|
152
|
+
Unmatched routes get the same JSON body on every adapter —
|
|
153
|
+
`{ "error": { "code": "NOT_FOUND", "message": "Route not found." } }` (`NOT_FOUND_RESPONSE`
|
|
154
|
+
from `@basaltkit/http`) — instead of Hono's plain-text default. An app that calls
|
|
155
|
+
`hono.notFound(…)` *later* still wins (Hono keeps the last handler); pass `notFound: false`
|
|
156
|
+
to opt out entirely.
|
|
157
|
+
|
|
158
|
+
### Streaming — SSE
|
|
159
|
+
|
|
160
|
+
A handler returning `sse(producer)` from `@basaltkit/http` becomes a `Response` backed by a
|
|
161
|
+
web `ReadableStream`, so it streams on Node, Bun, Deno and edge alike. Client aborts
|
|
162
|
+
(`request.signal`) are relayed to `stream.onClose()`. Same handler code as on Fastify and
|
|
163
|
+
Express.
|
|
164
|
+
|
|
114
165
|
### Enrichers and guards (authentication, tenancy, …)
|
|
115
166
|
|
|
116
167
|
Plugins register these functions in the container's metadata "buckets"; the adapter applies them to every route. Real example (from the package's tests):
|
|
@@ -226,10 +277,29 @@ In this mode errors are still standardized (each handler wraps `toErrorResponse`
|
|
|
226
277
|
| `routes` | `BasaltRoute[]` | No | `[]` | Routes (created with `route()` from `@basaltkit/http`) to mount. |
|
|
227
278
|
| `allowUnguardedMeta` | `boolean \| string[]` | No | fail loud at boot | Waives the boot check that every route declaring security meta (`auth`, `can`, `teamRole`) has a registered guard enforcing it (`UnguardedRouteMetaError` otherwise). `true` waives everything (edge/gateway auth); an array waives specific keys. |
|
|
228
279
|
| `app` | `Hono` | No | `new Hono()` | Bring your own Hono app; otherwise a new one is created. |
|
|
280
|
+
| `notFound` | `boolean` | No | `true` | Serve `NOT_FOUND_RESPONSE` (the neutral JSON 404) for unmatched routes. A later `hono.notFound(…)` of your own still wins; `false` opts out entirely. |
|
|
281
|
+
| `bodyLimit` | `number` | No | `DEFAULT_BODY_LIMIT` = `1_048_576` (1 MiB) | Maximum request body in bytes. A request whose `Content-Length` exceeds it is rejected `413 PAYLOAD_TOO_LARGE` before the body is read — Hono/edge has no default cap of its own. |
|
|
229
282
|
|
|
230
283
|
Behavior: registers the Hono app on the `HONO` token and an `HttpServerCollector` on the `HTTP_SERVER` token. On the `app:booted` event it mounts, in order: *after-hooks* middleware (metrics/tracing, measuring duration), *pre-hooks* middleware (security/CORS/rate limit; if one of these responds, the route doesn't run), the Basalt routes, and the extra edge plugin routes (`/livez`, `/metrics`, `/openapi.json`, …). Publishes the routes to the `'http:routes'` metadata bucket for OpenAPI/CLI/SDK.
|
|
231
284
|
|
|
232
|
-
> Note: this plugin has no `shutdown` step of its own — stopping the server (`serve` from `@hono/node-server`, etc.) is your responsibility.
|
|
285
|
+
> Note: this plugin has no `shutdown` step of its own — Basalt never starts a listener for you, so stopping the server (`serve` from `@hono/node-server`, etc.) is your responsibility.
|
|
286
|
+
|
|
287
|
+
### `DEFAULT_BODY_LIMIT`
|
|
288
|
+
|
|
289
|
+
`1_048_576` (1 MiB) — the default for `honoPlugin({ bodyLimit })`, exported so you can
|
|
290
|
+
reason about it or reuse it.
|
|
291
|
+
|
|
292
|
+
### Errors
|
|
293
|
+
|
|
294
|
+
| Error | Code | HTTP | When |
|
|
295
|
+
|---|---|---|---|
|
|
296
|
+
| `RequestValidationError` | `HTTP_VALIDATION` | 400 | `body`/`query`/`params` failed its Zod schema. Response carries `part` + `issues[]`. |
|
|
297
|
+
| `HttpError(status, code, message)` | *yours* | *yours* | Thrown deliberately from any layer. |
|
|
298
|
+
| `UnguardedRouteMetaError` | `HTTP_UNGUARDED_ROUTE_META` | — (boot) | A route declares a guarded key (`auth`/`can`/`teamRole`/`scopes`/`subscribed`/`feature`) with no guard enforcing it. Waive with `allowUnguardedMeta`. |
|
|
299
|
+
| — | `NOT_FOUND` | 404 | No route matched (unless `notFound: false`). |
|
|
300
|
+
| — | `PAYLOAD_TOO_LARGE` | 413 | Declared `Content-Length` exceeds `bodyLimit`. Body shape here is flat (`{ code, message }`), not the nested `{ error: … }` envelope. |
|
|
301
|
+
| — | `RATE_LIMITED` | 429 | `securityPlugin`'s limiter rejected the request. |
|
|
302
|
+
| — | `INTERNAL_ERROR` | 500 | Any other thrown error. The real message never reaches the client. |
|
|
233
303
|
|
|
234
304
|
### `HONO`
|
|
235
305
|
|
|
@@ -247,7 +317,7 @@ Dependency injection token (`Token<Hono>`): `app.container.get(HONO)` returns th
|
|
|
247
317
|
|
|
248
318
|
### What to import from where
|
|
249
319
|
|
|
250
|
-
This package
|
|
320
|
+
This package exports `honoPlugin`, `registerRoutes`, `HONO`, `DEFAULT_BODY_LIMIT`, and `HonoPluginOptions`. Everything else — `route`, `HttpError`, `RequestValidationError`, `NOT_FOUND_RESPONSE`, `sse`, `securityPlugin`, `RedisRateLimitStore`, `healthPlugin`, `metricsPlugin`, `tracingPlugin`, `openapiPlugin`, `escapeHtml`/`pageCsp`, types like `RequestEnricher`/`RouteGuard` — is imported from **`@basaltkit/http`**. (Unlike `@basaltkit/fastify`, this package re-exports nothing; that is a naming choice, not a capability gap.)
|
|
251
321
|
|
|
252
322
|
## Common errors and solutions (FAQ)
|
|
253
323
|
|
|
@@ -272,3 +342,6 @@ This package only exports `honoPlugin`, `registerRoutes`, `HONO`, and `HonoPlugi
|
|
|
272
342
|
- **`@basaltkit/fastify` / `@basaltkit/express`** — sibling adapters: the same routes, enrichers, guards, and edge plugins run on any of them without changes; switching frameworks (or runtime — Node → edge) is just switching the plugin.
|
|
273
343
|
- **`@basaltkit/auth` / `@basaltkit/tenancy` / `@basaltkit/permissions`** — register guards/enrichers on `'http:guards'`/`'http:enrichers'` and read the routes' `meta` (e.g. `meta: { auth: true }`); this adapter applies them automatically.
|
|
274
344
|
- **`@basaltkit/sdk` and the CLI** — consume the `'http:routes'` bucket (routes + Zod schemas) that this plugin publishes.
|
|
345
|
+
- **`@basaltkit/testing`** — `createTestApp({ adapter: 'hono' })` runs your suite against this adapter in-process (`hono.fetch(new Request(…))`, no socket).
|
|
346
|
+
|
|
347
|
+
Guides: [Adapters](/guide/adapters) · [Testing](/guide/testing) · [Security](/guide/security) · [Production](/guide/production)
|
package/dist/index.d.ts
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
import { Container } from '@basaltkit/core';
|
|
2
|
-
import { type BasaltRoute, type RequestEnricher, type RouteGuard } from '@basaltkit/http';
|
|
2
|
+
import { type HttpErrorReporter, type BasaltRoute, type RequestEnricher, type RouteGuard } from '@basaltkit/http';
|
|
3
3
|
import { Hono } from 'hono';
|
|
4
4
|
export declare const HONO: import("@basaltkit/core").Token<Hono<any, import("hono/types").BlankSchema, "/">>;
|
|
5
5
|
/** Default maximum request body size (1 MiB) — override via honoPlugin({ bodyLimit }). */
|
|
6
6
|
export declare const DEFAULT_BODY_LIMIT = 1048576;
|
|
7
7
|
/** Mounts Basalt routes on a Hono app (usable without the plugin). */
|
|
8
|
-
export declare function registerRoutes(app: Hono<any>, routes: BasaltRoute[], container?: Container, enrichers?: RequestEnricher[], guards?: RouteGuard[]): void;
|
|
8
|
+
export declare function registerRoutes(app: Hono<any>, routes: BasaltRoute[], container?: Container, enrichers?: RequestEnricher[], guards?: RouteGuard[], onError?: HttpErrorReporter): void;
|
|
9
9
|
export interface HonoPluginOptions {
|
|
10
10
|
routes?: BasaltRoute[];
|
|
11
11
|
/**
|
|
@@ -23,6 +23,12 @@ export interface HonoPluginOptions {
|
|
|
23
23
|
* text default. Default: true. An app calling `hono.notFound(…)` later
|
|
24
24
|
* still wins (Hono keeps the last handler); pass false to opt out entirely.
|
|
25
25
|
*/
|
|
26
|
+
/**
|
|
27
|
+
* Where failed requests are reported. Default: 5xx via `console.error` (with
|
|
28
|
+
* the stack) and 4xx via `console.warn`, prefixed `[basalt:http]`. Pass your
|
|
29
|
+
* own to route them into a real logger, or `() => {}` to silence them.
|
|
30
|
+
*/
|
|
31
|
+
onError?: HttpErrorReporter;
|
|
26
32
|
notFound?: boolean;
|
|
27
33
|
/**
|
|
28
34
|
* Maximum request body size in bytes. A request whose `Content-Length`
|
package/dist/index.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { Container, createToken, definePlugin, ensureMetadata } from '@basaltkit/core';
|
|
2
|
-
import { NOT_FOUND_RESPONSE, HttpServerCollector, HTTP_SERVER, runRoute, toErrorResponse, isSseResponse, sseProducerOf, driveSse, SSE_HEADERS, GUARDED_META_BUCKET, assertRoutesGuarded, } from '@basaltkit/http';
|
|
2
|
+
import { NOT_FOUND_RESPONSE, HttpServerCollector, HTTP_SERVER, runRoute, toErrorResponse, reportHttpError, isSseResponse, sseProducerOf, driveSse, SSE_HEADERS, GUARDED_META_BUCKET, assertRoutesGuarded, } from '@basaltkit/http';
|
|
3
3
|
import { Hono } from 'hono';
|
|
4
4
|
export const HONO = createToken('hono');
|
|
5
5
|
/** Default maximum request body size (1 MiB) — override via honoPlugin({ bodyLimit }). */
|
|
@@ -109,7 +109,7 @@ function toResponse(reply, payload) {
|
|
|
109
109
|
}
|
|
110
110
|
return new Response(body, { status: reply.statusCode, headers });
|
|
111
111
|
}
|
|
112
|
-
function handlerFor(definition, container, enrichers, guards) {
|
|
112
|
+
function handlerFor(definition, container, enrichers, guards, onError) {
|
|
113
113
|
return async (context) => {
|
|
114
114
|
const reply = new HonoReply(context);
|
|
115
115
|
try {
|
|
@@ -124,6 +124,13 @@ function handlerFor(definition, container, enrichers, guards) {
|
|
|
124
124
|
}
|
|
125
125
|
catch (error) {
|
|
126
126
|
const { status, body } = toErrorResponse(error);
|
|
127
|
+
// This adapter previously reported nothing at all — a 500 reached the
|
|
128
|
+
// client and left no trace whatsoever on the server.
|
|
129
|
+
const entry = { error, status, code: body.error.code, method: context.req.method, url: context.req.url };
|
|
130
|
+
if (onError)
|
|
131
|
+
onError(entry);
|
|
132
|
+
else
|
|
133
|
+
reportHttpError(entry);
|
|
127
134
|
return new Response(JSON.stringify(body), {
|
|
128
135
|
status,
|
|
129
136
|
headers: { 'content-type': 'application/json' },
|
|
@@ -132,9 +139,9 @@ function handlerFor(definition, container, enrichers, guards) {
|
|
|
132
139
|
};
|
|
133
140
|
}
|
|
134
141
|
/** Mounts Basalt routes on a Hono app (usable without the plugin). */
|
|
135
|
-
export function registerRoutes(app, routes, container, enrichers = [], guards = []) {
|
|
142
|
+
export function registerRoutes(app, routes, container, enrichers = [], guards = [], onError) {
|
|
136
143
|
for (const definition of routes) {
|
|
137
|
-
app.on(definition.method, definition.url, handlerFor(definition, container, enrichers, guards));
|
|
144
|
+
app.on(definition.method, definition.url, handlerFor(definition, container, enrichers, guards, onError));
|
|
138
145
|
}
|
|
139
146
|
}
|
|
140
147
|
/**
|
|
@@ -184,7 +191,7 @@ export function honoPlugin(options = {}) {
|
|
|
184
191
|
await next();
|
|
185
192
|
return undefined;
|
|
186
193
|
});
|
|
187
|
-
registerRoutes(app, routes, container, enrichers, guards);
|
|
194
|
+
registerRoutes(app, routes, container, enrichers, guards, options.onError);
|
|
188
195
|
// Neutral JSON 404 (an app's own later `notFound` call replaces it).
|
|
189
196
|
if (options.notFound !== false) {
|
|
190
197
|
app.notFound((context) => context.json(NOT_FOUND_RESPONSE, 404));
|
package/package.json
CHANGED
|
@@ -1,9 +1,13 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@basaltkit/hono",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.5.0",
|
|
4
|
+
"engines": {
|
|
5
|
+
"node": ">=22.5.0"
|
|
6
|
+
},
|
|
4
7
|
"description": "Hono adapter for Basalt: run the same typed routes, enrichers and guards on Hono (Node, Bun, Deno, edge) as on Fastify or Express.",
|
|
5
8
|
"license": "MIT",
|
|
6
9
|
"type": "module",
|
|
10
|
+
"sideEffects": false,
|
|
7
11
|
"exports": {
|
|
8
12
|
".": {
|
|
9
13
|
"types": "./dist/index.d.ts",
|
|
@@ -14,8 +18,8 @@
|
|
|
14
18
|
"dist"
|
|
15
19
|
],
|
|
16
20
|
"dependencies": {
|
|
17
|
-
"@basaltkit/core": "^1.3.
|
|
18
|
-
"@basaltkit/http": "^1.
|
|
21
|
+
"@basaltkit/core": "^1.3.1",
|
|
22
|
+
"@basaltkit/http": "^1.15.0"
|
|
19
23
|
},
|
|
20
24
|
"peerDependencies": {
|
|
21
25
|
"hono": "^4.0.0"
|