# @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" }`. ## 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,… ## 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