@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
@@ -0,0 +1,23 @@
1
+ /**
2
+ * Proxy request-path containment.
3
+ *
4
+ * A gateway mounts an upstream under `target.pathname` (for example
5
+ * `/public-api`) to expose only that subtree. The path builders used to
6
+ * concatenate the client's path onto the base and collapse `//` only, so
7
+ * `/../admin/keys` or `/%2e%2e/admin/keys` was forwarded as
8
+ * `/public-api/../admin/keys`, which the upstream resolves to `/admin/keys`.
9
+ *
10
+ * @module httpProxy/pathGuard
11
+ */
12
+ /**
13
+ * Whether a request path contains a `.` / `..` segment once percent-encoded
14
+ * dots and separators (`%2e`, `%2f`, `%5c`) and backslashes are taken into
15
+ * account, i.e. whether an upstream might resolve it outside its base.
16
+ */
17
+ export declare function hasProxyDotSegment(path: string): boolean;
18
+ /**
19
+ * Throws a `400 Bad Request` `HttpError` when the client-supplied path could
20
+ * escape the proxy target's base path.
21
+ */
22
+ export declare function assertProxyPathContained(path: string): void;
23
+ //# sourceMappingURL=httpProxy.pathGuard.d.ts.map
@@ -0,0 +1,43 @@
1
+ /**
2
+ * Proxy request-path containment.
3
+ *
4
+ * A gateway mounts an upstream under `target.pathname` (for example
5
+ * `/public-api`) to expose only that subtree. The path builders used to
6
+ * concatenate the client's path onto the base and collapse `//` only, so
7
+ * `/../admin/keys` or `/%2e%2e/admin/keys` was forwarded as
8
+ * `/public-api/../admin/keys`, which the upstream resolves to `/admin/keys`.
9
+ *
10
+ * @module httpProxy/pathGuard
11
+ */
12
+ import { badRequest } from "../httpErrors/factories/httpError.clientError.js";
13
+ const ENCODED_SEPARATORS = /%2e|%2f|%5c/gi;
14
+ function decodeSeparator(match) {
15
+ const code = match.toLowerCase();
16
+ if (code === "%2e") {
17
+ return ".";
18
+ }
19
+ return code === "%2f" ? "/" : "\\";
20
+ }
21
+ /**
22
+ * Whether a request path contains a `.` / `..` segment once percent-encoded
23
+ * dots and separators (`%2e`, `%2f`, `%5c`) and backslashes are taken into
24
+ * account, i.e. whether an upstream might resolve it outside its base.
25
+ */
26
+ export function hasProxyDotSegment(path) {
27
+ const end = path.search(/[?#]/);
28
+ const pathOnly = end === -1 ? path : path.slice(0, end);
29
+ const decoded = pathOnly.replace(ENCODED_SEPARATORS, decodeSeparator);
30
+ return decoded
31
+ .split(/[\\/]/)
32
+ .some((segment) => segment === "." || segment === "..");
33
+ }
34
+ /**
35
+ * Throws a `400 Bad Request` `HttpError` when the client-supplied path could
36
+ * escape the proxy target's base path.
37
+ */
38
+ export function assertProxyPathContained(path) {
39
+ if (hasProxyDotSegment(path)) {
40
+ throw badRequest("Proxy request path contains a dot segment.");
41
+ }
42
+ }
43
+ //# sourceMappingURL=httpProxy.pathGuard.js.map
@@ -5,4 +5,5 @@
5
5
  * and proxy request helpers.
6
6
  */
7
7
  export * from "./http.proxy.js";
8
+ export * from "./httpProxy.pathGuard.js";
8
9
  //# sourceMappingURL=index.d.ts.map
@@ -5,4 +5,5 @@
5
5
  * and proxy request helpers.
6
6
  */
7
7
  export * from "./http.proxy.js";
8
+ export * from "./httpProxy.pathGuard.js";
8
9
  //# sourceMappingURL=index.js.map
@@ -117,6 +117,13 @@ export declare function runWithRequestContext<T>(context: HttpRequestContext, ca
117
117
  export declare function getCurrentRequestContext(): HttpRequestContext | undefined;
118
118
  export declare function assertRequestContext(context: HttpRequestContext | undefined | null): asserts context is HttpRequestContext;
119
119
  export declare function isRequestContext(value: unknown): value is HttpRequestContext;
120
+ /**
121
+ * Returns the canonical path of a request-target. An origin-form target such
122
+ * as `//host/admin` stays a path; it is never parsed as an authority.
123
+ */
120
124
  export declare function getPathname(url: string): string;
125
+ /**
126
+ * Returns the search parameters of a request-target.
127
+ */
121
128
  export declare function getSearchParams(url: string): URLSearchParams;
122
129
  //# sourceMappingURL=httpRequest.context.d.ts.map
@@ -7,6 +7,7 @@
7
7
  * The context is intentionally framework-agnostic so adapters can populate it
8
8
  * from Node.js, Bun, Deno, or another HTTP runtime.
9
9
  */
10
+ import { parseRequestTarget } from "./target/httpRequest.target.js";
10
11
  /* -------------------------------------------------------------------------- */
11
12
  /* Constants */
12
13
  /* -------------------------------------------------------------------------- */
@@ -98,7 +99,11 @@ export class HttpRequestContext {
98
99
  /* Query */
99
100
  /* ------------------------------------------------------------------------ */
100
101
  get query() {
101
- return Object.freeze(Object.fromEntries(this.queryMap));
102
+ const query = Object.create(null);
103
+ for (const [name, value] of this.queryMap) {
104
+ query[name] = value;
105
+ }
106
+ return Object.freeze(query);
102
107
  }
103
108
  hasQuery(name) {
104
109
  return this.queryMap.has(name);
@@ -326,24 +331,18 @@ export function isRequestContext(value) {
326
331
  /* -------------------------------------------------------------------------- */
327
332
  /* URL Helpers */
328
333
  /* -------------------------------------------------------------------------- */
334
+ /**
335
+ * Returns the canonical path of a request-target. An origin-form target such
336
+ * as `//host/admin` stays a path; it is never parsed as an authority.
337
+ */
329
338
  export function getPathname(url) {
330
- try {
331
- const parsed = new URL(url, "http://zudojs.invalid");
332
- return parsed.pathname || "/";
333
- }
334
- catch {
335
- const pathname = url.split("?", 1)[0];
336
- return pathname || "/";
337
- }
339
+ return parseRequestTarget(url).pathname || "/";
338
340
  }
341
+ /**
342
+ * Returns the search parameters of a request-target.
343
+ */
339
344
  export function getSearchParams(url) {
340
- try {
341
- return new URL(url, "http://zudojs.invalid").searchParams;
342
- }
343
- catch {
344
- const query = url.includes("?") ? url.slice(url.indexOf("?") + 1) : "";
345
- return new URLSearchParams(query);
346
- }
345
+ return parseRequestTarget(url).searchParams;
347
346
  }
348
347
  /* -------------------------------------------------------------------------- */
349
348
  /* Internal Helpers */
@@ -5,4 +5,5 @@
5
5
  */
6
6
  export * from "./http.request.js";
7
7
  export * from "./httpRequest.context.js";
8
+ export * from "./target/index.js";
8
9
  //# sourceMappingURL=index.d.ts.map
@@ -5,4 +5,5 @@
5
5
  */
6
6
  export * from "./http.request.js";
7
7
  export * from "./httpRequest.context.js";
8
+ export * from "./target/index.js";
8
9
  //# sourceMappingURL=index.js.map
@@ -0,0 +1,45 @@
1
+ /**
2
+ * Canonical request-target parsing.
3
+ *
4
+ * Every place that derives a path or query from `request.url` (the request
5
+ * context, the router, path-scoped middleware, static files) parses it here,
6
+ * so routing and middleware can never disagree about which path a request
7
+ * addresses.
8
+ *
9
+ * `new URL(target, base)` is the wrong tool for an origin-form target:
10
+ * `//evil/admin` is read as a scheme-relative URL with authority `evil`, so
11
+ * the path silently became `/admin` while `request.url` still said
12
+ * `//evil/admin`. Prefixing the base as a string keeps an origin-form target
13
+ * a path.
14
+ *
15
+ * @module httpRequest/target
16
+ */
17
+ /**
18
+ * Parses a request-target (origin-form, absolute-form or `*`) into a URL.
19
+ *
20
+ * An origin-form target is never parsed as an authority. Unparseable input
21
+ * yields the root URL.
22
+ */
23
+ export declare function parseRequestTarget(target: string): URL;
24
+ /**
25
+ * Returns the canonical path of a request-target.
26
+ */
27
+ export declare function getCanonicalPath(target: string): string;
28
+ /**
29
+ * Explains why a request-target is refused, or returns `undefined` when it is
30
+ * acceptable.
31
+ *
32
+ * Refused: anything that is not origin-form, absolute-form (`http(s)://`) or
33
+ * the asterisk-form `*`; a backslash (WHATWG URL parsing treats it as a
34
+ * separator); and any `.` / `..` segment, plain or percent-encoded
35
+ * (`%2e%2e`). Those segments are resolved away by URL parsing, so a front
36
+ * proxy matching `/admin*` on the raw target and the router dispatching on
37
+ * the resolved path would otherwise disagree about the same request.
38
+ */
39
+ export declare function findRequestTargetViolation(target: string): string | undefined;
40
+ /**
41
+ * Whether a request-target is acceptable (see
42
+ * {@link findRequestTargetViolation}).
43
+ */
44
+ export declare function isCanonicalRequestTarget(target: string): boolean;
45
+ //# sourceMappingURL=httpRequest.target.d.ts.map
@@ -0,0 +1,93 @@
1
+ /**
2
+ * Canonical request-target parsing.
3
+ *
4
+ * Every place that derives a path or query from `request.url` (the request
5
+ * context, the router, path-scoped middleware, static files) parses it here,
6
+ * so routing and middleware can never disagree about which path a request
7
+ * addresses.
8
+ *
9
+ * `new URL(target, base)` is the wrong tool for an origin-form target:
10
+ * `//evil/admin` is read as a scheme-relative URL with authority `evil`, so
11
+ * the path silently became `/admin` while `request.url` still said
12
+ * `//evil/admin`. Prefixing the base as a string keeps an origin-form target
13
+ * a path.
14
+ *
15
+ * @module httpRequest/target
16
+ */
17
+ const TARGET_BASE = "http://zudojs.invalid";
18
+ const ABSOLUTE_FORM = /^https?:\/\//i;
19
+ const ENCODED_DOT = /%2e/gi;
20
+ /**
21
+ * Parses a request-target (origin-form, absolute-form or `*`) into a URL.
22
+ *
23
+ * An origin-form target is never parsed as an authority. Unparseable input
24
+ * yields the root URL.
25
+ */
26
+ export function parseRequestTarget(target) {
27
+ try {
28
+ if (target.startsWith("/")) {
29
+ return new URL(`${TARGET_BASE}${target}`);
30
+ }
31
+ if (ABSOLUTE_FORM.test(target)) {
32
+ return new URL(target);
33
+ }
34
+ return new URL(`${TARGET_BASE}/${target === "*" ? "" : target}`);
35
+ }
36
+ catch {
37
+ return new URL(`${TARGET_BASE}/`);
38
+ }
39
+ }
40
+ /**
41
+ * Returns the canonical path of a request-target.
42
+ */
43
+ export function getCanonicalPath(target) {
44
+ return parseRequestTarget(target).pathname || "/";
45
+ }
46
+ function rawPathOf(target) {
47
+ let path = target;
48
+ if (ABSOLUTE_FORM.test(path)) {
49
+ const afterScheme = path.indexOf("//") + 2;
50
+ const slash = path.indexOf("/", afterScheme);
51
+ path = slash === -1 ? "/" : path.slice(slash);
52
+ }
53
+ const end = path.search(/[?#]/);
54
+ return end === -1 ? path : path.slice(0, end);
55
+ }
56
+ /**
57
+ * Explains why a request-target is refused, or returns `undefined` when it is
58
+ * acceptable.
59
+ *
60
+ * Refused: anything that is not origin-form, absolute-form (`http(s)://`) or
61
+ * the asterisk-form `*`; a backslash (WHATWG URL parsing treats it as a
62
+ * separator); and any `.` / `..` segment, plain or percent-encoded
63
+ * (`%2e%2e`). Those segments are resolved away by URL parsing, so a front
64
+ * proxy matching `/admin*` on the raw target and the router dispatching on
65
+ * the resolved path would otherwise disagree about the same request.
66
+ */
67
+ export function findRequestTargetViolation(target) {
68
+ if (target === "*") {
69
+ return undefined;
70
+ }
71
+ if (!target.startsWith("/") && !ABSOLUTE_FORM.test(target)) {
72
+ return "Request target is not in origin-form or absolute-form.";
73
+ }
74
+ const path = rawPathOf(target);
75
+ if (path.includes("\\")) {
76
+ return "Request target contains a backslash.";
77
+ }
78
+ for (const segment of path.split("/")) {
79
+ const decoded = segment.replace(ENCODED_DOT, ".");
80
+ if (decoded === "." || decoded === "..") {
81
+ return "Request target contains a dot segment.";
82
+ }
83
+ }
84
+ return undefined;
85
+ }
86
+ /**
87
+ * Whether a request-target is acceptable (see
88
+ * {@link findRequestTargetViolation}).
89
+ */
90
+ export function isCanonicalRequestTarget(target) {
91
+ return findRequestTargetViolation(target) === undefined;
92
+ }
93
+ //# sourceMappingURL=httpRequest.target.js.map
@@ -0,0 +1,9 @@
1
+ /**
2
+ * @zudojs/http/httpRequest/target
3
+ *
4
+ * Canonical request-target parsing shared by the request context, router and
5
+ * path-scoped middleware, plus the dot-segment / non-origin-form check the
6
+ * Node adapter applies before a request is dispatched.
7
+ */
8
+ export * from "./httpRequest.target.js";
9
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1,9 @@
1
+ /**
2
+ * @zudojs/http/httpRequest/target
3
+ *
4
+ * Canonical request-target parsing shared by the request context, router and
5
+ * path-scoped middleware, plus the dot-segment / non-origin-form check the
6
+ * Node adapter applies before a request is dispatched.
7
+ */
8
+ export * from "./httpRequest.target.js";
9
+ //# sourceMappingURL=index.js.map
@@ -3,6 +3,7 @@
3
3
  *
4
4
  * @module httpResponse/helpers
5
5
  */
6
+ import { withSecureCookieDefaults } from "../httpCookies/httpCookies.defaults.js";
6
7
  import { assertSafeRedirect } from "../httpRedirect/http.redirect.js";
7
8
  /**
8
9
  * Creates a JSON response.
@@ -110,7 +111,7 @@ export function serializeResponseCookie(cookie) {
110
111
  if (!name || !COOKIE_NAME_TOKEN.test(name)) {
111
112
  throw new TypeError(`Invalid cookie name: ${JSON.stringify(name)}`);
112
113
  }
113
- const opts = cookie.options ?? {};
114
+ const opts = withSecureCookieDefaults(cookie.options, "Lax");
114
115
  if (name.startsWith("__Host-")) {
115
116
  if (!opts.secure) {
116
117
  throw new TypeError("A __Host- cookie requires the Secure attribute.");
@@ -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 */
@@ -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
  }