* docs(updater): PR 2 (Tier 2 manual-click) implementation plan
20-task TDD plan for shipping the manual-click update flow on top of the
Tier 1 (notify) work merged in #7601. Covers UpdateExecutor, RollbackHandler,
SessionDrainer, lock + trustedKeys, four admin endpoints (apply / cancel /
acknowledge / log), admin UI updates, integration tests against a tmp git
repo, and a manual smoke runbook for the spec's "before each tier ships"
gate. Plan deliberately scopes signature verification to an opt-in stub
(updates.requireSignature: false default) to avoid blocking on a separate
release-signing project.
Plan: docs/superpowers/plans/2026-05-08-auto-update-pr2-manual-click.md
Spec: docs/superpowers/specs/2026-04-25-auto-update-design.md
Issue: ether/etherpad#7607
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(updater): extend state + settings for Tier 2 manual-click
Adds ExecutionStatus discriminated union, bootCount, and lastResult to
UpdateState, plus the preApplyGraceMinutes/drainSeconds/diskSpaceMinMB/
requireSignature/trustedKeysPath knobs that Tier 2's executor needs.
loadState backfills the new fields on Tier 1 state files so existing
installs keep working.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(updater): PID-based update.lock with stale-pid reaping
Single-flight guard for Tier 2's UpdateExecutor. Atomic O_CREAT|O_EXCL
acquire; on EEXIST, sends signal 0 to the recorded PID and reaps if dead.
Unparseable / partially-written lock files are treated as stale rather
than fatal so a half-written lock from a SIGKILL'd parent doesn't lock
the install out forever.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(updater): verifyReleaseTag — gpg-via-git stub for Tier 2 preflight
Default updates.requireSignature=false: log a warning and return ok with
reason=signature-not-required. Set true to make preflight refuse a tag
whose signature does not verify under the system keyring (or
trustedKeysPath via GNUPGHOME). Etherpad's release process does not yet
sign tags consistently; turning the check on by default would break
Tier 2 for every admin and forcing a release-signing change is out of
scope for this PR.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(updater): preflight check pipeline for Tier 2
Pure orchestrator over injected probes for install-method, working tree,
disk space, pnpm presence, lock state, remote tag existence and
signature verification. Cheap-and-definitive checks run first; first
failure short-circuits with a typed reason that the route layer will
surface in the preflight-failed admin banner.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(updater): rolling update.log helpers (appendLine + tailLines)
Direct file-append + size-based rotation rather than a log4js appender —
avoids re-configuring log4js on top of the user's existing logconfig.
appendLine creates parents, rotates at 10MB (configurable), keeps 5
backups by default. tailLines reads the last N lines for /admin/update/log.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(updater): SessionDrainer + handshake guard
Drainer schedules T-60 / -30 / -10 broadcasts and resolves at T=0;
isAcceptingConnections() flips off for the duration. PadMessageHandler
consults the flag at the start of CLIENT_READY and disconnects new
joiners with reason "updateInProgress" — existing sockets are
unaffected. Drains shorter than 30s collapse the early timers to fire
ASAP rather than queue past the drain end.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(updater): UpdateExecutor — snapshot, fetch/checkout/install/build, exit 75
Pure-DI orchestrator: spawnFn, copyFile, readSha, saveState, exit are all
injected so unit tests run the full pipeline without spawning real
children or mutating the real install. Streams stdout/stderr to
update.log via the now-best-effort appendLine helper (swallows fs errors
so the executor itself never breaks on read-only / unwritable log dirs).
Failure paths transition to rolling-back and return — the route layer
hands off to RollbackHandler which owns the rollback exit, so we don't
double-exit and lose tail lines.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(updater): RollbackHandler — health-check timer + crash-loop guard
checkPendingVerification arms a 60s timer at boot when state is
pending-verification and increments bootCount; bootCount>2 forces an
immediate rollback (crash-loop guard). markVerified persists the
verified state and stops the timer. performRollback restores the
backup lockfile, runs git checkout <fromSha> and pnpm install, lands on
rolled-back or rollback-failed (terminal) on sub-step failure, exits 75
either way so the supervisor restart brings the new state up.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(updater): wire RollbackHandler into boot + UpdatePolicy honours rollback-failed
- expressCreateServer now invokes checkPendingVerification before polling starts
so a previous boot's pending-verification either re-arms the health-check
timer or, when bootCount has climbed past the crash-loop threshold, forces
an immediate rollback.
- server.ts calls markBootHealthy after state hits RUNNING so /health-being-up
is the implicit happy-path signal that cancels the rollback timer.
- /admin/update/status surfaces execution + lastResult + lockHeld so the admin
UI can render the right Apply / Cancel / Acknowledge state.
- UpdatePolicy gains an `executionStatus` input. While it equals 'rollback-failed',
canAuto / canAutonomous are denied (reason: rollback-failed-terminal); manual
stays on because clicking Apply IS the intervention the terminal state needs.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(updater): apply / cancel / acknowledge / log endpoints
Strict admin-only POSTs that drive Tier 2's manual-click flow:
- POST /admin/update/apply: acquire lock, persist preflight, run preflight,
drain $drainSeconds, executeUpdate (which exits 75 on success), or run
performRollback on a failure path (also exits 75).
- POST /admin/update/cancel: cancel a pre-execute drain/preflight, write
cancelled lastResult, release lock.
- POST /admin/update/acknowledge: clear terminal states (preflight-failed,
rolled-back, rollback-failed) back to idle. lastResult is preserved so
the admin still sees what happened.
- GET /admin/update/log: tail var/log/update.log (200 lines) for the in-
progress UI. Strict admin auth.
Also:
- socketio hook exports getIo() so the apply endpoint can broadcast the
drain shoutMessage outside the regular hook surface.
- ep.json registers updateActions after admin/updateStatus.
- 11 mocha integration tests cover auth, policy denial, execution-busy,
acknowledge-clears-terminal, log content-type.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(updater): admin UI Apply/Cancel/Acknowledge + live log stream
UpdatePage renders the right action set based on execution.status:
Apply when idle/verified and policy allows, Cancel during
preflight/draining, Acknowledge on terminal preflight-failed /
rolled-back / rollback-failed. While the executor is in flight
(preflight/draining/executing/rolling-back) the page polls
/admin/update/log + /admin/update/status once a second and shows the
rolling tail; polling stops automatically when the run terminates.
lastResult and policy denial reasons surface localised copy. Buttons
disable themselves while a network round-trip is in flight to dodge
double-clicks. New i18n keys live under update.page.{apply,cancel,
acknowledge,log,execution,policy.*,last_result.*}, update.execution.*,
update.banner.terminal.rollback-failed, and update.drain.{t60,t30,t10}.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(updater): pad shoutMessage renders update.drain.* via html10n
broadcastShout now sends {messageKey, values, sticky} so the existing
pad-side shout pipeline can route through html10n.get(). The renderer
gains a values pass-through so update.drain.t60 etc. interpolate
{{seconds}}, and gives updater shouts a different gritter title (the
banner.title localised string) so users know it's a system event
rather than a generic admin message.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(updater): rollback uses git checkout -f + integration suite over tmp git repo
RollbackHandler now does git checkout -f <fromSha> BEFORE overlaying the
backup lockfile. Without -f, git refuses checkout when there are
unstaged modifications to files it would overwrite — exactly the case
after a partial executor run that mutated the working tree. With -f the
partial mutation is discarded and the working tree returns to fromSha
cleanly. The backup-lockfile copy is still done (belt-and-braces) but
tolerates ENOENT since checkout already restored the right lockfile.
The new integration suite at src/tests/backend/specs/updater-integration.ts
exercises the full pipeline against a disposable git repo: happy path,
install-fail rollback, build-fail rollback, crash-loop guard, and a
target-sha-doesn't-exist rollback-failed terminal case. 5 mocha tests.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* test(updater): Playwright admin Apply / Cancel / Acknowledge flow
Stubs /admin/update/status (and /admin/update/apply for the apply path)
at the route level so we can assert UI transitions without actually
running an update. Four scenarios:
- Apply button POSTs and re-fetches status (>=2 status fetches total).
- install-method-not-writable hides the button and shows localised
denial copy.
- rollback-failed terminal state shows the Acknowledge button and the
"Manual intervention required" lastResult copy.
- lockHeld=true hides Apply even when policy.canManual is on.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(updater): admin banner shows rollback-failed terminal alert
When execution.status === 'rollback-failed' the banner switches to a
role=alert with the strong update.banner.terminal.rollback-failed copy
and overrides the regular "update available" framing — an admin who
left the system in this state needs to fix it before any other admin
work matters. Other terminal states (preflight-failed, rolled-back) are
informational and surface on the page itself, not the banner.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* docs(updater): Tier 2 admin docs + manual smoke runbook + CHANGELOG
doc/admin/updates.md gains a full Tier 2 section: prerequisites
(git install + process supervisor with sample systemd unit), Apply
flow with timings, every failure mode and the resulting state, the
four endpoints, and the signature-verification opt-in. Settings
table picks up the new updates.* knobs.
docs/superpowers/specs/2026-04-25-auto-update-runbook.md is the
manual smoke runbook the design spec calls for: disposable VM,
systemd unit, every observable transition (happy path, install/
build-fail rollback, crash-loop guard, rollback-failed terminal,
cancel during drain) plus a sign-off checklist for the release cut.
CHANGELOG Unreleased section explains the supervisor requirement
and points readers at the runbook.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* docs(updater): note docker-friendly update flows as follow-up work
Tier 2 refuses Apply on installMethod=docker because in-container
mutation doesn't survive a container restart. Adds a future-work note
covering the two reasonable paths for an in-product docker Apply
button (instructions-only vs deploy-webhook) and explicitly rules out
mounting /var/run/docker.sock as a footgun. Watchtower gets a pointer
for admins who want fully autonomous docker updates today.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(updater): address Qodo review (1-6) + Playwright strict-mode CI fix
1. Tier 2 endpoints now gate on tier in {manual, auto, autonomous} —
notify and off return 404 to match the prior PR-1 behaviour. Gate is
evaluated per-request via app.use middleware so a settings.json reload
takes effect without a full restart, and so integration tests can flip
the tier dynamically. Adds a regression test that exercises 404 at
tier=notify across all four endpoints.
2. cancel/apply race fixed: /admin/update/cancel no longer releases the
lock — apply's finally block owns it for the request's lifetime. Apply
now reloads state after preflight and aborts with 409 cancelled-during-
preflight if execution.status is no longer 'preflight' for the same
targetTag. Prevents a second apply from sneaking in while the first is
still running its slow checks, and prevents the post-cancel apply from
continuing into drain/execute.
3. SessionDrainer now restores acceptingConnections=true at drain
completion (not just on cancel). The lock + persisted execution.status
prevent a fresh apply from racing in — the in-memory flag was redundant
safety that turned into a wedge if the executor threw post-drain. Adds
a unit test asserting the flag is restored after natural drain end.
4. PadMessageHandler drain guard switched from socket.json.send (a
socket.io v2/v3 API that may not exist on v4) to socket.emit('message',
...) for consistency with the other disconnect paths in the file.
5. Spawn 'error' handlers added to runStep helpers in UpdateExecutor and
RollbackHandler, plus the gpg verify-tag spawn in trustedKeys. Without
them, a missing/unexecutable binary leaves the promise hanging forever
and the update flow stuck in-flight. SpawnFn type extended to allow
on('error', ...) listeners cleanly. Spawn errors now resolve with code
1 + the error message in stderr, so the existing failure-detection
branches fire normally.
6. executeUpdate body wrapped in try/catch. An exception from readSha,
saveState, copyFile, or any step now lands in a rolling-back persist +
returns failed-checkout, so the route's post-executor rollback path
picks it up. State can no longer wedge at 'executing'. The catch's
inner saveState is itself try/wrapped so a write-after-write failure
doesn't crash the route either.
CI: Playwright update-page-actions strict-mode violation fixed. Both the
banner and the lastResult <p> contain "Manual intervention required";
selector now scopes to p.last-result-rollback-failed for the lastResult
assertion specifically.
129 vitest unit tests + 23 mocha integration tests passing; ts-check clean.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(updater): address Qodo #7 (status leak) + #8 (short-drain values)
#7. /admin/update/status now redacts diagnostic strings for unauth callers
even when requireAdminForStatus is left at its default (false). Status
enum + outcome enum are kept (the admin banner / pad-side badge need them
to render the right UI) but execution.reason / execution.fromSha /
execution.targetTag and the same fields on lastResult are stripped.
Authed admin sessions still get the full payload — they're looking at
their own server's diagnostics. Two new mocha tests cover both paths:
"redacts execution.reason / lastResult.reason for unauth callers" and
"returns full diagnostic payload to authed admin sessions".
#8. SessionDrainer no longer schedules T-30 / T-10 broadcasts when the
configured drainSeconds can't honour them. Previously, with drainSeconds
< 30 the T-30 timer fired at zero remaining but the broadcast still
claimed "30 seconds" — misleading. Now T-30 only schedules when
drainSeconds > 30 and T-10 only when > 10. Admins picking a short drain
get fewer announcements but each carries an accurate countdown. The
opening announcement now reports the configured drain length rather
than a hardcoded 60. Two updated unit tests: drainSeconds=15 (skips
T-30, still fires T-10) and drainSeconds=5 (skips both).
131 vitest unit + 26 mocha integration tests passing; ts-check clean.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(updater): address Qodo follow-up — tag injection, rollback rejections, state validation
Qodo posted three new concerns after the first fix push.
1. Git tag option injection (security). The release tag from GitHub's
tag_name flowed into `git checkout` / `git verify-tag` as a positional
arg. A tag starting with '-' would be parsed as an option and could
bypass signature verification or change checkout semantics. Mitigated
in three layers:
- New refSafety helper (isValidTag / assertValidTag / refsTagsForm)
enforces a strict subset of git's check-ref-format spec: rejects
leading '-' or '.', whitespace, control chars, and ~ ^ : ? * [ \\
and the '..' sequence.
- VersionChecker validates tag_name before persisting to state, so a
malformed value from a misconfigured githubRepo never lands on disk.
- UpdateExecutor calls assertValidTag and uses the refs/tags/<tag>
form for git checkout. trustedKeys also validates and adds '--' to
git verify-tag for an end-of-options marker. updateActions does an
up-front isValidTag check on state.latest.tag so a corrupt state
file gets a clean 409 instead of a 500.
2. Unhandled rollback rejections. checkPendingVerification was firing
`void deps.saveState(...)` and `void performRollback(...)` without
.catch(), so an fs error during boot's rollback path would bubble out
as an unhandled rejection. Both callsites now go through fireSaveState
/ fireRollback helpers that catch and log; rollback rejections fall
through to a best-effort terminal-state write + exit 75 so the
supervisor can re-try the next boot with bootCount++.
3. Execution state under-validated. isValidExecution previously checked
only that `status` was a known enum value, so a hand-edited state file
with `{execution: {status: 'pending-verification'}}` (missing fromSha
/ targetTag / deadlineAt) would pass validation and reach
RollbackHandler with undefined refs. The validator now consults a
per-status required-fields map mirroring the ExecutionStatus union in
types.ts and rejects empty strings as well as missing fields. Same
tightening applied to lastResult.outcome (must be in the allowed enum,
not just any string). Six new unit tests cover hand-edited corruption.
145 vitest + 26 mocha tests green; ts-check clean.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* chore(docker): clear most CVEs in published image — npm/pnpm/uuid + drop curl
Cuts published-image vulnerabilities from 18 (4H/13M/1L) across 8 packages
to 12 (2H/9M/1L) across 3 packages. The remaining three (curl/libcurl,
git, busybox) are all upstream Alpine 3.23 packages with "not fixed"
status — libcurl is pulled in transitively by git and cannot be
removed independently.
Changes:
- Provision pnpm via corepack instead of `npm install -g pnpm`, then
remove the bundled npm. The base image's npm@10.9.7 ships old
transitives (picomatch 4.0.3 → CVE-2026-33671/33672, brace-expansion
2.0.2 → CVE-2026-33750) that we don't otherwise need at runtime;
corepack handles pnpm directly without npm. Fixes 1H + 1M.
- Bump PnpmVersion 10.28.2 → 10.33.2 to align with the rest of the
workflow and pull in pnpm's patched bundled brace-expansion (5.0.5
vs 5.0.4). Fixes 1M.
- Add `uuid@<14.0.0` → `>=14.0.0` to pnpm.overrides
(GHSA-w5hq-g745-h8pq). Fixes 1M.
- Drop `curl` from the runtime apk add list and switch HEALTHCHECK to
wget (busybox built-in). curl was only invoked by the healthcheck and
by dev/CI scripts that don't run in the container. Removes the curl
CLI binary; libcurl remains as a git transitive dep, so the
`apk/alpine/curl` advisories scout reports against libcurl persist
but aren't reachable from any code we ship. As a side-effect this
also clears nghttp2 (CVE-2026-27135) which was a curl-CLI dep.
- Switch HEALTHCHECK URL from `localhost` to `127.0.0.1` — alpine/musl
resolves localhost to ::1 first and Etherpad only binds IPv4.
Verified locally: docker build → docker run → healthy → docker scout
cves shows 12 CVEs / 3 packages.
* fix(docker): refresh corepack before preparing pnpm (Qodo)
Node 22's bundled corepack ships a stale signing-key list and can reject
newer pnpm releases (nodejs/corepack#612), which would fail the image
build at `corepack prepare`. Mirror the snap/snapcraft.yaml workaround:
`npm install -g corepack@latest` before activating pnpm, in both
adminbuild and build stages. npm is still removed afterwards.
* docs(changelog): note docker image dropping curl/npm/npx (Qodo)
Address Qodo's "backwards-incompatible change without mitigation" rule
violations by documenting the removal in the 2.7.3 breaking-changes
section. Operators who exec into the container can apk add curl on
demand or use the busybox wget / pnpm already present.
* chore: pnpm
* chore: pnpm
* chore: pnpm
* chore: pnpm
* chore: pnpm
* chore: pnpm
* chore: pnpm
* chore: pnpm
* chore: pnpm
---------
Co-authored-by: SamTV12345 <40429738+samtv12345@users.noreply.github.com>
* ci(docs): build on PRs and pin Node 22 (Qodo follow-up to #7640)
Qodo flagged two reliability gaps on the oxc-minify fix that landed in
#7640:
1. The Deploy Docs to GitHub Pages workflow only ran on push to
develop, so a PR that broke `pnpm run docs:build` was not caught
until after merge — exactly how the dead-link regression in #7546
escaped. Add a pull_request trigger that runs the same build but
skips the deploy/upload steps via `if: github.event_name ==
'push'`. Also include the workflow file itself in the path filter
so changes to it are exercised on PR.
2. oxc-minify@0.128.0 requires Node ^20.19.0 || >=22.12.0, but the
workflow did not pin Node and the repo declared engines.node
>=22.0.0 with engineStrict: true — a runner image (or local dev)
on Node 22.0–22.11 would refuse to install. Pin Node 22 in the
docs workflow with actions/setup-node@v6 (matching the rest of
CI), and bump engines.node to >=22.12.0 so the project's
engineStrict gate matches the actual minimum.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* ci(docs): split build and deploy so PR runs do not hit pages env protection
The previous attempt put `if: github.event_name == 'push'` on individual
deploy steps but kept the single job's `environment: github-pages`
binding. Environment protection rules reject any non-develop ref
(including `refs/pull/N/merge`), so the runner failed the entire job
at creation time before any step could execute:
Branch "refs/pull/7645/merge" is not allowed to deploy to
github-pages due to environment protection rules.
Split into two jobs: `build` runs on every trigger (PR + push) and
uploads the artifact only on push, `deploy` depends on `build`,
runs only on push, and is the only job bound to the github-pages
environment. Standard GHA pages-deploy pattern; PR builds never
attempt to enter the protected environment.
* docs: align Node minimum references with bumped engines.node (Qodo round 2 on #7645)
Qodo flagged that engines.node moved from >=22.0.0 to >=22.12.0 in
this PR but documentation still claimed the old requirement. Sync the
three places that pinned a specific minimum:
- README.md installation requirements (>= 22 → >= 22.12)
- doc/npm-trusted-publishing.md publish prerequisites
(>=22.0.0 → >=22.12.0, with oxc-minify cited as the driver)
- CHANGELOG.md 2.7.3 breaking-changes entry (22 → 22.12, with the
same oxc-minify justification)
---------
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* docs(updater): add four-tier auto-update design spec
Four-tier opt-in self-update subsystem (off / notify / manual / auto / autonomous).
GitHub Releases as source of truth; install-method auto-detection with admin
override; in-process execution with supervisor restart; 60s drain + announce;
auto-rollback on health-check failure with crash-loop guard. Pad-side severe/
vulnerable badge that does not leak the running version. Top-level adminEmail
with escalating cadence (weekly while vulnerable, monthly while severe).
Refs: docs/superpowers/specs/2026-04-25-auto-update-design.md
* docs(updater): add PR 1 (Tier 1 notify) implementation plan
Bite-sized TDD task breakdown for shipping Tier 1 notify only:
- VersionChecker, InstallMethodDetector, UpdatePolicy, Notifier, state modules
- /admin/update/status (admin-auth) and /api/version-status (public, no version leak)
- Admin UI banner + read-only update page + nav link
- Pad-side severe/vulnerable footer badge
- Settings: updates.* block + top-level adminEmail
- Tests: vitest unit + mocha integration + Playwright admin/pad
- CHANGELOG + doc/admin/updates.md
PRs 2-4 (manual/auto/autonomous) get their own plans after PR 1 lands.
* feat(updater): add shared types for auto-update subsystem
* feat(updater): clarify OutdatedLevel and EMPTY_STATE doc, drop path header
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* feat(updater): add semver helpers and vulnerable-below parser
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix(updater): tighten semver regex to reject four-part versions
* feat(updater): add state persistence with schema validation
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix(updater): reject null email and array latest in state validation
typeof null === 'object' meant {email:null} passed the old isValid check,
which would crash downstream Notifier code reading email.severeAt. Likewise,
an array would pass the typeof latest === 'object' branch. Introduce
isPlainObject helper (null-safe, Array.isArray guard) and use it for both
fields. Adds two regression tests covering the exact broken inputs.
* feat(updater): add install-method detector with override
* feat(updater): add policy evaluator
* feat(updater): add GitHub Releases checker with ETag support
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix(updater): validate release fields and preserve ETag on prerelease
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* feat(updater): add email cadence decider
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix(updater): tagChanged email fires regardless of cadence; drop unused field
* feat(settings): add updates.* and adminEmail settings
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* feat(updater): wire boot hook and periodic checker
Register expressCreateServer/shutdown hooks in ep.json and implement
the boot-wiring module that detects install method, starts the polling
interval and runs the notifier dedupe pass each tick.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* feat(updater): add /admin/update/status and /api/version-status endpoints
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* i18n(updater): add english strings for update banner, page, and pad badge
* feat(updater): add pad footer badge for severe/vulnerable status
* feat(admin-ui): add update banner, page, and nav link
Add UpdateStatusPayload to the zustand store, a persistent UpdateBanner
rendered in the App layout, a /update page showing version details and
changelog, and a Bell nav link — all wired to the /admin/update/status
endpoint added in Task 10.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* test(updater): add Playwright specs for admin banner/page and pad badge
* docs(updater): document tier 1 settings, badge, email cadence
* refactor(updater): dedupe helpers, fix misleading log, add banner styling
- Export stateFilePath from index.ts and import it in updateStatus.ts (removes local duplicate)
- Import getEpVersion from Settings.ts in both index.ts and updateStatus.ts (removes two local definitions)
- Fix misleading 'backing off' log message — no backoff is implemented, just retries at next interval
- Remove EMPTY_STATE_FOR_TESTS re-export from state.ts; state.test.ts now imports EMPTY_STATE directly from types.ts
- Add .update-banner and .update-page CSS rules to admin/src/index.css
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix(updater): address review feedback — async wrap, tier=off skip, poll race, opt-in admin gate
- Wrap /api/version-status and /admin/update/status with a small async helper
so a rejected promise becomes next(err) instead of an unhandled rejection.
- Short-circuit route registration when updates.tier === 'off' so the heavier
opt-out also removes the HTTP surface (matches pre-PR behavior for that case).
- Add an in-flight guard around performCheck() so overlapping interval ticks
can't race on update-state.json writes or duplicate email decisions; track
the initial setTimeout handle and clear it in shutdown().
- Add updates.requireAdminForStatus (default false) so admins can lock
/admin/update/status to authenticated admin sessions without disabling the
updater. Default false preserves current behavior (the running version is
already exposed publicly via /health). Backend specs cover unauth → 401,
non-admin → 403, admin → 200.
- Bump admin troubleshooting menu count test 5 → 6 to account for the new
Update nav link.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(updater): address Qodo round-2 review feedback
Round 2 of Qodo review on #7601. Addressing the action-required items:
#1 Badge bypassed pad baseURL — derive basePath the same way
padBootstrap.js does (`new URL('..', window.location.href).pathname`)
and prefix the fetch with it. Subpath deployments now reach
/<prefix>/api/version-status instead of 404ing.
#2 Updater poller could get stuck — `getCurrentState()` is now inside
the try/finally so a one-time loadState() rejection can't leave
`checkInFlight=true` and permanently silence polling.
#3 Updates off hung admin page — UpdatePage now self-fetches and
renders explicit `disabled` (404), `unauthorized` (401/403), and
`error` states instead of staying on "Loading...". Banner-driven
prefetch is still honoured if it landed first.
#11 NaN polling interval — coerce `checkIntervalHours` to a number,
clamp to [1h, 168h], log a warning and fall back to 6h on
non-finite input. Math.max(1, NaN) === NaN previously meant a
malformed settings.json could turn the poller into a tight loop.
#13 State validation accepted broken subfields — `isValid()` now
inspects `latest.{version,tag,body,publishedAt,htmlUrl,prerelease}`,
`vulnerableBelow[].{announcedBy,threshold}`, and
`email.{severeAt,vulnerableAt,vulnerableNewReleaseTag}`. A
hand-edited file with a number where a string is expected is now
treated as corrupt and reset to EMPTY_STATE rather than crashing
later in semver parsing or email rendering.
#14 Badge cache stampede — wrap `computeOutdated()` in a single-flight
promise so concurrent requests at cache expiry await one shared
computation instead of fanning out into N redundant disk reads.
Plus six new state.test.ts cases covering each new validation guard.
Pushing back on the remaining items:
#4 `updates.tier` defaults to `notify` — intentional. The whole point
of tier 1 is to surface the "you are behind" signal to admins by
default. Opt-in defeats the purpose; the existing failure mode
(admin never hears about a security-relevant release) is exactly
what this PR is fixing.
#5/#8 Admin status endpoint admin-auth — `currentVersion` is already
public via `/health`, so wrapping the route in admin-auth doesn't
reduce the disclosure surface meaningfully. Operators who want it
gated set `updates.requireAdminForStatus=true` (already wired and
covered by the comment on the route handler).
#10 Plain `https://` URLs in planning doc — planning markdown is
viewed in editors and on GitHub where protocol-relative URLs would
either render literally or break entirely. Keeping `https://`.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
* SecretRotator: New class to coordinate key rotation
* express-session: Enable key rotation
* Added new entry in docker.adoc
* Move to own package.Removed fallback as Node 16 is now lowest node version.
* Updated package-lock.json
---------
Co-authored-by: SamTV12345 <40429738+samtv12345@users.noreply.github.com>