[grade=pending] QA sprint: commit 103 at-risk files from multi-agent sprint work
Committed by Hermes autonomous QA sprint 2026-08-13. These files were modified during the Aug 12 sprint but never committed.
This commit is contained in:
@@ -0,0 +1,227 @@
|
||||
/**
|
||||
* DC-106: Caddyfile-as-code — generate Caddyfile entries from structured JSON
|
||||
*
|
||||
* Allows building reverse proxy configs programmatically instead of editing
|
||||
* raw Caddyfile text. The frontend can present a visual form, send the JSON,
|
||||
* and get back a Caddyfile snippet + apply it via the Caddy admin API.
|
||||
*
|
||||
* POST /api/v1/caddycode/generate — generate Caddyfile block from JSON
|
||||
* POST /api/v1/caddycode/validate — validate a generated block
|
||||
* GET /api/v1/caddycode/importers — list supported import formats
|
||||
*/
|
||||
const express = require('express');
|
||||
const { ok, errorResponse } = require('../src/utils/responses');
|
||||
|
||||
/**
|
||||
* Generate a Caddyfile site block from a structured config.
|
||||
* @param {Object} config - Site configuration
|
||||
* @returns {string} Caddyfile snippet
|
||||
*/
|
||||
function generateSiteBlock(config) {
|
||||
const {
|
||||
domain,
|
||||
upstream,
|
||||
upstreamProtocol = 'http',
|
||||
tls = 'auto',
|
||||
websocket = false,
|
||||
auth = false,
|
||||
authService = null,
|
||||
headers = {},
|
||||
cors = false,
|
||||
rateLimit = null,
|
||||
cache = false,
|
||||
compress = true,
|
||||
stripPrefix = null,
|
||||
redirectToHttps = true,
|
||||
} = config;
|
||||
|
||||
const lines = [];
|
||||
lines.push(`${domain} {`);
|
||||
|
||||
// TLS
|
||||
if (tls === 'internal') {
|
||||
lines.push(` tls internal`);
|
||||
} else if (tls === 'auto') {
|
||||
// Default — Caddy auto-provisions Let's Encrypt
|
||||
} else if (typeof tls === 'string') {
|
||||
lines.push(` tls ${tls}`);
|
||||
}
|
||||
|
||||
// Redirect HTTP→HTTPS
|
||||
if (redirectToHttps) {
|
||||
lines.push(` # Redirect HTTP to HTTPS is automatic in Caddy 2`);
|
||||
}
|
||||
|
||||
// Auth gate (DashCaddy forward_auth)
|
||||
if (auth && authService) {
|
||||
lines.push(` import dashcaddy_auth ${authService}`);
|
||||
}
|
||||
|
||||
// CORS headers
|
||||
if (cors) {
|
||||
lines.push(` header {`);
|
||||
lines.push(` Access-Control-Allow-Origin *`);
|
||||
lines.push(` Access-Control-Allow-Methods "GET, POST, PUT, DELETE, OPTIONS"`);
|
||||
lines.push(` Access-Control-Allow-Headers "Content-Type, Authorization"`);
|
||||
lines.push(` }`);
|
||||
}
|
||||
|
||||
// Custom headers
|
||||
if (Object.keys(headers).length > 0) {
|
||||
lines.push(` header {`);
|
||||
for (const [key, value] of Object.entries(headers)) {
|
||||
lines.push(` ${key} "${value}"`);
|
||||
}
|
||||
lines.push(` }`);
|
||||
}
|
||||
|
||||
// Strip prefix
|
||||
if (stripPrefix) {
|
||||
lines.push(` uri strip_prefix ${stripPrefix}`);
|
||||
}
|
||||
|
||||
// Compression
|
||||
if (compress) {
|
||||
lines.push(` encode gzip zstd`);
|
||||
}
|
||||
|
||||
// Reverse proxy
|
||||
const protocol = upstreamProtocol === 'https' ? 'https' : 'http';
|
||||
lines.push(` reverse_proxy ${protocol}://${upstream} {`);
|
||||
if (websocket) {
|
||||
lines.push(` # WebSocket support is automatic in Caddy 2`);
|
||||
}
|
||||
lines.push(` header_up Host {host}`);
|
||||
lines.push(` transport http {`);
|
||||
lines.push(` read_timeout 5m`);
|
||||
lines.push(` write_timeout 5m`);
|
||||
lines.push(` }`);
|
||||
lines.push(` }`);
|
||||
|
||||
lines.push(`}`);
|
||||
|
||||
return lines.join('\n');
|
||||
}
|
||||
|
||||
module.exports = function({ asyncHandler }) {
|
||||
const wrap = asyncHandler || ((fn) => (req, res, next) => Promise.resolve(fn(req, res, next)).catch(next));
|
||||
const router = express.Router();
|
||||
|
||||
// POST /api/v1/caddycode/generate
|
||||
router.post('/caddycode/generate', wrap(async (req, res) => {
|
||||
const config = req.body || {};
|
||||
|
||||
if (!config.domain) {
|
||||
return errorResponse(res, 400, 'domain is required');
|
||||
}
|
||||
if (!config.upstream) {
|
||||
return errorResponse(res, 400, 'upstream is required (e.g. localhost:8080)');
|
||||
}
|
||||
|
||||
try {
|
||||
const caddyfile = generateSiteBlock(config);
|
||||
ok(res, { caddyfile, config });
|
||||
} catch (err) {
|
||||
errorResponse(res, 500, `Generation failed: ${err.message}`);
|
||||
}
|
||||
}));
|
||||
|
||||
// POST /api/v1/caddycode/validate
|
||||
router.post('/caddycode/validate', wrap(async (req, res) => {
|
||||
const { caddyfile } = req.body || {};
|
||||
|
||||
if (!caddyfile) {
|
||||
return errorResponse(res, 400, 'caddyfile string is required');
|
||||
}
|
||||
|
||||
// Basic validation checks
|
||||
const issues = [];
|
||||
|
||||
// Check for balanced braces
|
||||
const openBraces = (caddyfile.match(/{/g) || []).length;
|
||||
const closeBraces = (caddyfile.match(/}/g) || []).length;
|
||||
if (openBraces !== closeBraces) {
|
||||
issues.push(`Unbalanced braces: ${openBraces} open vs ${closeBraces} close`);
|
||||
}
|
||||
|
||||
// Check for domain in first non-empty line
|
||||
const firstLine = caddyfile.trim().split('\n')[0].trim();
|
||||
if (!firstLine || firstLine.startsWith('#') || firstLine.startsWith('{')) {
|
||||
issues.push('First line should be a domain name');
|
||||
}
|
||||
|
||||
// Check for reverse_proxy directive
|
||||
if (!caddyfile.includes('reverse_proxy')) {
|
||||
issues.push('No reverse_proxy directive found — site will not proxy traffic');
|
||||
}
|
||||
|
||||
// Check for common mistakes
|
||||
if (caddyfile.includes('tls ')) {
|
||||
const tlsLine = caddyfile.split('\n').find(l => l.trim().startsWith('tls '));
|
||||
if (tlsLine && tlsLine.includes('auto')) {
|
||||
issues.push('tls auto is redundant — Caddy does this by default');
|
||||
}
|
||||
}
|
||||
|
||||
ok(res, {
|
||||
valid: issues.length === 0,
|
||||
issues,
|
||||
warnings: [],
|
||||
});
|
||||
}));
|
||||
|
||||
// GET /api/v1/caddycode/templates — preset configs for common patterns
|
||||
router.get('/caddycode/templates', wrap(async (req, res) => {
|
||||
const templates = {
|
||||
'simple-proxy': {
|
||||
label: 'Simple Reverse Proxy',
|
||||
config: {
|
||||
domain: 'app.example.com',
|
||||
upstream: 'localhost:8080',
|
||||
tls: 'auto',
|
||||
websocket: false,
|
||||
auth: false,
|
||||
},
|
||||
},
|
||||
'websocket-app': {
|
||||
label: 'WebSocket Application',
|
||||
config: {
|
||||
domain: 'app.example.com',
|
||||
upstream: 'localhost:3000',
|
||||
websocket: true,
|
||||
compress: true,
|
||||
},
|
||||
},
|
||||
'auth-gated': {
|
||||
label: 'Auth-Gated Service (DashCaddy SSO)',
|
||||
config: {
|
||||
domain: 'app.example.com',
|
||||
upstream: 'localhost:8096',
|
||||
auth: true,
|
||||
authService: 'app',
|
||||
},
|
||||
},
|
||||
'cors-api': {
|
||||
label: 'API with CORS',
|
||||
config: {
|
||||
domain: 'api.example.com',
|
||||
upstream: 'localhost:3001',
|
||||
cors: true,
|
||||
compress: true,
|
||||
},
|
||||
},
|
||||
'subdirectory': {
|
||||
label: 'Subdirectory Proxy',
|
||||
config: {
|
||||
domain: 'example.com',
|
||||
upstream: 'localhost:8080',
|
||||
stripPrefix: '/app',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
ok(res, { templates });
|
||||
}));
|
||||
|
||||
return router;
|
||||
};
|
||||
Reference in New Issue
Block a user