@interopio/gateway-server 0.20.0 → 0.22.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.
@@ -0,0 +1,726 @@
1
+ #!/usr/bin/env node
2
+
3
+ 'use strict';
4
+
5
+ const { parseArgs } = require('node:util');
6
+ const { configure } = require('@interopio/gateway/logging/core');
7
+
8
+ // import { GatewayServer } from './src/index.ts';
9
+ // import { mkcert, argon2, manage } from './src/tools/index.ts';
10
+
11
+ const { mkcert, argon2, manage } = require('@interopio/gateway-server/tools');
12
+ const { GatewayServer } = require('@interopio/gateway-server');
13
+
14
+ function showHelp() {
15
+ console.log(`
16
+ Usage: npx @interopio/gateway-server <command> [options]
17
+
18
+ Commands:
19
+ run Start the gateway server
20
+ manage Send management commands to a running gateway server
21
+ passwd Generate password hash (default using Argon2)
22
+ mkcert Generate client and/or server certificate signed by Dev CA
23
+
24
+ run options:
25
+ -p, --port <port> Specify port or port range to bind the server to (default: 0 i.e. random)
26
+ examples: 8385, 8000-8100, 3000,4000-4050
27
+ -H, --host <host> Network address to bind to (default: unspecified, listens on all)
28
+ examples: localhost, 127.0.0.1, 0.0.0.0, ::1
29
+ -u, --user <name> Enables basic authentication and sets the admin username
30
+ --no-auth Disable authentication (overrides config, sets auth type to 'none')
31
+ -S, --ssl, --tls Enable HTTPS. Auto-generates Dev CA and/or server certificates if not
32
+ present. Enables x509 client cert auth if --user not specified.
33
+ --cert <file> File with SSL/TLS certificate (default: ./gateway-server.crt)
34
+ --key <file> File with SSL/TLS private key (default: ./gateway-server.key)
35
+ --ca <file> File with CA certificates (default: ./gateway-ca.crt)
36
+ --ca-key <file> File with CA private key (default: ./gateway-ca.key)
37
+ --no-ssl, --no-tls Disable SSL/TLS (overrides config)
38
+ -g, --gateway Enable gateway endpoint (default route: /)
39
+ --no-gateway Disable gateway endpoint (overrides config)
40
+ -s, --static <location> Serve static files from specified location (e.g., ./public)
41
+ -c, --config <file> Server configuration file (JSON format)
42
+ --debug Enable debug logging (default: info level)
43
+
44
+ manage options:
45
+ --path <path> Path to the gateway control socket (named pipe on Windows, Unix socket)
46
+ examples: \\\\.\\pipe\\glue42-gateway-xxx, /tmp/gateway.sock
47
+ -p, --port <port> TCP port of the management server
48
+ --timeout <ms> Connection timeout in milliseconds (default: 5000)
49
+ <command> Management command to execute: info, shutdown
50
+ [key=value...] Additional command parameters as key=value pairs
51
+ Values are parsed as JSON (numbers, booleans, arrays, objects)
52
+ or kept as strings if not valid JSON
53
+ <json> Alternatively, pass entire command as a JSON object
54
+ example: '{"command":"update-auth","type":"basic"}'
55
+
56
+ passwd options:
57
+ (no args) Prompt for password interactively (masked input)
58
+ --stdin Read password from stdin (for piping, e.g., echo "pass" | ...)
59
+
60
+ mkcert options:
61
+ --client Generate client certificate (default: server certificate)
62
+ -u, --user <name> Common Name for the certificate (default: dev-user for client certs)
63
+ (--client only)
64
+ --ca <file> File with CA certificate (default: ./gateway-ca.crt)
65
+ --ca-key <file> File with CA private key (default: ./gateway-ca.key)
66
+ --key <file> Output file for private key (default: ./gateway-server.key for server,
67
+ ./gateway-client.key for client)
68
+ --cert <file> Output file for certificate (default: ./gateway-server.crt for server,
69
+ ./gateway-client.crt for client)
70
+ [name...] DNS names, IP addresses, or email addresses for subjectAltName
71
+ (DNS by default, prefix with 'IP:' or 'EMAIL:' for other types)
72
+
73
+ Global Options:
74
+ -v, --version Show version information and exit
75
+ --help Show this help message and exit
76
+
77
+ Examples:
78
+ gateway-server run -p 3000
79
+ gateway-server run -u admin --port 8385,8388 --debug
80
+ gateway-server run -p 8443 --ssl --user admin --gateway
81
+ gateway-server run --port 42443 --tls --ca-key ./ssl/ca.key --host example.com --gateway
82
+ gateway-server run -p 3000 --static ./public --config ./server-config.json
83
+ gateway-server run --config ./server-config.json --no-gateway --no-ssl --no-auth
84
+ gateway-server manage --path \\\\.\\pipe\\glue42-gateway-xxx info
85
+ gateway-server manage --path \\\\.\\pipe\\glue42-gateway-xxx shutdown
86
+ gateway-server manage --path \\\\.\\pipe\\glue42-gateway-xxx custom-cmd key1=value1 count=42 'data={"user":"test"}' --timeout 10000
87
+ gateway-server manage --path \\\\.\\pipe\\glue42-gateway-xxx '{"command":"update-auth","auth":{"token":"my-token"}}}'
88
+ gateway-server passwd
89
+ echo "mySecret123" | gateway-server passwd --stdin
90
+ gateway-server mkcert
91
+ gateway-server mkcert localhost 127.0.0.1 IP:192.168.1.100
92
+ gateway-server mkcert --client EMAIL:john.doe@example.com
93
+ gateway-server mkcert example.com *.example.com --key ./gateway-server.key --cert ./gateway-server.crt
94
+ `);
95
+ process.exit(0);
96
+ }
97
+
98
+ function showVersion() {
99
+ console.log(GatewayServer.VERSION);
100
+ process.exit(0);
101
+ }
102
+
103
+ // Parse command-line arguments
104
+ const { values: globalValues, positionals } = parseArgs({
105
+ args: process.argv.slice(2),
106
+ options: {
107
+ version: { type: 'boolean', short: 'v' },
108
+ help: { type: 'boolean', short: 'h' },
109
+ },
110
+ strict: false,
111
+ allowPositionals: true,
112
+ });
113
+
114
+ // Check for global flags first
115
+ if (globalValues.version) {
116
+ showVersion();
117
+ }
118
+ if (globalValues.help) {
119
+ showHelp();
120
+ }
121
+
122
+ // Determine command (required)
123
+ if (positionals.length === 0) {
124
+ console.error('Error: Command is required');
125
+ console.log('Use --help to see available commands');
126
+ process.exit(1);
127
+ }
128
+
129
+ const command = positionals[0];
130
+
131
+ // Route to appropriate command handler
132
+ switch (command) {
133
+ case 'run':
134
+ runServer().catch(e => {console.error(e); process.exit(1);});
135
+ break;
136
+ case 'manage':
137
+ manageCommand().catch(e => {console.error(e); process.exit(1);});
138
+ break;
139
+ case 'passwd':
140
+ passwdCommand().catch(e => {console.error(e); process.exit(1);});
141
+ break;
142
+ case 'mkcert':
143
+ mkcertCommand().catch(e => {console.error(e); process.exit(1);});
144
+ break;
145
+ default:
146
+ console.error(`Error: Unknown command "${command}"`);
147
+ console.log('Use --help to see available commands');
148
+ process.exit(1);
149
+ }
150
+
151
+ // Command: manage - Send management commands to a running gateway server
152
+ async function manageCommand() {
153
+ const { values, positionals } = parseArgs({
154
+ args: process.argv.slice(3), // Skip 'node', 'gateway-server', and 'manage'
155
+ options: {
156
+ path: { type: 'string' },
157
+ port: { type: 'string', short: 'p' },
158
+ timeout: { type: 'string' },
159
+ },
160
+ strict: true,
161
+ allowPositionals: true,
162
+ });
163
+
164
+ const socketPath = values.path;
165
+ const port = values.port ? parseInt(values.port, 10) : undefined;
166
+ const timeout = values.timeout ? parseInt(values.timeout, 10) : undefined;
167
+ const firstArg = positionals[0];
168
+
169
+ if (!socketPath && !port) {
170
+ console.error('Error: --path or --port is required');
171
+ console.error('Example: gateway-server manage --path \\\\.\\pipe\\glue42-gateway-xxx info');
172
+ process.exit(1);
173
+ }
174
+
175
+ if (!firstArg) {
176
+ console.error('Error: management command is required');
177
+ console.error('Available commands: info, shutdown');
178
+ console.error('Or pass a JSON object: gateway-server manage --path ... \'{"command":"info"}\'');
179
+ process.exit(1);
180
+ }
181
+
182
+ // Try to parse first argument as JSON (entire command object)
183
+ let commandParams;
184
+ if (firstArg.startsWith('{')) {
185
+ try {
186
+ commandParams = JSON.parse(firstArg);
187
+ if (!commandParams.command) {
188
+ console.error('Error: JSON command must have a "command" property');
189
+ process.exit(1);
190
+ }
191
+ }
192
+ catch (e) {
193
+ console.error(`Error: invalid JSON: ${e.message}`);
194
+ process.exit(1);
195
+ }
196
+ }
197
+ else {
198
+ // Parse as command name + key=value pairs
199
+ commandParams = { command: firstArg };
200
+ for (let i = 1; i < positionals.length; i++) {
201
+ const arg = positionals[i];
202
+ const eqIndex = arg.indexOf('=');
203
+ if (eqIndex === -1) {
204
+ console.error(`Error: invalid parameter "${arg}". Expected format: key=value`);
205
+ process.exit(1);
206
+ }
207
+ const key = arg.slice(0, eqIndex);
208
+ let value = arg.slice(eqIndex + 1);
209
+
210
+ // Try to parse as JSON for numbers, booleans, arrays, objects
211
+ try {
212
+ value = JSON.parse(value);
213
+ } catch {
214
+ // Keep as string if not valid JSON
215
+ }
216
+
217
+ commandParams[key] = value;
218
+ }
219
+ }
220
+
221
+ const server = socketPath ? { path: socketPath } : { port };
222
+ try {
223
+ const result = await manage.sendCommand(server, commandParams, { timeout });
224
+ if (result === undefined) {
225
+ console.log('<no response>');
226
+ }
227
+ else if (typeof result === 'string') {
228
+ console.log(result);
229
+ }
230
+ else {
231
+ console.log(JSON.stringify(result, null, 2));
232
+ }
233
+ process.exit(0);
234
+ }
235
+ catch (e) {
236
+ console.error(`Error: ${e.message || e}`);
237
+ process.exit(1);
238
+ }
239
+ }
240
+
241
+ // Command: passwd - Generate password hash
242
+ async function passwdCommand() {
243
+ const { values } = parseArgs({
244
+ args: process.argv.slice(3), // Skip 'node', 'gateway-server', and 'passwd'
245
+ options: {
246
+ stdin: { type: 'boolean' },
247
+ },
248
+ strict: true,
249
+ allowPositionals: false,
250
+ });
251
+
252
+ let password/*: string = undefined!*/;
253
+
254
+ // Read password from stdin if specified
255
+ if (values.stdin) {
256
+ password = await readPasswordFromStdin();
257
+ }
258
+
259
+ // If no password provided, prompt interactively
260
+ if (!password) {
261
+ password = await promptPassword('Enter password: ');
262
+ }
263
+
264
+ if (!password) {
265
+ console.error('Error: password is required');
266
+ process.exit(1);
267
+ }
268
+
269
+ const hashValue = await argon2.hash(password);
270
+ const hash = `{argon2id}${hashValue}`;
271
+ console.log(hash);
272
+ process.exit(0);
273
+ }
274
+
275
+ // Read password from piped stdin (non-interactive)
276
+ async function readPasswordFromStdin() {
277
+ // Check if stdin is a TTY (interactive terminal)
278
+ if (process.stdin.isTTY) {
279
+ // Interactive mode - use readline with hidden input
280
+ return promptPassword('Password: ');
281
+ }
282
+
283
+ // Piped mode - read until EOF or newline
284
+ const chunks/*: Uint8Array[]*/ = [];
285
+ for await (const chunk of process.stdin) {
286
+ chunks.push(chunk);
287
+ }
288
+ const content = Buffer.concat(chunks).toString('utf8');
289
+ // Strip trailing newline
290
+ return content.replace(/\r?\n$/, '');
291
+ }
292
+
293
+ // Prompt for password with masked input
294
+ async function promptPassword(prompt/*: string*/) {
295
+ return new Promise/*<string>*/((resolve) => {
296
+ process.stdout.write(prompt);
297
+
298
+ // Only set raw mode if stdin is a TTY
299
+ if (!process.stdin.isTTY) {
300
+ console.error('Error: Interactive password prompt requires a terminal');
301
+ process.exit(1);
302
+ }
303
+
304
+ process.stdin.setRawMode(true);
305
+ process.stdin.resume();
306
+ process.stdin.setEncoding('utf8');
307
+
308
+ let password = '';
309
+
310
+ const cleanup = () => {
311
+ process.stdin.setRawMode(false);
312
+ process.stdin.pause();
313
+ process.stdin.removeListener('data', onData);
314
+ process.stdout.write('\n');
315
+ };
316
+
317
+ const onData = (char/*: string*/) => {
318
+ // Ctrl+C
319
+ if (char === '\u0003') {
320
+ cleanup();
321
+ process.exit(1);
322
+ }
323
+
324
+ // Enter
325
+ if (char === '\r' || char === '\n') {
326
+ cleanup();
327
+ resolve(password);
328
+ return;
329
+ }
330
+
331
+ // Backspace
332
+ if (char === '\u007f' || char === '\b') {
333
+ if (password.length > 0) {
334
+ password = password.slice(0, -1);
335
+ process.stdout.write('\b \b');
336
+ }
337
+ return;
338
+ }
339
+
340
+ // Regular character
341
+ password += char;
342
+ process.stdout.write('*');
343
+ };
344
+
345
+ process.stdin.on('data', onData);
346
+ });
347
+ }
348
+
349
+ // Command: mkcert - Generate client or server certificate
350
+ async function mkcertCommand() {
351
+ const { values, positionals } = parseArgs({
352
+ args: process.argv.slice(3), // Skip 'node', 'gateway-server', and 'mkcert'
353
+ options: {
354
+ client: { type: 'boolean' },
355
+ user: { type: 'string', short: 'u' },
356
+ ca: { type: 'string' },
357
+ 'ca-key': { type: 'string' },
358
+ key: { type: 'string' },
359
+ cert: { type: 'string' },
360
+ },
361
+ strict: true,
362
+ allowPositionals: true,
363
+ });
364
+
365
+ const isClientCert = values.client || false;
366
+ const user = values.user || 'dev-user';
367
+ const caCert = values.ca || './gateway-ca.crt';
368
+ const caKey = values['ca-key'] || './gateway-ca.key';
369
+ let keyPath = values.key;
370
+ let certPath = values.cert;
371
+ const sanEntries = positionals;
372
+
373
+ // Default output paths
374
+ // Users typically specify just --cert, and key should go in the same file
375
+ if (!keyPath && !certPath) {
376
+ // Neither specified: use defaults based on certificate type
377
+ if (isClientCert) {
378
+ // Client certificate: combined file by default
379
+ keyPath = './gateway-client.key';
380
+ certPath = './gateway-client.crt';
381
+ }
382
+ else {
383
+ // Server certificate: separate files by default
384
+ keyPath = './gateway-server.key';
385
+ certPath = './gateway-server.crt';
386
+ }
387
+ }
388
+ else if (keyPath && !certPath) {
389
+ // Only --key specified: cert goes in the same file (combined)
390
+ certPath = keyPath;
391
+ }
392
+ else if (!keyPath && certPath) {
393
+ // Only --cert specified: key goes in the same file (combined)
394
+ keyPath = certPath;
395
+ }
396
+ // else: both specified, use as-is
397
+
398
+ if (sanEntries.length === 0) {
399
+ if (isClientCert) {
400
+ sanEntries.push(user);
401
+ }
402
+ else {
403
+ // Default SAN entry for server certs
404
+ sanEntries.push('localhost');
405
+ }
406
+ }
407
+
408
+ const { readFile, writeFile } = await import('node:fs/promises');
409
+ const { KEYUTIL, X509 } = await import('jsrsasign');
410
+
411
+ // Load CA key
412
+ let caKeyObj/*: ReturnType<typeof KEYUTIL.getKey>*/;
413
+ try {
414
+ const caKeyPem = await readFile(caKey, 'utf8');
415
+ caKeyObj = KEYUTIL.getKey(caKeyPem);
416
+ } catch (error) {
417
+ console.error(`Error: Cannot read CA key from ${caKey}`);
418
+ console.error(error?.['message']);
419
+ process.exit(1);
420
+ }
421
+
422
+ // Extract issuer from CA certificate
423
+ let issuer/*: string*/;
424
+ try {
425
+ const caCertPem = await readFile(caCert, 'utf8');
426
+ const caCertObj = new X509();
427
+ caCertObj.readCertPEM(caCertPem);
428
+ issuer = caCertObj.getSubjectString();
429
+ } catch (error) {
430
+ console.error(`Error: Cannot read CA certificate from ${caCert}`);
431
+ console.error(error?.['message']);
432
+ process.exit(1);
433
+ }
434
+
435
+ // Generate certificate using SAN entries
436
+ const cert = mkcert.generateCert(caKeyObj, issuer, sanEntries, isClientCert);
437
+
438
+ // Write files
439
+ try {
440
+ if (keyPath === certPath) {
441
+ // Concatenate cert and key into single file (cert first, then key)
442
+ const combined = cert.cert + cert.key;
443
+ await writeFile(keyPath, combined, { mode: 0o600 });
444
+ console.log(`${isClientCert ? 'Client' : 'Server'} certificate generated successfully:`);
445
+ console.log(` Combined (cert+key): ${keyPath}`);
446
+ if (sanEntries.length > 0) {
447
+ console.log(` SAN: ${sanEntries.join(', ')}`);
448
+ }
449
+ }
450
+ else {
451
+ // Write separate files
452
+ await writeFile(keyPath, cert.key, { mode: 0o600 });
453
+ await writeFile(certPath, cert.cert, { mode: 0o644 });
454
+ console.log(`${isClientCert ? 'Client' : 'Server'} certificate generated successfully:`);
455
+ console.log(` Private key: ${keyPath}`);
456
+ console.log(` Certificate: ${certPath}`);
457
+ if (sanEntries.length > 0) {
458
+ console.log(` SAN: ${sanEntries.join(', ')}`);
459
+ }
460
+ }
461
+
462
+ process.exit(0);
463
+ }
464
+ catch (error) {
465
+ console.error(`Error: Cannot write certificate files`);
466
+ console.error(error?.['message']);
467
+ process.exit(1);
468
+ }
469
+ }
470
+
471
+ // Command: run - Start the server
472
+ async function runServer() {
473
+ const { values } = parseArgs({
474
+ args: process.argv.slice(3), // Skip 'node', 'gateway-server', and 'run'
475
+ options: {
476
+ port: { type: 'string', short: 'p' },
477
+ host: { type: 'string', short: 'H' },
478
+ user: { type: 'string', short: 'u' },
479
+ debug: { type: 'boolean' },
480
+ ssl: { type: 'boolean', short: 'S' },
481
+ tls: { type: 'boolean' },
482
+ cert: { type: 'string' },
483
+ key: { type: 'string' },
484
+ ca: { type: 'string' },
485
+ 'ca-key': { type: 'string' },
486
+ config: { type: 'string', short: 'c' },
487
+ gateway: { type: 'boolean', short: 'g' },
488
+ static: { type: 'string', short: 's', multiple: true },
489
+ auth: { type: 'boolean' }
490
+ },
491
+ strict: true,
492
+ allowNegative: true,
493
+ allowPositionals: false,
494
+ });
495
+
496
+ const options = {
497
+ port: values.port,
498
+ host: values.host,
499
+ user: values.user,
500
+ debug: values.debug || false,
501
+ ssl: values.ssl || values.tls || false,
502
+ noSsl: values.ssl === false, // --no-ssl explicitly set
503
+ cert: values.cert,
504
+ key: values.key,
505
+ ca: values.ca,
506
+ caKey: values['ca-key'],
507
+ config: values.config,
508
+ gateway: values.gateway,
509
+ noGateway: values.gateway === false, // --no-gateway explicitly set
510
+ static: values.static,
511
+ };
512
+
513
+ configure({
514
+ level: options.debug ? {
515
+ gateway: 'debug',
516
+ "gateway.node": 'info',
517
+ } : 'info',
518
+ });
519
+
520
+ // Load server configuration from file if specified
521
+ let serverConfig/*: GatewayServer.ServerConfig*/ = {};
522
+
523
+ const { access, constants } = await import('node:fs/promises');
524
+ const { resolve, extname } = await import('node:path');
525
+
526
+ let configPath/*: string*/;
527
+ for (const fileName of
528
+ [
529
+ 'gateway-server.config.js',
530
+ 'gateway-server.config.mjs',
531
+ 'gateway-server.config.cjs',
532
+ 'gateway-server.config.ts',
533
+ 'gateway-server.config.mts',
534
+ 'gateway-server.config.cts',
535
+ 'gateway-server.config.json'
536
+ ]) {
537
+ configPath = resolve(process.cwd(), fileName);
538
+ try {
539
+ await access(configPath, constants.R_OK);
540
+ // found readable config file, load it
541
+ break;
542
+ }
543
+ catch {
544
+ configPath = undefined;
545
+ // continue to next candidate
546
+ }
547
+ }
548
+
549
+ if (options.config) {
550
+ configPath = options.config;
551
+ }
552
+ if (configPath) {
553
+ try {
554
+ if (['.json'].includes(extname(configPath))) {
555
+ // JSON config file - read and parse manually
556
+ const { readFile } = await import('node:fs/promises');
557
+ const configContent = await readFile(configPath, 'utf8');
558
+ serverConfig = JSON.parse(configContent);
559
+ }
560
+ else {
561
+ // JS/TS config file - import dynamically
562
+ const { pathToFileURL } = await import('node:url');
563
+ const importedConfig = await import(pathToFileURL(configPath));
564
+ serverConfig = importedConfig.default || importedConfig;
565
+ }
566
+ }
567
+ catch (error) {
568
+ console.error(`Error: Cannot read server config from ${configPath}`);
569
+ console.error(error?.['message']);
570
+ process.exit(1);
571
+ }
572
+ }
573
+
574
+ // Apply CLI overrides to server config
575
+ // CLI arguments take precedence over config file
576
+
577
+ // Override port if specified
578
+ if (values.port !== undefined) {
579
+ serverConfig.port = options.port;
580
+ }
581
+
582
+ // Override host if specified
583
+ if (values.host !== undefined) {
584
+ serverConfig.host = options.host;
585
+ }
586
+
587
+ // Override SSL configuration
588
+ if (options.noSsl) {
589
+ // --no-ssl: delete SSL config completely
590
+ delete serverConfig.ssl;
591
+ options.ssl = false;
592
+ } else if (options.ssl) {
593
+ // --ssl specified: merge CLI args with existing config
594
+ if (!serverConfig.ssl) {
595
+ serverConfig.ssl = {};
596
+ }
597
+ // Only set properties if CLI options are explicitly provided
598
+ if (options.key !== undefined) {
599
+ serverConfig.ssl.key = options.key;
600
+ }
601
+ if (options.cert !== undefined) {
602
+ serverConfig.ssl.cert = options.cert;
603
+ }
604
+ if (options.ca !== undefined) {
605
+ serverConfig.ssl.ca = options.ca;
606
+ }
607
+ }
608
+ // else: use whatever is in serverConfig (if any)
609
+
610
+ // Determine effective SSL state
611
+ const effectiveSsl = options.ssl || (!options.noSsl && serverConfig?.ssl !== undefined);
612
+
613
+ // Configure authentication
614
+ let auth/*: GatewayServer.AuthConfig*/;
615
+ if (values.auth === false) {
616
+ // --no-auth: delete auth config from server config and set to 'none'
617
+ delete serverConfig.auth;
618
+ auth = { type: 'none' };
619
+ }
620
+ else if (serverConfig.auth) {
621
+ // Use auth from config file if present
622
+ auth = serverConfig.auth;
623
+
624
+ // Allow --user CLI option to override auth.user.name from config
625
+ if (options.user) {
626
+ if (!auth.user) {
627
+ auth.user = {name: options.user};
628
+ }
629
+ auth.user.name = options.user;
630
+ }
631
+ }
632
+ else {
633
+ // No auth in config file, determine from CLI args or defaults
634
+ auth = {
635
+ type: options.user ? 'basic' : effectiveSsl ? 'x509' : 'none'
636
+ };
637
+
638
+ if (options.user) {
639
+ auth.user = { name: options.user };
640
+ }
641
+
642
+ if (effectiveSsl && auth.type === 'x509') {
643
+ auth.x509 = {
644
+ key: options.caKey || './gateway-ca.key',
645
+ // principalAltName: 'email',
646
+ };
647
+
648
+ // Ensure SSL config exists and has proper mutual TLS settings
649
+ if (!serverConfig.ssl) {
650
+ serverConfig.ssl = {};
651
+ }
652
+
653
+ // Enable client certificate verification with proper mutual TLS
654
+ serverConfig.ssl.requestCert = true;
655
+
656
+ // Enforce trust verification to prevent certificate spoofing
657
+ // rejectUnauthorized must be true to ensure only trusted client certs are accepted
658
+ serverConfig.ssl.rejectUnauthorized = true;
659
+
660
+ // Set CA for client certificate verification (required for mutual TLS)
661
+ // Use the same CA that signs client certificates
662
+ if (!serverConfig.ssl.ca) {
663
+ serverConfig.ssl.ca = options.ca || './gateway-ca.crt';
664
+ }
665
+ }
666
+ }
667
+
668
+ // Handle SSL certificate auto-generation when --ssl is specified via CLI
669
+ // but config file doesn't have server certificates
670
+ if (options.ssl && effectiveSsl && serverConfig.ssl) {
671
+ const hasCert = serverConfig.ssl.cert || serverConfig.ssl.key;
672
+ if (!hasCert) {
673
+ // SSL enabled but no server cert/key provided
674
+ // We need a CA key for auto-generating server certificates
675
+ // Set default CA key path if not already configured
676
+ if (!auth.x509) {
677
+ auth.x509 = {};
678
+ }
679
+ if (!auth.x509.key) {
680
+ // Use CLI option, or default to ./gateway-ca.key
681
+ auth.x509.key = options.caKey || './gateway-ca.key';
682
+ }
683
+ }
684
+ }
685
+
686
+ // Override gateway configuration
687
+ if (options.noGateway) {
688
+ // --no-gateway: delete gateway config completely
689
+ delete serverConfig.gateway;
690
+ } else if (options.gateway) {
691
+ // --gateway specified: enable gateway endpoint, respect existing config
692
+ if (!serverConfig.gateway) {
693
+ serverConfig.gateway = {};
694
+ }
695
+ // Set defaults only if not already present in config
696
+ if (!serverConfig.gateway.route) {
697
+ serverConfig.gateway.route = '/';
698
+ }
699
+ if (!serverConfig.gateway.authorize) {
700
+ serverConfig.gateway.authorize = { access: auth.type !== 'none' ? 'authenticated' : 'permitted' };
701
+ }
702
+ }
703
+ // else: use whatever is in serverConfig (if any)
704
+
705
+ // Override static resources if specified
706
+ if (options.static && options.static.length > 0) {
707
+ if (!serverConfig.resources) {
708
+ serverConfig.resources = {locations: options.static};
709
+ }
710
+ serverConfig.resources.locations = options.static;
711
+ }
712
+
713
+ // Set auth in server config
714
+ serverConfig.auth = auth;
715
+
716
+ const server = await GatewayServer.Factory({
717
+ ...(serverConfig)
718
+ });
719
+
720
+ process.on('SIGINT', async () => {
721
+ await server.close();
722
+ process.exit(0);
723
+ });
724
+
725
+ // process.title = `${GatewayServer.VERSION} ${server.address.port}`;
726
+ }