| |
| |
| |
| |
| |
| |
| |
| import type { |
| N8nWorkflow, |
| CredentialAnalysis, |
| RequiredCredential, |
| AvailableCredential, |
| MissingCredential, |
| } from '../types/workflow'; |
| import { N8N_NODE_REGISTRY } from '../knowledge/nodeRegistry'; |
|
|
| |
| const NODE_CREDENTIAL_MAP: Record<string, string | undefined> = { |
| 'n8n-nodes-base.telegram': 'telegramApi', |
| 'n8n-nodes-base.telegramTrigger': 'telegramApi', |
| 'n8n-nodes-base.openAi': 'openAiApi', |
| '@n8n/n8n-nodes-langchain.openAi': 'openAiApi', |
| '@n8n/n8n-nodes-langchain.agent': undefined, |
| '@n8n/n8n-nodes-langchain.lmChatAnthropic': 'anthropicApi', |
| '@n8n/n8n-nodes-langchain.memoryBufferWindow': undefined, |
| '@n8n/n8n-nodes-langchain.toolCode': undefined, |
| 'n8n-nodes-base.gmail': 'googleOAuth2Api', |
| 'n8n-nodes-base.emailReadImap': 'imap', |
| 'n8n-nodes-base.googleSheets': 'googleSheetsOAuth2Api', |
| 'n8n-nodes-base.slack': 'slackApi', |
| 'n8n-nodes-base.slackTrigger': 'slackOAuth2Api', |
| 'n8n-nodes-base.airtable': 'airtableTokenApi', |
| 'n8n-nodes-base.notion': 'notionApi', |
| 'n8n-nodes-base.notionTrigger': 'notionApi', |
| 'n8n-nodes-base.github': 'githubApi', |
| 'n8n-nodes-base.githubTrigger': 'githubApi', |
| 'n8n-nodes-base.stripe': 'stripeApi', |
| 'n8n-nodes-base.hubspot': 'hubspotApi', |
| 'n8n-nodes-base.postgres': 'postgres', |
| 'n8n-nodes-base.mysql': 'mySql', |
| 'n8n-nodes-base.mongodb': 'mongoDb', |
| 'n8n-nodes-base.redis': 'redis', |
| |
| 'n8n-nodes-base.httpRequest': undefined, |
| 'n8n-nodes-base.webhook': undefined, |
| 'n8n-nodes-base.scheduleTrigger': undefined, |
| 'n8n-nodes-base.manualTrigger': undefined, |
| 'n8n-nodes-base.code': undefined, |
| 'n8n-nodes-base.set': undefined, |
| 'n8n-nodes-base.if': undefined, |
| 'n8n-nodes-base.switch': undefined, |
| 'n8n-nodes-base.merge': undefined, |
| 'n8n-nodes-base.splitInBatches': undefined, |
| 'n8n-nodes-base.itemLists': undefined, |
| 'n8n-nodes-base.filter': undefined, |
| 'n8n-nodes-base.noOp': undefined, |
| 'n8n-nodes-base.stopAndError': undefined, |
| 'n8n-nodes-base.wait': undefined, |
| 'n8n-nodes-base.respondToWebhook': undefined, |
| 'n8n-nodes-base.stickyNote': undefined, |
| }; |
|
|
| const CREDENTIAL_SETUP_INSTRUCTIONS: Record<string, string> = { |
| telegramApi: 'Create a Telegram Bot via @BotFather, copy the bot token, then add "Telegram API" credential in n8n Settings β Credentials.', |
| openAiApi: 'Generate an API key at https://platform.openai.com/api-keys, then add "OpenAI" credential in n8n.', |
| anthropicApi: 'Get your API key at https://console.anthropic.com/, then add "Anthropic" credential in n8n.', |
| googleOAuth2Api: 'Set up a Google Cloud project, enable Gmail API, create OAuth2 credentials, then add "Google OAuth2 API" in n8n.', |
| googleSheetsOAuth2Api: 'Set up Google Cloud project, enable Sheets API, create OAuth2 credentials, then add "Google Sheets OAuth2 API" in n8n.', |
| slackApi: 'Create a Slack App at https://api.slack.com/apps with required scopes, then add "Slack API" credential in n8n.', |
| slackOAuth2Api: 'Create a Slack App with OAuth2, then add "Slack OAuth2 API" credential in n8n.', |
| airtableTokenApi: 'Generate a Personal Access Token at https://airtable.com/account, then add "Airtable Token API" in n8n.', |
| notionApi: 'Create an integration at https://www.notion.so/my-integrations, then add "Notion API" credential in n8n.', |
| githubApi: 'Generate a Personal Access Token at https://github.com/settings/tokens, then add "GitHub API" in n8n.', |
| stripeApi: 'Get your API key from https://dashboard.stripe.com/apikeys, then add "Stripe API" in n8n.', |
| hubspotApi: 'Create a Private App in HubSpot settings, copy the access token, then add "HubSpot API" in n8n.', |
| postgres: 'Provide your PostgreSQL host, port, database, username, and password in "Postgres" credential in n8n.', |
| mySql: 'Provide your MySQL host, port, database, username, and password in "MySQL" credential in n8n.', |
| mongoDb: 'Provide your MongoDB connection string in "MongoDB" credential in n8n.', |
| redis: 'Provide your Redis host, port, and optional password in "Redis" credential in n8n.', |
| imap: 'Provide your IMAP server host, port, username, and password in "IMAP" credential in n8n.', |
| }; |
|
|
| export class CredentialIntelligence { |
| private n8nBaseUrl: string; |
| private n8nApiKey: string; |
|
|
| constructor(n8nBaseUrl: string, n8nApiKey: string) { |
| this.n8nBaseUrl = n8nBaseUrl; |
| this.n8nApiKey = n8nApiKey; |
| } |
|
|
| async analyse(jobId: string, workflow: N8nWorkflow): Promise<CredentialAnalysis> { |
| |
| const required: RequiredCredential[] = []; |
| const requiredTypes = new Set<string>(); |
|
|
| workflow.nodes.forEach((node) => { |
| |
| const registryDef = N8N_NODE_REGISTRY[node.type]; |
| const credType = registryDef?.credentialType ?? NODE_CREDENTIAL_MAP[node.type]; |
|
|
| if (credType !== undefined && credType !== '') { |
| if (!requiredTypes.has(credType)) { |
| required.push({ |
| nodeId: node.id, |
| nodeName: node.name, |
| credentialType: credType, |
| credentialDisplayName: registryDef?.credentialDisplayName, |
| required: true, |
| }); |
| requiredTypes.add(credType); |
| } |
| } |
|
|
| |
| if (node.credentials) { |
| Object.keys(node.credentials).forEach((cType) => { |
| if (!requiredTypes.has(cType)) { |
| required.push({ |
| nodeId: node.id, |
| nodeName: node.name, |
| credentialType: cType, |
| required: true, |
| }); |
| requiredTypes.add(cType); |
| } |
| }); |
| } |
| }); |
|
|
| |
| let available: AvailableCredential[] = []; |
| let n8nReachable = false; |
| try { |
| const resp = await fetch(`${this.n8nBaseUrl}/api/v1/credentials`, { |
| headers: { 'X-N8N-API-KEY': this.n8nApiKey }, |
| signal: AbortSignal.timeout(8000), |
| }); |
| if (resp.ok) { |
| const data = await resp.json() as { data: AvailableCredential[] }; |
| available = data.data ?? []; |
| n8nReachable = true; |
| } |
| } catch { |
| |
| } |
|
|
| const availableTypes = new Set(available.map((c) => c.type)); |
|
|
| |
| const missingTypeMap = new Map<string, string[]>(); |
| required.forEach((req) => { |
| |
| if (n8nReachable && !availableTypes.has(req.credentialType)) { |
| const existing = missingTypeMap.get(req.credentialType) ?? []; |
| if (!existing.includes(req.nodeName)) existing.push(req.nodeName); |
| missingTypeMap.set(req.credentialType, existing); |
| } |
| }); |
|
|
| const missing: MissingCredential[] = [...missingTypeMap.entries()].map(([credType, nodeNames]) => ({ |
| credentialType: credType, |
| requiredByNodes: nodeNames, |
| setupInstructions: CREDENTIAL_SETUP_INSTRUCTIONS[credType] ?? `Set up "${credType}" credential in n8n Settings β Credentials.`, |
| })); |
|
|
| return { |
| jobId, |
| allCredentialsPresent: missing.length === 0, |
| n8nReachable, |
| required, |
| available, |
| missing, |
| analysedAt: new Date().toISOString(), |
| }; |
| } |
| } |
|
|