Files
2026-05-27 23:29:53 +01:00

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