@zudojs/http 1.2.0 → 1.4.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 (127) hide show
  1. package/README.md +221 -0
  2. package/dist/httpAdapter/http.adapters.d.ts +24 -3
  3. package/dist/httpAdapter/http.adapters.js +20 -11
  4. package/dist/httpAdapter/node/httpNode.adapter.d.ts +2 -1
  5. package/dist/httpAdapter/node/httpNode.adapter.js +17 -2
  6. package/dist/httpAdapter/node/httpNode.request.js +7 -1
  7. package/dist/httpAdapter/node/httpNode.type.d.ts +14 -0
  8. package/dist/httpAgent/http.agent.d.ts +22 -3
  9. package/dist/httpAgent/http.agent.js +52 -14
  10. package/dist/httpCacheControl/httpCacheControl.freshness.d.ts +7 -1
  11. package/dist/httpCacheControl/httpCacheControl.freshness.js +30 -3
  12. package/dist/httpClient/httpClient.retry.d.ts +17 -12
  13. package/dist/httpClient/httpClient.retry.js +35 -10
  14. package/dist/httpClient/httpClient.type.d.ts +14 -0
  15. package/dist/httpErrors/httpError.base.js +2 -2
  16. package/dist/httpErrors/httpError.util.d.ts +8 -0
  17. package/dist/httpErrors/httpError.util.js +12 -0
  18. package/dist/httpFetchMount/httpFetchMount.core.d.ts +25 -0
  19. package/dist/httpFetchMount/httpFetchMount.core.js +84 -0
  20. package/dist/httpFetchMount/httpFetchMount.request.d.ts +21 -0
  21. package/dist/httpFetchMount/httpFetchMount.request.js +100 -0
  22. package/dist/httpFetchMount/httpFetchMount.type.d.ts +56 -0
  23. package/dist/httpFetchMount/httpFetchMount.type.js +5 -0
  24. package/dist/httpFetchMount/index.d.ts +11 -0
  25. package/dist/httpFetchMount/index.js +10 -0
  26. package/dist/httpKeepAlive/httpKeepAlive.core.js +14 -2
  27. package/dist/httpMiddleware/builtin/logging/httpMiddleware.logging.d.ts +18 -0
  28. package/dist/httpMiddleware/builtin/logging/httpMiddleware.logging.js +17 -1
  29. package/dist/httpMiddleware/builtin/rateLimit/httpMiddleware.rateLimit.d.ts +6 -3
  30. package/dist/httpMiddleware/builtin/rateLimit/httpMiddleware.rateLimit.js +34 -6
  31. package/dist/httpMiddleware/builtin/security/httpMiddleware.security.d.ts +11 -0
  32. package/dist/httpMiddleware/builtin/security/httpMiddleware.security.js +25 -12
  33. package/dist/httpMiddleware/httpMiddleware.type.d.ts +9 -1
  34. package/dist/httpMiddleware/pipeline/httpPipeline.execution.js +22 -45
  35. package/dist/httpMiddleware/pipeline/httpPipeline.guardResponse.d.ts +36 -0
  36. package/dist/httpMiddleware/pipeline/httpPipeline.guardResponse.js +57 -0
  37. package/dist/httpMiddleware/pipeline/httpPipeline.helper.d.ts +2 -1
  38. package/dist/httpMiddleware/pipeline/httpPipeline.helper.js +10 -0
  39. package/dist/httpMiddleware/pipeline/index.d.ts +1 -0
  40. package/dist/httpMiddleware/pipeline/index.js +1 -0
  41. package/dist/httpNegotiation/httpNegotiation.core.d.ts +13 -0
  42. package/dist/httpNegotiation/httpNegotiation.core.js +60 -8
  43. package/dist/httpOpenApi/httpOpenApi.document.d.ts +44 -0
  44. package/dist/httpOpenApi/httpOpenApi.document.js +61 -0
  45. package/dist/httpOpenApi/httpOpenApi.mount.d.ts +31 -0
  46. package/dist/httpOpenApi/httpOpenApi.mount.js +58 -0
  47. package/dist/httpOpenApi/httpOpenApi.type.d.ts +54 -0
  48. package/dist/httpOpenApi/httpOpenApi.type.js +5 -0
  49. package/dist/httpOpenApi/index.d.ts +13 -0
  50. package/dist/httpOpenApi/index.js +12 -0
  51. package/dist/httpOpenApi/routeTable/index.d.ts +11 -0
  52. package/dist/httpOpenApi/routeTable/index.js +11 -0
  53. package/dist/httpOpenApi/routeTable/routeTable.collect.d.ts +19 -0
  54. package/dist/httpOpenApi/routeTable/routeTable.collect.js +89 -0
  55. package/dist/httpOpenApi/routeTable/routeTable.merge.d.ts +15 -0
  56. package/dist/httpOpenApi/routeTable/routeTable.merge.js +37 -0
  57. package/dist/httpOpenApi/routeTable/routeTable.template.d.ts +30 -0
  58. package/dist/httpOpenApi/routeTable/routeTable.template.js +67 -0
  59. package/dist/httpProxy/http.proxy.d.ts +9 -0
  60. package/dist/httpProxy/http.proxy.js +35 -3
  61. package/dist/httpQuery/index.d.ts +11 -2
  62. package/dist/httpQuery/index.js +11 -2
  63. package/dist/httpQuery/queryParse/index.d.ts +10 -0
  64. package/dist/httpQuery/queryParse/index.js +10 -0
  65. package/dist/httpQuery/queryParse/queryParse.flat.d.ts +13 -0
  66. package/dist/httpQuery/queryParse/queryParse.flat.js +44 -0
  67. package/dist/httpQuery/queryParse/queryParse.nested.d.ts +25 -0
  68. package/dist/httpQuery/queryParse/queryParse.nested.js +112 -0
  69. package/dist/httpQuery/queryParse/queryParse.tokenizer.d.ts +37 -0
  70. package/dist/httpQuery/queryParse/queryParse.tokenizer.js +95 -0
  71. package/dist/httpQuery/queryRequest/index.d.ts +9 -0
  72. package/dist/httpQuery/queryRequest/index.js +9 -0
  73. package/dist/httpQuery/queryRequest/query.request.d.ts +43 -0
  74. package/dist/httpQuery/queryRequest/query.request.js +96 -0
  75. package/dist/httpQuery/querySerialize/index.d.ts +10 -0
  76. package/dist/httpQuery/querySerialize/index.js +10 -0
  77. package/dist/httpQuery/querySerialize/query.util.d.ts +21 -0
  78. package/dist/httpQuery/querySerialize/query.util.js +67 -0
  79. package/dist/httpQuery/querySerialize/querySerialize.core.d.ts +12 -0
  80. package/dist/httpQuery/querySerialize/querySerialize.core.js +97 -0
  81. package/dist/httpQuery/queryTypes/index.d.ts +11 -0
  82. package/dist/httpQuery/queryTypes/index.js +9 -0
  83. package/dist/httpQuery/queryTypes/query.container.d.ts +16 -0
  84. package/dist/httpQuery/queryTypes/query.container.js +51 -0
  85. package/dist/httpQuery/queryTypes/query.limit.d.ts +25 -0
  86. package/dist/httpQuery/queryTypes/query.limit.js +32 -0
  87. package/dist/httpQuery/queryTypes/query.type.d.ts +62 -0
  88. package/dist/httpQuery/queryTypes/query.type.js +2 -0
  89. package/dist/httpRedirect/http.redirect.d.ts +6 -0
  90. package/dist/httpRedirect/http.redirect.js +53 -2
  91. package/dist/httpRequest/http.request.d.ts +61 -2
  92. package/dist/httpRequest/http.request.js +86 -35
  93. package/dist/httpRequest/httpRequest.context.d.ts +8 -0
  94. package/dist/httpRequest/httpRequest.context.js +23 -18
  95. package/dist/httpRequest/index.d.ts +1 -0
  96. package/dist/httpRequest/index.js +1 -0
  97. package/dist/httpRequest/requestId/httpRequest.requestId.d.ts +25 -0
  98. package/dist/httpRequest/requestId/httpRequest.requestId.js +34 -0
  99. package/dist/httpRequest/requestId/index.d.ts +7 -0
  100. package/dist/httpRequest/requestId/index.js +7 -0
  101. package/dist/httpRequest/target/httpRequest.target.d.ts +2 -2
  102. package/dist/httpRequest/target/httpRequest.target.js +23 -5
  103. package/dist/httpResponse/httpResponse.writer.js +15 -0
  104. package/dist/httpRouter/core/factory/httpRoute.factory.base.d.ts +24 -4
  105. package/dist/httpRouter/core/factory/httpRoute.factory.base.js +73 -17
  106. package/dist/httpRouter/core/factory/httpRoute.factory.js +3 -3
  107. package/dist/httpRouter/core/group/httpRouterGroup.core.js +13 -0
  108. package/dist/httpRouter/core/register/httpRouter.register.js +19 -21
  109. package/dist/httpRouter/core/types/httpRouter.type.d.ts +37 -1
  110. package/dist/httpRouter/core/util/httpRoute.util.d.ts +55 -0
  111. package/dist/httpRouter/core/util/httpRoute.util.js +101 -4
  112. package/dist/httpRouter/dispatch/httpRoute.dispatcher.d.ts +11 -0
  113. package/dist/httpRouter/dispatch/httpRoute.dispatcher.js +25 -6
  114. package/dist/httpRouter/matching/httpRoute.matcher.core.js +51 -6
  115. package/dist/httpRouter/matching/httpRoute.matcher.d.ts +0 -1
  116. package/dist/httpRouter/matching/httpRoute.matcher.js +64 -26
  117. package/dist/httpRouter/pattern/httpRoute.pattern.parse.d.ts +16 -0
  118. package/dist/httpRouter/pattern/httpRoute.pattern.parse.js +40 -11
  119. package/dist/httpRouter/pattern/index.d.ts +1 -1
  120. package/dist/httpRouter/pattern/index.js +1 -1
  121. package/dist/httpSecurity/httpSecurity.config.js +4 -1
  122. package/dist/httpSecurity/httpSecurity.validator.js +16 -7
  123. package/dist/httpServer/factory/httpServer.factory.d.ts +10 -9
  124. package/dist/httpServer/factory/httpServer.factory.js +8 -0
  125. package/dist/index.d.ts +2 -0
  126. package/dist/index.js +2 -0
  127. package/package.json +11 -8
@@ -0,0 +1,97 @@
1
+ import { DEFAULT_QUERY_MAX_DEPTH, HTTPQueryLimitError, } from "../queryTypes/index.js";
2
+ /**
3
+ * Serializes an object into a query string.
4
+ *
5
+ * Bounded in the same two ways the parser is. Nesting deeper than `maxDepth`
6
+ * and any cycle both throw {@link HTTPQueryLimitError}; previously either one
7
+ * produced a bare `RangeError: Maximum call stack size exceeded` from inside
8
+ * the walk, which no caller could distinguish from an internal fault.
9
+ */
10
+ export function stringifyQuery(query, options = {}) {
11
+ const params = new URLSearchParams();
12
+ appendObjectToSearchParams(params, query, undefined, options.maxDepth ?? DEFAULT_QUERY_MAX_DEPTH, new Set());
13
+ return params.toString();
14
+ }
15
+ export function buildQueryString(query, options = {}) {
16
+ const value = stringifyQuery(query, options);
17
+ return value ? `?${value}` : "";
18
+ }
19
+ /**
20
+ * Walks one object level.
21
+ *
22
+ * `seen` is scoped to the current path — marked on descent, deleted on
23
+ * ascent — so a value that legitimately appears twice in a DAG serializes
24
+ * twice, while a value that contains itself is rejected. A set that is never
25
+ * unmarked would reject the DAG instead.
26
+ */
27
+ function appendObjectToSearchParams(params, object, prefix, remainingDepth, seen) {
28
+ if (remainingDepth <= 0) {
29
+ throw new HTTPQueryLimitError("Query object nesting exceeds the maximum allowed depth.");
30
+ }
31
+ if (seen.has(object)) {
32
+ throw new HTTPQueryLimitError("Query object contains a circular reference.");
33
+ }
34
+ seen.add(object);
35
+ for (const [key, value] of Object.entries(object)) {
36
+ const path = prefix ? `${prefix}[${key}]` : key;
37
+ appendValue(params, value, path, remainingDepth, seen);
38
+ }
39
+ seen.delete(object);
40
+ }
41
+ function appendValue(params, value, path, remainingDepth, seen) {
42
+ if (value === undefined) {
43
+ return;
44
+ }
45
+ if (value === null) {
46
+ params.append(path, "null");
47
+ return;
48
+ }
49
+ if (Array.isArray(value)) {
50
+ if (remainingDepth <= 0) {
51
+ throw new HTTPQueryLimitError("Query object nesting exceeds the maximum allowed depth.");
52
+ }
53
+ if (seen.has(value)) {
54
+ throw new HTTPQueryLimitError("Query object contains a circular reference.");
55
+ }
56
+ seen.add(value);
57
+ for (const item of value) {
58
+ appendValue(params, item, path, remainingDepth - 1, seen);
59
+ }
60
+ seen.delete(value);
61
+ return;
62
+ }
63
+ if (isPlainObject(value)) {
64
+ appendObjectToSearchParams(params, value, path, remainingDepth - 1, seen);
65
+ return;
66
+ }
67
+ params.append(path, serializeQueryPrimitive(value));
68
+ }
69
+ function serializeQueryPrimitive(value) {
70
+ if (value === null) {
71
+ return "null";
72
+ }
73
+ if (value === undefined) {
74
+ return "";
75
+ }
76
+ if (typeof value === "boolean") {
77
+ return value ? "true" : "false";
78
+ }
79
+ if (typeof value === "string") {
80
+ return value;
81
+ }
82
+ if (typeof value === "number" || typeof value === "bigint") {
83
+ return String(value);
84
+ }
85
+ if (value instanceof Date) {
86
+ return value.toISOString();
87
+ }
88
+ return String(value);
89
+ }
90
+ function isPlainObject(value) {
91
+ if (value === null || typeof value !== "object") {
92
+ return false;
93
+ }
94
+ const prototype = Object.getPrototypeOf(value);
95
+ return prototype === Object.prototype || prototype === null;
96
+ }
97
+ //# sourceMappingURL=querySerialize.core.js.map
@@ -0,0 +1,11 @@
1
+ /**
2
+ * @zudojs/http/httpQuery/queryTypes
3
+ *
4
+ * Query value types, parser limits, and the prototype-safe container
5
+ * primitives every other query module builds on.
6
+ */
7
+ export type { QueryLimitOptions, QueryObject, QueryParseOptions, QueryPrimitive, QueryStringParseOptions, QueryStringPrimitive, QueryStringifyOptions, QueryStringValue, QueryValue, } from "./query.type.js";
8
+ export { DEFAULT_QUERY_MAX_DEPTH, DEFAULT_QUERY_MAX_KEY_LENGTH, DEFAULT_QUERY_MAX_KEYS, DEFAULT_QUERY_MAX_TOTAL_LENGTH, DEFAULT_QUERY_MAX_VALUE_LENGTH, HTTPQueryLimitError, resolveLimits, } from "./query.limit.js";
9
+ export type { QueryLimits } from "./query.limit.js";
10
+ export { createQueryContainer, isForbiddenQueryKey, isQueryObject, ownValue, queryValueToString, } from "./query.container.js";
11
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1,9 @@
1
+ /**
2
+ * @zudojs/http/httpQuery/queryTypes
3
+ *
4
+ * Query value types, parser limits, and the prototype-safe container
5
+ * primitives every other query module builds on.
6
+ */
7
+ export { DEFAULT_QUERY_MAX_DEPTH, DEFAULT_QUERY_MAX_KEY_LENGTH, DEFAULT_QUERY_MAX_KEYS, DEFAULT_QUERY_MAX_TOTAL_LENGTH, DEFAULT_QUERY_MAX_VALUE_LENGTH, HTTPQueryLimitError, resolveLimits, } from "./query.limit.js";
8
+ export { createQueryContainer, isForbiddenQueryKey, isQueryObject, ownValue, queryValueToString, } from "./query.container.js";
9
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1,16 @@
1
+ import type { QueryObject, QueryValue } from "./query.type.js";
2
+ export declare function isForbiddenQueryKey(key: string): boolean;
3
+ export declare function createQueryContainer(): Record<string, QueryValue>;
4
+ export declare function ownValue<T>(target: Record<string, T>, key: string): T | undefined;
5
+ export declare function isQueryObject(value: QueryValue | undefined): value is QueryObject;
6
+ /**
7
+ * Stringifies a parsed query value without invoking `String()` on it.
8
+ *
9
+ * Every container this module builds has a `null` prototype, so it inherits
10
+ * neither `toString` nor `valueOf`; `String(container)` throws
11
+ * `TypeError: Cannot convert object to primitive value`. Because the shape of
12
+ * a parsed value is attacker-chosen (`?a[b]=1&a=2` yields an array holding an
13
+ * object), that throw was reachable from any request.
14
+ */
15
+ export declare function queryValueToString(value: QueryValue): string;
16
+ //# sourceMappingURL=query.container.d.ts.map
@@ -0,0 +1,51 @@
1
+ /**
2
+ * Key names that would mutate a prototype chain if assigned to an object.
3
+ *
4
+ * `__proto__` is the direct pollution vector. `constructor` and `prototype`
5
+ * are blocked as defence in depth so no future container type re-opens the
6
+ * hole.
7
+ */
8
+ const FORBIDDEN_QUERY_KEYS = new Set([
9
+ "__proto__",
10
+ "constructor",
11
+ "prototype",
12
+ ]);
13
+ export function isForbiddenQueryKey(key) {
14
+ return FORBIDDEN_QUERY_KEYS.has(key);
15
+ }
16
+ export function createQueryContainer() {
17
+ return Object.create(null);
18
+ }
19
+ export function ownValue(target, key) {
20
+ return Object.prototype.hasOwnProperty.call(target, key)
21
+ ? target[key]
22
+ : undefined;
23
+ }
24
+ export function isQueryObject(value) {
25
+ return value !== null && typeof value === "object" && !Array.isArray(value);
26
+ }
27
+ /**
28
+ * Stringifies a parsed query value without invoking `String()` on it.
29
+ *
30
+ * Every container this module builds has a `null` prototype, so it inherits
31
+ * neither `toString` nor `valueOf`; `String(container)` throws
32
+ * `TypeError: Cannot convert object to primitive value`. Because the shape of
33
+ * a parsed value is attacker-chosen (`?a[b]=1&a=2` yields an array holding an
34
+ * object), that throw was reachable from any request.
35
+ */
36
+ export function queryValueToString(value) {
37
+ if (value === null) {
38
+ return "null";
39
+ }
40
+ if (typeof value === "string") {
41
+ return value;
42
+ }
43
+ if (typeof value === "number" || typeof value === "boolean") {
44
+ return String(value);
45
+ }
46
+ if (Array.isArray(value)) {
47
+ return value.map(queryValueToString).join(",");
48
+ }
49
+ return `[object Object]`;
50
+ }
51
+ //# sourceMappingURL=query.container.js.map
@@ -0,0 +1,25 @@
1
+ import { HttpError } from "@zudojs/errors";
2
+ import type { QueryLimitOptions } from "./query.type.js";
3
+ export declare const DEFAULT_QUERY_MAX_KEYS = 1000;
4
+ export declare const DEFAULT_QUERY_MAX_KEY_LENGTH = 4096;
5
+ export declare const DEFAULT_QUERY_MAX_VALUE_LENGTH = 16384;
6
+ export declare const DEFAULT_QUERY_MAX_TOTAL_LENGTH: number;
7
+ export declare const DEFAULT_QUERY_MAX_DEPTH = 10;
8
+ /**
9
+ * Thrown when a query string exceeds one of the parser's limits.
10
+ *
11
+ * Carries `statusCode: 414` so an error handler can answer with
12
+ * `414 URI Too Long` rather than treating the rejection as an internal fault.
13
+ */
14
+ export declare class HTTPQueryLimitError extends HttpError {
15
+ constructor(message: string);
16
+ }
17
+ /** Resolved form of {@link QueryLimitOptions}, with every default applied. */
18
+ export interface QueryLimits {
19
+ readonly maxKeys: number;
20
+ readonly maxKeyLength: number;
21
+ readonly maxValueLength: number;
22
+ readonly maxTotalLength: number;
23
+ }
24
+ export declare function resolveLimits(options: QueryLimitOptions): QueryLimits;
25
+ //# sourceMappingURL=query.limit.d.ts.map
@@ -0,0 +1,32 @@
1
+ import { HttpError } from "@zudojs/errors";
2
+ export const DEFAULT_QUERY_MAX_KEYS = 1000;
3
+ export const DEFAULT_QUERY_MAX_KEY_LENGTH = 4096;
4
+ export const DEFAULT_QUERY_MAX_VALUE_LENGTH = 16384;
5
+ export const DEFAULT_QUERY_MAX_TOTAL_LENGTH = 1024 * 1024;
6
+ export const DEFAULT_QUERY_MAX_DEPTH = 10;
7
+ /**
8
+ * Thrown when a query string exceeds one of the parser's limits.
9
+ *
10
+ * Carries `statusCode: 414` so an error handler can answer with
11
+ * `414 URI Too Long` rather than treating the rejection as an internal fault.
12
+ */
13
+ export class HTTPQueryLimitError extends HttpError {
14
+ constructor(message) {
15
+ super(message, {
16
+ statusCode: 414,
17
+ code: "HTTP_QUERY_LIMIT",
18
+ expose: true,
19
+ isOperational: true,
20
+ });
21
+ this.name = "HTTPQueryLimitError";
22
+ }
23
+ }
24
+ export function resolveLimits(options) {
25
+ return {
26
+ maxKeys: options.maxKeys ?? DEFAULT_QUERY_MAX_KEYS,
27
+ maxKeyLength: options.maxKeyLength ?? DEFAULT_QUERY_MAX_KEY_LENGTH,
28
+ maxValueLength: options.maxValueLength ?? DEFAULT_QUERY_MAX_VALUE_LENGTH,
29
+ maxTotalLength: options.maxTotalLength ?? DEFAULT_QUERY_MAX_TOTAL_LENGTH,
30
+ };
31
+ }
32
+ //# sourceMappingURL=query.limit.js.map
@@ -0,0 +1,62 @@
1
+ /**
2
+ * Query value shapes produced by {@link parseQuery}.
3
+ *
4
+ * `QueryValue` is recursive on purpose. A query string may address the same
5
+ * name both as a scalar and as a bracket path (`?a[b]=1&a=2`), so an array
6
+ * element can itself be an object. Typing the array as `QueryPrimitive[]`
7
+ * described a shape the parser could not actually produce, and callers that
8
+ * trusted it crashed on the mixed array.
9
+ */
10
+ export type QueryPrimitive = string | number | boolean | null;
11
+ export type QueryValue = QueryPrimitive | QueryValue[] | QueryObject;
12
+ export interface QueryObject {
13
+ readonly [key: string]: QueryValue;
14
+ }
15
+ /**
16
+ * Limits applied to every query string this module parses.
17
+ *
18
+ * All limits are enforced, on both the string and the `URLSearchParams` entry
19
+ * points. Exceeding one throws {@link HTTPQueryLimitError}, which carries a
20
+ * 414 status code so a server layer can map it to a response instead of a 500.
21
+ */
22
+ export interface QueryLimitOptions {
23
+ /** Maximum number of parameters, counted after comma expansion. Default 1000. */
24
+ readonly maxKeys?: number;
25
+ /** Maximum decoded length of a parameter name. Default 4096. */
26
+ readonly maxKeyLength?: number;
27
+ /** Maximum decoded length of a parameter value. Default 16384. */
28
+ readonly maxValueLength?: number;
29
+ /** Maximum length of the whole query string. Default 1 MiB. */
30
+ readonly maxTotalLength?: number;
31
+ }
32
+ export interface QueryParseOptions extends QueryLimitOptions {
33
+ /** Split values on `,` into an array. Default `false`. */
34
+ readonly commaSeparated?: boolean;
35
+ /** Decode `+` as a space. Default `true`. */
36
+ readonly plusAsSpace?: boolean;
37
+ /** Percent-decode names and values. Default `true`. */
38
+ readonly decode?: boolean;
39
+ /** Maximum bracket-path nesting depth. Default 10. */
40
+ readonly maxDepth?: number;
41
+ }
42
+ export type QueryStringPrimitive = string | number | boolean | null | undefined;
43
+ export type QueryStringValue = QueryStringPrimitive | readonly QueryStringPrimitive[];
44
+ export interface QueryStringParseOptions extends QueryLimitOptions {
45
+ /** Decode `+` as a space. Default `true`. */
46
+ readonly decodePlusAsSpace?: boolean;
47
+ /** Keep parameters whose name decodes to the empty string. Default `true`. */
48
+ readonly allowEmptyKeys?: boolean;
49
+ }
50
+ /**
51
+ * Bounds applied when serializing an object back into a query string.
52
+ *
53
+ * The parser is iterative and depth-capped, so it cannot be made to overflow
54
+ * the stack. The serializer walks the same shapes and needs the same bounds:
55
+ * without them a cyclic or deeply nested object threw a bare `RangeError`
56
+ * instead of a typed, catchable failure.
57
+ */
58
+ export interface QueryStringifyOptions {
59
+ /** Maximum nesting depth of the object being serialized. Default 10. */
60
+ readonly maxDepth?: number;
61
+ }
62
+ //# sourceMappingURL=query.type.d.ts.map
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=query.type.js.map
@@ -60,6 +60,12 @@ export declare function getRedirectMethod(statusCode: number, method: string): s
60
60
  export declare function createRedirectPolicy(options?: Partial<RedirectPolicy> | undefined): RedirectPolicy;
61
61
  export declare function canFollowRedirect(fromURL: string | URL, toURL: string | URL, policy: RedirectPolicy): boolean;
62
62
  export declare function resolveRedirectChain(initialURL: string | URL, locations: readonly (string | URL)[], policy?: Partial<RedirectPolicy> | undefined): URL[];
63
+ /**
64
+ * Whether the same location appears twice in a redirect chain.
65
+ *
66
+ * Relative locations are accepted: they are resolved against a fixed base, so
67
+ * `["/a", "/a"]` reports a loop instead of throwing `Invalid URL`.
68
+ */
63
69
  export declare function hasRedirectLoop(locations: readonly (string | URL)[]): boolean;
64
70
  export declare function assertNoRedirectLoop(locations: readonly (string | URL)[]): void;
65
71
  export declare function isSameOrigin(left: string | URL, right: string | URL): boolean;
@@ -202,10 +202,16 @@ export function resolveRedirectChain(initialURL, locations, policy = {}) {
202
202
  /* -------------------------------------------------------------------------- */
203
203
  /* Redirect Loop Detection */
204
204
  /* -------------------------------------------------------------------------- */
205
+ /**
206
+ * Whether the same location appears twice in a redirect chain.
207
+ *
208
+ * Relative locations are accepted: they are resolved against a fixed base, so
209
+ * `["/a", "/a"]` reports a loop instead of throwing `Invalid URL`.
210
+ */
205
211
  export function hasRedirectLoop(locations) {
206
212
  const seen = new Set();
207
213
  for (const location of locations) {
208
- const normalized = normalizeURL(location).href;
214
+ const normalized = redirectIdentity(location);
209
215
  if (seen.has(normalized)) {
210
216
  return true;
211
217
  }
@@ -421,11 +427,56 @@ export function getLocationHeader(headers) {
421
427
  /* -------------------------------------------------------------------------- */
422
428
  /* URL Normalization */
423
429
  /* -------------------------------------------------------------------------- */
430
+ /**
431
+ * The origin a relative `Location` is resolved against.
432
+ *
433
+ * A relative reference is legal under RFC 9110 and is what this module's own
434
+ * `createRedirect` / `formatLocation` emit by default, so the predicates
435
+ * below must answer for one rather than throw `Invalid URL`. `.invalid` is
436
+ * reserved by RFC 6761, so this can never collide with a real origin.
437
+ */
438
+ const RELATIVE_BASE = "http://redirect.invalid";
424
439
  function normalizeURL(value) {
425
440
  if (value instanceof URL) {
426
441
  return new URL(value.href);
427
442
  }
428
- return new URL(value);
443
+ try {
444
+ return new URL(value);
445
+ }
446
+ catch {
447
+ /* Not absolute — resolve it as the relative reference it is. */
448
+ }
449
+ try {
450
+ return new URL(value, RELATIVE_BASE);
451
+ }
452
+ catch {
453
+ return new URL(RELATIVE_BASE);
454
+ }
455
+ }
456
+ /**
457
+ * The identity a location is compared under when detecting a loop.
458
+ *
459
+ * An unparseable value keeps its literal form rather than collapsing onto the
460
+ * base origin, so two different malformed locations are never reported as a
461
+ * loop.
462
+ */
463
+ function redirectIdentity(location) {
464
+ if (location instanceof URL) {
465
+ return location.href;
466
+ }
467
+ const trimmed = location.trim();
468
+ try {
469
+ return new URL(trimmed).href;
470
+ }
471
+ catch {
472
+ /* Fall through to the relative resolution. */
473
+ }
474
+ try {
475
+ return new URL(trimmed, RELATIVE_BASE).href;
476
+ }
477
+ catch {
478
+ return trimmed;
479
+ }
429
480
  }
430
481
  function getEffectivePort(url) {
431
482
  if (url.port) {
@@ -1,4 +1,5 @@
1
1
  import type { IncomingHttpHeaders, IncomingMessage } from "node:http";
2
+ import type { TrustProxy } from "../httpTrustProxy/httpTrustProxy.type.js";
2
3
  import type { HTTPHeaders, HTTPMethod, HTTPParams, HTTPQuery, HTTPRequest } from "../httpTypes/http.types.js";
3
4
  export declare class NodeHTTPHeaders implements HTTPHeaders {
4
5
  private readonly headers;
@@ -20,6 +21,15 @@ export interface HTTPRequestOptions {
20
21
  readonly ip?: string;
21
22
  readonly ips?: readonly string[];
22
23
  readonly signal?: AbortSignal;
24
+ /**
25
+ * Which socket peers may speak for a client through `X-Forwarded-*`.
26
+ *
27
+ * Defaults to `false`: every forwarded header is ignored and the socket
28
+ * peer decides `ip`, `protocol` and `secure`. Set it to the address, CIDR
29
+ * range, preset or predicate matching the proxy in front of this process
30
+ * before `req.ip` may report a forwarded address.
31
+ */
32
+ readonly trustProxy?: TrustProxy;
23
33
  }
24
34
  export declare class NodeHTTPRequest implements HTTPRequest {
25
35
  readonly method: HTTPMethod;
@@ -62,8 +72,57 @@ export declare function createHTTPRequest(request: IncomingMessage, options?: HT
62
72
  export declare function getRequestPath(request: IncomingMessage): string;
63
73
  export declare function getRequestHost(request: IncomingMessage): string;
64
74
  export declare function getHostname(host: string): string;
65
- export declare function getRequestProtocol(request: IncomingMessage): string;
66
- export declare function getRequestIP(request: IncomingMessage): string | undefined;
75
+ /**
76
+ * Resolves the scheme the client used.
77
+ *
78
+ * `X-Forwarded-Proto` is written by whoever opened the socket, so it is read
79
+ * only when that peer is a configured trusted proxy, and only when it names
80
+ * `http` or `https` — a value such as `wss` is discarded rather than
81
+ * propagated. With the default `trustProxy` of `false` the socket's own TLS
82
+ * state is the only input.
83
+ *
84
+ * @param request - The incoming Node request.
85
+ * @param trustProxy - Which peers may speak through `X-Forwarded-Proto`.
86
+ * @returns `"https"` or `"http"`.
87
+ */
88
+ export declare function getRequestProtocol(request: IncomingMessage, trustProxy?: TrustProxy): string;
89
+ /**
90
+ * Resolves the client address.
91
+ *
92
+ * The socket peer is authoritative. `X-Forwarded-For` is consulted only when
93
+ * that peer is a configured trusted proxy, and the chain walk is delegated to
94
+ * `getClientIp` so there is a single implementation of the hop logic. With
95
+ * the default `trustProxy` of `false` the peer address is returned unchanged,
96
+ * which is what an allowlist, per-IP rate limit or audit trail keyed on
97
+ * `req.ip` needs.
98
+ *
99
+ * @param request - The incoming Node request.
100
+ * @param trustProxy - Which peers may speak through `X-Forwarded-For`.
101
+ * @returns The client address, or `undefined` when the socket has none.
102
+ */
103
+ export declare function getRequestIP(request: IncomingMessage, trustProxy?: TrustProxy): string | undefined;
104
+ /**
105
+ * Parses the query component of a request-target into a flat record.
106
+ *
107
+ * Delegates to the hardened `httpQuery` parser, the same one behind the Node
108
+ * adapter's `request.query` and the router's `ctx.query`. This function used
109
+ * to carry its own loop that accumulated into an object literal and read
110
+ * `result[key]` without an own-property check, which had two consequences on
111
+ * fully attacker-controlled input:
112
+ *
113
+ * - `?__proto__=a&__proto__=b` assigned an array through the `__proto__`
114
+ * setter, so the returned query object's prototype became that array. The
115
+ * parameter vanished from its own keys while the object silently gained
116
+ * `length`, `map` and the rest of `Array.prototype`.
117
+ * - `?constructor=x` read the inherited `Object` constructor as the "existing"
118
+ * value and stored it in the result, handing a handler
119
+ * `query.constructor === [Function: Object], "x"]`.
120
+ *
121
+ * It also applied none of the four documented query limits, so a request with
122
+ * 50,000 parameters was parsed in full. Delegating fixes all three, and makes
123
+ * a limit breach throw {@link HTTPQueryLimitError} (414) as it already did on
124
+ * every other request path.
125
+ */
67
126
  export declare function parseQueryString(url: string): HTTPQuery;
68
127
  export declare function normalizeHTTPMethod(method: string | undefined): HTTPMethod;
69
128
  export declare function parseAcceptHeader(value: string): string[];
@@ -1,5 +1,7 @@
1
1
  import { HTTP_HEADERS } from "../httpConstants/http.constants.js";
2
2
  import { InvalidJSONError } from "../httpErrors/httpError.helper.js";
3
+ import { parseQueryString as parseHardenedQueryString } from "../httpQuery/queryParse/index.js";
4
+ import { getClientIp, isTrustedPeer, } from "../httpTrustProxy/httpTrustProxy.helper.js";
3
5
  /* -------------------------------------------------------------------------- */
4
6
  /* Request Headers */
5
7
  /* -------------------------------------------------------------------------- */
@@ -72,7 +74,8 @@ export class NodeHTTPRequest {
72
74
  cachedJSON;
73
75
  jsonParsed = false;
74
76
  constructor(request, options = {}) {
75
- const protocol = getRequestProtocol(request);
77
+ const trustProxy = options.trustProxy ?? false;
78
+ const protocol = getRequestProtocol(request, trustProxy);
76
79
  const host = getRequestHost(request);
77
80
  const path = getRequestPath(request);
78
81
  this.method = normalizeHTTPMethod(request.method);
@@ -84,9 +87,9 @@ export class NodeHTTPRequest {
84
87
  this.params = options.params ?? {};
85
88
  this.protocol = protocol;
86
89
  this.hostname = getHostname(host);
87
- this.ip = options.ip ?? getRequestIP(request);
90
+ this.ip = options.ip ?? getRequestIP(request, trustProxy);
88
91
  this.ips = options.ips;
89
- this.secure = protocol === "https" || protocol === "wss";
92
+ this.secure = protocol === "https";
90
93
  this.rawBody = options.rawBody;
91
94
  this.body = options.body;
92
95
  this.signal = options.signal;
@@ -229,10 +232,41 @@ export function getHostname(host) {
229
232
  /* -------------------------------------------------------------------------- */
230
233
  /* Protocol */
231
234
  /* -------------------------------------------------------------------------- */
232
- export function getRequestProtocol(request) {
233
- const forwarded = request.headers[HTTP_HEADERS.X_FORWARDED_PROTO];
234
- if (typeof forwarded === "string") {
235
- return (forwarded.split(",", 1)[0] ?? "").trim().toLowerCase();
235
+ /** The only schemes a forwarded proto may name. */
236
+ const FORWARDED_PROTOCOLS = Object.freeze(["http", "https"]);
237
+ /**
238
+ * Presents an `IncomingMessage` in the shape `httpTrustProxy` works on, so
239
+ * this path and the Node adapter share one implementation of the hop logic.
240
+ */
241
+ function toProxyRequest(request) {
242
+ return {
243
+ headers: request.headers,
244
+ socket: { remoteAddress: request.socket?.remoteAddress },
245
+ };
246
+ }
247
+ /**
248
+ * Resolves the scheme the client used.
249
+ *
250
+ * `X-Forwarded-Proto` is written by whoever opened the socket, so it is read
251
+ * only when that peer is a configured trusted proxy, and only when it names
252
+ * `http` or `https` — a value such as `wss` is discarded rather than
253
+ * propagated. With the default `trustProxy` of `false` the socket's own TLS
254
+ * state is the only input.
255
+ *
256
+ * @param request - The incoming Node request.
257
+ * @param trustProxy - Which peers may speak through `X-Forwarded-Proto`.
258
+ * @returns `"https"` or `"http"`.
259
+ */
260
+ export function getRequestProtocol(request, trustProxy = false) {
261
+ if (isTrustedPeer(toProxyRequest(request), trustProxy)) {
262
+ const forwarded = request.headers[HTTP_HEADERS.X_FORWARDED_PROTO];
263
+ const value = Array.isArray(forwarded) ? forwarded[0] : forwarded;
264
+ if (typeof value === "string") {
265
+ const proto = (value.split(",", 1)[0] ?? "").trim().toLowerCase();
266
+ if (FORWARDED_PROTOCOLS.includes(proto)) {
267
+ return proto;
268
+ }
269
+ }
236
270
  }
237
271
  if ("encrypted" in request.socket &&
238
272
  request.socket.encrypted) {
@@ -243,43 +277,60 @@ export function getRequestProtocol(request) {
243
277
  /* -------------------------------------------------------------------------- */
244
278
  /* IP */
245
279
  /* -------------------------------------------------------------------------- */
246
- export function getRequestIP(request) {
247
- const forwarded = request.headers[HTTP_HEADERS.X_FORWARDED_FOR];
248
- if (typeof forwarded === "string") {
249
- const first = forwarded.split(",", 1)[0]?.trim();
250
- if (first) {
251
- return first;
252
- }
280
+ /**
281
+ * Resolves the client address.
282
+ *
283
+ * The socket peer is authoritative. `X-Forwarded-For` is consulted only when
284
+ * that peer is a configured trusted proxy, and the chain walk is delegated to
285
+ * `getClientIp` so there is a single implementation of the hop logic. With
286
+ * the default `trustProxy` of `false` the peer address is returned unchanged,
287
+ * which is what an allowlist, per-IP rate limit or audit trail keyed on
288
+ * `req.ip` needs.
289
+ *
290
+ * @param request - The incoming Node request.
291
+ * @param trustProxy - Which peers may speak through `X-Forwarded-For`.
292
+ * @returns The client address, or `undefined` when the socket has none.
293
+ */
294
+ export function getRequestIP(request, trustProxy = false) {
295
+ const proxyRequest = toProxyRequest(request);
296
+ const peer = proxyRequest.socket?.remoteAddress;
297
+ if (!isTrustedPeer(proxyRequest, trustProxy)) {
298
+ return peer ?? undefined;
253
299
  }
254
- return request.socket.remoteAddress ?? undefined;
300
+ return getClientIp(proxyRequest, trustProxy) ?? peer ?? undefined;
255
301
  }
256
302
  /* -------------------------------------------------------------------------- */
257
303
  /* Query */
258
304
  /* -------------------------------------------------------------------------- */
305
+ /**
306
+ * Parses the query component of a request-target into a flat record.
307
+ *
308
+ * Delegates to the hardened `httpQuery` parser, the same one behind the Node
309
+ * adapter's `request.query` and the router's `ctx.query`. This function used
310
+ * to carry its own loop that accumulated into an object literal and read
311
+ * `result[key]` without an own-property check, which had two consequences on
312
+ * fully attacker-controlled input:
313
+ *
314
+ * - `?__proto__=a&__proto__=b` assigned an array through the `__proto__`
315
+ * setter, so the returned query object's prototype became that array. The
316
+ * parameter vanished from its own keys while the object silently gained
317
+ * `length`, `map` and the rest of `Array.prototype`.
318
+ * - `?constructor=x` read the inherited `Object` constructor as the "existing"
319
+ * value and stored it in the result, handing a handler
320
+ * `query.constructor === [Function: Object], "x"]`.
321
+ *
322
+ * It also applied none of the four documented query limits, so a request with
323
+ * 50,000 parameters was parsed in full. Delegating fixes all three, and makes
324
+ * a limit breach throw {@link HTTPQueryLimitError} (414) as it already did on
325
+ * every other request path.
326
+ */
259
327
  export function parseQueryString(url) {
260
328
  const queryIndex = url.indexOf("?");
261
329
  if (queryIndex < 0) {
262
- return {};
263
- }
264
- const queryString = url.slice(queryIndex + 1);
265
- if (!queryString) {
266
- return {};
267
- }
268
- const searchParams = new URLSearchParams(queryString);
269
- const result = {};
270
- for (const [key, value] of searchParams.entries()) {
271
- const existing = result[key];
272
- if (existing === undefined) {
273
- result[key] = value;
274
- continue;
275
- }
276
- if (Array.isArray(existing)) {
277
- result[key] = [...existing, value];
278
- continue;
279
- }
280
- result[key] = [existing, value];
330
+ return parseHardenedQueryString(undefined);
281
331
  }
282
- return result;
332
+ const hashIndex = url.indexOf("#", queryIndex + 1);
333
+ return parseHardenedQueryString(url.slice(queryIndex + 1, hashIndex === -1 ? undefined : hashIndex));
283
334
  }
284
335
  /* -------------------------------------------------------------------------- */
285
336
  /* Method */
@@ -66,7 +66,15 @@ export declare class HttpRequestContext {
66
66
  private protocolValue;
67
67
  private hostnameValue;
68
68
  private portValue;
69
+ private readonly signalValue;
69
70
  constructor(init: RequestContextInit);
71
+ /**
72
+ * Aborts when the request is abandoned — for the Node adapter, when the
73
+ * client disconnects before the response finished. `RequestContextInit`
74
+ * always accepted a `signal`, but the constructor dropped it, so the
75
+ * router handed every handler a signal that could never fire.
76
+ */
77
+ get signal(): AbortSignal | undefined;
70
78
  get headers(): RequestHeaders;
71
79
  hasHeader(name: string): boolean;
72
80
  getHeader(name: string): string | undefined;