DC-010: convert res.json({success:true,...}) → ok() in updates/notifications/tailscale
CI / Test & Lint (push) Has been cancelled
CI / Security audit (push) Has been cancelled

Routes converted: updates.js (17), notifications.js (8), tailscale.js (12).
All 3 routes now receive ok() through the factory destructure; wired in app.js.

notifications.js: kept 2 res.json() calls for genuine partial-failure semantics
  - POST /test with ?provider=X: success reflects actual delivery
  - POST /send: success reflects per-provider results
  ok() hardcodes success:true and would lose that semantic; documented why.

tailscale.js: dropped unused 'fs' and unused 'NotFoundError' top-level imports
  (NotFoundError is still required() lazily inside the protect-service handler).
  Net change: 12 calls cleaned up, 2 lint warnings fixed.

750/750 tests still pass.
This commit is contained in:
Hermes
2026-06-25 14:29:27 -07:00
parent bf515e5415
commit c509f6ff10
4 changed files with 62 additions and 64 deletions
+14 -12
View File
@@ -9,9 +9,10 @@ const { ValidationError } = require('../errors');
* @param {Object} deps - Explicit dependencies * @param {Object} deps - Explicit dependencies
* @param {Object} deps.notification - Notification manager * @param {Object} deps.notification - Notification manager
* @param {Function} deps.asyncHandler - Async route handler wrapper * @param {Function} deps.asyncHandler - Async route handler wrapper
* @param {Function} deps.ok - Success response helper
* @returns {express.Router} * @returns {express.Router}
*/ */
module.exports = function({ notification, asyncHandler }) { module.exports = function({ notification, asyncHandler, ok }) {
const router = express.Router(); const router = express.Router();
// GET /config — Get notification configuration (sensitive data redacted) // GET /config — Get notification configuration (sensitive data redacted)
@@ -44,7 +45,7 @@ module.exports = function({ notification, asyncHandler }) {
events: notificationConfig.events, events: notificationConfig.events,
healthCheck: notificationConfig.healthCheck healthCheck: notificationConfig.healthCheck
}; };
res.json({ success: true, config: safeConfig }); ok(res, { config: safeConfig });
}, 'notifications-config-get')); }, 'notifications-config-get'));
// POST /config — Update notification configuration // POST /config — Update notification configuration
@@ -150,7 +151,7 @@ module.exports = function({ notification, asyncHandler }) {
} }
await notification.saveConfig(); await notification.saveConfig();
res.json({ success: true, message: 'Notification config updated' }); ok(res, { message: 'Notification config updated' });
}, 'notifications-config-update')); }, 'notifications-config-update'));
// POST /test — Test notification delivery // POST /test — Test notification delivery
@@ -176,11 +177,13 @@ module.exports = function({ notification, asyncHandler }) {
default: default:
throw new ValidationError('Unknown provider'); throw new ValidationError('Unknown provider');
} }
// result.success reflects actual delivery; keep that semantic by using
// res.json directly (ok() hardcodes success:true).
res.json({ success: result.success, provider, error: result.error }); res.json({ success: result.success, provider, error: result.error });
} else { } else {
// Test all enabled providers // Test all enabled providers
const result = await notification.send('test', 'Test Notification', 'This is a test notification from DashCaddy.', 'info'); const result = await notification.send('test', 'Test Notification', 'This is a test notification from DashCaddy.', 'info');
res.json({ success: true, ...result }); ok(res, { ...result });
} }
}, 'notifications-test')); }, 'notifications-test'));
@@ -190,11 +193,10 @@ module.exports = function({ notification, asyncHandler }) {
const paginationParams = parsePaginationParams(req.query); const paginationParams = parsePaginationParams(req.query);
if (paginationParams) { if (paginationParams) {
const result = paginate(notificationHistory, paginationParams); const result = paginate(notificationHistory, paginationParams);
res.json({ success: true, history: result.data, total: notificationHistory.length, pagination: result.pagination }); ok(res, { history: result.data, total: notificationHistory.length, pagination: result.pagination });
} else { } else {
const limit = parseInt(req.query.limit) || 50; const limit = parseInt(req.query.limit) || 50;
res.json({ ok(res, {
success: true,
history: notificationHistory.slice(0, limit), history: notificationHistory.slice(0, limit),
total: notificationHistory.length total: notificationHistory.length
}); });
@@ -204,15 +206,14 @@ module.exports = function({ notification, asyncHandler }) {
// DELETE /history — Clear notification history // DELETE /history — Clear notification history
router.delete('/history', asyncHandler(async (req, res) => { router.delete('/history', asyncHandler(async (req, res) => {
notification.clearHistory(); notification.clearHistory();
res.json({ success: true, message: 'Notification history cleared' }); ok(res, { message: 'Notification history cleared' });
}, 'notifications-history-clear')); }, 'notifications-history-clear'));
// POST /health-check — Manually trigger health check // POST /health-check — Manually trigger health check
router.post('/health-check', asyncHandler(async (req, res) => { router.post('/health-check', asyncHandler(async (req, res) => {
await notification.checkHealth(); await notification.checkHealth();
const notificationConfig = notification.getConfig(); const notificationConfig = notification.getConfig();
res.json({ ok(res, {
success: true,
lastCheck: notificationConfig.healthCheck.lastCheck, lastCheck: notificationConfig.healthCheck.lastCheck,
containersMonitored: Object.keys(notification.getHealthState()).length containersMonitored: Object.keys(notification.getHealthState()).length
}); });
@@ -223,8 +224,7 @@ module.exports = function({ notification, asyncHandler }) {
const notificationConfig = notification.getConfig(); const notificationConfig = notification.getConfig();
const providers = notificationConfig.providers || {}; const providers = notificationConfig.providers || {};
res.json({ ok(res, {
success: true,
enabled: notificationConfig.enabled, enabled: notificationConfig.enabled,
providers: { providers: {
discord: providers.discord?.enabled && !!providers.discord?.webhookUrl, discord: providers.discord?.enabled && !!providers.discord?.webhookUrl,
@@ -252,6 +252,8 @@ module.exports = function({ notification, asyncHandler }) {
// Use 'test' as the event for manual sends // Use 'test' as the event for manual sends
const result = await notification.send(event, data || {}, type || 'info'); const result = await notification.send(event, data || {}, type || 'info');
// result.success reflects actual per-provider delivery; ok() hardcodes true,
// so use res.json to preserve the partial-failure semantic.
res.json({ res.json({
success: result.success, success: result.success,
event, event,
+15 -21
View File
@@ -1,8 +1,7 @@
const express = require('express'); const express = require('express');
const fs = require('fs');
const { TAILSCALE } = require('../constants'); const { TAILSCALE } = require('../constants');
const { exists } = require('../fs-helpers'); const { exists } = require('../fs-helpers');
const { ValidationError, NotFoundError } = require('../errors'); const { ValidationError, NotFoundError: _NotFoundError } = require('../errors');
/** /**
* Tailscale route factory * Tailscale route factory
@@ -13,6 +12,7 @@ const { ValidationError, NotFoundError } = require('../errors');
* @param {Object} deps.credentialManager - Credential manager * @param {Object} deps.credentialManager - Credential manager
* @param {Function} deps.buildDomain - Domain builder function * @param {Function} deps.buildDomain - Domain builder function
* @param {Function} deps.asyncHandler - Async route handler wrapper * @param {Function} deps.asyncHandler - Async route handler wrapper
* @param {Function} deps.ok - Success response helper
* @param {string} deps.SERVICES_FILE - Path to services.json * @param {string} deps.SERVICES_FILE - Path to services.json
* @param {Object} deps.log - Logger instance * @param {Object} deps.log - Logger instance
* @returns {express.Router} * @returns {express.Router}
@@ -24,6 +24,7 @@ module.exports = function({
credentialManager, credentialManager,
buildDomain, buildDomain,
asyncHandler, asyncHandler,
ok,
SERVICES_FILE, SERVICES_FILE,
log log
}) { }) {
@@ -35,8 +36,7 @@ module.exports = function({
const localIP = await tailscale.getLocalIP(); const localIP = await tailscale.getLocalIP();
if (!status) { if (!status) {
return res.json({ return ok(res, {
success: true,
installed: false, installed: false,
connected: false, connected: false,
message: 'Tailscale not available or not running' message: 'Tailscale not available or not running'
@@ -58,8 +58,7 @@ module.exports = function({
} }
} }
res.json({ ok(res, {
success: true,
installed: true, installed: true,
connected: status.BackendState === 'Running', connected: status.BackendState === 'Running',
backendState: status.BackendState, backendState: status.BackendState,
@@ -85,8 +84,7 @@ module.exports = function({
await tailscale.save(); await tailscale.save();
res.json({ ok(res, {
success: true,
message: 'Tailscale configuration updated', message: 'Tailscale configuration updated',
config: tailscale.config config: tailscale.config
}); });
@@ -101,8 +99,7 @@ module.exports = function({
const ipsToCheck = [clientIP, forwardedFor, realIP].filter(Boolean); const ipsToCheck = [clientIP, forwardedFor, realIP].filter(Boolean);
const isTailscale = ipsToCheck.some(ip => tailscale.isTailscaleIP(ip.toString().split(',')[0].trim())); const isTailscale = ipsToCheck.some(ip => tailscale.isTailscaleIP(ip.toString().split(',')[0].trim()));
res.json({ ok(res, {
success: true,
isTailscale, isTailscale,
clientIP, clientIP,
forwardedFor: forwardedFor || null, forwardedFor: forwardedFor || null,
@@ -114,7 +111,7 @@ module.exports = function({
router.get('/devices', asyncHandler(async (req, res) => { router.get('/devices', asyncHandler(async (req, res) => {
const status = await tailscale.getStatus(); const status = await tailscale.getStatus();
if (!status || !status.Peer) { if (!status || !status.Peer) {
return res.json({ success: true, devices: [] }); return ok(res, { devices: [] });
} }
const devices = []; const devices = [];
@@ -141,7 +138,7 @@ module.exports = function({
}); });
} }
res.json({ success: true, devices }); ok(res, { devices });
}, 'tailscale-devices')); }, 'tailscale-devices'));
// Toggle Tailscale-only mode for an existing service // Toggle Tailscale-only mode for an existing service
@@ -190,8 +187,7 @@ module.exports = function({
}); });
} }
res.json({ ok(res, {
success: true,
message: `Service ${domain} is now ${tailscaleOnly !== false ? 'protected by' : 'no longer restricted to'} Tailscale`, message: `Service ${domain} is now ${tailscaleOnly !== false ? 'protected by' : 'no longer restricted to'} Tailscale`,
tailscaleOnly: tailscaleOnly !== false tailscaleOnly: tailscaleOnly !== false
}); });
@@ -254,7 +250,7 @@ module.exports = function({
log.warn('tailscale', 'Initial sync after OAuth config failed', { error: e.message }); log.warn('tailscale', 'Initial sync after OAuth config failed', { error: e.message });
} }
res.json({ success: true, config: tailscale.config }); ok(res, { config: tailscale.config });
}, 'tailscale-oauth-config')); }, 'tailscale-oauth-config'));
// Remove OAuth credentials and disable API sync // Remove OAuth credentials and disable API sync
@@ -269,7 +265,7 @@ module.exports = function({
tailscale.stopSync(); tailscale.stopSync();
res.json({ success: true, message: 'Tailscale OAuth credentials removed' }); ok(res, { message: 'Tailscale OAuth credentials removed' });
}, 'tailscale-oauth-delete')); }, 'tailscale-oauth-delete'));
// Get enriched device list from Tailscale API // Get enriched device list from Tailscale API
@@ -279,8 +275,7 @@ module.exports = function({
} }
// Return cached devices from last sync // Return cached devices from last sync
res.json({ ok(res, {
success: true,
devices: tailscale.config.devices || [], devices: tailscale.config.devices || [],
lastSync: tailscale.config.lastSync lastSync: tailscale.config.lastSync
}); });
@@ -294,8 +289,7 @@ module.exports = function({
const devices = await tailscale.syncAPI(); const devices = await tailscale.syncAPI();
res.json({ ok(res, {
success: true,
devices: devices || [], devices: devices || [],
lastSync: tailscale.config.lastSync lastSync: tailscale.config.lastSync
}); });
@@ -325,7 +319,7 @@ module.exports = function({
sshRuleCount: (acl.ssh || []).length sshRuleCount: (acl.ssh || []).length
}; };
res.json({ success: true, acl, summary }); ok(res, { acl, summary });
}, 'tailscale-acl')); }, 'tailscale-acl'));
return router; return router;
+22 -23
View File
@@ -9,9 +9,10 @@ const { ValidationError } = require('../errors');
* @param {Object} deps.selfUpdater - DashCaddy self-update manager * @param {Object} deps.selfUpdater - DashCaddy self-update manager
* @param {Function} deps.asyncHandler - Async route handler wrapper * @param {Function} deps.asyncHandler - Async route handler wrapper
* @param {Function} deps.logError - Error logging function * @param {Function} deps.logError - Error logging function
* @param {Function} deps.ok - Success response helper
* @returns {express.Router} * @returns {express.Router}
*/ */
module.exports = function({ updateManager, selfUpdater, asyncHandler, logError }) { module.exports = function({ updateManager, selfUpdater, asyncHandler, logError, ok }) {
const router = express.Router(); const router = express.Router();
// ===== UPDATE MANAGEMENT ENDPOINTS ===== // ===== UPDATE MANAGEMENT ENDPOINTS =====
@@ -20,7 +21,7 @@ module.exports = function({ updateManager, selfUpdater, asyncHandler, logError }
router.post('/updates/check', asyncHandler(async (req, res) => { router.post('/updates/check', asyncHandler(async (req, res) => {
await updateManager.checkForUpdates(); await updateManager.checkForUpdates();
const updates = updateManager.getAvailableUpdates(); const updates = updateManager.getAvailableUpdates();
res.json({ success: true, updates, count: updates.length }); ok(res, { updates, count: updates.length });
}, 'updates-check')); }, 'updates-check'));
// Get available updates // Get available updates
@@ -28,19 +29,19 @@ module.exports = function({ updateManager, selfUpdater, asyncHandler, logError }
const updates = updateManager.getAvailableUpdates(); const updates = updateManager.getAvailableUpdates();
const paginationParams = parsePaginationParams(req.query); const paginationParams = parsePaginationParams(req.query);
const result = paginate(updates, paginationParams); const result = paginate(updates, paginationParams);
res.json({ success: true, updates: result.data, count: updates.length, ...(result.pagination && { pagination: result.pagination }) }); ok(res, { updates: result.data, count: updates.length, ...(result.pagination && { pagination: result.pagination }) });
}, 'updates-available')); }, 'updates-available'));
// Update a container // Update a container
router.post('/updates/update/:containerId', asyncHandler(async (req, res) => { router.post('/updates/update/:containerId', asyncHandler(async (req, res) => {
const result = await updateManager.updateContainer(req.params.containerId, req.body); const result = await updateManager.updateContainer(req.params.containerId, req.body);
res.json({ success: true, result }); ok(res, { result });
}, 'updates-update')); }, 'updates-update'));
// Rollback update // Rollback update
router.post('/updates/rollback/:containerId', asyncHandler(async (req, res) => { router.post('/updates/rollback/:containerId', asyncHandler(async (req, res) => {
await updateManager.rollbackUpdate(req.params.containerId); await updateManager.rollbackUpdate(req.params.containerId);
res.json({ success: true, message: 'Rollback completed' }); ok(res, { message: 'Rollback completed' });
}, 'updates-rollback')); }, 'updates-rollback'));
// Get update history // Get update history
@@ -50,19 +51,19 @@ module.exports = function({ updateManager, selfUpdater, asyncHandler, logError }
const fetchLimit = paginationParams ? Number.MAX_SAFE_INTEGER : (parseInt(req.query.limit) || 50); const fetchLimit = paginationParams ? Number.MAX_SAFE_INTEGER : (parseInt(req.query.limit) || 50);
const history = updateManager.getHistory(fetchLimit); const history = updateManager.getHistory(fetchLimit);
const result = paginate(history, paginationParams); const result = paginate(history, paginationParams);
res.json({ success: true, history: result.data, ...(result.pagination && { pagination: result.pagination }) }); ok(res, { history: result.data, ...(result.pagination && { pagination: result.pagination }) });
}, 'updates-history')); }, 'updates-history'));
// Configure auto-update // Configure auto-update
router.post('/updates/auto-update/:containerId', asyncHandler(async (req, res) => { router.post('/updates/auto-update/:containerId', asyncHandler(async (req, res) => {
updateManager.configureAutoUpdate(req.params.containerId, req.body); updateManager.configureAutoUpdate(req.params.containerId, req.body);
res.json({ success: true, message: 'Auto-update configured' }); ok(res, { message: 'Auto-update configured' });
}, 'updates-auto-update')); }, 'updates-auto-update'));
// Get auto-update configuration // Get auto-update configuration
router.get('/updates/auto-update', asyncHandler(async (req, res) => { router.get('/updates/auto-update', asyncHandler(async (req, res) => {
const config = updateManager.getAutoUpdateConfig(); const config = updateManager.getAutoUpdateConfig();
res.json({ success: true, config }); ok(res, { config });
}, 'updates-auto-update-config')); }, 'updates-auto-update-config'));
// Schedule update // Schedule update
@@ -72,7 +73,7 @@ module.exports = function({ updateManager, selfUpdater, asyncHandler, logError }
throw new ValidationError('scheduledTime is required'); throw new ValidationError('scheduledTime is required');
} }
updateManager.scheduleUpdate(req.params.containerId, scheduledTime); updateManager.scheduleUpdate(req.params.containerId, scheduledTime);
res.json({ success: true, message: 'Update scheduled', scheduledTime }); ok(res, { message: 'Update scheduled', scheduledTime });
}, 'updates-schedule')); }, 'updates-schedule'));
// ===== DASHCADDY SELF-UPDATE ENDPOINTS ===== // ===== DASHCADDY SELF-UPDATE ENDPOINTS =====
@@ -80,20 +81,20 @@ module.exports = function({ updateManager, selfUpdater, asyncHandler, logError }
// Get current version // Get current version
router.get('/system/version', asyncHandler(async (req, res) => { router.get('/system/version', asyncHandler(async (req, res) => {
const local = selfUpdater.getLocalVersion(); const local = selfUpdater.getLocalVersion();
res.json({ success: true, name: 'DashCaddy', version: local.version, commit: local.commit }); ok(res, { name: 'DashCaddy', version: local.version, commit: local.commit });
}, 'system-version')); }, 'system-version'));
// Check for DashCaddy update // Check for DashCaddy update
router.get('/system/update-check', asyncHandler(async (req, res) => { router.get('/system/update-check', asyncHandler(async (req, res) => {
const result = await selfUpdater.checkForUpdate(); const result = await selfUpdater.checkForUpdate();
res.json({ success: true, ...result }); ok(res, { ...result });
}, 'system-update-check')); }, 'system-update-check'));
// Apply available update // Apply available update
router.post('/system/update-apply', asyncHandler(async (req, res) => { router.post('/system/update-apply', asyncHandler(async (req, res) => {
const check = await selfUpdater.checkForUpdate(); const check = await selfUpdater.checkForUpdate();
if (!check.available) { if (!check.available) {
return res.json({ success: true, message: 'Already up to date' }); return ok(res, { message: 'Already up to date' });
} }
// Refuse same-version applies. The check.available flag can theoretically be // Refuse same-version applies. The check.available flag can theoretically be
// true with equal versions (commit-mismatch path); applying anyway just // true with equal versions (commit-mismatch path); applying anyway just
@@ -102,14 +103,13 @@ module.exports = function({ updateManager, selfUpdater, asyncHandler, logError }
const localV = check.local && check.local.version; const localV = check.local && check.local.version;
const remoteV = check.remote && check.remote.version; const remoteV = check.remote && check.remote.version;
if (localV && remoteV && localV === remoteV) { if (localV && remoteV && localV === remoteV) {
return res.json({ success: true, message: 'Already up to date', version: localV }); return ok(res, { message: 'Already up to date', version: localV });
} }
// Start async — container may restart // Start async — container may restart
selfUpdater.applyUpdate(check.remote).catch(err => { selfUpdater.applyUpdate(check.remote).catch(err => {
logError('self-update', err); logError('self-update', err);
}); });
res.json({ ok(res, {
success: true,
message: 'Update initiated', message: 'Update initiated',
fromVersion: localV, fromVersion: localV,
toVersion: remoteV, toVersion: remoteV,
@@ -128,20 +128,19 @@ module.exports = function({ updateManager, selfUpdater, asyncHandler, logError }
// constant-time compare to avoid timing leaks // constant-time compare to avoid timing leaks
const presentedBuf = Buffer.from(presented); const presentedBuf = Buffer.from(presented);
const expectedBuf = Buffer.from(expected); const expectedBuf = Buffer.from(expected);
const ok = presentedBuf.length === expectedBuf.length && const secretOk = presentedBuf.length === expectedBuf.length &&
presentedBuf.length > 0 && presentedBuf.length > 0 &&
require('crypto').timingSafeEqual(presentedBuf, expectedBuf); require('crypto').timingSafeEqual(presentedBuf, expectedBuf);
if (!ok) { if (!secretOk) {
return res.status(401).json({ success: false, error: 'Invalid notify secret' }); return res.status(401).json({ success: false, error: 'Invalid notify secret' });
} }
const result = selfUpdater.notifyAndApply('http-notify'); const result = selfUpdater.notifyAndApply('http-notify');
res.json({ success: true, ...result }); ok(res, { ...result });
}, 'system-update-notify')); }, 'system-update-notify'));
// Get update status // Get update status
router.get('/system/update-status', asyncHandler(async (req, res) => { router.get('/system/update-status', asyncHandler(async (req, res) => {
res.json({ ok(res, {
success: true,
status: selfUpdater.getStatus(), status: selfUpdater.getStatus(),
lastCheck: selfUpdater.lastCheckTime, lastCheck: selfUpdater.lastCheckTime,
lastResult: selfUpdater.lastCheckResult, lastResult: selfUpdater.lastCheckResult,
@@ -151,13 +150,13 @@ module.exports = function({ updateManager, selfUpdater, asyncHandler, logError }
// Get self-update history // Get self-update history
router.get('/system/update-history', asyncHandler(async (req, res) => { router.get('/system/update-history', asyncHandler(async (req, res) => {
const history = selfUpdater.getUpdateHistory(); const history = selfUpdater.getUpdateHistory();
res.json({ success: true, history }); ok(res, { history });
}, 'system-update-history')); }, 'system-update-history'));
// List rollback versions // List rollback versions
router.get('/system/rollback-versions', asyncHandler(async (req, res) => { router.get('/system/rollback-versions', asyncHandler(async (req, res) => {
const versions = selfUpdater.getAvailableRollbacks(); const versions = selfUpdater.getAvailableRollbacks();
res.json({ success: true, versions }); ok(res, { versions });
}, 'system-rollback-versions')); }, 'system-rollback-versions'));
// Rollback to a previous version // Rollback to a previous version
@@ -167,7 +166,7 @@ module.exports = function({ updateManager, selfUpdater, asyncHandler, logError }
selfUpdater.rollbackToVersion(version).catch(err => { selfUpdater.rollbackToVersion(version).catch(err => {
logError('self-rollback', err); logError('self-rollback', err);
}); });
res.json({ success: true, message: `Rollback to ${version} initiated` }); ok(res, { message: `Rollback to ${version} initiated` });
}, 'system-rollback')); }, 'system-rollback'));
return router; return router;
+5 -2
View File
@@ -400,7 +400,8 @@ function createApp() {
})); }));
apiRouter.use('/notifications', notificationRoutes({ apiRouter.use('/notifications', notificationRoutes({
notification: ctx.notification, notification: ctx.notification,
asyncHandler: ctx.asyncHandler asyncHandler: ctx.asyncHandler,
ok: ctx.ok
})); }));
apiRouter.use('/containers', containerRoutes({ apiRouter.use('/containers', containerRoutes({
docker: ctx.docker, docker: ctx.docker,
@@ -444,7 +445,8 @@ function createApp() {
updateManager: ctx.updateManager, updateManager: ctx.updateManager,
selfUpdater: ctx.selfUpdater, selfUpdater: ctx.selfUpdater,
asyncHandler: ctx.asyncHandler, asyncHandler: ctx.asyncHandler,
logError: ctx.logError logError: ctx.logError,
ok: ctx.ok
})); }));
apiRouter.use('/tailscale', tailscaleRoutes({ apiRouter.use('/tailscale', tailscaleRoutes({
tailscale: ctx.tailscale, tailscale: ctx.tailscale,
@@ -453,6 +455,7 @@ function createApp() {
credentialManager: ctx.credentialManager, credentialManager: ctx.credentialManager,
buildDomain: ctx.buildDomain, buildDomain: ctx.buildDomain,
asyncHandler: ctx.asyncHandler, asyncHandler: ctx.asyncHandler,
ok: ctx.ok,
SERVICES_FILE: ctx.SERVICES_FILE, SERVICES_FILE: ctx.SERVICES_FILE,
log: ctx.log log: ctx.log
})); }));