@zudojs/http 1.0.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 (85) hide show
  1. package/README.md +64 -6
  2. package/dist/httpAdapter/http.adapter.d.ts +34 -3
  3. package/dist/httpAdapter/http.adapter.js +28 -10
  4. package/dist/httpAdapter/http.adapters.js +4 -34
  5. package/dist/httpAdapter/httpAdapter.errorResponse.d.ts +35 -0
  6. package/dist/httpAdapter/httpAdapter.errorResponse.js +137 -0
  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 +20 -1
  10. package/dist/httpAdapter/node/httpNode.adapter.js +80 -20
  11. package/dist/httpAdapter/node/httpNode.request.d.ts +12 -1
  12. package/dist/httpAdapter/node/httpNode.request.js +23 -16
  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/httpClient/httpClient.executor.js +14 -3
  17. package/dist/httpCookies/http.cookies.d.ts +29 -2
  18. package/dist/httpCookies/http.cookies.js +44 -24
  19. package/dist/httpCookies/httpCookies.defaults.d.ts +40 -0
  20. package/dist/httpCookies/httpCookies.defaults.js +42 -0
  21. package/dist/httpCookies/index.d.ts +1 -0
  22. package/dist/httpCookies/index.js +1 -0
  23. package/dist/httpMiddleware/builtin/conditional/httpMiddleware.conditional.d.ts +22 -1
  24. package/dist/httpMiddleware/builtin/conditional/httpMiddleware.conditional.js +19 -4
  25. package/dist/httpMiddleware/builtin/conditional/index.d.ts +1 -0
  26. package/dist/httpMiddleware/builtin/cors/httpMiddleware.cors.d.ts +8 -0
  27. package/dist/httpMiddleware/builtin/cors/httpMiddleware.cors.js +25 -22
  28. package/dist/httpMiddleware/builtin/helpers/httpMiddleware.accessor.js +2 -6
  29. package/dist/httpMiddleware/builtin/helpers/httpMiddleware.media.d.ts +33 -0
  30. package/dist/httpMiddleware/builtin/helpers/httpMiddleware.media.js +59 -0
  31. package/dist/httpMiddleware/builtin/helpers/index.d.ts +1 -0
  32. package/dist/httpMiddleware/builtin/helpers/index.js +1 -0
  33. package/dist/httpMiddleware/builtin/image/httpMiddleware.image.d.ts +13 -0
  34. package/dist/httpMiddleware/builtin/image/httpMiddleware.image.js +31 -53
  35. package/dist/httpMiddleware/builtin/index.d.ts +1 -0
  36. package/dist/httpMiddleware/builtin/index.js +1 -0
  37. package/dist/httpMiddleware/builtin/rateLimit/httpMiddleware.rateLimit.d.ts +47 -0
  38. package/dist/httpMiddleware/builtin/rateLimit/httpMiddleware.rateLimit.js +61 -0
  39. package/dist/httpMiddleware/builtin/rateLimit/index.d.ts +7 -0
  40. package/dist/httpMiddleware/builtin/rateLimit/index.js +7 -0
  41. package/dist/httpMiddleware/builtin/static/httpMiddleware.static.js +30 -4
  42. package/dist/httpMiddleware/builtin/video/httpMiddleware.video.d.ts +19 -1
  43. package/dist/httpMiddleware/builtin/video/httpMiddleware.video.js +38 -59
  44. package/dist/httpMiddleware/builtin/video/httpMiddleware.video.options.d.ts +12 -0
  45. package/dist/httpMiddleware/builtin/video/httpMiddleware.video.options.js +31 -0
  46. package/dist/httpMiddleware/httpMiddleware.error.d.ts +5 -31
  47. package/dist/httpMiddleware/httpMiddleware.error.js +5 -53
  48. package/dist/httpProxy/http.proxy.d.ts +7 -0
  49. package/dist/httpProxy/http.proxy.js +9 -0
  50. package/dist/httpProxy/httpProxy.pathGuard.d.ts +23 -0
  51. package/dist/httpProxy/httpProxy.pathGuard.js +43 -0
  52. package/dist/httpProxy/index.d.ts +1 -0
  53. package/dist/httpProxy/index.js +1 -0
  54. package/dist/httpRequest/httpRequest.context.d.ts +7 -0
  55. package/dist/httpRequest/httpRequest.context.js +15 -16
  56. package/dist/httpRequest/index.d.ts +1 -0
  57. package/dist/httpRequest/index.js +1 -0
  58. package/dist/httpRequest/target/httpRequest.target.d.ts +45 -0
  59. package/dist/httpRequest/target/httpRequest.target.js +93 -0
  60. package/dist/httpRequest/target/index.d.ts +9 -0
  61. package/dist/httpRequest/target/index.js +9 -0
  62. package/dist/httpResponse/httpResponse.context.d.ts +10 -0
  63. package/dist/httpResponse/httpResponse.context.js +13 -1
  64. package/dist/httpResponse/httpResponse.helper.d.ts +22 -0
  65. package/dist/httpResponse/httpResponse.helper.js +94 -24
  66. package/dist/httpRouter/core/registry/core/httpRegistry.helper.js +1 -1
  67. package/dist/httpRouter/core/util/httpRoute.util.d.ts +14 -0
  68. package/dist/httpRouter/core/util/httpRoute.util.js +18 -14
  69. package/dist/httpRouter/dispatch/httpRoute.dispatcher.js +12 -1
  70. package/dist/httpSecurity/httpSecurity.config.d.ts +6 -0
  71. package/dist/httpSecurity/httpSecurity.config.js +1 -0
  72. package/dist/httpSecurity/httpSecurity.guard.d.ts +7 -9
  73. package/dist/httpSecurity/httpSecurity.guard.js +7 -14
  74. package/dist/httpSecurity/httpSecurity.nodeGuard.d.ts +48 -0
  75. package/dist/httpSecurity/httpSecurity.nodeGuard.js +32 -0
  76. package/dist/httpSecurity/httpSecurity.validator.js +4 -2
  77. package/dist/httpSecurity/index.d.ts +2 -0
  78. package/dist/httpSecurity/index.js +1 -0
  79. package/dist/httpServer/core/httpServer.core.d.ts +11 -0
  80. package/dist/httpServer/core/httpServer.core.js +30 -4
  81. package/dist/httpTrustProxy/httpTrustProxy.compilation.d.ts +5 -2
  82. package/dist/httpTrustProxy/httpTrustProxy.compilation.js +19 -2
  83. package/dist/httpTrustProxy/httpTrustProxy.type.d.ts +3 -1
  84. package/dist/index.js +0 -1
  85. package/package.json +9 -5
@@ -5,6 +5,7 @@
5
5
  */
6
6
  import { DEFAULT_RESPONSE_STATUS } from "./core/httpResponse.type.js";
7
7
  import { getStatusText } from "./core/httpResponse.statusText.js";
8
+ import { assertSafeRedirect } from "../httpRedirect/http.redirect.js";
8
9
  export class HttpResponseContext {
9
10
  _status;
10
11
  _statusText;
@@ -86,10 +87,21 @@ export class HttpResponseContext {
86
87
  this._cookies = this._cookies.filter((c) => c.name !== name);
87
88
  return this;
88
89
  }
90
+ /**
91
+ * Redirects to `url`.
92
+ *
93
+ * The destination goes through `assertSafeRedirect`: a `javascript:` or
94
+ * `data:` URL, a scheme-relative `//evil.com`, or a value carrying a
95
+ * control character throws instead of being emitted as `Location`. An
96
+ * absolute `http(s)` URL and a same-origin path reference are accepted.
97
+ *
98
+ * @throws {TypeError} If the destination is not a safe redirect target.
99
+ */
89
100
  redirect(url, status = 302) {
101
+ const location = assertSafeRedirect(url);
90
102
  this._status = status;
91
103
  this._statusText = getStatusText(status);
92
- this._headers["location"] = url;
104
+ this._headers["location"] = location;
93
105
  return this;
94
106
  }
95
107
  markSent() {
@@ -30,6 +30,12 @@ export declare function htmlResponse(html: string, status?: number, headers?: Re
30
30
  };
31
31
  /**
32
32
  * Creates a redirect response.
33
+ *
34
+ * The destination goes through {@link assertSafeRedirect}: a `javascript:` or
35
+ * `data:` URL, a scheme-relative `//evil.com`, or a value carrying a control
36
+ * character throws instead of being emitted as `Location`.
37
+ *
38
+ * @throws {TypeError} If the destination is not a safe redirect target.
33
39
  */
34
40
  export declare function redirectResponse(url: string, status?: number, headers?: ResponseHeaders): {
35
41
  readonly status: number;
@@ -46,6 +52,22 @@ export declare function emptyResponse(status?: number, headers?: ResponseHeaders
46
52
  };
47
53
  /**
48
54
  * Serializes a response cookie into a Set-Cookie header string.
55
+ *
56
+ * This is the serializer the adapters use for `response.cookie()` and it
57
+ * previously concatenated every field verbatim, so a value such as
58
+ * `x; Domain=evil.com` became a second attribute and a `__Host-` cookie
59
+ * missing `Secure` was emitted (and silently dropped by the browser). It now
60
+ * applies the same rules as `serializeCookie()` in the cookies module:
61
+ *
62
+ * - the name must be an RFC 6265 token;
63
+ * - `__Host-` / `__Secure-` prefix constraints are enforced;
64
+ * - `Domain` and `Path` may not contain `;` or a control character;
65
+ * - a value that is not made only of `cookie-octet` characters is
66
+ * percent-encoded, so it can neither inject an attribute nor split the
67
+ * header (a value that is already valid is emitted unchanged);
68
+ * - `maxAge` must be finite and `expires` must be a valid `Date`.
69
+ *
70
+ * @throws {TypeError} If the name, a prefix rule or an attribute is invalid.
49
71
  */
50
72
  export declare function serializeResponseCookie(cookie: ResponseCookie): string;
51
73
  //# sourceMappingURL=httpResponse.helper.d.ts.map
@@ -3,6 +3,8 @@
3
3
  *
4
4
  * @module httpResponse/helpers
5
5
  */
6
+ import { withSecureCookieDefaults } from "../httpCookies/httpCookies.defaults.js";
7
+ import { assertSafeRedirect } from "../httpRedirect/http.redirect.js";
6
8
  /**
7
9
  * Creates a JSON response.
8
10
  */
@@ -44,12 +46,18 @@ export function htmlResponse(html, status = 200, headers = {}) {
44
46
  }
45
47
  /**
46
48
  * Creates a redirect response.
49
+ *
50
+ * The destination goes through {@link assertSafeRedirect}: a `javascript:` or
51
+ * `data:` URL, a scheme-relative `//evil.com`, or a value carrying a control
52
+ * character throws instead of being emitted as `Location`.
53
+ *
54
+ * @throws {TypeError} If the destination is not a safe redirect target.
47
55
  */
48
56
  export function redirectResponse(url, status = 302, headers = {}) {
49
57
  return {
50
58
  status,
51
59
  headers: {
52
- location: url,
60
+ location: assertSafeRedirect(url),
53
61
  ...headers,
54
62
  },
55
63
  body: undefined,
@@ -65,40 +73,102 @@ export function emptyResponse(status = 204, headers = {}) {
65
73
  body: undefined,
66
74
  };
67
75
  }
76
+ /**
77
+ * RFC 6265 `cookie-name`, which is an RFC 9110 `token`.
78
+ */
79
+ const COOKIE_NAME_TOKEN = /^[!#$%&'*+\-.^_`|~0-9A-Za-z]+$/;
80
+ /**
81
+ * RFC 6265 `cookie-octet`: printable US-ASCII minus CTLs, SP, DQUOTE, comma,
82
+ * semicolon and backslash.
83
+ */
84
+ const COOKIE_OCTETS = /^[\u0021\u0023-\u002b\u002d-\u003a\u003c-\u005b\u005d-\u007e]*$/;
85
+ /**
86
+ * Characters that must never appear in a cookie attribute value: `;` starts
87
+ * the next attribute, and any control character can split the header.
88
+ */
89
+ const COOKIE_ATTRIBUTE_FORBIDDEN = /[;\u0000-\u001f\u007f]/;
68
90
  /**
69
91
  * Serializes a response cookie into a Set-Cookie header string.
92
+ *
93
+ * This is the serializer the adapters use for `response.cookie()` and it
94
+ * previously concatenated every field verbatim, so a value such as
95
+ * `x; Domain=evil.com` became a second attribute and a `__Host-` cookie
96
+ * missing `Secure` was emitted (and silently dropped by the browser). It now
97
+ * applies the same rules as `serializeCookie()` in the cookies module:
98
+ *
99
+ * - the name must be an RFC 6265 token;
100
+ * - `__Host-` / `__Secure-` prefix constraints are enforced;
101
+ * - `Domain` and `Path` may not contain `;` or a control character;
102
+ * - a value that is not made only of `cookie-octet` characters is
103
+ * percent-encoded, so it can neither inject an attribute nor split the
104
+ * header (a value that is already valid is emitted unchanged);
105
+ * - `maxAge` must be finite and `expires` must be a valid `Date`.
106
+ *
107
+ * @throws {TypeError} If the name, a prefix rule or an attribute is invalid.
70
108
  */
71
109
  export function serializeResponseCookie(cookie) {
72
- let str = `${cookie.name}=${cookie.value}`;
73
- if (cookie.options) {
74
- const opts = cookie.options;
75
- if (opts.domain) {
76
- str += `; Domain=${opts.domain}`;
77
- }
78
- if (opts.path) {
79
- str += `; Path=${opts.path}`;
80
- }
81
- if (opts.expires instanceof Date) {
82
- str += `; Expires=${opts.expires.toUTCString()}`;
110
+ const name = cookie.name;
111
+ if (!name || !COOKIE_NAME_TOKEN.test(name)) {
112
+ throw new TypeError(`Invalid cookie name: ${JSON.stringify(name)}`);
113
+ }
114
+ const opts = withSecureCookieDefaults(cookie.options, "Lax");
115
+ if (name.startsWith("__Host-")) {
116
+ if (!opts.secure) {
117
+ throw new TypeError("A __Host- cookie requires the Secure attribute.");
83
118
  }
84
- if (typeof opts.maxAge === "number") {
85
- str += `; Max-Age=${opts.maxAge}`;
119
+ if (opts.domain) {
120
+ throw new TypeError("A __Host- cookie must not set a Domain attribute.");
86
121
  }
87
- if (opts.httpOnly) {
88
- str += "; HttpOnly";
122
+ if ((opts.path ?? "/") !== "/") {
123
+ throw new TypeError("A __Host- cookie requires Path=/.");
89
124
  }
90
- if (opts.secure) {
91
- str += "; Secure";
125
+ }
126
+ else if (name.startsWith("__Secure-") && !opts.secure) {
127
+ throw new TypeError("A __Secure- cookie requires the Secure attribute.");
128
+ }
129
+ const rawValue = String(cookie.value);
130
+ const value = COOKIE_OCTETS.test(rawValue)
131
+ ? rawValue
132
+ : encodeURIComponent(rawValue);
133
+ let str = `${name}=${value}`;
134
+ if (opts.domain) {
135
+ if (COOKIE_ATTRIBUTE_FORBIDDEN.test(opts.domain)) {
136
+ throw new TypeError("Invalid cookie domain.");
92
137
  }
93
- if (opts.sameSite) {
94
- str += `; SameSite=${opts.sameSite}`;
138
+ str += `; Domain=${opts.domain}`;
139
+ }
140
+ if (opts.path) {
141
+ if (COOKIE_ATTRIBUTE_FORBIDDEN.test(opts.path)) {
142
+ throw new TypeError("Invalid cookie path.");
95
143
  }
96
- if (opts.priority) {
97
- str += `; Priority=${opts.priority}`;
144
+ str += `; Path=${opts.path}`;
145
+ }
146
+ if (opts.expires instanceof Date) {
147
+ if (Number.isNaN(opts.expires.getTime())) {
148
+ throw new TypeError("Cookie expires must be a valid Date.");
98
149
  }
99
- if (opts.partitioned) {
100
- str += "; Partitioned";
150
+ str += `; Expires=${opts.expires.toUTCString()}`;
151
+ }
152
+ if (typeof opts.maxAge === "number") {
153
+ if (!Number.isFinite(opts.maxAge)) {
154
+ throw new TypeError("Cookie maxAge must be a finite number.");
101
155
  }
156
+ str += `; Max-Age=${Math.floor(opts.maxAge)}`;
157
+ }
158
+ if (opts.httpOnly) {
159
+ str += "; HttpOnly";
160
+ }
161
+ if (opts.secure) {
162
+ str += "; Secure";
163
+ }
164
+ if (opts.sameSite) {
165
+ str += `; SameSite=${opts.sameSite}`;
166
+ }
167
+ if (opts.priority) {
168
+ str += `; Priority=${opts.priority}`;
169
+ }
170
+ if (opts.partitioned) {
171
+ str += "; Partitioned";
102
172
  }
103
173
  return str;
104
174
  }
@@ -42,7 +42,7 @@ export function normalizePath(path, options) {
42
42
  normalized = normalized.toLowerCase();
43
43
  }
44
44
  if (!options.strict) {
45
- normalized = normalized.replace(/\/+$/, "") || "/";
45
+ normalized = normalized.replace(/(?<!\/)\/+$/, "") || "/";
46
46
  }
47
47
  return normalized;
48
48
  }
@@ -8,7 +8,21 @@ import type { HttpRequestContext as RequestContext } from "../../../httpRequest/
8
8
  export declare function getRequestMethod(request: RequestContext): string;
9
9
  export declare function getRequestUrl(request: RequestContext): string;
10
10
  export declare function getRequestSignal(request: RequestContext): AbortSignal | undefined;
11
+ /**
12
+ * Parses a request-target with the canonical parser shared by the request
13
+ * context and path-scoped middleware.
14
+ */
11
15
  export declare function parseUrl(value: string): URL;
16
+ /**
17
+ * Builds the router's query record.
18
+ *
19
+ * Uses the same parser as the Node adapter's `request.query`
20
+ * (`parseQueryString`): a repeated name is an array, the record has a `null`
21
+ * prototype, and `__proto__` / `constructor` / `prototype` are dropped. The
22
+ * two used to disagree (`ctx.query.role` an array, `request.getQuery("role")`
23
+ * the last value), which let middleware and handlers read different values
24
+ * for the same parameter.
25
+ */
12
26
  export declare function parseQuery(params: URLSearchParams): Readonly<Record<string, string | string[]>>;
13
27
  export declare function normalizePath(path: string): string;
14
28
  export declare function splitPath(path: string): string[];
@@ -4,6 +4,8 @@
4
4
  * Request accessors, URL/query parsing, and path normalization shared by the
5
5
  * router core.
6
6
  */
7
+ import { parseRequestTarget } from "../../../httpRequest/target/httpRequest.target.js";
8
+ import { parseQueryString } from "../../../httpQuery/http.query.js";
7
9
  import { InvalidRoutePatternError } from "../error/httpRouter.error.js";
8
10
  export function getRequestMethod(request) {
9
11
  const value = request.method;
@@ -19,23 +21,25 @@ export function getRequestUrl(request) {
19
21
  export function getRequestSignal(request) {
20
22
  return request.signal;
21
23
  }
24
+ /**
25
+ * Parses a request-target with the canonical parser shared by the request
26
+ * context and path-scoped middleware.
27
+ */
22
28
  export function parseUrl(value) {
23
- try {
24
- return new URL(value, "http://zudojs.local");
25
- }
26
- catch {
27
- return new URL("/", "http://zudojs.local");
28
- }
29
+ return parseRequestTarget(value);
29
30
  }
31
+ /**
32
+ * Builds the router's query record.
33
+ *
34
+ * Uses the same parser as the Node adapter's `request.query`
35
+ * (`parseQueryString`): a repeated name is an array, the record has a `null`
36
+ * prototype, and `__proto__` / `constructor` / `prototype` are dropped. The
37
+ * two used to disagree (`ctx.query.role` an array, `request.getQuery("role")`
38
+ * the last value), which let middleware and handlers read different values
39
+ * for the same parameter.
40
+ */
30
41
  export function parseQuery(params) {
31
- const result = {};
32
- for (const key of new Set(Array.from(params.keys()))) {
33
- const values = params.getAll(key);
34
- result[key] = values.length > 1 ? values : (values[0] ?? "");
35
- }
36
- return Object.freeze({
37
- ...result,
38
- });
42
+ return Object.freeze(parseQueryString(params.toString()));
39
43
  }
40
44
  /* -------------------------------------------------------------------------- */
41
45
  /* Path Helpers */
@@ -229,8 +229,19 @@ function toDispatchHandler(handler, context) {
229
229
  signal: getRequestSignal(request),
230
230
  }));
231
231
  if (result instanceof HttpResponseContext && result !== response) {
232
- response.status_code(result.status);
232
+ /*
233
+ * Everything the handler put on its response must survive the merge.
234
+ * Copying only status, headers and body dropped every cookie the
235
+ * handler set, its custom status text and its metadata.
236
+ */
237
+ response.setStatus(result.status, result.statusText);
233
238
  response.headers_obj(result.headers);
239
+ for (const cookie of result.cookies) {
240
+ response.setCookie(cookie);
241
+ }
242
+ for (const [key, value] of Object.entries(result.metadata)) {
243
+ response.setMetadata(key, value);
244
+ }
234
245
  response.setBody(result.body);
235
246
  }
236
247
  };
@@ -26,6 +26,12 @@ export interface HTTPSecurityConfig {
26
26
  readonly maxQueryLength?: number;
27
27
  /** Allowed Host header values. Empty = allow all. */
28
28
  readonly allowedHosts?: readonly string[];
29
+ /**
30
+ * Whether a request without a Host header is rejected (default: true).
31
+ * The Node adapter relaxes this for HTTP/1.0 requests, where Host is
32
+ * optional and some load-balancer health checks omit it.
33
+ */
34
+ readonly requireHost?: boolean;
29
35
  /**
30
36
  * Whether `X-Forwarded-*` headers may be trusted (default: `false`).
31
37
  *
@@ -20,6 +20,7 @@ export const DEFAULT_SECURITY_CONFIG = Object.freeze({
20
20
  maxUrlLength: 2048,
21
21
  maxQueryLength: 4096,
22
22
  allowedHosts: [],
23
+ requireHost: true,
23
24
  trustProxy: false,
24
25
  maxRequestIdLength: 128,
25
26
  requestIdPattern: /^[a-zA-Z0-9_-]+$/,
@@ -5,6 +5,7 @@
5
5
  * first security boundary. Rejects malformed, oversized, or
6
6
  * suspicious requests before they reach the router.
7
7
  */
8
+ import { HttpRequestGuardError } from "@zudojs/errors";
8
9
  import type { HTTPSecurityConfig } from "./httpSecurity.config.js";
9
10
  /** A request-like object for validation (keeps the guard decoupled from HTTP types). */
10
11
  export interface GuardableRequest {
@@ -39,16 +40,13 @@ export declare function guardRequest(request: GuardableRequest, config?: Partial
39
40
  */
40
41
  export declare function createRequestGuard(config?: Partial<HTTPSecurityConfig>): (request: GuardableRequest) => GuardResult;
41
42
  /**
42
- * Error thrown by {@link assertRequestAllowed}.
43
- *
44
- * Carries the status the response should use and the individual validation
45
- * failures, so a caller can log the detail without returning it to the client.
43
+ * Error thrown by {@link assertRequestAllowed}: the shared
44
+ * `HttpRequestGuardError` from `@zudojs/errors` (a `BaseError`, code
45
+ * `HTTP_REQUEST_REJECTED`, not exposed). It carries the status the response
46
+ * should use and the individual validation failures, so a caller can log
47
+ * the detail without returning it to the client.
46
48
  */
47
- export declare class HttpRequestGuardError extends Error {
48
- readonly statusCode: number;
49
- readonly errors: readonly string[];
50
- constructor(result: GuardResult);
51
- }
49
+ export { HttpRequestGuardError };
52
50
  /**
53
51
  * Runs the guard and throws {@link HttpRequestGuardError} when the request is
54
52
  * rejected.
@@ -5,6 +5,7 @@
5
5
  * first security boundary. Rejects malformed, oversized, or
6
6
  * suspicious requests before they reach the router.
7
7
  */
8
+ import { HttpRequestGuardError } from "@zudojs/errors";
8
9
  import { validateHeaders, validateHost, validateUrl, validateQuery, validateContentLength, validateRequestId, validateTransferEncoding, } from "./httpSecurity.validator.js";
9
10
  /**
10
11
  * Run all security validations against an incoming request.
@@ -52,21 +53,13 @@ export function createRequestGuard(config) {
52
53
  return (request) => guardRequest(request, config);
53
54
  }
54
55
  /**
55
- * Error thrown by {@link assertRequestAllowed}.
56
- *
57
- * Carries the status the response should use and the individual validation
58
- * failures, so a caller can log the detail without returning it to the client.
56
+ * Error thrown by {@link assertRequestAllowed}: the shared
57
+ * `HttpRequestGuardError` from `@zudojs/errors` (a `BaseError`, code
58
+ * `HTTP_REQUEST_REJECTED`, not exposed). It carries the status the response
59
+ * should use and the individual validation failures, so a caller can log
60
+ * the detail without returning it to the client.
59
61
  */
60
- export class HttpRequestGuardError extends Error {
61
- statusCode;
62
- errors;
63
- constructor(result) {
64
- super(`Request rejected by security guard: ${result.errors.join("; ")}`);
65
- this.name = "HttpRequestGuardError";
66
- this.statusCode = result.statusCode;
67
- this.errors = result.errors;
68
- }
69
- }
62
+ export { HttpRequestGuardError };
70
63
  /**
71
64
  * Runs the guard and throws {@link HttpRequestGuardError} when the request is
72
65
  * rejected.
@@ -0,0 +1,48 @@
1
+ /**
2
+ * Request guard wiring for the Node adapter.
3
+ *
4
+ * `guardRequest` (Host, request-ID, header-count, header-size, URL/query
5
+ * length and `Transfer-Encoding`/`Content-Length` checks) had no call site,
6
+ * so every one of those checks was inert unless an application wired it by
7
+ * hand. The Node adapter now runs it on every request before the request
8
+ * context is built or the body is read, and answers `400` when it refuses.
9
+ *
10
+ * @module httpSecurity/nodeGuard
11
+ */
12
+ import type { IncomingMessage } from "node:http";
13
+ import type { HTTPSecurityConfig } from "./httpSecurity.config.js";
14
+ import { type GuardResult } from "./httpSecurity.guard.js";
15
+ /**
16
+ * The adapter's `security` option.
17
+ *
18
+ * - `true` / omitted (default): run the guard with its defaults.
19
+ * - an object: run the guard with that configuration.
20
+ * - `false`: do not run the guard.
21
+ */
22
+ export type NodeAdapterSecurityOption = boolean | Partial<HTTPSecurityConfig>;
23
+ /**
24
+ * Adapter options contributed by the request guard.
25
+ */
26
+ export interface NodeAdapterSecurityOptions {
27
+ /**
28
+ * Request guard applied before dispatch (on by default). See
29
+ * {@link NodeAdapterSecurityOption}.
30
+ *
31
+ * The guard's `maxBodySize` defaults to "no limit" here so the adapter's own
32
+ * `maxBodySize` keeps answering oversized bodies with `413`; set it
33
+ * explicitly to have the guard refuse a too-large `Content-Length` with
34
+ * `400` before any body is read. `requireHost` defaults to `true` except for
35
+ * HTTP/1.0 requests, where Host is optional.
36
+ */
37
+ readonly security?: NodeAdapterSecurityOption;
38
+ }
39
+ /**
40
+ * A per-request guard for Node requests.
41
+ */
42
+ export type NodeRequestGuard = (request: IncomingMessage) => GuardResult;
43
+ /**
44
+ * Builds the guard the adapter runs on each request, or `undefined` when the
45
+ * guard is disabled with `security: false`.
46
+ */
47
+ export declare function createNodeRequestGuard(option: NodeAdapterSecurityOption | undefined): NodeRequestGuard | undefined;
48
+ //# sourceMappingURL=httpSecurity.nodeGuard.d.ts.map
@@ -0,0 +1,32 @@
1
+ /**
2
+ * Request guard wiring for the Node adapter.
3
+ *
4
+ * `guardRequest` (Host, request-ID, header-count, header-size, URL/query
5
+ * length and `Transfer-Encoding`/`Content-Length` checks) had no call site,
6
+ * so every one of those checks was inert unless an application wired it by
7
+ * hand. The Node adapter now runs it on every request before the request
8
+ * context is built or the body is read, and answers `400` when it refuses.
9
+ *
10
+ * @module httpSecurity/nodeGuard
11
+ */
12
+ import { guardRequest, } from "./httpSecurity.guard.js";
13
+ /**
14
+ * Builds the guard the adapter runs on each request, or `undefined` when the
15
+ * guard is disabled with `security: false`.
16
+ */
17
+ export function createNodeRequestGuard(option) {
18
+ if (option === false) {
19
+ return undefined;
20
+ }
21
+ const configured = typeof option === "object" ? option : {};
22
+ return (request) => guardRequest({
23
+ method: request.method ?? "GET",
24
+ url: request.url ?? "/",
25
+ headers: request.headers,
26
+ }, {
27
+ maxBodySize: Number.MAX_SAFE_INTEGER,
28
+ requireHost: request.httpVersion !== "1.0",
29
+ ...configured,
30
+ });
31
+ }
32
+ //# sourceMappingURL=httpSecurity.nodeGuard.js.map
@@ -36,10 +36,12 @@ export function validateHeaders(headers, config) {
36
36
  * Validate the Host header against allowed hosts.
37
37
  */
38
38
  export function validateHost(host, config) {
39
+ const cfg = resolveConfig(config);
39
40
  if (!host) {
40
- return { valid: false, errors: ["Missing Host header"] };
41
+ return cfg.requireHost
42
+ ? { valid: false, errors: ["Missing Host header"] }
43
+ : { valid: true, errors: [] };
41
44
  }
42
- const cfg = resolveConfig(config);
43
45
  if (cfg.allowedHosts.length === 0) {
44
46
  return { valid: true, errors: [] };
45
47
  }
@@ -11,4 +11,6 @@ export type { SecurityValidationResult } from "./httpSecurity.validator.js";
11
11
  export { validateHeaders, validateHost, validateUrl, validateQuery, validateContentLength, validateRequestId, validateTransferEncoding, } from "./httpSecurity.validator.js";
12
12
  export type { GuardableRequest, GuardResult } from "./httpSecurity.guard.js";
13
13
  export { guardRequest, createRequestGuard, assertRequestAllowed, HttpRequestGuardError, } from "./httpSecurity.guard.js";
14
+ export type { NodeAdapterSecurityOption, NodeAdapterSecurityOptions, NodeRequestGuard, } from "./httpSecurity.nodeGuard.js";
15
+ export { createNodeRequestGuard } from "./httpSecurity.nodeGuard.js";
14
16
  //# sourceMappingURL=index.d.ts.map
@@ -8,4 +8,5 @@
8
8
  export { DEFAULT_SECURITY_CONFIG } from "./httpSecurity.config.js";
9
9
  export { validateHeaders, validateHost, validateUrl, validateQuery, validateContentLength, validateRequestId, validateTransferEncoding, } from "./httpSecurity.validator.js";
10
10
  export { guardRequest, createRequestGuard, assertRequestAllowed, HttpRequestGuardError, } from "./httpSecurity.guard.js";
11
+ export { createNodeRequestGuard } from "./httpSecurity.nodeGuard.js";
11
12
  //# sourceMappingURL=index.js.map
@@ -45,6 +45,12 @@ export declare class HttpServer {
45
45
  close(): Promise<this>;
46
46
  setHandler(handler: HttpHandler): this;
47
47
  setErrorHandler(handler: HttpErrorHandler): this;
48
+ /**
49
+ * Installs `handler` on the adapter, wrapped so every dispatch is counted
50
+ * (`requests`, `onRequest`) and its completion reported (`onResponse`).
51
+ * Nothing called `recordRequest`/`recordResponse` before, so the counter
52
+ * and both events stayed at zero.
53
+ */
48
54
  private adapterHandler;
49
55
  private adapterErrorHandler;
50
56
  recordRequest(): void;
@@ -61,6 +67,11 @@ export declare class HttpServer {
61
67
  snapshot(): HttpServerSnapshot;
62
68
  toJSON(): HttpServerSnapshot;
63
69
  private performStart;
70
+ /**
71
+ * The adapter is told the shutdown timeout as its grace period, so
72
+ * in-flight requests get the documented `gracefulShutdownTimeout` rather
73
+ * than the Node adapter's hidden 10 s default.
74
+ */
64
75
  private performStop;
65
76
  private refreshAddress;
66
77
  }
@@ -6,6 +6,7 @@
6
6
  import { isHttpAdapter, startAdapter, stopAdapter, } from "../../httpAdapter/http.adapter.js";
7
7
  import { HttpServerLifecycleError, InvalidHttpServerStateError, HttpServerStartError, HttpServerStopError, } from "@zudojs/errors";
8
8
  import { withTimeout, validateShutdownTimeout, } from "../factory/httpServer.factory.js";
9
+ const SHUTDOWN_CLOSE_MARGIN_MS = 1_000;
9
10
  export class HttpServer {
10
11
  name;
11
12
  adapter;
@@ -170,7 +171,22 @@ export class HttpServer {
170
171
  this.adapterErrorHandler(handler);
171
172
  return this;
172
173
  }
173
- adapterHandler(handler) {
174
+ /**
175
+ * Installs `handler` on the adapter, wrapped so every dispatch is counted
176
+ * (`requests`, `onRequest`) and its completion reported (`onResponse`).
177
+ * Nothing called `recordRequest`/`recordResponse` before, so the counter
178
+ * and both events stayed at zero.
179
+ */
180
+ adapterHandler(application) {
181
+ const handler = async (request) => {
182
+ this.recordRequest();
183
+ try {
184
+ return await application(request);
185
+ }
186
+ finally {
187
+ this.recordResponse();
188
+ }
189
+ };
174
190
  const adapter = this.adapter;
175
191
  if (typeof adapter.setHandler === "function") {
176
192
  adapter.setHandler(handler);
@@ -262,13 +278,23 @@ export class HttpServer {
262
278
  async performStart() {
263
279
  await startAdapter(this.adapter);
264
280
  }
281
+ /**
282
+ * The adapter is told the shutdown timeout as its grace period, so
283
+ * in-flight requests get the documented `gracefulShutdownTimeout` rather
284
+ * than the Node adapter's hidden 10 s default.
285
+ */
286
+ /*
287
+ * The adapter gets the full timeout as its grace period and then destroys
288
+ * the remaining sockets; the margin only lets that close complete before
289
+ * the stop is reported as timed out.
290
+ */
265
291
  async performStop(force, timeout) {
266
292
  if (force) {
267
- await stopAdapter(this.adapter);
293
+ await stopAdapter(this.adapter, { graceMs: 0 });
268
294
  return;
269
295
  }
270
296
  try {
271
- await withTimeout(stopAdapter(this.adapter), timeout, "HTTP server shutdown timed out.");
297
+ await withTimeout(stopAdapter(this.adapter, { graceMs: timeout }), timeout + SHUTDOWN_CLOSE_MARGIN_MS, "HTTP server shutdown timed out.");
272
298
  }
273
299
  catch (error) {
274
300
  /*
@@ -277,7 +303,7 @@ export class HttpServer {
277
303
  * rebind the port. Escalate to a forced stop before rethrowing.
278
304
  */
279
305
  try {
280
- await stopAdapter(this.adapter);
306
+ await stopAdapter(this.adapter, { graceMs: 0 });
281
307
  }
282
308
  catch {
283
309
  /* The original timeout is the more useful error. */
@@ -23,10 +23,13 @@ export type TrustProxyPredicate = (value: string, index: number) => boolean;
23
23
  * - `"loopback"`, `"linklocal"`, `"uniquelocal"` / `"private"` — named ranges.
24
24
  * - an IP address, a CIDR range, a comma-separated list of either, or an array
25
25
  * of the same.
26
+ * - a hop count `n`: trust the `n` hops nearest the server (the socket peer
27
+ * is hop `0`), as Express/`proxy-addr` do. `0` trusts nothing.
26
28
  * - a custom predicate `(address, hopIndexFromPeer) => boolean`.
27
29
  *
28
- * Throws `TypeError` on a string that is none of the above rather than
29
- * returning a predicate that can never match.
30
+ * Throws `TypeError` on a string that is none of the above, or on a hop
31
+ * count that is not a non-negative integer, rather than returning a
32
+ * predicate that can never match.
30
33
  */
31
34
  export declare function compileTrustProxy(trustProxy: TrustProxy): TrustProxyPredicate;
32
35
  //# sourceMappingURL=httpTrustProxy.compilation.d.ts.map