roster-server 2.4.4 → 2.4.6

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/README.md CHANGED
@@ -386,6 +386,36 @@ process.on('message', (msg, connection) => {
386
386
  });
387
387
  ```
388
388
 
389
+ ### Production Pattern: Single Certificate Manager + Workers
390
+
391
+ For robust ACME behavior with cluster runtimes, run a single certificate manager process (primary) and keep workers in serving-only mode. This avoids challenge race conditions while keeping certificate lifecycle automatic.
392
+
393
+ ```javascript
394
+ // primary
395
+ const certManager = new Roster({
396
+ email: 'admin@example.com',
397
+ greenlockStorePath: '/srv/greenlock.d',
398
+ wwwPath: '/srv/www'
399
+ });
400
+ certManager.register('example.com', () => (req, res) => res.end('manager'));
401
+ await certManager.start(); // enables ACME challenge lifecycle
402
+ await certManager.ensureCertificate('example.com');
403
+
404
+ // worker
405
+ const workerRoster = new Roster({
406
+ email: 'admin@example.com',
407
+ greenlockStorePath: '/srv/greenlock.d',
408
+ wwwPath: '/srv/www',
409
+ autoCertificates: false
410
+ });
411
+ workerRoster.register('example.com', () => (req, res) => res.end('worker'));
412
+ await workerRoster.init();
413
+ const server = await workerRoster.createServingHttpsServer({ servername: 'example.com' });
414
+ server.listen(4336);
415
+ ```
416
+
417
+ Reference implementation: `demo/https-cluster-configurable.js`.
418
+
389
419
  ### API Reference
390
420
 
391
421
  #### `roster.init()` → `Promise<Roster>`
@@ -404,6 +434,22 @@ Returns the WebSocket upgrade dispatcher for a given port. Routes upgrades to th
404
434
 
405
435
  Returns a TLS SNI callback. It resolves certificates from `greenlockStorePath` and, when `autoCertificates` is enabled (default), can issue missing certificates automatically. Not available in local mode.
406
436
 
437
+ #### `roster.ensureCertificate(servername)` → `Promise<{ key, cert }>`
438
+
439
+ Ensures a certificate exists for `servername`. With `autoCertificates` enabled (default), it issues missing certificates automatically and returns PEMs.
440
+
441
+ #### `roster.loadCertificate(servername)` → `{ key, cert }`
442
+
443
+ Loads an existing certificate from `greenlockStorePath` without issuing new certificates. Useful for serving-only workers.
444
+
445
+ #### `roster.createManagedHttpsServer({ servername, port?, ensureCertificate?, tlsOptions? })` → `Promise<https.Server>`
446
+
447
+ Creates an HTTPS server prewired with default cert, SNI callback, and request/upgrade routing. By default it ensures certificate issuance before returning.
448
+
449
+ #### `roster.createServingHttpsServer({ servername, port?, tlsOptions? })` → `Promise<https.Server>`
450
+
451
+ Convenience alias for serving-only workers. Equivalent to `createManagedHttpsServer(..., ensureCertificate: false)`.
452
+
407
453
  #### `roster.attach(server, { port }?)` → `Roster`
408
454
 
409
455
  Convenience method. Wires `requestHandler` and `upgradeHandler` onto `server.on('request', ...)` and `server.on('upgrade', ...)`. Returns `this` for chaining.
@@ -1,30 +1,36 @@
1
1
  const cluster = require('cluster');
2
2
  const os = require('os');
3
3
  const path = require('path');
4
- const https = require('https');
5
4
  const Roster = require('../index.js');
6
5
 
7
6
  // Change these values to your target domain and HTTPS port.
8
7
  const CONFIG = {
9
- domain: 'chinchon.blyts.com',
8
+ domain: 'example.com',
10
9
  httpsPort: 4336,
11
10
  workers: Math.max(1, Math.min(2, os.cpus().length)),
11
+ certificateManagerHttpsPort: 0, // 0 = ephemeral, manager does not serve public traffic
12
12
  wwwPath: path.join(__dirname, 'www'),
13
13
  greenlockStorePath: path.join(__dirname, '..', 'greenlock.d'),
14
- email: 'mclasen@blyts.com',
15
- staging: false,
16
- autoCertificates: true
14
+ email: 'mclasen@example.com',
15
+ staging: false
17
16
  };
18
17
 
19
- async function startWorker() {
18
+ function createRoster({ isCertificateManager }) {
20
19
  const roster = new Roster({
21
20
  local: false,
22
21
  email: CONFIG.email,
23
22
  staging: CONFIG.staging,
24
23
  wwwPath: CONFIG.wwwPath,
25
24
  greenlockStorePath: CONFIG.greenlockStorePath,
26
- autoCertificates: CONFIG.autoCertificates
25
+ autoCertificates: isCertificateManager,
26
+ // Certificate manager can bind an ephemeral HTTPS port; workers serve real traffic.
27
+ port: isCertificateManager ? CONFIG.certificateManagerHttpsPort : 443
27
28
  });
29
+ return roster;
30
+ }
31
+
32
+ async function startWorker() {
33
+ const roster = createRoster({ isCertificateManager: false });
28
34
 
29
35
  // Domain is configured from CONFIG (single source of truth).
30
36
  roster.register(CONFIG.domain, () => {
@@ -35,19 +41,10 @@ async function startWorker() {
35
41
  });
36
42
 
37
43
  await roster.init();
38
- // Automatic cert issuance by roster-server (and renewal loop) in cluster-friendly mode.
39
- const defaultPems = await roster.ensureCertificate(CONFIG.domain);
40
-
41
- const server = https.createServer({
42
- key: defaultPems.key,
43
- cert: defaultPems.cert,
44
- SNICallback: roster.sniCallback(),
45
- minVersion: 'TLSv1.2',
46
- maxVersion: 'TLSv1.3'
44
+ const server = await roster.createServingHttpsServer({
45
+ servername: CONFIG.domain
47
46
  });
48
47
 
49
- roster.attach(server);
50
-
51
48
  server.listen(CONFIG.httpsPort, () => {
52
49
  console.log(`[worker ${process.pid}] listening on https://0.0.0.0:${CONFIG.httpsPort} for domain ${CONFIG.domain}`);
53
50
  });
@@ -58,7 +55,21 @@ async function startPrimary() {
58
55
  console.log(`domain=${CONFIG.domain} port=${CONFIG.httpsPort}`);
59
56
  console.log(`wwwPath=${CONFIG.wwwPath}`);
60
57
  console.log(`greenlockStorePath=${CONFIG.greenlockStorePath}\n`);
61
- console.log(`[primary] cert lifecycle managed by roster-server (autoCertificates=${CONFIG.autoCertificates})\n`);
58
+ console.log('[primary] cert lifecycle managed by roster-server\n');
59
+
60
+ // Primary is the single certificate manager to avoid ACME race conditions.
61
+ // It starts Roster standalone lifecycle so ACME http-01 challenge server (:80) is active.
62
+ const certificateManager = createRoster({ isCertificateManager: true });
63
+ certificateManager.register(CONFIG.domain, () => {
64
+ return (req, res) => {
65
+ res.writeHead(200);
66
+ res.end('certificate-manager');
67
+ };
68
+ });
69
+ await certificateManager.start();
70
+ await certificateManager.ensureCertificate(CONFIG.domain);
71
+ const subject = CONFIG.domain;
72
+ console.log(`[primary] certificate ready for ${CONFIG.domain} (subject=${subject})\n`);
62
73
 
63
74
  for (let i = 0; i < CONFIG.workers; i++) {
64
75
  cluster.fork();
package/index.js CHANGED
@@ -935,6 +935,24 @@ class Roster {
935
935
  return pems;
936
936
  }
937
937
 
938
+ loadCertificate(servername) {
939
+ if (this.local) {
940
+ throw new Error('loadCertificate() is not available in local mode');
941
+ }
942
+ if (!this._initialized) {
943
+ throw new Error('Call init() before loadCertificate()');
944
+ }
945
+ const normalizedServername = this._normalizeHostInput(servername).trim().toLowerCase();
946
+ if (!normalizedServername) {
947
+ throw new Error('servername is required');
948
+ }
949
+ const pems = this._resolvePemsForServername(normalizedServername);
950
+ if (!pems) {
951
+ throw new Error(`No certificate files available for ${normalizedServername}`);
952
+ }
953
+ return pems;
954
+ }
955
+
938
956
  async init() {
939
957
  if (this._initialized) return this;
940
958
  await this.loadSites();
@@ -991,6 +1009,46 @@ class Roster {
991
1009
  return this;
992
1010
  }
993
1011
 
1012
+ async createManagedHttpsServer(options = {}) {
1013
+ if (this.local) throw new Error('createManagedHttpsServer() is not available in local mode');
1014
+ if (!this._initialized) throw new Error('Call init() before createManagedHttpsServer()');
1015
+
1016
+ const {
1017
+ servername,
1018
+ port,
1019
+ ensureCertificate = true,
1020
+ tlsOptions = {}
1021
+ } = options;
1022
+
1023
+ const normalizedServername = this._normalizeHostInput(servername).trim().toLowerCase();
1024
+ if (!normalizedServername) {
1025
+ throw new Error('servername is required');
1026
+ }
1027
+
1028
+ const pems = ensureCertificate
1029
+ ? await this.ensureCertificate(normalizedServername)
1030
+ : this.loadCertificate(normalizedServername);
1031
+
1032
+ const server = https.createServer({
1033
+ minVersion: this.tlsMinVersion,
1034
+ maxVersion: this.tlsMaxVersion,
1035
+ ...tlsOptions,
1036
+ key: pems.key,
1037
+ cert: pems.cert,
1038
+ SNICallback: this.sniCallback()
1039
+ });
1040
+
1041
+ this.attach(server, { port });
1042
+ return server;
1043
+ }
1044
+
1045
+ async createServingHttpsServer(options = {}) {
1046
+ return this.createManagedHttpsServer({
1047
+ ...options,
1048
+ ensureCertificate: false
1049
+ });
1050
+ }
1051
+
994
1052
  startLocalMode() {
995
1053
  this.domainPorts = {};
996
1054
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "roster-server",
3
- "version": "2.4.4",
3
+ "version": "2.4.6",
4
4
  "description": "👾 RosterServer - A domain host router to host multiple HTTPS.",
5
5
  "main": "index.js",
6
6
  "scripts": {
@@ -147,6 +147,31 @@ process.on('message', (msg, connection) => {
147
147
  });
148
148
  ```
149
149
 
150
+ ### Pattern 7: Cluster Production (single cert manager + workers)
151
+ ```javascript
152
+ // PRIMARY: certificate manager (single process)
153
+ const manager = new Roster({
154
+ email: 'admin@example.com',
155
+ greenlockStorePath: '/srv/greenlock.d',
156
+ wwwPath: '/srv/www'
157
+ });
158
+ manager.register('example.com', () => (req, res) => res.end('manager'));
159
+ await manager.start();
160
+ await manager.ensureCertificate('example.com');
161
+
162
+ // WORKER: serving-only process
163
+ const worker = new Roster({
164
+ email: 'admin@example.com',
165
+ greenlockStorePath: '/srv/greenlock.d',
166
+ wwwPath: '/srv/www',
167
+ autoCertificates: false
168
+ });
169
+ worker.register('example.com', () => (req, res) => res.end('worker'));
170
+ await worker.init();
171
+ const httpsServer = await worker.createServingHttpsServer({ servername: 'example.com' });
172
+ httpsServer.listen(4336);
173
+ ```
174
+
150
175
  ## Key Configuration Options
151
176
 
152
177
  ```javascript
@@ -194,6 +219,15 @@ Returns `(servername, callback) => void` TLS SNI callback that resolves certs fr
194
219
  ### `roster.ensureCertificate(servername)`
195
220
  Forces certificate availability for a domain and returns `{ key, cert }`. With `autoCertificates` enabled (default), it issues certs automatically when missing.
196
221
 
222
+ ### `roster.loadCertificate(servername)`
223
+ Loads existing `{ key, cert }` from `greenlockStorePath` without issuing new certificates.
224
+
225
+ ### `roster.createManagedHttpsServer(options)`
226
+ Creates a pre-wired `https.Server` with default cert, SNI callback, and attached request/upgrade handlers.
227
+
228
+ ### `roster.createServingHttpsServer(options)`
229
+ Serving-only helper for worker processes. Same as `createManagedHttpsServer(..., ensureCertificate: false)`.
230
+
197
231
  ### `roster.attach(server, { port }?)`
198
232
  Convenience: wires `requestHandler` + `upgradeHandler` onto an external `http.Server` or `https.Server`. Returns `this`. Requires `init()` first.
199
233
 
@@ -933,6 +933,60 @@ describe('Roster ensureCertificate()', () => {
933
933
  });
934
934
  });
935
935
 
936
+ describe('Roster loadCertificate()', () => {
937
+ it('throws if called before init()', () => {
938
+ const roster = new Roster({ local: false });
939
+ assert.throws(() => roster.loadCertificate('example.com'), /Call init\(\) before loadCertificate/);
940
+ });
941
+
942
+ it('throws in local mode', async () => {
943
+ const roster = new Roster({ local: true });
944
+ roster.register('local-load.example', () => () => {});
945
+ await roster.init();
946
+ assert.throws(() => roster.loadCertificate('local-load.example'), /not available in local mode/);
947
+ });
948
+ });
949
+
950
+ describe('Roster createManagedHttpsServer()', () => {
951
+ it('throws if called before init()', async () => {
952
+ const roster = new Roster({ local: false });
953
+ await assert.rejects(
954
+ () => roster.createManagedHttpsServer({ servername: 'example.com' }),
955
+ /Call init\(\) before createManagedHttpsServer/
956
+ );
957
+ });
958
+
959
+ it('throws in local mode', async () => {
960
+ const roster = new Roster({ local: true });
961
+ roster.register('local-managed.example', () => () => {});
962
+ await roster.init();
963
+ await assert.rejects(
964
+ () => roster.createManagedHttpsServer({ servername: 'local-managed.example' }),
965
+ /not available in local mode/
966
+ );
967
+ });
968
+ });
969
+
970
+ describe('Roster createServingHttpsServer()', () => {
971
+ it('throws if called before init()', async () => {
972
+ const roster = new Roster({ local: false });
973
+ await assert.rejects(
974
+ () => roster.createServingHttpsServer({ servername: 'example.com' }),
975
+ /Call init\(\) before createManagedHttpsServer/
976
+ );
977
+ });
978
+
979
+ it('throws in local mode', async () => {
980
+ const roster = new Roster({ local: true });
981
+ roster.register('local-serving.example', () => () => {});
982
+ await roster.init();
983
+ await assert.rejects(
984
+ () => roster.createServingHttpsServer({ servername: 'local-serving.example' }),
985
+ /not available in local mode/
986
+ );
987
+ });
988
+ });
989
+
936
990
  describe('Roster attach()', () => {
937
991
  it('throws if called before init()', () => {
938
992
  const roster = new Roster({ local: true });
@@ -1,60 +0,0 @@
1
- const http = require('http');
2
- const Roster = require('../index.js');
3
-
4
- async function main() {
5
- const roster = new Roster({
6
- local: true,
7
- minLocalPort: 19500,
8
- maxLocalPort: 19550
9
- });
10
-
11
- roster.register('example.com', () => {
12
- return (req, res) => {
13
- res.writeHead(200, { 'Content-Type': 'text/plain' });
14
- res.end('[example.com] hello from external worker wiring');
15
- };
16
- });
17
-
18
- roster.register('api.example.com:9000', () => {
19
- return (req, res) => {
20
- res.writeHead(200, { 'Content-Type': 'application/json' });
21
- res.end(JSON.stringify({ ok: true, source: 'api.example.com:9000' }));
22
- };
23
- });
24
-
25
- // init() prepares virtual-host routing, but does not create/listen sockets.
26
- await roster.init();
27
-
28
- // This server represents your external runtime-owned worker server.
29
- const server443 = http.createServer();
30
- roster.attach(server443); // defaultPort routes (443)
31
-
32
- const server9000 = http.createServer();
33
- roster.attach(server9000, { port: 9000 }); // custom port routes
34
-
35
- await new Promise((resolve, reject) => {
36
- server443.listen(19501, 'localhost', resolve);
37
- server443.on('error', reject);
38
- });
39
-
40
- await new Promise((resolve, reject) => {
41
- server9000.listen(19502, 'localhost', resolve);
42
- server9000.on('error', reject);
43
- });
44
-
45
- console.log('\n✅ Cluster-friendly worker demo running');
46
- console.log('Roster did not bind ports itself. External servers own listen().\n');
47
- console.log('Try requests with Host headers:');
48
- console.log('curl -H "Host: example.com" http://localhost:19501/');
49
- console.log('curl -H "Host: api.example.com" http://localhost:19502/\n');
50
-
51
- // Sticky-session runtimes normally pass accepted sockets into the worker:
52
- // process.on('message', (msg, socket) => {
53
- // if (msg === 'sticky-session:connection') server443.emit('connection', socket);
54
- // });
55
- }
56
-
57
- main().catch((err) => {
58
- console.error('Demo failed:', err);
59
- process.exit(1);
60
- });