Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,14 @@

### Notable enhancements

- **Privacy — drop swagger-ui telemetry, document phone-homes, add opt-outs.**
- 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.
- New `privacy.updateCheck` (default `true`) — set to `false` to disable the hourly `UpdateCheck.ts` request to `${updateServer}/info.json`.
- 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.
- New [`PRIVACY.md`](PRIVACY.md) at repo root documenting both outbound calls, what they send, and how to turn each off.
- `bin/plugins/stalePlugins.ts` now reads `settings.updateServer` (was hardcoded to `static.etherpad.org`) and honours the new flag.
- Closes #7524.

- **Self-update subsystem — Tier 2 (manual click).**
- 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.
- Crash-loop guard: if the new version reboots more than twice without the health check completing, RollbackHandler forces a rollback regardless of the timer.
Expand Down
68 changes: 68 additions & 0 deletions PRIVACY.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
# 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. Plugins are out of
scope — audit any plugin you install.

## 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` (override via `updateServer`) |
| Frequency | hourly while the server runs |
| Payload | GET only; `User-Agent: Etherpad/<version>` |
| Purpose | surface an "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` (override via `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 via CLI still works) |
| Source | `src/static/js/pluginfw/installer.ts` |

## What we removed

`swagger-ui-express` was dropped because the upstream npm package
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` is now served by a vendored copy of [Scalar](https://github.com/scalar/scalar)
(MIT) with no outbound calls. The shell explicitly opts out of Scalar's
default font fetch (`withDefaultFonts: false`) and analytics
(`telemetry: false`), and pins a system-font stack via CSS.

`@scarf/scarf` is listed under `ignoredBuiltDependencies` in
`pnpm-workspace.yaml`, so its postinstall pixel is suppressed even if a
future transitive dep pulls Scarf in.

## 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`.
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@

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.

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.
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.

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.

Expand Down
10 changes: 10 additions & 0 deletions admin/src/index.css
Original file line number Diff line number Diff line change
Expand Up @@ -1044,6 +1044,16 @@ input, button, select, optgroup, textarea {
padding: 8px 8px 40px;
}

.pm-banner {
margin: 8px 0 16px;
padding: 12px 16px;
border-radius: 6px;
border: 1px solid var(--ink-3, #cbd5e1);
background: var(--surface-2, #f8fafc);
font-size: .9rem;
}
.pm-banner-info { border-left: 4px solid var(--accent, #0ea5e9); }

/* Header */
.pm-header {
display: flex;
Expand Down
11 changes: 11 additions & 0 deletions admin/src/pages/HomePage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import {IconButton} from "../components/IconButton.tsx";
export const HomePage = () => {
const pluginsSocket = useStore(state => state.pluginsSocket)
const [plugins, setPlugins] = useState<PluginDef[]>([])
const [catalogDisabled, setCatalogDisabled] = useState(false)
const installedPlugins = useStore(state => state.installedPlugins)
const setInstalledPlugins = useStore(state => state.setInstalledPlugins)
// Default sort: name ascending. PR #7716 set this to "downloads desc" but
Expand Down Expand Up @@ -89,11 +90,14 @@ export const HomePage = () => {
pluginsSocket.emit('search', searchParams)
}

const onCatalogDisabled = () => setCatalogDisabled(true)

pluginsSocket.on('results:installed', onInstalled)
pluginsSocket.on('results:updatable', onUpdatable)
pluginsSocket.on('finished:install', onFinishedInstall)
pluginsSocket.on('finished:uninstall', onFinishedUninstall)
pluginsSocket.on('connect', onConnect)
pluginsSocket.on('results:catalogDisabled', onCatalogDisabled)

pluginsSocket.emit('getInstalled')

Expand All @@ -105,6 +109,7 @@ export const HomePage = () => {
pluginsSocket.off('finished:install', onFinishedInstall)
pluginsSocket.off('finished:uninstall', onFinishedUninstall)
pluginsSocket.off('connect', onConnect)
pluginsSocket.off('results:catalogDisabled', onCatalogDisabled)
}
}, [pluginsSocket])

Expand Down Expand Up @@ -145,6 +150,12 @@ export const HomePage = () => {
return (
<div className="pm-page">

{catalogDisabled && (
<div className="pm-banner pm-banner-info" role="status">
<Trans i18nKey="admin_plugins.catalog_disabled"/>
</div>
)}

{/* ── Page header ────────────────────────────────────────────────── */}
<div className="pm-header">
<div>
Expand Down
9 changes: 8 additions & 1 deletion bin/plugins/stalePlugins.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,17 @@
// Returns a list of stale plugins and their authors email

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

(async () => {
const resp = await fetch('https://static.etherpad.org/plugins.full.json');
if (!settings.privacy.pluginCatalog) {
console.info(
'stalePlugins: plugin catalog disabled by privacy.pluginCatalog=false; exiting'
);
process.exit(0);
}
const resp = await fetch(`${settings.updateServer}/plugins.full.json`);
if (!resp.ok) throw new Error(`HTTP ${resp.status} ${resp.statusText}`);
const data: any = await resp.json();
for (const plugin of Object.keys(data)) {
Expand Down
Loading
Loading