Bantai
BANTAI

Rules

The building blocks that make decisions

Rules

Rules are the building blocks that make decisions. They evaluate input and return allow(), deny(), or skip() with optional metadata.

Basic Rule

A simple rule checks a condition and returns a result:

import { defineRule, allow, deny } from '@bantai-dev/core';

const adminRule = defineRule(
  appContext,
  'check-admin',
  async (input) => {
    if (input.role === 'admin') {
      return allow({ reason: 'User is admin' });
    }
    return deny({ reason: 'Admin access required' });
  }
);

Rules with Hooks

Rules can have hooks that execute on allow or deny:

const adminRule = defineRule(
  appContext,
  'check-admin',
  async (input) => {
    if (input.role === 'admin') {
      return allow({ reason: 'User is admin' });
    }
    return deny({ reason: 'Admin access required' });
  },
  {
    onAllow: async (input, { tools }) => {
      console.log(`Admin access granted to ${input.userId}`);
    },
    onDeny: async (input, { tools }) => {
      console.log(`Admin access denied to ${input.userId}`);
    },
  }
);

Hooks are useful for:

  • Logging access attempts
  • Updating counters
  • Sending notifications
  • Performing side effects

Async Rules

Rules can be asynchronous and perform async operations:

const asyncRule = defineRule(
  appContext,
  'check-database',
  async (input, { tools }) => {
    const user = await tools.database.getUser(input.userId);
    if (user?.active) {
      return allow({ reason: 'User is active' });
    }
    return deny({ reason: 'User is not active' });
  }
);

Using Tools in Rules

Rules can access tools from the context:

const rule = defineRule(
  contextWithStorage,
  'check-cache',
  async (input, { tools }) => {
    const cached = await tools.storage.get(`user:${input.userId}`);
    if (cached) {
      return allow({ reason: 'Found in cache' });
    }
    return deny({ reason: 'Not in cache' });
  }
);

Rule Result

Rules must return a RuleResult using allow(), deny(), or skip():

// Allow with optional reason and metadata
return allow({ reason: 'User is valid' });
return allow(); // Reason defaults to null
return allow({
  reason: 'User is valid',
  meta: { userId: '123', role: 'admin' }
});

// Deny with optional reason and metadata
return deny({ reason: 'User is invalid' });
return deny(); // Reason defaults to null
return deny({
  reason: 'User is invalid',
  meta: { errorCode: 'INVALID_USER', attempts: 3 }
});

// Skip with optional reason and metadata
return skip({ reason: 'Rule not applicable' });
return skip(); // Reason defaults to 'skipped'
return skip({
  reason: 'Rule not applicable',
  meta: { condition: 'country_not_provided' }
});

The reason is useful for:

  • Debugging
  • User feedback
  • Logging
  • Understanding why a rule passed or failed

The meta field is useful for:

  • Storing additional context about the decision
  • Passing structured data to audit logs
  • Including error codes or diagnostic information
  • Tracking rule-specific metadata

Best Practices

  1. Keep Rules Focused: Each rule should check one specific thing
  2. Provide Clear Reasons: Help with debugging and user feedback
  3. Use Tools for Side Effects: Keep rule logic pure, use hooks for side effects
  4. Handle Errors Gracefully: Return deny with a reason if something goes wrong

On this page