{"_id":"@arbor-authz/nestjs","_rev":"3-0710eeea732637456c4a0e027592f8fc","name":"@arbor-authz/nestjs","dist-tags":{"latest":"0.3.0"},"versions":{"0.1.0":{"name":"@arbor-authz/nestjs","version":"0.1.0","license":"MIT","_id":"@arbor-authz/nestjs@0.1.0","maintainers":[{"name":"eneamunwe","email":"eneamunwe@gmail.com"}],"dist":{"shasum":"a7006ec4c5984ae7e1a9d56d6904e814f087d425","tarball":"https://registry.npmjs.org/@arbor-authz/nestjs/-/nestjs-0.1.0.tgz","fileCount":58,"integrity":"sha512-pZjrxz9qa6/IKBPP7Lsp01pIYXU2Bbp+ensGOTEz4YOO7RxdWQEeZUNgl1mrfBSlOEEIACOcB5jkJHTvNtX4zA==","signatures":[{"sig":"MEQCIG0Ahk+qJ/R7qAgaNxj5OMYYZMDNjVg2fbZjhA+48UxfAiBM10RjxldCjaM+6wAxabu9NQzyIC00bZKkUldYoK4Ixg==","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":217614},"main":"dist/index.js","types":"dist/index.d.ts","gitHead":"6fa90e4d6d3323c210ecf288a0f98ea9ef75641a","scripts":{"test":"jest","build":"tsc -p tsconfig.build.json","prepublishOnly":"npm run build"},"_npmUser":{"name":"eneamunwe","email":"eneamunwe@gmail.com"},"_npmVersion":"11.6.4","description":"NestJS client module for Arbor Cedar Authorization Platform","directories":{},"_nodeVersion":"22.13.0","dependencies":{"jsonwebtoken":"^9.0.2"},"_hasShrinkwrap":false,"devDependencies":{"jest":"^30.3.0","rxjs":"^7.8.1","ts-jest":"^29.4.6","typescript":"^5.5.0","@types/jest":"^30.0.0","@types/node":"^20.14.0","@nestjs/core":"^10.4.0","@nestjs/common":"^10.4.0","reflect-metadata":"^0.2.2","@types/jsonwebtoken":"^9.0.6","@nestjs/platform-express":"^10.4.0"},"peerDependencies":{"rxjs":"^7.0.0","@nestjs/core":"^10.0.0","@nestjs/common":"^10.0.0","reflect-metadata":"^0.1.13 || ^0.2.0"},"_npmOperationalInternal":{"tmp":"tmp/nestjs_0.1.0_1773670529795_0.8521382125251336","host":"s3://npm-registry-packages-npm-production"}},"0.2.0":{"name":"@arbor-authz/nestjs","version":"0.2.0","license":"MIT","_id":"@arbor-authz/nestjs@0.2.0","maintainers":[{"name":"eneamunwe","email":"eneamunwe@gmail.com"}],"dist":{"shasum":"31c85ce39fef784acd38828a27f9ae9146b8c265","tarball":"https://registry.npmjs.org/@arbor-authz/nestjs/-/nestjs-0.2.0.tgz","fileCount":58,"integrity":"sha512-6n1zYPxffKjnKNkrBoqSR7FEBXznNd/aBaAUikjamr/ppvsY4e26eHLwWPgYeuZzQrOX0vU5x50uuSiYUCC2eQ==","signatures":[{"sig":"MEQCICSryxpH/9s31rvt7Rr7EB7m9nCxaT15+sF2AxhNfd31AiAZcSP99b+Q6QBUU/W1rwgBbXewUKo6EOiich0euUZDsA==","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":222983},"main":"dist/index.js","types":"dist/index.d.ts","gitHead":"be786d10665fb5906a5b1db7fadddcf6381681f7","scripts":{"test":"jest","build":"tsc -p tsconfig.build.json","prepublishOnly":"npm run build"},"_npmUser":{"name":"eneamunwe","email":"eneamunwe@gmail.com"},"_npmVersion":"11.6.4","description":"NestJS client module for Arbor Cedar Authorization Platform","directories":{},"_nodeVersion":"22.13.0","dependencies":{"jsonwebtoken":"^9.0.2"},"_hasShrinkwrap":false,"devDependencies":{"jest":"^30.3.0","rxjs":"^7.8.1","ts-jest":"^29.4.6","typescript":"^5.5.0","@types/jest":"^30.0.0","@types/node":"^22.0.0","@nestjs/core":"^11.0.0","@nestjs/common":"^11.0.0","reflect-metadata":"^0.2.2","@types/jsonwebtoken":"^9.0.6","@nestjs/platform-express":"^11.0.0"},"peerDependencies":{"rxjs":"^7.0.0","@nestjs/core":"^10.0.0 || ^11.0.0","@nestjs/common":"^10.0.0 || ^11.0.0","reflect-metadata":"^0.1.13 || ^0.2.0"},"_npmOperationalInternal":{"tmp":"tmp/nestjs_0.2.0_1773701701709_0.7999777193664022","host":"s3://npm-registry-packages-npm-production"}},"0.3.0":{"name":"@arbor-authz/nestjs","version":"0.3.0","description":"NestJS client module for Arbor Cedar Authorization Platform","main":"dist/index.js","types":"dist/index.d.ts","scripts":{"build":"tsc -p tsconfig.build.json","test":"jest","prepublishOnly":"npm run build"},"peerDependencies":{"@nestjs/common":"^10.0.0 || ^11.0.0","@nestjs/core":"^10.0.0 || ^11.0.0","reflect-metadata":"^0.1.13 || ^0.2.0","rxjs":"^7.0.0"},"dependencies":{"jsonwebtoken":"^9.0.2"},"devDependencies":{"@nestjs/common":"^11.0.0","@nestjs/core":"^11.0.0","@nestjs/platform-express":"^11.0.0","@nestjs/testing":"^11.1.29","@types/jest":"^30.0.0","@types/jsonwebtoken":"^9.0.6","@types/node":"^22.0.0","jest":"^30.3.0","reflect-metadata":"^0.2.2","rxjs":"^7.8.1","ts-jest":"^29.4.6","typescript":"^5.5.0"},"license":"MIT","gitHead":"8aaf2527c1138e37d9aa9a27f74e7cc1404f54a8","_id":"@arbor-authz/nestjs@0.3.0","_nodeVersion":"24.12.0","_npmVersion":"11.6.2","dist":{"integrity":"sha512-920uLbW70IzLpKoT8GuEPA1Zx7sNKF0FfQ5MOi+gHot0Gsop55aCjkocQo/StGUK8SinjLr3NBr7Zv1EZ79UGA==","shasum":"07c74af124743d8202df73d1db741c32dbea4ea6","tarball":"https://registry.npmjs.org/@arbor-authz/nestjs/-/nestjs-0.3.0.tgz","fileCount":58,"unpackedSize":246652,"signatures":[{"keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U","sig":"MEYCIQCpbtLQmXVndAySSkHHlnFI26j3+2f7pfiHbjTUpEjvVQIhAPLCNwowUd23UdFsSqg0cQFByj4QX1/enbjfIfbUmPh0"}]},"_npmUser":{"name":"eneamunwe","email":"eneamunwe@gmail.com"},"directories":{},"maintainers":[{"name":"eneamunwe","email":"eneamunwe@gmail.com"}],"_npmOperationalInternal":{"host":"s3://npm-registry-packages-npm-production","tmp":"tmp/nestjs_0.3.0_1786594081056_0.8758655852646577"},"_hasShrinkwrap":false}},"time":{"created":"2026-03-16T14:15:29.720Z","modified":"2026-08-13T04:08:01.397Z","0.1.0":"2026-03-16T14:15:29.963Z","0.2.0":"2026-03-16T22:55:01.878Z","0.3.0":"2026-08-13T04:08:01.209Z"},"license":"MIT","description":"NestJS client module for Arbor Cedar Authorization Platform","maintainers":[{"name":"eneamunwe","email":"eneamunwe@gmail.com"}],"readme":"# @arbor-authz/nestjs\n\nNestJS client module for the [Arbor](https://github.com/Amunwe-ENE/arbor-server) Cedar authorization platform.\n\nProvides a guard, decorators, and services that integrate Cedar authorization into any NestJS application. Your app resolves entities from its own database; Arbor evaluates Cedar policies and returns cryptographically signed decisions.\n\n## Table of Contents\n\n- [Installation](#installation)\n- [Quick Start](#quick-start)\n- [Core Concepts](#core-concepts)\n- [Configuration](#configuration)\n- [Decorators](#decorators)\n- [Resolvers](#resolvers)\n- [Examples](#examples)\n  - [E-commerce](#example-e-commerce-platform)\n  - [Multi-tenant SaaS](#example-multi-tenant-saas)\n  - [Role hierarchy](#example-role-based-hierarchy)\n  - [Feature flags](#example-feature-flags-via-context)\n  - [Custom user extraction](#example-custom-user-extraction)\n  - [Audit-only mode](#example-audit-only-mode-ondeny-log)\n  - [Manual authorization](#example-manual-authorization-without-guard)\n  - [Microservices](#example-microservice--grpc)\n- [API Reference](#api-reference)\n- [Security Model](#security-model)\n- [Troubleshooting](#troubleshooting)\n\n## Installation\n\n```bash\nnpm install @arbor-authz/nestjs\n```\n\n**Peer dependencies** (install if not already present):\n\n```bash\nnpm install @nestjs/common @nestjs/core reflect-metadata rxjs\n```\n\nCompatible with NestJS 10 and 11.\n\n## Quick Start\n\n### 1. Implement a PrincipalResolver\n\nMaps the authenticated user on the request to a Cedar principal string.\n\n```typescript\n// src/authz/principal.resolver.ts\nimport { Injectable } from '@nestjs/common';\nimport { PrincipalResolver } from '@arbor-authz/nestjs';\n\n@Injectable()\nexport class MyPrincipalResolver implements PrincipalResolver {\n  resolve(user: any, request: any): string {\n    // Return a Cedar entity reference string\n    return `User::\"${user.id}\"`;\n  }\n}\n```\n\n### 2. Implement an EntityResolver\n\nLoads the Cedar entities that Arbor needs to evaluate policies. This is where you query your database.\n\n```typescript\n// src/authz/entity.resolver.ts\nimport { Injectable } from '@nestjs/common';\nimport { EntityResolver, CedarEntity } from '@arbor-authz/nestjs';\nimport { UsersService } from '../users/users.service';\n\n@Injectable()\nexport class MyEntityResolver implements EntityResolver {\n  constructor(private readonly users: UsersService) {}\n\n  async resolve(params: {\n    principal: string;\n    resource: string;\n    action: string;\n    context: Record<string, any>;\n    request: any;\n  }): Promise<CedarEntity[]> {\n    const userId = params.principal.match(/::\"(.+)\"/)?.[1];\n    const user = await this.users.findById(userId);\n\n    return [\n      {\n        uid: { type: 'User', id: user.id },\n        attrs: { role: user.role, verified: user.emailVerified },\n        parents: user.teams.map(t => ({ type: 'Team', id: t.id })),\n      },\n    ];\n  }\n}\n```\n\n### 3. Register the module\n\n```typescript\n// app.module.ts\nimport { Module } from '@nestjs/common';\nimport { AuthzModule } from '@arbor-authz/nestjs';\nimport { MyPrincipalResolver } from './authz/principal.resolver';\nimport { MyEntityResolver } from './authz/entity.resolver';\n\n@Module({\n  imports: [\n    AuthzModule.forRoot({\n      serviceUrl: 'http://localhost:3100',\n      principalResolver: MyPrincipalResolver,\n      entityResolver: MyEntityResolver,\n    }),\n  ],\n})\nexport class AppModule {}\n```\n\n### 4. Protect your routes\n\n```typescript\nimport { Controller, Get, Post, Body, Param, UseGuards } from '@nestjs/common';\nimport { AuthzGuard, CedarAction, CedarResource } from '@arbor-authz/nestjs';\n\n@Controller('documents')\n@UseGuards(AuthGuard('jwt'), AuthzGuard) // auth first, then authz\nexport class DocumentsController {\n  @Get(':id')\n  @CedarAction('viewDocument')\n  @CedarResource((req) => `Document::\"${req.params.id}\"`)\n  findOne(@Param('id') id: string) {\n    return this.documentsService.findOne(id);\n  }\n\n  @Post()\n  @CedarAction('createDocument')\n  @CedarResource('Document::\"new\"')\n  create(@Body() dto: CreateDocumentDto) {\n    return this.documentsService.create(dto);\n  }\n}\n```\n\nThat's it. Requests to these routes will be authorized via Cedar policies on your Arbor server.\n\n---\n\n## Core Concepts\n\n### How a request flows through the guard\n\n```\nHTTP Request\n    │\n    ▼\n┌──────────────────┐\n│  AuthzGuard      │\n│                  │\n│  1. Read @Cedar* │  ← decorator metadata\n│     metadata     │\n│                  │\n│  2. Extract user │  ← userExtractor(req) → user object\n│                  │\n│  3. Resolve      │  ← PrincipalResolver.resolve(user, req) → \"User::\\\"alice\\\"\"\n│     principal    │\n│                  │\n│  4. Resolve      │  ← @CedarResource value or function\n│     resource     │\n│                  │\n│  5. Build        │  ← { ip, method, path } + @CedarContext\n│     context      │\n│                  │\n│  6. Resolve      │  ← EntityResolver.resolve({ principal, resource, ... })\n│     entities     │    queries YOUR database, returns Cedar entities\n│                  │\n│  7. POST to      │  ← sends { principal, action, resource, context, entities }\n│    Arbor server  │    to http://arbor-server:3100/authorize\n│                  │\n│  8. Verify       │  ← RS256 signature, expiry, nonce, request binding\n│    signed token  │\n│                  │\n│  9. Allow/Deny   │  ← ForbiddenException or pass-through\n└──────────────────┘\n```\n\n### Cedar entity model\n\nCedar policies reference **entities** — typed objects with attributes and parent relationships. Your `EntityResolver` builds these from your database so that Cedar can evaluate attribute-based and relationship-based policies.\n\n```typescript\n// A CedarEntity has three fields:\n{\n  uid: { type: 'User', id: 'alice' },        // unique identifier\n  attrs: { role: 'admin', verified: true },   // attributes policies can check\n  parents: [{ type: 'Team', id: 'eng' }],    // group membership / hierarchy\n}\n```\n\n### What happens on your Arbor server\n\nArbor evaluates Cedar policies like:\n\n```cedar\n// Allow verified users to view documents\npermit(\n  principal is User,\n  action == Action::\"viewDocument\",\n  resource is Document\n)\nwhen { principal.verified == true };\n\n// Allow team leads to delete documents\npermit(\n  principal in Team::\"leads\",\n  action == Action::\"deleteDocument\",\n  resource is Document\n);\n\n// Deny suspended users from everything\nforbid(\n  principal is User,\n  action,\n  resource\n)\nwhen { principal.suspended == true };\n```\n\n---\n\n## Configuration\n\n### forRoot (static)\n\n```typescript\nAuthzModule.forRoot({\n  serviceUrl: 'http://arbor-server:3100',\n  principalResolver: MyPrincipalResolver,\n  entityResolver: MyEntityResolver,\n  timeout: 3000,          // optional: HTTP timeout in ms (default: 5000)\n  onDeny: 'throw',        // optional: 'throw' | 'log' (default: 'throw')\n  jwksRefreshInterval: 3600000, // optional: JWKS cache TTL in ms (default: 1 hour)\n  userExtractor: (req) => req.auth, // optional: how to get user from request\n})\n```\n\n### forRootAsync (dynamic / from ConfigService)\n\n```typescript\nimport { ConfigModule, ConfigService } from '@nestjs/config';\n\n@Module({\n  imports: [\n    ConfigModule.forRoot(),\n    AuthzModule.forRootAsync({\n      imports: [ConfigModule],\n      useFactory: (config: ConfigService) => ({\n        serviceUrl: config.getOrThrow('ARBOR_URL'),\n        principalResolver: MyPrincipalResolver,\n        entityResolver: MyEntityResolver,\n        timeout: config.get('ARBOR_TIMEOUT', 5000),\n        onDeny: config.get('ARBOR_ON_DENY', 'throw') as 'throw' | 'log',\n      }),\n      inject: [ConfigService],\n    }),\n  ],\n})\nexport class AppModule {}\n```\n\n### Options reference\n\n| Option | Type | Default | Description |\n|--------|------|---------|-------------|\n| `serviceUrl` | `string` | **required** | URL of your Arbor server |\n| `principalResolver` | `Type<PrincipalResolver>` | **required** | Class that maps user → Cedar principal |\n| `entityResolver` | `Type<EntityResolver>` | **required** | Class that loads Cedar entities from your DB |\n| `userExtractor` | `(req) => any` | `(req) => req.user` | How to get the authenticated user from the request |\n| `timeout` | `number` | `5000` | HTTP timeout in ms for Arbor requests |\n| `onDeny` | `'throw' \\| 'log'` | `'throw'` | `'throw'` returns 403; `'log'` logs and allows |\n| `jwksRefreshInterval` | `number` | `3600000` | How often to refresh JWKS public keys (ms) |\n\n---\n\n## Decorators\n\n### @CedarAction(name)\n\nSpecifies the Cedar action for a route handler. **This is the trigger** — routes without `@CedarAction` are not protected by the guard.\n\n```typescript\n@CedarAction('viewPayment')\n```\n\nThe guard automatically wraps this as `Action::\"viewPayment\"` when sending to Arbor.\n\n### @CedarResource(value)\n\nSpecifies the Cedar resource. Accepts a static string or a function that receives the request.\n\n```typescript\n// Static — same resource for every request\n@CedarResource('Dashboard::\"main\"')\n\n// Dynamic — resource depends on route params\n@CedarResource((req) => `Payment::\"${req.params.id}\"`)\n\n// Dynamic — resource from query\n@CedarResource((req) => `Report::\"${req.query.reportId}\"`)\n```\n\n### @CedarContext(fn)\n\nAdds extra key-value pairs to the authorization context. The guard already includes `{ ip, method, path }` automatically.\n\n```typescript\n// Add request body fields to the context\n@CedarContext((req) => ({\n  amount: req.body.amount,\n  currency: req.body.currency,\n}))\n\n// Add headers\n@CedarContext((req) => ({\n  origin: req.headers.origin,\n  userAgent: req.headers['user-agent'],\n}))\n```\n\nThis lets you write policies like:\n\n```cedar\npermit(\n  principal is Customer,\n  action == Action::\"createPayment\",\n  resource is Payment\n)\nwhen { context.amount < principal.dailyLimit };\n```\n\n### @SkipAuthz()\n\nSkips authorization for a specific route, even when `AuthzGuard` is applied at the controller level.\n\n```typescript\n@Controller('payments')\n@UseGuards(AuthGuard('jwt'), AuthzGuard)\nexport class PaymentsController {\n  @Get('health')\n  @SkipAuthz()        // no authorization check\n  healthCheck() {\n    return { status: 'ok' };\n  }\n\n  @Get(':id')\n  @CedarAction('viewPayment')\n  @CedarResource((req) => `Payment::\"${req.params.id}\"`)\n  findOne(@Param('id') id: string) { ... }\n}\n```\n\n---\n\n## Resolvers\n\n### PrincipalResolver\n\nMaps the authenticated user object to a Cedar principal string.\n\n```typescript\nexport interface PrincipalResolver {\n  resolve(user: any, request: any): string;\n}\n```\n\nThe `user` parameter comes from `userExtractor(req)` (defaults to `req.user`). The `request` is the raw HTTP request, useful when you need headers or other request data.\n\n**Tips:**\n- Return the Cedar entity reference format: `Type::\"id\"`\n- Use the user's role or type to choose the Cedar entity type\n- The principal **must** match an entity in your `EntityResolver` output\n\n### EntityResolver\n\nLoads Cedar entities from your database for policy evaluation.\n\n```typescript\nexport interface EntityResolver {\n  resolve(params: {\n    principal: string;   // e.g., 'Customer::\"alice\"'\n    resource: string;    // e.g., 'Payment::\"pay-123\"'\n    action: string;      // e.g., 'Action::\"viewPayment\"'\n    context: Record<string, any>;\n    request: any;        // raw HTTP request\n  }): Promise<CedarEntity[]>;\n}\n```\n\n**Tips:**\n- Return entities for both the principal AND the resource\n- Include `parents` to model group membership (Cedar `in` operator)\n- Only load what's needed — parse the principal/resource strings to get IDs\n- The `request` parameter lets you access anything on the HTTP request\n\n---\n\n## Examples\n\n### Example: E-commerce platform\n\n**Scenario:** Customers can view their own orders. Staff can view and refund any order.\n\n```typescript\n// principal.resolver.ts\n@Injectable()\nexport class EcommercePrincipalResolver implements PrincipalResolver {\n  resolve(user: any): string {\n    if (user.isStaff) return `Staff::\"${user.id}\"`;\n    return `Customer::\"${user.id}\"`;\n  }\n}\n\n// entity.resolver.ts\n@Injectable()\nexport class EcommerceEntityResolver implements EntityResolver {\n  constructor(\n    private readonly customers: CustomersService,\n    private readonly orders: OrdersService,\n  ) {}\n\n  async resolve(params): Promise<CedarEntity[]> {\n    const entities: CedarEntity[] = [];\n\n    // Load the principal entity\n    const principalMatch = params.principal.match(/^(\\w+)::\"(.+)\"$/);\n    const [, principalType, principalId] = principalMatch;\n\n    if (principalType === 'Customer') {\n      const customer = await this.customers.findById(principalId);\n      entities.push({\n        uid: { type: 'Customer', id: principalId },\n        attrs: {\n          verified: customer.emailVerified,\n          tier: customer.tier,\n          dailyLimit: customer.dailyLimit,\n        },\n        parents: [],\n      });\n    } else if (principalType === 'Staff') {\n      entities.push({\n        uid: { type: 'Staff', id: principalId },\n        attrs: { department: 'support' },\n        parents: [{ type: 'StaffRole', id: 'support' }],\n      });\n    }\n\n    // Load the resource entity\n    const resourceMatch = params.resource.match(/^(\\w+)::\"(.+)\"$/);\n    if (resourceMatch) {\n      const [, resourceType, resourceId] = resourceMatch;\n      if (resourceType === 'Order' && resourceId !== 'new') {\n        const order = await this.orders.findById(resourceId);\n        entities.push({\n          uid: { type: 'Order', id: resourceId },\n          attrs: {\n            amount: order.amount,\n            status: order.status,\n            customerId: order.customerId,\n          },\n          parents: [],\n        });\n      }\n    }\n\n    return entities;\n  }\n}\n\n// orders.controller.ts\n@Controller('orders')\n@UseGuards(AuthGuard('jwt'), AuthzGuard)\nexport class OrdersController {\n  @Get(':id')\n  @CedarAction('viewOrder')\n  @CedarResource((req) => `Order::\"${req.params.id}\"`)\n  findOne(@Param('id') id: string) { ... }\n\n  @Post(':id/refund')\n  @CedarAction('refundOrder')\n  @CedarResource((req) => `Order::\"${req.params.id}\"`)\n  @CedarContext((req) => ({ refundReason: req.body.reason }))\n  refund(@Param('id') id: string, @Body() dto: RefundDto) { ... }\n\n  @Post()\n  @CedarAction('createOrder')\n  @CedarResource('Order::\"new\"')\n  @CedarContext((req) => ({ amount: req.body.amount }))\n  create(@Body() dto: CreateOrderDto) { ... }\n}\n```\n\n**Cedar policies on Arbor:**\n\n```cedar\n// Customers can view their own orders\npermit(\n  principal is Customer,\n  action == Action::\"viewOrder\",\n  resource is Order\n)\nwhen { resource.customerId == principal.uid.id };\n\n// Staff can view any order\npermit(\n  principal is Staff,\n  action == Action::\"viewOrder\",\n  resource is Order\n);\n\n// Only support staff can refund\npermit(\n  principal in StaffRole::\"support\",\n  action == Action::\"refundOrder\",\n  resource is Order\n);\n\n// Verified customers can create orders under their limit\npermit(\n  principal is Customer,\n  action == Action::\"createOrder\",\n  resource is Order\n)\nwhen {\n  principal.verified == true &&\n  context.amount <= principal.dailyLimit\n};\n```\n\n---\n\n### Example: Multi-tenant SaaS\n\n**Scenario:** Users belong to organizations. They can only access resources within their org.\n\n```typescript\n// principal.resolver.ts\n@Injectable()\nexport class TenantPrincipalResolver implements PrincipalResolver {\n  resolve(user: any): string {\n    return `User::\"${user.id}\"`;\n  }\n}\n\n// entity.resolver.ts\n@Injectable()\nexport class TenantEntityResolver implements EntityResolver {\n  constructor(\n    private readonly users: UsersService,\n    private readonly projects: ProjectsService,\n  ) {}\n\n  async resolve(params): Promise<CedarEntity[]> {\n    const entities: CedarEntity[] = [];\n    const userId = params.principal.match(/::\"(.+)\"/)?.[1];\n    const user = await this.users.findById(userId);\n\n    // User with org membership\n    entities.push({\n      uid: { type: 'User', id: userId },\n      attrs: { role: user.orgRole, email: user.email },\n      parents: [{ type: 'Org', id: user.orgId }],\n    });\n\n    // The org itself\n    entities.push({\n      uid: { type: 'Org', id: user.orgId },\n      attrs: { plan: user.org.plan },\n      parents: [],\n    });\n\n    // Load the project resource if applicable\n    const projectMatch = params.resource.match(/^Project::\"(.+)\"$/);\n    if (projectMatch) {\n      const project = await this.projects.findById(projectMatch[1]);\n      entities.push({\n        uid: { type: 'Project', id: project.id },\n        attrs: { name: project.name },\n        parents: [{ type: 'Org', id: project.orgId }],\n      });\n    }\n\n    return entities;\n  }\n}\n\n// projects.controller.ts\n@Controller('projects')\n@UseGuards(AuthGuard('jwt'), AuthzGuard)\nexport class ProjectsController {\n  @Get(':id')\n  @CedarAction('viewProject')\n  @CedarResource((req) => `Project::\"${req.params.id}\"`)\n  findOne(@Param('id') id: string) { ... }\n\n  @Delete(':id')\n  @CedarAction('deleteProject')\n  @CedarResource((req) => `Project::\"${req.params.id}\"`)\n  remove(@Param('id') id: string) { ... }\n}\n```\n\n**Cedar policies:**\n\n```cedar\n// Users can view projects in their org\npermit(\n  principal is User,\n  action == Action::\"viewProject\",\n  resource is Project\n)\nwhen { principal in resource.parent(Org) };\n\n// Only admins can delete projects\npermit(\n  principal is User,\n  action == Action::\"deleteProject\",\n  resource is Project\n)\nwhen {\n  principal in resource.parent(Org) &&\n  principal.role == \"admin\"\n};\n```\n\n---\n\n### Example: Role-based hierarchy\n\n**Scenario:** Model role inheritance using Cedar's `in` operator and parent relationships.\n\n```typescript\n// entity.resolver.ts — use parents to model role hierarchy\n@Injectable()\nexport class RoleEntityResolver implements EntityResolver {\n  async resolve(params): Promise<CedarEntity[]> {\n    const userId = params.principal.match(/::\"(.+)\"/)?.[1];\n    const user = await this.users.findById(userId);\n\n    // Build role hierarchy: viewer < editor < admin\n    const roleHierarchy: Record<string, string[]> = {\n      viewer: [],\n      editor: ['viewer'],                    // editor inherits viewer\n      admin: ['editor', 'viewer'],           // admin inherits both\n    };\n\n    return [\n      {\n        uid: { type: 'User', id: userId },\n        attrs: {},\n        parents: [\n          { type: 'Role', id: user.role },\n          ...roleHierarchy[user.role].map(r => ({ type: 'Role', id: r })),\n        ],\n      },\n    ];\n  }\n}\n```\n\n**Cedar policies:**\n\n```cedar\n// Viewers can read\npermit(principal in Role::\"viewer\", action == Action::\"read\", resource);\n\n// Editors can read + write (inherits viewer via parents)\npermit(principal in Role::\"editor\", action == Action::\"write\", resource);\n\n// Admins can do anything (inherits editor + viewer)\npermit(principal in Role::\"admin\", action == Action::\"delete\", resource);\n```\n\n---\n\n### Example: Feature flags via context\n\n**Scenario:** Use Cedar context to gate features by environment, time, or request properties.\n\n```typescript\n@Controller('reports')\n@UseGuards(AuthGuard('jwt'), AuthzGuard)\nexport class ReportsController {\n  @Post('export')\n  @CedarAction('exportReport')\n  @CedarResource('Report::\"export\"')\n  @CedarContext((req) => ({\n    format: req.body.format,         // 'csv' | 'pdf' | 'xlsx'\n    rowCount: req.body.rowCount,\n    environment: process.env.NODE_ENV,\n  }))\n  export(@Body() dto: ExportDto) { ... }\n}\n```\n\n**Cedar policies:**\n\n```cedar\n// Everyone can export CSV\npermit(\n  principal is User,\n  action == Action::\"exportReport\",\n  resource\n)\nwhen { context.format == \"csv\" };\n\n// Only premium users can export PDF with more than 1000 rows\npermit(\n  principal is User,\n  action == Action::\"exportReport\",\n  resource\n)\nwhen {\n  context.format == \"pdf\" &&\n  (context.rowCount <= 1000 || principal.tier == \"premium\")\n};\n\n// Block all exports in staging\nforbid(\n  principal,\n  action == Action::\"exportReport\",\n  resource\n)\nwhen { context.environment == \"staging\" };\n```\n\n---\n\n### Example: Custom user extraction\n\n**Scenario:** Your app doesn't use `req.user` — maybe it's `req.auth`, `req.session.user`, or a custom decorator.\n\n```typescript\n// Passport puts the user on req.user (default — no config needed)\nAuthzModule.forRoot({\n  serviceUrl: 'http://arbor:3100',\n  principalResolver: MyPrincipalResolver,\n  entityResolver: MyEntityResolver,\n})\n\n// express-jwt / Auth0 puts it on req.auth\nAuthzModule.forRoot({\n  serviceUrl: 'http://arbor:3100',\n  principalResolver: MyPrincipalResolver,\n  entityResolver: MyEntityResolver,\n  userExtractor: (req) => req.auth,\n})\n\n// Custom header-based auth\nAuthzModule.forRoot({\n  serviceUrl: 'http://arbor:3100',\n  principalResolver: MyPrincipalResolver,\n  entityResolver: MyEntityResolver,\n  userExtractor: (req) => ({\n    id: req.headers['x-user-id'],\n    role: req.headers['x-user-role'],\n  }),\n})\n\n// Session-based auth\nAuthzModule.forRoot({\n  serviceUrl: 'http://arbor:3100',\n  principalResolver: MyPrincipalResolver,\n  entityResolver: MyEntityResolver,\n  userExtractor: (req) => req.session?.user,\n})\n```\n\n---\n\n### Example: Audit-only mode (onDeny: 'log')\n\n**Scenario:** You want to roll out authorization gradually — log denials without blocking requests.\n\n```typescript\nAuthzModule.forRoot({\n  serviceUrl: 'http://arbor:3100',\n  principalResolver: MyPrincipalResolver,\n  entityResolver: MyEntityResolver,\n  onDeny: 'log',  // logs \"Authz denied: ...\" but allows the request through\n})\n```\n\nThis is useful for:\n- Shadow-mode rollout of new policies\n- Auditing what would be blocked before enforcing\n- Debugging policy issues in production\n\nWhen ready to enforce, change to `onDeny: 'throw'` (the default).\n\n---\n\n### Example: Manual authorization without guard\n\n**Scenario:** You need to check authorization inside business logic, not just at the route level.\n\n```typescript\nimport { Injectable } from '@nestjs/common';\nimport { AuthzClientService, CedarEntity } from '@arbor-authz/nestjs';\n\n@Injectable()\nexport class PaymentService {\n  constructor(private readonly authz: AuthzClientService) {}\n\n  async processRefund(userId: string, paymentId: string, amount: number) {\n    // Build entities manually\n    const entities: CedarEntity[] = [\n      {\n        uid: { type: 'User', id: userId },\n        attrs: { role: 'support' },\n        parents: [],\n      },\n      {\n        uid: { type: 'Payment', id: paymentId },\n        attrs: { amount, status: 'completed' },\n        parents: [],\n      },\n    ];\n\n    // Check authorization programmatically\n    const allowed = await this.authz.authorize({\n      principal: `User::\"${userId}\"`,\n      action: 'Action::\"refundPayment\"',\n      resource: `Payment::\"${paymentId}\"`,\n      context: { refundAmount: amount },\n      entities,\n    });\n\n    if (!allowed) {\n      throw new Error('Not authorized to process this refund');\n    }\n\n    // ... proceed with refund\n  }\n}\n```\n\n---\n\n### Example: Microservice / gRPC\n\n**Scenario:** Authorize non-HTTP requests (gRPC, message queues, cron jobs) using `AuthzClientService` directly.\n\n```typescript\nimport { Injectable } from '@nestjs/common';\nimport { AuthzClientService } from '@arbor-authz/nestjs';\n\n@Injectable()\nexport class OrderEventHandler {\n  constructor(private readonly authz: AuthzClientService) {}\n\n  async handleOrderCancellation(event: {\n    userId: string;\n    orderId: string;\n    reason: string;\n  }) {\n    const allowed = await this.authz.authorize({\n      principal: `User::\"${event.userId}\"`,\n      action: 'Action::\"cancelOrder\"',\n      resource: `Order::\"${event.orderId}\"`,\n      context: { reason: event.reason, source: 'event-handler' },\n      entities: [\n        {\n          uid: { type: 'User', id: event.userId },\n          attrs: {},\n          parents: [],\n        },\n        {\n          uid: { type: 'Order', id: event.orderId },\n          attrs: {},\n          parents: [],\n        },\n      ],\n    });\n\n    if (!allowed) {\n      this.logger.warn(`User ${event.userId} denied cancelOrder on ${event.orderId}`);\n      return;\n    }\n\n    await this.orders.cancel(event.orderId, event.reason);\n  }\n}\n```\n\n---\n\n## API Reference\n\n### AuthzModule\n\n| Method | Description |\n|--------|-------------|\n| `AuthzModule.forRoot(options)` | Register with static configuration |\n| `AuthzModule.forRootAsync(options)` | Register with async/factory configuration |\n\nThe module is registered globally — you don't need to import it in every module.\n\n### Decorators\n\n| Decorator | Target | Description |\n|-----------|--------|-------------|\n| `@CedarAction('name')` | Method | Cedar action name (triggers the guard) |\n| `@CedarResource('Type::\"id\"')` | Method | Static Cedar resource |\n| `@CedarResource((req) => string)` | Method | Dynamic Cedar resource from request |\n| `@CedarContext((req) => object)` | Method | Additional context key-value pairs |\n| `@SkipAuthz()` | Method | Skip authorization for this route |\n\n### Interfaces\n\n#### CedarEntity\n\n```typescript\ninterface CedarEntity {\n  uid: { type: string; id: string };\n  attrs: Record<string, any>;\n  parents: Array<{ type: string; id: string }>;\n}\n```\n\n#### PrincipalResolver\n\n```typescript\ninterface PrincipalResolver {\n  resolve(user: any, request: any): string;\n}\n```\n\n#### EntityResolver\n\n```typescript\ninterface EntityResolver {\n  resolve(params: {\n    principal: string;\n    resource: string;\n    action: string;\n    context: Record<string, any>;\n    request: any;\n  }): Promise<CedarEntity[]>;\n}\n```\n\n#### AuthzModuleOptions\n\n```typescript\ninterface AuthzModuleOptions {\n  serviceUrl: string;\n  entityResolver: Type<EntityResolver>;\n  principalResolver: Type<PrincipalResolver>;\n  userExtractor?: (req: any) => any;\n  jwksRefreshInterval?: number;\n  timeout?: number;\n  onDeny?: 'throw' | 'log';\n}\n```\n\n### Exported Services\n\nFor advanced use cases, inject these services directly:\n\n| Service | Description |\n|---------|-------------|\n| `AuthzClientService` | Send authorization requests to Arbor server |\n| `TokenVerifierService` | Verify signed decision tokens manually |\n| `JwksCacheService` | Access cached JWKS public keys |\n| `NonceCacheService` | Check or manage the nonce replay cache |\n\n---\n\n## Security Model\n\nEvery authorization decision from Arbor is signed with RS256 and verified by this client. The verification chain ensures:\n\n| Check | What it prevents |\n|-------|-----------------|\n| **RS256 signature** | Forged or tampered decisions |\n| **Token expiry** (5s default) | Stale/replayed decisions from the past |\n| **Nonce uniqueness** | Replay attacks (same token used twice) |\n| **Request binding** | Token reuse across different requests — principal, action, resource are embedded in the JWT |\n| **Context hash** | Context tampering — SHA-256 of the context is embedded |\n| **Entity hash** | Entity tampering — SHA-256 of entities is embedded |\n\nThe JWKS public keys are fetched from `{serviceUrl}/.well-known/jwks.json` and cached for 1 hour (configurable via `jwksRefreshInterval`).\n\n---\n\n## Troubleshooting\n\n### \"Authorization configuration error\" (403)\n\nA route has `@CedarAction` but is missing `@CedarResource`. Add `@CedarResource` to the route handler.\n\n### \"Authorization service timeout\" (403)\n\nThe Arbor server didn't respond within the timeout window. Check:\n- Is the Arbor server running and reachable?\n- Increase `timeout` in module options if your entity resolver is slow\n\n### \"Authorization service unavailable\" (403)\n\nNetwork error reaching Arbor. Check connectivity between your app and the Arbor server.\n\n### Routes are not being protected\n\nThe guard only activates on routes with `@CedarAction`. Routes without it pass through. Make sure:\n- `AuthzGuard` is applied (via `@UseGuards` on the controller or method)\n- `@CedarAction('...')` is on each method you want to protect\n\n### \"Access denied\" when it should be allowed\n\n1. Use the Arbor Admin UI **Test Bench** to evaluate the same request and see which policies matched\n2. Check your `EntityResolver` is returning the right entities with correct attributes\n3. Check your `PrincipalResolver` is returning the correct Cedar principal string\n4. Check your Cedar policies on the Arbor server match the entity types you're sending\n\n### Using with other guards\n\n`AuthzGuard` should come **after** your authentication guard:\n\n```typescript\n@UseGuards(AuthGuard('jwt'), AuthzGuard)  // auth first, then authz\n```\n\nIf auth fails, authz is never reached. If you reverse the order, `AuthzGuard` won't have a user to resolve.\n\n---\n\n## License\n\nMIT\n","readmeFilename":"README.md"}