Skip to content
Open
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
4 changes: 3 additions & 1 deletion apps/docs/components/icons.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9322,7 +9322,7 @@ export function NewRelicIcon(props: SVGProps<SVGSVGElement>) {
)
}

export function NetSuiteIcon(props: SVGProps<SVGSVGElement>) {
export function OracleIcon(props: SVGProps<SVGSVGElement>) {
return (
<svg {...props} viewBox='0 0 93.9 59.4' xmlns='http://www.w3.org/2000/svg'>
<path
Expand All @@ -9333,6 +9333,8 @@ export function NetSuiteIcon(props: SVGProps<SVGSVGElement>) {
)
}

export const NetSuiteIcon = OracleIcon

export function WizaIcon(props: SVGProps<SVGSVGElement>) {
return (
<svg {...props} viewBox='0 0 51 49' fill='none' xmlns='http://www.w3.org/2000/svg'>
Expand Down
5 changes: 5 additions & 0 deletions apps/docs/content/docs/cli/credentials.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,11 @@ Update Credential (personal API key required)
| `--auth-method <value>` | No | Provider authentication method. |
| `--private-key <value>` | No | Write-only PEM private key. |
| `--username <value>` | No | Provider run-as username. |
| `--tenancy-ocid <value>` | No | OCI tenancy OCID. |
| `--user-ocid <value>` | No | OCI user OCID. |
| `--fingerprint <value>` | No | OCI API-key fingerprint. |
| `--private-key-passphrase <value>` | No | Write-only OCI private-key passphrase. |
| `--region <value>` | No | OCI home region. |
| `--name <displayName>` | No | Alias for --display-name. |

</CommandTable>
Expand Down
5 changes: 5 additions & 0 deletions apps/docs/content/docs/cli/reference.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -473,6 +473,11 @@ sim credentials update <credentialId> [options]
| `--auth-method <value>` | No | Provider authentication method. |
| `--private-key <value>` | No | Write-only PEM private key. |
| `--username <value>` | No | Provider run-as username. |
| `--tenancy-ocid <value>` | No | OCI tenancy OCID. |
| `--user-ocid <value>` | No | OCI user OCID. |
| `--fingerprint <value>` | No | OCI API-key fingerprint. |
| `--private-key-passphrase <value>` | No | Write-only OCI private-key passphrase. |
| `--region <value>` | No | OCI home region. |
| `--name <displayName>` | No | Alias for --display-name. |

</CommandTable>
Expand Down
32 changes: 31 additions & 1 deletion apps/docs/openapi-v2-resources.json
Original file line number Diff line number Diff line change
Expand Up @@ -8203,13 +8203,43 @@
"writeOnly": true,
"type": "string",
"minLength": 1,
"maxLength": 8192
"maxLength": 65536
},
"username": {
"description": "Provider run-as username.",
"type": "string",
"minLength": 1,
"maxLength": 255
},
"tenancyOcid": {
"description": "OCI tenancy OCID.",
"type": "string",
"minLength": 1,
"maxLength": 255
},
"userOcid": {
"description": "OCI user OCID.",
"type": "string",
"minLength": 1,
"maxLength": 255
},
"fingerprint": {
"description": "OCI API-key fingerprint.",
"type": "string",
"minLength": 1,
"maxLength": 128
},
"privateKeyPassphrase": {
"description": "Write-only OCI private-key passphrase.",
"writeOnly": true,
"type": "string",
"maxLength": 4096
},
"region": {
"description": "OCI home region.",
"type": "string",
"minLength": 1,
"maxLength": 128
}
},
"additionalProperties": false,
Expand Down
32 changes: 24 additions & 8 deletions apps/sim/app/api/auth/oauth/token/route.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -256,19 +256,23 @@ describe('OAuth Token API Routes', () => {
describe('service account path', () => {
it('threads the NetSuite SuiteTalk instance URL into the token response', async () => {
const instanceUrl = 'https://1234567.suitetalk.api.netsuite.com'
authOAuthUtilsMockFns.mockResolveOAuthAccountId.mockResolvedValueOnce({
const resolvedCredential = {
accountId: '',
credentialId: 'netsuite-credential-id',
credentialType: 'service_account',
providerId: 'netsuite-service-account',
workspaceId: 'workspace-id',
usedCredentialTable: true,
})
} as const
authOAuthUtilsMockFns.mockResolveOAuthAccountId
.mockResolvedValueOnce(resolvedCredential)
.mockResolvedValueOnce(resolvedCredential)
mockAuthorizeCredentialUse.mockResolvedValueOnce({
ok: true,
authType: 'session',
requesterUserId: 'test-user-id',
workspaceId: 'workspace-id',
resolvedCredentialId: 'netsuite-credential-id',
})
mockResolveServiceAccountToken.mockResolvedValueOnce({
accessToken: 'netsuite-token',
Expand All @@ -285,19 +289,23 @@ describe('OAuth Token API Routes', () => {
})

it('should thread authStyle from the resolver into the response', async () => {
authOAuthUtilsMockFns.mockResolveOAuthAccountId.mockResolvedValueOnce({
const resolvedCredential = {
accountId: '',
credentialId: 'sa-credential-id',
credentialType: 'service_account',
providerId: 'pipedrive-service-account',
workspaceId: 'workspace-id',
usedCredentialTable: true,
})
} as const
authOAuthUtilsMockFns.mockResolveOAuthAccountId
.mockResolvedValueOnce(resolvedCredential)
.mockResolvedValueOnce(resolvedCredential)
mockAuthorizeCredentialUse.mockResolvedValueOnce({
ok: true,
authType: 'session',
requesterUserId: 'test-user-id',
workspaceId: 'workspace-id',
resolvedCredentialId: 'sa-credential-id',
})
mockResolveServiceAccountToken.mockResolvedValueOnce({
accessToken: 'pasted-api-token',
Expand All @@ -315,19 +323,23 @@ describe('OAuth Token API Routes', () => {
})

it('should omit authStyle for Bearer token-paste providers', async () => {
authOAuthUtilsMockFns.mockResolveOAuthAccountId.mockResolvedValueOnce({
const resolvedCredential = {
accountId: '',
credentialId: 'sa-credential-id',
credentialType: 'service_account',
providerId: 'hubspot-service-account',
workspaceId: 'workspace-id',
usedCredentialTable: true,
})
} as const
authOAuthUtilsMockFns.mockResolveOAuthAccountId
.mockResolvedValueOnce(resolvedCredential)
.mockResolvedValueOnce(resolvedCredential)
mockAuthorizeCredentialUse.mockResolvedValueOnce({
ok: true,
authType: 'session',
requesterUserId: 'test-user-id',
workspaceId: 'workspace-id',
resolvedCredentialId: 'sa-credential-id',
})
mockResolveServiceAccountToken.mockResolvedValueOnce({
accessToken: 'pat-token',
Expand All @@ -350,19 +362,23 @@ describe('OAuth Token API Routes', () => {
] as const)(
'surfaces the %s error code with status %i when the mint fails',
async (code, status) => {
authOAuthUtilsMockFns.mockResolveOAuthAccountId.mockResolvedValueOnce({
const resolvedCredential = {
accountId: '',
credentialId: 'sa-credential-id',
credentialType: 'service_account',
providerId: 'salesforce-service-account',
workspaceId: 'workspace-id',
usedCredentialTable: true,
})
} as const
authOAuthUtilsMockFns.mockResolveOAuthAccountId
.mockResolvedValueOnce(resolvedCredential)
.mockResolvedValueOnce(resolvedCredential)
mockAuthorizeCredentialUse.mockResolvedValueOnce({
ok: true,
authType: 'session',
requesterUserId: 'test-user-id',
workspaceId: 'workspace-id',
resolvedCredentialId: 'sa-credential-id',
})
mockResolveServiceAccountToken.mockRejectedValueOnce(
new TokenServiceAccountValidationError(code, status, { step: 'mint' })
Expand Down
64 changes: 64 additions & 0 deletions apps/sim/app/api/credentials/route.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -550,4 +550,68 @@ describe('POST /api/credentials', () => {
expect(dbChainMockFns.insert).not.toHaveBeenCalled()
})
})

it('forwards OCI API-key fields without returning secret material', async () => {
mockVerifyAndBuildServiceAccountSecret.mockResolvedValueOnce({
providerId: 'oci-api-key-service-account',
encryptedServiceAccountKey: 'encrypted-oci-blob',
displayName: 'ocid1.user.oc1..principal',
auditMetadata: {
principalKind: 'user',
principalId: 'ocid1.user.oc1..principal',
},
principal: { kind: 'user', id: 'ocid1.user.oc1..principal' },
})
queueTableRows(credential, [])
queueTableRows(credential, [])
queueTableRows(credential, [
{
id: 'credential-oci',
workspaceId: WORKSPACE_ID,
type: 'service_account',
displayName: 'ocid1.user.oc1..principal',
description: null,
unredacted: false,
providerId: 'oci-api-key-service-account',
accountId: null,
envKey: null,
envOwnerUserId: null,
encryptedServiceAccountKey: 'encrypted-oci-blob',
createdBy: 'user-1',
createdAt: new Date('2026-08-11T00:00:00.000Z'),
updatedAt: new Date('2026-08-11T00:00:00.000Z'),
},
])

const response = await POST(
createMockRequest('POST', {
workspaceId: WORKSPACE_ID,
type: 'service_account',
providerId: 'oci-api-key-service-account',
tenancyOcid: 'ocid1.tenancy.oc1..tenant',
userOcid: 'ocid1.user.oc1..principal',
fingerprint: '00:11:22:33:44:55:66:77:88:99:aa:bb:cc:dd:ee:ff',
privateKey: '-----BEGIN PRIVATE KEY-----\nkey\n-----END PRIVATE KEY-----',
privateKeyPassphrase: ' exact passphrase ',
region: 'us-ashburn-1',
})
)
const body = await response.text()

expect(response.status).toBe(201)
expect(mockVerifyAndBuildServiceAccountSecret).toHaveBeenCalledWith(
'oci-api-key-service-account',
expect.objectContaining({
tenancyOcid: 'ocid1.tenancy.oc1..tenant',
userOcid: 'ocid1.user.oc1..principal',
fingerprint: '00:11:22:33:44:55:66:77:88:99:aa:bb:cc:dd:ee:ff',
privateKey: '-----BEGIN PRIVATE KEY-----\nkey\n-----END PRIVATE KEY-----',
privateKeyPassphrase: ' exact passphrase ',
region: 'us-ashburn-1',
})
)
expect(body).not.toContain('PRIVATE KEY')
expect(body).not.toContain('exact passphrase')
expect(body).not.toContain('encrypted-oci-blob')
})
})
27 changes: 27 additions & 0 deletions apps/sim/app/api/v2/credentials/[credentialId]/route.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,33 @@ describe('PATCH /api/v2/credentials/[credentialId]', () => {
expect(body).not.toContain('MUST_NOT_LEAK_CIPHERTEXT')
})

it('forwards a complete OCI rotation tuple and preserves explicit passphrase clearing', async () => {

@cubic-dev-ai cubic-dev-ai Bot Sep 4, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P3: The test name claims it 'preserves explicit passphrase clearing', but the request body and the asserted update call never include privateKeyPassphrase, so that behavior is not tested. Either send privateKeyPassphrase: null in the request and assert it is forwarded (and the response omits it), or rename the test to drop the passphrase-clearing claim.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At apps/sim/app/api/v2/credentials/[credentialId]/route.test.ts, line 130:

<comment>The test name claims it 'preserves explicit passphrase clearing', but the request body and the asserted update call never include `privateKeyPassphrase`, so that behavior is not tested. Either send `privateKeyPassphrase: null` in the request and assert it is forwarded (and the response omits it), or rename the test to drop the passphrase-clearing claim.</comment>

<file context>
@@ -127,6 +127,33 @@ describe('PATCH /api/v2/credentials/[credentialId]', () => {
     expect(body).not.toContain('MUST_NOT_LEAK_CIPHERTEXT')
   })
 
+  it('forwards a complete OCI rotation tuple and preserves explicit passphrase clearing', async () => {
+    const request = patchRequest({
+      tenancyOcid: 'ocid1.tenancy.oc1..tenant',
</file context>
Fix with cubic

const request = patchRequest({
tenancyOcid: 'ocid1.tenancy.oc1..tenant',
userOcid: 'ocid1.user.oc1..replacement',
fingerprint: '00:11:22:33:44:55:66:77:88:99:aa:bb:cc:dd:ee:ff',
privateKey: '-----BEGIN PRIVATE KEY-----\nreplacement\n-----END PRIVATE KEY-----',
region: 'us-ashburn-1',
})
const response = await PATCH(request, context)

expect(response.status).toBe(200)
expect(mocks.update).toHaveBeenCalledWith({
principal: auth.principal,
input: {
tenancyOcid: 'ocid1.tenancy.oc1..tenant',
userOcid: 'ocid1.user.oc1..replacement',
fingerprint: '00:11:22:33:44:55:66:77:88:99:aa:bb:cc:dd:ee:ff',
privateKey: '-----BEGIN PRIVATE KEY-----\nreplacement\n-----END PRIVATE KEY-----',
region: 'us-ashburn-1',
credentialId: CREDENTIAL_ID,
assertedWorkspaceId: WORKSPACE_ID,
},
request,
})
expect(JSON.stringify(await response.json())).not.toContain('PRIVATE KEY')
})

it('asserts the workspace scope and preserves the credential id', async () => {
const request = patchRequest({ displayName: 'Zoom prod' })
await PATCH(request, context)
Expand Down
44 changes: 44 additions & 0 deletions apps/sim/app/api/v2/credentials/route.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -288,11 +288,55 @@ describe('POST /api/v2/credentials', () => {
authMethod: undefined,
privateKey: undefined,
username: undefined,
tenancyOcid: undefined,
userOcid: undefined,
fingerprint: undefined,
privateKeyPassphrase: undefined,
region: undefined,
},
request,
})
})

it('forwards OCI credential fields from the write-only credentials envelope', async () => {
const request = new NextRequest('http://localhost:3000/api/v2/credentials', {
method: 'POST',
headers: { 'content-type': 'application/json' },
body: JSON.stringify({
workspaceId: WORKSPACE_ID,
type: 'service_account',
providerId: 'oci-api-key-service-account',
credentials: JSON.stringify({
tenancyOcid: 'ocid1.tenancy.oc1..tenant',
userOcid: 'ocid1.user.oc1..user',
fingerprint: '00:11:22:33:44:55:66:77:88:99:aa:bb:cc:dd:ee:ff',
privateKey: '-----BEGIN PRIVATE KEY-----\nkey\n-----END PRIVATE KEY-----',
privateKeyPassphrase: ' exact passphrase ',
region: 'us-ashburn-1',
}),
}),
})
const response = await POST(request)
const body = await response.text()

expect(response.status).toBe(201)
expect(mocks.create).toHaveBeenCalledWith({
principal: { kind: 'personal_api_key', userId: 'user-1', keyId: 'key-1' },
input: expect.objectContaining({
providerId: 'oci-api-key-service-account',
tenancyOcid: 'ocid1.tenancy.oc1..tenant',
userOcid: 'ocid1.user.oc1..user',
fingerprint: '00:11:22:33:44:55:66:77:88:99:aa:bb:cc:dd:ee:ff',
privateKey: '-----BEGIN PRIVATE KEY-----\nkey\n-----END PRIVATE KEY-----',
privateKeyPassphrase: ' exact passphrase ',
region: 'us-ashburn-1',
}),
request,
})
expect(body).not.toContain('PRIVATE KEY')
expect(body).not.toContain('exact passphrase')
})

it('rejects an unknown service-account provider before the use case', async () => {
const response = await POST(
new NextRequest('http://localhost:3000/api/v2/credentials', {
Expand Down
Loading
Loading