@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.
- 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.js +4 -34
- 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/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/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/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/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/httpProxy/http.proxy.d.ts +7 -0
- package/dist/httpProxy/http.proxy.js +9 -0
- 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/httpRequest/httpRequest.context.d.ts +7 -0
- package/dist/httpRequest/httpRequest.context.js +15 -16
- 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 +93 -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/util/httpRoute.util.d.ts +14 -0
- package/dist/httpRouter/core/util/httpRoute.util.js +18 -14
- 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 +4 -2
- 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
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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 (!
|
|
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
|
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|