Files
2026-05-27 23:29:53 +01:00
..
2026-05-27 23:29:53 +01:00
2026-05-27 23:29:53 +01:00
2026-05-27 23:29:53 +01:00
2026-05-27 23:29:53 +01:00
2026-05-27 23:29:53 +01:00
2026-05-27 23:29:53 +01:00
2026-05-27 23:29:53 +01:00

@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 — Stable, frozen registry of every RBAC.* error code surfaced by the @suaveplan/api-rbac authorisation middleware and…
    • error-codes — Stable, frozen registry of every RBAC.* error code surfaced by the @suaveplan/api-rbac authorisation middleware and…
  • instruments — Owns the single OpenTelemetry counter api.rbac.denials_total and the factory that constructs the RbacInstruments re…
    • instruments — Owns the single OpenTelemetry counter api.rbac.denials_total and the factory that constructs the RbacInstruments re…
  • middleware — Ships createRbacMiddleware(options) — the per-request authorisation gate. The factory produces a framework-agnostic `…
    • middleware — Ships createRbacMiddleware(options) — the per-request authorisation gate. The factory produces a framework-agnostic `…
  • registry — Owns the shared, conflict-aware registry of plugin-declared scopes plus the registerPluginScopes(registry, manifest)…
    • registry — Owns the shared, conflict-aware registry of plugin-declared scopes plus the registerPluginScopes(registry, manifest)…
  • spec — Defines the canonical discriminated RouteAuthSpec union, its Zod schema, the inbound shape adapters fold down to it,…
    • spec — Defines the canonical discriminated RouteAuthSpec union, its Zod schema, the inbound shape adapters fold down to it,…

Install

bun add @suaveplan/api-rbac

Quick start

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 — RBAC_CODES registry (MISSING_SCOPE, RULE_DENY, NO_PRINCIPAL, SCOPE_CONFLICT, INVALID_SPEC) and the RbacError class.
  • instruments — the single counter api.rbac.denials_total plus createRbacInstruments / recordRbacDenial.
  • middleware — createRbacMiddleware and statusForRbacError mapping.
  • registry — ScopeRegistry plus registerPluginScopes(registry, manifest) for install-time conflict detection.
  • spec — 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