roster-server 2.4.12 β†’ 2.4.13

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.
@@ -2,6 +2,7 @@
2
2
 
3
3
  const fs = require('fs');
4
4
  const path = require('path');
5
+ const { pipeline } = require('stream/promises');
5
6
 
6
7
  const MIME_BY_EXT = {
7
8
  html: 'text/html',
@@ -71,7 +72,7 @@ function createStaticHandler(rootPath) {
71
72
  const root = path.resolve(rootPath);
72
73
 
73
74
  return function staticSiteFactory(virtualServer) {
74
- return function staticHandler(req, res) {
75
+ return async function staticHandler(req, res) {
75
76
  if (req.method !== 'GET' && req.method !== 'HEAD') {
76
77
  res.writeHead(405, { 'Content-Type': 'text/plain' });
77
78
  res.end('Method Not Allowed');
@@ -90,40 +91,48 @@ function createStaticHandler(rootPath) {
90
91
  return;
91
92
  }
92
93
 
93
- if (!fs.existsSync(filePath)) {
94
- res.writeHead(404, { 'Content-Type': 'text/plain' });
95
- res.end('Not Found');
96
- return;
97
- }
98
-
99
- const stat = fs.statSync(filePath);
100
- let servePath = filePath;
101
- if (!stat.isFile()) {
102
- if (!stat.isDirectory()) {
94
+ let file;
95
+ try {
96
+ let servePath = filePath;
97
+ let stat = await fs.promises.stat(servePath);
98
+ if (stat.isDirectory()) {
99
+ servePath = path.join(servePath, 'index.html');
100
+ stat = await fs.promises.stat(servePath);
101
+ }
102
+ if (!stat.isFile()) {
103
103
  res.writeHead(404, { 'Content-Type': 'text/plain' });
104
104
  res.end('Not Found');
105
105
  return;
106
106
  }
107
- const indexInDir = path.join(filePath, 'index.html');
108
- if (!fs.existsSync(indexInDir) || !fs.statSync(indexInDir).isFile()) {
107
+ file = await fs.promises.open(servePath, 'r');
108
+ stat = await file.stat();
109
+ if (res.destroyed) return;
110
+ if (!stat.isFile()) {
109
111
  res.writeHead(404, { 'Content-Type': 'text/plain' });
110
112
  res.end('Not Found');
111
113
  return;
112
114
  }
113
- servePath = indexInDir;
114
- }
115
-
116
- const contentType = getContentType(servePath);
117
- const content = fs.readFileSync(servePath);
118
-
119
- res.writeHead(200, {
120
- 'Content-Type': contentType,
121
- 'Content-Length': content.length
122
- });
123
- if (req.method === 'GET') {
124
- res.end(content);
125
- } else {
126
- res.end();
115
+ res.writeHead(200, {
116
+ 'Content-Type': getContentType(servePath),
117
+ 'Content-Length': stat.size
118
+ });
119
+ if (req.method === 'HEAD') {
120
+ res.end();
121
+ } else {
122
+ await pipeline(file.createReadStream(), res);
123
+ }
124
+ } catch (error) {
125
+ if (res.destroyed || res.writableEnded) return;
126
+ if (res.headersSent) {
127
+ res.destroy(error);
128
+ return;
129
+ }
130
+ const missing = error.code === 'ENOENT' || error.code === 'ENOTDIR';
131
+ const forbidden = error.code === 'EACCES' || error.code === 'EPERM';
132
+ res.writeHead(missing ? 404 : forbidden ? 403 : 500, { 'Content-Type': 'text/plain' });
133
+ res.end(missing ? 'Not Found' : forbidden ? 'Forbidden' : 'Internal Server Error');
134
+ } finally {
135
+ if (file) await file.close();
127
136
  }
128
137
  };
129
138
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "roster-server",
3
- "version": "2.4.12",
3
+ "version": "2.4.13",
4
4
  "description": "πŸ‘Ύ RosterServer - A domain host router to host multiple HTTPS.",
5
5
  "main": "index.js",
6
6
  "repository": {
@@ -1,367 +1,79 @@
1
1
  ---
2
2
  name: roster-server
3
- description: Virtual hosting for multiple HTTPS sites with Let's Encrypt SSL automation. Each domain gets isolated VirtualServer instance, supports Express/Socket.IO/custom handlers, static sites (index.html only, no Node), local HTTP dev mode with CRC32-based ports, automatic www redirects, and SNI certificate management. Static site logic is modular (lib/static-site-handler.js, lib/resolve-site-app.js).
3
+ description: Integrate and troubleshoot roster-server in applications using domain routing, HTTPS certificates, local HTTP, Express, Socket.IO, external servers, and graceful shutdown. Use when adding, configuring, or debugging RosterServer in a service.
4
4
  ---
5
5
 
6
- ## Quick Setup
6
+ # RosterServer integration
7
7
 
8
- ### Production
9
- ```javascript
10
- const Roster = require('roster-server');
11
-
12
- const roster = new Roster({
13
- email: 'admin@example.com',
14
- wwwPath: '/srv/www',
15
- greenlockStorePath: '/srv/greenlock.d',
16
- local: true
17
- });
8
+ Use this skill to configure a consuming application. Keep its existing configuration source, module format, and process lifecycle. Check the installed package's API when version differences matter; the upstream README may describe changes not yet installed.
18
9
 
19
- roster.start();
20
- ```
21
-
22
- ### Local Development
23
- ```javascript
24
- const roster = new Roster({
25
- local: true, // HTTP mode, no SSL
26
- wwwPath: './www'
27
- });
10
+ ## Choose the lifecycle owner
28
11
 
29
- roster.start().then(() => {
30
- console.log('example.com:', roster.getUrl('example.com'));
31
- // β†’ http://localhost:9465 (deterministic CRC32-based port)
32
- });
33
- ```
34
-
35
- ## Directory Structure
36
-
37
- ```
38
- project/
39
- β”œβ”€β”€ greenlock.d/ # SSL certificates (auto-generated)
40
- β”œβ”€β”€ www/
41
- β”‚ β”œβ”€β”€ example.com/
42
- β”‚ β”‚ └── index.js # Handler for example.com
43
- β”‚ β”œβ”€β”€ api.example.com/
44
- β”‚ β”‚ └── index.js # Handler for subdomain
45
- β”‚ β”œβ”€β”€ static-site.com/ # Static site (no index.js)
46
- β”‚ β”‚ β”œβ”€β”€ index.html
47
- β”‚ β”‚ β”œβ”€β”€ css/
48
- β”‚ β”‚ └── images/
49
- β”‚ └── *.example.com/
50
- β”‚ └── index.js # Wildcard: one handler for all subdomains
51
- └── server.js # Your setup
52
- ```
12
+ - **Local development:** `new Roster({ local: true, wwwPath: absolutePath })`, register sites, then `await roster.start()`. Local mode binds to `localhost` and assigns ports; read URLs with `roster.getUrl(domain)` after startup.
13
+ - **Standalone HTTPS:** set the real `email`, absolute `wwwPath` and `greenlockStorePath`; leave `local` false. `await roster.start()` owns ACME HTTP on port 80 and the configured HTTPS listeners. A different HTTPS port does not eliminate the need for port 80.
14
+ - **External server/worker:** `await roster.init()`, then `roster.attach(server, { port })` or an HTTPS helper. The caller owns listening and closing that server. `port` on `attach()` selects the routing table, not the TCP port.
15
+ - **Serving-only worker:** use `autoCertificates: false` with `init()` and `createServingHttpsServer({ servername })`. A separate manager must provide certificates first. Calling `start()` still enables the standalone ACME lifecycle.
53
16
 
54
- **Site resolution**: For each domain folder, RosterServer looks for `index.js` / `index.mjs` / `index.cjs` first. If none exist but `index.html` exists, it serves the folder as a static site (modular handler in `lib/static-site-handler.js`). Node app takes precedence when both exist.
17
+ `init()` invokes site factories and can write certificate configuration/start renewal work; it is not a side-effect-free inspection method. Keep domains and certificate configuration consistent across workers sharing a store. The legacy `cluster: true` launcher delegates process management to Greenlock; it is separate from using Roster with an external cluster manager.
55
18
 
56
- ## Handler Patterns
19
+ ## Sites and routing
57
20
 
58
- **Node app**: Each `www/{domain}/index.js` (or `.mjs`/`.cjs`) must export a function that receives `httpsServer` and returns a request handler.
59
-
60
- **Static site**: If the domain folder has no index script but has `index.html`, RosterServer serves the folder as static files (`GET /` β†’ `index.html`, other paths β†’ file or 404, path-traversal protected). No code required.
61
-
62
- ### Pattern 1: Basic HTTP Handler
63
- ```javascript
64
- module.exports = (httpsServer) => {
65
- return (req, res) => {
66
- res.writeHead(200, { 'Content-Type': 'text/plain' });
67
- res.end('Hello World');
68
- };
69
- };
70
- ```
71
-
72
- ### Pattern 2: Express App
73
- ```javascript
74
- const express = require('express');
75
-
76
- module.exports = (httpsServer) => {
77
- const app = express();
78
-
79
- app.get('/', (req, res) => res.send('Hello'));
80
- app.post('/api/data', (req, res) => res.json({ ok: true }));
81
-
82
- return app;
83
- };
84
- ```
21
+ Register sites before `init()`/`start()`. Export a synchronous factory `(virtualServer) => requestHandler`; the returned `(req, res)` handler may be async. An Express app is a valid return value. Never call `app.listen()` inside the factory.
85
22
 
86
- ### Pattern 3: Socket.IO
87
23
  ```javascript
88
- const { Server } = require('socket.io');
89
-
90
- module.exports = (httpsServer) => {
91
- const io = new Server(httpsServer);
92
-
93
- io.on('connection', (socket) => {
94
- socket.on('message', (data) => io.emit('message', data));
95
- });
96
-
97
- return (req, res) => {
98
- if (req.url && req.url.startsWith(io.opts.path)) return;
99
- res.writeHead(200);
100
- res.end('Socket.IO running');
101
- };
102
- };
103
- ```
104
-
105
- ### Pattern 4: Manual Registration
106
- ```javascript
107
- // In server.js, before roster.start()
108
- roster.register('example.com', (httpsServer) => {
109
- return (req, res) => {
110
- res.writeHead(200);
111
- res.end('Manual handler');
112
- };
113
- });
114
-
115
- // With custom port
116
- roster.register('api.example.com:8443', handler);
117
-
118
- // Wildcard: one handler for all subdomains (default port or custom)
119
- roster.register('*.example.com', handler);
120
- roster.register('*.example.com:8080', handler);
121
- ```
122
-
123
- ### Pattern 5: Static Site (no code)
124
- Place only `index.html` (and assets) in `www/example.com/`. No `index.js` needed. RosterServer serves files with path-traversal protection; `/` β†’ `index.html`, other paths β†’ file or 404. Implemented in `lib/static-site-handler.js` and `lib/resolve-site-app.js`.
125
-
126
- ### Pattern 6: Cluster-Friendly (external server)
127
- ```javascript
128
- const https = require('https');
129
24
  const Roster = require('roster-server');
25
+ const path = require('node:path');
130
26
 
131
- const roster = new Roster({
132
- email: 'admin@example.com',
133
- wwwPath: '/srv/www',
134
- greenlockStorePath: '/srv/greenlock.d'
135
- });
136
-
137
- await roster.init();
138
-
139
- const server = https.createServer({ SNICallback: roster.sniCallback() });
140
- roster.attach(server);
141
-
142
- // Master passes connections β€” worker never calls listen()
143
- process.on('message', (msg, connection) => {
144
- if (msg === 'sticky-session:connection') {
145
- server.emit('connection', connection);
146
- }
147
- });
27
+ const roster = new Roster({ local: true, wwwPath: path.resolve('www') });
28
+ roster.register('example.com', () => (req, res) => res.end('Hello'));
148
29
  ```
149
30
 
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');
31
+ - Discovery tries `index.js`, `index.mjs`, then `index.cjs`; use the module format appropriate to the application's package. Without an entry script, `index.html` enables static serving.
32
+ - `filename` changes the script basename only. `basePath` supplies default directories; it does not rebase explicitly supplied relative paths. Prefer absolute paths.
33
+ - Discovered sites stay on HTTPS **443**, even with another default `port`. Discovery replaces a manual registration for the same domain/port; distinct ports coexist.
34
+ - Manual `register('api.example.com:8443', factory)` selects that port. Exact hosts precede wildcard matches; `*.example.com` does not match the apex.
35
+ - Static serving streams files and supports HEAD and directory indexes, with strict missing-file 404s and no SPA fallback.
36
+ - Local ports are hash-based with in-instance collision handling, not OS availability detection. Do not hardcode example port numbers.
37
+ - Production `getUrl()` uses the instance's default port; it is not a per-registration custom-port lookup. A site registered only on another port can return `null`.
161
38
 
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
- ```
39
+ ## Socket.IO and asynchronous requests
174
40
 
175
- ### Pattern 8: Optional Scanner Blocking
176
- ```javascript
177
- const Roster = require('roster-server');
178
- const { createScannerBlocker } = require('roster-server/plugins/scanner-blocker.js');
41
+ Attach Socket.IO to the supplied virtual server. Return the ordinary HTTP handler and register cleanup in the same factory:
179
42
 
180
- const roster = new Roster({ local: true, wwwPath: './www' });
181
- roster.use(createScannerBlocker());
182
- roster.start();
183
- ```
184
-
185
- The plugin blocks common PHP, WordPress, repository, and sensitive-file probes before site handlers. Defaults are a 60-second window, 3 strikes, a 15-minute ban, 10,000 tracked clients, and `trustProxy: false`; pass only the values to override. Set `trustProxy: true` only behind a trusted reverse proxy that overwrites `X-Forwarded-For`. Ban state is per process and in memory; use `onBlock` to integrate a shared firewall or Fail2ban.
186
-
187
- ## Key Configuration Options
188
-
189
- ```javascript
190
- new Roster({
191
- email: 'admin@example.com', // Required for SSL
192
- wwwPath: '/srv/www', // Site handlers directory
193
- greenlockStorePath: '/srv/greenlock.d', // SSL storage
194
- dnsChallenge: { ... }, // Optional override. Default is local/manual DNS-01 (acme-dns-01-cli)
195
-
196
- // Environment
197
- local: false, // true = HTTP, false = HTTPS
198
- staging: false, // true = Let's Encrypt staging
199
-
200
- // Server
201
- hostname: '::',
202
- port: 443, // Default HTTPS port (NOT 80!)
203
-
204
- // Local mode
205
- minLocalPort: 4000,
206
- maxLocalPort: 9999,
207
-
208
- // Advanced
209
- filename: 'index', // Handler filename (no extension)
210
- basePath: '/srv' // Base for relative paths
211
- })
212
- ```
213
-
214
- ## Core API
215
-
216
- ### `roster.start()`
217
- Loads sites, generates SSL config, starts servers. Returns `Promise<void>`. Calls `init()` internally.
218
-
219
- ### `roster.init()`
220
- Loads sites, creates VirtualServers, prepares dispatchers β€” but creates **no servers** and calls **no `.listen()`**. Returns `Promise<Roster>`. Idempotent. Use this for cluster-friendly integration where an external manager owns the socket.
221
-
222
- ### `roster.requestHandler(port?)`
223
- Returns `(req, res) => void` dispatcher for a port (defaults to `defaultPort`). Requires `init()` first. Handles Host-header routing, www→non-www redirects, wildcard matching.
224
-
225
- ### `roster.upgradeHandler(port?)`
226
- Returns `(req, socket, head) => void` for WebSocket upgrade routing. Requires `init()` first.
227
-
228
- ### `roster.sniCallback()`
229
- Returns `(servername, callback) => void` TLS SNI callback that resolves certs from `greenlockStorePath`. With `autoCertificates` enabled (default), it can issue missing certs automatically. Production mode only. Requires `init()` first.
230
-
231
- ### `roster.ensureCertificate(servername)`
232
- Forces certificate availability for a domain and returns `{ key, cert }`. With `autoCertificates` enabled (default), it issues certs automatically when missing.
233
-
234
- ### `roster.loadCertificate(servername)`
235
- Loads existing `{ key, cert }` from `greenlockStorePath` without issuing new certificates.
236
-
237
- ### `roster.createManagedHttpsServer(options)`
238
- Creates a pre-wired `https.Server` with default cert, SNI callback, and attached request/upgrade handlers.
239
-
240
- ### `roster.createServingHttpsServer(options)`
241
- Serving-only helper for worker processes. Same as `createManagedHttpsServer(..., ensureCertificate: false)`.
242
-
243
- ### `roster.attach(server, { port }?)`
244
- Convenience: wires `requestHandler` + `upgradeHandler` onto an external `http.Server` or `https.Server`. Returns `this`. Requires `init()` first.
245
-
246
- ### `roster.register(domain, handler)`
247
- Manually register a domain handler. Domain can include port: `'api.com:8443'`. For wildcards use `'*.example.com'` or `'*.example.com:8080'`.
248
-
249
- ### `roster.use(plugin)`
250
- Registers a synchronous request plugin. It receives `(req, res, { host, domain })` and stops dispatch when it returns `true`.
251
-
252
- ### `roster.getUrl(domain)`
253
- Get environment-aware URL:
254
- - Local mode: `http://localhost:{port}`
255
- - Production: `https://{domain}` or `https://{domain}:{port}`
256
- - Returns `null` if domain not registered. Supports wildcard-matched hosts (e.g. `getUrl('api.example.com')` when `*.example.com` is registered).
257
-
258
- ## How It Works
259
-
260
- ### Request Flow
261
- 1. Request arrives β†’ Dispatcher extracts `Host` header
262
- 2. Strips `www.` prefix (301 redirect if present)
263
- 3. Looks up domain β†’ Gets `VirtualServer` instance
264
- 4. Routes to handler via `virtualServer.processRequest(req, res)`
265
-
266
- ### VirtualServer Architecture
267
- Each domain gets isolated server instance that simulates `http.Server`:
268
- - Captures `request` and `upgrade` event listeners
269
- - Complete separation between domains
270
- - No configuration conflicts between apps
271
-
272
- ### Port Assignment
273
- **Production**: Default 443, custom via `domain:port` syntax
274
- **Local**: CRC32 hash of domain β†’ deterministic port in range 4000-9999
275
- **Reserved**: Port 80 for ACME challenges only
276
-
277
- ### SSL Management
278
- - Automatic Let's Encrypt certificate generation
279
- - Auto-renewal 45 days before expiration
280
- - SNI support for multiple domains
281
- - Custom ports reuse certificates via SNI callback
282
- - **Wildcard** (`*.example.com`): use folder `www/*.example.com/` or `roster.register('*.example.com', handler)`. Default DNS-01 plugin is local/manual `acme-dns-01-cli`; set `dnsChallenge` only when overriding provider integration.
283
-
284
- ## Common Issues & Solutions
285
-
286
- **Port 443 in use**: Use different port `{ port: 8443 }`
287
- **Certificate failed**: Check firewall (ports 80, 443), verify DNS, try `staging: true`
288
- **Site not found**: Verify directory name matches domain. For Node: check `index.js` exports function. For static: ensure `index.html` exists (no index script).
289
- **Local port conflict**: Adjust `minLocalPort`/`maxLocalPort` range
290
- **Socket.IO not working**: Ensure handler checks `io.opts.path` and returns properly
291
-
292
- ## Best Practices
293
-
294
- 1. **Test with staging first**: `staging: true` to avoid Let's Encrypt rate limits
295
- 2. **Use local mode for dev**: `local: true` for faster iteration
296
- 3. **Environment variables**: Configure via `process.env` for portability
297
- 4. **Error handling**: Wrap handlers with try/catch, don't expose internals
298
- 5. **Socket.IO paths**: Always check `req.url.startsWith(io.opts.path)` in returned handler
299
- 6. **Port 80**: Never use as HTTPS port (reserved for ACME)
300
-
301
- ## Quick Examples
302
-
303
- ### Full Production Setup
304
43
  ```javascript
305
- const Roster = require('roster-server');
306
-
307
- const roster = new Roster({
308
- email: process.env.ADMIN_EMAIL,
309
- wwwPath: '/srv/www',
310
- greenlockStorePath: '/srv/greenlock.d',
311
- staging: process.env.NODE_ENV !== 'production'
312
- });
44
+ const { Server } = require('socket.io');
313
45
 
314
- roster.start().then(() => {
315
- console.log('RosterServer running');
316
- }).catch(err => {
317
- console.error('Startup failed:', err);
318
- process.exit(1);
319
- });
46
+ module.exports = (server) => {
47
+ const io = new Server(server);
48
+ server.onClose(() => new Promise(resolve => io.close(resolve)));
49
+ return (req, res) => res.end('Socket.IO site');
50
+ };
320
51
  ```
321
52
 
322
- ### Local Dev with Manual Registration
323
- ```javascript
324
- const roster = new Roster({ local: true, wwwPath: './www' });
325
-
326
- roster.register('test.local', (server) => {
327
- return (req, res) => {
328
- res.writeHead(200, { 'Content-Type': 'application/json' });
329
- res.end(JSON.stringify({ status: 'ok', url: roster.getUrl('test.local') }));
330
- };
331
- });
53
+ Socket.IO wraps the request listeners and captures the returned handler as its fallback. Do not add a manual endpoint exclusion to that handler; use `io.path()` when application code needs the path. Do not use `io.opts.path`.
332
54
 
333
- roster.start();
334
- ```
55
+ A virtual request listener owns its request even before it ends the response. Roster does not infer that an asynchronous listener declined the request. Thrown/rejected handler errors produce 500 or destroy an already-started response. Virtual servers share a process; they are not process isolation boundaries.
335
56
 
336
- ### Environment-Aware Configuration
337
- ```javascript
338
- const isProduction = process.env.NODE_ENV === 'production';
57
+ ## Shutdown
339
58
 
340
- const roster = new Roster({
341
- email: process.env.ADMIN_EMAIL || 'admin@example.com',
342
- wwwPath: process.env.WWW_PATH || './www',
343
- greenlockStorePath: process.env.SSL_PATH || './greenlock.d',
344
- local: !isProduction,
345
- staging: !isProduction,
346
- minLocalPort: parseInt(process.env.MIN_PORT) || 4000,
347
- maxLocalPort: parseInt(process.env.MAX_PORT) || 9999
348
- });
349
-
350
- roster.start();
351
- ```
59
+ - Connect `await roster.close()` to the application's existing shutdown flow. Roster does not install signal handlers or terminate processes.
60
+ - Register `virtualServer.onClose(fn)` for each site's timers, databases, and other resources. Return/await its cleanup Promise; wrap callback APIs when needed.
61
+ - Roster signals virtual `close` at shutdown start to release integrations such as long polling, closes routed WebSockets, drains HTTP, then runs cleanup hooks concurrently. Put dependent cleanup steps in one hook.
62
+ - Hooks run once. Failures are aggregated after other hooks are attempted. `closeTimeoutMs` bounds the complete operation; timeout cannot cancel arbitrary user Promises or in-flight ACME work.
63
+ - `close()` is idempotent and terminal. `init()`/`start()` share concurrent calls; startup failures clean up the instance. Create a new instance after closing or failed startup.
64
+ - For `attach()` and both HTTPS helpers, also close the caller-owned server. Roster removes its attached listeners but does not own that server's listener lifecycle. Remove manually wired dispatchers yourself.
65
+ - Coordinate each process separately; closing an instance does not stop the legacy Greenlock cluster's worker processes.
352
66
 
353
- ## Implementation Checklist
67
+ ## Certificates and plugins
354
68
 
355
- When implementing RosterServer:
69
+ - Use `staging: true` for ACME test issuance when requested. `init()` alone does not bind the HTTP challenge listener.
70
+ - Wildcard certificates require DNS-01. The default CLI wrapper needs manual TXT records unless an API provider is configured; `dnsChallenge: false` disables that integration, not HTTPS.
71
+ - Linode mode uses `ROSTER_DNS_PROVIDER=linode` and `LINODE_API_KEY` from the application's secret configuration; a key also selects Linode when no provider is set. With an explicit `dnsChallenge` object, include `module: 'acme-dns-01-cli'` to select the wrapper.
72
+ - For unattended Linode operation that must fail rather than fall back to manual DNS, set `dnsChallenge.dnsApiFallbackToManual: false`. Keep other provider choices explicit.
73
+ - `combineWildcardCerts` combines apex/www/wildcard issuance using DNS-01; `disableWildcard` ignores wildcard sites. Do not enable either as a generic troubleshooting step.
74
+ - `ensureCertificate(name)` loads existing PEMs or issues missing ones; `loadCertificate(name)` only reads files. Neither means β€œforce renewal.” File-based SNI caches detect certificate changes on supported runtimes.
75
+ - In local testing, Bun 1.3.4's `node:https` serving helper did not invoke `SNICallback`, including with the previous synchronous resolver. Do not promise SNI certificate reload merely because files changed; Node and Bun require separate runtime verification.
76
+ - Request plugins registered with `roster.use(fn)` are synchronous and run before redirects/dispatch. Return `true` only after handling the response. Promise returns produce 500.
77
+ - The optional `createScannerBlocker` comes from `roster-server/plugins/scanner-blocker.js`. Its ban state is per process and in memory. Enable `trustProxy` only when a trusted proxy overwrites `X-Forwarded-For`; `onBlock` does not itself provide shared bans.
356
78
 
357
- - [ ] Create `www/` directory structure with domain folders
358
- - [ ] Each domain has either `index.js` (or `.mjs`/`.cjs`) exporting `(httpsServer) => handler`, or `index.html` (and assets) for a static site
359
- - [ ] Configure email for Let's Encrypt notifications
360
- - [ ] Test with `local: true` first
361
- - [ ] Test with `staging: true` before production
362
- - [ ] Ensure ports 80 and 443 are open (production)
363
- - [ ] Verify DNS points to server
364
- - [ ] Never use port 80 as HTTPS port
365
- - [ ] Use `roster.getUrl(domain)` for environment-aware URLs
366
- - [ ] Handle Socket.IO paths correctly in returned handler
367
- - [ ] Implement error handling in handlers
79
+ Use the consuming application's installed README for the complete options and method signatures. The [upstream README](https://github.com/clasen/RosterServer#readme) is the project reference; prefer documentation matching the installed version.