UNPKG

n8n

Version:

n8n Workflow Automation Tool

982 lines 102 kB
"use strict"; var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { if (k2 === undefined) k2 = k; var desc = Object.getOwnPropertyDescriptor(m, k); if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { desc = { enumerable: true, get: function() { return m[k]; } }; } Object.defineProperty(o, k2, desc); }) : (function(o, m, k, k2) { if (k2 === undefined) k2 = k; o[k2] = m[k]; })); var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { Object.defineProperty(o, "default", { enumerable: true, value: v }); }) : function(o, v) { o["default"] = v; }); var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) { var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d; if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc); else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r; return c > 3 && r && Object.defineProperty(target, key, r), r; }; var __importStar = (this && this.__importStar) || (function () { var ownKeys = function(o) { ownKeys = Object.getOwnPropertyNames || function (o) { var ar = []; for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k; return ar; }; return ownKeys(o); }; return function (mod) { if (mod && mod.__esModule) return mod; var result = {}; if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]); __setModuleDefault(result, mod); return result; }; })(); var __metadata = (this && this.__metadata) || function (k, v) { if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v); }; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.InstanceAiAdapterService = void 0; exports.resolveDataTableByIdOrName = resolveDataTableByIdOrName; exports.truncateResultData = truncateResultData; exports.extractExecutionResult = extractExecutionResult; exports.formatExecutionError = formatExecutionError; exports.truncateNodeOutput = truncateNodeOutput; exports.extractNodeOutput = extractNodeOutput; exports.extractExecutionDebugInfo = extractExecutionDebugInfo; const node_crypto_1 = require("node:crypto"); const promises_1 = require("node:fs/promises"); const node_path_1 = __importDefault(require("node:path")); const instance_ai_1 = require("@n8n/instance-ai"); const config_1 = require("@n8n/config"); const constants_1 = require("@n8n/constants"); const instance_ai_settings_service_1 = require("./instance-ai-settings.service"); const node_definition_resolver_1 = require("./node-definition-resolver"); const web_research_1 = require("./web-research"); const db_1 = require("@n8n/db"); const backend_common_1 = require("@n8n/backend-common"); const di_1 = require("@n8n/di"); const permissions_1 = require("@n8n/permissions"); const typeorm_1 = require("@n8n/typeorm"); const n8n_workflow_1 = require("n8n-workflow"); const n8n_core_1 = require("n8n-core"); const active_executions_1 = require("../../active-executions"); const credentials_finder_service_1 = require("../../credentials/credentials-finder.service"); const credentials_service_1 = require("../../credentials/credentials.service"); const event_service_1 = require("../../events/event.service"); const execution_persistence_1 = require("../../executions/execution-persistence"); const license_1 = require("../../license"); const load_nodes_and_credentials_1 = require("../../load-nodes-and-credentials"); const node_types_1 = require("../../node-types"); const data_table_repository_1 = require("../../modules/data-table/data-table.repository"); const data_table_service_1 = require("../../modules/data-table/data-table.service"); const source_control_preferences_service_ee_1 = require("../../modules/source-control.ee/source-control-preferences.service.ee"); const check_access_1 = require("../../permissions.ee/check-access"); const dynamic_node_parameters_service_1 = require("../../services/dynamic-node-parameters.service"); const folder_service_1 = require("../../services/folder.service"); const project_service_ee_1 = require("../../services/project.service.ee"); const role_service_1 = require("../../services/role.service"); const ssrf_protection_service_1 = require("../../services/ssrf/ssrf-protection.service"); const tag_service_1 = require("../../services/tag.service"); const workflow_finder_service_1 = require("../../workflows/workflow-finder.service"); const workflow_history_service_1 = require("../../workflows/workflow-history/workflow-history.service"); const workflow_service_1 = require("../../workflows/workflow.service"); const workflow_service_ee_1 = require("../../workflows/workflow.service.ee"); const telemetry_1 = require("../../telemetry"); const workflow_runner_1 = require("../../workflow-runner"); const workflow_execute_additional_data_1 = require("../../workflow-execute-additional-data"); function resolveDisplayedDefaults(nodeProperties, parameters, nodeType, typeVersion, desc) { const stubNode = { id: '', name: '', type: nodeType, typeVersion, parameters: parameters, position: [0, 0], }; const resolved = n8n_workflow_1.NodeHelpers.getNodeParameters(nodeProperties, parameters, true, false, stubNode, desc); return resolved ?? parameters; } let InstanceAiAdapterService = class InstanceAiAdapterService { async getNodesFromCache() { if (this.nodesCache && Date.now() < this.nodesCache.expiresAt) { return await this.nodesCache.promise; } const filePath = node_path_1.default.join(this.instanceSettings.staticCacheDir, 'types/nodes.json'); const promise = (0, promises_1.readFile)(filePath, 'utf-8').then((json) => (0, n8n_workflow_1.jsonParse)(json)); this.nodesCache = { promise, expiresAt: Date.now() + this.NODES_CACHE_TTL_MS }; promise.catch(() => { this.nodesCache = null; }); return await promise; } constructor(logger, globalConfig, workflowService, workflowFinderService, workflowRepository, sharedWorkflowRepository, projectRepository, executionRepository, credentialsService, credentialsFinderService, activeExecutions, workflowRunner, loadNodesAndCredentials, nodeTypes, instanceSettings, dataTableService, dataTableRepository, dynamicNodeParametersService, folderService, projectService, tagService, sourceControlPreferencesService, settingsService, workflowHistoryService, enterpriseWorkflowService, license, executionPersistence, eventService, roleService, telemetry, aiBuilderTemporaryWorkflowRepository, ssrfProtectionService) { this.workflowService = workflowService; this.workflowFinderService = workflowFinderService; this.workflowRepository = workflowRepository; this.sharedWorkflowRepository = sharedWorkflowRepository; this.projectRepository = projectRepository; this.executionRepository = executionRepository; this.credentialsService = credentialsService; this.credentialsFinderService = credentialsFinderService; this.activeExecutions = activeExecutions; this.workflowRunner = workflowRunner; this.loadNodesAndCredentials = loadNodesAndCredentials; this.nodeTypes = nodeTypes; this.instanceSettings = instanceSettings; this.dataTableService = dataTableService; this.dataTableRepository = dataTableRepository; this.dynamicNodeParametersService = dynamicNodeParametersService; this.folderService = folderService; this.projectService = projectService; this.tagService = tagService; this.sourceControlPreferencesService = sourceControlPreferencesService; this.settingsService = settingsService; this.workflowHistoryService = workflowHistoryService; this.enterpriseWorkflowService = enterpriseWorkflowService; this.license = license; this.executionPersistence = executionPersistence; this.eventService = eventService; this.roleService = roleService; this.telemetry = telemetry; this.aiBuilderTemporaryWorkflowRepository = aiBuilderTemporaryWorkflowRepository; this.ssrfProtectionService = ssrfProtectionService; this.nodesCache = null; this.NODES_CACHE_TTL_MS = 5 * 60 * 1000; this.webResearchCache = new web_research_1.LRUCache({ maxEntries: 100, ttlMs: 15 * 60 * 1000, }); this.searchCache = new web_research_1.LRUCache({ maxEntries: 100, ttlMs: 15 * 60 * 1000, }); this.logger = logger.scoped('instance-ai'); this.allowSendingParameterValues = globalConfig.ai.allowSendingParameterValues; } createContext(user, options) { const { searchProxyConfig, pushRef, threadId } = options ?? {}; return { userId: user.id, workflowService: this.createWorkflowAdapter(user, threadId), executionService: this.createExecutionAdapter(user, pushRef, threadId), credentialService: this.createCredentialAdapter(user), nodeService: this.createNodeAdapter(user), dataTableService: this.createDataTableAdapter(user), webResearchService: this.createWebResearchAdapter(user, searchProxyConfig), workspaceService: this.createWorkspaceAdapter(user), licenseHints: this.buildLicenseHints(), logger: this.logger, nodeTypesProvider: this.nodeTypes, }; } buildLicenseHints() { const hints = []; if (!this.license.isLicensed('feat:namedVersions')) { hints.push('**Named workflow versions** — naming and describing workflow versions (update-workflow-version) is available on the Pro plan and above.'); } if (!this.license.isLicensed('feat:folders')) { hints.push('**Folders** — organizing workflows into folders (list-folders, create-folder, delete-folder, move-workflow-to-folder) is available on registered Community Edition or paid plans.'); } return hints; } assertInstanceNotReadOnly(resourceType) { if (this.sourceControlPreferencesService.getPreferences().branchReadOnly) { throw new Error(`Cannot modify ${resourceType} on a protected instance. This instance is in read-only mode.`); } } createProjectScopeHelpers(user) { const { projectRepository } = this; let personalProjectIdPromise = null; const getPersonalProjectId = async () => { personalProjectIdPromise ??= projectRepository .getPersonalProjectForUserOrFail(user.id) .then((p) => p.id); return await personalProjectIdPromise; }; const assertProjectScope = async (scopes, projectId) => { const allowed = await (0, check_access_1.userHasScopes)(user, scopes, false, { projectId }); if (!allowed) { throw new Error('User does not have the required permissions in this project'); } }; const resolveProjectId = async (scopes, providedProjectId) => { const projectId = providedProjectId ?? (await getPersonalProjectId()); await assertProjectScope(scopes, projectId); return projectId; }; return { getPersonalProjectId, assertProjectScope, resolveProjectId }; } createWorkflowAdapter(user, threadId) { const { workflowService, workflowFinderService, workflowRepository, sharedWorkflowRepository, aiBuilderTemporaryWorkflowRepository, workflowHistoryService, enterpriseWorkflowService, license, allowSendingParameterValues, telemetry, } = this; const assertNotReadOnly = () => this.assertInstanceNotReadOnly('workflows'); const { resolveProjectId } = this.createProjectScopeHelpers(user); const redactParameters = !allowSendingParameterValues; return { async list(options) { const filter = { ...(options?.status === 'all' ? {} : { isArchived: options?.status === 'archived' }), ...(options?.query ? { query: options.query } : {}), }; const { workflows } = await workflowService.getMany(user, { take: options?.limit ?? 50, filter, }); return workflows .filter((wf) => 'versionId' in wf) .map((wf) => ({ id: wf.id, name: wf.name, versionId: wf.versionId, activeVersionId: wf.activeVersionId ?? null, isArchived: wf.isArchived, createdAt: wf.createdAt.toISOString(), updatedAt: wf.updatedAt.toISOString(), })); }, async get(workflowId) { const workflow = await workflowFinderService.findWorkflowForUser(workflowId, user, [ 'workflow:read', ]); if (!workflow) { throw new Error(`Workflow ${workflowId} not found or not accessible`); } return toWorkflowDetail(workflow, { redactParameters }); }, async archive(workflowId) { assertNotReadOnly(); const result = await workflowService.archive(user, workflowId, { skipArchived: true }); if (!result) { throw new Error(`Workflow ${workflowId} not found or not accessible`); } }, async unarchive(workflowId) { assertNotReadOnly(); const result = await workflowService.unarchive(user, workflowId); if (!result) { throw new Error(`Workflow ${workflowId} not found or not accessible`); } }, async clearAiTemporary(workflowId) { assertNotReadOnly(); const workflow = await workflowFinderService.findWorkflowForUser(workflowId, user, [ 'workflow:update', ]); if (!workflow) return; if (!(await aiBuilderTemporaryWorkflowRepository.existsForWorkflow(workflowId))) return; await aiBuilderTemporaryWorkflowRepository.unmark(workflowId); }, async archiveIfAiTemporary(workflowId) { assertNotReadOnly(); const workflow = await workflowFinderService.findWorkflowForUser(workflowId, user, [ 'workflow:update', ]); if (!workflow) return false; if (!(await aiBuilderTemporaryWorkflowRepository.existsForWorkflow(workflowId))) { return false; } if (workflow.isArchived) { await aiBuilderTemporaryWorkflowRepository.unmark(workflowId); return false; } await workflowService.archive(user, workflowId, { skipArchived: true }); await aiBuilderTemporaryWorkflowRepository.unmark(workflowId); return true; }, async publish(workflowId, options) { const wf = await workflowService.activateWorkflow(user, workflowId, { versionId: options?.versionId, name: options?.name, description: options?.description, source: 'n8n-ai', }); if (!wf.activeVersionId) { throw new Error(`Workflow ${workflowId} was not activated — no active version set`); } if (threadId) { telemetry.track('Builder published workflow', { thread_id: threadId, workflow_id: workflowId, executed_by: 'ai', }); } return { activeVersionId: wf.activeVersionId }; }, async unpublish(workflowId) { await workflowService.deactivateWorkflow(user, workflowId, { source: 'n8n-ai', }); }, async getAsWorkflowJSON(workflowId) { const wf = await workflowFinderService.findWorkflowForUser(workflowId, user, [ 'workflow:read', ]); if (!wf) throw new Error(`Workflow ${workflowId} not found or not accessible`); return toWorkflowJSON(wf, { redactParameters }); }, async createFromWorkflowJSON(json, options) { assertNotReadOnly(); const projectId = await resolveProjectId(['workflow:create'], options?.projectId); const settings = (json.settings ?? {}); if (settings.redactionPolicy !== undefined) { const canUpdateRedaction = await (0, check_access_1.userHasScopes)(user, ['workflow:updateRedactionSetting'], false, { projectId }); if (!canUpdateRedaction) { delete settings.redactionPolicy; } } const newWorkflow = workflowRepository.create({ name: json.name, nodes: [], connections: {}, settings, active: false, versionId: (0, node_crypto_1.randomUUID)(), }); const saved = await workflowRepository.manager.transaction(async (transactionManager) => { const workflow = await transactionManager.save(db_1.WorkflowEntity, newWorkflow); await sharedWorkflowRepository.makeOwner([workflow.id], projectId, transactionManager); if (options?.markAsAiTemporary) { if (!threadId) { throw new n8n_workflow_1.UnexpectedError('Cannot mark AI-builder temporary workflow without a thread ID'); } await aiBuilderTemporaryWorkflowRepository.mark(workflow.id, threadId, transactionManager); } return workflow; }); let updateData = workflowRepository.create({ name: json.name, nodes: json.nodes, connections: json.connections, settings, pinData: sdkPinDataToRuntime(json.pinData), }); if (license.isSharingEnabled()) { updateData = await enterpriseWorkflowService.preventTampering(updateData, saved.id, user); } const updated = await workflowService.update(user, updateData, saved.id, { source: 'n8n-ai', }); if (threadId) { telemetry.track('Builder created workflow', { thread_id: threadId, workflow_id: updated.id, }); } return toWorkflowDetail(updated, { redactParameters }); }, async updateFromWorkflowJSON(workflowId, json, _options) { assertNotReadOnly(); const settings = (json.settings ?? {}); if (settings.redactionPolicy !== undefined) { const canUpdateRedaction = await (0, check_access_1.userHasScopes)(user, ['workflow:updateRedactionSetting'], false, { workflowId }); if (!canUpdateRedaction) { delete settings.redactionPolicy; } } let updateData = workflowRepository.create({ name: json.name, nodes: json.nodes, connections: json.connections, settings, pinData: sdkPinDataToRuntime(json.pinData), }); if (license.isSharingEnabled()) { updateData = await enterpriseWorkflowService.preventTampering(updateData, workflowId, user); } const updated = await workflowService.update(user, updateData, workflowId, { source: 'n8n-ai', }); if (threadId) { telemetry.track('Builder modified workflow', { thread_id: threadId, workflow_id: workflowId, }); } return toWorkflowDetail(updated, { redactParameters }); }, async listVersions(workflowId, options) { const take = options?.limit ?? 20; const skip = options?.skip ?? 0; const versions = await workflowHistoryService.getList(user, workflowId, take, skip); const workflow = await workflowFinderService.findWorkflowForUser(workflowId, user, [ 'workflow:read', ]); const activeVersionId = workflow?.activeVersionId ?? null; const currentDraftVersionId = workflow?.versionId ?? null; return versions.map((v) => ({ versionId: v.versionId, name: v.name ?? null, description: v.description ?? null, authors: v.authors, createdAt: v.createdAt.toISOString(), autosaved: v.autosaved ?? false, isActive: v.versionId === activeVersionId, isCurrentDraft: v.versionId === currentDraftVersionId, })); }, async getVersion(workflowId, versionId) { const version = await workflowHistoryService.getVersion(user, workflowId, versionId); const workflow = await workflowFinderService.findWorkflowForUser(workflowId, user, [ 'workflow:read', ]); const activeVersionId = workflow?.activeVersionId ?? null; const currentDraftVersionId = workflow?.versionId ?? null; return { versionId: version.versionId, name: version.name ?? null, description: version.description ?? null, authors: version.authors, createdAt: version.createdAt.toISOString(), autosaved: version.autosaved ?? false, isActive: version.versionId === activeVersionId, isCurrentDraft: version.versionId === currentDraftVersionId, nodes: (version.nodes ?? []).map((n) => ({ name: n.name, type: n.type, parameters: redactParameters ? undefined : n.parameters, position: n.position, })), connections: version.connections, }; }, async restoreVersion(workflowId, versionId) { const version = await workflowHistoryService.getVersion(user, workflowId, versionId); const updateData = workflowRepository.create({ nodes: version.nodes, connections: version.connections, }); await workflowService.update(user, updateData, workflowId, { source: 'n8n-ai', }); }, ...(this.license.isLicensed('feat:namedVersions') ? { async updateVersion(workflowId, versionId, data) { await workflowHistoryService.updateVersionForUser(user, workflowId, versionId, data); }, } : {}), }; } createExecutionAdapter(user, pushRef, threadId) { const { workflowFinderService, workflowRunner, activeExecutions, executionRepository, allowSendingParameterValues, license, roleService, telemetry, } = this; const assertNotReadOnly = () => this.assertInstanceNotReadOnly('executions'); const DEFAULT_TIMEOUT_MS = 5 * constants_1.Time.minutes.toMilliseconds; const MAX_TIMEOUT_MS = 10 * constants_1.Time.minutes.toMilliseconds; const assertExecutionAccess = async (executionId, scopes = ['workflow:read']) => { const execution = await executionRepository.findSingleExecution(executionId, { includeData: false, }); if (!execution) { throw new Error(`Execution ${executionId} not found`); } const workflow = await workflowFinderService.findWorkflowForUser(execution.workflowId, user, scopes); if (!workflow) { throw new Error(`Execution ${executionId} not found`); } return execution; }; return { async list(options) { const scope = 'workflow:read'; let sharingOptions; if (license.isSharingEnabled()) { const projectRoles = await roleService.rolesWithScope('project', [scope]); const workflowRoles = await roleService.rolesWithScope('workflow', [scope]); sharingOptions = { scopes: [scope], projectRoles, workflowRoles }; } else { sharingOptions = { workflowRoles: ['workflow:owner'], projectRoles: [permissions_1.PROJECT_OWNER_ROLE_SLUG], }; } const query = { kind: 'range', range: { limit: options?.limit ?? 20, lastId: undefined, firstId: undefined }, order: { startedAt: 'DESC' }, user, sharingOptions, ...(options?.workflowId ? { workflowId: options.workflowId } : {}), ...(options?.status ? { status: [options.status], } : {}), }; const executions = await executionRepository.findManyByRangeQuery(query); return executions.map((e) => ({ id: e.id, workflowId: e.workflowId, workflowName: e.workflowName ?? '', status: e.status, startedAt: String(e.startedAt ?? ''), finishedAt: e.stoppedAt ? String(e.stoppedAt) : undefined, mode: e.mode, })); }, async run(workflowId, inputData, options) { assertNotReadOnly(); const workflow = await workflowFinderService.findWorkflowForUser(workflowId, user, [ 'workflow:execute', ]); if (!workflow) { throw new Error(`Workflow ${workflowId} not found or not accessible`); } const nodes = workflow.nodes ?? []; const triggerNode = options?.triggerNodeName ? (nodes.find((n) => n.name === options.triggerNodeName) ?? findTriggerNode(nodes)) : findTriggerNode(nodes); const runData = { executionMode: triggerNode ? getExecutionModeForTrigger(triggerNode) : 'manual', workflowData: { ...workflow, settings: { ...workflow.settings, saveManualExecutions: true, saveDataSuccessExecution: 'all', saveDataErrorExecution: 'all', }, }, userId: user.id, pushRef, }; const workflowPinData = workflow.pinData ?? {}; const overridePinData = options?.pinData ? (sdkPinDataToRuntime(options.pinData) ?? {}) : {}; const basePinData = { ...workflowPinData, ...overridePinData }; if (inputData && triggerNode) { const triggerPinData = getPinDataForTrigger(triggerNode, inputData); const mergedPinData = { ...basePinData, ...triggerPinData }; runData.startNodes = [{ name: triggerNode.name, sourceData: null }]; runData.pinData = mergedPinData; runData.executionData = (0, n8n_workflow_1.createRunExecutionData)({ startData: {}, resultData: { pinData: mergedPinData, runData: {} }, executionData: { contextData: {}, metadata: {}, nodeExecutionStack: [ { node: triggerNode, data: { main: [triggerPinData[triggerNode.name]] }, source: null, }, ], waitingExecution: {}, waitingExecutionSource: {}, }, }); } else if (triggerNode) { runData.triggerToStartFrom = { name: triggerNode.name }; if (Object.keys(basePinData).length > 0) { runData.pinData = basePinData; } } else if (Object.keys(basePinData).length > 0) { runData.pinData = basePinData; } const trackBuilderExecutedWorkflow = (status) => { if (!threadId) return; telemetry.track('Builder executed workflow', { thread_id: threadId, workflow_id: workflowId, executed_by: 'ai', pinned_node_count: Object.keys(runData.pinData ?? {}).length, exec_type: runData.executionMode, status, }); }; const executionId = await workflowRunner.run(runData); const timeoutMs = Math.min(options?.timeout ?? DEFAULT_TIMEOUT_MS, MAX_TIMEOUT_MS); if (activeExecutions.has(executionId)) { let timeoutId; const timeoutPromise = new Promise((_, reject) => { timeoutId = setTimeout(() => { reject(new Error(`Execution timed out after ${timeoutMs}ms`)); }, timeoutMs); }); try { await Promise.race([ activeExecutions.getPostExecutePromise(executionId), timeoutPromise, ]); clearTimeout(timeoutId); } catch (error) { clearTimeout(timeoutId); if (error instanceof Error && error.message.includes('timed out')) { try { activeExecutions.stopExecution(executionId, new n8n_workflow_1.TimeoutExecutionCancelledError(executionId)); } catch { } const result = { executionId, status: 'error', error: `Execution timed out after ${timeoutMs}ms and was cancelled`, }; trackBuilderExecutedWorkflow(result.status); return result; } throw error; } } const result = await extractExecutionResult(executionRepository, executionId, allowSendingParameterValues); trackBuilderExecutedWorkflow(result.status); return result; }, async getStatus(executionId) { await assertExecutionAccess(executionId); const isRunning = activeExecutions.has(executionId); if (isRunning) { return { executionId, status: 'running' }; } return await extractExecutionResult(executionRepository, executionId, allowSendingParameterValues); }, async getResult(executionId) { await assertExecutionAccess(executionId); if (activeExecutions.has(executionId)) { await activeExecutions.getPostExecutePromise(executionId); } return await extractExecutionResult(executionRepository, executionId, allowSendingParameterValues); }, async stop(executionId) { assertNotReadOnly(); await assertExecutionAccess(executionId, ['workflow:execute']); if (!activeExecutions.has(executionId)) { return { success: false, message: `Execution ${executionId} is not currently running`, }; } try { activeExecutions.stopExecution(executionId, new n8n_workflow_1.TimeoutExecutionCancelledError(executionId)); return { success: true, message: `Execution ${executionId} cancelled` }; } catch { return { success: false, message: `Failed to cancel execution ${executionId}`, }; } }, async getDebugInfo(executionId) { await assertExecutionAccess(executionId); return await extractExecutionDebugInfo(executionRepository, executionId, allowSendingParameterValues); }, async getNodeOutput(executionId, nodeName, options) { await assertExecutionAccess(executionId); if (!allowSendingParameterValues) { return { nodeName, items: [], totalItems: 0, returned: { from: 0, to: 0 }, }; } return await extractNodeOutput(executionRepository, executionId, nodeName, options); }, }; } createCredentialAdapter(user) { const { credentialsService, credentialsFinderService, loadNodesAndCredentials } = this; return { async list(options) { if (options?.workflowId || options?.projectId) { const scoped = options.workflowId ? await credentialsService.getCredentialsAUserCanUseInAWorkflow(user, { workflowId: options.workflowId, }) : await credentialsService.getCredentialsAUserCanUseInAWorkflow(user, { projectId: options.projectId, }); const filtered = options.type ? scoped.filter((c) => c.type === options.type) : scoped; return filtered.map((c) => ({ id: c.id, name: c.name, type: c.type, })); } const credentials = await credentialsService.getMany(user, { listQueryOptions: { filter: options?.type ? { type: options.type } : undefined, }, includeGlobal: true, }); return credentials.map((c) => ({ id: c.id, name: c.name, type: c.type, })); }, async get(credentialId) { const credential = await credentialsService.getOne(user, credentialId, false); return { id: credential.id, name: credential.name, type: credential.type, }; }, async delete(credentialId) { await credentialsService.delete(user, credentialId); }, async test(credentialId) { const credential = await credentialsFinderService.findCredentialForUser(credentialId, user, ['credential:read']); if (!credential) { throw new Error(`Credential ${credentialId} not found or not accessible`); } const credentialsToTest = { id: credential.id, name: credential.name, type: credential.type, data: await credentialsService.decrypt(credential, true), }; const result = await credentialsService.test(user.id, credentialsToTest); return { success: result.status === 'OK', message: result.message, }; }, async isTestable(credentialType) { try { const credClass = loadNodesAndCredentials.getCredential(credentialType); if (credClass.type.test) return true; const known = loadNodesAndCredentials.knownCredentials; const supportedNodes = known[credentialType]?.supportedNodes ?? []; for (const nodeName of supportedNodes) { try { const loaded = loadNodesAndCredentials.getNode(nodeName); const nodeInstance = loaded.type; const nodeDesc = 'nodeVersions' in nodeInstance ? Object.values(nodeInstance.nodeVersions).pop()?.description : nodeInstance.description; const hasTestedBy = nodeDesc?.credentials?.some((cred) => cred.name === credentialType && cred.testedBy); if (hasTestedBy) return true; } catch { continue; } } return false; } catch { return false; } }, async getDocumentationUrl(credentialType) { try { const credClass = loadNodesAndCredentials.getCredential(credentialType); const slug = credClass.type.documentationUrl; if (!slug) return null; if (slug.startsWith('http')) return slug; return `https://docs.n8n.io/integrations/builtin/credentials/${slug}/`; } catch { return null; } }, getCredentialFields(credentialType) { try { const allTypes = [credentialType]; const known = loadNodesAndCredentials.knownCredentials; for (const typeName of allTypes) { const extendsArr = known[typeName]?.extends ?? []; allTypes.push(...extendsArr); } const fields = []; const seen = new Set(); for (const typeName of allTypes) { try { const credClass = loadNodesAndCredentials.getCredential(typeName); for (const prop of credClass.type.properties) { if (prop.type === 'hidden' || seen.has(prop.name)) continue; seen.add(prop.name); fields.push({ name: prop.name, displayName: prop.displayName, type: prop.type, required: prop.required ?? false, description: prop.description, }); } } catch { } } return fields; } catch { return []; } }, async searchCredentialTypes(query) { const q = query.toLowerCase().trim(); if (!q) return []; const known = loadNodesAndCredentials.knownCredentials; const results = []; for (const typeName of Object.keys(known)) { if (typeName.toLowerCase().includes(q)) { try { const credClass = loadNodesAndCredentials.getCredential(typeName); results.push({ type: typeName, displayName: credClass.type.displayName, }); } catch { results.push({ type: typeName, displayName: typeName }); } continue; } try { const credClass = loadNodesAndCredentials.getCredential(typeName); if (credClass.type.displayName.toLowerCase().includes(q)) { results.push({ type: typeName, displayName: credClass.type.displayName, }); } } catch { } } return results; }, async getAccountContext(credentialId) { const credential = await credentialsFinderService.findCredentialForUser(credentialId, user, ['credential:read']); if (!credential) { return { accountIdentifier: undefined }; } const mask = (id) => { const atIdx = id.indexOf('@'); if (atIdx > 0) { const local = id.slice(0, atIdx); const domain = id.slice(atIdx); const keep = Math.min(2, local.length); return local.slice(0, keep) + '***' + domain; } if (id.length <= 3) return id; return id.slice(0, 2) + '***' + id.slice(-1); }; try { const redacted = await credentialsService.decrypt(credential, false); if (typeof redacted.accountIdentifier === 'string' && redacted.accountIdentifier) { return { accountIdentifier: mask(redacted.accountIdentifier) }; } for (const key of ['email', 'user', 'username', 'account', 'serviceAccountEmail']) { const value = redacted[key]; if (typeof value === 'string' && value) { return { accountIdentifier: mask(value) }; } } const raw = await credentialsService.decrypt(credential, true); const tokenData = raw.oauthTokenData; if (tokenData && typeof tokenData === 'object') { const { OauthService } = await Promise.resolve().then(() => __importStar(require('../../oauth/oauth.service'))); const identifier = OauthService.extractAccountIdentifier(tokenData); if (identifier) { return { accountIdentifier: mask(identifier) }; } } return { accountIdentifier: undefined }; } catch { return { accountIdentifier: undefined }; } }, }; } createDataTableAdapter(user) { const { dataTableService, dataTableRepository } = this; const assertNotReadOnly = () => this.assertInstanceNotReadOnly('data tables'); const { resolveProjectId } = this.createProjectScopeHelpers(user); const logger = this.logger; const resolveAccessibleTable = async (scopes, dataTableId, disambiguator) => { const projectIdFilter = disambiguator?.projectId; const result = await resolveDataTableByIdOrName(dataTableRepository, logger, dataTableId, { projectIdFilter, accessFilter: async (id) => await (0, check_access_1.userHasScopes)(user, scopes, false, { dataTableId: id }), }); if (result.kind === 'miss') { throw new Error(`Data table "${dataTableId}" not found`); } if (result.kind === 'ambiguous') { const projectIds = result.candidates.map((c) => c.projectId).join(', '); throw new Error(`Data table name "${dataTableId}" is ambiguous across accessible projects ` + `(${projectIds}); pass the UUID or include a \`projectId\` to disambiguate.`); } if (projectIdFilter && result.table.projectId !== projectIdFilter) { throw new Error(`Data table "${dataTableId}" does not belong to project "${projectIdFilter}".`); } return result.table; }; const resolveProjectIdForTable = async (scopes, dataTableId, disambiguator) => { const table = await resolveAccessibleTable(scopes, dataTableId, disambiguator); return { projectId: table.projectId, resolvedId: table.id }; }; const resolveTableMeta = async (scopes, dataTableId, disambiguator) => { const table = await resolveAccessibleTable(scopes, dataTableId, disambiguator); return { projectId: table.projectId, tableName: table.name, resolvedId: table.id }; }; return { async list(options) { const projectId = await resolveProjectId(['dataTable:listProject'], options?.projectId); const { data: tables } = await dataTableService.getManyAndCount({ filter: { projectId }, }); return tables.map((t) => ({ id: t.id, name: t.name, projectId, columns: t.columns.map((c) => ({ id: c.id, name: c.name, type: c.type })), createdAt: t.createdAt.toISOString(), updatedAt: t.updatedAt.toISOString(), })); }, async create(name, columns, options) { assertNotReadOnly(); const projectId = await resolveProjectId(['dataTable:create'], options?.projectId); const result = await dataTableService.createDataTable(projectId, { name, columns }); return { id: result.id, name: result.name, projectId, columns: result.columns.map((c) => ({ id: c.id, name: c.name, type: c.type })), createdAt: result.createdAt.toISOString(), updatedAt: result.updatedAt.toISOString(), }; }, async delete(dataTableId, options) { assertNotReadOnly(); const { projectId, resolvedId } = await resolveProjectIdForTable(['dataTable:delete'], dataTableId, options); await dataTableService.deleteDataTable(resolvedId, projectId); }, async getSchema(dataTableId, options) { const { projectId, resolvedId } = await resolveProjectIdForTable(['dataTable:read'], dataTableId, options); const columns = await dataTableS