roster-server 2.4.12 β 2.4.14
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 +198 -398
- package/index.js +434 -126
- package/lib/static-site-handler.js +36 -27
- package/package.json +1 -1
- package/skills/roster-server/SKILL.md +50 -338
- package/test/https-integration.test.js +149 -0
- package/test/lifecycle.test.js +469 -0
- package/test/roster-server.test.js +22 -22
- package/test/scanner-blocker.test.js +3 -6
- package/test/signals.test.js +183 -0
- package/test/static-and-tls.test.js +273 -0
- package/vendor/greenlock-express/greenlock-express.js +1 -1
- package/vendor/greenlock-express/servers.js +2 -1
- package/vendor/greenlock-express/single.js +1 -1
- package/vendor/greenlock-express/worker.js +2 -2
package/README.md
CHANGED
|
@@ -1,513 +1,317 @@
|
|
|
1
1
|
# πΎ RosterServer
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Host multiple domains from one Node.js process, with HTTPS certificates managed through Greenlock, Express and Socket.IO integration, static file serving, and local HTTP development. Each site receives a virtual server for request and upgrade listeners; applications share the process.
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
## Installation
|
|
6
6
|
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
- **Automatic HTTPS** with Let's Encrypt via Greenlock.
|
|
10
|
-
- **Dynamic Site Loading**: Just drop your Node.js apps in the `www` folder.
|
|
11
|
-
- **Static Sites**: No code? No problem. Drop a folder with `index.html` (and assets) and RosterServer serves it automaticallyβmodular static handler with path-traversal protection and strict 404s.
|
|
12
|
-
- **Virtual Hosting**: Serve multiple domains from a single server.
|
|
13
|
-
- **Automatic Redirects**: Redirect `www` subdomains to the root domain.
|
|
14
|
-
- **Optional Request Plugins**: Run synchronous filters before site handlers, including the bundled scanner blocker.
|
|
15
|
-
- **Zero Configuration**: Well, almost zero. Just a tiny bit of setup.
|
|
16
|
-
- **Bun compatible**: Works with both Node.js and [Bun](https://bun.sh).
|
|
17
|
-
|
|
18
|
-
## π¦ Installation
|
|
19
|
-
|
|
20
|
-
```bash
|
|
21
|
-
npm install roster-server
|
|
22
|
-
```
|
|
23
|
-
|
|
24
|
-
Or with [Bun](https://bun.sh):
|
|
25
|
-
|
|
26
|
-
```bash
|
|
27
|
-
bun add roster-server
|
|
28
|
-
```
|
|
29
|
-
|
|
30
|
-
## π€ AI Skill
|
|
31
|
-
|
|
32
|
-
You can also add RosterServer as a skill for AI agentic development:
|
|
33
|
-
|
|
34
|
-
```bash
|
|
35
|
-
npx skills add https://github.com/clasen/RosterServer --skill roster-server
|
|
7
|
+
```sh
|
|
8
|
+
pnpm add roster-server
|
|
36
9
|
```
|
|
37
10
|
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
### Directory Structure
|
|
41
|
-
|
|
42
|
-
Your project should look something like this:
|
|
43
|
-
|
|
44
|
-
```
|
|
45
|
-
/srv/
|
|
46
|
-
βββ greenlock.d/
|
|
47
|
-
βββ roster/server.js
|
|
48
|
-
βββ www/
|
|
49
|
-
βββ example.com/
|
|
50
|
-
β βββ index.js
|
|
51
|
-
βββ subdomain.example.com/
|
|
52
|
-
β βββ index.js
|
|
53
|
-
βββ static-site.com/ # Static site: no index.js needed
|
|
54
|
-
β βββ index.html
|
|
55
|
-
β βββ css/
|
|
56
|
-
β βββ images/
|
|
57
|
-
βββ other-domain.com/
|
|
58
|
-
β βββ index.js
|
|
59
|
-
βββ *.example.com/ # Wildcard: one handler for all subdomains (api.example.com, app.example.com, etc.)
|
|
60
|
-
βββ index.js
|
|
61
|
-
```
|
|
62
|
-
|
|
63
|
-
Each domain folder can have either:
|
|
64
|
-
- **Node app**: `index.js`, `index.mjs`, or `index.cjs` (exporting a request handler).
|
|
65
|
-
- **Static site**: `index.html` (and any assets). If no JS entry exists, RosterServer serves the folder as static files. Node takes precedence when both exist.
|
|
66
|
-
|
|
67
|
-
### Wildcard DNS (*.example.com)
|
|
11
|
+
`npm install roster-server` and `bun add roster-server` are also supported installation commands. The package exports a CommonJS constructor that can also be imported as an ESM default. Server examples below use `.mjs`; site examples use `.cjs`.
|
|
68
12
|
|
|
69
|
-
|
|
13
|
+
## Quick start: local HTTP
|
|
70
14
|
|
|
71
|
-
|
|
72
|
-
2. **Register (default port)**: `roster.register('*.example.com', handler)` for the default HTTPS port.
|
|
73
|
-
3. **Register (custom port)**: `roster.register('*.example.com:8080', handler)` for a specific port.
|
|
74
|
-
|
|
75
|
-
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).
|
|
76
|
-
|
|
77
|
-
For fully automatic TXT records with Linode DNS, set:
|
|
78
|
-
|
|
79
|
-
```bash
|
|
80
|
-
export ROSTER_DNS_PROVIDER=linode
|
|
81
|
-
export LINODE_API_KEY=...
|
|
82
|
-
```
|
|
83
|
-
|
|
84
|
-
Then Roster creates/removes `_acme-challenge` TXT records automatically via `api.linode.com`.
|
|
85
|
-
If `LINODE_API_KEY` is present, this mode auto-enables by default for wildcard DNS-01.
|
|
86
|
-
|
|
87
|
-
Override with a custom plugin:
|
|
15
|
+
Save as `server.mjs` and run `node server.mjs` from your application directory:
|
|
88
16
|
|
|
89
17
|
```javascript
|
|
18
|
+
import path from 'node:path';
|
|
90
19
|
import Roster from 'roster-server';
|
|
91
20
|
|
|
92
21
|
const roster = new Roster({
|
|
93
|
-
|
|
94
|
-
wwwPath: '
|
|
95
|
-
greenlockStorePath: '/srv/greenlock.d',
|
|
96
|
-
dnsChallenge: { module: 'acme-dns-01-route53', /* provider options */ } // optional override
|
|
22
|
+
local: true,
|
|
23
|
+
wwwPath: path.resolve('www')
|
|
97
24
|
});
|
|
25
|
+
|
|
26
|
+
roster.register('example.com', () => (req, res) => res.end('Hello'));
|
|
27
|
+
await roster.start();
|
|
28
|
+
console.log(roster.getUrl('example.com'));
|
|
98
29
|
```
|
|
99
30
|
|
|
100
|
-
|
|
31
|
+
Local mode binds to `localhost`, skips certificates, and assigns domain ports within `minLocalPort`β`maxLocalPort` (default `4000`β`9999`). Assignments use a domain hash and resolve collisions within the instance; they do not probe for available OS ports. A bind failure rejects startup. Read the assigned URL with `getUrl()` instead of hardcoding a sample port.
|
|
101
32
|
|
|
102
|
-
|
|
33
|
+
For production HTTPS, use a real contact email, public DNS pointing to the host, and explicit storage paths:
|
|
103
34
|
|
|
104
35
|
```javascript
|
|
105
|
-
// /srv/roster/server.js
|
|
106
36
|
import Roster from 'roster-server';
|
|
107
37
|
|
|
108
|
-
const
|
|
38
|
+
const roster = new Roster({
|
|
109
39
|
email: 'admin@example.com',
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
};
|
|
40
|
+
wwwPath: '/srv/www',
|
|
41
|
+
greenlockStorePath: '/srv/greenlock.d'
|
|
42
|
+
});
|
|
113
43
|
|
|
114
|
-
|
|
115
|
-
server.start();
|
|
44
|
+
await roster.start();
|
|
116
45
|
```
|
|
117
46
|
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
RosterServer includes an optional request plugin that rejects common PHP, WordPress, repository, and sensitive-file probes before they reach a site handler. Suspicious paths always receive a `404`; after the configured number of strikes, every request from that client is rejected until the ban expires.
|
|
47
|
+
Standalone HTTPS opens port `80` for ACME challenges in addition to the configured HTTPS ports. Port `80` cannot be a production site port. Use `staging: true` when testing certificate issuance; staging certificates are not publicly trusted.
|
|
121
48
|
|
|
122
|
-
|
|
123
|
-
import Roster from 'roster-server';
|
|
124
|
-
import { createScannerBlocker } from 'roster-server/plugins/scanner-blocker.js';
|
|
49
|
+
## Sites and routing
|
|
125
50
|
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
}));
|
|
133
|
-
|
|
134
|
-
roster.start();
|
|
51
|
+
```text
|
|
52
|
+
www/
|
|
53
|
+
βββ example.com/index.cjs
|
|
54
|
+
βββ api.example.com/index.mjs
|
|
55
|
+
βββ static.example.com/index.html
|
|
56
|
+
βββ *.example.com/index.cjs
|
|
135
57
|
```
|
|
136
58
|
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
The in-memory strike and ban state is bounded by `maxTrackedClients`, belongs to one RosterServer process, and is cleared on restart. Use the optional `onBlock(event)` callback to feed a shared firewall or Fail2ban when bans must persist or span multiple workers. Routes such as `/atom` and `/articles/config` are not classified as scanner probes.
|
|
59
|
+
For each directory, discovery checks `index.js`, `index.mjs`, then `index.cjs`. If none exists, `index.html` enables static serving. `filename` changes the script basename; the static entry remains `index.html`. Match `.js` exports to the site's package module type, or use `.cjs`/`.mjs` explicitly.
|
|
140
60
|
|
|
141
|
-
|
|
61
|
+
Discovery happens during `init()`. Register manual sites before initialization. Discovered sites use HTTPS port **443**, even when the instance's `port` is different; a discovered registration replaces a manual registration for the same domain and port. Loading new sites requires a new instance or process restart.
|
|
142
62
|
|
|
143
|
-
|
|
63
|
+
### Site factory contract
|
|
144
64
|
|
|
145
|
-
|
|
146
|
-
- **Static site**: Put `index.html` and your assets (CSS, JS, images). RosterServer will serve files from that folder. `GET /` serves `index.html`; other paths serve the file if it exists, or 404. Path traversal is blocked. If both an index script and `index.html` exist, the script is used.
|
|
65
|
+
Export a **synchronous factory** `(virtualServer) => requestHandler`. The returned `(req, res)` handler may be asynchronous. Return an Express app directly; do not call `app.listen()` inside a factory.
|
|
147
66
|
|
|
148
|
-
|
|
67
|
+
Basic site, saved as `www/example.com/index.cjs`:
|
|
149
68
|
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
export default (httpsServer) => {
|
|
155
|
-
return (req, res) => {
|
|
156
|
-
res.writeHead(200, { 'Content-Type': 'text/plain; charset=utf-8' });
|
|
157
|
-
res.end('"Loco de pensar, queriendo entrar en razΓ³n, y el corazΓ³n tiene razones que la propia razΓ³n nunca entenderΓ‘."');
|
|
158
|
-
};
|
|
69
|
+
```javascript
|
|
70
|
+
module.exports = () => async (req, res) => {
|
|
71
|
+
res.writeHead(200, { 'Content-Type': 'text/plain; charset=utf-8' });
|
|
72
|
+
res.end('Hello');
|
|
159
73
|
};
|
|
160
74
|
```
|
|
161
75
|
|
|
162
|
-
|
|
163
|
-
```javascript:demo/www/express.example.com/index.js
|
|
164
|
-
import express from 'express';
|
|
76
|
+
Express site, with Express installed in the consuming application:
|
|
165
77
|
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
app.get('/', (req, res) => {
|
|
169
|
-
res.setHeader('Content-Type', 'text/plain; charset=utf-8');
|
|
170
|
-
res.send('"Loco de pensar, queriendo entrar en razΓ³n, y el corazΓ³n tiene razones que la propia razΓ³n nunca entenderΓ‘."');
|
|
171
|
-
});
|
|
78
|
+
```javascript
|
|
79
|
+
const express = require('express');
|
|
172
80
|
|
|
81
|
+
module.exports = () => {
|
|
82
|
+
const app = express();
|
|
83
|
+
app.get('/', (req, res) => res.json({ ok: true }));
|
|
173
84
|
return app;
|
|
174
|
-
}
|
|
85
|
+
};
|
|
175
86
|
```
|
|
176
87
|
|
|
177
|
-
|
|
178
|
-
```javascript:demo/www/sio.example.com/index.js
|
|
179
|
-
import { Server } from 'socket.io';
|
|
88
|
+
The virtual server supports request/upgrade listener integration; it is not a separately listening TCP server or a process isolation boundary. Request listeners own their requests, including asynchronous responses. Roster does not invoke the returned handler just because a listener has not finished. Wrappers can capture the site's HTTP fallback through `server.listeners('request')`.
|
|
180
89
|
|
|
181
|
-
|
|
182
|
-
const io = new Server(httpsServer);
|
|
90
|
+
Thrown or rejected request-handler errors produce a `500`, or destroy the response if headers were already sent.
|
|
183
91
|
|
|
184
|
-
|
|
185
|
-
console.log('A user connected');
|
|
92
|
+
### Socket.IO and resource cleanup
|
|
186
93
|
|
|
187
|
-
|
|
188
|
-
console.log('Message received:', msg);
|
|
189
|
-
io.emit('chat:message', msg);
|
|
190
|
-
});
|
|
94
|
+
With Socket.IO installed in the consuming application:
|
|
191
95
|
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
96
|
+
```javascript
|
|
97
|
+
const { Server } = require('socket.io');
|
|
98
|
+
|
|
99
|
+
module.exports = (server) => {
|
|
100
|
+
const io = new Server(server);
|
|
101
|
+
io.on('connection', socket => {
|
|
102
|
+
socket.on('chat:message', message => io.emit('chat:message', message));
|
|
195
103
|
});
|
|
104
|
+
server.onClose(() => new Promise(resolve => io.close(resolve)));
|
|
196
105
|
|
|
197
|
-
return (req, res) =>
|
|
198
|
-
if (req.url && req.url.startsWith(io.opts.path)) return;
|
|
199
|
-
res.writeHead(200);
|
|
200
|
-
res.end('Socket.IO server running');
|
|
201
|
-
};
|
|
106
|
+
return (req, res) => res.end('Socket.IO site');
|
|
202
107
|
};
|
|
203
108
|
```
|
|
204
109
|
|
|
205
|
-
|
|
206
|
-
```javascript:demo/www/manual.js
|
|
207
|
-
roster.register('example.com', (httpsServer) => {
|
|
208
|
-
return (req, res) => {
|
|
209
|
-
res.writeHead(200, { 'Content-Type': 'text/plain; charset=utf-8' });
|
|
210
|
-
res.end('"Loco de pensar, queriendo entrar en razΓ³n, y el corazΓ³n tiene razones que la propia razΓ³n nunca entenderΓ‘."');
|
|
211
|
-
};
|
|
212
|
-
});
|
|
213
|
-
```
|
|
110
|
+
Socket.IO handles its own endpoint and delegates other requests to the returned handler. No manual path exclusion is required. Use the public `io.path()` method if application code needs its configured path.
|
|
214
111
|
|
|
215
|
-
|
|
216
|
-
```javascript:demo/www/manual.js
|
|
217
|
-
roster.register('example.com:8080', (httpsServer) => {
|
|
218
|
-
return (req, res) => {
|
|
219
|
-
res.writeHead(200, { 'Content-Type': 'text/plain; charset=utf-8' });
|
|
220
|
-
res.end('"Mad with thought, striving to embrace reason, yet the heart holds reasons that reason itself shall never comprehend."');
|
|
221
|
-
};
|
|
222
|
-
});
|
|
223
|
-
```
|
|
112
|
+
### Manual registration and custom ports
|
|
224
113
|
|
|
225
|
-
|
|
114
|
+
After creating `roster`, register factories before `init()` or `start()`:
|
|
226
115
|
|
|
227
|
-
```
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
Or with Bun:
|
|
233
|
-
|
|
234
|
-
```bash
|
|
235
|
-
bun server.js
|
|
116
|
+
```javascript
|
|
117
|
+
const site = () => (req, res) => res.end('API');
|
|
118
|
+
roster.register('api.example.com', site);
|
|
119
|
+
roster.register('api.example.com:8443', site);
|
|
120
|
+
roster.register('*.example.com:8443', site);
|
|
236
121
|
```
|
|
237
122
|
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
## π€― But Wait, There's More!
|
|
241
|
-
|
|
242
|
-
### Static Sites (index.html)
|
|
243
|
-
|
|
244
|
-
Domains under `www` that have no `index.js`/`index.mjs`/`index.cjs` but do have `index.html` are served as static sites. The logic lives in `lib/static-site-handler.js` and `lib/resolve-site-app.js`:
|
|
245
|
-
|
|
246
|
-
- **`GET /`** and **`GET /index.html`** serve `index.html`.
|
|
247
|
-
- Any other path serves the file under the domain folder if it exists; otherwise **404** (strict, no SPA fallback).
|
|
248
|
-
- Path traversal (e.g. `/../`) is rejected with **403**.
|
|
249
|
-
- Content-Type is set from extension (html, css, js, images, fonts, etc.).
|
|
250
|
-
|
|
251
|
-
No Express or extra dependenciesβplain Node. At startup youβll see `(β) Loaded site: https://example.com (static)` for these domains.
|
|
252
|
-
|
|
253
|
-
### Automatic SSL Certificate Management
|
|
254
|
-
|
|
255
|
-
RosterServer uses [greenlock-express](https://www.npmjs.com/package/greenlock-express) to automatically obtain and renew SSL certificates from Let's Encrypt. No need to manually manage certificates ever again. Unless you enjoy that sort of thing. π§
|
|
256
|
-
|
|
257
|
-
### Redirects from `www`
|
|
123
|
+
A registration without an explicit port uses the instance's `port`. Exact hosts take precedence over wildcard matches. `*.example.com` does not match the apex `example.com`; register the apex separately if needed. Wildcard certificates require [DNS-01 configuration](#certificates-and-dns).
|
|
258
124
|
|
|
259
|
-
|
|
125
|
+
Requests to `www` hosts redirect to the host without `www`, preserving the explicit port in the Host header. `getUrl(domain)` normalizes `www` and supports wildcard lookups. In local mode it returns `localhost` or a subdomain such as `api.localhost`, with the assigned port. In production it uses the instance's default port; it is not a per-port URL lookup for registrations made only on non-default ports, which can return `null`.
|
|
260
126
|
|
|
261
|
-
###
|
|
127
|
+
### Static sites
|
|
262
128
|
|
|
263
|
-
|
|
129
|
+
A directory with `index.html` and no entry script needs no JavaScript factory:
|
|
264
130
|
|
|
265
|
-
|
|
131
|
+
- `GET` streams the requested file; `HEAD` reads metadata without reading the body.
|
|
132
|
+
- `/` and directory requests serve the corresponding `index.html`.
|
|
133
|
+
- Missing files return `404`; there is no SPA fallback. Unsupported methods return `405`.
|
|
134
|
+
- Paths that resolve outside the site root are rejected. Content type is inferred from the file extension.
|
|
266
135
|
|
|
267
|
-
|
|
136
|
+
Filesystem operations are asynchronous, and disconnected downloads close their file handles.
|
|
268
137
|
|
|
269
|
-
|
|
270
|
-
- `wwwPath` (string): Path to your `www` directory containing your sites.
|
|
271
|
-
- `greenlockStorePath` (string): Directory for Greenlock configuration.
|
|
272
|
-
- `dnsChallenge` (object|false): Optional override for wildcard DNS-01 challenge config. Default is `acme-dns-01-cli` wrapper with `propagationDelay: 120000`, `autoContinue: false`, and `dryRunDelay: 120000`. Manual mode still works, but you can enable automatic Linode DNS API mode by setting `ROSTER_DNS_PROVIDER=linode` and `LINODE_API_KEY`. In automatic mode, Roster creates/removes TXT records itself and still polls public resolvers every 15s before continuing. Set `false` to disable DNS challenge. You can pass `{ module: '...', propagationDelay: 180000 }` to tune DNS wait time (ms). 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`). When wildcard sites are present, Roster creates a separate wildcard certificate (`*.example.com`) that uses `dns-01`, while apex/www stay on the regular certificate flow (typically `http-01`), reducing manual TXT records.
|
|
273
|
-
- `staging` (boolean): Set to `true` to use Let's Encrypt's staging environment (for testing).
|
|
274
|
-
- `autoCertificates` (boolean): Enables automatic certificate issuance and renewal in production lifecycle. **Default: `true`**. Set to `false` only if certificates are managed externally.
|
|
275
|
-
- `certificateRenewIntervalMs` (number): Renewal check interval when `autoCertificates` is enabled (minimum 60s, default 12h).
|
|
276
|
-
- `local` (boolean): Set to `true` to run in local development mode.
|
|
277
|
-
- `minLocalPort` (number): Minimum port for local mode (default: 4000).
|
|
278
|
-
- `maxLocalPort` (number): Maximum port for local mode (default: 9999).
|
|
138
|
+
## Startup and shutdown
|
|
279
139
|
|
|
280
|
-
|
|
140
|
+
Concurrent `init()` calls share initialization, so each factory runs once per registered domain/port. Concurrent `start()` calls share startup. In standalone and local modes, `await roster.start()` waits for listeners to bind. Initialization or bind failures reject and clean up resources already initialized by the instance.
|
|
281
141
|
|
|
282
|
-
|
|
142
|
+
Register `virtualServer.onClose(fn)` in a factory for timers, databases, queues, or other site-owned resources. Hooks may return Promises, run once, and run concurrently after active HTTP requests drain. Put dependent cleanup operations in the same async hook. This is separate from the virtual server's `close` event, which is emitted when shutdown begins so integrations can release long polling requests.
|
|
283
143
|
|
|
284
|
-
|
|
144
|
+
`await roster.close()`:
|
|
285
145
|
|
|
286
|
-
|
|
146
|
+
1. Stops accepting work and cancels Roster's renewal/retry timers.
|
|
147
|
+
2. Closes listeners opened by `start()`, including ACME HTTP, and routed WebSockets; drains active HTTP requests and waits for pending certificate work.
|
|
148
|
+
3. Runs site cleanup hooks. Individual failures do not prevent other hooks from running; failures are reported in an `AggregateError`.
|
|
287
149
|
|
|
288
|
-
|
|
289
|
-
import Roster from 'roster-server';
|
|
290
|
-
|
|
291
|
-
const server = new Roster({
|
|
292
|
-
wwwPath: '/srv/www',
|
|
293
|
-
local: true, // Enable local development mode
|
|
294
|
-
minLocalPort: 4000, // Optional: minimum port (default: 4000)
|
|
295
|
-
maxLocalPort: 9999 // Optional: maximum port (default: 9999)
|
|
296
|
-
});
|
|
297
|
-
server.start();
|
|
298
|
-
```
|
|
299
|
-
|
|
300
|
-
### Port Assignment
|
|
301
|
-
|
|
302
|
-
In local mode, domains are automatically assigned ports based on a CRC32 hash of the domain name (default range 4000-9999, configurable via `minLocalPort` and `maxLocalPort`):
|
|
303
|
-
|
|
304
|
-
- `example.com` β `http://localhost:9465`
|
|
305
|
-
- `api.example.com` β `http://localhost:9388`
|
|
306
|
-
- And so on...
|
|
307
|
-
|
|
308
|
-
You can customize the port range:
|
|
309
|
-
|
|
310
|
-
```javascript
|
|
311
|
-
import Roster from 'roster-server';
|
|
312
|
-
|
|
313
|
-
const roster = new Roster({
|
|
314
|
-
local: true,
|
|
315
|
-
minLocalPort: 5000, // Start from port 5000
|
|
316
|
-
maxLocalPort: 6000 // Up to port 6000
|
|
317
|
-
});
|
|
318
|
-
```
|
|
319
|
-
|
|
320
|
-
### Getting URLs
|
|
150
|
+
`closeTimeoutMs` (default `30000`) bounds the whole operation. At the deadline Roster destroys remaining owned connections and active routed responses, attempts cleanup hooks, and rejects with a timeout. It cannot forcibly interrupt an application Promise or an ACME operation already in progress.
|
|
321
151
|
|
|
322
|
-
|
|
152
|
+
Closing is **idempotent and terminal**: repeated calls return the same Promise. Create a new instance after closure or failed initialization/startup.
|
|
323
153
|
|
|
324
|
-
|
|
154
|
+
For automatic shutdown on `SIGINT` or `SIGTERM`, explicitly enable `handleSignals`:
|
|
325
155
|
|
|
326
156
|
```javascript
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
const roster = new Roster({ local: true });
|
|
330
|
-
roster.register('example.com', handler);
|
|
331
|
-
|
|
157
|
+
const roster = new Roster({ handleSignals: true });
|
|
332
158
|
await roster.start();
|
|
333
|
-
|
|
334
|
-
// Get the URL - automatically adapts to environment
|
|
335
|
-
const url = roster.getUrl('example.com');
|
|
336
|
-
console.log(url);
|
|
337
|
-
// Local mode: http://localhost:9465
|
|
338
|
-
// Local subdomain: http://api.localhost:9465
|
|
339
|
-
// Production mode: https://example.com
|
|
340
159
|
```
|
|
341
160
|
|
|
342
|
-
|
|
343
|
-
- Returns the correct URL based on your environment (`local: true/false`)
|
|
344
|
-
- In **local mode**: Returns `http://localhost:{port}` for apex domains and `http://{subdomain}.localhost:{port}` for subdomains
|
|
345
|
-
- In **production mode**: Returns `https://{domain}` (or with custom port if configured)
|
|
346
|
-
- Handles `www.` prefix automatically (returns same URL)
|
|
347
|
-
- Returns `null` for domains that aren't registered
|
|
161
|
+
The option defaults to `false`. Listeners are installed once when `init()` or `start()` begins and removed when closure completes, including failed startup or cleanup. Signals call `roster.close()`; repeated signals during closure do not repeat cleanup. Existing application signal listeners remain installed. Shutdown failures are logged and set `process.exitCode = 1`.
|
|
348
162
|
|
|
349
|
-
|
|
163
|
+
Roster does not force process termination or close caller-owned servers. The process exits naturally when its remaining work finishes; other resources or cluster workers still require application coordination. If the application already coordinates shutdown, leave `handleSignals` disabled and connect Roster to that flow:
|
|
350
164
|
|
|
351
165
|
```javascript
|
|
352
|
-
|
|
166
|
+
async function shutdown() {
|
|
167
|
+
try {
|
|
168
|
+
await roster.close();
|
|
169
|
+
} catch (error) {
|
|
170
|
+
console.error(error);
|
|
171
|
+
process.exitCode = 1;
|
|
172
|
+
}
|
|
173
|
+
}
|
|
353
174
|
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
localRoster.register('example.com', handler);
|
|
357
|
-
localRoster.register('api.example.com', handler);
|
|
358
|
-
await localRoster.start();
|
|
359
|
-
console.log(localRoster.getUrl('example.com'));
|
|
360
|
-
// β http://localhost:9465
|
|
361
|
-
console.log(localRoster.getUrl('api.example.com'));
|
|
362
|
-
// β http://api.localhost:7342
|
|
363
|
-
|
|
364
|
-
// Production
|
|
365
|
-
const prodRoster = new Roster({ local: false });
|
|
366
|
-
prodRoster.register('example.com', handler);
|
|
367
|
-
await prodRoster.start();
|
|
368
|
-
console.log(prodRoster.getUrl('example.com'));
|
|
369
|
-
// β https://example.com
|
|
370
|
-
|
|
371
|
-
// Production with custom port
|
|
372
|
-
const customRoster = new Roster({ local: false, port: 8443 });
|
|
373
|
-
customRoster.register('api.example.com', handler);
|
|
374
|
-
await customRoster.start();
|
|
375
|
-
console.log(customRoster.getUrl('api.example.com'));
|
|
376
|
-
// β https://api.example.com:8443
|
|
175
|
+
process.once('SIGTERM', shutdown);
|
|
176
|
+
process.once('SIGINT', shutdown);
|
|
377
177
|
```
|
|
378
178
|
|
|
379
|
-
##
|
|
380
|
-
|
|
381
|
-
RosterServer can coexist with external cluster managers (sticky-session libraries, PM2 cluster, custom master/worker architectures) that already own the TCP socket and distribute connections. Instead of letting Roster create and bind servers, you initialize routing separately and wire it into your own server.
|
|
382
|
-
|
|
383
|
-
### How It Works
|
|
179
|
+
## External servers and workers
|
|
384
180
|
|
|
385
|
-
`
|
|
181
|
+
Use `init()` when your application or cluster manager owns the listener. It loads sites and invokes factories without binding ports. In production, it also generates certificate configuration and, unless `autoCertificates: false`, creates the certificate runtime and renewal timer.
|
|
386
182
|
|
|
387
|
-
|
|
183
|
+
A serving-only worker with existing certificates:
|
|
388
184
|
|
|
389
185
|
```javascript
|
|
390
|
-
import https from 'https';
|
|
391
186
|
import Roster from 'roster-server';
|
|
392
187
|
|
|
393
188
|
const roster = new Roster({
|
|
394
189
|
email: 'admin@example.com',
|
|
395
190
|
wwwPath: '/srv/www',
|
|
396
|
-
greenlockStorePath: '/srv/greenlock.d'
|
|
397
|
-
});
|
|
398
|
-
|
|
399
|
-
await roster.init();
|
|
400
|
-
|
|
401
|
-
// Create your own HTTPS server with Roster's SNI + routing
|
|
402
|
-
const server = https.createServer({ SNICallback: roster.sniCallback() });
|
|
403
|
-
roster.attach(server);
|
|
404
|
-
|
|
405
|
-
// Master passes connections via IPC β worker never calls listen()
|
|
406
|
-
process.on('message', (msg, connection) => {
|
|
407
|
-
if (msg === 'sticky-session:connection') {
|
|
408
|
-
server.emit('connection', connection);
|
|
409
|
-
}
|
|
410
|
-
});
|
|
411
|
-
```
|
|
412
|
-
|
|
413
|
-
### Production Pattern: Single Certificate Manager + Workers
|
|
414
|
-
|
|
415
|
-
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.
|
|
416
|
-
|
|
417
|
-
```javascript
|
|
418
|
-
// primary
|
|
419
|
-
const certManager = new Roster({
|
|
420
|
-
email: 'admin@example.com',
|
|
421
|
-
greenlockStorePath: '/srv/greenlock.d',
|
|
422
|
-
wwwPath: '/srv/www'
|
|
423
|
-
});
|
|
424
|
-
certManager.register('example.com', () => (req, res) => res.end('manager'));
|
|
425
|
-
await certManager.start(); // enables ACME challenge lifecycle
|
|
426
|
-
await certManager.ensureCertificate('example.com');
|
|
427
|
-
|
|
428
|
-
// worker
|
|
429
|
-
const workerRoster = new Roster({
|
|
430
|
-
email: 'admin@example.com',
|
|
431
191
|
greenlockStorePath: '/srv/greenlock.d',
|
|
432
|
-
wwwPath: '/srv/www',
|
|
433
192
|
autoCertificates: false
|
|
434
193
|
});
|
|
435
|
-
workerRoster.register('example.com', () => (req, res) => res.end('worker'));
|
|
436
|
-
await workerRoster.init();
|
|
437
|
-
const server = await workerRoster.createServingHttpsServer({ servername: 'example.com' });
|
|
438
|
-
server.listen(4336);
|
|
439
|
-
```
|
|
440
|
-
|
|
441
|
-
Reference implementation: `demo/https-cluster-configurable.js`.
|
|
442
|
-
|
|
443
|
-
### API Reference
|
|
444
194
|
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
#### `roster.requestHandler(port?)` β `(req, res) => void`
|
|
195
|
+
await roster.init();
|
|
196
|
+
const server = await roster.createServingHttpsServer({ servername: 'example.com' });
|
|
197
|
+
server.listen(8443);
|
|
198
|
+
```
|
|
450
199
|
|
|
451
|
-
|
|
200
|
+
The caller owns this server's listen/error/close lifecycle. To wire an existing HTTP(S) server instead, call `roster.attach(server, { port: 443 })`. The `port` selects a routing table; it does not change the server's listening port. Attach request handling only where Roster should own dispatch. Repeating the same attachment is a no-op; attaching that server to a different routing port throws.
|
|
452
201
|
|
|
453
|
-
|
|
202
|
+
During shutdown, `roster.close()` drains its routed requests, closes its routed upgraded sockets, and removes only listeners added by `attach()`. It **does not close the external server**, including servers returned by the HTTPS helpers. Coordinate both closures in the owning application. Manually wired `requestHandler()`/`upgradeHandler()` listeners must also be removed by their owner.
|
|
454
203
|
|
|
455
|
-
|
|
204
|
+
For multiple workers, keep certificate issuance in one manager process and use `autoCertificates: false` with `init()` in serving workers. The manager can use `start()` to provide ACME HTTP and `ensureCertificate()` before starting workers. Keep domain/certificate configuration consistent: `init()` still writes configuration even in serving-only mode. `start()` retains the standalone ACME lifecycle even if `autoCertificates` is false.
|
|
456
205
|
|
|
457
|
-
|
|
206
|
+
The legacy `cluster: true` launcher delegates process management to Greenlock. `close()` applies to the current Roster instance; it does not stop worker processes. External cluster managers should coordinate shutdown in each worker.
|
|
458
207
|
|
|
459
|
-
|
|
208
|
+
## Certificates and DNS
|
|
460
209
|
|
|
461
|
-
|
|
210
|
+
The default DNS-01 integration is a wrapper around `acme-dns-01-cli`, with `propagationDelay: 120000`, `autoContinue: false`, and `dryRunDelay` matching propagation delay. Without an API provider, DNS challenges require manual TXT records. `dnsChallenge: false` disables this integration, not HTTPS itself.
|
|
462
211
|
|
|
463
|
-
|
|
212
|
+
Wildcard sites normally receive a separate wildcard certificate using DNS-01; apex/www use their regular certificate flow. `combineWildcardCerts: true` puts apex, www, and wildcard names on the primary certificate and requires DNS-01. `disableWildcard: true` ignores wildcard site registrations and discovery.
|
|
464
213
|
|
|
465
|
-
|
|
214
|
+
For Linode, supply `LINODE_API_KEY` through the application's secret configuration and set `ROSTER_DNS_PROVIDER=linode`. A configured Linode key also selects that provider when no provider is specified. The wrapper creates/removes TXT records and checks DNS propagation. API failures can fall back to manual mode by default; for unattended operation that must fail instead, configure:
|
|
466
215
|
|
|
467
|
-
|
|
216
|
+
```javascript
|
|
217
|
+
const dnsChallenge = {
|
|
218
|
+
module: 'acme-dns-01-cli',
|
|
219
|
+
provider: 'linode',
|
|
220
|
+
dnsApiFallbackToManual: false
|
|
221
|
+
};
|
|
222
|
+
```
|
|
468
223
|
|
|
469
|
-
|
|
224
|
+
Pass this as the constructor's `dnsChallenge` option. To use another provider, install its Greenlock DNS plugin in the consuming application and supply its module name and provider options. Configure DNS timing on that object with `propagationDelay`, `dryRunDelay`, `dnsPollIntervalMs`, and `dnsPollTimeoutMs`; the wrapper's polling interval defaults to `15000` ms.
|
|
225
|
+
|
|
226
|
+
Roster's file-based SNI resolvers cache TLS contexts and detect changed certificate files through metadata, including updates from another process. Concurrent requests for the same certificate name share loads and checks. Standalone startup reuses the initialized certificate runtime and Roster's cancellable renewal loop. Loading an existing certificate does not itself force renewal.
|
|
227
|
+
|
|
228
|
+
## Configuration reference
|
|
229
|
+
|
|
230
|
+
Pass operational settings through the constructor, using the consuming application's configuration source. Prefer absolute `wwwPath` and `greenlockStorePath`. `basePath` supplies their defaults; it does not rebase explicitly supplied relative paths.
|
|
231
|
+
|
|
232
|
+
| Option | Default | Meaning |
|
|
233
|
+
| --- | --- | --- |
|
|
234
|
+
| `email` | `admin@example.com` | Replace with the real certificate contact. |
|
|
235
|
+
| `basePath` | Derived from package location | Default parent for `www` and `greenlock.d`; set explicitly when relying on it. |
|
|
236
|
+
| `wwwPath` | `basePath/www` | Discovered sites. |
|
|
237
|
+
| `greenlockStorePath` | `basePath/greenlock.d` | Certificate files and generated configuration. |
|
|
238
|
+
| `filename` | `index` | Script entry basename, without extension. |
|
|
239
|
+
| `local` | `false` | Local HTTP mode with assigned ports and no certificates. |
|
|
240
|
+
| `port` | `443` | Default port for manual registrations and routing helpers. |
|
|
241
|
+
| `hostname` | `::` | Production bind address; local mode uses `localhost`. |
|
|
242
|
+
| `minLocalPort`, `maxLocalPort` | `4000`, `9999` | Inclusive local assignment range. |
|
|
243
|
+
| `staging` | `false` | ACME staging environment. |
|
|
244
|
+
| `autoCertificates` | `true` | Certificate runtime during `init()`; disable for serving-only workers. |
|
|
245
|
+
| `certificateRenewIntervalMs` | `43200000` (12h) | Roster renewal check interval; minimum `60000`. |
|
|
246
|
+
| `closeTimeoutMs` | `30000` | Positive finite total shutdown deadline. |
|
|
247
|
+
| `handleSignals` | `false` | Close this instance on `SIGINT`/`SIGTERM`; does not force process exit. |
|
|
248
|
+
| `tlsMinVersion`, `tlsMaxVersion` | `TLSv1.2`, `TLSv1.3` | Protocol limits for created HTTPS servers. |
|
|
249
|
+
| `skipLocalCheck` | `true` | Skips Greenlock dry-run/local challenge checks. |
|
|
250
|
+
| `dnsChallenge` | CLI wrapper | DNS-01 options or `false`; see [certificates](#certificates-and-dns). |
|
|
251
|
+
| `disableWildcard`, `combineWildcardCerts` | `false`, `false` | Wildcard registration and certificate behavior. |
|
|
252
|
+
| `cluster` | `false` | Legacy Greenlock-managed cluster launcher. |
|
|
253
|
+
|
|
254
|
+
## API reference
|
|
255
|
+
|
|
256
|
+
| Method | Contract |
|
|
257
|
+
| --- | --- |
|
|
258
|
+
| `register(domain, factory)` | Register before initialization; accepts `domain:port` and wildcard domains. Returns `Roster`. |
|
|
259
|
+
| `use(plugin)` | Add a synchronous request plugin. Returns `Roster`. |
|
|
260
|
+
| `init()` | `Promise<Roster>`; prepare routing and factories without listening. |
|
|
261
|
+
| `start()` | Start standalone/local listeners; await readiness. |
|
|
262
|
+
| `close()` | `Promise<void>`; drain and clean up the instance. |
|
|
263
|
+
| `getUrl(domain)` | URL or `null`; local assignment is available after startup. See [routing](#manual-registration-and-custom-ports) for port limitations. |
|
|
264
|
+
| `requestHandler(port?)` | HTTP dispatcher for the selected routing port. Requires `init()`. |
|
|
265
|
+
| `upgradeHandler(port?)` | WebSocket upgrade dispatcher. Requires `init()`. |
|
|
266
|
+
| `attach(server, { port }?)` | Add both dispatchers to a caller-owned server. Requires `init()`; returns `Roster`. |
|
|
267
|
+
| `sniCallback()` | TLS SNI callback. Requires production-mode `init()`. |
|
|
268
|
+
| `ensureCertificate(servername)` | `Promise<{ key, cert }>`; load existing PEMs or issue missing ones when enabled. Requires production-mode `init()`. |
|
|
269
|
+
| `loadCertificate(servername)` | Synchronously load `{ key, cert }` without issuance. Requires production-mode `init()`. |
|
|
270
|
+
| `createManagedHttpsServer({ servername, port?, ensureCertificate?, tlsOptions? })` | `Promise<https.Server>` with certificates and dispatchers; does not listen. Certificate assurance defaults to `true`. Requires production-mode `init()`. |
|
|
271
|
+
| `createServingHttpsServer({ servername, port?, tlsOptions? })` | Same helper with `ensureCertificate: false`; set `autoCertificates: false` for a serving-only instance. |
|
|
272
|
+
| `virtualServer.onClose(fn)` | Register a cleanup hook; returns the virtual server. See [shutdown](#startup-and-shutdown). |
|
|
273
|
+
|
|
274
|
+
## Request plugins
|
|
275
|
+
|
|
276
|
+
Plugins run before redirects and site dispatch. They receive `(req, res, { host, domain })`; return `true` after handling the response, or `false`/`undefined` to continue. Plugins must be synchronous. Thrown errors or Promise returns produce a `500` response.
|
|
277
|
+
|
|
278
|
+
Enable the optional scanner blocker before startup:
|
|
470
279
|
|
|
471
|
-
|
|
280
|
+
```javascript
|
|
281
|
+
import { createScannerBlocker } from 'roster-server/plugins/scanner-blocker.js';
|
|
472
282
|
|
|
473
|
-
|
|
283
|
+
roster.use(createScannerBlocker());
|
|
284
|
+
```
|
|
474
285
|
|
|
475
|
-
|
|
286
|
+
The blocker rejects common PHP, WordPress, repository, and sensitive-file probes with `404`. Defaults: a `60000` ms strike window, `3` strikes, a `900000` ms ban, `10000` tracked clients, and `trustProxy: false`. Configure `windowMs`, `strikeThreshold`, `banDurationMs`, and `maxTrackedClients` on the plugin. State is bounded, in memory, and per process.
|
|
476
287
|
|
|
477
|
-
|
|
288
|
+
Enable `trustProxy` only behind a trusted proxy that overwrites `X-Forwarded-For`. The optional synchronous `onBlock(event)` callback can integrate the application's logging or enforcement; it does not itself provide shared or persistent bans.
|
|
478
289
|
|
|
479
|
-
|
|
290
|
+
## Troubleshooting and runtime limits
|
|
480
291
|
|
|
481
|
-
|
|
292
|
+
| Symptom | Check |
|
|
293
|
+
| --- | --- |
|
|
294
|
+
| Startup rejects with a port error | Free the conflicting port or change the configured range/port, then create a new instance. Standalone HTTPS also needs port 80. |
|
|
295
|
+
| A site returns `404` | Check the domain, routing port, entry filename, module type, and factory export. Discovered sites stay on 443. |
|
|
296
|
+
| A site import fails with a relative path | Pass an absolute `wwwPath`; explicit paths are not rebased onto `basePath`. |
|
|
297
|
+
| Static deep links return `404` | There is no SPA fallback; use a site handler when that behavior is required. |
|
|
298
|
+
| Socket.IO falls through or double-responds | Attach it to the supplied virtual server, return the ordinary HTTP handler, and let Socket.IO own its endpoint. |
|
|
299
|
+
| Certificates are missing in a worker | Ensure the manager has issued them into the same store before worker startup. `init()` alone does not bind ACME HTTP. |
|
|
300
|
+
| Shutdown times out | Inspect the aggregated errors and site hooks, active requests, and pending certificate operations. |
|
|
482
301
|
|
|
483
|
-
|
|
302
|
+
Local integration checks cover Node HTTP/TLS and Socket.IO polling/WebSocket shutdown on Node and Bun. In **Bun 1.3.4**, the tested `node:https` serving helper did not invoke `SNICallback`, and changing certificate files did not replace the served default certificate. This also occurred with the previous synchronous resolver; do not assume Node's SNI reload behavior on that runtime.
|
|
484
303
|
|
|
485
|
-
|
|
304
|
+
## Agent skill and development
|
|
486
305
|
|
|
487
|
-
|
|
306
|
+
The [RosterServer skill](skills/roster-server/SKILL.md) guides application integration:
|
|
488
307
|
|
|
489
|
-
```
|
|
490
|
-
|
|
491
|
-
await roster.start(); // full standalone mode, no changes needed
|
|
308
|
+
```sh
|
|
309
|
+
npx skills add https://github.com/clasen/RosterServer --skill roster-server
|
|
492
310
|
```
|
|
493
311
|
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
You might be thinking, "But setting up HTTPS and virtual hosts is supposed to be complicated and time-consuming!" Well, not anymore. With RosterServer, you can get back to writing code that matters, like defending Earth from alien invaders! πΎπΎπΎ
|
|
312
|
+
Contributors can run `pnpm test` for the Node test suite. Examples live in [demo](demo); they are separate from the published API reference above.
|
|
497
313
|
|
|
498
|
-
|
|
499
|
-
## π€ Contributing
|
|
500
|
-
|
|
501
|
-
Feel free to submit issues or pull requests. Or don't. I'm not your boss. π
|
|
502
|
-
|
|
503
|
-
If you find any issues or have suggestions for improvement, please open an issue or submit a pull request on the [GitHub repository](https://github.com/clasen/RosterServer).
|
|
504
|
-
|
|
505
|
-
## π Acknowledgments
|
|
506
|
-
|
|
507
|
-
- [Node.js](https://nodejs.org/) - JavaScript runtime
|
|
508
|
-
- [Greenlock](https://git.coolaj86.com/coolaj86/greenlock.js) - Fully-featured ACME client
|
|
509
|
-
|
|
510
|
-
## π License
|
|
314
|
+
## License
|
|
511
315
|
|
|
512
316
|
The MIT License (MIT)
|
|
513
317
|
|
|
@@ -518,7 +322,3 @@ Permission is hereby granted, free of charge, to any person obtaining a copy of
|
|
|
518
322
|
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
|
519
323
|
|
|
520
324
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
|
521
|
-
|
|
522
|
-
---
|
|
523
|
-
|
|
524
|
-
Happy hosting! π
|