PermDock
Adapters

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
ExportRole
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()).
openapiKernel 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

  1. permdock() runs after your auth middleware, resolves the subject once, and stores the instance on req.
  2. protect loads data, validates untrusted input at the boundary, decides, continues or responds.
  3. Handlers call req.permdock.assert(...) or filter for checks that need handler-local data; throws are routed by errorHandler().
  4. Decisions are emitted through on('decision') with req.method and req.route.path.

What it validates

  • Untrusted loadData results and request bodies against the resource schema under validate: 'boundary'.
  • Decision-endpoint bodies against the AuthZEN evaluations schema.
  • Startup order: protect used before permdock() throws a descriptive error at first request rather than reading undefined.

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.

Open questions

  • Whether to support Express 4 at all, or Express 5 only (which handles async rejections natively).
  • Whether req.permdockData should 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 by permdock openapi.

On this page