Skip to content

Changelog - 2026-02-16 (Updated 2026-02-25)

Authorization System & Auth Module Refactor

This release introduces a complete authorization system alongside a major restructuring of the authentication module. The authorization system supports Casbin integration and custom enforcers, following the same registry-based architecture as authentication.

NOTE

2026-02-25 Update: Major refactoring -- removed DefaultAuthorizationEnforcer and AbilityBuilder, moved enforcer options to co-locate with registry registration, added BaseFilteredAdapter and DrizzleCasbinAdapter, introduced CasbinRuleVariants constants, simplified IAuthorizeOptions, merged authorization flow from 8 steps to 7.

Overview

  • Authorization System: Enforcer-based authorization with voter pattern, rules caching, and role shortcuts
  • Casbin Enforcer: Integration with the casbin library for policy-engine-based authorization (optional peer dep)
  • Filtered Adapters: BaseFilteredAdapter (template method) + DrizzleCasbinAdapter (raw SQL via Drizzle) for loading user-scoped policies
  • Registry-based Registration: Enforcer class, name, type, and options co-located in AuthorizationEnforcerRegistry.register()
  • Auth Module Restructure: Consistent file organization across authenticate and authorize modules
  • Import Cleanup: Replaced barrel @/ imports with specific file paths to prevent circular dependencies
  • Helpers Rename: packages/helpers/src/helpers/ renamed to packages/helpers/src/modules/
  • Terminology Update: requirement renamed to spec, abilities renamed to rules throughout the authorization module

Breaking Changes

WARNING

This section contains changes that require migration or manual updates to existing code.

1. IAuthenticateRouteConfig Replaced by IAuthRouteConfig

The route config interface now supports both authentication and authorization:

Before:

typescript
import { IAuthenticateRouteConfig } from '@venizia/ignis';

interface MyRouteConfig extends IAuthenticateRouteConfig {
  // authenticate only
}

After:

typescript
import { IAuthRouteConfig } from '@venizia/ignis';

interface MyRouteConfig extends IAuthRouteConfig {
  authenticate?: { strategies?: TAuthStrategy[]; mode?: TAuthMode };
  authorize?: IAuthorizationSpec | IAuthorizationSpec[];
}

2. Helpers Source Directory Rename

Before:

packages/helpers/src/helpers/

After:

packages/helpers/src/modules/

Internal imports changed from @/helpers/... to @/modules/.... External imports via @venizia/ignis-helpers are unaffected.

3. Authorization Terminology: requirement to spec

Before:

typescript
authorize: { requirement: { action: 'read', resource: 'Article' } }

After:

typescript
import { AuthorizationActions } from '@venizia/ignis';

authorize: { action: AuthorizationActions.READ, resource: 'Article' }
// or
authorize({ spec: { action: AuthorizationActions.READ, resource: 'Article' } })

New Features

Authorization System

Files: packages/core/src/components/auth/authorize/

Problem: Ignis had authentication but no authorization. Developers needed to implement their own permission checking logic.

Solution: A pluggable enforcer-based authorization system that integrates seamlessly with the existing authentication middleware chain. Setup is a three-step process: bind global options, register the component, then register enforcers via the registry.

typescript
import {
  AuthorizeComponent,
  AuthorizeBindingKeys,
  AuthorizationEnforcerRegistry,
  AuthorizationEnforcerTypes,
  CasbinAuthorizationEnforcer,
  CasbinEnforcerModelDrivers,
  DrizzleCasbinAdapter,
  IAuthorizeOptions,
} from '@venizia/ignis';

// Step 1: Global options (simplified — no enforcer-specific config)
this.bind<IAuthorizeOptions>({ key: AuthorizeBindingKeys.OPTIONS }).toValue({
  defaultDecision: 'deny',
  alwaysAllowRoles: ['999_super-admin'],
});

// Step 2: Register component
this.component(AuthorizeComponent);

// Step 3: Register enforcer(s) with co-located options
AuthorizationEnforcerRegistry.getInstance().register({
  container: this,
  enforcers: [{
    enforcer: CasbinAuthorizationEnforcer,
    name: 'casbin',
    type: AuthorizationEnforcerTypes.CASBIN,
    options: {
      model: { driver: CasbinEnforcerModelDrivers.FILE, definition: './security/model.conf' },
      adapter: new DrizzleCasbinAdapter({ dataSource, entities: { ... } }),
      cached: { use: false },
    },
  }],
});

Benefits:

  • Role-based shortcuts (alwaysAllowRoles, allowedRoles)
  • Voter pattern for custom authorization logic
  • Rules caching per-request on Hono context
  • CRUD factory integration with per-route overrides
  • Casbin integration as optional peer dependency
  • Filtered adapter pattern for loading user-scoped policies
  • Registry-based enforcer registration with co-located options

Authorization Middleware Pipeline (7 Steps)

StepActionShort-circuits?
1Check SKIP_AUTHORIZATION flagYes
2Get authenticated userYes (401)
3Check role-based shortcuts (alwaysAllowRoles + allowedRoles)Yes
4Execute votersYes (DENY/ALLOW)
5Resolve enforcerNo
6Build/cache rulesNo
7Evaluate via enforcerYes (403)

CRUD Factory Authorization Support

typescript
import { AuthorizationActions } from '@venizia/ignis';

ControllerFactory.defineCrudController({
  entity: Article,
  repository: { name: 'ArticleRepository' },
  controller: { name: 'ArticleController', basePath: '/articles' },
  authenticate: { strategies: [Authentication.STRATEGY_JWT] },
  authorize: { action: AuthorizationActions.READ, resource: 'Article' },
  routes: {
    find: { authenticate: { skip: true } },           // Public
    deleteById: { authorize: { action: AuthorizationActions.DELETE, resource: 'Article' } }, // Override
    count: { authorize: { skip: true } },              // Skip authz only
  },
});

Shared AbstractAuthRegistry

File: packages/core/src/components/auth/base/abstract-auth-registry.ts

Both AuthenticationStrategyRegistry and AuthorizationEnforcerRegistry now extend a shared AbstractAuthRegistry<T> base class, eliminating duplicate registration/resolution logic.

AuthorizationRole Value Object

File: packages/core/src/components/auth/authorize/models/authorization-role.model.ts

Priority-based role comparison with identifier formatting:

typescript
import { AuthorizationRoles } from '@venizia/ignis';

AuthorizationRoles.SUPER_ADMIN.identifier;  // '999_super-admin'
AuthorizationRoles.ADMIN.isHigherThan({ target: AuthorizationRoles.USER }); // true

Refactoring

Auth Module Restructure

Both authenticate and authorize modules now follow a consistent internal structure:

auth/
├── authenticate/
│   ├── common/          # Constants, keys, types
│   ├── controllers/     # Auth controller factory
│   ├── middlewares/      # authenticate() middleware
│   ├── providers/       # AuthenticationProvider
│   ├── services/        # JWTTokenService, BasicTokenService
│   ├── strategies/      # JWT, Basic strategies + registry
│   └── index.ts
├── authorize/
│   ├── adapters/        # BaseFilteredAdapter, DrizzleCasbinAdapter
│   ├── common/          # Constants (CasbinRuleVariants, etc.), keys, types
│   ├── enforcers/       # CasbinAuthorizationEnforcer + registry
│   ├── middlewares/      # authorize() middleware
│   ├── models/          # AuthorizationRole
│   ├── providers/       # AuthorizationProvider
│   └── index.ts
├── base/                # AbstractAuthRegistry (shared)
└── models/              # Entity column helpers (User, Role, etc.)

Circular Import Prevention

All barrel @/ imports within the auth module and controller files were replaced with specific file path imports:

Before:

typescript
import { IAuthRouteConfig, TContext } from '@/base/controllers';
import { getError } from '@/helpers';

After:

typescript
import { IAuthRouteConfig, TContext } from '@/base/controllers/common/types';
import { getError } from '@venizia/ignis-helpers';

Pattern applied across 22+ files -- prevents circular dependency chains caused by barrel re-exports.

Singleton Convenience Functions

Moved standalone authenticate() and authorize() functions from provider modules to middleware modules for clearer separation:

authenticate/providers/ → authenticate/middlewares/authenticate.middleware.ts
authorize/providers/    → authorize/middlewares/authorize.middleware.ts

Files Changed

Core Package (packages/core) -- New Files

FileChanges
src/components/auth/authorize/common/constants.tsAuthorization, AuthorizationActions, AuthorizationDecisions, AuthorizationRoles, AuthorizationEnforcerTypes, CasbinEnforcerCachedDrivers, CasbinEnforcerModelDrivers, CasbinRuleVariants constants
src/components/auth/authorize/common/keys.tsAuthorizeBindingKeys (OPTIONS, ALWAYS_ALLOW_ROLES, enforcerOptions(name))
src/components/auth/authorize/common/types.tsIAuthorizationEnforcer, IAuthorizationSpec, IAuthorizeOptions, ICasbinEnforcerOptions, TAuthorizationVoter, TAuthorizeFn
src/components/auth/authorize/common/index.tsBarrel export for common
src/components/auth/authorize/adapters/base-filtered.tsBaseFilteredAdapter -- abstract template method pattern for casbin FilteredAdapter
src/components/auth/authorize/adapters/drizzle-casbin.tsDrizzleCasbinAdapter -- Drizzle-based read-only FilteredAdapter with raw SQL
src/components/auth/authorize/component.tsAuthorizeComponent -- validates options, binds alwaysAllowRoles
src/components/auth/authorize/enforcers/enforcer-registry.tsAuthorizationEnforcerRegistry singleton with type-discriminated register()
src/components/auth/authorize/enforcers/casbin.enforcer.tsCasbinAuthorizationEnforcer (optional peer dep, injects options from registry)
src/components/auth/authorize/middlewares/authorize.middleware.tsauthorize() standalone function
src/components/auth/authorize/providers/authorization.provider.tsAuthorizationProvider -- middleware factory
src/components/auth/authorize/models/authorization-role.model.tsAuthorizationRole value object
src/components/auth/base/abstract-auth-registry.tsAbstractAuthRegistry shared base class
src/__tests__/authorize/authorize.test.ts45 authorization tests

Core Package (packages/core) -- Modified Files

FileChanges
src/base/controllers/abstract.tsAdded authorize middleware injection in getRouteConfigs() and getJSXRouteConfigs()
src/base/controllers/common/types.tsIAuthRouteConfig -- added authorize field, TRouteAuthorizeConfig type
src/base/controllers/factory/definition.tsresolveRouteAuthorize() for CRUD factory authorization
src/base/controllers/factory/controller.tsauthorize support in defineControllerRouteConfigs()
src/components/auth/authenticate/strategies/strategy-registry.tsRefactored to extend AbstractAuthRegistry
src/components/auth/authenticate/index.tsUpdated barrel exports
src/components/auth/index.tsAdded authorize, base, and models exports
src/base/metadata/routes.tsUpdated IAuthRouteConfig references

Helpers Package (packages/helpers)

FileChanges
src/helpers/src/modules/Directory rename -- 59 internal import paths updated
src/index.tsUpdated barrel export path
package.jsonUpdated 7 sub-export paths in exports field

Tests

FileChanges
src/__tests__/authorize/authorize.test.ts45 tests covering: DefaultAuthorizationEnforcer, CasbinAuthorizationEnforcer, AuthorizationProvider middleware, AbilityBuilder, AuthorizationRole, voters, conditions, role shortcuts

Migration Guide

NOTE

Follow these steps if you're upgrading from a previous version.

Step 1: Update IAuthenticateRouteConfig References

If you used IAuthenticateRouteConfig in custom controllers, rename to IAuthRouteConfig:

typescript
// Before
import { IAuthenticateRouteConfig } from '@venizia/ignis';

// After
import { IAuthRouteConfig } from '@venizia/ignis';

Step 2: Add Authorization (Optional)

If you want authorization, follow the three-step setup:

typescript
import {
  AuthorizeBindingKeys,
  AuthorizeComponent,
  AuthorizationEnforcerRegistry,
  AuthorizationEnforcerTypes,
  CasbinAuthorizationEnforcer,
  CasbinEnforcerModelDrivers,
  IAuthorizeOptions,
} from '@venizia/ignis';

// Step 1: Bind global options (simplified — no enforcer-specific config)
this.bind<IAuthorizeOptions>({ key: AuthorizeBindingKeys.OPTIONS }).toValue({
  defaultDecision: 'deny',
  alwaysAllowRoles: ['999_super-admin'],
});

// Step 2: Register component
this.component(AuthorizeComponent);

// Step 3: Register enforcer(s) with co-located options
AuthorizationEnforcerRegistry.getInstance().register({
  container: this,
  enforcers: [{
    enforcer: CasbinAuthorizationEnforcer,
    name: 'casbin',
    type: AuthorizationEnforcerTypes.CASBIN,
    options: {
      model: { driver: CasbinEnforcerModelDrivers.FILE, definition: './security/model.conf' },
      cached: { use: false },
    },
  }],
});

Step 3: Add Authorization to Routes

Add authorize to route configs:

typescript
import { AuthorizationActions } from '@venizia/ignis';

this.defineRoute({
  configs: {
    path: '/',
    method: 'get',
    authenticate: { strategies: [Authentication.STRATEGY_JWT] },
    authorize: { action: AuthorizationActions.READ, resource: 'Article' },
    // ...
  },
  handler: async (context) => { ... },
});

Step 4: Update Internal Imports (If Extending Framework)

If you import from internal paths (not @venizia/ignis), update barrel imports to specific files:

typescript
// Before
import { TContext } from '@/base/controllers';

// After
import { TContext } from '@/base/controllers/common/types';