@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 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 ([src/lib/analysis.ts](src/lib/analysis.ts)). The rules tag log lines and health findings as `info`, `warning` or `critical` and attach **suggested next steps**.
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
- 2. Import the three workflows. In each one, select the **Unraid API** credential (`https://192.168.178.2`, Ignore SSL Issues on) and the Telegram credential.
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 [src/lib/queries.ts](src/lib/queries.ts) are written against `api/generated-schema.graphql` from the unraid/api repository. Check them again when Unraid changes the schema.
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: 'allowUnauthorizedCerts',
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
  },
@@ -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
- skipSslCertificateValidation: credentials.allowUnauthorizedCerts === true,
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
- function compilePattern(pattern) {
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 undefined;
18
+ return null;
17
19
  try {
18
- return new RegExp(pattern, 'i');
20
+ new RegExp(pattern, 'i');
21
+ return null;
19
22
  }
20
23
  catch (error) {
21
- throw new Error(`Invalid filter pattern "${pattern}": ${error.message}`);
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: ['main'],
461
- outputs: ['main'],
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
- if (error instanceof n8n_workflow_1.NodeOperationError && error.context.itemIndex === undefined)
705
- error.context.itemIndex = i;
706
- throw error;
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: ['main'],
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.0",
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.39.5",
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.39.5",
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",