@zudojs/http 1.1.0 → 1.2.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 (79) 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.js +4 -34
  5. package/dist/httpAdapter/httpAdapter.errorResponse.d.ts +2 -2
  6. package/dist/httpAdapter/httpAdapter.errorResponse.js +26 -9
  7. package/dist/httpAdapter/httpAdapter.logger.d.ts +19 -0
  8. package/dist/httpAdapter/httpAdapter.logger.js +21 -0
  9. package/dist/httpAdapter/node/httpNode.adapter.d.ts +12 -1
  10. package/dist/httpAdapter/node/httpNode.adapter.js +28 -14
  11. package/dist/httpAdapter/node/httpNode.request.d.ts +8 -7
  12. package/dist/httpAdapter/node/httpNode.request.js +17 -27
  13. package/dist/httpAdapter/node/httpNode.server.d.ts +6 -0
  14. package/dist/httpAdapter/node/httpNode.server.js +6 -0
  15. package/dist/httpAdapter/node/httpNode.type.d.ts +2 -1
  16. package/dist/httpCookies/http.cookies.d.ts +29 -2
  17. package/dist/httpCookies/http.cookies.js +44 -24
  18. package/dist/httpCookies/httpCookies.defaults.d.ts +40 -0
  19. package/dist/httpCookies/httpCookies.defaults.js +42 -0
  20. package/dist/httpCookies/index.d.ts +1 -0
  21. package/dist/httpCookies/index.js +1 -0
  22. package/dist/httpMiddleware/builtin/conditional/httpMiddleware.conditional.d.ts +22 -1
  23. package/dist/httpMiddleware/builtin/conditional/httpMiddleware.conditional.js +19 -4
  24. package/dist/httpMiddleware/builtin/conditional/index.d.ts +1 -0
  25. package/dist/httpMiddleware/builtin/cors/httpMiddleware.cors.d.ts +8 -0
  26. package/dist/httpMiddleware/builtin/cors/httpMiddleware.cors.js +25 -22
  27. package/dist/httpMiddleware/builtin/helpers/httpMiddleware.accessor.js +2 -6
  28. package/dist/httpMiddleware/builtin/helpers/httpMiddleware.media.d.ts +33 -0
  29. package/dist/httpMiddleware/builtin/helpers/httpMiddleware.media.js +59 -0
  30. package/dist/httpMiddleware/builtin/helpers/index.d.ts +1 -0
  31. package/dist/httpMiddleware/builtin/helpers/index.js +1 -0
  32. package/dist/httpMiddleware/builtin/image/httpMiddleware.image.d.ts +13 -0
  33. package/dist/httpMiddleware/builtin/image/httpMiddleware.image.js +31 -53
  34. package/dist/httpMiddleware/builtin/index.d.ts +1 -0
  35. package/dist/httpMiddleware/builtin/index.js +1 -0
  36. package/dist/httpMiddleware/builtin/rateLimit/httpMiddleware.rateLimit.d.ts +47 -0
  37. package/dist/httpMiddleware/builtin/rateLimit/httpMiddleware.rateLimit.js +61 -0
  38. package/dist/httpMiddleware/builtin/rateLimit/index.d.ts +7 -0
  39. package/dist/httpMiddleware/builtin/rateLimit/index.js +7 -0
  40. package/dist/httpMiddleware/builtin/static/httpMiddleware.static.js +18 -4
  41. package/dist/httpMiddleware/builtin/video/httpMiddleware.video.d.ts +19 -1
  42. package/dist/httpMiddleware/builtin/video/httpMiddleware.video.js +38 -59
  43. package/dist/httpMiddleware/builtin/video/httpMiddleware.video.options.d.ts +12 -0
  44. package/dist/httpMiddleware/builtin/video/httpMiddleware.video.options.js +31 -0
  45. package/dist/httpMiddleware/httpMiddleware.error.d.ts +5 -31
  46. package/dist/httpMiddleware/httpMiddleware.error.js +5 -53
  47. package/dist/httpProxy/http.proxy.d.ts +7 -0
  48. package/dist/httpProxy/http.proxy.js +9 -0
  49. package/dist/httpProxy/httpProxy.pathGuard.d.ts +23 -0
  50. package/dist/httpProxy/httpProxy.pathGuard.js +43 -0
  51. package/dist/httpProxy/index.d.ts +1 -0
  52. package/dist/httpProxy/index.js +1 -0
  53. package/dist/httpRequest/httpRequest.context.d.ts +7 -0
  54. package/dist/httpRequest/httpRequest.context.js +15 -16
  55. package/dist/httpRequest/index.d.ts +1 -0
  56. package/dist/httpRequest/index.js +1 -0
  57. package/dist/httpRequest/target/httpRequest.target.d.ts +45 -0
  58. package/dist/httpRequest/target/httpRequest.target.js +93 -0
  59. package/dist/httpRequest/target/index.d.ts +9 -0
  60. package/dist/httpRequest/target/index.js +9 -0
  61. package/dist/httpResponse/httpResponse.helper.js +2 -1
  62. package/dist/httpRouter/core/util/httpRoute.util.d.ts +14 -0
  63. package/dist/httpRouter/core/util/httpRoute.util.js +18 -14
  64. package/dist/httpSecurity/httpSecurity.config.d.ts +6 -0
  65. package/dist/httpSecurity/httpSecurity.config.js +1 -0
  66. package/dist/httpSecurity/httpSecurity.guard.d.ts +7 -9
  67. package/dist/httpSecurity/httpSecurity.guard.js +7 -14
  68. package/dist/httpSecurity/httpSecurity.nodeGuard.d.ts +48 -0
  69. package/dist/httpSecurity/httpSecurity.nodeGuard.js +32 -0
  70. package/dist/httpSecurity/httpSecurity.validator.js +4 -2
  71. package/dist/httpSecurity/index.d.ts +2 -0
  72. package/dist/httpSecurity/index.js +1 -0
  73. package/dist/httpServer/core/httpServer.core.d.ts +11 -0
  74. package/dist/httpServer/core/httpServer.core.js +30 -4
  75. package/dist/httpTrustProxy/httpTrustProxy.compilation.d.ts +5 -2
  76. package/dist/httpTrustProxy/httpTrustProxy.compilation.js +19 -2
  77. package/dist/httpTrustProxy/httpTrustProxy.type.d.ts +3 -1
  78. package/dist/index.js +0 -1
  79. package/package.json +5 -5
@@ -6,7 +6,8 @@
6
6
  import type { Server } from "node:http";
7
7
  import type { HttpAdapterOptions } from "../http.adapter.js";
8
8
  import type { TrustProxy } from "../../httpTrustProxy/httpTrustProxy.core.js";
9
- export interface NodeAdapterOptions extends HttpAdapterOptions {
9
+ import type { NodeAdapterSecurityOptions } from "../../httpSecurity/httpSecurity.nodeGuard.js";
10
+ export interface NodeAdapterOptions extends HttpAdapterOptions, NodeAdapterSecurityOptions {
10
11
  readonly host?: string;
11
12
  readonly port?: number;
12
13
  readonly server?: Server;
@@ -57,7 +57,14 @@ export declare const MAX_COOKIE_HEADER_LENGTH: number;
57
57
  * @returns The parsed cookie jar.
58
58
  */
59
59
  export declare function parseCookies(header: string | undefined): CookieCollection;
60
- export declare function serializeCookie(name: string, value: CookieValue, options?: CookieOptions): string;
60
+ /**
61
+ * Serializes a `Set-Cookie` header value.
62
+ *
63
+ * Unset attributes take the secure defaults (`Path=/; HttpOnly; Secure;
64
+ * SameSite=Lax`, see `DEFAULT_COOKIE_ATTRIBUTES`); pass `httpOnly: false`,
65
+ * `secure: false`, another `sameSite` or `path` to override them.
66
+ */
67
+ export declare function serializeCookie(name: string, value: CookieValue, cookieOptions?: CookieOptions): string;
61
68
  export interface CookieManager {
62
69
  get(name: string): string | undefined;
63
70
  has(name: string): boolean;
@@ -78,11 +85,31 @@ export interface SignedCookie {
78
85
  readonly value: string;
79
86
  readonly signature: string;
80
87
  }
88
+ /**
89
+ * Serializes a signed cookie. The MAC covers the cookie **name** as well as
90
+ * the value, so a signature minted for one cookie is not accepted for
91
+ * another; read it back with `parseSignedCookie(value, secret, name)`.
92
+ */
81
93
  export declare function serializeSignedCookie(name: string, value: string, options: SignedCookieOptions): string;
82
- export declare function parseSignedCookie(value: string | undefined, secret: string): string | undefined;
94
+ /**
95
+ * Verifies a signed cookie value and returns the original value, or
96
+ * `undefined` when the signature does not match.
97
+ *
98
+ * Pass the cookie's `name`: the signature produced by
99
+ * `serializeSignedCookie` is bound to it, so a value lifted from another
100
+ * signed cookie (for example a user-chosen display name replayed as
101
+ * `session_user`) is rejected. Without `name` only a legacy, value-only
102
+ * signature (from `signCookieValue(value, secret)`) verifies, and
103
+ * name-bound signatures never do.
104
+ */
105
+ export declare function parseSignedCookie(value: string | undefined, secret: string, name?: string): string | undefined;
83
106
  /**
84
107
  * Signs a cookie value with HMAC-SHA256.
85
108
  *
109
+ * Synchronous by contract, so the MAC is computed with `node:crypto`: every
110
+ * `@zudojs/crypto` HMAC helper is asynchronous. Verification compares with
111
+ * `@zudojs/crypto`'s constant-time `timingSafeEqualString`.
112
+ *
86
113
  * @param value - The value to authenticate.
87
114
  * @param secret - The signing key.
88
115
  * @returns The base64url signature.
@@ -1,4 +1,6 @@
1
- import { createHmac, timingSafeEqual as cryptoTimingSafeEqual, } from "node:crypto";
1
+ import { createHmac } from "node:crypto";
2
+ import { timingSafeEqualString } from "@zudojs/crypto";
3
+ import { withSecureCookieDefaults } from "./httpCookies.defaults.js";
2
4
  /* -------------------------------------------------------------------------- */
3
5
  /* Cookie Collection */
4
6
  /* -------------------------------------------------------------------------- */
@@ -123,7 +125,15 @@ export function parseCookies(header) {
123
125
  /* -------------------------------------------------------------------------- */
124
126
  /* Serialize Cookie */
125
127
  /* -------------------------------------------------------------------------- */
126
- export function serializeCookie(name, value, options = {}) {
128
+ /**
129
+ * Serializes a `Set-Cookie` header value.
130
+ *
131
+ * Unset attributes take the secure defaults (`Path=/; HttpOnly; Secure;
132
+ * SameSite=Lax`, see `DEFAULT_COOKIE_ATTRIBUTES`); pass `httpOnly: false`,
133
+ * `secure: false`, another `sameSite` or `path` to override them.
134
+ */
135
+ export function serializeCookie(name, value, cookieOptions = {}) {
136
+ const options = withSecureCookieDefaults(cookieOptions, "lax");
127
137
  validateCookieName(name);
128
138
  validateCookiePrefix(name, options);
129
139
  const encodedName = name;
@@ -229,11 +239,27 @@ export function createCookieManager(request, response) {
229
239
  },
230
240
  };
231
241
  }
242
+ /**
243
+ * Serializes a signed cookie. The MAC covers the cookie **name** as well as
244
+ * the value, so a signature minted for one cookie is not accepted for
245
+ * another; read it back with `parseSignedCookie(value, secret, name)`.
246
+ */
232
247
  export function serializeSignedCookie(name, value, options) {
233
- const signature = signCookieValue(value, options.secret);
248
+ const signature = signCookieValue(bindCookieName(name, value), options.secret);
234
249
  return serializeCookie(name, `${value}.${signature}`, options);
235
250
  }
236
- export function parseSignedCookie(value, secret) {
251
+ /**
252
+ * Verifies a signed cookie value and returns the original value, or
253
+ * `undefined` when the signature does not match.
254
+ *
255
+ * Pass the cookie's `name`: the signature produced by
256
+ * `serializeSignedCookie` is bound to it, so a value lifted from another
257
+ * signed cookie (for example a user-chosen display name replayed as
258
+ * `session_user`) is rejected. Without `name` only a legacy, value-only
259
+ * signature (from `signCookieValue(value, secret)`) verifies, and
260
+ * name-bound signatures never do.
261
+ */
262
+ export function parseSignedCookie(value, secret, name) {
237
263
  if (!value) {
238
264
  return undefined;
239
265
  }
@@ -243,15 +269,27 @@ export function parseSignedCookie(value, secret) {
243
269
  }
244
270
  const originalValue = value.slice(0, separator);
245
271
  const signature = value.slice(separator + 1);
246
- const expected = signCookieValue(originalValue, secret);
247
- if (!timingSafeEqual(signature, expected)) {
272
+ const expected = signCookieValue(name === undefined ? originalValue : bindCookieName(name, originalValue), secret);
273
+ if (!timingSafeEqualString(signature, expected)) {
248
274
  return undefined;
249
275
  }
250
276
  return originalValue;
251
277
  }
278
+ /**
279
+ * The MAC input for a name-bound signature. A cookie name is an RFC 6265
280
+ * token and cannot contain `=`, so the encoding is unambiguous.
281
+ */
282
+ function bindCookieName(name, value) {
283
+ validateCookieName(name);
284
+ return `${name}=${value}`;
285
+ }
252
286
  /**
253
287
  * Signs a cookie value with HMAC-SHA256.
254
288
  *
289
+ * Synchronous by contract, so the MAC is computed with `node:crypto`: every
290
+ * `@zudojs/crypto` HMAC helper is asynchronous. Verification compares with
291
+ * `@zudojs/crypto`'s constant-time `timingSafeEqualString`.
292
+ *
255
293
  * @param value - The value to authenticate.
256
294
  * @param secret - The signing key.
257
295
  * @returns The base64url signature.
@@ -396,22 +434,4 @@ function normalizePriority(value) {
396
434
  throw new TypeError(`Invalid cookie priority: ${String(value)}`);
397
435
  }
398
436
  }
399
- /* -------------------------------------------------------------------------- */
400
- /* Hash Helpers */
401
- /* -------------------------------------------------------------------------- */
402
- /**
403
- * Compares two signatures in constant time.
404
- *
405
- * @param left - The candidate signature.
406
- * @param right - The expected signature.
407
- * @returns `true` if the two are byte-identical.
408
- */
409
- function timingSafeEqual(left, right) {
410
- const leftBuffer = Buffer.from(left, "utf8");
411
- const rightBuffer = Buffer.from(right, "utf8");
412
- if (leftBuffer.length !== rightBuffer.length) {
413
- return false;
414
- }
415
- return cryptoTimingSafeEqual(leftBuffer, rightBuffer);
416
- }
417
437
  //# sourceMappingURL=http.cookies.js.map
@@ -0,0 +1,40 @@
1
+ /**
2
+ * Secure cookie defaults.
3
+ *
4
+ * Both cookie serializers (`serializeCookie` in this module and the
5
+ * `serializeResponseCookie` the adapters use for `response.cookie()`) start
6
+ * from these defaults, so a cookie set "the obvious way" is `HttpOnly`,
7
+ * `Secure`, `SameSite=Lax` and `Path=/`, matching the defaults of
8
+ * `@zudojs/security`'s `serializeCookie` and the AGENTS.md secure-defaults
9
+ * rule. Every default can be overridden explicitly (`httpOnly: false`,
10
+ * `secure: false`, `sameSite: "none"`, `path: "/app"`); an option left
11
+ * `undefined` keeps the default.
12
+ *
13
+ * @module httpCookies/defaults
14
+ */
15
+ /**
16
+ * The attributes every cookie gets unless the caller overrides them.
17
+ */
18
+ export declare const DEFAULT_COOKIE_ATTRIBUTES: Readonly<{
19
+ readonly path: "/";
20
+ readonly httpOnly: true;
21
+ readonly secure: true;
22
+ readonly sameSite: "lax";
23
+ }>;
24
+ interface DefaultableCookieOptions<S extends string> {
25
+ readonly path?: string;
26
+ readonly httpOnly?: boolean;
27
+ readonly secure?: boolean;
28
+ readonly sameSite?: S;
29
+ }
30
+ /**
31
+ * Returns `options` with the secure defaults filled in for every attribute
32
+ * the caller left `undefined`.
33
+ *
34
+ * @param options - The caller's cookie options.
35
+ * @param sameSite - The default `SameSite` value in the option type's own
36
+ * spelling (`"lax"` or `"Lax"`).
37
+ */
38
+ export declare function withSecureCookieDefaults<S extends string, T extends DefaultableCookieOptions<S>>(options: T | undefined, sameSite: S): T;
39
+ export {};
40
+ //# sourceMappingURL=httpCookies.defaults.d.ts.map
@@ -0,0 +1,42 @@
1
+ /**
2
+ * Secure cookie defaults.
3
+ *
4
+ * Both cookie serializers (`serializeCookie` in this module and the
5
+ * `serializeResponseCookie` the adapters use for `response.cookie()`) start
6
+ * from these defaults, so a cookie set "the obvious way" is `HttpOnly`,
7
+ * `Secure`, `SameSite=Lax` and `Path=/`, matching the defaults of
8
+ * `@zudojs/security`'s `serializeCookie` and the AGENTS.md secure-defaults
9
+ * rule. Every default can be overridden explicitly (`httpOnly: false`,
10
+ * `secure: false`, `sameSite: "none"`, `path: "/app"`); an option left
11
+ * `undefined` keeps the default.
12
+ *
13
+ * @module httpCookies/defaults
14
+ */
15
+ /**
16
+ * The attributes every cookie gets unless the caller overrides them.
17
+ */
18
+ export const DEFAULT_COOKIE_ATTRIBUTES = Object.freeze({
19
+ path: "/",
20
+ httpOnly: true,
21
+ secure: true,
22
+ sameSite: "lax",
23
+ });
24
+ /**
25
+ * Returns `options` with the secure defaults filled in for every attribute
26
+ * the caller left `undefined`.
27
+ *
28
+ * @param options - The caller's cookie options.
29
+ * @param sameSite - The default `SameSite` value in the option type's own
30
+ * spelling (`"lax"` or `"Lax"`).
31
+ */
32
+ export function withSecureCookieDefaults(options, sameSite) {
33
+ const source = (options ?? {});
34
+ return {
35
+ ...source,
36
+ path: source.path ?? DEFAULT_COOKIE_ATTRIBUTES.path,
37
+ httpOnly: source.httpOnly ?? DEFAULT_COOKIE_ATTRIBUTES.httpOnly,
38
+ secure: source.secure ?? DEFAULT_COOKIE_ATTRIBUTES.secure,
39
+ sameSite: source.sameSite ?? sameSite,
40
+ };
41
+ }
42
+ //# sourceMappingURL=httpCookies.defaults.js.map
@@ -4,4 +4,5 @@
4
4
  * HTTP httpCookies module.
5
5
  */
6
6
  export * from "./http.cookies.js";
7
+ export * from "./httpCookies.defaults.js";
7
8
  //# sourceMappingURL=index.d.ts.map
@@ -4,4 +4,5 @@
4
4
  * HTTP httpCookies module.
5
5
  */
6
6
  export * from "./http.cookies.js";
7
+ export * from "./httpCookies.defaults.js";
7
8
  //# sourceMappingURL=index.js.map
@@ -7,7 +7,28 @@ import type { HttpMiddleware, HttpMiddlewareContext } from "../../httpMiddleware
7
7
  import type { HttpResponseContext as ResponseContext } from "../../../httpResponse/httpResponse.context.js";
8
8
  export declare function createAsyncMiddleware(factory: (context: HttpMiddlewareContext) => Promise<HttpMiddleware>): HttpMiddleware;
9
9
  export declare function createConditionalMiddleware(predicate: (context: HttpMiddlewareContext) => boolean | Promise<boolean>, middleware: HttpMiddleware): HttpMiddleware;
10
- export declare function createPathMiddleware(path: string, middleware: HttpMiddleware): HttpMiddleware;
10
+ /**
11
+ * Options for {@link createPathMiddleware}.
12
+ */
13
+ export interface PathMiddlewareOptions {
14
+ /**
15
+ * Match the path case-sensitively. Defaults to `false`, the router's
16
+ * default, so a guard scoped to `/admin` also covers `/Admin`, which the
17
+ * router would dispatch to the same route.
18
+ */
19
+ readonly caseSensitive?: boolean;
20
+ }
21
+ /**
22
+ * Runs `middleware` only for requests addressed to `path`.
23
+ *
24
+ * The request path is normalised exactly as the router normalises it before
25
+ * matching: it is read with the canonical request-target parser, repeated
26
+ * slashes are collapsed, a trailing slash is ignored and (by default) case is
27
+ * ignored. An exact, case-sensitive comparison let `/Admin`, `/admin/` and
28
+ * `/admin//` skip a guard on `/admin` while the router still served the
29
+ * protected route.
30
+ */
31
+ export declare function createPathMiddleware(path: string, middleware: HttpMiddleware, options?: PathMiddlewareOptions): HttpMiddleware;
11
32
  export declare function createMethodMiddleware(method: string, middleware: HttpMiddleware): HttpMiddleware;
12
33
  export declare function createResponseMiddleware(response: Response | ResponseContext): HttpMiddleware;
13
34
  export declare function createShortCircuitMiddleware(predicate: (context: HttpMiddlewareContext) => boolean | Promise<boolean>, response: Response | ResponseContext): HttpMiddleware;
@@ -4,6 +4,7 @@
4
4
  * @module httpMiddleware/builtin/conditional
5
5
  */
6
6
  import { getRequestMethod, getRequestUrl, extractPathname, } from "../helpers/index.js";
7
+ import { normalizePath } from "../../../httpRouter/core/util/httpRoute.util.js";
7
8
  import { isWebResponse, bufferWebResponse, } from "../../../httpResponse/httpResponse.fromWeb.js";
8
9
  export function createAsyncMiddleware(factory) {
9
10
  return async (context, next) => {
@@ -19,12 +20,26 @@ export function createConditionalMiddleware(predicate, middleware) {
19
20
  return next();
20
21
  };
21
22
  }
22
- export function createPathMiddleware(path, middleware) {
23
- const targetPath = extractPathname(path);
23
+ /**
24
+ * Runs `middleware` only for requests addressed to `path`.
25
+ *
26
+ * The request path is normalised exactly as the router normalises it before
27
+ * matching: it is read with the canonical request-target parser, repeated
28
+ * slashes are collapsed, a trailing slash is ignored and (by default) case is
29
+ * ignored. An exact, case-sensitive comparison let `/Admin`, `/admin/` and
30
+ * `/admin//` skip a guard on `/admin` while the router still served the
31
+ * protected route.
32
+ */
33
+ export function createPathMiddleware(path, middleware, options = {}) {
34
+ const caseSensitive = options.caseSensitive === true;
35
+ const canonical = (value) => {
36
+ const normalized = normalizePath(value);
37
+ return caseSensitive ? normalized : normalized.toLowerCase();
38
+ };
39
+ const targetPath = canonical(extractPathname(path));
24
40
  return createConditionalMiddleware((context) => {
25
41
  const url = getRequestUrl(context.request);
26
- const pathname = extractPathname(url);
27
- return pathname === targetPath;
42
+ return canonical(extractPathname(url)) === targetPath;
28
43
  }, middleware);
29
44
  }
30
45
  export function createMethodMiddleware(method, middleware) {
@@ -4,4 +4,5 @@
4
4
  * @module httpMiddleware/builtin/conditional
5
5
  */
6
6
  export { createAsyncMiddleware, createConditionalMiddleware, createPathMiddleware, createMethodMiddleware, createResponseMiddleware, createShortCircuitMiddleware, } from "./httpMiddleware.conditional.js";
7
+ export type { PathMiddlewareOptions } from "./httpMiddleware.conditional.js";
7
8
  //# sourceMappingURL=index.d.ts.map
@@ -24,5 +24,13 @@ export interface CorsMiddlewareOptions {
24
24
  */
25
25
  readonly optionsSuccessStatus?: number;
26
26
  }
27
+ /**
28
+ * Creates the CORS middleware.
29
+ *
30
+ * @throws {ConfigurationError} At construction when a wildcard origin
31
+ * (including the default `"*"`) is combined with `credentials: true`.
32
+ * This used to be accepted and then fail every cross-origin request with a
33
+ * 500, which tests that send no `Origin` never noticed.
34
+ */
27
35
  export declare function createCorsMiddleware(options?: CorsMiddlewareOptions): HttpMiddleware;
28
36
  //# sourceMappingURL=httpMiddleware.cors.d.ts.map
@@ -3,6 +3,7 @@
3
3
  *
4
4
  * @module httpMiddleware/builtin/cors
5
5
  */
6
+ import { isOriginAllowed } from "@zudojs/security";
6
7
  import { applyHeadersToResponse } from "../helpers/index.js";
7
8
  function getRequestHeader(context, name) {
8
9
  const headers = context.request.headers;
@@ -13,30 +14,23 @@ function getRequestMethod(context) {
13
14
  return (context.request.method ?? "GET").toUpperCase();
14
15
  }
15
16
  /**
16
- * Decides whether an origin is allowed.
17
- *
18
- * The previous implementation ignored the request origin entirely and wrote
19
- * `Access-Control-Allow-Origin: *` on every response, so the documented
20
- * "reflect the origin" pattern allowed every origin on the internet.
17
+ * The `@zudojs/security` CORS configuration for this middleware's origin
18
+ * policy. `allowOrigin` defaults to `"*"`; a predicate is bound to the
19
+ * request context because `@zudojs/security` predicates take the origin only.
21
20
  */
22
- function resolveAllowedOrigin(origin, context, options) {
21
+ function toSecurityCorsConfig(options, context) {
23
22
  const configured = options.allowOrigin ?? "*";
24
- if (typeof configured === "function") {
25
- return configured(origin, context) ? origin : undefined;
26
- }
27
- const list = typeof configured === "string" ? [configured] : configured;
28
- if (list.includes("*")) {
29
- if (options.credentials) {
30
- /*
31
- * A wildcard origin with credentials is forbidden by the spec, and
32
- * reflecting the request origin instead would authorise every origin
33
- * to read authenticated responses.
34
- */
35
- throw new TypeError("CORS: a wildcard allowOrigin cannot be combined with credentials.");
36
- }
37
- return "*";
38
- }
39
- return list.includes(origin) ? origin : undefined;
23
+ const origin = typeof configured === "function"
24
+ ? (value) => context !== undefined && configured(value, context)
25
+ : configured;
26
+ return { origin, credentials: options.credentials === true };
27
+ }
28
+ /**
29
+ * Decides whether an origin is allowed, delegating to `@zudojs/security`'s
30
+ * `isOriginAllowed` so origin matching has one implementation.
31
+ */
32
+ function resolveAllowedOrigin(origin, context, options) {
33
+ return isOriginAllowed(origin, toSecurityCorsConfig(options, context));
40
34
  }
41
35
  function appendVary(headers, value) {
42
36
  const existing = headers.get("vary");
@@ -51,7 +45,16 @@ function appendVary(headers, value) {
51
45
  headers.set("vary", `${existing}, ${value}`);
52
46
  }
53
47
  }
48
+ /**
49
+ * Creates the CORS middleware.
50
+ *
51
+ * @throws {ConfigurationError} At construction when a wildcard origin
52
+ * (including the default `"*"`) is combined with `credentials: true`.
53
+ * This used to be accepted and then fail every cross-origin request with a
54
+ * 500, which tests that send no `Origin` never noticed.
55
+ */
54
56
  export function createCorsMiddleware(options = {}) {
57
+ isOriginAllowed(undefined, toSecurityCorsConfig(options));
55
58
  return async (context, next) => {
56
59
  const origin = getRequestHeader(context, "origin");
57
60
  /*
@@ -3,6 +3,7 @@
3
3
  *
4
4
  * @module httpMiddleware/builtin/helpers/accessors
5
5
  */
6
+ import { getCanonicalPath } from "../../../httpRequest/target/httpRequest.target.js";
6
7
  export function getRequestMethod(request) {
7
8
  const value = request.method;
8
9
  return value ?? "GET";
@@ -32,12 +33,7 @@ export function getContextSignal(request) {
32
33
  return value;
33
34
  }
34
35
  export function extractPathname(value) {
35
- try {
36
- return new URL(value, "http://zudojs.local").pathname;
37
- }
38
- catch {
39
- return value.split("?")[0] ?? value;
40
- }
36
+ return getCanonicalPath(value);
41
37
  }
42
38
  export function performanceNow() {
43
39
  if (typeof performance !== "undefined" &&
@@ -0,0 +1,33 @@
1
+ /**
2
+ * Shared helpers for the media (image / video) compression middleware.
3
+ *
4
+ * Both middleware used to read the stale `context.response` instead of the
5
+ * response `next()` returned, so a handler that returned a fresh response
6
+ * context (the documented style) was never compressed. These helpers read
7
+ * everything from the returned response.
8
+ *
9
+ * @module httpMiddleware/builtin/helpers/media
10
+ */
11
+ import type { HttpResponseContext as ResponseContext } from "../../../httpResponse/httpResponse.context.js";
12
+ /**
13
+ * Reports a compression failure. The uncompressed response is still served.
14
+ */
15
+ export type MediaCompressionErrorHandler = (error: unknown, response: ResponseContext) => void;
16
+ /**
17
+ * The media type of a response (`image/png; q=1` → `image/png`), lowercased.
18
+ */
19
+ export declare function getResponseMediaType(response: ResponseContext): string | undefined;
20
+ /**
21
+ * The response body as a Buffer, or `undefined` for a body that is not held
22
+ * in memory (streams, JSON values).
23
+ */
24
+ export declare function getResponseBytes(response: ResponseContext): Buffer | undefined;
25
+ /**
26
+ * Loads an optional peer module's callable default export.
27
+ */
28
+ export declare function loadOptionalModule<T>(specifier: string, install: string): Promise<T>;
29
+ /**
30
+ * Replaces the body and content type of a compressed response.
31
+ */
32
+ export declare function applyCompressedBody(response: ResponseContext, body: Buffer, contentType: string): ResponseContext;
33
+ //# sourceMappingURL=httpMiddleware.media.d.ts.map
@@ -0,0 +1,59 @@
1
+ /**
2
+ * Shared helpers for the media (image / video) compression middleware.
3
+ *
4
+ * Both middleware used to read the stale `context.response` instead of the
5
+ * response `next()` returned, so a handler that returned a fresh response
6
+ * context (the documented style) was never compressed. These helpers read
7
+ * everything from the returned response.
8
+ *
9
+ * @module httpMiddleware/builtin/helpers/media
10
+ */
11
+ /**
12
+ * The media type of a response (`image/png; q=1` → `image/png`), lowercased.
13
+ */
14
+ export function getResponseMediaType(response) {
15
+ const raw = response.headers["content-type"];
16
+ if (typeof raw !== "string") {
17
+ return undefined;
18
+ }
19
+ const mediaType = raw.split(";", 1)[0]?.trim().toLowerCase();
20
+ return mediaType ? mediaType : undefined;
21
+ }
22
+ /**
23
+ * The response body as a Buffer, or `undefined` for a body that is not held
24
+ * in memory (streams, JSON values).
25
+ */
26
+ export function getResponseBytes(response) {
27
+ const body = response.body;
28
+ if (Buffer.isBuffer(body)) {
29
+ return body;
30
+ }
31
+ if (body instanceof Uint8Array) {
32
+ return Buffer.from(body.buffer, body.byteOffset, body.byteLength);
33
+ }
34
+ return undefined;
35
+ }
36
+ /**
37
+ * Loads an optional peer module's callable default export.
38
+ */
39
+ export async function loadOptionalModule(specifier, install) {
40
+ try {
41
+ const mod = (await import(specifier));
42
+ return (mod.default ?? mod);
43
+ }
44
+ catch {
45
+ throw new Error(`${specifier} is not installed. Run: ${install}`);
46
+ }
47
+ }
48
+ /**
49
+ * Replaces the body and content type of a compressed response.
50
+ */
51
+ export function applyCompressedBody(response, body, contentType) {
52
+ response.setHeader("content-type", contentType);
53
+ if (response.headers["cache-control"] === undefined) {
54
+ response.setHeader("cache-control", "public, max-age=86400");
55
+ }
56
+ response.removeHeader("content-length");
57
+ return response.setBody(body);
58
+ }
59
+ //# sourceMappingURL=httpMiddleware.media.js.map
@@ -6,4 +6,5 @@
6
6
  export * from "./httpMiddleware.contextHelper.js";
7
7
  export * from "./httpMiddleware.accessor.js";
8
8
  export * from "./httpMiddleware.response.js";
9
+ export * from "./httpMiddleware.media.js";
9
10
  //# sourceMappingURL=index.d.ts.map
@@ -6,4 +6,5 @@
6
6
  export * from "./httpMiddleware.contextHelper.js";
7
7
  export * from "./httpMiddleware.accessor.js";
8
8
  export * from "./httpMiddleware.response.js";
9
+ export * from "./httpMiddleware.media.js";
9
10
  //# sourceMappingURL=index.js.map
@@ -6,6 +6,7 @@
6
6
  * Requires: npm install sharp
7
7
  */
8
8
  import type { HttpMiddleware } from "../../httpMiddleware.type.js";
9
+ import { type MediaCompressionErrorHandler } from "../helpers/index.js";
9
10
  export interface ImageCompressionOptions {
10
11
  readonly quality?: number;
11
12
  readonly format?: "jpeg" | "png" | "webp" | "avif";
@@ -17,10 +18,22 @@ export interface ImageCompressionMiddlewareOptions {
17
18
  readonly enabled?: boolean;
18
19
  readonly defaultQuality?: number;
19
20
  readonly defaultFormat?: "jpeg" | "png" | "webp" | "avif";
21
+ /** Upper bound on the output width; applied to every image. */
20
22
  readonly maxWidth?: number;
23
+ /** Upper bound on the output height; applied to every image. */
21
24
  readonly maxHeight?: number;
22
25
  readonly contentTypeMap?: Record<string, ImageCompressionOptions>;
26
+ /**
27
+ * Called when compression fails. The original response is still served.
28
+ * Defaults to a no-op.
29
+ */
30
+ readonly onError?: MediaCompressionErrorHandler;
23
31
  }
24
32
  export declare function compressImage(buffer: Buffer, options?: ImageCompressionOptions): Promise<Buffer>;
33
+ /**
34
+ * Compresses `image/*` responses. Status, headers and body are read from the
35
+ * response returned by `next()`, and `maxWidth` / `maxHeight` cap every
36
+ * output.
37
+ */
25
38
  export declare function createImageCompressionMiddleware(options?: ImageCompressionMiddlewareOptions): HttpMiddleware;
26
39
  //# sourceMappingURL=httpMiddleware.image.d.ts.map