@silentjma/n8n-nodes-unraid 0.1.0 → 0.1.2
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/CHANGELOG.md +26 -0
- package/README.md +12 -3
- package/dist/credentials/UnraidApi.credentials.js +13 -2
- package/dist/lib/client.js +2 -1
- package/dist/lib/logs.js +11 -4
- package/dist/nodes/Unraid/Unraid.node.js +14 -6
- package/dist/nodes/UnraidTrigger/UnraidTrigger.node.js +6 -2
- package/package.json +5 -3
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,32 @@ All notable changes to this package. Versions follow [semver](https://semver.org
|
|
|
4
4
|
|
|
5
5
|
## Unreleased
|
|
6
6
|
|
|
7
|
+
## 0.1.2 - 2026-10-26
|
|
8
|
+
|
|
9
|
+
Fixes n8n Creator Portal vetting ("Can't find credential file in repo").
|
|
10
|
+
|
|
11
|
+
### Changed
|
|
12
|
+
- Sources moved to n8n's standard layout: `credentials/`, `nodes/` and `lib/` at the repo root, no longer under `src/`. n8n's tools map `package.json`'s `dist/credentials/X.js` to `credentials/X.ts` in the repo. The published files and their `dist/` paths are unchanged.
|
|
13
|
+
- The credential's *Ignore SSL Issues* field is now named `ignoreSslIssues`. n8n's lint rules treat any field name containing "cert" as a secret. Credentials saved with the old `allowUnauthorizedCerts` field keep working: it is still declared (hidden) and still read.
|
|
14
|
+
- `npm run lint` no longer needs exemptions for n8n's `no-credential-reuse` and `credential-password-field` rules.
|
|
15
|
+
|
|
16
|
+
## 0.1.1 - 2026-10-26
|
|
17
|
+
|
|
18
|
+
Passes n8n's community package scanner (`npx @n8n/scan-community-package @silentjma/n8n-nodes-unraid`).
|
|
19
|
+
|
|
20
|
+
### Fixed
|
|
21
|
+
- `package.json`: `author` now includes the npm owner's email. The scanner requires an author email (`valid-author`), and n8n's Creator Portal reads it from npm to match the package to its owner ("Error getting author email from npm").
|
|
22
|
+
- Nodes declare their inputs and outputs with `NodeConnectionTypes.Main`.
|
|
23
|
+
- Errors from an operation are always raised as `NodeApiError` or `NodeOperationError`, with the item index, so n8n shows them properly per item.
|
|
24
|
+
- An invalid log filter pattern now fails with a clear node error. It no longer throws a plain `Error` from the log helpers.
|
|
25
|
+
|
|
26
|
+
### Added
|
|
27
|
+
- Credential icon. Node icons use the light/dark form.
|
|
28
|
+
- `npm run lint` now includes n8n's community node rules (`@n8n/eslint-plugin-community-nodes`, the ruleset the scanner runs), so CI catches scan failures before a release. ESLint is pinned to 9.29.0 to match that plugin.
|
|
29
|
+
|
|
30
|
+
### Docs
|
|
31
|
+
- Telegram needs a public HTTPS webhook. Set `N8N_WEBHOOK_URL`; on n8n 2.x, `WEBHOOK_URL` alone leaves test webhooks on the editor URL.
|
|
32
|
+
|
|
7
33
|
### Project
|
|
8
34
|
- Community files: CONTRIBUTING, CODE_OF_CONDUCT, SECURITY, SUPPORT, issue and pull request templates, Dependabot.
|
|
9
35
|
- CI on every push and pull request. A release workflow publishes to npm with provenance when a `v*` tag is pushed.
|
package/README.md
CHANGED
|
@@ -10,10 +10,12 @@ Unofficial n8n community nodes for the [Unraid API](https://docs.unraid.net/API/
|
|
|
10
10
|
- **Unraid**: an action node for System, Array, Disk, Share, Docker Container, Virtual Machine, Log File and Notification, plus a Custom GraphQL operation.
|
|
11
11
|
- **Unraid Trigger**: a polling trigger for new log lines (syslog or a container log), health problems, notifications, container state changes and container updates.
|
|
12
12
|
|
|
13
|
-
Both nodes use the same built-in rules ([
|
|
13
|
+
Both nodes use the same built-in rules ([lib/analysis.ts](lib/analysis.ts)). The rules tag log lines and health findings as `info`, `warning` or `critical` and attach **suggested next steps**.
|
|
14
14
|
|
|
15
15
|
## Installation
|
|
16
16
|
|
|
17
|
+
📦 **npm:** [@silentjma/n8n-nodes-unraid](https://www.npmjs.com/package/@silentjma/n8n-nodes-unraid)
|
|
18
|
+
|
|
17
19
|
**Community Nodes (recommended):** in n8n, open **Settings › Community Nodes › Install** and enter `@silentjma/n8n-nodes-unraid`.
|
|
18
20
|
|
|
19
21
|
**Manual / custom folder:** run `npm pack` and extract the tarball's `package/` folder into `~/.n8n/custom/n8n-nodes-unraid`. In Docker that is `/home/node/.n8n/custom`, which on Unraid is usually `/mnt/user/appdata/n8n/custom`. Then restart n8n.
|
|
@@ -46,6 +48,9 @@ Prefer workflows without a community node? [n8n-unraid-telegram-workflows](https
|
|
|
46
48
|
| Notification | Create (optionally only if unique), Get Many, Get Overview, Get Warnings and Alerts, Archive, Archive All |
|
|
47
49
|
| Custom GraphQL | Execute any query or mutation |
|
|
48
50
|
|
|
51
|
+
|
|
52
|
+
<img width="638" height="763" alt="{0E837C8A-E0E8-4CB5-903F-C4CD4459B37A}" src="https://github.com/user-attachments/assets/a3143eab-9a77-4ef6-99b7-35f639eb996e" />
|
|
53
|
+
|
|
49
54
|
Containers can be given by **name** (`plex`) or ID.
|
|
50
55
|
|
|
51
56
|
## Unraid Trigger events
|
|
@@ -71,6 +76,9 @@ On the first poll, the trigger only records a baseline. Turn on *Emit Existing o
|
|
|
71
76
|
| [Unraid · Docker updates & actions](workflows/unraid-docker.json) | Weekly (Sun 04:00), and called by the bot | Weekly: updates every container that has a new image, except `cfg.excludeFromUpdates`. It reports only when something changed. Also answers the Docker commands. |
|
|
72
77
|
| [Unraid · Sysadmin & Telegram bot](workflows/unraid-sysadmin-bot.json) | Telegram Trigger | The bot. Answers the sysadmin commands and forwards Docker commands to the Docker workflow. |
|
|
73
78
|
|
|
79
|
+
<img width="1184" height="743" alt="image" src="https://github.com/user-attachments/assets/606adfce-023e-4d05-b873-b86bea7c8f0e" />
|
|
80
|
+
|
|
81
|
+
|
|
74
82
|
A Telegram bot delivers updates to a single webhook, so **only the bot workflow has a Telegram Trigger**. The other two just send messages.
|
|
75
83
|
|
|
76
84
|
### Commands
|
|
@@ -104,7 +112,8 @@ help - All commands
|
|
|
104
112
|
### Setup
|
|
105
113
|
|
|
106
114
|
1. Create a bot with @BotFather and add a **Telegram** credential in n8n with its token.
|
|
107
|
-
|
|
115
|
+
n8n must be reachable at a **public HTTPS** address (for example a Cloudflare Tunnel or a reverse proxy), and the n8n container needs `N8N_WEBHOOK_URL=https://<that host>/`, otherwise Telegram rejects the trigger with *Bad request*. See [SUPPORT.md](SUPPORT.md).
|
|
116
|
+
2. Import the three workflows. In each one, select the **Unraid API** credential (your server as `https://<unraid-ip>`, Ignore SSL Issues on) and the Telegram credential.
|
|
108
117
|
3. Publish the **Sysadmin & Telegram bot** workflow and send the bot any message. While `cfg.allowedChatIds` is empty, the bot only replies with your chat ID and runs nothing else.
|
|
109
118
|
4. Put that ID in **Bot settings › cfg.allowedChatIds**, **Alert settings › cfg.telegramChatId** and **Settings › cfg.telegramChatId** (Docker workflow). After that, the bot ignores every other chat.
|
|
110
119
|
5. In the bot's **Run Docker workflow** node, pick *Unraid · Docker updates & actions*. Alternatively, regenerate with `DOCKER_WORKFLOW_ID=<id> npm run workflow` and re-import.
|
|
@@ -124,7 +133,7 @@ npm run build && npm run lint && npm run typecheck && npm test
|
|
|
124
133
|
npm pack # -> silentjma-n8n-nodes-unraid-0.1.0.tgz
|
|
125
134
|
```
|
|
126
135
|
|
|
127
|
-
The queries in [
|
|
136
|
+
The queries in [lib/queries.ts](lib/queries.ts) are written against `api/generated-schema.graphql` from the unraid/api repository. Check them again when Unraid changes the schema.
|
|
128
137
|
|
|
129
138
|
## Support and contributing
|
|
130
139
|
|
|
@@ -4,6 +4,7 @@ exports.UnraidApi = void 0;
|
|
|
4
4
|
class UnraidApi {
|
|
5
5
|
name = 'unraidApi';
|
|
6
6
|
displayName = 'Unraid API';
|
|
7
|
+
icon = { light: 'file:../nodes/Unraid/unraid.svg', dark: 'file:../nodes/Unraid/unraid.svg' };
|
|
7
8
|
documentationUrl = 'https://docs.unraid.net/API/how-to-use-the-api/';
|
|
8
9
|
properties = [
|
|
9
10
|
{
|
|
@@ -26,11 +27,21 @@ class UnraidApi {
|
|
|
26
27
|
},
|
|
27
28
|
{
|
|
28
29
|
displayName: 'Ignore SSL Issues',
|
|
29
|
-
name: '
|
|
30
|
+
name: 'ignoreSslIssues',
|
|
30
31
|
type: 'boolean',
|
|
31
32
|
default: false,
|
|
32
33
|
description: 'Whether to connect even if the server uses a self-signed or otherwise invalid certificate (the default for Unraid on a LAN IP)',
|
|
33
34
|
},
|
|
35
|
+
// Pre-0.1.2 name of "Ignore SSL Issues". Kept hidden so credentials saved with it keep working:
|
|
36
|
+
// n8n only loads stored values for declared fields. The password option satisfies n8n's name-based
|
|
37
|
+
// lint rule (the name contains "cert"); it has no effect on a hidden field.
|
|
38
|
+
{
|
|
39
|
+
displayName: 'Ignore SSL Issues (Legacy)',
|
|
40
|
+
name: 'allowUnauthorizedCerts',
|
|
41
|
+
type: 'hidden',
|
|
42
|
+
typeOptions: { password: true },
|
|
43
|
+
default: false,
|
|
44
|
+
},
|
|
34
45
|
];
|
|
35
46
|
authenticate = {
|
|
36
47
|
type: 'generic',
|
|
@@ -45,7 +56,7 @@ class UnraidApi {
|
|
|
45
56
|
baseURL: '={{$credentials.serverUrl.replace(/\\/+$/, "")}}',
|
|
46
57
|
url: '/graphql',
|
|
47
58
|
method: 'POST',
|
|
48
|
-
skipSslCertificateValidation: '={{$credentials.allowUnauthorizedCerts}}',
|
|
59
|
+
skipSslCertificateValidation: '={{$credentials.ignoreSslIssues === true || $credentials.allowUnauthorizedCerts === true}}',
|
|
49
60
|
headers: { 'Content-Type': 'application/json' },
|
|
50
61
|
body: { query: '{ online info { os { hostname } } }' },
|
|
51
62
|
},
|
package/dist/lib/client.js
CHANGED
|
@@ -16,7 +16,8 @@ async function unraidGraphql(query, variables = {}) {
|
|
|
16
16
|
headers: { 'Content-Type': 'application/json', Accept: 'application/json' },
|
|
17
17
|
body: { query, variables },
|
|
18
18
|
json: true,
|
|
19
|
-
|
|
19
|
+
// allowUnauthorizedCerts: the field's name before 0.1.2, still set on older saved credentials.
|
|
20
|
+
skipSslCertificateValidation: credentials.ignoreSslIssues === true || credentials.allowUnauthorizedCerts === true,
|
|
20
21
|
}));
|
|
21
22
|
}
|
|
22
23
|
catch (error) {
|
package/dist/lib/logs.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.splitLogContent = splitLogContent;
|
|
4
|
+
exports.invalidPatternReason = invalidPatternReason;
|
|
4
5
|
exports.compilePattern = compilePattern;
|
|
5
6
|
exports.filterLogLines = filterLogLines;
|
|
6
7
|
// Log text -> structured lines, optionally filtered by regex and classified with suggested next steps.
|
|
@@ -11,16 +12,22 @@ function splitLogContent(content, startLine) {
|
|
|
11
12
|
raw.pop();
|
|
12
13
|
return raw.map((line, i) => ({ lineNumber: startLine ? startLine + i : undefined, line }));
|
|
13
14
|
}
|
|
14
|
-
|
|
15
|
+
/** Why a filter pattern is not a valid regular expression, or null when it is fine (or empty). */
|
|
16
|
+
function invalidPatternReason(pattern) {
|
|
15
17
|
if (!pattern?.trim())
|
|
16
|
-
return
|
|
18
|
+
return null;
|
|
17
19
|
try {
|
|
18
|
-
|
|
20
|
+
new RegExp(pattern, 'i');
|
|
21
|
+
return null;
|
|
19
22
|
}
|
|
20
23
|
catch (error) {
|
|
21
|
-
|
|
24
|
+
return `Invalid filter pattern "${pattern}": ${error.message}`;
|
|
22
25
|
}
|
|
23
26
|
}
|
|
27
|
+
/** Case-insensitive regex for a filter pattern; undefined when empty or invalid (validate with invalidPatternReason first). */
|
|
28
|
+
function compilePattern(pattern) {
|
|
29
|
+
return invalidPatternReason(pattern) === null && pattern?.trim() ? new RegExp(pattern, 'i') : undefined;
|
|
30
|
+
}
|
|
24
31
|
function filterLogLines(lines, filter) {
|
|
25
32
|
const regex = compilePattern(filter.pattern);
|
|
26
33
|
const out = [];
|
|
@@ -451,14 +451,14 @@ class Unraid {
|
|
|
451
451
|
description = {
|
|
452
452
|
displayName: 'Unraid',
|
|
453
453
|
name: 'unraid',
|
|
454
|
-
icon: 'file:unraid.svg',
|
|
454
|
+
icon: { light: 'file:unraid.svg', dark: 'file:unraid.svg' },
|
|
455
455
|
group: ['transform'],
|
|
456
456
|
version: 1,
|
|
457
457
|
subtitle: '={{$parameter["operation"] + ": " + $parameter["resource"]}}',
|
|
458
458
|
description: 'Monitor and control an Unraid server through its GraphQL API',
|
|
459
459
|
defaults: { name: 'Unraid' },
|
|
460
|
-
inputs: [
|
|
461
|
-
outputs: [
|
|
460
|
+
inputs: [n8n_workflow_1.NodeConnectionTypes.Main],
|
|
461
|
+
outputs: [n8n_workflow_1.NodeConnectionTypes.Main],
|
|
462
462
|
usableAsTool: true,
|
|
463
463
|
credentials: [{ name: 'unraidApi', required: true }],
|
|
464
464
|
properties,
|
|
@@ -579,6 +579,9 @@ class Unraid {
|
|
|
579
579
|
}
|
|
580
580
|
else if (operation === 'getLogs') {
|
|
581
581
|
const o = this.getNodeParameter('logOptions', i, {});
|
|
582
|
+
const badPattern = (0, logs_1.invalidPatternReason)(o.pattern);
|
|
583
|
+
if (badPattern)
|
|
584
|
+
throw new n8n_workflow_1.NodeOperationError(this.getNode(), badPattern, { itemIndex: i });
|
|
582
585
|
const vars = { id: await containerId(), tail: this.getNodeParameter('tail', i) };
|
|
583
586
|
if (o.since)
|
|
584
587
|
vars.since = new Date(o.since).toISOString();
|
|
@@ -632,6 +635,9 @@ class Unraid {
|
|
|
632
635
|
}
|
|
633
636
|
else {
|
|
634
637
|
const o = this.getNodeParameter('logOptions', i, {});
|
|
638
|
+
const badPattern = (0, logs_1.invalidPatternReason)(o.pattern);
|
|
639
|
+
if (badPattern)
|
|
640
|
+
throw new n8n_workflow_1.NodeOperationError(this.getNode(), badPattern, { itemIndex: i });
|
|
635
641
|
const vars = { path: this.getNodeParameter('logPath', i), lines: this.getNodeParameter('lines', i) };
|
|
636
642
|
if (Number(o.startLine) > 0)
|
|
637
643
|
vars.startLine = Number(o.startLine);
|
|
@@ -701,9 +707,11 @@ class Unraid {
|
|
|
701
707
|
out.push({ json: { error: error.message }, pairedItem: { item: i } });
|
|
702
708
|
continue;
|
|
703
709
|
}
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
710
|
+
// Keep NodeApiError (it carries the HTTP details); wrap anything else so n8n shows it per item.
|
|
711
|
+
const nodeError = error instanceof n8n_workflow_1.NodeApiError ? error : new n8n_workflow_1.NodeOperationError(this.getNode(), error, { itemIndex: i });
|
|
712
|
+
if (nodeError.context.itemIndex === undefined)
|
|
713
|
+
nodeError.context.itemIndex = i;
|
|
714
|
+
throw nodeError;
|
|
707
715
|
}
|
|
708
716
|
}
|
|
709
717
|
return [out];
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.UnraidTrigger = void 0;
|
|
4
|
+
const n8n_workflow_1 = require("n8n-workflow");
|
|
4
5
|
const analysis_1 = require("../../lib/analysis");
|
|
5
6
|
const client_1 = require("../../lib/client");
|
|
6
7
|
const containers_1 = require("../../lib/containers");
|
|
@@ -17,7 +18,7 @@ class UnraidTrigger {
|
|
|
17
18
|
description = {
|
|
18
19
|
displayName: 'Unraid Trigger',
|
|
19
20
|
name: 'unraidTrigger',
|
|
20
|
-
icon: 'file:unraid.svg',
|
|
21
|
+
icon: { light: 'file:unraid.svg', dark: 'file:unraid.svg' },
|
|
21
22
|
group: ['trigger'],
|
|
22
23
|
version: 1,
|
|
23
24
|
subtitle: '={{$parameter["event"]}}',
|
|
@@ -25,7 +26,7 @@ class UnraidTrigger {
|
|
|
25
26
|
defaults: { name: 'Unraid Trigger' },
|
|
26
27
|
polling: true,
|
|
27
28
|
inputs: [],
|
|
28
|
-
outputs: [
|
|
29
|
+
outputs: [n8n_workflow_1.NodeConnectionTypes.Main],
|
|
29
30
|
credentials: [{ name: 'unraidApi', required: true }],
|
|
30
31
|
properties: [
|
|
31
32
|
{
|
|
@@ -153,6 +154,9 @@ class UnraidTrigger {
|
|
|
153
154
|
async poll() {
|
|
154
155
|
const event = this.getNodeParameter('event');
|
|
155
156
|
const options = this.getNodeParameter('options', {});
|
|
157
|
+
const badPattern = (0, logs_1.invalidPatternReason)(options.pattern);
|
|
158
|
+
if (badPattern)
|
|
159
|
+
throw new n8n_workflow_1.NodeOperationError(this.getNode(), badPattern);
|
|
156
160
|
const state = this.getWorkflowStaticData('node');
|
|
157
161
|
const manual = this.getMode() === 'manual';
|
|
158
162
|
const emitExisting = manual || options.emitExisting === true;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@silentjma/n8n-nodes-unraid",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.2",
|
|
4
4
|
"description": "Unofficial n8n community nodes for the Unraid GraphQL API: Docker, array, VMs, logs, notifications and a polling trigger for log/alert watching.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"n8n-community-node-package",
|
|
@@ -13,6 +13,7 @@
|
|
|
13
13
|
"license": "MIT",
|
|
14
14
|
"author": {
|
|
15
15
|
"name": "Mohamed Ayoub Jabane",
|
|
16
|
+
"email": "kilwanitro@gmail.com",
|
|
16
17
|
"url": "https://github.com/SilentJMA"
|
|
17
18
|
},
|
|
18
19
|
"homepage": "https://github.com/SilentJMA/n8n-nodes-unraid#readme",
|
|
@@ -62,9 +63,10 @@
|
|
|
62
63
|
]
|
|
63
64
|
},
|
|
64
65
|
"devDependencies": {
|
|
65
|
-
"@eslint/js": "9.
|
|
66
|
+
"@eslint/js": "9.29.0",
|
|
67
|
+
"@n8n/eslint-plugin-community-nodes": "0.37.0",
|
|
66
68
|
"@types/node": "22.20.5",
|
|
67
|
-
"eslint": "9.
|
|
69
|
+
"eslint": "9.29.0",
|
|
68
70
|
"eslint-plugin-n8n-nodes-base": "1.16.7",
|
|
69
71
|
"n8n-workflow": "2.43.5",
|
|
70
72
|
"typescript": "5.9.3",
|