šŸŽ© You're Invited:Meet the Socket team at Black Hat in Las Vegas, August 3-6.RSVP →
Sign In

roster-server

Package Overview
Dependencies
Maintainers
1
Versions
84
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

roster-server - npm Package Compare versions

Comparing version
2.4.2
to
2.4.4
.greenlockrc

Sorry, the diff of this file is not supported yet

+84
const cluster = require('cluster');
const os = require('os');
const path = require('path');
const https = require('https');
const Roster = require('../index.js');
// Change these values to your target domain and HTTPS port.
const CONFIG = {
domain: 'chinchon.blyts.com',
httpsPort: 4336,
workers: Math.max(1, Math.min(2, os.cpus().length)),
wwwPath: path.join(__dirname, 'www'),
greenlockStorePath: path.join(__dirname, '..', 'greenlock.d'),
email: 'mclasen@blyts.com',
staging: false,
autoCertificates: true
};
async function startWorker() {
const roster = new Roster({
local: false,
email: CONFIG.email,
staging: CONFIG.staging,
wwwPath: CONFIG.wwwPath,
greenlockStorePath: CONFIG.greenlockStorePath,
autoCertificates: CONFIG.autoCertificates
});
// Domain is configured from CONFIG (single source of truth).
roster.register(CONFIG.domain, () => {
return (req, res) => {
res.writeHead(200, { 'Content-Type': 'text/plain' });
res.end(`HTTPS cluster response from worker ${process.pid} for ${CONFIG.domain}`);
};
});
await roster.init();
// Automatic cert issuance by roster-server (and renewal loop) in cluster-friendly mode.
const defaultPems = await roster.ensureCertificate(CONFIG.domain);
const server = https.createServer({
key: defaultPems.key,
cert: defaultPems.cert,
SNICallback: roster.sniCallback(),
minVersion: 'TLSv1.2',
maxVersion: 'TLSv1.3'
});
roster.attach(server);
server.listen(CONFIG.httpsPort, () => {
console.log(`[worker ${process.pid}] listening on https://0.0.0.0:${CONFIG.httpsPort} for domain ${CONFIG.domain}`);
});
}
async function startPrimary() {
console.log(`\n[primary ${process.pid}] starting ${CONFIG.workers} workers`);
console.log(`domain=${CONFIG.domain} port=${CONFIG.httpsPort}`);
console.log(`wwwPath=${CONFIG.wwwPath}`);
console.log(`greenlockStorePath=${CONFIG.greenlockStorePath}\n`);
console.log(`[primary] cert lifecycle managed by roster-server (autoCertificates=${CONFIG.autoCertificates})\n`);
for (let i = 0; i < CONFIG.workers; i++) {
cluster.fork();
}
cluster.on('exit', (worker) => {
console.log(`[primary] worker ${worker.process.pid} exited, restarting...`);
cluster.fork();
});
}
if (cluster.isPrimary) {
startPrimary().catch((err) => {
console.error(`[primary ${process.pid}] startup failed`, err);
process.exit(1);
});
} else {
startWorker().catch((err) => {
console.error(`[worker ${process.pid}] startup failed`, err);
process.exit(1);
});
}
---
id: eyap8usul0
type: decision
title: 'Decision update: autoCertificates default true'
created: '2026-03-20 23:53:16'
---
# Decision update: autoCertificates default true
**What changed**: `autoCertificates` now defaults to `true` instead of `false`.
**Why**: User expectation and product value are that SSL lifecycle should be automatic and robust by default in RosterServer.
**Implementation**:
- Constructor default changed in `index.js` (`parseBooleanFlag(options.autoCertificates, true)`).
- Added constructor tests for default true and explicit opt-out (`autoCertificates: false`).
- Updated docs (`README.md`, `skills/roster-server/SKILL.md`) to reflect default-on behavior.
**Guardrails**:
- Users can still opt out with `autoCertificates: false` when certs are externally managed.
- `ensureCertificate()` keeps explicit error message when autoCertificates is disabled.
+157
-66

@@ -271,2 +271,8 @@ const fs = require('fs');

this.skipLocalCheck = parseBooleanFlag(options.skipLocalCheck, true);
this.autoCertificates = parseBooleanFlag(options.autoCertificates, true);
this.certificateRenewIntervalMs = Number.isFinite(Number(options.certificateRenewIntervalMs))
? Math.max(60000, Number(options.certificateRenewIntervalMs))
: 12 * 60 * 60 * 1000;
this._greenlockRuntime = null;
this._certificateRenewTimer = null;

@@ -779,15 +785,156 @@ const port = options.port === undefined ? 443 : options.port;

this._sniCallback = (servername, callback) => {
const normalizedServername = this._normalizeHostInput(servername).trim().toLowerCase();
try {
const pems = this._resolvePemsForServername(servername);
const pems = this._resolvePemsForServername(normalizedServername);
if (pems) {
callback(null, tls.createSecureContext({ key: pems.key, cert: pems.cert }));
} else {
callback(new Error(`No certificate files available for ${servername}`));
return;
}
} catch (error) {
callback(error);
return;
}
// Cluster-friendly automatic issuance path (no internal listen lifecycle).
if (!this._greenlockRuntime || !normalizedServername) {
callback(new Error(`No certificate files available for ${servername}`));
return;
}
this._greenlockRuntime.get({ servername: normalizedServername })
.then(() => {
const issued = this._resolvePemsForServername(normalizedServername);
if (issued) {
callback(null, tls.createSecureContext({ key: issued.key, cert: issued.cert }));
} else {
callback(new Error(`No certificate files available for ${servername}`));
}
})
.catch((error) => {
callback(error);
});
};
}
_buildGreenlockOptions() {
return {
packageRoot: __dirname,
configDir: this.greenlockStorePath,
maintainerEmail: this.email,
cluster: this.cluster,
staging: this.staging,
skipDryRun: this.skipLocalCheck,
skipChallengeTest: this.skipLocalCheck,
notify: (event, details) => {
const eventDomain = (() => {
if (!details || typeof details !== 'object') return null;
const directKeys = ['subject', 'servername', 'domain', 'hostname', 'host'];
for (const key of directKeys) {
if (typeof details[key] === 'string' && details[key].trim()) {
return details[key].trim().toLowerCase();
}
}
if (Array.isArray(details.altnames) && details.altnames.length > 0) {
const alt = details.altnames.find(name => typeof name === 'string' && name.trim());
if (alt) return alt.trim().toLowerCase();
}
if (Array.isArray(details.domains) && details.domains.length > 0) {
const domain = details.domains.find(name => typeof name === 'string' && name.trim());
if (domain) return domain.trim().toLowerCase();
}
if (details.identifier && typeof details.identifier.value === 'string' && details.identifier.value.trim()) {
return details.identifier.value.trim().toLowerCase();
}
return null;
})();
let msg;
if (typeof details === 'string') {
msg = details;
} else if (details instanceof Error) {
msg = details.stack || details.message;
} else if (details && typeof details === 'object' && typeof details.message === 'string') {
msg = details.message;
} else {
try {
msg = JSON.stringify(details);
} catch {
msg = String(details);
}
}
if (!msg || msg === 'undefined') msg = `[${event}] (no details)`;
if (eventDomain && !msg.includes(`[${eventDomain}]`)) {
msg = `[${eventDomain}] ${msg}`;
}
if (event === 'warning' && typeof msg === 'string') {
if (/acme-dns-01-cli.*(incorrect function signatures|deprecated use of callbacks)/i.test(msg)) return;
if (/dns-01 challenge plugin should have zones/i.test(msg)) return;
}
if (event === 'error') log.error(msg);
else if (event === 'warning') log.warn(msg);
else log.info(msg);
}
};
}
_getManagedCertificateSubjects() {
const uniqueDomains = new Set();
this.domains.forEach((domain) => {
const root = domain.startsWith('*.') ? wildcardRoot(domain) : domain.replace(/^www\./, '');
if (root) uniqueDomains.add(root);
});
const subjects = [];
uniqueDomains.forEach((domain) => {
subjects.push(domain);
const includeWildcard = this.wildcardZones.has(domain) && this.dnsChallenge && !this.combineWildcardCerts;
if (includeWildcard) subjects.push(`*.${domain}`);
});
return [...new Set(subjects)];
}
_startCertificateRenewLoop() {
if (!this._greenlockRuntime || this._certificateRenewTimer) return;
const subjects = this._getManagedCertificateSubjects();
if (subjects.length === 0) return;
this._certificateRenewTimer = setInterval(() => {
subjects.forEach((subject) => {
this._greenlockRuntime.get({ servername: subject }).catch((error) => {
log.warn(`āš ļø Certificate renew check failed for ${subject}: ${error?.message || error}`);
});
});
}, this.certificateRenewIntervalMs);
if (typeof this._certificateRenewTimer.unref === 'function') {
this._certificateRenewTimer.unref();
}
}
async ensureCertificate(servername) {
if (this.local) {
throw new Error('ensureCertificate() is not available in local mode');
}
if (!this._initialized) {
throw new Error('Call init() before ensureCertificate()');
}
const normalizedServername = this._normalizeHostInput(servername).trim().toLowerCase();
if (!normalizedServername) {
throw new Error('servername is required');
}
let pems = this._resolvePemsForServername(normalizedServername);
if (pems) return pems;
if (!this._greenlockRuntime) {
throw new Error('autoCertificates is disabled; enable { autoCertificates: true } to issue certificates automatically');
}
await this._greenlockRuntime.get({ servername: normalizedServername });
pems = this._resolvePemsForServername(normalizedServername);
if (!pems) {
throw new Error(`Certificate issuance completed but no PEM files were found for ${normalizedServername}`);
}
return pems;
}
async init() {

@@ -798,2 +945,5 @@ if (this._initialized) return this;

this.generateConfigJson();
if (this.autoCertificates) {
this._greenlockRuntime = GreenlockShim.create(this._buildGreenlockOptions());
}
}

@@ -803,2 +953,5 @@ this._initSiteHandlers();

this._initSniResolver();
if (this.autoCertificates) {
this._startCertificateRenewLoop();
}
}

@@ -898,65 +1051,3 @@ this._initialized = true;

const greenlockOptions = {
packageRoot: __dirname,
configDir: this.greenlockStorePath,
maintainerEmail: this.email,
cluster: this.cluster,
staging: this.staging,
skipDryRun: this.skipLocalCheck,
skipChallengeTest: this.skipLocalCheck,
notify: (event, details) => {
const eventDomain = (() => {
if (!details || typeof details !== 'object') return null;
const directKeys = ['subject', 'servername', 'domain', 'hostname', 'host'];
for (const key of directKeys) {
if (typeof details[key] === 'string' && details[key].trim()) {
return details[key].trim().toLowerCase();
}
}
if (Array.isArray(details.altnames) && details.altnames.length > 0) {
const alt = details.altnames.find(name => typeof name === 'string' && name.trim());
if (alt) return alt.trim().toLowerCase();
}
if (Array.isArray(details.domains) && details.domains.length > 0) {
const domain = details.domains.find(name => typeof name === 'string' && name.trim());
if (domain) return domain.trim().toLowerCase();
}
if (details.identifier && typeof details.identifier.value === 'string' && details.identifier.value.trim()) {
return details.identifier.value.trim().toLowerCase();
}
return null;
})();
let msg;
if (typeof details === 'string') {
msg = details;
} else if (details instanceof Error) {
msg = details.stack || details.message;
} else if (details && typeof details === 'object' && typeof details.message === 'string') {
msg = details.message;
} else {
try {
msg = JSON.stringify(details);
} catch {
msg = String(details);
}
}
if (!msg || msg === 'undefined') msg = `[${event}] (no details)`;
if (eventDomain && !msg.includes(`[${eventDomain}]`)) {
msg = `[${eventDomain}] ${msg}`;
}
if (event === 'warning' && typeof msg === 'string') {
if (/acme-dns-01-cli.*(incorrect function signatures|deprecated use of callbacks)/i.test(msg)) return;
if (/dns-01 challenge plugin should have zones/i.test(msg)) return;
}
if (event === 'error') log.error(msg);
else if (event === 'warning') log.warn(msg);
else log.info(msg);
}
};
const greenlockOptions = this._buildGreenlockOptions();
const greenlockRuntime = GreenlockShim.create(greenlockOptions);

@@ -963,0 +1054,0 @@ const greenlock = Greenlock.init({

{
"name": "roster-server",
"version": "2.4.2",
"version": "2.4.4",
"description": "šŸ‘¾ RosterServer - A domain host router to host multiple HTTPS.",

@@ -5,0 +5,0 @@ "main": "index.js",

@@ -250,2 +250,4 @@ # šŸ‘¾ RosterServer

- `staging` (boolean): Set to `true` to use Let's Encrypt's staging environment (for testing).
- `autoCertificates` (boolean): Enables automatic certificate issuance and renewal in production lifecycle. **Default: `true`**. Set to `false` only if certificates are managed externally.
- `certificateRenewIntervalMs` (number): Renewal check interval when `autoCertificates` is enabled (minimum 60s, default 12h).
- `local` (boolean): Set to `true` to run in local development mode.

@@ -404,3 +406,3 @@ - `minLocalPort` (number): Minimum port for local mode (default: 4000).

Returns a TLS SNI callback that resolves certificates from `greenlockStorePath` on disk. Not available in local mode.
Returns a TLS SNI callback. It resolves certificates from `greenlockStorePath` and, when `autoCertificates` is enabled (default), can issue missing certificates automatically. Not available in local mode.

@@ -407,0 +409,0 @@ #### `roster.attach(server, { port }?)` → `Roster`

@@ -192,4 +192,7 @@ ---

### `roster.sniCallback()`
Returns `(servername, callback) => void` TLS SNI callback that resolves certs from `greenlockStorePath`. Production mode only. Requires `init()` first.
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.
### `roster.ensureCertificate(servername)`
Forces certificate availability for a domain and returns `{ key, cert }`. With `autoCertificates` enabled (default), it issues certs automatically when missing.
### `roster.attach(server, { port }?)`

@@ -196,0 +199,0 @@ Convenience: wires `requestHandler` + `upgradeHandler` onto an external `http.Server` or `https.Server`. Returns `this`. Requires `init()` first.

@@ -321,2 +321,10 @@ 'use strict';

});
it('defaults autoCertificates to true', () => {
const roster = new Roster({ local: false });
assert.strictEqual(roster.autoCertificates, true);
});
it('allows disabling autoCertificates explicitly', () => {
const roster = new Roster({ local: false, autoCertificates: false });
assert.strictEqual(roster.autoCertificates, false);
});
});

@@ -905,2 +913,26 @@

describe('Roster ensureCertificate()', () => {
it('throws if called before init()', async () => {
const roster = new Roster({ local: false, autoCertificates: true });
await assert.rejects(() => roster.ensureCertificate('example.com'), /Call init\(\) before ensureCertificate/);
});
it('throws in local mode', async () => {
const roster = new Roster({ local: true, autoCertificates: true });
roster.register('local-cert.example', () => () => {});
await roster.init();
await assert.rejects(() => roster.ensureCertificate('local-cert.example'), /not available in local mode/);
});
it('throws when autoCertificates is disabled and cert is missing', async () => {
const roster = new Roster({ local: false, autoCertificates: false });
roster.register('missing-cert.example', () => () => {});
await roster.init();
await assert.rejects(
() => roster.ensureCertificate('missing-cert.example'),
/autoCertificates is disabled/
);
});
});
describe('Roster attach()', () => {

@@ -907,0 +939,0 @@ it('throws if called before init()', () => {

const https = require('https');
const fs = require('fs');
const path = require('path');
const Roster = require('../index.js');
function hasCertificateFor(storePath, subject) {
const base = path.join(storePath, 'live', subject);
return (
fs.existsSync(path.join(base, 'privkey.pem')) &&
fs.existsSync(path.join(base, 'cert.pem')) &&
fs.existsSync(path.join(base, 'chain.pem'))
);
}
async function main() {
// In this demo, certs are expected to already exist in greenlock.d/live/<subject>.
// init()+attach() do routing only; no ACME issuance/listen lifecycle is started.
const greenlockStorePath = process.env.GREENLOCK_STORE_PATH || path.join(__dirname, '..', 'greenlock.d');
const listenPort = Number(process.env.DEMO_HTTPS_PORT || 19643);
const roster = new Roster({
local: false,
email: process.env.ADMIN_EMAIL || 'admin@example.com',
greenlockStorePath
});
roster.register('example.com', () => {
return (req, res) => {
res.writeHead(200, { 'Content-Type': 'text/plain' });
res.end('hello from external HTTPS worker (default port routes)');
};
});
roster.register('api.example.com:9443', () => {
return (req, res) => {
res.writeHead(200, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ ok: true, source: 'api.example.com:9443 route table' }));
};
});
await roster.init();
const missing = ['example.com', '*.example.com'].filter((subject) => !hasCertificateFor(greenlockStorePath, subject));
if (missing.length > 0) {
console.log('\nāš ļø Missing certificate files for:');
missing.forEach((subject) => console.log(` - ${subject}`));
console.log('\nExpected files:');
console.log(' greenlock.d/live/<subject>/privkey.pem');
console.log(' greenlock.d/live/<subject>/cert.pem');
console.log(' greenlock.d/live/<subject>/chain.pem');
console.log('\nTip: run regular roster.start() once (or your cert manager) to issue certs first.\n');
}
const server = https.createServer({
SNICallback: roster.sniCallback(),
minVersion: 'TLSv1.2',
maxVersion: 'TLSv1.3'
});
// Attach default routing table (defaultPort = 443 routes)
roster.attach(server);
await new Promise((resolve, reject) => {
server.listen(listenPort, '0.0.0.0', resolve);
server.on('error', reject);
});
console.log('\nāœ… HTTPS cluster-friendly demo running');
console.log(`Listening on :${listenPort} with external server ownership`);
console.log('Roster only provides SNI + vhost routing (no internal listen/bootstrap).\n');
console.log('Try:');
console.log(`curl -k --resolve example.com:${listenPort}:127.0.0.1 https://example.com:${listenPort}/`);
console.log(`curl -k --resolve www.example.com:${listenPort}:127.0.0.1 https://www.example.com:${listenPort}/foo`);
console.log('\nFor custom route table port=9443, attach another server:');
console.log(' const srv9443 = https.createServer({ SNICallback: roster.sniCallback() });');
console.log(' roster.attach(srv9443, { port: 9443 });');
console.log(' srv9443.listen(19644);');
console.log(` curl -k --resolve api.example.com:19644:127.0.0.1 https://api.example.com:19644/`);
// Sticky-session runtime pattern:
// process.on('message', (msg, socket) => {
// if (msg === 'sticky-session:connection') server.emit('connection', socket);
// });
}
main().catch((err) => {
console.error('Demo failed:', err);
process.exit(1);
});