diff --git a/apps/web/app/api/metatags/route.ts b/apps/web/app/api/links/metatags/route.ts similarity index 58% rename from apps/web/app/api/metatags/route.ts rename to apps/web/app/api/links/metatags/route.ts index 683349fd638..056632c611c 100644 --- a/apps/web/app/api/metatags/route.ts +++ b/apps/web/app/api/links/metatags/route.ts @@ -6,37 +6,41 @@ import { getMetaTags } from "./utils"; export const runtime = "edge"; -const CORS_HEADERS = { - "Access-Control-Allow-Origin": "*", - "Access-Control-Allow-Methods": "GET, OPTIONS", -}; - export async function GET(req: NextRequest) { try { + const origin = req.headers.get("origin"); + // Validate the origin header and set CORS headers accordingly + const corsHeaders = { + "Access-Control-Allow-Methods": "GET", + "Access-Control-Allow-Headers": "Content-Type", + }; + + if (origin && origin.endsWith(".dub.co")) { + corsHeaders["Access-Control-Allow-Origin"] = origin; + } + + // Validate URL parameter const { url } = getUrlQuerySchema.parse({ url: req.nextUrl.searchParams.get("url"), }); + // Rate limit by IP await ratelimitOrThrow(req, "metatags"); + // Get metatags const metatags = await getMetaTags(url); + + // Return response return NextResponse.json( { ...metatags, poweredBy: "Dub.co - Link management for modern marketing teams", }, { - headers: CORS_HEADERS, + headers: corsHeaders, }, ); } catch (error) { - return handleAndReturnErrorResponse(error, CORS_HEADERS); + return handleAndReturnErrorResponse(error); } } - -export function OPTIONS() { - return new Response(null, { - status: 204, - headers: CORS_HEADERS, - }); -} diff --git a/apps/web/app/api/metatags/utils.ts b/apps/web/app/api/links/metatags/utils.ts similarity index 58% rename from apps/web/app/api/metatags/utils.ts rename to apps/web/app/api/links/metatags/utils.ts index 5be950ebb2c..04d1d173423 100644 --- a/apps/web/app/api/metatags/utils.ts +++ b/apps/web/app/api/links/metatags/utils.ts @@ -5,13 +5,36 @@ import he from "he"; import { parse } from "node-html-parser"; export const getHtml = async (url: string) => { - return await fetchWithTimeout(url, { - headers: { - "User-Agent": "Dub.co Metatags API (https://api.dub.co/metatags)", - }, - }) - .then((r) => r.text()) - .catch(() => null); + try { + const response = await fetchWithTimeout(url); + + if (!response.ok) { + // If we get a 406 or other error, check if it's a Cloudflare-protected site + const isCloudflare = response.headers.get("server") === "cloudflare"; + if (isCloudflare) { + console.warn(`Cloudflare-protected site detected: ${url}`); + return null; + } + console.error(`HTTP error! status: ${response.status} for URL: ${url}`); + return null; + } + + const text = await response.text(); + + // Check if the response contains Cloudflare's challenge page + if ( + text.includes("challenge-platform") || + text.includes("cf-browser-verification") + ) { + console.warn(`Cloudflare challenge page detected for: ${url}`); + return null; + } + + return text; + } catch (error) { + console.error(`Error fetching ${url}:`, error); + return null; + } }; export const getHeadChildNodes = (html) => { @@ -47,15 +70,42 @@ export const getRelativeUrl = (url: string, imageUrl: string) => { return new URL(imageUrl, baseURL).toString(); }; -export const getMetaTags = async (url: string) => { - const html = await getHtml(url); - if (!html) { +const generateFallbackMetadata = (url: string) => { + try { + const parsedUrl = new URL(url); + const hostname = parsedUrl.hostname; + const path = parsedUrl.pathname; + + // Clean up the path for title + const pathParts = path.split("/").filter(Boolean); + const lastPathPart = pathParts[pathParts.length - 1] || ""; + const formattedPath = lastPathPart + .split("-") + .map((word) => word.charAt(0).toUpperCase() + word.slice(1)) + .join(" "); + + return { + title: formattedPath || hostname.replace(/^www\./, ""), + description: `Visit ${hostname}${path}`, + image: null, + }; + } catch (e) { return { title: url, - description: "No description", + description: "No description available", image: null, }; } +}; + +export const getMetaTags = async (url: string) => { + const html = await getHtml(url); + if (!html) { + // If we couldn't fetch the HTML (e.g., due to Cloudflare protection), + // generate fallback metadata from the URL + return generateFallbackMetadata(url); + } + const { metaTags, title: titleTag, linkTags } = getHeadChildNodes(html); let object = {}; diff --git a/apps/web/app/api/track/lead/route.ts b/apps/web/app/api/track/lead/route.ts index 4215b9f1762..b64c057da0e 100644 --- a/apps/web/app/api/track/lead/route.ts +++ b/apps/web/app/api/track/lead/route.ts @@ -44,8 +44,8 @@ export const POST = withWorkspace( } = trackLeadRequestSchema .extend({ // add backwards compatibility - externalId: z.string().optional(), - customerId: z.string().optional(), + externalId: z.string().nullish(), + customerId: z.string().nullish(), }) .parse(body); diff --git a/apps/web/app/api/track/sale/route.ts b/apps/web/app/api/track/sale/route.ts index 0adb04f4e57..e96e7ccff68 100644 --- a/apps/web/app/api/track/sale/route.ts +++ b/apps/web/app/api/track/sale/route.ts @@ -42,8 +42,8 @@ export const POST = withWorkspace( } = trackSaleRequestSchema .extend({ // add backwards compatibility - externalId: z.string().optional(), - customerId: z.string().optional(), + externalId: z.string().nullish(), + customerId: z.string().nullish(), }) .parse(body); diff --git a/apps/web/app/app.dub.co/(auth)/oauth/authorize/page.tsx b/apps/web/app/app.dub.co/(auth)/oauth/authorize/page.tsx index 2bb27e8ef40..976337af7be 100644 --- a/apps/web/app/app.dub.co/(auth)/oauth/authorize/page.tsx +++ b/apps/web/app/app.dub.co/(auth)/oauth/authorize/page.tsx @@ -5,7 +5,7 @@ import { authorizeRequestSchema } from "@/lib/zod/schemas/oauth"; import EmptyState from "@/ui/shared/empty-state"; import { BlurImage, Logo } from "@dub/ui"; import { CircleWarning, CubeSettings } from "@dub/ui/icons"; -import { HOME_DOMAIN, constructMetadata } from "@dub/utils"; +import { constructMetadata } from "@dub/utils"; import { ArrowLeftRight } from "lucide-react"; import { redirect } from "next/navigation"; import { Suspense } from "react"; @@ -62,7 +62,7 @@ export default async function Authorize({ )} - + diff --git a/apps/web/app/app.dub.co/(onboarding)/onboarding/(steps)/link/form.tsx b/apps/web/app/app.dub.co/(onboarding)/onboarding/(steps)/link/form.tsx index ecc1821b78a..70b05c1f01d 100644 --- a/apps/web/app/app.dub.co/(onboarding)/onboarding/(steps)/link/form.tsx +++ b/apps/web/app/app.dub.co/(onboarding)/onboarding/(steps)/link/form.tsx @@ -73,7 +73,7 @@ export function Form() { // If url is valid, continue to generate metatags, else return null new URL(debouncedUrl); setLoadingPreviewImage(true); - const res = await fetch(`/api/metatags?url=${debouncedUrl}`); + const res = await fetch(`/api/links/metatags?url=${debouncedUrl}`); if (res.ok) { const results = await res.json(); setPreviewImage(results.image); diff --git a/apps/web/app/cloaked/[url]/page.tsx b/apps/web/app/cloaked/[url]/page.tsx index f110a6ffb19..e2f3cf30fc2 100644 --- a/apps/web/app/cloaked/[url]/page.tsx +++ b/apps/web/app/cloaked/[url]/page.tsx @@ -3,7 +3,7 @@ import { constructMetadata, getApexDomain, } from "@dub/utils"; -import { getMetaTags } from "app/api/metatags/utils"; +import { getMetaTags } from "app/api/links/metatags/utils"; export const runtime = "edge"; export const fetchCache = "force-no-store"; diff --git a/apps/web/lib/middleware/api.ts b/apps/web/lib/middleware/api.ts index 0988aaddfd9..c129bf9089e 100644 --- a/apps/web/lib/middleware/api.ts +++ b/apps/web/lib/middleware/api.ts @@ -1,18 +1,14 @@ import { parse } from "@/lib/middleware/utils"; -import { HOME_DOMAIN } from "@dub/utils"; import { NextRequest, NextResponse } from "next/server"; export default function ApiMiddleware(req: NextRequest) { - const { path, fullPath } = parse(req); + const { fullPath } = parse(req); - // special case for /metatags - if (path === "/metatags") { - const url = req.nextUrl.searchParams.get("url"); - if (!url) { - return NextResponse.redirect(`${HOME_DOMAIN}/tools/metatags`, { - status: 301, - }); - } + // redirect to dub.co for /metatags + if (fullPath.startsWith("/metatags")) { + return NextResponse.redirect("https://dub.co", { + status: 301, + }); } // Note: we don't have to account for paths starting with `/api` // since they're automatically excluded via our middleware matcher diff --git a/apps/web/lib/middleware/utils/bots-list.ts b/apps/web/lib/middleware/utils/bots-list.ts index 9a2d23140d3..d1c5d498de1 100644 --- a/apps/web/lib/middleware/utils/bots-list.ts +++ b/apps/web/lib/middleware/utils/bots-list.ts @@ -1,7 +1,6 @@ export const UA_BOTS = [ "bot", // most bots "crawler", // most crawlers - "metatags", // Dub.co Metatags API (https://api.dub.co/metatags) "chatgpt", // ChatGPT "bluesky", // Bluesky crawler "facebookexternalhit", // Facebook crawler @@ -18,6 +17,7 @@ export const UA_BOTS = [ "MetaInspector", // metatags.io "Go-http-client", // Go-http-client/1.1 is a bot: https://user-agents.net/string/go-http-client-1-1 "iframely", // https://iframely.com/docs/about (used by Notion, Linear) + "H1cbA69", // internal links/metatags API // new "ia_archiver", diff --git a/apps/web/lib/openapi/index.ts b/apps/web/lib/openapi/index.ts index d01bfb56337..ac1015e2d09 100644 --- a/apps/web/lib/openapi/index.ts +++ b/apps/web/lib/openapi/index.ts @@ -13,7 +13,6 @@ import { embedTokensPaths } from "./embed-tokens"; import { eventsPath } from "./events"; import { foldersPaths } from "./folders"; import { linksPaths } from "./links"; -import { metatagsPath } from "./metatags"; import { partnersPaths } from "./partners"; import { qrCodePaths } from "./qr"; import { tagsPaths } from "./tags"; @@ -56,7 +55,6 @@ export const document = createDocument({ ...workspacesPaths, ...embedTokensPaths, ...qrCodePaths, - ...metatagsPath, }, components: { schemas: { diff --git a/apps/web/lib/openapi/metatags/index.ts b/apps/web/lib/openapi/metatags/index.ts deleted file mode 100644 index b50d5920341..00000000000 --- a/apps/web/lib/openapi/metatags/index.ts +++ /dev/null @@ -1,38 +0,0 @@ -import z from "@/lib/zod"; -import { getUrlQuerySchema } from "@/lib/zod/schemas/links"; -import { metaTagsSchema } from "@/lib/zod/schemas/metatags"; -import { ZodOpenApiOperationObject, ZodOpenApiPathsObject } from "zod-openapi"; - -const getMetatags: ZodOpenApiOperationObject = { - operationId: "getMetatags", - "x-speakeasy-name-override": "get", - summary: "Retrieve the metatags for a URL", - description: "Retrieve the metatags for a URL.", - requestParams: { - query: getUrlQuerySchema.merge( - z.object({ - url: z.string().openapi({ - example: "https://dub.co", - description: "The URL to retrieve metatags for.", - }), - }), - ), - }, - responses: { - "200": { - description: "The retrieved metatags", - content: { - "application/json": { - schema: metaTagsSchema, - }, - }, - }, - }, - tags: ["Metatags"], -}; - -export const metatagsPath: ZodOpenApiPathsObject = { - "/metatags": { - get: getMetatags, - }, -}; diff --git a/apps/web/lib/types.ts b/apps/web/lib/types.ts index 2998c899971..d2953ea8ec9 100644 --- a/apps/web/lib/types.ts +++ b/apps/web/lib/types.ts @@ -1,5 +1,4 @@ import z from "@/lib/zod"; -import { metaTagsSchema } from "@/lib/zod/schemas/metatags"; import { PartnerEarningsSchema, PartnerProfileCustomerSchema, @@ -278,8 +277,6 @@ export const tagColors = [ export type DashboardProps = z.infer; -export type MetaTag = z.infer; - export type TokenProps = z.infer; export type OAuthAppProps = z.infer; diff --git a/apps/web/lib/upstash/record-metatags.ts b/apps/web/lib/upstash/record-metatags.ts index 3a2d311f52f..ef546c03a67 100644 --- a/apps/web/lib/upstash/record-metatags.ts +++ b/apps/web/lib/upstash/record-metatags.ts @@ -2,7 +2,7 @@ import { getDomainWithoutWWW } from "@dub/utils"; import { redis } from "./redis"; /** - * Recording metatags that were generated via `/api/metatags` + * Recording metatags that were generated via "/api/links/metatags" * If there's an error, it will be logged to a separate redis list for debugging **/ export async function recordMetatags(url: string, error: boolean) { diff --git a/apps/web/lib/zod/schemas/links.ts b/apps/web/lib/zod/schemas/links.ts index e8adadf9270..bb749573a3f 100644 --- a/apps/web/lib/zod/schemas/links.ts +++ b/apps/web/lib/zod/schemas/links.ts @@ -538,19 +538,19 @@ export const LinkSchema = z .string() .nullable() .describe( - "The title of the short link generated via `api.dub.co/metatags`. Will be used for Custom Social Media Cards if `proxy` is true.", + "The title of the short link. Will be used for Custom Social Media Cards if `proxy` is true.", ), description: z .string() .nullable() .describe( - "The description of the short link generated via `api.dub.co/metatags`. Will be used for Custom Social Media Cards if `proxy` is true.", + "The description of the short link. Will be used for Custom Social Media Cards if `proxy` is true.", ), image: z .string() .nullable() .describe( - "The image of the short link generated via `api.dub.co/metatags`. Will be used for Custom Social Media Cards if `proxy` is true.", + "The image of the short link. Will be used for Custom Social Media Cards if `proxy` is true.", ), video: z .string() diff --git a/apps/web/lib/zod/schemas/metatags.ts b/apps/web/lib/zod/schemas/metatags.ts deleted file mode 100644 index cb7280f27cf..00000000000 --- a/apps/web/lib/zod/schemas/metatags.ts +++ /dev/null @@ -1,23 +0,0 @@ -import z from "@/lib/zod"; - -export const metaTagsSchema = z.object({ - title: z - .string() - .nullable() - .describe("The meta title tag for the URL.") - .openapi({ - example: "Dub.co - Link Management for Modern Marketing Teams", - }), - description: z - .string() - .nullable() - .describe("The meta description tag for the URL.") - .openapi({ - example: "Dub.co is the open-source link management infrastructure ...", - }), - image: z - .string() - .nullable() - .describe("The OpenGraph image for the URL.") - .openapi({ example: "https://assets.dub.co/thumbnail.jpg" }), -}); diff --git a/apps/web/lib/zod/schemas/sales.ts b/apps/web/lib/zod/schemas/sales.ts index f8033d3df6b..c7dea11efa9 100644 --- a/apps/web/lib/zod/schemas/sales.ts +++ b/apps/web/lib/zod/schemas/sales.ts @@ -26,7 +26,7 @@ export const trackSaleRequestSchema = z.object({ .optional() .default("Purchase") .describe("The name of the sale event.") - .openapi({ examples: ["Purchase", "Upgrade", "Payment"] }), + .openapi({ example: "Invoice paid" }), invoiceId: z .string() .nullish() diff --git a/apps/web/next.config.js b/apps/web/next.config.js index f98cf13ca0b..49bed7f523e 100644 --- a/apps/web/next.config.js +++ b/apps/web/next.config.js @@ -165,30 +165,6 @@ module.exports = withAxiom({ } ), ), - { - source: "/metatags", - has: [ - { - type: "host", - value: "dub.sh", - }, - ], - destination: "https://dub.co/tools/metatags", - permanent: true, - statusCode: 301, - }, - { - source: "/metatags", - has: [ - { - type: "host", - value: "dub.co", - }, - ], - destination: "/tools/metatags", - permanent: true, - statusCode: 301, - }, { source: "/", has: [ diff --git a/apps/web/package.json b/apps/web/package.json index 270a9e40a1c..853457d892b 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -124,7 +124,7 @@ "vaul": "^1.1.2", "zod": "^3.22.4", "zod-error": "^1.5.0", - "zod-openapi": "^2.19.0" + "zod-openapi": "^4.2.4" }, "devDependencies": { "@types/he": "^1.2.3", diff --git a/apps/web/tests/metatags/retrieve-metatags.test.ts b/apps/web/tests/links/retrieve-metatags.test.ts similarity index 80% rename from apps/web/tests/metatags/retrieve-metatags.test.ts rename to apps/web/tests/links/retrieve-metatags.test.ts index e2236073990..bdf744ab6c3 100644 --- a/apps/web/tests/metatags/retrieve-metatags.test.ts +++ b/apps/web/tests/links/retrieve-metatags.test.ts @@ -1,13 +1,12 @@ -import { MetaTag } from "@/lib/types"; import { expect, test } from "vitest"; import { IntegrationHarness } from "../utils/integration"; -test("GET /metatags", async (ctx) => { +test("GET /links/metatags", async (ctx) => { const h = new IntegrationHarness(ctx); const { http } = await h.init(); - const { status, data: metatags } = await http.get({ - path: `/metatags`, + const { status, data: metatags } = await http.get({ + path: "/links/metatags", query: { url: "https://dub.co", }, diff --git a/apps/web/ui/links/link-builder/use-metatags.ts b/apps/web/ui/links/link-builder/use-metatags.ts index 7cd2942aa2e..374e67831fd 100644 --- a/apps/web/ui/links/link-builder/use-metatags.ts +++ b/apps/web/ui/links/link-builder/use-metatags.ts @@ -42,7 +42,7 @@ export function useMetatags({ enabled = true }: { enabled?: boolean } = {}) { // if url is valid, continue to generate metatags, else throw error and return null new URL(debouncedUrl); setGeneratingMetatags(true); - fetch(`/api/metatags?url=${debouncedUrl}`).then(async (res) => { + fetch(`/api/links/metatags?url=${debouncedUrl}`).then(async (res) => { if (res.status === 200) { const results = await res.json(); const truncatedTitle = truncate(results.title, 120); diff --git a/packages/ui/package.json b/packages/ui/package.json index 7ae6d61199d..f6f7c7020d9 100644 --- a/packages/ui/package.json +++ b/packages/ui/package.json @@ -1,7 +1,7 @@ { "name": "@dub/ui", "description": "UI components for Dub.co", - "version": "0.2.35", + "version": "0.2.36", "sideEffects": false, "main": "./dist/index.js", "module": "./dist/index.mjs", diff --git a/packages/ui/src/link-preview.tsx b/packages/ui/src/link-preview.tsx index 82f3a49f6f1..3dc301a5a0e 100644 --- a/packages/ui/src/link-preview.tsx +++ b/packages/ui/src/link-preview.tsx @@ -2,7 +2,7 @@ import { fetcher, getDomainWithoutWWW, getUrlFromString } from "@dub/utils"; import { Link2 } from "lucide-react"; -import { useRouter, useSearchParams } from "next/navigation"; +import { useSearchParams } from "next/navigation"; import { useEffect, useMemo, useRef } from "react"; import useSWR from "swr"; import { useDebounce } from "use-debounce"; @@ -10,7 +10,6 @@ import { useMediaQuery } from "./hooks"; import { LoadingCircle, Photo } from "./icons"; export function LinkPreview({ defaultUrl }: { defaultUrl?: string }) { - const router = useRouter(); const searchParams = useSearchParams(); const url = defaultUrl || searchParams?.get("url") || "https://github.com/dubinc/dub"; @@ -24,7 +23,8 @@ export function LinkPreview({ defaultUrl }: { defaultUrl?: string }) { description: string | null; image: string | null; }>( - debouncedUrl && `/api/metatags?url=${encodeURIComponent(debouncedUrl)}`, + debouncedUrl && + `/api/links/metatags?url=${encodeURIComponent(debouncedUrl)}`, fetcher, { revalidateOnFocus: false, @@ -56,13 +56,6 @@ export function LinkPreview({ defaultUrl }: { defaultUrl?: string }) { className="block w-full rounded-md border-neutral-200 pl-10 text-sm text-neutral-900 placeholder-neutral-400 shadow-lg focus:border-neutral-500 focus:outline-none focus:ring-neutral-500" placeholder="Enter your URL" defaultValue={url} - onChange={(e) => - router.replace( - `/tools/metatags${ - e.target.value.length > 0 ? `?url=${e.target.value}` : "" - }`, - ) - } aria-invalid="true" /> diff --git a/packages/utils/src/constants/layout.ts b/packages/utils/src/constants/layout.ts index c874cda1b95..e1fca2a6ccc 100644 --- a/packages/utils/src/constants/layout.ts +++ b/packages/utils/src/constants/layout.ts @@ -8,6 +8,5 @@ export const ALL_TOOLS = [ { name: "Google Link Shortener", slug: "google-link-shortener" }, { name: "Amazon Link Shortener", slug: "amazon-link-shortener" }, { name: "Figma Link Shortener", slug: "figma-link-shortener" }, - { name: "Metatags API", slug: "metatags" }, { name: "Link Inspector", slug: "inspector" }, ]; diff --git a/packages/utils/src/constants/main.ts b/packages/utils/src/constants/main.ts index d2f6e607e7b..ce6e1928ec9 100644 --- a/packages/utils/src/constants/main.ts +++ b/packages/utils/src/constants/main.ts @@ -3,8 +3,6 @@ export const APP_NAME = process.env.NEXT_PUBLIC_APP_NAME || "Dub.co"; export const SHORT_DOMAIN = process.env.NEXT_PUBLIC_APP_SHORT_DOMAIN || "dub.sh"; -export const HOME_DOMAIN = `https://${process.env.NEXT_PUBLIC_APP_DOMAIN}`; - export const APP_HOSTNAMES = new Set([ `app.${process.env.NEXT_PUBLIC_APP_DOMAIN}`, `preview.${process.env.NEXT_PUBLIC_APP_DOMAIN}`, diff --git a/packages/utils/src/functions/construct-metadata.ts b/packages/utils/src/functions/construct-metadata.ts index 32756a6de2c..fecf6172043 100644 --- a/packages/utils/src/functions/construct-metadata.ts +++ b/packages/utils/src/functions/construct-metadata.ts @@ -1,5 +1,4 @@ import { Metadata } from "next"; -import { HOME_DOMAIN } from "../constants"; export function constructMetadata({ title, @@ -73,7 +72,7 @@ export function constructMetadata({ creator: "@dubdotco", }, icons, - metadataBase: new URL(HOME_DOMAIN), + metadataBase: new URL("https://dub.co"), ...((url || canonicalUrl) && { alternates: { canonical: url || canonicalUrl, diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 8b7fc77cbda..a4ae930b647 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -361,8 +361,8 @@ importers: specifier: ^1.5.0 version: 1.5.0 zod-openapi: - specifier: ^2.19.0 - version: 2.19.0(zod@3.22.4) + specifier: ^4.2.4 + version: 4.2.4(zod@3.22.4) devDependencies: '@types/he': specifier: ^1.2.3 @@ -11921,9 +11921,9 @@ packages: zod-error@1.5.0: resolution: {integrity: sha512-zzopKZ/skI9iXpqCEPj+iLCKl9b88E43ehcU+sbRoHuwGd9F1IDVGQ70TyO6kmfiRL1g4IXkjsXK+g1gLYl4WQ==} - zod-openapi@2.19.0: - resolution: {integrity: sha512-OUAAyBDPPwZ9u61i4k/LieXUzP2re8kFjqdNh2AvHjsyi/aRNz9leDAtMGcSoSzUT5xUeQoACJufBI6FzzZyxA==} - engines: {node: '>=16.11'} + zod-openapi@4.2.4: + resolution: {integrity: sha512-tsrQpbpqFCXqVXUzi3TPwFhuMtLN3oNZobOtYnK6/5VkXsNdnIgyNr4r8no4wmYluaxzN3F7iS+8xCW8BmMQ8g==} + engines: {node: '>=18'} peerDependencies: zod: ^3.21.4 @@ -25761,7 +25761,7 @@ snapshots: dependencies: zod: 3.22.4 - zod-openapi@2.19.0(zod@3.22.4): + zod-openapi@4.2.4(zod@3.22.4): dependencies: zod: 3.22.4