node-red-contrib-i3x 0.0.2 → 0.0.3
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/.claude/settings.local.json +2 -1
- package/CHANGELOG.md +27 -0
- package/lib/i3x-client.js +32 -8
- package/lib/node-utils.js +27 -1
- package/nodes/i3x-browse.html +13 -0
- package/nodes/i3x-browse.js +14 -19
- package/nodes/i3x-history.html +14 -0
- package/nodes/i3x-history.js +6 -6
- package/nodes/i3x-read.html +15 -0
- package/nodes/i3x-read.js +6 -6
- package/nodes/i3x-server.html +926 -0
- package/nodes/i3x-server.js +122 -0
- package/nodes/i3x-subscribe.html +14 -1
- package/nodes/i3x-subscribe.js +9 -6
- package/nodes/i3x-write.html +15 -2
- package/nodes/i3x-write.js +3 -3
- package/package.json +1 -1
|
@@ -7,7 +7,8 @@
|
|
|
7
7
|
"Bash(npm test:*)",
|
|
8
8
|
"Bash(node-red:*)",
|
|
9
9
|
"Bash(npx node-red:*)",
|
|
10
|
-
"Bash(docker compose:*)"
|
|
10
|
+
"Bash(docker compose:*)",
|
|
11
|
+
"Bash(ls /home/la/private/node-red-contrib-i3x/docker-compose* /home/la/private/node-red-contrib-i3x/Dockerfile* 2>/dev/null)"
|
|
11
12
|
]
|
|
12
13
|
}
|
|
13
14
|
}
|
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,32 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.0.3 (2026-03-10)
|
|
4
|
+
|
|
5
|
+
Hardening, security improvements, and new browser widget features.
|
|
6
|
+
|
|
7
|
+
### Added
|
|
8
|
+
|
|
9
|
+
- **Live Values in Browser Widget** – The tree view now displays the current value, quality, and timestamp next to each element. Values are fetched automatically when expanding types or children, and on search results. Hover to see full value with quality and timestamp.
|
|
10
|
+
- **Test Connection Button** – Server config panel now includes a "Test Connection" button to verify connectivity without deploying
|
|
11
|
+
- **New admin endpoint** `POST /i3x-server/:id/browse/values` – Batch-reads live values for up to 50 elements (used by browser widget)
|
|
12
|
+
- **`statusError()` utility** – Smarter error message truncation (48 chars with `...`) replacing the hard `substring(0, 32)` cut across all nodes
|
|
13
|
+
- **`clampMaxDepth()` utility** – Validates and clamps `maxDepth` to 0–100 range, preventing negative or excessively large values
|
|
14
|
+
|
|
15
|
+
### Security
|
|
16
|
+
|
|
17
|
+
- **HTTPS warning** – Nodes now warn at startup when credentials are sent over plain HTTP to non-localhost servers
|
|
18
|
+
- **Error response sanitization** – `_wrapError()` strips sensitive fields (`token`, `password`, `apiKey`, `secret`) from API error response bodies to prevent accidental credential leakage in logs
|
|
19
|
+
|
|
20
|
+
### Fixed
|
|
21
|
+
|
|
22
|
+
- **SSE auth header duplication** – `streamSubscription()` now copies all configured headers cleanly via spread instead of manually duplicating individual auth headers
|
|
23
|
+
- **Poll interval minimum** – Increased from 500ms to 1000ms to prevent accidental API overload
|
|
24
|
+
|
|
25
|
+
### Changed
|
|
26
|
+
|
|
27
|
+
- Error status messages across all nodes now show up to 48 characters (was 32)
|
|
28
|
+
- `maxDepth` is validated on all nodes that accept it (read, history, subscribe)
|
|
29
|
+
|
|
3
30
|
## 0.0.2 (2026-03-05)
|
|
4
31
|
|
|
5
32
|
Compliance improvements based on [i3X Client Developer Guidelines](https://www.i3x.dev/sdk/category/client-developers).
|
package/lib/i3x-client.js
CHANGED
|
@@ -84,6 +84,14 @@ class I3XClient extends EventEmitter {
|
|
|
84
84
|
this.authType = config.authType || "none";
|
|
85
85
|
this.timeout = config.timeout || 10000;
|
|
86
86
|
|
|
87
|
+
// Warn if credentials are sent over plain HTTP (not localhost)
|
|
88
|
+
if (this.authType !== "none" && this.baseUrl && !this.baseUrl.startsWith("https://")) {
|
|
89
|
+
const isLocal = /^https?:\/\/(localhost|127\.0\.0\.1|::1)(:|\/|$)/.test(this.baseUrl);
|
|
90
|
+
if (!isLocal) {
|
|
91
|
+
this._httpsWarning = "Credentials sent over plain HTTP – use HTTPS in production";
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
87
95
|
const axiosConfig = {
|
|
88
96
|
baseURL: this._prefix(),
|
|
89
97
|
timeout: this.timeout,
|
|
@@ -328,13 +336,19 @@ class I3XClient extends EventEmitter {
|
|
|
328
336
|
let closed = false;
|
|
329
337
|
let reconnectCount = 0;
|
|
330
338
|
|
|
331
|
-
const headers = {
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
339
|
+
const headers = {
|
|
340
|
+
...this.http.defaults.headers.common,
|
|
341
|
+
...this.http.defaults.headers,
|
|
342
|
+
Accept: "text/event-stream",
|
|
343
|
+
};
|
|
344
|
+
// Remove non-header axios defaults that got spread in
|
|
345
|
+
delete headers.common;
|
|
346
|
+
delete headers.get;
|
|
347
|
+
delete headers.post;
|
|
348
|
+
delete headers.put;
|
|
349
|
+
delete headers.delete;
|
|
350
|
+
delete headers.patch;
|
|
351
|
+
delete headers.head;
|
|
338
352
|
if (this.http.defaults.auth) {
|
|
339
353
|
const b64 = Buffer.from(
|
|
340
354
|
`${this.http.defaults.auth.username}:${this.http.defaults.auth.password}`
|
|
@@ -513,7 +527,17 @@ class I3XClient extends EventEmitter {
|
|
|
513
527
|
if (err.response) {
|
|
514
528
|
wrapped.statusCode = err.response.status;
|
|
515
529
|
wrapped.statusText = err.response.statusText;
|
|
516
|
-
|
|
530
|
+
// Sanitize response body to avoid leaking auth details
|
|
531
|
+
const body = err.response.data;
|
|
532
|
+
if (body && typeof body === "object") {
|
|
533
|
+
const sanitized = { ...body };
|
|
534
|
+
for (const key of ["authorization", "token", "apiKey", "api_key", "password", "secret"]) {
|
|
535
|
+
delete sanitized[key];
|
|
536
|
+
}
|
|
537
|
+
wrapped.body = sanitized;
|
|
538
|
+
} else {
|
|
539
|
+
wrapped.body = body;
|
|
540
|
+
}
|
|
517
541
|
} else if (err.code) {
|
|
518
542
|
wrapped.code = err.code;
|
|
519
543
|
}
|
package/lib/node-utils.js
CHANGED
|
@@ -58,4 +58,30 @@ function safeSend(node, send) {
|
|
|
58
58
|
return send || function () { node.send.apply(node, arguments); };
|
|
59
59
|
}
|
|
60
60
|
|
|
61
|
-
|
|
61
|
+
/**
|
|
62
|
+
* Truncate an error message for node status display.
|
|
63
|
+
* Keeps up to 48 characters and appends "..." if truncated.
|
|
64
|
+
* @param {string} msg
|
|
65
|
+
* @returns {string}
|
|
66
|
+
*/
|
|
67
|
+
function statusError(msg) {
|
|
68
|
+
if (!msg) return "error";
|
|
69
|
+
if (msg.length <= 48) return msg;
|
|
70
|
+
return msg.substring(0, 45) + "...";
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Clamp maxDepth to a valid range (0–100).
|
|
75
|
+
* @param {number} val
|
|
76
|
+
* @param {number} [fallback=1]
|
|
77
|
+
* @returns {number}
|
|
78
|
+
*/
|
|
79
|
+
function clampMaxDepth(val, fallback) {
|
|
80
|
+
if (fallback === undefined) fallback = 1;
|
|
81
|
+
var n = parseInt(val, 10);
|
|
82
|
+
if (isNaN(n) || n < 0) return fallback;
|
|
83
|
+
if (n > 100) return 100;
|
|
84
|
+
return n;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
module.exports = { bindServer, parseIds, safeSend, statusError, clampMaxDepth };
|
package/nodes/i3x-browse.html
CHANGED
|
@@ -21,6 +21,9 @@
|
|
|
21
21
|
<label for="node-input-elementId"><i class="fa fa-crosshairs"></i> Element ID</label>
|
|
22
22
|
<input type="text" id="node-input-elementId" placeholder="optional – filter by element ID">
|
|
23
23
|
</div>
|
|
24
|
+
<div class="form-row i3x-browse-elementId">
|
|
25
|
+
<div id="i3x-browse-browser"></div>
|
|
26
|
+
</div>
|
|
24
27
|
<div class="form-row i3x-browse-typeId">
|
|
25
28
|
<label for="node-input-typeId"><i class="fa fa-filter"></i> Type ID</label>
|
|
26
29
|
<input type="text" id="node-input-typeId" placeholder="optional – filter objects by type">
|
|
@@ -66,6 +69,7 @@
|
|
|
66
69
|
<h3>Details</h3>
|
|
67
70
|
<p>This node talks to the <b>Explore</b> endpoints of the i3X API.
|
|
68
71
|
<code>msg</code> properties override the values configured in the node editor.</p>
|
|
72
|
+
<p>Use the <b>Browse</b> button to visually select an element from the server.</p>
|
|
69
73
|
</script>
|
|
70
74
|
|
|
71
75
|
<script type="text/javascript">
|
|
@@ -101,6 +105,15 @@
|
|
|
101
105
|
}
|
|
102
106
|
target.on("change", toggle);
|
|
103
107
|
toggle();
|
|
108
|
+
|
|
109
|
+
if (window.I3XBrowser) {
|
|
110
|
+
this._browser = I3XBrowser.create({
|
|
111
|
+
container: "#i3x-browse-browser",
|
|
112
|
+
serverField: "#node-input-server",
|
|
113
|
+
targetField: "#node-input-elementId",
|
|
114
|
+
mode: "single",
|
|
115
|
+
});
|
|
116
|
+
}
|
|
104
117
|
},
|
|
105
118
|
});
|
|
106
119
|
</script>
|
package/nodes/i3x-browse.js
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
*/
|
|
4
4
|
"use strict";
|
|
5
5
|
|
|
6
|
-
const { bindServer, safeSend } = require("../lib/node-utils");
|
|
6
|
+
const { bindServer, parseIds, safeSend, statusError } = require("../lib/node-utils");
|
|
7
7
|
|
|
8
8
|
module.exports = function (RED) {
|
|
9
9
|
function I3XBrowseNode(config) {
|
|
@@ -24,13 +24,13 @@ module.exports = function (RED) {
|
|
|
24
24
|
const client = node.server.client;
|
|
25
25
|
|
|
26
26
|
const target = msg.browseTarget || node.browseTarget;
|
|
27
|
-
const
|
|
27
|
+
const ids = parseIds(msg.elementId || node.elementId);
|
|
28
28
|
const typeId = msg.typeId || node.typeId;
|
|
29
29
|
const nsUri = msg.namespaceUri || node.namespaceUri;
|
|
30
30
|
const inclMeta = msg.includeMetadata !== undefined ? msg.includeMetadata : node.includeMetadata;
|
|
31
31
|
const relType = msg.relationshipType || node.relationshipType;
|
|
32
32
|
|
|
33
|
-
node.status({ fill: "blue", shape: "dot", text: "
|
|
33
|
+
node.status({ fill: "blue", shape: "dot", text: "browsing " + target + "..." });
|
|
34
34
|
|
|
35
35
|
try {
|
|
36
36
|
let result;
|
|
@@ -39,51 +39,46 @@ module.exports = function (RED) {
|
|
|
39
39
|
result = await client.getNamespaces();
|
|
40
40
|
break;
|
|
41
41
|
case "objecttypes":
|
|
42
|
-
if (
|
|
43
|
-
const ids = Array.isArray(elementId) ? elementId : [elementId];
|
|
42
|
+
if (ids.length) {
|
|
44
43
|
result = await client.queryObjectTypes(ids);
|
|
45
44
|
} else {
|
|
46
45
|
result = await client.getObjectTypes({ namespaceUri: nsUri || undefined });
|
|
47
46
|
}
|
|
48
47
|
break;
|
|
49
48
|
case "relationshiptypes":
|
|
50
|
-
if (
|
|
51
|
-
const ids = Array.isArray(elementId) ? elementId : [elementId];
|
|
49
|
+
if (ids.length) {
|
|
52
50
|
result = await client.queryRelationshipTypes(ids);
|
|
53
51
|
} else {
|
|
54
52
|
result = await client.getRelationshipTypes({ namespaceUri: nsUri || undefined });
|
|
55
53
|
}
|
|
56
54
|
break;
|
|
57
55
|
case "objects":
|
|
58
|
-
if (
|
|
59
|
-
const ids = Array.isArray(elementId) ? elementId : [elementId];
|
|
56
|
+
if (ids.length) {
|
|
60
57
|
result = await client.listObjects(ids, { includeMetadata: inclMeta });
|
|
61
58
|
} else {
|
|
62
59
|
result = await client.getObjects({ typeId: typeId || undefined, includeMetadata: inclMeta });
|
|
63
60
|
}
|
|
64
61
|
break;
|
|
65
62
|
case "related":
|
|
66
|
-
if (!
|
|
63
|
+
if (!ids.length) {
|
|
67
64
|
throw new Error("elementId is required for related objects query");
|
|
68
65
|
}
|
|
69
|
-
{
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
includeMetadata: inclMeta,
|
|
74
|
-
});
|
|
75
|
-
}
|
|
66
|
+
result = await client.getRelatedObjects(ids, {
|
|
67
|
+
relationshipType: relType || undefined,
|
|
68
|
+
includeMetadata: inclMeta,
|
|
69
|
+
});
|
|
76
70
|
break;
|
|
77
71
|
default:
|
|
78
72
|
throw new Error("Unknown browse target: " + target);
|
|
79
73
|
}
|
|
80
74
|
|
|
75
|
+
const count = Array.isArray(result) ? result.length : 1;
|
|
81
76
|
msg.payload = result;
|
|
82
|
-
node.status({ fill: "green", shape: "dot", text: "
|
|
77
|
+
node.status({ fill: "green", shape: "dot", text: count + " " + target });
|
|
83
78
|
send(msg);
|
|
84
79
|
if (done) done();
|
|
85
80
|
} catch (err) {
|
|
86
|
-
node.status({ fill: "red", shape: "ring", text: err.message
|
|
81
|
+
node.status({ fill: "red", shape: "ring", text: statusError(err.message) });
|
|
87
82
|
if (done) done(err); else node.error(err, msg);
|
|
88
83
|
}
|
|
89
84
|
});
|
package/nodes/i3x-history.html
CHANGED
|
@@ -11,6 +11,9 @@
|
|
|
11
11
|
<label for="node-input-elementIds"><i class="fa fa-list"></i> Element IDs</label>
|
|
12
12
|
<input type="text" id="node-input-elementIds" placeholder="comma-separated element IDs">
|
|
13
13
|
</div>
|
|
14
|
+
<div class="form-row">
|
|
15
|
+
<div id="i3x-history-browser"></div>
|
|
16
|
+
</div>
|
|
14
17
|
<div class="form-row">
|
|
15
18
|
<label for="node-input-startTime"><i class="fa fa-clock-o"></i> Start Time</label>
|
|
16
19
|
<input type="text" id="node-input-startTime" placeholder="ISO 8601 or relative (e.g. -1h, -7d)">
|
|
@@ -50,6 +53,7 @@
|
|
|
50
53
|
<p>Uses <code>POST /objects/history</code>. Time values support both ISO 8601 and
|
|
51
54
|
relative notation: <code>-30s</code> (seconds), <code>-5m</code> (minutes), <code>-1h</code> (hours),
|
|
52
55
|
<code>-7d</code> (days), <code>-1w</code> (weeks).</p>
|
|
56
|
+
<p>Use the <b>Browse</b> button to visually select elements from the server.</p>
|
|
53
57
|
</script>
|
|
54
58
|
|
|
55
59
|
<script type="text/javascript">
|
|
@@ -71,5 +75,15 @@
|
|
|
71
75
|
label: function () {
|
|
72
76
|
return this.name || this.elementIds || "i3x history";
|
|
73
77
|
},
|
|
78
|
+
oneditprepare: function () {
|
|
79
|
+
if (window.I3XBrowser) {
|
|
80
|
+
this._browser = I3XBrowser.create({
|
|
81
|
+
container: "#i3x-history-browser",
|
|
82
|
+
serverField: "#node-input-server",
|
|
83
|
+
targetField: "#node-input-elementIds",
|
|
84
|
+
mode: "multi",
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
},
|
|
74
88
|
});
|
|
75
89
|
</script>
|
package/nodes/i3x-history.js
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
*/
|
|
4
4
|
"use strict";
|
|
5
5
|
|
|
6
|
-
const { bindServer, parseIds, safeSend } = require("../lib/node-utils");
|
|
6
|
+
const { bindServer, parseIds, safeSend, statusError, clampMaxDepth } = require("../lib/node-utils");
|
|
7
7
|
|
|
8
8
|
/**
|
|
9
9
|
* Resolve relative time strings like "-1h", "-7d", "-30m" to ISO 8601.
|
|
@@ -28,8 +28,7 @@ module.exports = function (RED) {
|
|
|
28
28
|
node.elementIds = config.elementIds || "";
|
|
29
29
|
node.startTime = config.startTime || "";
|
|
30
30
|
node.endTime = config.endTime || "";
|
|
31
|
-
node.maxDepth =
|
|
32
|
-
if (isNaN(node.maxDepth)) node.maxDepth = 1;
|
|
31
|
+
node.maxDepth = clampMaxDepth(config.maxDepth);
|
|
33
32
|
|
|
34
33
|
if (!bindServer(node, RED, config.server)) return;
|
|
35
34
|
|
|
@@ -46,18 +45,19 @@ module.exports = function (RED) {
|
|
|
46
45
|
|
|
47
46
|
const startTime = resolveTime(msg.startTime || node.startTime);
|
|
48
47
|
const endTime = resolveTime(msg.endTime || node.endTime);
|
|
49
|
-
const maxDepth = msg.maxDepth !== undefined ?
|
|
48
|
+
const maxDepth = msg.maxDepth !== undefined ? clampMaxDepth(msg.maxDepth) : node.maxDepth;
|
|
50
49
|
|
|
51
50
|
node.status({ fill: "blue", shape: "dot", text: "querying..." });
|
|
52
51
|
|
|
53
52
|
try {
|
|
54
53
|
const result = await client.readHistory(ids, { startTime, endTime, maxDepth });
|
|
55
54
|
msg.payload = result;
|
|
56
|
-
|
|
55
|
+
const count = Array.isArray(result) ? result.length : 1;
|
|
56
|
+
node.status({ fill: "green", shape: "dot", text: count + " record" + (count !== 1 ? "s" : "") });
|
|
57
57
|
send(msg);
|
|
58
58
|
if (done) done();
|
|
59
59
|
} catch (err) {
|
|
60
|
-
node.status({ fill: "red", shape: "ring", text: err.message
|
|
60
|
+
node.status({ fill: "red", shape: "ring", text: statusError(err.message) });
|
|
61
61
|
if (done) done(err); else node.error(err, msg);
|
|
62
62
|
}
|
|
63
63
|
});
|
package/nodes/i3x-read.html
CHANGED
|
@@ -11,6 +11,9 @@
|
|
|
11
11
|
<label for="node-input-elementIds"><i class="fa fa-list"></i> Element IDs</label>
|
|
12
12
|
<input type="text" id="node-input-elementIds" placeholder="comma-separated element IDs">
|
|
13
13
|
</div>
|
|
14
|
+
<div class="form-row">
|
|
15
|
+
<div id="i3x-read-browser"></div>
|
|
16
|
+
</div>
|
|
14
17
|
<div class="form-row">
|
|
15
18
|
<label for="node-input-maxDepth"><i class="fa fa-level-down"></i> Max Depth</label>
|
|
16
19
|
<input type="number" id="node-input-maxDepth" placeholder="1" min="0" step="1">
|
|
@@ -37,6 +40,8 @@
|
|
|
37
40
|
<h3>Details</h3>
|
|
38
41
|
<p>Uses <code>POST /objects/value</code>. The <code>maxDepth</code> parameter controls whether
|
|
39
42
|
child component values are included recursively.</p>
|
|
43
|
+
<p>Use the <b>Browse</b> button to visually select elements from the server,
|
|
44
|
+
or provide element IDs via <code>msg.elementIds</code> at runtime.</p>
|
|
40
45
|
</script>
|
|
41
46
|
|
|
42
47
|
<script type="text/javascript">
|
|
@@ -56,5 +61,15 @@
|
|
|
56
61
|
label: function () {
|
|
57
62
|
return this.name || this.elementIds || "i3x read";
|
|
58
63
|
},
|
|
64
|
+
oneditprepare: function () {
|
|
65
|
+
if (window.I3XBrowser) {
|
|
66
|
+
this._browser = I3XBrowser.create({
|
|
67
|
+
container: "#i3x-read-browser",
|
|
68
|
+
serverField: "#node-input-server",
|
|
69
|
+
targetField: "#node-input-elementIds",
|
|
70
|
+
mode: "multi",
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
},
|
|
59
74
|
});
|
|
60
75
|
</script>
|
package/nodes/i3x-read.js
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
*/
|
|
4
4
|
"use strict";
|
|
5
5
|
|
|
6
|
-
const { bindServer, parseIds, safeSend } = require("../lib/node-utils");
|
|
6
|
+
const { bindServer, parseIds, safeSend, statusError, clampMaxDepth } = require("../lib/node-utils");
|
|
7
7
|
|
|
8
8
|
module.exports = function (RED) {
|
|
9
9
|
function I3XReadNode(config) {
|
|
@@ -11,8 +11,7 @@ module.exports = function (RED) {
|
|
|
11
11
|
const node = this;
|
|
12
12
|
|
|
13
13
|
node.elementIds = config.elementIds || "";
|
|
14
|
-
node.maxDepth =
|
|
15
|
-
if (isNaN(node.maxDepth)) node.maxDepth = 1;
|
|
14
|
+
node.maxDepth = clampMaxDepth(config.maxDepth);
|
|
16
15
|
|
|
17
16
|
if (!bindServer(node, RED, config.server)) return;
|
|
18
17
|
|
|
@@ -27,18 +26,19 @@ module.exports = function (RED) {
|
|
|
27
26
|
return;
|
|
28
27
|
}
|
|
29
28
|
|
|
30
|
-
const maxDepth = msg.maxDepth !== undefined ?
|
|
29
|
+
const maxDepth = msg.maxDepth !== undefined ? clampMaxDepth(msg.maxDepth) : node.maxDepth;
|
|
31
30
|
|
|
32
31
|
node.status({ fill: "blue", shape: "dot", text: "requesting..." });
|
|
33
32
|
|
|
34
33
|
try {
|
|
35
34
|
const result = await client.readValues(ids, { maxDepth });
|
|
36
35
|
msg.payload = result;
|
|
37
|
-
|
|
36
|
+
const count = Array.isArray(result) ? result.length : 1;
|
|
37
|
+
node.status({ fill: "green", shape: "dot", text: count + " value" + (count !== 1 ? "s" : "") });
|
|
38
38
|
send(msg);
|
|
39
39
|
if (done) done();
|
|
40
40
|
} catch (err) {
|
|
41
|
-
node.status({ fill: "red", shape: "ring", text: err.message
|
|
41
|
+
node.status({ fill: "red", shape: "ring", text: statusError(err.message) });
|
|
42
42
|
if (done) done(err); else node.error(err, msg);
|
|
43
43
|
}
|
|
44
44
|
});
|