Files
dashcaddy/dashcaddy-api/src/context/index.js
T
Krystie e99413150e
CI / Test & Lint (push) Canceled after 0s
CI / Security audit (push) Canceled after 0s
[glm-grade=B] fix: dead dashboard WS, corrupted error.log, auth-polling storm, re-auth freeze + CVE bumps
Adversarial audit 2026-08-16 (GLM-5.3 delegate, 2 rounds, 141 tool calls):

P0-1: Dashboard WebSocket (/api/v1/ws) dead on EVERY boot since DC-076.
server.js passed module exports (DependencyManager class, {AutoRestartManager}
namespace, SSLMonitor class) instead of createApp()'s live instances — first
.on() threw ERR_INVALID_ARG_TYPE, catch swallowed it. Fix: app.locals.ctx
exposed in src/app.js; server.js passes all 8 real EventEmitter instances.

P0-2: error.log corrupted since 2026-07-14. errorMiddleware called
logError(FILE, SIZE, path, err, meta) — 5 args into a 3-arg wrapper —
logging 'Error: 5242880' garbage every ~60s and DISCARDING the real error
object. Fix: correct 3-arg call + legacy-shape guard in logErrorWrapper +
~74 log.error sites swept to pass real error objects (AST-verified scope-
safe 71/71, 29/29 modules load clean).

P0-3: auth-polling storm (stranded grade=B commit never landed in prod):
401/403 behind TOTP gate hammered /api/v1/services/status + SSE reconnect
every 2-8s, with misleading direct-probe fallback marking services 'up'.
Fix landed + B-round MEDIUM follow-up: TOTP re-auth success now clears
_dcAuthLost, resumes SSE (new _sseResume clears the latch), and refreshes.

Also: eslintignore static-sites/ (33→0 errors); nodemailer 8→9.0.5 and
sharp 0.33→0.35.3 (3 high CVEs killed; jest green on new majors);
dockerode@5/uuid deferred (semver-major, Docker API surface).

Verification: 80/80 suites, 1837/1837 tests; ESLint 0 errors/743 warnings;
node --check all changed files; bundles rebuilt + SW cache bumped.

Judges: Codex quota-dead until Aug 19 (verified live) — GLM adversarial
delegate per operator directive 2026-08-07. Round 1: 98-call mechanical
verification (timed out pre-verdict). Round 2 (this grade): B, one MEDIUM
(re-auth freeze) — fixed in this commit as prescribed.
2026-08-16 04:18:07 -07:00

262 lines
7.2 KiB
JavaScript

/**
* 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 };