Skip to content

Commit fe1e8d5

Browse files
Bill LeoutsakosBill Leoutsakos
authored andcommitted
feat(credentials): complete OCI API key setup
1 parent 3e476e7 commit fe1e8d5

17 files changed

Lines changed: 576 additions & 275 deletions

File tree

‎apps/docs/components/icons.tsx‎

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9394,7 +9394,7 @@ export function NewRelicIcon(props: SVGProps<SVGSVGElement>) {
93949394
)
93959395
}
93969396

9397-
export function NetSuiteIcon(props: SVGProps<SVGSVGElement>) {
9397+
export function OracleIcon(props: SVGProps<SVGSVGElement>) {
93989398
return (
93999399
<svg {...props} viewBox='0 0 93.9 59.4' xmlns='http://www.w3.org/2000/svg'>
94009400
<path
@@ -9405,6 +9405,8 @@ export function NetSuiteIcon(props: SVGProps<SVGSVGElement>) {
94059405
)
94069406
}
94079407

9408+
export const NetSuiteIcon = OracleIcon
9409+
94089410
export function WizaIcon(props: SVGProps<SVGSVGElement>) {
94099411
return (
94109412
<svg {...props} viewBox='0 0 51 49' fill='none' xmlns='http://www.w3.org/2000/svg'>

‎apps/docs/content/docs/cli/credentials.mdx‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -113,6 +113,11 @@ Update Credential (personal API key required)
113113
| `--auth-method <value>` | No | Provider authentication method. |
114114
| `--private-key <value>` | No | Write-only PEM private key. |
115115
| `--username <value>` | No | Provider run-as username. |
116+
| `--tenancy-ocid <value>` | No | OCI tenancy OCID. |
117+
| `--user-ocid <value>` | No | OCI user OCID. |
118+
| `--fingerprint <value>` | No | OCI API-key fingerprint. |
119+
| `--private-key-passphrase <value>` | No | Write-only OCI private-key passphrase. |
120+
| `--region <value>` | No | OCI home region. |
116121
| `--name <displayName>` | No | Alias for --display-name. |
117122

118123
</CommandTable>

‎apps/docs/content/docs/cli/reference.mdx‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -473,6 +473,11 @@ sim credentials update <credentialId> [options]
473473
| `--auth-method <value>` | No | Provider authentication method. |
474474
| `--private-key <value>` | No | Write-only PEM private key. |
475475
| `--username <value>` | No | Provider run-as username. |
476+
| `--tenancy-ocid <value>` | No | OCI tenancy OCID. |
477+
| `--user-ocid <value>` | No | OCI user OCID. |
478+
| `--fingerprint <value>` | No | OCI API-key fingerprint. |
479+
| `--private-key-passphrase <value>` | No | Write-only OCI private-key passphrase. |
480+
| `--region <value>` | No | OCI home region. |
476481
| `--name <displayName>` | No | Alias for --display-name. |
477482

478483
</CommandTable>

‎apps/docs/openapi-v2-resources.json‎

Lines changed: 31 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9386,13 +9386,43 @@
93869386
"writeOnly": true,
93879387
"type": "string",
93889388
"minLength": 1,
9389-
"maxLength": 8192
9389+
"maxLength": 65536
93909390
},
93919391
"username": {
93929392
"description": "Provider run-as username.",
93939393
"type": "string",
93949394
"minLength": 1,
93959395
"maxLength": 255
9396+
},
9397+
"tenancyOcid": {
9398+
"description": "OCI tenancy OCID.",
9399+
"type": "string",
9400+
"minLength": 1,
9401+
"maxLength": 255
9402+
},
9403+
"userOcid": {
9404+
"description": "OCI user OCID.",
9405+
"type": "string",
9406+
"minLength": 1,
9407+
"maxLength": 255
9408+
},
9409+
"fingerprint": {
9410+
"description": "OCI API-key fingerprint.",
9411+
"type": "string",
9412+
"minLength": 1,
9413+
"maxLength": 128
9414+
},
9415+
"privateKeyPassphrase": {
9416+
"description": "Write-only OCI private-key passphrase.",
9417+
"writeOnly": true,
9418+
"type": "string",
9419+
"maxLength": 4096
9420+
},
9421+
"region": {
9422+
"description": "OCI home region.",
9423+
"type": "string",
9424+
"minLength": 1,
9425+
"maxLength": 128
93969426
}
93979427
},
93989428
"additionalProperties": false,

‎apps/sim/app/workspace/[workspaceId]/integrations/components/connect-service-account-modal/connect-service-account-modal.tsx‎

Lines changed: 210 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ import {
2525
import { getServiceAccountCoverageSentence } from '@/lib/integrations/credential-display'
2626
import {
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'
3031
import { 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. */
5152
const GOOGLE_SERVICE_ACCOUNT_DOCS_URL = 'https://docs.sim.ai/integrations/google-service-account'
5253
const 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

5558
function 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-
*/
140131
export 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

‎apps/sim/components/icons.tsx‎

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9394,7 +9394,7 @@ export function NewRelicIcon(props: SVGProps<SVGSVGElement>) {
93949394
)
93959395
}
93969396

9397-
export function NetSuiteIcon(props: SVGProps<SVGSVGElement>) {
9397+
export function OracleIcon(props: SVGProps<SVGSVGElement>) {
93989398
return (
93999399
<svg {...props} viewBox='0 0 93.9 59.4' xmlns='http://www.w3.org/2000/svg'>
94009400
<path
@@ -9405,6 +9405,8 @@ export function NetSuiteIcon(props: SVGProps<SVGSVGElement>) {
94059405
)
94069406
}
94079407

9408+
export const NetSuiteIcon = OracleIcon
9409+
94089410
export function WizaIcon(props: SVGProps<SVGSVGElement>) {
94099411
return (
94109412
<svg {...props} viewBox='0 0 51 49' fill='none' xmlns='http://www.w3.org/2000/svg'>

0 commit comments

Comments
 (0)