@zudojs/http 1.2.0 → 1.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (127) hide show
  1. package/README.md +221 -0
  2. package/dist/httpAdapter/http.adapters.d.ts +24 -3
  3. package/dist/httpAdapter/http.adapters.js +20 -11
  4. package/dist/httpAdapter/node/httpNode.adapter.d.ts +2 -1
  5. package/dist/httpAdapter/node/httpNode.adapter.js +17 -2
  6. package/dist/httpAdapter/node/httpNode.request.js +7 -1
  7. package/dist/httpAdapter/node/httpNode.type.d.ts +14 -0
  8. package/dist/httpAgent/http.agent.d.ts +22 -3
  9. package/dist/httpAgent/http.agent.js +52 -14
  10. package/dist/httpCacheControl/httpCacheControl.freshness.d.ts +7 -1
  11. package/dist/httpCacheControl/httpCacheControl.freshness.js +30 -3
  12. package/dist/httpClient/httpClient.retry.d.ts +17 -12
  13. package/dist/httpClient/httpClient.retry.js +35 -10
  14. package/dist/httpClient/httpClient.type.d.ts +14 -0
  15. package/dist/httpErrors/httpError.base.js +2 -2
  16. package/dist/httpErrors/httpError.util.d.ts +8 -0
  17. package/dist/httpErrors/httpError.util.js +12 -0
  18. package/dist/httpFetchMount/httpFetchMount.core.d.ts +25 -0
  19. package/dist/httpFetchMount/httpFetchMount.core.js +84 -0
  20. package/dist/httpFetchMount/httpFetchMount.request.d.ts +21 -0
  21. package/dist/httpFetchMount/httpFetchMount.request.js +100 -0
  22. package/dist/httpFetchMount/httpFetchMount.type.d.ts +56 -0
  23. package/dist/httpFetchMount/httpFetchMount.type.js +5 -0
  24. package/dist/httpFetchMount/index.d.ts +11 -0
  25. package/dist/httpFetchMount/index.js +10 -0
  26. package/dist/httpKeepAlive/httpKeepAlive.core.js +14 -2
  27. package/dist/httpMiddleware/builtin/logging/httpMiddleware.logging.d.ts +18 -0
  28. package/dist/httpMiddleware/builtin/logging/httpMiddleware.logging.js +17 -1
  29. package/dist/httpMiddleware/builtin/rateLimit/httpMiddleware.rateLimit.d.ts +6 -3
  30. package/dist/httpMiddleware/builtin/rateLimit/httpMiddleware.rateLimit.js +34 -6
  31. package/dist/httpMiddleware/builtin/security/httpMiddleware.security.d.ts +11 -0
  32. package/dist/httpMiddleware/builtin/security/httpMiddleware.security.js +25 -12
  33. package/dist/httpMiddleware/httpMiddleware.type.d.ts +9 -1
  34. package/dist/httpMiddleware/pipeline/httpPipeline.execution.js +22 -45
  35. package/dist/httpMiddleware/pipeline/httpPipeline.guardResponse.d.ts +36 -0
  36. package/dist/httpMiddleware/pipeline/httpPipeline.guardResponse.js +57 -0
  37. package/dist/httpMiddleware/pipeline/httpPipeline.helper.d.ts +2 -1
  38. package/dist/httpMiddleware/pipeline/httpPipeline.helper.js +10 -0
  39. package/dist/httpMiddleware/pipeline/index.d.ts +1 -0
  40. package/dist/httpMiddleware/pipeline/index.js +1 -0
  41. package/dist/httpNegotiation/httpNegotiation.core.d.ts +13 -0
  42. package/dist/httpNegotiation/httpNegotiation.core.js +60 -8
  43. package/dist/httpOpenApi/httpOpenApi.document.d.ts +44 -0
  44. package/dist/httpOpenApi/httpOpenApi.document.js +61 -0
  45. package/dist/httpOpenApi/httpOpenApi.mount.d.ts +31 -0
  46. package/dist/httpOpenApi/httpOpenApi.mount.js +58 -0
  47. package/dist/httpOpenApi/httpOpenApi.type.d.ts +54 -0
  48. package/dist/httpOpenApi/httpOpenApi.type.js +5 -0
  49. package/dist/httpOpenApi/index.d.ts +13 -0
  50. package/dist/httpOpenApi/index.js +12 -0
  51. package/dist/httpOpenApi/routeTable/index.d.ts +11 -0
  52. package/dist/httpOpenApi/routeTable/index.js +11 -0
  53. package/dist/httpOpenApi/routeTable/routeTable.collect.d.ts +19 -0
  54. package/dist/httpOpenApi/routeTable/routeTable.collect.js +89 -0
  55. package/dist/httpOpenApi/routeTable/routeTable.merge.d.ts +15 -0
  56. package/dist/httpOpenApi/routeTable/routeTable.merge.js +37 -0
  57. package/dist/httpOpenApi/routeTable/routeTable.template.d.ts +30 -0
  58. package/dist/httpOpenApi/routeTable/routeTable.template.js +67 -0
  59. package/dist/httpProxy/http.proxy.d.ts +9 -0
  60. package/dist/httpProxy/http.proxy.js +35 -3
  61. package/dist/httpQuery/index.d.ts +11 -2
  62. package/dist/httpQuery/index.js +11 -2
  63. package/dist/httpQuery/queryParse/index.d.ts +10 -0
  64. package/dist/httpQuery/queryParse/index.js +10 -0
  65. package/dist/httpQuery/queryParse/queryParse.flat.d.ts +13 -0
  66. package/dist/httpQuery/queryParse/queryParse.flat.js +44 -0
  67. package/dist/httpQuery/queryParse/queryParse.nested.d.ts +25 -0
  68. package/dist/httpQuery/queryParse/queryParse.nested.js +112 -0
  69. package/dist/httpQuery/queryParse/queryParse.tokenizer.d.ts +37 -0
  70. package/dist/httpQuery/queryParse/queryParse.tokenizer.js +95 -0
  71. package/dist/httpQuery/queryRequest/index.d.ts +9 -0
  72. package/dist/httpQuery/queryRequest/index.js +9 -0
  73. package/dist/httpQuery/queryRequest/query.request.d.ts +43 -0
  74. package/dist/httpQuery/queryRequest/query.request.js +96 -0
  75. package/dist/httpQuery/querySerialize/index.d.ts +10 -0
  76. package/dist/httpQuery/querySerialize/index.js +10 -0
  77. package/dist/httpQuery/querySerialize/query.util.d.ts +21 -0
  78. package/dist/httpQuery/querySerialize/query.util.js +67 -0
  79. package/dist/httpQuery/querySerialize/querySerialize.core.d.ts +12 -0
  80. package/dist/httpQuery/querySerialize/querySerialize.core.js +97 -0
  81. package/dist/httpQuery/queryTypes/index.d.ts +11 -0
  82. package/dist/httpQuery/queryTypes/index.js +9 -0
  83. package/dist/httpQuery/queryTypes/query.container.d.ts +16 -0
  84. package/dist/httpQuery/queryTypes/query.container.js +51 -0
  85. package/dist/httpQuery/queryTypes/query.limit.d.ts +25 -0
  86. package/dist/httpQuery/queryTypes/query.limit.js +32 -0
  87. package/dist/httpQuery/queryTypes/query.type.d.ts +62 -0
  88. package/dist/httpQuery/queryTypes/query.type.js +2 -0
  89. package/dist/httpRedirect/http.redirect.d.ts +6 -0
  90. package/dist/httpRedirect/http.redirect.js +53 -2
  91. package/dist/httpRequest/http.request.d.ts +61 -2
  92. package/dist/httpRequest/http.request.js +86 -35
  93. package/dist/httpRequest/httpRequest.context.d.ts +8 -0
  94. package/dist/httpRequest/httpRequest.context.js +23 -18
  95. package/dist/httpRequest/index.d.ts +1 -0
  96. package/dist/httpRequest/index.js +1 -0
  97. package/dist/httpRequest/requestId/httpRequest.requestId.d.ts +25 -0
  98. package/dist/httpRequest/requestId/httpRequest.requestId.js +34 -0
  99. package/dist/httpRequest/requestId/index.d.ts +7 -0
  100. package/dist/httpRequest/requestId/index.js +7 -0
  101. package/dist/httpRequest/target/httpRequest.target.d.ts +2 -2
  102. package/dist/httpRequest/target/httpRequest.target.js +23 -5
  103. package/dist/httpResponse/httpResponse.writer.js +15 -0
  104. package/dist/httpRouter/core/factory/httpRoute.factory.base.d.ts +24 -4
  105. package/dist/httpRouter/core/factory/httpRoute.factory.base.js +73 -17
  106. package/dist/httpRouter/core/factory/httpRoute.factory.js +3 -3
  107. package/dist/httpRouter/core/group/httpRouterGroup.core.js +13 -0
  108. package/dist/httpRouter/core/register/httpRouter.register.js +19 -21
  109. package/dist/httpRouter/core/types/httpRouter.type.d.ts +37 -1
  110. package/dist/httpRouter/core/util/httpRoute.util.d.ts +55 -0
  111. package/dist/httpRouter/core/util/httpRoute.util.js +101 -4
  112. package/dist/httpRouter/dispatch/httpRoute.dispatcher.d.ts +11 -0
  113. package/dist/httpRouter/dispatch/httpRoute.dispatcher.js +25 -6
  114. package/dist/httpRouter/matching/httpRoute.matcher.core.js +51 -6
  115. package/dist/httpRouter/matching/httpRoute.matcher.d.ts +0 -1
  116. package/dist/httpRouter/matching/httpRoute.matcher.js +64 -26
  117. package/dist/httpRouter/pattern/httpRoute.pattern.parse.d.ts +16 -0
  118. package/dist/httpRouter/pattern/httpRoute.pattern.parse.js +40 -11
  119. package/dist/httpRouter/pattern/index.d.ts +1 -1
  120. package/dist/httpRouter/pattern/index.js +1 -1
  121. package/dist/httpSecurity/httpSecurity.config.js +4 -1
  122. package/dist/httpSecurity/httpSecurity.validator.js +16 -7
  123. package/dist/httpServer/factory/httpServer.factory.d.ts +10 -9
  124. package/dist/httpServer/factory/httpServer.factory.js +8 -0
  125. package/dist/index.d.ts +2 -0
  126. package/dist/index.js +2 -0
  127. package/package.json +11 -8
@@ -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` (with
43
- * `Retry-After`, from the `@zudojs/security` handler) once a client exceeds
44
- * its allowance, and otherwise passes the request on.
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` (with
31
- * `Retry-After`, from the `@zudojs/security` handler) once a client exceeds
32
- * its allowance, and otherwise passes the request on.
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
- if (limiter.middleware(limitRequest, rejection).allowed) {
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
- return response.setBody(rejection.body ?? "Too Many Requests");
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
- * Headers applied when the caller does not override them.
11
+ * The headers applied when the caller does not override them.
10
12
  *
11
- * `createSecurityMiddleware()` previously defaulted every option to
12
- * `undefined` and therefore set no headers at all — its presence in a
13
- * codebase read as evidence that the control existed while shipping nothing.
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
- export type HttpMiddlewareResult = void | Response | RequestContext | ResponseContext | Promise<void | Response | RequestContext | ResponseContext>;
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, HttpMiddlewarePipelineError, } from "../httpMiddleware.error.js";
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
- try {
46
- const result = await entry.middleware(context, next);
47
- return normalizeResult(result, response);
48
- }
49
- catch (error) {
50
- /*
51
- * Rethrow rather than returning the untouched response. Swallowing here
52
- * let an inner failure resume the *outer* frames, so middleware that
53
- * runs after `await next()` — access logging, CORS and security header
54
- * emission, audit commits — executed against a response that was about
55
- * to be discarded, and recorded the request as a success.
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
- const middlewareError = error instanceof HttpMiddlewareError
71
- ? error
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
- throw new HttpMiddlewarePipelineError([middlewareError]);
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
- return negotiate(preferences, available, matchesEncoding);
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
- if (!best ||
327
- preference.quality > best.quality ||
328
- (preference.quality === best.quality &&
329
- preference.specificity > best.specificity)) {
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