feat: 1.5.0 prep — API v1 cutover, LICENSE, CHANGELOG, CI

- Remove legacy /api/ mount; all routes now under /api/v1/ only
- Update path matchers (CSRF excludes, public routes, audit log, rate limits)
- Move standalone routes (/api/network/ips, /api/docs, /api/docs/spec) to v1
- Update openapi.yaml (110 paths), CA pages, and 4 lingering frontend files
- Add LICENSE (proprietary EULA), CHANGELOG.md (Keep a Changelog format)
- Add .gitea/workflows/ci.yml (test+lint and security audit jobs)
- Fix 9 pre-existing no-empty lint errors so CI starts green
- Drop ad-hoc scratch reports and *.bak files from repo root

All 739 jest tests pass. Lint is clean.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Sami
2026-05-17 11:38:45 -07:00
co-authored by Claude Opus 4.7
parent cd8ccaba2b
commit d36705bd90
18 changed files with 663 additions and 252 deletions
@@ -169,7 +169,7 @@ describe('CSRF Protection', () => {
const origEnv = process.env.NODE_ENV;
process.env.NODE_ENV = 'production';
const excludedPaths = ['/api/totp/verify', '/api/totp/setup', '/health', '/api/health'];
const excludedPaths = ['/api/v1/totp/verify', '/api/v1/totp/setup', '/health', '/api/v1/health'];
for (const excludedPath of excludedPaths) {
const { req, res, next } = createMockReqRes({ method: 'POST', path: excludedPath });
csrfValidationMiddleware(req, res, next);
@@ -184,7 +184,7 @@ describe('CSRF Protection', () => {
process.env.NODE_ENV = 'production';
const { req, res, next } = createMockReqRes({
method: 'POST', path: '/api/auth/gate/plex'
method: 'POST', path: '/api/v1/auth/gate/plex'
});
csrfValidationMiddleware(req, res, next);
expect(next).toHaveBeenCalled();
@@ -296,7 +296,7 @@ describe('CSRF Protection', () => {
process.env.NODE_ENV = origEnv;
});
it('normalizes /api/v1/ prefix for exclusion matching', () => {
it('excludes /api/v1/ paths directly', () => {
const origEnv = process.env.NODE_ENV;
process.env.NODE_ENV = 'production';
+56 -56
View File
@@ -7,65 +7,65 @@ const MAX_ENTRIES = parseInt(process.env.AUDIT_MAX_ENTRIES || '1000', 10);
// Route path → readable action mapping
const ACTION_MAP = {
'POST /api/services/update': 'service.reorder',
'POST /api/services': 'service.create',
'PUT /api/services': 'service.update',
'DELETE /api/services/': 'service.delete',
'POST /api/site': 'caddy.add-site',
'POST /api/site/external': 'caddy.add-external',
'DELETE /api/site/': 'caddy.remove-site',
'POST /api/caddy/reload': 'caddy.reload',
'POST /api/dns/record': 'dns.add-record',
'DELETE /api/dns/record': 'dns.delete-record',
'POST /api/dns/credentials': 'dns.save-credentials',
'DELETE /api/dns/credentials': 'dns.delete-credentials',
'POST /api/dns/refresh-token': 'dns.refresh-token',
'POST /api/dns/update': 'dns.update-server',
'POST /api/containers/': 'container.action',
'DELETE /api/containers/': 'container.delete',
'POST /api/apps/deploy': 'container.deploy',
'DELETE /api/apps/': 'container.undeploy',
'POST /api/backups/execute': 'backup.execute',
'POST /api/backups/restore/': 'backup.restore',
'POST /api/backups/config': 'backup.config',
'POST /api/config': 'config.update',
'DELETE /api/config': 'config.reset',
'POST /api/notifications/config': 'config.notifications',
'POST /api/totp/setup': 'auth.totp-setup',
'POST /api/totp/verify-setup': 'auth.totp-activate',
'POST /api/totp/disable': 'auth.totp-disable',
'POST /api/totp/config': 'auth.totp-config',
'POST /api/credentials/rotate-key': 'config.rotate-key',
'POST /api/updates/update/': 'container.update',
'POST /api/updates/rollback/': 'container.rollback',
'POST /api/updates/auto-update/': 'container.auto-update',
'POST /api/updates/check': 'container.check-updates',
'POST /api/health-checks/': 'config.health-check',
'DELETE /api/health-checks/': 'config.health-check-delete',
'POST /api/monitoring/alerts/': 'config.monitoring-alert',
'DELETE /api/monitoring/alerts/': 'config.monitoring-alert-delete',
'POST /api/arr/smart-connect': 'service.arr-connect',
'POST /api/arr/credentials': 'config.arr-credentials',
'DELETE /api/arr/credentials/': 'config.arr-credentials-delete',
'POST /api/logo': 'config.logo-upload',
'DELETE /api/logo': 'config.logo-delete',
'POST /api/favicon': 'config.favicon-upload',
'DELETE /api/favicon': 'config.favicon-delete',
'POST /api/tailscale/config': 'config.tailscale',
'POST /api/tailscale/protect-service': 'config.tailscale-protect',
'POST /api/v1/services/update': 'service.reorder',
'POST /api/v1/services': 'service.create',
'PUT /api/v1/services': 'service.update',
'DELETE /api/v1/services/': 'service.delete',
'POST /api/v1/site': 'caddy.add-site',
'POST /api/v1/site/external': 'caddy.add-external',
'DELETE /api/v1/site/': 'caddy.remove-site',
'POST /api/v1/caddy/reload': 'caddy.reload',
'POST /api/v1/dns/record': 'dns.add-record',
'DELETE /api/v1/dns/record': 'dns.delete-record',
'POST /api/v1/dns/credentials': 'dns.save-credentials',
'DELETE /api/v1/dns/credentials': 'dns.delete-credentials',
'POST /api/v1/dns/refresh-token': 'dns.refresh-token',
'POST /api/v1/dns/update': 'dns.update-server',
'POST /api/v1/containers/': 'container.action',
'DELETE /api/v1/containers/': 'container.delete',
'POST /api/v1/apps/deploy': 'container.deploy',
'DELETE /api/v1/apps/': 'container.undeploy',
'POST /api/v1/backups/execute': 'backup.execute',
'POST /api/v1/backups/restore/': 'backup.restore',
'POST /api/v1/backups/config': 'backup.config',
'POST /api/v1/config': 'config.update',
'DELETE /api/v1/config': 'config.reset',
'POST /api/v1/notifications/config': 'config.notifications',
'POST /api/v1/totp/setup': 'auth.totp-setup',
'POST /api/v1/totp/verify-setup': 'auth.totp-activate',
'POST /api/v1/totp/disable': 'auth.totp-disable',
'POST /api/v1/totp/config': 'auth.totp-config',
'POST /api/v1/credentials/rotate-key': 'config.rotate-key',
'POST /api/v1/updates/update/': 'container.update',
'POST /api/v1/updates/rollback/': 'container.rollback',
'POST /api/v1/updates/auto-update/': 'container.auto-update',
'POST /api/v1/updates/check': 'container.check-updates',
'POST /api/v1/health-checks/': 'config.health-check',
'DELETE /api/v1/health-checks/': 'config.health-check-delete',
'POST /api/v1/monitoring/alerts/': 'config.monitoring-alert',
'DELETE /api/v1/monitoring/alerts/': 'config.monitoring-alert-delete',
'POST /api/v1/arr/smart-connect': 'service.arr-connect',
'POST /api/v1/arr/credentials': 'config.arr-credentials',
'DELETE /api/v1/arr/credentials/': 'config.arr-credentials-delete',
'POST /api/v1/logo': 'config.logo-upload',
'DELETE /api/v1/logo': 'config.logo-delete',
'POST /api/v1/favicon': 'config.favicon-upload',
'DELETE /api/v1/favicon': 'config.favicon-delete',
'POST /api/v1/tailscale/config': 'config.tailscale',
'POST /api/v1/tailscale/protect-service': 'config.tailscale-protect',
};
// Paths to skip logging (noisy or internal)
const SKIP_PATHS = [
'/api/totp/verify',
'/api/totp/check-session',
'/api/auth/gate/',
'/api/auth/app-token/',
'/api/audit-logs',
'/api/health',
'/api/v1/totp/verify',
'/api/v1/totp/check-session',
'/api/v1/auth/gate/',
'/api/v1/auth/app-token/',
'/api/v1/audit-logs',
'/api/v1/health',
'/health',
'/api/notifications/test',
'/api/notifications/health-check',
'/api/v1/notifications/test',
'/api/v1/notifications/health-check',
];
class AuditLogger {
@@ -82,14 +82,14 @@ class AuditLogger {
if (key.startsWith(pattern)) return action;
}
// Fallback: derive from path
const parts = urlPath.replace('/api/', '').split('/');
const parts = urlPath.replace('/api/v1/', '').split('/');
const category = parts[0] || 'unknown';
return `${category}.${method.toLowerCase()}`;
}
extractResource(urlPath) {
// Pull a meaningful resource identifier from the URL path
const parts = urlPath.replace('/api/', '').split('/');
const parts = urlPath.replace('/api/v1/', '').split('/');
if (parts.length >= 2) return parts.slice(1).join('/');
return parts[0] || '';
}
+8 -8
View File
@@ -740,7 +740,7 @@ class BackupManager extends EventEmitter {
size: data.length
};
} finally {
try { await client.end(); } catch (_) {}
try { await client.end(); } catch (_) { /* ignore */ }
}
}
@@ -759,7 +759,7 @@ class BackupManager extends EventEmitter {
const buffer = await client.get(location.path);
return Buffer.isBuffer(buffer) ? buffer : Buffer.from(buffer);
} finally {
try { await client.end(); } catch (_) {}
try { await client.end(); } catch (_) { /* ignore */ }
}
}
@@ -786,7 +786,7 @@ class BackupManager extends EventEmitter {
// Delete the probe
try {
await this._deleteFromDestination(location);
} catch (_) {}
} catch (_) { /* ignore */ }
const elapsed = Date.now() - start;
return {
@@ -817,14 +817,14 @@ class BackupManager extends EventEmitter {
const { Dropbox } = require('dropbox');
const creds = await this._getCloudCredentials('dropbox');
const dbx = new Dropbox({ accessToken: creds.token });
try { await dbx.filesDeleteV2({ path: location.path }); } catch (_) {}
try { await dbx.filesDeleteV2({ path: location.path }); } catch (_) { /* ignore */ }
return;
}
if (location.type === 'webdav') {
const { createClient } = require('webdav');
const creds = await this._getCloudCredentials('webdav');
const client = createClient(creds.url, { username: creds.username, password: creds.password });
try { await client.deleteFile(location.path); } catch (_) {}
try { await client.deleteFile(location.path); } catch (_) { /* ignore */ }
return;
}
if (location.type === 'sftp') {
@@ -839,9 +839,9 @@ class BackupManager extends EventEmitter {
password: creds.password || undefined,
privateKey: creds.privateKey || undefined
});
try { await client.delete(location.path); } catch (_) {}
try { await client.delete(location.path); } catch (_) { /* ignore */ }
} finally {
try { await client.end(); } catch (_) {}
try { await client.end(); } catch (_) { /* ignore */ }
}
return;
}
@@ -895,7 +895,7 @@ class BackupManager extends EventEmitter {
recovered = true;
console.log(`[BackupManager] Loaded backup from fallback location ${backup.locations[i].type}`);
break;
} catch (_) {}
} catch (_) { /* ignore */ }
}
if (!recovered) throw loadErr;
}
+2 -2
View File
@@ -16,7 +16,7 @@ DashCA now provides automatic SSL certificate generation for services on your ne
### 1. Generate/Download Certificate
**GET** `/api/ca/cert/:domain`
**GET** `/api/v1/ca/cert/:domain`
Generate and download an SSL certificate for the specified domain.
@@ -52,7 +52,7 @@ Invoke-WebRequest -Uri "https://ca.sami/api/ca/cert/dns1.sami?format=pfx" -OutFi
### 2. List Generated Certificates
**GET** `/api/ca/certs`
**GET** `/api/v1/ca/certs`
List all generated certificates with their status and expiration information.
+2 -2
View File
@@ -113,7 +113,7 @@ The landing page (`index.html`) includes:
- **DashCaddy Theme:** Dark mode with Sami Grotesk font
**API Integration:**
- Loads certificate info from `/api/ca/info` endpoint
- Loads certificate info from `/api/v1/ca/info` endpoint
- Falls back to static info if API unavailable
## Development Workflow
@@ -234,7 +234,7 @@ Returns CA expiration health status:
**Symptom:** Certificate info stuck on loading state
**Cause:** API endpoint not accessible
**Solution:** Check that dashcaddy-api server is running and `/api/ca/info` responds
**Solution:** Check that dashcaddy-api server is running and `/api/v1/ca/info` responds
## Testing Checklist
+5 -5
View File
@@ -952,7 +952,7 @@
async function loadCertInfo() {
try {
// Add cache-busting parameter
const response = await fetch('/api/ca/info?t=' + Date.now());
const response = await fetch('/api/v1/ca/info?t=' + Date.now());
if (!response.ok) {
console.error('API response not OK:', response.status, response.statusText);
throw new Error('Failed to fetch certificate info');
@@ -1151,7 +1151,7 @@
// Load service certificates
async function loadServiceCertificates() {
try {
const response = await fetch('/api/ca/certs');
const response = await fetch('/api/v1/ca/certs');
if (!response.ok) {
throw new Error('Failed to fetch certificates');
}
@@ -1205,17 +1205,17 @@
<strong>Fingerprint:</strong><br>${cert.fingerprint}
</div>
<div class="cert-actions">
<a href="/api/ca/cert/${cert.domain}?format=pfx&password=dashcaddy"
<a href="/api/v1/ca/cert/${cert.domain}?format=pfx&password=dashcaddy"
class="cert-btn cert-btn-primary"
download="${cert.domain}.pfx">
Download PFX
</a>
<a href="/api/ca/cert/${cert.domain}?format=pem"
<a href="/api/v1/ca/cert/${cert.domain}?format=pem"
class="cert-btn"
download="${cert.domain}.pem">
PEM
</a>
<a href="/api/ca/cert/${cert.domain}?format=crt"
<a href="/api/v1/ca/cert/${cert.domain}?format=crt"
class="cert-btn"
download="${cert.domain}.crt">
CRT
+7 -9
View File
@@ -118,20 +118,18 @@ function csrfValidationMiddleware(req, res, next) {
// Excluded paths that don't require CSRF validation
const excludedPaths = [
'/api/totp/verify',
'/api/totp/verify-setup',
'/api/totp/setup',
'/api/v1/totp/verify',
'/api/v1/totp/verify-setup',
'/api/v1/totp/setup',
'/health',
'/api/health',
'/api/v1/health',
// Machine-to-machine: publishing host POSTs here with its own shared-secret
// header (X-DashCaddy-Notify-Secret) — browsers never reach this endpoint.
'/api/system/update-notify'
'/api/v1/system/update-notify'
];
// Normalize /api/v1/... to /api/... so exclusions work with both prefixes
const normalizedPath = req.path.replace(/^\/api\/v1\//, '/api/');
const isExcluded = excludedPaths.some(path => normalizedPath === path) ||
normalizedPath.startsWith('/api/auth/gate/');
const isExcluded = excludedPaths.some(path => req.path === path) ||
req.path.startsWith('/api/v1/auth/gate/');
if (isExcluded) {
return next();
+39 -41
View File
@@ -92,7 +92,7 @@ module.exports = function configureMiddleware(app, {
res.on('finish', () => {
const duration = Date.now() - start;
metrics.recordRequest(req.method, req.path, res.statusCode, duration);
if (req.path !== '/health' && req.path !== '/api/health') {
if (req.path !== '/health' && req.path !== '/api/v1/health') {
const level = res.statusCode >= 500 ? 'error' : res.statusCode >= 400 ? 'warn' : 'debug';
log[level]('http', `${req.method} ${req.path} ${res.statusCode}`, {
ms: duration, ip: req.ip, id: req.id
@@ -108,11 +108,11 @@ module.exports = function configureMiddleware(app, {
return next();
}
if (req.path === '/health' || req.path === '/api/health' || req.path.startsWith('/probe/')) {
if (req.path === '/health' || req.path === '/api/v1/health' || req.path.startsWith('/probe/')) {
return next();
}
if (req.path.startsWith('/api/tailscale/')) {
if (req.path.startsWith('/api/v1/tailscale/')) {
return next();
}
@@ -273,41 +273,39 @@ module.exports = function configureMiddleware(app, {
// ── Public routes (bypass TOTP and JWT auth) ──
const PUBLIC_ROUTES = [
{ path: '/health', exact: true },
{ path: '/api/health', exact: true },
{ path: '/probe/', prefix: true },
{ path: '/api/tailscale/', prefix: true },
{ path: '/api/totp/config', exact: true, method: 'GET' },
{ path: '/api/totp/verify', exact: true },
{ path: '/api/totp/setup', exact: true, method: 'POST' },
{ path: '/api/totp/verify-setup', exact: true, method: 'POST' },
{ path: '/api/totp/check-session', exact: true },
{ path: '/api/auth/gate/', prefix: true },
{ path: '/api/auth/app-token/', prefix: true },
{ path: '/api/services', exact: true, method: 'GET' },
{ path: '/api/ca/info', exact: true, method: 'GET' },
{ path: '/api/ca/root.crt', exact: true, method: 'GET' },
{ path: '/api/ca/install-script', exact: true, method: 'GET' },
{ path: '/api/health/ca', exact: true, method: 'GET' },
{ path: '/api/ca/cert/', prefix: true, method: 'GET' },
{ path: '/api/ca/certs', exact: true, method: 'GET' },
{ path: '/api/csrf-token', exact: true, method: 'GET' },
{ path: '/api/logo', exact: true, method: 'GET' },
{ path: '/api/favicon', exact: true, method: 'GET' },
{ path: '/api/themes', exact: true, method: 'GET' },
{ path: '/api/license/status', exact: true, method: 'GET' },
{ path: '/api/license/feature/', prefix: true, method: 'GET' },
{ path: '/api/config', exact: true, method: 'GET' },
{ path: '/api/services/status', exact: true, method: 'GET' },
{ path: '/api/system/update-notify', exact: true, method: 'POST' },
{ path: '/health', exact: true },
{ path: '/api/v1/health', exact: true },
{ path: '/probe/', prefix: true },
{ path: '/api/v1/tailscale/', prefix: true },
{ path: '/api/v1/totp/config', exact: true, method: 'GET' },
{ path: '/api/v1/totp/verify', exact: true },
{ path: '/api/v1/totp/setup', exact: true, method: 'POST' },
{ path: '/api/v1/totp/verify-setup', exact: true, method: 'POST' },
{ path: '/api/v1/totp/check-session', exact: true },
{ path: '/api/v1/auth/gate/', prefix: true },
{ path: '/api/v1/auth/app-token/', prefix: true },
{ path: '/api/v1/services', exact: true, method: 'GET' },
{ path: '/api/v1/ca/info', exact: true, method: 'GET' },
{ path: '/api/v1/ca/root.crt', exact: true, method: 'GET' },
{ path: '/api/v1/ca/install-script', exact: true, method: 'GET' },
{ path: '/api/v1/health/ca', exact: true, method: 'GET' },
{ path: '/api/v1/ca/cert/', prefix: true, method: 'GET' },
{ path: '/api/v1/ca/certs', exact: true, method: 'GET' },
{ path: '/api/v1/csrf-token', exact: true, method: 'GET' },
{ path: '/api/v1/logo', exact: true, method: 'GET' },
{ path: '/api/v1/favicon', exact: true, method: 'GET' },
{ path: '/api/v1/themes', exact: true, method: 'GET' },
{ path: '/api/v1/license/status', exact: true, method: 'GET' },
{ path: '/api/v1/license/feature/', prefix: true, method: 'GET' },
{ path: '/api/v1/config', exact: true, method: 'GET' },
{ path: '/api/v1/services/status', exact: true, method: 'GET' },
{ path: '/api/v1/system/update-notify', exact: true, method: 'POST' },
];
function isPublicRoute(req) {
// Normalize /api/v1/... to /api/... so public routes work with both
const p = req.path.replace(/^\/api\/v1\//, '/api/');
return PUBLIC_ROUTES.some(r => {
if (r.method && req.method !== r.method) return false;
return r.prefix ? p.startsWith(r.path) : p === r.path;
return r.prefix ? req.path.startsWith(r.path) : req.path === r.path;
});
}
@@ -389,7 +387,7 @@ module.exports = function configureMiddleware(app, {
...RATE_LIMITS.GENERAL,
standardHeaders: true,
legacyHeaders: false,
skip: (req) => isTest || req.path === '/health' || req.path === '/api/health' || req.path.startsWith('/probe/') || req.path.startsWith('/api/auth/gate/') || req.path === '/api/totp/check-session' || req.path.endsWith('/health-checks/status') || req.path.endsWith('/csrf-token') || req.path === '/api/v1/dns/logs' || req.path === '/api/license/status' || req.path.startsWith('/api/license/feature/') || req.path === '/api/services' || req.path === '/api/config',
skip: (req) => isTest || req.path === '/health' || req.path === '/api/v1/health' || req.path.startsWith('/probe/') || req.path.startsWith('/api/v1/auth/gate/') || req.path === '/api/v1/totp/check-session' || req.path.endsWith('/health-checks/status') || req.path.endsWith('/csrf-token') || req.path === '/api/v1/dns/logs' || req.path === '/api/v1/license/status' || req.path.startsWith('/api/v1/license/feature/') || req.path === '/api/v1/services' || req.path === '/api/v1/config',
message: { success: false, error: 'Too many requests, please try again later' }
});
@@ -402,11 +400,11 @@ module.exports = function configureMiddleware(app, {
});
app.use(generalLimiter);
app.use('/api/dns/credentials', strictLimiter);
app.use('/api/apps/deploy', strictLimiter);
app.use('/api/backup/restore', strictLimiter);
app.use('/api/site', strictLimiter);
app.use('/api/credentials/rotate-key', strictLimiter);
app.use('/api/v1/dns/credentials', strictLimiter);
app.use('/api/v1/apps/deploy', strictLimiter);
app.use('/api/v1/backup/restore', strictLimiter);
app.use('/api/v1/site', strictLimiter);
app.use('/api/v1/credentials/rotate-key', strictLimiter);
const totpLimiter = rateLimit({
...RATE_LIMITS.TOTP,
@@ -414,8 +412,8 @@ module.exports = function configureMiddleware(app, {
legacyHeaders: false,
message: { success: false, error: 'Too many TOTP attempts, please try again later' }
});
app.use('/api/totp/verify', totpLimiter);
app.use('/api/totp/verify-setup', totpLimiter);
app.use('/api/v1/totp/verify', totpLimiter);
app.use('/api/v1/totp/verify-setup', totpLimiter);
// ── Audit logging middleware (logs non-GET API requests) ──
app.use(auditLogger.middleware());
+110 -110
View File
@@ -83,7 +83,7 @@ paths:
type: string
example: ok
/api/health:
/api/v1/health:
get:
tags: [Health & Status]
summary: API health check
@@ -118,7 +118,7 @@ paths:
schema:
$ref: '#/components/schemas/SuccessResponse'
/api/network/ips:
/api/v1/network/ips:
get:
tags: [Health & Status]
summary: Get network interface IPs
@@ -145,7 +145,7 @@ paths:
type: string
# TOTP Authentication
/api/totp/config:
/api/v1/totp/config:
get:
tags: [TOTP Authentication]
summary: Get TOTP configuration
@@ -184,7 +184,7 @@ paths:
schema:
$ref: '#/components/schemas/SuccessResponse'
/api/totp/setup:
/api/v1/totp/setup:
post:
tags: [TOTP Authentication]
summary: Generate TOTP secret
@@ -206,7 +206,7 @@ paths:
otpAuthUrl:
type: string
/api/totp/verify-setup:
/api/v1/totp/verify-setup:
post:
tags: [TOTP Authentication]
summary: Verify and activate TOTP
@@ -229,7 +229,7 @@ paths:
schema:
$ref: '#/components/schemas/SuccessResponse'
/api/totp/verify:
/api/v1/totp/verify:
post:
tags: [TOTP Authentication]
summary: Verify TOTP code and create session
@@ -259,7 +259,7 @@ paths:
type: string
format: date-time
/api/totp/check-session:
/api/v1/totp/check-session:
get:
tags: [TOTP Authentication]
summary: Check if session is valid
@@ -276,7 +276,7 @@ paths:
valid:
type: boolean
/api/totp/disable:
/api/v1/totp/disable:
post:
tags: [TOTP Authentication]
summary: Disable TOTP
@@ -299,7 +299,7 @@ paths:
$ref: '#/components/schemas/SuccessResponse'
# SSO Auth Gate
/api/auth/gate/{serviceId}:
/api/v1/auth/gate/{serviceId}:
get:
tags: [SSO Auth Gate]
summary: Forward auth endpoint for Caddy
@@ -315,7 +315,7 @@ paths:
'401':
description: Auth failed
/api/auth/app-token/{serviceId}:
/api/v1/auth/app-token/{serviceId}:
get:
tags: [SSO Auth Gate]
summary: Get app-specific session token
@@ -339,7 +339,7 @@ paths:
type: string
# Service Credentials
/api/service-creds/{serviceId}:
/api/v1/service-creds/{serviceId}:
post:
tags: [Service Credentials]
summary: Store service credentials
@@ -408,7 +408,7 @@ paths:
schema:
$ref: '#/components/schemas/SuccessResponse'
/api/seedhost-creds:
/api/v1/seedhost-creds:
post:
tags: [Service Credentials]
summary: Store seedhost credentials
@@ -459,7 +459,7 @@ paths:
$ref: '#/components/schemas/SuccessResponse'
# Tailscale
/api/tailscale/status:
/api/v1/tailscale/status:
get:
tags: [Tailscale]
summary: Get Tailscale status
@@ -482,7 +482,7 @@ paths:
hostname:
type: string
/api/tailscale/config:
/api/v1/tailscale/config:
post:
tags: [Tailscale]
summary: Update Tailscale config
@@ -504,7 +504,7 @@ paths:
schema:
$ref: '#/components/schemas/SuccessResponse'
/api/tailscale/check-connection:
/api/v1/tailscale/check-connection:
get:
tags: [Tailscale]
summary: Check if request is from Tailscale
@@ -521,7 +521,7 @@ paths:
isTailscale:
type: boolean
/api/tailscale/devices:
/api/v1/tailscale/devices:
get:
tags: [Tailscale]
summary: List Tailscale devices
@@ -540,7 +540,7 @@ paths:
items:
type: object
/api/tailscale/protect-service:
/api/v1/tailscale/protect-service:
post:
tags: [Tailscale]
summary: Add Tailscale ACLs
@@ -563,7 +563,7 @@ paths:
$ref: '#/components/schemas/SuccessResponse'
# Caddy Management
/api/caddyfile:
/api/v1/caddyfile:
get:
tags: [Caddy Management]
summary: Read Caddyfile
@@ -580,7 +580,7 @@ paths:
content:
type: string
/api/caddy/config:
/api/v1/caddy/config:
get:
tags: [Caddy Management]
summary: Get Caddy admin config
@@ -597,7 +597,7 @@ paths:
config:
type: object
/api/caddy/reload:
/api/v1/caddy/reload:
post:
tags: [Caddy Management]
summary: Reload Caddy
@@ -609,7 +609,7 @@ paths:
schema:
$ref: '#/components/schemas/SuccessResponse'
/api/caddy/get-cas:
/api/v1/caddy/get-cas:
get:
tags: [Caddy Management]
summary: Get certificate authorities
@@ -629,7 +629,7 @@ paths:
type: object
# Site Management
/api/site:
/api/v1/site:
post:
tags: [Site Management]
summary: Add site to Caddyfile
@@ -655,7 +655,7 @@ paths:
schema:
$ref: '#/components/schemas/SuccessResponse'
/api/site/external:
/api/v1/site/external:
post:
tags: [Site Management]
summary: Add external service proxy
@@ -683,7 +683,7 @@ paths:
schema:
$ref: '#/components/schemas/SuccessResponse'
/api/site/{domain}:
/api/v1/site/{domain}:
delete:
tags: [Site Management]
summary: Remove site from Caddyfile
@@ -702,7 +702,7 @@ paths:
$ref: '#/components/schemas/SuccessResponse'
# DNS Management
/api/dns/record:
/api/v1/dns/record:
post:
tags: [DNS Management]
summary: Create DNS record
@@ -757,7 +757,7 @@ paths:
schema:
$ref: '#/components/schemas/SuccessResponse'
/api/dns/resolve:
/api/v1/dns/resolve:
get:
tags: [DNS Management]
summary: Resolve DNS
@@ -790,7 +790,7 @@ paths:
items:
type: object
/api/dns/logs:
/api/v1/dns/logs:
get:
tags: [DNS Management]
summary: Get DNS query logs
@@ -826,7 +826,7 @@ paths:
items:
type: object
/api/dns/token-status:
/api/v1/dns/token-status:
get:
tags: [DNS Management]
summary: Check DNS token status
@@ -843,7 +843,7 @@ paths:
valid:
type: boolean
/api/dns/credentials:
/api/v1/dns/credentials:
post:
tags: [DNS Management]
summary: Store DNS credentials
@@ -894,7 +894,7 @@ paths:
schema:
$ref: '#/components/schemas/SuccessResponse'
/api/dns/refresh-token:
/api/v1/dns/refresh-token:
post:
tags: [DNS Management]
summary: Refresh DNS API token
@@ -906,7 +906,7 @@ paths:
schema:
$ref: '#/components/schemas/SuccessResponse'
/api/dns/check-update:
/api/v1/dns/check-update:
get:
tags: [DNS Management]
summary: Check for DNS server updates
@@ -923,7 +923,7 @@ paths:
updateAvailable:
type: boolean
/api/dns/update:
/api/v1/dns/update:
post:
tags: [DNS Management]
summary: Update DNS server
@@ -936,7 +936,7 @@ paths:
$ref: '#/components/schemas/SuccessResponse'
# Services Dashboard
/api/services:
/api/v1/services:
get:
tags: [Services Dashboard]
summary: List all services
@@ -992,7 +992,7 @@ paths:
schema:
$ref: '#/components/schemas/SuccessResponse'
/api/services/{id}:
/api/v1/services/{id}:
delete:
tags: [Services Dashboard]
summary: Delete service
@@ -1010,7 +1010,7 @@ paths:
schema:
$ref: '#/components/schemas/SuccessResponse'
/api/services/update:
/api/v1/services/update:
post:
tags: [Services Dashboard]
summary: Reorder services
@@ -1034,7 +1034,7 @@ paths:
$ref: '#/components/schemas/SuccessResponse'
# Assets & Branding
/api/assets/upload:
/api/v1/assets/upload:
post:
tags: [Assets & Branding]
summary: Upload asset file
@@ -1061,7 +1061,7 @@ paths:
filename:
type: string
/api/logo:
/api/v1/logo:
get:
tags: [Assets & Branding]
summary: Get custom logo
@@ -1104,7 +1104,7 @@ paths:
schema:
$ref: '#/components/schemas/SuccessResponse'
/api/favicon:
/api/v1/favicon:
get:
tags: [Assets & Branding]
summary: Get custom favicon
@@ -1148,7 +1148,7 @@ paths:
$ref: '#/components/schemas/SuccessResponse'
# Configuration
/api/config:
/api/v1/config:
get:
tags: [Configuration]
summary: Get DashCaddy config
@@ -1192,7 +1192,7 @@ paths:
$ref: '#/components/schemas/SuccessResponse'
# Backup & Restore
/api/backup/export:
/api/v1/backup/export:
get:
tags: [Backup & Restore]
summary: Export full backup
@@ -1209,7 +1209,7 @@ paths:
backup:
type: object
/api/backup/preview:
/api/v1/backup/preview:
post:
tags: [Backup & Restore]
summary: Preview backup contents
@@ -1235,7 +1235,7 @@ paths:
preview:
type: object
/api/backup/restore:
/api/v1/backup/restore:
post:
tags: [Backup & Restore]
summary: Restore from backup
@@ -1257,7 +1257,7 @@ paths:
$ref: '#/components/schemas/SuccessResponse'
# Credential Management
/api/credentials/list:
/api/v1/credentials/list:
get:
tags: [Credential Management]
summary: List all stored credentials
@@ -1276,7 +1276,7 @@ paths:
items:
type: string
/api/credentials/rotate-key:
/api/v1/credentials/rotate-key:
post:
tags: [Credential Management]
summary: Rotate encryption key
@@ -1289,7 +1289,7 @@ paths:
$ref: '#/components/schemas/SuccessResponse'
# Arr Stack Integration
/api/arr/detect:
/api/v1/arr/detect:
get:
tags: [Arr Stack Integration]
summary: Detect installed Arr apps
@@ -1308,7 +1308,7 @@ paths:
items:
type: object
/api/arr/configure-overseerr:
/api/v1/arr/configure-overseerr:
post:
tags: [Arr Stack Integration]
summary: Configure Overseerr
@@ -1330,7 +1330,7 @@ paths:
schema:
$ref: '#/components/schemas/SuccessResponse'
/api/arr/test-connection:
/api/v1/arr/test-connection:
post:
tags: [Arr Stack Integration]
summary: Test Arr service connection
@@ -1359,7 +1359,7 @@ paths:
connected:
type: boolean
/api/arr/auto-setup:
/api/v1/arr/auto-setup:
post:
tags: [Arr Stack Integration]
summary: Automatic Arr stack setup
@@ -1371,7 +1371,7 @@ paths:
schema:
$ref: '#/components/schemas/SuccessResponse'
/api/arr/credentials:
/api/v1/arr/credentials:
post:
tags: [Arr Stack Integration]
summary: Store Arr credentials
@@ -1409,7 +1409,7 @@ paths:
credentials:
type: object
/api/arr/credentials/{service}:
/api/v1/arr/credentials/{service}:
delete:
tags: [Arr Stack Integration]
summary: Delete Arr credentials
@@ -1427,7 +1427,7 @@ paths:
schema:
$ref: '#/components/schemas/SuccessResponse'
/api/arr/smart-detect:
/api/v1/arr/smart-detect:
get:
tags: [Arr Stack Integration]
summary: Smart detection of Arr services
@@ -1444,7 +1444,7 @@ paths:
detected:
type: object
/api/arr/smart-connect:
/api/v1/arr/smart-connect:
post:
tags: [Arr Stack Integration]
summary: Smart connect Arr stack
@@ -1471,7 +1471,7 @@ paths:
type: object
# Plex
/api/plex/libraries:
/api/v1/plex/libraries:
get:
tags: [Plex]
summary: Get Plex libraries
@@ -1491,7 +1491,7 @@ paths:
type: object
# Docker App Deployment
/api/apps/templates:
/api/v1/apps/templates:
get:
tags: [Docker App Deployment]
summary: Get all app templates
@@ -1510,7 +1510,7 @@ paths:
items:
$ref: '#/components/schemas/AppTemplate'
/api/apps/templates/{appId}:
/api/v1/apps/templates/{appId}:
get:
tags: [Docker App Deployment]
summary: Get specific template
@@ -1533,7 +1533,7 @@ paths:
template:
$ref: '#/components/schemas/AppTemplate'
/api/apps/check-port/{port}:
/api/v1/apps/check-port/{port}:
get:
tags: [Docker App Deployment]
summary: Check port availability
@@ -1556,7 +1556,7 @@ paths:
available:
type: boolean
/api/apps/suggest-port/{basePort}:
/api/v1/apps/suggest-port/{basePort}:
get:
tags: [Docker App Deployment]
summary: Suggest next available port
@@ -1579,7 +1579,7 @@ paths:
port:
type: integer
/api/apps/check-existing:
/api/v1/apps/check-existing:
post:
tags: [Docker App Deployment]
summary: Check if app deployed
@@ -1605,7 +1605,7 @@ paths:
exists:
type: boolean
/api/apps/deploy:
/api/v1/apps/deploy:
post:
tags: [Docker App Deployment]
summary: Deploy Docker app
@@ -1646,7 +1646,7 @@ paths:
url:
type: string
/api/apps/{appId}:
/api/v1/apps/{appId}:
delete:
tags: [Docker App Deployment]
summary: Delete deployed app
@@ -1664,7 +1664,7 @@ paths:
schema:
$ref: '#/components/schemas/SuccessResponse'
/api/apps/update-subdomain:
/api/v1/apps/update-subdomain:
post:
tags: [Docker App Deployment]
summary: Update app subdomain
@@ -1689,7 +1689,7 @@ paths:
$ref: '#/components/schemas/SuccessResponse'
# Container Management
/api/containers/{id}/start:
/api/v1/containers/{id}/start:
post:
tags: [Container Management]
summary: Start container
@@ -1707,7 +1707,7 @@ paths:
schema:
$ref: '#/components/schemas/SuccessResponse'
/api/containers/{id}/stop:
/api/v1/containers/{id}/stop:
post:
tags: [Container Management]
summary: Stop container
@@ -1725,7 +1725,7 @@ paths:
schema:
$ref: '#/components/schemas/SuccessResponse'
/api/containers/{id}/restart:
/api/v1/containers/{id}/restart:
post:
tags: [Container Management]
summary: Restart container
@@ -1743,7 +1743,7 @@ paths:
schema:
$ref: '#/components/schemas/SuccessResponse'
/api/containers/{id}/update:
/api/v1/containers/{id}/update:
post:
tags: [Container Management]
summary: Update container image
@@ -1761,7 +1761,7 @@ paths:
schema:
$ref: '#/components/schemas/SuccessResponse'
/api/containers/{id}/check-update:
/api/v1/containers/{id}/check-update:
get:
tags: [Container Management]
summary: Check for container updates
@@ -1784,7 +1784,7 @@ paths:
updateAvailable:
type: boolean
/api/containers/{id}/logs:
/api/v1/containers/{id}/logs:
get:
tags: [Container Management]
summary: Get container logs
@@ -1815,7 +1815,7 @@ paths:
logs:
type: string
/api/containers/{id}:
/api/v1/containers/{id}:
delete:
tags: [Container Management]
summary: Delete container
@@ -1833,7 +1833,7 @@ paths:
schema:
$ref: '#/components/schemas/SuccessResponse'
/api/containers/discover:
/api/v1/containers/discover:
get:
tags: [Container Management]
summary: Discover unmanaged containers
@@ -1853,7 +1853,7 @@ paths:
type: object
# Notifications
/api/notifications/config:
/api/v1/notifications/config:
get:
tags: [Notifications]
summary: Get notification config
@@ -1891,7 +1891,7 @@ paths:
schema:
$ref: '#/components/schemas/SuccessResponse'
/api/notifications/test:
/api/v1/notifications/test:
post:
tags: [Notifications]
summary: Send test notification
@@ -1903,7 +1903,7 @@ paths:
schema:
$ref: '#/components/schemas/SuccessResponse'
/api/notifications/history:
/api/v1/notifications/history:
get:
tags: [Notifications]
summary: Get notification history
@@ -1932,7 +1932,7 @@ paths:
schema:
$ref: '#/components/schemas/SuccessResponse'
/api/notifications/health-check:
/api/v1/notifications/health-check:
post:
tags: [Notifications]
summary: Trigger health check notification
@@ -1945,7 +1945,7 @@ paths:
$ref: '#/components/schemas/SuccessResponse'
# Container Stats & Logs
/api/stats/containers:
/api/v1/stats/containers:
get:
tags: [Container Stats & Logs]
summary: Get all container stats
@@ -1964,7 +1964,7 @@ paths:
items:
$ref: '#/components/schemas/ContainerStats'
/api/stats/container/{id}:
/api/v1/stats/container/{id}:
get:
tags: [Container Stats & Logs]
summary: Get specific container stats
@@ -1987,7 +1987,7 @@ paths:
stats:
$ref: '#/components/schemas/ContainerStats'
/api/logs/containers:
/api/v1/logs/containers:
get:
tags: [Container Stats & Logs]
summary: List containers with logs
@@ -2006,7 +2006,7 @@ paths:
items:
type: object
/api/logs/container/{id}:
/api/v1/logs/container/{id}:
get:
tags: [Container Stats & Logs]
summary: Get container log entries
@@ -2031,7 +2031,7 @@ paths:
items:
type: string
/api/logs/stream/{id}:
/api/v1/logs/stream/{id}:
get:
tags: [Container Stats & Logs]
summary: Stream container logs (SSE)
@@ -2049,7 +2049,7 @@ paths:
schema:
type: string
/api/logs/file:
/api/v1/logs/file:
get:
tags: [Container Stats & Logs]
summary: Read native log file
@@ -2077,7 +2077,7 @@ paths:
type: string
# Service Health
/api/health/services:
/api/v1/health/services:
get:
tags: [Service Health]
summary: Full health check for all services
@@ -2096,7 +2096,7 @@ paths:
items:
type: object
/api/health/cached:
/api/v1/health/cached:
get:
tags: [Service Health]
summary: Cached health results
@@ -2113,7 +2113,7 @@ paths:
cached:
type: object
/api/health/service/{id}:
/api/v1/health/service/{id}:
get:
tags: [Service Health]
summary: Health for specific service
@@ -2137,7 +2137,7 @@ paths:
type: boolean
# Resource Monitoring
/api/monitoring/stats:
/api/v1/monitoring/stats:
get:
tags: [Resource Monitoring]
summary: All container resource stats
@@ -2156,7 +2156,7 @@ paths:
items:
$ref: '#/components/schemas/ContainerStats'
/api/monitoring/stats/{containerId}:
/api/v1/monitoring/stats/{containerId}:
get:
tags: [Resource Monitoring]
summary: Specific container stats
@@ -2179,7 +2179,7 @@ paths:
stats:
$ref: '#/components/schemas/ContainerStats'
/api/monitoring/history/{containerId}:
/api/v1/monitoring/history/{containerId}:
get:
tags: [Resource Monitoring]
summary: Historical stats
@@ -2208,7 +2208,7 @@ paths:
items:
type: object
/api/monitoring/aggregated/{containerId}:
/api/v1/monitoring/aggregated/{containerId}:
get:
tags: [Resource Monitoring]
summary: Aggregated stats
@@ -2235,7 +2235,7 @@ paths:
aggregated:
type: object
/api/monitoring/alerts/{containerId}:
/api/v1/monitoring/alerts/{containerId}:
post:
tags: [Resource Monitoring]
summary: Configure alerts
@@ -2304,7 +2304,7 @@ paths:
$ref: '#/components/schemas/SuccessResponse'
# Automated Backups
/api/backups/config:
/api/v1/backups/config:
get:
tags: [Automated Backups]
summary: Get backup config
@@ -2344,7 +2344,7 @@ paths:
schema:
$ref: '#/components/schemas/SuccessResponse'
/api/backups/execute:
/api/v1/backups/execute:
post:
tags: [Automated Backups]
summary: Run manual backup
@@ -2361,7 +2361,7 @@ paths:
backupId:
type: string
/api/backups/history:
/api/v1/backups/history:
get:
tags: [Automated Backups]
summary: Get backup history
@@ -2380,7 +2380,7 @@ paths:
items:
type: object
/api/backups/restore/{backupId}:
/api/v1/backups/restore/{backupId}:
post:
tags: [Automated Backups]
summary: Restore from backup
@@ -2399,7 +2399,7 @@ paths:
$ref: '#/components/schemas/SuccessResponse'
# Health Checks
/api/health-check/status:
/api/v1/health-check/status:
get:
tags: [Health Checks]
summary: All service health status
@@ -2418,7 +2418,7 @@ paths:
items:
type: object
/api/health-check/stats/{serviceId}:
/api/v1/health-check/stats/{serviceId}:
get:
tags: [Health Checks]
summary: Detailed service stats
@@ -2445,7 +2445,7 @@ paths:
stats:
type: object
/api/health-check/configure/{serviceId}:
/api/v1/health-check/configure/{serviceId}:
post:
tags: [Health Checks]
summary: Configure health check
@@ -2492,7 +2492,7 @@ paths:
schema:
$ref: '#/components/schemas/SuccessResponse'
/api/health-check/incidents:
/api/v1/health-check/incidents:
get:
tags: [Health Checks]
summary: Open incidents
@@ -2511,7 +2511,7 @@ paths:
items:
type: object
/api/health-check/incidents/history:
/api/v1/health-check/incidents/history:
get:
tags: [Health Checks]
summary: Incident history
@@ -2536,7 +2536,7 @@ paths:
type: object
# Update Management
/api/updates/check:
/api/v1/updates/check:
post:
tags: [Update Management]
summary: Check for updates
@@ -2555,7 +2555,7 @@ paths:
items:
type: object
/api/updates/available:
/api/v1/updates/available:
get:
tags: [Update Management]
summary: Get available updates
@@ -2574,7 +2574,7 @@ paths:
items:
type: object
/api/updates/update/{containerId}:
/api/v1/updates/update/{containerId}:
post:
tags: [Update Management]
summary: Update container
@@ -2592,7 +2592,7 @@ paths:
schema:
$ref: '#/components/schemas/SuccessResponse'
/api/updates/rollback/{containerId}:
/api/v1/updates/rollback/{containerId}:
post:
tags: [Update Management]
summary: Rollback container
@@ -2610,7 +2610,7 @@ paths:
schema:
$ref: '#/components/schemas/SuccessResponse'
/api/updates/history:
/api/v1/updates/history:
get:
tags: [Update Management]
summary: Get update history
@@ -2634,7 +2634,7 @@ paths:
items:
type: object
/api/updates/auto-update/{containerId}:
/api/v1/updates/auto-update/{containerId}:
post:
tags: [Update Management]
summary: Configure auto-update
@@ -2661,7 +2661,7 @@ paths:
schema:
$ref: '#/components/schemas/SuccessResponse'
/api/updates/schedule/{containerId}:
/api/v1/updates/schedule/{containerId}:
post:
tags: [Update Management]
summary: Schedule update
@@ -2690,7 +2690,7 @@ paths:
$ref: '#/components/schemas/SuccessResponse'
# Error Logs
/api/error-logs:
/api/v1/error-logs:
get:
tags: [Error Logs]
summary: View error logs
@@ -2720,7 +2720,7 @@ paths:
$ref: '#/components/schemas/SuccessResponse'
# Filesystem Browser
/api/browse/roots:
/api/v1/browse/roots:
get:
tags: [Filesystem Browser]
summary: Get browseable roots
@@ -2739,7 +2739,7 @@ paths:
items:
type: string
/api/browse/dir:
/api/v1/browse/dir:
get:
tags: [Filesystem Browser]
summary: Browse directory
@@ -2771,7 +2771,7 @@ paths:
size:
type: integer
/api/media/detected-mounts:
/api/v1/media/detected-mounts:
get:
tags: [Filesystem Browser]
summary: Detect media mounts
@@ -2791,7 +2791,7 @@ paths:
type: object
# Audit Log
/api/audit-log:
/api/v1/audit-log:
get:
tags: [Audit Log]
summary: Query audit log
@@ -2844,7 +2844,7 @@ paths:
$ref: '#/components/schemas/SuccessResponse'
# API Documentation
/api/docs:
/api/v1/docs:
get:
tags: [API Documentation]
summary: API docs UI
@@ -2856,7 +2856,7 @@ paths:
schema:
type: string
/api/docs/spec:
/api/v1/docs/spec:
get:
tags: [API Documentation]
summary: OpenAPI spec
+1 -1
View File
@@ -146,7 +146,7 @@ module.exports = function({ backupManager, asyncHandler }) {
};
for (const k of keys[provider]) {
try { await credentialManager.delete(k); } catch (_) {}
try { await credentialManager.delete(k); } catch (_) { /* ignore */ }
}
success(res, { message: `${provider} credentials deleted` });
+5 -6
View File
@@ -446,9 +446,8 @@ async function createApp() {
res.json({ success: true, metrics: metrics.getSummary() });
});
// Mount at /api/v1 (canonical) and /api (legacy)
// Mount at /api/v1 (canonical, single version)
app.use('/api/v1', apiRouter);
app.use('/api', apiRouter);
// Root-level health check
app.get('/health', (req, res) => {
@@ -548,7 +547,7 @@ async function createApp() {
}, 'probe'));
// Network IPs endpoint
app.get('/api/network/ips', (req, res) => {
app.get('/api/v1/network/ips', (req, res) => {
try {
const os = require('os');
const envLan = process.env.HOST_LAN_IP;
@@ -585,7 +584,7 @@ async function createApp() {
});
// API Documentation
app.get('/api/docs', (req, res) => {
app.get('/api/v1/docs', (req, res) => {
res.setHeader('Content-Security-Policy', "default-src 'self'; script-src 'self' 'unsafe-inline' https://unpkg.com; style-src 'self' 'unsafe-inline' https://unpkg.com; img-src 'self' data: https:; connect-src 'self'; font-src 'self' data: https://unpkg.com;");
res.send(`<!DOCTYPE html>
<html lang="en">
@@ -598,12 +597,12 @@ async function createApp() {
<body>
<div id="swagger-ui"></div>
<script src="https://unpkg.com/swagger-ui-dist@5/swagger-ui-bundle.js"></script>
<script>SwaggerUIBundle({url:'/api/docs/spec',dom_id:'#swagger-ui',deepLinking:true})</script>
<script>SwaggerUIBundle({url:'/api/v1/docs/spec',dom_id:'#swagger-ui',deepLinking:true})</script>
</body>
</html>`);
});
app.get('/api/docs/spec', boundAsyncHandler(async (req, res) => {
app.get('/api/v1/docs/spec', boundAsyncHandler(async (req, res) => {
const path = require('path');
const { exists } = require('../fs-helpers');
const fsp = require('fs').promises;