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

@soudasuwa/permissions

v1.2.2

Published

Generic rule engine and access controller for Node.js

Readme

Rule Engine

A small, generic rule engine for Node.js used to evaluate access control decisions. Rules are expressed as plain objects and checked against a context object. The engine does not assume any specific property names so it can be adapted to a variety of domains.

Overview

The library processes a set of rules to determine whether a user may perform an action. Each rule describes the expected values for attributes in the context and can be combined with logical operators. Comparison and logic handlers are pluggable so you can extend the engine with domain specific behaviour.

Use Cases

  • Enforcing who may read, update or delete items in a todo application.
  • Authorising collaborative note editing or forum posts.
  • Controlling invoice workflows or other business processes.

Example rule sets for these scenarios are provided in the scenarios/ folder.

Quick Start

  1. Install dependencies:
npm install
  1. Define your rules:
const rules = [
  {
    when: { resource: "todo", action: "read" },
    rule: { "item.ownerId": { reference: "user.id" } },
  },
];
  1. Evaluate a context:
// After installing from npm
const { AccessController } = require("@soudasuwa/permissions");

const controller = new AccessController(rules).context({
  resource: "todo",
  action: "read",
});

const result = controller.pemit({
  user: { id: "u1" },
  item: { ownerId: "u1" },
});

console.log(result.passed); // true

AccessController also accepts a single rule object instead of an array.

Working with Context

AccessController.context() creates a new controller instance with extra values merged into the existing context. pemit() evaluates the final context against the rules. This makes it easy to build up a base context and re-use it across checks.

const base = new AccessController(rules).context({ resource: "note" });

// Add the action and per-request data before evaluating
const res = base.context({ action: "read" }).pemit({
  user: { id: "alice", role: "viewer" },
  note: { ownerId: "alice" },
});
console.log(res.passed); // true

Rule Examples

The engine can represent many styles of access control. Here are a few common patterns:

Role based

const rbacRules = [
  { when: { action: "delete" }, rule: { "user.role": "admin" } },
  {
    when: { action: "read" },
    rule: { "user.role": { in: ["admin", "viewer"] } },
  },
];

Attribute based

const abacRules = [
  {
    when: { action: "update" },
    rule: { "item.ownerId": { reference: "user.id" } },
  },
];

Value checks

const invoiceRules = [
  {
    when: { resource: "invoice", action: "pay" },
    rule: { "invoice.amount": { lessThan: 1000 } },
  },
];

Existence checks

const existenceRules = [
  { rule: { "user.id": { exists: true } } },
  { rule: { "session.token": { exists: false } } },
];

Implicit AND and nested objects

Multiple properties in a rule object are treated as an AND block. Nested objects expand into dotted paths.

const simpleRule = { resource: "todo", action: "read" };
// Equivalent to: { AND: [{ resource: "todo" }, { action: "read" }] }

const nestedRule = {
  user: { id: { exists: true } },
  item: { ownerId: { reference: "user.id" } },
};
// Equivalent to:
// {
//   "user.id": { exists: true },
//   "item.ownerId": { reference: "user.id" }
// }

Nested rule groups

const docRules = [
  {
    when: { resource: "doc" },
    rules: [
      { when: { action: "edit" }, rule: { "doc.ownerId": { reference: "user.id" } } },
      { when: { action: "view" }, rule: { "doc.shared": true } },
    ],
  },
];

Scenario Examples

The following rule sets expand on the basic patterns above. They are presented from simplest to most complex and mirror the sample tests in the scenarios/ folder.

Simple ToDo App

const todoRules = [
  {
    when: { resource: "todo" },
    rules: [
      {
        when: { action: "create" },
        rule: { "user.id": { exists: true }, "item.ownerId": { reference: "user.id" } },
      },
      { when: { action: "read" }, rule: { "item.ownerId": { reference: "user.id" } } },
      { when: { action: "update" }, rule: { "item.ownerId": { reference: "user.id" } } },
      { when: { action: "delete" }, rule: { "item.ownerId": { reference: "user.id" } } },
    ],
  },
];

Friends Tasks

const taskRules = [
  {
    when: { resource: "task" },
    rules: [
      {
        when: { action: "create" },
        rule: { "user.id": { exists: true }, "item.ownerId": { reference: "user.id" } },
      },
      {
        when: { action: "read" },
        rule: {
          OR: {
            "item.ownerId": { reference: "user.id" },
            "user.id": { in: { reference: "item.sharedWith" } },
          },
        },
      },
      {
        when: { action: "update" },
        rule: {
          OR: {
            "item.ownerId": { reference: "user.id" },
            "user.id": { in: { reference: "item.sharedWith" } },
          },
        },
      },
      { when: { action: "delete" }, rule: { "item.ownerId": { reference: "user.id" } } },
    ],
  },
];

Tic-Tac-Toe with Leaderboard

const gameRules = [
  {
    when: { resource: "game" },
    rules: [
      {
        when: { action: "create" },
        rule: { user: { role: "player", id: { in: { reference: "item.participants" } } } },
      },
      {
        when: { action: "move" },
        rule: {
          "user.id": { in: { reference: "item.participants" } },
          item: { status: { not: "complete" } },
        },
      },
      {
        when: { action: "read" },
        rule: {
          OR: [
            { "item.status": "complete" },
            {
              user: { id: { in: { reference: "item.participants" } } },
              item: { status: { not: "complete" } },
            },
          ],
        },
      },
    ],
  },
  {
    when: { resource: "leaderboard" },
    rules: [
      { when: { action: "read" }, rule: { "user.role": { in: ["player", "moderator"] } } },
      { when: { action: "update" }, rule: { "user.role": "moderator" } },
    ],
  },
];

Collaborative Notes

const noteRules = [
  {
    when: { resource: "note" },
    rules: [
      {
        when: { action: "create" },
        rule: {
          OR: {
            "notebook.ownerId": { reference: "user.id" },
            "user.id": { in: { reference: "notebook.editors" } },
          },
        },
      },
      {
        when: { action: "read" },
        rule: {
          OR: [
            { "notebook.ownerId": { reference: "user.id" } },
            { "user.id": { in: { reference: "notebook.editors" } } },
            { "user.id": { in: { reference: "notebook.viewers" } } },
          ],
        },
      },
      {
        when: { action: "update" },
        rule: {
          OR: {
            "notebook.ownerId": { reference: "user.id" },
            "user.id": { in: { reference: "notebook.editors" } },
          },
        },
      },
      {
        when: { action: "delete" },
        rule: {
          OR: {
            "notebook.ownerId": { reference: "user.id" },
            "user.id": { in: { reference: "notebook.editors" } },
          },
        },
      },
    ],
  },
  {
    when: { resource: "notebook" },
    rules: [
      { when: { action: "delete" }, rule: { "notebook.ownerId": { reference: "user.id" } } },
      { when: { action: "modifySharing" }, rule: { "notebook.ownerId": { reference: "user.id" } } },
    ],
  },
];

Discussion Forum

const forumRules = [
  {
    when: { resource: "category" },
    rules: [
      {
        when: { action: "view" },
        rule: {
          OR: {
            "category.isPrivate": { not: true },
            "user.id": { in: { reference: "category.allowedUsers" } },
            "user.role": "admin",
          },
        },
      },
    ],
  },
  {
    when: { resource: "topic" },
    rules: [
      {
        when: { action: "create" },
        rule: {
          OR: [
            { user: { role: "member" }, category: { isPrivate: { not: true } } },
            { user: { role: "member", id: { in: { reference: "category.allowedUsers" } } } },
          ],
        },
      },
    ],
  },
  {
    when: { resource: "post" },
    rules: [
      {
        when: { action: "editOwn" },
        rule: {
          user: { role: "member" },
          post: { authorId: { reference: "user.id" }, ageMinutes: { lessThan: 30 } },
        },
      },
      {
        when: { action: "editAnyModerator" },
        rule: {
          user: { role: "moderator", id: { in: { reference: "category.moderators" } } },
        },
      },
    ],
  },
  { when: { resource: "user", action: "adminDelete" }, rule: { "user.role": "admin" } },
];

Invoice Lifecycle

const invoiceRules = [
  {
    when: { resource: "invoice" },
    rules: [
      { when: { action: "view" }, rule: { "user.role": "admin" } },
      {
        when: { action: "view" },
        rule: {
          "user.role": "customer",
          invoice: {
            ownerId: { reference: "user.id" },
            status: { in: ["pending", "complete"] },
          },
        },
      },
      {
        when: { action: "edit" },
        rule: {
          "user.role": "admin",
          invoice: { status: { in: ["draft", "pending"] } },
        },
      },
      {
        when: { action: "pay" },
        rule: {
          "user.role": "customer",
          invoice: { ownerId: { reference: "user.id" }, status: "pending" },
        },
      },
    ],
  },
];

Features

  • Generic attribute matching – rules reference arbitrary paths within the context.
  • Comparison operators – equality, in, not, value reference, numeric comparisons (greaterThan, lessThan) and exists checks.
  • Logical composition – combine rules with AND, OR, XOR and NOT blocks.
  • Authorize helper – evaluate arrays of rules with optional when conditions.
  • Nested rule groups – share when conditions with child rules using a rules array.
  • AccessController – helper class for incrementally building a context.
  • Pluggable evaluator – provide custom logic or comparison handlers.
  • Functional rule builder – compose rules with helpers like field, ref, and and not.
  • Evaluation trace – inspect which rules triggered via the returned trace array.

Extending

Advanced scenarios may require custom logic, comparison operators or context resolution. The engine already includes AND, OR, XOR and NOT logic. Additional behaviors can be plugged into the DefaultEvaluator:

const nandLogic = {
  match: node => typeof node === "object" && node !== null && "NAND" in node,
  evaluate: (node, ctx, ev) => {
    const items = Array.isArray(node.NAND)
      ? node.NAND
      : Object.entries(node.NAND).map(([k, v]) => ({ [k]: v }));
    return !items.every(r => ev.evaluate(r, ctx).passed);
  },
};

const controller = new AccessController(rules, {
  evaluator: new DefaultEvaluator({ logic: [nandLogic] }),
});

Custom comparison handler example

const startsWith = {
  match: (_, exp) => typeof exp === "object" && exp !== null && "startsWith" in exp,
  evaluate: (attr, exp, ctx) => {
    const value = attr.split('.').reduce((o, k) => (o ? o[k] : undefined), ctx);
    return typeof value === 'string' && value.startsWith(exp.startsWith);
  },
};

const controller = new AccessController(rules, {
  evaluator: new DefaultEvaluator({ compare: [startsWith] }),
});

Custom context resolver example

const colonResolver = {
  resolve: (path, ctx) =>
    path.split(":").reduce((o, k) => (o ? o[k] : undefined), ctx),
};

const controller = new AccessController(rules, {
  evaluator: new DefaultEvaluator({ contextResolver: colonResolver }),
});

Custom rule node handler example

const allowIf = {
  match: node => typeof node === "object" && node !== null && "allowIf" in node,
  evaluate: (node, ctx, ev) => ev.evaluate(node.allowIf, ctx),
};

const controller = new AccessController(rules, {
  evaluator: new DefaultEvaluator({ nodes: [allowIf] }),
});

Functional rule builder example

const { field, ref, and, xor, not } = require("./ruleEngine");

const rule = and(
  xor(field("user.role", "admin"), field("user.role", "editor")),
  not(field("item.status", "archived")),
  field("user.id", ref("item.ownerId"))
);

const controller = new AccessController([rule]);
const okCtx = {
  user: { id: "u1" },
  item: { ownerId: "u1", status: "active" },
};
const result = controller.pemit(okCtx);
console.log(result.passed); // true

// Inspect evaluation trace
console.dir(result, { depth: null });

Inspecting evaluation results

AccessController.pemit() returns a trace array describing which rules were checked. This can be helpful for debugging permissions.

const out = controller.pemit(okCtx);
console.dir(out, { depth: null });
/* Example output:
{
  passed: true,
  trace: [
    {
      AND: [
        { "user.id": { reference: "item.ownerId" } },
        { NOT: { "item.status": "archived" } }
      ]
    },
    { NOT: { "item.status": "archived" } },
    { "user.id": { reference: "item.ownerId" } }
  ]
}
*/

Testing

Run the unit tests with:

npm test

Formatting and linting can be checked with:

npm run check