From 77da8a4ddde922cfaf2d054cda56c4b38b97838e Mon Sep 17 00:00:00 2001 From: Phillip Ho Date: Tue, 26 Nov 2024 18:15:51 +0800 Subject: [PATCH] feat: Add security headers in response (#779) * feat: Add security headers in response * change order * update cross-spawn package * fix import * wip * fix cors, remove basic auth * use const * small updates --- package.json | 6 +- src/server/index.ts | 24 +- src/server/middleware/adminRoutes.ts | 61 +-- src/server/middleware/auth.ts | 6 +- src/server/middleware/cors.ts | 95 +++++ src/server/middleware/cors/cors.ts | 366 ------------------ src/server/middleware/cors/index.ts | 16 - src/server/middleware/cors/vary.ts | 114 ------ src/server/middleware/engineMode.ts | 11 +- src/server/middleware/error.ts | 4 +- src/server/middleware/logs.ts | 14 +- .../middleware/{open-api.ts => openApi.ts} | 4 +- src/server/middleware/prometheus.ts | 13 +- src/server/middleware/rateLimit.ts | 8 +- src/server/middleware/securityHeaders.ts | 20 + src/server/middleware/websocket.ts | 14 +- src/server/routes/configuration/cors/set.ts | 4 +- src/server/routes/index.ts | 4 +- src/server/schemas/address.ts | 2 +- src/server/schemas/wallet/index.ts | 2 + src/tests/cors.test.ts | 25 -- src/tests/schema.test.ts | 5 +- src/utils/usage.ts | 6 +- test/e2e/tests/sign-transaction.test.ts | 18 +- yarn.lock | 61 +-- 25 files changed, 235 insertions(+), 668 deletions(-) create mode 100644 src/server/middleware/cors.ts delete mode 100644 src/server/middleware/cors/cors.ts delete mode 100644 src/server/middleware/cors/index.ts delete mode 100644 src/server/middleware/cors/vary.ts rename src/server/middleware/{open-api.ts => openApi.ts} (93%) create mode 100644 src/server/middleware/securityHeaders.ts delete mode 100644 src/tests/cors.test.ts diff --git a/package.json b/package.json index 6d156ac43..ffc1e895c 100644 --- a/package.json +++ b/package.json @@ -32,7 +32,6 @@ "@cloud-cryptographic-wallet/cloud-kms-signer": "^0.1.2", "@cloud-cryptographic-wallet/signer": "^0.0.5", "@ethersproject/json-wallets": "^5.7.0", - "@fastify/basic-auth": "^5.1.1", "@fastify/swagger": "^8.9.0", "@fastify/type-provider-typebox": "^3.2.0", "@fastify/websocket": "^8.2.0", @@ -67,7 +66,6 @@ "pg": "^8.11.3", "prisma": "^5.14.0", "prom-client": "^15.1.3", - "prool": "^0.0.16", "superjson": "^2.2.1", "thirdweb": "5.61.3", "uuid": "^9.0.1", @@ -91,6 +89,7 @@ "eslint-config-prettier": "^8.7.0", "openapi-typescript-codegen": "^0.25.0", "prettier": "^2.8.7", + "prool": "^0.0.16", "typescript": "^5.1.3", "vitest": "^2.0.3" }, @@ -112,6 +111,7 @@ "elliptic": ">=6.6.0", "micromatch": ">=4.0.8", "secp256k1": ">=4.0.4", - "ws": ">=8.17.1" + "ws": ">=8.17.1", + "cross-spawn": ">=7.0.6" } } diff --git a/src/server/index.ts b/src/server/index.ts index 66d7f2e10..8686aa63c 100644 --- a/src/server/index.ts +++ b/src/server/index.ts @@ -3,6 +3,7 @@ import fastify, { type FastifyInstance } from "fastify"; import * as fs from "node:fs"; import path from "node:path"; import { URL } from "node:url"; +import { getConfig } from "../utils/cache/getConfig"; import { clearCacheCron } from "../utils/cron/clearCacheCron"; import { env } from "../utils/env"; import { logger } from "../utils/logger"; @@ -15,9 +16,10 @@ import { withCors } from "./middleware/cors"; import { withEnforceEngineMode } from "./middleware/engineMode"; import { withErrorHandler } from "./middleware/error"; import { withRequestLogs } from "./middleware/logs"; -import { withOpenApi } from "./middleware/open-api"; +import { withOpenApi } from "./middleware/openApi"; import { withPrometheus } from "./middleware/prometheus"; import { withRateLimit } from "./middleware/rateLimit"; +import { withSecurityHeaders } from "./middleware/securityHeaders"; import { withWebSocket } from "./middleware/websocket"; import { withRoutes } from "./routes"; import { writeOpenApiToFile } from "./utils/openapi"; @@ -69,19 +71,23 @@ export const initServer = async () => { ...(env.ENABLE_HTTPS ? httpsObject : {}), }).withTypeProvider(); - server.decorateRequest("corsPreflightEnabled", false); + const config = await getConfig(); - await withCors(server); - await withRequestLogs(server); - await withPrometheus(server); - await withErrorHandler(server); - await withEnforceEngineMode(server); - await withRateLimit(server); + // Configure middleware + withErrorHandler(server); + withRequestLogs(server); + withSecurityHeaders(server); + withCors(server, config); + withRateLimit(server); + withEnforceEngineMode(server); + withServerUsageReporting(server); + withPrometheus(server); + + // Register routes await withWebSocket(server); await withAuth(server); await withOpenApi(server); await withRoutes(server); - await withServerUsageReporting(server); await withAdminRoutes(server); await server.ready(); diff --git a/src/server/middleware/adminRoutes.ts b/src/server/middleware/adminRoutes.ts index 76719a137..1dac982df 100644 --- a/src/server/middleware/adminRoutes.ts +++ b/src/server/middleware/adminRoutes.ts @@ -1,11 +1,10 @@ import { createBullBoard } from "@bull-board/api"; import { BullMQAdapter } from "@bull-board/api/bullMQAdapter"; import { FastifyAdapter } from "@bull-board/fastify"; -import fastifyBasicAuth from "@fastify/basic-auth"; import type { Queue } from "bullmq"; -import { timingSafeEqual } from "crypto"; import type { FastifyInstance } from "fastify"; import { StatusCodes } from "http-status-codes"; +import { timingSafeEqual } from "node:crypto"; import { env } from "../../utils/env"; import { CancelRecycledNoncesQueue } from "../../worker/queues/cancelRecycledNoncesQueue"; import { MigratePostgresTransactionsQueue } from "../../worker/queues/migratePostgresTransactionsQueue"; @@ -19,7 +18,9 @@ import { SendTransactionQueue } from "../../worker/queues/sendTransactionQueue"; import { SendWebhookQueue } from "../../worker/queues/sendWebhookQueue"; export const ADMIN_QUEUES_BASEPATH = "/admin/queues"; +const ADMIN_ROUTES_USERNAME = "admin"; const ADMIN_ROUTES_PASSWORD = env.THIRDWEB_API_SECRET_KEY; + // Add queues to monitor here. const QUEUES: Queue[] = [ SendWebhookQueue.q, @@ -35,21 +36,8 @@ const QUEUES: Queue[] = [ ]; export const withAdminRoutes = async (fastify: FastifyInstance) => { - // Configure basic auth. - await fastify.register(fastifyBasicAuth, { - validate: (username, password, req, reply, done) => { - if (assertAdminBasicAuth(username, password)) { - done(); - return; - } - done(new Error("Unauthorized")); - }, - authenticate: true, - }); - - // Set up routes after Fastify is set up. fastify.after(async () => { - // Register bullboard UI. + // Create a new route for Bullboard routes. const serverAdapter = new FastifyAdapter(); serverAdapter.setBasePath(ADMIN_QUEUES_BASEPATH); @@ -57,35 +45,50 @@ export const withAdminRoutes = async (fastify: FastifyInstance) => { queues: QUEUES.map((q) => new BullMQAdapter(q)), serverAdapter, }); + await fastify.register(serverAdapter.registerPlugin(), { basePath: ADMIN_QUEUES_BASEPATH, prefix: ADMIN_QUEUES_BASEPATH, }); - // Apply basic auth only to admin routes. - fastify.addHook("onRequest", (req, reply, done) => { + fastify.addHook("onRequest", async (req, reply) => { if (req.url.startsWith(ADMIN_QUEUES_BASEPATH)) { - fastify.basicAuth(req, reply, (error) => { - if (error) { - reply - .status(StatusCodes.UNAUTHORIZED) - .send({ error: "Unauthorized" }); - return done(error); - } - }); + const authHeader = req.headers.authorization; + + if (!authHeader || !authHeader.startsWith("Basic ")) { + reply + .status(StatusCodes.UNAUTHORIZED) + .header("WWW-Authenticate", 'Basic realm="Admin Routes"') + .send({ error: "Unauthorized" }); + return; + } + + // Parse the basic auth credentials (`Basic `). + const base64Credentials = authHeader.split(" ")[1]; + const credentials = Buffer.from(base64Credentials, "base64").toString( + "utf8", + ); + const [username, password] = credentials.split(":"); + + if (!assertAdminBasicAuth(username, password)) { + reply + .status(StatusCodes.UNAUTHORIZED) + .header("WWW-Authenticate", 'Basic realm="Admin Routes"') + .send({ error: "Unauthorized" }); + return; + } } - done(); }); }); }; const assertAdminBasicAuth = (username: string, password: string) => { - if (username === "admin") { + if (username === ADMIN_ROUTES_USERNAME) { try { const buf1 = Buffer.from(password.padEnd(100)); const buf2 = Buffer.from(ADMIN_ROUTES_PASSWORD.padEnd(100)); return timingSafeEqual(buf1, buf2); - } catch (e) {} + } catch {} } return false; }; diff --git a/src/server/middleware/auth.ts b/src/server/middleware/auth.ts index 539d36b12..19883aa98 100644 --- a/src/server/middleware/auth.ts +++ b/src/server/middleware/auth.ts @@ -25,7 +25,7 @@ import { logger } from "../../utils/logger"; import { sendWebhookRequest } from "../../utils/webhook"; import { Permission } from "../schemas/auth"; import { ADMIN_QUEUES_BASEPATH } from "./adminRoutes"; -import { OPENAPI_ROUTES } from "./open-api"; +import { OPENAPI_ROUTES } from "./openApi"; export type TAuthData = never; export type TAuthSession = { permissions: string }; @@ -43,7 +43,7 @@ declare module "fastify" { } } -export const withAuth = async (server: FastifyInstance) => { +export async function withAuth(server: FastifyInstance) { const config = await getConfig(); // Configure the ThirdwebAuth fastify plugin @@ -140,7 +140,7 @@ export const withAuth = async (server: FastifyInstance) => { message, }); }); -}; +} export const onRequest = async ({ req, diff --git a/src/server/middleware/cors.ts b/src/server/middleware/cors.ts new file mode 100644 index 000000000..be1e6eca6 --- /dev/null +++ b/src/server/middleware/cors.ts @@ -0,0 +1,95 @@ +import type { FastifyInstance } from "fastify"; +import type { ParsedConfig } from "../../schema/config"; +import { ADMIN_QUEUES_BASEPATH } from "./adminRoutes"; + +const STANDARD_METHODS = "GET,POST,DELETE,PUT,PATCH,HEAD,PUT,PATCH,POST,DELETE"; +const DEFAULT_ALLOWED_HEADERS = [ + "Authorization", + "Content-Type", + "ngrok-skip-browser-warning", +]; + +export function withCors(server: FastifyInstance, config: ParsedConfig) { + server.addHook("onRequest", async (request, reply) => { + const origin = request.headers.origin; + + // Allow backend calls (no origin header). + if (!origin) { + return; + } + + // Allow admin routes to be accessed from the same host. + if (request.url.startsWith(ADMIN_QUEUES_BASEPATH)) { + const host = request.headers.host; + const originHost = new URL(origin).host; + if (originHost !== host) { + reply.code(403).send({ error: "Invalid origin" }); + return; + } + return; + } + + const allowedOrigins = config.accessControlAllowOrigin + .split(",") + .map(sanitizeOrigin); + + // Always set `Vary: Origin` to prevent caching issues even on invalid origins. + reply.header("Vary", "Origin"); + + if (isAllowedOrigin(origin, allowedOrigins)) { + // Set CORS headers if valid origin. + reply.header("Access-Control-Allow-Origin", origin); + reply.header("Access-Control-Allow-Methods", STANDARD_METHODS); + + // Handle preflight requests + if (request.method === "OPTIONS") { + const requestedHeaders = + request.headers["access-control-request-headers"]; + reply.header( + "Access-Control-Allow-Headers", + requestedHeaders ?? DEFAULT_ALLOWED_HEADERS.join(","), + ); + + reply.header("Cache-Control", "public, max-age=3600"); + reply.header("Access-Control-Max-Age", "3600"); + reply.code(204).send(); + return; + } + } else { + reply.code(403).send({ error: "Invalid origin" }); + return; + } + }); +} + +function isAllowedOrigin(origin: string, allowedOrigins: string[]) { + return ( + allowedOrigins + // Check if the origin matches any allowed origins. + .some((allowed) => { + if (allowed === "https://thirdweb-preview.com") { + return /^https?:\/\/.*\.thirdweb-preview\.com$/.test(origin); + } + if (allowed === "https://thirdweb-dev.com") { + return /^https?:\/\/.*\.thirdweb-dev\.com$/.test(origin); + } + + // Allow wildcards in the origin. For example "foo.example.com" matches "*.example.com" + if (allowed.includes("*")) { + const wildcardPattern = allowed.replace(/\*/g, ".*"); + const regex = new RegExp(`^${wildcardPattern}$`); + return regex.test(origin); + } + + // Otherwise check for an exact match. + return origin === allowed; + }) + ); +} + +function sanitizeOrigin(origin: string) { + if (origin.endsWith("/")) { + return origin.slice(0, -1); + } + return origin; +} diff --git a/src/server/middleware/cors/cors.ts b/src/server/middleware/cors/cors.ts deleted file mode 100644 index e56238642..000000000 --- a/src/server/middleware/cors/cors.ts +++ /dev/null @@ -1,366 +0,0 @@ -import { - FastifyInstance, - FastifyReply, - FastifyRequest, - HookHandlerDoneFunction, -} from "fastify"; -import { getConfig } from "../../../utils/cache/getConfig"; -import { - addAccessControlRequestHeadersToVaryHeader, - addOriginToVaryHeader, -} from "./vary"; - -declare module "fastify" { - interface FastifyRequest { - corsPreflightEnabled: boolean; - } -} - -interface ArrayOfValueOrArray extends Array> {} - -type OriginCallback = ( - err: Error | null, - origin: ValueOrArray, -) => void; -type OriginType = string | boolean | RegExp; -type ValueOrArray = T | ArrayOfValueOrArray; -type OriginFunction = ( - origin: string | undefined, - callback: OriginCallback, -) => void; - -interface FastifyCorsOptions { - /** - * Configures the Access-Control-Allow-Origin CORS header. - */ - origin?: ValueOrArray | OriginFunction; - /** - * Configures the Access-Control-Allow-Credentials CORS header. - * Set to true to pass the header, otherwise it is omitted. - */ - credentials?: boolean; - /** - * Configures the Access-Control-Expose-Headers CORS header. - * Expects a comma-delimited string (ex: 'Content-Range,X-Content-Range') - * or an array (ex: ['Content-Range', 'X-Content-Range']). - * If not specified, no custom headers are exposed. - */ - exposedHeaders?: string | string[]; - /** - * Configures the Access-Control-Allow-Headers CORS header. - * Expects a comma-delimited string (ex: 'Content-Type,Authorization') - * or an array (ex: ['Content-Type', 'Authorization']). If not - * specified, defaults to reflecting the headers specified in the - * request's Access-Control-Request-Headers header. - */ - allowedHeaders?: string | string[]; - /** - * Configures the Access-Control-Allow-Methods CORS header. - * Expects a comma-delimited string (ex: 'GET,PUT,POST') or an array (ex: ['GET', 'PUT', 'POST']). - */ - methods?: string | string[]; - /** - * Configures the Access-Control-Max-Age CORS header. - * Set to an integer to pass the header, otherwise it is omitted. - */ - maxAge?: number; - /** - * Configures the Cache-Control header for CORS preflight responses. - * Set to an integer to pass the header as `Cache-Control: max-age=${cacheControl}`, - * or set to a string to pass the header as `Cache-Control: ${cacheControl}` (fully define - * the header value), otherwise the header is omitted. - */ - cacheControl?: number | string | null; - /** - * Pass the CORS preflight response to the route handler (default: false). - */ - preflightContinue?: boolean; - /** - * Provides a status code to use for successful OPTIONS requests, - * since some legacy browsers (IE11, various SmartTVs) choke on 204. - */ - optionsSuccessStatus?: number; - /** - * Pass the CORS preflight response to the route handler (default: false). - */ - preflight?: boolean; - /** - * Enforces strict requirement of the CORS preflight request headers (Access-Control-Request-Method and Origin). - * Preflight requests without the required headers will result in 400 errors when set to `true` (default: `true`). - */ - strictPreflight?: boolean; - /** - * Hide options route from the documentation built using fastify-swagger (default: true). - */ - hideOptionsRoute?: boolean; -} - -const defaultOptions = { - origin: "*", - methods: "GET,HEAD,PUT,PATCH,POST,DELETE", - preflightContinue: false, - optionsSuccessStatus: 204, - credentials: false, - exposedHeaders: undefined, - allowedHeaders: undefined, - maxAge: undefined, - preflight: true, - strictPreflight: true, -}; - -export const sanitizeOrigin = (data: string): string | RegExp => { - if (data.startsWith("/") && data.endsWith("/")) { - return new RegExp(data.slice(1, -1)); - } - - if (data.startsWith("*.")) { - const regex = data.replace("*.", ".*."); - return new RegExp(regex); - } - - if (data.includes("thirdweb-preview.com")) { - return new RegExp(/^https?:\/\/.*\.thirdweb-preview\.com$/); - } - if (data.includes("thirdweb-dev.com")) { - return new RegExp(/^https?:\/\/.*\.thirdweb-dev\.com$/); - } - - // Remove trailing slashes. - // The origin header does not include a trailing slash. - if (data.endsWith("/")) { - return data.slice(0, -1); - } - - return data; -}; - -export const fastifyCors = async ( - fastify: FastifyInstance, - req: FastifyRequest, - reply: FastifyReply, - opts: FastifyCorsOptions, - next: HookHandlerDoneFunction, -) => { - const config = await getConfig(); - - const corsListConfig = config.accessControlAllowOrigin.split(","); - /** - * @deprecated - * Future versions will remove this env var. - */ - const corsListEnv = process.env.ACCESS_CONTROL_ALLOW_ORIGIN?.split(",") ?? []; - const originArray = [...corsListConfig, ...corsListEnv]; - - opts.origin = originArray.map(sanitizeOrigin); - - let hideOptionsRoute = true; - if (opts.hideOptionsRoute !== undefined) { - hideOptionsRoute = opts.hideOptionsRoute; - } - const corsOptions = normalizeCorsOptions(opts); - addCorsHeadersHandler(fastify, corsOptions, req, reply, next); - - next(); -}; - -const normalizeCorsOptions = (opts: FastifyCorsOptions): FastifyCorsOptions => { - const corsOptions = { ...defaultOptions, ...opts }; - if (Array.isArray(opts.origin) && opts.origin.indexOf("*") !== -1) { - corsOptions.origin = "*"; - } - if (Number.isInteger(corsOptions.cacheControl)) { - // integer numbers are formatted this way - corsOptions.cacheControl = `max-age=${corsOptions.cacheControl}`; - } else if (typeof corsOptions.cacheControl !== "string") { - // strings are applied directly and any other value is ignored - corsOptions.cacheControl = undefined; - } - return corsOptions; -}; - -const addCorsHeadersHandler = ( - fastify: FastifyInstance, - options: FastifyCorsOptions, - req: FastifyRequest, - reply: FastifyReply, - next: HookHandlerDoneFunction, -) => { - // Always set Vary header - // https://github.com/rs/cors/issues/10 - addOriginToVaryHeader(reply); - - const resolveOriginOption = - typeof options.origin === "function" - ? resolveOriginWrapper(fastify, options.origin) - : (_: any, cb: any) => cb(null, options.origin); - - resolveOriginOption( - req, - (error: Error | null, resolvedOriginOption: boolean) => { - if (error !== null) { - return next(error); - } - - // Disable CORS and preflight if false - if (resolvedOriginOption === false) { - return next(); - } - - // Falsy values are invalid - if (!resolvedOriginOption) { - return next(new Error("Invalid CORS origin option")); - } - - addCorsHeaders(req, reply, resolvedOriginOption, options); - - if (req.raw.method === "OPTIONS" && options.preflight === true) { - // Strict mode enforces the required headers for preflight - if ( - options.strictPreflight === true && - (!req.headers.origin || !req.headers["access-control-request-method"]) - ) { - reply - .status(400) - .type("text/plain") - .send("Invalid Preflight Request"); - return; - } - - req.corsPreflightEnabled = true; - - addPreflightHeaders(req, reply, options); - - if (!options.preflightContinue) { - // Do not call the hook callback and terminate the request - // Safari (and potentially other browsers) need content-length 0, - // for 204 or they just hang waiting for a body - reply - .code(options.optionsSuccessStatus!) - .header("Content-Length", "0") - .send(); - return; - } - } - - return; - }, - ); -}; - -const addCorsHeaders = ( - req: FastifyRequest, - reply: FastifyReply, - originOption: any, - corsOptions: FastifyCorsOptions, -) => { - const origin = getAccessControlAllowOriginHeader( - req.headers.origin!, - originOption, - ); - - // In the case of origin not allowed the header is not - // written in the response. - // https://github.com/fastify/fastify-cors/issues/127 - if (origin) { - reply.header("Access-Control-Allow-Origin", origin); - } - - if (corsOptions.credentials) { - reply.header("Access-Control-Allow-Credentials", "true"); - } - - if (corsOptions.exposedHeaders !== null) { - reply.header( - "Access-Control-Expose-Headers", - Array.isArray(corsOptions.exposedHeaders) - ? corsOptions.exposedHeaders.join(", ") - : corsOptions.exposedHeaders, - ); - } -}; - -function addPreflightHeaders( - req: FastifyRequest, - reply: FastifyReply, - corsOptions: FastifyCorsOptions, -) { - reply.header( - "Access-Control-Allow-Methods", - Array.isArray(corsOptions.methods) - ? corsOptions.methods.join(", ") - : corsOptions.methods, - ); - - if (!corsOptions.allowedHeaders) { - addAccessControlRequestHeadersToVaryHeader(reply); - const reqAllowedHeaders = req.headers["access-control-request-headers"]; - if (reqAllowedHeaders !== undefined) { - reply.header("Access-Control-Allow-Headers", reqAllowedHeaders); - } - } else { - reply.header( - "Access-Control-Allow-Headers", - Array.isArray(corsOptions.allowedHeaders) - ? corsOptions.allowedHeaders.join(", ") - : corsOptions.allowedHeaders, - ); - } - - if (corsOptions.maxAge !== null) { - reply.header("Access-Control-Max-Age", String(corsOptions.maxAge)); - } - - if (corsOptions.cacheControl) { - reply.header("Cache-Control", corsOptions.cacheControl); - } -} - -const resolveOriginWrapper = (fastify: FastifyInstance, origin: any) => { - return (req: FastifyRequest, cb: any) => { - const result = origin.call(fastify, req.headers.origin, cb); - - // Allow for promises - if (result && typeof result.then === "function") { - result.then((res: any) => cb(null, res), cb); - } - }; -}; - -const getAccessControlAllowOriginHeader = ( - reqOrigin: string | undefined, - originOption: string, -) => { - if (originOption === "*") { - // allow any origin - return "*"; - } - - if (typeof originOption === "string") { - // fixed origin - return originOption; - } - - // reflect origin - return isRequestOriginAllowed(reqOrigin, originOption) ? reqOrigin : false; -}; - -const isRequestOriginAllowed = ( - reqOrigin: string | undefined, - allowedOrigin: string | RegExp, -) => { - if (Array.isArray(allowedOrigin)) { - for (let i = 0; i < allowedOrigin.length; ++i) { - if (isRequestOriginAllowed(reqOrigin, allowedOrigin[i])) { - return true; - } - } - return false; - } else if (typeof allowedOrigin === "string") { - return reqOrigin === allowedOrigin; - } else if (allowedOrigin instanceof RegExp && reqOrigin) { - allowedOrigin.lastIndex = 0; - return allowedOrigin.test(reqOrigin); - } else { - return !!allowedOrigin; - } -}; diff --git a/src/server/middleware/cors/index.ts b/src/server/middleware/cors/index.ts deleted file mode 100644 index e964945d9..000000000 --- a/src/server/middleware/cors/index.ts +++ /dev/null @@ -1,16 +0,0 @@ -import { FastifyInstance } from "fastify"; -import { fastifyCors } from "./cors"; - -export const withCors = async (server: FastifyInstance) => { - server.addHook("onRequest", (request, reply, next) => { - fastifyCors( - server, - request, - reply, - { - credentials: true, - }, - next, - ); - }); -}; diff --git a/src/server/middleware/cors/vary.ts b/src/server/middleware/cors/vary.ts deleted file mode 100644 index 2a4423138..000000000 --- a/src/server/middleware/cors/vary.ts +++ /dev/null @@ -1,114 +0,0 @@ -import type { FastifyReply } from "fastify"; -import LRUCache from "mnemonist/lru-cache"; - -/** - * Field Value Components - * Most HTTP header field values are defined using common syntax - * components (token, quoted-string, and comment) separated by - * whitespace or specific delimiting characters. Delimiters are chosen - * from the set of US-ASCII visual characters not allowed in a token - * (DQUOTE and "(),/:;<=>?@[\]{}"). - * - * field-name = token - * token = 1*tchar - * tchar = "!" / "#" / "$" / "%" / "&" / "'" / "*" - * / "+" / "-" / "." / "^" / "_" / "`" / "|" / "~" - * / DIGIT / ALPHA - * ; any VCHAR, except delimiters - * - * @see https://datatracker.ietf.org/doc/html/rfc7230#section-3.2.6 - */ - -const validFieldnameRE = /^[!#$%&'*+\-.^\w`|~]+$/u; -function validateFieldname(fieldname: string) { - if (validFieldnameRE.test(fieldname) === false) { - throw new TypeError("Fieldname contains invalid characters."); - } -} - -export const parse = (header: string) => { - header = header.trim().toLowerCase(); - const result = []; - - if (header.length === 0) { - // pass through - } else if (header.indexOf(",") === -1) { - result.push(header); - } else { - const il = header.length; - let i = 0; - let pos = 0; - let char; - - // tokenize the header - for (i = 0; i < il; ++i) { - char = header[i]; - // when we have whitespace set the pos to the next position - if (char === " ") { - pos = i + 1; - // `,` is the separator of vary-values - } else if (char === ",") { - // if pos and current position are not the same we have a valid token - if (pos !== i) { - result.push(header.slice(pos, i)); - } - // reset the positions - pos = i + 1; - } - } - - if (pos !== i) { - result.push(header.slice(pos, i)); - } - } - - return result; -}; - -function createAddFieldnameToVary(fieldname: string) { - const headerCache = new LRUCache(1000); - - validateFieldname(fieldname); - - return function (reply: FastifyReply) { - let header = reply.getHeader("Vary") as any; - - if (!header) { - reply.header("Vary", fieldname); - return; - } - - if (header === "*") { - return; - } - - if (fieldname === "*") { - reply.header("Vary", "*"); - return; - } - - if (Array.isArray(header)) { - header = header.join(", "); - } - - if (!headerCache.has(header)) { - const vals = parse(header); - - if (vals.indexOf("*") !== -1) { - headerCache.set(header, "*"); - } else if (vals.indexOf(fieldname.toLowerCase()) === -1) { - headerCache.set(header, header + ", " + fieldname); - } else { - headerCache.set(header, null); - } - } - const cached = headerCache.get(header); - if (cached !== null) { - reply.header("Vary", cached); - } - }; -} - -export const addOriginToVaryHeader = createAddFieldnameToVary("Origin"); -export const addAccessControlRequestHeadersToVaryHeader = - createAddFieldnameToVary("Access-Control-Request-Headers"); diff --git a/src/server/middleware/engineMode.ts b/src/server/middleware/engineMode.ts index 790cd1526..c111c5fdd 100644 --- a/src/server/middleware/engineMode.ts +++ b/src/server/middleware/engineMode.ts @@ -1,16 +1,17 @@ -import { FastifyInstance } from "fastify"; +import type { FastifyInstance } from "fastify"; import { env } from "../../utils/env"; -export const withEnforceEngineMode = async (server: FastifyInstance) => { +export function withEnforceEngineMode(server: FastifyInstance) { if (env.ENGINE_MODE === "sandbox") { server.addHook("onRequest", async (request, reply) => { if (request.method !== "GET") { return reply.status(405).send({ statusCode: 405, - error: "Read Only Mode. Method Not Allowed", - message: "Read Only Mode. Method Not Allowed", + error: "Engine is in read-only mode. Only GET requests are allowed.", + message: + "Engine is in read-only mode. Only GET requests are allowed.", }); } }); } -}; +} diff --git a/src/server/middleware/error.ts b/src/server/middleware/error.ts index 1db10c075..5b321cbfd 100644 --- a/src/server/middleware/error.ts +++ b/src/server/middleware/error.ts @@ -51,7 +51,7 @@ const isZodError = (err: unknown): boolean => { ); }; -export const withErrorHandler = async (server: FastifyInstance) => { +export function withErrorHandler(server: FastifyInstance) { server.setErrorHandler( (error: string | Error | CustomError | ZodError, request, reply) => { if (typeof error === "string") { @@ -133,4 +133,4 @@ export const withErrorHandler = async (server: FastifyInstance) => { }); }, ); -}; +} diff --git a/src/server/middleware/logs.ts b/src/server/middleware/logs.ts index 4c719547d..e5ed3a6b5 100644 --- a/src/server/middleware/logs.ts +++ b/src/server/middleware/logs.ts @@ -2,7 +2,7 @@ import type { FastifyInstance } from "fastify"; import { stringify } from "thirdweb/utils"; import { logger } from "../../utils/logger"; import { ADMIN_QUEUES_BASEPATH } from "./adminRoutes"; -import { OPENAPI_ROUTES } from "./open-api"; +import { OPENAPI_ROUTES } from "./openApi"; const SKIP_LOG_PATHS = new Set([ "", @@ -16,16 +16,15 @@ const SKIP_LOG_PATHS = new Set([ "/configuration/wallets", ]); -export const withRequestLogs = async (server: FastifyInstance) => { - server.addHook("onSend", (request, reply, payload, done) => { +export function withRequestLogs(server: FastifyInstance) { + server.addHook("onSend", async (request, reply, payload) => { if ( request.method === "OPTIONS" || !request.routeOptions.url || SKIP_LOG_PATHS.has(request.routeOptions.url) || request.routeOptions.url.startsWith(ADMIN_QUEUES_BASEPATH) ) { - done(); - return; + return payload; } const { method, routeOptions, headers, params, query, body } = request; @@ -37,6 +36,7 @@ export const withRequestLogs = async (server: FastifyInstance) => { "x-idempotency-key": headers["x-idempotency-key"], "x-account-address": headers["x-account-address"], "x-account-factory-address": headers["x-account-factory-address"], + "x-account-salt": headers["x-account-salt"], }; const paramsStr = @@ -67,6 +67,6 @@ export const withRequestLogs = async (server: FastifyInstance) => { ].join(" "), }); - done(); + return payload; }); -}; +} diff --git a/src/server/middleware/open-api.ts b/src/server/middleware/openApi.ts similarity index 93% rename from src/server/middleware/open-api.ts rename to src/server/middleware/openApi.ts index 2a4b53883..6a995d221 100644 --- a/src/server/middleware/open-api.ts +++ b/src/server/middleware/openApi.ts @@ -3,7 +3,7 @@ import type { FastifyInstance } from "fastify"; export const OPENAPI_ROUTES = ["/json", "/openapi.json", "/json/"]; -export const withOpenApi = async (server: FastifyInstance) => { +export async function withOpenApi(server: FastifyInstance) { await server.register(swagger, { openapi: { info: { @@ -39,4 +39,4 @@ export const withOpenApi = async (server: FastifyInstance) => { res.send(server.swagger()); }); } -}; +} diff --git a/src/server/middleware/prometheus.ts b/src/server/middleware/prometheus.ts index 2b0faaf24..64bae9d49 100644 --- a/src/server/middleware/prometheus.ts +++ b/src/server/middleware/prometheus.ts @@ -1,21 +1,20 @@ -import { FastifyInstance, FastifyReply, FastifyRequest } from "fastify"; +import type { FastifyInstance, FastifyReply, FastifyRequest } from "fastify"; import { env } from "../../utils/env"; import { recordMetrics } from "../../utils/prometheus"; -export const withPrometheus = async (server: FastifyInstance) => { +export function withPrometheus(server: FastifyInstance) { if (!env.METRICS_ENABLED) { return; } server.addHook( "onResponse", - (req: FastifyRequest, res: FastifyReply, done) => { + async (req: FastifyRequest, res: FastifyReply) => { const { method } = req; const url = req.routeOptions.url; const { statusCode } = res; - const duration = res.elapsedTime; // Use Fastify's built-in timing + const duration = res.elapsedTime; - // Record metrics recordMetrics({ event: "response_sent", params: { @@ -25,8 +24,6 @@ export const withPrometheus = async (server: FastifyInstance) => { method: method, }, }); - - done(); }, ); -}; +} diff --git a/src/server/middleware/rateLimit.ts b/src/server/middleware/rateLimit.ts index 5d87c9a11..97c3f72cd 100644 --- a/src/server/middleware/rateLimit.ts +++ b/src/server/middleware/rateLimit.ts @@ -3,12 +3,12 @@ import { StatusCodes } from "http-status-codes"; import { env } from "../../utils/env"; import { redis } from "../../utils/redis/redis"; import { createCustomError } from "./error"; -import { OPENAPI_ROUTES } from "./open-api"; +import { OPENAPI_ROUTES } from "./openApi"; const SKIP_RATELIMIT_PATHS = ["/", ...OPENAPI_ROUTES]; -export const withRateLimit = async (server: FastifyInstance) => { - server.addHook("onRequest", async (request, reply) => { +export function withRateLimit(server: FastifyInstance) { + server.addHook("onRequest", async (request, _reply) => { if (SKIP_RATELIMIT_PATHS.includes(request.url)) { return; } @@ -26,4 +26,4 @@ export const withRateLimit = async (server: FastifyInstance) => { ); } }); -}; +} diff --git a/src/server/middleware/securityHeaders.ts b/src/server/middleware/securityHeaders.ts new file mode 100644 index 000000000..5096d4811 --- /dev/null +++ b/src/server/middleware/securityHeaders.ts @@ -0,0 +1,20 @@ +import type { FastifyInstance } from "fastify"; + +export function withSecurityHeaders(server: FastifyInstance) { + server.addHook("onSend", async (_request, reply, payload) => { + reply.header( + "Strict-Transport-Security", + "max-age=31536000; includeSubDomains; preload", + ); + reply.header("Content-Security-Policy", "default-src 'self';"); + reply.header("X-Frame-Options", "DENY"); + reply.header("X-Content-Type-Options", "nosniff"); + reply.header("Referrer-Policy", "no-referrer"); + reply.header( + "Permissions-Policy", + "geolocation=(), camera=(), microphone=()", + ); + + return payload; + }); +} diff --git a/src/server/middleware/websocket.ts b/src/server/middleware/websocket.ts index 2f68cf2f2..6c7ebceef 100644 --- a/src/server/middleware/websocket.ts +++ b/src/server/middleware/websocket.ts @@ -1,15 +1,15 @@ import WebSocketPlugin from "@fastify/websocket"; -import { FastifyInstance } from "fastify"; +import type { FastifyInstance } from "fastify"; import { logger } from "../../utils/logger"; -export const withWebSocket = async (server: FastifyInstance) => { +export async function withWebSocket(server: FastifyInstance) { await server.register(WebSocketPlugin, { - errorHandler: function ( + errorHandler: ( error, conn /* SocketStream */, - req /* FastifyRequest */, - reply /* FastifyReply */, - ) { + _req /* FastifyRequest */, + _reply /* FastifyReply */, + ) => { logger({ service: "websocket", level: "error", @@ -20,4 +20,4 @@ export const withWebSocket = async (server: FastifyInstance) => { conn.destroy(error); }, }); -}; +} diff --git a/src/server/routes/configuration/cors/set.ts b/src/server/routes/configuration/cors/set.ts index 30a0c6946..54ddf3e84 100644 --- a/src/server/routes/configuration/cors/set.ts +++ b/src/server/routes/configuration/cors/set.ts @@ -1,5 +1,5 @@ -import { Static, Type } from "@sinclair/typebox"; -import { FastifyInstance } from "fastify"; +import { Type, type Static } from "@sinclair/typebox"; +import type { FastifyInstance } from "fastify"; import { StatusCodes } from "http-status-codes"; import { updateConfiguration } from "../../../../db/configuration/updateConfiguration"; import { getConfig } from "../../../../utils/cache/getConfig"; diff --git a/src/server/routes/index.ts b/src/server/routes/index.ts index 9daf6b452..4345859ca 100644 --- a/src/server/routes/index.ts +++ b/src/server/routes/index.ts @@ -111,7 +111,7 @@ import { getAllWebhooksData } from "./webhooks/getAll"; import { revokeWebhook } from "./webhooks/revoke"; import { testWebhookRoute } from "./webhooks/test"; -export const withRoutes = async (fastify: FastifyInstance) => { +export async function withRoutes(fastify: FastifyInstance) { // Backend Wallets await fastify.register(createBackendWallet); await fastify.register(removeBackendWallet); @@ -267,4 +267,4 @@ export const withRoutes = async (fastify: FastifyInstance) => { // Admin await fastify.register(getTransactionDetails); await fastify.register(getNonceDetailsRoute); -}; +} diff --git a/src/server/schemas/address.ts b/src/server/schemas/address.ts index 8779391bc..c7006f999 100644 --- a/src/server/schemas/address.ts +++ b/src/server/schemas/address.ts @@ -12,7 +12,7 @@ import { Type } from "@sinclair/typebox"; */ export const AddressSchema = Type.RegExp(/^0x[a-fA-F0-9]{40}$/, { description: "A contract or wallet address", - examples: ["0x152e208d08cd3ea1aa5d179b2e3eba7d1a733ef4"], + examples: ["0x000000000000000000000000000000000000dead"], }); export const TransactionHashSchema = Type.RegExp(/^0x[a-fA-F0-9]{64}$/, { diff --git a/src/server/schemas/wallet/index.ts b/src/server/schemas/wallet/index.ts index 4f5a87c9e..cd2c4d024 100644 --- a/src/server/schemas/wallet/index.ts +++ b/src/server/schemas/wallet/index.ts @@ -22,11 +22,13 @@ export const walletWithAAHeaderSchema = Type.Object({ "x-account-address": Type.Optional({ ...AddressSchema, description: "Smart account address", + examples: [], }), "x-account-factory-address": Type.Optional({ ...AddressSchema, description: "Smart account factory address. If omitted, Engine will try to resolve it from the contract.", + examples: [], }), "x-account-salt": Type.Optional( Type.String({ diff --git a/src/tests/cors.test.ts b/src/tests/cors.test.ts deleted file mode 100644 index 321709953..000000000 --- a/src/tests/cors.test.ts +++ /dev/null @@ -1,25 +0,0 @@ -import { describe, expect, it } from "vitest"; -import { sanitizeOrigin } from "../server/middleware/cors/cors"; - -describe("sanitizeOrigin", () => { - it("with leading and trailing slashes", () => { - expect(sanitizeOrigin("/foobar/")).toEqual(RegExp("foobar")); - }); - it("with leading wildcard", () => { - expect(sanitizeOrigin("*.foobar.com")).toEqual(RegExp(".*.foobar.com")); - }); - it("with thirdweb domains", () => { - expect(sanitizeOrigin("https://thirdweb-preview.com")).toEqual( - new RegExp(/^https?:\/\/.*\.thirdweb-preview\.com$/), - ); - expect(sanitizeOrigin("https://thirdweb-dev.com")).toEqual( - new RegExp(/^https?:\/\/.*\.thirdweb-dev\.com$/), - ); - }); - it("with trailing slashes", () => { - expect(sanitizeOrigin("https://foobar.com/")).toEqual("https://foobar.com"); - }); - it("fallback: don't change origin", () => { - expect(sanitizeOrigin("https://foobar.com")).toEqual("https://foobar.com"); - }); -}); diff --git a/src/tests/schema.test.ts b/src/tests/schema.test.ts index 4255b2e95..17f4b167a 100644 --- a/src/tests/schema.test.ts +++ b/src/tests/schema.test.ts @@ -45,10 +45,7 @@ describe("chainIdOrSlugSchema", () => { describe("AddressSchema", () => { it("should validate valid addresses", () => { expect( - Value.Check(AddressSchema, "0x152e208d08cd3ea1aa5d179b2e3eba7d1a733ef4"), - ).toBe(true); - expect( - Value.Check(AddressSchema, "0x1234567890abcdef1234567890abcdef12345678"), + Value.Check(AddressSchema, "0x000000000000000000000000000000000000dead"), ).toBe(true); }); diff --git a/src/utils/usage.ts b/src/utils/usage.ts index 5d218b787..002c437c8 100644 --- a/src/utils/usage.ts +++ b/src/utils/usage.ts @@ -3,7 +3,7 @@ import { UsageEvent } from "@thirdweb-dev/service-utils/cf-worker"; import { FastifyInstance } from "fastify"; import { Address, Hex } from "thirdweb"; import { ADMIN_QUEUES_BASEPATH } from "../server/middleware/adminRoutes"; -import { OPENAPI_ROUTES } from "../server/middleware/open-api"; +import { OPENAPI_ROUTES } from "../server/middleware/openApi"; import { contractParamSchema } from "../server/schemas/sharedApiSchemas"; import { walletWithAddressParamSchema } from "../server/schemas/wallet"; import { getChainIdFromChain } from "../server/utils/chain"; @@ -54,7 +54,7 @@ const SKIP_USAGE_PATHS = new Set([ ...OPENAPI_ROUTES, ]); -export const withServerUsageReporting = (server: FastifyInstance) => { +export function withServerUsageReporting(server: FastifyInstance) { // Skip reporting if CLIENT_ANALYTICS_URL is unset. if (env.CLIENT_ANALYTICS_URL === "") { return; @@ -98,7 +98,7 @@ export const withServerUsageReporting = (server: FastifyInstance) => { body: JSON.stringify(requestBody), }).catch(() => {}); // Catch uncaught exceptions since this fetch call is non-blocking. }); -}; +} export const reportUsage = (usageEvents: ReportUsageParams[]) => { // Skip reporting if CLIENT_ANALYTICS_URL is unset. diff --git a/test/e2e/tests/sign-transaction.test.ts b/test/e2e/tests/sign-transaction.test.ts index 16e04b7e4..7dabab5d7 100644 --- a/test/e2e/tests/sign-transaction.test.ts +++ b/test/e2e/tests/sign-transaction.test.ts @@ -9,7 +9,7 @@ describe("signTransaction route", () => { transaction: { type: 0, chainId: 1, - to: "0x152e208d08cd3ea1aa5d179b2e3eba7d1a733ef4", + to: "0x000000000000000000000000000000000000dead", nonce: "42", gasLimit: "88000", gasPrice: "2000000000", @@ -29,7 +29,7 @@ describe("signTransaction route", () => { transaction: { type: 1, chainId: 137, - to: "0x152e208d08cd3ea1aa5d179b2e3eba7d1a733ef4", + to: "0x000000000000000000000000000000000000dead", nonce: "42", gasLimit: "88000", maxFeePerGas: "2000000000", @@ -37,7 +37,7 @@ describe("signTransaction route", () => { value: "100000000000000000", accessList: [ { - address: "0x152e208d08cd3ea1aa5d179b2e3eba7d1a733ef4", + address: "0x000000000000000000000000000000000000dead", storageKeys: [ "0x0000000000000000000000000000000000000000000000000000000000000001", ], @@ -58,13 +58,13 @@ describe("signTransaction route", () => { transaction: { type: 2, chainId: 137, - to: "0x152e208d08cd3ea1aa5d179b2e3eba7d1a733ef4", + to: "0x000000000000000000000000000000000000dead", nonce: "42", gasLimit: "88000", value: "100000000000000000", accessList: [ { - address: "0x152e208d08cd3ea1aa5d179b2e3eba7d1a733ef4", + address: "0x000000000000000000000000000000000000dead", storageKeys: [ "0x0000000000000000000000000000000000000000000000000000000000000001", ], @@ -85,7 +85,7 @@ describe("signTransaction route", () => { transaction: { type: 3, chainId: 137, - to: "0x152e208d08cd3ea1aa5d179b2e3eba7d1a733ef4", + to: "0x000000000000000000000000000000000000dead", nonce: "42", gasLimit: "88000", maxFeePerGas: "2000000000", @@ -93,7 +93,7 @@ describe("signTransaction route", () => { value: "100000000000000000", accessList: [ { - address: "0x152e208d08cd3ea1aa5d179b2e3eba7d1a733ef4", + address: "0x000000000000000000000000000000000000dead", storageKeys: [ "0x0000000000000000000000000000000000000000000000000000000000000001", ], @@ -114,7 +114,7 @@ describe("signTransaction route", () => { transaction: { type: 4, chainId: 137, - to: "0x152e208d08cd3ea1aa5d179b2e3eba7d1a733ef4", + to: "0x000000000000000000000000000000000000dead", nonce: "42", gasLimit: "88000", maxFeePerGas: "2000000000", @@ -122,7 +122,7 @@ describe("signTransaction route", () => { value: "100000000000000000", accessList: [ { - address: "0x152e208d08cd3ea1aa5d179b2e3eba7d1a733ef4", + address: "0x000000000000000000000000000000000000dead", storageKeys: [ "0x0000000000000000000000000000000000000000000000000000000000000001", ], diff --git a/yarn.lock b/yarn.lock index 833d26027..1d724f715 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2330,14 +2330,6 @@ ajv-formats "^2.1.1" fast-uri "^2.0.0" -"@fastify/basic-auth@^5.1.1": - version "5.1.1" - resolved "https://registry.yarnpkg.com/@fastify/basic-auth/-/basic-auth-5.1.1.tgz#ff93d787bbbbd93b126550b797e1c416628c0a49" - integrity sha512-L4b7EK5LKZnV6fdH1+rQbjhkKGXjCfiKJ0JkdGHZQPBMHMiXDZF8xbZsCakWGf9c7jDXJicP3FPcIXUPBkuSeQ== - dependencies: - "@fastify/error" "^3.0.0" - fastify-plugin "^4.0.0" - "@fastify/cookie@^9.3.1": version "9.3.1" resolved "https://registry.yarnpkg.com/@fastify/cookie/-/cookie-9.3.1.tgz#48b89a356a23860c666e2fe522a084cc5c943d33" @@ -2346,7 +2338,7 @@ cookie-signature "^1.1.0" fastify-plugin "^4.0.0" -"@fastify/error@^3.0.0", "@fastify/error@^3.3.0", "@fastify/error@^3.4.0": +"@fastify/error@^3.3.0", "@fastify/error@^3.4.0": version "3.4.1" resolved "https://registry.yarnpkg.com/@fastify/error/-/error-3.4.1.tgz#b14bb4cac3dd4ec614becbc643d1511331a6425c" integrity sha512-wWSvph+29GR783IhmvdwWnN4bUxTD01Vm5Xad4i7i1VuAOItLvbPAb69sb0IQ2N57yprvhNIwAP5B6xfKTmjmQ== @@ -6700,10 +6692,10 @@ cross-fetch@^4.0.0: dependencies: node-fetch "^2.6.12" -cross-spawn@^7.0.0, cross-spawn@^7.0.2, cross-spawn@^7.0.3: - version "7.0.3" - resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-7.0.3.tgz#f73a85b9d5d41d045551c177e2882d4ac85728a6" - integrity sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w== +cross-spawn@>=7.0.6, cross-spawn@^7.0.0, cross-spawn@^7.0.2, cross-spawn@^7.0.3: + version "7.0.6" + resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-7.0.6.tgz#8a58fe78f00dcd70c370451759dfbfaf03e8ee9f" + integrity sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA== dependencies: path-key "^3.1.0" shebang-command "^2.0.0" @@ -7623,9 +7615,9 @@ execa@^8.0.1: strip-final-newline "^3.0.0" execa@^9.1.0: - version "9.4.0" - resolved "https://registry.yarnpkg.com/execa/-/execa-9.4.0.tgz#071ff6516c46eb82af9a559dba3c891637a10f3f" - integrity sha512-yKHlle2YGxZE842MERVIplWwNH5VYmqqcPFgtnlU//K8gxuFFXu0pwd/CrfXTumFpeEiufsP7+opT/bPJa1yVw== + version "9.5.1" + resolved "https://registry.yarnpkg.com/execa/-/execa-9.5.1.tgz#ab9b68073245e1111bba359962a34fcdb28deef2" + integrity sha512-QY5PPtSonnGwhhHDNI7+3RvY285c7iuJFFB+lU+oEzMY/gEGJ808owqJsrr8Otd1E/x07po1LkUBmdAc5duPAg== dependencies: "@sindresorhus/merge-streams" "^4.0.0" cross-spawn "^7.0.3" @@ -10290,9 +10282,9 @@ prettier@^2.8.7: integrity sha512-tdN8qQGvNjw4CHbY+XXk0JgCXn9QiF21a55rBe5LJAU+kDyC4WQn4+awm2Xfk2lQMk5fKup9XgzTZtGkjBdP9Q== pretty-ms@^9.0.0: - version "9.1.0" - resolved "https://registry.yarnpkg.com/pretty-ms/-/pretty-ms-9.1.0.tgz#0ad44de6086454f48a168e5abb3c26f8db1b3253" - integrity sha512-o1piW0n3tgKIKCwk2vpM/vOV13zjJzvP37Ioze54YlTHE06m4tjEbzg9WsKkvTuyYln2DHjo5pY4qrZGI0otpw== + version "9.2.0" + resolved "https://registry.yarnpkg.com/pretty-ms/-/pretty-ms-9.2.0.tgz#e14c0aad6493b69ed63114442a84133d7e560ef0" + integrity sha512-4yf0QO/sllf/1zbZWYnvWw3NxCQwLXKzIj0G849LSufP15BXKM0rbD2Z3wVnkMfjdn/CB0Dpp444gYAACdsplg== dependencies: parse-ms "^4.0.0" @@ -11087,16 +11079,7 @@ strict-uri-encode@^2.0.0: resolved "https://registry.yarnpkg.com/strict-uri-encode/-/strict-uri-encode-2.0.0.tgz#b9c7330c7042862f6b142dc274bbcc5866ce3546" integrity sha512-QwiXZgpRcKkhTj2Scnn++4PKtWsH0kpzZ62L2R6c/LUVYv7hVnZqcg2+sMuT6R7Jusu1vviK/MFsu6kNJfWlEQ== -"string-width-cjs@npm:string-width@^4.2.0": - version "4.2.3" - resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010" - integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== - dependencies: - emoji-regex "^8.0.0" - is-fullwidth-code-point "^3.0.0" - strip-ansi "^6.0.1" - -string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3: +"string-width-cjs@npm:string-width@^4.2.0", string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3: version "4.2.3" resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010" integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== @@ -11128,14 +11111,7 @@ string_decoder@~1.1.1: dependencies: safe-buffer "~5.1.0" -"strip-ansi-cjs@npm:strip-ansi@^6.0.1": - version "6.0.1" - resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9" - integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== - dependencies: - ansi-regex "^5.0.1" - -strip-ansi@^6.0.0, strip-ansi@^6.0.1: +"strip-ansi-cjs@npm:strip-ansi@^6.0.1", strip-ansi@^6.0.0, strip-ansi@^6.0.1: version "6.0.1" resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9" integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== @@ -12160,7 +12136,7 @@ wordwrap@^1.0.0: resolved "https://registry.yarnpkg.com/wordwrap/-/wordwrap-1.0.0.tgz#27584810891456a4171c8d0226441ade90cbcaeb" integrity sha512-gvVzJFlPycKc5dZN4yPkP8w7Dc37BtP1yczEneOb4uq34pXZcvrtRTmWV8W+Ume+XCxKgbjM+nevkyFPMybd4Q== -"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0": +"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0", wrap-ansi@^7.0.0: version "7.0.0" resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43" integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q== @@ -12178,15 +12154,6 @@ wrap-ansi@^6.2.0: string-width "^4.1.0" strip-ansi "^6.0.0" -wrap-ansi@^7.0.0: - version "7.0.0" - resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43" - integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q== - dependencies: - ansi-styles "^4.0.0" - string-width "^4.1.0" - strip-ansi "^6.0.0" - wrap-ansi@^8.1.0: version "8.1.0" resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-8.1.0.tgz#56dc22368ee570face1b49819975d9b9a5ead214"