NestJS
permdock/nest provides a module, a guard and a Protect decorator so Nest controllers get a request-scoped PermDock on the Express or Fastify platform.
Requires: @nestjs/common 12, @nestjs/core 12 and @nestjs/platform-express 12
Purpose
Nest structures authorization as guards and decorators, so permdock/nest maps the server kernel onto that shape: a module that registers the factory, a guard registered as APP_GUARD that builds the request-scoped instance and evaluates route metadata, a Protect decorator that attaches the permission (and optional loader) to a handler, and a parameter decorator to inject the instance. The kernel only needs the underlying Node request converted once. Outside HTTP (GraphQL, WebSocket gateways, microservices) a route with Protect metadata is denied unless the request option maps the ExecutionContext to a request; a route without metadata passes through.
API
// permdock.module.ts
import { Module } from "@nestjs/common";
import { createPermDock } from "permdock/nest";
import { policy } from "./policy";
export const { PermDockModule, Protect, InjectPermDock } = createPermDock(
policy,
{
subject: (request) => request.user ?? null,
},
);
@Module({
imports: [PermDockModule.forRoot({ guard: "global" })],
})
export class AppModule {}
// posts.controller.ts
@Controller("posts")
export class PostsController {
constructor(private readonly posts: PostsService) {}
@Delete(":id")
@Protect(permissions.post.delete, (request) => loadPost(request.params.id))
async remove(@InjectPermDock() permdock: PermDock, @Param("id") id: string) {
await this.posts.remove(id);
}
@Get()
async list(@InjectPermDock() permdock: PermDock) {
return permdock.filter(permissions.post.read, await this.posts.all());
}
}| Export | Role |
|---|---|
PermDockModule | Registers the kernel factory as a provider so guards and services can inject it. Also registers PermDockExceptionFilter as APP_FILTER. PermDockModule.forRoot({ guard: 'global' }) also registers PermDockGuard as APP_GUARD. |
PermDockGuard | CanActivate implementation. Builds the request-scoped instance once per request (stored on the request), reads Protect metadata from the handler and class, runs loadData, decides, and throws before the handler. Routes without Protect metadata pass through but still get an instance. Register it with forRoot({ guard: 'global' }), or as APP_GUARD with useExisting. |
Protect(permission, loadData?, options?) | Method or class decorator setting metadata for the guard. Multiple decorators on class and method are all enforced; rules that pass the same loadData function call it once per request. Protect(null, loadData?, { oauthScopes }) needs a principal and, from a delegated token, one of the OAuth scopes, but no permission. options is ProtectOptions ({ trusted: true } for a loader that returns a row the server loaded). |
InjectPermDock() | Parameter decorator injecting the request-scoped instance into handlers. |
PermDockExceptionFilter | Filter that renders PermDock exceptions (denied, approval required, revoked, validation, invalid signature) as application/problem+json with WWW-Authenticate when the kernel sets it. |
permdockHandler(options?) | Controller class for the AuthZEN-shaped decision endpoint. options.path is the controller path (default api/permdock) and may hold route params, for example ':org/permdock/access/v1/evaluations' with a tenant that reads req.params.org. |
decorateMethod(cls, key, ...decorators) | Applies method decorators in the order given without decorator syntax, for code limited to erasable TypeScript: decorateMethod(PostsController, 'update', Patch(':id'), Protect(permissions.post.update)). |
openapi | Kernel hook contract for @nestjs/swagger. |
request option | (context: ExecutionContext) => request | undefined. Maps a non-HTTP context (for example a GraphQL context.req or a WebSocket handshake) to the request the kernel reads. Without it, protected non-HTTP routes are denied. |
Gateways
connection(client, req, options?) opens a kernel Connection for a gateway client (typed NestSocket: a Socket.IO socket or a raw ws socket), memoised per client. When it aborts, the client receives permdock:error with the Problem Details and is disconnected, or a raw socket is closed with 1008. PermDockGuard on a @SubscribeMessage handler with Protect metadata checks each message against the client's connection the way HTTP protect does: the OAuth scope check, the loader (a null result is a 404 problem), the decision and the approval resume from the PermDock-Approval header on the mapped request. A denial is the usual Problem Details and the connection stays open; a message after revocation throws PermDockRevokedError. Pass revocations to PermDockModule.
@WebSocketGateway()
export class ProjectsGateway
implements OnGatewayConnection, OnGatewayDisconnect
{
async handleConnection(client: Socket) {
await permdock.connection(client, client.request, {
permission: permissions.project.read,
});
}
async handleDisconnect(client: Socket) {
(await permdock.connection(client, client.request)).close();
}
}Request lifecycle
permdock/nest follows the shared adapter contract through the server kernel. What differs on Nest:
PermDockGuardruns for every HTTP route; it converts the platform request, callssubject(request)once, builds the instance for the tenant thetenantoption reads from the routed request, and caches it on the request object. Both@nestjs/platform-expressand@nestjs/platform-fastifyare supported: the guard reads the parsed body or the raw stream of either, and the filter and decision controller write through aServerResponseor aFastifyReply. Another platform fails loudly with aTypeErrornaming both.- If the handler or class carries
Protectmetadata, the guard runs the loader, validates untrusted input after Nest's ownValidationPipehas run, and decides. Denials throw before the handler executes. AProtectdecorator referencing a permission from a different definition than the module's policy is a type error. - Handlers receive the instance through
@InjectPermDock()and may callassert,filterorwhere. PermDockExceptionFilterrenders by transport: HTTP gets the kernel's Problem Details; awsclient withemitreceives anexceptionevent carryingstatus: 'error', the title and the Problem Details body; other transports get the exception rethrown to their own filters.
Example app
apps/examples/nest: a posts module on the Express platform with the guard registered by PermDockModule.forRoot({ guard: 'global' }), Protect applied with decorateMethod on PATCH /posts/:id and POST /posts/:id/publish, and the decision endpoint controller.
tests/integration/src/http/nest.test.ts runs the testHttpAdapter scenarios on both platforms: FileInterceptor uploads on Express, @fastify/multipart on Fastify, an admin module under RouterModule and the decision controller at a tenant path.
Related standards
Last updated on
Elysia
permdock/elysia derives a request-scoped PermDock into Elysia's context and provides protect as a typed beforeHandle hook that keeps validated route types.
Node http
permdock/node wraps the Fetch kernel for raw node:http handlers (IncomingMessage and ServerResponse) and for any framework not covered by a dedicated adapter.