@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.
- package/README.md +221 -0
- package/dist/httpAdapter/http.adapters.d.ts +24 -3
- package/dist/httpAdapter/http.adapters.js +20 -11
- package/dist/httpAdapter/node/httpNode.adapter.d.ts +2 -1
- package/dist/httpAdapter/node/httpNode.adapter.js +17 -2
- package/dist/httpAdapter/node/httpNode.request.js +7 -1
- package/dist/httpAdapter/node/httpNode.type.d.ts +14 -0
- 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/httpClient/httpClient.retry.d.ts +17 -12
- package/dist/httpClient/httpClient.retry.js +35 -10
- package/dist/httpClient/httpClient.type.d.ts +14 -0
- package/dist/httpErrors/httpError.base.js +2 -2
- package/dist/httpErrors/httpError.util.d.ts +8 -0
- package/dist/httpErrors/httpError.util.js +12 -0
- package/dist/httpFetchMount/httpFetchMount.core.d.ts +25 -0
- package/dist/httpFetchMount/httpFetchMount.core.js +84 -0
- package/dist/httpFetchMount/httpFetchMount.request.d.ts +21 -0
- package/dist/httpFetchMount/httpFetchMount.request.js +100 -0
- package/dist/httpFetchMount/httpFetchMount.type.d.ts +56 -0
- package/dist/httpFetchMount/httpFetchMount.type.js +5 -0
- package/dist/httpFetchMount/index.d.ts +11 -0
- package/dist/httpFetchMount/index.js +10 -0
- package/dist/httpKeepAlive/httpKeepAlive.core.js +14 -2
- 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 +6 -3
- package/dist/httpMiddleware/builtin/rateLimit/httpMiddleware.rateLimit.js +34 -6
- package/dist/httpMiddleware/builtin/security/httpMiddleware.security.d.ts +11 -0
- package/dist/httpMiddleware/builtin/security/httpMiddleware.security.js +25 -12
- package/dist/httpMiddleware/httpMiddleware.type.d.ts +9 -1
- package/dist/httpMiddleware/pipeline/httpPipeline.execution.js +22 -45
- package/dist/httpMiddleware/pipeline/httpPipeline.guardResponse.d.ts +36 -0
- package/dist/httpMiddleware/pipeline/httpPipeline.guardResponse.js +57 -0
- package/dist/httpMiddleware/pipeline/httpPipeline.helper.d.ts +2 -1
- package/dist/httpMiddleware/pipeline/httpPipeline.helper.js +10 -0
- package/dist/httpMiddleware/pipeline/index.d.ts +1 -0
- package/dist/httpMiddleware/pipeline/index.js +1 -0
- package/dist/httpNegotiation/httpNegotiation.core.d.ts +13 -0
- package/dist/httpNegotiation/httpNegotiation.core.js +60 -8
- package/dist/httpOpenApi/httpOpenApi.document.d.ts +44 -0
- package/dist/httpOpenApi/httpOpenApi.document.js +61 -0
- package/dist/httpOpenApi/httpOpenApi.mount.d.ts +31 -0
- package/dist/httpOpenApi/httpOpenApi.mount.js +58 -0
- package/dist/httpOpenApi/httpOpenApi.type.d.ts +54 -0
- package/dist/httpOpenApi/httpOpenApi.type.js +5 -0
- package/dist/httpOpenApi/index.d.ts +13 -0
- package/dist/httpOpenApi/index.js +12 -0
- package/dist/httpOpenApi/routeTable/index.d.ts +11 -0
- package/dist/httpOpenApi/routeTable/index.js +11 -0
- package/dist/httpOpenApi/routeTable/routeTable.collect.d.ts +19 -0
- package/dist/httpOpenApi/routeTable/routeTable.collect.js +89 -0
- package/dist/httpOpenApi/routeTable/routeTable.merge.d.ts +15 -0
- package/dist/httpOpenApi/routeTable/routeTable.merge.js +37 -0
- package/dist/httpOpenApi/routeTable/routeTable.template.d.ts +30 -0
- package/dist/httpOpenApi/routeTable/routeTable.template.js +67 -0
- package/dist/httpProxy/http.proxy.d.ts +9 -0
- package/dist/httpProxy/http.proxy.js +35 -3
- 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 +8 -0
- package/dist/httpRequest/httpRequest.context.js +23 -18
- package/dist/httpRequest/index.d.ts +1 -0
- package/dist/httpRequest/index.js +1 -0
- package/dist/httpRequest/requestId/httpRequest.requestId.d.ts +25 -0
- package/dist/httpRequest/requestId/httpRequest.requestId.js +34 -0
- package/dist/httpRequest/requestId/index.d.ts +7 -0
- package/dist/httpRequest/requestId/index.js +7 -0
- package/dist/httpRequest/target/httpRequest.target.d.ts +2 -2
- package/dist/httpRequest/target/httpRequest.target.js +23 -5
- package/dist/httpResponse/httpResponse.writer.js +15 -0
- package/dist/httpRouter/core/factory/httpRoute.factory.base.d.ts +24 -4
- package/dist/httpRouter/core/factory/httpRoute.factory.base.js +73 -17
- package/dist/httpRouter/core/factory/httpRoute.factory.js +3 -3
- package/dist/httpRouter/core/group/httpRouterGroup.core.js +13 -0
- package/dist/httpRouter/core/register/httpRouter.register.js +19 -21
- package/dist/httpRouter/core/types/httpRouter.type.d.ts +37 -1
- package/dist/httpRouter/core/util/httpRoute.util.d.ts +55 -0
- package/dist/httpRouter/core/util/httpRoute.util.js +101 -4
- package/dist/httpRouter/dispatch/httpRoute.dispatcher.d.ts +11 -0
- package/dist/httpRouter/dispatch/httpRoute.dispatcher.js +25 -6
- 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.js +4 -1
- package/dist/httpSecurity/httpSecurity.validator.js +16 -7
- package/dist/httpServer/factory/httpServer.factory.d.ts +10 -9
- package/dist/httpServer/factory/httpServer.factory.js +8 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +2 -0
- package/package.json +11 -8
|
@@ -11,6 +11,24 @@ export interface RequestLogger {
|
|
|
11
11
|
export interface LoggingMiddlewareOptions {
|
|
12
12
|
readonly logger?: RequestLogger;
|
|
13
13
|
readonly includeHeaders?: boolean;
|
|
14
|
+
/**
|
|
15
|
+
* Extra header names whose value must be replaced with `[REDACTED]`, on top
|
|
16
|
+
* of the credential-bearing names `@zudojs/logger` already recognises
|
|
17
|
+
* (`authorization`, `proxy-authorization`, `cookie`, `set-cookie`, …).
|
|
18
|
+
*/
|
|
19
|
+
readonly redactHeaders?: readonly string[];
|
|
14
20
|
}
|
|
21
|
+
/**
|
|
22
|
+
* Creates the request/response logging middleware.
|
|
23
|
+
*
|
|
24
|
+
* With `includeHeaders` the header record is redacted before it reaches the
|
|
25
|
+
* logger, using the same matcher `@zudojs/logger` applies to log metadata.
|
|
26
|
+
* It used to be copied verbatim, so a bearer token and the whole session
|
|
27
|
+
* cookie landed in the log store on every request.
|
|
28
|
+
*
|
|
29
|
+
* @param options - Logger, header inclusion and extra redacted names.
|
|
30
|
+
* @returns A middleware that logs the start, completion and failure of a
|
|
31
|
+
* request.
|
|
32
|
+
*/
|
|
15
33
|
export declare function createLoggingMiddleware(options?: LoggingMiddlewareOptions): HttpMiddleware;
|
|
16
34
|
//# sourceMappingURL=httpMiddleware.logging.d.ts.map
|
|
@@ -3,8 +3,24 @@
|
|
|
3
3
|
*
|
|
4
4
|
* @module httpMiddleware/builtin/logging
|
|
5
5
|
*/
|
|
6
|
+
import { createSecretMatcher, redactLogValue } from "@zudojs/logger";
|
|
6
7
|
import { getRequestMethod, getRequestUrl, getRequestHeaders, getResponseStatus, } from "../helpers/index.js";
|
|
8
|
+
/**
|
|
9
|
+
* Creates the request/response logging middleware.
|
|
10
|
+
*
|
|
11
|
+
* With `includeHeaders` the header record is redacted before it reaches the
|
|
12
|
+
* logger, using the same matcher `@zudojs/logger` applies to log metadata.
|
|
13
|
+
* It used to be copied verbatim, so a bearer token and the whole session
|
|
14
|
+
* cookie landed in the log store on every request.
|
|
15
|
+
*
|
|
16
|
+
* @param options - Logger, header inclusion and extra redacted names.
|
|
17
|
+
* @returns A middleware that logs the start, completion and failure of a
|
|
18
|
+
* request.
|
|
19
|
+
*/
|
|
7
20
|
export function createLoggingMiddleware(options = {}) {
|
|
21
|
+
const isSecret = createSecretMatcher({
|
|
22
|
+
keys: options.redactHeaders ? [...options.redactHeaders] : undefined,
|
|
23
|
+
});
|
|
8
24
|
return async (context, next) => {
|
|
9
25
|
const startedAt = Date.now();
|
|
10
26
|
const request = context.request;
|
|
@@ -14,7 +30,7 @@ export function createLoggingMiddleware(options = {}) {
|
|
|
14
30
|
url: getRequestUrl(request),
|
|
15
31
|
...(options.includeHeaders
|
|
16
32
|
? {
|
|
17
|
-
headers: getRequestHeaders(request),
|
|
33
|
+
headers: redactLogValue(getRequestHeaders(request), isSecret),
|
|
18
34
|
}
|
|
19
35
|
: {}),
|
|
20
36
|
});
|
|
@@ -39,9 +39,12 @@ export type RateLimitMiddlewareOptions = RateLimiterOptions | {
|
|
|
39
39
|
readonly limiter: HttpRateLimiter;
|
|
40
40
|
};
|
|
41
41
|
/**
|
|
42
|
-
* Creates middleware that answers `429 Too Many Requests`
|
|
43
|
-
*
|
|
44
|
-
*
|
|
42
|
+
* Creates middleware that answers `429 Too Many Requests` once a client
|
|
43
|
+
* exceeds its allowance, and otherwise passes the request on. The 429 has a
|
|
44
|
+
* JSON body (`{"error":{"code":"RATE_LIMIT_EXCEEDED",...}}`) sent as
|
|
45
|
+
* `application/json`, and always a `Retry-After` header (the
|
|
46
|
+
* `@zudojs/security` handler's, or one computed from the limiter's reset
|
|
47
|
+
* time when a custom handler leaves it out).
|
|
45
48
|
*/
|
|
46
49
|
export declare function createRateLimitMiddleware(options: RateLimitMiddlewareOptions): HttpMiddleware;
|
|
47
50
|
//# sourceMappingURL=httpMiddleware.rateLimit.d.ts.map
|
|
@@ -18,7 +18,7 @@
|
|
|
18
18
|
*
|
|
19
19
|
* @module httpMiddleware/builtin/rateLimit
|
|
20
20
|
*/
|
|
21
|
-
import { createRateLimiter, parseClientIp, } from "@zudojs/security";
|
|
21
|
+
import { createRateLimiter, parseClientIp, retryAfterSeconds, } from "@zudojs/security";
|
|
22
22
|
import { createResponseContext } from "../../../httpResponse/httpResponse.context.js";
|
|
23
23
|
/**
|
|
24
24
|
* The `ip` given to the limiter for a request whose `remoteAddress` is
|
|
@@ -27,9 +27,12 @@ import { createResponseContext } from "../../../httpResponse/httpResponse.contex
|
|
|
27
27
|
*/
|
|
28
28
|
export const UNKNOWN_CLIENT_RATE_LIMIT_IP = "0.0.0.0";
|
|
29
29
|
/**
|
|
30
|
-
* Creates middleware that answers `429 Too Many Requests`
|
|
31
|
-
*
|
|
32
|
-
*
|
|
30
|
+
* Creates middleware that answers `429 Too Many Requests` once a client
|
|
31
|
+
* exceeds its allowance, and otherwise passes the request on. The 429 has a
|
|
32
|
+
* JSON body (`{"error":{"code":"RATE_LIMIT_EXCEEDED",...}}`) sent as
|
|
33
|
+
* `application/json`, and always a `Retry-After` header (the
|
|
34
|
+
* `@zudojs/security` handler's, or one computed from the limiter's reset
|
|
35
|
+
* time when a custom handler leaves it out).
|
|
33
36
|
*/
|
|
34
37
|
export function createRateLimitMiddleware(options) {
|
|
35
38
|
const limiter = "limiter" in options ? options.limiter : createRateLimiter(options);
|
|
@@ -42,16 +45,41 @@ export function createRateLimitMiddleware(options) {
|
|
|
42
45
|
headers: request.headers,
|
|
43
46
|
};
|
|
44
47
|
const rejection = { statusCode: 429, headers: {} };
|
|
45
|
-
|
|
48
|
+
const decision = limiter.middleware(limitRequest, rejection);
|
|
49
|
+
if (decision.allowed) {
|
|
46
50
|
return next();
|
|
47
51
|
}
|
|
48
52
|
const response = createResponseContext().setStatus(rejection.statusCode);
|
|
49
53
|
for (const [name, value] of Object.entries(rejection.headers)) {
|
|
50
54
|
response.setHeader(name.toLowerCase(), value);
|
|
51
55
|
}
|
|
52
|
-
|
|
56
|
+
if (response.headers["retry-after"] === undefined) {
|
|
57
|
+
response.setHeader("retry-after", String(retryAfterSeconds(decision)));
|
|
58
|
+
}
|
|
59
|
+
const body = rejection.body ?? DEFAULT_REJECTION_BODY;
|
|
60
|
+
/*
|
|
61
|
+
* The limiter's body is JSON, but it went out as a bare string, which the
|
|
62
|
+
* writer labels `text/plain`. A handler's own content type is kept.
|
|
63
|
+
*/
|
|
64
|
+
if (response.headers["content-type"] === undefined) {
|
|
65
|
+
response.setHeader("content-type", isJson(body) ? "application/json; charset=utf-8" : "text/plain; charset=utf-8");
|
|
66
|
+
}
|
|
67
|
+
return response.setBody(body);
|
|
53
68
|
};
|
|
54
69
|
}
|
|
70
|
+
/** Body sent when a custom limiter handler supplies none. */
|
|
71
|
+
const DEFAULT_REJECTION_BODY = JSON.stringify({
|
|
72
|
+
error: { code: "RATE_LIMIT_EXCEEDED", message: "Too Many Requests" },
|
|
73
|
+
});
|
|
74
|
+
function isJson(body) {
|
|
75
|
+
try {
|
|
76
|
+
JSON.parse(body);
|
|
77
|
+
return true;
|
|
78
|
+
}
|
|
79
|
+
catch {
|
|
80
|
+
return false;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
55
83
|
function clientIpOf(remoteAddress) {
|
|
56
84
|
return typeof remoteAddress === "string" &&
|
|
57
85
|
parseClientIp(remoteAddress) !== undefined
|
|
@@ -17,5 +17,16 @@ export interface SecurityMiddlewareOptions {
|
|
|
17
17
|
*/
|
|
18
18
|
readonly useDefaults?: boolean;
|
|
19
19
|
}
|
|
20
|
+
/**
|
|
21
|
+
* Creates the response-hardening middleware.
|
|
22
|
+
*
|
|
23
|
+
* Called with no options it emits the package's default security header set
|
|
24
|
+
* ({@link createDefaultSecurityHeaderOptions}); the options below layer over
|
|
25
|
+
* that set, and `useDefaults: false` drops it entirely.
|
|
26
|
+
*
|
|
27
|
+
* @param options - Explicit header values, layered over the baseline.
|
|
28
|
+
* @returns A middleware that adds the headers to the downstream response.
|
|
29
|
+
* @throws {TypeError} If a configured value contains a control character.
|
|
30
|
+
*/
|
|
20
31
|
export declare function createSecurityMiddleware(options?: SecurityMiddlewareOptions): HttpMiddleware;
|
|
21
32
|
//# sourceMappingURL=httpMiddleware.security.d.ts.map
|
|
@@ -4,19 +4,34 @@
|
|
|
4
4
|
* @module httpMiddleware/builtin/security
|
|
5
5
|
*/
|
|
6
6
|
import { isValidHeaderFieldValue } from "../../../httpHeaders/security/index.js";
|
|
7
|
+
import { createSecurityHeaders } from "../../../httpSecurityHeaders/httpSecurityHeader.factory.js";
|
|
8
|
+
import { createDefaultSecurityHeaderOptions } from "../../../httpSecurityHeaders/httpSecurityHeader.recommended.js";
|
|
7
9
|
import { withResponseHeaders } from "../helpers/index.js";
|
|
8
10
|
/**
|
|
9
|
-
*
|
|
11
|
+
* The headers applied when the caller does not override them.
|
|
10
12
|
*
|
|
11
|
-
*
|
|
12
|
-
* `
|
|
13
|
-
*
|
|
13
|
+
* This is the package's declared safe baseline — the very set
|
|
14
|
+
* `createDefaultSecurityHeaderOptions` was written for and, until now, was
|
|
15
|
+
* never called for. The middleware used to re-derive its own three-entry
|
|
16
|
+
* list, so `pipeline.use(createSecurityMiddleware())` emitted no
|
|
17
|
+
* `Content-Security-Policy`, no `Strict-Transport-Security`, no
|
|
18
|
+
* `Permissions-Policy` and none of the cross-origin isolation headers while
|
|
19
|
+
* reading as evidence that the control was in force.
|
|
20
|
+
*/
|
|
21
|
+
function defaultSecurityHeaders() {
|
|
22
|
+
return createSecurityHeaders(createDefaultSecurityHeaderOptions());
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Creates the response-hardening middleware.
|
|
26
|
+
*
|
|
27
|
+
* Called with no options it emits the package's default security header set
|
|
28
|
+
* ({@link createDefaultSecurityHeaderOptions}); the options below layer over
|
|
29
|
+
* that set, and `useDefaults: false` drops it entirely.
|
|
30
|
+
*
|
|
31
|
+
* @param options - Explicit header values, layered over the baseline.
|
|
32
|
+
* @returns A middleware that adds the headers to the downstream response.
|
|
33
|
+
* @throws {TypeError} If a configured value contains a control character.
|
|
14
34
|
*/
|
|
15
|
-
const DEFAULT_SECURITY_HEADERS = Object.freeze({
|
|
16
|
-
"x-content-type-options": "nosniff",
|
|
17
|
-
"x-frame-options": "DENY",
|
|
18
|
-
"referrer-policy": "strict-origin-when-cross-origin",
|
|
19
|
-
});
|
|
20
35
|
export function createSecurityMiddleware(options = {}) {
|
|
21
36
|
const configured = {
|
|
22
37
|
"strict-transport-security": options.strictTransportSecurity,
|
|
@@ -26,9 +41,7 @@ export function createSecurityMiddleware(options = {}) {
|
|
|
26
41
|
"content-security-policy": options.contentSecurityPolicy,
|
|
27
42
|
"referrer-policy": options.referrerPolicy,
|
|
28
43
|
};
|
|
29
|
-
const resolved = {
|
|
30
|
-
...(options.useDefaults === false ? {} : DEFAULT_SECURITY_HEADERS),
|
|
31
|
-
};
|
|
44
|
+
const resolved = options.useDefaults === false ? {} : defaultSecurityHeaders();
|
|
32
45
|
for (const [name, value] of Object.entries(configured)) {
|
|
33
46
|
if (value === undefined) {
|
|
34
47
|
continue;
|
|
@@ -3,9 +3,17 @@
|
|
|
3
3
|
*
|
|
4
4
|
* @module httpMiddleware/types
|
|
5
5
|
*/
|
|
6
|
+
import type { GuardResponse } from "@zudojs/middleware";
|
|
6
7
|
import type { HttpRequestContext as RequestContext } from "../httpRequest/httpRequest.context.js";
|
|
7
8
|
import type { HttpResponseContext as ResponseContext } from "../httpResponse/httpResponse.context.js";
|
|
8
|
-
|
|
9
|
+
/**
|
|
10
|
+
* What a middleware may return.
|
|
11
|
+
*
|
|
12
|
+
* A `GuardResponse` (from `createGuardResponse` in `@zudojs/middleware`) is
|
|
13
|
+
* sent with its own status, headers and body; it is how a guard in a
|
|
14
|
+
* lower-tier package, which cannot import this one, answers 401/403/404.
|
|
15
|
+
*/
|
|
16
|
+
export type HttpMiddlewareResult = void | Response | RequestContext | ResponseContext | GuardResponse | Promise<void | Response | RequestContext | ResponseContext | GuardResponse>;
|
|
9
17
|
export type HttpNext = () => Promise<ResponseContext>;
|
|
10
18
|
export type HttpMiddleware = (context: HttpMiddlewareContext, next: HttpNext) => HttpMiddlewareResult;
|
|
11
19
|
export interface HttpMiddlewareContext {
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
*
|
|
4
4
|
* @module httpMiddleware/pipeline/execution
|
|
5
5
|
*/
|
|
6
|
-
import { HttpMiddlewareError
|
|
6
|
+
import { HttpMiddlewareError } from "../httpMiddleware.error.js";
|
|
7
7
|
import { normalizeResult } from "./httpPipeline.helper.js";
|
|
8
8
|
import { list } from "./httpPipeline.registration.js";
|
|
9
9
|
export async function executePipeline(entries, request, response, options = {}, pipelineOptions) {
|
|
@@ -42,56 +42,33 @@ export async function executePipeline(entries, request, response, options = {},
|
|
|
42
42
|
}
|
|
43
43
|
return dispatch(index + 1);
|
|
44
44
|
};
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
*/
|
|
57
|
-
throw error instanceof HttpMiddlewareError
|
|
58
|
-
? error
|
|
59
|
-
: new HttpMiddlewareError(`Middleware "${entry.name}" threw an error.`, {
|
|
60
|
-
middlewareId: entry.id,
|
|
61
|
-
middlewareName: entry.name,
|
|
62
|
-
cause: error,
|
|
63
|
-
});
|
|
64
|
-
}
|
|
45
|
+
/*
|
|
46
|
+
* A failure propagates as the error that was thrown, never a wrapper and
|
|
47
|
+
* never swallowed. An outer middleware's `await next()` rejects with it,
|
|
48
|
+
* so code after `await next()` does not run unless that middleware
|
|
49
|
+
* catches it (`try/catch`, `try/finally`), and `instanceof NotFoundError`
|
|
50
|
+
* works there and in the server's `errorHandler`. It used to be wrapped
|
|
51
|
+
* in `HttpMiddlewareError` here and `HttpMiddlewarePipelineError` below,
|
|
52
|
+
* which hid the original in `cause` / `errors[0].cause`.
|
|
53
|
+
*/
|
|
54
|
+
const result = await entry.middleware(context, next);
|
|
55
|
+
return normalizeResult(result, response);
|
|
65
56
|
};
|
|
66
57
|
try {
|
|
67
58
|
return await dispatch(0);
|
|
68
59
|
}
|
|
69
60
|
catch (error) {
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
: new HttpMiddlewareError("HTTP middleware pipeline failed.", {
|
|
73
|
-
cause: error,
|
|
74
|
-
});
|
|
75
|
-
if (pipelineOptions.onError) {
|
|
76
|
-
try {
|
|
77
|
-
/*
|
|
78
|
-
* A successful `onError` is a genuine recovery and must be returned.
|
|
79
|
-
* Previously every caught error was recorded before `onError` ran and
|
|
80
|
-
* the recorded list was rethrown afterwards, so the handler's result
|
|
81
|
-
* was always discarded and the option could never take effect.
|
|
82
|
-
*/
|
|
83
|
-
const errorResult = await pipelineOptions.onError(middlewareError.cause ?? error, context);
|
|
84
|
-
return normalizeResult(errorResult, response);
|
|
85
|
-
}
|
|
86
|
-
catch (handlerError) {
|
|
87
|
-
/* The handler's own failure is reported, not swallowed. */
|
|
88
|
-
throw new HttpMiddlewarePipelineError([
|
|
89
|
-
middlewareError,
|
|
90
|
-
new HttpMiddlewareError("HTTP middleware error handler threw an error.", { cause: handlerError }),
|
|
91
|
-
]);
|
|
92
|
-
}
|
|
61
|
+
if (!pipelineOptions.onError) {
|
|
62
|
+
throw error;
|
|
93
63
|
}
|
|
94
|
-
|
|
64
|
+
/*
|
|
65
|
+
* A successful `onError` is a genuine recovery and is returned. If the
|
|
66
|
+
* handler throws, what it threw propagates: rethrowing the error it was
|
|
67
|
+
* given passes that original on, and throwing a translated error (a
|
|
68
|
+
* `NotFoundError` for a missing row) replaces it.
|
|
69
|
+
*/
|
|
70
|
+
const errorResult = await pipelineOptions.onError(error, context);
|
|
71
|
+
return normalizeResult(errorResult, response);
|
|
95
72
|
}
|
|
96
73
|
}
|
|
97
74
|
//# sourceMappingURL=httpPipeline.execution.js.map
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Turns a `@zudojs/middleware` guard response into a real response.
|
|
3
|
+
*
|
|
4
|
+
* A middleware that refuses a request — the permissions `authorize()` guard,
|
|
5
|
+
* the tenancy resolver — returns `createGuardResponse({ status, body })`.
|
|
6
|
+
* Route middleware used to ignore any returned object that was not an
|
|
7
|
+
* `HttpResponseContext`, so a plain `{ status: 403, body, headers }` reached
|
|
8
|
+
* the client as `200`: the handler did not run, but clients, caches and
|
|
9
|
+
* monitoring saw a success. Only the branded object is honoured; an unbranded
|
|
10
|
+
* object keeps its previous meaning, so data that happens to carry a
|
|
11
|
+
* `status` key is never read as a response.
|
|
12
|
+
*
|
|
13
|
+
* @module httpMiddleware/pipeline/guardResponse
|
|
14
|
+
*/
|
|
15
|
+
import type { GuardResponse } from "@zudojs/middleware";
|
|
16
|
+
import { HttpResponseContext } from "../../httpResponse/httpResponse.context.js";
|
|
17
|
+
/**
|
|
18
|
+
* Writes a guard response's status, headers and body onto `target`.
|
|
19
|
+
*
|
|
20
|
+
* Headers already on `target` (set by an outer middleware before it called
|
|
21
|
+
* `next()`, such as CORS) are kept unless the guard response overrides them.
|
|
22
|
+
*
|
|
23
|
+
* @param target - The response that stays authoritative.
|
|
24
|
+
* @param guard - The guard response to apply.
|
|
25
|
+
* @returns `target`.
|
|
26
|
+
* @throws {TypeError} If a header value contains a control character.
|
|
27
|
+
*/
|
|
28
|
+
export declare function applyGuardResponse(target: HttpResponseContext, guard: GuardResponse): HttpResponseContext;
|
|
29
|
+
/**
|
|
30
|
+
* Builds a fresh response context from a guard response.
|
|
31
|
+
*
|
|
32
|
+
* @param guard - The guard response to convert.
|
|
33
|
+
* @returns A new response context.
|
|
34
|
+
*/
|
|
35
|
+
export declare function guardResponseToContext(guard: GuardResponse): HttpResponseContext;
|
|
36
|
+
//# sourceMappingURL=httpPipeline.guardResponse.d.ts.map
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Turns a `@zudojs/middleware` guard response into a real response.
|
|
3
|
+
*
|
|
4
|
+
* A middleware that refuses a request — the permissions `authorize()` guard,
|
|
5
|
+
* the tenancy resolver — returns `createGuardResponse({ status, body })`.
|
|
6
|
+
* Route middleware used to ignore any returned object that was not an
|
|
7
|
+
* `HttpResponseContext`, so a plain `{ status: 403, body, headers }` reached
|
|
8
|
+
* the client as `200`: the handler did not run, but clients, caches and
|
|
9
|
+
* monitoring saw a success. Only the branded object is honoured; an unbranded
|
|
10
|
+
* object keeps its previous meaning, so data that happens to carry a
|
|
11
|
+
* `status` key is never read as a response.
|
|
12
|
+
*
|
|
13
|
+
* @module httpMiddleware/pipeline/guardResponse
|
|
14
|
+
*/
|
|
15
|
+
import { HttpResponseContext } from "../../httpResponse/httpResponse.context.js";
|
|
16
|
+
import { isValidHeaderFieldValue } from "../../httpHeaders/security/index.js";
|
|
17
|
+
/**
|
|
18
|
+
* Writes a guard response's status, headers and body onto `target`.
|
|
19
|
+
*
|
|
20
|
+
* Headers already on `target` (set by an outer middleware before it called
|
|
21
|
+
* `next()`, such as CORS) are kept unless the guard response overrides them.
|
|
22
|
+
*
|
|
23
|
+
* @param target - The response that stays authoritative.
|
|
24
|
+
* @param guard - The guard response to apply.
|
|
25
|
+
* @returns `target`.
|
|
26
|
+
* @throws {TypeError} If a header value contains a control character.
|
|
27
|
+
*/
|
|
28
|
+
export function applyGuardResponse(target, guard) {
|
|
29
|
+
target.setStatus(guard.status);
|
|
30
|
+
const body = guard.body;
|
|
31
|
+
if (body === undefined) {
|
|
32
|
+
target.setBody(undefined);
|
|
33
|
+
}
|
|
34
|
+
else if (typeof body === "string" || body instanceof Uint8Array) {
|
|
35
|
+
target.setBody(body);
|
|
36
|
+
}
|
|
37
|
+
else {
|
|
38
|
+
target.json(body);
|
|
39
|
+
}
|
|
40
|
+
for (const [name, value] of Object.entries(guard.headers)) {
|
|
41
|
+
if (!isValidHeaderFieldValue(value)) {
|
|
42
|
+
throw new TypeError(`Invalid value for response header "${name}": it contains a control character or surrounding whitespace.`);
|
|
43
|
+
}
|
|
44
|
+
target.setHeader(name, value);
|
|
45
|
+
}
|
|
46
|
+
return target;
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Builds a fresh response context from a guard response.
|
|
50
|
+
*
|
|
51
|
+
* @param guard - The guard response to convert.
|
|
52
|
+
* @returns A new response context.
|
|
53
|
+
*/
|
|
54
|
+
export function guardResponseToContext(guard) {
|
|
55
|
+
return applyGuardResponse(new HttpResponseContext(), guard);
|
|
56
|
+
}
|
|
57
|
+
//# sourceMappingURL=httpPipeline.guardResponse.js.map
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
*
|
|
4
4
|
* @module httpMiddleware/pipeline/helpers
|
|
5
5
|
*/
|
|
6
|
+
import { type GuardResponse } from "@zudojs/middleware";
|
|
6
7
|
import type { HttpMiddlewareContext, RegisteredMiddleware } from "../httpMiddleware.type.js";
|
|
7
8
|
import { HttpRequestContext as RequestContext } from "../../httpRequest/httpRequest.context.js";
|
|
8
9
|
import { HttpResponseContext as ResponseContext } from "../../httpResponse/httpResponse.context.js";
|
|
@@ -20,7 +21,7 @@ export declare function nextResult(context: HttpMiddlewareContext, dispatch: (in
|
|
|
20
21
|
* @returns The response context to continue the pipeline with.
|
|
21
22
|
* @throws {HttpMiddlewareError} If no response context can be determined.
|
|
22
23
|
*/
|
|
23
|
-
export declare function normalizeResult(result: void | Response | RequestContext | ResponseContext | undefined, fallback?: ResponseContext): ResponseContext;
|
|
24
|
+
export declare function normalizeResult(result: void | Response | RequestContext | ResponseContext | GuardResponse | undefined, fallback?: ResponseContext): ResponseContext;
|
|
24
25
|
/**
|
|
25
26
|
* Narrows a value to a response context.
|
|
26
27
|
*
|
|
@@ -3,10 +3,12 @@
|
|
|
3
3
|
*
|
|
4
4
|
* @module httpMiddleware/pipeline/helpers
|
|
5
5
|
*/
|
|
6
|
+
import { isGuardResponse } from "@zudojs/middleware";
|
|
6
7
|
import { HttpRequestContext as RequestContext } from "../../httpRequest/httpRequest.context.js";
|
|
7
8
|
import { HttpResponseContext as ResponseContext } from "../../httpResponse/httpResponse.context.js";
|
|
8
9
|
import { isWebResponse, webResponseToContext, } from "../../httpResponse/httpResponse.fromWeb.js";
|
|
9
10
|
import { HttpMiddlewareError } from "../httpMiddleware.error.js";
|
|
11
|
+
import { guardResponseToContext } from "./httpPipeline.guardResponse.js";
|
|
10
12
|
export async function nextResult(context, dispatch, index) {
|
|
11
13
|
return dispatch(index + 1);
|
|
12
14
|
}
|
|
@@ -24,6 +26,14 @@ export async function nextResult(context, dispatch, index) {
|
|
|
24
26
|
* @throws {HttpMiddlewareError} If no response context can be determined.
|
|
25
27
|
*/
|
|
26
28
|
export function normalizeResult(result, fallback) {
|
|
29
|
+
/*
|
|
30
|
+
* A guard response is checked before the structural `isResponseContext`
|
|
31
|
+
* test, which would otherwise pass the frozen plain object through as if it
|
|
32
|
+
* were a response context.
|
|
33
|
+
*/
|
|
34
|
+
if (isGuardResponse(result)) {
|
|
35
|
+
return guardResponseToContext(result);
|
|
36
|
+
}
|
|
27
37
|
/*
|
|
28
38
|
* The `Response` check must come first. `isResponseContext` tests for a
|
|
29
39
|
* `headers` property, and `headers` is a getter on `Response.prototype`, so
|
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
*/
|
|
6
6
|
export * from "./httpMiddleware.pipeline.js";
|
|
7
7
|
export * from "./httpPipeline.execution.js";
|
|
8
|
+
export * from "./httpPipeline.guardResponse.js";
|
|
8
9
|
export * from "./httpPipeline.helper.js";
|
|
9
10
|
export * from "./httpPipeline.registration.js";
|
|
10
11
|
//# sourceMappingURL=index.d.ts.map
|
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
*/
|
|
6
6
|
export * from "./httpMiddleware.pipeline.js";
|
|
7
7
|
export * from "./httpPipeline.execution.js";
|
|
8
|
+
export * from "./httpPipeline.guardResponse.js";
|
|
8
9
|
export * from "./httpPipeline.helper.js";
|
|
9
10
|
export * from "./httpPipeline.registration.js";
|
|
10
11
|
//# sourceMappingURL=index.js.map
|
|
@@ -99,6 +99,19 @@ export declare function negotiateCharset(header: string | undefined | null, avai
|
|
|
99
99
|
* @returns The selected alternative, or `undefined` if none is acceptable.
|
|
100
100
|
*/
|
|
101
101
|
export declare function negotiate<T>(preferences: readonly NegotiationPreference[], available: readonly T[], matcher: (accepted: string, available: T) => boolean): T | undefined;
|
|
102
|
+
/**
|
|
103
|
+
* Returns the weight a preference list assigns to one value.
|
|
104
|
+
*
|
|
105
|
+
* The **most specific** match wins, and only then the highest weight, per
|
|
106
|
+
* RFC 9110 section 12.4.2. Ranking by weight first let `*;q=1` override an
|
|
107
|
+
* explicit `gzip;q=0`, so a coding the client had refused came back with
|
|
108
|
+
* full quality.
|
|
109
|
+
*
|
|
110
|
+
* @param preferences - The parsed preferences.
|
|
111
|
+
* @param value - The alternative to weigh.
|
|
112
|
+
* @param matcher - Matches a preference value against an alternative.
|
|
113
|
+
* @returns The quality in `[0, 1]`, or `0` when nothing matches.
|
|
114
|
+
*/
|
|
102
115
|
export declare function getPreferenceQuality<T>(preferences: readonly NegotiationPreference[], value: T, matcher: (accepted: string, available: T) => boolean): number;
|
|
103
116
|
export declare function normalizeMediaType(value: string): string;
|
|
104
117
|
export declare function splitMediaType(value: string): [string, string] | undefined;
|
|
@@ -214,7 +214,36 @@ export function negotiateEncoding(header, available) {
|
|
|
214
214
|
if (preferences.length === 0) {
|
|
215
215
|
return available[0];
|
|
216
216
|
}
|
|
217
|
-
|
|
217
|
+
const selected = negotiate(preferences, available, matchesEncoding);
|
|
218
|
+
if (selected !== undefined) {
|
|
219
|
+
return selected;
|
|
220
|
+
}
|
|
221
|
+
/*
|
|
222
|
+
* RFC 9110 section 12.5.3: a representation with no content coding is
|
|
223
|
+
* acceptable unless the field explicitly excludes it with `identity;q=0`
|
|
224
|
+
* or a `*;q=0` that no identity entry overrides. Returning `undefined`
|
|
225
|
+
* here made `Accept-Encoding: zstd` look like "nothing is acceptable", so
|
|
226
|
+
* a caller answered 406 for a request it could have served uncompressed.
|
|
227
|
+
*/
|
|
228
|
+
const identity = available.find((value) => isIdentityEncoding(value));
|
|
229
|
+
if (identity === undefined || isIdentityRejected(preferences)) {
|
|
230
|
+
return undefined;
|
|
231
|
+
}
|
|
232
|
+
return identity;
|
|
233
|
+
}
|
|
234
|
+
/**
|
|
235
|
+
* Reports whether an `Accept-Encoding` field rejects the identity coding.
|
|
236
|
+
*
|
|
237
|
+
* @param preferences - The parsed preferences.
|
|
238
|
+
* @returns `true` when identity must not be served.
|
|
239
|
+
*/
|
|
240
|
+
function isIdentityRejected(preferences) {
|
|
241
|
+
const explicit = preferences.find((preference) => isIdentityEncoding(preference.value));
|
|
242
|
+
if (explicit) {
|
|
243
|
+
return !isAcceptableQuality(explicit.quality);
|
|
244
|
+
}
|
|
245
|
+
const wildcard = preferences.find((preference) => isWildcardEncoding(preference.value));
|
|
246
|
+
return wildcard !== undefined && !isAcceptableQuality(wildcard.quality);
|
|
218
247
|
}
|
|
219
248
|
export function getEncodingQuality(header, encoding) {
|
|
220
249
|
const preferences = parseAcceptEncoding(header);
|
|
@@ -319,20 +348,43 @@ function isExcluded(candidate, selected, rejections, matcher) {
|
|
|
319
348
|
return rejections.some((rejection) => rejection.specificity >= selected.specificity &&
|
|
320
349
|
matcher(rejection.value, candidate));
|
|
321
350
|
}
|
|
351
|
+
/**
|
|
352
|
+
* Returns the weight a preference list assigns to one value.
|
|
353
|
+
*
|
|
354
|
+
* The **most specific** match wins, and only then the highest weight, per
|
|
355
|
+
* RFC 9110 section 12.4.2. Ranking by weight first let `*;q=1` override an
|
|
356
|
+
* explicit `gzip;q=0`, so a coding the client had refused came back with
|
|
357
|
+
* full quality.
|
|
358
|
+
*
|
|
359
|
+
* @param preferences - The parsed preferences.
|
|
360
|
+
* @param value - The alternative to weigh.
|
|
361
|
+
* @param matcher - Matches a preference value against an alternative.
|
|
362
|
+
* @returns The quality in `[0, 1]`, or `0` when nothing matches.
|
|
363
|
+
*/
|
|
322
364
|
export function getPreferenceQuality(preferences, value, matcher) {
|
|
323
365
|
let best;
|
|
324
366
|
for (const preference of preferences) {
|
|
325
|
-
if (matcher(preference.value, value)) {
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
best = preference;
|
|
331
|
-
}
|
|
367
|
+
if (!matcher(preference.value, value)) {
|
|
368
|
+
continue;
|
|
369
|
+
}
|
|
370
|
+
if (!best || isMoreRelevant(preference, best)) {
|
|
371
|
+
best = preference;
|
|
332
372
|
}
|
|
333
373
|
}
|
|
334
374
|
return best?.quality ?? 0;
|
|
335
375
|
}
|
|
376
|
+
/**
|
|
377
|
+
* Orders two matching preferences: specificity, then weight, then position.
|
|
378
|
+
*/
|
|
379
|
+
function isMoreRelevant(candidate, best) {
|
|
380
|
+
if (candidate.specificity !== best.specificity) {
|
|
381
|
+
return candidate.specificity > best.specificity;
|
|
382
|
+
}
|
|
383
|
+
if (candidate.quality !== best.quality) {
|
|
384
|
+
return candidate.quality > best.quality;
|
|
385
|
+
}
|
|
386
|
+
return false;
|
|
387
|
+
}
|
|
336
388
|
/* -------------------------------------------------------------------------- */
|
|
337
389
|
/* Media Type Helpers */
|
|
338
390
|
/* -------------------------------------------------------------------------- */
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* OpenAPI document generation from a router's registered routes.
|
|
3
|
+
*/
|
|
4
|
+
import { type OpenAPIDocument, type OpenAPIManager } from "@zudojs/openapi";
|
|
5
|
+
import type { HttpOpenAPIOptions, HttpOpenAPIRouteSource } from "./httpOpenApi.type.js";
|
|
6
|
+
/**
|
|
7
|
+
* Generates an OpenAPI document from the routes a router has registered.
|
|
8
|
+
*
|
|
9
|
+
* Nothing is added by hand: every route's method and path come from the
|
|
10
|
+
* router, and its summary, tags, schemas and responses from the `openapi`
|
|
11
|
+
* option it was registered with. Call it again after adding routes and the
|
|
12
|
+
* new routes are in the result.
|
|
13
|
+
*
|
|
14
|
+
* ```ts
|
|
15
|
+
* const router = createRouter();
|
|
16
|
+
* router.get("/users/:id", getUser, {
|
|
17
|
+
* openapi: { summary: "Get a user", tags: ["users"],
|
|
18
|
+
* responses: { "200": { schema: userSchema } } },
|
|
19
|
+
* });
|
|
20
|
+
* const document = generateOpenAPIDocument(router, {
|
|
21
|
+
* info: { title: "Users API", version: "1.0.0" },
|
|
22
|
+
* exclude: ["/health"],
|
|
23
|
+
* });
|
|
24
|
+
* ```
|
|
25
|
+
*
|
|
26
|
+
* @throws {OpenAPIRouteError} When a documented route cannot be expressed.
|
|
27
|
+
* @throws {OpenAPIValidationError} When `validate` is set and the document
|
|
28
|
+
* is invalid.
|
|
29
|
+
*/
|
|
30
|
+
export declare function generateOpenAPIDocument(router: HttpOpenAPIRouteSource, options: HttpOpenAPIOptions): OpenAPIDocument;
|
|
31
|
+
/** A manager that follows a router's route table. */
|
|
32
|
+
export interface HttpRouterOpenAPI {
|
|
33
|
+
/** The manager, refreshed from the router. */
|
|
34
|
+
manager(): OpenAPIManager;
|
|
35
|
+
/** The current document, regenerated only when the route table changed. */
|
|
36
|
+
document(): OpenAPIDocument;
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Creates an `OpenAPIManager` kept in step with `router`: each access checks
|
|
40
|
+
* the route table and re-reads it only when a route was added or removed,
|
|
41
|
+
* so serving the document stays cheap while never going stale.
|
|
42
|
+
*/
|
|
43
|
+
export declare function createRouterOpenAPI(router: HttpOpenAPIRouteSource, options: HttpOpenAPIOptions): HttpRouterOpenAPI;
|
|
44
|
+
//# sourceMappingURL=httpOpenApi.document.d.ts.map
|