Skip to main content

utilities

Low-level matching, resolution, and condition functions exported from @gentleduck/iam for custom evaluation pipelines, testing, and routing guards.

Overview

The engine handles most use cases, but sometimes you need the underlying primitives. duck-iam exports a set of low-level functions for pattern matching, field resolution, and condition evaluation. These are the same functions the engine uses internally.

Typical reasons to reach for them:

  • Custom routing guards that match action/resource patterns without a full engine call.
  • Unit tests that assert matching logic in isolation.
  • Server middleware that resolves request attributes before evaluation.
  • Hand-rolled evaluation pipelines where you compose the pieces yourself.

All imports come from the root package:

import {
  matchesAction,
  matchesResource,
  matchesResourceHierarchical,
  matchesScope,
  resolve,
  evaluateOperator,
  resolveConditionValue,
} from '@gentleduck/iam'
import {
  matchesAction,
  matchesResource,
  matchesResourceHierarchical,
  matchesScope,
  resolve,
  evaluateOperator,
  resolveConditionValue,
} from '@gentleduck/iam'

Pattern Matchers

Four functions handle the pattern matching that rules use to decide whether they apply to a given action, resource, or scope.

matchesAction

Matches an action string against a pattern. '*' matches everything. Patterns ending in :* match any action that shares the prefix.

import { matchesAction } from '@gentleduck/iam'
 
matchesAction('*', 'delete')           // true  -- wildcard
matchesAction('read', 'read')          // true  -- exact
matchesAction('read', 'write')         // false
matchesAction('posts:*', 'posts:read') // true  -- prefix
matchesAction('posts:*', 'users:read') // false
import { matchesAction } from '@gentleduck/iam'
 
matchesAction('*', 'delete')           // true  -- wildcard
matchesAction('read', 'read')          // true  -- exact
matchesAction('read', 'write')         // false
matchesAction('posts:*', 'posts:read') // true  -- prefix
matchesAction('posts:*', 'users:read') // false

Signature:

function matchesAction(pattern: string, action: string): boolean
function matchesAction(pattern: string, action: string): boolean

matchesResource

Matches a resource type against a pattern. Same wildcard rules as matchesAction, plus hierarchical prefix matching: 'org' matches 'org:project:doc'.

import { matchesResource } from '@gentleduck/iam'
 
matchesResource('*', 'post')                // true
matchesResource('post', 'post')             // true
matchesResource('post', 'comment')          // false
matchesResource('org:*', 'org:project')     // true  -- prefix wildcard
matchesResource('org', 'org:project:doc')   // true  -- parent matches children
import { matchesResource } from '@gentleduck/iam'
 
matchesResource('*', 'post')                // true
matchesResource('post', 'post')             // true
matchesResource('post', 'comment')          // false
matchesResource('org:*', 'org:project')     // true  -- prefix wildcard
matchesResource('org', 'org:project:doc')   // true  -- parent matches children

Signature:

function matchesResource(pattern: string, resourceType: string): boolean
function matchesResource(pattern: string, resourceType: string): boolean

matchesResourceHierarchical

Dot-notation variant of resource matching. 'dashboard' matches 'dashboard.users.settings'. A '.*' suffix matches any child but not the parent itself.

import { matchesResourceHierarchical } from '@gentleduck/iam'
 
matchesResourceHierarchical('*', 'anything')                       // true
matchesResourceHierarchical('dashboard', 'dashboard')              // true
matchesResourceHierarchical('dashboard', 'dashboard.users')        // true
matchesResourceHierarchical('dashboard.*', 'dashboard.users')      // true
matchesResourceHierarchical('dashboard.*', 'dashboard')            // false -- parent excluded
import { matchesResourceHierarchical } from '@gentleduck/iam'
 
matchesResourceHierarchical('*', 'anything')                       // true
matchesResourceHierarchical('dashboard', 'dashboard')              // true
matchesResourceHierarchical('dashboard', 'dashboard.users')        // true
matchesResourceHierarchical('dashboard.*', 'dashboard.users')      // true
matchesResourceHierarchical('dashboard.*', 'dashboard')            // false -- parent excluded

Signature:

function matchesResourceHierarchical(pattern: string, resourceType: string): boolean
function matchesResourceHierarchical(pattern: string, resourceType: string): boolean

matchesScope

Scope matching for multi-tenant checks. A null/undefined or '*' pattern matches any scope (global permission). If the request has no scope, only global patterns match. Otherwise exact match.

import { matchesScope } from '@gentleduck/iam'
 
matchesScope(null, null)          // true  -- both global
matchesScope(undefined, 'org-1')  // true  -- global pattern matches any scope
matchesScope('*', 'org-1')        // true  -- wildcard
matchesScope('org-1', 'org-1')    // true  -- exact
matchesScope('org-1', 'org-2')    // false
matchesScope('org-1', null)       // false -- scoped pattern, no request scope
import { matchesScope } from '@gentleduck/iam'
 
matchesScope(null, null)          // true  -- both global
matchesScope(undefined, 'org-1')  // true  -- global pattern matches any scope
matchesScope('*', 'org-1')        // true  -- wildcard
matchesScope('org-1', 'org-1')    // true  -- exact
matchesScope('org-1', 'org-2')    // false
matchesScope('org-1', null)       // false -- scoped pattern, no request scope

Signature:

function matchesScope(
  pattern: string | undefined | null,
  scope: string | undefined | null,
): boolean
function matchesScope(
  pattern: string | undefined | null,
  scope: string | undefined | null,
): boolean

Field Resolution

resolve

Resolves a dot-path string against an AccessRequest. The engine uses this to extract field values when evaluating conditions (e.g., 'resource.attributes.ownerId').

Supported root paths: subject.*, resource.*, environment.*. Two shorthands exist: 'action' returns the request action, 'scope' returns the request scope (or null). Returns null for invalid paths. Blocks __proto__, constructor, and prototype traversal.

import { resolve } from '@gentleduck/iam'
 
const request = {
  subject: { id: 'user-1', roles: ['editor'], attributes: { department: 'eng' } },
  action: 'update',
  resource: { type: 'post', id: 'post-5', attributes: { ownerId: 'user-1' } },
  environment: { ip: '10.0.0.1' },
}
 
resolve(request, 'subject.id')                     // 'user-1'
resolve(request, 'subject.attributes.department')  // 'eng'
resolve(request, 'resource.attributes.ownerId')    // 'user-1'
resolve(request, 'environment.ip')                 // '10.0.0.1'
resolve(request, 'action')                         // 'update'
resolve(request, 'scope')                          // null
resolve(request, 'invalid.path')                   // null
import { resolve } from '@gentleduck/iam'
 
const request = {
  subject: { id: 'user-1', roles: ['editor'], attributes: { department: 'eng' } },
  action: 'update',
  resource: { type: 'post', id: 'post-5', attributes: { ownerId: 'user-1' } },
  environment: { ip: '10.0.0.1' },
}
 
resolve(request, 'subject.id')                     // 'user-1'
resolve(request, 'subject.attributes.department')  // 'eng'
resolve(request, 'resource.attributes.ownerId')    // 'user-1'
resolve(request, 'environment.ip')                 // '10.0.0.1'
resolve(request, 'action')                         // 'update'
resolve(request, 'scope')                          // null
resolve(request, 'invalid.path')                   // null

Signature:

function resolve(request: AccessRequest, path: string): AttributeValue
function resolve(request: AccessRequest, path: string): AttributeValue

AttributeValue is string | number | boolean | null | string[] | number[].

Condition Utilities

Two functions from the condition evaluator are exported for use in custom pipelines and testing.

evaluateOperator

Evaluates a single condition operator against two values. This is the function the engine calls for each leaf condition. Supports all built-in operators:

eq, neq, gt, gte, lt, lte, in, nin, contains, not_contains, starts_with, ends_with, matches, exists, not_exists, subset_of, superset_of.

import { evaluateOperator } from '@gentleduck/iam'
 
evaluateOperator('eq', 'admin', 'admin')            // true
evaluateOperator('neq', 'viewer', 'admin')           // true
evaluateOperator('gt', 10, 5)                        // true
evaluateOperator('in', 'editor', ['admin', 'editor']) // true
evaluateOperator('contains', ['a', 'b', 'c'], 'b')  // true
evaluateOperator('starts_with', 'hello world', 'hello') // true
evaluateOperator('matches', 'user-123', '^user-\\d+$')  // true
evaluateOperator('exists', 'anything', null)         // true
evaluateOperator('not_exists', null, null)           // true
import { evaluateOperator } from '@gentleduck/iam'
 
evaluateOperator('eq', 'admin', 'admin')            // true
evaluateOperator('neq', 'viewer', 'admin')           // true
evaluateOperator('gt', 10, 5)                        // true
evaluateOperator('in', 'editor', ['admin', 'editor']) // true
evaluateOperator('contains', ['a', 'b', 'c'], 'b')  // true
evaluateOperator('starts_with', 'hello world', 'hello') // true
evaluateOperator('matches', 'user-123', '^user-\\d+$')  // true
evaluateOperator('exists', 'anything', null)         // true
evaluateOperator('not_exists', null, null)           // true

Signature:

function evaluateOperator(
  op: Operator,
  fieldValue: AttributeValue,
  condValue: AttributeValue,
): boolean
function evaluateOperator(
  op: Operator,
  fieldValue: AttributeValue,
  condValue: AttributeValue,
): boolean

resolveConditionValue

Resolves $-prefixed variable references in condition values. If the value starts with $subject., $resource., or $environment., it is resolved against the request using the same dot-path logic as resolve(). Otherwise the value passes through unchanged. The engine calls this internally so conditions like { field: 'resource.attributes.ownerId', operator: 'eq', value: '$subject.id' } work.

import { resolveConditionValue } from '@gentleduck/iam'
 
const request = {
  subject: { id: 'user-1', roles: ['editor'], attributes: {} },
  action: 'update',
  resource: { type: 'post', attributes: { ownerId: 'user-1' } },
}
 
resolveConditionValue(request, '$subject.id')                  // 'user-1'
resolveConditionValue(request, '$resource.attributes.ownerId') // 'user-1'
resolveConditionValue(request, 'literal-string')               // 'literal-string'
resolveConditionValue(request, 42)                             // 42
import { resolveConditionValue } from '@gentleduck/iam'
 
const request = {
  subject: { id: 'user-1', roles: ['editor'], attributes: {} },
  action: 'update',
  resource: { type: 'post', attributes: { ownerId: 'user-1' } },
}
 
resolveConditionValue(request, '$subject.id')                  // 'user-1'
resolveConditionValue(request, '$resource.attributes.ownerId') // 'user-1'
resolveConditionValue(request, 'literal-string')               // 'literal-string'
resolveConditionValue(request, 42)                             // 42

Signature:

function resolveConditionValue(
  req: AccessRequest,
  value: AttributeValue,
): AttributeValue
function resolveConditionValue(
  req: AccessRequest,
  value: AttributeValue,
): AttributeValue