# `@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 a `Middleware` that consumes `RequestContext.principal` and the route's `auth` option. If `auth` is a string scope name, performs a default scope check (`principal.scopes.includes(scope)`); if `auth` is a `RuleSpec`, delegates evaluation to `@suaveplan/rules`. 403 on deny, pass-through on allow, 401 if `auth` is required and the principal is `null` (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's `auth` value into this shape before invoking the middleware. - ADDED: Manifest-scope registration helper `registerPluginScopes(registry, manifest)` — adds every scope from `manifest.scopes[]` to the host's `ScopeRegistry`. Conflicts (two plugins claiming the same scope name) raise `RBAC_SCOPE_CONFLICT`. - ADDED: `api.rbac.denials_total{plugin, route, reason}` counter — incremented on every deny. `reason` is `missing-scope` or `rule-deny` or `no-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 `Middleware` consumed 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 for `scope`, `rule`, and `public` route specs. - `src/middleware/middleware.spec-conformance.test.ts` — pins observability: denials emit `api.rbac.denials_total` with the correct `reason` label, asserted against a real in-memory `MeterProvider`. - `src/registry/registry.test.ts` — duplicate-scope registration raises `RBAC_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.