Package
hub-server
HTTP routes, device storage, and the Hub event bus used by the Hub backend.
deviceRoutes.js — device lifecycle routes
'use strict';
const crypto = require('crypto');
const jwt = require('jsonwebtoken');
// Admin rate limiter (module-level)
const _adminFailWindows = new Map();
function _adminCheckRate(ip) { /* sliding-window check */ }
function _adminRecordFail(ip) { /* increment */ }
function registerDeviceRoutes(app, getDeviceStore, { certManager, issueDeviceJWT, verifyDeviceJWT, deviceJwtTtl, emitHubEvent, tenantSlug, patStore = null, isDeviceAutoApprove, pubClient, rateLimiters = {} } = {}) {
// Challenge store: single-use, auto-expiry
const _deviceChallenges = new Map();
const CHALLENGE_TTL_MS = 60_000;
function _createChallenge(deviceId) { /* generate nonce */ }
function _consumeChallenge(deviceId, nonce) { /* validate and delete */ }
app.post('/api/devices/register', (req, res) => {
const deviceStore = getDeviceStore();
if (!deviceStore) return res.status(503).json({ error: 'Device auth not available' });
const { displayName, clientToken, publicKeySPKI } = req.body || {};
// Cert path vs token path handling ...
});
app.get('/api/devices/:deviceId/challenge', (req, res) => {
const deviceStore = getDeviceStore();
const { deviceId } = req.params;
// returns { nonce, expiresAt }
});
app.post('/api/devices/:deviceId/authenticate', async (req, res) => {
const { deviceId } = req.params;
const { nonce, signature } = req.body || {};
if (!_consumeChallenge(deviceId, nonce)) return res.status(401).json({ error: 'Challenge expired or invalid' });
const device = deviceStore.verifyDeviceChallenge(deviceId, nonce, signature);
if (!device) return res.status(403).json({ error: 'Signature verification failed' });
const token = issueDeviceJWT(device);
res.json({ token, expiresIn: deviceJwtTtl, role: device.role });
});
console.log('[device] Device routes registered.');
}
module.exports = { registerDeviceRoutes };
deviceStore.js — SQLite-backed device store
const path = require('path');
const fs = require('fs');
const crypto = require('crypto');
const DB_PATH = process.env.DEVICE_DB_PATH || path.join(__dirname, 'data', 'devices.db');
class DeviceStore {
constructor() {
const Database = require('better-sqlite3');
fs.mkdirSync(path.dirname(DB_PATH), { recursive: true });
this.db = new Database(DB_PATH);
this.db.pragma('journal_mode = WAL');
this._migrate();
}
_hashToken(raw) {
const salt = crypto.randomBytes(16).toString('hex');
const key = crypto.pbkdf2Sync(raw, salt, 100000, 32, 'sha256').toString('hex');
return `pbkdf2:${salt}:${key}`;
}
_verifyToken(raw, stored) {
const parts = stored.split(':');
if (parts.length !== 3 || parts[0] !== 'pbkdf2') return false;
const [, salt, expected] = parts;
const actual = crypto.pbkdf2Sync(raw, salt, 100000, 32, 'sha256').toString('hex');
const a = Buffer.from(actual, 'hex');
const e = Buffer.from(expected, 'hex');
return a.length === e.length && crypto.timingSafeEqual(a, e);
}
verifyDeviceChallenge(deviceId, nonce, signatureB64) {
const device = this.getDevice(deviceId);
if (!device || device.revoked || !device.public_key) return null;
try {
const publicKey = crypto.createPublicKey({ key: Buffer.from(device.public_key, 'base64'), format: 'der', type: 'spki' });
const valid = crypto.verify('sha256', Buffer.from(nonce), { key: publicKey, dsaEncoding: 'ieee-p1363' }, Buffer.from(signatureB64.replace(/-/g, '+').replace(/_/g, '/'), 'base64'));
if (!valid) return null;
} catch { return null; }
this.db.prepare(`UPDATE devices SET last_seen = ? WHERE id = ?`).run(Date.now(), deviceId);
const { token_hash, ...rest } = device;
return rest;
}
}
module.exports = DeviceStore;
eventBus.js — Hub event emitter
'use strict';
const EventEmitter = require('events');
const eventBus = new EventEmitter();
eventBus.setMaxListeners(100);
function emitHubEvent(name, payload) {
try {
eventBus.emit(name, payload);
} catch (err) {
console.error(`[eventBus] uncaught error in handler for "${name}":`, err);
}
}
module.exports = { eventBus, emitHubEvent };
certManager.js — X.509 device certificate CA
'use strict';
const forge = require('node-forge');
const fs = require('fs');
const crypto = require('crypto');
const CERT_TTL_DAYS = parseInt(process.env.DEVICE_CERT_TTL_DAYS || '365', 10);
const _origPublicKeyToAsn1 = forge.pki.publicKeyToAsn1;
forge.pki.publicKeyToAsn1 = forge.pki.publicKeyToSubjectPublicKeyInfo = function(key) {
if (key && key._ecSpkiAsn1) return key._ecSpkiAsn1;
return _origPublicKeyToAsn1.call(this, key);
};
class CertManager {
constructor() {
this._caKey = null;
this._caCert = null;
this._caCertPem = null;
this._load();
}
_load() {
let certPem = (process.env.DEVICE_CA_CERT || '').replace(/\\n/g, '\n').trim();
if (!certPem && process.env.DEVICE_CA_CERT_FILE) {
try { certPem = fs.readFileSync(process.env.DEVICE_CA_CERT_FILE, 'utf8').trim(); } catch (e) { console.warn('[cert] Could not read DEVICE_CA_CERT_FILE:', e.message); }
}
let keyPem = (process.env.DEVICE_CA_KEY || '').replace(/\\n/g, '\n').trim();
if (!keyPem && process.env.DEVICE_CA_KEY_FILE) {
try { keyPem = fs.readFileSync(process.env.DEVICE_CA_KEY_FILE, 'utf8').trim(); } catch (e) { console.warn('[cert] Could not read DEVICE_CA_KEY_FILE:', e.message); }
}
if (!certPem || !keyPem) {
console.warn('[cert] DEVICE_CA_CERT/DEVICE_CA_KEY not configured — device cert issuance unavailable.');
return;
}
try {
this._caCert = forge.pki.certificateFromPem(certPem);
this._caKey = forge.pki.privateKeyFromPem(keyPem);
this._caCertPem = certPem;
console.log('[cert] CA loaded. Device certificate issuance ready.');
} catch (e) { console.error('[cert] Failed to load CA key/cert:', e.message); }
}
isLoaded() { return !!(this._caKey && this._caCert); }
issueCert(deviceId, tenant, role, publicKeySpkiB64) {
if (!this.isLoaded()) throw new Error('CA not loaded — configure DEVICE_CA_CERT and DEVICE_CA_KEY');
const spkiDer = Buffer.from(publicKeySpkiB64, 'base64');
const spkiAsn1 = forge.asn1.fromDer(forge.util.createBuffer(spkiDer));
const cert = forge.pki.createCertificate();
cert.publicKey = { _ecSpkiAsn1: spkiAsn1 };
cert.serialNumber = crypto.randomBytes(16).toString('hex');
const now = new Date();
const exp = new Date(now); exp.setDate(exp.getDate() + CERT_TTL_DAYS);
cert.validity.notBefore = now; cert.validity.notAfter = exp;
cert.setSubject([
{ name: 'commonName', value: `device:${deviceId}` },
{ name: 'organizationName', value: tenant },
{ name: 'organizationalUnitName', value: role },
]);
cert.setIssuer(this._caCert.subject.attributes);
cert.setExtensions([
{ name: 'basicConstraints', cA: false },
{ name: 'subjectAltName', altNames: [{ type: 2, value: `device.${deviceId}.bafgo.internal` }] },
{ name: 'keyUsage', digitalSignature: true, nonRepudiation: true },
{ name: 'extKeyUsage', clientAuth: true },
{ name: 'authorityKeyIdentifier', keyIdentifier: this._caCert.generateSubjectKeyIdentifier().getBytes() },
]);
cert.sign(this._caKey, forge.md.sha256.create());
const certPem = forge.pki.certificateToPem(cert);
const fingerprint = this.getCertFingerprint(certPem);
return { certPem, fingerprint };
}
verifyCert(certPem) {
if (!this.isLoaded()) return false;
try {
const cert = new crypto.X509Certificate(certPem);
const now = new Date();
if (now < new Date(cert.validFrom) || now > new Date(cert.validTo)) return false;
const caCert = new crypto.X509Certificate(this._caCertPem);
return cert.verify(caCert.publicKey);
} catch { return false; }
}
getCertFingerprint(certPem) {
try {
const b64 = certPem.replace(/-----[^-]+-----/g, '').replace(/\s+/g, '');
const der = Buffer.from(b64, 'base64');
return crypto.createHash('sha256').update(der).digest('hex');
} catch { return ''; }
}
getCaCertPem() { return this._caCertPem; }
}
module.exports = CertManager;
quickconnect/quickConnectRoutes.js — Quick Connect pairing flow
'use strict';
const crypto = require('crypto');
const QC_CODE_ALPHABET = 'BCDFGHJKMNPQRSTVWXYZ23456789';
const QC_CODE_LEN = 6;
const QC_SESSION_TTL = 60; // seconds
function _generateCode() { /* generates XXX-XXX human code */ }
function _hashCode(code) { const normalized = code.replace(/-/g, '').toUpperCase(); return crypto.createHash('sha256').update(normalized).digest('hex'); }
function _isValidJwk(v) { return v && typeof v === 'object' && v.kty === 'EC' && v.crv === 'P-256' && typeof v.x === 'string' && typeof v.y === 'string'; }
// registerQuickConnectRoutes implements POST /initiate, POST /join, polling/status, and card exchange endpoints.
// It stores only hashed codes and ephemeral public keys; session state expires quickly (60s) in Redis.
module.exports = registerQuickConnectRoutes;
sync/* — Sovereign Sync routes and store
'use strict';
const crypto = require('crypto');
const SYNC_SNAPSHOT_MAX_BYTES = parseInt(process.env.SYNC_SNAPSHOT_MAX_BYTES || String(10 * 1024 * 1024), 10);
const SYNC_DELTA_MAX_BYTES = parseInt(process.env.SYNC_DELTA_MAX_BYTES || String(512 * 1024), 10);
function registerSyncRoutes(app, io, syncStore, verifyDeviceJWT) { /* routes: create room, join, deltas, snapshots, history */ }
module.exports = { registerSyncRoutes };
'use strict';
const path = require('path');
const fs = require('fs');
const crypto = require('crypto');
class SyncStore { /* SQLite-backed store: sync_rooms, sync_members, sync_deltas, sync_history_requests, sync_snapshots */ }
module.exports = SyncStore;