roster-server 2.0.4 β 2.1.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/README.md +57 -122
- package/demo/wildcard-example.js +31 -0
- package/index.js +292 -155
- package/package.json +4 -2
- package/skills/roster-server/SKILL.md +12 -4
- package/test/roster-server.test.js +446 -0
- package/vendor/acme-dns-01-cli-wrapper.js +161 -0
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:
|
|
@@ -39,17 +46,42 @@ Your project should look something like this:
|
|
|
39
46
|
βββ www/
|
|
40
47
|
βββ example.com/
|
|
41
48
|
β βββ index.js
|
|
42
|
-
|
|
49
|
+
βββ subdomain.example.com/
|
|
50
|
+
β βββ index.js
|
|
51
|
+
βββ other-domain.com/
|
|
43
52
|
β βββ index.js
|
|
44
|
-
βββ
|
|
45
|
-
βββ index.js
|
|
53
|
+
βββ *.example.com/ # Wildcard: one handler for all subdomains (api.example.com, app.example.com, etc.)
|
|
54
|
+
βββ index.js
|
|
46
55
|
```
|
|
47
56
|
|
|
57
|
+
### Wildcard DNS (*.example.com)
|
|
58
|
+
|
|
59
|
+
You can serve all subdomains of a domain with a single handler in three ways:
|
|
60
|
+
|
|
61
|
+
1. **Folder**: Create a directory named literally `*.example.com` under `www` (e.g. `www/*.example.com/index.js`). Any request to `api.example.com`, `app.example.com`, etc. will use that handler.
|
|
62
|
+
2. **Register (default port)**: `roster.register('*.example.com', handler)` for the default HTTPS port.
|
|
63
|
+
3. **Register (custom port)**: `roster.register('*.example.com:8080', handler)` for a specific port.
|
|
64
|
+
|
|
65
|
+
Wildcard SSL certificates require **DNS-01** validation (Let's Encrypt does not support HTTP-01 for wildcards). By default Roster uses `acme-dns-01-cli` through an internal wrapper (adds `propagationDelay` and modern plugin signatures). Override with a custom plugin:
|
|
66
|
+
|
|
67
|
+
```javascript
|
|
68
|
+
import Roster from 'roster-server';
|
|
69
|
+
|
|
70
|
+
const roster = new Roster({
|
|
71
|
+
email: 'admin@example.com',
|
|
72
|
+
wwwPath: '/srv/www',
|
|
73
|
+
greenlockStorePath: '/srv/greenlock.d',
|
|
74
|
+
dnsChallenge: { module: 'acme-dns-01-route53', /* provider options */ } // optional override
|
|
75
|
+
});
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
Set `dnsChallenge: false` to disable. For other DNS providers install the plugin in your app and pass it. See [Greenlock DNS plugins](https://git.rootprojects.org/root/greenlock-express.js#dns-01-challenge-plugins).
|
|
79
|
+
|
|
48
80
|
### Setting Up Your Server
|
|
49
81
|
|
|
50
82
|
```javascript
|
|
51
83
|
// /srv/roster/server.js
|
|
52
|
-
|
|
84
|
+
import Roster from 'roster-server';
|
|
53
85
|
|
|
54
86
|
const options = {
|
|
55
87
|
email: 'admin@example.com',
|
|
@@ -71,7 +103,7 @@ I'll help analyze the example files shown. You have 3 different implementations
|
|
|
71
103
|
|
|
72
104
|
1. **Basic HTTP Handler**:
|
|
73
105
|
```javascript:demo/www/example.com/index.js
|
|
74
|
-
|
|
106
|
+
export default (httpsServer) => {
|
|
75
107
|
return (req, res) => {
|
|
76
108
|
res.writeHead(200, { 'Content-Type': 'text/plain; charset=utf-8' });
|
|
77
109
|
res.end('"Loco de pensar, queriendo entrar en razΓ³n, y el corazΓ³n tiene razones que la propia razΓ³n nunca entenderΓ‘."');
|
|
@@ -81,9 +113,9 @@ module.exports = (httpsServer) => {
|
|
|
81
113
|
|
|
82
114
|
2. **Express App**:
|
|
83
115
|
```javascript:demo/www/express.example.com/index.js
|
|
84
|
-
|
|
116
|
+
import express from 'express';
|
|
85
117
|
|
|
86
|
-
|
|
118
|
+
export default (httpsServer) => {
|
|
87
119
|
const app = express();
|
|
88
120
|
app.get('/', (req, res) => {
|
|
89
121
|
res.setHeader('Content-Type', 'text/plain; charset=utf-8');
|
|
@@ -96,9 +128,9 @@ module.exports = (httpsServer) => {
|
|
|
96
128
|
|
|
97
129
|
3. **Socket.IO Server**:
|
|
98
130
|
```javascript:demo/www/sio.example.com/index.js
|
|
99
|
-
|
|
131
|
+
import { Server } from 'socket.io';
|
|
100
132
|
|
|
101
|
-
|
|
133
|
+
export default (httpsServer) => {
|
|
102
134
|
const io = new Server(httpsServer);
|
|
103
135
|
|
|
104
136
|
io.on('connection', (socket) => {
|
|
@@ -145,10 +177,16 @@ roster.register('example.com:8080', (httpsServer) => {
|
|
|
145
177
|
### Running the Server
|
|
146
178
|
|
|
147
179
|
```bash
|
|
148
|
-
#
|
|
180
|
+
# With Node.js
|
|
149
181
|
node server.js
|
|
150
182
|
```
|
|
151
183
|
|
|
184
|
+
Or with Bun:
|
|
185
|
+
|
|
186
|
+
```bash
|
|
187
|
+
bun server.js
|
|
188
|
+
```
|
|
189
|
+
|
|
152
190
|
And that's it! Your server is now hosting multiple HTTPS-enabled sites. π
|
|
153
191
|
|
|
154
192
|
## π€― But Wait, There's More!
|
|
@@ -172,122 +210,11 @@ When creating a new `RosterServer` instance, you can pass the following options:
|
|
|
172
210
|
- `email` (string): Your email for Let's Encrypt notifications.
|
|
173
211
|
- `wwwPath` (string): Path to your `www` directory containing your sites.
|
|
174
212
|
- `greenlockStorePath` (string): Directory for Greenlock configuration.
|
|
213
|
+
- `dnsChallenge` (object|false): Optional override for wildcard DNS-01 challenge config. Default is local/manual `acme-dns-01-cli` wrapper with `propagationDelay: 120000`, `autoContinue: false`, and `dryRunDelay: 120000`. This is safer for manual DNS providers (Linode/Cloudflare UI) because Roster waits longer and does not auto-advance in interactive terminals. Set `false` to disable. You can pass `{ module: '...', propagationDelay: 180000 }` to tune DNS wait time (ms). Set `autoContinue: true` (or env `ROSTER_DNS_AUTO_CONTINUE=1`) to continue automatically after delay. For Greenlock dry-runs (`_greenlock-dryrun-*`), delay defaults to `dryRunDelay` (same as `propagationDelay` unless overridden with `dnsChallenge.dryRunDelay` or env `ROSTER_DNS_DRYRUN_DELAY_MS`).
|
|
175
214
|
- `staging` (boolean): Set to `true` to use Let's Encrypt's staging environment (for testing).
|
|
176
215
|
- `local` (boolean): Set to `true` to run in local development mode.
|
|
177
216
|
- `minLocalPort` (number): Minimum port for local mode (default: 4000).
|
|
178
217
|
- `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
218
|
|
|
292
219
|
## π Local Development Mode
|
|
293
220
|
|
|
@@ -298,6 +225,8 @@ When `{ local: true }` is enabled, RosterServer **Skips SSL/HTTPS**: Runs pure H
|
|
|
298
225
|
### Setting Up Local Mode
|
|
299
226
|
|
|
300
227
|
```javascript
|
|
228
|
+
import Roster from 'roster-server';
|
|
229
|
+
|
|
301
230
|
const server = new Roster({
|
|
302
231
|
wwwPath: '/srv/www',
|
|
303
232
|
local: true, // Enable local development mode
|
|
@@ -318,6 +247,8 @@ In local mode, domains are automatically assigned ports based on a CRC32 hash of
|
|
|
318
247
|
You can customize the port range:
|
|
319
248
|
|
|
320
249
|
```javascript
|
|
250
|
+
import Roster from 'roster-server';
|
|
251
|
+
|
|
321
252
|
const roster = new Roster({
|
|
322
253
|
local: true,
|
|
323
254
|
minLocalPort: 5000, // Start from port 5000
|
|
@@ -332,6 +263,8 @@ RosterServer provides a method to get the URL for a domain that adapts automatic
|
|
|
332
263
|
**Instance Method: `roster.getUrl(domain)`**
|
|
333
264
|
|
|
334
265
|
```javascript
|
|
266
|
+
import Roster from 'roster-server';
|
|
267
|
+
|
|
335
268
|
const roster = new Roster({ local: true });
|
|
336
269
|
roster.register('example.com', handler);
|
|
337
270
|
|
|
@@ -354,6 +287,8 @@ This method:
|
|
|
354
287
|
**Example Usage:**
|
|
355
288
|
|
|
356
289
|
```javascript
|
|
290
|
+
import Roster from 'roster-server';
|
|
291
|
+
|
|
357
292
|
// Local development
|
|
358
293
|
const localRoster = new Roster({ local: true });
|
|
359
294
|
localRoster.register('example.com', handler);
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Wildcard DNS demo: one handler for all subdomains (*.example.com)
|
|
3
|
+
*
|
|
4
|
+
* Run from repo root: node demo/wildcard-example.js
|
|
5
|
+
* Then open the printed URL (or use curl with Host header) to see the wildcard response.
|
|
6
|
+
* Any subdomain (api.example.com, app.example.com, foo.example.com) uses the same handler.
|
|
7
|
+
*/
|
|
8
|
+
const Roster = require('../index.js');
|
|
9
|
+
const path = require('path');
|
|
10
|
+
const { wildcardRoot } = require('../index.js');
|
|
11
|
+
|
|
12
|
+
const roster = new Roster({
|
|
13
|
+
local: true,
|
|
14
|
+
wwwPath: path.join(__dirname, 'www'),
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
roster.start().then(() => {
|
|
18
|
+
const wildcardPattern = roster.domains.find((d) => d.startsWith('*.'));
|
|
19
|
+
const subdomain = wildcardPattern ? 'api.' + wildcardRoot(wildcardPattern) : 'api.example.com';
|
|
20
|
+
const wildcardUrl = roster.getUrl(subdomain);
|
|
21
|
+
|
|
22
|
+
console.log('\nπ Wildcard demo');
|
|
23
|
+
console.log(' Loaded:', wildcardPattern ? `https://${wildcardPattern}` : '(none)');
|
|
24
|
+
console.log(' Any subdomain uses the same handler.\n');
|
|
25
|
+
console.log(' Try:', wildcardUrl || '(no wildcard site in www path)');
|
|
26
|
+
if (wildcardUrl) {
|
|
27
|
+
const host = wildcardPattern ? 'api.' + wildcardRoot(wildcardPattern) : 'api.example.com';
|
|
28
|
+
console.log(' Or: curl -H "Host: ' + host + '"', wildcardUrl);
|
|
29
|
+
}
|
|
30
|
+
console.log('');
|
|
31
|
+
});
|