## Goal

BlogDuck has a working authorization engine. Now protect actual HTTP endpoints.
Pick the framework you use.

## How It Works

All server integrations share the same pattern:

1. Extract the user ID from the request (JWT, session, header)
2. Determine the action (from HTTP method) and resource (from URL path)
3. Call `engine.can()` with the extracted context
4. Allow or deny the request

### HTTP Method to Action Mapping

duck-iam maps HTTP methods to actions automatically via `IAM_METHOD_ACTION_MAP`:

```typescript
const IAM_METHOD_ACTION_MAP = {
  GET: 'read',
  HEAD: 'read',
  OPTIONS: 'read',
  POST: 'create',
  PUT: 'update',
  PATCH: 'update',
  DELETE: 'delete',
}
```

### URL Path to Resource Mapping

The default resource extraction parses the URL path:

```
/api/posts/123
      ^     ^
      |     +-- resourceId: '123'
      +-- resourceType: 'posts'
```

The first path segment after a base path is the resource type, the second is the resource ID.
Customize this with the `getResource` callback.

### Environment Extraction

The `extractEnvironment()` helper reads standard headers:

```typescript
import { extractEnvironment } from '@gentleduck/iam/server/generic'

const env = extractEnvironment(req)
// {
//   ip: '192.168.1.1',       // from req.ip or x-forwarded-for or x-real-ip
//   userAgent: 'Mozilla/...',  // from user-agent header
//   timestamp: 1708300000000,  // Date.now()
// }
```

## Generic Helpers

Framework-agnostic utilities that work with any server.

### generatePermissionMap()

Generate a `PermissionMap` for the client (Chapter 7):

```typescript
import { generatePermissionMap } from '@gentleduck/iam/server/generic'

const permissions = await generatePermissionMap(engine, userId, [
  { action: 'create', resource: 'post' },
  { action: 'update', resource: 'post', resourceId: 'post-1' },
  { action: 'delete', resource: 'post', resourceId: 'post-1' },
  { action: 'manage', resource: 'dashboard' },
  { action: 'manage', resource: 'user', scope: 'acme' },
])
// { 'create:post': true, 'update:post:post-1': true, ... }
```

Pass this map to the client `AccessProvider` for permission-based UI rendering.

### createSubjectCan()

Create a reusable `can` function bound to a user:

```typescript
import { createSubjectCan } from '@gentleduck/iam/server/generic'

const can = createSubjectCan(engine, userId)

if (await can('delete', 'post', 'post-1')) {
  // delete the post
}

if (await can('manage', 'dashboard')) {
  // show admin panel
}

// With scope
if (await can('manage', 'user', undefined, 'acme')) {
  // manage users in acme
}
```

## Engine Mode on the Server

All server integrations call `engine.can()` under the hood, which always returns a boolean
regardless of engine mode. Middleware, guards, and route handlers work identically in both
`'development'` and `'production'` mode.

Use `mode: 'production'` for maximum throughput. Production mode skips Decision object
allocation, timing, and hooks, giving roughly 2x faster:

```typescript title="src/access.ts"
import { IamEngine } from '@gentleduck/iam'
import { adapter } from './adapter'

export const engine = new IamEngine({
  adapter,
  mode: 'production', // recommended for servers -- ~2x faster, no Decision overhead
})
```

Temporarily switch back to `'development'` mode for decision traces. Since `engine.can()`
returns boolean either way, the rest of your code stays the same.

## Express

**Global middleware**

```typescript title="src/server.ts"
import express from 'express'
import { engine } from './access'  // mode: 'production' recommended
import { accessMiddleware } from '@gentleduck/iam/server/express'

const app = express()
app.use(express.json())

app.use(accessMiddleware(engine, {
  getUserId: (req) => req.headers['x-user-id'] as string,
  getScope: (req) => req.headers['x-organization'] as string,
}))
```

Missing user ID returns 401. Denied permission returns 403.

**Full options:**

```typescript
interface ExpressOptions {
  getUserId?: (req) => string | null        // default: req.user?.id
  getResource?: (req) => Resource           // default: parse from URL
  getAction?: (req) => string               // default: IAM_METHOD_ACTION_MAP
  getEnvironment?: (req) => Environment     // default: extractEnvironment()
  getScope?: (req) => string | undefined    // default: none
  onDenied?: (req, res) => void             // default: res.status(403).json(...)
  onError?: (err, req, res, next) => void   // default: res.status(500).json(...)
}
```

**Per-route guards**

```typescript title="src/server.ts"
import { guard } from '@gentleduck/iam/server/express'

// Explicit action and resource
app.delete('/api/posts/:id',
  guard(engine, 'delete', 'post', {
    getUserId: (req) => req.headers['x-user-id'] as string,
  }),
  (req, res) => {
    res.json({ deleted: req.params.id })
  }
)

// With a fixed scope
app.post('/api/admin/users',
  guard(engine, 'manage', 'user', {
    getUserId: (req) => req.headers['x-user-id'] as string,
    scope: 'admin',
  }),
  (req, res) => {
    res.json({ created: true })
  }
)
```

`guard()` takes an explicit action and resource. The resource ID is auto-extracted from `req.params.id`.

**Admin router**

```typescript title="src/server.ts"
import { Router } from 'express'
import { adminRouter } from '@gentleduck/iam/server/express'

app.use(
  '/api/access-admin',
  adminRouter(engine, {
    authorize: (req) => /* your check, e.g. req.user?.roles.includes('admin') */ true,
  })(() => Router()),
)
```

**Exposed endpoints:**

| Method | Path | Description | Body |
| --- | --- | --- | --- |
| GET | `/policies` | List all policies | |
| GET | `/roles` | List all roles | |
| PUT | `/policies` | Save/update a policy | `Policy` object |
| PUT | `/roles` | Save/update a role | `Role` object |
| POST | `/subjects/:id/roles` | Assign role to subject | `{ roleId, scope? }` |
| DELETE | `/subjects/:id/roles/:roleId` | Revoke role from subject | |

Protect the admin router with your own auth middleware. Never expose it to unauthenticated users.

To revoke one scoped assignment without affecting the same role in other scopes,
call `engine.admin.revokeRole(subjectId, roleId, scope)` directly.

## Hono

**Global middleware**

```typescript title="src/server.ts"
import { Hono } from 'hono'
import { engine } from './access'  // mode: 'production' recommended
import { accessMiddleware } from '@gentleduck/iam/server/hono'

const app = new Hono()

app.use('*', accessMiddleware(engine, {
  getUserId: (c) => c.get('userId') || c.req.header('x-user-id'),
  getScope: (c) => c.req.header('x-organization'),
}))
```

The default `getUserId` checks `c.get('userId')` first (set by your auth middleware),
then falls back to the `x-user-id` header.

For Cloudflare Workers, the IP is extracted from `cf-connecting-ip` (then `x-forwarded-for` as fallback).

**Full options:**

```typescript
interface HonoOptions {
  getUserId?: (c) => string | null
  getResource?: (c) => Resource
  getAction?: (c) => string
  getEnvironment?: (c) => Environment
  getScope?: (c) => string | undefined
  onDenied?: (c) => Response
  onError?: (err, c) => Response
}
```

**Per-route guards**

```typescript title="src/server.ts"
import { guard } from '@gentleduck/iam/server/hono'

app.delete('/api/posts/:id',
  guard(engine, 'delete', 'post', {
    getUserId: (c) => c.get('userId'),
  }),
  async (c) => {
    return c.json({ deleted: c.req.param('id') })
  }
)
```

## NestJS

**Create the access module**

```typescript title="src/access/access.module.ts"
import { Module } from '@nestjs/common'
import { createEngineProvider, IAM_ACCESS_ENGINE_TOKEN } from '@gentleduck/iam/server/nest'
import { engine } from './access'  // mode: 'production' recommended

@Module({
  providers: [
    createEngineProvider(() => engine),
  ],
  exports: [IAM_ACCESS_ENGINE_TOKEN],
})
export class AccessModule {}
```

`createEngineProvider()` returns a NestJS provider that makes the engine available
via dependency injection using the `IAM_ACCESS_ENGINE_TOKEN` injection token.

**Create the access guard**

```typescript title="src/access/access.guard.ts"
import { CanActivate, ExecutionContext, Injectable, Inject } from '@nestjs/common'
import { nestAccessGuard, IAM_ACCESS_ENGINE_TOKEN } from '@gentleduck/iam/server/nest'
import type { IamEngine } from '@gentleduck/iam'

@Injectable()
export class AccessGuard implements CanActivate {
  private check: ReturnType<typeof nestAccessGuard>

  constructor(@Inject(IAM_ACCESS_ENGINE_TOKEN) engine: IamEngine) {
    this.check = nestAccessGuard(engine, {
      getUserId: (req) => req.user?.id || req.user?.sub,
      getScope: (req) => req.headers['x-organization'],
      getResourceId: (req) => req.params?.id,
    })
  }

  async canActivate(context: ExecutionContext): Promise<boolean> {
    return this.check(context)
  }
}
```

**Full options:**

```typescript
interface NestGuardOptions {
  getUserId?: (req) => string | null          // default: req.user?.id or req.user?.sub
  getEnvironment?: (req) => Environment       // default: extractEnvironment()
  getResourceId?: (req) => string | undefined // default: req.params?.id
  getScope?: (req) => string | undefined      // default: none
  onError?: (err, req) => boolean             // default: return false (deny)
}
```

**Use the @Authorize decorator**

```typescript title="src/posts/posts.controller.ts"
import { Controller, Delete, Get, Post, Param, UseGuards } from '@nestjs/common'
import { Authorize } from '@gentleduck/iam/server/nest'
import { AccessGuard } from '../access/access.guard'

@Controller('posts')
@UseGuards(AccessGuard)
export class PostsController {
  // Explicit action and resource
  @Delete(':id')
  @Authorize({ action: 'delete', resource: 'post' })
  async deletePost(@Param('id') id: string) {
    return { deleted: id }
  }

  // With scope
  @Post('admin')
  @Authorize({ action: 'manage', resource: 'post', scope: 'admin' })
  async adminAction() {
    return { success: true }
  }

  // Infer action from HTTP method, resource from route path
  @Get()
  @Authorize()  // infer: true by default
  async listPosts() {
    return []
  }
}
```

When `@Authorize()` uses `infer: true` (the default), action is inferred from the HTTP method
via `IAM_METHOD_ACTION_MAP` and resource is inferred from the route path (last non-parameter segment).

If no `@Authorize` decorator is present, the guard allows the request through.

**AuthorizeMeta interface:**

```typescript
interface AuthorizeMeta {
  action?: string       // explicit action
  resource?: string     // explicit resource type
  scope?: string        // explicit scope
  infer?: boolean       // infer from HTTP method + route (default: true)
}
```

**Type-safe decorator**

```typescript title="src/access/authorize.ts"
import { createTypedAuthorize } from '@gentleduck/iam/server/nest'

type AppAction = 'read' | 'create' | 'update' | 'delete' | 'manage'
type AppResource = 'post' | 'comment' | 'user' | 'dashboard'
type AppScope = 'acme' | 'globex'

export const Authorize = createTypedAuthorize<AppAction, AppResource, AppScope>()

// Now typos are compile errors:
// @Authorize({ action: 'delet', resource: 'post' })  // TypeScript error
```

## Next.js (App Router)

**Protect API route handlers**

```typescript title="app/api/posts/[id]/route.ts"
import { withAccess } from '@gentleduck/iam/server/next'
import { engine } from '@/lib/access'  // mode: 'production' recommended

export const DELETE = withAccess(engine, 'delete', 'post',
  async (req, { params }) => {
    const { id } = await params
    return Response.json({ deleted: id })
  },
  {
    getUserId: (req) => req.headers.get('x-user-id'),
    scope: 'acme',
  }
)
```

**WithAccessOptions:**

```typescript
interface WithAccessOptions {
  getUserId?: (req: Request) => string | null | Promise<string | null>
  getEnvironment?: (req: Request) => Environment
  scope?: string
  onError?: (err: Error, req: Request) => Response
}
```

`getUserId` can be async, useful for reading from cookies or JWT.

**Check permissions in Server Components**

```typescript title="app/posts/[id]/page.tsx"
import { checkAccess, getPermissions } from '@gentleduck/iam/server/next'
import { engine } from '@/lib/access'

export default async function PostPage({ params }) {
  const { id } = await params
  const userId = 'alice'  // from your auth

  // Single check
  const canDelete = await checkAccess(engine, userId, 'delete', 'post', id)

  // Batch check for the client
  const permissions = await getPermissions(engine, userId, [
    { action: 'update', resource: 'post', resourceId: id },
    { action: 'delete', resource: 'post', resourceId: id },
    { action: 'create', resource: 'comment' },
  ])

  return (
    <div>
      <h1>Post {id}</h1>
      {canDelete && <button>Delete</button>}
      <ClientToolbar permissions={permissions} />
    </div>
  )
}
```

`checkAccess()` returns a boolean. `getPermissions()` returns a `PermissionMap`
for client-side hydration.

**Next.js middleware for route-level protection**

```typescript title="middleware.ts"
import { createNextMiddleware } from '@gentleduck/iam/server/next'
import { engine } from '@/lib/access'

const checkAccess = createNextMiddleware(engine, {
  rules: [
    { pattern: '/api/admin', resource: 'dashboard', action: 'manage' },
    { pattern: /^\/api\/posts\/\w+$/, resource: 'post' },
    { pattern: '/api/users', resource: 'user', scope: 'admin' },
  ],
  getUserId: (req) => req.headers.get('x-user-id'),
})

export async function middleware(req: Request) {
  const result = await checkAccess(req)
  if (result) return result  // 401 or 403
  // No match or allowed -- continue
}

export const config = {
  matcher: ['/api/:path*'],
}
```

**Rule matching:**

* String patterns use `path.startsWith(pattern)`, so `/api/admin` matches `/api/admin/users`
* Regex patterns use `pattern.test(path)` for precise matching
* First matching rule wins (order matters)
* No matching rule passes the request through
* Missing `action` is inferred from the HTTP method

**Rule interface:**

```typescript
interface Rule {
  pattern: string | RegExp  // URL path pattern
  resource: string          // resource type
  action?: string           // optional: explicit action (otherwise inferred)
  scope?: string            // optional: scope for this route
}
```

## Custom Error Responses

All integrations support custom error handlers:

```typescript
// Express
accessMiddleware(engine, {
  onDenied: (req, res) => {
    res.status(403).json({
      error: 'forbidden',
      message: `You cannot ${req.method.toLowerCase()} this resource`,
    })
  },
  onError: (err, req, res, next) => {
    console.error('Auth error:', err)
    res.status(500).json({ error: 'internal' })
  },
})

// Hono
accessMiddleware(engine, {
  onDenied: (c) => c.json({ error: 'forbidden' }, 403),
  onError: (err, c) => c.json({ error: 'internal' }, 500),
})

// NestJS (via guard options)
nestAccessGuard(engine, {
  onError: (err, req) => {
    console.error('Auth error:', err)
    return false  // deny on error
  },
})

// Next.js
withAccess(engine, 'delete', 'post', handler, {
  onError: (err, req) => Response.json({ error: 'internal' }, { status: 500 }),
})
```

Default error responses:

* `401 Unauthorized`: no user ID found
* `403 Forbidden`: permission denied
* `500 Internal Server Error`: unexpected error

## Permissions Endpoint Pattern

A common pattern is a dedicated endpoint that returns permissions for the client:

```typescript title="Express example"
app.get('/api/permissions', async (req, res) => {
  const userId = req.headers['x-user-id'] as string
  if (!userId) return res.status(401).json({ error: 'unauthorized' })

  const scope = req.headers['x-organization'] as string

  const permissions = await generatePermissionMap(engine, userId, [
    { action: 'create', resource: 'post', scope },
    { action: 'update', resource: 'post', scope },
    { action: 'delete', resource: 'post', scope },
    { action: 'manage', resource: 'dashboard', scope },
    { action: 'manage', resource: 'user', scope },
  ])

  res.json(permissions)
})
```

The client calls this endpoint and passes the result to `AccessProvider` (Chapter 7).

***

## Chapter 6 FAQ

Which framework integration should I use?

Use the integration that matches your backend. Express and Hono provide middleware
functions. NestJS provides decorators and guards. Next.js provides route handler
wrappers and Server Component helpers. If your framework isn't listed, use the generic
helpers (`generatePermissionMap`, `createSubjectCan`, `extractEnvironment`) and call
`engine.can()` directly in your middleware.

Should I use global middleware or per-route guards?

Per-route guards are more explicit: you see exactly what action and resource each
route requires. Global middleware is convenient when the URL structure maps cleanly
to resources. Most apps use both: global middleware for the common case and per-route
guards where you need explicit control.

How do I extract the user ID from a JWT?

duck-iam handles authorization only, not authentication. Use your existing auth
middleware (passport, next-auth, clerk, etc.) to verify the JWT and attach the user
to the request. Then pass `getUserId: (req) => req.user.id` to the duck-iam
middleware. Keeping auth and authz separate keeps both systems simpler.

How do I extract the scope from the URL?

Use the `getScope` callback. For a URL like `/api/orgs/acme/posts`, extract the
org slug: `getScope: (req) => req.params.orgId` (Express) or
`getScope: (c) => c.req.param('orgId')` (Hono). You can also read it from a header
(`X-Organization`) or from JWT claims.

Should I protect the admin router?

Yes. The admin router exposes mutation endpoints for policies, roles, and assignments.
Add authentication and authorization middleware before mounting it, for example
`guard(engine, 'manage', 'dashboard')` to restrict it to admin users only.

Do I need to change my middleware when switching engine modes?

No. `engine.can()` always returns a boolean regardless of mode, so all middleware,
guards, and route handlers work identically in both `'development'` and `'production'`
mode. Use `mode: 'production'` on servers for best throughput. It skips Decision
allocation, timing, and hooks, giving roughly 2x faster permission checks.

Does duck-iam depend on Express/Hono/NestJS?

No. Each integration uses lightweight type interfaces instead of importing the
framework, so duck-iam doesn't add the framework to your bundle or require specific
versions. The integration code uses generic request/response shapes compatible with
the framework's types.

How does NestJS action/resource inference work?

When `@Authorize()` uses `infer: true` (the default), the guard reads the HTTP method
to determine the action (via `IAM_METHOD_ACTION_MAP`) and parses the controller route path
for the resource type, taking the last non-parameter segment (ignoring `:id` style
params). For `@Controller('posts')` with `@Delete(':id')`, the inferred action is
`delete` and resource is `posts`.

***

Next: [Chapter 7: Client Libraries](/duck-iam/course/chapter-7)