JSPM

@lbstack/accessx

0.4.0
    • ESM via JSPM
    • ES Module Entrypoint
    • Export Map
    • Keywords
    • License
    • Repository URL
    • TypeScript Types
    • README
    • Created
    • Published
    • Downloads 15
    • Score
      100M100P100Q42634F
    • License MIT

    A role & resource based access control system with end to end type safety.

    Package Exports

    • @lbstack/accessx
    • @lbstack/accessx/dist/index.js

    This package does not declare an exports field, so the exports above have been automatically detected and optimized by JSPM instead. If any package subpath is missing, it is recommended to post an issue to the original package (@lbstack/accessx) to support the "exports" field. If that is not possible, create a JSPM override to customize the exports field for this package.

    Readme

    @lbstack/accessx

    ๐Ÿ“„ llm.txt (AI Friendly)

    A TypeScript-first RBAC permission engine with automatic permission generation, designed to be the single source of truth for:

    • Backend authorization
    • Frontend UI access control
    • Database permission storage
    • Admin permission management

    No manual permission strings.
    No role leakage to frontend.
    Full autocomplete everywhere.


    โœจ Key Features

    • ๐Ÿ” Automatic permission generation
    • ๐Ÿง  Strong TypeScript inference & autocomplete
    • ๐Ÿ—๏ธ Single initialization โ€“ use everywhere
    • ๐Ÿ–ฅ๏ธ Backend (Express / Nest / Hono)
    • ๐ŸŽจ Frontend (React hooks & components)
    • ๐Ÿ—„๏ธ Database-friendly permission keys
    • ๐Ÿ“ฆ Package & framework agnostic

    ๐Ÿงฉ Core Concept

    You define:

    • Roles
    • Actions
    • Resources (modules)

    The package automatically generates permissions in this format:

    RESOURCE_KEY:ACTION

    Example:

    BLOGS:CREATE BLOGS:READ USER:DELETE

    These permission keys are:

    • Stored in DB
    • Sent to frontend after login
    • Used in UI & API checks
    • Fully type-safe

    ๐Ÿ“ฆ Installation

    npm install @lbstack/accessx
    
    
    or
    
    pnpm add @lbstack/accessx
    
    ๐Ÿš€ Initialization (Single Source of Truth)
    import { createAccess } from "@lbstack/accessx";
    
    export const access = createAccess({
      roles: ["ADMIN", "EDITOR", "CUSTOMER"] as const,
    
      actions: ["CREATE", "READ", "UPDATE", "DELETE"] as const,
    
      resources: [
        {
          name: "Users",
          key: "USER",
          description: "User management",
        },
        {
          name: "Blogs",
          key: "BLOGS",
          description: "Blog posts",
        },
      ] as const,
    });
    
    
    โš ๏ธ as const is required for TypeScript autocomplete.
    
    ๐Ÿ”‘ Automatically Generated Permissions
    access.permissionKeys
    
    [
      "USER:CREATE",
      "USER:READ",
      "USER:UPDATE",
      "USER:DELETE",
      "BLOGS:CREATE",
      "BLOGS:READ",
      "BLOGS:UPDATE",
      "BLOGS:DELETE",
    ]
    
    
    No permission strings are written manually.
    
    ๐Ÿ—„๏ธ Database Usage
    Seed permissions table
    await db.permissions.insertMany(access.permissions);
    
    
    Each permission contains metadata:
    
    {
      key: "BLOGS:CREATE",
      resource: { name, key, description },
      action: "CREATE"
    }
    
    ๐Ÿ” Backend Usage
    Assign permissions to roles
    access.allow("ADMIN", "USER:DELETE");
    access.allow("EDITOR", "BLOGS:CREATE");
    
    Check permission (Service / Controller)
    access.can(user.role, "BLOGS:UPDATE");
    
    Normalize permissions from DB
    const permissionsFromDb = ["BLOGS:READ", "USER:DELETE"];
    
    const permissions = access.normalizePermissions(permissionsFromDb);
    
    
    Ensures only valid generated permissions are used.
    
    res.json({
      user,
      permissions: access.resolvePermissions(user.role),
    });

    ๐Ÿ”‘ Assigning Permissions

    There are two ways to assign permissions to roles: Manual (Static) and Dynamic (Database-linked).

    1. Manual Assignment (Static)

    Best for hardcoded defaults or simple applications. This is the standard, non-mandatory approach.

    // Single permission
    access.allow("ADMIN", "USER:DELETE");
    
    // Multiple permissions
    access.allow("EDITOR", ["BLOGS:CREATE", "BLOGS:READ", "BLOGS:UPDATE"]);
    
    // With custom ABAC conditions
    access.allow("USER", "BLOG:UPDATE", (context) => {
      return context.post.authorId === context.user.id;
    });

    2. Dynamic Assignment (Database + Cache)

    Best for production apps where permissions are managed in a DB or Admin Panel. This is optional but provides powerful caching and auto-sync benefits.

    await access.assignPermissions("ADMIN", 
      // 1. Fetcher: Returns the list of valid permissions from your DB
      async () => {
        const permissions = await db.query("SELECT key FROM permissions WHERE role = 'ADMIN'");
        return permissions.map(p => p.key);
      }, 
      {
        // OPTIONAL: A fast key-check (e.g., Redis version or DB timestamp)
        // The engine only re-runs the Fetcher if this key changes.
        invalidateKey: async () => await redis.get("perms:admin:version"),
        
        // OPTIONAL: Auto-check for updates every 60 seconds in the background
        interval: 60000 
      }
    );

    [!TIP] Extra Benefits: By using invalidateKey, you avoid hitting your database for every permission check. The engine keeps permissions in an in-memory cache and only refetches when your "version" key in Redis/DB changes.

    ๐Ÿ”„ Manual Refresh & Sync

    If you don't use the interval option, or if you need to force a sync after an admin update, use the refresh method.

    // Forces the engine to check invalidateKeys and re-fetch if they changed
    await access.refresh();
    
    // Refresh only a specific role
    await access.refresh("ADMIN");

    ๐ŸŽจ Frontend Usage (React)

    Frontend components are reactive. When you call access.refresh() or when an interval triggers an update, all components using the hooks will automatically re-render.

    1. useCan Hook (Engine Bound)

    Automatically re-renders when the engine's permissions for the given role change.

    const canEdit = access.useCan("EDITOR", "BLOGS:UPDATE");

    2. usePermissions Hook (Flexible Source)

    Manage permissions from any source (Static, Async, or Role). Provides loading state and a manual refresh trigger.

    const { permissions, loading, refresh } = access.usePermissions(async () => {
      const res = await api.get("/my-permissions");
      return res.data;
    });
    
    if (loading) return <Spinner />;
    
    return (
      <div>
        <button onClick={() => refresh()}>Sync Permissions</button>
        <Can permissions={permissions} permission="BLOG:CREATE">
          <CreatePost />
        </Can>
      </div>
    );

    3. <Can /> Component

    Works with both roles (engine-bound) and explicit permission arrays.

    // Role-based (Reactive)
    <access.Can role="ADMIN" permission="USER:DELETE">
      <DeleteButton />
    </access.Can>
    
    // Permission-based
    <access.Can permissions={userPerms} permission="BLOGS:READ">
      <PostList />
    </access.Can>

    ๐Ÿง  Type Safety & Autocomplete

    Invalid permission โ†’ โŒ TypeScript error

    Invalid resource/action โ†’ โŒ TypeScript error

    IDE auto-suggests valid permissions everywhere

    // โŒ Invalid "BLOGS:PUBLISH"

    // โœ… Valid "BLOGS:CREATE"

    ๐Ÿ—๏ธ API Reference Metadata access.roles access.actions access.resources access.permissions access.permissionKeys

    Backend access.allow(role, permission) access.assignPermissions(role, perms, options) // Async: Supports fetchers & invalidation access.can(role, permission) access.resolvePermissions(role) access.normalizePermissions(raw) access.refresh(role?) // Async: Trigger key check and conditional refetch

    Frontend access.useCan(permissions, permission) <access.Can />

    ๐Ÿ” Multi-Permission Assignment Assign multiple permissions to a role at once:

    access.allow("EDITOR", ["BLOGS:CREATE", "BLOGS:READ", "BLOGS:UPDATE"]);

    ๐Ÿ† Why Use @accessx/core?

    Zero manual permission creation

    DB, backend & frontend always in sync

    Enterprise-grade RBAC foundation for apps

    Scales to ABAC, multi-role, multi-tenant systems

    ๐Ÿง  ABAC (Attribute-Based Access Control)

    You can define dynamic permissions based on context.

    Pass a validator function directly to access.can. This function is executed at runtime.

    const isAllowed = access.can("USER", "BLOG:UPDATE", () => {
        // Your logic here
        return post.authorId === user.id;
    });

    This approach allows you to keep your permissions stored as pure data (strings) in your database while keeping the complex validation logic in your application code.

    2. Stored Conditions (Deprecated โš ๏ธ)

    [!WARNING] Defining conditions during assignment is deprecated and may be removed in future versions. Please use Runtime Validation instead.

    Defining conditions during assignment:

    // DEPRECATED
    access.allow("USER", "BLOG:UPDATE", (context) => {
      return context.post.authorId === context.user.id;
    });

    Checking with context object:

    // DEPRECATED
    const canEdit = access.can("USER", "BLOG:UPDATE", { user, post });

    3. Frontend Usage

    React components support the new pattern via the validator prop (requires update to React components, currently supports context object):

    <Can permissions={myPermissions} permission="BLOG:UPDATE" context={{ user, post }}>
      <button>Edit Post</button>
    </Can>

    ๐Ÿงช Testing

    The package includes a comprehensive test suite using Jest.

    npm test

    ๐Ÿ›ฃ๏ธ Roadmap

    • Multi-role users
    • Permission groups
    • JWT permission compression
    • CLI generator

    ๐Ÿ“„ License

    MIT

    ๐Ÿ’ก Inspiration

    Zanzibar (Google)

    Auth0 / Keycloak permission models

    CASL & OPA (simplified DX)

    One definition. One truth. Everywhere. ๐Ÿ”โœจ