@@ -25,6 +25,7 @@ import {
2525import { getServiceAccountCoverageSentence } from '@/lib/integrations/credential-display'
2626import {
2727 ATLASSIAN_SERVICE_ACCOUNT_PROVIDER_ID ,
28+ OCI_API_KEY_SERVICE_ACCOUNT_PROVIDER_ID ,
2829 SLACK_CUSTOM_BOT_PROVIDER_ID ,
2930} from '@/lib/oauth/types'
3031import { ClientCredentialAccountModal } from '@/app/workspace/[workspaceId]/integrations/components/connect-service-account-modal/client-credential-account-modal'
@@ -44,13 +45,15 @@ export type ServiceAccountProviderId =
4445 | typeof GOOGLE_SERVICE_ACCOUNT_PROVIDER_ID
4546 | typeof ATLASSIAN_SERVICE_ACCOUNT_PROVIDER_ID
4647 | typeof SLACK_CUSTOM_BOT_PROVIDER_ID
48+ | typeof OCI_API_KEY_SERVICE_ACCOUNT_PROVIDER_ID
4749 | TokenServiceAccountProviderId
4850 | ClientCredentialAccountProviderId
4951
50- /** Sim setup guides for each provider, docked bottom-left of each modal. */
5152const GOOGLE_SERVICE_ACCOUNT_DOCS_URL = 'https://docs.sim.ai/integrations/google-service-account'
5253const ATLASSIAN_SERVICE_ACCOUNT_DOCS_URL =
5354 'https://docs.sim.ai/integrations/atlassian-service-account'
55+ const OCI_API_KEY_DOCS_URL =
56+ 'https://docs.oracle.com/en-us/iaas/Content/API/Concepts/apisigningkey.htm'
5457
5558function openDocs ( url : string ) : void {
5659 window . open ( url , '_blank' , 'noopener,noreferrer' )
@@ -125,18 +128,6 @@ interface ConnectServiceAccountModalProps {
125128 onCreated ?: ( credentialId : string ) => void
126129}
127130
128- /**
129- * Connect-service-account modal mounted from the per-integration detail page.
130- * Self-contained: takes the resolved SA provider + service metadata from the
131- * caller and submits via `useCreateWorkspaceCredential`. Branches the body
132- * based on `serviceAccountProviderId`:
133- *
134- * - `google-service-account`: JSON-paste + drag/drop. Validated client-side
135- * against {@link serviceAccountJsonSchema} before submitting.
136- * - `atlassian-service-account`: API token + site domain. Validated by the
137- * server against the Atlassian API; user-facing errors are mapped from the
138- * route's `error.code`.
139- */
140131export function ConnectServiceAccountModal ( {
141132 open,
142133 onOpenChange,
@@ -211,6 +202,22 @@ export function ConnectServiceAccountModal({
211202 />
212203 )
213204 }
205+ if ( serviceAccountProviderId === OCI_API_KEY_SERVICE_ACCOUNT_PROVIDER_ID ) {
206+ return (
207+ < OciApiKeyServiceAccountModal
208+ key = { `${ credentialId ?? 'new' } :${ open ? 'open' : 'closed' } ` }
209+ open = { open }
210+ onOpenChange = { onOpenChange }
211+ workspaceId = { workspaceId }
212+ serviceName = { serviceName }
213+ serviceIcon = { serviceIcon }
214+ credentialId = { credentialId }
215+ initialDisplayName = { credentialDisplayName }
216+ initialDescription = { credentialDescription }
217+ onCreated = { onCreated }
218+ />
219+ )
220+ }
214221 return (
215222 < GoogleServiceAccountModal
216223 open = { open }
@@ -241,6 +248,196 @@ interface ProviderModalProps {
241248 onCreated ?: ( credentialId : string ) => void
242249}
243250
251+ function OciApiKeyServiceAccountModal ( {
252+ open,
253+ onOpenChange,
254+ workspaceId,
255+ serviceName,
256+ serviceIcon : ServiceIcon ,
257+ credentialId,
258+ initialDisplayName,
259+ initialDescription,
260+ onCreated,
261+ } : ProviderModalProps ) {
262+ const [ tenancyOcid , setTenancyOcid ] = useState ( '' )
263+ const [ userOcid , setUserOcid ] = useState ( '' )
264+ const [ fingerprint , setFingerprint ] = useState ( '' )
265+ const [ privateKey , setPrivateKey ] = useState ( '' )
266+ const [ privateKeyPassphrase , setPrivateKeyPassphrase ] = useState ( '' )
267+ const [ region , setRegion ] = useState ( '' )
268+ const [ displayName , setDisplayName ] = useState ( initialDisplayName ?? '' )
269+ const [ description , setDescription ] = useState ( initialDescription ?? '' )
270+ const [ error , setError ] = useState < string | null > ( null )
271+ const createCredential = useCreateWorkspaceCredential ( )
272+ const updateCredential = useUpdateWorkspaceCredential ( )
273+
274+ const isPending = createCredential . isPending || updateCredential . isPending
275+ const isDisabled =
276+ ! tenancyOcid . trim ( ) ||
277+ ! userOcid . trim ( ) ||
278+ ! fingerprint . trim ( ) ||
279+ ! privateKey . trim ( ) ||
280+ ! region . trim ( ) ||
281+ isPending
282+
283+ const clearError = ( ) => {
284+ if ( error ) setError ( null )
285+ }
286+
287+ const handleSubmit = async ( ) => {
288+ setError ( null )
289+ if ( isDisabled ) return
290+ const fields = {
291+ tenancyOcid : tenancyOcid . trim ( ) ,
292+ userOcid : userOcid . trim ( ) ,
293+ fingerprint : fingerprint . trim ( ) ,
294+ privateKey,
295+ ...( privateKeyPassphrase . length > 0 ? { privateKeyPassphrase } : { } ) ,
296+ region : region . trim ( ) ,
297+ displayName : displayName . trim ( ) || undefined ,
298+ description : description . trim ( ) || undefined ,
299+ }
300+ try {
301+ let connectedCredentialId = credentialId
302+ if ( credentialId ) {
303+ await updateCredential . mutateAsync ( { credentialId, ...fields } )
304+ } else {
305+ const created = await createCredential . mutateAsync ( {
306+ workspaceId,
307+ type : 'service_account' ,
308+ providerId : OCI_API_KEY_SERVICE_ACCOUNT_PROVIDER_ID ,
309+ ...fields ,
310+ } )
311+ connectedCredentialId = created . credential . id
312+ }
313+ if ( connectedCredentialId ) onCreated ?.( connectedCredentialId )
314+ onOpenChange ( false )
315+ } catch ( err : unknown ) {
316+ setError ( getErrorMessage ( err , 'Failed to add OCI API-key credential' ) )
317+ logger . error ( 'Failed to add OCI API-key credential' , err )
318+ }
319+ }
320+
321+ return (
322+ < ChipModal
323+ open = { open }
324+ onOpenChange = { onOpenChange }
325+ srTitle = { `Add ${ serviceName } API key` }
326+ dismissDisabled = { isPending }
327+ >
328+ < ChipModalHeader icon = { withBrandIcon ( ServiceIcon ) } onClose = { ( ) => onOpenChange ( false ) } >
329+ Add { serviceName } API key
330+ </ ChipModalHeader >
331+ < ChipModalBody >
332+ < ChipModalField
333+ type = 'input'
334+ title = 'Tenancy OCID'
335+ value = { tenancyOcid }
336+ onChange = { ( value ) => {
337+ setTenancyOcid ( value )
338+ clearError ( )
339+ } }
340+ placeholder = 'ocid1.tenancy.oc1..'
341+ autoComplete = 'off'
342+ mono
343+ required
344+ />
345+ < ChipModalField
346+ type = 'input'
347+ title = 'User OCID'
348+ value = { userOcid }
349+ onChange = { ( value ) => {
350+ setUserOcid ( value )
351+ clearError ( )
352+ } }
353+ placeholder = 'ocid1.user.oc1..'
354+ autoComplete = 'off'
355+ mono
356+ required
357+ />
358+ < ChipModalField
359+ type = 'input'
360+ title = 'Fingerprint'
361+ value = { fingerprint }
362+ onChange = { ( value ) => {
363+ setFingerprint ( value )
364+ clearError ( )
365+ } }
366+ placeholder = '00:11:22:33:44:55:66:77:88:99:aa:bb:cc:dd:ee:ff'
367+ autoComplete = 'off'
368+ mono
369+ required
370+ />
371+ < ChipModalField
372+ type = 'textarea'
373+ title = 'Private key'
374+ value = { privateKey }
375+ onChange = { ( value ) => {
376+ setPrivateKey ( value )
377+ clearError ( )
378+ } }
379+ placeholder = '-----BEGIN PRIVATE KEY-----'
380+ minHeight = { 120 }
381+ mono
382+ required
383+ />
384+ < ChipModalField
385+ type = 'input'
386+ inputType = 'password'
387+ title = 'Private-key passphrase'
388+ value = { privateKeyPassphrase }
389+ onChange = { ( value ) => {
390+ setPrivateKeyPassphrase ( value )
391+ clearError ( )
392+ } }
393+ placeholder = 'Optional'
394+ autoComplete = 'new-password'
395+ />
396+ < ChipModalField
397+ type = 'input'
398+ title = 'Region'
399+ value = { region }
400+ onChange = { ( value ) => {
401+ setRegion ( value )
402+ clearError ( )
403+ } }
404+ placeholder = 'us-ashburn-1'
405+ autoComplete = 'off'
406+ mono
407+ required
408+ />
409+ < ChipModalField
410+ type = 'input'
411+ title = 'Display name'
412+ value = { displayName }
413+ onChange = { setDisplayName }
414+ placeholder = 'Defaults to the OCI user OCID'
415+ autoComplete = 'off'
416+ />
417+ < ChipModalField
418+ type = 'textarea'
419+ title = 'Description'
420+ value = { description }
421+ onChange = { setDescription }
422+ placeholder = 'Optional description'
423+ maxLength = { 500 }
424+ minHeight = { 80 }
425+ />
426+ < ChipModalError > { error } </ ChipModalError >
427+ </ ChipModalBody >
428+ < ChipModalFooter
429+ onCancel = { ( ) => onOpenChange ( false ) }
430+ secondaryActions = { [ { label : 'Setup guide' , onClick : ( ) => openDocs ( OCI_API_KEY_DOCS_URL ) } ] }
431+ primaryAction = { {
432+ label : isPending ? 'Adding...' : credentialId ? 'Reconnect' : 'Add API key' ,
433+ onClick : handleSubmit ,
434+ disabled : isDisabled ,
435+ } }
436+ />
437+ </ ChipModal >
438+ )
439+ }
440+
244441/**
245442 * Google service-account flow. Accepts the raw JSON key (paste or drag/drop)
246443 * and validates against the shared `serviceAccountJsonSchema` so the same
0 commit comments