diff --git a/packages/ui/app/src/atoms/store.ts b/packages/ui/app/src/atoms/store.ts index 2ff6e1b416..82904e483c 100644 --- a/packages/ui/app/src/atoms/store.ts +++ b/packages/ui/app/src/atoms/store.ts @@ -1,3 +1,3 @@ -import { createStore } from "jotai"; +import { getDefaultStore } from "jotai"; -export const store = createStore(); +export const store = getDefaultStore(); diff --git a/packages/ui/app/src/auth/FernJWT.ts b/packages/ui/app/src/auth/FernJWT.ts index e684ba31d6..3680172b5e 100644 --- a/packages/ui/app/src/auth/FernJWT.ts +++ b/packages/ui/app/src/auth/FernJWT.ts @@ -1,5 +1,5 @@ import { SignJWT, jwtVerify } from "jose"; -import { AuthEdgeConfig, FernUser, FernUserSchema } from "./types"; +import { FernUser, FernUserSchema } from "./types"; // "user" is reserved for workos @@ -20,14 +20,6 @@ export async function verifyFernJWT(token: string, secret?: string, issuer?: str return FernUserSchema.parse(verified.payload.fern); } -export async function verifyFernJWTConfig(token: string, authConfig: AuthEdgeConfig | undefined): Promise { - if (authConfig?.type === "basic_token_verification") { - return verifyFernJWT(token, authConfig.secret, authConfig.issuer); - } else { - return verifyFernJWT(token); - } -} - const encoder = new TextEncoder(); function getJwtTokenSecret(secret?: string): Uint8Array { diff --git a/packages/ui/app/src/docs/NextApp.tsx b/packages/ui/app/src/docs/NextApp.tsx index 20c78b0a2d..2e2b135ad8 100644 --- a/packages/ui/app/src/docs/NextApp.tsx +++ b/packages/ui/app/src/docs/NextApp.tsx @@ -1,10 +1,9 @@ import { FernTooltipProvider, Toaster } from "@fern-ui/components"; import { EMPTY_OBJECT } from "@fern-ui/core-utils"; -import { Provider as JotaiProvider } from "jotai"; import type { AppProps } from "next/app"; import { ReactElement } from "react"; import { SWRConfig } from "swr"; -import { DocsProps, HydrateAtoms, store } from "../atoms"; +import { DocsProps, HydrateAtoms } from "../atoms"; import { FernErrorBoundary } from "../components/FernErrorBoundary"; import { LocalPreviewContextProvider } from "../contexts/local-preview"; import "../css/globals.scss"; @@ -22,36 +21,32 @@ export function NextApp({ Component, pageProps, router }: AppProps - - - - - - - - - - - - - + + + + + + + + + + + + ); } // local preview doesn't use getServerSideProps, so pageProps is always undefined export function LocalPreviewNextApp({ Component }: AppProps): ReactElement { return ( - - - - - - - - - - - + + + + + + + + + ); } diff --git a/packages/ui/docs-bundle/next.config.js b/packages/ui/docs-bundle/next.config.js index ca3163af7c..3011ade4cb 100644 --- a/packages/ui/docs-bundle/next.config.js +++ b/packages/ui/docs-bundle/next.config.js @@ -23,6 +23,10 @@ const DOCS_FILES_ALLOWLIST = [ }, ]; +// const DOCS_FILES_URLS = DOCS_FILES_ALLOWLIST.map( +// ({ protocol, hostname, port }) => `${protocol}://${hostname}${port ? `:${port}` : ""}`, +// ); + function isTruthy(value) { if (value == null) { return false; @@ -59,6 +63,92 @@ const nextConfig = { */ assetPrefix: cdnUri != null ? cdnUri.href : undefined, headers: async () => { + // const defaultSrc = ["'self'", "https://*.buildwithfern.com", "https://*.ferndocs.com", ...DOCS_FILES_URLS]; + + // const connectSrc = [ + // "'self'", + // "https://*.buildwithfern.com", + // "https://*.ferndocs.com", + // "wss://websocket.proxy.ferndocs.com", + // "https://*.algolia.net", + // "https://*.algolianet.com", + // "https://*.algolia.io", + // "https://*.posthog.com", + // "https://cdn.segment.com", + // "https://api.segment.io", + // "wss://api.getkoala.com", + // "https://www.google-analytics.com", + // "https://*.intercom.io", + // "wss://*.intercom.io", + // "https://*.fullstory.com", + // ]; + + // const scriptSrc = [ + // "'self'", + // "'unsafe-eval'", + // "'unsafe-inline'", + // "https://*.posthog.com", + // "https://cdn.segment.com", + // "https://www.googletagmanager.com", + // "https://*.intercomcdn.com", + // "https://*.intercom.io", + // "https://*.fullstory.com", + // ...DOCS_FILES_URLS, + // ]; + + // const styleSrc = ["'self'", "'unsafe-inline'", "https://cdn.jsdelivr.net"]; + + // const fontSrc = ["'self'", "data:", "https://*.intercomcdn.com", ...DOCS_FILES_URLS]; + + // if (cdnUri != null) { + // scriptSrc.push(`${cdnUri.origin}`); + // connectSrc.push(`${cdnUri.origin}`); + // styleSrc.push(`${cdnUri.origin}`); + // } + + // // enable vercel toolbar + // scriptSrc.push("https://vercel.live"); + // connectSrc.push("https://vercel.live"); + // connectSrc.push("wss://*.pusher.com"); + // styleSrc.push("https://vercel.live"); + // styleSrc.push("https://fonts.googleapis.com"); + + // const ContentSecurityPolicy = [ + // `default-src ${defaultSrc.join(" ")}`, + // `script-src ${scriptSrc.join(" ")}`, + // `style-src ${styleSrc.join(" ")}`, + // "img-src 'self' https: blob: data:", + // `connect-src ${connectSrc.join(" ")}`, + // "frame-src 'self' https:", + // "object-src 'none'", + // "base-uri 'self'", + // "form-action 'self'", + // "frame-ancestors 'none'", + // `font-src ${fontSrc.join(" ")}`, + // // "upgrade-insecure-requests", <-- this is ignored because Report-Only mode is enabled + // ]; + + // BEGIN CSP REPORT SUPPRESSION + // CSP reports to sentry have been disabled because they often come from downstream custom js + // that we can't do much about. This results in a very expensive sentry bill for very little value or marginal security. + // + // const reportUri = + // "https://o4507138224160768.ingest.sentry.io/api/4507148139495424/security/?sentry_key=216ad381a8f652e036b1833af58627e5"; + // + // const ReportTo = `{"group":"csp-endpoint","max_age":10886400,"endpoints":[{"url":"${reportUri}"}],"include_subdomains":true}`; + + // ContentSecurityPolicy.push("worker-src 'self' blob:"); + + // ContentSecurityPolicy.push(`report-uri ${reportUri}`); + // ContentSecurityPolicy.push("report-to csp-endpoint"); + + // const ContentSecurityHeaders = [ + // { key: "Content-Security-Policy-Report-Only", value: ContentSecurityPolicy.join("; ") }, + // // { key: "Report-To", value: ReportTo }, + // ]; + + // END CSP REPORT SUPPRESSION + const AccessControlHeaders = [ { key: "Access-Control-Allow-Origin", @@ -87,8 +177,107 @@ const nextConfig = { source: "/:prefix*/api/fern-docs/auth/:path*", headers: AccessControlHeaders, }, + // { + // source: "/:path*", + // headers: ContentSecurityHeaders, + // }, ]; }, + rewrites: async () => { + const HAS_FERN_DOCS_PREVIEW = { type: "cookie", key: "_fern_docs_preview", value: "(?.*)" }; + // const HAS_X_FORWARDED_HOST = { type: "header", key: "x-forwarded-host", value: "(?.*)" }; + const HAS_X_FERN_HOST = { type: "header", key: "x-fern-host", value: "(?.*)" }; + const HAS_HOST = { type: "host", value: "(?.*)" }; + + // The order of the following array is important. The first match will be used. + const WITH_MATCHED_HOST = [HAS_FERN_DOCS_PREVIEW, HAS_X_FERN_HOST, HAS_HOST]; + + const HAS_FERN_TOKEN = { type: "cookie", key: "fern_token" }; + const THREW_ERROR = { type: "query", key: "error", value: "true" }; + return { + beforeFiles: [ + /** + * while /_next/static routes are handled by the assetPrefix config, we need to handle the /_next/data routes separately + * when the user is hovering over a link, Next.js will prefetch the data route using `/_next/data` routes. We intercept + * the prefetch request at packages/ui/app/src/docs/NextApp.tsx and append the customer-defined basepath: + * + * i.e. /base/path/_next/data/* + * + * This rewrite rule will ensure that /base/path/_next/data/* is rewritten to /_next/data/* on the server + */ + { source: "/:prefix*/_next/:path*", destination: "/_next/:path*" }, + { source: "/:prefix*/api/fern-docs/:path*", destination: "/api/fern-docs/:path*" }, + { source: "/:prefix*/robots.txt", destination: "/api/fern-docs/robots.txt" }, + { source: "/:prefix*/sitemap.xml", destination: "/api/fern-docs/sitemap.xml" }, + /** + * Since we use cookie rewrites to determine if the path should be rewritten to /static or /dynamic, prefetch requests + * do not have access to these cookies, and will always be matched to /static. This rewrite rule will ensure that + * when the fern_token cookie is present, the /static route will be rewritten to /dynamic + */ + { + source: "/_next/data/:hash/static/:host/:path*", + has: [HAS_FERN_TOKEN], + destination: "/_next/data/:hash/dynamic/:host/:path*", + }, + /** + * This rewrite rule will ensure that when the `_fern_docs_preview` cookie is present, the /_next/data route will be + * rewritten to the host specified in the cookie. This is necessary for the PR Preview feature to work. + */ + { + source: "/_next/data/:hash/:subpath/:oldhost/:path*", + has: [HAS_FERN_DOCS_PREVIEW], + destination: "/_next/data/:hash/:subpath/:host/:path*", + }, + ], + afterFiles: [ + { source: "/_next/:path*", destination: "/_next/:path*" }, + { source: "/_vercel/:path*", destination: "/_vercel/:path*" }, + { source: "/robots.txt", destination: "/api/fern-docs/robots.txt" }, + { source: "/sitemap.xml", destination: "/api/fern-docs/sitemap.xml" }, + { source: "/:path*.rss", destination: "/api/fern-docs/changelog?format=rss&path=:path*" }, + { source: "/:path*.atom", destination: "/api/fern-docs/changelog?format=atom&path=:path*" }, + + // backwards compatibility with currently deployed FDR + { source: "/api/revalidate-all", destination: "/api/fern-docs/revalidate-all" }, + ], + fallback: [ + /** + * The following rewrite rules are used to determine if the path should be rewritten to /static or /dynamic + * On the presence of fern_token, or if the query contains error=true, the path will be rewritten to /dynamic + */ + ...WITH_MATCHED_HOST.map((HOST_RULE) => ({ + has: [HOST_RULE, HAS_FERN_TOKEN], + source: "/:path*", + destination: "/dynamic/:host/:path*", + })), + ...WITH_MATCHED_HOST.map((HOST_RULE) => ({ + has: [HOST_RULE, THREW_ERROR], + source: "/:path*", + destination: "/dynamic/:host/:path*", + })), + ...WITH_MATCHED_HOST.map((HOST_RULE) => ({ + has: [HOST_RULE], + source: "/:path*", + destination: "/static/:host/:path*", + })), + ...WITH_MATCHED_HOST.map((HOST_RULE) => ({ + has: [HOST_RULE, HAS_FERN_TOKEN], + source: "/", + destination: "/dynamic/:host/", + })), + ...WITH_MATCHED_HOST.map((HOST_RULE) => ({ + has: [HOST_RULE, THREW_ERROR], + source: "/", + destination: "/dynamic/:host/", + })), + ...WITH_MATCHED_HOST.map((HOST_RULE) => ({ + has: [HOST_RULE], + source: "/", + destination: "/static/:host/", + })), + ], + }; + }, images: { remotePatterns: DOCS_FILES_ALLOWLIST, path: cdnUri != null ? `${cdnUri.href}_next/image` : undefined, diff --git a/packages/ui/docs-bundle/src/middleware.ts b/packages/ui/docs-bundle/src/middleware.ts index bc087cbce1..bab1cfbf66 100644 --- a/packages/ui/docs-bundle/src/middleware.ts +++ b/packages/ui/docs-bundle/src/middleware.ts @@ -1,129 +1,13 @@ -// eslint-disable-next-line import/no-internal-modules -import { FernUser, getAuthEdgeConfig, verifyFernJWTConfig } from "@fern-ui/ui/auth"; -import { NextResponse, type MiddlewareConfig, type NextRequest } from "next/server"; -import urlJoin from "url-join"; -import { extractNextDataPathname } from "./utils/extractNextDataPathname"; +import { NextResponse, type NextRequest } from "next/server"; import { rewritePosthog } from "./utils/rewritePosthog"; -import { getXFernHostEdge } from "./utils/xFernHost"; -const API_FERN_DOCS_PATTERN = /^(?!\/api\/fern-docs\/).*(\/api\/fern-docs\/)/; -const CHANGELOG_PATTERN = /\.(rss|atom)$/; - -export async function middleware(request: NextRequest): Promise { - const xFernHost = getXFernHostEdge(request); - - /** - * Rewrite robots.txt - */ - if (request.nextUrl.pathname.endsWith("/robots.txt")) { - return NextResponse.rewrite(new URL("/api/fern-docs/robots.txt", request.url)); - } - - /** - * Rewrite sitemap.xml - */ - if (request.nextUrl.pathname.endsWith("/sitemap.xml")) { - return NextResponse.rewrite(new URL("/api/fern-docs/sitemap.xml", request.url)); - } - - /** - * Rewrite Posthog analytics ingestion - */ +export function middleware(request: NextRequest): NextResponse { if (request.nextUrl.pathname.includes("/api/fern-docs/analytics/posthog")) { return rewritePosthog(request); } - - /** - * Rewrite API routes to /api/fern-docs - */ - if (request.nextUrl.pathname.match(API_FERN_DOCS_PATTERN)) { - const pathname = request.nextUrl.pathname.replace(API_FERN_DOCS_PATTERN, "/api/fern-docs/"); - return NextResponse.rewrite(new URL(pathname, request.url)); - } - - /** - * Rewrite changelog rss and atom feeds - */ - const changelogFormat = request.nextUrl.pathname.match(CHANGELOG_PATTERN)?.[1]; - if (changelogFormat != null) { - const pathname = request.nextUrl.pathname.replace(new RegExp(`.${changelogFormat}$`), ""); - return NextResponse.rewrite( - new URL( - `/api/fern-docs/changelog?format=${changelogFormat}&path=${encodeURIComponent(pathname)}`, - request.url, - ), - ); - } - - const pathname = extractNextDataPathname(request.nextUrl.pathname); - - const fernToken = request.cookies.get("fern_token"); - - let user: FernUser | undefined = undefined; - - const authConfig = await getAuthEdgeConfig(xFernHost); - - // TODO: check if the site is SSO protected, and if so, redirect to the SSO provider - if (fernToken != null) { - try { - user = await verifyFernJWTConfig(fernToken.value, authConfig); - } catch (e) { - // eslint-disable-next-line no-console - console.error("Failed to verify fern_token", e); - } - } - - // using custom auth (e.g. qlty, propexo, etc) - if (authConfig != null && authConfig.type === "basic_token_verification" && user == null) { - const destination = new URL(authConfig.redirect); - destination.searchParams.set("state", urlJoin(`https://${xFernHost}`, pathname)); - return NextResponse.redirect(destination, { status: 302 }); - } - - /** - * error=true is a hack to force dynamic rendering when `_error.ts` is rendered. - * - * This is because: sometimes SSR'd markdown content throws an error during rendering, - * and we want to show a partially errored page to the user. - */ - const hasError = request.nextUrl.searchParams.get("error") === "true"; - - /** - * There are two types of pages in the docs bundle: - * - static = SSG pages - * - dynamic = SSR pages (because fern_token is present or there is an error) - */ - const isDynamic = user != null || hasError; - const prefix = isDynamic ? "/dynamic/" : "/static/"; - - /** - * Rewrite all other requests to /static/[host]/[[...slug]] or /dynamic/[host]/[[...slug]] - */ - const destination = new URL(urlJoin(prefix, xFernHost, pathname), request.url); - - /** - * Add __nextDataReq=1 query param to the destination URL if the request is for a nextjs data request - */ - if (request.nextUrl.pathname.includes("/_next/data/")) { - destination.searchParams.set("__nextDataReq", "1"); - } - - return NextResponse.rewrite(destination); + return NextResponse.next(); } -export const config: MiddlewareConfig = { - matcher: [ - /** - * Match all requests to posthog - */ - "/api/fern-docs/analytics/posthog/:path*", - /* - * Match all request paths except for the ones starting with: - * - api (API routes) - * - _next/static (static files) - * - _next/image (image optimization files) - * - favicon.ico (favicon file) - */ - "/((?!api/fern-docs|_next/static|_next/image|favicon.ico).*)", - ], +export const config = { + matcher: ["/api/fern-docs/analytics/posthog/:path*", "/:prefix*/api/fern-docs/analytics/posthog/:path*"], }; diff --git a/packages/ui/docs-bundle/src/pages/api/fern-docs/changelog.ts b/packages/ui/docs-bundle/src/pages/api/fern-docs/changelog.ts index 2e55663626..81d5b0f24f 100644 --- a/packages/ui/docs-bundle/src/pages/api/fern-docs/changelog.ts +++ b/packages/ui/docs-bundle/src/pages/api/fern-docs/changelog.ts @@ -5,6 +5,7 @@ import { assertNever } from "@fern-ui/core-utils"; import { getFrontmatter } from "@fern-ui/ui"; import { Feed, Item } from "feed"; import { NextApiRequest, NextApiResponse } from "next"; +import { NextResponse } from "next/server"; import { buildUrlFromApiNode } from "../../../utils/buildUrlFromApi"; import { loadWithUrl } from "../../../utils/loadWithUrl"; import { getXFernHostNode } from "../../../utils/xFernHost"; @@ -50,7 +51,7 @@ export default async function responseApiHandler(req: NextApiRequest, res: NextA const node = collector.slugMap.get(slug); if (node?.type !== "changelog") { - return res.status(404).end(); + return new NextResponse(null, { status: 404 }); } const link = `https://${xFernHost}/${node.slug}`; diff --git a/packages/ui/docs-bundle/src/pages/api/fern-docs/redirect.ts b/packages/ui/docs-bundle/src/pages/api/fern-docs/redirect.ts new file mode 100644 index 0000000000..57baf4d623 --- /dev/null +++ b/packages/ui/docs-bundle/src/pages/api/fern-docs/redirect.ts @@ -0,0 +1,31 @@ +import { NextRequest, NextResponse } from "next/server"; + +/** + * Why do we need this? + * + * This is a workaround for ISR pages that return a redirect to an external URL. + * When the page is built, NextJS will send a HEAD request to the destination URL to check if it is valid. + * This would add undesireable load to the destination server (e.g. the customer's server). + * + * By using this function, we can intercept the HEAD request but also return a 308 redirect to the destination URL. + */ + +export const runtime = "edge"; + +export default async function handler(req: NextRequest): Promise { + if (req.method !== "GET") { + // returns 405 on HEAD and other methods + return new NextResponse(null, { status: 405 }); + } + + const destination = req.nextUrl.searchParams.get("destination"); + if (destination == null) { + return new NextResponse(null, { status: 400 }); + } else if (destination.startsWith("http://") || destination.startsWith("https://")) { + return NextResponse.redirect(destination, { status: 308 }); + } else if (destination.startsWith("/")) { + return NextResponse.redirect(new URL(destination, req.nextUrl.origin).toString(), { status: 308 }); + } else { + return new NextResponse(null, { status: 400 }); + } +} diff --git a/packages/ui/docs-bundle/src/pages/dynamic/[host]/[[...slug]].tsx b/packages/ui/docs-bundle/src/pages/dynamic/[host]/[[...slug]].tsx index d4f958f1b7..491166c914 100644 --- a/packages/ui/docs-bundle/src/pages/dynamic/[host]/[[...slug]].tsx +++ b/packages/ui/docs-bundle/src/pages/dynamic/[host]/[[...slug]].tsx @@ -3,7 +3,6 @@ import { GetServerSideProps } from "next"; import { ComponentProps } from "react"; import { convertStaticToServerSidePropsResult } from "../../../utils/convertStaticToServerSidePropsResult"; import { getDocsPageProps, getDynamicDocsPageProps } from "../../../utils/getDocsPageProps"; -import { getNextPublicDocsDomain } from "../../../utils/xFernHost"; export default DocsPage; @@ -20,7 +19,7 @@ const getDocsServerSideProps: GetServerSideProps req, res, }) => { - const xFernHost = getNextPublicDocsDomain() ?? (params.host as string); + const xFernHost = process.env.NEXT_PUBLIC_DOCS_DOMAIN ?? (params.host as string); // eslint-disable-next-line no-console console.log(`[getDocsServerSideProps] host=${xFernHost}`); const slugArray = params.slug == null ? [] : Array.isArray(params.slug) ? params.slug : [params.slug]; diff --git a/packages/ui/docs-bundle/src/pages/static/[host]/[[...slug]].tsx b/packages/ui/docs-bundle/src/pages/static/[host]/[[...slug]].tsx index 5b679bdf86..96bdc955e6 100644 --- a/packages/ui/docs-bundle/src/pages/static/[host]/[[...slug]].tsx +++ b/packages/ui/docs-bundle/src/pages/static/[host]/[[...slug]].tsx @@ -2,13 +2,12 @@ import { DocsPage } from "@fern-ui/ui"; import { GetStaticPaths, GetStaticProps } from "next"; import { ComponentProps } from "react"; import { getDocsPageProps } from "../../../utils/getDocsPageProps"; -import { getNextPublicDocsDomain } from "../../../utils/xFernHost"; export default DocsPage; export const getStaticProps: GetStaticProps> = async (context) => { const { params = {} } = context; - const xFernHost = getNextPublicDocsDomain() ?? (params.host as string); + const xFernHost = process.env.NEXT_PUBLIC_DOCS_DOMAIN ?? (params.host as string); const slugArray = params.slug == null ? [] : Array.isArray(params.slug) ? params.slug : [params.slug]; return getDocsPageProps(xFernHost, slugArray); diff --git a/packages/ui/docs-bundle/src/utils/extractNextDataPathname.ts b/packages/ui/docs-bundle/src/utils/extractNextDataPathname.ts deleted file mode 100644 index f0a178c6b0..0000000000 --- a/packages/ui/docs-bundle/src/utils/extractNextDataPathname.ts +++ /dev/null @@ -1,15 +0,0 @@ -/** - * when prefetching `/learn/_next/data/build-id/learn/home.json`, sometimes the pathname is interpreted as - * `/learn/_next/data/build-id/learn/_next/data/build-id/learn/home.json.json`. - * - * This is a bug in Next.js that we need to work around. - * - * In both cases, we want to extract `/learn/home` - */ -export function extractNextDataPathname(pathname: string): string { - return ( - pathname.match(/\/_next\/data\/.*\/_next\/data\/[^/]*(\/.*)\.json.json/)?.[1] ?? - pathname.match(/\/_next\/data\/[^/]*(\/.*)\.json/)?.[1] ?? - pathname - ); -} diff --git a/packages/ui/docs-bundle/src/utils/getDocsPageProps.ts b/packages/ui/docs-bundle/src/utils/getDocsPageProps.ts index c4c36d90bd..34ff7dd375 100644 --- a/packages/ui/docs-bundle/src/utils/getDocsPageProps.ts +++ b/packages/ui/docs-bundle/src/utils/getDocsPageProps.ts @@ -19,7 +19,7 @@ import { serializeMdx, setMdxBundler, } from "@fern-ui/ui"; -import { FernUser, getAPIKeyInjectionConfigNode, getAuthEdgeConfig, verifyFernJWTConfig } from "@fern-ui/ui/auth"; +import { FernUser, getAPIKeyInjectionConfigNode, getAuthEdgeConfig, verifyFernJWT } from "@fern-ui/ui/auth"; import { getMdxBundler } from "@fern-ui/ui/bundlers"; import type { GetServerSidePropsResult, GetStaticPropsResult, Redirect } from "next"; import { NextApiRequestCookies } from "next/dist/server/api-utils"; @@ -60,6 +60,19 @@ export async function getDocsPageProps( return { notFound: true }; } + const config = await getAuthEdgeConfig(xFernHost); + if (config != null && config.type === "basic_token_verification") { + const destination = new URL(config.redirect); + destination.searchParams.set("state", urlJoin(`https://${xFernHost}`, `/${slug.join("/")}`)); + return { + redirect: { + // TODO: this will break if the docs tenant uses basepaths + destination: `/api/fern-docs/redirect?destination=${encodeURIComponent(destination.toString())}`, + permanent: false, + }, + }; + } + const pathname = decodeURI(slug != null ? slug.join("/") : ""); const url = buildUrl({ host: xFernHost, pathname }); // eslint-disable-next-line no-console @@ -103,7 +116,27 @@ export async function getDynamicDocsPageProps( try { const config = await getAuthEdgeConfig(xFernHost); - const user: FernUser | undefined = await verifyFernJWTConfig(cookies.fern_token, config); + let user: FernUser | undefined = undefined; + + // using custom auth (e.g. qlty, propexo, etc) + if (config?.type === "basic_token_verification") { + try { + user = await verifyFernJWT(cookies.fern_token, config.secret, config.issuer); + } catch (error) { + // eslint-disable-next-line no-console + console.error(error); + const destination = new URL(config.redirect); + destination.searchParams.set("state", urlJoin(`https://${xFernHost}`, `/${slug.join("/")}`)); + return { + redirect: { + destination: `/api/fern-docs/redirect?destination=${encodeURIComponent(destination.toString())}`, + permanent: false, + }, + }; + } + } else { + user = await verifyFernJWT(cookies.fern_token); + } // SSO if (user.partner === "workos") { diff --git a/packages/ui/docs-bundle/src/utils/xFernHost.ts b/packages/ui/docs-bundle/src/utils/xFernHost.ts index bc0ccd7c6c..708154d3d5 100644 --- a/packages/ui/docs-bundle/src/utils/xFernHost.ts +++ b/packages/ui/docs-bundle/src/utils/xFernHost.ts @@ -1,4 +1,3 @@ -import { withDefaultProtocol } from "@fern-ui/core-utils"; import type { NextApiRequest } from "next"; import type { NextRequest } from "next/server"; @@ -13,26 +12,10 @@ import type { NextRequest } from "next/server"; * _fern_docs_preview is used for previewing the docs. */ -export function getNextPublicDocsDomain(): string | undefined { - try { - const domain = process.env.NEXT_PUBLIC_DOCS_DOMAIN; - - if (domain == null) { - return undefined; - } - - return new URL(withDefaultProtocol(domain)).host; - } catch (e) { - // eslint-disable-next-line no-console - console.error(e); - return undefined; - } -} - export function getXFernHostEdge(req: NextRequest, useSearchParams = false): string { const hosts = [ useSearchParams ? req.nextUrl.searchParams.get("host") : undefined, - getNextPublicDocsDomain(), + process.env.NEXT_PUBLIC_DOCS_DOMAIN, req.cookies.get("_fern_docs_preview")?.value, // req.headers.get("x-forwarded-host"), req.headers.get("x-fern-host"), @@ -52,7 +35,7 @@ export function getXFernHostEdge(req: NextRequest, useSearchParams = false): str export function getXFernHostNode(req: NextApiRequest, useSearchParams = false): string { const hosts = [ useSearchParams ? req.query["host"] : undefined, - getNextPublicDocsDomain(), + process.env.NEXT_PUBLIC_DOCS_DOMAIN, req.cookies["_fern_docs_preview"], // req.headers["x-forwarded-host"], req.headers["x-fern-host"],