npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@ambushsoftworks/nestjs-rbac

v0.1.0

Published

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

Readme

@ambushsoftworks/nestjs-rbac

A framework-agnostic RBAC grant-management core for NestJS.

It ships the hard part of role/permission administration — the security mechanism — and lets your app supply the policy (the role→permission matrix) and the persistence (how grants are stored). The core has zero Prisma, ORM, or domain coupling: NestJS is the only (peer) dependency.

The headline guarantee is subset-confinement: an administrator can only ever grant a subset of the access they themselves hold. Combined with anti-self-escalation, a cross-tenant pin, and anti-lockout, this closes the classic privilege-escalation holes in self-service role management — and the checks are unconditional throws in the service body, not opt-in guards.

Install

npm install @ambushsoftworks/nestjs-rbac

NestJS packages are peer dependencies (so your app dedupes a single NestJS install):

npm install @nestjs/common @nestjs/core reflect-metadata rxjs

What it does

The core service, PermissionAdminService, manages per-account, DIVISION-scoped grants:

  • grantRole / revokeRole / changeMemberRole
  • grantPermission / revokePermission (direct-permission exceptions)
  • listRoles / listPermissions / effectiveGrants (catalog + introspection)
  • assertCanGrantRole (caller-side confinement check, e.g. when creating a new account + its first role in one transaction)

Every mutating operation enforces, unconditionally:

| Guarantee | What it prevents | |---|---| | Subset-confinement | Granting any role/permission whose effect exceeds the caller's own effective permission set (privilege escalation). | | Anti-self-escalation | Granting a role/permission to one's own account. | | Cross-tenant pin | Managing an account that lives in a different division/tenant than the caller — no super-admin bypass on this surface. | | 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. | | DIVISION-only scope | Persisting malformed CLIENT/SERVICE-scoped grants on a surface that carries no resource id. |

Errors are plain, transport-free subclasses you map to HTTP/GraphQL in your app:

  • RbacForbiddenError → 403
  • RbacNotFoundError → 404
  • RbacConflictError → 409

The interfaces you implement

You provide three things. None of them leak back into the core.

1. IPermissionAdminStore — persistence

How role grants and direct-permission grants are stored, read, soft-revoked, and counted. Implement it over your ORM/db. Key methods: getAccountDivisionId, getEffectiveGrants, hasActiveRole, hasActivePermission, grantRole, revokeRole, grantPermission, revokePermission, countActiveRoleHoldersInDivision, and the atomic revokeOwnerRoleIfNotLast (the anti-lockout race guard — implement this inside one transaction).

2. IRoleCatalog — policy (the vocabulary)

Your code-defined role→permission matrix. listRoles, listPermissions, getRolePermissions(roleCode), hasPermission(code). This stays in your app because the matrix is policy, not mechanism.

3. ownerRoleCodes — anti-lockout policy

The role codes that anchor anti-lockout (at least one active holder must always remain in a division), e.g. ['ORG_OWNER', 'SUPER_ADMIN'].

Optionally, IAccessGrantStore — the bare relationship-grant primitive ("account X has {read,write,share,delete} on resource-group Y"). Only needed if you consume the ACCESS_GRANT_STORE token; the management service does not require it.

Usage

Synchronous

import { Module } from '@nestjs/common';
import { RbacModule } from '@ambushsoftworks/nestjs-rbac';
import { MyPermissionAdminStore } from './rbac/my-permission-admin.store';
import { MyRoleCatalog } from './rbac/my-role-catalog';

@Module({
  imports: [
    RbacModule.forRoot({
      permissionAdminStore: new MyPermissionAdminStore(/* db */),
      roleCatalog: new MyRoleCatalog(),
      ownerRoleCodes: ['ORG_OWNER', 'SUPER_ADMIN'],
    }),
  ],
})
export class AppModule {}

Async (instances come from DI)

import { RbacModule } from '@ambushsoftworks/nestjs-rbac';

RbacModule.forRootAsync({
  imports: [PrismaModule],
  inject: [MyPermissionAdminStore, MyRoleCatalog],
  useFactory: (store: MyPermissionAdminStore, catalog: MyRoleCatalog) => ({
    permissionAdminStore: store,
    roleCatalog: catalog,
    ownerRoleCodes: ['ORG_OWNER', 'SUPER_ADMIN'],
  }),
});

Consuming the service

Inject the service by token and expose it through your own resolver/controller, mapping the core errors to your transport:

import { Inject, Injectable } from '@nestjs/common';
import {
  PERMISSION_ADMIN_SERVICE,
  PermissionAdminService,
} from '@ambushsoftworks/nestjs-rbac';

@Injectable()
export class MembersService {
  constructor(
    @Inject(PERMISSION_ADMIN_SERVICE)
    private readonly rbac: PermissionAdminService,
  ) {}

  changeRole(callerAccountId: string, callerDivisionId: string, targetAccountId: string, newRoleCode: string) {
    return this.rbac.changeMemberRole({
      callerAccountId,
      callerDivisionId,
      targetAccountId,
      newRoleCode,
    });
  }
}

Exports

  • RbacModule (+ RbacModuleOptions, RbacModuleAsyncOptions)
  • PermissionAdminService
  • Interfaces: IPermissionAdminStore, IRoleCatalog, IAccessGrantStore
  • Errors: RbacError, RbacForbiddenError, RbacNotFoundError, RbacConflictError
  • Tokens: PERMISSION_ADMIN_SERVICE, PERMISSION_ADMIN_STORE, ROLE_CATALOG, OWNER_ROLE_CODES, ACCESS_GRANT_STORE
  • Plain types: GrantScope, EffectiveGrants, RoleCatalogEntry, PermissionCatalogEntry, AccessGrant, and the grant/revoke input types.

A note on scope

This management surface is DIVISION-only by design — "division" being the generic tenant/workspace boundary. Narrower CLIENT/SERVICE-scoped grants (which carry a resource id) are composed by your own adapters, not this surface; it rejects them rather than persist malformed rows. Cross-tenant and cross-org administration belong to a separate platform-admin layer in your app, not here.

License

MIT © Ambush Softworks