fix(docs): include API key header in generated code samples (#6630)

This commit is contained in:
Theodore Li
2026-08-12 13:58:39 -04:00
committed by GitHub
parent 74212ef333
commit afa02939bb
4 changed files with 166 additions and 1 deletions
+2 -1
View File
@@ -16,7 +16,7 @@ import { CodeBlock } from '@/components/ui/code-block'
import { Heading } from '@/components/ui/heading'
import { ResponseSection } from '@/components/ui/response-section'
import { i18n } from '@/lib/i18n'
import { getApiSpecContent, openapi } from '@/lib/openapi'
import { getApiSpecContent, getAuthenticatedCodeSamples, openapi } from '@/lib/openapi'
import { type PageData, source } from '@/lib/source'
import { DOCS_BASE_URL } from '@/lib/urls'
@@ -71,6 +71,7 @@ function stripLocalePrefix(url: string, lang: string): string {
const APIPage = createAPIPage(openapi, {
playground: { enabled: false },
generateCodeSamples: getAuthenticatedCodeSamples,
client: {
operation: { APIExampleSelector },
},
@@ -0,0 +1,37 @@
'use client'
import type { CodeUsageGeneratorFn } from 'fumadocs-openapi/requests/generators'
import { createCodeUsageGeneratorRegistry } from 'fumadocs-openapi/requests/generators'
import { registerDefault } from 'fumadocs-openapi/requests/generators/all'
/**
* Context handed to {@link generateWithAuth} by the server: which built-in
* generator to delegate to, and the auth headers the sample must send.
*/
export interface AuthCodeSampleContext {
generatorId: string
headers: Record<string, string>
}
const generators = createCodeUsageGeneratorRegistry()
registerDefault(generators)
/**
* Wraps a built-in code-usage generator so the sample carries the operation's
* security headers. Fumadocs builds request data from declared parameters only,
* so an operation's security requirement never reaches the generated snippet.
*/
export const generateWithAuth: CodeUsageGeneratorFn = (url, data, context) => {
const { generatorId, headers } = context.server as AuthCodeSampleContext
const generator = generators.get(generatorId)
if (!generator) {
throw new Error(`[docs] Unknown code usage generator: ${generatorId}`)
}
const authHeaders: Record<string, { value: string }> = {}
for (const [name, value] of Object.entries(headers)) {
authHeaders[name] = { value }
}
return generator.generate(url, { ...data, header: { ...authHeaders, ...data.header } }, context)
}
+21
View File
@@ -0,0 +1,21 @@
import type { InlineCodeUsageGenerator } from 'fumadocs-openapi/requests/generators'
import { createCodeUsageGeneratorRegistry } from 'fumadocs-openapi/requests/generators'
import { registerDefault } from 'fumadocs-openapi/requests/generators/all'
import { generateWithAuth } from '@/lib/openapi-code-samples-client'
const generators = createCodeUsageGeneratorRegistry()
registerDefault(generators)
/**
* Replace every built-in language sample with one that prepends `headers`,
* preserving the built-in tab order, language, and label.
*/
export function buildAuthCodeSamples(headers: Record<string, string>): InlineCodeUsageGenerator[] {
return Array.from(generators.map().entries()).map(([id, generator]) => ({
id,
lang: generator.lang,
label: generator.label,
source: generateWithAuth,
serverContext: { generatorId: id, headers },
}))
}
+106
View File
@@ -1,6 +1,9 @@
import { readFileSync } from 'node:fs'
import { join } from 'node:path'
import type { MethodInformation } from 'fumadocs-openapi'
import type { InlineCodeUsageGenerator } from 'fumadocs-openapi/requests/generators'
import { createOpenAPI } from 'fumadocs-openapi/server'
import { buildAuthCodeSamples } from '@/lib/openapi-code-samples'
import { OPENAPI_SPEC_FILES } from '@/lib/openapi-specs'
export const openapi = createOpenAPI({
@@ -75,6 +78,109 @@ function getSpecs(): Record<string, unknown>[] {
return cachedSpecs
}
type SecurityRequirement = Record<string, string[]>
interface SecurityScheme {
type?: string
in?: string
name?: string
scheme?: string
}
interface SharedSecurity {
security: SecurityRequirement[]
schemes: Record<string, SecurityScheme>
}
const AUTH_SAMPLE_VALUE = 'YOUR_API_KEY'
let cachedSharedSecurity: SharedSecurity | null = null
/**
* Document-level security shared by every rendered spec. Code samples are
* generated from an operation alone, with no handle on the document that owns
* it, so the specs must agree on their default security — a spec that diverges
* would silently get another document's auth in its samples.
*/
function getSharedSecurity(): SharedSecurity {
if (cachedSharedSecurity) return cachedSharedSecurity
let shared: SharedSecurity | undefined
let sharedFile: string | undefined
getSpecs().forEach((spec, index) => {
const file = OPENAPI_SPEC_FILES[index]
const current: SharedSecurity = {
security: (spec.security as SecurityRequirement[] | undefined) ?? [],
schemes:
((spec.components as Record<string, unknown> | undefined)?.securitySchemes as
| Record<string, SecurityScheme>
| undefined) ?? {},
}
if (!shared) {
shared = current
sharedFile = file
return
}
if (JSON.stringify(current) !== JSON.stringify(shared)) {
throw new Error(
`[docs] ${file} declares different default security than ${sharedFile}. Every OpenAPI spec must share one security scheme so generated code samples stay correct.`
)
}
})
cachedSharedSecurity = shared ?? { security: [], schemes: {} }
return cachedSharedSecurity
}
/**
* Resolve a security requirement to the request headers a sample must send.
* The first non-empty alternative wins — an empty one means the operation also
* accepts anonymous callers, which is not what a reference example should show.
*/
function resolveAuthHeaders(
security: SecurityRequirement[],
schemes: Record<string, SecurityScheme>
): Record<string, string> {
const requirement = security.find((item) => Object.keys(item).length > 0)
if (!requirement) return {}
const headers: Record<string, string> = {}
for (const name of Object.keys(requirement)) {
const scheme = schemes[name]
if (!scheme) {
throw new Error(`[docs] Operation references undefined security scheme "${name}"`)
}
if (scheme.type === 'apiKey' && scheme.in === 'header' && scheme.name) {
headers[scheme.name] = AUTH_SAMPLE_VALUE
continue
}
if (scheme.type === 'http' && scheme.scheme === 'bearer') {
headers.Authorization = `Bearer ${AUTH_SAMPLE_VALUE}`
continue
}
throw new Error(
`[docs] Security scheme "${name}" (type ${scheme.type}) cannot be rendered as a request header in code samples`
)
}
return headers
}
/**
* Code samples for an operation, with its authentication header included.
* Fumadocs derives sample requests from declared parameters only, so without
* this every endpoint documents an unauthenticated call that returns `401`.
*/
export function getAuthenticatedCodeSamples(method: MethodInformation): InlineCodeUsageGenerator[] {
const shared = getSharedSecurity()
const security = (method.security as SecurityRequirement[] | undefined) ?? shared.security
const headers = resolveAuthHeaders(security, shared.schemes)
if (Object.keys(headers).length === 0) return []
return buildAuthCodeSamples(headers)
}
/**
* Locate an operation by path + method across every rendered spec, returning the
* operation together with the spec that owns it so `$ref`s resolve within the