@vitormnm/node-red-simple-opcua 1.0.0

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 (32) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +128 -0
  3. package/client/lib/opcua-client-browser.js +291 -0
  4. package/client/lib/opcua-client-read-service.js +16 -0
  5. package/client/lib/opcua-client-subscription-id-service.js +25 -0
  6. package/client/lib/opcua-client-subscription-service.js +171 -0
  7. package/client/lib/opcua-client-write-service.js +53 -0
  8. package/client/opcua-client-config.html +80 -0
  9. package/client/opcua-client-config.js +159 -0
  10. package/client/opcua-client-utils.js +320 -0
  11. package/client/opcua-client.html +1225 -0
  12. package/client/opcua-client.js +380 -0
  13. package/object.json +65 -0
  14. package/package.json +38 -0
  15. package/resources/editorClient.PNG +0 -0
  16. package/resources/editorServer.PNG +0 -0
  17. package/server/lib/opcua-address-space-alarm.js +341 -0
  18. package/server/lib/opcua-address-space-builder.js +1456 -0
  19. package/server/lib/opcua-config.js +543 -0
  20. package/server/lib/opcua-constants.js +106 -0
  21. package/server/lib/opcua-server-events-child.js +140 -0
  22. package/server/lib/opcua-server-methods.js +198 -0
  23. package/server/lib/opcua-server-runtime-child.js +729 -0
  24. package/server/lib/opcua-server-runtime.js +311 -0
  25. package/server/lib/opcua-server-status-child.js +188 -0
  26. package/server/lib/server-node-utils.js +16 -0
  27. package/server/opcua-server-io.html +347 -0
  28. package/server/opcua-server-io.js +463 -0
  29. package/server/opcua-server-registry.js +270 -0
  30. package/server/opcua-server.css +265 -0
  31. package/server/opcua-server.html +1548 -0
  32. package/server/opcua-server.js +143 -0
@@ -0,0 +1,543 @@
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
+ int32: "Int32",
413
+ float: "Float",
414
+ boolean: "Boolean",
415
+ string: "String",
416
+ bytestring: "ByteString"
417
+ };
418
+ const canonical = aliases[normalized.toLowerCase()] || normalized;
419
+ if (!DATA_TYPE_MAP[canonical]) {
420
+ throw new Error("Unsupported variable type: " + type);
421
+ }
422
+
423
+ return canonical;
424
+ }
425
+
426
+ normalizeAccess(access) {
427
+ const normalized = typeof access === "string" ? access.toLowerCase() : "readonly";
428
+ if (normalized === "rw") {
429
+ return "readwrite";
430
+ }
431
+ if (normalized === "ro") {
432
+ return "readonly";
433
+ }
434
+ if (normalized !== "readonly" && normalized !== "readwrite") {
435
+ throw new Error("Unsupported access mode: " + access);
436
+ }
437
+
438
+ return normalized;
439
+ }
440
+
441
+ normalizeAllowAnonymous(value) {
442
+ if (typeof value === "string") {
443
+ return value !== "false";
444
+ }
445
+
446
+ return value !== false;
447
+ }
448
+
449
+ normalizeMaxConnections(value) {
450
+ const parsed = Number(value);
451
+ if (!Number.isInteger(parsed) || parsed <= 0) {
452
+ return 10;
453
+ }
454
+ return parsed;
455
+ }
456
+
457
+ applySecuritySettings(policy, mode) {
458
+ const rawPolicy = typeof policy === "string" ? policy.trim() : "None";
459
+ const rawMode = typeof mode === "string" ? mode.trim() : "None";
460
+
461
+ let securityPolicy = Object.prototype.hasOwnProperty.call(SECURITY_POLICY_MAP, rawPolicy)
462
+ ? SECURITY_POLICY_MAP[rawPolicy]
463
+ : SECURITY_POLICY_MAP.None;
464
+ let securityMode = Object.prototype.hasOwnProperty.call(SECURITY_MODE_MAP, rawMode)
465
+ ? SECURITY_MODE_MAP[rawMode]
466
+ : SECURITY_MODE_MAP.None;
467
+
468
+ if (securityMode === MessageSecurityMode.None) {
469
+ securityPolicy = SecurityPolicy.None;
470
+ if (rawPolicy !== "None") {
471
+ this.node.warn("Security policy adjusted to None because security mode is None");
472
+ }
473
+ } else if (securityPolicy === SecurityPolicy.None) {
474
+ securityPolicy = SecurityPolicy.Basic256Sha256;
475
+ this.node.warn("Security policy adjusted to Basic256Sha256 because signed modes require a policy");
476
+ }
477
+
478
+ return {
479
+ securityPolicy,
480
+ securityMode
481
+ };
482
+ }
483
+
484
+ coerceValue(value, type) {
485
+ if (Array.isArray(value)) {
486
+ return value.map((item) => this.coerceScalarValue(item, type));
487
+ }
488
+
489
+ if (typeof value === "string") {
490
+ const trimmed = value.trim();
491
+ if (trimmed.startsWith("[") && trimmed.endsWith("]")) {
492
+ try {
493
+ const parsed = JSON.parse(trimmed);
494
+ if (Array.isArray(parsed)) {
495
+ return parsed.map((item) => this.coerceScalarValue(item, type));
496
+ }
497
+ } catch (error) {
498
+ throw new Error("Invalid array value for type " + type + ": " + error.message);
499
+ }
500
+ }
501
+ }
502
+
503
+ return this.coerceScalarValue(value, type);
504
+ }
505
+
506
+ coerceScalarValue(value, type) {
507
+ if (type === "Int32") {
508
+ const parsed = Number(value);
509
+ if (!Number.isFinite(parsed)) {
510
+ return 0;
511
+ }
512
+ return Math.trunc(parsed);
513
+ }
514
+
515
+ if (type === "Float") {
516
+ const parsed = Number(value);
517
+ if (!Number.isFinite(parsed)) {
518
+ return 0;
519
+ }
520
+ return parsed;
521
+ }
522
+
523
+ if (type === "Boolean") {
524
+ if (typeof value === "string") {
525
+ const normalized = value.trim().toLowerCase();
526
+ if (normalized === "false" || normalized === "0" || normalized === "") {
527
+ return false;
528
+ }
529
+ if (normalized === "true" || normalized === "1") {
530
+ return true;
531
+ }
532
+ }
533
+
534
+ return Boolean(value);
535
+ }
536
+
537
+ return value === undefined || value === null ? "" : String(value);
538
+ }
539
+ }
540
+
541
+ module.exports = {
542
+ OpcUaServerConfigParser
543
+ };
@@ -0,0 +1,106 @@
1
+ "use strict";
2
+
3
+ const os = require("os");
4
+ const opcua = require("node-opcua");
5
+
6
+ const {
7
+ OPCUAServer,
8
+ Variant,
9
+ DataType,
10
+ StatusCodes,
11
+ SecurityPolicy,
12
+ VariantArrayType,
13
+ MessageSecurityMode,
14
+ UserTokenType,
15
+ coerceNodeId ,
16
+ } = opcua;
17
+
18
+ const DEFAULT_PORT = 4840;
19
+ const DEFAULT_SERVER_NAME = "Node-RED OPC UA Server";
20
+ const DEFAULT_NAMESPACE_URI = "urn:node-red:opc-ua-server";
21
+ const DEFAULT_RESOURCE_PATH = "/";
22
+
23
+ const SECURITY_POLICY_MAP = {
24
+ None: SecurityPolicy.None,
25
+ Basic128Rsa15: SecurityPolicy.Basic128Rsa15,
26
+ Basic256: SecurityPolicy.Basic256,
27
+ Basic256Sha256: SecurityPolicy.Basic256Sha256,
28
+ Aes128_Sha256_RsaOaep: SecurityPolicy.Aes128_Sha256_RsaOaep,
29
+ Aes256_Sha256_RsaPss: SecurityPolicy.Aes256_Sha256_RsaPss
30
+ };
31
+
32
+ const SECURITY_MODE_MAP = {
33
+ None: MessageSecurityMode.None,
34
+ Sign: MessageSecurityMode.Sign,
35
+ SignAndEncrypt: MessageSecurityMode.SignAndEncrypt
36
+ };
37
+
38
+ const DATA_TYPE_MAP = {
39
+ Int16: DataType.Int16,
40
+ Int32: DataType.Int32,
41
+ Float: DataType.Float,
42
+ Boolean: DataType.Boolean,
43
+ String: DataType.String,
44
+ ByteString : DataType.ByteString
45
+ };
46
+
47
+ function normalizePort(port) {
48
+ const parsed = Number(port);
49
+ if (!Number.isInteger(parsed) || parsed <= 0) {
50
+ return DEFAULT_PORT;
51
+ }
52
+
53
+ return parsed;
54
+ }
55
+
56
+ function buildApplicationUri(serverName) {
57
+ const host = os.hostname() || "localhost";
58
+ return "urn:" + host + ":node-red:" + sanitizeUriSegment(serverName);
59
+ }
60
+
61
+ function sanitizeUriSegment(value) {
62
+ return String(value || "opc-ua-server")
63
+ .trim()
64
+ .replace(/\s+/g, "-")
65
+ .replace(/[^a-zA-Z0-9:_-]/g, "")
66
+ .toLowerCase();
67
+ }
68
+
69
+ function sanitizeNodeIdPath(path) {
70
+ return String(path || "")
71
+ .split(".")
72
+ .map((segment) => sanitizeNodeIdSegment(segment))
73
+ .filter((segment) => segment !== "")
74
+ .join(".");
75
+ }
76
+
77
+ function sanitizeNodeIdSegment(segment) {
78
+ const normalized = String(segment || "")
79
+ .trim()
80
+ .replace(/\s+/g, "_")
81
+ .replace(/[^a-zA-Z0-9._-]/g, "_");
82
+
83
+ return normalized || "item";
84
+ }
85
+
86
+ module.exports = {
87
+ OPCUAServer,
88
+ Variant,
89
+ DataType,
90
+ StatusCodes,
91
+ VariantArrayType,
92
+ SecurityPolicy,
93
+ MessageSecurityMode,
94
+ UserTokenType,
95
+ coerceNodeId,
96
+ DEFAULT_PORT,
97
+ DEFAULT_SERVER_NAME,
98
+ DEFAULT_NAMESPACE_URI,
99
+ DEFAULT_RESOURCE_PATH,
100
+ SECURITY_POLICY_MAP,
101
+ SECURITY_MODE_MAP,
102
+ DATA_TYPE_MAP,
103
+ normalizePort,
104
+ buildApplicationUri,
105
+ sanitizeNodeIdPath
106
+ };