diff --git a/apps/sim/app/workspace/[workspaceId]/home/components/message-content/components/special-tags/special-tags.tsx b/apps/sim/app/workspace/[workspaceId]/home/components/message-content/components/special-tags/special-tags.tsx index fd03be2d91a..e9e90fdd3e9 100644 --- a/apps/sim/app/workspace/[workspaceId]/home/components/message-content/components/special-tags/special-tags.tsx +++ b/apps/sim/app/workspace/[workspaceId]/home/components/message-content/components/special-tags/special-tags.tsx @@ -2478,11 +2478,10 @@ function ServiceAccountConnectDisplay({ () => (data.provider ? resolveServiceAccountIntegration(data.provider) : null), [data.provider] ) - const service = useMemo(() => (match ? resolveOAuthServiceForSlug(match.slug) : null), [match]) const target = useServiceAccountConnectTarget({ serviceAccountProviderId: match?.serviceAccountProviderId, serviceName: match?.serviceName, - serviceIcon: service?.serviceIcon, + serviceIcon: match?.serviceIcon, }) // A credentialId reconnects (rotates the secret on) that existing service diff --git a/apps/sim/app/workspace/[workspaceId]/home/components/suggested-actions/suggested-actions.tsx b/apps/sim/app/workspace/[workspaceId]/home/components/suggested-actions/suggested-actions.tsx index e4508e8d8a9..c8a1e0ca1e2 100644 --- a/apps/sim/app/workspace/[workspaceId]/home/components/suggested-actions/suggested-actions.tsx +++ b/apps/sim/app/workspace/[workspaceId]/home/components/suggested-actions/suggested-actions.tsx @@ -5,13 +5,14 @@ import { INTEGRATION_METADATA } from '@sim/deployment-config/integration-metadat import { ArrowRight, cn, OverflowText } from '@sim/emcn' import { Table } from '@sim/emcn/icons' import { stripVersionSuffix } from '@sim/utils/string' -import { useParams } from 'next/navigation' +import { useParams, useRouter } from 'next/navigation' import { usePostHog } from 'posthog-js/react' import { HomeSection } from '@/components/home/home-section' import { GmailIcon, SlackIcon } from '@/components/icons' import { resolveOAuthServiceForIntegration, resolveOAuthServiceForSlug, + resolveServiceAccountServiceForIntegration, } from '@/lib/integrations/oauth-service' import { captureEvent } from '@/lib/posthog/client' import { ConnectOAuthModal } from '@/app/workspace/[workspaceId]/components/connect-oauth-modal' @@ -21,6 +22,10 @@ import type { OAuthConnectTarget, } from '@/app/workspace/[workspaceId]/home/components/suggested-actions/types' import { weightedSample } from '@/app/workspace/[workspaceId]/home/components/suggested-actions/weighted-sample' +import { + CONNECT_MODE, + CONNECT_QUERY_PARAM, +} from '@/app/workspace/[workspaceId]/integrations/connect-route' import { BrandIcon } from '@/blocks/brand-icon' import { getAllBlockMeta } from '@/blocks/registry' import type { ModuleTag } from '@/blocks/types' @@ -240,6 +245,7 @@ interface SuggestedActionsProps { export function SuggestedActions({ onSelectPrompt, organizationId }: SuggestedActionsProps) { const params = useParams<{ workspaceId?: string }>() const workspaceId = organizationId ? undefined : params.workspaceId + const router = useRouter() const posthog = usePostHog() const { data: credentials = EMPTY_CREDENTIALS } = useWorkspaceCredentials({ @@ -312,7 +318,23 @@ export function SuggestedActions({ onSelectPrompt, organizationId }: SuggestedAc return } const target = resolveOAuthServiceForSlug(action.slug) - if (target) setOAuthTarget(target) + if (target) { + setOAuthTarget(target) + return + } + /** + * The row names an integration this surface cannot connect inline: one + * authenticated by a stored service account, or one whose OAuth service the + * catalog does not carry. Both used to drop the click silently. Hand off to + * the detail page instead — with the service-account deep link when that is + * the flow it offers, so the modal still opens in one click. + */ + const integration = INTEGRATION_METADATA.find((entry) => entry.slug === action.slug) + const connectSuffix = + integration && resolveServiceAccountServiceForIntegration(integration) + ? `?${CONNECT_QUERY_PARAM}=${CONNECT_MODE.serviceAccount}` + : '' + router.push(`/workspace/${workspaceId}/integrations/${action.slug}${connectSuffix}`) } const handleToggleExpanded = () => { diff --git a/apps/sim/app/workspace/[workspaceId]/integrations/[block]/integration-block-detail.test.tsx b/apps/sim/app/workspace/[workspaceId]/integrations/[block]/integration-block-detail.test.tsx new file mode 100644 index 00000000000..4cad4049067 --- /dev/null +++ b/apps/sim/app/workspace/[workspaceId]/integrations/[block]/integration-block-detail.test.tsx @@ -0,0 +1,186 @@ +/** + * @vitest-environment jsdom + */ + +import { act } from 'react' +import { deploymentShapeMock } from '@sim/testing/mocks/deployment-shape.mock' +import { nextNavigationMock } from '@sim/testing/mocks/next-navigation.mock' +import { createRoot, type Root } from 'react-dom/client' +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' +import type { Integration } from '@/lib/integrations/types' + +const { availabilityState } = vi.hoisted(() => ({ + /** `null` stands for an availability answer that has not arrived. */ + availabilityState: { + availability: null as { state: string; oauthAvailable: boolean } | null, + isLoading: false, + }, +})) + +vi.mock('next/navigation', () => nextNavigationMock) +vi.mock('nuqs', () => ({ useQueryState: () => [null, vi.fn()] })) +vi.mock('@/hooks/use-oauth-return', () => ({ useOAuthReturnRouter: () => {} })) +vi.mock('@/hooks/queries/credentials', () => ({ + useWorkspaceCredentials: () => ({ data: [], isPending: false }), +})) +vi.mock('@/app/workspace/[workspaceId]/integrations/hooks/use-scroll-restoration', () => ({ + useScrollRestoration: () => {}, +})) +vi.mock('@/lib/core/config/deployment-shape', () => deploymentShapeMock) +vi.mock('@/hooks/use-permission-config', () => ({ + usePermissionConfig: () => ({ + integrationAvailability: new Map( + availabilityState.availability + ? [ + ['snowflake', availabilityState.availability], + ['jira', availabilityState.availability], + ] + : [] + ), + isLoading: availabilityState.isLoading, + }), +})) + +/** Heavy leaf sections carry their own coverage; the header is what is under test. */ +vi.mock('@/app/workspace/[workspaceId]/integrations/[block]/integration-skills-section', () => ({ + IntegrationSkillsSection: () => null, +})) +vi.mock('@/app/workspace/[workspaceId]/integrations/components/integration-section', () => ({ + IntegrationSection: () => null, +})) +vi.mock('@/app/workspace/[workspaceId]/integrations/components/integrations-showcase', () => ({ + IntegrationTile: () => null, +})) +vi.mock( + '@/app/workspace/[workspaceId]/settings/components/settings-section/settings-section', + () => ({ + SettingsSection: () => null, + }) +) +vi.mock('@/app/workspace/[workspaceId]/components/connect-oauth-modal', () => ({ + ConnectOAuthModal: () =>
, +})) +vi.mock( + '@/app/workspace/[workspaceId]/integrations/components/connect-personal-token-modal', + () => ({ + ConnectPersonalTokenModal: () => null, + }) +) + +import { getServiceAccountConnectNoun } from '@/lib/credentials/service-account-provider-ids' +import { INTEGRATIONS } from '@/lib/integrations' +import { IntegrationBlockDetail } from '@/app/workspace/[workspaceId]/integrations/[block]/integration-block-detail' + +/** Snowflake authenticates only with a stored service account; Jira also offers OAuth. */ +const SERVICE_ACCOUNT_ONLY = INTEGRATIONS.find((i) => i.slug === 'snowflake') as Integration +const OAUTH_WITH_SERVICE_ACCOUNT = INTEGRATIONS.find((i) => i.slug === 'jira') as Integration + +/** + * Derived rather than written out: the vendor-accurate noun is owned by + * `getServiceAccountConnectNoun`, so hardcoding it here would make this test + * fail on a copy change that is none of its business. + */ +const STORED_CREDENTIAL_LABEL = `Add ${getServiceAccountConnectNoun('snowflake-service-account')}` + +let root: Root | null = null +let container: HTMLDivElement | null = null + +function mount(integration: Integration) { + ;(globalThis as { IS_REACT_ACT_ENVIRONMENT?: boolean }).IS_REACT_ACT_ENVIRONMENT = true + container = document.createElement('div') + document.body.appendChild(container) + root = createRoot(container) + act(() => + root?.render() + ) +} + +/** + * The header's primary action, tagged by control kind. The tag matters: a + * `ChipDropdown` trigger renders the same "Add to Sim" placeholder as the plain + * chip, so comparing label text alone cannot tell one connect option from two. + * Radix marks its trigger with `aria-haspopup`; a bare `Chip` carries none. + */ +function headerAction(): string { + const bar = container?.firstElementChild?.firstElementChild + const buttons = Array.from(bar?.querySelectorAll('button') ?? []) + return buttons + .map((b) => `${b.hasAttribute('aria-haspopup') ? 'dropdown' : 'chip'}:${b.textContent?.trim()}`) + .join('|') +} + +beforeEach(() => { + availabilityState.availability = { state: 'ready', oauthAvailable: false } + availabilityState.isLoading = false +}) + +afterEach(() => { + if (root) act(() => root?.unmount()) + container?.remove() + root = null + container = null +}) + +describe('IntegrationBlockDetail header action', () => { + it('offers the stored service account for an integration with no OAuth path', () => { + mount(SERVICE_ACCOUNT_ONLY) + + expect(headerAction()).toContain(`chip:${STORED_CREDENTIAL_LABEL}`) + }) + + it('keeps offering it while the availability answer is still in flight', () => { + availabilityState.availability = null + availabilityState.isLoading = true + mount(SERVICE_ACCOUNT_ONLY) + + expect(headerAction()).toContain(`chip:${STORED_CREDENTIAL_LABEL}`) + }) + + /** + * A failed request leaves availability unresolved once loading ends. Hiding the + * control there strands a user who has a valid stored account behind a fetch + * they cannot retry, so it fails open — the server still refuses a provider the + * deployment does not offer. + */ + it('fails open when the availability request settles with no answer', () => { + availabilityState.availability = null + availabilityState.isLoading = false + mount(SERVICE_ACCOUNT_ONLY) + + expect(headerAction()).toContain(`chip:${STORED_CREDENTIAL_LABEL}`) + }) + + /** + * The OAuth path already defaults to available while unknown, so relaxing the + * service-account one too would widen this header from a chip to a dropdown and + * collapse it again as the config lands. + */ + it('does not add a second option to an OAuth integration while loading', () => { + availabilityState.availability = null + availabilityState.isLoading = true + mount(OAUTH_WITH_SERVICE_ACCOUNT) + + expect(headerAction()).toBe('chip:Add to Sim') + }) + + /** + * "Unavailable" is a verdict about a connection the deployment grants. An + * integration authenticated by a stored service account still runs on the + * user's own API key, so it keeps the ordinary call to action instead. + */ + it('never calls a stored-credential integration unavailable', () => { + availabilityState.availability = { state: 'unavailable', oauthAvailable: false } + mount(SERVICE_ACCOUNT_ONLY) + + const action = headerAction() + expect(action).not.toContain('Unavailable') + expect(action).toContain('chip:Add to Sim') + }) + + it('still calls an OAuth integration unavailable when its client is missing', () => { + availabilityState.availability = { state: 'unavailable', oauthAvailable: false } + mount(OAUTH_WITH_SERVICE_ACCOUNT) + + expect(headerAction()).toContain('chip:Unavailable') + }) +}) 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 af31043f573..4af97190c70 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 @@ -12,6 +12,7 @@ import { type Integration, resolveCredentialDisplay, resolveOAuthServiceForIntegration, + resolveServiceAccountServiceForIntegration, } from '@/lib/integrations' import { credentialProviderMatchesService } from '@/lib/oauth' import { ConnectOAuthModal } from '@/app/workspace/[workspaceId]/components/connect-oauth-modal' @@ -69,6 +70,7 @@ export function IntegrationBlockDetail({ integration, workspaceId }: Integration const matchingTemplates = getTemplatesForBlock(integration.type) const suggestedSkills = getSuggestedSkillsForBlock(integration.type) const oauthService = resolveOAuthServiceForIntegration(integration) + const serviceAccountService = resolveServiceAccountServiceForIntegration(integration) const { integrationAvailability, isLoading: permissionConfigLoading } = usePermissionConfig() const { chatEnabled } = useDeploymentShape() const availability = integrationAvailability.get(integration.type.toLowerCase()) @@ -94,22 +96,39 @@ export function IntegrationBlockDetail({ integration, workspaceId }: Integration const connectedCredentials = useMemo(() => { if (integration.type === 'gitlab') return credentials.filter((c) => c.type === 'personal_token' && c.providerId === 'gitlab') - if (!oauthService) return [] + const credentialService = oauthService ?? serviceAccountService + if (!credentialService) return [] return credentials.filter( (c) => (c.type === 'oauth' || c.type === 'service_account') && c.providerId && - credentialProviderMatchesService(c.providerId, oauthService) + credentialProviderMatchesService(c.providerId, credentialService) ) - }, [credentials, oauthService, integration.type]) + }, [credentials, oauthService, serviceAccountService, integration.type]) const [serviceAccountOpen, setServiceAccountOpen] = useState(false) const serviceAccountTarget = useServiceAccountConnectTarget({ - serviceAccountProviderId: oauthService?.serviceAccountProviderId, - serviceName: oauthService?.serviceName, - serviceIcon: oauthService?.serviceIcon, + serviceAccountProviderId: serviceAccountService?.serviceAccountProviderId, + serviceName: serviceAccountService?.serviceName, + serviceIcon: serviceAccountService?.serviceIcon, }) - const serviceAccountDeploymentAvailable = - availability?.state === 'ready' || availability?.state === 'limited' + /** + * Unknown availability means two different things, and they want opposite + * defaults. + * + * While the permission config is still in flight the answer is imminent, so + * only an integration whose *sole* path is a stored service account offers the + * control — for one that also has OAuth, a `true` here would widen the header + * from a chip to a dropdown and then collapse it again as the config lands. + * + * Once both queries have settled and still produced nothing, the request + * failed, and withholding the control strands a user who has a perfectly good + * stored account behind a fetch they cannot retry. Fail open there, matching + * the `?? true` that `oauthAvailable` above already applies to the OAuth path; + * the server still refuses a provider the deployment does not offer. + */ + const serviceAccountDeploymentAvailable = availability + ? availability.state === 'ready' || availability.state === 'limited' + : !oauthService || !permissionConfigLoading const hasServiceAccount = serviceAccountDeploymentAvailable && Boolean(serviceAccountTarget) && @@ -147,28 +166,29 @@ export function IntegrationBlockDetail({ integration, workspaceId }: Integration setConnectMode, ]) - const connectOptions = oauthService - ? [ - ...(oauthAvailable - ? [ - { - value: CONNECT_MODE.oauth, - label: 'Connect with OAuth', - icon: oauthService.serviceIcon, - }, - ] - : []), - ...(hasServiceAccount - ? [ - { - value: CONNECT_MODE.serviceAccount, - label: serviceAccountConnectLabel, - icon: serviceAccountTarget?.serviceIcon ?? oauthService.serviceIcon, - }, - ] - : []), - ] - : [] + const connectOptions = + oauthService || serviceAccountService + ? [ + ...(oauthAvailable && oauthService + ? [ + { + value: CONNECT_MODE.oauth, + label: 'Connect with OAuth', + icon: oauthService.serviceIcon, + }, + ] + : []), + ...(hasServiceAccount + ? [ + { + value: CONNECT_MODE.serviceAccount, + label: serviceAccountConnectLabel, + icon: serviceAccountTarget?.serviceIcon ?? serviceAccountService?.serviceIcon, + }, + ] + : []), + ] + : [] const handleSelectConnectOption = (value: string) => { if (value === CONNECT_MODE.oauth) setOAuthOpen(true) @@ -180,6 +200,21 @@ export function IntegrationBlockDetail({ integration, workspaceId }: Integration router.push(`/workspace/${workspaceId}/home`) } + /** + * Shown when no connect flow is on offer. "Unavailable" is a verdict about a + * connection the deployment grants, so it belongs only to an integration with + * an OAuth path. One authenticated by a stored service account still runs on + * the user's own API key, and so keeps the catalog's ordinary call to action — + * the same fallback an integration with no credential service at all gets. + */ + const connectFallback = oauthService ? ( + Unavailable + ) : chatEnabled ? ( + + Add to Sim + + ) : null + return (
@@ -191,7 +226,7 @@ export function IntegrationBlockDetail({ integration, workspaceId }: Integration setPersonalTokenOpen(true)}> Add personal token - ) : oauthService ? ( + ) : oauthService || serviceAccountService ? ( connectOptions.length > 1 ? ( ) : ( - Unavailable + connectFallback ) - ) : chatEnabled ? ( - - Add to Sim - - ) : null} + ) : ( + connectFallback + )}
{personalTokenAvailable && ( @@ -247,7 +280,9 @@ export function IntegrationBlockDetail({ integration, workspaceId }: Integration onOpenChange={setServiceAccountOpen} workspaceId={workspaceId} serviceAccountProviderId={serviceAccountTarget.serviceAccountProviderId} - atlassianProduct={oauthService?.providerId === 'confluence' ? 'confluence' : 'jira'} + atlassianProduct={ + serviceAccountService?.providerId === 'confluence' ? 'confluence' : 'jira' + } serviceName={serviceAccountTarget.serviceName} serviceIcon={serviceAccountTarget.serviceIcon} /> diff --git a/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/search-modal/integration-search-items.test.ts b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/search-modal/integration-search-items.test.ts new file mode 100644 index 00000000000..0708d243445 --- /dev/null +++ b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/search-modal/integration-search-items.test.ts @@ -0,0 +1,76 @@ +/** + * @vitest-environment node + */ +import { describe, expect, it } from 'vitest' +import { CONNECT_MODE } from '@/app/workspace/[workspaceId]/integrations/connect-route' +import { buildIntegrationSearchItems } from '@/app/workspace/[workspaceId]/w/components/sidebar/components/search-modal/integration-search-items' + +/** An OAuth integration, a stored-service-account one, and one with no credential. */ +const OAUTH_SLUG = 'airtable' +const SERVICE_ACCOUNT_SLUG = 'claude-managed-agents' +const NO_CREDENTIAL_SLUG = '1password' + +function hrefFor(slug: string, items: ReturnType): string { + const item = items.find((candidate) => candidate.id === slug) + if (!item) throw new Error(`Missing search item for ${slug}`) + return item.href +} + +describe('buildIntegrationSearchItems', () => { + it('deep-links each integration to the connect flow its catalog entry describes', () => { + const items = buildIntegrationSearchItems('workspace-1') + + expect(hrefFor(OAUTH_SLUG, items)).toBe( + `/workspace/workspace-1/integrations/${OAUTH_SLUG}?connect=${CONNECT_MODE.oauth}` + ) + /** + * The default is what the sidebar falls back to while deployment + * availability is unknown. Assuming OAuth here would send this integration + * to a detail page with no OAuth flow to open, and the deep link would + * silently do nothing. + */ + expect(hrefFor(SERVICE_ACCOUNT_SLUG, items)).toBe( + `/workspace/workspace-1/integrations/${SERVICE_ACCOUNT_SLUG}?connect=${CONNECT_MODE.serviceAccount}` + ) + expect(hrefFor(NO_CREDENTIAL_SLUG, items)).toBe( + `/workspace/workspace-1/integrations/${NO_CREDENTIAL_SLUG}` + ) + }) + + it('offers the catalog flow to the resolver and never consults it without a credential', () => { + const seen: Record = {} + const items = buildIntegrationSearchItems( + 'workspace-1', + undefined, + (blockType, catalogMode) => { + seen[blockType] = catalogMode + return catalogMode + } + ) + + expect(seen.airtable).toBe(CONNECT_MODE.oauth) + expect(seen.managed_agent).toBe(CONNECT_MODE.serviceAccount) + expect(seen.onepassword).toBeUndefined() + expect(hrefFor(NO_CREDENTIAL_SLUG, items)).toBe( + `/workspace/workspace-1/integrations/${NO_CREDENTIAL_SLUG}` + ) + }) + + it('drops the deep link when the deployment offers no connect flow', () => { + const items = buildIntegrationSearchItems('workspace-1', undefined, () => null) + + expect(hrefFor(OAUTH_SLUG, items)).toBe(`/workspace/workspace-1/integrations/${OAUTH_SLUG}`) + expect(hrefFor(SERVICE_ACCOUNT_SLUG, items)).toBe( + `/workspace/workspace-1/integrations/${SERVICE_ACCOUNT_SLUG}` + ) + }) + + it('applies the block allowlist', () => { + const items = buildIntegrationSearchItems( + 'workspace-1', + (blockType) => blockType === 'managed_agent' + ) + + expect(items.map((item) => item.id)).toEqual([SERVICE_ACCOUNT_SLUG]) + }) +}) diff --git a/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/search-modal/integration-search-items.ts b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/search-modal/integration-search-items.ts index 1d82af1f806..1ca8ae631b7 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/search-modal/integration-search-items.ts +++ b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/search-modal/integration-search-items.ts @@ -3,6 +3,7 @@ import { blockTypeToIconMap, INTEGRATIONS, resolveCredentialDisplay } from '@/li import { CONNECT_MODE, CONNECT_QUERY_PARAM, + type ConnectMode, } from '@/app/workspace/[workspaceId]/integrations/connect-route' import type { IntegrationSearchItem } from '@/app/workspace/[workspaceId]/w/components/sidebar/components/search-modal/utils' import type { WorkspaceCredential } from '@/hooks/queries/credentials' @@ -21,7 +22,16 @@ const INTEGRATION_BASES: readonly { icon: ComponentType<{ className?: string }> bgColor: string slug: string - authType: string + /** + * The connect flow this integration would offer knowing only the catalog, or + * `null` when it has no credential at all. Derived from the credential + * services rather than `authType`, because an integration can be `api-key` + * there and still authenticate with a stored service account (NetSuite, + * Snowflake, Harmonic, Claude Platform). Used as the deep link while + * deployment availability is unknown, where assuming OAuth would send those + * four to a page that has no OAuth flow to open. + */ + catalogConnectMode: ConnectMode | null blockType: string }[] = INTEGRATIONS.flatMap((integration) => { const icon = blockTypeToIconMap[integration.type] @@ -33,7 +43,11 @@ const INTEGRATION_BASES: readonly { icon, bgColor: integration.bgColor, slug: integration.slug, - authType: integration.authType, + catalogConnectMode: integration.oauthServiceId + ? CONNECT_MODE.oauth + : integration.serviceAccountServiceId + ? CONNECT_MODE.serviceAccount + : null, blockType: integration.type, }, ] @@ -41,19 +55,27 @@ const INTEGRATION_BASES: readonly { /** * Builds the full integration catalog as search items for a given workspace. - * OAuth integrations link directly to the detail page with `?connect=oauth` so - * the connect modal auto-opens (via the detail page's `useEffect` on - * `CONNECT_QUERY_PARAM`). Non-OAuth integrations link to the plain detail page. + * An integration with a credential links to the detail page carrying the connect + * mode `getConnectMode` picks for it, so that modal auto-opens (via the detail + * page's `useEffect` on `CONNECT_QUERY_PARAM`). Everything else — and anything + * with no connect flow currently on offer — links to the plain detail page. + * + * `getConnectMode` receives the catalog's own answer as its second argument, to + * return verbatim when deployment availability cannot be read; returning `null` + * means the deployment offers no connect flow, which is not the same thing. */ export function buildIntegrationSearchItems( workspaceId: string, isBlockAllowed: (blockType: string) => boolean = () => true, - getConnectMode: ( - blockType: string - ) => (typeof CONNECT_MODE)[keyof typeof CONNECT_MODE] | null = () => CONNECT_MODE.oauth + getConnectMode: (blockType: string, catalogConnectMode: ConnectMode) => ConnectMode | null = ( + _blockType, + catalogConnectMode + ) => catalogConnectMode ): IntegrationSearchItem[] { return INTEGRATION_BASES.filter((base) => isBlockAllowed(base.blockType)).map((base) => { - const connectMode = base.authType === 'oauth' ? getConnectMode(base.blockType) : null + const connectMode = base.catalogConnectMode + ? getConnectMode(base.blockType, base.catalogConnectMode) + : null const connectSuffix = connectMode ? `?${CONNECT_QUERY_PARAM}=${connectMode}` : '' return { id: base.id, diff --git a/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/sidebar.tsx b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/sidebar.tsx index 683439f271b..c29fae78bdc 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/sidebar.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/sidebar.tsx @@ -1056,11 +1056,28 @@ export const Sidebar = memo(function Sidebar({ organizationHref }: SidebarProps) () => permissionConfig.hideIntegrationsTab ? [] - : buildIntegrationSearchItems(workspaceId, isBlockAllowed, (blockType) => { + : buildIntegrationSearchItems(workspaceId, isBlockAllowed, (blockType, catalogMode) => { const availability = integrationAvailability.get(blockType.toLowerCase()) - if (!availability) return CONNECT_MODE.oauth - if (availability?.oauthAvailable) return CONNECT_MODE.oauth - if (availability?.state === 'limited') return CONNECT_MODE.serviceAccount + /** + * Availability is unknown while it loads and after a failed fetch, + * so keep the catalog's own flow rather than assuming OAuth — that + * assumption sends a service-account-only integration to a page with + * no OAuth modal to open, costing the search result its one click. + */ + if (!availability) return catalogMode + if (availability.oauthAvailable) return CONNECT_MODE.oauth + /** + * Anything still connectable once OAuth is out is the stored + * service account, which is the detail page's own test for offering + * it. Matching only `limited` misses an integration whose *only* + * credential is a service account: it is plain `ready`, and would + * open the detail page with no modal. The caller only asks about + * integrations that have a credential service, so an ordinary + * API-key block never reaches this. + */ + if (availability.state === 'ready' || availability.state === 'limited') { + return CONNECT_MODE.serviceAccount + } return null }), [workspaceId, permissionConfig.hideIntegrationsTab, isBlockAllowed, integrationAvailability] diff --git a/apps/sim/lib/integrations/availability.server.test.ts b/apps/sim/lib/integrations/availability.server.test.ts index b976b30ef18..7f9a7d22865 100644 --- a/apps/sim/lib/integrations/availability.server.test.ts +++ b/apps/sim/lib/integrations/availability.server.test.ts @@ -12,6 +12,7 @@ import { afterAll, describe, expect, it } from 'vitest' import { getIntegrationTypesForOAuthServiceId, type IntegrationAvailability, + isDeploymentGatedIntegrationType, isOAuthServiceAllowedByIntegrationTypes, resolveIntegrationAvailability, resolveIntegrationAvailabilityStateForVisibility, @@ -34,6 +35,19 @@ afterAll(resetEnvMock) const integrations = integrationsJson.integrations as readonly Integration[] +/** + * Integrations whose only credential is a stored service account, each paired + * with the block type the catalog gates it by. The two differ for Claude + * Platform (`claude-platform` / `managed_agent`), so a parameterization keyed + * on the service id alone silently omits it. + */ +const STORED_CREDENTIAL_INTEGRATIONS = [ + { serviceId: 'netsuite', blockType: 'netsuite' }, + { serviceId: 'snowflake', blockType: 'snowflake' }, + { serviceId: 'harmonic', blockType: 'harmonic' }, + { serviceId: 'claude-platform', blockType: 'managed_agent' }, +] + function availabilityFor( type: string, values: Parameters[0] = {} @@ -44,6 +58,31 @@ function availabilityFor( } describe('integration availability', () => { + it.each(STORED_CREDENTIAL_INTEGRATIONS)( + 'makes $serviceId stored credentials available without configuring an OAuth client', + ({ serviceId, blockType }) => { + expect(availabilityFor(blockType)).toMatchObject({ + state: 'ready', + oauthAvailable: false, + serviceAccountAvailable: true, + missingFields: [], + }) + expect(getIntegrationTypesForOAuthServiceId(serviceId)).toEqual([blockType]) + /** + * Joining the deployment-gated set cannot hide these blocks: `isBlockAllowed` + * drops a gated type only at `unavailable`/`misconfigured`, and the `ready` + * asserted above holds for every deployment because none of these services + * carries a `deploymentRequirement`. + */ + expect(isDeploymentGatedIntegrationType(blockType)).toBe(true) + expect(isOAuthServiceAllowedByIntegrationTypes(serviceId, new Set([blockType]))).toBe(true) + expect(isOAuthServiceAllowedByIntegrationTypes(serviceId, new Set(['jira']))).toBe(false) + expect(SERVICE_ACCOUNT_METADATA_BY_OAUTH_SERVICE_ID[serviceId]?.providerId).toBe( + `${serviceId}-service-account` + ) + expect(CREDENTIAL_CONFIGURED_OAUTH_SERVICE_IDS).not.toContain(serviceId) + } + ) it('does not infer GitHub repository OAuth readiness from its API-key workflow block', () => { expect(availabilityFor('github_v2')).toMatchObject({ state: 'ready', oauthAvailable: false }) expect( @@ -190,24 +229,26 @@ describe('integration availability', () => { }) it('keeps service-account metadata in parity with canonical OAuth services', () => { - const oauthServiceIds = [ + const credentialServiceIds = [ ...new Set( - integrations.flatMap((integration) => - integration.authType === 'oauth' && integration.oauthServiceId - ? [integration.oauthServiceId] - : [] - ) + integrations.flatMap((integration) => { + const serviceId = integration.serviceAccountServiceId ?? integration.oauthServiceId + return serviceId ? [serviceId] : [] + }) ), ] const expectedServiceAccountIds: Record = {} const expectedCredentialConfiguredServiceIds: string[] = [] - for (const oauthServiceId of oauthServiceIds) { + for (const oauthServiceId of credentialServiceIds) { const canonical = getServiceConfigByServiceId(oauthServiceId) if (!canonical) throw new Error(`Missing canonical OAuth service ${oauthServiceId}`) const projected = SERVICE_ACCOUNT_METADATA_BY_OAUTH_SERVICE_ID[oauthServiceId] expect(projected?.providerId, oauthServiceId).toBe(canonical.serviceAccountProviderId) - if (canonical.clientConfiguration) { + if ( + canonical.clientConfiguration && + integrations.some((integration) => integration.oauthServiceId === oauthServiceId) + ) { expectedCredentialConfiguredServiceIds.push(oauthServiceId) } if (canonical.serviceAccountProviderId) { diff --git a/apps/sim/lib/integrations/credential-display.test.ts b/apps/sim/lib/integrations/credential-display.test.ts index b89865d2899..ac60759feab 100644 --- a/apps/sim/lib/integrations/credential-display.test.ts +++ b/apps/sim/lib/integrations/credential-display.test.ts @@ -5,7 +5,10 @@ import { import integrationsJson from '@sim/deployment-config/integrations.json' import { describe, expect, it } from 'vitest' import { getIntegrationsForCredentialProvider } from '@/lib/integrations/credential-display' -import { resolveOAuthServiceForIntegration } from '@/lib/integrations/oauth-service' +import { + resolveOAuthServiceForIntegration, + resolveServiceAccountServiceForIntegration, +} from '@/lib/integrations/oauth-service' import type { Integration } from '@/lib/integrations/types' import { OAUTH_PROVIDERS } from '@/lib/oauth/oauth' import { credentialProviderMatchesService } from '@/lib/oauth/utils' @@ -56,21 +59,17 @@ const EXPECTED_COVERAGE: Record = { 'google-tasks', 'google-vault', ], - 'harmonic-service-account': [], + 'harmonic-service-account': ['harmonic'], 'hubspot-service-account': ['hubspot'], 'linear-service-account': ['linear'], 'monday-service-account': ['monday'], 'notion-service-account': ['notion'], - // NetSuite remains an API-key catalog integration, like Snowflake, while its - // block uses the shared reusable-credential selector. - 'netsuite-service-account': [], + 'netsuite-service-account': ['oracle-netsuite'], 'pipedrive-service-account': ['pipedrive'], 'salesforce-service-account': ['salesforce'], 'shopify-service-account': ['shopify'], 'slack-custom-bot': ['slack'], - // Snowflake's catalog entry is api-key (there is no Snowflake OAuth client), - // so its credential is offered on the block rather than an integration page. - 'snowflake-service-account': [], + 'snowflake-service-account': ['snowflake'], 'trello-service-account': ['trello'], 'wealthbox-service-account': ['wealthbox'], 'webflow-service-account': ['webflow'], @@ -133,7 +132,9 @@ describe('service-account coverage', () => { const covered = new Set(getIntegrationsForCredentialProvider(providerId).map((i) => i.slug)) for (const integration of INTEGRATIONS) { - const service = resolveOAuthServiceForIntegration(integration) + const service = + resolveOAuthServiceForIntegration(integration) ?? + resolveServiceAccountServiceForIntegration(integration) if (!service) continue expect( credentialProviderMatchesService(providerId, service), diff --git a/apps/sim/lib/integrations/credential-visibility.server.test.ts b/apps/sim/lib/integrations/credential-visibility.server.test.ts index 566ce32c374..fc531fda911 100644 --- a/apps/sim/lib/integrations/credential-visibility.server.test.ts +++ b/apps/sim/lib/integrations/credential-visibility.server.test.ts @@ -68,6 +68,24 @@ describe('integration credential visibility', () => { }) it.each([ + { + serviceId: 'netsuite', + providerId: 'netsuite-service-account', + blockType: 'netsuite', + name: 'NetSuite', + }, + { + serviceId: 'snowflake', + providerId: 'snowflake-service-account', + blockType: 'snowflake', + name: 'Snowflake', + }, + { + serviceId: 'harmonic', + providerId: 'harmonic-service-account', + blockType: 'harmonic', + name: 'Harmonic', + }, { serviceId: 'coda', providerId: 'coda-service-account', diff --git a/apps/sim/lib/integrations/index.ts b/apps/sim/lib/integrations/index.ts index 463096463bc..3a5f47e20a5 100644 --- a/apps/sim/lib/integrations/index.ts +++ b/apps/sim/lib/integrations/index.ts @@ -68,6 +68,7 @@ export { type OAuthServiceMatch, resolveOAuthServiceForIntegration, resolveOAuthServiceForSlug, + resolveServiceAccountServiceForIntegration, } from '@/lib/integrations/oauth-service' export type { AuthType, FAQItem, Integration, IntegrationSummary } from '@/lib/integrations/types' export type { BlockMeta, BlockTemplate } from '@/blocks/types' diff --git a/apps/sim/lib/integrations/oauth-service.test.ts b/apps/sim/lib/integrations/oauth-service.test.ts index 0f7e18b5621..d088592bae7 100644 --- a/apps/sim/lib/integrations/oauth-service.test.ts +++ b/apps/sim/lib/integrations/oauth-service.test.ts @@ -3,6 +3,7 @@ import { describe, expect, it } from 'vitest' import { resolveOAuthServiceForSlug, resolveServiceAccountIntegration, + resolveServiceAccountServiceForIntegration, } from '@/lib/integrations/oauth-service' import type { Integration } from '@/lib/integrations/types' @@ -63,6 +64,60 @@ describe('resolveOAuthServiceForSlug', () => { }) describe('resolveServiceAccountIntegration', () => { + it.each(['netsuite', 'snowflake', 'harmonic', 'claude-platform'])( + 'resolves %s without offering OAuth', + (serviceId) => { + const integration = INTEGRATIONS.find((entry) => entry.serviceAccountServiceId === serviceId) + if (!integration) throw new Error(`Missing service-account integration for ${serviceId}`) + expect(integration.authType).toBe('api-key') + expect(resolveOAuthServiceForSlug(integration.slug)).toBeNull() + expect( + resolveServiceAccountServiceForIntegration(integration)?.serviceAccountProviderId + ).toBe(`${serviceId}-service-account`) + expect(resolveServiceAccountIntegration(serviceId)?.slug).toBe(integration.slug) + } + ) + + /** + * The match carries its own icon because the connect control cannot recover + * one for these four: `resolveOAuthServiceForSlug` answers `null` for a + * non-OAuth catalog entry, and a missing icon makes + * `useServiceAccountConnectTarget` return `null` — rendering nothing at all + * rather than a broken chip, which is why the gap was invisible. + */ + it('carries a service icon on every service-account match', () => { + const matches = INTEGRATIONS.map((entry) => ({ + slug: entry.slug, + match: resolveServiceAccountIntegration(entry.slug), + })).filter(({ match }) => match) + expect(matches.length).toBeGreaterThan(0) + for (const { slug, match } of matches) { + expect(typeof match?.serviceIcon, slug).toBe('function') + } + for (const serviceId of ['netsuite', 'snowflake', 'harmonic', 'claude-platform']) { + const integration = INTEGRATIONS.find((entry) => entry.serviceAccountServiceId === serviceId) + if (!integration) throw new Error(`Missing service-account integration for ${serviceId}`) + expect(resolveServiceAccountIntegration(integration.slug)?.serviceIcon, serviceId).toBe( + resolveServiceAccountServiceForIntegration(integration)?.serviceIcon + ) + } + }) + + it('only offers the OAuth fallback when the canonical service supports stored accounts', () => { + const jira = INTEGRATIONS.find((entry) => entry.slug === 'jira') + const x = INTEGRATIONS.find((entry) => entry.type === 'x') + if (!jira || !x) throw new Error('Missing OAuth integration fixtures') + expect(resolveServiceAccountServiceForIntegration(jira)?.serviceAccountProviderId).toBe( + 'atlassian-service-account' + ) + expect(resolveServiceAccountServiceForIntegration(x)).toBeNull() + expect( + resolveServiceAccountServiceForIntegration({ + ...x, + serviceAccountServiceId: 'unknown-service', + }) + ).toBeNull() + }) it.concurrent('keeps a named service instead of collapsing to the family default', () => { // Every Google integration issues the same google-service-account // credential, so a fuzzy matcher can silently answer Drive for all of diff --git a/apps/sim/lib/integrations/oauth-service.ts b/apps/sim/lib/integrations/oauth-service.ts index 36ac1922d7e..eff65d1472f 100644 --- a/apps/sim/lib/integrations/oauth-service.ts +++ b/apps/sim/lib/integrations/oauth-service.ts @@ -48,6 +48,27 @@ export function resolveOAuthServiceForIntegration( } } +interface ServiceAccountServiceMatch extends Omit { + serviceAccountProviderId: ServiceAccountProviderId +} + +/** Resolves a stored service-account connection independently of the OAuth catalog marker. */ +export function resolveServiceAccountServiceForIntegration( + integration: IntegrationMetadata +): ServiceAccountServiceMatch | null { + const serviceId = integration.serviceAccountServiceId ?? integration.oauthServiceId + if (!serviceId) return null + const service = getServiceConfigByServiceId(serviceId) + const serviceAccountProviderId = asServiceAccountProviderId(service?.serviceAccountProviderId) + if (!service || !serviceAccountProviderId) return null + return { + providerId: service.providerId, + serviceName: service.name, + serviceIcon: service.icon as ComponentType<{ className?: string }>, + serviceAccountProviderId, + } +} + /** * Resolves the integration entry for a catalog slug, then derives its OAuth * service match. Returns `null` when the slug is unknown or the matching @@ -67,6 +88,7 @@ export interface ServiceAccountIntegrationMatch { slug: string serviceAccountProviderId: ServiceAccountProviderId serviceName: string + serviceIcon: ComponentType<{ className?: string }> providerId: string } @@ -92,18 +114,19 @@ export const CANONICAL_SERVICE_ACCOUNT_SLUGS: Readonly> = /** * Every integration that offers a service-account flow, in catalog order. - * Built once — `resolveOAuthServiceForIntegration` walks `OAUTH_PROVIDERS` per + * Built once — `resolveServiceAccountServiceForIntegration` walks `OAUTH_PROVIDERS` per * entry, which is wasted work to repeat on each lookup. */ const SERVICE_ACCOUNT_INTEGRATIONS: readonly ServiceAccountIntegrationMatch[] = INTEGRATION_METADATA.flatMap((integration) => { - const match = resolveOAuthServiceForIntegration(integration) + const match = resolveServiceAccountServiceForIntegration(integration) if (!match?.serviceAccountProviderId) return [] return [ { slug: integration.slug, serviceAccountProviderId: match.serviceAccountProviderId, serviceName: integration.name, + serviceIcon: match.serviceIcon, providerId: match.providerId, }, ] diff --git a/apps/sim/vitest.setup.ts b/apps/sim/vitest.setup.ts index 47691953962..c747c54c273 100644 --- a/apps/sim/vitest.setup.ts +++ b/apps/sim/vitest.setup.ts @@ -131,6 +131,8 @@ vi.mock('@/blocks/registry', () => ({ outputs: {}, })), getAllBlocks: vi.fn(() => []), + getTemplatesForBlock: vi.fn(() => []), + getSuggestedSkillsForBlock: vi.fn(() => []), getLatestBlock: vi.fn(() => undefined), /** Detail-read accessor: version-resolved and projected through the viewer's visibility. */ getLatestBlockForViewer: vi.fn(() => undefined), diff --git a/packages/deployment-config/src/integration-availability.ts b/packages/deployment-config/src/integration-availability.ts index 8dcc11a6f4d..f79a5831dcb 100644 --- a/packages/deployment-config/src/integration-availability.ts +++ b/packages/deployment-config/src/integration-availability.ts @@ -22,9 +22,9 @@ export interface IntegrationAvailability { const credentialConfiguredOAuthServiceIds = new Set(CREDENTIAL_CONFIGURED_OAUTH_SERVICE_IDS) const deploymentGatedIntegrationTypes = new Set( - INTEGRATION_METADATA.filter((integration) => integration.authType === 'oauth').map( - (integration) => integration.type.toLowerCase() - ) + INTEGRATION_METADATA.filter( + (integration) => integration.authType === 'oauth' || integration.serviceAccountServiceId + ).map((integration) => integration.type.toLowerCase()) ) const integrationTypesByOAuthServiceId = new Map() /** Search authorization shares GitHub's integration policy while its workflow tools retain PAT auth. */ @@ -40,11 +40,14 @@ for (const [serviceId, integrationType] of tokenCredentialIntegrationTypes) { const tokenCredentialTypes = new Set(tokenCredentialIntegrationTypes.values()) const previewServiceAccountProvidersByIntegrationType = new Map() for (const integration of INTEGRATION_METADATA) { - if (integration.authType !== 'oauth' || !integration.oauthServiceId) continue - const serviceId = integration.oauthServiceId.toLowerCase() + const credentialServiceId = integration.serviceAccountServiceId ?? integration.oauthServiceId + if (!credentialServiceId) continue + const serviceId = credentialServiceId.toLowerCase() const current = integrationTypesByOAuthServiceId.get(serviceId) ?? [] const integrationType = integration.type.toLowerCase() - integrationTypesByOAuthServiceId.set(serviceId, [...current, integrationType]) + if (!current.includes(integrationType)) { + integrationTypesByOAuthServiceId.set(serviceId, [...current, integrationType]) + } const serviceAccount = getServiceAccountMetadata(serviceId) if (serviceAccount?.deploymentRequirement !== 'preview-gated') continue @@ -55,7 +58,7 @@ export function isDeploymentGatedIntegrationType(blockType: string): boolean { return deploymentGatedIntegrationTypes.has(blockType.toLowerCase()) } -/** Returns the generated integration block types authenticated by one OAuth service entry. */ +/** Returns block types using a canonical credential service, including stored service accounts. */ export function getIntegrationTypesForOAuthServiceId(serviceId: string): readonly string[] { return integrationTypesByOAuthServiceId.get(serviceId.toLowerCase()) ?? [] } @@ -147,6 +150,29 @@ export function resolveIntegrationAvailability( return resolveOAuthIntegrationAvailability(integration, values) } + if (integration.serviceAccountServiceId) { + const serviceAccount = getServiceAccountMetadata(integration.serviceAccountServiceId) + if (!serviceAccount) { + throw new Error(`Integration ${integration.slug} is missing service-account metadata`) + } + const capabilityId = resolveOAuthClientCapabilityId(integration.serviceAccountServiceId) + const serviceAccountAvailable = + serviceAccount.deploymentRequirement !== 'preview-gated' && + (serviceAccount.deploymentRequirement !== 'oauth-client' || + Boolean( + capabilityId && inspectOAuthClientCapability(capabilityId, values).state === 'ready' + )) + return { + type: integration.type, + slug: integration.slug, + name: integration.name, + state: serviceAccountAvailable ? 'ready' : 'unavailable', + oauthAvailable: false, + serviceAccountAvailable, + missingFields: [], + } + } + return { type: integration.type, slug: integration.slug, diff --git a/packages/deployment-config/src/integration-metadata.ts b/packages/deployment-config/src/integration-metadata.ts index caf3e718b69..fde6652b9e0 100644 --- a/packages/deployment-config/src/integration-metadata.ts +++ b/packages/deployment-config/src/integration-metadata.ts @@ -8,10 +8,11 @@ export interface IntegrationMetadata { name: string authType: 'oauth' | 'api-key' | 'none' oauthServiceId?: string + serviceAccountServiceId?: string bgColor: string integrationType: string } export const INTEGRATION_METADATA: readonly IntegrationMetadata[] = JSON.parse( - '[{"type":"onepassword","slug":"1password","name":"1Password","authType":"api-key","bgColor":"#FFFFFF","integrationType":"security"},{"type":"affinity","slug":"affinity","name":"Affinity","authType":"api-key","bgColor":"#1B1C1E","integrationType":"sales"},{"type":"agentmail","slug":"agentmail","name":"AgentMail","authType":"api-key","bgColor":"#000000","integrationType":"email"},{"type":"agentphone","slug":"agentphone","name":"AgentPhone","authType":"api-key","bgColor":"linear-gradient(135deg, #1a1a1a 0%, #0a2a14 100%)","integrationType":"communication"},{"type":"agiloft","slug":"agiloft","name":"Agiloft","authType":"api-key","bgColor":"#001028","integrationType":"productivity"},{"type":"ahrefs","slug":"ahrefs","name":"Ahrefs","authType":"api-key","bgColor":"#FFFFFF","integrationType":"analytics"},{"type":"airtable","slug":"airtable","name":"Airtable","authType":"oauth","oauthServiceId":"airtable","bgColor":"#FFFFFF","integrationType":"databases"},{"type":"airweave","slug":"airweave","name":"Airweave","authType":"api-key","bgColor":"#6366F1","integrationType":"search"},{"type":"algolia","slug":"algolia","name":"Algolia","authType":"api-key","bgColor":"#003DFF","integrationType":"search"},{"type":"dynamodb","slug":"amazon-dynamodb","name":"Amazon DynamoDB","authType":"api-key","bgColor":"linear-gradient(45deg, #2E27AD 0%, #527FFF 100%)","integrationType":"databases"},{"type":"rds","slug":"amazon-rds","name":"Amazon RDS","authType":"none","bgColor":"linear-gradient(45deg, #2E27AD 0%, #527FFF 100%)","integrationType":"databases"},{"type":"sqs","slug":"amazon-sqs","name":"Amazon SQS","authType":"api-key","bgColor":"linear-gradient(45deg, #2E27AD 0%, #527FFF 100%)","integrationType":"devops"},{"type":"amplitude","slug":"amplitude","name":"Amplitude","authType":"api-key","bgColor":"#13294B","integrationType":"analytics"},{"type":"apify","slug":"apify","name":"Apify","authType":"api-key","bgColor":"#FFFFFF","integrationType":"search"},{"type":"apollo","slug":"apollo","name":"Apollo","authType":"api-key","bgColor":"#EBF212","integrationType":"sales"},{"type":"arxiv","slug":"arxiv","name":"ArXiv","authType":"none","bgColor":"#FFFFFF","integrationType":"search"},{"type":"asana","slug":"asana","name":"Asana","authType":"oauth","oauthServiceId":"asana","bgColor":"#FFFFFF","integrationType":"productivity"},{"type":"ashby","slug":"ashby","name":"Ashby","authType":"api-key","bgColor":"#5D4ED6","integrationType":"hr"},{"type":"athena","slug":"athena","name":"Athena","authType":"none","bgColor":"linear-gradient(45deg, #4D27A8 0%, #A166FF 100%)","integrationType":"analytics"},{"type":"attio","slug":"attio","name":"Attio","authType":"oauth","oauthServiceId":"attio","bgColor":"#1D1E20","integrationType":"sales"},{"type":"appconfig","slug":"aws-appconfig","name":"AWS AppConfig","authType":"api-key","bgColor":"linear-gradient(45deg, #B0084D 0%, #FF4F8B 100%)","integrationType":"devops"},{"type":"iam","slug":"aws-iam","name":"AWS IAM","authType":"api-key","bgColor":"linear-gradient(45deg, #BD0816 0%, #FF5252 100%)","integrationType":"security"},{"type":"identity_center","slug":"aws-identity-center","name":"AWS Identity Center","authType":"api-key","bgColor":"linear-gradient(45deg, #BD0816 0%, #FF5252 100%)","integrationType":"security"},{"type":"secrets_manager","slug":"aws-secrets-manager","name":"AWS Secrets Manager","authType":"none","bgColor":"linear-gradient(45deg, #BD0816 0%, #FF5252 100%)","integrationType":"security"},{"type":"ses","slug":"aws-ses","name":"AWS SES","authType":"api-key","bgColor":"linear-gradient(45deg, #BD0816 0%, #FF5252 100%)","integrationType":"email"},{"type":"sts","slug":"aws-sts","name":"AWS STS","authType":"api-key","bgColor":"linear-gradient(45deg, #BD0816 0%, #FF5252 100%)","integrationType":"security"},{"type":"ssm","slug":"aws-systems-manager","name":"AWS Systems Manager","authType":"api-key","bgColor":"#E7157B","integrationType":"devops"},{"type":"textract_v2","slug":"aws-textract","name":"AWS Textract","authType":"api-key","bgColor":"linear-gradient(135deg, #055F4E 0%, #56C0A7 100%)","integrationType":"ai"},{"type":"microsoft_ad","slug":"azure-ad","name":"Azure AD","authType":"oauth","oauthServiceId":"microsoft-ad","bgColor":"#0078D4","integrationType":"security"},{"type":"azure_data_explorer","slug":"azure-data-explorer","name":"Azure Data Explorer","authType":"api-key","bgColor":"#FFFFFF","integrationType":"databases"},{"type":"azure_devops","slug":"azure-devops","name":"Azure DevOps","authType":"api-key","bgColor":"#0078D4","integrationType":"devops"},{"type":"bitbucket","slug":"bitbucket","name":"Bitbucket","authType":"oauth","oauthServiceId":"bitbucket","bgColor":"#FFFFFF","integrationType":"devops"},{"type":"box_v2","slug":"box","name":"Box","authType":"oauth","oauthServiceId":"box","bgColor":"#FFFFFF","integrationType":"documents"},{"type":"brandfetch","slug":"brandfetch","name":"Brandfetch","authType":"api-key","bgColor":"#000000","integrationType":"sales"},{"type":"brex","slug":"brex","name":"Brex","authType":"api-key","bgColor":"#171717","integrationType":"commerce"},{"type":"brightdata","slug":"bright-data","name":"Bright Data","authType":"api-key","bgColor":"#FFFFFF","integrationType":"search"},{"type":"browser_use","slug":"browser-use","name":"Browser Use","authType":"api-key","bgColor":"#181C1E","integrationType":"ai"},{"type":"buffer","slug":"buffer","name":"Buffer","authType":"api-key","bgColor":"#FFFFFF","integrationType":"marketing"},{"type":"calcom","slug":"cal-com","name":"Cal.com","authType":"oauth","oauthServiceId":"calcom","bgColor":"#292929","integrationType":"productivity"},{"type":"calendly","slug":"calendly","name":"Calendly","authType":"api-key","bgColor":"#FFFFFF","integrationType":"productivity"},{"type":"cbinsights","slug":"cb-insights","name":"CB Insights","authType":"api-key","bgColor":"#FFFFFF","integrationType":"sales"},{"type":"circleback","slug":"circleback","name":"Circleback","authType":"api-key","bgColor":"linear-gradient(180deg, #E0F7FA 0%, #FFFFFF 100%)","integrationType":"ai"},{"type":"managed_agent","slug":"claude-managed-agents","name":"Claude Managed Agents","authType":"api-key","bgColor":"#DA7756","integrationType":"ai"},{"type":"clay","slug":"clay","name":"Clay","authType":"api-key","bgColor":"#FFFFFF","integrationType":"sales"},{"type":"clerk","slug":"clerk","name":"Clerk","authType":"none","bgColor":"#131316","integrationType":"security"},{"type":"clickhouse","slug":"clickhouse","name":"ClickHouse","authType":"none","bgColor":"#f9ff69","integrationType":"databases"},{"type":"clickup","slug":"clickup","name":"ClickUp","authType":"oauth","oauthServiceId":"clickup","bgColor":"#FFFFFF","integrationType":"productivity"},{"type":"cloudflare","slug":"cloudflare","name":"Cloudflare","authType":"api-key","bgColor":"#F5F6FA","integrationType":"devops"},{"type":"cloudformation","slug":"cloudformation","name":"CloudFormation","authType":"none","bgColor":"linear-gradient(45deg, #B0084D 0%, #FF4F8B 100%)","integrationType":"devops"},{"type":"cloudtrail","slug":"cloudtrail","name":"CloudTrail","authType":"api-key","bgColor":"linear-gradient(45deg, #B0084D 0%, #FF4F8B 100%)","integrationType":"security"},{"type":"cloudwatch","slug":"cloudwatch","name":"CloudWatch","authType":"none","bgColor":"linear-gradient(45deg, #B0084D 0%, #FF4F8B 100%)","integrationType":"observability"},{"type":"coda","slug":"coda","name":"Coda","authType":"api-key","bgColor":"#FFFFFF","integrationType":"documents"},{"type":"codepipeline","slug":"codepipeline","name":"CodePipeline","authType":"api-key","bgColor":"linear-gradient(45deg, #2E27AD 0%, #527FFF 100%)","integrationType":"devops"},{"type":"confluence_v2","slug":"confluence","name":"Confluence","authType":"oauth","oauthServiceId":"confluence","bgColor":"#FFFFFF","integrationType":"documents"},{"type":"context_dev","slug":"context-dev","name":"Context.dev","authType":"api-key","bgColor":"#ffffff","integrationType":"search"},{"type":"convex","slug":"convex","name":"Convex","authType":"api-key","bgColor":"#FFFFFF","integrationType":"databases"},{"type":"crowdstrike","slug":"crowdstrike","name":"CrowdStrike","authType":"api-key","bgColor":"#E01F3D","integrationType":"security"},{"type":"crunchbase","slug":"crunchbase","name":"Crunchbase","authType":"api-key","bgColor":"#0287D1","integrationType":"sales"},{"type":"cursor_v2","slug":"cursor","name":"Cursor","authType":"api-key","bgColor":"#1E1E1E","integrationType":"devops"},{"type":"dagster","slug":"dagster","name":"Dagster","authType":"api-key","bgColor":"#ffffff","integrationType":"observability"},{"type":"databricks","slug":"databricks","name":"Databricks","authType":"api-key","bgColor":"#F9F7F4","integrationType":"databases"},{"type":"datadog","slug":"datadog","name":"Datadog","authType":"api-key","bgColor":"#632CA6","integrationType":"observability"},{"type":"datagma","slug":"datagma","name":"Datagma","authType":"api-key","bgColor":"#FFFFFF","integrationType":"sales"},{"type":"daytona","slug":"daytona","name":"Daytona","authType":"api-key","bgColor":"#FFFFFF","integrationType":"devops"},{"type":"devin","slug":"devin","name":"Devin","authType":"api-key","bgColor":"#12141A","integrationType":"devops"},{"type":"discord","slug":"discord","name":"Discord","authType":"api-key","bgColor":"#5865F2","integrationType":"communication"},{"type":"docusign","slug":"docusign","name":"DocuSign","authType":"oauth","oauthServiceId":"docusign","bgColor":"#FFFFFF","integrationType":"documents"},{"type":"downdetector","slug":"downdetector","name":"Downdetector","authType":"api-key","bgColor":"#FFFFFF","integrationType":"observability"},{"type":"dropbox_v2","slug":"dropbox","name":"Dropbox","authType":"oauth","oauthServiceId":"dropbox","bgColor":"#0061FF","integrationType":"documents"},{"type":"dropcontact","slug":"dropcontact","name":"Dropcontact","authType":"api-key","bgColor":"#0ABA9F","integrationType":"sales"},{"type":"dspy","slug":"dspy","name":"DSPy","authType":"api-key","bgColor":"#FFFFFF","integrationType":"ai"},{"type":"dub_v2","slug":"dub","name":"Dub","authType":"api-key","bgColor":"#181C1E","integrationType":"devops"},{"type":"duckduckgo","slug":"duckduckgo","name":"DuckDuckGo","authType":"none","bgColor":"#FFFFFF","integrationType":"search"},{"type":"dynatrace","slug":"dynatrace","name":"Dynatrace","authType":"api-key","bgColor":"#FFFFFF","integrationType":"observability"},{"type":"elasticsearch","slug":"elasticsearch","name":"Elasticsearch","authType":"api-key","bgColor":"#FFFFFF","integrationType":"databases"},{"type":"elevenlabs","slug":"elevenlabs","name":"ElevenLabs","authType":"api-key","bgColor":"#181C1E","integrationType":"ai"},{"type":"emailbison","slug":"email-bison","name":"Email Bison","authType":"api-key","bgColor":"#FB7A22","integrationType":"email"},{"type":"embeddings","slug":"embeddings","name":"Embeddings","authType":"api-key","bgColor":"#7B4DFF","integrationType":"ai"},{"type":"enrich","slug":"enrich","name":"Enrich","authType":"api-key","bgColor":"#E5E5E6","integrationType":"sales"},{"type":"enrow","slug":"enrow","name":"Enrow","authType":"api-key","bgColor":"#FFFFFF","integrationType":"sales"},{"type":"exa","slug":"exa","name":"Exa","authType":"api-key","bgColor":"#1F40ED","integrationType":"search"},{"type":"extend_v2","slug":"extend","name":"Extend","authType":"api-key","bgColor":"#000000","integrationType":"ai"},{"type":"fathom","slug":"fathom","name":"Fathom","authType":"api-key","bgColor":"#181C1E","integrationType":"analytics"},{"type":"findymail","slug":"findymail","name":"Findymail","authType":"api-key","bgColor":"#FFFFFF","integrationType":"sales"},{"type":"firecrawl","slug":"firecrawl","name":"Firecrawl","authType":"api-key","bgColor":"#181C1E","integrationType":"search"},{"type":"fireflies_v2","slug":"fireflies","name":"Fireflies","authType":"api-key","bgColor":"#100730","integrationType":"productivity"},{"type":"flint","slug":"flint","name":"Flint","authType":"api-key","bgColor":"#F6F54F","integrationType":"marketing"},{"type":"gamma","slug":"gamma","name":"Gamma","authType":"api-key","bgColor":"#002253","integrationType":"marketing"},{"type":"github_v2","slug":"github","name":"GitHub","authType":"api-key","bgColor":"#181C1E","integrationType":"devops"},{"type":"gitlab","slug":"gitlab","name":"GitLab","authType":"api-key","bgColor":"#FFFFFF","integrationType":"devops"},{"type":"gmail_v2","slug":"gmail","name":"Gmail","authType":"oauth","oauthServiceId":"gmail","bgColor":"#FFFFFF","integrationType":"email"},{"type":"gong","slug":"gong","name":"Gong","authType":"api-key","bgColor":"#8039DF","integrationType":"sales"},{"type":"google_ads","slug":"google-ads","name":"Google Ads","authType":"oauth","oauthServiceId":"google-ads","bgColor":"#FFFFFF","integrationType":"analytics"},{"type":"google_appsheet","slug":"google-appsheet","name":"Google AppSheet","authType":"api-key","bgColor":"#FFFFFF","integrationType":"databases"},{"type":"google_bigquery","slug":"google-bigquery","name":"Google BigQuery","authType":"oauth","oauthServiceId":"google-bigquery","bgColor":"#FFFFFF","integrationType":"databases"},{"type":"google_books","slug":"google-books","name":"Google Books","authType":"api-key","bgColor":"#FFFFFF","integrationType":"search"},{"type":"google_calendar_v2","slug":"google-calendar","name":"Google Calendar","authType":"oauth","oauthServiceId":"google-calendar","bgColor":"#FFFFFF","integrationType":"productivity"},{"type":"google_contacts","slug":"google-contacts","name":"Google Contacts","authType":"oauth","oauthServiceId":"google-contacts","bgColor":"#FFFFFF","integrationType":"productivity"},{"type":"google_docs","slug":"google-docs","name":"Google Docs","authType":"oauth","oauthServiceId":"google-docs","bgColor":"#FFFFFF","integrationType":"documents"},{"type":"google_drive","slug":"google-drive","name":"Google Drive","authType":"oauth","oauthServiceId":"google-drive","bgColor":"#FFFFFF","integrationType":"documents"},{"type":"google_forms","slug":"google-forms","name":"Google Forms","authType":"oauth","oauthServiceId":"google-forms","bgColor":"#FFFFFF","integrationType":"documents"},{"type":"google_groups","slug":"google-groups","name":"Google Groups","authType":"oauth","oauthServiceId":"google-groups","bgColor":"#E8F0FE","integrationType":"communication"},{"type":"google_maps","slug":"google-maps","name":"Google Maps","authType":"api-key","bgColor":"#FFFFFF","integrationType":"search"},{"type":"google_meet","slug":"google-meet","name":"Google Meet","authType":"oauth","oauthServiceId":"google-meet","bgColor":"#FFFFFF","integrationType":"communication"},{"type":"google_pagespeed","slug":"google-pagespeed","name":"Google PageSpeed","authType":"api-key","bgColor":"#FFFFFF","integrationType":"analytics"},{"type":"google_search","slug":"google-search","name":"Google Search","authType":"api-key","bgColor":"#FFFFFF","integrationType":"search"},{"type":"google_sheets_v2","slug":"google-sheets","name":"Google Sheets","authType":"oauth","oauthServiceId":"google-sheets","bgColor":"#FFFFFF","integrationType":"documents"},{"type":"google_slides_v2","slug":"google-slides","name":"Google Slides","authType":"oauth","oauthServiceId":"google-drive","bgColor":"#FFFFFF","integrationType":"documents"},{"type":"google_tasks","slug":"google-tasks","name":"Google Tasks","authType":"oauth","oauthServiceId":"google-tasks","bgColor":"#FFFFFF","integrationType":"productivity"},{"type":"google_translate","slug":"google-translate","name":"Google Translate","authType":"api-key","bgColor":"#FFFFFF","integrationType":"ai"},{"type":"google_vault","slug":"google-vault","name":"Google Vault","authType":"oauth","oauthServiceId":"google-vault","bgColor":"#E8F0FE","integrationType":"security"},{"type":"grafana","slug":"grafana","name":"Grafana","authType":"api-key","bgColor":"#FFFFFF","integrationType":"observability"},{"type":"grain_v2","slug":"grain","name":"Grain","authType":"api-key","bgColor":"#F6FAF9","integrationType":"productivity"},{"type":"granola","slug":"granola","name":"Granola","authType":"api-key","bgColor":"#B2C147","integrationType":"productivity"},{"type":"greenhouse","slug":"greenhouse","name":"Greenhouse","authType":"api-key","bgColor":"#469776","integrationType":"hr"},{"type":"greptile","slug":"greptile","name":"Greptile","authType":"api-key","bgColor":"#181C1E","integrationType":"devops"},{"type":"harmonic","slug":"harmonic","name":"Harmonic","authType":"api-key","bgColor":"#FFFFFF","integrationType":"sales"},{"type":"hex","slug":"hex","name":"Hex","authType":"api-key","bgColor":"#14151A","integrationType":"analytics"},{"type":"hubspot","slug":"hubspot","name":"HubSpot","authType":"oauth","oauthServiceId":"hubspot","bgColor":"#FF7A59","integrationType":"sales"},{"type":"huggingface","slug":"hugging-face","name":"Hugging Face","authType":"api-key","bgColor":"#0B0F19","integrationType":"ai"},{"type":"hunter","slug":"hunter-io","name":"Hunter.io","authType":"api-key","bgColor":"#FFFFFF","integrationType":"sales"},{"type":"icypeas","slug":"icypeas","name":"Icypeas","authType":"api-key","bgColor":"#d4d4d4","integrationType":"sales"},{"type":"incidentio","slug":"incident-io","name":"incident.io","authType":"api-key","bgColor":"#FFFFFF","integrationType":"observability"},{"type":"infisical","slug":"infisical","name":"Infisical","authType":"api-key","bgColor":"#F7FE62","integrationType":"security"},{"type":"instantly","slug":"instantly","name":"Instantly","authType":"api-key","bgColor":"#FFFFFF","integrationType":"email"},{"type":"intercom_v2","slug":"intercom","name":"Intercom","authType":"api-key","bgColor":"#FFFFFF","integrationType":"support"},{"type":"jina","slug":"jina","name":"Jina","authType":"api-key","bgColor":"#333333","integrationType":"search"},{"type":"jira","slug":"jira","name":"Jira","authType":"oauth","oauthServiceId":"jira","bgColor":"#FFFFFF","integrationType":"productivity"},{"type":"jira_service_management","slug":"jira-service-management","name":"Jira Service Management","authType":"oauth","oauthServiceId":"jira","bgColor":"#FFFFFF","integrationType":"support"},{"type":"jotform","slug":"jotform","name":"Jotform","authType":"api-key","bgColor":"#FFFFFF","integrationType":"productivity"},{"type":"jupyter_v2","slug":"jupyter","name":"Jupyter","authType":"api-key","bgColor":"#FFFFFF","integrationType":"devops"},{"type":"kalshi_v2","slug":"kalshi","name":"Kalshi","authType":"api-key","bgColor":"#09C285","integrationType":"analytics"},{"type":"ketch","slug":"ketch","name":"Ketch","authType":"none","bgColor":"#9B5CFF","integrationType":"security"},{"type":"lambda","slug":"lambda","name":"Lambda","authType":"api-key","bgColor":"linear-gradient(45deg, #C8511B 0%, #FF9900 100%)","integrationType":"devops"},{"type":"langsmith","slug":"langsmith","name":"LangSmith","authType":"api-key","bgColor":"#181C1E","integrationType":"observability"},{"type":"latex","slug":"latex","name":"LaTeX","authType":"none","bgColor":"#FFFFFF","integrationType":"documents"},{"type":"launchdarkly","slug":"launchdarkly","name":"LaunchDarkly","authType":"api-key","bgColor":"#191919","integrationType":"devops"},{"type":"leadmagic","slug":"leadmagic","name":"LeadMagic","authType":"api-key","bgColor":"#FFFFFF","integrationType":"sales"},{"type":"lemlist","slug":"lemlist","name":"Lemlist","authType":"api-key","bgColor":"#316BFF","integrationType":"email"},{"type":"linear_v2","slug":"linear","name":"Linear","authType":"oauth","oauthServiceId":"linear","bgColor":"#5E6AD2","integrationType":"productivity"},{"type":"linkedin","slug":"linkedin","name":"LinkedIn","authType":"oauth","oauthServiceId":"linkedin","bgColor":"#0072B1","integrationType":"sales"},{"type":"linkup","slug":"linkup","name":"Linkup","authType":"api-key","bgColor":"#D6D3C7","integrationType":"search"},{"type":"linq","slug":"linq","name":"Linq","authType":"api-key","bgColor":"#000000","integrationType":"communication"},{"type":"logfire","slug":"logfire","name":"Logfire","authType":"api-key","bgColor":"#000000","integrationType":"observability"},{"type":"logrocket","slug":"logrocket","name":"LogRocket","authType":"api-key","bgColor":"#764ABC","integrationType":"observability"},{"type":"loops","slug":"loops","name":"Loops","authType":"api-key","bgColor":"#FAFAF9","integrationType":"email"},{"type":"luma","slug":"luma","name":"Luma","authType":"api-key","bgColor":"#000000","integrationType":"productivity"},{"type":"mailchimp","slug":"mailchimp","name":"Mailchimp","authType":"api-key","bgColor":"#FFE01B","integrationType":"email"},{"type":"mailgun","slug":"mailgun","name":"Mailgun","authType":"api-key","bgColor":"#C12126","integrationType":"email"},{"type":"manageengine_sdp","slug":"manageengine-servicedesk-plus","name":"ManageEngine ServiceDesk Plus","authType":"oauth","oauthServiceId":"manageengine-sdp","bgColor":"#FFFFFF","integrationType":"support"},{"type":"mem0","slug":"mem0","name":"Mem0","authType":"api-key","bgColor":"#181C1E","integrationType":"ai"},{"type":"microsoft_dataverse_v2","slug":"microsoft-dataverse","name":"Microsoft Dataverse","authType":"oauth","oauthServiceId":"microsoft-dataverse","bgColor":"#FFFFFF","integrationType":"databases"},{"type":"microsoft_dynamics_365","slug":"microsoft-dynamics-365-crm","name":"Microsoft Dynamics 365 CRM","authType":"oauth","oauthServiceId":"microsoft-dataverse","bgColor":"#FFFFFF","integrationType":"sales"},{"type":"microsoft_excel_v2","slug":"microsoft-excel","name":"Microsoft Excel","authType":"oauth","oauthServiceId":"microsoft-excel","bgColor":"#FFFFFF","integrationType":"documents"},{"type":"microsoft_planner","slug":"microsoft-planner","name":"Microsoft Planner","authType":"oauth","oauthServiceId":"microsoft-planner","bgColor":"#FFFFFF","integrationType":"productivity"},{"type":"mssql","slug":"microsoft-sql-server","name":"Microsoft SQL Server","authType":"none","bgColor":"#FFFFFF","integrationType":"databases"},{"type":"microsoft_teams","slug":"microsoft-teams","name":"Microsoft Teams","authType":"oauth","oauthServiceId":"microsoft-teams","bgColor":"#FFFFFF","integrationType":"communication"},{"type":"microsoft_word","slug":"microsoft-word","name":"Microsoft Word","authType":"oauth","oauthServiceId":"microsoft-word","bgColor":"#FFFFFF","integrationType":"documents"},{"type":"millionverifier","slug":"millionverifier","name":"MillionVerifier","authType":"api-key","bgColor":"#FFFFFF","integrationType":"sales"},{"type":"mintlify","slug":"mintlify","name":"Mintlify","authType":"api-key","bgColor":"#000000","integrationType":"documents"},{"type":"mistral_parse_v3","slug":"mistral-parser","name":"Mistral Parser","authType":"api-key","bgColor":"#000000","integrationType":"ai"},{"type":"modal","slug":"modal","name":"Modal","authType":"api-key","bgColor":"#000000","integrationType":"ai"},{"type":"monday","slug":"monday","name":"Monday","authType":"oauth","oauthServiceId":"monday","bgColor":"#FFFFFF","integrationType":"productivity"},{"type":"mongodb","slug":"mongodb","name":"MongoDB","authType":"none","bgColor":"#FFFFFF","integrationType":"databases"},{"type":"mysql","slug":"mysql","name":"MySQL","authType":"none","bgColor":"#FFFFFF","integrationType":"databases"},{"type":"neo4j","slug":"neo4j","name":"Neo4j","authType":"none","bgColor":"#FFFFFF","integrationType":"databases"},{"type":"neverbounce","slug":"neverbounce","name":"NeverBounce","authType":"api-key","bgColor":"#064AF4","integrationType":"sales"},{"type":"new_relic","slug":"new-relic","name":"New Relic","authType":"api-key","bgColor":"#000000","integrationType":"observability"},{"type":"notion_v2","slug":"notion","name":"Notion","authType":"oauth","oauthServiceId":"notion","bgColor":"#FFFFFF","integrationType":"documents"},{"type":"obsidian","slug":"obsidian","name":"Obsidian","authType":"api-key","bgColor":"#0F0F0F","integrationType":"documents"},{"type":"okta","slug":"okta","name":"Okta","authType":"api-key","bgColor":"#191919","integrationType":"security"},{"type":"onedrive","slug":"onedrive","name":"OneDrive","authType":"oauth","oauthServiceId":"onedrive","bgColor":"#FFFFFF","integrationType":"documents"},{"type":"netsuite","slug":"oracle-netsuite","name":"Oracle NetSuite","authType":"api-key","bgColor":"#FFFFFF","integrationType":"commerce"},{"type":"otter","slug":"otter-ai","name":"Otter.ai","authType":"api-key","bgColor":"#FFFFFF","integrationType":"productivity"},{"type":"outlook","slug":"outlook","name":"Outlook","authType":"oauth","oauthServiceId":"outlook","bgColor":"#FFFFFF","integrationType":"email"},{"type":"pagerduty","slug":"pagerduty","name":"PagerDuty","authType":"api-key","bgColor":"#06AC38","integrationType":"observability"},{"type":"parallel_ai","slug":"parallel-ai","name":"Parallel AI","authType":"api-key","bgColor":"#1D1C1A","integrationType":"search"},{"type":"peopledatalabs","slug":"people-data-labs","name":"People Data Labs","authType":"api-key","bgColor":"#4831C3","integrationType":"sales"},{"type":"perplexity","slug":"perplexity","name":"Perplexity","authType":"api-key","bgColor":"#20808D","integrationType":"ai"},{"type":"persona","slug":"persona","name":"Persona","authType":"api-key","bgColor":"#FFFFFF","integrationType":"security"},{"type":"pinecone","slug":"pinecone","name":"Pinecone","authType":"api-key","bgColor":"#0D1117","integrationType":"databases"},{"type":"pipedrive","slug":"pipedrive","name":"Pipedrive","authType":"oauth","oauthServiceId":"pipedrive","bgColor":"#2E6936","integrationType":"sales"},{"type":"pitchbook","slug":"pitchbook","name":"PitchBook","authType":"api-key","bgColor":"#1D5080","integrationType":"analytics"},{"type":"planetscale","slug":"planetscale","name":"PlanetScale","authType":"api-key","bgColor":"#111111","integrationType":"databases"},{"type":"polymarket","slug":"polymarket","name":"Polymarket","authType":"none","bgColor":"#4C82FB","integrationType":"analytics"},{"type":"postgresql","slug":"postgresql","name":"PostgreSQL","authType":"none","bgColor":"#336791","integrationType":"databases"},{"type":"posthog","slug":"posthog","name":"PostHog","authType":"api-key","bgColor":"#FFFFFF","integrationType":"analytics"},{"type":"powerbi","slug":"power-bi","name":"Power BI","authType":"oauth","oauthServiceId":"microsoft-powerbi","bgColor":"#FFFFFF","integrationType":"analytics"},{"type":"profound","slug":"profound","name":"Profound","authType":"api-key","bgColor":"#000000","integrationType":"analytics"},{"type":"prospeo","slug":"prospeo","name":"Prospeo","authType":"api-key","bgColor":"#FF1A26","integrationType":"sales"},{"type":"pulse_v2","slug":"pulse","name":"Pulse","authType":"api-key","bgColor":"#FFFFFF","integrationType":"ai"},{"type":"qdrant","slug":"qdrant","name":"Qdrant","authType":"api-key","bgColor":"#1A223F","integrationType":"databases"},{"type":"quartr","slug":"quartr","name":"Quartr","authType":"api-key","bgColor":"#000000","integrationType":"analytics"},{"type":"quickbooks","slug":"quickbooks","name":"QuickBooks","authType":"oauth","oauthServiceId":"quickbooks","bgColor":"#2CA01C","integrationType":"commerce"},{"type":"quiver_v2","slug":"quiver","name":"Quiver","authType":"api-key","bgColor":"#FFFFFF","integrationType":"ai"},{"type":"rabbitmq","slug":"rabbitmq","name":"RabbitMQ","authType":"api-key","bgColor":"#FFFFFF","integrationType":"devops"},{"type":"railway","slug":"railway","name":"Railway","authType":"api-key","bgColor":"#000000","integrationType":"devops"},{"type":"rb2b","slug":"rb2b","name":"RB2B","authType":"api-key","bgColor":"#51FF00","integrationType":"sales"},{"type":"reddit","slug":"reddit","name":"Reddit","authType":"oauth","oauthServiceId":"reddit","bgColor":"#FF5700","integrationType":"communication"},{"type":"redis","slug":"redis","name":"Redis","authType":"api-key","bgColor":"#FF4438","integrationType":"databases"},{"type":"reducto_v2","slug":"reducto","name":"Reducto","authType":"api-key","bgColor":"#5c0c5c","integrationType":"ai"},{"type":"resend","slug":"resend","name":"Resend","authType":"api-key","bgColor":"#181C1E","integrationType":"email"},{"type":"revenuecat","slug":"revenuecat","name":"RevenueCat","authType":"api-key","bgColor":"#F25A5A","integrationType":"commerce"},{"type":"rippling","slug":"rippling","name":"Rippling","authType":"api-key","bgColor":"#502D3C","integrationType":"hr"},{"type":"rocketlane","slug":"rocketlane","name":"Rocketlane","authType":"api-key","bgColor":"#000000","integrationType":"productivity"},{"type":"rootly","slug":"rootly","name":"Rootly","authType":"api-key","bgColor":"#6C72C8","integrationType":"observability"},{"type":"s3","slug":"s3","name":"S3","authType":"api-key","bgColor":"linear-gradient(45deg, #1B660F 0%, #6CAE3E 100%)","integrationType":"documents"},{"type":"sailpoint","slug":"sailpoint","name":"SailPoint","authType":"api-key","bgColor":"#FFFFFF","integrationType":"security"},{"type":"salesforce","slug":"salesforce","name":"Salesforce","authType":"oauth","oauthServiceId":"salesforce","bgColor":"#FFFFFF","integrationType":"sales"},{"type":"sap_concur","slug":"sap-concur","name":"SAP Concur","authType":"api-key","bgColor":"#FFFFFF","integrationType":"productivity"},{"type":"sap_s4hana","slug":"sap-s4hana","name":"SAP S4HANA","authType":"api-key","bgColor":"#FFFFFF","integrationType":"hr"},{"type":"semrush","slug":"semrush","name":"Semrush","authType":"api-key","bgColor":"#FFFFFF","integrationType":"analytics"},{"type":"sendblue","slug":"sendblue","name":"Sendblue","authType":"api-key","bgColor":"#008BFF","integrationType":"communication"},{"type":"sendgrid","slug":"sendgrid","name":"SendGrid","authType":"api-key","bgColor":"#1A82E2","integrationType":"email"},{"type":"sentry","slug":"sentry","name":"Sentry","authType":"api-key","bgColor":"#362D59","integrationType":"observability"},{"type":"serper","slug":"serper","name":"Serper","authType":"api-key","bgColor":"#2B3543","integrationType":"search"},{"type":"servicenow_v2","slug":"servicenow","name":"ServiceNow","authType":"api-key","bgColor":"#032D42","integrationType":"support"},{"type":"sftp_v2","slug":"sftp","name":"SFTP","authType":"api-key","bgColor":"#2D3748","integrationType":"documents"},{"type":"sharepoint_v2","slug":"sharepoint","name":"SharePoint","authType":"oauth","oauthServiceId":"sharepoint","bgColor":"#FFFFFF","integrationType":"documents"},{"type":"shopify","slug":"shopify","name":"Shopify","authType":"oauth","oauthServiceId":"shopify","bgColor":"#FFFFFF","integrationType":"commerce"},{"type":"similarweb","slug":"similarweb","name":"Similarweb","authType":"api-key","bgColor":"#000922","integrationType":"analytics"},{"type":"sixtyfour","slug":"sixtyfour-ai","name":"Sixtyfour AI","authType":"api-key","bgColor":"#000000","integrationType":"sales"},{"type":"slack_v2","slug":"slack","name":"Slack","authType":"oauth","oauthServiceId":"slack","bgColor":"#611f69","integrationType":"communication"},{"type":"smartlead","slug":"smartlead","name":"Smartlead","authType":"api-key","bgColor":"#000000","integrationType":"email"},{"type":"smtp","slug":"smtp","name":"SMTP","authType":"api-key","bgColor":"#2D3748","integrationType":"email"},{"type":"snowflake","slug":"snowflake","name":"Snowflake","authType":"api-key","bgColor":"#FFFFFF","integrationType":"databases"},{"type":"splunk","slug":"splunk","name":"Splunk","authType":"api-key","bgColor":"#FFFFFF","integrationType":"observability"},{"type":"sportmonks","slug":"sportmonks","name":"Sportmonks","authType":"api-key","bgColor":"#171534","integrationType":"analytics"},{"type":"square","slug":"square","name":"Square","authType":"api-key","bgColor":"#000000","integrationType":"commerce"},{"type":"ssh_v2","slug":"ssh","name":"SSH","authType":"api-key","bgColor":"#000000","integrationType":"devops"},{"type":"stagehand","slug":"stagehand","name":"Stagehand","authType":"api-key","bgColor":"#FFC83C","integrationType":"ai"},{"type":"stripe","slug":"stripe","name":"Stripe","authType":"api-key","bgColor":"#635BFF","integrationType":"commerce"},{"type":"supabase","slug":"supabase","name":"Supabase","authType":"api-key","bgColor":"#1C1C1C","integrationType":"databases"},{"type":"tailscale","slug":"tailscale","name":"Tailscale","authType":"api-key","bgColor":"#2E2D2D","integrationType":"security"},{"type":"tavily","slug":"tavily","name":"Tavily","authType":"api-key","bgColor":"#FFFFFF","integrationType":"search"},{"type":"telegram","slug":"telegram","name":"Telegram","authType":"api-key","bgColor":"#FFFFFF","integrationType":"communication"},{"type":"temporal","slug":"temporal","name":"Temporal","authType":"api-key","bgColor":"#141414","integrationType":"devops"},{"type":"thrive","slug":"thrive","name":"Thrive","authType":"api-key","bgColor":"#FFFFFF","integrationType":"hr"},{"type":"tiktok","slug":"tiktok","name":"TikTok","authType":"oauth","oauthServiceId":"tiktok","bgColor":"#000000","integrationType":"communication"},{"type":"tinybird","slug":"tinybird","name":"Tinybird","authType":"api-key","bgColor":"#2EF598","integrationType":"analytics"},{"type":"tinyfish","slug":"tinyfish","name":"TinyFish","authType":"api-key","bgColor":"#FFFFFF","integrationType":"ai"},{"type":"trello","slug":"trello","name":"Trello","authType":"oauth","oauthServiceId":"trello","bgColor":"#0052CC","integrationType":"productivity"},{"type":"trigger_dev","slug":"trigger-dev","name":"Trigger.dev","authType":"api-key","bgColor":"#000000","integrationType":"devops"},{"type":"twilio_sms","slug":"twilio-sms","name":"Twilio SMS","authType":"api-key","bgColor":"#F22F46","integrationType":"communication"},{"type":"twilio_voice","slug":"twilio-voice","name":"Twilio Voice","authType":"api-key","bgColor":"#F22F46","integrationType":"communication"},{"type":"typeform","slug":"typeform","name":"Typeform","authType":"api-key","bgColor":"#262627","integrationType":"documents"},{"type":"upstash","slug":"upstash","name":"Upstash","authType":"api-key","bgColor":"#181C1E","integrationType":"databases"},{"type":"uptimerobot","slug":"uptimerobot","name":"UptimeRobot","authType":"api-key","bgColor":"#111921","integrationType":"observability"},{"type":"vanta","slug":"vanta","name":"Vanta","authType":"api-key","bgColor":"#F8F4F3","integrationType":"security"},{"type":"vercel","slug":"vercel","name":"Vercel","authType":"api-key","bgColor":"#171717","integrationType":"devops"},{"type":"wealthbox","slug":"wealthbox","name":"Wealthbox","authType":"oauth","oauthServiceId":"wealthbox","bgColor":"#FFFFFF","integrationType":"sales"},{"type":"webflow","slug":"webflow","name":"Webflow","authType":"oauth","oauthServiceId":"webflow","bgColor":"#FFFFFF","integrationType":"marketing"},{"type":"whatsapp","slug":"whatsapp","name":"WhatsApp","authType":"api-key","bgColor":"#25D366","integrationType":"communication"},{"type":"wikipedia","slug":"wikipedia","name":"Wikipedia","authType":"none","bgColor":"#000000","integrationType":"search"},{"type":"windchill","slug":"windchill","name":"Windchill","authType":"api-key","bgColor":"#FFFFFF","integrationType":"documents"},{"type":"wiza","slug":"wiza","name":"Wiza","authType":"api-key","bgColor":"#9284BC","integrationType":"sales"},{"type":"wordpress","slug":"wordpress","name":"WordPress","authType":"oauth","oauthServiceId":"wordpress","bgColor":"#21759B","integrationType":"marketing"},{"type":"workday","slug":"workday","name":"Workday","authType":"none","bgColor":"#F5F0EB","integrationType":"hr"},{"type":"x","slug":"x","name":"X","authType":"oauth","oauthServiceId":"x","bgColor":"#000000","integrationType":"communication"},{"type":"youcom","slug":"you-com","name":"You.com","authType":"api-key","bgColor":"#FFFFFF","integrationType":"search"},{"type":"youtube","slug":"youtube","name":"YouTube","authType":"api-key","bgColor":"#FF0000","integrationType":"communication"},{"type":"zendesk","slug":"zendesk","name":"Zendesk","authType":"api-key","bgColor":"#03363D","integrationType":"support"},{"type":"zep","slug":"zep","name":"Zep","authType":"api-key","bgColor":"#E8E8E8","integrationType":"ai"},{"type":"zerobounce","slug":"zerobounce","name":"ZeroBounce","authType":"api-key","bgColor":"#FFFFFF","integrationType":"sales"},{"type":"zoho_desk","slug":"zoho-desk","name":"Zoho Desk","authType":"oauth","oauthServiceId":"zoho-desk","bgColor":"#FFFFFF","integrationType":"support"},{"type":"zoom","slug":"zoom","name":"Zoom","authType":"oauth","oauthServiceId":"zoom","bgColor":"#2D8CFF","integrationType":"communication"},{"type":"zoominfo","slug":"zoominfo","name":"ZoomInfo","authType":"api-key","bgColor":"#EA1B15","integrationType":"sales"}]' + '[{"type":"onepassword","slug":"1password","name":"1Password","authType":"api-key","bgColor":"#FFFFFF","integrationType":"security"},{"type":"affinity","slug":"affinity","name":"Affinity","authType":"api-key","bgColor":"#1B1C1E","integrationType":"sales"},{"type":"agentmail","slug":"agentmail","name":"AgentMail","authType":"api-key","bgColor":"#000000","integrationType":"email"},{"type":"agentphone","slug":"agentphone","name":"AgentPhone","authType":"api-key","bgColor":"linear-gradient(135deg, #1a1a1a 0%, #0a2a14 100%)","integrationType":"communication"},{"type":"agiloft","slug":"agiloft","name":"Agiloft","authType":"api-key","bgColor":"#001028","integrationType":"productivity"},{"type":"ahrefs","slug":"ahrefs","name":"Ahrefs","authType":"api-key","bgColor":"#FFFFFF","integrationType":"analytics"},{"type":"airtable","slug":"airtable","name":"Airtable","authType":"oauth","oauthServiceId":"airtable","bgColor":"#FFFFFF","integrationType":"databases"},{"type":"airweave","slug":"airweave","name":"Airweave","authType":"api-key","bgColor":"#6366F1","integrationType":"search"},{"type":"algolia","slug":"algolia","name":"Algolia","authType":"api-key","bgColor":"#003DFF","integrationType":"search"},{"type":"dynamodb","slug":"amazon-dynamodb","name":"Amazon DynamoDB","authType":"api-key","bgColor":"linear-gradient(45deg, #2E27AD 0%, #527FFF 100%)","integrationType":"databases"},{"type":"rds","slug":"amazon-rds","name":"Amazon RDS","authType":"none","bgColor":"linear-gradient(45deg, #2E27AD 0%, #527FFF 100%)","integrationType":"databases"},{"type":"sqs","slug":"amazon-sqs","name":"Amazon SQS","authType":"api-key","bgColor":"linear-gradient(45deg, #2E27AD 0%, #527FFF 100%)","integrationType":"devops"},{"type":"amplitude","slug":"amplitude","name":"Amplitude","authType":"api-key","bgColor":"#13294B","integrationType":"analytics"},{"type":"apify","slug":"apify","name":"Apify","authType":"api-key","bgColor":"#FFFFFF","integrationType":"search"},{"type":"apollo","slug":"apollo","name":"Apollo","authType":"api-key","bgColor":"#EBF212","integrationType":"sales"},{"type":"arxiv","slug":"arxiv","name":"ArXiv","authType":"none","bgColor":"#FFFFFF","integrationType":"search"},{"type":"asana","slug":"asana","name":"Asana","authType":"oauth","oauthServiceId":"asana","bgColor":"#FFFFFF","integrationType":"productivity"},{"type":"ashby","slug":"ashby","name":"Ashby","authType":"api-key","bgColor":"#5D4ED6","integrationType":"hr"},{"type":"athena","slug":"athena","name":"Athena","authType":"none","bgColor":"linear-gradient(45deg, #4D27A8 0%, #A166FF 100%)","integrationType":"analytics"},{"type":"attio","slug":"attio","name":"Attio","authType":"oauth","oauthServiceId":"attio","bgColor":"#1D1E20","integrationType":"sales"},{"type":"appconfig","slug":"aws-appconfig","name":"AWS AppConfig","authType":"api-key","bgColor":"linear-gradient(45deg, #B0084D 0%, #FF4F8B 100%)","integrationType":"devops"},{"type":"iam","slug":"aws-iam","name":"AWS IAM","authType":"api-key","bgColor":"linear-gradient(45deg, #BD0816 0%, #FF5252 100%)","integrationType":"security"},{"type":"identity_center","slug":"aws-identity-center","name":"AWS Identity Center","authType":"api-key","bgColor":"linear-gradient(45deg, #BD0816 0%, #FF5252 100%)","integrationType":"security"},{"type":"secrets_manager","slug":"aws-secrets-manager","name":"AWS Secrets Manager","authType":"none","bgColor":"linear-gradient(45deg, #BD0816 0%, #FF5252 100%)","integrationType":"security"},{"type":"ses","slug":"aws-ses","name":"AWS SES","authType":"api-key","bgColor":"linear-gradient(45deg, #BD0816 0%, #FF5252 100%)","integrationType":"email"},{"type":"sts","slug":"aws-sts","name":"AWS STS","authType":"api-key","bgColor":"linear-gradient(45deg, #BD0816 0%, #FF5252 100%)","integrationType":"security"},{"type":"ssm","slug":"aws-systems-manager","name":"AWS Systems Manager","authType":"api-key","bgColor":"#E7157B","integrationType":"devops"},{"type":"textract_v2","slug":"aws-textract","name":"AWS Textract","authType":"api-key","bgColor":"linear-gradient(135deg, #055F4E 0%, #56C0A7 100%)","integrationType":"ai"},{"type":"microsoft_ad","slug":"azure-ad","name":"Azure AD","authType":"oauth","oauthServiceId":"microsoft-ad","bgColor":"#0078D4","integrationType":"security"},{"type":"azure_data_explorer","slug":"azure-data-explorer","name":"Azure Data Explorer","authType":"api-key","bgColor":"#FFFFFF","integrationType":"databases"},{"type":"azure_devops","slug":"azure-devops","name":"Azure DevOps","authType":"api-key","bgColor":"#0078D4","integrationType":"devops"},{"type":"bitbucket","slug":"bitbucket","name":"Bitbucket","authType":"oauth","oauthServiceId":"bitbucket","bgColor":"#FFFFFF","integrationType":"devops"},{"type":"box_v2","slug":"box","name":"Box","authType":"oauth","oauthServiceId":"box","bgColor":"#FFFFFF","integrationType":"documents"},{"type":"brandfetch","slug":"brandfetch","name":"Brandfetch","authType":"api-key","bgColor":"#000000","integrationType":"sales"},{"type":"brex","slug":"brex","name":"Brex","authType":"api-key","bgColor":"#171717","integrationType":"commerce"},{"type":"brightdata","slug":"bright-data","name":"Bright Data","authType":"api-key","bgColor":"#FFFFFF","integrationType":"search"},{"type":"browser_use","slug":"browser-use","name":"Browser Use","authType":"api-key","bgColor":"#181C1E","integrationType":"ai"},{"type":"buffer","slug":"buffer","name":"Buffer","authType":"api-key","bgColor":"#FFFFFF","integrationType":"marketing"},{"type":"calcom","slug":"cal-com","name":"Cal.com","authType":"oauth","oauthServiceId":"calcom","bgColor":"#292929","integrationType":"productivity"},{"type":"calendly","slug":"calendly","name":"Calendly","authType":"api-key","bgColor":"#FFFFFF","integrationType":"productivity"},{"type":"cbinsights","slug":"cb-insights","name":"CB Insights","authType":"api-key","bgColor":"#FFFFFF","integrationType":"sales"},{"type":"circleback","slug":"circleback","name":"Circleback","authType":"api-key","bgColor":"linear-gradient(180deg, #E0F7FA 0%, #FFFFFF 100%)","integrationType":"ai"},{"type":"managed_agent","slug":"claude-managed-agents","name":"Claude Managed Agents","authType":"api-key","serviceAccountServiceId":"claude-platform","bgColor":"#DA7756","integrationType":"ai"},{"type":"clay","slug":"clay","name":"Clay","authType":"api-key","bgColor":"#FFFFFF","integrationType":"sales"},{"type":"clerk","slug":"clerk","name":"Clerk","authType":"none","bgColor":"#131316","integrationType":"security"},{"type":"clickhouse","slug":"clickhouse","name":"ClickHouse","authType":"none","bgColor":"#f9ff69","integrationType":"databases"},{"type":"clickup","slug":"clickup","name":"ClickUp","authType":"oauth","oauthServiceId":"clickup","bgColor":"#FFFFFF","integrationType":"productivity"},{"type":"cloudflare","slug":"cloudflare","name":"Cloudflare","authType":"api-key","bgColor":"#F5F6FA","integrationType":"devops"},{"type":"cloudformation","slug":"cloudformation","name":"CloudFormation","authType":"none","bgColor":"linear-gradient(45deg, #B0084D 0%, #FF4F8B 100%)","integrationType":"devops"},{"type":"cloudtrail","slug":"cloudtrail","name":"CloudTrail","authType":"api-key","bgColor":"linear-gradient(45deg, #B0084D 0%, #FF4F8B 100%)","integrationType":"security"},{"type":"cloudwatch","slug":"cloudwatch","name":"CloudWatch","authType":"none","bgColor":"linear-gradient(45deg, #B0084D 0%, #FF4F8B 100%)","integrationType":"observability"},{"type":"coda","slug":"coda","name":"Coda","authType":"api-key","serviceAccountServiceId":"coda","bgColor":"#FFFFFF","integrationType":"documents"},{"type":"codepipeline","slug":"codepipeline","name":"CodePipeline","authType":"api-key","bgColor":"linear-gradient(45deg, #2E27AD 0%, #527FFF 100%)","integrationType":"devops"},{"type":"confluence_v2","slug":"confluence","name":"Confluence","authType":"oauth","oauthServiceId":"confluence","bgColor":"#FFFFFF","integrationType":"documents"},{"type":"context_dev","slug":"context-dev","name":"Context.dev","authType":"api-key","bgColor":"#ffffff","integrationType":"search"},{"type":"convex","slug":"convex","name":"Convex","authType":"api-key","bgColor":"#FFFFFF","integrationType":"databases"},{"type":"crowdstrike","slug":"crowdstrike","name":"CrowdStrike","authType":"api-key","bgColor":"#E01F3D","integrationType":"security"},{"type":"crunchbase","slug":"crunchbase","name":"Crunchbase","authType":"api-key","bgColor":"#0287D1","integrationType":"sales"},{"type":"cursor_v2","slug":"cursor","name":"Cursor","authType":"api-key","bgColor":"#1E1E1E","integrationType":"devops"},{"type":"dagster","slug":"dagster","name":"Dagster","authType":"api-key","bgColor":"#ffffff","integrationType":"observability"},{"type":"databricks","slug":"databricks","name":"Databricks","authType":"api-key","bgColor":"#F9F7F4","integrationType":"databases"},{"type":"datadog","slug":"datadog","name":"Datadog","authType":"api-key","bgColor":"#632CA6","integrationType":"observability"},{"type":"datagma","slug":"datagma","name":"Datagma","authType":"api-key","bgColor":"#FFFFFF","integrationType":"sales"},{"type":"daytona","slug":"daytona","name":"Daytona","authType":"api-key","bgColor":"#FFFFFF","integrationType":"devops"},{"type":"devin","slug":"devin","name":"Devin","authType":"api-key","bgColor":"#12141A","integrationType":"devops"},{"type":"discord","slug":"discord","name":"Discord","authType":"api-key","bgColor":"#5865F2","integrationType":"communication"},{"type":"docusign","slug":"docusign","name":"DocuSign","authType":"oauth","oauthServiceId":"docusign","bgColor":"#FFFFFF","integrationType":"documents"},{"type":"downdetector","slug":"downdetector","name":"Downdetector","authType":"api-key","bgColor":"#FFFFFF","integrationType":"observability"},{"type":"dropbox_v2","slug":"dropbox","name":"Dropbox","authType":"oauth","oauthServiceId":"dropbox","bgColor":"#0061FF","integrationType":"documents"},{"type":"dropcontact","slug":"dropcontact","name":"Dropcontact","authType":"api-key","bgColor":"#0ABA9F","integrationType":"sales"},{"type":"dspy","slug":"dspy","name":"DSPy","authType":"api-key","bgColor":"#FFFFFF","integrationType":"ai"},{"type":"dub_v2","slug":"dub","name":"Dub","authType":"api-key","bgColor":"#181C1E","integrationType":"devops"},{"type":"duckduckgo","slug":"duckduckgo","name":"DuckDuckGo","authType":"none","bgColor":"#FFFFFF","integrationType":"search"},{"type":"dynatrace","slug":"dynatrace","name":"Dynatrace","authType":"api-key","bgColor":"#FFFFFF","integrationType":"observability"},{"type":"elasticsearch","slug":"elasticsearch","name":"Elasticsearch","authType":"api-key","bgColor":"#FFFFFF","integrationType":"databases"},{"type":"elevenlabs","slug":"elevenlabs","name":"ElevenLabs","authType":"api-key","bgColor":"#181C1E","integrationType":"ai"},{"type":"emailbison","slug":"email-bison","name":"Email Bison","authType":"api-key","bgColor":"#FB7A22","integrationType":"email"},{"type":"embeddings","slug":"embeddings","name":"Embeddings","authType":"api-key","bgColor":"#7B4DFF","integrationType":"ai"},{"type":"enrich","slug":"enrich","name":"Enrich","authType":"api-key","bgColor":"#E5E5E6","integrationType":"sales"},{"type":"enrow","slug":"enrow","name":"Enrow","authType":"api-key","bgColor":"#FFFFFF","integrationType":"sales"},{"type":"exa","slug":"exa","name":"Exa","authType":"api-key","bgColor":"#1F40ED","integrationType":"search"},{"type":"extend_v2","slug":"extend","name":"Extend","authType":"api-key","bgColor":"#000000","integrationType":"ai"},{"type":"fathom","slug":"fathom","name":"Fathom","authType":"api-key","bgColor":"#181C1E","integrationType":"analytics"},{"type":"findymail","slug":"findymail","name":"Findymail","authType":"api-key","bgColor":"#FFFFFF","integrationType":"sales"},{"type":"firecrawl","slug":"firecrawl","name":"Firecrawl","authType":"api-key","bgColor":"#181C1E","integrationType":"search"},{"type":"fireflies_v2","slug":"fireflies","name":"Fireflies","authType":"api-key","bgColor":"#100730","integrationType":"productivity"},{"type":"flint","slug":"flint","name":"Flint","authType":"api-key","bgColor":"#F6F54F","integrationType":"marketing"},{"type":"gamma","slug":"gamma","name":"Gamma","authType":"api-key","bgColor":"#002253","integrationType":"marketing"},{"type":"github_v2","slug":"github","name":"GitHub","authType":"api-key","bgColor":"#181C1E","integrationType":"devops"},{"type":"gitlab","slug":"gitlab","name":"GitLab","authType":"api-key","bgColor":"#FFFFFF","integrationType":"devops"},{"type":"gmail_v2","slug":"gmail","name":"Gmail","authType":"oauth","oauthServiceId":"gmail","bgColor":"#FFFFFF","integrationType":"email"},{"type":"gong","slug":"gong","name":"Gong","authType":"api-key","bgColor":"#8039DF","integrationType":"sales"},{"type":"google_ads","slug":"google-ads","name":"Google Ads","authType":"oauth","oauthServiceId":"google-ads","bgColor":"#FFFFFF","integrationType":"analytics"},{"type":"google_appsheet","slug":"google-appsheet","name":"Google AppSheet","authType":"api-key","bgColor":"#FFFFFF","integrationType":"databases"},{"type":"google_bigquery","slug":"google-bigquery","name":"Google BigQuery","authType":"oauth","oauthServiceId":"google-bigquery","bgColor":"#FFFFFF","integrationType":"databases"},{"type":"google_books","slug":"google-books","name":"Google Books","authType":"api-key","bgColor":"#FFFFFF","integrationType":"search"},{"type":"google_calendar_v2","slug":"google-calendar","name":"Google Calendar","authType":"oauth","oauthServiceId":"google-calendar","bgColor":"#FFFFFF","integrationType":"productivity"},{"type":"google_contacts","slug":"google-contacts","name":"Google Contacts","authType":"oauth","oauthServiceId":"google-contacts","bgColor":"#FFFFFF","integrationType":"productivity"},{"type":"google_docs","slug":"google-docs","name":"Google Docs","authType":"oauth","oauthServiceId":"google-docs","bgColor":"#FFFFFF","integrationType":"documents"},{"type":"google_drive","slug":"google-drive","name":"Google Drive","authType":"oauth","oauthServiceId":"google-drive","bgColor":"#FFFFFF","integrationType":"documents"},{"type":"google_forms","slug":"google-forms","name":"Google Forms","authType":"oauth","oauthServiceId":"google-forms","bgColor":"#FFFFFF","integrationType":"documents"},{"type":"google_groups","slug":"google-groups","name":"Google Groups","authType":"oauth","oauthServiceId":"google-groups","bgColor":"#E8F0FE","integrationType":"communication"},{"type":"google_maps","slug":"google-maps","name":"Google Maps","authType":"api-key","bgColor":"#FFFFFF","integrationType":"search"},{"type":"google_meet","slug":"google-meet","name":"Google Meet","authType":"oauth","oauthServiceId":"google-meet","bgColor":"#FFFFFF","integrationType":"communication"},{"type":"google_pagespeed","slug":"google-pagespeed","name":"Google PageSpeed","authType":"api-key","bgColor":"#FFFFFF","integrationType":"analytics"},{"type":"google_search","slug":"google-search","name":"Google Search","authType":"api-key","bgColor":"#FFFFFF","integrationType":"search"},{"type":"google_sheets_v2","slug":"google-sheets","name":"Google Sheets","authType":"oauth","oauthServiceId":"google-sheets","bgColor":"#FFFFFF","integrationType":"documents"},{"type":"google_slides_v2","slug":"google-slides","name":"Google Slides","authType":"oauth","oauthServiceId":"google-drive","bgColor":"#FFFFFF","integrationType":"documents"},{"type":"google_tasks","slug":"google-tasks","name":"Google Tasks","authType":"oauth","oauthServiceId":"google-tasks","bgColor":"#FFFFFF","integrationType":"productivity"},{"type":"google_translate","slug":"google-translate","name":"Google Translate","authType":"api-key","bgColor":"#FFFFFF","integrationType":"ai"},{"type":"google_vault","slug":"google-vault","name":"Google Vault","authType":"oauth","oauthServiceId":"google-vault","bgColor":"#E8F0FE","integrationType":"security"},{"type":"grafana","slug":"grafana","name":"Grafana","authType":"api-key","bgColor":"#FFFFFF","integrationType":"observability"},{"type":"grain_v2","slug":"grain","name":"Grain","authType":"api-key","bgColor":"#F6FAF9","integrationType":"productivity"},{"type":"granola","slug":"granola","name":"Granola","authType":"api-key","bgColor":"#B2C147","integrationType":"productivity"},{"type":"greenhouse","slug":"greenhouse","name":"Greenhouse","authType":"api-key","bgColor":"#469776","integrationType":"hr"},{"type":"greptile","slug":"greptile","name":"Greptile","authType":"api-key","bgColor":"#181C1E","integrationType":"devops"},{"type":"harmonic","slug":"harmonic","name":"Harmonic","authType":"api-key","serviceAccountServiceId":"harmonic","bgColor":"#FFFFFF","integrationType":"sales"},{"type":"hex","slug":"hex","name":"Hex","authType":"api-key","bgColor":"#14151A","integrationType":"analytics"},{"type":"hubspot","slug":"hubspot","name":"HubSpot","authType":"oauth","oauthServiceId":"hubspot","bgColor":"#FF7A59","integrationType":"sales"},{"type":"huggingface","slug":"hugging-face","name":"Hugging Face","authType":"api-key","bgColor":"#0B0F19","integrationType":"ai"},{"type":"hunter","slug":"hunter-io","name":"Hunter.io","authType":"api-key","bgColor":"#FFFFFF","integrationType":"sales"},{"type":"icypeas","slug":"icypeas","name":"Icypeas","authType":"api-key","bgColor":"#d4d4d4","integrationType":"sales"},{"type":"incidentio","slug":"incident-io","name":"incident.io","authType":"api-key","bgColor":"#FFFFFF","integrationType":"observability"},{"type":"infisical","slug":"infisical","name":"Infisical","authType":"api-key","bgColor":"#F7FE62","integrationType":"security"},{"type":"instantly","slug":"instantly","name":"Instantly","authType":"api-key","bgColor":"#FFFFFF","integrationType":"email"},{"type":"intercom_v2","slug":"intercom","name":"Intercom","authType":"api-key","bgColor":"#FFFFFF","integrationType":"support"},{"type":"jina","slug":"jina","name":"Jina","authType":"api-key","bgColor":"#333333","integrationType":"search"},{"type":"jira","slug":"jira","name":"Jira","authType":"oauth","oauthServiceId":"jira","bgColor":"#FFFFFF","integrationType":"productivity"},{"type":"jira_service_management","slug":"jira-service-management","name":"Jira Service Management","authType":"oauth","oauthServiceId":"jira","bgColor":"#FFFFFF","integrationType":"support"},{"type":"jotform","slug":"jotform","name":"Jotform","authType":"api-key","bgColor":"#FFFFFF","integrationType":"productivity"},{"type":"jupyter_v2","slug":"jupyter","name":"Jupyter","authType":"api-key","bgColor":"#FFFFFF","integrationType":"devops"},{"type":"kalshi_v2","slug":"kalshi","name":"Kalshi","authType":"api-key","bgColor":"#09C285","integrationType":"analytics"},{"type":"ketch","slug":"ketch","name":"Ketch","authType":"none","bgColor":"#9B5CFF","integrationType":"security"},{"type":"lambda","slug":"lambda","name":"Lambda","authType":"api-key","bgColor":"linear-gradient(45deg, #C8511B 0%, #FF9900 100%)","integrationType":"devops"},{"type":"langsmith","slug":"langsmith","name":"LangSmith","authType":"api-key","bgColor":"#181C1E","integrationType":"observability"},{"type":"latex","slug":"latex","name":"LaTeX","authType":"none","bgColor":"#FFFFFF","integrationType":"documents"},{"type":"launchdarkly","slug":"launchdarkly","name":"LaunchDarkly","authType":"api-key","bgColor":"#191919","integrationType":"devops"},{"type":"leadmagic","slug":"leadmagic","name":"LeadMagic","authType":"api-key","bgColor":"#FFFFFF","integrationType":"sales"},{"type":"lemlist","slug":"lemlist","name":"Lemlist","authType":"api-key","bgColor":"#316BFF","integrationType":"email"},{"type":"linear_v2","slug":"linear","name":"Linear","authType":"oauth","oauthServiceId":"linear","bgColor":"#5E6AD2","integrationType":"productivity"},{"type":"linkedin","slug":"linkedin","name":"LinkedIn","authType":"oauth","oauthServiceId":"linkedin","bgColor":"#0072B1","integrationType":"sales"},{"type":"linkup","slug":"linkup","name":"Linkup","authType":"api-key","bgColor":"#D6D3C7","integrationType":"search"},{"type":"linq","slug":"linq","name":"Linq","authType":"api-key","bgColor":"#000000","integrationType":"communication"},{"type":"logfire","slug":"logfire","name":"Logfire","authType":"api-key","bgColor":"#000000","integrationType":"observability"},{"type":"logrocket","slug":"logrocket","name":"LogRocket","authType":"api-key","bgColor":"#764ABC","integrationType":"observability"},{"type":"loops","slug":"loops","name":"Loops","authType":"api-key","bgColor":"#FAFAF9","integrationType":"email"},{"type":"luma","slug":"luma","name":"Luma","authType":"api-key","bgColor":"#000000","integrationType":"productivity"},{"type":"mailchimp","slug":"mailchimp","name":"Mailchimp","authType":"api-key","bgColor":"#FFE01B","integrationType":"email"},{"type":"mailgun","slug":"mailgun","name":"Mailgun","authType":"api-key","bgColor":"#C12126","integrationType":"email"},{"type":"manageengine_sdp","slug":"manageengine-servicedesk-plus","name":"ManageEngine ServiceDesk Plus","authType":"oauth","oauthServiceId":"manageengine-sdp","bgColor":"#FFFFFF","integrationType":"support"},{"type":"mem0","slug":"mem0","name":"Mem0","authType":"api-key","bgColor":"#181C1E","integrationType":"ai"},{"type":"microsoft_dataverse_v2","slug":"microsoft-dataverse","name":"Microsoft Dataverse","authType":"oauth","oauthServiceId":"microsoft-dataverse","bgColor":"#FFFFFF","integrationType":"databases"},{"type":"microsoft_dynamics_365","slug":"microsoft-dynamics-365-crm","name":"Microsoft Dynamics 365 CRM","authType":"oauth","oauthServiceId":"microsoft-dataverse","bgColor":"#FFFFFF","integrationType":"sales"},{"type":"microsoft_excel_v2","slug":"microsoft-excel","name":"Microsoft Excel","authType":"oauth","oauthServiceId":"microsoft-excel","bgColor":"#FFFFFF","integrationType":"documents"},{"type":"microsoft_planner","slug":"microsoft-planner","name":"Microsoft Planner","authType":"oauth","oauthServiceId":"microsoft-planner","bgColor":"#FFFFFF","integrationType":"productivity"},{"type":"mssql","slug":"microsoft-sql-server","name":"Microsoft SQL Server","authType":"none","bgColor":"#FFFFFF","integrationType":"databases"},{"type":"microsoft_teams","slug":"microsoft-teams","name":"Microsoft Teams","authType":"oauth","oauthServiceId":"microsoft-teams","bgColor":"#FFFFFF","integrationType":"communication"},{"type":"microsoft_word","slug":"microsoft-word","name":"Microsoft Word","authType":"oauth","oauthServiceId":"microsoft-word","bgColor":"#FFFFFF","integrationType":"documents"},{"type":"millionverifier","slug":"millionverifier","name":"MillionVerifier","authType":"api-key","bgColor":"#FFFFFF","integrationType":"sales"},{"type":"mintlify","slug":"mintlify","name":"Mintlify","authType":"api-key","bgColor":"#000000","integrationType":"documents"},{"type":"mistral_parse_v3","slug":"mistral-parser","name":"Mistral Parser","authType":"api-key","bgColor":"#000000","integrationType":"ai"},{"type":"modal","slug":"modal","name":"Modal","authType":"api-key","bgColor":"#000000","integrationType":"ai"},{"type":"monday","slug":"monday","name":"Monday","authType":"oauth","oauthServiceId":"monday","bgColor":"#FFFFFF","integrationType":"productivity"},{"type":"mongodb","slug":"mongodb","name":"MongoDB","authType":"none","bgColor":"#FFFFFF","integrationType":"databases"},{"type":"mysql","slug":"mysql","name":"MySQL","authType":"none","bgColor":"#FFFFFF","integrationType":"databases"},{"type":"neo4j","slug":"neo4j","name":"Neo4j","authType":"none","bgColor":"#FFFFFF","integrationType":"databases"},{"type":"neverbounce","slug":"neverbounce","name":"NeverBounce","authType":"api-key","bgColor":"#064AF4","integrationType":"sales"},{"type":"new_relic","slug":"new-relic","name":"New Relic","authType":"api-key","bgColor":"#000000","integrationType":"observability"},{"type":"notion_v2","slug":"notion","name":"Notion","authType":"oauth","oauthServiceId":"notion","bgColor":"#FFFFFF","integrationType":"documents"},{"type":"obsidian","slug":"obsidian","name":"Obsidian","authType":"api-key","bgColor":"#0F0F0F","integrationType":"documents"},{"type":"okta","slug":"okta","name":"Okta","authType":"api-key","bgColor":"#191919","integrationType":"security"},{"type":"onedrive","slug":"onedrive","name":"OneDrive","authType":"oauth","oauthServiceId":"onedrive","bgColor":"#FFFFFF","integrationType":"documents"},{"type":"netsuite","slug":"oracle-netsuite","name":"Oracle NetSuite","authType":"api-key","serviceAccountServiceId":"netsuite","bgColor":"#FFFFFF","integrationType":"commerce"},{"type":"otter","slug":"otter-ai","name":"Otter.ai","authType":"api-key","bgColor":"#FFFFFF","integrationType":"productivity"},{"type":"outlook","slug":"outlook","name":"Outlook","authType":"oauth","oauthServiceId":"outlook","bgColor":"#FFFFFF","integrationType":"email"},{"type":"pagerduty","slug":"pagerduty","name":"PagerDuty","authType":"api-key","bgColor":"#06AC38","integrationType":"observability"},{"type":"parallel_ai","slug":"parallel-ai","name":"Parallel AI","authType":"api-key","bgColor":"#1D1C1A","integrationType":"search"},{"type":"peopledatalabs","slug":"people-data-labs","name":"People Data Labs","authType":"api-key","bgColor":"#4831C3","integrationType":"sales"},{"type":"perplexity","slug":"perplexity","name":"Perplexity","authType":"api-key","bgColor":"#20808D","integrationType":"ai"},{"type":"persona","slug":"persona","name":"Persona","authType":"api-key","bgColor":"#FFFFFF","integrationType":"security"},{"type":"pinecone","slug":"pinecone","name":"Pinecone","authType":"api-key","bgColor":"#0D1117","integrationType":"databases"},{"type":"pipedrive","slug":"pipedrive","name":"Pipedrive","authType":"oauth","oauthServiceId":"pipedrive","bgColor":"#2E6936","integrationType":"sales"},{"type":"pitchbook","slug":"pitchbook","name":"PitchBook","authType":"api-key","bgColor":"#1D5080","integrationType":"analytics"},{"type":"planetscale","slug":"planetscale","name":"PlanetScale","authType":"api-key","bgColor":"#111111","integrationType":"databases"},{"type":"polymarket","slug":"polymarket","name":"Polymarket","authType":"none","bgColor":"#4C82FB","integrationType":"analytics"},{"type":"postgresql","slug":"postgresql","name":"PostgreSQL","authType":"none","bgColor":"#336791","integrationType":"databases"},{"type":"posthog","slug":"posthog","name":"PostHog","authType":"api-key","bgColor":"#FFFFFF","integrationType":"analytics"},{"type":"powerbi","slug":"power-bi","name":"Power BI","authType":"oauth","oauthServiceId":"microsoft-powerbi","bgColor":"#FFFFFF","integrationType":"analytics"},{"type":"profound","slug":"profound","name":"Profound","authType":"api-key","bgColor":"#000000","integrationType":"analytics"},{"type":"prospeo","slug":"prospeo","name":"Prospeo","authType":"api-key","bgColor":"#FF1A26","integrationType":"sales"},{"type":"pulse_v2","slug":"pulse","name":"Pulse","authType":"api-key","bgColor":"#FFFFFF","integrationType":"ai"},{"type":"qdrant","slug":"qdrant","name":"Qdrant","authType":"api-key","bgColor":"#1A223F","integrationType":"databases"},{"type":"quartr","slug":"quartr","name":"Quartr","authType":"api-key","bgColor":"#000000","integrationType":"analytics"},{"type":"quickbooks","slug":"quickbooks","name":"QuickBooks","authType":"oauth","oauthServiceId":"quickbooks","bgColor":"#2CA01C","integrationType":"commerce"},{"type":"quiver_v2","slug":"quiver","name":"Quiver","authType":"api-key","bgColor":"#FFFFFF","integrationType":"ai"},{"type":"rabbitmq","slug":"rabbitmq","name":"RabbitMQ","authType":"api-key","bgColor":"#FFFFFF","integrationType":"devops"},{"type":"railway","slug":"railway","name":"Railway","authType":"api-key","bgColor":"#000000","integrationType":"devops"},{"type":"rb2b","slug":"rb2b","name":"RB2B","authType":"api-key","bgColor":"#51FF00","integrationType":"sales"},{"type":"reddit","slug":"reddit","name":"Reddit","authType":"oauth","oauthServiceId":"reddit","bgColor":"#FF5700","integrationType":"communication"},{"type":"redis","slug":"redis","name":"Redis","authType":"api-key","bgColor":"#FF4438","integrationType":"databases"},{"type":"reducto_v2","slug":"reducto","name":"Reducto","authType":"api-key","bgColor":"#5c0c5c","integrationType":"ai"},{"type":"resend","slug":"resend","name":"Resend","authType":"api-key","bgColor":"#181C1E","integrationType":"email"},{"type":"revenuecat","slug":"revenuecat","name":"RevenueCat","authType":"api-key","bgColor":"#F25A5A","integrationType":"commerce"},{"type":"rippling","slug":"rippling","name":"Rippling","authType":"api-key","bgColor":"#502D3C","integrationType":"hr"},{"type":"rocketlane","slug":"rocketlane","name":"Rocketlane","authType":"api-key","bgColor":"#000000","integrationType":"productivity"},{"type":"rootly","slug":"rootly","name":"Rootly","authType":"api-key","bgColor":"#6C72C8","integrationType":"observability"},{"type":"s3","slug":"s3","name":"S3","authType":"api-key","bgColor":"linear-gradient(45deg, #1B660F 0%, #6CAE3E 100%)","integrationType":"documents"},{"type":"sailpoint","slug":"sailpoint","name":"SailPoint","authType":"api-key","bgColor":"#FFFFFF","integrationType":"security"},{"type":"salesforce","slug":"salesforce","name":"Salesforce","authType":"oauth","oauthServiceId":"salesforce","bgColor":"#FFFFFF","integrationType":"sales"},{"type":"sap_concur","slug":"sap-concur","name":"SAP Concur","authType":"api-key","bgColor":"#FFFFFF","integrationType":"productivity"},{"type":"sap_s4hana","slug":"sap-s4hana","name":"SAP S4HANA","authType":"api-key","bgColor":"#FFFFFF","integrationType":"hr"},{"type":"semrush","slug":"semrush","name":"Semrush","authType":"api-key","bgColor":"#FFFFFF","integrationType":"analytics"},{"type":"sendblue","slug":"sendblue","name":"Sendblue","authType":"api-key","bgColor":"#008BFF","integrationType":"communication"},{"type":"sendgrid","slug":"sendgrid","name":"SendGrid","authType":"api-key","bgColor":"#1A82E2","integrationType":"email"},{"type":"sentry","slug":"sentry","name":"Sentry","authType":"api-key","bgColor":"#362D59","integrationType":"observability"},{"type":"serper","slug":"serper","name":"Serper","authType":"api-key","bgColor":"#2B3543","integrationType":"search"},{"type":"servicenow_v2","slug":"servicenow","name":"ServiceNow","authType":"api-key","bgColor":"#032D42","integrationType":"support"},{"type":"sftp_v2","slug":"sftp","name":"SFTP","authType":"api-key","bgColor":"#2D3748","integrationType":"documents"},{"type":"sharepoint_v2","slug":"sharepoint","name":"SharePoint","authType":"oauth","oauthServiceId":"sharepoint","bgColor":"#FFFFFF","integrationType":"documents"},{"type":"shopify","slug":"shopify","name":"Shopify","authType":"oauth","oauthServiceId":"shopify","bgColor":"#FFFFFF","integrationType":"commerce"},{"type":"similarweb","slug":"similarweb","name":"Similarweb","authType":"api-key","bgColor":"#000922","integrationType":"analytics"},{"type":"sixtyfour","slug":"sixtyfour-ai","name":"Sixtyfour AI","authType":"api-key","bgColor":"#000000","integrationType":"sales"},{"type":"slack_v2","slug":"slack","name":"Slack","authType":"oauth","oauthServiceId":"slack","bgColor":"#611f69","integrationType":"communication"},{"type":"smartlead","slug":"smartlead","name":"Smartlead","authType":"api-key","bgColor":"#000000","integrationType":"email"},{"type":"smtp","slug":"smtp","name":"SMTP","authType":"api-key","bgColor":"#2D3748","integrationType":"email"},{"type":"snowflake","slug":"snowflake","name":"Snowflake","authType":"api-key","serviceAccountServiceId":"snowflake","bgColor":"#FFFFFF","integrationType":"databases"},{"type":"splunk","slug":"splunk","name":"Splunk","authType":"api-key","bgColor":"#FFFFFF","integrationType":"observability"},{"type":"sportmonks","slug":"sportmonks","name":"Sportmonks","authType":"api-key","bgColor":"#171534","integrationType":"analytics"},{"type":"square","slug":"square","name":"Square","authType":"api-key","bgColor":"#000000","integrationType":"commerce"},{"type":"ssh_v2","slug":"ssh","name":"SSH","authType":"api-key","bgColor":"#000000","integrationType":"devops"},{"type":"stagehand","slug":"stagehand","name":"Stagehand","authType":"api-key","bgColor":"#FFC83C","integrationType":"ai"},{"type":"stripe","slug":"stripe","name":"Stripe","authType":"api-key","bgColor":"#635BFF","integrationType":"commerce"},{"type":"supabase","slug":"supabase","name":"Supabase","authType":"api-key","bgColor":"#1C1C1C","integrationType":"databases"},{"type":"tailscale","slug":"tailscale","name":"Tailscale","authType":"api-key","bgColor":"#2E2D2D","integrationType":"security"},{"type":"tavily","slug":"tavily","name":"Tavily","authType":"api-key","bgColor":"#FFFFFF","integrationType":"search"},{"type":"telegram","slug":"telegram","name":"Telegram","authType":"api-key","bgColor":"#FFFFFF","integrationType":"communication"},{"type":"temporal","slug":"temporal","name":"Temporal","authType":"api-key","bgColor":"#141414","integrationType":"devops"},{"type":"thrive","slug":"thrive","name":"Thrive","authType":"api-key","bgColor":"#FFFFFF","integrationType":"hr"},{"type":"tiktok","slug":"tiktok","name":"TikTok","authType":"oauth","oauthServiceId":"tiktok","bgColor":"#000000","integrationType":"communication"},{"type":"tinybird","slug":"tinybird","name":"Tinybird","authType":"api-key","bgColor":"#2EF598","integrationType":"analytics"},{"type":"tinyfish","slug":"tinyfish","name":"TinyFish","authType":"api-key","bgColor":"#FFFFFF","integrationType":"ai"},{"type":"trello","slug":"trello","name":"Trello","authType":"oauth","oauthServiceId":"trello","bgColor":"#0052CC","integrationType":"productivity"},{"type":"trigger_dev","slug":"trigger-dev","name":"Trigger.dev","authType":"api-key","bgColor":"#000000","integrationType":"devops"},{"type":"twilio_sms","slug":"twilio-sms","name":"Twilio SMS","authType":"api-key","bgColor":"#F22F46","integrationType":"communication"},{"type":"twilio_voice","slug":"twilio-voice","name":"Twilio Voice","authType":"api-key","bgColor":"#F22F46","integrationType":"communication"},{"type":"typeform","slug":"typeform","name":"Typeform","authType":"api-key","bgColor":"#262627","integrationType":"documents"},{"type":"upstash","slug":"upstash","name":"Upstash","authType":"api-key","bgColor":"#181C1E","integrationType":"databases"},{"type":"uptimerobot","slug":"uptimerobot","name":"UptimeRobot","authType":"api-key","bgColor":"#111921","integrationType":"observability"},{"type":"vanta","slug":"vanta","name":"Vanta","authType":"api-key","bgColor":"#F8F4F3","integrationType":"security"},{"type":"vercel","slug":"vercel","name":"Vercel","authType":"api-key","bgColor":"#171717","integrationType":"devops"},{"type":"wealthbox","slug":"wealthbox","name":"Wealthbox","authType":"oauth","oauthServiceId":"wealthbox","bgColor":"#FFFFFF","integrationType":"sales"},{"type":"webflow","slug":"webflow","name":"Webflow","authType":"oauth","oauthServiceId":"webflow","bgColor":"#FFFFFF","integrationType":"marketing"},{"type":"whatsapp","slug":"whatsapp","name":"WhatsApp","authType":"api-key","bgColor":"#25D366","integrationType":"communication"},{"type":"wikipedia","slug":"wikipedia","name":"Wikipedia","authType":"none","bgColor":"#000000","integrationType":"search"},{"type":"windchill","slug":"windchill","name":"Windchill","authType":"api-key","bgColor":"#FFFFFF","integrationType":"documents"},{"type":"wiza","slug":"wiza","name":"Wiza","authType":"api-key","bgColor":"#9284BC","integrationType":"sales"},{"type":"wordpress","slug":"wordpress","name":"WordPress","authType":"oauth","oauthServiceId":"wordpress","bgColor":"#21759B","integrationType":"marketing"},{"type":"workday","slug":"workday","name":"Workday","authType":"none","bgColor":"#F5F0EB","integrationType":"hr"},{"type":"x","slug":"x","name":"X","authType":"oauth","oauthServiceId":"x","bgColor":"#000000","integrationType":"communication"},{"type":"youcom","slug":"you-com","name":"You.com","authType":"api-key","bgColor":"#FFFFFF","integrationType":"search"},{"type":"youtube","slug":"youtube","name":"YouTube","authType":"api-key","bgColor":"#FF0000","integrationType":"communication"},{"type":"zendesk","slug":"zendesk","name":"Zendesk","authType":"api-key","bgColor":"#03363D","integrationType":"support"},{"type":"zep","slug":"zep","name":"Zep","authType":"api-key","bgColor":"#E8E8E8","integrationType":"ai"},{"type":"zerobounce","slug":"zerobounce","name":"ZeroBounce","authType":"api-key","bgColor":"#FFFFFF","integrationType":"sales"},{"type":"zoho_desk","slug":"zoho-desk","name":"Zoho Desk","authType":"oauth","oauthServiceId":"zoho-desk","bgColor":"#FFFFFF","integrationType":"support"},{"type":"zoom","slug":"zoom","name":"Zoom","authType":"oauth","oauthServiceId":"zoom","bgColor":"#2D8CFF","integrationType":"communication"},{"type":"zoominfo","slug":"zoominfo","name":"ZoomInfo","authType":"api-key","bgColor":"#EA1B15","integrationType":"sales"}]' ) diff --git a/packages/deployment-config/src/integrations.json b/packages/deployment-config/src/integrations.json index 4170c2b0681..0910cf25b04 100644 --- a/packages/deployment-config/src/integrations.json +++ b/packages/deployment-config/src/integrations.json @@ -1,5 +1,5 @@ { - "updatedAt": "2026-10-02", + "updatedAt": "2026-10-03", "integrations": [ { "type": "onepassword", @@ -4363,6 +4363,7 @@ "triggers": [], "triggerCount": 0, "authType": "api-key", + "serviceAccountServiceId": "claude-platform", "category": "tools", "integrationType": "ai", "tags": ["agentic", "llm"] @@ -5748,6 +5749,7 @@ "triggers": [], "triggerCount": 0, "authType": "api-key", + "serviceAccountServiceId": "coda", "category": "tools", "integrationType": "documents", "tags": ["note-taking", "knowledge-base", "spreadsheet", "project-management", "automation"] @@ -11553,6 +11555,7 @@ "triggers": [], "triggerCount": 0, "authType": "api-key", + "serviceAccountServiceId": "harmonic", "category": "tools", "integrationType": "sales", "tags": ["enrichment", "automation", "agentic"] @@ -17088,6 +17091,7 @@ "triggers": [], "triggerCount": 0, "authType": "api-key", + "serviceAccountServiceId": "netsuite", "category": "tools", "integrationType": "commerce", "tags": ["automation", "data-analytics", "payments"] @@ -23677,6 +23681,7 @@ "triggers": [], "triggerCount": 0, "authType": "api-key", + "serviceAccountServiceId": "snowflake", "category": "tools", "integrationType": "databases", "tags": ["data-warehouse", "data-analytics", "cloud"] diff --git a/packages/deployment-config/src/service-account-providers.generated.ts b/packages/deployment-config/src/service-account-providers.generated.ts index 5084ee16254..c79611b2038 100644 --- a/packages/deployment-config/src/service-account-providers.generated.ts +++ b/packages/deployment-config/src/service-account-providers.generated.ts @@ -8,7 +8,9 @@ export const SERVICE_ACCOUNT_PROVIDER_BY_OAUTH_SERVICE_ID = { attio: 'attio-service-account', box: 'box-service-account', calcom: 'calcom-service-account', + 'claude-platform': 'claude-platform-service-account', clickup: 'clickup-service-account', + coda: 'coda-service-account', confluence: 'atlassian-service-account', gmail: 'google-service-account', 'google-bigquery': 'google-service-account', @@ -22,15 +24,18 @@ export const SERVICE_ACCOUNT_PROVIDER_BY_OAUTH_SERVICE_ID = { 'google-sheets': 'google-service-account', 'google-tasks': 'google-service-account', 'google-vault': 'google-service-account', + harmonic: 'harmonic-service-account', hubspot: 'hubspot-service-account', jira: 'atlassian-service-account', linear: 'linear-service-account', monday: 'monday-service-account', + netsuite: 'netsuite-service-account', notion: 'notion-service-account', pipedrive: 'pipedrive-service-account', salesforce: 'salesforce-service-account', shopify: 'shopify-service-account', slack: 'slack-custom-bot', + snowflake: 'snowflake-service-account', trello: 'trello-service-account', wealthbox: 'wealthbox-service-account', webflow: 'webflow-service-account', diff --git a/scripts/check-integration-catalog.ts b/scripts/check-integration-catalog.ts index 546f85eba7b..eb7417e5cd9 100644 --- a/scripts/check-integration-catalog.ts +++ b/scripts/check-integration-catalog.ts @@ -8,6 +8,7 @@ import { stripVersionSuffix } from '@sim/utils/string' */ import { BLOCK_REGISTRY } from '../apps/sim/blocks/registry-maps' import { AuthMode, type BlockConfig } from '../apps/sim/blocks/types' +import { getServiceConfigByServiceId } from '../apps/sim/lib/oauth/utils' import { INTEGRATION_METADATA } from '../packages/deployment-config/src/integration-metadata' import integrationsJson from '../packages/deployment-config/src/integrations.json' import { DOCS_ORIGIN, DOCS_OUTPUT_PATH, defaultIntegrationDocsUrl } from './generate-docs' @@ -23,6 +24,7 @@ interface CatalogEntry { bgColor: string authType: CatalogAuthType oauthServiceId?: string + serviceAccountServiceId?: string } function resolveAuthType(block: BlockConfig): CatalogAuthType { @@ -57,7 +59,14 @@ function expectedEntry(block: BlockConfig): CatalogEntry { throw new Error(`Integration block "${block.type}" is missing integrationType`) } const authType = resolveAuthType(block) - const oauthServiceId = authType === 'oauth' ? resolveOAuthServiceId(block) : undefined + const credentialServiceId = resolveOAuthServiceId(block) + const oauthServiceId = authType === 'oauth' ? credentialServiceId : undefined + const serviceAccountServiceId = + authType !== 'oauth' && + credentialServiceId && + getServiceConfigByServiceId(credentialServiceId)?.serviceAccountProviderId + ? credentialServiceId + : undefined if (authType === 'oauth' && !oauthServiceId) { throw new Error(`OAuth integration block "${block.type}" is missing an OAuth service ID`) } @@ -73,6 +82,7 @@ function expectedEntry(block: BlockConfig): CatalogEntry { bgColor: block.bgColor, authType, ...(oauthServiceId ? { oauthServiceId } : {}), + ...(serviceAccountServiceId ? { serviceAccountServiceId } : {}), } } @@ -134,12 +144,22 @@ function verifyIntegrationCatalog(): void { const actual = integrationsJson.integrations as readonly CatalogEntry[] const expectedMetadata = actual.map( - ({ type, slug, name, authType, oauthServiceId, bgColor, integrationType }) => ({ + ({ + type, + slug, + name, + authType, + oauthServiceId, + serviceAccountServiceId, + bgColor, + integrationType, + }) => ({ type, slug, name, authType, ...(oauthServiceId ? { oauthServiceId } : {}), + ...(serviceAccountServiceId ? { serviceAccountServiceId } : {}), bgColor, integrationType, }) @@ -178,6 +198,7 @@ function verifyIntegrationCatalog(): void { 'bgColor', 'authType', 'oauthServiceId', + 'serviceAccountServiceId', ] as const) { if (generated[field] !== entry[field]) { issues.push(`"${type}" has stale ${field}`) diff --git a/scripts/generate-deployment-config.ts b/scripts/generate-deployment-config.ts index 12e01aeca9b..e6290ca5d32 100644 --- a/scripts/generate-deployment-config.ts +++ b/scripts/generate-deployment-config.ts @@ -47,14 +47,25 @@ function buildOAuthDeploymentFacts(): CanonicalOAuthDeploymentFacts { } const catalogServiceIds = new Set() + const oauthServiceIds = new Set() for (const integration of INTEGRATION_METADATA) { - if (integration.authType !== 'oauth') continue - if (!integration.oauthServiceId) { + if (integration.authType === 'oauth' && !integration.oauthServiceId) { throw new Error( 'Generated integration catalog contains an OAuth entry without oauthServiceId' ) } - catalogServiceIds.add(integration.oauthServiceId) + if (integration.authType === 'oauth' && integration.oauthServiceId) { + catalogServiceIds.add(integration.oauthServiceId) + oauthServiceIds.add(integration.oauthServiceId) + } + if (integration.serviceAccountServiceId) { + if (!canonicalServices.get(integration.serviceAccountServiceId)?.serviceAccountProviderId) { + throw new Error( + `Integration catalog references a service without a service-account provider: ${integration.serviceAccountServiceId}` + ) + } + catalogServiceIds.add(integration.serviceAccountServiceId) + } } const providers = new Map() @@ -64,7 +75,9 @@ function buildOAuthDeploymentFacts(): CanonicalOAuthDeploymentFacts { throw new Error(`Integration catalog references unknown OAuth service: ${serviceId}`) } const service = canonicalServices.get(serviceId) - if (service?.credentialConfigured) credentialConfiguredOAuthServiceIds.push(serviceId) + if (oauthServiceIds.has(serviceId) && service?.credentialConfigured) { + credentialConfiguredOAuthServiceIds.push(serviceId) + } const providerId = service?.serviceAccountProviderId if (providerId) providers.set(serviceId, providerId) } diff --git a/scripts/generate-docs.test.ts b/scripts/generate-docs.test.ts index 933d4c4c5ec..a4c74cfbc1b 100644 --- a/scripts/generate-docs.test.ts +++ b/scripts/generate-docs.test.ts @@ -9,6 +9,7 @@ import { extractAllBlockConfigs, extractBlockSuppliedParamIds, extractInheritedBlockCategory, + extractIntegrationCredentialServices, extractToolInfo, extractUserSettableParamIds, generateIconMappings, @@ -17,6 +18,54 @@ import { parsePropertiesContent, } from './generate-docs' +describe('integration credential relationships', () => { + const serviceAccountServiceIds = new Set([ + 'netsuite', + 'snowflake', + 'harmonic', + 'claude-platform', + 'jira', + ]) + it.each([ + { serviceId: 'netsuite', blockFile: 'netsuite' }, + { serviceId: 'snowflake', blockFile: 'snowflake' }, + { serviceId: 'harmonic', blockFile: 'harmonic' }, + { serviceId: 'claude-platform', blockFile: 'managed_agent' }, + ])( + 'projects $serviceId stored credentials without changing its auth marker', + ({ serviceId, blockFile }) => { + const source = fs.readFileSync( + path.join(__dirname, `../apps/sim/blocks/blocks/${blockFile}.ts`), + 'utf8' + ) + expect(extractIntegrationCredentialServices(source, serviceAccountServiceIds)).toEqual({ + serviceAccountServiceId: serviceId, + }) + } + ) + + it('preserves the OAuth relationship and ignores unrelated selector services', () => { + expect( + extractIntegrationCredentialServices( + `{ authMode: AuthMode.OAuth, subBlocks: [{ type: 'oauth-input', serviceId: 'jira' }] }`, + serviceAccountServiceIds + ) + ).toEqual({ oauthServiceId: 'jira' }) + expect( + extractIntegrationCredentialServices( + `{ authMode: AuthMode.ApiKey, subBlocks: [{ type: 'selector-input', serviceId: 'netsuite' }] }`, + serviceAccountServiceIds + ) + ).toEqual({}) + expect( + extractIntegrationCredentialServices( + `{ authMode: AuthMode.ApiKey, subBlocks: [{ type: 'oauth-input', serviceId: 'x' }] }`, + serviceAccountServiceIds + ) + ).toEqual({}) + }) +}) + describe('documentation editor icon metadata', () => { it('keeps core icons and inherited categories out of the integration catalog', async () => { const { docs, visible, coreBlockTypes } = await generateIconMappings() diff --git a/scripts/generate-docs.ts b/scripts/generate-docs.ts index 77e0c0b6d71..e56a093350c 100755 --- a/scripts/generate-docs.ts +++ b/scripts/generate-docs.ts @@ -491,6 +491,7 @@ interface IntegrationEntry { triggerCount: number authType: 'oauth' | 'api-key' | 'none' oauthServiceId?: string + serviceAccountServiceId?: string category: BlockCategory integrationType: IntegrationType tags?: string[] @@ -1773,6 +1774,20 @@ function isEnvironmentBoundCredential(blockContent: string): boolean { return subBlock ? /dependsOn\s*:\s*\[[^\]]*['"]environmentUrl['"]/.test(subBlock) : false } +/** Keeps stored service-account ownership independent of the catalog's OAuth marker. */ +export function extractIntegrationCredentialServices( + blockContent: string, + serviceAccountServiceIds: ReadonlySet +): { + oauthServiceId?: string + serviceAccountServiceId?: string +} { + const serviceId = extractOAuthServiceId(blockContent) + if (!serviceId) return {} + if (extractAuthType(blockContent) === 'oauth') return { oauthServiceId: serviceId } + return serviceAccountServiceIds.has(serviceId) ? { serviceAccountServiceId: serviceId } : {} +} + /** * Extract the list of trigger IDs from the block's `triggers.available` array. * Handles blocks that declare `triggers: { enabled: true, available: [...] }`. @@ -1944,6 +1959,12 @@ ${mappingEntries} * Applies the same visibility filters as the docs generation pipeline. */ async function writeIntegrationsJson(iconMapping: Record): Promise { + const { getAllOAuthServices } = await import('../apps/sim/lib/oauth/utils') + const serviceAccountServiceIds = new Set( + getAllOAuthServices() + .filter((service) => service.serviceAccountProviderId) + .map((service) => service.serviceId) + ) try { if (!fs.existsSync(INTEGRATIONS_DATA_PATH)) { fs.mkdirSync(INTEGRATIONS_DATA_PATH, { recursive: true }) @@ -2069,7 +2090,11 @@ async function writeIntegrationsJson(iconMapping: Record): Prom .replace(/^-|-$/g, '') const authType = extractAuthType(fileContent) - const oauthServiceId = authType === 'oauth' ? extractOAuthServiceId(fileContent) : undefined + const credentialServices = extractIntegrationCredentialServices( + fileContent, + serviceAccountServiceIds + ) + const { oauthServiceId } = credentialServices // 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. @@ -2094,7 +2119,7 @@ async function writeIntegrationsJson(iconMapping: Record): Prom triggers, triggerCount: triggers.length, authType, - ...(oauthServiceId ? { oauthServiceId } : {}), + ...credentialServices, category: 'tools', integrationType, ...(config.tags ? { tags: config.tags } : {}), @@ -2107,12 +2132,22 @@ async function writeIntegrationsJson(iconMapping: Record): Prom const metadataPath = path.join(INTEGRATIONS_CATALOG_PATH, 'integration-metadata.ts') const metadata = integrations.map( - ({ type, slug, name, authType, oauthServiceId, bgColor, integrationType }) => ({ + ({ + type, + slug, + name, + authType, + oauthServiceId, + serviceAccountServiceId, + bgColor, + integrationType, + }) => ({ type, slug, name, authType, ...(oauthServiceId ? { oauthServiceId } : {}), + ...(serviceAccountServiceId ? { serviceAccountServiceId } : {}), bgColor, integrationType, }) @@ -2130,6 +2165,7 @@ export interface IntegrationMetadata { name: string authType: 'oauth' | 'api-key' | 'none' oauthServiceId?: string + serviceAccountServiceId?: string bgColor: string integrationType: string }