@asenajs/hono-adapter 1.7.0 → 3.0.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 +23 -3
- package/dist/index.d.ts +15 -0
- package/dist/index.js +15 -1
- package/dist/index.js.map +1 -1
- package/dist/lib/HonoAdapter.d.ts +107 -48
- package/dist/lib/HonoAdapter.js +265 -201
- package/dist/lib/HonoAdapter.js.map +1 -1
- package/dist/lib/HonoContextWrapper.js +9 -2
- package/dist/lib/HonoContextWrapper.js.map +1 -1
- package/dist/lib/HonoWebsocketAdapter.d.ts +35 -3
- package/dist/lib/HonoWebsocketAdapter.js +63 -8
- package/dist/lib/HonoWebsocketAdapter.js.map +1 -1
- package/dist/lib/defaults/ConfigService.d.ts +35 -0
- package/dist/lib/defaults/ConfigService.js +22 -0
- package/dist/lib/defaults/ConfigService.js.map +1 -1
- package/dist/lib/errors.d.ts +89 -8
- package/dist/lib/errors.js +120 -6
- package/dist/lib/errors.js.map +1 -1
- package/dist/lib/middlewares/RateLimiterMiddleware.d.ts +13 -2
- package/dist/lib/middlewares/RateLimiterMiddleware.js +31 -2
- package/dist/lib/middlewares/RateLimiterMiddleware.js.map +1 -1
- package/dist/lib/types/HonoAdapterOptions.d.ts +14 -0
- package/dist/lib/types/HonoHandler.d.ts +2 -1
- package/dist/lib/utils/createHonoAdapter.js +1 -4
- package/dist/lib/utils/createHonoAdapter.js.map +1 -1
- package/package.json +15 -8
package/README.md
CHANGED
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
# Asena Hono Adapter
|
|
2
2
|
|
|
3
|
+
[](https://github.com/AsenaJs/hono-adapter)
|
|
4
|
+
[](https://bun.sh)
|
|
5
|
+
|
|
3
6
|
HTTP and WebSocket adapter implementation based on Hono web framework for Asena.js.
|
|
4
7
|
|
|
5
8
|
## Features
|
|
@@ -15,15 +18,28 @@ HTTP and WebSocket adapter implementation based on Hono web framework for Asena.
|
|
|
15
18
|
## Requirements
|
|
16
19
|
|
|
17
20
|
- [Bun](https://bun.sh) v1.3.12 or higher
|
|
21
|
+
- [@asenajs/asena](https://github.com/AsenaJs/Asena) v0.10.0 or higher (peer dependency)
|
|
22
|
+
- [Hono](https://hono.dev) v4.12.9 or higher (peer dependency)
|
|
23
|
+
- [Zod](https://zod.dev) v4.3.6 or higher (peer dependency)
|
|
18
24
|
- TypeScript v5.8.2 or above
|
|
19
25
|
|
|
20
26
|
## Installation
|
|
21
27
|
|
|
22
|
-
1. Install with NPM package manager:
|
|
23
28
|
```bash
|
|
24
|
-
bun add @asenajs/hono-adapter
|
|
29
|
+
bun add @asenajs/hono-adapter hono zod
|
|
25
30
|
```
|
|
26
31
|
|
|
32
|
+
`hono` and `zod` are **peer dependencies** - this adapter offers the wrapper, your project owns the
|
|
33
|
+
libraries it wraps. That is what keeps your code and the adapter on a single copy of `hono`; two
|
|
34
|
+
copies mean two `HTTPException` classes, and a deliberate 403 thrown from the wrong one is answered
|
|
35
|
+
500.
|
|
36
|
+
|
|
37
|
+
| Package manager | Command |
|
|
38
|
+
|:--|:--|
|
|
39
|
+
| bun, npm 7+, pnpm 8+ | `bun add hono zod` — peers auto-install, but declare them anyway so they survive a clean install |
|
|
40
|
+
| yarn 1 | `yarn add hono zod` — **required**, yarn 1 does not install peers for you |
|
|
41
|
+
|
|
42
|
+
|
|
27
43
|
## Usage
|
|
28
44
|
|
|
29
45
|
### Server Setup (index.ts)
|
|
@@ -112,7 +128,11 @@ describe("YourController", () => {
|
|
|
112
128
|
let baseUrl;
|
|
113
129
|
|
|
114
130
|
beforeEach(async () => {
|
|
115
|
-
|
|
131
|
+
// 10000-31999: below the kernel's ephemeral floor (net.ipv4.ip_local_port_range,
|
|
132
|
+
// 32768-60999). A server port drawn from that range collides with the outbound
|
|
133
|
+
// sockets the suite itself holds open - TIME_WAIT included - and Bun.serve then
|
|
134
|
+
// fails with EADDRINUSE, randomly, in whichever test happened to draw it.
|
|
135
|
+
const port = 10000 + Math.floor(Math.random() * 22000);
|
|
116
136
|
const [adapter, logger] = createHonoAdapter(new DefaultLogger());
|
|
117
137
|
const app = new AsenaServer(adapter).logger(logger).port(port);
|
|
118
138
|
|
package/dist/index.d.ts
CHANGED
|
@@ -4,4 +4,19 @@ export * from './lib/types';
|
|
|
4
4
|
export * from './lib/defaults';
|
|
5
5
|
export * from './lib/utils/createHonoAdapter';
|
|
6
6
|
export * from './lib/errors';
|
|
7
|
+
/**
|
|
8
|
+
* Hono's own `HTTPException`, re-exported so applications and middlewares stay on the copy of
|
|
9
|
+
* `hono` this adapter resolved - importing it from `hono/http-exception` directly is what lets a
|
|
10
|
+
* project end up with two `HTTPException` classes that `instanceof` disagrees about.
|
|
11
|
+
*
|
|
12
|
+
* This is *not* the class to throw for an ordinary HTTP error. That is `HttpException` from
|
|
13
|
+
* `@asenajs/asena/adapter`, which is the same class on every adapter, so the same `throw`
|
|
14
|
+
* compiles and behaves identically on ergenecore and here. It is deliberately not re-exported
|
|
15
|
+
* from this package: two throwable, branded classes whose names differ by the case of two letters
|
|
16
|
+
* is a trap for autocomplete, and only one of them is the portable one.
|
|
17
|
+
*
|
|
18
|
+
* Both are recognised by `isHttpException()` and both are answered from their own status, so
|
|
19
|
+
* anything hono's ecosystem raises - `hono/basic-auth`, `hono/bearer-auth`, `hono/jwt`, hono's
|
|
20
|
+
* validator - keeps working exactly as before.
|
|
21
|
+
*/
|
|
7
22
|
export { HTTPException } from 'hono/http-exception';
|
package/dist/index.js
CHANGED
|
@@ -4,6 +4,20 @@ export * from './lib/types';
|
|
|
4
4
|
export * from './lib/defaults';
|
|
5
5
|
export * from './lib/utils/createHonoAdapter';
|
|
6
6
|
export * from './lib/errors';
|
|
7
|
-
|
|
7
|
+
/**
|
|
8
|
+
* Hono's own `HTTPException`, re-exported so applications and middlewares stay on the copy of
|
|
9
|
+
* `hono` this adapter resolved - importing it from `hono/http-exception` directly is what lets a
|
|
10
|
+
* project end up with two `HTTPException` classes that `instanceof` disagrees about.
|
|
11
|
+
*
|
|
12
|
+
* This is *not* the class to throw for an ordinary HTTP error. That is `HttpException` from
|
|
13
|
+
* `@asenajs/asena/adapter`, which is the same class on every adapter, so the same `throw`
|
|
14
|
+
* compiles and behaves identically on ergenecore and here. It is deliberately not re-exported
|
|
15
|
+
* from this package: two throwable, branded classes whose names differ by the case of two letters
|
|
16
|
+
* is a trap for autocomplete, and only one of them is the portable one.
|
|
17
|
+
*
|
|
18
|
+
* Both are recognised by `isHttpException()` and both are answered from their own status, so
|
|
19
|
+
* anything hono's ecosystem raises - `hono/basic-auth`, `hono/bearer-auth`, `hono/jwt`, hono's
|
|
20
|
+
* validator - keeps working exactly as before.
|
|
21
|
+
*/
|
|
8
22
|
export { HTTPException } from 'hono/http-exception';
|
|
9
23
|
//# sourceMappingURL=index.js.map
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../index.ts"],"names":[],"mappings":"AAAA,cAAc,mBAAmB,CAAC;AAClC,cAAc,4BAA4B,CAAC;AAC3C,cAAc,aAAa,CAAC;AAC5B,cAAc,gBAAgB,CAAC;AAC/B,cAAc,+BAA+B,CAAC;AAC9C,cAAc,cAAc,CAAC;AAE7B
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../index.ts"],"names":[],"mappings":"AAAA,cAAc,mBAAmB,CAAC;AAClC,cAAc,4BAA4B,CAAC;AAC3C,cAAc,aAAa,CAAC;AAC5B,cAAc,gBAAgB,CAAC;AAC/B,cAAc,+BAA+B,CAAC;AAC9C,cAAc,cAAc,CAAC;AAE7B;;;;;;;;;;;;;;GAcG;AACH,OAAO,EAAE,aAAa,EAAE,MAAM,qBAAqB,CAAC"}
|
|
@@ -3,7 +3,7 @@ import type { Server } from 'bun';
|
|
|
3
3
|
import type { WebsocketRouteParams } from '@asenajs/asena/adapter';
|
|
4
4
|
import { AsenaAdapter, type AsenaServeOptions, type AsenaStartOptions, type BaseMiddleware, type RouteParams } from '@asenajs/asena/adapter';
|
|
5
5
|
import type { GlobalMiddlewareRouteConfig } from '@asenajs/asena/server/config';
|
|
6
|
-
import type { HonoAdapterOptions, HonoErrorHandler, StaticServeExtras } from './types';
|
|
6
|
+
import type { HonoAdapterOptions, HonoErrorHandler, HonoNotFoundHandler, StaticServeExtras } from './types';
|
|
7
7
|
import { type ServerLogger } from '@asenajs/asena/logger';
|
|
8
8
|
import type { ValidationSchema } from './defaults';
|
|
9
9
|
import type { Context as HonoAdapterContext } from './defaults/Context';
|
|
@@ -13,14 +13,17 @@ export declare class HonoAdapter extends AsenaAdapter<HonoAdapterContext, Valida
|
|
|
13
13
|
name: string;
|
|
14
14
|
app: Hono;
|
|
15
15
|
private readonly _strict;
|
|
16
|
+
private readonly logErrors;
|
|
16
17
|
private server;
|
|
17
18
|
private options;
|
|
18
19
|
/**
|
|
19
|
-
* Whether the application registered an error handler.
|
|
20
|
-
*
|
|
21
|
-
*
|
|
20
|
+
* Whether the *application* registered an error handler. Read only by `start()`, to decide
|
|
21
|
+
* whether the framework's own default needs registering - Hono keeps a single `onError`, so
|
|
22
|
+
* registering both would replace the application's.
|
|
22
23
|
*/
|
|
23
24
|
private hasErrorHandler;
|
|
25
|
+
/** Whether the application registered an onNotFound hook (see start()) */
|
|
26
|
+
private hasNotFoundHandler;
|
|
24
27
|
/**
|
|
25
28
|
* Normalizes a route path for Hono compatibility.
|
|
26
29
|
* Strips trailing slash (except root '/') because Hono's strict:false
|
|
@@ -28,6 +31,21 @@ export declare class HonoAdapter extends AsenaAdapter<HonoAdapterContext, Valida
|
|
|
28
31
|
* Registered routes must match this normalized form.
|
|
29
32
|
*/
|
|
30
33
|
private normalizePath;
|
|
34
|
+
/**
|
|
35
|
+
* Stops the HTTP surface and releases everything the adapter owns.
|
|
36
|
+
*
|
|
37
|
+
* The socket goes down first and the WebSocket adapter is torn down after it, for the same
|
|
38
|
+
* reason the core stops the adapter before running `@OnStop`: a transport has to outlive the
|
|
39
|
+
* things that publish through it. Destroying it while a message handler is still running would
|
|
40
|
+
* only turn a shutdown into an error in the log.
|
|
41
|
+
*
|
|
42
|
+
* `finally`, so the two failure directions are both covered - a `server.stop()` that rejects
|
|
43
|
+
* still releases the WebSocket resources, and a WebSocket teardown that fails cannot leave the
|
|
44
|
+
* port bound. A port that survives `stop()` is how a test suite starts failing with
|
|
45
|
+
* EADDRINUSE and how a rolling deploy keeps taking traffic it can no longer serve.
|
|
46
|
+
*
|
|
47
|
+
* @param closeActiveConnections - Whether to drop in-flight connections instead of draining them
|
|
48
|
+
*/
|
|
31
49
|
stop(closeActiveConnections?: boolean): Promise<void>;
|
|
32
50
|
private routeQueue;
|
|
33
51
|
/**
|
|
@@ -100,31 +118,86 @@ export declare class HonoAdapter extends AsenaAdapter<HonoAdapterContext, Valida
|
|
|
100
118
|
registerWebsocketRoute(params: WebsocketRouteParams<HonoAdapterContext>): Promise<void> | void;
|
|
101
119
|
start(options?: AsenaStartOptions): Promise<Server<WebSocketData<any>>>;
|
|
102
120
|
/**
|
|
103
|
-
* Registers
|
|
121
|
+
* Registers the handler for requests that match no route.
|
|
104
122
|
*
|
|
105
|
-
*
|
|
106
|
-
*
|
|
107
|
-
* - If user handler returns a response, it's used
|
|
108
|
-
* - Otherwise, HTTPException's default response is returned
|
|
109
|
-
* - Other errors follow normal error handling flow
|
|
123
|
+
* Separate from {@link onError}: an unmatched route is a routing outcome, not a thrown error,
|
|
124
|
+
* so the application's error handler never sees one and never has to discriminate.
|
|
110
125
|
*
|
|
111
|
-
* @param
|
|
126
|
+
* @param notFoundHandler - Handler that produces the 404 response
|
|
127
|
+
*/
|
|
128
|
+
onNotFound(notFoundHandler: HonoNotFoundHandler): void;
|
|
129
|
+
/**
|
|
130
|
+
* The 404 both adapters answer when no `onNotFound` is registered, and the log line that goes
|
|
131
|
+
* with it.
|
|
132
|
+
*
|
|
133
|
+
* Hono's own default is `text/plain`, so the same application used to produce a different
|
|
134
|
+
* body depending on which adapter it ran under - the specific complaint that started this
|
|
135
|
+
* work. Registered unconditionally in `start()` so the default holds even for an app with
|
|
136
|
+
* no config at all.
|
|
137
|
+
*
|
|
138
|
+
* `info`, not `warn`: a scanner walking /wp-admin, /.env and /phpmyadmin must not be able to
|
|
139
|
+
* fill the warning stream. Not `debug` either - a 404 nobody can see is how a mistyped route
|
|
140
|
+
* survives to production. Only reached when the framework is the one answering, so an
|
|
141
|
+
* application that shaped its own 404 gets no line from here.
|
|
142
|
+
*/
|
|
143
|
+
private defaultNotFoundResponse;
|
|
144
|
+
/**
|
|
145
|
+
* Registers the application's global error handler.
|
|
146
|
+
*
|
|
147
|
+
* Every error the application throws is offered to it first, `HTTPException` included, so an
|
|
148
|
+
* application can reshape its own 401/403/429 envelopes rather than only the 500s. Returning
|
|
149
|
+
* nothing means "not mine, use the default" - the framework then answers, and logs.
|
|
150
|
+
*
|
|
151
|
+
* @param errorHandler - Handler that produces the response for a thrown error
|
|
112
152
|
*
|
|
113
153
|
* @example
|
|
114
154
|
* ```typescript
|
|
115
155
|
* adapter.onError((error, context) => {
|
|
116
|
-
*
|
|
117
|
-
*
|
|
118
|
-
* // Add custom logging or return custom response
|
|
119
|
-
* return context.send({ custom: 'response' }, error.status);
|
|
156
|
+
* if (isHttpException(error)) {
|
|
157
|
+
* return context.send({ error: error.message }, error.status);
|
|
120
158
|
* }
|
|
121
159
|
*
|
|
122
|
-
* // Other errors
|
|
123
160
|
* return context.send({ error: 'Internal error' }, 500);
|
|
124
161
|
* });
|
|
125
162
|
* ```
|
|
126
163
|
*/
|
|
127
164
|
onError(errorHandler: HonoErrorHandler): void;
|
|
165
|
+
/**
|
|
166
|
+
* The response the framework itself answers with, and the log line that goes with it.
|
|
167
|
+
*
|
|
168
|
+
* Reached when the application declared no `onError`, when its handler declined by returning
|
|
169
|
+
* nothing, or when the handler threw. All three mean the framework is producing the response,
|
|
170
|
+
* so it is the framework that has to record what happened - otherwise an `onError` that
|
|
171
|
+
* returns `undefined` swallows a 500 with no trace anywhere.
|
|
172
|
+
*
|
|
173
|
+
* The body is the same one `@asenajs/ergenecore` answers. It used to be `text/plain` for an
|
|
174
|
+
* application with no config and `{error, message, timestamp}` for one whose handler declined,
|
|
175
|
+
* so the same failure produced three different envelopes across the two adapters.
|
|
176
|
+
*/
|
|
177
|
+
private defaultErrorResponse;
|
|
178
|
+
/**
|
|
179
|
+
* The status the caller will be answered with, for anything thrown.
|
|
180
|
+
*
|
|
181
|
+
* Shared by `defaultErrorResponse` and `logHandledError` so the response and the log line cannot
|
|
182
|
+
* disagree. They used to: the response was chosen by `instanceof HTTPException` while the log
|
|
183
|
+
* level was chosen by duck-typing a numeric `status` off the error. A plain `Error` carrying a
|
|
184
|
+
* stray `.status = 401` was therefore answered 500 and logged at debug as a 4xx, and a branded
|
|
185
|
+
* exception from a second copy was answered 500 and logged as whatever status it carried.
|
|
186
|
+
*/
|
|
187
|
+
private resolveErrorStatus;
|
|
188
|
+
/**
|
|
189
|
+
* Logs an error that is about to be handed to the application's error handler.
|
|
190
|
+
*
|
|
191
|
+
* The level is derived from the status the client will actually see - via the same
|
|
192
|
+
* `resolveErrorStatus` the response uses, so the two cannot drift. A 4xx is the caller's
|
|
193
|
+
* mistake and the auth/validation layer doing its job, so it must not flood the ERROR stream
|
|
194
|
+
* with stack traces - an unauthenticated request would otherwise be an attacker-controlled log
|
|
195
|
+
* amplifier. Only 5xx carries a stack, because that is the class of failure nobody predicted.
|
|
196
|
+
*
|
|
197
|
+
* `ServerLogger.debug` is optional, so fall back to `info` when an implementation does
|
|
198
|
+
* not provide it.
|
|
199
|
+
*/
|
|
200
|
+
private logHandledError;
|
|
128
201
|
serveOptions(options: () => Promise<AsenaServeOptions> | AsenaServeOptions): Promise<void>;
|
|
129
202
|
setPort(port: number): void;
|
|
130
203
|
private prepareMiddlewares;
|
|
@@ -144,15 +217,16 @@ export declare class HonoAdapter extends AsenaAdapter<HonoAdapterContext, Valida
|
|
|
144
217
|
/**
|
|
145
218
|
* @description Answer a failed validation.
|
|
146
219
|
*
|
|
147
|
-
* Throws a `ValidationError` so the failure travels the same path as every other
|
|
148
|
-
*
|
|
149
|
-
*
|
|
150
|
-
*
|
|
151
|
-
*
|
|
220
|
+
* Throws a `ValidationError` so the failure travels the same path as every other error and
|
|
221
|
+
* reaches `ConfigService.onError`, which is what the documentation has always described.
|
|
222
|
+
*
|
|
223
|
+
* The adapter used to answer its own 400 here when no error handler was configured, which made
|
|
224
|
+
* a validation failure the one 4xx that reached neither `onError` nor the log. That envelope
|
|
225
|
+
* did not disappear with the branch - it moved onto `ValidationError.getResponse()`, which
|
|
226
|
+
* `defaultErrorResponse` falls back to.
|
|
152
227
|
* @param {ZodError} error - The Zod failure
|
|
153
228
|
* @param {string} target - Which part of the request failed
|
|
154
|
-
* @
|
|
155
|
-
* @returns {Response} The fallback response, when no error handler is configured
|
|
229
|
+
* @returns {never} Always throws
|
|
156
230
|
*/
|
|
157
231
|
private handleValidationFailure;
|
|
158
232
|
/**
|
|
@@ -190,27 +264,16 @@ export declare class HonoAdapter extends AsenaAdapter<HonoAdapterContext, Valida
|
|
|
190
264
|
*/
|
|
191
265
|
private getGlobalMiddlewaresForPath;
|
|
192
266
|
/**
|
|
193
|
-
*
|
|
194
|
-
*
|
|
195
|
-
*
|
|
196
|
-
* -
|
|
197
|
-
*
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
*
|
|
202
|
-
*
|
|
203
|
-
*/
|
|
204
|
-
private groupRoutesByBasePath;
|
|
205
|
-
/**
|
|
206
|
-
* Finds common middlewares across all routes in a group
|
|
207
|
-
* Returns middlewares that appear in ALL routes
|
|
208
|
-
*/
|
|
209
|
-
private extractCommonMiddlewares;
|
|
210
|
-
/**
|
|
211
|
-
* Registers routes to Hono app with optimization
|
|
212
|
-
* Groups routes by base path and applies common middlewares at group level
|
|
213
|
-
* Also registers WebSocket routes
|
|
267
|
+
* Sorts the queued routes and registers each one with its own middlewares.
|
|
268
|
+
*
|
|
269
|
+
* There used to be a "common middleware" optimisation here: routes sharing a base path were
|
|
270
|
+
* mounted on a sub-app and the middlewares they had in common were hoisted to `group.use('*')`.
|
|
271
|
+
* It compared middlewares with `mw.constructor.name`, but by this point a middleware is the
|
|
272
|
+
* plain `{ handle, override }` literal PrepareMiddlewareService builds - so the name was
|
|
273
|
+
* `"Object"` for every one of them and the comparison was always true. Every route in a group
|
|
274
|
+
* got the *first* route's middlewares and had its own filtered away, which silently swapped
|
|
275
|
+
* authorisation guards between sibling routes. Measured at 100 routes it bought nothing, so
|
|
276
|
+
* it is gone rather than repaired.
|
|
214
277
|
*/
|
|
215
278
|
private optimizeAndRegisterRoutes;
|
|
216
279
|
/**
|
|
@@ -222,10 +285,6 @@ export declare class HonoAdapter extends AsenaAdapter<HonoAdapterContext, Valida
|
|
|
222
285
|
* Converts HTTP GET route to WebSocket upgrade endpoint
|
|
223
286
|
*/
|
|
224
287
|
private registerWebsocketRouteDirect;
|
|
225
|
-
/**
|
|
226
|
-
* Registers a group of routes with common middlewares at base path level
|
|
227
|
-
*/
|
|
228
|
-
private registerRouteGroup;
|
|
229
288
|
/**
|
|
230
289
|
* Registers a single route directly without grouping
|
|
231
290
|
*/
|