Express
permdock/express wraps the Fetch kernel as Express middleware, exposing req.permdock and a protect guard, with Problem Details errors routed through Express error handling.
Status: planned Phase: 2
Purpose
Express 4 and 5 use Node's IncomingMessage / ServerResponse rather than Fetch objects. permdock/express converts the request once, delegates to the server kernel, attaches the request-scoped PermDock as req.permdock, and provides protect as ordinary middleware. Async rejections are forwarded to next(err) so Express 4 does not hang (the fail-closed fix permix needed in its Express adapter).
API
import express from 'express'
import { createPermDock } from 'permdock/express'
import { policy } from './policy'
import { permissions } from './permissions'
export const { permdock, protect, errorHandler } = createPermDock(policy, { subject: (req) => req.user ?? null })
const app = express()
app.use(express.json())
app.use(permdock()) // req.permdock is the request-scoped PermDock
app.delete('/posts/:id', protect(permissions.post.delete, (req) => loadPost(req.params.id)), async (req, res) => {
await deletePost(req.permdockData)
res.status(204).end()
})
app.use(errorHandler()) // PermDockDeniedError → 403 application/problem+json| Export | Role |
|---|---|
permdock() | Middleware. Calls subject(req), builds the instance, sets req.permdock. The Request type extension is scoped to the factory result (a typed Request alias is exported), not to Express globally. |
protect(permission, loadData?) | Middleware. Loads data for instance actions (req.permdockData), decides, and either calls next() or writes the 403 Problem Details response. Rejections go to next(err). |
errorHandler() | Error middleware that maps PermDockDeniedError, PermDockApprovalRequiredError and PermDockValidationError thrown inside handlers to Problem Details responses; other errors pass through. |
permdockHandler() | Router mounting the AuthZEN-shaped decision endpoint: app.use('/api/permdock', permdockHandler()). |
openapi | Kernel hook contract for express-openapi or hand-written documents. |
Typing req.permdock
The factory returns a Request type alias (PermDockRequest) with permdock and permdockData declared. Handlers that need the instance annotate their req parameter with it, or use the exported handler(fn) wrapper that supplies the type. Global augmentation of express-serve-static-core is deliberately avoided: two factories (for example, one per tenant policy) would otherwise fight over the same declaration, and a test double would inherit production types.
app.get('/posts', handler(async (req, res) => {
res.json(req.permdock.filter(permissions.post.read, await listPosts()))
}))Request lifecycle
permdock()runs after your auth middleware, resolves the subject once, and stores the instance onreq.protectloads data, validates untrusted input at the boundary, decides, continues or responds.- Handlers call
req.permdock.assert(...)orfilterfor checks that need handler-local data; throws are routed byerrorHandler(). - Decisions are emitted through
on('decision')withreq.methodandreq.route.path.
What it validates
- Untrusted
loadDataresults and request bodies against the resource schema undervalidate: 'boundary'. - Decision-endpoint bodies against the AuthZEN
evaluationsschema. - Startup order:
protectused beforepermdock()throws a descriptive error at first request rather than readingundefined.
How denials surface
403 application/problem+json for denied; 403 with type ending in /approval-required and a token for approval-required; 400 with issues for boundary validation failures; 401 for anonymous subjects when a scheme is configured. See Problem Details.
Example app
apps/examples/express: posts API on Express 5 with permdock(), protect, errorHandler(), the decision endpoint, and supertest-based tests for the deny and approval-required bodies. An Express 4 compatibility test runs in tests/e2e.
Related standards
Open questions
- Whether to support Express 4 at all, or Express 5 only (which handles async rejections natively).
- Whether
req.permdockDatashould exist or the handler should call the loader itself. - The OpenAPI integration to target for the example:
express-openapi,swagger-jsdoc, or a hand-written document validated bypermdock openapi.
Hono
permdock/hono wraps the Fetch kernel as a Hono middleware and a protect route guard, with OpenAPI 3.2 security emitted through hono-openapi or @hono/zod-openapi.
Fastify
permdock/fastify registers the Fetch kernel as a Fastify plugin that decorates request.permdock and provides protect as a typed preHandler hook.