/** * Context assembly - Dependency injection container * Assembles all context objects needed by routes */ const { createDockerContext } = require('./docker'); const { createCaddyContext } = require('./caddy'); const { createDnsContext } = require('./dns'); const { createSessionContext } = require('./session'); const NotificationManager = require('../managers/notification-manager'); const tailscaleManager = require('../managers/tailscale-manager'); const { TailscaleCoordClient } = require('../managers/tailscale-coord'); const fs = require('fs'); /** * Assemble the full application context * This replaces the old "god object" ctx with explicit construction */ function assembleContext({ // Config siteConfig, buildDomain, buildServiceUrl, SERVICES_FILE, CONFIG_FILE, TOTP_CONFIG_FILE, TAILSCALE_CONFIG_FILE, NOTIFICATIONS_FILE, ERROR_LOG_FILE, DNS_CREDENTIALS_FILE, CADDYFILE_PATH, CADDY_ADMIN_URL, // State managers servicesStateManager, configStateManager, // DC-053 share store shareStore, // Managers credentialManager, authManager, licenseManager, healthChecker, updateManager, backupManager, resourceMonitor, auditLogger, portLockManager, selfUpdater, dockerMaintenance, logDigest, dockerSecurity, // Templates APP_TEMPLATES, TEMPLATE_CATEGORIES, DIFFICULTY_LEVELS, RECIPE_TEMPLATES, RECIPE_CATEGORIES, // Helpers asyncHandler, errorResponse, ok, fetchT, httpsAgent, log, logError, safeErrorMessage, getServiceById, readConfig, saveConfig, addServiceToConfig, validateURL, strictLimiter, totpConfig, saveTotpConfig, loadSiteConfig, loadNotificationConfig, resyncHealthChecker, // Middleware result middlewareResult, // App app, }) { // Create domain-specific contexts const docker = createDockerContext(dockerSecurity); const caddy = createCaddyContext(CADDYFILE_PATH, CADDY_ADMIN_URL, fetchT, httpsAgent, log, siteConfig, buildDomain); const dns = createDnsContext(siteConfig, buildDomain, credentialManager, fetchT, httpsAgent, log, DNS_CREDENTIALS_FILE); const session = createSessionContext(middlewareResult); // Create notification manager const notification = new NotificationManager({ NOTIFICATIONS_FILE, fetchT, docker, log, config: siteConfig }); // --- Tailscale coordination API client -------------------------------------- // Reads the API token from credentialManager on every call (not cached on // the client) so that PUT /api/v1/tailscale/settings takes effect // immediately without restarting the process. The metadata file // tailscale-config.json stores non-secret state (tailnet name, last // validation time, device count) so we don't have to hit the API just to // answer "is this configured?" in the UI. function loadTailscaleMetadata() { try { if (TAILSCALE_CONFIG_FILE && fs.existsSync(TAILSCALE_CONFIG_FILE)) { return JSON.parse(fs.readFileSync(TAILSCALE_CONFIG_FILE, 'utf8')); } } catch (_e) { /* corrupt file → treat as unconfigured */ } return { configured: false }; } function saveTailscaleMetadata(meta) { if (!TAILSCALE_CONFIG_FILE) return; try { fs.writeFileSync(TAILSCALE_CONFIG_FILE, JSON.stringify(meta, null, 2), 'utf8'); } catch (e) { log.error('tailscale-coord', e, null, { note: 'Failed to write tailscale-config.json' }); } } async function getCoordClient() { const tok = await credentialManager.retrieve('tailscale.coord.apiToken'); return new TailscaleCoordClient({ apiToken: tok || null }); } // Assemble flat context (temporary - routes still expect this) // Note: tailscale interface detection lives in src/utilities/network-detector.js // (DC-031). The empty `tailscale` stub previously wired here was dead code // — verified zero readers via grep across src/. const ctx = { // Namespaced contexts docker, caddy, dns, session, notification, // Tailscale manager — wraps `tailscale status --json` with 5min cache. // Replaces the long-standing null stub at src/app.js:189. See // src/managers/tailscale-manager.js for full API surface. tailscale: { getStatus: tailscaleManager.getStatus, getLocalIP: tailscaleManager.getLocalIP, getSummary: tailscaleManager.getSummary, getDevices: tailscaleManager.getDevices, isTailscaleIP: tailscaleManager.isTailscaleIP, invalidateCache: tailscaleManager.invalidateCache, getAccessToken: tailscaleManager.getAccessToken, startSyncTimer: tailscaleManager.startSyncTimer, stopSyncTimer: tailscaleManager.stopSyncTimer, syncAPI: tailscaleManager.syncAPI, }, // Tailscale coordination API client — talk to api.tailscale.com for // device management, pre-auth key creation, ACL reads/writes, and user // listing. Distinct from the local tailscaleManager above (which reads // the local tailscaled daemon). The API token is stored encrypted via // credentialManager and re-read on every call so settings changes take // effect without process restart. tailscaleCoord: { // Returns a fresh client each call — cheap (just a Map + token lookup), // and guarantees the latest token is used. getClient: getCoordClient, // Metadata helpers — read/write tailscale-config.json loadMetadata: loadTailscaleMetadata, saveMetadata: saveTailscaleMetadata, // Storage helpers — wraps credentialManager so route code doesn't // need to know the key naming convention. setApiToken: async (token) => { if (token) { await credentialManager.store('tailscale.coord.apiToken', token, { description: 'Tailscale coordination API token', source: 'settings-ui', }); } else { await credentialManager.delete('tailscale.coord.apiToken'); } }, hasApiToken: async () => { const tok = await credentialManager.retrieve('tailscale.coord.apiToken'); return !!tok; }, }, // App and config app, siteConfig, // State managers servicesStateManager, configStateManager, // DC-053 share store shareStore, // Managers credentialManager, authManager, licenseManager, healthChecker, updateManager, backupManager, resourceMonitor, auditLogger, portLockManager, selfUpdater, dockerMaintenance, logDigest, // Templates APP_TEMPLATES, TEMPLATE_CATEGORIES, DIFFICULTY_LEVELS, RECIPE_TEMPLATES, RECIPE_CATEGORIES, // Helpers asyncHandler, errorResponse, ok, fetchT, log, logError, safeErrorMessage, buildDomain, buildServiceUrl, getServiceById, readConfig, saveConfig, addServiceToConfig, validateURL, strictLimiter, // Config helpers totpConfig, saveTotpConfig, loadSiteConfig, loadNotificationConfig, resyncHealthChecker, // Middleware result (exposes renewCSRFToken etc.) middlewareResult, // File paths SERVICES_FILE, CONFIG_FILE, TOTP_CONFIG_FILE, TAILSCALE_CONFIG_FILE, NOTIFICATIONS_FILE, ERROR_LOG_FILE, }; return ctx; } module.exports = { assembleContext };