Relocate the ~6k-file docs tree from the repo root into plugins/document-navigator/docs/ so the plugin is self-contained and portable. docs/superpowers/ (specs and plans) stays at the repo root. Moved via cp+rm rather than git mv because directory rename() fails with EPERM on this WSL /mnt DrvFs mount; git rename-detection pairs the identical-content files at commit time. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
4.9 KiB
4.9 KiB
@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-rbacauthorisation middleware and…- error-codes — Stable, frozen registry of every
RBAC.*error code surfaced by the@suaveplan/api-rbacauthorisation middleware and…
- error-codes — Stable, frozen registry of every
- instruments — Owns the single OpenTelemetry counter
api.rbac.denials_totaland the factory that constructs theRbacInstrumentsre…- instruments — Owns the single OpenTelemetry counter
api.rbac.denials_totaland the factory that constructs theRbacInstrumentsre…
- instruments — Owns the single OpenTelemetry counter
- 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 `…
- middleware — Ships
- 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)…
- registry — Owns the shared, conflict-aware registry of plugin-declared scopes plus the
- spec — Defines the canonical discriminated
RouteAuthSpecunion, its Zod schema, the inbound shape adapters fold down to it,…- spec — Defines the canonical discriminated
RouteAuthSpecunion, its Zod schema, the inbound shape adapters fold down to it,…
- spec — Defines the canonical discriminated
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_CODESregistry (MISSING_SCOPE,RULE_DENY,NO_PRINCIPAL,SCOPE_CONFLICT,INVALID_SPEC) and theRbacErrorclass.instruments— the single counterapi.rbac.denials_totalpluscreateRbacInstruments/recordRbacDenial.middleware—createRbacMiddlewareandstatusForRbacErrormapping.registry—ScopeRegistryplusregisterPluginScopes(registry, manifest)for install-time conflict detection.spec— the canonicalRouteAuthSpecunion, the ZodRouteAuthSpecSchema, and thenormaliseRouteAuthfold.
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