import type { OpenAPIV3 } from '@gitbook/openapi-parser'; import { OpenAPIMediaTypeExamplesBody, OpenAPIMediaTypeExamplesSelector, } from './OpenAPICodeSampleInteractive'; import { OpenAPICodeSampleBody } from './OpenAPICodeSampleSelector'; import { ScalarApiButton } from './ScalarApiButton'; import { type CodeSampleGenerator, codeSampleGenerators, parseHostAndPath } from './code-samples'; import { type OpenAPIContext, getOpenAPIClientContext } from './context'; import { generateMediaTypeExamples, generateSchemaExample } from './generateSchemaExample'; import { stringifyOpenAPI } from './stringifyOpenAPI'; import type { OpenAPIOperationData } from './types'; import { mergeHeaders } from './util/headers'; import { extractOrigin, getAllServerOrigins, getDefaultServerURL, hasValidServerHost, } from './util/server'; import { resolvePrefillCodePlaceholderFromSecurityScheme, resolveURLWithPrefillCodePlaceholdersFromServer, } from './util/tryit-prefill'; import { checkIsReference, extractOperationSecurityInfo } from './utils'; const CUSTOM_CODE_SAMPLES_KEYS = ['x-custom-examples', 'x-code-samples', 'x-codeSamples'] as const; /** * Display code samples to execute the operation. * It supports the Redocly custom syntax as well (https://redocly.com/docs/api-reference-docs/specification-extensions/x-code-samples/) */ export function OpenAPICodeSample(props: { data: OpenAPIOperationData; context: OpenAPIContext; }) { const { data, context } = props; // If code samples are disabled at operation level, we don't display the code samples. if (data.operation['x-codeSamples'] === false) { return null; } const customCodeSamples = getCustomCodeSamples(props); // If code samples are disabled at the top-level and not custom code samples are defined, // we don't display the code samples. if (data['x-codeSamples'] === false && !customCodeSamples) { return null; } const samples = customCodeSamples ?? generateCodeSamples(props); if (samples.length === 0) { return null; } return ( ); } /** * Generate code samples for the operation. */ function generateCodeSamples(props: { data: OpenAPIOperationData; context: OpenAPIContext; }) { const { data, context } = props; const searchParams = new URLSearchParams(); const headersObject: { [k: string]: string } = {}; // The parser can sometimes returns invalid parameters (an object instead of an array). // It should get fixed in scalar, but in the meantime we just ignore the parameters in that case. const params = Array.isArray(data.operation.parameters) ? data.operation.parameters : []; params.forEach((param) => { if (!param) { return; } if (param.in === 'header' && param.required) { const example = param.schema ? generateSchemaExample(param.schema, { mode: 'write' }) : undefined; if (example !== undefined && param.name) { headersObject[param.name] = typeof example !== 'string' ? stringifyOpenAPI(example) : example; } } else if (param.in === 'query' && param.required) { const example = param.schema ? generateSchemaExample(param.schema, { mode: 'write' }) : undefined; if (example !== undefined && param.name) { searchParams.append( param.name, String(Array.isArray(example) ? example[0] : example) ); } } }); const requestBody = !checkIsReference(data.operation.requestBody) ? data.operation.requestBody : undefined; const defaultServerUrl = getDefaultServerURL(data.servers); let serverUrlPath = defaultServerUrl ? parseHostAndPath(defaultServerUrl).path : ''; serverUrlPath = serverUrlPath === '/' ? '' : serverUrlPath; const serverUrl = data.servers[0] ? resolveURLWithPrefillCodePlaceholdersFromServer(data.servers[0], defaultServerUrl) : defaultServerUrl; const serverUrlOrigin = serverUrl.replaceAll(serverUrlPath, ''); const path = serverUrlPath + data.path + (searchParams.size ? `?${searchParams.toString()}` : ''); const genericHeaders = { ...getSecurityHeaders({ securityRequirement: data.operation.security, securities: data.securities, }), ...headersObject, }; const mediaTypeRendererFactories = Object.entries(requestBody?.content ?? {}).map( ([mediaType, mediaTypeObject]) => { return (generator: CodeSampleGenerator) => { const mediaTypeHeaders = mergeHeaders(genericHeaders, { 'Content-Type': mediaType, }); return { mediaType, element: context.renderCodeBlock({ code: generator.generate({ url: { origin: serverUrlOrigin, path }, method: data.method, body: undefined, headers: mediaTypeHeaders, }), syntax: generator.syntax, }), examples: generateMediaTypeExamples(mediaTypeObject, { mode: 'write', }).map((example) => ({ example, element: context.renderCodeBlock({ code: generator.generate({ url: { origin: serverUrlOrigin, path }, method: data.method, body: example.value, headers: mediaTypeHeaders, }), syntax: generator.syntax, }), })), } satisfies MediaTypeRenderer; }; } ); return codeSampleGenerators.map((generator) => { if (mediaTypeRendererFactories.length > 0) { const renderers = mediaTypeRendererFactories.map((generate) => generate(generator)); return { key: `default-${generator.id}`, label: generator.label, body: ( ), footer: ( ), }; } return { key: `default-${generator.id}`, label: generator.label, body: context.renderCodeBlock({ code: generator.generate({ url: { origin: serverUrlOrigin, path }, method: data.method, body: undefined, headers: genericHeaders, }), syntax: generator.syntax, }), footer: , }; }); } export interface MediaTypeRenderer { mediaType: string; element: React.ReactNode; examples: Array<{ example: OpenAPIV3.ExampleObject; element: React.ReactNode; }>; } function OpenAPICodeSampleFooter(props: { data: OpenAPIOperationData; renderers: MediaTypeRenderer[]; context: OpenAPIContext; }) { const { data, context, renderers } = props; const { method, path, securities, servers } = data; const { specUrl } = context; const hideTryItPanel = data['x-hideTryItPanel'] || data.operation['x-hideTryItPanel']; const hasMultipleMediaTypes = renderers.length >= 2 || renderers.some((renderer) => renderer.examples.length >= 2); // Check if any server has a host that can be used in an HTTP request const hasValidHost = hasValidServerHost(servers); if (hideTryItPanel && !hasMultipleMediaTypes) { return null; } if (!validateHttpMethod(method) || (!hasMultipleMediaTypes && !hasValidHost)) { return null; } return (
{hasMultipleMediaTypes ? ( ) : ( )} {!hideTryItPanel && hasValidHost && specUrl && ( )}
); } /** * Build the client context for ScalarApiButton, resolving the signed proxy URL * with the allowed server hosts for SSRF protection. */ function resolveScalarClientContext( context: OpenAPIContext, servers: OpenAPIOperationData['servers'], specUrl: string ) { const clientContext = getOpenAPIClientContext(context); if (context.resolveProxyUrl) { // Collect all possible host+path entries from spec servers const origins = getAllServerOrigins(servers); // Add the spec URL so the proxy can resolve it const specOrigin = extractOrigin(specUrl); if (specOrigin) { origins.push(specOrigin); } clientContext.proxyUrl = context.resolveProxyUrl(origins) ?? undefined; } return clientContext; } /** * Get custom code samples for the operation. */ function getCustomCodeSamples(props: { data: OpenAPIOperationData; context: OpenAPIContext; }) { const { data, context } = props; let customCodeSamples: null | Array<{ key: string; label: string; body: React.ReactNode; }> = null; CUSTOM_CODE_SAMPLES_KEYS.forEach((key) => { const customSamples = data.operation[key]; if (customSamples && Array.isArray(customSamples)) { customCodeSamples = customSamples .filter((sample) => { return typeof sample.source === 'string' && typeof sample.lang === 'string'; }) .map((sample, index) => ({ key: `custom-sample-${sample.lang}-${index}`, label: sample.label || sample.lang, body: context.renderCodeBlock({ code: sample.source, syntax: sample.lang, }), footer: ( ), })); } }); return customCodeSamples; } export function getSecurityHeaders(args: { securityRequirement: OpenAPIV3.OperationObject['security']; securities: OpenAPIOperationData['securities']; }): { [key: string]: string; } { const { securityRequirement, securities } = args; const operationSecurityInfo = extractOperationSecurityInfo({ securityRequirement, securities }); if (operationSecurityInfo.length === 0) { return {}; } const selectedSecurity = operationSecurityInfo.at(0); if (!selectedSecurity) { return {}; } const headers: { [key: string]: string } = {}; for (const security of selectedSecurity.schemes) { switch (security.type) { case 'http': { // We do not use x-gitbook-prefix for http schemes to avoid confusion with the standard. let scheme = security.scheme; const defaultPlaceholderValue = scheme?.toLowerCase()?.includes('basic') ? 'username:password' : 'YOUR_SECRET_TOKEN'; const format = resolvePrefillCodePlaceholderFromSecurityScheme({ security: security, defaultPlaceholderValue, }); if (scheme?.includes('bearer')) { scheme = 'Bearer'; } else if (scheme?.includes('basic')) { scheme = 'Basic'; } else if (scheme?.includes('token')) { scheme = 'Token'; } else { scheme = scheme ?? ''; } headers.Authorization = `${scheme} ${format}`; break; } case 'apiKey': { if (security.in !== 'header') { break; } const name = security.name ?? 'Authorization'; const placeholder = resolvePrefillCodePlaceholderFromSecurityScheme({ security: security, defaultPlaceholderValue: 'YOUR_API_KEY', }); // Use x-gitbook-prefix if provided for apiKey schemes const prefix = security['x-gitbook-prefix']; headers[name] = prefix ? `${prefix} ${placeholder}` : placeholder; break; } case 'oauth2': { const prefix = security['x-gitbook-prefix'] ?? 'Bearer'; headers.Authorization = `${prefix} ${resolvePrefillCodePlaceholderFromSecurityScheme( { security: security, defaultPlaceholderValue: 'YOUR_OAUTH2_TOKEN', } )}`; break; } default: { break; } } } return headers; } function validateHttpMethod(method: string): method is OpenAPIV3.HttpMethods { return ['get', 'post', 'put', 'delete', 'patch', 'head', 'options', 'trace'].includes(method); }