etherpad-lite/docs/superpowers/specs/2026-05-08-issue-7693-admin-openapi-design.md
John McLear 4f1b524864
feat(admin): document admin endpoints in OpenAPI (#7693) (#7705)
* chore(admin): typesafe API client + TanStack Query rails (#7638) (#2)

* docs(admin): design for typesafe API client + TanStack Query rails (#7638)

Rails-only scope: codegen toolchain, runtime client, and provider — no
call-site migrations. Admin endpoints are not yet covered by the OpenAPI
spec, so a separate issue will follow before any migration is useful.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* docs(admin): implementation plan for typesafe API client rails (#7638)

Step-by-step task breakdown for the rails-only PR: codegen toolchain,
runtime client, TanStack Query provider, CI freshness check, docs. No
call-site migrations until admin endpoints are added to the OpenAPI
spec (separate follow-up).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat(api): export generateDefinitionForVersion from openapi hook

Required by the admin codegen script (#7638) to dump the OpenAPI spec
without booting Express. No behavior change for the request hook.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* chore(admin): add OpenAPI codegen + TanStack Query deps (#7638)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* chore(admin): add OpenAPI spec dump entry (#7638)

Loaded via tsx by gen-api.mjs in the next commit. Writes JSON to a file
path argument so log4js stdout output (from Settings init) does not
pollute the spec output.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* chore(admin): wire OpenAPI codegen into build (#7638)

Adds gen:api script and amends build/build-copy to regenerate
admin/src/api/schema.d.ts before compiling. The generated file is
checked in so it shows up in PR review and so a fresh checkout doesn't
need codegen to typecheck.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat(admin): typed openapi-fetch + react-query client (#7638)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat(admin): TanStack Query provider, dev-only devtools (#7638)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat(admin): mount TanStack Query provider at root (#7638)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* test(admin): smoke test for typed openapi-fetch client (#7638)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* ci(admin): verify generated OpenAPI schema is up to date (#7638)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* docs(admin): document OpenAPI codegen workflow (#7638)

Replaces the default Vite scaffold README with admin-specific scripts
table and codegen workflow notes.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* build(admin): exclude __tests__ from tsc include (#7638)

The smoke test imports node:test/node:assert which need @types/node.
Admin source is browser-only, so excluding __tests__ from the production
typecheck is cleaner than adding Node types to the bundle config. The
test still runs under tsx, which doesn't share this constraint.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* chore: regenerate lockfile with pnpm 10 to restore overrides block (#7638)

Adding admin deps with pnpm 11 stripped the top-level \`overrides:\`
section from pnpm-lock.yaml, which CI uses pnpm 10 to verify. Result:
ERR_PNPM_LOCKFILE_CONFIG_MISMATCH on every job. Re-running pnpm 10
\`install --lockfile-only\` restores the overrides block; the new admin
package entries land in the same commit. Two stale lockfile entries
not present in package.json (\`serialize-javascript\` version pin and
\`uuid@<14.0.0\`) were normalized by the regen — package.json is the
source of truth for those.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* build(docker): preserve strictDepBuilds=false in trimmed workspace yaml (#7638)

Adding tsx as an admin devDep brings esbuild@0.27.x into the resolved
subgraph, and the Dockerfile's runtime stage was overwriting
pnpm-workspace.yaml with a stripped-down version that lost the
strictDepBuilds=false setting from the source repo. With pnpm 10's
default of strictDepBuilds=true, the install then errors on
ERR_PNPM_IGNORED_BUILDS for esbuild + scarf rather than warning.

Restore the strictDepBuilds=false and the @scarf/scarf ignore in the
trimmed yaml so the production install matches develop's behavior.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(admin): point client baseUrl at /api/<version> via codegen (#7638)

Qodo flagged: with baseUrl='/' and schema paths like '/createGroup',
calls landed at /createGroup, but the backend mounts the FLAT-style
spec under /api/<version>/. So once a call site lands, every request
404s.

gen:api now also emits admin/src/api/version.ts containing
LATEST_API_VERSION (read from info.version in the spec) and a derived
API_BASE_URL = `/api/<version>`. client.ts imports API_BASE_URL.
Workflow freshness check covers both generated files.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* build(admin): cross-platform spawn in gen-api.mjs (#7638)

Windows CI failed because spawnSync('pnpm', ...) cannot resolve
pnpm.cmd without a shell. Set shell:true on win32 only so Linux/macOS
runs avoid Node's DEP0190 warning. All spawn args are literal strings,
so the shell variant is not an injection risk.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* chore(admin): gitignore generated schema/version, regen on every script (#7638)

Qodo flagged the committed admin/src/api/schema.d.ts as a build artifact
that violates the rule against committing generated files (rule 467291,
"Exclude build artifacts and runtime-generated files from version control").

This commit:
- Adds admin/src/api/{schema.d.ts,version.ts} to .gitignore.
- Removes both from VCS (the prior squash had committed them).
- Chains \`gen:api\` into the dev and test scripts so a fresh checkout
  lands a working dev server / test run without an extra step. build
  and build-copy already chained gen:api.
- Drops the now-redundant CI freshness diff step from
  frontend-admin-tests.yml — with the files no longer committed, the
  build step's gen:api invocation is the only check needed.
- Updates admin/README.md to describe the new generated-file workflow.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* docs(admin): design for admin OpenAPI coverage (#7693)

Adds the design doc for documenting `/admin-auth/*` and `/admin/update/status`
in the OpenAPI spec so the typed client generated by #7695 (`admin/src/api/
schema.d.ts`) gains admin call-sites the day it lands.

This PR is stacked on #7695 — codegen rails must merge first. Schema-only,
no call-site migrations (those are an explicit follow-up named in #7693).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* docs(admin): correct UpdateStatus schema to base-branch shape (#7693)

The first draft mirrored the Tier 2 (#7607) response shape, but this PR
stacks on #7695 whose updateStatus.ts only emits the Tier 1 fields. Fix
the schema, install-method enum, and tier enum to match types.ts and
the actual handler. Tier 2 amends UpdateStatus when it lands.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* docs(admin): implementation plan for admin OpenAPI coverage (#7693)

Step-by-step task breakdown for the schema-only PR: stub the document,
add /admin-auth/ + /admin/update/status paths and sub-schemas,
collision regression, /admin/openapi.json route, codegen merge, and CI
verification. No call-site migrations.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat(admin): stub OpenAPI document for admin endpoints (#7693)

Adds generateAdminDefinition() returning a minimal valid OpenAPI 3.0
document with no paths yet, plus security schemes for the two auth
modes (Basic + session cookie). Subsequent tasks fill in the actual
admin paths.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat(admin): document POST /admin-auth/ in OpenAPI (#7693)

Adds verifyAdminAccess as the operation that the admin UI's LoginScreen
and App session check both call. Documents Basic auth, session cookie,
and anonymous request modes plus their 200/401/403 responses.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat(admin): document GET /admin/update/status in OpenAPI (#7693)

Adds getUpdateStatus operation plus UpdateStatus, ReleaseInfo,
PolicyResult, and VulnerableBelowDirective sub-schemas. Property names
and enums mirror src/node/updater/types.ts and the response object
emitted by updateStatus.ts. Tier 2 (#7607) will amend UpdateStatus when
it ships execution/lastResult/lockHeld.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* test(admin): regression net for admin/public OpenAPI collisions (#7693)

Cross-checks admin paths, operationIds, and schema names against the
latest public spec. Today there are no overlaps; the test exists to
catch future renames before they break the merged client codegen.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat(admin): expose admin OpenAPI doc at /admin/openapi.json (#7693)

Mounts the admin OpenAPI document at /admin/openapi.json (CORS: *) via
an expressPreSession hook, matching the /api/openapi.json convention.
The admin SPA wildcard at /admin/{*filename} registers later in
expressCreateServer, so the JSON route wins. Live-route test confirms
JSON content-type and CORS header.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat(admin): mergeOpenAPI helper for codegen pipeline (#7693)

Pure-JS deep-merge of two OpenAPI 3.0 documents. Unions paths and
components by key; throws on collisions. Public document's info,
servers, and root security win over the admin document's. Used by
dump-spec.ts to produce a single merged JSON for openapi-typescript.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* docs(admin): fix stale "Section 3" reference in spec (#7693)

Drafting numbered the design walkthrough; final spec uses titled
sections. Update the back-reference accordingly.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat(admin): include admin OpenAPI in generated client (#7693)

Modifies dump-spec.ts to import generateAdminDefinition alongside the
public generator and feed both through mergeOpenAPI before writing the
JSON consumed by openapi-typescript. The resulting admin/src/api/
schema.d.ts paths interface now exposes /admin-auth/ and
/admin/update/status, ready for typed call-site adoption in a follow-up.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(admin): gate /admin/openapi.json behind a feature flag (#7693)

Address Qodo finding 1: new features must ship behind a flag, disabled
by default (CONTRIBUTING.md, AGENTS.MD, best_practices.md). Adds
settings.adminOpenAPI.enabled (default false). expressPreSession
returns early when the flag is off, so the route is dormant on a fresh
install.

The codegen pipeline imports generateAdminDefinition() in-process and
does not depend on the runtime route, so default-off has no effect on
the typed client. Operators who want third-party tooling (Postman,
swagger-ui, downstream clients) to consume the spec at runtime opt in
via settings.

Adds:
- SettingsType + defaults entry in src/node/utils/Settings.ts
- settings.json.template documentation
- A backend spec asserting expressPreSession is a no-op when the flag
  is off (live route test now sets the flag explicitly)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(admin): split fetchClient by surface; admin client baseUrl='/' (#7693)

Address Qodo finding 2 (correctness bug). The merged schema introduced
in this PR exposes both public-API paths (under /api/<version>/) and
admin paths (at root, e.g. /admin-auth/). The single fetchClient from
#7695 has baseUrl=API_BASE_URL ("/api/<version>"), so calling an admin
path through it would resolve to /api/<version>/admin-auth/ — wrong.

Fix:
- Narrow the generated `paths` interface by URL prefix
  (`/admin*` vs everything else).
- Export two clients: `fetchClient` (public, baseUrl=/api/<version>)
  and `adminFetchClient` (admin, baseUrl='/').
- Mirror with `$api` / `$adminApi` query hook factories.

TypeScript now rejects mixing surfaces at compile time:
  fetchClient.GET('/admin-auth/')        // type error: not in PublicPaths
  adminFetchClient.GET('/createGroup')   // type error: not in AdminPaths

Smoke test updated to assert all four exports exist.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* docs(admin): spec — feature flag + dual-client design (#7693)

Reflect Qodo-driven changes:
- /admin/openapi.json route is now feature-flagged (default off).
- admin/src/api/client.ts splits paths by URL prefix and exports two
  clients (public + admin) so TypeScript rejects mixing surfaces.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(admin): check adminOpenAPI flag per-request, not at hook setup (#7693)

The first feature-flag commit (dbcfb3902) gated the route by checking
the flag inside expressPreSession and skipping app.get() when off. That
broke under the full backend suite because common.init() is cached: the
first spec that calls it boots the server with whatever flag state was
present at that moment, and later specs cannot retroactively register
the route by toggling the flag.

Move the flag check inside the request handler:

- Route is always registered.
- Handler returns 404 application/json when the flag is off (so callers
  get a clear "feature disabled" signal rather than the SPA wildcard's
  text/html catch-all).
- Handler returns the spec + CORS * when the flag is on.

Tests now toggle the flag in-process and exercise both states against
the shared agent — no server restart needed. Added a "returns 404 JSON
when off" test alongside the existing "200 + spec + CORS when on".

Full backend suite: 1007 passing, 6 pending, 0 failures.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-10 15:55:33 +08:00

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 by LoginScreen.tsx and App.tsx).
  • GET /admin/update/status — Tier 1 update banner data (consumed by UpdateBanner.tsx and UpdatePage.tsx; introduced by #7601, merged on develop).

Out:

  • /admin/update/{apply,cancel,acknowledge,log} — Tier 2 endpoints from the in-flight feat/7607-auto-update-tier2-manual-click branch. That PR amends openapi-admin.ts when 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 replies 401.
  • 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/statusgetUpdateStatus

  • Security: [{ sessionCookie: [] }, {}] — cookie when updates.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 the UpdateStatus schema below.
    • 401 / 403 — only emitted when updates.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, best_practices.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 in node_modules).
  • paths['/admin-auth/'].post.operationId === 'verifyAdminAccess' and declares responses 200, 401, 403.
  • paths['/admin/update/status'].get.operationId === 'getUpdateStatus' and references #/components/schemas/UpdateStatus.
  • components.securitySchemes contains basicAuth and sessionCookie.
  • components.schemas.UpdateStatus.properties contains every property name emitted by updateStatus.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 security is 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

  1. Branch feat/7693-admin-openapi from chore/admin-typesafe-api-7638-upstream.
  2. Add openapi-admin.ts, merge-openapi.mjs; modify dump-spec.ts.
  3. Add backend spec and merge unit tests.
  4. Open PR #7693 as draft, base set to chore/admin-typesafe-api-7638-upstream.
  5. When PR #7695 merges to develop, change base to develop, rebase, mark ready for review.
  6. 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.