* 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>
* docs: refresh docs for 3.2.0 — correct stale content, document recent features
The hand-maintained VitePress docs under doc/ had drifted behind a lot of
recent work. They are authored prose (not generated from the OpenAPI spec),
so they need manual upkeep. This pass corrects content that was actively
wrong and documents features shipped since they were last touched.
Corrections (was wrong / misleading):
- cli.md: every command used `node bin/foo.js`, but the scripts are
TypeScript run via pnpm — copy-paste failed. Rewrote to
`pnpm run --filter bin <script>`, documented ~13 previously-undocumented
operator tools, and split running-vs-stopped requirements. Registered the
missing `compactStalePads` script in bin/package.json so the documented
invocation actually works.
- stats.md: described a pre-Prometheus world. Rewrote for the gated
`/stats` (JSON) and `/stats/prometheus` endpoints, the live metric set,
the opt-in `scalingDiveMetrics` instruments (#7756), and `measured-core`.
- admin/updates.md: removed three false "SMTP not yet wired" claims (it is,
via nodemailer + the `mail.*` block), documented the `node-engine-mismatch`
preflight check and the rollback/preflight failure emails, and stripped
obsolete "PR 1 / PR 2" staging language now that all tiers ship.
- api/http_api.md: added the undocumented `anonymizeAuthor` (GDPR Art. 17)
call, fixed copyPad/movePad version annotations (1.2.8 → 1.2.9), corrected
getPadID's param name (readOnlyID → roID), and dropped a reference to a
non-existent `getEtherpad` API call.
- skins.md: colibris is the current default, not an "experimental" skin for
a future 2.0.
- localization.md: bare `window._('key')` is unbound and returns undefined;
recommend `window.html10n.get(...)` / data-l10n-id instead.
- README.md: bumped the v2.2.5 upgrade example to v3.2.0; fixed a
docker.adoc link to docker.md.
- docker.md: added MAIL_*, ENABLE_METRICS, GDPR_AUTHOR_ERASURE_ENABLED,
PRIVACY_BANNER_*, PUBLIC_URL, AUTHENTICATION_METHOD, ENABLE_DARK_MODE,
ENABLE_PAD_WIDE_SETTINGS; fixed the SOCKETIO_MAX_HTTP_BUFFER_SIZE default
(50000 → 1000000).
New documentation:
- configuration.md (new): how settings + `${VAR:default}` substitution work,
trustProxy, and — the previously-undocumented feature — running under a
subpath/ingress via x-proxy-path / X-Forwarded-Prefix / X-Ingress-Path,
with the sanitizer rules and Traefik/NGINX examples. Wired into the
VitePress sidebar and the index hero.
- hooks_server-side.md: ccRegisterBlockElements (the server-side companion
plugin authors miss), exportConvert, exportHTMLSend, createServer,
restartServer, and clientReady (marked deprecated).
- hooks_client-side.md: aceDrop, acePaste, handleClientTimesliderMessage_<name>.
VitePress build passes. The legacy .adoc set was intentionally left in place
— it still feeds the per-version doc archives published to ether.github.com
at release time (bin/release.ts), so it is not dead and is out of scope here.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* docs: address Qodo review — drop .js invocations from configuration.md and CLI help
- configuration.md: the settings-override example referenced a nonexistent
`node src/node/server.js`. Use the supported launcher instead
(`bin/run.sh -s <file>`), and note the runtime is server.ts via tsx.
- compactStalePads.ts / compactPad.ts / compactAllPads.ts: their header
comments and runtime usage output still printed `node bin/*.js`, which
points at files that don't exist. Switched to the documented
`pnpm run --filter bin <script>` form so the --help text matches the docs.
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: design spec for #7799 outdated-notice redesign
Per-pad first-author gating, dismissable gritter, minor-or-more rule, drop vulnerable UI.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* docs: implementation plan for #7799 outdated-notice redesign
12 bite-sized tasks, TDD-first where applicable; closes the spec end-to-end.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(updater): add isMinorOrMoreBehind, drop major/vulnerable helpers
Adds isMinorOrMoreBehind(current, latest) which returns true only when
the latest release is at least one minor version ahead (patch-only deltas
return false). Removes isMajorBehind, parseVulnerableBelow, and
isVulnerable from versionCompare.ts — callers in updateStatus.ts,
VersionChecker.ts, and index.ts will be updated in subsequent tasks.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* refactor(updater): drop vulnerable-below directive and state field
Remove VulnerableBelowDirective type, UpdateState.vulnerableBelow field, and
all related scraping/checking logic (parseVulnerableBelow, isVulnerable imports).
Clean up Notifier, OpenAPI schema, and all test fixtures to match.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* refactor(updater): drop residual EmailSendLog vulnerable fields
Remove `vulnerableAt` and `vulnerableNewReleaseTag` from the
`EmailSendLog` interface, `EMPTY_STATE`, and the `isValidEmail`
validator — these backed the removed `vulnerable`/`vulnerable-new-release`
email kinds and are now dead code.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* feat(updater): add firstAuthorOf helper
Export firstAuthorOf() from updateStatus.ts — finds the lowest-numbered
author attrib in a pad's pool, skipping empty-string placeholders.
Covered by 6 vitest cases in tests/backend-new/specs/hooks/express/.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* feat(updater): add resolveRequestAuthor helper for HTTP GET
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* feat(updater): pad-aware /api/version-status with first-author gating
Replace global badge cache with a per-(padId, authorId) LRU cache. The
new response shape is {outdated: 'minor' | null, isFirstAuthor: boolean};
the old 'severe'/'vulnerable' enum is dropped entirely. computeOutdated
now resolves the pad's first author and compares it against the session
author before returning outdated:'minor', so the notice is only shown to
the person who created the pad.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix(updater): switch isSevere signal from major-only to minor-or-more behind
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* test(updater): end-to-end coverage for /api/version-status
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* docs(openapi): /api/version-status pad-aware shape and gating
Add the /api/version-status GET operation to the admin OpenAPI spec with
the new pad-aware response shape: outdated enum reduced to [minor]|null,
isFirstAuthor boolean, and an optional padId query param.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* chore(pad): remove unused #version-badge template and CSS
* feat(pad): replace persistent badge with first-author outdated gritter
Renames pad_version_badge.ts → pad_outdated_notice.ts and rewrites it
as a fire-and-forget gritter notice that only shows when the API reports
outdated=minor AND the current user is the pad's first author. Wires
the new maybeShowOutdatedNotice() call into pad.ts immediately after
showPrivacyBannerIfEnabled().
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* test(pad): playwright coverage for outdated notice gritter
Six Playwright specs exercise maybeShowOutdatedNotice: null response,
isFirstAuthor:false guard, positive appearance + text, X-dismiss,
500 server error tolerance, and 8 s auto-fade.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* docs(pad): outdated-notice redesign + drop vulnerable-below docs
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* chore(test): remove stale specs for deleted #version-badge surface
Delete the GET /api/version-status describe block from the legacy mocha
spec (asserted outdated:null and outdated:'severe' — both no longer match
the new response shape). The new vitest spec at
tests/backend-new/specs/hooks/express/updateStatus.test.ts covers this
surface comprehensively.
Delete src/tests/frontend-new/specs/pad-version-badge.spec.ts entirely:
all three tests reference the #version-badge DOM element removed in Task 8
and stub 'severe'/'vulnerable' enum values that no longer exist.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* chore: clean stale references to vulnerable/severe in types, emails, docs
- Remove OutdatedLevel type (null|'severe') from types.ts — no consumers
remain after the badge redesign removed the severe tier.
- Fix Notifier severe-email body: was "more than one major release behind"
but isSevere now fires on minor-or-more, so update to "at least one
minor release behind the latest published version".
- Drop "vulnerability directives" from the /admin/update/status OpenAPI
description; replace with the actual response fields.
- Remove stale vulnerableBelow field from UpdateStatusPayload in
admin/src/store/store.ts — server no longer sends it.
- Fix docs/admin/updates.md: "pad-side badge" → "pad-side notice".
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(API): exclude SYSTEM_AUTHOR_ID from listAuthorsOfPad
Pad.SYSTEM_AUTHOR_ID ('a.etherpad-system') is the synthetic author
Etherpad attributes inserts to when the HTTP API receives a call
without authorId (setText, setHTML, appendText, the server-side
import flows, and plugins like ep_post_data). It exists so the
changeset's text and attribs stay in sync — without ANY author
attribute, pad.atext drifts and clients fail setDocAText
reconciliation when loading the pad. See Pad.ts:96-105 for the
full rationale.
That bookkeeping detail was leaking through listAuthorsOfPad: a
pad whose only "contributor" is the system author still reported
one authorID, which the existing tests in pad.ts and
appendTextAuthor.ts (and presumably any caller that uses
listAuthorsOfPad to count real users) treat as a real participant.
Filter SYSTEM_AUTHOR_ID at the API surface so internal attribution
stays internal. getAllAuthors() and downstream callers (copy,
anonymize, atext verification) keep seeing the synthetic id —
this only narrows the public listAuthorsOfPad response.
Fixes#7785Fixes#7790
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* docs(api): note that listAuthorsOfPad omits the system author
Match the runtime behaviour from the previous commit — the
synthetic 'a.etherpad-system' author used for unattributed inserts
is filtered out of the listAuthorsOfPad response.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(userlist): click a user to open chat with @<name> prefilled
Newcomers to a multi-user pad regularly fail to discover the chat
panel and the @-mention convention. Make the user list itself the
discovery affordance: clicking another user's row opens chat (if
hidden) and prefills the input with "@<their_name> ", ready to send.
The skin gets a small visual cue — pointer cursor on .usertdname and
an underline on hover — so the affordance is visible without
requiring a redesign. The color swatch keeps its own click semantics
(color picker), so the swatch cell is excluded from the new handler.
To let bot/AI plugins substitute their trigger string for an
otherwise-useless @-mention of the bot's display name (e.g.
"@AI Assistant" → "@ai"), this adds a new client-side hook,
chatPrefillFromUser, that takes {authorId, name, prefill} and lets
the first plugin to return a non-empty string override the default
prefill. Documented in doc/api/hooks_client-side.md alongside
chatSendMessage.
Plugin errors in the hook are caught — a misbehaving plugin can't
break the click. If chat is hidden by pad settings, chat.show() is
a no-op and the click effectively does nothing, which matches the
existing behavior of "no chat means no chat-related affordances".
The new prefill never clobbers a real partial message in the input;
if the user was mid-typing something, the @-mention is appended
rather than replacing.
* fix(userlist): don't steal rename focus + add Playwright coverage
Two follow-ups on review of the click-to-chat handler:
1. Bug (Qodo, correctness): clicking the rename <input> on an unnamed
user's row triggered the new row handler, which then focused
#chatinput and made it impossible to name unnamed users from the
user list. Add an early-return that skips form controls inside
the row (input/textarea/select/button/a/[contenteditable=true]).
The swatch was already excluded; this widens the same idea to
anything that's interactive on its own merits.
2. Test coverage: add a frontend Playwright spec
(userlist_click_to_chat.spec.ts) covering the supported flows
and the new regression:
- clicking another named user opens chat and prefills "@<name> "
- clicking the swatch opens color picker, not chat
- clicking the rename <input> on an unnamed user keeps focus
on the input (regression test for the bug above)
- partial chat message is preserved when prefilling
* test: stabilise the partial-message preservation case
The 'partial message in chat input is preserved when prefilling'
case was flaking on CI. Three small changes:
- Seed the chat input with fill() rather than click() + keyboard.type().
Earlier the test was racing chat.focus()'s own setTimeout(100) — when
the keyboard.type started before that timer fired, the typing landed
in whatever element had focus at the time, which wasn't always the
chat input. fill() bypasses focus state entirely.
- Wait for the chat box to be visible before filling, so we don't race
the chaticon click handler.
- Replace the two sequential expect/wait pairs after the daveRow click
with one waitForFunction that asserts both 'hi there' and '@Dave' are
in the input together. The prefill is async (setTimeout(50) inside
the click handler), so a combined wait is more reliable than checking
one piece, then snapshotting and asserting the other.
The other three cases in this file passed unchanged on CI; only this
fourth one was racy.
* fix: don't commit local .claude worktrees / var state
These were accidentally added in ffe947706 by an over-broad git add -A.
Both paths are workspace-local and unrelated to this PR.
* feat(pad): compactHistory() + compactPad CLI for DB-size reclaim
Fixes#6194. Long-lived pads with heavy edit history dominate the DB —
the issue describes a ~400 MB Postgres after two months with ~100
users. Etherpad keeps every revision forever, and removing arbitrary
middle revisions is unsafe because state is reconstructed by composing
forward from key revisions.
What's safe: collapse the full history into a single base revision
that reproduces the current atext. The existing `copyPadWithoutHistory`
already does this for a new pad ID — this PR lifts that same changeset
pattern into an in-place operation and wires up an admin CLI.
- `Pad.compactHistory(authorId?)` (src/node/db/Pad.ts): composes the
current atext into one base changeset, deletes all existing rev
records, clears saved-revision bookmarks, and appends the new rev 0.
Text, attributes, and chat history are preserved; saved-revision
pointers are cleared. Returns the number of revisions removed.
- `API.compactPad(padID, authorId?)` (src/node/db/API.ts): public-API
wrapper around compactHistory. Reports `{removed}` so callers can
log savings.
- `APIHandler.ts`: register `compactPad` under a new `1.3.1` version,
bump `latestApiVersion`.
- `bin/compactPad.ts`: admin CLI. Reports the current revision count,
calls compactPad via the HTTP API, and prints how many revisions
were dropped.
- `src/tests/backend/specs/compactPad.ts`: four backend tests cover
the empty-pad no-op, the text-preservation + head=0 contract,
saved-revision cleanup, and that subsequent edits continue to
append cleanly on top of the collapsed base.
The operation is destructive so admins must opt in explicitly; the CLI
prints the before-count, and the recommended pre-flight is an
`.etherpad` export (backup).
Closes#6194
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(compact): delegate to copyPadWithoutHistory via temp-pad swap
The initial compactHistory() implementation built a custom base
changeset and re-ran appendRevision against a reset atext — but the
changeset was packed with oldLength=2 (matching copyPadWithoutHistory's
dest-pad init state) while the reset atext was only length 1, so
applyToText tripped its "mismatched apply: 1 / 2" assertion and every
test failed with a Changeset corruption error.
Switch to the tested path instead: copy the pad via
copyPadWithoutHistory to a uniquely-named temp pad (inherits all its
attribute/pool/changeset correctness), read the temp pad's rev records
back, delete the old ones under our pad's ID, write the new records in
their place, update in-memory state to match, and remove the temp pad.
Errors at any step fall through with a best-effort temp-pad cleanup.
Contract shifts slightly: the collapsed pad is head<=1 rather than
head=0, matching the shape of a freshly-imported pad (seed rev 0 +
content rev 1). Tests updated to assert that invariant plus
text-preservation, saved-revision cleanup, and append-after-compact.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* test(6194): match the head<=1 post-compact contract
Tests previously asserted head=0 exactly after compaction; the
temp-pad-swap path lands at head=1 (one seed rev plus one content
rev) matching the shape of a freshly-imported pad. Relax the
assertions to and derive the removed-count from
before-head minus after-head, so the tests still catch regressions in
text-preservation, saved-revision cleanup, and append-after-compact
without being tied to the exact implementation shape.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* refactor(6194): wrap existing Cleanup instead of duplicating it
Develop already ships a working revision-cleanup path under
`src/node/utils/Cleanup.ts` with two public helpers —
`deleteAllRevisions(padId)` (collapse full history via
copyPadWithoutHistory) and `deleteRevisions(padId, keepRevisions)`
(keep the last N). The admin-settings UI wires these up but neither
is exposed on the public API, and there's no CLI for operators who
want to run compaction outside the web UI. That's the gap this PR
now fills.
Changes from the prior revision of this PR:
- Drop `pad.compactHistory()` — it re-implemented what
`Cleanup.deleteAllRevisions` already does. Remove the duplicate.
- `API.compactPad(padID, keepRevisions?)` now delegates to Cleanup:
• keepRevisions null/undefined → deleteAllRevisions (full collapse)
• keepRevisions >= 0 → deleteRevisions(N) (keep last N)
Returns {ok, mode: 'all' | 'keepLast', keepRevisions?}.
- APIHandler `1.3.1`: signature updated to take `keepRevisions`
instead of `authorId`.
- `bin/compactPad.ts`: accepts `--keep N` for the keep-last mode,
shows before/after revision counts so operators see concrete
savings.
- Backend tests rewritten around the public API surface (mode
reporting, text preservation, input validation) rather than
internal method plumbing that no longer exists.
Net: strictly a thin public-API and CLI veneer over already-tested
Cleanup helpers. No new low-level logic.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* test(6194): assert content markers, not byte-exact atext
Cleanup.deleteAllRevisions internally calls copyPadWithoutHistory
twice (src → tempId, tempId → src with force=true), and each round
trip normalizes trailing whitespace. That meant my byte-exact
atext.text assertion failed in CI:
expected: '...line 3\n\n\n'
actual: '...line 3\n'
Swap the comparisons to use content markers (marker-alpha / beta /
gamma, keep-line-N). The test still catches the real regressions —
if compactPad lost content those markers would disappear — without
coupling to whitespace quirks of the existing Cleanup implementation.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(6194): correct API param + document compactPad in http_api docs
The 1.3.1 entry in APIHandler registered `['padID', 'authorId']`, but
`API.compactPad` takes `(padID, keepRevisions)` and the CLI sends a
`keepRevisions` query param. APIHandler.handle dispatches by URL field
name, so the previous wiring silently dropped `keepRevisions` and never
ran the keep-last branch over HTTP.
- Register `['padID', 'keepRevisions']` so the handler forwards the
CLI/HTTP arg into the API function.
- Add HTTP-level dispatch tests that hit `/api/1.3.1/compactPad` with
and without `keepRevisions`. The direct `api.compactPad()` tests
bypass the handler and would have missed this regression.
- Document compactPad in `doc/api/http_api.md` and `http_api.adoc`,
and bump the documented latest version from 1.3.0 to 1.3.1 to match
`latestApiVersion`.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(6194): add bin/compactAllPads for per-instance bulk compaction
`bin/compactPad <padID>` covers the case where you know which pad is
fat. For "reclaim space across the whole instance," composing
`listAllPads` + `compactPad` yourself is annoying; this script does it.
- Walks every pad on the instance and compacts it (full collapse, or
`--keep N` keep-last).
- Per-pad failures don't abort the run — they're logged, counted, and
the script exits 1 if any failed.
- `--dry-run` lists pads + revision counts without writing anything,
so operators can scope impact before committing.
- Reports `before → after` per pad and a total reclaimed count.
Deliberately not adding a `compactAllPads` HTTP API: bulk compaction
over a single HTTP request means one giant response and a long-held
connection. Operators who want this should run it locally, where they
can see progress and kill it cleanly. Staleness gating ("only pads
older than X days") is tracked separately as a follow-up.
Also registers `compactPad` and `compactAllPads` script aliases in
`bin/package.json` so they show up next to the other admin CLIs.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* test(6194): cover the bin/compactAllPads loop logic
Previous commit added the script but only exercised it by hand. The
loop itself — error tolerance, dry-run gating, keep-last passthrough,
the empty-instance and listAllPads-failure paths — had no automated
coverage.
- Refactor compactAllPads.ts to export `runCompactAll(api, opts, logger)`
and `parseArgs(argv)`. The CLI shell wires them up to axios+APIKEY
for production; tests use an in-memory `CompactAllApi` so we don't
need to stand up the apikey-auth path in mocha.
- Add 9 specs covering: arg parsing, full-collapse iteration,
--keep N passthrough, --dry-run skipping writes, single-pad failure
not aborting the run, pre-flight count failure tolerated, a
listAllPads failure short-circuiting cleanly, the empty-instance
no-op, and a final end-to-end test that runs `runCompactAll`
against the real `/api/1.3.1/compactPad` handler over supertest+JWT
to catch contract drift between the CompactAllApi shape and the
HTTP endpoints.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(6194): address Qodo review — gate, integer check, SSL
Three valid concerns from the Qodo review on 75a08a13:
1. **cleanup.enabled gate.** The admin/Cleanup-socket path checks
`settings.cleanup.enabled` before doing anything destructive; the
public API was bypassing that gate. Now `compactPad` mirrors the
admin path's check and returns a clear apierror when disabled, so
exposing the API doesn't accidentally widen the cleanup-opt-in
surface.
2. **Number.isFinite → Number.isInteger.** `2.5` was finite and
non-negative, so the old check let it through into
`Cleanup.deleteRevisions`, which does revision-index arithmetic
that assumes integer math. Reject at the API boundary instead of
silently misbehaving.
3. **SSL-aware baseURL in the bin scripts.** Other bin scripts
hardcode `http://`, but the rest of the codebase uses
`settings.ssl ? 'https' : 'http'`. The compact CLIs now do the
same, so they work against HTTPS deployments. (Other bin scripts
carry the same bug but fixing them is out of scope for this PR.)
Tests:
- New spec: `rejects fractional keepRevisions` (2.5 with the old
check passed; the new one rejects).
- New spec: `refuses to run when cleanup.enabled is false`. The
existing API tests opt in via a before-hook + restore, so they
still cover the success path under the new gate.
- API docs (`http_api.md` + `http_api.adoc`) document the gate and
the new error message.
Skipped Qodo concerns:
- "Wrong compactPad parameters" — already fixed in 26e12ff7
(the param map now correctly says `keepRevisions`, not `authorId`).
- "Unbounded revision deletions" / "No session eviction" / changeset
base-length / padCreate hook — these all targeted the earlier
on-Pad implementation that was refactored away. The current code
wraps `Cleanup.deleteAllRevisions` / `deleteRevisions`, which
already handle concurrency, locking, and hook semantics.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* docs: PR1 GDPR deletion-controls design spec
First of five GDPR PRs tracked in #6701. PR1 covers deletion controls:
one-time deletion token, allowPadDeletionByAllUsers flag, authorisation
matrix for handlePadDelete and the REST deletePad endpoint, a single
token-display modal for browser pad creators, and test coverage.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* docs: PR1 GDPR deletion-controls implementation plan
13 TDD-structured tasks covering PadDeletionManager unit tests, socket
+ REST three-way auth, clientVars wiring, one-time token modal,
delete-with-token UI, Playwright coverage, and PR handoff.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(gdpr): scaffolding for pad deletion tokens
PadDeletionManager stores a sha256-hashed per-pad deletion token and
verifies it with timing-safe comparison. createPad / createGroupPad
return the plaintext token once on first creation, and Pad.remove()
cleans it up. Gated behind the new allowPadDeletionByAllUsers flag
which defaults to false to preserve existing behaviour.
Part of #6701 (GDPR PR1).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix+test(gdpr): lazy DB access in PadDeletionManager + unit tests
Capturing DB.db at module-load time was null until DB.init() ran, which
broke importing the module outside a live server (including from the
test runner). Switch to DB.db.* at call time and add unit tests
exercising create/verify/remove plus timing-safe comparison.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(gdpr): three-way auth for socket PAD_DELETE
Creator cookie → valid deletion token → allowPadDeletionByAllUsers flag.
Anyone else still gets the existing refusal shout.
* feat(gdpr): optional deletionToken on programmatic deletePad
* feat(gdpr): advertise optional deletionToken on REST deletePad
* test(gdpr): cover deletePad authorisation matrix via REST
* feat(gdpr): surface padDeletionToken in clientVars for creators only
Revision-0 author on their first CLIENT_READY visit receives the
plaintext token; all subsequent CLIENT_READYs receive null because
createDeletionTokenIfAbsent is idempotent. Readonly sessions and any
other user never see the token.
* i18n(gdpr): strings for deletion-token modal and delete-with-token flow
* feat(gdpr): token modal + delete-with-token disclosure markup
* feat(gdpr): show deletion token once, allow delete via recovery token
* style(gdpr): modal + delete-with-token layout
* test(gdpr): Playwright coverage for deletion-token modal + delete-with-token
* fix(test): auto-dismiss deletion-token modal in goToNewPad helper
The token modal introduced in PR1 blocks clicks for every Playwright
test that creates a new pad via the shared helper. Add a one-line
dismissal so unrelated tests keep passing, and have the deletion-token
spec navigate inline via newPadKeepingModal() when it needs the modal
open to capture the token.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(test): dismiss deletion-token modal without focus transfer
Clicking the ack button transferred focus out of the pad iframe, which
made subsequent keyboard-driven tests (Tab / Enter) silently miss the
editor. Swap the click for a page.evaluate() that hides the modal and
nulls clientVars.padDeletionToken directly, leaving focus where it was.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(gdpr): PadDeletionManager race + document createPad/deletePad
Qodo review:
- createDeletionTokenIfAbsent() was a non-atomic read-then-write. Two
concurrent callers for the same pad could both return different
plaintext tokens while only the later hash was stored, leaving the
first caller with an unusable recovery token. Serialise per-pad via a
Promise chain and add a regression test that fires 8 concurrent
calls and asserts exactly one plaintext is emitted and validates.
- doc/api/http_api.md now documents createPad returning deletionToken
and deletePad accepting the optional deletionToken parameter.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(gdpr): always render delete-with-token in settings popup
The rebase onto develop placed the delete-pad-with-token details inside
the pad-settings-section conditional, which is only rendered when
enablePadWideSettings is true AND the section is toggled visible.
Second-device recovery (typing the captured token on a fresh browser)
must work without pad-wide settings enabled, so move the details out
to sit alongside the existing pad_deletion_token.spec.ts expectations.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(gdpr): require valid token when supplied, gate on auth, harden a11y/i18n
- PadMessageHandler: a supplied deletion token must validate; do not fall
back to the creator-cookie path when the token is wrong (was deleting
the pad anyway when the creator pasted a wrong token into the field).
- Skip token issuance + UI when requireAuthentication is on (creator
identity is stable, recovery token is redundant noise).
- Server emits messageKey instead of hardcoded English; both shout
handlers (inline alert and global gritter) localize via html10n.
- Suppress the global "Admin message" gritter for pad.deletionToken.*
shouts to avoid the "Admin message: undefined" duplicate.
- Token-modal a11y: role=dialog, aria-modal, aria-labelledby/describedby,
visually-hidden label on the token input, aria-live on Copy, focus to
the token input on open and restore on dismiss.
- Style the "Delete Pad with Token" disclosure to match the Delete pad
button; align the Copy/value row; pad the disclosure label.
Tests: Playwright now covers the creator-with-wrong-token path, asserts
no "Admin message" / "undefined" gritter on denial; backend API test
covers requireAuthentication suppressing the token.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(editor): add IDE-style line ops (duplicate / delete)
Addresses #6433 — the issue asked for VS-Code-style multi-line editing
for collaborative markdown editing. Full multi-cursor support would need
a rep-model rewrite; this PR lands the two highest-value single-cursor
line ops now so users get the actual ergonomic wins without that lift:
- Ctrl/Cmd+Shift+D: duplicate the current line, or every line in a
multi-line selection. Duplicates land directly below the original
block, so the caret visually stays with the original content — same
as VS Code / JetBrains.
- Ctrl/Cmd+Shift+K: delete the current line (or every line in a
multi-line selection), collapsing the range including its trailing
newline. Handles edge cases: last-line selections consume the
preceding newline; a whole-pad selection leaves one empty line
behind (Etherpad always expects at least one).
Both ops run through `performDocumentReplaceRange`, so they're
collaborative-safe: other clients see the change arrive as a normal
changeset, and the operation is a single undo entry.
Wire-up:
- `src/node/utils/Settings.ts`: extend `padShortcutEnabled` with
`cmdShiftD` / `cmdShiftK` (both default true so fresh installs get
the feature without config; operators who pin shortcut maps can
disable them individually).
- `src/static/js/ace2_inner.ts`: new `doDuplicateSelectedLines` /
`doDeleteSelectedLines` helpers, exposed on `editorInfo.ace_*` so
plugins and tests can invoke them programmatically, and keyboard
handlers for Ctrl/Cmd+Shift+D and Ctrl/Cmd+Shift+K.
Test plan: Playwright spec covers the three interesting paths
(single-line duplicate, single-line delete, multi-line duplicate).
Closes#6433
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* test(6433): type the bodyLines helper parameter
* fix(6433): preserve char attributes on duplicate + correct whole-pad delete
Addresses Qodo review feedback on #7564:
1. `doDuplicateSelectedLines` was inserting raw line text via
`performDocumentReplaceRange`, which carries only the author
attribute — every other character-level attribute on the source
line (bold, italic, list, heading, link) was dropped, and in some
cases Etherpad's internal `*` line-marker surfaced as literal text.
Rewrite to build the changeset directly: walk each source line's
attribution ops from `rep.alines[i]`, split the line text at op
boundaries, and call `builder.insert(segment, op.attribs)` once per
op. Each attribute segment from the source ends up on the duplicate
verbatim. Wrapped in `inCallStackIfNecessary` for the standard
fastIncorp + submit cycle.
2. `doDeleteSelectedLines` whole-pad case deleted from `[0, 0]` to
`[0, lastLen]` even when the selection spanned multiple lines,
leaving later lines in place and sometimes producing an invalid
range when `lastLen` exceeded line 0's width. Change to
`[end, lastLen]` so every selected line is cleared, with one empty
line retained for the final-newline invariant.
3. Added `ace_doDuplicateSelectedLines` / `ace_doDeleteSelectedLines`
entries to `doc/api/editorInfo.md` so plugin authors can discover
the new surface.
4. New Playwright spec asserting `<b>` tags survive duplication.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* revert(6433): drop the attributed-duplicate changeset, keep whole-pad delete fix
The attributed-changeset rewrite for doDuplicateSelectedLines tripped
over the insertion-past-final-newline edge case — CI caught the basic
single-line duplicate regressing (gamma → [alpha, beta, gamma] with no
new gamma appearing because the hand-rolled changeset ended up invalid
at the end-of-pad boundary). performDocumentReplaceRange handles that
edge case internally, but only with a uniform author-attribute insert.
Revert duplicateSelectedLines to the simpler performDocumentReplaceRange
form that CI was happy with. Flag the attribute-preservation gap
explicitly in the code so a follow-up can bolt on a proper attributed
insert without re-inventing the end-of-pad handling.
Whole-pad delete fix and editorInfo.md docs stay. Attribute-preservation
test in line_ops.spec.ts is removed along with the broken code.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* chore: Rename some occurences of etherpad-lite to etherpad
* chore: Adjust etherpad git urls
* chore: Rename more occurences from etherpad-lite to etherpad
* chore: Adjust default text
* Add docs for aceRegisterLineAttributes hook
Documents the new hook in both .md and .adoc client-side hook references.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Fix: correct source file path from .js to .ts
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This puts global state change logic with the rest of the global state
management logic. This also makes it possible to create temporary Pad
objects without triggering plugin actions.