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') // falseimport { 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') // falseSignature:
function matchesAction(pattern: string, action: string): booleanfunction matchesAction(pattern: string, action: string): booleanmatchesResource
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 childrenimport { 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 childrenSignature:
function matchesResource(pattern: string, resourceType: string): booleanfunction matchesResource(pattern: string, resourceType: string): booleanmatchesResourceHierarchical
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 excludedimport { 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 excludedSignature:
function matchesResourceHierarchical(pattern: string, resourceType: string): booleanfunction matchesResourceHierarchical(pattern: string, resourceType: string): booleanmatchesScope
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 scopeimport { 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 scopeSignature:
function matchesScope(
pattern: string | undefined | null,
scope: string | undefined | null,
): booleanfunction matchesScope(
pattern: string | undefined | null,
scope: string | undefined | null,
): booleanField 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') // nullimport { 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') // nullSignature:
function resolve(request: AccessRequest, path: string): AttributeValuefunction resolve(request: AccessRequest, path: string): AttributeValueAttributeValue 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) // trueimport { 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) // trueSignature:
function evaluateOperator(
op: Operator,
fieldValue: AttributeValue,
condValue: AttributeValue,
): booleanfunction evaluateOperator(
op: Operator,
fieldValue: AttributeValue,
condValue: AttributeValue,
): booleanresolveConditionValue
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) // 42import { 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) // 42Signature:
function resolveConditionValue(
req: AccessRequest,
value: AttributeValue,
): AttributeValuefunction resolveConditionValue(
req: AccessRequest,
value: AttributeValue,
): AttributeValue