From 275391a1b495b40ce655a14bf409a7db349ccab0 Mon Sep 17 00:00:00 2001 From: waleed Date: Fri, 12 Jun 2026 11:11:09 -0700 Subject: [PATCH 1/3] fix(integrations): resolve OAuth connect UI by service id instead of display name --- apps/sim/lib/integrations/integrations.json | 48 +++++++++++++++++ .../lib/integrations/oauth-service.test.ts | 52 +++++++++++++++++++ apps/sim/lib/integrations/oauth-service.ts | 34 ++++++------ apps/sim/lib/integrations/types.ts | 5 ++ apps/sim/lib/oauth/utils.test.ts | 31 +++++++++++ apps/sim/lib/oauth/utils.ts | 12 +++++ scripts/generate-docs.ts | 42 +++++++++++++++ 7 files changed, 205 insertions(+), 19 deletions(-) create mode 100644 apps/sim/lib/integrations/oauth-service.test.ts diff --git a/apps/sim/lib/integrations/integrations.json b/apps/sim/lib/integrations/integrations.json index 39763e91064..853a8728a02 100644 --- a/apps/sim/lib/integrations/integrations.json +++ b/apps/sim/lib/integrations/integrations.json @@ -441,6 +441,7 @@ ], "triggerCount": 1, "authType": "oauth", + "oauthServiceId": "airtable", "category": "tools", "integrationType": "databases", "tags": ["spreadsheet", "automation"] @@ -946,6 +947,7 @@ "triggers": [], "triggerCount": 0, "authType": "oauth", + "oauthServiceId": "asana", "category": "tools", "integrationType": "productivity", "tags": ["project-management", "ticketing", "automation"] @@ -1453,6 +1455,7 @@ ], "triggerCount": 22, "authType": "oauth", + "oauthServiceId": "attio", "category": "tools", "integrationType": "sales", "tags": ["sales-engagement", "enrichment"] @@ -1960,6 +1963,7 @@ "triggers": [], "triggerCount": 0, "authType": "oauth", + "oauthServiceId": "microsoft-ad", "category": "tools", "integrationType": "security", "tags": ["identity", "microsoft-365"] @@ -2138,6 +2142,7 @@ "triggers": [], "triggerCount": 0, "authType": "oauth", + "oauthServiceId": "box", "category": "tools", "integrationType": "documents", "tags": ["cloud", "content-management", "e-signatures"] @@ -2506,6 +2511,7 @@ ], "triggerCount": 9, "authType": "oauth", + "oauthServiceId": "calcom", "category": "tools", "integrationType": "productivity", "tags": ["scheduling", "calendar", "meeting"] @@ -3325,6 +3331,7 @@ ], "triggerCount": 23, "authType": "oauth", + "oauthServiceId": "confluence", "category": "tools", "integrationType": "documents", "tags": ["knowledge-base", "content-management", "note-taking"] @@ -4039,6 +4046,7 @@ "triggers": [], "triggerCount": 0, "authType": "oauth", + "oauthServiceId": "docusign", "category": "tools", "integrationType": "documents", "tags": ["e-signatures", "document-processing"] @@ -4098,6 +4106,7 @@ "triggers": [], "triggerCount": 0, "authType": "oauth", + "oauthServiceId": "dropbox", "category": "tools", "integrationType": "documents", "tags": ["cloud", "document-processing"] @@ -5506,6 +5515,7 @@ ], "triggerCount": 1, "authType": "oauth", + "oauthServiceId": "gmail", "category": "tools", "integrationType": "email", "tags": ["google-workspace", "messaging"] @@ -5663,6 +5673,7 @@ "triggers": [], "triggerCount": 0, "authType": "oauth", + "oauthServiceId": "google-ads", "category": "tools", "integrationType": "analytics", "tags": ["marketing", "google-workspace", "data-analytics"] @@ -5702,6 +5713,7 @@ "triggers": [], "triggerCount": 0, "authType": "oauth", + "oauthServiceId": "google-bigquery", "category": "tools", "integrationType": "databases", "tags": ["data-warehouse", "google-workspace", "data-analytics"] @@ -5794,6 +5806,7 @@ ], "triggerCount": 1, "authType": "oauth", + "oauthServiceId": "google-calendar", "category": "tools", "integrationType": "productivity", "tags": ["calendar", "scheduling", "google-workspace"] @@ -5837,6 +5850,7 @@ "triggers": [], "triggerCount": 0, "authType": "oauth", + "oauthServiceId": "google-contacts", "category": "tools", "integrationType": "productivity", "tags": ["google-workspace", "customer-support", "enrichment"] @@ -5868,6 +5882,7 @@ "triggers": [], "triggerCount": 0, "authType": "oauth", + "oauthServiceId": "google-docs", "category": "tools", "integrationType": "documents", "tags": ["google-workspace", "document-processing", "content-management"] @@ -5965,6 +5980,7 @@ ], "triggerCount": 1, "authType": "oauth", + "oauthServiceId": "google-drive", "category": "tools", "integrationType": "documents", "tags": ["cloud", "google-workspace", "document-processing"] @@ -6026,6 +6042,7 @@ ], "triggerCount": 1, "authType": "oauth", + "oauthServiceId": "google-forms", "category": "tools", "integrationType": "documents", "tags": ["google-workspace", "forms", "data-analytics"] @@ -6109,6 +6126,7 @@ "triggers": [], "triggerCount": 0, "authType": "oauth", + "oauthServiceId": "google-groups", "category": "tools", "integrationType": "communication", "tags": ["google-workspace", "messaging", "identity"] @@ -6223,6 +6241,7 @@ "triggers": [], "triggerCount": 0, "authType": "oauth", + "oauthServiceId": "google-meet", "category": "tools", "integrationType": "communication", "tags": ["meeting", "google-workspace", "scheduling"] @@ -6328,6 +6347,7 @@ ], "triggerCount": 1, "authType": "oauth", + "oauthServiceId": "google-sheets", "category": "tools", "integrationType": "documents", "tags": ["spreadsheet", "google-workspace", "data-analytics"] @@ -6555,6 +6575,7 @@ "triggers": [], "triggerCount": 0, "authType": "oauth", + "oauthServiceId": "google-drive", "category": "tools", "integrationType": "documents", "tags": ["google-workspace", "document-processing", "content-management"] @@ -6598,6 +6619,7 @@ "triggers": [], "triggerCount": 0, "authType": "oauth", + "oauthServiceId": "google-tasks", "category": "tools", "integrationType": "productivity", "tags": ["google-workspace", "project-management", "scheduling"] @@ -6672,6 +6694,7 @@ "triggers": [], "triggerCount": 0, "authType": "oauth", + "oauthServiceId": "google-vault", "category": "tools", "integrationType": "security", "tags": ["google-workspace", "document-processing"] @@ -7257,6 +7280,7 @@ ], "triggerCount": 1, "authType": "oauth", + "oauthServiceId": "hubspot", "category": "tools", "integrationType": "sales", "tags": ["marketing", "sales-engagement", "customer-support"] @@ -8128,6 +8152,7 @@ ], "triggerCount": 15, "authType": "oauth", + "oauthServiceId": "jira", "category": "tools", "integrationType": "productivity", "tags": ["project-management", "ticketing"] @@ -8309,6 +8334,7 @@ ], "triggerCount": 5, "authType": "oauth", + "oauthServiceId": "jira", "category": "tools", "integrationType": "support", "tags": ["customer-support", "ticketing", "incident-management"] @@ -9068,6 +9094,7 @@ ], "triggerCount": 15, "authType": "oauth", + "oauthServiceId": "linear", "category": "tools", "integrationType": "productivity", "tags": ["project-management", "ticketing"] @@ -9095,6 +9122,7 @@ "triggers": [], "triggerCount": 0, "authType": "oauth", + "oauthServiceId": "linkedin", "category": "tools", "integrationType": "sales", "tags": ["marketing", "sales-engagement"] @@ -9870,6 +9898,7 @@ "triggers": [], "triggerCount": 0, "authType": "oauth", + "oauthServiceId": "microsoft-dataverse", "category": "tools", "integrationType": "databases", "tags": ["microsoft-365", "data-warehouse", "cloud"] @@ -9897,6 +9926,7 @@ "triggers": [], "triggerCount": 0, "authType": "oauth", + "oauthServiceId": "microsoft-excel", "category": "tools", "integrationType": "documents", "tags": ["spreadsheet", "microsoft-365"] @@ -9968,6 +9998,7 @@ "triggers": [], "triggerCount": 0, "authType": "oauth", + "oauthServiceId": "microsoft-planner", "category": "tools", "integrationType": "productivity", "tags": ["project-management", "microsoft-365"] @@ -10049,6 +10080,7 @@ ], "triggerCount": 1, "authType": "oauth", + "oauthServiceId": "microsoft-teams", "category": "tools", "integrationType": "communication", "tags": ["messaging", "microsoft-365"] @@ -10211,6 +10243,7 @@ ], "triggerCount": 9, "authType": "oauth", + "oauthServiceId": "monday", "category": "tools", "integrationType": "productivity", "tags": ["project-management", "ticketing"] @@ -10469,6 +10502,7 @@ ], "triggerCount": 9, "authType": "oauth", + "oauthServiceId": "notion", "category": "tools", "integrationType": "documents", "tags": ["note-taking", "knowledge-base", "content-management"] @@ -10682,6 +10716,7 @@ "triggers": [], "triggerCount": 0, "authType": "oauth", + "oauthServiceId": "onedrive", "category": "tools", "integrationType": "documents", "tags": ["microsoft-365", "cloud", "document-processing"] @@ -10743,6 +10778,7 @@ ], "triggerCount": 1, "authType": "oauth", + "oauthServiceId": "outlook", "category": "tools", "integrationType": "email", "tags": ["microsoft-365", "messaging", "automation"] @@ -11160,6 +11196,7 @@ "triggers": [], "triggerCount": 0, "authType": "oauth", + "oauthServiceId": "pipedrive", "category": "tools", "integrationType": "sales", "tags": ["sales-engagement", "project-management"] @@ -12170,6 +12207,7 @@ "triggers": [], "triggerCount": 0, "authType": "oauth", + "oauthServiceId": "reddit", "category": "tools", "integrationType": "communication", "tags": ["content-management", "web-scraping"] @@ -13221,6 +13259,7 @@ ], "triggerCount": 6, "authType": "oauth", + "oauthServiceId": "salesforce", "category": "tools", "integrationType": "sales", "tags": ["sales-engagement", "customer-support"] @@ -14083,6 +14122,7 @@ "triggers": [], "triggerCount": 0, "authType": "oauth", + "oauthServiceId": "sharepoint", "category": "tools", "integrationType": "documents", "tags": ["microsoft-365", "content-management", "document-processing"] @@ -14186,6 +14226,7 @@ "triggers": [], "triggerCount": 0, "authType": "oauth", + "oauthServiceId": "shopify", "category": "tools", "integrationType": "commerce", "tags": ["payments", "automation"] @@ -14425,6 +14466,7 @@ ], "triggerCount": 1, "authType": "oauth", + "oauthServiceId": "slack", "category": "tools", "integrationType": "communication", "tags": ["messaging", "webhooks", "automation"], @@ -15272,6 +15314,7 @@ "triggers": [], "triggerCount": 0, "authType": "oauth", + "oauthServiceId": "trello", "category": "tools", "integrationType": "productivity", "tags": ["project-management", "ticketing"] @@ -16124,6 +16167,7 @@ "triggers": [], "triggerCount": 0, "authType": "oauth", + "oauthServiceId": "wealthbox", "category": "tools", "integrationType": "sales", "tags": ["sales-engagement"] @@ -16184,6 +16228,7 @@ ], "triggerCount": 4, "authType": "oauth", + "oauthServiceId": "webflow", "category": "tools", "integrationType": "marketing", "tags": ["content-management", "seo"] @@ -16401,6 +16446,7 @@ "triggers": [], "triggerCount": 0, "authType": "oauth", + "oauthServiceId": "wordpress", "category": "tools", "integrationType": "marketing", "tags": ["content-management", "seo"] @@ -16591,6 +16637,7 @@ "triggers": [], "triggerCount": 0, "authType": "oauth", + "oauthServiceId": "x", "category": "tools", "integrationType": "communication", "tags": ["marketing", "messaging"] @@ -16941,6 +16988,7 @@ ], "triggerCount": 6, "authType": "oauth", + "oauthServiceId": "zoom", "category": "tools", "integrationType": "communication", "tags": ["meeting", "calendar", "scheduling"] diff --git a/apps/sim/lib/integrations/oauth-service.test.ts b/apps/sim/lib/integrations/oauth-service.test.ts new file mode 100644 index 00000000000..7cbdc200574 --- /dev/null +++ b/apps/sim/lib/integrations/oauth-service.test.ts @@ -0,0 +1,52 @@ +/** + * @vitest-environment node + */ +import { describe, expect, it } from 'vitest' +import integrationsJson from '@/lib/integrations/integrations.json' +import { resolveOAuthServiceForSlug } from '@/lib/integrations/oauth-service' +import type { Integration } from '@/lib/integrations/types' + +const INTEGRATIONS = integrationsJson.integrations as readonly Integration[] + +describe('resolveOAuthServiceForSlug', () => { + it.concurrent('resolves integrations whose name differs from the OAuth service name', () => { + const jsm = resolveOAuthServiceForSlug('jira-service-management') + expect(jsm?.providerId).toBe('jira') + expect(jsm?.serviceName).toBe('Jira') + + const slides = resolveOAuthServiceForSlug('google-slides') + expect(slides?.providerId).toBe('google-drive') + + const monday = resolveOAuthServiceForSlug('monday') + expect(monday?.providerId).toBe('monday') + }) + + it.concurrent('resolves integrations whose name matches the OAuth service name', () => { + const jira = resolveOAuthServiceForSlug('jira') + expect(jira?.providerId).toBe('jira') + expect(jira?.serviceName).toBe('Jira') + + const gmail = resolveOAuthServiceForSlug('gmail') + expect(gmail?.providerId).toBe('google-email') + }) + + it.concurrent('returns null for unknown slugs', () => { + expect(resolveOAuthServiceForSlug('not-a-real-integration')).toBeNull() + }) + + it.concurrent('returns null for non-OAuth integrations', () => { + const apiKeyIntegration = INTEGRATIONS.find((entry) => entry.authType === 'api-key') + expect(apiKeyIntegration).toBeDefined() + expect(resolveOAuthServiceForSlug(apiKeyIntegration!.slug)).toBeNull() + }) + + it.concurrent('resolves every OAuth integration in the catalog', () => { + const oauthIntegrations = INTEGRATIONS.filter((entry) => entry.authType === 'oauth') + expect(oauthIntegrations.length).toBeGreaterThan(0) + + const unresolved = oauthIntegrations + .filter((entry) => resolveOAuthServiceForSlug(entry.slug) === null) + .map((entry) => entry.slug) + expect(unresolved).toEqual([]) + }) +}) diff --git a/apps/sim/lib/integrations/oauth-service.ts b/apps/sim/lib/integrations/oauth-service.ts index b97dcd5c71b..a88cef7326e 100644 --- a/apps/sim/lib/integrations/oauth-service.ts +++ b/apps/sim/lib/integrations/oauth-service.ts @@ -1,7 +1,7 @@ import type { ComponentType } from 'react' import integrationsJson from '@/lib/integrations/integrations.json' import type { Integration } from '@/lib/integrations/types' -import { OAUTH_PROVIDERS } from '@/lib/oauth' +import { getServiceConfigByServiceId } from '@/lib/oauth' import type { ServiceAccountProviderId } from '@/app/workspace/[workspaceId]/integrations/components/connect-service-account-modal' const INTEGRATIONS_DATA: readonly Integration[] = @@ -39,29 +39,25 @@ function asServiceAccountProviderId( } /** - * Looks up the OAuth service entry whose display name matches the integration - * (case-insensitive). Returns `null` for non-OAuth integrations or when no - * matching service is registered in `OAUTH_PROVIDERS`. + * Looks up the OAuth service entry registered under the integration's + * `oauthServiceId` — the service id its block declares on the `oauth-input` + * subBlock, carried into the catalog at generation time. Returns `null` for + * non-OAuth integrations or when no matching service is registered in + * `OAUTH_PROVIDERS`. */ export function resolveOAuthServiceForIntegration( integration: Integration ): OAuthServiceMatch | null { - if (integration.authType !== 'oauth') return null - const target = integration.name.toLowerCase() - for (const provider of Object.values(OAUTH_PROVIDERS)) { - for (const service of Object.values(provider.services)) { - if (service.name.toLowerCase() === target) { - return { - providerId: service.providerId, - requiredScopes: service.scopes ?? [], - serviceName: service.name, - serviceIcon: service.icon as ComponentType<{ className?: string }>, - serviceAccountProviderId: asServiceAccountProviderId(service.serviceAccountProviderId), - } - } - } + if (integration.authType !== 'oauth' || !integration.oauthServiceId) return null + const service = getServiceConfigByServiceId(integration.oauthServiceId) + if (!service) return null + return { + providerId: service.providerId, + requiredScopes: service.scopes ?? [], + serviceName: service.name, + serviceIcon: service.icon as ComponentType<{ className?: string }>, + serviceAccountProviderId: asServiceAccountProviderId(service.serviceAccountProviderId), } - return null } /** diff --git a/apps/sim/lib/integrations/types.ts b/apps/sim/lib/integrations/types.ts index d82b2ae43f3..f3da5aa719a 100644 --- a/apps/sim/lib/integrations/types.ts +++ b/apps/sim/lib/integrations/types.ts @@ -58,6 +58,11 @@ export interface Integration { triggerCount: number /** Authentication mode inferred from `BlockConfig.subBlocks`. */ authType: AuthType + /** + * OAuth service id from the block's `oauth-input` subBlock (a service key in + * `OAUTH_PROVIDERS`). Present exactly when `authType` is `'oauth'`. + */ + oauthServiceId?: string /** Hand-authored landing content baked in at generation time (see `landing-content.ts`). */ landingContent?: IntegrationLandingContent } diff --git a/apps/sim/lib/oauth/utils.test.ts b/apps/sim/lib/oauth/utils.test.ts index b455c723077..f087b6948f7 100644 --- a/apps/sim/lib/oauth/utils.test.ts +++ b/apps/sim/lib/oauth/utils.test.ts @@ -8,6 +8,7 @@ import { getScopesForService, getServiceByProviderAndId, getServiceConfigByProviderId, + getServiceConfigByServiceId, parseProvider, } from './utils' @@ -267,6 +268,36 @@ describe('getServiceConfigByProviderId', () => { }) }) +describe('getServiceConfigByServiceId', () => { + it.concurrent('should return service config for a service key', () => { + const service = getServiceConfigByServiceId('gmail') + + expect(service).toBeDefined() + expect(service?.providerId).toBe('google-email') + expect(service?.name).toBe('Gmail') + }) + + it.concurrent('should resolve the shared Jira service used by Jira Service Management', () => { + const service = getServiceConfigByServiceId('jira') + + expect(service).toBeDefined() + expect(service?.providerId).toBe('jira') + expect(service?.name).toBe('Jira') + }) + + it.concurrent('should not match on providerId values that are not service keys', () => { + const service = getServiceConfigByServiceId('google-email') + + expect(service).toBeNull() + }) + + it.concurrent('should return null for unknown service id', () => { + const service = getServiceConfigByServiceId('invalid-service') + + expect(service).toBeNull() + }) +}) + describe('getCanonicalScopesForProvider', () => { it.concurrent('should return scopes for valid providerId', () => { const scopes = getCanonicalScopesForProvider('google-email') diff --git a/apps/sim/lib/oauth/utils.ts b/apps/sim/lib/oauth/utils.ts index f95626781a2..dfa959acac7 100644 --- a/apps/sim/lib/oauth/utils.ts +++ b/apps/sim/lib/oauth/utils.ts @@ -487,6 +487,18 @@ export function getProviderIdFromServiceId(serviceId: string): string { return serviceId } +/** + * Looks up the OAuth service registered under the given service id (the key in + * a provider's `services` map). Returns `null` when no provider registers it. + */ +export function getServiceConfigByServiceId(serviceId: string): OAuthServiceConfig | null { + for (const provider of Object.values(OAUTH_PROVIDERS)) { + const service = provider.services[serviceId] + if (service) return service + } + return null +} + export function getServiceConfigByProviderId(providerId: string): OAuthServiceConfig | null { for (const provider of Object.values(OAUTH_PROVIDERS)) { for (const [key, service] of Object.entries(provider.services)) { diff --git a/scripts/generate-docs.ts b/scripts/generate-docs.ts index 0c0ab895918..bc5e193259a 100755 --- a/scripts/generate-docs.ts +++ b/scripts/generate-docs.ts @@ -222,6 +222,7 @@ interface IntegrationEntry { triggers: TriggerInfo[] triggerCount: number authType: 'oauth' | 'api-key' | 'none' + oauthServiceId?: string category: BlockCategory integrationType: IntegrationType tags?: string[] @@ -570,6 +571,36 @@ function extractAuthType(blockContent: string): 'oauth' | 'api-key' | 'none' { return 'none' } +/** + * Extract the OAuth service id from the block's `oauth-input` credential + * subBlock. Scoped to that subBlock's object literal so `serviceId` fields on + * other subBlocks (e.g. file selectors) are never picked up. + */ +function extractOAuthServiceId(blockContent: string): string | undefined { + const typeMatch = /type\s*:\s*['"]oauth-input['"]/.exec(blockContent) + if (!typeMatch) return undefined + + let depth = 0 + let objectStart = -1 + for (let i = typeMatch.index; i >= 0; i--) { + const char = blockContent[i] + if (char === '}') depth++ + else if (char === '{') { + if (depth === 0) { + objectStart = i + break + } + depth-- + } + } + if (objectStart === -1) return undefined + + const objectEnd = findMatchingClose(blockContent, objectStart) + if (objectEnd === -1) return undefined + const subBlockContent = blockContent.substring(objectStart, objectEnd) + return /serviceId\s*:\s*['"]([^'"]+)['"]/.exec(subBlockContent)?.[1] +} + /** * Extract the list of trigger IDs from the block's `triggers.available` array. * Handles blocks that declare `triggers: { enabled: true, available: [...] }`. @@ -820,6 +851,16 @@ async function writeIntegrationsJson(iconMapping: Record): Promi .replace(/^-|-$/g, '') const authType = extractAuthType(fileContent) + const oauthServiceId = authType === 'oauth' ? extractOAuthServiceId(fileContent) : undefined + // OAuth integrations resolve their connect UI through the service id + // (see `resolveOAuthServiceForIntegration`), so fail loudly rather than + // shipping a catalog entry that silently falls back to the API-key path. + if (authType === 'oauth' && !oauthServiceId) { + throw new Error( + `Block "${blockType}" is an OAuth integration but no \`serviceId\` could be ` + + `extracted from its \`oauth-input\` subBlock.` + ) + } integrations.push({ type: blockType, @@ -835,6 +876,7 @@ async function writeIntegrationsJson(iconMapping: Record): Promi triggers, triggerCount: triggers.length, authType, + ...(oauthServiceId ? { oauthServiceId } : {}), category: 'tools', integrationType, ...(config.tags ? { tags: config.tags } : {}), From 51ba3485658d4dc10ac9aba19d9238a3ced40769 Mon Sep 17 00:00:00 2001 From: waleed Date: Fri, 12 Jun 2026 11:16:12 -0700 Subject: [PATCH 2/3] test(integrations): pin OAuth service resolution for all catalog integrations; fix credential branding reverse lookup --- .../[block]/integration-block-detail.tsx | 3 +- .../connected-credential-detail.tsx | 21 +++-- .../lib/integrations/oauth-service.test.ts | 79 +++++++++++++++++++ 3 files changed, 93 insertions(+), 10 deletions(-) diff --git a/apps/sim/app/workspace/[workspaceId]/integrations/[block]/integration-block-detail.tsx b/apps/sim/app/workspace/[workspaceId]/integrations/[block]/integration-block-detail.tsx index 2868326b928..f16dbf2b10c 100644 --- a/apps/sim/app/workspace/[workspaceId]/integrations/[block]/integration-block-detail.tsx +++ b/apps/sim/app/workspace/[workspaceId]/integrations/[block]/integration-block-detail.tsx @@ -66,8 +66,7 @@ export function IntegrationBlockDetail({ integration, workspaceId }: Integration (c) => (c.type === 'oauth' || c.type === 'service_account') && c.providerId && - getServiceConfigByProviderId(c.providerId)?.name.toLowerCase() === - oauthService.serviceName.toLowerCase() + getServiceConfigByProviderId(c.providerId)?.providerId === oauthService.providerId ) }, [credentials, oauthService]) const [serviceAccountOpen, setServiceAccountOpen] = useState(false) diff --git a/apps/sim/app/workspace/[workspaceId]/integrations/connected/[credentialId]/connected-credential-detail.tsx b/apps/sim/app/workspace/[workspaceId]/integrations/connected/[credentialId]/connected-credential-detail.tsx index 82d9a0dfc30..4a8f70c916c 100644 --- a/apps/sim/app/workspace/[workspaceId]/integrations/connected/[credentialId]/connected-credential-detail.tsx +++ b/apps/sim/app/workspace/[workspaceId]/integrations/connected/[credentialId]/connected-credential-detail.tsx @@ -15,7 +15,7 @@ import { } from '@/components/emcn' import { ArrowLeft } from '@/components/emcn/icons' import { writeOAuthReturnContext } from '@/lib/credentials/client-state' -import { INTEGRATIONS } from '@/lib/integrations' +import { INTEGRATIONS, resolveOAuthServiceForIntegration } from '@/lib/integrations' import { getServiceConfigByProviderId } from '@/lib/oauth' import { AddPeopleModal, @@ -98,15 +98,20 @@ export function ConnectedCredentialDetail({ }, [credential]) /** - * Resolve the integration block type from the OAuth service name so the - * header tile can render with the same brand background used by the rows on - * the integrations list page. + * Resolve the integration block type from the credential's OAuth service so + * the header tile can render with the same brand background used by the rows + * on the integrations list page. Several integrations can share one service + * (e.g. Jira and Jira Service Management); the one named after the service + * is preferred since it is the service's canonical integration. */ const integrationBlockType = useMemo(() => { - const name = serviceConfig?.name.toLowerCase() - if (!name) return '' - const match = INTEGRATIONS.find((i) => i.name.toLowerCase() === name) - return match?.type ?? '' + if (!serviceConfig) return '' + const candidates = INTEGRATIONS.filter( + (i) => resolveOAuthServiceForIntegration(i)?.providerId === serviceConfig.providerId + ) + const serviceName = serviceConfig.name.toLowerCase() + const canonical = candidates.find((i) => i.name.toLowerCase() === serviceName) + return (canonical ?? candidates[0])?.type ?? '' }, [serviceConfig]) const handleReconnectOAuth = async () => { diff --git a/apps/sim/lib/integrations/oauth-service.test.ts b/apps/sim/lib/integrations/oauth-service.test.ts index 7cbdc200574..4cdf616ae4a 100644 --- a/apps/sim/lib/integrations/oauth-service.test.ts +++ b/apps/sim/lib/integrations/oauth-service.test.ts @@ -8,6 +8,64 @@ import type { Integration } from '@/lib/integrations/types' const INTEGRATIONS = integrationsJson.integrations as readonly Integration[] +/** + * Pinned slug → OAuth providerId mapping for every OAuth integration in the + * catalog. Guards against silent drift between block `serviceId`s, the + * generated catalog, and `OAUTH_PROVIDERS` — the failure mode that made + * Jira Service Management, Google Slides, and Monday fall back to the + * API-key connect path. + */ +const EXPECTED_PROVIDER_BY_SLUG: Record = { + airtable: 'airtable', + asana: 'asana', + attio: 'attio', + 'azure-ad': 'microsoft-ad', + box: 'box', + 'cal-com': 'calcom', + confluence: 'confluence', + docusign: 'docusign', + dropbox: 'dropbox', + gmail: 'google-email', + 'google-ads': 'google-ads', + 'google-bigquery': 'google-bigquery', + 'google-calendar': 'google-calendar', + 'google-contacts': 'google-contacts', + 'google-docs': 'google-docs', + 'google-drive': 'google-drive', + 'google-forms': 'google-forms', + 'google-groups': 'google-groups', + 'google-meet': 'google-meet', + 'google-sheets': 'google-sheets', + 'google-slides': 'google-drive', + 'google-tasks': 'google-tasks', + 'google-vault': 'google-vault', + hubspot: 'hubspot', + jira: 'jira', + 'jira-service-management': 'jira', + linear: 'linear', + linkedin: 'linkedin', + 'microsoft-dataverse': 'microsoft-dataverse', + 'microsoft-excel': 'microsoft-excel', + 'microsoft-planner': 'microsoft-planner', + 'microsoft-teams': 'microsoft-teams', + monday: 'monday', + notion: 'notion', + onedrive: 'onedrive', + outlook: 'outlook', + pipedrive: 'pipedrive', + reddit: 'reddit', + salesforce: 'salesforce', + sharepoint: 'sharepoint', + shopify: 'shopify', + slack: 'slack', + trello: 'trello', + wealthbox: 'wealthbox', + webflow: 'webflow', + wordpress: 'wordpress', + x: 'x', + zoom: 'zoom', +} + describe('resolveOAuthServiceForSlug', () => { it.concurrent('resolves integrations whose name differs from the OAuth service name', () => { const jsm = resolveOAuthServiceForSlug('jira-service-management') @@ -49,4 +107,25 @@ describe('resolveOAuthServiceForSlug', () => { .map((entry) => entry.slug) expect(unresolved).toEqual([]) }) + + it.concurrent('resolves the pinned provider for every enumerated OAuth integration', () => { + const resolved = Object.fromEntries( + Object.keys(EXPECTED_PROVIDER_BY_SLUG).map((slug) => [ + slug, + resolveOAuthServiceForSlug(slug)?.providerId ?? null, + ]) + ) + expect(resolved).toEqual(EXPECTED_PROVIDER_BY_SLUG) + }) + + it.concurrent('carries oauthServiceId for exactly the OAuth catalog entries', () => { + const missing = INTEGRATIONS.filter( + (entry) => entry.authType === 'oauth' && !entry.oauthServiceId + ).map((entry) => entry.slug) + const unexpected = INTEGRATIONS.filter( + (entry) => entry.authType !== 'oauth' && entry.oauthServiceId + ).map((entry) => entry.slug) + expect(missing).toEqual([]) + expect(unexpected).toEqual([]) + }) }) From fa21369402d0b680526b1990f526f332b53d0d94 Mon Sep 17 00:00:00 2001 From: waleed Date: Fri, 12 Jun 2026 11:18:05 -0700 Subject: [PATCH 3/3] fix(docs-gen): blank string literals and comments before brace scanning in extractOAuthServiceId --- scripts/generate-docs.ts | 22 +++++++++++++++++++--- 1 file changed, 19 insertions(+), 3 deletions(-) diff --git a/scripts/generate-docs.ts b/scripts/generate-docs.ts index bc5e193259a..c68b8a0b768 100755 --- a/scripts/generate-docs.ts +++ b/scripts/generate-docs.ts @@ -571,19 +571,35 @@ function extractAuthType(blockContent: string): 'oauth' | 'api-key' | 'none' { return 'none' } +/** + * Length-preserving copy of `content` with string-literal and comment + * interiors blanked out, so delimiter scans cannot be tripped by braces or + * quotes inside them. Indices into the result line up with indices into + * `content`. + */ +function blankStringsAndComments(content: string): string { + return content.replace( + /(['"`])(?:\\[\s\S]|(?!\1)[^\\])*\1|\/\/[^\n]*|\/\*[\s\S]*?\*\//g, + (match) => match[0] + match.slice(1, -1).replace(/[^\n]/g, ' ') + match[match.length - 1] + ) +} + /** * Extract the OAuth service id from the block's `oauth-input` credential * subBlock. Scoped to that subBlock's object literal so `serviceId` fields on - * other subBlocks (e.g. file selectors) are never picked up. + * other subBlocks (e.g. file selectors) are never picked up. Brace matching + * runs on a blanked copy of the content so string literals and comments + * containing braces cannot skew it. */ function extractOAuthServiceId(blockContent: string): string | undefined { const typeMatch = /type\s*:\s*['"]oauth-input['"]/.exec(blockContent) if (!typeMatch) return undefined + const scannable = blankStringsAndComments(blockContent) let depth = 0 let objectStart = -1 for (let i = typeMatch.index; i >= 0; i--) { - const char = blockContent[i] + const char = scannable[i] if (char === '}') depth++ else if (char === '{') { if (depth === 0) { @@ -595,7 +611,7 @@ function extractOAuthServiceId(blockContent: string): string | undefined { } if (objectStart === -1) return undefined - const objectEnd = findMatchingClose(blockContent, objectStart) + const objectEnd = findMatchingClose(scannable, objectStart) if (objectEnd === -1) return undefined const subBlockContent = blockContent.substring(objectStart, objectEnd) return /serviceId\s*:\s*['"]([^'"]+)['"]/.exec(subBlockContent)?.[1]