@replikanti/flowlint-core 0.9.2 → 0.9.4
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/README.md +83 -73
- package/dist/index.js +29 -26
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +29 -26
- package/dist/index.mjs.map +1 -1
- package/package.json +4 -2
package/README.md
CHANGED
|
@@ -1,75 +1,85 @@
|
|
|
1
|
-
# @replikanti/flowlint-core
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
```
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
- `
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
|
66
|
-
|
|
67
|
-
|
|
|
68
|
-
|
|
|
69
|
-
|
|
|
70
|
-
|
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
1
|
+
# @replikanti/flowlint-core
|
|
2
|
+
|
|
3
|
+

|
|
4
|
+
|
|
5
|
+
Core linting engine for n8n workflows. This package provides the fundamental building blocks for analyzing and validating n8n workflow files.
|
|
6
|
+
|
|
7
|
+
## Installation
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
npm install @replikanti/flowlint-core
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
## Usage
|
|
14
|
+
|
|
15
|
+
```typescript
|
|
16
|
+
import { parseN8n, runAllRules, loadConfig, defaultConfig } from '@replikanti/flowlint-core';
|
|
17
|
+
|
|
18
|
+
// Parse a workflow from JSON string
|
|
19
|
+
const workflow = parseN8n(workflowJsonString);
|
|
20
|
+
|
|
21
|
+
// Run all linting rules
|
|
22
|
+
const findings = runAllRules(workflow, {
|
|
23
|
+
path: 'my-workflow.n8n.json',
|
|
24
|
+
cfg: defaultConfig,
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
// Process findings
|
|
28
|
+
findings.forEach(finding => {
|
|
29
|
+
console.log(`[\${finding.severity.toUpperCase()}] \${finding.rule}: \${finding.message}`);
|
|
30
|
+
});
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
## API
|
|
34
|
+
|
|
35
|
+
### Parser
|
|
36
|
+
|
|
37
|
+
- `parseN8n(doc: string): Graph` - Parse n8n workflow JSON/YAML into a graph structure
|
|
38
|
+
|
|
39
|
+
### Linting
|
|
40
|
+
|
|
41
|
+
- `runAllRules(graph: Graph, ctx: RuleContext): Finding[]` - Run all enabled rules
|
|
42
|
+
|
|
43
|
+
### Configuration
|
|
44
|
+
|
|
45
|
+
- `loadConfig(configPath?: string): FlowLintConfig` - Load configuration from file
|
|
46
|
+
- `defaultConfig: FlowLintConfig` - Default configuration
|
|
47
|
+
- `parseConfig(content: string): FlowLintConfig` - Parse config from YAML string
|
|
48
|
+
|
|
49
|
+
### Validation
|
|
50
|
+
|
|
51
|
+
- `validateN8nWorkflow(data: unknown): void` - Validate workflow structure
|
|
52
|
+
|
|
53
|
+
## Testing
|
|
54
|
+
|
|
55
|
+
Run tests with coverage reporting:
|
|
56
|
+
|
|
57
|
+
```bash
|
|
58
|
+
npm run test:coverage
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
## Rules
|
|
62
|
+
|
|
63
|
+
This package includes 14 built-in rules:
|
|
64
|
+
|
|
65
|
+
| Rule | Description | Severity |
|
|
66
|
+
|------|-------------|----------|
|
|
67
|
+
| R1 | Rate limit retry | must |
|
|
68
|
+
| R2 | Error handling | must |
|
|
69
|
+
| R3 | Idempotency | should |
|
|
70
|
+
| R4 | Secrets exposure | must |
|
|
71
|
+
| R5 | Dead ends | nit |
|
|
72
|
+
| R6 | Long running | should |
|
|
73
|
+
| R7 | Alert/log enforcement | should |
|
|
74
|
+
| R8 | Unused data | nit |
|
|
75
|
+
| R9 | Config literals | should |
|
|
76
|
+
| R10 | Naming convention | nit |
|
|
77
|
+
| R11 | Deprecated nodes | should |
|
|
78
|
+
| R12 | Unhandled error path | must |
|
|
79
|
+
| R13 | Webhook acknowledgment | must |
|
|
80
|
+
| R14 | Retry-After compliance | should |
|
|
81
|
+
|
|
82
|
+
## License
|
|
83
|
+
|
|
74
84
|
MIT
|
|
75
85
|
|
package/dist/index.js
CHANGED
|
@@ -883,6 +883,30 @@ var metadata7 = {
|
|
|
883
883
|
description: "Ensures critical paths include logging or alerting steps.",
|
|
884
884
|
details: "For example, a failed payment processing branch should trigger an alert for monitoring."
|
|
885
885
|
};
|
|
886
|
+
function isPathHandled(graph, startNodeId) {
|
|
887
|
+
const queue = [startNodeId];
|
|
888
|
+
const visited = /* @__PURE__ */ new Set([startNodeId]);
|
|
889
|
+
let head = 0;
|
|
890
|
+
while (head < queue.length) {
|
|
891
|
+
const currentId = queue[head++];
|
|
892
|
+
const currentNode = graph.nodes.find((n) => n.id === currentId);
|
|
893
|
+
if (!currentNode) continue;
|
|
894
|
+
if (isNotificationNode(currentNode.type) || isErrorHandlerNode(currentNode.type, currentNode.name)) {
|
|
895
|
+
return true;
|
|
896
|
+
}
|
|
897
|
+
if (isRejoinNode(graph, currentId)) {
|
|
898
|
+
continue;
|
|
899
|
+
}
|
|
900
|
+
const outgoing = graph.edges.filter((e) => e.from === currentId);
|
|
901
|
+
for (const outEdge of outgoing) {
|
|
902
|
+
if (!visited.has(outEdge.to)) {
|
|
903
|
+
visited.add(outEdge.to);
|
|
904
|
+
queue.push(outEdge.to);
|
|
905
|
+
}
|
|
906
|
+
}
|
|
907
|
+
}
|
|
908
|
+
return false;
|
|
909
|
+
}
|
|
886
910
|
function r7AlertLogEnforcement(graph, ctx) {
|
|
887
911
|
const cfg = ctx.cfg.rules.alert_log_enforcement;
|
|
888
912
|
if (!cfg?.enabled) return [];
|
|
@@ -890,29 +914,7 @@ function r7AlertLogEnforcement(graph, ctx) {
|
|
|
890
914
|
const errorEdges = graph.edges.filter((edge) => edge.on === "error");
|
|
891
915
|
for (const edge of errorEdges) {
|
|
892
916
|
const fromNode = graph.nodes.find((n) => n.id === edge.from);
|
|
893
|
-
|
|
894
|
-
const queue = [edge.to];
|
|
895
|
-
const visited = /* @__PURE__ */ new Set([edge.to]);
|
|
896
|
-
let head = 0;
|
|
897
|
-
while (head < queue.length) {
|
|
898
|
-
const currentId = queue[head++];
|
|
899
|
-
const currentNode = graph.nodes.find((n) => n.id === currentId);
|
|
900
|
-
if (isNotificationNode(currentNode.type) || isErrorHandlerNode(currentNode.type, currentNode.name)) {
|
|
901
|
-
isHandled = true;
|
|
902
|
-
break;
|
|
903
|
-
}
|
|
904
|
-
if (isRejoinNode(graph, currentId)) {
|
|
905
|
-
continue;
|
|
906
|
-
}
|
|
907
|
-
const outgoing = graph.edges.filter((e) => e.from === currentId);
|
|
908
|
-
for (const outEdge of outgoing) {
|
|
909
|
-
if (!visited.has(outEdge.to)) {
|
|
910
|
-
visited.add(outEdge.to);
|
|
911
|
-
queue.push(outEdge.to);
|
|
912
|
-
}
|
|
913
|
-
}
|
|
914
|
-
}
|
|
915
|
-
if (!isHandled) {
|
|
917
|
+
if (!isPathHandled(graph, edge.to)) {
|
|
916
918
|
findings.push({
|
|
917
919
|
rule: metadata7.id,
|
|
918
920
|
severity: metadata7.severity,
|
|
@@ -1137,7 +1139,7 @@ var r14RetryAfterCompliance = createNodeRule(metadata14.id, metadata14.name, (no
|
|
|
1137
1139
|
const waitBetweenTries = node.flags?.waitBetweenTries;
|
|
1138
1140
|
if (waitBetweenTries !== void 0 && waitBetweenTries !== null) {
|
|
1139
1141
|
if (typeof waitBetweenTries === "number") return null;
|
|
1140
|
-
if (typeof waitBetweenTries === "string" && !isNaN(Number(waitBetweenTries)) && !waitBetweenTries.includes("{{")) {
|
|
1142
|
+
if (typeof waitBetweenTries === "string" && !Number.isNaN(Number(waitBetweenTries)) && !waitBetweenTries.includes("{{")) {
|
|
1141
1143
|
return null;
|
|
1142
1144
|
}
|
|
1143
1145
|
}
|
|
@@ -1301,9 +1303,10 @@ function loadConfig(configPath) {
|
|
|
1301
1303
|
}
|
|
1302
1304
|
return loadConfigFromCwd();
|
|
1303
1305
|
}
|
|
1306
|
+
var fsOverride = null;
|
|
1304
1307
|
function loadConfigFromFile(configPath) {
|
|
1305
1308
|
try {
|
|
1306
|
-
const fs = require("fs");
|
|
1309
|
+
const fs = fsOverride || require("fs");
|
|
1307
1310
|
if (!fs.existsSync(configPath)) {
|
|
1308
1311
|
return defaultConfig;
|
|
1309
1312
|
}
|
|
@@ -1315,7 +1318,7 @@ function loadConfigFromFile(configPath) {
|
|
|
1315
1318
|
}
|
|
1316
1319
|
function loadConfigFromCwd() {
|
|
1317
1320
|
try {
|
|
1318
|
-
const fs = require("fs");
|
|
1321
|
+
const fs = fsOverride || require("fs");
|
|
1319
1322
|
const path = require("path");
|
|
1320
1323
|
const candidates = [".flowlint.yml", ".flowlint.yaml", "flowlint.config.yml"];
|
|
1321
1324
|
const cwd = process.cwd();
|