{"_id":"@ambushsoftworks/nestjs-rbac","name":"@ambushsoftworks/nestjs-rbac","dist-tags":{"latest":"0.1.0"},"versions":{"0.1.0":{"name":"@ambushsoftworks/nestjs-rbac","version":"0.1.0","description":"Framework-agnostic RBAC core for NestJS: per-account role/permission grant management with subset-confinement, anti-self-escalation, cross-tenant pinning, and anti-lockout guarantees","main":"dist/index.js","types":"dist/index.d.ts","scripts":{"build":"tsc","build:watch":"tsc --watch","test":"jest","test:watch":"jest --watch","test:cov":"jest --coverage","lint":"eslint \"{src,tests}/**/*.ts\" --fix","format":"prettier --write \"src/**/*.ts\" \"tests/**/*.ts\"","prepublishOnly":"npm run build"},"keywords":["nestjs","rbac","access-control","authorization","permissions","roles","subset-confinement","privilege-escalation","multi-tenant","anti-lockout"],"author":{"name":"Ambush Softworks"},"license":"MIT","repository":{"type":"git","url":"git+https://gitlab.com/ambushworks/nestjs-rbac.git"},"peerDependencies":{"@nestjs/common":"^10.0.0 || ^11.0.0","@nestjs/core":"^10.0.0 || ^11.0.0","reflect-metadata":"^0.2.0","rxjs":"^7.0.0"},"devDependencies":{"@nestjs/common":"^11.0.0","@nestjs/core":"^11.0.0","@nestjs/testing":"^11.0.0","@types/jest":"^29.5.11","@types/node":"^20.11.5","jest":"^29.7.0","reflect-metadata":"^0.2.2","rxjs":"^7.8.2","ts-jest":"^29.1.1","typescript":"^5.3.3"},"jest":{"moduleFileExtensions":["js","json","ts"],"rootDir":"src","testRegex":".*\\.spec\\.ts$","transform":{"^.+\\.(t|j)s$":"ts-jest"},"collectCoverageFrom":["**/*.(t|j)s"],"coverageDirectory":"../coverage","testEnvironment":"node"},"_id":"@ambushsoftworks/nestjs-rbac@0.1.0","gitHead":"b7ecb36932a4df60b0b966e0945260d2e5a7faab","bugs":{"url":"https://gitlab.com/ambushworks/nestjs-rbac/issues"},"homepage":"https://gitlab.com/ambushworks/nestjs-rbac#readme","_nodeVersion":"22.22.0","_npmVersion":"10.9.4","dist":{"integrity":"sha512-B2vgiCIVr13hLZ9m/KmhJSwcgREpI4kkWgTu2vD/TXyrb6tOPXoOU9Au4XRXLnea3w1qPNooTbz4A+DJ6nggyQ==","shasum":"df9d4053592c8a369e0afb42feee6611044dabe6","tarball":"https://registry.npmjs.org/@ambushsoftworks/nestjs-rbac/-/nestjs-rbac-0.1.0.tgz","fileCount":47,"unpackedSize":61327,"signatures":[{"keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U","sig":"MEUCIDIPbS0xAfUnteii03o3mir0HqDShf6VGWIaHksmArxKAiEA+RP77mJr/WbaLVO3jqtDS/aSPvqFKcmHjKUGC/UZ5HI="}]},"_npmUser":{"name":"ambushsoftworks","email":"lloyd@capson.ca"},"directories":{},"maintainers":[{"name":"ambushsoftworks","email":"lloyd@capson.ca"}],"_npmOperationalInternal":{"host":"s3://npm-registry-packages-npm-production","tmp":"tmp/nestjs-rbac_0.1.0_1780149914309_0.48046897823874746"},"_hasShrinkwrap":false}},"time":{"created":"2026-05-30T14:05:14.167Z","0.1.0":"2026-05-30T14:05:14.466Z","modified":"2026-05-30T14:05:14.653Z"},"maintainers":[{"name":"ambushsoftworks","email":"lloyd@capson.ca"}],"description":"Framework-agnostic RBAC core for NestJS: per-account role/permission grant management with subset-confinement, anti-self-escalation, cross-tenant pinning, and anti-lockout guarantees","homepage":"https://gitlab.com/ambushworks/nestjs-rbac#readme","keywords":["nestjs","rbac","access-control","authorization","permissions","roles","subset-confinement","privilege-escalation","multi-tenant","anti-lockout"],"repository":{"type":"git","url":"git+https://gitlab.com/ambushworks/nestjs-rbac.git"},"author":{"name":"Ambush Softworks"},"bugs":{"url":"https://gitlab.com/ambushworks/nestjs-rbac/issues"},"license":"MIT","readme":"# @ambushsoftworks/nestjs-rbac\n\nA framework-agnostic **RBAC grant-management core** for NestJS.\n\nIt ships the hard part of role/permission administration — the **security\nmechanism** — and lets your app supply the policy (the role→permission matrix)\nand the persistence (how grants are stored). The core has **zero** Prisma,\nORM, or domain coupling: NestJS is the only (peer) dependency.\n\nThe headline guarantee is **subset-confinement**: an administrator can only\never grant a subset of the access they themselves hold. Combined with\nanti-self-escalation, a cross-tenant pin, and anti-lockout, this closes the\nclassic privilege-escalation holes in self-service role management — and the\nchecks are unconditional throws in the service body, not opt-in guards.\n\n## Install\n\n```bash\nnpm install @ambushsoftworks/nestjs-rbac\n```\n\nNestJS packages are **peer dependencies** (so your app dedupes a single NestJS\ninstall):\n\n```bash\nnpm install @nestjs/common @nestjs/core reflect-metadata rxjs\n```\n\n## What it does\n\nThe core service, `PermissionAdminService`, manages per-account, DIVISION-scoped\ngrants:\n\n- `grantRole` / `revokeRole` / `changeMemberRole`\n- `grantPermission` / `revokePermission` (direct-permission exceptions)\n- `listRoles` / `listPermissions` / `effectiveGrants` (catalog + introspection)\n- `assertCanGrantRole` (caller-side confinement check, e.g. when creating a new\n  account + its first role in one transaction)\n\nEvery mutating operation enforces, **unconditionally**:\n\n| Guarantee | What it prevents |\n|---|---|\n| **Subset-confinement** | Granting any role/permission whose effect exceeds the caller's own effective permission set (privilege escalation). |\n| **Anti-self-escalation** | Granting a role/permission to one's own account. |\n| **Cross-tenant pin** | Managing an account that lives in a different division/tenant than the caller — no super-admin bypass on this surface. |\n| **Anti-lockout** | Revoking/demoting the last active \"owner\" of a division. The owner re-count happens inside the revoke transaction, so concurrent last-owner revokes can't both commit. |\n| **DIVISION-only scope** | Persisting malformed CLIENT/SERVICE-scoped grants on a surface that carries no resource id. |\n\nErrors are plain, transport-free subclasses you map to HTTP/GraphQL in your app:\n\n- `RbacForbiddenError` → 403\n- `RbacNotFoundError` → 404\n- `RbacConflictError` → 409\n\n## The interfaces you implement\n\nYou provide three things. None of them leak back into the core.\n\n### 1. `IPermissionAdminStore` — persistence\n\nHow role grants and direct-permission grants are stored, read, soft-revoked,\nand counted. Implement it over your ORM/db. Key methods:\n`getAccountDivisionId`, `getEffectiveGrants`, `hasActiveRole`,\n`hasActivePermission`, `grantRole`, `revokeRole`, `grantPermission`,\n`revokePermission`, `countActiveRoleHoldersInDivision`, and the atomic\n`revokeOwnerRoleIfNotLast` (the anti-lockout race guard — implement this inside\none transaction).\n\n### 2. `IRoleCatalog` — policy (the vocabulary)\n\nYour code-defined role→permission matrix. `listRoles`, `listPermissions`,\n`getRolePermissions(roleCode)`, `hasPermission(code)`. This stays in your app\nbecause the matrix is policy, not mechanism.\n\n### 3. `ownerRoleCodes` — anti-lockout policy\n\nThe role codes that anchor anti-lockout (at least one active holder must always\nremain in a division), e.g. `['ORG_OWNER', 'SUPER_ADMIN']`.\n\nOptionally, `IAccessGrantStore` — the bare relationship-grant primitive\n(\"account X has {read,write,share,delete} on resource-group Y\"). Only needed if\nyou consume the `ACCESS_GRANT_STORE` token; the management service does not\nrequire it.\n\n## Usage\n\n### Synchronous\n\n```typescript\nimport { Module } from '@nestjs/common';\nimport { RbacModule } from '@ambushsoftworks/nestjs-rbac';\nimport { MyPermissionAdminStore } from './rbac/my-permission-admin.store';\nimport { MyRoleCatalog } from './rbac/my-role-catalog';\n\n@Module({\n  imports: [\n    RbacModule.forRoot({\n      permissionAdminStore: new MyPermissionAdminStore(/* db */),\n      roleCatalog: new MyRoleCatalog(),\n      ownerRoleCodes: ['ORG_OWNER', 'SUPER_ADMIN'],\n    }),\n  ],\n})\nexport class AppModule {}\n```\n\n### Async (instances come from DI)\n\n```typescript\nimport { RbacModule } from '@ambushsoftworks/nestjs-rbac';\n\nRbacModule.forRootAsync({\n  imports: [PrismaModule],\n  inject: [MyPermissionAdminStore, MyRoleCatalog],\n  useFactory: (store: MyPermissionAdminStore, catalog: MyRoleCatalog) => ({\n    permissionAdminStore: store,\n    roleCatalog: catalog,\n    ownerRoleCodes: ['ORG_OWNER', 'SUPER_ADMIN'],\n  }),\n});\n```\n\n### Consuming the service\n\nInject the service by token and expose it through your own resolver/controller,\nmapping the core errors to your transport:\n\n```typescript\nimport { Inject, Injectable } from '@nestjs/common';\nimport {\n  PERMISSION_ADMIN_SERVICE,\n  PermissionAdminService,\n} from '@ambushsoftworks/nestjs-rbac';\n\n@Injectable()\nexport class MembersService {\n  constructor(\n    @Inject(PERMISSION_ADMIN_SERVICE)\n    private readonly rbac: PermissionAdminService,\n  ) {}\n\n  changeRole(callerAccountId: string, callerDivisionId: string, targetAccountId: string, newRoleCode: string) {\n    return this.rbac.changeMemberRole({\n      callerAccountId,\n      callerDivisionId,\n      targetAccountId,\n      newRoleCode,\n    });\n  }\n}\n```\n\n## Exports\n\n- `RbacModule` (+ `RbacModuleOptions`, `RbacModuleAsyncOptions`)\n- `PermissionAdminService`\n- Interfaces: `IPermissionAdminStore`, `IRoleCatalog`, `IAccessGrantStore`\n- Errors: `RbacError`, `RbacForbiddenError`, `RbacNotFoundError`, `RbacConflictError`\n- Tokens: `PERMISSION_ADMIN_SERVICE`, `PERMISSION_ADMIN_STORE`, `ROLE_CATALOG`,\n  `OWNER_ROLE_CODES`, `ACCESS_GRANT_STORE`\n- Plain types: `GrantScope`, `EffectiveGrants`, `RoleCatalogEntry`,\n  `PermissionCatalogEntry`, `AccessGrant`, and the grant/revoke input types.\n\n## A note on scope\n\nThis management surface is **DIVISION-only** by design — \"division\" being the\ngeneric tenant/workspace boundary. Narrower CLIENT/SERVICE-scoped grants (which\ncarry a resource id) are composed by your own adapters, not this surface; it\nrejects them rather than persist malformed rows. Cross-tenant and cross-org\nadministration belong to a separate platform-admin layer in your app, not here.\n\n## License\n\nMIT © Ambush Softworks\n","readmeFilename":"README.md","_rev":"1-f21c7530628942cd837b5805f8524fe4"}