Skip to main content

types & namespaces

I-prefixed interfaces under per-module namespaces - the type-import contract for duck-iam consumers.

Convention

duck-iam ships TypeScript types under per-module namespaces. Two rules:

  1. Interfaces use the I prefix - IPolicy, IRule, IDecision, IAccessRequest, etc.
  2. Type aliases stay bare - Effect, Operator, Mode, PolicyCombine, CombiningAlgorithm.

Every type lives inside one namespace export per module. Consumers import the namespace once and access members through it:

import type { AccessControl, IamRequest } from '@gentleduck/iam'

function check(policy: AccessControl.IPolicy, req: IamRequest.IAccessRequest): AccessControl.IDecision {
  // ...
}

The pattern matches the rest of the duck-* monorepo (calendar, upload, etc.). Type aliases stay bare because adding I to Effect reads awkwardly and conveys no information.


Namespace map

Core namespaces

Exported from @gentleduck/iam:

NamespaceMembers
AccessControlIPolicy, IRule, IPermission, IRole, IDecision, ICondition, IConditionGroup, Effect, Operator, CombiningAlgorithm, PolicyCombine, Mode, ModeResult, ModePermissionMap, OpFn
RequestIAccessRequest, ISubject, IResource, IEnvironment, IScopedRole
AdapterIAdapter, IPolicyStore, IRoleStore, ISubjectStore, IReadOptions
PrimitivesAttributes, AttributeValue, Scalar
ClientPermissionKey, PermissionMap, IPermissionCheck
DotPathDotPaths, FlexibleDotPaths, DollarPaths, FlexibleDollarPaths, PathValue, FieldValue, ConditionValue, AttrValue, AttrValueAt, SubjectAttrs, SubjectAttrShape, ResourceAttrs, ResourceAttrShape, EnvAttrs, EnvAttrShape, ResourceAttrMap, ResolvedResourceAttrs, ResolvedResourceAttrPaths, IDefaultContext, IAnyAttributes
IamEngineTypesIConfig, IHooks, IAdmin, IMetricsEvent, IInvalidator, IInvalidateEvent, ISnapshot, IImportOptions, IImportResult, IHealth
EvaluateCombiner, IIndexedRule, IPolicyRuleIndex
ExplainIResult, ISubjectInfo, IPolicyTrace, IRuleTrace, ILeafTrace, IGroupTrace, Trace
ValidateIIssue, IResult, ValidationCode
ConfigIAccessConfig, IAccessConfigInput, InferAction, InferResource, InferScope, InferRole

Module-local namespaces

Each integration module exports its own option-bag namespace (added in 2.0):

NamespaceSourceMembers
Memory@gentleduck/iam/adapters/memoryIInit
File@gentleduck/iam/adapters/fileIInit, IFS, IState
Http@gentleduck/iam/adapters/httpIConfig (baseUrl, fetch, headers, retry, timeout, circuit-breaker)
Redis@gentleduck/iam/adapters/redisIConfig, ILike (minimal client surface)
Drizzle@gentleduck/iam/adapters/drizzleIConfig (db, tables, ops)
Express@gentleduck/iam/server/expressIOptions, IAdminRouterOptions, IAdminAuthorize
Hono@gentleduck/iam/server/honoIOptions, IAdminOptions, IAdminAuthorize, IRouterLike
Nest@gentleduck/iam/server/nestIAuthorizeMeta, IGuardOptions, IAdminOptions, IAdminAuthorize
Next@gentleduck/iam/server/nextIWithAccessOptions, IMiddlewareOptions, IAdminOptions, IAdminAuthorize
ReactClient@gentleduck/iam/client/reactIContextValue
RedisInvalidator@gentleduck/iam/invalidators/redisIConfig, IPubSubLike
Metrics@gentleduck/iam/observability/metricsIAggregator, ISnapshot, IConfig

The class IamEngine lives next to the IamEngineTypes namespace - same name avoided by suffixing the type-only namespace. IamMemoryAdapter / IamFileAdapter / etc. are classes; their config namespaces (Memory, File, Http, Redis, Drizzle) hold the option bags. ReactClient is named so to avoid clashing with the React package namespace at the import site.


Why namespaces

Three reasons.

1. Avoid name collisions across modules. IResult exists in both Validate and Explain. Without namespaces, one of them needs a longer name. With namespaces, both stay short and the context is explicit at the call site (IamValidate.IResult vs Explain.IResult).

2. One import per module. Instead of:

import type {
  IPolicy, IRule, IDecision, ICondition, IConditionGroup,
  Effect, CombiningAlgorithm, PolicyCombine,
} from '@gentleduck/iam'

You write:

import type { AccessControl } from '@gentleduck/iam'

...and access everything through AccessControl.X. Reading the call site you know which module each type belongs to.

3. Tree-shake friendliness. import type { AccessControl } is a single TypeScript type import - the bundler doesn't trace 14 distinct re-exports, and sideEffects: false in package.json lets it drop unused namespaces entirely.


Migrating from flat imports

If you have older code using the flat names:

// Before
import type { Policy, Rule, Decision } from '@gentleduck/iam'

function check(p: Policy): Decision { /* ... */ }

Two mechanical changes:

// After
import type { AccessControl } from '@gentleduck/iam'

function check(p: AccessControl.IPolicy): AccessControl.IDecision { /* ... */ }
  1. Drop the flat names from the import; pull in the namespace instead.
  2. Prefix references: Policy -> AccessControl.IPolicy, Rule -> AccessControl.IRule, etc.

The runtime values (IamEngine, IamMemoryAdapter, policy, defineRole, builders, etc.) are unchanged - only type imports moved.


See also

  • Engine - IamEngineTypes.IConfig and IamEngineTypes.IHooks shapes.
  • Adapters - IamAdapter.IAdapter contract.
  • Conditions - AccessControl.ICondition and AccessControl.IConditionGroup.