* docs: refresh docs for 3.2.0 — correct stale content, document recent features
The hand-maintained VitePress docs under doc/ had drifted behind a lot of
recent work. They are authored prose (not generated from the OpenAPI spec),
so they need manual upkeep. This pass corrects content that was actively
wrong and documents features shipped since they were last touched.
Corrections (was wrong / misleading):
- cli.md: every command used `node bin/foo.js`, but the scripts are
TypeScript run via pnpm — copy-paste failed. Rewrote to
`pnpm run --filter bin <script>`, documented ~13 previously-undocumented
operator tools, and split running-vs-stopped requirements. Registered the
missing `compactStalePads` script in bin/package.json so the documented
invocation actually works.
- stats.md: described a pre-Prometheus world. Rewrote for the gated
`/stats` (JSON) and `/stats/prometheus` endpoints, the live metric set,
the opt-in `scalingDiveMetrics` instruments (#7756), and `measured-core`.
- admin/updates.md: removed three false "SMTP not yet wired" claims (it is,
via nodemailer + the `mail.*` block), documented the `node-engine-mismatch`
preflight check and the rollback/preflight failure emails, and stripped
obsolete "PR 1 / PR 2" staging language now that all tiers ship.
- api/http_api.md: added the undocumented `anonymizeAuthor` (GDPR Art. 17)
call, fixed copyPad/movePad version annotations (1.2.8 → 1.2.9), corrected
getPadID's param name (readOnlyID → roID), and dropped a reference to a
non-existent `getEtherpad` API call.
- skins.md: colibris is the current default, not an "experimental" skin for
a future 2.0.
- localization.md: bare `window._('key')` is unbound and returns undefined;
recommend `window.html10n.get(...)` / data-l10n-id instead.
- README.md: bumped the v2.2.5 upgrade example to v3.2.0; fixed a
docker.adoc link to docker.md.
- docker.md: added MAIL_*, ENABLE_METRICS, GDPR_AUTHOR_ERASURE_ENABLED,
PRIVACY_BANNER_*, PUBLIC_URL, AUTHENTICATION_METHOD, ENABLE_DARK_MODE,
ENABLE_PAD_WIDE_SETTINGS; fixed the SOCKETIO_MAX_HTTP_BUFFER_SIZE default
(50000 → 1000000).
New documentation:
- configuration.md (new): how settings + `${VAR:default}` substitution work,
trustProxy, and — the previously-undocumented feature — running under a
subpath/ingress via x-proxy-path / X-Forwarded-Prefix / X-Ingress-Path,
with the sanitizer rules and Traefik/NGINX examples. Wired into the
VitePress sidebar and the index hero.
- hooks_server-side.md: ccRegisterBlockElements (the server-side companion
plugin authors miss), exportConvert, exportHTMLSend, createServer,
restartServer, and clientReady (marked deprecated).
- hooks_client-side.md: aceDrop, acePaste, handleClientTimesliderMessage_<name>.
VitePress build passes. The legacy .adoc set was intentionally left in place
— it still feeds the per-version doc archives published to ether.github.com
at release time (bin/release.ts), so it is not dead and is out of scope here.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* docs: address Qodo review — drop .js invocations from configuration.md and CLI help
- configuration.md: the settings-override example referenced a nonexistent
`node src/node/server.js`. Use the supported launcher instead
(`bin/run.sh -s <file>`), and note the runtime is server.ts via tsx.
- compactStalePads.ts / compactPad.ts / compactAllPads.ts: their header
comments and runtime usage output still printed `node bin/*.js`, which
points at files that don't exist. Switched to the documented
`pnpm run --filter bin <script>` form so the --help text matches the docs.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* 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>
* Roll Node.js floor back to >= 24 (Active LTS)
Closes#7779.
#7779 originally proposed bumping past the Node 25 stop-gap to Node 26.
After re-checking the release schedule, the cleaner LTS target is
actually Node 24:
- Node 24 (Krypton) is currently in Active LTS, supported until ~May 2028.
- Node 25 hit end-of-life on April 10 2026 — the floor merged in
#7752 / #7749 / #7754 a day ago ships an already-EOL major.
- Node 26 was released May 5 2026 and does not enter Active LTS until
October 2026.
So this PR reverts the Node 25 ratchet from those three PRs and lands
on Node 24 — Etherpad's runtime floor stays on a supported LTS for the
next ~2 years.
Runtime / infra
- `package.json` + `src/package.json`: `engines.node` `>=25.0.0` -> `>=24.0.0`
- `bin/functions.sh`, `bin/installer.sh`, `bin/installer.ps1`:
`REQUIRED_NODE_MAJOR` 25 -> 24
- `Dockerfile`: `node:25-alpine` -> `node:24-alpine` (both stages).
Corepack-via-npm workaround is intentionally kept: it works on
Node 24 (which still ships corepack) and on Node 25+ (which doesn't),
so the same recipe survives the next LTS bump without churn. Comments
reworded accordingly.
- `snap/snapcraft.yaml`: pinned `NODE_VERSION` 25.9.0 -> 24.15.0; design
notes + corepack comment adjusted
- `packaging/nfpm.yaml`: `nodejs (>= 25)` -> `nodejs (>= 24)` in
top-level depends + deb/rpm overrides
- `packaging/bin/etherpad`: comment matches the new pin
- `packaging/README.md`: build prereqs + apt install snippet point at
`node_24.x`; the long-stale "engines.node floor is 20" line is fixed
while we're here
- `.github/workflows/*.yml`: setup-node `node-version` 25 -> 24 across
every workflow; backend / frontend-admin / upgrade matrices
`[25]` -> `[24]`
- `.github/workflows/deb-package.yml`: `NODE_MAJOR=25` + `node_25.x`
smoke-test installer -> 24
- `bin/plugins/lib/npmpublish.yml`: 25 -> 24 (template propagates to
the ~80 ether/* plugins via update-plugins workflow)
Docs
- `README.md`: install one-liner + Requirements -> Node.js >= 24
- `doc/npm-trusted-publishing.md`: runner requirement -> Node 24
- `doc/plugins.md` / `doc/plugins.adoc`: plugin metadata example
`engines.node` -> `">=24.0.0"`
@types/node is left at ^25.8.0 — newer type definitions cover Node 24
runtime fine and avoid an unnecessary lockfile churn.
Companion homepage one-liner change to follow on ether/ether.github.com.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* docs(plugins): example engines.node = ">=22.0.0", not core's floor
Plugin code is overwhelmingly ace-hook glue and rarely uses Node-version-
specific APIs, so plugin engines.node should reflect the plugin's own
requirements, not track core. Showing core's 24-floor in the example
encouraged plugin authors to blindly copy a tighter pin than necessary
and locked plugins out of being installable on older Etherpad/Node
deployments. Use the most-recent Node LTS that has actually reached EOL
(20 -> EOL April 2026) as the example floor, i.e. >=22.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* docs: bump documented Node.js minimum to 25
Etherpad is moving its supported Node.js floor to >= 25 (CI matrix is
already pinned to 25 across all workflows on the node25-corepack-pnpm11
work). Sync the user-facing documentation so the install instructions,
requirements section, and plugin metadata example all reflect the new
minimum instead of Node 22 / 12.17.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* chore: require Node.js >= 25 (engines, installers, Dockerfile, snap, CI)
#7747 added Node 25 *support* but left the floor at Node 22. This
commit completes the cutover so the runtime requirement matches the
documentation bumped in the previous commit.
- package.json: engines.node ">=22.13.0" → ">=25.0.0"
- bin/functions.sh, bin/installer.sh, bin/installer.ps1: REQUIRED_NODE
bumped to 25 (controls the error message users see when they invoke
the installer or pnpm scripts on an older Node)
- Dockerfile: base image node:22-alpine → node:25-alpine (×2). Corepack
comment updated: Node 25 no longer ships corepack at all, so we
install it from npm rather than refreshing a stale signing-key list
- snap/snapcraft.yaml: pinned NODE_VERSION 22.22.2 → 25.9.0 and the
surrounding design notes rewritten to reflect Node 25 instead of 22
- .github/workflows/*.yml: matrix dropped from [22, 24, 25] to just
[25] (anything older now fails engines anyway). Stale comments in
build-and-deploy-docs.yml referencing vite 8's 22.12 floor cleaned up
- bin/plugins/lib/npmpublish.yml: setup-node 22 → 25 so the plugin
template propagated to every ether/* plugin matches the new minimum
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(docker): install pnpm directly on Node 25 (no corepack)
node:25-alpine doesn't ship corepack but does pre-install yarn at
/usr/local/bin/yarn, so `npm install -g corepack@latest` fails with
EEXIST trying to register its yarn shim. Per #7747, end-users install
pnpm via plain `npm install -g pnpm` on Node 25 — use the same flow in
the Dockerfile (and remove the unused yarn binary so it doesn't sit on
PATH inside the image). Drops COREPACK_HOME and the related
issue-7687 cache-sharing tweak since there's no corepack shim to share.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(7642): bin/compactStalePads — staleness-gated bulk compaction
Adds bin/compactStalePads with --older-than / --keep / --dry-run.
Composes listAllPads → getLastEdited → compactPad so hot pads in
active timeslider use are left alone and only the cold tail is
compacted. Targeting stays a CLI concern; compactPad's API surface
is unchanged.
Per-pad failures (including a getLastEdited fault) don't stop the
run — same error-tolerance shape as compactAllPads. End-to-end test
plumbs through the real /api/1.3.1/getLastEdited + compactPad
endpoints to lock the adapter contract.
Daily-cron variant (cleanup.compactOlderThanDays setting) deferred
to a follow-up so this PR stays focused on the on-demand operator
tool from the issue's primary acceptance bullet.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(7642): TOCTOU recheck before compaction + admin CLI docs
Qodo flagged two real issues:
1. Race window between staleness selection and compaction. On a long
bulk run a pad could become active between first-pass filtering and
compactPad, which would then kick those sessions. Added a getLastEdited
recheck right before each compact call; if the pad is now fresh it's
reclassified as skippedFresh rather than failed (the user did the
right thing — edited it — and we bow out).
2. doc/cli.md had nothing on pad compaction at all (gap predates this
PR; #6194 landed without doc updates). Added a Pad compaction section
covering all three CLIs — compactPad, compactAllPads, compactStalePads
— so the toolset is discoverable as a unit.
Tests cover both the recheck-skip path and a recheck-failure 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>
ep_disable_chat#75 ran with `disables: ["@feature:chat"]` declared in
ep.json but the helper printed "No 'disables' declared — running
standard test suite" and exec'd a vanilla `playwright test`, with
@feature:chat-tagged tests running anyway and timing out one by one.
Root cause: the auto-detect used `find -maxdepth 3 plugin_packages/
-name ep.json -not -path '*/.versions/*'`. Live-plugin-manager
installs plugins under `plugin_packages/.versions/<name>@<ver>/`
and exposes them as symlinks at `plugin_packages/ep_<name>`. find(1)
doesn't follow symlinks by default, so:
- the .versions/ ep.json was excluded by -not -path
- the symlink at plugin_packages/ep_<name> was visited but find
didn't recurse into it because it's a symlink, not a real dir
=> 0 candidates found, helper falls through to standard mode.
Switch to a shell glob with `-f` membership tests, which resolve
symlinks correctly. Verified against a synthetic install: the helper
now finds the disables list and prints "Plugin disables: @feature:chat"
before running pass 1.
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Pass 2 was running every test tagged with a disabled feature to
completion. For a busy tag like @feature:chat (12 tests) with the
default 90s per-test timeout, that's 10+ minutes of expected failures
piling up — ep_disable_chat's first PR #75 ran 14+ min before being
cancelled.
Pass 2 only needs *evidence* the feature is gone — one failing tagged
test is enough. Add:
--max-failures=1 stop on the first failure (~30s vs ~10min)
--timeout=30000 cap any single test at 30s
--retries=0 already there, but flagged: CI retry default
(up to 5 with WITH_PLUGINS=1) would retry an
"expected failure" multiple times.
Worst case (first tagged test happens to pass): we wait for the
second one to fail, ~60s. Still bounded.
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Plugins that intentionally remove a baseline Etherpad feature
(ep_disable_chat, ep_disable_change_author_name, ep_disable_error_messages,
ep_disable_reset_authorship_colours) currently break core tests for the
removed feature. Their main branches are red, their auto-publish gates
never fire, and Dependabot PRs pile up.
The temptation is to give these plugins an "opt-out of these tests"
flag — but that's a self-serving attestation: a plugin can claim "I
just disable chat, ignore those tests" and quietly break unrelated
functionality on the user's install. etherpad.org/plugins would still
show it green.
This commit introduces a small declared-disables contract that closes
that gap:
1. Core specs grow @feature:* Playwright tags. Initial set:
@feature:chat, @feature:username, @feature:clear-authorship,
@feature:error-gritter. Tags are added test-by-test where the
test exercises a single feature, so the contract stays precise.
2. Plugins declare which feature tags they disable in their ep.json:
{ "name": "ep_disable_chat", "disables": ["@feature:chat"], ... }
3. bin/run-frontend-tests-with-disables.sh enforces the contract via
two passes:
- Pass 1 (regression): every test NOT in the disabled list must
pass. Catches plugins that break things they don't claim to.
- Pass 2 (honesty): every test that IS in the disabled list
must FAIL. Catches plugins that lie about disabling features
they don't actually disable, and stops them from grep-inverting
arbitrary unrelated tests.
4. doc/PLUGIN_FEATURE_DISABLES.md walks the design and migration.
The disables list is in ep.json (publicly visible), so etherpad.org/plugins
can surface "this plugin disables: chat" alongside the green CI badge —
users see what they're losing before they install.
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(plugins): updatePlugins.sh actually updates installed plugins (#6670)
bin/updatePlugins.sh detected outdated plugins by running
`pnpm --filter ep_etherpad-lite outdated --depth=0`, but installed
plugins are not registered in src/package.json — bin/plugins.ts adds
them via linkInstaller.installPlugin which writes to
src/plugin_packages/.versions/<name>@<version>/ and tracks the result
in var/installed_plugins.json. pnpm has no view of them, so `outdated`
returns empty and the script always reported "All plugins are
up-to-date" even when newer versions existed on the registry. PR #7468
fixed npm→pnpm and install→update but kept the same broken detection
mechanism, which is why the issue stayed open after that PR landed.
Read the plugin list from var/installed_plugins.json instead, then
re-invoke linkInstaller.installPlugin(name) for each entry. Calling
the installer without a version pin resolves the registry-latest and
overwrites the existing pinned copy, so an outdated plugin is brought
to head while plugins already at latest are no-ops apart from the
pnpm cache hit.
Add an `update`/`up` action to bin/plugins.ts so users can also run
`pnpm run plugins update` directly, mirroring the existing
install/remove/list actions. updatePlugins.sh becomes a one-line
wrapper for backwards compatibility.
Reproduction (verified):
pnpm run install-plugins ep_markdown@11.0.5 # latest is 11.0.18
./bin/updatePlugins.sh # → 11.0.18
Edge cases tested: no plugins installed, missing installed_plugins.json,
already-at-latest re-run.
Closes#6670.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(plugins): validate ep_ prefix and dedupe + add regression test
Qodo flagged two issues on the original update() addition:
1. Security — update() trusted every name in var/installed_plugins.json,
so a corrupted or hand-edited manifest could coerce the script into
installing arbitrary npm packages. pluginfw/plugins.getPackages
already gates on the ep_ prefix; mirror that gate here.
2. Reliability — no automated regression test, so a future refactor
could silently bring back the broken behaviour.
Extract the safe-name filter to filterUpdatablePluginNames in
bin/commonPlugins.ts (pure, side-effect-free, prefix configurable, also
de-duplicates repeats so a duplicated entry installs once). Use it from
plugins.ts update().
Add src/tests/backend/specs/filterUpdatablePluginNames.ts covering: keep
prefixed names, drop ep_etherpad-lite, reject non-prefixed entries,
de-dupe repeats, tolerate missing/null/non-string name fields, empty
input, custom prefix.
Manually verified end-to-end on a live install: an
installed_plugins.json containing ep_markdown@11.0.5, a duplicate
ep_markdown, and a "malicious-package" entry runs `Updating plugins to
latest from registry: ep_markdown` (only) and ep_markdown ends up at
11.0.18 — the bad entries are silently filtered out.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(pad): compactHistory() + compactPad CLI for DB-size reclaim
Fixes#6194. Long-lived pads with heavy edit history dominate the DB —
the issue describes a ~400 MB Postgres after two months with ~100
users. Etherpad keeps every revision forever, and removing arbitrary
middle revisions is unsafe because state is reconstructed by composing
forward from key revisions.
What's safe: collapse the full history into a single base revision
that reproduces the current atext. The existing `copyPadWithoutHistory`
already does this for a new pad ID — this PR lifts that same changeset
pattern into an in-place operation and wires up an admin CLI.
- `Pad.compactHistory(authorId?)` (src/node/db/Pad.ts): composes the
current atext into one base changeset, deletes all existing rev
records, clears saved-revision bookmarks, and appends the new rev 0.
Text, attributes, and chat history are preserved; saved-revision
pointers are cleared. Returns the number of revisions removed.
- `API.compactPad(padID, authorId?)` (src/node/db/API.ts): public-API
wrapper around compactHistory. Reports `{removed}` so callers can
log savings.
- `APIHandler.ts`: register `compactPad` under a new `1.3.1` version,
bump `latestApiVersion`.
- `bin/compactPad.ts`: admin CLI. Reports the current revision count,
calls compactPad via the HTTP API, and prints how many revisions
were dropped.
- `src/tests/backend/specs/compactPad.ts`: four backend tests cover
the empty-pad no-op, the text-preservation + head=0 contract,
saved-revision cleanup, and that subsequent edits continue to
append cleanly on top of the collapsed base.
The operation is destructive so admins must opt in explicitly; the CLI
prints the before-count, and the recommended pre-flight is an
`.etherpad` export (backup).
Closes#6194
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(compact): delegate to copyPadWithoutHistory via temp-pad swap
The initial compactHistory() implementation built a custom base
changeset and re-ran appendRevision against a reset atext — but the
changeset was packed with oldLength=2 (matching copyPadWithoutHistory's
dest-pad init state) while the reset atext was only length 1, so
applyToText tripped its "mismatched apply: 1 / 2" assertion and every
test failed with a Changeset corruption error.
Switch to the tested path instead: copy the pad via
copyPadWithoutHistory to a uniquely-named temp pad (inherits all its
attribute/pool/changeset correctness), read the temp pad's rev records
back, delete the old ones under our pad's ID, write the new records in
their place, update in-memory state to match, and remove the temp pad.
Errors at any step fall through with a best-effort temp-pad cleanup.
Contract shifts slightly: the collapsed pad is head<=1 rather than
head=0, matching the shape of a freshly-imported pad (seed rev 0 +
content rev 1). Tests updated to assert that invariant plus
text-preservation, saved-revision cleanup, and append-after-compact.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* test(6194): match the head<=1 post-compact contract
Tests previously asserted head=0 exactly after compaction; the
temp-pad-swap path lands at head=1 (one seed rev plus one content
rev) matching the shape of a freshly-imported pad. Relax the
assertions to and derive the removed-count from
before-head minus after-head, so the tests still catch regressions in
text-preservation, saved-revision cleanup, and append-after-compact
without being tied to the exact implementation shape.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* refactor(6194): wrap existing Cleanup instead of duplicating it
Develop already ships a working revision-cleanup path under
`src/node/utils/Cleanup.ts` with two public helpers —
`deleteAllRevisions(padId)` (collapse full history via
copyPadWithoutHistory) and `deleteRevisions(padId, keepRevisions)`
(keep the last N). The admin-settings UI wires these up but neither
is exposed on the public API, and there's no CLI for operators who
want to run compaction outside the web UI. That's the gap this PR
now fills.
Changes from the prior revision of this PR:
- Drop `pad.compactHistory()` — it re-implemented what
`Cleanup.deleteAllRevisions` already does. Remove the duplicate.
- `API.compactPad(padID, keepRevisions?)` now delegates to Cleanup:
• keepRevisions null/undefined → deleteAllRevisions (full collapse)
• keepRevisions >= 0 → deleteRevisions(N) (keep last N)
Returns {ok, mode: 'all' | 'keepLast', keepRevisions?}.
- APIHandler `1.3.1`: signature updated to take `keepRevisions`
instead of `authorId`.
- `bin/compactPad.ts`: accepts `--keep N` for the keep-last mode,
shows before/after revision counts so operators see concrete
savings.
- Backend tests rewritten around the public API surface (mode
reporting, text preservation, input validation) rather than
internal method plumbing that no longer exists.
Net: strictly a thin public-API and CLI veneer over already-tested
Cleanup helpers. No new low-level logic.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* test(6194): assert content markers, not byte-exact atext
Cleanup.deleteAllRevisions internally calls copyPadWithoutHistory
twice (src → tempId, tempId → src with force=true), and each round
trip normalizes trailing whitespace. That meant my byte-exact
atext.text assertion failed in CI:
expected: '...line 3\n\n\n'
actual: '...line 3\n'
Swap the comparisons to use content markers (marker-alpha / beta /
gamma, keep-line-N). The test still catches the real regressions —
if compactPad lost content those markers would disappear — without
coupling to whitespace quirks of the existing Cleanup implementation.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(6194): correct API param + document compactPad in http_api docs
The 1.3.1 entry in APIHandler registered `['padID', 'authorId']`, but
`API.compactPad` takes `(padID, keepRevisions)` and the CLI sends a
`keepRevisions` query param. APIHandler.handle dispatches by URL field
name, so the previous wiring silently dropped `keepRevisions` and never
ran the keep-last branch over HTTP.
- Register `['padID', 'keepRevisions']` so the handler forwards the
CLI/HTTP arg into the API function.
- Add HTTP-level dispatch tests that hit `/api/1.3.1/compactPad` with
and without `keepRevisions`. The direct `api.compactPad()` tests
bypass the handler and would have missed this regression.
- Document compactPad in `doc/api/http_api.md` and `http_api.adoc`,
and bump the documented latest version from 1.3.0 to 1.3.1 to match
`latestApiVersion`.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(6194): add bin/compactAllPads for per-instance bulk compaction
`bin/compactPad <padID>` covers the case where you know which pad is
fat. For "reclaim space across the whole instance," composing
`listAllPads` + `compactPad` yourself is annoying; this script does it.
- Walks every pad on the instance and compacts it (full collapse, or
`--keep N` keep-last).
- Per-pad failures don't abort the run — they're logged, counted, and
the script exits 1 if any failed.
- `--dry-run` lists pads + revision counts without writing anything,
so operators can scope impact before committing.
- Reports `before → after` per pad and a total reclaimed count.
Deliberately not adding a `compactAllPads` HTTP API: bulk compaction
over a single HTTP request means one giant response and a long-held
connection. Operators who want this should run it locally, where they
can see progress and kill it cleanly. Staleness gating ("only pads
older than X days") is tracked separately as a follow-up.
Also registers `compactPad` and `compactAllPads` script aliases in
`bin/package.json` so they show up next to the other admin CLIs.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* test(6194): cover the bin/compactAllPads loop logic
Previous commit added the script but only exercised it by hand. The
loop itself — error tolerance, dry-run gating, keep-last passthrough,
the empty-instance and listAllPads-failure paths — had no automated
coverage.
- Refactor compactAllPads.ts to export `runCompactAll(api, opts, logger)`
and `parseArgs(argv)`. The CLI shell wires them up to axios+APIKEY
for production; tests use an in-memory `CompactAllApi` so we don't
need to stand up the apikey-auth path in mocha.
- Add 9 specs covering: arg parsing, full-collapse iteration,
--keep N passthrough, --dry-run skipping writes, single-pad failure
not aborting the run, pre-flight count failure tolerated, a
listAllPads failure short-circuiting cleanly, the empty-instance
no-op, and a final end-to-end test that runs `runCompactAll`
against the real `/api/1.3.1/compactPad` handler over supertest+JWT
to catch contract drift between the CompactAllApi shape and the
HTTP endpoints.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(6194): address Qodo review — gate, integer check, SSL
Three valid concerns from the Qodo review on 75a08a13:
1. **cleanup.enabled gate.** The admin/Cleanup-socket path checks
`settings.cleanup.enabled` before doing anything destructive; the
public API was bypassing that gate. Now `compactPad` mirrors the
admin path's check and returns a clear apierror when disabled, so
exposing the API doesn't accidentally widen the cleanup-opt-in
surface.
2. **Number.isFinite → Number.isInteger.** `2.5` was finite and
non-negative, so the old check let it through into
`Cleanup.deleteRevisions`, which does revision-index arithmetic
that assumes integer math. Reject at the API boundary instead of
silently misbehaving.
3. **SSL-aware baseURL in the bin scripts.** Other bin scripts
hardcode `http://`, but the rest of the codebase uses
`settings.ssl ? 'https' : 'http'`. The compact CLIs now do the
same, so they work against HTTPS deployments. (Other bin scripts
carry the same bug but fixing them is out of scope for this PR.)
Tests:
- New spec: `rejects fractional keepRevisions` (2.5 with the old
check passed; the new one rejects).
- New spec: `refuses to run when cleanup.enabled is false`. The
existing API tests opt in via a before-hook + restore, so they
still cover the success path under the new gate.
- API docs (`http_api.md` + `http_api.adoc`) document the gate and
the new error message.
Skipped Qodo concerns:
- "Wrong compactPad parameters" — already fixed in 26e12ff7
(the param map now correctly says `keepRevisions`, not `authorId`).
- "Unbounded revision deletions" / "No session eviction" / changeset
base-length / padCreate hook — these all targeted the earlier
on-Pad implementation that was refactored away. The current code
wraps `Cleanup.deleteAllRevisions` / `deleteRevisions`, which
already handle concurrency, locking, and hook semantics.
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: 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
* checkPlugin: flag absolute /static/plugins/ paths in templates (#5203)
Plugin templates that reference assets as \`/static/plugins/...\`
(absolute) silently break any Etherpad instance hosted behind a reverse
proxy at a sub-path — the browser resolves the path against the domain
root instead of the proxy prefix and the asset 404s. The right form is
\`../static/plugins/...\` (relative), which ep_embedmedia PR #4 fixed
manually and which #5203 asked for as a mechanical check.
Walk \`templates/\` and \`static/\` of the plugin, scan every \`*.ejs\` /
\`*.html\` for \`/static/plugins/\` not preceded by a URL scheme, dot, or
word char (so \`https://host/static/plugins/...\` and already-correct
\`../static/plugins/...\` stay untouched). Warn normally; in \`autofix\`
mode rewrite to the relative form in place.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* checkPlugin: skip autofix for static/*.html
Addresses Qodo review: HTML served from a plugin's static/ directory
resolves against /static/plugins/<plugin>/static/..., so rewriting
/static/plugins/... to ../static/plugins/... yields a broken URL. Keep
scanning static/ for warnings but no longer rewrite, and clarify the
remediation guidance to point at the file's own location.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
These files are stale: there's no CI/tooling left that reads them.
- .travis.yml — Etherpad moved to GitHub Actions years ago. The
workflows in .github/workflows/ are the source of truth.
- .lgtm.yml — LGTM was sunset by GitHub in late 2022.
- start.bat — README only documents the PowerShell installer for
Windows now (irm .../installer.ps1 | iex), no docs or scripts
reference start.bat.
- bin/installOnWindows.bat — same; not referenced by README, docs
or workflows.
Also drop the .travis.yml line from the plugin layout in
doc/plugins.md and replace it with a pointer at .github/workflows/.
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>