From e3a829d30cccaa9a7ad839eec4bcbb2d5ba3a947 Mon Sep 17 00:00:00 2001 From: Nolann Biron Date: Wed, 19 Nov 2025 14:29:17 +0100 Subject: [PATCH 1/4] Add `x-prefix` and `x-placeholder` for OpenAPI security scheme --- packages/openapi-parser/src/types.ts | 8 +++ .../src/OpenAPICodeSample.test.ts | 52 +++++++++++++++++++ .../react-openapi/src/OpenAPICodeSample.tsx | 40 +++++++++----- .../src/util/tryit-prefill.test.ts | 26 ++++++++++ .../react-openapi/src/util/tryit-prefill.ts | 4 ++ 5 files changed, 116 insertions(+), 14 deletions(-) create mode 100644 packages/react-openapi/src/OpenAPICodeSample.test.ts diff --git a/packages/openapi-parser/src/types.ts b/packages/openapi-parser/src/types.ts index a1af691ec3..4c00ce1893 100644 --- a/packages/openapi-parser/src/types.ts +++ b/packages/openapi-parser/src/types.ts @@ -75,6 +75,14 @@ export interface OpenAPICustomOperationProperties { */ export interface OpenAPICustomPrefillProperties { 'x-gitbook-prefill'?: string; + /** + * Placeholder to override the default one for the security scheme. + */ + 'x-placeholder'?: string; + /** + * Prefix to override the default one for the security scheme (e.g., "Bearer", "Basic", "Token"). + */ + 'x-prefix'?: string; } export type OpenAPIStability = 'experimental' | 'alpha' | 'beta'; diff --git a/packages/react-openapi/src/OpenAPICodeSample.test.ts b/packages/react-openapi/src/OpenAPICodeSample.test.ts new file mode 100644 index 0000000000..4dbe4bc3d1 --- /dev/null +++ b/packages/react-openapi/src/OpenAPICodeSample.test.ts @@ -0,0 +1,52 @@ +import { describe, expect, it } from 'bun:test'; +import { getSecurityHeaders } from './OpenAPICodeSample'; +import type { OpenAPIOperationData } from './types'; + +describe('getSecurityHeaders', () => { + it('should handle custom HTTP scheme with x-prefix', () => { + const securities: OpenAPIOperationData['securities'] = [ + [ + 'customScheme', + { + type: 'apiKey', + in: 'header', + name: 'Authorization', + 'x-prefix': 'CustomScheme', + }, + ], + ]; + + const result = getSecurityHeaders({ + securityRequirement: [{ customScheme: [] }], + securities, + }); + + expect(result).toEqual({ + Authorization: 'CustomScheme YOUR_API_KEY', + }); + }); + + it('should use x-prefix with x-placeholder together', () => { + const securities: OpenAPIOperationData['securities'] = [ + [ + 'customAuth', + { + type: 'apiKey', + in: 'header', + name: 'Authorization', + 'x-prefix': 'Token', + 'x-placeholder': 'MY_CUSTOM_TOKEN', + }, + ], + ]; + + const result = getSecurityHeaders({ + securityRequirement: [{ customAuth: [] }], + securities, + }); + + expect(result).toEqual({ + Authorization: 'Token MY_CUSTOM_TOKEN', + }); + }); +}); diff --git a/packages/react-openapi/src/OpenAPICodeSample.tsx b/packages/react-openapi/src/OpenAPICodeSample.tsx index 5287d4fe84..c85953fa64 100644 --- a/packages/react-openapi/src/OpenAPICodeSample.tsx +++ b/packages/react-openapi/src/OpenAPICodeSample.tsx @@ -290,7 +290,7 @@ function getCustomCodeSamples(props: { return customCodeSamples; } -function getSecurityHeaders(args: { +export function getSecurityHeaders(args: { securityRequirement: OpenAPIV3.OperationObject['security']; securities: OpenAPIOperationData['securities']; }): { @@ -314,7 +314,7 @@ function getSecurityHeaders(args: { for (const security of selectedSecurity.schemes) { switch (security.type) { case 'http': { - let scheme = security.scheme; + const scheme = security.scheme; const defaultPlaceholderValue = scheme?.toLowerCase()?.includes('basic') ? 'username:password' : 'YOUR_SECRET_TOKEN'; @@ -323,15 +323,21 @@ function getSecurityHeaders(args: { defaultPlaceholderValue, }); - if (scheme?.includes('bearer')) { - scheme = 'Bearer'; - } else if (scheme?.includes('basic')) { - scheme = 'Basic'; - } else if (scheme?.includes('token')) { - scheme = 'Token'; + // Use x-prefix if provided, otherwise fall back to default logic + let prefix = security['x-prefix']; + if (!prefix) { + if (scheme?.includes('bearer')) { + prefix = 'Bearer'; + } else if (scheme?.includes('basic')) { + prefix = 'Basic'; + } else if (scheme?.includes('token')) { + prefix = 'Token'; + } else { + prefix = scheme ?? ''; + } } - headers.Authorization = `${scheme} ${format}`; + headers.Authorization = `${prefix} ${format}`; break; } case 'apiKey': { @@ -339,17 +345,23 @@ function getSecurityHeaders(args: { break; } const name = security.name ?? 'Authorization'; - headers[name] = resolvePrefillCodePlaceholderFromSecurityScheme({ + const placeholder = resolvePrefillCodePlaceholderFromSecurityScheme({ security: security, defaultPlaceholderValue: 'YOUR_API_KEY', }); + // Use x-prefix if provided for apiKey schemes + const prefix = security['x-prefix']; + headers[name] = prefix ? `${prefix} ${placeholder}` : placeholder; break; } case 'oauth2': { - headers.Authorization = `Bearer ${resolvePrefillCodePlaceholderFromSecurityScheme({ - security: security, - defaultPlaceholderValue: 'YOUR_OAUTH2_TOKEN', - })}`; + const prefix = security['x-prefix'] ?? 'Bearer'; + headers.Authorization = `${prefix} ${resolvePrefillCodePlaceholderFromSecurityScheme( + { + security: security, + defaultPlaceholderValue: 'YOUR_OAUTH2_TOKEN', + } + )}`; break; } default: { diff --git a/packages/react-openapi/src/util/tryit-prefill.test.ts b/packages/react-openapi/src/util/tryit-prefill.test.ts index 0341269eac..e43debc1b4 100644 --- a/packages/react-openapi/src/util/tryit-prefill.test.ts +++ b/packages/react-openapi/src/util/tryit-prefill.test.ts @@ -415,6 +415,32 @@ describe('resolvePrefillCodePlaceholderFromSecurityScheme (integration style)', expect(result).toBe(''); }); + + it('should prioritize x-gitbook-prefill over x-placeholder when both are present', () => { + const result = resolvePrefillCodePlaceholderFromSecurityScheme({ + security: { + type: 'http', + scheme: 'bearer', + 'x-gitbook-prefill': '{{ visitor.claims.apiToken }}', + 'x-placeholder': 'API_TOKEN_KEY', + }, + }); + + expect(result).toBe('$$__X-GITBOOK-PREFILL[(visitor.claims.apiToken)]__$$'); + }); + + it('should return x-placeholder for apiKey scheme', () => { + const result = resolvePrefillCodePlaceholderFromSecurityScheme({ + security: { + type: 'apiKey', + in: 'header', + name: 'X-API-KEY', + 'x-placeholder': 'YOUR_API_KEY_HERE', + }, + }); + + expect(result).toBe('YOUR_API_KEY_HERE'); + }); }); describe('resolveURLWithPrefillCodePlaceholdersFromServer', () => { diff --git a/packages/react-openapi/src/util/tryit-prefill.ts b/packages/react-openapi/src/util/tryit-prefill.ts index b583b64ba4..7477c6d17f 100644 --- a/packages/react-openapi/src/util/tryit-prefill.ts +++ b/packages/react-openapi/src/util/tryit-prefill.ts @@ -177,6 +177,10 @@ export function resolvePrefillCodePlaceholderFromSecurityScheme(args: { const prefillExprParts = extractPrefillExpressionPartsFromSecurityScheme(security); if (prefillExprParts.length === 0) { + // If no x-gitbook-prefill, check for x-placeholder + if (security['x-placeholder']) { + return security['x-placeholder']; + } return defaultPlaceholderValue ?? ''; } const prefillExpr = templatePartsToExpression(prefillExprParts); From ee0d4484e4f2e3bf1eaafbdd5f326617171b8a15 Mon Sep 17 00:00:00 2001 From: Nolann Biron Date: Wed, 19 Nov 2025 14:47:54 +0100 Subject: [PATCH 2/4] changeset --- .changeset/five-worlds-kneel.md | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 .changeset/five-worlds-kneel.md diff --git a/.changeset/five-worlds-kneel.md b/.changeset/five-worlds-kneel.md new file mode 100644 index 0000000000..86739ff8f3 --- /dev/null +++ b/.changeset/five-worlds-kneel.md @@ -0,0 +1,6 @@ +--- +'@gitbook/openapi-parser': patch +'@gitbook/react-openapi': patch +--- + +Add x-prefix and x-placeholder for OpenAPI security scheme From 8747382742d5305d4040cf62477523648310c5d0 Mon Sep 17 00:00:00 2001 From: Nolann Biron Date: Fri, 21 Nov 2025 14:58:30 +0100 Subject: [PATCH 3/4] Use x-gitbook- --- .changeset/five-worlds-kneel.md | 2 +- packages/openapi-parser/src/types.ts | 6 +++--- packages/react-openapi/src/OpenAPICodeSample.test.ts | 10 +++++----- packages/react-openapi/src/OpenAPICodeSample.tsx | 10 +++++----- packages/react-openapi/src/util/tryit-prefill.test.ts | 8 ++++---- packages/react-openapi/src/util/tryit-prefill.ts | 6 +++--- 6 files changed, 21 insertions(+), 21 deletions(-) diff --git a/.changeset/five-worlds-kneel.md b/.changeset/five-worlds-kneel.md index 86739ff8f3..049f641e64 100644 --- a/.changeset/five-worlds-kneel.md +++ b/.changeset/five-worlds-kneel.md @@ -3,4 +3,4 @@ '@gitbook/react-openapi': patch --- -Add x-prefix and x-placeholder for OpenAPI security scheme +Add x-gitbook-prefix and x-gitbook-token-placeholder for OpenAPI security scheme diff --git a/packages/openapi-parser/src/types.ts b/packages/openapi-parser/src/types.ts index 4c00ce1893..1e6d42aa98 100644 --- a/packages/openapi-parser/src/types.ts +++ b/packages/openapi-parser/src/types.ts @@ -76,13 +76,13 @@ export interface OpenAPICustomOperationProperties { export interface OpenAPICustomPrefillProperties { 'x-gitbook-prefill'?: string; /** - * Placeholder to override the default one for the security scheme. + * Token placeholder used inside sample credentials (e.g., "Bearer ${token}"). */ - 'x-placeholder'?: string; + 'x-gitbook-token-placeholder'?: string; /** * Prefix to override the default one for the security scheme (e.g., "Bearer", "Basic", "Token"). */ - 'x-prefix'?: string; + 'x-gitbook-prefix'?: string; } export type OpenAPIStability = 'experimental' | 'alpha' | 'beta'; diff --git a/packages/react-openapi/src/OpenAPICodeSample.test.ts b/packages/react-openapi/src/OpenAPICodeSample.test.ts index 4dbe4bc3d1..5f8531a713 100644 --- a/packages/react-openapi/src/OpenAPICodeSample.test.ts +++ b/packages/react-openapi/src/OpenAPICodeSample.test.ts @@ -3,7 +3,7 @@ import { getSecurityHeaders } from './OpenAPICodeSample'; import type { OpenAPIOperationData } from './types'; describe('getSecurityHeaders', () => { - it('should handle custom HTTP scheme with x-prefix', () => { + it('should handle custom HTTP scheme with x-gitbook-prefix', () => { const securities: OpenAPIOperationData['securities'] = [ [ 'customScheme', @@ -11,7 +11,7 @@ describe('getSecurityHeaders', () => { type: 'apiKey', in: 'header', name: 'Authorization', - 'x-prefix': 'CustomScheme', + 'x-gitbook-prefix': 'CustomScheme', }, ], ]; @@ -26,7 +26,7 @@ describe('getSecurityHeaders', () => { }); }); - it('should use x-prefix with x-placeholder together', () => { + it('should use x-gitbook-prefix with x-gitbook-token-placeholder together', () => { const securities: OpenAPIOperationData['securities'] = [ [ 'customAuth', @@ -34,8 +34,8 @@ describe('getSecurityHeaders', () => { type: 'apiKey', in: 'header', name: 'Authorization', - 'x-prefix': 'Token', - 'x-placeholder': 'MY_CUSTOM_TOKEN', + 'x-gitbook-prefix': 'Token', + 'x-gitbook-token-placeholder': 'MY_CUSTOM_TOKEN', }, ], ]; diff --git a/packages/react-openapi/src/OpenAPICodeSample.tsx b/packages/react-openapi/src/OpenAPICodeSample.tsx index c85953fa64..cd8276ff2d 100644 --- a/packages/react-openapi/src/OpenAPICodeSample.tsx +++ b/packages/react-openapi/src/OpenAPICodeSample.tsx @@ -323,8 +323,8 @@ export function getSecurityHeaders(args: { defaultPlaceholderValue, }); - // Use x-prefix if provided, otherwise fall back to default logic - let prefix = security['x-prefix']; + // Use x-gitbook-prefix if provided, otherwise fall back to default logic + let prefix = security['x-gitbook-prefix']; if (!prefix) { if (scheme?.includes('bearer')) { prefix = 'Bearer'; @@ -349,13 +349,13 @@ export function getSecurityHeaders(args: { security: security, defaultPlaceholderValue: 'YOUR_API_KEY', }); - // Use x-prefix if provided for apiKey schemes - const prefix = security['x-prefix']; + // 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-prefix'] ?? 'Bearer'; + const prefix = security['x-gitbook-prefix'] ?? 'Bearer'; headers.Authorization = `${prefix} ${resolvePrefillCodePlaceholderFromSecurityScheme( { security: security, diff --git a/packages/react-openapi/src/util/tryit-prefill.test.ts b/packages/react-openapi/src/util/tryit-prefill.test.ts index e43debc1b4..158b7d2b18 100644 --- a/packages/react-openapi/src/util/tryit-prefill.test.ts +++ b/packages/react-openapi/src/util/tryit-prefill.test.ts @@ -416,26 +416,26 @@ describe('resolvePrefillCodePlaceholderFromSecurityScheme (integration style)', expect(result).toBe(''); }); - it('should prioritize x-gitbook-prefill over x-placeholder when both are present', () => { + it('should prioritize x-gitbook-prefill over x-gitbook-token-placeholder when both are present', () => { const result = resolvePrefillCodePlaceholderFromSecurityScheme({ security: { type: 'http', scheme: 'bearer', 'x-gitbook-prefill': '{{ visitor.claims.apiToken }}', - 'x-placeholder': 'API_TOKEN_KEY', + 'x-gitbook-token-placeholder': 'API_TOKEN_KEY', }, }); expect(result).toBe('$$__X-GITBOOK-PREFILL[(visitor.claims.apiToken)]__$$'); }); - it('should return x-placeholder for apiKey scheme', () => { + it('should return x-gitbook-token-placeholder for apiKey scheme', () => { const result = resolvePrefillCodePlaceholderFromSecurityScheme({ security: { type: 'apiKey', in: 'header', name: 'X-API-KEY', - 'x-placeholder': 'YOUR_API_KEY_HERE', + 'x-gitbook-token-placeholder': 'YOUR_API_KEY_HERE', }, }); diff --git a/packages/react-openapi/src/util/tryit-prefill.ts b/packages/react-openapi/src/util/tryit-prefill.ts index 7477c6d17f..99755ea9ba 100644 --- a/packages/react-openapi/src/util/tryit-prefill.ts +++ b/packages/react-openapi/src/util/tryit-prefill.ts @@ -177,9 +177,9 @@ export function resolvePrefillCodePlaceholderFromSecurityScheme(args: { const prefillExprParts = extractPrefillExpressionPartsFromSecurityScheme(security); if (prefillExprParts.length === 0) { - // If no x-gitbook-prefill, check for x-placeholder - if (security['x-placeholder']) { - return security['x-placeholder']; + // If no x-gitbook-prefill, check for x-gitbook-token-placeholder + if (security['x-gitbook-token-placeholder']) { + return security['x-gitbook-token-placeholder']; } return defaultPlaceholderValue ?? ''; } From 675d8dae1471a30e29657d26134b923d79148705 Mon Sep 17 00:00:00 2001 From: Nolann Biron Date: Fri, 21 Nov 2025 17:12:06 +0100 Subject: [PATCH 4/4] remove x-gitbook-token-placeholder for http --- .../src/OpenAPICodeSample.test.ts | 24 ++++++++++++++++++ .../react-openapi/src/OpenAPICodeSample.tsx | 25 ++++++++----------- .../src/util/tryit-prefill.test.ts | 4 +-- 3 files changed, 37 insertions(+), 16 deletions(-) diff --git a/packages/react-openapi/src/OpenAPICodeSample.test.ts b/packages/react-openapi/src/OpenAPICodeSample.test.ts index 5f8531a713..dbae8e87cf 100644 --- a/packages/react-openapi/src/OpenAPICodeSample.test.ts +++ b/packages/react-openapi/src/OpenAPICodeSample.test.ts @@ -49,4 +49,28 @@ describe('getSecurityHeaders', () => { Authorization: 'Token MY_CUSTOM_TOKEN', }); }); + + it('should not use x-gitbook-prefix for http scheme', () => { + const securities: OpenAPIOperationData['securities'] = [ + [ + 'customAuth', + { + type: 'http', + in: 'header', + name: 'Authorization', + scheme: 'bearer', + 'x-gitbook-prefix': 'Token', + }, + ], + ]; + + const result = getSecurityHeaders({ + securityRequirement: [{ customAuth: [] }], + securities, + }); + + expect(result).toEqual({ + Authorization: 'Bearer YOUR_SECRET_TOKEN', + }); + }); }); diff --git a/packages/react-openapi/src/OpenAPICodeSample.tsx b/packages/react-openapi/src/OpenAPICodeSample.tsx index cd8276ff2d..8805b6fbbf 100644 --- a/packages/react-openapi/src/OpenAPICodeSample.tsx +++ b/packages/react-openapi/src/OpenAPICodeSample.tsx @@ -314,7 +314,8 @@ export function getSecurityHeaders(args: { for (const security of selectedSecurity.schemes) { switch (security.type) { case 'http': { - const scheme = security.scheme; + // 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'; @@ -323,21 +324,17 @@ export function getSecurityHeaders(args: { defaultPlaceholderValue, }); - // Use x-gitbook-prefix if provided, otherwise fall back to default logic - let prefix = security['x-gitbook-prefix']; - if (!prefix) { - if (scheme?.includes('bearer')) { - prefix = 'Bearer'; - } else if (scheme?.includes('basic')) { - prefix = 'Basic'; - } else if (scheme?.includes('token')) { - prefix = 'Token'; - } else { - prefix = scheme ?? ''; - } + 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 = `${prefix} ${format}`; + headers.Authorization = `${scheme} ${format}`; break; } case 'apiKey': { diff --git a/packages/react-openapi/src/util/tryit-prefill.test.ts b/packages/react-openapi/src/util/tryit-prefill.test.ts index 158b7d2b18..43983f5858 100644 --- a/packages/react-openapi/src/util/tryit-prefill.test.ts +++ b/packages/react-openapi/src/util/tryit-prefill.test.ts @@ -419,8 +419,8 @@ describe('resolvePrefillCodePlaceholderFromSecurityScheme (integration style)', it('should prioritize x-gitbook-prefill over x-gitbook-token-placeholder when both are present', () => { const result = resolvePrefillCodePlaceholderFromSecurityScheme({ security: { - type: 'http', - scheme: 'bearer', + type: 'apiKey', + in: 'header', 'x-gitbook-prefill': '{{ visitor.claims.apiToken }}', 'x-gitbook-token-placeholder': 'API_TOKEN_KEY', },