@arkveil/nest
v0.2.1
Published
NestJS SDK for Arkveil — declarative ABAC permission checks via decorators and guards.
Maintainers
Readme
@arkveil/nest
Installation
npm install @arkveil/nest
# or
yarn add @arkveil/nest
# or
pnpm add @arkveil/nestFeatures
- 🔒 Declarative Permission Checks - Use decorators to protect your endpoints
- 🌐 Global Module - Configure once, use everywhere
- 🔄 Async Configuration - Support for async configuration with dependency injection
- 📡 Multi-Protocol Support - Works with HTTP, GraphQL, and WebSocket contexts
- 🎯 Type-Safe - Full TypeScript support with type definitions
Quick Start
1. Configure the Module
Option A: Synchronous Configuration
import { Module } from "@nestjs/common";
import { ArkveilModule } from "@arkveil/nest";
@Module({
imports: [
ArkveilModule.forRoot({
serviceUrl: "https://api.arkveil.com",
apiKey: "your-api-key",
getUserAttributes: (req) => ({
id: req.user?.id,
email: req.user?.email,
role: req.user?.role,
}),
getContextAttributes: (req) => ({
ip: req.ip,
userAgent: req.headers["user-agent"],
}),
}),
],
})
export class AppModule {}Option B: Async Configuration
import { Module } from "@nestjs/common";
import { ConfigModule, ConfigService } from "@nestjs/config";
import { ArkveilModule } from "@arkveil/nest";
@Module({
imports: [
ConfigModule.forRoot(),
ArkveilModule.forRootAsync({
imports: [ConfigModule],
useFactory: (configService: ConfigService) => ({
serviceUrl: configService.get("ARKVEIL_SERVICE_URL"),
apiKey: configService.get("ARKVEIL_API_KEY"),
getUserAttributes: (req) => ({
id: req.user?.id,
email: req.user?.email,
role: req.user?.role,
}),
}),
inject: [ConfigService],
}),
],
})
export class AppModule {}2. Protect Your Endpoints
Use the @PermissionPoint decorator to protect your endpoints:
import { Controller, Get, Post, Delete } from "@nestjs/common";
import { PermissionPoint } from "@arkveil/nest";
@Controller("articles")
export class ArticlesController {
@Get()
@PermissionPoint("content-service.article-read")
getAllArticles() {
return "List of articles";
}
@Post()
@PermissionPoint("content-service.article-create")
createArticle() {
return "Article created";
}
@Delete(":id")
@PermissionPoint("content-service.article-delete")
deleteArticle() {
return "Article deleted";
}
@Get("/admin")
@PermissionPoint("content-service.admin-access")
adminAction() {
return "Admin content";
}
}Typed Codes & Attributes
Get autocomplete and compile-time checking for the code passed to
@PermissionPoint (and for user/context attributes). Generate the file with
the Arkveil CLI (arkveil generate typescript -o src/arkveil.generated.ts) and
register it once via declaration merging:
// arkveil.generated.ts — generated by `arkveil generate typescript`
export type ArkveilCodes =
"content-service.article-read" | "content-service.article-delete";
declare module "arkveil" {
interface ArkveilCodeRegistry {
codes: ArkveilCodes;
}
// ...also augments ArkveilUserRegistry / ArkveilContextRegistry
}That's all — @PermissionPoint is now typed everywhere:
@PermissionPoint("content-service.article-delete") // ✅ autocompletes
@PermissionPoint("nope") // ❌ compile errorIf you'd rather not augment globally, build a typed decorator from an explicit union instead:
import { createPermissionPoint } from "@arkveil/nest";
import type { ArkveilCodes } from "./arkveil.generated";
// Re-export this and use it in place of the built-in PermissionPoint.
export const PermissionPoint = createPermissionPoint<ArkveilCodes>();Configuration Options
ArkveilModuleOptions
| Option | Type | Required | Description |
| ---------------------- | ---------- | -------- | -------------------------------------------- |
| serviceUrl | string | Yes | The URL of your Arkveil service |
| apiKey | string | Yes | Your Arkveil API key |
| version | string | No | API version (default: "v1") |
| timeout | number | No | Request timeout in milliseconds |
| retryAttempts | number | No | Number of retry attempts for failed requests |
| logger | Logger | No | Custom logger instance |
| getUserAttributes | Function | No | Extract user attributes from request |
| getContextAttributes | Function | No | Extract context attributes from request |
| onDenied | Function | No | Custom handler for denied access |
Advanced Usage
Custom User Attribute Extraction
ArkveilModule.forRoot({
serviceUrl: "https://api.arkveil.com",
apiKey: "your-api-key",
getUserAttributes: (req) => ({
// Custom logic to extract user attributes
id: req.headers["x-user-id"] || req.user?.id,
role: req.user?.role,
}),
});Adding Context Attributes
ArkveilModule.forRoot({
serviceUrl: "https://api.arkveil.com",
apiKey: "your-api-key",
getContextAttributes: (req) => ({
ip: req.ip,
userAgent: req.headers["user-agent"],
timestamp: new Date().toISOString(),
organizationId: req.user?.organizationId,
}),
});Custom Denied Handler
ArkveilModule.forRoot({
serviceUrl: "https://api.arkveil.com",
apiKey: "your-api-key",
onDenied: (req, res) => {
// Custom logic when access is denied
res.status(403).json({
error: "Access Denied",
message: "You do not have the required permissions",
requestId: req.id,
});
},
});GraphQL Support
The @PermissionPoint decorator works seamlessly with GraphQL resolvers:
import { Resolver, Query, Mutation } from "@nestjs/graphql";
import { PermissionPoint } from "@arkveil/nest";
@Resolver()
export class ArticleResolver {
@Query(() => [Article])
@PermissionPoint("content-service.article-read")
articles() {
return this.articleService.findAll();
}
@Mutation(() => Article)
@PermissionPoint("content-service.article-create")
createArticle(@Args("input") input: CreateArticleInput) {
return this.articleService.create(input);
}
}Using the Guard Directly
If you need more control, you can use the guard directly:
import { Controller, Get, UseGuards } from "@nestjs/common";
import { PermissionPointGuard } from "@arkveil/nest";
@Controller("articles")
@UseGuards(PermissionPointGuard)
export class ArticlesController {
@Get()
getAllArticles() {
return "List of articles";
}
}Error Handling
The SDK throws standard NestJS exceptions:
ForbiddenException- When the permission point is missing, the check is denied, or the check fails (fail-closed)
You can handle these using NestJS exception filters:
import {
ExceptionFilter,
Catch,
ArgumentsHost,
ForbiddenException,
} from "@nestjs/common";
@Catch(ForbiddenException)
export class ForbiddenExceptionFilter implements ExceptionFilter {
catch(exception: ForbiddenException, host: ArgumentsHost) {
const ctx = host.switchToHttp();
const response = ctx.getResponse();
response.status(403).json({
statusCode: 403,
message: "Access Denied",
timestamp: new Date().toISOString(),
});
}
}How It Works
- The
@PermissionPointdecorator marks an endpoint with a permission action ID - When a request comes in, the
PermissionPointGuardintercepts it - The guard extracts user information from the request
- It sends a permission check request to the Arkveil service
- If permission is granted, the request proceeds; otherwise, a
ForbiddenExceptionis thrown
Request Flow
Request → @PermissionPoint Decorator → PermissionPointGuard → Arkveil Service → Permission Check → Endpoint HandlerRow-level data protection
The module provides the core Arkveil client, so you can inject it and use
the data-protection methods — buildReadCondition (a SQL condition to AND
into your SELECTs) and buildWriteChecks (a boolean statement to run inside a
mutation's transaction):
import { Injectable } from "@nestjs/common";
import { Arkveil } from "arkveil";
@Injectable()
export class PaymentsService {
constructor(private readonly arkveil: Arkveil) {}
async listPayments(user: UserAttributes) {
const { readCondition } = await this.arkveil.buildReadCondition({
datasetCode: "billing.public.payments",
user,
context: {},
alias: "p",
});
return this.db.query(`SELECT * FROM payments p WHERE ${readCondition}`);
}
}See the arkveil core README for the
full contract, including when the write check must run relative to
CREATE/UPDATE/DELETE, the {{ids}} template helper, and the fail-closed
semantics.
Best Practices
- Always configure
getUserAttributes- This is how user identity and attributes reach the permission check - Use meaningful action IDs - Follow a consistent naming pattern (e.g.,
service.resource.action) - Add context attributes - Include relevant information like IP, organization, etc.
- Handle exceptions gracefully - Use exception filters for better error handling
- Test permissions - Write unit tests for your permission logic
Troubleshooting
User attributes are empty in the permission check
Make sure your authentication middleware/guard runs before the Arkveil guard so
that req.user is populated, and that getUserAttributes reads from it:
ArkveilModule.forRoot({
// ...
getUserAttributes: (req) => ({ id: req.user?.id }),
});"Permission check failed"
Check that:
- Your Arkveil service URL is correct
- Your API key is valid
- The action ID exists in your Arkveil configuration
GraphQL context issues
Make sure your GraphQL module is configured to pass the request:
GraphQLModule.forRoot({
context: ({ req }) => ({ req }),
});License
MIT
