* docs: design spec for issue #7570 (ueberdb2 driver bundling)
Spec for the upstream ueberDB fix (move 10 drivers back from optional
peer deps to dependencies) plus downstream etherpad-lite safety net
(explicit driver list + build-test-db-drivers CI job covering all 10
via presence check and MySQL+Postgres smoke tests).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* docs: implementation plan for issue #7570 ueberdb2 driver bundling
Covers upstream ueberDB PR (move drivers from optional peer deps back
to dependencies, publish 5.0.46) and downstream etherpad-lite PR
(bump ueberdb2, defensive driver list, build-test-db-drivers CI job
with presence + MySQL + Postgres stages gating publish).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(#7570): bundle DB drivers, add regression CI
- Bump ueberdb2 to ^5.0.47 (upstream ueberDB PR #939 re-bundles drivers
as real dependencies instead of optional peer deps, fixing the class
of Docker-prod "Cannot find module" failures).
- Declare all 10 ueberdb2 DB drivers as direct src dependencies as a
defensive safety net against a future upstream drift.
- Add build-test-db-drivers CI job that blocks the publish job:
* all-10-drivers presence check in the built prod image
* end-to-end MySQL smoke (reproduces the #7570 repro)
* end-to-end Postgres smoke
Any stage failure blocks Docker Hub / GHCR publish.
Supersedes #7571.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(ci): run driver presence test from src/ so node_modules resolves
The presence test ran node from the default cwd (/opt/etherpad-lite),
but the drivers are installed under /opt/etherpad-lite/src/node_modules
by the monorepo workspace. Adding `-w /opt/etherpad-lite/src` makes
Node resolve modules from src/node_modules where pnpm places them.
Matches how the production container itself runs: `pnpm run prod` is
invoked from src/ (cross-env + node --require tsx/cjs node/server.ts).
---------
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* ci: publish Docker images to GHCR alongside Docker Hub
Adds ghcr.io/ether/etherpad as a second publish target on release tags,
reusing the existing docker/metadata-action step so the same SemVer tags
(e.g. 2.6.1, 2.6, 2, latest) are pushed to both registries.
Motivation: downstream consumers (Helm charts in particular) hit Docker
Hub anonymous pull rate limits. GHCR has no such limits and the
workflow already runs with GITHUB_TOKEN, so this is additive with no
new secrets required.
Docker Hub remains the primary/canonical source; GHCR is a mirror.
Note: this only affects future release tags. The 2.6.1 tag already on
Docker Hub will need to be mirrored separately (e.g. via skopeo) if
downstream needs it on GHCR before the next release.
* address qodo review: scope packages:write to publish job, document GHCR
Two fixes from the qodo code review on #7569:
1. Overprivileged PR token (security). The original change set
'packages: write' at workflow level, which meant pull_request runs
(whose Test step executes PR-controlled code) also inherited push
access to GHCR. Splits the workflow into two jobs:
- build-test: runs on pull_request and push with contents:read
only. Does the single-arch load+test as before.
- publish: needs build-test, runs only on push with
packages:write. Does the multi-arch build-and-push, Docker Hub
description update, and ether-charts bump.
Docker Hub login is also now gated by job-level 'if' (same effect
as the previous step-level 'if').
2. Docs miss GHCR option. Updates doc/docker.md and README.md to
document the GHCR mirror alongside Docker Hub with equivalent pull
examples, so downstream users discovering via docs can choose the
mirror to avoid Docker Hub rate limits.
Adds an explicit `permissions: contents: read` block to update-plugins.yml.
Cross-repo work (cloning ether/ep_* repos, pushing updates, merging
Dependabot PRs) is authenticated via secrets.PLUGINS_PAT, so the default
GITHUB_TOKEN only needs read access for actions/checkout.
Addresses CodeQL code-scanning alert #115 ("Workflow does not contain
permissions"). Matches the pattern already used by the other workflows
under .github/workflows/.
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds a pnpm.overrides block to force-upgrade vulnerable transitive
dependencies to their patched versions. All 33 open Dependabot alerts on
ether/etherpad are against pnpm-lock.yaml; none of these packages are
direct dependencies of the workspace.
Bumps (vulnerable range → pinned):
- basic-ftp ≤5.2.2 → ≥5.3.0 (GHSA-5rq4-664w-9x2c,
GHSA-6v7q-wjvx-w8wg,
GHSA-rp42-5vxx-qpwr)
- brace-expansion <2.0.3 → ≥2.0.3 (GHSA-f886-m6hf-6m8v)
- diff <8.0.3 → ≥8.0.3 (GHSA-73rr-hh4g-fpgx)
- flatted <3.4.2 → ≥3.4.2 (GHSA-25h7-pfq9-p65f,
GHSA-rf6f-7fwh-wjgh)
- follow-redirects ≤1.15.11 → ≥1.16.0 (GHSA-r4q5-vmmm-2653)
- glob (10.x CLI) <10.5.0 → ≥10.5.0 (GHSA-5j98-mcp5-4vw2)
- js-yaml <4.1.1 → ≥4.1.1 (GHSA-mh29-5h37-fv8m)
- lodash ≤4.17.23 → ≥4.18.0 (GHSA-f23m-r3pf-42rh,
GHSA-r5fr-rjxr-66jc)
- minimatch (9.x) <9.0.7 → ≥9.0.7 (GHSA-23c5-xmqv-rm74,
GHSA-3ppc-4f35-3m26,
GHSA-7r86-cg39-jmmj)
- path-to-regexp (8.x) <8.4.0 → ≥8.4.0 (GHSA-27v5-c462-wpq7,
GHSA-j3q9-mxjg-w52f)
- picomatch (4.x) <4.0.4 → ≥4.0.4 (GHSA-3v7f-55p6-f55p,
GHSA-c2c7-rcm5-vvqj)
- qs <6.14.2 → ≥6.14.2 (GHSA-6rw7-vpxm-498p,
GHSA-w7fw-mjwx-w883)
- serialize-javascript ≤7.0.2 → ≥7.0.5 (GHSA-5c6j-r48x-rmvq,
GHSA-qj8w-gfj5-8c6v)
- socket.io-parser <4.2.6 → ≥4.2.6 (GHSA-677m-j7p3-52f9)
- tar <7.5.11 → ≥7.5.11 (GHSA-8qq5-rm4j-mr97,
GHSA-34x7-hfp2-rc4v,
GHSA-r6q2-hw4h-h46w,
GHSA-83g3-92jg-28cx,
GHSA-qffp-2rhf-9h96,
GHSA-9ppj-qmqm-q256)
- vite (non-aliased) <7.3.2 → ≥7.3.2 (GHSA-p9ff-h696-f583,
GHSA-v2wj-q39q-566r,
GHSA-4w7w-66w2-5vf9)
Scoped overrides are used where the vulnerable range is a specific major
line — e.g. `minimatch@>=9.0.0 <9.0.7` — so that 3.x/10.x lines resolving
via unrelated dependency chains are not disturbed. Otherwise the override
targets the bare package name.
Note: admin/ui/doc packages alias `vite` to `rolldown-vite@7.2.10`; those
are a separate package on npm and the vite CVEs do not apply to them.
- `pnpm install` succeeds
- `pnpm run ts-check` clean
- No source code changes; `tar` and `glob` are not directly imported by
etherpad-lite sources, so the major-version bumps (tar 6→7, glob 10→13)
affect only transitive consumers that already declare compatibility.
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
#7421 fixed the ESM/CJS interop bug where plugins using
require('ep_etherpad-lite/node/utils/Settings') got an object whose
.toolbar (and every other top-level field) was undefined, crashing
ep_font_color/ep_font_size/ep_plugin_helpers with "Cannot read
properties of undefined (reading 'indexOf')" during pad.html rendering.
That fix landed without a regression test.
Pin the contract: top-level settings fields must be reachable via a
CJS require(), the toolbar must keep its {left, right, timeslider}
shape, and setters on the shim must propagate to the underlying
settings object so reloadSettings() is visible to plugins.
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat: add timeslider line numbers
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* perf: coalesce timeslider line-number updates
Addresses Qodo review: updateLineNumbers() was called synchronously
from applyChangeset() on every changeset, forcing full-document layout
reads/writes during timeslider scrubbing/playback. scheduleLineNumberUpdate()
also queued a fresh double-rAF pair for every resize tick. Add a pending
flag so only one rAF pair is in flight, and route applyChangeset() through
the scheduler.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix: warn when a pending edit is not accepted
Show a gritter warning only when the pad disconnects while a local commit is still awaiting acceptance, leaving normal editing UI unchanged.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* test: cover unaccepted-commit warning path
Addresses Qodo review: adds regression coverage for the two contract
changes this PR introduces — acceptCommit() must clear the pending
marker so hasUnacceptedCommit() returns false after a server ACK, and
the disconnect handler must surface the unsaved-edit gritter when a
commit is still pending.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix: allow undo of clear authorship colors without disconnect (#2802)
When a user clears authorship colors and then undoes, the undo changeset
re-applies author attributes for all authors who contributed text. The
server was rejecting this because it treated any changeset containing
another author's ID as impersonation, disconnecting the user.
The fix distinguishes between:
- '+' ops (new text): still reject if attributed to another author
- '=' ops (attribute changes on existing text): allow restoring other
authors' attributes, which is needed for undo of clear authorship
Also removes the client-side workaround in undomodule.ts that prevented
clear authorship from being undone at all, and adds backend + frontend
tests covering the multi-author undo scenario.
Fixes: https://github.com/ether/etherpad-lite/issues/2802
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: use robust Playwright assertions in authorship undo tests
- Use toHaveAttribute with regex instead of raw getAttribute + toContain
- Check div/span attributes within pad body instead of broad selectors
- Use Playwright auto-retry (expect with timeout) instead of toHaveCount(0)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: handle confirm dialog and sync timing in Playwright tests
- Add page.on('dialog') handler to accept the confirm dialog triggered
by clearAuthorship when no text is selected (clears whole pad)
- Use auto-retrying toHaveAttribute assertions instead of raw getAttribute
- Increase cross-user sync timeouts to 15s for CI reliability
- Add retries: 2 to multi-user test for CI flakiness
- Scope assertions to pad body spans instead of broad selectors
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: use persistent socket listeners to avoid missing messages in CI
Replace sequential waitForSocketEvent loops with single persistent
listeners that filter messages inline. This prevents race conditions
where messages arrive between off/on listener cycles, causing timeouts
on slower CI runners.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: reject - ops with foreign author to prevent pool injection
The '-' op attribs are discarded from the document but still get added
to the pad's attribute pool by moveOpsToNewPool. Without this check, an
attacker could inject a fabricated author ID into the pool via a '-' op,
then use a '=' op to attribute text to that fabricated author (bypassing
the pool existence check).
Now all non-'=' ops (+, -) with foreign author IDs are rejected.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* test: use not.toHaveClass for cleared authorship spans
Addresses Qodo review: linestylefilter skips attribs with empty values,
so a span with author='' has no class attribute at all. The previous
negative-lookahead regex on the class attribute failed against a null
attribute and was flaky in CI. Switch to not.toHaveClass(/author-/),
which also passes when the attribute is missing.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Add a core timeslider playback speed setting with an original-speed default, a realtime mode that uses revision timestamps, and frontend coverage for the new behavior.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* 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 f336f24d. Follow the project convention: add the
new locale key to en.json only and let translations catch up via the
translation system, rather than putting inline fallback in the template.
* i18n: leave non-English locale files untouched
The PR had removed pad.importExport.abiword.innerHTML from ~82 locale
files alongside its removal from en.json. The replacement message uses
a new key (pad.importExport.noConverter.innerHTML) in en.json only, so
churning every localisation file for a key that is no longer referenced
produces useless translation diffs. Restore every non-en locale file to
its pre-PR state.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: JohnMcLear <220864+JohnMcLear@users.noreply.github.com>
* docs(openapi): document apikey auth in openapi.json (#7532)
The API accepts the key via ?apikey=, ?api_key=, or the apikey header, but
only ?apikey= was advertised in /api-docs.json. /api/{version}/openapi.json
was worse: it hardcoded an OAuth2 scheme even when Etherpad was started in
apikey auth mode.
Switch both generators on settings.authenticationMethod and publish apiKey
schemes for the query (apikey, api_key) and header (apikey) variants. The
openapi.ts definition is now regenerated per request so runtime settings
are reflected.
The raw authorization: <key> header still works in code but is deliberately
not documented — pinning it in the spec would ossify a quirk.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* refactor(openapi): add apiKeyAlias/apiKeyHeader conditionally in RestAPI.ts
In SSO mode, apiKeyAlias and apiKeyHeader were always present in
securitySchemes even though they're only relevant when
authenticationMethod is 'apikey'. Mirror the pattern used for the sso
scheme: add these two schemes dynamically inside the apikey branch, and
mark them optional in the TypeScript type annotation.
Agent-Logs-Url: https://github.com/ether/etherpad/sessions/1d440432-7389-462e-9aac-9a3c027640e8
Co-authored-by: JohnMcLear <220864+JohnMcLear@users.noreply.github.com>
---------
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: JohnMcLear <220864+JohnMcLear@users.noreply.github.com>
* 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>
pnpm's default \`auto\` package-import-method eventually falls through
to \`copyfile\`, which uses \`copy_file_range\`. That syscall fails on
ZFS with \`EAGAIN: resource temporarily unavailable\` (see
https://github.com/pnpm/pnpm/issues/7024), so \`docker compose build\`
aborts inside the \`RUN pnpm install\` step on any host with a ZFS
root. Operators had to hand-patch every pnpm invocation in the
Dockerfile and install scripts.
Force \`package-import-method=hardlink\` in \`.npmrc\` so all pnpm
invocations (Docker build, \`bin/installDeps.sh\`,
\`bin/installLocalPlugins.sh\`, \`bin/updatePlugins.sh\`) pick up the
setting automatically. Hardlinks are fast, save disk, and work on
every filesystem Etherpad supports.
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- Rewrite title + About to lead with what Etherpad is for
(authorship, sovereignty, malleability) before features
- Rewrite Project Status to make the maintainer ask specific and
to situate the project's 16-year track record
- Add new "Who uses Etherpad" section with categorical adopter
profiles so institutional evaluators have proof points on-page
No code or behaviour changes.
Co-authored-by: Claude Opus 4.6 (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>
* fix: flush pending changesets immediately after reconnect
After reconnecting, setUpSocket() did not call handleUserChanges(),
so any edits made while disconnected were not sent to the server until
the user made another change. This caused divergent pad state between
users.
Now calls handleUserChanges() after reconnect to immediately transmit
any pending local changesets.
Fixes#5108
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: defer handleUserChanges on connect to avoid editor init race
Calling handleUserChanges() synchronously in setUpSocket() caused
"Cannot read properties of null (reading 'changeset')" because the
editor isn't fully initialized on first connect. Deferred with
setTimeout(500ms) to allow initialization to complete.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* test: add test for pending changeset flush after reconnect
Verifies that edits made while disconnected are transmitted to the
server immediately upon reconnection.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: flush pending changesets on actual reconnect, not just initial connect
setUpSocket() only runs during initialization. Move handleUserChanges()
to the reconnect code path so pending edits are flushed when the
connection is re-established.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: flush pending changes when isPendingRevision clears after reconnect
The existing fix in setChannelState('CONNECTED') calls handleUserChanges(),
but at that point isPendingRevision is still true so changes are blocked.
The real trigger must be in setIsPendingRevision(): when it transitions
from true to false (after all CLIENT_RECONNECT messages are processed),
call handleUserChanges() to flush any locally-queued edits.
Also adds a targeted regression test that simulates the exact reconnect
state transitions and verifies pending edits reach the server.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* test: rewrite reconnect flush test for reliability
Replaced the fragile offline/online simulation with a direct test that
uses separate browser contexts. Simplified to a single test that
exercises the exact setIsPendingRevision(false) -> handleUserChanges()
codepath and verifies the flushed text is visible from another client.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* test: remove Playwright reconnect test (not feasible)
The reconnect test requires access to pad.collabClient internals which
are not exposed on window in the browser context. Playwright cannot
call setStateIdle/setIsPendingRevision/setChannelState. The backend
tests adequately cover the code fix.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: don't flush in setChannelState to avoid editor init race
Calling handleUserChanges() in setChannelState('CONNECTED') fires
synchronously on first connect before the editor is fully initialized,
breaking chat/user_name tests. The setIsPendingRevision(false) trigger
is sufficient for the reconnect path, and the existing
setTimeout(handleUserChanges, 500) in setUpSocket() already handles
the initial connect.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: remove setTimeout flush in setUpSocket — rely on setIsPendingRevision trigger
The setTimeout(handleUserChanges, 500) in setUpSocket was a timing hack
that:
- Only fires on initial connect (setUpSocket is called once at the end
of getCollabClient; reconnects go through pad.ts:248 which calls
setChannelState('CONNECTED') directly, bypassing setUpSocket).
- Doesn't actually fix issue #5108 (the reconnect-flush bug). That's
fixed deterministically by the wasPending && !value trigger in
setIsPendingRevision, which fires whenever the server's CLIENT_RECONNECT
message lands (both for noChanges and after replaying revisions).
- Introduced a 500ms race window on initial pad load.
The reconnect path now relies entirely on the deterministic event-based
trigger (setIsPendingRevision), with no timing assumptions.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Plugin CI is still failing on ERR_PNPM_IGNORED_BUILDS even with the
build-script policy declared in both pnpm-workspace.yaml (#7523) and
package.json (#7525). pnpm's strict-dep-builds defaults to true in 10+,
so any transitive dep with an unrecognized postinstall fails the build.
For etherpad-lite — and especially for downstream plugin repos that
pull this codebase as their core install — that's a footgun: the moment
some new transitive ships a postinstall, every plugin's CI explodes.
Set strictDepBuilds: false in pnpm-workspace.yaml AND
strict-dep-builds=false in .npmrc as a defensive layer, so unknown
postinstalls become a warning instead of a hard failure. The
allow/ignore lists still control what actually runs.
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Some pnpm versions don't read onlyBuiltDependencies / ignoredBuiltDependencies
from pnpm-workspace.yaml — leaving CI on plugin repos to fail with
ERR_PNPM_IGNORED_BUILDS even after #7523 added the workspace.yaml entries.
Mirror the same configuration into package.json's "pnpm" field, which is
the older (and more widely supported) location. The two files are kept in
sync; whichever pnpm version reads the values picks them up from one or
the other.
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The test was failing intermittently in CI with "Element is not attached
to the DOM" at selectText(). The long text typed by writeToPad triggers
DOM re-renders in Etherpad, and on slower CI runners the div locator
could detach between resolve and action.
Switch to keyboard-based selection (Ctrl+A) via the existing
selectAllText helper, which doesn't depend on a specific DOM element
staying attached. Verified stable across 10 repeated runs on chromium.
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: explicitly ignore scarf build scripts to fix bin install CI
My earlier commit removed @scarf/scarf from onlyBuiltDependencies, but
pnpm 10.7+ requires every encountered build script to be either allowed
(onlyBuiltDependencies) or explicitly ignored (ignoredBuiltDependencies),
otherwise it errors with ERR_PNPM_IGNORED_BUILDS.
The Update Plugins workflow runs pnpm install inside ./bin, which pulls
in ep_etherpad-lite -> swagger-ui-dist -> @scarf/scarf as a transitive
dep. Add scarf to ignoredBuiltDependencies so pnpm silently skips its
install-time telemetry script instead of failing the install.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix(tests): serialize admin-spec tests to avoid shared-state races
Admin tests mutate global server state (install/uninstall plugins,
save settings, restart etherpad). Running them in parallel — both
across browsers (chromium + firefox) and with playwright's
fullyParallel mode — caused intermittent failures where one test's
install would leak into another test's assertions (e.g. seeing 3
installed plugins when expecting 2, or "ep_font_colormain" in the
hooks list when expecting only core hooks).
Two changes:
1. Drop firefox from test-admin — admin UI is browser-agnostic and
running two browsers concurrently was the main race source
2. Add test.describe.configure({ mode: 'serial' }) to each admin
spec file as a safety net in case the project list or worker
count changes in the future
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix(tests): use ep_set_title_on_pad for admin plugin install test
ep_font_color depends on ep_plugin_helpers, so installing it via the
admin UI actually installs two plugins (ep_font_color + ep_plugin_helpers),
making the installed plugins table show 3 rows (including ep_etherpad-lite)
instead of the 2 the test asserts.
Switch to ep_set_title_on_pad which has no transitive deps, so install
produces exactly one new row as the test expects.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
pnpm 10.7+ blocks dependency postinstall scripts unless explicitly
approved via onlyBuiltDependencies. Only esbuild genuinely needs its
postinstall (to download platform-specific native binaries).
Remove @scarf/scarf (install-time telemetry from swagger-ui-dist) and
@swc/core (not in the dependency tree) from the approved list.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Bump ueberdb2 to ^5.0.42 — adds production dependencies for db drivers
5.0.42 moves core database drivers (dirty-ts, rusty-store-kv) to
production dependencies so they're installed in production/Docker.
Optional drivers (cassandra, mongodb, etc.) are now optional
peerDependencies.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Update to ueberdb2 ^5.0.43 — fixes Node engine requirement
5.0.43 relaxes the Node engine from >=22.22.0 to >=18.0.0,
matching etherpad-lite's supported Node versions.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Add docs for aceRegisterLineAttributes hook
Documents the new hook in both .md and .adoc client-side hook references.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Fix: correct source file path from .js to .ts
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The pnpm-workspace.yaml references admin, doc, and ui packages that
aren't copied into the production Docker image. This caused pnpm
install warnings and incomplete dependency resolution, which broke
ueberdb2's modular build (missing bin symlinks, broken module paths).
Fix: overwrite pnpm-workspace.yaml in the production stage with a
minimal version that only lists the packages present in the image
(src and bin).
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
5.0.41 includes the rolldown runtime fix (ether/ueberDB#925) needed
for the lazy-loaded database drivers to work at runtime.
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
ueberdb2 5.0.40 lazy-loads database drivers so only the configured
backend's dependencies need to be installed. Fixes the Docker production
build crash: "Cannot find module 'cassandra-driver'"
See ether/ueberDB#924
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
When pressing Enter in a line with attributes like heading or align,
the attributes were lost on the new line. Lists had special-case code
in doReturnKey() but all other line attributes were ignored.
This adds a new client hook aceRegisterLineAttributes that plugins can
use to declare which attributes should be preserved when a line is
split by Enter:
- Enter at middle/end of line: attribute is copied to the new line
- Enter at start of line (col 0): attribute moves down with the text,
the now-empty line above gets the attribute removed
Plugins register by returning attribute names from the hook:
exports.aceRegisterLineAttributes = () => ['heading'];
Fixesether/ep_headings2#7
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The plugin publish workflow ran `git push --follow-tags` after `pnpm
version patch`. `--follow-tags` is non-atomic per ref: if a concurrent
publish run won the race, the branch fast-forward would be rejected
but the tag push would still land — leaving a dangling `vN+1` tag with
no matching version-bump commit on the branch. Every subsequent push
would then fail forever with `npm error fatal: tag 'vN+1' already
exists`, because `pnpm version patch` would re-derive the same tag
name from the unchanged `package.json`.
On 2026-04-08, a single churn day (badge fixes + Dependabot merges
firing back-to-back) put ~46 plugins into this state simultaneously.
Recovery required hand-bumping `package.json` past the dangling tag
on every affected repo, twice (a second wave appeared after the first
sweep finished, racing the next wave of publishes).
Fix: use `git push --atomic origin <branch> <tag>` so the branch
update and the tag update succeed or fail as a single server-side
transaction. A rejected branch push now also rejects the tag push,
the run aborts cleanly, and the next workflow tick can retry against
the up-to-date refs without leaving any orphaned tags.
Also derive the new tag name from `package.json` after the bump
(rather than parsing pnpm version's stdout, which has historically
varied) and pass it explicitly into the push.
Adds a backend regression test that asserts the workflow file uses
`--atomic`, does not contain a literal `git push --follow-tags`
command (ignoring the historical comment), and includes both the
branch ref and the freshly-bumped tag in the atomic push. The test
gates against accidental reverts.
This file is the source of truth that `bin/plugins/checkPlugin.ts`
propagates into every `ether/ep_*` plugin's `.github/workflows/`, so
the next `update-plugins` cron tick will roll the fix out across all
plugins automatically.
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: setup-trusted-publishers.sh works with real npm trust CLI
Two issues found when running the script for the first time after #7490:
1. `npm trust github --file` wants ONLY the workflow filename basename
(e.g. `test-and-release.yml`), not the full
`.github/workflows/test-and-release.yml` path. npm errors out with
"GitHub Actions workflow must be just a file not a path" otherwise.
Constants updated.
2. `npm trust github` requires 2FA on accounts that have it enabled,
and there is no way to disable that requirement. Add a `--otp <code>`
pass-through flag and forward it to every call so a maintainer can
batch-process multiple packages within a single TOTP window.
Documented the limitation in the script header.
Also reword the call site so the npm command line is built without
shell-string round-tripping (passing $CMD through `$( $CMD )` was
unrelated to this bug but was bad practice).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: setup-trusted-publishers.sh recognizes 409 as already-configured
When --skip-existing is set, treat HTTP 409 Conflict from
POST /-/package/<name>/trust as 'already configured' so re-runs of
the bulk script don't fail on packages that were configured in a
previous run.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* test: cover setup-trusted-publishers.sh, harden against set -e, document --otp
Addresses qodo review on #7491:
- Add backend regression test that shims `npm` on PATH and asserts
`--file` is given the workflow basename (never a path), `--otp` is
forwarded to every `npm trust github` call when supplied, and the
loop survives a non-zero exit so `--skip-existing` can absorb 409
Conflict responses from the registry.
- Wrap the `npm trust github` invocation in `set +e` / `set -e`. The
`if configure_one` already shields the function from errexit in
practice, but a future refactor moving the call site out of an `if`
would silently reintroduce the bug — the explicit shim makes intent
obvious and survives such refactors.
- Document `--otp` and the 2FA / TOTP-expiry workflow in
doc/npm-trusted-publishing.md so maintainers don't follow the docs
and hit EOTP.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The daily update-plugins workflow already syncs boilerplate (workflows,
dependabot.yml, etc.) into every ether/ep_* repo via checkPlugin, but it
never closes the loop on the Dependabot PRs that config produces. With
plugin repos having no per-repo auto-merge wiring, those PRs sit green
indefinitely (e.g. ether/ep_loading_message#77).
Add a final step that, after the per-plugin updates run, walks every
ep_* repo and squash-merges any open Dependabot PR whose mergeStateStatus
is CLEAN — i.e. no conflicts, branch up to date, all required checks
green. Anything else (DIRTY, BLOCKED, BEHIND, UNSTABLE, …) is left alone
for a human.
No semver gating: trust each plugin's own CI to fail on a breaking
major bump rather than pre-filtering by version delta.
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>