PermDock
Adapters

NestJS

permdock/nest provides a module, a guard and a Protect decorator so Nest controllers get a request-scoped PermDock over either the Express or the Fastify platform adapter.

Status: planned Phase: 2

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. It works on both @nestjs/platform-express and @nestjs/platform-fastify because the kernel only needs the underlying request converted once. NestJS was permix's most requested and never-shipped adapter (issue 11).

API

// permdock.module.ts
import { Module } from '@nestjs/common'
import { createPermDock } from 'permdock/nest'
import { policy } from './policy'

export const { PermDockModule, PermDockGuard, Protect, InjectPermDock } = createPermDock(policy, {
  subject: (request) => request.user ?? null,
})

@Module({ imports: [PermDockModule], providers: [{ provide: APP_GUARD, useClass: PermDockGuard }] })
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())
  }
}
ExportRole
PermDockModuleRegisters the kernel factory as a provider so guards and services can inject it.
PermDockGuardCanActivate 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 a ForbiddenException carrying the Problem Details body. Routes without Protect metadata pass through but still get an instance.
Protect(permission, loadData?)Method or class decorator setting metadata for the guard. Multiple decorators on class and method are all enforced.
InjectPermDock()Parameter decorator injecting the request-scoped instance into handlers.
PermDockExceptionFilterOptional filter that renders PermDock exceptions as application/problem+json; without it Nest's default JSON body is used.
permdockHandlerController class for the AuthZEN-shaped decision endpoint, mountable under /api/permdock.

Request lifecycle

  1. PermDockGuard runs for every route; it converts the platform request, calls subject(request) once, builds the instance and caches it on the request object.
  2. If the handler or class carries Protect metadata, the guard runs the loader, validates untrusted input, and decides. Denials throw before the handler executes.
  3. Handlers receive the instance through @InjectPermDock() and may call assert, filter or where.
  4. PermDockExceptionFilter maps thrown PermDock errors to Problem Details; on('decision') events carry the controller and handler names.

What it validates

  • Untrusted loader results and bodies against the resource schema under validate: 'boundary', after Nest's own ValidationPipe has run.
  • Decision-endpoint bodies against the AuthZEN evaluations schema.
  • Metadata: a Protect decorator referencing a permission from a different definition than the module's policy is a type error.

How denials surface

403 application/problem+json for denied; 403 with type ending in /approval-required plus token; 400 with issues for boundary validation failures; 401 for anonymous subjects when a scheme is configured. See Problem Details.

Example app

apps/examples/nest: a posts module on the Express platform with the guard as APP_GUARD, Protect on CRUD handlers, @nestjs/swagger emitting securitySchemes through the openapi hook, the decision endpoint controller, and e2e tests with @nestjs/testing plus supertest run against both platform adapters.

Open questions

  • Exact identifier names. The plan is silent for Nest; this page proposes PermDockModule, PermDockGuard, Protect, InjectPermDock (parameter decorator, named to avoid clashing with the PermDock type) and PermDockExceptionFilter. The decorator could be Permission instead of Protect to mirror Nest's Roles convention.
  • Whether the guard should be global (APP_GUARD) by default or applied per controller, and how it composes with existing AuthGuard ordering.
  • How loadData obtains injected services (a loader that receives the ExecutionContext and a ModuleRef, or a class-based loader provider).
  • GraphQL and microservice transports: out of scope for Phase 2, but the guard should not break when context.getType() is not http.

On this page