Initial Commit
This commit is contained in:
71
openspec/changes/auth-require-api-permission/proposal.md
Normal file
71
openspec/changes/auth-require-api-permission/proposal.md
Normal file
@@ -0,0 +1,71 @@
|
||||
# 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.
|
||||
Reference in New Issue
Block a user