Package

hub-server

HTTP routes, device storage, and the Hub event bus used by the Hub backend.

deviceRoutes.js — device lifecycle routes

Registers `/api/devices/*` endpoints: registration, token exchange, challenges, admin actions. Includes in-memory single-use challenges and rate-limiting.

'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

Persistent store with PBKDF2-hashed tokens, pending/active device tables, app registrations, and cert support.

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

Issues and verifies device certificates using node-forge; supports EC SPKI passthrough for P-256 keys.

'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

Short code-based ephemeral pairing protocol with Redis-backed ephemeral sessions and Socket.IO notifications.

'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

Sync REST API implements rooms, members, deltas, snapshots, and history bootstrapping; `SyncStore` is an SQLite-backed implementation.

'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;