diff --git a/src/codegen/generators/typescript/channels/openapi.ts b/src/codegen/generators/typescript/channels/openapi.ts index 1fed6596..a42c2506 100644 --- a/src/codegen/generators/typescript/channels/openapi.ts +++ b/src/codegen/generators/typescript/channels/openapi.ts @@ -16,11 +16,12 @@ import {collectProtocolDependencies} from './utils'; import { renderHttpFetchClient, renderHttpCommonTypes -} from './protocols/http/fetch'; +} from './protocols/http'; import {getMessageTypeAndModule} from './utils'; import {pascalCase} from '../utils'; import {createMissingInputDocumentError} from '../../../errors'; import {resolveImportExtension} from '../../../utils'; +import {extractSecuritySchemes} from '../../../inputs/openapi/security'; type OpenAPIDocument = | OpenAPIV3.Document @@ -75,6 +76,9 @@ export async function generateTypeScriptChannelsForOpenAPI( const {openapiDocument} = validateOpenAPIContext(context); + // Extract security schemes from the OpenAPI document + const securitySchemes = extractSecuritySchemes(openapiDocument); + // Collect dependencies const deps = protocolDependencies['http_client']; const importExtension = resolveImportExtension( @@ -98,8 +102,9 @@ export async function generateTypeScriptChannelsForOpenAPI( ); // Generate common types once (stateless check) + // Pass security schemes to generate only relevant auth types if (protocolCodeFunctions['http_client'].length === 0 && renders.length > 0) { - const commonTypesCode = renderHttpCommonTypes(); + const commonTypesCode = renderHttpCommonTypes(securitySchemes); protocolCodeFunctions['http_client'].unshift(commonTypesCode); } diff --git a/src/codegen/generators/typescript/channels/protocols/http/client.ts b/src/codegen/generators/typescript/channels/protocols/http/client.ts new file mode 100644 index 00000000..44ac8da6 --- /dev/null +++ b/src/codegen/generators/typescript/channels/protocols/http/client.ts @@ -0,0 +1,285 @@ +/** + * Generates HTTP client functions for individual API operations. + * Each operation gets a typed function with request/response handling. + */ +import {HttpRenderType} from '../../../../../types'; +import {pascalCase} from '../../../utils'; +import {ChannelFunctionTypes, RenderHttpParameters} from '../../types'; + +/** + * Renders an HTTP fetch client function for a specific API operation. + */ +export function renderHttpFetchClient({ + requestTopic, + requestMessageType, + requestMessageModule, + replyMessageType, + replyMessageModule, + channelParameters, + method, + servers = [], + subName = pascalCase(requestTopic), + functionName = `${method.toLowerCase()}${subName}`, + includesStatusCodes = false +}: RenderHttpParameters): HttpRenderType { + const messageType = requestMessageModule + ? `${requestMessageModule}.${requestMessageType}` + : requestMessageType; + const replyType = replyMessageModule + ? `${replyMessageModule}.${replyMessageType}` + : replyMessageType; + + // Generate context interface name + const contextInterfaceName = `${pascalCase(functionName)}Context`; + + // Determine if operation has path parameters + const hasParameters = channelParameters !== undefined; + + // Generate the context interface (extends HttpClientContext) + const contextInterface = generateContextInterface( + contextInterfaceName, + messageType, + hasParameters, + method + ); + + // Generate the function implementation + const functionCode = generateFunctionImplementation({ + functionName, + contextInterfaceName, + replyType, + replyMessageModule, + replyMessageType, + messageType, + requestTopic, + hasParameters, + method, + servers, + includesStatusCodes + }); + + const code = `${contextInterface} + +${functionCode}`; + + return { + messageType, + replyType, + code, + functionName, + dependencies: [ + `import { URLSearchParams, URL } from 'url';`, + `import * as NodeFetch from 'node-fetch';` + ], + functionType: ChannelFunctionTypes.HTTP_CLIENT + }; +} + +/** + * Generate the context interface for an HTTP operation + */ +function generateContextInterface( + interfaceName: string, + messageType: string | undefined, + hasParameters: boolean, + method: string +): string { + const fields: string[] = []; + + // Add payload field for methods that have a body + if (messageType && ['POST', 'PUT', 'PATCH'].includes(method.toUpperCase())) { + fields.push(` payload: ${messageType};`); + } + + // Add parameters field if operation has path parameters + if (hasParameters) { + fields.push( + ` parameters: { getChannelWithParameters: (path: string) => string };` + ); + } + + // Add requestHeaders field (optional) for operations that support typed headers + // This is always optional since headers can also be passed via additionalHeaders + fields.push(` requestHeaders?: { marshal: () => string };`); + + const fieldsStr = fields.length > 0 ? `\n${fields.join('\n')}\n` : ''; + + return `export interface ${interfaceName} extends HttpClientContext {${fieldsStr}}`; +} + +/** + * Generate the function implementation + */ +function generateFunctionImplementation(params: { + functionName: string; + contextInterfaceName: string; + replyType: string; + replyMessageModule: string | undefined; + replyMessageType: string; + messageType: string | undefined; + requestTopic: string; + hasParameters: boolean; + method: string; + servers: string[]; + includesStatusCodes: boolean; +}): string { + const { + functionName, + contextInterfaceName, + replyType, + replyMessageModule, + replyMessageType, + messageType, + requestTopic, + hasParameters, + method, + servers, + includesStatusCodes + } = params; + + const defaultServer = servers[0] ?? "'localhost:3000'"; + const hasBody = + messageType && ['POST', 'PUT', 'PATCH'].includes(method.toUpperCase()); + + // Generate URL building code + const urlBuildCode = hasParameters + ? `let url = buildUrlWithParameters(config.server, '${requestTopic}', context.parameters);` + : 'let url = `${config.server}${config.path}`;'; + + // Generate headers initialization + const headersInit = `let headers = context.requestHeaders + ? applyTypedHeaders(context.requestHeaders, config.additionalHeaders) + : { 'Content-Type': 'application/json', ...config.additionalHeaders } as Record;`; + + // Generate body preparation + const bodyPrep = hasBody + ? `const body = context.payload?.marshal();` + : `const body = undefined;`; + + // Generate response parsing + // Use unmarshalByStatusCode if the payload is a union type with status code support + let responseParseCode: string; + if (replyMessageModule) { + responseParseCode = includesStatusCodes + ? `const responseData = ${replyMessageModule}.unmarshalByStatusCode(rawData, response.status);` + : `const responseData = ${replyMessageModule}.unmarshal(rawData);`; + } else { + responseParseCode = `const responseData = ${replyMessageType}.unmarshal(rawData);`; + } + + // Generate default context for optional context parameter + const contextDefault = !hasBody && !hasParameters ? ' = {}' : ''; + + return `async function ${functionName}(context: ${contextInterfaceName}${contextDefault}): Promise> { + // Apply defaults + const config = { + path: '${requestTopic}', + server: ${defaultServer}, + ...context, + }; + + // Validate OAuth2 config if present + if (config.auth?.type === 'oauth2' && AUTH_FEATURES.oauth2) { + validateOAuth2Config(config.auth); + } + + // Build headers + ${headersInit} + + // Build URL + ${urlBuildCode} + url = applyQueryParams(config.queryParams, url); + + // Apply pagination (can affect URL and/or headers) + const paginationResult = applyPagination(config.pagination, url, headers); + url = paginationResult.url; + headers = paginationResult.headers; + + // Apply authentication + const authResult = applyAuth(config.auth, headers, url); + headers = authResult.headers; + url = authResult.url; + + // Prepare body + ${bodyPrep} + + // Determine request function + const makeRequest = config.hooks?.makeRequest ?? defaultMakeRequest; + + // Build request params + let requestParams: HttpRequestParams = { + url, + method: '${method}', + headers, + body + }; + + // Apply beforeRequest hook + if (config.hooks?.beforeRequest) { + requestParams = await config.hooks.beforeRequest(requestParams); + } + + try { + // Execute request with retry logic + let response = await executeWithRetry(requestParams, makeRequest, config.retry); + + // Apply afterResponse hook + if (config.hooks?.afterResponse) { + response = await config.hooks.afterResponse(response, requestParams); + } + + // Handle OAuth2 token flows that require getting a token first + if (config.auth?.type === 'oauth2' && !config.auth.accessToken && AUTH_FEATURES.oauth2) { + const tokenFlowResponse = await handleOAuth2TokenFlow(config.auth, requestParams, makeRequest, config.retry); + if (tokenFlowResponse) { + response = tokenFlowResponse; + } + } + + // Handle 401 with token refresh + if (response.status === 401 && config.auth?.type === 'oauth2' && AUTH_FEATURES.oauth2) { + try { + const refreshResponse = await handleTokenRefresh(config.auth, requestParams, makeRequest, config.retry); + if (refreshResponse) { + response = refreshResponse; + } + } catch { + throw new Error('Unauthorized'); + } + } + + // Handle error responses + if (!response.ok) { + handleHttpError(response.status, response.statusText); + } + + // Parse response + const rawData = await response.json(); + ${responseParseCode} + + // Extract response metadata + const responseHeaders = extractHeaders(response); + const paginationInfo = extractPaginationInfo(responseHeaders, config.pagination); + + // Build response wrapper with pagination helpers + const result: HttpClientResponse<${replyType}> = { + data: responseData, + status: response.status, + statusText: response.statusText, + headers: responseHeaders, + rawData, + pagination: paginationInfo, + ...createPaginationHelpers(config, paginationInfo, ${functionName}), + }; + + return result; + + } catch (error) { + // Apply onError hook if present + if (config.hooks?.onError && error instanceof Error) { + throw await config.hooks.onError(error, requestParams); + } + throw error; + } +}`; +} diff --git a/src/codegen/generators/typescript/channels/protocols/http/fetch.ts b/src/codegen/generators/typescript/channels/protocols/http/common-types.ts similarity index 60% rename from src/codegen/generators/typescript/channels/protocols/http/fetch.ts rename to src/codegen/generators/typescript/channels/protocols/http/common-types.ts index 03f1e7ec..500e5636 100644 --- a/src/codegen/generators/typescript/channels/protocols/http/fetch.ts +++ b/src/codegen/generators/typescript/channels/protocols/http/common-types.ts @@ -1,12 +1,31 @@ -import {HttpRenderType} from '../../../../../types'; -import {pascalCase} from '../../../utils'; -import {ChannelFunctionTypes, RenderHttpParameters} from '../../types'; +/** + * Generates common types and helper functions shared across all HTTP client functions. + * This module renders the shared infrastructure code that is included once per generation. + */ +import {SecuritySchemeOptions} from '../../types'; +import { + analyzeSecuritySchemes, + escapeStringForCodeGen, + getApiKeyDefaults, + renderOAuth2Helpers, + renderOAuth2Stubs, + renderSecurityTypes +} from './security'; /** * Generates common types and helper functions shared across all HTTP client functions. * This should be called once per protocol generation to avoid code duplication. + * + * @param securitySchemes - Optional security schemes extracted from OpenAPI. + * When provided, only relevant auth types are generated. + * When undefined/empty, all auth types are generated for backward compatibility. */ -export function renderHttpCommonTypes(): string { +export function renderHttpCommonTypes( + securitySchemes?: SecuritySchemeOptions[] +): string { + const requirements = analyzeSecuritySchemes(securitySchemes); + const securityTypes = renderSecurityTypes(securitySchemes, requirements); + return `// ============================================================================ // Common Types - Shared across all HTTP client functions // ============================================================================ @@ -88,76 +107,24 @@ export interface TokenResponse { expiresIn?: number; } -// ============================================================================ -// Security Configuration Types - Grouped for better autocomplete -// ============================================================================ - -/** - * Bearer token authentication configuration - */ -export interface BearerAuth { - type: 'bearer'; - token: string; -} - -/** - * Basic authentication configuration (username/password) - */ -export interface BasicAuth { - type: 'basic'; - username: string; - password: string; -} - -/** - * API key authentication configuration - */ -export interface ApiKeyAuth { - type: 'apiKey'; - key: string; - name?: string; // Name of the API key parameter (default: 'X-API-Key') - in?: 'header' | 'query'; // Where to place the API key (default: 'header') -} +${securityTypes} /** - * OAuth2 authentication configuration - * - * Supports server-side flows only: - * - client_credentials: Server-to-server authentication - * - password: Resource owner password credentials (legacy, not recommended) - * - Pre-obtained accessToken: For tokens obtained via browser-based flows - * - * For browser-based flows (implicit, authorization_code), obtain the token - * separately and pass it as accessToken. + * Feature flags indicating which auth types are available. + * Used internally to conditionally call auth-specific helpers. */ -export interface OAuth2Auth { - type: 'oauth2'; - /** Pre-obtained access token (required if not using a server-side flow) */ - accessToken?: string; - /** Refresh token for automatic token renewal on 401 */ - refreshToken?: string; - /** Token endpoint URL (required for client_credentials/password flows and token refresh) */ - tokenUrl?: string; - /** Client ID (required for flows and token refresh) */ - clientId?: string; - /** Client secret (optional, depends on OAuth provider) */ - clientSecret?: string; - /** Requested scopes */ - scopes?: string[]; - /** Server-side flow type */ - flow?: 'password' | 'client_credentials'; - /** Username for password flow */ - username?: string; - /** Password for password flow */ - password?: string; - /** Callback when tokens are refreshed (for caching/persistence) */ - onTokenRefresh?: (newTokens: TokenResponse) => void; -} +const AUTH_FEATURES = { + oauth2: ${requirements.oauth2} +} as const; /** - * Union type for all authentication methods - provides autocomplete support + * Default values for API key authentication derived from the spec. + * These match the defaults documented in the ApiKeyAuth interface. */ -export type AuthConfig = BearerAuth | BasicAuth | ApiKeyAuth | OAuth2Auth; +const API_KEY_DEFAULTS = { + name: '${escapeStringForCodeGen(getApiKeyDefaults(requirements.apiKeySchemes).name)}', + in: '${escapeStringForCodeGen(getApiKeyDefaults(requirements.apiKeySchemes).in)}' as 'header' | 'query' | 'cookie' +} as const; // ============================================================================ // Pagination Types @@ -351,14 +318,16 @@ function applyAuth( } case 'apiKey': { - const keyName = auth.name ?? 'X-API-Key'; - const keyIn = auth.in ?? 'header'; + const keyName = auth.name ?? API_KEY_DEFAULTS.name; + const keyIn = auth.in ?? API_KEY_DEFAULTS.in; if (keyIn === 'header') { headers[keyName] = auth.key; - } else { + } else if (keyIn === 'query') { const separator = url.includes('?') ? '&' : '?'; url = \`\${url}\${separator}\${keyName}=\${encodeURIComponent(auth.key)}\`; + } else if (keyIn === 'cookie') { + headers['Cookie'] = \`\${keyName}=\${auth.key}\`; } break; } @@ -376,34 +345,6 @@ function applyAuth( return { headers, url }; } -/** - * Validate OAuth2 configuration based on flow type - */ -function validateOAuth2Config(auth: OAuth2Auth): void { - // If using a flow, validate required fields - switch (auth.flow) { - case 'client_credentials': - if (!auth.tokenUrl) throw new Error('OAuth2 Client Credentials flow requires tokenUrl'); - if (!auth.clientId) throw new Error('OAuth2 Client Credentials flow requires clientId'); - break; - - case 'password': - if (!auth.tokenUrl) throw new Error('OAuth2 Password flow requires tokenUrl'); - if (!auth.clientId) throw new Error('OAuth2 Password flow requires clientId'); - if (!auth.username) throw new Error('OAuth2 Password flow requires username'); - if (!auth.password) throw new Error('OAuth2 Password flow requires password'); - break; - - default: - // No flow specified - must have accessToken for OAuth2 to work - if (!auth.accessToken && !auth.flow) { - // This is fine - token refresh can still work if refreshToken is provided - // Or the request will just be made without auth - } - break; - } -} - /** * Apply pagination parameters to URL and/or headers based on configuration */ @@ -593,126 +534,6 @@ async function executeWithRetry( throw lastError ?? new Error('Request failed after retries'); } -/** - * Handle OAuth2 token flows (client_credentials, password) - */ -async function handleOAuth2TokenFlow( - auth: OAuth2Auth, - originalParams: HttpRequestParams, - makeRequest: (params: HttpRequestParams) => Promise, - retryConfig?: RetryConfig -): Promise { - if (!auth.flow || !auth.tokenUrl) return null; - - const params = new URLSearchParams(); - - if (auth.flow === 'client_credentials') { - params.append('grant_type', 'client_credentials'); - params.append('client_id', auth.clientId!); - } else if (auth.flow === 'password') { - params.append('grant_type', 'password'); - params.append('username', auth.username || ''); - params.append('password', auth.password || ''); - params.append('client_id', auth.clientId!); - } else { - return null; - } - - if (auth.clientSecret) { - params.append('client_secret', auth.clientSecret); - } - if (auth.scopes && auth.scopes.length > 0) { - params.append('scope', auth.scopes.join(' ')); - } - - const authHeaders: Record = { - 'Content-Type': 'application/x-www-form-urlencoded' - }; - - // Use basic auth for client credentials if both client ID and secret are provided - if (auth.flow === 'client_credentials' && auth.clientId && auth.clientSecret) { - const credentials = Buffer.from(\`\${auth.clientId}:\${auth.clientSecret}\`).toString('base64'); - authHeaders['Authorization'] = \`Basic \${credentials}\`; - params.delete('client_id'); - params.delete('client_secret'); - } - - const tokenResponse = await NodeFetch.default(auth.tokenUrl, { - method: 'POST', - headers: authHeaders, - body: params.toString() - }); - - if (!tokenResponse.ok) { - throw new Error(\`OAuth2 token request failed: \${tokenResponse.statusText}\`); - } - - const tokenData = await tokenResponse.json(); - const tokens: TokenResponse = { - accessToken: tokenData.access_token, - refreshToken: tokenData.refresh_token, - expiresIn: tokenData.expires_in - }; - - // Notify the client about the tokens - if (auth.onTokenRefresh) { - auth.onTokenRefresh(tokens); - } - - // Retry the original request with the new token - const updatedHeaders = { ...originalParams.headers }; - updatedHeaders['Authorization'] = \`Bearer \${tokens.accessToken}\`; - - return executeWithRetry({ ...originalParams, headers: updatedHeaders }, makeRequest, retryConfig); -} - -/** - * Handle OAuth2 token refresh on 401 response - */ -async function handleTokenRefresh( - auth: OAuth2Auth, - originalParams: HttpRequestParams, - makeRequest: (params: HttpRequestParams) => Promise, - retryConfig?: RetryConfig -): Promise { - if (!auth.refreshToken || !auth.tokenUrl || !auth.clientId) return null; - - const refreshResponse = await NodeFetch.default(auth.tokenUrl, { - method: 'POST', - headers: { - 'Content-Type': 'application/x-www-form-urlencoded' - }, - body: new URLSearchParams({ - grant_type: 'refresh_token', - refresh_token: auth.refreshToken, - client_id: auth.clientId, - ...(auth.clientSecret ? { client_secret: auth.clientSecret } : {}) - }).toString() - }); - - if (!refreshResponse.ok) { - throw new Error('Unauthorized'); - } - - const tokenData = await refreshResponse.json(); - const newTokens: TokenResponse = { - accessToken: tokenData.access_token, - refreshToken: tokenData.refresh_token || auth.refreshToken, - expiresIn: tokenData.expires_in - }; - - // Notify the client about the refreshed tokens - if (auth.onTokenRefresh) { - auth.onTokenRefresh(newTokens); - } - - // Retry the original request with the new token - const updatedHeaders = { ...originalParams.headers }; - updatedHeaders['Authorization'] = \`Bearer \${newTokens.accessToken}\`; - - return executeWithRetry({ ...originalParams, headers: updatedHeaders }, makeRequest, retryConfig); -} - /** * Handle HTTP error status codes with standardized messages */ @@ -984,283 +805,8 @@ function applyTypedHeaders( return headers; } - +${requirements.oauth2 ? renderOAuth2Helpers() : renderOAuth2Stubs()} // ============================================================================ // Generated HTTP Client Functions // ============================================================================`; } - -export function renderHttpFetchClient({ - requestTopic, - requestMessageType, - requestMessageModule, - replyMessageType, - replyMessageModule, - channelParameters, - method, - servers = [], - subName = pascalCase(requestTopic), - functionName = `${method.toLowerCase()}${subName}`, - includesStatusCodes = false -}: RenderHttpParameters): HttpRenderType { - const messageType = requestMessageModule - ? `${requestMessageModule}.${requestMessageType}` - : requestMessageType; - const replyType = replyMessageModule - ? `${replyMessageModule}.${replyMessageType}` - : replyMessageType; - - // Generate context interface name - const contextInterfaceName = `${pascalCase(functionName)}Context`; - - // Determine if operation has path parameters - const hasParameters = channelParameters !== undefined; - - // Generate the context interface (extends HttpClientContext) - const contextInterface = generateContextInterface( - contextInterfaceName, - messageType, - hasParameters, - method - ); - - // Generate the function implementation - const functionCode = generateFunctionImplementation({ - functionName, - contextInterfaceName, - replyType, - replyMessageModule, - replyMessageType, - messageType, - requestTopic, - hasParameters, - method, - servers, - includesStatusCodes - }); - - const code = `${contextInterface} - -${functionCode}`; - - return { - messageType, - replyType, - code, - functionName, - dependencies: [ - `import { URLSearchParams, URL } from 'url';`, - `import * as NodeFetch from 'node-fetch';` - ], - functionType: ChannelFunctionTypes.HTTP_CLIENT - }; -} - -/** - * Generate the context interface for an HTTP operation - */ -function generateContextInterface( - interfaceName: string, - messageType: string | undefined, - hasParameters: boolean, - method: string -): string { - const fields: string[] = []; - - // Add payload field for methods that have a body - if (messageType && ['POST', 'PUT', 'PATCH'].includes(method.toUpperCase())) { - fields.push(` payload: ${messageType};`); - } - - // Add parameters field if operation has path parameters - if (hasParameters) { - fields.push( - ` parameters: { getChannelWithParameters: (path: string) => string };` - ); - } - - // Add requestHeaders field (optional) for operations that support typed headers - // This is always optional since headers can also be passed via additionalHeaders - fields.push(` requestHeaders?: { marshal: () => string };`); - - const fieldsStr = fields.length > 0 ? `\n${fields.join('\n')}\n` : ''; - - return `export interface ${interfaceName} extends HttpClientContext {${fieldsStr}}`; -} - -/** - * Generate the function implementation - */ -function generateFunctionImplementation(params: { - functionName: string; - contextInterfaceName: string; - replyType: string; - replyMessageModule: string | undefined; - replyMessageType: string; - messageType: string | undefined; - requestTopic: string; - hasParameters: boolean; - method: string; - servers: string[]; - includesStatusCodes: boolean; -}): string { - const { - functionName, - contextInterfaceName, - replyType, - replyMessageModule, - replyMessageType, - messageType, - requestTopic, - hasParameters, - method, - servers, - includesStatusCodes - } = params; - - const defaultServer = servers[0] ?? "'localhost:3000'"; - const hasBody = - messageType && ['POST', 'PUT', 'PATCH'].includes(method.toUpperCase()); - - // Generate URL building code - const urlBuildCode = hasParameters - ? `let url = buildUrlWithParameters(config.server, '${requestTopic}', context.parameters);` - : 'let url = `${config.server}${config.path}`;'; - - // Generate headers initialization - const headersInit = `let headers = context.requestHeaders - ? applyTypedHeaders(context.requestHeaders, config.additionalHeaders) - : { 'Content-Type': 'application/json', ...config.additionalHeaders } as Record;`; - - // Generate body preparation - const bodyPrep = hasBody - ? `const body = context.payload?.marshal();` - : `const body = undefined;`; - - // Generate response parsing - // Use unmarshalByStatusCode if the payload is a union type with status code support - let responseParseCode: string; - if (replyMessageModule) { - responseParseCode = includesStatusCodes - ? `const responseData = ${replyMessageModule}.unmarshalByStatusCode(rawData, response.status);` - : `const responseData = ${replyMessageModule}.unmarshal(rawData);`; - } else { - responseParseCode = `const responseData = ${replyMessageType}.unmarshal(rawData);`; - } - - // Generate default context for optional context parameter - const contextDefault = !hasBody && !hasParameters ? ' = {}' : ''; - - return `async function ${functionName}(context: ${contextInterfaceName}${contextDefault}): Promise> { - // Apply defaults - const config = { - path: '${requestTopic}', - server: ${defaultServer}, - ...context, - }; - - // Validate OAuth2 config if present - if (config.auth?.type === 'oauth2') { - validateOAuth2Config(config.auth); - } - - // Build headers - ${headersInit} - - // Build URL - ${urlBuildCode} - url = applyQueryParams(config.queryParams, url); - - // Apply pagination (can affect URL and/or headers) - const paginationResult = applyPagination(config.pagination, url, headers); - url = paginationResult.url; - headers = paginationResult.headers; - - // Apply authentication - const authResult = applyAuth(config.auth, headers, url); - headers = authResult.headers; - url = authResult.url; - - // Prepare body - ${bodyPrep} - - // Determine request function - const makeRequest = config.hooks?.makeRequest ?? defaultMakeRequest; - - // Build request params - let requestParams: HttpRequestParams = { - url, - method: '${method}', - headers, - body - }; - - // Apply beforeRequest hook - if (config.hooks?.beforeRequest) { - requestParams = await config.hooks.beforeRequest(requestParams); - } - - try { - // Execute request with retry logic - let response = await executeWithRetry(requestParams, makeRequest, config.retry); - - // Apply afterResponse hook - if (config.hooks?.afterResponse) { - response = await config.hooks.afterResponse(response, requestParams); - } - - // Handle OAuth2 token flows that require getting a token first - if (config.auth?.type === 'oauth2' && !config.auth.accessToken) { - const tokenFlowResponse = await handleOAuth2TokenFlow(config.auth, requestParams, makeRequest, config.retry); - if (tokenFlowResponse) { - response = tokenFlowResponse; - } - } - - // Handle 401 with token refresh - if (response.status === 401 && config.auth?.type === 'oauth2') { - try { - const refreshResponse = await handleTokenRefresh(config.auth, requestParams, makeRequest, config.retry); - if (refreshResponse) { - response = refreshResponse; - } - } catch { - throw new Error('Unauthorized'); - } - } - - // Handle error responses - if (!response.ok) { - handleHttpError(response.status, response.statusText); - } - - // Parse response - const rawData = await response.json(); - ${responseParseCode} - - // Extract response metadata - const responseHeaders = extractHeaders(response); - const paginationInfo = extractPaginationInfo(responseHeaders, config.pagination); - - // Build response wrapper with pagination helpers - const result: HttpClientResponse<${replyType}> = { - data: responseData, - status: response.status, - statusText: response.statusText, - headers: responseHeaders, - rawData, - pagination: paginationInfo, - ...createPaginationHelpers(config, paginationInfo, ${functionName}), - }; - - return result; - - } catch (error) { - // Apply onError hook if present - if (config.hooks?.onError && error instanceof Error) { - throw await config.hooks.onError(error, requestParams); - } - throw error; - } -}`; -} diff --git a/src/codegen/generators/typescript/channels/protocols/http/index.ts b/src/codegen/generators/typescript/channels/protocols/http/index.ts index a90d5ed7..8a6e9aec 100644 --- a/src/codegen/generators/typescript/channels/protocols/http/index.ts +++ b/src/codegen/generators/typescript/channels/protocols/http/index.ts @@ -17,9 +17,25 @@ import { import {ChannelInterface} from '@asyncapi/parser'; import {HttpRenderType, SingleFunctionRenderType} from '../../../../../types'; import {ConstrainedObjectModel} from '@asyncapi/modelina'; -import {renderHttpFetchClient, renderHttpCommonTypes} from './fetch'; +import {renderHttpCommonTypes} from './common-types'; +import {renderHttpFetchClient} from './client'; -export {renderHttpFetchClient, renderHttpCommonTypes}; +// Re-export main functions for backward compatibility +export {renderHttpCommonTypes, renderHttpFetchClient}; + +// Re-export security utilities for external use +export { + analyzeSecuritySchemes, + escapeStringForCodeGen, + getApiKeyDefaults, + renderOAuth2Helpers, + renderOAuth2Stubs, + renderSecurityTypes, + type AuthTypeRequirements +} from './security'; + +// Re-export types +export type {SecuritySchemeOptions} from '../../types'; export async function generatehttpChannels( context: TypeScriptChannelsGeneratorContext, diff --git a/src/codegen/generators/typescript/channels/protocols/http/security.ts b/src/codegen/generators/typescript/channels/protocols/http/security.ts new file mode 100644 index 00000000..67cd19f9 --- /dev/null +++ b/src/codegen/generators/typescript/channels/protocols/http/security.ts @@ -0,0 +1,575 @@ +/** + * Security type generation for HTTP client. + * Analyzes OpenAPI security schemes and generates TypeScript auth interfaces. + */ +import {SecuritySchemeOptions} from '../../types'; + +/** + * Escapes special characters in strings that will be interpolated into generated code. + * Prevents syntax errors when OpenAPI spec values contain quotes, backticks, or template expressions. + */ +export function escapeStringForCodeGen(value: string | undefined): string { + if (!value) { + return ''; + } + return value + .replace(/\\/g, '\\\\') // Escape backslashes first + .replace(/\n/g, '\\n') // Escape newlines + .replace(/\r/g, '\\r') // Escape carriage returns + .replace(/'/g, "\\'") // Escape single quotes + .replace(/`/g, '\\`') // Escape backticks + .replace(/\$/g, '\\$') // Escape dollar signs (prevents ${} template evaluation) + .replace(/\*\//g, '*\\/'); // Escape */ to prevent JSDoc comment injection +} + +/** + * Determines which auth types are needed based on security schemes. + */ +export interface AuthTypeRequirements { + bearer: boolean; + basic: boolean; + apiKey: boolean; + oauth2: boolean; + apiKeySchemes: SecuritySchemeOptions[]; + oauth2Schemes: SecuritySchemeOptions[]; +} + +/** + * Analyzes security schemes to determine which auth types are needed. + */ +export function analyzeSecuritySchemes( + schemes: SecuritySchemeOptions[] | undefined +): AuthTypeRequirements { + // undefined or empty array = backward compatibility mode, generate all types + // This allows users to manually configure auth even if no schemes are defined + if (!schemes || schemes.length === 0) { + return { + bearer: true, + basic: true, + apiKey: true, + oauth2: true, + apiKeySchemes: [], + oauth2Schemes: [] + }; + } + + const requirements: AuthTypeRequirements = { + bearer: false, + basic: false, + apiKey: false, + oauth2: false, + apiKeySchemes: [], + oauth2Schemes: [] + }; + + for (const scheme of schemes) { + switch (scheme.type) { + case 'apiKey': + requirements.apiKey = true; + requirements.apiKeySchemes.push(scheme); + break; + case 'http': + if (scheme.httpScheme === 'bearer') { + requirements.bearer = true; + } else if (scheme.httpScheme === 'basic') { + requirements.basic = true; + } + break; + case 'oauth2': + case 'openIdConnect': + requirements.oauth2 = true; + requirements.oauth2Schemes.push(scheme); + break; + } + } + + return requirements; +} + +/** + * Generates the BearerAuth interface. + */ +function renderBearerAuthInterface(): string { + return `/** + * Bearer token authentication configuration + */ +export interface BearerAuth { + type: 'bearer'; + token: string; +}`; +} + +/** + * Generates the BasicAuth interface. + */ +function renderBasicAuthInterface(): string { + return `/** + * Basic authentication configuration (username/password) + */ +export interface BasicAuth { + type: 'basic'; + username: string; + password: string; +}`; +} + +/** + * Extracts API key defaults from schemes. + * If there's exactly one apiKey scheme, use its values; otherwise use standard defaults. + */ +export function getApiKeyDefaults(apiKeySchemes: SecuritySchemeOptions[]): { + name: string; + in: string; +} { + if (apiKeySchemes.length === 1) { + return { + name: apiKeySchemes[0].apiKeyName || 'X-API-Key', + in: apiKeySchemes[0].apiKeyIn || 'header' + }; + } + return { + name: 'X-API-Key', + in: 'header' + }; +} + +/** + * Generates the ApiKeyAuth interface with optional pre-populated defaults from spec. + */ +function renderApiKeyAuthInterface( + apiKeySchemes: SecuritySchemeOptions[] +): string { + const defaults = getApiKeyDefaults(apiKeySchemes); + + // For cookie support + const inType = apiKeySchemes.some((s) => s.apiKeyIn === 'cookie') + ? "'header' | 'query' | 'cookie'" + : "'header' | 'query'"; + + // Escape spec values for safe interpolation into generated code + const escapedDefaultName = escapeStringForCodeGen(defaults.name); + const escapedDefaultIn = escapeStringForCodeGen(defaults.in); + + return `/** + * API key authentication configuration + */ +export interface ApiKeyAuth { + type: 'apiKey'; + key: string; + name?: string; // Name of the API key parameter (default: '${escapedDefaultName}') + in?: ${inType}; // Where to place the API key (default: '${escapedDefaultIn}') +}`; +} + +/** + * Extracts the tokenUrl from OAuth2 flows. + */ +function extractTokenUrl( + flows: NonNullable +): string | undefined { + return ( + flows.clientCredentials?.tokenUrl || + flows.password?.tokenUrl || + flows.authorizationCode?.tokenUrl + ); +} + +/** + * Extracts the authorizationUrl from OAuth2 flows. + */ +function extractAuthorizationUrl( + flows: NonNullable +): string | undefined { + return ( + flows.implicit?.authorizationUrl || + flows.authorizationCode?.authorizationUrl + ); +} + +/** + * Collects all scopes from OAuth2 flows. + */ +function collectScopes( + flows: NonNullable +): Set { + const allScopes = new Set(); + const flowTypes = [ + flows.implicit, + flows.password, + flows.clientCredentials, + flows.authorizationCode + ]; + + for (const flow of flowTypes) { + if (flow?.scopes) { + Object.keys(flow.scopes).forEach((s) => allScopes.add(s)); + } + } + + return allScopes; +} + +interface OAuth2DocComments { + tokenUrlComment: string; + authorizationUrlComment: string; + scopesComment: string; +} + +/** + * Formats scopes into a documentation comment. + */ +function formatScopesComment(scopes: Set): string { + if (scopes.size === 0) { + return ''; + } + const scopeList = Array.from(scopes) + .slice(0, 3) + .map((scope) => escapeStringForCodeGen(scope)) + .join(', '); + const suffix = scopes.size > 3 ? '...' : ''; + return ` Available: ${scopeList}${suffix}`; +} + +/** + * Extracts documentation comments from a single OAuth2 scheme. + */ +function extractSchemeComments( + scheme: SecuritySchemeOptions, + existing: OAuth2DocComments +): OAuth2DocComments { + if (scheme.openIdConnectUrl) { + return { + ...existing, + tokenUrlComment: `OpenID Connect URL: '${escapeStringForCodeGen(scheme.openIdConnectUrl)}'` + }; + } + + if (!scheme.oauth2Flows) { + return existing; + } + + const tokenUrl = extractTokenUrl(scheme.oauth2Flows); + const authUrl = extractAuthorizationUrl(scheme.oauth2Flows); + const allScopes = collectScopes(scheme.oauth2Flows); + + return { + tokenUrlComment: tokenUrl + ? `default: '${escapeStringForCodeGen(tokenUrl)}'` + : existing.tokenUrlComment, + authorizationUrlComment: authUrl + ? ` Authorization URL: '${escapeStringForCodeGen(authUrl)}'` + : existing.authorizationUrlComment, + scopesComment: formatScopesComment(allScopes) || existing.scopesComment + }; +} + +/** + * Extracts documentation comments from OAuth2 schemes. + */ +function extractOAuth2DocComments( + oauth2Schemes: SecuritySchemeOptions[] +): OAuth2DocComments { + const initial: OAuth2DocComments = { + tokenUrlComment: + 'required for client_credentials/password flows and token refresh', + authorizationUrlComment: '', + scopesComment: '' + }; + + return oauth2Schemes.reduce( + (acc, scheme) => extractSchemeComments(scheme, acc), + initial + ); +} + +/** + * Generates the OAuth2Auth interface with optional pre-populated values from spec. + */ +function renderOAuth2AuthInterface( + oauth2Schemes: SecuritySchemeOptions[] +): string { + const {tokenUrlComment, authorizationUrlComment, scopesComment} = + extractOAuth2DocComments(oauth2Schemes); + + const flowsInfo = authorizationUrlComment + ? `\n *${authorizationUrlComment}` + : ''; + + return `/** + * OAuth2 authentication configuration + * + * Supports server-side flows only: + * - client_credentials: Server-to-server authentication + * - password: Resource owner password credentials (legacy, not recommended) + * - Pre-obtained accessToken: For tokens obtained via browser-based flows + * + * For browser-based flows (implicit, authorization_code), obtain the token + * separately and pass it as accessToken.${flowsInfo} + */ +export interface OAuth2Auth { + type: 'oauth2'; + /** Pre-obtained access token (required if not using a server-side flow) */ + accessToken?: string; + /** Refresh token for automatic token renewal on 401 */ + refreshToken?: string; + /** Token endpoint URL (${tokenUrlComment}) */ + tokenUrl?: string; + /** Client ID (required for flows and token refresh) */ + clientId?: string; + /** Client secret (optional, depends on OAuth provider) */ + clientSecret?: string; + /** Requested scopes${scopesComment} */ + scopes?: string[]; + /** Server-side flow type */ + flow?: 'password' | 'client_credentials'; + /** Username for password flow */ + username?: string; + /** Password for password flow */ + password?: string; + /** Callback when tokens are refreshed (for caching/persistence) */ + onTokenRefresh?: (newTokens: TokenResponse) => void; +}`; +} + +/** + * Generates the AuthConfig union type based on which auth types are needed. + */ +function renderAuthConfigType(requirements: AuthTypeRequirements): string { + const types: string[] = []; + + if (requirements.bearer) { + types.push('BearerAuth'); + } + if (requirements.basic) { + types.push('BasicAuth'); + } + if (requirements.apiKey) { + types.push('ApiKeyAuth'); + } + if (requirements.oauth2) { + types.push('OAuth2Auth'); + } + + // If no types are needed (e.g., no recognized security schemes), don't generate AuthConfig + // The auth field in HttpClientContext is optional, so this is safe + if (types.length === 0) { + return '// No authentication types needed for this API\nexport type AuthConfig = never;'; + } + + return `/** + * Union type for all authentication methods - provides autocomplete support + */ +export type AuthConfig = ${types.join(' | ')};`; +} + +/** + * Generates the security configuration types based on extracted security schemes. + */ +export function renderSecurityTypes( + schemes: SecuritySchemeOptions[] | undefined, + requirements?: AuthTypeRequirements +): string { + const authRequirements = requirements ?? analyzeSecuritySchemes(schemes); + + const bearerSection = authRequirements.bearer + ? `${renderBearerAuthInterface()}\n\n` + : ''; + + const basicSection = authRequirements.basic + ? `${renderBasicAuthInterface()}\n\n` + : ''; + + const apiKeySection = authRequirements.apiKey + ? `${renderApiKeyAuthInterface(authRequirements.apiKeySchemes)}\n\n` + : ''; + + const oauth2Section = authRequirements.oauth2 + ? `${renderOAuth2AuthInterface(authRequirements.oauth2Schemes)}\n\n` + : ''; + + return `// ============================================================================ +// Security Configuration Types - Grouped for better autocomplete +// ============================================================================ + +${bearerSection}${basicSection}${apiKeySection}${oauth2Section}${renderAuthConfigType(authRequirements)}`; +} + +/** + * Generate OAuth2 stub functions when OAuth2 is not available. + * These stubs ensure TypeScript compilation succeeds when generated code + * references OAuth2 functions, but the runtime guards prevent them from being called. + */ +export function renderOAuth2Stubs(): string { + return ` +// OAuth2 helpers not needed for this API - provide type-safe stubs +// These are never called due to AUTH_FEATURES.oauth2 runtime guards +type OAuth2Auth = never; +function validateOAuth2Config(_auth: OAuth2Auth): void {} +async function handleOAuth2TokenFlow( + _auth: OAuth2Auth, + _originalParams: HttpRequestParams, + _makeRequest: (params: HttpRequestParams) => Promise, + _retryConfig?: RetryConfig +): Promise { return null; } +async function handleTokenRefresh( + _auth: OAuth2Auth, + _originalParams: HttpRequestParams, + _makeRequest: (params: HttpRequestParams) => Promise, + _retryConfig?: RetryConfig +): Promise { return null; }`; +} + +/** + * Generates OAuth2-specific helper functions. + * Only included when OAuth2 auth is needed. + */ +export function renderOAuth2Helpers(): string { + return ` +/** + * Validate OAuth2 configuration based on flow type + */ +function validateOAuth2Config(auth: OAuth2Auth): void { + // If using a flow, validate required fields + switch (auth.flow) { + case 'client_credentials': + if (!auth.tokenUrl) throw new Error('OAuth2 Client Credentials flow requires tokenUrl'); + if (!auth.clientId) throw new Error('OAuth2 Client Credentials flow requires clientId'); + break; + + case 'password': + if (!auth.tokenUrl) throw new Error('OAuth2 Password flow requires tokenUrl'); + if (!auth.clientId) throw new Error('OAuth2 Password flow requires clientId'); + if (!auth.username) throw new Error('OAuth2 Password flow requires username'); + if (!auth.password) throw new Error('OAuth2 Password flow requires password'); + break; + + default: + // No flow specified - must have accessToken for OAuth2 to work + if (!auth.accessToken && !auth.flow) { + // This is fine - token refresh can still work if refreshToken is provided + // Or the request will just be made without auth + } + break; + } +} + +/** + * Handle OAuth2 token flows (client_credentials, password) + */ +async function handleOAuth2TokenFlow( + auth: OAuth2Auth, + originalParams: HttpRequestParams, + makeRequest: (params: HttpRequestParams) => Promise, + retryConfig?: RetryConfig +): Promise { + if (!auth.flow || !auth.tokenUrl) return null; + + const params = new URLSearchParams(); + + if (auth.flow === 'client_credentials') { + params.append('grant_type', 'client_credentials'); + params.append('client_id', auth.clientId!); + } else if (auth.flow === 'password') { + params.append('grant_type', 'password'); + params.append('username', auth.username || ''); + params.append('password', auth.password || ''); + params.append('client_id', auth.clientId!); + } else { + return null; + } + + if (auth.clientSecret) { + params.append('client_secret', auth.clientSecret); + } + if (auth.scopes && auth.scopes.length > 0) { + params.append('scope', auth.scopes.join(' ')); + } + + const authHeaders: Record = { + 'Content-Type': 'application/x-www-form-urlencoded' + }; + + // Use basic auth for client credentials if both client ID and secret are provided + if (auth.flow === 'client_credentials' && auth.clientId && auth.clientSecret) { + const credentials = Buffer.from(\`\${auth.clientId}:\${auth.clientSecret}\`).toString('base64'); + authHeaders['Authorization'] = \`Basic \${credentials}\`; + params.delete('client_id'); + params.delete('client_secret'); + } + + const tokenResponse = await NodeFetch.default(auth.tokenUrl, { + method: 'POST', + headers: authHeaders, + body: params.toString() + }); + + if (!tokenResponse.ok) { + throw new Error(\`OAuth2 token request failed: \${tokenResponse.statusText}\`); + } + + const tokenData = await tokenResponse.json(); + const tokens: TokenResponse = { + accessToken: tokenData.access_token, + refreshToken: tokenData.refresh_token, + expiresIn: tokenData.expires_in + }; + + // Notify the client about the tokens + if (auth.onTokenRefresh) { + auth.onTokenRefresh(tokens); + } + + // Retry the original request with the new token + const updatedHeaders = { ...originalParams.headers }; + updatedHeaders['Authorization'] = \`Bearer \${tokens.accessToken}\`; + + return executeWithRetry({ ...originalParams, headers: updatedHeaders }, makeRequest, retryConfig); +} + +/** + * Handle OAuth2 token refresh on 401 response + */ +async function handleTokenRefresh( + auth: OAuth2Auth, + originalParams: HttpRequestParams, + makeRequest: (params: HttpRequestParams) => Promise, + retryConfig?: RetryConfig +): Promise { + if (!auth.refreshToken || !auth.tokenUrl || !auth.clientId) return null; + + const refreshResponse = await NodeFetch.default(auth.tokenUrl, { + method: 'POST', + headers: { + 'Content-Type': 'application/x-www-form-urlencoded' + }, + body: new URLSearchParams({ + grant_type: 'refresh_token', + refresh_token: auth.refreshToken, + client_id: auth.clientId, + ...(auth.clientSecret ? { client_secret: auth.clientSecret } : {}) + }).toString() + }); + + if (!refreshResponse.ok) { + throw new Error('Unauthorized'); + } + + const tokenData = await refreshResponse.json(); + const newTokens: TokenResponse = { + accessToken: tokenData.access_token, + refreshToken: tokenData.refresh_token || auth.refreshToken, + expiresIn: tokenData.expires_in + }; + + // Notify the client about the refreshed tokens + if (auth.onTokenRefresh) { + auth.onTokenRefresh(newTokens); + } + + // Retry the original request with the new token + const updatedHeaders = { ...originalParams.headers }; + updatedHeaders['Authorization'] = \`Bearer \${newTokens.accessToken}\`; + + return executeWithRetry({ ...originalParams, headers: updatedHeaders }, makeRequest, retryConfig); +}`; +} diff --git a/src/codegen/generators/typescript/channels/types.ts b/src/codegen/generators/typescript/channels/types.ts index 234bb18b..0273d4e8 100644 --- a/src/codegen/generators/typescript/channels/types.ts +++ b/src/codegen/generators/typescript/channels/types.ts @@ -6,6 +6,10 @@ import {TypeScriptPayloadRenderType} from '../payloads'; import {TypeScriptParameterRenderType} from '../parameters'; import {ConstrainedObjectModel} from '@asyncapi/modelina'; import {OpenAPIV2, OpenAPIV3, OpenAPIV3_1} from 'openapi-types'; +import {SecuritySchemeOptions} from '../../../inputs/openapi/security'; + +// Re-export for convenience +export {SecuritySchemeOptions}; export enum ChannelFunctionTypes { NATS_JETSTREAM_PUBLISH = 'nats_jetstream_publish', diff --git a/src/codegen/inputs/openapi/security.ts b/src/codegen/inputs/openapi/security.ts new file mode 100644 index 00000000..1557d191 --- /dev/null +++ b/src/codegen/inputs/openapi/security.ts @@ -0,0 +1,298 @@ +/** + * Extracts security scheme information from OpenAPI 2.0/3.x documents. + * Converts security definitions to a normalized internal format. + */ +import {OpenAPIV2, OpenAPIV3, OpenAPIV3_1} from 'openapi-types'; + +/** + * Security scheme configuration options. + * + * Provides a normalized representation of security schemes from OpenAPI documents, + * supporting both OpenAPI 3.x securitySchemes and Swagger 2.0 securityDefinitions. + * This interface is used by generators to configure authentication in generated code. + */ +export interface SecuritySchemeOptions { + /** The name/key of the security scheme as defined in the spec */ + name: string; + /** Security scheme type */ + type: 'apiKey' | 'http' | 'oauth2' | 'openIdConnect'; + + /** For apiKey type: the name of the key parameter */ + apiKeyName?: string; + /** For apiKey type: where to place the key */ + apiKeyIn?: 'header' | 'query' | 'cookie'; + + /** For http type: the authentication scheme (bearer, basic, etc.) */ + httpScheme?: 'bearer' | 'basic' | string; + /** For http bearer: the format of the bearer token */ + bearerFormat?: string; + + /** For oauth2 type: the available flows */ + oauth2Flows?: { + implicit?: { + authorizationUrl: string; + scopes: Record; + }; + password?: { + tokenUrl: string; + scopes: Record; + }; + clientCredentials?: { + tokenUrl: string; + scopes: Record; + }; + authorizationCode?: { + authorizationUrl: string; + tokenUrl: string; + scopes: Record; + }; + }; + + /** For openIdConnect type: the OpenID Connect discovery URL */ + openIdConnectUrl?: string; +} + +type OpenAPIDocument = + | OpenAPIV3.Document + | OpenAPIV2.Document + | OpenAPIV3_1.Document; + +/** + * Extracts security schemes from an OpenAPI document. + * Handles both OpenAPI 3.x (components.securitySchemes) and + * Swagger 2.0 (securityDefinitions) formats. + */ +export function extractSecuritySchemes( + document: OpenAPIDocument +): SecuritySchemeOptions[] { + // Check if OpenAPI 3.x document + if ('openapi' in document) { + return extractOpenAPI3SecuritySchemes( + document as OpenAPIV3.Document | OpenAPIV3_1.Document + ); + } + + // Check if Swagger 2.0 document + if ('swagger' in document) { + return extractSwagger2SecuritySchemes(document as OpenAPIV2.Document); + } + + return []; +} + +/** + * Extracts security schemes from OpenAPI 3.x documents. + */ +function extractOpenAPI3SecuritySchemes( + document: OpenAPIV3.Document | OpenAPIV3_1.Document +): SecuritySchemeOptions[] { + const securitySchemes = document.components?.securitySchemes; + if (!securitySchemes) { + return []; + } + + const schemes: SecuritySchemeOptions[] = []; + + for (const [name, scheme] of Object.entries(securitySchemes)) { + // Skip $ref - should be dereferenced already + if ('$ref' in scheme) { + continue; + } + + const securityScheme = scheme as OpenAPIV3.SecuritySchemeObject; + const extracted = extractOpenAPI3Scheme(name, securityScheme); + if (extracted) { + schemes.push(extracted); + } + } + + return schemes; +} + +/** + * Extracts a single OpenAPI 3.x security scheme. + */ +function extractOpenAPI3Scheme( + name: string, + scheme: OpenAPIV3.SecuritySchemeObject +): SecuritySchemeOptions | undefined { + switch (scheme.type) { + case 'apiKey': + return { + name, + type: 'apiKey', + apiKeyName: scheme.name, + apiKeyIn: scheme.in as 'header' | 'query' | 'cookie' + }; + + case 'http': + return { + name, + type: 'http', + httpScheme: scheme.scheme as 'bearer' | 'basic', + bearerFormat: scheme.bearerFormat + }; + + case 'oauth2': + return { + name, + type: 'oauth2', + oauth2Flows: extractOAuth2Flows(scheme.flows) + }; + + case 'openIdConnect': + return { + name, + type: 'openIdConnect', + openIdConnectUrl: scheme.openIdConnectUrl + }; + + default: + return undefined; + } +} + +/** + * Extracts OAuth2 flows from OpenAPI 3.x OAuth2 security scheme. + */ +function extractOAuth2Flows( + flows: OpenAPIV3.OAuth2SecurityScheme['flows'] +): SecuritySchemeOptions['oauth2Flows'] { + const result: SecuritySchemeOptions['oauth2Flows'] = {}; + + if (flows.implicit) { + result.implicit = { + authorizationUrl: flows.implicit.authorizationUrl, + scopes: flows.implicit.scopes || {} + }; + } + + if (flows.password) { + result.password = { + tokenUrl: flows.password.tokenUrl, + scopes: flows.password.scopes || {} + }; + } + + if (flows.clientCredentials) { + result.clientCredentials = { + tokenUrl: flows.clientCredentials.tokenUrl, + scopes: flows.clientCredentials.scopes || {} + }; + } + + if (flows.authorizationCode) { + result.authorizationCode = { + authorizationUrl: flows.authorizationCode.authorizationUrl, + tokenUrl: flows.authorizationCode.tokenUrl, + scopes: flows.authorizationCode.scopes || {} + }; + } + + return result; +} + +/** + * Extracts security definitions from Swagger 2.0 documents. + */ +function extractSwagger2SecuritySchemes( + document: OpenAPIV2.Document +): SecuritySchemeOptions[] { + const securityDefinitions = document.securityDefinitions; + if (!securityDefinitions) { + return []; + } + + const schemes: SecuritySchemeOptions[] = []; + + for (const [name, definition] of Object.entries(securityDefinitions)) { + const extracted = extractSwagger2Scheme(name, definition); + if (extracted) { + schemes.push(extracted); + } + } + + return schemes; +} + +/** + * Extracts a single Swagger 2.0 security definition. + */ +function extractSwagger2Scheme( + name: string, + definition: OpenAPIV2.SecuritySchemeObject +): SecuritySchemeOptions | undefined { + switch (definition.type) { + case 'apiKey': + return { + name, + type: 'apiKey', + apiKeyName: definition.name, + apiKeyIn: definition.in as 'header' | 'query' + }; + + case 'basic': + // Swagger 2.0 has a separate 'basic' type which maps to http basic + return { + name, + type: 'http', + httpScheme: 'basic' + }; + + case 'oauth2': + return { + name, + type: 'oauth2', + oauth2Flows: extractSwagger2OAuth2Flow(definition) + }; + + default: + return undefined; + } +} + +/** + * Extracts OAuth2 flow from Swagger 2.0 format. + * Swagger 2.0 uses single 'flow' field instead of 'flows' object. + */ +function extractSwagger2OAuth2Flow( + definition: OpenAPIV2.SecuritySchemeOauth2 +): SecuritySchemeOptions['oauth2Flows'] { + const result: SecuritySchemeOptions['oauth2Flows'] = {}; + const scopes = definition.scopes || {}; + + switch (definition.flow) { + case 'implicit': + result.implicit = { + authorizationUrl: definition.authorizationUrl || '', + scopes + }; + break; + + case 'password': + result.password = { + tokenUrl: definition.tokenUrl || '', + scopes + }; + break; + + case 'application': + // Swagger 2.0 'application' maps to OpenAPI 3.x 'clientCredentials' + result.clientCredentials = { + tokenUrl: definition.tokenUrl || '', + scopes + }; + break; + + case 'accessCode': + // Swagger 2.0 'accessCode' maps to OpenAPI 3.x 'authorizationCode' + result.authorizationCode = { + authorizationUrl: definition.authorizationUrl || '', + tokenUrl: definition.tokenUrl || '', + scopes + }; + break; + } + + return result; +} diff --git a/test/codegen/generators/typescript/__snapshots__/channels.spec.ts.snap b/test/codegen/generators/typescript/__snapshots__/channels.spec.ts.snap index 52a490e3..5eab2593 100644 --- a/test/codegen/generators/typescript/__snapshots__/channels.spec.ts.snap +++ b/test/codegen/generators/typescript/__snapshots__/channels.spec.ts.snap @@ -99,30 +99,13 @@ export interface TokenResponse { // Security Configuration Types - Grouped for better autocomplete // ============================================================================ -/** - * Bearer token authentication configuration - */ -export interface BearerAuth { - type: 'bearer'; - token: string; -} - -/** - * Basic authentication configuration (username/password) - */ -export interface BasicAuth { - type: 'basic'; - username: string; - password: string; -} - /** * API key authentication configuration */ export interface ApiKeyAuth { type: 'apiKey'; key: string; - name?: string; // Name of the API key parameter (default: 'X-API-Key') + name?: string; // Name of the API key parameter (default: 'api_key') in?: 'header' | 'query'; // Where to place the API key (default: 'header') } @@ -136,6 +119,7 @@ export interface ApiKeyAuth { * * For browser-based flows (implicit, authorization_code), obtain the token * separately and pass it as accessToken. + * Authorization URL: 'http://petstore.swagger.io/api/oauth/dialog' */ export interface OAuth2Auth { type: 'oauth2'; @@ -149,7 +133,7 @@ export interface OAuth2Auth { clientId?: string; /** Client secret (optional, depends on OAuth provider) */ clientSecret?: string; - /** Requested scopes */ + /** Requested scopes Available: write:pets, read:pets */ scopes?: string[]; /** Server-side flow type */ flow?: 'password' | 'client_credentials'; @@ -164,7 +148,24 @@ export interface OAuth2Auth { /** * Union type for all authentication methods - provides autocomplete support */ -export type AuthConfig = BearerAuth | BasicAuth | ApiKeyAuth | OAuth2Auth; +export type AuthConfig = ApiKeyAuth | OAuth2Auth; + +/** + * Feature flags indicating which auth types are available. + * Used internally to conditionally call auth-specific helpers. + */ +const AUTH_FEATURES = { + oauth2: true +} as const; + +/** + * Default values for API key authentication derived from the spec. + * These match the defaults documented in the ApiKeyAuth interface. + */ +const API_KEY_DEFAULTS = { + name: 'api_key', + in: 'header' as 'header' | 'query' | 'cookie' +} as const; // ============================================================================ // Pagination Types @@ -358,14 +359,16 @@ function applyAuth( } case 'apiKey': { - const keyName = auth.name ?? 'X-API-Key'; - const keyIn = auth.in ?? 'header'; + const keyName = auth.name ?? API_KEY_DEFAULTS.name; + const keyIn = auth.in ?? API_KEY_DEFAULTS.in; if (keyIn === 'header') { headers[keyName] = auth.key; - } else { + } else if (keyIn === 'query') { const separator = url.includes('?') ? '&' : '?'; url = \`\${url}\${separator}\${keyName}=\${encodeURIComponent(auth.key)}\`; + } else if (keyIn === 'cookie') { + headers['Cookie'] = \`\${keyName}=\${auth.key}\`; } break; } @@ -383,34 +386,6 @@ function applyAuth( return { headers, url }; } -/** - * Validate OAuth2 configuration based on flow type - */ -function validateOAuth2Config(auth: OAuth2Auth): void { - // If using a flow, validate required fields - switch (auth.flow) { - case 'client_credentials': - if (!auth.tokenUrl) throw new Error('OAuth2 Client Credentials flow requires tokenUrl'); - if (!auth.clientId) throw new Error('OAuth2 Client Credentials flow requires clientId'); - break; - - case 'password': - if (!auth.tokenUrl) throw new Error('OAuth2 Password flow requires tokenUrl'); - if (!auth.clientId) throw new Error('OAuth2 Password flow requires clientId'); - if (!auth.username) throw new Error('OAuth2 Password flow requires username'); - if (!auth.password) throw new Error('OAuth2 Password flow requires password'); - break; - - default: - // No flow specified - must have accessToken for OAuth2 to work - if (!auth.accessToken && !auth.flow) { - // This is fine - token refresh can still work if refreshToken is provided - // Or the request will just be made without auth - } - break; - } -} - /** * Apply pagination parameters to URL and/or headers based on configuration */ @@ -600,126 +575,6 @@ async function executeWithRetry( throw lastError ?? new Error('Request failed after retries'); } -/** - * Handle OAuth2 token flows (client_credentials, password) - */ -async function handleOAuth2TokenFlow( - auth: OAuth2Auth, - originalParams: HttpRequestParams, - makeRequest: (params: HttpRequestParams) => Promise, - retryConfig?: RetryConfig -): Promise { - if (!auth.flow || !auth.tokenUrl) return null; - - const params = new URLSearchParams(); - - if (auth.flow === 'client_credentials') { - params.append('grant_type', 'client_credentials'); - params.append('client_id', auth.clientId!); - } else if (auth.flow === 'password') { - params.append('grant_type', 'password'); - params.append('username', auth.username || ''); - params.append('password', auth.password || ''); - params.append('client_id', auth.clientId!); - } else { - return null; - } - - if (auth.clientSecret) { - params.append('client_secret', auth.clientSecret); - } - if (auth.scopes && auth.scopes.length > 0) { - params.append('scope', auth.scopes.join(' ')); - } - - const authHeaders: Record = { - 'Content-Type': 'application/x-www-form-urlencoded' - }; - - // Use basic auth for client credentials if both client ID and secret are provided - if (auth.flow === 'client_credentials' && auth.clientId && auth.clientSecret) { - const credentials = Buffer.from(\`\${auth.clientId}:\${auth.clientSecret}\`).toString('base64'); - authHeaders['Authorization'] = \`Basic \${credentials}\`; - params.delete('client_id'); - params.delete('client_secret'); - } - - const tokenResponse = await NodeFetch.default(auth.tokenUrl, { - method: 'POST', - headers: authHeaders, - body: params.toString() - }); - - if (!tokenResponse.ok) { - throw new Error(\`OAuth2 token request failed: \${tokenResponse.statusText}\`); - } - - const tokenData = await tokenResponse.json(); - const tokens: TokenResponse = { - accessToken: tokenData.access_token, - refreshToken: tokenData.refresh_token, - expiresIn: tokenData.expires_in - }; - - // Notify the client about the tokens - if (auth.onTokenRefresh) { - auth.onTokenRefresh(tokens); - } - - // Retry the original request with the new token - const updatedHeaders = { ...originalParams.headers }; - updatedHeaders['Authorization'] = \`Bearer \${tokens.accessToken}\`; - - return executeWithRetry({ ...originalParams, headers: updatedHeaders }, makeRequest, retryConfig); -} - -/** - * Handle OAuth2 token refresh on 401 response - */ -async function handleTokenRefresh( - auth: OAuth2Auth, - originalParams: HttpRequestParams, - makeRequest: (params: HttpRequestParams) => Promise, - retryConfig?: RetryConfig -): Promise { - if (!auth.refreshToken || !auth.tokenUrl || !auth.clientId) return null; - - const refreshResponse = await NodeFetch.default(auth.tokenUrl, { - method: 'POST', - headers: { - 'Content-Type': 'application/x-www-form-urlencoded' - }, - body: new URLSearchParams({ - grant_type: 'refresh_token', - refresh_token: auth.refreshToken, - client_id: auth.clientId, - ...(auth.clientSecret ? { client_secret: auth.clientSecret } : {}) - }).toString() - }); - - if (!refreshResponse.ok) { - throw new Error('Unauthorized'); - } - - const tokenData = await refreshResponse.json(); - const newTokens: TokenResponse = { - accessToken: tokenData.access_token, - refreshToken: tokenData.refresh_token || auth.refreshToken, - expiresIn: tokenData.expires_in - }; - - // Notify the client about the refreshed tokens - if (auth.onTokenRefresh) { - auth.onTokenRefresh(newTokens); - } - - // Retry the original request with the new token - const updatedHeaders = { ...originalParams.headers }; - updatedHeaders['Authorization'] = \`Bearer \${newTokens.accessToken}\`; - - return executeWithRetry({ ...originalParams, headers: updatedHeaders }, makeRequest, retryConfig); -} - /** * Handle HTTP error status codes with standardized messages */ @@ -992,41 +847,188 @@ function applyTypedHeaders( return headers; } -// ============================================================================ -// Generated HTTP Client Functions -// ============================================================================ +/** + * Validate OAuth2 configuration based on flow type + */ +function validateOAuth2Config(auth: OAuth2Auth): void { + // If using a flow, validate required fields + switch (auth.flow) { + case 'client_credentials': + if (!auth.tokenUrl) throw new Error('OAuth2 Client Credentials flow requires tokenUrl'); + if (!auth.clientId) throw new Error('OAuth2 Client Credentials flow requires clientId'); + break; -export interface PostAddPetContext extends HttpClientContext { - payload: Pet; - requestHeaders?: { marshal: () => string }; + case 'password': + if (!auth.tokenUrl) throw new Error('OAuth2 Password flow requires tokenUrl'); + if (!auth.clientId) throw new Error('OAuth2 Password flow requires clientId'); + if (!auth.username) throw new Error('OAuth2 Password flow requires username'); + if (!auth.password) throw new Error('OAuth2 Password flow requires password'); + break; + + default: + // No flow specified - must have accessToken for OAuth2 to work + if (!auth.accessToken && !auth.flow) { + // This is fine - token refresh can still work if refreshToken is provided + // Or the request will just be made without auth + } + break; + } } -async function postAddPet(context: PostAddPetContext): Promise> { - // Apply defaults - const config = { - path: '/pet', - server: 'localhost:3000', - ...context, - }; +/** + * Handle OAuth2 token flows (client_credentials, password) + */ +async function handleOAuth2TokenFlow( + auth: OAuth2Auth, + originalParams: HttpRequestParams, + makeRequest: (params: HttpRequestParams) => Promise, + retryConfig?: RetryConfig +): Promise { + if (!auth.flow || !auth.tokenUrl) return null; - // Validate OAuth2 config if present - if (config.auth?.type === 'oauth2') { - validateOAuth2Config(config.auth); + const params = new URLSearchParams(); + + if (auth.flow === 'client_credentials') { + params.append('grant_type', 'client_credentials'); + params.append('client_id', auth.clientId!); + } else if (auth.flow === 'password') { + params.append('grant_type', 'password'); + params.append('username', auth.username || ''); + params.append('password', auth.password || ''); + params.append('client_id', auth.clientId!); + } else { + return null; } - // Build headers - let headers = context.requestHeaders - ? applyTypedHeaders(context.requestHeaders, config.additionalHeaders) - : { 'Content-Type': 'application/json', ...config.additionalHeaders } as Record; + if (auth.clientSecret) { + params.append('client_secret', auth.clientSecret); + } + if (auth.scopes && auth.scopes.length > 0) { + params.append('scope', auth.scopes.join(' ')); + } - // Build URL - let url = \`\${config.server}\${config.path}\`; - url = applyQueryParams(config.queryParams, url); + const authHeaders: Record = { + 'Content-Type': 'application/x-www-form-urlencoded' + }; - // Apply pagination (can affect URL and/or headers) - const paginationResult = applyPagination(config.pagination, url, headers); - url = paginationResult.url; - headers = paginationResult.headers; + // Use basic auth for client credentials if both client ID and secret are provided + if (auth.flow === 'client_credentials' && auth.clientId && auth.clientSecret) { + const credentials = Buffer.from(\`\${auth.clientId}:\${auth.clientSecret}\`).toString('base64'); + authHeaders['Authorization'] = \`Basic \${credentials}\`; + params.delete('client_id'); + params.delete('client_secret'); + } + + const tokenResponse = await NodeFetch.default(auth.tokenUrl, { + method: 'POST', + headers: authHeaders, + body: params.toString() + }); + + if (!tokenResponse.ok) { + throw new Error(\`OAuth2 token request failed: \${tokenResponse.statusText}\`); + } + + const tokenData = await tokenResponse.json(); + const tokens: TokenResponse = { + accessToken: tokenData.access_token, + refreshToken: tokenData.refresh_token, + expiresIn: tokenData.expires_in + }; + + // Notify the client about the tokens + if (auth.onTokenRefresh) { + auth.onTokenRefresh(tokens); + } + + // Retry the original request with the new token + const updatedHeaders = { ...originalParams.headers }; + updatedHeaders['Authorization'] = \`Bearer \${tokens.accessToken}\`; + + return executeWithRetry({ ...originalParams, headers: updatedHeaders }, makeRequest, retryConfig); +} + +/** + * Handle OAuth2 token refresh on 401 response + */ +async function handleTokenRefresh( + auth: OAuth2Auth, + originalParams: HttpRequestParams, + makeRequest: (params: HttpRequestParams) => Promise, + retryConfig?: RetryConfig +): Promise { + if (!auth.refreshToken || !auth.tokenUrl || !auth.clientId) return null; + + const refreshResponse = await NodeFetch.default(auth.tokenUrl, { + method: 'POST', + headers: { + 'Content-Type': 'application/x-www-form-urlencoded' + }, + body: new URLSearchParams({ + grant_type: 'refresh_token', + refresh_token: auth.refreshToken, + client_id: auth.clientId, + ...(auth.clientSecret ? { client_secret: auth.clientSecret } : {}) + }).toString() + }); + + if (!refreshResponse.ok) { + throw new Error('Unauthorized'); + } + + const tokenData = await refreshResponse.json(); + const newTokens: TokenResponse = { + accessToken: tokenData.access_token, + refreshToken: tokenData.refresh_token || auth.refreshToken, + expiresIn: tokenData.expires_in + }; + + // Notify the client about the refreshed tokens + if (auth.onTokenRefresh) { + auth.onTokenRefresh(newTokens); + } + + // Retry the original request with the new token + const updatedHeaders = { ...originalParams.headers }; + updatedHeaders['Authorization'] = \`Bearer \${newTokens.accessToken}\`; + + return executeWithRetry({ ...originalParams, headers: updatedHeaders }, makeRequest, retryConfig); +} +// ============================================================================ +// Generated HTTP Client Functions +// ============================================================================ + +export interface PostAddPetContext extends HttpClientContext { + payload: Pet; + requestHeaders?: { marshal: () => string }; +} + +async function postAddPet(context: PostAddPetContext): Promise> { + // Apply defaults + const config = { + path: '/pet', + server: 'localhost:3000', + ...context, + }; + + // Validate OAuth2 config if present + if (config.auth?.type === 'oauth2' && AUTH_FEATURES.oauth2) { + validateOAuth2Config(config.auth); + } + + // Build headers + let headers = context.requestHeaders + ? applyTypedHeaders(context.requestHeaders, config.additionalHeaders) + : { 'Content-Type': 'application/json', ...config.additionalHeaders } as Record; + + // Build URL + let url = \`\${config.server}\${config.path}\`; + url = applyQueryParams(config.queryParams, url); + + // Apply pagination (can affect URL and/or headers) + const paginationResult = applyPagination(config.pagination, url, headers); + url = paginationResult.url; + headers = paginationResult.headers; // Apply authentication const authResult = applyAuth(config.auth, headers, url); @@ -1062,7 +1064,7 @@ async function postAddPet(context: PostAddPetContext): Promise Promise, - retryConfig?: RetryConfig -): Promise { - if (!auth.flow || !auth.tokenUrl) return null; - - const params = new URLSearchParams(); - - if (auth.flow === 'client_credentials') { - params.append('grant_type', 'client_credentials'); - params.append('client_id', auth.clientId!); - } else if (auth.flow === 'password') { - params.append('grant_type', 'password'); - params.append('username', auth.username || ''); - params.append('password', auth.password || ''); - params.append('client_id', auth.clientId!); - } else { - return null; - } - - if (auth.clientSecret) { - params.append('client_secret', auth.clientSecret); - } - if (auth.scopes && auth.scopes.length > 0) { - params.append('scope', auth.scopes.join(' ')); - } - - const authHeaders: Record = { - 'Content-Type': 'application/x-www-form-urlencoded' - }; - - // Use basic auth for client credentials if both client ID and secret are provided - if (auth.flow === 'client_credentials' && auth.clientId && auth.clientSecret) { - const credentials = Buffer.from(\`\${auth.clientId}:\${auth.clientSecret}\`).toString('base64'); - authHeaders['Authorization'] = \`Basic \${credentials}\`; - params.delete('client_id'); - params.delete('client_secret'); - } - - const tokenResponse = await NodeFetch.default(auth.tokenUrl, { - method: 'POST', - headers: authHeaders, - body: params.toString() - }); - - if (!tokenResponse.ok) { - throw new Error(\`OAuth2 token request failed: \${tokenResponse.statusText}\`); - } - - const tokenData = await tokenResponse.json(); - const tokens: TokenResponse = { - accessToken: tokenData.access_token, - refreshToken: tokenData.refresh_token, - expiresIn: tokenData.expires_in - }; - - // Notify the client about the tokens - if (auth.onTokenRefresh) { - auth.onTokenRefresh(tokens); - } - - // Retry the original request with the new token - const updatedHeaders = { ...originalParams.headers }; - updatedHeaders['Authorization'] = \`Bearer \${tokens.accessToken}\`; - - return executeWithRetry({ ...originalParams, headers: updatedHeaders }, makeRequest, retryConfig); -} - -/** - * Handle OAuth2 token refresh on 401 response - */ -async function handleTokenRefresh( - auth: OAuth2Auth, - originalParams: HttpRequestParams, - makeRequest: (params: HttpRequestParams) => Promise, - retryConfig?: RetryConfig -): Promise { - if (!auth.refreshToken || !auth.tokenUrl || !auth.clientId) return null; - - const refreshResponse = await NodeFetch.default(auth.tokenUrl, { - method: 'POST', - headers: { - 'Content-Type': 'application/x-www-form-urlencoded' - }, - body: new URLSearchParams({ - grant_type: 'refresh_token', - refresh_token: auth.refreshToken, - client_id: auth.clientId, - ...(auth.clientSecret ? { client_secret: auth.clientSecret } : {}) - }).toString() - }); - - if (!refreshResponse.ok) { - throw new Error('Unauthorized'); - } - - const tokenData = await refreshResponse.json(); - const newTokens: TokenResponse = { - accessToken: tokenData.access_token, - refreshToken: tokenData.refresh_token || auth.refreshToken, - expiresIn: tokenData.expires_in - }; - - // Notify the client about the refreshed tokens - if (auth.onTokenRefresh) { - auth.onTokenRefresh(newTokens); - } - - // Retry the original request with the new token - const updatedHeaders = { ...originalParams.headers }; - updatedHeaders['Authorization'] = \`Bearer \${newTokens.accessToken}\`; - - return executeWithRetry({ ...originalParams, headers: updatedHeaders }, makeRequest, retryConfig); -} - /** * Handle HTTP error status codes with standardized messages */ @@ -2889,6 +2762,153 @@ function applyTypedHeaders( return headers; } +/** + * Validate OAuth2 configuration based on flow type + */ +function validateOAuth2Config(auth: OAuth2Auth): void { + // If using a flow, validate required fields + switch (auth.flow) { + case 'client_credentials': + if (!auth.tokenUrl) throw new Error('OAuth2 Client Credentials flow requires tokenUrl'); + if (!auth.clientId) throw new Error('OAuth2 Client Credentials flow requires clientId'); + break; + + case 'password': + if (!auth.tokenUrl) throw new Error('OAuth2 Password flow requires tokenUrl'); + if (!auth.clientId) throw new Error('OAuth2 Password flow requires clientId'); + if (!auth.username) throw new Error('OAuth2 Password flow requires username'); + if (!auth.password) throw new Error('OAuth2 Password flow requires password'); + break; + + default: + // No flow specified - must have accessToken for OAuth2 to work + if (!auth.accessToken && !auth.flow) { + // This is fine - token refresh can still work if refreshToken is provided + // Or the request will just be made without auth + } + break; + } +} + +/** + * Handle OAuth2 token flows (client_credentials, password) + */ +async function handleOAuth2TokenFlow( + auth: OAuth2Auth, + originalParams: HttpRequestParams, + makeRequest: (params: HttpRequestParams) => Promise, + retryConfig?: RetryConfig +): Promise { + if (!auth.flow || !auth.tokenUrl) return null; + + const params = new URLSearchParams(); + + if (auth.flow === 'client_credentials') { + params.append('grant_type', 'client_credentials'); + params.append('client_id', auth.clientId!); + } else if (auth.flow === 'password') { + params.append('grant_type', 'password'); + params.append('username', auth.username || ''); + params.append('password', auth.password || ''); + params.append('client_id', auth.clientId!); + } else { + return null; + } + + if (auth.clientSecret) { + params.append('client_secret', auth.clientSecret); + } + if (auth.scopes && auth.scopes.length > 0) { + params.append('scope', auth.scopes.join(' ')); + } + + const authHeaders: Record = { + 'Content-Type': 'application/x-www-form-urlencoded' + }; + + // Use basic auth for client credentials if both client ID and secret are provided + if (auth.flow === 'client_credentials' && auth.clientId && auth.clientSecret) { + const credentials = Buffer.from(\`\${auth.clientId}:\${auth.clientSecret}\`).toString('base64'); + authHeaders['Authorization'] = \`Basic \${credentials}\`; + params.delete('client_id'); + params.delete('client_secret'); + } + + const tokenResponse = await NodeFetch.default(auth.tokenUrl, { + method: 'POST', + headers: authHeaders, + body: params.toString() + }); + + if (!tokenResponse.ok) { + throw new Error(\`OAuth2 token request failed: \${tokenResponse.statusText}\`); + } + + const tokenData = await tokenResponse.json(); + const tokens: TokenResponse = { + accessToken: tokenData.access_token, + refreshToken: tokenData.refresh_token, + expiresIn: tokenData.expires_in + }; + + // Notify the client about the tokens + if (auth.onTokenRefresh) { + auth.onTokenRefresh(tokens); + } + + // Retry the original request with the new token + const updatedHeaders = { ...originalParams.headers }; + updatedHeaders['Authorization'] = \`Bearer \${tokens.accessToken}\`; + + return executeWithRetry({ ...originalParams, headers: updatedHeaders }, makeRequest, retryConfig); +} + +/** + * Handle OAuth2 token refresh on 401 response + */ +async function handleTokenRefresh( + auth: OAuth2Auth, + originalParams: HttpRequestParams, + makeRequest: (params: HttpRequestParams) => Promise, + retryConfig?: RetryConfig +): Promise { + if (!auth.refreshToken || !auth.tokenUrl || !auth.clientId) return null; + + const refreshResponse = await NodeFetch.default(auth.tokenUrl, { + method: 'POST', + headers: { + 'Content-Type': 'application/x-www-form-urlencoded' + }, + body: new URLSearchParams({ + grant_type: 'refresh_token', + refresh_token: auth.refreshToken, + client_id: auth.clientId, + ...(auth.clientSecret ? { client_secret: auth.clientSecret } : {}) + }).toString() + }); + + if (!refreshResponse.ok) { + throw new Error('Unauthorized'); + } + + const tokenData = await refreshResponse.json(); + const newTokens: TokenResponse = { + accessToken: tokenData.access_token, + refreshToken: tokenData.refresh_token || auth.refreshToken, + expiresIn: tokenData.expires_in + }; + + // Notify the client about the refreshed tokens + if (auth.onTokenRefresh) { + auth.onTokenRefresh(newTokens); + } + + // Retry the original request with the new token + const updatedHeaders = { ...originalParams.headers }; + updatedHeaders['Authorization'] = \`Bearer \${newTokens.accessToken}\`; + + return executeWithRetry({ ...originalParams, headers: updatedHeaders }, makeRequest, retryConfig); +} // ============================================================================ // Generated HTTP Client Functions // ============================================================================ @@ -2906,7 +2926,7 @@ async function getPingRequest(context: GetPingRequestContext = {}): Promise { + describe('renderHttpCommonTypes with security schemes', () => { + it('should generate only apiKey auth type when only apiKey scheme defined', () => { + const securitySchemes: SecuritySchemeOptions[] = [ + { + name: 'api_key', + type: 'apiKey', + apiKeyName: 'X-API-Key', + apiKeyIn: 'header' + } + ]; + + const result = renderHttpCommonTypes(securitySchemes); + + // Should include ApiKeyAuth interface + expect(result).toContain('export interface ApiKeyAuth'); + expect(result).toContain("type: 'apiKey'"); + + // Should include pre-populated values from the spec + expect(result).toContain('X-API-Key'); + expect(result).toContain('header'); + + // Should NOT include other auth types + expect(result).not.toContain('export interface BearerAuth'); + expect(result).not.toContain('export interface BasicAuth'); + expect(result).not.toContain('export interface OAuth2Auth'); + + // AuthConfig should only contain ApiKeyAuth + expect(result).toContain('export type AuthConfig = ApiKeyAuth'); + }); + + it('should generate only bearer auth type when http bearer scheme defined', () => { + const securitySchemes: SecuritySchemeOptions[] = [ + { + name: 'bearerAuth', + type: 'http', + httpScheme: 'bearer' + } + ]; + + const result = renderHttpCommonTypes(securitySchemes); + + expect(result).toContain('export interface BearerAuth'); + expect(result).toContain("type: 'bearer'"); + expect(result).not.toContain('export interface ApiKeyAuth'); + expect(result).not.toContain('export interface BasicAuth'); + expect(result).not.toContain('export interface OAuth2Auth'); + expect(result).toContain('export type AuthConfig = BearerAuth'); + }); + + it('should generate only basic auth type when http basic scheme defined', () => { + const securitySchemes: SecuritySchemeOptions[] = [ + { + name: 'basicAuth', + type: 'http', + httpScheme: 'basic' + } + ]; + + const result = renderHttpCommonTypes(securitySchemes); + + expect(result).toContain('export interface BasicAuth'); + expect(result).toContain("type: 'basic'"); + expect(result).not.toContain('export interface BearerAuth'); + expect(result).not.toContain('export interface ApiKeyAuth'); + expect(result).not.toContain('export interface OAuth2Auth'); + expect(result).toContain('export type AuthConfig = BasicAuth'); + }); + + it('should generate only oauth2 auth type when oauth2 scheme defined', () => { + const securitySchemes: SecuritySchemeOptions[] = [ + { + name: 'oauth2', + type: 'oauth2', + oauth2Flows: { + clientCredentials: { + tokenUrl: 'https://example.com/token', + scopes: {read: 'read access'} + } + } + } + ]; + + const result = renderHttpCommonTypes(securitySchemes); + + expect(result).toContain('export interface OAuth2Auth'); + expect(result).toContain("type: 'oauth2'"); + + // Should include pre-populated tokenUrl from the spec + expect(result).toContain('https://example.com/token'); + + expect(result).not.toContain('export interface BearerAuth'); + expect(result).not.toContain('export interface BasicAuth'); + expect(result).not.toContain('export interface ApiKeyAuth'); + expect(result).toContain('export type AuthConfig = OAuth2Auth'); + }); + + it('should generate multiple auth types when multiple schemes defined', () => { + const securitySchemes: SecuritySchemeOptions[] = [ + { + name: 'api_key', + type: 'apiKey', + apiKeyName: 'api_key', + apiKeyIn: 'header' + }, + { + name: 'bearerAuth', + type: 'http', + httpScheme: 'bearer' + } + ]; + + const result = renderHttpCommonTypes(securitySchemes); + + expect(result).toContain('export interface ApiKeyAuth'); + expect(result).toContain('export interface BearerAuth'); + expect(result).not.toContain('export interface BasicAuth'); + expect(result).not.toContain('export interface OAuth2Auth'); + + // AuthConfig should be a union of the defined types + expect(result).toMatch(/export type AuthConfig = (?:ApiKeyAuth \| BearerAuth|BearerAuth \| ApiKeyAuth)/); + }); + + it('should generate all auth types when no security schemes defined (backward compatibility)', () => { + const result = renderHttpCommonTypes(); // No argument - backward compatible + + expect(result).toContain('export interface BearerAuth'); + expect(result).toContain('export interface BasicAuth'); + expect(result).toContain('export interface ApiKeyAuth'); + expect(result).toContain('export interface OAuth2Auth'); + expect(result).toContain( + 'export type AuthConfig = BearerAuth | BasicAuth | ApiKeyAuth | OAuth2Auth' + ); + }); + + it('should generate all auth types when empty security schemes array provided', () => { + const result = renderHttpCommonTypes([]); + + expect(result).toContain('export interface BearerAuth'); + expect(result).toContain('export interface BasicAuth'); + expect(result).toContain('export interface ApiKeyAuth'); + expect(result).toContain('export interface OAuth2Auth'); + }); + + it('should generate auth interface with pre-populated apiKey name and location', () => { + const securitySchemes: SecuritySchemeOptions[] = [ + { + name: 'petstore_api_key', + type: 'apiKey', + apiKeyName: 'X-Petstore-Key', + apiKeyIn: 'query' + } + ]; + + const result = renderHttpCommonTypes(securitySchemes); + + // The generated interface should have name and in pre-set + expect(result).toContain('X-Petstore-Key'); + expect(result).toContain('query'); + }); + + it('should generate OAuth2 auth with tokenUrl from implicit flow', () => { + const securitySchemes: SecuritySchemeOptions[] = [ + { + name: 'oauth2', + type: 'oauth2', + oauth2Flows: { + implicit: { + authorizationUrl: 'https://example.com/authorize', + scopes: {} + } + } + } + ]; + + const result = renderHttpCommonTypes(securitySchemes); + + expect(result).toContain('export interface OAuth2Auth'); + expect(result).toContain('https://example.com/authorize'); + }); + + it('should handle openIdConnect type', () => { + const securitySchemes: SecuritySchemeOptions[] = [ + { + name: 'oidc', + type: 'openIdConnect', + openIdConnectUrl: 'https://example.com/.well-known/openid-configuration' + } + ]; + + const result = renderHttpCommonTypes(securitySchemes); + + // OpenID Connect should be treated similar to OAuth2 + expect(result).toContain('export interface OAuth2Auth'); + expect(result).toContain('https://example.com/.well-known/openid-configuration'); + }); + + it('should deduplicate auth types when same type defined multiple times', () => { + const securitySchemes: SecuritySchemeOptions[] = [ + { + name: 'api_key_header', + type: 'apiKey', + apiKeyName: 'X-API-Key', + apiKeyIn: 'header' + }, + { + name: 'api_key_query', + type: 'apiKey', + apiKeyName: 'api_key', + apiKeyIn: 'query' + } + ]; + + const result = renderHttpCommonTypes(securitySchemes); + + // Should only have one ApiKeyAuth interface + const apiKeyInterfaceMatches = result.match(/export interface ApiKeyAuth/g); + expect(apiKeyInterfaceMatches).toHaveLength(1); + + // AuthConfig should still only have ApiKeyAuth once + expect(result).toContain('export type AuthConfig = ApiKeyAuth'); + expect(result).not.toMatch(/ApiKeyAuth \| ApiKeyAuth/); + }); + }); +}); diff --git a/test/codegen/inputs/openapi/security.spec.ts b/test/codegen/inputs/openapi/security.spec.ts new file mode 100644 index 00000000..de3515a7 --- /dev/null +++ b/test/codegen/inputs/openapi/security.spec.ts @@ -0,0 +1,546 @@ +import {OpenAPIV3, OpenAPIV2} from 'openapi-types'; +import { + extractSecuritySchemes, + SecuritySchemeOptions +} from '../../../../src/codegen/inputs/openapi/security'; + +describe('OpenAPI Security Extraction', () => { + describe('extractSecuritySchemes', () => { + describe('OpenAPI 3.x', () => { + it('should extract apiKey security scheme from header', () => { + const document: OpenAPIV3.Document = { + openapi: '3.0.0', + info: {title: 'Test API', version: '1.0.0'}, + paths: {}, + components: { + securitySchemes: { + api_key: { + type: 'apiKey', + name: 'X-API-Key', + in: 'header' + } + } + } + }; + + const schemes = extractSecuritySchemes(document); + + expect(schemes).toHaveLength(1); + expect(schemes[0]).toEqual({ + name: 'api_key', + type: 'apiKey', + apiKeyName: 'X-API-Key', + apiKeyIn: 'header' + }); + }); + + it('should extract apiKey security scheme from query', () => { + const document: OpenAPIV3.Document = { + openapi: '3.0.0', + info: {title: 'Test API', version: '1.0.0'}, + paths: {}, + components: { + securitySchemes: { + api_key: { + type: 'apiKey', + name: 'api_key', + in: 'query' + } + } + } + }; + + const schemes = extractSecuritySchemes(document); + + expect(schemes).toHaveLength(1); + expect(schemes[0]).toEqual({ + name: 'api_key', + type: 'apiKey', + apiKeyName: 'api_key', + apiKeyIn: 'query' + }); + }); + + it('should extract apiKey security scheme from cookie', () => { + const document: OpenAPIV3.Document = { + openapi: '3.0.0', + info: {title: 'Test API', version: '1.0.0'}, + paths: {}, + components: { + securitySchemes: { + session: { + type: 'apiKey', + name: 'session_id', + in: 'cookie' + } + } + } + }; + + const schemes = extractSecuritySchemes(document); + + expect(schemes).toHaveLength(1); + expect(schemes[0]).toEqual({ + name: 'session', + type: 'apiKey', + apiKeyName: 'session_id', + apiKeyIn: 'cookie' + }); + }); + + it('should extract http bearer security scheme', () => { + const document: OpenAPIV3.Document = { + openapi: '3.0.0', + info: {title: 'Test API', version: '1.0.0'}, + paths: {}, + components: { + securitySchemes: { + bearerAuth: { + type: 'http', + scheme: 'bearer', + bearerFormat: 'JWT' + } + } + } + }; + + const schemes = extractSecuritySchemes(document); + + expect(schemes).toHaveLength(1); + expect(schemes[0]).toEqual({ + name: 'bearerAuth', + type: 'http', + httpScheme: 'bearer', + bearerFormat: 'JWT' + }); + }); + + it('should extract http basic security scheme', () => { + const document: OpenAPIV3.Document = { + openapi: '3.0.0', + info: {title: 'Test API', version: '1.0.0'}, + paths: {}, + components: { + securitySchemes: { + basicAuth: { + type: 'http', + scheme: 'basic' + } + } + } + }; + + const schemes = extractSecuritySchemes(document); + + expect(schemes).toHaveLength(1); + expect(schemes[0]).toEqual({ + name: 'basicAuth', + type: 'http', + httpScheme: 'basic' + }); + }); + + it('should extract oauth2 implicit flow', () => { + const document: OpenAPIV3.Document = { + openapi: '3.0.0', + info: {title: 'Test API', version: '1.0.0'}, + paths: {}, + components: { + securitySchemes: { + oauth2: { + type: 'oauth2', + flows: { + implicit: { + authorizationUrl: 'https://example.com/oauth/authorize', + scopes: { + 'read:pets': 'read your pets', + 'write:pets': 'modify pets in your account' + } + } + } + } + } + } + }; + + const schemes = extractSecuritySchemes(document); + + expect(schemes).toHaveLength(1); + expect(schemes[0]).toEqual({ + name: 'oauth2', + type: 'oauth2', + oauth2Flows: { + implicit: { + authorizationUrl: 'https://example.com/oauth/authorize', + scopes: { + 'read:pets': 'read your pets', + 'write:pets': 'modify pets in your account' + } + } + } + }); + }); + + it('should extract oauth2 authorization code flow', () => { + const document: OpenAPIV3.Document = { + openapi: '3.0.0', + info: {title: 'Test API', version: '1.0.0'}, + paths: {}, + components: { + securitySchemes: { + oauth2: { + type: 'oauth2', + flows: { + authorizationCode: { + authorizationUrl: 'https://example.com/oauth/authorize', + tokenUrl: 'https://example.com/oauth/token', + scopes: { + 'read:users': 'read user data' + } + } + } + } + } + } + }; + + const schemes = extractSecuritySchemes(document); + + expect(schemes).toHaveLength(1); + expect(schemes[0].oauth2Flows?.authorizationCode).toEqual({ + authorizationUrl: 'https://example.com/oauth/authorize', + tokenUrl: 'https://example.com/oauth/token', + scopes: { + 'read:users': 'read user data' + } + }); + }); + + it('should extract oauth2 client credentials flow', () => { + const document: OpenAPIV3.Document = { + openapi: '3.0.0', + info: {title: 'Test API', version: '1.0.0'}, + paths: {}, + components: { + securitySchemes: { + clientCredentials: { + type: 'oauth2', + flows: { + clientCredentials: { + tokenUrl: 'https://example.com/oauth/token', + scopes: { + admin: 'admin access' + } + } + } + } + } + } + }; + + const schemes = extractSecuritySchemes(document); + + expect(schemes).toHaveLength(1); + expect(schemes[0].oauth2Flows?.clientCredentials).toEqual({ + tokenUrl: 'https://example.com/oauth/token', + scopes: { + admin: 'admin access' + } + }); + }); + + it('should extract oauth2 password flow', () => { + const document: OpenAPIV3.Document = { + openapi: '3.0.0', + info: {title: 'Test API', version: '1.0.0'}, + paths: {}, + components: { + securitySchemes: { + passwordAuth: { + type: 'oauth2', + flows: { + password: { + tokenUrl: 'https://example.com/oauth/token', + scopes: {} + } + } + } + } + } + }; + + const schemes = extractSecuritySchemes(document); + + expect(schemes).toHaveLength(1); + expect(schemes[0].oauth2Flows?.password).toEqual({ + tokenUrl: 'https://example.com/oauth/token', + scopes: {} + }); + }); + + it('should extract openIdConnect security scheme', () => { + const document: OpenAPIV3.Document = { + openapi: '3.0.0', + info: {title: 'Test API', version: '1.0.0'}, + paths: {}, + components: { + securitySchemes: { + oidc: { + type: 'openIdConnect', + openIdConnectUrl: + 'https://example.com/.well-known/openid-configuration' + } + } + } + }; + + const schemes = extractSecuritySchemes(document); + + expect(schemes).toHaveLength(1); + expect(schemes[0]).toEqual({ + name: 'oidc', + type: 'openIdConnect', + openIdConnectUrl: + 'https://example.com/.well-known/openid-configuration' + }); + }); + + it('should extract multiple security schemes', () => { + const document: OpenAPIV3.Document = { + openapi: '3.0.0', + info: {title: 'Test API', version: '1.0.0'}, + paths: {}, + components: { + securitySchemes: { + api_key: { + type: 'apiKey', + name: 'X-API-Key', + in: 'header' + }, + bearerAuth: { + type: 'http', + scheme: 'bearer' + }, + oauth2: { + type: 'oauth2', + flows: { + implicit: { + authorizationUrl: 'https://example.com/oauth/authorize', + scopes: {} + } + } + } + } + } + }; + + const schemes = extractSecuritySchemes(document); + + expect(schemes).toHaveLength(3); + expect(schemes.map((s) => s.name)).toContain('api_key'); + expect(schemes.map((s) => s.name)).toContain('bearerAuth'); + expect(schemes.map((s) => s.name)).toContain('oauth2'); + }); + + it('should return empty array when no security schemes defined', () => { + const document: OpenAPIV3.Document = { + openapi: '3.0.0', + info: {title: 'Test API', version: '1.0.0'}, + paths: {} + }; + + const schemes = extractSecuritySchemes(document); + + expect(schemes).toEqual([]); + }); + + it('should return empty array when components is empty', () => { + const document: OpenAPIV3.Document = { + openapi: '3.0.0', + info: {title: 'Test API', version: '1.0.0'}, + paths: {}, + components: {} + }; + + const schemes = extractSecuritySchemes(document); + + expect(schemes).toEqual([]); + }); + }); + + describe('Swagger 2.0 (OpenAPI 2.0)', () => { + it('should extract apiKey security definition from header', () => { + const document: OpenAPIV2.Document = { + swagger: '2.0', + info: {title: 'Test API', version: '1.0.0'}, + paths: {}, + securityDefinitions: { + api_key: { + type: 'apiKey', + name: 'api_key', + in: 'header' + } + } + }; + + const schemes = extractSecuritySchemes(document); + + expect(schemes).toHaveLength(1); + expect(schemes[0]).toEqual({ + name: 'api_key', + type: 'apiKey', + apiKeyName: 'api_key', + apiKeyIn: 'header' + }); + }); + + it('should extract basic security definition', () => { + const document: OpenAPIV2.Document = { + swagger: '2.0', + info: {title: 'Test API', version: '1.0.0'}, + paths: {}, + securityDefinitions: { + basicAuth: { + type: 'basic' + } + } + }; + + const schemes = extractSecuritySchemes(document); + + expect(schemes).toHaveLength(1); + expect(schemes[0]).toEqual({ + name: 'basicAuth', + type: 'http', + httpScheme: 'basic' + }); + }); + + it('should extract oauth2 implicit flow from Swagger 2.0', () => { + const document: OpenAPIV2.Document = { + swagger: '2.0', + info: {title: 'Test API', version: '1.0.0'}, + paths: {}, + securityDefinitions: { + petstore_auth: { + type: 'oauth2', + flow: 'implicit', + authorizationUrl: 'https://petstore.swagger.io/oauth/authorize', + scopes: { + 'write:pets': 'modify pets', + 'read:pets': 'read pets' + } + } + } + }; + + const schemes = extractSecuritySchemes(document); + + expect(schemes).toHaveLength(1); + expect(schemes[0]).toEqual({ + name: 'petstore_auth', + type: 'oauth2', + oauth2Flows: { + implicit: { + authorizationUrl: 'https://petstore.swagger.io/oauth/authorize', + scopes: { + 'write:pets': 'modify pets', + 'read:pets': 'read pets' + } + } + } + }); + }); + + it('should extract oauth2 password flow from Swagger 2.0', () => { + const document: OpenAPIV2.Document = { + swagger: '2.0', + info: {title: 'Test API', version: '1.0.0'}, + paths: {}, + securityDefinitions: { + password_auth: { + type: 'oauth2', + flow: 'password', + tokenUrl: 'https://example.com/oauth/token', + scopes: {} + } + } + }; + + const schemes = extractSecuritySchemes(document); + + expect(schemes).toHaveLength(1); + expect(schemes[0].oauth2Flows?.password).toEqual({ + tokenUrl: 'https://example.com/oauth/token', + scopes: {} + }); + }); + + it('should extract oauth2 application (client_credentials) flow from Swagger 2.0', () => { + const document: OpenAPIV2.Document = { + swagger: '2.0', + info: {title: 'Test API', version: '1.0.0'}, + paths: {}, + securityDefinitions: { + app_auth: { + type: 'oauth2', + flow: 'application', + tokenUrl: 'https://example.com/oauth/token', + scopes: { + admin: 'admin access' + } + } + } + }; + + const schemes = extractSecuritySchemes(document); + + expect(schemes).toHaveLength(1); + expect(schemes[0].oauth2Flows?.clientCredentials).toEqual({ + tokenUrl: 'https://example.com/oauth/token', + scopes: { + admin: 'admin access' + } + }); + }); + + it('should extract oauth2 accessCode (authorization_code) flow from Swagger 2.0', () => { + const document: OpenAPIV2.Document = { + swagger: '2.0', + info: {title: 'Test API', version: '1.0.0'}, + paths: {}, + securityDefinitions: { + code_auth: { + type: 'oauth2', + flow: 'accessCode', + authorizationUrl: 'https://example.com/oauth/authorize', + tokenUrl: 'https://example.com/oauth/token', + scopes: {} + } + } + }; + + const schemes = extractSecuritySchemes(document); + + expect(schemes).toHaveLength(1); + expect(schemes[0].oauth2Flows?.authorizationCode).toEqual({ + authorizationUrl: 'https://example.com/oauth/authorize', + tokenUrl: 'https://example.com/oauth/token', + scopes: {} + }); + }); + + it('should return empty array when no securityDefinitions in Swagger 2.0', () => { + const document: OpenAPIV2.Document = { + swagger: '2.0', + info: {title: 'Test API', version: '1.0.0'}, + paths: {} + }; + + const schemes = extractSecuritySchemes(document); + + expect(schemes).toEqual([]); + }); + }); + }); +}); diff --git a/test/runtime/typescript/src/openapi/channels/http_client.ts b/test/runtime/typescript/src/openapi/channels/http_client.ts index a552de90..06f1f8a9 100644 --- a/test/runtime/typescript/src/openapi/channels/http_client.ts +++ b/test/runtime/typescript/src/openapi/channels/http_client.ts @@ -97,30 +97,13 @@ export interface TokenResponse { // Security Configuration Types - Grouped for better autocomplete // ============================================================================ -/** - * Bearer token authentication configuration - */ -export interface BearerAuth { - type: 'bearer'; - token: string; -} - -/** - * Basic authentication configuration (username/password) - */ -export interface BasicAuth { - type: 'basic'; - username: string; - password: string; -} - /** * API key authentication configuration */ export interface ApiKeyAuth { type: 'apiKey'; key: string; - name?: string; // Name of the API key parameter (default: 'X-API-Key') + name?: string; // Name of the API key parameter (default: 'api_key') in?: 'header' | 'query'; // Where to place the API key (default: 'header') } @@ -134,6 +117,7 @@ export interface ApiKeyAuth { * * For browser-based flows (implicit, authorization_code), obtain the token * separately and pass it as accessToken. + * Authorization URL: 'http://petstore.swagger.io/api/oauth/dialog' */ export interface OAuth2Auth { type: 'oauth2'; @@ -147,7 +131,7 @@ export interface OAuth2Auth { clientId?: string; /** Client secret (optional, depends on OAuth provider) */ clientSecret?: string; - /** Requested scopes */ + /** Requested scopes Available: write:pets, read:pets */ scopes?: string[]; /** Server-side flow type */ flow?: 'password' | 'client_credentials'; @@ -162,7 +146,24 @@ export interface OAuth2Auth { /** * Union type for all authentication methods - provides autocomplete support */ -export type AuthConfig = BearerAuth | BasicAuth | ApiKeyAuth | OAuth2Auth; +export type AuthConfig = ApiKeyAuth | OAuth2Auth; + +/** + * Feature flags indicating which auth types are available. + * Used internally to conditionally call auth-specific helpers. + */ +const AUTH_FEATURES = { + oauth2: true +} as const; + +/** + * Default values for API key authentication derived from the spec. + * These match the defaults documented in the ApiKeyAuth interface. + */ +const API_KEY_DEFAULTS = { + name: 'api_key', + in: 'header' as 'header' | 'query' | 'cookie' +} as const; // ============================================================================ // Pagination Types @@ -356,14 +357,16 @@ function applyAuth( } case 'apiKey': { - const keyName = auth.name ?? 'X-API-Key'; - const keyIn = auth.in ?? 'header'; + const keyName = auth.name ?? API_KEY_DEFAULTS.name; + const keyIn = auth.in ?? API_KEY_DEFAULTS.in; if (keyIn === 'header') { headers[keyName] = auth.key; - } else { + } else if (keyIn === 'query') { const separator = url.includes('?') ? '&' : '?'; url = `${url}${separator}${keyName}=${encodeURIComponent(auth.key)}`; + } else if (keyIn === 'cookie') { + headers['Cookie'] = `${keyName}=${auth.key}`; } break; } @@ -381,34 +384,6 @@ function applyAuth( return { headers, url }; } -/** - * Validate OAuth2 configuration based on flow type - */ -function validateOAuth2Config(auth: OAuth2Auth): void { - // If using a flow, validate required fields - switch (auth.flow) { - case 'client_credentials': - if (!auth.tokenUrl) throw new Error('OAuth2 Client Credentials flow requires tokenUrl'); - if (!auth.clientId) throw new Error('OAuth2 Client Credentials flow requires clientId'); - break; - - case 'password': - if (!auth.tokenUrl) throw new Error('OAuth2 Password flow requires tokenUrl'); - if (!auth.clientId) throw new Error('OAuth2 Password flow requires clientId'); - if (!auth.username) throw new Error('OAuth2 Password flow requires username'); - if (!auth.password) throw new Error('OAuth2 Password flow requires password'); - break; - - default: - // No flow specified - must have accessToken for OAuth2 to work - if (!auth.accessToken && !auth.flow) { - // This is fine - token refresh can still work if refreshToken is provided - // Or the request will just be made without auth - } - break; - } -} - /** * Apply pagination parameters to URL and/or headers based on configuration */ @@ -598,126 +573,6 @@ async function executeWithRetry( throw lastError ?? new Error('Request failed after retries'); } -/** - * Handle OAuth2 token flows (client_credentials, password) - */ -async function handleOAuth2TokenFlow( - auth: OAuth2Auth, - originalParams: HttpRequestParams, - makeRequest: (params: HttpRequestParams) => Promise, - retryConfig?: RetryConfig -): Promise { - if (!auth.flow || !auth.tokenUrl) return null; - - const params = new URLSearchParams(); - - if (auth.flow === 'client_credentials') { - params.append('grant_type', 'client_credentials'); - params.append('client_id', auth.clientId!); - } else if (auth.flow === 'password') { - params.append('grant_type', 'password'); - params.append('username', auth.username || ''); - params.append('password', auth.password || ''); - params.append('client_id', auth.clientId!); - } else { - return null; - } - - if (auth.clientSecret) { - params.append('client_secret', auth.clientSecret); - } - if (auth.scopes && auth.scopes.length > 0) { - params.append('scope', auth.scopes.join(' ')); - } - - const authHeaders: Record = { - 'Content-Type': 'application/x-www-form-urlencoded' - }; - - // Use basic auth for client credentials if both client ID and secret are provided - if (auth.flow === 'client_credentials' && auth.clientId && auth.clientSecret) { - const credentials = Buffer.from(`${auth.clientId}:${auth.clientSecret}`).toString('base64'); - authHeaders['Authorization'] = `Basic ${credentials}`; - params.delete('client_id'); - params.delete('client_secret'); - } - - const tokenResponse = await NodeFetch.default(auth.tokenUrl, { - method: 'POST', - headers: authHeaders, - body: params.toString() - }); - - if (!tokenResponse.ok) { - throw new Error(`OAuth2 token request failed: ${tokenResponse.statusText}`); - } - - const tokenData = await tokenResponse.json(); - const tokens: TokenResponse = { - accessToken: tokenData.access_token, - refreshToken: tokenData.refresh_token, - expiresIn: tokenData.expires_in - }; - - // Notify the client about the tokens - if (auth.onTokenRefresh) { - auth.onTokenRefresh(tokens); - } - - // Retry the original request with the new token - const updatedHeaders = { ...originalParams.headers }; - updatedHeaders['Authorization'] = `Bearer ${tokens.accessToken}`; - - return executeWithRetry({ ...originalParams, headers: updatedHeaders }, makeRequest, retryConfig); -} - -/** - * Handle OAuth2 token refresh on 401 response - */ -async function handleTokenRefresh( - auth: OAuth2Auth, - originalParams: HttpRequestParams, - makeRequest: (params: HttpRequestParams) => Promise, - retryConfig?: RetryConfig -): Promise { - if (!auth.refreshToken || !auth.tokenUrl || !auth.clientId) return null; - - const refreshResponse = await NodeFetch.default(auth.tokenUrl, { - method: 'POST', - headers: { - 'Content-Type': 'application/x-www-form-urlencoded' - }, - body: new URLSearchParams({ - grant_type: 'refresh_token', - refresh_token: auth.refreshToken, - client_id: auth.clientId, - ...(auth.clientSecret ? { client_secret: auth.clientSecret } : {}) - }).toString() - }); - - if (!refreshResponse.ok) { - throw new Error('Unauthorized'); - } - - const tokenData = await refreshResponse.json(); - const newTokens: TokenResponse = { - accessToken: tokenData.access_token, - refreshToken: tokenData.refresh_token || auth.refreshToken, - expiresIn: tokenData.expires_in - }; - - // Notify the client about the refreshed tokens - if (auth.onTokenRefresh) { - auth.onTokenRefresh(newTokens); - } - - // Retry the original request with the new token - const updatedHeaders = { ...originalParams.headers }; - updatedHeaders['Authorization'] = `Bearer ${newTokens.accessToken}`; - - return executeWithRetry({ ...originalParams, headers: updatedHeaders }, makeRequest, retryConfig); -} - /** * Handle HTTP error status codes with standardized messages */ @@ -990,6 +845,153 @@ function applyTypedHeaders( return headers; } +/** + * Validate OAuth2 configuration based on flow type + */ +function validateOAuth2Config(auth: OAuth2Auth): void { + // If using a flow, validate required fields + switch (auth.flow) { + case 'client_credentials': + if (!auth.tokenUrl) throw new Error('OAuth2 Client Credentials flow requires tokenUrl'); + if (!auth.clientId) throw new Error('OAuth2 Client Credentials flow requires clientId'); + break; + + case 'password': + if (!auth.tokenUrl) throw new Error('OAuth2 Password flow requires tokenUrl'); + if (!auth.clientId) throw new Error('OAuth2 Password flow requires clientId'); + if (!auth.username) throw new Error('OAuth2 Password flow requires username'); + if (!auth.password) throw new Error('OAuth2 Password flow requires password'); + break; + + default: + // No flow specified - must have accessToken for OAuth2 to work + if (!auth.accessToken && !auth.flow) { + // This is fine - token refresh can still work if refreshToken is provided + // Or the request will just be made without auth + } + break; + } +} + +/** + * Handle OAuth2 token flows (client_credentials, password) + */ +async function handleOAuth2TokenFlow( + auth: OAuth2Auth, + originalParams: HttpRequestParams, + makeRequest: (params: HttpRequestParams) => Promise, + retryConfig?: RetryConfig +): Promise { + if (!auth.flow || !auth.tokenUrl) return null; + + const params = new URLSearchParams(); + + if (auth.flow === 'client_credentials') { + params.append('grant_type', 'client_credentials'); + params.append('client_id', auth.clientId!); + } else if (auth.flow === 'password') { + params.append('grant_type', 'password'); + params.append('username', auth.username || ''); + params.append('password', auth.password || ''); + params.append('client_id', auth.clientId!); + } else { + return null; + } + + if (auth.clientSecret) { + params.append('client_secret', auth.clientSecret); + } + if (auth.scopes && auth.scopes.length > 0) { + params.append('scope', auth.scopes.join(' ')); + } + + const authHeaders: Record = { + 'Content-Type': 'application/x-www-form-urlencoded' + }; + + // Use basic auth for client credentials if both client ID and secret are provided + if (auth.flow === 'client_credentials' && auth.clientId && auth.clientSecret) { + const credentials = Buffer.from(`${auth.clientId}:${auth.clientSecret}`).toString('base64'); + authHeaders['Authorization'] = `Basic ${credentials}`; + params.delete('client_id'); + params.delete('client_secret'); + } + + const tokenResponse = await NodeFetch.default(auth.tokenUrl, { + method: 'POST', + headers: authHeaders, + body: params.toString() + }); + + if (!tokenResponse.ok) { + throw new Error(`OAuth2 token request failed: ${tokenResponse.statusText}`); + } + + const tokenData = await tokenResponse.json(); + const tokens: TokenResponse = { + accessToken: tokenData.access_token, + refreshToken: tokenData.refresh_token, + expiresIn: tokenData.expires_in + }; + + // Notify the client about the tokens + if (auth.onTokenRefresh) { + auth.onTokenRefresh(tokens); + } + + // Retry the original request with the new token + const updatedHeaders = { ...originalParams.headers }; + updatedHeaders['Authorization'] = `Bearer ${tokens.accessToken}`; + + return executeWithRetry({ ...originalParams, headers: updatedHeaders }, makeRequest, retryConfig); +} + +/** + * Handle OAuth2 token refresh on 401 response + */ +async function handleTokenRefresh( + auth: OAuth2Auth, + originalParams: HttpRequestParams, + makeRequest: (params: HttpRequestParams) => Promise, + retryConfig?: RetryConfig +): Promise { + if (!auth.refreshToken || !auth.tokenUrl || !auth.clientId) return null; + + const refreshResponse = await NodeFetch.default(auth.tokenUrl, { + method: 'POST', + headers: { + 'Content-Type': 'application/x-www-form-urlencoded' + }, + body: new URLSearchParams({ + grant_type: 'refresh_token', + refresh_token: auth.refreshToken, + client_id: auth.clientId, + ...(auth.clientSecret ? { client_secret: auth.clientSecret } : {}) + }).toString() + }); + + if (!refreshResponse.ok) { + throw new Error('Unauthorized'); + } + + const tokenData = await refreshResponse.json(); + const newTokens: TokenResponse = { + accessToken: tokenData.access_token, + refreshToken: tokenData.refresh_token || auth.refreshToken, + expiresIn: tokenData.expires_in + }; + + // Notify the client about the refreshed tokens + if (auth.onTokenRefresh) { + auth.onTokenRefresh(newTokens); + } + + // Retry the original request with the new token + const updatedHeaders = { ...originalParams.headers }; + updatedHeaders['Authorization'] = `Bearer ${newTokens.accessToken}`; + + return executeWithRetry({ ...originalParams, headers: updatedHeaders }, makeRequest, retryConfig); +} // ============================================================================ // Generated HTTP Client Functions // ============================================================================ @@ -1008,7 +1010,7 @@ async function postAddPet(context: PostAddPetContext): Promise Promise, - retryConfig?: RetryConfig -): Promise { - if (!auth.flow || !auth.tokenUrl) return null; - - const params = new URLSearchParams(); - - if (auth.flow === 'client_credentials') { - params.append('grant_type', 'client_credentials'); - params.append('client_id', auth.clientId!); - } else if (auth.flow === 'password') { - params.append('grant_type', 'password'); - params.append('username', auth.username || ''); - params.append('password', auth.password || ''); - params.append('client_id', auth.clientId!); - } else { - return null; - } - - if (auth.clientSecret) { - params.append('client_secret', auth.clientSecret); - } - if (auth.scopes && auth.scopes.length > 0) { - params.append('scope', auth.scopes.join(' ')); - } - - const authHeaders: Record = { - 'Content-Type': 'application/x-www-form-urlencoded' - }; - - // Use basic auth for client credentials if both client ID and secret are provided - if (auth.flow === 'client_credentials' && auth.clientId && auth.clientSecret) { - const credentials = Buffer.from(`${auth.clientId}:${auth.clientSecret}`).toString('base64'); - authHeaders['Authorization'] = `Basic ${credentials}`; - params.delete('client_id'); - params.delete('client_secret'); - } - - const tokenResponse = await NodeFetch.default(auth.tokenUrl, { - method: 'POST', - headers: authHeaders, - body: params.toString() - }); - - if (!tokenResponse.ok) { - throw new Error(`OAuth2 token request failed: ${tokenResponse.statusText}`); - } - - const tokenData = await tokenResponse.json(); - const tokens: TokenResponse = { - accessToken: tokenData.access_token, - refreshToken: tokenData.refresh_token, - expiresIn: tokenData.expires_in - }; - - // Notify the client about the tokens - if (auth.onTokenRefresh) { - auth.onTokenRefresh(tokens); - } - - // Retry the original request with the new token - const updatedHeaders = { ...originalParams.headers }; - updatedHeaders['Authorization'] = `Bearer ${tokens.accessToken}`; - - return executeWithRetry({ ...originalParams, headers: updatedHeaders }, makeRequest, retryConfig); -} - -/** - * Handle OAuth2 token refresh on 401 response - */ -async function handleTokenRefresh( - auth: OAuth2Auth, - originalParams: HttpRequestParams, - makeRequest: (params: HttpRequestParams) => Promise, - retryConfig?: RetryConfig -): Promise { - if (!auth.refreshToken || !auth.tokenUrl || !auth.clientId) return null; - - const refreshResponse = await NodeFetch.default(auth.tokenUrl, { - method: 'POST', - headers: { - 'Content-Type': 'application/x-www-form-urlencoded' - }, - body: new URLSearchParams({ - grant_type: 'refresh_token', - refresh_token: auth.refreshToken, - client_id: auth.clientId, - ...(auth.clientSecret ? { client_secret: auth.clientSecret } : {}) - }).toString() - }); - - if (!refreshResponse.ok) { - throw new Error('Unauthorized'); - } - - const tokenData = await refreshResponse.json(); - const newTokens: TokenResponse = { - accessToken: tokenData.access_token, - refreshToken: tokenData.refresh_token || auth.refreshToken, - expiresIn: tokenData.expires_in - }; - - // Notify the client about the refreshed tokens - if (auth.onTokenRefresh) { - auth.onTokenRefresh(newTokens); - } - - // Retry the original request with the new token - const updatedHeaders = { ...originalParams.headers }; - updatedHeaders['Authorization'] = `Bearer ${newTokens.accessToken}`; - - return executeWithRetry({ ...originalParams, headers: updatedHeaders }, makeRequest, retryConfig); -} - /** * Handle HTTP error status codes with standardized messages */ @@ -991,6 +862,153 @@ function applyTypedHeaders( return headers; } +/** + * Validate OAuth2 configuration based on flow type + */ +function validateOAuth2Config(auth: OAuth2Auth): void { + // If using a flow, validate required fields + switch (auth.flow) { + case 'client_credentials': + if (!auth.tokenUrl) throw new Error('OAuth2 Client Credentials flow requires tokenUrl'); + if (!auth.clientId) throw new Error('OAuth2 Client Credentials flow requires clientId'); + break; + + case 'password': + if (!auth.tokenUrl) throw new Error('OAuth2 Password flow requires tokenUrl'); + if (!auth.clientId) throw new Error('OAuth2 Password flow requires clientId'); + if (!auth.username) throw new Error('OAuth2 Password flow requires username'); + if (!auth.password) throw new Error('OAuth2 Password flow requires password'); + break; + + default: + // No flow specified - must have accessToken for OAuth2 to work + if (!auth.accessToken && !auth.flow) { + // This is fine - token refresh can still work if refreshToken is provided + // Or the request will just be made without auth + } + break; + } +} + +/** + * Handle OAuth2 token flows (client_credentials, password) + */ +async function handleOAuth2TokenFlow( + auth: OAuth2Auth, + originalParams: HttpRequestParams, + makeRequest: (params: HttpRequestParams) => Promise, + retryConfig?: RetryConfig +): Promise { + if (!auth.flow || !auth.tokenUrl) return null; + + const params = new URLSearchParams(); + + if (auth.flow === 'client_credentials') { + params.append('grant_type', 'client_credentials'); + params.append('client_id', auth.clientId!); + } else if (auth.flow === 'password') { + params.append('grant_type', 'password'); + params.append('username', auth.username || ''); + params.append('password', auth.password || ''); + params.append('client_id', auth.clientId!); + } else { + return null; + } + + if (auth.clientSecret) { + params.append('client_secret', auth.clientSecret); + } + if (auth.scopes && auth.scopes.length > 0) { + params.append('scope', auth.scopes.join(' ')); + } + + const authHeaders: Record = { + 'Content-Type': 'application/x-www-form-urlencoded' + }; + + // Use basic auth for client credentials if both client ID and secret are provided + if (auth.flow === 'client_credentials' && auth.clientId && auth.clientSecret) { + const credentials = Buffer.from(`${auth.clientId}:${auth.clientSecret}`).toString('base64'); + authHeaders['Authorization'] = `Basic ${credentials}`; + params.delete('client_id'); + params.delete('client_secret'); + } + + const tokenResponse = await NodeFetch.default(auth.tokenUrl, { + method: 'POST', + headers: authHeaders, + body: params.toString() + }); + + if (!tokenResponse.ok) { + throw new Error(`OAuth2 token request failed: ${tokenResponse.statusText}`); + } + + const tokenData = await tokenResponse.json(); + const tokens: TokenResponse = { + accessToken: tokenData.access_token, + refreshToken: tokenData.refresh_token, + expiresIn: tokenData.expires_in + }; + + // Notify the client about the tokens + if (auth.onTokenRefresh) { + auth.onTokenRefresh(tokens); + } + + // Retry the original request with the new token + const updatedHeaders = { ...originalParams.headers }; + updatedHeaders['Authorization'] = `Bearer ${tokens.accessToken}`; + + return executeWithRetry({ ...originalParams, headers: updatedHeaders }, makeRequest, retryConfig); +} + +/** + * Handle OAuth2 token refresh on 401 response + */ +async function handleTokenRefresh( + auth: OAuth2Auth, + originalParams: HttpRequestParams, + makeRequest: (params: HttpRequestParams) => Promise, + retryConfig?: RetryConfig +): Promise { + if (!auth.refreshToken || !auth.tokenUrl || !auth.clientId) return null; + + const refreshResponse = await NodeFetch.default(auth.tokenUrl, { + method: 'POST', + headers: { + 'Content-Type': 'application/x-www-form-urlencoded' + }, + body: new URLSearchParams({ + grant_type: 'refresh_token', + refresh_token: auth.refreshToken, + client_id: auth.clientId, + ...(auth.clientSecret ? { client_secret: auth.clientSecret } : {}) + }).toString() + }); + + if (!refreshResponse.ok) { + throw new Error('Unauthorized'); + } + + const tokenData = await refreshResponse.json(); + const newTokens: TokenResponse = { + accessToken: tokenData.access_token, + refreshToken: tokenData.refresh_token || auth.refreshToken, + expiresIn: tokenData.expires_in + }; + + // Notify the client about the refreshed tokens + if (auth.onTokenRefresh) { + auth.onTokenRefresh(newTokens); + } + + // Retry the original request with the new token + const updatedHeaders = { ...originalParams.headers }; + updatedHeaders['Authorization'] = `Bearer ${newTokens.accessToken}`; + + return executeWithRetry({ ...originalParams, headers: updatedHeaders }, makeRequest, retryConfig); +} // ============================================================================ // Generated HTTP Client Functions // ============================================================================ @@ -1009,7 +1027,7 @@ async function postPingPostRequest(context: PostPingPostRequestContext): Promise }; // Validate OAuth2 config if present - if (config.auth?.type === 'oauth2') { + if (config.auth?.type === 'oauth2' && AUTH_FEATURES.oauth2) { validateOAuth2Config(config.auth); } @@ -1061,7 +1079,7 @@ async function postPingPostRequest(context: PostPingPostRequestContext): Promise } // Handle OAuth2 token flows that require getting a token first - if (config.auth?.type === 'oauth2' && !config.auth.accessToken) { + if (config.auth?.type === 'oauth2' && !config.auth.accessToken && AUTH_FEATURES.oauth2) { const tokenFlowResponse = await handleOAuth2TokenFlow(config.auth, requestParams, makeRequest, config.retry); if (tokenFlowResponse) { response = tokenFlowResponse; @@ -1069,7 +1087,7 @@ async function postPingPostRequest(context: PostPingPostRequestContext): Promise } // Handle 401 with token refresh - if (response.status === 401 && config.auth?.type === 'oauth2') { + if (response.status === 401 && config.auth?.type === 'oauth2' && AUTH_FEATURES.oauth2) { try { const refreshResponse = await handleTokenRefresh(config.auth, requestParams, makeRequest, config.retry); if (refreshResponse) { @@ -1128,7 +1146,7 @@ async function getPingGetRequest(context: GetPingGetRequestContext = {}): Promis }; // Validate OAuth2 config if present - if (config.auth?.type === 'oauth2') { + if (config.auth?.type === 'oauth2' && AUTH_FEATURES.oauth2) { validateOAuth2Config(config.auth); } @@ -1180,7 +1198,7 @@ async function getPingGetRequest(context: GetPingGetRequestContext = {}): Promis } // Handle OAuth2 token flows that require getting a token first - if (config.auth?.type === 'oauth2' && !config.auth.accessToken) { + if (config.auth?.type === 'oauth2' && !config.auth.accessToken && AUTH_FEATURES.oauth2) { const tokenFlowResponse = await handleOAuth2TokenFlow(config.auth, requestParams, makeRequest, config.retry); if (tokenFlowResponse) { response = tokenFlowResponse; @@ -1188,7 +1206,7 @@ async function getPingGetRequest(context: GetPingGetRequestContext = {}): Promis } // Handle 401 with token refresh - if (response.status === 401 && config.auth?.type === 'oauth2') { + if (response.status === 401 && config.auth?.type === 'oauth2' && AUTH_FEATURES.oauth2) { try { const refreshResponse = await handleTokenRefresh(config.auth, requestParams, makeRequest, config.retry); if (refreshResponse) { @@ -1248,7 +1266,7 @@ async function putPingPutRequest(context: PutPingPutRequestContext): Promise { res.end(); }); - return runWithServer(app, port, async (server, actualPort) => { + return runWithServer(app, port, async (_server, actualPort) => { const response = await postPingPostRequest({ payload: requestMessage, server: `http://localhost:${actualPort}`, @@ -61,7 +61,7 @@ describe('HTTP Client - API Key and Basic Authentication', () => { res.end(); }); - return runWithServer(app, port, async (server, actualPort) => { + return runWithServer(app, port, async (_server, actualPort) => { const response = await postPingPostRequest({ payload: requestMessage, server: `http://localhost:${actualPort}`, @@ -106,7 +106,7 @@ describe('HTTP Client - API Key and Basic Authentication', () => { res.end(); }); - return runWithServer(app, port, async (server, actualPort) => { + return runWithServer(app, port, async (_server, actualPort) => { const response = await postPingPostRequest({ payload: requestMessage, server: `http://localhost:${actualPort}`, @@ -146,7 +146,7 @@ describe('HTTP Client - API Key and Basic Authentication', () => { res.end(); }); - return runWithServer(app, port, async (server, actualPort) => { + return runWithServer(app, port, async (_server, actualPort) => { const response = await postPingPostRequest({ payload: requestMessage, server: `http://localhost:${actualPort}`, @@ -172,7 +172,7 @@ describe('HTTP Client - API Key and Basic Authentication', () => { res.end(); }); - return runWithServer(app, port, async (server, actualPort) => { + return runWithServer(app, port, async (_server, actualPort) => { try { await postPingPostRequest({ payload: requestMessage, diff --git a/test/runtime/typescript/test/channels/request_reply/http_client/authentication.spec.ts b/test/runtime/typescript/test/channels/request_reply/http_client/authentication.spec.ts index 63060de5..646ed2c3 100644 --- a/test/runtime/typescript/test/channels/request_reply/http_client/authentication.spec.ts +++ b/test/runtime/typescript/test/channels/request_reply/http_client/authentication.spec.ts @@ -23,7 +23,7 @@ describe('HTTP Client - Authentication', () => { res.end(); }); - return runWithServer(app, port, async (server, actualPort) => { + return runWithServer(app, port, async (_server, actualPort) => { const auth: AuthConfig = { type: 'bearer', token: 'test-token-123' }; await getPingGetRequest({ @@ -50,7 +50,7 @@ describe('HTTP Client - Authentication', () => { res.end(); }); - return runWithServer(app, port, async (server, actualPort) => { + return runWithServer(app, port, async (_server, actualPort) => { const auth: AuthConfig = { type: 'basic', username: 'user', password: 'pass' }; await getPingGetRequest({ @@ -78,7 +78,7 @@ describe('HTTP Client - Authentication', () => { res.end(); }); - return runWithServer(app, port, async (server, actualPort) => { + return runWithServer(app, port, async (_server, actualPort) => { const auth: AuthConfig = { type: 'apiKey', key: 'my-api-key-123' }; await getPingGetRequest({ @@ -103,7 +103,7 @@ describe('HTTP Client - Authentication', () => { res.end(); }); - return runWithServer(app, port, async (server, actualPort) => { + return runWithServer(app, port, async (_server, actualPort) => { const auth: AuthConfig = { type: 'apiKey', key: 'my-api-key-123', @@ -133,7 +133,7 @@ describe('HTTP Client - Authentication', () => { res.end(); }); - return runWithServer(app, port, async (server, actualPort) => { + return runWithServer(app, port, async (_server, actualPort) => { const auth: AuthConfig = { type: 'apiKey', key: 'custom-key-value', @@ -165,7 +165,7 @@ describe('HTTP Client - Authentication', () => { res.end(); }); - return runWithServer(app, port, async (server, actualPort) => { + return runWithServer(app, port, async (_server, actualPort) => { await getPingGetRequest({ server: `http://localhost:${actualPort}`, auth: { @@ -199,7 +199,7 @@ describe('HTTP Client - Authentication', () => { res.end(); }); - return runWithServer(app, port, async (server, actualPort) => { + return runWithServer(app, port, async (_server, actualPort) => { const auth: AuthConfig = { type: 'oauth2', accessToken: 'oauth-access-token-xyz' @@ -233,7 +233,7 @@ describe('HTTP Client - Authentication', () => { res.end(); }); - return runWithServer(app, port, async (server, actualPort) => { + return runWithServer(app, port, async (_server, actualPort) => { await getPingGetRequest({ server: `http://localhost:${actualPort}`, auth: { type: 'bearer', token: 'my-token' }, diff --git a/test/runtime/typescript/test/channels/request_reply/http_client/basics.spec.ts b/test/runtime/typescript/test/channels/request_reply/http_client/basics.spec.ts index cb2209a5..900ab24a 100644 --- a/test/runtime/typescript/test/channels/request_reply/http_client/basics.spec.ts +++ b/test/runtime/typescript/test/channels/request_reply/http_client/basics.spec.ts @@ -24,7 +24,7 @@ describe('HTTP Client - Basics', () => { res.end(); }); - return runWithServer(app, port, async (server, actualPort) => { + return runWithServer(app, port, async (_server, actualPort) => { const response = await postPingPostRequest({ payload: requestMessage, server: `http://localhost:${actualPort}` @@ -53,7 +53,7 @@ describe('HTTP Client - Basics', () => { res.end(); }); - return runWithServer(app, port, async (server, actualPort) => { + return runWithServer(app, port, async (_server, actualPort) => { const response = await getPingGetRequest({ server: `http://localhost:${actualPort}`, pagination: { type: 'offset', offset: 0, limit: 20 } @@ -84,7 +84,7 @@ describe('HTTP Client - Basics', () => { res.end(); }); - return runWithServer(app, port, async (server, actualPort) => { + return runWithServer(app, port, async (_server, actualPort) => { await getPingGetRequest({ server: `http://localhost:${actualPort}`, queryParams: { @@ -107,7 +107,7 @@ describe('HTTP Client - Basics', () => { res.status(401).json({ error: 'Unauthorized' }); }); - return runWithServer(app, port, async (server, actualPort) => { + return runWithServer(app, port, async (_server, actualPort) => { await expect(getPingGetRequest({ server: `http://localhost:${actualPort}` })).rejects.toThrow('Unauthorized'); @@ -121,7 +121,7 @@ describe('HTTP Client - Basics', () => { res.status(403).json({ error: 'Forbidden' }); }); - return runWithServer(app, port, async (server, actualPort) => { + return runWithServer(app, port, async (_server, actualPort) => { await expect(getPingGetRequest({ server: `http://localhost:${actualPort}` })).rejects.toThrow('Forbidden'); @@ -135,7 +135,7 @@ describe('HTTP Client - Basics', () => { res.status(404).json({ error: 'Not Found' }); }); - return runWithServer(app, port, async (server, actualPort) => { + return runWithServer(app, port, async (_server, actualPort) => { await expect(getPingGetRequest({ server: `http://localhost:${actualPort}` })).rejects.toThrow('Not Found'); @@ -149,7 +149,7 @@ describe('HTTP Client - Basics', () => { res.status(500).json({ error: 'Internal Server Error' }); }); - return runWithServer(app, port, async (server, actualPort) => { + return runWithServer(app, port, async (_server, actualPort) => { await expect(getPingGetRequest({ server: `http://localhost:${actualPort}` })).rejects.toThrow('Internal Server Error'); diff --git a/test/runtime/typescript/test/channels/request_reply/http_client/hooks.spec.ts b/test/runtime/typescript/test/channels/request_reply/http_client/hooks.spec.ts index 3e1517a0..60d094ae 100644 --- a/test/runtime/typescript/test/channels/request_reply/http_client/hooks.spec.ts +++ b/test/runtime/typescript/test/channels/request_reply/http_client/hooks.spec.ts @@ -24,7 +24,7 @@ describe('HTTP Client - Hooks', () => { res.end(); }); - return runWithServer(app, port, async (server, actualPort) => { + return runWithServer(app, port, async (_server, actualPort) => { const hooks: HttpHooks = { beforeRequest: (params) => ({ ...params, @@ -57,7 +57,7 @@ describe('HTTP Client - Hooks', () => { res.end(); }); - return runWithServer(app, port, async (server, actualPort) => { + return runWithServer(app, port, async (_server, actualPort) => { const hooks: HttpHooks = { beforeRequest: async (params) => { await new Promise(resolve => setTimeout(resolve, 10)); @@ -94,7 +94,7 @@ describe('HTTP Client - Hooks', () => { res.end(); }); - return runWithServer(app, port, async (server, actualPort) => { + return runWithServer(app, port, async (_server, actualPort) => { const hooks: HttpHooks = { beforeRequest: (params) => ({ ...params, @@ -127,7 +127,7 @@ describe('HTTP Client - Hooks', () => { res.end(); }); - return runWithServer(app, port, async (server, actualPort) => { + return runWithServer(app, port, async (_server, actualPort) => { const hooks: HttpHooks = { beforeRequest: (params) => ({ ...params, @@ -159,7 +159,7 @@ describe('HTTP Client - Hooks', () => { res.end(); }); - return runWithServer(app, port, async (server, actualPort) => { + return runWithServer(app, port, async (_server, actualPort) => { const hooks: HttpHooks = { afterResponse: (response, params) => { afterResponseCalled = true; @@ -190,7 +190,7 @@ describe('HTTP Client - Hooks', () => { res.end(); }); - return runWithServer(app, port, async (server, actualPort) => { + return runWithServer(app, port, async (_server, actualPort) => { let startTime: number; const hooks: HttpHooks = { @@ -232,7 +232,7 @@ describe('HTTP Client - Hooks', () => { res.status(404).json({ error: 'Not Found' }); }); - return runWithServer(app, port, async (server, actualPort) => { + return runWithServer(app, port, async (_server, actualPort) => { const hooks: HttpHooks = { onError: (error, params) => { onErrorCalled = true; @@ -262,7 +262,7 @@ describe('HTTP Client - Hooks', () => { res.status(503).json({ error: 'Service Unavailable', retryAfter: 60 }); }); - return runWithServer(app, port, async (server, actualPort) => { + return runWithServer(app, port, async (_server, actualPort) => { const hooks: HttpHooks = { onError: (error, params) => { const enhancedError = new Error(`Request to ${params.url} failed: ${error.message}`); @@ -286,7 +286,7 @@ describe('HTTP Client - Hooks', () => { res.status(500).json({ error: 'Server Error' }); }); - return runWithServer(app, port, async (server, actualPort) => { + return runWithServer(app, port, async (_server, actualPort) => { const hooks: HttpHooks = { onError: async (error, params) => { await new Promise(resolve => setTimeout(resolve, 10)); @@ -322,7 +322,7 @@ describe('HTTP Client - Hooks', () => { res.end(); }); - return runWithServer(app, port, async (server, actualPort) => { + return runWithServer(app, port, async (_server, actualPort) => { const hooks: HttpHooks = { makeRequest: async (params) => { customMakeRequestCalled = true; @@ -359,7 +359,7 @@ describe('HTTP Client - Hooks', () => { res.end(); }); - return runWithServer(app, port, async (server, actualPort) => { + return runWithServer(app, port, async (_server, actualPort) => { const hooks: HttpHooks = { beforeRequest: (params) => { hookCalls.push('beforeRequest'); @@ -407,7 +407,7 @@ describe('HTTP Client - Hooks', () => { } }); - return runWithServer(app, port, async (server, actualPort) => { + return runWithServer(app, port, async (_server, actualPort) => { const hooks: HttpHooks = { beforeRequest: (params) => { hookCalls.push('beforeRequest'); @@ -450,7 +450,7 @@ describe('HTTP Client - Hooks', () => { res.end(); }); - return runWithServer(app, port, async (server, actualPort) => { + return runWithServer(app, port, async (_server, actualPort) => { const hooks: HttpHooks = { beforeRequest: (params) => { const url = new URL(params.url); diff --git a/test/runtime/typescript/test/channels/request_reply/http_client/methods.spec.ts b/test/runtime/typescript/test/channels/request_reply/http_client/methods.spec.ts index 4faf4b24..dc5a2f87 100644 --- a/test/runtime/typescript/test/channels/request_reply/http_client/methods.spec.ts +++ b/test/runtime/typescript/test/channels/request_reply/http_client/methods.spec.ts @@ -29,7 +29,7 @@ describe('HTTP Client - HTTP Methods', () => { res.end(); }); - return runWithServer(app, port, async (server, actualPort) => { + return runWithServer(app, port, async (_server, actualPort) => { const response = await getPingGetRequest({ server: `http://localhost:${actualPort}` }); @@ -58,7 +58,7 @@ describe('HTTP Client - HTTP Methods', () => { res.end(); }); - return runWithServer(app, port, async (server, actualPort) => { + return runWithServer(app, port, async (_server, actualPort) => { const response = await postPingPostRequest({ server: `http://localhost:${actualPort}`, payload: requestMessage @@ -89,7 +89,7 @@ describe('HTTP Client - HTTP Methods', () => { res.end(); }); - return runWithServer(app, port, async (server, actualPort) => { + return runWithServer(app, port, async (_server, actualPort) => { const response = await putPingPutRequest({ server: `http://localhost:${actualPort}`, payload: requestMessage @@ -116,7 +116,7 @@ describe('HTTP Client - HTTP Methods', () => { res.end(); }); - return runWithServer(app, port, async (server, actualPort) => { + return runWithServer(app, port, async (_server, actualPort) => { await putPingPutRequest({ server: `http://localhost:${actualPort}`, payload: requestMessage, @@ -141,7 +141,7 @@ describe('HTTP Client - HTTP Methods', () => { res.end(); }); - return runWithServer(app, port, async (server, actualPort) => { + return runWithServer(app, port, async (_server, actualPort) => { await putPingPutRequest({ server: `http://localhost:${actualPort}`, payload: requestMessage, @@ -167,7 +167,7 @@ describe('HTTP Client - HTTP Methods', () => { res.end(); }); - return runWithServer(app, port, async (server, actualPort) => { + return runWithServer(app, port, async (_server, actualPort) => { const response = await deletePingDeleteRequest({ server: `http://localhost:${actualPort}` }); @@ -191,7 +191,7 @@ describe('HTTP Client - HTTP Methods', () => { res.end(); }); - return runWithServer(app, port, async (server, actualPort) => { + return runWithServer(app, port, async (_server, actualPort) => { await deletePingDeleteRequest({ server: `http://localhost:${actualPort}`, auth: { type: 'bearer', token: 'delete-token' } @@ -219,7 +219,7 @@ describe('HTTP Client - HTTP Methods', () => { res.end(); }); - return runWithServer(app, port, async (server, actualPort) => { + return runWithServer(app, port, async (_server, actualPort) => { const response = await patchPingPatchRequest({ server: `http://localhost:${actualPort}`, payload: requestMessage @@ -247,7 +247,7 @@ describe('HTTP Client - HTTP Methods', () => { res.end(); }); - return runWithServer(app, port, async (server, actualPort) => { + return runWithServer(app, port, async (_server, actualPort) => { const response = await patchPingPatchRequest({ server: `http://localhost:${actualPort}`, payload: requestMessage, @@ -273,7 +273,7 @@ describe('HTTP Client - HTTP Methods', () => { res.end(); }); - return runWithServer(app, port, async (server, actualPort) => { + return runWithServer(app, port, async (_server, actualPort) => { // HEAD requests return no body, so the generated code may fail // This test verifies the method is sent correctly even if parsing fails try { @@ -301,7 +301,7 @@ describe('HTTP Client - HTTP Methods', () => { res.end(); }); - return runWithServer(app, port, async (server, actualPort) => { + return runWithServer(app, port, async (_server, actualPort) => { try { await headPingHeadRequest({ server: `http://localhost:${actualPort}`, @@ -330,7 +330,7 @@ describe('HTTP Client - HTTP Methods', () => { res.end(); }); - return runWithServer(app, port, async (server, actualPort) => { + return runWithServer(app, port, async (_server, actualPort) => { // OPTIONS requests typically return no body try { await optionsPingOptionsRequest({ @@ -355,7 +355,7 @@ describe('HTTP Client - HTTP Methods', () => { res.status(404).json({ error: 'Not Found' }); }); - return runWithServer(app, port, async (server, actualPort) => { + return runWithServer(app, port, async (_server, actualPort) => { await expect(putPingPutRequest({ server: `http://localhost:${actualPort}`, payload: requestMessage @@ -370,7 +370,7 @@ describe('HTTP Client - HTTP Methods', () => { res.status(500).json({ error: 'Internal Server Error' }); }); - return runWithServer(app, port, async (server, actualPort) => { + return runWithServer(app, port, async (_server, actualPort) => { await expect(deletePingDeleteRequest({ server: `http://localhost:${actualPort}` })).rejects.toThrow('Internal Server Error'); @@ -386,7 +386,7 @@ describe('HTTP Client - HTTP Methods', () => { res.status(403).json({ error: 'Forbidden' }); }); - return runWithServer(app, port, async (server, actualPort) => { + return runWithServer(app, port, async (_server, actualPort) => { await expect(patchPingPatchRequest({ server: `http://localhost:${actualPort}`, payload: requestMessage @@ -414,7 +414,7 @@ describe('HTTP Client - HTTP Methods', () => { } }); - return runWithServer(app, port, async (server, actualPort) => { + return runWithServer(app, port, async (_server, actualPort) => { const response = await putPingPutRequest({ server: `http://localhost:${actualPort}`, payload: requestMessage, @@ -443,7 +443,7 @@ describe('HTTP Client - HTTP Methods', () => { } }); - return runWithServer(app, port, async (server, actualPort) => { + return runWithServer(app, port, async (_server, actualPort) => { const response = await deletePingDeleteRequest({ server: `http://localhost:${actualPort}`, retry: { maxRetries: 3, initialDelayMs: 50, retryableStatusCodes: [502] } diff --git a/test/runtime/typescript/test/channels/request_reply/http_client/oauth2.spec.ts b/test/runtime/typescript/test/channels/request_reply/http_client/oauth2.spec.ts index db5987cb..16b448e4 100644 --- a/test/runtime/typescript/test/channels/request_reply/http_client/oauth2.spec.ts +++ b/test/runtime/typescript/test/channels/request_reply/http_client/oauth2.spec.ts @@ -36,7 +36,7 @@ describe('HTTP Client - OAuth2', () => { res.end(); }); - return runWithServer(app, port, async (server, actualPort) => { + return runWithServer(app, port, async (_server, actualPort) => { const auth: OAuth2Auth = { type: 'oauth2', flow: 'client_credentials', @@ -68,7 +68,7 @@ describe('HTTP Client - OAuth2', () => { res.json({ error: 'should not reach here' }); }); - return runWithServer(app, port, async (server, actualPort) => { + return runWithServer(app, port, async (_server, actualPort) => { const auth: OAuth2Auth = { type: 'oauth2', flow: 'client_credentials', @@ -109,7 +109,7 @@ describe('HTTP Client - OAuth2', () => { res.end(); }); - return runWithServer(app, port, async (server, actualPort) => { + return runWithServer(app, port, async (_server, actualPort) => { const auth: OAuth2Auth = { type: 'oauth2', flow: 'password', @@ -138,7 +138,7 @@ describe('HTTP Client - OAuth2', () => { res.json({ error: 'should not reach here' }); }); - return runWithServer(app, port, async (server, actualPort) => { + return runWithServer(app, port, async (_server, actualPort) => { const auth: OAuth2Auth = { type: 'oauth2', flow: 'password', @@ -184,7 +184,7 @@ describe('HTTP Client - OAuth2', () => { } }); - return runWithServer(app, port, async (server, actualPort) => { + return runWithServer(app, port, async (_server, actualPort) => { const auth: OAuth2Auth = { type: 'oauth2', accessToken: 'expired-token', @@ -243,7 +243,7 @@ describe('HTTP Client - OAuth2', () => { } }); - return runWithServer(app, port, async (server, actualPort) => { + return runWithServer(app, port, async (_server, actualPort) => { const auth: OAuth2Auth = { type: 'oauth2', flow: 'client_credentials', @@ -307,7 +307,7 @@ describe('HTTP Client - OAuth2', () => { } }); - return runWithServer(app, port, async (server, actualPort) => { + return runWithServer(app, port, async (_server, actualPort) => { const auth: OAuth2Auth = { type: 'oauth2', flow: 'password', @@ -367,7 +367,7 @@ describe('HTTP Client - OAuth2', () => { } }); - return runWithServer(app, port, async (server, actualPort) => { + return runWithServer(app, port, async (_server, actualPort) => { const auth: OAuth2Auth = { type: 'oauth2', accessToken: 'expired-token', @@ -422,7 +422,7 @@ describe('HTTP Client - OAuth2', () => { } }); - return runWithServer(app, port, async (server, actualPort) => { + return runWithServer(app, port, async (_server, actualPort) => { const auth: OAuth2Auth = { type: 'oauth2', flow: 'client_credentials', @@ -474,7 +474,7 @@ describe('HTTP Client - OAuth2', () => { } }); - return runWithServer(app, port, async (server, actualPort) => { + return runWithServer(app, port, async (_server, actualPort) => { const auth: OAuth2Auth = { type: 'oauth2', flow: 'client_credentials', @@ -527,7 +527,7 @@ describe('HTTP Client - OAuth2', () => { res.end(); }); - return runWithServer(app, port, async (server, actualPort) => { + return runWithServer(app, port, async (_server, actualPort) => { // Use 'implicit' flow which is not supported for token fetching const auth: OAuth2Auth = { type: 'oauth2', @@ -562,7 +562,7 @@ describe('HTTP Client - OAuth2', () => { res.end(); }); - return runWithServer(app, port, async (server, actualPort) => { + return runWithServer(app, port, async (_server, actualPort) => { // When no flow is specified but accessToken is provided, // it should be used directly without fetching const auth: OAuth2Auth = { @@ -597,7 +597,7 @@ describe('HTTP Client - OAuth2', () => { res.end(); }); - return runWithServer(app, port, async (server, actualPort) => { + return runWithServer(app, port, async (_server, actualPort) => { const auth: OAuth2Auth = { type: 'oauth2', flow: 'client_credentials', @@ -641,7 +641,7 @@ describe('HTTP Client - OAuth2', () => { } }); - return runWithServer(app, port, async (server, actualPort) => { + return runWithServer(app, port, async (_server, actualPort) => { const auth: OAuth2Auth = { type: 'oauth2', accessToken: 'expired-token', diff --git a/test/runtime/typescript/test/channels/request_reply/http_client/oauth2_client_credentials.spec.ts b/test/runtime/typescript/test/channels/request_reply/http_client/oauth2_client_credentials.spec.ts index 8d9ce369..feae72fb 100644 --- a/test/runtime/typescript/test/channels/request_reply/http_client/oauth2_client_credentials.spec.ts +++ b/test/runtime/typescript/test/channels/request_reply/http_client/oauth2_client_credentials.spec.ts @@ -69,7 +69,7 @@ describe('HTTP Client - OAuth2 Client Credentials Flow', () => { res.end(); }); - return runWithServer(app, port, async (server, actualPort) => { + return runWithServer(app, port, async (_server, actualPort) => { // Mock onTokenRefresh callback const onTokenRefresh = jest.fn(); @@ -112,7 +112,7 @@ describe('HTTP Client - OAuth2 Client Credentials Flow', () => { }); }); - return runWithServer(app, port, async (server, actualPort) => { + return runWithServer(app, port, async (_server, actualPort) => { try { await postPingPostRequest({ payload: requestMessage, @@ -213,7 +213,7 @@ describe('HTTP Client - OAuth2 Client Credentials Flow', () => { res.end(); }); - return runWithServer(app, port, async (server, actualPort) => { + return runWithServer(app, port, async (_server, actualPort) => { const response = await postPingPostRequest({ payload: requestMessage, server: `http://localhost:${actualPort}`, diff --git a/test/runtime/typescript/test/channels/request_reply/http_client/oauth2_implicit_flow.spec.ts b/test/runtime/typescript/test/channels/request_reply/http_client/oauth2_implicit_flow.spec.ts index e8bd561e..4a836c8f 100644 --- a/test/runtime/typescript/test/channels/request_reply/http_client/oauth2_implicit_flow.spec.ts +++ b/test/runtime/typescript/test/channels/request_reply/http_client/oauth2_implicit_flow.spec.ts @@ -58,7 +58,7 @@ describe('HTTP Client - OAuth2 Pre-obtained Access Token', () => { res.end(); }); - return runWithServer(app, port, async (server, actualPort) => { + return runWithServer(app, port, async (_server, actualPort) => { // This is how you'd use a token obtained from a browser-based flow // (implicit, authorization_code via PKCE, etc.) const response = await postPingPostRequest({ @@ -120,7 +120,7 @@ describe('HTTP Client - OAuth2 Pre-obtained Access Token', () => { res.status(401).json({ error: 'Invalid Token' }); }); - return runWithServer(app, port, async (server, actualPort) => { + return runWithServer(app, port, async (_server, actualPort) => { const onTokenRefresh = jest.fn(); // Use pre-obtained token with refresh capability @@ -160,7 +160,7 @@ describe('HTTP Client - OAuth2 Pre-obtained Access Token', () => { res.status(401).json({ error: 'Unauthorized' }); }); - return runWithServer(app, port, async (server, actualPort) => { + return runWithServer(app, port, async (_server, actualPort) => { // Simplest OAuth2 usage - just pass the access token const response = await postPingPostRequest({ payload: requestMessage, @@ -189,7 +189,7 @@ describe('HTTP Client - OAuth2 Pre-obtained Access Token', () => { res.json({ success: true }); }); - return runWithServer(app, port, async (server, actualPort) => { + return runWithServer(app, port, async (_server, actualPort) => { try { // OAuth2 config without access token and no server-side flow await postPingPostRequest({ diff --git a/test/runtime/typescript/test/channels/request_reply/http_client/oauth2_password_flow.spec.ts b/test/runtime/typescript/test/channels/request_reply/http_client/oauth2_password_flow.spec.ts index 921c5862..623f1672 100644 --- a/test/runtime/typescript/test/channels/request_reply/http_client/oauth2_password_flow.spec.ts +++ b/test/runtime/typescript/test/channels/request_reply/http_client/oauth2_password_flow.spec.ts @@ -69,7 +69,7 @@ describe('HTTP Client - OAuth2 Password Flow', () => { res.end(); }); - return runWithServer(app, port, async (server, actualPort) => { + return runWithServer(app, port, async (_server, actualPort) => { // Mock onTokenRefresh callback const onTokenRefresh = jest.fn(); @@ -117,7 +117,7 @@ describe('HTTP Client - OAuth2 Password Flow', () => { }); }); - return runWithServer(app, port, async (server, actualPort) => { + return runWithServer(app, port, async (_server, actualPort) => { try { await postPingPostRequest({ payload: requestMessage, @@ -211,7 +211,7 @@ describe('HTTP Client - OAuth2 Password Flow', () => { res.end(); }); - return runWithServer(app, port, async (server, actualPort) => { + return runWithServer(app, port, async (_server, actualPort) => { const response = await postPingPostRequest({ payload: requestMessage, server: `http://localhost:${actualPort}`, diff --git a/test/runtime/typescript/test/channels/request_reply/http_client/oauth2_refresh_token.spec.ts b/test/runtime/typescript/test/channels/request_reply/http_client/oauth2_refresh_token.spec.ts index 50a5bc5f..bcd2a6c8 100644 --- a/test/runtime/typescript/test/channels/request_reply/http_client/oauth2_refresh_token.spec.ts +++ b/test/runtime/typescript/test/channels/request_reply/http_client/oauth2_refresh_token.spec.ts @@ -84,7 +84,7 @@ describe('HTTP Client - OAuth2 Refresh Token Flow', () => { res.status(401).json(TestResponses.unauthorized('Invalid Token').body); }); - return runWithServer(app, port, async (server, actualPort) => { + return runWithServer(app, port, async (_server, actualPort) => { // Mock onTokenRefresh callback const onTokenRefresh = jest.fn(); @@ -138,7 +138,7 @@ describe('HTTP Client - OAuth2 Refresh Token Flow', () => { res.status(401).json(TestResponses.unauthorized('Token Expired').body); }); - return runWithServer(app, port, async (server, actualPort) => { + return runWithServer(app, port, async (_server, actualPort) => { try { await postPingPostRequest({ payload: requestMessage, @@ -172,7 +172,7 @@ describe('HTTP Client - OAuth2 Refresh Token Flow', () => { res.status(401).json(TestResponses.unauthorized('Token Expired').body); }); - return runWithServer(app, port, async (server, actualPort) => { + return runWithServer(app, port, async (_server, actualPort) => { try { await postPingPostRequest({ payload: requestMessage, @@ -236,7 +236,7 @@ describe('HTTP Client - OAuth2 Refresh Token Flow', () => { } }); - return runWithServer(app, port, async (server, actualPort) => { + return runWithServer(app, port, async (_server, actualPort) => { // Mock onTokenRefresh callback const onTokenRefresh = jest.fn(); diff --git a/test/runtime/typescript/test/channels/request_reply/http_client/openapi.spec.ts b/test/runtime/typescript/test/channels/request_reply/http_client/openapi.spec.ts index d77bcf4b..fb3bb704 100644 --- a/test/runtime/typescript/test/channels/request_reply/http_client/openapi.spec.ts +++ b/test/runtime/typescript/test/channels/request_reply/http_client/openapi.spec.ts @@ -26,7 +26,7 @@ describe('HTTP Client - OpenAPI Generated', () => { res.end(); }); - return runWithServer(app, port, async (server, actualPort) => { + return runWithServer(app, port, async (_server, actualPort) => { const response = await postAddPet({ payload: requestPet, server: `http://localhost:${actualPort}` @@ -55,7 +55,7 @@ describe('HTTP Client - OpenAPI Generated', () => { res.end(); }); - return runWithServer(app, port, async (server, actualPort) => { + return runWithServer(app, port, async (_server, actualPort) => { const response = await putUpdatePet({ payload: requestPet, server: `http://localhost:${actualPort}` @@ -82,7 +82,7 @@ describe('HTTP Client - OpenAPI Generated', () => { res.end(); }); - return runWithServer(app, port, async (server, actualPort) => { + return runWithServer(app, port, async (_server, actualPort) => { const params = new FindPetsByStatusAndCategoryParameters({ status: 'available', categoryId: 123 @@ -108,7 +108,7 @@ describe('HTTP Client - OpenAPI Generated', () => { res.status(400).json({ error: 'Bad Request' }); }); - return runWithServer(app, port, async (server, actualPort) => { + return runWithServer(app, port, async (_server, actualPort) => { const pet = new APet({ name: 'Test', photoUrls: [] }); await expect(postAddPet({ payload: pet, @@ -124,7 +124,7 @@ describe('HTTP Client - OpenAPI Generated', () => { res.status(404).json({ error: 'Not Found' }); }); - return runWithServer(app, port, async (server, actualPort) => { + return runWithServer(app, port, async (_server, actualPort) => { const params = new FindPetsByStatusAndCategoryParameters({ status: 'invalid', categoryId: 999 diff --git a/test/runtime/typescript/test/channels/request_reply/http_client/pagination.spec.ts b/test/runtime/typescript/test/channels/request_reply/http_client/pagination.spec.ts index 5129b6bd..ee74bc9f 100644 --- a/test/runtime/typescript/test/channels/request_reply/http_client/pagination.spec.ts +++ b/test/runtime/typescript/test/channels/request_reply/http_client/pagination.spec.ts @@ -25,7 +25,7 @@ describe('HTTP Client - Pagination', () => { res.end(); }); - return runWithServer(app, port, async (server, actualPort) => { + return runWithServer(app, port, async (_server, actualPort) => { const pagination: PaginationConfig = { type: 'offset', offset: 20, @@ -57,7 +57,7 @@ describe('HTTP Client - Pagination', () => { res.end(); }); - return runWithServer(app, port, async (server, actualPort) => { + return runWithServer(app, port, async (_server, actualPort) => { const pagination: PaginationConfig = { type: 'offset', in: 'header', @@ -90,7 +90,7 @@ describe('HTTP Client - Pagination', () => { res.end(); }); - return runWithServer(app, port, async (server, actualPort) => { + return runWithServer(app, port, async (_server, actualPort) => { const pagination: PaginationConfig = { type: 'offset', offset: 100, @@ -126,7 +126,7 @@ describe('HTTP Client - Pagination', () => { res.end(); }); - return runWithServer(app, port, async (server, actualPort) => { + return runWithServer(app, port, async (_server, actualPort) => { const pagination: PaginationConfig = { type: 'cursor', cursor: 'abc123xyz', @@ -165,7 +165,7 @@ describe('HTTP Client - Pagination', () => { res.end(); }); - return runWithServer(app, port, async (server, actualPort) => { + return runWithServer(app, port, async (_server, actualPort) => { const page1 = await getPingGetRequest({ server: `http://localhost:${actualPort}`, pagination: { type: 'cursor', limit: 10 } @@ -202,7 +202,7 @@ describe('HTTP Client - Pagination', () => { res.end(); }); - return runWithServer(app, port, async (server, actualPort) => { + return runWithServer(app, port, async (_server, actualPort) => { const pagination: PaginationConfig = { type: 'page', page: 3, @@ -234,7 +234,7 @@ describe('HTTP Client - Pagination', () => { res.end(); }); - return runWithServer(app, port, async (server, actualPort) => { + return runWithServer(app, port, async (_server, actualPort) => { const pagination: PaginationConfig = { type: 'range', start: 0, @@ -265,7 +265,7 @@ describe('HTTP Client - Pagination', () => { res.end(); }); - return runWithServer(app, port, async (server, actualPort) => { + return runWithServer(app, port, async (_server, actualPort) => { const page1 = await getPingGetRequest({ server: `http://localhost:${actualPort}`, pagination: { type: 'range', start: 0, end: 24, unit: 'items' } @@ -293,7 +293,7 @@ describe('HTTP Client - Pagination', () => { res.end(); }); - return runWithServer(app, port, async (server, actualPort) => { + return runWithServer(app, port, async (_server, actualPort) => { const response = await getPingGetRequest({ server: `http://localhost:${actualPort}`, pagination: { type: 'offset', offset: 0, limit: 20 } @@ -324,7 +324,7 @@ describe('HTTP Client - Pagination', () => { res.end(); }); - return runWithServer(app, port, async (server, actualPort) => { + return runWithServer(app, port, async (_server, actualPort) => { const page1 = await getPingGetRequest({ server: `http://localhost:${actualPort}`, pagination: { type: 'offset', offset: 0, limit: 20 } @@ -357,7 +357,7 @@ describe('HTTP Client - Pagination', () => { res.end(); }); - return runWithServer(app, port, async (server, actualPort) => { + return runWithServer(app, port, async (_server, actualPort) => { const page = await getPingGetRequest({ server: `http://localhost:${actualPort}`, pagination: { type: 'offset', offset: 60, limit: 20 } @@ -383,7 +383,7 @@ describe('HTTP Client - Pagination', () => { res.end(); }); - return runWithServer(app, port, async (server, actualPort) => { + return runWithServer(app, port, async (_server, actualPort) => { const response = await getPingGetRequest({ server: `http://localhost:${actualPort}`, pagination: { type: 'page', page: 1, pageSize: 20 } diff --git a/test/runtime/typescript/test/channels/request_reply/http_client/parameters-headers.spec.ts b/test/runtime/typescript/test/channels/request_reply/http_client/parameters-headers.spec.ts index 6d1b6ea8..57ccf4a7 100644 --- a/test/runtime/typescript/test/channels/request_reply/http_client/parameters-headers.spec.ts +++ b/test/runtime/typescript/test/channels/request_reply/http_client/parameters-headers.spec.ts @@ -27,7 +27,7 @@ describe('HTTP Client - Parameters and Headers', () => { }); }); - return runWithServer(app, port, async (server, actualPort) => { + return runWithServer(app, port, async (_server, actualPort) => { const parameters = new UserItemsParameters({ userId: 'user-123', itemId: '456' @@ -61,7 +61,7 @@ describe('HTTP Client - Parameters and Headers', () => { }); }); - return runWithServer(app, port, async (server, actualPort) => { + return runWithServer(app, port, async (_server, actualPort) => { await getGetUserItem({ server: `http://localhost:${actualPort}`, parameters: new UserItemsParameters({ userId: 'alice', itemId: '100' }) @@ -96,7 +96,7 @@ describe('HTTP Client - Parameters and Headers', () => { }); }); - return runWithServer(app, port, async (server, actualPort) => { + return runWithServer(app, port, async (_server, actualPort) => { const response = await getGetUserItem({ server: `http://localhost:${actualPort}`, parameters: new UserItemsParameters({ userId: 'secure-user', itemId: '999' }), @@ -128,7 +128,7 @@ describe('HTTP Client - Parameters and Headers', () => { }); }); - return runWithServer(app, port, async (server, actualPort) => { + return runWithServer(app, port, async (_server, actualPort) => { await getGetUserItem({ server: `http://localhost:${actualPort}`, parameters: new UserItemsParameters({ userId: 'user1', itemId: '42' }), @@ -164,7 +164,7 @@ describe('HTTP Client - Parameters and Headers', () => { }); }); - return runWithServer(app, port, async (server, actualPort) => { + return runWithServer(app, port, async (_server, actualPort) => { const headers = new ItemRequestHeaders({ xCorrelationId: 'corr-123-abc', xRequestId: 'req-456-def' @@ -206,7 +206,7 @@ describe('HTTP Client - Parameters and Headers', () => { }); }); - return runWithServer(app, port, async (server, actualPort) => { + return runWithServer(app, port, async (_server, actualPort) => { const headers = new ItemRequestHeaders({ xCorrelationId: 'required-only' }); @@ -245,7 +245,7 @@ describe('HTTP Client - Parameters and Headers', () => { }); }); - return runWithServer(app, port, async (server, actualPort) => { + return runWithServer(app, port, async (_server, actualPort) => { const response = await putUpdateUserItem({ server: `http://localhost:${actualPort}`, parameters: new UserItemsParameters({ userId: 'u', itemId: '1' }), @@ -279,7 +279,7 @@ describe('HTTP Client - Parameters and Headers', () => { }); }); - return runWithServer(app, port, async (server, actualPort) => { + return runWithServer(app, port, async (_server, actualPort) => { await putUpdateUserItem({ server: `http://localhost:${actualPort}`, parameters: new UserItemsParameters({ userId: 'u', itemId: '1' }), @@ -326,7 +326,7 @@ describe('HTTP Client - Parameters and Headers', () => { }); }); - return runWithServer(app, port, async (server, actualPort) => { + return runWithServer(app, port, async (_server, actualPort) => { const response = await putUpdateUserItem({ server: `http://localhost:${actualPort}`, parameters: new UserItemsParameters({ userId: 'full-user', itemId: '999' }), @@ -364,7 +364,7 @@ describe('HTTP Client - Parameters and Headers', () => { }); }); - return runWithServer(app, port, async (server, actualPort) => { + return runWithServer(app, port, async (_server, actualPort) => { const parameters = new UserItemsParameters({ userId: 'user-1', itemId: 'non-existent' diff --git a/test/runtime/typescript/test/channels/request_reply/http_client/retry.spec.ts b/test/runtime/typescript/test/channels/request_reply/http_client/retry.spec.ts index 7e6c2921..586a07c3 100644 --- a/test/runtime/typescript/test/channels/request_reply/http_client/retry.spec.ts +++ b/test/runtime/typescript/test/channels/request_reply/http_client/retry.spec.ts @@ -27,7 +27,7 @@ describe('HTTP Client - Retry Logic', () => { } }); - return runWithServer(app, port, async (server, actualPort) => { + return runWithServer(app, port, async (_server, actualPort) => { const retry: RetryConfig = { maxRetries: 3, initialDelayMs: 100, @@ -62,7 +62,7 @@ describe('HTTP Client - Retry Logic', () => { } }); - return runWithServer(app, port, async (server, actualPort) => { + return runWithServer(app, port, async (_server, actualPort) => { const retry: RetryConfig = { maxRetries: 5, initialDelayMs: 30, @@ -103,7 +103,7 @@ describe('HTTP Client - Retry Logic', () => { } }); - return runWithServer(app, port, async (server, actualPort) => { + return runWithServer(app, port, async (_server, actualPort) => { const retry: RetryConfig = { maxRetries: 3, initialDelayMs: 50, @@ -135,7 +135,7 @@ describe('HTTP Client - Retry Logic', () => { res.status(500).json({ error: 'Server Error' }); }); - return runWithServer(app, port, async (server, actualPort) => { + return runWithServer(app, port, async (_server, actualPort) => { const retry: RetryConfig = { maxRetries: 3, initialDelayMs: 50, @@ -171,7 +171,7 @@ describe('HTTP Client - Retry Logic', () => { } }); - return runWithServer(app, port, async (server, actualPort) => { + return runWithServer(app, port, async (_server, actualPort) => { const retry: RetryConfig = { maxRetries: 5, initialDelayMs: 100, @@ -212,7 +212,7 @@ describe('HTTP Client - Retry Logic', () => { } }); - return runWithServer(app, port, async (server, actualPort) => { + return runWithServer(app, port, async (_server, actualPort) => { const retry: RetryConfig = { maxRetries: 5, initialDelayMs: 100, @@ -246,7 +246,7 @@ describe('HTTP Client - Retry Logic', () => { res.status(400).json({ error: 'Bad Request' }); }); - return runWithServer(app, port, async (server, actualPort) => { + return runWithServer(app, port, async (_server, actualPort) => { const retry: RetryConfig = { maxRetries: 3, initialDelayMs: 50, @@ -279,7 +279,7 @@ describe('HTTP Client - Retry Logic', () => { } }); - return runWithServer(app, port, async (server, actualPort) => { + return runWithServer(app, port, async (_server, actualPort) => { const retry: RetryConfig = { maxRetries: 3, initialDelayMs: 50 @@ -305,7 +305,7 @@ describe('HTTP Client - Retry Logic', () => { res.status(500).json({ error: 'Server Error' }); }); - return runWithServer(app, port, async (server, actualPort) => { + return runWithServer(app, port, async (_server, actualPort) => { const retry: RetryConfig = { maxRetries: 3, initialDelayMs: 50, @@ -338,7 +338,7 @@ describe('HTTP Client - Retry Logic', () => { } }); - return runWithServer(app, port, async (server, actualPort) => { + return runWithServer(app, port, async (_server, actualPort) => { const retry: RetryConfig = { maxRetries: 3, initialDelayMs: 50 @@ -371,7 +371,7 @@ describe('HTTP Client - Retry Logic', () => { } }); - return runWithServer(app, port, async (server, actualPort) => { + return runWithServer(app, port, async (_server, actualPort) => { const retry: RetryConfig = { maxRetries: 3, initialDelayMs: 50 @@ -432,7 +432,7 @@ describe('HTTP Client - Retry Logic', () => { res.end(); }); - return runWithServer(app, port, async (server, actualPort) => { + return runWithServer(app, port, async (_server, actualPort) => { const retry: RetryConfig = { maxRetries: 3, initialDelayMs: 50 diff --git a/test/runtime/typescript/test/channels/request_reply/http_client/security_schemes.spec.ts b/test/runtime/typescript/test/channels/request_reply/http_client/security_schemes.spec.ts new file mode 100644 index 00000000..05399a22 --- /dev/null +++ b/test/runtime/typescript/test/channels/request_reply/http_client/security_schemes.spec.ts @@ -0,0 +1,235 @@ +/* eslint-disable no-console */ +/** + * Runtime tests for dynamically generated security types from OpenAPI securitySchemes. + * + * These tests verify that: + * 1. Generated AuthConfig only contains auth types defined in the OpenAPI spec + * 2. The narrowed types work correctly at runtime + * 3. OAuth2 auth works with access tokens + * 4. The generated code compiles and works correctly at runtime + */ +import {createTestServer, runWithServer} from './test-utils'; +import {APet} from '../../../../src/openapi/payloads/APet'; +import { + postAddPet +} from '../../../../src/openapi/channels/http_client'; + +// Type-level test: Verify AuthConfig is narrowed to only defined types +// If this compiles, the types are correctly generated +type AssertAuthConfigHasOAuth2 = Extract< + import('../../../../src/openapi/channels/http_client').AuthConfig, + {type: 'oauth2'} +> extends never + ? 'missing' + : 'present'; + +type AssertAuthConfigHasApiKey = Extract< + import('../../../../src/openapi/channels/http_client').AuthConfig, + {type: 'apiKey'} +> extends never + ? 'missing' + : 'present'; + +// These should NOT exist if security schemes are properly extracted +type AssertAuthConfigMissingBasic = Extract< + import('../../../../src/openapi/channels/http_client').AuthConfig, + {type: 'basic'} +> extends never + ? 'correctly-missing' + : 'unexpectedly-present'; + +type AssertAuthConfigMissingBearer = Extract< + import('../../../../src/openapi/channels/http_client').AuthConfig, + {type: 'bearer'} +> extends never + ? 'correctly-missing' + : 'unexpectedly-present'; + +// Type assertions - these will fail at compile time if the types are wrong +const _assertOAuth2Present: AssertAuthConfigHasOAuth2 = 'present'; +const _assertApiKeyPresent: AssertAuthConfigHasApiKey = 'present'; +const _assertBasicMissing: AssertAuthConfigMissingBasic = 'correctly-missing'; +const _assertBearerMissing: AssertAuthConfigMissingBearer = 'correctly-missing'; + +jest.setTimeout(15000); + +describe('HTTP Client - Security Schemes from OpenAPI', () => { + describe('apiKey authentication (api_key from spec)', () => { + it('should allow apiKey auth type since it is in the spec', async () => { + const {app, router, port} = createTestServer(); + + const responsePet = new APet({ + id: 1, + name: 'Fluffy', + photoUrls: ['http://example.com/fluffy.jpg'] + }); + let receivedApiKey: string | undefined; + + // The spec defines: name: "api_key", in: "header" + // The generated code uses 'api_key' as the default header name (from spec) + router.post('/pet', (req, res) => { + receivedApiKey = req.headers['api_key'] as string; + res.setHeader('Content-Type', 'application/json'); + res.write(responsePet.marshal()); + res.end(); + }); + + return runWithServer(app, port, async (_server, actualPort) => { + const requestPet = new APet({ + name: 'Fluffy', + photoUrls: ['http://example.com/fluffy.jpg'] + }); + + // Use apiKey auth - users need to provide name/in but the defaults + // from the spec are documented in the generated interface + await postAddPet({ + payload: requestPet, + server: `http://localhost:${actualPort}`, + auth: { + type: 'apiKey', + key: 'my-secret-api-key' + // Uses default header name 'api_key' from spec when not specified + } + }); + + expect(receivedApiKey).toBe('my-secret-api-key'); + }); + }); + + it('should allow specifying custom apiKey name from spec', async () => { + const {app, router, port} = createTestServer(); + + const responsePet = new APet({ + id: 1, + name: 'Fluffy', + photoUrls: [] + }); + let receivedApiKey: string | undefined; + + // The spec defines: name: "api_key", in: "header" + router.post('/pet', (req, res) => { + receivedApiKey = req.headers['api_key'] as string; + res.setHeader('Content-Type', 'application/json'); + res.write(responsePet.marshal()); + res.end(); + }); + + return runWithServer(app, port, async (_server, actualPort) => { + const requestPet = new APet({ + name: 'Fluffy', + photoUrls: [] + }); + + // Use the header name from the spec + await postAddPet({ + payload: requestPet, + server: `http://localhost:${actualPort}`, + auth: { + type: 'apiKey', + key: 'my-secret-api-key', + name: 'api_key', // From spec: components.securitySchemes.api_key.name + in: 'header' // From spec: components.securitySchemes.api_key.in + } + }); + + expect(receivedApiKey).toBe('my-secret-api-key'); + }); + }); + }); + + describe('oauth2 authentication (petstore_auth from spec)', () => { + it('should allow oauth2 auth type since it is in the spec', async () => { + const {app, router, port} = createTestServer(); + + const responsePet = new APet({ + id: 1, + name: 'Fluffy', + photoUrls: ['http://example.com/fluffy.jpg'] + }); + + router.post('/pet', (req, res) => { + res.setHeader('Content-Type', 'application/json'); + res.write(responsePet.marshal()); + res.end(); + }); + + return runWithServer(app, port, async (_server, actualPort) => { + const requestPet = new APet({ + name: 'Fluffy', + photoUrls: ['http://example.com/fluffy.jpg'] + }); + + // With a pre-obtained token, oauth2 works + const response = await postAddPet({ + payload: requestPet, + server: `http://localhost:${actualPort}`, + auth: { + type: 'oauth2', + accessToken: 'pre-obtained-token' + } + }); + + expect(response.status).toBe(200); + }); + }); + + it('should accept scopes as the spec defines them', async () => { + const {app, router, port} = createTestServer(); + + const responsePet = new APet({ + id: 1, + name: 'Fluffy', + photoUrls: [] + }); + + router.post('/pet', (req, res) => { + res.setHeader('Content-Type', 'application/json'); + res.write(responsePet.marshal()); + res.end(); + }); + + return runWithServer(app, port, async (_server, actualPort) => { + const requestPet = new APet({ + name: 'Test', + photoUrls: [] + }); + + // The spec defines scopes: write:pets, read:pets + // The generated interface documents these in the JSDoc + await postAddPet({ + payload: requestPet, + server: `http://localhost:${actualPort}`, + auth: { + type: 'oauth2', + accessToken: 'token', + scopes: ['write:pets', 'read:pets'] + } + }); + }); + }); + }); + + describe('auth type narrowing', () => { + it('should reject invalid auth types at compile time', async () => { + // This test verifies at compile time that invalid auth types are rejected + // If the security schemes are properly extracted, 'basic' and 'bearer' + // should not be valid auth types for this OpenAPI spec + // + // The following commented code would cause a TypeScript error: + // + // await postAddPet({ + // payload: requestPet, + // server: `http://localhost:${actualPort}`, + // auth: { type: 'basic', username: 'user', password: 'pass' } // TypeScript Error! + // }); + // + // await postAddPet({ + // payload: requestPet, + // server: `http://localhost:${actualPort}`, + // auth: { type: 'bearer', token: 'token' } // TypeScript Error! + // }); + + expect(true).toBe(true); // Placeholder - the real test is at compile time + }); + }); +}); diff --git a/test/runtime/typescript/test/channels/request_reply/http_client/test-utils.ts b/test/runtime/typescript/test/channels/request_reply/http_client/test-utils.ts index fc7d84fc..0eafab24 100644 --- a/test/runtime/typescript/test/channels/request_reply/http_client/test-utils.ts +++ b/test/runtime/typescript/test/channels/request_reply/http_client/test-utils.ts @@ -1,13 +1,6 @@ import express, { Router, Express } from 'express'; import bodyParser from 'body-parser'; -import { Server } from 'http'; - -/** - * Generate a random port between min and max (inclusive) - */ -function getRandomPort(min = 5779, max = 9875): number { - return Math.floor(Math.random() * (max - min + 1)) + min; -} +import { Server, AddressInfo } from 'http'; /** * Helper function to create an Express server for HTTP client tests @@ -24,54 +17,40 @@ export function createTestServer(): { app.use(bodyParser.urlencoded({ extended: true })); app.use(router); - // Generate a random port between 5779 and 9875 - const port = getRandomPort(); - - return { app, router, port }; + // Return a placeholder port - actual port will be assigned dynamically + return { app, router, port: 0 }; } /** * Start an Express server and run the test function - * This handles proper server cleanup after the test - * Automatically retries with a different port on EADDRINUSE errors + * This handles proper server cleanup after the test. + * Uses port 0 to let the OS assign an available port, avoiding EADDRINUSE errors. */ export function runWithServer( server: Express, - port: number, - testFn: (server: Server, port: number) => Promise, - maxRetries = 5 + _port: number, + testFn: (server: Server, port: number) => Promise ): Promise { return new Promise((resolve, reject) => { - let retries = 0; - let currentPort = port; - - const tryListen = () => { - const httpServer = server.listen(currentPort); + // Use port 0 to let the OS assign an available port + const httpServer = server.listen(0); - httpServer.on('listening', async () => { - try { - await testFn(httpServer, currentPort); - resolve(); - } catch (error) { - reject(error); - } finally { - httpServer.close(); - } - }); - - httpServer.on('error', (error: NodeJS.ErrnoException) => { - if (error.code === 'EADDRINUSE' && retries < maxRetries) { - retries++; - currentPort = getRandomPort(); - httpServer.close(); - tryListen(); - } else { - reject(error); - } - }); - }; + httpServer.on('error', (error) => { + reject(error); + }); - tryListen(); + httpServer.on('listening', async () => { + const address = httpServer.address() as AddressInfo; + const assignedPort = address.port; + try { + await testFn(httpServer, assignedPort); + resolve(); + } catch (error) { + reject(error); + } finally { + httpServer.close(); + } + }); }); } diff --git a/test/setup.js b/test/setup.js index 7642d7b6..9780fb17 100644 --- a/test/setup.js +++ b/test/setup.js @@ -3,3 +3,9 @@ // eslint-disable-next-line no-undef process.env.CODEGEN_TELEMETRY_DISABLED = '1'; +// Suppress Node.js deprecation warnings (e.g., DEP0040 for punycode) +// These come from transitive dependencies (tr46 via node-fetch via @asyncapi/parser) +// and pollute stderr in CLI tests that capture output +// eslint-disable-next-line no-undef +process.noDeprecation = true; +