npm.io
5.1.0 • Published 6d ago

@gentleduck/iam

Licence
MIT
Version
5.1.0
Deps
0
Size
4.4 MB
Vulns
0
Weekly
439

@gentleduck/iam

@gentleduck/iam

Modern ABAC/RBAC access control engine. Framework-agnostic core with integrations for Express, NestJS, Hono, Next.js, React, and Vue.

MIT - Changelog - Security - Docs

npm downloads MIT


Type-safe authorization engine for TypeScript. RBAC + ABAC with a policy engine, condition evaluation, scoped roles, and integrations for Express, NestJS, Hono, Next.js, React, Vue, and vanilla JS.

Zero runtime dependencies. Tree-shakeable. 23 KB full, under 1 KB per module.

Install

npm install @gentleduck/iam
# or
bun add @gentleduck/iam

Quick start

import { createIam } from '@gentleduck/iam/core'
import { MemoryAdapter } from '@gentleduck/iam/adapters/memory'

const access = createIam({
  actions: ['create', 'read', 'update', 'delete'] as const,
  resources: ['post', 'comment', 'user'] as const,
  roles: ['viewer', 'editor', 'admin'] as const,
})

const viewer = access.defineRole('viewer').grant('read', 'post').grant('read', 'comment').build()
const editor = access.defineRole('editor').inherits('viewer').grant('update', 'post').build()
const admin = access.defineRole('admin').inherits('editor').grantCRUD('post').grantCRUD('comment').build()

const policy = access
  .policy('blog')
  .rule('owner-edit', (r) => r.allow().on('update').of('post').when((w) => w.isOwner()))
  .build()

const adapter = new MemoryAdapter({
  policies: [policy],
  roles: [viewer, editor, admin],
  assignments: { 'user-1': ['editor'] },
})

const engine = access.createEngine({ adapter })
const allowed = await engine.can('user-1', 'read', { type: 'post', attributes: {} })
// true

Performance

Benchmarked against 7 JS authorization libraries using vitest bench. Simple RBAC check, ops/sec (higher is better):

Library ops/sec vs CASL
@casl/ability 16,857,000 baseline
@gentleduck/iam [PROD] 8,233,000 2x slower
easy-rbac 5,003,000 3.4x slower
@rbac/rbac 2,884,000 5.8x slower
accesscontrol 674,000 25x slower
casbin 143,000 118x slower
role-acl 140,000 120x slower

CASL is faster on raw lookups because it pre-compiles rules into a hash table at build time. duck-iam supports dynamic policies that can change at runtime, which costs an extra Map lookup per check.

For the smallest bundle, import only what you use via subpaths:

// Engine-only (skip adapters, server middleware, client wrappers)
import { IamEngine, evaluatePolicyFast } from '@gentleduck/iam/core'

// Each adapter, server adapter, and client wrapper is a separate entry
import { MemoryAdapter } from '@gentleduck/iam/adapters/memory'
import { adminRouter } from '@gentleduck/iam/server/express'
import { useAccess } from '@gentleduck/iam/client/react'

// Validator (12 KB) - lazy-loaded by engine.admin.savePolicy on first
// call, or imported directly for standalone validation tooling
import { validatePolicy } from '@gentleduck/iam/core/validate'

// Fluent builder (9 KB) - config-time only, separate subpath
import { policy, defineRole } from '@gentleduck/iam/core/builder'

import * from '@gentleduck/iam' pulls the everything-barrel (~41 KB gzipped). Real deployments using subpath imports + tree-shaking come in at 15-25 KB.

Features

  • RBAC + ABAC combined in one engine
  • Policy engine with 4 intra-policy algorithms (deny-overrides, allow-overrides, first-match, highest-priority) and 3 cross-policy combine modes (and / allow-overrides / first-applicable)
  • 18 condition operators (eq, neq, gt, lt, in, contains, starts_with, matches, exists, subset_of, and more)
  • Scoped roles for multi-tenant systems
  • Dev/prod mode: rich Decision objects in development, plain booleans in production
  • Explain API: full evaluation trace showing exactly why a permission was granted or denied
  • Lifecycle hooks: beforeEvaluate, afterEvaluate, onDeny, onError, onPolicyError, onMetrics
  • Type-safe config: actions, resources, roles, and scopes are validated at compile time
SRE primitives
  • engine.preload() - warm cache at boot
  • engine.healthCheck() - /healthz-ready probe with adapter latency + cache hit rate
  • engine.stats.get() / engine.stats.reset() - cache hit / miss counters per cache
  • engine.cache.invalidate() / invalidatePolicies() / invalidateRoles(id?) / invalidateSubject(id) - targeted cache flushes
  • engine.admin.export() / import(snapshot, { mode }) - schema-versioned policy + role snapshots for env promotion
  • engine.dispose() - release the cross-instance invalidator subscription on shutdown
  • IConfig.adapterTimeoutMs - AbortController-driven timeout on every adapter read (default 5 s)
  • IConfig.maxPolicies / maxRoles - load-time caps that fail closed
  • IConfig.allowFailOpen - explicit opt-in required to combine mode: 'production' with defaultEffect: 'allow'
  • IConfig.invalidator - cross-instance cache-invalidation broadcaster
  • createRedisInvalidator at @gentleduck/iam/invalidators/redis - pub/sub helper with self-echo filter
  • createMetricsAggregator at @gentleduck/iam/observability/metrics - p50 / p95 / p99 over onMetrics events
  • HttpAdapter retry + per-request timeout + circuit breaker (retries, backoff, threshold, cooldown)
  • Required authorize callback on every admin router (Express, Hono, Next, Nest)

Integrations

Server middleware
// Express
import { guard, adminRouter } from '@gentleduck/iam/server/express'
app.delete('/posts/:id', guard(engine, 'delete', 'post'), handler)
app.use('/admin', adminRouter(engine, { authorize: (req) => isAdmin(req) })(() => express.Router()))

// Hono
import { guard, bindAdminRouter } from '@gentleduck/iam/server/hono'
app.delete('/posts/:id', guard(engine, 'delete', 'post'), handler)
bindAdminRouter(adminApp, engine, { authorize: (c) => isAdmin(c) })

// NestJS
import { nestAccessGuard, Authorize, createAdminOperations } from '@gentleduck/iam/server/nest'
@Authorize({ action: 'delete', resource: 'post' })

// Next.js
import { withAccess, createAdminHandlers } from '@gentleduck/iam/server/next'
export const DELETE = withAccess(engine, 'delete', 'post', handler)
Client libraries
// React
import { createAccessControl } from '@gentleduck/iam/client/react'
const { AccessProvider, useAccess, Can, Cannot } = createAccessControl(React)

// Vue
import { createVueAccess } from '@gentleduck/iam/client/vue'
const { useAccess, Can, Cannot } = createVueAccess(vue)

// Vanilla JS
import { AccessClient } from '@gentleduck/iam/client/vanilla'
const client = await AccessClient.fromServer('/api/permissions')
client.can('read', 'post') // boolean
Database adapters
import { MemoryAdapter } from '@gentleduck/iam/adapters/memory'
import { FileAdapter } from '@gentleduck/iam/adapters/file'
import { PrismaAdapter } from '@gentleduck/iam/adapters/prisma'
import { DrizzleAdapter } from '@gentleduck/iam/adapters/drizzle'
import { RedisAdapter } from '@gentleduck/iam/adapters/redis'
import { HttpAdapter } from '@gentleduck/iam/adapters/http'
Operability
import { createRedisInvalidator } from '@gentleduck/iam/invalidators/redis'
import { createMetricsAggregator } from '@gentleduck/iam/observability/metrics'

const metrics = createMetricsAggregator()
const engine = new IamEngine({
  adapter,
  invalidator: createRedisInvalidator({ client: redis }),
  hooks: { onMetrics: metrics.record },
})

await engine.preload()
app.get('/healthz', async (_, res) => res.json(await engine.healthCheck()))
app.get('/metrics', (_, res) => res.json(metrics.snapshot()))

See the production deployment guide for cache TTL trade-offs, multi-node invalidation patterns, fail-closed defaults, and SLO targets.

Module sizes (gzipped)

Module Size
Core engine (typical import) ~15 KB
core/validate (admin only, lazy-loaded) 12 KB
core/builder (config-time only) 9 KB
core/explain (dev-mode trace) separate chunk
Each adapter 1.7 - 6 KB
Each server middleware 2.4 - 3.7 KB
Each client library 1.2 - 2.0 KB

The "full" bundle headline in benchmarks (~41 KB) is the worst-case "import everything" number - what import * from '@gentleduck/iam' would pull. Realistic deployments end up at 15-25 KB because adapters, server middleware, and clients live behind subpath imports and the validator is lazy-loaded only when admin write paths run. See the benchmarks page for per-profile measurements.

Docs

Contributing

PR checklist + style notes in the repo's CONTRIBUTING.md. Security disclosures: SECURITY.md.

License

MIT. See LICENSE.

Keywords