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.
@@ -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
- <dd><b>Auto Mode:</b> Shows the <em>Client Name</em> from your Config. Uses this name automatically.</dd>
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>
@@ -77,11 +77,10 @@ module.exports = function (RED) {
77
77
  return;
78
78
  }
79
79
  // Retrieve configuration node
80
- // Default to Connection's Client Name (Friendly Name) because Client ID is often a UUID
81
- // that doesn't match the desired Provider ID. User typically expects 'nodered' not '0069...'
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; // Only Primary sends definitions
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
- this.warn(`Definition update error: ${err.message}`);
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
@@ -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: 'green', shape: 'ring', text: 'ready (fallback)' });
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' });
@@ -176,37 +176,51 @@ module.exports = function (RED) {
176
176
  if (this.nc) {
177
177
  return this.nc;
178
178
  }
179
- // Ensure we have a valid token initially
180
- await this.getToken();
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
- // Use jwtAuthenticator to allow dynamic token refresh on reconnect
183
- try {
184
- this.nc = await connect({
185
- servers: `nats://${this.host}:${this.port}`,
186
- // Authenticator must be SYNCHRONOUS. We rely on background refresh to keep this.tokenInfo current.
187
- // Token function must be SYNCHRONOUS if we rely on background refresh.
188
- token: () => {
189
- return this.tokenInfo ? this.tokenInfo.token : '';
190
- },
191
- name: this.clientName,
192
- inboxPrefix: `_INBOX.${this.clientName}`,
193
- maxReconnectAttempts: -1, // Infinite reconnects
194
- reconnectTimeWait: 2000,
195
- });
196
- this.log(`NATS connecting with Name: '${this.clientName}'`);
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
- // Reset Failure timestamp on success
199
- this.authFailureTimestamp = 0;
205
+ this.log(`NATS connecting as Name: '${this.clientName}' (Dedup Active)`);
200
206
 
201
- } catch (e) {
202
- if (e.message && (e.message.includes('Authorization') || e.message.includes('Permissions') || e.message.includes('Authentication'))) {
203
- this.warn(`NATS Authorization failed. Invalidating token cache. Circuit Breaker active for 10s.`);
204
- this.tokenInfo = null; // Force fresh token next time
205
- this.authFailureTimestamp = Date.now(); // Start Cooldown
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
- this.error(`NATS connect failed: ${e.message}`);
208
- throw e;
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.39",
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",