11import { ToolNameSchema } from '../../../../schema' ;
2- import { ConfirmReview , Panel , Screen , StepIndicator , TextInput , WizardSelect } from '../../components' ;
2+ import { ConfirmReview , Panel , Screen , SecretInput , StepIndicator , TextInput , WizardSelect } from '../../components' ;
33import type { SelectableItem } from '../../components' ;
44import { HELP_TEXT } from '../../constants' ;
55import { useListNavigation } from '../../hooks' ;
66import { generateUniqueName } from '../../utils' ;
7+ import { useCreateIdentity , useExistingCredentialNames } from '../identity/useCreateIdentity.js' ;
78import type { AddGatewayTargetConfig , ComputeHost , TargetLanguage } from './types' ;
89import {
910 COMPUTE_HOST_OPTIONS ,
1011 MCP_TOOL_STEP_LABELS ,
12+ OUTBOUND_AUTH_OPTIONS ,
1113 SKIP_FOR_NOW ,
1214 SOURCE_OPTIONS ,
1315 TARGET_LANGUAGE_OPTIONS ,
1416} from './types' ;
1517import { useAddGatewayTargetWizard } from './useAddGatewayTargetWizard' ;
1618import { Box , Text } from 'ink' ;
17- import React , { useMemo } from 'react' ;
19+ import React , { useMemo , useState } from 'react' ;
1820
1921interface AddGatewayTargetScreenProps {
2022 existingGateways : string [ ] ;
@@ -30,6 +32,17 @@ export function AddGatewayTargetScreen({
3032 onExit,
3133} : AddGatewayTargetScreenProps ) {
3234 const wizard = useAddGatewayTargetWizard ( existingGateways ) ;
35+ const { names : existingCredentialNames } = useExistingCredentialNames ( ) ;
36+ const { createIdentity } = useCreateIdentity ( ) ;
37+
38+ // Outbound auth sub-step state
39+ const [ outboundAuthType , setOutboundAuthTypeLocal ] = useState < string | null > ( null ) ;
40+ const [ credentialName , setCredentialNameLocal ] = useState < string | null > ( null ) ;
41+ const [ isCreatingCredential , setIsCreatingCredential ] = useState ( false ) ;
42+ const [ oauthSubStep , setOauthSubStep ] = useState < 'name' | 'client-id' | 'client-secret' | 'discovery-url' > ( 'name' ) ;
43+ const [ oauthFields , setOauthFields ] = useState ( { name : '' , clientId : '' , clientSecret : '' , discoveryUrl : '' } ) ;
44+ const [ apiKeySubStep , setApiKeySubStep ] = useState < 'name' | 'api-key' > ( 'name' ) ;
45+ const [ apiKeyFields , setApiKeyFields ] = useState ( { name : '' , apiKey : '' } ) ;
3346
3447 const sourceItems : SelectableItem [ ] = useMemo (
3548 ( ) => SOURCE_OPTIONS . map ( o => ( { id : o . id , title : o . title , description : o . description } ) ) ,
@@ -54,10 +67,26 @@ export function AddGatewayTargetScreen({
5467 [ ]
5568 ) ;
5669
70+ const outboundAuthItems : SelectableItem [ ] = useMemo (
71+ ( ) => OUTBOUND_AUTH_OPTIONS . map ( o => ( { id : o . id , title : o . title , description : o . description } ) ) ,
72+ [ ]
73+ ) ;
74+
75+ const credentialItems : SelectableItem [ ] = useMemo ( ( ) => {
76+ const items : SelectableItem [ ] = [
77+ { id : 'create-new' , title : 'Create new credential' , description : 'Create a new credential inline' } ,
78+ ] ;
79+ existingCredentialNames . forEach ( name => {
80+ items . push ( { id : name , title : name , description : 'Use existing credential' } ) ;
81+ } ) ;
82+ return items ;
83+ } , [ existingCredentialNames ] ) ;
84+
5785 const isSourceStep = wizard . step === 'source' ;
5886 const isLanguageStep = wizard . step === 'language' ;
5987 const isGatewayStep = wizard . step === 'gateway' ;
6088 const isHostStep = wizard . step === 'host' ;
89+ const isOutboundAuthStep = wizard . step === 'outbound-auth' ;
6190 const isTextStep = wizard . step === 'name' || wizard . step === 'endpoint' ;
6291 const isConfirmStep = wizard . step === 'confirm' ;
6392 const noGatewaysAvailable = isGatewayStep && existingGateways . length === 0 ;
@@ -90,16 +119,147 @@ export function AddGatewayTargetScreen({
90119 isActive : isHostStep ,
91120 } ) ;
92121
122+ const outboundAuthNav = useListNavigation ( {
123+ items : outboundAuthItems ,
124+ onSelect : item => {
125+ const authType = item . id as 'OAUTH' | 'API_KEY' | 'NONE' ;
126+ setOutboundAuthTypeLocal ( authType ) ;
127+ if ( authType === 'NONE' ) {
128+ wizard . setOutboundAuth ( { type : 'NONE' } ) ;
129+ }
130+ } ,
131+ onExit : ( ) => wizard . goBack ( ) ,
132+ isActive : isOutboundAuthStep && ! outboundAuthType ,
133+ } ) ;
134+
135+ const credentialNav = useListNavigation ( {
136+ items : credentialItems ,
137+ onSelect : item => {
138+ if ( item . id === 'create-new' ) {
139+ setIsCreatingCredential ( true ) ;
140+ if ( outboundAuthType === 'OAUTH' ) {
141+ setOauthSubStep ( 'name' ) ;
142+ } else {
143+ setApiKeySubStep ( 'name' ) ;
144+ }
145+ } else {
146+ setCredentialNameLocal ( item . id ) ;
147+ wizard . setOutboundAuth ( { type : outboundAuthType as 'OAUTH' | 'API_KEY' , credentialName : item . id } ) ;
148+ }
149+ } ,
150+ onExit : ( ) => {
151+ setOutboundAuthTypeLocal ( null ) ;
152+ setCredentialNameLocal ( null ) ;
153+ setIsCreatingCredential ( false ) ;
154+ } ,
155+ isActive :
156+ isOutboundAuthStep &&
157+ ! ! outboundAuthType &&
158+ outboundAuthType !== 'NONE' &&
159+ ! credentialName &&
160+ ! isCreatingCredential ,
161+ } ) ;
162+
93163 useListNavigation ( {
94164 items : [ { id : 'confirm' , title : 'Confirm' } ] ,
95165 onSelect : ( ) => onComplete ( wizard . config ) ,
96166 onExit : ( ) => wizard . goBack ( ) ,
97167 isActive : isConfirmStep ,
98168 } ) ;
99169
170+ // OAuth creation handlers
171+ const handleOauthFieldSubmit = ( value : string ) => {
172+ const newFields = { ...oauthFields } ;
173+
174+ if ( oauthSubStep === 'name' ) {
175+ newFields . name = value ;
176+ setOauthFields ( newFields ) ;
177+ setOauthSubStep ( 'client-id' ) ;
178+ } else if ( oauthSubStep === 'client-id' ) {
179+ newFields . clientId = value ;
180+ setOauthFields ( newFields ) ;
181+ setOauthSubStep ( 'client-secret' ) ;
182+ } else if ( oauthSubStep === 'client-secret' ) {
183+ newFields . clientSecret = value ;
184+ setOauthFields ( newFields ) ;
185+ setOauthSubStep ( 'discovery-url' ) ;
186+ } else if ( oauthSubStep === 'discovery-url' ) {
187+ newFields . discoveryUrl = value ;
188+ setOauthFields ( newFields ) ;
189+
190+ // Create the credential
191+ void createIdentity ( {
192+ type : 'OAuthCredentialProvider' ,
193+ name : newFields . name ,
194+ clientId : newFields . clientId ,
195+ clientSecret : newFields . clientSecret ,
196+ discoveryUrl : newFields . discoveryUrl ,
197+ } ) . then ( result => {
198+ if ( result . ok ) {
199+ wizard . setOutboundAuth ( { type : 'OAUTH' , credentialName : newFields . name } ) ;
200+ } else {
201+ // Reset to credential selection on failure
202+ setIsCreatingCredential ( false ) ;
203+ setOauthSubStep ( 'name' ) ;
204+ setOauthFields ( { name : '' , clientId : '' , clientSecret : '' , discoveryUrl : '' } ) ;
205+ }
206+ } ) ;
207+ }
208+ } ;
209+
210+ const handleOauthFieldCancel = ( ) => {
211+ if ( oauthSubStep === 'name' ) {
212+ setIsCreatingCredential ( false ) ;
213+ setOauthFields ( { name : '' , clientId : '' , clientSecret : '' , discoveryUrl : '' } ) ;
214+ } else if ( oauthSubStep === 'client-id' ) {
215+ setOauthSubStep ( 'name' ) ;
216+ } else if ( oauthSubStep === 'client-secret' ) {
217+ setOauthSubStep ( 'client-id' ) ;
218+ } else if ( oauthSubStep === 'discovery-url' ) {
219+ setOauthSubStep ( 'client-secret' ) ;
220+ }
221+ } ;
222+
223+ // API Key creation handlers
224+ const handleApiKeyFieldSubmit = ( value : string ) => {
225+ const newFields = { ...apiKeyFields } ;
226+
227+ if ( apiKeySubStep === 'name' ) {
228+ newFields . name = value ;
229+ setApiKeyFields ( newFields ) ;
230+ setApiKeySubStep ( 'api-key' ) ;
231+ } else if ( apiKeySubStep === 'api-key' ) {
232+ newFields . apiKey = value ;
233+ setApiKeyFields ( newFields ) ;
234+
235+ void createIdentity ( {
236+ type : 'ApiKeyCredentialProvider' ,
237+ name : newFields . name ,
238+ apiKey : newFields . apiKey ,
239+ } ) . then ( result => {
240+ if ( result . ok ) {
241+ wizard . setOutboundAuth ( { type : 'API_KEY' , credentialName : newFields . name } ) ;
242+ } else {
243+ setIsCreatingCredential ( false ) ;
244+ setApiKeySubStep ( 'name' ) ;
245+ setApiKeyFields ( { name : '' , apiKey : '' } ) ;
246+ }
247+ } ) ;
248+ }
249+ } ;
250+
251+ const handleApiKeyFieldCancel = ( ) => {
252+ if ( apiKeySubStep === 'name' ) {
253+ setIsCreatingCredential ( false ) ;
254+ setApiKeyFields ( { name : '' , apiKey : '' } ) ;
255+ } else if ( apiKeySubStep === 'api-key' ) {
256+ setApiKeySubStep ( 'name' ) ;
257+ }
258+ } ;
259+
100260 const helpText = isConfirmStep
101261 ? HELP_TEXT . CONFIRM_CANCEL
102- : isTextStep
262+ : isTextStep || isCreatingCredential
103263 ? HELP_TEXT . TEXT_INPUT
104264 : HELP_TEXT . NAVIGATE_SELECT ;
105265
@@ -141,6 +301,107 @@ export function AddGatewayTargetScreen({
141301 />
142302 ) }
143303
304+ { isOutboundAuthStep && ! outboundAuthType && (
305+ < WizardSelect
306+ title = "Select outbound authentication"
307+ description = "How will this tool authenticate to external services?"
308+ items = { outboundAuthItems }
309+ selectedIndex = { outboundAuthNav . selectedIndex }
310+ />
311+ ) }
312+
313+ { isOutboundAuthStep &&
314+ outboundAuthType &&
315+ outboundAuthType !== 'NONE' &&
316+ ! credentialName &&
317+ ! isCreatingCredential && (
318+ < WizardSelect
319+ title = "Select credential"
320+ description = { `Choose a credential for ${ outboundAuthType } authentication` }
321+ items = { credentialItems }
322+ selectedIndex = { credentialNav . selectedIndex }
323+ />
324+ ) }
325+
326+ { isOutboundAuthStep && isCreatingCredential && outboundAuthType === 'OAUTH' && (
327+ < >
328+ { oauthSubStep === 'name' && (
329+ < TextInput
330+ key = "oauth-name"
331+ prompt = "Credential name"
332+ initialValue = { generateUniqueName ( 'MyOAuth' , existingCredentialNames ) }
333+ onSubmit = { handleOauthFieldSubmit }
334+ onCancel = { handleOauthFieldCancel }
335+ customValidation = { value => ! existingCredentialNames . includes ( value ) || 'Credential name already exists' }
336+ />
337+ ) }
338+ { oauthSubStep === 'client-id' && (
339+ < TextInput
340+ key = "oauth-client-id"
341+ prompt = "Client ID"
342+ onSubmit = { handleOauthFieldSubmit }
343+ onCancel = { handleOauthFieldCancel }
344+ customValidation = { value => value . trim ( ) . length > 0 || 'Client ID is required' }
345+ />
346+ ) }
347+ { oauthSubStep === 'client-secret' && (
348+ < SecretInput
349+ key = "oauth-client-secret"
350+ prompt = "Client Secret"
351+ onSubmit = { handleOauthFieldSubmit }
352+ onCancel = { handleOauthFieldCancel }
353+ customValidation = { value => value . trim ( ) . length > 0 || 'Client secret is required' }
354+ revealChars = { 4 }
355+ />
356+ ) }
357+ { oauthSubStep === 'discovery-url' && (
358+ < TextInput
359+ key = "oauth-discovery-url"
360+ prompt = "Discovery URL"
361+ placeholder = "https://example.com/.well-known/openid_configuration"
362+ onSubmit = { handleOauthFieldSubmit }
363+ onCancel = { handleOauthFieldCancel }
364+ customValidation = { value => {
365+ try {
366+ const url = new URL ( value ) ;
367+ if ( url . protocol !== 'http:' && url . protocol !== 'https:' ) {
368+ return 'Discovery URL must use http:// or https:// protocol' ;
369+ }
370+ return true ;
371+ } catch {
372+ return 'Must be a valid URL' ;
373+ }
374+ } }
375+ />
376+ ) }
377+ </ >
378+ ) }
379+
380+ { isOutboundAuthStep && isCreatingCredential && outboundAuthType === 'API_KEY' && (
381+ < >
382+ { apiKeySubStep === 'name' && (
383+ < TextInput
384+ key = "apikey-name"
385+ prompt = "Credential name"
386+ initialValue = { generateUniqueName ( 'MyApiKey' , existingCredentialNames ) }
387+ onSubmit = { handleApiKeyFieldSubmit }
388+ onCancel = { handleApiKeyFieldCancel }
389+ customValidation = { value => ! existingCredentialNames . includes ( value ) || 'Credential name already exists' }
390+ />
391+ ) }
392+ { apiKeySubStep === 'api-key' && (
393+ < SecretInput
394+ key = "apikey-value"
395+ prompt = "API Key"
396+ onSubmit = { handleApiKeyFieldSubmit }
397+ onCancel = { handleApiKeyFieldCancel }
398+ customValidation = { value => value . trim ( ) . length > 0 || 'API key is required' }
399+ revealChars = { 4 }
400+ />
401+ ) }
402+ </ >
403+ ) }
404+
144405 { isTextStep && (
145406 < TextInput
146407 key = { wizard . step }
@@ -183,6 +444,12 @@ export function AddGatewayTargetScreen({
183444 ...( wizard . config . gateway ? [ { label : 'Gateway' , value : wizard . config . gateway } ] : [ ] ) ,
184445 ...( ! wizard . config . gateway ? [ { label : 'Gateway' , value : '(none - assign later)' } ] : [ ] ) ,
185446 ...( wizard . config . source === 'create-new' ? [ { label : 'Host' , value : wizard . config . host } ] : [ ] ) ,
447+ ...( wizard . config . outboundAuth
448+ ? [
449+ { label : 'Auth Type' , value : wizard . config . outboundAuth . type } ,
450+ { label : 'Credential' , value : wizard . config . outboundAuth . credentialName ?? 'None' } ,
451+ ]
452+ : [ ] ) ,
186453 ...( wizard . config . source === 'create-new' ? [ { label : 'Source' , value : wizard . config . sourcePath } ] : [ ] ) ,
187454 ] }
188455 />
0 commit comments