/** * Credential Intelligence System — UPGRADED * Provider-agnostic, uses Node Registry for credential mapping * Inspects workflow to identify required credentials * Scans existing n8n credentials and maps types * Detects missing credentials and blocks activation if needed */ import type { N8nWorkflow, CredentialAnalysis, RequiredCredential, AvailableCredential, MissingCredential, } from '../types/workflow'; import { N8N_NODE_REGISTRY } from '../knowledge/nodeRegistry'; // Extended node → credential map (derived from registry + additional mappings) const NODE_CREDENTIAL_MAP: Record = { '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, // depends on sub-node '@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', // No credential needed '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 = { 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 { // ─── 1. Identify required credentials from workflow nodes ────────────── const required: RequiredCredential[] = []; const requiredTypes = new Set(); workflow.nodes.forEach((node) => { // Check registry-based credential (more accurate) 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); } } // Also check node.credentials field (explicitly set by compiler) 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); } }); } }); // ─── 2. Fetch available credentials from n8n ─────────────────────────── 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 { // n8n not accessible — non-fatal, report as warning } const availableTypes = new Set(available.map((c) => c.type)); // ─── 3. Identify missing credentials ────────────────────────────────── const missingTypeMap = new Map(); required.forEach((req) => { // Only report as missing if n8n is reachable and credential is not there 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(), }; } }