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

@asaidimu/iam

v6.0.0

Published

A lightweight, type-safe Identity and Access Management (IAM) system for client-side JavaScript/TypeScript applications.

Downloads

44

Readme

@asaidimu/iam - Identity & Access Management Toolkit

npm version License Build Status

A comprehensive, extensible library for building robust identity and access management (IAM) into JavaScript/TypeScript applications, featuring advanced session management and first-class React integration.


🚀 Quick Links


✨ Overview & Features

@asaidimu/iam provides a powerful and flexible foundation for managing user identities and controlling access to resources within your application. Designed with modern web applications in mind, it separates concerns into distinct, extensible modules for identity, access control, and seamless React integration. This library empowers developers to implement complex authorization logic, manage secure user sessions, and reactively update UI based on permissions, all while maintaining a high degree of type safety and performance.

This library offers the tools to define granular permissions, enforce security policies, and deliver a robust user experience. Its modular design allows you to use only the parts you need, while its React hooks and components dramatically simplify integrating IAM into your frontend.

Key Features

  • Flexible Identity Management: IdentityProvider abstraction for integrating with any authentication backend (OAuth, JWT, custom API).
  • Robust Session Management: Built-in session TTL, automatic refresh, and pluggable persistence (e.g., localStorage, IndexedDB).
  • Advanced Security Features: Optional idle timeout, warning notifications, window blur/minimize lock, and activity tracking for high-security environments.
  • Rule-Based Access Control: Define granular permissions using simple functions or complex CompositeRule structures with logical AND/OR/NOT/XOR/NAND/NOR operators.
  • High-Performance Rule Evaluation: Memoized rule evaluator with configurable cache TTL for efficient permission checks.
  • Comprehensive React Integration: Dedicated IAMProvider, hooks (useIdentity, usePermissions, useCan, useEvaluate), Higher-Order Components (withPermission), and conditional rendering components (PermissionGate, RuleGate).
  • Type-Safe API: Fully written in TypeScript, providing excellent developer experience and compile-time safety.
  • Extensible Design: Easily swap out persistence layers, integrate with different identity sources, and define custom rule logic.

📦 Installation & Setup

This library is available as an npm package.

Prerequisites

  • Node.js (LTS version recommended)
  • npm or yarn

Installation Steps

Install the package using your preferred package manager:

npm install @asaidimu/iam
# OR
yarn add @asaidimu/iam

The package includes TypeScript definitions by default.

Configuration

There are no global configuration files. All configurations are passed directly to the createAuthenticator and createAccessController functions, or via the IAMProvider component in React.

Verification

You can verify the installation by attempting to import a module in your project:

// For TypeScript projects
import { createAuthenticator, createAccessController } from '@asaidimu/iam';

// For JavaScript projects
const { createAuthenticator, createAccessController } = require('@asaidimu/iam');

console.log('IAM Toolkit imported successfully!');

📖 Usage Documentation

Core Concepts

At the heart of @asaidimu/iam are a few key abstractions:

  • Identity: Represents an authenticated user, containing permissions (array of strings) and properties (generic object for user-specific data).
  • IdentityProvider: Manages the authentication state, session lifecycle, and provides the current Identity. It handles login, logout, refresh, persistence, and notifies listeners of state changes.
  • IAMRule: A function that takes an IAMRuleContext (containing identity properties and a resource) and returns a boolean indicating access.
  • CompositeRule: Allows combining multiple IAMRules or other CompositeRules using logical operators (AND, OR, NOT, etc.).
  • IAMRuleSet: A Map that associates permission names (strings) with their corresponding IAMRule or CompositeRule.
  • AccessController: Evaluates IAMRules from an IAMRuleSet against the current Identity (from an IdentityProvider) and a given resource.
  • DataStorePersistence: An interface for plugging in various storage mechanisms to persist the identity and session state (e.g., localStorage, cookies, IndexedDB).

Custom Persistence Adapter

Before setting up your IdentityProvider, you might want to create a persistence adapter. This allows the IAM system to save and load the session state across browser sessions or tabs.

Here's an example using localStorage:

import { DataStorePersistence, Identity } from '@asaidimu/iam';

// Define the shape of your identity properties
interface UserProps {
  id: string;
  name: string;
  roles: string[];
}

// PersistedIdentity type is internal but good to know for context
interface PersistedIdentity<T> extends Identity<T, string> {
  sessionExpires?: number;
  lastActivity?: number;
}

const localStoragePersistence: DataStorePersistence<PersistedIdentity<UserProps>> = {
  set(id: string, state: PersistedIdentity<UserProps>): boolean {
    try {
      localStorage.setItem(`iam_identity_${id}`, JSON.stringify(state));
      return true;
    } catch (error) {
      console.error('Error saving identity to localStorage:', error);
      return false;
    }
  },
  get(): PersistedIdentity<UserProps> | null {
    try {
      const stored = localStorage.getItem(`iam_identity_identity`); // Default instanceId is 'identity'
      return stored ? JSON.parse(stored) : null;
    } catch (error) {
      console.error('Error loading identity from localStorage:', error);
      return null;
    }
  },
  subscribe(id: string, callback: (state: PersistedIdentity<UserProps>) => void): () => void {
    const handler = (event: StorageEvent) => {
      if (event.key === `iam_identity_${id}`) {
        const newState = event.newValue ? JSON.parse(event.newValue) : null;
        callback(newState);
      }
    };
    window.addEventListener('storage', handler);
    return () => window.removeEventListener('storage', handler);
  },
  clear(): boolean {
    try {
      localStorage.removeItem(`iam_identity_identity`);
      return true;
    } catch (error) {
      console.error('Error clearing identity from localStorage:', error);
      return false;
    }
  },
};

Setting up an Identity Provider

The createAuthenticator function is your primary way to manage user sessions. It takes handler functions for authentication, deauthentication, and optional session/security configurations.

import { createAuthenticator, type IdentityProvider } from '@asaidimu/iam';
// Assuming localStoragePersistence is defined as above
import { localStoragePersistence } from './persistence';

interface UserProps {
  id: string;
  name: string;
  email: string;
  roles: string[];
}

// Authentication parameters can be anything, here we use username and password
type AuthParams = [string, string];

const myIdentityProvider: IdentityProvider<UserProps> = createAuthenticator<UserProps, AuthParams>({
  onAuthenticate: async (username, password) => {
    // Simulate an API call to your backend
    console.log(`Attempting to authenticate ${username}...`);
    await new Promise(resolve => setTimeout(resolve, 500)); // Simulate network delay

    if (username === 'admin' && password === 'adminpass') {
      return {
        permissions: ['dashboard:view', 'users:manage', 'posts:create', 'posts:edit', 'posts:publish'],
        properties: { id: 'user-123', name: 'Admin User', email: '[email protected]', roles: ['admin', 'editor'] },
      };
    } else if (username === 'editor' && password === 'editorpass') {
        return {
          permissions: ['dashboard:view', 'posts:create', 'posts:edit'],
          properties: { id: 'user-456', name: 'Editor User', email: '[email protected]', roles: ['editor'] },
        };
    }
    return null; // Authentication failed
  },
  onDeauthenticate: async (identityProps) => {
    // Simulate API call to log out on the backend, clear cookies/tokens
    console.log(`Deauthenticating user: ${identityProps.name}`);
    await new Promise(resolve => setTimeout(resolve, 300));
    return true;
  },
  onRefresh: async (currentIdentity) => {
      // Simulate refreshing the session (e.g., using a refresh token)
      console.log(`Refreshing session for ${currentIdentity.properties.name}...`);
      await new Promise(resolve => setTimeout(resolve, 200));
      // In a real app, you'd make an API call here.
      // If successful, return updated identity, otherwise return null to trigger re-auth.
      return { ...currentIdentity, permissions: [...currentIdentity.permissions, 'refreshed:permission'] };
  },
  // Optional: Add persistence to save session across browser sessions/tabs
  persistence: localStoragePersistence,
  session: {
    ttl: 60 * 60 * 1000, // Session expires in 1 hour
    refreshInterval: 10 * 60 * 1000, // Attempt refresh 10 minutes before TTL
  },
  security: {
    enabled: true,
    maxIdleTime: 10 * 60 * 1000, // Auto-logout after 10 minutes of inactivity
    warningTime: 30 * 1000, // Show warning 30 seconds before logout
    lockOnWindowBlur: true, // Log out immediately if window loses focus
    onIdleWarning: (timeRemaining) => console.warn(`Idle warning! Session expiring in ${timeRemaining / 1000}s.`),
    onIdleTimeout: () => console.error('Session expired due to inactivity!'),
    onSessionLocked: () => console.warn('Session locked due to window blur.'),
    onSecurityEvent: (event, details) => console.log(`Security event: ${event}`, details),
  },
});

// Example usage of IdentityProvider
async function runAuthDemo() {
  await myIdentityProvider.authenticate('admin', 'adminpass');
  console.log('Current identity after login:', myIdentityProvider.identity()?.properties.name);

  // You can subscribe to changes
  const unsubscribe = myIdentityProvider.onChange(identityProps => {
    console.log('Identity changed:', identityProps ? identityProps.name : 'No user');
  });

  // Wait for a bit, simulate activity
  await new Promise(resolve => setTimeout(resolve, 2000));
  myIdentityProvider.resetIdleTimer?.(); // Reset idle timer if security is enabled

  await myIdentityProvider.deauthenticate();
  console.log('Current identity after logout:', myIdentityProvider.identity());

  unsubscribe(); // Don't forget to unsubscribe
  myIdentityProvider.destroy(); // Clean up listeners and timers when no longer needed
}

// runAuthDemo(); // Uncomment to run this demo in a non-React context

Defining Access Rules

Rules are the core of your authorization logic. They can be simple functions or complex CompositeRule objects.

import { IAMRuleSet, CompositeRule, type IAMRuleContext } from '@asaidimu/iam';

interface UserProps {
  id: string;
  name: string;
  email: string;
  roles: string[];
}

interface ResourceProps {
  id: string;
  ownerId: string;
  status: 'draft' | 'published';
  visibility: 'public' | 'private';
}

const myAccessRules: IAMRuleSet<UserProps, ResourceProps> = new Map();

// Simple rule: Only admin users can view the dashboard
myAccessRules.set('dashboard:view', ({ identity }) => {
  return !!identity && identity.roles.includes('admin');
});

// Simple rule: Only admins or editors can create posts
myAccessRules.set('posts:create', ({ identity }) => {
  return !!identity && (identity.roles.includes('admin') || identity.roles.includes('editor'));
});

// Rule with resource context: User can edit their own posts
myAccessRules.set('posts:edit', ({ identity, resource }) => {
  if (!identity || !resource) return false;
  return identity.id === resource.ownerId || identity.roles.includes('admin');
});

// Composite Rule: Admin can publish any post, OR editor can publish their own draft post
myAccessRules.set('posts:publish', {
  operator: 'OR',
  rules: [
    // Condition 1: User is admin
    ({ identity }) => !!identity && identity.roles.includes('admin'),
    // Condition 2: User is editor AND owns the resource AND resource is a draft
    {
      operator: 'AND',
      rules: [
        ({ identity }) => !!identity && identity.roles.includes('editor'),
        ({ identity, resource }) => !!identity && !!resource && identity.id === resource.ownerId,
        ({ resource }) => !!resource && resource.status === 'draft',
      ],
    },
  ],
});

// Another composite rule: Users can view private resources if they are admin OR owner
myAccessRules.set('resource:view-private', {
    operator: 'OR',
    rules: [
        ({ identity }) => !!identity && identity.roles.includes('admin'),
        ({ identity, resource }) => !!identity && !!resource && identity.id === resource.ownerId,
    ]
});

Using the Access Controller (Standalone)

You can use the access controller directly without React if you're building a backend, a CLI, or a vanilla JavaScript application.

import { createAccessController } from '@asaidimu/iam';
// Assuming myIdentityProvider and myAccessRules are defined as above
import { myIdentityProvider } from './identitySetup';
import { myAccessRules } from './accessRules';

// Create an access controller instance
const accessController = createAccessController(myIdentityProvider, myAccessRules, {
  defaultAllow: false, // If no rule is found for a permission, deny access by default
  cacheTTL: 5000, // Cache rule evaluation results for 5 seconds
});

// Example resource
const samplePost = { id: 'post-abc', ownerId: 'user-456', status: 'draft', visibility: 'private' };
const anotherPost = { id: 'post-xyz', ownerId: 'user-789', status: 'published', visibility: 'public' };

async function runAccessDemo() {
  // Simulate login
  await myIdentityProvider.authenticate('editor', 'editorpass');
  const currentUser = myIdentityProvider.identity();
  console.log(`\n--- Access checks for ${currentUser?.properties.name} ---`);

  // 1. `has(permission)`: Checks if the identity's `permissions` array *contains* the permission.
  //    Useful for simple permission checks where the rule logic is implicitly handled by the identity's granted permissions.
  //    If a rule exists, it's evaluated, but only if the identity already `has` the permission in its list.
  console.log('Can view dashboard (has):', accessController.has('dashboard:view')); // True (editor has this)
  console.log('Can manage users (has):', accessController.has('users:manage')); // False (editor does NOT have this)

  // 2. `is(permission)`: Checks if the *rule* for the permission evaluates to true.
  //    Ignores the `identity.permissions` array. Useful if permissions are purely rule-based.
  console.log('Can view dashboard (is):', accessController.is('dashboard:view')); // True (rule says editors can)
  console.log('Can manage users (is):', accessController.is('users:manage')); // False (rule says only admins)

  // 3. `can(permission, resource)`: Checks if the identity's `permissions` array *contains* the permission AND
  //    if the rule for the permission evaluates to true, potentially with resource context.
  //    This is often the most common method for resource-based authorization.
  console.log('Can create posts (can):', accessController.can('posts:create')); // True (editor has permission + rule allows)
  console.log('Can edit own post (can):', accessController.can('posts:edit', samplePost)); // True (editor owns samplePost)
  console.log('Can edit other\'s post (can):', accessController.can('posts:edit', anotherPost)); // False (editor doesn't own anotherPost)
  console.log('Can publish own draft post (can):', accessController.can('posts:publish', samplePost)); // True (editor owns draft samplePost)
  console.log('Can publish published post (can):', accessController.can('posts:publish', { ...samplePost, status: 'published' })); // False (rule requires draft)

  // 4. `evaluate(rule, resource)`: Directly evaluates a custom rule or CompositeRule, bypassing the permission map.
  //    Useful for one-off, dynamic rule evaluations not tied to a predefined permission name.
  const customRule = ({ identity, resource }: IAMRuleContext<UserProps, ResourceProps>) =>
    !!identity && identity.email.endsWith('@example.com') && resource?.visibility === 'public';
  console.log('Can view public resource with custom rule:', accessController.evaluate(customRule, anotherPost)); // True
  console.log('Can view private resource with custom rule:', accessController.evaluate(customRule, samplePost)); // False

  await myIdentityProvider.deauthenticate();
}

// runAccessDemo(); // Uncomment to run this demo

React Integration

The React module (src/react.tsx) provides an IAMProvider component and a suite of hooks and components for seamless integration into your React application.

// App.tsx
import React, { useState, useEffect } from 'react';
import {
  IAMProvider,
  useIdentity,
  usePermissions,
  useCan,
  useEvaluate,
  PermissionGate,
  RuleGate,
  useAccessController,
  useIdentityProvider,
  useStableSetup,
} from '@asaidimu/iam';

// Assume myIdentityProvider and myAccessRules are imported from their setup files
import { myIdentityProvider } from './identitySetup';
import { myAccessRules } from './accessRules';

// Define the shape of your identity and resource properties
interface UserProps {
  id: string;
  name: string;
  email: string;
  roles: string[];
}

interface ResourceProps {
  id: string;
  ownerId: string;
  status: 'draft' | 'published';
  visibility: 'public' | 'private';
}

function App() {
  // Use useStableSetup to ensure the setup function passed to IAMProvider is stable
  // This prevents unnecessary re-initialization of the provider/controller.
  const stableIAMSetup = useStableSetup(async () => {
    // In a real app, you might fetch initial identity/rules from an API here
    await new Promise(resolve => setTimeout(resolve, 100)); // Simulate async setup
    return {
      provider: myIdentityProvider,
      rules: myAccessRules,
    };
  });

  const customErrorComponent = (error: Error, retry: () => void) => (
    <div style={{ color: 'red' }}>
      <h2>Failed to load IAM</h2>
      <p>{error.message}</p>
      <button onClick={retry}>Try Again</button>
    </div>
  );

  return (
    <IAMProvider
      setup={stableIAMSetup}
      loadingComponent={<div style={{ padding: '20px', textAlign: 'center' }}>Loading identity and access control...</div>}
      errorComponent={customErrorComponent}
      onRetry={() => console.log('Retrying IAM setup...')}
    >
      <div style={{ fontFamily: 'Arial, sans-serif', maxWidth: '800px', margin: '20px auto', padding: '20px', border: '1px solid #eee', borderRadius: '8px', boxShadow: '0 2px 4px rgba(0,0,0,0.1)' }}>
        <h1>IAM Demo Application</h1>
        <AuthenticationSection />
        <DashboardContent />
      </div>
    </IAMProvider>
  );
}

function AuthenticationSection() {
  const user = useIdentity<UserProps>();
  const { authenticate, deauthenticate } = useIdentityProvider();
  const [username, setUsername] = useState('');
  const [password, setPassword] = useState('');
  const [authError, setAuthError] = useState<string | null>(null);

  const handleLogin = async () => {
    setAuthError(null);
    try {
      const success = await authenticate(username, password);
      if (!success) {
        setAuthError('Invalid credentials');
      }
    } catch (e: any) {
      setAuthError(e.message || 'Login failed');
    }
  };

  if (user) {
    return (
      <div style={{ marginBottom: '20px', padding: '15px', border: '1px solid #ccc', borderRadius: '4px', backgroundColor: '#e8f5e9' }}>
        <p>Logged in as: <strong>{user.name}</strong> ({user.roles.join(', ')})</p>
        <button onClick={() => deauthenticate()}>Logout</button>
      </div>
    );
  }

  return (
    <div style={{ marginBottom: '20px', padding: '15px', border: '1px solid #ccc', borderRadius: '4px', backgroundColor: '#ffe0b2' }}>
      <h2>Login</h2>
      <input
        type="text"
        placeholder="Username (e.g., admin, editor)"
        value={username}
        onChange={(e) => setUsername(e.target.value)}
        style={{ marginRight: '10px', padding: '8px' }}
      />
      <input
        type="password"
        placeholder="Password (e.g., adminpass, editorpass)"
        value={password}
        onChange={(e) => setPassword(e.target.value)}
        style={{ marginRight: '10px', padding: '8px' }}
      />
      <button onClick={handleLogin}>Login</button>
      {authError && <p style={{ color: 'red', marginTop: '10px' }}>{authError}</p>}
    </div>
  );
}

function DashboardContent() {
  const user = useIdentity<UserProps>(); // Get current user properties
  const canViewDashboard = usePermissions('dashboard:view'); // Check simple permission
  const canManageUsers = usePermissions('users:manage'); // Check another simple permission
  const canCreatePosts = useCan('posts:create'); // Check permission (no resource needed for this rule)

  // Example resource to check against
  const myPost: ResourceProps = { id: 'post-1', ownerId: user?.id || 'unknown', status: 'draft', visibility: 'private' };
  const othersPost: ResourceProps = { id: 'post-2', ownerId: 'some-other-id', status: 'published', visibility: 'public' };

  const canEditMyPost = useCan('posts:edit', myPost); // Check permission on a specific resource
  const canPublishMyDraftPost = useCan('posts:publish', myPost);
  const canPublishOthersPost = useCan('posts:publish', othersPost);
  const canViewOthersPrivatePost = useEvaluate<UserProps, ResourceProps>(
    ({ identity, resource }) => {
      // Custom inline rule: can view private if admin or owner
      if (!identity || !resource) return false;
      return resource.visibility === 'private' && (identity.roles.includes('admin') || identity.id === resource.ownerId);
    },
    othersPost
  );

  const { resetIdleTimer } = useIdentityProvider();
  useEffect(() => {
      if (user && resetIdleTimer) {
          // Simulate user activity to reset the idle timer
          const interval = setInterval(() => {
              console.log('User active, resetting idle timer...');
              resetIdleTimer();
          }, 60 * 1000); // Reset every minute
          return () => clearInterval(interval);
      }
  }, [user, resetIdleTimer]);


  return (
    <div style={{ borderTop: '1px solid #eee', paddingTop: '20px', marginTop: '20px' }}>
      <h2>Application Content</h2>

      <p>Current User ID: {user?.id || 'N/A'}</p>
      <p>Can View Dashboard: <strong>{canViewDashboard ? 'Yes' : 'No'}</strong></p>
      <p>Can Manage Users: <strong>{canManageUsers ? 'Yes' : 'No'}</strong></p>
      <p>Can Create Posts: <strong>{canCreatePosts ? 'Yes' : 'No'}</strong></p>
      <p>Can Edit My Post (ID: {myPost.id}): <strong>{canEditMyPost ? 'Yes' : 'No'}</strong></p>
      <p>Can Publish My Draft Post (ID: {myPost.id}): <strong>{canPublishMyDraftPost ? 'Yes' : 'No'}</strong></p>
      <p>Can Publish Others Post (ID: {othersPost.id}): <strong>{canPublishOthersPost ? 'Yes' : 'No'}</strong></p>
      <p>Can View Others Private Post (Custom Rule): <strong>{canViewOthersPrivatePost ? 'Yes' : 'No'}</strong></p>

      <h3>Conditional Rendering Examples</h3>

      <PermissionGate permission="dashboard:view" fallback={<p style={{ color: '#d32f2f' }}>Access Denied: You need 'dashboard:view' permission to see the admin section.</p>}>
        <div style={{ border: '1px dashed #4caf50', padding: '10px', marginTop: '10px', backgroundColor: '#e8f5e9' }}>
          <h4>Admin Dashboard Section</h4>
          <p>This content is only visible to users with the 'dashboard:view' permission.</p>
          {canManageUsers && <button>Manage Users</button>}
          {!canManageUsers && <p style={{ opacity: 0.6 }}>You cannot manage users.</p>}
        </div>
      </PermissionGate>

      <RuleGate rule={myAccessRules.get('posts:publish')!} resource={myPost} fallback={<p style={{ color: '#d32f2f' }}>Access Denied: You cannot publish this specific post.</p>}>
        <div style={{ border: '1px dashed #2196f3', padding: '10px', marginTop: '10px', backgroundColor: '#e3f2fd' }}>
          <h4>Publish Post Section</h4>
          <p>This content is visible if the `posts:publish` rule passes for your specific post ({myPost.id}).</p>
          <button>Publish My Draft Post</button>
        </div>
      </RuleGate>

      <RuleGate
        rule={{
          operator: 'AND',
          rules: [
            ({ identity }) => !!identity && identity.roles.includes('editor'),
            ({ resource }) => !!resource && resource.status === 'draft',
          ],
        }}
        resource={myPost}
        fallback={<p style={{ color: '#d32f2f' }}>Access Denied: You need to be an editor and the post must be a draft to see this rule-based content.</p>}
      >
        <div style={{ border: '1px dashed #ff9800', padding: '10px', marginTop: '10px', backgroundColor: '#fff3e0' }}>
          <h4>Editor Draft Tools</h4>
          <p>This content is visible only if you are an editor AND the current post (`myPost`) is a draft.</p>
          <button>Edit Draft</button>
        </div>
      </RuleGate>

    </div>
  );
}

export default App;

🏗️ Project Architecture

@asaidimu/iam is designed with a clear separation of concerns, making it modular, testable, and extensible.

  • Identity Module (identity.ts):

    • Purpose: Manages user authentication state, session lifecycle, and optional security features.
    • Components: createAuthenticator (the main factory function), IdentityProvider interface, SecurityConfig.
    • Functionality: Handles login (onAuthenticate), logout (onDeauthenticate), session refreshing (onRefresh), identity verification (onVerify), change notifications (onChange), persistence integration, and security features like idle timeouts, window blur locking, and activity tracking.
  • Access Module (access.ts):

    • Purpose: Provides the core logic for evaluating access rules against the current identity and resources.
    • Components: createAccessController (factory function), AccessController type, createMemoizedEvaluator.
    • Functionality: Offers methods like has, is, can, and evaluate for checking permissions. It utilizes memoization for performance and supports both simple and composite rule structures.
  • React Integration Module (react.tsx):

    • Purpose: Offers a user-friendly way to integrate IAM into React applications.
    • Components: IAMProvider (context provider), useIdentity, usePermissions, useCan, useEvaluate, useAccessController, useIdentityProvider (hooks), PermissionGate, RuleGate (components), withPermission (HOC).
    • Functionality: Provides a declarative way to wrap your application with IAM context, and reactive hooks/components to consume identity and access control data. Includes utilities for stable setup functions and robust loading/error handling.
  • Types & Interfaces (types.ts):

    • Purpose: Defines the foundational data structures and contracts used across all modules.
    • Components: Identity, IAMRuleContext, IAMRule, IAMRuleSet, IAMBooleanOperator, CompositeRule, DataStorePersistence.
    • Functionality: Ensures type safety, clarity, and consistency throughout the library, making it easier to understand, use, and extend.
  • Error Handling (error.ts):

    • Purpose: Provides a standardized custom error class for IAM-specific failures.
    • Components: IAMError.
    • Functionality: Allows for structured error reporting with specific codes and optional context, aiding in debugging and user feedback.

Data Flow: The IAMProvider (or createAuthenticator directly) establishes an IdentityProvider which manages the current user's Identity. This IdentityProvider is then passed to createAccessController along with a IAMRuleSet to create an AccessController. In React applications, the IAMContext makes both the IdentityProvider and AccessController available to child components via hooks like useIdentity and usePermissions, which react to changes in the identity provider's state.

Extension Points:

  • Custom IdentityProvider Logic: Override onAuthenticate, onDeauthenticate, onRefresh, onVerify, onAuthChange callbacks in createAuthenticator.
  • Custom DataStorePersistence: Implement the DataStorePersistence interface to integrate with any storage mechanism (e.g., cookies, server-side sessions, custom browser storage).
  • Custom IAMRules: Define any arbitrary function logic for your authorization rules, including asynchronous checks if needed (though IAMRule is synchronous by default; for async, you'd typically pre-fetch data or use another mechanism).
  • Security Event Handlers: Hook into onIdleWarning, onIdleTimeout, onSessionLocked, onWindowBlur, onSecurityEvent for custom notifications or actions in response to security events.

🛠️ Development & Contributing

Contributions are welcome! If you'd like to contribute, please follow these guidelines.

Development Setup

  1. Clone the repository:
    git clone https://github.com/asaidimu/iam.git # Replace with actual repo URL
    cd iam
  2. Install dependencies:
    npm install
    # OR
    yarn install
  3. Build the project:
    npm run build

Scripts

  • npm run build: Compiles the TypeScript source code into JavaScript.
  • npm test: Runs the test suite (e.g., Jest).
  • npm run lint: Lints the codebase (e.g., ESLint).
  • npm run format: Formats the codebase (e.g., Prettier).

Testing

Tests are crucial for maintaining the quality and stability of the library.

  • To run all tests: npm test
  • Ensure high test coverage for any new features or bug fixes.

Contributing Guidelines

  1. Fork the repository.
  2. Create a new branch for your feature or bug fix: git checkout -b feature/your-feature-name or git checkout -b bugfix/issue-description.
  3. Make your changes, ensuring they adhere to the project's coding standards (linting and formatting checks).
  4. Write tests for your changes.
  5. Ensure all tests pass: npm test.
  6. Commit your changes with clear and descriptive commit messages.
  7. Push your branch to your forked repository.
  8. Open a Pull Request against the main branch of the original repository.
    • Provide a clear description of your changes.
    • Reference any related issues.

Issue Reporting

If you find a bug or have a feature request, please open an issue on the GitHub Issues page.


📚 Additional Information

Troubleshooting

  • useIAMContext must be used within an IAMProvider:

    • Cause: You are attempting to use one of the IAM React hooks (e.g., useIdentity, usePermissions) outside of an IAMProvider component's tree.
    • Solution: Ensure that the component where you're using the hook is a child of IAMProvider. Typically, you'd wrap your entire application or the relevant part with <IAMProvider />.
  • Permissions or Identity not updating in React components:

    • Cause: The IdentityProvider's onChange mechanism might not be properly implemented or the component isn't reacting correctly. Ensure your createAuthenticator setup correctly calls notifyListeners() when the identity state changes.
    • Solution: Verify your IdentityProvider's onChange callback is correctly subscribed and publishing updates. Check if your onAuthenticate, onDeauthenticate, onRefresh functions are correctly calling updateIdentity.
  • createAuthenticator options not working as expected (e.g., persistence not saving):

    • Cause: Incorrect implementation of the DataStorePersistence adapter or issues with the chosen storage mechanism.
    • Solution: Double-check your DataStorePersistence implementation (especially set and get methods). Ensure instanceId is consistent if you have multiple IAM instances. For localStorage, ensure browser storage isn't blocked or full.
  • Rules are not evaluated or always return defaultAllow:

    • Cause: The permission string passed to accessController.has(), is(), or can() does not match any key in your IAMRuleSet.
    • Solution: Ensure the permission string precisely matches a key in the Map you passed to createAccessController.

FAQ

  • Why do I need useStableSetup for IAMProvider? The IAMProvider takes a setup function that returns a Promise. If this setup function is recreated on every render (which happens if it's defined directly inside your functional component), React might continuously re-run the setup process. useStableSetup memoizes this function, ensuring it remains the same across renders unless its dependencies explicitly change, preventing unnecessary re-initializations of your IdentityProvider and AccessController.

  • Can I implement my own IdentityProvider from scratch? Yes! The IdentityProvider is an interface. While createAuthenticator provides a robust, pre-built solution with many features, you can implement the IdentityProvider interface yourself if you have highly custom needs that createAuthenticator cannot accommodate.

  • How do has, is, and can differ?

    • has(permission): Checks if the identity's permissions array includes the given permission string. If a rule exists for this permission, it will also be evaluated, but has implies an explicit permission granted to the identity.
    • is(permission): Evaluates the rule associated with the given permission string, completely ignoring the identity.permissions array. This is for purely rule-based authorization.
    • can(permission, resource): This is the most common for resource-based checks. It acts like has (requires the permission in identity.permissions) and evaluates the associated rule, providing the resource context to the rule.
  • How can I handle Server-Side Rendering (SSR) with @asaidimu/iam? The React hooks utilize useSyncExternalStore which supports SSR by providing a getServerSnapshot function. By default, useIdentity, usePermissions, etc., will return null (or false) on the server, as a real authenticated identity typically doesn't exist in that context unless explicitly hydrated. You would need to hydrate the IdentityProvider state on the server (e.g., from request headers/cookies) before passing it to IAMProvider or createAccessController.

Changelog

  • See CHANGELOG.md for a history of changes. (Placeholder - create this file in your project)

License

This project is licensed under the MIT License - see the LICENSE file for details. (Placeholder - create this file in your project)

Acknowledgments

  • Inspired by robust authentication and authorization patterns found in various frameworks and libraries.
  • Built with TypeScript and React.