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>
* fix: capture head revision atomically with atext to prevent mismatched apply
When constructing CLIENT_VARS, pad.atext was captured at one point but
pad.getHeadRevisionNumber() was called later. If concurrent edits
advanced the revision between these two reads, the client received
initialAttributedText from rev N but rev=N+3, causing "mismatched apply"
errors when the next changeset arrived (expecting rev N+3 text).
Now captures headRev at the same time as atext and uses the captured
value consistently in CLIENT_VARS and sessionInfo.
Fixes#4040
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: flush missed revisions after socket joins pad room
During handleClientReady(), the server awaits the clientVars hook before
socket.join(). Any revisions appended during that await window are
broadcast to existing room members but the connecting socket misses them.
Call updatePadClients(pad) after joining to flush any such revisions.
Also adds a regression test that injects a slow clientVars hook and
verifies the connecting client receives catch-up changesets for edits
that occurred during the hook await window.
Fixes#4040
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* test: fix race condition in clientVars hook test
Listen for messages during handshake to avoid missing NEW_CHANGES that
arrive before the explicit waitForSocketEvent listener is attached.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: initialize sessionInfo.time before catch-up updatePadClients
The catch-up updatePadClients() call introduced in this PR could send
NEW_CHANGES with timeDelta=NaN because sessionInfo.time was never set
for new sessions. NaN poisons the client-side broadcast/timeslider
currentTime tracking.
Initialize sessionInfo.time to the timestamp of the snapshot revision
before the catch-up flush, with a fallback to Date.now() if the
revision date is unavailable.
Also strengthens the regression tests:
- Validate that initialAttributedText matches the pad AText at the
EXACT advertised rev (not just the latest pad text), using
pad.getInternalRevisionAText(rev).
- Add a load test that hammers the pad with concurrent edits while
multiple clients connect, asserting CLIENT_VARS consistency under
the exact race condition the fix is targeting.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* test: replace open-ended load loop with bounded mid-handshake edit
The previous load test ran 'while (!stopLoad) await pad.setText(...)'
in the background while the test connected clients. This saturated
ueberDB's write queue and on shutdown the queued writes never drained,
hanging the mocha process for the full 6h GitHub Actions job timeout.
Replace it with a bounded approach: a clientVars hook lands 3 edits
mid-handshake (deterministic, no background loop, no shutdown hang).
Still exercises the exact race the fix targets — an edit advancing
the rev after the atext snapshot but before CLIENT_VARS is sent —
and asserts AText / rev consistency via getInternalRevisionAText.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* test: address remaining Qodo concerns on PR #7480
Addresses Qodo review items 1, 2, 5 from
https://github.com/ether/etherpad-lite/issues/comments/4194702740 :
- Concern 1 (no loadTesting reproduction test): the suite now toggles
settings.loadTest = true in before(), restores in after(). The
middle test also pre-populates the pad with 20 revisions before
connecting so we genuinely exercise a busy/loaded pad rather than a
fresh one.
- Concern 2 (no CLIENT_VARS / NEW_CHANGES delay test): the slow
clientVars hook in the middle test now has explicit setTimeout
delays before AND after the mid-handshake edits, so the race window
between atext snapshot and CLIENT_VARS send is observably wide
rather than relying on async scheduling alone. The test also
collects post-handshake messages and asserts a NEW_CHANGES catch-up
arrives when the pad advanced past the advertised rev.
- Concern 5 (test doesn't validate rev): both rev-consistency tests
use pad.getInternalRevisionAText(advertisedRev) and assert text and
attribs match, not just `pad.text() === clientVars.text`.
Concerns 3 (connect can miss revisions) and 4 (NaN timeDelta) were
already addressed in earlier commits on this branch via the catch-up
updatePadClients() call and the sessionInfo.time initialization.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* feat: migrate npm publish to OIDC trusted publishing (#7401)
Replaces NPM_TOKEN-based publishing with npm Trusted Publishing over
OIDC for both etherpad-lite core and the shared plugin publish
template. Tokens no longer expire every 90 days; each publish
authenticates via a short-lived OIDC token issued to the GitHub
Actions runner.
Changes:
- bin/plugins/lib/npmpublish.yml: the reusable workflow propagated to
every ether/ep_* plugin via the update-plugins cron. Now bumps Node
to 22, upgrades npm to >=11.5.1, declares id-token: write, drops
NODE_AUTH_TOKEN, and calls `npm publish --provenance --access public`
directly (not via pnpm/gnpm wrappers, which obscure the npm CLI
version requirement).
- bin/plugins/lib/test-and-release.yml: the parent workflow that calls
npmpublish.yml as a reusable workflow. Top-level and release-job
permissions now grant id-token: write so the OIDC token can flow
into the called workflow.
- .github/workflows/releaseEtherpad.yml: core's own publish workflow
for the ep_etherpad package. Same OIDC migration; keeps the gnpm
install + rename steps but switches the final publish to npm.
- doc/npm-trusted-publishing.md: explains how trusted publishing
works, the one-time per-package setup that has to happen on
npmjs.com, requirements (Node 22.14+, npm 11.5.1+, cloud runners),
and common errors.
The next update-plugins cron run will propagate the new template to
every plugin. Once that lands and the trusted publisher is configured
on npmjs.com per package, the NPM_TOKEN secret can be removed.
Closes#7401
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* feat: add bin/setup-trusted-publishers.sh for bulk OIDC config (#7401)
Adds a script that automates the per-package trusted-publisher setup
that previously had to be done by clicking through npmjs.com once for
each of the 80+ ep_* plugins. Uses the new `npm trust github` CLI
(npm >= 11.5.1) so the whole org can be configured in one shot:
npm login
bin/setup-trusted-publishers.sh
The script:
- Discovers every non-archived ether/ep_* repo via `gh repo list`
- Maps ep_etherpad to the etherpad-lite repo / releaseEtherpad.yml,
and every plugin to its same-named repo / test-and-release.yml
- Runs `npm trust github <pkg> --repository <org>/<repo> --file
<workflow> --yes` for each package
- Supports --dry-run, --packages <comma list>, and --skip-existing
- Verifies npm >= 11.5.1 and that the user is logged in before doing
anything destructive
Doc updated to feature the script as the recommended setup path,
with manual web-UI steps kept as a fallback.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: don't bump CI Node version to 22 for OIDC
npm 11.5.1 (the version that ships trusted publishing) actually
requires '^20.17.0 || >=22.9.0', not Node 22.14+. The npm docs
recommend Node 22 but only because that's what bundles a recent
enough npm — installing 'npm@latest' on top of Node 20.17+ works
just as well.
The repo already requires Node >= 20.0.0 in engines.node and the
setup-node@v6 'version: 20' input resolves to the latest 20.x
(currently 20.20+), which satisfies npm 11's range. Revert the CI
publish workflows from node-version: 22 back to 20 so this PR does
not raise the Node bar at all.
Doc updated to explain the actual constraint.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* feat: add one-line installer script (#7466)
Adds bin/installer.sh, a small POSIX shell script that:
- Verifies prerequisites (git, Node.js >= 18)
- Installs pnpm globally if missing (with sudo fallback)
- Clones etherpad-lite (configurable branch / dir)
- Runs `pnpm i` and `pnpm run build:etherpad`
- Optionally starts Etherpad if ETHERPAD_RUN=1
Users can now install Etherpad with a single command:
curl -fsSL https://raw.githubusercontent.com/ether/etherpad-lite/master/bin/installer.sh | sh
README updated to feature the one-liner above the existing
Docker-Compose / manual install instructions.
Closes#7466
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* test: add installer-test workflow + Windows PowerShell installer
- bin/installer.ps1: PowerShell port of installer.sh so the one-liner
also works on Windows via 'irm ... | iex'.
- .github/workflows/installer-test.yml: end-to-end CI that runs each
installer against the PR's own commit (via ETHERPAD_REPO/BRANCH env
vars), verifies clone + node_modules + admin SPA artifacts, and
smoke-tests by starting Etherpad and curling /api. Runs on
ubuntu-latest, macos-latest, and windows-latest. Includes a
shellcheck job for installer.sh.
- README: feature the Windows one-liner alongside the POSIX one.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* test: fix windows smoke test - wrap pnpm in cmd /c
Start-Process can't run pnpm.cmd directly ("not a valid Win32 application").
Wrap it via cmd.exe /c instead, and bump the wait window to 90s for slower
Windows runners. Also dump stderr alongside stdout when the smoke test
fails for easier debugging.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: address Qodo review on installer (#7485)
Two correctness issues caught by Qodo:
1. Node version mismatch: installer required Node >= 18, but the repo's
engines.node is >= 20. Bump REQUIRED_NODE_MAJOR to 20 in both shell
and PowerShell installers, and update the README's quick-install
prerequisite and Requirements section to match.
2. Branch ignored for existing checkouts: when ETHERPAD_DIR already
existed, the script ran 'git pull --ff-only' on whatever branch
happened to be checked out, ignoring ETHERPAD_BRANCH and never
verifying ETHERPAD_REPO. The existing-dir path now:
- validates the remote URL matches ETHERPAD_REPO
- refuses to clobber uncommitted changes (excluding pnpm-lock.yaml,
which pnpm i rewrites during install)
- fetches with --tags --prune
- checks out ETHERPAD_BRANCH as a branch or detaches at it as a tag
- prints the resulting commit short SHA for clarity
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 CJS compatibility block added in fd97532 only defined getters,
making settings properties read-only for plugins using require().
Plugins like ep_webrtc need to mutate settings (e.g. requireAuthentication)
in tests. Add setters so CJS consumers can write properties too.
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: increase max socket.io message size to 10MB for large pastes
The default maxHttpBufferSize of 50KB caused socket.io to drop
connections when pasting >10,000 characters. Increased to 10MB which
safely accommodates large paste operations.
Fixes#4951
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* chore: reduce default maxHttpBufferSize to 1MB
10MB was too generous and creates a DoS vector. 1MB (socket.io's own
default) is sufficient for large pastes while limiting memory abuse.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: prevent race condition in session cleanup timeout
When the cleanup timeout fires, check the in-memory exp.real before
reading from the DB. If touch() extended the expiry (but the old
timeout fires late, e.g. on slow CI), reschedule instead of reading
potentially stale cached data from the DB and destroying the session.
Also increased test expiry times so the "touch after eligible for
refresh" test isn't sensitive to event loop delays on slow machines.
Fixes flaky SessionStore test from #7448.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: dev mode entrypoint paths respect x-proxy-path header
In dev mode, the /watch/* script paths were hard-coded as absolute
paths without considering the x-proxy-path header used for subdirectory
reverse proxy setups. This caused 404s for the script tags when hosting
Etherpad on a subdirectory URL (e.g., /pad).
Now reads the x-proxy-path header from the request and prefixes the
entrypoint path, matching how admin.ts handles proxy paths.
Fixes#7137
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* test: make proxy path tests deterministic in production mode
Tests now verify entrypoint paths and x-proxy-path header handling
in production mode (where tests run) rather than conditionally
asserting only in dev mode.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* security: sanitize x-proxy-path header to prevent XSS
The header value was injected directly into <script src="...">
without sanitization. An attacker who can set request headers could
inject arbitrary HTML/JS. Now only allows path-safe characters.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: very old .etherpad imports could break import due to lack of author metadata, allow this now
* test: add regression tests for old .etherpad import without author
Tests that importing an old .etherpad export (circa 2014) where
revision records lack meta.author succeeds without error, and that
getRevisionAuthor returns '' for such revisions.
Covers the fix for #6785.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: use correct path for connection diagnostics POST
The relative path '../ep/pad/connection-diagnostic-info' resolved
incorrectly in subdirectory setups. Use absolute path from the
application root.
Fixes#4191
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* test: verify connection diagnostics endpoint is reachable
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: numbered list wrapped lines now indent correctly
Changed text-indent to padding-left for ordered list indentation.
text-indent only affects the first line, so wrapped text didn't
align with the numbered content above it.
Fixes#2581
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* test: verify numbered list uses padding-left instead of text-indent
Regression test for #2581. Verifies that ordered list items use
padding-left (which indents all lines including wrapped ones) rather
than text-indent (which only indents the first line).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: sort language dropdown alphabetically by native name
Languages in the settings dropdown were ordered by language code,
making it hard to find specific languages. Now sorted alphabetically
by their native display name.
Fixes#3263
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* test: verify language dropdown is sorted by native name
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: preserve ordered list numbering across unordered list interruptions in export
When ordered lists were interrupted by unordered lists, each new <ol>
segment started at 1 instead of continuing the previous numbering.
Track running counts per indent level and emit start attributes.
Fixes#6471
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: respect explicit start attributes and reset counters per level
- line.start takes priority over counter-based continuation when present
- Counter is seeded from line.start to keep subsequent continuations aligned
- Counters for closed indent levels are cleared when list depth decreases
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: RTL URL parameter rtl=false now correctly disables RTL mode
The rtl parameter callback only handled rtl=true (checkVal was 'true'),
so rtl=false was ignored and the layout stayed in RTL from the cookie.
Now accepts any value and sets rtlIsTrue = (val === 'true'). Also
always applies the RTL setting instead of only when true, so switching
from rtl=true to rtl=false takes effect.
Fixes#5559
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: only override RTL when explicitly set via URL/server config
The unconditional changeViewOption('rtlIsTrue', false) overwrote
cookie-persisted RTL preferences and language-direction defaults.
Track explicit setting with rtlIsExplicit flag so we only override
when the user or server actually specified an rtl value.
Adds regression tests for rtl=true, rtl=false, and cookie persistence.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: move RTL override into postAceInit to fix race condition
The RTL changeViewOption call was racing with padeditor.init() — the
async setViewOptions(initialViewOptions) at the end of init overwrote
the URL-param-based RTL setting. Moving it into postAceInit ensures
padeditor is fully initialized. Also switched tests to use Playwright
auto-retrying assertions for robustness.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: resolve Playwright test failures for RTL URL parameter
Three issues fixed:
- setCheckbox used .attr('checked') instead of .prop('checked'), so the
JS checked property was never set and Playwright saw unchecked state
- html10n localized event overwrote RTL setting from URL params and
cookie preferences; now skips override when either is active
- Server default padOptions.rtl:false was treated as explicit, overwriting
cookie-persisted RTL; added fromUrl flag to distinguish URL from server
All 94 Playwright tests and 740 backend tests pass locally.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: correct readFileSync calls in LinkInstaller to fix plugin installation
pathToFileURL() was incorrectly wrapping paths passed to readFileSync(),
causing ENOENT errors that were silently caught. Using plain paths with
'utf-8' encoding fixes plugin dependency resolution.
Fixes#6811
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* test: add backend tests for LinkInstaller dependency resolution
Covers the readFileSync fix from the plugin installation bug where
pathToFileURL incorrectly wrapped file paths.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: only track dependency in map after successful setup
Previously dependenciesMap.set() ran after the catch block, marking
dependencies as tracked even when linking or package.json reading
failed. This blocked later cleanup via removeSubDependency().
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: use pnpm instead of npm in updatePlugins.sh
The script used npm outdated which doesn't work with pnpm workspaces,
and pnpm install which doesn't update existing packages. Changed to
pnpm outdated and pnpm update respectively.
Fixes#6670
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: scope plugin updates to ep_etherpad-lite and exclude core package
- Use --filter ep_etherpad-lite so pnpm operates on the right workspace
- Exclude ep_etherpad-lite from the plugin list
- Handle pnpm outdated exit codes correctly (returns 1 when outdated)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: handle git submodule in Docker build
When etherpad-lite is checked out as a submodule, .git is a file
(gitlink) instead of a directory, so COPY .git/HEAD fails. The
previous glob-based workaround (HEA[D], ref[s]) does not work with
buildah (containers/buildah#5742).
Copy the whole .git entry instead and remove it in a RUN step when it
is a submodule gitlink file. The .dockerignore already strips heavy
objects so the image size is unaffected for normal checkouts.
Fixes#6663
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* docs: document BUILD_ENV=copy bypass for builds without .git
Adds a comment explaining how to build from source tarballs or other
contexts where .git metadata is unavailable.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
When the cleanup timeout fires, check the in-memory exp.real before
reading from the DB. If touch() extended the expiry (but the old
timeout fires late, e.g. on slow CI), reschedule instead of reading
potentially stale cached data from the DB and destroying the session.
Also increased test expiry times so the "touch after eligible for
refresh" test isn't sensitive to event loop delays on slow machines.
Fixes flaky SessionStore test from #7448.
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: add periodic cleanup of expired/stale sessions from database
SessionStore now runs a periodic cleanup (every hour, plus once on
startup) that removes:
- Sessions with expired cookies (expires date in the past)
- Sessions with no expiry that contain no data beyond the default
cookie (the empty sessions that accumulate indefinitely per #5010)
Without this, sessions accumulated forever in the database because:
1. Sessions with no maxAge never got an expiry date
2. On server restart, in-memory expiration timeouts were lost
3. There was no mechanism to clean up sessions that were never
accessed again
Fixes#5010
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: resolve TypeScript error for sessionStore.startCleanup()
Use a local variable for the SessionStore instance to avoid type
narrowing issues with the module-level Store|null variable.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: address Qodo review — chained timeouts, cleanup tests, docs
- Replace setInterval with chained setTimeout to prevent overlapping
cleanup runs on large databases
- Store and clear startup timeout in shutdown() to prevent leaks
- Add .unref() on all timers so they don't delay process exit
- Fix misleading docstring — cleanup removes empty no-expiry sessions,
not sessions older than STALE_SESSION_MAX_AGE_MS (removed unused const)
- Add 5 regression tests: expired sessions removed, empty sessions
removed, sessions with data preserved, valid sessions preserved,
shutdown cancels timer
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* feat: add cookie.sessionCleanup setting to control session cleanup
Session cleanup is now gated behind cookie.sessionCleanup (default
true). Admins who want to keep stale sessions can set this to false
in settings.json.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
PRs now run a minimal test matrix; full matrix runs on push to develop.
Changes:
- Backend tests: PRs test on Node 24 only (Linux). Windows tests only
run on push to develop. Reduces from 12 to 2 jobs for PRs.
- Upgrade-from-latest-release: PRs test on Node 24 only (1 job vs 3).
- Frontend admin tests: PRs test on Node 24 only (1 job vs 3).
This reduces PR CI from ~25 jobs to ~10, preventing runner exhaustion
when multiple PRs are merged in succession. The full matrix (3 Node
versions × Linux + Windows) still runs on every push to develop.
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>