roster-server 1.9.4 → 1.9.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -18,6 +18,14 @@ Welcome to **RosterServer**, the ultimate domain host router with automatic HTTP
18
18
  npm install roster-server
19
19
  ```
20
20
 
21
+ ## 🤖 AI Skill
22
+
23
+ You can also add RosterServer as a skill for AI agentic development:
24
+
25
+ ```bash
26
+ npx skills add https://github.com/clasen/RosterServer --skill roster-server
27
+ ```
28
+
21
29
  ## 🛠️ Usage
22
30
 
23
31
  ### Directory Structure
@@ -205,27 +213,11 @@ const roster = new Roster({
205
213
  });
206
214
  ```
207
215
 
208
- ### Getting Local URLs
209
-
210
- RosterServer provides two methods to get the local URL for a domain:
211
-
212
- **1. Static Method (Predictable, No Instance Required):**
213
-
214
- ```javascript
215
- // Get the URL before starting the server (using default range 4000-9999)
216
- const url = Roster.getLocalUrl('example.com');
217
- console.log(url); // http://localhost:9465
218
-
219
- // Or specify custom port range
220
- const customUrl = Roster.getLocalUrl('example.com', {
221
- minLocalPort: 5000,
222
- maxLocalPort: 6000
223
- });
224
- ```
216
+ ### Getting URLs
225
217
 
226
- This static method calculates the port deterministically using CRC32, so you can predict the URL before even creating a Roster instance.
218
+ RosterServer provides a method to get the URL for a domain that adapts automatically to your environment:
227
219
 
228
- **2. Instance Method (After Registration):**
220
+ **Instance Method: `roster.getUrl(domain)`**
229
221
 
230
222
  ```javascript
231
223
  const roster = new Roster({ local: true });
@@ -233,29 +225,43 @@ roster.register('example.com', handler);
233
225
 
234
226
  await roster.start();
235
227
 
236
- // Get the actual URL assigned to the domain
237
- const url = roster.getLocalUrl('example.com');
238
- console.log(url); // http://localhost:9465
228
+ // Get the URL - automatically adapts to environment
229
+ const url = roster.getUrl('example.com');
230
+ console.log(url);
231
+ // Local mode: http://localhost:9465
232
+ // Production mode: https://example.com
239
233
  ```
240
234
 
241
- This instance method returns the actual URL assigned to the domain after the server starts. It's useful when you need to confirm the URL or when there might be port collisions.
235
+ This method:
236
+ - Returns the correct URL based on your environment (`local: true/false`)
237
+ - In **local mode**: Returns `http://localhost:{port}` with the assigned port
238
+ - In **production mode**: Returns `https://{domain}` (or with custom port if configured)
239
+ - Handles `www.` prefix automatically (returns same URL)
240
+ - Returns `null` for domains that aren't registered
242
241
 
243
242
  **Example Usage:**
244
243
 
245
244
  ```javascript
246
- const roster = new Roster({ local: true });
247
-
248
- roster.register('example.com', (httpsServer) => {
249
- return (req, res) => {
250
- res.writeHead(200);
251
- res.end('Hello World!');
252
- };
253
- });
254
-
255
- roster.start().then(() => {
256
- const url = roster.getLocalUrl('example.com');
257
- console.log(`Server available at: ${url}`);
258
- });
245
+ // Local development
246
+ const localRoster = new Roster({ local: true });
247
+ localRoster.register('example.com', handler);
248
+ await localRoster.start();
249
+ console.log(localRoster.getUrl('example.com'));
250
+ // → http://localhost:9465
251
+
252
+ // Production
253
+ const prodRoster = new Roster({ local: false });
254
+ prodRoster.register('example.com', handler);
255
+ await prodRoster.start();
256
+ console.log(prodRoster.getUrl('example.com'));
257
+ // → https://example.com
258
+
259
+ // Production with custom port
260
+ const customRoster = new Roster({ local: false, port: 8443 });
261
+ customRoster.register('api.example.com', handler);
262
+ await customRoster.start();
263
+ console.log(customRoster.getUrl('api.example.com'));
264
+ // → https://api.example.com:8443
259
265
  ```
260
266
 
261
267
  ## 🧂 A Touch of Magic
@@ -7,15 +7,7 @@ const roster = new Roster({
7
7
  maxLocalPort: 5100 // Custom maximum port
8
8
  });
9
9
 
10
- console.log('\n📍 Static URL Prediction with custom range (5000-5100):');
11
- console.log('example.com →', Roster.getLocalUrl('example.com', {
12
- minLocalPort: 5000,
13
- maxLocalPort: 5100
14
- }));
15
- console.log('api.example.com →', Roster.getLocalUrl('api.example.com', {
16
- minLocalPort: 5000,
17
- maxLocalPort: 5100
18
- }));
10
+ console.log('\n🔧 Creating server with custom port range (5000-5100)...\n');
19
11
 
20
12
  roster.register('example.com', (httpsServer) => {
21
13
  return (req, res) => {
@@ -32,9 +24,9 @@ roster.register('api.example.com', (httpsServer) => {
32
24
  });
33
25
 
34
26
  roster.start().then(() => {
35
- console.log('\n🚀 Server Started with custom port range:');
36
- console.log('example.com →', roster.getLocalUrl('example.com'));
37
- console.log('api.example.com →', roster.getLocalUrl('api.example.com'));
27
+ console.log('🚀 Server Started with custom port range:');
28
+ console.log('example.com →', roster.getUrl('example.com'));
29
+ console.log('api.example.com →', roster.getUrl('api.example.com'));
38
30
 
39
31
  console.log('\n✅ Both domains running in custom port range (5000-5100)!');
40
32
  });
@@ -0,0 +1,55 @@
1
+ const Roster = require('../index.js');
2
+
3
+ // Example 1: Local mode
4
+ console.log('\n📍 EXAMPLE 1: Local Development Mode\n');
5
+ const localRoster = new Roster({ local: true });
6
+
7
+ localRoster.register('example.com', (httpsServer) => {
8
+ return (req, res) => {
9
+ res.writeHead(200, { 'Content-Type': 'text/plain' });
10
+ res.end('Hello from local!');
11
+ };
12
+ });
13
+
14
+ localRoster.start().then(() => {
15
+ console.log('Local URL:', localRoster.getUrl('example.com'));
16
+ console.log('→ Returns: http://localhost:{port}\n');
17
+ });
18
+
19
+ // Example 2: Production mode (simulated, without actually starting)
20
+ console.log('📍 EXAMPLE 2: Production Mode (simulated)\n');
21
+ const prodRoster = new Roster({
22
+ local: false,
23
+ email: 'admin@example.com'
24
+ });
25
+
26
+ prodRoster.register('example.com', (httpsServer) => {
27
+ return (req, res) => {
28
+ res.writeHead(200);
29
+ res.end('Hello from production!');
30
+ };
31
+ });
32
+
33
+ // Don't actually start (would need real SSL), just show what URL would be
34
+ console.log('Production URL (without starting):', 'https://example.com');
35
+ console.log('→ Would return: https://example.com\n');
36
+
37
+ // Example 3: Production with custom port
38
+ console.log('📍 EXAMPLE 3: Production with Custom Port (simulated)\n');
39
+ const customPortRoster = new Roster({
40
+ local: false,
41
+ port: 8443,
42
+ email: 'admin@example.com'
43
+ });
44
+
45
+ customPortRoster.register('api.example.com', (httpsServer) => {
46
+ return (req, res) => {
47
+ res.writeHead(200);
48
+ res.end('API');
49
+ };
50
+ });
51
+
52
+ console.log('Custom port URL (without starting):', 'https://api.example.com:8443');
53
+ console.log('→ Would return: https://api.example.com:8443\n');
54
+
55
+ console.log('✅ getUrl() adapts to the environment automatically!');
@@ -1,12 +1,8 @@
1
1
  const Roster = require('../index.js');
2
2
 
3
- // Example 1: Get URL before creating instance (static method)
4
- console.log('\n📍 Static URL Prediction (before server starts):');
5
- console.log('example.com →', Roster.getLocalUrl('example.com'));
6
- console.log('api.example.com →', Roster.getLocalUrl('api.example.com'));
7
- console.log('test.example.com →', Roster.getLocalUrl('test.example.com'));
3
+ // Example: Get URL after registration (adapts to environment)
4
+ console.log('\n🔧 Creating local development server...\n');
8
5
 
9
- // Example 2: Get URL after registration (instance method)
10
6
  const roster = new Roster({ local: true });
11
7
 
12
8
  roster.register('example.com', (httpsServer) => {
@@ -24,17 +20,17 @@ roster.register('api.example.com', (httpsServer) => {
24
20
  });
25
21
 
26
22
  roster.start().then(() => {
27
- console.log('\n🚀 Server Started - Actual URLs:');
28
- console.log('example.com →', roster.getLocalUrl('example.com'));
29
- console.log('api.example.com →', roster.getLocalUrl('api.example.com'));
23
+ console.log('🚀 Server Started - URLs (based on environment):');
24
+ console.log('example.com →', roster.getUrl('example.com'));
25
+ console.log('api.example.com →', roster.getUrl('api.example.com'));
30
26
 
31
27
  // Test with www prefix (should return same URL)
32
28
  console.log('\n🔄 Testing www prefix handling:');
33
- console.log('www.example.com →', roster.getLocalUrl('www.example.com'));
29
+ console.log('www.example.com →', roster.getUrl('www.example.com'));
34
30
 
35
31
  // Test non-existent domain
36
32
  console.log('\n❌ Testing non-existent domain:');
37
- console.log('nonexistent.com →', roster.getLocalUrl('nonexistent.com') || 'null (domain not registered)');
33
+ console.log('nonexistent.com →', roster.getUrl('nonexistent.com') || 'null (domain not registered)');
38
34
 
39
35
  console.log('\n✅ All domains running!');
40
36
  });
package/demo/socketio.js CHANGED
@@ -27,11 +27,7 @@ roster.register('example.com', (httpsServer) => {
27
27
  });
28
28
 
29
29
  roster.start().then(() => {
30
- // Get local URL for registered domain (requires instance)
31
- const url = roster.getLocalUrl('example.com');
30
+ // Get URL for registered domain (adapts to environment)
31
+ const url = roster.getUrl('example.com');
32
32
  console.log(`✅ Socket.IO server available at: ${url}`);
33
-
34
- // Get local URL without instance (static method - predictable port)
35
- const staticUrl = Roster.getLocalUrl('example.com');
36
- console.log(`ℹ️ Static prediction: ${staticUrl}`);
37
33
  });
@@ -0,0 +1,64 @@
1
+ const Roster = require('../index.js');
2
+
3
+ console.log('\n🧪 Testing getUrl() method in different scenarios\n');
4
+
5
+ // Test 1: Local mode with default port
6
+ console.log('TEST 1: Local mode (default port)');
7
+ const local1 = new Roster({ local: true });
8
+ local1.register('example.com', () => (req, res) => res.end('OK'));
9
+ local1.start().then(() => {
10
+ const url = local1.getUrl('example.com');
11
+ console.log(`✓ example.com → ${url}`);
12
+ console.assert(url.startsWith('http://localhost:'), 'Should be localhost HTTP');
13
+ });
14
+
15
+ // Test 2: Local mode with custom port range
16
+ console.log('\nTEST 2: Local mode (custom port range)');
17
+ const local2 = new Roster({ local: true, minLocalPort: 6000, maxLocalPort: 6100 });
18
+ local2.register('test.com', () => (req, res) => res.end('OK'));
19
+ local2.start().then(() => {
20
+ const url = local2.getUrl('test.com');
21
+ console.log(`✓ test.com → ${url}`);
22
+ const port = parseInt(url.split(':')[2]);
23
+ console.assert(port >= 6000 && port <= 6100, 'Port should be in custom range');
24
+ });
25
+
26
+ // Test 3: Production mode (default HTTPS port)
27
+ console.log('\nTEST 3: Production mode (default HTTPS)');
28
+ const prod1 = new Roster({ local: false, email: 'admin@example.com' });
29
+ prod1.register('example.com', () => (req, res) => res.end('OK'));
30
+ const prodUrl1 = prod1.getUrl('example.com');
31
+ console.log(`✓ example.com → ${prodUrl1}`);
32
+ console.assert(prodUrl1 === 'https://example.com', 'Should be HTTPS without port');
33
+
34
+ // Test 4: Production mode (custom port)
35
+ console.log('\nTEST 4: Production mode (custom port)');
36
+ const prod2 = new Roster({ local: false, port: 8443, email: 'admin@example.com' });
37
+ prod2.register('api.example.com', () => (req, res) => res.end('OK'));
38
+ const prodUrl2 = prod2.getUrl('api.example.com');
39
+ console.log(`✓ api.example.com → ${prodUrl2}`);
40
+ console.assert(prodUrl2 === 'https://api.example.com:8443', 'Should include custom port');
41
+
42
+ // Test 5: www prefix handling
43
+ console.log('\nTEST 5: www prefix handling');
44
+ const local3 = new Roster({ local: true });
45
+ local3.register('example.com', () => (req, res) => res.end('OK'));
46
+ local3.start().then(() => {
47
+ const url1 = local3.getUrl('example.com');
48
+ const url2 = local3.getUrl('www.example.com');
49
+ console.log(`✓ example.com → ${url1}`);
50
+ console.log(`✓ www.example.com → ${url2}`);
51
+ console.assert(url1 === url2, 'www should return same URL');
52
+ });
53
+
54
+ // Test 6: Non-existent domain
55
+ console.log('\nTEST 6: Non-existent domain');
56
+ const local4 = new Roster({ local: true });
57
+ local4.register('example.com', () => (req, res) => res.end('OK'));
58
+ local4.start().then(() => {
59
+ const url = local4.getUrl('nonexistent.com');
60
+ console.log(`✓ nonexistent.com → ${url}`);
61
+ console.assert(url === null, 'Should return null for unregistered domain');
62
+
63
+ console.log('\n✅ All tests passed!\n');
64
+ });
package/index.js CHANGED
@@ -367,41 +367,32 @@ class Roster {
367
367
  }
368
368
 
369
369
  /**
370
- * Get the local URL for a domain when running in local mode
371
- * @param {string} domain - The domain name (e.g., 'example.com')
372
- * @param {Object} options - Optional configuration
373
- * @param {number} options.minLocalPort - Minimum port range (default: 4000)
374
- * @param {number} options.maxLocalPort - Maximum port range (default: 9999)
375
- * @returns {string} The local URL (e.g., 'http://localhost:4321')
376
- */
377
- static getLocalUrl(domain, options = {}) {
378
- const minPort = options.minLocalPort || 4000;
379
- const maxPort = options.maxLocalPort || 9999;
380
-
381
- // Remove www prefix if present
382
- const cleanDomain = domain.startsWith('www.') ? domain.slice(4) : domain;
383
-
384
- // Calculate deterministic port
385
- const port = domainToPort(cleanDomain, minPort, maxPort);
386
-
387
- return `http://localhost:${port}`;
388
- }
389
-
390
- /**
391
- * Get the local URL for a domain that was registered on this instance
370
+ * Get the URL for a domain based on the current environment
392
371
  * @param {string} domain - The domain name
393
- * @returns {string|null} The local URL if found, null otherwise
372
+ * @returns {string|null} The URL if domain is registered, null otherwise
394
373
  */
395
- getLocalUrl(domain) {
374
+ getUrl(domain) {
396
375
  // Remove www prefix if present
397
376
  const cleanDomain = domain.startsWith('www.') ? domain.slice(4) : domain;
398
377
 
399
- // Check if domain has a port assigned
400
- if (this.domainPorts && this.domainPorts[cleanDomain]) {
401
- return `http://localhost:${this.domainPorts[cleanDomain]}`;
378
+ // Check if domain is registered
379
+ const isRegistered = this.sites[cleanDomain] || this.sites[`www.${cleanDomain}`];
380
+ if (!isRegistered) {
381
+ return null;
402
382
  }
403
383
 
404
- return null;
384
+ // Return URL based on environment
385
+ if (this.local) {
386
+ // Local mode: return localhost URL with assigned port
387
+ if (this.domainPorts && this.domainPorts[cleanDomain]) {
388
+ return `http://localhost:${this.domainPorts[cleanDomain]}`;
389
+ }
390
+ return null;
391
+ } else {
392
+ // Production mode: return HTTPS URL
393
+ const port = this.defaultPort === 443 ? '' : `:${this.defaultPort}`;
394
+ return `https://${cleanDomain}${port}`;
395
+ }
405
396
  }
406
397
 
407
398
  createVirtualServer(domain) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "roster-server",
3
- "version": "1.9.4",
3
+ "version": "1.9.8",
4
4
  "description": "👾 RosterServer - A domain host router to host multiple HTTPS.",
5
5
  "main": "index.js",
6
6
  "scripts": {
@@ -0,0 +1,260 @@
1
+ # RosterServer Skill
2
+
3
+ ## Overview
4
+
5
+ RosterServer is a virtual hosting platform for multiple HTTPS sites with automatic SSL via Let's Encrypt. Each domain gets an isolated `VirtualServer` instance to prevent configuration conflicts between different application types (Express, Socket.IO, etc).
6
+
7
+ ## Quick Setup
8
+
9
+ ### Production
10
+ ```javascript
11
+ const Roster = require('roster-server');
12
+
13
+ const roster = new Roster({
14
+ email: 'admin@example.com',
15
+ wwwPath: '/srv/www',
16
+ greenlockStorePath: '/srv/greenlock.d',
17
+ staging: false // Use true for testing
18
+ });
19
+
20
+ roster.start();
21
+ ```
22
+
23
+ ### Local Development
24
+ ```javascript
25
+ const roster = new Roster({
26
+ local: true, // HTTP mode, no SSL
27
+ wwwPath: './www',
28
+ minLocalPort: 4000, // Optional
29
+ maxLocalPort: 9999 // Optional
30
+ });
31
+
32
+ roster.start().then(() => {
33
+ console.log('example.com:', roster.getUrl('example.com'));
34
+ // → http://localhost:9465 (deterministic CRC32-based port)
35
+ });
36
+ ```
37
+
38
+ ## Directory Structure
39
+
40
+ ```
41
+ project/
42
+ ├── greenlock.d/ # SSL certificates (auto-generated)
43
+ ├── www/
44
+ │ ├── example.com/
45
+ │ │ └── index.js # Handler for example.com
46
+ │ └── api.example.com/
47
+ │ └── index.js # Handler for subdomain
48
+ └── server.js # Your setup
49
+ ```
50
+
51
+ ## Handler Patterns
52
+
53
+ Each `www/{domain}/index.js` must export a function that receives `httpsServer` and returns a request handler.
54
+
55
+ ### Pattern 1: Basic HTTP Handler
56
+ ```javascript
57
+ module.exports = (httpsServer) => {
58
+ return (req, res) => {
59
+ res.writeHead(200, { 'Content-Type': 'text/plain' });
60
+ res.end('Hello World');
61
+ };
62
+ };
63
+ ```
64
+
65
+ ### Pattern 2: Express App
66
+ ```javascript
67
+ const express = require('express');
68
+
69
+ module.exports = (httpsServer) => {
70
+ const app = express();
71
+
72
+ app.get('/', (req, res) => res.send('Hello'));
73
+ app.post('/api/data', (req, res) => res.json({ ok: true }));
74
+
75
+ return app;
76
+ };
77
+ ```
78
+
79
+ ### Pattern 3: Socket.IO
80
+ ```javascript
81
+ const { Server } = require('socket.io');
82
+
83
+ module.exports = (httpsServer) => {
84
+ const io = new Server(httpsServer);
85
+
86
+ io.on('connection', (socket) => {
87
+ socket.on('message', (data) => io.emit('message', data));
88
+ });
89
+
90
+ return (req, res) => {
91
+ if (req.url && req.url.startsWith(io.opts.path)) return;
92
+ res.writeHead(200);
93
+ res.end('Socket.IO running');
94
+ };
95
+ };
96
+ ```
97
+
98
+ ### Pattern 4: Manual Registration
99
+ ```javascript
100
+ // In server.js, before roster.start()
101
+ roster.register('example.com', (httpsServer) => {
102
+ return (req, res) => {
103
+ res.writeHead(200);
104
+ res.end('Manual handler');
105
+ };
106
+ });
107
+
108
+ // With custom port
109
+ roster.register('api.example.com:8443', handler);
110
+ ```
111
+
112
+ ## Key Configuration Options
113
+
114
+ ```javascript
115
+ new Roster({
116
+ email: 'admin@example.com', // Required for SSL
117
+ wwwPath: '/srv/www', // Site handlers directory
118
+ greenlockStorePath: '/srv/greenlock.d', // SSL storage
119
+
120
+ // Environment
121
+ local: false, // true = HTTP, false = HTTPS
122
+ staging: false, // true = Let's Encrypt staging
123
+
124
+ // Server
125
+ hostname: '0.0.0.0',
126
+ port: 443, // Default HTTPS port (NOT 80!)
127
+
128
+ // Local mode
129
+ minLocalPort: 4000,
130
+ maxLocalPort: 9999,
131
+
132
+ // Advanced
133
+ filename: 'index', // Handler filename (no extension)
134
+ basePath: '/srv' // Base for relative paths
135
+ })
136
+ ```
137
+
138
+ ## Core API
139
+
140
+ ### `roster.start()`
141
+ Loads sites, generates SSL config, starts servers. Returns `Promise<void>`.
142
+
143
+ ### `roster.register(domain, handler)`
144
+ Manually register a domain handler. Domain can include port: `'api.com:8443'`.
145
+
146
+ ### `roster.getUrl(domain)`
147
+ Get environment-aware URL:
148
+ - Local mode: `http://localhost:{port}`
149
+ - Production: `https://{domain}` or `https://{domain}:{port}`
150
+ - Returns `null` if domain not registered
151
+
152
+ ## How It Works
153
+
154
+ ### Request Flow
155
+ 1. Request arrives → Dispatcher extracts `Host` header
156
+ 2. Strips `www.` prefix (301 redirect if present)
157
+ 3. Looks up domain → Gets `VirtualServer` instance
158
+ 4. Routes to handler via `virtualServer.processRequest(req, res)`
159
+
160
+ ### VirtualServer Architecture
161
+ Each domain gets isolated server instance that simulates `http.Server`:
162
+ - Captures `request` and `upgrade` event listeners
163
+ - Complete separation between domains
164
+ - No configuration conflicts between apps
165
+
166
+ ### Port Assignment
167
+ **Production**: Default 443, custom via `domain:port` syntax
168
+ **Local**: CRC32 hash of domain → deterministic port in range 4000-9999
169
+ **Reserved**: Port 80 for ACME challenges only
170
+
171
+ ### SSL Management
172
+ - Automatic Let's Encrypt certificate generation
173
+ - Auto-renewal 45 days before expiration
174
+ - SNI support for multiple domains
175
+ - Custom ports reuse certificates via SNI callback
176
+
177
+ ## Common Issues & Solutions
178
+
179
+ **Port 443 in use**: Use different port `{ port: 8443 }`
180
+ **Certificate failed**: Check firewall (ports 80, 443), verify DNS, try `staging: true`
181
+ **Site not found**: Verify directory name matches domain, check `index.js` exports function
182
+ **Local port conflict**: Adjust `minLocalPort`/`maxLocalPort` range
183
+ **Socket.IO not working**: Ensure handler checks `io.opts.path` and returns properly
184
+
185
+ ## Best Practices
186
+
187
+ 1. **Test with staging first**: `staging: true` to avoid Let's Encrypt rate limits
188
+ 2. **Use local mode for dev**: `local: true` for faster iteration
189
+ 3. **Environment variables**: Configure via `process.env` for portability
190
+ 4. **Error handling**: Wrap handlers with try/catch, don't expose internals
191
+ 5. **Socket.IO paths**: Always check `req.url.startsWith(io.opts.path)` in returned handler
192
+ 6. **Port 80**: Never use as HTTPS port (reserved for ACME)
193
+
194
+ ## Quick Examples
195
+
196
+ ### Full Production Setup
197
+ ```javascript
198
+ const Roster = require('roster-server');
199
+
200
+ const roster = new Roster({
201
+ email: process.env.ADMIN_EMAIL,
202
+ wwwPath: '/srv/www',
203
+ greenlockStorePath: '/srv/greenlock.d',
204
+ staging: process.env.NODE_ENV !== 'production'
205
+ });
206
+
207
+ roster.start().then(() => {
208
+ console.log('RosterServer running');
209
+ }).catch(err => {
210
+ console.error('Startup failed:', err);
211
+ process.exit(1);
212
+ });
213
+ ```
214
+
215
+ ### Local Dev with Manual Registration
216
+ ```javascript
217
+ const roster = new Roster({ local: true, wwwPath: './www' });
218
+
219
+ roster.register('test.local', (server) => {
220
+ return (req, res) => {
221
+ res.writeHead(200, { 'Content-Type': 'application/json' });
222
+ res.end(JSON.stringify({ status: 'ok', url: roster.getUrl('test.local') }));
223
+ };
224
+ });
225
+
226
+ roster.start();
227
+ ```
228
+
229
+ ### Environment-Aware Configuration
230
+ ```javascript
231
+ const isProduction = process.env.NODE_ENV === 'production';
232
+
233
+ const roster = new Roster({
234
+ email: process.env.ADMIN_EMAIL || 'admin@example.com',
235
+ wwwPath: process.env.WWW_PATH || './www',
236
+ greenlockStorePath: process.env.SSL_PATH || './greenlock.d',
237
+ local: !isProduction,
238
+ staging: !isProduction,
239
+ minLocalPort: parseInt(process.env.MIN_PORT) || 4000,
240
+ maxLocalPort: parseInt(process.env.MAX_PORT) || 9999
241
+ });
242
+
243
+ roster.start();
244
+ ```
245
+
246
+ ## Implementation Checklist
247
+
248
+ When implementing RosterServer:
249
+
250
+ - [ ] Create `www/` directory structure with domain folders
251
+ - [ ] Each domain has `index.js` exporting `(httpsServer) => handler`
252
+ - [ ] Configure email for Let's Encrypt notifications
253
+ - [ ] Test with `local: true` first
254
+ - [ ] Test with `staging: true` before production
255
+ - [ ] Ensure ports 80 and 443 are open (production)
256
+ - [ ] Verify DNS points to server
257
+ - [ ] Never use port 80 as HTTPS port
258
+ - [ ] Use `roster.getUrl(domain)` for environment-aware URLs
259
+ - [ ] Handle Socket.IO paths correctly in returned handler
260
+ - [ ] Implement error handling in handlers