diff --git a/.changeset/five-worlds-kneel.md b/.changeset/five-worlds-kneel.md new file mode 100644 index 0000000000..049f641e64 --- /dev/null +++ b/.changeset/five-worlds-kneel.md @@ -0,0 +1,6 @@ +--- +'@gitbook/openapi-parser': patch +'@gitbook/react-openapi': patch +--- + +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 a1af691ec3..1e6d42aa98 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; + /** + * Token placeholder used inside sample credentials (e.g., "Bearer ${token}"). + */ + 'x-gitbook-token-placeholder'?: string; + /** + * Prefix to override the default one for the security scheme (e.g., "Bearer", "Basic", "Token"). + */ + '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 new file mode 100644 index 0000000000..dbae8e87cf --- /dev/null +++ b/packages/react-openapi/src/OpenAPICodeSample.test.ts @@ -0,0 +1,76 @@ +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-gitbook-prefix', () => { + const securities: OpenAPIOperationData['securities'] = [ + [ + 'customScheme', + { + type: 'apiKey', + in: 'header', + name: 'Authorization', + 'x-gitbook-prefix': 'CustomScheme', + }, + ], + ]; + + const result = getSecurityHeaders({ + securityRequirement: [{ customScheme: [] }], + securities, + }); + + expect(result).toEqual({ + Authorization: 'CustomScheme YOUR_API_KEY', + }); + }); + + it('should use x-gitbook-prefix with x-gitbook-token-placeholder together', () => { + const securities: OpenAPIOperationData['securities'] = [ + [ + 'customAuth', + { + type: 'apiKey', + in: 'header', + name: 'Authorization', + 'x-gitbook-prefix': 'Token', + 'x-gitbook-token-placeholder': 'MY_CUSTOM_TOKEN', + }, + ], + ]; + + const result = getSecurityHeaders({ + securityRequirement: [{ customAuth: [] }], + securities, + }); + + expect(result).toEqual({ + 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 5287d4fe84..8805b6fbbf 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,6 +314,7 @@ function getSecurityHeaders(args: { for (const security of selectedSecurity.schemes) { switch (security.type) { case 'http': { + // 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' @@ -329,6 +330,8 @@ function getSecurityHeaders(args: { scheme = 'Basic'; } else if (scheme?.includes('token')) { scheme = 'Token'; + } else { + scheme = scheme ?? ''; } headers.Authorization = `${scheme} ${format}`; @@ -339,17 +342,23 @@ function getSecurityHeaders(args: { break; } const name = security.name ?? 'Authorization'; - headers[name] = resolvePrefillCodePlaceholderFromSecurityScheme({ + const placeholder = resolvePrefillCodePlaceholderFromSecurityScheme({ security: security, defaultPlaceholderValue: 'YOUR_API_KEY', }); + // Use x-gitbook-prefix if provided for apiKey schemes + const prefix = security['x-gitbook-prefix']; + headers[name] = prefix ? `${prefix} ${placeholder}` : placeholder; break; } case 'oauth2': { - headers.Authorization = `Bearer ${resolvePrefillCodePlaceholderFromSecurityScheme({ - security: security, - defaultPlaceholderValue: 'YOUR_OAUTH2_TOKEN', - })}`; + const prefix = security['x-gitbook-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..43983f5858 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-gitbook-token-placeholder when both are present', () => { + const result = resolvePrefillCodePlaceholderFromSecurityScheme({ + security: { + type: 'apiKey', + in: 'header', + 'x-gitbook-prefill': '{{ visitor.claims.apiToken }}', + 'x-gitbook-token-placeholder': 'API_TOKEN_KEY', + }, + }); + + expect(result).toBe('$$__X-GITBOOK-PREFILL[(visitor.claims.apiToken)]__$$'); + }); + + it('should return x-gitbook-token-placeholder for apiKey scheme', () => { + const result = resolvePrefillCodePlaceholderFromSecurityScheme({ + security: { + type: 'apiKey', + in: 'header', + name: 'X-API-KEY', + 'x-gitbook-token-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..99755ea9ba 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-gitbook-token-placeholder + if (security['x-gitbook-token-placeholder']) { + return security['x-gitbook-token-placeholder']; + } return defaultPlaceholderValue ?? ''; } const prefillExpr = templatePartsToExpression(prefillExprParts);