Skip to content

Commit 3fb7422

Browse files
committed
Add x-prefix and x-placeholder for OpenAPI security scheme
1 parent c51076e commit 3fb7422

File tree

5 files changed

+151
-13
lines changed

5 files changed

+151
-13
lines changed

packages/openapi-parser/src/types.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,15 @@ export interface OpenAPICustomOperationProperties {
7575
*/
7676
export interface OpenAPICustomPrefillProperties {
7777
'x-gitbook-prefill'?: string;
78+
/**
79+
* Placeholder to override the default one for the security scheme.
80+
*/
81+
'x-placeholder'?: string;
82+
/**
83+
* Prefix to override the default one for the security scheme (e.g., "Bearer", "Basic", "Token").
84+
* Used in Authorization headers for HTTP and OAuth2 security schemes.
85+
*/
86+
'x-prefix'?: string;
7887
}
7988

8089
export type OpenAPIStability = 'experimental' | 'alpha' | 'beta';
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
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-prefix', () => {
7+
const securities: OpenAPIOperationData['securities'] = [
8+
[
9+
'customScheme',
10+
{
11+
type: 'apiKey',
12+
in: 'header',
13+
name: 'Authorization',
14+
'x-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_SECRET_TOKEN',
26+
});
27+
});
28+
29+
it('should use x-prefix with x-placeholder together', () => {
30+
const securities: OpenAPIOperationData['securities'] = [
31+
[
32+
'customAuth',
33+
{
34+
type: 'apiKey',
35+
in: 'header',
36+
name: 'Authorization',
37+
'x-prefix': 'Token',
38+
'x-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+
});

packages/react-openapi/src/OpenAPICodeSample.tsx

Lines changed: 22 additions & 13 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,7 +314,7 @@ function getSecurityHeaders(args: {
314314
for (const security of selectedSecurity.schemes) {
315315
switch (security.type) {
316316
case 'http': {
317-
let scheme = security.scheme;
317+
const scheme = security.scheme;
318318
const defaultPlaceholderValue = scheme?.toLowerCase()?.includes('basic')
319319
? 'username:password'
320320
: 'YOUR_SECRET_TOKEN';
@@ -323,15 +323,21 @@ function getSecurityHeaders(args: {
323323
defaultPlaceholderValue,
324324
});
325325

326-
if (scheme?.includes('bearer')) {
327-
scheme = 'Bearer';
328-
} else if (scheme?.includes('basic')) {
329-
scheme = 'Basic';
330-
} else if (scheme?.includes('token')) {
331-
scheme = 'Token';
326+
// Use x-prefix if provided, otherwise fall back to default logic
327+
let prefix = security['x-prefix'];
328+
if (!prefix) {
329+
if (scheme?.includes('bearer')) {
330+
prefix = 'Bearer';
331+
} else if (scheme?.includes('basic')) {
332+
prefix = 'Basic';
333+
} else if (scheme?.includes('token')) {
334+
prefix = 'Token';
335+
} else {
336+
prefix = scheme ?? '';
337+
}
332338
}
333339

334-
headers.Authorization = `${scheme} ${format}`;
340+
headers.Authorization = `${prefix} ${format}`;
335341
break;
336342
}
337343
case 'apiKey': {
@@ -346,10 +352,13 @@ function getSecurityHeaders(args: {
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-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: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -415,6 +415,70 @@ describe('resolvePrefillCodePlaceholderFromSecurityScheme (integration style)',
415415

416416
expect(result).toBe('');
417417
});
418+
419+
it('should return x-placeholder when x-gitbook-prefill is not present', () => {
420+
const result = resolvePrefillCodePlaceholderFromSecurityScheme({
421+
security: {
422+
type: 'apiKey',
423+
in: 'header',
424+
name: 'Authorization',
425+
'x-placeholder': 'API_TOKEN_KEY',
426+
},
427+
});
428+
429+
expect(result).toBe('API_TOKEN_KEY');
430+
});
431+
432+
it('should use x-placeholder with defaultPlaceholderValue fallback', () => {
433+
const result = resolvePrefillCodePlaceholderFromSecurityScheme({
434+
security: {
435+
type: 'http',
436+
scheme: 'bearer',
437+
'x-placeholder': 'MY_CUSTOM_TOKEN',
438+
},
439+
defaultPlaceholderValue: 'YOUR_API_TOKEN',
440+
});
441+
442+
expect(result).toBe('MY_CUSTOM_TOKEN');
443+
});
444+
445+
it('should prioritize x-gitbook-prefill over x-placeholder when both are present', () => {
446+
const result = resolvePrefillCodePlaceholderFromSecurityScheme({
447+
security: {
448+
type: 'http',
449+
scheme: 'bearer',
450+
'x-gitbook-prefill': '{{ visitor.claims.apiToken }}',
451+
'x-placeholder': 'API_TOKEN_KEY',
452+
},
453+
});
454+
455+
expect(result).toBe('$$__X-GITBOOK-PREFILL[(visitor.claims.apiToken)]__$$');
456+
});
457+
458+
it('should return x-placeholder for apiKey scheme', () => {
459+
const result = resolvePrefillCodePlaceholderFromSecurityScheme({
460+
security: {
461+
type: 'apiKey',
462+
in: 'header',
463+
name: 'X-API-KEY',
464+
'x-placeholder': 'YOUR_API_KEY_HERE',
465+
},
466+
});
467+
468+
expect(result).toBe('YOUR_API_KEY_HERE');
469+
});
470+
471+
it('should return x-placeholder for basic auth scheme', () => {
472+
const result = resolvePrefillCodePlaceholderFromSecurityScheme({
473+
security: {
474+
type: 'http',
475+
scheme: 'basic',
476+
'x-placeholder': 'username:password',
477+
},
478+
});
479+
480+
expect(result).toBe('username:password');
481+
});
418482
});
419483

420484
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-placeholder
181+
if (security['x-placeholder']) {
182+
return security['x-placeholder'];
183+
}
180184
return defaultPlaceholderValue ?? '';
181185
}
182186
const prefillExpr = templatePartsToExpression(prefillExprParts);

0 commit comments

Comments
 (0)