Skip to main content

methods

engine.can, check, authorize, permissions, explain, preload, healthCheck, stats, dispose - when to use each.

engine.can()

The simplest check. Returns true or false.

const allowed = await engine.can(
  'user-1', // subject ID
  'update', // action
  { type: 'post', attributes: {} }, // resource
)

if (!allowed) {
  throw new Error('Forbidden')
}

Signature:

engine.can(
  subjectId: string,
  action: TAction,
  resource: IamRequest.IResource<TResource>,
  environment?: IamRequest.IAccessRequest['environment'],
  scope?: TScope,
): Promise<boolean>

can() always returns boolean regardless of engine mode. It's the simple-API method - use it when you don't need the full decision metadata.


engine.check()

Same as can() but returns the full AccessControl.IDecision object instead of a boolean (in development mode):

const decision = await engine.check('user-1', 'delete', {
  type: 'post',
  id: 'post-123',
  attributes: { ownerId: 'user-2' },
})

if (!decision.allowed) {
  console.log('Denied:', decision.reason)
  // "Denied: No matching rules -> deny"
}

In production mode, check() returns a plain boolean - same as can(). The IDecision object is a development-mode feature.

Use check() when you want to log/audit the deciding policy and rule, not just the outcome.


engine.authorize()

Takes a complete IamRequest.IAccessRequest object. The low-level method that can() and check() call internally. Use it when you already have a resolved subject:

const subject = await engine.resolveSubject('user-1') // internal helper

const decision = await engine.authorize({
  subject,
  action: 'update',
  resource: { type: 'post', id: 'post-123', attributes: { ownerId: 'user-1' } },
  environment: { ip: '192.168.1.1' },
  scope: 'org-1',
})

If the request includes a scope, the engine enriches the subject with matching scoped roles before evaluation. For example, if the subject has scopedRoles: [{ role: 'admin', scope: 'org-1' }] and the request scope is 'org-1', 'admin' is added to the effective roles for this check.

If any error occurs during evaluation (adapter failure, hook exception, etc.), the engine catches it, calls the onError hook if defined, and returns a deny decision with the error message as the reason.

Caveat: if your onError hook itself throws, the surrounding authorize() / can() / check() call can reject.


engine.permissions()

Batch check multiple permissions at once. Loads data once, evaluates many. Returns a typed PermissionMap keyed by one of four forms:

  • "action:resource"
  • "action:resource:resourceId"
  • "scope:action:resource"
  • "scope:action:resource:resourceId"
const perms = await engine.permissions('user-1', [
  { action: 'read', resource: 'post' },
  { action: 'create', resource: 'post' },
  { action: 'delete', resource: 'post' },
  { action: 'manage', resource: 'user' },
])

// perms = {
//   'read:post': true,
//   'create:post': true,
//   'delete:post': false,
//   'manage:user': false,
// }

Each check in the array is a PermissionCheck:

interface PermissionCheck<
  TAction extends string = string,
  TResource extends string = string,
  TScope extends string = string,
> {
  readonly action: TAction
  readonly resource: TResource
  readonly resourceId?: string
  readonly scope?: TScope
}

This is the method to use when rendering a UI that needs many permissions at once (which buttons to show/hide). It's also what server middleware uses to generate PermissionMap objects for client hydration.

The returned map uses the same key format as the client libraries and the root-level iamBuildPermissionKey() helper. If you're writing custom hydration code, tests, or a framework integration outside the shipped clients, prefer that helper instead of hand-building key strings.

With scopes and resource IDs

const perms = await engine.permissions('user-1', [
  { action: 'update', resource: 'post', resourceId: 'post-123', scope: 'org-1' },
  { action: 'delete', resource: 'post', resourceId: 'post-123', scope: 'org-1' },
])

// perms = {
//   'org-1:update:post:post-123': true,
//   'org-1:delete:post:post-123': false,
// }

Performance note

permissions() resolves the subject and policies once, then evaluates each requested permission with its own scope and hooks. It's much faster than calling can() in a loop - single DB hit, single subject resolution, batch evaluation.

For large arrays (50+ checks), permissions() outperforms a loop by 10x+.

The return type is AccessControl.ModePermissionMap<TMode> - a boolean value per key in production mode, an AccessControl.IDecision per key in development mode.


engine.explain()

Returns a full evaluation trace. Development mode only - calling in production mode throws.

const result = await engine.explain('user-1', 'delete', {
  type: 'post',
  id: 'post-1',
  attributes: { ownerId: 'user-2' },
})

console.log(result.summary)
console.log(result.policies) // per-policy breakdowns
console.log(result.rules) // per-rule match details with actual vs expected values

explain() runs the same pipeline as check() but builds a richer trace object instead of an IDecision. Side-effect hooks (afterEvaluate, onDeny, onError) do not fire - it's read-only.

beforeEvaluate does run, since it can affect the evaluation (e.g. adding timestamp to the environment).

See explain and debug for the full trace API.


engine.preload()

Warms mergedPolicyCache so the first request after boot doesn't pay the full listPolicies + listRoles + rolesToPolicy + index-build cost. Call once at app startup.

// app startup
await engine.preload()
server.listen(PORT)

Bench shows ~15x speedup on the first call vs cold.


engine.healthCheck()

Liveness + readiness probe. One timed-out listPolicies round-trip plus a cache-hit-rate snapshot. Wire to /healthz. Returns an IamEngineTypes.IHealth:

interface IHealth {
  ok: boolean              // false means the orchestrator should pull this instance
  adapter: 'ok' | 'fail'   // adapter probe outcome
  cacheHitRate: number     // aggregate hit rate across caches; 0 when no traffic yet
  adapterLatencyMs: number // round-trip latency of the probe
  lastError?: string       // present when `adapter === 'fail'`
}
app.get('/healthz', async (_, res) => {
  const h = await engine.healthCheck()
  res.status(h.ok ? 200 : 503).json(h)
})

Cheap enough for 5-10 s probe intervals.


engine.cache

Cache-invalidation facet. Four targeted flushes for use after policy/role/subject mutations. Pass { broadcast: false } when applying an event received from another instance.

engine.cache.invalidate(opts?)                  // drop every cache + in-flight resolver
engine.cache.invalidateSubject(subjectId, opts?) // drop one subject's resolved roles + attrs
engine.cache.invalidatePolicies(opts?)           // drop cached policies (after policy CRUD)
engine.cache.invalidateRoles(roleId?, opts?)     // drop cached roles + RBAC policy; selectively drops affected subjects

With an invalidator wired, each call publishes an event so peer instances flush their local caches. Pass { broadcast: false } to suppress the publish when applying an inbound event (otherwise instances ping-pong).

Module-level iamFlushSharedCaches() (separate import) wipes the process-wide regex + path caches shared across every Engine. Schedule periodically in multi-tenant deployments.

import { iamFlushSharedCaches } from '@gentleduck/iam'
setInterval(iamFlushSharedCaches, 5 * 60 * 1000)

engine.stats.get() / engine.stats.reset()

Cache hit / miss counters per cache. Use for alerting on cache-effectiveness regressions in production.

const s = engine.stats.get()
// -> { policies, roles, rbacPolicy, mergedPolicies, subjects }
//     each { hits, misses, size }

engine.stats.reset() // zero the counters

Counters accumulate from construction; reset on a sampling interval if you want windowed metrics.


engine.dispose()

Releases the cross-instance invalidator subscription. Call on shutdown.

process.on('SIGTERM', () => engine.dispose())

No-op if no invalidator was wired. Safe to call multiple times.


When to use which

NeedUse
Simple yes/no checkcan()
Decision metadata (rule/policy/reason)check()
Pre-built IamRequest.IAccessRequestauthorize()
Batch checks for UI hydrationpermissions()
Debugging why a decision happenedexplain()
Warm cache at bootpreload()
/healthz probehealthCheck()
Cache effectiveness telemetrystats() / resetStats()
Shutdown cleanupdispose()