@asaidimu/iam v4.0.0
@asaidimu/iam
A lightweight, type-safe Identity and Access Management (IAM) system for client-side JavaScript/TypeScript applications.
ð Table of Contents
- ð Overview & Features
- ðĶ Installation & Setup
- ð Usage Documentation
- âïļ Integration with React
- ðïļ Project Architecture
- ð ïļ Development & Contributing
- âđïļ Additional Information
ð Overview & Features
@asaidimu/iam is a versatile and robust Identity and Access Management (IAM) library designed specifically for client-side JavaScript and TypeScript applications. It provides a clean abstraction layer that separates authentication from authorization, enabling you to integrate with any identity provider (e.g., Firebase, Auth0, Supabase, custom JWT) while maintaining a consistent and powerful permission system.
This library empowers developers to build applications with sophisticated access control without being coupled to a specific backend authentication service. It emphasizes type safety, performance, and flexibility, ensuring a smooth developer experience and a secure application.
Key Features
- ð Authentication Abstraction: Define custom
IdentityProviderimplementations to integrate seamlessly with any authentication backend, keeping your core application logic provider-agnostic. - ðĶ Fine-grained Access Control: Implement declarative, rule-based permissions (e.g., "can edit post," "has admin role") that are evaluated against the current identity and optional resource context.
- ð§Đ Composable Rules: Build complex access rules using logical operators (
AND,OR,NOT,XOR,NAND,NOR) to combine simpler rules, allowing for highly flexible authorization policies. - ⥠Performance Optimized: Features built-in memoization for rule evaluation, minimizing redundant computations and ensuring efficient access checks with a configurable cache TTL.
- ð Cross-tab Synchronization: Optional
DataStorePersistenceallows session state to be synchronized across multiple browser tabs or instances, providing a consistent user experience. - âąïļ Session Management: Configurable session Time-To-Live (TTL) automatically handles session expiration and renewal.
- ð Type Safety: Fully written in TypeScript, providing strong typing for identities, properties, rules, and resources, enhancing developer productivity and reducing errors.
- âïļ React Integration: Includes a dedicated module (
@asaidimu/iam/react) with React hooks (useIdentity,usePermissions,useCan,useEvaluate) and components (PermissionGate,RuleGate) for seamless integration into React applications. - ðŠķ Lightweight & Modular: Designed with minimal external dependencies, keeping the bundle size small and allowing you to use only the parts you need.
ðĶ Installation & Setup
Prerequisites
- Node.js (16.x or higher)
- Bun (recommended for development) or npm/yarn
- TypeScript (5.x or higher) for optimal usage
- Familiarity with modern JavaScript/TypeScript module systems and React (for React integration).
Installation Steps
Install @asaidimu/iam using your preferred package manager:
# Using Bun (recommended)
bun add @asaidimu/iam
# Using npm
npm install @asaidimu/iam
# Using Yarn
yarn add @asaidimu/iamConfiguration
@asaidimu/iam is configured programmatically within your application code. There are no external configuration files required for the library itself. All setup involves defining your IdentityProvider, SessionManager, and IAMRuleSet directly in your JavaScript/TypeScript files.
Verification
After installation, you can quickly verify that the package is correctly installed by trying to import a core module:
// verify.ts
import { createAuthenticator } from '@asaidimu/iam';
console.log('IAM imported successfully!');
// To run this:
// bun install
// bun run --hot verify.ts
// or `npx ts-node verify.ts` if you have ts-node installedIf no errors are reported, the package is ready for use.
ð Usage Documentation
This section walks you through the core concepts and practical examples of using @asaidimu/iam.
1. Create an Identity Provider
The IdentityProvider is the bridge between your authentication system and @asaidimu/iam. It defines how users authenticate, deauthenticate, retrieve their identity, and how changes in their authentication state are observed.
You can use createAuthenticator for custom integrations.
Using createAuthenticator (for custom auth)
This factory function allows you to define your own authentication and deauthentication logic, returning an Identity object with permissions and properties.
import { createAuthenticator, type Identity } from '@asaidimu/iam';
// Define the shape of your identity properties
interface MyIdentityProps {
id: string;
name: string;
email: string;
role: 'admin' | 'editor' | 'viewer';
}
// Example: Authenticator for a simple username/password system
const authProvider = createAuthenticator<MyIdentityProps, [string, string]>({
authenticator: async (username, password) => {
// Replace with your actual authentication logic (e.g., API call to your backend)
// This example simulates a network request.
await new Promise(resolve => setTimeout(resolve, 500)); // Simulate async
if (username === 'test@example.com' && password === 'password123') {
// Upon successful authentication, return an Identity object.
// `permissions` are string identifiers for general access checks.
// `properties` hold all specific user data.
return {
permissions: ['read:posts', 'create:posts', 'edit:own_posts', 'view:dashboard'],
properties: {
id: 'user-123',
name: 'John Doe',
email: username,
role: 'editor',
},
};
}
return null; // Return null if authentication fails
},
deauthenticator: async (userProps) => {
// Replace with your actual deauthentication logic (e.g., API call to logout)
// This example simulates a network request.
await new Promise(resolve => setTimeout(resolve, 300)); // Simulate async
console.log(`Deauthenticating user ${userProps.email}`);
return true; // Indicate successful deauthentication
},
});
// Example usage:
// const isAuthenticated = await authProvider.authenticate('test@example.com', 'password123');
// console.log('Authenticated:', isAuthenticated); // true
// const currentIdentity = authProvider.identity();
// console.log('Current identity:', currentIdentity?.properties.name); // John Doe
// await authProvider.deauthenticate();
// console.log('Identity after deauth:', authProvider.identity()); // null2. Set Up Session Management
The SessionManager handles the lifecycle of the authenticated identity, including expiration and persistence. It acts as the single source of truth for the current session state.
import { createSessionManager, type Identity, type DataStorePersistence } from '@asaidimu/iam';
// Assuming 'authProvider' is created as shown above (e.g., from './my-auth-provider.ts')
// import { authProvider } from './my-auth-provider';
interface MyIdentityProps { /* ... as defined above ... */ }
// Define a simple localStorage persistence adapter.
// This allows the session to persist across browser tabs and even browser restarts.
const localStoragePersistence: DataStorePersistence<Identity<MyIdentityProps, string>> = {
set: (id, state) => {
try {
localStorage.setItem(id, JSON.stringify(state));
return true;
} catch (e) {
console.error('Error persisting state to localStorage:', e);
return false;
}
},
get: () => {
try {
const item = localStorage.getItem('my-app-session'); // Use a consistent key
return item ? JSON.parse(item) : null;
} catch (e) {
console.error('Error retrieving state from localStorage:', e);
return null;
}
},
// The subscribe method is crucial for cross-tab synchronization.
// It listens to 'storage' events from other browser tabs.
subscribe: (id, callback) => {
const handler = (e: StorageEvent) => {
if (e.key === id) {
callback(e.newValue ? JSON.parse(e.newValue) : null);
}
};
window.addEventListener('storage', handler);
return () => window.removeEventListener('storage', handler);
},
clear: () => {
try {
localStorage.removeItem('my-app-session');
return true;
} catch (e) {
console.error('Error clearing state from localStorage:', e);
return false;
}
}
};
const sessionManager = createSessionManager<MyIdentityProps>(authProvider, {
sessionTTL: 60 * 60 * 1000, // Optional: session will expire after 1 hour (in milliseconds)
persistence: localStoragePersistence, // Optional: persist session to localStorage for cross-tab sync
instanceId: 'my-app-session' // Optional: unique ID for this session instance, defaults to 'default-session'
});
// Subscribe to session state changes (e.g., login, logout, expiration)
// This is useful for global UI updates or application-wide state changes.
const unsubscribe = sessionManager.subscribe((identityProps) => {
if (identityProps) {
console.log('User logged in or identity updated:', identityProps.email);
} else {
console.log('User logged out or session expired.');
}
});
// Remember to call unsubscribe() when the listener is no longer needed
// (e.g., on application unmount or specific component cleanup).
// unsubscribe();3. Define Access Control Rules
Rules are the heart of the access control system. They are functions that take an IAMRuleContext (containing the current identity and an optional resource) and return a boolean indicating access. Rules can be combined using logical operators (and, or, not, xor, nand, nor) for complex logic.
import { and, or, not, IAMBooleanOperator, type IAMRuleSet, type IAMRuleContext, type IAMRule, type CompositeRule } from '@asaidimu/iam';
// Assume MyIdentityProps and PostType are defined based on your application's data
interface MyIdentityProps {
id: string;
name: string;
email: string;
role: 'admin' | 'editor' | 'viewer';
permissions: string[]; // Permissions from identity provider
}
interface PostType {
id: string;
authorId: string;
status: 'draft' | 'published' | 'archived';
category: 'news' | 'blog' | 'tutorial';
isFeatured: boolean;
}
// Define your rules using an IAMRuleSet (a Map where keys are permission names)
const rules: IAMRuleSet<MyIdentityProps, PostType> = new Map();
// --- Basic Rules ---
// Rule 1: 'read:posts' - Requires any authenticated identity
rules.set('read:posts', ({ identity }) => !!identity);
// Rule 2: 'create:posts' - Requires 'editor' or 'admin' role
rules.set('create:posts', ({ identity }) =>
identity?.role === 'editor' || identity?.role === 'admin'
);
// Rule 3: 'edit:own_post' - User can edit their own post
const canEditOwnPost: IAMRule<MyIdentityProps, PostType> = ({ identity, resource }) =>
!!identity && !!resource && identity.id === resource.authorId;
rules.set('edit:own_post', canEditOwnPost);
// Rule 4: 'edit:any_post' - Admin can edit any post
const isAdmin: IAMRule<MyIdentityProps, PostType> = ({ identity }) =>
identity?.role === 'admin';
rules.set('edit:any_post', isAdmin);
// Rule 5: 'publish:post' - User can publish a post if they are an editor/admin AND the post is a draft
rules.set('publish:post',
and(
or(isAdmin, ({ identity }) => identity?.role === 'editor'), // Must be admin OR editor
({ resource }) => resource?.status === 'draft' // And the post must be a draft
)
);
// --- More Complex Composite Rules ---
// Rule 6: 'delete:private_data' - Requires admin role AND is not a public resource
interface SecureResource {
isPublic?: boolean;
}
rules.set('delete:private_data',
and(
isAdmin,
not(({ resource }: IAMRuleContext<MyIdentityProps, SecureResource>) => resource?.isPublic === true) // Resource is NOT public
)
);
// Rule 7: 'access:admin_features' - User must be admin OR an editor AND verified
rules.set('access:admin_features',
or(
isAdmin,
and(
({ identity }) => identity?.role === 'editor',
({ identity }) => (identity as MyIdentityProps)?.email.endsWith('@verified.com') // Example of a custom property check
)
)
);
// Rule 8: 'moderate:comments' - XOR operator example: Either admin OR editor, but NOT both (unlikely, but demonstrates XOR)
rules.set('moderate:comments',
{
operator: 'XOR', // Using object literal for composite rule
rules: [
isAdmin,
({ identity }) => identity?.role === 'editor'
]
}
);4. Create and Use Access Controller
The AccessController uses the SessionManager and your defined IAMRuleSet to perform access checks. It's the primary interface for authorization logic in your application.
import { createAccessController } from '@asaidimu/iam';
// Assuming 'sessionManager' and 'rules' are created as shown above
// import { sessionManager } from './session-setup';
// import { rules } from './rules-definition';
// Define the types used in your rules
interface MyIdentityProps { /* ... as defined above ... */ }
interface PostType { /* ... as defined above ... */ }
const access = createAccessController<MyIdentityProps, PostType>(sessionManager, rules, {
defaultAllow: false, // Optional: If a rule is not found, default to deny (false). Default is false.
// Set to `true` to allow access if no matching rule is defined.
cacheTTL: 10000 // Optional: Rule evaluation results cached for 10 seconds (default 5000ms)
});
// Example Usage:
// Check if user has specific permissions (requires explicit permission in identity.permissions array)
// Use 'has' when you expect the permission string to be present in the authenticated identity's
// `permissions` array, usually derived from roles or scope claims.
if (access.has('read:posts')) {
console.log('User can read posts (permission string found).');
} else {
console.log('User cannot read posts (permission string not found).');
}
// Check if user has ALL specified permissions
if (access.has(['read:posts', 'create:posts'])) {
console.log('User can read AND create posts (both permission strings found).');
}
// Check if a permission is granted based on its rule, irrespective of identity.permissions array
// Use 'is' when you want to evaluate a rule defined in your IAMRuleSet directly.
// The rule itself determines access, not just the presence of a string in `identity.permissions`.
if (access.is('create:posts')) {
console.log('User role allows creating posts (rule evaluated to true).');
}
// Check if user can perform an action on a specific resource
// 'can' combines an implicit permission check (like 'has') with a rule evaluation that can use `resource` context.
const myPost: PostType = { id: 'post-123', authorId: 'user-123', status: 'draft', category: 'blog', isFeatured: false }; // Assume user-123 is the current identity ID
const anotherUserPost: PostType = { id: 'post-456', authorId: 'user-other', status: 'published', category: 'news', isFeatured: true };
if (access.can('edit:own_post', myPost)) {
console.log('User can edit their own post.');
} else {
console.log('User cannot edit their own post (or post is not theirs).');
}
if (access.can('publish:post', myPost)) {
console.log('User can publish this draft post.');
} else {
console.log('User cannot publish this post.');
}
// Evaluate a custom rule directly (useful for ad-hoc checks or rules not mapped to a permission string)
// 'evaluate' is the most flexible, allowing you to pass any rule or composite rule directly.
const isManagerRule = ({ identity }: IAMRuleContext<MyIdentityProps>) => identity?.role === 'admin' || identity?.role === 'editor';
if (access.evaluate(isManagerRule)) {
console.log('Current user is a manager or admin.');
}
// Example with a resource-specific ad-hoc rule
const isFeaturedTutorial = ({ resource }: IAMRuleContext<any, PostType>) =>
resource?.isFeatured && resource.category === 'tutorial';
if (access.evaluate(isFeaturedTutorial, anotherUserPost)) {
console.log('The other user\'s post is a featured tutorial.');
} else {
console.log('The other user\'s post is not a featured tutorial.');
}Advanced Usage
Dynamic Rule Creation
You can create functions that generate rules, making your rule definitions more reusable and dynamic, especially useful for checks like resource ownership based on different property names.
// Create a rule factory for resource ownership checks
// T is IdentityProps, R is ResourceType
function isOwner<T, R extends { [key: string]: any }>(resourceKey: keyof R): IAMRule<T, R> {
return ({ identity, resource }) => {
if (!identity || !resource || !resource[resourceKey]) return false;
// Assuming identity.id is the user's unique identifier
return identity.id === resource[resourceKey];
};
}
// Use with various resources and their respective owner ID properties
rules.set('edit:document', isOwner<MyIdentityProps, { id: string; documentOwnerId: string }>('documentOwnerId'));
rules.set('delete:project', isOwner<MyIdentityProps, { id: string; projectOwnerId: string }>('projectOwnerId'));
// Example usage:
// const myDoc = { id: 'doc-1', documentOwnerId: 'user-123' };
// access.can('edit:document', myDoc); // true if current user is 'user-123'Role-Based Access Control (RBAC)
Implement hierarchical RBAC by defining a role hierarchy and a utility rule to check roles and their inherited permissions.
// Define a simple role hierarchy: a role grants permissions of roles below it.
const roleHierarchy: Record<MyIdentityProps['role'], MyIdentityProps['role'][]> = {
admin: ['editor', 'viewer'],
editor: ['viewer'],
viewer: [],
user: ['viewer'], // Default role
guest: [] // No special inherited roles for guest
};
// Function to check if identity has a specific role or inherits it
function hasRole<T>(requiredRole: MyIdentityProps['role']): IAMRule<T> {
return ({ identity }) => {
if (!identity || !identity.role) return false;
// Check if the current user's role directly matches the required role
if (identity.role === requiredRole) return true;
// Check if the current user's role implicitly grants the required role
const grantedRoles = roleHierarchy[identity.role];
return grantedRoles ? grantedRoles.includes(requiredRole) : false;
};
}
// Use in rules for role-based permissions
rules.set('manage:users', hasRole('admin'));
rules.set('approve:content', or(hasRole('admin'), hasRole('editor'))); // Admins or Editors can approve
rules.set('view:dashboard', hasRole('viewer')); // Even simplest roles can be enforcedCustom Rule Composition
Create custom functions for composing rules beyond and, or, not to fit specific business logic patterns.
// Create a rule that requires all specified permissions to be in the identity's permissions array
// This is a direct check against the `identity.permissions` field.
function requireAllIdentityPermissions<T>(...requiredPerms: string[]): IAMRule<T> {
return ({ identity }) => {
if (!identity || !identity.permissions) return false;
return requiredPerms.every(perm => identity.permissions.includes(perm));
};
}
// Create a rule that requires any of the specified permissions to be in the identity's permissions array
function requireAnyIdentityPermission<T>(...requiredPerms: string[]): IAMRule<T> {
return ({ identity }) => {
if (!identity || !identity.permissions) return false;
return requiredPerms.some(perm => identity.permissions.includes(perm));
};
}
// Use in rules
rules.set('full:content_management', requireAllIdentityPermissions<MyIdentityProps>('create:content', 'edit:content', 'delete:content'));
rules.set('access:reports', requireAnyIdentityPermission<MyIdentityProps>('view:analytics', 'download:reports'));âïļ Integration with React
@asaidimu/iam provides a dedicated React integration module (@asaidimu/iam/react) with hooks and components to simplify building dynamic UIs based on authentication and authorization state.
Setting up IAM with React
It's recommended to create a single file (e.g., src/iam-setup.ts or src/hooks/useIAM.ts) to initialize and export your IAM instances and hooks, making them easily accessible throughout your React application.
// src/iam-setup.ts
import {
createAuthenticator,
createSessionManager,
createAccessController,
and, or, not,
type IAMRuleSet,
type Identity,
type DataStorePersistence
} from '@asaidimu/iam';
import { createIAMHooks } from '@asaidimu/iam/react';
// --- 1. Define Identity and Resource Types ---
// These types should match the actual data your application handles.
interface AppUserIdentity {
id: string;
email: string;
role: 'admin' | 'editor' | 'user' | 'guest';
permissions: string[]; // Actual permissions/scopes from backend
}
interface AppResource {
id: string;
authorId?: string;
status?: 'draft' | 'published' | 'archived';
isPublic?: boolean;
category?: string;
}
// --- 2. Initialize Identity Provider (Choose one option based on your auth solution) ---
const authProvider = createAuthenticator<AppUserIdentity, [string, string]>({
authenticator: async (email, password) => {
const response = await fetch('/api/login', { method: 'POST', body: JSON.stringify({ email, password }) });
const data = await response.json();
if (response.ok && data.user) {
return {
permissions: data.user.permissions || [],
properties: { id: data.user.id, email: data.user.email, role: data.user.role }
};
}
return null;
},
deauthenticator: async (userProps) => {
const response = await fetch('/api/logout', { method: 'POST', body: JSON.stringify({ userId: userProps.id }) });
return response.ok;
}
});
// --- 3. Set Up Session Manager ---
// This persistence adapter uses localStorage to store the session data.
const localStoragePersistence: DataStorePersistence<Identity<AppUserIdentity, string>> = {
set: (id, state) => {
localStorage.setItem(id, JSON.stringify(state));
return true;
},
get: () => {
const item = localStorage.getItem('my-app-session-iam'); // Use a consistent, unique key
return item ? JSON.parse(item) : null;
},
subscribe: (id, callback) => {
const handler = (e: StorageEvent) => {
if (e.key === id) {
callback(e.newValue ? JSON.parse(e.newValue) : null);
}
};
window.addEventListener('storage', handler);
return () => window.removeEventListener('storage', handler);
},
clear: () => {
localStorage.removeItem('my-app-session-iam');
return true;
}
};
const sessionManager = createSessionManager<AppUserIdentity>(authProvider, {
sessionTTL: 8 * 60 * 60 * 1000, // Session active for 8 hours (in milliseconds)
persistence: localStoragePersistence, // Persist session in localStorage
instanceId: 'my-app-session-iam' // Unique ID for this session instance
});
// --- 4. Define Access Control Rules ---
const rules: IAMRuleSet<AppUserIdentity, AppResource> = new Map();
// General authentication check (identity exists)
rules.set('is:authenticated', ({ identity }) => !!identity);
// Role-based checks
rules.set('is:admin', ({ identity }) => identity?.role === 'admin');
rules.set('is:editor', ({ identity }) => identity?.role === 'editor' || identity?.role === 'admin');
rules.set('is:guest', ({ identity }) => identity?.role === 'guest' || !identity); // Guest if role is guest or unauthenticated
// Resource-based checks (example for a Post resource)
rules.set('view:post', ({ identity, resource }) => {
if (!resource) return false;
// Anyone can view published posts. Editors/Admins can view drafts. Owners can view anything.
if (resource.status === 'published' && resource.isPublic) return true;
if (!identity) return false; // Unauthenticated cannot view private posts
return identity.role === 'admin' ||
identity.role === 'editor' ||
(identity.id === resource.authorId);
});
rules.set('create:post', ({ identity }) => identity?.role === 'admin' || identity?.role === 'editor');
rules.set('edit:post', ({ identity, resource }) => {
if (!identity || !resource) return false;
// Admins can edit any post. Editors can edit published posts. Owners can edit their own.
return identity.role === 'admin' ||
(identity.role === 'editor' && resource.status === 'published') ||
(identity.id === resource.authorId);
});
rules.set('delete:post', ({ identity, resource }) => {
if (!identity || !resource) return false;
// Only admins can delete posts
return identity.role === 'admin';
});
// Composite rules example
rules.set('manage:drafts',
and(
({ identity }) => identity?.role === 'editor' || identity?.role === 'admin',
({ resource }) => resource?.status === 'draft' // Only manage drafts
)
);
// --- 5. Create and Export IAM Hooks and Components ---
// This is the primary export for your React application.
export const {
useIdentity, // Hook: Get current identity properties
usePermissions, // Hook: Check if identity has specific permissions (from identity.permissions array)
useCan, // Hook: Check if identity can perform action on resource (rule-based evaluation)
useEvaluate, // Hook: Evaluate a custom rule directly (ad-hoc checks)
PermissionGate, // Component: Conditionally render children based on `usePermissions`
RuleGate, // Component: Conditionally render children based on `useEvaluate`
withPermission, // HOC: Wrap components for conditional rendering based on `usePermissions`
access // Direct access to the access controller instance for non-hook/component use cases
} = createIAMHooks<AppUserIdentity, AppResource>(sessionManager, rules);
// Also export the session manager and auth provider for direct imperative access if needed
// (e.g., for login/logout buttons in your application).
export { sessionManager, authProvider };Using IAM Hooks in React Components
Once iam-setup.ts is configured, you can import and use the hooks and components in your React application:
// src/components/HomePage.tsx
import React from 'react';
// Import hooks and components directly from your setup file
import { useIdentity, usePermissions, useCan, PermissionGate, sessionManager, authProvider } from '../iam-setup';
interface PostDataType {
id: string;
authorId: string;
status: 'draft' | 'published';
title: string;
}
function HomePage() {
const identity = useIdentity(); // Gets { id, email, role, permissions } or null
const isLoggedIn = !!identity;
const currentUserName = identity?.email || 'Guest';
// Example post data
const myPost: PostDataType = { id: 'post-101', authorId: identity?.id || 'unknown', status: 'draft', title: 'My Draft Post' };
const othersPublishedPost: PostDataType = { id: 'post-102', authorId: 'another-user', status: 'published', title: 'A Published Post' };
// Check permissions with 'usePermissions' (checks `identity.permissions` array)
const canViewReports = usePermissions('view:reports');
// Check access to a specific resource with 'useCan' (evaluates the rule configured in iam-setup)
const canEditMyDraftPost = useCan('edit:post', myPost);
const canDeleteOthersPublishedPost = useCan('delete:post', othersPublishedPost);
const handleLogin = async () => {
// Example: Login using your auth provider
const success = await authProvider.authenticate('test@example.com', 'password123');
if (success) {
console.log('Logged in!');
// UI will automatically update due to sessionManager.subscribe
} else {
console.error('Login failed.');
}
};
const handleLogout = async () => {
await authProvider.deauthenticate();
console.log('Logged out!');
};
return (
<div>
<h1>Welcome, {currentUserName}!</h1>
{!isLoggedIn && (
<>
<p>Please log in to access full features.</p>
<button onClick={handleLogin}>Log In (Example)</button>
</>
)}
{isLoggedIn && (
<>
<button onClick={handleLogout}>Log Out</button>
<h2>Your Dashboard</h2>
{canViewReports && <p>You have access to detailed reports.</p>}
<PermissionGate permission="create:post" fallback={<p>You cannot create posts.</p>}>
<button>Create New Post</button>
</PermissionGate>
{/* Example of using useCan for a resource */}
{canEditMyDraftPost && (
<button onClick={() => console.log('Editing post:', myPost.id)}>
Edit "{myPost.title}"
</button>
)}
{canDeleteOthersPublishedPost && (
<button onClick={() => console.log('Deleting post:', othersPublishedPost.id)}>
Delete "{othersPublishedPost.title}" (Admin Only)
</button>
)}
{/* A gate that checks if the user is an admin or editor */}
<PermissionGate permission={['is:admin', 'is:editor']} fallback={null}>
<p>Admin/Editor specific content goes here.</p>
</PermissionGate>
</>
)}
</div>
);
}
export default HomePage;Admin Panel Example with RuleGate
The RuleGate component is highly flexible as it directly evaluates any IAMRule or CompositeRule you provide, allowing for very specific contextual checks.
// src/components/AdminPanel.tsx
import React from 'react';
import { PermissionGate, RuleGate, useEvaluate, useCan } from '../iam-setup'; // Import from your setup file
import { and, not, type IAMRuleContext, type IAMRule } from '@asaidimu/iam'; // Import operators for inline rules
// Assume AppUserIdentity is defined as in iam-setup.ts
interface AppUserIdentity {
id: string;
email: string;
role: 'admin' | 'editor' | 'user' | 'guest';
permissions: string[];
}
function AdminPanel() {
// Example of a custom rule that only allows admins AND if it's not the weekend
// This rule is defined directly in the component, but could also be in iam-setup.
const isAdminAndWeekday: IAMRule<AppUserIdentity, unknown> = and(
({ identity }) => identity?.role === 'admin',
() => {
const day = new Date().getDay();
return day !== 0 && day !== 6; // Not Sunday (0) or Saturday (6)
}
);
// Use the hook to evaluate this custom rule
const isAuthorizedForWeekdayOperations = useEvaluate(isAdminAndWeekday);
// Example of using useCan for a hypothetical sensitive action
const canAccessSensitiveLogs = useCan('access:sensitive_logs');
return (
// Top-level gate: only allow admins to view the admin panel at all
<PermissionGate
permission="is:admin"
fallback={<p>You are not authorized to view the admin panel.</p>}
>
<div>
<h1>Administrator Dashboard</h1>
<PermissionGate permission="create:user">
<button>Add New User</button>
</PermissionGate>
<PermissionGate permission="delete:post">
<button>Delete Any Post</button>
</PermissionGate>
{canAccessSensitiveLogs && (
<p>
<button>View Sensitive System Logs</button>
</p>
)}
{/* A RuleGate that evaluates a custom rule for a specific section */}
<RuleGate
rule={({ identity }) => identity?.role === 'admin'} // Simple rule check for specific section
fallback={<p>Some features are only available to direct administrators.</p>}
>
<h2>System Configuration</h2>
<p>Manage application settings, backups, and critical operations.</p>
</RuleGate>
{isAuthorizedForWeekdayOperations && (
<p className="text-green-600">
You have full administrative privileges and it's a weekday. Proceed with caution for critical operations!
</p>
)}
{!isAuthorizedForWeekdayOperations && (
<p className="text-red-600">
Administrative operations are restricted on weekends or if you're not an admin.
</p>
)}
</div>
</PermissionGate>
);
}
export default AdminPanel;Higher-Order Component (HOC) withPermission
The withPermission HOC is a convenient way to wrap existing components, making them only render if the user has the specified permissions.
// src/components/UserManagementPage.tsx
import React from 'react';
import { withPermission } from '../iam-setup'; // Import from your setup file
interface UserManagementProps {
// Any props this component might receive, e.g., for filtering
teamId?: string;
}
function UserManagement({ teamId }: UserManagementProps) {
return (
<div>
<h1>Manage Users {teamId ? `for Team ${teamId}` : ''}</h1>
<p>This section allows managing user accounts and roles within the application.</p>
{/* ... User management UI elements ... */}
<ul>
<li>Add new users</li>
<li>Edit user roles</li>
<li>Deactivate accounts</li>
</ul>
</div>
);
}
// Wrap the component with the HOC to enforce 'manage:users' permission.
// If the user doesn't have the permission, the FallbackComponent will be rendered instead.
export default withPermission(
'manage:users', // The permission string(s) required
UserManagement, // The component to render if authorized
// Optional: Fallback component to render if permission is denied (can be null for nothing)
() => <p>You do not have the necessary permissions to manage users.</p>
);ðïļ Project Architecture
@asaidimu/iam is designed with a clear separation of concerns, making it modular, extensible, and easy to understand.
Core Components
IdentityProvider<IdentityProps>: An interface that defines how an authentication source provides user identity. It's responsible forauthenticate,deauthenticate,identityretrieval,refresh, andonChangesubscriptions. This is the primary extension point for integrating with any authentication service (e.g., Firebase, Auth0, custom API, Supabase).SessionManager<IdentityProps>: Built on top of anIdentityProvider, it handles the overall user session. This includes managing session expiration (sessionTTL), persisting the identity across sessions (DataStorePersistence), and notifying subscribers of session changes. It provides a stable view of the current authenticated user's properties.AccessController<IdentityProps, ResourceType>: This component consumes the current identity from theSessionManagerand applies a set of predefinedIAMRules (from anIAMRuleSet) to determine access. It provides high-level methods likehas,is,can, andevaluatefor performing authorization checks.IAMRule&IAMRuleSet: The fundamental building blocks for authorization. AnIAMRuleis a function that takes anIAMRuleContext(containing identity properties and an optional resource) and returns a boolean. AnIAMRuleSetis aMapthat stores these rules, keyed by permission names.CompositeRule: A structure for combining multipleIAMRules with logical operators (AND,OR,NOT,XOR,NAND,NOR), allowing for complex and nested authorization logic.DataStorePersistence<T>: An interface allowing you to plug in any storage mechanism (e.g.,localStorage,IndexedDB, cookies) for persisting session state, enabling features like "remember me" or cross-tab synchronization.IAMError: A custom error class for consistent error handling within the IAM system.- React Integration (
react.tsx): A separate module providing higher-level abstractions (hooks and components) for React applications, simplifying the process of building permission-aware UIs.
Data Flow
- Authentication: An
IdentityProvider(e.g.,createAuthenticator) authenticates a user based on credentials or existing session and returns anIdentityobject (containingpermissionsarray andpropertiesobject). It also emitsonChangeevents when the identity state changes. - Session Management: The
SessionManagersubscribes to theIdentityProvider'sonChangeevents. When anIdentityis received, it stores it (optionally usingDataStorePersistence), sets an expiration (sessionTTL), and then notifies its own subscribers about the change to the identity'sproperties. It also monitorsDataStorePersistencefor external changes (e.g., from other tabs). - Access Control: The
AccessControllerdepends on theSessionManagerto retrieve the current identity whenever an access check is requested (has,can,evaluate). It then looks up the relevantIAMRulefrom itsIAMRuleSetand evaluates it using the providedIAMRuleContext(identity properties, optional resource). Rule evaluations are memoized for performance. - UI Rendering (React): In React applications,
createIAMHooksprovides hooks that useuseSyncExternalStoreto subscribe toSessionManagerchanges. This ensures that React components automatically re-render when the identity or permission state changes, allowing for dynamic UI adjustments (e.g., showing/hiding buttons, rendering different content).
Extension Points
- Custom
IdentityProvider: Implement theIdentityProviderinterface (or usecreateAuthenticator) to support any custom authentication flow or third-party authentication service - Custom
DataStorePersistence: Implement theDataStorePersistenceinterface to use any storage backend for session state (e.g.,sessionStorage, a custom in-memory store for testing, or integration with a global state management library). - Custom
IAMRules: Rules are simple functions, offering infinite flexibility to define complex authorization logic tailored to your application's specific business requirements. You can define rules that check identity properties, resource properties, external conditions, or combinations thereof. - Custom
CompositeRuleOperators: While the library provides standard boolean operators, theCompositeRuleinterface is extensible, allowing for more complex logical compositions if needed.
ð ïļ Development & Contributing
Contributions are welcome! Follow these guidelines to set up your development environment and contribute to @asaidimu/iam.
Development Setup
- Clone the repository:
git clone https://github.com/asaidimu/iam.git cd iam - Install dependencies:
This project uses Bun for package management and scripting, which is recommended for development.
If you prefer npm/yarn, ensure you have a compatible version of Node.js (v16+) installed, then:bun installnpm install # or yarn install
Available Scripts
The following scripts are defined in package.json and can be run using bun run <script-name> or npm run <script-name>:
bun ci: Installs dependencies, typically used in CI environments to ensure a clean install.bun clean: Removes thedistdirectory, which contains compiled output.bun prebuild: Executes before thebuildscript. It first runsbun cleanand then executes a TypeScript synchronization script (.sync-package.ts) which likely preparesdist.package.jsonfor publishing.bun build: Compiles TypeScript source files fromsrcto thedistdirectory. It generates CommonJS (CJS) and ES Module (ESM) formats, along with TypeScript declaration files (.d.ts). This is the primary command to compile the library.bun postbuild: Executes after thebuildscript. It copies essential files (README.md,LICENSE.md,dist.package.json) into thedistdirectory, ensuring they are included in the published package.
Testing
While a dedicated test script might not be explicitly listed (it might be handled by semantic-release's CI flow), a typical TypeScript project would use a testing framework. You can likely run tests using a standard command:
- To run tests:
bun test # or (if configured) npm test - Code Coverage: (If configured with a testing tool like Vitest or Jest)
bun test --coverage
Contributing Guidelines
We welcome bug reports, feature requests, and pull requests!
- Fork the repository and create your branch from
main. - Ensure existing tests pass and add new tests for any new features or bug fixes.
- Adhere to the coding style (ESLint and Prettier are configured in the project for consistent code formatting).
- Write clear, concise commit messages following the Conventional Commits specification (e.g.,
feat: add new feature,fix: resolve bug,chore: update dependencies). This project usessemantic-releasefor automated versioning and changelog generation, which relies on these commit messages. - Open a Pull Request with a descriptive title and a detailed explanation of your changes. Link to any relevant issues.
Issue Reporting
Found a bug or have a feature idea? Please open an issue on our GitHub Issues page. Provide as much detail as possible, including steps to reproduce bugs, expected vs. actual behavior, and your environment. https://github.com/asaidimu/iam/issues
âđïļ Additional Information
Troubleshooting
- Identity not updating:
- Verify your
IdentityProvider'sonChangelistener is correctly notifying its subscribers with the new identity state. - Ensure your
SessionManageris correctly subscribed to theIdentityProvider's changes. - If using persistence, check that
DataStorePersistence.setis saving the state andDataStorePersistence.subscribeis correctly listening forstorageevents across tabs.
- Verify your
- Rules not firing/incorrect results:
- Confirm your
IAMRuleContext(identity, resource) is correctly populated and passed to theAccessControllermethods (has,can,evaluate). - Check for typos in permission names (
rules.get('your:permission')). - Ensure your rule logic handles
nullorundefinedidentity/resource properties gracefully (e.g., using optional chaining?.). - If you're dynamically changing rules at runtime and observing stale results, note that
AccessControlleruses memoization. Re-creating theAccessControllerinstance would clear its internal cache.
- Confirm your
- Persistence issues:
- Double-check your
DataStorePersistenceimplementation, especiallyset,get,subscribe, andclearmethods for correct storage key usage andJSON.stringify/JSON.parsefor serialization/deserialization. - Ensure the
instanceIdpassed tocreateSessionManageris consistent across tabs if you expect cross-tab synchronization.
- Double-check your
- React component not re-rendering:
- Confirm that
useSyncExternalStoreincreateIAMHooksis correctly wired tosessionManager.subscribeand that its getter functions (the second argument) accurately reflect the state that should trigger a re-render. - Ensure that the
sessionManager.subscribecallback is indeed being fired when the underlying identity changes.
- Confirm that
Changelog & Roadmap
For a detailed history of changes, new features, and breaking changes, please refer to the CHANGELOG.md file.
Future plans for @asaidimu/iam may include:
- More built-in
IdentityProviderintegrations for popular authentication services (e.g., Firebase, Auth0 directly). - Advanced rule debugging and tracing tools for complex authorization policies.
- Further optimizations for performance, potentially with Web Workers for heavy rule sets.
- Enhanced server-side rendering (SSR) support for React hooks and data hydration.
- More comprehensive documentation and recipes for common authorization patterns.
License
This project is licensed under the MIT License. See the LICENSE.md file for the full license text.
Acknowledgments
Developed and maintained by Saidimu. Special thanks to the open-source community for the tools and inspiration that make projects like this possible, particularly the TypeScript, React, and Bun communities.