mirror of
https://github.com/ether/etherpad-lite.git
synced 2026-07-17 16:47:05 +00:00
* 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>
This commit is contained in:
parent
8c4f974122
commit
278acb10cb
25 changed files with 4050 additions and 85 deletions
|
|
@ -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.
|
||||
|
|
|
|||
68
PRIVACY.md
Normal file
68
PRIVACY.md
Normal file
|
|
@ -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`.
|
||||
|
|
@ -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.
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -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')
|
||||
|
||||
|
|
@ -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])
|
||||
|
||||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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)) {
|
||||
|
|
|
|||
1131
docs/superpowers/plans/2026-05-15-issue-7524-swagger-ui-telemetry.md
Normal file
1131
docs/superpowers/plans/2026-05-15-issue-7524-swagger-ui-telemetry.md
Normal file
File diff suppressed because it is too large
Load diff
|
|
@ -0,0 +1,246 @@
|
|||
# Issue #7524 — Drop swagger-ui, take a telemetry stance, opt out our phone-homes
|
||||
|
||||
**Status:** approved 2026-05-15
|
||||
**Closes:** [ether/etherpad#7524](https://github.com/ether/etherpad/issues/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](https://github.com/swagger-api/swagger-ui/issues/10573)).
|
||||
|
||||
Etherpad itself makes two outbound calls:
|
||||
|
||||
1. `src/node/utils/UpdateCheck.ts` — hourly `GET ${updateServer}/info.json`
|
||||
for the admin "update available" notice. No opt-out today.
|
||||
2. `src/static/js/pluginfw/installer.ts:179` — `GET ${updateServer}/plugins.json`
|
||||
on 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.org` itself 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.1` from `src/package.json` dependencies.
|
||||
- `@types/swagger-ui-express ^4.1.8` from `src/package.json` devDependencies.
|
||||
- `import {serve, setup} from 'swagger-ui-express'` at `src/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 from
|
||||
`https://unpkg.com/rapidoc@9.3.x/dist/rapidoc-min.js` (MIT, ~370KB),
|
||||
committed as a static asset. Pinned exact version recorded in
|
||||
`src/static/vendor/rapidoc/VERSION`. No CDN fetch at runtime.
|
||||
- `src/static/api-docs.html` — minimal HTML shell:
|
||||
```html
|
||||
<!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.html` and the
|
||||
static asset under `/static/vendor/rapidoc/`. The simplest path is to
|
||||
drop the file under `src/static/` and let the existing static-file
|
||||
middleware pick it up; if that needs an explicit route, add it next to
|
||||
the `/api-docs.json` handler in `RestAPI.ts`.
|
||||
|
||||
**Kept (unchanged):**
|
||||
- `/api-docs.json` route at `RestAPI.ts:1449-1453`.
|
||||
- `src/node/types/SwaggerUIResource.ts` (TypeScript type used by `openapi.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`:
|
||||
|
||||
```ts
|
||||
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 when `settings.privacy.updateCheck === false`,
|
||||
logging once: `Update check disabled by privacy.updateCheck=false`.
|
||||
No fetch, no scheduled retry.
|
||||
- `getLatestVersion()` — returns `undefined` when disabled. The existing
|
||||
caller at `src/node/hooks/express/adminsettings.ts:105` already tolerates
|
||||
undefined; the admin panel simply omits the "update available" line.
|
||||
|
||||
**`src/static/js/pluginfw/installer.ts`:**
|
||||
- `getAvailablePlugins()` — early-throw when
|
||||
`settings.privacy.pluginCatalog === false` with a tagged error:
|
||||
`Error('Plugin catalog disabled (privacy.pluginCatalog=false)')`.
|
||||
- Admin consumer at `src/node/hooks/express/adminplugins.ts` catches 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 read `settings.updateServer` and respect
|
||||
`settings.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](PRIVACY.md).
|
||||
|
||||
**`CHANGELOG.md`** (or release notes file) — single entry under the new
|
||||
release section:
|
||||
|
||||
> **Privacy:** Dropped `swagger-ui-express` (third-party telemetry);
|
||||
> `/api-docs` now served by vendored RapiDoc. Added `privacy.updateCheck`
|
||||
> and `privacy.pluginCatalog` opt-outs. See `PRIVACY.md`.
|
||||
|
||||
## Testing
|
||||
|
||||
**Backend (vitest, required per project memory):**
|
||||
- `Settings.test.ts` — `privacy.updateCheck` and `privacy.pluginCatalog`
|
||||
default to `true` when not set in `settings.json`.
|
||||
- New `UpdateCheck.test.ts` — `check()` makes no fetch when
|
||||
`privacy.updateCheck === false` (fetch mocked, assert no call).
|
||||
- New `installer.test.ts` — `getAvailablePlugins()` throws the tagged
|
||||
disabled error when `privacy.pluginCatalog === false`.
|
||||
|
||||
**Manual smoke (pre-merge, on port 9003):**
|
||||
1. Start dev server, open `/api-docs` — confirm RapiDoc renders the spec
|
||||
and DevTools Network tab shows zero third-party hosts.
|
||||
2. Set `privacy.updateCheck: false`, restart — confirm no request to
|
||||
`static.etherpad.org/info.json` and admin "update available" line is
|
||||
absent.
|
||||
3. Set `privacy.pluginCatalog: false`, open admin plugins — confirm the
|
||||
manual install-by-name fallback renders; `ep_align` installs 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 install` clean.
|
||||
- `grep -ri "swagger" src/ --exclude-dir=node_modules` returns only the
|
||||
unrelated comment at `openapi.ts:810` and the kept `SwaggerUIResource.ts`
|
||||
type.
|
||||
- `grep -E "fetch\(|XMLHttpRequest|sendBeacon|scarf|google" src/static/vendor/rapidoc/rapidoc-min.js`
|
||||
reviewed; results documented in PR description.
|
||||
|
||||
## Rollout
|
||||
|
||||
- Branch: `feature/7524-drop-swagger-ui-telemetry` off `develop`.
|
||||
- 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.
|
||||
75
pnpm-lock.yaml
generated
75
pnpm-lock.yaml
generated
|
|
@ -343,9 +343,6 @@ importers:
|
|||
surrealdb:
|
||||
specifier: ^2.0.3
|
||||
version: 2.0.3(tslib@2.8.1)(typescript@6.0.3)
|
||||
swagger-ui-express:
|
||||
specifier: ^5.0.1
|
||||
version: 5.0.1(express@5.2.1)
|
||||
tinycon:
|
||||
specifier: 0.6.8
|
||||
version: 0.6.8
|
||||
|
|
@ -434,9 +431,6 @@ importers:
|
|||
'@types/supertest':
|
||||
specifier: ^7.2.0
|
||||
version: 7.2.0
|
||||
'@types/swagger-ui-express':
|
||||
specifier: ^4.1.8
|
||||
version: 4.1.8
|
||||
'@types/underscore':
|
||||
specifier: ^1.13.0
|
||||
version: 1.13.0
|
||||
|
|
@ -1757,9 +1751,6 @@ packages:
|
|||
'@rushstack/eslint-patch@1.16.1':
|
||||
resolution: {integrity: sha512-TvZbIpeKqGQQ7X0zSCvPH9riMSFQFSggnfBjFZ1mEoILW+UuXCKwOoPcgjMwiUtRqFZ8jWhPJc4um14vC6I4ag==}
|
||||
|
||||
'@scarf/scarf@1.4.0':
|
||||
resolution: {integrity: sha512-xxeapPiUXdZAE3che6f3xogoJPeZgig6omHEy1rIY5WVsB3H2BHNnZH+gHG6x91SCWyQCzWGsuL2Hh3ClO5/qQ==}
|
||||
|
||||
'@shikijs/core@3.23.0':
|
||||
resolution: {integrity: sha512-NSWQz0riNb67xthdm5br6lAkvpDJRTgB36fxlo37ZzM2yq0PQFFzbd8psqC2XMPgCzo1fW6cVi18+ArJ44wqgA==}
|
||||
|
||||
|
|
@ -1969,9 +1960,6 @@ packages:
|
|||
'@types/mime-types@3.0.1':
|
||||
resolution: {integrity: sha512-xRMsfuQbnRq1Ef+C+RKaENOxXX87Ygl38W1vDfPHRku02TgQr+Qd8iivLtAMcR0KF5/29xlnFihkTlbqFrGOVQ==}
|
||||
|
||||
'@types/mime@1.3.5':
|
||||
resolution: {integrity: sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w==}
|
||||
|
||||
'@types/mocha@10.0.10':
|
||||
resolution: {integrity: sha512-xPyYSz1cMPnJQhl0CLMH68j3gprKZaTjG3s5Vi+fDgx+uhG9NOXwbVt52eFS8ECyXhyKcjDLCBEqBExKuiZb7Q==}
|
||||
|
||||
|
|
@ -2019,15 +2007,9 @@ packages:
|
|||
'@types/semver@7.7.1':
|
||||
resolution: {integrity: sha512-FmgJfu+MOcQ370SD0ev7EI8TlCAfKYU+B4m5T3yXc1CiRN94g/SZPtsCkk506aUDtlMnFZvasDwHHUcZUEaYuA==}
|
||||
|
||||
'@types/send@0.17.4':
|
||||
resolution: {integrity: sha512-x2EM6TJOybec7c52BX0ZspPodMsQUd5L6PRwOunVyVUhXiBSKf3AezDL8Dgvgt5o0UfKNfuA0eMLr2wLT4AiBA==}
|
||||
|
||||
'@types/send@1.2.1':
|
||||
resolution: {integrity: sha512-arsCikDvlU99zl1g69TcAB3mzZPpxgw0UQnaHeC1Nwb015xp8bknZv5rIfri9xTOcMuaVgvabfIRA7PSZVuZIQ==}
|
||||
|
||||
'@types/serve-static@1.15.7':
|
||||
resolution: {integrity: sha512-W8Ym+h8nhuRwaKPaDw34QUkwsGi6Rc4yYqvKFo5rm2FUEhCFbzVWrxXUxuKK8TASjWsysJY0nsmNCGhCOIsrOw==}
|
||||
|
||||
'@types/serve-static@2.2.0':
|
||||
resolution: {integrity: sha512-8mam4H1NHLtu7nmtalF7eyBH14QyOASmcxHhSfEoRyr0nP/YdoesEtU+uSRvMe96TW/HPTtkoKqQLl53N7UXMQ==}
|
||||
|
||||
|
|
@ -2043,9 +2025,6 @@ packages:
|
|||
'@types/supertest@7.2.0':
|
||||
resolution: {integrity: sha512-uh2Lv57xvggst6lCqNdFAmDSvoMG7M/HDtX4iUCquxQ5EGPtaPM5PL5Hmi7LCvOG8db7YaCPNJEeoI8s/WzIQw==}
|
||||
|
||||
'@types/swagger-ui-express@4.1.8':
|
||||
resolution: {integrity: sha512-AhZV8/EIreHFmBV5wAs0gzJUNq9JbbSXgJLQubCC0jtIo6prnI9MIRRxnU4MZX9RB9yXxF1V4R7jtLl/Wcj31g==}
|
||||
|
||||
'@types/tar@6.1.13':
|
||||
resolution: {integrity: sha512-IznnlmU5f4WcGTh2ltRu/Ijpmk8wiWXfF0VA4s+HPjHZgvFggk1YaIkbo5krX/zUCzWF8N/l4+W/LNxnvAJ8nw==}
|
||||
|
||||
|
|
@ -5438,15 +5417,6 @@ packages:
|
|||
swagger-schema-official@2.0.0-bab6bed:
|
||||
resolution: {integrity: sha512-rCC0NWGKr/IJhtRuPq/t37qvZHI/mH4I4sxflVM+qgVe5Z2uOCivzWaVbuioJaB61kvm5UvB7b49E+oBY0M8jA==}
|
||||
|
||||
swagger-ui-dist@5.20.6:
|
||||
resolution: {integrity: sha512-q/1dwcCOQb+qsNkb+1VWRdGEEVdBtOTH4vv9rICjPwJXOwq/JSRkBbuEMjMe161Oxsp589+8Ff5nE4HTPLWIAw==}
|
||||
|
||||
swagger-ui-express@5.0.1:
|
||||
resolution: {integrity: sha512-SrNU3RiBGTLLmFU8GIJdOdanJTl4TOmT27tt3bWWHppqYmAZ6IDuEuBvMU6nZq0zLEe6b/1rACXCgLZqO6ZfrA==}
|
||||
engines: {node: '>= v0.10.32'}
|
||||
peerDependencies:
|
||||
express: '>=4.0.0 || >=5.0.0-beta'
|
||||
|
||||
symbol-tree@3.2.4:
|
||||
resolution: {integrity: sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==}
|
||||
|
||||
|
|
@ -7204,8 +7174,6 @@ snapshots:
|
|||
|
||||
'@rushstack/eslint-patch@1.16.1': {}
|
||||
|
||||
'@scarf/scarf@1.4.0': {}
|
||||
|
||||
'@shikijs/core@3.23.0':
|
||||
dependencies:
|
||||
'@shikijs/types': 3.23.0
|
||||
|
|
@ -7447,8 +7415,6 @@ snapshots:
|
|||
|
||||
'@types/mime-types@3.0.1': {}
|
||||
|
||||
'@types/mime@1.3.5': {}
|
||||
|
||||
'@types/mocha@10.0.10': {}
|
||||
|
||||
'@types/ms@2.1.0': {}
|
||||
|
|
@ -7502,21 +7468,10 @@ snapshots:
|
|||
|
||||
'@types/semver@7.7.1': {}
|
||||
|
||||
'@types/send@0.17.4':
|
||||
dependencies:
|
||||
'@types/mime': 1.3.5
|
||||
'@types/node': 25.7.0
|
||||
|
||||
'@types/send@1.2.1':
|
||||
dependencies:
|
||||
'@types/node': 25.7.0
|
||||
|
||||
'@types/serve-static@1.15.7':
|
||||
dependencies:
|
||||
'@types/http-errors': 2.0.5
|
||||
'@types/node': 25.7.0
|
||||
'@types/send': 0.17.4
|
||||
|
||||
'@types/serve-static@2.2.0':
|
||||
dependencies:
|
||||
'@types/http-errors': 2.0.5
|
||||
|
|
@ -7540,11 +7495,6 @@ snapshots:
|
|||
'@types/methods': 1.1.4
|
||||
'@types/superagent': 8.1.9
|
||||
|
||||
'@types/swagger-ui-express@4.1.8':
|
||||
dependencies:
|
||||
'@types/express': 5.0.6
|
||||
'@types/serve-static': 1.15.7
|
||||
|
||||
'@types/tar@6.1.13':
|
||||
dependencies:
|
||||
'@types/node': 25.7.0
|
||||
|
|
@ -8769,10 +8719,10 @@ snapshots:
|
|||
'@rushstack/eslint-patch': 1.16.1
|
||||
'@typescript-eslint/eslint-plugin': 7.18.0(@typescript-eslint/parser@7.18.0(eslint@10.4.0)(typescript@6.0.3))(eslint@10.4.0)(typescript@6.0.3)
|
||||
'@typescript-eslint/parser': 7.18.0(eslint@10.4.0)(typescript@6.0.3)
|
||||
eslint-import-resolver-typescript: 3.9.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@7.18.0(eslint@10.4.0)(typescript@6.0.3))(eslint@10.4.0))(eslint@10.4.0)
|
||||
eslint-import-resolver-typescript: 3.9.1(eslint-plugin-import@2.32.0)(eslint@10.4.0)
|
||||
eslint-plugin-cypress: 2.15.2(eslint@10.4.0)
|
||||
eslint-plugin-eslint-comments: 3.2.0(eslint@10.4.0)
|
||||
eslint-plugin-import: 2.32.0(@typescript-eslint/parser@7.18.0(eslint@10.4.0)(typescript@6.0.3))(eslint-import-resolver-typescript@3.9.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@7.18.0(eslint@10.4.0)(typescript@6.0.3))(eslint@10.4.0))(eslint@10.4.0))(eslint@10.4.0)
|
||||
eslint-plugin-import: 2.32.0(@typescript-eslint/parser@7.18.0(eslint@10.4.0)(typescript@6.0.3))(eslint-import-resolver-typescript@3.9.1)(eslint@10.4.0)
|
||||
eslint-plugin-mocha: 10.5.0(eslint@10.4.0)
|
||||
eslint-plugin-n: 17.24.0(eslint@10.4.0)(typescript@6.0.3)
|
||||
eslint-plugin-prefer-arrow: 1.2.3(eslint@10.4.0)
|
||||
|
|
@ -8793,7 +8743,7 @@ snapshots:
|
|||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
eslint-import-resolver-typescript@3.9.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@7.18.0(eslint@10.4.0)(typescript@6.0.3))(eslint@10.4.0))(eslint@10.4.0):
|
||||
eslint-import-resolver-typescript@3.9.1(eslint-plugin-import@2.32.0)(eslint@10.4.0):
|
||||
dependencies:
|
||||
'@nolyfill/is-core-module': 1.0.39
|
||||
debug: 4.4.3(supports-color@8.1.1)
|
||||
|
|
@ -8804,18 +8754,18 @@ snapshots:
|
|||
stable-hash: 0.0.5
|
||||
tinyglobby: 0.2.16
|
||||
optionalDependencies:
|
||||
eslint-plugin-import: 2.32.0(@typescript-eslint/parser@7.18.0(eslint@10.4.0)(typescript@6.0.3))(eslint-import-resolver-typescript@3.9.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@7.18.0(eslint@10.4.0)(typescript@6.0.3))(eslint@10.4.0))(eslint@10.4.0))(eslint@10.4.0)
|
||||
eslint-plugin-import: 2.32.0(@typescript-eslint/parser@7.18.0(eslint@10.4.0)(typescript@6.0.3))(eslint-import-resolver-typescript@3.9.1)(eslint@10.4.0)
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
eslint-module-utils@2.12.1(@typescript-eslint/parser@7.18.0(eslint@10.4.0)(typescript@6.0.3))(eslint-import-resolver-node@0.3.10)(eslint-import-resolver-typescript@3.9.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@7.18.0(eslint@10.4.0)(typescript@6.0.3))(eslint@10.4.0))(eslint@10.4.0))(eslint@10.4.0):
|
||||
eslint-module-utils@2.12.1(@typescript-eslint/parser@7.18.0(eslint@10.4.0)(typescript@6.0.3))(eslint-import-resolver-node@0.3.10)(eslint-import-resolver-typescript@3.9.1)(eslint@10.4.0):
|
||||
dependencies:
|
||||
debug: 3.2.7
|
||||
optionalDependencies:
|
||||
'@typescript-eslint/parser': 7.18.0(eslint@10.4.0)(typescript@6.0.3)
|
||||
eslint: 10.4.0
|
||||
eslint-import-resolver-node: 0.3.10
|
||||
eslint-import-resolver-typescript: 3.9.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@7.18.0(eslint@10.4.0)(typescript@6.0.3))(eslint@10.4.0))(eslint@10.4.0)
|
||||
eslint-import-resolver-typescript: 3.9.1(eslint-plugin-import@2.32.0)(eslint@10.4.0)
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
|
|
@ -8837,7 +8787,7 @@ snapshots:
|
|||
eslint: 10.4.0
|
||||
ignore: 5.3.2
|
||||
|
||||
eslint-plugin-import@2.32.0(@typescript-eslint/parser@7.18.0(eslint@10.4.0)(typescript@6.0.3))(eslint-import-resolver-typescript@3.9.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@7.18.0(eslint@10.4.0)(typescript@6.0.3))(eslint@10.4.0))(eslint@10.4.0))(eslint@10.4.0):
|
||||
eslint-plugin-import@2.32.0(@typescript-eslint/parser@7.18.0(eslint@10.4.0)(typescript@6.0.3))(eslint-import-resolver-typescript@3.9.1)(eslint@10.4.0):
|
||||
dependencies:
|
||||
'@rtsao/scc': 1.1.0
|
||||
array-includes: 3.1.9
|
||||
|
|
@ -8848,7 +8798,7 @@ snapshots:
|
|||
doctrine: 2.1.0
|
||||
eslint: 10.4.0
|
||||
eslint-import-resolver-node: 0.3.10
|
||||
eslint-module-utils: 2.12.1(@typescript-eslint/parser@7.18.0(eslint@10.4.0)(typescript@6.0.3))(eslint-import-resolver-node@0.3.10)(eslint-import-resolver-typescript@3.9.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@7.18.0(eslint@10.4.0)(typescript@6.0.3))(eslint@10.4.0))(eslint@10.4.0))(eslint@10.4.0)
|
||||
eslint-module-utils: 2.12.1(@typescript-eslint/parser@7.18.0(eslint@10.4.0)(typescript@6.0.3))(eslint-import-resolver-node@0.3.10)(eslint-import-resolver-typescript@3.9.1)(eslint@10.4.0)
|
||||
hasown: 2.0.2
|
||||
is-core-module: 2.16.1
|
||||
is-glob: 4.0.3
|
||||
|
|
@ -11366,15 +11316,6 @@ snapshots:
|
|||
|
||||
swagger-schema-official@2.0.0-bab6bed: {}
|
||||
|
||||
swagger-ui-dist@5.20.6:
|
||||
dependencies:
|
||||
'@scarf/scarf': 1.4.0
|
||||
|
||||
swagger-ui-express@5.0.1(express@5.2.1):
|
||||
dependencies:
|
||||
express: 5.2.1
|
||||
swagger-ui-dist: 5.20.6
|
||||
|
||||
symbol-tree@3.2.4: {}
|
||||
|
||||
tabbable@6.4.0: {}
|
||||
|
|
|
|||
|
|
@ -417,6 +417,17 @@
|
|||
|
||||
"updateServer": "https://etherpad.org/ep_infos",
|
||||
|
||||
/*
|
||||
* Outbound network calls. See PRIVACY.md for what each one sends.
|
||||
* - updateCheck=false : disables hourly version check (UpdateCheck.ts)
|
||||
* - pluginCatalog=false: disables admin plugin browser
|
||||
* (manual install-by-name via CLI still works)
|
||||
*/
|
||||
"privacy": {
|
||||
"updateCheck": true,
|
||||
"pluginCatalog": true
|
||||
},
|
||||
|
||||
/*
|
||||
* Should we suppress errors from being visible in the default Pad Text?
|
||||
*/
|
||||
|
|
|
|||
|
|
@ -61,6 +61,7 @@
|
|||
"admin_plugins.available_search.placeholder": "Search for plugins to install",
|
||||
"admin_plugins.check_updates": "Check for updates",
|
||||
"admin_plugins.core_count": "{{count}} core",
|
||||
"admin_plugins.catalog_disabled": "Plugin catalog is disabled by your operator (privacy.pluginCatalog=false). To install a plugin, run `pnpm run plugins i ep_<name>` from the server.",
|
||||
"admin_plugins.crumbs": "Plugins",
|
||||
"admin_plugins.description": "Description",
|
||||
"admin_plugins.disables.label": "Disables:",
|
||||
|
|
|
|||
|
|
@ -5,8 +5,8 @@ import {ErrorCaused} from "../types/ErrorCaused";
|
|||
import createHTTPError from "http-errors";
|
||||
|
||||
const apiHandler = require('./APIHandler')
|
||||
import {serve, setup} from 'swagger-ui-express'
|
||||
import express from "express";
|
||||
import path from "path";
|
||||
|
||||
import settings from '../utils/Settings';
|
||||
|
||||
|
|
@ -1437,12 +1437,9 @@ export const expressCreateServer = async (hookName: string, {app}: ArgsExpressTy
|
|||
}
|
||||
|
||||
|
||||
app.use('/api-docs', serve);
|
||||
app.get('/api-docs', setup(undefined, {
|
||||
swaggerOptions: {
|
||||
url: '/api-docs.json',
|
||||
},
|
||||
}));
|
||||
app.get('/api-docs', (_req, res) => {
|
||||
res.sendFile(path.join(settings.root, 'src', 'static', 'api-docs.html'));
|
||||
});
|
||||
|
||||
app.use(express.json());
|
||||
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ import {PackageData, PackageInfo} from "../../types/PackageInfo";
|
|||
import semver from 'semver';
|
||||
import log4js from 'log4js';
|
||||
import {MapArrayType} from "../../types/MapType";
|
||||
import settings from "../../utils/Settings";
|
||||
|
||||
const pluginDefs = require('../../../static/js/pluginfw/plugin_defs');
|
||||
const logger = log4js.getLogger('adminPlugins');
|
||||
|
|
@ -49,31 +50,43 @@ exports.socketio = (hookName:string, args:ArgsExpressType, cb:Function) => {
|
|||
const installed =
|
||||
Object.keys(pluginDefs.plugins).map((plugin) => pluginDefs.plugins[plugin].package);
|
||||
|
||||
const updatable = await checkPluginForUpdates();
|
||||
|
||||
installed.forEach((plugin) => {
|
||||
plugin.updatable = updatable.includes(plugin.name);
|
||||
})
|
||||
if (settings.privacy.pluginCatalog) {
|
||||
const updatable = await checkPluginForUpdates();
|
||||
installed.forEach((plugin) => {
|
||||
plugin.updatable = updatable.includes(plugin.name);
|
||||
})
|
||||
}
|
||||
// When the catalog is disabled, `updatable` simply stays unset on
|
||||
// each installed plugin — the admin UI renders no "update available"
|
||||
// badge, which is correct.
|
||||
|
||||
socket.emit('results:installed', {installed});
|
||||
});
|
||||
|
||||
|
||||
socket.on('checkUpdates', async () => {
|
||||
if (!settings.privacy.pluginCatalog) {
|
||||
socket.emit('results:catalogDisabled');
|
||||
return;
|
||||
}
|
||||
// Check plugins for updates
|
||||
try {
|
||||
const updatable = checkPluginForUpdates();
|
||||
const updatable = await checkPluginForUpdates();
|
||||
|
||||
socket.emit('results:updatable', {updatable});
|
||||
} catch (err) {
|
||||
const errc = err as ErrorCaused
|
||||
console.warn(errc.stack || errc.toString());
|
||||
|
||||
socket.emit('results:updatable', {updatable: {}});
|
||||
socket.emit('results:updatable', {updatable: []});
|
||||
}
|
||||
});
|
||||
|
||||
socket.on('getAvailable', async (query:string) => {
|
||||
if (!settings.privacy.pluginCatalog) {
|
||||
socket.emit('results:catalogDisabled');
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const results = await getAvailablePlugins(/* maxCacheAge:*/ false);
|
||||
socket.emit('results:available', results);
|
||||
|
|
@ -84,6 +97,10 @@ exports.socketio = (hookName:string, args:ArgsExpressType, cb:Function) => {
|
|||
});
|
||||
|
||||
socket.on('search', async (query: QueryType) => {
|
||||
if (!settings.privacy.pluginCatalog) {
|
||||
socket.emit('results:catalogDisabled');
|
||||
return;
|
||||
}
|
||||
try {
|
||||
if (query.searchTerm) logger.info(`Plugin search: ${query.searchTerm}'`);
|
||||
const results = await search(query.searchTerm, /* maxCacheAge:*/ 60 * 10);
|
||||
|
|
|
|||
|
|
@ -193,6 +193,10 @@ export type SettingsType = {
|
|||
learnMoreUrl: string | null,
|
||||
dismissal: 'dismissible' | 'sticky',
|
||||
},
|
||||
privacy: {
|
||||
updateCheck: boolean,
|
||||
pluginCatalog: boolean,
|
||||
},
|
||||
skinName: string | null,
|
||||
skinVariants: string,
|
||||
ip: string,
|
||||
|
|
@ -425,6 +429,14 @@ const settings: SettingsType = {
|
|||
learnMoreUrl: null,
|
||||
dismissal: 'dismissible',
|
||||
},
|
||||
privacy: {
|
||||
// Outbound calls. See PRIVACY.md.
|
||||
// Set to false to disable hourly version check (UpdateCheck.ts).
|
||||
updateCheck: true,
|
||||
// Set to false to disable plugin-catalog fetch from updateServer
|
||||
// (installer.ts). Manual install via CLI still works.
|
||||
pluginCatalog: true,
|
||||
},
|
||||
/*
|
||||
* Skin name.
|
||||
*
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@ type Infos = {
|
|||
const updateInterval = 60 * 60 * 1000; // 1 hour
|
||||
let infos: Infos;
|
||||
let lastLoadingTime: number | null = null;
|
||||
let loggedDisabled = false;
|
||||
|
||||
const loadEtherpadInformations = () => {
|
||||
if (lastLoadingTime !== null && Date.now() - lastLoadingTime < updateInterval) {
|
||||
|
|
@ -38,6 +39,7 @@ const loadEtherpadInformations = () => {
|
|||
|
||||
|
||||
export const getLatestVersion = () => {
|
||||
if (!settings.privacy.updateCheck) return undefined;
|
||||
needsUpdate().catch();
|
||||
return infos?.latestVersion;
|
||||
};
|
||||
|
|
@ -55,6 +57,13 @@ const needsUpdate = async (cb?: Function) => {
|
|||
};
|
||||
|
||||
export const check = () => {
|
||||
if (!settings.privacy.updateCheck) {
|
||||
if (!loggedDisabled) {
|
||||
console.info('Update check disabled by privacy.updateCheck=false (see PRIVACY.md)');
|
||||
loggedDisabled = true;
|
||||
}
|
||||
return;
|
||||
}
|
||||
needsUpdate((needsUpdate: boolean) => {
|
||||
if (needsUpdate) {
|
||||
console.warn(`Update available: Download the actual version ${infos.latestVersion}`);
|
||||
|
|
|
|||
|
|
@ -83,7 +83,6 @@
|
|||
"socket.io-client": "^4.8.3",
|
||||
"superagent": "10.3.0",
|
||||
"surrealdb": "^2.0.3",
|
||||
"swagger-ui-express": "^5.0.1",
|
||||
"tinycon": "0.6.8",
|
||||
"tsx": "4.22.0",
|
||||
"ueberdb2": "^5.0.48",
|
||||
|
|
@ -119,7 +118,6 @@
|
|||
"@types/semver": "^7.7.1",
|
||||
"@types/sinon": "^21.0.1",
|
||||
"@types/supertest": "^7.2.0",
|
||||
"@types/swagger-ui-express": "^4.1.8",
|
||||
"@types/underscore": "^1.13.0",
|
||||
"@types/whatwg-mimetype": "^5.0.0",
|
||||
"chokidar": "^5.0.0",
|
||||
|
|
|
|||
33
src/static/api-docs.html
Normal file
33
src/static/api-docs.html
Normal file
|
|
@ -0,0 +1,33 @@
|
|||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<title>Etherpad API</title>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<meta name="referrer" content="no-referrer" />
|
||||
<style>
|
||||
html, body { margin: 0; padding: 0; height: 100%; }
|
||||
/* Scalar ships @font-face rules pointing at fonts.scalar.com.
|
||||
withDefaultFonts:false disables that import; the rule below
|
||||
forces a system-font stack so nothing fetches over the wire. */
|
||||
:root, .scalar-app, body { font-family: system-ui, -apple-system, sans-serif !important; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
<script src="/static/vendor/scalar/standalone.js"></script>
|
||||
<script>
|
||||
Scalar.createApiReference('#app', {
|
||||
url: '/api-docs.json',
|
||||
theme: 'default',
|
||||
layout: 'modern',
|
||||
hideClientButton: true,
|
||||
showDeveloperTools: 'never',
|
||||
withDefaultFonts: false,
|
||||
telemetry: false,
|
||||
agent: { disabled: true },
|
||||
mcp: { disabled: true }
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
|
@ -17,6 +17,7 @@ import settings, {
|
|||
reloadSettings
|
||||
} from '../../../node/utils/Settings';
|
||||
import {LinkInstaller} from "./LinkInstaller";
|
||||
import {assertPluginCatalogEnabled} from "./pluginCatalogGuard";
|
||||
import {
|
||||
checkEngineCompatibility,
|
||||
EngineIncompatibleError,
|
||||
|
|
@ -199,6 +200,7 @@ export let availablePlugins:MapArrayType<PackageInfo>|null = null;
|
|||
let cacheTimestamp = 0;
|
||||
|
||||
export const getAvailablePlugins = async (maxCacheAge: number | false) => {
|
||||
assertPluginCatalogEnabled();
|
||||
const nowTimestamp = Math.round(Date.now() / 1000);
|
||||
|
||||
// check cache age before making any request
|
||||
|
|
|
|||
11
src/static/js/pluginfw/pluginCatalogGuard.ts
Normal file
11
src/static/js/pluginfw/pluginCatalogGuard.ts
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
'use strict';
|
||||
|
||||
import settings from '../../../node/utils/Settings';
|
||||
|
||||
export const assertPluginCatalogEnabled = () => {
|
||||
if (!settings.privacy.pluginCatalog) {
|
||||
throw new Error(
|
||||
'Plugin catalog disabled by privacy.pluginCatalog=false (see PRIVACY.md)'
|
||||
);
|
||||
}
|
||||
};
|
||||
21
src/static/vendor/scalar/LICENSE
vendored
Normal file
21
src/static/vendor/scalar/LICENSE
vendored
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
MIT License
|
||||
|
||||
Copyright (c) 2023-present Scalar
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
3
src/static/vendor/scalar/VERSION
vendored
Normal file
3
src/static/vendor/scalar/VERSION
vendored
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
@scalar/api-reference 1.57.2 (standalone bundle) - vendored 2026-05-16
|
||||
source: https://cdn.jsdelivr.net/npm/@scalar/api-reference@1.57.2/dist/browser/standalone.js
|
||||
sha256: 538bea0c451ae72ca26242244491a35b3bee4e1ba282f0076f454a68b83788e5
|
||||
2364
src/static/vendor/scalar/standalone.js
vendored
Normal file
2364
src/static/vendor/scalar/standalone.js
vendored
Normal file
File diff suppressed because one or more lines are too long
21
src/tests/backend-new/specs/privacy/installer-optout.test.ts
Normal file
21
src/tests/backend-new/specs/privacy/installer-optout.test.ts
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
import {describe, it, expect, beforeEach} from 'vitest';
|
||||
import settings from '../../../../node/utils/Settings';
|
||||
import {assertPluginCatalogEnabled} from '../../../../static/js/pluginfw/pluginCatalogGuard';
|
||||
|
||||
describe('Plugin catalog opt-out guard', () => {
|
||||
beforeEach(() => {
|
||||
settings.privacy.pluginCatalog = true;
|
||||
});
|
||||
|
||||
it('throws tagged error when privacy.pluginCatalog is false', () => {
|
||||
settings.privacy.pluginCatalog = false;
|
||||
expect(() => assertPluginCatalogEnabled()).toThrow(
|
||||
/privacy\.pluginCatalog=false/
|
||||
);
|
||||
});
|
||||
|
||||
it('does not throw when privacy.pluginCatalog is true', () => {
|
||||
settings.privacy.pluginCatalog = true;
|
||||
expect(() => assertPluginCatalogEnabled()).not.toThrow();
|
||||
});
|
||||
});
|
||||
|
|
@ -0,0 +1,12 @@
|
|||
import {describe, it, expect} from 'vitest';
|
||||
import settings from '../../../../node/utils/Settings';
|
||||
|
||||
describe('privacy settings defaults', () => {
|
||||
it('privacy.updateCheck defaults to true', () => {
|
||||
expect(settings.privacy.updateCheck).toBe(true);
|
||||
});
|
||||
|
||||
it('privacy.pluginCatalog defaults to true', () => {
|
||||
expect(settings.privacy.pluginCatalog).toBe(true);
|
||||
});
|
||||
});
|
||||
|
|
@ -0,0 +1,31 @@
|
|||
import {describe, it, expect, beforeEach, vi} from 'vitest';
|
||||
import settings from '../../../../node/utils/Settings';
|
||||
import {check} from '../../../../node/utils/UpdateCheck';
|
||||
|
||||
describe('UpdateCheck opt-out', () => {
|
||||
beforeEach(() => {
|
||||
vi.restoreAllMocks();
|
||||
});
|
||||
|
||||
it('check() does not call fetch when privacy.updateCheck is false', async () => {
|
||||
settings.privacy.updateCheck = false;
|
||||
const fetchSpy = vi.spyOn(globalThis, 'fetch').mockResolvedValue(
|
||||
new Response('{}', {status: 200})
|
||||
);
|
||||
check();
|
||||
// Allow any internal microtasks to settle.
|
||||
await new Promise((r) => setImmediate(r));
|
||||
expect(fetchSpy).not.toHaveBeenCalled();
|
||||
settings.privacy.updateCheck = true;
|
||||
});
|
||||
|
||||
it('check() calls fetch when privacy.updateCheck is true', async () => {
|
||||
settings.privacy.updateCheck = true;
|
||||
const fetchSpy = vi.spyOn(globalThis, 'fetch').mockResolvedValue(
|
||||
new Response(JSON.stringify({latestVersion: '99.0.0'}), {status: 200})
|
||||
);
|
||||
check();
|
||||
await new Promise((r) => setImmediate(r));
|
||||
expect(fetchSpy).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
Loading…
Add table
Add a link
Reference in a new issue