From 3b73f63cb4270f23562ba24187e7d2948689c4da Mon Sep 17 00:00:00 2001 From: Andrew Jiang Date: Thu, 10 Oct 2024 17:04:23 -0400 Subject: [PATCH 1/3] feat: add state to logout url --- .../converters/db/convertAPIDefinitionToDb.ts | 6 ++-- packages/ui/docs-bundle/src/middleware.ts | 6 ++-- packages/ui/docs-bundle/src/pages/_error.tsx | 2 +- .../[api]/endpoint/[endpoint].ts | 4 +-- .../api-definition/[api]/webhook/[webhook].ts | 4 +-- .../[api]/websocket/[websocket].ts | 4 +-- .../api/fern-docs/auth/api-key-injection.ts | 4 +-- .../src/pages/api/fern-docs/auth/callback.ts | 16 ++++------ .../pages/api/fern-docs/auth/jwt/callback.ts | 16 ++++------ .../src/pages/api/fern-docs/auth/logout.ts | 18 +++++++----- .../src/pages/api/fern-docs/changelog.ts | 4 +-- .../src/pages/api/fern-docs/feature-flags.ts | 4 +-- .../fern-docs/integrations/launchdarkly.ts | 6 ++-- .../integrations/launchdarkly/identify.ts | 4 +-- .../pages/api/fern-docs/oauth/ory/callback.ts | 18 +++++------- .../api/fern-docs/oauth/webflow/callback.ts | 18 +++++------- .../pages/api/fern-docs/revalidate-all/v3.ts | 4 +-- .../pages/api/fern-docs/revalidate-all/v4.ts | 4 +-- .../pages/api/fern-docs/revalidate-path.ts | 4 +-- .../src/pages/api/fern-docs/robots.txt.ts | 4 +-- .../src/pages/api/fern-docs/search.ts | 4 +-- .../src/pages/api/fern-docs/search/cohere.ts | 4 +-- .../src/pages/api/fern-docs/sitemap.xml.ts | 4 +-- .../{[host] => [domain]}/[[...slug]].tsx | 6 ++-- .../{[host] => [domain]}/[[...slug]].tsx | 8 +++-- .../src/server/LoadDocsPerformanceTracker.ts | 6 ++-- .../src/server/getDocsPageProps.ts | 13 +++++---- .../src/server/getDynamicDocsPageProps.ts | 7 +++-- .../ui/docs-bundle/src/server/pageRoutes.ts | 10 +++---- .../src/server/redirectWithLoginError.ts | 12 ++++++++ packages/ui/docs-bundle/src/server/safeUrl.ts | 5 ++-- .../src/server/withInitialProps.ts | 29 +++++++++++++------ .../docs-bundle/src/server/xfernhost/edge.ts | 6 ++-- .../docs-bundle/src/server/xfernhost/node.ts | 15 +++++++++- 34 files changed, 155 insertions(+), 124 deletions(-) rename packages/ui/docs-bundle/src/pages/dynamic/{[host] => [domain]}/[[...slug]].tsx (67%) rename packages/ui/docs-bundle/src/pages/static/{[host] => [domain]}/[[...slug]].tsx (77%) create mode 100644 packages/ui/docs-bundle/src/server/redirectWithLoginError.ts diff --git a/packages/fdr-sdk/src/converters/db/convertAPIDefinitionToDb.ts b/packages/fdr-sdk/src/converters/db/convertAPIDefinitionToDb.ts index 3c4addeea0..aa47b05dd5 100644 --- a/packages/fdr-sdk/src/converters/db/convertAPIDefinitionToDb.ts +++ b/packages/fdr-sdk/src/converters/db/convertAPIDefinitionToDb.ts @@ -677,9 +677,9 @@ class ApiDefinitionTransformationContext { for (const environment of environments) { const entry = this.uniqueBaseUrls[environment.id]; if (entry != null) { - entry.add(this.getHost(environment.baseUrl)); + entry.add(this.getHostEdge(environment.baseUrl)); } else { - this.uniqueBaseUrls[environment.id] = new Set([this.getHost(environment.baseUrl)]); + this.uniqueBaseUrls[environment.id] = new Set([this.getHostEdge(environment.baseUrl)]); } } } @@ -693,7 +693,7 @@ class ApiDefinitionTransformationContext { return false; } - private getHost(url: string) { + private getHostEdge(url: string) { const parsedBaseUrl = new URL(url); return parsedBaseUrl.host; } diff --git a/packages/ui/docs-bundle/src/middleware.ts b/packages/ui/docs-bundle/src/middleware.ts index d26edf488b..b50b2628f0 100644 --- a/packages/ui/docs-bundle/src/middleware.ts +++ b/packages/ui/docs-bundle/src/middleware.ts @@ -1,7 +1,7 @@ import { extractBuildId, extractNextDataPathname } from "@/server/extractNextDataPathname"; import { getPageRoute, getPageRouteMatch, getPageRoutePath } from "@/server/pageRoutes"; import { rewritePosthog } from "@/server/rewritePosthog"; -import { getXFernHostEdge } from "@/server/xfernhost/edge"; +import { getDocsDomainEdge } from "@/server/xfernhost/edge"; import { withDefaultProtocol } from "@fern-api/ui-core-utils"; import type { FernUser } from "@fern-ui/fern-docs-auth"; import { getAuthEdgeConfig } from "@fern-ui/fern-docs-edge-config"; @@ -16,7 +16,7 @@ const API_FERN_DOCS_PATTERN = /^(?!\/api\/fern-docs\/).*(\/api\/fern-docs\/)/; const CHANGELOG_PATTERN = /\.(rss|atom)$/; export const middleware: NextMiddleware = async (request) => { - const xFernHost = getXFernHostEdge(request); + const xFernHost = getDocsDomainEdge(request); const nextUrl = request.nextUrl.clone(); const headers = new Headers(request.headers); @@ -138,7 +138,7 @@ export const middleware: NextMiddleware = async (request) => { } /** - * Rewrite all other requests to /static/[host]/[[...slug]] or /dynamic/[host]/[[...slug]] + * Rewrite all other requests to /static/[domain]/[[...slug]] or /dynamic/[domain]/[[...slug]] */ nextUrl.pathname = getPageRoute(!isDynamic, xFernHost, pathname); diff --git a/packages/ui/docs-bundle/src/pages/_error.tsx b/packages/ui/docs-bundle/src/pages/_error.tsx index 3addf95fd6..c277bd7dbe 100644 --- a/packages/ui/docs-bundle/src/pages/_error.tsx +++ b/packages/ui/docs-bundle/src/pages/_error.tsx @@ -3,7 +3,7 @@ import Error, { ErrorProps } from "next/error"; import { ReactElement } from "react"; export function parseResolvedUrl(resolvedUrl: string): string { - // if resolvedUrl is `/static/[host]/[...slug]` or `/dynamic/[host]/[..slug]` then return '/[...slug]` + // if resolvedUrl is `/static/[domain]/[...slug]` or `/dynamic/[domain]/[..slug]` then return '/[...slug]` const match = resolvedUrl.match(/\/(static|dynamic)\/[^/]+(.*)/); return match?.[2] ?? resolvedUrl; } diff --git a/packages/ui/docs-bundle/src/pages/api/fern-docs/api-definition/[api]/endpoint/[endpoint].ts b/packages/ui/docs-bundle/src/pages/api/fern-docs/api-definition/[api]/endpoint/[endpoint].ts index 7f100e2724..37dc7160f5 100644 --- a/packages/ui/docs-bundle/src/pages/api/fern-docs/api-definition/[api]/endpoint/[endpoint].ts +++ b/packages/ui/docs-bundle/src/pages/api/fern-docs/api-definition/[api]/endpoint/[endpoint].ts @@ -1,4 +1,4 @@ -import { getXFernHostNode } from "@/server/xfernhost/node"; +import { getDocsDomainNode } from "@/server/xfernhost/node"; import * as ApiDefinition from "@fern-api/fdr-sdk/api-definition"; import { getFeatureFlags } from "@fern-ui/fern-docs-edge-config"; import { ApiDefinitionLoader } from "@fern-ui/fern-docs-server"; @@ -6,7 +6,7 @@ import { getMdxBundler } from "@fern-ui/ui/bundlers"; import { NextApiHandler, NextApiResponse } from "next"; const resolveApiHandler: NextApiHandler = async (req, res: NextApiResponse) => { - const xFernHost = getXFernHostNode(req); + const xFernHost = getDocsDomainNode(req); const { api, endpoint } = req.query; if (req.method !== "GET" || typeof api !== "string" || typeof endpoint !== "string") { res.status(400).end(); diff --git a/packages/ui/docs-bundle/src/pages/api/fern-docs/api-definition/[api]/webhook/[webhook].ts b/packages/ui/docs-bundle/src/pages/api/fern-docs/api-definition/[api]/webhook/[webhook].ts index 73a3b55fd6..b908b9bfb7 100644 --- a/packages/ui/docs-bundle/src/pages/api/fern-docs/api-definition/[api]/webhook/[webhook].ts +++ b/packages/ui/docs-bundle/src/pages/api/fern-docs/api-definition/[api]/webhook/[webhook].ts @@ -1,4 +1,4 @@ -import { getXFernHostNode } from "@/server/xfernhost/node"; +import { getDocsDomainNode } from "@/server/xfernhost/node"; import * as ApiDefinition from "@fern-api/fdr-sdk/api-definition"; import { getFeatureFlags } from "@fern-ui/fern-docs-edge-config"; import { ApiDefinitionLoader } from "@fern-ui/fern-docs-server"; @@ -6,7 +6,7 @@ import { getMdxBundler } from "@fern-ui/ui/bundlers"; import { NextApiHandler, NextApiResponse } from "next"; const resolveApiHandler: NextApiHandler = async (req, res: NextApiResponse) => { - const xFernHost = getXFernHostNode(req); + const xFernHost = getDocsDomainNode(req); const { api, webhook } = req.query; if (req.method !== "GET" || typeof api !== "string" || typeof webhook !== "string") { res.status(400).end(); diff --git a/packages/ui/docs-bundle/src/pages/api/fern-docs/api-definition/[api]/websocket/[websocket].ts b/packages/ui/docs-bundle/src/pages/api/fern-docs/api-definition/[api]/websocket/[websocket].ts index 428e159177..8605ea7662 100644 --- a/packages/ui/docs-bundle/src/pages/api/fern-docs/api-definition/[api]/websocket/[websocket].ts +++ b/packages/ui/docs-bundle/src/pages/api/fern-docs/api-definition/[api]/websocket/[websocket].ts @@ -1,4 +1,4 @@ -import { getXFernHostNode } from "@/server/xfernhost/node"; +import { getDocsDomainNode } from "@/server/xfernhost/node"; import * as ApiDefinition from "@fern-api/fdr-sdk/api-definition"; import { getFeatureFlags } from "@fern-ui/fern-docs-edge-config"; import { ApiDefinitionLoader } from "@fern-ui/fern-docs-server"; @@ -6,7 +6,7 @@ import { getMdxBundler } from "@fern-ui/ui/bundlers"; import { NextApiHandler, NextApiResponse } from "next"; const resolveApiHandler: NextApiHandler = async (req, res: NextApiResponse) => { - const xFernHost = getXFernHostNode(req); + const xFernHost = getDocsDomainNode(req); const { api, websocket } = req.query; if (req.method !== "GET" || typeof api !== "string" || typeof websocket !== "string") { res.status(400).end(); diff --git a/packages/ui/docs-bundle/src/pages/api/fern-docs/auth/api-key-injection.ts b/packages/ui/docs-bundle/src/pages/api/fern-docs/auth/api-key-injection.ts index 2657db009b..821211d5fd 100644 --- a/packages/ui/docs-bundle/src/pages/api/fern-docs/auth/api-key-injection.ts +++ b/packages/ui/docs-bundle/src/pages/api/fern-docs/auth/api-key-injection.ts @@ -1,7 +1,7 @@ import { OAuth2Client } from "@/server/auth/OAuth2Client"; import { getAPIKeyInjectionConfig } from "@/server/auth/getApiKeyInjectionConfig"; import { withSecureCookie } from "@/server/auth/withSecure"; -import { getXFernHostEdge } from "@/server/xfernhost/edge"; +import { getDocsDomainEdge } from "@/server/xfernhost/edge"; import { APIKeyInjectionConfig, OryAccessTokenSchema } from "@fern-ui/fern-docs-auth"; import { getAuthEdgeConfig } from "@fern-ui/fern-docs-edge-config"; import { COOKIE_FERN_TOKEN } from "@fern-ui/fern-docs-utils"; @@ -12,7 +12,7 @@ import type { OauthScope } from "webflow-api/api/types/OAuthScope"; export const runtime = "edge"; export default async function handler(req: NextRequest): Promise> { - const domain = getXFernHostEdge(req); + const domain = getDocsDomainEdge(req); const edgeConfig = await getAuthEdgeConfig(domain); // assume that if the edge config is set for webflow, api key injection is always enabled diff --git a/packages/ui/docs-bundle/src/pages/api/fern-docs/auth/callback.ts b/packages/ui/docs-bundle/src/pages/api/fern-docs/auth/callback.ts index 0de1eed4c7..5e1b56c84c 100644 --- a/packages/ui/docs-bundle/src/pages/api/fern-docs/auth/callback.ts +++ b/packages/ui/docs-bundle/src/pages/api/fern-docs/auth/callback.ts @@ -1,8 +1,9 @@ import { signFernJWT } from "@/server/auth/FernJWT"; import { withSecureCookie } from "@/server/auth/withSecure"; +import { redirectWithLoginError } from "@/server/redirectWithLoginError"; import { safeUrl } from "@/server/safeUrl"; import { getWorkOS, getWorkOSClientId } from "@/server/workos"; -import { getXFernHostEdge, getXFernHostHeaderFallbackOrigin } from "@/server/xfernhost/edge"; +import { getDocsDomainEdge, getHostEdge } from "@/server/xfernhost/edge"; import { withDefaultProtocol } from "@fern-api/ui-core-utils"; import { FernUser } from "@fern-ui/fern-docs-auth"; import { getAuthEdgeConfig } from "@fern-ui/fern-docs-edge-config"; @@ -11,25 +12,18 @@ import { NextRequest, NextResponse } from "next/server"; export const runtime = "edge"; -function redirectWithLoginError(location: string, errorMessage: string): NextResponse { - const url = new URL(location); - url.searchParams.set("loginError", errorMessage); - // TODO: validate allowlist of domains to prevent open redirects - return NextResponse.redirect(url.toString()); -} - export default async function GET(req: NextRequest): Promise { if (req.method !== "GET") { return new NextResponse(null, { status: 405 }); } - const domain = getXFernHostEdge(req); + const domain = getDocsDomainEdge(req); // The authorization code returned by AuthKit const code = req.nextUrl.searchParams.get("code"); const state = req.nextUrl.searchParams.get("state"); const error = req.nextUrl.searchParams.get("error"); const error_description = req.nextUrl.searchParams.get("error_description"); - const redirectLocation = safeUrl(state) ?? withDefaultProtocol(getXFernHostHeaderFallbackOrigin(req)); + const redirectLocation = safeUrl(state) ?? safeUrl(withDefaultProtocol(getHostEdge(req))); if (error != null) { return redirectWithLoginError(redirectLocation, error_description ?? error); @@ -76,7 +70,7 @@ export default async function GET(req: NextRequest): Promise { const token = await signFernJWT(fernUser, user); // TODO: validate allowlist of domains to prevent open redirects - const res = NextResponse.redirect(redirectLocation); + const res = redirectLocation ? NextResponse.redirect(redirectLocation) : NextResponse.next(); res.cookies.set(COOKIE_FERN_TOKEN, token, withSecureCookie()); return res; } catch (error) { diff --git a/packages/ui/docs-bundle/src/pages/api/fern-docs/auth/jwt/callback.ts b/packages/ui/docs-bundle/src/pages/api/fern-docs/auth/jwt/callback.ts index 0b536d2a0c..e46467a029 100644 --- a/packages/ui/docs-bundle/src/pages/api/fern-docs/auth/jwt/callback.ts +++ b/packages/ui/docs-bundle/src/pages/api/fern-docs/auth/jwt/callback.ts @@ -1,7 +1,8 @@ import { verifyFernJWTConfig } from "@/server/auth/FernJWT"; import { withSecureCookie } from "@/server/auth/withSecure"; +import { redirectWithLoginError } from "@/server/redirectWithLoginError"; import { safeUrl } from "@/server/safeUrl"; -import { getXFernHostEdge, getXFernHostHeaderFallbackOrigin } from "@/server/xfernhost/edge"; +import { getDocsDomainEdge, getHostEdge } from "@/server/xfernhost/edge"; import { withDefaultProtocol } from "@fern-api/ui-core-utils"; import { getAuthEdgeConfig } from "@fern-ui/fern-docs-edge-config"; import { COOKIE_FERN_TOKEN } from "@fern-ui/fern-docs-utils"; @@ -9,25 +10,18 @@ import { NextRequest, NextResponse } from "next/server"; export const runtime = "edge"; -function redirectWithLoginError(location: string, errorMessage: string): NextResponse { - const url = new URL(location); - url.searchParams.set("loginError", errorMessage); - // TODO: validate allowlist of domains to prevent open redirects - return NextResponse.redirect(url.toString()); -} - export default async function handler(req: NextRequest): Promise { if (req.method !== "GET") { return new NextResponse(null, { status: 405 }); } - const domain = getXFernHostEdge(req); + const domain = getDocsDomainEdge(req); const edgeConfig = await getAuthEdgeConfig(domain); // since we expect the callback to be redirected to, the token will be in the query params const token = req.nextUrl.searchParams.get(COOKIE_FERN_TOKEN); const state = req.nextUrl.searchParams.get("state"); - const redirectLocation = safeUrl(state) ?? withDefaultProtocol(getXFernHostHeaderFallbackOrigin(req)); + const redirectLocation = safeUrl(state) ?? safeUrl(withDefaultProtocol(getHostEdge(req))); if (edgeConfig?.type !== "basic_token_verification" || token == null) { // eslint-disable-next-line no-console @@ -39,7 +33,7 @@ export default async function handler(req: NextRequest): Promise { await verifyFernJWTConfig(token, edgeConfig); // TODO: validate allowlist of domains to prevent open redirects - const res = NextResponse.redirect(redirectLocation); + const res = redirectLocation ? NextResponse.redirect(redirectLocation) : NextResponse.next(); res.cookies.set(COOKIE_FERN_TOKEN, token, withSecureCookie()); return res; } catch (e) { diff --git a/packages/ui/docs-bundle/src/pages/api/fern-docs/auth/logout.ts b/packages/ui/docs-bundle/src/pages/api/fern-docs/auth/logout.ts index 69a5347d48..69b8d92d26 100644 --- a/packages/ui/docs-bundle/src/pages/api/fern-docs/auth/logout.ts +++ b/packages/ui/docs-bundle/src/pages/api/fern-docs/auth/logout.ts @@ -1,5 +1,5 @@ import { safeUrl } from "@/server/safeUrl"; -import { getXFernHostEdge, getXFernHostHeaderFallbackOrigin } from "@/server/xfernhost/edge"; +import { getDocsDomainEdge, getHostEdge } from "@/server/xfernhost/edge"; import { withDefaultProtocol } from "@fern-api/ui-core-utils"; import { getAuthEdgeConfig } from "@fern-ui/fern-docs-edge-config"; import { COOKIE_ACCESS_TOKEN, COOKIE_FERN_TOKEN, COOKIE_REFRESH_TOKEN } from "@fern-ui/fern-docs-utils"; @@ -8,17 +8,21 @@ import { NextRequest, NextResponse } from "next/server"; export const runtime = "edge"; export default async function GET(req: NextRequest): Promise { - const domain = getXFernHostEdge(req); + const domain = getDocsDomainEdge(req); const authConfig = await getAuthEdgeConfig(domain); - const logoutUrl = authConfig?.type === "basic_token_verification" ? authConfig.logout : undefined; + const logoutUrl = authConfig?.type === "basic_token_verification" ? safeUrl(authConfig.logout) : undefined; - const state = req.nextUrl.searchParams.get("state"); + const state = safeUrl(req.nextUrl.searchParams.get("state")); - const redirectLocation = - safeUrl(logoutUrl) ?? safeUrl(state) ?? withDefaultProtocol(getXFernHostHeaderFallbackOrigin(req)); + // if logout url is provided, append the state to it before redirecting + if (logoutUrl != null && state != null) { + logoutUrl?.searchParams.set("state", state.toString()); + } - const res = NextResponse.redirect(redirectLocation); + const redirectLocation = logoutUrl ?? state ?? safeUrl(withDefaultProtocol(getHostEdge(req))); + + const res = redirectLocation ? NextResponse.redirect(redirectLocation) : NextResponse.next(); res.cookies.delete(COOKIE_FERN_TOKEN); res.cookies.delete(COOKIE_ACCESS_TOKEN); res.cookies.delete(COOKIE_REFRESH_TOKEN); 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 7fd7025dc9..596bdea257 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 @@ -1,5 +1,5 @@ import { DocsLoader } from "@/server/DocsLoader"; -import { getXFernHostNode } from "@/server/xfernhost/node"; +import { getDocsDomainNode } from "@/server/xfernhost/node"; import type { DocsV1Read } from "@fern-api/fdr-sdk/client/types"; import * as FernNavigation from "@fern-api/fdr-sdk/navigation"; import { NodeCollector } from "@fern-api/fdr-sdk/navigation"; @@ -24,7 +24,7 @@ export default async function responseApiHandler(req: NextApiRequest, res: NextA return res.status(400).end(); } - const xFernHost = getXFernHostNode(req); + const xFernHost = getDocsDomainNode(req); const fernToken = req.cookies[COOKIE_FERN_TOKEN]; const loader = DocsLoader.for(xFernHost, fernToken); diff --git a/packages/ui/docs-bundle/src/pages/api/fern-docs/feature-flags.ts b/packages/ui/docs-bundle/src/pages/api/fern-docs/feature-flags.ts index 35164afaad..f5279005b3 100644 --- a/packages/ui/docs-bundle/src/pages/api/fern-docs/feature-flags.ts +++ b/packages/ui/docs-bundle/src/pages/api/fern-docs/feature-flags.ts @@ -1,4 +1,4 @@ -import { getXFernHostEdge } from "@/server/xfernhost/edge"; +import { getDocsDomainEdge } from "@/server/xfernhost/edge"; import { getFeatureFlags } from "@fern-ui/fern-docs-edge-config"; import { FeatureFlags } from "@fern-ui/fern-docs-utils"; import { NextRequest, NextResponse } from "next/server"; @@ -6,6 +6,6 @@ import { NextRequest, NextResponse } from "next/server"; export const runtime = "edge"; export default async function handler(req: NextRequest): Promise> { - const domain = getXFernHostEdge(req); + const domain = getDocsDomainEdge(req); return NextResponse.json(await getFeatureFlags(domain)); } diff --git a/packages/ui/docs-bundle/src/pages/api/fern-docs/integrations/launchdarkly.ts b/packages/ui/docs-bundle/src/pages/api/fern-docs/integrations/launchdarkly.ts index 53d0891703..22055690e4 100644 --- a/packages/ui/docs-bundle/src/pages/api/fern-docs/integrations/launchdarkly.ts +++ b/packages/ui/docs-bundle/src/pages/api/fern-docs/integrations/launchdarkly.ts @@ -1,5 +1,5 @@ import { verifyFernJWTConfig } from "@/server/auth/FernJWT"; -import { getXFernHostEdge } from "@/server/xfernhost/edge"; +import { getDocsDomainEdge } from "@/server/xfernhost/edge"; import { LaunchDarklyEdgeConfig, getAuthEdgeConfig, getLaunchDarklySettings } from "@fern-ui/fern-docs-edge-config"; import { COOKIE_EMAIL, COOKIE_FERN_TOKEN } from "@fern-ui/fern-docs-utils"; import { randomUUID } from "crypto"; @@ -24,7 +24,7 @@ interface LaunchDarklyInfo { } export default async function handler(req: NextRequest): Promise> { - const domain = getXFernHostEdge(req); + const domain = getDocsDomainEdge(req); const config = await safeGetLaunchDarklySettings(domain); const clientSideId = config?.["client-side-id"]; @@ -68,7 +68,7 @@ async function getUserContext(req: NextRequest): Promise { const email = req.nextUrl.searchParams.get(COOKIE_EMAIL); // TODO: validate allowlist of domains to prevent open redirects - const res = NextResponse.redirect(withDefaultProtocol(getXFernHostHeaderFallbackOrigin(req))); + const res = NextResponse.redirect(withDefaultProtocol(getHostEdge(req))); if (email) { res.cookies.set({ name: COOKIE_EMAIL, value: email }); diff --git a/packages/ui/docs-bundle/src/pages/api/fern-docs/oauth/ory/callback.ts b/packages/ui/docs-bundle/src/pages/api/fern-docs/oauth/ory/callback.ts index 1fdc7e70ed..a267583638 100644 --- a/packages/ui/docs-bundle/src/pages/api/fern-docs/oauth/ory/callback.ts +++ b/packages/ui/docs-bundle/src/pages/api/fern-docs/oauth/ory/callback.ts @@ -1,8 +1,9 @@ import { signFernJWT } from "@/server/auth/FernJWT"; import { OAuth2Client } from "@/server/auth/OAuth2Client"; import { withSecureCookie } from "@/server/auth/withSecure"; +import { redirectWithLoginError } from "@/server/redirectWithLoginError"; import { safeUrl } from "@/server/safeUrl"; -import { getXFernHostEdge, getXFernHostHeaderFallbackOrigin } from "@/server/xfernhost/edge"; +import { getDocsDomainEdge, getHostEdge } from "@/server/xfernhost/edge"; import { withDefaultProtocol } from "@fern-api/ui-core-utils"; import { FernUser, OryAccessTokenSchema } from "@fern-ui/fern-docs-auth"; import { getAuthEdgeConfig } from "@fern-ui/fern-docs-edge-config"; @@ -11,25 +12,22 @@ import { NextRequest, NextResponse } from "next/server"; export const runtime = "edge"; -function redirectWithLoginError(location: string, errorMessage: string): NextResponse { - const url = new URL(location); - url.searchParams.set("loginError", errorMessage); - // TODO: validate allowlist of domains to prevent open redirects - return NextResponse.redirect(url.toString()); -} - export default async function GET(req: NextRequest): Promise { if (req.method !== "GET") { return new NextResponse(null, { status: 405 }); } - const domain = getXFernHostEdge(req); + const domain = getDocsDomainEdge(req); const code = req.nextUrl.searchParams.get("code"); const state = req.nextUrl.searchParams.get("state"); const error = req.nextUrl.searchParams.get("error"); const error_description = req.nextUrl.searchParams.get("error_description"); - const redirectLocation = safeUrl(state) ?? withDefaultProtocol(getXFernHostHeaderFallbackOrigin(req)); + const redirectLocation = safeUrl(state) ?? safeUrl(withDefaultProtocol(getHostEdge(req))); + + if (redirectLocation == null) { + return new NextResponse(null, { status: 500 }); + } if (error != null) { // eslint-disable-next-line no-console diff --git a/packages/ui/docs-bundle/src/pages/api/fern-docs/oauth/webflow/callback.ts b/packages/ui/docs-bundle/src/pages/api/fern-docs/oauth/webflow/callback.ts index ffc1c100f3..c5d47910e9 100644 --- a/packages/ui/docs-bundle/src/pages/api/fern-docs/oauth/webflow/callback.ts +++ b/packages/ui/docs-bundle/src/pages/api/fern-docs/oauth/webflow/callback.ts @@ -1,6 +1,7 @@ import { withSecureCookie } from "@/server/auth/withSecure"; +import { redirectWithLoginError } from "@/server/redirectWithLoginError"; import { safeUrl } from "@/server/safeUrl"; -import { getXFernHostEdge, getXFernHostHeaderFallbackOrigin } from "@/server/xfernhost/edge"; +import { getDocsDomainEdge, getHostEdge } from "@/server/xfernhost/edge"; import { withDefaultProtocol } from "@fern-api/ui-core-utils"; import { getAuthEdgeConfig } from "@fern-ui/fern-docs-edge-config"; import { NextRequest, NextResponse } from "next/server"; @@ -8,25 +9,22 @@ import { WebflowClient } from "webflow-api"; export const runtime = "edge"; -function redirectWithLoginError(location: string, errorMessage: string): NextResponse { - const url = new URL(location); - url.searchParams.set("loginError", errorMessage); - // TODO: validate allowlist of domains to prevent open redirects - return NextResponse.redirect(url.toString()); -} - export default async function GET(req: NextRequest): Promise { if (req.method !== "GET") { return new NextResponse(null, { status: 405 }); } - const domain = getXFernHostEdge(req); + const domain = getDocsDomainEdge(req); const code = req.nextUrl.searchParams.get("code"); const state = req.nextUrl.searchParams.get("state"); const error = req.nextUrl.searchParams.get("error"); const error_description = req.nextUrl.searchParams.get("error_description"); - const redirectLocation = safeUrl(state) ?? withDefaultProtocol(getXFernHostHeaderFallbackOrigin(req)); + const redirectLocation = safeUrl(state) ?? safeUrl(withDefaultProtocol(getHostEdge(req))); + + if (redirectLocation == null) { + return new NextResponse(null, { status: 500 }); + } if (error != null) { // eslint-disable-next-line no-console diff --git a/packages/ui/docs-bundle/src/pages/api/fern-docs/revalidate-all/v3.ts b/packages/ui/docs-bundle/src/pages/api/fern-docs/revalidate-all/v3.ts index 6243868696..3501bf37f9 100644 --- a/packages/ui/docs-bundle/src/pages/api/fern-docs/revalidate-all/v3.ts +++ b/packages/ui/docs-bundle/src/pages/api/fern-docs/revalidate-all/v3.ts @@ -1,7 +1,7 @@ import { DocsKVCache } from "@/server/DocsCache"; import { DocsLoader } from "@/server/DocsLoader"; import { Revalidator } from "@/server/revalidator"; -import { getXFernHostNode } from "@/server/xfernhost/node"; +import { getDocsDomainNode } from "@/server/xfernhost/node"; import { NodeCollector } from "@fern-api/fdr-sdk/navigation"; import type { FernDocs } from "@fern-fern/fern-docs-sdk"; import { NextApiHandler, NextApiRequest, NextApiResponse } from "next"; @@ -29,7 +29,7 @@ const handler: NextApiHandler = async ( req: NextApiRequest, res: NextApiResponse, ): Promise => { - const xFernHost = getXFernHostNode(req, true); + const xFernHost = getDocsDomainNode(req, true); // never proivde a token here because revalidation should only be done on public routes (for now) const loader = DocsLoader.for(xFernHost, undefined); diff --git a/packages/ui/docs-bundle/src/pages/api/fern-docs/revalidate-all/v4.ts b/packages/ui/docs-bundle/src/pages/api/fern-docs/revalidate-all/v4.ts index cf4e2873f6..f11fc01833 100644 --- a/packages/ui/docs-bundle/src/pages/api/fern-docs/revalidate-all/v4.ts +++ b/packages/ui/docs-bundle/src/pages/api/fern-docs/revalidate-all/v4.ts @@ -1,7 +1,7 @@ import { DocsKVCache } from "@/server/DocsCache"; import { DocsLoader } from "@/server/DocsLoader"; import { Revalidator } from "@/server/revalidator"; -import { getXFernHostNode } from "@/server/xfernhost/node"; +import { getDocsDomainNode } from "@/server/xfernhost/node"; import { NodeCollector } from "@fern-api/fdr-sdk/navigation"; import type { FernDocs } from "@fern-fern/fern-docs-sdk"; import { NextApiHandler, NextApiRequest, NextApiResponse } from "next"; @@ -38,7 +38,7 @@ const handler: NextApiHandler = async ( return res.status(400).json({ total: 0, results: [] }); } - const xFernHost = getXFernHostNode(req, true); + const xFernHost = getDocsDomainNode(req, true); // never proivde a token here because revalidation should only be done on public routes (for now) const loader = DocsLoader.for(xFernHost, undefined); diff --git a/packages/ui/docs-bundle/src/pages/api/fern-docs/revalidate-path.ts b/packages/ui/docs-bundle/src/pages/api/fern-docs/revalidate-path.ts index db8b01069c..e8dbf977c1 100644 --- a/packages/ui/docs-bundle/src/pages/api/fern-docs/revalidate-path.ts +++ b/packages/ui/docs-bundle/src/pages/api/fern-docs/revalidate-path.ts @@ -1,5 +1,5 @@ import { Revalidator } from "@/server/revalidator"; -import { getXFernHostNode } from "@/server/xfernhost/node"; +import { getDocsDomainNode } from "@/server/xfernhost/node"; import { FernDocs } from "@fern-fern/fern-docs-sdk"; import { NextApiHandler, NextApiRequest, NextApiResponse } from "next"; @@ -11,7 +11,7 @@ const handler: NextApiHandler = async ( req: NextApiRequest, res: NextApiResponse, ): Promise => { - const xFernHost = getXFernHostNode(req, true); + const xFernHost = getDocsDomainNode(req, true); const revalidate = new Revalidator(res, xFernHost); const path = req.query.path; diff --git a/packages/ui/docs-bundle/src/pages/api/fern-docs/robots.txt.ts b/packages/ui/docs-bundle/src/pages/api/fern-docs/robots.txt.ts index 9efaabf7f2..9ac306bb1f 100644 --- a/packages/ui/docs-bundle/src/pages/api/fern-docs/robots.txt.ts +++ b/packages/ui/docs-bundle/src/pages/api/fern-docs/robots.txt.ts @@ -1,4 +1,4 @@ -import { getXFernHostEdge } from "@/server/xfernhost/edge"; +import { getDocsDomainEdge } from "@/server/xfernhost/edge"; import { withDefaultProtocol } from "@fern-api/ui-core-utils"; import { getSeoDisabled } from "@fern-ui/fern-docs-edge-config"; import { NextRequest, NextResponse } from "next/server"; @@ -7,7 +7,7 @@ import urlJoin from "url-join"; export const runtime = "edge"; export default async function GET(req: NextRequest): Promise { - const xFernHost = getXFernHostEdge(req); + const xFernHost = getDocsDomainEdge(req); const basePath = req.nextUrl.pathname.split("/robots.txt")[0] || ""; const sitemap = urlJoin(withDefaultProtocol(xFernHost), basePath, "/sitemap.xml"); diff --git a/packages/ui/docs-bundle/src/pages/api/fern-docs/search.ts b/packages/ui/docs-bundle/src/pages/api/fern-docs/search.ts index 8dffdb2e69..e07f244df3 100644 --- a/packages/ui/docs-bundle/src/pages/api/fern-docs/search.ts +++ b/packages/ui/docs-bundle/src/pages/api/fern-docs/search.ts @@ -1,6 +1,6 @@ import { checkViewerAllowedEdge } from "@/server/auth/checkViewerAllowed"; import { loadWithUrl } from "@/server/loadWithUrl"; -import { getXFernHostEdge } from "@/server/xfernhost/edge"; +import { getDocsDomainEdge } from "@/server/xfernhost/edge"; import { getAuthEdgeConfig, getInkeepSettings } from "@fern-ui/fern-docs-edge-config"; import { SearchConfig, getSearchConfig } from "@fern-ui/search-utils"; import { provideRegistryService } from "@fern-ui/ui"; @@ -13,7 +13,7 @@ export default async function handler(req: NextRequest): Promise { return new Response(null, { status: 405 }); } - const docsUrl = getXFernHostEdge(req); + const docsUrl = getDocsDomainEdge(req); // HACK: only allow requests from cohere if (!docsUrl.includes("cohere")) { diff --git a/packages/ui/docs-bundle/src/pages/api/fern-docs/sitemap.xml.ts b/packages/ui/docs-bundle/src/pages/api/fern-docs/sitemap.xml.ts index d08899de24..70adde5116 100644 --- a/packages/ui/docs-bundle/src/pages/api/fern-docs/sitemap.xml.ts +++ b/packages/ui/docs-bundle/src/pages/api/fern-docs/sitemap.xml.ts @@ -1,6 +1,6 @@ import { DocsLoader } from "@/server/DocsLoader"; import { conformTrailingSlash } from "@/server/trailingSlash"; -import { getXFernHostEdge } from "@/server/xfernhost/edge"; +import { getDocsDomainEdge } from "@/server/xfernhost/edge"; import { NodeCollector } from "@fern-api/fdr-sdk/navigation"; import { withDefaultProtocol } from "@fern-api/ui-core-utils"; import { COOKIE_FERN_TOKEN } from "@fern-ui/fern-docs-utils"; @@ -14,7 +14,7 @@ export default async function GET(req: NextRequest): Promise { if (req.method !== "GET") { return new NextResponse(null, { status: 405 }); } - const xFernHost = getXFernHostEdge(req); + const xFernHost = getDocsDomainEdge(req); // load the root node const fernToken = req.cookies.get(COOKIE_FERN_TOKEN)?.value; diff --git a/packages/ui/docs-bundle/src/pages/dynamic/[host]/[[...slug]].tsx b/packages/ui/docs-bundle/src/pages/dynamic/[domain]/[[...slug]].tsx similarity index 67% rename from packages/ui/docs-bundle/src/pages/dynamic/[host]/[[...slug]].tsx rename to packages/ui/docs-bundle/src/pages/dynamic/[domain]/[[...slug]].tsx index de05241228..f2ff2d59c4 100644 --- a/packages/ui/docs-bundle/src/pages/dynamic/[host]/[[...slug]].tsx +++ b/packages/ui/docs-bundle/src/pages/dynamic/[domain]/[[...slug]].tsx @@ -1,4 +1,5 @@ import { getDynamicDocsPageProps } from "@/server/getDynamicDocsPageProps"; +import { getHostNodeStatic } from "@/server/xfernhost/node"; import { DocsPage } from "@fern-ui/ui"; import { GetServerSideProps } from "next"; @@ -9,8 +10,9 @@ export const getServerSideProps: GetServerSideProps = async ({ req, res, query, res.statusCode = 500; } - const xFernHost = params.host as string; + const domain = params.domain as string; + const host = getHostNodeStatic() ?? domain; const slugArray = params.slug == null ? [] : Array.isArray(params.slug) ? params.slug : [params.slug]; - return getDynamicDocsPageProps(xFernHost, slugArray, req.cookies); + return getDynamicDocsPageProps(domain, host, slugArray, req.cookies); }; diff --git a/packages/ui/docs-bundle/src/pages/static/[host]/[[...slug]].tsx b/packages/ui/docs-bundle/src/pages/static/[domain]/[[...slug]].tsx similarity index 77% rename from packages/ui/docs-bundle/src/pages/static/[host]/[[...slug]].tsx rename to packages/ui/docs-bundle/src/pages/static/[domain]/[[...slug]].tsx index 3adcc44646..19018a5b8d 100644 --- a/packages/ui/docs-bundle/src/pages/static/[host]/[[...slug]].tsx +++ b/packages/ui/docs-bundle/src/pages/static/[domain]/[[...slug]].tsx @@ -1,6 +1,7 @@ import { DocsKVCache } from "@/server/DocsCache"; import { getDocsPageProps } from "@/server/getDocsPageProps"; import { withSSGProps } from "@/server/withSSGProps"; +import { getHostNodeStatic } from "@/server/xfernhost/node"; import * as FernNavigation from "@fern-api/fdr-sdk/navigation"; import { DocsPage } from "@fern-ui/ui"; import { GetStaticPaths, GetStaticProps } from "next"; @@ -10,12 +11,13 @@ export default DocsPage; export const getStaticProps: GetStaticProps> = async (context) => { const { params = {} } = context; - const xFernHost = params.host as string; + const domain = params.domain as string; + const host = getHostNodeStatic() ?? domain; const slugArray = params.slug == null ? [] : Array.isArray(params.slug) ? params.slug : [params.slug]; - const props = await withSSGProps(getDocsPageProps(xFernHost, slugArray)); + const props = await withSSGProps(getDocsPageProps(domain, host, slugArray)); - const cache = DocsKVCache.getInstance(xFernHost); + const cache = DocsKVCache.getInstance(domain); const slug = FernNavigation.slugjoin(...slugArray); if ("props" in props) { diff --git a/packages/ui/docs-bundle/src/server/LoadDocsPerformanceTracker.ts b/packages/ui/docs-bundle/src/server/LoadDocsPerformanceTracker.ts index 11f7c85418..5b70813ed4 100644 --- a/packages/ui/docs-bundle/src/server/LoadDocsPerformanceTracker.ts +++ b/packages/ui/docs-bundle/src/server/LoadDocsPerformanceTracker.ts @@ -8,15 +8,15 @@ import type { LoadWithUrlResponse } from "./loadWithUrl"; export class LoadDocsPerformanceTracker { static init({ - host, + domain, slug, auth, }: { - host: string; + domain: string; slug: string[]; auth: AuthPartner | undefined; }): LoadDocsPerformanceTracker { - return new LoadDocsPerformanceTracker(host, slug, auth); + return new LoadDocsPerformanceTracker(domain, slug, auth); } private constructor( diff --git a/packages/ui/docs-bundle/src/server/getDocsPageProps.ts b/packages/ui/docs-bundle/src/server/getDocsPageProps.ts index 417f224fe4..a42dc20d35 100644 --- a/packages/ui/docs-bundle/src/server/getDocsPageProps.ts +++ b/packages/ui/docs-bundle/src/server/getDocsPageProps.ts @@ -15,25 +15,28 @@ export interface User { } export async function getDocsPageProps( - xFernHost: string | undefined, + domain: string | undefined, + host: string, slug: string[], auth?: AuthProps, ): Promise { - if (xFernHost == null || Array.isArray(xFernHost)) { + if (domain == null || Array.isArray(domain)) { return { notFound: true }; } - const performance = LoadDocsPerformanceTracker.init({ host: xFernHost, slug, auth: auth?.partner }); + const performance = LoadDocsPerformanceTracker.init({ domain, slug, auth: auth?.partner }); /** * Load the docs for the given URL. */ - const docs = await performance.trackLoadDocsPromise(loadWithUrl(xFernHost, auth)); + const docs = await performance.trackLoadDocsPromise(loadWithUrl(domain, auth)); /** * Convert the docs into initial props for the page. */ - const initialProps = await performance.trackInitialPropsPromise(withInitialProps({ docs, slug, xFernHost, auth })); + const initialProps = await performance.trackInitialPropsPromise( + withInitialProps({ docs, slug, domain, host, auth }), + ); /** * Send performance data to Vercel Analytics. diff --git a/packages/ui/docs-bundle/src/server/getDynamicDocsPageProps.ts b/packages/ui/docs-bundle/src/server/getDynamicDocsPageProps.ts index 8f952839d1..ff1a4a2210 100644 --- a/packages/ui/docs-bundle/src/server/getDynamicDocsPageProps.ts +++ b/packages/ui/docs-bundle/src/server/getDynamicDocsPageProps.ts @@ -9,7 +9,8 @@ import { getDocsPageProps } from "./getDocsPageProps"; type GetServerSideDocsPagePropsResult = GetServerSidePropsResult>; export async function getDynamicDocsPageProps( - xFernHost: string, + domain: string, + host: string, slug: string[], cookies: NextApiRequestCookies, ): Promise { @@ -17,7 +18,7 @@ export async function getDynamicDocsPageProps( try { if (cookies[COOKIE_FERN_TOKEN]) { - authProps = await withAuthProps(xFernHost, cookies[COOKIE_FERN_TOKEN]); + authProps = await withAuthProps(domain, cookies[COOKIE_FERN_TOKEN]); } } catch (e) { // eslint-disable-next-line no-console @@ -28,5 +29,5 @@ export async function getDynamicDocsPageProps( * Authenticated user is guaranteed to have a valid token because the middleware * would have redirected them to the login page */ - return getDocsPageProps(xFernHost, slug, authProps); + return getDocsPageProps(domain, host, slug, authProps); } diff --git a/packages/ui/docs-bundle/src/server/pageRoutes.ts b/packages/ui/docs-bundle/src/server/pageRoutes.ts index 186e0cd4a6..eba840605b 100644 --- a/packages/ui/docs-bundle/src/server/pageRoutes.ts +++ b/packages/ui/docs-bundle/src/server/pageRoutes.ts @@ -2,16 +2,16 @@ import getAssetPathFromRoute from "next/dist/shared/lib/router/utils/get-asset-p import { removeTrailingSlash } from "next/dist/shared/lib/router/utils/remove-trailing-slash"; import urlJoin from "url-join"; -export function getPageRoute(ssg: boolean, host: string, pathname: string): string { +export function getPageRoute(ssg: boolean, domain: string, pathname: string): string { const prefix = ssg ? "static" : "dynamic"; - return urlJoin("/", prefix, host, pathname); + return urlJoin("/", prefix, domain, pathname); } export function getPageRouteMatch(ssg: boolean, buildId: string): string { - return `/_next/data/${buildId}/${ssg ? "static" : "dynamic"}/[host]/[[...slug]].json`; + return `/_next/data/${buildId}/${ssg ? "static" : "dynamic"}/[domain]/[[...slug]].json`; } -export function getPageRoutePath(ssg: boolean, buildId: string, host: string, pathname: string): string { +export function getPageRoutePath(ssg: boolean, buildId: string, domain: string, pathname: string): string { const dataRoute = getAssetPathFromRoute(removeTrailingSlash(pathname), ".json"); /** @@ -21,5 +21,5 @@ export function getPageRoutePath(ssg: boolean, buildId: string, host: string, pa return `/_next/data/${buildId}${dataRoute}`; } - return `/_next/data/${buildId}/${ssg ? "static" : "dynamic"}/${host}${dataRoute}`; + return `/_next/data/${buildId}/${ssg ? "static" : "dynamic"}/${domain}${dataRoute}`; } diff --git a/packages/ui/docs-bundle/src/server/redirectWithLoginError.ts b/packages/ui/docs-bundle/src/server/redirectWithLoginError.ts new file mode 100644 index 0000000000..c06ffe1f68 --- /dev/null +++ b/packages/ui/docs-bundle/src/server/redirectWithLoginError.ts @@ -0,0 +1,12 @@ +import { NextResponse } from "next/server"; + +export function redirectWithLoginError(location: URL | undefined, errorMessage: string): NextResponse { + if (location == null) { + return new NextResponse(null, { status: 500 }); + } + + const url = new URL(location); + url.searchParams.set("loginError", errorMessage); + // TODO: validate allowlist of domains to prevent open redirects + return NextResponse.redirect(url.toString()); +} diff --git a/packages/ui/docs-bundle/src/server/safeUrl.ts b/packages/ui/docs-bundle/src/server/safeUrl.ts index 0ee68ad83f..cd1ea1c040 100644 --- a/packages/ui/docs-bundle/src/server/safeUrl.ts +++ b/packages/ui/docs-bundle/src/server/safeUrl.ts @@ -1,6 +1,6 @@ import { withDefaultProtocol } from "@fern-api/ui-core-utils"; -export function safeUrl(url: string | null | undefined): string | undefined { +export function safeUrl(url: string | null | undefined): URL | undefined { if (url == null) { return undefined; } @@ -8,8 +8,7 @@ export function safeUrl(url: string | null | undefined): string | undefined { url = withDefaultProtocol(url); try { - new URL(url); - return url; + return new URL(url); } catch (e) { // eslint-disable-next-line no-console console.error(e); diff --git a/packages/ui/docs-bundle/src/server/withInitialProps.ts b/packages/ui/docs-bundle/src/server/withInitialProps.ts index f055c4cdc5..2f8a1416b5 100644 --- a/packages/ui/docs-bundle/src/server/withInitialProps.ts +++ b/packages/ui/docs-bundle/src/server/withInitialProps.ts @@ -33,18 +33,26 @@ import { withVersionSwitcherInfo } from "./withVersionSwitcherInfo"; interface WithInitialProps { docs: LoadWithUrlResponse; slug: string[]; - xFernHost: string; + /** + * Docs domain + */ + domain: string; + /** + * Hostname of this request (i.e. localhost, or preview URL, otherwise the docs domain in production) + */ + host: string; auth?: AuthProps; } export async function withInitialProps({ docs: docsResponse, slug: slugArray, - xFernHost, + domain, + host, auth, }: WithInitialProps): Promise>> { if (!docsResponse.ok) { - return handleLoadDocsError(xFernHost, slugArray, docsResponse.error); + return handleLoadDocsError(domain, slugArray, docsResponse.error); } const docs = docsResponse.body; @@ -59,10 +67,10 @@ export async function withInitialProps({ return redirect; } - const featureFlags = await getFeatureFlags(xFernHost); + const featureFlags = await getFeatureFlags(domain); - const authConfig = await getAuthEdgeConfig(xFernHost); - const loader = DocsLoader.for(xFernHost) + const authConfig = await getAuthEdgeConfig(domain); + const loader = DocsLoader.for(domain) .withFeatureFlags(featureFlags) .withAuth(authConfig, auth) .withLoadDocsForUrlResponse(docs); @@ -177,7 +185,7 @@ export async function withInitialProps({ if (authConfig?.type === "basic_token_verification") { if (auth == null) { const redirect = new URL(withDefaultProtocol(authConfig.redirect)); - redirect.searchParams.set("state", urlJoin(withDefaultProtocol(xFernHost), slug)); + redirect.searchParams.set("state", urlJoin(withDefaultProtocol(host), slug)); navbarLinks.push({ type: "outlined", @@ -188,10 +196,13 @@ export async function withInitialProps({ rounded: false, }); } else { + const logout = new URL(getApiRoute("/api/fern-docs/auth/logout"), withDefaultProtocol(host)); + logout.searchParams.set("state", urlJoin(withDefaultProtocol(host), slug)); + navbarLinks.push({ type: "outlined", text: "Logout", - url: FernNavigation.Url(getApiRoute("/api/fern-docs/auth/logout")), + url: FernNavigation.Url(logout.toString()), icon: undefined, rightIcon: undefined, rounded: false, @@ -288,7 +299,7 @@ export async function withInitialProps({ docs.definition.filesV2, docs.definition.apis, node, - await getSeoDisabled(xFernHost), + await getSeoDisabled(domain), isTrailingSlashEnabled(), ), user: auth?.user, diff --git a/packages/ui/docs-bundle/src/server/xfernhost/edge.ts b/packages/ui/docs-bundle/src/server/xfernhost/edge.ts index d730465013..6c0353b03e 100644 --- a/packages/ui/docs-bundle/src/server/xfernhost/edge.ts +++ b/packages/ui/docs-bundle/src/server/xfernhost/edge.ts @@ -6,7 +6,7 @@ import { cleanHost } from "./util"; /** * Note: x-fern-host is always appended to the request header by cloudfront for all *.docs.buildwithfern.com requests. */ -export function getXFernHostEdge(req: NextRequest, useSearchParams = false): string { +export function getDocsDomainEdge(req: NextRequest, useSearchParams = false): string { const hosts = [ useSearchParams ? req.nextUrl.searchParams.get("host") : undefined, getNextPublicDocsDomain(), @@ -26,7 +26,7 @@ export function getXFernHostEdge(req: NextRequest, useSearchParams = false): str } // use this for testing auth-based redirects on development and preview environments -export function getXFernHostHeaderFallbackOrigin(req: NextRequest): string { +export function getHostEdge(req: NextRequest): string { if ( process.env.NODE_ENV === "development" || process.env.VERCEL_ENV === "preview" || @@ -34,5 +34,5 @@ export function getXFernHostHeaderFallbackOrigin(req: NextRequest): string { ) { return req.nextUrl.host; } - return cleanHost(req.headers.get(HEADER_X_FERN_HOST)) ?? req.nextUrl.host; + return getDocsDomainEdge(req); } diff --git a/packages/ui/docs-bundle/src/server/xfernhost/node.ts b/packages/ui/docs-bundle/src/server/xfernhost/node.ts index ed24223684..d98c2f627d 100644 --- a/packages/ui/docs-bundle/src/server/xfernhost/node.ts +++ b/packages/ui/docs-bundle/src/server/xfernhost/node.ts @@ -6,7 +6,7 @@ import { cleanHost } from "./util"; /** * Note: x-fern-host is always appended to the request header by cloudfront for all *.docs.buildwithfern.com requests. */ -export function getXFernHostNode(req: NextApiRequest, useSearchParams = false): string { +export function getDocsDomainNode(req: NextApiRequest, useSearchParams = false): string { const hosts = [ useSearchParams ? req.query["host"] : undefined, getNextPublicDocsDomain(), @@ -28,3 +28,16 @@ export function getXFernHostNode(req: NextApiRequest, useSearchParams = false): throw new Error("Could not determine xFernHost from request."); } + +// attempts to construct the host from environment variables, when the req object is not available +export function getHostNodeStatic(): string | undefined { + if (process.env.NODE_ENV === "development") { + return `${process.env.HOST || "localhost"}:${process.env.PORT || 3000}`; + } + + if (process.env.VERCEL_ENV === "preview" || process.env.VERCEL_ENV === "development") { + return process.env.VERCEL_URL; + } + + return undefined; +} From 2e18301eb3e4ea3271383cec379e68a3dae1094a Mon Sep 17 00:00:00 2001 From: Andrew Jiang Date: Thu, 10 Oct 2024 17:09:25 -0400 Subject: [PATCH 2/3] revert --- .../fdr-sdk/src/converters/db/convertAPIDefinitionToDb.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/fdr-sdk/src/converters/db/convertAPIDefinitionToDb.ts b/packages/fdr-sdk/src/converters/db/convertAPIDefinitionToDb.ts index aa47b05dd5..3c4addeea0 100644 --- a/packages/fdr-sdk/src/converters/db/convertAPIDefinitionToDb.ts +++ b/packages/fdr-sdk/src/converters/db/convertAPIDefinitionToDb.ts @@ -677,9 +677,9 @@ class ApiDefinitionTransformationContext { for (const environment of environments) { const entry = this.uniqueBaseUrls[environment.id]; if (entry != null) { - entry.add(this.getHostEdge(environment.baseUrl)); + entry.add(this.getHost(environment.baseUrl)); } else { - this.uniqueBaseUrls[environment.id] = new Set([this.getHostEdge(environment.baseUrl)]); + this.uniqueBaseUrls[environment.id] = new Set([this.getHost(environment.baseUrl)]); } } } @@ -693,7 +693,7 @@ class ApiDefinitionTransformationContext { return false; } - private getHostEdge(url: string) { + private getHost(url: string) { const parsedBaseUrl = new URL(url); return parsedBaseUrl.host; } From 43f600485b8609eba49460248726e48f93e07d21 Mon Sep 17 00:00:00 2001 From: Andrew Jiang Date: Thu, 10 Oct 2024 17:11:26 -0400 Subject: [PATCH 3/3] final fixes --- .../src/pages/api/fern-docs/oauth/ory/callback.ts | 6 +----- .../src/pages/api/fern-docs/oauth/webflow/callback.ts | 6 +----- 2 files changed, 2 insertions(+), 10 deletions(-) diff --git a/packages/ui/docs-bundle/src/pages/api/fern-docs/oauth/ory/callback.ts b/packages/ui/docs-bundle/src/pages/api/fern-docs/oauth/ory/callback.ts index a267583638..4489388196 100644 --- a/packages/ui/docs-bundle/src/pages/api/fern-docs/oauth/ory/callback.ts +++ b/packages/ui/docs-bundle/src/pages/api/fern-docs/oauth/ory/callback.ts @@ -25,10 +25,6 @@ export default async function GET(req: NextRequest): Promise { const error_description = req.nextUrl.searchParams.get("error_description"); const redirectLocation = safeUrl(state) ?? safeUrl(withDefaultProtocol(getHostEdge(req))); - if (redirectLocation == null) { - return new NextResponse(null, { status: 500 }); - } - if (error != null) { // eslint-disable-next-line no-console console.error(`OAuth2 error: ${error} - ${error_description}`); @@ -59,7 +55,7 @@ export default async function GET(req: NextRequest): Promise { }; const expires = token.exp == null ? undefined : new Date(token.exp * 1000); // TODO: validate allowlist of domains to prevent open redirects - const res = NextResponse.redirect(redirectLocation); + const res = redirectLocation ? NextResponse.redirect(redirectLocation) : NextResponse.next(); res.cookies.set(COOKIE_FERN_TOKEN, await signFernJWT(fernUser), withSecureCookie({ expires })); res.cookies.set(COOKIE_ACCESS_TOKEN, access_token, withSecureCookie({ expires })); if (refresh_token != null) { diff --git a/packages/ui/docs-bundle/src/pages/api/fern-docs/oauth/webflow/callback.ts b/packages/ui/docs-bundle/src/pages/api/fern-docs/oauth/webflow/callback.ts index c5d47910e9..9d5d329849 100644 --- a/packages/ui/docs-bundle/src/pages/api/fern-docs/oauth/webflow/callback.ts +++ b/packages/ui/docs-bundle/src/pages/api/fern-docs/oauth/webflow/callback.ts @@ -22,10 +22,6 @@ export default async function GET(req: NextRequest): Promise { const error_description = req.nextUrl.searchParams.get("error_description"); const redirectLocation = safeUrl(state) ?? safeUrl(withDefaultProtocol(getHostEdge(req))); - if (redirectLocation == null) { - return new NextResponse(null, { status: 500 }); - } - if (error != null) { // eslint-disable-next-line no-console console.error(`OAuth2 error: ${error} - ${error_description}`); @@ -55,7 +51,7 @@ export default async function GET(req: NextRequest): Promise { }); // TODO: validate allowlist of domains to prevent open redirects - const res = NextResponse.redirect(redirectLocation); + const res = redirectLocation ? NextResponse.redirect(redirectLocation) : NextResponse.next(); res.cookies.set("access_token", accessToken, withSecureCookie()); return res; } catch (error) {