diff --git a/CHANGELOG.md b/CHANGELOG.md
index 826a45635..789a7a23d 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -2,6 +2,15 @@
## Version 28
+### v28.1.0
+
+- Added support for cookie handling:
+ - Cookie parsing can be enabled and configured in config (requires to install `cookie-parser`);
+ - `cookies` and `signedCookies` can be used as `inputSources` in config;
+ - `createCookieMiddleware()` creates a Middleware that exposes `setCookie()` and `clearCookie()` helpers into context
+ as well as the `getCookie()` one as an alternative to using cookies within `inputSources`;
+ - Documentation depicts request parameters when Middleware has `security` schema with `type: cookie`.
+
### v28.0.1
- Adjusted the list of well-known headers, recognized by Documentation generator:
diff --git a/README.md b/README.md
index 23602bf46..66f97fcdf 100644
--- a/README.md
+++ b/README.md
@@ -34,17 +34,18 @@ Start your API server with I/O schema validation and custom middlewares in minut
15. [Child logger](#child-logger)
5. [Advanced features](#advanced-features)
1. [Customizing input sources](#customizing-input-sources)
- 2. [Headers as input source](#headers-as-an-input-source)
- 3. [Response customization](#response-customization)
- 4. [Empty response](#empty-response)
- 5. [Non-JSON response](#non-json-response) including file downloads
- 6. [Error handling](#error-handling)
- 7. [Production mode](#production-mode)
- 8. [HTML Forms (URL encoded)](#html-forms-url-encoded)
- 9. [File uploads](#file-uploads)
- 10. [Connect to your own express app](#connect-to-your-own-express-app)
- 11. [Testing endpoints](#testing-endpoints)
- 12. [Testing middlewares](#testing-middlewares)
+ 2. [Headers as an input source](#headers-as-an-input-source)
+ 3. [Cookies](#cookies)
+ 4. [Response customization](#response-customization)
+ 5. [Empty response](#empty-response)
+ 6. [Non-JSON response](#non-json-response) including file downloads
+ 7. [Error handling](#error-handling)
+ 8. [Production mode](#production-mode)
+ 9. [HTML Forms (URL encoded)](#html-forms-url-encoded)
+ 10. [File uploads](#file-uploads)
+ 11. [Connect to your own express app](#connect-to-your-own-express-app)
+ 12. [Testing endpoints](#testing-endpoints)
+ 13. [Testing middlewares](#testing-middlewares)
6. [Integration and Documentation](#integration-and-documentation)
1. [Zod Plugin](#zod-plugin)
2. [End-to-End Type Safety](#end-to-end-type-safety)
@@ -818,6 +819,49 @@ factory.build({
});
```
+## Cookies
+
+Install `cookie-parser` as well as `@types/cookie-parser` and enable `cookies` in your config. To validate cookies add
+`"cookies"` and/or `"signedCookies"` to your `inputSources` (the order [matters](#customizing-input-sources)!):
+
+```ts
+import { createConfig } from "express-zod-api";
+
+const config = createConfig({
+ cookies: { secret: "my-secret" }, // or true; the secret enables signedCookies
+ inputSources: {
+ get: ["query", "params", "cookies", "signedCookies"], // for methods of your choice
+ },
+});
+```
+
+Consider `createCookieMiddleware()` that makes a Middleware providing `setCookie()` and `clearCookie()` helpers,
+as well as `getCookie()` — alternative to the cookies as an input source:
+
+```ts
+import { createCookieMiddleware, Middleware } from "express-zod-api";
+
+const cookieDrivenFactory = factory
+ .addMiddleware(
+ createCookieMiddleware({ httpOnly: true, sameSite: "lax", path: "/" }), // recommended base options
+ )
+ .addMiddleware(
+ new Middleware({
+ security: { type: "cookie", name: "session" }, // improves Documentation
+ input: z.object({ session: z.string() }), // alternatively, use getCookie
+ handler: async ({ input: { session }, ctx: { getCookie } }) => {
+ assert.equal(session, getCookie("session")); // getCookie reads from signedCookies first
+ },
+ }),
+ );
+
+const sessionSettingEndpoint = cookieDrivenFactory.buildVoid({
+ handler: async ({ ctx: { getCookie, setCookie } }) => {
+ setCookie("session", "abc123", { httpOnly: false }); // overridden cookie options
+ },
+});
+```
+
## Response customization
`ResultHandler` is responsible for transmitting consistent responses containing the endpoint output or an error.
diff --git a/dataflow.svg b/dataflow.svg
index 72886fee4..cd1fb5042 100644
--- a/dataflow.svg
+++ b/dataflow.svg
@@ -1,4 +1,4 @@
-
+
\ No newline at end of file
diff --git a/example/config.ts b/example/config.ts
index 1d262d477..d49d77573 100644
--- a/example/config.ts
+++ b/example/config.ts
@@ -13,6 +13,7 @@ export const config = createConfig({
limitError: createHttpError(413, "The file is too large"), // affects uploadAvatarEndpoint
},
compression: true, // affects sendAvatarEndpoint
+ cookies: true, // for uploadAvatarEndpoint
// third-party middlewares serving their own routes or establishing their own routing besides the API
beforeRouting: ({ app }) => {
app.use(
@@ -23,6 +24,7 @@ export const config = createConfig({
},
inputSources: {
patch: ["headers", "body", "params"], // affects authMiddleware used by updateUserEndpoint
+ post: ["body", "params", "files", "cookies"], // cookies for uploadAvatarEndpoint
},
cors: true,
});
@@ -47,5 +49,6 @@ declare module "express-zod-api" {
files: unknown;
subscriptions: unknown;
forms: unknown;
+ cookies: unknown;
}
}
diff --git a/example/endpoints/login.ts b/example/endpoints/login.ts
new file mode 100644
index 000000000..c967a44f4
--- /dev/null
+++ b/example/endpoints/login.ts
@@ -0,0 +1,31 @@
+import { cookieAssistedFactory } from "../factories.ts";
+import { z } from "zod";
+import { randomUUID, scrypt } from "node:crypto";
+import createHttpError from "http-errors";
+import assert from "node:assert/strict";
+import { promisify } from "node:util";
+
+/** @desc The endpoint demonstrates setting a cookie */
+export const loginEndpoint = cookieAssistedFactory.build({
+ method: "post",
+ tag: "cookies",
+ input: z.object({
+ username: z.string().trim().nonempty(),
+ password: z.string().trim().nonempty(),
+ }),
+ output: z.object({ message: z.string() }),
+ handler: async ({ input: { username, password }, ctx: { setCookie } }) => {
+ const key = await promisify(scrypt)(
+ password,
+ "kinda salt",
+ 16,
+ );
+ assert(
+ username === "admin" &&
+ key.toString("hex") === "79ad19b8c03bc92a2f25ed865400264e",
+ createHttpError(401, "Invalid credentials"),
+ );
+ setCookie("session", { token: randomUUID() });
+ return { message: "Logged in" };
+ },
+});
diff --git a/example/endpoints/upload-avatar.ts b/example/endpoints/upload-avatar.ts
index 3ce55af96..7c5e77843 100644
--- a/example/endpoints/upload-avatar.ts
+++ b/example/endpoints/upload-avatar.ts
@@ -1,8 +1,12 @@
import { z } from "zod";
-import { defaultEndpointsFactory, ez } from "express-zod-api";
+import { ez } from "express-zod-api";
import { createHash } from "node:crypto";
+import { cookieAuthenticatedFactory } from "../factories.ts";
+import assert from "node:assert/strict";
+import createHttpError from "http-errors";
-export const uploadAvatarEndpoint = defaultEndpointsFactory.build({
+/** @desc The endpoint demonstrates handling a file upload and cookie as an input source */
+export const uploadAvatarEndpoint = cookieAuthenticatedFactory.build({
method: "post",
tag: "files",
description: "Handles a file upload.",
@@ -16,11 +20,14 @@ export const uploadAvatarEndpoint = defaultEndpointsFactory.build({
hash: z.string(),
otherInputs: z.record(z.string(), z.any()),
}),
- handler: async ({ input: { avatar, ...rest } }) => ({
- name: avatar.name,
- size: avatar.size,
- mime: avatar.mimetype,
- hash: createHash("sha1").update(avatar.data).digest("hex"),
- otherInputs: rest,
- }),
+ handler: async ({ input: { avatar, ...rest }, ctx: { session } }) => {
+ assert(session.token, createHttpError(401, "Unauthorized"));
+ return {
+ name: avatar.name,
+ size: avatar.size,
+ mime: avatar.mimetype,
+ hash: createHash("sha1").update(avatar.data).digest("hex"),
+ otherInputs: rest,
+ };
+ },
});
diff --git a/example/example.client.ts b/example/example.client.ts
index aaf09a4df..1e3469dc9 100644
--- a/example/example.client.ts
+++ b/example/example.client.ts
@@ -205,6 +205,38 @@ interface HeadV1UserListNegativeResponseVariants {
400: HeadV1UserListNegativeVariant1;
}
+/** post /v1/login */
+type PostV1LoginInput = {
+ username: string;
+ password: string;
+};
+
+/** post /v1/login */
+type PostV1LoginPositiveVariant1 = {
+ status: "success";
+ data: {
+ message: string;
+ };
+};
+
+/** post /v1/login */
+interface PostV1LoginPositiveResponseVariants {
+ 200: PostV1LoginPositiveVariant1;
+}
+
+/** post /v1/login */
+type PostV1LoginNegativeVariant1 = {
+ status: "error";
+ error: {
+ message: string;
+ };
+};
+
+/** post /v1/login */
+interface PostV1LoginNegativeResponseVariants {
+ 400: PostV1LoginNegativeVariant1;
+}
+
/** get /v1/avatar/send */
type GetV1AvatarSendInput = {
userId: string;
@@ -291,6 +323,9 @@ interface HeadV1AvatarStreamNegativeResponseVariants {
/** post /v1/avatar/upload */
type PostV1AvatarUploadInput = {
+ session: {
+ token: string;
+ };
avatar: any;
};
@@ -513,6 +548,7 @@ export type Path =
| "/v1/user/:id"
| "/v1/user/create"
| "/v1/user/list"
+ | "/v1/login"
| "/v1/avatar/send"
| "/v1/avatar/stream"
| "/v1/avatar/upload"
@@ -531,6 +567,7 @@ export interface Input {
"post /v1/user/create": PostV1UserCreateInput;
"get /v1/user/list": GetV1UserListInput;
"head /v1/user/list": HeadV1UserListInput;
+ "post /v1/login": PostV1LoginInput;
/** @deprecated */
"get /v1/avatar/send": GetV1AvatarSendInput;
/** @deprecated */
@@ -554,6 +591,7 @@ export interface PositiveResponse {
"post /v1/user/create": SomeOf;
"get /v1/user/list": SomeOf;
"head /v1/user/list": SomeOf;
+ "post /v1/login": SomeOf;
/** @deprecated */
"get /v1/avatar/send": SomeOf;
/** @deprecated */
@@ -577,6 +615,7 @@ export interface NegativeResponse {
"post /v1/user/create": SomeOf;
"get /v1/user/list": SomeOf;
"head /v1/user/list": SomeOf;
+ "post /v1/login": SomeOf;
/** @deprecated */
"get /v1/avatar/send": SomeOf;
/** @deprecated */
@@ -607,6 +646,8 @@ export interface EncodedResponse {
GetV1UserListNegativeResponseVariants;
"head /v1/user/list": HeadV1UserListPositiveResponseVariants &
HeadV1UserListNegativeResponseVariants;
+ "post /v1/login": PostV1LoginPositiveResponseVariants &
+ PostV1LoginNegativeResponseVariants;
/** @deprecated */
"get /v1/avatar/send": GetV1AvatarSendPositiveResponseVariants &
GetV1AvatarSendNegativeResponseVariants;
@@ -655,6 +696,9 @@ export interface Response {
"head /v1/user/list":
| PositiveResponse["head /v1/user/list"]
| NegativeResponse["head /v1/user/list"];
+ "post /v1/login":
+ | PositiveResponse["post /v1/login"]
+ | NegativeResponse["post /v1/login"];
/** @deprecated */
"get /v1/avatar/send":
| PositiveResponse["get /v1/avatar/send"]
@@ -702,6 +746,7 @@ export const endpointTags = {
"post /v1/user/create": ["users"],
"get /v1/user/list": ["users"],
"head /v1/user/list": ["users"],
+ "post /v1/login": ["cookies"],
"get /v1/avatar/send": ["files", "users"],
"head /v1/avatar/send": ["files", "users"],
"get /v1/avatar/stream": ["users", "files"],
diff --git a/example/example.documentation.yaml b/example/example.documentation.yaml
index 014ebab98..5cb3a1df5 100644
--- a/example/example.documentation.yaml
+++ b/example/example.documentation.yaml
@@ -466,6 +466,79 @@ paths:
description: HEAD /v1/user/list Positive response
"400":
description: HEAD /v1/user/list Negative response
+ /v1/login:
+ post:
+ operationId: PostV1Login
+ tags:
+ - cookies
+ requestBody:
+ description: POST /v1/login Request body
+ content:
+ application/json:
+ schema:
+ type: object
+ properties:
+ username:
+ type: string
+ minLength: 1
+ password:
+ type: string
+ minLength: 1
+ required:
+ - username
+ - password
+ required: true
+ responses:
+ "200":
+ description: POST /v1/login Positive response
+ content:
+ application/json:
+ schema:
+ type: object
+ properties:
+ status:
+ type: string
+ const: success
+ data:
+ type: object
+ properties:
+ message:
+ type: string
+ required:
+ - message
+ additionalProperties: false
+ required:
+ - status
+ - data
+ additionalProperties: false
+ "400":
+ description: POST /v1/login Negative response
+ content:
+ application/json:
+ schema:
+ type: object
+ properties:
+ status:
+ type: string
+ const: error
+ error:
+ type: object
+ properties:
+ message:
+ type: string
+ required:
+ - message
+ additionalProperties: false
+ required:
+ - status
+ - error
+ additionalProperties: false
+ examples:
+ example1:
+ value:
+ status: error
+ error:
+ message: Sample error message
/v1/avatar/send:
get:
operationId: GetV1AvatarSend
@@ -571,6 +644,18 @@ paths:
description: Handles a file upload.
tags:
- files
+ parameters:
+ - name: session
+ in: cookie
+ required: true
+ description: POST /v1/avatar/upload Parameter
+ schema:
+ type: object
+ properties:
+ token:
+ type: string
+ required:
+ - token
requestBody:
description: POST /v1/avatar/upload Request body
content:
@@ -583,8 +668,9 @@ paths:
format: binary
required:
- avatar
- additionalProperties: {}
required: true
+ security:
+ - APIKEY_3: []
responses:
"200":
description: POST /v1/avatar/upload Positive response
@@ -1068,6 +1154,10 @@ components:
type: apiKey
in: header
name: token
+ APIKEY_3:
+ type: apiKey
+ in: cookie
+ name: session
links: {}
callbacks: {}
tags:
diff --git a/example/factories.ts b/example/factories.ts
index e63782016..6c7d47f3e 100644
--- a/example/factories.ts
+++ b/example/factories.ts
@@ -7,7 +7,11 @@ import {
EventStreamFactory,
defaultEndpointsFactory,
} from "express-zod-api";
-import { authMiddleware } from "./middlewares.ts";
+import {
+ authMiddleware,
+ cookieAssistingMiddleware,
+ sessionMiddleware,
+} from "./middlewares.ts";
import { createReadStream } from "node:fs";
import { z } from "zod";
import { stat } from "node:fs/promises";
@@ -16,6 +20,15 @@ import { stat } from "node:fs/promises";
export const keyAndTokenAuthenticatedEndpointsFactory =
defaultEndpointsFactory.addMiddleware(authMiddleware);
+/** @desc This factory adds session read from cookie into context */
+export const cookieAuthenticatedFactory =
+ defaultEndpointsFactory.addMiddleware(sessionMiddleware);
+
+/** @desc This factory adds setCookie() helper to context */
+export const cookieAssistedFactory = defaultEndpointsFactory.addMiddleware(
+ cookieAssistingMiddleware,
+);
+
/** @desc This factory sends the file as string located in the "data" property of the endpoint's output */
export const fileSendingEndpointsFactory = new EndpointsFactory(
new ResultHandler({
diff --git a/example/index.spec.ts b/example/index.spec.ts
index 41a7d1c62..9d4dc2e72 100644
--- a/example/index.spec.ts
+++ b/example/index.spec.ts
@@ -236,7 +236,15 @@ describe("Example", async () => {
data.append("obj[some]", "thing");
const response = await fetch(
`http://localhost:${port}/v1/avatar/upload`,
- { method: "POST", body: data },
+ {
+ method: "POST",
+ body: data,
+ headers: {
+ Cookie:
+ "session=j%3A%7B%22token%22%3A%22553280ce-ab20-4481-a9dc-fd3fc4f6759c%22%7D; " +
+ "Path=/; HttpOnly; SameSite=Lax",
+ },
+ },
);
expect(response.headers.get("access-control-allow-methods")).toBe(
"POST, OPTIONS",
@@ -250,10 +258,11 @@ describe("Example", async () => {
otherInputs: {
arr: ["456", "789"],
num: "123",
- obj: {
- some: "thing",
- },
+ obj: { some: "thing" },
str: "test string value",
+ Path: "/", // from cookie
+ SameSite: "Lax",
+ session: { token: "553280ce-ab20-4481-a9dc-fd3fc4f6759c" },
},
size: 48687,
},
@@ -314,6 +323,21 @@ describe("Example", async () => {
await vi.waitFor(() => assert(stack.length > 2), { timeout: 5e3 });
subscription.source.close();
});
+
+ test("Should send readable cookies", async () => {
+ const response = await fetch(`http://localhost:${port}/v1/login`, {
+ method: "POST",
+ headers: { "Content-Type": "application/json" },
+ body: JSON.stringify({ username: "admin", password: "test" }),
+ });
+ expect(response.status).toBe(200);
+ expect(await response.json()).toEqual({
+ data: { message: "Logged in" },
+ status: "success",
+ });
+ expect(response.headers.get("set-cookie")).toMatch(/^session=j/);
+ console.log(response.headers.get("set-cookie"));
+ });
});
describe("Protocol", () => {
diff --git a/example/middlewares.ts b/example/middlewares.ts
index c4b922b76..bb432454e 100644
--- a/example/middlewares.ts
+++ b/example/middlewares.ts
@@ -1,7 +1,11 @@
import createHttpError from "http-errors";
import assert from "node:assert/strict";
import { z } from "zod";
-import { Middleware, type Method } from "express-zod-api";
+import {
+ createCookieMiddleware,
+ Middleware,
+ type Method,
+} from "express-zod-api";
export const authMiddleware = new Middleware({
security: {
@@ -22,6 +26,20 @@ export const authMiddleware = new Middleware({
},
});
+/** @desc This middleware uses cookie as an input source and reads session from it */
+export const sessionMiddleware = new Middleware({
+ security: { type: "cookie", name: "session" },
+ input: z.object({ session: z.object({ token: z.string() }) }),
+ handler: async ({ input: { session } }) => ({ session }),
+});
+
+/** @desc This middleware provides setCookie() helper to context */
+export const cookieAssistingMiddleware = createCookieMiddleware({
+ httpOnly: true,
+ sameSite: "lax",
+ path: "/",
+});
+
export const methodProviderMiddleware = new Middleware({
handler: async ({ request }) => ({
method: request.method.toLowerCase() as Method,
diff --git a/example/routing.ts b/example/routing.ts
index 20139e457..339dc5132 100644
--- a/example/routing.ts
+++ b/example/routing.ts
@@ -12,6 +12,7 @@ import { retrieveUserEndpoint } from "./endpoints/retrieve-user.ts";
import { sendAvatarEndpoint } from "./endpoints/send-avatar.ts";
import { updateUserEndpoint } from "./endpoints/update-user.ts";
import { streamAvatarEndpoint } from "./endpoints/stream-avatar.ts";
+import { loginEndpoint } from "./endpoints/login.ts";
export const routing: Routing = {
v1: {
@@ -29,6 +30,7 @@ export const routing: Routing = {
// this one demonstrates the legacy array-based response
list: listUsersEndpoint,
},
+ login: loginEndpoint, // demonstrates cookie sending
avatar: {
// custom result handler examples with a file serving
send: sendAvatarEndpoint.deprecated(), // demo for deprecated route
diff --git a/express-zod-api/package.json b/express-zod-api/package.json
index 9f18210a4..9dd38dab0 100644
--- a/express-zod-api/package.json
+++ b/express-zod-api/package.json
@@ -49,10 +49,12 @@
"peerDependencies": {
"@express-zod-api/zod-plugin": "workspace:^",
"@types/compression": "^1.7.5",
+ "@types/cookie-parser": "^1.4.10",
"@types/express": "^5.0.0",
"@types/express-fileupload": "^1.5.0",
"@types/http-errors": "^2.0.2",
"compression": "^1.8.0",
+ "cookie-parser": "^1.4.7",
"express": "^5.1.0",
"express-fileupload": "^1.5.0",
"http-errors": "^2.0.1",
@@ -66,6 +68,9 @@
"@types/compression": {
"optional": true
},
+ "@types/cookie-parser": {
+ "optional": true
+ },
"@types/express": {
"optional": true
},
@@ -78,6 +83,9 @@
"compression": {
"optional": true
},
+ "cookie-parser": {
+ "optional": true
+ },
"express-fileupload": {
"optional": true
},
@@ -88,6 +96,7 @@
"devDependencies": {
"@express-zod-api/zod-plugin": "workspace:^",
"@types/compression": "catalog:dev",
+ "@types/cookie-parser": "^1.4.10",
"@types/cors": "^2.8.19",
"@types/depd": "^1.1.37",
"@types/express": "catalog:dev",
@@ -97,6 +106,7 @@
"@types/ramda": "catalog:dev",
"camelize-ts": "catalog:dev",
"compression": "catalog:dev",
+ "cookie-parser": "^1.4.7",
"cors": "^2.8.5",
"depd": "^2.0.0",
"express": "catalog:dev",
diff --git a/express-zod-api/src/config-type.ts b/express-zod-api/src/config-type.ts
index f9c6bcbf2..e3c87bfa2 100644
--- a/express-zod-api/src/config-type.ts
+++ b/express-zod-api/src/config-type.ts
@@ -1,6 +1,7 @@
import type compression from "compression";
import type { IRouter, Request, RequestHandler } from "express";
import type fileUpload from "express-fileupload";
+import type cookieParser from "cookie-parser";
import type { ServerOptions } from "node:https";
import type { BuiltinLoggerConfig } from "./builtin-logger";
import type { AbstractEndpoint } from "./endpoint";
@@ -12,7 +13,13 @@ import type { GetLogger } from "./server-helpers";
export type InputSource = keyof Pick<
Request,
- "query" | "body" | "files" | "params" | "headers"
+ | "query"
+ | "body"
+ | "files"
+ | "params"
+ | "headers"
+ | "cookies"
+ | "signedCookies"
>;
export type InputSources = Record;
@@ -123,6 +130,11 @@ type UploadOptions = Pick<
beforeUpload?: BeforeUpload;
};
+interface CookieParserOptions extends cookieParser.CookieParseOptions {
+ /** @desc The secret string or array used by cookie-parser for signed cookies */
+ secret?: Parameters[0];
+}
+
type CompressionOptions = Pick<
compression.CompressionOptions,
"threshold" | "level" | "strategy" | "chunkSize" | "memLevel"
@@ -181,6 +193,13 @@ export interface ServerConfig extends CommonConfig {
* @requires compression
*/
compression?: boolean | CompressionOptions;
+ /**
+ * @desc Enable cookie parsing via cookie-parser.
+ * @requires cookie-parser
+ * @example true
+ * @example { secret: "my-secret" }
+ */
+ cookies?: boolean | CookieParserOptions;
/**
* @desc Configure or customize the parser for request query string
* @example "simple" // for "node:querystring" module, array elements must be repeated: ?a=1&a=2
diff --git a/express-zod-api/src/cookie-middleware.ts b/express-zod-api/src/cookie-middleware.ts
new file mode 100644
index 000000000..debd4e59d
--- /dev/null
+++ b/express-zod-api/src/cookie-middleware.ts
@@ -0,0 +1,37 @@
+import { Middleware } from "./middleware";
+import type { CookieOptions } from "express";
+import type { z } from "zod";
+
+/**
+ * @desc Creates a Middleware providing cookie-setting convenience methods.
+ * @param baseOptions — Default options applied to every setCookie / clearCookie call.
+ * @example createCookieMiddleware({ httpOnly: true, secure: true, path: "/" })
+ */
+export const createCookieMiddleware = (baseOptions?: CookieOptions) =>
+ new Middleware({
+ handler: async ({ request, response }) => ({
+ /**
+ * @desc Reads a cookie value. Checks signedCookies first, then falls back to cookies.
+ * @requires cookie-parser
+ * @see ServerConfig.cookies
+ * */
+ getCookie: (name: string): z.core.util.JSONType | undefined =>
+ request.signedCookies?.[name] ?? request.cookies?.[name],
+ /** @desc Sets a cookie on the response. Express converts non-string values to JSON. */
+ setCookie: (
+ name: string,
+ value: string | z.core.util.JSONType,
+ overrides?: CookieOptions,
+ ) => {
+ response.cookie(name, value, { ...baseOptions, ...overrides });
+ },
+ /** @desc Clears a cookie on the response. */
+ clearCookie: (
+ name: string,
+ /** Express ignores certain options: expires, maxAge */
+ overrides?: Omit,
+ ) => {
+ response.clearCookie(name, { ...baseOptions, ...overrides });
+ },
+ }),
+ });
diff --git a/express-zod-api/src/documentation-helpers.ts b/express-zod-api/src/documentation-helpers.ts
index d334c6546..c2f9e0ce6 100644
--- a/express-zod-api/src/documentation-helpers.ts
+++ b/express-zod-api/src/documentation-helpers.ts
@@ -266,9 +266,9 @@ const enumerateExamples = (examples: unknown[]): ExamplesObject | undefined =>
export const defaultIsHeader = (
name: string,
- familiar?: string[],
+ familiar?: Set,
): name is `x-${string}` =>
- familiar?.includes(name) ||
+ familiar?.has(name) ||
name.startsWith("x-") ||
getWellKnownHeaders().has(name);
@@ -280,7 +280,8 @@ export const depictRequestParams = ({
makeRef,
composition,
isHeader,
- security,
+ securityHeaders,
+ securityCookies,
description = `${method.toUpperCase()} ${path} Parameter`,
}: ReqResCommons & {
composition: "inline" | "components";
@@ -288,20 +289,20 @@ export const depictRequestParams = ({
request: z.core.JSONSchema.BaseSchema;
inputSources: InputSource[];
isHeader?: IsHeader;
- security?: Alternatives;
+ securityHeaders?: Set;
+ securityCookies?: Set;
}) => {
const flat = flattenIO(request);
const pathParams = getRoutePathParams(path);
const isQueryEnabled = inputSources.includes("query");
const areParamsEnabled = inputSources.includes("params");
const areHeadersEnabled = inputSources.includes("headers");
- const securityHeaders = R.chain(
- R.filter((entry: Security) => entry.type === "header"),
- security ?? [],
- ).map(({ name }) => name);
+ const areCookiesEnabled =
+ inputSources.includes("cookies") || inputSources.includes("signedCookies");
const getLocation = (name: string) => {
if (areParamsEnabled && pathParams.includes(name)) return "path";
+ if (areCookiesEnabled && securityCookies?.has(name)) return "cookie";
if (
areHeadersEnabled &&
(isHeader?.(name, method, path) ?? defaultIsHeader(name, securityHeaders))
diff --git a/express-zod-api/src/documentation.ts b/express-zod-api/src/documentation.ts
index f1a8bb5a8..6e0fde9e4 100644
--- a/express-zod-api/src/documentation.ts
+++ b/express-zod-api/src/documentation.ts
@@ -15,6 +15,7 @@ import { getInputSources, makeCleanId } from "./common-helpers";
import type { CommonConfig } from "./config-type";
import { processContainers } from "./logical-container";
import type { ClientMethod } from "./method";
+import { getSecurityNames } from "./security";
import {
depictBody,
depictRequestParams,
@@ -202,12 +203,12 @@ export class Documentation extends OpenApiBuilder {
);
const request = depictRequest({ ...commons, schema: inputSchema });
- const security = processContainers(endpoint.security);
const depictedParams = depictRequestParams({
...commons,
inputSources,
isHeader,
- security,
+ securityHeaders: getSecurityNames(endpoint.security, "header"),
+ securityCookies: getSecurityNames(endpoint.security, "cookie"),
request,
description: descriptions?.requestParameter?.({
method,
@@ -256,7 +257,7 @@ export class Documentation extends OpenApiBuilder {
: undefined;
const securityRefs = depictSecurityRefs(
- depictSecurity(security, inputSources),
+ depictSecurity(processContainers(endpoint.security), inputSources),
scopes,
(securitySchema) => {
const name = this.#ensureUniqSecuritySchemaName(securitySchema);
diff --git a/express-zod-api/src/index.ts b/express-zod-api/src/index.ts
index 986cecf0c..21f5f627b 100644
--- a/express-zod-api/src/index.ts
+++ b/express-zod-api/src/index.ts
@@ -8,6 +8,7 @@ export { getMessageFromError } from "./common-helpers";
export { ensureHttpError } from "./result-helpers";
export { BuiltinLogger } from "./builtin-logger";
export { Middleware } from "./middleware";
+export { createCookieMiddleware } from "./cookie-middleware";
export {
ResultHandler,
defaultResultHandler,
diff --git a/express-zod-api/src/security.ts b/express-zod-api/src/security.ts
index 6bb64e549..955891f56 100644
--- a/express-zod-api/src/security.ts
+++ b/express-zod-api/src/security.ts
@@ -1,3 +1,10 @@
+import * as R from "ramda";
+import {
+ isLogicalAnd,
+ isLogicalOr,
+ type LogicalContainer,
+} from "./logical-container";
+
export interface BasicSecurity {
type: "basic";
}
@@ -93,3 +100,23 @@ export type Security =
| CookieSecurity
| OpenIdSecurity
| OAuth2Security;
+
+type NamedSecurityType = Extract["type"];
+
+const pickNames = (
+ container: LogicalContainer,
+ type: NamedSecurityType,
+): string[] => {
+ if (isLogicalAnd(container))
+ return R.chain((one) => pickNames(one, type), container.and);
+ if (isLogicalOr(container))
+ return R.chain((one) => pickNames(one, type), container.or);
+ if (container.type === type) return [container.name];
+ return [];
+};
+
+/** @desc Extract security names of a given type from logical containers without generating combinations */
+export const getSecurityNames = (
+ containers: LogicalContainer[],
+ type: NamedSecurityType,
+): Set => new Set(R.chain((one) => pickNames(one, type), containers));
diff --git a/express-zod-api/src/server-helpers.ts b/express-zod-api/src/server-helpers.ts
index 7be3b0812..e2125c2b8 100644
--- a/express-zod-api/src/server-helpers.ts
+++ b/express-zod-api/src/server-helpers.ts
@@ -1,3 +1,4 @@
+import type cookieParser from "cookie-parser";
import type fileUpload from "express-fileupload";
import { loadPeer } from "./peer-helpers";
import type { AbstractResultHandler } from "./result-handler";
@@ -81,6 +82,18 @@ export const createUploadFailureHandler =
next();
};
+export const createCookieParser = async ({
+ config,
+}: {
+ config: ServerConfig;
+}): Promise => {
+ const parser = await loadPeer("cookie-parser");
+ const { secret, ...rest } = {
+ ...(typeof config.cookies === "object" && config.cookies),
+ };
+ return parser(secret, Object.keys(rest).length ? rest : undefined);
+};
+
export const createUploadLogger = (
logger: ActualLogger,
): Pick => ({ log: logger.debug.bind(logger) });
diff --git a/express-zod-api/src/server.ts b/express-zod-api/src/server.ts
index c74ba07bf..100984bf0 100644
--- a/express-zod-api/src/server.ts
+++ b/express-zod-api/src/server.ts
@@ -14,6 +14,7 @@ import { loadPeer } from "./peer-helpers";
import { defaultResultHandler } from "./result-handler";
import { initRouting, type Parsers, type Routing } from "./routing";
import {
+ createCookieParser,
createLoggingMiddleware,
createNotFoundHandler,
createCatcher,
@@ -78,6 +79,7 @@ export const createServer = async (config: ServerConfig, routing: Routing) => {
),
);
}
+ if (config.cookies) app.use(await createCookieParser({ config }));
await config.beforeRouting?.({ app, getLogger });
const parsers: Parsers = {
diff --git a/express-zod-api/tests/__snapshots__/documentation-helpers.spec.ts.snap b/express-zod-api/tests/__snapshots__/documentation-helpers.spec.ts.snap
index 621ca0865..f3d048d0d 100644
--- a/express-zod-api/tests/__snapshots__/documentation-helpers.spec.ts.snap
+++ b/express-zod-api/tests/__snapshots__/documentation-helpers.spec.ts.snap
@@ -315,6 +315,33 @@ exports[`Documentation helpers > depictRequestParams() > Features 1180 and 2344:
]
`;
+exports[`Documentation helpers > depictRequestParams() > should depict cookie params when enabled via CookieSecurity 1`] = `
+[
+ {
+ "deprecated": undefined,
+ "description": "GET /v1/user/:id Parameter",
+ "examples": undefined,
+ "in": "cookie",
+ "name": "session",
+ "required": true,
+ "schema": {
+ "type": "string",
+ },
+ },
+ {
+ "deprecated": undefined,
+ "description": "GET /v1/user/:id Parameter",
+ "examples": undefined,
+ "in": "query",
+ "name": "page",
+ "required": true,
+ "schema": {
+ "type": "string",
+ },
+ },
+]
+`;
+
exports[`Documentation helpers > depictRequestParams() > should depict none if both query and params are disabled 1`] = `[]`;
exports[`Documentation helpers > depictRequestParams() > should depict only path params if query is disabled 1`] = `
diff --git a/express-zod-api/tests/__snapshots__/documentation.spec.ts.snap b/express-zod-api/tests/__snapshots__/documentation.spec.ts.snap
index fb1c9105f..4ae29d87a 100644
--- a/express-zod-api/tests/__snapshots__/documentation.spec.ts.snap
+++ b/express-zod-api/tests/__snapshots__/documentation.spec.ts.snap
@@ -1343,6 +1343,119 @@ servers:
"
`;
+exports[`Documentation > Basic cases > should handle CookieSecurity in params and security section 1`] = `
+"openapi: 3.1.0
+info:
+ title: Testing CookieSecurity
+ version: 3.4.5
+paths:
+ /v1/getSomething:
+ get:
+ operationId: GetV1GetSomething
+ parameters:
+ - name: session
+ in: cookie
+ required: true
+ description: GET /v1/getSomething Parameter
+ schema:
+ type: string
+ - name: page
+ in: query
+ required: true
+ description: GET /v1/getSomething Parameter
+ schema:
+ type: number
+ security:
+ - APIKEY_1: []
+ responses:
+ "200":
+ description: GET /v1/getSomething Positive response
+ content:
+ application/json:
+ schema:
+ type: object
+ properties:
+ status:
+ type: string
+ const: success
+ data:
+ type: object
+ properties: {}
+ additionalProperties: false
+ required:
+ - status
+ - data
+ additionalProperties: false
+ "400":
+ description: GET /v1/getSomething Negative response
+ content:
+ application/json:
+ schema:
+ type: object
+ properties:
+ status:
+ type: string
+ const: error
+ error:
+ type: object
+ properties:
+ message:
+ type: string
+ required:
+ - message
+ additionalProperties: false
+ required:
+ - status
+ - error
+ additionalProperties: false
+ examples:
+ example1:
+ value:
+ status: error
+ error:
+ message: Sample error message
+ head:
+ operationId: HeadV1GetSomething
+ parameters:
+ - name: session
+ in: cookie
+ required: true
+ description: HEAD /v1/getSomething Parameter
+ schema:
+ type: string
+ - name: page
+ in: query
+ required: true
+ description: HEAD /v1/getSomething Parameter
+ schema:
+ type: number
+ security:
+ - APIKEY_1: []
+ responses:
+ "200":
+ description: HEAD /v1/getSomething Positive response
+ "400":
+ description: HEAD /v1/getSomething Negative response
+components:
+ schemas: {}
+ responses: {}
+ parameters: {}
+ examples: {}
+ requestBodies: {}
+ headers: {}
+ securitySchemes:
+ APIKEY_1:
+ type: apiKey
+ in: cookie
+ name: session
+ links: {}
+ callbacks: {}
+tags: []
+servers:
+ - url: https://example.com
+"
+`;
+
exports[`Documentation > Basic cases > should handle bigint, boolean, date, null and readonly 1`] = `
"openapi: 3.1.0
info:
diff --git a/express-zod-api/tests/__snapshots__/index.spec.ts.snap b/express-zod-api/tests/__snapshots__/index.spec.ts.snap
index d4e754fd4..a60e24351 100644
--- a/express-zod-api/tests/__snapshots__/index.spec.ts.snap
+++ b/express-zod-api/tests/__snapshots__/index.spec.ts.snap
@@ -41,6 +41,8 @@ exports[`Index Entrypoint > exports > attachRouting should have certain value 1`
exports[`Index Entrypoint > exports > createConfig should have certain value 1`] = `[Function]`;
+exports[`Index Entrypoint > exports > createCookieMiddleware should have certain value 1`] = `[Function]`;
+
exports[`Index Entrypoint > exports > createServer should have certain value 1`] = `[Function]`;
exports[`Index Entrypoint > exports > defaultEndpointsFactory should have certain value 1`] = `
@@ -80,6 +82,7 @@ exports[`Index Entrypoint > exports > should have certain entities exposed 1`] =
"ensureHttpError",
"BuiltinLogger",
"Middleware",
+ "createCookieMiddleware",
"ResultHandler",
"defaultResultHandler",
"arrayResultHandler",
diff --git a/express-zod-api/tests/common-helpers.spec.ts b/express-zod-api/tests/common-helpers.spec.ts
index 98b401b08..1e3f914ec 100644
--- a/express-zod-api/tests/common-helpers.spec.ts
+++ b/express-zod-api/tests/common-helpers.spec.ts
@@ -223,6 +223,31 @@ describe("Common Helpers", () => {
"x-request-id": "test",
});
});
+
+ test.each([
+ {
+ cookies: { session: "abc", theme: "dark" },
+ },
+ {
+ signedCookies: { session: "signed-abc" },
+ },
+ {
+ cookies: { session: "unsigned", theme: "dark" },
+ signedCookies: { session: "signed" },
+ },
+ {}, // graceful handling expected
+ ])("should include cookies when enabled %#", (props) => {
+ const req = makeRequestMock({
+ method: "GET",
+ ...props,
+ });
+ expect(
+ getInput(req, { get: ["query", "cookies", "signedCookies"] }),
+ ).toEqual({
+ ...props.cookies,
+ ...props.signedCookies,
+ });
+ });
});
describe("getMessageFromError()", () => {
diff --git a/express-zod-api/tests/config-type.spec.ts b/express-zod-api/tests/config-type.spec.ts
index 377708920..2bba30907 100644
--- a/express-zod-api/tests/config-type.spec.ts
+++ b/express-zod-api/tests/config-type.spec.ts
@@ -1,5 +1,6 @@
import type { Express, IRouter } from "express";
import { createConfig } from "../src";
+import type { InputSource } from "../src/config-type";
describe("ConfigType", () => {
describe("createConfig()", () => {
@@ -39,4 +40,18 @@ describe("ConfigType", () => {
expect(config).toEqual(argument);
});
});
+
+ describe("InputSource", () => {
+ test("should list the selected properties of Request", () => {
+ expectTypeOf().toEqualTypeOf<
+ | "query"
+ | "body"
+ | "files"
+ | "params"
+ | "headers"
+ | "cookies"
+ | "signedCookies"
+ >();
+ });
+ });
});
diff --git a/express-zod-api/tests/cookie-middleware.spec.ts b/express-zod-api/tests/cookie-middleware.spec.ts
new file mode 100644
index 000000000..fcdc6b728
--- /dev/null
+++ b/express-zod-api/tests/cookie-middleware.spec.ts
@@ -0,0 +1,53 @@
+import { createCookieMiddleware, testMiddleware } from "../src";
+import { expect } from "vitest";
+
+describe("Cookie middleware", () => {
+ describe("createCookieMiddleware", () => {
+ test("should create a Middleware instance", () => {
+ const middleware = createCookieMiddleware();
+ const { constructor } = Object.getPrototypeOf(middleware);
+ expect(constructor.name).toBe("Middleware");
+ });
+
+ test.each([undefined, { httpOnly: true, secure: true, path: "/" }])(
+ "should return setCookie and clearCookie helpers %#",
+ async (baseOptions) => {
+ const { output, responseMock } = await testMiddleware({
+ middleware: createCookieMiddleware(baseOptions),
+ requestProps: {
+ cookies: { session: "asdf" },
+ signedCookies: { session: "qwerty" },
+ },
+ });
+ const { getCookie, setCookie, clearCookie } = output as Awaited<
+ ReturnType["execute"]>
+ >;
+ expect(typeof getCookie).toBe("function");
+ expect(typeof setCookie).toBe("function");
+ expect(typeof clearCookie).toBe("function");
+ expect(getCookie("missing")).toBeUndefined();
+ expect(getCookie("session")).toBe("qwerty");
+ setCookie("session", "abc123", { httpOnly: false });
+ expect(responseMock.cookies).toHaveProperty("session", {
+ options: { ...baseOptions, httpOnly: false },
+ value: "abc123",
+ });
+ setCookie("prefs", { theme: "dark", lang: "en" });
+ expect(responseMock.cookies).toHaveProperty("prefs", {
+ value: { theme: "dark", lang: "en" },
+ options: baseOptions ?? {},
+ });
+ clearCookie("session");
+ expect(responseMock.cookies).toHaveProperty("session", {
+ options: {
+ ...baseOptions,
+ path: "/",
+ expires: expect.any(Date),
+ },
+ value: "",
+ });
+ expect(Number(responseMock.cookies["session"].options.expires)).toBe(1);
+ },
+ );
+ });
+});
diff --git a/express-zod-api/tests/documentation-helpers.spec.ts b/express-zod-api/tests/documentation-helpers.spec.ts
index 28d8c8862..091b11090 100644
--- a/express-zod-api/tests/documentation-helpers.spec.ts
+++ b/express-zod-api/tests/documentation-helpers.spec.ts
@@ -368,7 +368,7 @@ describe("Documentation helpers", () => {
{ name: "authorization", expected: true },
{
name: "secure",
- familiar: ["secure"],
+ familiar: new Set(["secure"]),
expected: true,
},
{ name: "unknown", expected: false },
@@ -464,7 +464,26 @@ describe("Documentation helpers", () => {
},
inputSources: ["query", "headers", "params"],
composition: "inline",
- security: [[{ type: "header", name: "secure" }]],
+ securityHeaders: new Set(["secure"]),
+ ...requestCtx,
+ }),
+ ).toMatchSnapshot();
+ });
+
+ test("should depict cookie params when enabled via CookieSecurity", () => {
+ expect(
+ depictRequestParams({
+ request: {
+ properties: {
+ session: { type: "string" },
+ page: { type: "string" },
+ },
+ required: ["session", "page"],
+ type: "object",
+ },
+ inputSources: ["query", "cookies", "params"],
+ composition: "inline",
+ securityCookies: new Set(["session"]),
...requestCtx,
}),
).toMatchSnapshot();
diff --git a/express-zod-api/tests/documentation.spec.ts b/express-zod-api/tests/documentation.spec.ts
index 2972721ad..e96dcced9 100644
--- a/express-zod-api/tests/documentation.spec.ts
+++ b/express-zod-api/tests/documentation.spec.ts
@@ -581,6 +581,34 @@ describe("Documentation", () => {
expect(spec).toMatchSnapshot();
});
+ test("should handle CookieSecurity in params and security section", () => {
+ const mw = new Middleware({
+ security: { type: "cookie", name: "session" },
+ handler: vi.fn(),
+ });
+ const spec = new Documentation({
+ config: createConfig({
+ cors: true,
+ inputSources: { get: ["query", "cookies"] },
+ }),
+ routing: {
+ v1: {
+ getSomething: defaultEndpointsFactory.addMiddleware(mw).buildVoid({
+ input: z.object({
+ session: z.string(),
+ page: z.number(),
+ }),
+ handler: vi.fn(),
+ }),
+ },
+ },
+ version: "3.4.5",
+ title: "Testing CookieSecurity",
+ serverUrl: "https://example.com",
+ }).getSpecAsYaml();
+ expect(spec).toMatchSnapshot();
+ });
+
test("should ensure the uniq operation ids", () => {
const spec = new Documentation({
config: sampleConfig,
diff --git a/express-zod-api/tests/express-mock.ts b/express-zod-api/tests/express-mock.ts
index 1c0925853..9fa62774b 100644
--- a/express-zod-api/tests/express-mock.ts
+++ b/express-zod-api/tests/express-mock.ts
@@ -3,9 +3,11 @@ const expressRawMock = vi.fn();
const expressUrlencodedMock = vi.fn();
const compressionMock = vi.fn();
const fileUploadMock = vi.fn();
+const cookieParserMock = vi.fn();
vi.mock("compression", () => ({ default: compressionMock }));
vi.mock("express-fileupload", () => ({ default: fileUploadMock }));
+vi.mock("cookie-parser", () => ({ default: cookieParserMock }));
const staticHandler = vi.fn();
const staticMock = vi.fn(() => staticHandler);
@@ -35,6 +37,7 @@ vi.mock("express", () => ({ default: expressMock }));
export {
compressionMock,
fileUploadMock,
+ cookieParserMock,
expressMock,
appMock,
expressJsonMock,
diff --git a/express-zod-api/tests/security.spec.ts b/express-zod-api/tests/security.spec.ts
new file mode 100644
index 000000000..9a7e7f291
--- /dev/null
+++ b/express-zod-api/tests/security.spec.ts
@@ -0,0 +1,64 @@
+import { getSecurityNames } from "../src/security";
+
+describe("Security", () => {
+ describe("getSecurityNames", () => {
+ test("should return empty set for empty containers", () => {
+ expect(getSecurityNames([], "header")).toHaveProperty("size", 0);
+ });
+
+ test.each([
+ { type: "basic" as const },
+ { and: [{ type: "basic" as const }, { type: "bearer" as const }] },
+ ])(
+ "should return empty set for containers without matching type %#",
+ (container) => {
+ expect(getSecurityNames([container], "header")).toHaveProperty(
+ "size",
+ 0,
+ );
+ },
+ );
+
+ test.each([
+ { type: "header" as const, name: "test" },
+ { type: "cookie" as const, name: "test" },
+ { type: "input" as const, name: "test" },
+ ])("should extract names from $type container", (container) => {
+ expect(Array.from(getSecurityNames([container], container.type))).toEqual(
+ ["test"],
+ );
+ });
+
+ test.each([
+ {
+ and: [
+ { type: "header" as const, name: "x-auth" },
+ { type: "header" as const, name: "x-token" },
+ ],
+ },
+ {
+ or: [
+ { type: "header" as const, name: "x-auth" },
+ { type: "header" as const, name: "x-token" },
+ ],
+ },
+ {
+ and: [
+ { type: "header" as const, name: "x-auth" },
+ {
+ or: [
+ { type: "header" as const, name: "x-token" },
+ { type: "basic" as const },
+ { type: "cookie" as const, name: "session" },
+ ],
+ },
+ ],
+ },
+ ])("should extract names from nested container %#", (container) => {
+ expect(Array.from(getSecurityNames([container], "header"))).toEqual([
+ "x-auth",
+ "x-token",
+ ]);
+ });
+ });
+});
diff --git a/express-zod-api/tests/server.spec.ts b/express-zod-api/tests/server.spec.ts
index 07b80bd0a..81b35c89f 100644
--- a/express-zod-api/tests/server.spec.ts
+++ b/express-zod-api/tests/server.spec.ts
@@ -3,6 +3,7 @@ import { givePort } from "../../tools/ports";
import {
appMock,
compressionMock,
+ cookieParserMock,
expressJsonMock,
expressUrlencodedMock,
expressMock,
@@ -279,20 +280,32 @@ describe("Server", () => {
startupLogo: false,
logger: { level: "warn" },
} satisfies ServerConfig;
- const routingMock = {
- v1: {
- test: new EndpointsFactory(defaultResultHandler).build({
- output: z.object({}),
- handler: vi.fn(),
- }),
- },
- };
- await createServer(configMock, routingMock);
+ await createServer(configMock, {});
expect(appMock.use).toHaveBeenCalledTimes(3);
expect(compressionMock).toHaveBeenCalledTimes(1);
expect(compressionMock).toHaveBeenCalledWith(undefined);
});
+ test.each([true, { secret: "my-secret" }])(
+ "should enable cookie parser on demand %#",
+ async (cookies) => {
+ const configMock = {
+ http: { listen: givePort() },
+ cookies,
+ cors: true,
+ startupLogo: false,
+ logger: { level: "warn" },
+ } satisfies ServerConfig;
+ await createServer(configMock, {});
+ expect(appMock.use).toHaveBeenCalledTimes(3);
+ expect(cookieParserMock).toHaveBeenCalledTimes(1);
+ expect(cookieParserMock).toHaveBeenCalledWith(
+ typeof cookies === "object" ? cookies.secret : undefined,
+ undefined,
+ );
+ },
+ );
+
test("should enable uploads on request", async () => {
const configMock = {
http: { listen: givePort() },
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 4367b994c..a9edadbf4 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -210,6 +210,9 @@ importers:
'@types/compression':
specifier: catalog:dev
version: 1.8.1
+ '@types/cookie-parser':
+ specifier: ^1.4.10
+ version: 1.4.10(@types/express@5.0.6)
'@types/cors':
specifier: ^2.8.19
version: 2.8.19
@@ -237,6 +240,9 @@ importers:
compression:
specifier: catalog:dev
version: 1.8.1
+ cookie-parser:
+ specifier: ^1.4.7
+ version: 1.4.7
cors:
specifier: ^2.8.5
version: 2.8.6
@@ -590,6 +596,11 @@ packages:
'@types/connect@3.4.38':
resolution: {integrity: sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==}
+ '@types/cookie-parser@1.4.10':
+ resolution: {integrity: sha512-B4xqkqfZ8Wek+rCOeRxsjMS9OgvzebEzzLYw7NHYuvzb7IdxOkI0ZHGgeEBX4PUM7QGVvNSK60T3OvWj3YfBRg==}
+ peerDependencies:
+ '@types/express': '*'
+
'@types/cors@2.8.19':
resolution: {integrity: sha512-mFNylyeyqN93lfe/9CSxOGREz8cpzAhH+E93xJ4xWQf62V8sQ/24reV2nyzUWM6H6Xji+GGHpkbLe7pVoUEskg==}
@@ -915,6 +926,13 @@ packages:
convert-source-map@2.0.0:
resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==}
+ cookie-parser@1.4.7:
+ resolution: {integrity: sha512-nGUvgXnotP3BsjiLX2ypbQnWoGUPIIfHQNZkkC668ntrzGWEZVW70HDEB1qnNGMicPje6EttlIgzo51YSwNQGw==}
+ engines: {node: '>= 0.8.0'}
+
+ cookie-signature@1.0.6:
+ resolution: {integrity: sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==}
+
cookie-signature@1.2.2:
resolution: {integrity: sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==}
engines: {node: '>=6.6.0'}
@@ -2145,6 +2163,10 @@ snapshots:
dependencies:
'@types/node': 25.7.0
+ '@types/cookie-parser@1.4.10(@types/express@5.0.6)':
+ dependencies:
+ '@types/express': 5.0.6
+
'@types/cors@2.8.19':
dependencies:
'@types/node': 25.7.0
@@ -2579,6 +2601,13 @@ snapshots:
convert-source-map@2.0.0: {}
+ cookie-parser@1.4.7:
+ dependencies:
+ cookie: 0.7.2
+ cookie-signature: 1.0.6
+
+ cookie-signature@1.0.6: {}
+
cookie-signature@1.2.2: {}
cookie@0.7.2: {}