# NestJS

Source: https://permdock.com/docs/adapters/nest

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 [#purpose]

Nest structures authorization as guards and decorators, so `permdock/nest` maps the [server kernel](/docs/adapters/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 [#api]

```ts
// 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 [#gateways]

`connection(client, req, options?)` opens a kernel [`Connection`](/docs/adapters/server-kernel#connections) 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`.

```ts
@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 [#request-lifecycle]

`permdock/nest` follows the [shared adapter contract](/docs/adapters) through the [server kernel](/docs/adapters/server-kernel). What differs on Nest:

1. `PermDockGuard` runs for every HTTP route; it converts the platform request, calls `subject(request)` once, builds the instance for the tenant the `tenant` option reads from the routed request, and caches it on the request object. Both `@nestjs/platform-express` and `@nestjs/platform-fastify` are supported: the guard reads the parsed body or the raw stream of either, and the filter and decision controller write through a `ServerResponse` or a `FastifyReply`. Another platform fails loudly with a `TypeError` naming both.
2. If the handler or class carries `Protect` metadata, the guard runs the loader, validates untrusted input after Nest's own `ValidationPipe` has run, and decides. Denials throw before the handler executes. A `Protect` decorator referencing a permission from a different definition than the module's policy is a type error.
3. Handlers receive the instance through `@InjectPermDock()` and may call `assert`, `filter` or `where`.
4. `PermDockExceptionFilter` renders by transport: HTTP gets the kernel's Problem Details; a `ws` client with `emit` receives an `exception` event carrying `status: 'error'`, the title and the Problem Details body; other transports get the exception rethrown to their own filters.

## Example app [#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`](/docs/adapters/testing) 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 [#related-standards]

* [Problem Details](/docs/standards/problem-details), [OpenAPI 3.2](/docs/standards/openapi).
