Skip to content

Commit 278acb1

Browse files
JohnMcLearclaude
andauthored
Drop swagger-ui, document telemetry, add opt-outs (#7524) (#7757)
* 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>
1 parent 8c4f974 commit 278acb1

25 files changed

Lines changed: 4050 additions & 85 deletions

CHANGELOG.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,14 @@
66

77
### Notable enhancements
88

9+
- **Privacy — drop swagger-ui telemetry, document phone-homes, add opt-outs.**
10+
- Dropped `swagger-ui-express` because upstream injects a Scarf analytics pixel that cannot be disabled at install or runtime (see [swagger-api/swagger-ui#10573](https://github.com/swagger-api/swagger-ui/issues/10573)). `/api-docs` now serves a vendored copy of [Scalar](https://github.com/scalar/scalar) (MIT) configured with `withDefaultFonts: false` and `telemetry: false` so no outbound calls are made.
11+
- New `privacy.updateCheck` (default `true`) — set to `false` to disable the hourly `UpdateCheck.ts` request to `${updateServer}/info.json`.
12+
- New `privacy.pluginCatalog` (default `true`) — set to `false` to disable the admin plugins page fetch of `${updateServer}/plugins.json`. CLI install-by-name still works.
13+
- New [`PRIVACY.md`](PRIVACY.md) at repo root documenting both outbound calls, what they send, and how to turn each off.
14+
- `bin/plugins/stalePlugins.ts` now reads `settings.updateServer` (was hardcoded to `static.etherpad.org`) and honours the new flag.
15+
- Closes #7524.
16+
917
- **Self-update subsystem — Tier 2 (manual click).**
1018
- Admins on a git install can click "Apply update" at `/admin/update`. Etherpad runs a 60s session drain (with T-60 / T-30 / T-10 broadcasts to every pad), `git fetch / checkout / pnpm install --frozen-lockfile / pnpm run build:ui`, and exits with code 75 so a process supervisor restarts it on the new version. The next boot runs a 60s health check; if `/health` doesn't come up the previous SHA + lockfile are restored automatically.
1119
- Crash-loop guard: if the new version reboots more than twice without the health check completing, RollbackHandler forces a rollback regardless of the timer.

PRIVACY.md

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
# Privacy in Etherpad
2+
3+
## What this document is
4+
5+
A complete, current list of every network call Etherpad's own code makes
6+
to a third party, plus how to turn each one off. Plugins are out of
7+
scope — audit any plugin you install.
8+
9+
## TL;DR
10+
11+
Etherpad ships with two outbound calls to `etherpad.org`. Both are
12+
documented below. Both can be disabled with a single config value each.
13+
No analytics, no usage pings, no third-party SDKs at runtime.
14+
15+
## Outbound calls
16+
17+
### 1. Version check
18+
19+
| | |
20+
|---|---|
21+
| URL | `https://static.etherpad.org/info.json` (override via `updateServer`) |
22+
| Frequency | hourly while the server runs |
23+
| Payload | GET only; `User-Agent: Etherpad/<version>` |
24+
| Purpose | surface an "update available" notice in the admin panel |
25+
| Disable | set `privacy.updateCheck: false` in `settings.json` |
26+
| Source | `src/node/utils/UpdateCheck.ts` |
27+
28+
### 2. Plugin catalog
29+
30+
| | |
31+
|---|---|
32+
| URL | `https://static.etherpad.org/plugins.json` (override via `updateServer`) |
33+
| Frequency | on admin-plugins page load (cached 10 min) |
34+
| Payload | GET only; same `User-Agent` |
35+
| Purpose | list installable `ep_*` plugins in the admin UI |
36+
| Disable | set `privacy.pluginCatalog: false` in `settings.json` (manual install via CLI still works) |
37+
| Source | `src/static/js/pluginfw/installer.ts` |
38+
39+
## What we removed
40+
41+
`swagger-ui-express` was dropped because the upstream npm package
42+
injects a Scarf analytics pixel that cannot be disabled at install or
43+
runtime (see [swagger-api/swagger-ui#10573](https://github.com/swagger-api/swagger-ui/issues/10573)).
44+
`/api-docs` is now served by a vendored copy of [Scalar](https://github.com/scalar/scalar)
45+
(MIT) with no outbound calls. The shell explicitly opts out of Scalar's
46+
default font fetch (`withDefaultFonts: false`) and analytics
47+
(`telemetry: false`), and pins a system-font stack via CSS.
48+
49+
`@scarf/scarf` is listed under `ignoredBuiltDependencies` in
50+
`pnpm-workspace.yaml`, so its postinstall pixel is suppressed even if a
51+
future transitive dep pulls Scarf in.
52+
53+
## What we will not add
54+
55+
- usage analytics or telemetry SDKs
56+
- crash reporters that send data without explicit opt-in
57+
- third-party CDN dependencies at runtime
58+
- dependencies whose install or runtime phones home
59+
60+
## Plugins
61+
62+
Third-party plugins are out of this guarantee. Plugins run in your
63+
Etherpad process with full access; audit any plugin you install.
64+
65+
## Reporting
66+
67+
Found an outbound call this doc doesn't list? Open an issue with the
68+
label `privacy`.

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010

1111
Every keystroke is attributed to its author. Every revision is preserved. The timeslider lets you scrub through a document's entire history, character by character. Author colours make collaboration visible at a glance — not buried in a menu.
1212

13-
Etherpad runs on your server, under your governance. No telemetry. No upsells. AI is a plugin you install, pointed at the model you choose, running on infrastructure you control — not a feature decided for you in a boardroom you weren't in.
13+
Etherpad runs on your server, under your governance. No telemetry. No upsells. AI is a plugin you install, pointed at the model you choose, running on infrastructure you control — not a feature decided for you in a boardroom you weren't in. See [PRIVACY.md](PRIVACY.md) for the two opt-out network calls Etherpad's own code makes and how to disable each.
1414

1515
The code is Apache 2.0. The data format is open. It [scales to thousands of simultaneous editors per pad](http://scale.etherpad.org/). Translated into 105 languages. Extended through hundreds of plugins. Used by Wikimedia, governments, public-sector institutions, and self-hosters worldwide since 2009.
1616

admin/src/index.css

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1044,6 +1044,16 @@ input, button, select, optgroup, textarea {
10441044
padding: 8px 8px 40px;
10451045
}
10461046

1047+
.pm-banner {
1048+
margin: 8px 0 16px;
1049+
padding: 12px 16px;
1050+
border-radius: 6px;
1051+
border: 1px solid var(--ink-3, #cbd5e1);
1052+
background: var(--surface-2, #f8fafc);
1053+
font-size: .9rem;
1054+
}
1055+
.pm-banner-info { border-left: 4px solid var(--accent, #0ea5e9); }
1056+
10471057
/* Header */
10481058
.pm-header {
10491059
display: flex;

admin/src/pages/HomePage.tsx

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import {IconButton} from "../components/IconButton.tsx";
99
export const HomePage = () => {
1010
const pluginsSocket = useStore(state => state.pluginsSocket)
1111
const [plugins, setPlugins] = useState<PluginDef[]>([])
12+
const [catalogDisabled, setCatalogDisabled] = useState(false)
1213
const installedPlugins = useStore(state => state.installedPlugins)
1314
const setInstalledPlugins = useStore(state => state.setInstalledPlugins)
1415
// Default sort: name ascending. PR #7716 set this to "downloads desc" but
@@ -89,11 +90,14 @@ export const HomePage = () => {
8990
pluginsSocket.emit('search', searchParams)
9091
}
9192

93+
const onCatalogDisabled = () => setCatalogDisabled(true)
94+
9295
pluginsSocket.on('results:installed', onInstalled)
9396
pluginsSocket.on('results:updatable', onUpdatable)
9497
pluginsSocket.on('finished:install', onFinishedInstall)
9598
pluginsSocket.on('finished:uninstall', onFinishedUninstall)
9699
pluginsSocket.on('connect', onConnect)
100+
pluginsSocket.on('results:catalogDisabled', onCatalogDisabled)
97101

98102
pluginsSocket.emit('getInstalled')
99103

@@ -105,6 +109,7 @@ export const HomePage = () => {
105109
pluginsSocket.off('finished:install', onFinishedInstall)
106110
pluginsSocket.off('finished:uninstall', onFinishedUninstall)
107111
pluginsSocket.off('connect', onConnect)
112+
pluginsSocket.off('results:catalogDisabled', onCatalogDisabled)
108113
}
109114
}, [pluginsSocket])
110115

@@ -145,6 +150,12 @@ export const HomePage = () => {
145150
return (
146151
<div className="pm-page">
147152

153+
{catalogDisabled && (
154+
<div className="pm-banner pm-banner-info" role="status">
155+
<Trans i18nKey="admin_plugins.catalog_disabled"/>
156+
</div>
157+
)}
158+
148159
{/* ── Page header ────────────────────────────────────────────────── */}
149160
<div className="pm-header">
150161
<div>

bin/plugins/stalePlugins.ts

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,17 @@
33
// Returns a list of stale plugins and their authors email
44

55
import process from "node:process";
6+
import settings from "../../src/node/utils/Settings";
67
const currentTime = new Date();
78

89
(async () => {
9-
const resp = await fetch('https://static.etherpad.org/plugins.full.json');
10+
if (!settings.privacy.pluginCatalog) {
11+
console.info(
12+
'stalePlugins: plugin catalog disabled by privacy.pluginCatalog=false; exiting'
13+
);
14+
process.exit(0);
15+
}
16+
const resp = await fetch(`${settings.updateServer}/plugins.full.json`);
1017
if (!resp.ok) throw new Error(`HTTP ${resp.status} ${resp.statusText}`);
1118
const data: any = await resp.json();
1219
for (const plugin of Object.keys(data)) {

0 commit comments

Comments
 (0)