JSPM

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

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

Package Exports

  • @ahksolution/permissions-sdk
  • @ahksolution/permissions-sdk/dist/index.js

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

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 {}

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

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

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
// }

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