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 🙏

© 2025 – Pkg Stats / Ryan Hefner

access-control-kit

v0.1.1

Published

Framework-agnostic Policy-based Access Control with React bindings

Readme

Access Control Kit

A powerful, type-safe, and framework-agnostic access control library. Define your policies once and use them anywhere—Node.js, React, Vue, Svelte, or plain JavaScript.

DEMO LINK

License

MIT

Features

  • 🔒 Type-Safe: Automatic type inference for resources and actions based on your configuration.
  • 📝 Statement-Based Policy: Granular control with 'allow' and 'deny' effects, similar to AWS IAM.
  • 🎯 Specificity-Based Evaluation: More specific statements override broader ones, enabling fine-grained control.
  • 🚀 Framework Agnostic Core: Pure JS/TS logic (access-control-kit) that can be used in any environment.
  • ⚛️ React Bindings: First-class support for React (access-control-kit/react) with Providers, Hooks, and Guards.
  • 🌍 Universal: Share the same policy logic across your entire stack (Backend & Frontend).
  • 🎯 Attribute-Based Access Control (ABAC): Support for flexible runtime contexts.
  • 🃏 Wildcard Support: Support for * actions and resources.
  • 🛡️ Secure Defaults: Default deny policy with explicit allow overrides.

Installation

npm install access-control-kit
# or
yarn add access-control-kit
# or
pnpm add access-control-kit

Quick Start

This guide shows you how to set up access-control-kit in both bare JavaScript/TypeScript projects and React applications.


Part 1: Bare JavaScript/TypeScript Project

Perfect for Node.js backends, API routes, serverless functions, or any non-React JavaScript environment.

Step 1: Create Access Control Configuration

Create a centralized access-control.ts file to define your resources, actions, and export utilities.

// access-control.ts
import { getAccessControl, type TAccessControlPolicy } from 'access-control-kit';

// Define your resources and actions
// Use 'as const' to ensure type inference
export const config = {
  POST: ['create', 'read', 'update', 'delete'],
  USER: ['read', 'invite', 'delete'],
  SETTINGS: ['view', 'edit'],
} as const;

// Export the type for use in your app
export type AccessControlPolicy = TAccessControlPolicy<typeof config>;

// Export the getAccessControl utility
export { getAccessControl };

Step 2: Fetch or Define User Policy

Create a utility to fetch or generate the user's access policy based on their role, permissions, or attributes.

// utils/getUserPolicy.ts
import type { AccessControlPolicy } from '../access-control';

// Example: Fetch policy from API
export async function fetchUserPolicy(userId: string): Promise<AccessControlPolicy> {
  const response = await fetch(`/api/users/${userId}/policy`);
  return response.json();
}

// Or define static policies based on roles
export function getRoleBasedPolicy(role: 'admin' | 'editor' | 'viewer'): AccessControlPolicy {
  switch (role) {
    case 'admin':
      return [{ resource: '*', actions: ['*'], effect: 'allow' }];
    
    case 'editor':
      return [
        { resource: 'POST', actions: ['create', 'read', 'update'], effect: 'allow' },
        { resource: 'POST', actions: ['delete'], effect: 'allow', contexts: [{ authorId: 'current-user' }] },
        { resource: 'USER', actions: ['read'], effect: 'allow' },
      ];
    
    case 'viewer':
      return [
        { resource: 'POST', actions: ['read'], effect: 'allow' },
        { resource: 'USER', actions: ['read'], effect: 'allow' },
      ];
    
    default:
      return [];
  }
}

Step 3: Use in Your Application

// Example: Express.js API route
import { getAccessControl } from './access-control';
import { fetchUserPolicy } from './utils/getUserPolicy';

app.post('/api/posts', async (req, res) => {
  // Get user policy
  const policy = await fetchUserPolicy(req.user.id);
  const { can } = getAccessControl(policy);
  
  // Check permission
  if (!can('POST', 'create')) {
    return res.status(403).json({ error: 'Forbidden' });
  }
  
  // Proceed with creation
  const post = await createPost(req.body);
  res.json(post);
});

// Example: Check with context
app.delete('/api/posts/:id', async (req, res) => {
  const policy = await fetchUserPolicy(req.user.id);
  const post = await getPost(req.params.id);
  const { can } = getAccessControl(policy);
  
  // Check if user can delete (might require ownership)
  if (!can('POST', 'delete', { authorId: post.authorId })) {
    return res.status(403).json({ error: 'Forbidden' });
  }
  
  await deletePost(req.params.id);
  res.json({ success: true });
});

// Example: Check multiple permissions
app.get('/api/posts/:id/actions', async (req, res) => {
  const policy = await fetchUserPolicy(req.user.id);
  const post = await getPost(req.params.id);
  const { can, canAll, canAny } = getAccessControl(policy);
  
  res.json({
    canRead: can('POST', 'read'),
    canUpdate: can('POST', 'update', { authorId: post.authorId }),
    canDelete: can('POST', 'delete', { authorId: post.authorId }),
    canManage: canAll('POST', ['update', 'delete'], { authorId: post.authorId }),
    hasAnyAccess: canAny('POST', ['read', 'update', 'delete']),
  });
});

Part 2: React Application

Perfect for React, Next.js, or any React-based frontend application.

Step 1: Create Access Control Configuration

Create a centralized access-control.ts file with React-specific exports.

// access-control.ts
import { createAccessControl, type TAccessControlPolicy } from 'access-control-kit/react';

// Define your resources and actions
export const config = {
  POST: ['create', 'read', 'update', 'delete'],
  USER: ['read', 'invite', 'delete'],
  SETTINGS: ['view', 'edit'],
} as const;

// Export the type for use in your app
export type AccessControlPolicy = TAccessControlPolicy<typeof config>;

// Create and export React utilities
export const {
  AccessControlProvider,
  useAccessControl,
  AccessControlGuard,
  withAccessControl,
} = createAccessControl(config);

Step 2: Setup Provider in Your App

Wrap your application with the AccessControlProvider and fetch the user's policy.

[!NOTE] The isLoading prop is only meaningful if your policy is fetched or generated asynchronously. If you have a static policy or it's already available, you can omit this prop (defaults to false).

// App.tsx
import { useState, useEffect } from 'react';
import { AccessControlProvider, type AccessControlPolicy } from './access-control';

// Utility to fetch user policy asynchronously
async function fetchUserPolicy(userId: string): Promise<AccessControlPolicy> {
  const response = await fetch(`/api/users/${userId}/policy`);
  return response.json();
}

function App() {
  const [policy, setPolicy] = useState<AccessControlPolicy>([]);
  const [isLoading, setIsLoading] = useState(true);
  
  useEffect(() => {
    // Fetch policy on mount
    fetchUserPolicy('current-user-id')
      .then(setPolicy)
      .finally(() => setIsLoading(false));
  }, []);
  
  return (
    <AccessControlProvider accessControlPolicy={policy} isLoading={isLoading}>
      <Dashboard />
    </AccessControlProvider>
  );
}

Step 3: Use Access Control Utilities

A. Using the useAccessControl Hook

// components/PostActions.tsx
import { useAccessControl } from '../access-control';

interface PostActionsProps {
  post: {
    id: string;
    authorId: string;
    title: string;
  };
}

function PostActions({ post }: PostActionsProps) {
  const { can, canAll, isLoading } = useAccessControl();
  
  if (isLoading) {
    // Show skeleton while policy is loading
    return (
      <div className="post-actions">
        <div className="skeleton skeleton-title" />
        <div className="skeleton skeleton-button" />
        <div className="skeleton skeleton-button" />
      </div>
    );
  }
  
  const canUpdate = can('POST', 'update', { authorId: post.authorId });
  const canDelete = can('POST', 'delete', { authorId: post.authorId });
  const canManage = canAll('POST', ['update', 'delete'], { authorId: post.authorId });
  
  return (
    <div className="post-actions">
      <h3>{post.title}</h3>
      
      {canUpdate && (
        <button onClick={() => editPost(post.id)}>
          Edit Post
        </button>
      )}
      
      {canDelete && (
        <button onClick={() => deletePost(post.id)}>
          Delete Post
        </button>
      )}
      
      {canManage && (
        <button onClick={() => openAdvancedSettings(post.id)}>
          Advanced Settings
        </button>
      )}
    </div>
  );
}

B. Using the AccessControlGuard Component

// components/SettingsPage.tsx
import { AccessControlGuard } from '../access-control';

function SettingsPage() {
  return (
    <div>
      <h1>Settings</h1>
      
      {/* Standard mode with fallback */}
      <AccessControlGuard
        resource="SETTINGS"
        action="view"
        fallback={<p>You don't have access to view settings.</p>}
        loadingFallback={<div className="skeleton skeleton-content" />}
      >
        <SettingsContent />
      </AccessControlGuard>
      
      {/* PassThrough mode (render props) */}
      <AccessControlGuard resource="SETTINGS" action="edit" passThrough>
        {({ allowed, isLoading }) => (
          <button 
            disabled={!allowed || isLoading}
            onClick={saveSettings}
          >
            {isLoading ? 'Checking...' : allowed ? 'Save Settings' : 'Read Only'}
          </button>
        )}
      </AccessControlGuard>
    </div>
  );
}

C. Using the withAccessControl HOC

// components/AdminPanel.tsx
import { withAccessControl } from '../access-control';

function AdminPanel() {
  return (
    <div>
      <h1>Admin Panel</h1>
      <p>Welcome to the admin area!</p>
    </div>
  );
}

// Protect the entire component
export default withAccessControl(
  AdminPanel,
  'SETTINGS',
  'edit',
  undefined,
  () => <div>Access Denied: Admin privileges required.</div>,
  () => <div className="skeleton skeleton-page" />
);

D. Complete Dashboard Example

// components/Dashboard.tsx
import { useAccessControl, AccessControlGuard } from '../access-control';

function Dashboard() {
  const { can, canAny, isLoading } = useAccessControl();
  
  if (isLoading) {
    return <div className="skeleton skeleton-dashboard" />;
  }
  
  return (
    <div className="dashboard">
      <h1>Dashboard</h1>
      
      {/* Conditional rendering with hook */}
      {can('POST', 'create') && (
        <button onClick={createNewPost}>
          Create New Post
        </button>
      )}
      
      {/* Guard component for sections */}
      <AccessControlGuard
        resource="USER"
        action="invite"
        fallback={<p>You cannot invite users.</p>}
      >
        <InviteUserForm />
      </AccessControlGuard>
      
      {/* Show section if user has ANY post permission */}
      {canAny('POST', ['create', 'read', 'update', 'delete']) && (
        <PostsSection />
      )}
      
      {/* Settings with passThrough */}
      <AccessControlGuard resource="SETTINGS" action="view" passThrough>
        {({ allowed }) => (
          <div className={allowed ? 'settings-enabled' : 'settings-disabled'}>
            <h2>Settings</h2>
            {allowed ? <SettingsForm /> : <p>Contact admin for access.</p>}
          </div>
        )}
      </AccessControlGuard>
    </div>
  );
}

Summary

  • Bare JS/TS: Use getAccessControl(policy) to get can, canAll, canAny utilities
  • React: Use createAccessControl(config) to get Provider, useAccessControl hook, AccessControlGuard component, and withAccessControl HOC
  • Centralized Config: Export everything from access-control.ts for consistency
  • Type Safety: Use as const on config for full TypeScript inference

Core API (access-control-kit)

The core module is framework-agnostic and can be used anywhere.

getAccessControl(policy)

Evaluates permissions based on the provided policy.

Parameters:

  • policy: TAccessControlPolicy<T> - The access control policy to evaluate.

Returns: Object with:

  • policy: The original policy
  • can(resource, action, context?): Check if a single action is allowed
  • canAll(resource, actions, context?): Check if ALL actions are allowed
  • canAny(resource, actions, context?): Check if ANY action is allowed

Example:

import { getAccessControl } from 'access-control-kit';

const policy = [
  { resource: 'POST', actions: ['read', 'update'], effect: 'allow' }
];

const { can, canAll, canAny } = getAccessControl(policy);

can('POST', 'read'); // true
can('POST', 'delete'); // false

canAll('POST', ['read', 'update']); // true
canAny('POST', ['read', 'delete']); // true

can(resource, action, context?)

Check if a specific action on a resource is allowed.

Parameters:

  • resource: Resource key from your config
  • action: Action name from your config
  • context?: Optional runtime context object or array of objects

Returns: boolean

Examples:

// Simple check
can('POST', 'read'); // true/false

// With single context
can('POST', 'update', { authorId: 'user-123' }); // true if policy allows

// With multiple contexts (OR logic)
can('POST', 'update', [
  { authorId: 'user-123' },
  { role: 'admin' }
]); // true if ANY context matches

canAll(resource, actions, context?)

Check if ALL specified actions are allowed.

Parameters:

  • resource: Resource key
  • actions: Array of action names
  • context?: Optional context

Returns: boolean

Example:

canAll('POST', ['read', 'update']); // true only if BOTH are allowed

canAny(resource, actions, context?)

Check if ANY of the specified actions are allowed.

Parameters:

  • resource: Resource key
  • actions: Array of action names
  • context?: Optional context

Returns: boolean

Example:

canAny('POST', ['read', 'delete']); // true if EITHER is allowed

React API (access-control-kit/react)

React-specific bindings with full TypeScript support.

createAccessControl(config)

Factory function to create typed React utilities.

Parameters:

  • config: Your resource/action configuration object

Returns: Object with:

  • AccessControlProvider: Context provider component
  • useAccessControl: Hook to access permissions
  • AccessControlGuard: Component guard
  • withAccessControl: HOC wrapper

Example:

import { createAccessControl } from 'access-control-kit/react';

const config = {
  POST: ['create', 'read', 'update', 'delete'],
} as const;

export const {
  AccessControlProvider,
  useAccessControl,
  AccessControlGuard,
  withAccessControl,
} = createAccessControl(config);

<AccessControlProvider>

Context provider that makes the policy available to child components.

Props:

  • accessControlPolicy: TAccessControlPolicy<T> - The policy to enforce
  • isLoading?: boolean - Optional loading state (default: false)
  • children: React.ReactNode

Example:

<AccessControlProvider accessControlPolicy={policy} isLoading={isLoading}>
  <App />
</AccessControlProvider>

useAccessControl()

Hook to access the policy context.

Returns: Object with:

  • can(resource, action, context?): Check permission
  • canAll(resource, actions, context?): Check all permissions
  • canAny(resource, actions, context?): Check any permission
  • policy: The current policy
  • isLoading: Loading state

Example:

function MyComponent() {
  const { can, isLoading } = useAccessControl();
  
  if (isLoading) return <div>Loading...</div>;
  
  return (
    <div>
      {can('POST', 'update', { authorId: currentUser.id }) && (
        <button>Edit Post</button>
      )}
    </div>
  );
}

<AccessControlGuard>

Component that conditionally renders children based on permissions.

Props:

  • resource: Resource key
  • action: Action name
  • context?: Optional runtime context
  • fallback?: Content to show if denied (default: null)
  • loadingFallback?: Content to show while loading (default: null)
  • passThrough?: If true, enables render props mode
  • children: Content to show if allowed, or render function if passThrough

Standard Mode:

<AccessControlGuard 
  resource="POST" 
  action="update"
  context={{ authorId: post.authorId }}
  fallback={<span>No Access</span>}
  loadingFallback={<span>Checking...</span>}
>
  <button>Edit Post</button>
</AccessControlGuard>

PassThrough Mode (Render Props):

<AccessControlGuard resource="POST" action="update" passThrough>
  {({ allowed, isLoading }) => (
    <button disabled={!allowed || isLoading}>
      {isLoading ? 'Checking...' : allowed ? 'Edit' : 'Locked'}
    </button>
  )}
</AccessControlGuard>

withAccessControl(Component, resource, action, context?, FallbackComponent?, LoadingComponent?)

Higher-Order Component to protect a component with access control.

Parameters:

  • Component: Component to wrap
  • resource: Resource key
  • action: Action name
  • context?: Optional context
  • FallbackComponent?: Component to show if denied
  • LoadingComponent?: Component to show while loading

Example:

const ProtectedSettings = withAccessControl(
  SettingsPage,
  'SETTINGS',
  'edit',
  undefined,
  () => <div>Access Denied</div>,
  () => <div>Loading...</div>
);

Recipes

Role-Based Access Control (RBAC)

import { TAccessControlPolicy } from 'access-control-kit';

type Role = 'ADMIN' | 'EDITOR' | 'VIEWER';

export function getRolePolicy(role: Role): TAccessControlPolicy<typeof config> {
  switch (role) {
    case 'ADMIN':
      return [{ resource: '*', actions: ['*'], effect: 'allow' }];
    
    case 'EDITOR':
      return [
        { resource: 'POST', actions: ['create', 'read', 'update'], effect: 'allow' },
        { resource: 'POST', actions: ['delete'], effect: 'allow', contexts: [{ authorId: 'current-user' }] },
      ];
    
    case 'VIEWER':
      return [{ resource: 'POST', actions: ['read'], effect: 'allow' }];
    
    default:
      return [];
  }
}

Attribute-Based Access Control (ABAC)

// Policy with contexts
const policy = [
  {
    resource: 'DOCUMENT',
    actions: ['view'],
    effect: 'allow',
    contexts: [
      { department: 'engineering' },
      { public: true }
    ]
  }
];

// Usage - checks if ANY context matches
can('DOCUMENT', 'view', { department: 'engineering' }); // true
can('DOCUMENT', 'view', { public: true }); // true
can('DOCUMENT', 'view', { department: 'sales' }); // false

Specificity-Based Evaluation

access-control-kit uses specificity-based evaluation where more specific statements (with more context keys) override broader ones. This enables sophisticated access control patterns.

How Specificity Works

  1. Specificity Score: Number of keys in the context object

    • No context: specificity = 0
    • { userId: 'x' }: specificity = 1
    • { userId: 'x', postId: 'y' }: specificity = 2
  2. Evaluation Rules:

    • More specific statements take precedence
    • Among equally specific statements, deny wins
    • Statements without context are least specific

Example: Override Broad Deny with Specific Allow

const policy = [
  // Broad deny - specificity = 1
  {
    resource: 'POST',
    actions: ['delete'],
    effect: 'deny',
    contexts: [{ userId: 'restricted-user' }]
  },
  // Specific allow - specificity = 2 (overrides above)
  {
    resource: 'POST',
    actions: ['delete'],
    effect: 'allow',
    contexts: [{ userId: 'restricted-user', postId: 'owned-post' }]
  }
];

// User can delete their own post despite broad deny
can('POST', 'delete', { userId: 'restricted-user', postId: 'owned-post' }); // ✅ true

// But cannot delete other posts
can('POST', 'delete', { userId: 'restricted-user', postId: 'other-post' }); // ❌ false

Example: Multi-Level Specificity

const policy = [
  // Level 0: Deny all (no context)
  { resource: 'DOCUMENT', actions: ['view'], effect: 'deny' },
  
  // Level 1: Allow for department (1 key)
  { resource: 'DOCUMENT', actions: ['view'], effect: 'allow', contexts: [{ department: 'eng' }] },
  
  // Level 2: Deny for specific doc (2 keys)
  { resource: 'DOCUMENT', actions: ['view'], effect: 'deny', contexts: [{ department: 'eng', docId: 'secret' }] }
];

can('DOCUMENT', 'view'); // ❌ false (level 0 deny)
can('DOCUMENT', 'view', { department: 'eng' }); // ✅ true (level 1 allow)
can('DOCUMENT', 'view', { department: 'eng', docId: 'public' }); // ✅ true (level 1 allow)
can('DOCUMENT', 'view', { department: 'eng', docId: 'secret' }); // ❌ false (level 2 deny)

Multiple Contexts (OR Logic)

// Check if user has access via multiple potential contexts
const userContexts = [
  { department: 'engineering', role: 'intern' },
  { department: 'sales', role: 'lead' }
];

can('DOCUMENT', 'view', userContexts); // true if ANY context matches policy

Loading States

function App() {
  const [policy, setPolicy] = useState([]);
  const [isLoading, setIsLoading] = useState(true);
  
  useEffect(() => {
    fetchPolicy().then(p => {
      setPolicy(p);
      setIsLoading(false);
    });
  }, []);
  
  return (
    <AccessControlProvider accessControlPolicy={policy} isLoading={isLoading}>
      <MyComponent />
    </AccessControlProvider>
  );
}

Server-Side Validation

// API Route (Next.js App Router)
import { getAccessControl } from 'access-control-kit';

export async function DELETE(req: Request, { params }: { params: { id: string } }) {
  const session = await getSession();
  const policy = await getUserPolicy(session.userId);
  const post = await getPost(params.id);
  
  const { can } = getAccessControl(policy);
  
  if (!can('POST', 'delete', { authorId: post.authorId })) {
    return new Response('Forbidden', { status: 403 });
  }
  
  await deletePost(params.id);
  return new Response('OK');
}

Building Wrappers for Other Frameworks

Since access-control-kit core is framework-agnostic, you can easily build wrappers for Vue, Svelte, Angular, etc.

Vue Example

// useAccessControl.ts
import { getAccessControl } from 'access-control-kit';
import { computed, unref } from 'vue';

export function useAccessControl(policyRef) {
  // Re-create access control object when policy changes
  const accessControl = computed(() => getAccessControl(unref(policyRef)));
  
  return {
    // Delegate to the current access control instance
    can: (resource, action, context) => accessControl.value.can(resource, action, context),
    canAll: (resource, actions, context) => accessControl.value.canAll(resource, actions, context),
    canAny: (resource, actions, context) => accessControl.value.canAny(resource, actions, context),
  };
}

Svelte Example

// accessControl.ts
import { getAccessControl } from 'access-control-kit';
import { writable, derived } from 'svelte/store';

export function createAccessControlStore(initialPolicy) {
  const policy = writable(initialPolicy);
  
  // Derived store that updates whenever policy changes
  // Returns { can, canAll, canAny, policy }
  const accessControl = derived(policy, $policy => getAccessControl($policy));
  
  return {
    policy,
    subscribe: accessControl.subscribe, // Make it a store
  };
}

// Usage in component:
// <script>
//   import { createAccessControlStore } from './accessControl';
//   const ac = createAccessControlStore([]);
// </script>
//
// {#if $ac.can('POST', 'read')}...{/if}

TypeScript Support

Full TypeScript support with automatic type inference:

const config = {
  POST: ['create', 'read', 'update', 'delete'],
  USER: ['read', 'invite'],
} as const;

const { can } = createAccessControl(config);

// ✅ TypeScript knows these are valid
can('POST', 'read');
can('USER', 'invite');

// ❌ TypeScript errors
can('POST', 'invalid'); // Error: 'invalid' is not a valid action
can('INVALID', 'read'); // Error: 'INVALID' is not a valid resource

License

MIT © Aashish Rai