roster-server 1.9.6 → 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 +8 -0
- package/package.json +1 -1
- package/skills/roster-server/SKILL.md +260 -0
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
|
package/package.json
CHANGED
|
@@ -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
|