Skip to main content

createAccessConfig()

The factory — input shape, optional fields, how const assertions work, and what the return value gives you.

Factory signature

import { createAccessConfig } from '@gentleduck/iam'
 
const access = createAccessConfig({
  actions: ['create', 'read', 'update', 'delete', 'manage'] as const,
  resources: ['post', 'comment', 'user', 'dashboard'] as const,
  scopes: ['org-1', 'org-2'] as const,
  roles: ['viewer', 'editor', 'admin', 'super-admin'] as const,
})
import { createAccessConfig } from '@gentleduck/iam'
 
const access = createAccessConfig({
  actions: ['create', 'read', 'update', 'delete', 'manage'] as const,
  resources: ['post', 'comment', 'user', 'dashboard'] as const,
  scopes: ['org-1', 'org-2'] as const,
  roles: ['viewer', 'editor', 'admin', 'super-admin'] as const,
})

Input shape

interface AccessConfigInput<TActions, TResources, TScopes, TRoles, TContext> {
  actions: TActions // readonly string[] with const assertion
  resources: TResources // readonly string[] with const assertion
  scopes?: TScopes // optional, readonly string[] with const assertion
  roles?: TRoles // optional, readonly string[] with const assertion
  context?: TContext // optional, phantom field for typed dot-path intellisense
}
interface AccessConfigInput<TActions, TResources, TScopes, TRoles, TContext> {
  actions: TActions // readonly string[] with const assertion
  resources: TResources // readonly string[] with const assertion
  scopes?: TScopes // optional, readonly string[] with const assertion
  roles?: TRoles // optional, readonly string[] with const assertion
  context?: TContext // optional, phantom field for typed dot-path intellisense
}
FieldRequiredWhat it constrains
actionsyesgrant(action, ...), r.on(action), engine.can(_, action, ...)
resourcesyesgrant(_, resource), r.of(resource), resource targets
scopesnoforScope(scope), scoped grant, engine.can(... scope)
rolesnodefineRole(id), inherits(id), w.role(id), w.roles(...ids)
contextnoPhantom field — see typed context

If you omit scopes or roles, the config still works. Those parameters accept string without constraint. When roles is provided, defineRole(), when().role(), and all builders that reference roles are constrained to the declared role IDs.


How as const works

TypeScript's as const assertion is required for type safety. Without it, arrays are widened:

// Without as const — types are string[]
const actions = ['create', 'read', 'update']
// typeof actions = string[]
 
// With as const — types are literal tuples
const actions = ['create', 'read', 'update'] as const
// typeof actions = readonly ['create', 'read', 'update']
// Without as const — types are string[]
const actions = ['create', 'read', 'update']
// typeof actions = string[]
 
// With as const — types are literal tuples
const actions = ['create', 'read', 'update'] as const
// typeof actions = readonly ['create', 'read', 'update']

When you pass as const arrays to createAccessConfig(), the factory uses conditional types to extract the union of literal values:

type TAction = (typeof actions)[number]
// = 'create' | 'read' | 'update'
 
type TRole = (typeof roles)[number]
// = 'viewer' | 'editor' | 'admin'
type TAction = (typeof actions)[number]
// = 'create' | 'read' | 'update'
 
type TRole = (typeof roles)[number]
// = 'viewer' | 'editor' | 'admin'

These union types flow through all builders, constraining every parameter: actions, resources, scopes, and roles.


What you get back

createAccessConfig() returns an AccessConfig object with typed builder methods:

MethodPurpose
access.defineRole(id)Typed role builder
access.policy(id)Typed policy builder
access.defineRule(id)Typed standalone rule builder
access.when()Typed condition builder for reusable groups
access.createEngine(config)Typed engine instance
access.checks(arr)Typed permission check array (for engine.permissions())
access.validateRoles(roles)Runtime validation, same as standalone validateRoles()
access.validatePolicy(p)Runtime validation for untrusted policy objects

See methods reference for full details.


Inferring types from the config

Sometimes you need the union types elsewhere — e.g. defining types in a shared module. Extract them from the config:

const access = createAccessConfig({
  actions: ['create', 'read', 'update'] as const,
  resources: ['post'] as const,
})
 
type Action = Parameters<typeof access.defineRule>['0'] extends infer S
  ? S
  : never
// or via the input arrays directly:
type ActionFromInput = (typeof access)['_actions'][number]
const access = createAccessConfig({
  actions: ['create', 'read', 'update'] as const,
  resources: ['post'] as const,
})
 
type Action = Parameters<typeof access.defineRule>['0'] extends infer S
  ? S
  : never
// or via the input arrays directly:
type ActionFromInput = (typeof access)['_actions'][number]

In practice, define your unions next to the config and re-export:

// access.ts
export const access = createAccessConfig({
  actions: ['create', 'read', 'update', 'delete'] as const,
  resources: ['post', 'comment'] as const,
} as const)
 
export type AppAction = 'create' | 'read' | 'update' | 'delete'
export type AppResource = 'post' | 'comment'
// access.ts
export const access = createAccessConfig({
  actions: ['create', 'read', 'update', 'delete'] as const,
  resources: ['post', 'comment'] as const,
} as const)
 
export type AppAction = 'create' | 'read' | 'update' | 'delete'
export type AppResource = 'post' | 'comment'

This is one source of truth for both runtime and type-level uses.


Optional fields behavior

Without roles

const access = createAccessConfig({
  actions: ['read', 'write'] as const,
  resources: ['post'] as const,
  // no roles
})
 
access.defineRole('any-string-here') // OK — no constraint
access.when().role('any-string') // OK
const access = createAccessConfig({
  actions: ['read', 'write'] as const,
  resources: ['post'] as const,
  // no roles
})
 
access.defineRole('any-string-here') // OK — no constraint
access.when().role('any-string') // OK

Without a roles declaration, role-related parameters accept any string.

Without scopes

const access = createAccessConfig({
  actions: ['read'] as const,
  resources: ['post'] as const,
  // no scopes
})
 
access.defineRole('viewer').grant('read', 'post', 'any-scope') // OK — no constraint
const access = createAccessConfig({
  actions: ['read'] as const,
  resources: ['post'] as const,
  // no scopes
})
 
access.defineRole('viewer').grant('read', 'post', 'any-scope') // OK — no constraint

Same — without scopes, scope params accept any string.

Without context

The condition builder accepts any string for .attr(), .resourceAttr(), .env(), and .check() field paths, with AttributeValue for values. You lose dot-path autocomplete and value narrowing but the runtime behavior is identical.

See typed context for what context adds.