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.
262 lines
7.2 KiB
JavaScript
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 };
|