feat: Backend esm vitest#5
Conversation
Bumps [ueberdb2](https://github.com/ether/ueberDB) from 5.0.34 to 5.0.45. - [Changelog](https://github.com/ether/ueberDB/blob/main/CHANGELOG.md) - [Commits](ether/ueberDB@v5.0.34...v5.0.45) --- updated-dependencies: - dependency-name: ueberdb2 dependency-version: 5.0.45 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.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>
…er#7523) * 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>
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>
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 ether#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>
Plugin CI is still failing on ERR_PNPM_IGNORED_BUILDS even with the build-script policy declared in both pnpm-workspace.yaml (ether#7523) and package.json (ether#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>
* 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 ether#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 ether#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>
Bumps [oidc-provider](https://github.com/panva/node-oidc-provider) from 9.8.0 to 9.8.1. - [Release notes](https://github.com/panva/node-oidc-provider/releases) - [Changelog](https://github.com/panva/node-oidc-provider/blob/main/CHANGELOG.md) - [Commits](panva/node-oidc-provider@v9.8.0...v9.8.1) --- updated-dependencies: - dependency-name: oidc-provider dependency-version: 9.8.1 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
…7531) 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>
) - 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>
…ZFS (ether#7342) (ether#7533) 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 pnpm/pnpm#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>
…r#7536) Bumps the dev-dependencies group with 5 updates: | Package | From | To | | --- | --- | --- | | [@types/express-session](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/express-session) | `1.18.2` | `1.19.0` | | [eslint-config-etherpad](https://github.com/ether/eslint-config-etherpad) | `4.0.4` | `4.0.5` | | [typescript](https://github.com/microsoft/TypeScript) | `6.0.2` | `6.0.3` | | [eslint-plugin-react-hooks](https://github.com/facebook/react/tree/HEAD/packages/eslint-plugin-react-hooks) | `7.0.1` | `7.1.0` | | [react-i18next](https://github.com/i18next/react-i18next) | `17.0.3` | `17.0.4` | Updates `@types/express-session` from 1.18.2 to 1.19.0 - [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases) - [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/express-session) Updates `eslint-config-etherpad` from 4.0.4 to 4.0.5 - [Commits](ether/eslint-config-etherpad@v4.0.4...v4.0.5) Updates `typescript` from 6.0.2 to 6.0.3 - [Release notes](https://github.com/microsoft/TypeScript/releases) - [Commits](microsoft/TypeScript@v6.0.2...v6.0.3) Updates `eslint-plugin-react-hooks` from 7.0.1 to 7.1.0 - [Release notes](https://github.com/facebook/react/releases) - [Changelog](https://github.com/facebook/react/blob/main/packages/eslint-plugin-react-hooks/CHANGELOG.md) - [Commits](https://github.com/facebook/react/commits/HEAD/packages/eslint-plugin-react-hooks) Updates `react-i18next` from 17.0.3 to 17.0.4 - [Changelog](https://github.com/i18next/react-i18next/blob/master/CHANGELOG.md) - [Commits](i18next/react-i18next@v17.0.3...v17.0.4) --- updated-dependencies: - dependency-name: "@types/express-session" dependency-version: 1.19.0 dependency-type: direct:development update-type: version-update:semver-minor dependency-group: dev-dependencies - dependency-name: eslint-config-etherpad dependency-version: 4.0.5 dependency-type: direct:development update-type: version-update:semver-patch dependency-group: dev-dependencies - dependency-name: typescript dependency-version: 6.0.3 dependency-type: direct:development update-type: version-update:semver-patch dependency-group: dev-dependencies - dependency-name: eslint-plugin-react-hooks dependency-version: 7.1.0 dependency-type: direct:development update-type: version-update:semver-minor dependency-group: dev-dependencies - dependency-name: react-i18next dependency-version: 17.0.4 dependency-type: direct:development update-type: version-update:semver-patch dependency-group: dev-dependencies ... Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Bumps [oidc-provider](https://github.com/panva/node-oidc-provider) from 9.8.1 to 9.8.2. - [Release notes](https://github.com/panva/node-oidc-provider/releases) - [Changelog](https://github.com/panva/node-oidc-provider/blob/main/CHANGELOG.md) - [Commits](panva/node-oidc-provider@v9.8.1...v9.8.2) --- updated-dependencies: - dependency-name: oidc-provider dependency-version: 9.8.2 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
…ther#5203) (ether#7535) * checkPlugin: flag absolute /static/plugins/ paths in templates (ether#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 ether#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>
* docs(openapi): document apikey auth in openapi.json (ether#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>
* feat!: replace Abiword with LibreOffice and add DOCX export (ether#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 f336f24. 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>
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>
…7430) * fix: allow undo of clear authorship colors without disconnect (ether#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: ether#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>
* 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>
* Add creator-owned pad settings defaults Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Refine pad settings layout Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Fix settings popup heading and width Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Explain enforced user settings Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Cover creator override flow Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Let creators bypass enforced settings Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Address pad settings follow-ups Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --------- Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.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>
ether#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>
* 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
…t alerts (ether#7556) 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>
…kflow (ether#7557) 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 ether#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>
* 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 ether#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.
* docs: design spec for issue ether#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 ether#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(ether#7570): bundle DB drivers, add regression CI - Bump ueberdb2 to ^5.0.47 (upstream ueberDB PR ether#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 ether#7570 repro) * end-to-end Postgres smoke Any stage failure blocks Docker Hub / GHCR publish. Supersedes ether#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>
Bumps [ueberdb2](https://github.com/ether/ueberDB) from 5.0.45 to 5.0.48. - [Changelog](https://github.com/ether/ueberDB/blob/main/CHANGELOG.md) - [Commits](ether/ueberDB@v5.0.45...v5.0.48) --- updated-dependencies: - dependency-name: ueberdb2 dependency-version: 5.0.48 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Bumps [axios](https://github.com/axios/axios) from 1.15.0 to 1.15.1. - [Release notes](https://github.com/axios/axios/releases) - [Changelog](https://github.com/axios/axios/blob/v1.x/CHANGELOG.md) - [Commits](axios/axios@v1.15.0...v1.15.1) --- updated-dependencies: - dependency-name: axios dependency-version: 1.15.1 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
* fix(API): exclude SYSTEM_AUTHOR_ID from listAuthorsOfPad
Pad.SYSTEM_AUTHOR_ID ('a.etherpad-system') is the synthetic author
Etherpad attributes inserts to when the HTTP API receives a call
without authorId (setText, setHTML, appendText, the server-side
import flows, and plugins like ep_post_data). It exists so the
changeset's text and attribs stay in sync — without ANY author
attribute, pad.atext drifts and clients fail setDocAText
reconciliation when loading the pad. See Pad.ts:96-105 for the
full rationale.
That bookkeeping detail was leaking through listAuthorsOfPad: a
pad whose only "contributor" is the system author still reported
one authorID, which the existing tests in pad.ts and
appendTextAuthor.ts (and presumably any caller that uses
listAuthorsOfPad to count real users) treat as a real participant.
Filter SYSTEM_AUTHOR_ID at the API surface so internal attribution
stays internal. getAllAuthors() and downstream callers (copy,
anonymize, atext verification) keep seeing the synthetic id —
this only narrows the public listAuthorsOfPad response.
Fixes ether#7785
Fixes ether#7790
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* docs(api): note that listAuthorsOfPad omits the system author
Match the runtime behaviour from the previous commit — the
synthetic 'a.etherpad-system' author used for unattributed inserts
is filtered out of the listAuthorsOfPad response.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
) * fix(export): surface checkValidRev error message in response body A non-numeric :rev (e.g. /p/foo/test1/export/txt) was reaching checkValidRev, which throws CustomError('rev is not a number', 'apierror'). The error fell through the route handler's .catch(next), so Express's default error renderer kicked in and returned a 500 with the generic HTML page <title>Error</title> / <pre>Internal Server Error</pre>. The thrown message never made it to the body, so callers had no way to tell why the request failed. Catch the apierror in the route handler and send err.message as a text/plain 500 body. Other errors still propagate to next(err) so unrelated failures keep their existing handling. Also retitle the test (was "is 403" while asserting expect(500)) — leftover label from an earlier expectation. Fixes ether#7788 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(export): validate rev before attachment, broaden error catch Qodo feedback on ether#7792: 1) ExportHandler.doExport set Content-Disposition (via res.attachment) before calling checkValidRev. If the rev was invalid, the route-level catch returned a plain-text 500 — but the attachment header was still in place, so browsers offered to save the error message as a file. Move checkValidRev to the top of doExport so an invalid rev never touches the attachment header. 2) The catch only converted CustomError('...', 'apierror') into a plain-text response. Other export errors (conversion failures, fs issues, soffice problems) still fell through to Express's default HTML renderer — non-deterministic for API callers and confusing when they were already half-downloading a file. Surface every export failure as a deterministic text/plain 500. apierrors carry user-facing messages, so send err.message verbatim. For other errors, log the full stack server-side and still emit err.message (or 'Internal Server Error' if absent) so the response body is never the Express HTML stack page. Also clear Content-Disposition in the catch as a safety net. Backend tests (importexportGetPost.ts): 58 passing, 0 failing. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…ther#7791) When an ordered-list level was the only consumer of olItemCounts, closing any list at that depth (including an unordered list that happens to share the level) reset olItemCounts[level] to 0. A later, unrelated ordered list at the same depth then took the "counter exists but is 0" branch in the ol-opening logic and emitted `<ol class="...">` without the start attribute that line.start would have supplied. Round-trip: importing <ul>...<ul>...</ul></ul><ol><li>x<ol><li>y</li></ol></li></ol> exported the inner ol as `<ol class="number">` instead of `<ol start="2" class="number">`, because the closing of the inner bullet ul wrote olItemCounts[2]=0 before the outer ol even opened. Gate the reset on line.listTypeName === 'number' so closing an unordered list never touches the ol bookkeeping. Closing an actual ordered list still resets, as ether#7470 intended. Fixes ether#7786 Fixes ether#7787 Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…#7789) * fix(backend-tests): un-skip tests/backend/specs/{api,admin}/* The pnpm test script's glob `tests/backend/specs/**.ts` only matched files at the top level of tests/backend/specs/. Every spec under tests/backend/specs/api/ (14 files) and tests/backend/specs/admin/ (2 files) has been silently skipped by CI — including the failing tests reported in ether#7785, ether#7786, ether#7787, ether#7788. Switch to passing the directories with `--extension ts --recursive`, which makes mocha walk the tree the way --recursive is documented to. Local run after this change picks up 16 additional spec files and surfaces 6 newly-visible failures: 4 already filed (ether#7785–ether#7788) plus one that wasn't yet filed (appendTextAuthor.ts: "appendText without authorId does not attribute to any author"). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * test(backend): regression check for tests/backend/specs/{api,admin} discovery Read the pnpm test script from src/package.json, hand mocha the same arguments under --dry-run --list-files, and assert that a representative spec from tests/backend/specs/api/ and tests/backend/specs/admin/ appears in the discovered list. Locks in the glob fix from this PR: if anyone re-narrows the script back to the previous tests/backend/specs/**.ts pattern (which only matches depth 1), this vitest fails with a clear "glob missed ..." message instead of letting the affected specs silently drop out of CI. Verified the test FAILS when the script is reverted to the broken glob. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…r#7607) (ether#7753) * docs(updater): plan tier 4 — autonomous update in maintenance window (ether#7607) Maps PR 4 of the auto-update design spec (§"Tier 4 — autonomous") to concrete files, tasks, and verification steps. Subsequent commits scaffold against this plan. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(updater): MaintenanceWindow module — wall-clock window math for tier 4 Pure module: parseWindow, inWindow, nextWindowStart. Supports tz=local|utc and cross-midnight ranges. Used by upcoming Scheduler + UpdatePolicy changes. 22 vitest unit tests cover format validation, same-day + cross-midnight boundaries, and host-local vs UTC clock comparisons. DST handling is absorbed by JS Date constructor's wall-clock normalization (documented in the file header). Refs ether#7607 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(updater): tier 4 backend — window-gated UpdatePolicy + Scheduler Wires MaintenanceWindow into the existing tier 3 backend so autonomous updates only fire while `now` is inside `updates.maintenanceWindow`. UpdatePolicy - new optional `maintenanceWindow` input - canAutonomous flips on only for git+tier=autonomous+parse-valid window - new reasons `maintenance-window-missing` / `maintenance-window-invalid` - rollback-failed still wins over window denial Scheduler - decideSchedule snaps scheduledFor forward to nextWindowStart when canAutonomous + grace lands outside the window - decideTriggerApply returns a new `{action: 'defer'}` when canAutonomous + fire-time is outside the window; carries nextStart for the runner - canAutonomous=false preserves Tier 3 behavior unchanged index.ts wires settings.updates.maintenanceWindow through both passes and re-arms the timer on defer. Status endpoint surface (nextWindowOpensAt) + admin UI picker land in a follow-up commit. Settings adds `maintenanceWindow: {start, end, tz} | null`, defaulting to null. settings.json.template / settings.json.docker document the shape. Tests - 22 vitest cases for MaintenanceWindow already cover the math - 4 new UpdatePolicy cases for the window outcomes - 6 new Scheduler cases for tier-4 schedule/trigger paths - Full backend-new suite: 629 passed (35 files) Refs ether#7607 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(updater): tier 4 admin UI — window status, deferred subtitle, banner GET /admin/update/status now returns: - `maintenanceWindow`: the parsed window object (admin sessions only) - `nextWindowOpensAt`: ISO of the next window opening when tier=autonomous UpdatePage - new "Maintenance window" section when tier=autonomous, shows current window summary + next opens at, or "Not configured" when unset - scheduled panel now appends a "deferred until <iso>" line when the backend has snapped scheduledFor to the next window opening UpdateBanner - new variant when tier=autonomous and policy.reason is `maintenance-window-missing` or `maintenance-window-invalid`, linking to /admin/update i18n - 8 new keys under `update.banner.*`, `update.page.policy.*`, `update.page.scheduled.*`, `update.window.*` (en.json only; translations follow via the usual locale workflow) Interactive picker is intentionally deferred — admins edit `updates.maintenanceWindow` via the parsed JSONC settings editor (ether#7709). A follow-up commit may add a thin write-through component if the JSONC round-trip turns out to be too rough for typical operators. Refs ether#7607 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * docs(updater): tier 4 — window gate, DST notes, runbook §12 (ether#7607) CHANGELOG: flip Tier 4 from "designed, not yet implemented" to current. Document maintenanceWindow shape, snap-forward, defer-at-fire, and the two missing/invalid policy reasons. doc/admin/updates.md: new "Tier 4 — autonomous in a maintenance window" section with config example, policy gating, DST/timezone notes, admin UI behavior. runbook: §12 walks a disposable VM through missing-window, malformed, outside-window deferral, fire-at-opening, and window-closes-mid-grace. Adds five sign-off checklist items. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * test(updater): tier 4 window-boundary integration (ether#7607) Mocha integration covering the four scenarios called out in the spec §"Tier 4 — autonomous": - outside-window: decideSchedule snaps scheduledFor forward to the next opening and the snapped value round-trips through saveState - inside-window at fire-time: decideTriggerApply returns fire - window-closes-mid-grace: decideTriggerApply returns defer with nextStart at the next opening; persisted state moves forward - cancel during deferred-grace: state returns to idle, and the next decideSchedule pass re-emits a schedule snapped to the next opening All 4 cases passing locally under tsx mocha. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(updater): real SMTP via nodemailer (mail.* settings) (ether#7607) Replaces the (would send email) stub introduced in PR ether#7601 with a nodemailer-backed transport. The dependency is lazy-imported so installs that don't set mail.host pay no runtime cost. Settings additions - new top-level mail block: host, port, secure, from, auth (user/pass) - mail.host=null keeps the legacy log-only behaviour; the Notifier still updates dedupe state so we don't re-evaluate every tick - settings.json.template documents the shape inline - settings.json.docker reads MAIL_HOST / MAIL_FROM / MAIL_PORT / MAIL_SECURE from env so operators can configure via container env Transport - lazy import('nodemailer') on first send - transport cached by host; settings reload picks up new host without needing a restart - send errors are swallowed (logged warn) so a transient SMTP failure can never poison the surrounding updater state machine - successful sends log at info; legacy "(would send email)" path remains the visible signal when mail is disabled Refs ether#7607 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(updater): preflight checks target tag's engines.node (ether#7607) Before mutating the working tree, runPreflight now reads the target tag's package.json via `git show <tag>:package.json` and verifies that process.versions.node satisfies its engines.node range. Failures land at preflight-failed cleanly (no rollback needed — nothing has changed yet). Motivation: a release that bumps the Node floor used to either fail mid-`pnpm install` (which then rolls back successfully) or restart on the new build and crash in the boot path (which then rolls back via the health-check timer). Both paths recover, but they burn a drain + restart cycle on a condition we can reject upfront. Implementation - new PreflightReason `node-engine-mismatch` - new dep `readTargetEnginesNode(tag)` — runs the git-show as a child process with stdio captured to a string; missing tag / missing file / malformed JSON / missing engines.node all resolve to null (treated as "no constraint, pass") - uses existing semver dep with includePrerelease: true - new PreflightInput field `currentNodeVersion`; threaded from process.versions.node in both wirings (scheduler + manual apply) - check runs *after* signature verification so we trust the package.json - PreflightResult carries an optional `detail` string; applyPipeline appends it to the lastResult.reason so the admin UI shows e.g. "node-engine-mismatch: target requires Node >=26.0.0, running 25.0.0" Tests: 6 new vitest cases (no engines.node, satisfies, fails below floor, caret range, loose-spaced range, ordering after signature). Full backend-new: 635 passed (was 629). Refs ether#7607 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(updater): email admin on auto-rollback / preflight-failed (ether#7607) Before this commit, only the terminal rollback-failed state emailed the admin. Auto-recovered failures (rolled-back-install-failed, rolled-back- build-failed, rolled-back-health-check, rolled-back-crash-loop) and pre- flight-failed surfaced only via the /admin/update banner — so a 3am autonomous update that failed because of, say, a Node engine bump would roll back silently and stay invisible until the admin next logged in. Notifier - new EmailKinds: 'update-preflight-failed', 'update-rolled-back', 'update-rollback-failed' - new pure decideOutcomeEmail(input) → {toSend, newState} - dedupe key `<outcome>:<targetTag>` in EmailSendLog.lastFailureKey: same outcome on same tag emits one email per cycle (kills retry-loop spam); a different outcome or different tag resets the key - rollback-failed always fires (terminal — overrides dedupe) - state.ts validator + loadState backfill the new field for legacy state files (Tier 1/2/3 installs upgrading in place) Wiring - new index.ts helper notifyApplyFailure() loads state, runs the pure notifier, sends (via the nodemailer-backed sendEmailViaSmtp from the previous commit), persists the new dedupe key — all best-effort - schedulerTriggerApply: fires on applyUpdate returning preflight-failed or rolled-back - /admin/update/apply HTTP handler: same - boot path in expressCreateServer: if state.lastResult is a failure outcome we haven't already emailed about, fire then. Covers: - health-check timeout rollback (timer expired between boots) - crash-loop forced rollback caught on a later boot - preflight-failed where the process didn't get to email before exit - unacknowledged rollback-failed terminal Tests - 8 new vitest cases for decideOutcomeEmail (adminEmail=null, each outcome's content, dedupe by tag, dedupe by outcome, rollback-failed bypass) - Full backend-new suite: 643 passed (was 635) Refs ether#7607 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(updater): address Qodo review on tier 4 - UpdatePage: only show "deferred until" subtitle when scheduledFor actually matches nextWindowOpensAt. The previous `scheduledFor > now + 60s` heuristic misfired during a normal in-window 15-min grace period. - applyPipeline: return the enriched preflight reason (`reason: detail`) instead of only `pf.reason`, so /admin/update/apply 409 bodies and failure-notify emails preserve diagnostics like the Node engine mismatch detail. - updater/index: key the cached nodemailer transport on the full set of SMTP options (host + port + secure + auth) so runtime changes to port/credentials via reloadSettings() invalidate the cache. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
ether#7796) * test(admin): skip anonymizeAuthorSocket suite when ep_hash_auth is installed ether#7789 un-hid this suite from CI and immediately surfaced a 14-minute stall on every with-plugins matrix run (Linux + Windows + the 'Upgrade from latest release' workflow). Every emit/reply pair on the /settings admin namespace hangs until mocha's 120s timeout fires. Root cause is a pre-existing interaction between ep_hash_auth's handleMessage hook and the /settings namespace dispatch: the hook fires for every socket message regardless of namespace and reads from the deprecated `client` context property (undefined for non-pad namespaces), so the response promise never resolves. Tracked separately in ether#7795. Until that lands, gate the suite on require.resolve('ep_hash_auth'). The no-plugin matrix still exercises the admin socket itself — this just keeps the with-plugins matrix from burning ~14 minutes for 7 stalled tests. Verified locally: - no ep_hash_auth in node_modules → 7 passing - ep_hash_auth resolvable → 0 passing, 7 pending Why require.resolve and not pluginDefs.plugins[...]: Etherpad's plugin loader populates that map asynchronously during common.init. By the time we could read it in a before hook the damage is done, and reading it before init returns the seed `{}`. Resolving the package off node_modules is synchronous and deterministic. Refs ether#7795 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(test): probe at the application layer; restore settings safely on skip Two Qodo follow-ups on this PR: 1) Replace the static `require.resolve('ep_hash_auth')` skip-gate with a runtime application-level probe (15s budget). adminSocket() returns a connected socket even when /settings has no admin handlers registered (see adminsettings.ts:25 — non-admin sockets exit early without binding listeners). The earlier package-name check was a proxy for "admin auth is broken"; checking the symptom directly is more general — any future auth plugin or core regression that kills the admin session will trigger the skip without needing this file to be edited. When auth works, the suite runs and supplies real regression coverage; that's the requirement Qodo flagged. 2) Guard after() with a setupCompleted flag. The skip-via-this.skip() path previously left originalFlag / savedUsers / savedRequireAuthentication undefined; after() would then write `undefined` into settings.gdprAuthorErasure.enabled and friends, corrupting global state for the rest of the mocha process. Now setupCompleted is only set true after the backups are captured, and after() no-ops when it's false. Verified locally: - no-plugin matrix → 7 passing (2s) - broken-auth sim → 0 passing, 7 pending (17s) Refs ether#7795 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…eck (ether#7794) * test(backend): make the glob regression check Windows-safe The previous version called execFileSync('npx', ...). On Windows runners (and any Windows host where npx is installed as npx.cmd), node's child_process.spawn does not auto-pick the .cmd shim, so the call fails with spawnSync npx ENOENT. CI on develop went red for "Windows without plugins" because of this — Linux passed. Resolve mocha's JS entry directly via require.resolve and run it under the current node process. No shell, no .cmd resolution, identical behaviour on every platform. Also normalise the absolute paths mocha prints to POSIX-relative form so the toContain() assertions match on both Linux (which emits forward slashes) and Windows (backslashes). Verified locally that the test still fails when the package.json glob is reverted to the broken tests/backend/specs/**.ts pattern. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * test(backend): use path.relative for cross-platform glob normalization Qodo flagged the prefix-strip + sep-split approach as brittle. On Windows runners mocha can emit paths with mixed separators or different drive-letter casing, in which case startsWith() misses the prefix and the assertions fail against absolute paths. path.relative(srcRoot, abs) handles drive-letter casing and mixed separators consistently, then a final replace([\\/]) -> '/' yields POSIX-relative paths regardless of how mocha printed them. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Summarises the changes since v3.0.0: Tier 4 autonomous-update-in- maintenance-window lands for real (was forward-documented in 3.0.0), real SMTP via nodemailer, Node engine preflight, rollback/preflight email notifications, security hardening bundle (JWT, temp-file tokens, token transfer, x-proxy-path sanitiser, Pad.appendRevision author invariant, setPadRaw legacy rewrite), and the api/admin backend specs that were silently skipped by the glob.
…her#7798) * fix(admin/pads): apply filter chip server-side, before pagination Before: PadPage's filter chip (`active`/`recent`/`empty`/`stale`) ran on the client AFTER the 12-row page slice was already on screen. On a deployment with hundreds of pads it produced obviously wrong results — click "empty pads" on page 1 with 100 empties and only the 0–12 empties within the current page passed the filter. thm reported this on a 3.1.0 deployment. Move the filter into `PadSearchQuery` so the `/settings` socket can apply it before slicing: 1. pattern filter on names (cheap) 2. hydrate metadata for the matching pad universe iff a non-`all` filter is set or a non-`padName` sort is requested 3. apply filter chip on the hydrated set 4. sort + slice → `total` reflects the filtered universe so the pagination footer makes sense The original handler also had a 4-way `if/else if` that duplicated the hydrate-and-sort loop per `sortBy`. Folded those into one pipeline with a single comparator switch. Client side, `PadPage.tsx`: - drop the client-side `filteredResults` filter (server already filters) - chip click writes `filter` into searchParams (debounced refetch) and resets `currentPage` to 0 - older clients that don't send `filter` keep working — server defaults to `all` Stats cards (totalUsers/activeCount/emptyCount) still count the visible page only — that's a pre-existing UI limitation tracked separately. Closes the regression thm reported. Test plan - `tsc --noEmit` clean (server + admin) - New backend spec `padLoadFilter.ts` exercises filter:empty with small `limit` to lock in the bug-fix, plus all/active/omitted cases - `5 passing` locally on Node 25 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * address Qodo review on ether#7798 1. Functional setState updaters for every searchParams mutation (Qodo bug 1). The debounced pattern handler captured a render-time snapshot of searchParams; a faster chip click or sort change in between would be silently reverted when the debounce fired. Now every mutation merges against the latest state. 2. Concurrency-limited hydration (Qodo bug 3). The earlier draft issued Promise.all over the full candidate set, fanning out to thousands of in-flight padManager.getPad() reads on busy deployments. New mapWithConcurrency() caps concurrent loads at 16 — empirically enough to saturate a single ueberDB driver without pushing the event loop into back-pressure. 3. Test cleanup deletes the injected test-admin (Qodo bug 4). The original snapshot/restore pattern saved `settings.users` by reference; reassigning the same reference in after() left the inserted key in place and could leak into later backend specs. 4. Document the new `filter` field on the `padLoad` socket query in admin/README.md (Qodo rule violation 2). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…ning on form controls (ether#7797) * fix(l10n): short-circuit form controls in translateNode (no spurious warning) `<select data-l10n-id="...">` with `<option>` element children — the pattern used by ep_headings2, ep_align, ep_font_size, ep_font_family, … — used to drop into the textContent branch of html10n.translateNode and hunt for a text-node child to overwrite. There is none, so the loop exited with `found = false` and emitted: Unexpected error: could not translate element content for key ep_headings.style The SELECT/INPUT/TEXTAREA aria-label fallback already lived inside the same else-branch, *after* the warning, so the accessible name landed correctly but the noisy console line still fired on every pad load. Move the form-control case into its own `else if`, before the text-node hunt: aria-label is the only sensible localization target for these elements (a <select>'s text is its <option> labels, not its own name). Closes the console warning reported on Etherpad 3.1.0. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(test): actually exercise the form-control short-circuit branch Qodo's review on ether#7797 caught two real test bugs: 1. `pad.toolbar.bold.title` ends in `.title`, which is in html10n's attribute allowlist. translateNode picks `prop = 'title'`, takes the first branch (node[prop] = str.str), and never reaches the textContent path where the warning lives. The test would pass without my fix. Switched the key to `pad.loading` — same stable pad-bundle translation, but the suffix isn't in the allowlist, so `prop` defaults to `textContent` and the test actually exercises the regression path. 2. `page.waitForTimeout(50)` is a fixed sleep, but `html10n.localize` runs its work inside a `build()` callback, so completion timing depends on the loader. Replaced with a deterministic `html10n.mt .bind('localized', …)` await. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* docs: design spec for ether#7799 outdated-notice redesign Per-pad first-author gating, dismissable gritter, minor-or-more rule, drop vulnerable UI. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * docs: implementation plan for ether#7799 outdated-notice redesign 12 bite-sized tasks, TDD-first where applicable; closes the spec end-to-end. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(updater): add isMinorOrMoreBehind, drop major/vulnerable helpers Adds isMinorOrMoreBehind(current, latest) which returns true only when the latest release is at least one minor version ahead (patch-only deltas return false). Removes isMajorBehind, parseVulnerableBelow, and isVulnerable from versionCompare.ts — callers in updateStatus.ts, VersionChecker.ts, and index.ts will be updated in subsequent tasks. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * refactor(updater): drop vulnerable-below directive and state field Remove VulnerableBelowDirective type, UpdateState.vulnerableBelow field, and all related scraping/checking logic (parseVulnerableBelow, isVulnerable imports). Clean up Notifier, OpenAPI schema, and all test fixtures to match. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * refactor(updater): drop residual EmailSendLog vulnerable fields Remove `vulnerableAt` and `vulnerableNewReleaseTag` from the `EmailSendLog` interface, `EMPTY_STATE`, and the `isValidEmail` validator — these backed the removed `vulnerable`/`vulnerable-new-release` email kinds and are now dead code. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * feat(updater): add firstAuthorOf helper Export firstAuthorOf() from updateStatus.ts — finds the lowest-numbered author attrib in a pad's pool, skipping empty-string placeholders. Covered by 6 vitest cases in tests/backend-new/specs/hooks/express/. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * feat(updater): add resolveRequestAuthor helper for HTTP GET Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * feat(updater): pad-aware /api/version-status with first-author gating Replace global badge cache with a per-(padId, authorId) LRU cache. The new response shape is {outdated: 'minor' | null, isFirstAuthor: boolean}; the old 'severe'/'vulnerable' enum is dropped entirely. computeOutdated now resolves the pad's first author and compares it against the session author before returning outdated:'minor', so the notice is only shown to the person who created the pad. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * fix(updater): switch isSevere signal from major-only to minor-or-more behind Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * test(updater): end-to-end coverage for /api/version-status Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * docs(openapi): /api/version-status pad-aware shape and gating Add the /api/version-status GET operation to the admin OpenAPI spec with the new pad-aware response shape: outdated enum reduced to [minor]|null, isFirstAuthor boolean, and an optional padId query param. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * chore(pad): remove unused #version-badge template and CSS * feat(pad): replace persistent badge with first-author outdated gritter Renames pad_version_badge.ts → pad_outdated_notice.ts and rewrites it as a fire-and-forget gritter notice that only shows when the API reports outdated=minor AND the current user is the pad's first author. Wires the new maybeShowOutdatedNotice() call into pad.ts immediately after showPrivacyBannerIfEnabled(). Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * test(pad): playwright coverage for outdated notice gritter Six Playwright specs exercise maybeShowOutdatedNotice: null response, isFirstAuthor:false guard, positive appearance + text, X-dismiss, 500 server error tolerance, and 8 s auto-fade. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * docs(pad): outdated-notice redesign + drop vulnerable-below docs Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * chore(test): remove stale specs for deleted #version-badge surface Delete the GET /api/version-status describe block from the legacy mocha spec (asserted outdated:null and outdated:'severe' — both no longer match the new response shape). The new vitest spec at tests/backend-new/specs/hooks/express/updateStatus.test.ts covers this surface comprehensively. Delete src/tests/frontend-new/specs/pad-version-badge.spec.ts entirely: all three tests reference the #version-badge DOM element removed in Task 8 and stub 'severe'/'vulnerable' enum values that no longer exist. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * chore: clean stale references to vulnerable/severe in types, emails, docs - Remove OutdatedLevel type (null|'severe') from types.ts — no consumers remain after the badge redesign removed the severe tier. - Fix Notifier severe-email body: was "more than one major release behind" but isSevere now fires on minor-or-more, so update to "at least one minor release behind the latest published version". - Drop "vulnerability directives" from the /admin/update/status OpenAPI description; replace with the actual response fields. - Remove stale vulnerableBelow field from UpdateStatusPayload in admin/src/store/store.ts — server no longer sends it. - Fix docs/admin/updates.md: "pad-side badge" → "pad-side notice". Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…her#7804) (ether#7805) * fix(updater): resolve pad author from token cookie, not session.user Etherpad does not populate an authorID into the express-session user object for pad visitors, so resolveRequestAuthor() always returned null in production, causing computeOutdated() to return EMPTY and the pad-side gritter to never fire. Replace the session-based lookup with a cookie-based path that mirrors how the socket.io handshake resolves pad-visitor identity: read the HttpOnly `token` (or `<prefix>token`) cookie and call authorManager.getAuthorId(token, user) via dynamic import (same circular-init guard pattern as the PadManager import). Update the test harness to mock AuthorManager instead of injecting a fake req.session.user.author, and to set req.cookies.token directly. All 9 cases continue to pass. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * docs(openapi): clarify admin spec scope includes pad-side endpoints /api/version-status is a public pad-side endpoint but lives in the admin OpenAPI document because it shares the same internal route registration. Add a note to info.description so downstream tooling consumers are not misled into treating it as an admin-only route. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> --------- Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
…) (ether#7807) * docs: spec for admin/settings resolved runtime values (ether#7803) Side-channel resolved+redacted settings alongside raw file blob. Form view dropdowns and env pill chips reflect actual runtime values instead of falling back to template defaults. Save round-trip is unchanged so ${VAR:default} literals stay intact on disk. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * docs: implementation plan for admin/settings resolved runtime (ether#7803) Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(admin): add redactor for resolved settings payload (ether#7803) Pure helper that walks the live settings module and replaces known sensitive paths (users.*.password, dbSettings.password, sso.clients[*].client_secret, sessionKey, …) with [REDACTED] sentinel. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(admin): emit redacted runtime settings on /settings socket load (ether#7803) Existing 'results' raw-file blob is unchanged so the textarea editor and saveSettings round-trip continue to preserve \${VAR:default} literals on disk. New 'resolved' field carries the in-memory settings module run through the redactor — admin SPA can use it to show actual runtime values next to env-var placeholders. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(admin): show resolved runtime value on EnvPill (ether#7803) Admin SPA now stores the resolved field from the /settings socket payload and exposes useResolvedAt(path) to walk it. EnvPill renders a "→ active value" chip when the path is resolved, or "→ ••••••" with a redacted tooltip when the server returned the [REDACTED] sentinel. Old-server fallback (undefined resolved) keeps current behaviour. The admin test script glob now picks up .test.tsx alongside .test.ts so the new EnvPill tests run under tsx --test. Closes ether#7803. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…er#7806) * docs: design for URL base-path support (ether#7802) Spec covers the architecture, header handling rules, components touched, backwards-compatibility story, risks, and test plan for honoring X-Forwarded-Prefix / X-Ingress-Path under trustProxy. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * docs: amend ether#7802 spec with discovery of pre-existing proxy-path helpers After exploring the codebase, much of the proposed architecture is already in place (sanitizeProxyPath, padBootstrap.js basePath derivation, admin SPA rewrite). Spec now reflects the actual delta: header source expansion, /manifest.json prefix-awareness, socialMeta proxyPath honoring, and template URL touch-ups for index/timeslider/pad/export. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * docs(plan): implementation plan for URL base-path support (ether#7802) Adds the bite-sized TDD task list to ship X-Forwarded-Prefix / X-Ingress-Path support: extends sanitizeProxyPath, makes /manifest.json and socialMeta prefix-aware, touches up the remaining leading-slash URLs in index/pad/timeslider/export templates, fixes a pre-existing manifest .. count bug. Drops the originally-proposed <base href> belt-and-braces after discovering it'd break the existing relative URLs in pad.html/timeslider.html and wouldn't help plugin DOM injection anyway (path-absolute URLs ignore <base>'s path component). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(proxy): accept X-Forwarded-Prefix and X-Ingress-Path under trustProxy (ether#7802) Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(pwa): make /manifest.json honor sanitised proxy-path (ether#7802) Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(social-meta): honor proxyPath in from-request og:url and og:image (ether#7802) Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(templates): index.html manifest + jslicense links honor proxyPath (ether#7802) Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(templates): pad.html reconnect/jslicense honor proxyPath; fix manifest .. count (ether#7802) Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(templates): timeslider.html reconnect/jslicense honor proxyPath; fix manifest .. count (ether#7802) Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(templates): export_html.html manifest honors proxyPath when available (ether#7802) Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * test: end-to-end coverage for X-Forwarded-Prefix / X-Ingress-Path (ether#7802) Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * docs(settings): trustProxy also enables X-Forwarded-Prefix / X-Ingress-Path (ether#7802) Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…ther#7808) ep_readonly_guest is archived (read-only on GitHub) and its authenticate hook unconditionally swaps req.session.user with a read-only guest, even when the request carries an HTTP Authorization header. That silently demoted admin login attempts and stalled the anonymizeAuthorSocket tests for 14 min/run on every with-plugins CI matrix (ether#7795). The pre-fix theory blamed ep_hash_auth.handleMessage; the actual hook trace is a red herring — handleMessage only fires on the /pad namespace and never on /settings. ep_guest is the maintained successor (same authors, same purpose). 1.0.72 on npm already includes the "defer to basic auth / admin paths" fix backported to ep_readonly_guest by intent here. Swapping the matrix unblocks the anonymizeAuthorSocket suite on Linux, Windows, and the upgrade-from-latest-release workflow. The runtime probe added in ether#7796 stays — it still catches any other authenticate-hook plugin that rejects the test's plain-text credentials (e.g. a future ep_hash_auth-style hashed-only plugin). Reattribute its comment so future readers don't chase ep_hash_auth. Closes ether#7795. Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…er#7819) (ether#7820) The admin saveSettings socket had zero direct backend coverage and the e2e 'restart works' test only checked the page renders after restart — neither catches a deployment that resets settings.json on restart, nor the user-visible workflow that triggered ether#7819 (add a top-level plugin block via Raw, save, watch it disappear). Adds three backend specs for the saveSettings socket: - payload is written byte-for-byte to settings.settingsFilename - augmenting existing JSON with a new top-level block round-trips through the next load reply - /* */ comments survive the write path Adds one e2e spec mirroring the ether#7819 workflow: open Raw, prepend an ep_oauth-shaped top-level block, save, restartEtherpad(), re-login, confirm the block is still in Raw and surfaces as its own Form-view section ('Ep oauth' from humanize()). Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Added a question about using abstraction like Docker in the bug report template.
… (ether#7821) * test(docker): admin save persists across container restart (ether#7819) The OP reports the symptom on the official Docker image specifically. Adds two layers of coverage to docker.yml's build-test job, driven from inside a container started against the same TEST_TAG the existing test-container step uses: 1. New mocha spec adminSettings_7819.ts under tests/container/specs/api — authenticates against /admin, opens the /settings socket, saves an augmented JSON with an ep_oauth-shaped top-level block, and asserts the next load reply contains the marker. Intentionally leaves the marker on disk so the workflow can inspect it. 2. docker.yml now `docker exec test grep`s for the marker after test-container, then `docker restart`s the container, waits for the health probe, and re-greps. Both checks must pass — the first proves the socket-driven save actually touched the file inside the container layer; the second proves an in-place restart doesn't reset it. A recreate (docker rm + docker run) would wipe the file, but that's expected (image layer) and out of scope. Container is started with `-e ADMIN_PASSWORD=changeme1` so the existing settings.json.docker provisions the admin user; pad.js doesn't touch /admin so the existing API specs are unaffected. test-container timeout bumped 5s → 30s to cover socket connect + save round-trip, and the mocha discovery extension list now includes `ts` so the new spec is picked up. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * test(docker): authenticate via /admin-auth/ POST, surface auth/load failures fast (ether#7819) CI failed on ether#7821 with a generic 20s mocha timeout because the spec hit GET /admin/ to grab a session cookie. webaccess.ts only treats paths starting with /admin-auth as requireAdmin — and the container runs with REQUIRE_AUTHENTICATION=false (default), so GET /admin/ never issued a Basic challenge and Set-Cookie was empty. The socket then connected unauthenticated, adminsettings.ts's connection handler returned early without binding any listeners, and the load() promise hung until mocha killed the test with no useful diagnostic. Switch to POST /admin-auth/ (always-requireAdmin, regardless of settings.requireAuthentication). Assert a 2xx with at least one Set-Cookie before proceeding. Add an 8s timeout + meaningful error message to load() so the "session was not admin" failure mode reports immediately instead of burning the suite budget. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * test(docker): replace splice with hand-built payload (ether#7819) Last CI failed because the splice-after-last-} approach landed a comma between an existing trailing-comma-before-comment and the close brace of settings.json.docker, producing `, /* … */, "ep_oauth"` — invalid JSON. settings.json.docker uses jsonc `/* */` and `//` comments and a trailing-comma-before-comment-before-close shape that's annoying to patch from the test side, and the existing isJSONClean has zero backend coverage so the splice is going through Etherpad's lenient write path anyway. Switch to a hand-built minimal-but-viable settings document containing the ep_oauth block. Three properties hold: - We're testing the WRITE path, not the synthesis path. Whatever bytes we send, the next `load` must return verbatim. - The post-save document must survive `docker restart` (the next step in docker.yml) — minimal-but-viable means port/users/dbType are present so Etherpad boots back up and HEALTHCHECK passes. - The next `load` reply must equal the bytes we saved (`reply.results === augmented`) — stronger than `.includes()`. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Bumps [tsx](https://github.com/privatenumber/tsx) from 4.22.0 to 4.22.3. - [Release notes](https://github.com/privatenumber/tsx/releases) - [Changelog](https://github.com/privatenumber/tsx/blob/master/release.config.cjs) - [Commits](privatenumber/tsx@v4.22.0...v4.22.3) --- updated-dependencies: - dependency-name: tsx dependency-version: 4.22.3 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Bumps [openapi-backend](https://github.com/openapistack/openapi-backend) from 5.16.1 to 5.17.0. - [Release notes](https://github.com/openapistack/openapi-backend/releases) - [Commits](openapistack/openapi-backend@5.16.1...5.17.0) --- updated-dependencies: - dependency-name: openapi-backend dependency-version: 5.17.0 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Bumps [pg](https://github.com/brianc/node-postgres/tree/HEAD/packages/pg) from 8.20.0 to 8.21.0. - [Changelog](https://github.com/brianc/node-postgres/blob/master/CHANGELOG.md) - [Commits](https://github.com/brianc/node-postgres/commits/pg@8.21.0/packages/pg) --- updated-dependencies: - dependency-name: pg dependency-version: 8.21.0 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Bumps [@tanstack/react-query-devtools](https://github.com/TanStack/query/tree/HEAD/packages/react-query-devtools) from 5.100.10 to 5.100.11. - [Release notes](https://github.com/TanStack/query/releases) - [Changelog](https://github.com/TanStack/query/blob/main/packages/react-query-devtools/CHANGELOG.md) - [Commits](https://github.com/TanStack/query/commits/@tanstack/react-query-devtools@5.100.11/packages/react-query-devtools) --- updated-dependencies: - dependency-name: "@tanstack/react-query-devtools" dependency-version: 5.100.11 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Bumps [js-cookie](https://github.com/js-cookie/js-cookie) from 3.0.6 to 3.0.7. - [Release notes](https://github.com/js-cookie/js-cookie/releases) - [Commits](https://github.com/js-cookie/js-cookie/commits/v3.0.7) --- updated-dependencies: - dependency-name: js-cookie dependency-version: 3.0.7 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
…ith 8 updates (ether#7825) Bumps the dev-dependencies group with 8 updates in the / directory: | Package | From | To | | --- | --- | --- | | [@types/node](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/node) | `25.8.0` | `25.9.1` | | [vitest](https://github.com/vitest-dev/vitest/tree/HEAD/packages/vitest) | `4.1.6` | `4.1.7` | | [@types/react](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/react) | `19.2.14` | `19.2.15` | | [@typescript-eslint/eslint-plugin](https://github.com/typescript-eslint/typescript-eslint/tree/HEAD/packages/eslint-plugin) | `8.59.3` | `8.59.4` | | [@typescript-eslint/parser](https://github.com/typescript-eslint/typescript-eslint/tree/HEAD/packages/parser) | `8.59.3` | `8.59.4` | | [react-hook-form](https://github.com/react-hook-form/react-hook-form) | `7.75.0` | `7.76.0` | | [vite-plugin-babel](https://github.com/owlsdepartment/vite-plugin-babel) | `1.7.1` | `1.7.3` | | [oxc-minify](https://github.com/oxc-project/oxc/tree/HEAD/napi/minify) | `0.131.0` | `0.132.0` | Updates `@types/node` from 25.8.0 to 25.9.1 - [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases) - [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/node) Updates `vitest` from 4.1.6 to 4.1.7 - [Release notes](https://github.com/vitest-dev/vitest/releases) - [Commits](https://github.com/vitest-dev/vitest/commits/v4.1.7/packages/vitest) Updates `@types/react` from 19.2.14 to 19.2.15 - [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases) - [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/react) Updates `@typescript-eslint/eslint-plugin` from 8.59.3 to 8.59.4 - [Release notes](https://github.com/typescript-eslint/typescript-eslint/releases) - [Changelog](https://github.com/typescript-eslint/typescript-eslint/blob/main/packages/eslint-plugin/CHANGELOG.md) - [Commits](https://github.com/typescript-eslint/typescript-eslint/commits/v8.59.4/packages/eslint-plugin) Updates `@typescript-eslint/parser` from 8.59.3 to 8.59.4 - [Release notes](https://github.com/typescript-eslint/typescript-eslint/releases) - [Changelog](https://github.com/typescript-eslint/typescript-eslint/blob/main/packages/parser/CHANGELOG.md) - [Commits](https://github.com/typescript-eslint/typescript-eslint/commits/v8.59.4/packages/parser) Updates `react-hook-form` from 7.75.0 to 7.76.0 - [Release notes](https://github.com/react-hook-form/react-hook-form/releases) - [Changelog](https://github.com/react-hook-form/react-hook-form/blob/master/CHANGELOG.md) - [Commits](react-hook-form/react-hook-form@v7.75.0...v7.76.0) Updates `vite-plugin-babel` from 1.7.1 to 1.7.3 - [Commits](https://github.com/owlsdepartment/vite-plugin-babel/commits) Updates `oxc-minify` from 0.131.0 to 0.132.0 - [Release notes](https://github.com/oxc-project/oxc/releases) - [Changelog](https://github.com/oxc-project/oxc/blob/main/napi/minify/CHANGELOG.md) - [Commits](https://github.com/oxc-project/oxc/commits/crates_v0.132.0/napi/minify) --- updated-dependencies: - dependency-name: "@types/node" dependency-version: 25.9.1 dependency-type: direct:development update-type: version-update:semver-minor dependency-group: dev-dependencies - dependency-name: vitest dependency-version: 4.1.7 dependency-type: direct:development update-type: version-update:semver-patch dependency-group: dev-dependencies - dependency-name: "@types/react" dependency-version: 19.2.15 dependency-type: direct:development update-type: version-update:semver-patch dependency-group: dev-dependencies - dependency-name: "@typescript-eslint/eslint-plugin" dependency-version: 8.59.4 dependency-type: direct:development update-type: version-update:semver-patch dependency-group: dev-dependencies - dependency-name: "@typescript-eslint/parser" dependency-version: 8.59.4 dependency-type: direct:development update-type: version-update:semver-patch dependency-group: dev-dependencies - dependency-name: react-hook-form dependency-version: 7.76.0 dependency-type: direct:development update-type: version-update:semver-minor dependency-group: dev-dependencies - dependency-name: vite-plugin-babel dependency-version: 1.7.3 dependency-type: direct:development update-type: version-update:semver-patch dependency-group: dev-dependencies - dependency-name: oxc-minify dependency-version: 0.132.0 dependency-type: direct:development update-type: version-update:semver-minor dependency-group: dev-dependencies ... Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Port of upstream ether#7605 Converts backend tests from mocha to vitest and migrates to ESM.
There was a problem hiding this comment.
5 issues found across 733 files
Prompt for AI agents (unresolved issues)
Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.
<file name=".github/workflows/release.yml">
<violation number="1" location=".github/workflows/release.yml:75">
P2: `dangerously-allow-all-builds=true` disables pnpm’s dependency build-script guard for all packages; restrict allowed builds to the packages that actually need it (e.g., `sharp`).</violation>
</file>
<file name="admin/src/components/ColorSwatch.tsx">
<violation number="1" location="admin/src/components/ColorSwatch.tsx:13">
P2: Duplicate color `#ffa8a8` appears at two positions in the PALETTE (indices 8 and 55). For an author-color palette, duplicates reduce distinct color count — two authors with different index values would receive the same swatch color.</violation>
</file>
<file name="admin/scripts/gen-api.mjs">
<violation number="1" location="admin/scripts/gen-api.mjs:37">
P2: Avoid calling `process.exit()` inside this `try` block; it can bypass the `finally` cleanup and leak temp directories on failures.</violation>
</file>
<file name="admin/src/components/settings/widgets/NumberInput.tsx">
<violation number="1" location="admin/src/components/settings/widgets/NumberInput.tsx:34">
P2: Blur does not resync `draft` with the current prop value, so value changes that occurred while focused can leave stale text in the input.</violation>
</file>
<file name="admin/src/components/UpdateBanner.tsx">
<violation number="1" location="admin/src/components/UpdateBanner.tsx:7">
P2: Handle non-finite countdown values in `fmtRemaining`; malformed `scheduledFor` currently renders `NaN` in the banner.</violation>
</file>
Note: This PR contains a large number of files. cubic only reviews up to 100 files per PR, so some files may not have been reviewed. cubic prioritizes the most important files to review.
On a pro plan you can use ultrareview for larger PRs.
Re-trigger cubic
| # install script must run to fetch the platform binary. pnpm 11 | ||
| # turned ignored-builds into an error; allow all builds for this | ||
| # external repo since we don't control its pnpm-workspace.yaml. | ||
| run: pnpm install --frozen-lockfile --config.dangerously-allow-all-builds=true |
There was a problem hiding this comment.
P2: dangerously-allow-all-builds=true disables pnpm’s dependency build-script guard for all packages; restrict allowed builds to the packages that actually need it (e.g., sharp).
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At .github/workflows/release.yml, line 75:
<comment>`dangerously-allow-all-builds=true` disables pnpm’s dependency build-script guard for all packages; restrict allowed builds to the packages that actually need it (e.g., `sharp`).</comment>
<file context>
@@ -48,24 +48,31 @@ jobs:
+ # install script must run to fetch the platform binary. pnpm 11
+ # turned ignored-builds into an error; allow all builds for this
+ # external repo since we don't control its pnpm-workspace.yaml.
+ run: pnpm install --frozen-lockfile --config.dangerously-allow-all-builds=true
working-directory: ether.github.com
- name: Set git user
</file context>
| run: pnpm install --frozen-lockfile --config.dangerously-allow-all-builds=true | |
| run: pnpm install --frozen-lockfile --allow-build=sharp |
| // admin already has many other small constants inline. | ||
| const PALETTE = [ | ||
| '#ffc7c7', '#fff1c7', '#e3ffc7', '#c7ffd5', '#c7ffff', '#c7d5ff', | ||
| '#e3c7ff', '#ffc7f1', '#ffa8a8', '#ffe699', '#cfff9e', '#99ffb3', |
There was a problem hiding this comment.
P2: Duplicate color #ffa8a8 appears at two positions in the PALETTE (indices 8 and 55). For an author-color palette, duplicates reduce distinct color count — two authors with different index values would receive the same swatch color.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At admin/src/components/ColorSwatch.tsx, line 13:
<comment>Duplicate color `#ffa8a8` appears at two positions in the PALETTE (indices 8 and 55). For an author-color palette, duplicates reduce distinct color count — two authors with different index values would receive the same swatch color.</comment>
<file context>
@@ -0,0 +1,37 @@
+// admin already has many other small constants inline.
+const PALETTE = [
+ '#ffc7c7', '#fff1c7', '#e3ffc7', '#c7ffd5', '#c7ffff', '#c7d5ff',
+ '#e3c7ff', '#ffc7f1', '#ffa8a8', '#ffe699', '#cfff9e', '#99ffb3',
+ '#a3ffff', '#99b3ff', '#cc99ff', '#ff99e5', '#e7b1b1', '#e9dcAf',
+ '#cde9af', '#bfedcc', '#b1e7e7', '#c3cdee', '#d2b8ea', '#eec3e6',
</file context>
| ); | ||
| if (dump.status !== 0) { | ||
| console.error(`dump-spec.ts failed with exit code ${dump.status}`); | ||
| process.exit(dump.status ?? 1); |
There was a problem hiding this comment.
P2: Avoid calling process.exit() inside this try block; it can bypass the finally cleanup and leak temp directories on failures.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At admin/scripts/gen-api.mjs, line 37:
<comment>Avoid calling `process.exit()` inside this `try` block; it can bypass the `finally` cleanup and leak temp directories on failures.</comment>
<file context>
@@ -0,0 +1,78 @@
+ );
+ if (dump.status !== 0) {
+ console.error(`dump-spec.ts failed with exit code ${dump.status}`);
+ process.exit(dump.status ?? 1);
+ }
+
</file context>
| data-testid={`field-${path.join('.')}`} | ||
| value={draft} | ||
| onFocus={() => { focusedRef.current = true; }} | ||
| onBlur={() => { focusedRef.current = false; }} |
There was a problem hiding this comment.
P2: Blur does not resync draft with the current prop value, so value changes that occurred while focused can leave stale text in the input.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At admin/src/components/settings/widgets/NumberInput.tsx, line 34:
<comment>Blur does not resync `draft` with the current prop value, so value changes that occurred while focused can leave stale text in the input.</comment>
<file context>
@@ -0,0 +1,48 @@
+ data-testid={`field-${path.join('.')}`}
+ value={draft}
+ onFocus={() => { focusedRef.current = true; }}
+ onBlur={() => { focusedRef.current = false; }}
+ onChange={e => {
+ const next = e.target.value;
</file context>
| import {useStore} from '../store/store'; | ||
|
|
||
| const fmtRemaining = (ms: number): string => { | ||
| if (ms <= 0) return '0s'; |
There was a problem hiding this comment.
P2: Handle non-finite countdown values in fmtRemaining; malformed scheduledFor currently renders NaN in the banner.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At admin/src/components/UpdateBanner.tsx, line 7:
<comment>Handle non-finite countdown values in `fmtRemaining`; malformed `scheduledFor` currently renders `NaN` in the banner.</comment>
<file context>
@@ -0,0 +1,104 @@
+import {useStore} from '../store/store';
+
+const fmtRemaining = (ms: number): string => {
+ if (ms <= 0) return '0s';
+ const s = Math.floor(ms / 1000);
+ const m = Math.floor(s / 60);
</file context>
Port of upstream ether#7605
Converts backend tests from mocha to vitest and migrates to ESM.
Summary by cubic
Moves the backend test suite to ESM and replaces Mocha with
vitest, then updates CI to run on Node ≥24 withpnpm. Also adds first‑class packaging (Debian/Snap), refreshes the admin UI, and ships several editor and privacy improvements.New Features
showMenuRightparam.CI & Packaging
vitestunder ESM; GitHub Actions upgraded (Node 24,pnpmcaching, consolidated workflows)..deband Snap build/publish pipelines; Docker and release workflows hardened; APT repo publishing added..npmrcuses hardlink installs; stricter dependency policy relaxed to avoid CI breaks; dependency and action versions bumped.Written for commit ef58b53. Summary will update on new commits. Review in cubic