3.3.0
3.3 is primarily a security-hardening release. A defence-in-depth pass tightens the HTTP API entry points, switches random-id generation to a CSPRNG, escapes exported data-* attributes, and flips the shipped Docker deployment defaults so a fresh install no longer boots with implicit credentials or a trusting proxy. Alongside that, the ep_* pad-options passthrough that shipped opt-in in 3.0.0 is now on by default, the in-pad timeslider learns to honour the editor's view settings (authorship colours, font family, line numbers), and a long tail of pad-editor layout, RTL, and URL-encoding fixes lands. The release also carries the root-cause fix for the long-standing Windows backend-test "silent ELIFECYCLE" flake.
Notable enhancements
- Plugin pad options on by default —
settings.enablePluginPadOptionsnow defaults totrue(#7841). The flag that gates theep_*passthrough on pad options (shipped opt-in in 3.0.0, #7698) is flipped to default-on, so plugins such asep_plugin_helpers'padToggle/padSelectride the existing broadcast/persist rail out of the box. This closesep_comments_page#422— stock 3.x deploymentsconsole.warned on every pad load because the helper detectedenablePluginPadOptions === false. Thesettings.json.templateenv-var default is flipped to match, so Docker/supervisor configs without an explicit value get the new behaviour. Existing deployments with an explicit"enablePluginPadOptions": falsekeep that value — no migration needed — and the protocol shape is unchanged for older clients. - Timeslider — honour the editor's view settings (#7899). The in-pad timeslider now respects
showAuthorshipColors,padFontFamily, and line-numbers, bridged from the pad-settings checkboxes into the embedded timeslider iframe so the two views agree.nice-select.tsdispatches a nativechangeevent after the jQuery trigger so theaddEventListener-based bridge inpad_mode.tsfires (jQuery 3.7.1'strigger()does not dispatch native DOM events), and the font-family reset is fixed for jQuery 3 (which ignores anullcss value). The five ad-hoc listener stores inpad_mode.tsare consolidated into onebindOuter()path and the three view-setting bridges into a single data-drivenbridgeView()(refactor only). - Admin settings — explain env-var substitution and surface auth errors (#7819 / #7826). Three env-var-only UX improvements driven by #7819 (a Docker operator saved an
ep_oauthblock in the Raw view and reported it "disappeared", not realisingsettings.jsonon disk is a template, not the effective config): a banner above the editor explaining the template/substitution model (rendered only when the loaded file contains a${VAR}placeholder); a read-only Effective tab exposing the redacted runtime settings the backend already emitted asresolved(also gated on${VAR}); and anadmin_auth_errorevent so a misrouted Traefik+SSO session that isn't admin gets a clear toast instead of a silent "save did nothing". A reconnect-loop guard suppresses the SPA's auto-reconnect once an auth error has been received. No behaviour change for installs without${VAR}placeholders.
Security hardening
A defence-in-depth pass across the API, token, export, and deployment surfaces:
- HTTP API request handling, random IDs, and plugin loading (#7906).
pad_utils.randomStringnow generates random IDs viacrypto.getRandomValues(CSPRNG) instead ofMath.random.OAuth2Providercompares passwords withcrypto.timingSafeEqualon the raw UTF-8 bytes (resolving the CodeQL "insufficient computational effort" alert) behind a uniform failure delay, and looks users up via own-property access only.API.appendChatMessagethrowspadID does not existrather than creating the pad, consistent with the other content API methods. The/api/2REST router forwards only theauthorizationheader (not the full request header set) and falls back to it whenever the field is falsy, matching theopenapi.tshandler so both routers authenticate identically.LinkInstallervalidates plugin dependency names before building filesystem paths from them, and the admin file server returns a generic error while logging details server-side. - Escape exported
data-*attributes; warn on default/placeholder credentials (#7905).ExportHtmlnow escapes the name and value of attributes emitted by theexportHtmlAdditionalTagsWithDatahook, consistent with the URL/text escaping already applied to exported HTML.Settingslogs a warning (error level underNODE_ENV=production) when an account uses a default/placeholder password from the shipped config, and the check is extended to coversso.clients[].client_secretso enabling SSO without settingADMIN_SECRET/USER_SECRETis flagged the same way. - Docker deployment defaults — require explicit credentials, default
TRUST_PROXYoff (#7907). The shippeddocker-composenow requiresADMIN_PASSWORDand the database password to be provided explicitly (no implicit fallback) and defaultsTRUST_PROXYtofalse. Operators relying on the previous implicit defaults must now set these values explicitly.
Notable fixes
- History mode — lay the timeslider iframe in the editor's flex slot (#7903). In-pad history mode positioned
#history-frame-mountas aninset:0absolute overlay over#editorcontainerbox, which took the iframe out of flow and hid any in-flow side panel (e.g.ep_webrtc's#rtcboxvideo column) beneath it — so history mode and live mode disagreed. The iframe now occupies the same in-flow flex slot the live editor uses, and a latent specificity bug (thebody.history-mode #editorcontainer { display: none }hide rule was outranked by the two-id layout rule, so the live editor was only ever painted over) is fixed by giving the hide rule matching specificity. Adds apadmode.spec.tsregression test. - Pad editor — restore URL wrapping (#7894 / #7896). Long URLs in the pad editor overflowed instead of wrapping because the global
a { white-space: nowrap }rule overrode the wrapping properties on#innerdocbody. Explicitwhite-space/word-wrap/overflow-wrapon#innerdocbody arestores wrapping inside the editor while preserving no-wrap for links elsewhere in the UI. - RTL content option no longer flips the whole page (#7900 / #7901). The per-pad RTL content option (
rtlIsTrue) wrote the direction to the top-leveldocument.documentElement, flipping the entire page — toolbar and chrome included. The content direction is now applied to the inner editor document (targetDoc.documentElement); page direction stays owned by the UI language (l10n.ts). Adds a frontend test asserting the inner editor flips while the top-level<html>dir is unchanged. - Pad-wide view settings apply to the creator's own view (#7900 / #7902). Because a creator is never "enforced upon themselves", a stale personal view-override cookie (e.g.
rtlIsTrue=falsefrom an earlier toggle) silently masked the pad-wide value they later set, so the control appeared to do nothing on their own screen. Changing a pad-wide view option now syncs the creator's personal pref to the chosen value; the precedence model is unchanged (the creator can still override afterwards via "My view"). - URL view-option params lost to a
padeditor.initrace (#7840 / #7843).?showLineNumbers=falseand?useMonospaceFont=truewere silently clobbered shortly after load — the same race #7464 fixed for?rtl=false, but the neighbouringshowLineNumbers/noColors/useMonospaceFontGlobalblocks were left at the synchronous-tail site. The fix is generalised to all three (moved intopostAceInit). Mostly observable in cross-context iframe embeds that start with noprefscookie. Addsurl_view_options.spec.ts. - Default welcome text attributed to the system author (#7885 / #7887). Auto-generated default pad content (
settings.defaultPadText/padDefaultContenthook) carried the creating user'sauthorattribute and rendered in their authorship colour, even though they never wrote it. The welcome text'sauthorattribute is nowPad.SYSTEM_AUTHOR_ID, while revision 0'smeta.authorstays the real creator so ownership (pad-wide settings gate, deletion token) is preserved. Explicitly provided text (e.g. HTTP APIcreatePadwith text + author) keeps the real author. - URL-encode pad names in the admin 'Open' button and recent pads (#7865 / #7895). Pad names are
encodeURIComponent-d in the adminPadPageOpen href and the colibris recent-pads href, anddecodeURIComponent-d when read back from the URL pathname; legacy URL-encoded recent-pads names are normalised before re-encoding to prevent double-encoding (%2F→%252F). The admin Openwindow.opengainsnoopener,noreferrer. - OIDC — fix broken
OIDCAdapterflows (#7837). Repairs the adapter flows and widens the storage type to includestringfor theuserCodeindex; adds regression tests. - Accessibility — dialog titles/descriptions and a missing l10n key (#7835 / #7836). Adds the
index.codekey referenced byindex.htmlbut never defined (which produced a "Couldn't find translation key" console error on the landing page), and gives every admin@radix-ui/react-dialogDialog.ContentaDialog.TitleandDialog.Description(visually hidden where there's no visible heading), silencing Radix's a11y warnings. A new backend spec fails CI if anydata-l10n-idinsrc/templates/*.htmlis missing fromen.json. - Offline/air-gapped Docker boot — stop pnpm self-provisioning a pinned version (issue #7911). The official image installs pnpm directly (corepack was dropped for Node 25+). Because the image's pnpm intentionally lags the
packageManagerpin inpackage.json(pnpm 11.1.x enforces a minimum-release-age policy the frozen-lockfile build can't satisfy), pnpm treated every call — including the informationalpnpm --versionprobe Etherpad runs at startup — as a request to download the pinned build. Behind a firewall that download failed (Failed to get pnpm version: … Command exited with code 1), breaking startup. The Dockerfile now setspnpm_config_pm_on_fail=ignore, and the startup probe plus the updater's pnpm-on-PATH checks run with the same flag, so pnpm uses the installed version instead of reaching for the network (without changing which pnpm runs the build-time install). A backend spec fails CI if that guard is dropped while a version gap exists. - Firefox authorship colours — tag early keystrokes with the right author (#7910). The inner editor's
thisAuthorstarts empty and is only populated when collab_client's queuedsetProperty('userAuthor', userId)reaches the iframe (applied asynchronously viapendingInit). Under Firefox timing the first keystrokes could beat it, so freshly typed text — and early line-attribute changes (lists, headings, alignment) — were taggedauthor='', which canonicalises to an unattributed insert that the server's pad-corruption guard rejects, dropping the whole change and losing authorship (the intermittentclear_authorship_colorflake, where undo couldn't restore the author colour). AgetLocalAuthor()helper now falls back toclientVars.userId(the same id, available synchronously) wheneverthisAuthoris still empty, applied at the text-insert sites and to seeddocumentAttributeManager.author; the intentional clear-authorship path and the server-side guard are unchanged. - Dark mode — fix the white address bar and the light-flash on load (#7909, issue #7606). Dark-mode users still saw a white mobile address bar above the dark toolbar, and the whole page flashed light before going dark. Both came from rendering the light state server-side and switching to dark only after the JS bundle ran: iOS Safari reads
theme-colorat parse time and doesn't reliably repaint on a later JS mutation, and the page painted light before the bundle applied the dark skin classes. The server now emits aprefers-color-scheme-scopedtheme-colorpair so the address bar is correct at first paint, plus a small blocking<head>script that applies the dark skin classes before the stylesheet paints. Both are gated onenableDarkMode(default on) and the colibris skin;pad.tsstill runs on init to wire up the#options-darkmodetoggle (which now updates everytheme-colormeta) and theme the editor iframes. Applies to the pad and timeslider views.
Internal / contributor-facing
- Root-caused and fixed the Windows backend-test "silent ELIFECYCLE" flake (#7866). The ~22% Windows flake — rotating across random spec files, no mocha summary, no JS trace — was diagnosed from a full-memory dump as two distinct causes. (1) A timing-fragile test abandoned by mocha keeps running and later throws an orphan unhandled rejection;
server.ts's process-globaluncaughtException/unhandledRejectionhandlers (correct for a real Etherpad process) escalated that into a cleanprocess.exit. They are now gated behindrequire.main === module, and the backend-test bootstraps (common.ts,diagnostics.ts) log orphan rejections instead of rethrowing. (2) A stack-buffer overrun in Node 24.x's bundled libuv Windows TCP-connect path (uv__tcp_connect) corrupts memory under the suite's localhost-connection churn; CI pins the Windows backend job to Node 24.16.0 (libuv 1.52.1, the bisected fix), referencing upstreamnodejs/node#63620. Linux stays on Node 24 LTS. - Removed the now-unneeded ELIFECYCLE diagnostic scaffolding (#7846 / #7838 / #7842 / #7868). The OS-level sidecar watcher, the diagnostics heartbeat/running-test pointer, and the mid-test snapshot — added to chase the flake above — are removed now that the cause is known.
- Docs — document the Docker
settings.jsonwritable-layer and env-var-vs-file semantics (#7819 / #7827). Two operator-facing gaps surfaced by #7819: that the on-disksettings.jsonis a template (env substitution happens in memory at load time), and that the default compose putssettings.jsonin the container's writable layer with no host mount, so admin edits are lost ondown/pull/watchtower but survive a plainrestart. Adds prose + a recreate-vs-restart table todoc/docker.mdand a commented-out opt-in bind mount to the compose files. - Docs refresh for 3.2.0 (#7888), dropped three redundant top-level files (#7839), dropped a fragile viewport assertion in the enter test (#7845), and a backend-test fix-up.
Dependencies
- Two major bumps:
redis5.12.1 → 6.0.0 (#7869) andejs5.0.2 → 6.0.1 (#7860). ueberdb26.1.2 → 6.1.8,mssql12.5.3 → 12.5.5,nodemailer8.0.7 → 8.0.10,mysql23.22.3 → 3.22.5 (#7915),undici8.3.0 → 8.4.1 (#7914),pdfkit0.18.0 → 0.19.0 (#7916),oidc-provider9.8.3 → 9.8.4,@elastic/elasticsearch9.4.1 → 9.4.2,lru-cache11.5.0 → 11.5.1,rate-limiter-flexible11.1.0 → 11.1.1,semver7.8.1 → 7.8.2,js-cookie3.0.7 → 3.0.8,tsx4.22.3 → 4.22.4,@radix-ui/react-switch1.2.6 → 1.3.0 (#7913),@tanstack/react-query5.100.11 → 5.101.0 (+ devtools), plusi18next,react-router-dom, and several dev-dependency group bumps (#7912).
Localisation
- Multiple updates from translatewiki.net.