npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@ahksolution/permissions-sdk

v1.3.1

Published

gRPC client SDK for AHK Solution Permissions Microservice - provides NestJS guard, decorators, and client for inter-service permission checks

Readme

@ahksolution/permissions-sdk

gRPC client SDK for the AHK Solution Permissions Microservice. Provides NestJS integration for:

  • JWT Authentication - Validate tokens via gRPC (no JWT secret needed in consuming services)
  • Permission Checks - RBAC and ABAC support via gRPC

Installation

# From private npm registry
npm install @ahksolution/permissions-sdk

# Or using pnpm
pnpm add @ahksolution/permissions-sdk

Peer Dependencies (must be installed in your project):

pnpm add @nestjs/microservices @grpc/grpc-js @grpc/proto-loader

Quick Start

1. Register the Module

// app.module.ts
import { Module } from '@nestjs/common';
import { PermissionsClientModule } from '@ahksolution/permissions-sdk';

@Module({
  imports: [
    // Static configuration
    PermissionsClientModule.register({
      url: 'localhost:50051',
    }),
  ],
})
export class AppModule {}

With async configuration (recommended for production):

// app.module.ts
import { Module } from '@nestjs/common';
import { ConfigModule, ConfigService } from '@nestjs/config';
import { PermissionsClientModule } from '@ahksolution/permissions-sdk';

@Module({
  imports: [
    ConfigModule.forRoot(),
    PermissionsClientModule.registerAsync({
      imports: [ConfigModule],
      useFactory: (config: ConfigService) => ({
        url: config.get<string>('PERMISSIONS_SERVICE_URL', 'localhost:50051'),
      }),
      inject: [ConfigService],
    }),
  ],
})
export class AppModule {}

2. Apply Guards Globally (Recommended)

For full authentication and authorization, apply both guards globally:

// app.module.ts
import { Module } from '@nestjs/common';
import { APP_GUARD } from '@nestjs/core';
import {
  PermissionsClientModule,
  JwtAuthGuard,
  PermissionsGuard,
} from '@ahksolution/permissions-sdk';

@Module({
  imports: [PermissionsClientModule.register({ url: 'localhost:50051' })],
  providers: [
    // Order matters: authentication first, then authorization
    { provide: APP_GUARD, useClass: JwtAuthGuard },
    { provide: APP_GUARD, useClass: PermissionsGuard },
  ],
})
export class AppModule {}

3. Use Decorators in Controllers

import { Controller, Get, Post, Body } from '@nestjs/common';
import { Public, CurrentUser, RequirePermissions, JwtUserData } from '@ahksolution/permissions-sdk';

@Controller('orders')
export class OrdersController {
  // Public route - bypasses JWT authentication
  @Public()
  @Get('health')
  health() {
    return { status: 'ok' };
  }

  // Authenticated route - no specific permission required
  @Get('me')
  getProfile(@CurrentUser() user: JwtUserData) {
    return {
      id: user.id,
      email: user.email,
      roles: user.roles,
    };
  }

  // Authenticated + requires specific permission
  @Post()
  @RequirePermissions('orders:create')
  createOrder(@CurrentUser() user: JwtUserData, @Body() dto: CreateOrderDto) {
    return this.orderService.create(user.id, dto);
  }

  // Get specific user property
  @Get('my-orders')
  @RequirePermissions('orders:read')
  getMyOrders(@CurrentUser('id') userId: string) {
    return this.orderService.findByUser(userId);
  }
}

JWT Authentication

The SDK provides JWT authentication that validates tokens via gRPC call to the permissions service. No JWT secret is required in consuming services - all validation happens centrally.

How It Works

  1. JwtAuthGuard extracts the token from Authorization: Bearer <token> header
  2. Calls ValidateToken gRPC method on the permissions service
  3. Permissions service verifies the token and returns user data
  4. User data (with roles and permissions) is attached to request.user

JwtUserData Type

interface JwtUserData {
  id: string;
  email: string | null;
  phone: string | null;
  userType: string;
  status: string;
  isProfileComplete: boolean;
  roles: RoleInfo[]; // User's roles
  permissions: string[]; // User's permission codes
  hasAllAccess: boolean; // True if user has wildcard access
}

interface RoleInfo {
  id: string;
  code: string;
  name: string;
  isSystem: boolean;
}

Decorators

| Decorator | Description | | -------------------- | ------------------------------------------------ | | @Public() | Mark route as public (bypass JWT authentication) | | @CurrentUser() | Get full user object from request | | @CurrentUser('id') | Get specific property from user |

Permission Checking

Using Decorators (Recommended)

import {
  RequirePermissions,
  RequireAnyPermission,
  RequireAllPermissions,
} from '@ahksolution/permissions-sdk';

@Controller('orders')
export class OrdersController {
  // Require a single permission
  @Post()
  @RequirePermissions('orders:create')
  create() {}

  // Require ALL permissions (AND logic) - default behavior
  @Post(':id/approve')
  @RequirePermissions(['orders:read', 'orders:approve'])
  approve() {}

  // Require ANY of the permissions (OR logic)
  @Delete(':id')
  @RequirePermissions(['orders:delete', 'admin:full'], { mode: 'any' })
  delete() {}

  // Using alias decorators for clarity
  @Post(':id/export')
  @RequireAllPermissions(['orders:read', 'orders:export'])
  export() {}

  @Post(':id/cancel')
  @RequireAnyPermission(['orders:cancel', 'orders:manage'])
  cancel() {}
}

Using the Client Service

Inject PermissionsGrpcClient to check permissions programmatically:

import { Injectable } from '@nestjs/common';
import { PermissionsGrpcClient } from '@ahksolution/permissions-sdk';

@Injectable()
export class OrderService {
  constructor(private readonly permissions: PermissionsGrpcClient) {}

  async createOrder(userId: string, orderData: CreateOrderDto) {
    // Simple boolean check
    const canCreate = await this.permissions.hasPermission(userId, 'orders:create');
    if (!canCreate) {
      throw new ForbiddenException('You do not have permission to create orders');
    }

    // Continue with order creation...
  }

  async deleteOrder(userId: string, orderId: string) {
    // Check multiple permissions (user needs ANY of these)
    const canDelete = await this.permissions.hasAnyPermission(userId, [
      'orders:delete',
      'orders:manage',
      'admin:full',
    ]);

    if (!canDelete) {
      throw new ForbiddenException('You do not have permission to delete orders');
    }

    // Continue with deletion...
  }
}

API Reference

Guards

| Guard | Description | | ------------------ | ------------------------------------------------------------ | | JwtAuthGuard | Validates JWT tokens via gRPC. Attaches user to request. | | PermissionsGuard | Checks permissions based on @RequirePermissions decorator. |

PermissionsGrpcClient

| Method | Description | | --------------------------------------------------------- | ----------------------------------------------------------- | | validateToken(token) | Validates JWT and returns ValidateTokenResult | | hasPermission(userId, permissionCode) | Returns boolean - does user have this permission? | | hasAllPermissions(userId, permissionCodes) | Returns boolean - does user have ALL permissions? | | hasAnyPermission(userId, permissionCodes) | Returns boolean - does user have ANY permission? | | checkPermission(userId, permissionCode, options?) | Returns full EvaluationResult with details | | checkBulkPermissions(userId, permissionCodes, options?) | Returns results for multiple permissions | | getEffectivePermissions(userId) | Returns all permissions and roles for a user | | getUserInfo(userId) | Returns complete user profile with roles & permissions | | getUserRoles(userId) | Returns user's roles only | | getUserPermissions(userId) | Returns user's permission codes only | | getBulkUserInfo(userIds, options?) | Returns user data for multiple users with optional flags | | createUser(options) | Creates a new user with validations and optional magic link |

Decorators

| Decorator | Description | | -------------------------------------------- | -------------------------------------------- | | @Public() | Mark route as public (bypass JWT auth) | | @CurrentUser() | Get authenticated user from request | | @CurrentUser('property') | Get specific property from user | | @RequirePermissions(permissions, options?) | Require permission(s) with configurable mode | | @RequireAllPermissions(permissions) | Shorthand for mode: 'all' | | @RequireAnyPermission(permissions) | Shorthand for mode: 'any' |

Options

interface RequirePermissionsOptions {
  // 'all' = user must have ALL permissions (default)
  // 'any' = user must have at least ONE permission
  mode?: 'all' | 'any';

  // Custom error message when denied
  errorMessage?: string;

  // Include request params as resource context for ABAC
  includeResourceContext?: boolean;
}

Advanced Usage

Full Evaluation Result

Get detailed information about permission decisions:

const result = await this.permissions.checkPermission(userId, 'orders:create');

console.log(result);
// {
//   allowed: true,
//   source: 'rbac',           // 'rbac' | 'abac' | 'break-glass' | 'denied'
//   matchedRoles: ['ADMIN'],
//   matchedPolicies: [],
//   reason: 'Permission granted via role(s): ADMIN',
//   evaluationTimeMs: 3
// }

ABAC Context

Pass resource and request context for attribute-based access control:

const result = await this.permissions.checkPermission(userId, 'documents:read', {
  resource: {
    id: 'doc-123',
    type: 'document',
    ownerId: 'user-456',
    department: 'engineering',
  },
  request: {
    ip: '192.168.1.100',
    method: 'GET',
    path: '/api/documents/doc-123',
  },
});

Get All User Permissions

const effective = await this.permissions.getEffectivePermissions(userId);

console.log(effective);
// {
//   permissions: ['users:read', 'users:create', 'orders:read'],
//   roles: [
//     { id: '...', code: 'USER', name: 'User', isSystem: false },
//     { id: '...', code: 'ORDER_VIEWER', name: 'Order Viewer', isSystem: false }
//   ],
//   version: 1,
//   computedAt: Date
// }

User Data Methods (v1.2.0+)

Fetch user data directly by userId without token validation. Useful for service-to-service calls.

import { Injectable, NotFoundException } from '@nestjs/common';
import { PermissionsGrpcClient } from '@ahksolution/permissions-sdk';

@Injectable()
export class UserProfileService {
  constructor(private readonly permissions: PermissionsGrpcClient) {}

  // Get complete user profile with roles and permissions
  async getUserProfile(userId: string) {
    const result = await this.permissions.getUserInfo(userId);
    if (!result.found) {
      throw new NotFoundException(result.errorMessage); // 'USER_NOT_FOUND' | 'USER_INACTIVE'
    }
    return result.user;
    // {
    //   id: '...',
    //   email: '[email protected]',
    //   phone: '+1234567890',
    //   userType: 'CUSTOMER',
    //   status: 'ACTIVE',
    //   isProfileComplete: true,
    //   roles: [{ id, code, name, isSystem }],
    //   permissions: ['orders:read', 'orders:create'],
    //   hasAllAccess: false
    // }
  }

  // Get only user's roles
  async getUserRoleNames(userId: string) {
    const result = await this.permissions.getUserRoles(userId);
    if (!result.found) {
      throw new NotFoundException(result.errorMessage);
    }
    return result.roles.map((r) => r.name); // ['Admin', 'Manager']
  }

  // Get only user's permissions
  async canAccessFeature(userId: string, featurePermission: string) {
    const result = await this.permissions.getUserPermissions(userId);
    if (!result.found) return false;

    // Check if user has all access or the specific permission
    return result.hasAllAccess || result.permissions.includes(featurePermission);
  }
}

Bulk User Data (v1.4.0+)

Fetch data for multiple users in a single gRPC call with optional flags to control what data is returned.

import { Injectable, NotFoundException } from '@nestjs/common';
import { PermissionsGrpcClient } from '@ahksolution/permissions-sdk';

@Injectable()
export class TeamService {
  constructor(private readonly permissions: PermissionsGrpcClient) {}

  // Get basic info for multiple users (no roles/permissions)
  async getTeamMembers(userIds: string[]) {
    const result = await this.permissions.getBulkUserInfo(userIds);

    return Object.entries(result.users)
      .filter(([_, item]) => item.found)
      .map(([userId, item]) => ({
        id: item.user!.id,
        email: item.user!.email,
        name: item.user!.profile?.fullName,
      }));
  }

  // Get users with their roles included
  async getTeamWithRoles(userIds: string[]) {
    const result = await this.permissions.getBulkUserInfo(userIds, {
      includeRoles: true,
    });

    return Object.entries(result.users)
      .filter(([_, item]) => item.found)
      .map(([userId, item]) => ({
        id: item.user!.id,
        email: item.user!.email,
        roles: item.user!.roles.map((r) => r.name),
      }));
  }

  // Get users with both roles and permissions
  async getTeamWithFullAccess(userIds: string[]) {
    const result = await this.permissions.getBulkUserInfo(userIds, {
      includeRoles: true,
      includePermissions: true,
    });

    return Object.entries(result.users).map(([userId, item]) => {
      if (!item.found) {
        return { userId, error: item.errorMessage };
      }
      return {
        userId,
        email: item.user!.email,
        roles: item.user!.roles,
        permissions: item.user!.permissions,
        hasAllAccess: item.user!.hasAllAccess,
      };
    });
  }

  // Handle mixed results (some users found, some not)
  async validateUserIds(userIds: string[]) {
    const result = await this.permissions.getBulkUserInfo(userIds);

    const found: string[] = [];
    const notFound: string[] = [];
    const inactive: string[] = [];

    for (const [userId, item] of Object.entries(result.users)) {
      if (item.found) {
        found.push(userId);
      } else if (item.errorMessage === 'USER_INACTIVE') {
        inactive.push(userId);
      } else {
        notFound.push(userId);
      }
    }

    return { found, notFound, inactive };
  }
}

Options:

| Option | Type | Default | Description | | -------------------- | --------- | ------- | ------------------------------------ | | includeRoles | boolean | false | Include user roles in response | | includePermissions | boolean | false | Include user permissions in response |

Return Types:

interface GetBulkUserInfoResult {
  users: Record<string, BulkUserInfoItem>;
}

interface BulkUserInfoItem {
  found: boolean;
  errorMessage?: string; // 'USER_NOT_FOUND' | 'USER_INACTIVE' | 'INTERNAL_ERROR'
  user?: BulkUserData; // Only populated if found=true
}

interface BulkUserData {
  id: string;
  email: string | null;
  phone: string | null;
  userType: string;
  status: string;
  isProfileComplete: boolean;
  roles: RoleInfo[]; // Empty array if includeRoles=false
  permissions: string[]; // Empty array if includePermissions=false
  hasAllAccess: boolean;
  profile?: UserProfileData;
}

interface GetUserInfoResult {
  found: boolean;
  errorMessage?: string; // 'USER_NOT_FOUND' | 'USER_INACTIVE' | 'INTERNAL_ERROR'
  user?: JwtUserData; // Only populated if found=true
}

interface GetUserRolesResult {
  found: boolean;
  errorMessage?: string;
  roles: RoleInfo[];
}

interface GetUserPermissionsResult {
  found: boolean;
  errorMessage?: string;
  permissions: string[];
  hasAllAccess: boolean;
}

Create User (v1.5.0+)

Create new users via gRPC with full validation support including email uniqueness, magic link sending, role assignment, and organization assignment.

import { Injectable, BadRequestException, ConflictException } from '@nestjs/common';
import { PermissionsGrpcClient, UserType } from '@ahksolution/permissions-sdk';

@Injectable()
export class UserManagementService {
  constructor(private readonly permissions: PermissionsGrpcClient) {}

  // Create an admin user with magic link
  async createAdminUser(email: string, firstName: string, lastName: string) {
    const result = await this.permissions.createUser({
      firstName,
      lastName,
      email,
      userType: UserType.ADMIN,
      sendMagicLink: true, // Default: true
    });

    if (!result.success) {
      // Handle specific error codes
      if (result.errorCode === 'EMAIL_EXISTS') {
        throw new ConflictException('A user with this email already exists');
      }
      throw new BadRequestException(result.errorMessage);
    }

    return {
      userId: result.user!.id,
      email: result.user!.email,
      magicLinkSent: result.magicLinkSent,
    };
  }

  // Create a customer user with roles and organization
  async createCustomerWithRoles(data: {
    firstName: string;
    lastName: string;
    email?: string;
    phone?: string;
    departmentId?: string;
    designationId?: string;
    roleIds?: string[];
  }) {
    const result = await this.permissions.createUser({
      firstName: data.firstName,
      lastName: data.lastName,
      email: data.email,
      phone: data.phone,
      userType: UserType.CUSTOMER,
      departmentId: data.departmentId,
      designationId: data.designationId,
      roleIds: data.roleIds,
      sendMagicLink: false, // Don't send magic link for customers
    });

    if (!result.success) {
      this.handleCreateUserError(result.errorCode, result.errorMessage);
    }

    return result.user;
  }

  private handleCreateUserError(errorCode?: string, errorMessage?: string): never {
    switch (errorCode) {
      case 'EMAIL_EXISTS':
        throw new ConflictException('Email already registered');
      case 'PHONE_EXISTS':
        throw new ConflictException('Phone number already registered');
      case 'INVALID_ROLE':
        throw new BadRequestException('One or more role IDs are invalid');
      case 'INVALID_ORG':
        throw new BadRequestException('Invalid department or designation ID');
      case 'VALIDATION_ERROR':
        throw new BadRequestException(errorMessage ?? 'Validation failed');
      default:
        throw new BadRequestException(errorMessage ?? 'Failed to create user');
    }
  }
}

Options:

interface CreateUserOptions {
  firstName: string; // Required
  lastName: string; // Required
  email?: string; // Required if phone not provided
  phone?: string; // Required if email not provided
  userType: UserType; // Required: UserType.ADMIN or UserType.CUSTOMER
  departmentId?: string; // Optional: Department for org assignment
  designationId?: string; // Optional: Designation for org assignment
  roleIds?: string[]; // Optional: Role IDs to assign to user
  sendMagicLink?: boolean; // Optional: Send magic link email (default: true)
}

enum UserType {
  ADMIN = 'ADMIN',
  CUSTOMER = 'CUSTOMER',
}

Return Types:

interface CreateUserResult {
  success: boolean;
  errorCode?: string; // 'EMAIL_EXISTS' | 'PHONE_EXISTS' | 'INVALID_ROLE' | 'INVALID_ORG' | 'VALIDATION_ERROR' | 'INTERNAL_ERROR'
  errorMessage?: string;
  user?: CreatedUserData; // Only populated if success=true
  magicLinkSent: boolean;
}

interface CreatedUserData {
  id: string;
  email: string | null;
  phone: string | null;
  userType: string;
  status: string;
  profile?: UserProfileData;
}

Error Codes:

| Error Code | Description | | ------------------ | -------------------------------------- | | EMAIL_EXISTS | A user with this email already exists | | PHONE_EXISTS | A user with this phone number exists | | INVALID_ROLE | One or more role IDs are invalid | | INVALID_ORG | Invalid department or designation ID | | VALIDATION_ERROR | General validation error (see message) | | INTERNAL_ERROR | Server-side error during user creation |

Environment Variables

| Variable | Description | Default | | ------------------------- | ------------------- | ----------------- | | PERMISSIONS_SERVICE_URL | gRPC server address | localhost:50051 |

Error Handling

The guard throws ForbiddenException when permission is denied:

// Default error
throw new ForbiddenException('Access denied. Required permission(s): orders:create');

// Custom error message
@RequirePermissions('orders:create', {
  errorMessage: 'You need order creation privileges'
})

Complete Example

// app.module.ts
import { Module } from '@nestjs/common';
import { APP_GUARD } from '@nestjs/core';
import { ConfigModule, ConfigService } from '@nestjs/config';
import {
  PermissionsClientModule,
  JwtAuthGuard,
  PermissionsGuard,
} from '@ahksolution/permissions-sdk';

@Module({
  imports: [
    ConfigModule.forRoot(),
    PermissionsClientModule.registerAsync({
      imports: [ConfigModule],
      useFactory: (config: ConfigService) => ({
        url: config.get('PERMISSIONS_SERVICE_URL', 'localhost:50051'),
      }),
      inject: [ConfigService],
    }),
  ],
  providers: [
    { provide: APP_GUARD, useClass: JwtAuthGuard },
    { provide: APP_GUARD, useClass: PermissionsGuard },
  ],
})
export class AppModule {}

// users.controller.ts
import { Controller, Get, Post, Body, Param } from '@nestjs/common';
import {
  Public,
  CurrentUser,
  RequirePermissions,
  RequireAnyPermission,
  JwtUserData,
} from '@ahksolution/permissions-sdk';

@Controller('users')
export class UsersController {
  // Public endpoint - no auth required
  @Public()
  @Get('health')
  health() {
    return { status: 'ok' };
  }

  // Auth required, no specific permission
  @Get('profile')
  getProfile(@CurrentUser() user: JwtUserData) {
    return user;
  }

  // Auth + specific permission required
  @Get()
  @RequirePermissions('users:list')
  findAll() {
    return this.userService.findAll();
  }

  @Post()
  @RequirePermissions('users:create')
  create(@Body() dto: CreateUserDto, @CurrentUser('id') createdBy: string) {
    return this.userService.create(dto, createdBy);
  }

  @Get(':id')
  @RequireAnyPermission(['users:read', 'users:manage'])
  findOne(@Param('id') id: string) {
    return this.userService.findOne(id);
  }
}

Important Notes

  1. No JWT Secret Required: The SDK validates tokens via gRPC call to the permissions service. You don't need to configure JWT secrets in consuming services.

  2. Guard Order Matters: When using both guards globally, JwtAuthGuard must run before PermissionsGuard (authentication before authorization).

  3. Permissions Service Required: This SDK is a client for the AHK Solution Permissions Microservice. It will not function without a running instance exposing a gRPC endpoint.

  4. Same-Pod Deployment: For optimal performance, deploy consuming services in the same pod/network as the permissions service to minimize gRPC latency.

License

MIT