n8n-guard 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/LICENSE +21 -0
- package/README.md +199 -0
- package/dist/checks/dbHealth.d.ts +36 -0
- package/dist/checks/dbHealth.js +69 -0
- package/dist/checks/dbHealth.js.map +1 -0
- package/dist/checks/gitDrift.d.ts +41 -0
- package/dist/checks/gitDrift.js +193 -0
- package/dist/checks/gitDrift.js.map +1 -0
- package/dist/checks/retention.d.ts +29 -0
- package/dist/checks/retention.js +58 -0
- package/dist/checks/retention.js.map +1 -0
- package/dist/checks/stuckExecutions.d.ts +28 -0
- package/dist/checks/stuckExecutions.js +95 -0
- package/dist/checks/stuckExecutions.js.map +1 -0
- package/dist/config.d.ts +28 -0
- package/dist/config.js +36 -0
- package/dist/config.js.map +1 -0
- package/dist/guard/runner.d.ts +49 -0
- package/dist/guard/runner.js +95 -0
- package/dist/guard/runner.js.map +1 -0
- package/dist/guard/service.d.ts +42 -0
- package/dist/guard/service.js +128 -0
- package/dist/guard/service.js.map +1 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +16 -0
- package/dist/index.js.map +1 -0
- package/dist/n8n/api.d.ts +44 -0
- package/dist/n8n/api.js +77 -0
- package/dist/n8n/api.js.map +1 -0
- package/dist/n8n/datastore.d.ts +29 -0
- package/dist/n8n/datastore.js +169 -0
- package/dist/n8n/datastore.js.map +1 -0
- package/dist/n8n/sqlite.d.ts +13 -0
- package/dist/n8n/sqlite.js +17 -0
- package/dist/n8n/sqlite.js.map +1 -0
- package/dist/server.d.ts +5 -0
- package/dist/server.js +115 -0
- package/dist/server.js.map +1 -0
- package/package.json +56 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 flow-84
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
# n8n-guard
|
|
2
|
+
|
|
3
|
+
[](https://www.npmjs.com/package/n8n-guard)
|
|
4
|
+
[](https://registry.modelcontextprotocol.io/v0/servers?search=n8n-guard)
|
|
5
|
+
[](LICENSE)
|
|
6
|
+
[](https://nodejs.org)
|
|
7
|
+
|
|
8
|
+
An MCP server for the operational layer *underneath* the n8n Public API.
|
|
9
|
+
|
|
10
|
+
Every n8n MCP server on the registry today is a wrapper around the same public
|
|
11
|
+
endpoints: list workflows, list executions, create, activate, delete. That is
|
|
12
|
+
useful for building workflows. It is close to useless for keeping a self-hosted
|
|
13
|
+
instance alive, because the things that actually take an instance down are not
|
|
14
|
+
in that API at all.
|
|
15
|
+
|
|
16
|
+
n8n-guard answers the four questions the API cannot:
|
|
17
|
+
|
|
18
|
+
1. **How big is the datastore, and when does it become a problem?** The Public
|
|
19
|
+
API has no endpoint for database size. n8n-guard reads the SQLite file or
|
|
20
|
+
queries Postgres directly, breaks the size down per table, derives the growth
|
|
21
|
+
rate, and tells you how many days of headroom are left.
|
|
22
|
+
2. **Which executions are stuck?** Listing executions by status is not
|
|
23
|
+
detection. A run sitting in `running` looks identical to a healthy long job
|
|
24
|
+
until you compare it against what that workflow normally takes. n8n-guard
|
|
25
|
+
builds a per-workflow median from finished runs and flags the outliers.
|
|
26
|
+
3. **What is retention costing?** How many execution records are stored, how far
|
|
27
|
+
back they reach, and what a given retention window would free.
|
|
28
|
+
4. **Has the instance drifted from git?** Existing servers diff one API object
|
|
29
|
+
against another. n8n-guard diffs the running instance against the JSON
|
|
30
|
+
exports in a repository: live but never exported, exported but deleted,
|
|
31
|
+
and content that quietly diverged.
|
|
32
|
+
|
|
33
|
+
It is strictly read-only. n8n-guard never writes to your instance, never deletes
|
|
34
|
+
executions, and never activates or edits a workflow. It reports; you decide.
|
|
35
|
+
|
|
36
|
+
## Install
|
|
37
|
+
|
|
38
|
+
```bash
|
|
39
|
+
npx -y n8n-guard
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
Requires Node.js 22 or newer (it uses the built-in `node:sqlite`). For a
|
|
43
|
+
permanent install use `npm install -g n8n-guard`.
|
|
44
|
+
|
|
45
|
+
### Claude Desktop / any MCP client
|
|
46
|
+
|
|
47
|
+
```json
|
|
48
|
+
{
|
|
49
|
+
"mcpServers": {
|
|
50
|
+
"n8n-guard": {
|
|
51
|
+
"command": "n8n-guard",
|
|
52
|
+
"env": {
|
|
53
|
+
"N8N_URL": "https://n8n.example.com",
|
|
54
|
+
"N8N_API_KEY": "n8n_api_...",
|
|
55
|
+
"N8N_SQLITE_PATH": "/var/lib/n8n/.n8n/database.sqlite",
|
|
56
|
+
"N8N_GIT_REPO_PATH": "/srv/workflow-exports"
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
## Configuration
|
|
64
|
+
|
|
65
|
+
Everything is an environment variable. There are no hardcoded hosts, paths,
|
|
66
|
+
container names or accounts, so the server runs against any self-hosted
|
|
67
|
+
instance.
|
|
68
|
+
|
|
69
|
+
| Variable | Required | Default | Purpose |
|
|
70
|
+
|---|---|---|---|
|
|
71
|
+
| `N8N_URL` | no | `http://localhost:5678` | Base URL of the instance |
|
|
72
|
+
| `N8N_API_KEY` | for execution and workflow checks | none | Public API key (Settings, n8n API) |
|
|
73
|
+
| `N8N_SQLITE_PATH` | for datastore checks on SQLite | none | Path to `database.sqlite` |
|
|
74
|
+
| `N8N_POSTGRES_URL` | for datastore checks on Postgres | none | Connection string, takes precedence over SQLite |
|
|
75
|
+
| `N8N_GIT_REPO_PATH` | for the drift check | none | Checkout holding exported workflow JSON |
|
|
76
|
+
| `N8N_GUARD_STUCK_MINUTES` | no | `15` | Minutes in `running` before a run counts as stuck |
|
|
77
|
+
| `N8N_GUARD_DB_WARN_BYTES` | no | `1073741824` (1 GB) | Datastore warning threshold |
|
|
78
|
+
| `N8N_GUARD_DB_CRITICAL_BYTES` | no | `4294967296` (4 GB) | Datastore critical threshold |
|
|
79
|
+
| `N8N_GUARD_TIMEOUT_MS` | no | `15000` | Per-request API timeout |
|
|
80
|
+
|
|
81
|
+
Partial configuration is a supported mode, not an error. With only `N8N_URL` and
|
|
82
|
+
`N8N_SQLITE_PATH` you still get disk and retention answers; the API-backed checks
|
|
83
|
+
report that they are unconfigured and the rest of the run continues.
|
|
84
|
+
|
|
85
|
+
## Tools
|
|
86
|
+
|
|
87
|
+
| Tool | What it does |
|
|
88
|
+
|---|---|
|
|
89
|
+
| `instance_info` | Reachability, configured datastore, workflow inventory. Shows which checks are usable. |
|
|
90
|
+
| `db_health` | Datastore size, per-table breakdown, growth rate, projected days until the critical threshold. |
|
|
91
|
+
| `stuck_executions` | Runs stuck in `running`, judged against the absolute threshold and each workflow's own median. |
|
|
92
|
+
| `retention_report` | Execution count, age span, and what pruning to a retention window would free. |
|
|
93
|
+
| `git_drift` | Live workflows against the repository exports: missing, deleted, drifted, duplicated. |
|
|
94
|
+
| `guard_run` | All of the above in one resilient sweep. This is the one to schedule. |
|
|
95
|
+
|
|
96
|
+
## Example call
|
|
97
|
+
|
|
98
|
+
Point the server at an instance and let your client call a tool. Every tool
|
|
99
|
+
answers with a readable summary first and the full structured payload after it:
|
|
100
|
+
|
|
101
|
+
```
|
|
102
|
+
## Retention report
|
|
103
|
+
- 200 execution records spanning 8.3 days (24.1/day)
|
|
104
|
+
- nothing older than 30 days, retention is already tight
|
|
105
|
+
- n8n-guard never deletes. To act on this, set EXECUTIONS_DATA_PRUNE=true and
|
|
106
|
+
EXECUTIONS_DATA_MAX_AGE=720 (hours) on the n8n instance, then restart it.
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
followed by the structured payload the summary was derived from:
|
|
110
|
+
|
|
111
|
+
```json
|
|
112
|
+
{
|
|
113
|
+
"executionCount": 200,
|
|
114
|
+
"oldestExecutionAt": "2026-08-28T23:43:28.651Z",
|
|
115
|
+
"spanDays": 8.29,
|
|
116
|
+
"executionsPerDay": 24.12,
|
|
117
|
+
"buckets": [
|
|
118
|
+
{ "olderThanDays": 7, "rows": 32, "share": 0.16, "estimatedBytes": 3277 },
|
|
119
|
+
{ "olderThanDays": 30, "rows": 0, "share": 0, "estimatedBytes": 0 }
|
|
120
|
+
]
|
|
121
|
+
}
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
`guard_run` wraps all six checks. With only a datastore configured it reports
|
|
125
|
+
the gap instead of aborting:
|
|
126
|
+
|
|
127
|
+
```
|
|
128
|
+
## Guard run (degraded)
|
|
129
|
+
- stuck_executions failed after 3 attempt(s): N8N_API_KEY is not set, so the n8n Public API cannot be queried
|
|
130
|
+
- git_drift failed after 2 attempt(s): N8N_GIT_REPO_PATH is not set, so there is nothing to diff the instance against
|
|
131
|
+
- 4 of 6 checks completed despite the failures above
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
## Recovery: `guard_run` survives a partial outage
|
|
135
|
+
|
|
136
|
+
A monitoring run that aborts on the first unreachable dependency is worthless
|
|
137
|
+
precisely when it matters. The API being down *is* the incident, and it is no
|
|
138
|
+
reason to stop looking at the disk.
|
|
139
|
+
|
|
140
|
+
`guard_run` isolates every step. A failing step is retried with backoff, and if
|
|
141
|
+
it still fails it becomes a finding while the remaining checks run to completion.
|
|
142
|
+
Steps that depend on a failed step are skipped with the reason attached rather
|
|
143
|
+
than crashing on missing input. The report says so plainly:
|
|
144
|
+
|
|
145
|
+
```
|
|
146
|
+
## Guard run (degraded)
|
|
147
|
+
- stuck_executions failed after 3 attempt(s): cannot reach http://n8n.internal:5678: fetch failed
|
|
148
|
+
- git_drift skipped: depends on instance, which failed
|
|
149
|
+
- 4 of 6 checks completed despite the failures above
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
This behaviour is covered by tests in `test/unit/runner.test.ts` and
|
|
153
|
+
`test/unit/serviceRecovery.test.ts`, the latter running a real SQLite datastore
|
|
154
|
+
against a deliberately dead API endpoint.
|
|
155
|
+
|
|
156
|
+
## Try it on a throwaway instance
|
|
157
|
+
|
|
158
|
+
The compose file starts a fresh n8n you can safely point the server at. The data
|
|
159
|
+
directory is bind-mounted, which is exactly how n8n-guard reads a real
|
|
160
|
+
deployment.
|
|
161
|
+
|
|
162
|
+
```bash
|
|
163
|
+
git clone https://github.com/flow-84/n8n-guard && cd n8n-guard
|
|
164
|
+
npm install && npm run build
|
|
165
|
+
|
|
166
|
+
docker compose up -d
|
|
167
|
+
./scripts/demo-setup.sh # owner, API key, workflows, and a drifted export folder
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
The script generates the owner password, keeps it in the gitignored
|
|
171
|
+
`.n8n-demo-credentials`, and prints it along with the environment block to run
|
|
172
|
+
the server with. It seeds one
|
|
173
|
+
workflow that matches its export, one that was never exported, and one export
|
|
174
|
+
whose workflow does not exist, so `git_drift` has something real to find.
|
|
175
|
+
|
|
176
|
+
```bash
|
|
177
|
+
export N8N_URL=http://localhost:5678
|
|
178
|
+
export N8N_API_KEY=... # printed by the script
|
|
179
|
+
export N8N_SQLITE_PATH=$PWD/.n8n-demo/database.sqlite
|
|
180
|
+
export N8N_GIT_REPO_PATH=$PWD/.n8n-demo-exports
|
|
181
|
+
npm start
|
|
182
|
+
```
|
|
183
|
+
|
|
184
|
+
Use `N8N_DEMO_PORT=5679 docker compose up -d` if port 5678 is already taken by
|
|
185
|
+
an instance you care about.
|
|
186
|
+
|
|
187
|
+
## Tests
|
|
188
|
+
|
|
189
|
+
```bash
|
|
190
|
+
npm test # unit tests, no instance required
|
|
191
|
+
npm run test:integration # against the compose instance above
|
|
192
|
+
```
|
|
193
|
+
|
|
194
|
+
The integration suite skips the API-backed cases when `N8N_API_KEY` is unset, so
|
|
195
|
+
it stays runnable on a bare `docker compose up`.
|
|
196
|
+
|
|
197
|
+
## License
|
|
198
|
+
|
|
199
|
+
MIT
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import type { DatastoreStats } from "../n8n/datastore.js";
|
|
2
|
+
export type Severity = "ok" | "warning" | "critical";
|
|
3
|
+
export interface DbHealthThresholds {
|
|
4
|
+
warnBytes: number;
|
|
5
|
+
criticalBytes: number;
|
|
6
|
+
}
|
|
7
|
+
export interface DbHealthReport {
|
|
8
|
+
kind: DatastoreStats["kind"];
|
|
9
|
+
totalBytes: number;
|
|
10
|
+
totalHuman: string;
|
|
11
|
+
severity: Severity;
|
|
12
|
+
components: {
|
|
13
|
+
name: string;
|
|
14
|
+
bytes: number;
|
|
15
|
+
}[];
|
|
16
|
+
tables: {
|
|
17
|
+
table: string;
|
|
18
|
+
rows: number;
|
|
19
|
+
bytes: number | null;
|
|
20
|
+
share: number | null;
|
|
21
|
+
}[];
|
|
22
|
+
/** Average bytes added per day, derived from the age of the oldest execution. */
|
|
23
|
+
growthBytesPerDay: number | null;
|
|
24
|
+
/** Days until the datastore reaches `criticalBytes` at the observed growth rate. */
|
|
25
|
+
daysUntilCritical: number | null;
|
|
26
|
+
/** Bytes stored per execution record; the lever that retention pruning pulls on. */
|
|
27
|
+
bytesPerExecution: number | null;
|
|
28
|
+
findings: string[];
|
|
29
|
+
}
|
|
30
|
+
export declare function humanBytes(bytes: number): string;
|
|
31
|
+
/**
|
|
32
|
+
* Pure assessment of a datastore snapshot. The n8n Public API exposes none of
|
|
33
|
+
* this: it has no endpoint for database size, so an API wrapper cannot warn
|
|
34
|
+
* before the disk fills up and the instance stops accepting executions.
|
|
35
|
+
*/
|
|
36
|
+
export declare function assessDatabaseHealth(stats: DatastoreStats, thresholds: DbHealthThresholds, now?: number): DbHealthReport;
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
export function humanBytes(bytes) {
|
|
2
|
+
const units = ["B", "KB", "MB", "GB", "TB"];
|
|
3
|
+
let value = Math.abs(bytes);
|
|
4
|
+
let unit = 0;
|
|
5
|
+
while (value >= 1024 && unit < units.length - 1) {
|
|
6
|
+
value /= 1024;
|
|
7
|
+
unit += 1;
|
|
8
|
+
}
|
|
9
|
+
const sign = bytes < 0 ? "-" : "";
|
|
10
|
+
return `${sign}${value.toFixed(value >= 100 || unit === 0 ? 0 : 1)} ${units[unit]}`;
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* Pure assessment of a datastore snapshot. The n8n Public API exposes none of
|
|
14
|
+
* this: it has no endpoint for database size, so an API wrapper cannot warn
|
|
15
|
+
* before the disk fills up and the instance stops accepting executions.
|
|
16
|
+
*/
|
|
17
|
+
export function assessDatabaseHealth(stats, thresholds, now = Date.now()) {
|
|
18
|
+
const findings = [];
|
|
19
|
+
let severity = "ok";
|
|
20
|
+
if (stats.totalBytes >= thresholds.criticalBytes)
|
|
21
|
+
severity = "critical";
|
|
22
|
+
else if (stats.totalBytes >= thresholds.warnBytes)
|
|
23
|
+
severity = "warning";
|
|
24
|
+
if (severity !== "ok") {
|
|
25
|
+
findings.push(`datastore is ${humanBytes(stats.totalBytes)}, at or past the ${severity} threshold of ` +
|
|
26
|
+
`${humanBytes(severity === "critical" ? thresholds.criticalBytes : thresholds.warnBytes)}`);
|
|
27
|
+
}
|
|
28
|
+
const oldest = stats.oldestExecutionAt ? Date.parse(stats.oldestExecutionAt) : NaN;
|
|
29
|
+
const ageDays = Number.isNaN(oldest) ? null : Math.max((now - oldest) / 86_400_000, 1 / 24);
|
|
30
|
+
const growthBytesPerDay = ageDays === null ? null : stats.totalBytes / ageDays;
|
|
31
|
+
let daysUntilCritical = null;
|
|
32
|
+
if (growthBytesPerDay !== null && growthBytesPerDay > 0) {
|
|
33
|
+
const headroom = thresholds.criticalBytes - stats.totalBytes;
|
|
34
|
+
daysUntilCritical = headroom <= 0 ? 0 : headroom / growthBytesPerDay;
|
|
35
|
+
if (daysUntilCritical <= 30) {
|
|
36
|
+
findings.push(`at ${humanBytes(growthBytesPerDay)}/day the datastore reaches ` +
|
|
37
|
+
`${humanBytes(thresholds.criticalBytes)} in about ${daysUntilCritical.toFixed(1)} days`);
|
|
38
|
+
if (severity === "ok")
|
|
39
|
+
severity = "warning";
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
const bytesPerExecution = stats.executionCount > 0 ? stats.totalBytes / stats.executionCount : null;
|
|
43
|
+
const tables = stats.tables
|
|
44
|
+
.map((t) => ({ ...t, share: t.bytes !== null && stats.totalBytes > 0 ? t.bytes / stats.totalBytes : null }))
|
|
45
|
+
.sort((a, b) => (b.bytes ?? 0) - (a.bytes ?? 0) || b.rows - a.rows);
|
|
46
|
+
const dominant = tables.find((t) => t.share !== null && t.share >= 0.5);
|
|
47
|
+
if (dominant) {
|
|
48
|
+
findings.push(`${dominant.table} holds ${(dominant.share * 100).toFixed(0)}% of the datastore`);
|
|
49
|
+
}
|
|
50
|
+
if (stats.kind === "sqlite" && tables.every((t) => t.bytes === null)) {
|
|
51
|
+
findings.push("per-table sizes unavailable: this SQLite build lacks the dbstat module, row counts only");
|
|
52
|
+
}
|
|
53
|
+
if (findings.length === 0) {
|
|
54
|
+
findings.push(`datastore is ${humanBytes(stats.totalBytes)} and below all thresholds`);
|
|
55
|
+
}
|
|
56
|
+
return {
|
|
57
|
+
kind: stats.kind,
|
|
58
|
+
totalBytes: stats.totalBytes,
|
|
59
|
+
totalHuman: humanBytes(stats.totalBytes),
|
|
60
|
+
severity,
|
|
61
|
+
components: stats.components,
|
|
62
|
+
tables,
|
|
63
|
+
growthBytesPerDay,
|
|
64
|
+
daysUntilCritical,
|
|
65
|
+
bytesPerExecution,
|
|
66
|
+
findings,
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
//# sourceMappingURL=dbHealth.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"dbHealth.js","sourceRoot":"","sources":["../../src/checks/dbHealth.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import type { N8nWorkflow } from "../n8n/api.js";
|
|
2
|
+
export interface ExportedWorkflow {
|
|
3
|
+
id: string | null;
|
|
4
|
+
name: string;
|
|
5
|
+
nodes?: unknown[];
|
|
6
|
+
connections?: Record<string, unknown>;
|
|
7
|
+
/** Repo-relative path of the file this was read from. */
|
|
8
|
+
file: string;
|
|
9
|
+
}
|
|
10
|
+
export type DriftKind = "missing_in_git" | "missing_in_instance" | "content_drift" | "duplicate_export";
|
|
11
|
+
export interface DriftEntry {
|
|
12
|
+
kind: DriftKind;
|
|
13
|
+
workflowId: string | null;
|
|
14
|
+
name: string;
|
|
15
|
+
file: string | null;
|
|
16
|
+
active: boolean | null;
|
|
17
|
+
detail: string;
|
|
18
|
+
}
|
|
19
|
+
export interface GitDriftReport {
|
|
20
|
+
liveCount: number;
|
|
21
|
+
exportedCount: number;
|
|
22
|
+
inSync: number;
|
|
23
|
+
drift: DriftEntry[];
|
|
24
|
+
findings: string[];
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Fingerprint of what a workflow *does*: node types, their parameters and the
|
|
28
|
+
* wiring. Canvas positions, timestamps and per-instance ids are excluded, so a
|
|
29
|
+
* dragged node is not reported as drift.
|
|
30
|
+
*/
|
|
31
|
+
export declare function fingerprintWorkflow(workflow: {
|
|
32
|
+
nodes?: unknown[];
|
|
33
|
+
connections?: unknown;
|
|
34
|
+
}): string;
|
|
35
|
+
/**
|
|
36
|
+
* The diff every existing n8n MCP server is missing: it compares two API
|
|
37
|
+
* objects, not the running instance against what is versioned in git.
|
|
38
|
+
*/
|
|
39
|
+
export declare function diffWorkflows(live: N8nWorkflow[], exported: ExportedWorkflow[]): GitDriftReport;
|
|
40
|
+
/** Reads every workflow-shaped JSON file under `repoPath`, recursively. */
|
|
41
|
+
export declare function loadExportedWorkflows(repoPath: string): Promise<ExportedWorkflow[]>;
|
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { readdir, readFile, stat } from "node:fs/promises";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
/**
|
|
5
|
+
* Fingerprint of what a workflow *does*: node types, their parameters and the
|
|
6
|
+
* wiring. Canvas positions, timestamps and per-instance ids are excluded, so a
|
|
7
|
+
* dragged node is not reported as drift.
|
|
8
|
+
*/
|
|
9
|
+
export function fingerprintWorkflow(workflow) {
|
|
10
|
+
const nodes = Array.isArray(workflow.nodes) ? workflow.nodes : [];
|
|
11
|
+
const normalizedNodes = nodes
|
|
12
|
+
.map((raw) => {
|
|
13
|
+
const node = (raw ?? {});
|
|
14
|
+
return {
|
|
15
|
+
name: node.name ?? null,
|
|
16
|
+
type: node.type ?? null,
|
|
17
|
+
typeVersion: node.typeVersion ?? null,
|
|
18
|
+
disabled: node.disabled ?? false,
|
|
19
|
+
parameters: node.parameters ?? {},
|
|
20
|
+
};
|
|
21
|
+
})
|
|
22
|
+
.sort((a, b) => String(a.name).localeCompare(String(b.name)));
|
|
23
|
+
return createHash("sha256")
|
|
24
|
+
.update(stableStringify({ nodes: normalizedNodes, connections: workflow.connections ?? {} }))
|
|
25
|
+
.digest("hex");
|
|
26
|
+
}
|
|
27
|
+
function stableStringify(value) {
|
|
28
|
+
if (value === null || typeof value !== "object")
|
|
29
|
+
return JSON.stringify(value) ?? "null";
|
|
30
|
+
if (Array.isArray(value))
|
|
31
|
+
return `[${value.map(stableStringify).join(",")}]`;
|
|
32
|
+
const entries = Object.entries(value)
|
|
33
|
+
.filter(([, v]) => v !== undefined)
|
|
34
|
+
.sort(([a], [b]) => a.localeCompare(b));
|
|
35
|
+
return `{${entries.map(([k, v]) => `${JSON.stringify(k)}:${stableStringify(v)}`).join(",")}}`;
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* The diff every existing n8n MCP server is missing: it compares two API
|
|
39
|
+
* objects, not the running instance against what is versioned in git.
|
|
40
|
+
*/
|
|
41
|
+
export function diffWorkflows(live, exported) {
|
|
42
|
+
const available = new Set(exported);
|
|
43
|
+
const claimed = new Map();
|
|
44
|
+
const pick = (predicate) => {
|
|
45
|
+
for (const item of available)
|
|
46
|
+
if (predicate(item))
|
|
47
|
+
return item;
|
|
48
|
+
return undefined;
|
|
49
|
+
};
|
|
50
|
+
// Two passes, because an export may only be claimed once. Duplicating a
|
|
51
|
+
// workflow in n8n keeps the name, so same-name pairs are the normal case:
|
|
52
|
+
// matching by name first would let one of them consume the export another
|
|
53
|
+
// workflow owns by id, and the unexported twin would silently look in sync.
|
|
54
|
+
const assignment = new Map();
|
|
55
|
+
for (const workflow of live) {
|
|
56
|
+
const match = pick((item) => item.id !== null && item.id === String(workflow.id));
|
|
57
|
+
if (match) {
|
|
58
|
+
available.delete(match);
|
|
59
|
+
claimed.set(match, workflow);
|
|
60
|
+
assignment.set(workflow, match);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
for (const workflow of live) {
|
|
64
|
+
if (assignment.has(workflow))
|
|
65
|
+
continue;
|
|
66
|
+
const match = pick((item) => item.name === workflow.name);
|
|
67
|
+
if (match) {
|
|
68
|
+
available.delete(match);
|
|
69
|
+
claimed.set(match, workflow);
|
|
70
|
+
assignment.set(workflow, match);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
const drift = [];
|
|
74
|
+
let inSync = 0;
|
|
75
|
+
for (const workflow of live) {
|
|
76
|
+
const match = assignment.get(workflow);
|
|
77
|
+
if (!match) {
|
|
78
|
+
drift.push({
|
|
79
|
+
kind: "missing_in_git",
|
|
80
|
+
workflowId: String(workflow.id),
|
|
81
|
+
name: workflow.name,
|
|
82
|
+
file: null,
|
|
83
|
+
active: workflow.active,
|
|
84
|
+
detail: workflow.active
|
|
85
|
+
? "active in the instance but not exported to the repository"
|
|
86
|
+
: "present in the instance but not exported to the repository",
|
|
87
|
+
});
|
|
88
|
+
continue;
|
|
89
|
+
}
|
|
90
|
+
if (fingerprintWorkflow(workflow) === fingerprintWorkflow(match)) {
|
|
91
|
+
inSync += 1;
|
|
92
|
+
}
|
|
93
|
+
else {
|
|
94
|
+
drift.push({
|
|
95
|
+
kind: "content_drift",
|
|
96
|
+
workflowId: String(workflow.id),
|
|
97
|
+
name: workflow.name,
|
|
98
|
+
file: match.file,
|
|
99
|
+
active: workflow.active,
|
|
100
|
+
detail: `nodes or connections differ between the instance and ${match.file}`,
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
// Whatever is left over was claimed by nobody. If a live workflow already
|
|
105
|
+
// took an export with the same identity, the leftovers are redundant copies
|
|
106
|
+
// of it; otherwise the workflow they describe is gone from the instance.
|
|
107
|
+
const identities = new Map();
|
|
108
|
+
for (const [item, workflow] of claimed) {
|
|
109
|
+
if (item.id)
|
|
110
|
+
identities.set(`id:${item.id}`, workflow);
|
|
111
|
+
identities.set(`name:${item.name}`, workflow);
|
|
112
|
+
}
|
|
113
|
+
for (const item of available) {
|
|
114
|
+
const owner = (item.id ? identities.get(`id:${item.id}`) : undefined) ?? identities.get(`name:${item.name}`);
|
|
115
|
+
if (owner) {
|
|
116
|
+
drift.push({
|
|
117
|
+
kind: "duplicate_export",
|
|
118
|
+
workflowId: String(owner.id),
|
|
119
|
+
name: owner.name,
|
|
120
|
+
file: item.file,
|
|
121
|
+
active: owner.active,
|
|
122
|
+
detail: `a second export file claims this workflow, alongside ${assignment.get(owner).file}`,
|
|
123
|
+
});
|
|
124
|
+
continue;
|
|
125
|
+
}
|
|
126
|
+
drift.push({
|
|
127
|
+
kind: "missing_in_instance",
|
|
128
|
+
workflowId: item.id,
|
|
129
|
+
name: item.name,
|
|
130
|
+
file: item.file,
|
|
131
|
+
active: null,
|
|
132
|
+
detail: "exported in the repository but not present in the instance (deleted or never imported)",
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
const counts = drift.reduce((acc, entry) => {
|
|
136
|
+
acc[entry.kind] = (acc[entry.kind] ?? 0) + 1;
|
|
137
|
+
return acc;
|
|
138
|
+
}, {});
|
|
139
|
+
const findings = drift.length === 0
|
|
140
|
+
? [`all ${live.length} workflows match their exports`]
|
|
141
|
+
: [
|
|
142
|
+
`${drift.length} differences across ${live.length} live and ${exported.length} exported workflows`,
|
|
143
|
+
...Object.entries(counts).map(([kind, n]) => `${kind}: ${n}`),
|
|
144
|
+
];
|
|
145
|
+
return { liveCount: live.length, exportedCount: exported.length, inSync, drift, findings };
|
|
146
|
+
}
|
|
147
|
+
const SKIP_DIRS = new Set([".git", "node_modules", ".github", "dist", "build"]);
|
|
148
|
+
/** Reads every workflow-shaped JSON file under `repoPath`, recursively. */
|
|
149
|
+
export async function loadExportedWorkflows(repoPath) {
|
|
150
|
+
const root = path.resolve(repoPath);
|
|
151
|
+
const info = await stat(root).catch(() => null);
|
|
152
|
+
if (!info?.isDirectory()) {
|
|
153
|
+
throw new Error(`N8N_GIT_REPO_PATH is not a readable directory: ${repoPath}`);
|
|
154
|
+
}
|
|
155
|
+
const found = [];
|
|
156
|
+
const walk = async (dir) => {
|
|
157
|
+
const entries = await readdir(dir, { withFileTypes: true }).catch(() => []);
|
|
158
|
+
for (const entry of entries) {
|
|
159
|
+
const full = path.join(dir, entry.name);
|
|
160
|
+
if (entry.isDirectory()) {
|
|
161
|
+
if (!SKIP_DIRS.has(entry.name))
|
|
162
|
+
await walk(full);
|
|
163
|
+
continue;
|
|
164
|
+
}
|
|
165
|
+
if (!entry.name.endsWith(".json"))
|
|
166
|
+
continue;
|
|
167
|
+
const parsed = await readFile(full, "utf8").then((text) => JSON.parse(text)).catch(() => null);
|
|
168
|
+
for (const candidate of Array.isArray(parsed) ? parsed : [parsed]) {
|
|
169
|
+
const workflow = toExported(candidate, path.relative(root, full));
|
|
170
|
+
if (workflow)
|
|
171
|
+
found.push(workflow);
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
};
|
|
175
|
+
await walk(root);
|
|
176
|
+
return found;
|
|
177
|
+
}
|
|
178
|
+
function toExported(value, file) {
|
|
179
|
+
if (!value || typeof value !== "object")
|
|
180
|
+
return null;
|
|
181
|
+
const record = value;
|
|
182
|
+
// A workflow export always carries nodes; package.json and friends do not.
|
|
183
|
+
if (!Array.isArray(record.nodes))
|
|
184
|
+
return null;
|
|
185
|
+
return {
|
|
186
|
+
id: record.id === undefined || record.id === null ? null : String(record.id),
|
|
187
|
+
name: typeof record.name === "string" ? record.name : path.basename(file, ".json"),
|
|
188
|
+
nodes: record.nodes,
|
|
189
|
+
connections: (record.connections ?? {}),
|
|
190
|
+
file,
|
|
191
|
+
};
|
|
192
|
+
}
|
|
193
|
+
//# sourceMappingURL=gitDrift.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"gitDrift.js","sourceRoot":"","sources":["../../src/checks/gitDrift.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import type { DatastoreStats } from "../n8n/datastore.js";
|
|
2
|
+
export interface RetentionReport {
|
|
3
|
+
executionCount: number;
|
|
4
|
+
oldestExecutionAt: string | null;
|
|
5
|
+
newestExecutionAt: string | null;
|
|
6
|
+
spanDays: number | null;
|
|
7
|
+
executionsPerDay: number | null;
|
|
8
|
+
buckets: {
|
|
9
|
+
olderThanDays: number;
|
|
10
|
+
rows: number;
|
|
11
|
+
share: number;
|
|
12
|
+
estimatedBytes: number | null;
|
|
13
|
+
}[];
|
|
14
|
+
recommendation: {
|
|
15
|
+
keepDays: number;
|
|
16
|
+
prunableRows: number;
|
|
17
|
+
estimatedBytesFreed: number | null;
|
|
18
|
+
estimatedHumanFreed: string | null;
|
|
19
|
+
} | null;
|
|
20
|
+
findings: string[];
|
|
21
|
+
/** n8n prunes on its own only when these are set; guard reports, it never writes. */
|
|
22
|
+
hint: string;
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Turns raw execution counts into "what does deleting old runs actually buy me".
|
|
26
|
+
* Byte figures are estimates: they assume execution rows cost the same on
|
|
27
|
+
* average, which is what the row-count-to-size ratio can support.
|
|
28
|
+
*/
|
|
29
|
+
export declare function analyzeRetention(stats: DatastoreStats, keepDays?: number, now?: number): RetentionReport;
|