@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.
- package/README.md +53 -3
- package/dist/httpAdapter/http.adapter.d.ts +34 -3
- package/dist/httpAdapter/http.adapter.js +22 -6
- package/dist/httpAdapter/http.adapters.d.ts +24 -3
- package/dist/httpAdapter/http.adapters.js +24 -45
- package/dist/httpAdapter/httpAdapter.errorResponse.d.ts +2 -2
- package/dist/httpAdapter/httpAdapter.errorResponse.js +26 -9
- package/dist/httpAdapter/httpAdapter.logger.d.ts +19 -0
- package/dist/httpAdapter/httpAdapter.logger.js +21 -0
- package/dist/httpAdapter/node/httpNode.adapter.d.ts +12 -1
- package/dist/httpAdapter/node/httpNode.adapter.js +28 -14
- package/dist/httpAdapter/node/httpNode.request.d.ts +8 -7
- package/dist/httpAdapter/node/httpNode.request.js +17 -27
- package/dist/httpAdapter/node/httpNode.server.d.ts +6 -0
- package/dist/httpAdapter/node/httpNode.server.js +6 -0
- package/dist/httpAdapter/node/httpNode.type.d.ts +2 -1
- package/dist/httpAgent/http.agent.d.ts +22 -3
- package/dist/httpAgent/http.agent.js +52 -14
- package/dist/httpCacheControl/httpCacheControl.freshness.d.ts +7 -1
- package/dist/httpCacheControl/httpCacheControl.freshness.js +30 -3
- package/dist/httpCookies/http.cookies.d.ts +29 -2
- package/dist/httpCookies/http.cookies.js +44 -24
- package/dist/httpCookies/httpCookies.defaults.d.ts +40 -0
- package/dist/httpCookies/httpCookies.defaults.js +42 -0
- package/dist/httpCookies/index.d.ts +1 -0
- package/dist/httpCookies/index.js +1 -0
- package/dist/httpKeepAlive/httpKeepAlive.core.js +14 -2
- package/dist/httpMiddleware/builtin/conditional/httpMiddleware.conditional.d.ts +22 -1
- package/dist/httpMiddleware/builtin/conditional/httpMiddleware.conditional.js +19 -4
- package/dist/httpMiddleware/builtin/conditional/index.d.ts +1 -0
- package/dist/httpMiddleware/builtin/cors/httpMiddleware.cors.d.ts +8 -0
- package/dist/httpMiddleware/builtin/cors/httpMiddleware.cors.js +25 -22
- package/dist/httpMiddleware/builtin/helpers/httpMiddleware.accessor.js +2 -6
- package/dist/httpMiddleware/builtin/helpers/httpMiddleware.media.d.ts +33 -0
- package/dist/httpMiddleware/builtin/helpers/httpMiddleware.media.js +59 -0
- package/dist/httpMiddleware/builtin/helpers/index.d.ts +1 -0
- package/dist/httpMiddleware/builtin/helpers/index.js +1 -0
- package/dist/httpMiddleware/builtin/image/httpMiddleware.image.d.ts +13 -0
- package/dist/httpMiddleware/builtin/image/httpMiddleware.image.js +31 -53
- package/dist/httpMiddleware/builtin/index.d.ts +1 -0
- package/dist/httpMiddleware/builtin/index.js +1 -0
- package/dist/httpMiddleware/builtin/logging/httpMiddleware.logging.d.ts +18 -0
- package/dist/httpMiddleware/builtin/logging/httpMiddleware.logging.js +17 -1
- package/dist/httpMiddleware/builtin/rateLimit/httpMiddleware.rateLimit.d.ts +47 -0
- package/dist/httpMiddleware/builtin/rateLimit/httpMiddleware.rateLimit.js +61 -0
- package/dist/httpMiddleware/builtin/rateLimit/index.d.ts +7 -0
- package/dist/httpMiddleware/builtin/rateLimit/index.js +7 -0
- package/dist/httpMiddleware/builtin/security/httpMiddleware.security.d.ts +11 -0
- package/dist/httpMiddleware/builtin/security/httpMiddleware.security.js +25 -12
- package/dist/httpMiddleware/builtin/static/httpMiddleware.static.js +18 -4
- package/dist/httpMiddleware/builtin/video/httpMiddleware.video.d.ts +19 -1
- package/dist/httpMiddleware/builtin/video/httpMiddleware.video.js +38 -59
- package/dist/httpMiddleware/builtin/video/httpMiddleware.video.options.d.ts +12 -0
- package/dist/httpMiddleware/builtin/video/httpMiddleware.video.options.js +31 -0
- package/dist/httpMiddleware/httpMiddleware.error.d.ts +5 -31
- package/dist/httpMiddleware/httpMiddleware.error.js +5 -53
- package/dist/httpNegotiation/httpNegotiation.core.d.ts +13 -0
- package/dist/httpNegotiation/httpNegotiation.core.js +60 -8
- package/dist/httpProxy/http.proxy.d.ts +16 -0
- package/dist/httpProxy/http.proxy.js +44 -3
- package/dist/httpProxy/httpProxy.pathGuard.d.ts +23 -0
- package/dist/httpProxy/httpProxy.pathGuard.js +43 -0
- package/dist/httpProxy/index.d.ts +1 -0
- package/dist/httpProxy/index.js +1 -0
- package/dist/httpQuery/index.d.ts +11 -2
- package/dist/httpQuery/index.js +11 -2
- package/dist/httpQuery/queryParse/index.d.ts +10 -0
- package/dist/httpQuery/queryParse/index.js +10 -0
- package/dist/httpQuery/queryParse/queryParse.flat.d.ts +13 -0
- package/dist/httpQuery/queryParse/queryParse.flat.js +44 -0
- package/dist/httpQuery/queryParse/queryParse.nested.d.ts +25 -0
- package/dist/httpQuery/queryParse/queryParse.nested.js +112 -0
- package/dist/httpQuery/queryParse/queryParse.tokenizer.d.ts +37 -0
- package/dist/httpQuery/queryParse/queryParse.tokenizer.js +95 -0
- package/dist/httpQuery/queryRequest/index.d.ts +9 -0
- package/dist/httpQuery/queryRequest/index.js +9 -0
- package/dist/httpQuery/queryRequest/query.request.d.ts +43 -0
- package/dist/httpQuery/queryRequest/query.request.js +96 -0
- package/dist/httpQuery/querySerialize/index.d.ts +10 -0
- package/dist/httpQuery/querySerialize/index.js +10 -0
- package/dist/httpQuery/querySerialize/query.util.d.ts +21 -0
- package/dist/httpQuery/querySerialize/query.util.js +67 -0
- package/dist/httpQuery/querySerialize/querySerialize.core.d.ts +12 -0
- package/dist/httpQuery/querySerialize/querySerialize.core.js +97 -0
- package/dist/httpQuery/queryTypes/index.d.ts +11 -0
- package/dist/httpQuery/queryTypes/index.js +9 -0
- package/dist/httpQuery/queryTypes/query.container.d.ts +16 -0
- package/dist/httpQuery/queryTypes/query.container.js +51 -0
- package/dist/httpQuery/queryTypes/query.limit.d.ts +25 -0
- package/dist/httpQuery/queryTypes/query.limit.js +32 -0
- package/dist/httpQuery/queryTypes/query.type.d.ts +62 -0
- package/dist/httpQuery/queryTypes/query.type.js +2 -0
- package/dist/httpRedirect/http.redirect.d.ts +6 -0
- package/dist/httpRedirect/http.redirect.js +53 -2
- package/dist/httpRequest/http.request.d.ts +61 -2
- package/dist/httpRequest/http.request.js +86 -35
- package/dist/httpRequest/httpRequest.context.d.ts +7 -0
- package/dist/httpRequest/httpRequest.context.js +26 -34
- package/dist/httpRequest/index.d.ts +1 -0
- package/dist/httpRequest/index.js +1 -0
- package/dist/httpRequest/target/httpRequest.target.d.ts +45 -0
- package/dist/httpRequest/target/httpRequest.target.js +111 -0
- package/dist/httpRequest/target/index.d.ts +9 -0
- package/dist/httpRequest/target/index.js +9 -0
- package/dist/httpResponse/httpResponse.helper.js +2 -1
- package/dist/httpRouter/core/factory/httpRoute.factory.base.d.ts +18 -1
- package/dist/httpRouter/core/factory/httpRoute.factory.base.js +49 -6
- package/dist/httpRouter/core/factory/httpRoute.factory.js +3 -3
- package/dist/httpRouter/core/register/httpRouter.register.js +16 -21
- package/dist/httpRouter/core/types/httpRouter.type.d.ts +6 -0
- package/dist/httpRouter/core/util/httpRoute.util.d.ts +61 -0
- package/dist/httpRouter/core/util/httpRoute.util.js +102 -17
- package/dist/httpRouter/dispatch/httpRoute.dispatcher.d.ts +11 -0
- package/dist/httpRouter/dispatch/httpRoute.dispatcher.js +5 -3
- package/dist/httpRouter/matching/httpRoute.matcher.core.js +51 -6
- package/dist/httpRouter/matching/httpRoute.matcher.d.ts +0 -1
- package/dist/httpRouter/matching/httpRoute.matcher.js +64 -26
- package/dist/httpRouter/pattern/httpRoute.pattern.parse.d.ts +16 -0
- package/dist/httpRouter/pattern/httpRoute.pattern.parse.js +40 -11
- package/dist/httpRouter/pattern/index.d.ts +1 -1
- package/dist/httpRouter/pattern/index.js +1 -1
- package/dist/httpSecurity/httpSecurity.config.d.ts +6 -0
- package/dist/httpSecurity/httpSecurity.config.js +1 -0
- package/dist/httpSecurity/httpSecurity.guard.d.ts +7 -9
- package/dist/httpSecurity/httpSecurity.guard.js +7 -14
- package/dist/httpSecurity/httpSecurity.nodeGuard.d.ts +48 -0
- package/dist/httpSecurity/httpSecurity.nodeGuard.js +32 -0
- package/dist/httpSecurity/httpSecurity.validator.js +20 -9
- package/dist/httpSecurity/index.d.ts +2 -0
- package/dist/httpSecurity/index.js +1 -0
- package/dist/httpServer/core/httpServer.core.d.ts +11 -0
- package/dist/httpServer/core/httpServer.core.js +30 -4
- package/dist/httpTrustProxy/httpTrustProxy.compilation.d.ts +5 -2
- package/dist/httpTrustProxy/httpTrustProxy.compilation.js +19 -2
- package/dist/httpTrustProxy/httpTrustProxy.type.d.ts +3 -1
- package/dist/index.js +0 -1
- package/package.json +5 -5
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type { QueryObject } from "../queryTypes/index.js";
|
|
2
|
+
/**
|
|
3
|
+
* Deep-copies a parsed query object.
|
|
4
|
+
*
|
|
5
|
+
* Structural rather than a `JSON.parse(JSON.stringify(…))` round-trip. That
|
|
6
|
+
* round-trip rebuilt every level with `Object.prototype`, so the clone of a
|
|
7
|
+
* hardened, null-prototype query silently regained `toString`, `constructor`
|
|
8
|
+
* and every other inherited member — the exact property the parser exists to
|
|
9
|
+
* guarantee. It also violated the repo rule against calling `JSON` directly.
|
|
10
|
+
*/
|
|
11
|
+
export declare function cloneQuery(query: QueryObject): QueryObject;
|
|
12
|
+
/**
|
|
13
|
+
* Merges query objects left to right into a fresh null-prototype object.
|
|
14
|
+
*
|
|
15
|
+
* Every value is deep-copied on the way in. Assigning the source's own
|
|
16
|
+
* nested object by reference made the result alias its inputs, so mutating
|
|
17
|
+
* `merged.a.b` also changed the source query the caller had already handed
|
|
18
|
+
* to something else.
|
|
19
|
+
*/
|
|
20
|
+
export declare function mergeQuery(...queries: QueryObject[]): QueryObject;
|
|
21
|
+
//# sourceMappingURL=query.util.d.ts.map
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { createQueryContainer, DEFAULT_QUERY_MAX_DEPTH, HTTPQueryLimitError, isForbiddenQueryKey, isQueryObject, ownValue, } from "../queryTypes/index.js";
|
|
2
|
+
/**
|
|
3
|
+
* Deep-copies a parsed query object.
|
|
4
|
+
*
|
|
5
|
+
* Structural rather than a `JSON.parse(JSON.stringify(…))` round-trip. That
|
|
6
|
+
* round-trip rebuilt every level with `Object.prototype`, so the clone of a
|
|
7
|
+
* hardened, null-prototype query silently regained `toString`, `constructor`
|
|
8
|
+
* and every other inherited member — the exact property the parser exists to
|
|
9
|
+
* guarantee. It also violated the repo rule against calling `JSON` directly.
|
|
10
|
+
*/
|
|
11
|
+
export function cloneQuery(query) {
|
|
12
|
+
return cloneQueryObject(query, DEFAULT_QUERY_MAX_DEPTH);
|
|
13
|
+
}
|
|
14
|
+
function cloneQueryObject(source, depth) {
|
|
15
|
+
if (depth <= 0) {
|
|
16
|
+
throw new HTTPQueryLimitError("Query object nesting exceeds the maximum allowed depth.");
|
|
17
|
+
}
|
|
18
|
+
const result = createQueryContainer();
|
|
19
|
+
for (const [key, value] of Object.entries(source)) {
|
|
20
|
+
if (isForbiddenQueryKey(key)) {
|
|
21
|
+
continue;
|
|
22
|
+
}
|
|
23
|
+
result[key] = cloneQueryValue(value, depth);
|
|
24
|
+
}
|
|
25
|
+
return result;
|
|
26
|
+
}
|
|
27
|
+
function cloneQueryValue(value, depth) {
|
|
28
|
+
if (Array.isArray(value)) {
|
|
29
|
+
return value.map((item) => cloneQueryValue(item, depth - 1));
|
|
30
|
+
}
|
|
31
|
+
if (isQueryObject(value)) {
|
|
32
|
+
return cloneQueryObject(value, depth - 1);
|
|
33
|
+
}
|
|
34
|
+
return value;
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Merges query objects left to right into a fresh null-prototype object.
|
|
38
|
+
*
|
|
39
|
+
* Every value is deep-copied on the way in. Assigning the source's own
|
|
40
|
+
* nested object by reference made the result alias its inputs, so mutating
|
|
41
|
+
* `merged.a.b` also changed the source query the caller had already handed
|
|
42
|
+
* to something else.
|
|
43
|
+
*/
|
|
44
|
+
export function mergeQuery(...queries) {
|
|
45
|
+
const result = createQueryContainer();
|
|
46
|
+
for (const query of queries) {
|
|
47
|
+
mergeQueryObject(result, query, DEFAULT_QUERY_MAX_DEPTH);
|
|
48
|
+
}
|
|
49
|
+
return result;
|
|
50
|
+
}
|
|
51
|
+
function mergeQueryObject(target, source, depth) {
|
|
52
|
+
if (depth <= 0) {
|
|
53
|
+
throw new HTTPQueryLimitError("Query object nesting exceeds the maximum allowed depth.");
|
|
54
|
+
}
|
|
55
|
+
for (const [key, value] of Object.entries(source)) {
|
|
56
|
+
if (isForbiddenQueryKey(key)) {
|
|
57
|
+
continue;
|
|
58
|
+
}
|
|
59
|
+
const existing = ownValue(target, key);
|
|
60
|
+
if (isQueryObject(existing) && isQueryObject(value)) {
|
|
61
|
+
mergeQueryObject(existing, value, depth - 1);
|
|
62
|
+
continue;
|
|
63
|
+
}
|
|
64
|
+
target[key] = cloneQueryValue(value, depth);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
//# sourceMappingURL=query.util.js.map
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { QueryObject, QueryStringifyOptions } 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 declare function stringifyQuery(query: QueryObject | Record<string, unknown>, options?: QueryStringifyOptions): string;
|
|
11
|
+
export declare function buildQueryString(query: QueryObject | Record<string, unknown>, options?: QueryStringifyOptions): string;
|
|
12
|
+
//# sourceMappingURL=querySerialize.core.d.ts.map
|
|
@@ -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
|
|
@@ -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 =
|
|
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
|
-
|
|
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
|
-
|
|
66
|
-
|
|
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[];
|