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:
Waleed
2026-06-12 11:38:01 -07:00
committed by GitHub
parent 2c4d9e98f9
commit 636bd74f06
9 changed files with 314 additions and 29 deletions
@@ -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)
@@ -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([])
})
})
+15 -19
View File
@@ -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
}
/**
+5
View File
@@ -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
}
+31
View File
@@ -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')
+12
View File
@@ -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)) {
+58
View File
@@ -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 } : {}),