@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.
Files changed (137) hide show
  1. package/README.md +53 -3
  2. package/dist/httpAdapter/http.adapter.d.ts +34 -3
  3. package/dist/httpAdapter/http.adapter.js +22 -6
  4. package/dist/httpAdapter/http.adapters.d.ts +24 -3
  5. package/dist/httpAdapter/http.adapters.js +24 -45
  6. package/dist/httpAdapter/httpAdapter.errorResponse.d.ts +2 -2
  7. package/dist/httpAdapter/httpAdapter.errorResponse.js +26 -9
  8. package/dist/httpAdapter/httpAdapter.logger.d.ts +19 -0
  9. package/dist/httpAdapter/httpAdapter.logger.js +21 -0
  10. package/dist/httpAdapter/node/httpNode.adapter.d.ts +12 -1
  11. package/dist/httpAdapter/node/httpNode.adapter.js +28 -14
  12. package/dist/httpAdapter/node/httpNode.request.d.ts +8 -7
  13. package/dist/httpAdapter/node/httpNode.request.js +17 -27
  14. package/dist/httpAdapter/node/httpNode.server.d.ts +6 -0
  15. package/dist/httpAdapter/node/httpNode.server.js +6 -0
  16. package/dist/httpAdapter/node/httpNode.type.d.ts +2 -1
  17. package/dist/httpAgent/http.agent.d.ts +22 -3
  18. package/dist/httpAgent/http.agent.js +52 -14
  19. package/dist/httpCacheControl/httpCacheControl.freshness.d.ts +7 -1
  20. package/dist/httpCacheControl/httpCacheControl.freshness.js +30 -3
  21. package/dist/httpCookies/http.cookies.d.ts +29 -2
  22. package/dist/httpCookies/http.cookies.js +44 -24
  23. package/dist/httpCookies/httpCookies.defaults.d.ts +40 -0
  24. package/dist/httpCookies/httpCookies.defaults.js +42 -0
  25. package/dist/httpCookies/index.d.ts +1 -0
  26. package/dist/httpCookies/index.js +1 -0
  27. package/dist/httpKeepAlive/httpKeepAlive.core.js +14 -2
  28. package/dist/httpMiddleware/builtin/conditional/httpMiddleware.conditional.d.ts +22 -1
  29. package/dist/httpMiddleware/builtin/conditional/httpMiddleware.conditional.js +19 -4
  30. package/dist/httpMiddleware/builtin/conditional/index.d.ts +1 -0
  31. package/dist/httpMiddleware/builtin/cors/httpMiddleware.cors.d.ts +8 -0
  32. package/dist/httpMiddleware/builtin/cors/httpMiddleware.cors.js +25 -22
  33. package/dist/httpMiddleware/builtin/helpers/httpMiddleware.accessor.js +2 -6
  34. package/dist/httpMiddleware/builtin/helpers/httpMiddleware.media.d.ts +33 -0
  35. package/dist/httpMiddleware/builtin/helpers/httpMiddleware.media.js +59 -0
  36. package/dist/httpMiddleware/builtin/helpers/index.d.ts +1 -0
  37. package/dist/httpMiddleware/builtin/helpers/index.js +1 -0
  38. package/dist/httpMiddleware/builtin/image/httpMiddleware.image.d.ts +13 -0
  39. package/dist/httpMiddleware/builtin/image/httpMiddleware.image.js +31 -53
  40. package/dist/httpMiddleware/builtin/index.d.ts +1 -0
  41. package/dist/httpMiddleware/builtin/index.js +1 -0
  42. package/dist/httpMiddleware/builtin/logging/httpMiddleware.logging.d.ts +18 -0
  43. package/dist/httpMiddleware/builtin/logging/httpMiddleware.logging.js +17 -1
  44. package/dist/httpMiddleware/builtin/rateLimit/httpMiddleware.rateLimit.d.ts +47 -0
  45. package/dist/httpMiddleware/builtin/rateLimit/httpMiddleware.rateLimit.js +61 -0
  46. package/dist/httpMiddleware/builtin/rateLimit/index.d.ts +7 -0
  47. package/dist/httpMiddleware/builtin/rateLimit/index.js +7 -0
  48. package/dist/httpMiddleware/builtin/security/httpMiddleware.security.d.ts +11 -0
  49. package/dist/httpMiddleware/builtin/security/httpMiddleware.security.js +25 -12
  50. package/dist/httpMiddleware/builtin/static/httpMiddleware.static.js +18 -4
  51. package/dist/httpMiddleware/builtin/video/httpMiddleware.video.d.ts +19 -1
  52. package/dist/httpMiddleware/builtin/video/httpMiddleware.video.js +38 -59
  53. package/dist/httpMiddleware/builtin/video/httpMiddleware.video.options.d.ts +12 -0
  54. package/dist/httpMiddleware/builtin/video/httpMiddleware.video.options.js +31 -0
  55. package/dist/httpMiddleware/httpMiddleware.error.d.ts +5 -31
  56. package/dist/httpMiddleware/httpMiddleware.error.js +5 -53
  57. package/dist/httpNegotiation/httpNegotiation.core.d.ts +13 -0
  58. package/dist/httpNegotiation/httpNegotiation.core.js +60 -8
  59. package/dist/httpProxy/http.proxy.d.ts +16 -0
  60. package/dist/httpProxy/http.proxy.js +44 -3
  61. package/dist/httpProxy/httpProxy.pathGuard.d.ts +23 -0
  62. package/dist/httpProxy/httpProxy.pathGuard.js +43 -0
  63. package/dist/httpProxy/index.d.ts +1 -0
  64. package/dist/httpProxy/index.js +1 -0
  65. package/dist/httpQuery/index.d.ts +11 -2
  66. package/dist/httpQuery/index.js +11 -2
  67. package/dist/httpQuery/queryParse/index.d.ts +10 -0
  68. package/dist/httpQuery/queryParse/index.js +10 -0
  69. package/dist/httpQuery/queryParse/queryParse.flat.d.ts +13 -0
  70. package/dist/httpQuery/queryParse/queryParse.flat.js +44 -0
  71. package/dist/httpQuery/queryParse/queryParse.nested.d.ts +25 -0
  72. package/dist/httpQuery/queryParse/queryParse.nested.js +112 -0
  73. package/dist/httpQuery/queryParse/queryParse.tokenizer.d.ts +37 -0
  74. package/dist/httpQuery/queryParse/queryParse.tokenizer.js +95 -0
  75. package/dist/httpQuery/queryRequest/index.d.ts +9 -0
  76. package/dist/httpQuery/queryRequest/index.js +9 -0
  77. package/dist/httpQuery/queryRequest/query.request.d.ts +43 -0
  78. package/dist/httpQuery/queryRequest/query.request.js +96 -0
  79. package/dist/httpQuery/querySerialize/index.d.ts +10 -0
  80. package/dist/httpQuery/querySerialize/index.js +10 -0
  81. package/dist/httpQuery/querySerialize/query.util.d.ts +21 -0
  82. package/dist/httpQuery/querySerialize/query.util.js +67 -0
  83. package/dist/httpQuery/querySerialize/querySerialize.core.d.ts +12 -0
  84. package/dist/httpQuery/querySerialize/querySerialize.core.js +97 -0
  85. package/dist/httpQuery/queryTypes/index.d.ts +11 -0
  86. package/dist/httpQuery/queryTypes/index.js +9 -0
  87. package/dist/httpQuery/queryTypes/query.container.d.ts +16 -0
  88. package/dist/httpQuery/queryTypes/query.container.js +51 -0
  89. package/dist/httpQuery/queryTypes/query.limit.d.ts +25 -0
  90. package/dist/httpQuery/queryTypes/query.limit.js +32 -0
  91. package/dist/httpQuery/queryTypes/query.type.d.ts +62 -0
  92. package/dist/httpQuery/queryTypes/query.type.js +2 -0
  93. package/dist/httpRedirect/http.redirect.d.ts +6 -0
  94. package/dist/httpRedirect/http.redirect.js +53 -2
  95. package/dist/httpRequest/http.request.d.ts +61 -2
  96. package/dist/httpRequest/http.request.js +86 -35
  97. package/dist/httpRequest/httpRequest.context.d.ts +7 -0
  98. package/dist/httpRequest/httpRequest.context.js +26 -34
  99. package/dist/httpRequest/index.d.ts +1 -0
  100. package/dist/httpRequest/index.js +1 -0
  101. package/dist/httpRequest/target/httpRequest.target.d.ts +45 -0
  102. package/dist/httpRequest/target/httpRequest.target.js +111 -0
  103. package/dist/httpRequest/target/index.d.ts +9 -0
  104. package/dist/httpRequest/target/index.js +9 -0
  105. package/dist/httpResponse/httpResponse.helper.js +2 -1
  106. package/dist/httpRouter/core/factory/httpRoute.factory.base.d.ts +18 -1
  107. package/dist/httpRouter/core/factory/httpRoute.factory.base.js +49 -6
  108. package/dist/httpRouter/core/factory/httpRoute.factory.js +3 -3
  109. package/dist/httpRouter/core/register/httpRouter.register.js +16 -21
  110. package/dist/httpRouter/core/types/httpRouter.type.d.ts +6 -0
  111. package/dist/httpRouter/core/util/httpRoute.util.d.ts +61 -0
  112. package/dist/httpRouter/core/util/httpRoute.util.js +102 -17
  113. package/dist/httpRouter/dispatch/httpRoute.dispatcher.d.ts +11 -0
  114. package/dist/httpRouter/dispatch/httpRoute.dispatcher.js +5 -3
  115. package/dist/httpRouter/matching/httpRoute.matcher.core.js +51 -6
  116. package/dist/httpRouter/matching/httpRoute.matcher.d.ts +0 -1
  117. package/dist/httpRouter/matching/httpRoute.matcher.js +64 -26
  118. package/dist/httpRouter/pattern/httpRoute.pattern.parse.d.ts +16 -0
  119. package/dist/httpRouter/pattern/httpRoute.pattern.parse.js +40 -11
  120. package/dist/httpRouter/pattern/index.d.ts +1 -1
  121. package/dist/httpRouter/pattern/index.js +1 -1
  122. package/dist/httpSecurity/httpSecurity.config.d.ts +6 -0
  123. package/dist/httpSecurity/httpSecurity.config.js +1 -0
  124. package/dist/httpSecurity/httpSecurity.guard.d.ts +7 -9
  125. package/dist/httpSecurity/httpSecurity.guard.js +7 -14
  126. package/dist/httpSecurity/httpSecurity.nodeGuard.d.ts +48 -0
  127. package/dist/httpSecurity/httpSecurity.nodeGuard.js +32 -0
  128. package/dist/httpSecurity/httpSecurity.validator.js +20 -9
  129. package/dist/httpSecurity/index.d.ts +2 -0
  130. package/dist/httpSecurity/index.js +1 -0
  131. package/dist/httpServer/core/httpServer.core.d.ts +11 -0
  132. package/dist/httpServer/core/httpServer.core.js +30 -4
  133. package/dist/httpTrustProxy/httpTrustProxy.compilation.d.ts +5 -2
  134. package/dist/httpTrustProxy/httpTrustProxy.compilation.js +19 -2
  135. package/dist/httpTrustProxy/httpTrustProxy.type.d.ts +3 -1
  136. package/dist/index.js +0 -1
  137. package/package.json +5 -5
@@ -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;
@@ -7,6 +7,8 @@ import { readFile, stat } from "node:fs/promises";
7
7
  import { createHash } from "node:crypto";
8
8
  import { extname, join, resolve, sep } from "node:path";
9
9
  import { applyHeadersToResponse } from "../helpers/index.js";
10
+ import { parseRequestTarget } from "../../../httpRequest/target/httpRequest.target.js";
11
+ import { evaluateConditionalRequest } from "../../../httpConditional/httpConditional.core.js";
10
12
  const DEFAULT_INDEX = "index.html";
11
13
  const DEFAULT_MAX_AGE = 3600;
12
14
  const DEFAULT_MAX_FILE_SIZE = 10 * 1024 * 1024;
@@ -141,7 +143,7 @@ export function createStaticMiddleware(options) {
141
143
  */
142
144
  let url;
143
145
  try {
144
- url = new URL(context.request.url, "http://zudojs.invalid");
146
+ url = parseRequestTarget(context.request.url);
145
147
  }
146
148
  catch {
147
149
  return next();
@@ -200,9 +202,21 @@ export function createStaticMiddleware(options) {
200
202
  * returns the whole body and has no 206 path, so advertising range
201
203
  * support makes range-aware clients misbehave.
202
204
  */
203
- const ifNoneMatch = context.request.headers["if-none-match"];
204
- if (ifNoneMatch === etag) {
205
- return applyHeadersToResponse(context.response, responseHeaders).setStatus(304);
205
+ /*
206
+ * RFC 9110 section 13.2.2 evaluation: `If-None-Match` lists, weak tags
207
+ * and `*`, then `If-Modified-Since`, plus `If-Match` /
208
+ * `If-Unmodified-Since` (412). An exact string compare missed all of
209
+ * those and re-sent the whole file.
210
+ */
211
+ const headers = context.request.headers;
212
+ const conditional = evaluateConditionalRequest(context.request.method, {
213
+ ifMatch: headers["if-match"],
214
+ ifNoneMatch: headers["if-none-match"],
215
+ ifModifiedSince: headers["if-modified-since"],
216
+ ifUnmodifiedSince: headers["if-unmodified-since"],
217
+ }, { etag, lastModified });
218
+ if (conditional.statusCode !== undefined) {
219
+ return applyHeadersToResponse(context.response, responseHeaders).setStatus(conditional.statusCode);
206
220
  }
207
221
  return applyHeadersToResponse(context.response, responseHeaders).setBody(fileData);
208
222
  };
@@ -3,14 +3,17 @@
3
3
  *
4
4
  * @module httpMiddleware/builtin/video
5
5
  *
6
- * Requires: npm install fluent-ffmpeg
6
+ * Requires: npm install fluent-ffmpeg (and an ffmpeg binary)
7
7
  */
8
8
  import type { HttpMiddleware } from "../../httpMiddleware.type.js";
9
+ import { type MediaCompressionErrorHandler } from "../helpers/index.js";
9
10
  export interface VideoCompressionOptions {
10
11
  readonly bitrate?: string;
11
12
  readonly preset?: "ultrafast" | "superfast" | "veryfast" | "faster" | "fast" | "medium" | "slow" | "slower" | "veryslow";
12
13
  readonly crf?: number;
13
14
  readonly format?: "mp4" | "webm" | "mov";
15
+ /** Input container; defaults to `mp4`. */
16
+ readonly inputFormat?: string;
14
17
  readonly scale?: {
15
18
  readonly width?: number;
16
19
  readonly height?: number;
@@ -20,7 +23,22 @@ export interface VideoCompressionMiddlewareOptions {
20
23
  readonly enabled?: boolean;
21
24
  readonly contentTypeMap?: Record<string, VideoCompressionOptions>;
22
25
  readonly tempDir?: string;
26
+ /**
27
+ * Called when compression fails. The original response is still served.
28
+ */
29
+ readonly onError?: MediaCompressionErrorHandler;
23
30
  }
31
+ /**
32
+ * Transcodes a video held in memory.
33
+ *
34
+ * `fluent-ffmpeg`'s export is a factory: `input()` exists only on the
35
+ * command it returns. Calling it on the module (as this did) threw
36
+ * `ffmpegInstance.input is not a function` on every call.
37
+ */
24
38
  export declare function compressVideo(inputBuffer: Buffer, options?: VideoCompressionOptions): Promise<Buffer>;
39
+ /**
40
+ * Compresses `video/*` responses, reading status, headers and body from the
41
+ * response returned by `next()`.
42
+ */
25
43
  export declare function createVideoCompressionMiddleware(options?: VideoCompressionMiddlewareOptions): HttpMiddleware;
26
44
  //# sourceMappingURL=httpMiddleware.video.d.ts.map
@@ -3,79 +3,58 @@
3
3
  *
4
4
  * @module httpMiddleware/builtin/video
5
5
  *
6
- * Requires: npm install fluent-ffmpeg
6
+ * Requires: npm install fluent-ffmpeg (and an ffmpeg binary)
7
+ */
8
+ import { Readable } from "node:stream";
9
+ import { applyCompressedBody, getResponseBytes, getResponseMediaType, loadOptionalModule, } from "../helpers/index.js";
10
+ import { CONTAINER_TYPES, DEFAULT_VIDEO_FORMAT as DEFAULT_FORMAT, buildOutputOptions, } from "./httpMiddleware.video.options.js";
11
+ /**
12
+ * Transcodes a video held in memory.
13
+ *
14
+ * `fluent-ffmpeg`'s export is a factory: `input()` exists only on the
15
+ * command it returns. Calling it on the module (as this did) threw
16
+ * `ffmpegInstance.input is not a function` on every call.
7
17
  */
8
- import { applyHeadersToResponse } from "../helpers/index.js";
9
- const DEFAULT_CRF = 28;
10
- const DEFAULT_PRESET = "medium";
11
- const DEFAULT_FORMAT = "mp4";
12
- async function getFfmpeg() {
13
- try {
14
- const mod = await import("fluent-ffmpeg");
15
- return mod.default ?? mod;
16
- }
17
- catch {
18
- throw new Error("fluent-ffmpeg is not installed. Run: npm install fluent-ffmpeg");
19
- }
20
- }
21
18
  export async function compressVideo(inputBuffer, options = {}) {
22
- const ffmpegInstance = await getFfmpeg();
23
- const crf = options.crf ?? DEFAULT_CRF;
24
- const preset = options.preset ?? DEFAULT_PRESET;
25
- const format = options.format ?? DEFAULT_FORMAT;
19
+ const ffmpeg = await loadOptionalModule("fluent-ffmpeg", "npm install fluent-ffmpeg");
26
20
  return new Promise((resolve, reject) => {
27
- const outputBuffer = [];
28
- ffmpegInstance
29
- .input(inputBuffer)
30
- .inputFormat("mp4")
31
- .outputOptions([
32
- `-crf ${crf}`,
33
- `-preset ${preset}`,
34
- "-movflags +faststart",
35
- ])
36
- .format(format)
37
- .on("end", () => {
38
- resolve(Buffer.concat(outputBuffer));
39
- })
40
- .on("error", (err) => {
41
- reject(err);
42
- })
43
- .pipe((err, stdout) => {
44
- if (err) {
45
- reject(err);
46
- return;
47
- }
48
- stdout.on("data", (chunk) => {
49
- outputBuffer.push(chunk);
50
- });
51
- });
21
+ const chunks = [];
22
+ const output = ffmpeg()
23
+ .input(Readable.from([inputBuffer]))
24
+ .inputFormat(options.inputFormat ?? "mp4")
25
+ .outputOptions(buildOutputOptions(options))
26
+ .format(options.format ?? DEFAULT_FORMAT)
27
+ .on("error", reject)
28
+ .pipe();
29
+ output.on("data", (chunk) => chunks.push(chunk));
30
+ output.on("end", () => resolve(Buffer.concat(chunks)));
31
+ output.on("error", reject);
52
32
  });
53
33
  }
34
+ /**
35
+ * Compresses `video/*` responses, reading status, headers and body from the
36
+ * response returned by `next()`.
37
+ */
54
38
  export function createVideoCompressionMiddleware(options = {}) {
55
39
  const enabled = options.enabled ?? true;
56
- return async (context, next) => {
57
- if (!enabled) {
58
- return next();
59
- }
40
+ return async (_context, next) => {
60
41
  const response = await next();
61
- const contentType = context.response.headers["content-type"];
62
- if (!contentType || !contentType.startsWith("video/")) {
42
+ const mediaType = getResponseMediaType(response);
43
+ if (!enabled || !mediaType?.startsWith("video/")) {
63
44
  return response;
64
45
  }
65
- const body = context.response.body;
66
- if (!Buffer.isBuffer(body)) {
46
+ const body = getResponseBytes(response);
47
+ if (!body || body.length === 0) {
67
48
  return response;
68
49
  }
69
- const compressionOptions = options.contentTypeMap?.[contentType] ?? {};
50
+ const compression = options.contentTypeMap?.[mediaType] ?? {};
51
+ const format = compression.format ?? DEFAULT_FORMAT;
70
52
  try {
71
- const compressed = await compressVideo(body, compressionOptions);
72
- const newContentType = "video/mp4";
73
- const headers = new Headers(response.headers);
74
- headers.set("content-type", newContentType);
75
- headers.set("cache-control", headers.get("cache-control") ?? "public, max-age=86400");
76
- return applyHeadersToResponse(response, headers).setBody(compressed);
53
+ const compressed = await compressVideo(body, compression);
54
+ return applyCompressedBody(response, compressed, CONTAINER_TYPES[format] ?? "video/mp4");
77
55
  }
78
- catch {
56
+ catch (error) {
57
+ options.onError?.(error, response);
79
58
  return response;
80
59
  }
81
60
  };
@@ -0,0 +1,12 @@
1
+ /**
2
+ * FFmpeg argument building for the video compression middleware.
3
+ *
4
+ * @module httpMiddleware/builtin/video/options
5
+ */
6
+ import type { VideoCompressionOptions } from "./httpMiddleware.video.js";
7
+ export declare const DEFAULT_VIDEO_FORMAT = "mp4";
8
+ /** Media type per output container. */
9
+ export declare const CONTAINER_TYPES: Readonly<Record<string, string>>;
10
+ /** Builds the ffmpeg output options for a compression request. */
11
+ export declare function buildOutputOptions(options: VideoCompressionOptions): string[];
12
+ //# sourceMappingURL=httpMiddleware.video.options.d.ts.map
@@ -0,0 +1,31 @@
1
+ /**
2
+ * FFmpeg argument building for the video compression middleware.
3
+ *
4
+ * @module httpMiddleware/builtin/video/options
5
+ */
6
+ const DEFAULT_CRF = 28;
7
+ const DEFAULT_PRESET = "medium";
8
+ export const DEFAULT_VIDEO_FORMAT = "mp4";
9
+ /** Media type per output container. */
10
+ export const CONTAINER_TYPES = Object.freeze({
11
+ mp4: "video/mp4",
12
+ webm: "video/webm",
13
+ mov: "video/quicktime",
14
+ });
15
+ /** Builds the ffmpeg output options for a compression request. */
16
+ export function buildOutputOptions(options) {
17
+ const output = [
18
+ `-crf ${options.crf ?? DEFAULT_CRF}`,
19
+ `-preset ${options.preset ?? DEFAULT_PRESET}`,
20
+ /* A pipe is not seekable, so the moov atom must be fragmented. */
21
+ "-movflags frag_keyframe+empty_moov",
22
+ ];
23
+ if (options.bitrate) {
24
+ output.push(`-b:v ${options.bitrate}`);
25
+ }
26
+ if (options.scale?.width || options.scale?.height) {
27
+ output.push(`-vf scale=${options.scale.width ?? -2}:${options.scale.height ?? -2}`);
28
+ }
29
+ return output;
30
+ }
31
+ //# sourceMappingURL=httpMiddleware.video.options.js.map
@@ -1,38 +1,12 @@
1
1
  /**
2
2
  * HTTP middleware error types.
3
3
  *
4
- * Extends the shared MiddlewareError from @zudojs/errors with
5
- * HTTP-specific middleware error information.
4
+ * `HttpMiddlewareError` and `HttpMiddlewarePipelineError` live in
5
+ * `@zudojs/errors` (both `BaseError`s, 500, not exposed, codes
6
+ * `HTTP_MIDDLEWARE_ERROR` / `MIDDLEWARE_PIPELINE_ERROR`); they are
7
+ * re-exported here so existing imports keep working.
6
8
  *
7
9
  * @module httpMiddleware/errors
8
10
  */
9
- import { BaseError } from "@zudojs/errors";
10
- /**
11
- * Error raised by an HTTP middleware function.
12
- */
13
- export declare class HttpMiddlewareError extends BaseError {
14
- /**
15
- * The unique identifier of the middleware.
16
- */
17
- readonly middlewareId: string | undefined;
18
- /**
19
- * The name of the middleware.
20
- */
21
- readonly middlewareName: string | undefined;
22
- constructor(message: string, options?: {
23
- readonly middlewareId?: string;
24
- readonly middlewareName?: string;
25
- readonly cause?: unknown;
26
- });
27
- }
28
- /**
29
- * Error raised when the HTTP middleware pipeline fails.
30
- */
31
- export declare class HttpMiddlewarePipelineError extends BaseError {
32
- /**
33
- * The errors that occurred during pipeline execution.
34
- */
35
- readonly errors: readonly HttpMiddlewareError[];
36
- constructor(errors: readonly HttpMiddlewareError[]);
37
- }
11
+ export { HttpMiddlewareError, HttpMiddlewarePipelineError, type HttpMiddlewareErrorOptions, } from "@zudojs/errors";
38
12
  //# sourceMappingURL=httpMiddleware.error.d.ts.map
@@ -1,60 +1,12 @@
1
1
  /**
2
2
  * HTTP middleware error types.
3
3
  *
4
- * Extends the shared MiddlewareError from @zudojs/errors with
5
- * HTTP-specific middleware error information.
4
+ * `HttpMiddlewareError` and `HttpMiddlewarePipelineError` live in
5
+ * `@zudojs/errors` (both `BaseError`s, 500, not exposed, codes
6
+ * `HTTP_MIDDLEWARE_ERROR` / `MIDDLEWARE_PIPELINE_ERROR`); they are
7
+ * re-exported here so existing imports keep working.
6
8
  *
7
9
  * @module httpMiddleware/errors
8
10
  */
9
- import { BaseError } from "@zudojs/errors";
10
- /* -------------------------------------------------------------------------- */
11
- /* Errors */
12
- /* -------------------------------------------------------------------------- */
13
- /**
14
- * Error raised by an HTTP middleware function.
15
- */
16
- export class HttpMiddlewareError extends BaseError {
17
- /**
18
- * The unique identifier of the middleware.
19
- */
20
- middlewareId;
21
- /**
22
- * The name of the middleware.
23
- */
24
- middlewareName;
25
- constructor(message, options = {}) {
26
- super(message, {
27
- code: "HTTP_MIDDLEWARE_ERROR",
28
- statusCode: 500,
29
- expose: false,
30
- cause: options.cause,
31
- });
32
- this.name = "HttpMiddlewareError";
33
- this.middlewareId = options.middlewareId;
34
- this.middlewareName = options.middlewareName;
35
- }
36
- }
37
- /**
38
- * Error raised when the HTTP middleware pipeline fails.
39
- */
40
- export class HttpMiddlewarePipelineError extends BaseError {
41
- /**
42
- * The errors that occurred during pipeline execution.
43
- */
44
- errors;
45
- constructor(errors) {
46
- const message = errors.length === 1
47
- ? `HTTP middleware pipeline failed: ${errors[0]?.message ?? "Unknown error"}`
48
- : `HTTP middleware pipeline failed with ${errors.length} errors: ${errors
49
- .map((e) => e.message)
50
- .join(", ")}`;
51
- super(message, {
52
- code: "MIDDLEWARE_PIPELINE_ERROR",
53
- statusCode: 500,
54
- expose: false,
55
- });
56
- this.name = "HttpMiddlewarePipelineError";
57
- this.errors = Object.freeze([...errors]);
58
- }
59
- }
11
+ export { HttpMiddlewareError, HttpMiddlewarePipelineError, } from "@zudojs/errors";
60
12
  //# sourceMappingURL=httpMiddleware.error.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
  /* -------------------------------------------------------------------------- */
@@ -128,6 +128,13 @@ export declare function assertSafeProxyTarget(target: string | URL, options?: Pr
128
128
  export declare function assertSafeProxyRedirect(location: string | URL, currentURL: string | URL, options?: ProxySecurityOptions): URL;
129
129
  export declare function resolveProxyTarget(target: string | URL, options?: ProxySecurityOptions): ProxyTarget;
130
130
  export declare function isValidProxyTarget(target: string | URL | undefined | null): boolean;
131
+ /**
132
+ * Joins a request path onto a proxy base path.
133
+ *
134
+ * @throws {HttpError} 400 when `requestPath` contains a `.` / `..` segment
135
+ * (plain or percent-encoded), which an upstream would resolve outside
136
+ * `basePath`.
137
+ */
131
138
  export declare function joinProxyPath(basePath: string, requestPath: string): string;
132
139
  export declare function rewriteProxyPath(path: string, options?: ProxyRewriteOptions): string;
133
140
  export declare function normalizeProxyPath(path: string): string;
@@ -160,6 +167,15 @@ export declare function applyProxyHeaders(headers: readonly HTTPHeader[], additi
160
167
  * with `httpTrustProxy.getClientIp` rather than copying the raw header.
161
168
  */
162
169
  export declare function setForwardedHeaders(headers: readonly HTTPHeader[], target: ProxyTarget, client?: ProxyClientContext): HTTPHeader[];
170
+ /**
171
+ * Builds an RFC 7239 `Forwarded` field value.
172
+ *
173
+ * Every parameter is escaped as a `quoted-string` when it is not a bare
174
+ * token, and the finished value is checked the same way `setForwardedHeaders`
175
+ * checks the values it writes.
176
+ *
177
+ * @throws {TypeError} If any parameter contains a control character.
178
+ */
163
179
  export declare function createForwardedHeader(address: ForwardedAddress): string;
164
180
  export declare function parseForwardedHeader(value: string | undefined | null): ForwardedAddress[];
165
181
  /**
@@ -10,7 +10,8 @@
10
10
  import { appendHeader, deleteHeader, getHeader, setHeader, } from "../httpProtocol/http.protocol.js";
11
11
  import { isValidHTTPURL, isValidHeaderValue } from "../httpValidation/index.js";
12
12
  import { isLinkLocalAddress, isLoopbackAddress, isUniqueLocalAddress, parseIpAddress, } from "../httpTrustProxy/httpTrustProxy.ip.js";
13
- import { assertSafeHeaderValue } from "../httpHeaders/security/index.js";
13
+ import { assertSafeHeaderValue, escapeHeaderQuotedString, } from "../httpHeaders/security/index.js";
14
+ import { assertProxyPathContained } from "./httpProxy.pathGuard.js";
14
15
  /* -------------------------------------------------------------------------- */
15
16
  /* Target */
16
17
  /* -------------------------------------------------------------------------- */
@@ -71,6 +72,16 @@ function isBlockedLiteralAddress(hostname) {
71
72
  (address.bytes[1] ?? 0) <= 127) {
72
73
  return true;
73
74
  }
75
+ /* 192.0.0.0/24 IETF protocol assignments (192.0.0.8, 192.0.0.170, …). */
76
+ if (first === 192 &&
77
+ (address.bytes[1] ?? 0) === 0 &&
78
+ (address.bytes[2] ?? 0) === 0) {
79
+ return true;
80
+ }
81
+ /* 198.18.0.0/15 benchmarking. */
82
+ if (first === 198 && ((address.bytes[1] ?? 0) & 0xfe) === 18) {
83
+ return true;
84
+ }
74
85
  return false;
75
86
  }
76
87
  const first = address.bytes[0] ?? 0;
@@ -204,7 +215,15 @@ function parseProxyURL(target) {
204
215
  /* -------------------------------------------------------------------------- */
205
216
  /* Path Handling */
206
217
  /* -------------------------------------------------------------------------- */
218
+ /**
219
+ * Joins a request path onto a proxy base path.
220
+ *
221
+ * @throws {HttpError} 400 when `requestPath` contains a `.` / `..` segment
222
+ * (plain or percent-encoded), which an upstream would resolve outside
223
+ * `basePath`.
224
+ */
207
225
  export function joinProxyPath(basePath, requestPath) {
226
+ assertProxyPathContained(requestPath);
208
227
  const base = normalizeProxyPath(basePath);
209
228
  const request = requestPath.startsWith("/") ? requestPath : `/${requestPath}`;
210
229
  if (base === "/") {
@@ -352,6 +371,15 @@ function appendForwardedValue(headers, name, value) {
352
371
  /* -------------------------------------------------------------------------- */
353
372
  /* Standard Forwarded Header */
354
373
  /* -------------------------------------------------------------------------- */
374
+ /**
375
+ * Builds an RFC 7239 `Forwarded` field value.
376
+ *
377
+ * Every parameter is escaped as a `quoted-string` when it is not a bare
378
+ * token, and the finished value is checked the same way `setForwardedHeaders`
379
+ * checks the values it writes.
380
+ *
381
+ * @throws {TypeError} If any parameter contains a control character.
382
+ */
355
383
  export function createForwardedHeader(address) {
356
384
  const parts = [];
357
385
  if (address.for) {
@@ -366,7 +394,9 @@ export function createForwardedHeader(address) {
366
394
  if (address.protocol) {
367
395
  parts.push(`proto=${formatForwardedValue(address.protocol)}`);
368
396
  }
369
- return parts.join("; ");
397
+ const value = parts.join("; ");
398
+ assertSafeHeaderValue(value);
399
+ return value;
370
400
  }
371
401
  export function parseForwardedHeader(value) {
372
402
  if (!value || value.trim().length === 0) {
@@ -525,11 +555,22 @@ function formatForwardedIdentifier(value) {
525
555
  }
526
556
  return formatForwardedValue(value);
527
557
  }
558
+ /**
559
+ * Emits an RFC 7239 parameter value.
560
+ *
561
+ * Wrapping a value in quotes does not neutralise a CR or LF — the control
562
+ * character survives into the field value and an attacker-chosen header
563
+ * follows it on the wire. `escapeHeaderQuotedString` rejects those characters
564
+ * rather than escaping them, which is what every other quoted-parameter
565
+ * emitter in this package already uses.
566
+ *
567
+ * @throws {TypeError} If the value contains a forbidden control character.
568
+ */
528
569
  function formatForwardedValue(value) {
529
570
  if (/^[A-Za-z0-9._:-]+$/.test(value)) {
530
571
  return value;
531
572
  }
532
- return `"${value.replace(/\\/g, "\\\\").replace(/"/g, '\\"')}"`;
573
+ return `"${escapeHeaderQuotedString(value)}"`;
533
574
  }
534
575
  function unquoteForwardedValue(value) {
535
576
  if (value.length >= 2 && value.startsWith('"') && value.endsWith('"')) {