## Install

```typescript
import {
  nestAccessGuard,
  Authorize,
  createTypedAuthorize,
  createEngineProvider,
  IAM_ACCESS_ENGINE_TOKEN,
  IAM_ACCESS_METADATA_KEY,
} from '@gentleduck/iam/server/nest'
```

Zero hard dependency on `@nestjs/common` - uses minimal duck-typed interfaces. `reflect-metadata` is used when available, with a direct property fallback so the decorator works without it.

***

## Guard factory

Create a NestJS-compatible guard function. It reads metadata from the `@Authorize` decorator on each handler.

```typescript
import { nestAccessGuard } from '@gentleduck/iam/server/nest'

const canAccess = nestAccessGuard(engine, {
  getUserId: (req) => req.user?.id ?? req.user?.sub,
  getScope: (req) => req.headers['x-org-id'] as string | undefined,
  getResourceId: (req) => req.params?.id,
  onError: (err, req) => false,
})

// Register as a global guard in your module:
// APP_GUARD -> { provide: APP_GUARD, useValue: { canActivate: canAccess } }
```

Handlers without an `@Authorize` decorator are allowed by default. The guard only activates when metadata is present.

***

## The Authorize decorator

Annotate controller methods with their required permissions.

```typescript
import { Authorize } from '@gentleduck/iam/server/nest'

@Controller('posts')
export class PostsController {
  @Delete(':id')
  @Authorize({ action: 'delete', resource: 'post' })
  async deletePost(@Param('id') id: string) {
    return this.postsService.delete(id)
  }

  @Post()
  @Authorize({ action: 'create', resource: 'post' })
  async createPost(@Body() dto: CreatePostDto) {
    return this.postsService.create(dto)
  }

  // Infer action from HTTP method, resource from route path
  @Get()
  @Authorize({ infer: true })
  async listPosts() {
    return this.postsService.list()
  }
}
```

### Scoped authorization

Attach a scope to restrict access to a specific context.

```typescript
@Authorize({ action: 'manage', resource: 'billing', scope: 'admin' })
async updateBilling() { ... }
```

If no scope is set on the decorator, the guard falls back to the `getScope` option from the guard factory.

***

## Type-safe decorator

`createTypedAuthorize` constrains the decorator to your application's exact action, resource, and scope types. Typos become compile-time errors.

```typescript
import { createTypedAuthorize } from '@gentleduck/iam/server/nest'

type Action = 'create' | 'read' | 'update' | 'delete' | 'manage'
type Resource = 'post' | 'user' | 'billing' | 'report'
type Scope = 'admin' | 'member'

const Auth = createTypedAuthorize<Action, Resource, Scope>()

// This compiles:
@Auth({ action: 'delete', resource: 'post' })

// This is a compile error - "craete" is not in Action:
@Auth({ action: 'craete', resource: 'post' })
```

***

## Engine provider

Register the engine as a NestJS provider:

```typescript
import { Module } from '@nestjs/common'
import { APP_GUARD } from '@nestjs/core'
import {
  createEngineProvider,
  IAM_ACCESS_ENGINE_TOKEN,
  nestAccessGuard,
} from '@gentleduck/iam/server/nest'

@Module({
  providers: [
    createEngineProvider(() => new IamEngine({ adapter })),
    {
      provide: APP_GUARD,
      useFactory: (engine) => ({ canActivate: nestAccessGuard(engine) }),
      inject: [IAM_ACCESS_ENGINE_TOKEN],
    },
  ],
})
export class AppModule {}
```

The engine factory can be async - return a `Promise<IamEngine>` to support adapter setup that requires `await`.

***

## Notes

* **`reflect-metadata` is optional but recommended.** The integration uses it when available and falls back to a property attached directly to the descriptor. In a normal Nest app, load `reflect-metadata` once at the entry point.
* **No `@Authorize` = allowed.** A handler without metadata is treated as public. Use route-level Nest guards or other middleware for blanket coverage.
* **`onError` returns boolean.** Default is `() => false` (deny on engine error). Return `true` only if you want a fail-open behavior in specific cases.

***

## Options reference

| Option | Type | Default | Description |
| --- | --- | --- | --- |
| `getUserId` | `(req) -> string or null` | `req.user?.id ?? req.user?.sub` | Extract the subject ID |
| `getEnvironment` | `(req) -> Environment` | IP + user agent + timestamp | Extract environment context |
| `getResourceId` | `(req) -> string or undefined` | `req.params?.id` | Extract the resource instance ID |
| `getScope` | `(req) -> string or undefined` | `undefined` | Fallback scope when decorator has none |
| `onError` | `(err, req) -> boolean` | `() => false` | Return true to allow on error, false to deny |

***

## Exported tokens

| Token | Type | Purpose |
| --- | --- | --- |
| `IAM_ACCESS_ENGINE_TOKEN` | `string` (`'ACCESS_ENGINE'`) | DI token for the engine |
| `IAM_ACCESS_METADATA_KEY` | `string` (`'duck-iam:authorize'`) | Reflect metadata key for the decorator |

***

## Admin operations

NestJS routes via controllers, so duck-iam ships gated admin *operations* (not a router factory). `createAdminOperations` returns a record of pre-gated functions to plug into your controller methods. The `authorize` callback is **required** - the factory throws if it's missing.

```typescript
import { Controller, Get, Put, Post, Delete, Inject, Req, Body, Param } from '@nestjs/common'
import { IAM_ACCESS_ENGINE_TOKEN, createAdminOperations } from '@gentleduck/iam/server/nest'

@Controller('admin')
export class IamAdminController {
  private readonly h

  constructor(@Inject(IAM_ACCESS_ENGINE_TOKEN) engine) {
    this.h = createAdminOperations(engine, {
      authorize: (req) => req.user?.role === 'platform-admin',
    })
  }

  @Get('policies')        listPolicies(@Req() req) { return this.h.listPolicies(req) }
  @Get('roles')           listRoles(@Req() req)    { return this.h.listRoles(req) }
  @Put('policies')        savePolicy(@Req() req, @Body() body) { return this.h.savePolicy(req, body) }
  @Put('roles')           saveRole(@Req() req, @Body() body)   { return this.h.saveRole(req, body) }

  @Post('subjects/:id/roles')
  assignRole(@Req() req, @Param('id') id, @Body() body) { return this.h.assignRole(req, id, body) }

  @Delete('subjects/:id/roles/:roleId')
  revokeRole(@Req() req, @Param('id') id, @Param('roleId') roleId) { return this.h.revokeRole(req, id, roleId) }
}
```

Every operation throws `Error & { status: 401 }` when `authorize` returns false - wire your Nest exception filter to map that to a `401` response, or call `authorize` yourself inside a `@UseGuards()` and skip this helper.

### CSRF protection (default-on)

Mutation operations (`savePolicy`, `saveRole`, `assignRole`, `revokeRole`) run
a `Sec-Fetch-Site` check by default (SEC-103 / CAVEAT-2). Failures throw
`Error & { status: 403 }`. Browsers populate the header automatically;
cross-site form posts are rejected, same-site / same-origin requests pass.
Non-browser callers (no header) pass - they must be gated by bearer / mTLS.

```ts
// Default - cookie-auth admin UIs get CSRF protection with no opt-in.
createAdminOperations(engine, { authorize })

// Server-to-server bearer/mTLS API - disable the check entirely.
createAdminOperations(engine, { authorize, csrfCheck: false })

// Stricter - Origin allowlist.
const ADMIN_ORIGINS = new Set(['https://admin.example.com'])
createAdminOperations(engine, {
  authorize,
  csrfCheck: (req) => ADMIN_ORIGINS.has(req.headers?.origin as string),
})
```

The admin operations options object conforms to `Nest.IAdminOptions` with `authorize` of shape `Nest.IAdminAuthorize`. The metadata read by the guard from `@Authorize` conforms to `Nest.IAuthorizeMeta`, and the guard factory accepts `Nest.IGuardOptions`.

***

## Types

All types live under the `Nest` namespace at `@gentleduck/iam/server/nest`. Type-only - zero bundle cost.

* `Nest.IAuthorizeMeta` - shape of the metadata stamped onto a handler by `@Authorize` and `createTypedAuthorize`.
* `Nest.IGuardOptions` - options for `nestAccessGuard` (`getUserId`, `getEnvironment`, `getResourceId`, `getScope`, `onError`).
* `Nest.IAdminAuthorize` - signature of the required `authorize` callback for `createAdminOperations`.
* `Nest.IAdminOptions` - options for `createAdminOperations`.

```typescript
import type { Nest } from '@gentleduck/iam/server/nest'

const guardOpts: Nest.IGuardOptions = {
  getUserId: (req) => req.user?.id ?? req.user?.sub,
}

const meta: Nest.IAuthorizeMeta = { action: 'delete', resource: 'post' }
```

Deprecated bare aliases (`IAuthorizeMeta`, `IGuardOptions`, `IAdminAuthorize`, `IAdminOptions`) remain for back-compat and will be removed in 3.0.