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
|
### 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).**
|
- **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.
|
- 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.
|
- 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.
|
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.
|
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;
|
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 */
|
/* Header */
|
||||||
.pm-header {
|
.pm-header {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
|
|
||||||
|
|
@ -9,6 +9,7 @@ import {IconButton} from "../components/IconButton.tsx";
|
||||||
export const HomePage = () => {
|
export const HomePage = () => {
|
||||||
const pluginsSocket = useStore(state => state.pluginsSocket)
|
const pluginsSocket = useStore(state => state.pluginsSocket)
|
||||||
const [plugins, setPlugins] = useState<PluginDef[]>([])
|
const [plugins, setPlugins] = useState<PluginDef[]>([])
|
||||||
|
const [catalogDisabled, setCatalogDisabled] = useState(false)
|
||||||
const installedPlugins = useStore(state => state.installedPlugins)
|
const installedPlugins = useStore(state => state.installedPlugins)
|
||||||
const setInstalledPlugins = useStore(state => state.setInstalledPlugins)
|
const setInstalledPlugins = useStore(state => state.setInstalledPlugins)
|
||||||
// Default sort: name ascending. PR #7716 set this to "downloads desc" but
|
// Default sort: name ascending. PR #7716 set this to "downloads desc" but
|
||||||
|
|
@ -89,11 +90,14 @@ export const HomePage = () => {
|
||||||
pluginsSocket.emit('search', searchParams)
|
pluginsSocket.emit('search', searchParams)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const onCatalogDisabled = () => setCatalogDisabled(true)
|
||||||
|
|
||||||
pluginsSocket.on('results:installed', onInstalled)
|
pluginsSocket.on('results:installed', onInstalled)
|
||||||
pluginsSocket.on('results:updatable', onUpdatable)
|
pluginsSocket.on('results:updatable', onUpdatable)
|
||||||
pluginsSocket.on('finished:install', onFinishedInstall)
|
pluginsSocket.on('finished:install', onFinishedInstall)
|
||||||
pluginsSocket.on('finished:uninstall', onFinishedUninstall)
|
pluginsSocket.on('finished:uninstall', onFinishedUninstall)
|
||||||
pluginsSocket.on('connect', onConnect)
|
pluginsSocket.on('connect', onConnect)
|
||||||
|
pluginsSocket.on('results:catalogDisabled', onCatalogDisabled)
|
||||||
|
|
||||||
pluginsSocket.emit('getInstalled')
|
pluginsSocket.emit('getInstalled')
|
||||||
|
|
||||||
|
|
@ -105,6 +109,7 @@ export const HomePage = () => {
|
||||||
pluginsSocket.off('finished:install', onFinishedInstall)
|
pluginsSocket.off('finished:install', onFinishedInstall)
|
||||||
pluginsSocket.off('finished:uninstall', onFinishedUninstall)
|
pluginsSocket.off('finished:uninstall', onFinishedUninstall)
|
||||||
pluginsSocket.off('connect', onConnect)
|
pluginsSocket.off('connect', onConnect)
|
||||||
|
pluginsSocket.off('results:catalogDisabled', onCatalogDisabled)
|
||||||
}
|
}
|
||||||
}, [pluginsSocket])
|
}, [pluginsSocket])
|
||||||
|
|
||||||
|
|
@ -145,6 +150,12 @@ export const HomePage = () => {
|
||||||
return (
|
return (
|
||||||
<div className="pm-page">
|
<div className="pm-page">
|
||||||
|
|
||||||
|
{catalogDisabled && (
|
||||||
|
<div className="pm-banner pm-banner-info" role="status">
|
||||||
|
<Trans i18nKey="admin_plugins.catalog_disabled"/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
{/* ── Page header ────────────────────────────────────────────────── */}
|
{/* ── Page header ────────────────────────────────────────────────── */}
|
||||||
<div className="pm-header">
|
<div className="pm-header">
|
||||||
<div>
|
<div>
|
||||||
|
|
|
||||||
|
|
@ -3,10 +3,17 @@
|
||||||
// Returns a list of stale plugins and their authors email
|
// Returns a list of stale plugins and their authors email
|
||||||
|
|
||||||
import process from "node:process";
|
import process from "node:process";
|
||||||
|
import settings from "../../src/node/utils/Settings";
|
||||||
const currentTime = new Date();
|
const currentTime = new Date();
|
||||||
|
|
||||||
(async () => {
|
(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}`);
|
if (!resp.ok) throw new Error(`HTTP ${resp.status} ${resp.statusText}`);
|
||||||
const data: any = await resp.json();
|
const data: any = await resp.json();
|
||||||
for (const plugin of Object.keys(data)) {
|
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:
|
surrealdb:
|
||||||
specifier: ^2.0.3
|
specifier: ^2.0.3
|
||||||
version: 2.0.3(tslib@2.8.1)(typescript@6.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:
|
tinycon:
|
||||||
specifier: 0.6.8
|
specifier: 0.6.8
|
||||||
version: 0.6.8
|
version: 0.6.8
|
||||||
|
|
@ -434,9 +431,6 @@ importers:
|
||||||
'@types/supertest':
|
'@types/supertest':
|
||||||
specifier: ^7.2.0
|
specifier: ^7.2.0
|
||||||
version: 7.2.0
|
version: 7.2.0
|
||||||
'@types/swagger-ui-express':
|
|
||||||
specifier: ^4.1.8
|
|
||||||
version: 4.1.8
|
|
||||||
'@types/underscore':
|
'@types/underscore':
|
||||||
specifier: ^1.13.0
|
specifier: ^1.13.0
|
||||||
version: 1.13.0
|
version: 1.13.0
|
||||||
|
|
@ -1757,9 +1751,6 @@ packages:
|
||||||
'@rushstack/eslint-patch@1.16.1':
|
'@rushstack/eslint-patch@1.16.1':
|
||||||
resolution: {integrity: sha512-TvZbIpeKqGQQ7X0zSCvPH9riMSFQFSggnfBjFZ1mEoILW+UuXCKwOoPcgjMwiUtRqFZ8jWhPJc4um14vC6I4ag==}
|
resolution: {integrity: sha512-TvZbIpeKqGQQ7X0zSCvPH9riMSFQFSggnfBjFZ1mEoILW+UuXCKwOoPcgjMwiUtRqFZ8jWhPJc4um14vC6I4ag==}
|
||||||
|
|
||||||
'@scarf/scarf@1.4.0':
|
|
||||||
resolution: {integrity: sha512-xxeapPiUXdZAE3che6f3xogoJPeZgig6omHEy1rIY5WVsB3H2BHNnZH+gHG6x91SCWyQCzWGsuL2Hh3ClO5/qQ==}
|
|
||||||
|
|
||||||
'@shikijs/core@3.23.0':
|
'@shikijs/core@3.23.0':
|
||||||
resolution: {integrity: sha512-NSWQz0riNb67xthdm5br6lAkvpDJRTgB36fxlo37ZzM2yq0PQFFzbd8psqC2XMPgCzo1fW6cVi18+ArJ44wqgA==}
|
resolution: {integrity: sha512-NSWQz0riNb67xthdm5br6lAkvpDJRTgB36fxlo37ZzM2yq0PQFFzbd8psqC2XMPgCzo1fW6cVi18+ArJ44wqgA==}
|
||||||
|
|
||||||
|
|
@ -1969,9 +1960,6 @@ packages:
|
||||||
'@types/mime-types@3.0.1':
|
'@types/mime-types@3.0.1':
|
||||||
resolution: {integrity: sha512-xRMsfuQbnRq1Ef+C+RKaENOxXX87Ygl38W1vDfPHRku02TgQr+Qd8iivLtAMcR0KF5/29xlnFihkTlbqFrGOVQ==}
|
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':
|
'@types/mocha@10.0.10':
|
||||||
resolution: {integrity: sha512-xPyYSz1cMPnJQhl0CLMH68j3gprKZaTjG3s5Vi+fDgx+uhG9NOXwbVt52eFS8ECyXhyKcjDLCBEqBExKuiZb7Q==}
|
resolution: {integrity: sha512-xPyYSz1cMPnJQhl0CLMH68j3gprKZaTjG3s5Vi+fDgx+uhG9NOXwbVt52eFS8ECyXhyKcjDLCBEqBExKuiZb7Q==}
|
||||||
|
|
||||||
|
|
@ -2019,15 +2007,9 @@ packages:
|
||||||
'@types/semver@7.7.1':
|
'@types/semver@7.7.1':
|
||||||
resolution: {integrity: sha512-FmgJfu+MOcQ370SD0ev7EI8TlCAfKYU+B4m5T3yXc1CiRN94g/SZPtsCkk506aUDtlMnFZvasDwHHUcZUEaYuA==}
|
resolution: {integrity: sha512-FmgJfu+MOcQ370SD0ev7EI8TlCAfKYU+B4m5T3yXc1CiRN94g/SZPtsCkk506aUDtlMnFZvasDwHHUcZUEaYuA==}
|
||||||
|
|
||||||
'@types/send@0.17.4':
|
|
||||||
resolution: {integrity: sha512-x2EM6TJOybec7c52BX0ZspPodMsQUd5L6PRwOunVyVUhXiBSKf3AezDL8Dgvgt5o0UfKNfuA0eMLr2wLT4AiBA==}
|
|
||||||
|
|
||||||
'@types/send@1.2.1':
|
'@types/send@1.2.1':
|
||||||
resolution: {integrity: sha512-arsCikDvlU99zl1g69TcAB3mzZPpxgw0UQnaHeC1Nwb015xp8bknZv5rIfri9xTOcMuaVgvabfIRA7PSZVuZIQ==}
|
resolution: {integrity: sha512-arsCikDvlU99zl1g69TcAB3mzZPpxgw0UQnaHeC1Nwb015xp8bknZv5rIfri9xTOcMuaVgvabfIRA7PSZVuZIQ==}
|
||||||
|
|
||||||
'@types/serve-static@1.15.7':
|
|
||||||
resolution: {integrity: sha512-W8Ym+h8nhuRwaKPaDw34QUkwsGi6Rc4yYqvKFo5rm2FUEhCFbzVWrxXUxuKK8TASjWsysJY0nsmNCGhCOIsrOw==}
|
|
||||||
|
|
||||||
'@types/serve-static@2.2.0':
|
'@types/serve-static@2.2.0':
|
||||||
resolution: {integrity: sha512-8mam4H1NHLtu7nmtalF7eyBH14QyOASmcxHhSfEoRyr0nP/YdoesEtU+uSRvMe96TW/HPTtkoKqQLl53N7UXMQ==}
|
resolution: {integrity: sha512-8mam4H1NHLtu7nmtalF7eyBH14QyOASmcxHhSfEoRyr0nP/YdoesEtU+uSRvMe96TW/HPTtkoKqQLl53N7UXMQ==}
|
||||||
|
|
||||||
|
|
@ -2043,9 +2025,6 @@ packages:
|
||||||
'@types/supertest@7.2.0':
|
'@types/supertest@7.2.0':
|
||||||
resolution: {integrity: sha512-uh2Lv57xvggst6lCqNdFAmDSvoMG7M/HDtX4iUCquxQ5EGPtaPM5PL5Hmi7LCvOG8db7YaCPNJEeoI8s/WzIQw==}
|
resolution: {integrity: sha512-uh2Lv57xvggst6lCqNdFAmDSvoMG7M/HDtX4iUCquxQ5EGPtaPM5PL5Hmi7LCvOG8db7YaCPNJEeoI8s/WzIQw==}
|
||||||
|
|
||||||
'@types/swagger-ui-express@4.1.8':
|
|
||||||
resolution: {integrity: sha512-AhZV8/EIreHFmBV5wAs0gzJUNq9JbbSXgJLQubCC0jtIo6prnI9MIRRxnU4MZX9RB9yXxF1V4R7jtLl/Wcj31g==}
|
|
||||||
|
|
||||||
'@types/tar@6.1.13':
|
'@types/tar@6.1.13':
|
||||||
resolution: {integrity: sha512-IznnlmU5f4WcGTh2ltRu/Ijpmk8wiWXfF0VA4s+HPjHZgvFggk1YaIkbo5krX/zUCzWF8N/l4+W/LNxnvAJ8nw==}
|
resolution: {integrity: sha512-IznnlmU5f4WcGTh2ltRu/Ijpmk8wiWXfF0VA4s+HPjHZgvFggk1YaIkbo5krX/zUCzWF8N/l4+W/LNxnvAJ8nw==}
|
||||||
|
|
||||||
|
|
@ -5438,15 +5417,6 @@ packages:
|
||||||
swagger-schema-official@2.0.0-bab6bed:
|
swagger-schema-official@2.0.0-bab6bed:
|
||||||
resolution: {integrity: sha512-rCC0NWGKr/IJhtRuPq/t37qvZHI/mH4I4sxflVM+qgVe5Z2uOCivzWaVbuioJaB61kvm5UvB7b49E+oBY0M8jA==}
|
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:
|
symbol-tree@3.2.4:
|
||||||
resolution: {integrity: sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==}
|
resolution: {integrity: sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==}
|
||||||
|
|
||||||
|
|
@ -7204,8 +7174,6 @@ snapshots:
|
||||||
|
|
||||||
'@rushstack/eslint-patch@1.16.1': {}
|
'@rushstack/eslint-patch@1.16.1': {}
|
||||||
|
|
||||||
'@scarf/scarf@1.4.0': {}
|
|
||||||
|
|
||||||
'@shikijs/core@3.23.0':
|
'@shikijs/core@3.23.0':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@shikijs/types': 3.23.0
|
'@shikijs/types': 3.23.0
|
||||||
|
|
@ -7447,8 +7415,6 @@ snapshots:
|
||||||
|
|
||||||
'@types/mime-types@3.0.1': {}
|
'@types/mime-types@3.0.1': {}
|
||||||
|
|
||||||
'@types/mime@1.3.5': {}
|
|
||||||
|
|
||||||
'@types/mocha@10.0.10': {}
|
'@types/mocha@10.0.10': {}
|
||||||
|
|
||||||
'@types/ms@2.1.0': {}
|
'@types/ms@2.1.0': {}
|
||||||
|
|
@ -7502,21 +7468,10 @@ snapshots:
|
||||||
|
|
||||||
'@types/semver@7.7.1': {}
|
'@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':
|
'@types/send@1.2.1':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@types/node': 25.7.0
|
'@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':
|
'@types/serve-static@2.2.0':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@types/http-errors': 2.0.5
|
'@types/http-errors': 2.0.5
|
||||||
|
|
@ -7540,11 +7495,6 @@ snapshots:
|
||||||
'@types/methods': 1.1.4
|
'@types/methods': 1.1.4
|
||||||
'@types/superagent': 8.1.9
|
'@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':
|
'@types/tar@6.1.13':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@types/node': 25.7.0
|
'@types/node': 25.7.0
|
||||||
|
|
@ -8769,10 +8719,10 @@ snapshots:
|
||||||
'@rushstack/eslint-patch': 1.16.1
|
'@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/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)
|
'@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-cypress: 2.15.2(eslint@10.4.0)
|
||||||
eslint-plugin-eslint-comments: 3.2.0(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-mocha: 10.5.0(eslint@10.4.0)
|
||||||
eslint-plugin-n: 17.24.0(eslint@10.4.0)(typescript@6.0.3)
|
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)
|
eslint-plugin-prefer-arrow: 1.2.3(eslint@10.4.0)
|
||||||
|
|
@ -8793,7 +8743,7 @@ snapshots:
|
||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- supports-color
|
- 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:
|
dependencies:
|
||||||
'@nolyfill/is-core-module': 1.0.39
|
'@nolyfill/is-core-module': 1.0.39
|
||||||
debug: 4.4.3(supports-color@8.1.1)
|
debug: 4.4.3(supports-color@8.1.1)
|
||||||
|
|
@ -8804,18 +8754,18 @@ snapshots:
|
||||||
stable-hash: 0.0.5
|
stable-hash: 0.0.5
|
||||||
tinyglobby: 0.2.16
|
tinyglobby: 0.2.16
|
||||||
optionalDependencies:
|
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:
|
transitivePeerDependencies:
|
||||||
- supports-color
|
- 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:
|
dependencies:
|
||||||
debug: 3.2.7
|
debug: 3.2.7
|
||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
'@typescript-eslint/parser': 7.18.0(eslint@10.4.0)(typescript@6.0.3)
|
'@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-node: 0.3.10
|
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:
|
transitivePeerDependencies:
|
||||||
- supports-color
|
- supports-color
|
||||||
|
|
||||||
|
|
@ -8837,7 +8787,7 @@ snapshots:
|
||||||
eslint: 10.4.0
|
eslint: 10.4.0
|
||||||
ignore: 5.3.2
|
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:
|
dependencies:
|
||||||
'@rtsao/scc': 1.1.0
|
'@rtsao/scc': 1.1.0
|
||||||
array-includes: 3.1.9
|
array-includes: 3.1.9
|
||||||
|
|
@ -8848,7 +8798,7 @@ snapshots:
|
||||||
doctrine: 2.1.0
|
doctrine: 2.1.0
|
||||||
eslint: 10.4.0
|
eslint: 10.4.0
|
||||||
eslint-import-resolver-node: 0.3.10
|
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
|
hasown: 2.0.2
|
||||||
is-core-module: 2.16.1
|
is-core-module: 2.16.1
|
||||||
is-glob: 4.0.3
|
is-glob: 4.0.3
|
||||||
|
|
@ -11366,15 +11316,6 @@ snapshots:
|
||||||
|
|
||||||
swagger-schema-official@2.0.0-bab6bed: {}
|
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: {}
|
symbol-tree@3.2.4: {}
|
||||||
|
|
||||||
tabbable@6.4.0: {}
|
tabbable@6.4.0: {}
|
||||||
|
|
|
||||||
|
|
@ -417,6 +417,17 @@
|
||||||
|
|
||||||
"updateServer": "https://etherpad.org/ep_infos",
|
"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?
|
* 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.available_search.placeholder": "Search for plugins to install",
|
||||||
"admin_plugins.check_updates": "Check for updates",
|
"admin_plugins.check_updates": "Check for updates",
|
||||||
"admin_plugins.core_count": "{{count}} core",
|
"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.crumbs": "Plugins",
|
||||||
"admin_plugins.description": "Description",
|
"admin_plugins.description": "Description",
|
||||||
"admin_plugins.disables.label": "Disables:",
|
"admin_plugins.disables.label": "Disables:",
|
||||||
|
|
|
||||||
|
|
@ -5,8 +5,8 @@ import {ErrorCaused} from "../types/ErrorCaused";
|
||||||
import createHTTPError from "http-errors";
|
import createHTTPError from "http-errors";
|
||||||
|
|
||||||
const apiHandler = require('./APIHandler')
|
const apiHandler = require('./APIHandler')
|
||||||
import {serve, setup} from 'swagger-ui-express'
|
|
||||||
import express from "express";
|
import express from "express";
|
||||||
|
import path from "path";
|
||||||
|
|
||||||
import settings from '../utils/Settings';
|
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', (_req, res) => {
|
||||||
app.get('/api-docs', setup(undefined, {
|
res.sendFile(path.join(settings.root, 'src', 'static', 'api-docs.html'));
|
||||||
swaggerOptions: {
|
});
|
||||||
url: '/api-docs.json',
|
|
||||||
},
|
|
||||||
}));
|
|
||||||
|
|
||||||
app.use(express.json());
|
app.use(express.json());
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -9,6 +9,7 @@ import {PackageData, PackageInfo} from "../../types/PackageInfo";
|
||||||
import semver from 'semver';
|
import semver from 'semver';
|
||||||
import log4js from 'log4js';
|
import log4js from 'log4js';
|
||||||
import {MapArrayType} from "../../types/MapType";
|
import {MapArrayType} from "../../types/MapType";
|
||||||
|
import settings from "../../utils/Settings";
|
||||||
|
|
||||||
const pluginDefs = require('../../../static/js/pluginfw/plugin_defs');
|
const pluginDefs = require('../../../static/js/pluginfw/plugin_defs');
|
||||||
const logger = log4js.getLogger('adminPlugins');
|
const logger = log4js.getLogger('adminPlugins');
|
||||||
|
|
@ -49,31 +50,43 @@ exports.socketio = (hookName:string, args:ArgsExpressType, cb:Function) => {
|
||||||
const installed =
|
const installed =
|
||||||
Object.keys(pluginDefs.plugins).map((plugin) => pluginDefs.plugins[plugin].package);
|
Object.keys(pluginDefs.plugins).map((plugin) => pluginDefs.plugins[plugin].package);
|
||||||
|
|
||||||
const updatable = await checkPluginForUpdates();
|
if (settings.privacy.pluginCatalog) {
|
||||||
|
const updatable = await checkPluginForUpdates();
|
||||||
installed.forEach((plugin) => {
|
installed.forEach((plugin) => {
|
||||||
plugin.updatable = updatable.includes(plugin.name);
|
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.emit('results:installed', {installed});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
socket.on('checkUpdates', async () => {
|
socket.on('checkUpdates', async () => {
|
||||||
|
if (!settings.privacy.pluginCatalog) {
|
||||||
|
socket.emit('results:catalogDisabled');
|
||||||
|
return;
|
||||||
|
}
|
||||||
// Check plugins for updates
|
// Check plugins for updates
|
||||||
try {
|
try {
|
||||||
const updatable = checkPluginForUpdates();
|
const updatable = await checkPluginForUpdates();
|
||||||
|
|
||||||
socket.emit('results:updatable', {updatable});
|
socket.emit('results:updatable', {updatable});
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
const errc = err as ErrorCaused
|
const errc = err as ErrorCaused
|
||||||
console.warn(errc.stack || errc.toString());
|
console.warn(errc.stack || errc.toString());
|
||||||
|
|
||||||
socket.emit('results:updatable', {updatable: {}});
|
socket.emit('results:updatable', {updatable: []});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
socket.on('getAvailable', async (query:string) => {
|
socket.on('getAvailable', async (query:string) => {
|
||||||
|
if (!settings.privacy.pluginCatalog) {
|
||||||
|
socket.emit('results:catalogDisabled');
|
||||||
|
return;
|
||||||
|
}
|
||||||
try {
|
try {
|
||||||
const results = await getAvailablePlugins(/* maxCacheAge:*/ false);
|
const results = await getAvailablePlugins(/* maxCacheAge:*/ false);
|
||||||
socket.emit('results:available', results);
|
socket.emit('results:available', results);
|
||||||
|
|
@ -84,6 +97,10 @@ exports.socketio = (hookName:string, args:ArgsExpressType, cb:Function) => {
|
||||||
});
|
});
|
||||||
|
|
||||||
socket.on('search', async (query: QueryType) => {
|
socket.on('search', async (query: QueryType) => {
|
||||||
|
if (!settings.privacy.pluginCatalog) {
|
||||||
|
socket.emit('results:catalogDisabled');
|
||||||
|
return;
|
||||||
|
}
|
||||||
try {
|
try {
|
||||||
if (query.searchTerm) logger.info(`Plugin search: ${query.searchTerm}'`);
|
if (query.searchTerm) logger.info(`Plugin search: ${query.searchTerm}'`);
|
||||||
const results = await search(query.searchTerm, /* maxCacheAge:*/ 60 * 10);
|
const results = await search(query.searchTerm, /* maxCacheAge:*/ 60 * 10);
|
||||||
|
|
|
||||||
|
|
@ -193,6 +193,10 @@ export type SettingsType = {
|
||||||
learnMoreUrl: string | null,
|
learnMoreUrl: string | null,
|
||||||
dismissal: 'dismissible' | 'sticky',
|
dismissal: 'dismissible' | 'sticky',
|
||||||
},
|
},
|
||||||
|
privacy: {
|
||||||
|
updateCheck: boolean,
|
||||||
|
pluginCatalog: boolean,
|
||||||
|
},
|
||||||
skinName: string | null,
|
skinName: string | null,
|
||||||
skinVariants: string,
|
skinVariants: string,
|
||||||
ip: string,
|
ip: string,
|
||||||
|
|
@ -425,6 +429,14 @@ const settings: SettingsType = {
|
||||||
learnMoreUrl: null,
|
learnMoreUrl: null,
|
||||||
dismissal: 'dismissible',
|
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.
|
* Skin name.
|
||||||
*
|
*
|
||||||
|
|
|
||||||
|
|
@ -13,6 +13,7 @@ type Infos = {
|
||||||
const updateInterval = 60 * 60 * 1000; // 1 hour
|
const updateInterval = 60 * 60 * 1000; // 1 hour
|
||||||
let infos: Infos;
|
let infos: Infos;
|
||||||
let lastLoadingTime: number | null = null;
|
let lastLoadingTime: number | null = null;
|
||||||
|
let loggedDisabled = false;
|
||||||
|
|
||||||
const loadEtherpadInformations = () => {
|
const loadEtherpadInformations = () => {
|
||||||
if (lastLoadingTime !== null && Date.now() - lastLoadingTime < updateInterval) {
|
if (lastLoadingTime !== null && Date.now() - lastLoadingTime < updateInterval) {
|
||||||
|
|
@ -38,6 +39,7 @@ const loadEtherpadInformations = () => {
|
||||||
|
|
||||||
|
|
||||||
export const getLatestVersion = () => {
|
export const getLatestVersion = () => {
|
||||||
|
if (!settings.privacy.updateCheck) return undefined;
|
||||||
needsUpdate().catch();
|
needsUpdate().catch();
|
||||||
return infos?.latestVersion;
|
return infos?.latestVersion;
|
||||||
};
|
};
|
||||||
|
|
@ -55,6 +57,13 @@ const needsUpdate = async (cb?: Function) => {
|
||||||
};
|
};
|
||||||
|
|
||||||
export const check = () => {
|
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) => {
|
needsUpdate((needsUpdate: boolean) => {
|
||||||
if (needsUpdate) {
|
if (needsUpdate) {
|
||||||
console.warn(`Update available: Download the actual version ${infos.latestVersion}`);
|
console.warn(`Update available: Download the actual version ${infos.latestVersion}`);
|
||||||
|
|
|
||||||
|
|
@ -83,7 +83,6 @@
|
||||||
"socket.io-client": "^4.8.3",
|
"socket.io-client": "^4.8.3",
|
||||||
"superagent": "10.3.0",
|
"superagent": "10.3.0",
|
||||||
"surrealdb": "^2.0.3",
|
"surrealdb": "^2.0.3",
|
||||||
"swagger-ui-express": "^5.0.1",
|
|
||||||
"tinycon": "0.6.8",
|
"tinycon": "0.6.8",
|
||||||
"tsx": "4.22.0",
|
"tsx": "4.22.0",
|
||||||
"ueberdb2": "^5.0.48",
|
"ueberdb2": "^5.0.48",
|
||||||
|
|
@ -119,7 +118,6 @@
|
||||||
"@types/semver": "^7.7.1",
|
"@types/semver": "^7.7.1",
|
||||||
"@types/sinon": "^21.0.1",
|
"@types/sinon": "^21.0.1",
|
||||||
"@types/supertest": "^7.2.0",
|
"@types/supertest": "^7.2.0",
|
||||||
"@types/swagger-ui-express": "^4.1.8",
|
|
||||||
"@types/underscore": "^1.13.0",
|
"@types/underscore": "^1.13.0",
|
||||||
"@types/whatwg-mimetype": "^5.0.0",
|
"@types/whatwg-mimetype": "^5.0.0",
|
||||||
"chokidar": "^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
|
reloadSettings
|
||||||
} from '../../../node/utils/Settings';
|
} from '../../../node/utils/Settings';
|
||||||
import {LinkInstaller} from "./LinkInstaller";
|
import {LinkInstaller} from "./LinkInstaller";
|
||||||
|
import {assertPluginCatalogEnabled} from "./pluginCatalogGuard";
|
||||||
import {
|
import {
|
||||||
checkEngineCompatibility,
|
checkEngineCompatibility,
|
||||||
EngineIncompatibleError,
|
EngineIncompatibleError,
|
||||||
|
|
@ -199,6 +200,7 @@ export let availablePlugins:MapArrayType<PackageInfo>|null = null;
|
||||||
let cacheTimestamp = 0;
|
let cacheTimestamp = 0;
|
||||||
|
|
||||||
export const getAvailablePlugins = async (maxCacheAge: number | false) => {
|
export const getAvailablePlugins = async (maxCacheAge: number | false) => {
|
||||||
|
assertPluginCatalogEnabled();
|
||||||
const nowTimestamp = Math.round(Date.now() / 1000);
|
const nowTimestamp = Math.round(Date.now() / 1000);
|
||||||
|
|
||||||
// check cache age before making any request
|
// 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