Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 20 additions & 6 deletions src/codegen/generators/typescript/channels/openapi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,11 @@ import {
} from './types';
import {ConstrainedObjectModel} from '@asyncapi/modelina';
import {collectProtocolDependencies} from './utils';
import {renderHttpFetchClient, renderHttpCommonTypes} from './protocols/http';
import {
renderHttpFetchClient,
renderHttpCommonTypes,
analyzeSecuritySchemes
} from './protocols/http';
import {getMessageTypeAndModule} from './utils';
import {camelCase} from '../utils';
import {createMissingInputDocumentError} from '../../../errors';
Expand Down Expand Up @@ -92,11 +96,17 @@ export async function generateTypeScriptChannelsForOpenAPI(
importExtension
);

// OAuth2 request handling is only generated when the spec defines an OAuth2
// scheme; otherwise the narrowed AuthConfig union would make that code fail to
// type-check.
const oauth2Enabled = analyzeSecuritySchemes(securitySchemes).oauth2;

// Process all operations and collect renders
const renders = processOpenAPIOperations(
openapiDocument,
payloads,
parameters
parameters,
oauth2Enabled
);

// Generate common types once (stateless check)
Expand Down Expand Up @@ -129,7 +139,8 @@ export async function generateTypeScriptChannelsForOpenAPI(
function processOpenAPIOperations(
openapiDocument: OpenAPIDocument,
payloads: TypeScriptPayloadRenderType,
parameters: TypeScriptParameterRenderType
parameters: TypeScriptParameterRenderType,
oauth2Enabled: boolean
): ReturnType<typeof renderHttpFetchClient>[] {
const renders: ReturnType<typeof renderHttpFetchClient>[] = [];

Expand All @@ -144,7 +155,8 @@ function processOpenAPIOperations(
method,
path,
payloads,
parameters
parameters,
oauth2Enabled
);
if (render) {
renders.push(render);
Expand All @@ -163,7 +175,8 @@ function processOperation(
method: HttpMethod,
path: string,
payloads: TypeScriptPayloadRenderType,
parameters: TypeScriptParameterRenderType
parameters: TypeScriptParameterRenderType,
oauth2Enabled: boolean
): ReturnType<typeof renderHttpFetchClient> | undefined {
// eslint-disable-next-line security/detect-object-injection
const operation = (pathItem as Record<string, unknown>)[method] as
Expand Down Expand Up @@ -250,7 +263,8 @@ function processOperation(
| undefined,
includesStatusCodes: replyIncludesStatusCodes,
description,
deprecated
deprecated,
oauth2Enabled
});
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,8 @@ export function renderHttpFetchClient({
functionName = `${method.toLowerCase()}${subName}`,
includesStatusCodes = false,
description,
deprecated
deprecated,
oauth2Enabled = true
}: RenderHttpParameters): HttpRenderType {
const messageType = requestMessageModule
? `${requestMessageModule}.${requestMessageType}`
Expand Down Expand Up @@ -66,7 +67,8 @@ export function renderHttpFetchClient({
method,
servers,
includesStatusCodes,
jsDoc
jsDoc,
oauth2Enabled
});

const code = `${contextInterface}
Expand Down Expand Up @@ -134,6 +136,7 @@ function generateFunctionImplementation(params: {
servers: string[];
includesStatusCodes: boolean;
jsDoc: string;
oauth2Enabled: boolean;
}): string {
const {
functionName,
Expand All @@ -147,7 +150,8 @@ function generateFunctionImplementation(params: {
method,
servers,
includesStatusCodes,
jsDoc
jsDoc,
oauth2Enabled
} = params;

const defaultServer = servers[0] ?? "'localhost:3000'";
Expand Down Expand Up @@ -183,6 +187,42 @@ function generateFunctionImplementation(params: {
// Generate default context for optional context parameter
const contextDefault = !hasBody && !hasParameters ? ' = {}' : '';

// OAuth2 request handling is only emitted when the API actually defines an
// OAuth2 scheme; otherwise these branches reference fields/functions that the
// narrowed AuthConfig union no longer carries and would fail to type-check.
const oauth2ValidateBlock = oauth2Enabled
? ` // Validate OAuth2 config if present
if (config.auth?.type === 'oauth2' && AUTH_FEATURES.oauth2) {
validateOAuth2Config(config.auth);
}

`
: '';

const oauth2TokenBlock = oauth2Enabled
? `
// 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');
}
}
`
: '';

return `${jsDoc}
async function ${functionName}(context: ${contextInterfaceName}${contextDefault}): Promise<HttpClientResponse<${replyType}>> {
// Apply defaults
Expand All @@ -192,12 +232,7 @@ async function ${functionName}(context: ${contextInterfaceName}${contextDefault}
...context,
};

// Validate OAuth2 config if present
if (config.auth?.type === 'oauth2' && AUTH_FEATURES.oauth2) {
validateOAuth2Config(config.auth);
}

// Build headers
${oauth2ValidateBlock} // Build headers
${headersInit}

// Build URL
Expand Down Expand Up @@ -241,27 +276,7 @@ async function ${functionName}(context: ${contextInterfaceName}${contextDefault}
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');
}
}

${oauth2TokenBlock}
// Handle error responses
if (!response.ok) {
handleHttpError(response.status, response.statusText);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,8 @@ import {
analyzeSecuritySchemes,
escapeStringForCodeGen,
getApiKeyDefaults,
renderApplyAuthCases,
renderOAuth2Helpers,
renderOAuth2Stubs,
renderSecurityTypes
} from './security';

Expand All @@ -25,6 +25,35 @@ export function renderHttpCommonTypes(
): string {
const requirements = analyzeSecuritySchemes(securitySchemes);
const securityTypes = renderSecurityTypes(securitySchemes, requirements);
const applyAuthCases = renderApplyAuthCases(requirements);

// Only emit the AUTH_FEATURES flag when OAuth2 code is generated, and the
// API_KEY_DEFAULTS const when an apiKey case is generated - otherwise they
// become unused declarations in the output.
const authFeaturesBlock = requirements.oauth2
? `
/**
* Feature flags indicating which auth types are available.
* Used internally to conditionally call auth-specific helpers.
*/
const AUTH_FEATURES = {
oauth2: ${requirements.oauth2}
} as const;
`
: '';

const apiKeyDefaultsBlock = requirements.apiKey
? `
/**
* Default values for API key authentication derived from the spec.
* These match the defaults documented in the ApiKeyAuth interface.
*/
const API_KEY_DEFAULTS = {
name: '${escapeStringForCodeGen(getApiKeyDefaults(requirements.apiKeySchemes).name)}',
in: '${escapeStringForCodeGen(getApiKeyDefaults(requirements.apiKeySchemes).in)}' as 'header' | 'query' | 'cookie'
} as const;
`
: '';

return `// ============================================================================
// Common Types - Shared across all HTTP client functions
Expand Down Expand Up @@ -108,24 +137,7 @@ export interface TokenResponse {
}

${securityTypes}

/**
* Feature flags indicating which auth types are available.
* Used internally to conditionally call auth-specific helpers.
*/
const AUTH_FEATURES = {
oauth2: ${requirements.oauth2}
} 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: '${escapeStringForCodeGen(getApiKeyDefaults(requirements.apiKeySchemes).name)}',
in: '${escapeStringForCodeGen(getApiKeyDefaults(requirements.apiKeySchemes).in)}' as 'header' | 'query' | 'cookie'
} as const;

${authFeaturesBlock}${apiKeyDefaultsBlock}
// ============================================================================
// Pagination Types
// ============================================================================
Expand Down Expand Up @@ -307,39 +319,7 @@ function applyAuth(
if (!auth) return { headers, url };

switch (auth.type) {
case 'bearer':
headers['Authorization'] = \`Bearer \${auth.token}\`;
break;

case 'basic': {
const credentials = Buffer.from(\`\${auth.username}:\${auth.password}\`).toString('base64');
headers['Authorization'] = \`Basic \${credentials}\`;
break;
}

case 'apiKey': {
const keyName = auth.name ?? API_KEY_DEFAULTS.name;
const keyIn = auth.in ?? API_KEY_DEFAULTS.in;

if (keyIn === 'header') {
headers[keyName] = auth.key;
} 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;
}

case 'oauth2': {
// If we have an access token, use it directly
// Token flows (client_credentials, password) are handled separately
if (auth.accessToken) {
headers['Authorization'] = \`Bearer \${auth.accessToken}\`;
}
break;
}
${applyAuthCases}
}

return { headers, url };
Expand Down Expand Up @@ -805,7 +785,7 @@ function applyTypedHeaders(

return headers;
}
${requirements.oauth2 ? renderOAuth2Helpers() : renderOAuth2Stubs()}
${requirements.oauth2 ? renderOAuth2Helpers() : ''}
// ============================================================================
// Generated HTTP Client Functions
// ============================================================================`;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,6 @@ export {
escapeStringForCodeGen,
getApiKeyDefaults,
renderOAuth2Helpers,
renderOAuth2Stubs,
renderSecurityTypes,
type AuthTypeRequirements
} from './security';
Expand Down
Loading
Loading