From fe6a373bf80320b790115f3fd7cc1c9a991bbc53 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stefan=20M=C3=BCller?= Date: Wed, 22 Apr 2026 11:52:53 +0200 Subject: [PATCH 001/388] feat: Remove paths from plugin packages (#7580) --- src/node/handler/PadMessageHandler.ts | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/node/handler/PadMessageHandler.ts b/src/node/handler/PadMessageHandler.ts index 8285a3a8a..d0f34600c 100644 --- a/src/node/handler/PadMessageHandler.ts +++ b/src/node/handler/PadMessageHandler.ts @@ -1068,6 +1068,11 @@ const handleClientReady = async (socket:any, message: ClientReadyMessage) => { throw new Error('corrupt pad'); } + let pluginsSanitized: any = plugins.plugins + Object.keys(plugins.plugins).forEach(function(element) { + const p: any = plugins.plugins[element].package + pluginsSanitized[element].package = {name: p.name, version: p.version}; + }); // Warning: never ever send sessionInfo.padId to the client. If the client is read only you // would open a security hole 1 swedish mile wide... const canEditPadSettings = settings.enablePadWideSettings && @@ -1116,7 +1121,7 @@ const handleClientReady = async (socket:any, message: ClientReadyMessage) => { exportAvailable: exportAvailable(), docxExport: settings.docxExport, plugins: { - plugins: plugins.plugins, + plugins: pluginsSanitized, parts: plugins.parts, }, indentationOnNewLine: settings.indentationOnNewLine, From 97b9c3b128da138d73576a06de065df2a62aedeb Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 22 Apr 2026 18:20:03 +0100 Subject: [PATCH 002/388] build(deps): bump axios from 1.15.1 to 1.15.2 (#7581) Bumps [axios](https://github.com/axios/axios) from 1.15.1 to 1.15.2. - [Release notes](https://github.com/axios/axios/releases) - [Changelog](https://github.com/axios/axios/blob/v1.x/CHANGELOG.md) - [Commits](https://github.com/axios/axios/compare/v1.15.1...v1.15.2) --- updated-dependencies: - dependency-name: axios dependency-version: 1.15.2 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- bin/package.json | 2 +- pnpm-lock.yaml | 24 ++++++++++++------------ src/package.json | 2 +- 3 files changed, 14 insertions(+), 14 deletions(-) diff --git a/bin/package.json b/bin/package.json index 53f1f6738..539e403fa 100644 --- a/bin/package.json +++ b/bin/package.json @@ -7,7 +7,7 @@ "doc": "doc" }, "dependencies": { - "axios": "^1.15.1", + "axios": "^1.15.2", "ep_etherpad-lite": "workspace:../src", "log4js": "^6.9.1", "semver": "^7.7.4", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index abcf54bc7..ec125864c 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -125,8 +125,8 @@ importers: bin: dependencies: axios: - specifier: ^1.15.1 - version: 1.15.1 + specifier: ^1.15.2 + version: 1.15.2 ep_etherpad-lite: specifier: workspace:../src version: link:../src @@ -161,7 +161,7 @@ importers: devDependencies: vitepress: specifier: ^2.0.0-alpha.17 - version: 2.0.0-alpha.17(@types/node@25.6.0)(axios@1.15.1)(esbuild@0.28.0)(jwt-decode@4.0.0)(postcss@8.5.8)(tsx@4.21.0)(typescript@6.0.3) + version: 2.0.0-alpha.17(@types/node@25.6.0)(axios@1.15.2)(esbuild@0.28.0)(jwt-decode@4.0.0)(postcss@8.5.8)(tsx@4.21.0)(typescript@6.0.3) src: dependencies: @@ -172,8 +172,8 @@ importers: specifier: ^3.2.6 version: 3.2.6 axios: - specifier: ^1.15.1 - version: 1.15.1 + specifier: ^1.15.2 + version: 1.15.2 cassandra-driver: specifier: ^4.8.0 version: 4.8.0 @@ -2436,8 +2436,8 @@ packages: resolution: {integrity: sha512-NZKeq9AfyQvEeNlN0zSYAaWrmBffJh3IELMZfRpJVWgrpEbtEpnjvzqBPf+mxoI287JohRDoa+/nsfqqiZmF6g==} engines: {node: '>= 6.0.0'} - axios@1.15.1: - resolution: {integrity: sha512-WOG+Jj8ZOvR0a3rAn+Tuf1UQJRxw5venr6DgdbJzngJE3qG7X0kL83CZGpdHMxEm+ZK3seAbvFsw4FfOfP9vxg==} + axios@1.15.2: + resolution: {integrity: sha512-wLrXxPtcrPTsNlJmKjkPnNPK2Ihe0hn0wGSaTEiHRPxwjvJwT3hKmXF4dpqxmPO9SoNb2FsYXj/xEo0gHN+D5A==} babel-plugin-react-compiler@19.1.0-rc.3: resolution: {integrity: sha512-mjRn69WuTz4adL0bXGx8Rsyk1086zFJeKmes6aK0xPuK3aaXmDJdLHqwKKMrpm6KAI1MCoUK72d2VeqQbu8YIA==} @@ -7475,13 +7475,13 @@ snapshots: '@vueuse/shared': 14.2.1(vue@3.5.30(typescript@6.0.3)) vue: 3.5.30(typescript@6.0.3) - '@vueuse/integrations@14.2.1(axios@1.15.1)(focus-trap@8.0.0)(jwt-decode@4.0.0)(vue@3.5.30(typescript@6.0.3))': + '@vueuse/integrations@14.2.1(axios@1.15.2)(focus-trap@8.0.0)(jwt-decode@4.0.0)(vue@3.5.30(typescript@6.0.3))': dependencies: '@vueuse/core': 14.2.1(vue@3.5.30(typescript@6.0.3)) '@vueuse/shared': 14.2.1(vue@3.5.30(typescript@6.0.3)) vue: 3.5.30(typescript@6.0.3) optionalDependencies: - axios: 1.15.1 + axios: 1.15.2 focus-trap: 8.0.0 jwt-decode: 4.0.0 @@ -7640,7 +7640,7 @@ snapshots: aws-ssl-profiles@1.1.2: {} - axios@1.15.1: + axios@1.15.2: dependencies: follow-redirects: 1.16.0 form-data: 4.0.5 @@ -11004,7 +11004,7 @@ snapshots: fsevents: 2.3.3 tsx: 4.21.0 - vitepress@2.0.0-alpha.17(@types/node@25.6.0)(axios@1.15.1)(esbuild@0.28.0)(jwt-decode@4.0.0)(postcss@8.5.8)(tsx@4.21.0)(typescript@6.0.3): + vitepress@2.0.0-alpha.17(@types/node@25.6.0)(axios@1.15.2)(esbuild@0.28.0)(jwt-decode@4.0.0)(postcss@8.5.8)(tsx@4.21.0)(typescript@6.0.3): dependencies: '@docsearch/css': 4.6.0 '@docsearch/js': 4.6.0 @@ -11018,7 +11018,7 @@ snapshots: '@vue/devtools-api': 8.1.0 '@vue/shared': 3.5.30 '@vueuse/core': 14.2.1(vue@3.5.30(typescript@6.0.3)) - '@vueuse/integrations': 14.2.1(axios@1.15.1)(focus-trap@8.0.0)(jwt-decode@4.0.0)(vue@3.5.30(typescript@6.0.3)) + '@vueuse/integrations': 14.2.1(axios@1.15.2)(focus-trap@8.0.0)(jwt-decode@4.0.0)(vue@3.5.30(typescript@6.0.3)) focus-trap: 8.0.0 mark.js: 8.11.1 minisearch: 7.2.0 diff --git a/src/package.json b/src/package.json index 4f3d70ec3..75151ee03 100644 --- a/src/package.json +++ b/src/package.json @@ -32,7 +32,7 @@ "dependencies": { "@elastic/elasticsearch": "^9.3.4", "async": "^3.2.6", - "axios": "^1.15.1", + "axios": "^1.15.2", "cassandra-driver": "^4.8.0", "cookie-parser": "^1.4.7", "cross-env": "^10.1.0", From b45d89c12916c66154a2989a097396dab38640b5 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 22 Apr 2026 18:38:49 +0100 Subject: [PATCH 003/388] build(deps-dev): bump the dev-dependencies group across 1 directory with 9 updates (#7579) Bumps the dev-dependencies group with 9 updates in the / directory: | Package | From | To | | --- | --- | --- | | [eslint](https://github.com/eslint/eslint) | `10.2.0` | `10.2.1` | | [vitest](https://github.com/vitest-dev/vitest/tree/HEAD/packages/vitest) | `4.1.4` | `4.1.5` | | [@typescript-eslint/eslint-plugin](https://github.com/typescript-eslint/typescript-eslint/tree/HEAD/packages/eslint-plugin) | `8.58.2` | `8.59.0` | | [@typescript-eslint/parser](https://github.com/typescript-eslint/typescript-eslint/tree/HEAD/packages/parser) | `8.58.2` | `8.59.0` | | [eslint-plugin-react-hooks](https://github.com/facebook/react/tree/HEAD/packages/eslint-plugin-react-hooks) | `7.1.0` | `7.1.1` | | [i18next](https://github.com/i18next/i18next) | `26.0.5` | `26.0.6` | | [react-hook-form](https://github.com/react-hook-form/react-hook-form) | `7.72.1` | `7.73.1` | | [react-router-dom](https://github.com/remix-run/react-router/tree/HEAD/packages/react-router-dom) | `7.14.1` | `7.14.2` | | [vite-plugin-static-copy](https://github.com/sapphi-red/vite-plugin-static-copy) | `4.0.1` | `4.1.0` | Updates `eslint` from 10.2.0 to 10.2.1 - [Release notes](https://github.com/eslint/eslint/releases) - [Commits](https://github.com/eslint/eslint/compare/v10.2.0...v10.2.1) Updates `vitest` from 4.1.4 to 4.1.5 - [Release notes](https://github.com/vitest-dev/vitest/releases) - [Commits](https://github.com/vitest-dev/vitest/commits/v4.1.5/packages/vitest) Updates `@typescript-eslint/eslint-plugin` from 8.58.2 to 8.59.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.59.0/packages/eslint-plugin) Updates `@typescript-eslint/parser` from 8.58.2 to 8.59.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.59.0/packages/parser) Updates `eslint-plugin-react-hooks` from 7.1.0 to 7.1.1 - [Release notes](https://github.com/facebook/react/releases) - [Changelog](https://github.com/facebook/react/blob/main/packages/eslint-plugin-react-hooks/CHANGELOG.md) - [Commits](https://github.com/facebook/react/commits/eslint-plugin-react-hooks@7.1.1/packages/eslint-plugin-react-hooks) Updates `i18next` from 26.0.5 to 26.0.6 - [Release notes](https://github.com/i18next/i18next/releases) - [Changelog](https://github.com/i18next/i18next/blob/master/CHANGELOG.md) - [Commits](https://github.com/i18next/i18next/compare/v26.0.5...v26.0.6) Updates `react-hook-form` from 7.72.1 to 7.73.1 - [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.72.1...v7.73.1) Updates `react-router-dom` from 7.14.1 to 7.14.2 - [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.14.2/packages/react-router-dom) Updates `vite-plugin-static-copy` from 4.0.1 to 4.1.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@4.0.1...vite-plugin-static-copy@4.1.0) --- updated-dependencies: - dependency-name: eslint dependency-version: 10.2.1 dependency-type: direct:development update-type: version-update:semver-patch dependency-group: dev-dependencies - dependency-name: vitest dependency-version: 4.1.5 dependency-type: direct:development update-type: version-update:semver-patch dependency-group: dev-dependencies - dependency-name: "@typescript-eslint/eslint-plugin" dependency-version: 8.59.0 dependency-type: direct:development update-type: version-update:semver-minor dependency-group: dev-dependencies - dependency-name: "@typescript-eslint/parser" dependency-version: 8.59.0 dependency-type: direct:development update-type: version-update:semver-minor dependency-group: dev-dependencies - dependency-name: eslint-plugin-react-hooks dependency-version: 7.1.1 dependency-type: direct:development update-type: version-update:semver-patch dependency-group: dev-dependencies - dependency-name: i18next dependency-version: 26.0.6 dependency-type: direct:development update-type: version-update:semver-patch dependency-group: dev-dependencies - dependency-name: react-hook-form dependency-version: 7.73.1 dependency-type: direct:development update-type: version-update:semver-minor dependency-group: dev-dependencies - dependency-name: react-router-dom dependency-version: 7.14.2 dependency-type: direct:development update-type: version-update:semver-patch dependency-group: dev-dependencies - dependency-name: vite-plugin-static-copy dependency-version: 4.1.0 dependency-type: direct:development update-type: version-update:semver-minor dependency-group: dev-dependencies ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- admin/package.json | 16 +- pnpm-lock.yaml | 560 ++++++++++++++++++++++----------------------- src/package.json | 4 +- 3 files changed, 287 insertions(+), 293 deletions(-) diff --git a/admin/package.json b/admin/package.json index d556ebbe5..e5d626a70 100644 --- a/admin/package.json +++ b/admin/package.json @@ -18,26 +18,26 @@ "@radix-ui/react-toast": "^1.2.15", "@types/react": "^19.2.14", "@types/react-dom": "^19.2.3", - "@typescript-eslint/eslint-plugin": "^8.58.2", - "@typescript-eslint/parser": "^8.58.2", + "@typescript-eslint/eslint-plugin": "^8.59.0", + "@typescript-eslint/parser": "^8.59.0", "@vitejs/plugin-react": "^6.0.1", "babel-plugin-react-compiler": "19.1.0-rc.3", - "eslint": "^10.2.0", - "eslint-plugin-react-hooks": "^7.1.0", + "eslint": "^10.2.1", + "eslint-plugin-react-hooks": "^7.1.1", "eslint-plugin-react-refresh": "^0.5.2", - "i18next": "^26.0.5", + "i18next": "^26.0.6", "i18next-browser-languagedetector": "^8.2.1", "lucide-react": "^1.8.0", "react": "^19.2.5", "react-dom": "^19.2.5", - "react-hook-form": "^7.72.1", + "react-hook-form": "^7.73.1", "react-i18next": "^17.0.4", - "react-router-dom": "^7.14.1", + "react-router-dom": "^7.14.2", "socket.io-client": "^4.8.3", "typescript": "^6.0.3", "vite": "npm:rolldown-vite@7.2.10", "vite-plugin-babel": "^1.6.0", - "vite-plugin-static-copy": "^4.0.1", + "vite-plugin-static-copy": "^4.1.0", "zustand": "^5.0.12" }, "overrides": { diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index ec125864c..edc19d77d 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -59,11 +59,11 @@ importers: specifier: ^19.2.3 version: 19.2.3(@types/react@19.2.14) '@typescript-eslint/eslint-plugin': - specifier: ^8.58.2 - version: 8.58.2(@typescript-eslint/parser@8.58.2(eslint@10.2.0)(typescript@6.0.3))(eslint@10.2.0)(typescript@6.0.3) + specifier: ^8.59.0 + version: 8.59.0(@typescript-eslint/parser@8.59.0(eslint@10.2.1)(typescript@6.0.3))(eslint@10.2.1)(typescript@6.0.3) '@typescript-eslint/parser': - specifier: ^8.58.2 - version: 8.58.2(eslint@10.2.0)(typescript@6.0.3) + specifier: ^8.59.0 + version: 8.59.0(eslint@10.2.1)(typescript@6.0.3) '@vitejs/plugin-react': specifier: ^6.0.1 version: 6.0.1(babel-plugin-react-compiler@19.1.0-rc.3)(rolldown-vite@7.2.10(@types/node@25.6.0)(tsx@4.21.0)) @@ -71,17 +71,17 @@ importers: specifier: 19.1.0-rc.3 version: 19.1.0-rc.3 eslint: - specifier: ^10.2.0 - version: 10.2.0 + specifier: ^10.2.1 + version: 10.2.1 eslint-plugin-react-hooks: - specifier: ^7.1.0 - version: 7.1.0(eslint@10.2.0) + specifier: ^7.1.1 + version: 7.1.1(eslint@10.2.1) eslint-plugin-react-refresh: specifier: ^0.5.2 - version: 0.5.2(eslint@10.2.0) + version: 0.5.2(eslint@10.2.1) i18next: - specifier: ^26.0.5 - version: 26.0.5(typescript@6.0.3) + specifier: ^26.0.6 + version: 26.0.6(typescript@6.0.3) i18next-browser-languagedetector: specifier: ^8.2.1 version: 8.2.1 @@ -95,14 +95,14 @@ importers: specifier: ^19.2.5 version: 19.2.5(react@19.2.5) react-hook-form: - specifier: ^7.72.1 - version: 7.72.1(react@19.2.5) + specifier: ^7.73.1 + version: 7.73.1(react@19.2.5) react-i18next: specifier: ^17.0.4 - version: 17.0.4(i18next@26.0.5(typescript@6.0.3))(react-dom@19.2.5(react@19.2.5))(react@19.2.5)(typescript@6.0.3) + version: 17.0.4(i18next@26.0.6(typescript@6.0.3))(react-dom@19.2.5(react@19.2.5))(react@19.2.5)(typescript@6.0.3) react-router-dom: - specifier: ^7.14.1 - version: 7.14.1(react-dom@19.2.5(react@19.2.5))(react@19.2.5) + specifier: ^7.14.2 + version: 7.14.2(react-dom@19.2.5(react@19.2.5))(react@19.2.5) socket.io-client: specifier: ^4.8.3 version: 4.8.3 @@ -116,8 +116,8 @@ importers: specifier: ^1.6.0 version: 1.6.0(@babel/core@7.29.0)(rolldown-vite@7.2.10(@types/node@25.6.0)(tsx@4.21.0)) vite-plugin-static-copy: - specifier: ^4.0.1 - version: 4.0.1(rolldown-vite@7.2.10(@types/node@25.6.0)(tsx@4.21.0)) + specifier: ^4.1.0 + version: 4.1.0(rolldown-vite@7.2.10(@types/node@25.6.0)(tsx@4.21.0)) zustand: specifier: ^5.0.12 version: 5.0.12(@types/react@19.2.14)(react@19.2.5)(use-sync-external-store@1.6.0(react@19.2.5)) @@ -413,11 +413,11 @@ importers: specifier: ^5.0.0 version: 5.0.0 eslint: - specifier: ^10.2.0 - version: 10.2.0 + specifier: ^10.2.1 + version: 10.2.1 eslint-config-etherpad: specifier: ^4.0.5 - version: 4.0.5(eslint@10.2.0)(typescript@6.0.3) + version: 4.0.5(eslint@10.2.1)(typescript@6.0.3) etherpad-cli-client: specifier: ^3.0.5 version: 3.0.5 @@ -449,8 +449,8 @@ importers: specifier: ^6.0.3 version: 6.0.3 vitest: - specifier: ^4.1.4 - version: 4.1.4(@opentelemetry/api@1.9.0)(@types/node@25.6.0)(jsdom@29.0.2(@noble/hashes@1.8.0))(vite@8.0.8(@types/node@25.6.0)(esbuild@0.28.0)(tsx@4.21.0)) + specifier: ^4.1.5 + version: 4.1.5(@opentelemetry/api@1.9.0)(@types/node@25.6.0)(jsdom@29.0.2(@noble/hashes@1.8.0))(vite@8.0.8(@types/node@25.6.0)(esbuild@0.28.0)(tsx@4.21.0)) ui: devDependencies: @@ -1049,24 +1049,24 @@ packages: resolution: {integrity: sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==} engines: {node: ^12.0.0 || ^14.0.0 || >=16.0.0} - '@eslint/config-array@0.23.4': - resolution: {integrity: sha512-lf19F24LSMfF8weXvW5QEtnLqW70u7kgit5e9PSx0MsHAFclGd1T9ynvWEMDT1w5J4Qt54tomGeAhdoAku1Xow==} + '@eslint/config-array@0.23.5': + resolution: {integrity: sha512-Y3kKLvC1dvTOT+oGlqNQ1XLqK6D1HU2YXPc52NmAlJZbMMWDzGYXMiPRJ8TYD39muD/OTjlZmNJ4ib7dvSrMBA==} engines: {node: ^20.19.0 || ^22.13.0 || >=24} - '@eslint/config-helpers@0.5.4': - resolution: {integrity: sha512-jJhqiY3wPMlWWO3370M86CPJ7pt8GmEwSLglMfQhjXal07RCvhmU0as4IuUEW5SJeunfItiEetHmSxCCe9lDBg==} + '@eslint/config-helpers@0.5.5': + resolution: {integrity: sha512-eIJYKTCECbP/nsKaaruF6LW967mtbQbsw4JTtSVkUQc9MneSkbrgPJAbKl9nWr0ZeowV8BfsarBmPpBzGelA2w==} engines: {node: ^20.19.0 || ^22.13.0 || >=24} - '@eslint/core@1.2.0': - resolution: {integrity: sha512-8FTGbNzTvmSlc4cZBaShkC6YvFMG0riksYWRFKXztqVdXaQbcZLXlFbSpC05s70sGEsXAw0qwhx69JiW7hQS7A==} + '@eslint/core@1.2.1': + resolution: {integrity: sha512-MwcE1P+AZ4C6DWlpin/OmOA54mmIZ/+xZuJiQd4SyB29oAJjN30UW9wkKNptW2ctp4cEsvhlLY/CsQ1uoHDloQ==} engines: {node: ^20.19.0 || ^22.13.0 || >=24} - '@eslint/object-schema@3.0.4': - resolution: {integrity: sha512-55lO/7+Yp0ISKRP0PsPtNTeNGapXaO085aELZmWCVc5SH3jfrqpuU6YgOdIxMS99ZHkQN1cXKE+cdIqwww9ptw==} + '@eslint/object-schema@3.0.5': + resolution: {integrity: sha512-vqTaUEgxzm+YDSdElad6PiRoX4t8VGDjCtt05zn4nU810UIx/uNEV7/lZJ6KwFThKZOzOxzXy48da+No7HZaMw==} engines: {node: ^20.19.0 || ^22.13.0 || >=24} - '@eslint/plugin-kit@0.7.0': - resolution: {integrity: sha512-ejvBr8MQCbVsWNZnCwDXjUKq40MDmHalq7cJ6e9s/qzTUFIIo/afzt1Vui9T97FM/V/pN4YsFVoed5NIa96RDg==} + '@eslint/plugin-kit@0.7.1': + resolution: {integrity: sha512-rZAP3aVgB9ds9KOeUSL+zZ21hPmo8dh6fnIFwRQj5EAZl9gzR7wxYbYXYysAM8CTqGmUGyp2S4kUdV17MnGuWQ==} engines: {node: ^20.19.0 || ^22.13.0 || >=24} '@exodus/bytes@1.15.0': @@ -1078,12 +1078,16 @@ packages: '@noble/hashes': optional: true - '@humanfs/core@0.19.1': - resolution: {integrity: sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==} + '@humanfs/core@0.19.2': + resolution: {integrity: sha512-UhXNm+CFMWcbChXywFwkmhqjs3PRCmcSa/hfBgLIb7oQ5HNb1wS0icWsGtSAUNgefHeI+eBrA8I1fxmbHsGdvA==} engines: {node: '>=18.18.0'} - '@humanfs/node@0.16.7': - resolution: {integrity: sha512-/zUx+yOsIrG4Y43Eh2peDeKCxlRt/gET6aHfaKpuq267qXdYDFViVHfMaLyygZOnl0kGWxFIgsBy8QFuTLUXEQ==} + '@humanfs/node@0.16.8': + resolution: {integrity: sha512-gE1eQNZ3R++kTzFUpdGlpmy8kDZD/MLyHqDwqjkVQI0JMdI1D51sy1H958PNXYkM2rAac7e5/CnIKZrHtPh3BQ==} + engines: {node: '>=18.18.0'} + + '@humanfs/types@0.15.0': + resolution: {integrity: sha512-ZZ1w0aoQkwuUuC7Yf+7sdeaNfqQiiLcSRbfI08oAxqLtpXQr9AIVX7Ay7HLDuiLYAaFPu8oBYNq/QIi9URHJ3Q==} engines: {node: '>=18.18.0'} '@humanwhocodes/module-importer@1.0.1': @@ -1974,11 +1978,11 @@ packages: typescript: optional: true - '@typescript-eslint/eslint-plugin@8.58.2': - resolution: {integrity: sha512-aC2qc5thQahutKjP+cl8cgN9DWe3ZUqVko30CMSZHnFEHyhOYoZSzkGtAI2mcwZ38xeImDucI4dnqsHiOYuuCw==} + '@typescript-eslint/eslint-plugin@8.59.0': + resolution: {integrity: sha512-HyAZtpdkgZwpq8Sz3FSUvCR4c+ScbuWa9AksK2Jweub7w4M3yTz4O11AqVJzLYjy/B9ZWPyc81I+mOdJU/bDQw==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: - '@typescript-eslint/parser': ^8.58.2 + '@typescript-eslint/parser': ^8.59.0 eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 typescript: '>=4.8.4 <6.1.0' @@ -1992,15 +1996,15 @@ packages: typescript: optional: true - '@typescript-eslint/parser@8.58.2': - resolution: {integrity: sha512-/Zb/xaIDfxeJnvishjGdcR4jmr7S+bda8PKNhRGdljDM+elXhlvN0FyPSsMnLmJUrVG9aPO6dof80wjMawsASg==} + '@typescript-eslint/parser@8.59.0': + resolution: {integrity: sha512-TI1XGwKbDpo9tRW8UDIXCOeLk55qe9ZFGs8MTKU6/M08HWTw52DD/IYhfQtOEhEdPhLMT26Ka/x7p70nd3dzDg==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 typescript: '>=4.8.4 <6.1.0' - '@typescript-eslint/project-service@8.58.2': - resolution: {integrity: sha512-Cq6UfpZZk15+r87BkIh5rDpi38W4b+Sjnb8wQCPPDDweS/LRCFjCyViEbzHk5Ck3f2QDfgmlxqSa7S7clDtlfg==} + '@typescript-eslint/project-service@8.59.0': + resolution: {integrity: sha512-Lw5ITrR5s5TbC19YSvlr63ZfLaJoU6vtKTHyB0GQOpX0W7d5/Ir6vUahWi/8Sps/nOukZQ0IB3SmlxZnjaKVnw==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: typescript: '>=4.8.4 <6.1.0' @@ -2009,12 +2013,12 @@ packages: resolution: {integrity: sha512-jjhdIE/FPF2B7Z1uzc6i3oWKbGcHb87Qw7AWj6jmEqNOfDFbJWtjt/XfwCpvNkpGWlcJaog5vTR+VV8+w9JflA==} engines: {node: ^18.18.0 || >=20.0.0} - '@typescript-eslint/scope-manager@8.58.2': - resolution: {integrity: sha512-SgmyvDPexWETQek+qzZnrG6844IaO02UVyOLhI4wpo82dpZJY9+6YZCKAMFzXb7qhx37mFK1QcPQ18tud+vo6Q==} + '@typescript-eslint/scope-manager@8.59.0': + resolution: {integrity: sha512-UzR16Ut8IpA3Mc4DbgAShlPPkVm8xXMWafXxB0BocaVRHs8ZGakAxGRskF7FId3sdk9lgGD73GSFaWmWFDE4dg==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - '@typescript-eslint/tsconfig-utils@8.58.2': - resolution: {integrity: sha512-3SR+RukipDvkkKp/d0jP0dyzuls3DbGmwDpVEc5wqk5f38KFThakqAAO0XMirWAE+kT00oTauTbzMFGPoAzB0A==} + '@typescript-eslint/tsconfig-utils@8.59.0': + resolution: {integrity: sha512-91Sbl3s4Kb3SybliIY6muFBmHVv+pYXfybC4Oolp3dvk8BvIE3wOPc+403CWIT7mJNkfQRGtdqghzs2+Z91Tqg==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: typescript: '>=4.8.4 <6.1.0' @@ -2029,8 +2033,8 @@ packages: typescript: optional: true - '@typescript-eslint/type-utils@8.58.2': - resolution: {integrity: sha512-Z7EloNR/B389FvabdGeTo2XMs4W9TjtPiO9DAsmT0yom0bwlPyRjkJ1uCdW1DvrrrYP50AJZ9Xc3sByZA9+dcg==} + '@typescript-eslint/type-utils@8.59.0': + resolution: {integrity: sha512-3TRiZaQSltGqGeNrJzzr1+8YcEobKH9rHnqIp/1psfKFmhRQDNMGP5hBufanYTGznwShzVLs3Mz+gDN7HkWfXg==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 @@ -2040,8 +2044,8 @@ packages: resolution: {integrity: sha512-iZqi+Ds1y4EDYUtlOOC+aUmxnE9xS/yCigkjA7XpTKV6nCBd3Hp/PRGGmdwnfkV2ThMyYldP1wRpm/id99spTQ==} engines: {node: ^18.18.0 || >=20.0.0} - '@typescript-eslint/types@8.58.2': - resolution: {integrity: sha512-9TukXyATBQf/Jq9AMQXfvurk+G5R2MwfqQGDR2GzGz28HvY/lXNKGhkY+6IOubwcquikWk5cjlgPvD2uAA7htQ==} + '@typescript-eslint/types@8.59.0': + resolution: {integrity: sha512-nLzdsT1gdOgFxxxwrlNVUBzSNBEEHJ86bblmk4QAS6stfig7rcJzWKqCyxFy3YRRHXDWEkb2NralA1nOYkkm/A==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} '@typescript-eslint/typescript-estree@7.18.0': @@ -2053,8 +2057,8 @@ packages: typescript: optional: true - '@typescript-eslint/typescript-estree@8.58.2': - resolution: {integrity: sha512-ELGuoofuhhoCvNbQjFFiobFcGgcDCEm0ThWdmO4Z0UzLqPXS3KFvnEZ+SHewwOYHjM09tkzOWXNTv9u6Gqtyuw==} + '@typescript-eslint/typescript-estree@8.59.0': + resolution: {integrity: sha512-O9Re9P1BmBLFJyikRbQpLku/QA3/AueZNO9WePLBwQrvkixTmDe8u76B6CYUAITRl/rHawggEqUGn5QIkVRLMw==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: typescript: '>=4.8.4 <6.1.0' @@ -2065,8 +2069,8 @@ packages: peerDependencies: eslint: ^8.56.0 - '@typescript-eslint/utils@8.58.2': - resolution: {integrity: sha512-QZfjHNEzPY8+l0+fIXMvuQ2sJlplB4zgDZvA+NmvZsZv3EQwOcc1DuIU1VJUTWZ/RKouBMhDyNaBMx4sWvrzRA==} + '@typescript-eslint/utils@8.59.0': + resolution: {integrity: sha512-I1R/K7V07XsMJ12Oaxg/O9GfrysGTmCRhvZJBv0RE0NcULMzjqVpR5kRRQjHsz3J/bElU7HwCO7zkqL+MSUz+g==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 @@ -2076,8 +2080,8 @@ packages: resolution: {integrity: sha512-cDF0/Gf81QpY3xYyJKDV14Zwdmid5+uuENhjH2EqFaF0ni+yAyq/LzMaIJdhNJXZI7uLzwIlA+V7oWoyn6Curg==} engines: {node: ^18.18.0 || >=20.0.0} - '@typescript-eslint/visitor-keys@8.58.2': - resolution: {integrity: sha512-f1WO2Lx8a9t8DARmcWAUPJbu0G20bJlj8L4z72K00TMeJAoyLr/tHhI/pzYBLrR4dXWkcxO1cWYZEOX8DKHTqA==} + '@typescript-eslint/visitor-keys@8.59.0': + resolution: {integrity: sha512-/uejZt4dSere1bx12WLlPfv8GktzcaDtuJ7s42/HEZ5zGj9oxRaD4bj7qwSunXkf+pbAhFt2zjpHYUiT5lHf0Q==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} '@typespec/ts-http-runtime@0.3.5': @@ -2182,11 +2186,11 @@ packages: vite: '>=7.3.2' vue: ^3.2.25 - '@vitest/expect@4.1.4': - resolution: {integrity: sha512-iPBpra+VDuXmBFI3FMKHSFXp3Gx5HfmSCE8X67Dn+bwephCnQCaB7qWK2ldHa+8ncN8hJU8VTMcxjPpyMkUjww==} + '@vitest/expect@4.1.5': + resolution: {integrity: sha512-PWBaRY5JoKuRnHlUHfpV/KohFylaDZTupcXN1H9vYryNLOnitSw60Mw9IAE2r67NbwwzBw/Cc/8q9BK3kIX8Kw==} - '@vitest/mocker@4.1.4': - resolution: {integrity: sha512-R9HTZBhW6yCSGbGQnDnH3QHfJxokKN4KB+Yvk9Q1le7eQNYwiCyKxmLmurSpFy6BzJanSLuEUDrD+j97Q+ZLPg==} + '@vitest/mocker@4.1.5': + resolution: {integrity: sha512-/x2EmFC4mT4NNzqvC3fmesuV97w5FC903KPmey4gsnJiMQ3Be1IlDKVaDaG8iqaLFHqJ2FVEkxZk5VmeLjIItw==} peerDependencies: msw: ^2.4.9 vite: '>=7.3.2' @@ -2196,20 +2200,20 @@ packages: vite: optional: true - '@vitest/pretty-format@4.1.4': - resolution: {integrity: sha512-ddmDHU0gjEUyEVLxtZa7xamrpIefdEETu3nZjWtHeZX4QxqJ7tRxSteHVXJOcr8jhiLoGAhkK4WJ3WqBpjx42A==} + '@vitest/pretty-format@4.1.5': + resolution: {integrity: sha512-7I3q6l5qr03dVfMX2wCo9FxwSJbPdwKjy2uu/YPpU3wfHvIL4QHwVRp57OfGrDFeUJ8/8QdfBKIV12FTtLn00g==} - '@vitest/runner@4.1.4': - resolution: {integrity: sha512-xTp7VZ5aXP5ZJrn15UtJUWlx6qXLnGtF6jNxHepdPHpMfz/aVPx+htHtgcAL2mDXJgKhpoo2e9/hVJsIeFbytQ==} + '@vitest/runner@4.1.5': + resolution: {integrity: sha512-2D+o7Pr82IEO46YPpoA/YU0neeyr6FTerQb5Ro7BUnBuv6NQtT/kmVnczngiMEBhzgqz2UZYl5gArejsyERDSQ==} - '@vitest/snapshot@4.1.4': - resolution: {integrity: sha512-MCjCFgaS8aZz+m5nTcEcgk/xhWv0rEH4Yl53PPlMXOZ1/Ka2VcZU6CJ+MgYCZbcJvzGhQRjVrGQNZqkGPttIKw==} + '@vitest/snapshot@4.1.5': + resolution: {integrity: sha512-zypXEt4KH/XgKGPUz4eC2AvErYx0My5hfL8oDb1HzGFpEk1P62bxSohdyOmvz+d9UJwanI68MKwr2EquOaOgMQ==} - '@vitest/spy@4.1.4': - resolution: {integrity: sha512-XxNdAsKW7C+FLydqFJLb5KhJtl3PGCMmYwFRfhvIgxJvLSXhhVI1zM8f1qD3Zg7RCjTSzDVyct6sghs9UEgBEQ==} + '@vitest/spy@4.1.5': + resolution: {integrity: sha512-2lNOsh6+R2Idnf1TCZqSwYlKN2E/iDlD8sgU59kYVl+OMDmvldO1VDk39smRfpUNwYpNRVn3w4YfuC7KfbBnkQ==} - '@vitest/utils@4.1.4': - resolution: {integrity: sha512-13QMT+eysM5uVGa1rG4kegGYNp6cnQcsTc67ELFbhNLQO+vgsygtYJx2khvdt4gVQqSSpC/KT5FZZxUpP3Oatw==} + '@vitest/utils@4.1.5': + resolution: {integrity: sha512-76wdkrmfXfqGjueGgnb45ITPyUi1ycZ4IHgC2bhPDUfWHklY/q3MdLOAB+TF1e6xfl8NxNY0ZYaPCFNWSsw3Ug==} '@vue/compiler-core@3.5.30': resolution: {integrity: sha512-s3DfdZkcu/qExZ+td75015ljzHc6vE+30cFMGRPROYjqkroYI5NV2X1yAMX9UeyBNWB9MxCfPcsjpLS11nzkkw==} @@ -2459,8 +2463,8 @@ packages: resolution: {integrity: sha512-lGe34o6EHj9y3Kts9R4ZYs/Gr+6N7MCaMlIFA3F1R2O5/m7K06AxfSeO5530PEERE6/WyEg3lsuyw4GHlPZHog==} engines: {node: ^4.5.0 || >= 5.9} - baseline-browser-mapping@2.10.19: - resolution: {integrity: sha512-qCkNLi2sfBOn8XhZQ0FXsT1Ki/Yo5P90hrkRamVFRS7/KV9hpfA4HkoWNU152+8w0zPjnxo5psx5NL3PSGgv5g==} + baseline-browser-mapping@2.10.21: + resolution: {integrity: sha512-Q+rUQ7Uz8AHM7DEaNdwvfFCTq7a43lNTzuS94eiWqwyxfV/wJv+oUivef51T91mmRY4d4A1u9rcSvkeufCVXlA==} engines: {node: '>=6.0.0'} hasBin: true @@ -2500,9 +2504,6 @@ packages: brace-expansion@1.1.14: resolution: {integrity: sha512-MWPGfDxnyzKU7rNOW9SP/c50vi3xrmrua/+6hfPbCS2ABNWfx24vPidzvC7krjU/RTo235sV776ymlsMtGKj8g==} - brace-expansion@2.0.3: - resolution: {integrity: sha512-MCV/fYJEbqx68aE58kv2cA/kiky1G8vux3OR6/jbS+jIMe/6fJWa0DTzJU7dqijOWYwHi1t29FlfYI9uytqlpA==} - brace-expansion@5.0.5: resolution: {integrity: sha512-VZznLgtwhn+Mact9tfiwx64fA9erHH/MCXEUfB/0bX/6Fz6ny5EGTXYltMocqg4xFAQZtnO3DHWWXi8RiuN7cQ==} engines: {node: 18 || 20 || >=22} @@ -2553,8 +2554,8 @@ packages: resolution: {integrity: sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==} engines: {node: '>=10'} - caniuse-lite@1.0.30001788: - resolution: {integrity: sha512-6q8HFp+lOQtcf7wBK+uEenxymVWkGKkjFpCvw5W25cmMwEDU45p1xQFBQv8JDlMMry7eNxyBaR+qxgmTUZkIRQ==} + caniuse-lite@1.0.30001790: + resolution: {integrity: sha512-bOoxfJPyYo+ds6W0YfptaCWbFnJYjh2Y1Eow5lRv+vI2u8ganPZqNm1JwNh0t2ELQCqIWg4B3dWEusgAmsoyOw==} cassandra-driver@4.8.0: resolution: {integrity: sha512-HritfMGq9V7SuESeSodHvArs0mLuMk7uh+7hQK2lqdvXrvm50aWxb4RPxkK3mPDdsgHjJ427xNRFITMH2ei+Sw==} @@ -2883,8 +2884,8 @@ packages: engines: {node: '>=0.12.18'} hasBin: true - electron-to-chromium@1.5.340: - resolution: {integrity: sha512-908qahOGocRMinT2nM3ajCEM99H4iPdv84eagPP3FfZy/1ZGeOy2CZYzjhms81ckOPCXPlW7LkY4XpxD8r1DrA==} + electron-to-chromium@1.5.343: + resolution: {integrity: sha512-YHnQ3MXI08icvL9ZKnEBy05F2EQ8ob01UaMOuMbM8l+4UcAq6MPPbBTJBbsBUg3H8JeZNt+O4fjsoWth3p6IFg==} emoji-regex@8.0.0: resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==} @@ -3077,8 +3078,8 @@ packages: peerDependencies: eslint: ^7.0.0 || ^8.0.0 || ^9.0.0 - eslint-plugin-react-hooks@7.1.0: - resolution: {integrity: sha512-LDicyhrRFrIaheDYryeM2W8gWyZXnAs4zIr2WVPiOSeTmIu2RjR4x/9N0xLaRWZ+9hssBDGo3AadcohuzAvSvg==} + eslint-plugin-react-hooks@7.1.1: + resolution: {integrity: sha512-f2I7Gw6JbvCexzIInuSbZpfdQ44D7iqdWX01FKLvrPgqxoE7oMj8clOfto8U6vYiz4yd5oKu39rRSVOe1zRu0g==} engines: {node: '>=18'} peerDependencies: eslint: ^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 || ^9.0.0 || ^10.0.0 @@ -3114,8 +3115,8 @@ packages: resolution: {integrity: sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==} engines: {node: ^20.19.0 || ^22.13.0 || >=24} - eslint@10.2.0: - resolution: {integrity: sha512-+L0vBFYGIpSNIt/KWTpFonPrqYvgKw1eUI5Vn7mEogrQcWtWYtNQ7dNqC+px/J0idT3BAkiWrhfS7k+Tum8TUA==} + eslint@10.2.1: + resolution: {integrity: sha512-wiyGaKsDgqXvF40P8mDwiUp/KQjE1FdrIEJsM8PZ3XCiniTMXS3OHWWUe5FI5agoCnr8x4xPrTDZuxsBlNHl+Q==} engines: {node: ^20.19.0 || ^22.13.0 || >=24} hasBin: true peerDependencies: @@ -3536,8 +3537,8 @@ packages: i18next-browser-languagedetector@8.2.1: resolution: {integrity: sha512-bZg8+4bdmaOiApD7N7BPT9W8MLZG+nPTOFlLiJiT8uzKXFjhxw4v2ierCXOwB5sFDMtuA5G4kgYZ0AznZxQ/cw==} - i18next@26.0.5: - resolution: {integrity: sha512-9uHb4T27TdV36phJXcbpnRPt5yzAfqHXVrdASvmHZyPuZJtrLythd+GyXhiaHV5LlpuuskbAqhwPjmfTbKbi8w==} + i18next@26.0.6: + resolution: {integrity: sha512-A4U6eCXodIbrhf8EarRurB9/4ebyaurH4+fu4gig9bqxmpSt+fCAFm/GpRQDcN1Xzu/LdFCx4nYHsnM1edIIbg==} peerDependencies: typescript: ^5 || ^6 peerDependenciesMeta: @@ -4161,10 +4162,6 @@ packages: minimatch@3.1.5: resolution: {integrity: sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==} - minimatch@9.0.9: - resolution: {integrity: sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==} - engines: {node: '>=16 || 14 >=14.17'} - minimist@1.2.8: resolution: {integrity: sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==} @@ -4295,8 +4292,8 @@ packages: resolution: {integrity: sha512-VBlAiynj3VMLrotgwOS3OyECFxas5y7ltLcK4t41lMUZeaK15Ym4QRkqN0EQKAFL42q9i21EPKjzLUPfltR72A==} engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} - node-releases@2.0.37: - resolution: {integrity: sha512-1h5gKZCF+pO/o3Iqt5Jp7wc9rH3eJJ0+nh/CIoiRwjRxde/hAHyLPXYN4V3CqKAbiZPSeJFSWHmJsbkicta0Eg==} + node-releases@2.0.38: + resolution: {integrity: sha512-3qT/88Y3FbH/Kx4szpQQ4HzUbVrHPKTLVpVocKiLfoYvw9XSGOX2FmD2d6DrXbVYyAQTF2HeF6My8jmzx7/CRw==} nodeify@1.0.1: resolution: {integrity: sha512-n7C2NyEze8GCo/z73KdbjRsBiLbv6eBn1FxwYKQ23IqGo7pQY3mhQan61Sv7eEDJCiyUjTVrVkXTzJCo1dW7Aw==} @@ -4600,8 +4597,8 @@ packages: peerDependencies: react: ^19.2.5 - react-hook-form@7.72.1: - resolution: {integrity: sha512-RhwBoy2ygeVZje+C+bwJ8g0NjTdBmDlJvAUHTxRjTmSUKPYsKfMphkS2sgEMotsY03bP358yEYlnUeZy//D9Ig==} + react-hook-form@7.73.1: + resolution: {integrity: sha512-VAfVYOPcx3piiEVQy95vyFmBwbVUsP/AUIN+mpFG8h11yshDd444nn0VyfaGWSRnhOLVgiDu7HIuBtAIzxn9dA==} engines: {node: '>=18.0.0'} peerDependencies: react: ^16.8.0 || ^17 || ^18 || ^19 @@ -4642,15 +4639,15 @@ packages: '@types/react': optional: true - react-router-dom@7.14.1: - resolution: {integrity: sha512-ZkrQuwwhGibjQLqH1eCdyiZyLWglPxzxdl5tgwgKEyCSGC76vmAjleGocRe3J/MLfzMUIKwaFJWpFVJhK3d2xA==} + react-router-dom@7.14.2: + resolution: {integrity: sha512-YZcM5ES8jJSM+KrJ9BdvHHqlnGTg5tH3sC5ChFRj4inosKctdyzBDhOyyHdGk597q2OT6NTrCA1OvB/YDwfekQ==} engines: {node: '>=20.0.0'} peerDependencies: react: '>=18' react-dom: '>=18' - react-router@7.14.1: - resolution: {integrity: sha512-5BCvFskyAAVumqhEKh/iPhLOIkfxcEUz8WqFIARCkMg8hZZzDYX9CtwxXA0e+qT8zAxmMC0x3Ckb9iMONwc5jg==} + react-router@7.14.2: + resolution: {integrity: sha512-yCqNne6I8IB6rVCH7XUvlBK7/QKyqypBFGv+8dj4QBFJiiRX+FG7/nkdAvGElyvVZ/HQP5N19wzteuTARXi5Gw==} engines: {node: '>=20.0.0'} peerDependencies: react: '>=18' @@ -5072,8 +5069,8 @@ packages: resolution: {integrity: sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==} engines: {node: '>= 0.8'} - std-env@4.0.0: - resolution: {integrity: sha512-zUMPtQ/HBY3/50VbpkupYHbRroTRZJPRLvreamgErJVys0ceuzMkD44J/QjqhHjOzK42GQ3QZIeFG1OYfOtKqQ==} + std-env@4.1.0: + resolution: {integrity: sha512-Rq7ybcX2RuC55r9oaPVEW7/xu3tj8u4GeBYHBWCychFtzMIr86A7e3PPEBPT37sHStKX3+TiX/Fr/ACmJLVlLQ==} stop-iteration-iterator@1.1.0: resolution: {integrity: sha512-eLoXW/DHyl62zxY4SCaIgnRhuMr6ri4juEYARS8E6sCEqzKpOiE521Ucofdx+KnDZl5xmvGYaaKCk5FEOxJCoQ==} @@ -5448,8 +5445,8 @@ packages: '@babel/core': ^7.0.0 vite: '>=7.3.2' - vite-plugin-static-copy@4.0.1: - resolution: {integrity: sha512-r3kQUrrimduikhyRm58ayemoxsgB8lZdn/JULLL4wpXHAZlYejtyZx7E/id7dwRtIOSYWu/tWvFjdEOTzso2MA==} + vite-plugin-static-copy@4.1.0: + resolution: {integrity: sha512-9XOarNV7LgP0KBB7AApxdgFikLXx3daZdqjC3AevYsL6MrUH62zphonLUs2a6LZc1HN1GY+vQdheZ8VVJb6dQQ==} engines: {node: ^22.0.0 || >=24.0.0} peerDependencies: vite: '>=7.3.2' @@ -5512,20 +5509,20 @@ packages: postcss: optional: true - vitest@4.1.4: - resolution: {integrity: sha512-tFuJqTxKb8AvfyqMfnavXdzfy3h3sWZRWwfluGbkeR7n0HUev+FmNgZ8SDrRBTVrVCjgH5cA21qGbCffMNtWvg==} + vitest@4.1.5: + resolution: {integrity: sha512-9Xx1v3/ih3m9hN+SbfkUyy0JAs72ap3r7joc87XL6jwF0jGg6mFBvQ1SrwaX+h8BlkX6Hz9shdd1uo6AF+ZGpg==} engines: {node: ^20.0.0 || ^22.0.0 || >=24.0.0} hasBin: true peerDependencies: '@edge-runtime/vm': '*' '@opentelemetry/api': ^1.9.0 '@types/node': ^20.0.0 || ^22.0.0 || >=24.0.0 - '@vitest/browser-playwright': 4.1.4 - '@vitest/browser-preview': 4.1.4 - '@vitest/browser-webdriverio': 4.1.4 - '@vitest/coverage-istanbul': 4.1.4 - '@vitest/coverage-v8': 4.1.4 - '@vitest/ui': 4.1.4 + '@vitest/browser-playwright': 4.1.5 + '@vitest/browser-preview': 4.1.5 + '@vitest/browser-webdriverio': 4.1.5 + '@vitest/coverage-istanbul': 4.1.5 + '@vitest/coverage-v8': 4.1.5 + '@vitest/ui': 4.1.5 happy-dom: '*' jsdom: '*' vite: '>=7.3.2' @@ -6277,47 +6274,52 @@ snapshots: '@esbuild/win32-x64@0.28.0': optional: true - '@eslint-community/eslint-utils@4.9.1(eslint@10.2.0)': + '@eslint-community/eslint-utils@4.9.1(eslint@10.2.1)': dependencies: - eslint: 10.2.0 + eslint: 10.2.1 eslint-visitor-keys: 3.4.3 '@eslint-community/regexpp@4.12.2': {} - '@eslint/config-array@0.23.4': + '@eslint/config-array@0.23.5': dependencies: - '@eslint/object-schema': 3.0.4 + '@eslint/object-schema': 3.0.5 debug: 4.4.3(supports-color@8.1.1) minimatch: 10.2.5 transitivePeerDependencies: - supports-color - '@eslint/config-helpers@0.5.4': + '@eslint/config-helpers@0.5.5': dependencies: - '@eslint/core': 1.2.0 + '@eslint/core': 1.2.1 - '@eslint/core@1.2.0': + '@eslint/core@1.2.1': dependencies: '@types/json-schema': 7.0.15 - '@eslint/object-schema@3.0.4': {} + '@eslint/object-schema@3.0.5': {} - '@eslint/plugin-kit@0.7.0': + '@eslint/plugin-kit@0.7.1': dependencies: - '@eslint/core': 1.2.0 + '@eslint/core': 1.2.1 levn: 0.4.1 '@exodus/bytes@1.15.0(@noble/hashes@1.8.0)': optionalDependencies: '@noble/hashes': 1.8.0 - '@humanfs/core@0.19.1': {} - - '@humanfs/node@0.16.7': + '@humanfs/core@0.19.2': dependencies: - '@humanfs/core': 0.19.1 + '@humanfs/types': 0.15.0 + + '@humanfs/node@0.16.8': + dependencies: + '@humanfs/core': 0.19.2 + '@humanfs/types': 0.15.0 '@humanwhocodes/retry': 0.4.3 + '@humanfs/types@0.15.0': {} + '@humanwhocodes/module-importer@1.0.1': {} '@humanwhocodes/retry@0.4.3': {} @@ -7118,15 +7120,15 @@ snapshots: dependencies: '@types/webidl-conversions': 7.0.3 - '@typescript-eslint/eslint-plugin@7.18.0(@typescript-eslint/parser@7.18.0(eslint@10.2.0)(typescript@6.0.3))(eslint@10.2.0)(typescript@6.0.3)': + '@typescript-eslint/eslint-plugin@7.18.0(@typescript-eslint/parser@7.18.0(eslint@10.2.1)(typescript@6.0.3))(eslint@10.2.1)(typescript@6.0.3)': dependencies: '@eslint-community/regexpp': 4.12.2 - '@typescript-eslint/parser': 7.18.0(eslint@10.2.0)(typescript@6.0.3) + '@typescript-eslint/parser': 7.18.0(eslint@10.2.1)(typescript@6.0.3) '@typescript-eslint/scope-manager': 7.18.0 - '@typescript-eslint/type-utils': 7.18.0(eslint@10.2.0)(typescript@6.0.3) - '@typescript-eslint/utils': 7.18.0(eslint@10.2.0)(typescript@6.0.3) + '@typescript-eslint/type-utils': 7.18.0(eslint@10.2.1)(typescript@6.0.3) + '@typescript-eslint/utils': 7.18.0(eslint@10.2.1)(typescript@6.0.3) '@typescript-eslint/visitor-keys': 7.18.0 - eslint: 10.2.0 + eslint: 10.2.1 graphemer: 1.4.0 ignore: 5.3.2 natural-compare: 1.4.0 @@ -7136,15 +7138,15 @@ snapshots: transitivePeerDependencies: - supports-color - '@typescript-eslint/eslint-plugin@8.58.2(@typescript-eslint/parser@8.58.2(eslint@10.2.0)(typescript@6.0.3))(eslint@10.2.0)(typescript@6.0.3)': + '@typescript-eslint/eslint-plugin@8.59.0(@typescript-eslint/parser@8.59.0(eslint@10.2.1)(typescript@6.0.3))(eslint@10.2.1)(typescript@6.0.3)': dependencies: '@eslint-community/regexpp': 4.12.2 - '@typescript-eslint/parser': 8.58.2(eslint@10.2.0)(typescript@6.0.3) - '@typescript-eslint/scope-manager': 8.58.2 - '@typescript-eslint/type-utils': 8.58.2(eslint@10.2.0)(typescript@6.0.3) - '@typescript-eslint/utils': 8.58.2(eslint@10.2.0)(typescript@6.0.3) - '@typescript-eslint/visitor-keys': 8.58.2 - eslint: 10.2.0 + '@typescript-eslint/parser': 8.59.0(eslint@10.2.1)(typescript@6.0.3) + '@typescript-eslint/scope-manager': 8.59.0 + '@typescript-eslint/type-utils': 8.59.0(eslint@10.2.1)(typescript@6.0.3) + '@typescript-eslint/utils': 8.59.0(eslint@10.2.1)(typescript@6.0.3) + '@typescript-eslint/visitor-keys': 8.59.0 + eslint: 10.2.1 ignore: 7.0.5 natural-compare: 1.4.0 ts-api-utils: 2.5.0(typescript@6.0.3) @@ -7152,35 +7154,35 @@ snapshots: transitivePeerDependencies: - supports-color - '@typescript-eslint/parser@7.18.0(eslint@10.2.0)(typescript@6.0.3)': + '@typescript-eslint/parser@7.18.0(eslint@10.2.1)(typescript@6.0.3)': dependencies: '@typescript-eslint/scope-manager': 7.18.0 '@typescript-eslint/types': 7.18.0 '@typescript-eslint/typescript-estree': 7.18.0(typescript@6.0.3) '@typescript-eslint/visitor-keys': 7.18.0 debug: 4.4.3(supports-color@8.1.1) - eslint: 10.2.0 + eslint: 10.2.1 optionalDependencies: typescript: 6.0.3 transitivePeerDependencies: - supports-color - '@typescript-eslint/parser@8.58.2(eslint@10.2.0)(typescript@6.0.3)': + '@typescript-eslint/parser@8.59.0(eslint@10.2.1)(typescript@6.0.3)': dependencies: - '@typescript-eslint/scope-manager': 8.58.2 - '@typescript-eslint/types': 8.58.2 - '@typescript-eslint/typescript-estree': 8.58.2(typescript@6.0.3) - '@typescript-eslint/visitor-keys': 8.58.2 + '@typescript-eslint/scope-manager': 8.59.0 + '@typescript-eslint/types': 8.59.0 + '@typescript-eslint/typescript-estree': 8.59.0(typescript@6.0.3) + '@typescript-eslint/visitor-keys': 8.59.0 debug: 4.4.3(supports-color@8.1.1) - eslint: 10.2.0 + eslint: 10.2.1 typescript: 6.0.3 transitivePeerDependencies: - supports-color - '@typescript-eslint/project-service@8.58.2(typescript@6.0.3)': + '@typescript-eslint/project-service@8.59.0(typescript@6.0.3)': dependencies: - '@typescript-eslint/tsconfig-utils': 8.58.2(typescript@6.0.3) - '@typescript-eslint/types': 8.58.2 + '@typescript-eslint/tsconfig-utils': 8.59.0(typescript@6.0.3) + '@typescript-eslint/types': 8.59.0 debug: 4.4.3(supports-color@8.1.1) typescript: 6.0.3 transitivePeerDependencies: @@ -7191,34 +7193,34 @@ snapshots: '@typescript-eslint/types': 7.18.0 '@typescript-eslint/visitor-keys': 7.18.0 - '@typescript-eslint/scope-manager@8.58.2': + '@typescript-eslint/scope-manager@8.59.0': dependencies: - '@typescript-eslint/types': 8.58.2 - '@typescript-eslint/visitor-keys': 8.58.2 + '@typescript-eslint/types': 8.59.0 + '@typescript-eslint/visitor-keys': 8.59.0 - '@typescript-eslint/tsconfig-utils@8.58.2(typescript@6.0.3)': + '@typescript-eslint/tsconfig-utils@8.59.0(typescript@6.0.3)': dependencies: typescript: 6.0.3 - '@typescript-eslint/type-utils@7.18.0(eslint@10.2.0)(typescript@6.0.3)': + '@typescript-eslint/type-utils@7.18.0(eslint@10.2.1)(typescript@6.0.3)': dependencies: '@typescript-eslint/typescript-estree': 7.18.0(typescript@6.0.3) - '@typescript-eslint/utils': 7.18.0(eslint@10.2.0)(typescript@6.0.3) + '@typescript-eslint/utils': 7.18.0(eslint@10.2.1)(typescript@6.0.3) debug: 4.4.3(supports-color@8.1.1) - eslint: 10.2.0 + eslint: 10.2.1 ts-api-utils: 1.4.3(typescript@6.0.3) optionalDependencies: typescript: 6.0.3 transitivePeerDependencies: - supports-color - '@typescript-eslint/type-utils@8.58.2(eslint@10.2.0)(typescript@6.0.3)': + '@typescript-eslint/type-utils@8.59.0(eslint@10.2.1)(typescript@6.0.3)': dependencies: - '@typescript-eslint/types': 8.58.2 - '@typescript-eslint/typescript-estree': 8.58.2(typescript@6.0.3) - '@typescript-eslint/utils': 8.58.2(eslint@10.2.0)(typescript@6.0.3) + '@typescript-eslint/types': 8.59.0 + '@typescript-eslint/typescript-estree': 8.59.0(typescript@6.0.3) + '@typescript-eslint/utils': 8.59.0(eslint@10.2.1)(typescript@6.0.3) debug: 4.4.3(supports-color@8.1.1) - eslint: 10.2.0 + eslint: 10.2.1 ts-api-utils: 2.5.0(typescript@6.0.3) typescript: 6.0.3 transitivePeerDependencies: @@ -7226,7 +7228,7 @@ snapshots: '@typescript-eslint/types@7.18.0': {} - '@typescript-eslint/types@8.58.2': {} + '@typescript-eslint/types@8.59.0': {} '@typescript-eslint/typescript-estree@7.18.0(typescript@6.0.3)': dependencies: @@ -7235,7 +7237,7 @@ snapshots: debug: 4.4.3(supports-color@8.1.1) globby: 11.1.0 is-glob: 4.0.3 - minimatch: 9.0.9 + minimatch: 10.2.5 semver: 7.7.4 ts-api-utils: 1.4.3(typescript@6.0.3) optionalDependencies: @@ -7243,12 +7245,12 @@ snapshots: transitivePeerDependencies: - supports-color - '@typescript-eslint/typescript-estree@8.58.2(typescript@6.0.3)': + '@typescript-eslint/typescript-estree@8.59.0(typescript@6.0.3)': dependencies: - '@typescript-eslint/project-service': 8.58.2(typescript@6.0.3) - '@typescript-eslint/tsconfig-utils': 8.58.2(typescript@6.0.3) - '@typescript-eslint/types': 8.58.2 - '@typescript-eslint/visitor-keys': 8.58.2 + '@typescript-eslint/project-service': 8.59.0(typescript@6.0.3) + '@typescript-eslint/tsconfig-utils': 8.59.0(typescript@6.0.3) + '@typescript-eslint/types': 8.59.0 + '@typescript-eslint/visitor-keys': 8.59.0 debug: 4.4.3(supports-color@8.1.1) minimatch: 10.2.5 semver: 7.7.4 @@ -7258,24 +7260,24 @@ snapshots: transitivePeerDependencies: - supports-color - '@typescript-eslint/utils@7.18.0(eslint@10.2.0)(typescript@6.0.3)': + '@typescript-eslint/utils@7.18.0(eslint@10.2.1)(typescript@6.0.3)': dependencies: - '@eslint-community/eslint-utils': 4.9.1(eslint@10.2.0) + '@eslint-community/eslint-utils': 4.9.1(eslint@10.2.1) '@typescript-eslint/scope-manager': 7.18.0 '@typescript-eslint/types': 7.18.0 '@typescript-eslint/typescript-estree': 7.18.0(typescript@6.0.3) - eslint: 10.2.0 + eslint: 10.2.1 transitivePeerDependencies: - supports-color - typescript - '@typescript-eslint/utils@8.58.2(eslint@10.2.0)(typescript@6.0.3)': + '@typescript-eslint/utils@8.59.0(eslint@10.2.1)(typescript@6.0.3)': dependencies: - '@eslint-community/eslint-utils': 4.9.1(eslint@10.2.0) - '@typescript-eslint/scope-manager': 8.58.2 - '@typescript-eslint/types': 8.58.2 - '@typescript-eslint/typescript-estree': 8.58.2(typescript@6.0.3) - eslint: 10.2.0 + '@eslint-community/eslint-utils': 4.9.1(eslint@10.2.1) + '@typescript-eslint/scope-manager': 8.59.0 + '@typescript-eslint/types': 8.59.0 + '@typescript-eslint/typescript-estree': 8.59.0(typescript@6.0.3) + eslint: 10.2.1 typescript: 6.0.3 transitivePeerDependencies: - supports-color @@ -7285,9 +7287,9 @@ snapshots: '@typescript-eslint/types': 7.18.0 eslint-visitor-keys: 3.4.3 - '@typescript-eslint/visitor-keys@8.58.2': + '@typescript-eslint/visitor-keys@8.59.0': dependencies: - '@typescript-eslint/types': 8.58.2 + '@typescript-eslint/types': 8.59.0 eslint-visitor-keys: 5.0.1 '@typespec/ts-http-runtime@0.3.5': @@ -7360,44 +7362,44 @@ snapshots: vite: 8.0.8(@types/node@25.6.0)(esbuild@0.28.0)(tsx@4.21.0) vue: 3.5.30(typescript@6.0.3) - '@vitest/expect@4.1.4': + '@vitest/expect@4.1.5': dependencies: '@standard-schema/spec': 1.1.0 '@types/chai': 5.2.3 - '@vitest/spy': 4.1.4 - '@vitest/utils': 4.1.4 + '@vitest/spy': 4.1.5 + '@vitest/utils': 4.1.5 chai: 6.2.2 tinyrainbow: 3.1.0 - '@vitest/mocker@4.1.4(vite@8.0.8(@types/node@25.6.0)(esbuild@0.28.0)(tsx@4.21.0))': + '@vitest/mocker@4.1.5(vite@8.0.8(@types/node@25.6.0)(esbuild@0.28.0)(tsx@4.21.0))': dependencies: - '@vitest/spy': 4.1.4 + '@vitest/spy': 4.1.5 estree-walker: 3.0.3 magic-string: 0.30.21 optionalDependencies: vite: 8.0.8(@types/node@25.6.0)(esbuild@0.28.0)(tsx@4.21.0) - '@vitest/pretty-format@4.1.4': + '@vitest/pretty-format@4.1.5': dependencies: tinyrainbow: 3.1.0 - '@vitest/runner@4.1.4': + '@vitest/runner@4.1.5': dependencies: - '@vitest/utils': 4.1.4 + '@vitest/utils': 4.1.5 pathe: 2.0.3 - '@vitest/snapshot@4.1.4': + '@vitest/snapshot@4.1.5': dependencies: - '@vitest/pretty-format': 4.1.4 - '@vitest/utils': 4.1.4 + '@vitest/pretty-format': 4.1.5 + '@vitest/utils': 4.1.5 magic-string: 0.30.21 pathe: 2.0.3 - '@vitest/spy@4.1.4': {} + '@vitest/spy@4.1.5': {} - '@vitest/utils@4.1.4': + '@vitest/utils@4.1.5': dependencies: - '@vitest/pretty-format': 4.1.4 + '@vitest/pretty-format': 4.1.5 convert-source-map: 2.0.0 tinyrainbow: 3.1.0 @@ -7662,7 +7664,7 @@ snapshots: base64id@2.0.0: {} - baseline-browser-mapping@2.10.19: {} + baseline-browser-mapping@2.10.21: {} basic-ftp@5.3.0: {} @@ -7708,10 +7710,6 @@ snapshots: balanced-match: 1.0.2 concat-map: 0.0.1 - brace-expansion@2.0.3: - dependencies: - balanced-match: 1.0.2 - brace-expansion@5.0.5: dependencies: balanced-match: 4.0.4 @@ -7724,10 +7722,10 @@ snapshots: browserslist@4.28.2: dependencies: - baseline-browser-mapping: 2.10.19 - caniuse-lite: 1.0.30001788 - electron-to-chromium: 1.5.340 - node-releases: 2.0.37 + baseline-browser-mapping: 2.10.21 + caniuse-lite: 1.0.30001790 + electron-to-chromium: 1.5.343 + node-releases: 2.0.38 update-browserslist-db: 1.2.3(browserslist@4.28.2) bson@7.2.0: {} @@ -7764,7 +7762,7 @@ snapshots: camelcase@6.3.0: {} - caniuse-lite@1.0.30001788: {} + caniuse-lite@1.0.30001790: {} cassandra-driver@4.8.0: dependencies: @@ -8047,7 +8045,7 @@ snapshots: ejs@5.0.2: {} - electron-to-chromium@1.5.340: {} + electron-to-chromium@1.5.343: {} emoji-regex@8.0.0: {} @@ -8254,24 +8252,24 @@ snapshots: optionalDependencies: source-map: 0.6.1 - eslint-compat-utils@0.5.1(eslint@10.2.0): + eslint-compat-utils@0.5.1(eslint@10.2.1): dependencies: - eslint: 10.2.0 + eslint: 10.2.1 semver: 7.7.4 - eslint-config-etherpad@4.0.5(eslint@10.2.0)(typescript@6.0.3): + eslint-config-etherpad@4.0.5(eslint@10.2.1)(typescript@6.0.3): dependencies: '@rushstack/eslint-patch': 1.16.1 - '@typescript-eslint/eslint-plugin': 7.18.0(@typescript-eslint/parser@7.18.0(eslint@10.2.0)(typescript@6.0.3))(eslint@10.2.0)(typescript@6.0.3) - '@typescript-eslint/parser': 7.18.0(eslint@10.2.0)(typescript@6.0.3) - eslint-import-resolver-typescript: 3.9.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@7.18.0(eslint@10.2.0)(typescript@6.0.3))(eslint@10.2.0))(eslint@10.2.0) - eslint-plugin-cypress: 2.15.2(eslint@10.2.0) - eslint-plugin-eslint-comments: 3.2.0(eslint@10.2.0) - eslint-plugin-import: 2.32.0(@typescript-eslint/parser@7.18.0(eslint@10.2.0)(typescript@6.0.3))(eslint-import-resolver-typescript@3.9.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@7.18.0(eslint@10.2.0)(typescript@6.0.3))(eslint@10.2.0))(eslint@10.2.0))(eslint@10.2.0) - eslint-plugin-mocha: 10.5.0(eslint@10.2.0) - eslint-plugin-n: 17.24.0(eslint@10.2.0)(typescript@6.0.3) - eslint-plugin-prefer-arrow: 1.2.3(eslint@10.2.0) - eslint-plugin-promise: 6.6.0(eslint@10.2.0) + '@typescript-eslint/eslint-plugin': 7.18.0(@typescript-eslint/parser@7.18.0(eslint@10.2.1)(typescript@6.0.3))(eslint@10.2.1)(typescript@6.0.3) + '@typescript-eslint/parser': 7.18.0(eslint@10.2.1)(typescript@6.0.3) + eslint-import-resolver-typescript: 3.9.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@7.18.0(eslint@10.2.1)(typescript@6.0.3))(eslint@10.2.1))(eslint@10.2.1) + eslint-plugin-cypress: 2.15.2(eslint@10.2.1) + eslint-plugin-eslint-comments: 3.2.0(eslint@10.2.1) + eslint-plugin-import: 2.32.0(@typescript-eslint/parser@7.18.0(eslint@10.2.1)(typescript@6.0.3))(eslint-import-resolver-typescript@3.9.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@7.18.0(eslint@10.2.1)(typescript@6.0.3))(eslint@10.2.1))(eslint@10.2.1))(eslint@10.2.1) + eslint-plugin-mocha: 10.5.0(eslint@10.2.1) + eslint-plugin-n: 17.24.0(eslint@10.2.1)(typescript@6.0.3) + eslint-plugin-prefer-arrow: 1.2.3(eslint@10.2.1) + eslint-plugin-promise: 6.6.0(eslint@10.2.1) eslint-plugin-you-dont-need-lodash-underscore: 6.14.0 transitivePeerDependencies: - eslint @@ -8288,51 +8286,51 @@ snapshots: transitivePeerDependencies: - supports-color - eslint-import-resolver-typescript@3.9.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@7.18.0(eslint@10.2.0)(typescript@6.0.3))(eslint@10.2.0))(eslint@10.2.0): + eslint-import-resolver-typescript@3.9.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@7.18.0(eslint@10.2.1)(typescript@6.0.3))(eslint@10.2.1))(eslint@10.2.1): dependencies: '@nolyfill/is-core-module': 1.0.39 debug: 4.4.3(supports-color@8.1.1) - eslint: 10.2.0 + eslint: 10.2.1 get-tsconfig: 4.14.0 is-bun-module: 1.3.0 rspack-resolver: 1.3.0 stable-hash: 0.0.5 tinyglobby: 0.2.16 optionalDependencies: - eslint-plugin-import: 2.32.0(@typescript-eslint/parser@7.18.0(eslint@10.2.0)(typescript@6.0.3))(eslint-import-resolver-typescript@3.9.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@7.18.0(eslint@10.2.0)(typescript@6.0.3))(eslint@10.2.0))(eslint@10.2.0))(eslint@10.2.0) + eslint-plugin-import: 2.32.0(@typescript-eslint/parser@7.18.0(eslint@10.2.1)(typescript@6.0.3))(eslint-import-resolver-typescript@3.9.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@7.18.0(eslint@10.2.1)(typescript@6.0.3))(eslint@10.2.1))(eslint@10.2.1))(eslint@10.2.1) transitivePeerDependencies: - supports-color - eslint-module-utils@2.12.1(@typescript-eslint/parser@7.18.0(eslint@10.2.0)(typescript@6.0.3))(eslint-import-resolver-node@0.3.10)(eslint-import-resolver-typescript@3.9.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@7.18.0(eslint@10.2.0)(typescript@6.0.3))(eslint@10.2.0))(eslint@10.2.0))(eslint@10.2.0): + eslint-module-utils@2.12.1(@typescript-eslint/parser@7.18.0(eslint@10.2.1)(typescript@6.0.3))(eslint-import-resolver-node@0.3.10)(eslint-import-resolver-typescript@3.9.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@7.18.0(eslint@10.2.1)(typescript@6.0.3))(eslint@10.2.1))(eslint@10.2.1))(eslint@10.2.1): dependencies: debug: 3.2.7 optionalDependencies: - '@typescript-eslint/parser': 7.18.0(eslint@10.2.0)(typescript@6.0.3) - eslint: 10.2.0 + '@typescript-eslint/parser': 7.18.0(eslint@10.2.1)(typescript@6.0.3) + eslint: 10.2.1 eslint-import-resolver-node: 0.3.10 - eslint-import-resolver-typescript: 3.9.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@7.18.0(eslint@10.2.0)(typescript@6.0.3))(eslint@10.2.0))(eslint@10.2.0) + eslint-import-resolver-typescript: 3.9.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@7.18.0(eslint@10.2.1)(typescript@6.0.3))(eslint@10.2.1))(eslint@10.2.1) transitivePeerDependencies: - supports-color - eslint-plugin-cypress@2.15.2(eslint@10.2.0): + eslint-plugin-cypress@2.15.2(eslint@10.2.1): dependencies: - eslint: 10.2.0 + eslint: 10.2.1 globals: 13.24.0 - eslint-plugin-es-x@7.8.0(eslint@10.2.0): + eslint-plugin-es-x@7.8.0(eslint@10.2.1): dependencies: - '@eslint-community/eslint-utils': 4.9.1(eslint@10.2.0) + '@eslint-community/eslint-utils': 4.9.1(eslint@10.2.1) '@eslint-community/regexpp': 4.12.2 - eslint: 10.2.0 - eslint-compat-utils: 0.5.1(eslint@10.2.0) + eslint: 10.2.1 + eslint-compat-utils: 0.5.1(eslint@10.2.1) - eslint-plugin-eslint-comments@3.2.0(eslint@10.2.0): + eslint-plugin-eslint-comments@3.2.0(eslint@10.2.1): dependencies: escape-string-regexp: 1.0.5 - eslint: 10.2.0 + eslint: 10.2.1 ignore: 5.3.2 - eslint-plugin-import@2.32.0(@typescript-eslint/parser@7.18.0(eslint@10.2.0)(typescript@6.0.3))(eslint-import-resolver-typescript@3.9.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@7.18.0(eslint@10.2.0)(typescript@6.0.3))(eslint@10.2.0))(eslint@10.2.0))(eslint@10.2.0): + eslint-plugin-import@2.32.0(@typescript-eslint/parser@7.18.0(eslint@10.2.1)(typescript@6.0.3))(eslint-import-resolver-typescript@3.9.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@7.18.0(eslint@10.2.1)(typescript@6.0.3))(eslint@10.2.1))(eslint@10.2.1))(eslint@10.2.1): dependencies: '@rtsao/scc': 1.1.0 array-includes: 3.1.9 @@ -8341,9 +8339,9 @@ snapshots: array.prototype.flatmap: 1.3.3 debug: 3.2.7 doctrine: 2.1.0 - eslint: 10.2.0 + eslint: 10.2.1 eslint-import-resolver-node: 0.3.10 - eslint-module-utils: 2.12.1(@typescript-eslint/parser@7.18.0(eslint@10.2.0)(typescript@6.0.3))(eslint-import-resolver-node@0.3.10)(eslint-import-resolver-typescript@3.9.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@7.18.0(eslint@10.2.0)(typescript@6.0.3))(eslint@10.2.0))(eslint@10.2.0))(eslint@10.2.0) + eslint-module-utils: 2.12.1(@typescript-eslint/parser@7.18.0(eslint@10.2.1)(typescript@6.0.3))(eslint-import-resolver-node@0.3.10)(eslint-import-resolver-typescript@3.9.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@7.18.0(eslint@10.2.1)(typescript@6.0.3))(eslint@10.2.1))(eslint@10.2.1))(eslint@10.2.1) hasown: 2.0.2 is-core-module: 2.16.1 is-glob: 4.0.3 @@ -8355,25 +8353,25 @@ snapshots: string.prototype.trimend: 1.0.9 tsconfig-paths: 3.15.0 optionalDependencies: - '@typescript-eslint/parser': 7.18.0(eslint@10.2.0)(typescript@6.0.3) + '@typescript-eslint/parser': 7.18.0(eslint@10.2.1)(typescript@6.0.3) transitivePeerDependencies: - eslint-import-resolver-typescript - eslint-import-resolver-webpack - supports-color - eslint-plugin-mocha@10.5.0(eslint@10.2.0): + eslint-plugin-mocha@10.5.0(eslint@10.2.1): dependencies: - eslint: 10.2.0 - eslint-utils: 3.0.0(eslint@10.2.0) + eslint: 10.2.1 + eslint-utils: 3.0.0(eslint@10.2.1) globals: 13.24.0 rambda: 7.5.0 - eslint-plugin-n@17.24.0(eslint@10.2.0)(typescript@6.0.3): + eslint-plugin-n@17.24.0(eslint@10.2.1)(typescript@6.0.3): dependencies: - '@eslint-community/eslint-utils': 4.9.1(eslint@10.2.0) + '@eslint-community/eslint-utils': 4.9.1(eslint@10.2.1) enhanced-resolve: 5.20.1 - eslint: 10.2.0 - eslint-plugin-es-x: 7.8.0(eslint@10.2.0) + eslint: 10.2.1 + eslint-plugin-es-x: 7.8.0(eslint@10.2.1) get-tsconfig: 4.14.0 globals: 15.15.0 globrex: 0.1.2 @@ -8383,28 +8381,28 @@ snapshots: transitivePeerDependencies: - typescript - eslint-plugin-prefer-arrow@1.2.3(eslint@10.2.0): + eslint-plugin-prefer-arrow@1.2.3(eslint@10.2.1): dependencies: - eslint: 10.2.0 + eslint: 10.2.1 - eslint-plugin-promise@6.6.0(eslint@10.2.0): + eslint-plugin-promise@6.6.0(eslint@10.2.1): dependencies: - eslint: 10.2.0 + eslint: 10.2.1 - eslint-plugin-react-hooks@7.1.0(eslint@10.2.0): + eslint-plugin-react-hooks@7.1.1(eslint@10.2.1): dependencies: '@babel/core': 7.29.0 '@babel/parser': 7.29.2 - eslint: 10.2.0 + eslint: 10.2.1 hermes-parser: 0.25.1 zod: 4.3.6 zod-validation-error: 4.0.2(zod@4.3.6) transitivePeerDependencies: - supports-color - eslint-plugin-react-refresh@0.5.2(eslint@10.2.0): + eslint-plugin-react-refresh@0.5.2(eslint@10.2.1): dependencies: - eslint: 10.2.0 + eslint: 10.2.1 eslint-plugin-you-dont-need-lodash-underscore@6.14.0: dependencies: @@ -8417,9 +8415,9 @@ snapshots: esrecurse: 4.3.0 estraverse: 5.3.0 - eslint-utils@3.0.0(eslint@10.2.0): + eslint-utils@3.0.0(eslint@10.2.1): dependencies: - eslint: 10.2.0 + eslint: 10.2.1 eslint-visitor-keys: 2.1.0 eslint-visitor-keys@2.1.0: {} @@ -8428,15 +8426,15 @@ snapshots: eslint-visitor-keys@5.0.1: {} - eslint@10.2.0: + eslint@10.2.1: dependencies: - '@eslint-community/eslint-utils': 4.9.1(eslint@10.2.0) + '@eslint-community/eslint-utils': 4.9.1(eslint@10.2.1) '@eslint-community/regexpp': 4.12.2 - '@eslint/config-array': 0.23.4 - '@eslint/config-helpers': 0.5.4 - '@eslint/core': 1.2.0 - '@eslint/plugin-kit': 0.7.0 - '@humanfs/node': 0.16.7 + '@eslint/config-array': 0.23.5 + '@eslint/config-helpers': 0.5.5 + '@eslint/core': 1.2.1 + '@eslint/plugin-kit': 0.7.1 + '@humanfs/node': 0.16.8 '@humanwhocodes/module-importer': 1.0.1 '@humanwhocodes/retry': 0.4.3 '@types/estree': 1.0.8 @@ -8951,7 +8949,7 @@ snapshots: dependencies: '@babel/runtime': 7.28.6 - i18next@26.0.5(typescript@6.0.3): + i18next@26.0.6(typescript@6.0.3): dependencies: '@babel/runtime': 7.29.2 optionalDependencies: @@ -9542,10 +9540,6 @@ snapshots: dependencies: brace-expansion: 1.1.14 - minimatch@9.0.9: - dependencies: - brace-expansion: 2.0.3 - minimist@1.2.8: {} minipass@4.2.8: {} @@ -9662,7 +9656,7 @@ snapshots: node-domexception: 1.0.0 web-streams-polyfill: 3.3.3 - node-releases@2.0.37: {} + node-releases@2.0.38: {} nodeify@1.0.1: dependencies: @@ -9997,15 +9991,15 @@ snapshots: react: 19.2.5 scheduler: 0.27.0 - react-hook-form@7.72.1(react@19.2.5): + react-hook-form@7.73.1(react@19.2.5): dependencies: react: 19.2.5 - react-i18next@17.0.4(i18next@26.0.5(typescript@6.0.3))(react-dom@19.2.5(react@19.2.5))(react@19.2.5)(typescript@6.0.3): + react-i18next@17.0.4(i18next@26.0.6(typescript@6.0.3))(react-dom@19.2.5(react@19.2.5))(react@19.2.5)(typescript@6.0.3): dependencies: '@babel/runtime': 7.29.2 html-parse-stringify: 3.0.1 - i18next: 26.0.5(typescript@6.0.3) + i18next: 26.0.6(typescript@6.0.3) react: 19.2.5 use-sync-external-store: 1.6.0(react@19.2.5) optionalDependencies: @@ -10031,13 +10025,13 @@ snapshots: optionalDependencies: '@types/react': 19.2.14 - react-router-dom@7.14.1(react-dom@19.2.5(react@19.2.5))(react@19.2.5): + react-router-dom@7.14.2(react-dom@19.2.5(react@19.2.5))(react@19.2.5): dependencies: react: 19.2.5 react-dom: 19.2.5(react@19.2.5) - react-router: 7.14.1(react-dom@19.2.5(react@19.2.5))(react@19.2.5) + react-router: 7.14.2(react-dom@19.2.5(react@19.2.5))(react@19.2.5) - react-router@7.14.1(react-dom@19.2.5(react@19.2.5))(react@19.2.5): + react-router@7.14.2(react-dom@19.2.5(react@19.2.5))(react@19.2.5): dependencies: cookie: 1.1.1 react: 19.2.5 @@ -10542,7 +10536,7 @@ snapshots: statuses@2.0.2: {} - std-env@4.0.0: {} + std-env@4.1.0: {} stop-iteration-iterator@1.1.0: dependencies: @@ -10983,12 +10977,12 @@ snapshots: '@babel/core': 7.29.0 vite: rolldown-vite@7.2.10(@types/node@25.6.0)(tsx@4.21.0) - vite-plugin-static-copy@4.0.1(rolldown-vite@7.2.10(@types/node@25.6.0)(tsx@4.21.0)): + vite-plugin-static-copy@4.1.0(rolldown-vite@7.2.10(@types/node@25.6.0)(tsx@4.21.0)): dependencies: chokidar: 3.6.0 p-map: 7.0.4 picocolors: 1.1.1 - tinyglobby: 0.2.15 + tinyglobby: 0.2.16 vite: rolldown-vite@7.2.10(@types/node@25.6.0)(tsx@4.21.0) vite@8.0.8(@types/node@25.6.0)(esbuild@0.28.0)(tsx@4.21.0): @@ -11053,22 +11047,22 @@ snapshots: - universal-cookie - yaml - vitest@4.1.4(@opentelemetry/api@1.9.0)(@types/node@25.6.0)(jsdom@29.0.2(@noble/hashes@1.8.0))(vite@8.0.8(@types/node@25.6.0)(esbuild@0.28.0)(tsx@4.21.0)): + vitest@4.1.5(@opentelemetry/api@1.9.0)(@types/node@25.6.0)(jsdom@29.0.2(@noble/hashes@1.8.0))(vite@8.0.8(@types/node@25.6.0)(esbuild@0.28.0)(tsx@4.21.0)): dependencies: - '@vitest/expect': 4.1.4 - '@vitest/mocker': 4.1.4(vite@8.0.8(@types/node@25.6.0)(esbuild@0.28.0)(tsx@4.21.0)) - '@vitest/pretty-format': 4.1.4 - '@vitest/runner': 4.1.4 - '@vitest/snapshot': 4.1.4 - '@vitest/spy': 4.1.4 - '@vitest/utils': 4.1.4 + '@vitest/expect': 4.1.5 + '@vitest/mocker': 4.1.5(vite@8.0.8(@types/node@25.6.0)(esbuild@0.28.0)(tsx@4.21.0)) + '@vitest/pretty-format': 4.1.5 + '@vitest/runner': 4.1.5 + '@vitest/snapshot': 4.1.5 + '@vitest/spy': 4.1.5 + '@vitest/utils': 4.1.5 es-module-lexer: 2.0.0 expect-type: 1.3.0 magic-string: 0.30.21 obug: 2.1.1 pathe: 2.0.3 picomatch: 4.0.4 - std-env: 4.0.0 + std-env: 4.1.0 tinybench: 2.9.0 tinyexec: 1.1.1 tinyglobby: 0.2.16 diff --git a/src/package.json b/src/package.json index 75151ee03..0e7746c10 100644 --- a/src/package.json +++ b/src/package.json @@ -118,7 +118,7 @@ "@types/underscore": "^1.13.0", "@types/whatwg-mimetype": "^5.0.0", "chokidar": "^5.0.0", - "eslint": "^10.2.0", + "eslint": "^10.2.1", "eslint-config-etherpad": "^4.0.5", "etherpad-cli-client": "^3.0.5", "mocha": "^11.7.5", @@ -130,7 +130,7 @@ "split-grid": "^1.0.11", "supertest": "^7.2.2", "typescript": "^6.0.3", - "vitest": "^4.1.4" + "vitest": "^4.1.5" }, "engines": { "node": ">=20.0.0", From bd762a2fda3df07a3618e6aedceec8b9699f32f1 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 22 Apr 2026 18:39:08 +0100 Subject: [PATCH 004/388] build(deps): bump mysql2 from 3.22.1 to 3.22.2 (#7578) Bumps [mysql2](https://github.com/sidorares/node-mysql2) from 3.22.1 to 3.22.2. - [Release notes](https://github.com/sidorares/node-mysql2/releases) - [Changelog](https://github.com/sidorares/node-mysql2/blob/master/Changelog.md) - [Commits](https://github.com/sidorares/node-mysql2/compare/v3.22.1...v3.22.2) --- updated-dependencies: - dependency-name: mysql2 dependency-version: 3.22.2 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- pnpm-lock.yaml | 32 +++++++++++++++++++++----------- src/package.json | 2 +- 2 files changed, 22 insertions(+), 12 deletions(-) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index edc19d77d..b1177b2a2 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -256,8 +256,8 @@ importers: specifier: ^12.2.1 version: 12.3.1(@azure/core-client@1.10.1) mysql2: - specifier: ^3.22.0 - version: 3.22.1(@types/node@25.6.0) + specifier: ^3.22.2 + version: 3.22.2(@types/node@25.6.0) nano: specifier: ^11.0.5 version: 11.0.5 @@ -3545,10 +3545,6 @@ packages: typescript: optional: true - iconv-lite@0.7.0: - resolution: {integrity: sha512-cf6L2Ds3h57VVmkZe+Pn+5APsT7FpqJtEhhieDCvrE2MK5Qk9MyffgQyuxQTm6BChfeZNtcOLHp9IcWRVcIcBQ==} - engines: {node: '>=0.10.0'} - iconv-lite@0.7.2: resolution: {integrity: sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw==} engines: {node: '>=0.10.0'} @@ -4243,6 +4239,12 @@ packages: peerDependencies: '@types/node': '>= 8' + mysql2@3.22.2: + resolution: {integrity: sha512-snC/L6YoCJPFpozZo3p3hiOlt9ItQ7sCnLSziFLlIttEzsPhrdcPT8g21BiQ7Oqif25W4Xq1IFuBzBvoFYDf0Q==} + engines: {node: '>= 8.0'} + peerDependencies: + '@types/node': '>= 8' + named-placeholders@1.1.6: resolution: {integrity: sha512-Tz09sEL2EEuv5fFowm419c1+a/jSMiBjI9gHxVLrVdbUkkNUUfjsVYs9pVZu5oCon/kmRh9TfLEObFtkVxmY0w==} engines: {node: '>=8.0.0'} @@ -7697,7 +7699,7 @@ snapshots: content-type: 1.0.5 debug: 4.4.3(supports-color@8.1.1) http-errors: 2.0.1 - iconv-lite: 0.7.0 + iconv-lite: 0.7.2 on-finished: 2.4.1 qs: 6.15.0 raw-body: 3.0.2 @@ -8955,10 +8957,6 @@ snapshots: optionalDependencies: typescript: 6.0.3 - iconv-lite@0.7.0: - dependencies: - safer-buffer: 2.1.2 - iconv-lite@0.7.2: dependencies: safer-buffer: 2.1.2 @@ -9622,6 +9620,18 @@ snapshots: named-placeholders: 1.1.6 sql-escaper: 1.3.3 + mysql2@3.22.2(@types/node@25.6.0): + dependencies: + '@types/node': 25.6.0 + aws-ssl-profiles: 1.1.2 + denque: 2.1.0 + generate-function: 2.3.1 + iconv-lite: 0.7.2 + long: 5.3.2 + lru.min: 1.1.4 + named-placeholders: 1.1.6 + sql-escaper: 1.3.3 + named-placeholders@1.1.6: dependencies: lru.min: 1.1.4 diff --git a/src/package.json b/src/package.json index 0e7746c10..59a7e3d0a 100644 --- a/src/package.json +++ b/src/package.json @@ -60,7 +60,7 @@ "mime-types": "^3.0.2", "mongodb": "^7.1.1", "mssql": "^12.2.1", - "mysql2": "^3.22.0", + "mysql2": "^3.22.2", "nano": "^11.0.5", "oidc-provider": "9.8.2", "openapi-backend": "^5.16.1", From 6bb879ed03933d60e365ab3ed00fd6d966d84ccc Mon Sep 17 00:00:00 2001 From: John McLear Date: Wed, 22 Apr 2026 18:45:50 +0100 Subject: [PATCH 005/388] feat(packaging): add Debian (.deb) build via nfpm with systemd unit (#7559) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat(packaging): add Debian (.deb) build via nfpm with systemd unit First-class Debian packaging for Etherpad, producing signed-ready etherpad-lite__.deb artefacts for amd64 and arm64 from a single nfpm manifest. Installing the package gives users: - /opt/etherpad-lite with a prebuilt, self-contained node_modules/ — no pnpm required at runtime, just `nodejs (>= 20)`. - etherpad system user/group, created via `adduser` in preinst. - /etc/etherpad-lite/settings.json seeded from the template on first install, preserved across upgrades, removed on `purge`. - /var/lib/etherpad-lite owned by etherpad:etherpad, with the default dirty-DB retargeted there so ProtectSystem=strict works. - /lib/systemd/system/etherpad-lite.service — hardened unit (NoNewPrivileges, ProtectSystem=strict, ProtectHome, PrivateTmp, RestrictAddressFamilies) with Restart=on-failure. - /usr/bin/etherpad-lite CLI wrapper running `node --import tsx/esm`. CI (.github/workflows/deb-package.yml) triggers on v* tags, builds both arches via native runners (ubuntu-latest + ubuntu-24.04-arm), smoke-tests the amd64 package end-to-end (install → systemctl start → curl /health → purge → confirm user removed), and attaches the artefacts to the GitHub Release. Publishing to an APT repo (Cloudsmith, Launchpad PPA, self-hosted reprepro) is intentionally out of scope — needs a governance decision on who holds the signing key. Recipes are documented in packaging/README.md. Refs #7529 Co-Authored-By: Claude Opus 4.7 (1M context) * fix(deb): fail smoke test on /health timeout, tighten default-file perms, 2-space indent Addresses Qodo review feedback on #7559: 1. Smoke test false-positive: the `for` loop polling /health never failed the job if the endpoint stayed down — `curl && break || sleep 2` keeps returning 0 from the trailing `sleep`, so `set -e` never trips. CI could attach a broken .deb to a release. Fix: track success explicitly and exit 1 (plus dump journald logs for diagnostics) when the service never becomes healthy. 2. /etc/default/etherpad-lite was world-readable (0644). systemd loads it via `EnvironmentFile=…`, and Etherpad supports ${ENV_VAR}-substitution for secrets (DB_PASSWORD etc.), so any local user could read anything admins drop there. Fix: install the conffile as root:etherpad 0640 — only root and the service user can read it. 3. Indentation: reflow maintainer scripts from 4-space to 2-space to match the repo style rule. Co-Authored-By: Claude Opus 4.7 (1M context) --------- Co-authored-by: Claude Opus 4.7 (1M context) --- .github/workflows/deb-package.yml | 161 ++++++++++++++++++++++++ packaging/README.md | 89 +++++++++++++ packaging/bin/etherpad-lite | 18 +++ packaging/nfpm.yaml | 117 +++++++++++++++++ packaging/scripts/postinstall.sh | 68 ++++++++++ packaging/scripts/postremove.sh | 43 +++++++ packaging/scripts/preinstall.sh | 28 +++++ packaging/scripts/preremove.sh | 20 +++ packaging/systemd/etherpad-lite.default | 7 ++ packaging/systemd/etherpad-lite.service | 48 +++++++ 10 files changed, 599 insertions(+) create mode 100644 .github/workflows/deb-package.yml create mode 100644 packaging/README.md create mode 100755 packaging/bin/etherpad-lite create mode 100644 packaging/nfpm.yaml create mode 100755 packaging/scripts/postinstall.sh create mode 100755 packaging/scripts/postremove.sh create mode 100755 packaging/scripts/preinstall.sh create mode 100755 packaging/scripts/preremove.sh create mode 100644 packaging/systemd/etherpad-lite.default create mode 100644 packaging/systemd/etherpad-lite.service diff --git a/.github/workflows/deb-package.yml b/.github/workflows/deb-package.yml new file mode 100644 index 000000000..81f64f61e --- /dev/null +++ b/.github/workflows/deb-package.yml @@ -0,0 +1,161 @@ +name: Debian package +on: + push: + tags: + - 'v[0-9]+.[0-9]+.[0-9]+' + - 'v[0-9]+.[0-9]+.[0-9]+-*' + workflow_dispatch: + inputs: + ref: + description: 'Git ref to package (defaults to current)' + required: false + +permissions: + contents: write # attach release assets + +env: + NFPM_VERSION: v2.43.0 + +jobs: + build: + name: Build .deb (${{ matrix.arch }}) + runs-on: ${{ matrix.runner }} + strategy: + fail-fast: false + matrix: + include: + - arch: amd64 + runner: ubuntu-latest + - arch: arm64 + runner: ubuntu-24.04-arm + steps: + - name: Checkout + uses: actions/checkout@v6 + with: + ref: ${{ inputs.ref || github.ref }} + + - name: Resolve version + id: v + run: | + if [ "${GITHUB_REF_TYPE}" = "tag" ]; then + VERSION="${GITHUB_REF_NAME#v}" + else + VERSION="$(node -p "require('./package.json').version")" + fi + echo "version=${VERSION}" >>"$GITHUB_OUTPUT" + echo "Packaging version: ${VERSION}" + + - uses: pnpm/action-setup@v6 + with: + version: 10 + + - name: Setup Node + uses: actions/setup-node@v6 + with: + node-version: '22' + cache: pnpm + + - name: Install dependencies + run: pnpm install --frozen-lockfile + + - name: Build UI + admin + run: pnpm run build:etherpad + + - name: Install nfpm + run: | + set -e + NFPM_ARCH=amd64 + [ "${{ matrix.arch }}" = "arm64" ] && NFPM_ARCH=arm64 + curl -fsSL -o /tmp/nfpm.deb \ + "https://github.com/goreleaser/nfpm/releases/download/${NFPM_VERSION}/nfpm_${NFPM_VERSION#v}_${NFPM_ARCH}.deb" + sudo dpkg -i /tmp/nfpm.deb + + - name: Stage tree for packaging + run: | + set -eux + STAGE=staging/opt/etherpad-lite + mkdir -p "${STAGE}" + + # Production footprint = src/ + bin/ + node_modules/ + metadata. + cp -a src bin package.json pnpm-workspace.yaml README.md LICENSE \ + node_modules "${STAGE}/" + + # Make pnpm-workspace.yaml production-only (same trick Dockerfile uses). + printf 'packages:\n - src\n - bin\n' > "${STAGE}/pnpm-workspace.yaml" + + mkdir -p packaging/etc + cp settings.json.template packaging/etc/settings.json.dist + + # Purge test fixtures and dev caches from node_modules to shrink size. + find "${STAGE}/node_modules" -type d \ + \( -name test -o -name tests -o -name '__tests__' \ + -o -name example -o -name examples -o -name docs \) \ + -prune -exec rm -rf {} + 2>/dev/null || true + find "${STAGE}/node_modules" -type f \ + \( -name '*.md' -o -name '*.ts.map' -o -name '*.map' \ + -o -name 'CHANGELOG*' -o -name 'HISTORY*' \) \ + -delete 2>/dev/null || true + + - name: Build .deb + env: + VERSION: ${{ steps.v.outputs.version }} + ARCH: ${{ matrix.arch }} + run: | + mkdir -p dist + nfpm package --packager deb -f packaging/nfpm.yaml --target dist/ + + - name: Smoke-test the package (amd64 only) + if: matrix.arch == 'amd64' + run: | + set -eux + sudo apt-get update + sudo apt-get install -y nodejs + sudo dpkg -i dist/*.deb || sudo apt-get install -f -y + test -x /usr/bin/etherpad-lite + test -f /etc/etherpad-lite/settings.json + test -L /opt/etherpad-lite/settings.json + id etherpad + systemctl cat etherpad-lite.service + sudo systemctl start etherpad-lite + ok= + for i in $(seq 1 30); do + if curl -fsS http://127.0.0.1:9001/health; then + ok=1 + break + fi + sleep 2 + done + if [ -z "${ok}" ]; then + # Attach logs so the failing run is diagnosable. + sudo journalctl -u etherpad-lite --no-pager -n 200 || true + exit 1 + fi + sudo systemctl stop etherpad-lite + sudo dpkg --purge etherpad-lite + ! id etherpad 2>/dev/null + + - name: Upload artifact + uses: actions/upload-artifact@v4 + with: + name: etherpad-lite-${{ steps.v.outputs.version }}-${{ matrix.arch }}-deb + path: dist/*.deb + if-no-files-found: error + + release: + name: Attach to GitHub Release + needs: build + if: startsWith(github.ref, 'refs/tags/v') + runs-on: ubuntu-latest + permissions: + contents: write + steps: + - uses: actions/download-artifact@v4 + with: + path: dist + pattern: etherpad-lite-*-deb + merge-multiple: true + - name: Attach .deb files to release + uses: softprops/action-gh-release@v2 + with: + files: dist/*.deb + fail_on_unmatched_files: true diff --git a/packaging/README.md b/packaging/README.md new file mode 100644 index 000000000..20b5b5f7b --- /dev/null +++ b/packaging/README.md @@ -0,0 +1,89 @@ +# Etherpad Debian / RPM packaging + +Produces native `.deb` (and, with the same manifest, `.rpm` / `.apk`) +packages for Etherpad using [nfpm](https://nfpm.goreleaser.com). + +## Layout + +``` +packaging/ + nfpm.yaml # nfpm package manifest + bin/etherpad-lite # /usr/bin launcher + scripts/ # preinst / postinst / prerm / postrm + systemd/etherpad-lite.service + systemd/etherpad-lite.default + etc/settings.json.dist # populated in CI from settings.json.template +``` + +Built artefacts land in `./dist/`. + +## Building locally + +Prereqs: Node 22, pnpm 10+, nfpm. + +```sh +pnpm install --frozen-lockfile +pnpm run build:etherpad + +# Stage the tree the way CI does: +STAGE=staging/opt/etherpad-lite +mkdir -p "$STAGE" +cp -a src bin package.json pnpm-workspace.yaml README.md LICENSE \ + node_modules "$STAGE/" +printf 'packages:\n - src\n - bin\n' > "$STAGE/pnpm-workspace.yaml" +cp settings.json.template packaging/etc/settings.json.dist + +VERSION=$(node -p "require('./package.json').version") \ +ARCH=amd64 \ + nfpm package --packager deb -f packaging/nfpm.yaml --target dist/ +``` + +## Installing + +```sh +sudo apt install ./dist/etherpad-lite_2.6.1_amd64.deb +sudo systemctl start etherpad-lite +curl http://localhost:9001/health +``` + +`apt` will pull in `nodejs (>= 20)`; on Ubuntu 22.04 add NodeSource first: + +```sh +curl -fsSL https://deb.nodesource.com/setup_22.x | sudo -E bash - +``` + +## Configuration + +- Edit `/etc/etherpad-lite/settings.json`, then + `sudo systemctl restart etherpad-lite`. +- Environment overrides: `/etc/default/etherpad-lite`. +- Logs: `journalctl -u etherpad-lite -f`. +- Data (dirty-DB default): `/var/lib/etherpad-lite/`. + +## Upgrading + +`dpkg --install etherpad-lite_.deb` (or `apt install`) replaces the +app tree under `/opt/etherpad-lite` while preserving +`/etc/etherpad-lite/*` and `/var/lib/etherpad-lite/*`. The service is +restarted automatically. + +## Removing + +- `sudo apt remove etherpad-lite` — keeps config and data. +- `sudo apt purge etherpad-lite` — also removes config, data, and the + `etherpad` system user. + +## Publishing to an APT repository (follow-up) + +Out of scope here — requires credentials and ownership decisions. +Recipes once a repo is picked: + +- **Cloudsmith** (easiest, free OSS tier): + `cloudsmith push deb ether/etherpad-lite/any-distro/any-version dist/*.deb` +- **Launchpad PPA**: requires signed source packages (a `debian/` tree), + which nfpm does not produce — use `debuild` separately. +- **Self-hosted reprepro**: + `reprepro -b /srv/apt includedeb stable dist/*.deb` + +Wire the chosen option into `.github/workflows/deb-package.yml` after +the `release` job. diff --git a/packaging/bin/etherpad-lite b/packaging/bin/etherpad-lite new file mode 100755 index 000000000..20143154e --- /dev/null +++ b/packaging/bin/etherpad-lite @@ -0,0 +1,18 @@ +#!/bin/sh +# /usr/bin/etherpad-lite - thin wrapper that runs Etherpad in production mode. +# Invoked by the etherpad-lite.service systemd unit. +set -e + +APP_DIR="${ETHERPAD_DIR:-/opt/etherpad-lite}" +cd "${APP_DIR}" + +: "${NODE_ENV:=production}" +export NODE_ENV +export ETHERPAD_PRODUCTION=true + +# Run the server through tsx's ESM loader (shipped in node_modules). +# No pnpm needed at runtime. +exec node \ + --import "file://${APP_DIR}/src/node_modules/tsx/dist/esm/index.mjs" \ + "${APP_DIR}/src/node/server.ts" \ + "$@" diff --git a/packaging/nfpm.yaml b/packaging/nfpm.yaml new file mode 100644 index 000000000..eaf8ffce8 --- /dev/null +++ b/packaging/nfpm.yaml @@ -0,0 +1,117 @@ +# nfpm configuration for etherpad-lite Debian/RPM/APK packages. +# Build with: nfpm package --packager deb --target dist/ +# See: https://nfpm.goreleaser.com/configuration/ + +name: etherpad-lite +arch: ${ARCH} # amd64 | arm64 (exported by CI) +platform: linux +version: ${VERSION} # e.g. 2.6.1, stripped of leading "v" +version_schema: semver +release: "1" +section: web +priority: optional +maintainer: "Etherpad Foundation " +description: | + Etherpad is a real-time collaborative editor for the web. + This package installs Etherpad as a systemd service running + from /opt/etherpad-lite with configuration in /etc/etherpad-lite. +vendor: "Etherpad Foundation" +homepage: https://etherpad.org +license: Apache-2.0 + +depends: + - nodejs (>= 20) + - adduser + - ca-certificates + +recommends: + - libreoffice # enables DOC/DOCX/PDF/ODT export + - curl + +suggests: + - postgresql-client + - mariadb-client + +conflicts: + - etherpad # legacy bin/buildDebian.sh package name + +replaces: + - etherpad + +provides: + - etherpad + +# --------------------------------------------------------------------------- +# Contents. staging/ is populated by CI before invoking nfpm: +# staging/opt/etherpad-lite/ -- source + node_modules + built assets +# --------------------------------------------------------------------------- +contents: + - src: ./staging/opt/etherpad-lite + dst: /opt/etherpad-lite + type: tree + file_info: + mode: 0755 + owner: root + group: root + + - src: ./packaging/systemd/etherpad-lite.service + dst: /lib/systemd/system/etherpad-lite.service + file_info: + mode: 0644 + + # Default environment file (conffile: preserved on upgrade). + # Mode 0640 + group=etherpad so passwords/secrets admins drop in here + # are only readable by root and the etherpad service user — /etc/default + # is world-readable by default (0644), which would leak DB creds etc. + - src: ./packaging/systemd/etherpad-lite.default + dst: /etc/default/etherpad-lite + type: config|noreplace + file_info: + mode: 0640 + owner: root + group: etherpad + + - src: ./packaging/bin/etherpad-lite + dst: /usr/bin/etherpad-lite + file_info: + mode: 0755 + + # Template used by postinstall to seed /etc/etherpad-lite/settings.json. + # Intentionally NOT a conffile: postinstall creates the real settings.json + # once on first install and never touches it again, so upgrades don't + # prompt with dpkg merge dialogs. + - src: ./packaging/etc/settings.json.dist + dst: /usr/share/etherpad-lite/settings.json.dist + file_info: + mode: 0644 + + - dst: /etc/etherpad-lite + type: dir + file_info: + mode: 0755 + - dst: /var/lib/etherpad-lite + type: dir + file_info: + mode: 0750 + - dst: /var/log/etherpad-lite + type: dir + file_info: + mode: 0750 + +scripts: + preinstall: ./packaging/scripts/preinstall.sh + postinstall: ./packaging/scripts/postinstall.sh + preremove: ./packaging/scripts/preremove.sh + postremove: ./packaging/scripts/postremove.sh + +overrides: + deb: + depends: + - nodejs (>= 20) + - adduser + - ca-certificates + rpm: + depends: + - nodejs >= 20 + - shadow-utils + - ca-certificates diff --git a/packaging/scripts/postinstall.sh b/packaging/scripts/postinstall.sh new file mode 100755 index 000000000..9ae2d122b --- /dev/null +++ b/packaging/scripts/postinstall.sh @@ -0,0 +1,68 @@ +#!/bin/sh +# postinstall - runs after files have been unpacked. +# Debian actions: configure | abort-upgrade | abort-remove | abort-deconfigure +set -e + +ETC_DIR=/etc/etherpad-lite +VAR_DIR=/var/lib/etherpad-lite +LOG_DIR=/var/log/etherpad-lite +APP_DIR=/opt/etherpad-lite +DIST_SETTINGS=/usr/share/etherpad-lite/settings.json.dist +ACTIVE_SETTINGS="${ETC_DIR}/settings.json" + +case "$1" in + configure) + mkdir -p "${ETC_DIR}" "${VAR_DIR}" "${LOG_DIR}" + chown root:etherpad "${ETC_DIR}" + chmod 0750 "${ETC_DIR}" + chown etherpad:etherpad "${VAR_DIR}" "${LOG_DIR}" + chmod 0750 "${VAR_DIR}" "${LOG_DIR}" + + if [ ! -e "${ACTIVE_SETTINGS}" ]; then + cp "${DIST_SETTINGS}" "${ACTIVE_SETTINGS}" + # Point the default dirty-DB at /var/lib so ProtectSystem=strict works. + sed -i \ + 's|"filename": "var/dirty.db"|"filename": "/var/lib/etherpad-lite/dirty.db"|' \ + "${ACTIVE_SETTINGS}" + chown root:etherpad "${ACTIVE_SETTINGS}" + chmod 0640 "${ACTIVE_SETTINGS}" + fi + + # Etherpad reads settings.json from CWD (/opt/etherpad-lite). Expose + # the /etc copy there via symlink. + ln -sfn "${ACTIVE_SETTINGS}" "${APP_DIR}/settings.json" + + if [ -d "${APP_DIR}/var" ]; then + chown -R etherpad:etherpad "${APP_DIR}/var" || true + fi + + if [ -d /run/systemd/system ] && command -v systemctl >/dev/null 2>&1; then + systemctl daemon-reload || true + # Enable on first install; leave state alone on upgrade. + if [ -z "$2" ]; then + systemctl enable etherpad-lite.service >/dev/null 2>&1 || true + fi + # Restart on upgrade to pick up new code (skip on fresh install -- + # admin may want to configure first). + if [ -n "$2" ]; then + systemctl try-restart etherpad-lite.service >/dev/null 2>&1 || true + fi + fi + + cat <&2 + exit 1 + ;; +esac + +exit 0 diff --git a/packaging/scripts/postremove.sh b/packaging/scripts/postremove.sh new file mode 100755 index 000000000..b9ed40808 --- /dev/null +++ b/packaging/scripts/postremove.sh @@ -0,0 +1,43 @@ +#!/bin/sh +# postremove - runs after files are removed. +# Debian actions: remove | purge | upgrade | failed-upgrade | abort-install | +# abort-upgrade | disappear +set -e + +APP_DIR=/opt/etherpad-lite + +case "$1" in + remove) + [ -L "${APP_DIR}/settings.json" ] && rm -f "${APP_DIR}/settings.json" || true + if [ -d /run/systemd/system ] && command -v systemctl >/dev/null 2>&1; then + systemctl daemon-reload || true + fi + ;; + + purge) + rm -rf /etc/etherpad-lite + rm -rf /var/lib/etherpad-lite + rm -rf /var/log/etherpad-lite + + if getent passwd etherpad >/dev/null 2>&1; then + deluser --system etherpad >/dev/null 2>&1 || true + fi + if getent group etherpad >/dev/null 2>&1; then + delgroup --system etherpad >/dev/null 2>&1 || true + fi + + if [ -d /run/systemd/system ] && command -v systemctl >/dev/null 2>&1; then + systemctl daemon-reload || true + fi + ;; + + upgrade|failed-upgrade|abort-install|abort-upgrade|disappear) + ;; + + *) + echo "postremove called with unknown argument: $1" >&2 + exit 1 + ;; +esac + +exit 0 diff --git a/packaging/scripts/preinstall.sh b/packaging/scripts/preinstall.sh new file mode 100755 index 000000000..d85c69054 --- /dev/null +++ b/packaging/scripts/preinstall.sh @@ -0,0 +1,28 @@ +#!/bin/sh +# preinstall - runs before files are unpacked. +# Debian actions: install | upgrade | abort-upgrade +set -e + +case "$1" in + install|upgrade) + if ! getent group etherpad >/dev/null 2>&1; then + addgroup --system etherpad + fi + if ! getent passwd etherpad >/dev/null 2>&1; then + adduser --system --ingroup etherpad \ + --home /var/lib/etherpad-lite \ + --no-create-home \ + --shell /usr/sbin/nologin \ + --gecos "Etherpad service user" \ + etherpad + fi + ;; + abort-upgrade) + ;; + *) + echo "preinstall called with unknown argument: $1" >&2 + exit 1 + ;; +esac + +exit 0 diff --git a/packaging/scripts/preremove.sh b/packaging/scripts/preremove.sh new file mode 100755 index 000000000..851fbb8b8 --- /dev/null +++ b/packaging/scripts/preremove.sh @@ -0,0 +1,20 @@ +#!/bin/sh +# preremove - runs before files are removed. +# Debian actions: remove | upgrade | deconfigure | failed-upgrade +set -e + +case "$1" in + remove|upgrade|deconfigure) + if [ -d /run/systemd/system ] && command -v systemctl >/dev/null 2>&1; then + systemctl stop etherpad-lite.service >/dev/null 2>&1 || true + fi + ;; + failed-upgrade) + ;; + *) + echo "preremove called with unknown argument: $1" >&2 + exit 1 + ;; +esac + +exit 0 diff --git a/packaging/systemd/etherpad-lite.default b/packaging/systemd/etherpad-lite.default new file mode 100644 index 000000000..3ad92b0cd --- /dev/null +++ b/packaging/systemd/etherpad-lite.default @@ -0,0 +1,7 @@ +# /etc/default/etherpad-lite +# Environment overrides for the etherpad-lite systemd service. +# Any variable referenced by ${VAR:default} in settings.json can be set here. + +NODE_ENV=production +# PORT=9001 +# NODE_OPTIONS=--max-old-space-size=2048 diff --git a/packaging/systemd/etherpad-lite.service b/packaging/systemd/etherpad-lite.service new file mode 100644 index 000000000..f13d7d6c6 --- /dev/null +++ b/packaging/systemd/etherpad-lite.service @@ -0,0 +1,48 @@ +[Unit] +Description=Etherpad - real-time collaborative editor +Documentation=https://etherpad.org https://github.com/ether/etherpad-lite +After=network-online.target +Wants=network-online.target + +[Service] +Type=simple +User=etherpad +Group=etherpad +WorkingDirectory=/opt/etherpad-lite +EnvironmentFile=-/etc/default/etherpad-lite +ExecStart=/usr/bin/etherpad-lite +Restart=on-failure +RestartSec=5s +TimeoutStopSec=20s + +StandardOutput=journal +StandardError=journal +SyslogIdentifier=etherpad-lite + +# --- Sandboxing --------------------------------------------------------- +NoNewPrivileges=true +ProtectSystem=strict +ProtectHome=true +PrivateTmp=true +PrivateDevices=true +ProtectKernelTunables=true +ProtectKernelModules=true +ProtectKernelLogs=true +ProtectControlGroups=true +ProtectHostname=true +ProtectClock=true +RestrictRealtime=true +RestrictSUIDSGID=true +RestrictNamespaces=true +LockPersonality=true +MemoryDenyWriteExecute=false # Node's JIT needs W+X mappings +SystemCallArchitectures=native +RestrictAddressFamilies=AF_UNIX AF_INET AF_INET6 AF_NETLINK +UMask=0027 + +ReadWritePaths=/var/lib/etherpad-lite /var/log/etherpad-lite /etc/etherpad-lite + +LimitNOFILE=65536 + +[Install] +WantedBy=multi-user.target From de5feb2eb5422f713557382106c555a278fdef20 Mon Sep 17 00:00:00 2001 From: John McLear Date: Wed, 22 Apr 2026 18:47:13 +0100 Subject: [PATCH 006/388] =?UTF-8?q?Revert=20"feat(packaging):=20add=20Debi?= =?UTF-8?q?an=20(.deb)=20build=20via=20nfpm=20with=20systemd=20unit=20(?= =?UTF-8?q?=E2=80=A6"=20(#7582)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This reverts commit 6bb879ed03933d60e365ab3ed00fd6d966d84ccc. --- .github/workflows/deb-package.yml | 161 ------------------------ packaging/README.md | 89 ------------- packaging/bin/etherpad-lite | 18 --- packaging/nfpm.yaml | 117 ----------------- packaging/scripts/postinstall.sh | 68 ---------- packaging/scripts/postremove.sh | 43 ------- packaging/scripts/preinstall.sh | 28 ----- packaging/scripts/preremove.sh | 20 --- packaging/systemd/etherpad-lite.default | 7 -- packaging/systemd/etherpad-lite.service | 48 ------- 10 files changed, 599 deletions(-) delete mode 100644 .github/workflows/deb-package.yml delete mode 100644 packaging/README.md delete mode 100755 packaging/bin/etherpad-lite delete mode 100644 packaging/nfpm.yaml delete mode 100755 packaging/scripts/postinstall.sh delete mode 100755 packaging/scripts/postremove.sh delete mode 100755 packaging/scripts/preinstall.sh delete mode 100755 packaging/scripts/preremove.sh delete mode 100644 packaging/systemd/etherpad-lite.default delete mode 100644 packaging/systemd/etherpad-lite.service diff --git a/.github/workflows/deb-package.yml b/.github/workflows/deb-package.yml deleted file mode 100644 index 81f64f61e..000000000 --- a/.github/workflows/deb-package.yml +++ /dev/null @@ -1,161 +0,0 @@ -name: Debian package -on: - push: - tags: - - 'v[0-9]+.[0-9]+.[0-9]+' - - 'v[0-9]+.[0-9]+.[0-9]+-*' - workflow_dispatch: - inputs: - ref: - description: 'Git ref to package (defaults to current)' - required: false - -permissions: - contents: write # attach release assets - -env: - NFPM_VERSION: v2.43.0 - -jobs: - build: - name: Build .deb (${{ matrix.arch }}) - runs-on: ${{ matrix.runner }} - strategy: - fail-fast: false - matrix: - include: - - arch: amd64 - runner: ubuntu-latest - - arch: arm64 - runner: ubuntu-24.04-arm - steps: - - name: Checkout - uses: actions/checkout@v6 - with: - ref: ${{ inputs.ref || github.ref }} - - - name: Resolve version - id: v - run: | - if [ "${GITHUB_REF_TYPE}" = "tag" ]; then - VERSION="${GITHUB_REF_NAME#v}" - else - VERSION="$(node -p "require('./package.json').version")" - fi - echo "version=${VERSION}" >>"$GITHUB_OUTPUT" - echo "Packaging version: ${VERSION}" - - - uses: pnpm/action-setup@v6 - with: - version: 10 - - - name: Setup Node - uses: actions/setup-node@v6 - with: - node-version: '22' - cache: pnpm - - - name: Install dependencies - run: pnpm install --frozen-lockfile - - - name: Build UI + admin - run: pnpm run build:etherpad - - - name: Install nfpm - run: | - set -e - NFPM_ARCH=amd64 - [ "${{ matrix.arch }}" = "arm64" ] && NFPM_ARCH=arm64 - curl -fsSL -o /tmp/nfpm.deb \ - "https://github.com/goreleaser/nfpm/releases/download/${NFPM_VERSION}/nfpm_${NFPM_VERSION#v}_${NFPM_ARCH}.deb" - sudo dpkg -i /tmp/nfpm.deb - - - name: Stage tree for packaging - run: | - set -eux - STAGE=staging/opt/etherpad-lite - mkdir -p "${STAGE}" - - # Production footprint = src/ + bin/ + node_modules/ + metadata. - cp -a src bin package.json pnpm-workspace.yaml README.md LICENSE \ - node_modules "${STAGE}/" - - # Make pnpm-workspace.yaml production-only (same trick Dockerfile uses). - printf 'packages:\n - src\n - bin\n' > "${STAGE}/pnpm-workspace.yaml" - - mkdir -p packaging/etc - cp settings.json.template packaging/etc/settings.json.dist - - # Purge test fixtures and dev caches from node_modules to shrink size. - find "${STAGE}/node_modules" -type d \ - \( -name test -o -name tests -o -name '__tests__' \ - -o -name example -o -name examples -o -name docs \) \ - -prune -exec rm -rf {} + 2>/dev/null || true - find "${STAGE}/node_modules" -type f \ - \( -name '*.md' -o -name '*.ts.map' -o -name '*.map' \ - -o -name 'CHANGELOG*' -o -name 'HISTORY*' \) \ - -delete 2>/dev/null || true - - - name: Build .deb - env: - VERSION: ${{ steps.v.outputs.version }} - ARCH: ${{ matrix.arch }} - run: | - mkdir -p dist - nfpm package --packager deb -f packaging/nfpm.yaml --target dist/ - - - name: Smoke-test the package (amd64 only) - if: matrix.arch == 'amd64' - run: | - set -eux - sudo apt-get update - sudo apt-get install -y nodejs - sudo dpkg -i dist/*.deb || sudo apt-get install -f -y - test -x /usr/bin/etherpad-lite - test -f /etc/etherpad-lite/settings.json - test -L /opt/etherpad-lite/settings.json - id etherpad - systemctl cat etherpad-lite.service - sudo systemctl start etherpad-lite - ok= - for i in $(seq 1 30); do - if curl -fsS http://127.0.0.1:9001/health; then - ok=1 - break - fi - sleep 2 - done - if [ -z "${ok}" ]; then - # Attach logs so the failing run is diagnosable. - sudo journalctl -u etherpad-lite --no-pager -n 200 || true - exit 1 - fi - sudo systemctl stop etherpad-lite - sudo dpkg --purge etherpad-lite - ! id etherpad 2>/dev/null - - - name: Upload artifact - uses: actions/upload-artifact@v4 - with: - name: etherpad-lite-${{ steps.v.outputs.version }}-${{ matrix.arch }}-deb - path: dist/*.deb - if-no-files-found: error - - release: - name: Attach to GitHub Release - needs: build - if: startsWith(github.ref, 'refs/tags/v') - runs-on: ubuntu-latest - permissions: - contents: write - steps: - - uses: actions/download-artifact@v4 - with: - path: dist - pattern: etherpad-lite-*-deb - merge-multiple: true - - name: Attach .deb files to release - uses: softprops/action-gh-release@v2 - with: - files: dist/*.deb - fail_on_unmatched_files: true diff --git a/packaging/README.md b/packaging/README.md deleted file mode 100644 index 20b5b5f7b..000000000 --- a/packaging/README.md +++ /dev/null @@ -1,89 +0,0 @@ -# Etherpad Debian / RPM packaging - -Produces native `.deb` (and, with the same manifest, `.rpm` / `.apk`) -packages for Etherpad using [nfpm](https://nfpm.goreleaser.com). - -## Layout - -``` -packaging/ - nfpm.yaml # nfpm package manifest - bin/etherpad-lite # /usr/bin launcher - scripts/ # preinst / postinst / prerm / postrm - systemd/etherpad-lite.service - systemd/etherpad-lite.default - etc/settings.json.dist # populated in CI from settings.json.template -``` - -Built artefacts land in `./dist/`. - -## Building locally - -Prereqs: Node 22, pnpm 10+, nfpm. - -```sh -pnpm install --frozen-lockfile -pnpm run build:etherpad - -# Stage the tree the way CI does: -STAGE=staging/opt/etherpad-lite -mkdir -p "$STAGE" -cp -a src bin package.json pnpm-workspace.yaml README.md LICENSE \ - node_modules "$STAGE/" -printf 'packages:\n - src\n - bin\n' > "$STAGE/pnpm-workspace.yaml" -cp settings.json.template packaging/etc/settings.json.dist - -VERSION=$(node -p "require('./package.json').version") \ -ARCH=amd64 \ - nfpm package --packager deb -f packaging/nfpm.yaml --target dist/ -``` - -## Installing - -```sh -sudo apt install ./dist/etherpad-lite_2.6.1_amd64.deb -sudo systemctl start etherpad-lite -curl http://localhost:9001/health -``` - -`apt` will pull in `nodejs (>= 20)`; on Ubuntu 22.04 add NodeSource first: - -```sh -curl -fsSL https://deb.nodesource.com/setup_22.x | sudo -E bash - -``` - -## Configuration - -- Edit `/etc/etherpad-lite/settings.json`, then - `sudo systemctl restart etherpad-lite`. -- Environment overrides: `/etc/default/etherpad-lite`. -- Logs: `journalctl -u etherpad-lite -f`. -- Data (dirty-DB default): `/var/lib/etherpad-lite/`. - -## Upgrading - -`dpkg --install etherpad-lite_.deb` (or `apt install`) replaces the -app tree under `/opt/etherpad-lite` while preserving -`/etc/etherpad-lite/*` and `/var/lib/etherpad-lite/*`. The service is -restarted automatically. - -## Removing - -- `sudo apt remove etherpad-lite` — keeps config and data. -- `sudo apt purge etherpad-lite` — also removes config, data, and the - `etherpad` system user. - -## Publishing to an APT repository (follow-up) - -Out of scope here — requires credentials and ownership decisions. -Recipes once a repo is picked: - -- **Cloudsmith** (easiest, free OSS tier): - `cloudsmith push deb ether/etherpad-lite/any-distro/any-version dist/*.deb` -- **Launchpad PPA**: requires signed source packages (a `debian/` tree), - which nfpm does not produce — use `debuild` separately. -- **Self-hosted reprepro**: - `reprepro -b /srv/apt includedeb stable dist/*.deb` - -Wire the chosen option into `.github/workflows/deb-package.yml` after -the `release` job. diff --git a/packaging/bin/etherpad-lite b/packaging/bin/etherpad-lite deleted file mode 100755 index 20143154e..000000000 --- a/packaging/bin/etherpad-lite +++ /dev/null @@ -1,18 +0,0 @@ -#!/bin/sh -# /usr/bin/etherpad-lite - thin wrapper that runs Etherpad in production mode. -# Invoked by the etherpad-lite.service systemd unit. -set -e - -APP_DIR="${ETHERPAD_DIR:-/opt/etherpad-lite}" -cd "${APP_DIR}" - -: "${NODE_ENV:=production}" -export NODE_ENV -export ETHERPAD_PRODUCTION=true - -# Run the server through tsx's ESM loader (shipped in node_modules). -# No pnpm needed at runtime. -exec node \ - --import "file://${APP_DIR}/src/node_modules/tsx/dist/esm/index.mjs" \ - "${APP_DIR}/src/node/server.ts" \ - "$@" diff --git a/packaging/nfpm.yaml b/packaging/nfpm.yaml deleted file mode 100644 index eaf8ffce8..000000000 --- a/packaging/nfpm.yaml +++ /dev/null @@ -1,117 +0,0 @@ -# nfpm configuration for etherpad-lite Debian/RPM/APK packages. -# Build with: nfpm package --packager deb --target dist/ -# See: https://nfpm.goreleaser.com/configuration/ - -name: etherpad-lite -arch: ${ARCH} # amd64 | arm64 (exported by CI) -platform: linux -version: ${VERSION} # e.g. 2.6.1, stripped of leading "v" -version_schema: semver -release: "1" -section: web -priority: optional -maintainer: "Etherpad Foundation " -description: | - Etherpad is a real-time collaborative editor for the web. - This package installs Etherpad as a systemd service running - from /opt/etherpad-lite with configuration in /etc/etherpad-lite. -vendor: "Etherpad Foundation" -homepage: https://etherpad.org -license: Apache-2.0 - -depends: - - nodejs (>= 20) - - adduser - - ca-certificates - -recommends: - - libreoffice # enables DOC/DOCX/PDF/ODT export - - curl - -suggests: - - postgresql-client - - mariadb-client - -conflicts: - - etherpad # legacy bin/buildDebian.sh package name - -replaces: - - etherpad - -provides: - - etherpad - -# --------------------------------------------------------------------------- -# Contents. staging/ is populated by CI before invoking nfpm: -# staging/opt/etherpad-lite/ -- source + node_modules + built assets -# --------------------------------------------------------------------------- -contents: - - src: ./staging/opt/etherpad-lite - dst: /opt/etherpad-lite - type: tree - file_info: - mode: 0755 - owner: root - group: root - - - src: ./packaging/systemd/etherpad-lite.service - dst: /lib/systemd/system/etherpad-lite.service - file_info: - mode: 0644 - - # Default environment file (conffile: preserved on upgrade). - # Mode 0640 + group=etherpad so passwords/secrets admins drop in here - # are only readable by root and the etherpad service user — /etc/default - # is world-readable by default (0644), which would leak DB creds etc. - - src: ./packaging/systemd/etherpad-lite.default - dst: /etc/default/etherpad-lite - type: config|noreplace - file_info: - mode: 0640 - owner: root - group: etherpad - - - src: ./packaging/bin/etherpad-lite - dst: /usr/bin/etherpad-lite - file_info: - mode: 0755 - - # Template used by postinstall to seed /etc/etherpad-lite/settings.json. - # Intentionally NOT a conffile: postinstall creates the real settings.json - # once on first install and never touches it again, so upgrades don't - # prompt with dpkg merge dialogs. - - src: ./packaging/etc/settings.json.dist - dst: /usr/share/etherpad-lite/settings.json.dist - file_info: - mode: 0644 - - - dst: /etc/etherpad-lite - type: dir - file_info: - mode: 0755 - - dst: /var/lib/etherpad-lite - type: dir - file_info: - mode: 0750 - - dst: /var/log/etherpad-lite - type: dir - file_info: - mode: 0750 - -scripts: - preinstall: ./packaging/scripts/preinstall.sh - postinstall: ./packaging/scripts/postinstall.sh - preremove: ./packaging/scripts/preremove.sh - postremove: ./packaging/scripts/postremove.sh - -overrides: - deb: - depends: - - nodejs (>= 20) - - adduser - - ca-certificates - rpm: - depends: - - nodejs >= 20 - - shadow-utils - - ca-certificates diff --git a/packaging/scripts/postinstall.sh b/packaging/scripts/postinstall.sh deleted file mode 100755 index 9ae2d122b..000000000 --- a/packaging/scripts/postinstall.sh +++ /dev/null @@ -1,68 +0,0 @@ -#!/bin/sh -# postinstall - runs after files have been unpacked. -# Debian actions: configure | abort-upgrade | abort-remove | abort-deconfigure -set -e - -ETC_DIR=/etc/etherpad-lite -VAR_DIR=/var/lib/etherpad-lite -LOG_DIR=/var/log/etherpad-lite -APP_DIR=/opt/etherpad-lite -DIST_SETTINGS=/usr/share/etherpad-lite/settings.json.dist -ACTIVE_SETTINGS="${ETC_DIR}/settings.json" - -case "$1" in - configure) - mkdir -p "${ETC_DIR}" "${VAR_DIR}" "${LOG_DIR}" - chown root:etherpad "${ETC_DIR}" - chmod 0750 "${ETC_DIR}" - chown etherpad:etherpad "${VAR_DIR}" "${LOG_DIR}" - chmod 0750 "${VAR_DIR}" "${LOG_DIR}" - - if [ ! -e "${ACTIVE_SETTINGS}" ]; then - cp "${DIST_SETTINGS}" "${ACTIVE_SETTINGS}" - # Point the default dirty-DB at /var/lib so ProtectSystem=strict works. - sed -i \ - 's|"filename": "var/dirty.db"|"filename": "/var/lib/etherpad-lite/dirty.db"|' \ - "${ACTIVE_SETTINGS}" - chown root:etherpad "${ACTIVE_SETTINGS}" - chmod 0640 "${ACTIVE_SETTINGS}" - fi - - # Etherpad reads settings.json from CWD (/opt/etherpad-lite). Expose - # the /etc copy there via symlink. - ln -sfn "${ACTIVE_SETTINGS}" "${APP_DIR}/settings.json" - - if [ -d "${APP_DIR}/var" ]; then - chown -R etherpad:etherpad "${APP_DIR}/var" || true - fi - - if [ -d /run/systemd/system ] && command -v systemctl >/dev/null 2>&1; then - systemctl daemon-reload || true - # Enable on first install; leave state alone on upgrade. - if [ -z "$2" ]; then - systemctl enable etherpad-lite.service >/dev/null 2>&1 || true - fi - # Restart on upgrade to pick up new code (skip on fresh install -- - # admin may want to configure first). - if [ -n "$2" ]; then - systemctl try-restart etherpad-lite.service >/dev/null 2>&1 || true - fi - fi - - cat <&2 - exit 1 - ;; -esac - -exit 0 diff --git a/packaging/scripts/postremove.sh b/packaging/scripts/postremove.sh deleted file mode 100755 index b9ed40808..000000000 --- a/packaging/scripts/postremove.sh +++ /dev/null @@ -1,43 +0,0 @@ -#!/bin/sh -# postremove - runs after files are removed. -# Debian actions: remove | purge | upgrade | failed-upgrade | abort-install | -# abort-upgrade | disappear -set -e - -APP_DIR=/opt/etherpad-lite - -case "$1" in - remove) - [ -L "${APP_DIR}/settings.json" ] && rm -f "${APP_DIR}/settings.json" || true - if [ -d /run/systemd/system ] && command -v systemctl >/dev/null 2>&1; then - systemctl daemon-reload || true - fi - ;; - - purge) - rm -rf /etc/etherpad-lite - rm -rf /var/lib/etherpad-lite - rm -rf /var/log/etherpad-lite - - if getent passwd etherpad >/dev/null 2>&1; then - deluser --system etherpad >/dev/null 2>&1 || true - fi - if getent group etherpad >/dev/null 2>&1; then - delgroup --system etherpad >/dev/null 2>&1 || true - fi - - if [ -d /run/systemd/system ] && command -v systemctl >/dev/null 2>&1; then - systemctl daemon-reload || true - fi - ;; - - upgrade|failed-upgrade|abort-install|abort-upgrade|disappear) - ;; - - *) - echo "postremove called with unknown argument: $1" >&2 - exit 1 - ;; -esac - -exit 0 diff --git a/packaging/scripts/preinstall.sh b/packaging/scripts/preinstall.sh deleted file mode 100755 index d85c69054..000000000 --- a/packaging/scripts/preinstall.sh +++ /dev/null @@ -1,28 +0,0 @@ -#!/bin/sh -# preinstall - runs before files are unpacked. -# Debian actions: install | upgrade | abort-upgrade -set -e - -case "$1" in - install|upgrade) - if ! getent group etherpad >/dev/null 2>&1; then - addgroup --system etherpad - fi - if ! getent passwd etherpad >/dev/null 2>&1; then - adduser --system --ingroup etherpad \ - --home /var/lib/etherpad-lite \ - --no-create-home \ - --shell /usr/sbin/nologin \ - --gecos "Etherpad service user" \ - etherpad - fi - ;; - abort-upgrade) - ;; - *) - echo "preinstall called with unknown argument: $1" >&2 - exit 1 - ;; -esac - -exit 0 diff --git a/packaging/scripts/preremove.sh b/packaging/scripts/preremove.sh deleted file mode 100755 index 851fbb8b8..000000000 --- a/packaging/scripts/preremove.sh +++ /dev/null @@ -1,20 +0,0 @@ -#!/bin/sh -# preremove - runs before files are removed. -# Debian actions: remove | upgrade | deconfigure | failed-upgrade -set -e - -case "$1" in - remove|upgrade|deconfigure) - if [ -d /run/systemd/system ] && command -v systemctl >/dev/null 2>&1; then - systemctl stop etherpad-lite.service >/dev/null 2>&1 || true - fi - ;; - failed-upgrade) - ;; - *) - echo "preremove called with unknown argument: $1" >&2 - exit 1 - ;; -esac - -exit 0 diff --git a/packaging/systemd/etherpad-lite.default b/packaging/systemd/etherpad-lite.default deleted file mode 100644 index 3ad92b0cd..000000000 --- a/packaging/systemd/etherpad-lite.default +++ /dev/null @@ -1,7 +0,0 @@ -# /etc/default/etherpad-lite -# Environment overrides for the etherpad-lite systemd service. -# Any variable referenced by ${VAR:default} in settings.json can be set here. - -NODE_ENV=production -# PORT=9001 -# NODE_OPTIONS=--max-old-space-size=2048 diff --git a/packaging/systemd/etherpad-lite.service b/packaging/systemd/etherpad-lite.service deleted file mode 100644 index f13d7d6c6..000000000 --- a/packaging/systemd/etherpad-lite.service +++ /dev/null @@ -1,48 +0,0 @@ -[Unit] -Description=Etherpad - real-time collaborative editor -Documentation=https://etherpad.org https://github.com/ether/etherpad-lite -After=network-online.target -Wants=network-online.target - -[Service] -Type=simple -User=etherpad -Group=etherpad -WorkingDirectory=/opt/etherpad-lite -EnvironmentFile=-/etc/default/etherpad-lite -ExecStart=/usr/bin/etherpad-lite -Restart=on-failure -RestartSec=5s -TimeoutStopSec=20s - -StandardOutput=journal -StandardError=journal -SyslogIdentifier=etherpad-lite - -# --- Sandboxing --------------------------------------------------------- -NoNewPrivileges=true -ProtectSystem=strict -ProtectHome=true -PrivateTmp=true -PrivateDevices=true -ProtectKernelTunables=true -ProtectKernelModules=true -ProtectKernelLogs=true -ProtectControlGroups=true -ProtectHostname=true -ProtectClock=true -RestrictRealtime=true -RestrictSUIDSGID=true -RestrictNamespaces=true -LockPersonality=true -MemoryDenyWriteExecute=false # Node's JIT needs W+X mappings -SystemCallArchitectures=native -RestrictAddressFamilies=AF_UNIX AF_INET AF_INET6 AF_NETLINK -UMask=0027 - -ReadWritePaths=/var/lib/etherpad-lite /var/log/etherpad-lite /etc/etherpad-lite - -LimitNOFILE=65536 - -[Install] -WantedBy=multi-user.target From 67e542d2b983671badbfa9304564cfda2eef018d Mon Sep 17 00:00:00 2001 From: John McLear Date: Thu, 23 Apr 2026 08:57:30 +0100 Subject: [PATCH 007/388] fix(editor): preserve U+00A0 non-breaking space (#3037) (#7585) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix(editor): preserve U+00A0 non-breaking space (#3037) Non-breaking spaces were silently normalized to regular spaces at every ingestion point, so typed/pasted/imported nbsps never reached the changeset and users could not glue words against line-wrap in French or other languages that require nbsp typography. Removed the four strip sites that replaced U+00A0 with U+0020: - src/node/db/Pad.ts cleanText - src/static/js/contentcollector.ts textify - src/static/js/ace2_inner.ts textify - src/static/js/ace2_inner.ts importText raw-text guard Updated both processSpaces functions (domline and ExportHtml) to tokenize U+00A0 as a separate unit, emit it verbatim as  , and treat it as content (not whitespace) for the run-collapse bookkeeping so adjacent regular-space runs aren't miscounted. Added backend round-trip tests for spliceText and setText, and extended the cleanText case table. Updated the existing contentcollector and importexport specs whose expectations encoded the previous buggy behavior; they now assert genuine nbsp preservation. Verified manually in Firefox: clipboard U+00A0 → paste → pad → getText returns c2 a0; getHTML emits `100 km`. Co-Authored-By: Claude Opus 4.7 (1M context) * fix(contentcollector): collapse display-artifact nbsp runs on DOM read-back processSpaces is a lossy one-way display transform: leading/trailing spaces and all-but-the-last of a run get rendered as   so HTML doesn't collapse them. When incorporateUserChanges reads text back from the DOM, those display-artifact nbsps were being stored in the changeset model instead of being normalized back to plain spaces. This broke handleReturnIndentation, whose /^ *(?:)/ regex only matches ASCII spaces: auto-indent after `foo:\n` produced 4 spaces instead of the expected prev-indent (2) + THE_TAB (4) = 6, because the previous line's model had nbsps where it used to have spaces. Fix: in contentcollector.textify, collapse any [  ]+ run back to plain spaces UNLESS the run is pure U+00A0 AND strictly interior to word chars. That preserves user-intended typographic nbsps like "100 km" while undoing the one-way display transform. Updated 7 contentcollector tests and 7 importexport tests whose assertions needed to reflect the new rule (boundary/mixed runs collapse; pure-interior nbsp runs preserve). Fixes the Playwright regression in indentation.spec.ts:117 that the previous commit introduced. * fix(contentcollector): canonicalize nbsp runs at line assembly, not per text node Addresses Qodo code review feedback on PR #7585. ## Bug fix — nbsp lost at DOM text-node boundary The previous approach ran the "collapse display-artifact nbsp" rule inside textify(), which is called per individual DOM TEXT_NODE. A user-intended nbsp sitting at a text-node boundary (e.g., 100 km ) was incorrectly seen as non-interior (before === '' for the second text node) and normalized back to a regular space. Fix: move the canonicalization out of textify() and run it on each fully assembled line string inside cc.finish(). The rule remains: [ ]+ run -> plain spaces UNLESS pure U+00A0 AND strictly interior to non-ws chars It is length-preserving, so attribute offsets and line lengths are unaffected. Added a regression test (contentcollector.spec.ts) for the cross-span case. ## Docs concern Reverted the type-only addition of spliceText to PadType. spliceText is an existing Pad runtime method; the backend test now uses a cast (`(pad as any).spliceText`) so the PR does not expand the declared public type surface, avoiding a separate documentation requirement. --------- Co-authored-by: Claude Opus 4.7 (1M context) --- src/node/db/Pad.ts | 3 +-- src/node/utils/ExportHtml.ts | 12 ++++++--- src/static/js/ace2_inner.ts | 6 ++--- src/static/js/contentcollector.ts | 30 ++++++++++++++++++++- src/static/js/domline.ts | 10 +++++-- src/tests/backend/specs/Pad.ts | 19 +++++++++++++ src/tests/backend/specs/api/importexport.ts | 8 +++--- src/tests/backend/specs/contentcollector.ts | 14 ++++++++-- 8 files changed, 85 insertions(+), 17 deletions(-) diff --git a/src/node/db/Pad.ts b/src/node/db/Pad.ts index 7f4006233..30b18e6b5 100644 --- a/src/node/db/Pad.ts +++ b/src/node/db/Pad.ts @@ -51,8 +51,7 @@ type PadSettings = { */ exports.cleanText = (txt:string): string => txt.replace(/\r\n/g, '\n') .replace(/\r/g, '\n') - .replace(/\t/g, ' ') - .replace(/\xa0/g, ' '); + .replace(/\t/g, ' '); class Pad { private db: Database; diff --git a/src/node/utils/ExportHtml.ts b/src/node/utils/ExportHtml.ts index 700b4403f..fd8341654 100644 --- a/src/node/utils/ExportHtml.ts +++ b/src/node/utils/ExportHtml.ts @@ -537,13 +537,16 @@ const _processSpaces = (s: string) => { const doesWrap = true; if (s.indexOf('<') < 0 && !doesWrap) { // short-cut - return s.replace(/ /g, ' '); + return s.replace(/[ \u00a0]/g, ' '); } - const parts = []; - s.replace(/<[^>]*>?| |[^ <]+/g, (m) => { + const parts: string[] = []; + s.replace(/<[^>]*>?|[ \u00a0]|[^ \u00a0<]+/g, (m) => { parts.push(m); return m }); + // U+00A0 is content for run-bookkeeping - it terminates a space run + // just like a word character would, so runs of regular spaces adjacent + // to a nbsp are not miscounted as one long run (issue #3037). if (doesWrap) { let endOfLine = true; let beforeSpace = false; @@ -578,6 +581,9 @@ const _processSpaces = (s: string) => { } } } + for (let i = 0; i < parts.length; i++) { + if (parts[i] === '\u00a0') parts[i] = ' '; + } return parts.join(''); }; diff --git a/src/static/js/ace2_inner.ts b/src/static/js/ace2_inner.ts index 3ca880a94..63eb6ac53 100644 --- a/src/static/js/ace2_inner.ts +++ b/src/static/js/ace2_inner.ts @@ -476,8 +476,8 @@ function Ace2Inner(editorInfo, cssManagers) { if (text.charAt(text.length - 1) !== '\n') { throw new Error('new raw text must end with newline'); } - if (/[\r\t\xa0]/.exec(text)) { - throw new Error('new raw text must not contain CR, tab, or nbsp'); + if (/[\r\t]/.exec(text)) { + throw new Error('new raw text must not contain CR or tab'); } lines = text.substring(0, text.length - 1).split('\n'); } else { @@ -2042,7 +2042,7 @@ function Ace2Inner(editorInfo, cssManagers) { (nonEmpty) => domline.createDomLine(nonEmpty, doesWrap, browser, document); const textify = - (str) => str.replace(/[\n\r ]/g, ' ').replace(/\xa0/g, ' ').replace(/\t/g, ' '); + (str) => str.replace(/[\n\r ]/g, ' ').replace(/\t/g, ' '); const _blockElems = { div: 1, diff --git a/src/static/js/contentcollector.ts b/src/static/js/contentcollector.ts index a2140d6fd..5538ecd5d 100644 --- a/src/static/js/contentcollector.ts +++ b/src/static/js/contentcollector.ts @@ -79,9 +79,31 @@ const makeContentCollector = (collectStyles, abrowser, apool, className2Author) const textify = (str) => sanitizeUnicode( str.replace(/(\n | \n)/g, ' ') .replace(/[\n\r ]/g, ' ') - .replace(/\xa0/g, ' ') .replace(/\t/g, ' ')); + // processSpaces (domline.ts, ExportHtml.ts) is a lossy one-way display + // transform: leading/trailing spaces and all-but-the-last space in a run + // are rendered as   to defeat HTML whitespace collapsing, so any + // round-trip through the DOM sees nbsps where the model has plain spaces. + // To keep the model canonical, a [ \u00a0]+ run read back from the fully + // assembled line is collapsed to plain spaces unless it is strictly + // interior to non-whitespace chars AND contains only U+00A0 (issue #3037 - + // user-intended nbsp between words such as "100 km"). The rule runs on + // the whole line (not per DOM text node) so that nbsps sitting at a + // span boundary - e.g. 100 km - are still + // seen as interior. The transform is length-preserving so attribution + // offsets stay consistent. + const canonicalizeNbspRuns = (s: string) => s.replace( + /[ \u00a0]+/g, + (run: string, offset: number, src: string) => { + const before = offset > 0 ? src[offset - 1] : ''; + const after = offset + run.length < src.length + ? src[offset + run.length] : ''; + const pureNbsp = !run.includes(' '); + const interiorOfWord = /\S/.test(before) && /\S/.test(after); + return pureNbsp && interiorOfWord ? run : ' '.repeat(run.length); + }); + const getAssoc = (node, name) => node[`_magicdom_${name}`]; const lines = (() => { @@ -642,6 +664,12 @@ const makeContentCollector = (collectStyles, abrowser, apool, className2Author) lineStrings.length--; lineAttribs.length--; + // Canonicalize display-artifact nbsps on the whole-line string (issue + // #3037). Length-preserving, so no attribute adjustments are required. + for (let i = 0; i < lineStrings.length; i++) { + lineStrings[i] = canonicalizeNbspRuns(lineStrings[i]); + } + const ss = getSelectionStart(); const se = getSelectionEnd(); diff --git a/src/static/js/domline.ts b/src/static/js/domline.ts index 0cdc465da..bb78c3aeb 100644 --- a/src/static/js/domline.ts +++ b/src/static/js/domline.ts @@ -231,12 +231,15 @@ domline.createDomLine = (nonEmpty, doesWrap, optBrowser, optDocument) => { domline.processSpaces = (s, doesWrap) => { if (s.indexOf('<') < 0 && !doesWrap) { // short-cut - return s.replace(/ /g, ' '); + return s.replace(/[ \u00a0]/g, ' '); } const parts = []; - s.replace(/<[^>]*>?| |[^ <]+/g, (m) => { + s.replace(/<[^>]*>?|[ \u00a0]|[^ \u00a0<]+/g, (m) => { parts.push(m); }); + // U+00A0 is content for run-bookkeeping - it terminates a space run + // just like a word character would, so runs of regular spaces adjacent + // to a nbsp are not miscounted as one long run (issue #3037). if (doesWrap) { let endOfLine = true; let beforeSpace = false; @@ -271,6 +274,9 @@ domline.processSpaces = (s, doesWrap) => { } } } + for (let i = 0; i < parts.length; i++) { + if (parts[i] === '\u00a0') parts[i] = ' '; + } return parts.join(''); }; diff --git a/src/tests/backend/specs/Pad.ts b/src/tests/backend/specs/Pad.ts index 42bcc33d8..dd70a5127 100644 --- a/src/tests/backend/specs/Pad.ts +++ b/src/tests/backend/specs/Pad.ts @@ -45,6 +45,9 @@ describe(__filename, function () { ['x\ry\n', 'x\ny\n'], ['x\r\ny\n', 'x\ny\n'], ['x\r\r\ny\n', 'x\n\ny\n'], + // Non-breaking space (U+00A0) must survive cleanText (issue #3037). + ['100\u00a0km\n', '100\u00a0km\n'], + ['a\u00a0\u00a0b\n', 'a\u00a0\u00a0b\n'], ]; for (const [input, want] of testCases) { it(`${JSON.stringify(input)} -> ${JSON.stringify(want)}`, async function () { @@ -53,6 +56,22 @@ describe(__filename, function () { } }); + describe('non-breaking space preservation (issue #3037)', function () { + it('spliceText round-trips U+00A0', async function () { + pad = await padManager.getPad(padId, ''); + // spliceText is an existing runtime Pad method; cast avoids + // adding a type-only declaration to PadType in this PR. + await (pad as any).spliceText(0, 0, '100\u00a0km'); + assert.equal(pad!.text(), '100\u00a0km\n'); + }); + + it('setText round-trips U+00A0', async function () { + pad = await padManager.getPad(padId, ''); + await pad!.setText('a\u00a0b\n'); + assert.equal(pad!.text(), 'a\u00a0b\n'); + }); + }); + describe('padDefaultContent hook', function () { it('runs when a pad is created without specific text', async function () { const p = new Promise((resolve) => { diff --git a/src/tests/backend/specs/api/importexport.ts b/src/tests/backend/specs/api/importexport.ts index 8010671ad..25816ff73 100644 --- a/src/tests/backend/specs/api/importexport.ts +++ b/src/tests/backend/specs/api/importexport.ts @@ -79,8 +79,8 @@ const testImports:MapArrayType = { // XXX the HTML between "than" and "one" looks strange description: 'non-breaking space should be preserved, but can be replaced when it', input: 'Text with  more   than  one space.
', - wantHTML: 'Text with  more   than  one space.

', - wantText: 'Text with more than one space.\n\n', + wantHTML: 'Text with  more   than  one space.

', + wantText: 'Text\u00a0with more\u00a0\u00a0\u00a0than one space.\n\n', }, 'multiplenbsp': { description: 'Multiple non-breaking space should be preserved', @@ -91,8 +91,8 @@ const testImports:MapArrayType = { 'multipleNonBreakingSpaceBetweenWords': { description: 'A normal space is always inserted before a word', input: '  word1  word2   word3
', - wantHTML: '  word1  word2   word3

', - wantText: ' word1 word2 word3\n\n', + wantHTML: '  word1  word2   word3

', + wantText: ' word1\u00a0\u00a0word2\u00a0\u00a0\u00a0word3\n\n', }, 'nonBreakingSpacePreceededBySpaceBetweenWords': { description: 'A non-breaking space preceded by a normal space', diff --git a/src/tests/backend/specs/contentcollector.ts b/src/tests/backend/specs/contentcollector.ts index f7b7a3adf..107c67dc8 100644 --- a/src/tests/backend/specs/contentcollector.ts +++ b/src/tests/backend/specs/contentcollector.ts @@ -185,7 +185,7 @@ const testCases = [ description: 'non-breaking and normal space should be preserved', html: 'Text with  more   than  one space.
', wantAlines: ['+10'], - wantText: ['Text with more than one space.'], + wantText: ['Text\u00a0with more\u00a0\u00a0\u00a0than one space.'], }, { description: 'Multiple nbsp should be preserved', @@ -197,7 +197,7 @@ const testCases = [ description: 'Multiple nbsp between words ', html: '  word1  word2   word3
', wantAlines: ['+m'], - wantText: [' word1 word2 word3'], + wantText: [' word1\u00a0\u00a0word2\u00a0\u00a0\u00a0word3'], }, { description: 'A non-breaking space preceded by a normal space', @@ -359,6 +359,16 @@ pre ], wantText: ['before', '*bare item', 'after'], }, + { + // Regression for PR #7585 review feedback: a user-intended nbsp + // sitting at a DOM text-node boundary (split across spans) must + // still be preserved because canonicalization runs on the whole + // line, not per text node. + description: 'nbsp preserved across span boundary', + html: '

100 km

', + wantAlines: ['+6'], + wantText: ['100\u00a0km'], + }, ]; describe(__filename, function () { From 9e352ca4f3539a16bfe4a8f78b8800018a187b78 Mon Sep 17 00:00:00 2001 From: John McLear Date: Thu, 23 Apr 2026 10:18:57 +0100 Subject: [PATCH 008/388] fix(clientVars): stop mutating the shared plugin registry during sanitization (#7587) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit PadMessageHandler built the `pluginsSanitized` payload for clientVars by aliasing `plugins.plugins` and then mutating each entry's `package` field in place: let pluginsSanitized: any = plugins.plugins; Object.keys(plugins.plugins).forEach(function(element) { const p: any = plugins.plugins[element].package; pluginsSanitized[element].package = {name: p.name, version: p.version}; }); Because `pluginsSanitized` is a reference to `plugins.plugins`, the assignment clobbered the server-side plugin registry. After the first pad connection, every plugin's `package` object held only `{name, version}` — `realPath`, `path`, and `location` were gone. Minify.ts resolves `/static/plugins/ep_*/...` URLs via `plugin.package.realPath`. Once the field disappeared, every subsequent static asset request for a bundled plugin 500'd with: TypeError [ERR_INVALID_ARG_TYPE]: The "path" argument must be of type string. Received undefined at Object.join (node:path:1354:7) at _minify (src/node/utils/Minify.ts:181:23) Symptoms on Chromium: plugin CSS/JS assets fail to load (e.g. /static/plugins/ep_font_size/static/css/size.css returns 500), so plugins partially render or don't work at all. Firefox swallows the resulting console errors quietly. Fix: extract the sanitization into a pure helper `sanitizePluginsForWire` that returns a fresh object graph and never touches the input. The helper is covered by a new backend spec that: * verifies the sanitized output has only {name, version} in `package` * asserts the input registry's realPath/path/location survive the call * runs the call repeatedly and confirms non-destructiveness * mutates the returned copy and asserts the input is independent Verified live with the dev server: before the fix, `/static/plugins/ ep_font_size/static/css/size.css` 500'd after visiting any pad; after the fix it returns 200 both before and after pad connections. --- src/node/handler/PadMessageHandler.ts | 34 ++++++++-- .../backend/specs/sanitizePluginsForWire.ts | 65 +++++++++++++++++++ 2 files changed, 94 insertions(+), 5 deletions(-) create mode 100644 src/tests/backend/specs/sanitizePluginsForWire.ts diff --git a/src/node/handler/PadMessageHandler.ts b/src/node/handler/PadMessageHandler.ts index d0f34600c..006831f76 100644 --- a/src/node/handler/PadMessageHandler.ts +++ b/src/node/handler/PadMessageHandler.ts @@ -109,6 +109,34 @@ function getActivePadCountFromSessionInfos() { } exports.getActivePadCountFromSessionInfos = getActivePadCountFromSessionInfos; +/** + * Build a sanitized copy of the plugins registry suitable for sending to the + * client as part of clientVars. The shape is preserved but each plugin's + * `package` field is reduced to `{name, version}` so internal paths (realPath, + * path, location) are not leaked to the browser. + * + * CRITICAL: this function MUST NOT mutate the shared server-side registry. + * Other components — notably `src/node/utils/Minify.ts` — read + * `plugins.plugins[x].package.realPath` on every static asset request to + * resolve `/static/plugins/ep_/...` URLs to disk. Mutating the shared object + * in place would clobber `realPath` and cause every such request to 500 with + * `ERR_INVALID_ARG_TYPE: The "path" argument must be of type string`. + */ +const sanitizePluginsForWire = ( + pluginsRegistry: MapArrayType, +): MapArrayType => { + const out: MapArrayType = {}; + for (const [name, plugin] of Object.entries(pluginsRegistry)) { + const p: any = plugin.package; + out[name] = { + ...plugin, + package: {name: p.name, version: p.version}, + }; + } + return out; +}; +exports.sanitizePluginsForWire = sanitizePluginsForWire; + stats.gauge('totalUsers', () => getTotalActiveUsers()); stats.gauge('activePads', () => { return getActivePadCountFromSessionInfos(); @@ -1068,11 +1096,7 @@ const handleClientReady = async (socket:any, message: ClientReadyMessage) => { throw new Error('corrupt pad'); } - let pluginsSanitized: any = plugins.plugins - Object.keys(plugins.plugins).forEach(function(element) { - const p: any = plugins.plugins[element].package - pluginsSanitized[element].package = {name: p.name, version: p.version}; - }); + const pluginsSanitized = sanitizePluginsForWire(plugins.plugins); // Warning: never ever send sessionInfo.padId to the client. If the client is read only you // would open a security hole 1 swedish mile wide... const canEditPadSettings = settings.enablePadWideSettings && diff --git a/src/tests/backend/specs/sanitizePluginsForWire.ts b/src/tests/backend/specs/sanitizePluginsForWire.ts new file mode 100644 index 000000000..052a35be4 --- /dev/null +++ b/src/tests/backend/specs/sanitizePluginsForWire.ts @@ -0,0 +1,65 @@ +'use strict'; + +import {strict as assert} from 'assert'; +const {sanitizePluginsForWire} = require('../../../node/handler/PadMessageHandler'); + +describe(__filename, function () { + const makeRegistry = () => ({ + ep_example: { + parts: [{name: 'ep_example', plugin: 'ep_example'}], + package: { + name: 'ep_example', + version: '1.2.3', + realPath: '/real/path/to/ep_example', + path: '/node_modules/ep_example', + location: '/real/path/to/ep_example', + }, + }, + ep_other: { + parts: [{name: 'ep_other', plugin: 'ep_other'}], + package: { + name: 'ep_other', + version: '0.1.0', + realPath: '/real/path/to/ep_other', + path: '/node_modules/ep_other', + location: '/real/path/to/ep_other', + }, + }, + }); + + it('returns a sanitized registry with only name and version in package', function () { + const registry = makeRegistry(); + const sanitized = sanitizePluginsForWire(registry); + assert.deepEqual(Object.keys(sanitized).sort(), ['ep_example', 'ep_other']); + assert.deepEqual(sanitized.ep_example.package, {name: 'ep_example', version: '1.2.3'}); + assert.deepEqual(sanitized.ep_other.package, {name: 'ep_other', version: '0.1.0'}); + }); + + it('does not mutate the input registry (issue: realPath clobbered on pad join)', function () { + const registry = makeRegistry(); + sanitizePluginsForWire(registry); + // The original objects MUST still carry realPath and the other internal + // path fields — Minify.ts relies on them for every /static/plugins/... + // asset request. Before the fix, the sanitization mutated these in place + // and caused every such request to 500 after the first pad connection. + assert.equal(registry.ep_example.package.realPath, '/real/path/to/ep_example'); + assert.equal(registry.ep_example.package.path, '/node_modules/ep_example'); + assert.equal(registry.ep_other.package.realPath, '/real/path/to/ep_other'); + assert.equal(registry.ep_other.package.path, '/node_modules/ep_other'); + }); + + it('repeated calls remain non-destructive', function () { + const registry = makeRegistry(); + for (let i = 0; i < 5; i++) sanitizePluginsForWire(registry); + assert.equal(registry.ep_example.package.realPath, '/real/path/to/ep_example'); + assert.equal(registry.ep_other.package.realPath, '/real/path/to/ep_other'); + }); + + it('returned copy and input are independent (mutation of result does not affect input)', function () { + const registry = makeRegistry(); + const sanitized = sanitizePluginsForWire(registry); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (sanitized.ep_example as any).package.name = 'tampered'; + assert.equal(registry.ep_example.package.name, 'ep_example'); + }); +}); From a1bb958b187aa9207409891d41b816a2e21551e6 Mon Sep 17 00:00:00 2001 From: John McLear Date: Thu, 23 Apr 2026 10:38:46 +0100 Subject: [PATCH 009/388] fix(pad): stop hardcoding lang='en', let the client auto-detect locale (#7586) (#7588) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `Pad.normalizePadSettings()` was defaulting `lang` to the literal string 'en' when `rawPadSettings.lang` was not a string. That value flowed into `clientVars.padOptions.lang` and then into `getParams()` in pad.ts, which calls `html10n.localize([serverValue, 'en'])` as a callback for the `lang` setting. The result: every pad forced English on load, overriding the browser's Accept-Language and the existing auto-detect chain in l10n.ts (cookie -> navigator.language -> 'en'). The regression was introduced in #7545 ("Add creator-owned pad settings defaults", commit e0ccdb4d9). 2.6.1 did not have this default, so auto-detect worked there. 2.7.0 broke it. Fix: default `lang` to null. The client's existing flow already handles null correctly — getParams() at pad.ts:172 has `if (serverValue == null) continue;`, so the forced-localize callback simply does not fire, and l10n.ts's browser-language auto-detect runs. Pad-settings dropdown consumer at pad.ts:489 already uses `padOptions.lang || 'en'` so null renders fine there too. `PadSettings.lang` is now typed `string | null` to match. Added three backend regression tests under `normalizePadSettings lang`: * defaults to null when lang is absent (so client auto-detects) * preserves an explicit string lang (creator override still works) * drops non-string lang values to null rather than coercing to 'en' Manual verification: with Firefox set to German, loading a fresh pad now renders the UI in German. Index and timeslider continued to work as before. Setting `?lang=de` or a language cookie continues to override browser detection, as intended. Fixes #7586 --- src/node/db/Pad.ts | 8 ++++++-- src/tests/backend/specs/Pad.ts | 19 +++++++++++++++++++ 2 files changed, 25 insertions(+), 2 deletions(-) diff --git a/src/node/db/Pad.ts b/src/node/db/Pad.ts index 30b18e6b5..54fd0bb64 100644 --- a/src/node/db/Pad.ts +++ b/src/node/db/Pad.ts @@ -39,7 +39,7 @@ type PadSettings = { showChat: boolean; alwaysShowChat: boolean; chatAndUsers: boolean; - lang: string; + lang: string | null; view: PadViewSettings; }; @@ -91,7 +91,11 @@ class Pad { !!rawPadSettings.showChat, alwaysShowChat: !!rawPadSettings.alwaysShowChat, chatAndUsers: !!rawPadSettings.chatAndUsers, - lang: typeof rawPadSettings.lang === 'string' ? rawPadSettings.lang : 'en', + // Default to null (not 'en') so the client's l10n auto-detect chain + // (cookie -> navigator.language -> 'en' fallback) runs. Hardcoding 'en' + // forces English on every pad regardless of the browser's Accept-Language + // and broke #7586 (German system saw English pad UI in v2.7.0). + lang: typeof rawPadSettings.lang === 'string' ? rawPadSettings.lang : null, view: { showAuthorColors: rawView.showAuthorColors == null ? true : !!rawView.showAuthorColors, showLineNumbers: rawView.showLineNumbers == null ? diff --git a/src/tests/backend/specs/Pad.ts b/src/tests/backend/specs/Pad.ts index dd70a5127..13bc79e5a 100644 --- a/src/tests/backend/specs/Pad.ts +++ b/src/tests/backend/specs/Pad.ts @@ -159,4 +159,23 @@ describe(__filename, function () { assert.equal(pad!.text(), `${want}\n`); }); }); + + describe('normalizePadSettings lang (issue #7586)', function () { + it('defaults lang to null when not provided, so client auto-detects locale', function () { + const ps = Pad.Pad.normalizePadSettings({}); + assert.equal(ps.lang, null); + }); + + it('preserves an explicit string lang (creator override)', function () { + const ps = Pad.Pad.normalizePadSettings({lang: 'de'}); + assert.equal(ps.lang, 'de'); + }); + + it('drops non-string lang values to null rather than coercing to "en"', function () { + for (const bogus of [42, true, {}, [], null, undefined]) { + const ps = Pad.Pad.normalizePadSettings({lang: bogus}); + assert.equal(ps.lang, null, `bogus input ${JSON.stringify(bogus)}`); + } + }); + }); }); From cea0cdaea4853cf2e900e62411912d423209c6b4 Mon Sep 17 00:00:00 2001 From: "translatewiki.net" Date: Thu, 23 Apr 2026 14:03:58 +0200 Subject: [PATCH 010/388] Localisation updates from https://translatewiki.net. --- src/locales/ar.json | 17 ++++++++++++++++- src/locales/he.json | 6 ++++++ src/locales/ko.json | 15 ++++++++++++++- src/locales/mk.json | 14 ++++++++++++++ src/locales/ru.json | 11 +++++++++++ src/locales/sr-ec.json | 1 - src/locales/sr-el.json | 1 - 7 files changed, 61 insertions(+), 4 deletions(-) diff --git a/src/locales/ar.json b/src/locales/ar.json index 0b24afd6f..11eb6e1cd 100644 --- a/src/locales/ar.json +++ b/src/locales/ar.json @@ -93,13 +93,19 @@ "pad.loading": "جارٍ التحميل...", "pad.noCookie": "الكوكيز غير متاحة. الرجاء السماح بتحميل الكوكيز على متصفحك!", "pad.permissionDenied": "ليس لديك إذن لدخول هذا الباد", - "pad.settings.padSettings": "إعدادات الباد", + "pad.settings.title": "إعدادات", + "pad.settings.padSettings": "إعدادات لوحة اللمس", + "pad.settings.userSettings": "إعدادات المستخدم", "pad.settings.myView": "رؤيتي", + "pad.settings.disablechat": "تعطيل الدردشة", + "pad.settings.darkMode": "الوضع الداكن", "pad.settings.stickychat": "الدردشة دائما على الشاشة", "pad.settings.chatandusers": "أظهر الدردشة والمستخدمين", "pad.settings.colorcheck": "ألوان التأليف", "pad.settings.linenocheck": "أرقام الأسطر", "pad.settings.rtlcheck": "قراءة المحتويات من اليمين إلى اليسار؟", + "pad.settings.enforceSettings": "فرض الإعدادات على المستخدمين الآخرين", + "pad.settings.enforcedNotice": "هذه الإعدادات مقفلة من قِبل مُصمم هذه اللوحة. تواصل مع مُصمم اللوحة إذا كنت ترغب في تغييرها.", "pad.settings.fontType": "نوع الخط:", "pad.settings.fontType.normal": "عادي", "pad.settings.language": "اللغة:", @@ -117,6 +123,7 @@ "pad.importExport.exportword": "مايكروسوفت وورد", "pad.importExport.exportpdf": "صيغة المستندات المحمولة", "pad.importExport.exportopen": "ODF (نسق المستند المفتوح)", + "pad.importExport.noConverter.innerHTML": "لا يمكنك الاستيراد إلا من تنسيقات النصوص العادية أو تنسيقات HTML. لمزيد من ميزات الاستيراد المتقدمة، يرجى تثبيت LibreOffice .", "pad.modals.connected": "متصل.", "pad.modals.reconnecting": "إعادة الاتصال ببادك..", "pad.modals.forcereconnect": "فرض إعادة الاتصال", @@ -147,6 +154,8 @@ "pad.modals.disconnected": "لم تعد متصلا.", "pad.modals.disconnected.explanation": "تم فقدان الاتصال بالخادم", "pad.modals.disconnected.cause": "قد يكون الخادم غير متوفر. يرجى إعلام مسؤول الخدمة إذا كان هذا لا يزال يحدث.", + "pad.gritter.unacceptedCommit.title": "تعديل غير محفوظ", + "pad.gritter.unacceptedCommit.text": "لم يتم حفظ تعديلك الأخير بعد. أعد الاتصال وحاول مرة أخرى.", "pad.share": "شارك هذه الباد", "pad.share.readonly": "للقراءة فقط", "pad.share.link": "وصلة", @@ -165,6 +174,12 @@ "timeslider.exportCurrent": "تصدير النسخة الحالية ك:", "timeslider.version": "إصدار {{version}}", "timeslider.saved": "محفوظ {{month}} {{day}}, {{year}}", + "timeslider.settings.playbackSpeed": "سرعة التشغيل:", + "timeslider.settings.playbackSpeed.original": "أعمال أصلية", + "timeslider.settings.playbackSpeed.realtime": "في الوقت الحالى", + "timeslider.settings.playbackSpeed.200ms": "200 مللي ثانية", + "timeslider.settings.playbackSpeed.500ms": "500 مللي ثانية", + "timeslider.settings.playbackSpeed.1000ms": "1000 مللي ثانية", "timeslider.playPause": "تشغيل / إيقاف مؤقت لمحتويات الباد", "timeslider.backRevision": "عد إلى مراجعة في هذه الباد", "timeslider.forwardRevision": "انطلق إلى مراجعة في هذه الباد", diff --git a/src/locales/he.json b/src/locales/he.json index 785e68701..73d7bcacb 100644 --- a/src/locales/he.json +++ b/src/locales/he.json @@ -2,6 +2,7 @@ "@metadata": { "authors": [ "Amire80", + "Neriah", "Ofrahod", "Steeve815", "YaronSh", @@ -158,6 +159,11 @@ "timeslider.exportCurrent": "יצוא הגרסה הנוכחית בתור:", "timeslider.version": "גרסה {{version}}", "timeslider.saved": "נשמרה ב־{{day}} ב{{month}} {{year}}", + "timeslider.settings.playbackSpeed": "מהירות נגינה:", + "timeslider.settings.playbackSpeed.realtime": "זמן אמת", + "timeslider.settings.playbackSpeed.200ms": "200 מ\"ש", + "timeslider.settings.playbackSpeed.500ms": "500 מ\"ש", + "timeslider.settings.playbackSpeed.1000ms": "1000 מ\"ש", "timeslider.playPause": "לנגן / לעצור את תוכן הפנקס", "timeslider.backRevision": "לחזור לגרסה של הפנקס הזה", "timeslider.forwardRevision": "ללכת לגרסה חדשה יותר בפנקס הזה", diff --git a/src/locales/ko.json b/src/locales/ko.json index dd4d9b181..44f592488 100644 --- a/src/locales/ko.json +++ b/src/locales/ko.json @@ -74,13 +74,18 @@ "pad.loading": "불러오는 중...", "pad.noCookie": "쿠키를 찾지 못했습니다. 브라우저에서 쿠키를 허용해 주십시오! 세션과 설정은 방문 간 저장되지 않습니다. 일부 브라우저의 iFrame에 이더패드가 포함된 것이 그 이유일 수 있습니다. 이더패드가 상위 iFrame과 동일한 서브도메인/도메인에 위치하는지 확인해 주십시오", "pad.permissionDenied": "이 패드에 접근할 권한이 없습니다", - "pad.settings.padSettings": "패드 설정", + "pad.settings.title": "설정", + "pad.settings.padSettings": "패드 너비 설정", + "pad.settings.userSettings": "사용자 설정", "pad.settings.myView": "내 보기", + "pad.settings.disablechat": "채팅 비활성화", + "pad.settings.darkMode": "어두운 모드", "pad.settings.stickychat": "화면에 항상 대화 보기", "pad.settings.chatandusers": "대화와 사용자 보기", "pad.settings.colorcheck": "작성자 표시 색상", "pad.settings.linenocheck": "줄 번호", "pad.settings.rtlcheck": "우횡서(오른쪽에서 왼쪽으로)입니까?", + "pad.settings.enforceSettings": "다른 사용자에게 설정 적용", "pad.settings.fontType": "글꼴 종류:", "pad.settings.fontType.normal": "보통", "pad.settings.language": "언어:", @@ -128,6 +133,8 @@ "pad.modals.disconnected": "연결이 끊어졌습니다.", "pad.modals.disconnected.explanation": "서버에서 연결을 잃었습니다", "pad.modals.disconnected.cause": "서버를 사용할 수 없습니다. 이 문제가 계속 발생하면 서비스 관리자에게 알려주시기 바랍니다.", + "pad.gritter.unacceptedCommit.title": "저장되지 않은 편집", + "pad.gritter.unacceptedCommit.text": "최근 편집이 아직 저장되지 않았습니다. 다시 연결하고 다시 시도하세요.", "pad.share": "이 패드 공유하기", "pad.share.readonly": "읽기 전용", "pad.share.link": "링크", @@ -146,6 +153,12 @@ "timeslider.exportCurrent": "현재 버전으로 내보내기:", "timeslider.version": "버전 {{version}}", "timeslider.saved": "{{year}}년 {{month}} {{day}}일에 저장함", + "timeslider.settings.playbackSpeed": "재생 속도:", + "timeslider.settings.playbackSpeed.original": "원래 속도", + "timeslider.settings.playbackSpeed.realtime": "실시간", + "timeslider.settings.playbackSpeed.200ms": "200ms", + "timeslider.settings.playbackSpeed.500ms": "500ms", + "timeslider.settings.playbackSpeed.1000ms": "1000ms", "timeslider.playPause": "시작 / 패드 내용을 일시 중지", "timeslider.backRevision": "패드의 수정판으로 다시 가기", "timeslider.forwardRevision": "패드의 수정판을 앞으로 이동 시키기", diff --git a/src/locales/mk.json b/src/locales/mk.json index b1d425889..44f9e66df 100644 --- a/src/locales/mk.json +++ b/src/locales/mk.json @@ -86,12 +86,17 @@ "pad.permissionDenied": "За овде не е потребна дозвола за пристап", "pad.settings.title": "Нагодувања", "pad.settings.padSettings": "Поставки за целата тетратка", + "pad.settings.userSettings": "Кориснички нагодувања", "pad.settings.myView": "Мој поглед", + "pad.settings.disablechat": "Оневозможи разговор", + "pad.settings.darkMode": "Темен изглед", "pad.settings.stickychat": "Разговорите секогаш на екранот", "pad.settings.chatandusers": "Прикажи разговор и корисници", "pad.settings.colorcheck": "Авторски бои", "pad.settings.linenocheck": "Броеви на редовите", "pad.settings.rtlcheck": "Содржините да се читаат од десно на лево?", + "pad.settings.enforceSettings": "Примени нагодувања за другите корисници", + "pad.settings.enforcedNotice": "Овие нагодувања се заклучени за вас од создавачот на тетратката. Побарајте од него ако сакате да бидат изменети.", "pad.settings.fontType": "Тип на фонт:", "pad.settings.fontType.normal": "Нормален", "pad.settings.language": "Јазик:", @@ -109,6 +114,7 @@ "pad.importExport.exportword": "Microsoft Word", "pad.importExport.exportpdf": "PDF", "pad.importExport.exportopen": "ODF (Open Document Format)", + "pad.importExport.noConverter.innerHTML": "Можете да увезувате само од прост текст или HTML-форматти. Понапредни можности за увоз ќе добиете ако го воспоставите LibreOffice.", "pad.modals.connected": "Поврзано.", "pad.modals.reconnecting": "Ве преповрзувам со тетратката...", "pad.modals.forcereconnect": "Наметни преповрзување", @@ -139,6 +145,8 @@ "pad.modals.disconnected": "Врската е прекината.", "pad.modals.disconnected.explanation": "Врската со опслужувачот е прекината", "pad.modals.disconnected.cause": "Опслужувачот може да е недостапен. Известете го администраторот ако ова продолжи да ви се случува.", + "pad.gritter.unacceptedCommit.title": "Незачувано уредување", + "pad.gritter.unacceptedCommit.text": "Вашето скорешно уредување сè уште не е зачувано. Преповрзете се и обидете се пак.", "pad.share": "Сподели ја тетраткава", "pad.share.readonly": "Само читање", "pad.share.link": "Врска", @@ -157,6 +165,12 @@ "timeslider.exportCurrent": "Извези ја тековната верзија како:", "timeslider.version": "Верзија {{version}}", "timeslider.saved": "Зачувано на {{day}} {{month}} {{year}} г.", + "timeslider.settings.playbackSpeed": "Брзина на репродукција:", + "timeslider.settings.playbackSpeed.original": "Изворна брзина", + "timeslider.settings.playbackSpeed.realtime": "Реално време", + "timeslider.settings.playbackSpeed.200ms": "200 мс", + "timeslider.settings.playbackSpeed.500ms": "500 мс", + "timeslider.settings.playbackSpeed.1000ms": "1000 мс", "timeslider.playPause": "Пушти/запри содржина на тетратката", "timeslider.backRevision": "Назад за една преработка на тетратката", "timeslider.forwardRevision": "Напред за една преработка на тетратката", diff --git a/src/locales/ru.json b/src/locales/ru.json index 6f200e55d..5e9e4fb72 100644 --- a/src/locales/ru.json +++ b/src/locales/ru.json @@ -98,8 +98,12 @@ "pad.loading": "Загружается…", "pad.noCookie": "Куки не найдены. Пожалуйста, включите куки в вашем браузере! Ваш сеанс и настройки не будут сохранены между посещениями. Это может быть связано с тем, что Etherpad включен в iFrame в некоторых браузерах. Убедитесь, что Etherpad находится в том же поддомене/домене, что и родительский iFrame.", "pad.permissionDenied": "У вас нет разрешения на доступ", + "pad.settings.title": "Настройки", "pad.settings.padSettings": "Настройки документа", + "pad.settings.userSettings": "Пользовательские настройки", "pad.settings.myView": "Мой вид", + "pad.settings.disablechat": "Отключить чат", + "pad.settings.darkMode": "Тёмная тема", "pad.settings.stickychat": "Всегда отображать чат", "pad.settings.chatandusers": "Показать чат и пользователей", "pad.settings.colorcheck": "Цвета документа", @@ -152,6 +156,7 @@ "pad.modals.disconnected": "Соединение разорвано.", "pad.modals.disconnected.explanation": "Подключение к серверу потеряно", "pad.modals.disconnected.cause": "Сервер, возможно, недоступен. Пожалуйста, сообщите администратору службы, если проблема будет повторятся.", + "pad.gritter.unacceptedCommit.title": "Несохранённая правка", "pad.share": "Поделиться", "pad.share.readonly": "Только чтение", "pad.share.link": "Ссылка", @@ -170,6 +175,12 @@ "timeslider.exportCurrent": "Экспортировать текущую версию как:", "timeslider.version": "Версия {{version}}", "timeslider.saved": "Сохранено {{day}}.{{month}}.{{year}}", + "timeslider.settings.playbackSpeed": "Скорость воспроизведения:", + "timeslider.settings.playbackSpeed.original": "Оригинальная скорость", + "timeslider.settings.playbackSpeed.realtime": "В реальном времени", + "timeslider.settings.playbackSpeed.200ms": "200 мс", + "timeslider.settings.playbackSpeed.500ms": "500 мс", + "timeslider.settings.playbackSpeed.1000ms": "1000 мс", "timeslider.playPause": "Воспроизведение / Пауза содержимого документа", "timeslider.backRevision": "Назад на одну версию документа", "timeslider.forwardRevision": "Вперёд на одну версию документа", diff --git a/src/locales/sr-ec.json b/src/locales/sr-ec.json index f45d0b003..634e7b31f 100644 --- a/src/locales/sr-ec.json +++ b/src/locales/sr-ec.json @@ -37,7 +37,6 @@ "pad.colorpicker.cancel": "Откажи", "pad.loading": "Учитавам…", "pad.permissionDenied": "Немате дозволу да приступите овом паду", - "pad.settings.padSettings": "Подешавања пада", "pad.settings.myView": "Мој приказ", "pad.settings.stickychat": "Ћаскање увек на екрану", "pad.settings.chatandusers": "Прикажи ћаскање и кориснике", diff --git a/src/locales/sr-el.json b/src/locales/sr-el.json index f52ca296c..f85e374b3 100644 --- a/src/locales/sr-el.json +++ b/src/locales/sr-el.json @@ -26,7 +26,6 @@ "pad.loading": "Učitavam…", "pad.noCookie": "Kolačić nije pronađen. Molimo da uključite kolačiće u vašem pregledavaču!", "pad.permissionDenied": "Nemate dozvolu da pristupite ovom padu", - "pad.settings.padSettings": "Podešavanja pada", "pad.settings.myView": "Moj prikaz", "pad.settings.stickychat": "Ćaskanje uvek na ekranu", "pad.settings.chatandusers": "Prikaži ćaskanje i korisnike", From 9a99515ecd2ecc5ac5043b04ffdeb7726b290c26 Mon Sep 17 00:00:00 2001 From: SamTV12345 <40429738+samtv12345@users.noreply.github.com> Date: Thu, 23 Apr 2026 23:05:56 +0200 Subject: [PATCH 011/388] chore: added release notes for 2.7.1 --- CHANGELOG.md | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index bd3486ec2..4c23547eb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,11 @@ +# 2.7.1 + +### Notable enhancements and fixes + +- fixed stop harcoding lang=en, letting the client auto detect locale +- Stop mutating the shared plugin registry during sanitization +- Preserve non-breaking space + # 2.7.0 ### Breaking changes From 249241d9a42359bfa4c88a6053b5af3363f2f520 Mon Sep 17 00:00:00 2001 From: Etherpad Release Bot Date: Thu, 23 Apr 2026 21:08:25 +0000 Subject: [PATCH 012/388] bump version --- admin/package.json | 2 +- bin/package.json | 2 +- package.json | 2 +- src/package.json | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/admin/package.json b/admin/package.json index e5d626a70..f55c7f01a 100644 --- a/admin/package.json +++ b/admin/package.json @@ -1,7 +1,7 @@ { "name": "admin", "private": true, - "version": "2.7.0", + "version": "2.7.1", "type": "module", "scripts": { "dev": "vite", diff --git a/bin/package.json b/bin/package.json index 539e403fa..483a4a967 100644 --- a/bin/package.json +++ b/bin/package.json @@ -1,6 +1,6 @@ { "name": "bin", - "version": "2.7.0", + "version": "2.7.1", "description": "", "main": "checkAllPads.js", "directories": { diff --git a/package.json b/package.json index e37dc77cb..9b3547b56 100644 --- a/package.json +++ b/package.json @@ -49,7 +49,7 @@ "url": "https://github.com/ether/etherpad.git" }, "engineStrict": true, - "version": "2.7.0", + "version": "2.7.1", "license": "Apache-2.0", "pnpm": { "onlyBuiltDependencies": [ diff --git a/src/package.json b/src/package.json index 59a7e3d0a..c5e768838 100644 --- a/src/package.json +++ b/src/package.json @@ -157,6 +157,6 @@ "debug:socketio": "cross-env DEBUG=socket.io* node --require tsx/cjs node/server.ts", "test:vitest": "vitest" }, - "version": "2.7.0", + "version": "2.7.1", "license": "Apache-2.0" } From 7b9a5eb01a31d6426c24db6658b639ea0fefe7e3 Mon Sep 17 00:00:00 2001 From: John McLear Date: Fri, 24 Apr 2026 03:04:18 +0100 Subject: [PATCH 013/388] fix(a11y): dialog semantics, focus management, icon labels, html lang (#7584) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix(a11y): negotiate lang/dir per request and set on Server-renders the html element with `lang` and `dir` matching the client's Accept-Language header (negotiated against availableLangs from i18n hooks). Falls back to `en`/`ltr` if no match. This gives screen readers a correct document language during the brief window before client-side html10n refines it (l10n.ts already sets both attributes after locale data loads). Co-Authored-By: Claude Opus 4.7 (1M context) * fix(a11y): dialog semantics on popups; fix aria-role typo on userlist Adds role=dialog, aria-modal=true, and either aria-labelledby (when an h1 is present) or aria-label (for popups without an h1) to: - #settings, #import_export, #embed, #skin-variants (labelledby) - #connectivity, #users, #mycolorpicker (aria-label) Fixes the invalid aria-role="document" attribute on #otherusers; it's now role=region with aria-live=polite so screen readers announce collaborator joins/leaves. Container aria-label values are English-only for now — Etherpad's html10n implementation only supports localizing specific attributes (title, alt, placeholder, etc), not aria-label on container nodes. Localization can follow once html10n grows that affordance. Co-Authored-By: Claude Opus 4.7 (1M context) * fix(a11y): focus management and Escape-to-close for popups Three additions to toggleDropDown / _bodyKeyEvent: - Remember the trigger element (document.activeElement) when opening a popup, so we can restore focus when it closes. - On open, focus the first focusable element inside the popup so keyboard users land inside the dialog instead of staying on the trigger button. - Escape pressed while focus is inside a popup closes it, then the restore-focus path runs and the trigger button is refocused. Replaces the previous behavior where Escape from inside a popup did nothing; users had to click outside to dismiss. Co-Authored-By: Claude Opus 4.7 (1M context) * fix(a11y): make chaticon and chat header controls real buttons - #chaticon:
+ ``` + Move the `onclick="chat.show();return false;"` to a JS handler in `chat.ts` `init()` (find existing init): + ```ts + $('#chaticon').on('click', (e) => { e.preventDefault(); chat.show(); }); + ``` + (If `chat.show()` is already wired by another listener, just remove the inline `onclick` and rely on the existing handler — confirm by greping.) + +- [ ] **Step 4.3:** Replace chat header close/stick anchors: + ```html + + + ``` + Move their inline `onClick` handlers to `chat.ts`: + ```ts + $('#titlecross').on('click', (e) => { e.preventDefault(); chat.hide(); }); + $('#titlesticky').on('click', (e) => { e.preventDefault(); chat.stickToScreen(true); }); + ``` + +- [ ] **Step 4.4:** Inspect CSS for `#chaticon` / `#titlecross` / `#titlesticky`. Buttons get default browser styling (border, padding) that may break the layout. Add a CSS reset block in `src/static/css/pad/chat.css` (or wherever those selectors already live): + ```css + #chaticon, #titlecross, #titlesticky { + background: transparent; + border: 0; + padding: 0; + font: inherit; + color: inherit; + cursor: pointer; + } + #chaticon:focus-visible, #titlecross:focus-visible, #titlesticky:focus-visible { + outline: 2px solid #0066cc; + outline-offset: 2px; + } + ``` + Find the right file by grepping `#chaticon` in `src/static/css`. + +- [ ] **Step 4.5:** ts-check: + ```bash + pnpm --dir src run ts-check + ``` + +- [ ] **Step 4.6:** Commit: + ```bash + git add src/templates/pad.html src/static/js/chat.ts src/static/css + git commit -m "fix(a11y): make chaticon and chat header controls real buttons" + ``` + +--- + +### Task 5: Add `icon.*` locale namespace and label icon-only controls + +**Files:** +- Modify: `src/locales/en.json` — add new keys +- Modify: `src/templates/pad.html` — apply `data-l10n-id` to `aria-label` on icon-only elements + +html10n supports per-attribute translation via `key.attr` style — for `aria-label`, the convention used elsewhere in this codebase is `data-l10n-id="key"` plus a sibling key `key.aria-label`. Check existing usage by grepping `aria-label` in `src/locales/en.json`: + +- [ ] **Step 5.1:** Grep current usage: + ```bash + grep -n "aria-label\|.title" src/locales/en.json | head -20 + ``` + Determine the convention. If html10n uses `{key}.aria-label`, follow that. Otherwise use plain `key` and apply via `aria-label` directly in HTML (no l10n on the aria-label) and accept English-only for now. + +- [ ] **Step 5.2:** Add to `src/locales/en.json` after the `pad.chat.*` block: + ```json + "pad.icon.export.etherpad": "Export as Etherpad", + "pad.icon.export.html": "Export as HTML", + "pad.icon.export.plain": "Export as plain text", + "pad.icon.export.word": "Export as Microsoft Word", + "pad.icon.export.pdf": "Export as PDF", + "pad.icon.export.opendocument": "Export as ODF", + "pad.icon.showmore": "Show more toolbar buttons", + ``` + (Insert with correct JSON commas.) + +- [ ] **Step 5.3:** Apply to the export `` elements in `src/templates/pad.html:215-232`: + ```html + + ``` + Repeat per format. Add `aria-hidden="true"` to the inner `` since the link itself now carries the label. + +- [ ] **Step 5.4:** Convert the show-more span to a button on `pad.html:74`: + ```html + + ``` + Verify CSS targeting `.show-more-icon-btn` doesn't depend on element type — grep first. + +- [ ] **Step 5.5:** Theme switcher knob (`pad.html:172`) currently has `aria-label="theme-switcher-knob"` which is a CSS-class-style label, not human text. Change to `aria-label="Toggle theme"`. + +- [ ] **Step 5.6:** Commit: + ```bash + git add src/locales/en.json src/templates/pad.html + git commit -m "fix(a11y): accessible names for icon-only buttons and links" + ``` + +--- + +### Task 6: Playwright test for dialog semantics + Escape + +**Files:** +- Create: `src/tests/frontend-new/specs/a11y_dialogs.spec.ts` + +Cover the high-impact promises: settings popup opens with role=dialog, Escape closes it, focus returns to trigger. + +- [ ] **Step 6.1:** Write the failing test: + ```ts + import {expect, test} from "@playwright/test"; + import {goToNewPad} from "../helper/padHelper"; + + test.beforeEach(async ({page}) => { await goToNewPad(page); }); + + test('settings popup has dialog semantics and Escape closes it', async ({page}) => { + const settingsButton = page.locator('.buttonicon.buttonicon-cog'); + await settingsButton.click(); + + const dialog = page.locator('#settings'); + await expect(dialog).toHaveAttribute('role', 'dialog'); + await expect(dialog).toHaveAttribute('aria-modal', 'true'); + await expect(dialog).toBeVisible(); + + await page.keyboard.press('Escape'); + await expect(dialog).toBeHidden(); + + // Focus should return to the trigger + const focused = await page.evaluate(() => document.activeElement?.className || ''); + expect(focused).toContain('buttonicon-cog'); + }); + + test('html element has lang attribute', async ({page}) => { + const lang = await page.locator('html').getAttribute('lang'); + expect(lang).toBeTruthy(); + expect(lang!.length).toBeGreaterThan(0); + }); + + test('export links have accessible names', async ({page}) => { + await page.locator('.buttonicon.buttonicon-import_export').click(); + const pdfLink = page.locator('#exportpdfa'); + const label = await pdfLink.getAttribute('aria-label'); + expect(label).toBeTruthy(); + }); + + test('chaticon is a button with accessible name', async ({page}) => { + const chatIcon = page.locator('#chaticon'); + const tagName = await chatIcon.evaluate(el => el.tagName.toLowerCase()); + expect(tagName).toBe('button'); + const label = await chatIcon.getAttribute('aria-label'); + expect(label).toBeTruthy(); + }); + ``` + +- [ ] **Step 6.2:** Verify the Playwright spec runs (headless per project rule): + ```bash + cd src && pnpm exec playwright test tests/frontend-new/specs/a11y_dialogs.spec.ts --reporter=list + ``` + Expected: all 4 tests pass. + +- [ ] **Step 6.3:** Commit: + ```bash + git add src/tests/frontend-new/specs/a11y_dialogs.spec.ts + git commit -m "test(a11y): verify dialog semantics, html lang, export labels, chat button" + ``` + +--- + +### Task 7: Run the full local checks before push + +- [ ] **Step 7.1:** ts-check from `src/`: + ```bash + pnpm --dir src run ts-check + ``` + +- [ ] **Step 7.2:** Backend tests: + ```bash + pnpm --dir src run test:backend + ``` + +- [ ] **Step 7.3:** Push and open a PR against `johnmclear/etherpad-lite`: + ```bash + git push -u fork fix/a11y-dialogs-labels-lang + gh pr create --repo johnmclear/etherpad-lite --base develop --head fix/a11y-dialogs-labels-lang \ + --title "fix(a11y): dialog semantics, icon labels, html lang" \ + --body "..." + ``` + +- [ ] **Step 7.4:** Post `/review` comment on the PR to trigger Qodo. + +--- + +## Self-review notes + +- **Spec coverage:** Original audit's high-impact items were (a) dialog semantics + focus trap, (b) aria-labels via icon.* namespace, (c) html lang. All three covered (Tasks 2+3, 4+5, 1). Bonus: aria-role typo on userlist (Task 2.8) and chat header buttons (Task 4.3). +- **Out of scope, deliberately:** focus-visible CSS sweep, contrast pass, touch-target sizing, full focus-trap library (we do simple init-focus + Escape, not Tab cycling — adequate for these short popups, library can come later). +- **Risk:** Adding `hidden` attribute to popups changes initial render — confirmed CSS does not depend on absence of `hidden` (CSS uses `.popup-show` to display). Need to check that `display: none` from `.popup` (default) and `hidden` don't conflict in unwanted ways; `hidden` is a stronger signal and should be fine. diff --git a/src/static/css/pad/chat.css b/src/static/css/pad/chat.css index bfda0109a..91d7312a4 100644 --- a/src/static/css/pad/chat.css +++ b/src/static/css/pad/chat.css @@ -76,11 +76,21 @@ text-align: right; text-decoration: none; cursor: pointer; + background: transparent; + border: 0; + padding: 0; + font-family: inherit; + line-height: 1; } #titlebar .stick-to-screen-btn { font-size: 10px; padding-top: 2px; } +#titlebar .stick-to-screen-btn:focus-visible, +#titlebar .hide-reduce-btn:focus-visible { + outline: 2px solid #0066cc; + outline-offset: 2px; +} /* -- MESSAGES -- */ #chattext { @@ -121,10 +131,32 @@ /* -- CHAT ICON -- */ #chaticon { + /* #chaticon was converted from to
<% e.begin_block("afterEditbar"); %><% e.end_block(); %> @@ -114,11 +117,11 @@ -