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

casbin-client

v1.0.0

Published

Lightweight client for Casbin

Readme

casbin-client

casbin-client is a library which facilitates manipulation, management, and storage of user permissions in a frontend application for the purposes of authorization. It supports various access control policies, like RBAC, ABAC, ACL, etc.

It is primarily a library for Casbin and strives to be a more modern and polymorphic alternative to the official Casbin.js client library; it is a complete rewrite from the ground up, sharing zero code with its predecessor. It can and will work without any dependencies, however, so having any knowledge of Casbin is entierly optional.

// simple
const user = { can: authorizer(() => permissions) };

user.can('read', 'data');

// with caching
const user = createAuthorizer(() => permissions, { store: sessionStorage });
// and/or promises
const user = createAuthorizer(Promise.resolve(permissions), { store: sessionStorage });

user.can('read', 'data');

// with full casbin model and policy parsing
const user = createAuthorizer(() => (
  fromPolicySource(policy, { parseExpression })
), { store: sessionStorage });

user.can('read', 'data');

| Feature | casbin-client | casbin.js | |---------|-----------------|-------------| | 🌟 Modern tech-stack and dev practices | ✅ TypeScript, DI, FP | 🥀 Babel, OOP | | 🏝️ Less external dependencies | ✅ core/model/policy are dependency-free; only the parser uses subscript | 🥀 Mandatory axios, babel, casbin-core | | 💻 Ergonomic development experience | ✅ Import and use how you like | 🥀 Use in compliance with assumptions hidden in source code | | 🪄 Support for various runtime modes | ✅ Supports both regular (sync) and async modes | 🥀 Every method is async | | 🪶 Lightweight and tree-shakeable | ✅ 0.5KB↔8KB, take what you need | 🥀 90KB+, no tree-shaking | | 🔌 Extendable | ✅ Pluginable at every step | 🥀 Depend on implementation details | | 🤝 Type-safe | ✅ Use typed policies to enforce type safety | 🥀 Untyped strings only | | 🌐 Environment-independent | ✅ Works in any modern JS environment | 🥀 CommonJS build only | | ⚙️ Reliability | ✅ | 🥀 No tests... | | 🔃 More to come... | | |

Install

npm i casbin-client
# or
bun add casbin-client

Use

Basics

At the centrepoint is the concept of an Authorizer - a singleton that looks at users' permissions and decides if the "user" can or cannot do certain actions:

import { createAuthorizer } from 'casbin-client';

const permissions = {
  read: ['data']
};

const user = createAuthorizer(() => permissions);

if (user.can('read', 'data')) {
  console.log('Yay, we can read data!');
}
//...

createAuthorizer takes a simple Permissions factory as its primary argument and provides a semantic interface to read from it. It never modifies or tampers with the original object, acting like a simple view on it.
If permissions need changing, simply update them:

//...
if (!user.can('read', 'users')) {
  console.log('Oops, wrong permissions!');
}

permissions.read = ['data', 'users'];

if (user.can('read', 'users')) {
  console.log('Yay, we can read users!');
}

And that's the basics!

Note
Checks fan out with AND semantics - can(['read', 'write'], 'data') is true only if every action is permitted. Empty arrays are treated as a denial (can('read', []) === false) rather than vacuously allowed, so a degenerate input never accidentally opens a gate.

Modules

There are 7 isolated modules:

Each module is independent from others, and thus very has little effect on the final bundle size of your application.

authorizer

As covered in the basics section, casbin-client exports a simple createAuthorizer function, with some helper types.
But what if even this is too much?
Enter, casbin-client/core:

import { authorizer, type Permissions } from 'casbin-client/core';

const permissions = {
  read: ['data', 'users'] as const
} satisfies Permissions; // enables full autocomplete

const can = authorizer(() => permissions);

if (can('read', 'data')) {
  console.log('Yay, we can read data!');
}
// Logs "Yay, we can read data!"

It accepts a simple AuthorizerOptions object as its second argument:

import { type AuthorizerOptions } from 'casbin-client/core';

const options = {
  fallback: (action, object) => object !== 'database' && action !== 'delete',
  // A fallback function to resolve missing permissions

  matchAction: (action, source) => source?.[action],
  // A matcher for actions in permissions (default value)

  matchObject: (object, objects) => objects?.includes(object),
  // A matcher for objects in actions (default value)
};

const can = authorizer(() => permissions, options);

if (can('delete', 'database')) {
  console.log('We are doomed!');
} else {
  console.log('Phew, we are safe.');
}
// Logs "Phew, we are safe."

createAuthorizer

This is a much more versatile factory function.
It allows automatic caching using the Storage API and working with promises.

createAuthorizer also accepts two arguments:

  • a Permissions object:
    import { type Permissions } from 'casbin-client';
    
    const permissions: Permissions = {
      read: ['data', 'users']
    };
  • and customization options (optional)
    import { type SyncAuthorizerOptions } from 'casbin-client';
    
    const options: SyncAuthorizerOptions = {
      store: sessionStorage,
      // A `WebStorage`-shaped object (e.g. `sessionStorage`) to cache permissions in
    
      key: 'auth',
      // A unique key to store the permissions in the store
    
      fallback: (action, object) => object !== 'database' && action !== 'delete',
      // A fallback function to resolve missing permissions
    
      onError: (error, context) => console.warn(`[casbin-client] ${context}`, error),
      // Reports recoverable errors (corrupt cache, misconfiguration); the library always fails safe
    };

And allows for simple permission checking:

import { createAuthorizer } from 'casbin-client';

const user = createAuthorizer(() => permissions, options);

if (user.can('delete', 'database')) {
  console.log('We are doomed!');
} else {
  console.log('Phew, we are safe.');
}
// Logs "Phew, we are safe."

Note

The .can method always re-runs the permission factory!
In reactive UI-frameworks it is advised to wrap its calls with a computed primitive, like useMemo or computed.

Async mode

Note

This mode is not for usage with reactive UI frameworks like react, solid, or vue.
In the context of reactive data in UI components, it's better to use createAuthorizer in combination with reactive primitives like useQuery, createResource, or computed.

The "Async mode" is for the case when there's no way to use a reactive primitive and the execution context is synchronous.

createAuthorizer makes it easy to work with promises, because the permissions factory can also be a promise:

const permissionsUrl = 'https://raw.githubusercontent.com/Raiondesu/casbin-client/refs/heads/main/examples/permissions.json';
const remotePermissions = fetch(permissionsUrl).then(r => r.json());

createAuthorizer simply treats the promise as a factory:

const user = createAuthorizer(remotePermissions, options);

// ...
// some time later in a file far far away
if (user.can('read', 'data')) {
  console.log('Yay, we can read data!');
}

In the context of a single function this is, of course, not possible, so the promise is proxied and can be awaited separately:

await user.remote;

if (user.can('read', 'data')) {
  console.log('Yay, we can read data!');
}

Typing

Both authorizer and createAuthorizer accept a generic parameter, which can be automatically inferred from permissions:

type MyPermissions = {
  read: ['data']
};

const permissions: any = {
  read: ['data']
};

const auth = createAuthorizer<MyPermissions>(() => permissions);

// Full autocomplete and type checking!
auth.can('read', 'data');

casbin-client/model

Allows to parse and use a Casbin model.

import { parseModel } from 'casbin-client/model';

const model = `
  [request_definition]
  r = sub, obj, act

  [policy_definition]
  p = sub, obj, act

  [role_definition]
  g = _, _

  [policy_effect]
  e = some(where (p.eft == allow))

  [matchers]
  m = r.obj == p.obj && r.act == p.act && g(r.sub, p.sub)
`;

const parsed = parseModel(model);

console.log(parsed.matchers.m({
  r: { sub: 'alice', act: 'read', obj: 'data' },
  p: { sub: 'reader', act: 'read', obj: 'data' },
  g: (r, p) => 'alice' === r && 'reader' === p,
  ...parsed.matchers,
  ...parsed.policyEffect
}));
//> true

casbin-client/policy

Allows to parse and use a Casbin model with a Casbin policy.

This module implements the most essential sub-set of read-only features from casbin-core.

See the list of missing features to gauge if this is useful for your project.

import { createAuthorizer } from 'casbin-client';
import { fromPolicySource } from 'casbin-client/policy';
import { parseExpression } from 'casbin-client/parser';

const model = `
  [request_definition]
  r = sub, obj, act

  [policy_definition]
  p = sub, obj, act

  [role_definition]
  g = _, _

  [policy_effect]
  e = some(where (p.eft == allow))

  [matchers]
  m = r.obj == p.obj && r.act == p.act && g(r.sub, p.sub)
`;

// Result from `CasbinJsGetUserPermission` or otherwise manually loaded
const policy = {
  g: [
    ["g", "alice", "reader"],
    ["g", "alice", "writer"],
    ["g", "bob", "reader"],
    ["g", "cathy", "admin"],
  ],
  m: model,
  p: [
    ["p", "reader", "data", "read"],
    ["p", "writer", "data", "write"],
    ["p", "admin", "data", "delete"],
  ]
};

// Note that this is a costly function to call
const permissions = fromPolicySource(policy);

const user = createAuthorizer(() => permissions);

if (user.can('read', 'data')) {
  console.log('Yay, we can read data!');
}

const alicePermissions = fromPolicySource(policy, {
  request: ['r', 'alice'],
  parseExpression, // required to filter by subject - without it, filtering fails closed (denies)
});

const alice = createAuthorizer(() => alicePermissions);

if (alice.can('read', 'data')) {
  console.log('Yay, alice can read data!');
}

if (!alice.can('delete', 'data')) {
  console.log('Nope, alice cannot delete data!');
}

casbin-client/parser

This module uses subscript's sandboxed justin evaluator, with Casbin's in array-membership semantics added on top.

import { parseExpression } from 'casbin-client/parser';

const run = parseExpression('"a" in b && b.a() === true');

console.log(run({ b: { a: () => true } }));
//> true

It can be passed into model and policy parsers as options, in order to enable complete Casbin experience in JS:

const reader = fromPolicySource(policy, {
  request: ['r', 'bob'],
  parseExpression,
});

const bob = createAuthorizer(() => reader);

if (bob.can.read('data')) {
  console.log('Yeah, Bob can read');
}

casbin-client/functions

Casbin's built-in pattern matchers - keyMatch, keyMatch2, regexMatch, globMatch - for path-style resources like /data/*.

They are available by default inside policy matchers, so a model can use them as-is:

const model = `
  [request_definition]
  r = sub, obj, act
  [policy_definition]
  p = sub, obj, act
  [matchers]
  m = r.sub == p.sub && keyMatch(r.obj, p.obj) && r.act == p.act
`;

For the common frontend case - holding permissions that contain patterns and checking concrete paths against them - use byPattern to turn any matcher into a matchObject:

import { authorizer } from 'casbin-client/core';
import { byPattern, keyMatch } from 'casbin-client/functions';

const can = authorizer(() => ({ read: ['/data/*'] }), {
  matchObject: byPattern(keyMatch),
});

can('read', '/data/123'); //> true
can('read', '/other');    //> false

You can supply or override functions via the functions option of fromPolicySource:

fromPolicySource(policy, { request: ['r', 'alice'], parseExpression, functions: { keyMatch: myKeyMatch } });

casbin-client/react

First-class React bindings. react is an optional peer dependency (>=18). The integration is data-source agnostic - you own fetching/shaping and pass the resolved policy + a loading flag; it handles memoization, the loading-vs-denied distinction, and SSR hydration.

Note
Three states are kept distinct: loading / allowed / denied. isLoading is a separate field, never folded into can() - so a pending policy denies by default (secure), and you branch on isLoading first to show a placeholder instead of a flash of "denied".

Server vs. client

casbin-client/react is a set of Client Component bindings - they use hooks, so the module carries a 'use client' directive. There are two doors into the library:

  • Server-side authorization (Server Components, route handlers, middleware, edge, SSR loaders): use the core - import { createAuthorizer } from 'casbin-client'. It is pure, dependency-free, carries no directive, and runs anywhere.
    import { createAuthorizer } from 'casbin-client';
    const { can } = createAuthorizer(() => permissions); // any server or client context
  • Client-component gating (interactive UI that needs hooks/state): use casbin-client/react.

You cannot use these hooks inside a pure Server Component - but that is what the core is for.

The headless hook

useAuthorizer(permissions, options?) needs no Provider - the truest drop-in:

'use client';
import { useAuthorizer, byPattern, globMatch } from 'casbin-client/react';
import { useQuery } from '@tanstack/react-query'; // or SWR / RTK / raw fetch — anything

function DataPanel() {
  const { data, isLoading } = useQuery({ queryKey: ['perms'], queryFn: fetchPermissions });

  const { can, isReady } = useAuthorizer(data, {
    isLoading,
    matchObject: byPattern(globMatch), // patterns: `/data/*` matches `/data/123` at check time
  });

  if (!isReady) return <Skeleton />;          // loading !== denied
  return can('read', '/data/42') ? <Data /> : <NoAccess />;
}

The typed factory (app-wide gating)

createAuthorizerContext<Permissions>() bakes your Permissions type in once, so can.read('data') autocomplete survives the Provider boundary. Returns a typed { AuthorizerProvider, useAuthorizer, useCan, Can }:

// acl.ts — declare the scope once
import { createAuthorizerContext, byPattern, globMatch } from 'casbin-client/react';
import type { Permissions, AuthorizerOptions } from 'casbin-client/react';

export type AppPermissions = Permissions<'read' | 'write' | 'delete', string>;

// Hoist options to module scope for a stable identity (so the authorizer doesn't rebuild).
export const aclOptions: AuthorizerOptions<AppPermissions> = { matchObject: byPattern(globMatch) };

export const { AuthorizerProvider, useCan, useAuthorizer, Can } =
  createAuthorizerContext<AppPermissions>();
// providers.tsx — you own the data layer
'use client';
import { AuthorizerProvider, aclOptions } from './acl';

export function AclProvider({ children }) {
  const { data, isLoading } = useQuery({ queryKey: ['perms'], queryFn: fetchPermissions });
  return (
    <AuthorizerProvider permissions={data} isLoading={isLoading} options={aclOptions}>
      {children}
    </AuthorizerProvider>
  );
}
// any component — hook or declarative gate
import { useCan, Can } from './acl';

const can = useCan();
can.delete('/users/42');                    // curried + narrowed

<Can action="read" object="/billing" loading={<Skeleton />} fallback={<NoAccess />}>
  <BillingPanel />
</Can>

Migrating from an ad-hoc wrapper

| Hand-rolled wrapper | casbin-client/react | |---|---| | useMemo(() => createAuthorizer(...), [items]) | useAuthorizer(permissions, options) (memo built in) | | useState + useEffect hydration flag | built in via useSyncExternalStore (mismatch-proof) | | isLoading = !hydrated \|\| query.isLoading | pass isLoading; hydration is folded in | | new RegExp('^' + obj.replace('*', '.*') + '$') | byPattern(globMatch) (tested; no first-*-only bug) | | coupled to a specific data hook | pass permissions + isLoading from any source |

Note
createAuthorizer's factory re-runs on every check, so the hook memoizes the authorizer on the permissions reference. Pass the upstream data reference directly and memoize any reshape on the source (not inline in render), or the authorizer rebuilds every render.

Async permissions

The Promise-based mode of createAuthorizer is not for React (it can't notify React of resolution). Resolve the promise in your data layer (react-query/SWR/useState) and feed the resolved value into useAuthorizer.

Why

Casbin is amazing for dynamic and polymorphic control of user access. But the official client-side library left a lot to be desired. Being a de-facto extension on the casbin-core package for Node.js, it brings in a lot of unneeded dependencies and wraps them in an API that is awkward to use in a modern JS ecosystem.

Roadmap / TODO list

  • [x] Process simple policies ({ write: ['data'], read: ['data'] })
  • [x] Custom storage or DB providers for caching
  • [x] Simple integration with any network/query client
  • [x] Ability to check user permissions using policies and model matchers
  • [x] Ability to parse permissions from policies without the baggage of matchers and effects
  • [x] Reliable error reporting (via the onError reporter)
  • [x] Transitive RBAC role inheritance (g(alice, admin) + g(admin, super)g(alice, super))
  • [x] Policy effects (allow / deny-override)
  • [x] React integration (casbin-client/react); other frameworks to come
  • [ ] Generate ambient types from policy csv or permissions json
  • [ ] Parse permissions at the type level from policy source
  • [x] Support for complex pattern-matching (/data/*, keyMatch(...))
  • [ ] Support for internal eval(...) and other built-in functions
  • [ ] Support for custom matcher contexts
  • [x] Full test coverage

Feel like something's missing? Submit an issue!
Wanna help? Fork and submit a PR!

Security notice

casbin-client/parser is built on subscript's sandboxed justin evaluator. Matcher and effect expressions cannot reach the Function constructor, prototypes, or JS globals, so they cannot execute arbitrary code - the eval()-class risk of earlier versions (and of casbin.js) is closed.

One caveat remains, so prefer trusted model text where you can: there is no evaluation timeout, and an expression may call methods on the context values you pass it (e.g. someString.repeat(1e9)), so a hostile expression can still cause heavy computation (a DoS) on the main thread - it just can't run arbitrary code or read outside its context.

The defaults are otherwise fail-closed: without parseExpression, request filtering denies rather than grants, and unparseable input is reported through onError and skipped.

Contributing

Prerequisites:

  • bun:
    curl -fsSL https://bun.sh/install | bash
    powershell -c "irm bun.sh/install.ps1 | iex"

To install dependencies:

bun install

Build

bun run build

Test

bun run test