@vitormnm/node-red-simple-opcua 1.6.3 → 1.8.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 (45) hide show
  1. package/README.md +104 -136
  2. package/client/lib/opcua-client-browser.js +254 -11
  3. package/client/lib/opcua-client-method-service.js +1 -1
  4. package/client/lib/opcua-client-subscription-service.js +0 -2
  5. package/client/lib/opcua-client-write-service.js +14 -4
  6. package/client/opcua-client-config.html +118 -1
  7. package/client/opcua-client-config.js +112 -9
  8. package/client/opcua-client-help.html +6 -0
  9. package/client/opcua-client-utils.js +158 -10
  10. package/client/opcua-client.html +8 -0
  11. package/client/opcua-client.js +97 -1
  12. package/client/view/opcua-client.js +106 -14
  13. package/examples/flows_simple_opc.json +1 -1
  14. package/package.json +2 -2
  15. package/server/lib/opcua-address-space-alarm.js +95 -32
  16. package/server/lib/opcua-address-space-builder.js +717 -59
  17. package/server/lib/opcua-config.js +110 -35
  18. package/server/lib/opcua-server-events-child.js +31 -5
  19. package/server/lib/opcua-server-runtime-child.js +424 -27
  20. package/server/lib/opcua-server-runtime.js +52 -5
  21. package/server/lib/opcua-server-status-child.js +46 -15
  22. package/server/nodered/simple_opcua/server/certificates/mutex +0 -0
  23. package/server/nodered/simple_opcua/server/certificates/own/certs/server_selfsigned_cert_2048.pem +25 -0
  24. package/server/nodered/simple_opcua/server/certificates/own/certs/server_selfsigned_cert_2048.pem.mutex +0 -0
  25. package/server/nodered/simple_opcua/server/certificates/own/openssl.cnf +72 -0
  26. package/server/nodered/simple_opcua/server/certificates/own/private/private_key.pem +28 -0
  27. package/server/nodered/simple_opcua/server/certificates/trusted/certs/NodeOPCUA-Client@tuf[c5a9e20a8b680cdff76aaf0165bb3c9318da37a5].pem +25 -0
  28. package/server/nodered/simple_opcua/server/myServer1/mutex +0 -0
  29. package/server/nodered/simple_opcua/server/myServer1/own/certs/server_selfsigned_cert_2048.pem +25 -0
  30. package/server/nodered/simple_opcua/server/myServer1/own/certs/server_selfsigned_cert_2048.pem.mutex +0 -0
  31. package/server/nodered/simple_opcua/server/myServer1/own/openssl.cnf +72 -0
  32. package/server/nodered/simple_opcua/server/myServer1/own/private/private_key.pem +28 -0
  33. package/server/nodered/simple_opcua/server/myServer1/trusted/certs/NodeOPCUA-Client@tuf[91e520c64ff891c67168f08a46dd194071e15dae].pem +25 -0
  34. package/server/nodered/simple_opcua/server/myServer1/trusted/certs/NodeOPCUA-Client@tuf[98ae95da627cea4c500753c319161a3554ee38d7].pem +25 -0
  35. package/server/nodered/simple_opcua/server/myServer1/trusted/certs/NodeOPCUA-Client@tuf[aef8d7a1cfba13d84189a0bcf1694208fc51a7f9].pem +25 -0
  36. package/server/nodered/simple_opcua/server/myServer1/trusted/certs/NodeOPCUA-Client@tuf[c5a9e20a8b680cdff76aaf0165bb3c9318da37a5].pem +25 -0
  37. package/server/nodered/simple_opcua/server/myServer1/trusted/certs/NodeOPCUA-Client@tuf[ebdf9acf1d02e347917a14108d3144799c638ea3].pem +25 -0
  38. package/server/opcua-server-io.html +93 -1
  39. package/server/opcua-server-io.js +153 -29
  40. package/server/opcua-server-registry.js +8 -2
  41. package/server/opcua-server.css +64 -0
  42. package/server/opcua-server.html +168 -44
  43. package/server/opcua-server.js +115 -5
  44. package/server/view/opcua-server.css +100 -6
  45. package/server/view/opcua-server.js +746 -48
@@ -257,18 +257,33 @@ class OpcUaServerProcess {
257
257
 
258
258
 
259
259
 
260
- process.send({
261
- type: "send",
262
- data: msg,
263
- nodeId: nodeId
264
- });
260
+ let hasFailures = false;
261
+ if (readArrayResults) {
262
+ const failed = readArrayResults.filter(item => item && item.status !== "Good");
263
+ if (failed.length) {
264
+ hasFailures = true;
265
+ }
266
+ }
267
+ if (result.directError) {
268
+ hasFailures = true;
269
+ }
270
+
271
+ if (!hasFailures) {
272
+ process.send({
273
+ type: "send",
274
+ data: msg,
275
+ nodeId: nodeId
276
+ });
277
+ }
265
278
 
266
279
  process.send({
267
280
  type: "status",
268
281
  data: {
269
- fill: "green",
282
+ fill: hasFailures ? (result.directError ? "red" : "yellow") : "green",
270
283
  shape: "dot",
271
- text: result.identifiers.length > 1 ? "read " + result.identifiers.length + " tags" : "read " + result.identifiers[0]
284
+ text: hasFailures
285
+ ? (result.directError ? "read failed" : "partial read failed")
286
+ : (result.identifiers.length > 1 ? "read " + result.identifiers.length + " tags" : "read " + result.identifiers[0])
272
287
  },
273
288
  nodeId: nodeId
274
289
  });
@@ -308,6 +323,7 @@ class OpcUaServerProcess {
308
323
  type: "error",
309
324
  data: { fill: "red", shape: "ring", text: "failed read" },
310
325
  error: error.message,
326
+ originalMsg: msg,
311
327
  nodeId: nodeId
312
328
  });
313
329
  }
@@ -362,6 +378,7 @@ class OpcUaServerProcess {
362
378
  type: "error",
363
379
  data: { fill: "red", shape: "ring", text: "failed write" },
364
380
  error: error.message,
381
+ originalMsg: msg,
365
382
  nodeId: nodeId
366
383
  });
367
384
  }
@@ -381,6 +398,14 @@ class OpcUaServerProcess {
381
398
  const target = msg && msg.opcuaServerIo ? msg.opcuaServerIo : {};
382
399
  const identifierType = this.resolveIdentifierType(target);
383
400
 
401
+ let isSingleTag = false;
402
+ try {
403
+ this.resolveIdentifier(target);
404
+ isSingleTag = true;
405
+ } catch (e) {
406
+ // not a single tag
407
+ }
408
+
384
409
  // Buffer serializado pelo IPC
385
410
  if (
386
411
  payload &&
@@ -447,7 +472,7 @@ class OpcUaServerProcess {
447
472
  }
448
473
 
449
474
  // Array de objetos
450
- else if (Array.isArray(payload)) {
475
+ else if (Array.isArray(payload) && (!isSingleTag || this.isBatchWritePayload(payload))) {
451
476
 
452
477
  if (!payload.length) {
453
478
  throw new Error("msg.payload array does not contain any items");
@@ -524,21 +549,33 @@ class OpcUaServerProcess {
524
549
  msg.topic = writtenPaths[0];
525
550
  }
526
551
 
527
- process.send({
528
- type: "send",
529
- data: msg,
530
- nodeId
531
- });
552
+ let hasFailures = false;
553
+ if (Array.isArray(msg.payload) && msg.payload.length && msg.payload[0] && typeof msg.payload[0].status === "string") {
554
+ const failed = msg.payload.filter(item => item.status !== "Good");
555
+ if (failed.length) {
556
+ hasFailures = true;
557
+ }
558
+ }
559
+ if (directError) {
560
+ hasFailures = true;
561
+ }
562
+
563
+ if (!hasFailures) {
564
+ process.send({
565
+ type: "send",
566
+ data: msg,
567
+ nodeId
568
+ });
569
+ }
532
570
 
533
571
  process.send({
534
572
  type: "status",
535
573
  data: {
536
- fill: "green",
574
+ fill: hasFailures ? (directError ? "red" : "yellow") : "green",
537
575
  shape: "dot",
538
- text:
539
- writtenPaths.length > 1
540
- ? `write ${writtenPaths.length} tags`
541
- : `write ${writtenPaths[0]}`
576
+ text: hasFailures
577
+ ? (directError ? "write failed" : "partial write failed")
578
+ : (writtenPaths.length > 1 ? `write ${writtenPaths.length} tags` : `write ${writtenPaths[0]}`)
542
579
  },
543
580
  nodeId
544
581
  });
@@ -582,11 +619,24 @@ class OpcUaServerProcess {
582
619
  text: "failed write"
583
620
  },
584
621
  error: error.message,
622
+ originalMsg: msg,
585
623
  nodeId
586
624
  });
587
625
  }
588
626
  }
589
627
 
628
+ isBatchWritePayload(payload) {
629
+ if (!Array.isArray(payload) || payload.length === 0) {
630
+ return false;
631
+ }
632
+ return payload.every(item =>
633
+ item &&
634
+ typeof item === "object" &&
635
+ !Array.isArray(item) &&
636
+ (item.path !== undefined || item.nodeId !== undefined || item.identifier !== undefined)
637
+ );
638
+ }
639
+
590
640
  resolveIdentifierType(target) {
591
641
  return target && target.identifierType === "nodeId" ? "nodeId" : "path";
592
642
  }
@@ -855,10 +905,12 @@ class OpcUaServerProcess {
855
905
 
856
906
  readActiveAlarms(msg, nodeId) {
857
907
  try {
858
- var result = registry.getActiveAlarms(this.node)
859
- const msg2 = {
860
- payload: result
861
- }
908
+ const result = registry.getActiveAlarms(this.node);
909
+ const safeResult = Array.isArray(result) ? result : [];
910
+
911
+ const msg2 = Object.assign({}, msg || {}, {
912
+ payload: safeResult
913
+ });
862
914
 
863
915
  process.send({
864
916
  type: "send",
@@ -871,17 +923,336 @@ class OpcUaServerProcess {
871
923
  data: {
872
924
  fill: "green",
873
925
  shape: "dot",
874
- text: result.paths.length > 1 ? "read " + result.paths.length + " tags" : "read " + result.paths[0]
926
+ text: safeResult.length + " active alarm" + (safeResult.length !== 1 ? "s" : "")
927
+ },
928
+ nodeId: nodeId
929
+ });
930
+ } catch (error) {
931
+ process.send({
932
+ type: "error",
933
+ data: error.message,
934
+ nodeId: nodeId
935
+ });
936
+ }
937
+ }
938
+
939
+ async readActiveSessions(msg, nodeId) {
940
+ try {
941
+ await this.ensureReady();
942
+ const server = this.node && this.node.server;
943
+ if (!server || !server.engine) {
944
+ throw new Error("OPC UA server is not available");
945
+ }
946
+
947
+ const rawSessions = server.engine._sessions || {};
948
+ const sessions = Object.values(rawSessions).map((session) => buildSessionSnapshot(session));
949
+
950
+ if (msg) {
951
+ const outMsg = Object.assign({}, msg);
952
+ outMsg.payload = sessions;
953
+
954
+ process.send({
955
+ type: "send",
956
+ data: outMsg,
957
+ nodeId: nodeId
958
+ });
959
+ }
960
+
961
+ process.send({
962
+ type: "status",
963
+ data: {
964
+ fill: "green",
965
+ shape: "dot",
966
+ text: sessions.length === 1
967
+ ? "1 session"
968
+ : sessions.length + " sessions"
969
+ },
970
+ nodeId: nodeId
971
+ });
972
+ } catch (error) {
973
+ if (msg) {
974
+ process.send({
975
+ type: "error",
976
+ data: { fill: "red", shape: "ring", text: "failed getSessions" },
977
+ error: error.message,
978
+ originalMsg: msg,
979
+ nodeId: nodeId
980
+ });
981
+ } else {
982
+ process.send({
983
+ type: "status",
984
+ data: { fill: "red", shape: "ring", text: "failed getSessions: " + error.message },
985
+ nodeId: nodeId
986
+ });
987
+ }
988
+ }
989
+ }
990
+
991
+ async deleteActiveSessions(msg, nodeId) {
992
+ try {
993
+ await this.ensureReady();
994
+ const server = this.node && this.node.server;
995
+ if (!server || !server.engine) {
996
+ throw new Error("OPC UA server is not available");
997
+ }
998
+
999
+ const payload = msg && Array.isArray(msg.payload) ? msg.payload : [];
1000
+ if (!payload.length) {
1001
+ throw new Error("msg.payload must be a non-empty array of { sessionId } objects");
1002
+ }
1003
+
1004
+ const engine = server.engine;
1005
+ const rawSessions = engine._sessions || {};
1006
+
1007
+ const results = payload.map((item) => {
1008
+ const requestedId = String(item && item.sessionId || "").trim();
1009
+ if (!requestedId) {
1010
+ return { sessionId: requestedId, status: "error", error: "sessionId is required" };
1011
+ }
1012
+
1013
+ // Sessions are keyed by authenticationToken; find by matching nodeId (the GUID sessionId)
1014
+ const found = Object.values(rawSessions).find(
1015
+ (s) => safeToString(s.nodeId) === requestedId
1016
+ );
1017
+
1018
+ if (!found) {
1019
+ return { sessionId: requestedId, status: "not_found" };
1020
+ }
1021
+
1022
+ try {
1023
+ engine.closeSession(found.authenticationToken, true, "Forcing");
1024
+ return { sessionId: requestedId, status: "deleted" };
1025
+ } catch (closeError) {
1026
+ return { sessionId: requestedId, status: "error", error: closeError.message };
1027
+ }
1028
+ });
1029
+
1030
+ const deletedCount = results.filter((r) => r.status === "deleted").length;
1031
+ const notFoundCount = results.filter((r) => r.status === "not_found").length;
1032
+ const errorCount = results.filter((r) => r.status === "error").length;
1033
+
1034
+ if (msg) {
1035
+ const outMsg = Object.assign({}, msg);
1036
+ outMsg.payload = results;
1037
+
1038
+ process.send({
1039
+ type: "send",
1040
+ data: outMsg,
1041
+ nodeId: nodeId
1042
+ });
1043
+ }
1044
+
1045
+ const statusParts = [];
1046
+ if (deletedCount) statusParts.push("deleted " + deletedCount);
1047
+ if (notFoundCount) statusParts.push("not found " + notFoundCount);
1048
+ if (errorCount) statusParts.push("error " + errorCount);
1049
+
1050
+ process.send({
1051
+ type: "status",
1052
+ data: {
1053
+ fill: errorCount ? "red" : (notFoundCount ? "yellow" : "green"),
1054
+ shape: "dot",
1055
+ text: statusParts.join(", ") || "no sessions"
875
1056
  },
876
1057
  nodeId: nodeId
877
1058
  });
878
- } catch {
1059
+ } catch (error) {
1060
+ if (msg) {
1061
+ process.send({
1062
+ type: "error",
1063
+ data: { fill: "red", shape: "ring", text: "failed deleteSessions" },
1064
+ error: error.message,
1065
+ originalMsg: msg,
1066
+ nodeId: nodeId
1067
+ });
1068
+ } else {
1069
+ process.send({
1070
+ type: "status",
1071
+ data: { fill: "red", shape: "ring", text: "failed deleteSessions: " + error.message },
1072
+ nodeId: nodeId
1073
+ });
1074
+ }
1075
+ }
1076
+ }
1077
+
1078
+ async validateLogin(msg, nodeId) {
1079
+ try {
1080
+ await this.ensureReady();
1081
+ const runtime = this.runtime;
1082
+ if (!runtime) {
1083
+ throw new Error("OPC UA server is not available");
1084
+ }
1085
+
1086
+ const payload = msg && msg.payload ? msg.payload : {};
1087
+ const username = typeof payload.userName === "string" ? payload.userName : (typeof payload.username === "string" ? payload.username : "");
1088
+ const password = typeof payload.password === "string" ? payload.password : "";
1089
+
1090
+ const normalizedUserName = username.trim();
1091
+ const users = Array.isArray(runtime.users) ? runtime.users : [];
1092
+ const user = users.find((entry) => entry && entry.username === normalizedUserName);
1093
+
1094
+ let isValid = false;
1095
+ if (user) {
1096
+ if (user.password && user.password === password) {
1097
+ isValid = true;
1098
+ } else if (user.passwordHash) {
1099
+ let bcrypt = null;
1100
+ try {
1101
+ bcrypt = require("bcryptjs");
1102
+ } catch (e) {
1103
+ bcrypt = null;
1104
+ }
1105
+ if (bcrypt) {
1106
+ try {
1107
+ isValid = bcrypt.compareSync(password, user.passwordHash);
1108
+ } catch (err) {
1109
+ // ignore comparison error
1110
+ }
1111
+ }
1112
+ }
1113
+ }
1114
+
1115
+ let result;
1116
+ if (isValid) {
1117
+ const groups = typeof user.group === "string"
1118
+ ? user.group.split(",").map(g => g.trim()).filter(Boolean)
1119
+ : Array.isArray(user.group)
1120
+ ? user.group
1121
+ : [];
1122
+
1123
+ result = {
1124
+ status: "Good",
1125
+ username: user.username,
1126
+ group: user.group,
1127
+ groups: groups
1128
+ };
1129
+ } else {
1130
+ result = {
1131
+ status: "erro",
1132
+ message: "Invalid username or password"
1133
+ };
1134
+ }
1135
+
1136
+ if (msg) {
1137
+ const outMsg = Object.assign({}, msg);
1138
+ outMsg.payload = result;
1139
+
1140
+ process.send({
1141
+ type: "send",
1142
+ data: outMsg,
1143
+ nodeId: nodeId
1144
+ });
1145
+ }
879
1146
 
1147
+ process.send({
1148
+ type: "status",
1149
+ data: {
1150
+ fill: isValid ? "green" : "yellow",
1151
+ shape: "dot",
1152
+ text: isValid ? "Login: Good" : "Login: erro"
1153
+ },
1154
+ nodeId: nodeId
1155
+ });
1156
+ } catch (error) {
1157
+ if (msg) {
1158
+ const outMsg = Object.assign({}, msg);
1159
+ outMsg.payload = {
1160
+ status: "erro",
1161
+ message: error.message
1162
+ };
1163
+ process.send({
1164
+ type: "send",
1165
+ data: outMsg,
1166
+ nodeId: nodeId
1167
+ });
1168
+ }
1169
+ process.send({
1170
+ type: "status",
1171
+ data: { fill: "red", shape: "ring", text: "Login error: " + error.message },
1172
+ nodeId: nodeId
1173
+ });
880
1174
  }
881
1175
  }
882
1176
 
883
1177
  }
884
1178
 
1179
+ /**
1180
+ * Serializes a node-opcua ServerSession into a plain, IPC-safe object.
1181
+ */
1182
+ function buildSessionSnapshot(session) {
1183
+ return {
1184
+ sessionId: safeToString(session.nodeId),
1185
+ sessionName: String(session.sessionName || ""),
1186
+ status: String(session.__status || ""),
1187
+ creationDate: session.creationDate instanceof Date ? session.creationDate.toISOString() : null,
1188
+ sessionTimeout: safeNumber(session.sessionTimeout),
1189
+ clientLastContactTime: safeNumber(session.clientLastContactTime),
1190
+ channelId: session.channelId != null ? session.channelId : null,
1191
+ clientDescription: buildClientDescription(session.clientDescription),
1192
+ userIdentityToken: buildUserIdentityToken(session.userIdentityToken),
1193
+ channel: buildChannelInfo(session.channel),
1194
+ currentSubscriptionCount: safeNumber(session.currentSubscriptionCount),
1195
+ cumulatedSubscriptionCount: safeNumber(session.cumulatedSubscriptionCount),
1196
+ currentMonitoredItemCount: safeNumber(session.currentMonitoredItemCount),
1197
+ aborted: Boolean(session.aborted)
1198
+ };
1199
+ }
1200
+
1201
+ function safeToString(value) {
1202
+ try {
1203
+ return value != null ? String(value) : null;
1204
+ } catch (_) {
1205
+ return null;
1206
+ }
1207
+ }
1208
+
1209
+ function safeNumber(value) {
1210
+ const n = Number(value);
1211
+ return Number.isFinite(n) ? n : null;
1212
+ }
1213
+
1214
+ function buildClientDescription(desc) {
1215
+ if (!desc || typeof desc !== "object") {
1216
+ return null;
1217
+ }
1218
+ return {
1219
+ applicationUri: safeToString(desc.applicationUri),
1220
+ productUri: safeToString(desc.productUri),
1221
+ applicationName: desc.applicationName && desc.applicationName.text
1222
+ ? String(desc.applicationName.text)
1223
+ : safeToString(desc.applicationName),
1224
+ applicationType: safeToString(desc.applicationType)
1225
+ };
1226
+ }
1227
+
1228
+ function buildUserIdentityToken(token) {
1229
+ if (!token || typeof token !== "object") {
1230
+ return null;
1231
+ }
1232
+ return {
1233
+ policyId: safeToString(token.policyId),
1234
+ userName: safeToString(token.userName),
1235
+ // Never expose passwords or raw credential bytes
1236
+ tokenType: safeToString(token.schema && token.schema.name)
1237
+ };
1238
+ }
1239
+
1240
+ function buildChannelInfo(channel) {
1241
+ if (!channel || typeof channel !== "object") {
1242
+ return null;
1243
+ }
1244
+ return {
1245
+ channelId: channel.channelId != null ? channel.channelId : null,
1246
+ remoteAddress: safeToString(channel.remoteAddress),
1247
+ remotePort: safeNumber(channel.remotePort),
1248
+ bytesRead: safeNumber(channel.bytesRead),
1249
+ bytesWritten: safeNumber(channel.bytesWritten),
1250
+ transactionsCount: safeNumber(channel.transactionsCount),
1251
+ securityMode: safeToString(channel.securityMode),
1252
+ securityPolicy: safeToString(channel.securityPolicy)
1253
+ };
1254
+ }
1255
+
885
1256
  /**
886
1257
  * Instância única do processo
887
1258
  */
@@ -932,14 +1303,36 @@ process.on("message", async (msg) => {
932
1303
  break;
933
1304
 
934
1305
  case "buildServerSnapshot":
935
- OpcUaServerStatusNode(serverProcess.node, msg.msg, msg.nodeId)
936
- break
1306
+ try {
1307
+ await serverProcess.ensureReady();
1308
+ OpcUaServerStatusNode(serverProcess.node, msg.msg, msg.nodeId);
1309
+ } catch (error) {
1310
+ process.send({
1311
+ type: "status",
1312
+ data: {
1313
+ fill: msg.msg ? "red" : "yellow",
1314
+ shape: "ring",
1315
+ text: msg.msg ? "Status: " + error.message : "waiting for server"
1316
+ },
1317
+ nodeId: msg.nodeId
1318
+ });
1319
+ }
1320
+ break;
937
1321
  case "eventsServer":
938
1322
  eventsServer(serverProcess.node, msg.node, msg.nodeId)
939
1323
  break;
940
1324
  case "readActiveAlarms":
941
1325
  serverProcess.readActiveAlarms(msg.msg, msg.nodeId)
942
1326
  break;
1327
+ case "readActiveSessions":
1328
+ await serverProcess.readActiveSessions(msg.msg, msg.nodeId);
1329
+ break;
1330
+ case "deleteActiveSessions":
1331
+ await serverProcess.deleteActiveSessions(msg.msg, msg.nodeId);
1332
+ break;
1333
+ case "validateLogin":
1334
+ await serverProcess.validateLogin(msg.msg, msg.nodeId);
1335
+ break;
943
1336
 
944
1337
 
945
1338
 
@@ -977,4 +1370,8 @@ process.on("unhandledRejection", (reason) => {
977
1370
  data: "Unhandled Rejection: " + (reason?.message || reason),
978
1371
  nodeId: serverProcess.node.id
979
1372
  });
980
- });
1373
+ });
1374
+
1375
+ if (require.main !== module) {
1376
+ module.exports = { OpcUaServerProcess };
1377
+ }
@@ -2,13 +2,17 @@
2
2
 
3
3
 
4
4
 
5
+ const path = require("path");
5
6
  const {
6
7
  OPCUAServer,
7
8
  UserTokenType,
8
9
  buildApplicationUri,
9
10
  makeRoles,
10
11
  WellKnownRoles,
11
- resolveNodeId
12
+ resolveNodeId,
13
+ OPCUACertificateManager,
14
+ SecurityPolicy,
15
+ MessageSecurityMode
12
16
  } = require("./opcua-constants");
13
17
  const { OpcUaAddressSpaceBuilder } = require("./opcua-address-space-builder");
14
18
  const { OpcUaServerMethods } = require("./opcua-server-methods");
@@ -29,13 +33,22 @@ class OpcUaServerRuntime {
29
33
  this.serverName = options.settings.serverName;
30
34
  this.port = options.settings.port;
31
35
  this.maxConnections = options.settings.maxConnections;
36
+ this.minSessionTimeout = options.settings.minSessionTimeout;
37
+ this.defaultSessionTimeout = options.settings.defaultSessionTimeout;
38
+ this.maxSessionTimeout = options.settings.maxSessionTimeout;
32
39
  this.namespaceUri = options.settings.namespaceUri;
33
40
  this.resourcePath = options.settings.resourcePath;
34
41
  this.allowAnonymous = options.settings.allowAnonymous;
42
+ this.automaticallyAcceptUnknownCertificate = options.settings.automaticallyAcceptUnknownCertificate;
43
+ this.certificatesFolder = options.settings.certificatesFolder;
35
44
  this.groups = options.settings.groups;
36
45
  this.users = options.settings.users;
37
- this.securityPolicy = options.settings.securityPolicy;
38
- this.securityMode = options.settings.securityMode;
46
+ this.securityPolicies = Array.isArray(options.settings.securityPolicies) && options.settings.securityPolicies.length > 0
47
+ ? options.settings.securityPolicies
48
+ : [SecurityPolicy.None];
49
+ this.securityModes = Array.isArray(options.settings.securityModes) && options.settings.securityModes.length > 0
50
+ ? options.settings.securityModes
51
+ : [MessageSecurityMode.None];
39
52
  this.treeConfig = options.settings.treeConfig;
40
53
 
41
54
  this.server = null;
@@ -52,6 +65,28 @@ class OpcUaServerRuntime {
52
65
  return;
53
66
  }
54
67
 
68
+ const certificateFolder = this.certificatesFolder || path.resolve(__dirname, "..", "..", "certificates");
69
+ this.serverCertificateManager = new OPCUACertificateManager({
70
+ rootFolder: certificateFolder,
71
+ automaticallyAcceptUnknownCertificate: this.automaticallyAcceptUnknownCertificate
72
+ });
73
+ await this.serverCertificateManager.initialize();
74
+
75
+ // Ensure directories exist immediately on startup
76
+ const fs = require("fs");
77
+ try {
78
+ const trustedDir = path.join(certificateFolder, "trusted", "certs");
79
+ const rejectedDir = path.join(certificateFolder, "rejected");
80
+ if (!fs.existsSync(trustedDir)) {
81
+ fs.mkdirSync(trustedDir, { recursive: true });
82
+ }
83
+ if (!fs.existsSync(rejectedDir)) {
84
+ fs.mkdirSync(rejectedDir, { recursive: true });
85
+ }
86
+ } catch (e) {
87
+ // Ignore directory creation errors
88
+ }
89
+
55
90
  this.server = new OPCUAServer(this.buildServerOptions());
56
91
  await this.server.initialize();
57
92
 
@@ -74,6 +109,7 @@ class OpcUaServerRuntime {
74
109
 
75
110
  this.addressSpaceBuilder.rebuild(this.treeConfig);
76
111
  await this.server.start();
112
+
77
113
  this.registry.registerServer(this);
78
114
 
79
115
  //Methods
@@ -124,6 +160,9 @@ class OpcUaServerRuntime {
124
160
  this.ensureReady();
125
161
  this.syncNamespaces(treeConfig);
126
162
  this.treeConfig = treeConfig;
163
+ // Refresh the user list in the builder so newly added/removed users are
164
+ // recognised in access events without requiring a full server restart.
165
+ this.addressSpaceBuilder.updateUsers(this.users);
127
166
  this.addressSpaceBuilder.sync(treeConfig);
128
167
  }
129
168
 
@@ -200,9 +239,17 @@ class OpcUaServerRuntime {
200
239
  });
201
240
  }
202
241
 
242
+ const certificatesFolder = this.certificatesFolder || path.resolve(__dirname, "..", "..", "certificates");
243
+
203
244
  return {
204
245
  port: this.port,
246
+ minSessionTimeout: this.minSessionTimeout !== undefined ? this.minSessionTimeout : 100,
247
+ defaultSessionTimeout: this.defaultSessionTimeout !== undefined ? this.defaultSessionTimeout : 30000,
248
+ maxSessionTimeout: this.maxSessionTimeout !== undefined ? this.maxSessionTimeout : 3000000,
205
249
  resourcePath: this.resourcePath,
250
+ serverCertificateManager: this.serverCertificateManager,
251
+ certificateFile: path.join(certificatesFolder, "own", "certs", "server_selfsigned_cert_2048.pem"),
252
+ privateKeyFile: path.join(certificatesFolder, "own", "private", "private_key.pem"),
206
253
  buildInfo: {
207
254
  productName: "opc-ua-server",
208
255
  buildNumber: "1",
@@ -216,8 +263,8 @@ class OpcUaServerRuntime {
216
263
  applicationUri: buildApplicationUri(this.serverName),
217
264
  productUri: "urn:node-red:opc-ua-server"
218
265
  },
219
- securityPolicies: [this.securityPolicy],
220
- securityModes: [this.securityMode],
266
+ securityPolicies: this.securityPolicies,
267
+ securityModes: this.securityModes,
221
268
  allowAnonymous: this.allowAnonymous,
222
269
  userManager: {
223
270
  isValidUser: (username, password) => this.isValidUser(username, password),