Skip to content

Commit afa0293

Browse files
fix(docs): include API key header in generated code samples (#6630)
1 parent 74212ef commit afa0293

4 files changed

Lines changed: 166 additions & 1 deletion

File tree

apps/docs/app/[lang]/[[...slug]]/page.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ import { CodeBlock } from '@/components/ui/code-block'
1616
import { Heading } from '@/components/ui/heading'
1717
import { ResponseSection } from '@/components/ui/response-section'
1818
import { i18n } from '@/lib/i18n'
19-
import { getApiSpecContent, openapi } from '@/lib/openapi'
19+
import { getApiSpecContent, getAuthenticatedCodeSamples, openapi } from '@/lib/openapi'
2020
import { type PageData, source } from '@/lib/source'
2121
import { DOCS_BASE_URL } from '@/lib/urls'
2222

@@ -71,6 +71,7 @@ function stripLocalePrefix(url: string, lang: string): string {
7171

7272
const APIPage = createAPIPage(openapi, {
7373
playground: { enabled: false },
74+
generateCodeSamples: getAuthenticatedCodeSamples,
7475
client: {
7576
operation: { APIExampleSelector },
7677
},
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
'use client'
2+
3+
import type { CodeUsageGeneratorFn } from 'fumadocs-openapi/requests/generators'
4+
import { createCodeUsageGeneratorRegistry } from 'fumadocs-openapi/requests/generators'
5+
import { registerDefault } from 'fumadocs-openapi/requests/generators/all'
6+
7+
/**
8+
* Context handed to {@link generateWithAuth} by the server: which built-in
9+
* generator to delegate to, and the auth headers the sample must send.
10+
*/
11+
export interface AuthCodeSampleContext {
12+
generatorId: string
13+
headers: Record<string, string>
14+
}
15+
16+
const generators = createCodeUsageGeneratorRegistry()
17+
registerDefault(generators)
18+
19+
/**
20+
* Wraps a built-in code-usage generator so the sample carries the operation's
21+
* security headers. Fumadocs builds request data from declared parameters only,
22+
* so an operation's security requirement never reaches the generated snippet.
23+
*/
24+
export const generateWithAuth: CodeUsageGeneratorFn = (url, data, context) => {
25+
const { generatorId, headers } = context.server as AuthCodeSampleContext
26+
const generator = generators.get(generatorId)
27+
if (!generator) {
28+
throw new Error(`[docs] Unknown code usage generator: ${generatorId}`)
29+
}
30+
31+
const authHeaders: Record<string, { value: string }> = {}
32+
for (const [name, value] of Object.entries(headers)) {
33+
authHeaders[name] = { value }
34+
}
35+
36+
return generator.generate(url, { ...data, header: { ...authHeaders, ...data.header } }, context)
37+
}
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
import type { InlineCodeUsageGenerator } from 'fumadocs-openapi/requests/generators'
2+
import { createCodeUsageGeneratorRegistry } from 'fumadocs-openapi/requests/generators'
3+
import { registerDefault } from 'fumadocs-openapi/requests/generators/all'
4+
import { generateWithAuth } from '@/lib/openapi-code-samples-client'
5+
6+
const generators = createCodeUsageGeneratorRegistry()
7+
registerDefault(generators)
8+
9+
/**
10+
* Replace every built-in language sample with one that prepends `headers`,
11+
* preserving the built-in tab order, language, and label.
12+
*/
13+
export function buildAuthCodeSamples(headers: Record<string, string>): InlineCodeUsageGenerator[] {
14+
return Array.from(generators.map().entries()).map(([id, generator]) => ({
15+
id,
16+
lang: generator.lang,
17+
label: generator.label,
18+
source: generateWithAuth,
19+
serverContext: { generatorId: id, headers },
20+
}))
21+
}

apps/docs/lib/openapi.ts

Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
11
import { readFileSync } from 'node:fs'
22
import { join } from 'node:path'
3+
import type { MethodInformation } from 'fumadocs-openapi'
4+
import type { InlineCodeUsageGenerator } from 'fumadocs-openapi/requests/generators'
35
import { createOpenAPI } from 'fumadocs-openapi/server'
6+
import { buildAuthCodeSamples } from '@/lib/openapi-code-samples'
47
import { OPENAPI_SPEC_FILES } from '@/lib/openapi-specs'
58

69
export const openapi = createOpenAPI({
@@ -75,6 +78,109 @@ function getSpecs(): Record<string, unknown>[] {
7578
return cachedSpecs
7679
}
7780

81+
type SecurityRequirement = Record<string, string[]>
82+
83+
interface SecurityScheme {
84+
type?: string
85+
in?: string
86+
name?: string
87+
scheme?: string
88+
}
89+
90+
interface SharedSecurity {
91+
security: SecurityRequirement[]
92+
schemes: Record<string, SecurityScheme>
93+
}
94+
95+
const AUTH_SAMPLE_VALUE = 'YOUR_API_KEY'
96+
97+
let cachedSharedSecurity: SharedSecurity | null = null
98+
99+
/**
100+
* Document-level security shared by every rendered spec. Code samples are
101+
* generated from an operation alone, with no handle on the document that owns
102+
* it, so the specs must agree on their default security — a spec that diverges
103+
* would silently get another document's auth in its samples.
104+
*/
105+
function getSharedSecurity(): SharedSecurity {
106+
if (cachedSharedSecurity) return cachedSharedSecurity
107+
108+
let shared: SharedSecurity | undefined
109+
let sharedFile: string | undefined
110+
111+
getSpecs().forEach((spec, index) => {
112+
const file = OPENAPI_SPEC_FILES[index]
113+
const current: SharedSecurity = {
114+
security: (spec.security as SecurityRequirement[] | undefined) ?? [],
115+
schemes:
116+
((spec.components as Record<string, unknown> | undefined)?.securitySchemes as
117+
| Record<string, SecurityScheme>
118+
| undefined) ?? {},
119+
}
120+
121+
if (!shared) {
122+
shared = current
123+
sharedFile = file
124+
return
125+
}
126+
127+
if (JSON.stringify(current) !== JSON.stringify(shared)) {
128+
throw new Error(
129+
`[docs] ${file} declares different default security than ${sharedFile}. Every OpenAPI spec must share one security scheme so generated code samples stay correct.`
130+
)
131+
}
132+
})
133+
134+
cachedSharedSecurity = shared ?? { security: [], schemes: {} }
135+
return cachedSharedSecurity
136+
}
137+
138+
/**
139+
* Resolve a security requirement to the request headers a sample must send.
140+
* The first non-empty alternative wins — an empty one means the operation also
141+
* accepts anonymous callers, which is not what a reference example should show.
142+
*/
143+
function resolveAuthHeaders(
144+
security: SecurityRequirement[],
145+
schemes: Record<string, SecurityScheme>
146+
): Record<string, string> {
147+
const requirement = security.find((item) => Object.keys(item).length > 0)
148+
if (!requirement) return {}
149+
150+
const headers: Record<string, string> = {}
151+
for (const name of Object.keys(requirement)) {
152+
const scheme = schemes[name]
153+
if (!scheme) {
154+
throw new Error(`[docs] Operation references undefined security scheme "${name}"`)
155+
}
156+
if (scheme.type === 'apiKey' && scheme.in === 'header' && scheme.name) {
157+
headers[scheme.name] = AUTH_SAMPLE_VALUE
158+
continue
159+
}
160+
if (scheme.type === 'http' && scheme.scheme === 'bearer') {
161+
headers.Authorization = `Bearer ${AUTH_SAMPLE_VALUE}`
162+
continue
163+
}
164+
throw new Error(
165+
`[docs] Security scheme "${name}" (type ${scheme.type}) cannot be rendered as a request header in code samples`
166+
)
167+
}
168+
return headers
169+
}
170+
171+
/**
172+
* Code samples for an operation, with its authentication header included.
173+
* Fumadocs derives sample requests from declared parameters only, so without
174+
* this every endpoint documents an unauthenticated call that returns `401`.
175+
*/
176+
export function getAuthenticatedCodeSamples(method: MethodInformation): InlineCodeUsageGenerator[] {
177+
const shared = getSharedSecurity()
178+
const security = (method.security as SecurityRequirement[] | undefined) ?? shared.security
179+
const headers = resolveAuthHeaders(security, shared.schemes)
180+
if (Object.keys(headers).length === 0) return []
181+
return buildAuthCodeSamples(headers)
182+
}
183+
78184
/**
79185
* Locate an operation by path + method across every rendered spec, returning the
80186
* operation together with the spec that owns it so `$ref`s resolve within the

0 commit comments

Comments
 (0)