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
32 changes: 29 additions & 3 deletions src/cli/commands/add/__tests__/validate.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -614,17 +614,43 @@ describe('validate', () => {
expect(result.error).toContain('not applicable');
});

it('rejects --outbound-auth for api-gateway type', async () => {
it('rejects --outbound-auth oauth for api-gateway type', async () => {
const result = await validateAddGatewayTargetOptions({
name: 'my-api',
type: 'api-gateway',
restApiId: 'abc123',
stage: 'prod',
gateway: 'my-gateway',
outboundAuthType: 'NONE',
outboundAuthType: 'OAUTH',
});
expect(result.valid).toBe(false);
expect(result.error).toContain('not applicable');
expect(result.error).toContain('OAuth is not supported');
});

it('accepts --outbound-auth api-key with --credential-name for api-gateway type', async () => {
const result = await validateAddGatewayTargetOptions({
name: 'my-api',
type: 'api-gateway',
restApiId: 'abc123',
stage: 'prod',
gateway: 'my-gateway',
outboundAuthType: 'API_KEY',
credentialName: 'my-key',
});
expect(result.valid).toBe(true);
});

it('rejects --outbound-auth api-key without --credential-name for api-gateway type', async () => {
const result = await validateAddGatewayTargetOptions({
name: 'my-api',
type: 'api-gateway',
restApiId: 'abc123',
stage: 'prod',
gateway: 'my-gateway',
outboundAuthType: 'API_KEY',
});
expect(result.valid).toBe(false);
expect(result.error).toContain('--credential-name is required');
});

it('rejects --host with mcp-server type', async () => {
Expand Down
11 changes: 7 additions & 4 deletions src/cli/commands/add/validate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -281,10 +281,13 @@ export async function validateAddGatewayTargetOptions(options: AddGatewayTargetO
return { valid: false, error: '--language is not applicable for api-gateway type' };
}
if (options.outboundAuthType) {
return { valid: false, error: '--outbound-auth is not applicable for api-gateway type' };
}
if (options.credentialName) {
return { valid: false, error: '--credential-name is not applicable for api-gateway type' };
const authLower = options.outboundAuthType.toLowerCase();
if (authLower === 'oauth') {
return { valid: false, error: 'OAuth is not supported for api-gateway type' };
}
if ((authLower === 'api-key' || authLower === 'api_key') && !options.credentialName) {
return { valid: false, error: '--credential-name is required with --outbound-auth api-key' };
}
}
if (options.oauthClientId || options.oauthClientSecret || options.oauthDiscoveryUrl || options.oauthScopes) {
return { valid: false, error: 'OAuth options are not applicable for api-gateway type' };
Expand Down
12 changes: 12 additions & 0 deletions src/cli/primitives/GatewayTargetPrimitive.ts
Original file line number Diff line number Diff line change
Expand Up @@ -275,6 +275,7 @@ export class GatewayTargetPrimitive extends BasePrimitive<AddGatewayTargetOption
const outboundAuthMap: Record<string, 'OAUTH' | 'API_KEY' | 'NONE'> = {
oauth: 'OAUTH',
'api-key': 'API_KEY',
api_key: 'API_KEY',
none: 'NONE',
};

Expand All @@ -296,6 +297,16 @@ export class GatewayTargetPrimitive extends BasePrimitive<AddGatewayTargetOption
},
]
: undefined,
...(cliOptions.outboundAuthType
? {
outboundAuth: {
type: (outboundAuthMap[cliOptions.outboundAuthType.toLowerCase()] ?? 'NONE') as
| 'API_KEY'
| 'NONE',
credentialName: cliOptions.credentialName,
},
}
: {}),
};
const result = await this.createApiGatewayTarget(config);
const output = { success: true, toolName: result.toolName };
Expand Down Expand Up @@ -507,6 +518,7 @@ export class GatewayTargetPrimitive extends BasePrimitive<AddGatewayTargetOption
toolFilters: config.toolFilters ?? [{ filterPath: '/*', methods: ['GET'] }],
},
},
...(config.outboundAuth && { outboundAuth: config.outboundAuth }),
};

gateway.targets.push(target);
Expand Down
10 changes: 5 additions & 5 deletions src/cli/tui/hooks/useCdkPreflight.ts
Original file line number Diff line number Diff line change
Expand Up @@ -68,8 +68,8 @@ export interface PreflightResult {
missingCredentials: MissingCredential[];
/** KMS key ARN used for identity token vault encryption */
identityKmsKeyArn?: string;
/** OAuth credential ARNs from pre-deploy setup */
oauthCredentials: Record<string, { credentialProviderArn: string; clientSecretArn?: string; callbackUrl?: string }>;
/** Credential ARNs (API key + OAuth) from pre-deploy setup */
allCredentials: Record<string, { credentialProviderArn: string; clientSecretArn?: string; callbackUrl?: string }>;
startPreflight: () => Promise<void>;
confirmTeardown: () => void;
cancelTeardown: () => void;
Expand Down Expand Up @@ -128,7 +128,7 @@ export function useCdkPreflight(options: PreflightOptions): PreflightResult {
const [runtimeCredentials, setRuntimeCredentials] = useState<SecureCredentials | null>(null);
const [skipIdentitySetup, setSkipIdentitySetup] = useState(false);
const [identityKmsKeyArn, setIdentityKmsKeyArn] = useState<string | undefined>(undefined);
const [oauthCredentials, setOauthCredentials] = useState<
const [allCredentials, setAllCredentials] = useState<
Record<string, { credentialProviderArn: string; clientSecretArn?: string; callbackUrl?: string }>
>({});
const [teardownConfirmed, setTeardownConfirmed] = useState(false);
Expand Down Expand Up @@ -723,7 +723,6 @@ export function useCdkPreflight(options: PreflightOptions): PreflightResult {
};
}
}
setOauthCredentials(creds);
Object.assign(deployedCredentials, creds);

logger.endStep('success');
Expand All @@ -732,6 +731,7 @@ export function useCdkPreflight(options: PreflightOptions): PreflightResult {

// Write partial deployed state with credential ARNs before CDK synth
if (Object.keys(deployedCredentials).length > 0) {
setAllCredentials(deployedCredentials);
const configIO = new ConfigIO();
const target = context.awsTargets[0];
const existingState = await configIO.readDeployedState().catch(() => ({ targets: {} }) as DeployedState);
Expand Down Expand Up @@ -890,7 +890,7 @@ export function useCdkPreflight(options: PreflightOptions): PreflightResult {
hasCredentialsError,
missingCredentials,
identityKmsKeyArn,
oauthCredentials,
allCredentials,
startPreflight,
confirmTeardown,
cancelTeardown,
Expand Down
8 changes: 4 additions & 4 deletions src/cli/tui/screens/deploy/useDeployFlow.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ export interface PreSynthesized {
stackNames: string[];
switchableIoHost?: SwitchableIoHost;
identityKmsKeyArn?: string;
oauthCredentials?: Record<string, { credentialProviderArn: string; clientSecretArn?: string; callbackUrl?: string }>;
allCredentials?: Record<string, { credentialProviderArn: string; clientSecretArn?: string; callbackUrl?: string }>;
}

interface DeployFlowOptions {
Expand Down Expand Up @@ -114,7 +114,7 @@ export function useDeployFlow(options: DeployFlowOptions = {}): DeployFlowState
const stackNames = preSynthesized?.stackNames ?? preflight.stackNames;
const switchableIoHost = preSynthesized?.switchableIoHost ?? preflight.switchableIoHost;
const identityKmsKeyArn = preSynthesized?.identityKmsKeyArn ?? preflight.identityKmsKeyArn;
const oauthCredentials = preSynthesized?.oauthCredentials ?? preflight.oauthCredentials;
const allCredentials = preSynthesized?.allCredentials ?? preflight.allCredentials;

const [publishAssetsStep, setPublishAssetsStep] = useState<Step>({ label: 'Publish assets', status: 'pending' });
const [deployStep, setDeployStep] = useState<Step>({ label: 'Deploy to AWS', status: 'pending' });
Expand Down Expand Up @@ -269,7 +269,7 @@ export function useDeployFlow(options: DeployFlowOptions = {}): DeployFlowState
existingState,
identityKmsKeyArn,
memories,
credentials: Object.keys(oauthCredentials).length > 0 ? oauthCredentials : undefined,
credentials: Object.keys(allCredentials).length > 0 ? allCredentials : undefined,
});
await configIO.writeDeployedState(deployedState);

Expand All @@ -282,7 +282,7 @@ export function useDeployFlow(options: DeployFlowOptions = {}): DeployFlowState
if (allStatuses.length > 0) {
setTargetStatuses(allStatuses);
}
}, [context, stackNames, logger, identityKmsKeyArn, oauthCredentials]);
}, [context, stackNames, logger, identityKmsKeyArn, allCredentials]);

// Start deploy when preflight completes OR when shouldStartDeploy is set
useEffect(() => {
Expand Down
45 changes: 31 additions & 14 deletions src/cli/tui/screens/mcp/AddGatewayTargetFlow.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,11 @@ import { AddIdentityScreen } from '../identity/AddIdentityScreen';
import type { AddIdentityConfig } from '../identity/types';
import { useCreateIdentity, useExistingCredentials, useExistingIdentityNames } from '../identity/useCreateIdentity';
import { AddGatewayTargetScreen } from './AddGatewayTargetScreen';
import type { AddGatewayTargetConfig, GatewayTargetWizardState } from './types';
import type { AddGatewayTargetConfig, AddGatewayTargetStep, GatewayTargetWizardState } from './types';
import React, { useCallback, useEffect, useMemo, useState } from 'react';

type FlowState =
| { name: 'create-wizard' }
| { name: 'create-wizard'; resumeConfig?: GatewayTargetWizardState; resumeStep?: AddGatewayTargetStep }
| { name: 'creating-credential'; pendingConfig: GatewayTargetWizardState }
| { name: 'create-success'; toolName: string; projectPath: string; loading?: boolean; loadingMessage?: string }
| { name: 'error'; message: string };
Expand Down Expand Up @@ -45,6 +45,11 @@ export function AddGatewayTargetFlow({
[credentials]
);

const apiKeyCredentialNames = useMemo(
() => credentials.filter(c => c.type === 'ApiKeyCredentialProvider').map(c => c.name),
[credentials]
);

// In non-interactive mode, exit after success (but not while loading)
useEffect(() => {
if (!isInteractive && flow.name === 'create-success' && !flow.loading) {
Expand Down Expand Up @@ -110,22 +115,22 @@ export function AddGatewayTargetFlow({
void createIdentity(createConfig).then(result => {
if (result.ok && flow.name === 'creating-credential') {
const pending = flow.pendingConfig;
// Credential creation is only reachable from the mcpServer outbound-auth step
handleCreateComplete({
targetType: 'mcpServer',
name: pending.name,
description: pending.description ?? `Tool for ${pending.name}`,
endpoint: pending.endpoint!,
gateway: pending.gateway!,
toolDefinition: pending.toolDefinition!,
outboundAuth: { type: 'OAUTH', credentialName: result.result.name },
const authType = pending.targetType === 'apiGateway' ? 'API_KEY' : 'OAUTH';
// Resume wizard at confirm step with the new credential attached
setFlow({
name: 'create-wizard',
resumeConfig: {
...pending,
outboundAuth: { type: authType, credentialName: result.result.name },
},
resumeStep: 'confirm',
});
} else if (!result.ok) {
setFlow({ name: 'error', message: result.error });
}
});
},
[flow, createIdentity, handleCreateComplete]
[flow, createIdentity]
);

// Create wizard
Expand All @@ -135,21 +140,33 @@ export function AddGatewayTargetFlow({
existingGateways={existingGateways}
existingToolNames={existingToolNames}
existingOAuthCredentialNames={oauthCredentialNames}
existingApiKeyCredentialNames={apiKeyCredentialNames}
onComplete={handleCreateComplete}
onCreateCredential={handleCreateCredential}
onExit={onBack}
initialConfig={flow.resumeConfig}
initialStep={flow.resumeStep}
/>
);
}

// Creating credential via identity screen
if (flow.name === 'creating-credential') {
const resumeStep = flow.pendingConfig.targetType === 'apiGateway' ? 'api-gateway-auth' : 'outbound-auth';
return (
<AddIdentityScreen
existingIdentityNames={existingIdentityNames}
onComplete={handleIdentityComplete}
onExit={() => setFlow({ name: 'create-wizard' })}
initialType="OAuthCredentialProvider"
onExit={() =>
setFlow({
name: 'create-wizard',
resumeConfig: flow.pendingConfig,
resumeStep: resumeStep,
})
}
initialType={
flow.pendingConfig.targetType === 'apiGateway' ? 'ApiKeyCredentialProvider' : 'OAuthCredentialProvider'
}
/>
);
}
Expand Down
Loading
Loading