@interopio/gateway-server 0.20.0 → 0.21.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.
package/gateway-server CHANGED
@@ -3,6 +3,7 @@
3
3
  `use strict`;
4
4
 
5
5
  import { writeFileSync } from 'node:fs';
6
+ import { parseArgs } from 'node:util';
6
7
  import { configure } from '@interopio/gateway/logging/core';
7
8
 
8
9
  // import { GatewayServer } from './src/index.ts';
@@ -13,48 +14,61 @@ import { mkcert, argon2 } from './dist/tools/index.js';
13
14
 
14
15
  function showHelp() {
15
16
  console.log(`
16
- Usage: gateway-server <command> [options]
17
+ Usage: npx @interopio/gateway-server <command> [options]
17
18
 
18
19
  Commands:
19
- run Run the gateway server (default if no command specified)
20
- passwd Generate password hash (default using Argon2)
21
- mkcert Generate client and/or server certificate signed by Dev CA
20
+ run Start the gateway server
21
+ passwd Generate password hash (default using Argon2)
22
+ mkcert Generate client and/or server certificate signed by Dev CA
22
23
 
23
24
  run options:
24
- -p, --port <port> Specify port or port range to bind the server to (default: 0 i.e. random available port)
25
- examples: 8385, 8000-8100, 3000,4000-4050
26
- -H, --host <host> Network address to bind to (default: unspecified, listens on all interfaces)
27
- examples: localhost, 127.0.0.1, 0.0.0.0, ::1
28
- -u, --user <name> Enables basic authentication and sets the admin username
29
- -S, --ssl, --tls Enable HTTPS. Auto-generates Dev CA and/or server certificates if not present. Enables x509 client cert auth if --user not specified.
30
- --cert <file> File with SSL/TLS certificate (default: ./gateway-server.crt)
31
- --key <file> File with SSL/TLS private key (default: ./gateway-server.key)
32
- --ca <file> File with CA certificates (default: ./gateway-ca.crt)
33
- --ca-key <file> File with CA private key (default: ./gateway-ca.key)
34
- --debug Enable debug logging (default: info level)
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)
35
43
 
36
44
  passwd options:
37
- (no args) Prompt for password interactively (masked input)
38
- --stdin Read password from stdin (for piping, e.g., echo "pass" | ...)
45
+ (no args) Prompt for password interactively (masked input)
46
+ --stdin Read password from stdin (for piping, e.g., echo "pass" | ...)
39
47
 
40
48
  mkcert options:
41
- --client Generate client certificate (default: server certificate)
42
- -u, --user <name> Common Name for the certificate (default: dev-user for client certs) (--client only)
43
- --ca <file> File with CA certificate (default: ./gateway-ca.crt)
44
- --ca-key <file> File with CA private key (default: ./gateway-ca.key)
45
- --key <file> Output file for private key (default: ./gateway-server.key for server, ./gateway-client.key for client)
46
- --cert <file> Output file for certificate (default: ./gateway-server.crt for server, ./gateway-client.crt for client)
47
- [name...] DNS names, IP addresses, or email addresses for subjectAltName
48
- (DNS by default, prefix with 'IP:' or 'EMAIL:' for other types)
49
+ --client Generate client certificate (default: server certificate)
50
+ -u, --user <name> Common Name for the certificate (default: dev-user for client certs)
51
+ (--client only)
52
+ --ca <file> File with CA certificate (default: ./gateway-ca.crt)
53
+ --ca-key <file> File with CA private key (default: ./gateway-ca.key)
54
+ --key <file> Output file for private key (default: ./gateway-server.key for server,
55
+ ./gateway-client.key for client)
56
+ --cert <file> Output file for certificate (default: ./gateway-server.crt for server,
57
+ ./gateway-client.crt for client)
58
+ [name...] DNS names, IP addresses, or email addresses for subjectAltName
59
+ (DNS by default, prefix with 'IP:' or 'EMAIL:' for other types)
49
60
 
50
61
  Global Options:
51
- -v, --version Show version information and exit
52
- --help Show this help message and exit
62
+ -v, --version Show version information and exit
63
+ --help Show this help message and exit
53
64
 
54
65
  Examples:
55
66
  gateway-server run -p 3000
56
67
  gateway-server run -u admin --port 8385,8388 --debug
57
- gateway-server run -p 8443 --ssl --user admin
68
+ gateway-server run -p 8443 --ssl --user admin --gateway
69
+ gateway-server run --port 42443 --tls --ca-key ./ssl/ca.key --host medes --gateway
70
+ gateway-server run -p 3000 --static ./public --config ./server-config.json
71
+ gateway-server run --config ./server-config.json --no-gateway --no-ssl --no-auth
58
72
  gateway-server passwd
59
73
  echo "mySecret123" | gateway-server passwd --stdin
60
74
  gateway-server mkcert
@@ -70,37 +84,44 @@ function showVersion() {
70
84
  process.exit(0);
71
85
  }
72
86
 
73
-
74
87
  // Parse command-line arguments
75
- const args = process.argv.slice(2);
88
+ const { values: globalValues, positionals } = parseArgs({
89
+ args: process.argv.slice(2),
90
+ options: {
91
+ version: { type: 'boolean', short: 'v' },
92
+ help: { type: 'boolean', short: 'h' },
93
+ },
94
+ strict: false,
95
+ allowPositionals: true,
96
+ });
76
97
 
77
98
  // Check for global flags first
78
- if (args.includes('--version') || args.includes('-v')) {
99
+ if (globalValues.version) {
79
100
  showVersion();
80
101
  }
81
- if (args.includes('--help') || args.includes('-h')) {
102
+ if (globalValues.help) {
82
103
  showHelp();
83
104
  }
84
105
 
85
- // Determine command (default to 'run' for backward compatibility)
86
- let command = 'run';
87
- let commandArgs = args;
88
-
89
- if (args.length > 0 && !args[0].startsWith('-')) {
90
- command = args[0];
91
- commandArgs = args.slice(1);
106
+ // Determine command (required)
107
+ if (positionals.length === 0) {
108
+ console.error('Error: Command is required');
109
+ console.log('Use --help to see available commands');
110
+ process.exit(1);
92
111
  }
93
112
 
113
+ const command = positionals[0];
114
+
94
115
  // Route to appropriate command handler
95
116
  switch (command) {
96
117
  case 'run':
97
- await runServer(commandArgs);
118
+ await runServer();
98
119
  break;
99
120
  case 'passwd':
100
- await passwdCommand(commandArgs);
121
+ await passwdCommand();
101
122
  break;
102
123
  case 'mkcert':
103
- await mkcertCommand(commandArgs);
124
+ await mkcertCommand();
104
125
  break;
105
126
  default:
106
127
  console.error(`Error: Unknown command "${command}"`);
@@ -109,23 +130,20 @@ switch (command) {
109
130
  }
110
131
 
111
132
  // Command: passwd - Generate password hash
112
- async function passwdCommand(args) {
113
- let password = undefined;
114
- let fromStdin = false;
133
+ async function passwdCommand() {
134
+ const { values } = parseArgs({
135
+ args: process.argv.slice(3), // Skip 'node', 'gateway-server', and 'passwd'
136
+ options: {
137
+ stdin: { type: 'boolean' },
138
+ },
139
+ strict: true,
140
+ allowPositionals: false,
141
+ });
115
142
 
116
- for (let i = 0; i < args.length; i++) {
117
- if (args[i] === '--stdin') {
118
- fromStdin = true;
119
- } else {
120
- console.error(`Error: Unknown option "${args[i]}"`);
121
- console.log('Use: gateway-server passwd');
122
- console.log(' or: gateway-server passwd --stdin');
123
- process.exit(1);
124
- }
125
- }
143
+ let password = undefined;
126
144
 
127
145
  // Read password from stdin if specified
128
- if (fromStdin) {
146
+ if (values.stdin) {
129
147
  password = await readPasswordFromStdin();
130
148
  }
131
149
 
@@ -220,42 +238,28 @@ async function promptPassword(prompt) {
220
238
  }
221
239
 
222
240
  // Command: mkcert - Generate client or server certificate
223
- async function mkcertCommand(args) {
224
- let isClientCert = false;
225
- let user = 'dev-user';
226
- let caCert = './gateway-ca.crt';
227
- let caKey = './gateway-ca.key';
228
- let keyPath = undefined;
229
- let certPath = undefined;
230
- const sanEntries = [];
231
-
232
- for (let i = 0; i < args.length; i++) {
233
- if (args[i] === '--client') {
234
- isClientCert = true;
235
- } else if ((args[i] === '--user' || args[i] === '-u') && i + 1 < args.length) {
236
- user = args[i + 1];
237
- i++;
238
- } else if (args[i] === '--ca' && i + 1 < args.length) {
239
- caCert = args[i + 1];
240
- i++;
241
- } else if (args[i] === '--ca-key' && i + 1 < args.length) {
242
- caKey = args[i + 1];
243
- i++;
244
- } else if (args[i] === '--key' && i + 1 < args.length) {
245
- keyPath = args[i + 1];
246
- i++;
247
- } else if (args[i] === '--cert' && i + 1 < args.length) {
248
- certPath = args[i + 1];
249
- i++;
250
- } else if (!args[i].startsWith('-')) {
251
- // Positional argument - add to SAN entries
252
- sanEntries.push(args[i]);
253
- } else {
254
- console.error(`Error: Unknown option "${args[i]}"`);
255
- console.log('Use: gateway-server mkcert [--client] [--user <name>] [--ca <file>] [--ca-key <file>] [--key <file>] [--cert <file>] [name...]');
256
- process.exit(1);
257
- }
258
- }
241
+ async function mkcertCommand() {
242
+ const { values, positionals } = parseArgs({
243
+ args: process.argv.slice(3), // Skip 'node', 'gateway-server', and 'mkcert'
244
+ options: {
245
+ client: { type: 'boolean' },
246
+ user: { type: 'string', short: 'u' },
247
+ ca: { type: 'string' },
248
+ 'ca-key': { type: 'string' },
249
+ key: { type: 'string' },
250
+ cert: { type: 'string' },
251
+ },
252
+ strict: true,
253
+ allowPositionals: true,
254
+ });
255
+
256
+ const isClientCert = values.client || false;
257
+ const user = values.user || 'dev-user';
258
+ const caCert = values.ca || './gateway-ca.crt';
259
+ const caKey = values['ca-key'] || './gateway-ca.key';
260
+ let keyPath = values.key;
261
+ let certPath = values.cert;
262
+ const sanEntries = positionals;
259
263
 
260
264
  // Default output paths
261
265
  // Users typically specify just --cert, and key should go in the same file
@@ -351,83 +355,209 @@ async function mkcertCommand(args) {
351
355
  }
352
356
 
353
357
  // Command: run - Start the server
354
- async function runServer(args) {
358
+ async function runServer() {
359
+ const { values } = parseArgs({
360
+ args: process.argv.slice(3), // Skip 'node', 'gateway-server', and 'run'
361
+ options: {
362
+ port: { type: 'string', short: 'p' },
363
+ host: { type: 'string', short: 'H' },
364
+ user: { type: 'string', short: 'u' },
365
+ debug: { type: 'boolean' },
366
+ ssl: { type: 'boolean', short: 'S' },
367
+ tls: { type: 'boolean' },
368
+ cert: { type: 'string' },
369
+ key: { type: 'string' },
370
+ ca: { type: 'string' },
371
+ 'ca-key': { type: 'string' },
372
+ config: { type: 'string', short: 'c' },
373
+ gateway: { type: 'boolean', short: 'g' },
374
+ static: { type: 'string', short: 's', multiple: true },
375
+ auth: { type: 'boolean' }
376
+ },
377
+ strict: true,
378
+ allowNegative: true,
379
+ allowPositionals: false,
380
+ });
381
+
355
382
  const options = {
356
- port: 0,
357
- host: undefined,
358
- user: undefined,
359
- debug: false,
360
- ssl: false,
361
- cert: undefined,
362
- key: undefined,
363
- ca: undefined,
364
- caKey: undefined
383
+ port: values.port,
384
+ host: values.host,
385
+ user: values.user,
386
+ debug: values.debug || false,
387
+ ssl: values.ssl || values.tls || false,
388
+ noSsl: values.ssl === false, // --no-ssl explicitly set
389
+ cert: values.cert,
390
+ key: values.key,
391
+ ca: values.ca,
392
+ caKey: values['ca-key'],
393
+ config: values.config,
394
+ gateway: values.gateway,
395
+ noGateway: values.gateway === false, // --no-gateway explicitly set
396
+ static: values.static,
365
397
  };
366
398
 
367
- for (let i = 0; i < args.length; i++) {
368
- if ((args[i] === '--port' || args[i] === '-p') && i + 1 < args.length) {
369
- options.port = args[i + 1];
370
- i++;
371
- } else if ((args[i] === '--host' || args[i] === '-H') && i + 1 < args.length) {
372
- options.host = args[i + 1];
373
- i++;
374
- } else if ((args[i] === '--user' || args[i] === '-u') && i + 1 < args.length) {
375
- options.user = args[i + 1];
376
- i++;
377
- } else if (args[i] === '--ssl' || args[i] === '-S' || args[i] === '--tls') {
378
- options.ssl = true;
379
- } else if (args[i] === '--cert' && i + 1 < args.length) {
380
- options.cert = args[i + 1];
381
- i++;
382
- } else if (args[i] === '--key' && i + 1 < args.length) {
383
- options.key = args[i + 1];
384
- i++;
385
- } else if (args[i] === '--ca' && i + 1 < args.length) {
386
- options.ca = args[i + 1];
387
- i++;
388
- } else if (args[i] === '--ca-key' && i + 1 < args.length) {
389
- options.caKey = args[i + 1];
390
- i++;
391
- } else if (args[i] === '--debug') {
392
- options.debug = true;
393
- } else {
394
- console.error(`Error: Unknown option "${args[i]}"`);
395
- console.log('Use --help to see available options');
399
+ configure({
400
+ level: options.debug ? 'debug' : 'info',
401
+ });
402
+
403
+ // Load server configuration from file if specified
404
+ let serverConfig = {};
405
+ if (options.config) {
406
+ try {
407
+ const { readFileSync } = await import('node:fs');
408
+ const configContent = readFileSync(options.config, 'utf8');
409
+ serverConfig = JSON.parse(configContent);
410
+ } catch (error) {
411
+ console.error(`Error: Cannot read server config from ${options.config}`);
412
+ console.error(error.message);
396
413
  process.exit(1);
397
414
  }
398
415
  }
399
416
 
400
- configure({
401
- level: options.debug ? 'debug' : 'info',
402
- });
417
+ // Apply CLI overrides to server config
418
+ // CLI arguments take precedence over config file
403
419
 
404
- const auth = {
405
- type: options.user ? 'basic' : options.ssl ? 'x509' : 'none'
406
- };
420
+ // Override port if specified
421
+ if (values.port !== undefined) {
422
+ serverConfig.port = options.port;
423
+ }
424
+
425
+ // Override host if specified
426
+ if (values.host !== undefined) {
427
+ serverConfig.host = options.host;
428
+ }
407
429
 
408
- if (options.user) {
409
- auth.user = { name: options.user, roles: ['admin'] };
430
+ // Override SSL configuration
431
+ if (options.noSsl) {
432
+ // --no-ssl: delete SSL config completely
433
+ delete serverConfig.ssl;
434
+ options.ssl = false;
435
+ } else if (options.ssl) {
436
+ // --ssl specified: merge CLI args with existing config
437
+ if (!serverConfig.ssl) {
438
+ serverConfig.ssl = {};
439
+ }
440
+ // Only set properties if CLI options are explicitly provided
441
+ if (options.key !== undefined) {
442
+ serverConfig.ssl.key = options.key;
443
+ }
444
+ if (options.cert !== undefined) {
445
+ serverConfig.ssl.cert = options.cert;
446
+ }
447
+ if (options.ca !== undefined) {
448
+ serverConfig.ssl.ca = options.ca;
449
+ }
410
450
  }
451
+ // else: use whatever is in serverConfig (if any)
452
+
453
+ // Determine effective SSL state
454
+ const effectiveSsl = options.ssl || (!options.noSsl && serverConfig.ssl !== undefined);
411
455
 
412
- if (options.ssl) {
413
- auth.x509 = {
414
- key: options.caKey || './gateway-ca.key',
415
- // principalAltName: 'email',
456
+ // Configure authentication
457
+ let auth;
458
+ if (values.auth === false) {
459
+ // --no-auth: delete auth config from server config and set to 'none'
460
+ delete serverConfig.auth;
461
+ auth = { type: 'none' };
462
+ }
463
+ else if (serverConfig.auth) {
464
+ // Use auth from config file if present
465
+ auth = serverConfig.auth;
466
+
467
+ // Allow --user CLI option to override auth.user.name from config
468
+ if (options.user) {
469
+ if (!auth.user) {
470
+ auth.user = {};
471
+ }
472
+ auth.user.name = options.user;
473
+ }
474
+ }
475
+ else {
476
+ // No auth in config file, determine from CLI args or defaults
477
+ auth = {
478
+ type: options.user ? 'basic' : effectiveSsl ? 'x509' : 'none'
416
479
  };
480
+
481
+ if (options.user) {
482
+ auth.user = { name: options.user };
483
+ }
484
+
485
+ if (effectiveSsl && auth.type === 'x509') {
486
+ auth.x509 = {
487
+ key: options.caKey || './gateway-ca.key',
488
+ // principalAltName: 'email',
489
+ };
490
+
491
+ // Ensure SSL config exists and has proper mutual TLS settings
492
+ if (!serverConfig.ssl) {
493
+ serverConfig.ssl = {};
494
+ }
495
+
496
+ // Enable client certificate verification with proper mutual TLS
497
+ serverConfig.ssl.requestCert = true;
498
+
499
+ // Enforce trust verification to prevent certificate spoofing
500
+ // rejectUnauthorized must be true to ensure only trusted client certs are accepted
501
+ serverConfig.ssl.rejectUnauthorized = true;
502
+
503
+ // Set CA for client certificate verification (required for mutual TLS)
504
+ // Use the same CA that signs client certificates
505
+ if (!serverConfig.ssl.ca) {
506
+ serverConfig.ssl.ca = options.ca || './gateway-ca.crt';
507
+ }
508
+ }
509
+ }
510
+
511
+ // Handle SSL certificate auto-generation when --ssl is specified via CLI
512
+ // but config file doesn't have server certificates
513
+ if (options.ssl && effectiveSsl && serverConfig.ssl) {
514
+ const hasCert = serverConfig.ssl.cert || serverConfig.ssl.key;
515
+ if (!hasCert) {
516
+ // SSL enabled but no server cert/key provided
517
+ // We need a CA key for auto-generating server certificates
518
+ // Set default CA key path if not already configured
519
+ if (!auth.x509) {
520
+ auth.x509 = {};
521
+ }
522
+ if (!auth.x509.key) {
523
+ // Use CLI option, or default to ./gateway-ca.key
524
+ auth.x509.key = options.caKey || './gateway-ca.key';
525
+ }
526
+ }
527
+ }
528
+
529
+ // Override gateway configuration
530
+ if (options.noGateway) {
531
+ // --no-gateway: delete gateway config completely
532
+ delete serverConfig.gateway;
533
+ } else if (options.gateway) {
534
+ // --gateway specified: enable gateway endpoint, respect existing config
535
+ if (!serverConfig.gateway) {
536
+ serverConfig.gateway = {};
537
+ }
538
+ // Set defaults only if not already present in config
539
+ if (!serverConfig.gateway.route) {
540
+ serverConfig.gateway.route = '/';
541
+ }
542
+ if (!serverConfig.gateway.authorize) {
543
+ serverConfig.gateway.authorize = { access: auth.type !== 'none' ? 'authenticated' : 'permitted' };
544
+ }
417
545
  }
546
+ // else: use whatever is in serverConfig (if any)
547
+
548
+ // Override static resources if specified
549
+ if (options.static && options.static.length > 0) {
550
+ if (!serverConfig.resources) {
551
+ serverConfig.resources = {};
552
+ }
553
+ serverConfig.resources.locations = options.static;
554
+ }
555
+
556
+ // Set auth in server config
557
+ serverConfig.auth = auth;
418
558
 
419
559
  const server = await GatewayServer.Factory({
420
- port: options.port,
421
- host: options.host,
422
- ssl: options.ssl ? {
423
- key: options.key,
424
- cert: options.cert,
425
- ca: options.ca,
426
- requestCert: auth.type === 'x509',
427
- rejectUnauthorized: false
428
- } : undefined,
429
- auth,
430
- gateway: { route: '/gw', authorize: { access: auth.type !== 'none' ? 'authenticated' : 'permitted' } },
560
+ ...(serverConfig),
431
561
  app: async (_configurer) => {
432
562
  // No custom app logic for now
433
563
  },
@@ -438,5 +568,5 @@ async function runServer(args) {
438
568
  process.exit(0);
439
569
  });
440
570
 
441
- process.title = `${GatewayServer.VERSION} ${server.address.port}`;
571
+ // process.title = `${GatewayServer.VERSION} ${server.address.port}`;
442
572
  }
@@ -109,6 +109,10 @@ export namespace GatewayServer {
109
109
  },
110
110
 
111
111
  app?: ServerCustomizer,
112
+ resources?: {
113
+ enabled?: boolean,
114
+ locations: string[]
115
+ },
112
116
  gateway?: IOGateway.GatewayConfig & ServerWebSocketOptions & {
113
117
  route?: string,
114
118
  /**
@@ -121,6 +125,12 @@ export namespace GatewayServer {
121
125
  * @since 0.20.0
122
126
  */
123
127
  scope?: 'singleton' | 'principal',
128
+ mesh?: IOGateway.MeshConfig & {
129
+ enabled?: boolean
130
+ }
131
+ metrics?: IOGateway.MetricsConfig & {
132
+ enabled?: boolean
133
+ }
124
134
  }
125
135
  }
126
136
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@interopio/gateway-server",
3
- "version": "0.20.0",
3
+ "version": "0.21.0",
4
4
  "keywords": [
5
5
  "gateway",
6
6
  "server",
package/readme.md CHANGED
@@ -25,6 +25,11 @@ npm install @interopio/gateway-server
25
25
 
26
26
  ## Getting Started
27
27
 
28
+ ### CLI:
29
+ ```shell
30
+ npx @interopio/gateway-server run --port 8385 --gateway
31
+ ```
32
+ ### API:
28
33
  ```typescript
29
34
  import GatewayServer, { type Server } from '@interopio/gateway-server';
30
35
 
@@ -1,4 +1,6 @@
1
- import {GatewayServer} from '../../gateway-server';
1
+ import type { Middleware, SslInfo } from './server';
2
+ import { GatewayServer } from '../../gateway-server';
3
+
2
4
 
3
5
  export interface TestClient {
4
6
  readonly fetch: (input: string | URL, init?: RequestInit) => Promise<Response>;
@@ -10,6 +12,8 @@ export interface TestClientBuilder {
10
12
  }
11
13
 
12
14
  export interface MockServerSpec {
15
+ middleware(...middleware: Middleware): this;
16
+ sslInfo(sslInfo?: SslInfo): this;
13
17
  configureClient(): TestClientBuilder;
14
18
  build(): Promise<TestClient>;
15
19
  }