Ctrl K
ComunidadGuías…

API.md

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
# Good Phorum API Preparation

Selected page routes return JSON when requested. Use the existing response
contracts below; a separate versioned REST API remains planned. New API surfaces
require explicit contracts and tests.

## Versioning

Reserve this prefix for future REST routes:

```text
/api/v1/...
```

Versioned APIs must preserve:

- stable JSON response envelopes;
- explicit authentication and authorization;
- CSRF-free token auth only for non-browser clients;
- browser routes continuing to use CSRF-protected sessions;
- event/audit consistency for writes.

## Internal Contracts

Presenters and controller response helpers maintain the current JSON contracts.
Renaming an existing field requires a compatibility test.

### Private composer browser routes

The composer uses the following session-authenticated routes. Request JSON with
`Accept: application/json`; native form responses remain available. Every draft
or preview response sends `Cache-Control: private, no-store`. POST requests need
the browser's CSRF field and pass the existing participation/rate-limit guards.
These routes do not introduce `/api/v1` or token authentication.

| Method and path | JSON response / input |
| --- | --- |
| `GET /drafts` | `{drafts: [...]}` for the current owner's unexpired drafts. |
| `GET /drafts/:draft_id` | `{draft: {...}}`; another owner's or expired draft returns 404. |
| `POST /drafts` | `{ok, status, draft, error}`; composer fields include `draft_id`, `draft_revision`, `kind`, category/thread context, title, `body_source`, visibility and `command_id`. |
| `POST /drafts/:draft_id/delete` | Result envelope; needs the current `draft_revision`. |
| `POST /composer/preview` | `{ok: 1, html}` from the canonical safe body renderer; accepts composer fields and does not save/read a draft. |

Revision zero creates a draft; subsequent saves/delete require its current
revision. A stale revision returns 409 and invalid input returns 400. An exact
save retry returns its previous acknowledgement. Use the returned revision and
publication command id for subsequent requests. Owner ids are supplied by the
session, never by the form. The [composer guide](docs/architecture/composer-drafts.md)
describes limits, native responses, expiry and atomic publication, including the
completed local HTTP, PostgreSQL, JavaScript and browser checks. Deployment and
release gates remain separate.

## Token Architecture

Future bot/mobile integrations should use a dedicated token boundary:

- token hashes stored server-side;
- scoped permissions through RBAC/ABAC;
- short-lived access tokens where possible;
- revocation audit;
- rate limits per token, actor, and IP class.

Session cookies remain the browser default.

### Notification badge browser route

`GET /notifications/summary` returns `{ "unread_count": 3 }` for the signed-in
member. The existing permission-aware unread query stops at 100; the interface
shows `99+` at that threshold. No inbox bodies or recipient identifiers are
returned. Responses use `Cache-Control: private, no-store`; anonymous requests
return 401, rate-limited requests 429, and unavailable storage 503. The route
shares the existing notification HTTP limit (120 requests per 60 seconds).

The navigation normally subscribes to the signed-in member's notification
channel over the same-origin `/realtime` WebSocket. Its snapshot and subsequent
`notification.badge` frames update the badge without HTTP polling. The page
advertises this transport only when the realtime listener is enabled. See
[the browser client lifecycle](docs/realtime.md#browser-notification-client).

If WebSocket is unsupported, unavailable or denied, this summary is the
fallback, at most once per minute while visible. Requests coalesce; hidden
tabs release their socket and timers. The inbox reuses its server-rendered
count until a snapshot or fallback updates it. An HTTP response begun before
a push cannot replace the newer count. Temporary failures retain the last
known badge; no notification data is persisted in browser storage.


## Community completion (ADR 0151)

All writes require the existing session, CSRF token, write throttling and a fresh
UUID `command_id`. JSON is selected with `Accept: application/json`; mutations
are PostgreSQL function calls with atomic authorization and command replay.

| Method | Path | Purpose |
| --- | --- | --- |
| GET/POST | `/admin/registration` | Staff policy, email-bound invitations and admission decisions. POST `operation`: policy/invite/revoke/approve/reject. |
| POST | `/c/:category_id/subscription` | `operation`: follow/mute/remove using the existing category subscription. |
| GET/POST | `/t/:thread_id/tools/poll` | Read/create/vote/close a single-choice poll. POST `operation`, with question/options (one option per line) or choice. |
| GET/POST | `/moderation/transfers` | Global thread moderators inspect history or move/split/merge. Forms accept source/target discussion links and comma-separated post numbers. |
| POST | `/moderation/transfers/:transfer_id/undo` | Checked reversal; intervening placement/audience changes may conflict. |

Individual ballots and invitation secrets are absent from public results. An
invitation secret is returned only on its first successful creation response;
replay returns its id. Staff-only pages and poll responses use no-store.
See [ADR 0151](docs/adr/0151-admission-polls-category-follows-and-thread-organization.md)
for bounds, audiences, rollback and privacy semantics.

### Discussion navigation and scoped search

`GET /t/:thread_id?at=N` opens the first readable message at or after the
positive ordinal `N`, using the existing indexed keyset and bounded page size.
An ordinal is not a count of messages visible to the reader. Hidden or private
messages remain subject to the ordinary thread and post permissions.

`GET /t/:thread_id?post=UUID` resolves a readable post in that thread and opens
its page. Missing, inaccessible and mismatched posts return the same not-found
response. Both navigation modes bypass the first-page public response cache.

`GET /search?q=TEXT&thread_id=UUID` restricts search to an accessible discussion.
The thread filter is applied before candidate and result limits. Malformed or
inaccessible discussion identifiers return not found instead of broadening the
search. Apply migration 1030 and refresh runtime function grants before deploying
the updated search catalogue.

The reply editor suggests participants already visible on the page after `@`.
Typing `[[` followed by at least three characters requests up to five discussion
titles from the existing permission-aware, rate-limited autocomplete endpoint.
Selection inserts ordinary source text and follows the normal draft and publish
paths. Both navigation forms and scoped search work without JavaScript.

## Portable archives and protocol discovery

| Method | Path | Result |
| --- | --- | --- |
| GET | `/t/:thread_id/archive.mbox` | Complete authorized thread as a downloadable mboxrd archive. |
| GET | `/c/:category_id/archive.mbox` | Complete authorized category archive, with a stable category List-ID. |
| GET | `/p/:post_id/signature` | Detached Ed25519 envelope for the currently readable post revision; null for an unsigned legacy revision. |
| GET | `/.well-known/gpforum-signing-keys` | Current and archived trusted public JWKs, retained independently of the database. |
| POST | `/websub` | Subscription form gateway to the configured trusted HTTPS hub for the fixed public Atom topic. |

Archives and post attestations apply the existing space, category, thread and
post permissions. Private responses use `Cache-Control: private, no-store`.
Exports verify all persisted attestations before delivery; a mismatch fails
the response rather than returning a newly signed altered copy. Archive
messages contain pseudonymous sender addresses and do not expose account
email addresses. Legacy revisions can be attested with `gpforum sign-posts`.

With `GPFORUM_WEBSUB_HUB_URL` set, `/feed.atom` advertises the hub in its Atom
and HTTP Link metadata; the outbox publishes updates. The hub handles callback
verification, subscription leases and signed deliveries. The local gateway
accepts URL-encoded WebSub subscription parameters and answers 202 only after
the hub accepts them. It answers 404 when disabled, 400 for invalid forms,
413 for oversized requests, 415 for another media type, 429 for a rate limit
and 502 for upstream failure. Good Phorum never contacts subscriber callback URLs.

See [community protocol operations](docs/ops/community-protocols.md) for mail
reply capabilities, Gemini TLS, key retention and verified backup setup.

## Fixed plugin API

The in-process extension boundary is [Plugin API v1](docs/plugin-api-v1.md),
with a [closed JSON Schema](etc/plugins/api-v1.schema.json) and runtime
validation. Eight independent modules implement the same immutable URL request
and plain response contract. No core service, controller, database handle,
secret, raw HTML or callback crosses that boundary. This is separate from the
future external HTTP API and grants no bot or publishing authority.

Link modules implement `link.resolve.v1`; the separate Mathematics module
implements `math.render.v1` with immutable source requests and bounded,
validated equation spans. Core owns local KaTeX rendering and MathML output.

Administrator GUI routes manage core-owned settings under browser session
authentication: `GET /admin/plugins`, `POST /admin/plugins/:plugin`,
`POST /admin/plugins/probe/link`, `GET /admin/configuration`,
`POST /admin/plugins/probe/math`,
`POST /admin/configuration`, and
`POST /admin/configuration/:setting/reset`. Writes require global admin
manage permission, CSRF, participation, rate limits, command ids and revision
preconditions. General settings save atomically; secrets never appear in
responses. These are administrator browser workflows, not public integration
endpoints. See [the management guide](docs/integrations.md).