Files
suaveplan/openspec/changes/auth-require-api-permission/proposal.md
2026-05-27 23:29:53 +01:00

72 lines
6.3 KiB
Markdown

# 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.