Skip to content

Commit 9ea5b3a

Browse files
committed
fix(salesforce): canonicalize connected provider ids in the copilot credential tool
A credential stored under an alternate authorization server was recorded in `connectedProviderIds` under its own id, while the not-connected list compares against the service's canonical id — so a sandbox-only Salesforce user was reported as both connected and not connected. Record the canonical id instead. Also types the JWT test's assertion decoder instead of returning `any`.
1 parent 4b2a883 commit 9ea5b3a

3 files changed

Lines changed: 55 additions & 3 deletions

File tree

apps/sim/lib/copilot/tools/server/user/get-credentials.test.ts

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -264,6 +264,50 @@ describe('getCredentialsServerTool', () => {
264264
).not.toContain('claude-platform')
265265
})
266266

267+
it('does not list a service as not-connected when only an alternate provider is connected', async () => {
268+
// A credential stored under an alternate authorization server
269+
// (`salesforce-sandbox`) still connects the canonical service. Recording the
270+
// raw id would list Salesforce as connected AND not connected at once.
271+
getAllOAuthServicesMock.mockReturnValue([
272+
{
273+
serviceId: 'salesforce',
274+
providerId: 'salesforce',
275+
additionalProviderIds: ['salesforce-sandbox'],
276+
serviceAccountProviderId: 'salesforce-service-account',
277+
name: 'Salesforce',
278+
description: 'Salesforce CRM',
279+
baseProvider: 'salesforce',
280+
authType: 'oauth',
281+
},
282+
])
283+
// beforeEach already queued the default Google row; replace the queue so
284+
// the sandbox account is the only credential this case sees.
285+
resetDbChainMock()
286+
wireDb(
287+
[
288+
{
289+
id: 'acct-sf-sandbox',
290+
providerId: 'salesforce-sandbox',
291+
accountId: 'sf-1',
292+
idToken: null,
293+
updatedAt: new Date('2026-04-17T02:26:05.546Z'),
294+
},
295+
],
296+
[{ email: 'brent@cellular.so' }]
297+
)
298+
299+
const result = await getCredentialsServerTool.execute({}, { userId: 'user-1' })
300+
301+
expect(
302+
result.oauth.connected.credentials.map((c: { provider: string }) => c.provider)
303+
).toContain('salesforce-sandbox')
304+
expect(
305+
result.oauth.notConnected.services.map(
306+
(service: { providerId: string }) => service.providerId
307+
)
308+
).not.toContain('salesforce')
309+
})
310+
267311
it('hides shared service-account credentials disallowed for the viewer', async () => {
268312
getUserPermissionConfigMock.mockResolvedValue({ allowedIntegrations: ['slack'] })
269313
getAccessibleOAuthCredentialsMock.mockResolvedValue([

apps/sim/lib/copilot/tools/server/user/get-credentials.ts

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -110,7 +110,11 @@ export const getCredentialsServerTool: BaseServerTool<GetCredentialsParams, any>
110110
credentialProviderMatchesService(providerId, candidate)
111111
)
112112
if (!credentialVisibility.isCredentialVisible({ providerId, type: 'oauth' })) continue
113-
connectedProviderIds.add(providerId)
113+
// The canonical id, not the credential's own: `notConnectedServices` below
114+
// compares against `service.providerId`, so recording an alternate
115+
// authorization server's id (`salesforce-sandbox`) verbatim would list the
116+
// service as both connected and not connected.
117+
connectedProviderIds.add(service?.providerId ?? providerId)
114118

115119
const [baseProvider, featureType = 'default'] = providerId.split('-')
116120
let displayName = ''
@@ -163,7 +167,7 @@ export const getCredentialsServerTool: BaseServerTool<GetCredentialsParams, any>
163167
const service = allOAuthServices.find((candidate) =>
164168
credentialProviderMatchesService(cred.providerId, candidate)
165169
)
166-
connectedProviderIds.add(cred.providerId)
170+
connectedProviderIds.add(service?.providerId ?? cred.providerId)
167171
const [, featureType = 'default'] = cred.providerId.split('-')
168172
connectedCredentials.push({
169173
id: cred.id,

apps/sim/lib/credentials/client-credential-accounts/minters/salesforce.test.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -362,7 +362,11 @@ describe('mintSalesforceServiceAccountToken (JWT bearer)', () => {
362362
}
363363

364364
/** Pulls the posted assertion apart and verifies its RS256 signature. */
365-
function readPostedAssertion(): { header: any; claims: any; verified: boolean } {
365+
function readPostedAssertion(): {
366+
header: { alg: string; typ: string }
367+
claims: { aud: string; iss: string; sub: string; exp: number }
368+
verified: boolean
369+
} {
366370
const [url, init] = mockFetch.mock.calls[0]
367371
expect(url).toBe(TOKEN_URL)
368372
const body = new URLSearchParams(init.body as string)

0 commit comments

Comments
 (0)