@zudojs/http 1.2.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 (69) hide show
  1. package/dist/httpAdapter/http.adapters.d.ts +24 -3
  2. package/dist/httpAdapter/http.adapters.js +20 -11
  3. package/dist/httpAdapter/node/httpNode.request.js +1 -1
  4. package/dist/httpAgent/http.agent.d.ts +22 -3
  5. package/dist/httpAgent/http.agent.js +52 -14
  6. package/dist/httpCacheControl/httpCacheControl.freshness.d.ts +7 -1
  7. package/dist/httpCacheControl/httpCacheControl.freshness.js +30 -3
  8. package/dist/httpKeepAlive/httpKeepAlive.core.js +14 -2
  9. package/dist/httpMiddleware/builtin/logging/httpMiddleware.logging.d.ts +18 -0
  10. package/dist/httpMiddleware/builtin/logging/httpMiddleware.logging.js +17 -1
  11. package/dist/httpMiddleware/builtin/security/httpMiddleware.security.d.ts +11 -0
  12. package/dist/httpMiddleware/builtin/security/httpMiddleware.security.js +25 -12
  13. package/dist/httpNegotiation/httpNegotiation.core.d.ts +13 -0
  14. package/dist/httpNegotiation/httpNegotiation.core.js +60 -8
  15. package/dist/httpProxy/http.proxy.d.ts +9 -0
  16. package/dist/httpProxy/http.proxy.js +35 -3
  17. package/dist/httpQuery/index.d.ts +11 -2
  18. package/dist/httpQuery/index.js +11 -2
  19. package/dist/httpQuery/queryParse/index.d.ts +10 -0
  20. package/dist/httpQuery/queryParse/index.js +10 -0
  21. package/dist/httpQuery/queryParse/queryParse.flat.d.ts +13 -0
  22. package/dist/httpQuery/queryParse/queryParse.flat.js +44 -0
  23. package/dist/httpQuery/queryParse/queryParse.nested.d.ts +25 -0
  24. package/dist/httpQuery/queryParse/queryParse.nested.js +112 -0
  25. package/dist/httpQuery/queryParse/queryParse.tokenizer.d.ts +37 -0
  26. package/dist/httpQuery/queryParse/queryParse.tokenizer.js +95 -0
  27. package/dist/httpQuery/queryRequest/index.d.ts +9 -0
  28. package/dist/httpQuery/queryRequest/index.js +9 -0
  29. package/dist/httpQuery/queryRequest/query.request.d.ts +43 -0
  30. package/dist/httpQuery/queryRequest/query.request.js +96 -0
  31. package/dist/httpQuery/querySerialize/index.d.ts +10 -0
  32. package/dist/httpQuery/querySerialize/index.js +10 -0
  33. package/dist/httpQuery/querySerialize/query.util.d.ts +21 -0
  34. package/dist/httpQuery/querySerialize/query.util.js +67 -0
  35. package/dist/httpQuery/querySerialize/querySerialize.core.d.ts +12 -0
  36. package/dist/httpQuery/querySerialize/querySerialize.core.js +97 -0
  37. package/dist/httpQuery/queryTypes/index.d.ts +11 -0
  38. package/dist/httpQuery/queryTypes/index.js +9 -0
  39. package/dist/httpQuery/queryTypes/query.container.d.ts +16 -0
  40. package/dist/httpQuery/queryTypes/query.container.js +51 -0
  41. package/dist/httpQuery/queryTypes/query.limit.d.ts +25 -0
  42. package/dist/httpQuery/queryTypes/query.limit.js +32 -0
  43. package/dist/httpQuery/queryTypes/query.type.d.ts +62 -0
  44. package/dist/httpQuery/queryTypes/query.type.js +2 -0
  45. package/dist/httpRedirect/http.redirect.d.ts +6 -0
  46. package/dist/httpRedirect/http.redirect.js +53 -2
  47. package/dist/httpRequest/http.request.d.ts +61 -2
  48. package/dist/httpRequest/http.request.js +86 -35
  49. package/dist/httpRequest/httpRequest.context.js +11 -18
  50. package/dist/httpRequest/target/httpRequest.target.d.ts +2 -2
  51. package/dist/httpRequest/target/httpRequest.target.js +23 -5
  52. package/dist/httpRouter/core/factory/httpRoute.factory.base.d.ts +18 -1
  53. package/dist/httpRouter/core/factory/httpRoute.factory.base.js +49 -6
  54. package/dist/httpRouter/core/factory/httpRoute.factory.js +3 -3
  55. package/dist/httpRouter/core/register/httpRouter.register.js +16 -21
  56. package/dist/httpRouter/core/types/httpRouter.type.d.ts +6 -0
  57. package/dist/httpRouter/core/util/httpRoute.util.d.ts +47 -0
  58. package/dist/httpRouter/core/util/httpRoute.util.js +85 -4
  59. package/dist/httpRouter/dispatch/httpRoute.dispatcher.d.ts +11 -0
  60. package/dist/httpRouter/dispatch/httpRoute.dispatcher.js +5 -3
  61. package/dist/httpRouter/matching/httpRoute.matcher.core.js +51 -6
  62. package/dist/httpRouter/matching/httpRoute.matcher.d.ts +0 -1
  63. package/dist/httpRouter/matching/httpRoute.matcher.js +64 -26
  64. package/dist/httpRouter/pattern/httpRoute.pattern.parse.d.ts +16 -0
  65. package/dist/httpRouter/pattern/httpRoute.pattern.parse.js +40 -11
  66. package/dist/httpRouter/pattern/index.d.ts +1 -1
  67. package/dist/httpRouter/pattern/index.js +1 -1
  68. package/dist/httpSecurity/httpSecurity.validator.js +16 -7
  69. package/package.json +5 -5
@@ -10,7 +10,7 @@
10
10
  import { appendHeader, deleteHeader, getHeader, setHeader, } from "../httpProtocol/http.protocol.js";
11
11
  import { isValidHTTPURL, isValidHeaderValue } from "../httpValidation/index.js";
12
12
  import { isLinkLocalAddress, isLoopbackAddress, isUniqueLocalAddress, parseIpAddress, } from "../httpTrustProxy/httpTrustProxy.ip.js";
13
- import { assertSafeHeaderValue } from "../httpHeaders/security/index.js";
13
+ import { assertSafeHeaderValue, escapeHeaderQuotedString, } from "../httpHeaders/security/index.js";
14
14
  import { assertProxyPathContained } from "./httpProxy.pathGuard.js";
15
15
  /* -------------------------------------------------------------------------- */
16
16
  /* Target */
@@ -72,6 +72,16 @@ function isBlockedLiteralAddress(hostname) {
72
72
  (address.bytes[1] ?? 0) <= 127) {
73
73
  return true;
74
74
  }
75
+ /* 192.0.0.0/24 IETF protocol assignments (192.0.0.8, 192.0.0.170, …). */
76
+ if (first === 192 &&
77
+ (address.bytes[1] ?? 0) === 0 &&
78
+ (address.bytes[2] ?? 0) === 0) {
79
+ return true;
80
+ }
81
+ /* 198.18.0.0/15 benchmarking. */
82
+ if (first === 198 && ((address.bytes[1] ?? 0) & 0xfe) === 18) {
83
+ return true;
84
+ }
75
85
  return false;
76
86
  }
77
87
  const first = address.bytes[0] ?? 0;
@@ -361,6 +371,15 @@ function appendForwardedValue(headers, name, value) {
361
371
  /* -------------------------------------------------------------------------- */
362
372
  /* Standard Forwarded Header */
363
373
  /* -------------------------------------------------------------------------- */
374
+ /**
375
+ * Builds an RFC 7239 `Forwarded` field value.
376
+ *
377
+ * Every parameter is escaped as a `quoted-string` when it is not a bare
378
+ * token, and the finished value is checked the same way `setForwardedHeaders`
379
+ * checks the values it writes.
380
+ *
381
+ * @throws {TypeError} If any parameter contains a control character.
382
+ */
364
383
  export function createForwardedHeader(address) {
365
384
  const parts = [];
366
385
  if (address.for) {
@@ -375,7 +394,9 @@ export function createForwardedHeader(address) {
375
394
  if (address.protocol) {
376
395
  parts.push(`proto=${formatForwardedValue(address.protocol)}`);
377
396
  }
378
- return parts.join("; ");
397
+ const value = parts.join("; ");
398
+ assertSafeHeaderValue(value);
399
+ return value;
379
400
  }
380
401
  export function parseForwardedHeader(value) {
381
402
  if (!value || value.trim().length === 0) {
@@ -534,11 +555,22 @@ function formatForwardedIdentifier(value) {
534
555
  }
535
556
  return formatForwardedValue(value);
536
557
  }
558
+ /**
559
+ * Emits an RFC 7239 parameter value.
560
+ *
561
+ * Wrapping a value in quotes does not neutralise a CR or LF — the control
562
+ * character survives into the field value and an attacker-chosen header
563
+ * follows it on the wire. `escapeHeaderQuotedString` rejects those characters
564
+ * rather than escaping them, which is what every other quoted-parameter
565
+ * emitter in this package already uses.
566
+ *
567
+ * @throws {TypeError} If the value contains a forbidden control character.
568
+ */
537
569
  function formatForwardedValue(value) {
538
570
  if (/^[A-Za-z0-9._:-]+$/.test(value)) {
539
571
  return value;
540
572
  }
541
- return `"${value.replace(/\\/g, "\\\\").replace(/"/g, '\\"')}"`;
573
+ return `"${escapeHeaderQuotedString(value)}"`;
542
574
  }
543
575
  function unquoteForwardedValue(value) {
544
576
  if (value.length >= 2 && value.startsWith('"') && value.endsWith('"')) {
@@ -1,7 +1,16 @@
1
1
  /**
2
2
  * @zudojs/http/httpQuery
3
3
  *
4
- * HTTP query string parsing and utilities.
4
+ * HTTP query string parsing, request accessors, and serialization.
5
+ *
6
+ * Every parser in this module enforces the same four limits
7
+ * ({@link QueryLimitOptions}) and drops `__proto__`, `constructor` and
8
+ * `prototype`, on both the string and the `URLSearchParams` entry points.
9
+ * Exceeding a limit throws {@link HTTPQueryLimitError}, which carries a 414
10
+ * status code.
5
11
  */
6
- export * from "./http.query.js";
12
+ export * from "./queryTypes/index.js";
13
+ export * from "./queryParse/index.js";
14
+ export * from "./queryRequest/index.js";
15
+ export * from "./querySerialize/index.js";
7
16
  //# sourceMappingURL=index.d.ts.map
@@ -1,7 +1,16 @@
1
1
  /**
2
2
  * @zudojs/http/httpQuery
3
3
  *
4
- * HTTP query string parsing and utilities.
4
+ * HTTP query string parsing, request accessors, and serialization.
5
+ *
6
+ * Every parser in this module enforces the same four limits
7
+ * ({@link QueryLimitOptions}) and drops `__proto__`, `constructor` and
8
+ * `prototype`, on both the string and the `URLSearchParams` entry points.
9
+ * Exceeding a limit throws {@link HTTPQueryLimitError}, which carries a 414
10
+ * status code.
5
11
  */
6
- export * from "./http.query.js";
12
+ export * from "./queryTypes/index.js";
13
+ export * from "./queryParse/index.js";
14
+ export * from "./queryRequest/index.js";
15
+ export * from "./querySerialize/index.js";
7
16
  //# sourceMappingURL=index.js.map
@@ -0,0 +1,10 @@
1
+ /**
2
+ * @zudojs/http/httpQuery/queryParse
3
+ *
4
+ * The query-string tokenizer and the two parsers built on it: a nested,
5
+ * bracket-path parser and a flat one. Both enforce the same limits.
6
+ */
7
+ export { decodeQueryComponent, stripQueryPrefix, tokenizeQuery, tokenizeSearchParams, } from "./queryParse.tokenizer.js";
8
+ export { normalizeQueryValue, parseQuery, parseQueryKey, } from "./queryParse.nested.js";
9
+ export { parseQueryString } from "./queryParse.flat.js";
10
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1,10 @@
1
+ /**
2
+ * @zudojs/http/httpQuery/queryParse
3
+ *
4
+ * The query-string tokenizer and the two parsers built on it: a nested,
5
+ * bracket-path parser and a flat one. Both enforce the same limits.
6
+ */
7
+ export { decodeQueryComponent, stripQueryPrefix, tokenizeQuery, tokenizeSearchParams, } from "./queryParse.tokenizer.js";
8
+ export { normalizeQueryValue, parseQuery, parseQueryKey, } from "./queryParse.nested.js";
9
+ export { parseQueryString } from "./queryParse.flat.js";
10
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1,13 @@
1
+ import type { QueryStringParseOptions } from "../queryTypes/index.js";
2
+ /**
3
+ * Parses a query string into a flat record of decoded values.
4
+ *
5
+ * Unlike {@link parseQuery} this applies no bracket-path nesting and no value
6
+ * coercion: every value is the decoded string, and a repeated name yields an
7
+ * array. Both functions share one tokenizer and one set of limits.
8
+ *
9
+ * The record has a `null` prototype and `__proto__` / `constructor` /
10
+ * `prototype` are dropped, so no parameter name can reach a prototype chain.
11
+ */
12
+ export declare function parseQueryString(input: string | undefined | null, options?: QueryStringParseOptions): Record<string, string | string[]>;
13
+ //# sourceMappingURL=queryParse.flat.d.ts.map
@@ -0,0 +1,44 @@
1
+ import { createQueryContainer, isForbiddenQueryKey, ownValue, resolveLimits, } from "../queryTypes/index.js";
2
+ import { stripQueryPrefix, tokenizeQuery } from "./queryParse.tokenizer.js";
3
+ /**
4
+ * Parses a query string into a flat record of decoded values.
5
+ *
6
+ * Unlike {@link parseQuery} this applies no bracket-path nesting and no value
7
+ * coercion: every value is the decoded string, and a repeated name yields an
8
+ * array. Both functions share one tokenizer and one set of limits.
9
+ *
10
+ * The record has a `null` prototype and `__proto__` / `constructor` /
11
+ * `prototype` are dropped, so no parameter name can reach a prototype chain.
12
+ */
13
+ export function parseQueryString(input, options = {}) {
14
+ const result = createQueryContainer();
15
+ if (input === undefined || input === null || input === "") {
16
+ return result;
17
+ }
18
+ const value = stripQueryPrefix(input);
19
+ if (value.length === 0) {
20
+ return result;
21
+ }
22
+ const allowEmptyKeys = options.allowEmptyKeys ?? true;
23
+ const entries = tokenizeQuery(value, resolveLimits(options), true, options.decodePlusAsSpace !== false, false);
24
+ for (const [key, item] of entries) {
25
+ if (!allowEmptyKeys && key.length === 0) {
26
+ continue;
27
+ }
28
+ if (isForbiddenQueryKey(key)) {
29
+ continue;
30
+ }
31
+ const existing = ownValue(result, key);
32
+ if (existing === undefined) {
33
+ result[key] = item;
34
+ continue;
35
+ }
36
+ if (Array.isArray(existing)) {
37
+ existing.push(item);
38
+ continue;
39
+ }
40
+ result[key] = [existing, item];
41
+ }
42
+ return result;
43
+ }
44
+ //# sourceMappingURL=queryParse.flat.js.map
@@ -0,0 +1,25 @@
1
+ import type { QueryObject, QueryParseOptions, QueryPrimitive } from "../queryTypes/index.js";
2
+ /**
3
+ * Parses a query string into a nested object.
4
+ *
5
+ * The returned objects have a `null` prototype: a parameter named
6
+ * `__proto__` can never reach `Object.prototype`, and a lookup of an
7
+ * unrelated key can never return an inherited member.
8
+ */
9
+ export declare function parseQuery(query: string | URLSearchParams | undefined, options?: QueryParseOptions): QueryObject;
10
+ /**
11
+ * Splits a bracket/dot path such as `a[b][c]` into its segments.
12
+ *
13
+ * A path containing `__proto__`, `constructor` or `prototype` yields an empty
14
+ * array, which callers treat as "drop this parameter entirely".
15
+ */
16
+ export declare function parseQueryKey(key: string): string[];
17
+ /**
18
+ * Coerces the well-known literals and numbers that round-trip exactly.
19
+ *
20
+ * A numeric literal is only converted when `String(Number(value)) === value`,
21
+ * so a 20-digit identifier stays the string it arrived as instead of being
22
+ * silently rewritten to a different number.
23
+ */
24
+ export declare function normalizeQueryValue(value: string): QueryPrimitive;
25
+ //# sourceMappingURL=queryParse.nested.d.ts.map
@@ -0,0 +1,112 @@
1
+ import { createQueryContainer, DEFAULT_QUERY_MAX_DEPTH, HTTPQueryLimitError, isForbiddenQueryKey, isQueryObject, ownValue, resolveLimits, } from "../queryTypes/index.js";
2
+ import { stripQueryPrefix, tokenizeQuery, tokenizeSearchParams, } from "./queryParse.tokenizer.js";
3
+ /**
4
+ * Parses a query string into a nested object.
5
+ *
6
+ * The returned objects have a `null` prototype: a parameter named
7
+ * `__proto__` can never reach `Object.prototype`, and a lookup of an
8
+ * unrelated key can never return an inherited member.
9
+ */
10
+ export function parseQuery(query, options = {}) {
11
+ if (query === undefined) {
12
+ return createQueryContainer();
13
+ }
14
+ const limits = resolveLimits(options);
15
+ const maxDepth = options.maxDepth ?? DEFAULT_QUERY_MAX_DEPTH;
16
+ const commaSeparated = options.commaSeparated === true;
17
+ const entries = query instanceof URLSearchParams
18
+ ? tokenizeSearchParams(query, limits, commaSeparated)
19
+ : tokenizeQuery(stripQueryPrefix(query), limits, options.decode !== false, options.plusAsSpace !== false, commaSeparated);
20
+ const result = createQueryContainer();
21
+ for (const [key, value] of entries) {
22
+ appendQueryValue(result, parseQueryKey(key), value, maxDepth);
23
+ }
24
+ return result;
25
+ }
26
+ /**
27
+ * Splits a bracket/dot path such as `a[b][c]` into its segments.
28
+ *
29
+ * A path containing `__proto__`, `constructor` or `prototype` yields an empty
30
+ * array, which callers treat as "drop this parameter entirely".
31
+ */
32
+ export function parseQueryKey(key) {
33
+ if (!key) {
34
+ return [""];
35
+ }
36
+ const normalized = key.replace(/\]/g, "");
37
+ const segments = normalized.split(/[.[\]]+/).filter((part) => part.length > 0);
38
+ if (segments.some(isForbiddenQueryKey)) {
39
+ return [];
40
+ }
41
+ return segments;
42
+ }
43
+ /**
44
+ * Writes one value into the nested result object.
45
+ *
46
+ * Iterative rather than recursive so a deeply nested key cannot overflow the
47
+ * stack, depth-capped so it cannot allocate without bound, and every level is
48
+ * a null-prototype container read through `hasOwnProperty`.
49
+ */
50
+ function appendQueryValue(target, path, value, maxDepth) {
51
+ if (path.length === 0) {
52
+ return;
53
+ }
54
+ if (path.length > maxDepth) {
55
+ throw new HTTPQueryLimitError("Query parameter nesting exceeds the maximum allowed depth.");
56
+ }
57
+ let current = target;
58
+ for (let index = 0; index < path.length - 1; index += 1) {
59
+ const segment = path[index];
60
+ if (segment === undefined) {
61
+ continue;
62
+ }
63
+ const existing = ownValue(current, segment);
64
+ if (isQueryObject(existing)) {
65
+ current = existing;
66
+ continue;
67
+ }
68
+ const next = createQueryContainer();
69
+ current[segment] = next;
70
+ current = next;
71
+ }
72
+ const leaf = path[path.length - 1];
73
+ if (leaf === undefined) {
74
+ return;
75
+ }
76
+ const existing = ownValue(current, leaf);
77
+ if (existing === undefined) {
78
+ current[leaf] = normalizeQueryValue(value);
79
+ return;
80
+ }
81
+ if (Array.isArray(existing)) {
82
+ existing.push(normalizeQueryValue(value));
83
+ return;
84
+ }
85
+ current[leaf] = [existing, normalizeQueryValue(value)];
86
+ }
87
+ /**
88
+ * Coerces the well-known literals and numbers that round-trip exactly.
89
+ *
90
+ * A numeric literal is only converted when `String(Number(value)) === value`,
91
+ * so a 20-digit identifier stays the string it arrived as instead of being
92
+ * silently rewritten to a different number.
93
+ */
94
+ export function normalizeQueryValue(value) {
95
+ if (value === "null") {
96
+ return null;
97
+ }
98
+ if (value === "true") {
99
+ return true;
100
+ }
101
+ if (value === "false") {
102
+ return false;
103
+ }
104
+ if (/^-?(?:0|[1-9]\d*)(?:\.\d+)?$/.test(value)) {
105
+ const number = Number(value);
106
+ if (Number.isFinite(number) && String(number) === value) {
107
+ return number;
108
+ }
109
+ }
110
+ return value;
111
+ }
112
+ //# sourceMappingURL=queryParse.nested.js.map
@@ -0,0 +1,37 @@
1
+ import type { QueryLimits } from "../queryTypes/index.js";
2
+ /**
3
+ * Percent-decodes a query component.
4
+ *
5
+ * A malformed sequence such as `%ZZ` makes `decodeURIComponent` throw a
6
+ * `URIError`. Any client can send one, so the raw component is returned
7
+ * instead of propagating an unhandled error onto the request path. The raw
8
+ * bytes are never interpolated into an error message.
9
+ */
10
+ export declare function decodeQueryComponent(value: string, decode?: boolean, plusAsSpace?: boolean): string;
11
+ export declare function stripQueryPrefix(input: string): string;
12
+ /**
13
+ * Splits a raw query string into decoded `[name, value]` pairs, enforcing
14
+ * every configured limit as it goes.
15
+ *
16
+ * This is the single tokenizer for the module: {@link parseQuery} and
17
+ * {@link parseQueryString} are both built on it — including their
18
+ * `URLSearchParams` entry points — so the limits and the decoding rules
19
+ * cannot drift apart.
20
+ *
21
+ * `maxKeys` counts *emitted* pairs, not `&`-separated segments. Counting
22
+ * segments let `?a=1,2,3,...` expand past the cap under `commaSeparated`,
23
+ * because the split happened after the check: a single parameter could yield
24
+ * thousands of entries with `maxKeys` set to 1.
25
+ */
26
+ export declare function tokenizeQuery(query: string, limits: QueryLimits, decode: boolean, plusAsSpace: boolean, commaSeparated: boolean): Array<readonly [string, string]>;
27
+ /**
28
+ * Tokenizes a `URLSearchParams` through the same path as a raw string.
29
+ *
30
+ * `toString()` re-encodes, and the tokenizer decodes again, so the round-trip
31
+ * is lossless. Routing it through the tokenizer is what makes `maxTotalLength`
32
+ * and `commaSeparated` apply here at all: the previous bespoke loop checked
33
+ * only the per-key and per-value caps, so `parseQuery(new URLSearchParams(…))`
34
+ * silently ignored two of the four documented limits and the comma option.
35
+ */
36
+ export declare function tokenizeSearchParams(params: URLSearchParams, limits: QueryLimits, commaSeparated: boolean): Array<readonly [string, string]>;
37
+ //# sourceMappingURL=queryParse.tokenizer.d.ts.map
@@ -0,0 +1,95 @@
1
+ import { HTTPQueryLimitError } from "../queryTypes/index.js";
2
+ /**
3
+ * Percent-decodes a query component.
4
+ *
5
+ * A malformed sequence such as `%ZZ` makes `decodeURIComponent` throw a
6
+ * `URIError`. Any client can send one, so the raw component is returned
7
+ * instead of propagating an unhandled error onto the request path. The raw
8
+ * bytes are never interpolated into an error message.
9
+ */
10
+ export function decodeQueryComponent(value, decode = true, plusAsSpace = true) {
11
+ const input = plusAsSpace ? value.replace(/\+/g, " ") : value;
12
+ if (!decode) {
13
+ return input;
14
+ }
15
+ try {
16
+ return decodeURIComponent(input);
17
+ }
18
+ catch {
19
+ return input;
20
+ }
21
+ }
22
+ export function stripQueryPrefix(input) {
23
+ let value = input;
24
+ if (value.startsWith("?")) {
25
+ value = value.slice(1);
26
+ }
27
+ const hashIndex = value.indexOf("#");
28
+ if (hashIndex !== -1) {
29
+ value = value.slice(0, hashIndex);
30
+ }
31
+ return value;
32
+ }
33
+ /**
34
+ * Splits a raw query string into decoded `[name, value]` pairs, enforcing
35
+ * every configured limit as it goes.
36
+ *
37
+ * This is the single tokenizer for the module: {@link parseQuery} and
38
+ * {@link parseQueryString} are both built on it — including their
39
+ * `URLSearchParams` entry points — so the limits and the decoding rules
40
+ * cannot drift apart.
41
+ *
42
+ * `maxKeys` counts *emitted* pairs, not `&`-separated segments. Counting
43
+ * segments let `?a=1,2,3,...` expand past the cap under `commaSeparated`,
44
+ * because the split happened after the check: a single parameter could yield
45
+ * thousands of entries with `maxKeys` set to 1.
46
+ */
47
+ export function tokenizeQuery(query, limits, decode, plusAsSpace, commaSeparated) {
48
+ if (query.length > limits.maxTotalLength) {
49
+ throw new HTTPQueryLimitError("Query string exceeds the maximum allowed length.");
50
+ }
51
+ const entries = [];
52
+ const push = (key, value) => {
53
+ if (entries.length >= limits.maxKeys) {
54
+ throw new HTTPQueryLimitError("Query string contains too many parameters.");
55
+ }
56
+ entries.push([key, value]);
57
+ };
58
+ for (const part of query.split("&")) {
59
+ if (part === "") {
60
+ continue;
61
+ }
62
+ const separator = part.indexOf("=");
63
+ const rawKey = separator === -1 ? part : part.slice(0, separator);
64
+ const rawValue = separator === -1 ? "" : part.slice(separator + 1);
65
+ const key = decodeQueryComponent(rawKey, decode, plusAsSpace);
66
+ if (key.length > limits.maxKeyLength) {
67
+ throw new HTTPQueryLimitError("Query parameter name exceeds the maximum allowed length.");
68
+ }
69
+ const value = decodeQueryComponent(rawValue, decode, plusAsSpace);
70
+ if (value.length > limits.maxValueLength) {
71
+ throw new HTTPQueryLimitError("Query parameter value exceeds the maximum allowed length.");
72
+ }
73
+ if (commaSeparated && value.includes(",")) {
74
+ for (const item of value.split(",")) {
75
+ push(key, item);
76
+ }
77
+ continue;
78
+ }
79
+ push(key, value);
80
+ }
81
+ return entries;
82
+ }
83
+ /**
84
+ * Tokenizes a `URLSearchParams` through the same path as a raw string.
85
+ *
86
+ * `toString()` re-encodes, and the tokenizer decodes again, so the round-trip
87
+ * is lossless. Routing it through the tokenizer is what makes `maxTotalLength`
88
+ * and `commaSeparated` apply here at all: the previous bespoke loop checked
89
+ * only the per-key and per-value caps, so `parseQuery(new URLSearchParams(…))`
90
+ * silently ignored two of the four documented limits and the comma option.
91
+ */
92
+ export function tokenizeSearchParams(params, limits, commaSeparated) {
93
+ return tokenizeQuery(params.toString(), limits, true, true, commaSeparated);
94
+ }
95
+ //# sourceMappingURL=queryParse.tokenizer.js.map
@@ -0,0 +1,9 @@
1
+ /**
2
+ * @zudojs/http/httpQuery/queryRequest
3
+ *
4
+ * Accessors that read the query component of an {@link HTTPRequest}. Every
5
+ * accessor here answers about the same parsed object, so middleware and a
6
+ * handler asking the same question get the same answer.
7
+ */
8
+ export { getQuery, getQueryString, getQueryStrings, getQueryValue, getSearchParams, hasQuery, querySize, } from "./query.request.js";
9
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1,9 @@
1
+ /**
2
+ * @zudojs/http/httpQuery/queryRequest
3
+ *
4
+ * Accessors that read the query component of an {@link HTTPRequest}. Every
5
+ * accessor here answers about the same parsed object, so middleware and a
6
+ * handler asking the same question get the same answer.
7
+ */
8
+ export { getQuery, getQueryString, getQueryStrings, getQueryValue, getSearchParams, hasQuery, querySize, } from "./query.request.js";
9
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1,43 @@
1
+ import type { HTTPRequest } from "../../httpTypes/http.types.js";
2
+ import type { QueryObject, QueryParseOptions, QueryValue } from "../queryTypes/index.js";
3
+ export declare function getQuery(request: HTTPRequest, options?: QueryParseOptions): QueryObject;
4
+ export declare function getQueryValue(request: HTTPRequest, key: string): QueryValue | undefined;
5
+ /**
6
+ * Returns a single parameter as a string.
7
+ *
8
+ * A parameter that appeared more than once, or as a bracket path, has no
9
+ * single string form and yields `undefined`; use {@link getQueryStrings} for
10
+ * those. A literal `?a=null` yields `"null"` — the text the client sent.
11
+ * This used to return `null` while declaring `string | undefined`, so a
12
+ * caller doing `value.length` on a non-`undefined` result crashed.
13
+ */
14
+ export declare function getQueryString(request: HTTPRequest, key: string): string | undefined;
15
+ /**
16
+ * Returns every value a parameter carries, as strings.
17
+ *
18
+ * The parsed shape is attacker-chosen: `?a[b]=1&a=2` puts a null-prototype
19
+ * object inside the array for `a`, and `String()` on such an object throws
20
+ * `TypeError: Cannot convert object to primitive value`. Stringifying through
21
+ * {@link queryValueToString} keeps this total for every reachable input.
22
+ */
23
+ export declare function getQueryStrings(request: HTTPRequest, key: string): string[];
24
+ /**
25
+ * Returns the raw search parameters of a request-target.
26
+ *
27
+ * This is the undecoded, unnested view. It applies none of the parser's
28
+ * limits and drops no key, so prefer {@link getQuery} for anything that
29
+ * reaches application code.
30
+ */
31
+ export declare function getSearchParams(request: HTTPRequest): URLSearchParams;
32
+ /**
33
+ * Reports whether a parameter is present in the parsed query.
34
+ *
35
+ * Answers about the same object {@link getQuery} returns, not about the raw
36
+ * search params. The two used to disagree: `hasQuery(req, "a")` was `false`
37
+ * for `?a[b]=1` (which `getQuery` exposes as `a`), and
38
+ * `hasQuery(req, "__proto__")` was `true` for a key the parser drops.
39
+ */
40
+ export declare function hasQuery(request: HTTPRequest, key: string): boolean;
41
+ /** Number of distinct parameter names in the parsed query. */
42
+ export declare function querySize(request: HTTPRequest): number;
43
+ //# sourceMappingURL=query.request.d.ts.map
@@ -0,0 +1,96 @@
1
+ import { createQueryContainer, ownValue, queryValueToString, } from "../queryTypes/index.js";
2
+ import { parseQuery } from "../queryParse/index.js";
3
+ /**
4
+ * Extracts the query component of a request-target.
5
+ *
6
+ * The fragment is stripped: a proxy or a client that leaves `#…` on the
7
+ * request line must not turn it into a parameter.
8
+ */
9
+ function queryComponent(url) {
10
+ if (!url) {
11
+ return undefined;
12
+ }
13
+ const queryIndex = url.indexOf("?");
14
+ if (queryIndex === -1) {
15
+ return undefined;
16
+ }
17
+ const hashIndex = url.indexOf("#", queryIndex + 1);
18
+ return url.slice(queryIndex + 1, hashIndex === -1 ? undefined : hashIndex);
19
+ }
20
+ export function getQuery(request, options = {}) {
21
+ const query = queryComponent(request.url);
22
+ if (query === undefined) {
23
+ return createQueryContainer();
24
+ }
25
+ return parseQuery(query, options);
26
+ }
27
+ export function getQueryValue(request, key) {
28
+ return ownValue(getQuery(request), key);
29
+ }
30
+ /**
31
+ * Returns a single parameter as a string.
32
+ *
33
+ * A parameter that appeared more than once, or as a bracket path, has no
34
+ * single string form and yields `undefined`; use {@link getQueryStrings} for
35
+ * those. A literal `?a=null` yields `"null"` — the text the client sent.
36
+ * This used to return `null` while declaring `string | undefined`, so a
37
+ * caller doing `value.length` on a non-`undefined` result crashed.
38
+ */
39
+ export function getQueryString(request, key) {
40
+ const value = getQueryValue(request, key);
41
+ if (value === undefined || Array.isArray(value)) {
42
+ return undefined;
43
+ }
44
+ if (typeof value === "object" && value !== null) {
45
+ return undefined;
46
+ }
47
+ return queryValueToString(value);
48
+ }
49
+ /**
50
+ * Returns every value a parameter carries, as strings.
51
+ *
52
+ * The parsed shape is attacker-chosen: `?a[b]=1&a=2` puts a null-prototype
53
+ * object inside the array for `a`, and `String()` on such an object throws
54
+ * `TypeError: Cannot convert object to primitive value`. Stringifying through
55
+ * {@link queryValueToString} keeps this total for every reachable input.
56
+ */
57
+ export function getQueryStrings(request, key) {
58
+ const value = getQueryValue(request, key);
59
+ if (value === undefined) {
60
+ return [];
61
+ }
62
+ if (Array.isArray(value)) {
63
+ return value.map(queryValueToString);
64
+ }
65
+ return [queryValueToString(value)];
66
+ }
67
+ /**
68
+ * Returns the raw search parameters of a request-target.
69
+ *
70
+ * This is the undecoded, unnested view. It applies none of the parser's
71
+ * limits and drops no key, so prefer {@link getQuery} for anything that
72
+ * reaches application code.
73
+ */
74
+ export function getSearchParams(request) {
75
+ const query = queryComponent(request.url ?? "");
76
+ if (query === undefined) {
77
+ return new URLSearchParams();
78
+ }
79
+ return new URLSearchParams(query);
80
+ }
81
+ /**
82
+ * Reports whether a parameter is present in the parsed query.
83
+ *
84
+ * Answers about the same object {@link getQuery} returns, not about the raw
85
+ * search params. The two used to disagree: `hasQuery(req, "a")` was `false`
86
+ * for `?a[b]=1` (which `getQuery` exposes as `a`), and
87
+ * `hasQuery(req, "__proto__")` was `true` for a key the parser drops.
88
+ */
89
+ export function hasQuery(request, key) {
90
+ return getQueryValue(request, key) !== undefined;
91
+ }
92
+ /** Number of distinct parameter names in the parsed query. */
93
+ export function querySize(request) {
94
+ return Object.keys(getQuery(request)).length;
95
+ }
96
+ //# sourceMappingURL=query.request.js.map
@@ -0,0 +1,10 @@
1
+ /**
2
+ * @zudojs/http/httpQuery/querySerialize
3
+ *
4
+ * Serializing an object back into a query string, and the clone/merge
5
+ * helpers that operate on an already-parsed query. All three are bounded the
6
+ * same way the parser is.
7
+ */
8
+ export { buildQueryString, stringifyQuery, } from "./querySerialize.core.js";
9
+ export { cloneQuery, mergeQuery } from "./query.util.js";
10
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1,10 @@
1
+ /**
2
+ * @zudojs/http/httpQuery/querySerialize
3
+ *
4
+ * Serializing an object back into a query string, and the clone/merge
5
+ * helpers that operate on an already-parsed query. All three are bounded the
6
+ * same way the parser is.
7
+ */
8
+ export { buildQueryString, stringifyQuery, } from "./querySerialize.core.js";
9
+ export { cloneQuery, mergeQuery } from "./query.util.js";
10
+ //# sourceMappingURL=index.js.map