Merge krystie-improvements into main

Resolves 24 conflicts between Hermes (DC-008/009/010 + response-helper
envelope standardization) and Krystie (DC-005 src/ refactor path fixes,
DC-006 TOTP integration, DC-007 new test suites, cloud backup
destinations).

Conflict resolutions:
- src/utils/logging.js:    took ours (consumers depend on logError/
                            safeErrorMessage/createLogger exports)
- src/config/site.js:      merged (her factored validateAndLogConfig +
                            applyConfigFields helpers)
- src/context/dns.js:      took hers (admin/readonly role iteration for
                            write operations)
- src/utilities/backup-
  manager.js:              took hers (Dropbox/WebDAV/SFTP cloud feature)
- status/dist/*, status/
  sw.js:                   took hers (minified bundles + newer SW cache)

Additional fix (post-merge regression):
- src/monitoring/health-checker.js: fixed DC-005 path miss —
  'require(./platform-paths)' → 'require(../../platform-paths)'

Test status: 921/922 passing. One known failure in logging.test.js
(async file-handle timing) tracked as follow-up.
This commit is contained in:
Hermes
2026-06-25 16:43:10 -07:00
171 changed files with 11759 additions and 1006 deletions
+1 -1
View File
@@ -1,7 +1,7 @@
/**
* Async handler wrapper - Eliminates try/catch boilerplate
*/
const { AppError } = require('../../errors');
const { AppError } = require('../utilities/errors');
/**
* Wrap async route handlers - catches errors and logs them
+11 -3
View File
@@ -3,7 +3,7 @@
*/
const http = require('http');
const https = require('https');
const { TIMEOUTS } = require('../../constants');
const { TIMEOUTS } = require('../utilities/constants');
// HTTPS agent that trusts internal CA certs (self-signed .sami TLD etc.)
// Lazy-initialized singleton to avoid creating a new agent per request.
@@ -38,7 +38,15 @@ function fetchT(url, opts = {}, timeoutMs = TIMEOUTS.HTTP_DEFAULT) {
if (!opts.signal) {
opts = { ...opts, signal: AbortSignal.timeout(timeoutMs) };
}
delete opts.timeout;
// The `timeout` key in fetch() opts is silently ignored by undici. Callers
// should use the third arg of fetchT() (timeoutMs) instead. If a caller
// passes `timeout: N` here, it's almost certainly a bug — we used to silently
// strip it, which masked the issue. Now we surface it in logs and strip it.
if ('timeout' in opts) {
console.warn(`[fetchT] opts.timeout=${opts.timeout} is ignored — pass timeoutMs as the 3rd arg of fetchT() instead. Called from: ${new Error().stack.split('\n').slice(2, 4).join(' <- ')}`);
const { timeout: _timeout, ...rest } = opts;
opts = rest;
}
return fetch(url, opts);
}
@@ -160,7 +168,7 @@ function _httpFetch(url, opts = {}, timeoutMs = TIMEOUTS.HTTP_DEFAULT) {
});
});
});
req.on('timeout', () => {
req.destroy();
reject(new Error(`Request to ${url} timed out after ${timeoutMs}ms`));
+107 -5
View File
@@ -1,22 +1,124 @@
/**
* Response helpers - Standard API response formats
*
* Single source of truth for HTTP response shapes across DashCaddy.
* Standard envelope: { success: true, ...data } or { success: false, error: "..." }.
*
* All routes should import from this module — do not call res.json/res.status
* directly with the response shape, use these helpers instead.
*/
const { HTTP_STATUS } = require('../utilities/constants');
// ── Success helpers ────────────────────────────────────────────
/**
* Standard error response
* Standard success response. Use this in route handlers.
* Wraps the data object with a `success: true` envelope.
* @param {object} res Express response
* @param {object} [data={}] fields to include in the response body
* @param {number} [statusCode=200] HTTP status code
*/
function ok(res, data = {}, statusCode = HTTP_STATUS.OK) {
return res.status(statusCode).json({ success: true, ...data });
}
/**
* Alias for `ok` — prefer `ok` in new code, but kept for code that imports as `success`.
*/
function success(res, data, statusCode) {
return ok(res, data, statusCode);
}
/**
* Success response with a human-readable message field.
* Use when there's no data to return, just confirmation.
*/
function successMessage(res, message, statusCode = HTTP_STATUS.OK) {
return res.status(statusCode).json({ success: true, message });
}
/**
* 201 Created response.
*/
function created(res, data = {}) {
return res.status(HTTP_STATUS.CREATED).json({ success: true, ...data });
}
/**
* 204 No Content response.
*/
function noContent(res) {
return res.status(HTTP_STATUS.NO_CONTENT).send();
}
// ── Error helpers ──────────────────────────────────────────────
/**
* Standard error response. Use this in route handlers.
* @param {object} res Express response
* @param {number} statusCode HTTP status code
* @param {string} message Human-readable error message
* @param {object} [extras={}] additional fields to merge into the response
*/
function errorResponse(res, statusCode, message, extras = {}) {
return res.status(statusCode).json({ success: false, error: message, ...extras });
}
/**
* Standard success response
* Alias for `errorResponse` — kept for code that imports as `error`.
*/
function ok(res, data = {}) {
return res.json({ success: true, ...data });
function error(res, message, statusCode = HTTP_STATUS.INTERNAL_ERROR) {
return res.status(statusCode).json({ success: false, error: message });
}
/**
* 400 Bad Request — invalid input from the user.
*/
function validationError(res, message) {
return res.status(HTTP_STATUS.BAD_REQUEST).json({ success: false, error: message });
}
/**
* 401 Unauthorized — no valid credentials.
*/
function unauthorized(res, message = 'Unauthorized') {
return res.status(HTTP_STATUS.UNAUTHORIZED).json({ success: false, error: message });
}
/**
* 403 Forbidden — credentials valid but permission denied.
*/
function forbidden(res, message = 'Forbidden') {
return res.status(HTTP_STATUS.FORBIDDEN).json({ success: false, error: message });
}
/**
* 404 Not Found — resource doesn't exist.
*/
function notFound(res, message = 'Not found') {
return res.status(HTTP_STATUS.NOT_FOUND).json({ success: false, error: message });
}
/**
* 409 Conflict — request conflicts with current state (e.g. duplicate).
*/
function conflict(res, message) {
return res.status(HTTP_STATUS.CONFLICT).json({ success: false, error: message });
}
module.exports = {
errorResponse,
// Success helpers
ok,
success,
successMessage,
created,
noContent,
// Error helpers
errorResponse,
error,
validationError,
unauthorized,
forbidden,
notFound,
conflict,
};