* chore(root): drop redundant top-level files Three small clean-ups to make the project root easier to scan for new contributors: - best_practices.md was a near-duplicate of CONTRIBUTING.md. Merge its two unique bullets (PRs MUST include a description / flag empty descriptions as incomplete) into CONTRIBUTING.md and remove the file. - .pr_agent.toml is a two-line Qodo PR-bot config. It has no functional references in the repo and Qodo currently doesn't review this repo. - tests/ -> src/tests was an unused root symlink. Verified no workflow, script, eslint config, or playwright config relies on the root path (all callers use src/tests/... or the ep_etherpad-lite package path). No CI, build, or docs updates needed. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * chore(root): fix dead tests/frontend + best_practices.md refs Follow-up on Qodo review feedback for #7839: - CONTRIBUTING.md: front-end-tests path now reads src/tests/frontend/ instead of the dead tests/frontend/ (the root tests symlink is gone). The browser URL <yourdomainhere>/tests/frontend stays — that's an Express route served by the test runner, not a filesystem path. - docs/superpowers/specs/...openapi-design.md: drop best_practices.md from the parenthetical list of policy sources (now CONTRIBUTING.md and AGENTS.MD), since best_practices.md has been removed. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
14 KiB
Issue 7693 — Document admin endpoints in the OpenAPI spec
Status: design approved 2026-05-08
Issue: https://github.com/ether/etherpad/issues/7693
Stacks on: PR #7695 (chore/admin-typesafe-api-7638-upstream) — codegen rails
Related: #7601 (introduced /admin/update/status); #7607 (Tier 2 update endpoints, in-flight)
Goal
Add OpenAPI definitions for the admin endpoints currently consumed by the admin
UI so the typed client generated by PR #7695 (admin/src/api/schema.d.ts)
gains admin call-sites the day it lands.
This PR adds the schema only. No call-sites migrate — that is the explicit follow-up named in #7693.
Scope
In:
POST /admin-auth/— login + session check (consumed byLoginScreen.tsxandApp.tsx).GET /admin/update/status— Tier 1 update banner data (consumed byUpdateBanner.tsxandUpdatePage.tsx; introduced by #7601, merged on develop).
Out:
/admin/update/{apply,cancel,acknowledge,log}— Tier 2 endpoints from the in-flightfeat/7607-auto-update-tier2-manual-clickbranch. That PR amendsopenapi-admin.tswhen it lands.- The admin SPA static-file route (
/admin/{*filename}) — not an API. /admin/socket.io/*— websocket; out of OpenAPI scope./api/version-status— already public, belongs in the public spec, not the admin spec.- Migrating any of the four admin
fetch()call-sites to$api.
Architecture
File layout (new files marked NEW)
src/node/hooks/express/
├── openapi.ts unchanged — APIHandler-driven public spec
└── openapi-admin.ts NEW — hand-authored OpenAPI 3.0 doc for admin routes
src/tests/backend/specs/
└── openapi-admin.ts NEW — Mocha specs asserting document shape
admin/scripts/
├── dump-spec.ts MODIFIED — also import generateAdminDefinition,
│ deep-merge into one document, write merged JSON
├── merge-openapi.mjs NEW — focused deep-merge with collision detection
├── __tests__/
│ └── merge-openapi.test.mjs NEW — node --test unit specs for the merge
└── gen-api.mjs unchanged — still calls dump-spec.ts then
openapi-typescript on the resulting JSON
openapi-admin.ts is a static OpenAPI document (no APIHandler reflection).
Hand-authored because admin routes aren't registered through APIHandler — they
are plain Express handlers. This keeps openapi.ts's 771-line generator
untouched and avoids tangling two different generation strategies in one
module.
Why merge in dump-spec.ts rather than at openapi-typescript time
openapi-typescript only accepts one input. We could run it twice and emit
two .d.ts files, but the chosen design (see "Codegen merge" below) is a
single merged schema.d.ts. The merge therefore happens at JSON-dump time,
before openapi-typescript runs.
Two clients, one schema
The merged schema covers two surfaces with different baseUrls (public API
under /api/<version>/, admin endpoints at root). A single runtime client
with one baseUrl cannot target both correctly. admin/src/api/client.ts
therefore narrows the generated paths interface by URL prefix and exports
two clients:
type AdminPath = Extract<keyof paths, `/admin${string}`>;
type PublicPath = Exclude<keyof paths, AdminPath>;
export const fetchClient = createClient<Pick<paths, PublicPath>>({ baseUrl: API_BASE_URL });
export const adminFetchClient = createClient<Pick<paths, AdminPath>>({ baseUrl: '/' });
export const $api = createQueryHooks(fetchClient);
export const $adminApi = createQueryHooks(adminFetchClient);
Narrowing at the type level means TypeScript rejects calling an admin path
on fetchClient (or vice versa) at compile time — the runtime baseUrl
mismatch is unrepresentable.
OpenAPI document contents
Info & security schemes
openapi: 3.0.2
info:
title: Etherpad Admin API
version: <getEpVersion()>
description: |
Authenticated administrative endpoints consumed by the Etherpad admin UI.
Distinct from the public /api/{version}/* surface served by openapi.json.
components:
securitySchemes:
basicAuth:
type: http
scheme: basic
sessionCookie:
type: apiKey
in: cookie
name: express_sid
basicAuth covers the login POST to /admin-auth/. sessionCookie covers
post-login admin sessions established by express-session (cookie name
express_sid is the Etherpad default; if a deployment overrides it the spec
remains structurally correct — only the documented cookie name shifts).
The two schemes coexist on /admin-auth/; only sessionCookie applies on
/admin/update/status.
Paths
POST /admin-auth/ — verifyAdminAccess
- Security:
[{ basicAuth: [] }, { sessionCookie: [] }, {}]— Basic or session cookie or none. The empty object documents that the server accepts the request without auth and replies401. - Responses:
200— admin verified (Basic logged in, or session cookie was valid for an admin user). Empty body.401— no auth presented and no session. Empty body.403— auth presented or session present, but the user is not an admin. Empty body.
- Description: notes that POST with
Authorization: Basic …establishes an admin session; POST with no auth header verifies an existing one.
This single-operation modeling matches reality: the route is one middleware-terminated path that branches on what the client sends. Two operations on the same path would imply different server behavior the admin UI does not actually depend on.
GET /admin/update/status — getUpdateStatus
- Security:
[{ sessionCookie: [] }, {}]— cookie whenupdates.requireAdminForStatus=true, otherwise anonymous OK. The conditional is documented in the description; clients that depend on receiving the full diagnostic payload should send the session cookie. - Responses:
200— JSON body matching theUpdateStatusschema below.401/403— only emitted whenupdates.requireAdminForStatus=true.
Response schema UpdateStatus mirrors the runtime shape returned by
src/node/hooks/express/updateStatus.ts:res.json({...}) on the base branch
(chore/admin-typesafe-api-7638-upstream, which mirrors develop's Tier 1):
UpdateStatus:
type: object
required: [currentVersion, installMethod, tier, vulnerableBelow]
properties:
currentVersion: { type: string }
latest: { $ref: '#/components/schemas/ReleaseInfo', nullable: true }
lastCheckAt: { type: string, format: date-time, nullable: true }
installMethod: { type: string, enum: [auto, git, docker, npm, managed] }
tier: { type: string, enum: [off, notify, manual, auto, autonomous] }
policy: { $ref: '#/components/schemas/PolicyResult', nullable: true }
vulnerableBelow:
type: array
items: { $ref: '#/components/schemas/VulnerableBelowDirective' }
Sub-schemas (ReleaseInfo, PolicyResult, VulnerableBelowDirective)
mirror the exported interfaces in src/node/updater/types.ts exactly:
ReleaseInfo:version,tag,body,publishedAt,prerelease,htmlUrl.PolicyResult:canNotify,canManual,canAuto,canAutonomous,reason.VulnerableBelowDirective:announcedBy,threshold.
The Tier 2 PR (#7607) will amend UpdateStatus to add execution,
lastResult, and lockHeld (with their corresponding sub-schemas) when it
ships its own changes to updateStatus.ts. Those fields are out of scope
here.
Public exposure (runtime)
openapi-admin.ts exports an expressPreSession hook that conditionally
mounts:
GET /admin/openapi.json (CORS: *)
The route is gated by settings.adminOpenAPI.enabled, default false,
per the project's "new features behind a flag, off by default" policy
(CONTRIBUTING.md, AGENTS.MD). When the flag is off,
expressPreSession returns early and the route is dormant.
When enabled, the route registers in expressPreSession, which runs before
expressCreateServer (where admin.ts registers the SPA wildcard
/admin/{*filename}). The earlier registration ensures
/admin/openapi.json resolves before the wildcard catches it.
Codegen does not depend on this route — dump-spec.ts calls
generateAdminDefinition() in-process. The route exists for downstream
tooling (Postman, swagger-ui, third-party clients) that operators choose to
expose.
Codegen merge
merge-openapi.mjs exports one function:
mergeOpenAPI(publicDoc, adminDoc) -> mergedDoc
Rules:
| Section | Rule |
|---|---|
paths |
Union by path key. Collision throws. |
components.schemas |
Union by name. Collision throws. |
components.parameters |
Union by name. Collision throws. |
components.responses |
Union by name. Collision throws. |
components.securitySchemes |
Union by name. Collision throws. |
security (root) |
Public spec's root security is preserved; admin paths declare their own per-operation security so admin requirements never apply to public paths. |
info, servers |
Public spec wins. |
Throwing on collision is intentional: silent overwrite is a footgun, and the backend test below catches collisions before merge runs in CI.
Tests
Backend — src/tests/backend/specs/openapi-admin.ts
Mocha specs against generateAdminDefinition(). No live HTTP.
- Document is valid OpenAPI 3.0 (smoke check via
openapi-schema-validation, already innode_modules). paths['/admin-auth/'].post.operationId === 'verifyAdminAccess'and declares responses200,401,403.paths['/admin/update/status'].get.operationId === 'getUpdateStatus'and references#/components/schemas/UpdateStatus.components.securitySchemescontainsbasicAuthandsessionCookie.components.schemas.UpdateStatus.propertiescontains every property name emitted byupdateStatus.ts:res.json({...}). Cross-checked by importing the same handler and asserting key parity. This is the regression net for spec/handler drift.- Admin operationIds and admin path keys do not collide with the public
spec (cross-loaded via
generateDefinitionForVersion). Cross-collision is impossible today (admin paths start with/admin, public paths are flat or/createGroup-style), but the test fails loudly if a future rename breaks the assumption.
Codegen merge — admin/scripts/__tests__/merge-openapi.test.mjs
Node --test runner (already used by #7695 for client.test.ts).
- Two minimal docs merge into the expected union.
- Path collision throws.
- Schema-name collision throws.
- Public root
securityis preserved when admin doc declares no root security. - Per-operation security on admin paths survives the merge unchanged.
No frontend tests this PR
No call-sites migrate, so there is nothing UI-observable to assert. Migration PRs add Playwright coverage when they touch each fetch.
Risks & mitigations
| Risk | Mitigation |
|---|---|
UpdateStatus schema drifts from updateStatus.ts over time |
Backend spec cross-checks property names against the handler. Tier 2 PR amends both spec and handler in one change. |
Tier 2 (#7607) rebase conflicts with the new openapi-admin.ts |
This PR adds only /admin/update/status. Tier 2 appends new entries — no conflict on the existing one. |
merge-openapi.mjs silently overwrites a duplicate |
Throws on collision. Backend spec cross-checks against the public spec. |
/admin/openapi.json collides with /admin/{*filename} SPA wildcard |
openapi-admin.ts registers in expressPreSession; admin.ts registers in expressCreateServer. Earlier hook wins. Backend smoke test confirms 200 + JSON content-type. |
| #7695 changes shape before it merges, breaking our base | This PR is stacked on #7695's branch. Rebase when #7695 rebases. PR description documents the dependency. |
express_sid is not the actual cookie name in some deployments |
Documented; spec is structurally correct; deployments that override it can still consume a typed client. |
Rollout
- Branch
feat/7693-admin-openapifromchore/admin-typesafe-api-7638-upstream. - Add
openapi-admin.ts,merge-openapi.mjs; modifydump-spec.ts. - Add backend spec and merge unit tests.
- Open PR #7693 as draft, base set to
chore/admin-typesafe-api-7638-upstream. - When PR #7695 merges to develop, change base to
develop, rebase, mark ready for review. - Follow-up PR (separately tracked) migrates the four admin
fetch()sites:LoginScreen.tsx,App.tsx,UpdateBanner.tsx,UpdatePage.tsx.
Open question deferred to implementation
The express_sid cookie name is the documented default but Etherpad
deployments can override it via settings. Implementation will read the
configured name at spec-generation time (or document the override path) so
the spec reflects the running configuration. If reading the configured name
is awkward at codegen time (it requires booting Settings), the spec keeps
the default and notes the override in the description.