@zudojs/http 1.1.0 → 1.3.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 (137) 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.d.ts +24 -3
  5. package/dist/httpAdapter/http.adapters.js +24 -45
  6. package/dist/httpAdapter/httpAdapter.errorResponse.d.ts +2 -2
  7. package/dist/httpAdapter/httpAdapter.errorResponse.js +26 -9
  8. package/dist/httpAdapter/httpAdapter.logger.d.ts +19 -0
  9. package/dist/httpAdapter/httpAdapter.logger.js +21 -0
  10. package/dist/httpAdapter/node/httpNode.adapter.d.ts +12 -1
  11. package/dist/httpAdapter/node/httpNode.adapter.js +28 -14
  12. package/dist/httpAdapter/node/httpNode.request.d.ts +8 -7
  13. package/dist/httpAdapter/node/httpNode.request.js +17 -27
  14. package/dist/httpAdapter/node/httpNode.server.d.ts +6 -0
  15. package/dist/httpAdapter/node/httpNode.server.js +6 -0
  16. package/dist/httpAdapter/node/httpNode.type.d.ts +2 -1
  17. package/dist/httpAgent/http.agent.d.ts +22 -3
  18. package/dist/httpAgent/http.agent.js +52 -14
  19. package/dist/httpCacheControl/httpCacheControl.freshness.d.ts +7 -1
  20. package/dist/httpCacheControl/httpCacheControl.freshness.js +30 -3
  21. package/dist/httpCookies/http.cookies.d.ts +29 -2
  22. package/dist/httpCookies/http.cookies.js +44 -24
  23. package/dist/httpCookies/httpCookies.defaults.d.ts +40 -0
  24. package/dist/httpCookies/httpCookies.defaults.js +42 -0
  25. package/dist/httpCookies/index.d.ts +1 -0
  26. package/dist/httpCookies/index.js +1 -0
  27. package/dist/httpKeepAlive/httpKeepAlive.core.js +14 -2
  28. package/dist/httpMiddleware/builtin/conditional/httpMiddleware.conditional.d.ts +22 -1
  29. package/dist/httpMiddleware/builtin/conditional/httpMiddleware.conditional.js +19 -4
  30. package/dist/httpMiddleware/builtin/conditional/index.d.ts +1 -0
  31. package/dist/httpMiddleware/builtin/cors/httpMiddleware.cors.d.ts +8 -0
  32. package/dist/httpMiddleware/builtin/cors/httpMiddleware.cors.js +25 -22
  33. package/dist/httpMiddleware/builtin/helpers/httpMiddleware.accessor.js +2 -6
  34. package/dist/httpMiddleware/builtin/helpers/httpMiddleware.media.d.ts +33 -0
  35. package/dist/httpMiddleware/builtin/helpers/httpMiddleware.media.js +59 -0
  36. package/dist/httpMiddleware/builtin/helpers/index.d.ts +1 -0
  37. package/dist/httpMiddleware/builtin/helpers/index.js +1 -0
  38. package/dist/httpMiddleware/builtin/image/httpMiddleware.image.d.ts +13 -0
  39. package/dist/httpMiddleware/builtin/image/httpMiddleware.image.js +31 -53
  40. package/dist/httpMiddleware/builtin/index.d.ts +1 -0
  41. package/dist/httpMiddleware/builtin/index.js +1 -0
  42. package/dist/httpMiddleware/builtin/logging/httpMiddleware.logging.d.ts +18 -0
  43. package/dist/httpMiddleware/builtin/logging/httpMiddleware.logging.js +17 -1
  44. package/dist/httpMiddleware/builtin/rateLimit/httpMiddleware.rateLimit.d.ts +47 -0
  45. package/dist/httpMiddleware/builtin/rateLimit/httpMiddleware.rateLimit.js +61 -0
  46. package/dist/httpMiddleware/builtin/rateLimit/index.d.ts +7 -0
  47. package/dist/httpMiddleware/builtin/rateLimit/index.js +7 -0
  48. package/dist/httpMiddleware/builtin/security/httpMiddleware.security.d.ts +11 -0
  49. package/dist/httpMiddleware/builtin/security/httpMiddleware.security.js +25 -12
  50. package/dist/httpMiddleware/builtin/static/httpMiddleware.static.js +18 -4
  51. package/dist/httpMiddleware/builtin/video/httpMiddleware.video.d.ts +19 -1
  52. package/dist/httpMiddleware/builtin/video/httpMiddleware.video.js +38 -59
  53. package/dist/httpMiddleware/builtin/video/httpMiddleware.video.options.d.ts +12 -0
  54. package/dist/httpMiddleware/builtin/video/httpMiddleware.video.options.js +31 -0
  55. package/dist/httpMiddleware/httpMiddleware.error.d.ts +5 -31
  56. package/dist/httpMiddleware/httpMiddleware.error.js +5 -53
  57. package/dist/httpNegotiation/httpNegotiation.core.d.ts +13 -0
  58. package/dist/httpNegotiation/httpNegotiation.core.js +60 -8
  59. package/dist/httpProxy/http.proxy.d.ts +16 -0
  60. package/dist/httpProxy/http.proxy.js +44 -3
  61. package/dist/httpProxy/httpProxy.pathGuard.d.ts +23 -0
  62. package/dist/httpProxy/httpProxy.pathGuard.js +43 -0
  63. package/dist/httpProxy/index.d.ts +1 -0
  64. package/dist/httpProxy/index.js +1 -0
  65. package/dist/httpQuery/index.d.ts +11 -2
  66. package/dist/httpQuery/index.js +11 -2
  67. package/dist/httpQuery/queryParse/index.d.ts +10 -0
  68. package/dist/httpQuery/queryParse/index.js +10 -0
  69. package/dist/httpQuery/queryParse/queryParse.flat.d.ts +13 -0
  70. package/dist/httpQuery/queryParse/queryParse.flat.js +44 -0
  71. package/dist/httpQuery/queryParse/queryParse.nested.d.ts +25 -0
  72. package/dist/httpQuery/queryParse/queryParse.nested.js +112 -0
  73. package/dist/httpQuery/queryParse/queryParse.tokenizer.d.ts +37 -0
  74. package/dist/httpQuery/queryParse/queryParse.tokenizer.js +95 -0
  75. package/dist/httpQuery/queryRequest/index.d.ts +9 -0
  76. package/dist/httpQuery/queryRequest/index.js +9 -0
  77. package/dist/httpQuery/queryRequest/query.request.d.ts +43 -0
  78. package/dist/httpQuery/queryRequest/query.request.js +96 -0
  79. package/dist/httpQuery/querySerialize/index.d.ts +10 -0
  80. package/dist/httpQuery/querySerialize/index.js +10 -0
  81. package/dist/httpQuery/querySerialize/query.util.d.ts +21 -0
  82. package/dist/httpQuery/querySerialize/query.util.js +67 -0
  83. package/dist/httpQuery/querySerialize/querySerialize.core.d.ts +12 -0
  84. package/dist/httpQuery/querySerialize/querySerialize.core.js +97 -0
  85. package/dist/httpQuery/queryTypes/index.d.ts +11 -0
  86. package/dist/httpQuery/queryTypes/index.js +9 -0
  87. package/dist/httpQuery/queryTypes/query.container.d.ts +16 -0
  88. package/dist/httpQuery/queryTypes/query.container.js +51 -0
  89. package/dist/httpQuery/queryTypes/query.limit.d.ts +25 -0
  90. package/dist/httpQuery/queryTypes/query.limit.js +32 -0
  91. package/dist/httpQuery/queryTypes/query.type.d.ts +62 -0
  92. package/dist/httpQuery/queryTypes/query.type.js +2 -0
  93. package/dist/httpRedirect/http.redirect.d.ts +6 -0
  94. package/dist/httpRedirect/http.redirect.js +53 -2
  95. package/dist/httpRequest/http.request.d.ts +61 -2
  96. package/dist/httpRequest/http.request.js +86 -35
  97. package/dist/httpRequest/httpRequest.context.d.ts +7 -0
  98. package/dist/httpRequest/httpRequest.context.js +26 -34
  99. package/dist/httpRequest/index.d.ts +1 -0
  100. package/dist/httpRequest/index.js +1 -0
  101. package/dist/httpRequest/target/httpRequest.target.d.ts +45 -0
  102. package/dist/httpRequest/target/httpRequest.target.js +111 -0
  103. package/dist/httpRequest/target/index.d.ts +9 -0
  104. package/dist/httpRequest/target/index.js +9 -0
  105. package/dist/httpResponse/httpResponse.helper.js +2 -1
  106. package/dist/httpRouter/core/factory/httpRoute.factory.base.d.ts +18 -1
  107. package/dist/httpRouter/core/factory/httpRoute.factory.base.js +49 -6
  108. package/dist/httpRouter/core/factory/httpRoute.factory.js +3 -3
  109. package/dist/httpRouter/core/register/httpRouter.register.js +16 -21
  110. package/dist/httpRouter/core/types/httpRouter.type.d.ts +6 -0
  111. package/dist/httpRouter/core/util/httpRoute.util.d.ts +61 -0
  112. package/dist/httpRouter/core/util/httpRoute.util.js +102 -17
  113. package/dist/httpRouter/dispatch/httpRoute.dispatcher.d.ts +11 -0
  114. package/dist/httpRouter/dispatch/httpRoute.dispatcher.js +5 -3
  115. package/dist/httpRouter/matching/httpRoute.matcher.core.js +51 -6
  116. package/dist/httpRouter/matching/httpRoute.matcher.d.ts +0 -1
  117. package/dist/httpRouter/matching/httpRoute.matcher.js +64 -26
  118. package/dist/httpRouter/pattern/httpRoute.pattern.parse.d.ts +16 -0
  119. package/dist/httpRouter/pattern/httpRoute.pattern.parse.js +40 -11
  120. package/dist/httpRouter/pattern/index.d.ts +1 -1
  121. package/dist/httpRouter/pattern/index.js +1 -1
  122. package/dist/httpSecurity/httpSecurity.config.d.ts +6 -0
  123. package/dist/httpSecurity/httpSecurity.config.js +1 -0
  124. package/dist/httpSecurity/httpSecurity.guard.d.ts +7 -9
  125. package/dist/httpSecurity/httpSecurity.guard.js +7 -14
  126. package/dist/httpSecurity/httpSecurity.nodeGuard.d.ts +48 -0
  127. package/dist/httpSecurity/httpSecurity.nodeGuard.js +32 -0
  128. package/dist/httpSecurity/httpSecurity.validator.js +20 -9
  129. package/dist/httpSecurity/index.d.ts +2 -0
  130. package/dist/httpSecurity/index.js +1 -0
  131. package/dist/httpServer/core/httpServer.core.d.ts +11 -0
  132. package/dist/httpServer/core/httpServer.core.js +30 -4
  133. package/dist/httpTrustProxy/httpTrustProxy.compilation.d.ts +5 -2
  134. package/dist/httpTrustProxy/httpTrustProxy.compilation.js +19 -2
  135. package/dist/httpTrustProxy/httpTrustProxy.type.d.ts +3 -1
  136. package/dist/index.js +0 -1
  137. package/package.json +5 -5
@@ -20,6 +20,7 @@ export interface CompiledRoutePath {
20
20
  readonly segments: readonly CompiledSegment[];
21
21
  readonly score: number;
22
22
  readonly strictTrailingSlash: boolean;
23
+ readonly expectsTrailingSlash: boolean;
23
24
  }
24
25
  /**
25
26
  * Compiles a route path into its segments.
@@ -35,4 +36,19 @@ export declare function compileRoute(path: string, strictTrailingSlash: boolean)
35
36
  * Scores compiled segments by specificity.
36
37
  */
37
38
  export declare function scoreSegments(segments: readonly CompiledSegment[]): number;
39
+ /**
40
+ * Compares two compiled patterns by specificity, most specific first.
41
+ *
42
+ * Segments are compared left to right by kind (literal, then parameter, then
43
+ * wildcard), which is how a router is expected to rank patterns. Summing the
44
+ * kinds into one scalar — as this used to — let a longer but entirely
45
+ * parameterised pattern such as `/:p/:q/:r/:s` outrank a literal-anchored
46
+ * `/admin/*rest`, so a request to `/admin/a/b/c` bypassed the admin route and
47
+ * every guard registered on it.
48
+ *
49
+ * @param left - The first pattern's segments.
50
+ * @param right - The second pattern's segments.
51
+ * @returns A negative number when `left` is more specific.
52
+ */
53
+ export declare function compareSegmentSpecificity(left: readonly CompiledSegment[], right: readonly CompiledSegment[]): number;
38
54
  //# sourceMappingURL=httpRoute.pattern.parse.d.ts.map
@@ -13,7 +13,7 @@
13
13
  * - `/assets/*path` trailing wildcard
14
14
  */
15
15
  import { InvalidRoutePatternError } from "../core/error/httpRouter.error.js";
16
- import { splitPath, validateParameterName, } from "../core/util/httpRoute.util.js";
16
+ import { hasTrailingSlash, splitRoutePattern, validateParameterName, } from "../core/util/httpRoute.util.js";
17
17
  const SEGMENT_SCORE_LITERAL = 3;
18
18
  const SEGMENT_SCORE_PARAMETER = 2;
19
19
  const SEGMENT_SCORE_WILDCARD = 1;
@@ -22,7 +22,7 @@ const SEGMENT_SCORE_WILDCARD = 1;
22
22
  */
23
23
  export function compileRouteSegments(path) {
24
24
  const segments = [];
25
- const parts = splitPath(path);
25
+ const parts = splitRoutePattern(path);
26
26
  for (const part of parts) {
27
27
  if (part.startsWith(":")) {
28
28
  segments.push(parseParameter(part, path));
@@ -56,6 +56,7 @@ export function compileRoute(path, strictTrailingSlash) {
56
56
  segments,
57
57
  score: scoreSegments(segments),
58
58
  strictTrailingSlash,
59
+ expectsTrailingSlash: hasTrailingSlash(path),
59
60
  });
60
61
  }
61
62
  /**
@@ -64,18 +65,46 @@ export function compileRoute(path, strictTrailingSlash) {
64
65
  export function scoreSegments(segments) {
65
66
  let score = 0;
66
67
  for (const segment of segments) {
67
- if (segment.type === "literal") {
68
- score += SEGMENT_SCORE_LITERAL;
69
- }
70
- else if (segment.type === "parameter") {
71
- score += SEGMENT_SCORE_PARAMETER;
72
- }
73
- else {
74
- score += SEGMENT_SCORE_WILDCARD;
75
- }
68
+ score += segmentScore(segment);
76
69
  }
77
70
  return score;
78
71
  }
72
+ function segmentScore(segment) {
73
+ if (segment === undefined) {
74
+ return 0;
75
+ }
76
+ if (segment.type === "literal") {
77
+ return SEGMENT_SCORE_LITERAL;
78
+ }
79
+ if (segment.type === "parameter") {
80
+ return SEGMENT_SCORE_PARAMETER;
81
+ }
82
+ return SEGMENT_SCORE_WILDCARD;
83
+ }
84
+ /**
85
+ * Compares two compiled patterns by specificity, most specific first.
86
+ *
87
+ * Segments are compared left to right by kind (literal, then parameter, then
88
+ * wildcard), which is how a router is expected to rank patterns. Summing the
89
+ * kinds into one scalar — as this used to — let a longer but entirely
90
+ * parameterised pattern such as `/:p/:q/:r/:s` outrank a literal-anchored
91
+ * `/admin/*rest`, so a request to `/admin/a/b/c` bypassed the admin route and
92
+ * every guard registered on it.
93
+ *
94
+ * @param left - The first pattern's segments.
95
+ * @param right - The second pattern's segments.
96
+ * @returns A negative number when `left` is more specific.
97
+ */
98
+ export function compareSegmentSpecificity(left, right) {
99
+ const length = Math.max(left.length, right.length);
100
+ for (let index = 0; index < length; index += 1) {
101
+ const difference = segmentScore(right[index]) - segmentScore(left[index]);
102
+ if (difference !== 0) {
103
+ return difference;
104
+ }
105
+ }
106
+ return 0;
107
+ }
79
108
  function parseParameter(segment, path) {
80
109
  let value = segment.slice(1);
81
110
  let optional = false;
@@ -14,6 +14,6 @@ export { parseSegments } from "./core/httpPattern.segmentParsing.js";
14
14
  export { buildRegex } from "./core/httpPattern.regexBuilding.js";
15
15
  export { testRoutePattern, matchRoutePattern, matchRoutePatterns, testRoutePatterns, } from "./core/httpPattern.matching.js";
16
16
  export { createRoutePattern, createStrictRoutePattern, createCaseInsensitiveRoutePattern, } from "./httpPattern.factory.js";
17
- export { compileRoute, compileRouteSegments, scoreSegments, } from "./httpRoute.pattern.parse.js";
17
+ export { compileRoute, compileRouteSegments, scoreSegments, compareSegmentSpecificity, } from "./httpRoute.pattern.parse.js";
18
18
  export type { CompiledRoutePath } from "./httpRoute.pattern.parse.js";
19
19
  //# sourceMappingURL=index.d.ts.map
@@ -13,5 +13,5 @@ export { parseSegments } from "./core/httpPattern.segmentParsing.js";
13
13
  export { buildRegex } from "./core/httpPattern.regexBuilding.js";
14
14
  export { testRoutePattern, matchRoutePattern, matchRoutePatterns, testRoutePatterns, } from "./core/httpPattern.matching.js";
15
15
  export { createRoutePattern, createStrictRoutePattern, createCaseInsensitiveRoutePattern, } from "./httpPattern.factory.js";
16
- export { compileRoute, compileRouteSegments, scoreSegments, } from "./httpRoute.pattern.parse.js";
16
+ export { compileRoute, compileRouteSegments, scoreSegments, compareSegmentSpecificity, } from "./httpRoute.pattern.parse.js";
17
17
  //# sourceMappingURL=index.js.map
@@ -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
@@ -20,14 +20,23 @@ export function validateHeaders(headers, config) {
20
20
  if (entries.length > cfg.maxHeaders) {
21
21
  errors.push(`Too many headers: ${entries.length} > ${cfg.maxHeaders}`);
22
22
  }
23
+ /*
24
+ * `IncomingMessage.headers` returns an array for `set-cookie` and for any
25
+ * header a non-Node caller supplies as a list. Checking only `typeof value
26
+ * === "string"` let every one of those values past the size cap and the
27
+ * CRLF filter while the guard still reported `allowed: true`.
28
+ */
23
29
  for (const [key, value] of entries) {
24
- if (typeof value === "string" && value.length > cfg.maxHeaderValueSize) {
25
- errors.push(`Header "${key}" value too large: ${value.length} > ${cfg.maxHeaderValueSize}`);
26
- }
27
- if (cfg.enableCrlfProtection &&
28
- typeof value === "string" &&
29
- /[\r\n]/.test(value)) {
30
- errors.push(`Header "${key}" contains CRLF characters`);
30
+ for (const element of Array.isArray(value) ? value : [value]) {
31
+ if (typeof element !== "string") {
32
+ continue;
33
+ }
34
+ if (element.length > cfg.maxHeaderValueSize) {
35
+ errors.push(`Header "${key}" value too large: ${element.length} > ${cfg.maxHeaderValueSize}`);
36
+ }
37
+ if (cfg.enableCrlfProtection && /[\r\n]/.test(element)) {
38
+ errors.push(`Header "${key}" contains CRLF characters`);
39
+ }
31
40
  }
32
41
  }
33
42
  return { valid: errors.length === 0, errors };
@@ -36,10 +45,12 @@ export function validateHeaders(headers, config) {
36
45
  * Validate the Host header against allowed hosts.
37
46
  */
38
47
  export function validateHost(host, config) {
48
+ const cfg = resolveConfig(config);
39
49
  if (!host) {
40
- return { valid: false, errors: ["Missing Host header"] };
50
+ return cfg.requireHost
51
+ ? { valid: false, errors: ["Missing Host header"] }
52
+ : { valid: true, errors: [] };
41
53
  }
42
- const cfg = resolveConfig(config);
43
54
  if (cfg.allowedHosts.length === 0) {
44
55
  return { valid: true, errors: [] };
45
56
  }
@@ -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
@@ -88,10 +88,13 @@ function buildListPredicate(trustProxy) {
88
88
  * - `"loopback"`, `"linklocal"`, `"uniquelocal"` / `"private"` — named ranges.
89
89
  * - an IP address, a CIDR range, a comma-separated list of either, or an array
90
90
  * of the same.
91
+ * - a hop count `n`: trust the `n` hops nearest the server (the socket peer
92
+ * is hop `0`), as Express/`proxy-addr` do. `0` trusts nothing.
91
93
  * - a custom predicate `(address, hopIndexFromPeer) => boolean`.
92
94
  *
93
- * Throws `TypeError` on a string that is none of the above rather than
94
- * returning a predicate that can never match.
95
+ * Throws `TypeError` on a string that is none of the above, or on a hop
96
+ * count that is not a non-negative integer, rather than returning a
97
+ * predicate that can never match.
95
98
  */
96
99
  export function compileTrustProxy(trustProxy) {
97
100
  if (typeof trustProxy === "function") {
@@ -100,6 +103,9 @@ export function compileTrustProxy(trustProxy) {
100
103
  if (trustProxy === true) {
101
104
  return () => true;
102
105
  }
106
+ if (typeof trustProxy === "number") {
107
+ return compileHopCount(trustProxy);
108
+ }
103
109
  /* Anything that is not a string or a list (including `false`, `undefined`
104
110
  * and any malformed runtime value) trusts nothing. */
105
111
  if (typeof trustProxy !== "string" && !Array.isArray(trustProxy)) {
@@ -116,4 +122,15 @@ export function compileTrustProxy(trustProxy) {
116
122
  compilationCache.set(trustProxy, predicate);
117
123
  return predicate;
118
124
  }
125
+ /**
126
+ * Hop-count trust. The numeric form was typed and documented ("trust N
127
+ * proxies") but fell through to "trust nothing", so behind a load balancer
128
+ * every client shared the balancer's address.
129
+ */
130
+ function compileHopCount(hops) {
131
+ if (!Number.isSafeInteger(hops) || hops < 0) {
132
+ throw new TypeError(`trustProxy hop count must be a non-negative integer, got ${hops}.`);
133
+ }
134
+ return (_address, index) => index < hops;
135
+ }
119
136
  //# sourceMappingURL=httpTrustProxy.compilation.js.map
@@ -3,7 +3,9 @@
3
3
  *
4
4
  * @module httpTrustProxy/types
5
5
  */
6
- export type TrustProxy = boolean | "all" | "linklocal" | "loopback" | string | readonly string[] | ((value: string, index: number) => boolean);
6
+ export type TrustProxy = boolean
7
+ /** Hop count: trust the `n` hops nearest the server (the peer is hop 0). */
8
+ | number | "all" | "linklocal" | "loopback" | string | readonly string[] | ((value: string, index: number) => boolean);
7
9
  export interface TrustProxyOptions {
8
10
  readonly trustProxy?: TrustProxy;
9
11
  readonly maxDepth?: number;
package/dist/index.js CHANGED
@@ -7,7 +7,6 @@
7
7
  * TypeScript reports TS2308 (ambiguous re-exports) for these duplicates.
8
8
  * This is intentional — consumers can import from either path.
9
9
  */
10
- /* eslint-disable @typescript-eslint/no-explicit-any */
11
10
  export * from "./httpProtocol/index.js";
12
11
  export * from "./httpTypes/index.js";
13
12
  export * from "./httpConstants/index.js";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zudojs/http",
3
- "version": "1.1.0",
3
+ "version": "1.3.0",
4
4
  "description": "HTTP primitives, request handling, routing, middleware, and server infrastructure for Zudojs applications.",
5
5
  "license": "MIT",
6
6
  "author": {
@@ -24,10 +24,10 @@
24
24
  "!dist/.tsbuildinfo"
25
25
  ],
26
26
  "dependencies": {
27
- "@zudojs/core": "1.1.0",
28
- "@zudojs/errors": "1.0.1",
29
- "@zudojs/logger": "1.1.0",
30
- "@zudojs/security": "1.0.1"
27
+ "@zudojs/crypto": "1.3.0",
28
+ "@zudojs/errors": "1.2.0",
29
+ "@zudojs/logger": "1.3.0",
30
+ "@zudojs/security": "1.2.0"
31
31
  },
32
32
  "devDependencies": {
33
33
  "@types/node": "^26.4.1",