6.3 KiB
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:- Reads
req.apiKey(must have been attached byvalidateApiKeyMiddleware). - If absent, calls
next(new ApiKeyInvalidError({ message: "API key required" }))and returns. - If present, checks
hasPermission(apiKey, permission). On miss, callsnext(new PermissionDeniedError({ permission })). - On match, calls
next()to advance the chain.
- Reads
- ADDED:
requireAnyApiPermission(permissions: readonly string[], options?: ApiKeyMiddlewareOptions): MiddlewareFn— passes if the API key holds AT LEAST one of the listed permissions. Mirrors the existingrequireAnyPermissionuser-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-permissionfor tree-shaking. - MODIFIED: root
src/index.tsre-exports the three new middleware factories alongside the existingvalidateApiKeyMiddleware.
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
MiddlewareFnis the genesis convention; consumers wrap it for their framework. See the existingauth-middleware.mdfor the pattern. - Permission-tree resolution (e.g.
user:writeimpliesuser: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
PermissionDeniedErroralready carries{ permission, userId? }context for downstream audit log adapters; no logging here. - Replacing
req.apiKeymutation pattern with agetRequestApiKey(req)helper — out of scope. Thereq.apiKeymutation matches the existingvalidateApiKeyMiddlewarecontract; 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.apiKeypath, missing-permission path, wildcard*path, edge cases for empty permission arrays inrequireAnyApiPermission/requireAllApiPermissionspackages/auth/auth/src/api-keys/require-api-permission/require-api-permission.spec-conformance.test.ts— pinsMiddlewareFntyping (expectTypeOf), pins behavior under both(req, res, next)and Hono-style adapter shapes, OTelauth.apikey.permission_check_totalcounter 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"→ onlypackages/auth/auth/package.jsonitself).
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.