* chore(deps): vendor the unmaintained `security` escaper into core
The `security` npm package (escapeHTML / escapeHTMLAttribute and the JS/CSS
encoders) has had no release since 2012, yet it sits directly in Etherpad's
client-side XSS-defense path (pad_utils, domline) and the server-side HTML
export. Rather than keep a 14-year-old, single-maintainer dependency guarding
output encoding, vendor its implementation into core.
- static/js/security.ts now contains the escaping logic directly (reproduced
verbatim from security@1.0.0, MIT, Chad Weider — byte-identical output) and
no longer does `require('security')`. The full public API is preserved, so
plugins that `require('ep_etherpad-lite/static/js/security')` keep working
unchanged.
- pad_utils.ts requires the local './security' module instead of the bare
'security' specifier (domline.ts and ExportHtml.ts already did).
- Drop `security` from src/package.json dependencies and from Minify's
LIBRARY_WHITELIST (no bare specifier is served to the browser anymore).
Added tests/backend/specs/security.ts locking the byte-for-byte escaping
output so the vendored copy can never silently drift.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix: use ESM named exports so vitest can resolve the security module
CI "Run the new vitest tests" failed with `Cannot find module './security'`
from pad_utils.ts. vitest/vite's CJS require() shim doesn't add a `.ts`
extension when resolving a relative specifier, so `require('./security')`
couldn't locate security.ts. (The old bare `require('security')` resolved to
a real .js in node_modules, which is why this only surfaced after vendoring.)
- security.ts now uses ESM `export const` for the seven helpers instead of a
`module.exports = {...}` block.
- pad_utils.ts imports it as `import * as Security from './security'`, which
goes through vite's resolver (knows .ts) and is also properly typed.
CJS consumers (domline.ts, ExportHtml.ts, the backend spec) keep working via
tsx/esbuild ESM->CJS interop. Verified: tsc clean, full vitest suite 721
passing, and the mocha security/export/import specs 27 passing.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* ci: force fresh run (prior run used a stale merge ref after reopen)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix: remove ReDoS in vendored JSON-string-literal regex
CodeQL flagged a high-severity exponential-backtracking alert on the
JSON-string-literal regex vendored from the `security` package:
`/"(?:\\.|[^"])*"/`. The `[^"]` class also matches a backslash, so it overlaps
with the `\\.` alternative and backtracks exponentially on adversarial input
like `"\!\!\!...` (no closing quote). The original lived inside node_modules so
it was never scanned; vendoring it surfaced the alert.
Fix to the canonical linear form `/"(?:[^"\\]|\\.)*"/`, where the backslash is
excluded from the character class so the two alternatives are mutually
exclusive. It matches exactly the same well-formed JSON string literals (and
encodeJavaScriptData only ever runs it over JSON.stringify output), so behaviour
is unchanged for valid input.
Added tests: encodeJavaScriptData output + a ReDoS guard that runs the regex
over 50k adversarial chars and asserts it returns in well under a second.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* chore(deps): drop three unmaintained dependencies from core
Remove dependencies whose upstreams are effectively abandoned, replacing
each with a maintained alternative or native API. No behaviour change for
users; reduces the production dependency surface.
- unorm (last publish 2019): replace `UNorm.nfc(s)` in contentcollector
with native `String.prototype.normalize('NFC')`, available in every
supported Node and browser. Also drop it from Minify's LIBRARY_WHITELIST.
- find-root (last publish 2017): inline a ~10-line equivalent in
AbsolutePaths.findEtherpadRoot(), mirroring find-root's semantics
(closest ancestor containing package.json, throw if none).
- jsonminify (last publish 2021): swap settings parsing to jsonc-parser
(already used by the admin workspace, actively maintained). The old
`jsonminify(str).replace(',]', ']').replace(',}', '}')` had two bugs that
jsonc-parser's allowTrailingComma fixes: String#replace only swapped the
FIRST trailing comma of each kind, and the blind replace corrupted ',]' /
',}' byte sequences inside string values (e.g. URLs).
Added a regression test in settings.ts covering multiple trailing commas
and ',]'/',}' inside string values.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix: drop stale $unorm bundle entry from tar.json
The unorm removal left `$unorm/lib/unorm.js` listed in the ace2_inner.js
client bundle manifest. With unorm uninstalled, getTar() would point at a
node_modules asset that no longer exists, producing 404s when loading that
bundle. Nothing imports unorm anymore (contentcollector now uses native
String.prototype.normalize), so the entry is dead and removed.
Caught by Qodo review.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix: update container loadSettings helper to jsonc-parser
tests/container/loadSettings.js is a standalone helper (separate from
node/utils/Settings.ts) that parsed settings.json.docker with jsonminify
directly. Removing jsonminify from dependencies broke the container test
suite with MODULE_NOT_FOUND. Switch it to jsonc-parser to match Settings.ts.
Verified loadSettings() parses settings.json.docker and applies the
container ip/port overrides.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* docs: migrate useful wiki content into the VitePress manual (#7990)
The GitHub wiki is being retired; documentation should ship with the
software. This migrates the still-accurate, non-duplicate wiki pages into
the published VitePress site (doc/**/*.md + the sidebar in
doc/.vitepress/config.mts) so they are versioned, searchable and portable:
- deployment.md: reverse-proxy configs (Nginx/Apache/Caddy/Traefik/
HAProxy) with the WebSocket-upgrade rules, subdirectory hosting via
X-Proxy-Path, native HTTPS via the ssl block, a systemd unit, and the
Istio manifest (with the Redis-adapter multi-replica caveat).
- accessibility.md: editor keyboard shortcuts (verified against
ace2_inner.ts / broadcast_slider.ts / pad_editbar.ts), toolbar
navigation, NVDA notes.
- faq.md: install methods, URL-path reference, listing/deleting pads
(API-first), backup/restore, and history pruning.
- development.md: source-tree tour, the pad<->format conversion pipeline,
the internal DB API, and the Fontello toolbar-icon workflow.
- database.md: the key/value schema plus connecting MySQL/PostgreSQL/Redis
backends and a pgloader MySQL->PostgreSQL migration (database docs were
previously absent from the VitePress site).
Every page was checked against the current source before inclusion:
corrected the apt instructions to the live signed repo (stable/main,
signed-by key), dropped the unpublished snap, fixed the Redis dbSettings
(flat host/port/password or url, not the obsolete client_options),
dropped charset from the PostgreSQL example, and removed a phantom
getEtherpad API reference. The VitePress site builds cleanly
(pnpm run docs:build) with the dead-link checker enabled.
Closes#7990
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* docs: add verified hands-on changeset/atext walkthrough (#7990)
Migrate the practical Changeset-library tutorial from the wiki into
changeset_library.md, rewritten against the current API: unpack(),
deserializeOps() (replacing the deprecated opIterator) and
new AttributePool() (replacing the removed AttributePoolFactory). Every
example output was produced by running the code against the current
Changeset.ts / AttributePool.ts, not copied from the wiki. Also fixes a
stale ether/etherpad-lite source link.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(import): correct outdated import-format help message (#7988)
The import dialog's "no converter" notice claimed only plain text and
HTML could be imported and linked users to the legacy AbiWord wiki page,
prompting them to install LibreOffice for formats that already work
natively.
Etherpad imports .txt, .html, .docx (via mammoth) and .etherpad files
without LibreOffice; only .pdf/.odt/.doc/.rtf still need it. Update the
message to say so and move the help link to the ether/etherpad org.
Closes#7988
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(import): point help link to new LibreOffice wiki page (Qodo #7989)
The AbiWord wiki pages were vandalized/empty. Added a clean LibreOffice
setup page on the wiki and point the import dialog there instead.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(import): reference Etherpad docs instead of wiki for LibreOffice
The wiki is being retired, so don't link to it. Point users at the
documentation site for installing LibreOffice for extra import formats.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Both scripts still called the pre-v6 callback-style ueberdb2 API, producing
type errors (masked in places by `// @ts-ignore`) against the current
promise-based signatures (`set(key, value)`, `init()`, `close()` — no
callback/extra args):
importSqlFile.ts(73) initDb(null) Expected 0 arguments, but got 1
migrateDirtyDBtoRealDB.ts(51) db.set(k,v,bcb,wcb) Expected 2 arguments, but got 4
migrateDirtyDBtoRealDB.ts(56) db.close(null) Expected 0 arguments, but got 1
migrateDirtyDBtoRealDB.ts(57) dirty.close(null) Expected 0 arguments, but got 1
- importSqlFile: drop the unused `util` import and the `util.promisify`
wrappers; `await db.init()`, `await db.set(...)`, `await db.close()`
directly. Removes two `// @ts-ignore` that were hiding the broken calls.
- migrateDirtyDBtoRealDB: replace the bcb/wcb callback machinery with
`await db.set(key, value)` in the loop and call `close()` with no args.
Also fixes the progress log which referenced an undefined `length`
instead of `keys.length`.
Pure type/correctness cleanup; behaviour is unchanged (writes are now
awaited, which is equivalent or safer). `tsc --noEmit` on the bin package
is now clean.
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
`bin/migrateDB.ts` opens a source and target ueberdb2 Database, copies all
keys, then resolves without closing either connection or calling
process.exit(). Two problems with ueberdb2 6.1.x:
- 6.1.x keeps an internal keep-alive timer running until close() is called,
so the migration process hangs forever after "Done syncing dbs" instead
of exiting. (Pre-6.1.x it exited on its own once the work was done.)
- Target writes are buffered and only guaranteed flushed to disk on close()
/flush(), so an operator who Ctrl-Cs the apparently-finished process could
end up with an incomplete migration.
Close the target then the source on both the success and error paths (which
flushes buffered writes and clears the keep-alive timer) and exit with an
explicit status code, matching the pattern already used in
migrateDirtyDBtoRealDB.ts.
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The "Installer test" workflow has hung for 6 hours (until GitHub's job
ceiling cancels it) on every ubuntu/macos run since v3.2.0. The smoke
test starts `pnpm run prod` in the background, confirms /api responds,
then tears it down with:
kill "$PID"
wait "$PID"
`pnpm run prod` is a nested launcher (pnpm -> pnpm --filter -> node), so
$PID is only the outer pnpm. SIGTERM is forwarded down the chain and the
script then `wait`s on it, but if the node server doesn't exit (e.g. a
live flush timer keeping the event loop alive) the wait blocks forever
and the step never releases its output pipe -> 6h hang. Windows passed
because it uses `Stop-Process -Force`.
Fix the teardown to be robust regardless of server shutdown behaviour:
- `set -m` so the launcher gets its own process group
- kill the whole group (SIGTERM, then SIGKILL fallback) via a trap
- drop the blocking `wait`
- add `timeout-minutes: 8` to both smoke steps as a hard backstop so a
future hang fails in minutes, not 6 hours
This unblocks CI on PRs that touch the installer workflow. The
underlying clean-shutdown regression (server not exiting on SIGTERM,
likely the ueberDB flush-timer setInterval) is tracked separately.
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The releaseEtherpad workflow renames ep_etherpad-lite -> ep_etherpad and
publishes ./src to npm, but that publish is not load-bearing:
- `ep_etherpad` has 0 dependents on npm; nothing in this repo depends on it.
- Plugins import `ep_etherpad-lite` resolved from the LOCAL core install, and
plugin CI clones `ether/etherpad` rather than `npm install`-ing core.
- Etherpad is run via git clone / Docker / zip / snap, never `npm install`.
It has been failing with E404 (the ep_etherpad package has no OIDC trusted
publisher configured on npmjs.com), which is why npm is stuck at 2.5.0 while
3.0/3.1/3.2/3.3 shipped fine without it.
Rather than chase a trusted-publisher setup for a publish nobody consumes,
park the workflow: gate the job behind an explicit `confirm: true` dispatch
input so a stray run fails fast with a clear message instead of a confusing
404, and document the status in the header + the AGENTS.MD Releasing section.
This is the package owner's (samtv12345) call: either finish the trusted-
publisher config to revive it, or remove the workflow. Parked pending that
decision; nothing about the release depends on it.
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(pad): keep token-less Delete pad reachable without pad-wide settings (#7959)
The token-less "Delete pad" button (#delete-pad) was nested inside the
enablePadWideSettings-gated pad-settings section, so disabling pad-wide
settings removed the only way to delete a pad without a recovery token.
Combined with #7926 hiding the token disclosure when deletion needs no
token (e.g. allowPadDeletionByAllUsers), a user who was allowed to delete
could be left with no deletion UI at all.
Pad deletion is unrelated to pad-wide settings, so:
- Move #delete-pad out of the enablePadWideSettings block in pad.html; it
is now always rendered and hidden by default.
- Add a canDeletePad clientVar (isCreator || allowPadDeletionByAllUsers)
and drive the button's visibility from it in pad_editor.ts, mirroring the
existing canDeleteWithoutToken handling for the token disclosure.
The two controls are now mutually coherent and neither depends on
enablePadWideSettings: the plain button shows when this session can delete
without a token, the recovery-token disclosure shows otherwise.
Tests:
- backend padDeletionUiPlacement.ts: #delete-pad is rendered with
enablePadWideSettings both on and off (fails without the template move).
- backend socketio.ts: canDeletePad reflects the creator/allow-all matrix,
including a non-creator who only gains it under allowPadDeletionByAllUsers.
- frontend pad_settings.spec.ts: asserts #delete-pad is no longer a
descendant of #pad-settings-section.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(pad): never let readonly sessions delete via token-less paths (#7959)
Qodo review of #7960: `canDeletePad` was `isCreator || allowPadDeletionByAllUsers`,
so under allowPadDeletionByAllUsers a readonly viewer received
canDeletePad=true and the relocated #delete-pad button unhid for them.
Worse, the server-side handlePadDelete `flagOk`/`creatorOk` branches never
checked session.readonly either, so a readonly-link holder could actually
delete the pad without a token — a data-loss hole that the new always-rendered
button would expose.
Exclude readonly sessions from both the clientVar and the server's token-less
authorization paths. A valid recovery token (tokenOk) stays a sufficient
credential regardless of session mode.
Test: socketio.ts asserts a readonly viewer gets canDeletePad=false and that a
token-less PAD_DELETE from a readonly session leaves the pad intact (red before
this change on the clientVar assertion).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Pin the transitive @opentelemetry/core dep (pulled in via
@elastic/elasticsearch -> @elastic/transport) to >=2.8.0 to clear
GHSA-8988-4f7v-96qf / CVE-2026-54285: W3CBaggagePropagator.extract()
did not enforce W3C size limits on inbound baggage headers, allowing
unbounded memory allocation. @elastic/transport declares the dep as
"2.x" so 2.8.0 satisfies the existing range with no parent bump, and
2.8.0's @opentelemetry/api peer range (>=1.0.0 <1.10.0) is satisfied
by the 1.9.1 already in the tree.
Override added to pnpm-workspace.yaml alongside the other CVE
force-bumps (pnpm 11 ignores root package.json pnpm.overrides).
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
ueberdb2 6.1.10 rewrote the cache/buffer layer (CacheAndBufferLayer.ts),
replacing the constructor's always-on, *referenced* `setInterval` flush timer
with a lazily-armed `setTimeout` that is `.unref()`'d and only created when
there are dirty keys. On a fresh, empty dirty DB there are no dirty keys, so no
flush timer is ever armed and ueberdb2 no longer anchors Node's event loop. In
the packaged (.deb/systemd) production boot this exposes a startup window where
the loop has no referenced handle and the process exits cleanly (code 0) before
`server.listen()` binds the port — so the server never serves /health.
Symptom on develop: the Debian-package amd64 smoke test failed on three
consecutive pushes (#7966 ueberdb2 6.1.9->6.1.12, then #7965, #7967), with the
service logging the version banner then "Deactivated successfully" and the
health check on :9001 never connecting. Backend/Docker/downstream-smoke stayed
green because they keep the loop alive by other means; only the bare
fresh-empty-dirty-DB packaged boot hits the gap. ueberdb2 6.1.12 is the latest
published release, so there is no fixed version to roll forward to yet — pin
back to the last green release (6.1.9) on both src/ and bin/.
Also add a `pull_request` trigger to the Debian-package workflow (scoped to the
same production-footprint paths). The smoke step is the only check that catches
this "boots then exits before binding" class of regression, but it previously
ran only on push to develop — i.e. *after* merge — which is exactly why the
Dependabot bump turned develop red instead of being blocked at PR time. The
release/apt-publish jobs are tag-guarded, so PRs run the build+smoke job only.
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* PadManager: reject unreachable '.' and '..' pad ids
isValidPadId accepted pad ids that consist only of URL dot-segments
('.' and '..'). Per the WHATWG URL standard a browser normalises
"/p/." to "/p/" and "/p/.." to "/", so such a pad can never be opened
or exported: the request arrives at "/p/" and Etherpad answers
"Cannot GET /p/". The pad is created in the database but is forever
unreachable.
Reject these ids in isValidPadId so the broken pads can no longer be
created, and add a regression test that fails without the fix.
* adminsettings: allow deleting legacy '.'/'..' pads
The isValidPadId tightening makes getPad() reject the pad ids '.' and
'..', which also blocks their deletion: a pad with such an id created
before the change still exists (doesPadExists is true), so the admin
deletePad handler takes the "healthy" branch where getPad() now throws.
The outer catch swallowed that error without emitting a terminal
results:deletePad, leaving an undeletable orphan in the admin UI.
Fall back to the existing raw key purge when getPad() throws, so these
pads can still be removed. Adds a regression test.
* tests: run the isValidPadId regression under the mocha suite
The original unit test lived in the vitest backend-new suite, but PadManager
loads DB, Pad and customError with CommonJS require() at import time. Under
vitest those require() calls are resolved by Node natively and fail on the .ts
sources ("Cannot find module '../utils/customError'"); vi.mock could not
intercept them, so the suite errored before any test ran. It also used a
top-level `await import`, which tripped tsc TS1378 under the project tsconfig.
Move the test to the mocha backend suite, which runs with --import=tsx and
resolves the .ts requires natively, so PadManager can be required directly with
no mocking. isValidPadId is a pure function and DB only connects lazily in
DB.init(), so loading the module has no side effects and no database is needed.
* deps: resolve open Dependabot security alerts
Bump transitive dependencies flagged by Dependabot via pnpm-workspace
overrides. Refreshes stale override floors that the advisory ranges have
since grown past, and adds overrides for newly-flagged packages:
- form-data -> >=4.0.6 (GHSA-hmw2-7cc7-3qxx, high)
- ws -> >=8.21.0 (GHSA-96hv-2xvq-fx4p / GHSA-58qx-3vcg-4xpx, high/med)
- esbuild -> >=0.28.1 (GHSA-gv7w-rqvm-qjhr / GHSA-g7r4-m6w7-qqqr, high/low)
- basic-ftp -> >=5.3.1 (GHSA-rpmf-866q-6p89, high) [stale floor: 5.3.0 still vuln]
- tar -> >=7.5.16 (GHSA-vmf3-w455-68vh, med) [stale floor: 7.5.11]
- js-yaml -> >=4.2.0 (GHSA-h67p-54hq-rp68, med) [stale floor: 4.1.1 now vuln]
- qs -> >=6.15.2 (GHSA-q8mj-m7cp-5q26, med) [stale floor: 6.14.2 still vuln]
- ip-address -> >=10.1.1 (GHSA-v2v4-37r5-5v8g, med)
- @babel/core-> >=7.29.6 (GHSA-4x5r-pxfx-6jf8, low)
ts-check, backend test-utils, and the vite/esbuild production build all
pass locally on Node 24.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* deps: cap basic-ftp override to 5.x to avoid major bump
Qodo flagged the open-ended `basic-ftp@<5.3.1: '>=5.3.1'` override as
resolving to 6.0.1 (a surprise major) on the runtime plugin-install path
(live-plugin-manager -> proxy-agent -> get-uri -> basic-ftp). The CVE fix
(5.3.1) exists on the 5.x line, so bound the override to '>=5.3.1 <6.0.0'
for the minimal, lowest-risk patch. Now resolves to basic-ftp@5.3.1.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* feat(pad): suppress deletion token for durable identities; relabel recovery action (#7926)
Builds on the allowPadDeletionByAllUsers suppression with the rest of the
ideas discussed on the issue.
Server (handleClientReady):
- A creator's deletion token is now also withheld when they have a *durable*
identity: authenticated (req.session.user with a username) AND the deployment
pins that identity to a stable authorID via a getAuthorId hook. Only then does
the creator path (author === revision-0 author) survive a cookie clear or a
different device, making the recovery token redundant.
- This deliberately tightens the previous `requireAuthentication => always
suppress` rule: without a getAuthorId hook the authorID still comes from the
per-browser token cookie, so an authenticated user on a second device is NOT
the creator. Withholding the token there would strand them, so they now keep
getting one. SSO deployments using the documented getAuthorId pattern get the
clean no-modal experience.
- New `canDeleteWithoutToken` clientVar (allowPadDeletionByAllUsers OR durable
identity) drives the client label.
Client (pad_editor.ts):
- When canDeleteWithoutToken, the recovery-token disclosure summary is labelled
plainly "Delete Pad" (reusing the already-translated pad.settings.deletePad
key) instead of the jargon "Delete with token".
Tests (socketio.ts): anonymous -> token; allowPadDeletionByAllUsers -> none;
authenticated without a getAuthorId hook -> token; authenticated with one ->
none. Verified in a browser for both label/modal outcomes.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(pad): render recovery disclosure for all sessions; hide it (not relabel) when no token is needed
Addresses Qodo review on #7930:
1. Token UI fully suppressed when not needed (was: only the summary relabelled).
When canDeleteWithoutToken (allowPadDeletionByAllUsers or a durable identity),
pad_editor.ts now hides the whole #delete-pad-with-token disclosure — label,
token field and submit — so no deletion-token wording remains. With the plain
"Delete Pad" button present this is also the cleaner UX.
2. Recovery disclosure no longer gated on !requireAuthentication. Because a token
can now be issued under requireAuthentication when the deployment lacks a
durable getAuthorId mapping, the template must render the recovery form there
too — otherwise an authenticated creator gets a token with no UI to enter it
on another device. It is rendered hidden by default and shown by the client
only when canDeleteWithoutToken is false.
3. API.createPad aligned: also returns null deletionToken under
allowPadDeletionByAllUsers, matching the socket/UI path.
Tests: deletePad.ts gains a createPad-under-allowPadDeletionByAllUsers case.
Verified live in Chromium: allowAll -> disclosure hidden, no modal; default
anonymous -> disclosure visible, modal shown.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* feat(settings): env-var overrides for update-check/plugin-catalog/updater (offline installs)
Air-gapped and firewalled deployments could not disable Etherpad's outbound
calls (the hourly version check, the admin plugin catalogue, and the
self-updater) without editing settings.json inside the container image — the
shipped settings.json.docker hardcoded updates.tier and omitted the privacy
block entirely, so there was no env-var to flip.
Wire the relevant keys through the existing ${ENV:default} substitution in both
settings.json.docker and settings.json.template:
- PRIVACY_UPDATE_CHECK (privacy.updateCheck, default true)
- PRIVACY_PLUGIN_CATALOG (privacy.pluginCatalog, default true)
- UPDATES_TIER (updates.tier, default notify; "off" = no calls)
- UPDATES_SOURCE / UPDATES_CHANNEL / UPDATES_CHECK_INTERVAL_HOURS /
UPDATES_GITHUB_REPO / UPDATES_REQUIRE_ADMIN_FOR_STATUS (docker)
- UPDATE_SERVER (updateServer endpoint)
Document the full set in doc/docker.md (new "Updates & privacy" section) and
cross-link from doc/admin/updates.md. Add backend regression tests that parse
the shipped settings.json.docker and settings.json.template and assert the
overrides apply with correct boolean/numeric coercion, so a future edit that
drops the ${ENV} placeholders fails loudly.
Addresses #7911 (item 2). No code changes — config + docs + tests only.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* docs: address Qodo review — point to PRIVACY.md, clarify tier=off scope
- Outbound-call docs/comments referenced doc/privacy.md (the storage/logging
doc); the canonical outbound-call inventory is repo-root PRIVACY.md, which the
runtime messages in UpdateCheck.ts / Settings.ts also reference. Re-point
settings.json.{template,docker} and doc/docker.md there.
- doc/admin/updates.md said updates.tier="off" means "no HTTP request will leave
the instance", but the legacy UpdateCheck.ts call to ${updateServer}/info.json
is gated by privacy.updateCheck, not updates.tier. Clarify that air-gapped
installs must set PRIVACY_UPDATE_CHECK=false too.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* docs: link PRIVACY.md via absolute URL to satisfy VitePress dead-link check
PRIVACY.md lives at the repo root, outside the doc/ tree VitePress builds, so a
relative link to it (../PRIVACY.md / ../../PRIVACY.md) is flagged as a dead link
and fails `docs:build`. Use the absolute GitHub URL instead, matching how
doc/configuration.md already links settings.json.template.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>