Skip to content

fix: flush pending changesets immediately after reconnect#2

Open
deepshekhardas wants to merge 130 commits into
developfrom
fix/pending-changeset-reconnect
Open

fix: flush pending changesets immediately after reconnect#2
deepshekhardas wants to merge 130 commits into
developfrom
fix/pending-changeset-reconnect

Conversation

@deepshekhardas
Copy link
Copy Markdown
Owner

@deepshekhardas deepshekhardas commented May 10, 2026

Summary

Fixes issue where edits made while disconnected sat as pending local changesets and were only transmitted when the user made another edit.

Changes

  • When transitions from to (after reconnect and server revisions are applied), call to flush any queued local changes.

Why this fix

After reconnecting, called and but never called . The fix in ensures pending changes are flushed once all server revisions have been applied.

Fixes ether#5108


Summary by cubic

Flushes any offline edits immediately after reconnect by sending pending changes as soon as server revisions finish applying. This fixes pending changesets that only synced after making a new edit.

  • New Features

    • Added cookie name prefixing via settings.cookie.prefix to avoid collisions with other apps (backward compatible; defaults to empty).
    • Session store now periodically cleans up expired/stale sessions (configurable via settings.cookie.sessionCleanup).
  • Bug Fixes

    • Timeslider no longer breaks on identity changesets; playback and slider position advance reliably.
    • PageDown moves the caret by a full page; improved clamping for selection.
    • Pad deletion waits for server confirmation before navigating; server awaits pad.remove(); shows proper error if not allowed.
    • appendText API attributes inserted text to the supplied author so colors and author lists are correct.
    • Lists: fixed numbering after bullets or indents, improved continuity, and ensured indented text exports without bullet markers; batching avoids O(n²) renumbering.
    • API: POST requests with JSON bodies no longer hang; guarded against parameter pollution; safer method handling.
    • Stability: ignore errors from browser extensions during init and global error handling; correct numConnectedUsers for the joining user.
    • i18n: URL lang reliably overrides defaults, custom strings load without stale cache, language list sorted by native name, and window._() always available for plugins.
    • Accessibility and input: Escape exits the editor to the toolbar (without trapping), improved screen reader roles/aria, removed per-line aria-live to stop character echo, popups fit small screens, and compose/dead-key input no longer eats spaces on Firefox/Linux.

Written for commit f430da5. Summary will update on new commits.

translatewiki and others added 30 commits February 19, 2026 13:03
Bumps [jsdom](https://github.com/jsdom/jsdom) from 28.0.0 to 28.1.0.
- [Release notes](https://github.com/jsdom/jsdom/releases)
- [Changelog](https://github.com/jsdom/jsdom/blob/main/Changelog.md)
- [Commits](jsdom/jsdom@28.0.0...28.1.0)

---
updated-dependencies:
- dependency-name: jsdom
  dependency-version: 28.1.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Bumps [underscore](https://github.com/jashkenas/underscore) from 1.13.7 to 1.13.8.
- [Commits](https://github.com/jashkenas/underscore/commits)

---
updated-dependencies:
- dependency-name: underscore
  dependency-version: 1.13.8
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Bumps [oidc-provider](https://github.com/panva/node-oidc-provider) from 9.6.0 to 9.6.1.
- [Release notes](https://github.com/panva/node-oidc-provider/releases)
- [Changelog](https://github.com/panva/node-oidc-provider/blob/main/CHANGELOG.md)
- [Commits](panva/node-oidc-provider@v9.6.0...v9.6.1)

---
updated-dependencies:
- dependency-name: oidc-provider
  dependency-version: 9.6.1
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
basically copy/pasting contributing.md but gives qodo a reference point, I'd of used a sym link but I don't know how well that would have worked so for now lets just add some cruft and see what happens..
* docs: add AGENTS.MD for AI and developer guidance

* docs: update project name from Etherpad Lite to Etherpad

* docs: fix incorrect test directory path in AGENTS.MD

* docs: correct test stack description in AGENTS.MD (Mocha is primary)

* docs: fix incorrect easysync documentation path in AGENTS.MD

* chore: add .pr_agent.toml to enable automatic PR review/description on push

* docs: remove nodejs version from README and AGENTS.MD (prefer package.json)

* docs: standardize all indentation to 2 spaces

* chore: update src/package.json node engine to match root (>=20.0.0)
Bumps [docker/login-action](https://github.com/docker/login-action) from 3 to 4.
- [Release notes](https://github.com/docker/login-action/releases)
- [Commits](docker/login-action@v3...v4)

---
updated-dependencies:
- dependency-name: docker/login-action
  dependency-version: '4'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Bumps [docker/build-push-action](https://github.com/docker/build-push-action) from 6 to 7.
- [Release notes](https://github.com/docker/build-push-action/releases)
- [Commits](docker/build-push-action@v6...v7)

---
updated-dependencies:
- dependency-name: docker/build-push-action
  dependency-version: '7'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Bumps [docker/metadata-action](https://github.com/docker/metadata-action) from 5 to 6.
- [Release notes](https://github.com/docker/metadata-action/releases)
- [Commits](docker/metadata-action@v5...v6)

---
updated-dependencies:
- dependency-name: docker/metadata-action
  dependency-version: '6'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Bumps [docker/setup-buildx-action](https://github.com/docker/setup-buildx-action) from 3 to 4.
- [Release notes](https://github.com/docker/setup-buildx-action/releases)
- [Commits](docker/setup-buildx-action@v3...v4)

---
updated-dependencies:
- dependency-name: docker/setup-buildx-action
  dependency-version: '4'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Bumps [express-rate-limit](https://github.com/express-rate-limit/express-rate-limit) from 8.2.1 to 8.2.2.
- [Release notes](https://github.com/express-rate-limit/express-rate-limit/releases)
- [Commits](express-rate-limit/express-rate-limit@v8.2.1...v8.2.2)

---
updated-dependencies:
- dependency-name: express-rate-limit
  dependency-version: 8.2.2
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Bumps [docker/setup-qemu-action](https://github.com/docker/setup-qemu-action) from 3 to 4.
- [Release notes](https://github.com/docker/setup-qemu-action/releases)
- [Commits](docker/setup-qemu-action@v3...v4)

---
updated-dependencies:
- dependency-name: docker/setup-qemu-action
  dependency-version: '4'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
…er#7358)

Bumps the dev-dependencies group with 16 updates:

| Package | From | To |
| --- | --- | --- |
| [@types/formidable](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/formidable) | `3.4.6` | `3.5.0` |
| [@types/jquery](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/jquery) | `3.5.33` | `4.0.0` |
| [@types/jsdom](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/jsdom) | `27.0.0` | `28.0.0` |
| [@types/node](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/node) | `25.2.3` | `25.3.5` |
| [@types/supertest](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/supertest) | `6.0.3` | `7.2.0` |
| [@types/whatwg-mimetype](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/whatwg-mimetype) | `3.0.2` | `5.0.0` |
| [eslint](https://github.com/eslint/eslint) | `10.0.0` | `10.0.3` |
| [sinon](https://github.com/sinonjs/sinon) | `21.0.1` | `21.0.2` |
| [@typescript-eslint/eslint-plugin](https://github.com/typescript-eslint/typescript-eslint/tree/HEAD/packages/eslint-plugin) | `8.55.0` | `8.56.1` |
| [@typescript-eslint/parser](https://github.com/typescript-eslint/typescript-eslint/tree/HEAD/packages/parser) | `8.55.0` | `8.56.1` |
| [eslint-plugin-react-refresh](https://github.com/ArnaudBarre/eslint-plugin-react-refresh) | `0.5.0` | `0.5.2` |
| [i18next](https://github.com/i18next/i18next) | `25.8.8` | `25.8.16` |
| [lucide-react](https://github.com/lucide-icons/lucide/tree/HEAD/packages/lucide-react) | `0.564.0` | `0.577.0` |
| [react-hook-form](https://github.com/react-hook-form/react-hook-form) | `7.71.1` | `7.71.2` |
| [react-i18next](https://github.com/i18next/react-i18next) | `16.5.4` | `16.5.6` |
| [react-router-dom](https://github.com/remix-run/react-router/tree/HEAD/packages/react-router-dom) | `7.13.0` | `7.13.1` |


Updates `@types/formidable` from 3.4.6 to 3.5.0
- [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases)
- [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/formidable)

Updates `@types/jquery` from 3.5.33 to 4.0.0
- [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases)
- [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/jquery)

Updates `@types/jsdom` from 27.0.0 to 28.0.0
- [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases)
- [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/jsdom)

Updates `@types/node` from 25.2.3 to 25.3.5
- [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases)
- [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/node)

Updates `@types/supertest` from 6.0.3 to 7.2.0
- [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases)
- [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/supertest)

Updates `@types/whatwg-mimetype` from 3.0.2 to 5.0.0
- [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases)
- [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/whatwg-mimetype)

Updates `eslint` from 10.0.0 to 10.0.3
- [Release notes](https://github.com/eslint/eslint/releases)
- [Commits](eslint/eslint@v10.0.0...v10.0.3)

Updates `sinon` from 21.0.1 to 21.0.2
- [Release notes](https://github.com/sinonjs/sinon/releases)
- [Changelog](https://github.com/sinonjs/sinon/blob/main/docs/changelog.md)
- [Commits](sinonjs/sinon@v21.0.1...v21.0.2)

Updates `@typescript-eslint/eslint-plugin` from 8.55.0 to 8.56.1
- [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.56.1/packages/eslint-plugin)

Updates `@typescript-eslint/parser` from 8.55.0 to 8.56.1
- [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.56.1/packages/parser)

Updates `eslint-plugin-react-refresh` from 0.5.0 to 0.5.2
- [Release notes](https://github.com/ArnaudBarre/eslint-plugin-react-refresh/releases)
- [Changelog](https://github.com/ArnaudBarre/eslint-plugin-react-refresh/blob/main/CHANGELOG.md)
- [Commits](ArnaudBarre/eslint-plugin-react-refresh@v0.5.0...v0.5.2)

Updates `i18next` from 25.8.8 to 25.8.16
- [Release notes](https://github.com/i18next/i18next/releases)
- [Changelog](https://github.com/i18next/i18next/blob/master/CHANGELOG.md)
- [Commits](i18next/i18next@v25.8.8...v25.8.16)

Updates `lucide-react` from 0.564.0 to 0.577.0
- [Release notes](https://github.com/lucide-icons/lucide/releases)
- [Commits](https://github.com/lucide-icons/lucide/commits/0.577.0/packages/lucide-react)

Updates `react-hook-form` from 7.71.1 to 7.71.2
- [Release notes](https://github.com/react-hook-form/react-hook-form/releases)
- [Changelog](https://github.com/react-hook-form/react-hook-form/blob/master/CHANGELOG.md)
- [Commits](react-hook-form/react-hook-form@v7.71.1...v7.71.2)

Updates `react-i18next` from 16.5.4 to 16.5.6
- [Changelog](https://github.com/i18next/react-i18next/blob/master/CHANGELOG.md)
- [Commits](i18next/react-i18next@v16.5.4...v16.5.6)

Updates `react-router-dom` from 7.13.0 to 7.13.1
- [Release notes](https://github.com/remix-run/react-router/releases)
- [Changelog](https://github.com/remix-run/react-router/blob/main/packages/react-router-dom/CHANGELOG.md)
- [Commits](https://github.com/remix-run/react-router/commits/react-router-dom@7.13.1/packages/react-router-dom)

---
updated-dependencies:
- dependency-name: "@types/formidable"
  dependency-version: 3.5.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: dev-dependencies
- dependency-name: "@types/jquery"
  dependency-version: 4.0.0
  dependency-type: direct:development
  update-type: version-update:semver-major
  dependency-group: dev-dependencies
- dependency-name: "@types/jsdom"
  dependency-version: 28.0.0
  dependency-type: direct:development
  update-type: version-update:semver-major
  dependency-group: dev-dependencies
- dependency-name: "@types/node"
  dependency-version: 25.3.5
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: dev-dependencies
- dependency-name: "@types/supertest"
  dependency-version: 7.2.0
  dependency-type: direct:development
  update-type: version-update:semver-major
  dependency-group: dev-dependencies
- dependency-name: "@types/whatwg-mimetype"
  dependency-version: 5.0.0
  dependency-type: direct:development
  update-type: version-update:semver-major
  dependency-group: dev-dependencies
- dependency-name: eslint
  dependency-version: 10.0.3
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: dev-dependencies
- dependency-name: sinon
  dependency-version: 21.0.2
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: dev-dependencies
- dependency-name: "@typescript-eslint/eslint-plugin"
  dependency-version: 8.56.1
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: dev-dependencies
- dependency-name: "@typescript-eslint/parser"
  dependency-version: 8.56.1
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: dev-dependencies
- dependency-name: eslint-plugin-react-refresh
  dependency-version: 0.5.2
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: dev-dependencies
- dependency-name: i18next
  dependency-version: 25.8.16
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: dev-dependencies
- dependency-name: lucide-react
  dependency-version: 0.577.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: dev-dependencies
- dependency-name: react-hook-form
  dependency-version: 7.71.2
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: dev-dependencies
- dependency-name: react-i18next
  dependency-version: 16.5.6
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: dev-dependencies
- dependency-name: react-router-dom
  dependency-version: 7.13.1
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: dev-dependencies
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Bumps [openapi-backend](https://github.com/openapistack/openapi-backend) from 5.15.0 to 5.16.1.
- [Release notes](https://github.com/openapistack/openapi-backend/releases)
- [Commits](openapistack/openapi-backend@5.15.0...5.16.1)

---
updated-dependencies:
- dependency-name: openapi-backend
  dependency-version: 5.16.1
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Bumps [jose](https://github.com/panva/jose) from 6.1.3 to 6.2.1.
- [Release notes](https://github.com/panva/jose/releases)
- [Changelog](https://github.com/panva/jose/blob/main/CHANGELOG.md)
- [Commits](panva/jose@v6.1.3...v6.2.1)

---
updated-dependencies:
- dependency-name: jose
  dependency-version: 6.2.1
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Bumps [ejs](https://github.com/mde/ejs) from 4.0.1 to 5.0.1.
- [Release notes](https://github.com/mde/ejs/releases)
- [Changelog](https://github.com/mde/ejs/blob/main/RELEASE_NOTES_v4.md)
- [Commits](mde/ejs@v4.0.1...v5.0.1)

---
updated-dependencies:
- dependency-name: ejs
  dependency-version: 5.0.1
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Bumps [oidc-provider](https://github.com/panva/node-oidc-provider) from 9.6.1 to 9.7.0.
- [Release notes](https://github.com/panva/node-oidc-provider/releases)
- [Changelog](https://github.com/panva/node-oidc-provider/blob/main/CHANGELOG.md)
- [Commits](panva/node-oidc-provider@v9.6.1...v9.7.0)

---
updated-dependencies:
- dependency-name: oidc-provider
  dependency-version: 9.7.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Bumps [axios](https://github.com/axios/axios) from 1.13.5 to 1.13.6.
- [Release notes](https://github.com/axios/axios/releases)
- [Changelog](https://github.com/axios/axios/blob/v1.x/CHANGELOG.md)
- [Commits](axios/axios@v1.13.5...v1.13.6)

---
updated-dependencies:
- dependency-name: axios
  dependency-version: 1.13.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>
…r#7366)

Bumps the dev-dependencies group with 7 updates:

| Package | From | To |
| --- | --- | --- |
| [@types/node](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/node) | `25.3.5` | `25.4.0` |
| [vitest](https://github.com/vitest-dev/vitest/tree/HEAD/packages/vitest) | `4.0.18` | `4.1.0` |
| [@typescript-eslint/eslint-plugin](https://github.com/typescript-eslint/typescript-eslint/tree/HEAD/packages/eslint-plugin) | `8.56.1` | `8.57.0` |
| [@typescript-eslint/parser](https://github.com/typescript-eslint/typescript-eslint/tree/HEAD/packages/parser) | `8.56.1` | `8.57.0` |
| [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/tree/HEAD/packages/plugin-react) | `5.1.4` | `6.0.0` |
| [i18next](https://github.com/i18next/i18next) | `25.8.16` | `25.8.18` |
| [react-i18next](https://github.com/i18next/react-i18next) | `16.5.6` | `16.5.8` |


Updates `@types/node` from 25.3.5 to 25.4.0
- [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases)
- [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/node)

Updates `vitest` from 4.0.18 to 4.1.0
- [Release notes](https://github.com/vitest-dev/vitest/releases)
- [Commits](https://github.com/vitest-dev/vitest/commits/v4.1.0/packages/vitest)

Updates `@typescript-eslint/eslint-plugin` from 8.56.1 to 8.57.0
- [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.57.0/packages/eslint-plugin)

Updates `@typescript-eslint/parser` from 8.56.1 to 8.57.0
- [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.57.0/packages/parser)

Updates `@vitejs/plugin-react` from 5.1.4 to 6.0.0
- [Release notes](https://github.com/vitejs/vite-plugin-react/releases)
- [Changelog](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react/CHANGELOG.md)
- [Commits](https://github.com/vitejs/vite-plugin-react/commits/plugin-react@6.0.0/packages/plugin-react)

Updates `i18next` from 25.8.16 to 25.8.18
- [Release notes](https://github.com/i18next/i18next/releases)
- [Changelog](https://github.com/i18next/i18next/blob/master/CHANGELOG.md)
- [Commits](i18next/i18next@v25.8.16...v25.8.18)

Updates `react-i18next` from 16.5.6 to 16.5.8
- [Changelog](https://github.com/i18next/react-i18next/blob/master/CHANGELOG.md)
- [Commits](i18next/react-i18next@v16.5.6...v16.5.8)

---
updated-dependencies:
- dependency-name: "@types/node"
  dependency-version: 25.4.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: dev-dependencies
- dependency-name: vitest
  dependency-version: 4.1.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: dev-dependencies
- dependency-name: "@typescript-eslint/eslint-plugin"
  dependency-version: 8.57.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: dev-dependencies
- dependency-name: "@typescript-eslint/parser"
  dependency-version: 8.57.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: dev-dependencies
- dependency-name: "@vitejs/plugin-react"
  dependency-version: 6.0.0
  dependency-type: direct:development
  update-type: version-update:semver-major
  dependency-group: dev-dependencies
- dependency-name: i18next
  dependency-version: 25.8.18
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: dev-dependencies
- dependency-name: react-i18next
  dependency-version: 16.5.8
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: dev-dependencies
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Bumps [actions/upload-artifact](https://github.com/actions/upload-artifact) from 6 to 7.
- [Release notes](https://github.com/actions/upload-artifact/releases)
- [Commits](actions/upload-artifact@v6...v7)

---
updated-dependencies:
- dependency-name: actions/upload-artifact
  dependency-version: '7'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Bumps [esbuild](https://github.com/evanw/esbuild) from 0.27.3 to 0.27.4.
- [Release notes](https://github.com/evanw/esbuild/releases)
- [Changelog](https://github.com/evanw/esbuild/blob/main/CHANGELOG.md)
- [Commits](evanw/esbuild@v0.27.3...v0.27.4)

---
updated-dependencies:
- dependency-name: esbuild
  dependency-version: 0.27.4
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Bumps [express-rate-limit](https://github.com/express-rate-limit/express-rate-limit) from 8.2.2 to 8.3.1.
- [Release notes](https://github.com/express-rate-limit/express-rate-limit/releases)
- [Commits](express-rate-limit/express-rate-limit@v8.2.2...v8.3.1)

---
updated-dependencies:
- dependency-name: express-rate-limit
  dependency-version: 8.3.1
  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>
…r#7368)

Bumps the dev-dependencies group with 3 updates: [@types/node](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/node), [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/tree/HEAD/packages/plugin-react) and [vite-plugin-static-copy](https://github.com/sapphi-red/vite-plugin-static-copy).


Updates `@types/node` from 25.4.0 to 25.5.0
- [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases)
- [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/node)

Updates `@vitejs/plugin-react` from 6.0.0 to 6.0.1
- [Release notes](https://github.com/vitejs/vite-plugin-react/releases)
- [Changelog](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react/CHANGELOG.md)
- [Commits](https://github.com/vitejs/vite-plugin-react/commits/plugin-react@6.0.1/packages/plugin-react)

Updates `vite-plugin-static-copy` from 3.2.0 to 3.3.0
- [Release notes](https://github.com/sapphi-red/vite-plugin-static-copy/releases)
- [Changelog](https://github.com/sapphi-red/vite-plugin-static-copy/blob/main/CHANGELOG.md)
- [Commits](https://github.com/sapphi-red/vite-plugin-static-copy/compare/vite-plugin-static-copy@3.2.0...vite-plugin-static-copy@3.3.0)

---
updated-dependencies:
- dependency-name: "@types/node"
  dependency-version: 25.5.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: dev-dependencies
- dependency-name: "@vitejs/plugin-react"
  dependency-version: 6.0.1
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: dev-dependencies
- dependency-name: vite-plugin-static-copy
  dependency-version: 3.3.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>
JohnMcLear and others added 28 commits April 5, 2026 10:14
)

PRs now run a minimal test matrix; full matrix runs on push to develop.

Changes:
- Backend tests: PRs test on Node 24 only (Linux). Windows tests only
  run on push to develop. Reduces from 12 to 2 jobs for PRs.
- Upgrade-from-latest-release: PRs test on Node 24 only (1 job vs 3).
- Frontend admin tests: PRs test on Node 24 only (1 job vs 3).

This reduces PR CI from ~25 jobs to ~10, preventing runner exhaustion
when multiple PRs are merged in succession. The full matrix (3 Node
versions × Linux + Windows) still runs on every push to develop.

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…her#7448)

* fix: add periodic cleanup of expired/stale sessions from database

SessionStore now runs a periodic cleanup (every hour, plus once on
startup) that removes:
- Sessions with expired cookies (expires date in the past)
- Sessions with no expiry that contain no data beyond the default
  cookie (the empty sessions that accumulate indefinitely per ether#5010)

Without this, sessions accumulated forever in the database because:
1. Sessions with no maxAge never got an expiry date
2. On server restart, in-memory expiration timeouts were lost
3. There was no mechanism to clean up sessions that were never
   accessed again

Fixes ether#5010

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: resolve TypeScript error for sessionStore.startCleanup()

Use a local variable for the SessionStore instance to avoid type
narrowing issues with the module-level Store|null variable.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: address Qodo review — chained timeouts, cleanup tests, docs

- Replace setInterval with chained setTimeout to prevent overlapping
  cleanup runs on large databases
- Store and clear startup timeout in shutdown() to prevent leaks
- Add .unref() on all timers so they don't delay process exit
- Fix misleading docstring — cleanup removes empty no-expiry sessions,
  not sessions older than STALE_SESSION_MAX_AGE_MS (removed unused const)
- Add 5 regression tests: expired sessions removed, empty sessions
  removed, sessions with data preserved, valid sessions preserved,
  shutdown cancels timer

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* feat: add cookie.sessionCleanup setting to control session cleanup

Session cleanup is now gated behind cookie.sessionCleanup (default
true). Admins who want to keep stale sessions can set this to false
in settings.json.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
When the cleanup timeout fires, check the in-memory exp.real before
reading from the DB. If touch() extended the expiry (but the old
timeout fires late, e.g. on slow CI), reschedule instead of reading
potentially stale cached data from the DB and destroying the session.

Also increased test expiry times so the "touch after eligible for
refresh" test isn't sensitive to event loop delays on slow machines.

Fixes flaky SessionStore test from ether#7448.

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: handle git submodule in Docker build

When etherpad-lite is checked out as a submodule, .git is a file
(gitlink) instead of a directory, so COPY .git/HEAD fails.  The
previous glob-based workaround (HEA[D], ref[s]) does not work with
buildah (podman-container-tools/buildah#5742).

Copy the whole .git entry instead and remove it in a RUN step when it
is a submodule gitlink file.  The .dockerignore already strips heavy
objects so the image size is unaffected for normal checkouts.

Fixes ether#6663

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* docs: document BUILD_ENV=copy bypass for builds without .git

Adds a comment explaining how to build from source tarballs or other
contexts where .git metadata is unavailable.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: use pnpm instead of npm in updatePlugins.sh

The script used npm outdated which doesn't work with pnpm workspaces,
and pnpm install which doesn't update existing packages. Changed to
pnpm outdated and pnpm update respectively.

Fixes ether#6670

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: scope plugin updates to ep_etherpad-lite and exclude core package

- Use --filter ep_etherpad-lite so pnpm operates on the right workspace
- Exclude ep_etherpad-lite from the plugin list
- Handle pnpm outdated exit codes correctly (returns 1 when outdated)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…lation (ether#7467)

* fix: correct readFileSync calls in LinkInstaller to fix plugin installation

pathToFileURL() was incorrectly wrapping paths passed to readFileSync(),
causing ENOENT errors that were silently caught. Using plain paths with
'utf-8' encoding fixes plugin dependency resolution.

Fixes ether#6811

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* test: add backend tests for LinkInstaller dependency resolution

Covers the readFileSync fix from the plugin installation bug where
pathToFileURL incorrectly wrapped file paths.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: only track dependency in map after successful setup

Previously dependenciesMap.set() ran after the catch block, marking
dependencies as tracked even when linking or package.json reading
failed. This blocked later cleanup via removeSubDependency().

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…er#7464)

* fix: RTL URL parameter rtl=false now correctly disables RTL mode

The rtl parameter callback only handled rtl=true (checkVal was 'true'),
so rtl=false was ignored and the layout stayed in RTL from the cookie.
Now accepts any value and sets rtlIsTrue = (val === 'true'). Also
always applies the RTL setting instead of only when true, so switching
from rtl=true to rtl=false takes effect.

Fixes ether#5559

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: only override RTL when explicitly set via URL/server config

The unconditional changeViewOption('rtlIsTrue', false) overwrote
cookie-persisted RTL preferences and language-direction defaults.
Track explicit setting with rtlIsExplicit flag so we only override
when the user or server actually specified an rtl value.

Adds regression tests for rtl=true, rtl=false, and cookie persistence.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: move RTL override into postAceInit to fix race condition

The RTL changeViewOption call was racing with padeditor.init() — the
async setViewOptions(initialViewOptions) at the end of init overwrote
the URL-param-based RTL setting. Moving it into postAceInit ensures
padeditor is fully initialized. Also switched tests to use Playwright
auto-retrying assertions for robustness.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: resolve Playwright test failures for RTL URL parameter

Three issues fixed:
- setCheckbox used .attr('checked') instead of .prop('checked'), so the
  JS checked property was never set and Playwright saw unchecked state
- html10n localized event overwrote RTL setting from URL params and
  cookie preferences; now skips override when either is active
- Server default padOptions.rtl:false was treated as explicit, overwriting
  cookie-persisted RTL; added fromUrl flag to distinguish URL from server

All 94 Playwright tests and 740 backend tests pass locally.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…xport (ether#7470)

* fix: preserve ordered list numbering across unordered list interruptions in export

When ordered lists were interrupted by unordered lists, each new <ol>
segment started at 1 instead of continuing the previous numbering.
Track running counts per indent level and emit start attributes.

Fixes ether#6471

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: respect explicit start attributes and reset counters per level

- line.start takes priority over counter-based continuation when present
- Counter is seeded from line.start to keep subsequent continuations aligned
- Counters for closed indent levels are cleared when list depth decreases

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: sort language dropdown alphabetically by native name

Languages in the settings dropdown were ordered by language code,
making it hard to find specific languages. Now sorted alphabetically
by their native display name.

Fixes ether#3263

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* test: verify language dropdown is sorted by native name

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: numbered list wrapped lines now indent correctly

Changed text-indent to padding-left for ordered list indentation.
text-indent only affects the first line, so wrapped text didn't
align with the numbered content above it.

Fixes ether#2581

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* test: verify numbered list uses padding-left instead of text-indent

Regression test for ether#2581. Verifies that ordered list items use
padding-left (which indents all lines including wrapped ones) rather
than text-indent (which only indents the first line).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: use correct path for connection diagnostics POST

The relative path '../ep/pad/connection-diagnostic-info' resolved
incorrectly in subdirectory setups. Use absolute path from the
application root.

Fixes ether#4191

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* test: verify connection diagnostics endpoint is reachable

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
ether#7473)

* fix: very old .etherpad imports could break import due to lack of author metadata, allow this now

* test: add regression tests for old .etherpad import without author

Tests that importing an old .etherpad export (circa 2014) where
revision records lack meta.author succeeds without error, and that
getRevisionAuthor returns '' for such revisions.

Covers the fix for ether#6785.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: prevent race condition in session cleanup timeout

When the cleanup timeout fires, check the in-memory exp.real before
reading from the DB. If touch() extended the expiry (but the old
timeout fires late, e.g. on slow CI), reschedule instead of reading
potentially stale cached data from the DB and destroying the session.

Also increased test expiry times so the "touch after eligible for
refresh" test isn't sensitive to event loop delays on slow machines.

Fixes flaky SessionStore test from ether#7448.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: dev mode entrypoint paths respect x-proxy-path header

In dev mode, the /watch/* script paths were hard-coded as absolute
paths without considering the x-proxy-path header used for subdirectory
reverse proxy setups. This caused 404s for the script tags when hosting
Etherpad on a subdirectory URL (e.g., /pad).

Now reads the x-proxy-path header from the request and prefixes the
entrypoint path, matching how admin.ts handles proxy paths.

Fixes ether#7137

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* test: make proxy path tests deterministic in production mode

Tests now verify entrypoint paths and x-proxy-path header handling
in production mode (where tests run) rather than conditionally
asserting only in dev mode.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* security: sanitize x-proxy-path header to prevent XSS

The header value was injected directly into <script src="...">
without sanitization. An attacker who can set request headers could
inject arbitrary HTML/JS. Now only allows path-safe characters.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…her#7474)

* fix: increase max socket.io message size to 10MB for large pastes

The default maxHttpBufferSize of 50KB caused socket.io to drop
connections when pasting >10,000 characters. Increased to 10MB which
safely accommodates large paste operations.

Fixes ether#4951

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* chore: reduce default maxHttpBufferSize to 1MB

10MB was too generous and creates a DoS vector. 1MB (socket.io's own
default) is sufficient for large pastes while limiting memory abuse.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The CJS compatibility block added in fd97532 only defined getters,
making settings properties read-only for plugins using require().
Plugins like ep_webrtc need to mutate settings (e.g. requireAuthentication)
in tests. Add setters so CJS consumers can write properties too.

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…7484)

Bumps [rate-limiter-flexible](https://github.com/animir/node-rate-limiter-flexible) from 10.0.1 to 11.0.0.
- [Release notes](https://github.com/animir/node-rate-limiter-flexible/releases)
- [Commits](animir/node-rate-limiter-flexible@v10.0.1...v11.0.0)

---
updated-dependencies:
- dependency-name: rate-limiter-flexible
  dependency-version: 11.0.0
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Bumps [lru-cache](https://github.com/isaacs/node-lru-cache) from 11.2.7 to 11.3.0.
- [Changelog](https://github.com/isaacs/node-lru-cache/blob/main/CHANGELOG.md)
- [Commits](isaacs/node-lru-cache@v11.2.7...v11.3.0)

---
updated-dependencies:
- dependency-name: lru-cache
  dependency-version: 11.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>
…r#7482)

Bumps the dev-dependencies group with 2 updates: [eslint](https://github.com/eslint/eslint) and [vite-plugin-static-copy](https://github.com/sapphi-red/vite-plugin-static-copy).


Updates `eslint` from 10.1.0 to 10.2.0
- [Release notes](https://github.com/eslint/eslint/releases)
- [Commits](eslint/eslint@v10.1.0...v10.2.0)

Updates `vite-plugin-static-copy` from 4.0.0 to 4.0.1
- [Release notes](https://github.com/sapphi-red/vite-plugin-static-copy/releases)
- [Changelog](https://github.com/sapphi-red/vite-plugin-static-copy/blob/main/CHANGELOG.md)
- [Commits](https://github.com/sapphi-red/vite-plugin-static-copy/compare/vite-plugin-static-copy@4.0.0...vite-plugin-static-copy@4.0.1)

---
updated-dependencies:
- dependency-name: eslint
  dependency-version: 10.2.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: dev-dependencies
- dependency-name: vite-plugin-static-copy
  dependency-version: 4.0.1
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: dev-dependencies
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
After reconnecting, setUpSocket() did not call handleUserChanges(),
so any edits made while disconnected were not sent to the server until
the user made another change. This caused divergent pad state between
users.

Now calls handleUserChanges() after reconnect to immediately transmit
any pending local changesets.

Fixes ether#5108

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Calling handleUserChanges() synchronously in setUpSocket() caused
"Cannot read properties of null (reading 'changeset')" because the
editor isn't fully initialized on first connect. Deferred with
setTimeout(500ms) to allow initialization to complete.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Verifies that edits made while disconnected are transmitted to the
server immediately upon reconnection.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…onnect

setUpSocket() only runs during initialization. Move handleUserChanges()
to the reconnect code path so pending edits are flushed when the
connection is re-established.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The existing fix in setChannelState('CONNECTED') calls handleUserChanges(),
but at that point isPendingRevision is still true so changes are blocked.
The real trigger must be in setIsPendingRevision(): when it transitions
from true to false (after all CLIENT_RECONNECT messages are processed),
call handleUserChanges() to flush any locally-queued edits.

Also adds a targeted regression test that simulates the exact reconnect
state transitions and verifies pending edits reach the server.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Replaced the fragile offline/online simulation with a direct test that
uses separate browser contexts. Simplified to a single test that
exercises the exact setIsPendingRevision(false) -> handleUserChanges()
codepath and verifies the flushed text is visible from another client.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The reconnect test requires access to pad.collabClient internals which
are not exposed on window in the browser context. Playwright cannot
call setStateIdle/setIsPendingRevision/setChannelState. The backend
tests adequately cover the code fix.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Calling handleUserChanges() in setChannelState('CONNECTED') fires
synchronously on first connect before the editor is fully initialized,
breaking chat/user_name tests. The setIsPendingRevision(false) trigger
is sufficient for the reconnect path, and the existing
setTimeout(handleUserChanges, 500) in setUpSocket() already handles
the initial connect.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…ision trigger

The setTimeout(handleUserChanges, 500) in setUpSocket was a timing hack
that:
- Only fires on initial connect (setUpSocket is called once at the end
  of getCollabClient; reconnects go through pad.ts:248 which calls
  setChannelState('CONNECTED') directly, bypassing setUpSocket).
- Doesn't actually fix issue ether#5108 (the reconnect-flush bug). That's
  fixed deterministically by the wasPending && !value trigger in
  setIsPendingRevision, which fires whenever the server's CLIENT_RECONNECT
  message lands (both for noChanges and after replaying revisions).
- Introduced a 500ms race window on initial pad load.

The reconnect path now relies entirely on the deterministic event-based
trigger (setIsPendingRevision), with no timing assumptions.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Copy link
Copy Markdown

@cubic-dev-ai cubic-dev-ai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

5 issues found across 174 files

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="src/node/hooks/express/specialpages.ts">

<violation number="1" location="src/node/hooks/express/specialpages.ts:24">
P1: `x-proxy-path` sanitization still allows protocol-relative URLs (for example `//host`), enabling external script loading via `entrypoint`.</violation>
</file>

<file name="admin/src/localization/i18n.ts">

<violation number="1" location="admin/src/localization/i18n.ts:29">
P1: Return after invoking the error callback to avoid calling the backend callback twice on JSON parse failures.</violation>
</file>

<file name="README.md">

<violation number="1" location="README.md:103">
P2: The README now omits the minimum supported Node.js version, which conflicts with the project requirement (`node >=20.0.0`) and can cause users to run unsupported environments.</violation>
</file>

<file name="admin/src/components/IconButton.tsx">

<violation number="1" location="admin/src/components/IconButton.tsx:13">
P3: Avoid concatenating an optional `className` directly; when omitted it renders `"undefined"` as a CSS class.</violation>
</file>

<file name="admin/package.json">

<violation number="1" location="admin/package.json:23">
P1: `@vitejs/plugin-react` 6.x requires Vite 8, but this package is still pinned to Vite 7.2.10, creating an incompatible dependency set.</violation>
</file>

Note: This PR contains a large number of files. cubic only reviews up to 75 files per PR, so some files may not have been reviewed. cubic prioritizes the most important files to review.
On a pro plan you can use ultrareview for larger PRs.

// Only allow path-like characters (letters, digits, hyphens, underscores, slashes, dots).
const sanitizeProxyPath = (req: any): string => {
const raw = req.header('x-proxy-path') || '';
return raw.replace(/[^a-zA-Z0-9\-_\/\.]/g, '');
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1: x-proxy-path sanitization still allows protocol-relative URLs (for example //host), enabling external script loading via entrypoint.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At src/node/hooks/express/specialpages.ts, line 24:

<comment>`x-proxy-path` sanitization still allows protocol-relative URLs (for example `//host`), enabling external script loading via `entrypoint`.</comment>

<file context>
@@ -17,6 +17,13 @@ import prometheus from "../../prometheus";
+// Only allow path-like characters (letters, digits, hyphens, underscores, slashes, dots).
+const sanitizeProxyPath = (req: any): string => {
+  const raw = req.header('x-proxy-path') || '';
+  return raw.replace(/[^a-zA-Z0-9\-_\/\.]/g, '');
+};
+
</file context>
Suggested change
return raw.replace(/[^a-zA-Z0-9\-_\/\.]/g, '');
return raw.replace(/[^a-zA-Z0-9\-_\/\.]/g, '').replace(/\/{2,}/g, '/').replace(/\.\./g, '').replace(/^\/?/, '/').replace(/\/$/, '');

try {
json = JSON.parse(await localeJSON.text())
} catch(e) {
callback(new Error("Error loading"), null);
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1: Return after invoking the error callback to avoid calling the backend callback twice on JSON parse failures.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At admin/src/localization/i18n.ts, line 29:

<comment>Return after invoking the error callback to avoid calling the backend callback twice on JSON parse failures.</comment>

<file context>
@@ -6,52 +6,50 @@ import LanguageDetector from 'i18next-browser-languagedetector'
+    try {
+      json = JSON.parse(await localeJSON.text())
+    } catch(e) {
+      callback(new Error("Error loading"), null);
+    }
+
</file context>
Suggested change
callback(new Error("Error loading"), null);
return callback(new Error("Error loading"), null);

Comment thread admin/package.json
"@vitejs/plugin-react": "^5.1.4",
"@typescript-eslint/eslint-plugin": "^8.58.0",
"@typescript-eslint/parser": "^8.58.0",
"@vitejs/plugin-react": "^6.0.1",
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1: @vitejs/plugin-react 6.x requires Vite 8, but this package is still pinned to Vite 7.2.10, creating an incompatible dependency set.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At admin/package.json, line 23:

<comment>`@vitejs/plugin-react` 6.x requires Vite 8, but this package is still pinned to Vite 7.2.10, creating an incompatible dependency set.</comment>

<file context>
@@ -18,27 +18,27 @@
-    "@vitejs/plugin-react": "^5.1.4",
+    "@typescript-eslint/eslint-plugin": "^8.58.0",
+    "@typescript-eslint/parser": "^8.58.0",
+    "@vitejs/plugin-react": "^6.0.1",
     "babel-plugin-react-compiler": "19.1.0-rc.3",
-    "eslint": "^10.0.0",
</file context>
Suggested change
"@vitejs/plugin-react": "^6.0.1",
"@vitejs/plugin-react": "^5.1.4",

Comment thread README.md
### Requirements

[Node.js](https://nodejs.org/) >= **18.18.2**.
[Node.js](https://nodejs.org/).
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: The README now omits the minimum supported Node.js version, which conflicts with the project requirement (node >=20.0.0) and can cause users to run unsupported environments.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At README.md, line 103:

<comment>The README now omits the minimum supported Node.js version, which conflicts with the project requirement (`node >=20.0.0`) and can cause users to run unsupported environments.</comment>

<file context>
@@ -100,7 +100,7 @@ volumes:
 ### Requirements
 
-[Node.js](https://nodejs.org/) >= **18.18.2**.
+[Node.js](https://nodejs.org/).
 
 ### Windows, macOS, Linux
</file context>
Suggested change
[Node.js](https://nodejs.org/).
[Node.js](https://nodejs.org/) >= **20.0.0**.

{icon}
<span>{title}</span>
</button>
return <button style={style} onClick={onClick} className={"icon-button "+ className} disabled={disabled}>
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P3: Avoid concatenating an optional className directly; when omitted it renders "undefined" as a CSS class.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At admin/src/components/IconButton.tsx, line 13:

<comment>Avoid concatenating an optional `className` directly; when omitted it renders `"undefined"` as a CSS class.</comment>

<file context>
@@ -1,17 +1,17 @@
-        {icon}
-        <span>{title}</span>
-        </button>
+  return <button style={style}  onClick={onClick} className={"icon-button "+ className} disabled={disabled}>
+    {icon}
+    <span>{title}</span>
</file context>

deepshekhardas pushed a commit that referenced this pull request Jun 2, 2026
…d-test ELIFECYCLE (ether#7838)

* test(ci): heartbeat + running-test pointer in backend test diagnostics

Backend tests silently die with code 255 mid-suite ~22% of the time on
develop (most often Windows-with-plugins, Node 24). Each kill lands
300±50 ms after the previous test's clean ✔ teardown line and produces
no failing-test marker, no error, no Mocha summary, and — despite the
unconditional handlers in `diagnostics.ts` — none of the JS-level death
events fire either. Recent example: run 26311025244 (`Windows with
Plugins (24)`); both attempts crashed at completely different "last
test" locations, so the dying test itself isn't to blame.

The existing diagnostics only set lastSeenTest in afterEach, so if the
kill lands during the NEXT test's setup or body — which is exactly the
~300ms gap we observe — the pointer reads as the previous (passing)
test. That hides whether we're between tests or inside one, and which
one.

Two changes:

1. Track currentTest in beforeEach as well as lastFinishedTest in
   afterEach. Every diag line now carries both, so the death point is
   bracketable regardless of which lifecycle phase the kill interrupts.

2. Add a 1Hz heartbeat that writeSyncs the running-test name plus
   `process.memoryUsage()` (rss, heap) and the active-handle and
   active-request counts. The interval is unref'd so it never holds the
   event loop open by itself. Cost is roughly one extra log line per
   second of mocha runtime (~60-120 lines per CI run).

When the next failure fires, the last heartbeat narrows the kill window
to ≤1s, the running pointer names the test on the rails at that moment,
and the handle/memory trace gives a sparkline that exposes sudden
spikes — a leaked socket, an unref'd timer, a runaway map — that
would otherwise be invisible at the runner-log level.

No behavior change on successful runs.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix: heartbeat _getActiveHandles optional chain bug (Qodo #2)

Qodo correctly flagged `_getActiveHandles?.().length` as a latent
TypeError: `?.()` guards the call but the call's `undefined` return
on a missing method still hits `.length`, which throws. Since the
heartbeat fires on a setInterval inside the mocha bootstrap, a Node
build without the underscore-prefixed internals would take down the
whole backend test run.

Capture the array first, then read `.length` only when it actually
exists. -1 stays as the "API missing" sentinel.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* test(ci): per-test start diag + drop stray console.log noise

Follow-up to the heartbeat PR after run 26397693748 confirmed the
diagnostic works (the kill landed at importexportGetPost.ts
'Import authorization checks > authn anonymous !exist -> fail',
~300 ms after the previous test's ✔). Two cleanups so the next
failure pinpoints faster and reads cleaner:

1. diagnostics.ts: emit a `test start: <name>` diag line in the
   mocha beforeEach hook, after setting the currentTest pointer.
   The 1Hz heartbeat misses tests that take less than a second,
   and the silent kills land ~300 ms after a test boundary —
   precisely the gap where heartbeat resolution fails. A start
   line per test gives sub-millisecond resolution on which test
   was on the rails when the process died.

2. specs/api/importexportGetPost.ts: drop a stray
   `console.log(importedPads)` debug leftover (and the duplicate
   `await importEtherpad(records)` only present to feed it) in
   the `malformed .etherpad files are rejected` block. The leftover
   dumped a ~600-line reflection of a supertest Response object
   to the CI log on every successful run, drowning the surrounding
   test output and making the silent-kill window much harder to
   read.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* test(ci): write node-report on every heartbeat tick

Run 26398054688 narrowed the kill to a specific test
(pad.ts > Gets text on a pad Id and doesn't have an excess newline)
but the test body is a trivial supertest GET — the kill bypasses
all JS handlers, so we can't capture stack state at death.
Two failures across two runs share the shape: an agent.{get,post}
+ common.generateJWTToken() call dies ~300-600 ms after test start,
with no JS-visible cause. The next step is V8 + native stack.

Hook into the existing 1Hz heartbeat to call
process.report.writeReport(path) whenever a report directory is set.
The Windows backend-tests workflow already wires up
`--report-directory=${{ github.workspace }}/node-report` via
NODE_OPTIONS and uploads that directory as an artifact on failure,
so the rolling snapshots ride for free on the existing upload step.

Each report (~50 KB) contains:
  - V8 + native call stacks for all threads
  - libuv active handles (open TCP, timers, file handles)
  - JS heap statistics
  - resourceUsage + system info
  - shared-object list

On the next reproduction the latest report before ELIFECYCLE will
sit ~0-1 s before the kill — enough to see whether the V8 stack
is inside jose's WebCrypto sign path, inside supertest's TCP
roundtrip, or somewhere unexpected entirely.

NODE_REPORT_DIR is also honored as an explicit override for local
repro / non-workflow runs.

Cost: ~6 files (~300 KB) per Windows backend-test failure, plus
~50 ms event-loop pause per heartbeat. No-op when neither env var
is set.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix: writeReport with bare filename, not mixed-slash absolute path

Run 26398830249 exposed the path-separator bug in the previous commit:
every heartbeat tick on the Windows runner logged

  Failed to open Node.js report file:
  D:\a\etherpad\etherpad/node-report/hb-NNNN-...json
  directory: D:\a\etherpad\etherpad/node-report (errno: 22)

— EINVAL. The workflow sets --report-directory with forward-slash
separators on Windows, then this code concatenated another `/` plus
the filename, producing a path Node's report writer rejects.

writeReport(fileName) takes a BARE filename and resolves it against
the configured report directory using the platform-correct separator
internally. Switch to that. For local repro overrides via
NODE_REPORT_DIR, push the path into process.report.directory (the
documented config knob) instead of joining it into the call site.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* test(ci): write node-report on test boundaries, throttled to 4Hz

Run 26398985832 proved the heartbeat-only report cadence isn't tight
enough: the last report before the kill was hb-0013 at +16201ms,
~1.5 s before ELIFECYCLE at +17701ms — during which ~30 tests fired,
including the dying one (`authn anonymous !exist -> fail`). The
captured V8 stack is just our heartbeat code, not the dying test.

Move the writeReport call to a shared tryWriteReport() helper and
invoke it from BOTH the heartbeat AND mocha's beforeEach hook,
throttled to one report per 250 ms. That gives ≤250 ms resolution
on the kill window — close enough that the latest report captures
state from inside the dying test rather than from the test ~30
slots earlier. The heartbeat always writes (so we don't lose the
no-test-running ticks during setup); beforeEach only writes when
the throttle window has elapsed.

Cost ceiling: ~4 reports/sec × ~12 s test phase ≈ 48 reports
(~2.5 MB) per failing run. Each writeReport adds ~50 ms of
event-loop pause — at 4Hz that's 20% of wall time spent in
diagnostics, which is acceptable for a temporary debug-only
bootstrap.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* test(ci): drop beforeEach report throttle from 250ms to 100ms

Run 26399285213's rerun captured a sixth death point on the new 4Hz
cadence (`socketio.ts > Duplicate-author handling > cookie identity:
same-author second socket kicks the first`, kill at +45953ms, 271ms
after test start). The throttle suppressed the dying test's own
beforeEach: previous boundary write landed 128 ms earlier and the
next 31 ms after that, both inside the 250 ms window. Last captured
report (be-0100) is from the previous test.

100 ms is still well above the inter-test cadence in fast burst
suites (tests fire 2-5 ms apart, so 20-50 of them get throttled to a
single write, ceiling ~10 writes/sec). But it's tight enough that
any death-window neighbour ≥100 ms after the previous report — the
shape we keep observing — gets its own boundary snapshot.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Pending changesets are not immediately retransmitted after reconnect

4 participants