# Auth `requireApiPermission` middleware **Package**: `@suaveplan/auth` **Status**: PROPOSED **Type**: feature ## Why `@suaveplan/auth` (`packages/auth/auth/src/`) ships `validateApiKeyMiddleware` (which attaches the validated `ApiKey` to the request) and `hasPermission` (which checks whether an `ApiKey` grants a permission), but lacks a chainable `requireApiPermission(permission)` middleware that combines the two. Without it, every consumer wires the `validateApiKeyMiddleware` → `hasPermission` → 403 dance by hand, and the legacy upstream package shipped this as a first-class export. Genesis already adopted the framework-agnostic `MiddlewareFn` shape (see `packages/flow/rate-limiter/src/middleware/rate-limit-middleware/rate-limit-middleware.ts`) — porting `requireApiPermission` onto that same shape closes the parity gap with one small, well-bounded module. Legacy reference: `packages/universal/auth/src/api-keys.ts` L487-L506 (the 19-line `requireApiPermission` export). Genesis already has the `PermissionDeniedError` class registered with `AUTH.PERMISSION_DENIED` so no new error code is required. ## What changes ### `@suaveplan/auth` - ADDED: `requireApiPermission(permission: string, options?: ApiKeyMiddlewareOptions): MiddlewareFn` — returns a framework-agnostic middleware that: 1. Reads `req.apiKey` (must have been attached by `validateApiKeyMiddleware`). 2. If absent, calls `next(new ApiKeyInvalidError({ message: "API key required" }))` and returns. 3. If present, checks `hasPermission(apiKey, permission)`. On miss, calls `next(new PermissionDeniedError({ permission }))`. 4. On match, calls `next()` to advance the chain. - ADDED: `requireAnyApiPermission(permissions: readonly string[], options?: ApiKeyMiddlewareOptions): MiddlewareFn` — passes if the API key holds AT LEAST one of the listed permissions. Mirrors the existing `requireAnyPermission` user-permission middleware. - ADDED: `requireAllApiPermissions(permissions: readonly string[], options?: ApiKeyMiddlewareOptions): MiddlewareFn` — passes if the API key holds ALL listed permissions. - ADDED: subpath export `./api-keys/require-api-permission/require-api-permission` for tree-shaking. - MODIFIED: root `src/index.ts` re-exports the three new middleware factories alongside the existing `validateApiKeyMiddleware`. The implementation lives at `packages/auth/auth/src/api-keys/require-api-permission/require-api-permission.ts` (new feature dir), keeping `api-keys.ts` focused on the lifecycle calls. The `MiddlewareFn` shape is the same `(req, res, next)` triple already used by `validateApiKeyMiddleware`, the JWT middlewares, and the rate-limiter middleware — compatible with Express directly and with Hono via the same thin adapter all genesis middleware uses. ## Out of scope - **Express / Hono adapter packages** — out of scope. The framework-agnostic `MiddlewareFn` is the genesis convention; consumers wrap it for their framework. See the existing `auth-middleware.md` for the pattern. - **Permission-tree resolution** (e.g. `user:write` implies `user:read`) — out of scope. Genesis treats permissions as flat strings with a single `*` wildcard. Hierarchical permissions are a separate change once a real consumer needs them. - **Audit-logging on permission denial** — out of scope. The thrown `PermissionDeniedError` already carries `{ permission, userId? }` context for downstream audit log adapters; no logging here. - **Replacing `req.apiKey` mutation pattern with a `getRequestApiKey(req)` helper** — out of scope. The `req.apiKey` mutation matches the existing `validateApiKeyMiddleware` contract; changing both is a separate refactor. ## Source mapping This change derives from `../suaveplan/enterprise/auth.md` and the upstream `api-keys.ts` source. | Requirement | Source file | Source lines | Notes | |---|---|---|---| | `requireApiPermission(permission)` | `packages/universal/auth/src/api-keys.ts` | L487-L506 | Verbatim port; replace upstream's `res.status(401).json(...)` express-only path with the genesis `next(error)` framework-agnostic path used by `validateApiKeyMiddleware` (auth.types.ts L96-L99) | ### Requirements with no source line | Requirement | Justification | |---|---| | `requireAnyApiPermission(permissions)` | Symmetry with the user-permission `requireAnyPermission` middleware in `auth-middleware.ts`. Upstream had only the single-permission form; genesis adds the array form for parity with the user-permission surface. | | `requireAllApiPermissions(permissions)` | Same symmetry argument as above. | | Framework-agnostic `MiddlewareFn` shape (vs upstream's express-only `(req, res, next)` with `res.status(...)`) | Genesis convention: every auth middleware returns a `MiddlewareFn` and signals failure via `next(error)`, never via `res.status(...)`. See `validateApiKeyMiddleware` at `packages/auth/auth/src/api-keys/api-keys.ts` L233-L279 and `auth-middleware.ts` for the established pattern. Upstream's express-only path would break in Hono-style adapters. | ## Verification approach Each requirement carries a `**Verified by:**` line in the spec delta. - `packages/auth/auth/src/api-keys/require-api-permission/require-api-permission.test.ts` — covers all three middleware factories: success path, missing-`req.apiKey` path, missing-permission path, wildcard `*` path, edge cases for empty permission arrays in `requireAnyApiPermission` / `requireAllApiPermissions` - `packages/auth/auth/src/api-keys/require-api-permission/require-api-permission.spec-conformance.test.ts` — pins `MiddlewareFn` typing (`expectTypeOf`), pins behavior under both `(req, res, next)` and Hono-style adapter shapes, OTel `auth.apikey.permission_check_total` counter emission ## Impact ### Affected dependents Run `bunx turbo ls --filter=...@suaveplan/auth` to enumerate. - No dependents (verified by `grep -rl "@suaveplan/auth" /home/ubuntu/web/genesis/packages/ --include="package.json"` → only `packages/auth/auth/package.json` itself). Pure additive change — no consumer-visible behavior changes to existing exports. ## Migration impact None — additive only. Existing `validateApiKeyMiddleware`, `hasPermission`, and the user-permission middleware (`requirePermission`, `requireAnyPermission`, `requireAllPermissions`) are unchanged. Consumers that currently chain `validateApiKeyMiddleware` → custom permission check can migrate to `requireApiPermission` at their own pace; both patterns continue to work.