{"_id":"@autorix/nestjs","name":"@autorix/nestjs","dist-tags":{"latest":"0.1.0"},"versions":{"0.1.0":{"name":"@autorix/nestjs","version":"0.1.0","description":"NestJS integration for Autorix policy evaluator","license":"MIT","type":"module","main":"./dist/index.cjs","module":"./dist/index.js","types":"./dist/index.d.ts","exports":{".":{"import":"./dist/index.js","require":"./dist/index.cjs"},"./package.json":"./package.json"},"sideEffects":false,"publishConfig":{"access":"public"},"scripts":{"build":"tsup src/index.ts --format cjs,esm --dts","test":"vitest -c ../../vitest.config.ts run","clean":"rm -rf dist"},"dependencies":{"@autorix/core":"^0.1.0","@autorix/storage":"^0.1.0","reflect-metadata":"^0.2.0"},"peerDependencies":{"@nestjs/common":"^11.0.0","@nestjs/core":"^11.0.0"},"devDependencies":{"@nestjs/common":"^11.0.0","@nestjs/core":"^11.0.0"},"_id":"@autorix/nestjs@0.1.0","gitHead":"1f16489a0f65a7008cff45592f431bf7d906bfc2","_nodeVersion":"22.13.1","_npmVersion":"10.9.2","dist":{"integrity":"sha512-s8YmENzQAtzpwRoaCPboxcbBRNMnfalRvQW0yV3rSDJ6MuK4UPtvZm1DKm/RrTSByYBFbfpA85b2j93hpmfKZw==","shasum":"bd12846b2b716ba1648a5ceaf3c309e3528f7f40","tarball":"https://registry.npmjs.org/@autorix/nestjs/-/nestjs-0.1.0.tgz","fileCount":9,"unpackedSize":55864,"signatures":[{"keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U","sig":"MEYCIQCDJ2sJ7c9Dzx48nisNQv0bp7pueVERD4ktNXpcJNLz3gIhAPECMKe83vH39+KrT1k133Ndfa8lgdfqPPdRkd8pDSJD"}]},"_npmUser":{"name":"chechoo","email":"sergiogalaz60@gmail.com"},"directories":{},"maintainers":[{"name":"chechoo","email":"sergiogalaz60@gmail.com"}],"_npmOperationalInternal":{"host":"s3://npm-registry-packages-npm-production","tmp":"tmp/nestjs_0.1.0_1768705269969_0.16550525689330575"},"_hasShrinkwrap":false}},"time":{"created":"2026-01-18T03:01:09.829Z","0.1.0":"2026-01-18T03:01:10.121Z","modified":"2026-01-18T03:01:10.419Z"},"maintainers":[{"name":"chechoo","email":"sergiogalaz60@gmail.com"}],"description":"NestJS integration for Autorix policy evaluator","license":"MIT","readme":"# @autorix/nestjs\n\n**NestJS Integration for Autorix** - Seamless authorization for your NestJS applications with decorators and guards.\n\n## 📋 Overview\n\n`@autorix/nestjs` provides a complete NestJS integration for the Autorix policy evaluation engine. Protect your routes and controllers with simple decorators while maintaining fine-grained, policy-based access control.\n\n## ✨ Features\n\n- 🎨 **Decorator-based API** - Clean and intuitive route protection\n- 🛡️ **Global Guard** - Apply authorization across your entire application\n- 🔧 **Highly Customizable** - Custom resolvers for principal, scope, and context\n- 🎯 **Resource-aware** - ABAC support with resource attributes and metadata\n- 🚀 **Zero-config Defaults** - Works out of the box with sensible defaults\n- 📦 **Type-safe** - Full TypeScript support\n- 🔄 **Multi-tenant Ready** - Built-in tenant isolation support\n\n## 📦 Installation\n\n```bash\nnpm install @autorix/nestjs @autorix/core @autorix/storage\n```\n\n```bash\npnpm add @autorix/nestjs @autorix/core @autorix/storage\n```\n\n```bash\nyarn add @autorix/nestjs @autorix/core @autorix/storage\n```\n\n## 🚀 Quick Start\n\n### 1. Setup Module\n\n```typescript\nimport { Module } from '@nestjs/common';\nimport { APP_GUARD } from '@nestjs/core';\nimport { AutorixModule, AutorixGuard } from '@autorix/nestjs';\nimport { MemoryPolicyProvider } from '@autorix/storage';\n\n@Module({\n  imports: [\n    AutorixModule.forRoot({\n      policyProvider: new MemoryPolicyProvider([\n        {\n          id: 'policy-1',\n          scope: { type: 'TENANT', id: 'tenant-123' },\n          document: {\n            Statement: [\n              {\n                Effect: 'Allow',\n                Action: ['document:read', 'document:list'],\n                Resource: 'document/*',\n              },\n            ],\n          },\n        },\n      ]),\n    }),\n  ],\n  providers: [\n    {\n      provide: APP_GUARD,\n      useClass: AutorixGuard,\n    },\n  ],\n})\nexport class AppModule {}\n```\n\n### 2. Protect Your Routes\n\n```typescript\nimport { Controller, Get, Post, Delete, Param } from '@nestjs/common';\nimport { Policy, ResourceParam } from '@autorix/nestjs';\n\n@Controller('documents')\nexport class DocumentController {\n  @Get()\n  @Policy('document:list')\n  async listDocuments() {\n    return this.documentService.findAll();\n  }\n\n  @Get(':id')\n  @Policy('document:read')\n  @ResourceParam('document')\n  async getDocument(@Param('id') id: string) {\n    return this.documentService.findOne(id);\n  }\n\n  @Post()\n  @Policy('document:create')\n  async createDocument(@Body() dto: CreateDocumentDto) {\n    return this.documentService.create(dto);\n  }\n\n  @Delete(':id')\n  @Policy('document:delete')\n  @ResourceParam('document')\n  async deleteDocument(@Param('id') id: string) {\n    return this.documentService.delete(id);\n  }\n}\n```\n\n## 📚 Core Concepts\n\n### Decorators\n\n#### `@Policy(...actions: string[])`\n\nDefines the actions required to access a route. Multiple actions can be specified.\n\n```typescript\n@Policy('document:read')\n@Get(':id')\ngetDocument() { }\n\n@Policy('document:read', 'document:update')\n@Get(':id')\nviewAndEdit() { }\n```\n\n#### `@ResourceParam(type: string, param?: string)`\n\nExtracts resource information from route parameters. By default, uses the `id` parameter.\n\n```typescript\n@ResourceParam('document')           // Uses :id param\n@Get(':id')\n\n@ResourceParam('user', 'userId')     // Uses :userId param\n@Get(':userId')\n```\n\n#### `@Resource(meta: AutorixResourceMeta)`\n\nAdvanced resource resolution with custom resolvers for ID, attributes, and tenant.\n\n```typescript\n@Resource({\n  type: 'document',\n  id: async ({ req }) => req.params.id,\n  attributes: async ({ req }) => {\n    const doc = await getDocument(req.params.id);\n    return { \n      ownerId: doc.ownerId,\n      isPublic: doc.isPublic \n    };\n  },\n  tenantId: async ({ req }) => req.user.tenantId,\n})\n@Get(':id')\n```\n\n## 🔧 Configuration\n\n### Module Options\n\nConfigure Autorix behavior with custom resolvers:\n\n```typescript\nAutorixModule.forRoot({\n  policyProvider: myPolicyProvider,\n  options: {\n    scopeResolver: async (ctx: ExecutionContext) => {\n      const req = ctx.switchToHttp().getRequest();\n      return {\n        type: 'TENANT',\n        id: req.headers['x-tenant-id'],\n      };\n    },\n    \n    principalResolver: async (ctx: ExecutionContext) => {\n      const req = ctx.switchToHttp().getRequest();\n      return {\n        principalId: req.user.id,\n        roleIds: req.user.roles,\n        groupIds: req.user.groups,\n        principalAttributes: {\n          email: req.user.email,\n          department: req.user.department,\n        },\n      };\n    },\n    \n    contextResolver: async (ctx, scope, principal, resource) => {\n      const req = ctx.switchToHttp().getRequest();\n      return {\n        principal: {\n          id: principal.principalId,\n          tenantId: scope?.id,\n          roles: principal.roleIds,\n          ...principal.principalAttributes,\n        },\n        resource,\n        request: {\n          method: req.method,\n          path: req.url,\n          ip: req.ip,\n        },\n        scope,\n      };\n    },\n  },\n})\n```\n\n### Options Interface\n\n```typescript\ninterface AutorixNestjsOptions {\n  // Determines the scope (tenant/workspace/etc) for loading policies\n  scopeResolver?: (ctx: ExecutionContext) => \n    Promise<AutorixScope> | AutorixScope;\n  \n  // Determines who the principal (user) is + roles/groups\n  principalResolver?: (ctx: ExecutionContext) => \n    Promise<PrincipalResolverResult> | PrincipalResolverResult;\n  \n  // Builds the canonical AutorixContext for ABAC\n  contextResolver?: (\n    ctx: ExecutionContext,\n    scope: AutorixScope,\n    principal: PrincipalResolverResult,\n    resource?: Record<string, any>\n  ) => Promise<AutorixContext> | AutorixContext;\n}\n```\n\n## 🎯 Advanced Usage\n\n### Multi-Action Policies\n\nRequire multiple actions to be allowed:\n\n```typescript\n@Controller('admin')\nexport class AdminController {\n  @Policy('admin:access', 'user:manage')\n  @Get('users')\n  manageUsers() {\n    // Requires BOTH actions to be allowed\n  }\n}\n```\n\n### Class-level Policies\n\nApply policies to all routes in a controller:\n\n```typescript\n@Controller('documents')\n@Policy('document:access')  // Required for all routes\nexport class DocumentController {\n  @Get()\n  @Policy('document:list')  // Requires BOTH document:access AND document:list\n  list() { }\n  \n  @Get(':id')\n  @Policy('document:read')  // Requires BOTH document:access AND document:read\n  get() { }\n}\n```\n\n### Resource with Custom Attributes\n\nUse resource attributes in policy conditions:\n\n```typescript\n@Controller('documents')\nexport class DocumentController {\n  @Get(':id')\n  @Policy('document:read')\n  @Resource({\n    type: 'document',\n    id: async ({ req }) => req.params.id,\n    attributes: async ({ req }) => {\n      const document = await this.service.findOne(req.params.id);\n      return {\n        ownerId: document.ownerId,\n        isPublic: document.isPublic,\n        status: document.status,\n      };\n    },\n  })\n  async getDocument(@Param('id') id: string) {\n    return this.service.findOne(id);\n  }\n}\n```\n\n**Policy Example:**\n```typescript\n{\n  Statement: [\n    {\n      Effect: 'Allow',\n      Action: 'document:read',\n      Resource: 'document/*',\n      Condition: {\n        StringEquals: {\n          'resource.ownerId': '${principal.id}',\n        },\n      },\n    },\n    {\n      Effect: 'Allow',\n      Action: 'document:read',\n      Resource: 'document/*',\n      Condition: {\n        Bool: {\n          'resource.isPublic': true,\n        },\n      },\n    },\n  ],\n}\n```\n\n### Custom Scope Resolver\n\nImplement custom scoping logic:\n\n```typescript\nAutorixModule.forRoot({\n  policyProvider: myProvider,\n  options: {\n    scopeResolver: async (ctx) => {\n      const req = ctx.switchToHttp().getRequest();\n      \n      // Workspace-level scoping\n      if (req.headers['x-workspace-id']) {\n        return {\n          type: 'WORKSPACE',\n          id: req.headers['x-workspace-id'],\n        };\n      }\n      \n      // Tenant-level scoping\n      if (req.user?.tenantId) {\n        return {\n          type: 'TENANT',\n          id: req.user.tenantId,\n        };\n      }\n      \n      // Global scope\n      return { type: 'GLOBAL' };\n    },\n  },\n})\n```\n\n### Custom Principal Resolver\n\nExtract principal information from different sources:\n\n```typescript\nAutorixModule.forRoot({\n  policyProvider: myProvider,\n  options: {\n    principalResolver: async (ctx) => {\n      const req = ctx.switchToHttp().getRequest();\n      \n      // From JWT token\n      const user = req.user;\n      \n      // Fetch additional attributes from database\n      const userDetails = await userService.getDetails(user.id);\n      \n      return {\n        principalId: user.id,\n        roleIds: userDetails.roles.map(r => r.id),\n        groupIds: userDetails.groups.map(g => g.id),\n        principalAttributes: {\n          email: user.email,\n          department: userDetails.department,\n          level: userDetails.level,\n          isVerified: userDetails.emailVerified,\n        },\n      };\n    },\n  },\n})\n```\n\n## 🔍 Examples\n\n### Example 1: Basic CRUD with Authorization\n\n```typescript\nimport { Controller, Get, Post, Put, Delete, Body, Param } from '@nestjs/common';\nimport { Policy, ResourceParam } from '@autorix/nestjs';\n\n@Controller('articles')\nexport class ArticleController {\n  constructor(private readonly articleService: ArticleService) {}\n\n  @Get()\n  @Policy('article:list')\n  async list() {\n    return this.articleService.findAll();\n  }\n\n  @Get(':id')\n  @Policy('article:read')\n  @ResourceParam('article')\n  async get(@Param('id') id: string) {\n    return this.articleService.findOne(id);\n  }\n\n  @Post()\n  @Policy('article:create')\n  async create(@Body() dto: CreateArticleDto) {\n    return this.articleService.create(dto);\n  }\n\n  @Put(':id')\n  @Policy('article:update')\n  @ResourceParam('article')\n  async update(@Param('id') id: string, @Body() dto: UpdateArticleDto) {\n    return this.articleService.update(id, dto);\n  }\n\n  @Delete(':id')\n  @Policy('article:delete')\n  @ResourceParam('article')\n  async delete(@Param('id') id: string) {\n    return this.articleService.delete(id);\n  }\n}\n```\n\n**Corresponding Policy:**\n```typescript\n{\n  Statement: [\n    {\n      Sid: 'AllowReadPublic',\n      Effect: 'Allow',\n      Action: ['article:list', 'article:read'],\n      Resource: 'article/*',\n    },\n    {\n      Sid: 'AllowOwnArticleManagement',\n      Effect: 'Allow',\n      Action: ['article:*'],\n      Resource: 'article/*',\n      Condition: {\n        StringEquals: {\n          'resource.ownerId': '${principal.id}',\n        },\n      },\n    },\n    {\n      Sid: 'AdminFullAccess',\n      Effect: 'Allow',\n      Action: 'article:*',\n      Resource: 'article/*',\n      Condition: {\n        StringLike: {\n          'principal.roles': '*admin*',\n        },\n      },\n    },\n  ],\n}\n```\n\n### Example 2: Multi-tenant Application\n\n```typescript\n@Controller('projects')\nexport class ProjectController {\n  @Get()\n  @Policy('project:list')\n  async list(@Req() req: Request) {\n    // Policy ensures user can only see projects in their tenant\n    return this.projectService.findAll(req.user.tenantId);\n  }\n\n  @Post()\n  @Policy('project:create')\n  async create(@Body() dto: CreateProjectDto) {\n    return this.projectService.create(dto);\n  }\n\n  @Get(':id')\n  @Policy('project:read')\n  @Resource({\n    type: 'project',\n    id: ({ req }) => req.params.id,\n    tenantId: async ({ req }) => {\n      const project = await this.projectService.findOne(req.params.id);\n      return project.tenantId;\n    },\n  })\n  async get(@Param('id') id: string) {\n    return this.projectService.findOne(id);\n  }\n}\n```\n\n**Multi-tenant Policy:**\n```typescript\n{\n  Statement: [\n    {\n      Sid: 'SameTenantOnly',\n      Effect: 'Allow',\n      Action: 'project:*',\n      Resource: 'project/*',\n      Condition: {\n        StringEquals: {\n          'principal.tenantId': '${resource.tenantId}',\n        },\n      },\n    },\n  ],\n}\n```\n\n### Example 3: Role-based with ABAC\n\n```typescript\n@Controller('reports')\nexport class ReportController {\n  @Get()\n  @Policy('report:list')\n  async list() {\n    return this.reportService.findAll();\n  }\n\n  @Get(':id')\n  @Policy('report:read')\n  @Resource({\n    type: 'report',\n    id: ({ req }) => req.params.id,\n    attributes: async ({ req }) => {\n      const report = await this.reportService.findOne(req.params.id);\n      return {\n        ownerId: report.ownerId,\n        department: report.department,\n        sensitivity: report.sensitivity,\n      };\n    },\n  })\n  async get(@Param('id') id: string) {\n    return this.reportService.findOne(id);\n  }\n\n  @Post()\n  @Policy('report:create')\n  async create(@Body() dto: CreateReportDto) {\n    return this.reportService.create(dto);\n  }\n}\n```\n\n**ABAC Policy:**\n```typescript\n{\n  Statement: [\n    {\n      Sid: 'OwnReports',\n      Effect: 'Allow',\n      Action: 'report:*',\n      Resource: 'report/*',\n      Condition: {\n        StringEquals: {\n          'resource.ownerId': '${principal.id}',\n        },\n      },\n    },\n    {\n      Sid: 'DepartmentReports',\n      Effect: 'Allow',\n      Action: ['report:read', 'report:list'],\n      Resource: 'report/*',\n      Condition: {\n        StringEquals: {\n          'resource.department': '${principal.department}',\n        },\n      },\n    },\n    {\n      Sid: 'DenySensitive',\n      Effect: 'Deny',\n      Action: 'report:read',\n      Resource: 'report/*',\n      Condition: {\n        StringEquals: {\n          'resource.sensitivity': 'high',\n        },\n      },\n    },\n  ],\n}\n```\n\n## 🔐 Default Behavior\n\n### Default Scope Resolver\nExtracts tenant from `req.tenantId` or `req.user.tenantId`:\n```typescript\n{ type: 'TENANT', id: req.user.tenantId }\n```\n\n### Default Principal Resolver\nExtracts principal from `req.user`:\n```typescript\n{\n  principalId: req.user.id ?? req.user.sub,\n  roleIds: req.user.roles ?? [],\n  groupIds: req.user.groups ?? [],\n  principalAttributes: req.user,\n}\n```\n\n### Default Context Resolver\nBuilds context with principal, request info, and resource:\n```typescript\n{\n  principal: {\n    id: principalId,\n    tenantId: scope?.id,\n    roles: roleIds,\n    ...principalAttributes,\n  },\n  resource: resource ?? undefined,\n  request: {\n    method: req.method,\n    path: req.url,\n  },\n  scope: { type: scope.type, id: scope.id },\n}\n```\n\n## 🚨 Error Handling\n\nThe guard throws standard NestJS exceptions:\n\n- **`UnauthorizedException`** - When principal cannot be resolved (no `req.user`)\n- **`ForbiddenException`** - When policy evaluation denies access or resource resolution fails\n\n```typescript\n@Controller('documents')\nexport class DocumentController {\n  @Get(':id')\n  @Policy('document:read')\n  @ResourceParam('document')\n  async get(@Param('id') id: string) {\n    // If denied: throws ForbiddenException\n    // If no user: throws UnauthorizedException\n    return this.service.findOne(id);\n  }\n}\n```\n\nHandle exceptions globally:\n\n```typescript\nimport { ExceptionFilter, Catch, ArgumentsHost, ForbiddenException } from '@nestjs/common';\n\n@Catch(ForbiddenException)\nexport class AutorixExceptionFilter implements ExceptionFilter {\n  catch(exception: ForbiddenException, host: ArgumentsHost) {\n    const ctx = host.switchToHttp();\n    const response = ctx.getResponse();\n    \n    response.status(403).json({\n      statusCode: 403,\n      message: 'Access Denied',\n      error: 'Forbidden',\n      details: exception.message,\n    });\n  }\n}\n```\n\n## 🧪 Testing\n\n### Testing Controllers with Autorix\n\n```typescript\nimport { Test } from '@nestjs/testing';\nimport { AutorixGuard } from '@autorix/nestjs';\nimport { DocumentController } from './document.controller';\n\ndescribe('DocumentController', () => {\n  let controller: DocumentController;\n  let guard: AutorixGuard;\n\n  beforeEach(async () => {\n    const moduleRef = await Test.createTestingModule({\n      controllers: [DocumentController],\n      providers: [\n        {\n          provide: AutorixGuard,\n          useValue: {\n            canActivate: jest.fn().mockResolvedValue(true),\n          },\n        },\n      ],\n    }).compile();\n\n    controller = moduleRef.get(DocumentController);\n    guard = moduleRef.get(AutorixGuard);\n  });\n\n  it('should allow access when authorized', async () => {\n    const result = await controller.list();\n    expect(guard.canActivate).toHaveBeenCalled();\n    expect(result).toBeDefined();\n  });\n\n  it('should deny access when unauthorized', async () => {\n    jest.spyOn(guard, 'canActivate').mockResolvedValue(false);\n    \n    await expect(controller.list()).rejects.toThrow(ForbiddenException);\n  });\n});\n```\n\n### Integration Testing\n\n```typescript\nimport { Test } from '@nestjs/testing';\nimport { INestApplication } from '@nestjs/common';\nimport * as request from 'supertest';\nimport { AppModule } from './app.module';\n\ndescribe('Authorization (e2e)', () => {\n  let app: INestApplication;\n\n  beforeAll(async () => {\n    const moduleRef = await Test.createTestingModule({\n      imports: [AppModule],\n    }).compile();\n\n    app = moduleRef.createNestApplication();\n    await app.init();\n  });\n\n  it('GET /documents - should allow with valid token', () => {\n    return request(app.getHttpServer())\n      .get('/documents')\n      .set('Authorization', 'Bearer valid-token')\n      .expect(200);\n  });\n\n  it('GET /documents - should deny without token', () => {\n    return request(app.getHttpServer())\n      .get('/documents')\n      .expect(401);\n  });\n\n  afterAll(async () => {\n    await app.close();\n  });\n});\n```\n\n## 📊 Best Practices\n\n1. **Use Class-level policies for common requirements**\n   ```typescript\n   @Controller('admin')\n   @Policy('admin:access')  // All routes require this\n   export class AdminController { }\n   ```\n\n2. **Combine @Policy with @ResourceParam for resource-specific checks**\n   ```typescript\n   @Delete(':id')\n   @Policy('document:delete')\n   @ResourceParam('document')\n   async delete(@Param('id') id: string) { }\n   ```\n\n3. **Use @Resource for complex ABAC scenarios**\n   ```typescript\n   @Resource({\n     type: 'document',\n     id: ({ req }) => req.params.id,\n     attributes: async ({ req }) => await fetchAttributes(req.params.id),\n   })\n   ```\n\n4. **Keep policies in a centralized location**\n   - Store policies in a database\n   - Version control your policy definitions\n   - Use policy templates for common patterns\n\n5. **Test your authorization logic thoroughly**\n   - Test both allow and deny scenarios\n   - Test with different roles and contexts\n   - Use integration tests for critical paths\n\n## 🔗 Related Packages\n\n- **[@autorix/core](../core)** - Core policy evaluation engine\n- **[@autorix/storage](../storage)** - Policy storage providers\n\n## 📄 License\n\nMIT © Autorix\n\n## 🤝 Contributing\n\nContributions are welcome! Please feel free to submit a Pull Request.\n\n## 📞 Support\n\nFor issues and questions, please use the [GitHub Issues](https://github.com/yourusername/autorix/issues) page.\n","readmeFilename":"README.md","_rev":"1-8f65c582ec873e9541a78e0acb683eca"}