first commit

This commit is contained in:
2026-05-29 08:20:56 +01:00
commit 949299f58d
6368 changed files with 527176 additions and 0 deletions

View File

@@ -0,0 +1,90 @@
# RFC 7807 problem+json renderer
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
Render `CmsError` (and arbitrary throwables) as RFC 7807 problem+json
HTTP responses without requiring `@suaveplan/api-plugin-cms` to depend
on any HTTP framework. The renderer accepts a small structural
`HttpResponseLike` interface that is compatible with Hono, Fastify,
Bun's stdlib `Response`, and any framework whose response object
exposes `header(name, value)` plus `body(data, status)`. Status
mapping is encoded inline as a `Record<string, number>` lookup table
that the package-level spec-conformance test asserts is exhaustive
over `CMS_CODES`.
## Features
- Pure `formatProblemJson(err)` returns the JSON body for logging or
side-channel use.
- `respondProblemJson(res, err)` writes header + body and returns the
framework's `Response` for `return res.body(...)` style handlers.
- `PROBLEM_JSON_CONTENT_TYPE` constant — `application/problem+json` per
RFC 7807.
- Local `ProblemDetails` interface matching the IETF spec, with an
extension-index signature so callers can add fields per RFC §3.2.
- Status table:
- `CMS.CONTENT_NOT_FOUND` / `CMS.MODEL_NOT_FOUND` → 404
- `CMS.INVALID_TRANSITION` / `CMS.DUPLICATE_SLUG` / `CMS.MODEL_IN_USE` → 409
- `CMS.VALIDATION_FAILED` → 400
- `CMS.INVALID_INPUT` → 500
- Generic `SuaveplanError` instances render with HTTP 500 but with the
original code so log scrapers still see structure.
- Non-error throwables render `{ type: "about:blank", status: 500 }`.
## Usage: Basic
```ts
import { Hono } from "hono";
import { respondProblemJson } from "@suaveplan/api-plugin-cms";
const app = new Hono();
app.onError((err, c) => respondProblemJson(c, err));
```
## Usage: Advanced
Side-channel logging without writing to a response (e.g. forwarding
the body to an audit pipeline):
```ts
import { formatProblemJson } from "@suaveplan/api-plugin-cms";
try {
engine.transitionStatus(id, target);
} catch (err) {
const body = formatProblemJson(err);
await audit.log({
type: "cms.transition.failed",
severity: body.status >= 500 ? "error" : "warning",
...body,
});
throw err;
}
```
## API Reference
| Symbol | Description |
|---|---|
| `PROBLEM_JSON_CONTENT_TYPE` | `"application/problem+json"`. |
| `interface ProblemDetails` | RFC 7807 body shape with an extension index. |
| `interface HttpResponseLike` | `{ header(n, v); body(d, s): Response }`. |
| `respondProblemJson(res, err)` | Write header + body; return `Response`. |
| `formatProblemJson(err)` | Pure variant. |
## Implementation Notes
The status lookup is a `Readonly<Record<string, number>>` keyed by the
fully-qualified `CMS.*` code. If a `CmsError` is constructed with a
code not in the table (unreachable in normal use; possible only in
tests that bypass `defineCodes`), the renderer falls back to HTTP 500.
Generic `SuaveplanError` instances render with HTTP 500 + the
original `code` field so the response is still grep-friendly. The
`HttpResponseLike` shape was chosen over `Context` (Hono) to keep
this package free of framework dependencies — see
`@suaveplan/api-plugin-database`'s `routes/problem-json.ts` for the
same pattern.