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

@vielzeug/permit

v1.1.3

Published

Type-safe, flexible role-based access control (RBAC) system for TypeScript applications. Simple, powerful permission management with zero dependencies.

Readme

@vielzeug/permit

Type-safe, flexible role-based access control (RBAC) system for TypeScript applications. Simple, powerful permission management with zero dependencies.

Features

  • Type-Safe - Full TypeScript support with generic user and data types
  • Flexible Permissions - Static boolean or dynamic function-based checks
  • Wildcard Support - Define permissions for all roles or resources
  • Normalized Matching - Case-insensitive, trimmed role/resource comparison
  • Security-First - Safe handling of malformed users with ANONYMOUS role
  • Runtime Validation - Validates permission actions at registration
  • Zero Dependencies - Lightweight with only @vielzeug/logit for optional logging
  • Deep Copy Protection - Immutable permission registry inspection
  • Framework Agnostic - Works with any JavaScript/TypeScript framework

Installation

# pnpm
pnpm add @vielzeug/permit

# npm
npm install @vielzeug/permit

# yarn
yarn add @vielzeug/permit

Quick Start

import { Permit } from '@vielzeug/permit';

// Define permissions
Permit.register('admin', 'posts', {
  read: true,
  create: true,
  update: true,
  delete: true,
});

Permit.register('editor', 'posts', {
  read: true,
  create: true,
  update: (user, data) => user.id === data.authorId, // Dynamic check
  delete: false,
});

// Check permissions
const user = { id: '123', roles: ['editor'] };

Permit.check(user, 'posts', 'read'); // true
Permit.check(user, 'posts', 'create'); // true
Permit.check(user, 'posts', 'update', { authorId: '123' }); // true
Permit.check(user, 'posts', 'update', { authorId: '456' }); // false
Permit.check(user, 'posts', 'delete'); // false

Core Concepts

Permission Actions

Four standard CRUD actions are supported:

  • read - Read/view access
  • create - Create new resources
  • update - Modify existing resources
  • delete - Remove resources

Permission Types

Static Permissions (Boolean)

Permit.register('admin', 'posts', {
  read: true,  // Always allowed
  delete: false, // Always denied
});

Dynamic Permissions (Function)

Permit.register('editor', 'posts', {
  update: (user, data) => {
    // Only allow if user owns the post
    return user.id === data.authorId;
  },
});

// Function permissions require data parameter
Permit.check(user, 'posts', 'update', { authorId: '123' }); // true/false
Permit.check(user, 'posts', 'update'); // false (no data provided)

Wildcards

Wildcard Role - Apply to All Users

import { WILDCARD } from '@vielzeug/permit';

// All users can view posts
Permit.register(WILDCARD, 'posts', { read: true });

const anyUser = { id: '999', roles: ['guest'] };
Permit.check(anyUser, 'posts', 'read'); // true

Wildcard Resource - Apply to All Resources

// Admins can view everything
Permit.register('admin', WILDCARD, { read: true });

const admin = { id: '1', roles: ['admin'] };
Permit.check(admin, 'posts', 'read'); // true
Permit.check(admin, 'comments', 'read'); // true
Permit.check(admin, 'anything', 'read'); // true

Precedence

Specific permissions override wildcard permissions:

Permit.register('admin', WILDCARD, { read: true });
Permit.register('admin', 'secrets', { read: false });

const admin = { id: '1', roles: ['admin'] };
Permit.check(admin, 'posts', 'read'); // true (wildcard)
Permit.check(admin, 'secrets', 'read'); // false (specific override)

Anonymous Users

Use the ANONYMOUS role for unauthenticated users:

import { ANONYMOUS } from '@vielzeug/permit';

// Public read access
Permit.register(ANONYMOUS, 'posts', { read: true });

// Malformed users are treated as anonymous
const malformedUser = null;
Permit.check(malformedUser, 'posts', 'read'); // true

Normalization

All roles and resources are normalized (trimmed and lowercased) to prevent mismatches:

Permit.register('Admin', 'Posts', { read: true });

const user = { id: '1', roles: ['ADMIN'] };
Permit.check(user, 'posts', 'read'); // true
Permit.check(user, 'POSTS', 'read'); // true
Permit.check(user, '  posts  ', 'read'); // true

API Reference

Permit.register()

Register permissions for a role and resource.

Permit.register<TUser, TData>(
  role: string,
  resource: string,
  actions: Partial<Record<PermissionAction, PermissionCheck<TUser, TData>>>
): void

Parameters:

  • role - Role identifier (normalized)
  • resource - Resource identifier (normalized)
  • actions - Object mapping actions to permissions (boolean or function)

Throws:

  • Error if role or resource is empty
  • Error if invalid action is provided

Example:

Permit.register('moderator', 'comments', {
  read: true,
  delete: (user, data) => {
    // Moderators can delete spam or their own comments
    return data.isSpam || user.id === data.authorId;
  },
});

Behavior:

  • Merges with existing permissions (doesn't replace)
  • Validates action keys at runtime
  • Normalizes role and resource

Permit.set()

Set permissions for a role and resource, optionally replacing existing ones.

Permit.set<TUser, TData>(
  role: string,
  resource: string,
  actions: Partial<Record<PermissionAction, PermissionCheck<TUser, TData>>>,
  replace?: boolean
): void

Parameters:

  • role - Role identifier
  • resource - Resource identifier
  • actions - Permission actions
  • replace - If true, replaces existing; if false, merges (default: false)

Example:

// Merge with existing
Permit.set('editor', 'posts', { read: true, create: true });

// Replace completely
Permit.set('editor', 'posts', { read: true }, true);

Permit.check()

Check if a user has permission to perform an action.

Permit.check<TUser, TData>(
  user: TUser,
  resource: string,
  action: PermissionAction,
  data?: TData
): boolean

Parameters:

  • user - User object with id and roles properties
  • resource - Resource identifier
  • action - Permission action to check
  • data - Optional contextual data for function-based permissions

Returns: true if allowed, false otherwise

Example:

const user = { id: '123', roles: ['editor'] };

// Static check
Permit.check(user, 'posts', 'read'); // boolean

// Dynamic check with data
Permit.check(user, 'posts', 'update', { authorId: '123' }); // boolean

Behavior:

  • First-match-wins policy (first allow grants access)
  • Checks specific roles before wildcard
  • Function permissions return false if data is undefined
  • Malformed users treated as ANONYMOUS + WILDCARD

Permit.unregister()

Remove permissions for a role and resource.

Permit.unregister(
  role: string,
  resource: string,
  action?: PermissionAction
): void

Parameters:

  • role - Role identifier
  • resource - Resource identifier
  • action - Optional specific action to remove

Example:

// Remove specific action
Permit.unregister('editor', 'posts', 'delete');

// Remove all actions for resource
Permit.unregister('editor', 'posts');

Behavior:

  • Automatically cleans up empty resource entries
  • Automatically cleans up empty role entries
  • Safe to call on non-existent permissions

Permit.hasRole()

Check if a user has a specific role.

Permit.hasRole(user: BaseUser, role: string): boolean

Parameters:

  • user - User object
  • role - Role to check for

Returns: true if user has the role, false otherwise

Example:

const user = { id: '1', roles: ['admin', 'editor'] };

Permit.hasRole(user, 'admin'); // true
Permit.hasRole(user, 'ADMIN'); // true (normalized)
Permit.hasRole(user, 'moderator'); // false

Behavior:

  • Case-insensitive comparison
  • Returns true for ANONYMOUS if user is malformed

Permit.clear()

Remove all registered permissions.

Permit.clear(): void

Example:

Permit.clear();
// All permissions removed

Permit.roles (getter)

Get a deep copy of all registered permissions.

get roles(): RolesWithPermissions

Returns: Deep copy of the permission registry

Example:

const permissions = Permit.roles;

// Inspect structure
for (const [role, resources] of permissions) {
  console.log(`Role: ${role}`);
  for (const [resource, actions] of resources) {
    console.log(`  Resource: ${resource}`, actions);
  }
}

Behavior:

  • Returns deep copy (modifications don't affect internal state)
  • Useful for debugging and introspection

Advanced Usage

Multi-Role Users

Users with multiple roles get permissions from all their roles:

Permit.register('admin', 'posts', { delete: true });
Permit.register('editor', 'posts', { update: true });

const user = { id: '1', roles: ['admin', 'editor'] };

Permit.check(user, 'posts', 'delete'); // true (from admin)
Permit.check(user, 'posts', 'update'); // true (from editor)

Conditional Permissions

Complex business logic in function-based permissions:

Permit.register('editor', 'posts', {
  update: (user, data) => {
    // Multiple conditions
    if (data.isLocked) return false;
    if (user.id === data.authorId) return true;
    if (data.isPublished && user.roles.includes('senior-editor')) return true;
    return false;
  },
});

Type-Safe Permissions

Use TypeScript generics for type safety:

interface User {
  id: string;
  roles: string[];
  department: string;
}

interface PostData {
  authorId: string;
  department: string;
  isPublished: boolean;
}

Permit.register<User, PostData>('editor', 'posts', {
  update: (user, data) => {
    // TypeScript knows the types
    return user.department === data.department && !data.isPublished;
  },
});

const user: User = { id: '1', roles: ['editor'], department: 'tech' };
const post: PostData = { authorId: '2', department: 'tech', isPublished: false };

Permit.check(user, 'posts', 'update', post); // Type-safe

Permission Auditing

Inspect all registered permissions:

function auditPermissions() {
  const permissions = Permit.roles;
  
  for (const [role, resources] of permissions) {
    console.log(`\nRole: ${role}`);
    
    for (const [resource, actions] of resources) {
      console.log(`  Resource: ${resource}`);
      
      for (const [action, permission] of Object.entries(actions)) {
        const type = typeof permission === 'function' ? 'dynamic' : 'static';
        console.log(`    ${action}: ${type}`);
      }
    }
  }
}

Testing Permissions

import { Permit, WILDCARD, ANONYMOUS } from '@vielzeug/permit';

describe('Permissions', () => {
  beforeEach(() => {
    Permit.clear(); // Clean slate
  });

  it('should allow admin to delete posts', () => {
    Permit.register('admin', 'posts', { delete: true });
    
    const admin = { id: '1', roles: ['admin'] };
    expect(Permit.check(admin, 'posts', 'delete')).toBe(true);
  });

  it('should deny non-owners from editing', () => {
    Permit.register('editor', 'posts', {
      update: (user, data) => user.id === data.authorId,
    });
    
    const editor = { id: '1', roles: ['editor'] };
    expect(Permit.check(editor, 'posts', 'update', { authorId: '2' })).toBe(false);
  });
});

Security Considerations

Malformed User Handling

Malformed users (missing id or roles) are treated as ANONYMOUS + WILDCARD:

// Configure permissions for anonymous users
Permit.register(ANONYMOUS, 'posts', { read: true });

const malformed = null;
Permit.check(malformed, 'posts', 'read'); // true (has ANONYMOUS role)
Permit.check(malformed, 'posts', 'create'); // false

Warning: Malformed users also receive the WILDCARD role, so they'll inherit any wildcard permissions. Ensure wildcard permissions are intended for public access.

Function Permission Requirements

Function-based permissions require data to evaluate:

Permit.register('editor', 'posts', {
  update: (user, data) => user.id === data.authorId,
});

// Without data - always returns false
Permit.check(user, 'posts', 'update'); // false

// With data - evaluates function
Permit.check(user, 'posts', 'update', { authorId: '123' }); // true/false

Allow-on-Any Policy

Permission checks use "allow on any true" policy:

  • First matching allow grants access
  • No explicit deny rules
  • Absence of permission = deny
Permit.register('role1', 'posts', { read: false });
Permit.register('role2', 'posts', { read: true });

const user = { id: '1', roles: ['role1', 'role2'] };
Permit.check(user, 'posts', 'read'); // true (role2 allows)

Types

// User type
export type BaseUser = {
  id: string;
  roles: string[];
};

// Permission actions
export type PermissionAction = 'read' | 'create' | 'update' | 'delete';

// Permission check (static or dynamic)
export type PermissionCheck<T, D> = boolean | ((user: T, data: D) => boolean);

// Permission map for a resource
export type PermissionMap<T, D> = Partial<Record<PermissionAction, PermissionCheck<T, D>>>;

// Resource permissions map
export type ResourcePermissions<T, D> = Map<string, PermissionMap<T, D>>;

// Full permissions registry
export type RolesWithPermissions<T, D> = Map<string, ResourcePermissions<T, D>>;

// Constants
export const WILDCARD = '*';
export const ANONYMOUS = 'anonymous';

Best Practices

1. Use Specific Roles First

Define specific role permissions before wildcards to ensure proper precedence:

// Specific first
Permit.register('admin', 'secrets', { read: true });
Permit.register('user', 'secrets', { read: false });

// Then wildcards
Permit.register(WILDCARD, 'public', { read: true });

2. Validate Data in Functions

Always validate data exists before accessing properties:

Permit.register('editor', 'posts', {
  update: (user, data) => {
    if (!data || !data.authorId) return false;
    return user.id === data.authorId;
  },
});

3. Use Type Parameters

Leverage TypeScript for type safety:

interface MyUser extends BaseUser {
  department: string;
}

interface MyData {
  department: string;
}

Permit.register<MyUser, MyData>('manager', 'reports', {
  read: (user, data) => user.department === data.department,
});

4. Document Permission Logic

Add comments for complex permission rules:

Permit.register('editor', 'posts', {
  update: (user, data) => {
    // Editors can update:
    // 1. Their own unpublished posts
    // 2. Published posts in their department (if senior)
    if (user.id === data.authorId && !data.isPublished) return true;
    if (data.department === user.department && user.isSenior) return true;
    return false;
  },
});

5. Use Constants for Roles

Define role constants for consistency:

const ROLES = {
  ADMIN: 'admin',
  EDITOR: 'editor',
  VIEWER: 'viewer',
} as const;

Permit.register(ROLES.ADMIN, 'posts', { delete: true });

Comparison

| Feature | @vielzeug/permit | casl | accesscontrol | |---------|------------------|------|---------------| | Bundle Size | ~2KB | ~15KB | ~10KB | | Dependencies | 1 (logging) | Multiple | 0 | | TypeScript | First-class | Good | Basic | | Dynamic Permissions | ✅ Functions | ✅ Conditions | ❌ | | Normalization | ✅ Built-in | ❌ | ❌ | | Wildcards | ✅ Role + Resource | ⚠️ Limited | ✅ | | Type Exports | ✅ All types | ⚠️ Some | ❌ | | Security Defaults | ✅ Safe malformed users | ⚠️ | ⚠️ |

License

MIT © Helmuth Saatkamp

Links


Part of the Vielzeug ecosystem - A collection of type-safe utilities for modern web development.