mirror of
https://github.com/ether/etherpad-lite.git
synced 2026-07-17 16:47:05 +00:00
176 commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
f79cabe74c
|
Env-var overrides for offline/air-gapped installs (update check, plugin catalog, updater) (#7917)
* feat(settings): env-var overrides for update-check/plugin-catalog/updater (offline installs)
Air-gapped and firewalled deployments could not disable Etherpad's outbound
calls (the hourly version check, the admin plugin catalogue, and the
self-updater) without editing settings.json inside the container image — the
shipped settings.json.docker hardcoded updates.tier and omitted the privacy
block entirely, so there was no env-var to flip.
Wire the relevant keys through the existing ${ENV:default} substitution in both
settings.json.docker and settings.json.template:
- PRIVACY_UPDATE_CHECK (privacy.updateCheck, default true)
- PRIVACY_PLUGIN_CATALOG (privacy.pluginCatalog, default true)
- UPDATES_TIER (updates.tier, default notify; "off" = no calls)
- UPDATES_SOURCE / UPDATES_CHANNEL / UPDATES_CHECK_INTERVAL_HOURS /
UPDATES_GITHUB_REPO / UPDATES_REQUIRE_ADMIN_FOR_STATUS (docker)
- UPDATE_SERVER (updateServer endpoint)
Document the full set in doc/docker.md (new "Updates & privacy" section) and
cross-link from doc/admin/updates.md. Add backend regression tests that parse
the shipped settings.json.docker and settings.json.template and assert the
overrides apply with correct boolean/numeric coercion, so a future edit that
drops the ${ENV} placeholders fails loudly.
Addresses #7911 (item 2). No code changes — config + docs + tests only.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* docs: address Qodo review — point to PRIVACY.md, clarify tier=off scope
- Outbound-call docs/comments referenced doc/privacy.md (the storage/logging
doc); the canonical outbound-call inventory is repo-root PRIVACY.md, which the
runtime messages in UpdateCheck.ts / Settings.ts also reference. Re-point
settings.json.{template,docker} and doc/docker.md there.
- doc/admin/updates.md said updates.tier="off" means "no HTTP request will leave
the instance", but the legacy UpdateCheck.ts call to ${updateServer}/info.json
is gated by privacy.updateCheck, not updates.tier. Clarify that air-gapped
installs must set PRIVACY_UPDATE_CHECK=false too.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* docs: link PRIVACY.md via absolute URL to satisfy VitePress dead-link check
PRIVACY.md lives at the repo root, outside the doc/ tree VitePress builds, so a
relative link to it (../PRIVACY.md / ../../PRIVACY.md) is flagged as a dead link
and fails `docs:build`. Use the absolute GitHub URL instead, matching how
doc/configuration.md already links settings.json.template.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
||
|
|
79e3d46127
|
feat(settings): default settings.enablePluginPadOptions to true (#7841)
This flag gates the ep_* passthrough on padoptions that shipped in 3.0.0 (PR #7698). It was introduced as opt-in, but the intent in shipping it was to let plugins like ep_plugin_helpers' padToggle / padSelect ride the existing broadcast/persist rail out of the box — flipping the default closes the gap. Why now - ep_comments_page#422 (and sibling per-plugin reports discussed on Discord): stock 3.x deployments console.warn on every pad load because the helper detects clientVars.enablePluginPadOptions === false and tells the admin to flip it. With the flag default-true, the warning stops firing on fresh installs while still surfacing for operators who have explicitly opted out. - Plugins that already depend on ep_plugin_helpers >= 0.6 expect the pad-wide path to work; the default-false gate silently no-op'd pad.changePadOption('ep_*', …) and made the helper UI inert. Scope - Settings.ts default flipped to true; comment rewritten to describe the new "operator opt-out" model rather than the old AGENTS.MD §52 opt-in framing (that policy still applies to *new* features; this one has shipped and proven safe). - settings.json.template env-var substitution default flipped to true so docker / supervisor configs without an explicit value get the new behavior. - doc/plugins.md updated to match (default true, opt-out via settings.json) and the PluginCapabilities source comment. - Backend test describe-blocks relabeled — "true" is now "(default)", "false" is now "(operator opt-out)". Both branches still cover the same matrix so the size-cap / namespace-validation paths stay exercised. Compat - Existing deployments with an explicit `"enablePluginPadOptions": false` in settings.json keep that value — no migration needed. - Older clients only read clientVars.enablePluginPadOptions; the protocol shape is unchanged. Closes ep_comments_page#422 (helper warning suppression for stock deployments). Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
d0180033e4
|
fix: page sessionstorage cleanup to avoid OOM (#7830) (#7831)
* fix: page sessionstorage cleanup to avoid OOM (#7830) SessionStore._cleanup() previously called `findKeys('sessionstorage:*', null)`, materialising every session key into a single array. On decade- old MariaDB installs with millions of sessions this OOMs the node process within ~15 minutes — see #7830. Switch to ueberdb2 6.1.0's findKeysPaged with a 500-key page size, and yield to the event loop between pages so the DB driver can release each page's buffered rows and request handlers can interleave. The break is now driven by `page.length === 0` rather than `page.length < CLEANUP_PAGE_SIZE` so a stubbed/throttled paged source still iterates the full keyspace. Adds a regression test that seeds 50 sessionstorage rows, monkey-patches `DB.findKeysPaged` to use a 4-key page, runs cleanup, and asserts every expired row is removed plus every valid row preserved across page boundaries. Closes #7830 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix: address Qodo review on #7831 Four follow-ups raised by Qodo on the session cleanup paging fix: - DB.ts: fail-fast at init() if any required wrapper method (incl. findKeysPaged) is missing, so a stale ueberdb2 pin surfaces at boot rather than crashing the first cleanup run an hour later. - SessionStore: bound a single _cleanup() run to 10 minutes. Under sustained session creation the keyspace can grow faster than cleanup drains it; without a budget the next scheduled run would never fire. When the budget hits, log a warning and let the next run continue. - SessionStore: log the defensive `page[0] <= after` cursor-stall break. Previously the loop exited silently, leaving expired rows behind with no operator-visible signal of the backend regression. - Tests: the paged-cleanup regression test now removes both expiredSids AND validSids in finally, so a failed assertion doesn't leak rows. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * docs: note paged session cleanup in CHANGELOG + settings template CHANGELOG.md picks up an entry under 3.1.0 Notable fixes describing the OOM cause, the paged iteration, the 10-minute per-run budget, the cursor-stall logging, and the fail-fast init guard. settings.json.template's sessionCleanup comment adds the page-size, budget, and pointer to #7830 so admins can reason about the new behaviour from the template alone. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * chore: regenerate lockfile against ueberdb2 6.1.2 Now that ether/ueberDB#983 unblocked the publish workflow (OIDC trusted publishing), ueberdb2 6.1.2 is live on npm and the `^6.1.0` pin in src/package.json resolves cleanly. Resolves the ERR_PNPM_OUTDATED_LOCKFILE that was blocking CI on this PR. 29 SessionStore backend tests still green against the published tarball. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
86edd67f58
|
feat: support X-Forwarded-Prefix and X-Ingress-Path (#7802) (#7806)
* docs: design for URL base-path support (#7802) Spec covers the architecture, header handling rules, components touched, backwards-compatibility story, risks, and test plan for honoring X-Forwarded-Prefix / X-Ingress-Path under trustProxy. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * docs: amend #7802 spec with discovery of pre-existing proxy-path helpers After exploring the codebase, much of the proposed architecture is already in place (sanitizeProxyPath, padBootstrap.js basePath derivation, admin SPA rewrite). Spec now reflects the actual delta: header source expansion, /manifest.json prefix-awareness, socialMeta proxyPath honoring, and template URL touch-ups for index/timeslider/pad/export. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * docs(plan): implementation plan for URL base-path support (#7802) Adds the bite-sized TDD task list to ship X-Forwarded-Prefix / X-Ingress-Path support: extends sanitizeProxyPath, makes /manifest.json and socialMeta prefix-aware, touches up the remaining leading-slash URLs in index/pad/timeslider/export templates, fixes a pre-existing manifest .. count bug. Drops the originally-proposed <base href> belt-and-braces after discovering it'd break the existing relative URLs in pad.html/timeslider.html and wouldn't help plugin DOM injection anyway (path-absolute URLs ignore <base>'s path component). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(proxy): accept X-Forwarded-Prefix and X-Ingress-Path under trustProxy (#7802) Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(pwa): make /manifest.json honor sanitised proxy-path (#7802) Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(social-meta): honor proxyPath in from-request og:url and og:image (#7802) Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(templates): index.html manifest + jslicense links honor proxyPath (#7802) Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(templates): pad.html reconnect/jslicense honor proxyPath; fix manifest .. count (#7802) Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(templates): timeslider.html reconnect/jslicense honor proxyPath; fix manifest .. count (#7802) Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(templates): export_html.html manifest honors proxyPath when available (#7802) Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * test: end-to-end coverage for X-Forwarded-Prefix / X-Ingress-Path (#7802) Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * docs(settings): trustProxy also enables X-Forwarded-Prefix / X-Ingress-Path (#7802) Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
962bfe8649
|
feat(updater): tier 4 — autonomous update in maintenance window (#7607) (#7753)
* docs(updater): plan tier 4 — autonomous update in maintenance window (#7607) Maps PR 4 of the auto-update design spec (§"Tier 4 — autonomous") to concrete files, tasks, and verification steps. Subsequent commits scaffold against this plan. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(updater): MaintenanceWindow module — wall-clock window math for tier 4 Pure module: parseWindow, inWindow, nextWindowStart. Supports tz=local|utc and cross-midnight ranges. Used by upcoming Scheduler + UpdatePolicy changes. 22 vitest unit tests cover format validation, same-day + cross-midnight boundaries, and host-local vs UTC clock comparisons. DST handling is absorbed by JS Date constructor's wall-clock normalization (documented in the file header). Refs #7607 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(updater): tier 4 backend — window-gated UpdatePolicy + Scheduler Wires MaintenanceWindow into the existing tier 3 backend so autonomous updates only fire while `now` is inside `updates.maintenanceWindow`. UpdatePolicy - new optional `maintenanceWindow` input - canAutonomous flips on only for git+tier=autonomous+parse-valid window - new reasons `maintenance-window-missing` / `maintenance-window-invalid` - rollback-failed still wins over window denial Scheduler - decideSchedule snaps scheduledFor forward to nextWindowStart when canAutonomous + grace lands outside the window - decideTriggerApply returns a new `{action: 'defer'}` when canAutonomous + fire-time is outside the window; carries nextStart for the runner - canAutonomous=false preserves Tier 3 behavior unchanged index.ts wires settings.updates.maintenanceWindow through both passes and re-arms the timer on defer. Status endpoint surface (nextWindowOpensAt) + admin UI picker land in a follow-up commit. Settings adds `maintenanceWindow: {start, end, tz} | null`, defaulting to null. settings.json.template / settings.json.docker document the shape. Tests - 22 vitest cases for MaintenanceWindow already cover the math - 4 new UpdatePolicy cases for the window outcomes - 6 new Scheduler cases for tier-4 schedule/trigger paths - Full backend-new suite: 629 passed (35 files) Refs #7607 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(updater): tier 4 admin UI — window status, deferred subtitle, banner GET /admin/update/status now returns: - `maintenanceWindow`: the parsed window object (admin sessions only) - `nextWindowOpensAt`: ISO of the next window opening when tier=autonomous UpdatePage - new "Maintenance window" section when tier=autonomous, shows current window summary + next opens at, or "Not configured" when unset - scheduled panel now appends a "deferred until <iso>" line when the backend has snapped scheduledFor to the next window opening UpdateBanner - new variant when tier=autonomous and policy.reason is `maintenance-window-missing` or `maintenance-window-invalid`, linking to /admin/update i18n - 8 new keys under `update.banner.*`, `update.page.policy.*`, `update.page.scheduled.*`, `update.window.*` (en.json only; translations follow via the usual locale workflow) Interactive picker is intentionally deferred — admins edit `updates.maintenanceWindow` via the parsed JSONC settings editor (#7709). A follow-up commit may add a thin write-through component if the JSONC round-trip turns out to be too rough for typical operators. Refs #7607 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * docs(updater): tier 4 — window gate, DST notes, runbook §12 (#7607) CHANGELOG: flip Tier 4 from "designed, not yet implemented" to current. Document maintenanceWindow shape, snap-forward, defer-at-fire, and the two missing/invalid policy reasons. doc/admin/updates.md: new "Tier 4 — autonomous in a maintenance window" section with config example, policy gating, DST/timezone notes, admin UI behavior. runbook: §12 walks a disposable VM through missing-window, malformed, outside-window deferral, fire-at-opening, and window-closes-mid-grace. Adds five sign-off checklist items. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * test(updater): tier 4 window-boundary integration (#7607) Mocha integration covering the four scenarios called out in the spec §"Tier 4 — autonomous": - outside-window: decideSchedule snaps scheduledFor forward to the next opening and the snapped value round-trips through saveState - inside-window at fire-time: decideTriggerApply returns fire - window-closes-mid-grace: decideTriggerApply returns defer with nextStart at the next opening; persisted state moves forward - cancel during deferred-grace: state returns to idle, and the next decideSchedule pass re-emits a schedule snapped to the next opening All 4 cases passing locally under tsx mocha. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(updater): real SMTP via nodemailer (mail.* settings) (#7607) Replaces the (would send email) stub introduced in PR #7601 with a nodemailer-backed transport. The dependency is lazy-imported so installs that don't set mail.host pay no runtime cost. Settings additions - new top-level mail block: host, port, secure, from, auth (user/pass) - mail.host=null keeps the legacy log-only behaviour; the Notifier still updates dedupe state so we don't re-evaluate every tick - settings.json.template documents the shape inline - settings.json.docker reads MAIL_HOST / MAIL_FROM / MAIL_PORT / MAIL_SECURE from env so operators can configure via container env Transport - lazy import('nodemailer') on first send - transport cached by host; settings reload picks up new host without needing a restart - send errors are swallowed (logged warn) so a transient SMTP failure can never poison the surrounding updater state machine - successful sends log at info; legacy "(would send email)" path remains the visible signal when mail is disabled Refs #7607 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(updater): preflight checks target tag's engines.node (#7607) Before mutating the working tree, runPreflight now reads the target tag's package.json via `git show <tag>:package.json` and verifies that process.versions.node satisfies its engines.node range. Failures land at preflight-failed cleanly (no rollback needed — nothing has changed yet). Motivation: a release that bumps the Node floor used to either fail mid-`pnpm install` (which then rolls back successfully) or restart on the new build and crash in the boot path (which then rolls back via the health-check timer). Both paths recover, but they burn a drain + restart cycle on a condition we can reject upfront. Implementation - new PreflightReason `node-engine-mismatch` - new dep `readTargetEnginesNode(tag)` — runs the git-show as a child process with stdio captured to a string; missing tag / missing file / malformed JSON / missing engines.node all resolve to null (treated as "no constraint, pass") - uses existing semver dep with includePrerelease: true - new PreflightInput field `currentNodeVersion`; threaded from process.versions.node in both wirings (scheduler + manual apply) - check runs *after* signature verification so we trust the package.json - PreflightResult carries an optional `detail` string; applyPipeline appends it to the lastResult.reason so the admin UI shows e.g. "node-engine-mismatch: target requires Node >=26.0.0, running 25.0.0" Tests: 6 new vitest cases (no engines.node, satisfies, fails below floor, caret range, loose-spaced range, ordering after signature). Full backend-new: 635 passed (was 629). Refs #7607 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(updater): email admin on auto-rollback / preflight-failed (#7607) Before this commit, only the terminal rollback-failed state emailed the admin. Auto-recovered failures (rolled-back-install-failed, rolled-back- build-failed, rolled-back-health-check, rolled-back-crash-loop) and pre- flight-failed surfaced only via the /admin/update banner — so a 3am autonomous update that failed because of, say, a Node engine bump would roll back silently and stay invisible until the admin next logged in. Notifier - new EmailKinds: 'update-preflight-failed', 'update-rolled-back', 'update-rollback-failed' - new pure decideOutcomeEmail(input) → {toSend, newState} - dedupe key `<outcome>:<targetTag>` in EmailSendLog.lastFailureKey: same outcome on same tag emits one email per cycle (kills retry-loop spam); a different outcome or different tag resets the key - rollback-failed always fires (terminal — overrides dedupe) - state.ts validator + loadState backfill the new field for legacy state files (Tier 1/2/3 installs upgrading in place) Wiring - new index.ts helper notifyApplyFailure() loads state, runs the pure notifier, sends (via the nodemailer-backed sendEmailViaSmtp from the previous commit), persists the new dedupe key — all best-effort - schedulerTriggerApply: fires on applyUpdate returning preflight-failed or rolled-back - /admin/update/apply HTTP handler: same - boot path in expressCreateServer: if state.lastResult is a failure outcome we haven't already emailed about, fire then. Covers: - health-check timeout rollback (timer expired between boots) - crash-loop forced rollback caught on a later boot - preflight-failed where the process didn't get to email before exit - unacknowledged rollback-failed terminal Tests - 8 new vitest cases for decideOutcomeEmail (adminEmail=null, each outcome's content, dedupe by tag, dedupe by outcome, rollback-failed bypass) - Full backend-new suite: 643 passed (was 635) Refs #7607 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(updater): address Qodo review on tier 4 - UpdatePage: only show "deferred until" subtitle when scheduledFor actually matches nextWindowOpensAt. The previous `scheduledFor > now + 60s` heuristic misfired during a normal in-window 15-min grace period. - applyPipeline: return the enriched preflight reason (`reason: detail`) instead of only `pf.reason`, so /admin/update/apply 409 bodies and failure-notify emails preserve diagnostics like the Node engine mismatch detail. - updater/index: key the cached nodemailer transport on the full set of SMTP options (host + port + secure + auth) so runtime changes to port/credentials via reloadSettings() invalidate the cache. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
278acb10cb
|
Drop swagger-ui, document telemetry, add opt-outs (#7524) (#7757)
* docs: design spec for #7524 drop swagger-ui + privacy opt-outs Three-deliverable plan: vendor RapiDoc to replace swagger-ui-express (Scarf-injecting), add privacy.updateCheck and privacy.pluginCatalog opt-outs for our two outbound calls, and ship PRIVACY.md as a public stance doc. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * docs: implementation plan for #7524 swagger-ui + privacy opt-outs Twelve TDD-flavoured tasks: privacy settings shape, UpdateCheck + installer opt-outs (each with a failing-test-first cycle), admin backend/UI plumbing, dependency drop, vendored RapiDoc, PRIVACY.md, final verification matrix. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(privacy): add privacy block to settings shape Adds privacy.updateCheck and privacy.pluginCatalog, both defaulting to true so behavior is unchanged until operators opt out. Refs #7524 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(privacy): honour privacy.updateCheck=false in UpdateCheck check() and getLatestVersion() now early-return when the setting is off. Logs once on first skip. The admin "update available" panel already tolerates an undefined latestVersion. Refs #7524 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(privacy): honour privacy.pluginCatalog=false in installer Extracts the gate into pluginCatalogGuard.ts so it can be unit-tested under vitest without dragging in the CJS require() chain from installer.ts. getAvailablePlugins() now throws the tagged disabled error before any fetch. Refs #7524 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(privacy): emit results:catalogDisabled when pluginCatalog off Short-circuits the four catalog-driven socket events. The install/ uninstall events are untouched so operators can still install by plugin name even when the catalog is disabled. Refs #7524 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(bin): stalePlugins reads updateServer and honours privacy flag Was hardcoding static.etherpad.org and ignoring opt-out. Now exits 0 cleanly when privacy.pluginCatalog=false. Refs #7524 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * docs(settings): document privacy block in settings template Refs #7524 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(api-docs): replace swagger-ui-express with RapiDoc shell Drops the swagger-ui-express dep (third-party Scarf telemetry pixel, see swagger-api/swagger-ui#10573) and serves /api-docs with a static HTML shell that mounts <rapi-doc>. /api-docs.json is unchanged. The vendored RapiDoc asset is added in the next commit so the tree is broken for one diff hunk — pair this with the rapidoc-min.js commit during review. Refs #7524 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(api-docs): vendor RapiDoc 9.3.4 (MIT) as static asset Pinned bundle with checksum in VERSION. Replaces swagger-ui-dist which shipped a Scarf telemetry pixel. Disables RapiDoc's bundled Google Fonts request via load-fonts="false" plus explicit regular-font/mono-font system stacks — RapiDoc's CSS @font-face rules would otherwise fetch Open Sans from fonts.gstatic.com at render time. Also fixes the /api-docs route's res.sendFile to use an absolute path resolved via settings.root (the previous {root: 'src/static'} was resolved from CWD which is already src/, producing src/src/static). Refs #7524 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(admin): banner when plugin catalog is disabled Subscribes to results:catalogDisabled and renders a localized info banner on the plugins page. install/uninstall still function via CLI. Refs #7524 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * docs: PRIVACY.md and README/CHANGELOG pointers Publishes Etherpad's stance on telemetry: two documented, opt-out outbound calls; no third-party analytics; no install-time phone-homes in our deps. Refs #7524 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(admin): await checkPluginForUpdates and emit array on error Qodo flagged that checkUpdates emitted the unresolved Promise (missing await) and emitted {} for updatable on the error path, both breaking the admin UI's expected string[] shape. Pre-existing bug surfaced when the surrounding block was edited for the privacy.pluginCatalog gate. Refs #7524 * feat(api-docs): swap RapiDoc for Scalar (actively maintained) Per @SamTV12345's review on #7757: RapiDoc has been effectively unmaintained for a while. Scalar (https://github.com/scalar/scalar) is MIT-licensed, actively developed, and ships a self-contained standalone bundle that works the same way for our purposes. Privacy posture is preserved by configuring the embed: - withDefaultFonts: false (no fonts.scalar.com woff2 fetch) - telemetry: false (defensive) - agent.disabled: true (no api.scalar.com/vector/* calls) - mcp.disabled: true (no MCP integration) - showDeveloperTools: 'never' - hideClientButton: true Verified with headless Chromium: page loads /api-docs, mounts Scalar, renders the Etherpad OpenAPI document, and makes zero requests to any host other than localhost. Vendor: - src/static/vendor/scalar/standalone.js (@scalar/api-reference 1.57.2) - src/static/vendor/scalar/VERSION (sha256 pinned) - src/static/vendor/scalar/LICENSE (MIT) Removed: - src/static/vendor/rapidoc/* Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
cbe551b432
|
feat(updater): tier 3 — auto update with grace window (#7607) (#7720)
* feat(updater): scheduled execution state + graceStartTag dedupe field (#7607) Preparation for Tier 3 of the auto-update subsystem: - ExecutionStatus gains `scheduled` (targetTag, scheduledFor, startedAt). - EmailSendLog gains `graceStartTag` for one-shot grace-start email dedupe. - state validator accepts the new shape, requires per-status fields, and backfills graceStartTag=null on a Tier 1/2 state file. Plus the implementation plan at docs/superpowers/plans/2026-05-11-auto-update-pr3-tier3-auto.md. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(updater): decideSchedule pure decision function (#7607) Adds src/node/updater/Scheduler.ts with the Tier 3 pure decision logic: - schedules when canAuto + idle/verified/terminal-cleared - reschedules when a newer tag appears mid-grace - emits a grace-start email (once per tag) when adminEmail is set - cancels a stale schedule when policy flips canAuto off - no-ops during in-flight / terminal states - clamps preApplyGraceMinutes to [0, 7 days] Also extends Notifier's EmailKind union with 'grace-start' so the decision result types correctly. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(updater): scheduler timer runner with arm/cancel (#7607) Adds createSchedulerRunner to Scheduler.ts: - arm(): clears any prior timer, sets a fresh one for scheduledFor - cancel(): clears the pending timer, idempotent - past scheduledFor → fires with delay=0 (rehydrate after restart-in-grace) - single-fire-per-arm semantics; armedFor cleared on fire Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * refactor(updater): extract apply pipeline shared by HTTP + scheduler (#7607) Lifts the preflight → drain → execute orchestration out of the /admin/update/apply HTTP handler into src/node/updater/applyPipeline.ts. The HTTP handler keeps its 4xx status mapping; the pipeline owns the state transitions, lock release, drain coordination, and rollback hand- off. The new ApplyPipelineDeps interface accepts an onAccepted callback so the HTTP path can still 202 mid-flow while the Tier 3 scheduler path (next commit) can no-op. Adds `scheduled` to the apply allowed-entry list so an admin can "Apply now" during the Tier 3 grace window. 13 vitest cases cover happy / preflight-failed / cancelled / busy / lock-held / scheduled-entry / rollback / lock-release. Existing 12 mocha integration tests still pass without change. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(updater): wire Tier 3 scheduler into boot + performCheck (#7607) - expressCreateServer instantiates the scheduler runner and rehydrates the timer when a prior boot left state.execution = scheduled - performCheck evaluates decideSchedule after the notifier pass: schedule transitions state + sends grace-start email + arms timer; cancel-schedule resets to idle + cancels timer - shutdown cancels the timer - exposes cancelScheduler() so the cancel endpoint (next commit) can drop the pending schedule - buildSchedulerApplyDeps() supplies the full production-wired pipeline deps (preflight, executor, rollback) for the scheduler-triggered apply Adds tests/backend/specs/updater-scheduler-integration.ts covering boot-rehydrate fire-on-past and the decision-to-state round-trip. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(updater): cancel handler supports Tier 3 scheduled state (#7607) POST /admin/update/cancel now accepts execution.status === 'scheduled' in addition to preflight/draining. The handler calls cancelScheduler() to drop the pending in-process timer, then transitions state to idle with lastResult.outcome = 'cancelled' (mirroring the existing pattern). Adds a Tier 3 integration test that seeds a scheduled state, calls /admin/update/cancel, and asserts the state machine landed correctly. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(admin): countdown + cancel UI for Tier 3 scheduled updates (#7607) - store.ts: extend Execution union with the scheduled variant - UpdatePage.tsx: render countdown panel during scheduled; Apply button is relabelled "Apply now" so the admin can skip the remaining grace; Cancel button accepts scheduled state - UpdateBanner.tsx: dedicated scheduled banner with live remaining time - en.json: new i18n keys (execution.scheduled, banner.scheduled, page.scheduled.{title,countdown,apply_now}) Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * test(updater): playwright spec for Tier 3 scheduled UI (#7607) Three cases against a mocked /admin/update/status: - countdown panel + Apply now + Cancel render when execution is scheduled - Cancel button posts /admin/update/cancel and triggers re-fetch - /admin (banner) shows "Auto-update to <tag> scheduled" copy Mirrors the existing update-page-actions.spec.ts mock pattern (page.route). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * docs(updater): document Tier 3 auto with grace window (#7607) - doc/admin/updates.md: flip Tier 3 from "designed, not yet implemented" to current; expand preApplyGraceMinutes table row; add a Tier 3 section explaining schedule / cancel / Apply now / restart-in-grace and the grace-start email - settings.json.template: clarify the preApplyGraceMinutes comment - CHANGELOG.md: Unreleased entry for Tier 3 - runbook §11: full Tier 3 smoke (happy, cancel, apply-now, restart-in- grace, email) plus the additional sign-off checkboxes Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(admin): UpdatePage handles missing execution field; scope spec locator (#7607) Two CI fixes for PR #7720: 1. UpdatePage.tsx — optional-chain us.execution.status. Integration test stubs (update-banner.spec.ts) ship payloads without the Tier 2/3 execution / lastResult / lockHeld fields; without optional chaining on the new scheduled-derivation line the whole page crashed before the h1 rendered, breaking the unrelated "renders current version" test. 2. update-scheduled.spec.ts — scope the v2.7.2 assertion to the .update-scheduled section. The regex was matching three elements (banner, countdown panel, changelog link) and tripped Playwright's strict-mode locator check. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(updater): address Qodo review (Tier 3 race conditions + tier-off bypass) (#7607) Four fixes for bugs flagged by Qodo's review of PR #7720: 1. **Tier=off bypasses scheduler** (correctness). expressCreateServer used to instantiate the scheduler and rehydrate any persisted `scheduled` state regardless of `updates.tier`. A user who set `tier: "off"` after a schedule had been persisted would still see the timer fire after restart. The boot path now skips scheduler creation when tier is off and explicitly clears a stale scheduled state to idle (logged so the admin sees what happened). 2. **Timer fire skips state recheck** (reliability). The scheduler's timer callback called applyUpdate() directly. Race: admin clicks Cancel at the same instant the timer fires, or the tier flips during the grace window. Now schedulerTriggerApply re-loads state and re-evaluates policy via a new pure decideTriggerApply() helper in Scheduler.ts. If state is no longer scheduled (or scheduled for a different tag), aborts. If policy now denies auto, persists state back to idle and aborts. 3. **Apply-now leaves scheduler timer armed** (correctness). The apply endpoint accepts `scheduled` as an entry status but didn't cancel the in-process scheduler timer. After the admin clicks Apply now, the still-armed timer could later fire and attempt another apply (especially if the manual one finishes in preflight-failed, which is also an allowed-entry status). Apply handler now calls cancelScheduler() when entering from `scheduled`. 4. **scheduledFor not validated as timestamp** (reliability). State validator only required scheduledFor / startedAt etc. to be non-empty strings; a hand-edited "scheduledFor": "garbage" would pass validation and yield NaN delay → immediate fire. The validator now requires known timestamp fields to be parseable via Date.parse(). Tests: 6 new decideTriggerApply cases + 3 new state.ts validation cases. 189 vitest pass / 29 mocha integration pass / 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> |
||
|
|
efb8328084
|
feat(updater): tier 2 — manual-click update from /admin/update (#7607) (#7704)
* 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> |
||
|
|
4f1b524864
|
feat(admin): document admin endpoints in OpenAPI (#7693) (#7705)
* chore(admin): typesafe API client + TanStack Query rails (#7638) (#2)
* docs(admin): design for typesafe API client + TanStack Query rails (#7638)
Rails-only scope: codegen toolchain, runtime client, and provider — no
call-site migrations. Admin endpoints are not yet covered by the OpenAPI
spec, so a separate issue will follow before any migration is useful.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* docs(admin): implementation plan for typesafe API client rails (#7638)
Step-by-step task breakdown for the rails-only PR: codegen toolchain,
runtime client, TanStack Query provider, CI freshness check, docs. No
call-site migrations until admin endpoints are added to the OpenAPI
spec (separate follow-up).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(api): export generateDefinitionForVersion from openapi hook
Required by the admin codegen script (#7638) to dump the OpenAPI spec
without booting Express. No behavior change for the request hook.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* chore(admin): add OpenAPI codegen + TanStack Query deps (#7638)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* chore(admin): add OpenAPI spec dump entry (#7638)
Loaded via tsx by gen-api.mjs in the next commit. Writes JSON to a file
path argument so log4js stdout output (from Settings init) does not
pollute the spec output.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* chore(admin): wire OpenAPI codegen into build (#7638)
Adds gen:api script and amends build/build-copy to regenerate
admin/src/api/schema.d.ts before compiling. The generated file is
checked in so it shows up in PR review and so a fresh checkout doesn't
need codegen to typecheck.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(admin): typed openapi-fetch + react-query client (#7638)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(admin): TanStack Query provider, dev-only devtools (#7638)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(admin): mount TanStack Query provider at root (#7638)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* test(admin): smoke test for typed openapi-fetch client (#7638)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* ci(admin): verify generated OpenAPI schema is up to date (#7638)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* docs(admin): document OpenAPI codegen workflow (#7638)
Replaces the default Vite scaffold README with admin-specific scripts
table and codegen workflow notes.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* build(admin): exclude __tests__ from tsc include (#7638)
The smoke test imports node:test/node:assert which need @types/node.
Admin source is browser-only, so excluding __tests__ from the production
typecheck is cleaner than adding Node types to the bundle config. The
test still runs under tsx, which doesn't share this constraint.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* chore: regenerate lockfile with pnpm 10 to restore overrides block (#7638)
Adding admin deps with pnpm 11 stripped the top-level \`overrides:\`
section from pnpm-lock.yaml, which CI uses pnpm 10 to verify. Result:
ERR_PNPM_LOCKFILE_CONFIG_MISMATCH on every job. Re-running pnpm 10
\`install --lockfile-only\` restores the overrides block; the new admin
package entries land in the same commit. Two stale lockfile entries
not present in package.json (\`serialize-javascript\` version pin and
\`uuid@<14.0.0\`) were normalized by the regen — package.json is the
source of truth for those.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* build(docker): preserve strictDepBuilds=false in trimmed workspace yaml (#7638)
Adding tsx as an admin devDep brings esbuild@0.27.x into the resolved
subgraph, and the Dockerfile's runtime stage was overwriting
pnpm-workspace.yaml with a stripped-down version that lost the
strictDepBuilds=false setting from the source repo. With pnpm 10's
default of strictDepBuilds=true, the install then errors on
ERR_PNPM_IGNORED_BUILDS for esbuild + scarf rather than warning.
Restore the strictDepBuilds=false and the @scarf/scarf ignore in the
trimmed yaml so the production install matches develop's behavior.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(admin): point client baseUrl at /api/<version> via codegen (#7638)
Qodo flagged: with baseUrl='/' and schema paths like '/createGroup',
calls landed at /createGroup, but the backend mounts the FLAT-style
spec under /api/<version>/. So once a call site lands, every request
404s.
gen:api now also emits admin/src/api/version.ts containing
LATEST_API_VERSION (read from info.version in the spec) and a derived
API_BASE_URL = `/api/<version>`. client.ts imports API_BASE_URL.
Workflow freshness check covers both generated files.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* build(admin): cross-platform spawn in gen-api.mjs (#7638)
Windows CI failed because spawnSync('pnpm', ...) cannot resolve
pnpm.cmd without a shell. Set shell:true on win32 only so Linux/macOS
runs avoid Node's DEP0190 warning. All spawn args are literal strings,
so the shell variant is not an injection risk.
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(admin): gitignore generated schema/version, regen on every script (#7638)
Qodo flagged the committed admin/src/api/schema.d.ts as a build artifact
that violates the rule against committing generated files (rule 467291,
"Exclude build artifacts and runtime-generated files from version control").
This commit:
- Adds admin/src/api/{schema.d.ts,version.ts} to .gitignore.
- Removes both from VCS (the prior squash had committed them).
- Chains \`gen:api\` into the dev and test scripts so a fresh checkout
lands a working dev server / test run without an extra step. build
and build-copy already chained gen:api.
- Drops the now-redundant CI freshness diff step from
frontend-admin-tests.yml — with the files no longer committed, the
build step's gen:api invocation is the only check needed.
- Updates admin/README.md to describe the new generated-file workflow.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* docs(admin): design for admin OpenAPI coverage (#7693)
Adds the design doc for documenting `/admin-auth/*` and `/admin/update/status`
in the OpenAPI spec so the typed client generated by #7695 (`admin/src/api/
schema.d.ts`) gains admin call-sites the day it lands.
This PR is stacked on #7695 — codegen rails must merge first. Schema-only,
no call-site migrations (those are an explicit follow-up named in #7693).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* docs(admin): correct UpdateStatus schema to base-branch shape (#7693)
The first draft mirrored the Tier 2 (#7607) response shape, but this PR
stacks on #7695 whose updateStatus.ts only emits the Tier 1 fields. Fix
the schema, install-method enum, and tier enum to match types.ts and
the actual handler. Tier 2 amends UpdateStatus when it lands.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* docs(admin): implementation plan for admin OpenAPI coverage (#7693)
Step-by-step task breakdown for the schema-only PR: stub the document,
add /admin-auth/ + /admin/update/status paths and sub-schemas,
collision regression, /admin/openapi.json route, codegen merge, and CI
verification. No call-site migrations.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(admin): stub OpenAPI document for admin endpoints (#7693)
Adds generateAdminDefinition() returning a minimal valid OpenAPI 3.0
document with no paths yet, plus security schemes for the two auth
modes (Basic + session cookie). Subsequent tasks fill in the actual
admin paths.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(admin): document POST /admin-auth/ in OpenAPI (#7693)
Adds verifyAdminAccess as the operation that the admin UI's LoginScreen
and App session check both call. Documents Basic auth, session cookie,
and anonymous request modes plus their 200/401/403 responses.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(admin): document GET /admin/update/status in OpenAPI (#7693)
Adds getUpdateStatus operation plus UpdateStatus, ReleaseInfo,
PolicyResult, and VulnerableBelowDirective sub-schemas. Property names
and enums mirror src/node/updater/types.ts and the response object
emitted by updateStatus.ts. Tier 2 (#7607) will amend UpdateStatus when
it ships execution/lastResult/lockHeld.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* test(admin): regression net for admin/public OpenAPI collisions (#7693)
Cross-checks admin paths, operationIds, and schema names against the
latest public spec. Today there are no overlaps; the test exists to
catch future renames before they break the merged client codegen.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(admin): expose admin OpenAPI doc at /admin/openapi.json (#7693)
Mounts the admin OpenAPI document at /admin/openapi.json (CORS: *) via
an expressPreSession hook, matching the /api/openapi.json convention.
The admin SPA wildcard at /admin/{*filename} registers later in
expressCreateServer, so the JSON route wins. Live-route test confirms
JSON content-type and CORS header.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(admin): mergeOpenAPI helper for codegen pipeline (#7693)
Pure-JS deep-merge of two OpenAPI 3.0 documents. Unions paths and
components by key; throws on collisions. Public document's info,
servers, and root security win over the admin document's. Used by
dump-spec.ts to produce a single merged JSON for openapi-typescript.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* docs(admin): fix stale "Section 3" reference in spec (#7693)
Drafting numbered the design walkthrough; final spec uses titled
sections. Update the back-reference accordingly.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(admin): include admin OpenAPI in generated client (#7693)
Modifies dump-spec.ts to import generateAdminDefinition alongside the
public generator and feed both through mergeOpenAPI before writing the
JSON consumed by openapi-typescript. The resulting admin/src/api/
schema.d.ts paths interface now exposes /admin-auth/ and
/admin/update/status, ready for typed call-site adoption in a follow-up.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(admin): gate /admin/openapi.json behind a feature flag (#7693)
Address Qodo finding 1: new features must ship behind a flag, disabled
by default (CONTRIBUTING.md, AGENTS.MD, best_practices.md). Adds
settings.adminOpenAPI.enabled (default false). expressPreSession
returns early when the flag is off, so the route is dormant on a fresh
install.
The codegen pipeline imports generateAdminDefinition() in-process and
does not depend on the runtime route, so default-off has no effect on
the typed client. Operators who want third-party tooling (Postman,
swagger-ui, downstream clients) to consume the spec at runtime opt in
via settings.
Adds:
- SettingsType + defaults entry in src/node/utils/Settings.ts
- settings.json.template documentation
- A backend spec asserting expressPreSession is a no-op when the flag
is off (live route test now sets the flag explicitly)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(admin): split fetchClient by surface; admin client baseUrl='/' (#7693)
Address Qodo finding 2 (correctness bug). The merged schema introduced
in this PR exposes both public-API paths (under /api/<version>/) and
admin paths (at root, e.g. /admin-auth/). The single fetchClient from
#7695 has baseUrl=API_BASE_URL ("/api/<version>"), so calling an admin
path through it would resolve to /api/<version>/admin-auth/ — wrong.
Fix:
- Narrow the generated `paths` interface by URL prefix
(`/admin*` vs everything else).
- Export two clients: `fetchClient` (public, baseUrl=/api/<version>)
and `adminFetchClient` (admin, baseUrl='/').
- Mirror with `$api` / `$adminApi` query hook factories.
TypeScript now rejects mixing surfaces at compile time:
fetchClient.GET('/admin-auth/') // type error: not in PublicPaths
adminFetchClient.GET('/createGroup') // type error: not in AdminPaths
Smoke test updated to assert all four exports exist.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* docs(admin): spec — feature flag + dual-client design (#7693)
Reflect Qodo-driven changes:
- /admin/openapi.json route is now feature-flagged (default off).
- admin/src/api/client.ts splits paths by URL prefix and exports two
clients (public + admin) so TypeScript rejects mixing surfaces.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(admin): check adminOpenAPI flag per-request, not at hook setup (#7693)
The first feature-flag commit (
|
||
|
|
ed4579c701
|
docs(7538): soffice is now optional for docx/pdf (#7707)
Native DOCX export, PDF export, and DOCX import shipped in #7568 via pure-JS in-process converters -- LibreOffice/soffice is no longer required for those formats. Stale comments in settings.json.template and settings.json.docker still implied otherwise ("will only allow plain text and HTML import/exports"), and the docker docs told users to configure soffice for DOCX as well. Update them to match what's actually in core: - soffice present: handles all office formats (existing behavior) - soffice null: docx export, pdf export, docx import work natively; odt/doc/rtf export and pdf import still need soffice Touches: - settings.json.template (soffice + docxExport comments) - settings.json.docker (same) - doc/docker.md ("Office-format import/export" section) - doc/docker.adoc (same section + the SOFFICE table row, matching what doc/docker.md already says since #7568) No code changes, no behavior change -- documentation only. Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
85c941fe95
|
feat(padOptions): pass plugin-namespaced ep_* keys through applyPadSettings (#7698)
* feat(padOptions): pass plugin-namespaced ep_* keys through applyPadSettings
Native pad-wide settings ride a single padOptions object: the server seeds
clientVars.initialOptions, the client mutates via pad.changePadOption(), and
the existing padoptions COLLABROOM message broadcasts changes. Plugins can't
use the same rail today because applyPadSettings (client) and
normalizePadSettings (server) silently drop any key not in their hardcoded
whitelist.
Add a passthrough loop that preserves keys matching /^ep_[a-z0-9_]+$/ on both
sides. Plugins can now stash their pad-wide values under their own namespace
(e.g. pad.padOptions.ep_table_of_contents = {enabled: true}) and inherit the
existing broadcast, persistence, creator-only-write enforcement, and
enforceSettings semantics for free.
A new src/node/utils/PluginCapabilities module exposes
padOptionsPluginPassthrough = true so plugins can feature-detect via
require() and fall back to per-user behavior on older cores.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* Address Qodo review on PR #7698
Four concerns raised by Qodo (qodo-free-for-open-source-projects):
1. Feature flag — AGENTS.MD §52 requires new features behind a flag,
disabled by default. Add `enablePluginPadOptions` (default false) gating
the passthrough on both server (normalizePadSettings) and client
(applyPadSettings, via clientVars). Plugins detect the runtime state
through clientVars.enablePluginPadOptions; the static
PluginCapabilities flag stays as the "core can do this" signal.
2. Documentation — add a "Plugin-namespaced pad-wide options" section to
doc/plugins.md covering capability detection, the runtime flag, the
key namespace pattern, and the validation rules. Mirror the flag
description in settings.json.template.
3. Unbounded payload — values for ep_* keys are persisted with the pad and
broadcast to every connected client, so an unvalidated path was a
reliability hazard. Validate every ep_* value:
- Must round-trip through JSON.stringify (rejects functions, symbols,
BigInt, circular refs).
- Per-key serialized size capped at 64 KB.
- Combined ep_* size capped at 256 KB per pad.
Rejects drop the value with a console.warn line; the rest of the pad
settings round-trip cleanly.
4. PadOption type — add `[k: \`ep_${string}\`]: unknown` index signature
so the SocketIO message type matches runtime behavior; TS callers no
longer need unsafe casts to read plugin-namespaced keys.
Also extends the backend test suite with cases covering the runtime flag
(off/on), JSON-serializability rejection, per-key cap, and total cap.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(snap-tests): assert_grep — use here-string to dodge pipefail SIGPIPE
`assert_grep` ran `printf '%s' "$out" | grep -q -F -- "$needle"` under
`set -o pipefail`. When grep matched early it closed its stdin, printf
got SIGPIPE on its next write (exit 141), and pipefail propagated the
broken-pipe failure to the pipeline — making `if` see non-zero and
falling into the FAIL branch even though grep itself succeeded.
Failure was timing-dependent: it only fired when `$out` was large enough
that printf hadn't flushed before grep exited. CI ubuntu-latest tipped
into the racy path on PR #7698 once `settings.json.template` grew by 11
lines (the new `enablePluginPadOptions` flag); the symptom was the
`Wrapper unit tests` step reporting `dbType rewritten to sqlite ✗` with
"got: /*…" output even though the seeded file did contain the needle.
Replace the pipe with a here-string so grep gets its input in one shot
with no pipe between processes — no SIGPIPE possible. The fail-message
`head -3` is converted to a here-string for the same reason.
Repro on a runner whose pipe-buffer flush is slower than grep's first
match would have hit the same flake on any PR; the bug isn't about
this particular template change.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
||
|
|
90aafb115e
|
feat(socialMeta): settings.socialMeta.description override (#7599 follow-up) (#7691)
Issue #7599 follow-up from @stffen: the OG description has no obvious settings.json knob and the i18n catalog default is English, but most preview crawlers (WhatsApp, Signal, Slack, Telegram, Facebook) don't send Accept-Language and so always hit the English fallback regardless of how many locale files translate `pad.social.description`. Keep the i18n catalog as the default source — translatable strings belong in locale files, per the original Qodo review on PR #7635 — but add an explicit `socialMeta.description` setting that wins when set as a non-empty string, regardless of negotiated language. This is the lever that fixes the crawler case for non-English instances without re-introducing per-language config in settings.json (operators who want that still use customLocaleStrings). - Empty/whitespace overrides are treated as unset (would otherwise silently blank the preview). - Override is HTML-escaped via the same path as every other value. - og:locale stays language-negotiated; only the description is forced. - Documented next to publicURL in settings.json.template and settings.json.docker (env var SOCIAL_META_DESCRIPTION). The customLocaleStrings example now spells out pad.social.description so operators discover both routes. 5 new unit specs + 4 new integration specs cover override-wins, null/missing fallback, blank-treated-as-unset, and HTML-escaping. Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
b751fd7b9e
|
feat(settings): enable Pad-wide Settings by default; fix misleading modal title (#7679)
* feat(settings): enable Pad-wide Settings by default; fix misleading modal title The creator-owned Pad-wide Settings feature (#7545) shipped behind a flag that defaulted to false. With the flag off the modal still rendered an H1 of `pad.settings.padSettings` ("Pad-wide Settings") for *every* user, even though no pad-wide controls were ever shown. Two readers in different browsers both saw "Pad-wide Settings" as the modal title, which looked like a creator-gate regression but was just a copy bug. Two changes: 1. Flip the default of `enablePadWideSettings` to `true` (Settings.ts plus both settings templates). With the feature on, the creator (revision-0 author) gets a real "Pad-wide Settings" section gated by `clientVars.canEditPadSettings`, while every other user sees only "User Settings" — matching the design intent of #7545. This is a behavior change, so the settings comments are expanded to describe what the toggle now does. 2. Drop the conditional H1 in `src/templates/pad.html` and always use `pad.settings.title` ("Settings"). Operators who explicitly disable the feature shouldn't see a label that lies about a section that isn't rendered. Adds backend regression coverage in `tests/backend/specs/socketio.ts`: - Different browsers (different cookie jars => different authorIDs): only the first joiner gets `canEditPadSettings: true`. - Same browser, two tabs (shared HttpOnly token cookie => same authorID): both connections are the same identity, both correctly land on the creator path. * test(settings): regression coverage for the settings modal H1 Asserts the rendered `/p/<id>` HTML always uses `data-l10n-id="pad.settings.title"` for the modal heading, regardless of `enablePadWideSettings`. Catches a re-introduction of the old conditional that printed "Pad-wide Settings" for every user when the feature was off. Action of Qodo review feedback on PR #7679. |
||
|
|
69bb1e19c5
|
feat(gdpr): author erasure (PR5 of #6701) (#7550)
* docs: PR5 GDPR author erasure design spec * docs: PR5 GDPR author erasure implementation plan * feat(gdpr): AuthorManager.anonymizeAuthor — Art. 17 erasure * test(gdpr): AuthorManager.anonymizeAuthor unit tests * feat(gdpr): REST anonymizeAuthor on API version 1.3.1 * test(gdpr): REST anonymizeAuthor end-to-end * docs(gdpr): right-to-erasure section + anonymizeAuthor example * fix(gdpr): make anonymizeAuthor resumable on partial failure Qodo review: the `erased: true` sentinel was written before the chat scrub loop, so a throw during scrub left chat messages untouched while subsequent calls short-circuited on `existing.erased` and never finished. Split the write: zero the display identity first (still hides the name), run the chat scrub, and only then stamp `erased: true` so a retry resumes the sweep. Regression test covers the partial-run → retry path. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
487842006c
|
feat(gdpr): configurable privacy banner (PR4 of #6701) (#7549)
* docs: PR4 GDPR privacy banner design spec
* docs: PR4 GDPR privacy banner implementation plan
* feat(gdpr): typed privacyBanner setting block + public getter exposure
* feat(gdpr): send privacyBanner config to the browser via clientVars
* feat(gdpr): privacy banner DOM (hidden by default)
* feat(gdpr): render privacy banner on pad load when enabled
* style(gdpr): privacy banner layout
* test+fix(gdpr): privacy banner Playwright + hidden-attr CSS override
* docs(gdpr): privacyBanner configuration section
* fix(gdpr): reject unsafe learnMoreUrl schemes
Qodo review: showPrivacyBannerIfEnabled assigned config.learnMoreUrl
directly to <a href>, so a misconfigured settings.privacyBanner.
learnMoreUrl of `javascript:alert(1)` or `data:…<script>…` would run
script on click. Validate via URL parsing and allow only http(s) /
mailto; everything else yields no link. Playwright regression guards
the four cases (javascript, data, https, mailto).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(privacy-banner): drop unneeded !important on [hidden] rule
Class+attribute selector already outranks `.privacy-banner { display: flex }`
on specificity (0,2,0 vs 0,1,0), so `!important` was redundant. Adds a
comment explaining why so a future reader doesn't put it back.
Per Sam's review on #7549.
* refactor(privacy-banner): render as a persistent gritter, not custom DOM
Drops the bespoke #privacy-banner template + ~50 lines of popup.css and
delegates to $.gritter.add({sticky: true, position: 'bottom'}). The
notice now matches every other gritter on the pad (theme variables,
shadow, animation, (X) close), sits in the bottom corner instead of
above the editor, and inherits dark-mode handling for free.
The two dismissal modes survive intact:
- dismissible: gritter closes on (X); before_close persists a flag
in localStorage so the notice is suppressed on subsequent loads.
- sticky: closes for the current session only; never persists; the
next pad load shows it again.
learnMoreUrl still goes through the same safeUrl() filter so a
javascript:/data:/vbscript: URL can't smuggle a script handler into the
anchor (Qodo's review concern remains addressed).
Tests: src/tests/frontend-new/specs/privacy_banner.spec.ts now drives
the real showPrivacyBannerIfEnabled via a __etherpad_privacyBanner__
test hook and asserts against the rendered gritter, instead of the
previous tests that mutated DOM by hand and never exercised the
function under test. Coverage adds: enabled=false short-circuit,
dismissible-flag-respected on subsequent show, sticky-ignores-flag,
sticky-close-does-not-persist, javascript: rejection, data: rejection,
and mailto: allow-list.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(privacy-banner): noreferrer + validate dismissal (Qodo)
Two follow-ups from Qodo's review on #7549:
1. The Learn-more link now sets `rel="noreferrer noopener"` (was just
`noopener`). Without `noreferrer` the browser sends the pad URL as a
Referer to the operator-configured external policy site, which leaks
pad identifiers to a third party. Matches the rel pattern already
used by pad_utils.ts.
2. `privacyBanner.dismissal` is now validated in reloadSettings(): an
unknown value falls back to 'dismissible' with a `logger.warn`, in
the same shape as the existing ipLogging validation a few lines up.
The client also guards defensively (treats anything other than the
exact string 'sticky' as 'dismissible') so that hot-reload paths
that skip the server validator can't silently degrade a typo'd
'sticky' into "no close button persisted, no localStorage suppression".
Test added: spec asserts the rel attribute, and a new test exercises
the dismissal fallback (sets dismissal:'wat', asserts the gritter is
shown, the (X) closes it, and the dismissal flag is persisted — i.e.
the unknown value is treated like 'dismissible').
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(privacy-banner): gate test hook on webdriver, align doc with sticky behavior
Two follow-ups from Qodo's second review on #7549.
Rule violation: __etherpad_privacyBanner__ was published on every pad
load even when privacyBanner.enabled was false, so the disabled-by-
default feature still added an observable global. Gate the assignment
on `navigator.webdriver` — Playwright/ChromeDriver/Selenium set this
to true; production browsers do not — so the hook is only present for
tests and the disabled path is genuinely zero-side-effect.
Bug 3 (sticky still closable): doc/privacy.md previously claimed
`dismissal: "sticky"` removes the close button, but the gritter
implementation always renders (X). Aligning the doc with reality —
sticky now means "shows on every load, but closable for the session"
— rather than adding bespoke CSS to a vanilla gritter (matches the
"don't style it differently than other gritter messages" preference
that drove the gritter migration in
|
||
|
|
9014d3a7c4
|
fix(colors): pick WCAG-higher-contrast text for author colors (#7565)
* feat(colors): clamp author backgrounds to WCAG 2.1 AA on render
Fixes #7377.
Authors can pick any color via the color picker, so a user who chooses
a dark red ends up with black text rendered on a background that fails
WCAG 2.1 AA (4.5:1) — unreadable, but there is no way for *viewers* to
remediate since they cannot change another author's color. Screenshot
in the issue shows exactly this.
This PR lands a viewer-side clamp. For each author background, if
neither black nor white text would satisfy the target contrast ratio,
the bg is iteratively blended toward white until black text does. The
author's stored color is untouched — turning off the new
padOptions.enforceReadableAuthorColors flag restores the raw colors
immediately.
New helpers in src/static/js/colorutils.ts:
- relativeLuminance(triple) — WCAG 2.1 relative-luminance formula
- contrastRatio(c1, c2) — in [1, 21]; >=4.5 = AA, >=7.0 = AAA
- ensureReadableBackground(hex, minContrast = 4.5)
— returns a hex that meets minContrast
against black text, preserving hue
Wire-up:
- src/static/js/ace2_inner.ts (setAuthorStyle): pass bgcolor through
ensureReadableBackground before picking text color. Gated on
padOptions.enforceReadableAuthorColors (default true). Guarded by
colorutils.isCssHex so the few non-hex values (CSS vars, etc.) skip
the clamp and pass through unchanged.
- Settings.ts / settings.json.template / settings.json.docker: new
padOptions.enforceReadableAuthorColors flag, default true, with a
matching PAD_OPTIONS_ENFORCE_READABLE_AUTHOR_COLORS env var in the
docker template.
- doc/docker.md: env-var row.
- src/tests/backend/specs/colorutils.ts: new unit coverage for the
three new helpers, including the exact #cc0000 failure case from
the issue screenshot.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* refactor(7377): simplify — just pick higher-contrast text, drop bg clamp
First iteration added an iterative bg-lightening helper
(ensureReadableBackground) gated by a new padOptions flag. CI caught the
correct simpler framing: because WCAG contrast is symmetric in [1, 21],
at least one of black/white always clears AA (4.5:1) for any sRGB
colour. The real bug was that the pre-fix textColorFromBackgroundColor
used a plain-luminosity cutoff (< 0.5 → white), which produced
sub-AA combinations like white-on-red (#ff0000) at 4.0:1.
Reduce the PR to the minimal surface:
- colorutils.textColorFromBackgroundColor now picks whichever of
black/white has the higher WCAG contrast ratio against the bg.
- colorutils.relativeLuminance and colorutils.contrastRatio are kept
as reusable building blocks; ensureReadableBackground is dropped
(no caller needed it once text selection was fixed).
- ace2_inner.ts setAuthorStyle no longer needs the opt-in flag or the
isCssHex guard — the helper handles every input its caller already
passes.
- padOptions.enforceReadableAuthorColors setting reverted along with
settings.json.template, settings.json.docker, and doc/docker.md.
- Tests replaced: instead of asserting the bg gets lightened, assert
that the chosen text colour clears AA for every primary. Covers the
exact #ff0000 failure case from the issue screenshot.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* test(7377): assert relative-contrast invariant, not absolute AA
Pure primaries like #ff0000 cannot clear WCAG AA (4.5:1) against either
#222 or #fff — the best either can do is ~4.0:1. No text-colour choice
alone fixes that; bg clamping would be a separate concern. The test
should therefore verify the *real* invariant: the chosen text colour
must produce the higher contrast of the two options, regardless of
whether that contrast clears any absolute threshold.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(7377): compare against rendered #222/#fff, not pure black/white
First cut of textColorFromBackgroundColor computed contrast against
pure black (L=0) and pure white (L=1), then returned the concrete
#222/#fff the pad actually renders with. For some mid-saturation
backgrounds the two comparisons disagreed — e.g. #ff0000:
vs pure black = 5.25 → pick black → render #222 → actual 3.98
vs pure white = 4.00 → would-render #fff → actual 4.00
The helper picked the wrong option because it compared against the
wrong target. Compare against the actual rendered colours so the
returned text colour is genuinely the higher-contrast choice.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* test(7377): pick unambiguous colibris test bgs
#ff0000 lives right at the boundary for the two text choices (4.00 vs
3.98), so the test for colibris-skin mapping was entangled with the
border-case selector pick. Use #ffeedd (clearly light → dark text
wins) and #111111 (clearly dark → light text wins) so the test
isolates the skin mapping from the tie-breaking logic.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(7377): use rendered text colour + clamp bg to actually meet AA
Local repro of the issue exposed two real bugs in the previous fix:
1. textColorFromBackgroundColor compared bg against a hardcoded #222 —
but in the colibris skin --super-dark-color resolves to #485365.
For the issue's exact case (#9AB3FA author bg) the selector returned
var(--super-dark-color) thinking it was getting a 7.7:1 ratio, while
the browser actually rendered 3.78:1 — identical to what the issue
screenshot reported. This PR's previous behaviour on the issue's
inputs was unchanged from the pre-fix.
2. For mid-saturation pastels (#9AB3FA) and pure primaries (#ff0000)
neither rendered dark nor white text can clear AA. Text-colour
selection alone genuinely cannot fix this band; the ensureReadable
bg clamp dropped in
|
||
|
|
e0a989094d
|
feat(colors): add padOptions.fadeInactiveAuthorColors with toolbar UI (#7554)
Adds a new pad option `fadeInactiveAuthorColors` (default `true`) that controls whether each author's caret/background fades toward white as they go inactive. Configurable server-side (`settings.json` / `PAD_OPTIONS_FADE_INACTIVE_AUTHOR_COLORS`), per-pad in the Pad Settings panel, per-user in the My View panel, or via `?fadeInactiveAuthorColors=false`. Disabling the fade is useful on busy pads where every faded author visually counts as a second on-screen color (a 30-author pad becomes a 60-color pad), or when inactivity tracking is undesirable for whatever reason. Closes #7138. |
||
|
|
5e8704f8d8
|
feat(gdpr): pad deletion controls (PR1 of #6701) (#7546)
* docs: PR1 GDPR deletion-controls design spec First of five GDPR PRs tracked in #6701. PR1 covers deletion controls: one-time deletion token, allowPadDeletionByAllUsers flag, authorisation matrix for handlePadDelete and the REST deletePad endpoint, a single token-display modal for browser pad creators, and test coverage. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * docs: PR1 GDPR deletion-controls implementation plan 13 TDD-structured tasks covering PadDeletionManager unit tests, socket + REST three-way auth, clientVars wiring, one-time token modal, delete-with-token UI, Playwright coverage, and PR handoff. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(gdpr): scaffolding for pad deletion tokens PadDeletionManager stores a sha256-hashed per-pad deletion token and verifies it with timing-safe comparison. createPad / createGroupPad return the plaintext token once on first creation, and Pad.remove() cleans it up. Gated behind the new allowPadDeletionByAllUsers flag which defaults to false to preserve existing behaviour. Part of #6701 (GDPR PR1). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix+test(gdpr): lazy DB access in PadDeletionManager + unit tests Capturing DB.db at module-load time was null until DB.init() ran, which broke importing the module outside a live server (including from the test runner). Switch to DB.db.* at call time and add unit tests exercising create/verify/remove plus timing-safe comparison. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(gdpr): three-way auth for socket PAD_DELETE Creator cookie → valid deletion token → allowPadDeletionByAllUsers flag. Anyone else still gets the existing refusal shout. * feat(gdpr): optional deletionToken on programmatic deletePad * feat(gdpr): advertise optional deletionToken on REST deletePad * test(gdpr): cover deletePad authorisation matrix via REST * feat(gdpr): surface padDeletionToken in clientVars for creators only Revision-0 author on their first CLIENT_READY visit receives the plaintext token; all subsequent CLIENT_READYs receive null because createDeletionTokenIfAbsent is idempotent. Readonly sessions and any other user never see the token. * i18n(gdpr): strings for deletion-token modal and delete-with-token flow * feat(gdpr): token modal + delete-with-token disclosure markup * feat(gdpr): show deletion token once, allow delete via recovery token * style(gdpr): modal + delete-with-token layout * test(gdpr): Playwright coverage for deletion-token modal + delete-with-token * fix(test): auto-dismiss deletion-token modal in goToNewPad helper The token modal introduced in PR1 blocks clicks for every Playwright test that creates a new pad via the shared helper. Add a one-line dismissal so unrelated tests keep passing, and have the deletion-token spec navigate inline via newPadKeepingModal() when it needs the modal open to capture the token. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(test): dismiss deletion-token modal without focus transfer Clicking the ack button transferred focus out of the pad iframe, which made subsequent keyboard-driven tests (Tab / Enter) silently miss the editor. Swap the click for a page.evaluate() that hides the modal and nulls clientVars.padDeletionToken directly, leaving focus where it was. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(gdpr): PadDeletionManager race + document createPad/deletePad Qodo review: - createDeletionTokenIfAbsent() was a non-atomic read-then-write. Two concurrent callers for the same pad could both return different plaintext tokens while only the later hash was stored, leaving the first caller with an unusable recovery token. Serialise per-pad via a Promise chain and add a regression test that fires 8 concurrent calls and asserts exactly one plaintext is emitted and validates. - doc/api/http_api.md now documents createPad returning deletionToken and deletePad accepting the optional deletionToken parameter. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(gdpr): always render delete-with-token in settings popup The rebase onto develop placed the delete-pad-with-token details inside the pad-settings-section conditional, which is only rendered when enablePadWideSettings is true AND the section is toggled visible. Second-device recovery (typing the captured token on a fresh browser) must work without pad-wide settings enabled, so move the details out to sit alongside the existing pad_deletion_token.spec.ts expectations. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(gdpr): require valid token when supplied, gate on auth, harden a11y/i18n - PadMessageHandler: a supplied deletion token must validate; do not fall back to the creator-cookie path when the token is wrong (was deleting the pad anyway when the creator pasted a wrong token into the field). - Skip token issuance + UI when requireAuthentication is on (creator identity is stable, recovery token is redundant noise). - Server emits messageKey instead of hardcoded English; both shout handlers (inline alert and global gritter) localize via html10n. - Suppress the global "Admin message" gritter for pad.deletionToken.* shouts to avoid the "Admin message: undefined" duplicate. - Token-modal a11y: role=dialog, aria-modal, aria-labelledby/describedby, visually-hidden label on the token input, aria-live on Copy, focus to the token input on open and restore on dismiss. - Style the "Delete Pad with Token" disclosure to match the Delete pad button; align the Copy/value row; pad the disclosure label. Tests: Playwright now covers the creator-with-wrong-token path, asserts no "Admin message" / "undefined" gritter on denial; backend API test covers requireAuthentication suppressing the token. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
6195289198
|
feat(gdpr): IP/privacy audit (PR2 of #6701) (#7547)
* docs: PR2 GDPR IP/privacy audit design spec Second of five GDPR PRs (#6701). Audit identifies four log-sites that leak IPs despite disableIPlogging=true, proposes a tri-state ipLogging setting with a back-compat shim, and specifies a doc/privacy.md that documents Etherpad's actual IP handling. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * docs: PR2 GDPR IP/privacy audit implementation plan 7 TDD-structured tasks: anonymizeIp helper + unit tests, tri-state ipLogging setting with disableIPlogging deprecation shim, wiring through 5 leaking log sites, clientVars.clientIp removal, access-log integration test, doc/privacy.md, and PR handoff. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(gdpr): anonymizeIp helper with v4/v6/v4-mapped truncation * feat(gdpr): tri-state ipLogging setting + disableIPlogging shim * fix(gdpr): route every IP log site through anonymizeIp Closes four leaks where disableIPlogging was silently ignored (rate-limit warn, both auth-log calls in webaccess, import/export rate-limit warn) and normalises the four that did honour the flag onto the new ipLogging tri-state via the shared helper. * chore(gdpr): drop dead clientVars.clientIp placeholder Server side: remove the literal '127.0.0.1' assignments from both clientVars and collab_client_vars. Type side: drop clientIp from ClientVarPayload and ServerVar. pad.getClientIp now returns the same '127.0.0.1' literal as a plugin-compat shim (pad_utils.uniqueId still uses it as a prefix). * test(gdpr): ipLogging modes + disableIPlogging shim * docs(gdpr): operator-facing privacy and IP handling statement * fix(gdpr): validate ipLogging at load + regression test for log sites Qodo review: - settings.ipLogging is loaded as a trusted union but nothing enforced the shape. An unknown value (e.g. a typo or null) silently fell through to anonymizeIp's "truncated" branch and emitted partially redacted IPs. Fall back to "anonymous" with a WARN at load time. - New regression test scans the four known log-sites for raw req.ip / socket.request.ip / request.ip inside logger calls that don't wrap through anonymizeIp / logIp, so a future edit that re-introduces a raw IP fails CI. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
e39dbde887
|
feat(updater): tier 1 — notify admin and pad users of available updates (#7601)
* 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>
|
||
|
|
85f9a5f2f5
|
feat: Open Graph & Twitter Card metadata for pad/timeslider/home (closes #7599) (#7635)
* docs(spec): Open Graph metadata for pad pages (issue #7599) Spec for adding og:* and twitter:card meta tags to /p/:pad, the timeslider, and the homepage so shared links unfurl with a useful preview in chat apps. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * docs(spec): expand OG spec — i18n (locale map + og:locale) and a11y (image:alt) Address review feedback: socialDescription accepts a per-language map, og:locale is emitted from the negotiated render language, and image:alt attributes are emitted for screen readers in chat clients. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat: emit Open Graph & Twitter Card metadata for pad/timeslider/home Closes #7599. Pad URLs shared in chat apps (WhatsApp, Signal, Slack, etc.) previously unfurled with no preview because the rendered HTML carried no OG or Twitter Card metadata. This change emits og:title, og:description, og:image, og:url, og:site_name, og:type, og:locale, og:image:alt and the equivalent twitter:* tags on the pad page, the timeslider, and the homepage. A new settings.json key `socialDescription` controls the description. It accepts either a plain string applied to every locale or a per-language map keyed by BCP-47 tag with an optional `default` fallback. og:locale is emitted from the language already negotiated via req.acceptsLanguages and og:image:alt provides screen-reader text for chat-client previews. Pad names from the URL are HTML-escaped before being interpolated into og:title to prevent reflected XSS via crafted pad IDs. Tests: src/tests/backend/specs/socialMeta.ts covers the default, per-locale override, locale fallback, URL decoding, XSS escape, and the timeslider/homepage variants. Semver: minor (new setting; templates emit additional tags but no existing behavior changes). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(test): use valid pad-name char in URL-decode test Spaces aren't allowed in pad names — Etherpad redirected /p/Has%20Space* to a sanitized name (302), so the og:title assertion failed. Use %2D ("-") instead, which is a valid pad-name character and still exercises the URL-decode path. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(socialMeta): don't double-decode pad name from req.params.pad Express has already URL-decoded :pad route params before they reach the handler. Calling decodeURIComponent on the result throws URIError for pad names containing a literal "%" — e.g. the URL /p/100%25 yields req.params.pad === "100%", and decodeURIComponent("100%") throws. This would have prevented the page from rendering for some valid pad IDs. Drop the redundant decode and add a regression test for the "%" case. Reported by Qodo on PR #7635. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * refactor(socialMeta): source description from i18n catalog, drop settings key Per review: the OG description is a translatable string and belongs in Etherpad's locale files alongside the rest of the UI strings, not in settings.json. Operators who want to override it per-language continue to use the standard customLocaleStrings mechanism — no new config surface. Changes: - Add "pad.social.description" to src/locales/en.json (default English). - Export i18n.locales so server-side renderers can look up translations. - socialMeta.renderSocialMeta now takes a `locales` map and resolves renderLang → primary subtag → en, instead of taking a per-locale map from settings. - Remove `socialDescription` from Settings.ts, settings.json.template, settings.json.docker (the key never shipped). - Update tests and spec doc to reflect i18n-sourced description. Reported by Qodo on PR #7635 (also confirmed feature is fine to land default-on; no flag needed). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * test(socialMeta): add unit tests for pure helpers 21 cases exercising buildSocialMetaHtml and renderSocialMeta directly, without HTTP/DB. Covers tag enumeration, HTML escaping, og:locale region formatting, title composition (pad/timeslider/home), description i18n resolution (exact/primary/en fallback, missing catalog), image URL (default favicon vs absolute settings.favicon vs alt text), canonical URL building with query-string stripping, the literal "%" no-throw regression, and attribute-breakout escape. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(socialMeta): defend og:url/og:image against host-header poisoning Previously og:url and og:image were built from req.protocol + req.get('host'), both of which can be client-controlled (Host header directly, or X-Forwarded-* under trust proxy). A crafted Host could make the server emit OG tags pointing at an attacker's origin — harmful if any cache fronts the response or if a vulnerable proxy forwards the headers unsanitized. Two-layer defense: 1. New optional setting `publicURL` lets operators pin the canonical origin used for shared link previews ("https://pad.example"). When set, og:url and og:image use it unconditionally. Sanitized at use time: must be http(s)://host[:port] with no path, no userinfo, no trailing slash; malformed values fall back to the request. 2. When `publicURL` is unset, the request-derived fallback now strictly validates the Host header against /^[a-z0-9]([a-z0-9.-]{0,253}[a-z0-9])?(:\d{1,5})?$/i and caps the scheme to "http"/"https". A crafted Host (CRLF injection, userinfo, "<script>") is replaced with "localhost" instead of being echoed into og:url. Reported by Qodo on PR #7635. Tests: 5 new unit cases covering publicURL preference, trailing-slash strip, malformed-publicURL fallback, Host validation, scheme cap. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * refactor(socialMeta): tighten types, drop `any` - `req: any` -> express `Request` (covers acceptsLanguages/protocol/get/originalUrl). - `settings: any` -> local `SocialMetaSettings` interface narrowed to the three fields we actually read (title/favicon/publicURL); avoids coupling to the full Settings module surface. - `availableLangs: {[k: string]: any}` -> `{[lang: string]: unknown}`; only keys are read, so values stay deliberately unconstrained. No runtime change. All 26 socialMeta unit tests still pass. Per Sam's review on #7635. --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
e8c9efb5c5
|
chore: Rename some occurences of etherpad-lite to etherpad (#7552)
* chore: Rename some occurences of etherpad-lite to etherpad * chore: Adjust etherpad git urls * chore: Rename more occurences from etherpad-lite to etherpad * chore: Adjust default text |
||
|
|
e0ccdb4d9f
|
Add creator-owned pad settings defaults (#7545)
* Add creator-owned pad settings defaults Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Refine pad settings layout Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Fix settings popup heading and width Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Explain enforced user settings Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Cover creator override flow Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Let creators bypass enforced settings Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Address pad settings follow-ups Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --------- Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> |
||
|
|
7ec581afca
|
feat!: replace Abiword with LibreOffice and add DOCX export (#7539)
* feat!: replace Abiword with LibreOffice and add DOCX export (#4805)
The Abiword converter is dropped. Abiword's DOCX export is weak and the
project is niche on modern platforms; LibreOffice (soffice) is the
common deployment path and now serves as the sole converter backend.
DOCX is added as an export format and becomes the new target for the
"Microsoft Word" UI button. The /export/doc URL still works for legacy
API consumers.
BREAKING CHANGE: The 'abiword' setting, the INSTALL_ABIWORD Dockerfile
build arg, the abiwordAvailable clientVar, and the
#importmessageabiword UI element (with locale key
pad.importExport.abiword.innerHTML) are removed. Deployments relying on
Abiword must configure 'soffice' instead.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat: add docxExport feature flag and abiword deprecation WARN
- Add `docxExport: true` setting to opt out of DOCX (use legacy DOC)
- Pass `docxExport` to client via clientVars
- Use `docxExport` flag in pad_impexp.ts for Word button format
- Emit a specific WARN when deprecated `abiword` config is detected
- Update settings.json.template and settings.json.docker with docxExport
- Add docxExport to ClientVarPayload type in SocketIOMessage.ts
Agent-Logs-Url: https://github.com/ether/etherpad/sessions/9afc5291-73b2-4b66-b028-feed39e7056f
Co-authored-by: JohnMcLear <220864+JohnMcLear@users.noreply.github.com>
* refactor: extract wordFormat variable and improve docxExport comment
Agent-Logs-Url: https://github.com/ether/etherpad/sessions/9afc5291-73b2-4b66-b028-feed39e7056f
Co-authored-by: JohnMcLear <220864+JohnMcLear@users.noreply.github.com>
* fix: restore import-limitation message when no converter is configured
The abiword removal dropped both the #importmessageabiword DOM element
and its locale key, but Copilot's refactor still expected the show()
call to surface a message when exportAvailable === 'no'. Result: users
with no soffice binary got silent failure instead of an explanation.
Add #importmessagenoconverter back with updated, LibreOffice-focused
copy (new locale key pad.importExport.noConverter.innerHTML) and flip
the hidden prop when the client knows no converter is available.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* i18n: inline English fallback for noConverter import message
The original abiword message existed in ~70 locale files and was
removed from all of them by this PR. The replacement key was only
added to en.json, so non-English users had an empty div until
translators localize. Follow the project's usual pad.html pattern
(e.g. line 146's "Font type:") and include the English text inside
the div as the fallback content; html10n replaces it when a
translation is available.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* Revert "i18n: inline English fallback for noConverter import message"
This reverts commit
|
||
|
|
29faec4a04
|
fix: increase max socket.io message size to 10MB for large pastes (#7474)
* fix: increase max socket.io message size to 10MB for large pastes The default maxHttpBufferSize of 50KB caused socket.io to drop connections when pasting >10,000 characters. Increased to 10MB which safely accommodates large paste operations. Fixes #4951 Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * chore: reduce default maxHttpBufferSize to 1MB 10MB was too generous and creates a DoS vector. 1MB (socket.io's own default) is sufficient for large pastes while limiting memory abuse. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com> |
||
|
|
da9f5ac4ee
|
fix: add periodic cleanup of expired/stale sessions from database (#7448)
* fix: add periodic cleanup of expired/stale sessions from database SessionStore now runs a periodic cleanup (every hour, plus once on startup) that removes: - Sessions with expired cookies (expires date in the past) - Sessions with no expiry that contain no data beyond the default cookie (the empty sessions that accumulate indefinitely per #5010) Without this, sessions accumulated forever in the database because: 1. Sessions with no maxAge never got an expiry date 2. On server restart, in-memory expiration timeouts were lost 3. There was no mechanism to clean up sessions that were never accessed again Fixes #5010 Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: resolve TypeScript error for sessionStore.startCleanup() Use a local variable for the SessionStore instance to avoid type narrowing issues with the module-level Store|null variable. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: address Qodo review — chained timeouts, cleanup tests, docs - Replace setInterval with chained setTimeout to prevent overlapping cleanup runs on large databases - Store and clear startup timeout in shutdown() to prevent leaks - Add .unref() on all timers so they don't delay process exit - Fix misleading docstring — cleanup removes empty no-expiry sessions, not sessions older than STALE_SESSION_MAX_AGE_MS (removed unused const) - Add 5 regression tests: expired sessions removed, empty sessions removed, sessions with data preserved, valid sessions preserved, shutdown cancels timer Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * feat: add cookie.sessionCleanup setting to control session cleanup Session cleanup is now gated behind cookie.sessionCleanup (default true). Admins who want to keep stale sessions can set this to false in settings.json. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com> |
||
|
|
474918a881
|
feat: make cookie names configurable with prefix setting (#7450)
* feat: make cookie names configurable with prefix setting Add cookie.prefix setting (default "ep_") that gets prepended to all cookie names set by Etherpad. This prevents conflicts with other applications on the same domain that use generic cookie names like "sessionID" or "token". Affected cookies: token, sessionID, language, prefs/prefsHttp, express_sid. The prefix is passed to the client via clientVars.cookiePrefix in the bootstrap templates so it's available before the handshake. Server-side cookie reads fall back to unprefixed names for backward compatibility during migration. Fixes #664 Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: default cookie prefix to empty string for backward compatibility Changing the default to "ep_" would invalidate all existing sessions on upgrade since express-session only looks for the configured cookie name. Default to "" (no prefix) so upgrades are non-breaking — users opt-in to prefixed names by setting cookie.prefix in settings.json. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: address Qodo review — cookie prefix migration and fallbacks - l10n.ts: Read prefixed language cookie with fallback to unprefixed - welcome.ts: Use cookiePrefix for token transfer reads - timeslider.ts: Use prefix for sessionID in socket messages - pad_cookie.ts: Fall back to unprefixed prefs cookie for migration - indexBootstrap.js: Pass cookiePrefix via clientVars to welcome page - specialpages.ts: Pass settings to indexBootstrap template Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: escape regex metacharacters in cookie prefix, document Vite hardcode - l10n.ts: Escape special regex characters in cookiePrefix before using it in RegExp constructor to prevent runtime errors - padViteBootstrap.js: Add comment noting the hardcoded prefix is dev-only and must match settings.json Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * security: validate cookie prefix to prevent header injection Reject cookie.prefix values containing characters outside [a-zA-Z0-9_-] to prevent HTTP header injection via crafted cookie names (e.g., \r\n sequences). Falls back to empty prefix with an error log. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com> |
||
|
|
21f0992a17
|
chore: added settings for darkmode and relaxed width and height of inputs (#7204)
* chore: added settings for darkmode and relaxed width and height of inputs * chore: add explanation for showRecentPads |
||
|
|
60a40d53a7
|
chore: enabled dark mode (#7057) | ||
|
|
233b8fcc04 | feat: added home button also in settings.ts as default | ||
|
|
483153493f | feat: added home button | ||
|
|
cd5a0227dc | feat: fixed templates | ||
|
|
40884fa96b | feat: disable stats endpoint if enableMetrics is false | ||
|
|
b6bdbe1a2a |
settings: use correct syntax for default values containing "\n" in settings.json.template
|
||
|
|
1ad9418a6f
|
Add code for revision cleanup (#6442)
* Add initial code for revision cleanup * Some improvements - code cleanup * Cleanup logging * Add button in admin backend to cleanup revisions of a specific pad * Disable cleanup by default and show errors in admin area * Improve cleanup code * Load revisions for cleanup in parallel * Consider saved revisions during pad cleanup |
||
|
|
4891243c27
|
Added support for alternative update servers. (#6645) | ||
|
|
c8797efaf2
|
Adding a layoutType (#6559)
This will allow end users to change the console output to not output colors |
||
|
|
0b0d882477
|
Increase the maxHttpBufferSize (#6409) | ||
|
|
556c3c8e5b
|
Readded support for apikey (#6382) | ||
|
|
8a76d2c680
|
chore: add docs how to obtain bearer token (#6328)
* chore: add docs how to obtain bearer token * Added configurable ttl to settings --------- Co-authored-by: SamTV12345 <40429738+samtv12345@users.noreply.github.com> |
||
|
|
fb56809e55
|
Feat/oauth2 (#6281): Added oauth to API paths
* Added oauth provider. * Fixed provider. * Added auth flow. * Fixed auth flow and added scaffolding vite config. * Added working oauth2. * Fixed dockerfile. * Adapted run.sh script * Moved api tests to oauth2. * Updated security schemes. * Removed api key from existance. * Fixed installation * Added missing issuer in config. * Fixed dev dependencies. * Updated lock file. |
||
|
|
b2be2ca714
|
Migrate Socket.IO from Version 2 to Version 3 🚀 (#6152)
* feat :migrate socket.io 2 -> 3 * fix: backend test * fix: ts error * rm * reset the test timeout * Updated cli client. * Updated lock file. * Use updated load tester. --------- Co-authored-by: SamTV12345 <40429738+samtv12345@users.noreply.github.com> |
||
|
|
d5fc948705
|
Removed tidy html. (#6039) | ||
|
|
2bb431e7e5
|
express-session: Implement and enable key rotation (#5362) by @rhansen
* 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> |
||
|
|
675c0130b9
|
allow option to make pad names case-insensitive (#5501) by @DanielHabenicht
* New option to make pad names case-insensitive fixes #3844 * fix helper.gotoTimeslider() * fix helper.aNewPad() return value * Update src/node/utils/Settings.js Co-authored-by: Richard Hansen <rhansen@rhansen.org> * remove timeout * rename enforceLowerCasePadIds to lowerCasePadIds * use before and after hooks * update with socket specific test * enforce sanitizing padID for websocket connections - only enforce for newly created pads, to combat case-sensitive pad name hijacking * Added updated package.json file. --------- Co-authored-by: Richard Hansen <rhansen@rhansen.org> Co-authored-by: SamTV12345 <40429738+samtv12345@users.noreply.github.com> |
||
|
|
861a929a43 |
docker: Sync settings.json.docker with .template
|
||
|
|
692749d1cf | express-session: Extend session lifetime if user is active | ||
|
|
023e58cfe6 | express-session: Set a finite cookie lifetime | ||
|
|
8c857a85ac |
pad: Use null as default for userName, userColor options
These options are used as strings, so it doesn't make sense to default them to a boolean value. Note that this change has no effect due to a bug in how pad options are processed; that bug will be fixed in a future commit. |
||
|
|
61b608e264 |
pad: Use null as default for lang option
It doesn't make sense to override the browser's language with `en-gb` by default. Note that this change has no effect due to a bug in how pad options are processed; that bug will be fixed in a future commit. |