86 lines
4.9 KiB
Markdown
86 lines
4.9 KiB
Markdown
# @suaveplan/api-rbac - Role/scope/rule-based authorisation middleware for the Suaveplan API plugin system. Produces a framework-agnostic `Middleware` that consumes the resolved `Principal` plus a route's `RouteAuthSpec` (scope name, @suaveplan/rules Rule, or public bypass), emits 401 when a non-public route has no principal, 403 on missing-scope or rule-deny, and increments `api.rbac.denials_total{plugin, route, reason}`. Ships a conflict-aware `ScopeRegistry` plus `registerPluginScopes(registry, manifest)` so plugin-declared scopes auto-register at install and duplicates raise `RBAC.SCOPE_CONFLICT`. Universal, Zod-validated boundaries, real OTel surface.
|
|
|
|
Scope + rule-based authorisation middleware for the Suaveplan API plugin
|
|
system. Produces a framework-agnostic `Middleware` that consumes the
|
|
resolved `Principal` from `RequestContext` and the matched route's
|
|
`RouteAuthSpec`, performs the gate, and either calls `next()` or throws a
|
|
typed `RbacError`. Every denial increments
|
|
`api.rbac.denials_total{plugin, route, reason}` with `reason ∈
|
|
{ "missing-scope", "rule-deny", "no-principal" }`.
|
|
|
|
<!-- begin:auto-docs -->
|
|
## Documentation tree
|
|
|
|
- [error-codes](./src/error-codes/error-codes.md) — Stable, frozen registry of every `RBAC.*` error code surfaced by the `@suaveplan/api-rbac` authorisation middleware and…
|
|
- [error-codes](./src/error-codes/error-codes.md) — Stable, frozen registry of every `RBAC.*` error code surfaced by the `@suaveplan/api-rbac` authorisation middleware and…
|
|
- [instruments](./src/instruments/instruments.md) — Owns the single OpenTelemetry counter `api.rbac.denials_total` and the factory that constructs the `RbacInstruments` re…
|
|
- [instruments](./src/instruments/instruments.md) — Owns the single OpenTelemetry counter `api.rbac.denials_total` and the factory that constructs the `RbacInstruments` re…
|
|
- [middleware](./src/middleware/middleware.md) — Ships `createRbacMiddleware(options)` — the per-request authorisation gate. The factory produces a framework-agnostic `…
|
|
- [middleware](./src/middleware/middleware.md) — Ships `createRbacMiddleware(options)` — the per-request authorisation gate. The factory produces a framework-agnostic `…
|
|
- [registry](./src/registry/registry.md) — Owns the shared, conflict-aware registry of plugin-declared scopes plus the `registerPluginScopes(registry, manifest)`…
|
|
- [registry](./src/registry/registry.md) — Owns the shared, conflict-aware registry of plugin-declared scopes plus the `registerPluginScopes(registry, manifest)`…
|
|
- [spec](./src/spec/spec.md) — Defines the canonical discriminated `RouteAuthSpec` union, its Zod schema, the inbound shape adapters fold down to it,…
|
|
- [spec](./src/spec/spec.md) — Defines the canonical discriminated `RouteAuthSpec` union, its Zod schema, the inbound shape adapters fold down to it,…
|
|
<!-- end:auto-docs -->
|
|
|
|
## Install
|
|
|
|
```bash
|
|
bun add @suaveplan/api-rbac
|
|
```
|
|
|
|
## Quick start
|
|
|
|
```ts
|
|
import { createRbacMiddleware } from "@suaveplan/api-rbac";
|
|
|
|
const middleware = createRbacMiddleware({
|
|
routeFor: () => ({
|
|
plugin: "billing",
|
|
path: "/invoices",
|
|
auth: { kind: "scope", scope: "billing:read" },
|
|
}),
|
|
});
|
|
```
|
|
|
|
The middleware is the abstract `Middleware` from `@suaveplan/api-types` —
|
|
adapters (`@suaveplan/api-router-hono`, `api-router-in-memory`, etc.) plug
|
|
it into their pipeline directly.
|
|
|
|
## Modules
|
|
|
|
- [`error-codes`](./src/error-codes/error-codes.md) — `RBAC_CODES` registry
|
|
(`MISSING_SCOPE`, `RULE_DENY`, `NO_PRINCIPAL`, `SCOPE_CONFLICT`,
|
|
`INVALID_SPEC`) and the `RbacError` class.
|
|
- [`instruments`](./src/instruments/instruments.md) — the single counter
|
|
`api.rbac.denials_total` plus `createRbacInstruments` / `recordRbacDenial`.
|
|
- [`middleware`](./src/middleware/middleware.md) — `createRbacMiddleware`
|
|
and `statusForRbacError` mapping.
|
|
- [`registry`](./src/registry/registry.md) — `ScopeRegistry` plus
|
|
`registerPluginScopes(registry, manifest)` for install-time conflict
|
|
detection.
|
|
- [`spec`](./src/spec/spec.md) — the canonical `RouteAuthSpec` union, the
|
|
Zod `RouteAuthSpecSchema`, and the `normaliseRouteAuth` fold.
|
|
|
|
## OpenTelemetry
|
|
|
|
- counter `api.rbac.denials_total{plugin, route, reason}` — emitted on
|
|
every deny path.
|
|
|
|
Reason label values: `"missing-scope" | "rule-deny" | "no-principal"`.
|
|
|
|
## Error codes
|
|
|
|
| Code | HTTP | When |
|
|
| ---------------------- | ---- | -------------------------------------------------------- |
|
|
| `RBAC.NO_PRINCIPAL` | 401 | non-public route reached with `principal === null` |
|
|
| `RBAC.MISSING_SCOPE` | 403 | principal lacks the required scope |
|
|
| `RBAC.RULE_DENY` | 403 | `@suaveplan/rules` evaluation returned `passed === false`|
|
|
| `RBAC.SCOPE_CONFLICT` | 500 | two plugins claim the same scope name |
|
|
| `RBAC.INVALID_SPEC` | 500 | `RouteAuthSpec` failed Zod validation |
|
|
|
|
## License
|
|
|
|
MIT
|
|
|