Commit graph

5 commits

Author SHA1 Message Date
John McLear
04045fe532
Roll Node.js floor back to >= 24 (Active LTS) — closes #7779 (#7781)
* 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>
2026-05-16 13:07:59 +01:00
John McLear
7d537f3deb
Require Node.js >= 25 (engines, installers, Dockerfile, snap, CI, docs) (#7749)
* 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>
2026-05-15 10:04:24 +01:00
John McLear
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>
2026-05-07 17:17:05 +01:00
John McLear
958590d1c8
chore(docker): clear most CVEs in published image (npm/pnpm/uuid + drop curl) (#7674)
* chore(docker): clear most CVEs in published image — npm/pnpm/uuid + drop curl

Cuts published-image vulnerabilities from 18 (4H/13M/1L) across 8 packages
to 12 (2H/9M/1L) across 3 packages. The remaining three (curl/libcurl,
git, busybox) are all upstream Alpine 3.23 packages with "not fixed"
status — libcurl is pulled in transitively by git and cannot be
removed independently.

Changes:

- Provision pnpm via corepack instead of `npm install -g pnpm`, then
  remove the bundled npm. The base image's npm@10.9.7 ships old
  transitives (picomatch 4.0.3 → CVE-2026-33671/33672, brace-expansion
  2.0.2 → CVE-2026-33750) that we don't otherwise need at runtime;
  corepack handles pnpm directly without npm. Fixes 1H + 1M.

- Bump PnpmVersion 10.28.2 → 10.33.2 to align with the rest of the
  workflow and pull in pnpm's patched bundled brace-expansion (5.0.5
  vs 5.0.4). Fixes 1M.

- Add `uuid@<14.0.0` → `>=14.0.0` to pnpm.overrides
  (GHSA-w5hq-g745-h8pq). Fixes 1M.

- Drop `curl` from the runtime apk add list and switch HEALTHCHECK to
  wget (busybox built-in). curl was only invoked by the healthcheck and
  by dev/CI scripts that don't run in the container. Removes the curl
  CLI binary; libcurl remains as a git transitive dep, so the
  `apk/alpine/curl` advisories scout reports against libcurl persist
  but aren't reachable from any code we ship. As a side-effect this
  also clears nghttp2 (CVE-2026-27135) which was a curl-CLI dep.

- Switch HEALTHCHECK URL from `localhost` to `127.0.0.1` — alpine/musl
  resolves localhost to ::1 first and Etherpad only binds IPv4.

Verified locally: docker build → docker run → healthy → docker scout
cves shows 12 CVEs / 3 packages.

* fix(docker): refresh corepack before preparing pnpm (Qodo)

Node 22's bundled corepack ships a stale signing-key list and can reject
newer pnpm releases (nodejs/corepack#612), which would fail the image
build at `corepack prepare`. Mirror the snap/snapcraft.yaml workaround:
`npm install -g corepack@latest` before activating pnpm, in both
adminbuild and build stages. npm is still removed afterwards.

* docs(changelog): note docker image dropping curl/npm/npx (Qodo)

Address Qodo's "backwards-incompatible change without mitigation" rule
violations by documenting the removal in the 2.7.3 breaking-changes
section. Operators who exec into the container can apk add curl on
demand or use the busybox wget / pnpm already present.

* chore: pnpm

* chore: pnpm

* chore: pnpm

* chore: pnpm

* chore: pnpm

* chore: pnpm

* chore: pnpm

* chore: pnpm

* chore: pnpm

---------

Co-authored-by: SamTV12345 <40429738+samtv12345@users.noreply.github.com>
2026-05-06 22:00:13 +02:00
John McLear
02e37e0112
feat(packaging): publish Etherpad as a Snap (#7558)
* feat(packaging): publish Etherpad as a Snap

Adds first-class Snap packaging so Ubuntu / snapd users can install via
`sudo snap install etherpad-lite`.

- snap/snapcraft.yaml — core24, strict confinement, builds with pnpm
  against a pinned Node.js 22 runtime. Version is auto-derived from
  src/package.json so `snap info` tracks upstream release numbering.
- snap/local/bin/etherpad-service — launch wrapper that seeds
  $SNAP_COMMON/etc/settings.json on first run (rewriting the default
  dirty-DB path to a writable $SNAP_COMMON location) and execs Etherpad
  via `node --import tsx/esm`.
- snap/local/bin/etherpad-healthcheck-wrapper — HTTP probe for external
  supervisors, falling back to Node if curl isn't staged.
- snap/local/bin/etherpad-cli — thin passthrough to Etherpad's bin/
  scripts (importSqlFile, checkPad, etc.).
- snap/hooks/configure — exposes `snap set etherpad-lite port=<n>` and
  `ip=<addr>` with validation, restarts the service when running.
- snap/README.md — build / install / configure / publish instructions.
- .github/workflows/snap-publish.yml — builds on every v* tag, uploads
  a short-lived artifact, publishes to `edge`, and then promotes to
  `stable` through a manually-approved GitHub Environment. Requires a
  one-time `snapcraft register etherpad-lite` plus provisioning of the
  `SNAPCRAFT_STORE_CREDENTIALS` repo secret (instructions inline).

Pad data (dirty DB, logs) lives in /var/snap/etherpad-lite/common/ and
survives snap refreshes. The read-only $SNAP squashfs is never written
to at runtime.

Refs #7529

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(snap): pass --settings flag, env-subst ip/port, 2-space indent

Addresses Qodo review feedback on #7558:

1. Settings file ignored: Etherpad's Settings loader reads `argv.settings`,
   not the `EP_SETTINGS` env var. Without `--settings`, the launcher's
   seeded $SNAP_COMMON/etc/settings.json is never loaded; Etherpad falls
   back to <install-root>/settings.json, which lives on the read-only
   squashfs — so the default dirty-DB path ends up unwritable and the
   daemon fails to persist pads. Fix: pass `--settings "${SETTINGS}"` to
   node; drop the EP_SETTINGS export.

2. `snap set` overrides were no-ops: the seeded settings.json carries the
   template's literal `"ip": "0.0.0.0"` / `"port": 9001` values, which
   override the env-based defaults Etherpad exposes via ${…}
   substitution. Users following the README saw the listener stay put
   after `snap set etherpad-lite port=…`. Fix: after copying the
   template on first run, rewrite the top-level `ip` and `port` lines
   to `"${IP:0.0.0.0}"` / `"${PORT:9001}"`. Use `0,/…/` anchors so the
   `dbSettings.port` entry further down stays literal.

3. Indentation: reflow the new shell scripts from 4-space to 2-space to
   match the repo style rule.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(snap): default seeded settings to sqlite, not dirty

settings.json.template's own comment says dirty is for testing only.
A Snap install is the "not testing" case — shipping it by default
means every `sudo snap install etherpad-lite` starts on a DB the
project explicitly recommends against.

Rewrite the postinstall sed to switch dbType: "dirty" → "sqlite" and
point filename at $SNAP_COMMON/var/etherpad.db. sqlite is already
shipped in-tree via ueberdb2 → rusty-store-kv (prebuilt napi-rs
binary, no build deps), so this works under strict confinement with
zero snap.yaml changes.

Only affects first-run seeding; existing $SNAP_COMMON/etc/settings.json
is never touched on refresh.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(snap): rename to "etherpad", glob tag filter, harden cli

- Snap is registered as `etherpad` (the project's only name) — drops the
  legacy `etherpad-lite` from the name, app, paths, install dir, configure
  hook, README and workflow artifact. The daemon app shares the snap name,
  so `snap install etherpad` exposes a bare `etherpad` command; the bin/
  passthrough is now `etherpad.cli`.
- snap-publish.yml: GitHub Actions tag filters use globs, not regex. The
  prior `v?[0-9]+.[0-9]+.[0-9]+` pattern would never match a real release
  tag (Qodo review). Replace with two glob entries covering `vX.Y.Z` and
  `X.Y.Z`.
- etherpad-cli: reject path-traversal in the `<bin-script>` arg (anything
  containing `/`, `..`, or empty) and add a default `*)` case so files
  with unsupported extensions fail loud instead of silently exiting 0
  (Qodo review).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(snap): unbreak build — refresh corepack, drop pnpm prune

Two issues hit on the first real `snapcraft pack` of this recipe:

- `corepack prepare pnpm@10.33.0 --activate` failed with
  `Cannot find matching keyid` because Node 22.12's bundled corepack
  ships a stale signing-key list and rejects newer pnpm releases
  (nodejs/corepack#612). Refresh corepack itself via npm before
  preparing pnpm.
- `pnpm prune --prod` is interactive on workspace projects: it asks
  "The modules directories will be removed and reinstalled from
  scratch. Proceed? (Y/n)" and deadlocks on stdin under sudo + tee.
  Replace it with the explicit "wipe node_modules + prod reinstall"
  pattern, which is non-interactive, faster (pnpm resolves the prod
  graph from its CAS cache), and byte-identical in result.

Verified locally: `snapcraft pack --destructive-mode` produces
`etherpad_2.6.1_amd64.snap` end-to-end in ~3 min.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(snap): unbreak runtime — tsx resolution, var/ writability, env

Three runtime crashes surfaced when actually installing the built snap
under strict confinement. Fixed each, plus a smoke-test script.

- `tsx` is in the `src` workspace's node_modules under pnpm hoisting,
  not at the snap install root. The wrapper now `cd "${APP_DIR}/src"`
  and uses bare `--import tsx` (matching `bin/cleanRun.sh`); the prior
  `--import tsx/esm` triggered ERR_REQUIRE_CYCLE on Etherpad's mixed
  CJS/ESM source tree.
- Etherpad's plugin installer writes `var/installed_plugins.json` via
  __dirname-relative paths, which resolve to absolute paths inside the
  read-only snap squashfs (EROFS). snap layouts can't intercept paths
  inside `$SNAP`, so replace the shipped `var/` dir with a symlink to
  `/var/snap/etherpad/common/etherpad-app-var/` (auto-created by the
  wrapper on first run). Persistent state survives `snap refresh`.
- Drop the unused `EP_SETTINGS` and `EP_DATA_DIR` env vars from the
  app's `environment:` block. Etherpad's settings loader doesn't read
  them — it reads `argv.settings`, which the wrapper already passes via
  `--settings`. They were producing `[WARN] settings - Unknown Setting`
  noise on every start.

Add `snap/tests/smoke.sh`: rebuild + install + configure test port 9003
+ assert listener + curl /health + tail logs. Local verified output:
  HTTP 200, body {"status":"pass","releaseId":"2.6.1"}, server logs
  `Etherpad is running` on `http://0.0.0.0:9003/`.

.gitignore now excludes destructive-mode build outputs (parts/, stage/,
prime/, .craft/, *.snap).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* test(snap): wrapper unit tests, PR CI build, expanded docs

Coverage in snap/tests/ (47 assertions, ~5s, no snapd/sudo/network):
- test-snapcraft-yaml.sh: required keys, name validity, daemon-app
  matches snap name, no etherpad-lite regression, env-var whitelist.
- test-cli.sh: path-traversal rejection, .ts/.sh dispatch, default-case
  rejection, no-args usage.
- test-configure.sh: port (1-65535) and ip (v4/v6) validation via
  mocked snapctl.
- test-service-bootstrap.sh: first-run seeding from
  settings.json.template, sed rewrite of dbType/filename/ip/port,
  writable-dir creation, snapctl override propagation to node env,
  idempotency on second run, default fallbacks.
- run-all.sh: bash -n syntax check on every wrapper + hook, then
  sources each test file and reports totals. All assertions use port
  9003 (project test convention).

CI in .github/workflows/snap-build.yml:
- Triggers on PR / push-to-develop touching snap/, settings.json.template,
  or the workflow itself.
- Job 1 wrapper-tests: runs run-all.sh.
- Job 2 snap-pack: snapcraft pack --destructive-mode, uploads .snap as
  PR artifact for sideload.
- Stays separate from snap-publish.yml (tag-triggered, store-bound).

snap/README.md fully rewritten:
- User-facing usage, install, configure
- Architecture: file layout, var/-symlink rationale, settings.json
  rewrite rationale, double-pnpm-install rationale, daemon-name-shares-
  snap-name rationale
- Three test layers with exactly when/why to run each
- Dev workflow loop
- Publishing maintainer setup
- Troubleshooting for every failure mode hit during this PR (EROFS,
  tsx not found, ERR_REQUIRE_CYCLE, snap-store-down, pnpm prune hang)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* docs(snap): replace dead snapcraft.io/docs/releasing-to-the-snap-store link

That URL now 404s. Point at the canonical documentation.ubuntu.com
locations instead, broken out into the specific pages a maintainer
actually needs:

- Register a snap (to claim the name)
- snapcraft export-login (to generate the SNAPCRAFT_STORE_CREDENTIALS
  secret)
- Publishing how-to index (root index for everything else)

Same fix in the snap-publish.yml header comment.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-02 13:19:10 +01:00