mirror of
https://github.com/simstudioai/sim.git
synced 2026-09-24 15:45:35 +08:00
fix(integrations): resolve OAuth connect UI by service id instead of display name (#5001)
* fix(integrations): resolve OAuth connect UI by service id instead of display name * test(integrations): pin OAuth service resolution for all catalog integrations; fix credential branding reverse lookup * fix(docs-gen): blank string literals and comments before brace scanning in extractOAuthServiceId
This commit is contained in:
+1
-2
@@ -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)
|
||||
|
||||
+13
-8
@@ -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 () => {
|
||||
|
||||
@@ -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"]
|
||||
|
||||
@@ -0,0 +1,131 @@
|
||||
/**
|
||||
* @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[]
|
||||
|
||||
/**
|
||||
* 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<string, string> = {
|
||||
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')
|
||||
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([])
|
||||
})
|
||||
|
||||
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([])
|
||||
})
|
||||
})
|
||||
@@ -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
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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')
|
||||
|
||||
@@ -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)) {
|
||||
|
||||
@@ -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,52 @@ 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. 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 = scannable[i]
|
||||
if (char === '}') depth++
|
||||
else if (char === '{') {
|
||||
if (depth === 0) {
|
||||
objectStart = i
|
||||
break
|
||||
}
|
||||
depth--
|
||||
}
|
||||
}
|
||||
if (objectStart === -1) return undefined
|
||||
|
||||
const objectEnd = findMatchingClose(scannable, 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 +867,16 @@ async function writeIntegrationsJson(iconMapping: Record<string, string>): 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 +892,7 @@ async function writeIntegrationsJson(iconMapping: Record<string, string>): Promi
|
||||
triggers,
|
||||
triggerCount: triggers.length,
|
||||
authType,
|
||||
...(oauthServiceId ? { oauthServiceId } : {}),
|
||||
category: 'tools',
|
||||
integrationType,
|
||||
...(config.tags ? { tags: config.tags } : {}),
|
||||
|
||||
Reference in New Issue
Block a user