@ailura/nestjs-hono-adapter 1.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.
Files changed (118) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +411 -0
  3. package/dist/body.d.ts +35 -0
  4. package/dist/body.d.ts.map +1 -0
  5. package/dist/body.js +180 -0
  6. package/dist/body.js.map +1 -0
  7. package/dist/bridge.d.ts +64 -0
  8. package/dist/bridge.d.ts.map +1 -0
  9. package/dist/bridge.js +168 -0
  10. package/dist/bridge.js.map +1 -0
  11. package/dist/closing.d.ts +13 -0
  12. package/dist/closing.d.ts.map +1 -0
  13. package/dist/closing.js +30 -0
  14. package/dist/closing.js.map +1 -0
  15. package/dist/context.d.ts +22 -0
  16. package/dist/context.d.ts.map +1 -0
  17. package/dist/context.js +2 -0
  18. package/dist/context.js.map +1 -0
  19. package/dist/cors-middleware.d.ts +64 -0
  20. package/dist/cors-middleware.d.ts.map +1 -0
  21. package/dist/cors-middleware.js +211 -0
  22. package/dist/cors-middleware.js.map +1 -0
  23. package/dist/handler-bridge.d.ts +51 -0
  24. package/dist/handler-bridge.d.ts.map +1 -0
  25. package/dist/handler-bridge.js +122 -0
  26. package/dist/handler-bridge.js.map +1 -0
  27. package/dist/hono-lifecycle.d.ts +90 -0
  28. package/dist/hono-lifecycle.d.ts.map +1 -0
  29. package/dist/hono-lifecycle.js +169 -0
  30. package/dist/hono-lifecycle.js.map +1 -0
  31. package/dist/index.d.ts +16 -0
  32. package/dist/index.d.ts.map +1 -0
  33. package/dist/index.js +8 -0
  34. package/dist/index.js.map +1 -0
  35. package/dist/path.d.ts +3 -0
  36. package/dist/path.d.ts.map +1 -0
  37. package/dist/path.js +144 -0
  38. package/dist/path.js.map +1 -0
  39. package/dist/query.d.ts +19 -0
  40. package/dist/query.d.ts.map +1 -0
  41. package/dist/query.js +238 -0
  42. package/dist/query.js.map +1 -0
  43. package/dist/response-helpers.d.ts +16 -0
  44. package/dist/response-helpers.d.ts.map +1 -0
  45. package/dist/response-helpers.js +45 -0
  46. package/dist/response-helpers.js.map +1 -0
  47. package/dist/response-writer.d.ts +28 -0
  48. package/dist/response-writer.d.ts.map +1 -0
  49. package/dist/response-writer.js +52 -0
  50. package/dist/response-writer.js.map +1 -0
  51. package/dist/route-adapter.d.ts +53 -0
  52. package/dist/route-adapter.d.ts.map +1 -0
  53. package/dist/route-adapter.js +141 -0
  54. package/dist/route-adapter.js.map +1 -0
  55. package/dist/server-adapter.d.ts +106 -0
  56. package/dist/server-adapter.d.ts.map +1 -0
  57. package/dist/server-adapter.js +153 -0
  58. package/dist/server-adapter.js.map +1 -0
  59. package/dist/sse.d.ts +67 -0
  60. package/dist/sse.d.ts.map +1 -0
  61. package/dist/sse.js +211 -0
  62. package/dist/sse.js.map +1 -0
  63. package/dist/static-assets.d.ts +39 -0
  64. package/dist/static-assets.d.ts.map +1 -0
  65. package/dist/static-assets.js +155 -0
  66. package/dist/static-assets.js.map +1 -0
  67. package/dist/version-filter.d.ts +24 -0
  68. package/dist/version-filter.d.ts.map +1 -0
  69. package/dist/version-filter.js +107 -0
  70. package/dist/version-filter.js.map +1 -0
  71. package/dist/versioned-route.d.ts +21 -0
  72. package/dist/versioned-route.d.ts.map +1 -0
  73. package/dist/versioned-route.js +15 -0
  74. package/dist/versioned-route.js.map +1 -0
  75. package/dist/views.d.ts +42 -0
  76. package/dist/views.d.ts.map +1 -0
  77. package/dist/views.js +110 -0
  78. package/dist/views.js.map +1 -0
  79. package/dist/ws-adapter.d.ts +81 -0
  80. package/dist/ws-adapter.d.ts.map +1 -0
  81. package/dist/ws-adapter.js +214 -0
  82. package/dist/ws-adapter.js.map +1 -0
  83. package/dist/ws-client.d.ts +68 -0
  84. package/dist/ws-client.d.ts.map +1 -0
  85. package/dist/ws-client.js +135 -0
  86. package/dist/ws-client.js.map +1 -0
  87. package/dist/ws-server.d.ts +23 -0
  88. package/dist/ws-server.d.ts.map +1 -0
  89. package/dist/ws-server.js +37 -0
  90. package/dist/ws-server.js.map +1 -0
  91. package/dist/ws.d.ts +14 -0
  92. package/dist/ws.d.ts.map +1 -0
  93. package/dist/ws.js +12 -0
  94. package/dist/ws.js.map +1 -0
  95. package/package.json +99 -0
  96. package/src/body.ts +251 -0
  97. package/src/bridge.ts +308 -0
  98. package/src/closing.ts +38 -0
  99. package/src/context.ts +25 -0
  100. package/src/cors-middleware.ts +347 -0
  101. package/src/handler-bridge.ts +226 -0
  102. package/src/hono-lifecycle.ts +259 -0
  103. package/src/index.ts +27 -0
  104. package/src/path.ts +169 -0
  105. package/src/query.ts +304 -0
  106. package/src/response-helpers.ts +60 -0
  107. package/src/response-writer.ts +100 -0
  108. package/src/route-adapter.ts +261 -0
  109. package/src/server-adapter.ts +294 -0
  110. package/src/sse.ts +274 -0
  111. package/src/static-assets.ts +247 -0
  112. package/src/version-filter.ts +170 -0
  113. package/src/versioned-route.ts +30 -0
  114. package/src/views.ts +188 -0
  115. package/src/ws-adapter.ts +329 -0
  116. package/src/ws-client.ts +190 -0
  117. package/src/ws-server.ts +40 -0
  118. package/src/ws.ts +13 -0
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Ailura Code
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,411 @@
1
+ # @ailura/nestjs-hono-adapter
2
+
3
+ An HTTP adapter that runs a NestJS application on
4
+ [Hono](https://hono.dev), with no Express or Fastify underneath.
5
+
6
+ Nest has no official Hono adapter. This package implements the
7
+ Nest 11/12 `AbstractHttpAdapter` contract directly on Hono:
8
+ routes are registered on a Hono application, and Hono's Web
9
+ `Request` and `Response` are translated to and from the objects
10
+ Nest reads and writes. The two published alternatives target
11
+ Nest 11 and answer incorrectly — one returns a success status to
12
+ a handler that threw, the other writes every response twice — so
13
+ owning the adapter is the smaller cost.
14
+
15
+ ## Install
16
+
17
+ ```sh
18
+ bun add @ailura/nestjs-hono-adapter hono @hono/node-server
19
+ ```
20
+
21
+ ```sh
22
+ npm install @ailura/nestjs-hono-adapter hono @hono/node-server
23
+ ```
24
+
25
+ `@nestjs/common`, `@nestjs/core`, `hono` and `@hono/node-server`
26
+ are peer dependencies, so the application decides their
27
+ versions.
28
+
29
+ ## Use
30
+
31
+ ```ts
32
+ import { NestFactory } from '@nestjs/core';
33
+ import { ServerAdapter } from '@ailura/nestjs-hono-adapter';
34
+
35
+ import { AppModule } from './app.module.ts';
36
+
37
+ const app = await NestFactory.create(
38
+ AppModule,
39
+ new ServerAdapter({ bodyLimit: '2mb', trustProxy: true }),
40
+ );
41
+ app.enableCors({
42
+ credentials: true,
43
+ origin: ['https://app.example.com'],
44
+ });
45
+ await app.listen(3000);
46
+ ```
47
+
48
+ `getType()` answers `hono`, which is the value ecosystem
49
+ packages branch on.
50
+
51
+ ## Options
52
+
53
+ | Option | Type | Default | Effect |
54
+ | --------------- | ------------------- | ------- | ------------------------------------------------------------------------------------ |
55
+ | `bodyLimit` | `number \| string` | `1mb` | Largest request body, as bytes or as a size such as `'512kb'`; `0` removes the limit |
56
+ | `rawBody` | `boolean` | `false` | Keep the bytes that were read in `NestRequest.rawBody` |
57
+ | `secureHeaders` | `boolean \| object` | `true` | Install `hono/secure-headers`, with its defaults or with the given options |
58
+ | `trustProxy` | `boolean` | `false` | Read `x-forwarded-proto`, `x-forwarded-for` and `x-forwarded-host` |
59
+ | `views` | `object` | — | The engine a `@Render()` handler renders with, and where templates are read from |
60
+
61
+ ## Routes
62
+
63
+ Nest's path dialect is translated as the application starts:
64
+
65
+ | Nest | Hono |
66
+ | ------------------ | ------------------ |
67
+ | `/users/:id` | `/users/:id` |
68
+ | `/users/:id?` | `/users/:id?` |
69
+ | `/users{/:id}` | `/users/:id?` |
70
+ | `/users/:id(\\d+)` | `/users/:id{\\d+}` |
71
+ | `/files/*rest` | `/files/*` |
72
+
73
+ A path the router cannot read — `/users?`, a stray `}` or `(`,
74
+ or a group that holds more than one parameter — throws as the
75
+ application starts, rather than answering 404 later.
76
+
77
+ Versioning works over URI, header, media type and the custom
78
+ strategy, including versioned redirects.
79
+
80
+ ## Query strings
81
+
82
+ `@Query()` receives the shape the platform parsers produce: a
83
+ repeated name becomes a list, `tags[]` appends, `tags[0]`
84
+ indexes and `filter[name]` nests. Values are percent-decoded,
85
+ `+` reads as a space, and `__proto__`, `constructor` and
86
+ `prototype` are dropped rather than written into the tree.
87
+
88
+ ## Request bodies
89
+
90
+ The body is read when a handler reaches for it, not by
91
+ middleware:
92
+
93
+ - JSON, including the `+json` suffix types;
94
+ - URL-encoded and text bodies;
95
+ - `multipart/form-data`, through Hono's `parseBody()`; fields
96
+ land on `NestRequest.body` and files on `NestRequest.files`;
97
+ - every other content type as a `Buffer`.
98
+
99
+ A body that does not match its content type is refused with
100
+ Nest's own `BadRequestException`, and one over `bodyLimit` with
101
+ `PayloadTooLargeException`, so both travel through the exception
102
+ layer and its filters.
103
+ `app.useBodyParser('json', { limit: '1kb' })` overrides the
104
+ limit for the parser names it is called with.
105
+
106
+ With `rawBody: true` — the adapter option, or the same option on
107
+ `NestFactory.create` — `NestRequest.rawBody` holds the bytes
108
+ read, which is what a signed webhook needs. Multipart bodies
109
+ never fill it, because the platform parser consumes the stream.
110
+
111
+ ## Responses
112
+
113
+ A returned value is answered as Nest answers it: an object as
114
+ JSON, a string as text, a number as a status. `@Header()`,
115
+ `@HttpCode()`, `@Redirect()` and `StreamableFile` are all
116
+ honoured.
117
+
118
+ `@Res()` works, and so does `@Res({ passthrough: true })`:
119
+ Hono's own response helpers (`json`, `text`, `html`, `body`,
120
+ `redirect`, `notFound`, `newResponse`) are the imperative API
121
+ here, and the adapter makes sure the response a handler builds
122
+ that way is the one that is sent. A `Response` assigned straight
123
+ to `context.res` is sent as well.
124
+
125
+ ## Server-sent events
126
+
127
+ `@Sse()` is served like any other route, and its frames reach
128
+ the client as the observable emits them rather than when it
129
+ completes:
130
+
131
+ ```ts
132
+ @Sse('events')
133
+ events(): Observable<MessageEvent> {
134
+ return interval(1000).pipe(
135
+ map((n) => ({ data: { n }, id: String(n) })),
136
+ );
137
+ }
138
+ ```
139
+
140
+ The answer carries `content-type: text/event-stream` and the
141
+ frame shape is Nest's: `data`, with an object serialised as
142
+ JSON, and, when present, `type` as `event:`, `id`, `retry` and
143
+ `comment`. A handler may answer an observable or a promise of
144
+ one, and `@Header()` reaches the stream. The status is the one
145
+ Nest computed for the route: `200` by default, and `@HttpCode()`
146
+ on the versions that carry it (11.2 and later).
147
+
148
+ A stream that fails before its first frame is answered by the
149
+ exception layer like any other failure; one that fails after a
150
+ frame has been sent ends the stream with an `error` event. A
151
+ client that disconnects unsubscribes the observable, so its
152
+ teardown runs and nothing is left ticking — and, on Nest 11.2
153
+ and later, the abort signal `@SseSignal()` injects is aborted at
154
+ the same moment. `app.close()` waits for an open stream to
155
+ finish unless `forceCloseConnections` is set, the same way it
156
+ waits for any long-lived request.
157
+
158
+ ## Views
159
+
160
+ Rendering is left to the deployment: this adapter reads the
161
+ template and a function renders it, so no template engine
162
+ becomes a dependency of the package.
163
+
164
+ ```ts
165
+ import handlebars from 'handlebars';
166
+
167
+ const adapter = new ServerAdapter({
168
+ views: {
169
+ directory: 'views',
170
+ engine: (source, data) => handlebars.compile(source)(data),
171
+ },
172
+ });
173
+ ```
174
+
175
+ `engine` receives the template source and the object the handler
176
+ returned, and may answer a promise, so any engine fits. The same
177
+ two things can be named on the application, which is what Nest
178
+ declares for the purpose:
179
+
180
+ ```ts
181
+ app.setBaseViewsDir('views');
182
+ app.setViewEngine('hbs');
183
+ ```
184
+
185
+ A handler marks its route with `@Render('hello')`, and the
186
+ extension the engine named is appended, so the file looked up is
187
+ `views/hello.hbs`. A view that is missing is answered as
188
+ `NotFoundException`, and naming an engine before one was
189
+ configured throws, rather than rendering an empty page.
190
+
191
+ ## Static assets
192
+
193
+ `app.useStaticAssets(path, options)` mounts one directory, or a
194
+ list of them, on Hono's own static handler. A request that names
195
+ no file in them travels on to the routes, so an application
196
+ keeps its own not-found answer.
197
+
198
+ ```ts
199
+ app.useStaticAssets('public', {
200
+ maxAge: '7d',
201
+ prefix: '/public',
202
+ });
203
+ ```
204
+
205
+ | Option | Effect |
206
+ | ----------- | ----------------------------------------------------------------------------------------------------------------------------- |
207
+ | `prefix` | Where the files hang; without it, the root |
208
+ | `index` | File a directory request is answered with; `index.html` unless set |
209
+ | `maxAge` | Writes `cache-control: public, max-age=…`; a number is milliseconds, a string is read as `ms`, `s`, `m`, `h`, `d`, `w` or `y` |
210
+ | `immutable` | Adds `immutable` to that header, and needs `maxAge` |
211
+
212
+ `redirect` is accepted and not acted on: a directory request is
213
+ answered with its index whether or not the path ends in a slash,
214
+ so a redirect would only add a round trip. The options Hono's
215
+ handler decides for itself are refused when they are passed, so
216
+ a deployment is told at startup rather than noticing later:
217
+ `setHeaders`, `extensions`, `fallthrough: false`, `dotfiles`,
218
+ `index: false`, `etag: false`, and `immutable` without a
219
+ `maxAge`. An ETag is always written.
220
+
221
+ ## TLS, proxies and shutdown
222
+
223
+ Pass Node's TLS options to Nest and the adapter builds an
224
+ `https.Server`:
225
+
226
+ ```ts
227
+ await NestFactory.create(AppModule, new ServerAdapter(), {
228
+ httpsOptions: { key, cert },
229
+ });
230
+ ```
231
+
232
+ `getHttpServer()` returns that server, so an application can
233
+ read its address or attach a listener.
234
+
235
+ With `trustProxy: true`, `x-forwarded-proto`, `x-forwarded-for`
236
+ and `x-forwarded-host` fill `NestRequest.protocol`, `secure`,
237
+ `ip`, `ips` and `hostname`; without it they are ignored. The
238
+ socket address is always used as the peer.
239
+
240
+ `app.close()` stops accepting connections and then closes the
241
+ server. `return503OnClosing: true` answers `503` to the requests
242
+ that arrive while it is closing, and
243
+ `forceCloseConnections: true` destroys the connections the
244
+ server is still holding instead of waiting for them — both from
245
+ the same options object Nest accepts.
246
+
247
+ ## Hono underneath
248
+
249
+ `getHono()` answers the typed `Hono` application, so a project
250
+ that already knows Hono can register its own middleware and
251
+ routes before the application listens:
252
+
253
+ ```ts
254
+ const adapter = new ServerAdapter();
255
+ adapter.getHono().use('*', async (context, next) => {
256
+ context.header('x-served-by', 'hono');
257
+ await next();
258
+ });
259
+ ```
260
+
261
+ `getInstance()` answers the same application, which is the
262
+ untyped accessor Nest itself declares.
263
+
264
+ ## WebSockets
265
+
266
+ Gateways have their own Nest adapter, kept behind the `./ws`
267
+ subpath so an HTTP-only deployment never resolves
268
+ `@nestjs/websockets` or `@hono/node-ws`: both are optional
269
+ peers, and neither is installed for the routes above.
270
+
271
+ ```ts
272
+ import { HonoWsAdapter } from '@ailura/nestjs-hono-adapter/ws';
273
+
274
+ const adapter = new ServerAdapter();
275
+ const app = await NestFactory.create(AppModule, adapter);
276
+ app.useWebSocketAdapter(new HonoWsAdapter(adapter));
277
+ await app.listen(3000);
278
+ ```
279
+
280
+ A gateway is declared the way Nest declares one, and its `path`
281
+ is registered on the same Hono application the routes hang on:
282
+
283
+ ```ts
284
+ @WebSocketGateway({ path: '/ws' })
285
+ class EventsGateway {
286
+ @SubscribeMessage('ping')
287
+ ping(@MessageBody() data: unknown): unknown {
288
+ return { event: 'ping', data };
289
+ }
290
+ }
291
+ ```
292
+
293
+ A client sends `{ event, data, id? }` — or a bare string, which
294
+ reads as the event name — and receives `{ event, data }`, with
295
+ the `id` echoed when it asked for one. A handler that returns an
296
+ observable is subscribed per connection and unsubscribed when
297
+ that connection closes, so nothing leaks between clients.
298
+
299
+ What cannot be honoured is refused when the gateway starts
300
+ rather than half served: a gateway with its own `port`, because
301
+ every gateway is served on the HTTP adapter's own server, and a
302
+ `namespace`, because the path is the whole address here.
303
+ `@ConnectedSocket()` receives the socket, with the `send`,
304
+ `close`, `on` and `once` Nest's contract declares. `@Ack()` is
305
+ not wired: the acknowledgement travels back as the echoed `id`,
306
+ which is what a client without a parser expects.
307
+
308
+ ## Microservices
309
+
310
+ A microservice transport is Nest's own and never passes through
311
+ the HTTP adapter, so a hybrid application works: the routes are
312
+ served by Hono while the microservice listens on its transport,
313
+ and `app.close()` stops both.
314
+
315
+ ```ts
316
+ const app = await NestFactory.create(
317
+ AppModule,
318
+ new ServerAdapter(),
319
+ );
320
+ app.connectMicroservice({
321
+ options: { host: '127.0.0.1', port: 4000 },
322
+ transport: Transport.TCP,
323
+ });
324
+ await app.startAllMicroservices();
325
+ await app.listen(3000);
326
+ ```
327
+
328
+ ## CORS
329
+
330
+ `app.enableCors()` records the options instead of installing
331
+ Nest's Express middleware: the adapter answers with Hono's own
332
+ `cors` in front of every route.
333
+
334
+ An origin may be a string, a list, a regular expression, `'*'`,
335
+ or a callback in the shape Nest documents:
336
+
337
+ ```ts
338
+ app.enableCors({
339
+ origin: (origin, respond) => {
340
+ respond(
341
+ undefined,
342
+ allowed.has(origin ?? '') ? origin : false,
343
+ );
344
+ },
345
+ });
346
+ ```
347
+
348
+ The callback runs once per request, may answer an error to fail
349
+ the request outright, and is awaited, so an asynchronous
350
+ decision works. `preflightContinue: true` answers nothing
351
+ itself: the preflight headers are copied onto the response and
352
+ the router answers, which is what an application with its own
353
+ `OPTIONS` route wants.
354
+
355
+ Allowed origins are passed in by the application, which reads
356
+ them from its own configuration. This package reads no
357
+ environment variable, so one deployment policy is not baked into
358
+ the library.
359
+
360
+ ## Requirements
361
+
362
+ - Node 22.12 or later (22.x and 24.x tested; 26.x ready as it
363
+ enters LTS).
364
+ - Nest 11 or 12.
365
+ - Hono 4 and `@hono/node-server` 2.
366
+
367
+ ## Development
368
+
369
+ Bun installs and runs the workspace; the gates are oxlint,
370
+ oxfmt, `tsc` and `bun test`.
371
+
372
+ ```sh
373
+ bun install
374
+ bun run check
375
+ ```
376
+
377
+ `bun run check` is exactly what CI runs: lint, format,
378
+ typecheck, test, build.
379
+
380
+ The cases start a real application on an ephemeral port and talk
381
+ to it over `fetch`, so they cover the path and query dialects,
382
+ every body type, the response forms, event streams, CORS, views,
383
+ static assets, TLS selection, proxy headers and shutdown. The
384
+ TLS case runs its check in a child Node process, because Bun
385
+ gives `node:http` and `node:https` the same `Server` class and
386
+ `instanceof` cannot tell them apart there.
387
+
388
+ The adapter builds against two Nest majors. The suite runs
389
+ against the version the lockfile pins, and the CI compatibility
390
+ job installs Nest 11 and 12 on Node 22 and 24 — every
391
+ `@nestjs/*` package moves together, because a mixed install is
392
+ what a gateway or a microservice case would fail on rather than
393
+ the adapter. To run it locally, move the four packages in one
394
+ call and put them back afterwards:
395
+
396
+ ```sh
397
+ bun add --exact @nestjs/common@11.x @nestjs/core@11.x \
398
+ @nestjs/websockets@11.x @nestjs/microservices@11.x
399
+ bun run check
400
+ bun install
401
+ ```
402
+
403
+ Bun transpiles the tests from the root `tsconfig.json`, and the
404
+ fixtures are Nest controllers whose decorators are the legacy
405
+ kind, which is why that file turns `experimentalDecorators` and
406
+ `emitDecoratorMetadata` on. The library declares no decorator,
407
+ so neither flag changes what `bun run build` emits.
408
+
409
+ ## License
410
+
411
+ MIT
package/dist/body.d.ts ADDED
@@ -0,0 +1,35 @@
1
+ import { Buffer } from 'node:buffer';
2
+ import type { NestContext } from './context.ts';
3
+ /**
4
+ * A payload as the pipeline reads it: the parsed value, the
5
+ * uploaded files when the request was multipart, and the bytes
6
+ * when the application asked to keep them.
7
+ */
8
+ interface ParsedBody {
9
+ body: unknown;
10
+ files: Record<string, unknown> | undefined;
11
+ rawBody: Buffer | undefined;
12
+ }
13
+ interface BodyOptions {
14
+ readonly bodyLimit: number | undefined;
15
+ readonly rawBody: boolean;
16
+ }
17
+ /**
18
+ * Reads a size the way Nest's own parsers do, so a limit such
19
+ * as `100kb` means the same thing here as it does on Express.
20
+ */
21
+ declare function toByteLimit(limit: number | string): number;
22
+ /**
23
+ * Reads the payload for the pipeline.
24
+ *
25
+ * On Express and Fastify Nest installs parser middleware that
26
+ * fills `req.body` before the pipeline runs. Hono parses on
27
+ * demand, so the adapter reads the body here and hands it over
28
+ * in the same place. A payload that does not match its content
29
+ * type is refused with the exception Nest raises for a failed
30
+ * parse, so filters and logging see it like any other failure.
31
+ */
32
+ declare function readBody(context: NestContext, options: BodyOptions): Promise<ParsedBody>;
33
+ export { readBody, toByteLimit };
34
+ export type { ParsedBody };
35
+ //# sourceMappingURL=body.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"body.d.ts","sourceRoot":"","sources":["../src/body.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,MAAM,aAAa,CAAC;AAOrC,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,cAAc,CAAC;AA+BhD;;;;GAIG;AACH,UAAU,UAAU;IAClB,IAAI,EAAE,OAAO,CAAC;IACd,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,SAAS,CAAC;IAC3C,OAAO,EAAE,MAAM,GAAG,SAAS,CAAC;CAC7B;AAED,UAAU,WAAW;IACnB,QAAQ,CAAC,SAAS,EAAE,MAAM,GAAG,SAAS,CAAC;IACvC,QAAQ,CAAC,OAAO,EAAE,OAAO,CAAC;CAC3B;AAQD;;;GAGG;AACH,iBAAS,WAAW,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,GAAG,MAAM,CAenD;AAiJD;;;;;;;;;GASG;AACH,iBAAS,QAAQ,CACf,OAAO,EAAE,WAAW,EACpB,OAAO,EAAE,WAAW,GACnB,OAAO,CAAC,UAAU,CAAC,CAUrB;AAED,OAAO,EAAE,QAAQ,EAAE,WAAW,EAAE,CAAC;AACjC,YAAY,EAAE,UAAU,EAAE,CAAC"}
package/dist/body.js ADDED
@@ -0,0 +1,180 @@
1
+ import { Buffer } from 'node:buffer';
2
+ import { BadRequestException, PayloadTooLargeException, } from '@nestjs/common';
3
+ import { parseQuery } from './query.js';
4
+ /** Methods that never carry a payload. */
5
+ const BODYLESS_METHODS = new Set(['GET', 'HEAD']);
6
+ /** The media types that carry a JSON document. */
7
+ const JSON_TYPE = /^application\/(?:.+\+)?json$/u;
8
+ /** How a limit may be written, in the notation Nest accepts. */
9
+ const LIMIT = /^\s*(?<amount>\d+(?:\.\d+)?)\s*(?<unit>b|kb|mb|gb|tb|pb)?\s*$/iu;
10
+ /** The size a kibibyte stands for, which every unit is built on. */
11
+ const BYTES_PER_KIB = 1024;
12
+ const BYTES_PER_MIB = BYTES_PER_KIB * BYTES_PER_KIB;
13
+ const BYTES_PER_GIB = BYTES_PER_MIB * BYTES_PER_KIB;
14
+ const BYTES_PER_TIB = BYTES_PER_GIB * BYTES_PER_KIB;
15
+ const BYTES_PER_PIB = BYTES_PER_TIB * BYTES_PER_KIB;
16
+ /** The size each unit of a limit stands for. */
17
+ const UNITS = new Map([
18
+ ['b', 1],
19
+ ['gb', BYTES_PER_GIB],
20
+ ['kb', BYTES_PER_KIB],
21
+ ['mb', BYTES_PER_MIB],
22
+ ['pb', BYTES_PER_PIB],
23
+ ['tb', BYTES_PER_TIB],
24
+ ]);
25
+ /**
26
+ * Reads a size the way Nest's own parsers do, so a limit such
27
+ * as `100kb` means the same thing here as it does on Express.
28
+ */
29
+ function toByteLimit(limit) {
30
+ if (typeof limit === 'number') {
31
+ return limit;
32
+ }
33
+ const match = LIMIT.exec(limit);
34
+ if (match === null) {
35
+ throw new TypeError(`The body limit "${limit}" is not a size such as ` +
36
+ '"100kb" or "1mb".');
37
+ }
38
+ const { amount, unit } = match.groups ?? {};
39
+ const bytes = Number(amount ?? '');
40
+ const suffix = (unit ?? 'b').toLowerCase();
41
+ return Math.floor(bytes * (UNITS.get(suffix) ?? 1));
42
+ }
43
+ function mediaType(context) {
44
+ const header = context.req.header('content-type') ?? '';
45
+ return (header.split(';')[0] ?? '').trim().toLowerCase();
46
+ }
47
+ function assertWithinLength(context, limit) {
48
+ if (limit === undefined) {
49
+ return;
50
+ }
51
+ const declared = context.req.header('content-length');
52
+ if (declared === undefined) {
53
+ return;
54
+ }
55
+ const length = Number(declared);
56
+ if (Number.isFinite(length) && length > limit) {
57
+ throw new PayloadTooLargeException();
58
+ }
59
+ }
60
+ function assertWithinSize(size, limit) {
61
+ if (limit !== undefined && size > limit) {
62
+ throw new PayloadTooLargeException();
63
+ }
64
+ }
65
+ async function readBytes(context, limit) {
66
+ const bytes = new Uint8Array(await context.req.arrayBuffer());
67
+ assertWithinSize(bytes.byteLength, limit);
68
+ return bytes;
69
+ }
70
+ function textOf(bytes) {
71
+ return new TextDecoder('utf-8').decode(bytes);
72
+ }
73
+ function parseJson(bytes) {
74
+ const text = textOf(bytes).trim();
75
+ if (text === '') {
76
+ return {};
77
+ }
78
+ try {
79
+ return JSON.parse(text);
80
+ }
81
+ catch {
82
+ throw new BadRequestException('Malformed request body');
83
+ }
84
+ }
85
+ function parseBytes(bytes, type) {
86
+ if (JSON_TYPE.test(type)) {
87
+ return parseJson(bytes);
88
+ }
89
+ if (type === 'application/x-www-form-urlencoded') {
90
+ return parseQuery(textOf(bytes));
91
+ }
92
+ if (type.startsWith('text/')) {
93
+ return textOf(bytes);
94
+ }
95
+ if (bytes.byteLength === 0) {
96
+ return undefined;
97
+ }
98
+ return Buffer.from(bytes);
99
+ }
100
+ async function readForm(context) {
101
+ try {
102
+ return await context.req.parseBody();
103
+ }
104
+ catch {
105
+ throw new BadRequestException('Malformed request body');
106
+ }
107
+ }
108
+ function splitFields(form) {
109
+ const fields = {};
110
+ const files = {};
111
+ for (const [name, value] of Object.entries(form)) {
112
+ if (typeof value === 'string') {
113
+ fields[name] = value;
114
+ }
115
+ else {
116
+ files[name] = value;
117
+ }
118
+ }
119
+ return { fields, files };
120
+ }
121
+ /**
122
+ * Reads a multipart request into the two halves a controller
123
+ * expects: the text fields in the body, the uploads in `files`.
124
+ * The bytes are never buffered, so `rawBody` stays empty.
125
+ */
126
+ async function readMultipart(context) {
127
+ const form = await readForm(context);
128
+ const parts = splitFields(form);
129
+ const parsed = {
130
+ body: parts.fields,
131
+ files: undefined,
132
+ rawBody: undefined,
133
+ };
134
+ if (Object.keys(parts.files).length > 0) {
135
+ parsed.files = parts.files;
136
+ }
137
+ return parsed;
138
+ }
139
+ function emptyBody() {
140
+ return {
141
+ body: undefined,
142
+ files: undefined,
143
+ rawBody: undefined,
144
+ };
145
+ }
146
+ async function readPayload(context, type, options) {
147
+ const bytes = await readBytes(context, options.bodyLimit);
148
+ const parsed = {
149
+ body: parseBytes(bytes, type),
150
+ files: undefined,
151
+ rawBody: undefined,
152
+ };
153
+ if (options.rawBody) {
154
+ parsed.rawBody = Buffer.from(bytes);
155
+ }
156
+ return parsed;
157
+ }
158
+ /**
159
+ * Reads the payload for the pipeline.
160
+ *
161
+ * On Express and Fastify Nest installs parser middleware that
162
+ * fills `req.body` before the pipeline runs. Hono parses on
163
+ * demand, so the adapter reads the body here and hands it over
164
+ * in the same place. A payload that does not match its content
165
+ * type is refused with the exception Nest raises for a failed
166
+ * parse, so filters and logging see it like any other failure.
167
+ */
168
+ function readBody(context, options) {
169
+ if (BODYLESS_METHODS.has(context.req.method)) {
170
+ return Promise.resolve(emptyBody());
171
+ }
172
+ const type = mediaType(context);
173
+ assertWithinLength(context, options.bodyLimit);
174
+ if (type.startsWith('multipart/form-data')) {
175
+ return readMultipart(context);
176
+ }
177
+ return readPayload(context, type, options);
178
+ }
179
+ export { readBody, toByteLimit };
180
+ //# sourceMappingURL=body.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"body.js","sourceRoot":"","sources":["../src/body.ts"],"names":[],"mappings":"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"}