@vitormnm/node-red-simple-opcua 1.4.3 → 1.6.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.
Files changed (34) hide show
  1. package/client/icons/opcua.svg +132 -132
  2. package/client/lib/opcua-client-browser.js +368 -330
  3. package/client/lib/opcua-client-method-service.js +88 -88
  4. package/client/lib/opcua-client-read-service.js +27 -15
  5. package/client/lib/opcua-client-subscription-id-service.js +24 -24
  6. package/client/lib/opcua-client-subscription-service.js +175 -170
  7. package/client/lib/opcua-client-write-service.js +146 -146
  8. package/client/opcua-client-config.html +80 -80
  9. package/client/opcua-client-utils.js +217 -22
  10. package/client/opcua-client.html +140 -140
  11. package/client/view/opcua-client.js +1144 -1140
  12. package/examples/flows_simple_opc.json +1 -2851
  13. package/icons/opcua.svg +132 -132
  14. package/icons/opcua2.svg +132 -132
  15. package/package.json +3 -3
  16. package/server/icons/opcua.svg +132 -132
  17. package/server/lib/opcua-address-space-alarm.js +341 -341
  18. package/server/lib/opcua-address-space-builder.js +1797 -1485
  19. package/server/lib/opcua-config.js +800 -546
  20. package/server/lib/opcua-constants.js +120 -109
  21. package/server/lib/opcua-server-events-child.js +139 -139
  22. package/server/lib/opcua-server-methods.js +2 -0
  23. package/server/lib/opcua-server-runtime-child.js +874 -819
  24. package/server/lib/opcua-server-runtime.js +385 -311
  25. package/server/lib/opcua-server-status-child.js +187 -187
  26. package/server/lib/server-node-utils.js +16 -16
  27. package/server/opcua-server-io.html +346 -346
  28. package/server/opcua-server-io.js +497 -496
  29. package/server/opcua-server-registry.js +270 -270
  30. package/server/opcua-server.css +265 -265
  31. package/server/opcua-server.html +158 -1643
  32. package/server/opcua-server.js +24 -13
  33. package/server/view/opcua-server.css +496 -0
  34. package/server/view/opcua-server.js +1585 -0
@@ -1,546 +1,800 @@
1
- "use strict";
2
-
3
- const {
4
- DEFAULT_NAMESPACE_URI,
5
- DEFAULT_RESOURCE_PATH,
6
- DEFAULT_SERVER_NAME,
7
- MessageSecurityMode,
8
- SecurityPolicy,
9
- SECURITY_MODE_MAP,
10
- SECURITY_POLICY_MAP,
11
- DATA_TYPE_MAP,
12
- normalizePort
13
- } = require("./opcua-constants");
14
-
15
- class OpcUaServerConfigParser {
16
- constructor(node) {
17
- this.node = node;
18
- }
19
-
20
- parseNodeConfig(config, credentials) {
21
- const security = this.applySecuritySettings(config.securityPolicy, config.securityMode);
22
- return {
23
- id: this.node.id,
24
- name: config.name,
25
- serverName: config.serverName || DEFAULT_SERVER_NAME,
26
- port: normalizePort(config.port),
27
- maxConnections: this.normalizeMaxConnections(config.maxConnections),
28
- namespaceUri: config.namespaceUri || DEFAULT_NAMESPACE_URI,
29
- resourcePath: config.resourcePath || DEFAULT_RESOURCE_PATH,
30
- treeConfig: this.parseTreeConfig(config.tree),
31
- allowAnonymous: this.normalizeAllowAnonymous(config.allowAnonymous),
32
- users: this.parseUsersConfig(config.users, credentials),
33
- securityPolicy: security.securityPolicy,
34
- securityMode: security.securityMode
35
- };
36
- }
37
-
38
- parseTreeConfig(rawTree) {
39
- try {
40
- return this.normalizeTreeConfig(rawTree);
41
- } catch (error) {
42
- this.node.warn("Invalid tree configuration in editor, using empty tree: " + error.message);
43
- return { objects: [], folders: [], objectsTypes: [], nameSpaces: [] };
44
- }
45
- }
46
-
47
- parseUsersConfig(rawUsers, credentials) {
48
- try {
49
- const users = this.normalizeUsersConfig(rawUsers);
50
- const credentialUser = this.normalizeCredentialUser(credentials);
51
- if (credentialUser) {
52
- users.unshift(credentialUser);
53
- }
54
- return users;
55
- } catch (error) {
56
- this.node.warn("Invalid users configuration in editor, using only credential user: " + error.message);
57
- const credentialUser = this.normalizeCredentialUser(credentials);
58
- return credentialUser ? [credentialUser] : [];
59
- }
60
- }
61
-
62
- normalizeTreeConfig(rawTree) {
63
- let parsed = rawTree;
64
-
65
- if (parsed === undefined || parsed === null || parsed === "") {
66
- parsed = { objects: [], folders: [], objectsTypes: [], nameSpaces: [] };
67
- }
68
-
69
- if (typeof parsed === "string") {
70
- parsed = JSON.parse(parsed);
71
- }
72
-
73
- if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
74
- throw new Error("Tree configuration must be an object");
75
- }
76
-
77
- // Normalize root-level type definitions first so instances can reference them
78
- const objectsTypes = this.normalizeObjectTypes(parsed.objectsTypes || parsed.objectTypes || []);
79
-
80
- // Build a lookup map by type name for fast resolution
81
- this._objectsTypesMap = {};
82
- for (const typeDef of objectsTypes) {
83
- this._objectsTypesMap[typeDef.name] = typeDef;
84
- }
85
-
86
- return {
87
- objects: this.normalizeObjects(parsed.objects || []),
88
- folders: this.normalizeFolders(parsed.folders || []),
89
- objectsTypes,
90
- nameSpaces: this.normalizeNamespaces(parsed.nameSpaces || parsed.namespaces || [])
91
- };
92
- }
93
-
94
- normalizeNamespaces(rawNamespaces) {
95
- if (!Array.isArray(rawNamespaces)) {
96
- throw new Error("'nameSpaces' must be an array");
97
- }
98
-
99
- const seenIds = new Set();
100
- return rawNamespaces.map((namespaceConfig) => {
101
- const normalized = this.normalizeNamespaceDefinition(namespaceConfig);
102
- if (seenIds.has(normalized.id)) {
103
- throw new Error("Duplicate namespace id: " + normalized.id);
104
- }
105
- seenIds.add(normalized.id);
106
- return normalized;
107
- });
108
- }
109
-
110
- normalizeNamespaceDefinition(namespaceConfig) {
111
- if (!namespaceConfig || typeof namespaceConfig !== "object" || Array.isArray(namespaceConfig)) {
112
- throw new Error("Each namespace must be an object");
113
- }
114
-
115
- const id = this.normalizeNamespaceId(namespaceConfig.id);
116
- const name = typeof namespaceConfig.name === "string" ? namespaceConfig.name.trim() : "";
117
-
118
- if (!name) {
119
- throw new Error("Each namespace requires a non-empty name");
120
- }
121
-
122
- return {
123
- id,
124
- name
125
- };
126
- }
127
-
128
- normalizeObjectTypes(objectTypes) {
129
- if (!Array.isArray(objectTypes)) {
130
- throw new Error("'objectsTypes' must be an array");
131
- }
132
-
133
- return objectTypes.map((objectTypeConfig) => this.normalizeBranch(objectTypeConfig, "object type"));
134
- }
135
-
136
- normalizeUsersConfig(rawUsers) {
137
- let parsed = rawUsers;
138
-
139
- if (parsed === undefined || parsed === null || parsed === "") {
140
- parsed = [];
141
- }
142
-
143
- if (typeof parsed === "string") {
144
- parsed = JSON.parse(parsed);
145
- }
146
-
147
- if (!Array.isArray(parsed)) {
148
- throw new Error("Users configuration must be an array");
149
- }
150
-
151
- return parsed.map((userConfig) => this.normalizeUser(userConfig));
152
- }
153
-
154
- normalizeCredentialUser(credentials) {
155
- const safeCredentials = credentials || {};
156
- const username = typeof safeCredentials.username === "string" ? safeCredentials.username.trim() : "";
157
- const password = typeof safeCredentials.password === "string" ? safeCredentials.password : "";
158
-
159
- if (!username || !password) {
160
- return null;
161
- }
162
-
163
- return {
164
- username,
165
- password,
166
- passwordHash: ""
167
- };
168
- }
169
-
170
- normalizeFolders(folders) {
171
- if (!Array.isArray(folders)) {
172
- throw new Error("'folders' must be an array");
173
- }
174
-
175
- return folders.map((folderConfig) => this.normalizeBranch(folderConfig, "folder"));
176
- }
177
-
178
- normalizeObjects(objects) {
179
- if (!Array.isArray(objects)) {
180
- throw new Error("'objects' must be an array");
181
- }
182
-
183
- return objects.map((objectConfig) => this.normalizeBranch(objectConfig, "object"));
184
- }
185
-
186
- normalizeBranch(branchConfig, branchType) {
187
- if (!branchConfig || typeof branchConfig !== "object" || Array.isArray(branchConfig)) {
188
- throw new Error("Each " + branchType + " must be an object");
189
- }
190
-
191
- const name = this.requiredName(branchConfig, branchType);
192
-
193
- return {
194
- name,
195
- displayName: branchConfig.displayName || name,
196
- description: branchConfig.description || "",
197
- nodeId: this.normalizeOptionalNodeId(branchConfig.nodeId),
198
- namespaceId: this.normalizeNamespaceId(branchConfig.namespaceId),
199
- folders: Array.isArray(branchConfig.folders)
200
- ? branchConfig.folders.map((folderConfig) => this.normalizeBranch(folderConfig, "folder"))
201
- : [],
202
- objects: Array.isArray(branchConfig.objects)
203
- ? branchConfig.objects.map((objectConfig) => this.normalizeBranch(objectConfig, "object"))
204
- : [],
205
- variables: Array.isArray(branchConfig.variables)
206
- ? branchConfig.variables.map((variableConfig) => this.normalizeVariable(variableConfig))
207
- : [],
208
- methods: Array.isArray(branchConfig.methods)
209
- ? branchConfig.methods.map((methodConfig) => this.normalizeMethod(methodConfig))
210
- : Array.isArray(branchConfig.method)
211
- ? branchConfig.method.map((methodConfig) => this.normalizeMethod(methodConfig))
212
- : [],
213
- objectsTypes: Array.isArray(branchConfig.objectsTypes)
214
- ? branchConfig.objectsTypes.map((objectTypeConfig) => this.normalizeObjectTypeInstance(objectTypeConfig))
215
- : Array.isArray(branchConfig.objectTypes)
216
- ? branchConfig.objectTypes.map((objectTypeConfig) => this.normalizeObjectTypeInstance(objectTypeConfig))
217
- : [],
218
- alarms: Array.isArray(branchConfig.alarms)
219
- ? branchConfig.alarms.map((alarmConfig) => this.normalizeAlarm(alarmConfig))
220
- : []
221
- };
222
- }
223
-
224
- normalizeObjectTypeInstance(objectTypeConfig) {
225
- if (!objectTypeConfig || typeof objectTypeConfig !== "object" || Array.isArray(objectTypeConfig)) {
226
- throw new Error("Each object type instance must be an object");
227
- }
228
-
229
- const normalizedBranch = this.normalizeBranch(objectTypeConfig, "object type instance");
230
- const objectsType = typeof objectTypeConfig.objectsType === "string" && objectTypeConfig.objectsType.trim()
231
- ? objectTypeConfig.objectsType.trim()
232
- : typeof objectTypeConfig.objectType === "string" && objectTypeConfig.objectType.trim()
233
- ? objectTypeConfig.objectType.trim()
234
- : "";
235
-
236
- if (!objectsType) {
237
- throw new Error("Each object type instance requires a non-empty objectsType");
238
- }
239
-
240
- normalizedBranch.objectsType = objectsType;
241
-
242
- // Children inherited from the type definition are intentionally NOT injected here.
243
- // The builder (walkInheritedChildren) locates and registers them after node-opcua
244
- // creates them automatically via addObject({ typeDefinition }). Injecting them here
245
- // would cause addVariable to be called with the type's nodeId, triggering a
246
- // "nodeId already registered" error from node-opcua.
247
-
248
- return normalizedBranch;
249
- }
250
-
251
- // Returns the string value after "s=" in a nodeId like "ns=2;s=Motor_type2"
252
- _extractNodeIdValue(nodeId) {
253
- if (!nodeId) return "";
254
- const m = nodeId.match(/(?:^|;)s=(.+)$/);
255
- return m ? m[1] : "";
256
- }
257
-
258
- normalizeVariable(variableConfig) {
259
- if (!variableConfig || typeof variableConfig !== "object" || Array.isArray(variableConfig)) {
260
- throw new Error("Each variable must be an object");
261
- }
262
- const name = this.requiredName(variableConfig, "variable");
263
- const type = this.normalizeType(variableConfig.type);
264
- const access = this.normalizeAccess(variableConfig.access);
265
-
266
- return {
267
- name,
268
- type,
269
- access,
270
- value: this.coerceValue(variableConfig.value, type),
271
- description: variableConfig.description || "",
272
- displayName: variableConfig.displayName || name,
273
- nodeId: this.normalizeOptionalNodeId(variableConfig.nodeId),
274
- namespaceId: this.normalizeNamespaceId(variableConfig.namespaceId)
275
- };
276
- }
277
-
278
- normalizeMethod(methodConfig) {
279
- if (!methodConfig || typeof methodConfig !== "object" || Array.isArray(methodConfig)) {
280
- throw new Error("Each method must be an object");
281
- }
282
-
283
- const name = this.requiredName(methodConfig, "method");
284
-
285
- return {
286
- name,
287
- displayName: methodConfig.displayName || name,
288
- description: methodConfig.description || "",
289
- nodeId: this.normalizeOptionalNodeId(methodConfig.nodeId),
290
- namespaceId: this.normalizeNamespaceId(methodConfig.namespaceId),
291
- inputs: Array.isArray(methodConfig.inputs)
292
- ? methodConfig.inputs.map((arg) => this.normalizeMethodArg(arg))
293
- : Array.isArray(methodConfig.inputArguments)
294
- ? methodConfig.inputArguments.map((arg) => this.normalizeMethodArg(arg))
295
- : [],
296
- outputs: Array.isArray(methodConfig.outputs)
297
- ? methodConfig.outputs.map((arg) => this.normalizeMethodArg(arg))
298
- : Array.isArray(methodConfig.outputArguments)
299
- ? methodConfig.outputArguments.map((arg) => this.normalizeMethodArg(arg))
300
- : []
301
- };
302
- }
303
-
304
- normalizeMethodArg(arg) {
305
- if (!arg || typeof arg !== "object" || Array.isArray(arg)) {
306
- throw new Error("Each method arg must be an object");
307
- }
308
-
309
- const name = this.requiredName(arg, "method arg");
310
-
311
- return {
312
- name,
313
- type: this.normalizeType(arg.type),
314
- displayName: arg.displayName || name,
315
- description: arg.description || ""
316
- };
317
- }
318
-
319
- normalizeAlarm(alarmConfig) {
320
- if (!alarmConfig || typeof alarmConfig !== "object" || Array.isArray(alarmConfig)) {
321
- throw new Error("Each alarm must be an object");
322
- }
323
-
324
- const name = this.requiredName(alarmConfig, "alarm");
325
- const type = typeof alarmConfig.type === "string" && alarmConfig.type.trim()
326
- ? alarmConfig.type.trim()
327
- : "levelAlarm";
328
-
329
- const base = {
330
- name,
331
- type,
332
- sourceName: typeof alarmConfig.sourceName === "string" ? alarmConfig.sourceName : name,
333
- severity: Number.isFinite(Number(alarmConfig.severity)) ? Number(alarmConfig.severity) : 500,
334
- variableNodeId: typeof alarmConfig.variableNodeId === "string" ? alarmConfig.variableNodeId : "",
335
- displayName: typeof alarmConfig.displayName === "string" ? alarmConfig.displayName : "",
336
- description: typeof alarmConfig.description === "string" ? alarmConfig.description : "",
337
- nodeId: this.normalizeOptionalNodeId(alarmConfig.nodeId),
338
- namespaceId: this.normalizeNamespaceId(alarmConfig.namespaceId),
339
- enabled: typeof alarmConfig.enabled === "boolean" ? alarmConfig.enabled : true
340
- };
341
-
342
- if (type === "levelAlarm") {
343
- base.highHighLimit = Number.isFinite(Number(alarmConfig.highHighLimit)) ? Number(alarmConfig.highHighLimit) : 100;
344
- base.highHighMessage = typeof alarmConfig.highHighMessage === "string" ? alarmConfig.highHighMessage : "High High alarm";
345
- base.highLimit = Number.isFinite(Number(alarmConfig.highLimit)) ? Number(alarmConfig.highLimit) : 80;
346
- base.highMessage = typeof alarmConfig.highMessage === "string" ? alarmConfig.highMessage : "High alarm";
347
- base.lowLimit = Number.isFinite(Number(alarmConfig.lowLimit)) ? Number(alarmConfig.lowLimit) : 20;
348
- base.lowMessage = typeof alarmConfig.lowMessage === "string" ? alarmConfig.lowMessage : "Low alarm";
349
- base.lowLowLimit = Number.isFinite(Number(alarmConfig.lowLowLimit)) ? Number(alarmConfig.lowLowLimit) : 0;
350
- base.lowLowMessage = typeof alarmConfig.lowLowMessage === "string" ? alarmConfig.lowLowMessage : "Low Low alarm";
351
- } else if (type === "digitalAlarm") {
352
- base.normalStateValue = Number.isFinite(Number(alarmConfig.normalStateValue)) ? Number(alarmConfig.normalStateValue) : 0;
353
- base.digitalMessage = typeof alarmConfig.digitalMessage === "string" ? alarmConfig.digitalMessage : "Digital alarm";
354
- }
355
-
356
- return base;
357
- }
358
-
359
- normalizeUser(userConfig) {
360
- if (!userConfig || typeof userConfig !== "object" || Array.isArray(userConfig)) {
361
- throw new Error("Each user must be an object");
362
- }
363
-
364
- const username = typeof userConfig.username === "string" ? userConfig.username.trim() : "";
365
- const passwordHash = typeof userConfig.passwordHash === "string" ? userConfig.passwordHash : "";
366
- const password = typeof userConfig.password === "string" ? userConfig.password : "";
367
-
368
- if (!username) {
369
- throw new Error("Each user requires a non-empty username");
370
- }
371
-
372
- if (!passwordHash && !password) {
373
- throw new Error("Each user requires a password or password hash");
374
- }
375
-
376
- return {
377
- username,
378
- password,
379
- passwordHash
380
- };
381
- }
382
-
383
- requiredName(entry, label) {
384
- if (!entry || typeof entry.name !== "string" || !entry.name.trim()) {
385
- throw new Error("Each " + label + " requires a non-empty name");
386
- }
387
-
388
- return entry.name.trim();
389
- }
390
-
391
- normalizeOptionalNodeId(nodeId) {
392
- return typeof nodeId === "string" ? nodeId.trim() : "";
393
- }
394
-
395
- normalizeNamespaceId(namespaceId) {
396
- if (namespaceId === undefined || namespaceId === null || namespaceId === "") {
397
- return 2;
398
- }
399
-
400
- const parsed = Number(namespaceId);
401
- if (!Number.isInteger(parsed) || parsed < 2) {
402
- throw new Error("Namespace id must be an integer greater than or equal to 2");
403
- }
404
-
405
- return parsed;
406
- }
407
-
408
- normalizeType(type) {
409
- const normalized = typeof type === "string" ? type.trim() : "";
410
- const aliases = {
411
- int16: "Int16",
412
- uint16: "UInt16",
413
- int32: "Int32",
414
- uint32: "UInt32",
415
- float: "Float",
416
- boolean: "Boolean",
417
- string: "String",
418
- bytestring: "ByteString",
419
- localizedText: "LocalizedText",
420
- };
421
- const canonical = aliases[normalized.toLowerCase()] || normalized;
422
- if (!DATA_TYPE_MAP[canonical]) {
423
- throw new Error("Unsupported variable type: " + type);
424
- }
425
-
426
- return canonical;
427
- }
428
-
429
- normalizeAccess(access) {
430
- const normalized = typeof access === "string" ? access.toLowerCase() : "readonly";
431
- if (normalized === "rw") {
432
- return "readwrite";
433
- }
434
- if (normalized === "ro") {
435
- return "readonly";
436
- }
437
- if (normalized !== "readonly" && normalized !== "readwrite") {
438
- throw new Error("Unsupported access mode: " + access);
439
- }
440
-
441
- return normalized;
442
- }
443
-
444
- normalizeAllowAnonymous(value) {
445
- if (typeof value === "string") {
446
- return value !== "false";
447
- }
448
-
449
- return value !== false;
450
- }
451
-
452
- normalizeMaxConnections(value) {
453
- const parsed = Number(value);
454
- if (!Number.isInteger(parsed) || parsed <= 0) {
455
- return 10;
456
- }
457
- return parsed;
458
- }
459
-
460
- applySecuritySettings(policy, mode) {
461
- const rawPolicy = typeof policy === "string" ? policy.trim() : "None";
462
- const rawMode = typeof mode === "string" ? mode.trim() : "None";
463
-
464
- let securityPolicy = Object.prototype.hasOwnProperty.call(SECURITY_POLICY_MAP, rawPolicy)
465
- ? SECURITY_POLICY_MAP[rawPolicy]
466
- : SECURITY_POLICY_MAP.None;
467
- let securityMode = Object.prototype.hasOwnProperty.call(SECURITY_MODE_MAP, rawMode)
468
- ? SECURITY_MODE_MAP[rawMode]
469
- : SECURITY_MODE_MAP.None;
470
-
471
- if (securityMode === MessageSecurityMode.None) {
472
- securityPolicy = SecurityPolicy.None;
473
- if (rawPolicy !== "None") {
474
- this.node.warn("Security policy adjusted to None because security mode is None");
475
- }
476
- } else if (securityPolicy === SecurityPolicy.None) {
477
- securityPolicy = SecurityPolicy.Basic256Sha256;
478
- this.node.warn("Security policy adjusted to Basic256Sha256 because signed modes require a policy");
479
- }
480
-
481
- return {
482
- securityPolicy,
483
- securityMode
484
- };
485
- }
486
-
487
- coerceValue(value, type) {
488
- if (Array.isArray(value)) {
489
- return value.map((item) => this.coerceScalarValue(item, type));
490
- }
491
-
492
- if (typeof value === "string") {
493
- const trimmed = value.trim();
494
- if (trimmed.startsWith("[") && trimmed.endsWith("]")) {
495
- try {
496
- const parsed = JSON.parse(trimmed);
497
- if (Array.isArray(parsed)) {
498
- return parsed.map((item) => this.coerceScalarValue(item, type));
499
- }
500
- } catch (error) {
501
- throw new Error("Invalid array value for type " + type + ": " + error.message);
502
- }
503
- }
504
- }
505
-
506
- return this.coerceScalarValue(value, type);
507
- }
508
-
509
- coerceScalarValue(value, type) {
510
- if (type === "Int32") {
511
- const parsed = Number(value);
512
- if (!Number.isFinite(parsed)) {
513
- return 0;
514
- }
515
- return Math.trunc(parsed);
516
- }
517
-
518
- if (type === "Float") {
519
- const parsed = Number(value);
520
- if (!Number.isFinite(parsed)) {
521
- return 0;
522
- }
523
- return parsed;
524
- }
525
-
526
- if (type === "Boolean") {
527
- if (typeof value === "string") {
528
- const normalized = value.trim().toLowerCase();
529
- if (normalized === "false" || normalized === "0" || normalized === "") {
530
- return false;
531
- }
532
- if (normalized === "true" || normalized === "1") {
533
- return true;
534
- }
535
- }
536
-
537
- return Boolean(value);
538
- }
539
-
540
- return value === undefined || value === null ? "" : String(value);
541
- }
542
- }
543
-
544
- module.exports = {
545
- OpcUaServerConfigParser
546
- };
1
+ "use strict";
2
+
3
+ const {
4
+ DEFAULT_NAMESPACE_URI,
5
+ DEFAULT_RESOURCE_PATH,
6
+ DEFAULT_SERVER_NAME,
7
+ MessageSecurityMode,
8
+ SecurityPolicy,
9
+ SECURITY_MODE_MAP,
10
+ SECURITY_POLICY_MAP,
11
+ DATA_TYPE_MAP,
12
+ normalizePort
13
+ } = require("./opcua-constants");
14
+
15
+ class OpcUaServerConfigParser {
16
+ constructor(node) {
17
+ this.node = node;
18
+ }
19
+
20
+ parseNodeConfig(config, credentials) {
21
+ const security = this.applySecuritySettings(config.securityPolicy, config.securityMode);
22
+ const auth = this.parseAuthConfig(config, credentials);
23
+ return {
24
+ id: this.node.id,
25
+ name: config.name,
26
+ serverName: config.serverName || DEFAULT_SERVER_NAME,
27
+ port: normalizePort(config.port),
28
+ maxConnections: this.normalizeMaxConnections(config.maxConnections),
29
+ namespaceUri: config.namespaceUri || DEFAULT_NAMESPACE_URI,
30
+ resourcePath: config.resourcePath || DEFAULT_RESOURCE_PATH,
31
+ treeConfig: this.parseTreeConfig(config.tree),
32
+ allowAnonymous: this.normalizeAllowAnonymous(config.allowAnonymous),
33
+ groups: auth.groups,
34
+ users: auth.users,
35
+ securityPolicy: security.securityPolicy,
36
+ securityMode: security.securityMode
37
+ };
38
+ }
39
+
40
+ parseTreeConfig(rawTree) {
41
+ try {
42
+ return this.normalizeTreeConfig(rawTree);
43
+ } catch (error) {
44
+ this.node.warn("Invalid tree configuration in editor, using empty tree: " + error.message);
45
+ return { objects: [], folders: [], objectsTypes: [], nameSpaces: [] };
46
+ }
47
+ }
48
+
49
+ parseAuthConfig(config, credentials) {
50
+ const safeConfig = config || {};
51
+ let groups = [];
52
+ let users = [];
53
+
54
+ try {
55
+ groups = this.normalizeGroupsConfig(
56
+ credentials && credentials.groups !== undefined ? credentials.groups : safeConfig.groups
57
+ );
58
+ } catch (error) {
59
+ this.node.warn("Invalid groups configuration in editor, using derived groups only: " + error.message);
60
+ }
61
+
62
+ try {
63
+ users = this.normalizeUsersConfig(
64
+ credentials && credentials.users !== undefined ? credentials.users : safeConfig.users
65
+ );
66
+ } catch (error) {
67
+ this.node.warn("Invalid users configuration in editor, using only legacy credential user: " + error.message);
68
+ }
69
+
70
+ const credentialUser = this.normalizeCredentialUser(credentials);
71
+ if (credentialUser) {
72
+ users.unshift(credentialUser);
73
+ }
74
+
75
+ return {
76
+ groups: this.buildResolvedGroups(groups, users),
77
+ users
78
+ };
79
+ }
80
+
81
+ buildResolvedGroups(groups, users) {
82
+ const resolved = [];
83
+ const seen = new Set();
84
+
85
+ const addGroup = (groupName) => {
86
+ const normalized = typeof groupName === "string" ? groupName.trim() : "";
87
+ if (!normalized || seen.has(normalized)) {
88
+ return;
89
+ }
90
+ seen.add(normalized);
91
+ resolved.push(normalized);
92
+ };
93
+
94
+ (Array.isArray(groups) ? groups : []).forEach(addGroup);
95
+ (Array.isArray(users) ? users : []).forEach((user) => {
96
+ if (user && user.group) {
97
+ if (typeof user.group === "string") {
98
+ user.group.split(",").forEach(addGroup);
99
+ } else if (Array.isArray(user.group)) {
100
+ user.group.forEach(addGroup);
101
+ } else {
102
+ addGroup(user.group);
103
+ }
104
+ }
105
+ });
106
+
107
+ return resolved;
108
+ }
109
+
110
+ normalizeTreeConfig(rawTree) {
111
+ let parsed = rawTree;
112
+
113
+ if (parsed === undefined || parsed === null || parsed === "") {
114
+ parsed = { objects: [], folders: [], objectsTypes: [], nameSpaces: [] };
115
+ }
116
+
117
+ if (typeof parsed === "string") {
118
+ parsed = JSON.parse(parsed);
119
+ }
120
+
121
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
122
+ throw new Error("Tree configuration must be an object");
123
+ }
124
+
125
+ // Normalize root-level type definitions first so instances can reference them
126
+ const objectsTypes = this.normalizeObjectTypes(parsed.objectsTypes || parsed.objectTypes || []);
127
+
128
+ // Build a lookup map by type name for fast resolution
129
+ this._objectsTypesMap = {};
130
+ for (const typeDef of objectsTypes) {
131
+ this._objectsTypesMap[typeDef.name] = typeDef;
132
+ }
133
+
134
+ const enumerations = this.normalizeEnumerations(parsed.enumerations || parsed.enumeration || []);
135
+ this._enumerationsMap = {};
136
+ for (const enumDef of enumerations) {
137
+ this._enumerationsMap[enumDef.name] = enumDef;
138
+ }
139
+
140
+ return {
141
+ objects: this.normalizeObjects(parsed.objects || []),
142
+ folders: this.normalizeFolders(parsed.folders || []),
143
+ objectsTypes,
144
+ enumerations,
145
+ nameSpaces: this.normalizeNamespaces(parsed.nameSpaces || parsed.namespaces || [])
146
+ };
147
+ }
148
+
149
+ normalizeNamespaces(rawNamespaces) {
150
+ if (!Array.isArray(rawNamespaces)) {
151
+ throw new Error("'nameSpaces' must be an array");
152
+ }
153
+
154
+ const seenIds = new Set();
155
+ return rawNamespaces.map((namespaceConfig) => {
156
+ const normalized = this.normalizeNamespaceDefinition(namespaceConfig);
157
+ if (seenIds.has(normalized.id)) {
158
+ throw new Error("Duplicate namespace id: " + normalized.id);
159
+ }
160
+ seenIds.add(normalized.id);
161
+ return normalized;
162
+ });
163
+ }
164
+
165
+ normalizeNamespaceDefinition(namespaceConfig) {
166
+ if (!namespaceConfig || typeof namespaceConfig !== "object" || Array.isArray(namespaceConfig)) {
167
+ throw new Error("Each namespace must be an object");
168
+ }
169
+
170
+ const id = this.normalizeNamespaceId(namespaceConfig.id);
171
+ const name = typeof namespaceConfig.name === "string" ? namespaceConfig.name.trim() : "";
172
+
173
+ if (!name) {
174
+ throw new Error("Each namespace requires a non-empty name");
175
+ }
176
+
177
+ return {
178
+ id,
179
+ name
180
+ };
181
+ }
182
+
183
+ normalizeAccessPermissions(rawPermissions) {
184
+ let values = rawPermissions;
185
+
186
+ if (values === undefined || values === null || values === "") {
187
+ values = ["public"];
188
+ }
189
+
190
+ if (typeof values === "string") {
191
+ values = values.indexOf(",") >= 0
192
+ ? values.split(",")
193
+ : [values];
194
+ }
195
+
196
+ if (!Array.isArray(values)) {
197
+ throw new Error("'accessPermission' must be an array or string");
198
+ }
199
+
200
+ const seen = new Set();
201
+ const normalized = values.reduce((result, value) => {
202
+ const permission = String(value || "").trim().toLowerCase();
203
+ if (!permission || seen.has(permission)) {
204
+ return result;
205
+ }
206
+ seen.add(permission);
207
+ result.push(permission);
208
+ return result;
209
+ }, []);
210
+
211
+ return normalized.length ? normalized : ["public"];
212
+ }
213
+
214
+ normalizeObjectTypes(objectTypes) {
215
+ if (!Array.isArray(objectTypes)) {
216
+ throw new Error("'objectsTypes' must be an array");
217
+ }
218
+
219
+ return objectTypes.map((objectTypeConfig) => this.normalizeBranch(objectTypeConfig, "object type"));
220
+ }
221
+
222
+ normalizeGroupsConfig(rawGroups) {
223
+ let parsed = rawGroups;
224
+
225
+ if (parsed === undefined || parsed === null || parsed === "") {
226
+ parsed = [];
227
+ }
228
+
229
+ if (typeof parsed === "string") {
230
+ parsed = JSON.parse(parsed);
231
+ }
232
+
233
+ if (!Array.isArray(parsed)) {
234
+ throw new Error("Groups configuration must be an array");
235
+ }
236
+
237
+ const seen = new Set();
238
+ return parsed.reduce((groups, groupConfig) => {
239
+ const groupName = this.normalizeGroup(groupConfig);
240
+ if (!seen.has(groupName)) {
241
+ seen.add(groupName);
242
+ groups.push(groupName);
243
+ }
244
+ return groups;
245
+ }, []);
246
+ }
247
+
248
+ normalizeGroup(groupConfig) {
249
+ if (typeof groupConfig === "string") {
250
+ const groupName = groupConfig.trim();
251
+ if (!groupName) {
252
+ throw new Error("Each group requires a non-empty name");
253
+ }
254
+ return groupName;
255
+ }
256
+
257
+ if (!groupConfig || typeof groupConfig !== "object" || Array.isArray(groupConfig)) {
258
+ throw new Error("Each group must be a string or an object");
259
+ }
260
+
261
+ const groupName = typeof groupConfig.name === "string" ? groupConfig.name.trim() : "";
262
+ if (!groupName) {
263
+ throw new Error("Each group requires a non-empty name");
264
+ }
265
+
266
+ return groupName;
267
+ }
268
+
269
+ normalizeUsersConfig(rawUsers) {
270
+ let parsed = rawUsers;
271
+
272
+ if (parsed === undefined || parsed === null || parsed === "") {
273
+ parsed = [];
274
+ }
275
+
276
+ if (typeof parsed === "string") {
277
+ parsed = JSON.parse(parsed);
278
+ }
279
+
280
+ if (!Array.isArray(parsed)) {
281
+ throw new Error("Users configuration must be an array");
282
+ }
283
+
284
+ return parsed.map((userConfig) => this.normalizeUser(userConfig));
285
+ }
286
+
287
+ normalizeCredentialUser(credentials) {
288
+ const safeCredentials = credentials || {};
289
+ const username = typeof safeCredentials.username === "string" ? safeCredentials.username.trim() : "";
290
+ const password = typeof safeCredentials.password === "string" ? safeCredentials.password : "";
291
+
292
+ if (!username || !password) {
293
+ return null;
294
+ }
295
+
296
+ return {
297
+ username,
298
+ password,
299
+ passwordHash: "",
300
+ group: "default"
301
+ };
302
+ }
303
+
304
+ normalizeFolders(folders) {
305
+ if (!Array.isArray(folders)) {
306
+ throw new Error("'folders' must be an array");
307
+ }
308
+
309
+ return folders.map((folderConfig) => this.normalizeBranch(folderConfig, "folder"));
310
+ }
311
+
312
+ normalizeObjects(objects) {
313
+ if (!Array.isArray(objects)) {
314
+ throw new Error("'objects' must be an array");
315
+ }
316
+
317
+ return objects.map((objectConfig) => this.normalizeBranch(objectConfig, "object"));
318
+ }
319
+
320
+ normalizeBranch(branchConfig, branchType) {
321
+ if (!branchConfig || typeof branchConfig !== "object" || Array.isArray(branchConfig)) {
322
+ throw new Error("Each " + branchType + " must be an object");
323
+ }
324
+
325
+ const name = this.requiredName(branchConfig, branchType);
326
+
327
+ return {
328
+ name,
329
+ displayName: branchConfig.displayName || name,
330
+ description: branchConfig.description || "",
331
+ nodeId: this.normalizeOptionalNodeId(branchConfig.nodeId),
332
+ namespaceId: this.normalizeNamespaceId(branchConfig.namespaceId),
333
+ accessPermission: this.normalizeAccessPermissions(branchConfig.accessPermission || branchConfig.accessPermissions),
334
+ folders: Array.isArray(branchConfig.folders)
335
+ ? branchConfig.folders.map((folderConfig) => this.normalizeBranch(folderConfig, "folder"))
336
+ : [],
337
+ objects: Array.isArray(branchConfig.objects)
338
+ ? branchConfig.objects.map((objectConfig) => this.normalizeBranch(objectConfig, "object"))
339
+ : [],
340
+ variables: Array.isArray(branchConfig.variables)
341
+ ? branchConfig.variables.map((variableConfig) => this.normalizeVariable(variableConfig))
342
+ : [],
343
+ methods: Array.isArray(branchConfig.methods)
344
+ ? branchConfig.methods.map((methodConfig) => this.normalizeMethod(methodConfig))
345
+ : Array.isArray(branchConfig.method)
346
+ ? branchConfig.method.map((methodConfig) => this.normalizeMethod(methodConfig))
347
+ : [],
348
+ objectsTypes: Array.isArray(branchConfig.objectsTypes)
349
+ ? branchConfig.objectsTypes.map((objectTypeConfig) => this.normalizeObjectTypeInstance(objectTypeConfig))
350
+ : Array.isArray(branchConfig.objectTypes)
351
+ ? branchConfig.objectTypes.map((objectTypeConfig) => this.normalizeObjectTypeInstance(objectTypeConfig))
352
+ : [],
353
+ alarms: Array.isArray(branchConfig.alarms)
354
+ ? branchConfig.alarms.map((alarmConfig) => this.normalizeAlarm(alarmConfig))
355
+ : []
356
+ };
357
+ }
358
+
359
+ normalizeObjectTypeInstance(objectTypeConfig) {
360
+ if (!objectTypeConfig || typeof objectTypeConfig !== "object" || Array.isArray(objectTypeConfig)) {
361
+ throw new Error("Each object type instance must be an object");
362
+ }
363
+
364
+ const normalizedBranch = this.normalizeBranch(objectTypeConfig, "object type instance");
365
+ const objectsType = typeof objectTypeConfig.objectsType === "string" && objectTypeConfig.objectsType.trim()
366
+ ? objectTypeConfig.objectsType.trim()
367
+ : typeof objectTypeConfig.objectType === "string" && objectTypeConfig.objectType.trim()
368
+ ? objectTypeConfig.objectType.trim()
369
+ : "";
370
+
371
+ if (!objectsType) {
372
+ throw new Error("Each object type instance requires a non-empty objectsType");
373
+ }
374
+
375
+ normalizedBranch.objectsType = objectsType;
376
+
377
+ // Children inherited from the type definition are intentionally NOT injected here.
378
+ // The builder (walkInheritedChildren) locates and registers them after node-opcua
379
+ // creates them automatically via addObject({ typeDefinition }). Injecting them here
380
+ // would cause addVariable to be called with the type's nodeId, triggering a
381
+ // "nodeId already registered" error from node-opcua.
382
+
383
+ return normalizedBranch;
384
+ }
385
+
386
+ normalizeEnumerations(enumerations) {
387
+ if (!Array.isArray(enumerations)) {
388
+ throw new Error("'enumerations' must be an array");
389
+ }
390
+ return enumerations.map((config) => this.normalizeEnumeration(config));
391
+ }
392
+
393
+ normalizeEnumeration(enumerationConfig) {
394
+ if (!enumerationConfig || typeof enumerationConfig !== "object" || Array.isArray(enumerationConfig)) {
395
+ throw new Error("Each enumeration must be an object");
396
+ }
397
+
398
+ const name = this.requiredName(enumerationConfig, "enumeration");
399
+
400
+ let enumerationStates = [];
401
+ if (Array.isArray(enumerationConfig.enumeration)) {
402
+ enumerationStates = enumerationConfig.enumeration.map(state => {
403
+ return {
404
+ value: Number.isFinite(Number(state.value)) ? Number(state.value) : 0,
405
+ displayName: typeof state.displayName === "string" ? state.displayName : ""
406
+ };
407
+ });
408
+ }
409
+
410
+ return {
411
+ name,
412
+ displayName: enumerationConfig.displayName || name,
413
+ description: enumerationConfig.description || "",
414
+ nodeId: this.normalizeOptionalNodeId(enumerationConfig.nodeId),
415
+ namespaceId: this.normalizeNamespaceId(enumerationConfig.namespaceId),
416
+ accessPermission: this.normalizeAccessPermissions(enumerationConfig.accessPermission || enumerationConfig.accessPermissions),
417
+ enumeration: enumerationStates
418
+ };
419
+ }
420
+
421
+ // Returns the string value after "s=" in a nodeId like "ns=2;s=Motor_type2"
422
+ _extractNodeIdValue(nodeId) {
423
+ if (!nodeId) return "";
424
+ const m = nodeId.match(/(?:^|;)s=(.+)$/);
425
+ return m ? m[1] : "";
426
+ }
427
+
428
+ normalizeVariable(variableConfig) {
429
+ if (!variableConfig || typeof variableConfig !== "object" || Array.isArray(variableConfig)) {
430
+ throw new Error("Each variable must be an object");
431
+ }
432
+ const name = this.requiredName(variableConfig, "variable");
433
+ const type = this.normalizeType(variableConfig.type);
434
+ const access = this.normalizeAccess(variableConfig.access);
435
+
436
+ return {
437
+ name,
438
+ type,
439
+ access,
440
+ value: this.coerceValue(variableConfig.value, type),
441
+ description: variableConfig.description || "",
442
+ displayName: variableConfig.displayName || name,
443
+ nodeId: this.normalizeOptionalNodeId(variableConfig.nodeId),
444
+ namespaceId: this.normalizeNamespaceId(variableConfig.namespaceId),
445
+ accessPermission: this.normalizeAccessPermissions(variableConfig.accessPermission || variableConfig.accessPermissions)
446
+ };
447
+ }
448
+
449
+ normalizeMethod(methodConfig) {
450
+ if (!methodConfig || typeof methodConfig !== "object" || Array.isArray(methodConfig)) {
451
+ throw new Error("Each method must be an object");
452
+ }
453
+
454
+ const name = this.requiredName(methodConfig, "method");
455
+
456
+ return {
457
+ name,
458
+ displayName: methodConfig.displayName || name,
459
+ description: methodConfig.description || "",
460
+ nodeId: this.normalizeOptionalNodeId(methodConfig.nodeId),
461
+ namespaceId: this.normalizeNamespaceId(methodConfig.namespaceId),
462
+ accessPermission: this.normalizeAccessPermissions(methodConfig.accessPermission || methodConfig.accessPermissions),
463
+ inputs: Array.isArray(methodConfig.inputs)
464
+ ? methodConfig.inputs.map((arg) => this.normalizeMethodArg(arg))
465
+ : Array.isArray(methodConfig.inputArguments)
466
+ ? methodConfig.inputArguments.map((arg) => this.normalizeMethodArg(arg))
467
+ : [],
468
+ outputs: Array.isArray(methodConfig.outputs)
469
+ ? methodConfig.outputs.map((arg) => this.normalizeMethodArg(arg))
470
+ : Array.isArray(methodConfig.outputArguments)
471
+ ? methodConfig.outputArguments.map((arg) => this.normalizeMethodArg(arg))
472
+ : []
473
+ };
474
+ }
475
+
476
+ normalizeMethodArg(arg) {
477
+ if (!arg || typeof arg !== "object" || Array.isArray(arg)) {
478
+ throw new Error("Each method arg must be an object");
479
+ }
480
+
481
+ const name = this.requiredName(arg, "method arg");
482
+
483
+ return {
484
+ name,
485
+ type: this.normalizeType(arg.type),
486
+ displayName: arg.displayName || name,
487
+ description: arg.description || ""
488
+ };
489
+ }
490
+
491
+ normalizeAlarm(alarmConfig) {
492
+ if (!alarmConfig || typeof alarmConfig !== "object" || Array.isArray(alarmConfig)) {
493
+ throw new Error("Each alarm must be an object");
494
+ }
495
+
496
+ const name = this.requiredName(alarmConfig, "alarm");
497
+ const type = typeof alarmConfig.type === "string" && alarmConfig.type.trim()
498
+ ? alarmConfig.type.trim()
499
+ : "levelAlarm";
500
+
501
+ const base = {
502
+ name,
503
+ type,
504
+ sourceName: typeof alarmConfig.sourceName === "string" ? alarmConfig.sourceName : name,
505
+ severity: Number.isFinite(Number(alarmConfig.severity)) ? Number(alarmConfig.severity) : 500,
506
+ variableNodeId: typeof alarmConfig.variableNodeId === "string" ? alarmConfig.variableNodeId : "",
507
+ displayName: typeof alarmConfig.displayName === "string" ? alarmConfig.displayName : "",
508
+ description: typeof alarmConfig.description === "string" ? alarmConfig.description : "",
509
+ nodeId: this.normalizeOptionalNodeId(alarmConfig.nodeId),
510
+ namespaceId: this.normalizeNamespaceId(alarmConfig.namespaceId),
511
+ accessPermission: this.normalizeAccessPermissions(alarmConfig.accessPermission || alarmConfig.accessPermissions),
512
+ enabled: typeof alarmConfig.enabled === "boolean" ? alarmConfig.enabled : true
513
+ };
514
+
515
+ if (type === "levelAlarm") {
516
+ base.highHighLimit = Number.isFinite(Number(alarmConfig.highHighLimit)) ? Number(alarmConfig.highHighLimit) : 100;
517
+ base.highHighMessage = typeof alarmConfig.highHighMessage === "string" ? alarmConfig.highHighMessage : "High High alarm";
518
+ base.highLimit = Number.isFinite(Number(alarmConfig.highLimit)) ? Number(alarmConfig.highLimit) : 80;
519
+ base.highMessage = typeof alarmConfig.highMessage === "string" ? alarmConfig.highMessage : "High alarm";
520
+ base.lowLimit = Number.isFinite(Number(alarmConfig.lowLimit)) ? Number(alarmConfig.lowLimit) : 20;
521
+ base.lowMessage = typeof alarmConfig.lowMessage === "string" ? alarmConfig.lowMessage : "Low alarm";
522
+ base.lowLowLimit = Number.isFinite(Number(alarmConfig.lowLowLimit)) ? Number(alarmConfig.lowLowLimit) : 0;
523
+ base.lowLowMessage = typeof alarmConfig.lowLowMessage === "string" ? alarmConfig.lowLowMessage : "Low Low alarm";
524
+ } else if (type === "digitalAlarm") {
525
+ base.normalStateValue = Number.isFinite(Number(alarmConfig.normalStateValue)) ? Number(alarmConfig.normalStateValue) : 0;
526
+ base.digitalMessage = typeof alarmConfig.digitalMessage === "string" ? alarmConfig.digitalMessage : "Digital alarm";
527
+ }
528
+
529
+ return base;
530
+ }
531
+
532
+ normalizeUser(userConfig) {
533
+ if (!userConfig || typeof userConfig !== "object" || Array.isArray(userConfig)) {
534
+ throw new Error("Each user must be an object");
535
+ }
536
+
537
+ const username = typeof userConfig.username === "string" ? userConfig.username.trim() : "";
538
+ const passwordHash = typeof userConfig.passwordHash === "string" ? userConfig.passwordHash : "";
539
+ const password = typeof userConfig.password === "string" ? userConfig.password : "";
540
+
541
+ let group = "";
542
+ if (userConfig.group !== undefined) {
543
+ if (Array.isArray(userConfig.group)) {
544
+ group = userConfig.group.map(g => typeof g === "string" ? g.trim() : "").filter(Boolean).join(",");
545
+ } else if (typeof userConfig.group === "string") {
546
+ group = userConfig.group.trim();
547
+ }
548
+ } else if (userConfig.role !== undefined) {
549
+ if (Array.isArray(userConfig.role)) {
550
+ group = userConfig.role.map(r => typeof r === "string" ? r.trim() : "").filter(Boolean).join(",");
551
+ } else if (typeof userConfig.role === "string") {
552
+ group = userConfig.role.trim();
553
+ }
554
+ }
555
+
556
+ if (!username) {
557
+ throw new Error("Each user requires a non-empty username");
558
+ }
559
+
560
+ if (!passwordHash && !password) {
561
+ throw new Error("Each user requires a password or password hash");
562
+ }
563
+
564
+ if (!group) {
565
+ throw new Error("Each user requires a non-empty group");
566
+ }
567
+
568
+ return {
569
+ username,
570
+ password,
571
+ passwordHash,
572
+ group
573
+ };
574
+ }
575
+
576
+ requiredName(entry, label) {
577
+ if (!entry || typeof entry.name !== "string" || !entry.name.trim()) {
578
+ throw new Error("Each " + label + " requires a non-empty name");
579
+ }
580
+
581
+ return entry.name.trim();
582
+ }
583
+
584
+ normalizeOptionalNodeId(nodeId) {
585
+ return typeof nodeId === "string" ? nodeId.trim() : "";
586
+ }
587
+
588
+ normalizeNamespaceId(namespaceId) {
589
+ if (namespaceId === undefined || namespaceId === null || namespaceId === "") {
590
+ return 2;
591
+ }
592
+
593
+ const parsed = Number(namespaceId);
594
+ if (!Number.isInteger(parsed) || parsed < 2) {
595
+ throw new Error("Namespace id must be an integer greater than or equal to 2");
596
+ }
597
+
598
+ return parsed;
599
+ }
600
+
601
+ normalizeType(type) {
602
+ const normalized = typeof type === "string" ? type.trim() : "";
603
+ const aliases = {
604
+ int16: "Int16",
605
+ uint16: "UInt16",
606
+ int32: "Int32",
607
+ uint32: "UInt32",
608
+ int64: "Int64",
609
+ float: "Float",
610
+ boolean: "Boolean",
611
+ string: "String",
612
+ bytestring: "ByteString",
613
+ localizedText: "LocalizedText",
614
+ };
615
+ const canonical = aliases[normalized.toLowerCase()] || normalized;
616
+ if (!DATA_TYPE_MAP[canonical] && (!this._enumerationsMap || !this._enumerationsMap[canonical])) {
617
+ throw new Error("Unsupported variable type: " + type);
618
+ }
619
+
620
+ return canonical;
621
+ }
622
+
623
+ normalizeAccess(access) {
624
+ const normalized = typeof access === "string" ? access.toLowerCase() : "readonly";
625
+ if (normalized === "rw") {
626
+ return "readwrite";
627
+ }
628
+ if (normalized === "ro") {
629
+ return "readonly";
630
+ }
631
+ if (normalized !== "readonly" && normalized !== "readwrite") {
632
+ throw new Error("Unsupported access mode: " + access);
633
+ }
634
+
635
+ return normalized;
636
+ }
637
+
638
+ normalizeAllowAnonymous(value) {
639
+ if (typeof value === "string") {
640
+ return value !== "false";
641
+ }
642
+
643
+ return value !== false;
644
+ }
645
+
646
+ normalizeMaxConnections(value) {
647
+ const parsed = Number(value);
648
+ if (!Number.isInteger(parsed) || parsed <= 0) {
649
+ return 10;
650
+ }
651
+ return parsed;
652
+ }
653
+
654
+ applySecuritySettings(policy, mode) {
655
+ const rawPolicy = typeof policy === "string" ? policy.trim() : "None";
656
+ const rawMode = typeof mode === "string" ? mode.trim() : "None";
657
+
658
+ let securityPolicy = Object.prototype.hasOwnProperty.call(SECURITY_POLICY_MAP, rawPolicy)
659
+ ? SECURITY_POLICY_MAP[rawPolicy]
660
+ : SECURITY_POLICY_MAP.None;
661
+ let securityMode = Object.prototype.hasOwnProperty.call(SECURITY_MODE_MAP, rawMode)
662
+ ? SECURITY_MODE_MAP[rawMode]
663
+ : SECURITY_MODE_MAP.None;
664
+
665
+ if (securityMode === MessageSecurityMode.None) {
666
+ securityPolicy = SecurityPolicy.None;
667
+ if (rawPolicy !== "None") {
668
+ this.node.warn("Security policy adjusted to None because security mode is None");
669
+ }
670
+ } else if (securityPolicy === SecurityPolicy.None) {
671
+ securityPolicy = SecurityPolicy.Basic256Sha256;
672
+ this.node.warn("Security policy adjusted to Basic256Sha256 because signed modes require a policy");
673
+ }
674
+
675
+ return {
676
+ securityPolicy,
677
+ securityMode
678
+ };
679
+ }
680
+
681
+ coerceValue(value, type) {
682
+ if (Array.isArray(value)) {
683
+ return value.map((item) => this.coerceScalarValue(item, type));
684
+ }
685
+
686
+ if (typeof value === "string") {
687
+ const trimmed = value.trim();
688
+ if (trimmed.startsWith("[") && trimmed.endsWith("]")) {
689
+ try {
690
+ const parsed = JSON.parse(trimmed);
691
+ if (Array.isArray(parsed)) {
692
+ return parsed.map((item) => this.coerceScalarValue(item, type));
693
+ }
694
+ } catch (error) {
695
+ throw new Error("Invalid array value for type " + type + ": " + error.message);
696
+ }
697
+ }
698
+ }
699
+
700
+ return this.coerceScalarValue(value, type);
701
+ }
702
+
703
+ coerceScalarValue(value, type) {
704
+ if (type === "Int32" || (this._enumerationsMap && this._enumerationsMap[type])) {
705
+ const parsed = Number(value);
706
+ if (!Number.isFinite(parsed)) {
707
+ return 0;
708
+ }
709
+ return Math.trunc(parsed);
710
+ }
711
+
712
+ if (type === "Int64") {
713
+ const minVal = -9223372036854775808n;
714
+ const maxVal = 9223372036854775807n;
715
+ try {
716
+ let bigintVal = BigInt(value);
717
+ if (bigintVal < minVal) bigintVal = minVal;
718
+ else if (bigintVal > maxVal) bigintVal = maxVal;
719
+ return String(bigintVal);
720
+ } catch (error) {
721
+ const parsed = Number(value);
722
+ if (Number.isFinite(parsed)) {
723
+ if (parsed >= 9223372036854775807) {
724
+ return String(maxVal);
725
+ }
726
+ if (parsed <= -9223372036854775808) {
727
+ return String(minVal);
728
+ }
729
+ try {
730
+ let bigintVal = BigInt(Math.trunc(parsed));
731
+ if (bigintVal < minVal) bigintVal = minVal;
732
+ else if (bigintVal > maxVal) bigintVal = maxVal;
733
+ return String(bigintVal);
734
+ } catch (e2) {
735
+ return "0";
736
+ }
737
+ }
738
+ return "0";
739
+ }
740
+ }
741
+
742
+ if (type === "UInt64") {
743
+ const minVal = 0n;
744
+ const maxVal = 18446744073709551615n;
745
+ try {
746
+ let bigintVal = BigInt(value);
747
+ if (bigintVal < minVal) bigintVal = minVal;
748
+ else if (bigintVal > maxVal) bigintVal = maxVal;
749
+ return String(bigintVal);
750
+ } catch (error) {
751
+ const parsed = Number(value);
752
+ if (Number.isFinite(parsed)) {
753
+ if (parsed >= 18446744073709551615) {
754
+ return String(maxVal);
755
+ }
756
+ if (parsed <= 0) {
757
+ return String(minVal);
758
+ }
759
+ try {
760
+ let bigintVal = BigInt(Math.trunc(parsed));
761
+ if (bigintVal < minVal) bigintVal = minVal;
762
+ else if (bigintVal > maxVal) bigintVal = maxVal;
763
+ return String(bigintVal);
764
+ } catch (e2) {
765
+ return "0";
766
+ }
767
+ }
768
+ return "0";
769
+ }
770
+ }
771
+
772
+ if (type === "Float") {
773
+ const parsed = Number(value);
774
+ if (!Number.isFinite(parsed)) {
775
+ return 0;
776
+ }
777
+ return parsed;
778
+ }
779
+
780
+ if (type === "Boolean") {
781
+ if (typeof value === "string") {
782
+ const normalized = value.trim().toLowerCase();
783
+ if (normalized === "false" || normalized === "0" || normalized === "") {
784
+ return false;
785
+ }
786
+ if (normalized === "true" || normalized === "1") {
787
+ return true;
788
+ }
789
+ }
790
+
791
+ return Boolean(value);
792
+ }
793
+
794
+ return value === undefined || value === null ? "" : String(value);
795
+ }
796
+ }
797
+
798
+ module.exports = {
799
+ OpcUaServerConfigParser
800
+ };