node-opcua-server-configuration 2.177.0 → 2.179.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/dist/clientTools/push_certificate_management_client.js +1 -1
  2. package/dist/clientTools/push_certificate_management_client.js.map +1 -1
  3. package/dist/server/file_transaction_manager.js +26 -13
  4. package/dist/server/file_transaction_manager.js.map +1 -1
  5. package/dist/server/install_certificate_file_watcher.js +4 -2
  6. package/dist/server/install_certificate_file_watcher.js.map +1 -1
  7. package/dist/server/install_push_certificate_management.d.ts +35 -3
  8. package/dist/server/install_push_certificate_management.js +119 -27
  9. package/dist/server/install_push_certificate_management.js.map +1 -1
  10. package/dist/server/promote_trust_list.js +6 -6
  11. package/dist/server/promote_trust_list.js.map +1 -1
  12. package/dist/server/push_certificate_manager/create_signing_request.js.map +1 -1
  13. package/dist/server/push_certificate_manager/update_certificate.js +34 -4
  14. package/dist/server/push_certificate_manager/update_certificate.js.map +1 -1
  15. package/dist/server/push_certificate_manager_helpers.js +8 -4
  16. package/dist/server/push_certificate_manager_helpers.js.map +1 -1
  17. package/dist/server/push_certificate_manager_server_impl.d.ts +4 -2
  18. package/dist/server/push_certificate_manager_server_impl.js +0 -3
  19. package/dist/server/push_certificate_manager_server_impl.js.map +1 -1
  20. package/dist/server/tools.js +1 -1
  21. package/dist/server/tools.js.map +1 -1
  22. package/package.json +30 -30
  23. package/source/clientTools/push_certificate_management_client.ts +1 -1
  24. package/source/server/file_transaction_manager.ts +26 -13
  25. package/source/server/install_certificate_file_watcher.ts +5 -4
  26. package/source/server/install_push_certificate_management.ts +175 -58
  27. package/source/server/promote_trust_list.ts +16 -15
  28. package/source/server/push_certificate_manager/create_signing_request.ts +4 -2
  29. package/source/server/push_certificate_manager/update_certificate.ts +36 -7
  30. package/source/server/push_certificate_manager_helpers.ts +9 -5
  31. package/source/server/push_certificate_manager_server_impl.ts +6 -9
  32. package/source/server/tools.ts +1 -1
@@ -5,10 +5,11 @@
5
5
  import fs from "node:fs";
6
6
  import os from "node:os";
7
7
  import path from "node:path";
8
- import { make_debugLog, make_errorLog, make_warningLog } from "node-opcua-debug";
8
+ import { checkDebugFlag, make_debugLog, make_errorLog, make_warningLog } from "node-opcua-debug";
9
9
  import { randomBytes } from "node-opcua-utils";
10
10
 
11
11
  const debugLog = make_debugLog("ServerConfiguration");
12
+ const doDebug = checkDebugFlag("ServerConfiguration");
12
13
  const errorLog = make_errorLog("ServerConfiguration");
13
14
  const warningLog = make_warningLog("ServerConfiguration");
14
15
 
@@ -16,7 +17,8 @@ type Functor = () => Promise<void>;
16
17
 
17
18
  async function _copyFile(source: string, dest: string): Promise<void> {
18
19
  try {
19
- debugLog("copying file \n source ", source, "\n =>\n dest ", dest);
20
+ // c8 ignore next
21
+ doDebug && debugLog("copying file \n source ", source, "\n =>\n dest ", dest);
20
22
  const sourceExist = fs.existsSync(source);
21
23
  if (sourceExist) {
22
24
  await fs.promises.copyFile(source, dest);
@@ -30,7 +32,8 @@ async function _deleteFile(file: string): Promise<void> {
30
32
  try {
31
33
  const exists = fs.existsSync(file);
32
34
  if (exists) {
33
- debugLog("deleting file ", file);
35
+ // c8 ignore next
36
+ doDebug && debugLog("deleting file ", file);
34
37
  await fs.promises.unlink(file);
35
38
  }
36
39
  } catch (err) {
@@ -39,7 +42,8 @@ async function _deleteFile(file: string): Promise<void> {
39
42
  }
40
43
 
41
44
  async function _moveFile(source: string, dest: string): Promise<void> {
42
- debugLog("moving file file \n source ", source, "\n =>\n dest ", dest);
45
+ // c8 ignore next
46
+ doDebug && debugLog("moving file file \n source ", source, "\n =>\n dest ", dest);
43
47
  try {
44
48
  await _copyFile(source, dest);
45
49
  await _deleteFile(source);
@@ -50,7 +54,8 @@ async function _moveFile(source: string, dest: string): Promise<void> {
50
54
 
51
55
  async function _moveFileWithBackup(source: string, dest: string, backupPath: string): Promise<void> {
52
56
  // let make a copy of the destination file
53
- debugLog("moveFileWithBackup file \n source ", source, "\n =>\n dest ", dest);
57
+ // c8 ignore next
58
+ doDebug && debugLog("moveFileWithBackup file \n source ", source, "\n =>\n dest ", dest);
54
59
  await _copyFile(dest, backupPath);
55
60
  await _moveFile(source, dest);
56
61
  }
@@ -161,7 +166,8 @@ export class FileTransactionManager {
161
166
  * Commit the transaction by executing all pending file operations.
162
167
  */
163
168
  public async applyFileOps(): Promise<void> {
164
- debugLog("start applyFileOps");
169
+ // c8 ignore next
170
+ doDebug && debugLog("start applyFileOps");
165
171
  const fileOperation = this.#pendingFileOps.splice(0);
166
172
 
167
173
  try {
@@ -169,7 +175,8 @@ export class FileTransactionManager {
169
175
  const op = fileOperation.shift();
170
176
  await op?.();
171
177
  }
172
- debugLog("end applyFileOps");
178
+ // c8 ignore next
179
+ doDebug && debugLog("end applyFileOps");
173
180
 
174
181
  // Transaction successful - clean up backup files
175
182
  await this.#cleanupBackupFiles();
@@ -182,7 +189,8 @@ export class FileTransactionManager {
182
189
  // Rollback: restore all backup files to their original locations
183
190
  try {
184
191
  await this.#rollbackTransaction();
185
- debugLog("Transaction rollback successful");
192
+ // c8 ignore next
193
+ doDebug && debugLog("Transaction rollback successful");
186
194
  } catch (rollbackErr) {
187
195
  errorLog("Critical: Rollback failed:", (rollbackErr as Error).message);
188
196
  errorLog("Certificate state may be inconsistent - manual intervention required");
@@ -202,7 +210,8 @@ export class FileTransactionManager {
202
210
  * This restores files from their *_old backups to recover the previous state.
203
211
  */
204
212
  async #rollbackTransaction(): Promise<void> {
205
- debugLog("Rolling back transaction, restoring", this.#backupFiles.size, "backup files");
213
+ // c8 ignore next
214
+ doDebug && debugLog("Rolling back transaction, restoring", this.#backupFiles.size, "backup files");
206
215
 
207
216
  const rollbackPromises: Promise<void>[] = [];
208
217
 
@@ -212,7 +221,8 @@ export class FileTransactionManager {
212
221
  try {
213
222
  // Check if backup exists before trying to restore
214
223
  if (fs.existsSync(backupPath)) {
215
- debugLog("Restoring backup:", backupPath, "to", dest);
224
+ // c8 ignore next
225
+ doDebug && debugLog("Restoring backup:", backupPath, "to", dest);
216
226
  await _copyFile(backupPath, dest);
217
227
  // Delete backup immediately after restoration
218
228
  await _deleteFile(backupPath);
@@ -225,7 +235,8 @@ export class FileTransactionManager {
225
235
  }
226
236
 
227
237
  await Promise.all(rollbackPromises);
228
- debugLog("Transaction rollback completed");
238
+ // c8 ignore next
239
+ doDebug && debugLog("Transaction rollback completed");
229
240
  }
230
241
 
231
242
  /**
@@ -233,7 +244,8 @@ export class FileTransactionManager {
233
244
  * Removes all *_old backup files that were created during the transaction.
234
245
  */
235
246
  async #cleanupBackupFiles(): Promise<void> {
236
- debugLog("Cleaning up", this.#backupFiles.size, "backup files");
247
+ // c8 ignore next
248
+ doDebug && debugLog("Cleaning up", this.#backupFiles.size, "backup files");
237
249
 
238
250
  const cleanupPromises: Promise<void>[] = [];
239
251
 
@@ -265,7 +277,8 @@ export class FileTransactionManager {
265
277
  }
266
278
 
267
279
  async #executeCleanupTasks(): Promise<void> {
268
- debugLog("Executing cleanup tasks");
280
+ // c8 ignore next
281
+ doDebug && debugLog("Executing cleanup tasks");
269
282
  const tasks = this.#cleanupTasks.splice(0);
270
283
  for (const task of tasks) {
271
284
  try {
@@ -1,12 +1,12 @@
1
1
  import fs from "node:fs";
2
2
  import path from "node:path";
3
3
  import type { ITypedEventEmitter, UAObject, UAObjectEvents } from "node-opcua-address-space-base";
4
- import { make_debugLog } from "node-opcua-debug";
4
+ import { checkDebugFlag, make_debugLog } from "node-opcua-debug";
5
5
 
6
6
  const debugLog = make_debugLog("ServerConfiguration");
7
+ const doDebug = checkDebugFlag("ServerConfiguration");
7
8
 
8
-
9
- export interface CertificateChangeEvents extends UAObjectEvents {
9
+ export interface CertificateChangeEvents extends UAObjectEvents {
10
10
  certificateChange: () => void;
11
11
  }
12
12
  export function installCertificateFileWatcher(
@@ -20,7 +20,8 @@ export function installCertificateFileWatcher(
20
20
  (_eventType: "rename" | "change", filename) => {
21
21
  /** */
22
22
  if (filename === fileToWatch) {
23
- debugLog("filename changed = ", filename, fileToWatch);
23
+ // c8 ignore next
24
+ doDebug && debugLog("filename changed = ", filename, fileToWatch);
24
25
  node.emit("certificateChange");
25
26
  }
26
27
  }
@@ -8,10 +8,17 @@ import chalk from "chalk";
8
8
  import type { AddressSpace, UAServerConfiguration } from "node-opcua-address-space";
9
9
  import { assert } from "node-opcua-assert";
10
10
  import { OPCUACertificateManager } from "node-opcua-certificate-manager";
11
- import { DiskCertificateKeyPairProvider, type ICertificateKeyPairProvider } from "node-opcua-common";
12
- import { type Certificate, split_der, exploreCertificateInfo } from "node-opcua-crypto/web";
11
+ import {
12
+ DiskCertificateKeyPairProvider,
13
+ type ICertificateChainProvider,
14
+ type ICertificateKeyPairProvider,
15
+ ResolvedCertificateKeyPairProvider,
16
+ resolvePrivateKeyProviderIfNeeded
17
+ } from "node-opcua-common";
18
+ import { readPrivateKey } from "node-opcua-crypto";
19
+ import { type Certificate, exploreCertificateInfo, split_der } from "node-opcua-crypto/web";
13
20
  import { checkDebugFlag, make_debugLog, make_errorLog, make_warningLog } from "node-opcua-debug";
14
- import { invalidateServerCertificateCache, type OPCUAServer, type OPCUAServerEndPoint } from "node-opcua-server";
21
+ import type { OPCUAServer, OPCUAServerEndPoint } from "node-opcua-server";
15
22
  import { type StatusCode, StatusCodes } from "node-opcua-status-code";
16
23
  import { type ApplicationDescriptionOptions, ServerState } from "node-opcua-types";
17
24
 
@@ -35,6 +42,7 @@ export interface OPCUAServerPartial extends ICertificateKeyPairProvider {
35
42
  createDefaultCertificate(): Promise<void>;
36
43
  setProvider(provider: ICertificateKeyPairProvider): void;
37
44
  invalidateCachedCertificates(): void;
45
+ getCertificateChainProvider(): ICertificateChainProvider;
38
46
  }
39
47
 
40
48
  async function onCertificateAboutToChange(server: OPCUAServer) {
@@ -56,25 +64,106 @@ async function onCertificateAboutToChange(server: OPCUAServer) {
56
64
  */
57
65
  async function onCertificateChange(server: OPCUAServer) {
58
66
  doDebug && debugLog("on CertificateChanged");
59
- invalidateServerCertificateCache(server);
67
+ await reresolveServerCertificateProvider(server);
60
68
  }
61
69
 
62
70
  /**
63
- * Deferred channel restart: called after the ApplyChanges method
64
- * response has been sent. Shuts down all existing secure channels
65
- * (which forces clients to reconnect with the new cert) and resumes
66
- * the endpoints.
71
+ * Resolve the private key immediately after a rotation, bypassing
72
+ * `OPCUACertificateManager.getPrivateKey()`'s cache.
73
+ *
74
+ * `getPrivateKey()` decrypts the on-disk key *once* and caches it for the
75
+ * manager instance's whole lifetime (pki's documented behavior — see
76
+ * `CertificateManager.getPrivateKey`). That's exactly right for a normal
77
+ * startup resolve (`resolvePrivateKeyProviderIfNeeded`, used by
78
+ * `initializeCM()`), but wrong here: `UpdateCertificate`/`ApplyChanges` just
79
+ * replaced the on-disk key file underneath this *same, long-lived*
80
+ * `certificateManager` instance, so a cached call would silently keep
81
+ * returning the pre-rotation key — certificate and key would then belong to
82
+ * different key pairs, and every subsequent handshake fails signature
83
+ * verification.
84
+ *
85
+ * `privateKeyProvider` mode has no such cache — pki consults the injected
86
+ * provider on every call — so this only special-cases `privateKeyPassphrase`
87
+ * (reading the file directly, with the manager's own configured passphrase,
88
+ * instead of going through the memoized `getPrivateKey()`).
89
+ */
90
+ async function resolvePrivateKeyAfterRotation(certificateManager: OPCUACertificateManager, privateKeyFile: string) {
91
+ const passphrase = await certificateManager.getPrivateKeyPassphrase();
92
+ if (passphrase !== undefined) {
93
+ return readPrivateKey(privateKeyFile, passphrase);
94
+ }
95
+ return certificateManager.getPrivateKey();
96
+ }
97
+
98
+ /**
99
+ * Refresh the server's certificate/private-key provider after a rotation,
100
+ * and push the result to every endpoint.
101
+ *
102
+ * When the certificate manager is passphrase-/provider-managed, a
103
+ * synchronous "invalidate, let it lazily re-read the file" (the old
104
+ * behavior) cannot decrypt the key — so the key is instead resolved here,
105
+ * eagerly and asynchronously (see `resolvePrivateKeyAfterRotation`), and the
106
+ * resulting provider handed out explicitly to the server and every
107
+ * endpoint.
108
+ *
109
+ * For a plain (unmanaged) certificate manager, this falls back to exactly
110
+ * the previous behavior: invalidate the server's and every endpoint's
111
+ * cached certificate/key so the next access re-reads the (plaintext) files
112
+ * from disk.
113
+ */
114
+ async function reresolveServerCertificateProvider(server: OPCUAServer): Promise<void> {
115
+ const certificateManager = server.serverCertificateManager;
116
+ const isManaged = certificateManager instanceof OPCUACertificateManager && certificateManager.isPrivateKeyManaged();
117
+ if (isManaged) {
118
+ const privateKey = await resolvePrivateKeyAfterRotation(certificateManager, server.privateKeyFile);
119
+ server.setProvider(new ResolvedCertificateKeyPairProvider(server.certificateFile, server.privateKeyFile, privateKey));
120
+ const provider = server.getCertificateChainProvider();
121
+ for (const endpoint of server.endpoints) {
122
+ endpoint.setCertificateProvider(provider);
123
+ }
124
+ } else {
125
+ server.invalidateCachedCertificates();
126
+ for (const endpoint of server.endpoints) {
127
+ endpoint.invalidateCertificates();
128
+ }
129
+ }
130
+ }
131
+
132
+ /**
133
+ * Deferred step, called after the ApplyChanges method response has been
134
+ * sent (so the admin client gets its answer before its own channel is
135
+ * affected).
136
+ *
137
+ * With `closeChannelsOnApplyChanges` (the default) every existing secure
138
+ * channel is shut down immediately, forcing all connected clients to
139
+ * reconnect with the new certificate right away. Without it, existing
140
+ * channels are left alone and continue — including OpenSecureChannel
141
+ * *Renews* — under the certificate/key pair each channel is bound to,
142
+ * while new connections use the new pair. See
143
+ * {@link InstallPushCertificateManagementOnServerOptions.closeChannelsOnApplyChanges}
144
+ * for when each is appropriate.
145
+ *
146
+ * Either way the endpoints, suspended in `onCertificateAboutToChange`,
147
+ * are resumed here.
67
148
  */
68
- async function onApplyChangesCompleted(server: OPCUAServer) {
69
- doDebug && debugLog(chalk.yellow(" onApplyChangesCompleted => shutting down channels"));
70
- await server.shutdownChannels();
71
- doDebug && debugLog(chalk.yellow(" onApplyChangesCompleted => channels shut down"));
149
+ async function onApplyChangesCompleted(server: OPCUAServer, closeChannels: boolean) {
150
+ if (closeChannels) {
151
+ doDebug && debugLog(chalk.yellow(" onApplyChangesCompleted => shutting down channels"));
152
+ await server.shutdownChannels();
153
+ doDebug && debugLog(chalk.yellow(" onApplyChangesCompleted => channels shut down"));
154
+ } else {
155
+ doDebug &&
156
+ debugLog(chalk.yellow(" onApplyChangesCompleted => leaving existing channels open (bound to their original pair)"));
157
+ }
72
158
 
73
159
  doDebug && debugLog(chalk.yellow(" onApplyChangesCompleted => resuming end points"));
74
160
  await server.resumeEndPoints();
75
161
  doDebug && debugLog(chalk.yellow(" onApplyChangesCompleted => end points resumed"));
76
162
 
77
- debugLog(chalk.yellow("channels have been closed -> client should reconnect "));
163
+ if (closeChannels) {
164
+ // c8 ignore next
165
+ doDebug && debugLog(chalk.yellow("channels have been closed -> client should reconnect "));
166
+ }
78
167
  }
79
168
 
80
169
  /**
@@ -88,9 +177,11 @@ async function install(this: OPCUAServerPartial): Promise<void> {
88
177
  const certFile = path.join(this.serverCertificateManager.rootDir, CERT_PEM_RELATIVE_PATH);
89
178
  const keyFile = this.serverCertificateManager.privateKey;
90
179
 
91
- // Inject a new disk provider pointing at the cert manager's
92
- // paths. The server's certificateFile/privateKeyFile getters
93
- // now automatically return the new paths.
180
+ // Inject a new disk provider pointing at the cert manager's paths. The
181
+ // server's certificateFile/privateKeyFile getters now automatically
182
+ // return the new paths. If the key turns out to be passphrase-encrypted,
183
+ // resolvePrivateKeyProviderIfNeeded() below immediately replaces this
184
+ // with a resolved, in-memory provider.
94
185
  this.setProvider(new DiskCertificateKeyPairProvider(certFile, keyFile));
95
186
 
96
187
  // Delegate to the base server's createDefaultCertificate() which
@@ -98,20 +189,59 @@ async function install(this: OPCUAServerPartial): Promise<void> {
98
189
  // proper subject via makeSubject(), mutex locking, and file checks.
99
190
  await this.createDefaultCertificate();
100
191
 
101
- // Invalidate any previously cached secrets so that
102
- // getCertificateChain() / getPrivateKey() will re-read from disk.
103
- this.invalidateCachedCertificates();
192
+ // Resolve the (possibly passphrase-encrypted) private key once,
193
+ // asynchronously, and install the resolved provider — a synchronous
194
+ // disk re-read (the old invalidateCachedCertificates() behavior) cannot
195
+ // decrypt an encrypted key. For a plaintext key this just replaces the
196
+ // disk provider above with an equivalent resolved one.
197
+ await resolvePrivateKeyProviderIfNeeded(this, this.serverCertificateManager, false);
104
198
  }
105
199
 
106
200
  interface UAServerConfigurationEx extends UAServerConfiguration {
107
201
  $pushCertificateManager: PushCertificateManagerServerImpl;
108
202
  }
109
203
 
110
- export async function installPushCertificateManagementOnServer(server: OPCUAServer): Promise<void> {
111
- if (!server.engine || !server.engine.addressSpace) {
204
+ export interface InstallPushCertificateManagementOnServerOptions {
205
+ /**
206
+ * What happens to secure channels that are already open when
207
+ * `ApplyChanges` installs a new server certificate (and possibly a new
208
+ * private key).
209
+ *
210
+ * - `true` (default): every existing channel is shut down as soon as
211
+ * the ApplyChanges response has been sent. All connected clients
212
+ * reconnect immediately with the new certificate. This is the safe
213
+ * choice when the rotation is a response to a **compromised or
214
+ * revoked** key: nothing keeps running under the old one.
215
+ *
216
+ * - `false`: existing channels are left open and the rotation is
217
+ * **seamless** for them. Each server channel is bound to the
218
+ * certificate/key pair it was created with (OPC UA Part 6 §6.7.2
219
+ * defines the thumbprint check against "the Certificate it is using
220
+ * for the SecureChannel", and §6.7.4 requires the client to close the
221
+ * channel if a response is signed with a different certificate than
222
+ * the request was encrypted to — so the old pair is the only one a
223
+ * Renew on an old channel can conformantly use). Connected clients
224
+ * keep renewing their security token under the old pair, unmodified
225
+ * third-party clients included; new connections use the new pair.
226
+ * Choose this for a **planned** rotation. The old pair remains in use
227
+ * by those channels until they close naturally — which is exactly why
228
+ * `true` stays the default: when the rotation answers a **compromised
229
+ * or revoked** key, nothing must keep running under it.
230
+ *
231
+ * @defaultValue true
232
+ */
233
+ closeChannelsOnApplyChanges?: boolean;
234
+ }
235
+
236
+ export async function installPushCertificateManagementOnServer(
237
+ server: OPCUAServer,
238
+ options?: InstallPushCertificateManagementOnServerOptions
239
+ ): Promise<void> {
240
+ const closeChannelsOnApplyChanges = options?.closeChannelsOnApplyChanges ?? true;
241
+ if (!server.engine?.addressSpace) {
112
242
  throw new Error(
113
243
  "Server must have a valid address space. " +
114
- "You need to call installPushCertificateManagementOnServer after server has been initialized"
244
+ "You need to call installPushCertificateManagementOnServer after server has been initialized"
115
245
  );
116
246
  }
117
247
  await install.call(server as unknown as OPCUAServerPartial);
@@ -124,15 +254,19 @@ export async function installPushCertificateManagementOnServer(server: OPCUAServ
124
254
  if (!(server.serverCertificateManager instanceof OPCUACertificateManager)) {
125
255
  throw new Error(
126
256
  "installPushCertificateManagementOnServer requires a" +
127
- " disk-based OPCUACertificateManager as" +
128
- " serverCertificateManager"
257
+ " disk-based OPCUACertificateManager as" +
258
+ " serverCertificateManager"
129
259
  );
130
260
  }
131
261
  const cm = server.serverCertificateManager;
132
- const certFile = path.join(cm.rootDir, CERT_PEM_RELATIVE_PATH);
133
- const keyFile = cm.privateKey;
262
+ // Reuse the server's own provider (installed by install() above, and
263
+ // already resolved if the key is passphrase-encrypted) rather than
264
+ // constructing a fresh DiskCertificateKeyPairProvider from the file
265
+ // paths — a plain disk provider would re-read the file and fail
266
+ // synchronously on an encrypted key.
267
+ const provider = server.getCertificateChainProvider();
134
268
  for (const endpoint of server.endpoints) {
135
- endpoint.setCertificateProvider(new DiskCertificateKeyPairProvider(certFile, keyFile));
269
+ endpoint.setCertificateProvider(provider);
136
270
  }
137
271
 
138
272
  await installPushCertificateManagement(server.engine.addressSpace, {
@@ -162,11 +296,11 @@ export async function installPushCertificateManagementOnServer(server: OPCUAServ
162
296
  });
163
297
 
164
298
  serverConfigurationPriv.$pushCertificateManager.on("applyChangesCompleted", () => {
165
- // Fire-and-forget: schedule channel teardown + endpoint
299
+ // Fire-and-forget: schedule (optional) channel teardown + endpoint
166
300
  // resumption AFTER the method response is sent.
167
301
  setImmediate(async () => {
168
302
  try {
169
- await onApplyChangesCompleted(server);
303
+ await onApplyChangesCompleted(server, closeChannelsOnApplyChanges);
170
304
  } catch (err) {
171
305
  errorLog("onApplyChangesCompleted error:", (err as Error).message);
172
306
  }
@@ -238,18 +372,12 @@ function isRelaxableCertificateError(statusCode: StatusCode): boolean {
238
372
  * connection from unintentionally granting trust to every
239
373
  * certificate signed by the same CA.
240
374
  */
241
- async function autoTrustCertificateChain(
242
- server: OPCUAServer,
243
- certificate: Certificate
244
- ): Promise<void> {
375
+ async function autoTrustCertificateChain(server: OPCUAServer, certificate: Certificate): Promise<void> {
245
376
  let chain: Certificate[];
246
377
  try {
247
378
  chain = split_der(certificate);
248
379
  } catch (err) {
249
- warningLog(
250
- "[NoConfiguration] Cannot parse certificate chain for auto-trust:",
251
- (err as Error).message
252
- );
380
+ warningLog("[NoConfiguration] Cannot parse certificate chain for auto-trust:", (err as Error).message);
253
381
  return;
254
382
  }
255
383
 
@@ -263,10 +391,7 @@ async function autoTrustCertificateChain(
263
391
  try {
264
392
  exploreCertificateInfo(cert);
265
393
  } catch (err) {
266
- warningLog(
267
- "[NoConfiguration] Skipping invalid certificate in chain for auto-trust:",
268
- (err as Error).message
269
- );
394
+ warningLog("[NoConfiguration] Skipping invalid certificate in chain for auto-trust:", (err as Error).message);
270
395
  continue;
271
396
  }
272
397
 
@@ -278,10 +403,7 @@ async function autoTrustCertificateChain(
278
403
  // ENOENT can happen if another concurrent call already
279
404
  // moved the cert from rejected to trusted.
280
405
  if ((err as Error & { code?: string }).code !== "ENOENT") {
281
- warningLog(
282
- "[NoConfiguration] Failed to auto-trust leaf certificate:",
283
- (err as Error).message
284
- );
406
+ warningLog("[NoConfiguration] Failed to auto-trust leaf certificate:", (err as Error).message);
285
407
  }
286
408
  }
287
409
  } else {
@@ -290,10 +412,7 @@ async function autoTrustCertificateChain(
290
412
  try {
291
413
  await cm.addIssuer(cert);
292
414
  } catch (err) {
293
- warningLog(
294
- "[NoConfiguration] Failed to add issuer certificate:",
295
- (err as Error).message
296
- );
415
+ warningLog("[NoConfiguration] Failed to add issuer certificate:", (err as Error).message);
297
416
  }
298
417
  }
299
418
  }
@@ -312,10 +431,7 @@ async function autoTrustCertificateChain(
312
431
  * building but do not become trust anchors.
313
432
  */
314
433
  function installCertificateRelaxationHook(server: OPCUAServer): void {
315
- const adjustCertificateStatus = async (
316
- statusCode: StatusCode,
317
- certificate: Certificate
318
- ): Promise<StatusCode> => {
434
+ const adjustCertificateStatus = async (statusCode: StatusCode, certificate: Certificate): Promise<StatusCode> => {
319
435
  // Only relax in NoConfiguration state
320
436
  if (server.engine.getServerState() !== ServerState.NoConfiguration) {
321
437
  return statusCode;
@@ -326,11 +442,12 @@ function installCertificateRelaxationHook(server: OPCUAServer): void {
326
442
  return statusCode;
327
443
  }
328
444
 
329
- doDebug && warningLog(
330
- `[NoConfiguration] Relaxing certificate check:`,
331
- `${statusCode.toString()} → Good`,
332
- "(server is awaiting GDS provisioning)"
333
- );
445
+ doDebug &&
446
+ warningLog(
447
+ `[NoConfiguration] Relaxing certificate check:`,
448
+ `${statusCode.toString()} → Good`,
449
+ "(server is awaiting GDS provisioning)"
450
+ );
334
451
 
335
452
  // Auto-trust the leaf certificate; issuer CAs go to issuers/
336
453
  await autoTrustCertificateChain(server, certificate);
@@ -73,7 +73,7 @@ function trustListIsAlreadyOpened(trustList: UATrustList): boolean {
73
73
  return false;
74
74
  }
75
75
  const dataValue = openCountNode.readValue();
76
- if (!dataValue || !dataValue.value) {
76
+ if (!dataValue?.value) {
77
77
  return false;
78
78
  }
79
79
  const openCount = dataValue.value.value as number;
@@ -109,13 +109,10 @@ function updateLastUpdateTime(trustList: UATrustList): void {
109
109
  * Uses async fs.promises to avoid blocking the event loop on startup
110
110
  * when PKI directories are large or on slow filesystems.
111
111
  */
112
- async function getNewestMtimeFromPkiStore(
113
- cm: OPCUACertificateManager,
114
- isAborted?: () => boolean
115
- ): Promise<Date | null> {
112
+ async function getNewestMtimeFromPkiStore(cm: OPCUACertificateManager, isAborted?: () => boolean): Promise<Date | null> {
116
113
  const dirs = [cm.trustedFolder, cm.crlFolder, cm.issuersCertFolder, cm.issuersCrlFolder];
117
114
  let newest: Date | null = null;
118
-
115
+
119
116
  for (const dir of dirs) {
120
117
  if (isAborted?.()) break;
121
118
 
@@ -130,7 +127,7 @@ async function getNewestMtimeFromPkiStore(
130
127
  } catch {
131
128
  continue;
132
129
  }
133
-
130
+
134
131
  // Process stats sequentially to avoid threadpool exhaustion
135
132
  // and event-loop lag when directories have thousands of files.
136
133
  for (const entry of entries) {
@@ -174,10 +171,10 @@ async function _initializeLastUpdateTimeFromFilesystem(trustList: UATrustListEx)
174
171
 
175
172
  trustList.$$initaliseMTimePromise = (async () => {
176
173
  const startTime = Date.now();
177
- let isAborted = false;
178
-
179
- // Note: Removed abortHandler from registerShutdownTask because AddressSpace
180
- // does not have an unregister mechanism, which causes `_shutdownTasks` to leak
174
+ const isAborted = false;
175
+
176
+ // Note: Removed abortHandler from registerShutdownTask because AddressSpace
177
+ // does not have an unregister mechanism, which causes `_shutdownTasks` to leak
181
178
  // continuously if promoteTrustList is called multiple times.
182
179
  // Sequential scanning is fast enough that it won't block shutdown significantly.
183
180
 
@@ -193,7 +190,9 @@ async function _initializeLastUpdateTimeFromFilesystem(trustList: UATrustListEx)
193
190
  if (!currentValue || currentValue.getTime() <= 0) {
194
191
  const newest = await getNewestMtimeFromPkiStore(cm, () => isAborted);
195
192
  if (isAborted) {
196
- console.log(`[node-opcua] _initializeLastUpdateTimeFromFilesystem aborted for ${trustList.browseName.toString()}`);
193
+ console.log(
194
+ `[node-opcua] _initializeLastUpdateTimeFromFilesystem aborted for ${trustList.browseName.toString()}`
195
+ );
197
196
  return;
198
197
  }
199
198
  if (newest) {
@@ -238,7 +237,9 @@ async function _initializeLastUpdateTimeFromFilesystem(trustList: UATrustListEx)
238
237
  });
239
238
  }
240
239
 
241
- console.log(`[node-opcua] _initializeLastUpdateTimeFromFilesystem took ${Date.now() - startTime}ms for ${trustList.browseName.toString()}`);
240
+ console.log(
241
+ `[node-opcua] _initializeLastUpdateTimeFromFilesystem took ${Date.now() - startTime}ms for ${trustList.browseName.toString()}`
242
+ );
242
243
  } finally {
243
244
  trustList.$$initaliseMTimePromise = undefined;
244
245
  }
@@ -464,7 +465,6 @@ function verificationStatusToStatusCode(status: VerificationStatus): StatusCode
464
465
  return StatusCodes.BadCertificateUntrusted;
465
466
  case VerificationStatus.BadSecurityChecksFailed:
466
467
  return StatusCodes.BadSecurityChecksFailed;
467
- case VerificationStatus.BadCertificateInvalid:
468
468
  default:
469
469
  return StatusCodes.BadCertificateInvalid;
470
470
  }
@@ -662,7 +662,8 @@ export async function promoteTrustList(trustList: UATrustList) {
662
662
  MemFs.unlinkSync(filename);
663
663
  }
664
664
  } catch (err) {
665
- debugLog("could not release trust list backing file", filename, (err as Error).message);
665
+ // c8 ignore next
666
+ doDebug && debugLog("could not release trust list backing file", filename, (err as Error).message);
666
667
  }
667
668
  });
668
669
 
@@ -139,8 +139,10 @@ export async function executeCreateSigningRequest(
139
139
  errorLog(
140
140
  "CreateSigningRequest failed during CSR generation:",
141
141
  (err as Error).message,
142
- "subject=", subjectName,
143
- "applicationUri=", serverImpl.applicationUri
142
+ "subject=",
143
+ subjectName,
144
+ "applicationUri=",
145
+ serverImpl.applicationUri
144
146
  );
145
147
  return { statusCode: StatusCodes.BadInternalError };
146
148
  }