Commit Graph
17 Commits
Author SHA1 Message Date
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 cbe0c912fc DC-040: repurpose post-deploy-patches.sh as a verifier (fail-loud, not patch-and-continue)
CI / Test & Lint (push) Has been cancelled
CI / Security audit (push) Has been cancelled
Empirically measured against all 4 release versions + origin/main: every
patch in the old script is a no-op against every current release. v1.14.4
(the version that originally needed patches) doesn't even ship src/ in the
tarball — the old script silently no-op'd on it because it couldn't find
files to patch, then the build crashed with MODULE_NOT_FOUND in production.

Repurposed as a verifier: 5 hard checks (server.js requires, license-manager
path, src/ tree presence, license-keygen.js at root, generic src/ require
path scan) + informational warnings. Exits 1 on ANY failure with a clear
'Build should be ABORTED' message naming the v1.14.4-class bug if relevant.
Old behaviour was 'patch and continue' (silently hid regressions); new
behaviour is 'fail loud' (every regression now produces a build abort).

Files changed:
- scripts/dashcaddy-post-deploy-patches.sh — rewritten as verifier (222→274
  lines, header explains the empirical evidence + behaviour change)
- dashcaddy-api/scripts/test-dashcaddy-post-deploy-verifier.sh — new
  regression test, 17 assertions across 10 scenarios (clean tree, missing
  files, broken requires, empty src/, missing app.js, absolute path, etc.)

Empirical measurements documented:
- origin/main: 5/5 checks pass
- v1.14.9 (latest): 5/5 checks pass (0 patches applied under old script)
- v1.14.8: 5/5 checks pass (0 patches applied under old script)
- v1.14.4: 2/5 checks FAIL under new verifier (src/ missing, license-manager
  in wrong location) — old script silently no-op'd on the same input

Tests: 1214/1214 Jest + 31 shell assertions. Lint: 150 warnings, all
pre-existing in untouched files (zero new warnings introduced).
2026-07-13 15:09:08 -07:00
Hermes 1cc112f1e4 DC-038: backup trigger.json + result.json in dashcaddy-update.sh
The host-side updater only backed up code + data/, leaving trigger.json and
result.json unarchived. After a failed update, operators had to reconstruct
'what was being attempted' by joining timestamps across files. Now the
backup captures both files into a 'update-state/' subdir alongside code +
data backups, keyed by from-version.

- New `backup_update_state()` function in dashcaddy-update.sh: idempotent,
  tolerates absent files (cleans up empty subdir), tolerates chattr +i
  (unlock/copy/relock).
- Wired into main() right after `backup_data_dir`, before `cleanup_old_backups`.
- Deliberately does NOT auto-restore trigger.json on rollback — the rollback
  handler reads a fresh trigger.json written by the operator/container;
  restoring the previous attempt's trigger would clobber the active rollback
  request. Backups are read-only forensic evidence.
- New `dashcaddy-api/scripts/test-dashcaddy-update-backup.sh` (14 assertions,
  5 test groups): both-files-present, partial-present, no-files-present,
  idempotency, main() flow ordering. All 14 pass.
- Synced the duplicate at `dashcaddy-api/scripts/dashcaddy-update.sh`
  (md5-identical to scripts/dashcaddy-update.sh).

Tests: 1214/1214 pass (zero change). Lint: 150 warnings, all pre-existing
in untouched files (zero new warnings introduced).
2026-07-13 13:27:38 -07:00
Krystie bfa4ba570e DC-025: harden updater — channel gate + safe locked-file replacement
CI / Test & Lint (push) Has been cancelled
CI / Security audit (push) Has been cancelled
The host-side updater has been silently broken in two ways:

1. Empty staging directories would cause rm -rf of live routes/src with no
   replacement, leaving the host tree gutted while the container kept serving
   from its own image. Now deploy_tree() refuses to delete unless the staging
   source has actual files.

2. chattr +i on critical files (used to protect security-hotfixed routes from
   being clobbered by upstream tarballs) caused rm -rf to partially execute
   then fail under set -e, leaving the host in a half-deleted state. Now
   deploy_tree() scans for immutable files, unlocks them before replace,
   and re-locks them after — so security-locked files survive every update.

Also adds:
- Channel gate: trigger.json channel=prerelease/beta/rc/alpha is rejected
  unless ALLOW_PRERELEASE=true is set in /opt/dashcaddy/updates/channel.conf.
  Default is 'stable only', safe for production. Staging hosts opt in.
- channel.conf.example documenting the new opt-in mechanism.

Verified end-to-end: manual trigger.json → path unit fired → routes (53 files)
+ src (62 files) deployed → container rebuilt → health check passed. totp.js
remained locked with security edits intact.
2026-07-01 04:02:30 -07:00
Krystie a5f51e4a0c DC-023: operational fixes — DNS, rate limiter, version sync
CI / Test & Lint (push) Has been cancelled
CI / Security audit (push) Has been cancelled
- VERSION: bump from 1.14.4 to 1.14.6 to match package.json (HEAD had stale value)
- middleware.js: apply existing totpLimiter (10/15min) to /totp/setup endpoint
  (was previously unmetered, allowing secret enumeration)
- dashcaddy-update.sh: hook post-deploy-patches.sh into the update flow
  so the container can survive transitions between broken → fixed tarballs
- start.sh: add --add-host flags for get.dashcaddy.net and get2.dashcaddy.net
  so the container can resolve the release server (was failing with ENOTFOUND)
2026-07-01 03:10:53 -07:00
Krystie e73bfbb0a1 DC-021: build pipeline now ships src/ + hygiene for generated artifacts
The release tarball previously omitted dashcaddy-api/src/, which meant the
in-container self-updater had to apply post-deploy patches (dashcaddy-post-
deploy-patches.sh) to work around missing files. That script generates 37
flat copies of src/ files at the dashcaddy-api/ root level to satisfy
broken require() paths. With proper src/ shipping, those files become
obsolete, but they were still being shown as untracked in git.

Changes:
- BUILD-PIPELINE-FIX.md documents the build pipeline fix (in /opt/dashcaddy-release/
  build-release.sh — sibling repo, not tracked here)
- .gitignore now ignores the 37 generated post-deploy artifacts plus the
  backups/ and updates/ runtime directories, so 'git status' stays clean
- scripts/dashcaddy-post-deploy-patches.sh is now tracked so it's preserved
  across rebuilds (still useful as a safety net for transitional installs)
2026-07-01 03:09:59 -07:00
Hermes 4c60ed1ccf DC-002: Sync root VERSION with package.json + keep them in sync via release.sh
CI / Test & Lint (push) Has been cancelled
CI / Security audit (push) Has been cancelled
- Updated root VERSION file from 1.13.0 → 1.13.4 to match package.json.
- scripts/release.sh now writes both files on every release bump, and
  stages VERSION alongside package.json in the release commit.
- This prevents the drift that caused the stale VERSION in the first place.
2026-06-13 11:13:54 -07:00
Hermes 7557a6364a ops: add host-side update script to repo, include dns-providers/ in backup/deploy/restore paths
CI / Test & Lint (push) Has been cancelled
CI / Security audit (push) Has been cancelled
2026-06-10 15:18:48 -07:00
Hermes 954be9e868 feat: auto-restart policies, SSL monitoring, DNS propagation, dependency tracking, config drift detection
CI / Test & Lint (push) Has been cancelled
CI / Security audit (push) Has been cancelled
2026-06-10 14:43:46 -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
Sami fd2e906963 fix(release): stage build-rewritten files (sw.js, index.html) in release.sh
build.js rewrites three things during `node build.js`:
  - status/dist/*.js (bundle output)
  - status/index.html (CSP hash for inline bootstrap)
  - status/sw.js     (cache name derived from bundle content)

release.sh was only staging status/dist/. Result: when a release didn't
touch index.html or sw.js source, the post-build modifications to those
two files were left unstaged, the commit included only dist/, and the
tarball shipped the stale sw.js. Clients then kept the previous SW
cache name -> activate handler never wiped the cache -> precached old
bundles served forever even after they were "updated" on disk.

Now stage index.html and sw.js too. They're tracked (not gitignored)
so a plain `git add` is enough; the commit is a no-op when nothing
actually changed.
2026-05-17 02:44:35 -07:00
Sami 7e23cb5b06 fix(updates): allow notify endpoint past CSRF + portable JSON parse in release.sh
- csrf-protection: skip CSRF validation on /api/system/update-notify. The
  endpoint has its own X-DashCaddy-Notify-Secret auth and is only ever called
  machine-to-machine; browsers never reach it. Without this, the CSRF cookie
  check rejects the notify POST before the secret comparison runs.
- release.sh: the verify step piped curl into `node -p ".../dev/stdin"` which
  works on Linux but blows up on Windows/git-bash. Replaced with portable
  grep+sed extraction so the same script works on both publisher OSes.
2026-05-16 23:55:08 -07:00
Sami c66fe498b6 feat(updates): seamless release flow — push-notify, VERSION copy, robust mirror
- self-updater: per-instance notify secret (auto-generated), notifyAndApply()
  triggers an immediate check+apply for the publishing host
- routes: POST /api/system/update-notify (X-DashCaddy-Notify-Secret gated,
  added to public-routes allowlist so TOTP doesn't block machine-to-machine)
- dashcaddy-update.sh: include VERSION in backup/deploy/rollback copy lists;
  belt-and-suspenders write trigger.json commit to VERSION post-deploy.
  Fixes drift where /app/VERSION stayed at the old commit after self-update.
- release.sh: mirror failures are non-fatal+loud; HTTP-verify get2 after
  rsync; auto-notify co-located instance via /opt/dashcaddy/updates/notify-secret
  (or honour DASHCADDY_NOTIFY_TARGETS for multi-instance setups).
2026-05-16 23:46:53 -07:00
Krystie 22c48076c0 Merge remote changes - resolve conflicts: keep remote package.json v1.4.0, keep local self-updater.js logic, features.js regenerated by build 2026-05-14 21:08:20 -07:00
Krystie 947007438f Update: version 1.2.0, new version UI features, CSP hash auto-update, bug fixes 2026-05-14 21:04:35 -07:00
SamiandClaude Opus 4.7 f65af5d7fd fix(updater): stop false-positive "update available" loop when commit is unknown
Dockerfile never received DASHCADDY_COMMIT at build, so /app/VERSION held
'unknown'. _isNewer then treated same-version-different-commit as newer,
making the auto-updater rebuild the container indefinitely (each rebuild
still produced commit='unknown').

- self-updater._isNewer: normalize commits; treat unknown/null/empty as no
  commit info and fall back to pure version comparison
- self-updater._autoCheckAndApply + routes/updates: refuse to apply when
  local version >= remote version (belt-and-suspenders)
- update-management.js: hide '(unknown)' from version label
- Dockerfile: COPY VERSION instead of writing from build arg
- VERSION: committed placeholder ('dev'); scripts/release.sh now writes
  the real short SHA into the tarball's VERSION before tar-ing, so every
  published release ships with an accurate commit

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-12 18:20:59 -07:00
SamiandClaude Opus 4.7 54196a2d4f chore: add scripts/release.sh for cutting + publishing releases
Automates what was previously a six-step manual process that, twice
in this codebase's history, has produced version skew between git and
the released tarball (v1.2.0 was published with package.json 1.2.0 in
the tarball but the bump was never committed back to gitea — making
"what code is in v1.2.0?" answerable only by extracting the tarball).

The script:
- Refuses to run with a dirty tree, off main, or already at the
  target version.
- Bumps dashcaddy-api/package.json, rebuilds status/dist/, commits
  + pushes to gitea — so the released artifact and gitea HEAD are
  always in lockstep.
- Clones gitea HEAD on the release host, verifies the cloned commit
  matches what we just pushed (catches a stale clone or a missed
  push), tars it, computes sha256, writes version.json.
- Refreshes install.sh on the release host alongside the tarball
  (fresh installs use the install.sh from the latest release).
- Mirrors the release dir to the get2 backup via rsync.
- Verifies live by curling version.json and re-hashing the served
  tarball.

Hosts overridable via DASHCADDY_RELEASE_HOST / DASHCADDY_MIRROR_HOST
/ DASHCADDY_GITEA_URL env vars.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-06 20:28:45 -07:00