Skip to content

Commit 461e15f

Browse files
authored
Add x-gitbook-prefix and x-gitbook-token-placeholder for OpenAPI security scheme (#3820)
1 parent 2989f79 commit 461e15f

File tree

6 files changed

+135
-6
lines changed

6 files changed

+135
-6
lines changed

.changeset/five-worlds-kneel.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
---
2+
'@gitbook/openapi-parser': patch
3+
'@gitbook/react-openapi': patch
4+
---
5+
6+
Add x-gitbook-prefix and x-gitbook-token-placeholder for OpenAPI security scheme

packages/openapi-parser/src/types.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,14 @@ export interface OpenAPICustomOperationProperties {
7575
*/
7676
export interface OpenAPICustomPrefillProperties {
7777
'x-gitbook-prefill'?: string;
78+
/**
79+
* Token placeholder used inside sample credentials (e.g., "Bearer ${token}").
80+
*/
81+
'x-gitbook-token-placeholder'?: string;
82+
/**
83+
* Prefix to override the default one for the security scheme (e.g., "Bearer", "Basic", "Token").
84+
*/
85+
'x-gitbook-prefix'?: string;
7886
}
7987

8088
export type OpenAPIStability = 'experimental' | 'alpha' | 'beta';
Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
import { describe, expect, it } from 'bun:test';
2+
import { getSecurityHeaders } from './OpenAPICodeSample';
3+
import type { OpenAPIOperationData } from './types';
4+
5+
describe('getSecurityHeaders', () => {
6+
it('should handle custom HTTP scheme with x-gitbook-prefix', () => {
7+
const securities: OpenAPIOperationData['securities'] = [
8+
[
9+
'customScheme',
10+
{
11+
type: 'apiKey',
12+
in: 'header',
13+
name: 'Authorization',
14+
'x-gitbook-prefix': 'CustomScheme',
15+
},
16+
],
17+
];
18+
19+
const result = getSecurityHeaders({
20+
securityRequirement: [{ customScheme: [] }],
21+
securities,
22+
});
23+
24+
expect(result).toEqual({
25+
Authorization: 'CustomScheme YOUR_API_KEY',
26+
});
27+
});
28+
29+
it('should use x-gitbook-prefix with x-gitbook-token-placeholder together', () => {
30+
const securities: OpenAPIOperationData['securities'] = [
31+
[
32+
'customAuth',
33+
{
34+
type: 'apiKey',
35+
in: 'header',
36+
name: 'Authorization',
37+
'x-gitbook-prefix': 'Token',
38+
'x-gitbook-token-placeholder': 'MY_CUSTOM_TOKEN',
39+
},
40+
],
41+
];
42+
43+
const result = getSecurityHeaders({
44+
securityRequirement: [{ customAuth: [] }],
45+
securities,
46+
});
47+
48+
expect(result).toEqual({
49+
Authorization: 'Token MY_CUSTOM_TOKEN',
50+
});
51+
});
52+
53+
it('should not use x-gitbook-prefix for http scheme', () => {
54+
const securities: OpenAPIOperationData['securities'] = [
55+
[
56+
'customAuth',
57+
{
58+
type: 'http',
59+
in: 'header',
60+
name: 'Authorization',
61+
scheme: 'bearer',
62+
'x-gitbook-prefix': 'Token',
63+
},
64+
],
65+
];
66+
67+
const result = getSecurityHeaders({
68+
securityRequirement: [{ customAuth: [] }],
69+
securities,
70+
});
71+
72+
expect(result).toEqual({
73+
Authorization: 'Bearer YOUR_SECRET_TOKEN',
74+
});
75+
});
76+
});

packages/react-openapi/src/OpenAPICodeSample.tsx

Lines changed: 15 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -290,7 +290,7 @@ function getCustomCodeSamples(props: {
290290
return customCodeSamples;
291291
}
292292

293-
function getSecurityHeaders(args: {
293+
export function getSecurityHeaders(args: {
294294
securityRequirement: OpenAPIV3.OperationObject['security'];
295295
securities: OpenAPIOperationData['securities'];
296296
}): {
@@ -314,6 +314,7 @@ function getSecurityHeaders(args: {
314314
for (const security of selectedSecurity.schemes) {
315315
switch (security.type) {
316316
case 'http': {
317+
// We do not use x-gitbook-prefix for http schemes to avoid confusion with the standard.
317318
let scheme = security.scheme;
318319
const defaultPlaceholderValue = scheme?.toLowerCase()?.includes('basic')
319320
? 'username:password'
@@ -329,6 +330,8 @@ function getSecurityHeaders(args: {
329330
scheme = 'Basic';
330331
} else if (scheme?.includes('token')) {
331332
scheme = 'Token';
333+
} else {
334+
scheme = scheme ?? '';
332335
}
333336

334337
headers.Authorization = `${scheme} ${format}`;
@@ -339,17 +342,23 @@ function getSecurityHeaders(args: {
339342
break;
340343
}
341344
const name = security.name ?? 'Authorization';
342-
headers[name] = resolvePrefillCodePlaceholderFromSecurityScheme({
345+
const placeholder = resolvePrefillCodePlaceholderFromSecurityScheme({
343346
security: security,
344347
defaultPlaceholderValue: 'YOUR_API_KEY',
345348
});
349+
// Use x-gitbook-prefix if provided for apiKey schemes
350+
const prefix = security['x-gitbook-prefix'];
351+
headers[name] = prefix ? `${prefix} ${placeholder}` : placeholder;
346352
break;
347353
}
348354
case 'oauth2': {
349-
headers.Authorization = `Bearer ${resolvePrefillCodePlaceholderFromSecurityScheme({
350-
security: security,
351-
defaultPlaceholderValue: 'YOUR_OAUTH2_TOKEN',
352-
})}`;
355+
const prefix = security['x-gitbook-prefix'] ?? 'Bearer';
356+
headers.Authorization = `${prefix} ${resolvePrefillCodePlaceholderFromSecurityScheme(
357+
{
358+
security: security,
359+
defaultPlaceholderValue: 'YOUR_OAUTH2_TOKEN',
360+
}
361+
)}`;
353362
break;
354363
}
355364
default: {

packages/react-openapi/src/util/tryit-prefill.test.ts

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -415,6 +415,32 @@ describe('resolvePrefillCodePlaceholderFromSecurityScheme (integration style)',
415415

416416
expect(result).toBe('');
417417
});
418+
419+
it('should prioritize x-gitbook-prefill over x-gitbook-token-placeholder when both are present', () => {
420+
const result = resolvePrefillCodePlaceholderFromSecurityScheme({
421+
security: {
422+
type: 'apiKey',
423+
in: 'header',
424+
'x-gitbook-prefill': '{{ visitor.claims.apiToken }}',
425+
'x-gitbook-token-placeholder': 'API_TOKEN_KEY',
426+
},
427+
});
428+
429+
expect(result).toBe('$$__X-GITBOOK-PREFILL[(visitor.claims.apiToken)]__$$');
430+
});
431+
432+
it('should return x-gitbook-token-placeholder for apiKey scheme', () => {
433+
const result = resolvePrefillCodePlaceholderFromSecurityScheme({
434+
security: {
435+
type: 'apiKey',
436+
in: 'header',
437+
name: 'X-API-KEY',
438+
'x-gitbook-token-placeholder': 'YOUR_API_KEY_HERE',
439+
},
440+
});
441+
442+
expect(result).toBe('YOUR_API_KEY_HERE');
443+
});
418444
});
419445

420446
describe('resolveURLWithPrefillCodePlaceholdersFromServer', () => {

packages/react-openapi/src/util/tryit-prefill.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -177,6 +177,10 @@ export function resolvePrefillCodePlaceholderFromSecurityScheme(args: {
177177
const prefillExprParts = extractPrefillExpressionPartsFromSecurityScheme(security);
178178

179179
if (prefillExprParts.length === 0) {
180+
// If no x-gitbook-prefill, check for x-gitbook-token-placeholder
181+
if (security['x-gitbook-token-placeholder']) {
182+
return security['x-gitbook-token-placeholder'];
183+
}
180184
return defaultPlaceholderValue ?? '';
181185
}
182186
const prefillExpr = templatePartsToExpression(prefillExprParts);

0 commit comments

Comments
 (0)