3.2 KiB
@suaveplan/api-rbac — scope + rule-based authorisation middleware
Package: @suaveplan/api-rbac
Status: PROPOSED
Type: new-package
Stage: 8 — see umbrella openspec/changes/api-plugin-foundation/
Why
Per design doc §6 step 5, the per-request pipeline includes an RBAC gate that compares the route's declared auth against the resolved principal.scopes. Plugins declare scopes in their manifest (§3) and assign them per route via router.get("/x", { auth: "billing:read" }, …). Centralising the gate in a single middleware keeps "forgot the auth middleware" out of the class-of-bug list, and gives operators a single place to wire complex policy via @suaveplan/rules.
What changes
- ADDED:
createRbacMiddleware({ rules })— produces aMiddlewarethat consumesRequestContext.principaland the route'sauthoption. Ifauthis a string scope name, performs a default scope check (principal.scopes.includes(scope)); ifauthis aRuleSpec, delegates evaluation to@suaveplan/rules. 403 on deny, pass-through on allow, 401 ifauthis required and the principal isnull(non-public route with no resolved principal). - ADDED:
RouteAuthSpec— discriminated union:{ kind: "scope", scope: string } | { kind: "rule", rule: RuleSpec } | { kind: "public" }. The router translates the route option'sauthvalue into this shape before invoking the middleware. - ADDED: Manifest-scope registration helper
registerPluginScopes(registry, manifest)— adds every scope frommanifest.scopes[]to the host'sScopeRegistry. Conflicts (two plugins claiming the same scope name) raiseRBAC_SCOPE_CONFLICT. - ADDED:
api.rbac.denials_total{plugin, route, reason}counter — incremented on every deny.reasonismissing-scopeorrule-denyorno-principal. - ADDED: Error codes (
RBAC_*):MISSING_SCOPE,RULE_DENY,NO_PRINCIPAL,SCOPE_CONFLICT.
Out of scope
- Session resolution (that's
api-auth-*adapters, design doc §2.3). - Tenant resolution (that's
api-tenant-resolver-*adapters). - Rate limiting (that's
api-rate-limit). - The router itself — this is a
Middlewareconsumed by any router adapter.
Source mapping
Genesis-original. Source of design intent: docs/superpowers/specs/2026-05-13-api-plugin-foundation-design.md.
| Requirement | Design doc section |
|---|---|
| Per-request RBAC gate | §6 step 5 |
| Plugin-declared scopes auto-registered | §3 (scopes block), §5 |
Rule-policy delegation to @suaveplan/rules |
§6 step 5 |
api.rbac.denials_total counter |
§12.3 (host-emitted), §12.4 |
Verification approach
src/middleware/middleware.test.ts— allow / deny matrices forscope,rule, andpublicroute specs.src/middleware/middleware.spec-conformance.test.ts— pins observability: denials emitapi.rbac.denials_totalwith the correctreasonlabel, asserted against a real in-memoryMeterProvider.src/registry/registry.test.ts— duplicate-scope registration raisesRBAC_SCOPE_CONFLICT.tests/dep-graph.test.ts— runtime deps (api-types,error,logger,rules).
Migration impact
None — additive only. api-host wires this middleware at boot.