Skip to content

Commit b724b08

Browse files
committed
fix(integrations): never answer with a sibling's tile
Two integrations can share one OAuth id: Google Slides is authenticated by Drive's `google-drive` service and Jira Service Management by Jira's `jira`. Indexing first-write-wins made those ids resolve to whichever sorted first, so the dialog connecting Slides could wear Drive's brand. An id claimed by more than one block type now resolves to neither, and the caller keeps the service-specific mark it already had. A wrong brand is worse than no tile.
1 parent fdb5cde commit b724b08

2 files changed

Lines changed: 41 additions & 12 deletions

File tree

apps/sim/lib/integrations/oauth-service.test.ts

Lines changed: 23 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -185,6 +185,13 @@ describe('resolveServiceAccountIntegration', () => {
185185
})
186186
})
187187

188+
/**
189+
* Integrations whose `oauthServiceId` is shared with a sibling, so the id names
190+
* a pair rather than a block: Google Slides rides Drive's service, Jira Service
191+
* Management rides Jira's. The bridge deliberately resolves none of them.
192+
*/
193+
const SHARED_OAUTH_ID_SLUGS = ['google-drive', 'google-slides', 'jira', 'jira-service-management']
194+
188195
/** Resolved block type with any version suffix dropped, for stable assertions. */
189196
function baseTypeFor(...keys: (string | undefined)[]): string | undefined {
190197
const blockType = resolveIntegrationBlockTypeForOAuth(...keys)
@@ -223,18 +230,27 @@ describe('resolveIntegrationBlockTypeForOAuth', () => {
223230
expect(resolveIntegrationBlockTypeForOAuth(undefined, '')).toBeUndefined()
224231
})
225232

226-
it.concurrent('resolves every OAuth integration in the catalog to a block type', () => {
233+
it.concurrent('refuses to guess when one OAuth id names more than one block', () => {
234+
// Google Slides is authenticated by Drive's service and JSM by Jira's, so
235+
// these ids name a pair. Answering with either member would put the wrong
236+
// brand on the other's connect dialog, so the bridge declines.
237+
expect(resolveIntegrationBlockTypeForOAuth('google-drive')).toBeUndefined()
238+
expect(resolveIntegrationBlockTypeForOAuth('jira')).toBeUndefined()
239+
})
240+
241+
it.concurrent('resolves every OAuth integration whose id names it alone', () => {
227242
// A credential surface that cannot reach a block type falls back to the
228243
// colourless OAUTH_PROVIDERS mark, which is the bug this bridge exists to
229-
// close — so no OAuth integration may be missing from the index.
244+
// close — so every unambiguous integration must be in the index, and the
245+
// only permitted misses are the shared ids above.
230246
const unresolved = INTEGRATIONS.filter(
231247
(integration) =>
232248
integration.authType === 'oauth' &&
233249
integration.oauthServiceId &&
234250
!resolveIntegrationBlockTypeForOAuth(integration.oauthServiceId)
235251
).map((integration) => integration.slug)
236252

237-
expect(unresolved).toEqual([])
253+
expect(unresolved.sort()).toEqual(SHARED_OAUTH_ID_SLUGS)
238254
})
239255
})
240256

@@ -244,7 +260,10 @@ describe('the OAuth bridge reaches a renderable tile', () => {
244260
// A type that resolves but has no registered icon or colour would paint an
245261
// empty square in the connect dialog — worse than the grey mark it replaced.
246262
const broken = INTEGRATIONS.filter(
247-
(integration) => integration.authType === 'oauth' && integration.oauthServiceId
263+
(integration) =>
264+
integration.authType === 'oauth' &&
265+
integration.oauthServiceId &&
266+
!SHARED_OAUTH_ID_SLUGS.includes(integration.slug)
248267
).flatMap((integration) => {
249268
const blockType = resolveIntegrationBlockTypeForOAuth(integration.oauthServiceId)
250269
if (!blockType) return [`${integration.slug}: unresolved`]

apps/sim/lib/integrations/oauth-service.ts

Lines changed: 18 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -72,28 +72,38 @@ export function resolveOAuthServiceForSlug(slug: string): OAuthServiceMatch | nu
7272
* dialog. This is the bridge back: given any OAuth id, the block whose config
7373
* owns the icon and `bgColor`.
7474
*
75-
* First write wins, so a family provider resolves to the same member every
76-
* time rather than flipping with catalog order.
75+
* A key two integrations both claim resolves to neither. Google Slides is
76+
* authenticated by Drive's `google-drive` service and Jira Service Management
77+
* by Jira's `jira`, so those ids name a pair, not a block — and picking the
78+
* one that happens to sort first would put Drive's tile on the dialog opening
79+
* Slides. Dropping the key falls the caller back to the service-specific mark
80+
* it already had, which is the honest answer: no wrong brand.
7781
*/
7882
const BLOCK_TYPE_BY_OAUTH_KEY: ReadonlyMap<string, string> = (() => {
79-
const index = new Map<string, string>()
80-
const add = (key: string | undefined, blockType: string) => {
83+
const claims = new Map<string, Set<string>>()
84+
const claim = (key: string | undefined, blockType: string) => {
8185
if (!key) return
8286
const normalized = key.toLowerCase()
83-
if (!index.has(normalized)) index.set(normalized, blockType)
87+
const owners = claims.get(normalized)
88+
if (owners) owners.add(blockType)
89+
else claims.set(normalized, new Set([blockType]))
8490
}
8591

8692
for (const integration of INTEGRATIONS_DATA) {
8793
if (integration.authType !== 'oauth' || !integration.oauthServiceId) continue
8894
const service = getServiceConfigByServiceId(integration.oauthServiceId)
8995
if (!service) continue
90-
add(integration.oauthServiceId, integration.type)
91-
add(service.providerId, integration.type)
96+
claim(integration.oauthServiceId, integration.type)
97+
claim(service.providerId, integration.type)
9298
for (const extraProviderId of service.additionalProviderIds ?? []) {
93-
add(extraProviderId, integration.type)
99+
claim(extraProviderId, integration.type)
94100
}
95101
}
96102

103+
const index = new Map<string, string>()
104+
for (const [key, owners] of claims) {
105+
if (owners.size === 1) index.set(key, owners.values().next().value as string)
106+
}
97107
return index
98108
})()
99109

0 commit comments

Comments
 (0)