roster-server 2.0.4 → 2.0.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.
Files changed (3) hide show
  1. package/README.md +14 -113
  2. package/index.js +99 -111
  3. package/package.json +2 -1
package/README.md CHANGED
@@ -11,6 +11,7 @@ Welcome to **RosterServer**, the ultimate domain host router with automatic HTTP
11
11
  - **Virtual Hosting**: Serve multiple domains from a single server.
12
12
  - **Automatic Redirects**: Redirect `www` subdomains to the root domain.
13
13
  - **Zero Configuration**: Well, almost zero. Just a tiny bit of setup.
14
+ - **Bun compatible**: Works with both Node.js and [Bun](https://bun.sh).
14
15
 
15
16
  ## 📦 Installation
16
17
 
@@ -18,6 +19,12 @@ Welcome to **RosterServer**, the ultimate domain host router with automatic HTTP
18
19
  npm install roster-server
19
20
  ```
20
21
 
22
+ Or with [Bun](https://bun.sh):
23
+
24
+ ```bash
25
+ bun add roster-server
26
+ ```
27
+
21
28
  ## 🤖 AI Skill
22
29
 
23
30
  You can also add RosterServer as a skill for AI agentic development:
@@ -145,10 +152,16 @@ roster.register('example.com:8080', (httpsServer) => {
145
152
  ### Running the Server
146
153
 
147
154
  ```bash
148
- # /srv/roster/server.js
155
+ # With Node.js
149
156
  node server.js
150
157
  ```
151
158
 
159
+ Or with Bun:
160
+
161
+ ```bash
162
+ bun server.js
163
+ ```
164
+
152
165
  And that's it! Your server is now hosting multiple HTTPS-enabled sites. 🎉
153
166
 
154
167
  ## 🤯 But Wait, There's More!
@@ -176,118 +189,6 @@ When creating a new `RosterServer` instance, you can pass the following options:
176
189
  - `local` (boolean): Set to `true` to run in local development mode.
177
190
  - `minLocalPort` (number): Minimum port for local mode (default: 4000).
178
191
  - `maxLocalPort` (number): Maximum port for local mode (default: 9999).
179
- - `tlsMode` (string): TLS backend to use — `'auto'` (default), `'greenlock'`, or `'static'`. See [TLS Configuration](#-tls-configuration) below.
180
- - `tlsDomain` (string): Domain whose cert files are pre-loaded as the server default in static mode (optional).
181
- - `tls` (object): Additional TLS options passed to `https.createServer` (e.g. `minVersion`, `maxVersion`, `ciphers`).
182
-
183
- ## 🔒 TLS Configuration
184
-
185
- RosterServer supports three TLS backends, selectable with the `tlsMode` option.
186
-
187
- ### Behavior matrix
188
-
189
- | `tlsMode` | Runtime | HTTPS server |
190
- |-----------|---------|-------------|
191
- | `'auto'` (default) | Node.js | Greenlock SNI — certs managed and auto-renewed automatically |
192
- | `'auto'` (default) | Bun | Static file certs from `greenlockStorePath/live/<domain>/` |
193
- | `'greenlock'` | any | Always Greenlock SNI |
194
- | `'static'` | any | Always static file certs |
195
-
196
- Bun's TLS stack does not support Greenlock's async SNICallback, causing a `tlsv1 alert protocol version` error. `'auto'` mode detects the runtime and picks the correct backend transparently.
197
-
198
- ### Static mode cert layout
199
-
200
- In `'auto'` (Bun) or `'static'` mode, certs are read per-domain from:
201
-
202
- ```
203
- greenlockStorePath/
204
- live/
205
- example.com/
206
- privkey.pem
207
- cert.pem
208
- chain.pem
209
- api.example.com/
210
- privkey.pem
211
- cert.pem
212
- chain.pem
213
- ```
214
-
215
- Greenlock populates this layout automatically when it renews certificates, so no extra tooling is required.
216
-
217
- ### Default TLS options
218
-
219
- The static-cert path enforces secure defaults that can be overridden with the `tls` option:
220
-
221
- ```javascript
222
- { minVersion: 'TLSv1.2', maxVersion: 'TLSv1.3' }
223
- ```
224
-
225
- ### Examples
226
-
227
- **Default — works on both Node and Bun without changes:**
228
-
229
- ```javascript
230
- const roster = new Roster({
231
- email: 'admin@example.com',
232
- wwwPath: '/srv/www'
233
- // tlsMode defaults to 'auto'
234
- });
235
- roster.start();
236
- ```
237
-
238
- **Force Greenlock on all runtimes:**
239
-
240
- ```javascript
241
- const roster = new Roster({
242
- email: 'admin@example.com',
243
- wwwPath: '/srv/www',
244
- tlsMode: 'greenlock'
245
- });
246
- roster.start();
247
- ```
248
-
249
- **Force static certs with a pre-loaded default cert (avoids SNI-less connection failures):**
250
-
251
- ```javascript
252
- const roster = new Roster({
253
- email: 'admin@example.com',
254
- wwwPath: '/srv/www',
255
- tlsMode: 'static',
256
- tlsDomain: 'example.com' // loaded as the server's fallback cert
257
- });
258
- roster.start();
259
- ```
260
-
261
- **Custom TLS options (e.g. restrict ciphers):**
262
-
263
- ```javascript
264
- const roster = new Roster({
265
- email: 'admin@example.com',
266
- wwwPath: '/srv/www',
267
- tls: {
268
- minVersion: 'TLSv1.2',
269
- ciphers: 'TLS_AES_256_GCM_SHA384:TLS_CHACHA20_POLY1305_SHA256'
270
- }
271
- });
272
- roster.start();
273
- ```
274
-
275
- ### Smoke test
276
-
277
- After deploying, verify TLS is working:
278
-
279
- ```bash
280
- curl -v https://example.com
281
- # Should show TLSv1.2 or TLSv1.3 in the handshake — no "alert protocol version" errors
282
- ```
283
-
284
- Server logs will display the active mode on startup:
285
-
286
- ```
287
- Runtime: bun | TLS mode: static
288
- HTTPS port 443: using static certs from /srv/greenlock.d/live [static]
289
- HTTPS server listening on port 443
290
- ```
291
192
 
292
193
  ## 🏠 Local Development Mode
293
194
 
package/index.js CHANGED
@@ -166,84 +166,14 @@ class Roster {
166
166
  this.filename = options.filename || 'index';
167
167
  this.minLocalPort = options.minLocalPort || 4000;
168
168
  this.maxLocalPort = options.maxLocalPort || 9999;
169
+ this.tlsMinVersion = options.tlsMinVersion ?? 'TLSv1.2';
170
+ this.tlsMaxVersion = options.tlsMaxVersion ?? 'TLSv1.3';
169
171
 
170
172
  const port = options.port === undefined ? 443 : options.port;
171
173
  if (port === 80 && !this.local) {
172
174
  throw new Error('⚠️ Port 80 is reserved for ACME challenge. Please use a different port.');
173
175
  }
174
176
  this.defaultPort = port;
175
-
176
- const validTlsModes = ['auto', 'greenlock', 'static'];
177
- this.tlsMode = options.tlsMode || 'auto';
178
- if (!validTlsModes.includes(this.tlsMode)) {
179
- throw new Error(`Invalid tlsMode "${this.tlsMode}". Must be one of: ${validTlsModes.join(', ')}`);
180
- }
181
- this.tlsDomain = options.tlsDomain || null;
182
- this.tlsOptions = options.tls || {};
183
- }
184
-
185
- detectRuntime() {
186
- return typeof Bun !== 'undefined' ? 'bun' : 'node';
187
- }
188
-
189
- getEffectiveTlsMode() {
190
- if (this.tlsMode !== 'auto') {
191
- return this.tlsMode;
192
- }
193
- return this.detectRuntime() === 'bun' ? 'static' : 'greenlock';
194
- }
195
-
196
- getDefaultTlsOptions() {
197
- return Object.assign({ minVersion: 'TLSv1.2', maxVersion: 'TLSv1.3' }, this.tlsOptions);
198
- }
199
-
200
- createSNICallback() {
201
- return (domain, callback) => {
202
- try {
203
- const certPath = path.join(this.greenlockStorePath, 'live', domain);
204
- const keyPath = path.join(certPath, 'privkey.pem');
205
- const certFilePath = path.join(certPath, 'cert.pem');
206
- const chainPath = path.join(certPath, 'chain.pem');
207
-
208
- if (fs.existsSync(keyPath) && fs.existsSync(certFilePath) && fs.existsSync(chainPath)) {
209
- const key = fs.readFileSync(keyPath, 'utf8');
210
- const cert = fs.readFileSync(certFilePath, 'utf8');
211
- const chain = fs.readFileSync(chainPath, 'utf8');
212
-
213
- callback(null, tls.createSecureContext({ key, cert: cert + chain }));
214
- } else {
215
- callback(new Error(`No certificate files available for ${domain} at ${certPath}`));
216
- }
217
- } catch (error) {
218
- callback(error);
219
- }
220
- };
221
- }
222
-
223
- createStaticHttpsServer(dispatcher) {
224
- const tlsOpts = Object.assign(this.getDefaultTlsOptions(), {
225
- SNICallback: this.createSNICallback()
226
- });
227
-
228
- if (this.tlsDomain) {
229
- const certPath = path.join(this.greenlockStorePath, 'live', this.tlsDomain);
230
- const keyPath = path.join(certPath, 'privkey.pem');
231
- const certFilePath = path.join(certPath, 'cert.pem');
232
- const chainPath = path.join(certPath, 'chain.pem');
233
-
234
- const missing = [keyPath, certFilePath, chainPath].filter(p => !fs.existsSync(p));
235
- if (missing.length > 0) {
236
- throw new Error(
237
- `Static TLS cert files missing for domain "${this.tlsDomain}":\n` +
238
- missing.map(p => ` - ${p}`).join('\n')
239
- );
240
- }
241
-
242
- tlsOpts.key = fs.readFileSync(keyPath, 'utf8');
243
- tlsOpts.cert = fs.readFileSync(certFilePath, 'utf8') + fs.readFileSync(chainPath, 'utf8');
244
- }
245
-
246
- return https.createServer(tlsOpts, dispatcher);
247
177
  }
248
178
 
249
179
  async loadSites() {
@@ -446,13 +376,13 @@ class Roster {
446
376
  getUrl(domain) {
447
377
  // Remove www prefix if present
448
378
  const cleanDomain = domain.startsWith('www.') ? domain.slice(4) : domain;
449
-
379
+
450
380
  // Check if domain is registered
451
381
  const isRegistered = this.sites[cleanDomain] || this.sites[`www.${cleanDomain}`];
452
382
  if (!isRegistered) {
453
383
  return null;
454
384
  }
455
-
385
+
456
386
  // Return URL based on environment
457
387
  if (this.local) {
458
388
  // Local mode: return localhost URL with assigned port
@@ -509,7 +439,7 @@ class Roster {
509
439
  startLocalMode() {
510
440
  // Store mapping of domain to port for later retrieval
511
441
  this.domainPorts = {};
512
-
442
+
513
443
  // Create a simple HTTP server for each domain with CRC32-based ports
514
444
  for (const [hostKey, siteApp] of Object.entries(this.sites)) {
515
445
  const domain = hostKey.split(':')[0]; // Remove port if present
@@ -521,7 +451,7 @@ class Roster {
521
451
 
522
452
  // Calculate deterministic port based on domain CRC32, with collision detection
523
453
  const port = this.assignPortToDomain(domain);
524
-
454
+
525
455
  // Store domain → port mapping
526
456
  this.domainPorts[domain] = port;
527
457
 
@@ -651,10 +581,6 @@ class Roster {
651
581
  };
652
582
  };
653
583
 
654
- const runtime = this.detectRuntime();
655
- const effectiveTlsMode = this.getEffectiveTlsMode();
656
- log.info(`Runtime: ${runtime} | TLS mode: ${effectiveTlsMode}`);
657
-
658
584
  httpServer.listen(80, this.hostname, () => {
659
585
  log.info('HTTP server listening on port 80');
660
586
  });
@@ -671,53 +597,115 @@ class Roster {
671
597
  if (virtualServer) {
672
598
  virtualServer.processUpgrade(req, socket, head);
673
599
  } else {
600
+ // No virtual server found, destroy the socket
674
601
  socket.destroy();
675
602
  }
676
603
  };
677
604
  };
678
605
 
679
- const attachHttpsListeners = (httpsServer, portNum, upgradeHandler) => {
680
- httpsServer.on('upgrade', upgradeHandler);
681
- httpsServer.on('error', (error) => {
682
- log.error(`HTTPS server error on port ${portNum}:`, error.message);
683
- });
684
- httpsServer.on('tlsClientError', (error) => {
685
- if (!error.message.includes('http request')) {
686
- log.error(`TLS error on port ${portNum}:`, error.message);
687
- }
688
- });
689
- };
690
-
691
606
  // Handle different port types
692
607
  for (const [port, portData] of Object.entries(sitesByPort)) {
693
608
  const portNum = parseInt(port);
694
609
  const dispatcher = createDispatcher(portData);
695
610
  const upgradeHandler = createUpgradeHandler(portData);
696
611
 
697
- let httpsServer;
612
+ if (portNum === this.defaultPort) {
613
+ // Bun has known gaps around SNICallback compatibility.
614
+ // Fallback to static cert loading for the primary domain on default HTTPS port.
615
+ const isBunRuntime = typeof Bun !== 'undefined' || process.release?.name === 'bun';
616
+ const tlsOpts = { minVersion: this.tlsMinVersion, maxVersion: this.tlsMaxVersion };
617
+ let httpsServer;
618
+
619
+ if (isBunRuntime) {
620
+ const primaryDomain = Object.keys(portData.virtualServers)[0];
621
+ const certPath = path.join(this.greenlockStorePath, 'live', primaryDomain);
622
+ const keyPath = path.join(certPath, 'privkey.pem');
623
+ const certFilePath = path.join(certPath, 'cert.pem');
624
+ const chainPath = path.join(certPath, 'chain.pem');
625
+
626
+ if (fs.existsSync(keyPath) && fs.existsSync(certFilePath) && fs.existsSync(chainPath)) {
627
+ const key = fs.readFileSync(keyPath, 'utf8');
628
+ const cert = fs.readFileSync(certFilePath, 'utf8');
629
+ const chain = fs.readFileSync(chainPath, 'utf8');
630
+ httpsServer = https.createServer({
631
+ ...tlsOpts,
632
+ key,
633
+ cert: cert + chain
634
+ }, dispatcher);
635
+ log.warn(`⚠️ Bun runtime detected: using static TLS cert for ${primaryDomain} on port ${portNum}`);
636
+ } else {
637
+ log.warn(`⚠️ Bun runtime detected but cert files missing for ${primaryDomain}; falling back to Greenlock HTTPS server`);
638
+ httpsServer = glx.httpsServer(tlsOpts, dispatcher);
639
+ }
640
+ } else {
641
+ httpsServer = glx.httpsServer(tlsOpts, dispatcher);
642
+ }
643
+
644
+ this.portServers[portNum] = httpsServer;
698
645
 
699
- if (portNum === this.defaultPort && effectiveTlsMode === 'greenlock') {
700
- log.info(`HTTPS port ${portNum}: using Greenlock SNI (certs managed automatically)`);
701
- httpsServer = glx.httpsServer(null, dispatcher);
646
+ // Handle WebSocket upgrade events
647
+ httpsServer.on('upgrade', upgradeHandler);
648
+
649
+ httpsServer.listen(portNum, this.hostname, () => {
650
+ log.info(`HTTPS server listening on port ${portNum}`);
651
+ });
702
652
  } else {
703
- const modeLabel = portNum === this.defaultPort ? effectiveTlsMode : 'static';
704
- log.info(`HTTPS port ${portNum}: using static certs from ${path.join(this.greenlockStorePath, 'live')} [${modeLabel}]`);
705
- const httpsOptions = Object.assign(this.getDefaultTlsOptions(), {
706
- SNICallback: this.createSNICallback()
653
+ // Create HTTPS server for custom ports using Greenlock certificates
654
+ const httpsOptions = {
655
+ minVersion: this.tlsMinVersion,
656
+ maxVersion: this.tlsMaxVersion,
657
+ // SNI callback to get certificates dynamically
658
+ SNICallback: (domain, callback) => {
659
+ try {
660
+ const certPath = path.join(this.greenlockStorePath, 'live', domain);
661
+ const keyPath = path.join(certPath, 'privkey.pem');
662
+ const certFilePath = path.join(certPath, 'cert.pem');
663
+ const chainPath = path.join(certPath, 'chain.pem');
664
+
665
+ if (fs.existsSync(keyPath) && fs.existsSync(certFilePath) && fs.existsSync(chainPath)) {
666
+ const key = fs.readFileSync(keyPath, 'utf8');
667
+ const cert = fs.readFileSync(certFilePath, 'utf8');
668
+ const chain = fs.readFileSync(chainPath, 'utf8');
669
+
670
+ callback(null, tls.createSecureContext({
671
+ key: key,
672
+ cert: cert + chain
673
+ }));
674
+ } else {
675
+ callback(new Error(`No certificate files available for ${domain}`));
676
+ }
677
+ } catch (error) {
678
+ callback(error);
679
+ }
680
+ }
681
+ };
682
+
683
+ const httpsServer = https.createServer(httpsOptions, dispatcher);
684
+
685
+ // Handle WebSocket upgrade events
686
+ httpsServer.on('upgrade', upgradeHandler);
687
+
688
+ httpsServer.on('error', (error) => {
689
+ log.error(`HTTPS server error on port ${portNum}:`, error.message);
707
690
  });
708
- httpsServer = https.createServer(httpsOptions, dispatcher);
709
- }
710
691
 
711
- attachHttpsListeners(httpsServer, portNum, upgradeHandler);
712
- this.portServers[portNum] = httpsServer;
692
+ httpsServer.on('tlsClientError', (error) => {
693
+ // Suppress HTTP request errors to avoid log spam
694
+ if (!error.message.includes('http request')) {
695
+ log.error(`TLS error on port ${portNum}:`, error.message);
696
+ }
697
+ });
713
698
 
714
- httpsServer.listen(portNum, this.hostname, (error) => {
715
- if (error) {
716
- log.error(`Failed to start HTTPS server on port ${portNum}:`, error.message);
717
- } else {
718
- log.info(`HTTPS server listening on port ${portNum}`);
719
- }
720
- });
699
+ this.portServers[portNum] = httpsServer;
700
+
701
+ httpsServer.listen(portNum, this.hostname, (error) => {
702
+ if (error) {
703
+ log.error(`Failed to start HTTPS server on port ${portNum}:`, error.message);
704
+ } else {
705
+ log.info(`HTTPS server listening on port ${portNum}`);
706
+ }
707
+ });
708
+ }
721
709
  }
722
710
  });
723
711
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "roster-server",
3
- "version": "2.0.4",
3
+ "version": "2.0.6",
4
4
  "description": "👾 RosterServer - A domain host router to host multiple HTTPS.",
5
5
  "main": "index.js",
6
6
  "scripts": {
@@ -26,6 +26,7 @@
26
26
  "express",
27
27
  "greenlock-express",
28
28
  "shotx",
29
+ "bun",
29
30
  "clasen"
30
31
  ],
31
32
  "author": "Martin Clasen",