diff --git a/.changeset/tasty-masks-sneeze.md b/.changeset/tasty-masks-sneeze.md new file mode 100644 index 000000000..6a74c81b5 --- /dev/null +++ b/.changeset/tasty-masks-sneeze.md @@ -0,0 +1,5 @@ +--- +"@gitbook/react-openapi": patch +--- + +Support x-gitbook-token-placeholder as TryIt prefill fallback for HTTP auth (basic and bearer) diff --git a/packages/react-openapi/src/OpenAPICodeSample.test.ts b/packages/react-openapi/src/OpenAPICodeSample.test.ts index dbae8e87c..6cd09ebff 100644 --- a/packages/react-openapi/src/OpenAPICodeSample.test.ts +++ b/packages/react-openapi/src/OpenAPICodeSample.test.ts @@ -50,6 +50,71 @@ describe('getSecurityHeaders', () => { }); }); + it('should use default placeholder for basic auth', () => { + const securities: OpenAPIOperationData['securities'] = [ + [ + 'basicAuth', + { + type: 'http', + scheme: 'basic', + }, + ], + ]; + + const result = getSecurityHeaders({ + securityRequirement: [{ basicAuth: [] }], + securities, + }); + + expect(result).toEqual({ + Authorization: 'Basic username:password', + }); + }); + + it('should use x-gitbook-token-placeholder for basic auth', () => { + const securities: OpenAPIOperationData['securities'] = [ + [ + 'basicAuth', + { + type: 'http', + scheme: 'basic', + 'x-gitbook-token-placeholder': 'admin:secret123', + }, + ], + ]; + + const result = getSecurityHeaders({ + securityRequirement: [{ basicAuth: [] }], + securities, + }); + + expect(result).toEqual({ + Authorization: 'Basic admin:secret123', + }); + }); + + it('should use x-gitbook-token-placeholder for bearer auth', () => { + const securities: OpenAPIOperationData['securities'] = [ + [ + 'bearerAuth', + { + type: 'http', + scheme: 'bearer', + 'x-gitbook-token-placeholder': 'MY_CUSTOM_TOKEN', + }, + ], + ]; + + const result = getSecurityHeaders({ + securityRequirement: [{ bearerAuth: [] }], + securities, + }); + + expect(result).toEqual({ + Authorization: 'Bearer MY_CUSTOM_TOKEN', + }); + }); + it('should not use x-gitbook-prefix for http scheme', () => { const securities: OpenAPIOperationData['securities'] = [ [ diff --git a/packages/react-openapi/src/util/tryit-prefill.test.ts b/packages/react-openapi/src/util/tryit-prefill.test.ts index 43983f585..349b83d94 100644 --- a/packages/react-openapi/src/util/tryit-prefill.test.ts +++ b/packages/react-openapi/src/util/tryit-prefill.test.ts @@ -118,6 +118,144 @@ describe('resolveTryItPrefillForOperation', () => { }); }); + it('should use x-gitbook-token-placeholder as fallback for basic auth in try-it', () => { + const operation: OpenAPIOperationData = { + path: '/users', + method: 'GET', + operation: { summary: 'List users' }, + servers: [{ url: 'https://api.example.com' }], + securities: [ + [ + 'basicAuth', + { + type: 'http', + scheme: 'basic', + 'x-gitbook-token-placeholder': 'admin:secret123', + }, + ], + ], + }; + + const prefillInputContext: PrefillInputContextData = { + visitor: { claims: {} }, + }; + + const result = resolveTryItPrefillForOperation({ + operation, + prefillInputContext, + }); + + expect(result).toEqual({ + authentication: { + securitySchemes: { + basicAuth: { username: 'admin', password: 'secret123' }, + }, + }, + }); + }); + + it('should use x-gitbook-token-placeholder as fallback for bearer auth in try-it', () => { + const operation: OpenAPIOperationData = { + path: '/users', + method: 'GET', + operation: { summary: 'List users' }, + servers: [{ url: 'https://api.example.com' }], + securities: [ + [ + 'bearerAuth', + { + type: 'http', + scheme: 'bearer', + 'x-gitbook-token-placeholder': 'my-default-token', + }, + ], + ], + }; + + const prefillInputContext: PrefillInputContextData = { + visitor: { claims: {} }, + }; + + const result = resolveTryItPrefillForOperation({ + operation, + prefillInputContext, + }); + + expect(result).toEqual({ + authentication: { + securitySchemes: { + bearerAuth: { token: 'my-default-token' }, + }, + }, + }); + }); + + it('should fall back to x-gitbook-token-placeholder when x-gitbook-prefill exists but prefillInputContext is null', () => { + const operation: OpenAPIOperationData = { + path: '/users', + method: 'GET', + operation: { summary: 'List users' }, + servers: [{ url: 'https://api.example.com' }], + securities: [ + [ + 'basicAuth', + { + type: 'http', + scheme: 'basic', + 'x-gitbook-prefill': '{{ visitor.claims.basicAuth }}', + 'x-gitbook-token-placeholder': 'admin:secret123', + }, + ], + ], + }; + + const result = resolveTryItPrefillForOperation({ + operation, + prefillInputContext: null, + }); + + expect(result).toEqual({ + authentication: { + securitySchemes: { + basicAuth: { username: 'admin', password: 'secret123' }, + }, + }, + }); + }); + + it('should fall back to x-gitbook-token-placeholder when x-gitbook-prefill expression resolves to undefined', () => { + const operation: OpenAPIOperationData = { + path: '/users', + method: 'GET', + operation: { summary: 'List users' }, + servers: [{ url: 'https://api.example.com' }], + securities: [ + [ + 'bearerAuth', + { + type: 'http', + scheme: 'bearer', + 'x-gitbook-prefill': '{{ visitor.claims.missing }}', + 'x-gitbook-token-placeholder': 'fallback-token', + }, + ], + ], + }; + + const result = resolveTryItPrefillForOperation({ + operation, + prefillInputContext: { visitor: { claims: {} } }, + }); + + expect(result).toEqual({ + authentication: { + securitySchemes: { + bearerAuth: { token: 'fallback-token' }, + }, + }, + }); + }); + it('should return empty object if no visitor data matches prefill expression', () => { const operation: OpenAPIOperationData = { path: '/orgs//spaces', @@ -167,7 +305,9 @@ describe('resolveTryItPrefillForOperation', () => { const prefillInputContext: PrefillInputContextData = { visitor: { - claims: { api: { endpointUrl: 'https://api.gitbook-staging.com/v1/' } }, + claims: { + api: { endpointUrl: 'https://api.gitbook-staging.com/v1/' }, + }, }, }; @@ -214,7 +354,9 @@ describe('resolveTryItPrefillForOperation', () => { // Override env const overrideEnvResult = resolveTryItPrefillForOperation({ operation, - prefillInputContext: { visitor: { claims: { api: { env: 'staging' } } } }, + prefillInputContext: { + visitor: { claims: { api: { env: 'staging' } } }, + }, }); expect(overrideEnvResult).toEqual({ servers: [ @@ -232,7 +374,9 @@ describe('resolveTryItPrefillForOperation', () => { // Override version const overrideVersionResult = resolveTryItPrefillForOperation({ operation, - prefillInputContext: { visitor: { claims: { api: { version: 'v2' } } } }, + prefillInputContext: { + visitor: { claims: { api: { version: 'v2' } } }, + }, }); expect(overrideVersionResult).toEqual({ servers: [ @@ -467,7 +611,10 @@ describe('resolveURLWithPrefillCodePlaceholdersFromServer', () => { const result = resolveURLWithPrefillCodePlaceholdersFromServer({ url: 'https://{region}.example.com', variables: { - region: { default: 'us-east-1', 'x-gitbook-prefill': '{{ user.region }}' }, + region: { + default: 'us-east-1', + 'x-gitbook-prefill': '{{ user.region }}', + }, }, }); @@ -492,7 +639,10 @@ describe('resolveURLWithPrefillCodePlaceholdersFromServer', () => { url: 'https://{region}.example.com/{version}', 'x-gitbook-prefill': '{{ user.baseUrl }}', variables: { - region: { default: 'us-east-1', 'x-gitbook-prefill': '{{ user.region }}' }, + region: { + default: 'us-east-1', + 'x-gitbook-prefill': '{{ user.region }}', + }, version: { default: 'v1' }, }, }); diff --git a/packages/react-openapi/src/util/tryit-prefill.ts b/packages/react-openapi/src/util/tryit-prefill.ts index 99755ea9b..4d17d0cd1 100644 --- a/packages/react-openapi/src/util/tryit-prefill.ts +++ b/packages/react-openapi/src/util/tryit-prefill.ts @@ -33,18 +33,18 @@ export function resolveTryItPrefillForOperation(args: { prefillInputContext, } = args; - if (!prefillInputContext) { - return {}; - } - - const runtime = new ExpressionRuntime(); - const resolveTryItPrefillExpression = (expr: string) => { - const parts = parseTemplate(expr); - if (!parts.length) { - return undefined; - } - return runtime.evaluateTemplate(expr, prefillInputContext); - }; + const resolveTryItPrefillExpression = prefillInputContext + ? (() => { + const runtime = new ExpressionRuntime(); + return (expr: string) => { + const parts = parseTemplate(expr); + if (!parts.length) { + return undefined; + } + return runtime.evaluateTemplate(expr, prefillInputContext); + }; + })() + : undefined; const prefillAuth = securities ? resolveTryItPrefillAuthForOperationSecurities({ @@ -54,7 +54,10 @@ export function resolveTryItPrefillForOperation(args: { : undefined; const prefillServers = servers - ? resolveTryItPrefillServersForOperationServers({ servers, resolveTryItPrefillExpression }) + ? resolveTryItPrefillServersForOperationServers({ + servers, + resolveTryItPrefillExpression, + }) : []; return { @@ -68,15 +71,17 @@ export function resolveTryItPrefillForOperation(args: { */ function resolveTryItPrefillAuthForOperationSecurities(args: { securities: OpenAPIOperationData['securities']; - resolveTryItPrefillExpression: (expr: string) => string | undefined; + resolveTryItPrefillExpression?: (expr: string) => string | undefined; }): ApiClientConfiguration['authentication'] | undefined { const { securities, resolveTryItPrefillExpression } = args; const prefillAuthConfig: ApiClientConfiguration['authentication']['securitySchemes'] = {}; for (const [schemeName, security] of Object.values(securities)) { - const tryitPrefillAuthValue = security[PREFILL_CUSTOM_PROPERTY] - ? resolveTryItPrefillExpression(security[PREFILL_CUSTOM_PROPERTY]) + const resolvedPrefill = security[PREFILL_CUSTOM_PROPERTY] + ? resolveTryItPrefillExpression?.(security[PREFILL_CUSTOM_PROPERTY]) : undefined; + const tryitPrefillAuthValue = + resolvedPrefill || security['x-gitbook-token-placeholder'] || undefined; if (!tryitPrefillAuthValue) { continue; @@ -120,7 +125,7 @@ function resolveTryItPrefillAuthForOperationSecurities(args: { */ function resolveTryItPrefillServersForOperationServers(args: { servers: OpenAPIOperationData['servers']; - resolveTryItPrefillExpression: (expr: string) => string | undefined; + resolveTryItPrefillExpression?: (expr: string) => string | undefined; }): ApiClientConfiguration['servers'] | undefined { const { servers, resolveTryItPrefillExpression } = args; const resolvedServers: ApiClientConfiguration['servers'] = []; @@ -128,9 +133,10 @@ function resolveTryItPrefillServersForOperationServers(args: { for (const server of servers) { // Url-level prefill const tryItPrefillServerUrlExpr = server[PREFILL_CUSTOM_PROPERTY]; - const tryItPrefillServerUrlValue = tryItPrefillServerUrlExpr - ? resolveTryItPrefillExpression(tryItPrefillServerUrlExpr) - : undefined; + const tryItPrefillServerUrlValue = + tryItPrefillServerUrlExpr && resolveTryItPrefillExpression + ? resolveTryItPrefillExpression(tryItPrefillServerUrlExpr) + : undefined; const variables: { [variable: string]: OpenAPIV3.ServerVariableObject } = server.variables ? { ...server.variables } @@ -143,7 +149,7 @@ function resolveTryItPrefillServersForOperationServers(args: { variable; const tryItPrefillVarValue = tryItPrefillVarExpr - ? resolveTryItPrefillExpression(tryItPrefillVarExpr) + ? resolveTryItPrefillExpression?.(tryItPrefillVarExpr) : undefined; variables[varName] = { ...variableProps, @@ -266,5 +272,7 @@ function templatePartsToExpression(parts: ReturnType) { } function toPrefillCodePlaceholder(expression: string, defaultValue?: string) { - return `$$__X-GITBOOK-PREFILL[(${expression})${defaultValue ? ` ?? '${defaultValue}'` : ''}]__$$`; + return `$$__X-GITBOOK-PREFILL[(${expression})${ + defaultValue ? ` ?? '${defaultValue}'` : '' + }]__$$`; }