Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
59e87a3
feat(docs): support multiple security schemes in API explorer with OR…
devin-ai-integration[bot] Nov 6, 2025
cee1ca4
feat(docs): implement independent auth scheme selection in API explorer
devin-ai-integration[bot] Nov 6, 2025
7772a6f
fix(docs): move client hooks from server-only component to fix CI lint
devin-ai-integration[bot] Nov 6, 2025
4ea5849
fix(docs): remove unused lang parameter from PlaygroundAuthorizationF…
devin-ai-integration[bot] Nov 6, 2025
278d4ac
fix(docs): apply biome formatting to fix CI lint
devin-ai-integration[bot] Nov 6, 2025
246f85d
select auths
kennyderek Nov 6, 2025
76501de
dropdown selector
kennyderek Nov 6, 2025
b505759
format
kennyderek Nov 6, 2025
aecdcf3
vercel comment
kennyderek Nov 6, 2025
7c7f256
Merge branch 'app' into kenny/auth-selector-playground
kennyderek Nov 6, 2025
b89824c
Merge branch 'app' into kenny/auth-selector-playground
fern-support Nov 7, 2025
233c886
Add auth type name
kennyderek Nov 7, 2025
f09817d
Merge branch 'app' into kenny/auth-selector-playground
kennyderek Nov 7, 2025
b40ed8e
with desc
kennyderek Nov 9, 2025
8456090
Merge branch 'app' into kenny/auth-selector-playground
kennyderek Nov 9, 2025
4f65d96
format
kennyderek Nov 9, 2025
f677ff6
Merge branch 'app' into kenny/auth-selector-playground
kennyderek Nov 9, 2025
db25aaa
Merge branch 'app' into kenny/auth-selector-playground
fern-support Nov 9, 2025
9945cd4
store everything by auth key
kennyderek Nov 9, 2025
b721f98
format
kennyderek Nov 9, 2025
6780b55
linkify function
kennyderek Nov 9, 2025
5a7b869
turn green
kennyderek Nov 9, 2025
269041b
Update packages/fern-docs/bundle/src/components/playground/auth/linki…
fern-support Nov 9, 2025
7de5682
format
kennyderek Nov 9, 2025
66add62
format
kennyderek Nov 9, 2025
79e48be
vercel comment
kennyderek Nov 9, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 22 additions & 0 deletions packages/fdr-sdk/src/api-definition/endpoint-context.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import type { EndpointNode, GrpcNode, TypeId, WebhookNode, WebSocketNode } from
import type {
ApiDefinition,
AuthScheme,
AuthSchemeId,
EndpointDefinition,
EndpointId,
ObjectProperty,
Expand All @@ -11,11 +12,17 @@ import type {
} from "./latest";
import { prune } from "./prune";

export type AuthSchemeWithKey = {
key: AuthSchemeId;
scheme: AuthScheme;
};

export type EndpointContext = {
node: EndpointNode;
endpoint: EndpointDefinition;
globalHeaders: ObjectProperty[];
auths: AuthScheme[];
authsWithKeys: AuthSchemeWithKey[];
types: Record<TypeId, TypeDefinition>;
};

Expand All @@ -35,6 +42,13 @@ export function createEndpointContext(
node,
endpoint,
auths: endpoint.auth?.map((id) => api.auths[id]).filter((auth): auth is AuthScheme => auth != null) ?? [],
authsWithKeys:
endpoint.auth
?.map((id) => {
const scheme = api.auths[id];
return scheme ? { key: id, scheme } : null;
})
.filter((item): item is AuthSchemeWithKey => item != null) ?? [],
globalHeaders: api.globalHeaders ?? [],
types: api.types
};
Expand All @@ -45,6 +59,7 @@ export type WebSocketContext = {
channel: WebSocketChannel;
globalHeaders: ObjectProperty[];
auths: AuthScheme[];
authsWithKeys: AuthSchemeWithKey[];
types: Record<TypeId, TypeDefinition>;
};

Expand All @@ -64,6 +79,13 @@ export function createWebSocketContext(
node,
channel,
auths: channel.auth?.map((id) => api.auths[id]).filter((auth): auth is AuthScheme => auth != null) ?? [],
authsWithKeys:
channel.auth
?.map((id) => {
const scheme = api.auths[id];
return scheme ? { key: id, scheme } : null;
})
.filter((item): item is AuthSchemeWithKey => item != null) ?? [],
globalHeaders: api.globalHeaders ?? [],
types: api.types
};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -103,8 +103,8 @@ export async function EndpointContent({
}
footer={<FooterLayout bottomNavigation={bottomNavigation} hideFeedback={hideFeedback} lang={lang} />}
>
<PlaygroundKeyboardTrigger />
<MdxServerComponentProseSuspense mdx={endpoint.description} />
<PlaygroundKeyboardTrigger key="keyboard-trigger" />
<MdxServerComponentProseSuspense key="description" mdx={endpoint.description} />
</ReferenceLayout>
</EndpointContextProvider>
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ export async function EndpointContentLeft({

return (
<>
<TypeDefinitionAnchorPart part="request">
<TypeDefinitionAnchorPart key="request" part="request">
{showAuth && auths.length > 0 && (
<TypeDefinitionAnchorPart part="auth">
<EndpointAuthSection auths={auths} lang={lang} />
Expand Down Expand Up @@ -107,7 +107,7 @@ export async function EndpointContentLeft({
)
) : null}
</TypeDefinitionAnchorPart>
<TypeDefinitionResponse>
<TypeDefinitionResponse key="response">
<TypeDefinitionAnchorPart part="response">
{endpoint.responses?.[0] != null ? (
endpoint.responses.length > 1 ? (
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -59,9 +59,9 @@ export async function ExplorerContent({
if (!context) return null;

let oauthReferencedContext: EndpointContext | undefined;
const auth = context.auths[0];
if (auth?.type === "oAuth") {
const oAuthValue = auth.value;
const firstAuth = context.auths[0];
if (firstAuth?.type === "oAuth") {
const oAuthValue = firstAuth.value;
if (oAuthValue.type === "clientCredentials") {
const clientCredentials = oAuthValue.value;
if (clientCredentials.type === "referencedEndpoint") {
Expand All @@ -77,6 +77,7 @@ export async function ExplorerContent({
endpoint,
globalHeaders,
auths: authSchemes.filter((a) => a.type !== "oAuth"),
authsWithKeys: [], // OAuth referenced endpoints don't need authsWithKeys
types
};
}
Expand All @@ -87,9 +88,8 @@ export async function ExplorerContent({
}
}

const authForm = auth != null && (
const authForm = context.authsWithKeys.length > 0 && (
<PlaygroundAuthorizationFormCard
auth={auth}
context={context}
oauthReferencedContext={oauthReferencedContext}
lang={lang}
Expand All @@ -109,8 +109,8 @@ export async function ExplorerContent({
} else if (node.type === "webSocket") {
const context = createWebSocketContext(node, api);
if (!context) return null;
const authForm = context.auths[0] != null && (
<PlaygroundAuthorizationFormCard context={context} auth={context.auths[0]} lang={lang} />
const authForm = context.authsWithKeys.length > 0 && (
<PlaygroundAuthorizationFormCard context={context} lang={lang} />
);
return (
<ApiDefinitionIdProvider value={node.apiDefinitionId}>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,16 @@ import { FernSyntaxHighlighter } from "@fern-docs/components/syntax-highlighter"
import { useAtom, useAtomValue } from "jotai";
import { type FC, useMemo } from "react";

import { PLAYGROUND_AUTH_STATE_ATOM, PLAYGROUND_AUTH_STATE_OAUTH_ATOM } from "@/state/playground";
import {
PLAYGROUND_AUTH_STATE_ATOM,
PLAYGROUND_AUTH_STATE_OAUTH_ATOM,
PLAYGROUND_SELECTED_AUTH_TYPE_ATOM
} from "@/state/playground";

import { PlaygroundCodeSnippetResolverBuilder } from "./code-snippets/resolver";
import { useSnippet } from "./code-snippets/useSnippet";
import type { PlaygroundEndpointRequestFormState } from "./types";
import { getAuthKey } from "./utils";
import { usePlaygroundBaseUrl } from "./utils/select-environment";

interface PlaygroundRequestPreviewProps {
Expand All @@ -21,13 +26,41 @@ interface PlaygroundRequestPreviewProps {
export const PlaygroundRequestPreview: FC<PlaygroundRequestPreviewProps> = ({ context, formState, requestType }) => {
const authState = useAtomValue(PLAYGROUND_AUTH_STATE_ATOM);
const [oAuthValue, setOAuthValue] = useAtom(PLAYGROUND_AUTH_STATE_OAUTH_ATOM);
const selectedAuthType = useAtomValue(PLAYGROUND_SELECTED_AUTH_TYPE_ATOM);
const [baseUrl] = usePlaygroundBaseUrl(context.endpoint, context.node.apiDefinitionId);

// Determine which auth to use based on the selected auth type, and get its key
const { selectedAuth, authKey } = useMemo(() => {
if (context.authsWithKeys.length === 0) {
return { selectedAuth: undefined, authKey: undefined };
}

// If a specific auth type is selected, find it
if (selectedAuthType) {
const selectedAuthWithKey = context.authsWithKeys.find(
(authWithKey) => getAuthKey(authWithKey) === selectedAuthType
);
if (selectedAuthWithKey) {
return {
selectedAuth: selectedAuthWithKey.scheme,
authKey: getAuthKey(selectedAuthWithKey)
};
}
}

// Default to the first auth
const firstAuth = context.authsWithKeys[0];
return {
selectedAuth: firstAuth?.scheme,
authKey: firstAuth ? getAuthKey(firstAuth) : undefined
};
}, [context.authsWithKeys, selectedAuthType]);

const builder = useMemo(() => new PlaygroundCodeSnippetResolverBuilder(context, true), [context]);

const resolver = useMemo(
() => oAuthValue && builder.createRedacted(authState, formState, baseUrl, setOAuthValue),
[authState, builder, formState, oAuthValue, baseUrl, setOAuthValue]
() => oAuthValue && builder.createRedacted(authState, formState, baseUrl, setOAuthValue, selectedAuth, authKey),
[authState, builder, formState, oAuthValue, baseUrl, setOAuthValue, selectedAuth, authKey]
);
const code = useSnippet(resolver, requestType);

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
"use client";

import type { EndpointContext, WebSocketContext } from "@fern-api/fdr-sdk/api-definition";
import { PlaygroundAuthorizationFormCard } from "./PlaygroundAuthorizationFormCard";

interface PlaygroundAuthSwitcherProps {
context: EndpointContext | WebSocketContext;
oauthReferencedContext?: EndpointContext;
lang: string;
}

export function PlaygroundAuthSwitcher({ context, oauthReferencedContext, lang }: PlaygroundAuthSwitcherProps) {
if (context.authsWithKeys.length === 0) {
return null;
}

return (
<PlaygroundAuthorizationFormCard
context={context}
oauthReferencedContext={oauthReferencedContext}
lang={lang}
/>
);
}
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import { FoundOAuthReferencedEndpointForm } from "./PlaygroundOAuthForm";

interface PlaygroundAuthorizationFormProps {
auth: APIV1Read.ApiAuth;
authKey: string;
context: EndpointContext | WebSocketContext;
oauthReferencedContext?: EndpointContext;
disabled: boolean;
Expand All @@ -18,6 +19,7 @@ interface PlaygroundAuthorizationFormProps {

export const PlaygroundAuthorizationForm: FC<PlaygroundAuthorizationFormProps> = ({
auth,
authKey,
context,
oauthReferencedContext,
disabled,
Expand All @@ -32,7 +34,7 @@ export const PlaygroundAuthorizationForm: FC<PlaygroundAuthorizationFormProps> =
basicAuth: (basicAuth) => (
<PlaygroundBasicAuthForm basicAuth={basicAuth} disabled={disabled} lang={lang} />
),
header: (header) => <PlaygroundHeaderAuthForm header={header} disabled={disabled} />,
header: (header) => <PlaygroundHeaderAuthForm header={header} authKey={authKey} disabled={disabled} />,
oAuth: (oAuth) => {
if ("endpoint" in context) {
return visitDiscriminatedUnion(oAuth.value, "type")._visit({
Expand Down
Original file line number Diff line number Diff line change
@@ -1,10 +1,13 @@
import "server-only";
"use client";

import type { EndpointContext, WebSocketContext } from "@fern-api/fdr-sdk/api-definition";
import type { APIV1Read } from "@fern-api/fdr-sdk/client/types";
import { visitDiscriminatedUnion } from "@fern-api/ui-core-utils";
import { useAtomValue } from "jotai";
import type React from "react";
import type { ReactElement } from "react";
import { type ReactElement, useMemo } from "react";
import { PLAYGROUND_SELECTED_AUTH_TYPE_ATOM } from "@/state/playground";
import { getAuthKey } from "../utils";

import { PlaygroundAuthorizationForm } from "./PlaygroundAuthorizationForm";
import {
Expand All @@ -18,19 +21,57 @@ import { PlaygroundBearerAuthForm } from "./PlaygroundBearerAuthForm";
import { FoundOAuthReferencedEndpointForm } from "./PlaygroundOAuthForm";

interface PlaygroundAuthorizationFormCardProps {
auth: APIV1Read.ApiAuth;
context: EndpointContext | WebSocketContext;
oauthReferencedContext?: EndpointContext;
disabled?: boolean;
lang: string;
authIndex?: number;
totalAuthCount?: number;
allAuthTypes?: string[];
allAuths?: APIV1Read.ApiAuth[];
}
export function PlaygroundAuthorizationFormCard({
auth,
context,
oauthReferencedContext,
disabled = false,
lang
lang,
authIndex = 0,
totalAuthCount = 1,
allAuthTypes = [],
allAuths = []
}: PlaygroundAuthorizationFormCardProps): ReactElement<any> | null {
const selectedAuthType = useAtomValue(PLAYGROUND_SELECTED_AUTH_TYPE_ATOM);

// Determine which auth to show based on the selected auth type, and get its key
const { auth, authKey } = useMemo(() => {
if (context.authsWithKeys.length === 0) {
return { auth: undefined, authKey: undefined };
}

// If a specific auth type is selected, find it
if (selectedAuthType) {
const selectedAuthWithKey = context.authsWithKeys.find(
(authWithKey) => getAuthKey(authWithKey) === selectedAuthType
);
if (selectedAuthWithKey) {
return {
auth: selectedAuthWithKey.scheme,
authKey: getAuthKey(selectedAuthWithKey)
};
}
}

// Default to the first auth
const firstAuth = context.authsWithKeys[0];
return {
auth: firstAuth?.scheme,
authKey: firstAuth ? getAuthKey(firstAuth) : undefined
};
}, [context.authsWithKeys, selectedAuthType]);

if (!auth || !authKey) {
return null;
}
let oauthForm: React.ReactNode = null;

if (auth.type === "oAuth" && "endpoint" in context) {
Expand Down Expand Up @@ -67,19 +108,28 @@ export function PlaygroundAuthorizationFormCard({
}

return (
<PlaygroundAuthorizationFormCardRoot>
<PlaygroundAuthorizationFormCardRoot
authIndex={authIndex}
auth={auth}
totalAuthCount={totalAuthCount}
allAuthTypes={allAuthTypes}
allAuths={allAuths}
>
<PlaygroundAuthorizationCardTrigger
key="trigger"
context={context}
auth={auth}
oauthReferencedContext={oauthReferencedContext}
disabled={disabled}
lang={lang}
allAuths={allAuths}
/>
<PlaygroundAuthorizationFormCardContent>
<PlaygroundAuthorizationFormCardContent key="content">
<div className="fern-dropdown max-h-full">
{oauthForm || (
<PlaygroundAuthorizationForm
auth={auth}
authKey={authKey}
context={context}
oauthReferencedContext={oauthReferencedContext}
disabled={disabled}
Expand Down
Loading