@pagelines/n8n-mcp 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.github/workflows/ci.yml +38 -0
- package/README.md +155 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.js +192 -0
- package/dist/n8n-client.d.ts +54 -0
- package/dist/n8n-client.js +275 -0
- package/dist/n8n-client.test.d.ts +1 -0
- package/dist/n8n-client.test.js +184 -0
- package/dist/tools.d.ts +6 -0
- package/dist/tools.js +260 -0
- package/dist/types.d.ts +132 -0
- package/dist/types.js +5 -0
- package/dist/validators.d.ts +10 -0
- package/dist/validators.js +171 -0
- package/dist/validators.test.d.ts +1 -0
- package/dist/validators.test.js +148 -0
- package/package.json +42 -0
- package/server.json +58 -0
- package/src/index.ts +243 -0
- package/src/n8n-client.test.ts +227 -0
- package/src/n8n-client.ts +361 -0
- package/src/tools.ts +273 -0
- package/src/types.ts +107 -0
- package/src/validators.test.ts +180 -0
- package/src/validators.ts +208 -0
- package/tsconfig.json +15 -0
- package/vitest.config.ts +8 -0
package/dist/tools.js
ADDED
|
@@ -0,0 +1,260 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MCP Tool Definitions
|
|
3
|
+
* Minimal, focused toolset for workflow management
|
|
4
|
+
*/
|
|
5
|
+
export const tools = [
|
|
6
|
+
// ─────────────────────────────────────────────────────────────
|
|
7
|
+
// Workflow Operations
|
|
8
|
+
// ─────────────────────────────────────────────────────────────
|
|
9
|
+
{
|
|
10
|
+
name: 'workflow_list',
|
|
11
|
+
description: 'List all workflows. Returns id, name, active status.',
|
|
12
|
+
inputSchema: {
|
|
13
|
+
type: 'object',
|
|
14
|
+
properties: {
|
|
15
|
+
active: {
|
|
16
|
+
type: 'boolean',
|
|
17
|
+
description: 'Filter by active status',
|
|
18
|
+
},
|
|
19
|
+
limit: {
|
|
20
|
+
type: 'number',
|
|
21
|
+
description: 'Max results (default 100)',
|
|
22
|
+
},
|
|
23
|
+
},
|
|
24
|
+
},
|
|
25
|
+
},
|
|
26
|
+
{
|
|
27
|
+
name: 'workflow_get',
|
|
28
|
+
description: 'Get a workflow by ID. Returns full workflow with nodes, connections, settings.',
|
|
29
|
+
inputSchema: {
|
|
30
|
+
type: 'object',
|
|
31
|
+
properties: {
|
|
32
|
+
id: {
|
|
33
|
+
type: 'string',
|
|
34
|
+
description: 'Workflow ID',
|
|
35
|
+
},
|
|
36
|
+
},
|
|
37
|
+
required: ['id'],
|
|
38
|
+
},
|
|
39
|
+
},
|
|
40
|
+
{
|
|
41
|
+
name: 'workflow_create',
|
|
42
|
+
description: 'Create a new workflow. Returns the created workflow.',
|
|
43
|
+
inputSchema: {
|
|
44
|
+
type: 'object',
|
|
45
|
+
properties: {
|
|
46
|
+
name: {
|
|
47
|
+
type: 'string',
|
|
48
|
+
description: 'Workflow name (use snake_case)',
|
|
49
|
+
},
|
|
50
|
+
nodes: {
|
|
51
|
+
type: 'array',
|
|
52
|
+
description: 'Array of node definitions',
|
|
53
|
+
items: {
|
|
54
|
+
type: 'object',
|
|
55
|
+
properties: {
|
|
56
|
+
name: { type: 'string' },
|
|
57
|
+
type: { type: 'string' },
|
|
58
|
+
typeVersion: { type: 'number' },
|
|
59
|
+
position: {
|
|
60
|
+
type: 'array',
|
|
61
|
+
items: { type: 'number' },
|
|
62
|
+
minItems: 2,
|
|
63
|
+
maxItems: 2,
|
|
64
|
+
},
|
|
65
|
+
parameters: { type: 'object' },
|
|
66
|
+
credentials: { type: 'object' },
|
|
67
|
+
},
|
|
68
|
+
required: ['name', 'type', 'typeVersion', 'position', 'parameters'],
|
|
69
|
+
},
|
|
70
|
+
},
|
|
71
|
+
connections: {
|
|
72
|
+
type: 'object',
|
|
73
|
+
description: 'Connection map: { "NodeName": { "main": [[{ "node": "TargetNode", "type": "main", "index": 0 }]] } }',
|
|
74
|
+
},
|
|
75
|
+
settings: {
|
|
76
|
+
type: 'object',
|
|
77
|
+
description: 'Workflow settings',
|
|
78
|
+
},
|
|
79
|
+
},
|
|
80
|
+
required: ['name', 'nodes', 'connections'],
|
|
81
|
+
},
|
|
82
|
+
},
|
|
83
|
+
{
|
|
84
|
+
name: 'workflow_update',
|
|
85
|
+
description: `Update a workflow using patch operations. ALWAYS preserves existing data.
|
|
86
|
+
|
|
87
|
+
Operations:
|
|
88
|
+
- addNode: Add a new node
|
|
89
|
+
- removeNode: Remove a node and its connections
|
|
90
|
+
- updateNode: Update node properties (INCLUDE ALL existing parameters to preserve them)
|
|
91
|
+
- addConnection: Connect two nodes
|
|
92
|
+
- removeConnection: Disconnect two nodes
|
|
93
|
+
- updateSettings: Update workflow settings
|
|
94
|
+
- updateName: Rename the workflow
|
|
95
|
+
|
|
96
|
+
Example: { "operations": [{ "type": "updateNode", "nodeName": "my_node", "properties": { "parameters": { ...allExistingParams, "newParam": "value" } } }] }`,
|
|
97
|
+
inputSchema: {
|
|
98
|
+
type: 'object',
|
|
99
|
+
properties: {
|
|
100
|
+
id: {
|
|
101
|
+
type: 'string',
|
|
102
|
+
description: 'Workflow ID',
|
|
103
|
+
},
|
|
104
|
+
operations: {
|
|
105
|
+
type: 'array',
|
|
106
|
+
description: 'Array of patch operations',
|
|
107
|
+
items: {
|
|
108
|
+
type: 'object',
|
|
109
|
+
properties: {
|
|
110
|
+
type: {
|
|
111
|
+
type: 'string',
|
|
112
|
+
enum: ['addNode', 'removeNode', 'updateNode', 'addConnection', 'removeConnection', 'updateSettings', 'updateName'],
|
|
113
|
+
},
|
|
114
|
+
// For addNode
|
|
115
|
+
node: { type: 'object' },
|
|
116
|
+
// For removeNode, updateNode
|
|
117
|
+
nodeName: { type: 'string' },
|
|
118
|
+
// For updateNode
|
|
119
|
+
properties: { type: 'object' },
|
|
120
|
+
// For connections
|
|
121
|
+
from: { type: 'string' },
|
|
122
|
+
to: { type: 'string' },
|
|
123
|
+
fromOutput: { type: 'number' },
|
|
124
|
+
toInput: { type: 'number' },
|
|
125
|
+
outputType: { type: 'string' },
|
|
126
|
+
inputType: { type: 'string' },
|
|
127
|
+
// For updateSettings
|
|
128
|
+
settings: { type: 'object' },
|
|
129
|
+
// For updateName
|
|
130
|
+
name: { type: 'string' },
|
|
131
|
+
},
|
|
132
|
+
required: ['type'],
|
|
133
|
+
},
|
|
134
|
+
},
|
|
135
|
+
},
|
|
136
|
+
required: ['id', 'operations'],
|
|
137
|
+
},
|
|
138
|
+
},
|
|
139
|
+
{
|
|
140
|
+
name: 'workflow_delete',
|
|
141
|
+
description: 'Delete a workflow permanently.',
|
|
142
|
+
inputSchema: {
|
|
143
|
+
type: 'object',
|
|
144
|
+
properties: {
|
|
145
|
+
id: {
|
|
146
|
+
type: 'string',
|
|
147
|
+
description: 'Workflow ID',
|
|
148
|
+
},
|
|
149
|
+
},
|
|
150
|
+
required: ['id'],
|
|
151
|
+
},
|
|
152
|
+
},
|
|
153
|
+
{
|
|
154
|
+
name: 'workflow_activate',
|
|
155
|
+
description: 'Activate a workflow (enable triggers).',
|
|
156
|
+
inputSchema: {
|
|
157
|
+
type: 'object',
|
|
158
|
+
properties: {
|
|
159
|
+
id: {
|
|
160
|
+
type: 'string',
|
|
161
|
+
description: 'Workflow ID',
|
|
162
|
+
},
|
|
163
|
+
},
|
|
164
|
+
required: ['id'],
|
|
165
|
+
},
|
|
166
|
+
},
|
|
167
|
+
{
|
|
168
|
+
name: 'workflow_deactivate',
|
|
169
|
+
description: 'Deactivate a workflow (disable triggers).',
|
|
170
|
+
inputSchema: {
|
|
171
|
+
type: 'object',
|
|
172
|
+
properties: {
|
|
173
|
+
id: {
|
|
174
|
+
type: 'string',
|
|
175
|
+
description: 'Workflow ID',
|
|
176
|
+
},
|
|
177
|
+
},
|
|
178
|
+
required: ['id'],
|
|
179
|
+
},
|
|
180
|
+
},
|
|
181
|
+
{
|
|
182
|
+
name: 'workflow_execute',
|
|
183
|
+
description: 'Execute a workflow via its webhook trigger. Workflow must have a webhook node.',
|
|
184
|
+
inputSchema: {
|
|
185
|
+
type: 'object',
|
|
186
|
+
properties: {
|
|
187
|
+
id: {
|
|
188
|
+
type: 'string',
|
|
189
|
+
description: 'Workflow ID',
|
|
190
|
+
},
|
|
191
|
+
data: {
|
|
192
|
+
type: 'object',
|
|
193
|
+
description: 'Data to send to the webhook',
|
|
194
|
+
},
|
|
195
|
+
},
|
|
196
|
+
required: ['id'],
|
|
197
|
+
},
|
|
198
|
+
},
|
|
199
|
+
// ─────────────────────────────────────────────────────────────
|
|
200
|
+
// Execution Operations
|
|
201
|
+
// ─────────────────────────────────────────────────────────────
|
|
202
|
+
{
|
|
203
|
+
name: 'execution_list',
|
|
204
|
+
description: 'List workflow executions. Filter by workflow or status.',
|
|
205
|
+
inputSchema: {
|
|
206
|
+
type: 'object',
|
|
207
|
+
properties: {
|
|
208
|
+
workflowId: {
|
|
209
|
+
type: 'string',
|
|
210
|
+
description: 'Filter by workflow ID',
|
|
211
|
+
},
|
|
212
|
+
status: {
|
|
213
|
+
type: 'string',
|
|
214
|
+
enum: ['success', 'error', 'waiting'],
|
|
215
|
+
description: 'Filter by status',
|
|
216
|
+
},
|
|
217
|
+
limit: {
|
|
218
|
+
type: 'number',
|
|
219
|
+
description: 'Max results (default 20)',
|
|
220
|
+
},
|
|
221
|
+
},
|
|
222
|
+
},
|
|
223
|
+
},
|
|
224
|
+
{
|
|
225
|
+
name: 'execution_get',
|
|
226
|
+
description: 'Get execution details including run data and errors.',
|
|
227
|
+
inputSchema: {
|
|
228
|
+
type: 'object',
|
|
229
|
+
properties: {
|
|
230
|
+
id: {
|
|
231
|
+
type: 'string',
|
|
232
|
+
description: 'Execution ID',
|
|
233
|
+
},
|
|
234
|
+
},
|
|
235
|
+
required: ['id'],
|
|
236
|
+
},
|
|
237
|
+
},
|
|
238
|
+
// ─────────────────────────────────────────────────────────────
|
|
239
|
+
// Validation
|
|
240
|
+
// ─────────────────────────────────────────────────────────────
|
|
241
|
+
{
|
|
242
|
+
name: 'workflow_validate',
|
|
243
|
+
description: `Validate a workflow against best practices:
|
|
244
|
+
- snake_case naming
|
|
245
|
+
- Explicit node references (no $json)
|
|
246
|
+
- No hardcoded IDs
|
|
247
|
+
- No hardcoded secrets
|
|
248
|
+
- No orphan nodes`,
|
|
249
|
+
inputSchema: {
|
|
250
|
+
type: 'object',
|
|
251
|
+
properties: {
|
|
252
|
+
id: {
|
|
253
|
+
type: 'string',
|
|
254
|
+
description: 'Workflow ID to validate',
|
|
255
|
+
},
|
|
256
|
+
},
|
|
257
|
+
required: ['id'],
|
|
258
|
+
},
|
|
259
|
+
},
|
|
260
|
+
];
|
package/dist/types.d.ts
ADDED
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* n8n API Types
|
|
3
|
+
* Minimal types for workflow management
|
|
4
|
+
*/
|
|
5
|
+
export interface N8nNode {
|
|
6
|
+
id: string;
|
|
7
|
+
name: string;
|
|
8
|
+
type: string;
|
|
9
|
+
typeVersion: number;
|
|
10
|
+
position: [number, number];
|
|
11
|
+
parameters: Record<string, unknown>;
|
|
12
|
+
credentials?: Record<string, {
|
|
13
|
+
id: string;
|
|
14
|
+
name: string;
|
|
15
|
+
}>;
|
|
16
|
+
disabled?: boolean;
|
|
17
|
+
notes?: string;
|
|
18
|
+
notesInFlow?: boolean;
|
|
19
|
+
}
|
|
20
|
+
export interface N8nConnection {
|
|
21
|
+
node: string;
|
|
22
|
+
type: string;
|
|
23
|
+
index: number;
|
|
24
|
+
}
|
|
25
|
+
export interface N8nConnections {
|
|
26
|
+
[nodeName: string]: {
|
|
27
|
+
[outputType: string]: N8nConnection[][];
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
export interface N8nWorkflow {
|
|
31
|
+
id: string;
|
|
32
|
+
name: string;
|
|
33
|
+
active: boolean;
|
|
34
|
+
nodes: N8nNode[];
|
|
35
|
+
connections: N8nConnections;
|
|
36
|
+
settings?: Record<string, unknown>;
|
|
37
|
+
staticData?: Record<string, unknown>;
|
|
38
|
+
tags?: {
|
|
39
|
+
id: string;
|
|
40
|
+
name: string;
|
|
41
|
+
}[];
|
|
42
|
+
createdAt: string;
|
|
43
|
+
updatedAt: string;
|
|
44
|
+
}
|
|
45
|
+
export interface N8nWorkflowListItem {
|
|
46
|
+
id: string;
|
|
47
|
+
name: string;
|
|
48
|
+
active: boolean;
|
|
49
|
+
createdAt: string;
|
|
50
|
+
updatedAt: string;
|
|
51
|
+
tags?: {
|
|
52
|
+
id: string;
|
|
53
|
+
name: string;
|
|
54
|
+
}[];
|
|
55
|
+
}
|
|
56
|
+
export interface N8nExecution {
|
|
57
|
+
id: string;
|
|
58
|
+
workflowId: string;
|
|
59
|
+
finished: boolean;
|
|
60
|
+
mode: string;
|
|
61
|
+
startedAt: string;
|
|
62
|
+
stoppedAt?: string;
|
|
63
|
+
status: 'success' | 'error' | 'waiting' | 'running';
|
|
64
|
+
data?: {
|
|
65
|
+
resultData?: {
|
|
66
|
+
runData?: Record<string, unknown>;
|
|
67
|
+
error?: {
|
|
68
|
+
message: string;
|
|
69
|
+
};
|
|
70
|
+
};
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
export interface N8nExecutionListItem {
|
|
74
|
+
id: string;
|
|
75
|
+
workflowId: string;
|
|
76
|
+
status: string;
|
|
77
|
+
startedAt: string;
|
|
78
|
+
stoppedAt?: string;
|
|
79
|
+
mode: string;
|
|
80
|
+
}
|
|
81
|
+
export type PatchOperation = {
|
|
82
|
+
type: 'addNode';
|
|
83
|
+
node: Omit<N8nNode, 'id'> & {
|
|
84
|
+
id?: string;
|
|
85
|
+
};
|
|
86
|
+
} | {
|
|
87
|
+
type: 'removeNode';
|
|
88
|
+
nodeName: string;
|
|
89
|
+
} | {
|
|
90
|
+
type: 'updateNode';
|
|
91
|
+
nodeName: string;
|
|
92
|
+
properties: Partial<N8nNode>;
|
|
93
|
+
} | {
|
|
94
|
+
type: 'addConnection';
|
|
95
|
+
from: string;
|
|
96
|
+
to: string;
|
|
97
|
+
fromOutput?: number;
|
|
98
|
+
toInput?: number;
|
|
99
|
+
outputType?: string;
|
|
100
|
+
inputType?: string;
|
|
101
|
+
} | {
|
|
102
|
+
type: 'removeConnection';
|
|
103
|
+
from: string;
|
|
104
|
+
to: string;
|
|
105
|
+
fromOutput?: number;
|
|
106
|
+
toInput?: number;
|
|
107
|
+
outputType?: string;
|
|
108
|
+
} | {
|
|
109
|
+
type: 'updateSettings';
|
|
110
|
+
settings: Record<string, unknown>;
|
|
111
|
+
} | {
|
|
112
|
+
type: 'updateName';
|
|
113
|
+
name: string;
|
|
114
|
+
} | {
|
|
115
|
+
type: 'activate';
|
|
116
|
+
} | {
|
|
117
|
+
type: 'deactivate';
|
|
118
|
+
};
|
|
119
|
+
export interface ValidationWarning {
|
|
120
|
+
node?: string;
|
|
121
|
+
rule: string;
|
|
122
|
+
message: string;
|
|
123
|
+
severity: 'error' | 'warning' | 'info';
|
|
124
|
+
}
|
|
125
|
+
export interface ValidationResult {
|
|
126
|
+
valid: boolean;
|
|
127
|
+
warnings: ValidationWarning[];
|
|
128
|
+
}
|
|
129
|
+
export interface N8nListResponse<T> {
|
|
130
|
+
data: T[];
|
|
131
|
+
nextCursor?: string;
|
|
132
|
+
}
|
package/dist/types.js
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Opinion-based workflow validation
|
|
3
|
+
* Enforces best practices from n8n-best-practices.md
|
|
4
|
+
*/
|
|
5
|
+
import type { N8nWorkflow, ValidationResult, ValidationWarning } from './types.js';
|
|
6
|
+
export declare function validateWorkflow(workflow: N8nWorkflow): ValidationResult;
|
|
7
|
+
/**
|
|
8
|
+
* Validate a partial update to ensure it won't cause issues
|
|
9
|
+
*/
|
|
10
|
+
export declare function validatePartialUpdate(currentWorkflow: N8nWorkflow, nodeName: string, newParameters: Record<string, unknown>): ValidationWarning[];
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Opinion-based workflow validation
|
|
3
|
+
* Enforces best practices from n8n-best-practices.md
|
|
4
|
+
*/
|
|
5
|
+
export function validateWorkflow(workflow) {
|
|
6
|
+
const warnings = [];
|
|
7
|
+
// Validate workflow name
|
|
8
|
+
validateSnakeCase(workflow.name, 'workflow', warnings);
|
|
9
|
+
// Validate each node
|
|
10
|
+
for (const node of workflow.nodes) {
|
|
11
|
+
validateNode(node, warnings);
|
|
12
|
+
}
|
|
13
|
+
// Check for orphan nodes (no connections)
|
|
14
|
+
validateConnections(workflow, warnings);
|
|
15
|
+
return {
|
|
16
|
+
valid: warnings.filter((w) => w.severity === 'error').length === 0,
|
|
17
|
+
warnings,
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
function validateNode(node, warnings) {
|
|
21
|
+
// Check node name is snake_case
|
|
22
|
+
validateSnakeCase(node.name, `node "${node.name}"`, warnings);
|
|
23
|
+
// Check for $json usage (should use explicit references)
|
|
24
|
+
checkForJsonUsage(node, warnings);
|
|
25
|
+
// Check for hardcoded IDs
|
|
26
|
+
checkForHardcodedIds(node, warnings);
|
|
27
|
+
// Check for hardcoded secrets
|
|
28
|
+
checkForHardcodedSecrets(node, warnings);
|
|
29
|
+
}
|
|
30
|
+
function validateSnakeCase(name, context, warnings) {
|
|
31
|
+
// Allow spaces for readability, but check the underlying format
|
|
32
|
+
const normalized = name.toLowerCase().replace(/\s+/g, '_');
|
|
33
|
+
// Check if it matches snake_case pattern (allowing numbers)
|
|
34
|
+
const isSnakeCase = /^[a-z][a-z0-9_]*$/.test(normalized);
|
|
35
|
+
// Also allow names that are already snake_case
|
|
36
|
+
const isAlreadySnake = /^[a-z][a-z0-9_]*$/.test(name);
|
|
37
|
+
if (!isSnakeCase && !isAlreadySnake) {
|
|
38
|
+
warnings.push({
|
|
39
|
+
rule: 'snake_case',
|
|
40
|
+
message: `${context} should use snake_case naming: "${name}" → "${normalized}"`,
|
|
41
|
+
severity: 'warning',
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
function checkForJsonUsage(node, warnings) {
|
|
46
|
+
const params = JSON.stringify(node.parameters);
|
|
47
|
+
// Detect $json without explicit node reference
|
|
48
|
+
// Bad: $json.field, {{ $json.field }}
|
|
49
|
+
// Good: $('node_name').item.json.field
|
|
50
|
+
const badPatterns = [
|
|
51
|
+
/\$json\./g,
|
|
52
|
+
/\{\{\s*\$json\./g,
|
|
53
|
+
];
|
|
54
|
+
for (const pattern of badPatterns) {
|
|
55
|
+
if (pattern.test(params)) {
|
|
56
|
+
warnings.push({
|
|
57
|
+
node: node.name,
|
|
58
|
+
rule: 'explicit_reference',
|
|
59
|
+
message: `Node "${node.name}" uses $json - use explicit $('node_name').item.json.field instead`,
|
|
60
|
+
severity: 'warning',
|
|
61
|
+
});
|
|
62
|
+
break;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
function checkForHardcodedIds(node, warnings) {
|
|
67
|
+
const params = JSON.stringify(node.parameters);
|
|
68
|
+
// Detect patterns that look like hardcoded IDs
|
|
69
|
+
const idPatterns = [
|
|
70
|
+
// Discord IDs (17-19 digit numbers)
|
|
71
|
+
/["']\d{17,19}["']/g,
|
|
72
|
+
// UUIDs
|
|
73
|
+
/["'][0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}["']/gi,
|
|
74
|
+
// MongoDB ObjectIds
|
|
75
|
+
/["'][0-9a-f]{24}["']/gi,
|
|
76
|
+
];
|
|
77
|
+
for (const pattern of idPatterns) {
|
|
78
|
+
if (pattern.test(params)) {
|
|
79
|
+
warnings.push({
|
|
80
|
+
node: node.name,
|
|
81
|
+
rule: 'no_hardcoded_ids',
|
|
82
|
+
message: `Node "${node.name}" may contain hardcoded IDs - consider using config nodes or environment variables`,
|
|
83
|
+
severity: 'info',
|
|
84
|
+
});
|
|
85
|
+
break;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
function checkForHardcodedSecrets(node, warnings) {
|
|
90
|
+
const params = JSON.stringify(node.parameters).toLowerCase();
|
|
91
|
+
// Check for common secret patterns
|
|
92
|
+
const secretPatterns = [
|
|
93
|
+
/api[_-]?key["']\s*:\s*["'][^"']+["']/i,
|
|
94
|
+
/secret["']\s*:\s*["'][^"']+["']/i,
|
|
95
|
+
/password["']\s*:\s*["'][^"']+["']/i,
|
|
96
|
+
/token["']\s*:\s*["'][a-z0-9]{20,}["']/i,
|
|
97
|
+
];
|
|
98
|
+
for (const pattern of secretPatterns) {
|
|
99
|
+
if (pattern.test(params)) {
|
|
100
|
+
warnings.push({
|
|
101
|
+
node: node.name,
|
|
102
|
+
rule: 'no_hardcoded_secrets',
|
|
103
|
+
message: `Node "${node.name}" may contain hardcoded secrets - use $env.VAR_NAME instead`,
|
|
104
|
+
severity: 'error',
|
|
105
|
+
});
|
|
106
|
+
break;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
function validateConnections(workflow, warnings) {
|
|
111
|
+
const connectedNodes = new Set();
|
|
112
|
+
// Collect all nodes that have connections (as source or target)
|
|
113
|
+
for (const [sourceName, outputs] of Object.entries(workflow.connections)) {
|
|
114
|
+
connectedNodes.add(sourceName);
|
|
115
|
+
for (const outputType of Object.values(outputs)) {
|
|
116
|
+
for (const connections of outputType) {
|
|
117
|
+
for (const conn of connections) {
|
|
118
|
+
connectedNodes.add(conn.node);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
// Check for orphan nodes (excluding trigger nodes which don't need incoming)
|
|
124
|
+
const triggerTypes = [
|
|
125
|
+
'n8n-nodes-base.webhook',
|
|
126
|
+
'n8n-nodes-base.scheduleTrigger',
|
|
127
|
+
'n8n-nodes-base.manualTrigger',
|
|
128
|
+
'n8n-nodes-base.emailTrigger',
|
|
129
|
+
'@n8n/n8n-nodes-langchain.chatTrigger',
|
|
130
|
+
];
|
|
131
|
+
for (const node of workflow.nodes) {
|
|
132
|
+
const isTrigger = triggerTypes.some((t) => node.type.includes(t.split('.')[1]));
|
|
133
|
+
if (!isTrigger && !connectedNodes.has(node.name)) {
|
|
134
|
+
warnings.push({
|
|
135
|
+
node: node.name,
|
|
136
|
+
rule: 'orphan_node',
|
|
137
|
+
message: `Node "${node.name}" has no connections - may be orphaned`,
|
|
138
|
+
severity: 'warning',
|
|
139
|
+
});
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
/**
|
|
144
|
+
* Validate a partial update to ensure it won't cause issues
|
|
145
|
+
*/
|
|
146
|
+
export function validatePartialUpdate(currentWorkflow, nodeName, newParameters) {
|
|
147
|
+
const warnings = [];
|
|
148
|
+
const currentNode = currentWorkflow.nodes.find((n) => n.name === nodeName);
|
|
149
|
+
if (!currentNode) {
|
|
150
|
+
warnings.push({
|
|
151
|
+
node: nodeName,
|
|
152
|
+
rule: 'node_exists',
|
|
153
|
+
message: `Node "${nodeName}" not found in workflow`,
|
|
154
|
+
severity: 'error',
|
|
155
|
+
});
|
|
156
|
+
return warnings;
|
|
157
|
+
}
|
|
158
|
+
// Check for parameter loss
|
|
159
|
+
const currentKeys = Object.keys(currentNode.parameters);
|
|
160
|
+
const newKeys = Object.keys(newParameters);
|
|
161
|
+
const missingKeys = currentKeys.filter((k) => !newKeys.includes(k));
|
|
162
|
+
if (missingKeys.length > 0) {
|
|
163
|
+
warnings.push({
|
|
164
|
+
node: nodeName,
|
|
165
|
+
rule: 'parameter_preservation',
|
|
166
|
+
message: `Update will remove parameters: ${missingKeys.join(', ')}. Include all existing parameters to preserve them.`,
|
|
167
|
+
severity: 'error',
|
|
168
|
+
});
|
|
169
|
+
}
|
|
170
|
+
return warnings;
|
|
171
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|