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

@nebutra/permissions

v0.1.2

Published

> **Status: Foundation** — CASL in-process evaluation supports deterministic role inheritance, ABAC conditions, and field-level rules. OpenFGA support now uses the store-scoped REST API, but model and tuple lifecycle management remain external to this pac

Readme

Status: Foundation — CASL in-process evaluation supports deterministic role inheritance, ABAC conditions, and field-level rules. OpenFGA support now uses the store-scoped REST API, but model and tuple lifecycle management remain external to this package.

@nebutra/permissions

RBAC (Role-Based Access Control) and ABAC (Attribute-Based Access Control) permissions engine for the Nebutra-Sailor monorepo. Built on CASL with optional OpenFGA support for relationship-based access at scale.

Features

  • CASL-based in-process evaluation — Fast, no network calls, great for UI + API middleware
  • OpenFGA integration — Store-scoped REST checks, writes, deletes, and list-objects calls for managed/self-hosted Zanzibar relationship graphs
  • Role hierarchy — Roles inherit permissions from parent roles; child rules override inherited grants deterministically
  • ABAC conditions — Dynamic field resolution at evaluation time
  • Field-level permissions — Restrict access to specific fields through CASL-backed checks
  • React hooks & componentsusePermission(), <Can> component for UI gate-keeping
  • Hono middleware — Automatic permission checks in API routes
  • Provider auto-detection — Automatically picks CASL or OpenFGA based on env vars

Installation

pnpm add @nebutra/permissions

Quick Start

1. API Middleware (Hono)

import { createPermissions, attachPermissionContext, requirePermission } from "@nebutra/permissions";
import { Hono } from "hono";

const app = new Hono();
const permissions = createPermissions();

app.use(
  attachPermissionContext(async (c) => {
    // Extract user from JWT, session, etc.
    const user = await getCurrentUser(c);
    return {
      userId: user.id,
      tenantId: user.tenantId,
      roles: user.roles,
    };
  })
);

// Protect a route
app.patch("/documents/:id", requirePermission("update", "Document"), async (c) => {
  const doc = await db.document.findUnique({ where: { id: c.req.param("id") } });
  // Update logic...
  return c.json(doc);
});

2. React Components

import { PermissionProvider, Can, Cannot, usePermission } from "@nebutra/permissions";
import { createCASLProvider } from "@nebutra/permissions/casl";

const provider = createCASLProvider();

export function App() {
  const userContext = {
    userId: "user_123",
    tenantId: "org_456",
    roles: ["member"],
  };

  return (
    <PermissionProvider provider={provider} context={userContext}>
      <DocumentEditor />
    </PermissionProvider>
  );
}

function DocumentEditor() {
  return (
    <div>
      <Can action="read" resource="Document" subject={document}>
        <p>{document.content}</p>
      </Can>

      <Can action="update" resource="Document" subject={document}>
        <button>Edit</button>
      </Can>

      <Cannot action="delete" resource="Document">
        <p className="text-red-500">You cannot delete this document</p>
      </Cannot>
    </div>
  );
}

3. Define Custom Roles

import { createCASLProvider, type RoleDefinition } from "@nebutra/permissions";

const customRoles: RoleDefinition[] = [
  {
    role: "editor",
    inherits: "member",
    description: "Can create and edit documents",
    rules: [
      {
        action: ["create", "update"],
        resource: "Document",
      },
    ],
  },
];

const provider = createCASLProvider(customRoles);

Role Hierarchy

Roles inherit permissions from parent roles. The default roles are:

  • owner — Full control
  • admin — Everything except billing and workspace deletion (inherits owner)
  • member — CRUD own resources, read shared (inherits viewer)
  • viewer — Read-only access to shared resources
  • billing_admin — Manage billing (inherits viewer)
  • guest — Limited access to shared resources
{
  role: "editor",
  inherits: ["member", "reviewer"], // Multiple inheritance
  rules: [ /* inherited rules apply first; this role's rules can override */ ]
}

ABAC Conditions

Dynamically resolve conditions at evaluation time using template syntax:

const rule: PermissionRule = {
  action: "update",
  resource: "Document",
  conditions: {
    createdBy: "${user.userId}",       // Resolved from context
    tenantId: "${user.tenantId}",
    visibility: "private"
  }
};

Variables resolved from PermissionContext:

  • ${user.userId}
  • ${user.tenantId}
  • ${user.attributes.team} — Custom attributes

Field-Level Permissions

Restrict access to specific document fields:

const rule: PermissionRule = {
  action: "read",
  resource: "Document",
  fields: ["title", "content"], // Only these fields readable
};

permissions.can(context, "read", "Document", undefined, "title"); // true
permissions.can(context, "read", "Document", undefined, "internalNotes"); // false

Provider Configuration

CASL (Default, In-Process)

Fast, no network calls. Great for UI and API middleware.

import { createCASLProvider } from "@nebutra/permissions/casl";

const provider = createCASLProvider();

Auto-detection: Uses CASL if no OPENFGA_API_URL env var is set.

OpenFGA (Relationship-Based)

OpenFGA uses the store-scoped REST API and fails closed when configuration or network calls are invalid. This package does not manage OpenFGA authorization models or tuple migrations.

import { createOpenFGAProvider } from "@nebutra/permissions/openfga";

const provider = createOpenFGAProvider(
  {
    apiUrl: "http://openfga.internal:8080",
    storeId: "store_abc123",
    authToken: process.env.OPENFGA_AUTH_TOKEN,
  },
  roles
);

// Write relationship tuples
await provider.write([
  { user: "user_123", relation: "member", object: "team:acme" },
  { user: "team:acme", relation: "parent", object: "org:company" },
]);

// Check access
const allowed = await provider.check(
  "user_123",
  "can_edit",
  "document:doc_456"
);

Auto-detection: Uses OpenFGA if OPENFGA_API_URL env var is set.

Environment Variables

# Provider selection (auto-detects if empty)
PERMISSIONS_PROVIDER=casl              # "casl" | "openfga"

# OpenFGA configuration
OPENFGA_API_URL=http://openfga:8080    # Triggers OpenFGA provider
OPENFGA_STORE_ID=abc123                # Required for OpenFGA REST calls
OPENFGA_AUTH_TOKEN=secret              # Optional for managed OpenFGA

API Reference

createPermissions(config?)

Initialize the global permissions manager (singleton).

const permissions = createPermissions({
  provider: "openfga",
  roles: customRoles,
  openFgaApiUrl: "http://openfga:8080",
  openFgaStoreId: "abc123",
  openFgaAuthToken: process.env.OPENFGA_AUTH_TOKEN,
});

getPermissions()

Get the global permissions manager instance.

const permissions = getPermissions();
const can = permissions.can(context, "read", "Document");

requirePermission(action, resource, options?)

Hono middleware for automatic permission checks.

app.delete(
  "/documents/:id",
  requirePermission("delete", "Document", {
    extractSubject: (c) => ({ id: c.req.param("id") }),
    onDenied: (c, error) => c.json({ error: error.message }, 403),
  }),
  deleteDocumentHandler
);

attachPermissionContext(extractUser)

Hono middleware to attach user context from request.

app.use(
  attachPermissionContext(async (c) => {
    const token = c.req.header("authorization");
    const user = await verifyToken(token);
    return {
      userId: user.id,
      tenantId: user.organizationId,
      roles: user.roles,
      attributes: { team: user.teamId },
    };
  })
);

<Can> / <Cannot> (React)

Conditionally render based on permissions.

<Can action="update" resource="Document" subject={doc} fallback={<p>Read-only</p>}>
  <DocumentForm />
</Can>

<Cannot action="publish" resource="Document">
  <p>Publishing disabled</p>
</Cannot>

usePermission(action, resource, subject?)

Hook to check permissions in components.

function DocumentButton() {
  const canDelete = usePermission("delete", "Document", document);
  return canDelete ? <button>Delete</button> : null;
}

Examples

Example 1: Team Workspace with Roles

// API setup
const permissions = createPermissions();

app.use(
  attachPermissionContext(async (c) => {
    const user = await getCurrentUser(c);
    return {
      userId: user.id,
      tenantId: user.workspaceId,
      roles: user.workspaceRoles,
    };
  })
);

app.get(
  "/projects",
  async (c) => {
    const user = c.get("user");
    const projects = await db.project.findMany({
      where: {
        workspaceId: user.tenantId,
        // If using Prisma + CASL
        // ...accessibleBy(ability, "read").project
      },
    });
    return c.json(projects);
  }
);

// React component
<PermissionProvider provider={provider} context={userContext}>
  {projects.map((project) => (
    <div key={project.id}>
      <h3>{project.name}</h3>
      <Can action="update" resource="Project" subject={project}>
        <EditProjectButton projectId={project.id} />
      </Can>
    </div>
  ))}
</PermissionProvider>

Example 2: Custom Role with Conditions

const advancedRoles: RoleDefinition[] = [
  {
    role: "project_owner",
    description: "Own projects within a workspace",
    rules: [
      {
        action: ["create", "read", "update", "delete"],
        resource: "Project",
        conditions: {
          "ownerId": "${user.userId}",
          "workspaceId": "${user.tenantId}",
        },
      },
      {
        action: ["invite"],
        resource: "User",
        conditions: { "workspaceId": "${user.tenantId}" },
      },
    ],
  },
];

const provider = createCASLProvider(advancedRoles);

Troubleshooting

Permission check always returns false

  • Verify user context is being set correctly (roles, userId, tenantId)
  • Check role definitions include the required rule
  • Ensure conditions match the subject data

CASL provider caching issues

Clear the provider cache:

import { CASLProvider } from "@nebutra/permissions/casl";

const provider = new CASLProvider();
provider.clearCache();

OpenFGA connection errors

Verify OPENFGA_API_URL is reachable:

curl http://openfga:8080/health

Check logs for OpenFGA check error messages.

License

MIT