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.
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. A PermDock error raised by permdock(), protect or the decision endpoint (a rejected Web Bot Auth signature, a validation failure) is answered with Problem Details directly; any other rejection is 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(permdock()); // req.permdock is the request-scoped PermDock
app.use(express.json()); // either order: permdock() never reads the body stream
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?, options?) | Middleware. The tenant option ((req) => req.params.org) is resolved for each protect, after routing. Loads data for instance actions (req.permdockData), decides, and either calls next() or writes the 403 Problem Details response. PermDock errors become Problem Details; other 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 (mergeParams: true) mounting the AuthZEN-shaped decision endpoint: app.use('/api/permdock', permdockHandler()), or app.use('/:org/permdock', permdockHandler()) to decide in the path tenant. |
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 returned withPermDock(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",
withPermDock(async (req, res) => {
res.json(req.permdock.filter(permissions.post.read, await listPosts()));
}),
);Request lifecycle
permdock/express follows the shared adapter contract through the server kernel. What differs on Express:
permdock()runs after your auth middleware. The WebRequestbehindreq.permdockreads the Node stream only when something consumes its body, soexpress.json(), multer or busboy mounted afterpermdock()still receive the whole body.protectstores the loaded row onreq.permdockData, so the handler does not run the loader twice. Assertions thrown inside handlers are routed byerrorHandler().- A sub-router needs
express.Router({ mergeParams: true })for atenantresolver that reads a parent param such as:org. protectused beforepermdock()throws a descriptive error at the first request rather than readingundefined.- Decision events carry
req.methodandreq.route.path.
Express 4 and 5 are both supported (express >=4): rejections PermDock does not map are forwarded to next(err), which Express 4 needs and Express 5 tolerates. Denials are the kernel's Problem Details responses.
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. No test runs Express 4; the catalog pins Express 5. tests/integration/src/http/express.test.ts runs the testHttpAdapter scenarios on node:http with permdock() mounted before express.json(), a multer upload and a mergeParams sub-router.
Related standards
Last updated on
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.