docs/api-reference.md was hand-written, and by the time anyone checked it was documenting twenty-three endpoints that answer 404 while missing others that exist. That is what hand-written reference material does: it is correct on the day it is written and silently wrong afterwards. A second hand-written document, in YAML this time, would rot the same way. So paths, methods and mount points are read from the Express router stack at request time. They cannot disagree with the app, because they are the app: 186 paths, 215 operations, and — checked — no /learning endpoints, which is what the prose version went on claiming for weeks after that feature was deleted. What introspection cannot know is what an endpoint is *for*. That half lives in src/utils/openapiRoutes.js, keyed by "METHOD /path", and it is the half that rots, so it is the half that is enforced: a Playwright spec fetches the live document and fails when the number of operations without a summary rises above 199 — the debt as measured today. A ratchet, not a target. Adding an endpoint pushes the count over and fails the build; describing one lowers the number. The failure lists the operations by name, so it says what to write. Whether an operation is public is stated per route rather than inferred from middleware. Guessing wrong there is worse in both directions: calling a public endpoint protected hides a hole, and the reverse invites a bug report. The contract spec lives in e2e rather than the unit suite because it needs the whole app mounted, and requiring server.js from node:test pulls in the database pool and hangs the run — that has happened here before. Also: e2e now runs in CI on dev, gated by a shell check inside the step rather than a job-level "if", which this Forgejo dispatches anyway and then kills with "Early termination". Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Dv6sqaY6Vq3ChZHMem3cnU
78 lines
4.1 KiB
JavaScript
78 lines
4.1 KiB
JavaScript
// ============================================================
|
|
// OPENAPI — what each endpoint is for
|
|
// ============================================================
|
|
// Paths and methods are read from the router (src/utils/openapi.js). This is
|
|
// the half introspection cannot supply: what an operation does, what it takes,
|
|
// and whether it needs a session.
|
|
//
|
|
// Keyed by "METHOD /path" exactly as the route is mounted, with :params left as
|
|
// they appear in the code. A route with no entry here fails test/openapi.test.js
|
|
// — that is the point. A new endpoint cannot ship without a sentence saying what
|
|
// it is, and an entry for an endpoint that no longer exists fails the same test
|
|
// from the other direction.
|
|
//
|
|
// `public: true` marks an operation that works with no session. Say it
|
|
// explicitly: guessing from middleware gets it wrong in the direction that
|
|
// hides a hole.
|
|
|
|
// Path parameters, described once rather than at every route that takes them.
|
|
var parameters = {
|
|
id: 'Identifier of the record, scoped to the signed-in account.',
|
|
workflow: 'Which feature the job belongs to (for example my_resources).',
|
|
key: 'Setting key, for example clinical_assistant.chat_model.',
|
|
slug: 'URL-safe name of the document.'
|
|
};
|
|
|
|
var operations = {
|
|
// ── Session ─────────────────────────────────────────────────────────
|
|
'POST /api/auth/login': {
|
|
summary: 'Sign in with a password',
|
|
description: 'Returns a token and sets the ped_auth cookie. Rate limited.',
|
|
public: true
|
|
},
|
|
'POST /api/auth/login-code/request': {
|
|
summary: 'Email a single-use sign-in code',
|
|
description: 'Always answers the same way whether or not the address has an account, so it cannot be used to discover who is registered.',
|
|
public: true
|
|
},
|
|
'POST /api/auth/login-code/verify': {
|
|
summary: 'Exchange a sign-in code for a session',
|
|
public: true
|
|
},
|
|
'POST /api/auth/register': {
|
|
summary: 'Create an account',
|
|
description: 'Requires an invitation code while registration is invite-only.',
|
|
public: true
|
|
},
|
|
'GET /api/auth/registration-status': {
|
|
summary: 'Whether registration is open, and whether it needs an invitation',
|
|
description: 'Read by the sign-in screen to decide whether to offer the register link.',
|
|
public: true
|
|
},
|
|
'POST /api/auth/logout': { summary: 'End this session' },
|
|
'GET /api/auth/me': { summary: 'The signed-in account' },
|
|
|
|
// ── Clinical assistant ──────────────────────────────────────────────
|
|
'POST /api/clinical-assistant/ask': {
|
|
summary: 'Ask a question against the clinical corpus',
|
|
description: 'Streams the answer as server-sent events, with the retrieved sources.'
|
|
},
|
|
|
|
// ── My Resources ────────────────────────────────────────────────────
|
|
'GET /api/my-resources': { summary: 'Teaching material belonging to this account' },
|
|
'GET /api/my-resources/:id': { summary: 'One saved resource' },
|
|
'POST /api/my-resources/generate': { summary: 'Generate a deck or handout' },
|
|
'POST /api/my-resources/:id/refine': { summary: 'Revise a saved resource' },
|
|
'GET /api/my-resources/:id/export': { summary: 'Download as PowerPoint, Word or PDF' },
|
|
'GET /api/my-resources/theme-sample/:id': {
|
|
summary: 'A sample deck in one theme',
|
|
description: 'Every slide layout with placeholder text, as a PowerPoint file, so a theme can be judged before it is used.'
|
|
},
|
|
'POST /api/my-resources/:id/to-nextcloud': { summary: 'Send the rendered file to the owner\'s Nextcloud' },
|
|
|
|
// ── Health ──────────────────────────────────────────────────────────
|
|
'GET /api/health': { summary: 'Liveness', public: true },
|
|
'GET /api/build': { summary: 'The revision this container is running', public: true }
|
|
};
|
|
|
|
module.exports = { operations: operations, parameters: parameters };
|