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

@perdieminc/acl

v2.2.0

Published

Shared ACL (Access Control List) bitmask helpers and constants for PerDiem

Downloads

436

Readme

@perdieminc/acl

Shared ACL (Access Control List) bitmask library for PerDiem. Used by both the API (Node.js/Hapi) and the Dashboard (Next.js/TypeScript).

How It Works

Permissions are encoded as a bitmask integer stored in a bigint column (64-bit). Each of the 15 permission groups occupies 2 bits, giving three access levels:

| Binary | Decimal | Level | |--------|---------|-------| | 00 | 0 | None | | 01 | 1 | Read | | 10 | 2 | Write |

The 15 groups are laid out sequentially in the integer:

Bits:  [29-28] [27-26] [25-24] ... [3-2] [1-0]
Group: Deliver TeamMgm Settings ... Inv   Menu

For example, if a user has READ on Menu Management (offset 0) and WRITE on Inventory (offset 2):

Binary: ...00 10 01
              ^^ ^^
              |  └─ Menu Management = 01 (Read)
              └──── Inventory = 10 (Write)

Decimal: 9

Predefined Roles

Each predefined role (super_owner, owner, general_manager, manager, employee) maps to a fixed array of 15 access levels. These are derived at runtime from ROLE_DEFAULTS — the bitmask is NOT stored in the database for predefined roles.

| Role | Description | |------|-------------| | super_owner | Full write access to everything (analytics is read-only) | | owner | Same as super_owner | | general_manager | Full write except team management (read) | | manager | Mixed read/write on operational groups, no settings/team/features | | employee | Read-only on core groups, no access to admin areas | | custom | Owner-defined per-group access levels, stored in users.acl column |

Custom Role

The custom role allows owners to define exact access levels per group for a user. Unlike predefined roles where the bitmask is derived at runtime, custom roles store the bitmask integer directly in the users.acl database column.

How it works:

  1. The owner selects specific access levels (None / Read / Write) for each of the 15 permission groups
  2. The selections are encoded into a bitmask using setPermission or buildMaskFromArray
  3. The bitmask is saved to the users.acl column in the database
  4. At runtime, resolveAcl("custom", user.acl) returns the stored bitmask

Fallback behavior: If a custom user has no stored acl value (null/undefined), the default is all NONE (zero access). This is fail-safe — no permissions are granted until explicitly set by the owner.

Resolution Logic

resolveAcl(role, customAcl):
  if customAcl is a number → use it directly
  else → buildMaskForRole(role)     // predefined defaults, or 0 for custom

Installation

This package is hosted on GitHub (not npm). Install it via:

# Using SSH
npm install git+ssh://[email protected]:PerDiemInc/acl.git

# Using HTTPS
npm install git+https://github.com/PerDiemInc/acl.git

# Specific version/tag
npm install git+ssh://[email protected]:PerDiemInc/acl.git#v1.0.0

# Local development (file reference)
npm install ../acl

In package.json:

{
  "dependencies": {
    "@perdieminc/acl": "git+ssh://[email protected]:PerDiemInc/acl.git"
  }
}

Usage

CommonJS (API)

const {
  ACL_ACCESS_LEVEL,
  ACL_GROUP,
  hasAccess,
  resolveAcl,
  setPermission,
} = require("@perdieminc/acl");

// Check if a user can write to orders
const acl = resolveAcl(user.access_level, user.acl);
if (hasAccess(acl, ACL_GROUP.ORDERS, ACL_ACCESS_LEVEL.WRITE)) {
  // Allow order modification
}

// Build a custom bitmask
let mask = 0;
mask = setPermission(mask, ACL_GROUP.MENU_MANAGEMENT, ACL_ACCESS_LEVEL.WRITE);
mask = setPermission(mask, ACL_GROUP.ORDERS, ACL_ACCESS_LEVEL.READ);
// Store `mask` in users.acl column

Creating a Custom Role

import {
  ACL_ACCESS_LEVEL,
  ACL_GROUP,
  setPermission,
  buildMaskFromArray,
  resolveAcl,
  hasAccess,
} from "@perdieminc/acl";

const { NONE, READ, WRITE } = ACL_ACCESS_LEVEL;

// Option A: Build incrementally with setPermission
let mask = 0;
mask = setPermission(mask, ACL_GROUP.MENU_MANAGEMENT, WRITE);
mask = setPermission(mask, ACL_GROUP.ORDERS, READ);
mask = setPermission(mask, ACL_GROUP.INVENTORY, READ);
// All other groups default to NONE (no access)

// Option B: Build from a full array of 15 levels (same order as ACL_GROUP_META)
const mask2 = buildMaskFromArray([
  WRITE, READ, READ, NONE, NONE, NONE, NONE, NONE,
  NONE, NONE, NONE, NONE, NONE, NONE, NONE,
]);
// mask === mask2

// Save to DB: UPDATE users SET access_level = 'custom', acl = $mask WHERE id = $userId

// Later, resolve and check access
const acl = resolveAcl("custom", mask);
hasAccess(acl, ACL_GROUP.MENU_MANAGEMENT, WRITE); // true
hasAccess(acl, ACL_GROUP.ORDERS, READ);            // true
hasAccess(acl, ACL_GROUP.ORDERS, WRITE);           // false
hasAccess(acl, ACL_GROUP.CUSTOMERS, READ);         // false (not granted)

TypeScript / ESM (Dashboard)

import {
  ACL_ACCESS_LEVEL,
  ACL_GROUP,
  ACL_GROUP_META,
  getPermission,
  hasAccess,
  resolveAcl,
  type ACLAccessLevel,
  type ACLGroupKey,
} from "@perdieminc/acl";

// Iterate over all groups
ACL_GROUP_META.forEach((group) => {
  const level: ACLAccessLevel = getPermission(userAcl, group.bitOffset);
  console.log(`${group.label}: ${level}`);
});

Permission Groups

| # | Group Key | Label | Bit Offset | |---|-----------|-------|------------| | 0 | MENU_MANAGEMENT | Menu Management | 0 | | 1 | INVENTORY | Inventory & Stock | 2 | | 2 | ORDERS | Orders & Catering | 4 | | 3 | CUSTOMERS | Customers & Store Credit | 6 | | 4 | LOYALTY | Loyalty, Subscriptions & Rewards | 8 | | 5 | PROMOTIONS | Promotions & Coupons | 10 | | 6 | NOTIFICATIONS | Notifications & Emails | 12 | | 7 | POSTS | Posts & Content | 14 | | 8 | LOCATIONS | Locations & Business Hours | 16 | | 9 | ORDER_PLACEMENT | Order Placement | 18 | | 10 | APP_CUSTOMIZATION | App Customization & Branding | 20 | | 11 | ANALYTICS | Analytics & Reporting | 22 | | 12 | SETTINGS | Settings & Payments | 24 | | 13 | TEAM_MANAGEMENT | Team Management | 26 | | 14 | DELIVERY | Delivery | 28 |

Adding New Permissions

New permission groups must be appended at the end. Here is how:

  1. In src/groups.ts, add a new entry to ACL_GROUP with the next bit offset (current max is 28, so the next would be 30):
export const ACL_GROUP = Object.freeze({
  // ... existing groups ...
  DELIVERY: 28 as const,
  NEW_GROUP: 30 as const,  // ← append here
});
  1. In the same file, add a matching entry to ACL_GROUP_META:
{ key: "NEW_GROUP", label: "New Group", description: "Description", bitOffset: ACL_GROUP.NEW_GROUP },
  1. In src/types.ts, add the new key to ACLGroupKey and the new offset to ACLGroupBitOffset.

  2. In src/roles.ts, add a default level for each role in ROLE_DEFAULTS (append to each array):

export const ROLE_DEFAULTS: RoleDefaultsMap = Object.freeze({
  super_owner:     [...existing, WRITE],
  owner:           [...existing, WRITE],
  general_manager: [...existing, READ],
  manager:         [...existing, NONE],
  employee:        [...existing, NONE],
});
  1. Bump the version in package.json and update consumers.

Maximum: 32 groups (bigint / 64-bit, 2 bits each). Currently using 15/32 slots.

Why Existing Permissions Cannot Be Changed

The bit offset for each group is its position in the bitmask. Changing an existing group's offset would break every stored ACL integer in the database:

  • If you move ORDERS from offset 4 to offset 6, every user's stored bitmask would now interpret their CUSTOMERS permissions as ORDERS and vice versa.
  • All existing data would be silently corrupted without any error.

Similarly, you cannot insert a new group between existing ones or reorder them. The bit layout is a contract between the code and the stored data, similar to a protobuf schema where field numbers are immutable once assigned.

What you can do:

  • Append new groups at the end (next available offset)
  • Deprecate a group by ignoring its bits (set to NONE in defaults), but its offset is permanently reserved
  • Rename a group's label/description (the key and offset stay the same)

API Reference

Constants

  • ACL_ACCESS_LEVEL - { NONE: 0, READ: 1, WRITE: 2 }
  • ACL_GROUP - Bit offset for each of the 15 permission groups
  • ACL_GROUP_META - Array of { key, label, description, bitOffset } metadata
  • ROLE_DEFAULTS - Default access levels for each predefined role

Functions

| Function | Description | |----------|-------------| | getPermission(acl, bitOffset) | Extract 2-bit access level for a group | | setPermission(acl, bitOffset, level) | Set 2-bit access level for a group | | buildMaskFromArray(levels) | Build bitmask from array of 15 levels | | buildMaskForRole(role) | Build bitmask for a predefined role | | maskToArray(acl) | Decompose bitmask into array of 15 levels | | hasAccess(acl, bitOffset, requiredLevel) | Check if user meets minimum level | | resolveAcl(role, customAcl) | Resolve effective ACL (custom or role default) | | accessLevelLabel(level) | Get human-readable label ("None"/"Read"/"Write") |

Project Structure

src/
├── types.ts          # Type definitions (ACLAccessLevel, ACLGroupKey, etc.)
├── access-levels.ts  # ACL_ACCESS_LEVEL constant (None/Read/Write)
├── groups.ts         # ACL_GROUP bit offsets + ACL_GROUP_META metadata
├── roles.ts          # ROLE_DEFAULTS for predefined roles
├── helpers.ts        # Bitmask helper functions
└── index.ts          # Barrel re-export
test/
└── index.test.ts     # Full test suite

Running Tests

# Run tests (uses tsx + Node.js built-in test runner)
npm test

# Build TypeScript to dist/
npm run build

# Lint with biome
npm run lint

# Format with biome
npm run format

# Check all (lint + format)
npm run check

Development

# Clone
git clone [email protected]:PerDiemInc/acl.git
cd acl

# Install dev dependencies
npm install

# Build
npm run build

# Run tests
npm test

# Lint & format
npm run check

Development & Release

Creating a New Release

When you're ready to release a new version:

  1. Update version and create tag (automatically):

    npm version patch  # For bug fixes (4.0.1 -> 4.0.2)
    npm version minor  # For new features (4.0.1 -> 4.1.0)
    npm version major  # For breaking changes (4.0.1 -> 5.0.0)

    This automatically updates package.json, creates a commit, and creates a git tag.

  2. Push the tag (triggers automatic publish):

    git push origin master --follow-tags
  3. GitHub Actions automatically:

    • ✅ Runs all tests
    • ✅ Publishes to npm
    • ✅ Creates a GitHub Release with auto-generated notes

Manual Release (if needed)

If you need to publish manually without the automation:

npm publish --access public

Delete a Tag (if you made a mistake)

# Delete local tag
git tag -d v4.0.2

# Delete remote tag
git push origin --delete v4.0.2