# Public API — `mgf-fastapi`

> ⚠️ **This file documents `master` HEAD.** Names listed here may
> be unreleased. To learn what your installed wheel actually
> ships, run `pip show mgf-fastapi` and compare against the
> `__version__` field at the top of `PUBLIC_API.json`. The git
> tag matching the published version is authoritative.

This document is the **contract list** for `mgf-fastapi`. Every name
listed below is a public name that consumers MAY depend on.

The contract terms are defined in
[`mgf-standard/docs/standards/API_DESIGN.md`](https://codeberg.org/magogi-admin/mgf-standard/src/branch/main/docs/standards/API_DESIGN.md). In
short:

- **Stability tiers:**
  - `experimental` — MAY change shape in any MINOR release
    (the 0.x window keeps everything experimental in practice).
  - `stable` — follows the deprecation cycle; removal only in MAJOR.
  - `deprecated` — slated for removal; emits `DeprecationWarning`.
- **Names not listed below are private.** Anything starting with
  `_` (underscore-prefixed module names like `_lifespan.py`) is
  internal and MAY change without notice.
- **The 0.x window applies.** Per [SemVer](https://semver.org/) 0.x
  semantics, MINOR releases MAY break the public API. Pin tightly:
  `mgf-fastapi = ">=0.X.0,<0.Y"`.

---

## `mgf.fastapi`

Top-level FastAPI integration adapters. Re-exports the load-bearing
public names from the submodules. Closed-box: depends on
`mgf-common` + `fastapi` only.

| Name | Tier | Description |
|---|---|---|
| `setup_lifespan(*, app_name=None, app_version=None, settings=None)` | experimental | Build a FastAPI lifespan that runs `mgf.common.bootstrap()` at app startup. The resulting `AppContext` is stashed on `app.state.mgf_context` for dependency injection. LIFO unwind on shutdown. |
| `RequestIdMiddleware` | experimental | ASGI middleware. Injects/forwards `X-Request-Id` header (case-insensitive); generates UUID4 if absent. Sets `request.state.request_id` AND a contextvar shared with `mgf.common._request_id` (so cross-framework processes correlate end-to-end). Echoes in response. |
| `current_request_id() -> str` | experimental | Read the per-request id from the contextvar. Returns empty string outside a request. PEP 567 propagates across async children. |
| `REQUEST_ID_HEADER = "X-Request-Id"` | experimental | Canonical header name. Inbound case-insensitive; outbound canonical. |
| `ExceptionTranslationMiddleware` | experimental | ASGI middleware. Catches `AppError` from handlers; maps to typed JSON responses via `status_map` (default per category) AND falls back to `HttpError.http_status` (PAPER-24 resolution) for HTTP-01 leaves like `HttpUnauthorized`, `HttpForbidden`, `HttpNotFound`, etc. — including sibling-domain concretes like `HmacVerificationError` (401 via inheritance). Configurable via `status_map=` and `response_factory=` kwargs. Includes `request_id` in error body when `RequestIdMiddleware` is installed. |
| `DEFAULT_STATUS_MAP: dict[type[AppError], int]` | experimental | Default exception → HTTP status mapping. `SchemaValidationError`→400, `ResourceNotFoundError`→404, `ResourceAlreadyExistsError`→409, `HostEnvironmentError`→503, `ConfigError`/`OperationError`/`VaultError`/`AppError`→500. |
| `get_app_context(request) -> AppContext` | experimental | FastAPI dependency. Reads `request.app.state.mgf_context`; raises `AppConfigError` if unset (lifespan not wired). |
| `get_settings(request) -> MgfSettings` | experimental | FastAPI dependency. Returns the `MgfSettings` from the active context. |
| `get_request_id(request) -> str` | experimental | FastAPI dependency. Reads `request.state.request_id`; returns `""` if `RequestIdMiddleware` is not installed. |
| `HmacVerificationError` | experimental | Re-exported from `mgf.fastapi.exceptions`. Convenience top-level alias for the most-commonly-caught sibling-domain exception. |

---

## `mgf.fastapi.exceptions`

Sibling-domain concrete exception leaves. Per the federation rule
(decision **D3** in the [federation roadmap](https://codeberg.org/magogi-admin/mgf-common/src/branch/main/docs/release/federation_roadmap.md)),
siblings own their domain concretes; mgf-common owns hierarchy
roots + generic status-code leaves.

| Name | Tier | Description |
|---|---|---|
| `HmacVerificationError` | experimental | A webhook HMAC signature failed verification. Inherits `mgf.common.exceptions.HttpUnauthorized` (status 401). Raised by `mgf.fastapi.webhooks.HmacWebhookVerifier` on any failure mode (missing header, unparseable timestamp, replay-window miss, signature mismatch). PAPER-24's `http_status` resolution maps it to 401 in `ExceptionTranslationMiddleware` without an explicit `status_map` entry. |

---

## `mgf.fastapi.webhooks`

Svix-shape HMAC webhook verification. Pulls the per-consumer
verification dance into one tested primitive. Closes
[`mgf-common`'s PAPER-26](https://codeberg.org/magogi-admin/mgf-common/src/branch/main/FEEDBACK.md)
(originally shipped in mgf-common v0.26; moved to this sibling at
mgf-common v0.28 / mgf-fastapi v0.1).

| Name | Tier | Description |
|---|---|---|
| `WebhookHeaderSchema(id_header, timestamp_header, signature_header)` | experimental | Frozen dataclass naming the three Svix-style headers a provider uses. Header names are case-folded to lowercase on construction. |
| `SVIX_SCHEMA: WebhookHeaderSchema` | experimental | Pre-built schema for Svix's canonical headers (`svix-id` / `svix-timestamp` / `svix-signature`). Used by Clerk verbatim. Stripe / GitHub / Slack are deferred to follow-up papers. |
| `HmacWebhookVerifier(*, secret, schema=SVIX_SCHEMA, replay_window_seconds=300, signature_prefix="v1,", now=time.time)` | experimental | Transport-agnostic verifier. `verify(headers, body) -> (event_id, ts)`. Raises `HmacVerificationError` on any failure (missing header, unparseable timestamp, replay-window miss, signature mismatch). Stateless and thread-safe. **Cost (PF-06):** `verify` is **O(payload)** time (one HMAC-SHA256 over the whole body) and **O(payload)** transient memory; the FastAPI adapter `verify_request` caps the body at `max_body_bytes` (default 1 MiB) so the hot path is bounded. |
| `verify_request(request, verifier) -> (event_id, ts)` | experimental | Async FastAPI request adapter. Reads `await request.body()`, lifts `request.headers` into the verifier's input, returns `verifier.verify(...)`. Caller MUST NOT have consumed the body yet. |

See [`docs/recipes/webhooks.md`](docs/recipes/webhooks.md)
for the full walkthrough including key rotation, custom schemas,
and the consumer pattern for replacing per-provider verifiers.

---

## `mgf.fastapi.security`

Request-side security primitives for FastAPI. Closes
[`mgf-common`'s PAPER-27](https://codeberg.org/magogi-admin/mgf-common/src/branch/main/FEEDBACK.md)
(originally shipped in mgf-common v0.27; moved to this sibling at
mgf-common v0.28 / mgf-fastapi v0.1) and
[`mgf-common`'s PAPER-29](https://codeberg.org/magogi-admin/mgf-common/src/branch/main/FEEDBACK.md)
(`bearer_session` + `AuthVerifier`, shipped in mgf-fastapi v0.3).

| Name | Tier | Description |
|---|---|---|
| `IpAllowlist(cidrs=LOOPBACK_CIDRS, *, trust_proxy=False)` | experimental | FastAPI dependency that 403s requests whose client IP isn't in `cidrs`. Concentrates the proxy-trust footgun in one knob — `trust_proxy=False` default; honouring `X-Forwarded-For` requires explicit opt-in. IPv4 + IPv6 dual-stack with cross-version matches deliberately rejected. Misconfigured CIDRs raise at construction. **Cost (PF-06):** per-request `__call__` is **O(n)** in the number of configured networks (a linear CIDR scan) and O(1) memory; keep the allowlist small. |
| `LOOPBACK_CIDRS: tuple[str, ...]` | experimental | The default `cidrs=` for `IpAllowlist` — `("127.0.0.0/8", "::1/128")`. Loopback-only so tests work out-of-the-box; consumers OVERRIDE for production. |
| `bearer_session(verifier_attr="auth_provider", *, cookie_name=None)` | experimental | Factory: returns a FastAPI dependency that reads `Authorization: Bearer <token>` (case-insensitive scheme) and resolves the principal via `app.state.<verifier_attr>.verify_session(token)`. Optional `cookie_name` fallback for browser clients. Translates every failure mode (missing header, missing verifier, verifier-raised exception) to `HttpUnauthorized` → 401 via the HTTP-01 `http_status` fallback. Consumers recover precise return typing via `Annotated[MyPrincipal, Depends(bearer_session())]`. |
| `AuthVerifier[P]` | experimental | `typing.Protocol[P]` (covariant). The shape consumers implement: `async def verify_session(self, token: str) -> P`. `P` is consumer-defined (dataclass, pydantic model, tenant-membership tuple — whatever the app uses downstream). Used structurally; no inheritance required. |

---

## `mgf.fastapi.db`

FastAPI-shaped SQLAlchemy helpers — the bits of the old
`mgf.common.db` surface that need Starlette/FastAPI types at runtime.
Pure-SQLAlchemy helpers (`create_engine`, `create_sessionmaker`,
`tenant_session`) live in
[mgf-sqlalchemy](https://pypi.org/project/mgf-sqlalchemy/).

Optional `[sqlalchemy]` extra: `pip install 'mgf-fastapi[sqlalchemy]'`
(pulls `mgf-sqlalchemy>=0.1`). Without it, `import mgf.fastapi.db`
raises `ImportError` at module load time.

| Name | Tier | Description |
|---|---|---|
| `get_session(request) -> AsyncIterator[AsyncSession]` | experimental | FastAPI-Depends-compatible async generator. Yields one `AsyncSession` per request from `request.app.state.mgf_db_sessionmaker`. Closes via the context manager when the handler returns. Raises `mgf.common.exceptions.AppConfigError` if `setup_db` didn't run in the lifespan. Commit/rollback is the consumer's call — no global policy is imposed. |
| `setup_db(app, *, database_url, **engine_kwargs)` | experimental | Async context manager for FastAPI lifespans. Calls `mgf.sqlalchemy.create_engine(database_url, **engine_kwargs)` + `create_sessionmaker(engine)`, stashes them on `app.state.mgf_db_engine` / `app.state.mgf_db_sessionmaker`, and disposes the engine on exit. Compose inside a larger lifespan that also runs `mgf.common.bootstrap()`. |

---

## `mgf.fastapi.testing`

Test-server lifecycle helper. Closes
[`mgf-common`'s PAPER-28](https://codeberg.org/magogi-admin/mgf-common/src/branch/main/FEEDBACK.md)
(originally shipped in mgf-common v0.27; moved to this sibling at
mgf-common v0.28 / mgf-fastapi v0.1).

Optional `[testing]` extra: `pip install 'mgf-fastapi[testing]'`
(pulls `uvicorn>=0.30` + `httpx>=0.27`).

| Name | Tier | Description |
|---|---|---|
| `run_test_app(app, *, port=None, host="127.0.0.1", log_level="warning", startup_timeout_seconds=5.0)` | experimental | Async context manager. Starts uvicorn for `app` on a free port; yields `f"http://{host}:{port}"`; tears down cleanly on context exit (including under exception). `port=None` picks a free ephemeral port. Surfaces server-startup exceptions immediately instead of timing out silently. Raises `TimeoutError` if uvicorn doesn't reach `started` within `startup_timeout_seconds`. `CancelledError` from the serve task is suppressed on shutdown (expected). |

---

## Summary

| Module | Public name count |
|---|---|
| `mgf.fastapi` | 10 |
| `mgf.fastapi.exceptions` | 1 |
| `mgf.fastapi.webhooks` | 4 |
| `mgf.fastapi.security` | 4 |
| `mgf.fastapi.db` | 2 (gated behind `[sqlalchemy]` extra) |
| `mgf.fastapi.testing` | 1 |
| **Total** | **22** |

(`HmacVerificationError` counted once — at `mgf.fastapi.exceptions`. The
re-export at `mgf.fastapi` is a convenience alias.)

---

## Cross-references

- [`CHANGELOG.md`](CHANGELOG.md) — release history.
- [`docs/recipes/`](docs/recipes/) — full how-to for each surface.
- [`docs/cutover/v0.1.0.md`](docs/cutover/v0.1.0.md) — maiden voyage migration story (the v0.28 split).
- [`docs/cutover/v0.2.0.md`](docs/cutover/v0.2.0.md) — v0.2.0 paired-extraction migration (the v0.30 split, adding `mgf.fastapi.db`).
- [`docs/cutover/v0.3.0.md`](docs/cutover/v0.3.0.md) — v0.3.0 PAPER-29 (`bearer_session` + `AuthVerifier`).
- [`mgf-common/FEEDBACK.md`](https://codeberg.org/magogi-admin/mgf-common/src/branch/main/FEEDBACK.md) — design conversation queue.
- [`mgf-standard/docs/standards/`](https://codeberg.org/magogi-admin/mgf-standard/src/branch/main/docs/standards/) — the federation-wide engineering standards.
- [`mgf-common/docs/release/federation_roadmap.md`](https://codeberg.org/magogi-admin/mgf-common/src/branch/main/docs/release/federation_roadmap.md) — the split plan that created this sibling.

## `mgf.fastapi.logs`

Per-app web log surface (unified-logging Wave 3, mgf-fastapi v0.6.4). Optional
`[livepush]` extra (`pip install 'mgf-fastapi[livepush]'`); without it,
`import mgf.fastapi.logs` raises `ImportError`.

| Name | Tier | Notes |
|---|---|---|
| `DEFAULT_LOG_CHANNEL` | experimental | Default broker channel name for the log stream. |
| `LogBroadcaster` | experimental | Drains the logging pipeline to a broker (best-effort, never-raises). |
| `build_log_stream_service` | experimental | Wires the SSE log-stream service over mgf-livepush. |
| `create_logs_router` | experimental | FastAPI router exposing the per-app log SSE endpoint. |

## `mgf.fastapi` — rate limiting

Token-bucket rate-limit middleware (mgf-fastapi v0.6.0), re-exported at the
package top level.

| Name | Tier | Notes |
|---|---|---|
| `RateLimitMiddleware` | experimental | ASGI middleware enforcing a per-key token-bucket limit. |
| `TokenBucket` | experimental | The in-process token-bucket primitive (swap to a Redis-backed impl for multi-process). **Cost (PF-06):** O(1) time per check, but **O(k) memory unbounded in the number of distinct keys** seen (one bucket retained per key) — a high-cardinality key space is a memory-pressure vector; bound the key space or use a Redis-backed backend in production. |

> **Versioning.** `__version__` is the package version (mirrors `pyproject`),
> pinned coherent with the release tag in CI (`tag-coherence`).

