* 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>
* test(timeslider): port legacy mocha specs to Playwright, retire orphaned suite
The legacy src/tests/frontend/specs/ mocha suite is run by no CI workflow,
so its timeslider coverage was dead — a regression in the in-pad history UI
(#7659/#7946) sailed through CI. Port the still-meaningful cases to
frontend-new Playwright specs, re-targeted at the real in-pad UI (outer
banner / slider / export links that pad_mode.ts drives) rather than the
isolated ?embed=1 iframe the existing specs use:
- timeslider_revision_labels.spec.ts (from timeslider_labels.js): the
#history-banner shows 'Version N' + a valid (non-NaN) date and timer, and
both update when scrubbing to revision 0.
- timeslider_export_links.spec.ts (from timeslider_numeric_padID.js and the
'checks the export url' case of timeslider_revisions.js): the outer export
hrefs target /p/<pad>/<rev>/export/<type> for the viewed revision, including
a numeric pad id, and follow the slider to revision 0.
- timeslider_deeplink.spec.ts (from the 'jumps to a revision given in the url'
case): a #rev/N hash — and the legacy #N shortlink form — boots straight
into history mode at that revision.
Delete the three now-ported legacy specs. Verified passing on Chromium and
Firefox. The star-marker case of timeslider_revisions.js is already covered by
timeslider_saved_revisions.spec.ts (#7948).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* test(timeslider): make deep-link spec robust on Firefox
Assert the canonical #rev/0 URL and the slider landing on revision 0 rather
than the #history-banner-rev label text. The banner label is populated via a
MutationObserver bridge that races the iframe load on the bootstrap path in
Firefox (it is already covered for the normal button-entry flow by
timeslider_revision_labels.spec.ts); the URL + slider value are the
deterministic signals that the deep link entered history at the right revision.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* test(timeslider): address Qodo review on #7949
- Pin locale to en-US in the revision-labels spec so the localized 'Version N'
label and 'Saved <Month> <day>, <year>' date assertions are deterministic and
the date stays Date-parseable, instead of depending on the runner's locale.
- Use a high-entropy numeric pad id (timestamp + random) in the export-links
spec so reruns against a persistent DB can't collide on the same pad.
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(pad): show saved-revision markers in in-pad history mode (#7946)
When #7659 moved the timeslider into the pad as an embedded iframe, the
user-facing control became the outer #history-slider-input range input.
The saved-revision stars are still drawn by broadcast_slider.ts into the
iframe's #ui-slider-bar, but that DOM is hidden in embed mode, and
pad_mode.ts bridged rev/max/value/timer/authors to the outer slider but
never the saved revisions. Result: clicking "Save Revision" appeared to
work but no markers showed in the timeslider (regression in 3.3.x).
Bridge the embedded timeslider's clientVars.savedRevisions onto the outer
slider as percentage-positioned star markers, rendered on first sync and
re-rendered when the slider max changes. Markers are a purely visual,
aria-hidden overlay (keyboard/SR users already reach any revision via the
slider + step buttons) with a click-to-seek convenience for mouse users.
Adds a frontend-new Playwright spec exercising the real user flow (save a
revision in the pad, enter in-pad history mode, assert a visible marker on
the outer slider). The only prior coverage lived in the legacy mocha
suite (src/tests/frontend/specs/timeslider_revisions.js) which no CI
workflow runs, and the modern timeslider specs drive the ?embed=1 iframe
directly and never exercise the outer history UI — so this regression was
invisible to CI.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(pad): live saved-revision markers + Qodo review fixes (#7946)
Address Qodo review on #7948:
- Live updates (bug 1): the server's SAVE_REVISION handler never broadcast
NEW_SAVEDREV, so the client handler that adds a star was dead and no open
timeslider ever updated live. Wire it up: Pad.addSavedRevision returns the
new revision (undefined on duplicate); handleSaveRevisionMessage broadcasts
NEW_SAVEDREV to the pad room. pad_mode.ts now sources outer markers from the
embedded slider's live #ui-slider-bar .star DOM (labels from clientVars) and
observes that bar so a revision saved by a collaborator appears on an
already-open history slider. Live editors ignore the unknown message type.
- Single-revision pad (bug 2): allow max === 0 so a revision saved at rev 0
still renders a marker instead of being cleared by the old max <= 0 guard.
- Test rigor (bug 3): assert the marker's inline left percentage directly
instead of falling back to a layout coordinate, which let left:0% pass.
Adds a two-client Playwright test for the live path (verified it fails with
the server broadcast removed). Backend Pad + pad API specs (88) still pass.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Bumps ueberdb2 6.1.8 -> 6.1.9 (postgres pool-error handling + TCP
keep-alive for #7878; redis/rethink connection-error handlers so a
dropped DB connection no longer crashes the process) and adds the
3.3.1 CHANGELOG entry required by bin/release.ts before the release
workflow can run.
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(admin): don't let one unreadable pad empty the Manage-pads list (#7935)
The admin /settings `padLoad` handler hydrates every pad via getPad() to
build the listing (the default lastEdited sort forces a full scan).
findKeys('pad:*','*:*:*') returns every key under the `pad:` prefix,
including legacy/foreign or migration-corrupted records — e.g. a value
stored as a JSON string rather than a pad object, which a botched
dirty.db -> PostgreSQL migration produces. Loading such a record makes
Pad.init throw `Cannot use 'in' operator to search for 'pool'`.
The handler had no try/catch, so one bad record rejected the whole
request: no `results:padLoad` was emitted (the SPA showed an empty
"No results" state forever — the reported symptom) and the unhandled
rejection could exit the server.
Make loadMeta resilient — a failed getPad() logs a warning and returns
zeroed metadata so the bad pad still surfaces for deletion instead of
hiding every other pad — and wrap the handler so it always emits a
terminal reply and never bubbles to the process-level handler.
Tests:
- backend: tests/backend/specs/admin/padLoadResilience.ts asserts a
corrupt pad:* record no longer hides the readable pads (fails without
the fix: no results:padLoad reply + server exit).
- e2e: tests/frontend-new/admin-spec/admin_pads_page.spec.ts covers
create pad -> welcome-page recent-pads -> /admin Manage-pads UI.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(admin): sanitize padLoad error logs to avoid leaking pad content
Qodo review: the error logs (and the error field returned to the SPA)
used `${err}` / err.message verbatim. For the corruption case the
TypeError message embeds the raw stored value ("...'pool' in <value>"),
which for a real pad can be document text — so logging it verbatim could
leak content, bloat logs, and let embedded newlines forge log lines.
Add a safeErr() helper (error name + single-line, 120-char-capped
message, control chars stripped) and use it in both the per-pad warning
and the outer handler error log, and for the error field emitted to the
client.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(admin): make surfaced corrupt pads deletable from the admin UI
Qodo review: padLoad now surfaces unreadable pad:* records (zeroed
metadata) so admins can delete them, but deletePad's normal path
(doesPadExists -> getPad -> Pad.remove) can't touch such a record:
doesPadExists() returns false for a non-object value and getPad() throws,
so the surfaced pad was effectively undeletable and the handler emitted
nothing.
When doesPadExists() is false but a raw `pad:${id}` value is present,
fall back to a raw key purge: sweep sub-keys (revs/chat/deletionToken/…)
via findKeys, drop the readonly mapping, then padManager.removePad()
(which removes the main key + pad-list + cache entry). Always emit a
terminal results:deletePad reply (including for an already-absent id) so
the UI clears the row instead of silently doing nothing, and wrap the
handler so failures are logged (sanitized) rather than swallowed.
Adds a backend test asserting a surfaced corrupt pad can be deleted and
disappears from the listing.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* test(admin): assert corrupt-pad delete via listing, not raw db.get
The delete test probed `db.get('pad:<id>')` for null right after deletePad.
That passed on postgres but failed on the dirty backend (Windows CI):
ueberdb2's per-backend read/write buffering can return the just-removed
value immediately after remove(), so it asserted storage internals rather
than behaviour. The deletion is still durable (same db.remove() every pad
uses; the in-memory pad-list entry is dropped synchronously). Assert the
behavioural outcome instead — the corrupt pad disappears from the padLoad
listing while the good pad remains.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
When `allowPadDeletionByAllUsers` is true, anyone can delete a pad with no
token at all (handlePadDelete's flagOk branch), so the one-time deletion
token is pointless and the "Save your pad deletion token" modal only
overwhelms users who will never need it.
Gate token issuance on `!settings.allowPadDeletionByAllUsers` so no token
reaches clientVars; the client's showDeletionTokenModalIfPresent() then
returns early and the modal never appears. No new setting — it derives
automatically from the existing one.
Closes#7926.
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>