* docs: design spec for #7524 drop swagger-ui + privacy opt-outs Three-deliverable plan: vendor RapiDoc to replace swagger-ui-express (Scarf-injecting), add privacy.updateCheck and privacy.pluginCatalog opt-outs for our two outbound calls, and ship PRIVACY.md as a public stance doc. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * docs: implementation plan for #7524 swagger-ui + privacy opt-outs Twelve TDD-flavoured tasks: privacy settings shape, UpdateCheck + installer opt-outs (each with a failing-test-first cycle), admin backend/UI plumbing, dependency drop, vendored RapiDoc, PRIVACY.md, final verification matrix. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(privacy): add privacy block to settings shape Adds privacy.updateCheck and privacy.pluginCatalog, both defaulting to true so behavior is unchanged until operators opt out. Refs #7524 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(privacy): honour privacy.updateCheck=false in UpdateCheck check() and getLatestVersion() now early-return when the setting is off. Logs once on first skip. The admin "update available" panel already tolerates an undefined latestVersion. Refs #7524 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(privacy): honour privacy.pluginCatalog=false in installer Extracts the gate into pluginCatalogGuard.ts so it can be unit-tested under vitest without dragging in the CJS require() chain from installer.ts. getAvailablePlugins() now throws the tagged disabled error before any fetch. Refs #7524 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(privacy): emit results:catalogDisabled when pluginCatalog off Short-circuits the four catalog-driven socket events. The install/ uninstall events are untouched so operators can still install by plugin name even when the catalog is disabled. Refs #7524 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(bin): stalePlugins reads updateServer and honours privacy flag Was hardcoding static.etherpad.org and ignoring opt-out. Now exits 0 cleanly when privacy.pluginCatalog=false. Refs #7524 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * docs(settings): document privacy block in settings template Refs #7524 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(api-docs): replace swagger-ui-express with RapiDoc shell Drops the swagger-ui-express dep (third-party Scarf telemetry pixel, see swagger-api/swagger-ui#10573) and serves /api-docs with a static HTML shell that mounts <rapi-doc>. /api-docs.json is unchanged. The vendored RapiDoc asset is added in the next commit so the tree is broken for one diff hunk — pair this with the rapidoc-min.js commit during review. Refs #7524 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(api-docs): vendor RapiDoc 9.3.4 (MIT) as static asset Pinned bundle with checksum in VERSION. Replaces swagger-ui-dist which shipped a Scarf telemetry pixel. Disables RapiDoc's bundled Google Fonts request via load-fonts="false" plus explicit regular-font/mono-font system stacks — RapiDoc's CSS @font-face rules would otherwise fetch Open Sans from fonts.gstatic.com at render time. Also fixes the /api-docs route's res.sendFile to use an absolute path resolved via settings.root (the previous {root: 'src/static'} was resolved from CWD which is already src/, producing src/src/static). Refs #7524 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(admin): banner when plugin catalog is disabled Subscribes to results:catalogDisabled and renders a localized info banner on the plugins page. install/uninstall still function via CLI. Refs #7524 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * docs: PRIVACY.md and README/CHANGELOG pointers Publishes Etherpad's stance on telemetry: two documented, opt-out outbound calls; no third-party analytics; no install-time phone-homes in our deps. Refs #7524 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(admin): await checkPluginForUpdates and emit array on error Qodo flagged that checkUpdates emitted the unresolved Promise (missing await) and emitted {} for updatable on the error path, both breaking the admin UI's expected string[] shape. Pre-existing bug surfaced when the surrounding block was edited for the privacy.pluginCatalog gate. Refs #7524 * feat(api-docs): swap RapiDoc for Scalar (actively maintained) Per @SamTV12345's review on #7757: RapiDoc has been effectively unmaintained for a while. Scalar (https://github.com/scalar/scalar) is MIT-licensed, actively developed, and ships a self-contained standalone bundle that works the same way for our purposes. Privacy posture is preserved by configuring the embed: - withDefaultFonts: false (no fonts.scalar.com woff2 fetch) - telemetry: false (defensive) - agent.disabled: true (no api.scalar.com/vector/* calls) - mcp.disabled: true (no MCP integration) - showDeveloperTools: 'never' - hideClientButton: true Verified with headless Chromium: page loads /api-docs, mounts Scalar, renders the Etherpad OpenAPI document, and makes zero requests to any host other than localhost. Vendor: - src/static/vendor/scalar/standalone.js (@scalar/api-reference 1.57.2) - src/static/vendor/scalar/VERSION (sha256 pinned) - src/static/vendor/scalar/LICENSE (MIT) Removed: - src/static/vendor/rapidoc/* Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
9.9 KiB
Issue #7524 — Drop swagger-ui, take a telemetry stance, opt out our phone-homes
Status: approved 2026-05-15
Closes: ether/etherpad#7524
Author: John McLear (design brainstormed with Claude)
Branch: feature/7524-drop-swagger-ui-telemetry off develop
Goal
Eliminate the only known third-party telemetry vector in Etherpad's runtime dependency tree (swagger-ui's Scarf pixel) and give operators explicit, documented opt-outs for the two outbound calls Etherpad itself makes.
Background
Etherpad currently ships swagger-ui-express ^5.0.1 to render the OpenAPI
spec at /api-docs. Upstream's npm distribution injects a Scarf analytics
pixel that cannot be disabled at install or runtime (see
swagger-api/swagger-ui#10573).
Etherpad itself makes two outbound calls:
src/node/utils/UpdateCheck.ts— hourlyGET ${updateServer}/info.jsonfor the admin "update available" notice. No opt-out today.src/static/js/pluginfw/installer.ts:179—GET ${updateServer}/plugins.jsonon admin-plugins page load (10 min cache). No opt-out today.
Both share the updateServer setting (default https://static.etherpad.org).
There is no public document stating Etherpad's position on telemetry.
Out of scope
- Replacing
static.etherpad.orgitself or hosting a mirror. - Auditing telemetry beyond the two known endpoints.
- Changing
/api-docs.json(spec endpoint unchanged). - The admin OpenAPI editor effort (issue #7693, separate PR).
Deliverables
Three deliverables ship together in one PR closing #7524:
1. Replace swagger-ui-express with vendored RapiDoc
Removed:
swagger-ui-express ^5.0.1fromsrc/package.jsondependencies.@types/swagger-ui-express ^4.1.8fromsrc/package.jsondevDependencies.import {serve, setup} from 'swagger-ui-express'atsrc/node/handler/RestAPI.ts:8.- The three-line route block at
src/node/handler/RestAPI.ts:1440-1445(app.use('/api-docs', serve)+app.get('/api-docs', setup(...))).
Added:
src/static/vendor/rapidoc/rapidoc-min.js— vendored fromhttps://unpkg.com/rapidoc@9.3.x/dist/rapidoc-min.js(MIT, ~370KB), committed as a static asset. Pinned exact version recorded insrc/static/vendor/rapidoc/VERSION. No CDN fetch at runtime.src/static/api-docs.html— minimal HTML shell:<!doctype html><html><head><title>Etherpad API</title> <script type="module" src="/static/vendor/rapidoc/rapidoc-min.js"></script> </head><body> <rapi-doc spec-url="/api-docs.json" theme="light" render-style="read" show-header="false" allow-server-selection="false"></rapi-doc> </body></html>- Route registration that serves
/api-docs→api-docs.htmland the static asset under/static/vendor/rapidoc/. The simplest path is to drop the file undersrc/static/and let the existing static-file middleware pick it up; if that needs an explicit route, add it next to the/api-docs.jsonhandler inRestAPI.ts.
Kept (unchanged):
/api-docs.jsonroute atRestAPI.ts:1449-1453.src/node/types/SwaggerUIResource.ts(TypeScript type used byopenapi.ts, no runtime swagger-ui dependency).- The unrelated swagger comment at
openapi.ts:810.
Verification of clean vendoring:
Before committing the vendored file, grep it for:
fetch(, XMLHttpRequest, sendBeacon, scarf, googletag, analytics,
navigator.connection. Any hit must be reviewed and confirmed as either
same-origin spec loading (legit) or removed. Outcome is documented in the
PR description.
2. Privacy opt-out config
src/node/utils/Settings.ts — new block alongside the existing
privacyBanner:
privacy: {
updateCheck: boolean, // default true
pluginCatalog: boolean, // default true
},
Defaults are true so behavior is identical to today; operators flip to
false to silence each call. Both default-true keeps the change
non-breaking for existing installs.
src/node/utils/UpdateCheck.ts:
check()— early-return whensettings.privacy.updateCheck === false, logging once:Update check disabled by privacy.updateCheck=false. No fetch, no scheduled retry.getLatestVersion()— returnsundefinedwhen disabled. The existing caller atsrc/node/hooks/express/adminsettings.ts:105already tolerates undefined; the admin panel simply omits the "update available" line.
src/static/js/pluginfw/installer.ts:
getAvailablePlugins()— early-throw whensettings.privacy.pluginCatalog === falsewith a tagged error:Error('Plugin catalog disabled (privacy.pluginCatalog=false)').- Admin consumer at
src/node/hooks/express/adminplugins.tscatches this specific error and renders a fallback panel: "Plugin catalog is disabled. Enter a plugin name to install manually." with a free-text install input.install(pluginName)itself remains functional — only browsing is gated.
bin/plugins/stalePlugins.ts:
- Currently hardcodes
https://static.etherpad.org/plugins.full.json. Rewrite to readsettings.updateServerand respectsettings.privacy.pluginCatalog. When disabled, log and exit 0 (dev tool; failing isn't useful).
settings.json.template:
Add the privacy block with comments pointing readers to PRIVACY.md.
3. PRIVACY.md + README link
New PRIVACY.md at the repo root, factual and short:
# Privacy in Etherpad
## What this document is
A complete, current list of every network call Etherpad's own code
makes to a third party, plus how to turn each one off.
## TL;DR
Etherpad ships with two outbound calls to etherpad.org. Both are
documented below. Both can be disabled with a single config value
each. No analytics, no usage pings, no third-party SDKs at runtime.
## Outbound calls
### 1. Version check
- URL: https://static.etherpad.org/info.json (or `updateServer`)
- Frequency: hourly while the server runs
- Payload: GET only; User-Agent header carries "Etherpad/<version>"
- Purpose: surface a "update available" notice in the admin panel
- Disable: set `privacy.updateCheck: false` in settings.json
- Source: src/node/utils/UpdateCheck.ts
### 2. Plugin catalog
- URL: https://static.etherpad.org/plugins.json (or `updateServer`)
- Frequency: on admin-plugins page load (cached 10 min)
- Payload: GET only; same User-Agent
- Purpose: list installable ep_* plugins in the admin UI
- Disable: set `privacy.pluginCatalog: false` in settings.json
(manual install-by-name still works)
- Source: src/static/js/pluginfw/installer.ts
## What we removed
swagger-ui-express was dropped in <PR #> because the upstream package
injects a Scarf analytics pixel that cannot be disabled at build or
runtime. /api-docs is now served by a vendored copy of RapiDoc (MIT)
with no outbound calls.
## What we will not add
- usage analytics or telemetry SDKs
- crash reporters that send data without explicit opt-in
- third-party CDN dependencies at runtime
- dependencies whose install or runtime phones home
## Plugins
Third-party plugins are out of this guarantee. Plugins run in your
Etherpad process with full access; audit any plugin you install.
## Reporting
Found an outbound call this doc doesn't list? Open an issue with the
label `privacy`.
README.md — one line near the top under the existing intro:
Privacy: Etherpad makes two opt-out network calls and ships no third-party telemetry. See PRIVACY.md.
CHANGELOG.md (or release notes file) — single entry under the new
release section:
Privacy: Dropped
swagger-ui-express(third-party telemetry);/api-docsnow served by vendored RapiDoc. Addedprivacy.updateCheckandprivacy.pluginCatalogopt-outs. SeePRIVACY.md.
Testing
Backend (vitest, required per project memory):
Settings.test.ts—privacy.updateCheckandprivacy.pluginCatalogdefault totruewhen not set insettings.json.- New
UpdateCheck.test.ts—check()makes no fetch whenprivacy.updateCheck === false(fetch mocked, assert no call). - New
installer.test.ts—getAvailablePlugins()throws the tagged disabled error whenprivacy.pluginCatalog === false.
Manual smoke (pre-merge, on port 9003):
- Start dev server, open
/api-docs— confirm RapiDoc renders the spec and DevTools Network tab shows zero third-party hosts. - Set
privacy.updateCheck: false, restart — confirm no request tostatic.etherpad.org/info.jsonand admin "update available" line is absent. - Set
privacy.pluginCatalog: false, open admin plugins — confirm the manual install-by-name fallback renders;ep_aligninstalls by name.
Existing e2e: Run admin-page Playwright suites; any test that depended on swagger-ui's specific DOM needs updating to RapiDoc selectors or removing.
Dependency hygiene:
pnpm installclean.grep -ri "swagger" src/ --exclude-dir=node_modulesreturns only the unrelated comment atopenapi.ts:810and the keptSwaggerUIResource.tstype.grep -E "fetch\(|XMLHttpRequest|sendBeacon|scarf|google" src/static/vendor/rapidoc/rapidoc-min.jsreviewed; results documented in PR description.
Rollout
- Branch:
feature/7524-drop-swagger-ui-telemetryoffdevelop. - Single PR closing #7524.
- After push, wait ~20s, run
gh pr checks. Fix CI failures inline before moving on. - Action all Qodo review comments inline.
Rollback
All changes are either additive (the privacy block, both defaults true)
or one-for-one (swagger-ui-express → vendored RapiDoc, same URL surface).
Reverting the merge restores prior behavior cleanly.
Risks
- Operators with proxies fronting
/api-docs— URL unchanged, transparent. - API consumers scraping
/api-docs.json— untouched. - Custom admin pages that depended on swagger-ui's specific DOM — unlikely (core only); will surface in CI.
- RapiDoc upstream eventually adds telemetry — mitigated by vendoring a pinned version and re-grepping on each bump.