Skip to content

Commit cf680e6

Browse files
fix(credentials): stabilize oauth draft retries
1 parent 7fcf26f commit cf680e6

4 files changed

Lines changed: 34 additions & 7 deletions

File tree

apps/sim/lib/credentials/application/create-credential-connection.test.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,7 @@ describe('createCredentialConnection', () => {
9797
providerId: 'google-email',
9898
credentialId: undefined,
9999
displayName: 'Work Gmail',
100+
displayNameDefinesIntent: true,
100101
})
101102
expect(result).toEqual({
102103
authorizationUrl: 'https://sim.ai/api/auth/oauth2/authorize?draftId=draft-1',
@@ -123,6 +124,7 @@ describe('createCredentialConnection', () => {
123124
providerId: 'google-email',
124125
credentialId: 'credential-1',
125126
displayName: 'Existing Gmail',
127+
displayNameDefinesIntent: false,
126128
})
127129
})
128130
})

apps/sim/lib/credentials/application/create-credential-connection.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ export const createCredentialConnection = defineAuthorizedWorkspaceUseCase({
4242
providerId: target.providerId,
4343
credentialId: target.credentialId,
4444
displayName,
45+
displayNameDefinesIntent: input.providerId !== undefined,
4546
})
4647
const authorizationUrl = new URL('/api/auth/oauth2/authorize', getBaseUrl())
4748
authorizationUrl.searchParams.set('draftId', draft.id)

apps/sim/lib/credentials/connect-draft.test.ts

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
/**
22
* @vitest-environment node
33
*/
4-
import { dbChainMockFns, resetDbChainMock } from '@sim/testing'
4+
import { dbChainMockFns, drizzleOrmMock, resetDbChainMock, schemaMock } from '@sim/testing'
55
import { beforeEach, describe, expect, it, vi } from 'vitest'
66

77
const { mockGenerateId } = vi.hoisted(() => ({
@@ -28,6 +28,7 @@ describe('createConnectDraft', () => {
2828
workspaceId: 'workspace-1',
2929
providerId: 'google-email',
3030
displayName: 'Work Gmail',
31+
displayNameDefinesIntent: true,
3132
})
3233

3334
expect(dbChainMockFns.values).toHaveBeenCalledWith(
@@ -43,6 +44,26 @@ describe('createConnectDraft', () => {
4344
expect(result).toEqual({ id: 'active-draft-id', expiresAt })
4445
})
4546

47+
it('refreshes a reconnect target when its mutable display name changes', async () => {
48+
const expiresAt = new Date('2026-08-13T20:15:00.000Z')
49+
dbChainMockFns.returning.mockResolvedValueOnce([{ id: 'active-draft-id', expiresAt }])
50+
51+
await expect(
52+
createConnectDraft({
53+
userId: 'user-1',
54+
workspaceId: 'workspace-1',
55+
providerId: 'google-email',
56+
credentialId: 'credential-1',
57+
displayName: 'Renamed Gmail',
58+
})
59+
).resolves.toEqual({ id: 'active-draft-id', expiresAt })
60+
61+
expect(drizzleOrmMock.eq).not.toHaveBeenCalledWith(
62+
schemaMock.pendingCredentialDraft.displayName,
63+
'Renamed Gmail'
64+
)
65+
})
66+
4667
it('fails fast when an active draft has a different connection intent', async () => {
4768
dbChainMockFns.returning.mockResolvedValueOnce([])
4869

apps/sim/lib/credentials/connect-draft.ts

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,8 @@ export async function createConnectDraft(params: {
2929
credentialId?: string
3030
/** Reconnect only: the credential's actual name, so audit records stay accurate. */
3131
displayName?: string
32+
/** Whether an explicitly requested name distinguishes this new-connection intent. */
33+
displayNameDefinesIntent?: boolean
3234
}): Promise<CreatedConnectDraft> {
3335
const { userId, workspaceId, providerId, credentialId } = params
3436

@@ -67,6 +69,12 @@ export async function createConnectDraft(params: {
6769
and(eq(pendingCredentialDraft.userId, userId), lt(pendingCredentialDraft.expiresAt, now))
6870
)
6971
const id = generateId()
72+
const sameTarget = credentialId
73+
? eq(pendingCredentialDraft.credentialId, credentialId)
74+
: isNull(pendingCredentialDraft.credentialId)
75+
const sameIntent = params.displayNameDefinesIntent
76+
? and(sameTarget, eq(pendingCredentialDraft.displayName, displayName))
77+
: sameTarget
7078
const [draft] = await db
7179
.insert(pendingCredentialDraft)
7280
.values({
@@ -86,12 +94,7 @@ export async function createConnectDraft(params: {
8694
pendingCredentialDraft.workspaceId,
8795
],
8896
set: { expiresAt, createdAt: now },
89-
setWhere: and(
90-
eq(pendingCredentialDraft.displayName, displayName),
91-
credentialId
92-
? eq(pendingCredentialDraft.credentialId, credentialId)
93-
: isNull(pendingCredentialDraft.credentialId)
94-
),
97+
setWhere: sameIntent,
9598
})
9699
.returning({ id: pendingCredentialDraft.id, expiresAt: pendingCredentialDraft.expiresAt })
97100

0 commit comments

Comments
 (0)