@asenajs/hono-adapter 1.6.0 → 2.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 CHANGED
@@ -1,5 +1,8 @@
1
1
  # Asena Hono Adapter
2
2
 
3
+ [![Version](https://img.shields.io/badge/version-2.0.0-blue.svg)](https://github.com/AsenaJs/hono-adapter)
4
+ [![Bun Version](https://img.shields.io/badge/Bun-1.3.12%2B-blueviolet)](https://bun.sh)
5
+
3
6
  HTTP and WebSocket adapter implementation based on Hono web framework for Asena.js.
4
7
 
5
8
  ## Features
@@ -112,7 +115,11 @@ describe("YourController", () => {
112
115
  let baseUrl;
113
116
 
114
117
  beforeEach(async () => {
115
- const port = Math.floor(Math.random() * 55000) + 10000;
118
+ // 10000-31999: below the kernel's ephemeral floor (net.ipv4.ip_local_port_range,
119
+ // 32768-60999). A server port drawn from that range collides with the outbound
120
+ // sockets the suite itself holds open - TIME_WAIT included - and Bun.serve then
121
+ // fails with EADDRINUSE, randomly, in whichever test happened to draw it.
122
+ const port = 10000 + Math.floor(Math.random() * 22000);
116
123
  const [adapter, logger] = createHonoAdapter(new DefaultLogger());
117
124
  const app = new AsenaServer(adapter).logger(logger).port(port);
118
125
 
package/dist/index.d.ts CHANGED
@@ -3,4 +3,5 @@ export * from './lib/HonoWebsocketAdapter';
3
3
  export * from './lib/types';
4
4
  export * from './lib/defaults';
5
5
  export * from './lib/utils/createHonoAdapter';
6
+ export * from './lib/errors';
6
7
  export { HTTPException } from 'hono/http-exception';
package/dist/index.js CHANGED
@@ -3,6 +3,7 @@ export * from './lib/HonoWebsocketAdapter';
3
3
  export * from './lib/types';
4
4
  export * from './lib/defaults';
5
5
  export * from './lib/utils/createHonoAdapter';
6
+ export * from './lib/errors';
6
7
  // Re-export Hono's HTTPException for user convenience in middlewares
7
8
  export { HTTPException } from 'hono/http-exception';
8
9
  //# 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;AAE9C,qEAAqE;AACrE,OAAO,EAAE,aAAa,EAAE,MAAM,qBAAqB,CAAC"}
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,qEAAqE;AACrE,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,8 +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;
19
+ /**
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.
23
+ */
24
+ private hasErrorHandler;
25
+ /** Whether the application registered an onNotFound hook (see start()) */
26
+ private hasNotFoundHandler;
18
27
  /**
19
28
  * Normalizes a route path for Hono compatibility.
20
29
  * Strips trailing slash (except root '/') because Hono's strict:false
@@ -94,31 +103,76 @@ export declare class HonoAdapter extends AsenaAdapter<HonoAdapterContext, Valida
94
103
  registerWebsocketRoute(params: WebsocketRouteParams<HonoAdapterContext>): Promise<void> | void;
95
104
  start(options?: AsenaStartOptions): Promise<Server<WebSocketData<any>>>;
96
105
  /**
97
- * Registers global error handler with enhanced error handling capabilities
106
+ * Registers the handler for requests that match no route.
107
+ *
108
+ * Separate from {@link onError}: an unmatched route is a routing outcome, not a thrown error,
109
+ * so the application's error handler never sees one and never has to discriminate.
110
+ *
111
+ * @param notFoundHandler - Handler that produces the 404 response
112
+ */
113
+ onNotFound(notFoundHandler: HonoNotFoundHandler): void;
114
+ /**
115
+ * The 404 both adapters answer when no `onNotFound` is registered, and the log line that goes
116
+ * with it.
117
+ *
118
+ * Hono's own default is `text/plain`, so the same application used to produce a different
119
+ * body depending on which adapter it ran under - the specific complaint that started this
120
+ * work. Registered unconditionally in `start()` so the default holds even for an app with
121
+ * no config at all.
122
+ *
123
+ * `info`, not `warn`: a scanner walking /wp-admin, /.env and /phpmyadmin must not be able to
124
+ * fill the warning stream. Not `debug` either - a 404 nobody can see is how a mistyped route
125
+ * survives to production. Only reached when the framework is the one answering, so an
126
+ * application that shaped its own 404 gets no line from here.
127
+ */
128
+ private defaultNotFoundResponse;
129
+ /**
130
+ * Registers the application's global error handler.
98
131
  *
99
- * Supports Hono's HTTPException for proper status code handling:
100
- * - HTTPException instances are passed to user handler for custom handling
101
- * - If user handler returns a response, it's used
102
- * - Otherwise, HTTPException's default response is returned
103
- * - Other errors follow normal error handling flow
132
+ * Every error the application throws is offered to it first, `HTTPException` included, so an
133
+ * application can reshape its own 401/403/429 envelopes rather than only the 500s. Returning
134
+ * nothing means "not mine, use the default" - the framework then answers, and logs.
104
135
  *
105
- * @param errorHandler - Custom error handler function
136
+ * @param errorHandler - Handler that produces the response for a thrown error
106
137
  *
107
138
  * @example
108
139
  * ```typescript
109
140
  * adapter.onError((error, context) => {
110
- * // HTTPException handling (optional custom behavior)
111
- * if (error instanceof HTTPException) {
112
- * // Add custom logging or return custom response
113
- * return context.send({ custom: 'response' }, error.status);
141
+ * if (isHttpException(error)) {
142
+ * return context.send({ error: error.message }, error.status);
114
143
  * }
115
144
  *
116
- * // Other errors
117
145
  * return context.send({ error: 'Internal error' }, 500);
118
146
  * });
119
147
  * ```
120
148
  */
121
149
  onError(errorHandler: HonoErrorHandler): void;
150
+ /**
151
+ * The response the framework itself answers with, and the log line that goes with it.
152
+ *
153
+ * Reached when the application declared no `onError`, when its handler declined by returning
154
+ * nothing, or when the handler threw. All three mean the framework is producing the response,
155
+ * so it is the framework that has to record what happened - otherwise an `onError` that
156
+ * returns `undefined` swallows a 500 with no trace anywhere.
157
+ *
158
+ * The body is the same one `@asenajs/ergenecore` answers. It used to be `text/plain` for an
159
+ * application with no config and `{error, message, timestamp}` for one whose handler declined,
160
+ * so the same failure produced three different envelopes across the two adapters.
161
+ */
162
+ private defaultErrorResponse;
163
+ /**
164
+ * Logs an error that is about to be handed to the application's error handler.
165
+ *
166
+ * The level is derived from the status the client will actually see: a 4xx is the
167
+ * caller's mistake and the auth/validation layer doing its job, so it must not flood the
168
+ * ERROR stream with stack traces - an unauthenticated request would otherwise be an
169
+ * attacker-controlled log amplifier. Only 5xx carries a stack, because that is the class
170
+ * of failure nobody predicted.
171
+ *
172
+ * `ServerLogger.debug` is optional, so fall back to `info` when an implementation does
173
+ * not provide it.
174
+ */
175
+ private logHandledError;
122
176
  serveOptions(options: () => Promise<AsenaServeOptions> | AsenaServeOptions): Promise<void>;
123
177
  setPort(port: number): void;
124
178
  private prepareMiddlewares;
@@ -135,6 +189,21 @@ export declare class HonoAdapter extends AsenaAdapter<HonoAdapterContext, Valida
135
189
  * @returns Array of Hono middleware handlers for validation
136
190
  */
137
191
  private prepareValidator;
192
+ /**
193
+ * @description Answer a failed validation.
194
+ *
195
+ * Throws a `ValidationError` so the failure travels the same path as every other error and
196
+ * reaches `ConfigService.onError`, which is what the documentation has always described.
197
+ *
198
+ * The adapter used to answer its own 400 here when no error handler was configured, which made
199
+ * a validation failure the one 4xx that reached neither `onError` nor the log. That envelope
200
+ * did not disappear with the branch - it moved onto `ValidationError.getResponse()`, which
201
+ * `defaultErrorResponse` falls back to.
202
+ * @param {ZodError} error - The Zod failure
203
+ * @param {string} target - Which part of the request failed
204
+ * @returns {never} Always throws
205
+ */
206
+ private handleValidationFailure;
138
207
  /**
139
208
  * Registers global middlewares at the top level of the Hono app
140
209
  *
@@ -170,27 +239,16 @@ export declare class HonoAdapter extends AsenaAdapter<HonoAdapterContext, Valida
170
239
  */
171
240
  private getGlobalMiddlewaresForPath;
172
241
  /**
173
- * Extracts base path from a route path
174
- * Examples:
175
- * - "/api/users/:id" -> "/api/users"
176
- * - "/users" -> "/users"
177
- * - "/" -> "/"
178
- */
179
- private extractBasePath;
180
- /**
181
- * Groups routes by their base path
182
- * Returns a map of base path -> routes with that base path
183
- */
184
- private groupRoutesByBasePath;
185
- /**
186
- * Finds common middlewares across all routes in a group
187
- * Returns middlewares that appear in ALL routes
188
- */
189
- private extractCommonMiddlewares;
190
- /**
191
- * Registers routes to Hono app with optimization
192
- * Groups routes by base path and applies common middlewares at group level
193
- * Also registers WebSocket routes
242
+ * Sorts the queued routes and registers each one with its own middlewares.
243
+ *
244
+ * There used to be a "common middleware" optimisation here: routes sharing a base path were
245
+ * mounted on a sub-app and the middlewares they had in common were hoisted to `group.use('*')`.
246
+ * It compared middlewares with `mw.constructor.name`, but by this point a middleware is the
247
+ * plain `{ handle, override }` literal PrepareMiddlewareService builds - so the name was
248
+ * `"Object"` for every one of them and the comparison was always true. Every route in a group
249
+ * got the *first* route's middlewares and had its own filtered away, which silently swapped
250
+ * authorisation guards between sibling routes. Measured at 100 routes it bought nothing, so
251
+ * it is gone rather than repaired.
194
252
  */
195
253
  private optimizeAndRegisterRoutes;
196
254
  /**
@@ -202,10 +260,6 @@ export declare class HonoAdapter extends AsenaAdapter<HonoAdapterContext, Valida
202
260
  * Converts HTTP GET route to WebSocket upgrade endpoint
203
261
  */
204
262
  private registerWebsocketRouteDirect;
205
- /**
206
- * Registers a group of routes with common middlewares at base path level
207
- */
208
- private registerRouteGroup;
209
263
  /**
210
264
  * Registers a single route directly without grouping
211
265
  */