/** * Network provider registry (DC-140, specs/PRIVATE-NETWORK-SPEC.md §3). * * Single resolution point between feature modules and providers: no feature * code may import a provider module directly — they call getActiveProvider(). * * State is deliberately process-local and defaults to INACTIVE: no provider * is active until the setup wizard (DC-152) connects one. Until then, * getActiveProvider()/getCapabilities() throw ProviderNotActiveError. * * registerProvider(factory) — factory is a function ({ getCoordClient, * localManager, execFileImpl } = {}) → provider instance. The tailscale * factory defaults to lazily binding the existing singletons * (src/context getCoordClient pattern + src/managers/tailscale-manager), * but every dependency is injectable so tests stay hermetic. */ 'use strict'; const { ProviderNotActiveError } = require('./errors'); const { CONTRACT_METHODS } = require('./provider'); // id → factory(provider deps) → provider instance const _factories = new Map(); let _activeId = null; let _activeProvider = null; /** Lately-bound defaults (required lazily so jest.mock stays effective). */ function _defaultDeps() { /* eslint-disable global-require -- lazy on purpose: keeps registry require-time side effects at zero and respects jest module mocks. */ return { localManager: require('../managers/tailscale-manager'), execFileImpl: null, }; } /** * Register a provider factory under its id. * @param {string} id - provider id, e.g. 'tailscale' (must match provider.id()) * @param {Function} factory - (deps) => provider instance * @returns {void} */ function registerProvider(id, factory) { if (typeof id !== 'string' || !id) { throw new TypeError('registerProvider: id must be a non-empty string'); } if (typeof factory !== 'function') { throw new TypeError('registerProvider: factory must be a function'); } _factories.set(id, factory); // If this id is currently active, drop the cached instance so the next // getActiveProvider() rebuilds from the (possibly new) factory. if (_activeId === id) _activeProvider = null; } /** Build (or rebuild) the instance for `_activeId` from its factory. */ function _buildActive() { const factory = _factories.get(_activeId); const provider = factory ? factory(_defaultDeps()) : null; if (!_isContractProvider(provider)) { throw new Error('Provider "' + _activeId + '" does not satisfy the NetworkProvider contract'); } _activeProvider = provider; return _activeProvider; } /** * True when the id refers to a registered provider whose instances satisfy * the NetworkProvider contract surface (all CONTRACT_METHODS present). */ function _isContractProvider(p) { if (!p) return false; return CONTRACT_METHODS.every((m) => typeof p[m] === 'function'); } /** * Activate a registered provider. Rebuilding the instance resets any state * the previous one carried. * @param {string} id * @returns {object} the newly active provider instance * @throws {Error} when the id is not registered, or the instance built from * it does not satisfy the documented contract. */ function setActiveProvider(id) { const factory = _factories.get(id); if (!factory) { throw new Error('Unknown network provider: ' + String(id)); } const provider = factory(_defaultDeps()); if (!_isContractProvider(provider)) { throw new Error('Provider "' + id + '" does not satisfy the NetworkProvider contract'); } if (typeof provider.id !== 'function' || provider.id() !== id) { throw new Error('Provider "' + id + '" reports a different id'); } _activeId = id; _activeProvider = provider; return _activeProvider; } /** * The active provider instance. Feature modules' ONLY entry point into * provider functionality. * @throws {ProviderNotActiveError} when no provider has been activated */ function getActiveProvider() { if (!_activeId) { throw new ProviderNotActiveError( 'No active network provider. Complete the Private Network setup wizard first.' ); } // Factory may have been re-registered since activation → rebuild lazily. return _activeProvider || _buildActive(); } /** * Capability flags of the active provider (spec §3): feature modules check * these and degrade cleanly when a capability is absent. * @returns {{invites:boolean, splitDnsApi:boolean, perPeerFirewall:boolean, selfHosted:boolean}} * @throws {ProviderNotActiveError} when no provider has been activated */ function getCapabilities() { return getActiveProvider().capabilities(); } /** List registered provider ids (setup wizard mode picker). */ function listProviderIds() { return [..._factories.keys()]; } /** Deactivate (setup wizard disconnect / tests). Next getActiveProvider() throws. */ function reset() { _activeId = null; _activeProvider = null; } /** Currently active provider id, or null — read-only introspection. */ function getActiveProviderId() { return _activeId; } module.exports = { registerProvider, setActiveProvider, getActiveProvider, getCapabilities, listProviderIds, getActiveProviderId, reset, };