@zudojs/http 1.1.0 → 1.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +53 -3
- package/dist/httpAdapter/http.adapter.d.ts +34 -3
- package/dist/httpAdapter/http.adapter.js +22 -6
- package/dist/httpAdapter/http.adapters.d.ts +24 -3
- package/dist/httpAdapter/http.adapters.js +24 -45
- package/dist/httpAdapter/httpAdapter.errorResponse.d.ts +2 -2
- package/dist/httpAdapter/httpAdapter.errorResponse.js +26 -9
- package/dist/httpAdapter/httpAdapter.logger.d.ts +19 -0
- package/dist/httpAdapter/httpAdapter.logger.js +21 -0
- package/dist/httpAdapter/node/httpNode.adapter.d.ts +12 -1
- package/dist/httpAdapter/node/httpNode.adapter.js +28 -14
- package/dist/httpAdapter/node/httpNode.request.d.ts +8 -7
- package/dist/httpAdapter/node/httpNode.request.js +17 -27
- package/dist/httpAdapter/node/httpNode.server.d.ts +6 -0
- package/dist/httpAdapter/node/httpNode.server.js +6 -0
- package/dist/httpAdapter/node/httpNode.type.d.ts +2 -1
- package/dist/httpAgent/http.agent.d.ts +22 -3
- package/dist/httpAgent/http.agent.js +52 -14
- package/dist/httpCacheControl/httpCacheControl.freshness.d.ts +7 -1
- package/dist/httpCacheControl/httpCacheControl.freshness.js +30 -3
- package/dist/httpCookies/http.cookies.d.ts +29 -2
- package/dist/httpCookies/http.cookies.js +44 -24
- package/dist/httpCookies/httpCookies.defaults.d.ts +40 -0
- package/dist/httpCookies/httpCookies.defaults.js +42 -0
- package/dist/httpCookies/index.d.ts +1 -0
- package/dist/httpCookies/index.js +1 -0
- package/dist/httpKeepAlive/httpKeepAlive.core.js +14 -2
- package/dist/httpMiddleware/builtin/conditional/httpMiddleware.conditional.d.ts +22 -1
- package/dist/httpMiddleware/builtin/conditional/httpMiddleware.conditional.js +19 -4
- package/dist/httpMiddleware/builtin/conditional/index.d.ts +1 -0
- package/dist/httpMiddleware/builtin/cors/httpMiddleware.cors.d.ts +8 -0
- package/dist/httpMiddleware/builtin/cors/httpMiddleware.cors.js +25 -22
- package/dist/httpMiddleware/builtin/helpers/httpMiddleware.accessor.js +2 -6
- package/dist/httpMiddleware/builtin/helpers/httpMiddleware.media.d.ts +33 -0
- package/dist/httpMiddleware/builtin/helpers/httpMiddleware.media.js +59 -0
- package/dist/httpMiddleware/builtin/helpers/index.d.ts +1 -0
- package/dist/httpMiddleware/builtin/helpers/index.js +1 -0
- package/dist/httpMiddleware/builtin/image/httpMiddleware.image.d.ts +13 -0
- package/dist/httpMiddleware/builtin/image/httpMiddleware.image.js +31 -53
- package/dist/httpMiddleware/builtin/index.d.ts +1 -0
- package/dist/httpMiddleware/builtin/index.js +1 -0
- package/dist/httpMiddleware/builtin/logging/httpMiddleware.logging.d.ts +18 -0
- package/dist/httpMiddleware/builtin/logging/httpMiddleware.logging.js +17 -1
- package/dist/httpMiddleware/builtin/rateLimit/httpMiddleware.rateLimit.d.ts +47 -0
- package/dist/httpMiddleware/builtin/rateLimit/httpMiddleware.rateLimit.js +61 -0
- package/dist/httpMiddleware/builtin/rateLimit/index.d.ts +7 -0
- package/dist/httpMiddleware/builtin/rateLimit/index.js +7 -0
- package/dist/httpMiddleware/builtin/security/httpMiddleware.security.d.ts +11 -0
- package/dist/httpMiddleware/builtin/security/httpMiddleware.security.js +25 -12
- package/dist/httpMiddleware/builtin/static/httpMiddleware.static.js +18 -4
- package/dist/httpMiddleware/builtin/video/httpMiddleware.video.d.ts +19 -1
- package/dist/httpMiddleware/builtin/video/httpMiddleware.video.js +38 -59
- package/dist/httpMiddleware/builtin/video/httpMiddleware.video.options.d.ts +12 -0
- package/dist/httpMiddleware/builtin/video/httpMiddleware.video.options.js +31 -0
- package/dist/httpMiddleware/httpMiddleware.error.d.ts +5 -31
- package/dist/httpMiddleware/httpMiddleware.error.js +5 -53
- package/dist/httpNegotiation/httpNegotiation.core.d.ts +13 -0
- package/dist/httpNegotiation/httpNegotiation.core.js +60 -8
- package/dist/httpProxy/http.proxy.d.ts +16 -0
- package/dist/httpProxy/http.proxy.js +44 -3
- package/dist/httpProxy/httpProxy.pathGuard.d.ts +23 -0
- package/dist/httpProxy/httpProxy.pathGuard.js +43 -0
- package/dist/httpProxy/index.d.ts +1 -0
- package/dist/httpProxy/index.js +1 -0
- package/dist/httpQuery/index.d.ts +11 -2
- package/dist/httpQuery/index.js +11 -2
- package/dist/httpQuery/queryParse/index.d.ts +10 -0
- package/dist/httpQuery/queryParse/index.js +10 -0
- package/dist/httpQuery/queryParse/queryParse.flat.d.ts +13 -0
- package/dist/httpQuery/queryParse/queryParse.flat.js +44 -0
- package/dist/httpQuery/queryParse/queryParse.nested.d.ts +25 -0
- package/dist/httpQuery/queryParse/queryParse.nested.js +112 -0
- package/dist/httpQuery/queryParse/queryParse.tokenizer.d.ts +37 -0
- package/dist/httpQuery/queryParse/queryParse.tokenizer.js +95 -0
- package/dist/httpQuery/queryRequest/index.d.ts +9 -0
- package/dist/httpQuery/queryRequest/index.js +9 -0
- package/dist/httpQuery/queryRequest/query.request.d.ts +43 -0
- package/dist/httpQuery/queryRequest/query.request.js +96 -0
- package/dist/httpQuery/querySerialize/index.d.ts +10 -0
- package/dist/httpQuery/querySerialize/index.js +10 -0
- package/dist/httpQuery/querySerialize/query.util.d.ts +21 -0
- package/dist/httpQuery/querySerialize/query.util.js +67 -0
- package/dist/httpQuery/querySerialize/querySerialize.core.d.ts +12 -0
- package/dist/httpQuery/querySerialize/querySerialize.core.js +97 -0
- package/dist/httpQuery/queryTypes/index.d.ts +11 -0
- package/dist/httpQuery/queryTypes/index.js +9 -0
- package/dist/httpQuery/queryTypes/query.container.d.ts +16 -0
- package/dist/httpQuery/queryTypes/query.container.js +51 -0
- package/dist/httpQuery/queryTypes/query.limit.d.ts +25 -0
- package/dist/httpQuery/queryTypes/query.limit.js +32 -0
- package/dist/httpQuery/queryTypes/query.type.d.ts +62 -0
- package/dist/httpQuery/queryTypes/query.type.js +2 -0
- package/dist/httpRedirect/http.redirect.d.ts +6 -0
- package/dist/httpRedirect/http.redirect.js +53 -2
- package/dist/httpRequest/http.request.d.ts +61 -2
- package/dist/httpRequest/http.request.js +86 -35
- package/dist/httpRequest/httpRequest.context.d.ts +7 -0
- package/dist/httpRequest/httpRequest.context.js +26 -34
- package/dist/httpRequest/index.d.ts +1 -0
- package/dist/httpRequest/index.js +1 -0
- package/dist/httpRequest/target/httpRequest.target.d.ts +45 -0
- package/dist/httpRequest/target/httpRequest.target.js +111 -0
- package/dist/httpRequest/target/index.d.ts +9 -0
- package/dist/httpRequest/target/index.js +9 -0
- package/dist/httpResponse/httpResponse.helper.js +2 -1
- package/dist/httpRouter/core/factory/httpRoute.factory.base.d.ts +18 -1
- package/dist/httpRouter/core/factory/httpRoute.factory.base.js +49 -6
- package/dist/httpRouter/core/factory/httpRoute.factory.js +3 -3
- package/dist/httpRouter/core/register/httpRouter.register.js +16 -21
- package/dist/httpRouter/core/types/httpRouter.type.d.ts +6 -0
- package/dist/httpRouter/core/util/httpRoute.util.d.ts +61 -0
- package/dist/httpRouter/core/util/httpRoute.util.js +102 -17
- package/dist/httpRouter/dispatch/httpRoute.dispatcher.d.ts +11 -0
- package/dist/httpRouter/dispatch/httpRoute.dispatcher.js +5 -3
- package/dist/httpRouter/matching/httpRoute.matcher.core.js +51 -6
- package/dist/httpRouter/matching/httpRoute.matcher.d.ts +0 -1
- package/dist/httpRouter/matching/httpRoute.matcher.js +64 -26
- package/dist/httpRouter/pattern/httpRoute.pattern.parse.d.ts +16 -0
- package/dist/httpRouter/pattern/httpRoute.pattern.parse.js +40 -11
- package/dist/httpRouter/pattern/index.d.ts +1 -1
- package/dist/httpRouter/pattern/index.js +1 -1
- package/dist/httpSecurity/httpSecurity.config.d.ts +6 -0
- package/dist/httpSecurity/httpSecurity.config.js +1 -0
- package/dist/httpSecurity/httpSecurity.guard.d.ts +7 -9
- package/dist/httpSecurity/httpSecurity.guard.js +7 -14
- package/dist/httpSecurity/httpSecurity.nodeGuard.d.ts +48 -0
- package/dist/httpSecurity/httpSecurity.nodeGuard.js +32 -0
- package/dist/httpSecurity/httpSecurity.validator.js +20 -9
- package/dist/httpSecurity/index.d.ts +2 -0
- package/dist/httpSecurity/index.js +1 -0
- package/dist/httpServer/core/httpServer.core.d.ts +11 -0
- package/dist/httpServer/core/httpServer.core.js +30 -4
- package/dist/httpTrustProxy/httpTrustProxy.compilation.d.ts +5 -2
- package/dist/httpTrustProxy/httpTrustProxy.compilation.js +19 -2
- package/dist/httpTrustProxy/httpTrustProxy.type.d.ts +3 -1
- package/dist/index.js +0 -1
- package/package.json +5 -5
|
@@ -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
|
-
|
|
23
|
-
|
|
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
|
-
|
|
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
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
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
|
|
21
|
+
function toSecurityCorsConfig(options, context) {
|
|
23
22
|
const configured = options.allowOrigin ?? "*";
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
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
|
-
|
|
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,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
|
|
@@ -5,23 +5,14 @@
|
|
|
5
5
|
*
|
|
6
6
|
* Requires: npm install sharp
|
|
7
7
|
*/
|
|
8
|
-
import {
|
|
8
|
+
import { applyCompressedBody, getResponseBytes, getResponseMediaType, loadOptionalModule, } from "../helpers/index.js";
|
|
9
9
|
const DEFAULT_QUALITY = 80;
|
|
10
10
|
const DEFAULT_FORMAT = "jpeg";
|
|
11
|
-
async function getSharp() {
|
|
12
|
-
try {
|
|
13
|
-
const mod = await import("sharp");
|
|
14
|
-
return mod.default ?? mod;
|
|
15
|
-
}
|
|
16
|
-
catch {
|
|
17
|
-
throw new Error("Sharp is not installed. Run: npm install sharp");
|
|
18
|
-
}
|
|
19
|
-
}
|
|
20
11
|
export async function compressImage(buffer, options = {}) {
|
|
21
|
-
const
|
|
12
|
+
const sharp = await loadOptionalModule("sharp", "npm install sharp");
|
|
22
13
|
const quality = options.quality ?? DEFAULT_QUALITY;
|
|
23
14
|
const format = options.format ?? DEFAULT_FORMAT;
|
|
24
|
-
let pipeline =
|
|
15
|
+
let pipeline = sharp(buffer);
|
|
25
16
|
if (options.width || options.height) {
|
|
26
17
|
pipeline = pipeline.resize({
|
|
27
18
|
width: options.width,
|
|
@@ -30,59 +21,46 @@ export async function compressImage(buffer, options = {}) {
|
|
|
30
21
|
withoutEnlargement: true,
|
|
31
22
|
});
|
|
32
23
|
}
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
case "webp":
|
|
39
|
-
return pipeline.webp({ quality }).toBuffer();
|
|
40
|
-
case "avif":
|
|
41
|
-
return pipeline.avif({ quality }).toBuffer();
|
|
42
|
-
default:
|
|
43
|
-
return pipeline.toBuffer();
|
|
24
|
+
return pipeline[format]({ quality }).toBuffer();
|
|
25
|
+
}
|
|
26
|
+
function capDimension(requested, max) {
|
|
27
|
+
if (max === undefined) {
|
|
28
|
+
return requested;
|
|
44
29
|
}
|
|
30
|
+
return requested === undefined ? max : Math.min(requested, max);
|
|
45
31
|
}
|
|
32
|
+
/**
|
|
33
|
+
* Compresses `image/*` responses. Status, headers and body are read from the
|
|
34
|
+
* response returned by `next()`, and `maxWidth` / `maxHeight` cap every
|
|
35
|
+
* output.
|
|
36
|
+
*/
|
|
46
37
|
export function createImageCompressionMiddleware(options = {}) {
|
|
47
38
|
const enabled = options.enabled ?? true;
|
|
48
|
-
const defaultQuality = options.defaultQuality ?? DEFAULT_QUALITY;
|
|
49
|
-
const defaultFormat = options.defaultFormat ?? DEFAULT_FORMAT;
|
|
50
39
|
const contentTypeMap = options.contentTypeMap ?? {};
|
|
51
|
-
return async (
|
|
52
|
-
if (!enabled) {
|
|
53
|
-
return next();
|
|
54
|
-
}
|
|
40
|
+
return async (_context, next) => {
|
|
55
41
|
const response = await next();
|
|
56
|
-
const
|
|
57
|
-
if (!
|
|
42
|
+
const mediaType = getResponseMediaType(response);
|
|
43
|
+
if (!enabled || !mediaType?.startsWith("image/")) {
|
|
58
44
|
return response;
|
|
59
45
|
}
|
|
60
|
-
const
|
|
61
|
-
if (!
|
|
46
|
+
const buffer = getResponseBytes(response);
|
|
47
|
+
if (!buffer || buffer.length === 0) {
|
|
62
48
|
return response;
|
|
63
49
|
}
|
|
64
|
-
const
|
|
65
|
-
const
|
|
66
|
-
quality: defaultQuality,
|
|
67
|
-
format: defaultFormat,
|
|
68
|
-
};
|
|
50
|
+
const mapped = contentTypeMap[mediaType] ?? {};
|
|
51
|
+
const format = mapped.format ?? options.defaultFormat ?? DEFAULT_FORMAT;
|
|
69
52
|
try {
|
|
70
|
-
const compressed = await compressImage(buffer,
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
? "image/avif"
|
|
79
|
-
: contentType;
|
|
80
|
-
const headers = new Headers(response.headers);
|
|
81
|
-
headers.set("content-type", newContentType);
|
|
82
|
-
headers.set("cache-control", headers.get("cache-control") ?? "public, max-age=86400");
|
|
83
|
-
return applyHeadersToResponse(response, headers).setBody(compressed);
|
|
53
|
+
const compressed = await compressImage(buffer, {
|
|
54
|
+
...mapped,
|
|
55
|
+
quality: mapped.quality ?? options.defaultQuality ?? DEFAULT_QUALITY,
|
|
56
|
+
format,
|
|
57
|
+
width: capDimension(mapped.width, options.maxWidth),
|
|
58
|
+
height: capDimension(mapped.height, options.maxHeight),
|
|
59
|
+
});
|
|
60
|
+
return applyCompressedBody(response, compressed, `image/${format}`);
|
|
84
61
|
}
|
|
85
|
-
catch {
|
|
62
|
+
catch (error) {
|
|
63
|
+
options.onError?.(error, response);
|
|
86
64
|
return response;
|
|
87
65
|
}
|
|
88
66
|
};
|
|
@@ -13,6 +13,7 @@ export * from "./helpers/index.js";
|
|
|
13
13
|
export * from "./static/index.js";
|
|
14
14
|
export * from "./image/index.js";
|
|
15
15
|
export * from "./video/index.js";
|
|
16
|
+
export * from "./rateLimit/index.js";
|
|
16
17
|
/** State middleware — identity pass-through. */
|
|
17
18
|
export declare function createStateMiddleware(): import("../httpMiddleware.type.js").HttpMiddleware;
|
|
18
19
|
//# sourceMappingURL=index.d.ts.map
|
|
@@ -13,6 +13,7 @@ export * from "./helpers/index.js";
|
|
|
13
13
|
export * from "./static/index.js";
|
|
14
14
|
export * from "./image/index.js";
|
|
15
15
|
export * from "./video/index.js";
|
|
16
|
+
export * from "./rateLimit/index.js";
|
|
16
17
|
/** State middleware — identity pass-through. */
|
|
17
18
|
export function createStateMiddleware() {
|
|
18
19
|
return async (_context, next) => {
|
|
@@ -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
|
});
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Rate limiting middleware built on `@zudojs/security`.
|
|
3
|
+
*
|
|
4
|
+
* AGENTS.md requires public endpoints to be rate limited with
|
|
5
|
+
* `createRateLimiter` from `@zudojs/security`, and `@zudojs/http` offered no
|
|
6
|
+
* way to do that. This wraps the shared limiter; it does not re-implement it.
|
|
7
|
+
*
|
|
8
|
+
* The client key is `request.remoteAddress`, which the Node adapter has
|
|
9
|
+
* already resolved through its `trustProxy` setting, so forwarded headers
|
|
10
|
+
* only count when the adapter trusts the peer that sent them.
|
|
11
|
+
*
|
|
12
|
+
* A request with no usable client address (a Unix-socket peer, a context
|
|
13
|
+
* built without `remoteAddress`, a destroyed socket) is counted in one
|
|
14
|
+
* shared bucket, {@link UNKNOWN_CLIENT_RATE_LIMIT_IP}. The `@zudojs/security`
|
|
15
|
+
* default key generator refuses such a request with a `ConfigurationError`,
|
|
16
|
+
* which would otherwise surface as a 500, and skipping the limiter for it
|
|
17
|
+
* would give it unlimited requests.
|
|
18
|
+
*
|
|
19
|
+
* @module httpMiddleware/builtin/rateLimit
|
|
20
|
+
*/
|
|
21
|
+
import { createRateLimiter, type RateLimiterOptions } from "@zudojs/security";
|
|
22
|
+
import type { HttpMiddleware } from "../../httpMiddleware.type.js";
|
|
23
|
+
/**
|
|
24
|
+
* The `ip` given to the limiter for a request whose `remoteAddress` is
|
|
25
|
+
* missing or is not an IP address. `0.0.0.0` is never a real peer address,
|
|
26
|
+
* so these requests share one bucket without colliding with a client.
|
|
27
|
+
*/
|
|
28
|
+
export declare const UNKNOWN_CLIENT_RATE_LIMIT_IP = "0.0.0.0";
|
|
29
|
+
/**
|
|
30
|
+
* The limiter instance returned by `@zudojs/security`'s `createRateLimiter`.
|
|
31
|
+
*/
|
|
32
|
+
export type HttpRateLimiter = ReturnType<typeof createRateLimiter>;
|
|
33
|
+
/**
|
|
34
|
+
* Options for {@link createRateLimitMiddleware}: either the
|
|
35
|
+
* `@zudojs/security` limiter options, or an existing `limiter` to share
|
|
36
|
+
* between routes.
|
|
37
|
+
*/
|
|
38
|
+
export type RateLimitMiddlewareOptions = RateLimiterOptions | {
|
|
39
|
+
readonly limiter: HttpRateLimiter;
|
|
40
|
+
};
|
|
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.
|
|
45
|
+
*/
|
|
46
|
+
export declare function createRateLimitMiddleware(options: RateLimitMiddlewareOptions): HttpMiddleware;
|
|
47
|
+
//# sourceMappingURL=httpMiddleware.rateLimit.d.ts.map
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Rate limiting middleware built on `@zudojs/security`.
|
|
3
|
+
*
|
|
4
|
+
* AGENTS.md requires public endpoints to be rate limited with
|
|
5
|
+
* `createRateLimiter` from `@zudojs/security`, and `@zudojs/http` offered no
|
|
6
|
+
* way to do that. This wraps the shared limiter; it does not re-implement it.
|
|
7
|
+
*
|
|
8
|
+
* The client key is `request.remoteAddress`, which the Node adapter has
|
|
9
|
+
* already resolved through its `trustProxy` setting, so forwarded headers
|
|
10
|
+
* only count when the adapter trusts the peer that sent them.
|
|
11
|
+
*
|
|
12
|
+
* A request with no usable client address (a Unix-socket peer, a context
|
|
13
|
+
* built without `remoteAddress`, a destroyed socket) is counted in one
|
|
14
|
+
* shared bucket, {@link UNKNOWN_CLIENT_RATE_LIMIT_IP}. The `@zudojs/security`
|
|
15
|
+
* default key generator refuses such a request with a `ConfigurationError`,
|
|
16
|
+
* which would otherwise surface as a 500, and skipping the limiter for it
|
|
17
|
+
* would give it unlimited requests.
|
|
18
|
+
*
|
|
19
|
+
* @module httpMiddleware/builtin/rateLimit
|
|
20
|
+
*/
|
|
21
|
+
import { createRateLimiter, parseClientIp, } from "@zudojs/security";
|
|
22
|
+
import { createResponseContext } from "../../../httpResponse/httpResponse.context.js";
|
|
23
|
+
/**
|
|
24
|
+
* The `ip` given to the limiter for a request whose `remoteAddress` is
|
|
25
|
+
* missing or is not an IP address. `0.0.0.0` is never a real peer address,
|
|
26
|
+
* so these requests share one bucket without colliding with a client.
|
|
27
|
+
*/
|
|
28
|
+
export const UNKNOWN_CLIENT_RATE_LIMIT_IP = "0.0.0.0";
|
|
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.
|
|
33
|
+
*/
|
|
34
|
+
export function createRateLimitMiddleware(options) {
|
|
35
|
+
const limiter = "limiter" in options ? options.limiter : createRateLimiter(options);
|
|
36
|
+
return async (context, next) => {
|
|
37
|
+
const request = context.request;
|
|
38
|
+
const limitRequest = {
|
|
39
|
+
ip: clientIpOf(request.remoteAddress),
|
|
40
|
+
method: request.method,
|
|
41
|
+
path: request.path,
|
|
42
|
+
headers: request.headers,
|
|
43
|
+
};
|
|
44
|
+
const rejection = { statusCode: 429, headers: {} };
|
|
45
|
+
if (limiter.middleware(limitRequest, rejection).allowed) {
|
|
46
|
+
return next();
|
|
47
|
+
}
|
|
48
|
+
const response = createResponseContext().setStatus(rejection.statusCode);
|
|
49
|
+
for (const [name, value] of Object.entries(rejection.headers)) {
|
|
50
|
+
response.setHeader(name.toLowerCase(), value);
|
|
51
|
+
}
|
|
52
|
+
return response.setBody(rejection.body ?? "Too Many Requests");
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
function clientIpOf(remoteAddress) {
|
|
56
|
+
return typeof remoteAddress === "string" &&
|
|
57
|
+
parseClientIp(remoteAddress) !== undefined
|
|
58
|
+
? remoteAddress
|
|
59
|
+
: UNKNOWN_CLIENT_RATE_LIMIT_IP;
|
|
60
|
+
}
|
|
61
|
+
//# sourceMappingURL=httpMiddleware.rateLimit.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @zudojs/http/httpMiddleware/builtin/rateLimit
|
|
3
|
+
*
|
|
4
|
+
* Rate limiting middleware wrapping `@zudojs/security`'s `createRateLimiter`.
|
|
5
|
+
*/
|
|
6
|
+
export { createRateLimitMiddleware, UNKNOWN_CLIENT_RATE_LIMIT_IP, type HttpRateLimiter, type RateLimitMiddlewareOptions, } from "./httpMiddleware.rateLimit.js";
|
|
7
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @zudojs/http/httpMiddleware/builtin/rateLimit
|
|
3
|
+
*
|
|
4
|
+
* Rate limiting middleware wrapping `@zudojs/security`'s `createRateLimiter`.
|
|
5
|
+
*/
|
|
6
|
+
export { createRateLimitMiddleware, UNKNOWN_CLIENT_RATE_LIMIT_IP, } from "./httpMiddleware.rateLimit.js";
|
|
7
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -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
|