node-red-contrib-uos-nats 1.3.39 → 1.3.46
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/nodes/datahub-output.html +3 -1
- package/nodes/datahub-output.js +17 -5
- package/nodes/datahub-write.js +1 -1
- package/nodes/uos-config.js +41 -27
- package/package.json +1 -1
|
@@ -135,7 +135,9 @@
|
|
|
135
135
|
<dd>Connection node with host/port and OAuth credentials.</dd>
|
|
136
136
|
<dt>Provider ID</dt>
|
|
137
137
|
<dd>The unique ID for this provider. <b>Note:</b> You cannot use <code>u_os_sbm</code> (Reserved system name).</dd>
|
|
138
|
-
<
|
|
138
|
+
<dt>Provider ID</dt>
|
|
139
|
+
<dd>The unique ID for this provider. <b>Note:</b> You cannot use <code>u_os_sbm</code> (Reserved system name).</dd>
|
|
140
|
+
<dd><b>Auto Mode:</b> If empty, uses your **Client Name** (e.g. `nodered`) to match your Connection.</dd>
|
|
139
141
|
<dt>Keep-Alive (s)</dt>
|
|
140
142
|
<dd>Interval in seconds to refresh the provider definition. Default: <b>300s (5 min)</b>.</dd>
|
|
141
143
|
<dt>Allow external writes</dt>
|
package/nodes/datahub-output.js
CHANGED
|
@@ -77,11 +77,10 @@ module.exports = function (RED) {
|
|
|
77
77
|
return;
|
|
78
78
|
}
|
|
79
79
|
// Retrieve configuration node
|
|
80
|
-
//
|
|
81
|
-
//
|
|
80
|
+
// Auto Mode: Prefer Client Name ("nodered") to match Connection Name.
|
|
81
|
+
// This ensures consistency with DataHub UI expectations.
|
|
82
82
|
let defaultId = connection.clientName;
|
|
83
83
|
if (!defaultId) {
|
|
84
|
-
// Fallback to ID if Name is missing (should not happen as Name is mandatory)
|
|
85
84
|
defaultId = connection.clientId || 'nodered';
|
|
86
85
|
}
|
|
87
86
|
|
|
@@ -134,6 +133,7 @@ module.exports = function (RED) {
|
|
|
134
133
|
};
|
|
135
134
|
|
|
136
135
|
// Check Singleton Status
|
|
136
|
+
this.fatalError = false;
|
|
137
137
|
let isPrimary = false;
|
|
138
138
|
const existingNodeId = providerRegistry.get(this.providerId);
|
|
139
139
|
if (existingNodeId && existingNodeId !== this.id) {
|
|
@@ -147,7 +147,8 @@ module.exports = function (RED) {
|
|
|
147
147
|
// --- SEND DEFINITION HELPER ---
|
|
148
148
|
const sendDefinitionUpdate = async (modPayloads, modSubjects) => {
|
|
149
149
|
if (!nc) return;
|
|
150
|
-
if (!isPrimary) return;
|
|
150
|
+
if (!isPrimary) return;
|
|
151
|
+
if (this.fatalError) return; // permanent lockout
|
|
151
152
|
|
|
152
153
|
try {
|
|
153
154
|
const { payload, fingerprint: fp } = modPayloads.buildProviderDefinitionEvent(definitions);
|
|
@@ -160,7 +161,17 @@ module.exports = function (RED) {
|
|
|
160
161
|
await nc.flush();
|
|
161
162
|
console.log(`[DataHub Output] Definition published. FP: ${fp}`);
|
|
162
163
|
} catch (err) {
|
|
163
|
-
|
|
164
|
+
let msg = err.message || '';
|
|
165
|
+
// Check for fatal Auth/Permission errors
|
|
166
|
+
if (msg.includes('Authorization') || msg.includes('permissions') || msg.includes('10003') || msg.includes('Access Denied')) {
|
|
167
|
+
this.fatalError = true;
|
|
168
|
+
this.error(`FATAL AUTH ERROR: ${msg}. Stopping provider to protect connection.`);
|
|
169
|
+
this.status({ fill: 'red', shape: 'dot', text: 'auth blocked (permanent)' });
|
|
170
|
+
// Clear heartbeats
|
|
171
|
+
if (outputHeartbeat) clearInterval(outputHeartbeat);
|
|
172
|
+
} else {
|
|
173
|
+
this.warn(`Definition update error: ${err.message}`);
|
|
174
|
+
}
|
|
164
175
|
}
|
|
165
176
|
};
|
|
166
177
|
|
|
@@ -186,6 +197,7 @@ module.exports = function (RED) {
|
|
|
186
197
|
// console.log('[DataHub Output] Heartbeat skipped: NATS closed.');
|
|
187
198
|
return;
|
|
188
199
|
}
|
|
200
|
+
if (this.fatalError) return; // permanent lockout
|
|
189
201
|
if (!loadedPayloads || !loadedSubjects) return;
|
|
190
202
|
|
|
191
203
|
// If we have no definitions yet, nothing to send
|
package/nodes/datahub-write.js
CHANGED
|
@@ -170,7 +170,7 @@ module.exports = function (RED) {
|
|
|
170
170
|
node.resolvedId = this.variableId;
|
|
171
171
|
// Log as debug implies we handle it gracefully => No User Warn
|
|
172
172
|
node.debug(`ID Resolution failed for '${this.variableKey}' (${err.message}). Using configured ID: ${this.variableId}`);
|
|
173
|
-
node.status({ fill: '
|
|
173
|
+
node.status({ fill: 'yellow', shape: 'dot', text: 'fallback (key missing)' });
|
|
174
174
|
} else {
|
|
175
175
|
node.warn(`ID Resolution failed for '${this.variableKey}': ${err.message}`);
|
|
176
176
|
node.status({ fill: 'red', shape: 'dot', text: 'resolution failed' });
|
package/nodes/uos-config.js
CHANGED
|
@@ -176,37 +176,51 @@ module.exports = function (RED) {
|
|
|
176
176
|
if (this.nc) {
|
|
177
177
|
return this.nc;
|
|
178
178
|
}
|
|
179
|
-
//
|
|
180
|
-
|
|
179
|
+
// Deduplication: Return existing promise if we are already connecting
|
|
180
|
+
if (this.connectionPromise) {
|
|
181
|
+
// this.log('Joining pending connection request...');
|
|
182
|
+
return this.connectionPromise;
|
|
183
|
+
}
|
|
181
184
|
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
185
|
+
this.connectionPromise = (async () => {
|
|
186
|
+
try {
|
|
187
|
+
// Ensure we have a valid token initially
|
|
188
|
+
await this.getToken();
|
|
189
|
+
|
|
190
|
+
this.nc = await connect({
|
|
191
|
+
servers: `nats://${this.host}:${this.port}`,
|
|
192
|
+
// Authenticator must be SYNCHRONOUS. We rely on background refresh to keep this.tokenInfo current.
|
|
193
|
+
// Token function must be SYNCHRONOUS if we rely on background refresh.
|
|
194
|
+
token: () => {
|
|
195
|
+
return this.tokenInfo ? this.tokenInfo.token : '';
|
|
196
|
+
},
|
|
197
|
+
// REVERT: Use Configured Client Name for Connection.
|
|
198
|
+
// Using UUID caused "Authorization Violation" for some users.
|
|
199
|
+
name: this.clientName,
|
|
200
|
+
inboxPrefix: `_INBOX.${this.clientName}`,
|
|
201
|
+
maxReconnectAttempts: -1, // Infinite reconnects
|
|
202
|
+
reconnectTimeWait: 2000,
|
|
203
|
+
});
|
|
197
204
|
|
|
198
|
-
|
|
199
|
-
this.authFailureTimestamp = 0;
|
|
205
|
+
this.log(`NATS connecting as Name: '${this.clientName}' (Dedup Active)`);
|
|
200
206
|
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
this.
|
|
204
|
-
|
|
205
|
-
|
|
207
|
+
// Reset Failure timestamp on success
|
|
208
|
+
this.authFailureTimestamp = 0;
|
|
209
|
+
return this.nc;
|
|
210
|
+
|
|
211
|
+
} catch (e) {
|
|
212
|
+
if (e.message && (e.message.includes('Authorization') || e.message.includes('Permissions') || e.message.includes('Authentication'))) {
|
|
213
|
+
this.warn(`NATS Authorization failed. Invalidating token cache. Circuit Breaker active for 10s.`);
|
|
214
|
+
this.tokenInfo = null; // Force fresh token next time
|
|
215
|
+
this.authFailureTimestamp = Date.now(); // Start Cooldown
|
|
216
|
+
}
|
|
217
|
+
this.error(`NATS connect failed: ${e.message}`);
|
|
218
|
+
throw e;
|
|
219
|
+
} finally {
|
|
220
|
+
this.connectionPromise = null;
|
|
206
221
|
}
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
}
|
|
222
|
+
})();
|
|
223
|
+
return this.connectionPromise;
|
|
210
224
|
this.nc.closed().then(() => {
|
|
211
225
|
this.nc = null;
|
|
212
226
|
this.emit('disconnected');
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "node-red-contrib-uos-nats",
|
|
3
|
-
"version": "1.3.
|
|
3
|
+
"version": "1.3.46",
|
|
4
4
|
"description": "Node-RED nodes for Weidmüller u-OS Data Hub. Read, write, and provide variables via NATS protocol with OAuth2 authentication. Features: Variable Key resolution, custom icons, example flows, and provider definition caching.",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "IoTUeli",
|