@zudojs/http 1.1.0 → 1.2.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 (79) hide show
  1. package/README.md +53 -3
  2. package/dist/httpAdapter/http.adapter.d.ts +34 -3
  3. package/dist/httpAdapter/http.adapter.js +22 -6
  4. package/dist/httpAdapter/http.adapters.js +4 -34
  5. package/dist/httpAdapter/httpAdapter.errorResponse.d.ts +2 -2
  6. package/dist/httpAdapter/httpAdapter.errorResponse.js +26 -9
  7. package/dist/httpAdapter/httpAdapter.logger.d.ts +19 -0
  8. package/dist/httpAdapter/httpAdapter.logger.js +21 -0
  9. package/dist/httpAdapter/node/httpNode.adapter.d.ts +12 -1
  10. package/dist/httpAdapter/node/httpNode.adapter.js +28 -14
  11. package/dist/httpAdapter/node/httpNode.request.d.ts +8 -7
  12. package/dist/httpAdapter/node/httpNode.request.js +17 -27
  13. package/dist/httpAdapter/node/httpNode.server.d.ts +6 -0
  14. package/dist/httpAdapter/node/httpNode.server.js +6 -0
  15. package/dist/httpAdapter/node/httpNode.type.d.ts +2 -1
  16. package/dist/httpCookies/http.cookies.d.ts +29 -2
  17. package/dist/httpCookies/http.cookies.js +44 -24
  18. package/dist/httpCookies/httpCookies.defaults.d.ts +40 -0
  19. package/dist/httpCookies/httpCookies.defaults.js +42 -0
  20. package/dist/httpCookies/index.d.ts +1 -0
  21. package/dist/httpCookies/index.js +1 -0
  22. package/dist/httpMiddleware/builtin/conditional/httpMiddleware.conditional.d.ts +22 -1
  23. package/dist/httpMiddleware/builtin/conditional/httpMiddleware.conditional.js +19 -4
  24. package/dist/httpMiddleware/builtin/conditional/index.d.ts +1 -0
  25. package/dist/httpMiddleware/builtin/cors/httpMiddleware.cors.d.ts +8 -0
  26. package/dist/httpMiddleware/builtin/cors/httpMiddleware.cors.js +25 -22
  27. package/dist/httpMiddleware/builtin/helpers/httpMiddleware.accessor.js +2 -6
  28. package/dist/httpMiddleware/builtin/helpers/httpMiddleware.media.d.ts +33 -0
  29. package/dist/httpMiddleware/builtin/helpers/httpMiddleware.media.js +59 -0
  30. package/dist/httpMiddleware/builtin/helpers/index.d.ts +1 -0
  31. package/dist/httpMiddleware/builtin/helpers/index.js +1 -0
  32. package/dist/httpMiddleware/builtin/image/httpMiddleware.image.d.ts +13 -0
  33. package/dist/httpMiddleware/builtin/image/httpMiddleware.image.js +31 -53
  34. package/dist/httpMiddleware/builtin/index.d.ts +1 -0
  35. package/dist/httpMiddleware/builtin/index.js +1 -0
  36. package/dist/httpMiddleware/builtin/rateLimit/httpMiddleware.rateLimit.d.ts +47 -0
  37. package/dist/httpMiddleware/builtin/rateLimit/httpMiddleware.rateLimit.js +61 -0
  38. package/dist/httpMiddleware/builtin/rateLimit/index.d.ts +7 -0
  39. package/dist/httpMiddleware/builtin/rateLimit/index.js +7 -0
  40. package/dist/httpMiddleware/builtin/static/httpMiddleware.static.js +18 -4
  41. package/dist/httpMiddleware/builtin/video/httpMiddleware.video.d.ts +19 -1
  42. package/dist/httpMiddleware/builtin/video/httpMiddleware.video.js +38 -59
  43. package/dist/httpMiddleware/builtin/video/httpMiddleware.video.options.d.ts +12 -0
  44. package/dist/httpMiddleware/builtin/video/httpMiddleware.video.options.js +31 -0
  45. package/dist/httpMiddleware/httpMiddleware.error.d.ts +5 -31
  46. package/dist/httpMiddleware/httpMiddleware.error.js +5 -53
  47. package/dist/httpProxy/http.proxy.d.ts +7 -0
  48. package/dist/httpProxy/http.proxy.js +9 -0
  49. package/dist/httpProxy/httpProxy.pathGuard.d.ts +23 -0
  50. package/dist/httpProxy/httpProxy.pathGuard.js +43 -0
  51. package/dist/httpProxy/index.d.ts +1 -0
  52. package/dist/httpProxy/index.js +1 -0
  53. package/dist/httpRequest/httpRequest.context.d.ts +7 -0
  54. package/dist/httpRequest/httpRequest.context.js +15 -16
  55. package/dist/httpRequest/index.d.ts +1 -0
  56. package/dist/httpRequest/index.js +1 -0
  57. package/dist/httpRequest/target/httpRequest.target.d.ts +45 -0
  58. package/dist/httpRequest/target/httpRequest.target.js +93 -0
  59. package/dist/httpRequest/target/index.d.ts +9 -0
  60. package/dist/httpRequest/target/index.js +9 -0
  61. package/dist/httpResponse/httpResponse.helper.js +2 -1
  62. package/dist/httpRouter/core/util/httpRoute.util.d.ts +14 -0
  63. package/dist/httpRouter/core/util/httpRoute.util.js +18 -14
  64. package/dist/httpSecurity/httpSecurity.config.d.ts +6 -0
  65. package/dist/httpSecurity/httpSecurity.config.js +1 -0
  66. package/dist/httpSecurity/httpSecurity.guard.d.ts +7 -9
  67. package/dist/httpSecurity/httpSecurity.guard.js +7 -14
  68. package/dist/httpSecurity/httpSecurity.nodeGuard.d.ts +48 -0
  69. package/dist/httpSecurity/httpSecurity.nodeGuard.js +32 -0
  70. package/dist/httpSecurity/httpSecurity.validator.js +4 -2
  71. package/dist/httpSecurity/index.d.ts +2 -0
  72. package/dist/httpSecurity/index.js +1 -0
  73. package/dist/httpServer/core/httpServer.core.d.ts +11 -0
  74. package/dist/httpServer/core/httpServer.core.js +30 -4
  75. package/dist/httpTrustProxy/httpTrustProxy.compilation.d.ts +5 -2
  76. package/dist/httpTrustProxy/httpTrustProxy.compilation.js +19 -2
  77. package/dist/httpTrustProxy/httpTrustProxy.type.d.ts +3 -1
  78. package/dist/index.js +0 -1
  79. package/package.json +5 -5
package/README.md CHANGED
@@ -2,6 +2,12 @@
2
2
 
3
3
  HTTP primitives, request handling, routing, middleware, and server infrastructure for Zudojs applications.
4
4
 
5
+ <!-- zudo-docs:start -->
6
+
7
+ **Documentation:** [zudojs.oyinlola.site/docs/packages-http](https://zudojs.oyinlola.site/docs/packages-http) · **For AI agents:** [Markdown version](https://zudojs.oyinlola.site/docs/packages-http.md), [llms.txt](https://zudojs.oyinlola.site/llms.txt)
8
+
9
+ <!-- zudo-docs:end -->
10
+
5
11
  ## Installation
6
12
 
7
13
  ```bash
@@ -28,9 +34,53 @@ await server.start();
28
34
  ```
29
35
 
30
36
  A handler receives an `HttpRequestContext` and may return an
31
- `HttpResponseContext`, a plain `{ status, headers, body }` object, or any JSON
32
- value (which is sent as `application/json`). Throwing an `HttpError` created by
33
- `notFound()`, `unauthorized()` and friends answers with that error's status.
37
+ `HttpResponseContext` or any JSON value. **Every value that is not an
38
+ `HttpResponseContext` is data**: a plain object such as `{ status: "ok" }` is
39
+ sent as `application/json`, whatever its keys. To choose the status, headers
40
+ or raw body, return `createResponseContext({ status, headers, body })`.
41
+ Throwing an `HttpError` created by `notFound()`, `unauthorized()` and friends
42
+ answers with that error's status. When errors are wrapped, the **outermost**
43
+ error that carries a status wins; only the middleware pipeline's own
44
+ wrappers are looked through, so `new HttpError(502, "Bad Gateway", { cause })`
45
+ answers 502 and never exposes the cause.
46
+
47
+ ## Secure defaults
48
+
49
+ - **Request guard.** The Node adapter runs `guardRequest` on every request
50
+ before the body is read and answers `400` when it refuses: Host,
51
+ `X-Request-Id` format, header count and size, URL and query length, and
52
+ `Transfer-Encoding`/`Content-Length` smuggling. Tune it with
53
+ `createNodeHttpAdapter({ security: { allowedHosts: ["api.example.com"] } })`
54
+ or turn it off with `security: false`. HTTP/1.0 requests may omit Host, and
55
+ oversized bodies still get `413` from `maxBodySize`.
56
+ - **Canonical request targets.** Request targets with `.`/`..` segments
57
+ (including `%2e%2e`), backslashes or a non-origin form are refused with
58
+ `400`. `request.path`, the router and `createPathMiddleware` all read the
59
+ path with one parser, and `//host/admin` is a path, never an authority.
60
+ - **Path-scoped middleware** matches the way the router does: case-insensitive
61
+ and ignoring repeated or trailing slashes (`{ caseSensitive: true }` to opt
62
+ out).
63
+ - **Query strings** are parsed once: a repeated name is an array in both
64
+ `request.query`/`getQuery()` and the router's `ctx.query`.
65
+ - **Cookies** default to `Path=/; HttpOnly; Secure; SameSite=Lax`; override
66
+ any attribute explicitly. Signed cookies are bound to their name: read them
67
+ with `parseSignedCookie(value, secret, name)`.
68
+ - **`trustProxy`** accepts a hop count (`trustProxy: 1` trusts one proxy).
69
+ - **CORS** origin matching is `@zudojs/security`'s `isOriginAllowed`, and a
70
+ wildcard origin with `credentials: true` throws when the middleware is
71
+ created. **Rate limiting**: `createRateLimitMiddleware({ max, windowMs })`
72
+ wraps `@zudojs/security`'s `createRateLimiter`. Requests with no usable
73
+ client address share one bucket (`UNKNOWN_CLIENT_RATE_LIMIT_IP`,
74
+ `0.0.0.0`): they are limited together, never unlimited and never a 500.
75
+ - Signed-cookie signatures are compared with `@zudojs/crypto`'s constant-time
76
+ `timingSafeEqualString`.
77
+ - Contexts built by the stock adapters log through a `@zudojs/logger` console
78
+ logger named `http`, which redacts secret metadata fields (`authorization`,
79
+ `password`, `apiKey`, …).
80
+ - `HttpRequestGuardError`, `HttpMiddlewareError` and
81
+ `HttpMiddlewarePipelineError` are the `@zudojs/errors` classes, re-exported.
82
+ - `HttpServer.stop()` gives in-flight requests the full
83
+ `gracefulShutdownTimeout`.
34
84
 
35
85
  ## Features
36
86
 
@@ -64,7 +64,12 @@ export interface HttpAdapter {
64
64
  */
65
65
  setErrorHandler?(handler: HttpErrorHandler): void;
66
66
  start?(): void | Promise<void>;
67
- stop?(): void | Promise<void>;
67
+ /**
68
+ * Stops the adapter. `options.graceMs` bounds how long in-flight requests
69
+ * may run before their connections are closed; `HttpServer.stop()` passes
70
+ * its `gracefulShutdownTimeout` here.
71
+ */
72
+ stop?(options?: HttpAdapterStopOptions): void | Promise<void>;
68
73
  }
69
74
  import { HttpAdapterError } from "@zudojs/errors";
70
75
  export { HttpAdapterError };
@@ -96,7 +101,7 @@ export declare abstract class BaseHttpAdapter implements HttpAdapter {
96
101
  protected handleError(error: unknown, request: HttpRequestContext, input: unknown, response: HttpResponseContext): Promise<void>;
97
102
  protected write(input: unknown, context: HttpResponseContext): Promise<void>;
98
103
  start(): Promise<void>;
99
- stop(): Promise<void>;
104
+ stop(_options?: HttpAdapterStopOptions): Promise<void>;
100
105
  get isStarted(): boolean;
101
106
  }
102
107
  export interface GenericAdapterOptions extends HttpAdapterOptions {
@@ -128,9 +133,35 @@ export declare class HttpAdapterRegistry {
128
133
  clear(): void;
129
134
  }
130
135
  export declare function startAdapter(adapter: HttpAdapter): Promise<void>;
131
- export declare function stopAdapter(adapter: HttpAdapter): Promise<void>;
136
+ /**
137
+ * Options passed to {@link HttpAdapter.stop}.
138
+ */
139
+ export interface HttpAdapterStopOptions {
140
+ /** Milliseconds in-flight requests may run before connections are closed. */
141
+ readonly graceMs?: number;
142
+ }
143
+ export declare function stopAdapter(adapter: HttpAdapter, options?: HttpAdapterStopOptions): Promise<void>;
132
144
  export declare function isHttpAdapter(value: unknown): value is HttpAdapter;
145
+ /**
146
+ * Turns a handler's return value into a response.
147
+ *
148
+ * Only an `HttpResponseContext` is treated as a response; `undefined`/`null`
149
+ * is an empty 200. **Every other value, including any plain object, is
150
+ * data** and is sent as `application/json`. To answer with a status, headers
151
+ * or a raw body, return `createResponseContext({ status, headers, body })`.
152
+ *
153
+ * A plain object used to count as a response init whenever it had any of
154
+ * eight common keys, so `{ status: "ok" }` became a 500 and a row with a
155
+ * `body` or `metadata` column was silently emptied or truncated.
156
+ */
133
157
  export declare function normalizeHandlerResult(result: HttpHandlerResult): HttpResponseContext;
158
+ /**
159
+ * Whether a value has the shape of a response init.
160
+ *
161
+ * @deprecated No longer used to interpret handler results: a plain object
162
+ * returned from a handler is always sent as JSON (see
163
+ * {@link normalizeHandlerResult}). Kept for API compatibility.
164
+ */
134
165
  export declare function isResponseContextInit(value: unknown): value is ResponseContextInit;
135
166
  export declare function mergeResponseContext(target: HttpResponseContext, source: HttpResponseContext): HttpResponseContext;
136
167
  //# sourceMappingURL=http.adapter.d.ts.map
@@ -110,7 +110,7 @@ export class BaseHttpAdapter {
110
110
  }
111
111
  this.started = true;
112
112
  }
113
- async stop() {
113
+ async stop(_options) {
114
114
  if (!this.started) {
115
115
  return;
116
116
  }
@@ -222,9 +222,9 @@ export async function startAdapter(adapter) {
222
222
  await adapter.start();
223
223
  }
224
224
  }
225
- export async function stopAdapter(adapter) {
225
+ export async function stopAdapter(adapter, options) {
226
226
  if (adapter.stop) {
227
- await adapter.stop();
227
+ await adapter.stop(options);
228
228
  }
229
229
  }
230
230
  /* -------------------------------------------------------------------------- */
@@ -244,6 +244,18 @@ export function isHttpAdapter(value) {
244
244
  /* -------------------------------------------------------------------------- */
245
245
  /* Result Normalization */
246
246
  /* -------------------------------------------------------------------------- */
247
+ /**
248
+ * Turns a handler's return value into a response.
249
+ *
250
+ * Only an `HttpResponseContext` is treated as a response; `undefined`/`null`
251
+ * is an empty 200. **Every other value, including any plain object, is
252
+ * data** and is sent as `application/json`. To answer with a status, headers
253
+ * or a raw body, return `createResponseContext({ status, headers, body })`.
254
+ *
255
+ * A plain object used to count as a response init whenever it had any of
256
+ * eight common keys, so `{ status: "ok" }` became a 500 and a row with a
257
+ * `body` or `metadata` column was silently emptied or truncated.
258
+ */
247
259
  export function normalizeHandlerResult(result) {
248
260
  if (result instanceof HttpResponseContext) {
249
261
  return result;
@@ -251,11 +263,15 @@ export function normalizeHandlerResult(result) {
251
263
  if (result === undefined || result === null) {
252
264
  return createResponseContext();
253
265
  }
254
- if (isResponseContextInit(result)) {
255
- return createResponseContext(result);
256
- }
257
266
  return createResponseContext().json(result);
258
267
  }
268
+ /**
269
+ * Whether a value has the shape of a response init.
270
+ *
271
+ * @deprecated No longer used to interpret handler results: a plain object
272
+ * returned from a handler is always sent as JSON (see
273
+ * {@link normalizeHandlerResult}). Kept for API compatibility.
274
+ */
259
275
  export function isResponseContextInit(value) {
260
276
  if (value === null || typeof value !== "object") {
261
277
  return false;
@@ -1,6 +1,7 @@
1
1
  import { createHTTPContext } from "../httpContext/http.context.js";
2
2
  import { createHTTPRequest } from "../httpRequest/http.request.js";
3
3
  import { createHTTPResponse } from "../httpResponse/http.response.js";
4
+ import { createDefaultContextLogger } from "./httpAdapter.logger.js";
4
5
  /* -------------------------------------------------------------------------- */
5
6
  /* Node Adapter */
6
7
  /* -------------------------------------------------------------------------- */
@@ -18,7 +19,7 @@ export class NodeHTTPAdapter {
18
19
  response,
19
20
  state: options.state ?? {},
20
21
  signal: options.signal,
21
- logger: createFallbackLogger(),
22
+ logger: createDefaultContextLogger(),
22
23
  });
23
24
  }
24
25
  }
@@ -45,7 +46,7 @@ export class NodeContextAdapter {
45
46
  response,
46
47
  state: options.state ?? {},
47
48
  signal: options.signal,
48
- logger: createFallbackLogger(),
49
+ logger: createDefaultContextLogger(),
49
50
  });
50
51
  }
51
52
  }
@@ -87,7 +88,7 @@ export function adaptNodeContext(request, response, options = {}) {
87
88
  response: adaptNodeResponse(response),
88
89
  state: options.state ?? {},
89
90
  signal: options.signal,
90
- logger: createFallbackLogger(),
91
+ logger: createDefaultContextLogger(),
91
92
  });
92
93
  }
93
94
  /* -------------------------------------------------------------------------- */
@@ -103,35 +104,4 @@ export function isHTTPAdapter(value) {
103
104
  typeof candidate.createResponse === "function" &&
104
105
  typeof candidate.createContext === "function");
105
106
  }
106
- /* -------------------------------------------------------------------------- */
107
- /* Fallback Logger */
108
- /* -------------------------------------------------------------------------- */
109
- function createFallbackLogger() {
110
- return {
111
- info(message, metadata) {
112
- if (metadata !== undefined) {
113
- console.info(message, metadata);
114
- }
115
- else {
116
- console.info(message);
117
- }
118
- },
119
- warn(message, metadata) {
120
- if (metadata !== undefined) {
121
- console.warn(message, metadata);
122
- }
123
- else {
124
- console.warn(message);
125
- }
126
- },
127
- error(message, metadata) {
128
- if (metadata !== undefined) {
129
- console.error(message, metadata);
130
- }
131
- else {
132
- console.error(message);
133
- }
134
- },
135
- };
136
- }
137
107
  //# sourceMappingURL=http.adapters.js.map
@@ -23,8 +23,8 @@ export interface ResolvedErrorResponse {
23
23
  /**
24
24
  * Builds the default response for an unhandled error.
25
25
  *
26
- * - An error (or any error in its `cause` / `errors` chain) with a 4xx/5xx
27
- * `statusCode` is answered with that status.
26
+ * - The outermost error with a 4xx/5xx `statusCode` (looking through the
27
+ * pipeline's own wrappers only) is answered with that status.
28
28
  * - Its `message` and `code` are included only when the error opts in with
29
29
  * `expose: true` (the `@zudojs/errors` default for 4xx); otherwise the
30
30
  * body carries the status text alone, so a 5xx never leaks internals.
@@ -17,6 +17,7 @@
17
17
  */
18
18
  import { getStatusText } from "../httpResponse/core/httpResponse.statusText.js";
19
19
  import { isValidHeaderFieldValue } from "../httpHeaders/security/index.js";
20
+ import { HttpMiddlewareError, HttpMiddlewarePipelineError, } from "../httpMiddleware/httpMiddleware.error.js";
20
21
  /* -------------------------------------------------------------------------- */
21
22
  /* Constants */
22
23
  /* -------------------------------------------------------------------------- */
@@ -30,11 +31,25 @@ const MAX_UNWRAP_DEPTH = 8;
30
31
  /* Resolution */
31
32
  /* -------------------------------------------------------------------------- */
32
33
  /**
33
- * Locates the innermost error that carries an HTTP status.
34
+ * Whether `error` is one of the pipeline's own wrappers. These are added by
35
+ * the framework on the way out and always report 500, so they are looked
36
+ * through; nothing else is.
37
+ */
38
+ function isFrameworkWrapper(error) {
39
+ return (error instanceof HttpMiddlewareError ||
40
+ error instanceof HttpMiddlewarePipelineError);
41
+ }
42
+ /**
43
+ * Locates the error whose status answers the request.
34
44
  *
35
- * The innermost one wins because outer errors are wrappers added on the way
36
- * out (middleware, pipeline, adapter); the error the application actually
37
- * threw sits at the bottom of the chain.
45
+ * The **outermost** error that carries a status wins. Only the framework's
46
+ * own wrappers (`HttpMiddlewareError`, `HttpMiddlewarePipelineError`) are
47
+ * unwrapped. An application that throws
48
+ * `new HttpError(502, "Bad Gateway", { cause: upstream401 })` is mapping an
49
+ * upstream failure on purpose; taking the innermost status sent the client
50
+ * the upstream's 401, its message and its `WWW-Authenticate` header.
51
+ * An error that is neither a wrapper nor carries a status ends the search
52
+ * (generic 500), so a `cause` the application attached is never exposed.
38
53
  */
39
54
  function findStatusError(error, depth, seen) {
40
55
  if (error === null || typeof error !== "object" || depth > MAX_UNWRAP_DEPTH) {
@@ -45,6 +60,11 @@ function findStatusError(error, depth, seen) {
45
60
  }
46
61
  seen.add(error);
47
62
  const candidate = error;
63
+ if (!isFrameworkWrapper(error)) {
64
+ return isHttpStatus(candidate.statusCode)
65
+ ? candidate
66
+ : undefined;
67
+ }
48
68
  const nested = [];
49
69
  if (Array.isArray(candidate.errors)) {
50
70
  nested.push(...candidate.errors);
@@ -58,9 +78,6 @@ function findStatusError(error, depth, seen) {
58
78
  return found;
59
79
  }
60
80
  }
61
- if (isHttpStatus(candidate.statusCode)) {
62
- return candidate;
63
- }
64
81
  return undefined;
65
82
  }
66
83
  function isHttpStatus(value) {
@@ -72,8 +89,8 @@ function isHttpStatus(value) {
72
89
  /**
73
90
  * Builds the default response for an unhandled error.
74
91
  *
75
- * - An error (or any error in its `cause` / `errors` chain) with a 4xx/5xx
76
- * `statusCode` is answered with that status.
92
+ * - The outermost error with a 4xx/5xx `statusCode` (looking through the
93
+ * pipeline's own wrappers only) is answered with that status.
77
94
  * - Its `message` and `code` are included only when the error opts in with
78
95
  * `expose: true` (the `@zudojs/errors` default for 4xx); otherwise the
79
96
  * body carries the status text alone, so a 5xx never leaks internals.
@@ -0,0 +1,19 @@
1
+ /**
2
+ * Default logger for contexts built by the stock adapters.
3
+ *
4
+ * @module httpAdapter/httpAdapter.logger
5
+ */
6
+ import { type Logger } from "@zudojs/logger";
7
+ /**
8
+ * Creates the logger a stock adapter gives each `HTTPContext` when the
9
+ * caller supplies none: a `@zudojs/logger` console logger named `http`.
10
+ *
11
+ * It goes through the logger's secret-field redaction, so metadata such as
12
+ * `authorization`, `password` or `apiKey` passed to `ctx.log()` is printed
13
+ * as `[REDACTED]`. The earlier fallback called `console.*` with the raw
14
+ * metadata object. One logger per context (construction costs a few
15
+ * microseconds), so `setLevel()` / `disable()` / `close()` on one context's
16
+ * logger cannot silence another's.
17
+ */
18
+ export declare function createDefaultContextLogger(): Logger;
19
+ //# sourceMappingURL=httpAdapter.logger.d.ts.map
@@ -0,0 +1,21 @@
1
+ /**
2
+ * Default logger for contexts built by the stock adapters.
3
+ *
4
+ * @module httpAdapter/httpAdapter.logger
5
+ */
6
+ import { createDefaultLogger } from "@zudojs/logger";
7
+ /**
8
+ * Creates the logger a stock adapter gives each `HTTPContext` when the
9
+ * caller supplies none: a `@zudojs/logger` console logger named `http`.
10
+ *
11
+ * It goes through the logger's secret-field redaction, so metadata such as
12
+ * `authorization`, `password` or `apiKey` passed to `ctx.log()` is printed
13
+ * as `[REDACTED]`. The earlier fallback called `console.*` with the raw
14
+ * metadata object. One logger per context (construction costs a few
15
+ * microseconds), so `setLevel()` / `disable()` / `close()` on one context's
16
+ * logger cannot silence another's.
17
+ */
18
+ export function createDefaultContextLogger() {
19
+ return createDefaultLogger("http");
20
+ }
21
+ //# sourceMappingURL=httpAdapter.logger.js.map
@@ -10,6 +10,7 @@ import { Server } from "node:http";
10
10
  import { HttpRequestContext } from "../../httpRequest/httpRequest.context.js";
11
11
  import { HttpResponseContext } from "../../httpResponse/httpResponse.context.js";
12
12
  import { BaseHttpAdapter } from "../http.adapter.js";
13
+ import type { HttpAdapterStopOptions } from "../http.adapter.js";
13
14
  import type { HttpResponseWriter } from "../../httpResponse/httpResponse.writer.js";
14
15
  import type { NodeAdapterOptions, NodeServerAddress } from "./httpNode.type.js";
15
16
  export declare class NodeHttpAdapter extends BaseHttpAdapter {
@@ -25,6 +26,7 @@ export declare class NodeHttpAdapter extends BaseHttpAdapter {
25
26
  private readonly connectionsCheckingInterval;
26
27
  private readonly shutdownGraceMs;
27
28
  private readonly events;
29
+ private readonly requestGuard;
28
30
  private server;
29
31
  private ownsServer;
30
32
  /**
@@ -50,6 +52,10 @@ export declare class NodeHttpAdapter extends BaseHttpAdapter {
50
52
  */
51
53
  private attachNodeBody;
52
54
  private executeNodeHandler;
55
+ /**
56
+ * Plain objects are data and are sent as JSON; see
57
+ * `normalizeHandlerResult`.
58
+ */
53
59
  private normalizeResult;
54
60
  private handleNodeError;
55
61
  private writeBadRequest;
@@ -65,7 +71,12 @@ export declare class NodeHttpAdapter extends BaseHttpAdapter {
65
71
  private dispatchNodeRequest;
66
72
  private emitAdapterError;
67
73
  start(): Promise<void>;
68
- stop(): Promise<void>;
74
+ /**
75
+ * Closes the server. The grace period is `options.graceMs` (the server's
76
+ * `gracefulShutdownTimeout`), capped by an explicit `shutdownGraceMs`;
77
+ * with neither it is 10 s.
78
+ */
79
+ stop(options?: HttpAdapterStopOptions): Promise<void>;
69
80
  }
70
81
  export declare function createNodeHttpAdapter(options?: NodeAdapterOptions): NodeHttpAdapter;
71
82
  //# sourceMappingURL=httpNode.adapter.d.ts.map
@@ -9,13 +9,15 @@
9
9
  import { IncomingMessage, Server, ServerResponse, createServer, } from "node:http";
10
10
  import { HttpRequestContext } from "../../httpRequest/httpRequest.context.js";
11
11
  import { HttpResponseContext, createResponseContext, } from "../../httpResponse/httpResponse.context.js";
12
- import { BaseHttpAdapter } from "../http.adapter.js";
12
+ import { BaseHttpAdapter, normalizeHandlerResult } from "../http.adapter.js";
13
13
  import { writeResponse } from "../../httpResponse/httpResponse.writer.js";
14
14
  import { DEFAULT_HOST, DEFAULT_PORT, DEFAULT_MAX_BODY_SIZE, NODE_DEFAULT_HEADERS_TIMEOUT, NODE_DEFAULT_REQUEST_TIMEOUT, NODE_DEFAULT_KEEP_ALIVE_TIMEOUT, validatePort, validateMaxBodySize, } from "./httpNode.type.js";
15
15
  import { NodeResponseWriter } from "./httpNode.response.js";
16
+ import { compileTrustProxy } from "../../httpTrustProxy/httpTrustProxy.compilation.js";
17
+ import { createNodeRequestGuard } from "../../httpSecurity/httpSecurity.nodeGuard.js";
16
18
  import { createNodeRequestContext } from "./httpNode.request.js";
17
19
  import { resolveErrorResponse } from "../httpAdapter.errorResponse.js";
18
- import { isIncomingMessage, isServerResponse, isNodeRequestResponsePair, configureServer, listen, closeServer, readNodeRequestBody, NodeRequestBodyTooLargeError, isResponseContextLike, } from "./httpNode.server.js";
20
+ import { isIncomingMessage, isServerResponse, isNodeRequestResponsePair, configureServer, listen, closeServer, readNodeRequestBody, NodeRequestBodyTooLargeError, } from "./httpNode.server.js";
19
21
  /* -------------------------------------------------------------------------- */
20
22
  /* Node HTTP Adapter */
21
23
  /* -------------------------------------------------------------------------- */
@@ -32,6 +34,7 @@ export class NodeHttpAdapter extends BaseHttpAdapter {
32
34
  connectionsCheckingInterval;
33
35
  shutdownGraceMs;
34
36
  events;
37
+ requestGuard;
35
38
  server;
36
39
  ownsServer = false;
37
40
  /**
@@ -73,7 +76,11 @@ export class NodeHttpAdapter extends BaseHttpAdapter {
73
76
  Math.min(30_000, this.headersTimeout);
74
77
  this.shutdownGraceMs = options.shutdownGraceMs;
75
78
  this.trustProxy = options.trustProxy;
79
+ if (options.trustProxy !== undefined) {
80
+ compileTrustProxy(options.trustProxy);
81
+ }
76
82
  this.events = options.events ?? {};
83
+ this.requestGuard = createNodeRequestGuard(options.security);
77
84
  this.server = options.server;
78
85
  this.ownsServer = !options.server;
79
86
  }
@@ -132,6 +139,10 @@ export class NodeHttpAdapter extends BaseHttpAdapter {
132
139
  }
133
140
  const request = input.request;
134
141
  const response = input.response;
142
+ if (this.requestGuard && !this.requestGuard(request).allowed) {
143
+ await this.writeBadRequest(response);
144
+ return;
145
+ }
135
146
  let context;
136
147
  try {
137
148
  context = this.createRequest(request);
@@ -183,17 +194,12 @@ export class NodeHttpAdapter extends BaseHttpAdapter {
183
194
  }
184
195
  return this.handler(request);
185
196
  }
197
+ /**
198
+ * Plain objects are data and are sent as JSON; see
199
+ * `normalizeHandlerResult`.
200
+ */
186
201
  normalizeResult(result) {
187
- if (result instanceof HttpResponseContext) {
188
- return result;
189
- }
190
- if (result === undefined || result === null) {
191
- return createResponseContext();
192
- }
193
- if (isResponseContextLike(result)) {
194
- return createResponseContext(result);
195
- }
196
- return createResponseContext().json(result);
202
+ return normalizeHandlerResult(result);
197
203
  }
198
204
  async handleNodeError(error, request, response) {
199
205
  if (response.headersSent) {
@@ -326,12 +332,20 @@ export class NodeHttpAdapter extends BaseHttpAdapter {
326
332
  }
327
333
  await super.start();
328
334
  }
329
- async stop() {
335
+ /**
336
+ * Closes the server. The grace period is `options.graceMs` (the server's
337
+ * `gracefulShutdownTimeout`), capped by an explicit `shutdownGraceMs`;
338
+ * with neither it is 10 s.
339
+ */
340
+ async stop(options = {}) {
330
341
  if (!this.server || !this.server.listening) {
331
342
  await super.stop();
332
343
  return;
333
344
  }
334
- await closeServer(this.server, { graceMs: this.shutdownGraceMs });
345
+ const graceMs = options.graceMs === undefined
346
+ ? this.shutdownGraceMs
347
+ : Math.min(options.graceMs, this.shutdownGraceMs ?? Infinity);
348
+ await closeServer(this.server, { graceMs });
335
349
  if (this.clientErrorListener) {
336
350
  this.server.off("clientError", this.clientErrorListener);
337
351
  this.clientErrorListener = undefined;
@@ -21,13 +21,14 @@ export declare function getNodeRemoteAddress(request: IncomingMessage, options?:
21
21
  /**
22
22
  * Parses the request-target's query string into a flat record.
23
23
  *
24
- * Every value is attacker-controlled. `decodeURIComponent` throws on a
25
- * malformed sequence such as `%E0`, and this ran before the adapter's
26
- * try/catch, so one such request tore the connection down instead of being
27
- * answered. Decoding is delegated to the query module's non-throwing
28
- * decoder, which also gives `+` its form-encoding meaning. A pair is split on
29
- * its **first** `=` so `a=b=c` keeps the value `b=c`.
24
+ * Every value is attacker-controlled, so decoding never throws (a malformed
25
+ * `%E0` is kept raw) and `+` has its form-encoding meaning. This uses the same
26
+ * parser as the router's `ctx.query` (`parseQueryString`), so both agree: a
27
+ * repeated name is an array (`?role=user&role=admin` gives
28
+ * `["user", "admin"]`), the record has a `null` prototype, and
29
+ * `__proto__` / `constructor` / `prototype` are dropped. Previously the last
30
+ * value silently won here while the router returned the array.
30
31
  */
31
- export declare function parseNodeQuery(request: IncomingMessage): Readonly<Record<string, string>>;
32
+ export declare function parseNodeQuery(request: IncomingMessage): Readonly<Record<string, string | readonly string[]>>;
32
33
  export declare function createNodeRequestContext(request: IncomingMessage, options?: NodeRequestOptions): HttpRequestContext;
33
34
  //# sourceMappingURL=httpNode.request.d.ts.map
@@ -6,7 +6,8 @@
6
6
  import { HttpRequestContext, createRequestContext, } from "../../httpRequest/httpRequest.context.js";
7
7
  import { getClientIp, isTrustedProxy, } from "../../httpTrustProxy/httpTrustProxy.core.js";
8
8
  import { removePort, extractPort } from "./httpNode.server.js";
9
- import { decodeQueryComponent } from "../../httpQuery/http.query.js";
9
+ import { parseQueryString } from "../../httpQuery/http.query.js";
10
+ import { findRequestTargetViolation } from "../../httpRequest/target/httpRequest.target.js";
10
11
  /* -------------------------------------------------------------------------- */
11
12
  /* Proxy Trust */
12
13
  /* -------------------------------------------------------------------------- */
@@ -122,53 +123,42 @@ export function getNodeRemoteAddress(request, options = {}) {
122
123
  /**
123
124
  * Parses the request-target's query string into a flat record.
124
125
  *
125
- * Every value is attacker-controlled. `decodeURIComponent` throws on a
126
- * malformed sequence such as `%E0`, and this ran before the adapter's
127
- * try/catch, so one such request tore the connection down instead of being
128
- * answered. Decoding is delegated to the query module's non-throwing
129
- * decoder, which also gives `+` its form-encoding meaning. A pair is split on
130
- * its **first** `=` so `a=b=c` keeps the value `b=c`.
126
+ * Every value is attacker-controlled, so decoding never throws (a malformed
127
+ * `%E0` is kept raw) and `+` has its form-encoding meaning. This uses the same
128
+ * parser as the router's `ctx.query` (`parseQueryString`), so both agree: a
129
+ * repeated name is an array (`?role=user&role=admin` gives
130
+ * `["user", "admin"]`), the record has a `null` prototype, and
131
+ * `__proto__` / `constructor` / `prototype` are dropped. Previously the last
132
+ * value silently won here while the router returned the array.
131
133
  */
132
134
  export function parseNodeQuery(request) {
133
135
  const url = request.url;
134
136
  if (!url) {
135
- return Object.freeze({});
137
+ return Object.freeze(Object.create(null));
136
138
  }
137
139
  const questionIndex = url.indexOf("?");
138
140
  if (questionIndex === -1) {
139
- return Object.freeze({});
141
+ return Object.freeze(Object.create(null));
140
142
  }
141
143
  const hashIndex = url.indexOf("#", questionIndex + 1);
142
144
  const queryString = url.slice(questionIndex + 1, hashIndex === -1 ? undefined : hashIndex);
143
- if (!queryString) {
144
- return Object.freeze({});
145
- }
146
- const params = {};
147
- for (const pair of queryString.split("&")) {
148
- if (pair === "") {
149
- continue;
150
- }
151
- const separator = pair.indexOf("=");
152
- const rawKey = separator === -1 ? pair : pair.slice(0, separator);
153
- const rawValue = separator === -1 ? "" : pair.slice(separator + 1);
154
- const key = decodeQueryComponent(rawKey);
155
- if (key) {
156
- params[key] = decodeQueryComponent(rawValue);
157
- }
158
- }
159
- return Object.freeze(params);
145
+ return Object.freeze(parseQueryString(queryString));
160
146
  }
161
147
  /* -------------------------------------------------------------------------- */
162
148
  /* Request Context */
163
149
  /* -------------------------------------------------------------------------- */
164
150
  export function createNodeRequestContext(request, options = {}) {
151
+ const url = request.url ?? "/";
152
+ const violation = findRequestTargetViolation(url);
153
+ if (violation !== undefined) {
154
+ throw new TypeError(violation);
155
+ }
165
156
  const headers = getNodeRequestHeaders(request);
166
157
  const protocol = getNodeRequestProtocol(request, options);
167
158
  const hostname = getNodeRequestHostname(request, options);
168
159
  const port = getNodeRequestPort(request, options);
169
160
  const remoteAddress = getNodeRemoteAddress(request, options);
170
161
  const query = parseNodeQuery(request);
171
- const url = request.url ?? "/";
172
162
  return createRequestContext({
173
163
  method: request.method?.toUpperCase() ?? "GET",
174
164
  url,
@@ -43,6 +43,12 @@ export declare function getSearchPart(url: string): string;
43
43
  import { RequestBodyTooLargeError as NodeRequestBodyTooLargeError } from "@zudojs/errors";
44
44
  export { NodeRequestBodyTooLargeError };
45
45
  export declare function readNodeRequestBody(request: IncomingMessage, maxBodySize?: number): Promise<Uint8Array>;
46
+ /**
47
+ * Whether a value has the shape of a response init.
48
+ *
49
+ * @deprecated No longer used by the Node adapter: a plain object returned
50
+ * from a handler is always sent as JSON. Kept for API compatibility.
51
+ */
46
52
  export declare function isResponseContextLike(value: unknown): value is {
47
53
  readonly status?: number;
48
54
  readonly statusText?: string;
@@ -250,6 +250,12 @@ export function readNodeRequestBody(request, maxBodySize = DEFAULT_MAX_BODY_SIZE
250
250
  /* -------------------------------------------------------------------------- */
251
251
  /* Response Result Detection */
252
252
  /* -------------------------------------------------------------------------- */
253
+ /**
254
+ * Whether a value has the shape of a response init.
255
+ *
256
+ * @deprecated No longer used by the Node adapter: a plain object returned
257
+ * from a handler is always sent as JSON. Kept for API compatibility.
258
+ */
253
259
  export function isResponseContextLike(value) {
254
260
  if (value === null || typeof value !== "object") {
255
261
  return false;