9 Commits
Author SHA1 Message Date
DashCaddy Polish Loop a9bb4a1835 fix(caddy-upstreams): validate host is known upstream on all 3 mute endpoints (DC-073) [glm-grade=A]
Bug class: silent state corruption via path-style endpoint inconsistency.

Pre-fix, only POST /caddy/upstreams/mute (bare body-style) rejected unknown
hosts with a 400. The path-style POST /caddy/upstreams/:host/mute and
POST /caddy/upstreams/:host/unmute endpoints skipped that check entirely.
An authenticated operator could POST /caddy/upstreams/phantom.test:12345/mute
and caddyUpstreamWatcher.setMuted() would silently add the phantom host
to its muted Set and _saveState() would persist it to disk. The phantom
entry survives container restarts and pollutes the snapshot view.

Fix: consolidate validation in a single validateAndMuteHost() helper used
by all three mute endpoints. The helper enforces (1) host format charset,
(2) length cap, (3) membership in caddyUpstreamWatcher.upstreams (the
live registry populated by scanSites()). No phantom host can reach setMuted.

Tests: 15 new regression tests in
__tests__/routes/caddy-upstreams-dc073.routes.test.js — exercises the
helper directly (unit) and via each endpoint (integration), asserts
rejection happens BEFORE setMuted is called (no state corruption), and
the existing 3 caddy-upstreams.routes.test.js cases still pass. Router
introspection test asserts no duplicate route registrations.

Full suite: 2342/2342 tests / 101 suites.
2026-08-18 15:10:36 -07:00
Hermes 1c02131fe0 fix(server): DC-058 close 3 P0 bugs — restore.js missing dep, dns.js ok ref, dead /billing/checkout
CI / Test & Lint (push) Has been cancelled
CI / Security audit (push) Has been cancelled
1. routes/apps/restore.js: backupManager was being passed by the
   aggregator (routes/apps/index.js:58) but never destructured in the
   factory signature. Every apps/restore request 500'd with
   ReferenceError. Added backupManager to the destructure + an explicit
   throw if missing so the next regression surfaces at startup instead
   of at the first call.

2. routes/dns.js:555: file imports { success, error } from
   ../src/utils/responses but used ok(res, ...) (defunct alias). DNS
   credential save path 500'd. Changed to success() to match the rest
   of the file.

3. src/utilities/middleware.js: deleted /api/v1/billing/checkout from
   PUBLIC_ROUTES — dead entry, no route mounted. Drift test caught it
   (DC-017 guard). Updated the comment to cover both checkout + webhook
   as removed.

Tests: 1428/1428 pass (drift test now green).
Lint: 0 no-undef errors across src/ + routes/ (was 7).

Refs: DashCaddy audit 2026-08-02
2026-08-03 00:43:34 -07:00
Hermes 7f6203b2f7 fix(readme): correct license badge — MIT to Proprietary EULA, bump version badge to 1.15.0
The README showed MIT license and version 1.0.0 — both wrong. LICENSE
file is a 125-line proprietary EULA (added at v1.5.0). Version badge
was stale from initial release.

Refs: DashCaddy audit 2026-08-02
2026-08-03 00:37:15 -07:00
Hermes Agent bd480a69a7 BACKLOG + CHANGELOG: mark DC-049 done
CI / Test & Lint (push) Has been cancelled
CI / Security audit (push) Has been cancelled
Track record of the auth-gate UI work plus the historical delta from
DC-046/047/050/049 commit chain. No code changes.
2026-07-20 02:23:26 -07:00
Hermes Agent c54739e110 DC-049 follow-on: email fallback link on TOTP-only overlay
CI / Test & Lint (push) Has been cancelled
CI / Security audit (push) Has been cancelled
When only TOTP is enabled (today's production state for everyone),
auth-gate.js was falling through to the legacy TOTP overlay with no
visible path to the email provider. The email method was unreachable
from the UI even when configured. Fixed: append a small 'Or sign in
with email instead ->' link to the bottom of the TOTP card. Clicking
swaps the body to the email challenge form.

Why this matters even for the single-totp path: email is the
phone-friendly, no-app-required recovery path. Operator forgets their
TOTP secret at 2am, they can request a link without touching the
authenticator app. The link just wasn't reachable before.

Renders the link only when the methods response includes both totp
and email — preserves the truly-single-provider case unchanged.
2026-07-20 02:21:59 -07:00
Hermes Agent 56f1a001f2 start.sh auto-sync dashboard bundle into /var/www/dashcaddy-status/
CI / Test & Lint (push) Has been cancelled
CI / Security audit (push) Has been cancelled
After every rebuild the freshly-baked dashboard bundle lives in
/opt/dashcaddy/status/dist/ + sw.js. DNS2 also serves files from
/var/www/dashcaddy-status/dist/ + sw.js (the original Windows-installer
mirror path). Without an explicit copy step between build and start.sh,
the served bundle stays on whatever hash was there before, while the API
responds with new code. That mismatch is what shows up in the dashboard
as "version unavailable" + "no data" widgets — saw it in the deploy
that followed DC-046/047 (fixed by a manual cp this time, never again).

Sync block runs before docker run:
  cp /opt/dashcaddy/status/dist/*.js /var/www/dashcaddy-status/dist/
  cp /opt/dashcaddy/status/sw.js   /var/www/dashcaddy-status/
  cp /opt/dashcaddy/status/index.html /var/www/dashcaddy-status/

All  guarded so set -e doesn't kill the container start on a
single per-file failure (e.g. read-only mount, missing dir). Missing
source dir is a WARN + no-op rather than a fatal — fresh installs
without status/dist/ don't get a stale-bundle problem, just a log line.

Test: scripts/test-start-sh-sync.sh — 7 assertions across 4 cases
(fresh-copy, missing-source, idempotent-re-sync, set-e-survives-permission-
denied). All pass.
2026-07-20 02:07:25 -07:00
Hermes Agent c619d3a36b DC-046 DC-047 pluggable auth providers + email magic link
CI / Test & Lint (push) Has been cancelled
CI / Security audit (push) Has been cancelled
Pluggable AuthProvider framework for any future auth method (OIDC, SAML,
passkeys) to plug in without touching the auth path again. Two
implementations ship:

  * TOTP — refactored from routes/auth/totp.js into src/auth/providers/totp.js
    as one AuthProvider impl. Legacy /api/v1/totp/* routes stay mounted for
    back-compat; new /api/v1/auth/login/totp/* routes use the new shape.

  * EmailMagicLink — src/auth/providers/email.js. Initiate issues a 32-byte
    base64url token, stores its SHA-256 hash in data/email-tokens.json
    (atomic lockfile-based mutation, automatic TTL cleanup), and delivers via
    nodemailer if providers.email.{host,port,username,password} is set OR
    falls back to log.info('auth', 'email magic link issued', ...) for dev.
    Verify accepts the token, marks it used, creates the same DashCaddy
    session cookie that TOTP uses (single global cookie model).

createAuthProviderRegistry() composes both implementations and exposes
them via /api/v1/auth/login/{methods, :provider/initiate, :provider/verify,
recovery-info} and /api/v1/auth/disable/:provider. PUBLIC_ROUTES + CSRF
exemptions updated to use :provider placeholder (parameterized for future
providers).

Test fix: src/utilities/middleware.js PUBLIC_ROUTES and csrf-protection.js
both switched to the :provider form because the prior literal 'totp'
wouldn't match the parameterized mount path Express 4.22 produces.

Test fix: __tests__/public-routes-drift.test.js extractMountPath() was
broken for Express 4.22's new ^/path/?(?=/|$) source format (no \?\?
terminator in the literal-mount case). Rewrote the parser to normalize
escaped slashes + trailing lookaheads instead of relying on regex
matching against the raw source.

New: __tests__/auth-provider-registry.test.js — 9 tests covering registry
composition, getProvider round-trip, listEnabled no-secrets-leak guarantee,
enabled-flag respect, listAll vs listEnabled distinction, email provider
dev-console fallback (token written to JSON store + log.info with
deliveredVia: 'dev-console' + response masked), verify rejects unknown
tokens via AuthenticationError.

Tests: 1241/1241 passing across 46 suites (was 1232; +9 new).

DNS2 deploy: this commit + bump VERSION to 1.16.0 + publish tarball to
get.dashcaddy.net + docker build + bash start.sh. The image-layer migration
from DC-050 also runs on first container recreate post-merge.
2026-07-20 01:40:33 -07:00
Hermes Agent 894e091335 DC-050 harden dataDir + add image-layer migration
CI / Test & Lint (push) Has been cancelled
CI / Security audit (push) Has been cancelled
Three-part fix for the silent data-loss failure mode that survives DC-039:
If SERVICES_FILE env was unset, platformPaths.dataDir resolved to /etc/dashcaddy
(image-layer path), and audit/license/error logs would silently land there and
vanish on every container recreate.

1. platform-paths.assertSafe({mode:'production'}) — throws FATAL on forbidden
   zones (/app/src,routes,scripts,utils,managers,security + /etc/* + /usr + /var).
   Bypassed with SKIP_DATA_DIR_GUARD=1.
2. server.js calls assertSafe() before any runtime work.
3. start.sh one-time migration: scans 6 known image-layer zombie paths,
   copies non-empty content to bind mount with 'migrated-' prefix,
   gated by sentinel file. Survives set -e per-file failures.

19/19 platform-paths tests + 5/5 shell migration tests.
Suite: 1066/1067 (1 pre-existing public-routes-drift failure from in-flight
auth refactor, untouched by this commit).

Verified live on DNS2: live audit log at /app/data/audit-log.json (315KB,
active) is unaffected; vestigial 2-byte /app/src/security/audit-log.json +
140KB /app/src/utils/error.log (pre-DC-039 era) will be recovered on next
container recreate.
2026-07-20 00:53:05 -07:00
hermes 0aa7244cf4 infra: Samihost fail2ban watchdog (auto-unban trusted IPs, drift guard, cap at 200)
CI / Test & Lint (push) Has been cancelled
CI / Security audit (push) Has been cancelled
2026-06-10 11:28:42 -07:00