Both scripts still called the pre-v6 callback-style ueberdb2 API, producing
type errors (masked in places by `// @ts-ignore`) against the current
promise-based signatures (`set(key, value)`, `init()`, `close()` — no
callback/extra args):
importSqlFile.ts(73) initDb(null) Expected 0 arguments, but got 1
migrateDirtyDBtoRealDB.ts(51) db.set(k,v,bcb,wcb) Expected 2 arguments, but got 4
migrateDirtyDBtoRealDB.ts(56) db.close(null) Expected 0 arguments, but got 1
migrateDirtyDBtoRealDB.ts(57) dirty.close(null) Expected 0 arguments, but got 1
- importSqlFile: drop the unused `util` import and the `util.promisify`
wrappers; `await db.init()`, `await db.set(...)`, `await db.close()`
directly. Removes two `// @ts-ignore` that were hiding the broken calls.
- migrateDirtyDBtoRealDB: replace the bcb/wcb callback machinery with
`await db.set(key, value)` in the loop and call `close()` with no args.
Also fixes the progress log which referenced an undefined `length`
instead of `keys.length`.
Pure type/correctness cleanup; behaviour is unchanged (writes are now
awaited, which is equivalent or safer). `tsc --noEmit` on the bin package
is now clean.
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
`bin/migrateDB.ts` opens a source and target ueberdb2 Database, copies all
keys, then resolves without closing either connection or calling
process.exit(). Two problems with ueberdb2 6.1.x:
- 6.1.x keeps an internal keep-alive timer running until close() is called,
so the migration process hangs forever after "Done syncing dbs" instead
of exiting. (Pre-6.1.x it exited on its own once the work was done.)
- Target writes are buffered and only guaranteed flushed to disk on close()
/flush(), so an operator who Ctrl-Cs the apparently-finished process could
end up with an incomplete migration.
Close the target then the source on both the success and error paths (which
flushes buffered writes and clears the keep-alive timer) and exit with an
explicit status code, matching the pattern already used in
migrateDirtyDBtoRealDB.ts.
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
ueberdb2 6.1.10 rewrote the cache/buffer layer (CacheAndBufferLayer.ts),
replacing the constructor's always-on, *referenced* `setInterval` flush timer
with a lazily-armed `setTimeout` that is `.unref()`'d and only created when
there are dirty keys. On a fresh, empty dirty DB there are no dirty keys, so no
flush timer is ever armed and ueberdb2 no longer anchors Node's event loop. In
the packaged (.deb/systemd) production boot this exposes a startup window where
the loop has no referenced handle and the process exits cleanly (code 0) before
`server.listen()` binds the port — so the server never serves /health.
Symptom on develop: the Debian-package amd64 smoke test failed on three
consecutive pushes (#7966 ueberdb2 6.1.9->6.1.12, then #7965, #7967), with the
service logging the version banner then "Deactivated successfully" and the
health check on :9001 never connecting. Backend/Docker/downstream-smoke stayed
green because they keep the loop alive by other means; only the bare
fresh-empty-dirty-DB packaged boot hits the gap. ueberdb2 6.1.12 is the latest
published release, so there is no fixed version to roll forward to yet — pin
back to the last green release (6.1.9) on both src/ and bin/.
Also add a `pull_request` trigger to the Debian-package workflow (scoped to the
same production-footprint paths). The smoke step is the only check that catches
this "boots then exits before binding" class of regression, but it previously
ran only on push to develop — i.e. *after* merge — which is exactly why the
Dependabot bump turned develop red instead of being blocked at PR time. The
release/apt-publish jobs are tag-guarded, so PRs run the build+smoke job only.
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Bumps ueberdb2 6.1.8 -> 6.1.9 (postgres pool-error handling + TCP
keep-alive for #7878; redis/rethink connection-error handlers so a
dropped DB connection no longer crashes the process) and adds the
3.3.1 CHANGELOG entry required by bin/release.ts before the release
workflow can run.
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* chore(release): remove deprecated bin/createRelease.sh
This script has carried a "DEPRECATED since Etherpad 1.7.0 (2018-08-17),
left here just for documentation" banner for years and is dead code:
- It authenticates to the GitHub API with the `?access_token=` query
parameter, which GitHub removed in 2021 — every API call (token check,
branch merge, release publish) now fails outright.
- It targets the old `ether/etherpad-lite` repo paths and calls
`bin/buildForWindows.sh` / `make docs`, neither of which is how releases
are built anymore.
- Nothing references it (no workflow, script, or doc).
The current release flow is the "Release etherpad" workflow
(.github/workflows/release.yml) driving bin/release.ts, then the tag-push
triggers handleRelease.yml + releaseEtherpad.yml. createRelease.sh only adds
confusion, so remove it.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* docs(agents): document the release procedure and docs publishing
Add a "Releasing" section to AGENTS.MD so maintainers have a single
reference for cutting a release, instead of reverse-engineering it from
bin/release.ts and the workflow files.
Covers:
- Prerequisites: the CHANGELOG `# X.Y.Z` guard, and the requirement that all
four package.json files agree (release.ts reads the current version from
src/package.json) — the desync that blocked the 3.3.0 release.
- The one-dispatch flow: "Release etherpad" -> bin/release.ts -> tag push,
and what the vX.Y.Z tag auto-triggers (handleRelease GitHub Release,
docker, snap-publish), plus the separate manual npm publish dispatch.
- Documentation: the two distinct kinds of doc work — per-PR doc/ updates in
behaviour-change PRs, and the automated release-time versioned-docs publish
into the ether.github.com sibling repo.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The Release workflow has failed on the last two dispatches with:
Error: No changelog record for 3.4.0, please create changelog record
at bin/release.ts:146
3.3.0 was never released (latest tag/release is v3.2.0). The repo already
carries a complete `# 3.3.0` changelog section, and the intent is to ship
3.3.0 next. A prior commit reverted the root and admin/ package.json back to
3.2.0 to set up a clean 3.2.0 -> 3.3.0 minor bump, but missed src/package.json
and bin/package.json, which were left at 3.3.0.
bin/release.ts reads the current version from src/package.json, so it computed
minor(3.3.0) = 3.4.0 and aborted on the missing 3.4.0 changelog. Syncing src
and bin back to 3.2.0 (matching root + admin) makes the next `minor` dispatch
compute 3.2.0 -> 3.3.0, which satisfies the existing changelog check.
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* 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>