node-opcua-server-configuration 2.167.0 → 2.169.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 (34) hide show
  1. package/dist/clientTools/push_certificate_management_client.js.map +1 -1
  2. package/dist/index.d.ts +2 -1
  3. package/dist/index.js +2 -1
  4. package/dist/index.js.map +1 -1
  5. package/dist/server/file_transaction_manager.d.ts +10 -0
  6. package/dist/server/file_transaction_manager.js +23 -0
  7. package/dist/server/file_transaction_manager.js.map +1 -1
  8. package/dist/server/{install_push_certitifate_management.d.ts → install_push_certificate_management.d.ts} +3 -2
  9. package/dist/server/install_push_certificate_management.js +263 -0
  10. package/dist/server/install_push_certificate_management.js.map +1 -0
  11. package/dist/server/promote_trust_list.js +154 -3
  12. package/dist/server/promote_trust_list.js.map +1 -1
  13. package/dist/server/push_certificate_manager/create_signing_request.js +19 -13
  14. package/dist/server/push_certificate_manager/create_signing_request.js.map +1 -1
  15. package/dist/server/push_certificate_manager/update_certificate.js +21 -9
  16. package/dist/server/push_certificate_manager/update_certificate.js.map +1 -1
  17. package/dist/server/push_certificate_manager_helpers.js.map +1 -1
  18. package/dist/server/push_certificate_manager_server_impl.js.map +1 -1
  19. package/dist/server/trust_list_server.js +5 -0
  20. package/dist/server/trust_list_server.js.map +1 -1
  21. package/package.json +24 -26
  22. package/source/clientTools/push_certificate_management_client.ts +4 -8
  23. package/source/index.ts +2 -1
  24. package/source/server/file_transaction_manager.ts +25 -0
  25. package/source/server/install_push_certificate_management.ts +332 -0
  26. package/source/server/promote_trust_list.ts +185 -9
  27. package/source/server/push_certificate_manager/create_signing_request.ts +27 -17
  28. package/source/server/push_certificate_manager/update_certificate.ts +25 -8
  29. package/source/server/push_certificate_manager_helpers.ts +1 -1
  30. package/source/server/push_certificate_manager_server_impl.ts +3 -9
  31. package/source/server/trust_list_server.ts +7 -2
  32. package/dist/server/install_push_certitifate_management.js +0 -144
  33. package/dist/server/install_push_certitifate_management.js.map +0 -1
  34. package/source/server/install_push_certitifate_management.ts +0 -193
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "node-opcua-server-configuration",
3
- "version": "2.167.0",
3
+ "version": "2.169.0",
4
4
  "description": "pure nodejs OPCUA SDK - module server-configuration",
5
5
  "scripts": {
6
6
  "build": "tsc -b",
@@ -15,36 +15,34 @@
15
15
  "dependencies": {
16
16
  "chalk": "4.1.2",
17
17
  "memfs": "^4.57.1",
18
- "node-opcua-address-space": "2.167.0",
19
- "node-opcua-address-space-base": "2.167.0",
18
+ "node-opcua-address-space": "2.169.0",
19
+ "node-opcua-address-space-base": "2.169.0",
20
20
  "node-opcua-assert": "2.164.0",
21
- "node-opcua-basic-types": "2.167.0",
22
- "node-opcua-binary-stream": "2.167.0",
23
- "node-opcua-certificate-manager": "2.167.0",
24
- "node-opcua-common": "2.167.0",
21
+ "node-opcua-basic-types": "2.169.0",
22
+ "node-opcua-binary-stream": "2.169.0",
23
+ "node-opcua-certificate-manager": "2.169.0",
24
+ "node-opcua-common": "2.169.0",
25
25
  "node-opcua-constants": "2.157.0",
26
- "node-opcua-crypto": "5.3.3",
27
- "node-opcua-data-model": "2.167.0",
28
- "node-opcua-debug": "2.165.0",
29
- "node-opcua-file-transfer": "2.167.0",
26
+ "node-opcua-crypto": "5.3.5",
27
+ "node-opcua-data-model": "2.169.0",
28
+ "node-opcua-debug": "2.168.0",
29
+ "node-opcua-file-transfer": "2.169.0",
30
30
  "node-opcua-hostname": "2.167.0",
31
- "node-opcua-nodeid": "2.167.0",
32
- "node-opcua-pki": "6.12.0",
33
- "node-opcua-pseudo-session": "2.167.0",
34
- "node-opcua-secure-channel": "2.167.0",
35
- "node-opcua-server": "2.167.0",
36
- "node-opcua-service-translate-browse-path": "2.167.0",
37
- "node-opcua-status-code": "2.167.0",
38
- "node-opcua-types": "2.167.0",
39
- "node-opcua-variant": "2.167.0"
31
+ "node-opcua-nodeid": "2.169.0",
32
+ "node-opcua-pki": "6.13.0",
33
+ "node-opcua-pseudo-session": "2.169.0",
34
+ "node-opcua-secure-channel": "2.169.0",
35
+ "node-opcua-server": "2.169.0",
36
+ "node-opcua-service-translate-browse-path": "2.169.0",
37
+ "node-opcua-status-code": "2.169.0",
38
+ "node-opcua-types": "2.169.0",
39
+ "node-opcua-variant": "2.169.0"
40
40
  },
41
41
  "devDependencies": {
42
- "@types/mocha": "^10.0.10",
43
42
  "bcryptjs": "3.0.3",
44
- "mocha": "11.7.5",
45
- "node-opcua-client": "2.167.0",
46
- "node-opcua-data-value": "2.167.0",
47
- "node-opcua-leak-detector": "2.165.0",
43
+ "node-opcua-client": "2.169.0",
44
+ "node-opcua-data-value": "2.169.0",
45
+ "node-opcua-leak-detector": "2.169.0",
48
46
  "node-opcua-nodesets": "2.163.1"
49
47
  },
50
48
  "author": "Etienne Rossignon",
@@ -62,7 +60,7 @@
62
60
  "internet of things"
63
61
  ],
64
62
  "homepage": "http://node-opcua.github.io/",
65
- "gitHead": "5decfa86ee53a36ecd3bb454e7bf6e3dd27c7a4e",
63
+ "gitHead": "82d570d3e95bea689cbbe30096279885c5282245",
66
64
  "files": [
67
65
  "dist",
68
66
  "source"
@@ -3,7 +3,7 @@
3
3
  */
4
4
  import type { ByteString } from "node-opcua-basic-types";
5
5
  import { BinaryStream } from "node-opcua-binary-stream";
6
- import { combine_der, type Certificate } from "node-opcua-crypto/web";
6
+ import { type Certificate, combine_der } from "node-opcua-crypto/web";
7
7
  import { AttributeIds, coerceQualifiedName, type QualifiedNameLike } from "node-opcua-data-model";
8
8
  import { ClientFile, OpenFileMode } from "node-opcua-file-transfer";
9
9
  import { NodeId, resolveNodeId } from "node-opcua-nodeid";
@@ -141,10 +141,7 @@ export class TrustListClient extends ClientFile implements ITrustList {
141
141
  return callMethodResult.outputArguments?.[0].value as boolean;
142
142
  }
143
143
 
144
- async addCertificate(
145
- certificateChain: Certificate | Certificate[],
146
- isTrustedCertificate: boolean
147
- ): Promise<StatusCode> {
144
+ async addCertificate(certificateChain: Certificate | Certificate[], isTrustedCertificate: boolean): Promise<StatusCode> {
148
145
  await this.ensureInitialized();
149
146
 
150
147
  if (Array.isArray(certificateChain)) {
@@ -153,8 +150,7 @@ export class TrustListClient extends ClientFile implements ITrustList {
153
150
  }
154
151
  }
155
152
 
156
- const compositeCertificate: Buffer =
157
- Array.isArray(certificateChain) ? combine_der(certificateChain) : certificateChain;
153
+ const compositeCertificate: Buffer = Array.isArray(certificateChain) ? combine_der(certificateChain) : certificateChain;
158
154
 
159
155
  const inputArguments: VariantLike[] = [
160
156
  { dataType: DataType.ByteString, value: compositeCertificate },
@@ -223,7 +219,7 @@ export class CertificateGroup {
223
219
  constructor(
224
220
  public session: IBasicSessionAsync,
225
221
  public nodeId: NodeId
226
- ) { }
222
+ ) {}
227
223
  async getCertificateTypes(): Promise<NodeId[]> {
228
224
  const browsePathResult = await this.session.translateBrowsePath(makeBrowsePath(this.nodeId, "/CertificateTypes"));
229
225
  if (browsePathResult.statusCode.isNotGood()) {
package/source/index.ts CHANGED
@@ -6,9 +6,10 @@
6
6
  export * from "./clientTools/certificate_types.js";
7
7
  export * from "./clientTools/push_certificate_management_client.js";
8
8
  export * from "./push_certificate_manager.js";
9
- export * from "./server/install_push_certitifate_management.js";
9
+ export * from "./server/install_push_certificate_management.js";
10
10
  export * from "./server/promote_trust_list.js";
11
11
  export * from "./server/push_certificate_manager/subject_to_string.js";
12
12
  export * from "./server/push_certificate_manager_helpers.js";
13
13
  export * from "./server/push_certificate_manager_server_impl.js";
14
+ export * from "./server/trust_list_server.js";
14
15
  export * from "./standard_certificate_types.js";
@@ -93,6 +93,31 @@ export class FileTransactionManager {
93
93
  this.addFileOp(() => this.#moveFileWithBackupTracked(tempFilePath, destinationPath));
94
94
  }
95
95
 
96
+ /**
97
+ * Stages a file for deletion during the transaction.
98
+ *
99
+ * The file is backed up before removal so it can be restored
100
+ * if the transaction is rolled back. If the file does not
101
+ * exist at apply time the operation is silently skipped.
102
+ *
103
+ * @param filePath - absolute path of the file to remove
104
+ */
105
+ public stageFileRemoval(filePath: string): void {
106
+ this.addFileOp(async () => {
107
+ if (!fs.existsSync(filePath)) {
108
+ return;
109
+ }
110
+ // Create a backup before deleting so rollback can restore it
111
+ const tmpDir = await this.getTmpDir();
112
+ const uniqueFileName = `${crypto.randomBytes(16).toString("hex")}_backup.tmp`;
113
+ const backupPath = path.join(tmpDir, uniqueFileName);
114
+ this.#backupFiles.set(filePath, backupPath);
115
+
116
+ await _copyFile(filePath, backupPath);
117
+ await _deleteFile(filePath);
118
+ });
119
+ }
120
+
96
121
  public addFileOp(functor: Functor): void {
97
122
  this.#pendingFileOps.push(functor);
98
123
  }
@@ -0,0 +1,332 @@
1
+ /**
2
+ * @module node-opcua-server-configuration-server
3
+ */
4
+ import path from "node:path";
5
+
6
+ import chalk from "chalk";
7
+
8
+ import type { AddressSpace, UAServerConfiguration } from "node-opcua-address-space";
9
+ import { assert } from "node-opcua-assert";
10
+ import type { OPCUACertificateManager } from "node-opcua-certificate-manager";
11
+ import { type ICertificateKeyPairProvider, invalidateCachedSecrets } from "node-opcua-common";
12
+ import { type Certificate, split_der, exploreCertificateInfo } from "node-opcua-crypto/web";
13
+ import { checkDebugFlag, make_debugLog, make_errorLog, make_warningLog } from "node-opcua-debug";
14
+ import { invalidateServerCertificateCache, type OPCUAServer, type OPCUAServerEndPoint } from "node-opcua-server";
15
+ import { type StatusCode, StatusCodes } from "node-opcua-status-code";
16
+ import { type ApplicationDescriptionOptions, ServerState } from "node-opcua-types";
17
+
18
+ import { installPushCertificateManagement } from "./push_certificate_manager_helpers.js";
19
+ import type { ActionQueue, PushCertificateManagerServerImpl } from "./push_certificate_manager_server_impl.js";
20
+
21
+ const debugLog = make_debugLog("ServerConfiguration");
22
+ const doDebug = checkDebugFlag("ServerConfiguration");
23
+ const errorLog = make_errorLog("ServerConfiguration");
24
+ const warningLog = make_warningLog("ServerConfiguration");
25
+
26
+ /** Relative path from cert manager root to the leaf certificate PEM. */
27
+ const CERT_PEM_RELATIVE_PATH = "own/certs/certificate.pem";
28
+
29
+ export interface OPCUAServerPartial extends ICertificateKeyPairProvider {
30
+ serverInfo?: ApplicationDescriptionOptions;
31
+ serverCertificateManager: OPCUACertificateManager;
32
+ privateKeyFile: string;
33
+ certificateFile: string;
34
+ engine: { addressSpace?: AddressSpace };
35
+ createDefaultCertificate(): Promise<void>;
36
+ }
37
+
38
+ async function onCertificateAboutToChange(server: OPCUAServer) {
39
+ doDebug && debugLog(chalk.yellow(" onCertificateAboutToChange => Suspending End points"));
40
+ await server.suspendEndPoints();
41
+ doDebug && debugLog(chalk.yellow(" onCertificateAboutToChange => End points suspended"));
42
+ }
43
+
44
+ /**
45
+ * onCertificateChange is called when the serverConfiguration notifies
46
+ * that the server certificate and/or private key has changed.
47
+ *
48
+ * This function invalidates all cached certificates so that new
49
+ * connections immediately serve the updated certificate.
50
+ *
51
+ * Channel teardown is deferred to the `applyChangesCompleted` event
52
+ * so the ApplyChanges OPC-UA method can return its response before
53
+ * the admin client's own channel is destroyed.
54
+ */
55
+ async function onCertificateChange(server: OPCUAServer) {
56
+ doDebug && debugLog("on CertificateChanged");
57
+ invalidateServerCertificateCache(server);
58
+ }
59
+
60
+ /**
61
+ * Deferred channel restart: called after the ApplyChanges method
62
+ * response has been sent. Shuts down all existing secure channels
63
+ * (which forces clients to reconnect with the new cert) and resumes
64
+ * the endpoints.
65
+ */
66
+ async function onApplyChangesCompleted(server: OPCUAServer) {
67
+ doDebug && debugLog(chalk.yellow(" onApplyChangesCompleted => shutting down channels"));
68
+ await server.shutdownChannels();
69
+ doDebug && debugLog(chalk.yellow(" onApplyChangesCompleted => channels shut down"));
70
+
71
+ doDebug && debugLog(chalk.yellow(" onApplyChangesCompleted => resuming end points"));
72
+ await server.resumeEndPoints();
73
+ doDebug && debugLog(chalk.yellow(" onApplyChangesCompleted => end points resumed"));
74
+
75
+ debugLog(chalk.yellow("channels have been closed -> client should reconnect "));
76
+ }
77
+
78
+ /**
79
+ * Redirect the server's `certificateFile` and `privateKeyFile`
80
+ * properties to the cert manager's paths, create a default
81
+ * certificate if none exists, and invalidate cached secrets.
82
+ */
83
+ async function install(this: OPCUAServerPartial): Promise<void> {
84
+ doDebug && debugLog("install push certificate management", this.serverCertificateManager.rootDir);
85
+
86
+ Object.defineProperty(this, "privateKeyFile", {
87
+ get: () => this.serverCertificateManager.privateKey,
88
+ configurable: true,
89
+ enumerable: true
90
+ });
91
+ Object.defineProperty(this, "certificateFile", {
92
+ get: () => path.join(this.serverCertificateManager.rootDir, CERT_PEM_RELATIVE_PATH),
93
+ configurable: true,
94
+ enumerable: true
95
+ });
96
+
97
+ // Delegate to the base server's createDefaultCertificate() which
98
+ // handles DNS (fqdn + hostname + configured), IPs (auto + configured),
99
+ // proper subject via makeSubject(), mutex locking, and file checks.
100
+ await this.createDefaultCertificate();
101
+
102
+ // Invalidate any previously cached secrets so that
103
+ // getCertificateChain() / getPrivateKey() will re-read from disk.
104
+ invalidateCachedSecrets(this);
105
+ }
106
+
107
+ interface UAServerConfigurationEx extends UAServerConfiguration {
108
+ $pushCertificateManager: PushCertificateManagerServerImpl;
109
+ }
110
+
111
+ export async function installPushCertificateManagementOnServer(server: OPCUAServer): Promise<void> {
112
+ if (!server.engine || !server.engine.addressSpace) {
113
+ throw new Error(
114
+ "Server must have a valid address space. " +
115
+ "You need to call installPushCertificateManagementOnServer after server has been initialized"
116
+ );
117
+ }
118
+ await install.call(server as unknown as OPCUAServerPartial);
119
+
120
+ // After install() redirected certificateFile / privateKeyFile,
121
+ // the SecretHolder(this) in each endpoint already follows the
122
+ // new paths. Just invalidate their cached values so the next
123
+ // access re-reads from the cert manager's files.
124
+ invalidateServerCertificateCache(server);
125
+
126
+ await installPushCertificateManagement(server.engine.addressSpace, {
127
+ applicationGroup: server.serverCertificateManager,
128
+ userTokenGroup: server.userCertificateManager,
129
+
130
+ applicationUri: server.serverInfo.applicationUri || "InvalidURI"
131
+ });
132
+
133
+ const serverConfiguration = server.engine.addressSpace.rootFolder.objects.server.getChildByName("ServerConfiguration");
134
+ const serverConfigurationPriv = serverConfiguration as UAServerConfigurationEx;
135
+ assert(serverConfigurationPriv.$pushCertificateManager);
136
+
137
+ serverConfigurationPriv.$pushCertificateManager.on("CertificateAboutToChange", (actionQueue: ActionQueue) => {
138
+ actionQueue.push(async (): Promise<void> => {
139
+ doDebug && debugLog("CertificateAboutToChange Event received");
140
+ await onCertificateAboutToChange(server);
141
+ doDebug && debugLog("CertificateAboutToChange Event processed");
142
+ });
143
+ });
144
+ serverConfigurationPriv.$pushCertificateManager.on("CertificateChanged", (actionQueue: ActionQueue) => {
145
+ actionQueue.push(async (): Promise<void> => {
146
+ doDebug && debugLog("CertificateChanged Event received");
147
+ await onCertificateChange(server);
148
+ doDebug && debugLog("CertificateChanged Event processed");
149
+ });
150
+ });
151
+
152
+ serverConfigurationPriv.$pushCertificateManager.on("applyChangesCompleted", () => {
153
+ // Fire-and-forget: schedule channel teardown + endpoint
154
+ // resumption AFTER the method response is sent.
155
+ setImmediate(async () => {
156
+ try {
157
+ await onApplyChangesCompleted(server);
158
+ } catch (err) {
159
+ errorLog("onApplyChangesCompleted error:", (err as Error).message);
160
+ }
161
+ });
162
+ });
163
+
164
+ // ── Install NoConfiguration certificate relaxation ─────────
165
+ //
166
+ // When the server is in NoConfiguration state (awaiting GDS
167
+ // provisioning), relax certain certificate trust/CRL errors
168
+ // so that the GDS client can connect and provision the server.
169
+ //
170
+ // This hook is ONLY installed when push certificate management
171
+ // is active — bare servers are completely unaffected.
172
+ installCertificateRelaxationHook(server);
173
+ }
174
+
175
+ // ── Certificate relaxation for NoConfiguration state ──────────
176
+
177
+ /**
178
+ * Status codes that can be relaxed during NoConfiguration state.
179
+ *
180
+ * These represent "trust infrastructure not yet set up" situations
181
+ * (missing issuers, missing CRLs) — NOT security violations
182
+ * (revoked, expired, invalid).
183
+ *
184
+ * ### Why `BadCertificateUntrusted` MUST be included (SECURITY NOTE)
185
+ *
186
+ * In `NoConfiguration` the server's trust store is **empty** — no
187
+ * trusted certificates and no CRLs exist yet. A GDS client that
188
+ * connects to provision the server will inevitably present a
189
+ * certificate that is "untrusted" simply because nothing is trusted
190
+ * yet. Removing `BadCertificateUntrusted` from this list would make
191
+ * it impossible for the GDS to connect, breaking the entire push
192
+ * certificate management provisioning workflow (chicken-and-egg).
193
+ *
194
+ * **Security boundary:** this relaxation is ONLY active while the
195
+ * server state is `ServerState.NoConfiguration`. Once the server
196
+ * transitions to `Running` (after successful provisioning) the
197
+ * relaxation hook returns the original status code unchanged and
198
+ * normal strict certificate validation applies. The accepted
199
+ * certificate is auto-trusted (see `autoTrustCertificateChain`)
200
+ * so that subsequent connections succeed under normal validation.
201
+ *
202
+ * Errors that indicate an active security violation (revoked,
203
+ * expired, invalid signature, wrong usage) are **never** relaxed,
204
+ * even in `NoConfiguration`.
205
+ */
206
+ function isRelaxableCertificateError(statusCode: StatusCode): boolean {
207
+ return (
208
+ StatusCodes.BadCertificateUntrusted.equals(statusCode) ||
209
+ StatusCodes.BadCertificateRevocationUnknown.equals(statusCode) ||
210
+ StatusCodes.BadCertificateIssuerRevocationUnknown.equals(statusCode) ||
211
+ StatusCodes.BadCertificateChainIncomplete.equals(statusCode)
212
+ );
213
+ }
214
+
215
+ /**
216
+ * Auto-trust the client's leaf certificate during NoConfiguration.
217
+ *
218
+ * Only the **leaf** (chain[0]) is placed in the trusted store —
219
+ * this is sufficient for the GDS client to reconnect after the
220
+ * server transitions to Running (the PKI verifier short-circuits
221
+ * to `Good` when the leaf itself is explicitly trusted).
222
+ *
223
+ * Issuer (CA) certificates from the chain are added to the
224
+ * **issuers/** store for chain-building purposes, but they do
225
+ * NOT become trust anchors. This prevents a single provisioning
226
+ * connection from unintentionally granting trust to every
227
+ * certificate signed by the same CA.
228
+ */
229
+ async function autoTrustCertificateChain(
230
+ server: OPCUAServer,
231
+ certificate: Certificate
232
+ ): Promise<void> {
233
+ let chain: Certificate[];
234
+ try {
235
+ chain = split_der(certificate);
236
+ } catch (err) {
237
+ warningLog(
238
+ "[NoConfiguration] Cannot parse certificate chain for auto-trust:",
239
+ (err as Error).message
240
+ );
241
+ return;
242
+ }
243
+
244
+ const cm = server.serverCertificateManager;
245
+
246
+ for (let i = 0; i < chain.length; i++) {
247
+ const cert = chain[i];
248
+ // Validate the DER structure before persisting.
249
+ // Garbage data (e.g. zero-filled buffers) parses into tiny blobs
250
+ // that are not valid X.509 certificates.
251
+ try {
252
+ exploreCertificateInfo(cert);
253
+ } catch (err) {
254
+ warningLog(
255
+ "[NoConfiguration] Skipping invalid certificate in chain for auto-trust:",
256
+ (err as Error).message
257
+ );
258
+ continue;
259
+ }
260
+
261
+ if (i === 0) {
262
+ // Leaf certificate → trust explicitly
263
+ try {
264
+ await cm.trustCertificate(cert);
265
+ } catch (err) {
266
+ // ENOENT can happen if another concurrent call already
267
+ // moved the cert from rejected to trusted.
268
+ if ((err as Error & { code?: string }).code !== "ENOENT") {
269
+ warningLog(
270
+ "[NoConfiguration] Failed to auto-trust leaf certificate:",
271
+ (err as Error).message
272
+ );
273
+ }
274
+ }
275
+ } else {
276
+ // Issuer CA certificate → add to issuers/ (chain-building
277
+ // only, does NOT establish a trust anchor)
278
+ try {
279
+ await cm.addIssuer(cert);
280
+ } catch (err) {
281
+ warningLog(
282
+ "[NoConfiguration] Failed to add issuer certificate:",
283
+ (err as Error).message
284
+ );
285
+ }
286
+ }
287
+ }
288
+ }
289
+
290
+ /**
291
+ * Install the `onAdjustCertificateStatus` hook on every endpoint.
292
+ *
293
+ * When the server is in `ServerState.NoConfiguration`, the hook
294
+ * relaxes trust/CRL errors so that a GDS client with a valid
295
+ * full-chain certificate can connect and provision the server.
296
+ *
297
+ * The leaf certificate is auto-trusted so that after the server
298
+ * transitions to Running, the same client is accepted by normal
299
+ * validation. Issuer CAs are placed in `issuers/` for chain
300
+ * building but do not become trust anchors.
301
+ */
302
+ function installCertificateRelaxationHook(server: OPCUAServer): void {
303
+ const adjustCertificateStatus = async (
304
+ statusCode: StatusCode,
305
+ certificate: Certificate
306
+ ): Promise<StatusCode> => {
307
+ // Only relax in NoConfiguration state
308
+ if (server.engine.getServerState() !== ServerState.NoConfiguration) {
309
+ return statusCode;
310
+ }
311
+
312
+ // Only relax trust-infrastructure errors, NOT security errors
313
+ if (!isRelaxableCertificateError(statusCode)) {
314
+ return statusCode;
315
+ }
316
+
317
+ doDebug && warningLog(
318
+ `[NoConfiguration] Relaxing certificate check:`,
319
+ `${statusCode.toString()} → Good`,
320
+ "(server is awaiting GDS provisioning)"
321
+ );
322
+
323
+ // Auto-trust the leaf certificate; issuer CAs go to issuers/
324
+ await autoTrustCertificateChain(server, certificate);
325
+
326
+ return StatusCodes.Good;
327
+ };
328
+
329
+ for (const endpoint of server.endpoints) {
330
+ (endpoint as OPCUAServerEndPoint).onAdjustCertificateStatus = adjustCertificateStatus;
331
+ }
332
+ }