mirror of
https://github.com/ether/etherpad-lite.git
synced 2026-07-17 16:47:05 +00:00
9808 commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
40eeb214d3
|
build(deps-dev): bump the dev-dependencies group across 1 directory with 8 updates (#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](https://github.com/react-hook-form/react-hook-form/compare/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> |
||
|
|
1d2374a9ba
|
build(deps): bump js-cookie from 3.0.6 to 3.0.7 (#7813)
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> |
||
|
|
8112b7a05c
|
build(deps): bump @tanstack/react-query-devtools (#7814)
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> |
||
|
|
3c4edd1707
|
build(deps): bump pg from 8.20.0 to 8.21.0 (#7815)
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> |
||
|
|
7bb7d9731b
|
build(deps): bump openapi-backend from 5.16.1 to 5.17.0 (#7818)
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](https://github.com/openapistack/openapi-backend/compare/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> |
||
|
|
9bb39205e3
|
build(deps): bump tsx from 4.22.0 to 4.22.3 (#7822)
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](https://github.com/privatenumber/tsx/compare/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> |
||
|
|
abda0485d3
|
test(docker): admin save persists across container restart (#7819) (#7821)
* test(docker): admin save persists across container restart (#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 (#7819) CI failed on #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 (#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> |
||
|
|
49111f2fc9
|
Enhance bug report template with abstraction question
Added a question about using abstraction like Docker in the bug report template. |
||
|
|
abff8124e8
|
test(admin): cover saveSettings round-trip + restart persistence (#7819) (#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 #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 #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> |
||
|
|
271eb6ab5c
|
ci: swap deprecated ep_readonly_guest for ep_guest in plugin matrix (#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 (#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 #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 #7795. Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
4d94b1b465
|
Localisation updates from https://translatewiki.net. | ||
|
|
86edd67f58
|
feat: support X-Forwarded-Prefix and X-Ingress-Path (#7802) (#7806)
* docs: design for URL base-path support (#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 #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 (#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 (#7802) Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(pwa): make /manifest.json honor sanitised proxy-path (#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 (#7802) Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(templates): index.html manifest + jslicense links honor proxyPath (#7802) Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(templates): pad.html reconnect/jslicense honor proxyPath; fix manifest .. count (#7802) Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(templates): timeslider.html reconnect/jslicense honor proxyPath; fix manifest .. count (#7802) Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(templates): export_html.html manifest honors proxyPath when available (#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 (#7802) Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * docs(settings): trustProxy also enables X-Forwarded-Prefix / X-Ingress-Path (#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> |
||
|
|
4d998d6ef2
|
fix(admin): show resolved runtime values on /admin/settings (#7803) (#7807)
* docs: spec for admin/settings resolved runtime values (#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 (#7803) Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(admin): add redactor for resolved settings payload (#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 (#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 (#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 #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> |
||
|
|
f6ab8561ae
|
fix(pad): outdated notice — resolve author from token cookie (Qodo #7804) (#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> |
||
|
|
29dac6bfcc
|
fix(pad): redesign outdated-version notice (#7799) (#7804)
* docs: design spec for #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 #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> |
||
|
|
acc7a5dd4f
|
fix(l10n): silence spurious 'could not translate element content' warning on form controls (#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 #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> |
||
|
|
10558ed115
|
fix(admin/pads): apply filter chip server-side, before pagination (#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 #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>
|
||
|
|
228b49f394 | Merge branch 'master' into develop | ||
|
|
8fb20384dc | bump version | ||
|
|
fa1c6b2e43 | Merge branch 'develop' | ||
|
|
11a4463130 |
docs(changelog): v3.1.0
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. |
||
|
|
662637d1ac
|
test(backend): fix Windows ENOENT in backend-tests-glob regression check (#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>
|
||
|
|
b195b135e4
|
test(admin): skip anonymizeAuthorSocket when ep_hash_auth is installed (#7796)
* test(admin): skip anonymizeAuthorSocket suite when ep_hash_auth is installed #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 #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 #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 #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> |
||
|
|
962bfe8649
|
feat(updater): tier 4 — autonomous update in maintenance window (#7607) (#7753)
* docs(updater): plan tier 4 — autonomous update in maintenance window (#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 #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 #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 (#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 #7607 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * docs(updater): tier 4 — window gate, DST notes, runbook §12 (#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 (#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) (#7607) Replaces the (would send email) stub introduced in PR #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 #7607 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(updater): preflight checks target tag's engines.node (#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 #7607 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(updater): email admin on auto-rollback / preflight-failed (#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 #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> |
||
|
|
dbd4662d2b
|
fix(backend-tests): un-skip api/ and admin/ subdirectory specs (#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 #7785, #7786, #7787, #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 (#7785–#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>
|
||
|
|
8f499b458d
|
fix(ExportHtml): don't poison ol counter when closing a sibling ul (#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 #7470 intended. Fixes #7786 Fixes #7787 Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
0e90184b9b
|
fix(export): surface checkValidRev error message on bad :rev (#7792)
* 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 #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 #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>
|
||
|
|
fba4a17fcc
|
fix(API): hide SYSTEM_AUTHOR_ID from listAuthorsOfPad (#7793)
* 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 #7785
Fixes #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>
|
||
|
|
8c6104c5d5
|
harden: assorted server-side tightening for 3.0.2 (#7784)
* harden: assorted security tightening across server entry points
A bundle of defence-in-depth hardening picked up during an internal
audit pass. Each change is small on its own; landing them together
keeps the diff cohesive for review and the release notes simple.
Production-side changes:
- src/node/handler/APIHandler.ts: tighten the OAuth JWT validation
path on the HTTP API. Verify the signature before reading any
claim off the payload, and require the admin claim to be strictly
true (not just present). Switch the apikey comparison to
crypto.timingSafeEqual.
- src/node/handler/{Import,Export}Handler.ts: derive temp-file path
tokens from crypto.randomBytes(16) instead of Math.random.
- src/node/hooks/express/tokenTransfer.ts: enforce a 5-minute TTL
on transfer records, make redemption single-use (remove before
response), and drop the author token from the response body —
the HttpOnly cookie is the only delivery channel.
- src/node/utils/sanitizeProxyPath.ts (new): shared sanitiser for
the `x-proxy-path` header. Used by admin.ts (HTML/JS/CSS
substitution) and specialpages.ts (legacy timeslider redirect).
Strips characters outside [A-Za-z0-9_./-], collapses leading
`//+` to a single `/`, rejects `..` traversal. admin.ts also
emits Vary: x-proxy-path and Cache-Control: private, no-store.
- src/node/db/Pad.ts + src/node/utils/ImportHtml.ts: centralise
the "every insert op carries an author attribute" invariant in
Pad.appendRevision so all non-wire callers (setText, setHTML,
restoreRevision, plugin paths) get the same check the socket
handler already enforces. Pad.init and setPadHTML now
substitute SYSTEM_AUTHOR_ID when no author is supplied — same
pattern setText/spliceText already use.
Tests:
- src/tests/backend/specs/api/jwtAdminClaim.ts (5 cases)
- src/tests/backend/specs/tokenTransfer.ts (6 cases)
- src/tests/backend/specs/proxyPathRedirect.ts (5 cases)
- src/tests/backend/specs/padInsertAuthorInvariant.ts (4 cases)
- src/tests/backend-new/specs/sanitizeProxyPath.test.ts (14 vitest cases)
- src/tests/backend/common.ts: add generateJWTTokenAdminFalse helper.
Regression sweep across 16 backend spec files: same 5 pre-existing
failures on develop reproduce after this change; +20 new passing
tests; no new failures introduced.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* harden: rewrite imported .etherpad records to satisfy the insert-op invariant
Legacy .etherpad exports (and exports from older server-internal flows
that didn't substitute SYSTEM_AUTHOR_ID) can contain `+content` insert
ops without an `author` attribute. The previous commit's appendRevision
guard rejects that shape, but setPadRaw bulk-writes records directly
to the DB and never goes through appendRevision -- so a hand-crafted
.etherpad file could persist non-conforming data that any subsequent
setText / setHTML / restoreRevision call would then refuse to extend.
Add a pre-pass over the parsed import that:
- walks revs in numeric order, sanitising each changeset's `+` ops
against the cumulative pad pool (mutating the pool to register
SYSTEM_AUTHOR_ID when needed);
- re-applies each (post-sanitisation) changeset to a running atext
so the head atext and any key-rev meta.atext / meta.pool
snapshots are re-derived in lock-step with the rewritten revs
(otherwise pad.check's deep-equal at the end of setPadRaw would
fail on the attribute-number drift between the sanitised head
state and the now-stale key-rev snapshot);
- leaves already-conforming payloads untouched (no log noise on
good imports).
Returns the number of ops rewritten so the import can log a single
warning per legacy file rather than per-record. Pure-newline `+` ops
are exempted -- same whitelist as the wire-side guard.
Tests:
- tests/backend/specs/padInsertAuthorInvariant.ts: two new cases
drive setPadRaw end-to-end with a hand-crafted legacy payload
(asserts the head atext gains a `*N` attribute reference and the
pool registers `[author, a.etherpad-system]`) and with a
conforming payload (asserts the data round-trips unchanged).
Regression sweep across 17 backend spec files: 209 passing / 12
pending / 5 failing -- same 5 pre-existing failures present on
unmodified develop. +2 new passing tests; no new failures introduced.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
||
|
|
149c6a24e7 | Merge branch 'develop' | ||
|
|
b96e262782 | Merge branch 'master' into develop | ||
|
|
1864cd2fc1 | bump version | ||
|
|
4e58153c5c | chore: added changelog for v3 | ||
|
|
8b9d9f31ec
|
build(deps): bump ueberdb2 from 5.0.48 to 6.0.2 (#7734) | ||
|
|
a4261c2895
|
fix(a11y): drop role=textbox / aria-multiline from innerdocbody (#7778) (#7782)
Murphy's 2026-05-16 re-test of #7255 reported "you still can't cycle through the text properly line by line to press links and such". The narrower toolbar/measurement fixes in #7777 don't address this — it's caused by the editor body advertising textbox semantics. role="textbox" + aria-multiline="true" pin NVDA/JAWS into focus mode for the whole pad. In focus mode arrow keys move the caret one character at a time, the P/H/K rotor shortcuts are suppressed, and links don't surface in the links list. That matches Murphy's symptoms exactly. contenteditable="true" by itself is enough to tell AT this is editable. Without the textbox role, NVDA/JAWS browse the content as document-mode HTML — line-by-line arrow nav, headings rotor, links list all return. aria-label / aria-describedby stay so the pad is still announced as "Pad content" with the keyboard hint on focus. This is the lighter alternative to the AT-only read mirror originally sketched in #7778 — ARIA-only, no DOM restructuring, no plugin impact. Refs #7255 #7777 |
||
|
|
278acb10cb
|
Drop swagger-ui, document telemetry, add opt-outs (#7524) (#7757)
* docs: design spec for #7524 drop swagger-ui + privacy opt-outs Three-deliverable plan: vendor RapiDoc to replace swagger-ui-express (Scarf-injecting), add privacy.updateCheck and privacy.pluginCatalog opt-outs for our two outbound calls, and ship PRIVACY.md as a public stance doc. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * docs: implementation plan for #7524 swagger-ui + privacy opt-outs Twelve TDD-flavoured tasks: privacy settings shape, UpdateCheck + installer opt-outs (each with a failing-test-first cycle), admin backend/UI plumbing, dependency drop, vendored RapiDoc, PRIVACY.md, final verification matrix. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(privacy): add privacy block to settings shape Adds privacy.updateCheck and privacy.pluginCatalog, both defaulting to true so behavior is unchanged until operators opt out. Refs #7524 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(privacy): honour privacy.updateCheck=false in UpdateCheck check() and getLatestVersion() now early-return when the setting is off. Logs once on first skip. The admin "update available" panel already tolerates an undefined latestVersion. Refs #7524 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(privacy): honour privacy.pluginCatalog=false in installer Extracts the gate into pluginCatalogGuard.ts so it can be unit-tested under vitest without dragging in the CJS require() chain from installer.ts. getAvailablePlugins() now throws the tagged disabled error before any fetch. Refs #7524 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(privacy): emit results:catalogDisabled when pluginCatalog off Short-circuits the four catalog-driven socket events. The install/ uninstall events are untouched so operators can still install by plugin name even when the catalog is disabled. Refs #7524 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(bin): stalePlugins reads updateServer and honours privacy flag Was hardcoding static.etherpad.org and ignoring opt-out. Now exits 0 cleanly when privacy.pluginCatalog=false. Refs #7524 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * docs(settings): document privacy block in settings template Refs #7524 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(api-docs): replace swagger-ui-express with RapiDoc shell Drops the swagger-ui-express dep (third-party Scarf telemetry pixel, see swagger-api/swagger-ui#10573) and serves /api-docs with a static HTML shell that mounts <rapi-doc>. /api-docs.json is unchanged. The vendored RapiDoc asset is added in the next commit so the tree is broken for one diff hunk — pair this with the rapidoc-min.js commit during review. Refs #7524 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(api-docs): vendor RapiDoc 9.3.4 (MIT) as static asset Pinned bundle with checksum in VERSION. Replaces swagger-ui-dist which shipped a Scarf telemetry pixel. Disables RapiDoc's bundled Google Fonts request via load-fonts="false" plus explicit regular-font/mono-font system stacks — RapiDoc's CSS @font-face rules would otherwise fetch Open Sans from fonts.gstatic.com at render time. Also fixes the /api-docs route's res.sendFile to use an absolute path resolved via settings.root (the previous {root: 'src/static'} was resolved from CWD which is already src/, producing src/src/static). Refs #7524 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(admin): banner when plugin catalog is disabled Subscribes to results:catalogDisabled and renders a localized info banner on the plugins page. install/uninstall still function via CLI. Refs #7524 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * docs: PRIVACY.md and README/CHANGELOG pointers Publishes Etherpad's stance on telemetry: two documented, opt-out outbound calls; no third-party analytics; no install-time phone-homes in our deps. Refs #7524 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(admin): await checkPluginForUpdates and emit array on error Qodo flagged that checkUpdates emitted the unresolved Promise (missing await) and emitted {} for updatable on the error path, both breaking the admin UI's expected string[] shape. Pre-existing bug surfaced when the surrounding block was edited for the privacy.pluginCatalog gate. Refs #7524 * feat(api-docs): swap RapiDoc for Scalar (actively maintained) Per @SamTV12345's review on #7757: RapiDoc has been effectively unmaintained for a while. Scalar (https://github.com/scalar/scalar) is MIT-licensed, actively developed, and ships a self-contained standalone bundle that works the same way for our purposes. Privacy posture is preserved by configuring the embed: - withDefaultFonts: false (no fonts.scalar.com woff2 fetch) - telemetry: false (defensive) - agent.disabled: true (no api.scalar.com/vector/* calls) - mcp.disabled: true (no MCP integration) - showDeveloperTools: 'never' - hideClientButton: true Verified with headless Chromium: page loads /api-docs, mounts Scalar, renders the Etherpad OpenAPI document, and makes zero requests to any host other than localhost. Vendor: - src/static/vendor/scalar/standalone.js (@scalar/api-reference 1.57.2) - src/static/vendor/scalar/VERSION (sha256 pinned) - src/static/vendor/scalar/LICENSE (MIT) Removed: - src/static/vendor/rapidoc/* Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
8c4f974122
|
fix(pad): keep menu_right visible on readonly pads by default (#7783)
PR #X (issue #5182) added a client-side `$('#editbar .menu_right').hide()` for readonly pads, opt-out via `?showMenuRight=true`. The intent — clean chrome for iframe-embedded readonly announcement pads — was good but the implementation hid the userlist toggle along with import/export. That has two unwanted effects on non-embed deployments: * Plugins like ep_guest inject their "Log In" button into `#myuser` inside the userlist popup, which lives under `.menu_right`. When the guest user (readOnly: true) lands on a readonly pad, the button they need to escape readonly is hidden. Chicken-and-egg. * Settings, embed, home, timeslider, showusers are all legitimately useful for readonly viewers and got removed alongside the actual write-only controls. The server already does the right thing without help from the client: * src/node/utils/toolbar.ts:282-290 strips `savedrevision` from the right toolbar when isReadOnly is true. * src/static/css/pad/popup_import_export.css:1 has `.readonly .acl-write { display: none }`, which hides the Import column of the import/export popup. Export stays visible (and is legitimately useful in readonly). Drop the client-side blanket hide. The iframe-embed use case from #5182 is still served by `?showMenuRight=false` (the existing handler at src/static/js/pad.ts:91-107), and `?showControls=false` continues to hide the entire editbar for callers who want even the left menu gone. Tests: rewrite `hide_menu_right.spec.ts`. The "readonly pad hides .menu_right by default" assertion is inverted; a new test confirms `?showMenuRight=false` still hides on readonly pads. Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
294158e773
|
harden: reject USER_CHANGES inserts without an author attribute (#7773)
* harden: reject USER_CHANGES inserts without an author attribute Insert ops MUST carry the author attribute reference so that pad.atext.text and pad.atext.attribs stay in lock-step. An accepted insert with empty attribs would grow text without contributing matching attribute markers, leaving the stored AText in a state where the two iterables disagree on length when reconstructed. Downstream clients then fail reconciliation in ace2_inner.ts:setDocAText with 'mismatch error setting raw text in setDocAText' on every subsequent pad load — making the affected pad effectively unloadable until manually repaired. This commit adds a single defensive check inside the existing per-op validation loop in handleUserChanges: when an op is a '+' (insert) and its attribs string doesn't yield an 'author' entry via AttributeMap.fromString, reject with badChangeset. The check piggybacks on the wireApool that was already constructed for the prior author-match validation, so no extra parsing. Test fixtures in messages.ts were updated to send proper author-attributed inserts plus the matching apool (mirroring what the JS web client always does). A new regression test 'insert without author attribute is rejected' locks in the new behaviour. * harden: also close the HTTP API / plugin path via stable system author The first commit closed the socket.io USER_CHANGES hole. This commit closes the parallel path through Pad.spliceText (used by API.setText, API.appendText, the import flow, and plugins like ep_post_data) where an unattributed insert would otherwise produce a malformed AText. Approach: instead of REJECTING (which would break ep_post_data and many existing tests that call setText/appendText without an authorId), substitute a stable system author when none is provided. The resulting changeset is properly attributed, the AText stays well-formed, and existing callers continue to work unchanged. Plugins that want named author attribution should still pass an explicit authorId (e.g., one allocated via authorManager.createAuthor). Pad.SYSTEM_AUTHOR_ID = 'a.etherpad-system' — a stable identifier that appears in the pad's attribute pool when internal callers (HTTP API, plugins, server-side imports) write text without naming an author. The existing 'attribute changes by another author' protections still apply to socket.io USER_CHANGES paths — a remote client can't impersonate the system author for inserts (their session author check fires first). Test: - Pad.ts spec adds 'spliceText with empty authorId attributes to the system author' — verifies pad text lands AND the pool contains the system-author binding. Existing tests that pass an authorId are unaffected. * harden: reject USER_CHANGES that would strand the trailing newline Etherpad's pad text always ends with '\n'. _handleUserChanges previously appended a separate `nlChangeset` correction revision whenever the applied USER_CHANGES left the pad without a trailing '\n'. The stored pad ended up well-formed, but the FIRST NEW_CHANGES broadcast (the malformed user revision itself) reached browsers BEFORE the correction did, and applyToAttribution's MergingOpAssembler aborts with "line assembler not finished" on a non-'\n'-terminated doc — the watching browser session then dropped the changeset and any subsequent edits silently no-op'd until the user reloaded. Replace the silent auto-correction with an explicit reject. Compute `applyToText(rebasedChangeset, prevText)` before appendRevision; if the result doesn't end with '\n', throw -> badChangeset disconnect. Clients must emit USER_CHANGES whose application preserves the invariant — this matches what the JS web client already does and forces non-JS clients (etherpad-pad, third-party integrations) to surface their bugs in their own logs instead of stranding the trailing newline in pad revision history. Also fixes a latent retransmission-detection bug surfaced by this PR's author-attrib changes: moveOpsToNewPool renumbers `*N` references to whatever slot the pad pool assigns, which can differ from the wire form's slot. Comparing the raw client wire against the stored revision form (`changeset === c`) then misses legitimate retransmissions and the same edit gets duplicated. Snapshot the post-pool-mapping form (`canonicalCs`) and compare that against `c` instead. Backend test additions: - 'changeset that would strand the trailing \\n is rejected' covers the new rejection path with wire `Z:6>1|1=6*0+1$X` against `hello\n`. - handleMessageSecurity test now captures roSocket's own authorId and uses it in the apool sent through roSocket, because the prior PR commit made `*0` referencing the wrong author a hard reject. All 1130 backend tests pass. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * chore: bump etherpad-cli-client to ^4.0.3 4.0.3 sends author-attributed inserts and preserves the trailing newline, complying with this PR's tightened USER_CHANGES validation. The rate-limit CI workflow drives the test pad via this client, so without the bump the new server-side rejects fire on the very first \`pad.append()\` and the rate-limit disconnect never gets a chance to arrive — testlimits.sh exits 0 instead of 1 and the rate-limit job fails with "ratelimit was not triggered when sending every 99 ms". Refs ether/etherpad-cli-client#131 * harden: reject USER_CHANGES that name the reserved system author The session-author equality check already rejects wire `*N` that names a different real user, but `a.etherpad-system` is server- internal — it's only used when spliceText / setText is called with an empty authorId from HTTP API or plugin paths. A wire op that names it is either a confused client or an attempt to launder edits through a reserved attribution slot. Refuse. Backend test 'insert claiming the reserved system author is rejected' locks in the new behavior with wire `Z:1>5*0+5$hello` plus an apool that maps slot 0 to `a.etherpad-system`. All 1131 backend tests pass. Inline literal `'a.etherpad-system'` rather than importing the constant from `Pad.SYSTEM_AUTHOR_ID` — `require('../db/Pad')` at PadMessageHandler module scope returned a partially-initialized class via the padManager circular path, leaving the static-field access undefined at runtime. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
2a865a3a4a
|
feat(admin): show "requires newer Etherpad" when installing incompatible plugin (#7763) (#7771)
* feat(admin): show "requires newer Etherpad" when installing incompatible plugin (#7763) Old admins on out-of-date Etherpad installations get no feedback when they click Install on a plugin that needs a newer core. live-plugin-manager doesn't honor engines.node, and the admin UI dropped the error payload that adminplugins.ts already emits. This wires up an end-to-end signal: - pluginEngineCheck.ts: pure helper comparing a plugin's engines.node range against process.version, with a stable error code (PLUGIN_REQUIRES_NEWER_ETHERPAD) and a message that avoids leaking the Node-version implementation detail. Unparseable ranges fall through as compatible so the preflight is opportunistic, not a gate. 8 unit tests cover the happy + edge paths. - installer.ts: install() now best-effort fetches the published plugin's engines.node from npmjs.org, runs the preflight, and short-circuits with the typed error before invoking live-plugin-manager. Also wraps the body in try/catch so the error reaches the socket callback (it was silently dropped on every install failure today, including network errors). - HomePage.tsx: surfaces finished:install.error as a toast, using a dedicated i18n key when the code is PLUGIN_REQUIRES_NEWER_ETHERPAD and a generic fallback otherwise. - en.json: two new strings, parameterized by {{plugin}} and {{error}}. The message admins see is intentionally about Etherpad, not Node — upgrading Etherpad pulls the Node requirement along with it. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(plugins): don't restart server when every install in the batch failed Qodo PR review on #7771 caught a side effect introduced earlier in this PR. Before this PR, install() never invoked its callback on error, so a failed install left the task counter inflated and onAllTasksFinished() never ran — masking, but not fixing, the bug. Once install() correctly propagates errors to its cb, the counter hits zero on failure too, and onAllTasksFinished() fires hooks.aCallAll('restartServer'). A no-op preflight rejection (EngineIncompatibleError) would then disconnect every connected pad. Fix: extract wrapTaskCb + task state into InstallerTaskQueue and track whether at least one task in the current batch succeeded. Only fire the "all finished" side effect when something actually changed. Failed-only batches do nothing. Seven unit tests cover the matrix: single success, single failure (the regression), mixed batch (still restarts), all-failed batch, batch reset, null cb, two-task drain. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(plugins): time-bound the engines preflight registry fetch Qodo PR review on #7771 flagged that fetchPluginEnginesNode awaits fetch() with no timeout. A stalled DNS lookup or hung connection to registry.npmjs.org would block install() forever — the finished:install socket event would never fire and the admin UI would stay spinning with no error to surface. Wrap the fetch in AbortSignal.timeout(5000). On any failure (network, HTTP error, abort) fetchPluginEnginesNode returns undefined, which the preflight then treats as "no engines info → compatible," so a slow registry never blocks an install that would otherwise succeed. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
924059257d
|
fix(a11y): name role=toolbar regions, hide linemetricsdiv from AT (#7255) (#7777)
* fix(a11y): name role=toolbar regions, hide linemetricsdiv from AT (#7255) Two regressions called out in the 2026-05-16 follow-up on #7255, after the firefox accessibility inspector flagged them: (1) The "Ether X" announcement between the editor and chat button was the outer ace iframe (titled "Ether") plus a single 'x' text leaf the renderer appends to outerdocbody for line-height measurement (linemetricsdiv in ace.ts). Add aria-hidden=true on creation so AT skips the measurement node entirely. Same approach we used for sidediv in PR #7758. (2) The two formatting/actions <ul role="toolbar"> regions and the history-mode role=toolbar div had no accessible name. Lighthouse + the firefox a11y panel both flagged this. Putting data-l10n-id directly on the <ul> would either destroy its <li> children (textContent branch) or not populate aria-label (the html10n auto-aria-label code path skips non-form-control elements), and a hidden <span> child inside the <ul> would be invalid HTML. Solution: three visually-hidden <span> labels sitting just before #editbar, each carrying data-l10n-id for translation, referenced from the toolbars via aria-labelledby. Apply the same treatment to .show-more-icon-btn, whose aria-label was previously hardcoded English (no data-l10n-id, so untranslated). Adds Playwright assertions for linemetricsdiv aria-hidden and the resolved accessible-name text of each toolbar. Updates the existing show-more test to expect aria-labelledby (it previously asserted hardcoded English aria-label). Refs #7255 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(a11y): reuse translated history-controls label key (Qodo PR review) Qodo flagged: adding `aria-labelledby="editbar-history-label"` on #history-controls overrode the `aria-label` that pad_mode.ts sets from `pad.historyMode.controlsLabel`. That key is already translated in multiple locales (en/de/nl/...); the new `pad.editor.toolbar.history` key was English-only, so non-English users would regress from a localized history-toolbar name to the English fallback. Point the hidden label span at the existing translated key instead of minting a new one, drop the new key from en.json, and update the Playwright expectation to match the translated string ("Pad history controls"). The aria-label that pad_mode.ts still writes to #history-controls is now redundant (aria-labelledby wins) but harmless, and leaving it preserves the runtime relocalization path. Refs ether/etherpad#7777 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(a11y): mark toolbar li/a wrappers presentational (Lighthouse, #7255) Lighthouse's axe-core `listitem` rule fires on every toolbar button because role="toolbar" on the <ul> overrides its implicit role="list", leaving the <li> children "orphaned" by axe's heuristic. Murphy's 2026-05-16 follow-up on #7255 attached the Chrome DevTools Lighthouse panel screenshot of this exact failure. Marking the <li>+<a> wrappers role="presentation" tells axe-core they are layout scaffolding for the toolbar role, while the inner <button> keeps its semantics for AT. Same treatment for SelectButton's <li> wrapper. The Separator's <li> also gets aria-hidden=true so AT does not announce an empty list item between toolbar buttons. CSS and JS selectors that still target `.toolbar ul li` continue to work — role="presentation" only affects the accessibility tree, not the DOM tree. No visual or behavioral change for sighted users. Adds a Playwright spec that walks every rendered toolbar <li>/<a> and asserts role="presentation" so future toolbar.ts tweaks can't silently re-introduce the Lighthouse failure. Refs ether/etherpad#7255 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(a11y): label #online_count for AT (#7255 - "number next to the user icon") Murphy's 2026-05-16 follow-up cut off mid-bullet ("It's not clear what the number next to the …"). Best guess: the user-count badge in the showusers toolbar button. Currently it exposes a bare digit to AT — "5" with no context — because the visible badge text is also the entire accessible content. Append a localized aria-label generated from a new pad.userlist.onlineCount key (plural macro for one / other) whenever the count updates, so AT announces "5 connected users" instead of the bare digit. Add role=status and aria-live=polite so the count change is announced inline without forcing the user to refocus the button. Visible badge digit unchanged. html10n.get is null-safe (falls back to an English template so AT never gets back "undefined" before the locale bundle has loaded). Adds a Playwright spec verifying role/aria-live and that the aria-label contains "connected user". Refs ether/etherpad#7255 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(a11y): tighten #online_count + plugin-emitted toolbar <li>s (#7255) Two small follow-ups on top of the toolbar/online-count work: (1) #online_count had no accessible label on solo-author pads. updateNumberOfOnlineUsers — which writes the localized aria-label — only fires on userJoin/userLeave/status change, never on initial single-author load. Call it at the end of init() so the badge ships with its label on first paint. Also bind html10n's 'localized' event so non-English users get the translated label after the locale bundle arrives (matches the keyboard-hint / history-toolbar pattern). Harden the Playwright spec to use polling toHaveAttribute instead of one-shot getAttribute. (2) Sweep role="presentation" onto plugin-emitted toolbar <li>s in pad_editbar.ts init(). Core's toolbar.ts emits its <li>s with the role already, but plugins (ep_headings2, ep_align, ep_font_*, ep_print, ...) ship their own editbarButtons.ejs templates that emit <li> directly, so Lighthouse's listitem rule kept firing on the "with plugins" test runs. Runtime sweep covers anything in the editbar at init time, no plugin coordination needed. Refs ether/etherpad#7255 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
04045fe532
|
Roll Node.js floor back to >= 24 (Active LTS) — closes #7779 (#7781)
* Roll Node.js floor back to >= 24 (Active LTS) Closes #7779. #7779 originally proposed bumping past the Node 25 stop-gap to Node 26. After re-checking the release schedule, the cleaner LTS target is actually Node 24: - Node 24 (Krypton) is currently in Active LTS, supported until ~May 2028. - Node 25 hit end-of-life on April 10 2026 — the floor merged in #7752 / #7749 / #7754 a day ago ships an already-EOL major. - Node 26 was released May 5 2026 and does not enter Active LTS until October 2026. So this PR reverts the Node 25 ratchet from those three PRs and lands on Node 24 — Etherpad's runtime floor stays on a supported LTS for the next ~2 years. Runtime / infra - `package.json` + `src/package.json`: `engines.node` `>=25.0.0` -> `>=24.0.0` - `bin/functions.sh`, `bin/installer.sh`, `bin/installer.ps1`: `REQUIRED_NODE_MAJOR` 25 -> 24 - `Dockerfile`: `node:25-alpine` -> `node:24-alpine` (both stages). Corepack-via-npm workaround is intentionally kept: it works on Node 24 (which still ships corepack) and on Node 25+ (which doesn't), so the same recipe survives the next LTS bump without churn. Comments reworded accordingly. - `snap/snapcraft.yaml`: pinned `NODE_VERSION` 25.9.0 -> 24.15.0; design notes + corepack comment adjusted - `packaging/nfpm.yaml`: `nodejs (>= 25)` -> `nodejs (>= 24)` in top-level depends + deb/rpm overrides - `packaging/bin/etherpad`: comment matches the new pin - `packaging/README.md`: build prereqs + apt install snippet point at `node_24.x`; the long-stale "engines.node floor is 20" line is fixed while we're here - `.github/workflows/*.yml`: setup-node `node-version` 25 -> 24 across every workflow; backend / frontend-admin / upgrade matrices `[25]` -> `[24]` - `.github/workflows/deb-package.yml`: `NODE_MAJOR=25` + `node_25.x` smoke-test installer -> 24 - `bin/plugins/lib/npmpublish.yml`: 25 -> 24 (template propagates to the ~80 ether/* plugins via update-plugins workflow) Docs - `README.md`: install one-liner + Requirements -> Node.js >= 24 - `doc/npm-trusted-publishing.md`: runner requirement -> Node 24 - `doc/plugins.md` / `doc/plugins.adoc`: plugin metadata example `engines.node` -> `">=24.0.0"` @types/node is left at ^25.8.0 — newer type definitions cover Node 24 runtime fine and avoid an unnecessary lockfile churn. Companion homepage one-liner change to follow on ether/ether.github.com. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * docs(plugins): example engines.node = ">=22.0.0", not core's floor Plugin code is overwhelmingly ace-hook glue and rarely uses Node-version- specific APIs, so plugin engines.node should reflect the plugin's own requirements, not track core. Showing core's 24-floor in the example encouraged plugin authors to blindly copy a tighter pin than necessary and locked plugins out of being installable on older Etherpad/Node deployments. Use the most-recent Node LTS that has actually reached EOL (20 -> EOL April 2026) as the example floor, i.e. >=22. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
79f525b0a7
|
fix(a11y): action Qodo review on PR #7758 (#7764)
Three bugs and one test-flake risk that Qodo flagged on
|
||
|
|
986f139a61
|
feat(metrics): 3 Prometheus counters for scaling dive (#7756) (#7762)
* feat(metrics): expose 3 Prometheus counters for the scaling dive Per the spec section 6 of #7756: enables the load-test harness to attribute *where* time goes on the server, not just the gauge headline (CPU / event-loop / memory) the dive doc starts from. New /stats/prometheus rows: - etherpad_pad_users{padId} — gauge, derived from sessioninfos on each scrape. Lets the harness confirm the pad it points at actually has the expected concurrency. - etherpad_changeset_apply_duration_seconds — histogram observed inside handleUserChanges. Separates "apply path is slow" from "fan-out is slow" when latency rises. - etherpad_socket_emits_total{type} — counter at the broadcast emit sites (handleCustomObjectMessage, handleCustomMessage, sendChatMessageToPadClients) and inside the NEW_CHANGES per-socket loop in updatePadClients. Bucketed by message type so the harness can measure the amplification factor of each lever (especially the fan-out batching lever). Metric handles live in a new prom-instruments.ts module rather than in prometheus.ts itself, so PadMessageHandler can import the recording helpers without creating a circular dependency (prometheus.ts already requires PadMessageHandler). Tests: smoke test verifies recordSocketEmit + recordChangesetApply move the underlying counters/histogram. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(metrics): address Qodo review — flag-gate, scope histogram, bound label cardinality Three issues raised on the initial PR: 1. **Feature flag.** Per project compliance rule, new features must be behind a flag and disabled by default. Adds `settings.scalingDiveMetrics` (default `false`). When off, recordSocketEmit() / recordChangesetApply() short-circuit to no-ops and the metrics are never even registered with the Prometheus register. Enable only when running the ether/etherpad-load-test scaling-dive harness. 2. **Histogram scope.** Previously the etherpad_changeset_apply_duration_seconds timer wrapped the whole handleUserChanges() body — including `await exports.updatePadClients(pad)` — so the histogram measured apply+fan-out, defeating its stated purpose. Now stopped immediately after the apply work (`assert.equal(...rev, r)`), before the ACCEPT_COMMIT socket emit and the updatePadClients call. Failed applies deliberately don't observe so the success-path distribution stays clean. 3. **Label cardinality.** handleCustomMessage was passing the user-supplied msgString (an HTTP-API param) directly as the `type` label value. A misbehaving API caller could grow prom-client's internal label map until OOM. Now bucketed against a known-types allowlist; anything outside it lands in `other`. Tests updated: 5/5 — covers happy path, "other" bucketing of unknown/unsafe labels, and that the flag-disabled state is a true no-op. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
21e1ae2fa3
|
security: allow integrator sessionID cookie to be HttpOnly (#7045) (#7755)
* security: allow integrator sessionID cookie to be HttpOnly (#7045) The integrator-set sessionID cookie was forced to be non-HttpOnly because Etherpad's own client JS read it via document.cookie and forwarded it in the socket.io CLIENT_READY payload, exposing it to XSS. Mirror the GDPR PR3 author-token migration: read sessionID from the socket.io handshake's Cookie header in PadMessageHandler.handleClientReady, falling back to the legacy message-level field with a one-time deprecation warning per socket. Drop the client-side Cookies.get('sessionID') reads in pad.ts and timeslider.ts so the field is no longer sent by current clients. Existing integrators that set sessionID without HttpOnly keep working unchanged; the field on the message becomes optional and integrators should now mark the cookie HttpOnly; Secure; SameSite=Lax. Closes #7045 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(security): treat undecodable handshake cookies as absent (Qodo #7755) decodeURIComponent() throws URIError on malformed values like `%ZZ`. The unguarded call in PadMessageHandler.handleClientReady's readCookie() let a single bad cookie abort CLIENT_READY for that socket, allowing unauthenticated peers to spam server error logs and lock themselves out of pads. Catch URIError and treat the value as absent so the legacy message-level field still serves as a fallback. Other error classes still propagate. Add a backend test that asserts a `sessionID=%ZZ` cookie no longer aborts the handshake. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
72917f4457
|
build(deps-dev): bump the dev-dependencies group with 3 updates (#7759)
Bumps the dev-dependencies group with 3 updates: [eslint](https://github.com/eslint/eslint), [vite-plugin-babel](https://github.com/owlsdepartment/vite-plugin-babel) and [oxc-minify](https://github.com/oxc-project/oxc/tree/HEAD/napi/minify). Updates `eslint` from 10.3.0 to 10.4.0 - [Release notes](https://github.com/eslint/eslint/releases) - [Commits](https://github.com/eslint/eslint/compare/v10.3.0...v10.4.0) Updates `vite-plugin-babel` from 1.7.0 to 1.7.1 - [Commits](https://github.com/owlsdepartment/vite-plugin-babel/commits) Updates `oxc-minify` from 0.130.0 to 0.131.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.131.0/napi/minify) --- updated-dependencies: - dependency-name: eslint dependency-version: 10.4.0 dependency-type: direct:development update-type: version-update:semver-minor dependency-group: dev-dependencies - dependency-name: vite-plugin-babel dependency-version: 1.7.1 dependency-type: direct:development update-type: version-update:semver-patch dependency-group: dev-dependencies - dependency-name: oxc-minify dependency-version: 0.131.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> |
||
|
|
7a1d9519f6
|
fix(a11y): skip-to-content link + hide line numbers from screen readers (#7255) (#7758)
* fix(a11y): skip-to-content link + hide line numbers from AT (#7255) PR #7451 landed editor-region labelling but didn't address two remaining screen-reader complaints from the original report: 1. No way to bypass the toolbar. Murphy noted that screen-reader users had to swipe through ~18 toolbar buttons to reach the editor (WCAG 2.4.1 Bypass Blocks). Adds a skip link as the first child of <body>, hidden offscreen until keyboard focus reveals it. Click handler in pad.ts routes through ace_focus so the inner contenteditable actually receives focus — a plain href="#editorcontainer" anchor only scrolls. 2. Line numbers read individually as "1, 2, 3, ...". The sidediv is visual scaffolding for sighted users; it carries no useful information for AT. Adds aria-hidden="true" on creation in ace.ts so screen readers skip it entirely. Also fixes two issues found while implementing this: - The Escape/Alt+F9 hint that PR #7451 added in the inner iframe's body was being wiped by Ace2Inner.init() (line splices manage body children). Move it to the inner <head> — aria-describedby resolves by ID anywhere in the same document. Localize the text via html10n.get('pad.editor.keyboardHint'). - Skip link and keyboard hint text are both routed through new locale keys (pad.editor.skipToContent, pad.editor.keyboardHint) so they translate via the existing html10n pipeline. Adds two Playwright assertions for the new behavior. Refs #7255 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(a11y): remove editor auto-focus so skip link is Tab-reachable PR #7758 added a skip-to-content link as the first body child, but on local testing it was unreachable via Tab from the URL bar — the standard WCAG 2.4.1 entry path. Root cause: postAceInit calls padeditor.ace.focus() on load, which moves focus into the editor iframe. Tab inside the editor inserts an indent character (handled by ace2_inner's key handler) rather than bubbling to the parent page, so the skip link sat in the DOM but no user could ever reach it. Drop the load-time auto-focus. Initial focus is now on <body>, and the first Tab focuses #skip-to-content as expected. Users now click or Tab into the editor; existing Escape → toolbar exit (PR #7451) and the new Enter-on-skip-link path both still work. Cost: the long-standing "start typing immediately on a fresh pad" UX goes away — one extra click to start editing. This matches the modern accessible default for rich text editors (Slack, Discord, Google Docs all require an explicit focus before accepting input) and removes a keyboard trap that violated WCAG 2.1.2 even after PR #7451's Escape escape hatch. Adds a Playwright assertion that fresh-page Tab focuses the skip link. Refs #7255 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * test: focus the editor before typing in three specs These three Playwright tests started failing after the autofocus removal in |
||
|
|
6895eb80c8
|
build(deps): bump js-cookie from 3.0.5 to 3.0.6 (#7760)
Bumps [js-cookie](https://github.com/js-cookie/js-cookie) from 3.0.5 to 3.0.6. - [Release notes](https://github.com/js-cookie/js-cookie/releases) - [Commits](https://github.com/js-cookie/js-cookie/commits) --- updated-dependencies: - dependency-name: js-cookie dependency-version: 3.0.6 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> |
||
|
|
e6eed050f0
|
build(deps): bump undici from 8.2.0 to 8.3.0 (#7761)
Bumps [undici](https://github.com/nodejs/undici) from 8.2.0 to 8.3.0. - [Release notes](https://github.com/nodejs/undici/releases) - [Commits](https://github.com/nodejs/undici/compare/v8.2.0...v8.3.0) --- updated-dependencies: - dependency-name: undici dependency-version: 8.3.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> |
||
|
|
80c385e657
|
fix(deb): keep plugin_packages in-tree so admin-installed plugins can resolve ep_etherpad-lite (#7750)
* fix(deb): keep plugin_packages in-tree to fix admin-installed plugins
The .deb postinstall symlinked /opt/etherpad/src/plugin_packages to
/var/lib/etherpad/plugin_packages so the etherpad user could install
plugins under ProtectSystem=strict. Node.js resolves symlinks to their
realpath before walking node_modules, so a plugin installed via the
admin UI lived under /var/lib/etherpad/... and could no longer reach
the bundled ep_etherpad-lite in /opt/etherpad/node_modules. Every
require('ep_etherpad-lite/...') in installed plugins (and the matching
esbuild client-bundle build) failed with MODULE_NOT_FOUND, etherpad
exited, and the systemd unit restart-looped (ether/ep_comments_page#416).
Keep plugin_packages as a real in-tree directory, make it (and its
.versions/ subdir) group-writable by etherpad like node_modules already
is, and add it to ReadWritePaths= in the unit. Migrate the contents of
any pre-existing /var/lib/etherpad/plugin_packages symlink target back
in-tree on upgrade.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* ci(deb): update assertions for in-tree plugin_packages + cover migration
The previous assertions in packaging/test-local.sh and the deb-package
workflow checked that /opt/etherpad/src/plugin_packages was a symlink to
/var/lib/etherpad/plugin_packages -- the layout this PR is removing.
They would have failed under the new postinst.
- Assert plugin_packages is a real directory (not a symlink), owned by
group etherpad with mode 2775, matching node_modules.
- After the happy-path /health check, simulate a pre-fix install by
recreating the symlink with a marker plugin, re-run the postinst via
dpkg-reconfigure, and assert the marker payload was migrated back
in-tree and the symlink is gone. Locks in regression coverage for the
upgrade path (ether/ep_comments_page#416).
* ci(deb): add ep_layout_trip_wire fixture to gate plugin_packages layout
Layout assertions alone don't actually exercise the failure mode from
ether/ep_comments_page#416: they confirm /opt/etherpad/src/plugin_packages
is a real directory but never load a plugin whose index.js does
require('ep_etherpad-lite/...') from the on-disk realpath.
Ship a tiny test plugin under packaging/test-fixtures/ep_layout_trip_wire
that exercises the four require() patterns from the bug report (eejs,
Settings, log4js, pad_utils) and emits a marker line from
expressCreateServer. Both packaging/test-local.sh and the deb-package
workflow now stage it into plugin_packages/.versions/, wire up the
toplevel + node_modules symlinks live-plugin-manager would create,
list it in installed_plugins.json, restart etherpad, and assert:
* marker line appears in the journal (every require resolved), and
* no "Cannot find module 'ep_etherpad-lite" appears anywhere.
Verified locally that the fixture loads under the in-tree layout
(marker present) and fails under a symlinked-out-of-tree layout
(marker absent, MODULE_NOT_FOUND in the log) -- so the gate catches
the regression in both directions.
* fix(deb): clean up runtime plugin artifacts on purge
With plugin_packages now living under /opt/etherpad/src/plugin_packages,
admin-installed plugins land in dpkg-unmanaged paths (the .versions/
stage that live-plugin-manager populates plus matching ep_* symlinks
under src/node_modules/). dpkg --purge would leave them behind because
the manifest never recorded them.
In postremove's purge branch: explicitly rm -rf plugin_packages and any
runtime ep_* symlinks in node_modules, then rm -rf the whole APP_DIR
as belt-and-braces against other runtime drift. Verified in a sandbox
that a staged ep_runtime@1.0.0 + node_modules/ep_runtime symlink are
both gone after purge runs.
Add post-purge assertions to both packaging/test-local.sh and the
deb-package workflow: /opt/etherpad/src/plugin_packages and
/var/lib/etherpad must not exist after dpkg --purge.
Addresses Qodo PR #7750 review item 3 (\"Purge cleanup misses
plugins\", Reliability).
---------
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
||
|
|
83a1bcccd8
|
build(deps): bump mssql from 12.5.2 to 12.5.3 (#7743)
Bumps [mssql](https://github.com/tediousjs/node-mssql) from 12.5.2 to 12.5.3. - [Release notes](https://github.com/tediousjs/node-mssql/releases) - [Changelog](https://github.com/tediousjs/node-mssql/blob/master/CHANGELOG.txt) - [Commits](https://github.com/tediousjs/node-mssql/compare/v12.5.2...v12.5.3) --- updated-dependencies: - dependency-name: mssql dependency-version: 12.5.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> |